Summary
Add an offline proposal-validation step that empirically scores each proposal by replaying it against the conversation turn(s) it came from, and use that grounded score instead of (or alongside) the model's self-assigned confidence.
Today the analysis pipeline is generate → materialise → emit: session-overview synthesises proposals, each carries a model-assigned confidence, and that number is never tested. The proposal is shipped on the model's say-so. This issue proposes closing the loop with a generate → validate → keep/rank step that is feasible entirely offline using data we already store.
Motivation / evidence
Dogfooding finding (10-session corpus). The single most-wrong proposal in the run carried the highest confidence (95%) — it told the agent to "verify the git push target," when the push was always correct and the real defect was an unrelated gh pr create invocation missing --repo. Meanwhile, genuine single-shot corrections (e.g. "this repo does not use CircleCI") scored below the friction threshold and produced no proposal at all. In short: self-assigned confidence was anti-correlated with correctness.
Literature convergence. Every serious method in the automatic prompt/instruction-optimization line — APE, ProTeGi, OPRO, DSPy/MIPRO, TextGrad, and GEPA (arXiv:2507.19457) — shares the loop generate → validate against data → keep only if it measurably helps. pi-prospector is the outlier: it generates and emits with no validation. Separately, the experiential-agent and evaluation literature warns explicitly that self-rating is untrustworthy:
- Huang et al., "Large Language Models Cannot Self-Correct Reasoning Yet" (arXiv:2310.01798): self-generated judgements need external feedback.
- LLM-judge work (e.g. JudgeBench) documents verbosity/sycophancy bias and that self-verification has high recall but low precision at confirming correctness.
- ExpeL (arXiv:2308.10144) extracts insights by contrasting trajectories and filters the insight pool rather than trusting every extraction.
- Constitutional AI (arXiv:2212.08073) compiles each principle into a concrete critique template — exactly the mechanism this issue reuses.
Proposed change (clearly defined)
Introduce a new analyzer, proposal-validate, that runs after session-overview and consumes its summary nodes. For each materialised proposal it performs a replay test:
- Resolve the proposal's originating turn(s). Follow the evidence trail:
proposal ←produces– summary node –consumes→ classification node –anchors→ message. (See "Required prerequisite" below — proposals must pin the message id(s) they are about.)
- Re-run the existing
turn-pair-llm classifier on each originating turn, but with the candidate proposal text injected into the system context as a standing instruction the agent "already had." Use a different model than the one that generated the proposal (to avoid same-model self-agreement bias).
- Compare classifications (with-rule vs. without-rule):
- friction averted (
friction_type: none / is_genuine_correction: false / sentiment improved) → supported;
- no change or worse → unsupported.
- Emit a
validation node (new node kind) that consumes the source node and stores the comparison result and a derived validated_score ∈ [0,1].
- Write the grounded result back onto the proposal: set
validated_score, a validation_status (supported | unsupported | unvalidated), and the id of the validating validation node. Ranking and the proposals view use validated_score first, falling back to model confidence only when unvalidated.
Required prerequisite (sub-task)
Proposals are materialised from a session-level summary node that consumes many turns, so today a single proposal is not pinned to the specific turn(s) it concerns. Before validation can target a turn, session-overview must attach source_message_ids (the user-message ids of the high-signal turns that motivated each proposal) to every entry in its improvement_proposals array, and proposal-materializer must persist them (new proposals column or via anchors edges from the proposal). This is small and self-contained but is a hard dependency for replay.
Data-model changes
- New node kind
validation added to the NodeKind TypeBox union in src/analyze/types.ts and to the analysis_nodes.node_kind documentation. Its content is a Type.Object({...}) schema (per AGENTS.md — no bare interfaces): { proposal_input_key, with_rule, without_rule, supported, validated_score, model_used }.
proposals table gains validated_score REAL, validation_status TEXT NOT NULL DEFAULT 'unvalidated', validation_node_id TEXT. Clean v2 schema edit in src/db/schema.ts (no migration needed — recompute regenerates derived data).
- Proposal schema (
improvement_proposals entries) gains source_message_ids: string[]; RawProposal/normalizeProposal in proposal-materializer.ts updated to carry it.
- New SQL lives only in
db/queries.ts / db/analysis-queries.ts.
Identity / architecture fit
The validation node is content-addressed like every other node: its input_key derives from (analyzer = proposal-validate, version, config fingerprint incl. validator model, source set = the source node's output_key), and its output_key = H(input_key | content). This means:
- replays are idempotent and reproducible on any machine after a wipe, and
prospect verify covers validation nodes for free,
- the result is auditable: "this proposal was kept because injecting it averted friction on message
<id>," traceable by edges back to the transcript (preserves the Invariant in DESIGN.md §4 that every proposal traces to conversation evidence).
It also stays within the non-goal of no automatic editing (DESIGN.md §5): validation produces a grounded confidence, never an edit. The validator runs as the deterministic→LLM layering already in place (turn-pair-llm is reused).
CLI / UX
- New action/command
prospect validate (and --prospect validate headless) that runs the proposal-validate analyzer over open proposals.
prospect proposals ranks by validated_score (desc, nulls last) and labels each line replay-validated / model-rated so the human can tell the two apart.
prospect proposals --full shows the with-rule vs. without-rule classification delta as part of the evidence block.
Caveats (call out in DESIGN.md)
The validator inherits the classifier's own blind spots — most notably that the classifier currently sees only message text, not tool calls/outputs (tracked separately). So:
- use a different model for validation than for generation;
- label the score "replay-validated", not "ground truth";
- keep it advisory and human-in-the-loop.
Even with these limits, an empirically-grounded score is strictly better than the self-rating that demonstrably failed on the corpus.
Acceptance criteria
Out of scope
- Feeding tool-call arguments/outputs into the classifier (tracked separately; would improve both generation and validation but is independent).
- Cross-session proposal consolidation / dedup, contradiction detection, and rule retirement (separate "memory lifecycle" work).
- Any automatic editing of steering artifacts.
References
Citations below were independently verified; the broader prompt-optimization line (APE, ProTeGi, OPRO, DSPy/MIPRO, TextGrad) is named without per-paper ids to avoid propagating unverified identifiers.
- GEPA — Reflective Prompt Evolution — arXiv:2507.19457
- ExpeL — LLM Agents Are Experiential Learners — arXiv:2308.10144
- Constitutional AI — Harmlessness from AI Feedback (critique templates) — arXiv:2212.08073
- Huang et al. — Large Language Models Cannot Self-Correct Reasoning Yet — arXiv:2310.01798
- DSPy — Compiling Declarative LM Calls into Self-Improving Pipelines — arXiv:2310.03714
- JudgeBench — LLM-judge bias benchmark (named; id not independently verified)
Migrated from v2nic#6.
Summary
Add an offline proposal-validation step that empirically scores each proposal by replaying it against the conversation turn(s) it came from, and use that grounded score instead of (or alongside) the model's self-assigned
confidence.Today the analysis pipeline is generate → materialise → emit:
session-overviewsynthesises proposals, each carries a model-assignedconfidence, and that number is never tested. The proposal is shipped on the model's say-so. This issue proposes closing the loop with a generate → validate → keep/rank step that is feasible entirely offline using data we already store.Motivation / evidence
Dogfooding finding (10-session corpus). The single most-wrong proposal in the run carried the highest confidence (95%) — it told the agent to "verify the git push target," when the push was always correct and the real defect was an unrelated
gh pr createinvocation missing--repo. Meanwhile, genuine single-shot corrections (e.g. "this repo does not use CircleCI") scored below the friction threshold and produced no proposal at all. In short: self-assigned confidence was anti-correlated with correctness.Literature convergence. Every serious method in the automatic prompt/instruction-optimization line — APE, ProTeGi, OPRO, DSPy/MIPRO, TextGrad, and GEPA (arXiv:2507.19457) — shares the loop generate → validate against data → keep only if it measurably helps. pi-prospector is the outlier: it generates and emits with no validation. Separately, the experiential-agent and evaluation literature warns explicitly that self-rating is untrustworthy:
Proposed change (clearly defined)
Introduce a new analyzer,
proposal-validate, that runs aftersession-overviewand consumes itssummarynodes. For each materialised proposal it performs a replay test:proposal ←produces– summary node –consumes→ classification node –anchors→ message. (See "Required prerequisite" below — proposals must pin the message id(s) they are about.)turn-pair-llmclassifier on each originating turn, but with the candidate proposal text injected into the system context as a standing instruction the agent "already had." Use a different model than the one that generated the proposal (to avoid same-model self-agreement bias).friction_type: none/is_genuine_correction: false/sentimentimproved) → supported;validationnode (new node kind) thatconsumesthe source node and stores the comparison result and a derivedvalidated_score ∈ [0,1].validated_score, avalidation_status(supported|unsupported|unvalidated), and the id of the validatingvalidationnode. Ranking and theproposalsview usevalidated_scorefirst, falling back to modelconfidenceonly whenunvalidated.Required prerequisite (sub-task)
Proposals are materialised from a session-level
summarynode that consumes many turns, so today a single proposal is not pinned to the specific turn(s) it concerns. Before validation can target a turn,session-overviewmust attachsource_message_ids(the user-message ids of the high-signal turns that motivated each proposal) to every entry in itsimprovement_proposalsarray, andproposal-materializermust persist them (newproposalscolumn or viaanchorsedges from the proposal). This is small and self-contained but is a hard dependency for replay.Data-model changes
validationadded to theNodeKindTypeBox union insrc/analyze/types.tsand to theanalysis_nodes.node_kinddocumentation. Its content is aType.Object({...})schema (per AGENTS.md — no bare interfaces):{ proposal_input_key, with_rule, without_rule, supported, validated_score, model_used }.proposalstable gainsvalidated_score REAL,validation_status TEXT NOT NULL DEFAULT 'unvalidated',validation_node_id TEXT. Clean v2 schema edit insrc/db/schema.ts(no migration needed — recompute regenerates derived data).improvement_proposalsentries) gainssource_message_ids: string[];RawProposal/normalizeProposalinproposal-materializer.tsupdated to carry it.db/queries.ts/db/analysis-queries.ts.Identity / architecture fit
The
validationnode is content-addressed like every other node: itsinput_keyderives from(analyzer = proposal-validate, version, config fingerprint incl. validator model, source set = the source node's output_key), and itsoutput_key = H(input_key | content). This means:prospect verifycovers validation nodes for free,<id>," traceable by edges back to the transcript (preserves the Invariant in DESIGN.md §4 that every proposal traces to conversation evidence).It also stays within the non-goal of no automatic editing (DESIGN.md §5): validation produces a grounded confidence, never an edit. The validator runs as the deterministic→LLM layering already in place (
turn-pair-llmis reused).CLI / UX
prospect validate(and--prospect validateheadless) that runs theproposal-validateanalyzer over open proposals.prospect proposalsranks byvalidated_score(desc, nulls last) and labels each linereplay-validated/model-ratedso the human can tell the two apart.prospect proposals --fullshows the with-rule vs. without-rule classification delta as part of the evidence block.Caveats (call out in DESIGN.md)
The validator inherits the classifier's own blind spots — most notably that the classifier currently sees only message text, not tool calls/outputs (tracked separately). So:
Even with these limits, an empirically-grounded score is strictly better than the self-rating that demonstrably failed on the corpus.
Acceptance criteria
NodeKindunion and schema includevalidation;prospect verifyvalidates these nodes.session-overviewattachessource_message_idsto each proposal; materializer persists them.proposal-validateanalyzer: resolves originating turn(s), re-runsturn-pair-llmwith/without the candidate rule via a configurable (distinct) validator model, emits avalidationnode, and writesvalidated_score/validation_status/validation_node_idonto the proposal.proposalsranking and--fullview use and display the validated score and areplay-validatedvsmodel-ratedlabel.input_key/output_key) and reproducible across a DB wipe.unsupportedand a well-supported onesupported, end to end.Out of scope
References
Citations below were independently verified; the broader prompt-optimization line (APE, ProTeGi, OPRO, DSPy/MIPRO, TextGrad) is named without per-paper ids to avoid propagating unverified identifiers.
Migrated from v2nic#6.