@stdd/plugin 0.9.0

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 (80) hide show
  1. package/.claude-plugin/plugin.json +9 -0
  2. package/.codex-plugin/plugin.json +21 -0
  3. package/LICENSE +21 -0
  4. package/README.md +47 -0
  5. package/extensions/stdd.mjs +77 -0
  6. package/hooks/claude-hooks.json +28 -0
  7. package/hooks/codex-hooks.json +28 -0
  8. package/package.json +38 -0
  9. package/runtime/adapters/README.md +158 -0
  10. package/runtime/cli/check.mjs +555 -0
  11. package/runtime/cli/ci.mjs +190 -0
  12. package/runtime/cli/claude-hooks.mjs +689 -0
  13. package/runtime/cli/config.mjs +27 -0
  14. package/runtime/cli/evidence.mjs +249 -0
  15. package/runtime/cli/generated-files.mjs +1693 -0
  16. package/runtime/cli/held-fs.mjs +415 -0
  17. package/runtime/cli/init.mjs +883 -0
  18. package/runtime/cli/ledger.mjs +1470 -0
  19. package/runtime/cli/lib.mjs +909 -0
  20. package/runtime/cli/path-bytes.mjs +83 -0
  21. package/runtime/cli/policy.mjs +112 -0
  22. package/runtime/cli/recorders.mjs +188 -0
  23. package/runtime/cli/review-fs.mjs +825 -0
  24. package/runtime/cli/review.mjs +1065 -0
  25. package/runtime/cli/runtime.mjs +32 -0
  26. package/runtime/cli/scope.mjs +185 -0
  27. package/runtime/cli/snapshot.mjs +897 -0
  28. package/runtime/cli/state-validation.mjs +168 -0
  29. package/runtime/cli/status.mjs +580 -0
  30. package/runtime/cli/stdd.mjs +536 -0
  31. package/runtime/cli/worker-fs.mjs +971 -0
  32. package/runtime/cli/worker-metadata.mjs +139 -0
  33. package/runtime/cli/worker.mjs +779 -0
  34. package/runtime/method/README.md +634 -0
  35. package/runtime/method/reference-commands.md +147 -0
  36. package/runtime/method/reference-generated-state.md +151 -0
  37. package/runtime/method/reference-integration.md +233 -0
  38. package/runtime/package.json +65 -0
  39. package/runtime/playbooks/brainstorming.md +46 -0
  40. package/runtime/playbooks/debugging.md +36 -0
  41. package/runtime/playbooks/delegate-slice.md +129 -0
  42. package/runtime/playbooks/finish-change.md +46 -0
  43. package/runtime/playbooks/implement.md +26 -0
  44. package/runtime/playbooks/investigation.md +33 -0
  45. package/runtime/playbooks/managed-playbooks.json +14 -0
  46. package/runtime/playbooks/planning.md +177 -0
  47. package/runtime/playbooks/pr-green.md +50 -0
  48. package/runtime/playbooks/start-change.md +37 -0
  49. package/runtime/playbooks/worktrees.md +45 -0
  50. package/runtime/prebuilds/stdd-fs/darwin-arm64/stdd-fs +0 -0
  51. package/runtime/prebuilds/stdd-fs/darwin-x64/stdd-fs +0 -0
  52. package/runtime/prebuilds/stdd-fs/linux-arm64/stdd-fs +0 -0
  53. package/runtime/prebuilds/stdd-fs/linux-x64/stdd-fs +0 -0
  54. package/runtime/prebuilds/stdd-fs/manifest.json +47 -0
  55. package/runtime/prebuilds/stdd-fs/win32-arm64/stdd-fs.exe +0 -0
  56. package/runtime/prebuilds/stdd-fs/win32-x64/stdd-fs.exe +0 -0
  57. package/runtime/sdk/adapters.mjs +279 -0
  58. package/runtime/sdk/file-observation.mjs +12 -0
  59. package/runtime/sdk/index.d.ts +140 -0
  60. package/runtime/sdk/index.mjs +31 -0
  61. package/runtime/sdk/native-fs.mjs +1235 -0
  62. package/runtime/sdk/path.mjs +71 -0
  63. package/runtime/sdk/text.mjs +42 -0
  64. package/runtime/sdk/workflow.mjs +294 -0
  65. package/runtime/templates/deferred-design.md +47 -0
  66. package/runtime/templates/github-stdd.yml +42 -0
  67. package/runtime/templates/gitlab-stdd.yml +72 -0
  68. package/runtime/templates/pr-description.md +35 -0
  69. package/scripts/adopting-root.mjs +42 -0
  70. package/scripts/stdd-hook.mjs +72 -0
  71. package/skills/stdd-brainstorming/SKILL.md +48 -0
  72. package/skills/stdd-debugging/SKILL.md +38 -0
  73. package/skills/stdd-delegate-slice/SKILL.md +118 -0
  74. package/skills/stdd-finish-change/SKILL.md +40 -0
  75. package/skills/stdd-implement/SKILL.md +28 -0
  76. package/skills/stdd-investigation/SKILL.md +35 -0
  77. package/skills/stdd-planning/SKILL.md +165 -0
  78. package/skills/stdd-pr-green/SKILL.md +52 -0
  79. package/skills/stdd-start-change/SKILL.md +39 -0
  80. package/skills/stdd-worktrees/SKILL.md +46 -0
@@ -0,0 +1,536 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { format } from "node:util";
5
+ import { check, doctor } from "./check.mjs";
6
+ import { ciCommand } from "./ci.mjs";
7
+ import { loadConfig } from "./config.mjs";
8
+ import { checkPr, evidence } from "./evidence.mjs";
9
+ import {
10
+ KNOWN_CAPABILITIES,
11
+ KNOWN_CI,
12
+ KNOWN_TOOLS,
13
+ VERSION,
14
+ validateAdapterSelection,
15
+ } from "./generated-files.mjs";
16
+ import { configure, init, interview } from "./init.mjs";
17
+ import {
18
+ appendLedger,
19
+ defer,
20
+ finishTask,
21
+ REVIEW_VIAS,
22
+ resetTask,
23
+ resolveRepoDir,
24
+ startTask,
25
+ } from "./ledger.mjs";
26
+ import { didYouMean } from "./lib.mjs";
27
+ import { policyAdd, policyAllow, policyShow } from "./policy.mjs";
28
+ import { recordDocs, recordRun } from "./recorders.mjs";
29
+ import { reviewCleanup, reviewRun, reviewSubmit } from "./review.mjs";
30
+ import { fail } from "./runtime.mjs";
31
+ import { scopeCheck, sliceNew } from "./scope.mjs";
32
+ import { status, statusGate, stopHookCmd } from "./status.mjs";
33
+ import { workerCollect, workerCreate } from "./worker.mjs";
34
+ import { readWorkerMetadata, WORKER_LOCAL_COMMANDS } from "./worker-metadata.mjs";
35
+
36
+ // --- argument parsing (strict: unknown flags are errors) ---
37
+ console.log = (...args) => fs.writeSync(process.stdout.fd, `${format(...args)}\n`);
38
+ console.error = (...args) => fs.writeSync(process.stderr.fd, `${format(...args)}\n`);
39
+
40
+ async function main() {
41
+ const [, , command, ...rest] = process.argv;
42
+
43
+ let commandWorker;
44
+ if (command !== "stop-hook") {
45
+ try {
46
+ commandWorker = readWorkerMetadata(process.cwd());
47
+ } catch (err) {
48
+ fail(err.message);
49
+ }
50
+ }
51
+ if (
52
+ commandWorker &&
53
+ command &&
54
+ (!WORKER_LOCAL_COMMANDS.has(command) ||
55
+ (command === "doctor" && (rest.length !== 1 || rest[0] !== "--readiness")) ||
56
+ (command === "status" && !rest.includes("--local")))
57
+ ) {
58
+ fail(
59
+ `stdd ${command ?? "command"} is source-checkout-owned and unavailable in a managed gitless worker; ` +
60
+ "run it from the bound source checkout",
61
+ );
62
+ }
63
+
64
+ // Ledger commands parse their own arguments (`--` passthrough, repeated
65
+ // positionals) and exit here; the strict generic loop below never sees them.
66
+ if (command === "red" || command === "verify") {
67
+ const sep = rest.indexOf("--");
68
+ if (sep === -1 || sep === rest.length - 1) {
69
+ fail(`${command} needs a command: stdd ${command} -- <cmd> [args…]`);
70
+ }
71
+ if (sep !== 0) fail(`unexpected argument before --: ${rest[0]}`);
72
+ recordRun(resolveRepoDir(process.cwd()), command, rest.slice(sep + 1));
73
+ }
74
+ if (command === "task") {
75
+ const subcommand = rest[0];
76
+ const cwd = resolveRepoDir(process.cwd());
77
+ if (subcommand === "start") {
78
+ startTask(cwd, rest.slice(1).join(" "));
79
+ } else if (subcommand === "finish") {
80
+ if (rest.length > 1) fail(`unexpected argument: ${rest[1]}`);
81
+ finishTask(cwd);
82
+ } else if (subcommand === "reset") {
83
+ resetTask(cwd, rest.slice(1).join(" ") || null);
84
+ } else {
85
+ fail(`unknown task subcommand "${subcommand ?? ""}" — use start, finish, or reset`);
86
+ }
87
+ return 0;
88
+ }
89
+ if (command === "docs") {
90
+ let reason = null;
91
+ const words = [];
92
+ for (let i = 0; i < rest.length; i++) {
93
+ const arg = rest[i];
94
+ if (arg === "--reason" || arg.startsWith("--reason=")) {
95
+ reason = arg.includes("=") ? arg.slice("--reason=".length) : (rest[++i] ?? "");
96
+ if (!reason) fail("--reason requires a value");
97
+ } else if (arg.startsWith("--")) {
98
+ fail(`unknown flag: ${arg}`);
99
+ } else {
100
+ words.push(arg);
101
+ }
102
+ }
103
+ recordDocs(resolveRepoDir(process.cwd()), words[0], words.slice(1), reason);
104
+ return 0;
105
+ }
106
+ if (command === "note") {
107
+ const text = rest.join(" ").trim();
108
+ if (!text) fail("note needs text: stdd note <text>");
109
+ appendLedger(resolveRepoDir(process.cwd()), { event: "note", text });
110
+ console.log("stdd note: recorded");
111
+ return 0;
112
+ }
113
+ if (command === "defer") {
114
+ const text = rest.join(" ").trim();
115
+ if (!text) fail("defer needs text: stdd defer <what is cut and why>");
116
+ defer(resolveRepoDir(process.cwd()), text);
117
+ return 0;
118
+ }
119
+ if (command === "policy") {
120
+ const subcommand = rest[0];
121
+ if (subcommand !== "add" && subcommand !== "allow" && subcommand !== "show") {
122
+ fail(`unknown policy subcommand "${subcommand ?? ""}" — use show, add, or allow`);
123
+ }
124
+ const cwd = resolveRepoDir(process.cwd());
125
+ if (subcommand === "show") {
126
+ if (rest.length > 1) fail(`unexpected argument: ${rest[1]}`);
127
+ try {
128
+ return policyShow(cwd);
129
+ } catch (err) {
130
+ fail(err.message);
131
+ }
132
+ }
133
+ if (subcommand === "add") {
134
+ const text = rest.slice(1).join(" ").trim();
135
+ if (!text) fail("policy add needs text: stdd policy add <what this project decided>");
136
+ try {
137
+ await policyAdd(cwd, text);
138
+ } catch (err) {
139
+ fail(err.message);
140
+ }
141
+ console.log("stdd policy: note recorded");
142
+ return 0;
143
+ }
144
+ let condition = null;
145
+ for (let i = 2; i < rest.length; i++) {
146
+ const arg = rest[i];
147
+ if (arg === "--when" || arg.startsWith("--when=")) {
148
+ condition = arg.includes("=") ? arg.slice("--when=".length) : (rest[++i] ?? "");
149
+ } else {
150
+ fail(`unexpected argument: ${arg}`);
151
+ }
152
+ }
153
+ try {
154
+ await policyAllow(cwd, rest[1] ?? "", condition ?? "");
155
+ } catch (err) {
156
+ fail(err.message);
157
+ }
158
+ console.log("stdd policy: permission recorded");
159
+ return 0;
160
+ }
161
+ function parseGenericList(value, flag, { noun, example, known }) {
162
+ const values = value.split(",").filter(Boolean);
163
+ if (values.length === 0) fail(`${flag} requires a value, e.g. ${flag} ${example}`);
164
+ const unknown = values.filter((item) => !known.includes(item));
165
+ if (unknown.length > 0) fail(`unknown ${noun}: ${unknown.join(", ")} (known: ${known.join(", ")})`);
166
+ return values;
167
+ }
168
+
169
+ function parseScopeFlags(args, startIndex) {
170
+ let frozen = [];
171
+ let allowed = [];
172
+ for (let i = startIndex; i < args.length; i++) {
173
+ const arg = args[i];
174
+ if (arg === "--frozen" || arg.startsWith("--frozen=")) {
175
+ const value = arg.includes("=") ? arg.slice("--frozen=".length) : (args[++i] ?? "");
176
+ frozen = value.split(",").filter(Boolean);
177
+ if (frozen.length === 0) fail("--frozen requires globs, e.g. --frozen docs/**");
178
+ } else if (arg === "--allowed" || arg.startsWith("--allowed=")) {
179
+ const value = arg.includes("=") ? arg.slice("--allowed=".length) : (args[++i] ?? "");
180
+ allowed = value.split(",").filter(Boolean);
181
+ if (allowed.length === 0) fail("--allowed requires globs, e.g. --allowed src/**");
182
+ } else {
183
+ fail(`unexpected argument: ${arg}`);
184
+ }
185
+ }
186
+ return { frozen, allowed };
187
+ }
188
+
189
+ if (command === "worker") {
190
+ const subcommand = rest[0];
191
+ if (subcommand !== "create" && subcommand !== "collect") {
192
+ fail(`unknown worker subcommand "${subcommand ?? ""}" — use create or collect`);
193
+ }
194
+ const directory = rest[1];
195
+ if (!directory || directory.startsWith("--")) {
196
+ fail(`worker ${subcommand} needs a directory`);
197
+ }
198
+ if (subcommand === "collect") {
199
+ if (rest.length > 2) fail(`unexpected argument: ${rest[2]}`);
200
+ await workerCollect(resolveRepoDir(process.cwd()), directory);
201
+ return 0;
202
+ }
203
+ const { frozen, allowed } = parseScopeFlags(rest, 2);
204
+ await workerCreate(resolveRepoDir(process.cwd()), directory, frozen, allowed);
205
+ return 0;
206
+ }
207
+ if (command === "slice") {
208
+ if (rest[0] !== "new") {
209
+ fail(`unknown slice subcommand "${rest[0] ?? ""}" — use "stdd slice new"`);
210
+ }
211
+ const { frozen, allowed } = parseScopeFlags(rest, 1);
212
+ sliceNew(resolveRepoDir(process.cwd()), frozen, allowed);
213
+ return 0;
214
+ }
215
+ if (command === "scope") {
216
+ if (rest.length > 0) fail(`unexpected argument: ${rest[0]}`);
217
+ scopeCheck(resolveRepoDir(process.cwd()));
218
+ return 0;
219
+ }
220
+ if (command === "stop-hook") {
221
+ let agent = "claude";
222
+ if (rest.length > 0) {
223
+ if (rest[0] !== "--agent" || !rest[1] || rest.length > 2) {
224
+ fail(`stop-hook accepts only --agent claude|codex`);
225
+ }
226
+ agent = rest[1];
227
+ if (agent !== "claude" && agent !== "codex") fail(`unknown hook agent "${agent}"`);
228
+ }
229
+ // raw cwd: the command resolves the repo itself, softly — a hook must
230
+ // fail open even outside a usable worktree
231
+ stopHookCmd(process.cwd(), agent);
232
+ }
233
+ if (command === "status") {
234
+ const unknown = rest.filter((a) => a !== "--json" && a !== "--gate" && a !== "--local");
235
+ if (unknown.length > 0) fail(`unexpected argument: ${unknown[0]}`);
236
+ if (rest.includes("--gate")) statusGate(resolveRepoDir(process.cwd()));
237
+ status(resolveRepoDir(process.cwd()), rest.includes("--json"), rest.includes("--local"));
238
+ return 0;
239
+ }
240
+ if (command === "review") {
241
+ let via = null;
242
+ let timeout = 600;
243
+ let timeoutSpecified = false;
244
+ let result = null;
245
+ let force = false;
246
+ let reason = null;
247
+ let cleanup = false;
248
+ for (let i = 0; i < rest.length; i++) {
249
+ const arg = rest[i];
250
+ if (arg === "--force") {
251
+ force = true;
252
+ } else if (arg === "--reason" || arg.startsWith("--reason=")) {
253
+ reason = arg.includes("=") ? arg.slice("--reason=".length) : (rest[++i] ?? "");
254
+ if (!reason) fail("--reason requires the text explaining what the extra round should settle");
255
+ } else if (arg === "--cleanup") {
256
+ cleanup = true;
257
+ } else if (arg === "--via" || arg.startsWith("--via=")) {
258
+ via = arg.includes("=") ? arg.slice("--via=".length) : (rest[++i] ?? "");
259
+ if (!via) fail(`--via requires a value (${REVIEW_VIAS.join(", ")})`);
260
+ } else if (arg === "--timeout" || arg.startsWith("--timeout=")) {
261
+ const value = arg.includes("=") ? arg.slice("--timeout=".length) : (rest[++i] ?? "");
262
+ timeoutSpecified = true;
263
+ timeout = Number(value);
264
+ // integer seconds, bounded: spawnSync needs a sane uint32 of
265
+ // milliseconds, and a parse-time failure must precede any side
266
+ // effect (request event, brief file)
267
+ if (!/^\d+$/.test(value) || !Number.isInteger(timeout) || timeout < 1 || timeout > 86_400)
268
+ fail("--timeout requires whole seconds between 1 and 86400, e.g. --timeout 600");
269
+ } else if (arg === "--result" || arg.startsWith("--result=")) {
270
+ result = arg.includes("=") ? arg.slice("--result=".length) : (rest[++i] ?? "");
271
+ if (!result) fail("--result requires a file path or - for stdin");
272
+ } else {
273
+ fail(`unexpected argument: ${arg}`);
274
+ }
275
+ }
276
+ const cwd = resolveRepoDir(process.cwd());
277
+ if (cleanup) {
278
+ if (result !== null || via !== null || timeoutSpecified || force || reason !== null) {
279
+ fail("--cleanup cancels open review requests and cannot be combined with dispatch flags");
280
+ }
281
+ return (await reviewCleanup(cwd)) ? 0 : 1;
282
+ } else if (result !== null) {
283
+ if (via !== null) fail("--result grades an existing request — --via belongs to the dispatch call");
284
+ if (timeoutSpecified || force || reason !== null) {
285
+ fail(
286
+ "--result grades an existing request — --timeout, --force, and --reason belong to the dispatch call",
287
+ );
288
+ }
289
+ return await reviewSubmit(cwd, loadConfig(cwd), result);
290
+ } else {
291
+ // spending a round past the budget is a decision, so it carries its
292
+ // own justification; a reason without --force decides nothing
293
+ if (force && reason === null) {
294
+ fail("--force spends a review round past the budget — say why with --reason <text>");
295
+ }
296
+ if (!force && reason !== null) {
297
+ fail("--reason records why a round was forced — it belongs with --force");
298
+ }
299
+ return await reviewRun(cwd, via, timeout, reason);
300
+ }
301
+ }
302
+ let tools = null;
303
+ let ci = null;
304
+ let baseRefArg = null;
305
+ let prArg = null;
306
+ let readinessOnly = false;
307
+ let hooksFlag = false;
308
+ let capabilitiesArg = null;
309
+ let sessionHookFlag = false;
310
+ let stopHookFlag = false;
311
+ let reviewViaArg = null;
312
+ let maxRoundsArg = null;
313
+ let interviewFlag = false;
314
+ let watchFlag = false;
315
+ let intervalArg = 15;
316
+ let timeoutArg = 1800;
317
+
318
+ // Normalize attached values structurally; only the parsedArg branches below
319
+ // decide which flags accept values, preserving boolean and unknown-flag errors.
320
+ const GENERIC_ATTACHED_VALUE = /^(--[^=]+)=([\s\S]*)$/u;
321
+ const positional = [];
322
+ for (let i = 0; i < rest.length; i++) {
323
+ const arg = rest[i];
324
+ const valueMatch = GENERIC_ATTACHED_VALUE.exec(arg);
325
+ const parsedArg = valueMatch?.[1] ?? arg;
326
+ const parsedValue = valueMatch?.[2] ?? rest[i + 1] ?? "";
327
+ const parsedValueIndex = valueMatch ? i : i + 1;
328
+ // Valued flags compare parsedArg and explicitly consume parsedValueIndex;
329
+ // boolean flags compare raw arg, so --hooks=yes remains unknown.
330
+ if (parsedArg === "--base") {
331
+ if (command !== "check-pr" && command !== "evidence") {
332
+ fail(`--base is only valid for "stdd check-pr" and "stdd evidence"`);
333
+ }
334
+ i = parsedValueIndex;
335
+ baseRefArg = parsedValue;
336
+ if (!baseRefArg) fail("--base requires a git ref, e.g. --base origin/main");
337
+ } else if (arg === "--hooks") {
338
+ if (command !== "init") fail(`--hooks is only valid for "stdd init"`);
339
+ hooksFlag = true;
340
+ } else if (arg === "--session-hook") {
341
+ if (command !== "init") fail(`--session-hook is only valid for "stdd init"`);
342
+ sessionHookFlag = true;
343
+ } else if (arg === "--stop-hook") {
344
+ if (command !== "init" && command !== "configure") {
345
+ fail(`--stop-hook is only valid for "stdd init" and "stdd configure"`);
346
+ }
347
+ stopHookFlag = true;
348
+ } else if (parsedArg === "--review-via") {
349
+ if (command !== "configure") fail(`--review-via is only valid for "stdd configure"`);
350
+ i = parsedValueIndex;
351
+ reviewViaArg = parsedValue;
352
+ if (!REVIEW_VIAS.includes(reviewViaArg)) {
353
+ fail(`--review-via must be one of: ${REVIEW_VIAS.join(", ")}`);
354
+ }
355
+ } else if (parsedArg === "--max-rounds") {
356
+ if (command !== "configure") fail(`--max-rounds is only valid for "stdd configure"`);
357
+ i = parsedValueIndex;
358
+ const value = parsedValue;
359
+ maxRoundsArg = Number(value);
360
+ if (!/^\d+$/.test(value) || !Number.isSafeInteger(maxRoundsArg)) {
361
+ fail("--max-rounds requires a non-negative safe integer (0 = unlimited)");
362
+ }
363
+ } else if (arg === "--interview") {
364
+ if (command !== "init") fail(`--interview is only valid for "stdd init"`);
365
+ interviewFlag = true;
366
+ } else if (parsedArg === "--capabilities") {
367
+ if (command !== "init" && command !== "configure") {
368
+ fail(`--capabilities is only valid for "stdd init" and "stdd configure"`);
369
+ }
370
+ i = parsedValueIndex;
371
+ capabilitiesArg = parseGenericList(parsedValue, "--capabilities", {
372
+ noun: "capability(ies)",
373
+ example: "subagents,worktrees",
374
+ known: KNOWN_CAPABILITIES,
375
+ });
376
+ } else if (arg === "--readiness") {
377
+ if (command !== "doctor") fail(`--readiness is only valid for "stdd doctor"`);
378
+ readinessOnly = true;
379
+ } else if (arg === "--watch") {
380
+ if (command !== "ci") fail(`--watch is only valid for "stdd ci"`);
381
+ watchFlag = true;
382
+ } else if (parsedArg === "--interval") {
383
+ if (command !== "ci") fail(`--interval is only valid for "stdd ci"`);
384
+ i = parsedValueIndex;
385
+ const value = parsedValue;
386
+ intervalArg = Number(value);
387
+ if (!Number.isFinite(intervalArg) || intervalArg < 0) {
388
+ fail("--interval requires seconds, e.g. --interval 15");
389
+ }
390
+ } else if (parsedArg === "--timeout") {
391
+ if (command !== "ci") fail(`--timeout is only valid for "stdd ci"`);
392
+ i = parsedValueIndex;
393
+ const value = parsedValue;
394
+ timeoutArg = Number(value);
395
+ if (!Number.isFinite(timeoutArg) || timeoutArg < 0) {
396
+ fail("--timeout requires seconds, e.g. --timeout 1800");
397
+ }
398
+ } else if (parsedArg === "--pr") {
399
+ if (command !== "check-pr") fail(`--pr is only valid for "stdd check-pr"`);
400
+ i = parsedValueIndex;
401
+ prArg = parsedValue;
402
+ if (!prArg) fail("--pr requires a PR number, or . for the current branch's PR");
403
+ } else if (parsedArg === "--ci") {
404
+ if (command !== "init") fail(`--ci is only valid for "stdd init"`);
405
+ i = parsedValueIndex;
406
+ ci = parseGenericList(parsedValue, "--ci", {
407
+ noun: "ci provider(s)",
408
+ example: "github",
409
+ known: KNOWN_CI,
410
+ });
411
+ try {
412
+ ci = validateAdapterSelection("ci", ci, KNOWN_CI);
413
+ } catch (err) {
414
+ fail(`--ci ${err.message.replace(/^ci /, "")}`);
415
+ }
416
+ } else if (parsedArg === "--tools") {
417
+ if (command !== "init") fail(`--tools is only valid for "stdd init"`);
418
+ i = parsedValueIndex;
419
+ tools = parseGenericList(parsedValue, "--tools", {
420
+ noun: "tool(s)",
421
+ example: "claude,codex,pi",
422
+ known: KNOWN_TOOLS,
423
+ });
424
+ try {
425
+ tools = validateAdapterSelection("tools", tools, KNOWN_TOOLS, {
426
+ nonEmpty: true,
427
+ });
428
+ } catch (err) {
429
+ fail(`--tools ${err.message.replace(/^tools /, "")}`);
430
+ }
431
+ } else if (arg.startsWith("--")) {
432
+ fail(`unknown flag: ${arg}`);
433
+ } else {
434
+ positional.push(arg);
435
+ }
436
+ }
437
+ if (positional.length > 1) fail(`unexpected argument: ${positional[1]}`);
438
+ const targetDir = path.resolve(positional[0] ?? ".");
439
+
440
+ switch (command) {
441
+ case "init": {
442
+ if (
443
+ interviewFlag &&
444
+ (tools || ci || capabilitiesArg || hooksFlag || sessionHookFlag || stopHookFlag)
445
+ ) {
446
+ fail("--interview replaces the other init flags — drop them and answer the questions instead");
447
+ }
448
+ const opts = interviewFlag
449
+ ? await interview()
450
+ : {
451
+ tools: tools ?? KNOWN_TOOLS,
452
+ ci: ci ?? [],
453
+ hooks: hooksFlag,
454
+ sessionHook: sessionHookFlag,
455
+ stopHook: stopHookFlag,
456
+ capabilitiesList: capabilitiesArg,
457
+ };
458
+ await init(targetDir, opts);
459
+ break;
460
+ }
461
+ case "configure":
462
+ await configure(targetDir, {
463
+ capabilitiesList: capabilitiesArg,
464
+ reviewVia: reviewViaArg,
465
+ maxRounds: maxRoundsArg,
466
+ stopHook: stopHookFlag,
467
+ });
468
+ break;
469
+ case "check":
470
+ check(targetDir);
471
+ break;
472
+ case "doctor":
473
+ await doctor(targetDir, readinessOnly);
474
+ break;
475
+ case "check-pr":
476
+ checkPr(positional[0], baseRefArg, prArg);
477
+ break;
478
+ case "evidence":
479
+ // Without an explicit dir, evidence anchors to the repo like the
480
+ // recorders — its config and ledger live at the root .stdd/.
481
+ evidence(positional[0] ? targetDir : resolveRepoDir(process.cwd()), baseRefArg);
482
+ break;
483
+ case "ci":
484
+ await ciCommand(positional[0] ?? null, watchFlag, intervalArg, timeoutArg);
485
+ break;
486
+ case "--version":
487
+ case "version":
488
+ console.log(VERSION);
489
+ break;
490
+ default: {
491
+ if (command) {
492
+ const guess = didYouMean(command, [
493
+ "init",
494
+ "configure",
495
+ "check",
496
+ "check-pr",
497
+ "evidence",
498
+ "doctor",
499
+ "status",
500
+ "task",
501
+ "docs",
502
+ "red",
503
+ "verify",
504
+ "note",
505
+ "defer",
506
+ "policy",
507
+ "slice",
508
+ "worker",
509
+ "scope",
510
+ "review",
511
+ "ci",
512
+ "version",
513
+ ]);
514
+ console.error(`stdd: unknown command "${command}"${guess ? ` — did you mean "${guess}"?` : ""}`);
515
+ }
516
+ console.log(
517
+ "Usage: stdd <init|configure|check|check-pr|evidence|doctor|task|status|ci|docs|red|verify|note|defer|policy|slice|worker|scope|review|stop-hook> " +
518
+ "[dir|pr-body-file|pr] [--tools claude,codex,pi] [--ci github,gitlab,generic] [--hooks] " +
519
+ "[--session-hook] [--interview] [--base <ref>] " +
520
+ "[--pr <n|.>] [--watch] [--readiness] [--json] [--gate] [--local] [--reason <why>] " +
521
+ "[--capabilities <list>] [--via subagent|codex|claude] [--review-via <route>] " +
522
+ "[--max-rounds <n>] [--stop-hook] [--cleanup] [--force] [--result <file|->] " +
523
+ "[--frozen <globs>] [--allowed <globs>] [--when <condition>] [--interval <s>] [--timeout <s>] [-- <cmd>]",
524
+ );
525
+ return command ? 1 : 0;
526
+ }
527
+ }
528
+ return 0;
529
+ }
530
+
531
+ try {
532
+ process.exitCode = await main();
533
+ } catch (error) {
534
+ console.error(`stdd: ${error?.message ?? String(error)}`);
535
+ process.exitCode = 1;
536
+ }