@navels/neal 0.2.0 → 0.3.0
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.
- package/README.md +206 -209
- package/SECURITY.md +16 -19
- package/dist/neal/adjudicator/{blocked-adjudicator.js → consultant.js} +21 -21
- package/dist/neal/adjudicator/execute.js +33 -33
- package/dist/neal/adjudicator/final-completion.js +2 -31
- package/dist/neal/adjudicator/planning.js +230 -38
- package/dist/neal/agents/prompts.js +6 -17
- package/dist/neal/agents/rounds.js +43 -73
- package/dist/neal/agents/schemas.js +47 -13
- package/dist/neal/blocked-guidance.js +16 -9
- package/dist/neal/cli.js +2 -2
- package/dist/neal/commands/check.js +2 -2
- package/dist/neal/commands/compat.js +100 -36
- package/dist/neal/commands/interactive-activity.js +2 -2
- package/dist/neal/commands/recovery-guidance.js +20 -8
- package/dist/neal/commands/runtime.js +4 -2
- package/dist/neal/config.js +16 -7
- package/dist/neal/context/inline-review-context.js +13 -28
- package/dist/neal/context/reviewer-context.js +19 -14
- package/dist/neal/eval/reviewer-eval-manifest.js +86 -0
- package/dist/neal/eval/reviewer-eval.js +155 -0
- package/dist/neal/interactive-controls.js +5 -0
- package/dist/neal/orchestrator/completion.js +49 -2
- package/dist/neal/orchestrator/failures.js +7 -0
- package/dist/neal/orchestrator/notifications.js +9 -9
- package/dist/neal/orchestrator/phases/planning.js +117 -80
- package/dist/neal/orchestrator/phases/recovery.js +151 -115
- package/dist/neal/orchestrator/phases/review.js +28 -1
- package/dist/neal/orchestrator/transitions.js +7 -1
- package/dist/neal/orchestrator.js +3 -0
- package/dist/neal/plan-queue.js +29 -1
- package/dist/neal/prompts/execute.js +7 -19
- package/dist/neal/prompts/planning.js +64 -55
- package/dist/neal/prompts/review-doctrine.js +13 -50
- package/dist/neal/prompts/specialized.js +7 -17
- package/dist/neal/prompts/specs.js +180 -32
- package/dist/neal/providers/agent-settings-isolation.js +35 -0
- package/dist/neal/providers/anthropic-claude.js +18 -1
- package/dist/neal/providers/detection.js +3 -7
- package/dist/neal/providers/git-config-isolation.js +83 -0
- package/dist/neal/providers/openai-codex.js +25 -3
- package/dist/neal/providers/{generic-agentic-tools.js → openai-compatible-tools.js} +11 -5
- package/dist/neal/providers/openai-compatible.js +1078 -430
- package/dist/neal/providers/openrouter-routing.js +68 -0
- package/dist/neal/providers/pricing.js +1 -1
- package/dist/neal/providers/rate-card.js +6 -32
- package/dist/neal/providers/registry.js +6 -17
- package/dist/neal/providers/types.js +12 -0
- package/dist/neal/resume-decision.js +7 -6
- package/dist/neal/resume-planner.js +9 -0
- package/dist/neal/retrospective.js +3 -3
- package/dist/neal/review-debt.js +30 -0
- package/dist/neal/review-findings/provider.js +17 -29
- package/dist/neal/review.js +17 -1
- package/dist/neal/state-invariants.js +11 -3
- package/dist/neal/state-views.js +27 -5
- package/dist/neal/state.js +59 -15
- package/dist/neal/status.js +28 -1
- package/dist/neal/support.js +3 -3
- package/docs/README.md +22 -18
- package/docs/{ADJUDICATOR_INVENTORY.md → adjudicator-inventory.md} +58 -57
- package/docs/architecture.md +79 -83
- package/docs/automation.md +11 -11
- package/docs/compat.md +91 -61
- package/docs/compatible-models.md +145 -121
- package/docs/demo.md +15 -5
- package/docs/maintenance.md +65 -34
- package/docs/plan-format.md +61 -33
- package/docs/prompt-evals.md +96 -0
- package/docs/{PROMPT_SPECS.md → prompt-specs.md} +52 -48
- package/docs/providers.md +279 -349
- package/docs/release.md +18 -19
- package/docs/review-convergence.md +246 -0
- package/docs/state-machine.md +61 -59
- package/docs/storage.md +40 -31
- package/docs/troubleshooting.md +32 -29
- package/examples/compat/add-edit-verify/PLAN.md +2 -0
- package/examples/compat/add-edit-verify/broken.diff +2 -2
- package/examples/compat/add-edit-verify/good.diff +2 -2
- package/examples/compat/add-edit-verify/src/add.js +1 -1
- package/examples/compat/manifest.json +25 -29
- package/examples/compat/sum-grep-edit/PLAN.md +33 -0
- package/examples/compat/sum-grep-edit/broken.diff +12 -0
- package/examples/compat/sum-grep-edit/good.diff +12 -0
- package/examples/compat/sum-grep-edit/package.json +5 -0
- package/examples/compat/sum-grep-edit/src/numbers.js +9 -0
- package/examples/compat/sum-grep-edit/test/numbers.test.js +15 -0
- package/examples/issue-triage-js/README.md +0 -15
- package/examples/reviewer-eval/clean-doc/base/src/clamp.ts +3 -0
- package/examples/reviewer-eval/clean-doc/change.diff +9 -0
- package/examples/reviewer-eval/clean-extract/base/src/greet.ts +3 -0
- package/examples/reviewer-eval/clean-extract/change.diff +13 -0
- package/examples/reviewer-eval/dropped-error/base/src/load.ts +4 -0
- package/examples/reviewer-eval/dropped-error/change.diff +15 -0
- package/examples/reviewer-eval/manifest.json +45 -0
- package/examples/reviewer-eval/off-by-one/base/src/paginate.ts +5 -0
- package/examples/reviewer-eval/off-by-one/change.diff +11 -0
- package/{config.yml → neal.yml} +1 -1
- package/package.json +15 -14
- package/dist/neal/providers/generic-agentic.js +0 -1256
- package/docs/comparison.md +0 -105
- package/docs/issue-pipeline.md +0 -124
- package/examples/compat/is-even-add-test/PLAN.md +0 -30
- package/examples/compat/is-even-add-test/broken.diff +0 -11
- package/examples/compat/is-even-add-test/good.diff +0 -11
- package/examples/compat/is-even-add-test/package.json +0 -5
- package/examples/compat/is-even-add-test/src/is-even.js +0 -3
- package/examples/compat/is-even-add-test/test/is-even.test.js +0 -9
- package/examples/compat/reverse-grep-edit/PLAN.md +0 -32
- package/examples/compat/reverse-grep-edit/broken.diff +0 -12
- package/examples/compat/reverse-grep-edit/good.diff +0 -12
- package/examples/compat/reverse-grep-edit/package.json +0 -5
- package/examples/compat/reverse-grep-edit/src/strings.js +0 -10
- package/examples/compat/reverse-grep-edit/test/strings.test.js +0 -15
package/docs/release.md
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
# Release
|
|
1
|
+
# Release process
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
which is triggered manually with `workflow_dispatch`.
|
|
3
|
+
Releases are manual, versioned npm operations. Normal CI only verifies pushes
|
|
4
|
+
and pull requests. The GitHub Actions `Publish` workflow handles publishing and
|
|
5
|
+
runs only through `workflow_dispatch`.
|
|
7
6
|
|
|
8
|
-
## Versioning
|
|
7
|
+
## Versioning policy
|
|
9
8
|
|
|
10
|
-
|
|
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.
|
|
@@ -23,10 +22,10 @@ Examples:
|
|
|
23
22
|
changing documented behavior.
|
|
24
23
|
- Minor before `1.0.0`: `0.2.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
|
|
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
|
|
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,12 +40,12 @@ 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
|
|
43
|
+
compatibility findings. The resulting neal release should match the user-facing
|
|
45
44
|
impact: use a patch release for a compatibility fix that preserves documented
|
|
46
45
|
behavior, a minor release for behavior changes before `1.0.0`, and a major
|
|
47
46
|
release after `1.0.0` if a documented public contract breaks.
|
|
48
47
|
|
|
49
|
-
## Local
|
|
48
|
+
## Local release readiness
|
|
50
49
|
|
|
51
50
|
Prepare a release with a normal pull request that changes `package.json.version`
|
|
52
51
|
to the intended exact SemVer version:
|
|
@@ -71,7 +70,7 @@ availability, and checks remote `v<version>` tag availability when an `origin`
|
|
|
71
70
|
remote is configured. Package verification confirms the built CLI and packed
|
|
72
71
|
tarball.
|
|
73
72
|
|
|
74
|
-
## Manual
|
|
73
|
+
## Manual publish workflow
|
|
75
74
|
|
|
76
75
|
After the release-preparation pull request is merged and normal CI passes, run
|
|
77
76
|
the `Publish` workflow manually from `main` with:
|
|
@@ -97,12 +96,12 @@ the real publish.
|
|
|
97
96
|
|
|
98
97
|
The real-publish path is **staged**: the workflow runs `npm stage publish`,
|
|
99
98
|
which places the version in a staged, not-publicly-available state. A
|
|
100
|
-
maintainer then reviews and approves it with 2FA
|
|
99
|
+
maintainer then reviews and approves it with 2FA: `npm stage list`,
|
|
101
100
|
`npm stage view <stage-id>` / `npm stage download <stage-id>`, and
|
|
102
101
|
`npm stage approve <stage-id>` (or the npmjs.com UI). Nothing reaches `latest`
|
|
103
102
|
without that human approval, so a compromised workflow cannot ship directly.
|
|
104
103
|
|
|
105
|
-
## First-
|
|
104
|
+
## First-publish history
|
|
106
105
|
|
|
107
106
|
`0.1.0` was published manually on 2026-07-12 with an interactive 2FA publish
|
|
108
107
|
from a maintainer terminal: npm trusted publishing cannot be configured for a
|
|
@@ -110,26 +109,26 @@ package that has never been published, so the first publish had to
|
|
|
110
109
|
authenticate directly. Every release after `0.1.0` goes through the `Publish`
|
|
111
110
|
workflow and the staged flow above.
|
|
112
111
|
|
|
113
|
-
## Trusted
|
|
112
|
+
## Trusted publishing setup
|
|
114
113
|
|
|
115
114
|
The publish workflow relies on trusted publishing/OIDC and intentionally has no
|
|
116
115
|
npm-token fallback. It grants `id-token: write` for OIDC, keeps repository
|
|
117
116
|
contents read-only, uses the `npm-publish` GitHub environment, and runs
|
|
118
117
|
`npm stage publish` only on the real-publish path (staged publishing requires
|
|
119
|
-
npm >= 11.15.0
|
|
118
|
+
npm >= 11.15.0, so the workflow upgrades npm accordingly).
|
|
120
119
|
|
|
121
120
|
The npm-side configuration for `@navels/neal`:
|
|
122
121
|
|
|
123
122
|
- Publishing access: **Require two-factor authentication and disallow tokens**.
|
|
124
|
-
This blocks every traditional token permanently
|
|
123
|
+
This blocks every traditional token permanently. Trusted publishers are
|
|
125
124
|
unaffected because they use OIDC, and staged approvals always require a
|
|
126
125
|
maintainer's 2FA.
|
|
127
126
|
- Trusted publisher: owner/repo `navels/neal`, workflow filename
|
|
128
127
|
`publish.yml`, environment `npm-publish`, allowed action **`npm stage
|
|
129
|
-
publish` only** (stage-only
|
|
128
|
+
publish` only** (stage-only, plain `npm publish` is deliberately not
|
|
130
129
|
granted).
|
|
131
130
|
|
|
132
|
-
## Release
|
|
131
|
+
## Release boundaries
|
|
133
132
|
|
|
134
133
|
The current workflow checks that the remote `v<version>` tag is available, but
|
|
135
134
|
it does not create tags or GitHub releases. Do not promise or perform release
|
|
@@ -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.
|
package/docs/state-machine.md
CHANGED
|
@@ -1,29 +1,29 @@
|
|
|
1
|
-
#
|
|
1
|
+
# State machine
|
|
2
2
|
|
|
3
3
|
The product-level storage layout, artifact classifications, run pointers,
|
|
4
4
|
retention guidance, and no-global-index decision are documented in
|
|
5
|
-
[storage.md](storage.md).
|
|
6
|
-
state invariants
|
|
5
|
+
[storage.md](storage.md). What follows covers the persisted ledgers and the
|
|
6
|
+
state invariants neal enforces when reading and writing them.
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
neal keeps two persisted ledgers:
|
|
9
9
|
|
|
10
10
|
- `OrchestrationState` is the child-run ledger. It lives in each run directory as `RUN_STATE.json` and records one plan or execute writer run.
|
|
11
11
|
- `PlanAndExecuteQueueState` is the parent queue ledger. It lives under `.neal/queues/<queue-id>/QUEUE_STATE.json` and tracks serial `neal run` children, their active stage, child run ids, and queue-level stop status.
|
|
12
12
|
|
|
13
|
-
These ledgers are related but separate. A queue item points at child run state paths
|
|
13
|
+
These ledgers are related but separate. A queue item points at child run state paths. Child runs do not own the queue's status, item ordering, or current queue pointer.
|
|
14
14
|
|
|
15
|
-
## Hydration
|
|
15
|
+
## Hydration and validation
|
|
16
16
|
|
|
17
|
-
`src/neal/state.ts` owns v1 child-run hydration. It reads JSON, checks the basic shape, requires the current v1 fields
|
|
17
|
+
`src/neal/state.ts` owns v1 child-run hydration. It reads JSON, checks the basic shape, requires the current v1 fields neal writes, and rejects unknown enum-like strings while hydrating phase references. Missing or malformed required child-run fields fail load instead of receiving invented defaults.
|
|
18
18
|
|
|
19
19
|
`src/neal/state-invariants.ts` owns semantic validation after hydration and before save. `assertValidOrchestrationState` is pure: it has no filesystem, git, provider, logger, or clock dependency. The same invariant layer runs when `loadState` returns hydrated state and when `saveState` builds the timestamped next state.
|
|
20
20
|
|
|
21
21
|
Queue state has its own parser and invariants in `src/neal/plan-queue.ts`, centered on `parsePlanAndExecuteQueueState` and related queue item parsing helpers.
|
|
22
22
|
|
|
23
|
-
## Schema
|
|
23
|
+
## Schema versions
|
|
24
24
|
|
|
25
|
-
Persisted JSON that
|
|
26
|
-
`RUN_STATE.json` hydrates through `normalizeStateV1
|
|
25
|
+
Persisted JSON that neal reads as durable schema is versioned at v1.
|
|
26
|
+
`RUN_STATE.json` hydrates through `normalizeStateV1`. `.neal/current.json`,
|
|
27
27
|
queue state, `.neal/current-queue.json`, queue child links, run narratives,
|
|
28
28
|
squash audit results, and the active writer lock all have v1 write shapes or
|
|
29
29
|
parsers.
|
|
@@ -32,7 +32,7 @@ Run `meta.json` is support data, not child-run state. Optional context artifacts
|
|
|
32
32
|
such as `plan-progress.json` are written as v1 and read defensively so malformed
|
|
33
33
|
or unsupported support data does not replace the canonical run ledger.
|
|
34
34
|
|
|
35
|
-
## State
|
|
35
|
+
## State views and public lifecycle
|
|
36
36
|
|
|
37
37
|
Persisted v1 child-run state remains record-shaped, but most callers should not
|
|
38
38
|
treat `OrchestrationState` as one large nullable programming model.
|
|
@@ -55,7 +55,7 @@ human output can say `waiting_for_guidance`, `paused`, `blocked`, `failed`, or
|
|
|
55
55
|
fields such as `effectiveStatus`, `waitingForOperatorGuidance`, and
|
|
56
56
|
`pendingOperatorGuidance`.
|
|
57
57
|
|
|
58
|
-
## Phase
|
|
58
|
+
## Phase and status
|
|
59
59
|
|
|
60
60
|
Known child-run phases and statuses are centralized in `state-invariants.ts` through `ORCHESTRATION_PHASES`, `ORCHESTRATION_STATUSES`, and their runtime guard helpers. Runnable dispatch is authored in `src/neal/orchestrator/run-loop.ts` as purpose-specific registries for plan, execute, interactive recovery, and execute finalization phases. Those registries are composed into `RUNNABLE_PHASE_REGISTRY` for dispatch and exposed through top-level-mode helpers so tests can assert which phases are runnable for plan and execute runs.
|
|
61
61
|
|
|
@@ -68,16 +68,18 @@ The enforced phase/status relationship is intentionally small:
|
|
|
68
68
|
|
|
69
69
|
Read-only `neal review` is not a writer-run mode. It writes isolated findings artifacts under `.neal/reviews/<review-id>/` and does not create `OrchestrationState`.
|
|
70
70
|
|
|
71
|
-
## Recovery
|
|
71
|
+
## Recovery state
|
|
72
72
|
|
|
73
73
|
Interactive blocked recovery is owned by `interactiveBlockedRecovery` while the active phase is `interactive_blocked_recovery`. The invariant layer validates execute-mode ownership, supported source phases, bounded turn counters, contiguous turn numbers, and disposition result phases.
|
|
74
74
|
|
|
75
|
-
Every block class
|
|
76
|
-
the split-plan invalid-payload block
|
|
77
|
-
`enterInteractiveBlockedRecovery` chokepoint, where the
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
75
|
+
Every block class (coder-blocked signals, reviewer `review_stuck` deadlocks, and
|
|
76
|
+
the split-plan invalid-payload block) funnels through the single
|
|
77
|
+
`enterInteractiveBlockedRecovery` chokepoint, where the consultant triages it (see
|
|
78
|
+
Site A below). The consultant is read-only: it never grants authorization, expands
|
|
79
|
+
scope, or waives verification gates. A recoverable verdict acts automatically under
|
|
80
|
+
both run modes. The modes differ only on a non-recoverable verdict, which finalizes
|
|
81
|
+
the run terminally under unattended runs and yields to the operator (carrying the
|
|
82
|
+
verdict as advice) under attended runs.
|
|
81
83
|
|
|
82
84
|
Public resume eligibility is classified by `src/neal/resume-decision.ts` before
|
|
83
85
|
any recovery mutation. That read-only decision layer combines loaded child-run
|
|
@@ -91,7 +93,7 @@ selected actions only after the selected run has been classified as executable.
|
|
|
91
93
|
`state-invariants.ts` mirrors the allowed phase sets so changes to recovery
|
|
92
94
|
behavior are visible in focused tests.
|
|
93
95
|
|
|
94
|
-
## Unattended
|
|
96
|
+
## Unattended mode
|
|
95
97
|
|
|
96
98
|
`--unattended` / `agent.unattended` resolves to a persisted
|
|
97
99
|
`OrchestrationState.unattended` boolean (default `false`) for both plan and
|
|
@@ -99,17 +101,17 @@ execute top-level modes, so a separate `neal resume` process and the `neal run`
|
|
|
99
101
|
plan→execute hand-off see it without re-passing a flag. The flag overrides the
|
|
100
102
|
config key. The resolved value is also threaded into the planner, reviewer,
|
|
101
103
|
coder, final-completion, and plan-reviewer prompts, where it adds one autonomy
|
|
102
|
-
line only when true
|
|
104
|
+
line only when true. With `unattended` false the rendered prompts are
|
|
103
105
|
byte-identical to attended output.
|
|
104
106
|
|
|
105
|
-
Unattended changes only the three structural operator-block sites
|
|
107
|
+
Unattended changes only the three structural operator-block sites. It never
|
|
106
108
|
weakens verification, authorization, or squash/grading, and never removes
|
|
107
109
|
`block_for_operator` from any decision surface. Every unattended branch gates on
|
|
108
110
|
structural state (`state.unattended`, `actionResolution.effectiveAction`,
|
|
109
111
|
`phase`, `blockedFromPhase`, the bounded auto-resume counter), never on
|
|
110
112
|
substring-matching assistant or guidance text.
|
|
111
113
|
|
|
112
|
-
- **Site A
|
|
114
|
+
- **Site A: execute-mode interactive recovery.** All fresh blocks funnel
|
|
113
115
|
through `enterInteractiveBlockedRecovery` (`src/neal/orchestrator/phases/recovery.ts`).
|
|
114
116
|
Under unattended, while `unattendedAutoResumeCount < UNATTENDED_MAX_AUTO_RESUMES`
|
|
115
117
|
(a module constant, reconciled so it never pushes past
|
|
@@ -119,46 +121,46 @@ substring-matching assistant or guidance text.
|
|
|
119
121
|
message uses, increments the persisted counter, and lets the run proceed into
|
|
120
122
|
the recovery phase. Past the cap (or the `maxTurns` boundary) it runs the
|
|
121
123
|
shared terminal-fail action instead of waiting.
|
|
122
|
-
- **
|
|
124
|
+
- **The consultant (bounded, both modes).** Inside
|
|
123
125
|
`enterInteractiveBlockedRecovery`, *before* the generic auto-resume / yield
|
|
124
|
-
decision, eligible blocks are triaged by
|
|
125
|
-
(`
|
|
126
|
+
decision, eligible blocks are triaged by the read-only consultant
|
|
127
|
+
(`runConsultant`, in `src/neal/adjudicator/consultant.ts`,
|
|
126
128
|
running through the same no-write reviewer plumbing the review/final-completion
|
|
127
129
|
reviewers use, making zero commits and zero file edits). Eligible source phases
|
|
128
|
-
(`
|
|
130
|
+
(`CONSULTANT_ELIGIBLE_SOURCE_PHASES`) are the coder-block phases (`coder_scope`
|
|
129
131
|
/ `coder_response` / `coder_optional_response`, which also carry the split-plan
|
|
130
132
|
invalid-payload block) and the reviewer `review_stuck` phases (`reviewer_scope` /
|
|
131
|
-
`reviewer_plan`). The
|
|
133
|
+
`reviewer_plan`). The consultant first applies an anti-thrash guard
|
|
132
134
|
(`recentBlocks`, keyed on scope identity + source phase + normalized blocker key +
|
|
133
135
|
evidence fingerprint): a same-scope repeat with no new evidence short-circuits to
|
|
134
136
|
`recoverable:false` without an LLM round. Otherwise it returns a verdict
|
|
135
|
-
`{ recoverable, triageCategory, resolutionDirective, rationale }`.
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
genuine
|
|
140
|
-
the shared terminal-fail action
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
`
|
|
144
|
-
(default `1`, `0` disables)
|
|
137
|
+
`{ recoverable, triageCategory, resolutionDirective, rationale }`. A `recoverable`
|
|
138
|
+
`misunderstanding` verdict with a concrete in-scope directive enters recovery with
|
|
139
|
+
that directive injected as the pending turn (consumed exactly like a human
|
|
140
|
+
`neal resume --message`) under both run modes. The modes differ only on a
|
|
141
|
+
`recoverable:false` genuine blocker (`authorization` / `external_precondition` /
|
|
142
|
+
`impossible_task`): unattended runs the shared terminal-fail action, while attended
|
|
143
|
+
persists the verdict as `interactiveBlockedRecovery.consultantAdvice` and yields for
|
|
144
|
+
the operator. It is bounded by the counter
|
|
145
|
+
`consultantAttemptCount` against the `consultant_max_attempts` knob
|
|
146
|
+
(default `1`, `0` disables). It's a separate budget that never touches
|
|
145
147
|
`unattendedAutoResumeCount` or `interactiveBlockedRecovery.maxTurns`. Every other
|
|
146
148
|
case (ineligible source phase, disabled/exhausted cap, turn cap, or any
|
|
147
|
-
|
|
148
|
-
unchanged, writing neither `recentBlocks` nor `
|
|
149
|
+
consultant error) falls through to the generic auto-resume / yield path
|
|
150
|
+
unchanged, writing neither `recentBlocks` nor `consultantAdvice`.
|
|
149
151
|
The decisions are auditable from the structured event log via the
|
|
150
|
-
`
|
|
152
|
+
`consultant.{start,verdict,resolved,declined}` events, which
|
|
151
153
|
carry `scopeNumber`, `sourcePhase`, `blockedReason`, and (on `verdict`/`resolved`)
|
|
152
154
|
`recoverable`, `triageCategory`, `targetCanonicalIds`, and the post-increment
|
|
153
|
-
`
|
|
154
|
-
- **Sites B and C
|
|
155
|
+
`consultantAttemptCount`.
|
|
156
|
+
- **Sites B and C: final-completion review and the top-level plan-review gate.**
|
|
155
157
|
These gates block directly (bypassing the recovery chokepoint), and their own
|
|
156
|
-
budgets
|
|
157
|
-
review-round/convergence cap
|
|
158
|
+
budgets (the final-completion continue-execution cap and the
|
|
159
|
+
review-round/convergence cap) already bounded the autonomous effort. Under
|
|
158
160
|
unattended they run the shared terminal-fail action immediately rather than
|
|
159
|
-
saving `status:'blocked'
|
|
161
|
+
saving `status:'blocked'`. There is no auto-resume and no synthesized
|
|
160
162
|
`pendingPlanReviewGuidance`. Site C edits only the `topLevelMode !== 'execute'`
|
|
161
|
-
branch of `finalizeBlockedPlanReviewResponse
|
|
163
|
+
branch of `finalizeBlockedPlanReviewResponse`. Execute-mode derived-plan-review
|
|
162
164
|
blocks (`topLevelMode === 'execute'`) re-enter site A and are handled there.
|
|
163
165
|
|
|
164
166
|
The shared terminal-fail action is `persistUnattendedBlockUnresolvedFailure`
|
|
@@ -167,14 +169,14 @@ The shared terminal-fail action is `persistUnattendedBlockUnresolvedFailure`
|
|
|
167
169
|
`phase`/`blockedFromPhase` for diagnostics), re-render execution artifacts, write
|
|
168
170
|
a `failed` checkpoint retrospective, and emit the classified
|
|
169
171
|
`unattended.block_unresolved` log event (`reason:'unattended_block_unresolved'`
|
|
170
|
-
plus the `UnattendedBlockSite` origin)
|
|
172
|
+
plus the `UnattendedBlockSite` origin), deliberately without `notifyBlocked`,
|
|
171
173
|
which is the attended wait notification. The run exits with writer code `3`, and
|
|
172
174
|
any produced diff/plan is left unsubmitted as an artifact. There is no top-level
|
|
173
|
-
reason field on `OrchestrationState
|
|
175
|
+
reason field on `OrchestrationState`. The classification lives in the log event and
|
|
174
176
|
retrospective. Attended runs are unchanged and still wait for
|
|
175
177
|
`neal resume --message` at all three sites.
|
|
176
178
|
|
|
177
|
-
## Resume
|
|
179
|
+
## Resume planning
|
|
178
180
|
|
|
179
181
|
New-run initialization and existing-run resume reconciliation are separate.
|
|
180
182
|
`loadOrInitialize` still provides the command-facing entrypoint, but resume
|
|
@@ -198,10 +200,10 @@ normalization, event logging, pointer writes, and execution artifacts.
|
|
|
198
200
|
when the shared decision says interactive recovery is waiting for guidance.
|
|
199
201
|
Pending guidance resumes with plain `neal resume --run <run-id>`. Queue
|
|
200
202
|
continuation still belongs to `continuePlanAndExecuteQueueFromChildRun` after
|
|
201
|
-
the resumed child run finishes
|
|
203
|
+
the resumed child run finishes. Selecting a child run for resume does not
|
|
202
204
|
advance or repair the parent queue by itself.
|
|
203
205
|
|
|
204
|
-
## Derived
|
|
206
|
+
## Derived plans
|
|
205
207
|
|
|
206
208
|
Derived-plan fields are child-run state because they describe replacement execution for the active child run, not the parent queue. The invariant layer enforces these ownership rules:
|
|
207
209
|
|
|
@@ -212,23 +214,23 @@ Derived-plan fields are child-run state because they describe replacement execut
|
|
|
212
214
|
|
|
213
215
|
Derived-plan execution should continue to flow through the existing orchestrator transition helpers before any future shape refactor changes the persisted fields.
|
|
214
216
|
|
|
215
|
-
## Final
|
|
217
|
+
## Final completion
|
|
216
218
|
|
|
217
|
-
Final-completion review is execute-mode only. The `final_completion_review` phase requires a `finalCompletionSummary`. When a reviewer verdict exists, `finalCompletionResolvedAction` must match the effective action, including the continue-execution cap case where
|
|
219
|
+
Final-completion review is execute-mode only. The `final_completion_review` phase requires a `finalCompletionSummary`. When a reviewer verdict exists, `finalCompletionResolvedAction` must match the effective action, including the continue-execution cap case where neal resolves to operator blocking instead of starting another scope.
|
|
218
220
|
|
|
219
221
|
When final completion review asks to continue execution, the orchestrator may clear summary and verdict fields and reopen `coder_scope`. That reopened running state is valid.
|
|
220
222
|
|
|
221
|
-
Accepted execute scopes also pass through execute finalization before
|
|
223
|
+
Accepted execute scopes also pass through execute finalization before neal either opens the next scope or starts final-completion review. The persisted internal runnable cursor for that step is `execute_finalization`, which is separate from the public `neal squash` command. Public `neal squash` is a post-run command with selection, preview, and interactive TTY confirmation before it rewrites history.
|
|
222
224
|
|
|
223
|
-
## Atomic
|
|
225
|
+
## Atomic writes and locks
|
|
224
226
|
|
|
225
227
|
`src/neal/atomic-write.ts` provides per-file atomic replacement helpers. State, current run pointers, queue state, queue pointers, queue links, queue summaries, run metadata, progress artifacts, review/final-completion/recovery/split-plan/retrospective artifacts, run narratives, and default squash audit writes use temp-file-plus-rename writes where in scope.
|
|
226
228
|
|
|
227
229
|
This is not a multi-file transaction. If a process stops between writes, each individual JSON or text file should be either the previous complete file or the next complete file, but related files can briefly disagree. Resume and status commands must continue to tolerate that by resolving run paths and validating loaded state.
|
|
228
230
|
|
|
229
|
-
The active-run lock is separate. `src/neal/run-lock.ts` uses exclusive creation for `.neal/active-run.lock
|
|
231
|
+
The active-run lock is separate. `src/neal/run-lock.ts` uses exclusive creation for `.neal/active-run.lock`. That acquisition path should not be converted to atomic rename because the exclusive-create behavior is the mutual exclusion mechanism.
|
|
230
232
|
|
|
231
|
-
## Change
|
|
233
|
+
## Change checklist
|
|
232
234
|
|
|
233
235
|
When adding a child-run phase:
|
|
234
236
|
|
|
@@ -253,7 +255,7 @@ When adding a queue-state field:
|
|
|
253
255
|
- Parse and validate it in `parsePlanAndExecuteQueueState` or `parsePlanAndExecuteQueueItem`.
|
|
254
256
|
- Include it in queue summary or current queue pointer output only when it is operator-facing or needed for resume.
|
|
255
257
|
|
|
256
|
-
## Future
|
|
258
|
+
## Future refactor path
|
|
257
259
|
|
|
258
260
|
Do not jump directly from the current record-shaped `OrchestrationState` to a
|
|
259
261
|
persisted discriminated union. The lower-risk path now in place is to keep v1
|