@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.
Files changed (92) 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-BIb2Wx8W.js → CodeViewer-CAf_j27u.js} +1 -1
  5. package/dist/client/assets/{UnifiedDiffViewer-DpWiGlQo.js → UnifiedDiffViewer-wBjEJWa1.js} +1 -1
  6. package/dist/client/assets/{index-DX583DEv.js → index-5EtDb9nj.js} +1197 -476
  7. package/dist/client/index.html +1 -1
  8. package/dist/config.js +52 -11
  9. package/dist/config.js.map +1 -1
  10. package/dist/pi-webui-plugins/workspace-memory/pi-webui-plugin.js +3 -2
  11. package/dist/pi-webui-plugins/workspace-tasks/pi-webui-plugin.js +1 -0
  12. package/dist/plugin-api.d.ts +30 -0
  13. package/dist/server/app.js +6 -1
  14. package/dist/server/app.js.map +1 -1
  15. package/dist/server/realtime/sessionEventHub.js +40 -5
  16. package/dist/server/realtime/sessionEventHub.js.map +1 -1
  17. package/dist/server/realtime/sessionStatusCoalescer.js +137 -0
  18. package/dist/server/realtime/sessionStatusCoalescer.js.map +1 -0
  19. package/dist/server/sessiond/sessionProxyRoutes.js +2 -0
  20. package/dist/server/sessiond/sessionProxyRoutes.js.map +1 -1
  21. package/dist/server/sessiond.js +57 -5
  22. package/dist/server/sessiond.js.map +1 -1
  23. package/dist/server/sessions/modelPolicyCapability.js +88 -0
  24. package/dist/server/sessions/modelPolicyCapability.js.map +1 -0
  25. package/dist/server/sessions/modelPolicyTool.js +31 -0
  26. package/dist/server/sessions/modelPolicyTool.js.map +1 -0
  27. package/dist/server/sessions/modelTierRegistry.js +14 -0
  28. package/dist/server/sessions/modelTierRegistry.js.map +1 -1
  29. package/dist/server/sessions/modelTierSettingsRoutes.js +36 -0
  30. package/dist/server/sessions/modelTierSettingsRoutes.js.map +1 -0
  31. package/dist/server/sessions/modelTierSettingsService.js +100 -0
  32. package/dist/server/sessions/modelTierSettingsService.js.map +1 -0
  33. package/dist/server/sessions/piSessionService.js +1643 -433
  34. package/dist/server/sessions/piSessionService.js.map +1 -1
  35. package/dist/server/sessions/sessionDefaultsRoutes.js +39 -0
  36. package/dist/server/sessions/sessionDefaultsRoutes.js.map +1 -1
  37. package/dist/server/sessions/sessionDefaultsService.js +36 -6
  38. package/dist/server/sessions/sessionDefaultsService.js.map +1 -1
  39. package/dist/server/sessions/sessionMetadataStore.js +107 -13
  40. package/dist/server/sessions/sessionMetadataStore.js.map +1 -1
  41. package/dist/server/sessions/sessionModelPolicy.js +118 -0
  42. package/dist/server/sessions/sessionModelPolicy.js.map +1 -0
  43. package/dist/server/sessions/sessionReorder.js +117 -0
  44. package/dist/server/sessions/sessionReorder.js.map +1 -0
  45. package/dist/server/sessions/sessionRouteFastifyOptions.js +8 -0
  46. package/dist/server/sessions/sessionRouteFastifyOptions.js.map +1 -0
  47. package/dist/server/sessions/sessionRoutes.js +208 -3
  48. package/dist/server/sessions/sessionRoutes.js.map +1 -1
  49. package/dist/server/sessions/starterModelPolicyPreferenceStore.js +164 -0
  50. package/dist/server/sessions/starterModelPolicyPreferenceStore.js.map +1 -0
  51. package/dist/server/sessions/utilityModelExtension.js +199 -0
  52. package/dist/server/sessions/utilityModelExtension.js.map +1 -0
  53. package/dist/server/sessions/utilityModelResolver.js +77 -0
  54. package/dist/server/sessions/utilityModelResolver.js.map +1 -0
  55. package/dist/server/sessions/utilityModelSettingsRoutes.js +62 -0
  56. package/dist/server/sessions/utilityModelSettingsRoutes.js.map +1 -0
  57. package/dist/server/sessions/utilityModelSettingsService.js +101 -0
  58. package/dist/server/sessions/utilityModelSettingsService.js.map +1 -0
  59. package/dist/server/skills/optionalSkillInstall.js +69 -0
  60. package/dist/server/skills/optionalSkillInstall.js.map +1 -0
  61. package/dist/server/skills/optionalSkillInstaller.js +148 -0
  62. package/dist/server/skills/optionalSkillInstaller.js.map +1 -0
  63. package/dist/shared/apiTypes.d.ts +129 -3
  64. package/dist/shared/apiTypes.js +10 -0
  65. package/dist/shared/apiTypes.js.map +1 -1
  66. package/dist/shared/capabilities.js +15 -0
  67. package/dist/shared/capabilities.js.map +1 -1
  68. package/dist/shared/federatedRoutes.js +7 -0
  69. package/dist/shared/federatedRoutes.js.map +1 -1
  70. package/docs/assets/pi-webui-desktop.png +0 -0
  71. package/docs/config.md +61 -3
  72. package/docs/plugins.md +93 -16
  73. package/optional-skills/deterministic-subagent-driven-development/SKILL.md +224 -0
  74. package/optional-skills/deterministic-subagent-driven-development/pi-webui-skill.json +28 -0
  75. package/optional-skills/deterministic-subagent-driven-development/prompts/final-reviewer.md +132 -0
  76. package/optional-skills/deterministic-subagent-driven-development/prompts/implementer.md +101 -0
  77. package/optional-skills/deterministic-subagent-driven-development/prompts/re-reviewer.md +60 -0
  78. package/optional-skills/deterministic-subagent-driven-development/prompts/task-reviewer.md +80 -0
  79. package/optional-skills/deterministic-subagent-driven-development/references/capability-contract.md +174 -0
  80. package/optional-skills/deterministic-subagent-driven-development/references/plan-contract.md +268 -0
  81. package/optional-skills/deterministic-subagent-driven-development/references/state-machine.md +177 -0
  82. package/optional-skills/deterministic-subagent-driven-development/scripts/lib/manifest.mjs +258 -0
  83. package/optional-skills/deterministic-subagent-driven-development/scripts/lib/plan-policy.mjs +350 -0
  84. package/optional-skills/deterministic-subagent-driven-development/scripts/lib/prompt-renderer.mjs +290 -0
  85. package/optional-skills/deterministic-subagent-driven-development/scripts/lib/state-machine.mjs +1264 -0
  86. package/optional-skills/deterministic-subagent-driven-development/scripts/lib/state-store.mjs +532 -0
  87. package/optional-skills/deterministic-subagent-driven-development/scripts/sdd-state +3 -0
  88. package/optional-skills/deterministic-subagent-driven-development/scripts/sdd-state.mjs +349 -0
  89. package/optional-skills/deterministic-writing-plans/SKILL.md +232 -0
  90. package/optional-skills/deterministic-writing-plans/references/grammar.md +84 -0
  91. package/optional-skills/deterministic-writing-plans/templates/plan-skeleton.md +143 -0
  92. package/package.json +13 -9
@@ -0,0 +1,349 @@
1
+ /**
2
+ * Facade and CLI for the deterministic SDD state helper.
3
+ *
4
+ * Importing this module has no side effects: it neither writes output nor sets an
5
+ * exit code. All parsing and tier logic lives in `lib/plan-policy.mjs`; this file
6
+ * only adapts that logic to argv.
7
+ */
8
+
9
+ import { createHash } from "node:crypto";
10
+ import { readFileSync, realpathSync, renameSync, writeFileSync } from "node:fs";
11
+ import { dirname, resolve } from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+
14
+ /** The skill root, resolved from this module so cwd never affects rendering. */
15
+ const SKILL_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
16
+
17
+ import { verifyManifest, writeManifest } from "./lib/manifest.mjs";
18
+ import { parsePlanText, roleTier } from "./lib/plan-policy.mjs";
19
+ import { renderPrompt } from "./lib/prompt-renderer.mjs";
20
+ import {
21
+ clearStaleLock,
22
+ EXIT,
23
+ initRun,
24
+ lockStatus,
25
+ repairAudit,
26
+ show,
27
+ StoreError,
28
+ transition,
29
+ } from "./lib/state-store.mjs";
30
+
31
+ export {
32
+ assertRuntimeList,
33
+ buildManifest,
34
+ computeRuntimeHash,
35
+ ManifestError,
36
+ readManifest,
37
+ RUNTIME_FILES,
38
+ runtimeFilePaths,
39
+ verifyManifest,
40
+ writeManifest,
41
+ } from "./lib/manifest.mjs";
42
+
43
+ export { renderPrompt } from "./lib/prompt-renderer.mjs";
44
+
45
+ export {
46
+ clearStaleLock,
47
+ EXIT,
48
+ initRun,
49
+ lockStatus,
50
+ repairAudit,
51
+ show,
52
+ StoreError,
53
+ transition,
54
+ } from "./lib/state-store.mjs";
55
+
56
+ export {
57
+ computeRunId,
58
+ createInitialState,
59
+ dispatchKeyFor,
60
+ EVENT_TYPES,
61
+ PHASES,
62
+ reduceState,
63
+ StateError,
64
+ STATE_VERSION,
65
+ TERMINAL_PHASES,
66
+ TRANSITIONS,
67
+ validateState,
68
+ } from "./lib/state-machine.mjs";
69
+
70
+ export {
71
+ finalReviewerTier,
72
+ fixerTier,
73
+ parsePlanText,
74
+ reReviewerTier,
75
+ reviewerTier,
76
+ roleTier,
77
+ tierDirective,
78
+ tierEcho,
79
+ tierLabel,
80
+ TIERS,
81
+ } from "./lib/plan-policy.mjs";
82
+
83
+ const USAGE = [
84
+ "usage:",
85
+ " sdd-state validate-plan PLAN_FILE",
86
+ " sdd-state role-tier --implementer TIER --role ROLE [--round N]",
87
+ " sdd-state init --plan PLAN --state STATE --progress PROGRESS --repo-root ROOT",
88
+ " --worktree TREE --branch BRANCH --base-ref REF --merge-base SHA",
89
+ " sdd-state show --state STATE --progress PROGRESS",
90
+ " sdd-state transition --state STATE --progress PROGRESS --plan PLAN",
91
+ " --expected-revision N --event-file EVENT_JSON",
92
+ " sdd-state repair-audit --state STATE --progress PROGRESS",
93
+ " sdd-state lock-status --state STATE",
94
+ " sdd-state clear-stale-lock --state STATE --expected-owner-token TOKEN",
95
+ " --decision-file DECISION_JSON",
96
+ " sdd-state render-prompt --tier TIER --role ROLE --context CONTEXT_JSON",
97
+ " --output PROMPT_FILE",
98
+ " sdd-state manifest-create --source-root SOURCE --package-json PACKAGE_JSON",
99
+ " --output MANIFEST",
100
+ " sdd-state manifest-hash --manifest MANIFEST",
101
+ ].join("\n");
102
+
103
+ /** Require a set of flags, naming every missing one at once. */
104
+ function requireFlags(flags, names) {
105
+ const missing = names.filter((name) => flags.get(name) === undefined);
106
+ if (missing.length > 0) {
107
+ throw new Error(`missing required flag(s): ${missing.map((n) => `--${n}`).join(", ")}\n${USAGE}`);
108
+ }
109
+ return Object.fromEntries(names.map((name) => [name, flags.get(name)]));
110
+ }
111
+
112
+ function parseFlags(args) {
113
+ const flags = new Map();
114
+ for (let index = 0; index < args.length; index += 1) {
115
+ const token = args[index];
116
+ if (!token.startsWith("--")) throw new Error(`unexpected argument: ${token}`);
117
+ const value = args[index + 1];
118
+ if (value === undefined || value.startsWith("--")) {
119
+ throw new Error(`flag ${token} requires a value`);
120
+ }
121
+ flags.set(token.slice(2), value);
122
+ index += 1;
123
+ }
124
+ return flags;
125
+ }
126
+
127
+ function validatePlanCommand(args) {
128
+ const planPath = args[0];
129
+ if (planPath === undefined || args.length > 1) throw new Error(USAGE);
130
+
131
+ const bytes = readFileSync(planPath);
132
+ const parsed = parsePlanText(bytes.toString("utf8"), planPath);
133
+ return {
134
+ planPath,
135
+ planDigest: createHash("sha256").update(bytes).digest("hex"),
136
+ globalConstraints: parsed.globalConstraints,
137
+ tasks: parsed.tasks.map((task) => ({
138
+ number: task.number,
139
+ title: task.title,
140
+ implementerTier: task.implementerTier,
141
+ })),
142
+ };
143
+ }
144
+
145
+ function roleTierCommand(args) {
146
+ const flags = parseFlags(args);
147
+ const implementer = flags.get("implementer");
148
+ const role = flags.get("role");
149
+ if (implementer === undefined || role === undefined) throw new Error(USAGE);
150
+
151
+ const rawRound = flags.get("round");
152
+ if (rawRound === undefined) return roleTier({ implementer, role });
153
+
154
+ if (!/^[0-9]+$/u.test(rawRound)) throw new Error(`fix round must be an integer: ${rawRound}`);
155
+ return roleTier({ implementer, role, round: Number(rawRound) });
156
+ }
157
+
158
+ function main(argv) {
159
+ const [command, ...args] = argv;
160
+ switch (command) {
161
+ case "validate-plan":
162
+ console.log(JSON.stringify(validatePlanCommand(args), null, 2));
163
+ return 0;
164
+ case "role-tier":
165
+ console.log(JSON.stringify(roleTierCommand(args)));
166
+ return 0;
167
+ case "init": {
168
+ const flags = parseFlags(args);
169
+ const required = requireFlags(flags, [
170
+ "plan",
171
+ "state",
172
+ "progress",
173
+ "repo-root",
174
+ "worktree",
175
+ "branch",
176
+ "base-ref",
177
+ "merge-base",
178
+ ]);
179
+ const result = initRun({
180
+ planPath: required.plan,
181
+ statePath: required.state,
182
+ progressPath: required.progress,
183
+ repoRoot: required["repo-root"],
184
+ worktree: required.worktree,
185
+ branch: required.branch,
186
+ baseRef: required["base-ref"],
187
+ mergeBase: required["merge-base"],
188
+ });
189
+ console.log(JSON.stringify({ revision: result.state.revision, phase: result.state.phase }));
190
+ return 0;
191
+ }
192
+ case "show": {
193
+ const flags = parseFlags(args);
194
+ const required = requireFlags(flags, ["state", "progress"]);
195
+ const result = show({ statePath: required.state, progressPath: required.progress });
196
+ console.log(JSON.stringify(result, null, 2));
197
+ // A repairable ledger is reported through the exit code so a script does
198
+ // not have to parse stdout to notice.
199
+ return result.audit.status === "AUDIT_REPAIR_NEEDED"
200
+ ? EXIT.AUDIT_REPAIR_NEEDED
201
+ : result.audit.status === "AUDIT_CORRUPT"
202
+ ? EXIT.IDENTITY
203
+ : 0;
204
+ }
205
+ case "transition": {
206
+ const flags = parseFlags(args);
207
+ const required = requireFlags(flags, [
208
+ "state",
209
+ "progress",
210
+ "plan",
211
+ "expected-revision",
212
+ "event-file",
213
+ ]);
214
+ const revision = required["expected-revision"];
215
+ if (!/^[0-9]+$/u.test(revision)) {
216
+ throw new Error(`--expected-revision must be a non-negative integer: ${revision}`);
217
+ }
218
+ const result = transition({
219
+ statePath: required.state,
220
+ progressPath: required.progress,
221
+ planPath: required.plan,
222
+ expectedRevision: Number(revision),
223
+ eventFile: required["event-file"],
224
+ worktree: flags.get("worktree"),
225
+ branch: flags.get("branch"),
226
+ mergeBase: flags.get("merge-base"),
227
+ });
228
+ console.log(JSON.stringify({ revision: result.state.revision, phase: result.state.phase }));
229
+ return 0;
230
+ }
231
+ case "repair-audit": {
232
+ const flags = parseFlags(args);
233
+ const required = requireFlags(flags, ["state", "progress"]);
234
+ console.log(
235
+ JSON.stringify(repairAudit({ statePath: required.state, progressPath: required.progress })),
236
+ );
237
+ return 0;
238
+ }
239
+ case "lock-status": {
240
+ const flags = parseFlags(args);
241
+ const required = requireFlags(flags, ["state"]);
242
+ console.log(JSON.stringify(lockStatus({ statePath: required.state })));
243
+ return 0;
244
+ }
245
+ case "render-prompt": {
246
+ const flags = parseFlags(args);
247
+ const required = requireFlags(flags, ["tier", "role", "context", "output"]);
248
+ const raw = readFileSync(required.context, "utf8");
249
+ if (Buffer.byteLength(raw, "utf8") > 64 * 1024) {
250
+ throw new Error("the context file exceeds 64 KiB");
251
+ }
252
+ let context;
253
+ try {
254
+ context = JSON.parse(raw);
255
+ } catch {
256
+ throw new Error(`the context file is not valid JSON: ${required.context}`);
257
+ }
258
+ const rendered = renderPrompt({
259
+ tier: required.tier,
260
+ role: required.role,
261
+ context,
262
+ skillRoot: SKILL_ROOT,
263
+ });
264
+ // Atomic: validation has already passed, so a partial prompt never appears
265
+ // at the output path where a dispatch could pick it up.
266
+ const temporaryPath = `${required.output}.tmp`;
267
+ writeFileSync(temporaryPath, rendered);
268
+ renameSync(temporaryPath, required.output);
269
+ console.log(JSON.stringify({ output: required.output, bytes: Buffer.byteLength(rendered, "utf8") }));
270
+ return 0;
271
+ }
272
+ case "clear-stale-lock": {
273
+ const flags = parseFlags(args);
274
+ const required = requireFlags(flags, ["state", "expected-owner-token", "decision-file"]);
275
+ console.log(
276
+ JSON.stringify(
277
+ clearStaleLock({
278
+ statePath: required.state,
279
+ expectedOwnerToken: required["expected-owner-token"],
280
+ decisionFile: required["decision-file"],
281
+ }),
282
+ ),
283
+ );
284
+ return 0;
285
+ }
286
+ case "manifest-create": {
287
+ const flags = parseFlags(args);
288
+ const required = requireFlags(flags, ["source-root", "package-json", "output"]);
289
+ const manifest = writeManifest({
290
+ sourceRoot: required["source-root"],
291
+ packageJsonPath: required["package-json"],
292
+ outputPath: required.output,
293
+ });
294
+ console.log(
295
+ JSON.stringify({
296
+ written: required.output,
297
+ runtimeHash: manifest.runtimeHash,
298
+ runtimeFileCount: manifest.runtimeFiles.length,
299
+ sourcePackage: manifest.sourcePackage,
300
+ }),
301
+ );
302
+ return 0;
303
+ }
304
+ case "manifest-hash": {
305
+ const flags = parseFlags(args);
306
+ const required = requireFlags(flags, ["manifest"]);
307
+ // Verify against the manifest's own directory so the check cannot be
308
+ // pointed at an unrelated tree by passing a mismatched source root.
309
+ const { manifest, runtimeHash } = verifyManifest(
310
+ required.manifest,
311
+ dirname(resolve(required.manifest)),
312
+ );
313
+ console.log(
314
+ JSON.stringify({
315
+ manifest: required.manifest,
316
+ runtimeHash,
317
+ runtimeFileCount: manifest.runtimeFiles.length,
318
+ verified: true,
319
+ }),
320
+ );
321
+ return 0;
322
+ }
323
+ default:
324
+ throw new Error(command === undefined ? USAGE : `unknown command: ${command}\n${USAGE}`);
325
+ }
326
+ }
327
+
328
+ function isDirectExecution() {
329
+ const entryPath = process.argv[1];
330
+ if (entryPath === undefined) return false;
331
+ // Node resolves symlinks for import.meta.url but leaves argv[1] as supplied, so
332
+ // a literal comparison fails when this file is reached through a symlink. That
333
+ // failure is silent: main() never runs and the exit code is still 0.
334
+ return realpathSync(resolve(entryPath)) === realpathSync(fileURLToPath(import.meta.url));
335
+ }
336
+
337
+ if (isDirectExecution()) {
338
+ try {
339
+ process.exitCode = main(process.argv.slice(2));
340
+ } catch (error) {
341
+ console.error(error instanceof Error ? error.message : String(error));
342
+ // A StoreError carries the exit code its failure mode maps to; anything else
343
+ // is a validation failure from argument handling.
344
+ // A StoreError carries the exit code its failure mode maps to. A ManifestError
345
+ // is not a StoreError, so integrity failures land on VALIDATION alongside
346
+ // argument handling -- a wrong hash is a bad input, not lock contention.
347
+ process.exitCode = error instanceof StoreError ? error.code : EXIT.VALIDATION;
348
+ }
349
+ }
@@ -0,0 +1,232 @@
1
+ ---
2
+ name: deterministic-writing-plans
3
+ description: Use when you have a spec or requirements for a multi-step task, before touching code, and the plan will be executed by the deterministic subagent-driven-development controller
4
+ ---
5
+
6
+ # Deterministic Writing Plans
7
+
8
+ ## Overview
9
+
10
+ Write implementation plans that the deterministic subagent-driven-development
11
+ controller can run without manual repair. Assume the implementer has zero
12
+ context for this codebase and questionable taste: document which files to
13
+ touch, the actual code, how to test it, and what neighbouring tasks named
14
+ things. Give them bite-sized tasks. DRY. YAGNI. TDD. Frequent commits.
15
+
16
+ Assume a skilled developer who knows almost nothing about our toolset or
17
+ problem domain, and who does not know good test design well.
18
+
19
+ **Announce at start:** "I'm using the deterministic-writing-plans skill to
20
+ create the implementation plan."
21
+
22
+ **Save plans to:** `docs/superpowers/plans/YYYY-MM-DD-<feature-name>.md`
23
+ (user preferences for plan location override this default).
24
+
25
+ ## What makes this different
26
+
27
+ Every task carries a machine-checked tier annotation, and headings are exactly
28
+ `## Task N:`. The controller parses the plan; a plan that does not satisfy the
29
+ grammar is rejected outright with a diagnostic, not repaired. Two consequences:
30
+
31
+ - **Copy `templates/plan-skeleton.md` and edit it.** It is a literal, valid plan
32
+ and a test parses it with the controller's own parser on every run. Starting
33
+ from it means starting from something known to parse.
34
+ - **The grammar is the whole contract.** A hand-written plan that satisfies it
35
+ is just as valid. Nothing checks which skill produced a plan.
36
+
37
+ **Using the wrong skill?** If no controller will run this plan, use the plain
38
+ `writing-plans` skill instead. The grammar here is additive and harmless to a
39
+ human reader, so a deterministic plan is never wrong for a human to execute, but
40
+ tier annotations are pure overhead when nothing dispatches on them.
41
+
42
+ ## Non-negotiable grammar
43
+
44
+ Verified against the controller's parser, not paraphrased. See
45
+ `references/grammar.md` for the observed diagnostics and the eight rejections
46
+ pinned in `tests/grammar-rejections.test.mjs`.
47
+
48
+ - Task heading is exactly `## Task <N>: <Title>`. `###` is an error, not a
49
+ tolerated variant. `<N>` starts at 1 and increases by 1 with no gaps.
50
+ - Each task carries exactly one `**Implementer tier:** <Value>` line, outside
51
+ any code fence, where `<Value>` is TitleCase: `Economy`, `Fast`, `Standard`,
52
+ `Advanced`, `Capable`, or `Frontier`. Lowercase is a hard error. A trailing
53
+ space is a hard error.
54
+ - `## Global Constraints` may appear at most once, and must precede the first
55
+ task. A second one is a hard error (`duplicate Global Constraints section`);
56
+ one placed after Task 1 is a hard error (`Global Constraints must precede the
57
+ first task`).
58
+ - Fenced content is inert. A tier line inside a fence does not count, and the
59
+ task will be rejected as having no tier.
60
+ - Do not put a `---` rule immediately after `## Global Constraints` or at the
61
+ end of a task. The parser absorbs it into that section, and the text is
62
+ injected verbatim into child briefs.
63
+ - **Never use a plain `##` heading inside a task body.** Any non-canonical H2
64
+ terminates the task silently: every line after it, including remaining steps
65
+ and the commit step, is discarded with no diagnostic. Use `###` or deeper for
66
+ subheadings within a task. This is the most destructive mistake available to
67
+ a plan author, because the plan still validates.
68
+
69
+ ## Plan Document Header
70
+
71
+ **Every plan MUST start with this header.** The grammar does not enforce it, so
72
+ nothing will reject a plan without it; it is required because a reader arriving
73
+ cold, human or agent, has no other orientation.
74
+
75
+ ```markdown
76
+ # [Feature Name] Implementation Plan
77
+
78
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use the deterministic
79
+ > deterministic-subagent-driven-development controller to implement this plan task-by-task.
80
+
81
+ **Goal:** [One sentence describing what this builds]
82
+
83
+ **Architecture:** [2-3 sentences about approach]
84
+
85
+ **Tech Stack:** [Key technologies/libraries]
86
+
87
+ ## Global Constraints
88
+
89
+ [The spec's project-wide requirements — version floors, dependency limits,
90
+ naming and copy rules, platform requirements — one line each, with exact
91
+ values copied verbatim from the spec.]
92
+ ```
93
+
94
+ `## Global Constraints` carries a specific obligation. It is injected verbatim
95
+ into **every** task brief, and it is the only channel to a subagent that never
96
+ sees the plan. Copy exact values from the spec rather than summarizing: a child
97
+ cannot infer a version floor stated approximately. One line each. If the spec
98
+ has no project-wide requirements, say so in one explicit line rather than
99
+ omitting the section.
100
+
101
+ ## Choosing the implementer tier
102
+
103
+ Annotate **only** the implementer. The task reviewer, re-reviewer, fixer, and
104
+ the three final roles are derived by formula from it; naming them in the plan
105
+ invites disagreement with the controller's `role-tier`, which is authoritative.
106
+
107
+ The tier table lives in the controller's plan contract, at
108
+ `../deterministic-subagent-driven-development/references/plan-contract.md`, under "Choosing the
109
+ implementer tier". Read it there rather than from a copy here, so there is one
110
+ source of truth. The two skills ship together, so that path resolves; if you
111
+ have installed this skill alone, the table is the one thing you are missing. Two rules from it
112
+ matter enough to restate:
113
+
114
+ - **`standard` is the floor whenever the implementer must decide anything.**
115
+ Cheap models take two to three times the turns on multi-step work, which
116
+ costs more in total than the tier saved.
117
+ - **A wrong tier is a cost and latency defect, not a correctness one.** Review
118
+ gates catch bad implementations. Do not treat the annotation as a quality
119
+ lever, and do not inflate every task to `capable` "to be safe".
120
+
121
+ Honest caveat: the mapping from task shape to tier is reasoned, not measured. No
122
+ eval yet establishes that `advanced` beats `standard` on a given task shape. Use
123
+ it as a default, and override it when you know something about the work.
124
+
125
+ ## Scope Check
126
+
127
+ If the spec covers multiple independent subsystems, suggest breaking it into
128
+ separate plans, one per subsystem. Each plan should produce working, testable
129
+ software on its own.
130
+
131
+ ## File Structure
132
+
133
+ Before defining tasks, map out which files will be created or modified and what
134
+ each is responsible for. This is where decomposition gets locked in.
135
+
136
+ - Design units with clear boundaries and well-defined interfaces. One clear
137
+ responsibility per file.
138
+ - Prefer smaller, focused files. You reason best about code you can hold in
139
+ context at once, and edits are more reliable when files are focused.
140
+ - Files that change together live together. Split by responsibility, not by
141
+ technical layer.
142
+ - In existing codebases follow established patterns. Do not unilaterally
143
+ restructure, though splitting a file you are already modifying is reasonable.
144
+
145
+ ## Task Right-Sizing
146
+
147
+ A task is the smallest unit that carries its own test cycle and is worth a fresh
148
+ reviewer's gate. Fold setup, configuration, scaffolding, and documentation into
149
+ the task whose deliverable needs them. Split only where a reviewer could
150
+ meaningfully reject one task while approving its neighbour. Each task ends with
151
+ an independently testable deliverable.
152
+
153
+ Every task is dispatched to a fresh subagent that sees only its own brief plus
154
+ `## Global Constraints`. A task that assumes conversational context will fail.
155
+
156
+ ## Bite-Sized Step Granularity
157
+
158
+ Each step is one action, two to five minutes: write the failing test; run it and
159
+ confirm it fails; write the minimal implementation; run the tests and confirm
160
+ they pass; commit. Steps use `- [ ]` checkbox syntax.
161
+
162
+ Those boxes are a readability convention here, not the progress mechanism. The
163
+ controller's `state.json` is canonical and it pins the plan's digest, so editing
164
+ the plan mid-run, including ticking a box, changes the digest and halts the run
165
+ for a human decision. Leave the boxes unticked and let the controller track.
166
+
167
+ ## Files blocks
168
+
169
+ Each task opens with the exact paths it touches, using `path:start-end` line
170
+ ranges for modifications so an implementer edits the right region:
171
+
172
+ ```text
173
+ **Files:**
174
+ - Create: `src/parse/tokens.ts`
175
+ - Modify: `src/cli/index.ts:1-20`
176
+ - Test: `src/parse/tokens.test.ts`
177
+ ```
178
+
179
+ ## Interfaces blocks are load-bearing
180
+
181
+ Each task states what it consumes and what it produces, with exact names and
182
+ types:
183
+
184
+ ```text
185
+ **Interfaces:**
186
+ - Consumes: `tokenize(input: string): Token[]` from Task 1, with
187
+ `Token = { kind: "word" | "space"; text: string }`.
188
+ - Produces: `runCli(argv: string[]): Promise<number>`, resolving to the exit code.
189
+ ```
190
+
191
+ This is not documentation. An implementer sees only its own brief, so this block
192
+ is the only way it learns the names its neighbours use. A task that consumes an
193
+ earlier task's output without restating the signature will guess, and guess wrong.
194
+
195
+ ## No Placeholders
196
+
197
+ Every step must contain the actual content the implementer needs. These are
198
+ **plan failures** — never write them:
199
+
200
+ - "TBD", "TODO", "implement later", "fill in details"
201
+ - "Add appropriate error handling" / "add validation" / "handle edge cases"
202
+ - "Write tests for the above" without the actual test code
203
+ - "Similar to Task N" — repeat the code; tasks are read in isolation, and in
204
+ this controller they are literally dispatched in isolation
205
+ - Steps that say what to do without showing how (code steps need code blocks)
206
+ - References to types, functions, or methods not defined in any task
207
+
208
+ ## Self-Review
209
+
210
+ After writing the plan, check it against the spec with fresh eyes. This is a
211
+ checklist you run yourself, not a subagent dispatch.
212
+
213
+ 1. **Spec coverage.** For each requirement in the spec, name the task that
214
+ implements it. List gaps and add tasks for them.
215
+ 2. **Placeholder scan.** Search for the red flags above and fix them.
216
+ 3. **Type consistency.** Do types, signatures, and property names in later
217
+ tasks match what earlier tasks defined? `clearLayers()` in Task 3 and
218
+ `clearFullLayers()` in Task 7 is a bug.
219
+ 4. **Interfaces completeness.** For each task after the first, does its
220
+ Consumes block restate every signature it depends on?
221
+ 5. **Grammar.** Run the controller's `validate-plan` against the saved file.
222
+ Run `sdd-state validate-plan docs/superpowers/plans/<file>.md` from the
223
+ controller's `scripts/` directory. Do not hand over a plan you have not
224
+ seen parse.
225
+
226
+ Fix inline; no need to re-review.
227
+
228
+ ## Execution Handoff
229
+
230
+ State where the plan was saved and that it validated. Then hand off to the
231
+ deterministic subagent-driven-development controller, which dispatches a fresh
232
+ subagent per task with two-stage review between tasks.
@@ -0,0 +1,84 @@
1
+ # Grammar, as observed
2
+
3
+ Every row below was produced by running the controller's `parsePlanText` against
4
+ the input, not by reading the regexes. The pinning tests are in
5
+ `tests/grammar-rejections.test.mjs`; each asserts the specific diagnostic, so a
6
+ reworded message fails loudly instead of passing a loose match.
7
+
8
+ The authority is `optional-skills/deterministic-subagent-driven-development/scripts/lib/plan-policy.mjs`:
9
+
10
+ ```js
11
+ const TASK_HEADING = /^## Task ([1-9][0-9]*): (\S(?:.*\S)?)$/u;
12
+ const TIER_FIELD =
13
+ /^\*\*Implementer tier:\*\* (Economy|Fast|Standard|Advanced|Capable|Frontier)$/u;
14
+ const GLOBAL_HEADING = /^## Global Constraints$/u;
15
+ ```
16
+
17
+ ## Accepted
18
+
19
+ | Input | Result |
20
+ | ----------------------------------------------------- | --------------------------------- |
21
+ | `## Task 1: Only task` + `**Implementer tier:** Fast` | parses; tier normalized to `fast` |
22
+
23
+ TitleCase in the document, lowercase on the wire. The parser normalizes at that
24
+ boundary, so no dispatch site has to remember to.
25
+
26
+ ## Rejected
27
+
28
+ | Mistake | Diagnostic |
29
+ | ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
30
+ | `### Task 1: Only task` | `task-like heading is not canonical` — names the depth found, the depth required, and emits the corrected heading verbatim |
31
+ | `**Implementer tier:** fast` | `malformed Implementer tier field: **Implementer tier:** fast` |
32
+ | `**Implementer tier:** Fast ` (trailing space) | `malformed Implementer tier field` |
33
+ | tier line absent | `Task 1 has no Implementer tier`, with the exact line to add |
34
+ | `## Task 1:` then `## Task 3:` | `expected Task 2 but found Task 3` |
35
+ | tier line inside a ``` fence | `Task 1 has no Implementer tier` — fenced content is inert |
36
+ | two `## Global Constraints` sections | `duplicate Global Constraints section` |
37
+ | `## Global Constraints` after Task 1 | `Global Constraints must precede the first task` |
38
+
39
+ The depth diagnostic is worth quoting in full, because it repairs the plan for you:
40
+
41
+ ```text
42
+ <plan>:3: task-like heading is not canonical: ### Task 1: Only task
43
+ found heading depth "###" but the deterministic controller requires "##"
44
+ rewrite it as: ## Task 1: Only task
45
+ and give every task a tier line on its own: **Implementer tier:** Advanced
46
+ a tier-annotated plan is a precondition of tiered dispatch; the controller never guesses a tier
47
+ ```
48
+
49
+ The two worth internalizing are the ones that look correct on screen: a
50
+ lowercase tier reads naturally because lowercase is what travels on the wire,
51
+ and a trailing space is invisible. Both are hard errors.
52
+
53
+ ## The `##` truncation trap
54
+
55
+ Worse than the rule trap below, and the reason to read this file. A plain `##`
56
+ heading inside a task body terminates the task. Every line after it is
57
+ discarded: remaining steps, the commit step, everything. The plan still
58
+ validates, and there is no diagnostic.
59
+
60
+ Observed, with `## Notes` placed after Step 1 of a three-step task:
61
+
62
+ ```text
63
+ tasks parsed: 1
64
+ body: "- [ ] **Step 1: first**"
65
+ Step 2 survived? false
66
+ Commit step survived? false
67
+ ```
68
+
69
+ `plan-policy.mjs` treats any non-canonical H2 as terminating the open section
70
+ without capturing it. `###` and deeper are safe, as is bold text used as a
71
+ pseudo-heading. Use `###` for any subheading within a task.
72
+
73
+ Pinned by `tests/grammar-rejections.test.mjs`, which asserts both the loss under
74
+ `##` and the survival under `###`.
75
+
76
+ ## The horizontal-rule trap
77
+
78
+ The parser attributes a `---` line to the section it follows. A rule immediately
79
+ after `## Global Constraints`, or at the end of a task body, is absorbed into
80
+ that section's text and then injected verbatim into child briefs. It is not a
81
+ parse error, so nothing warns you. Let headings do the separating.
82
+
83
+ Pinned by `tests/plan-skeleton.test.mjs`, which asserts no `^-{3,}$` line
84
+ survives in the constraints or any task body.