@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,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.
|