@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.
- package/README.md +17 -2
- package/dist/cli.js +265 -32
- package/dist/cli.js.map +1 -1
- package/dist/client/assets/{CodeViewer-DLZCHlMU.js → CodeViewer-CAf_j27u.js} +1 -1
- package/dist/client/assets/{UnifiedDiffViewer-XCb8NlrO.js → UnifiedDiffViewer-wBjEJWa1.js} +1 -1
- package/dist/client/assets/{index-BaDGxg8F.js → index-5EtDb9nj.js} +782 -457
- package/dist/client/index.html +1 -1
- package/dist/config.js +48 -11
- package/dist/config.js.map +1 -1
- package/dist/pi-webui-plugins/workspace-tasks/pi-webui-plugin.js +1 -0
- package/dist/plugin-api.d.ts +2 -0
- package/dist/server/app.js +6 -1
- package/dist/server/app.js.map +1 -1
- package/dist/server/realtime/sessionEventHub.js +40 -5
- package/dist/server/realtime/sessionEventHub.js.map +1 -1
- package/dist/server/realtime/sessionStatusCoalescer.js +137 -0
- package/dist/server/realtime/sessionStatusCoalescer.js.map +1 -0
- package/dist/server/sessiond/sessionProxyRoutes.js +1 -0
- package/dist/server/sessiond/sessionProxyRoutes.js.map +1 -1
- package/dist/server/sessiond.js +39 -5
- package/dist/server/sessiond.js.map +1 -1
- package/dist/server/sessions/modelPolicyCapability.js +88 -0
- package/dist/server/sessions/modelPolicyCapability.js.map +1 -0
- package/dist/server/sessions/modelPolicyTool.js +31 -0
- package/dist/server/sessions/modelPolicyTool.js.map +1 -0
- package/dist/server/sessions/modelTierRegistry.js +14 -0
- package/dist/server/sessions/modelTierRegistry.js.map +1 -1
- package/dist/server/sessions/piSessionService.js +1643 -433
- package/dist/server/sessions/piSessionService.js.map +1 -1
- package/dist/server/sessions/sessionDefaultsRoutes.js +39 -0
- package/dist/server/sessions/sessionDefaultsRoutes.js.map +1 -1
- package/dist/server/sessions/sessionDefaultsService.js +36 -6
- package/dist/server/sessions/sessionDefaultsService.js.map +1 -1
- package/dist/server/sessions/sessionMetadataStore.js +107 -13
- package/dist/server/sessions/sessionMetadataStore.js.map +1 -1
- package/dist/server/sessions/sessionModelPolicy.js +118 -0
- package/dist/server/sessions/sessionModelPolicy.js.map +1 -0
- package/dist/server/sessions/sessionReorder.js +117 -0
- package/dist/server/sessions/sessionReorder.js.map +1 -0
- package/dist/server/sessions/sessionRouteFastifyOptions.js +8 -0
- package/dist/server/sessions/sessionRouteFastifyOptions.js.map +1 -0
- package/dist/server/sessions/sessionRoutes.js +208 -3
- package/dist/server/sessions/sessionRoutes.js.map +1 -1
- package/dist/server/sessions/starterModelPolicyPreferenceStore.js +164 -0
- package/dist/server/sessions/starterModelPolicyPreferenceStore.js.map +1 -0
- package/dist/server/sessions/utilityModelExtension.js +199 -0
- package/dist/server/sessions/utilityModelExtension.js.map +1 -0
- package/dist/server/sessions/utilityModelResolver.js +77 -0
- package/dist/server/sessions/utilityModelResolver.js.map +1 -0
- package/dist/server/sessions/utilityModelSettingsRoutes.js +62 -0
- package/dist/server/sessions/utilityModelSettingsRoutes.js.map +1 -0
- package/dist/server/sessions/utilityModelSettingsService.js +101 -0
- package/dist/server/sessions/utilityModelSettingsService.js.map +1 -0
- package/dist/server/skills/optionalSkillInstall.js +69 -0
- package/dist/server/skills/optionalSkillInstall.js.map +1 -0
- package/dist/server/skills/optionalSkillInstaller.js +148 -0
- package/dist/server/skills/optionalSkillInstaller.js.map +1 -0
- package/dist/shared/apiTypes.d.ts +111 -3
- package/dist/shared/apiTypes.js +9 -0
- package/dist/shared/apiTypes.js.map +1 -1
- package/dist/shared/capabilities.js +12 -0
- package/dist/shared/capabilities.js.map +1 -1
- package/dist/shared/federatedRoutes.js +5 -0
- package/dist/shared/federatedRoutes.js.map +1 -1
- package/docs/assets/pi-webui-desktop.png +0 -0
- package/docs/config.md +38 -3
- package/optional-skills/deterministic-subagent-driven-development/SKILL.md +224 -0
- package/optional-skills/deterministic-subagent-driven-development/pi-webui-skill.json +28 -0
- package/optional-skills/deterministic-subagent-driven-development/prompts/final-reviewer.md +132 -0
- package/optional-skills/deterministic-subagent-driven-development/prompts/implementer.md +101 -0
- package/optional-skills/deterministic-subagent-driven-development/prompts/re-reviewer.md +60 -0
- package/optional-skills/deterministic-subagent-driven-development/prompts/task-reviewer.md +80 -0
- package/optional-skills/deterministic-subagent-driven-development/references/capability-contract.md +174 -0
- package/optional-skills/deterministic-subagent-driven-development/references/plan-contract.md +268 -0
- package/optional-skills/deterministic-subagent-driven-development/references/state-machine.md +177 -0
- package/optional-skills/deterministic-subagent-driven-development/scripts/lib/manifest.mjs +258 -0
- package/optional-skills/deterministic-subagent-driven-development/scripts/lib/plan-policy.mjs +350 -0
- package/optional-skills/deterministic-subagent-driven-development/scripts/lib/prompt-renderer.mjs +290 -0
- package/optional-skills/deterministic-subagent-driven-development/scripts/lib/state-machine.mjs +1264 -0
- package/optional-skills/deterministic-subagent-driven-development/scripts/lib/state-store.mjs +532 -0
- package/optional-skills/deterministic-subagent-driven-development/scripts/sdd-state +3 -0
- package/optional-skills/deterministic-subagent-driven-development/scripts/sdd-state.mjs +349 -0
- package/optional-skills/deterministic-writing-plans/SKILL.md +232 -0
- package/optional-skills/deterministic-writing-plans/references/grammar.md +84 -0
- package/optional-skills/deterministic-writing-plans/templates/plan-skeleton.md +143 -0
- package/package.json +12 -9
package/optional-skills/deterministic-subagent-driven-development/references/capability-contract.md
ADDED
|
@@ -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.
|