@hyperdreamer/pi-webui 1.10.7 → 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-BIb2Wx8W.js → CodeViewer-CAf_j27u.js} +1 -1
- package/dist/client/assets/{UnifiedDiffViewer-DpWiGlQo.js → UnifiedDiffViewer-wBjEJWa1.js} +1 -1
- package/dist/client/assets/{index-DX583DEv.js → index-5EtDb9nj.js} +1197 -476
- package/dist/client/index.html +1 -1
- package/dist/config.js +52 -11
- package/dist/config.js.map +1 -1
- package/dist/pi-webui-plugins/workspace-memory/pi-webui-plugin.js +3 -2
- package/dist/pi-webui-plugins/workspace-tasks/pi-webui-plugin.js +1 -0
- package/dist/plugin-api.d.ts +30 -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 +2 -0
- package/dist/server/sessiond/sessionProxyRoutes.js.map +1 -1
- package/dist/server/sessiond.js +57 -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/modelTierSettingsRoutes.js +36 -0
- package/dist/server/sessions/modelTierSettingsRoutes.js.map +1 -0
- package/dist/server/sessions/modelTierSettingsService.js +100 -0
- package/dist/server/sessions/modelTierSettingsService.js.map +1 -0
- 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 +129 -3
- package/dist/shared/apiTypes.js +10 -0
- package/dist/shared/apiTypes.js.map +1 -1
- package/dist/shared/capabilities.js +15 -0
- package/dist/shared/capabilities.js.map +1 -1
- package/dist/shared/federatedRoutes.js +7 -0
- package/dist/shared/federatedRoutes.js.map +1 -1
- package/docs/assets/pi-webui-desktop.png +0 -0
- package/docs/config.md +61 -3
- package/docs/plugins.md +93 -16
- 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 +13 -9
|
@@ -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.
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime ownership manifest for the optional deterministic SDD skill.
|
|
3
|
+
*
|
|
4
|
+
* The manifest answers two questions a consumer cannot otherwise answer: which
|
|
5
|
+
* package owns this skill, and whether the runtime tree on disk is the tree that
|
|
6
|
+
* was generated. It deliberately does not answer "is this up to date" -- that
|
|
7
|
+
* comparison belongs to an installer, which knows the installed version.
|
|
8
|
+
*
|
|
9
|
+
* The runtime list is explicit rather than globbed. A glob would silently absorb
|
|
10
|
+
* a new file into the shipped surface; an explicit list makes every addition a
|
|
11
|
+
* reviewed edit.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { createHash } from "node:crypto";
|
|
15
|
+
import { readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
16
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
17
|
+
|
|
18
|
+
/** Manifest schema version. Bump only for a breaking shape change. */
|
|
19
|
+
export const MANIFEST_SCHEMA_VERSION = 1;
|
|
20
|
+
|
|
21
|
+
/** Canonical skill name. Must match the `name` in `SKILL.md` frontmatter. */
|
|
22
|
+
export const SKILL_NAME = "deterministic-subagent-driven-development";
|
|
23
|
+
|
|
24
|
+
/** Distribution posture. `opt-in` means never auto-registered by the package. */
|
|
25
|
+
export const DISTRIBUTION = "opt-in";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Hash algorithm identifier.
|
|
29
|
+
*
|
|
30
|
+
* `sha256-path-nul-bytes-v1`: for each runtime file in sorted relative-path
|
|
31
|
+
* order, absorb the UTF-8 relative path, a NUL, the raw file bytes, and a NUL.
|
|
32
|
+
* The NUL delimiters make the encoding unambiguous: without them, a rename that
|
|
33
|
+
* shifted bytes between path and content could preserve the digest.
|
|
34
|
+
*/
|
|
35
|
+
export const RUNTIME_HASH_ALGORITHM = "sha256-path-nul-bytes-v1";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Files that constitute the runtime skill, sorted.
|
|
39
|
+
*
|
|
40
|
+
* Excludes `evals/` and `tests/` by construction: those carry fake capability
|
|
41
|
+
* tools and adversarial prompts that must never reach a consumer's skill tree.
|
|
42
|
+
*/
|
|
43
|
+
export const RUNTIME_FILES = Object.freeze([
|
|
44
|
+
"SKILL.md",
|
|
45
|
+
"prompts/final-reviewer.md",
|
|
46
|
+
"prompts/implementer.md",
|
|
47
|
+
"prompts/re-reviewer.md",
|
|
48
|
+
"prompts/task-reviewer.md",
|
|
49
|
+
"references/capability-contract.md",
|
|
50
|
+
"references/plan-contract.md",
|
|
51
|
+
"references/state-machine.md",
|
|
52
|
+
"scripts/lib/manifest.mjs",
|
|
53
|
+
"scripts/lib/plan-policy.mjs",
|
|
54
|
+
"scripts/lib/prompt-renderer.mjs",
|
|
55
|
+
"scripts/lib/state-machine.mjs",
|
|
56
|
+
"scripts/lib/state-store.mjs",
|
|
57
|
+
"scripts/sdd-state",
|
|
58
|
+
"scripts/sdd-state.mjs",
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
const SEMVER_PATTERN =
|
|
62
|
+
/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u;
|
|
63
|
+
|
|
64
|
+
/** Raised for any manifest validation or integrity failure. */
|
|
65
|
+
export class ManifestError extends Error {
|
|
66
|
+
constructor(message) {
|
|
67
|
+
super(message);
|
|
68
|
+
this.name = "ManifestError";
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Reject a runtime entry that is absolute, traverses upward, duplicates another,
|
|
74
|
+
* or names excluded development evidence.
|
|
75
|
+
*
|
|
76
|
+
* Validation runs before any hashing so a rejected list never produces a digest.
|
|
77
|
+
*/
|
|
78
|
+
export function assertRuntimeList(runtimeFiles) {
|
|
79
|
+
if (!Array.isArray(runtimeFiles) || runtimeFiles.length === 0) {
|
|
80
|
+
throw new ManifestError("runtime file list must be a non-empty array");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const seen = new Set();
|
|
84
|
+
for (const entry of runtimeFiles) {
|
|
85
|
+
if (typeof entry !== "string" || entry.length === 0) {
|
|
86
|
+
throw new ManifestError("runtime entries must be non-empty strings");
|
|
87
|
+
}
|
|
88
|
+
if (isAbsolute(entry) || entry.startsWith("/")) {
|
|
89
|
+
throw new ManifestError(`runtime entry must be relative: ${entry}`);
|
|
90
|
+
}
|
|
91
|
+
if (entry.includes("\\")) {
|
|
92
|
+
throw new ManifestError(`runtime entry must use forward slashes: ${entry}`);
|
|
93
|
+
}
|
|
94
|
+
const segments = entry.split("/");
|
|
95
|
+
if (segments.includes("..") || segments.includes(".") || segments.includes("")) {
|
|
96
|
+
throw new ManifestError(`runtime entry must be a normalized path: ${entry}`);
|
|
97
|
+
}
|
|
98
|
+
if (entry.startsWith("evals/") || entry.startsWith("tests/")) {
|
|
99
|
+
throw new ManifestError(`runtime entry must not ship development evidence: ${entry}`);
|
|
100
|
+
}
|
|
101
|
+
if (seen.has(entry)) {
|
|
102
|
+
throw new ManifestError(`duplicate runtime entry: ${entry}`);
|
|
103
|
+
}
|
|
104
|
+
seen.add(entry);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const sorted = [...runtimeFiles].sort();
|
|
108
|
+
if (sorted.some((entry, index) => entry !== runtimeFiles[index])) {
|
|
109
|
+
throw new ManifestError("runtime file list must be sorted");
|
|
110
|
+
}
|
|
111
|
+
return sorted;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Resolve a runtime entry against the source root.
|
|
116
|
+
*
|
|
117
|
+
* There is no second escape check here on purpose. `assertRuntimeList` runs first
|
|
118
|
+
* on every path into this module and rejects absolute paths, backslashes, and any
|
|
119
|
+
* `..` segment, so a validated entry cannot escape. A redundant guard here would
|
|
120
|
+
* be unreachable, and an unreachable guard is worse than none: no test can pin it,
|
|
121
|
+
* so it silently rots while implying a protection it never performs.
|
|
122
|
+
*
|
|
123
|
+
* Symlinks are a separate matter and deliberately not handled: the runtime list is
|
|
124
|
+
* a frozen constant in this file, not caller input, so there is no untrusted path
|
|
125
|
+
* by which a runtime entry could become a link out of the tree.
|
|
126
|
+
*/
|
|
127
|
+
function resolveInside(sourceRoot, relativePath) {
|
|
128
|
+
return join(resolve(sourceRoot), relativePath);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Compute the runtime digest over the validated, sorted list.
|
|
133
|
+
*
|
|
134
|
+
* Missing files surface as a `ManifestError` rather than a raw ENOENT, because a
|
|
135
|
+
* manifest naming a file that does not exist is a manifest defect.
|
|
136
|
+
*/
|
|
137
|
+
export function computeRuntimeHash(sourceRoot, runtimeFiles = RUNTIME_FILES) {
|
|
138
|
+
const sorted = assertRuntimeList(runtimeFiles);
|
|
139
|
+
const hash = createHash("sha256");
|
|
140
|
+
for (const relativePath of sorted) {
|
|
141
|
+
const absolutePath = resolveInside(sourceRoot, relativePath);
|
|
142
|
+
let bytes;
|
|
143
|
+
try {
|
|
144
|
+
bytes = readFileSync(absolutePath);
|
|
145
|
+
} catch (cause) {
|
|
146
|
+
throw new ManifestError(`runtime file is missing: ${relativePath}`, { cause });
|
|
147
|
+
}
|
|
148
|
+
hash.update(Buffer.from(relativePath, "utf8"));
|
|
149
|
+
hash.update(Buffer.from([0]));
|
|
150
|
+
hash.update(bytes);
|
|
151
|
+
hash.update(Buffer.from([0]));
|
|
152
|
+
}
|
|
153
|
+
return hash.digest("hex");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Read the owning package's name and version, validating the version is semver. */
|
|
157
|
+
function readSourcePackage(packageJsonPath) {
|
|
158
|
+
let parsed;
|
|
159
|
+
try {
|
|
160
|
+
parsed = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
161
|
+
} catch (cause) {
|
|
162
|
+
throw new ManifestError(`cannot read package manifest: ${packageJsonPath}`, { cause });
|
|
163
|
+
}
|
|
164
|
+
if (typeof parsed.name !== "string" || parsed.name.length === 0) {
|
|
165
|
+
throw new ManifestError("package manifest has no name");
|
|
166
|
+
}
|
|
167
|
+
if (typeof parsed.version !== "string" || !SEMVER_PATTERN.test(parsed.version)) {
|
|
168
|
+
throw new ManifestError(`package version is not valid semver: ${String(parsed.version)}`);
|
|
169
|
+
}
|
|
170
|
+
return { name: parsed.name, version: parsed.version };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Build the manifest object without writing it. */
|
|
174
|
+
export function buildManifest({ sourceRoot, packageJsonPath, runtimeFiles = RUNTIME_FILES }) {
|
|
175
|
+
const sorted = assertRuntimeList(runtimeFiles);
|
|
176
|
+
const sourcePackage = readSourcePackage(packageJsonPath);
|
|
177
|
+
// Hash last: a validation failure must never leave a digest to be trusted.
|
|
178
|
+
const runtimeHash = computeRuntimeHash(sourceRoot, sorted);
|
|
179
|
+
return {
|
|
180
|
+
schemaVersion: MANIFEST_SCHEMA_VERSION,
|
|
181
|
+
name: SKILL_NAME,
|
|
182
|
+
distribution: DISTRIBUTION,
|
|
183
|
+
sourcePackage,
|
|
184
|
+
runtimeHashAlgorithm: RUNTIME_HASH_ALGORITHM,
|
|
185
|
+
runtimeHash,
|
|
186
|
+
runtimeFiles: sorted,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Write the manifest atomically.
|
|
192
|
+
*
|
|
193
|
+
* The digest is computed fully before the first byte is written, and the file
|
|
194
|
+
* appears via `rename`, so a reader never observes a partial manifest or a
|
|
195
|
+
* placeholder digest. There is no intermediate sentinel hash to leak.
|
|
196
|
+
*/
|
|
197
|
+
export function writeManifest({ sourceRoot, packageJsonPath, outputPath, runtimeFiles }) {
|
|
198
|
+
const manifest = buildManifest({ sourceRoot, packageJsonPath, runtimeFiles });
|
|
199
|
+
const serialized = `${JSON.stringify(manifest, null, 2)}\n`;
|
|
200
|
+
const temporaryPath = `${outputPath}.tmp-${process.pid.toString(36)}`;
|
|
201
|
+
writeFileSync(temporaryPath, serialized, { encoding: "utf8", mode: 0o644 });
|
|
202
|
+
renameSync(temporaryPath, outputPath);
|
|
203
|
+
return manifest;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Parse and structurally validate a manifest file. */
|
|
207
|
+
export function readManifest(manifestPath) {
|
|
208
|
+
let parsed;
|
|
209
|
+
try {
|
|
210
|
+
parsed = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
211
|
+
} catch (cause) {
|
|
212
|
+
throw new ManifestError(`cannot read manifest: ${manifestPath}`, { cause });
|
|
213
|
+
}
|
|
214
|
+
if (parsed.schemaVersion !== MANIFEST_SCHEMA_VERSION) {
|
|
215
|
+
throw new ManifestError(`unsupported manifest schema version: ${String(parsed.schemaVersion)}`);
|
|
216
|
+
}
|
|
217
|
+
if (parsed.name !== SKILL_NAME) {
|
|
218
|
+
throw new ManifestError(`unexpected skill name: ${String(parsed.name)}`);
|
|
219
|
+
}
|
|
220
|
+
if (parsed.distribution !== DISTRIBUTION) {
|
|
221
|
+
throw new ManifestError(`unexpected distribution: ${String(parsed.distribution)}`);
|
|
222
|
+
}
|
|
223
|
+
if (parsed.runtimeHashAlgorithm !== RUNTIME_HASH_ALGORITHM) {
|
|
224
|
+
throw new ManifestError(
|
|
225
|
+
`unsupported runtime hash algorithm: ${String(parsed.runtimeHashAlgorithm)}`,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
if (typeof parsed.runtimeHash !== "string" || !/^[0-9a-f]{64}$/u.test(parsed.runtimeHash)) {
|
|
229
|
+
throw new ManifestError("runtime hash must be 64 lowercase hex characters");
|
|
230
|
+
}
|
|
231
|
+
assertRuntimeList(parsed.runtimeFiles);
|
|
232
|
+
return parsed;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Recompute the digest from the tree beside the manifest and compare.
|
|
237
|
+
*
|
|
238
|
+
* Returns the manifest and digest on success; throws on mismatch. The source root
|
|
239
|
+
* is the manifest's own directory, so verification cannot be pointed at a
|
|
240
|
+
* different tree by accident.
|
|
241
|
+
*/
|
|
242
|
+
export function verifyManifest(manifestPath, sourceRoot) {
|
|
243
|
+
const manifest = readManifest(manifestPath);
|
|
244
|
+
const actual = computeRuntimeHash(sourceRoot, manifest.runtimeFiles);
|
|
245
|
+
if (actual !== manifest.runtimeHash) {
|
|
246
|
+
throw new ManifestError(
|
|
247
|
+
`runtime hash mismatch: manifest records ${manifest.runtimeHash} but the tree hashes to ${actual}`,
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
return { manifest, runtimeHash: actual };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Absolute paths of the manifest's runtime files, for callers that copy the tree. */
|
|
254
|
+
export function runtimeFilePaths(sourceRoot, runtimeFiles = RUNTIME_FILES) {
|
|
255
|
+
return assertRuntimeList(runtimeFiles).map((relativePath) =>
|
|
256
|
+
join(resolve(sourceRoot), relativePath),
|
|
257
|
+
);
|
|
258
|
+
}
|