@hyperdreamer/pi-webui 1.11.0-beta.1 → 1.11.0-beta.10

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 (86) hide show
  1. package/README.md +17 -2
  2. package/dist/cli.js +265 -32
  3. package/dist/cli.js.map +1 -1
  4. package/dist/client/assets/{CodeViewer-DLZCHlMU.js → CodeViewer-CAf_j27u.js} +1 -1
  5. package/dist/client/assets/{UnifiedDiffViewer-XCb8NlrO.js → UnifiedDiffViewer-wBjEJWa1.js} +1 -1
  6. package/dist/client/assets/{index-BaDGxg8F.js → index-5EtDb9nj.js} +782 -457
  7. package/dist/client/index.html +1 -1
  8. package/dist/config.js +48 -11
  9. package/dist/config.js.map +1 -1
  10. package/dist/pi-webui-plugins/workspace-tasks/pi-webui-plugin.js +1 -0
  11. package/dist/plugin-api.d.ts +2 -0
  12. package/dist/server/app.js +6 -1
  13. package/dist/server/app.js.map +1 -1
  14. package/dist/server/realtime/sessionEventHub.js +40 -5
  15. package/dist/server/realtime/sessionEventHub.js.map +1 -1
  16. package/dist/server/realtime/sessionStatusCoalescer.js +137 -0
  17. package/dist/server/realtime/sessionStatusCoalescer.js.map +1 -0
  18. package/dist/server/sessiond/sessionProxyRoutes.js +1 -0
  19. package/dist/server/sessiond/sessionProxyRoutes.js.map +1 -1
  20. package/dist/server/sessiond.js +39 -5
  21. package/dist/server/sessiond.js.map +1 -1
  22. package/dist/server/sessions/modelPolicyCapability.js +88 -0
  23. package/dist/server/sessions/modelPolicyCapability.js.map +1 -0
  24. package/dist/server/sessions/modelPolicyTool.js +31 -0
  25. package/dist/server/sessions/modelPolicyTool.js.map +1 -0
  26. package/dist/server/sessions/modelTierRegistry.js +14 -0
  27. package/dist/server/sessions/modelTierRegistry.js.map +1 -1
  28. package/dist/server/sessions/piSessionService.js +1643 -433
  29. package/dist/server/sessions/piSessionService.js.map +1 -1
  30. package/dist/server/sessions/sessionDefaultsRoutes.js +39 -0
  31. package/dist/server/sessions/sessionDefaultsRoutes.js.map +1 -1
  32. package/dist/server/sessions/sessionDefaultsService.js +36 -6
  33. package/dist/server/sessions/sessionDefaultsService.js.map +1 -1
  34. package/dist/server/sessions/sessionMetadataStore.js +107 -13
  35. package/dist/server/sessions/sessionMetadataStore.js.map +1 -1
  36. package/dist/server/sessions/sessionModelPolicy.js +118 -0
  37. package/dist/server/sessions/sessionModelPolicy.js.map +1 -0
  38. package/dist/server/sessions/sessionReorder.js +117 -0
  39. package/dist/server/sessions/sessionReorder.js.map +1 -0
  40. package/dist/server/sessions/sessionRouteFastifyOptions.js +8 -0
  41. package/dist/server/sessions/sessionRouteFastifyOptions.js.map +1 -0
  42. package/dist/server/sessions/sessionRoutes.js +208 -3
  43. package/dist/server/sessions/sessionRoutes.js.map +1 -1
  44. package/dist/server/sessions/starterModelPolicyPreferenceStore.js +164 -0
  45. package/dist/server/sessions/starterModelPolicyPreferenceStore.js.map +1 -0
  46. package/dist/server/sessions/utilityModelExtension.js +199 -0
  47. package/dist/server/sessions/utilityModelExtension.js.map +1 -0
  48. package/dist/server/sessions/utilityModelResolver.js +77 -0
  49. package/dist/server/sessions/utilityModelResolver.js.map +1 -0
  50. package/dist/server/sessions/utilityModelSettingsRoutes.js +62 -0
  51. package/dist/server/sessions/utilityModelSettingsRoutes.js.map +1 -0
  52. package/dist/server/sessions/utilityModelSettingsService.js +101 -0
  53. package/dist/server/sessions/utilityModelSettingsService.js.map +1 -0
  54. package/dist/server/skills/optionalSkillInstall.js +69 -0
  55. package/dist/server/skills/optionalSkillInstall.js.map +1 -0
  56. package/dist/server/skills/optionalSkillInstaller.js +148 -0
  57. package/dist/server/skills/optionalSkillInstaller.js.map +1 -0
  58. package/dist/shared/apiTypes.d.ts +111 -3
  59. package/dist/shared/apiTypes.js +9 -0
  60. package/dist/shared/apiTypes.js.map +1 -1
  61. package/dist/shared/capabilities.js +12 -0
  62. package/dist/shared/capabilities.js.map +1 -1
  63. package/dist/shared/federatedRoutes.js +5 -0
  64. package/dist/shared/federatedRoutes.js.map +1 -1
  65. package/docs/assets/pi-webui-desktop.png +0 -0
  66. package/docs/config.md +38 -3
  67. package/optional-skills/deterministic-subagent-driven-development/SKILL.md +224 -0
  68. package/optional-skills/deterministic-subagent-driven-development/pi-webui-skill.json +28 -0
  69. package/optional-skills/deterministic-subagent-driven-development/prompts/final-reviewer.md +132 -0
  70. package/optional-skills/deterministic-subagent-driven-development/prompts/implementer.md +101 -0
  71. package/optional-skills/deterministic-subagent-driven-development/prompts/re-reviewer.md +60 -0
  72. package/optional-skills/deterministic-subagent-driven-development/prompts/task-reviewer.md +80 -0
  73. package/optional-skills/deterministic-subagent-driven-development/references/capability-contract.md +174 -0
  74. package/optional-skills/deterministic-subagent-driven-development/references/plan-contract.md +268 -0
  75. package/optional-skills/deterministic-subagent-driven-development/references/state-machine.md +177 -0
  76. package/optional-skills/deterministic-subagent-driven-development/scripts/lib/manifest.mjs +258 -0
  77. package/optional-skills/deterministic-subagent-driven-development/scripts/lib/plan-policy.mjs +350 -0
  78. package/optional-skills/deterministic-subagent-driven-development/scripts/lib/prompt-renderer.mjs +290 -0
  79. package/optional-skills/deterministic-subagent-driven-development/scripts/lib/state-machine.mjs +1264 -0
  80. package/optional-skills/deterministic-subagent-driven-development/scripts/lib/state-store.mjs +532 -0
  81. package/optional-skills/deterministic-subagent-driven-development/scripts/sdd-state +3 -0
  82. package/optional-skills/deterministic-subagent-driven-development/scripts/sdd-state.mjs +349 -0
  83. package/optional-skills/deterministic-writing-plans/SKILL.md +232 -0
  84. package/optional-skills/deterministic-writing-plans/references/grammar.md +84 -0
  85. package/optional-skills/deterministic-writing-plans/templates/plan-skeleton.md +143 -0
  86. package/package.json +12 -9
@@ -0,0 +1,174 @@
1
+ # Version-1 Capability and Spawn-Result Contract
2
+
3
+ This file is the canonical handoff to the backend plan that implements these
4
+ capabilities. It is frozen at version 1: every later task validates against the
5
+ shapes defined here rather than against a live backend.
6
+
7
+ This reference is **not** loaded into no-guidance or original-skill evaluation
8
+ controls. It describes tool contracts, not controller procedure.
9
+
10
+ Wire tiers are lowercase. Plan documents keep title case (`**Implementer tier:**
11
+ Capable`). Canonical order is Economy, Fast, Standard, Advanced, Capable,
12
+ Frontier.
13
+
14
+ ## `get_model_policy`
15
+
16
+ A zero-parameter, read-only tool. It never mutates policy, never applies a tier,
17
+ and never returns credentials or endpoints.
18
+
19
+ It advertises no tier slash commands. An earlier draft of this file listed
20
+ `/tier-economy`...`/tier-frontier` plus `/tier-up` and `/tier-down`. No such
21
+ command is registered in the runtime: tier selection is the typed `tier` field on
22
+ `spawn_subsession` and nothing else. The rendered `Model tier: <tier>` line is a
23
+ human-readable consistency label, not a command.
24
+
25
+ ```ts
26
+ type ModelTier = "economy" | "fast" | "standard" | "advanced" | "capable" | "frontier";
27
+
28
+ interface ExactModelSelection {
29
+ model: { provider: string; id: string };
30
+ thinkingLevel: string;
31
+ }
32
+
33
+ interface GetModelPolicyV1 {
34
+ contractVersion: 1;
35
+ policy: {
36
+ mode: "exact" | "tiered";
37
+ rememberedTier: ModelTier | null;
38
+ currentTier: ModelTier | null;
39
+ currentRuntime: ExactModelSelection;
40
+ nextRequestResolved: ExactModelSelection | null;
41
+ blockedReason: string | null;
42
+ };
43
+ ladder: { valid: boolean; revision: string | null; blockedReason: string | null };
44
+ trackedDispatch: {
45
+ contractVersion: 1;
46
+ tierField: true;
47
+ scope: "parent-session";
48
+ canonicalInputs: readonly ["cwd", "prompt", "tier"];
49
+ returnsSessionId: true;
50
+ };
51
+ }
52
+ ```
53
+
54
+ ### Conditional invariants
55
+
56
+ Tuples use the `ExactModelSelection` shape `{ model: { provider, id },
57
+ thinkingLevel }` and carry model identity and supported thinking only.
58
+
59
+ | Condition | Requirement |
60
+ | --- | --- |
61
+ | Exact mode | `currentTier` is `null`; `currentRuntime` and `nextRequestResolved` are both non-null and equal |
62
+ | Valid tiered mode | `currentTier` is non-null; ladder is complete and valid; latest resolved tuple is non-null |
63
+ | Invalid tiered mode | `nextRequestResolved` may be `null` only when `ladder.blockedReason` is a non-empty actionable reason; this state is capability-blocking |
64
+ | Any policy blocked reason | Capability-blocking regardless of mode |
65
+ | Invalid ladder, either mode | Capability-blocking. Children are dispatched *by tier* whatever the parent's mode, and reviewer/fixer tiers are derived by formula, so any unresolvable rung can fail a later round |
66
+
67
+ A capability-blocking state yields `CAPABILITY_BLOCKED` before any worktree
68
+ mutation, plan mutation, or dispatch.
69
+
70
+ ## `spawn_subsession` success details
71
+
72
+ This section describes the **implemented** runtime, verified against
73
+ `src/server/sessions/spawnSubsessionTool.ts` and
74
+ `src/server/sessions/piSessionService.ts`. Earlier drafts of this file specified
75
+ server-side dispatch keying and replay deduplication. The runtime has neither.
76
+
77
+ ```ts
78
+ interface SpawnSubsessionParamsV1 {
79
+ prompt: string;
80
+ cwd?: string;
81
+ tier?: ModelTier;
82
+ }
83
+
84
+ interface SpawnSubsessionDetailsV1 {
85
+ sessionId: string;
86
+ cwd: string;
87
+ }
88
+ ```
89
+
90
+ There is no `dispatchKey` parameter, no `reused` flag, and no returned
91
+ `policyApplication`. Parent→child lineage is durable through the parent session's
92
+ `pi-webui.subsession.spawned` custom entry, which `listSubsessions` rehydrates
93
+ from the persisted session file, so lineage survives a daemon restart. What the
94
+ runtime does not provide is *correlation of a repeated call to an earlier child*.
95
+
96
+ ### Tier binding
97
+
98
+ `tier` is the binding channel. A supplied tier resolves through the machine's
99
+ configured ladder to an exact model and thinking level, applied as model-then-
100
+ thinking before the child's first request. An unresolvable tier fails the spawn
101
+ without creating a child and without substituting a neighbouring tier. An omitted
102
+ `tier` inherits the parent's model.
103
+
104
+ Prompt text never selects a model. The runtime does not scan prompt bytes for
105
+ model-selection commands, so a `Model tier: <tier>` line is a human-readable echo
106
+ with zero control effect. A test fake that recovers a tier from this label is
107
+ exercising a channel the runtime does not implement, and cannot detect a child
108
+ that ignored the typed field.
109
+
110
+ The one exception is a guard, not a mechanism: a leading label that *disagrees*
111
+ with the typed `tier` is rejected before child creation, so a stale rendered
112
+ prompt cannot silently imply a tier that was not requested.
113
+
114
+ ### No dispatch idempotency
115
+
116
+ Repeating a spawn call creates a second child. The contract that consumers may
117
+ rely on is therefore **detectable** non-idempotency, not prevented duplication:
118
+
119
+ - `dispatchKey` is controller-owned. It names a row in the controller's own
120
+ dispatch ledger, is never sent to the tool, and exists so recovery can
121
+ correlate a recorded intent to the `sessionId` the tool returned.
122
+ - A crash between the spawn call and the ledger write can orphan a child. This
123
+ window cannot be closed client-side. It must be *visible*: an intent without a
124
+ recorded `sessionId` is ambiguous and requires an explicit ruling.
125
+ - Authority for whether work happened is commits and artifacts, never session
126
+ identity. A lost correlation degrades to inspecting `git log` and report files,
127
+ not to an unrecoverable run.
128
+
129
+ ### Fail-closed conditions
130
+
131
+ Missing required fields, unknown tier values, a tier absent from the configured
132
+ ladder, an unavailable model, and a leading tier label disagreeing with the typed
133
+ tier all fail before a child is created.
134
+
135
+ ## Recovery-input properties
136
+
137
+ **Recovery must never re-render the prompt.** Dispatch intent stores the rendered
138
+ prompt bytes, and a ruling to reissue sends those stored bytes verbatim.
139
+ Re-rendering couples recovery to renderer output, so any drift — including
140
+ interior drift such as an added blank line, which trimming cannot absorb — changes
141
+ what the child receives on a path whose whole purpose is exactness.
142
+
143
+ Storing the bytes is the entire mitigation. Earlier drafts additionally
144
+ fingerprinted `cwd` and prompt bytes for identity comparison and specified
145
+ normalization of byte-order marks, CRLF, and outer whitespace. With no
146
+ server-side deduplication there is nothing to compare against, so both the
147
+ fingerprint and its normalization rules are removed rather than kept as unused
148
+ ceremony.
149
+
150
+ ## Resolved divergence: the eval fake once modelled the withdrawn dedup contract
151
+
152
+ `evals/fake-sdd-tools.mjs` originally implemented an earlier draft of this file: a
153
+ `dispatchKey` parameter, a `reused` flag, conflicting-reuse rejection, and a
154
+ returned `policyApplication`. None of that exists in the runtime, so a fake
155
+ asserting it could only manufacture confidence in a channel the runtime never had.
156
+
157
+ That divergence is **closed**. The fake was realigned to the real contract before
158
+ the controller GREEN run: `spawn_subsession` takes `{ prompt, cwd, tier }`, returns
159
+ `{ sessionId, cwd }`, and creates a new child on every call with no deduplication.
160
+ The two controller scenarios that referenced the withdrawn fields
161
+ (`missing-capability-contract`, `dispatch-intent-crash-recovery`) were regenerated
162
+ against the real contract at the same time.
163
+
164
+ The prerequisite this section once stated -- realign the fake and regenerate those
165
+ scenarios before certifying controller behavior -- was therefore satisfied, not
166
+ waived. It is kept here as history because the reasoning still governs: a fake that
167
+ validates a channel the runtime does not implement must never be used to certify
168
+ controller behavior, and the version-1 field list above remains the authority on
169
+ what the fake is allowed to model.
170
+
171
+ The consequence for evidence is that controller results predating the realignment
172
+ are not comparable to results after it. The baseline reported in
173
+ `evals/baseline-report.md` was recorded against the older fake; `evals/green-report.md`
174
+ notes that limitation rather than presenting the two as a matched pair.
@@ -0,0 +1,268 @@
1
+ # Plan and Artifact Contract
2
+
3
+ What a plan must contain for the deterministic controller to run it, and what each
4
+ artifact may hold. The grammar here is **copied from** `scripts/lib/plan-policy.mjs`
5
+ and never widened; that module is the authority. Transitions live in
6
+ [`state-machine.md`](state-machine.md) and are not restated here.
7
+
8
+ ## Contents
9
+
10
+ - [Plan grammar](#plan-grammar)
11
+ - [Tiers and role formulas](#tiers-and-role-formulas)
12
+ - [Choosing the implementer tier](#choosing-the-implementer-tier)
13
+ - [Identity pinning](#identity-pinning)
14
+ - [Preflight](#preflight)
15
+ - [Report schemas](#report-schemas)
16
+ - [The fix package](#the-fix-package)
17
+ - [Artifact separation](#artifact-separation)
18
+ - [Bounds](#bounds)
19
+ - [Blocked-state recovery](#blocked-state-recovery)
20
+
21
+ ## Plan grammar
22
+
23
+ A task heading is exactly:
24
+
25
+ ```text
26
+ ## Task <N>: <Title>
27
+ ```
28
+
29
+ `<N>` starts at 1 and increases by 1 with no gaps. Any other task-like ATX heading
30
+ outside a code fence is an error, including `### Task 1:`. This matters in practice
31
+ because the `writing-plans` skill currently emits `### Task N:` with no tier field,
32
+ so a plan from that skill must be converted before this controller will accept it.
33
+ `validate-plan` names the depth found, the depth required, and the repair.
34
+
35
+ Each task carries exactly one tier field, outside any code fence:
36
+
37
+ ```text
38
+ **Implementer tier:** Advanced
39
+ ```
40
+
41
+ Title case in the plan document, lowercase on the wire. The parser normalizes at
42
+ that boundary so no dispatch site has to remember to.
43
+
44
+ An optional `## Global Constraints` section precedes the first task and appears at
45
+ most once. When present it is included in **every** task brief, because a child
46
+ that never sees the plan cannot infer a constraint stated only there.
47
+
48
+ Fence handling follows the parser exactly: fenced content is inert, so a fenced
49
+ `## Task 1:` example is not a task. Indented four-space blocks are ordinary
50
+ content.
51
+
52
+ **A tier-annotated plan is a precondition, not an inference.** A plan missing a
53
+ tier enters `PLAN_INVALID` with a diagnostic naming the repair. The controller
54
+ never guesses a tier: guessing is precisely what the typed `tier` parameter exists
55
+ to eliminate.
56
+
57
+ ## Tiers and role formulas
58
+
59
+ Six tiers, ascending: `economy`, `fast`, `standard`, `advanced`, `capable`,
60
+ `frontier`.
61
+
62
+ | Role | Tier |
63
+ | --- | --- |
64
+ | Implementer | the plan's `**Implementer tier:**` for that task |
65
+ | Task reviewer | implementer + 1, floored at `standard`, capped at `frontier` |
66
+ | Re-reviewer | same formula as the task reviewer |
67
+ | Fixer | implementer, + 1 rung at fix round 4, + 2 rungs at round 5 |
68
+ | Final reviewer, final fixer, final re-reviewer | always `frontier` |
69
+
70
+ ### Choosing the implementer tier
71
+
72
+ The controller never picks this value; a human does, while writing the plan and
73
+ seeing every task at once. Only the implementer is annotated, because the five
74
+ other roles derive from it. A plan that also names reviewer or fixer tiers invites
75
+ disagreement with `role-tier`, which is authoritative.
76
+
77
+ | Task shape | Tier |
78
+ | --- | --- |
79
+ | Plan text contains the complete code; the work is transcription plus running tests | `economy` |
80
+ | Single-file mechanical change against an exact spec | `fast` |
81
+ | One or two files, complete spec, no integration concerns | `standard` |
82
+ | Several files with integration concerns, or behavior described in prose rather than code | `advanced` |
83
+ | Requires design judgement or broad codebase understanding | `capable` |
84
+ | Not chosen by hand; final review always runs here | `frontier` |
85
+
86
+ Two rules override the table.
87
+
88
+ **`standard` is the floor whenever the implementer must decide anything.** Wall-clock
89
+ and context cost scale with turns taken, and the cheapest models routinely take two
90
+ to three times the turns on multi-step work, costing more in total than the tier
91
+ saved. `economy` is correct only when the plan hands over literal code to transcribe.
92
+
93
+ **A wrong tier is a cost and latency defect, not a correctness one.** Review gates,
94
+ not tier, are what catch bad implementations. Choose for throughput and spend, and
95
+ do not treat the annotation as a quality lever.
96
+
97
+ The escalation map is `{1:0, 2:0, 3:0, 4:1, 5:2}`. A consequence worth stating:
98
+ at round 5 the fixer can sit one rung **above** the re-reviewer, because the
99
+ re-reviewer formula takes no round. That is pinned in tests as a deliberate
100
+ decision; changing it requires changing this contract.
101
+
102
+ `tier` is the binding channel. The rendered prompt also opens with
103
+ `Model tier: <lowercase>` as a human-readable echo. The echo carries no control
104
+ effect — the runtime never parses prompt text to select a model — but PI WEBUI
105
+ rejects a leading label that *disagrees* with the typed tier, so the echo works as
106
+ a cross-check. An absent echo is not an error; a disagreeing one is.
107
+
108
+ ## Identity pinning
109
+
110
+ At init the run pins: the plan's SHA-256 digest, repo root, worktree, run root,
111
+ branch, base ref, and merge base. Then
112
+
113
+ ```text
114
+ runId = sha256(planDigest ⁰ worktree ⁰ branch ⁰ mergeBase ⁰ createdAt)
115
+ dispatchKey = <runId>:task-<n>:<role>:attempt-<n>[:round-<n>]
116
+ ```
117
+
118
+ (`⁰` is a NUL byte; no component may contain one.)
119
+
120
+ `dispatchKey` is **controller-owned**. It is never passed to `spawn_subsession`,
121
+ which accepts only `{ prompt, cwd, tier }` and returns `{ sessionId, cwd }`. The
122
+ key names a row in this run's own ledger so recovery can correlate an intent to
123
+ the session the tool returned. It buys correlation, not idempotency.
124
+
125
+ Every mutation recomputes the plan digest. Drift fails closed with exit 4: a plan
126
+ edited mid-run invalidates every tier already dispatched, so the run stops for a
127
+ human decision rather than continuing against a plan nobody reviewed.
128
+
129
+ **Ground truth for "was this work done" is Git commits and artifacts on disk**,
130
+ not session identity. This is inherited from the original SDD skill, and it is why
131
+ a lost correlation degrades to reading `git log` and report files rather than to an
132
+ unrecoverable run.
133
+
134
+ ## Preflight
135
+
136
+ The run wants a fresh worktree at a known merge base. If the tree is dirty or the
137
+ branch has unexpected commits, preflight reports a conflict and the run enters
138
+ `PREFLIGHT_DECISION_REQUIRED`. Leaving it needs a persisted ruling naming a
139
+ decision and a reason. Untracked build output is usually fine to proceed past;
140
+ uncommitted source changes usually are not. The controller does not decide that
141
+ silently.
142
+
143
+ ## Report schemas
144
+
145
+ Every child writes exactly one bounded report at the report path it was given, and
146
+ returns exactly one status.
147
+
148
+ **Implementer and fixer:**
149
+
150
+ ```text
151
+ STATUS: DONE | DONE_WITH_CONCERNS | NEEDS_CONTEXT | BLOCKED
152
+
153
+ CHANGES:
154
+ - <file>: <what changed and why>
155
+
156
+ TESTS:
157
+ - <command>: <result, with counts>
158
+
159
+ CONCERNS: (required when DONE_WITH_CONCERNS; the reducer rejects an
160
+ empty list, so a hedged status must name something)
161
+ - kind: observational | correctness | scope
162
+ note: <one line>
163
+
164
+ COMMIT: <sha> (when the role commits)
165
+ ```
166
+
167
+ `observational` concerns pass to review. A `correctness` or `scope` concern routes
168
+ to `CONCERN_DECISION_REQUIRED` and needs a ruling, because those two are findings
169
+ wearing a softer word.
170
+
171
+ `NEEDS_CONTEXT` must name what is missing and why it is load-bearing. Two
172
+ enrichments are allowed at the planned tier; a third blocks. Enrichment is not a
173
+ fix round and never touches `fixRound`.
174
+
175
+ **Task reviewer and re-reviewer:**
176
+
177
+ ```text
178
+ SPEC: PASS | FAIL
179
+ QUALITY: APPROVED | CHANGES_REQUESTED
180
+ FINDINGS:
181
+ - id: F-<n>
182
+ severity: Critical | Important | Minor
183
+ loadBearing: yes | no
184
+ location: <file>:<line>
185
+ evidence: <what was observed, not what was assumed>
186
+ impact: <consequence>
187
+ correction: <what would resolve it>
188
+ ```
189
+
190
+ Both axes are required and independent. Task completion needs `PASS` **and**
191
+ `APPROVED` with no open load-bearing finding.
192
+
193
+ `Critical` and `Important` are load-bearing: they open a fix round and can never be
194
+ parked. `Minor` is contestable and may be parked with a ruling that names evidence.
195
+ Severity is fixed when first reported and cannot be re-reported lower, which closes
196
+ the obvious route to dismissing a finding.
197
+
198
+ A re-reviewer returns one verdict per open finding — `RESOLVED`, `STILL_PRESENT`,
199
+ `REGRESSION`, or `NEEDS_CONTEXT` — scoped to that fix's Git range. It may report a
200
+ regression the fix introduced. It may not expand into a fresh whole-task review.
201
+
202
+ ## The fix package
203
+
204
+ Every fix round dispatches a **fresh child with no memory of prior rounds**. The
205
+ package must therefore carry:
206
+
207
+ 1. the task brief, including Global Constraints;
208
+ 2. the persistent implementer report;
209
+ 3. the exact open findings, by ID, with evidence;
210
+ 4. **each prior attempted correction and why it failed**;
211
+ 5. the relevant tests;
212
+ 6. the scoped diff for the range under repair.
213
+
214
+ Item 4 is the one that is easy to omit and expensive to omit. Without it, round 3
215
+ can re-apply the fix that failed in round 2, spend a full review cycle, and arrive
216
+ back at the same finding. The child must read that history and must not repeat a
217
+ correction already recorded as failed.
218
+
219
+ ## Artifact separation
220
+
221
+ | Artifact | Location | Writer |
222
+ | --- | --- | --- |
223
+ | Deliverables | the worktree | implementer, fixer |
224
+ | Reports | run root | each child, one file each |
225
+ | Task briefs | run root | `task-brief` |
226
+ | Review packages | run root | `review-package` |
227
+ | Rendered prompts | run root | `render-prompt` |
228
+ | `state.json` | run root | the store, under lock |
229
+ | `progress.md` | run root | the store, append-only |
230
+
231
+ `state.json` is canonical; `progress.md` is derived. Never hand-edit either. A
232
+ hand-edited state is indistinguishable from a corrupted one, and the reducer is
233
+ built to refuse exactly the plausible-looking repair a stuck controller would
234
+ otherwise invent.
235
+
236
+ ## Bounds
237
+
238
+ Each tested at the limit and one byte past it.
239
+
240
+ | Thing | Bound |
241
+ | --- | --- |
242
+ | Task brief | 256 KiB |
243
+ | Rendered prompt | 384 KiB |
244
+ | Child or reviewer report | 64 KiB |
245
+ | `state.json` | 1 MiB |
246
+ | One audit line | 8 KiB |
247
+ | Finding records | 256 |
248
+ | Any single path | 4096 UTF-8 bytes |
249
+ | Any recorded human string | 256 characters, single line |
250
+
251
+ Recorded strings reject control characters and the audit marker outright rather
252
+ than escaping them, so no reason text can forge a transition record.
253
+
254
+ ## Blocked-state recovery
255
+
256
+ | State | What it means | What clears it |
257
+ | --- | --- | --- |
258
+ | `CAPABILITY_BLOCKED` | tier resolution or the policy tool is unavailable | fix the environment; re-init |
259
+ | `PLAN_INVALID` | grammar, tier, or digest failure | repair the plan; re-init, since the digest changed |
260
+ | `PREFLIGHT_DECISION_REQUIRED` | the tree is not in the expected shape | a persisted preflight ruling |
261
+ | `DISPATCH_AMBIGUOUS` | the spawn/correlate window was crossed | a ruling: adopt an observed session, or reissue stored bytes |
262
+ | `DISPATCH_MISMATCH_BLOCKED` | a dispatch did not match its intent | human inspection; no automatic path |
263
+ | `TASK_BLOCKED` / `FINAL_BLOCKED` | terminal | a human decision outside the run |
264
+
265
+ `DISPATCH_AMBIGUOUS` deserves emphasis: the runtime offers no dispatch
266
+ idempotency, so a crash between the spawn call and the ledger write can orphan a
267
+ child. That window cannot be closed from the controller. The guarantee is that it
268
+ is always **visible** and never silently resolved.
@@ -0,0 +1,177 @@
1
+ # SDD State Machine Reference
2
+
3
+ The reducer in `scripts/lib/state-machine.mjs` is the authority. This document
4
+ describes it; it never redefines it. `TRANSITIONS` and `PHASES` are exported and
5
+ the test suite asserts this table against them, so drift fails a test rather than
6
+ misleading a reader.
7
+
8
+ ## Contents
9
+
10
+ - [Two rules that shape everything](#two-rules-that-shape-everything)
11
+ - [Canonical direction of truth](#canonical-direction-of-truth)
12
+ - [Phases](#phases)
13
+ - [Transitions](#transitions)
14
+ - [Counters and their bounds](#counters-and-their-bounds)
15
+ - [The finding ledger](#the-finding-ledger)
16
+ - [Dispatch identity and the ambiguity window](#dispatch-identity-and-the-ambiguity-window)
17
+ - [Recovery authority](#recovery-authority)
18
+
19
+ ## Two rules that shape everything
20
+
21
+ **Recording a result and deciding what it means are separate transitions.** A
22
+ `*-finished` event only pins a bounded artifact and a verdict. A separate,
23
+ explicit controller event selects the next phase. A child's report can never
24
+ choose the phase it leads to.
25
+
26
+ **Nothing is inferred from absence.** Every branch a human would call a judgement
27
+ call requires a persisted ruling naming a decision and a reason. The reducer never
28
+ picks the agreeable option by default.
29
+
30
+ Both rules come from measured failure, not taste. In the recorded baseline, both
31
+ conditions on `post-compaction-illegal-transition` produced the correct phase
32
+ token and then inverted the canonical-artifact rule and invented repair
33
+ mechanisms. One minted a fabricated dispatch key.
34
+
35
+ ## Canonical direction of truth
36
+
37
+ `state.json` is canonical. The progress ledger is an append-only audit trail
38
+ **derived from it**. This direction is not negotiable.
39
+
40
+ Authority for whether work actually happened is Git commits and artifacts on
41
+ disk, inherited from the original SDD skill: trust the ledger and `git log` over
42
+ your own recollection. A lost correlation between a dispatch and a session
43
+ degrades to inspecting commits and reports, never to an unrecoverable run.
44
+
45
+ ## Phases
46
+
47
+ 30 phases. Six are terminal and accept no continuation event.
48
+
49
+ | Group | Phases |
50
+ | --- | --- |
51
+ | Gates | `CAPABILITY_CHECK`, `PLAN_VALIDATE`, `PREFLIGHT_DECISION_REQUIRED`, `WORKSPACE_READY` |
52
+ | Task loop | `IMPLEMENT_DISPATCH_INTENT`, `IMPLEMENT_RUNNING`, `IMPLEMENT_RESULT`, `CONTEXT_REQUIRED`, `CONCERN_DECISION_REQUIRED` |
53
+ | Review loop | `TASK_REVIEW_DISPATCH_INTENT`, `TASK_REVIEW_RUNNING`, `TASK_REVIEW_DECISION` |
54
+ | Fix loop | `FIX_DISPATCH_INTENT`, `FIX_RUNNING`, `REREVIEW_DISPATCH_INTENT`, `REREVIEW_RUNNING`, `TASK_COMPLETE` |
55
+ | Final loop | `FINAL_REVIEW_DISPATCH_INTENT`, `FINAL_REVIEW_RUNNING`, `FINAL_FIX_DISPATCH_INTENT`, `FINAL_FIX_RUNNING`, `FINAL_REREVIEW_DISPATCH_INTENT`, `FINAL_REREVIEW_RUNNING` |
56
+ | Ambiguity | `DISPATCH_AMBIGUOUS` |
57
+ | Terminal | `CAPABILITY_BLOCKED`, `PLAN_INVALID`, `TASK_BLOCKED`, `DISPATCH_MISMATCH_BLOCKED`, `FINAL_BLOCKED`, `COMPLETE` |
58
+
59
+ ## Transitions
60
+
61
+ 61 registered `(phase, event)` pairs. Any pair absent from this table is an
62
+ illegal transition.
63
+
64
+ | Source | Event | Destination |
65
+ | --- | --- | --- |
66
+ | any nonterminal | `recovery-ruling-recorded` | same phase; requires reason and receipt |
67
+ | `CAPABILITY_CHECK` | `capability-confirmed` | `PLAN_VALIDATE` |
68
+ | `CAPABILITY_CHECK` | `capability-missing` | `CAPABILITY_BLOCKED` |
69
+ | `PLAN_VALIDATE` | `plan-valid` | `PLAN_VALIDATE`, validation pinned |
70
+ | `PLAN_VALIDATE` | `plan-invalid` / `plan-conflict` | `PLAN_INVALID` |
71
+ | `PLAN_VALIDATE` | `preflight-clean` | `WORKSPACE_READY` |
72
+ | `PLAN_VALIDATE` | `preflight-conflict` | `PREFLIGHT_DECISION_REQUIRED` |
73
+ | `PREFLIGHT_DECISION_REQUIRED` | `preflight-approved` | `WORKSPACE_READY` |
74
+ | `PREFLIGHT_DECISION_REQUIRED` | `preflight-rejected` | `FINAL_BLOCKED` |
75
+ | `WORKSPACE_READY` | `implement-dispatch-intended` | `IMPLEMENT_DISPATCH_INTENT` |
76
+ | any `*_DISPATCH_INTENT` | `dispatch-started` | matching `*_RUNNING` |
77
+ | any `*_DISPATCH_INTENT` | `dispatch-mismatch` | `DISPATCH_MISMATCH_BLOCKED` |
78
+ | any `*_DISPATCH_INTENT` | `dispatch-window-crossed` | `DISPATCH_AMBIGUOUS` |
79
+ | `DISPATCH_AMBIGUOUS` | `dispatch-ruling-recorded` | the recorded intent's running phase (adopt) or intent phase (reissue) |
80
+ | `IMPLEMENT_RUNNING` | `implementer-finished` | `IMPLEMENT_RESULT` |
81
+ | `IMPLEMENT_RESULT` | `implementer-status-recorded` | status-pinned `IMPLEMENT_RESULT`, `CONTEXT_REQUIRED`, `CONCERN_DECISION_REQUIRED`, or `TASK_BLOCKED` |
82
+ | status-pinned `IMPLEMENT_RESULT` | `task-review-dispatch-intended` | `TASK_REVIEW_DISPATCH_INTENT` |
83
+ | `CONTEXT_REQUIRED` | `context-dispatch-intended` | `IMPLEMENT_DISPATCH_INTENT` |
84
+ | `CONTEXT_REQUIRED` | `context-limit-reached` | `TASK_BLOCKED` |
85
+ | `CONCERN_DECISION_REQUIRED` | `concern-ruling-recorded` | status-pinned `IMPLEMENT_RESULT` or `TASK_BLOCKED` |
86
+ | `TASK_REVIEW_RUNNING` | `task-review-finished` | `TASK_REVIEW_DECISION` |
87
+ | `TASK_REVIEW_DECISION` | `review-approved` | `TASK_COMPLETE` |
88
+ | `TASK_REVIEW_DECISION` | `fix-dispatch-intended` | `FIX_DISPATCH_INTENT` |
89
+ | `TASK_REVIEW_DECISION` | `review-blocked` | `TASK_BLOCKED` |
90
+ | `FIX_RUNNING` | `rereview-dispatch-intended` | `REREVIEW_DISPATCH_INTENT` |
91
+ | `FIX_RUNNING` | `fixer-blocked` | `TASK_BLOCKED` |
92
+ | `REREVIEW_RUNNING` | `rereview-finished` | result-pinned `REREVIEW_RUNNING` |
93
+ | result-pinned `REREVIEW_RUNNING` | `rereview-approved` | `TASK_COMPLETE` |
94
+ | result-pinned `REREVIEW_RUNNING` | `task-park-ruling-recorded` | result-pinned `REREVIEW_RUNNING` |
95
+ | result-pinned `REREVIEW_RUNNING` | `next-fix-dispatch-intended` | `FIX_DISPATCH_INTENT` |
96
+ | result-pinned `REREVIEW_RUNNING` | `rereview-blocked` | `TASK_BLOCKED` |
97
+ | `TASK_COMPLETE` | `next-task-ready` | `WORKSPACE_READY` at the next task |
98
+ | `TASK_COMPLETE` | `final-review-dispatch-intended` | `FINAL_REVIEW_DISPATCH_INTENT` |
99
+ | `FINAL_REVIEW_RUNNING` | `final-review-finished` | result-pinned `FINAL_REVIEW_RUNNING` |
100
+ | result-pinned `FINAL_REVIEW_RUNNING` | `final-complete` | `COMPLETE` |
101
+ | result-pinned `FINAL_REVIEW_RUNNING` | `final-fix-dispatch-intended` | `FINAL_FIX_DISPATCH_INTENT` |
102
+ | result-pinned `FINAL_REVIEW_RUNNING` | `final-blocked` | `FINAL_BLOCKED` |
103
+ | `FINAL_FIX_RUNNING` | `final-rereview-dispatch-intended` | `FINAL_REREVIEW_DISPATCH_INTENT` |
104
+ | `FINAL_FIX_RUNNING` | `final-fixer-blocked` | `FINAL_BLOCKED` |
105
+ | `FINAL_REREVIEW_RUNNING` | `final-rereview-finished` | result-pinned `FINAL_REREVIEW_RUNNING` |
106
+ | result-pinned `FINAL_REREVIEW_RUNNING` | `final-complete` | `COMPLETE` |
107
+ | result-pinned `FINAL_REREVIEW_RUNNING` | `final-park-ruling-recorded` | result-pinned `FINAL_REREVIEW_RUNNING` |
108
+ | result-pinned `FINAL_REREVIEW_RUNNING` | `final-blocked` | `FINAL_BLOCKED` |
109
+
110
+ Every `*-dispatch-intended` event carries a full dispatch intent and enters its
111
+ named intent phase **before** any spawn. Task completion requires spec `PASS` and
112
+ quality `APPROVED` together, with no open load-bearing finding.
113
+
114
+ ## Counters and their bounds
115
+
116
+ | Field | Bound | Notes |
117
+ | --- | --- | --- |
118
+ | `contextAttempts` | 0–2 | A third `NEEDS_CONTEXT` must block. Never touches `fixRound`. |
119
+ | `fixRound` | 0–5 | The fixer tier escalates one rung at round 4 and two at round 5. A load-bearing residual at round 5 blocks. |
120
+ | `finalFixUsed` | one wave | A second final-fix wave is not legal. |
121
+ | `revision` | +1 per transition | Exactly one increment, including recovery rulings. |
122
+ | `recoveryRulings` | unbounded | Counted so interventions are visible in the audit trail. |
123
+
124
+ `currentImplementerTier` is derived **only** from the immutable task index
125
+ captured at initialization, never from a live re-parse.
126
+
127
+ ## The finding ledger
128
+
129
+ Findings are keyed by immutable ID. Severity is recorded at report time and can
130
+ never be re-reported at a different level, so a finding cannot be downgraded on
131
+ its way to being dismissed.
132
+
133
+ `Critical` and `Important` are load-bearing: they open a fix round and can never
134
+ be parked. `Minor` is contestable and may be parked with a persisted ruling that
135
+ names evidence.
136
+
137
+ Reporting is additive and may grow the ledger. Adjudication is a set operation
138
+ that may change dispositions but can neither add nor remove entries, which is the
139
+ retention guarantee: no event can silently drop an open, deferred, or parked
140
+ finding. Every disposition requires evidence.
141
+
142
+ ## Dispatch identity and the ambiguity window
143
+
144
+ `dispatchKey` is controller-owned, composed as
145
+ `<runId>:task-<n>:<role>:attempt-<n>[:round-<n>]`. It is **never** passed to
146
+ `spawn_subsession`, which accepts only `{ prompt, cwd, tier }` and returns
147
+ `{ sessionId, cwd }`.
148
+
149
+ The runtime provides no deduplication, so the key buys **correlation, not
150
+ idempotency**. A crash between the spawn call and the correlation write can orphan
151
+ a child. That window cannot be closed from here; it can only be made visible.
152
+ `DISPATCH_AMBIGUOUS` does exactly that, and leaving it requires an explicit
153
+ ruling: `adopt` names an observed session id, `reissue` sends the stored prompt
154
+ bytes and accepts a possible orphan. The reducer never picks.
155
+
156
+ An intent stores the exact rendered prompt bytes, bounded at 384 KiB, before any
157
+ session exists. Recovery reissues those bytes verbatim and never re-renders,
158
+ because re-rendering couples recovery to renderer output.
159
+
160
+ The typed `tier` selects the model. A leading `Model tier: <tier>` line is a
161
+ human-readable echo with no control effect: absent is fine, and disagreement with
162
+ the typed tier is reported as renderer/formula divergence.
163
+
164
+ ## Recovery authority
165
+
166
+ `recovery-ruling-recorded` is legal in any nonterminal phase, never changes the
167
+ phase, and requires both a reason and a receipt. It exists so an intervention
168
+ appears in the audit trail instead of hiding.
169
+
170
+ Terminal phases accept no continuation event. Recovery from a terminal phase is a
171
+ human decision made outside the run.
172
+
173
+ **Never hand-edit `state.json` or the progress ledger.** Every change goes through
174
+ a transition so the revision, audit line, and validation all advance together. A
175
+ hand-edited state is indistinguishable from a corrupted one, and the reducer is
176
+ built to refuse exactly the kind of plausible-looking repair a stuck controller
177
+ would otherwise invent.