@navels/neal 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/README.md +206 -209
  2. package/SECURITY.md +16 -19
  3. package/dist/neal/adjudicator/{blocked-adjudicator.js → consultant.js} +21 -21
  4. package/dist/neal/adjudicator/execute.js +33 -33
  5. package/dist/neal/adjudicator/final-completion.js +2 -31
  6. package/dist/neal/adjudicator/planning.js +230 -38
  7. package/dist/neal/agents/prompts.js +6 -17
  8. package/dist/neal/agents/rounds.js +43 -73
  9. package/dist/neal/agents/schemas.js +47 -13
  10. package/dist/neal/blocked-guidance.js +16 -9
  11. package/dist/neal/cli.js +2 -2
  12. package/dist/neal/commands/check.js +2 -2
  13. package/dist/neal/commands/compat.js +100 -36
  14. package/dist/neal/commands/interactive-activity.js +2 -2
  15. package/dist/neal/commands/recovery-guidance.js +20 -8
  16. package/dist/neal/commands/runtime.js +4 -2
  17. package/dist/neal/config.js +16 -7
  18. package/dist/neal/context/inline-review-context.js +13 -28
  19. package/dist/neal/context/reviewer-context.js +19 -14
  20. package/dist/neal/eval/reviewer-eval-manifest.js +86 -0
  21. package/dist/neal/eval/reviewer-eval.js +155 -0
  22. package/dist/neal/interactive-controls.js +5 -0
  23. package/dist/neal/orchestrator/completion.js +49 -2
  24. package/dist/neal/orchestrator/failures.js +7 -0
  25. package/dist/neal/orchestrator/notifications.js +9 -9
  26. package/dist/neal/orchestrator/phases/planning.js +117 -80
  27. package/dist/neal/orchestrator/phases/recovery.js +151 -115
  28. package/dist/neal/orchestrator/phases/review.js +28 -1
  29. package/dist/neal/orchestrator/transitions.js +7 -1
  30. package/dist/neal/orchestrator.js +3 -0
  31. package/dist/neal/plan-queue.js +29 -1
  32. package/dist/neal/prompts/execute.js +7 -19
  33. package/dist/neal/prompts/planning.js +64 -55
  34. package/dist/neal/prompts/review-doctrine.js +13 -50
  35. package/dist/neal/prompts/specialized.js +7 -17
  36. package/dist/neal/prompts/specs.js +180 -32
  37. package/dist/neal/providers/agent-settings-isolation.js +35 -0
  38. package/dist/neal/providers/anthropic-claude.js +18 -1
  39. package/dist/neal/providers/detection.js +3 -7
  40. package/dist/neal/providers/git-config-isolation.js +83 -0
  41. package/dist/neal/providers/openai-codex.js +25 -3
  42. package/dist/neal/providers/{generic-agentic-tools.js → openai-compatible-tools.js} +11 -5
  43. package/dist/neal/providers/openai-compatible.js +1078 -430
  44. package/dist/neal/providers/openrouter-routing.js +68 -0
  45. package/dist/neal/providers/pricing.js +1 -1
  46. package/dist/neal/providers/rate-card.js +6 -32
  47. package/dist/neal/providers/registry.js +6 -17
  48. package/dist/neal/providers/types.js +12 -0
  49. package/dist/neal/resume-decision.js +7 -6
  50. package/dist/neal/resume-planner.js +9 -0
  51. package/dist/neal/retrospective.js +3 -3
  52. package/dist/neal/review-debt.js +30 -0
  53. package/dist/neal/review-findings/provider.js +17 -29
  54. package/dist/neal/review.js +17 -1
  55. package/dist/neal/state-invariants.js +11 -3
  56. package/dist/neal/state-views.js +27 -5
  57. package/dist/neal/state.js +59 -15
  58. package/dist/neal/status.js +28 -1
  59. package/dist/neal/support.js +3 -3
  60. package/docs/README.md +22 -18
  61. package/docs/{ADJUDICATOR_INVENTORY.md → adjudicator-inventory.md} +58 -57
  62. package/docs/architecture.md +79 -83
  63. package/docs/automation.md +11 -11
  64. package/docs/compat.md +91 -61
  65. package/docs/compatible-models.md +145 -121
  66. package/docs/demo.md +15 -5
  67. package/docs/maintenance.md +65 -34
  68. package/docs/plan-format.md +61 -33
  69. package/docs/prompt-evals.md +96 -0
  70. package/docs/{PROMPT_SPECS.md → prompt-specs.md} +52 -48
  71. package/docs/providers.md +279 -349
  72. package/docs/release.md +63 -81
  73. package/docs/review-convergence.md +246 -0
  74. package/docs/state-machine.md +61 -59
  75. package/docs/storage.md +40 -31
  76. package/docs/troubleshooting.md +32 -29
  77. package/examples/compat/add-edit-verify/PLAN.md +2 -0
  78. package/examples/compat/add-edit-verify/broken.diff +2 -2
  79. package/examples/compat/add-edit-verify/good.diff +2 -2
  80. package/examples/compat/add-edit-verify/src/add.js +1 -1
  81. package/examples/compat/manifest.json +25 -29
  82. package/examples/compat/sum-grep-edit/PLAN.md +33 -0
  83. package/examples/compat/sum-grep-edit/broken.diff +12 -0
  84. package/examples/compat/sum-grep-edit/good.diff +12 -0
  85. package/examples/compat/sum-grep-edit/package.json +5 -0
  86. package/examples/compat/sum-grep-edit/src/numbers.js +9 -0
  87. package/examples/compat/sum-grep-edit/test/numbers.test.js +15 -0
  88. package/examples/issue-triage-js/README.md +0 -15
  89. package/examples/reviewer-eval/clean-doc/base/src/clamp.ts +3 -0
  90. package/examples/reviewer-eval/clean-doc/change.diff +9 -0
  91. package/examples/reviewer-eval/clean-extract/base/src/greet.ts +3 -0
  92. package/examples/reviewer-eval/clean-extract/change.diff +13 -0
  93. package/examples/reviewer-eval/dropped-error/base/src/load.ts +4 -0
  94. package/examples/reviewer-eval/dropped-error/change.diff +15 -0
  95. package/examples/reviewer-eval/manifest.json +45 -0
  96. package/examples/reviewer-eval/off-by-one/base/src/paginate.ts +5 -0
  97. package/examples/reviewer-eval/off-by-one/change.diff +11 -0
  98. package/{config.yml → neal.yml} +1 -1
  99. package/package.json +15 -14
  100. package/dist/neal/providers/generic-agentic.js +0 -1256
  101. package/docs/comparison.md +0 -105
  102. package/docs/issue-pipeline.md +0 -124
  103. package/examples/compat/is-even-add-test/PLAN.md +0 -30
  104. package/examples/compat/is-even-add-test/broken.diff +0 -11
  105. package/examples/compat/is-even-add-test/good.diff +0 -11
  106. package/examples/compat/is-even-add-test/package.json +0 -5
  107. package/examples/compat/is-even-add-test/src/is-even.js +0 -3
  108. package/examples/compat/is-even-add-test/test/is-even.test.js +0 -9
  109. package/examples/compat/reverse-grep-edit/PLAN.md +0 -32
  110. package/examples/compat/reverse-grep-edit/broken.diff +0 -12
  111. package/examples/compat/reverse-grep-edit/good.diff +0 -12
  112. package/examples/compat/reverse-grep-edit/package.json +0 -5
  113. package/examples/compat/reverse-grep-edit/src/strings.js +0 -10
  114. package/examples/compat/reverse-grep-edit/test/strings.test.js +0 -15
package/docs/release.md CHANGED
@@ -1,13 +1,12 @@
1
- # Release Process
1
+ # Release process
2
2
 
3
- This is the canonical tracked release guide for Neal. Releases are manual,
4
- versioned npm operations. Normal CI remains verification-only for pushes and
5
- pull requests; publishing is isolated to the GitHub Actions `Publish` workflow,
6
- which is triggered manually with `workflow_dispatch`.
3
+ Releases start from the manual GitHub Actions `Publish` workflow. The workflow
4
+ checks and stages the npm package, waits for a maintainer's npm approval, then
5
+ creates the matching GitHub tag and release.
7
6
 
8
- ## Versioning Policy
7
+ ## Versioning policy
9
8
 
10
- Neal uses Semantic Versioning. `package.json.version` changes only in an
9
+ neal uses Semantic Versioning. `package.json.version` changes only in an
11
10
  intentional release-preparation pull request, not in ordinary implementation
12
11
  commits. Release versions are exact SemVer strings without a leading `v`, and
13
12
  an npm version must never be reused.
@@ -19,14 +18,14 @@ exit codes, or durable run artifacts require a major version.
19
18
 
20
19
  Examples:
21
20
 
22
- - Patch: `0.1.1` fixes a defect or compatibility issue without intentionally
21
+ - Patch: `0.3.2` fixes a defect or compatibility issue without intentionally
23
22
  changing documented behavior.
24
- - Minor before `1.0.0`: `0.2.0` adds a feature or meaningful behavior change
23
+ - Minor before `1.0.0`: `0.4.0` adds a feature or meaningful behavior change
25
24
  while the public contract is still settling.
26
- - Major: `1.0.0` establishes the stable public contract; later breaking
25
+ - Major: `1.0.0` establishes the stable public contract. Later breaking
27
26
  contract changes require `2.0.0`, `3.0.0`, and so on.
28
27
 
29
- ## SDK Dependency Policy
28
+ ## SDK dependency policy
30
29
 
31
30
  Direct coding-agent and AI SDK runtime dependencies must use exact SemVer specs
32
31
  in `package.json`. Do not use `latest`, caret ranges, tilde ranges, wildcards,
@@ -41,105 +40,86 @@ published CLI package:
41
40
 
42
41
  An SDK update should land as a normal dependency pull request that updates both
43
42
  `package.json` and `pnpm-lock.yaml`, runs normal CI, and records any provider
44
- compatibility findings. The resulting Neal release should match the user-facing
45
- impact: use a patch release for a compatibility fix that preserves documented
46
- behavior, a minor release for behavior changes before `1.0.0`, and a major
47
- release after `1.0.0` if a documented public contract breaks.
43
+ compatibility findings. Match the neal release to the user-facing impact: use a
44
+ patch release for a compatibility fix that preserves documented behavior, a
45
+ minor release for behavior changes before `1.0.0`, and a major release after
46
+ `1.0.0` if a documented public contract breaks.
48
47
 
49
- ## Local Release Readiness
48
+ ## Prepare a release
50
49
 
51
- Prepare a release with a normal pull request that changes `package.json.version`
52
- to the intended exact SemVer version:
50
+ Bump `package.json.version` and add a nonempty `## [<version>]` section to
51
+ `CHANGELOG.md` in a normal release-preparation pull request:
53
52
 
54
53
  ```sh
55
54
  pnpm version <version> --no-git-tag-version
56
55
  ```
57
56
 
58
- Run the local release-readiness gates from the repository root:
57
+ Run the release gates from the repository root:
59
58
 
60
59
  ```sh
61
60
  RELEASE_VERSION=<version> RELEASE_DRY_RUN=true pnpm run validate:release
62
61
  pnpm typecheck
63
62
  pnpm test
63
+ pnpm lint
64
64
  pnpm build
65
65
  node scripts/verify-package.mjs
66
66
  ```
67
67
 
68
- `validate:release` checks that the workflow input version matches
69
- `package.json.version`, validates required package metadata, checks npm version
70
- availability, and checks remote `v<version>` tag availability when an `origin`
71
- remote is configured. Package verification confirms the built CLI and packed
72
- tarball.
68
+ `validate:release` checks the package version, required metadata, direct SDK
69
+ pins, changelog section, npm version availability, and remote tag availability.
73
70
 
74
- ## Manual Publish Workflow
71
+ ## Publish
75
72
 
76
- After the release-preparation pull request is merged and normal CI passes, run
77
- the `Publish` workflow manually from `main` with:
73
+ After the release-preparation pull request is merged and normal CI passes, open
74
+ **Actions > Publish > Run workflow** on `main`.
78
75
 
79
- - `version`: the exact `package.json.version`, without a leading `v`
80
- - `dry_run`: `true`
76
+ Run it first with the exact version and `dry_run: true`. This runs every gate
77
+ and `npm publish --dry-run` without changing npm or GitHub.
81
78
 
82
- The workflow is guarded to `refs/heads/main`, uses the `npm-publish`
83
- environment, grants `contents: read` and `id-token: write`, installs with
84
- `pnpm install --frozen-lockfile`, and runs these gates before any publish step:
79
+ Run it again with `dry_run: false` after reviewing the dry run. The workflow:
85
80
 
86
- ```sh
87
- RELEASE_VERSION=<version> RELEASE_DRY_RUN=<dry_run> pnpm run validate:release
88
- pnpm typecheck
89
- pnpm test
90
- pnpm build
91
- node scripts/verify-package.mjs
92
- ```
93
-
94
- Review the dry-run result before any real publish. Run `Publish` again with
95
- `dry_run: false` only when a release-preparation plan has explicitly authorized
96
- the real publish.
97
-
98
- The real-publish path is **staged**: the workflow runs `npm stage publish`,
99
- which places the version in a staged, not-publicly-available state. A
100
- maintainer then reviews and approves it with 2FA — `npm stage list`,
101
- `npm stage view <stage-id>` / `npm stage download <stage-id>`, and
102
- `npm stage approve <stage-id>` (or the npmjs.com UI). Nothing reaches `latest`
103
- without that human approval, so a compromised workflow cannot ship directly.
104
-
105
- ## First-Publish History
81
+ 1. Runs the same release gates.
82
+ 2. Runs `npm stage publish` and writes the stage ID and approval commands to the
83
+ GitHub job summary.
84
+ 3. Waits up to 60 minutes for the package to become public.
85
+ 4. Verifies npm's signed provenance against the workflow's repository, path,
86
+ branch, and exact commit SHA.
87
+ 5. Creates `v<version>` at that commit and a GitHub release from the matching
88
+ changelog section.
106
89
 
107
- `0.1.0` was published manually on 2026-07-12 with an interactive 2FA publish
108
- from a maintainer terminal: npm trusted publishing cannot be configured for a
109
- package that has never been published, so the first publish had to
110
- authenticate directly. Every release after `0.1.0` goes through the `Publish`
111
- workflow and the staged flow above.
90
+ Review the stage with `npm stage view <stage-id>` or
91
+ `npm stage download <stage-id>`. Approve it with 2FA using
92
+ `npm stage approve <stage-id>` or the npmjs.com UI. npm won't publish the
93
+ package until that approval happens.
112
94
 
113
- ## Trusted Publishing Setup
95
+ ## Recovery
114
96
 
115
- The publish workflow relies on trusted publishing/OIDC and intentionally has no
116
- npm-token fallback. It grants `id-token: write` for OIDC, keeps repository
117
- contents read-only, uses the `npm-publish` GitHub environment, and runs
118
- `npm stage publish` only on the real-publish path (staged publishing requires
119
- npm >= 11.15.0; the workflow upgrades npm accordingly).
97
+ Rerun `Publish` with the same version and `dry_run: false` after a timeout or a
98
+ partial failure. The workflow checks existing state before taking action:
120
99
 
121
- The npm-side configuration for `@navels/neal`:
100
+ - If the npm version isn't public, it stages it and waits for approval.
101
+ - If the npm version is public, it skips staging and verifies its signed
102
+ provenance against the current workflow commit.
103
+ - If the tag or GitHub release already exists at the expected commit, it keeps
104
+ it and finishes successfully.
105
+ - If the npm provenance or tag points to another commit, it stops without
106
+ changing the existing release.
122
107
 
123
- - Publishing access: **Require two-factor authentication and disallow tokens**.
124
- This blocks every traditional token permanently; trusted publishers are
125
- unaffected because they use OIDC, and staged approvals always require a
126
- maintainer's 2FA.
127
- - Trusted publisher: owner/repo `navels/neal`, workflow filename
128
- `publish.yml`, environment `npm-publish`, allowed action **`npm stage
129
- publish` only** (stage-only; plain `npm publish` is deliberately not
130
- granted).
108
+ If the approval wait times out, approve the existing npm stage before rerunning.
109
+ Do not try to stage the same version a second time.
131
110
 
132
- ## Release Boundaries
111
+ ## Trusted publishing setup
133
112
 
134
- The current workflow checks that the remote `v<version>` tag is available, but
135
- it does not create tags or GitHub releases. Do not promise or perform release
136
- marker creation as part of the current publish workflow. If tags or GitHub
137
- releases are needed after a successful publish, add them in a separate plan with
138
- the required permissions and explicit operator approval.
113
+ The workflow uses npm trusted publishing with no token fallback. The npm trusted
114
+ publisher is configured for owner/repository `navels/neal`, workflow
115
+ `publish.yml`, environment `npm-publish`, and the **`npm stage publish` only**
116
+ permission. Publishing access requires two-factor authentication and disallows
117
+ traditional tokens.
139
118
 
140
- Recovery rule for later real publishes: if `npm publish` succeeds but a later
141
- release-marker step fails in a separate plan, do not rerun the publish blindly.
142
- First confirm the npm version exists, then create only the missing marker.
119
+ The workflow grants `id-token: write` for npm's OpenID Connect authentication
120
+ and `contents: write` for the final tag and GitHub release. The `npm-publish`
121
+ environment accepts only `main`. npm's stage-only permission means the workflow
122
+ can't make a package public without a maintainer's 2FA approval.
143
123
 
144
124
  Official references:
145
125
 
@@ -147,5 +127,7 @@ Official references:
147
127
  <https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/trigger-a-workflow>
148
128
  - npm trusted publishing:
149
129
  <https://docs.npmjs.com/trusted-publishers/>
130
+ - npm staged packages:
131
+ <https://docs.npmjs.com/staged-publishing/>
150
132
  - npm trust command:
151
133
  <https://docs.npmjs.com/cli/v11/commands/npm-trust/>
@@ -0,0 +1,246 @@
1
+ # Plan-review convergence
2
+
3
+ Plan review either converges or the run fails. The reviewer is asked "is this
4
+ plan execution-ready?" fresh every round against a document that grows with each
5
+ fix, so every individually-defensible new finding forces another full revision
6
+ round. The only non-acceptance exits used to be terminal failures: reaching the
7
+ round cap, and an unattended coder block. That produced two failure modes in
8
+ real runs: long negotiations that keep re-litigating verification strength after
9
+ the plan is already correct, and runs that terminally fail when a coder
10
+ legitimately needs author input it must not fabricate.
11
+
12
+ The convergence policy addresses both failure modes as **deterministic
13
+ orchestration policy**, not prompt tuning. It never shortens a negotiation that
14
+ surfaced a genuine plan-correctness defect, and only stops burning rounds on
15
+ verification-hardening demands once the plan itself is correct. And a
16
+ coder-authored plan-stage block no longer terminally fails: it lands as a
17
+ recoverable blocked-with-reason state that an operator can answer via
18
+ `neal resume --message` (see
19
+ [Coder-authored plan-stage block recovery](#coder-authored-plan-stage-block-recovery)).
20
+
21
+ The core round policy is a single pure function,
22
+ `synthesizePlanReviewRoundFromFindings` in `src/neal/adjudicator/planning.ts`,
23
+ shared by the runtime (`runPlanReviewPhase`) and by the fixture replay harness
24
+ under `test/fixtures/plan-review-ledgers/` (see that directory's `SCHEMA.md`), so
25
+ the policy pinned by the committed ledgers is exactly the policy that runs.
26
+
27
+ ## Finding classes
28
+
29
+ Every plan-review finding carries a declared `findingClass`:
30
+
31
+ - **`plan_correctness`**: the plan would build the wrong thing, build an
32
+ unverifiable thing, or contains an impossible or self-contradictory
33
+ instruction. These always force a revision round (or block at the cap). The
34
+ auto-generated plan-structure findings default to `plan_correctness`.
35
+ - **`verification_hardening`**: the finding only demands strengthening *how the
36
+ plan will be verified* (more oracles, more pinning, more coverage of
37
+ already-specified behavior) without asserting the plan builds the wrong or an
38
+ unverifiable thing. Past the round threshold these can convert to recorded
39
+ debt instead of forcing another round.
40
+
41
+ The class has two deliberately different boundaries:
42
+
43
+ - **Reviewer-payload boundary (strict).** An *absent* class normalizes to
44
+ `plan_correctness`. A *present-but-invalid* class value is rejected exactly as
45
+ an invalid `severity` is: the sequential payload validator throws
46
+ `must be exactly one of: plan_correctness, verification_hardening`, so an
47
+ invalid class is a validation error, never a silent downgrade.
48
+ - **Persisted-state boundary (tolerant).** When hydrating a saved run,
49
+ `hydrateFinding` maps a present-but-unknown class to `plan_correctness` and
50
+ leaves an absent class `undefined` (execute-review findings never carry a
51
+ class and must stay unaffected). Older run states load without error.
52
+
53
+ The system-wide fail-safe does **not** depend on hydration filling absence: the
54
+ decision sites convert only an *explicit* `verification_hardening`, so any
55
+ absent/`undefined`/unknown class is always round-forcing (blocking).
56
+
57
+ ## The debt round threshold
58
+
59
+ The knob `neal.plan_review_debt_round_threshold` (default **3**, read via
60
+ `getPlanReviewDebtRoundThreshold(cwd)`) is the reviewer round at or past which a
61
+ novel verification-hardening finding may convert to debt instead of forcing a
62
+ round. Below the threshold, every blocking finding forces a revision round
63
+ regardless of class. Early rounds are for genuine convergence, not banking.
64
+
65
+ ## Disposition policy
66
+
67
+ For each round the policy partitions the merged open blocking findings
68
+ (`classifyPlanReviewConvergence`) into `debtConvertible` and `roundForcing`:
69
+
70
+ Pre-cap, a finding is `debtConvertible` only when **all** of:
71
+
72
+ - its class is exactly `verification_hardening`
73
+ - it is a **first occurrence** of its canonical (the minimum round across all
74
+ merged findings sharing that canonical equals the current round)
75
+ - the current round is `>= threshold`.
76
+
77
+ Everything else is `roundForcing`: any `plan_correctness`/absent/unknown class,
78
+ **any repeat occurrence** of an existing canonical (a re-raised point still earns
79
+ a dedicated round pre-cap), or any below-threshold round.
80
+
81
+ From that partition:
82
+
83
+ - **Arrival-time conversion.** A `debtConvertible` finding banks as `deferred`
84
+ plan-review debt the moment it arrives, even when a co-occurring
85
+ `plan_correctness` or repeat finding forces the round, so novelty-bounded
86
+ hardening asks never extend the round count.
87
+ - **Accept-with-recorded-reservations landing.** When every open blocking
88
+ finding is `debtConvertible` and none is `roundForcing`, the round lands
89
+ accepted (`landAcceptedWithDebt`). It reuses the existing acceptance
90
+ transition (`accept_plan` → `done` for top-level plan review,
91
+ `accept_derived_plan` for a derived-plan review), so no adjudication-spec
92
+ outcome is added.
93
+
94
+ Branch precedence in `resolvePlanReviewDisposition` is strict:
95
+
96
+ 1. convergence block (reopen / stall) →
97
+ 2. round-forcing blocking findings →
98
+ 3. accept-with-debt →
99
+ 4. open non-blocking findings →
100
+ 5. clean acceptance.
101
+
102
+ Because `roundForcing` outranks `landAcceptedWithDebt`, a single open
103
+ plan-correctness or repeat blocker keeps the round forced even when convertible
104
+ hardening findings are present alongside it.
105
+
106
+ ## Cap behavior
107
+
108
+ At the round cap (`round >= roundLimit`) there are no rounds left to force, so the
109
+ convertible predicate becomes **class-only**: *every* open
110
+ `verification_hardening` finding converts regardless of first-occurrence or
111
+ threshold. The cap contract:
112
+
113
+ - If the convergence block did **not** fire and *every* open blocking finding is
114
+ `verification_hardening`, the run lands accepted-with-debt (all convert).
115
+ - If any open blocking finding is `plan_correctness` (or absent/unknown class),
116
+ the run terminal-blocks with the existing max-rounds reason.
117
+
118
+ The convergence block keeps precedence at the cap too: a re-opened
119
+ (`getReopenedCanonical`, ≥3 blocking rounds of one canonical) or stalled
120
+ (`hasRepeatedUnresolvedBlockingCanonicals`) finding, *even hardening-class*,
121
+ terminal-blocks rather than converting. So a re-litigated-but-not-reopened
122
+ hardening point converts at the cap, but a genuinely stuck one still blocks.
123
+
124
+ ## Plan-review debt: two fields, two lifecycles
125
+
126
+ Debt is modeled as a **canonical-keyed projection of the current findings**
127
+ (`toPlanReviewDebt`), never an accumulator: for each canonical whose
128
+ latest-round finding is a `deferred` `verification_hardening` finding, exactly
129
+ one debt item is emitted (latest round wins, `originRound` = that finding's
130
+ round). A banked canonical that later reopens, is fixed, or is rejected simply
131
+ drops out of the projection. The lifecycle is automatic, with no stale or
132
+ duplicate entries and no ad-hoc removal rule.
133
+
134
+ There are two distinct top-level state fields:
135
+
136
+ - **`planReviewDebt`**: the **current-negotiation** projection. Recomputed as
137
+ `toPlanReviewDebt(mergedFindings)` everywhere the current findings change
138
+ (`synthesizePlanReviewRound` / `runPlanReviewPhase` and
139
+ `runPlanningResponsePhase`). This is what a top-level plan run carries to the
140
+ queue on completion.
141
+ - **`inheritedPlanReviewDebt`**: **durable, write-once**. Seeded only at init
142
+ from the queue handoff and **never recomputed by any plan-review phase**.
143
+
144
+ Two fields are required because an *execution* child re-enters plan review: the
145
+ execute runnable-phase registry includes `reviewer_plan`,
146
+ `coder_plan_response`, and `coder_plan_optional_response`, so
147
+ `runPlanReviewPhase` runs inside the execution child during split-plan recovery.
148
+ If a single recomputed field were both seeded from the inherited debt and
149
+ recomputed as `toPlanReviewDebt(mergedFindings)`, that derived-plan review would
150
+ project only the derived plan's own findings and **erase** the inherited
151
+ top-level debt before the next scope reviewer saw it. Keeping
152
+ `inheritedPlanReviewDebt` write-once and untouched by any plan-review phase
153
+ guarantees a derived-plan review in the execution child can only ever mutate
154
+ `planReviewDebt`, never the inherited debt the execution reviewer actually needs.
155
+
156
+ ## Cross-run handoff (and its exclusion)
157
+
158
+ Plan→execution handoff is **cross-run, not in-process**: `neal run` runs planning
159
+ and execution as separate fresh child runs. The durable carrier is the
160
+ `PlanAndExecuteQueueItem`:
161
+
162
+ 1. On planning-child completion, `completePlanningStage` copies
163
+ `finalState.planReviewDebt` onto the queue item alongside `acceptedPlanPath`
164
+ (covering both in-process completion and the cross-process resume through
165
+ `continuePlanAndExecuteQueueFromChildRun`).
166
+ 2. When the fresh execution child starts, `runFreshPlanAndExecuteChild` passes
167
+ the item's debt as the `inheritedPlanReviewDebt` init option **only** for the
168
+ execution stage. `createInitialState` seeds the durable
169
+ `inheritedPlanReviewDebt` state field from it (the recomputed `planReviewDebt`
170
+ always starts empty).
171
+ 3. `buildReviewerContextPacket` surfaces the inherited debt to the execution
172
+ reviewer as full items (`canonicalId`, `findingClass`, `originRound`,
173
+ `claim`, `requiredAction`) under a dedicated `## Inherited Plan-Review Debt`
174
+ section, not a bare count.
175
+
176
+ **Exclusion (by design):** cross-run propagation is supported only inside a
177
+ single `neal run` queue (planning child → execution child). A standalone
178
+ `neal plan` run persists debt only in its own run record. A later
179
+ `neal execute`/`neal run` against the accepted plan is a fresh run that
180
+ re-reviews the plan and does **not** inherit that debt.
181
+
182
+ ## Where debt is visible
183
+
184
+ - **`REVIEW.md`** (`renderReviewMarkdown`): each finding line carries
185
+ `- Finding class:` (with `n/a` for classless execute-review findings), and a
186
+ dedicated `## Plan Review Debt` section lists `Inherited:` and `Current:`
187
+ groups.
188
+ - **`neal status`** (`buildStatusSnapshot` / `renderHumanStatusSnapshot`): a
189
+ `planReviewDebt` snapshot field sums both arrays with a per-item `inherited`
190
+ flag, rendered as a `Plan review debt:` line.
191
+ - **Reviewer context packet**: the `## Inherited Plan-Review Debt` section
192
+ described above.
193
+
194
+ ## Coder-authored plan-stage block recovery
195
+
196
+ The other non-acceptance exit is a plan-stage coder block. A coder-authored
197
+ plan-stage **response** block (a `coder_plan_response` or
198
+ `coder_plan_optional_response` block, the only plan-stage path that previously
199
+ terminal-failed) lands as the documented blocked contract instead of a terminal
200
+ failure. `finalizeBlockedPlanReviewResponse`
201
+ (`src/neal/orchestrator/phases/planning.ts`) takes an explicit `blockCause`
202
+ (`coder_authored` | `dirty_worktree` | `reviewer_convergence`), and for a
203
+ `coder_authored` block on the top-level plan stage, in **both attended and
204
+ unattended runs**, it:
205
+
206
+ - persists `status: 'blocked'` with a durable `blockerReason` (the coder's
207
+ reported blocker), so the writer exits **2** (not `failed`/exit 3) and the
208
+ reason surfaces in both the JSON and human `neal status` renderings.
209
+ - makes the block answerable via `neal resume --message`, reusing the existing
210
+ `pendingPlanReviewGuidance` channel and `runPlanningResponseAdjudication`
211
+ delivery. `recordPlanReviewGuidance`
212
+ (`src/neal/commands/recovery-guidance.ts`) maps the origin to the phase that
213
+ consumes the guidance: a `reviewer_plan` block still delivers to
214
+ `coder_plan_response` (unchanged), while a coder-authored response block returns
215
+ to its own origin phase (a `coder_plan_optional_response` block returns to
216
+ `coder_plan_optional_response`), so the re-run selects the right open findings
217
+ and delivers the guidance instead of accepting on an empty blocking set.
218
+ Recorded guidance forces a response adjudication even when a prior blocked
219
+ response closed every finding, so the operator's answer is never silently
220
+ discarded.
221
+
222
+ The invariant is that `blockerReason` is `null` whenever `status !== 'blocked'`.
223
+ The resume-planner blocked→running transitions clear it so it never outlives its
224
+ block.
225
+
226
+ Only `coder_authored` response blocks take this recoverable landing, and the
227
+ durable `blockerReason` is the discriminator. A `dirty_worktree` safety block
228
+ (the planner dirtied non-plan files with no operator to clean them) records no
229
+ `blockerReason`: it lands at the same response phase but stays a normal blocked
230
+ state: it is not reported or answerable as waiting for `--message` guidance, and
231
+ it keeps its prior bare-resume behavior when a resumable planner session exists. A
232
+ `dirty_worktree` safety block and reviewer cap/stall exhaustion (`reviewer_convergence`)
233
+ under `unattended` keep terminal-failing (exit 3).
234
+
235
+ **Exclusion (the initial `coder_plan` authoring block):** the author-input route
236
+ does **not** cover the initial `coder_plan` block. That block already lands
237
+ `status: 'blocked'` (exit 2, not a terminal failure) and stays resumable via a
238
+ bare `neal resume`. Delivering an operator answer to a re-run authoring round
239
+ would require a second guidance-delivery channel this policy deliberately avoids.
240
+ Its `--message` route is a deliberate follow-up, not part of this policy.
241
+
242
+ ## Related references
243
+
244
+ - `docs/plan-format.md`: executable plan shapes and normalization.
245
+ - `docs/state-machine.md`: persisted run and queue state invariants.
246
+ - `docs/adjudicator-inventory.md`: the shared coder/reviewer loop inventory.