@stdd/plugin 0.9.1 → 0.10.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 (34) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/package.json +1 -1
  4. package/runtime/adapters/README.md +23 -12
  5. package/runtime/cli/check.mjs +1 -1
  6. package/runtime/cli/generated-files.mjs +37 -13
  7. package/runtime/cli/init.mjs +5 -59
  8. package/runtime/cli/status.mjs +14 -4
  9. package/runtime/cli/stdd.mjs +2 -21
  10. package/runtime/method/README.md +63 -13
  11. package/runtime/method/reference-commands.md +7 -1
  12. package/runtime/method/reference-integration.md +55 -50
  13. package/runtime/package.json +1 -1
  14. package/runtime/playbooks/brainstorming.md +56 -37
  15. package/runtime/playbooks/delegate-slice.md +7 -1
  16. package/runtime/playbooks/finish-change.md +10 -3
  17. package/runtime/playbooks/investigation.md +11 -5
  18. package/runtime/playbooks/planning.md +5 -1
  19. package/runtime/playbooks/start-change.md +30 -16
  20. package/runtime/sdk/adapters.mjs +18 -66
  21. package/runtime/sdk/index.d.ts +0 -9
  22. package/runtime/sdk/index.mjs +0 -4
  23. package/skills/stdd-brainstorming/SKILL.md +56 -37
  24. package/skills/stdd-debugging/SKILL.md +1 -1
  25. package/skills/stdd-delegate-slice/SKILL.md +8 -2
  26. package/skills/stdd-finish-change/SKILL.md +11 -4
  27. package/skills/stdd-implement/SKILL.md +1 -1
  28. package/skills/stdd-investigation/SKILL.md +11 -5
  29. package/skills/stdd-planning/SKILL.md +6 -2
  30. package/skills/stdd-pr-green/SKILL.md +1 -1
  31. package/skills/stdd-start-change/SKILL.md +30 -16
  32. package/skills/stdd-worktrees/SKILL.md +1 -1
  33. package/runtime/templates/github-stdd.yml +0 -42
  34. package/runtime/templates/gitlab-stdd.yml +0 -72
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stdd",
3
- "version": "0.9.1",
3
+ "version": "0.10.0",
4
4
  "description": "Native STDD workflow skills and lifecycle context",
5
5
  "author": {
6
6
  "name": "Azamat Almazbek uulu"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stdd",
3
- "version": "0.9.1",
3
+ "version": "0.10.0",
4
4
  "description": "Native STDD workflow skills and lifecycle context for Codex",
5
5
  "author": {
6
6
  "name": "Azamat Almazbek uulu"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stdd/plugin",
3
- "version": "0.9.1",
3
+ "version": "0.10.0",
4
4
  "description": "Universal STDD workflow skills and lifecycle integration for Codex, Claude Code, and Pi",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -6,6 +6,15 @@ runs the adapters; re-run it after upgrading stdd to refresh the output.
6
6
 
7
7
  ## Common output
8
8
 
9
+ Every host receives the same five-route always-on contract. Investigation
10
+ answers current-state factual or diagnostic questions directly, Brainstorming
11
+ explores opinions and future or hypothetical behavior directly, and only
12
+ explicit intent to persist or modify the repository enters Start Change.
13
+ Implement and Finish Change execute and close that action. Unknown current
14
+ facts may cause Investigation → Brainstorming; ordinary docs or code reading
15
+ inside Brainstorming does not. The renderer preserves host-native invocation
16
+ syntax: Claude `/name`, Codex `$name`, and Pi `/skill:name`.
17
+
9
18
  Every init installs `.stdd/` into the target repo:
10
19
 
11
20
  ```
@@ -95,18 +104,7 @@ the extension queues one corrective follow-up model turn and then fails open;
95
104
  it never creates an unbounded continuation loop. A conflicting user-owned
96
105
  `.pi/extensions/stdd.js` is not overwritten.
97
106
 
98
- ## CI
99
-
100
- CI adapters only transport provider state into portable CLI commands:
101
-
102
- - GitHub writes `.github/workflows/stdd.yml`;
103
- - GitLab writes `.gitlab/stdd.gitlab-ci.yml`; same-project MRs authenticate
104
- with `CI_JOB_TOKEN`, while a fork source project must be on the target's
105
- CI job-token allowlist. A trusted controlled fork may instead provide a
106
- masked and hidden `STDD_GITLAB_READ_API_TOKEN` scoped to target-project
107
- `read_api`; target secrets must never be exposed to untrusted fork code;
108
- - generic prints the `check` and `check-pr` command contract without writing
109
- provider configuration.
107
+ ## Adapter composition and renderer tokens
110
108
 
111
109
  The public SDK exposes the built-in adapter registry and render functions so
112
110
  other packages can add a host without importing `cli/` internals.
@@ -128,6 +126,15 @@ capabilities (`subagents` on, `crossCli` off). Its planning skill names
128
126
  `--via subagent`; it never names a cross-CLI reviewer, emits a renderer token,
129
127
  or falls back to manual self-review.
130
128
 
129
+ ## CI
130
+
131
+ Adapters compile playbooks for agents; they do not write CI. A provider
132
+ workflow is infrastructure the repository owns, and stdd generates none of it.
133
+ The contract a job composes is `stdd check .` over the checkout and the live
134
+ review description piped to `stdd check-pr - --base <ref>`; see the `## CI`
135
+ section of `method/reference-integration.md` for the three things a
136
+ hand-written job has to get right.
137
+
131
138
  ## Plugin distribution
132
139
 
133
140
  `plugins/stdd/` is one generated distribution for Codex, Claude Code, and Pi.
@@ -148,6 +155,10 @@ fail-open and never forward arbitrary child output.
148
155
 
149
156
  ## Design rules for adapters
150
157
 
158
+ - The five direct/action routes are mandatory and stay semantically identical
159
+ across hosts; only their invocation syntax changes.
160
+ - Idle lifecycle output is neutral: discussion and read-only work do not need a
161
+ task, and generated hooks do not prime agents to create one.
151
162
  - One source of truth: adapters copy or point, never fork playbook content.
152
163
  - Always-on instructions contain invariants and routing only; detailed
153
164
  workflows live in lazy skills.
@@ -529,7 +529,7 @@ export async function doctor(targetDir, readinessOnly = false) {
529
529
  report(
530
530
  false,
531
531
  `.github/workflows/${file} validates the frozen event payload body — ` +
532
- "body edits will not be re-checked; see stdd init --ci github",
532
+ "body edits will not be re-checked; fetch it live from the API instead",
533
533
  );
534
534
  }
535
535
  }
@@ -5,7 +5,6 @@ import { fileURLToPath } from "node:url";
5
5
  import {
6
6
  AGENT_ADAPTERS,
7
7
  assertSemanticVersion,
8
- CI_ADAPTERS,
9
8
  getAgentAdapter,
10
9
  renderAgentInstructions,
11
10
  } from "../sdk/adapters.mjs";
@@ -27,7 +26,6 @@ import { MANIFEST_HASH_PATTERN } from "./state-validation.mjs";
27
26
  export const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
28
27
  export const VERSION = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, "package.json"), "utf8")).version;
29
28
  export const KNOWN_TOOLS = Object.keys(AGENT_ADAPTERS);
30
- export const KNOWN_CI = Object.keys(CI_ADAPTERS);
31
29
  export const KNOWN_CAPABILITIES = Object.keys(DEFAULT_CONFIG.capabilities);
32
30
  export const CLEANUP_JOURNAL_REL = ".stdd/cleanup-transaction.json";
33
31
  const SHIPPED_PLAYBOOK_FILES = new Set(
@@ -79,14 +77,30 @@ export function validateAdapterSelection(field, values, known, { nonEmpty = fals
79
77
  return [...values];
80
78
  }
81
79
 
80
+ // `ci` is a retired key, not a required one: installs made before provider CI
81
+ // adapters were removed still carry it, and rejecting their manifest would
82
+ // turn an upgrade into a hard failure. It is accepted, never validated against
83
+ // a registry, and never written back.
84
+ const RETIRED_MANIFEST_TARGET_KEYS = ["ci"];
85
+ const RETIRED_CI_PROVIDERS = ["github", "gitlab", "generic"];
86
+
87
+ // The paths those adapters used to write. Nothing generates them any more, but
88
+ // a pre-0.10.0 manifest still lists them, so they stay recognized outputs —
89
+ // otherwise the upgrade fails validation before it can do anything. They are
90
+ // released rather than retired, so the sweep never deletes an adopter's CI
91
+ // gate — see finalizeGeneratedFilesWithCapabilities.
92
+ const RETIRED_GENERATED_OUTPUTS = [".github/workflows/stdd.yml", ".gitlab/stdd.gitlab-ci.yml"];
93
+
82
94
  function validateManifestTargets(value) {
83
- const required = ["tools", "ci", "hooks", "sessionHook", "stopHook"];
95
+ const required = ["tools", "hooks", "sessionHook", "stopHook"];
84
96
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
85
97
  throw new TypeError("must be an object");
86
98
  }
87
99
  const keys = Object.keys(value);
88
100
  const missing = required.filter((key) => !Object.hasOwn(value, key));
89
- const unknown = keys.filter((key) => !required.includes(key));
101
+ const unknown = keys.filter(
102
+ (key) => !required.includes(key) && !RETIRED_MANIFEST_TARGET_KEYS.includes(key),
103
+ );
90
104
  if (missing.length > 0 || unknown.length > 0) {
91
105
  throw new TypeError(
92
106
  `must contain exactly ${required.join(", ")}${
@@ -97,13 +111,17 @@ function validateManifestTargets(value) {
97
111
  const tools = validateAdapterSelection("tools", value.tools, KNOWN_TOOLS, {
98
112
  nonEmpty: true,
99
113
  });
100
- const ci = validateAdapterSelection("ci", value.ci, KNOWN_CI);
114
+ // A retired key is still graded before it is discarded: tolerating the
115
+ // upgrade is not the same as accepting a corrupt manifest, and `check`
116
+ // exists to notice one.
117
+ if (Object.hasOwn(value, "ci")) {
118
+ validateAdapterSelection("ci", value.ci, RETIRED_CI_PROVIDERS);
119
+ }
101
120
  for (const field of ["hooks", "sessionHook", "stopHook"]) {
102
121
  if (typeof value[field] !== "boolean") throw new TypeError(`${field} must be a boolean`);
103
122
  }
104
123
  return {
105
124
  tools,
106
- ci,
107
125
  hooks: value.hooks,
108
126
  sessionHook: value.sessionHook,
109
127
  stopHook: value.stopHook,
@@ -188,9 +206,7 @@ function isRecognizedGeneratedOutput(file) {
188
206
  const exact = new Set([
189
207
  ".stdd/method.md",
190
208
  ...Object.values(AGENT_ADAPTERS).map((adapter) => adapter.snippetFile),
191
- ...Object.values(CI_ADAPTERS)
192
- .map((adapter) => adapter.outputFile)
193
- .filter((output) => output !== null),
209
+ ...RETIRED_GENERATED_OUTPUTS,
194
210
  ...[...KNOWN_MANAGED_PLAYBOOK_FILES].map((name) => `.stdd/playbooks/${name}`),
195
211
  ]);
196
212
  if (exact.has(file)) return true;
@@ -1116,6 +1132,18 @@ export async function finalizeGeneratedFilesWithCapabilities(
1116
1132
  try {
1117
1133
  for (const [file, hash] of Object.entries(oldFiles)) {
1118
1134
  if (Object.hasOwn(generated, file)) continue;
1135
+ // Release, do not retire. A pre-0.10.0 install lists a generated
1136
+ // provider workflow here; nothing generates one now, so the sweep
1137
+ // below would delete it for being byte-identical and unclaimed — an
1138
+ // upgrade that silently removes the repository's CI gate. Skipping it
1139
+ // leaves the file on disk and out of the new manifest: the adopter
1140
+ // owns it from here, edits and all.
1141
+ if (RETIRED_GENERATED_OUTPUTS.includes(file)) {
1142
+ console.log(
1143
+ `Left ${file} in place — stdd no longer generates provider CI; it is yours to maintain`,
1144
+ );
1145
+ continue;
1146
+ }
1119
1147
  const inspected = await nativeInspectOutput(context, file);
1120
1148
  if (inspected.kind === "missing") continue;
1121
1149
  if (inspected.kind !== "ok") {
@@ -1356,7 +1384,6 @@ export async function finalizeGeneratedFilesWithCapabilities(
1356
1384
  files: generated,
1357
1385
  targets: {
1358
1386
  tools: targets.tools,
1359
- ci: targets.ci,
1360
1387
  hooks: targets.hooks,
1361
1388
  sessionHook: targets.sessionHook,
1362
1389
  stopHook: targets.stopHook,
@@ -1470,9 +1497,6 @@ function discoverGeneratedOutputs(targetDir) {
1470
1497
  ...loadLocalPlaybooks(targetDir).map((playbook) => playbook.meta.name),
1471
1498
  ]);
1472
1499
  for (const playbook of shippedPlaybooks) addIfPresent(`.stdd/playbooks/${playbook.file}`);
1473
- for (const adapter of Object.values(CI_ADAPTERS)) {
1474
- if (adapter.outputFile) addIfPresent(adapter.outputFile);
1475
- }
1476
1500
 
1477
1501
  for (const adapter of Object.values(AGENT_ADAPTERS)) {
1478
1502
  // Shipped and validated local skill names reserve generated-output paths. Surface an
@@ -3,13 +3,11 @@ import path from "node:path";
3
3
  import { createInterface } from "node:readline/promises";
4
4
  import {
5
5
  AGENT_ADAPTERS,
6
- CI_ADAPTERS,
7
6
  CROSS_CLI_REVIEW_VIA_TOKEN,
8
7
  getAgentAdapter,
9
8
  MANDATORY_ROUTING_SKILLS,
10
9
  renderAgentInstructions,
11
10
  renderAgentSkill,
12
- renderCiTemplate,
13
11
  } from "../sdk/adapters.mjs";
14
12
  import { resolveWritableRepoPath } from "../sdk/path.mjs";
15
13
  import { hasLocalStddBinary, isStddSourceCheckout, prepareAgentHooks } from "./claude-hooks.mjs";
@@ -29,7 +27,6 @@ import {
29
27
  renderInstalledMethod,
30
28
  SOURCE_RUNNER,
31
29
  STAMP,
32
- VERSION,
33
30
  validateAdapterSelection,
34
31
  } from "./generated-files.mjs";
35
32
  import {
@@ -233,9 +230,6 @@ export async function interview() {
233
230
  // The first selected native host is the driver for the repository-level
234
231
  // default. Generated skills still carry a per-host explicit override.
235
232
  const reviewVia = await askReviewVia(ask, close, recommendedReviewVia(tools, capabilities));
236
- const ci = (await yes("Install the GitHub Actions gate (stdd check + PR evidence)?", true))
237
- ? ["github"]
238
- : [];
239
233
  const hooks = await yes("Install the pre-push hook (stdd check — fast, offline)?", true);
240
234
  const sessionHook =
241
235
  tools.length > 0 ? await yes("Wire native agent session hooks (stdd status --local)?", true) : false;
@@ -261,7 +255,6 @@ export async function interview() {
261
255
  }
262
256
  return {
263
257
  tools,
264
- ci,
265
258
  hooks,
266
259
  sessionHook,
267
260
  stopHook,
@@ -301,14 +294,12 @@ export async function configure(targetDir, opts) {
301
294
  }
302
295
  // installs made before targets were remembered: infer what the previous
303
296
  // init actually GENERATED from manifest.files — live directories lie (a
304
- // stray empty .claude/skills must not smuggle claude in) and an
305
- // inferred blank would make the stale-file cleanup delete the CI
306
- // workflow. The filesystem is the last resort with no usable manifest;
307
- // hook files and settings entries are user-owned, never
308
- // manifest-tracked, so they are always read from their files.
297
+ // stray empty .claude/skills must not smuggle claude in). The filesystem
298
+ // is the last resort with no usable manifest; hook files and settings
299
+ // entries are user-owned, never manifest-tracked, so they are always read
300
+ // from their files.
309
301
  if (!targets) {
310
302
  const tools = [];
311
- const ci = [];
312
303
  const skillRootCounts = new Map();
313
304
  for (const adapter of Object.values(AGENT_ADAPTERS)) {
314
305
  skillRootCounts.set(adapter.skillRoot, (skillRootCounts.get(adapter.skillRoot) ?? 0) + 1);
@@ -324,9 +315,6 @@ export async function configure(targetDir, opts) {
324
315
  tools.push(adapter.id);
325
316
  }
326
317
  }
327
- for (const adapter of Object.values(CI_ADAPTERS)) {
328
- if (adapter.outputFile && manifestFiles.includes(adapter.outputFile)) ci.push(adapter.id);
329
- }
330
318
  } else {
331
319
  for (const adapter of Object.values(AGENT_ADAPTERS)) {
332
320
  const ownsDistinctSkillRoot = skillRootCounts.get(adapter.skillRoot) === 1;
@@ -337,11 +325,6 @@ export async function configure(targetDir, opts) {
337
325
  tools.push(adapter.id);
338
326
  }
339
327
  }
340
- for (const adapter of Object.values(CI_ADAPTERS)) {
341
- if (adapter.outputFile && fs.existsSync(path.join(targetDir, adapter.outputFile))) {
342
- ci.push(adapter.id);
343
- }
344
- }
345
328
  }
346
329
  let settingsText = "";
347
330
  for (const relative of new Set(Object.values(AGENT_ADAPTERS).map((adapter) => adapter.hooksFile))) {
@@ -353,7 +336,6 @@ export async function configure(targetDir, opts) {
353
336
  }
354
337
  targets = {
355
338
  tools: tools.length > 0 ? tools : ["claude"],
356
- ci,
357
339
  hooks: fs.existsSync(path.join(targetDir, ".stdd", "hooks", "pre-push")),
358
340
  sessionHook:
359
341
  settingsText.includes("stdd status") ||
@@ -414,14 +396,8 @@ export async function configure(targetDir, opts) {
414
396
  );
415
397
  }
416
398
  const desiredStopHook = stopHook || targets.stopHook;
417
- const existingCi = targets.ci.filter((provider) => {
418
- const outputFile = CI_ADAPTERS[provider].outputFile;
419
- return outputFile === null || fs.existsSync(path.join(targetDir, outputFile));
420
- });
421
399
  await init(targetDir, {
422
400
  tools: targets.tools,
423
- ci: existingCi,
424
- rememberedCiTargets: targets.ci,
425
401
  hooks: false,
426
402
  sessionHook: false,
427
403
  stopHook: desiredStopHook,
@@ -437,9 +413,8 @@ export async function configure(targetDir, opts) {
437
413
  }
438
414
 
439
415
  export async function init(targetDir, opts) {
440
- const { tools, ci, hooks, sessionHook, capabilitiesList } = opts;
416
+ const { tools, hooks, sessionHook, capabilitiesList } = opts;
441
417
  const stopHook = Boolean(opts.stopHook);
442
- const rememberedCiTargets = opts.rememberedCiTargets ?? ci;
443
418
  const rememberedHookTargets = opts.rememberedHookTargets ?? {
444
419
  hooks: Boolean(hooks),
445
420
  sessionHook: Boolean(sessionHook),
@@ -553,19 +528,6 @@ export async function init(targetDir, opts) {
553
528
  }),
554
529
  });
555
530
  }
556
- const ciPlans = new Map();
557
- for (const provider of ci) {
558
- const adapter = CI_ADAPTERS[provider];
559
- if (adapter.outputFile !== null) {
560
- ciPlans.set(
561
- provider,
562
- renderCiTemplate(
563
- fs.readFileSync(path.join(PKG_ROOT, "templates", adapter.templateFile), "utf8"),
564
- { stamp: STAMP, version: VERSION },
565
- ),
566
- );
567
- }
568
- }
569
531
  const publicationPaths = new Set([".stdd/method.md", ".stdd/config.json", ".gitignore"]);
570
532
  for (const pb of kitActive) publicationPaths.add(`.stdd/playbooks/${pb.file}`);
571
533
  for (const { adapter, skills } of toolPlans.values()) {
@@ -578,7 +540,6 @@ export async function init(targetDir, opts) {
578
540
  for (const adapter of Object.values(AGENT_ADAPTERS)) {
579
541
  publicationPaths.add(adapter.instructionsFile);
580
542
  }
581
- for (const provider of ciPlans.keys()) publicationPaths.add(CI_ADAPTERS[provider].outputFile);
582
543
  if (hooks) publicationPaths.add(".stdd/hooks/pre-push");
583
544
  publicationPaths.add(".stdd/policy.md");
584
545
  for (const relative of publicationPaths) {
@@ -755,20 +716,6 @@ export async function init(targetDir, opts) {
755
716
  console.log(`Removed the managed STDD section from deselected ${adapter.instructionsFile}`);
756
717
  }
757
718
 
758
- for (const provider of ci) {
759
- const adapter = CI_ADAPTERS[provider];
760
- if (adapter.outputFile === null) {
761
- console.log(
762
- `Portable CI contract for ${adapter.id} (compose with your provider's checkout and live PR/MR body):\n` +
763
- ` npx --yes @stdd/cli@${VERSION} check .\n` +
764
- ` printf '%s' "$REVIEW_BODY" | npx --yes @stdd/cli@${VERSION} check-pr - --base "$BASE_REF"`,
765
- );
766
- continue;
767
- }
768
- await writeGenerated(adapter.outputFile, ciPlans.get(provider));
769
- console.log(`Installed ${adapter.outputFile} (${provider} live review evidence)`);
770
- }
771
-
772
719
  // Repository-owned standing decisions. Seeded once and then hands-off:
773
720
  // user-owned after generation like config.json, never manifested, so a
774
721
  // recorded permission survives every later init.
@@ -865,7 +812,6 @@ export async function init(targetDir, opts) {
865
812
  retainedCleanupJournals: [...previouslyRetainedCleanupJournals, ...recoveredCleanupJournals],
866
813
  targets: {
867
814
  tools,
868
- ci: rememberedCiTargets,
869
815
  hooks: rememberedHookTargets.hooks,
870
816
  sessionHook: rememberedHookTargets.sessionHook,
871
817
  stopHook: rememberedHookTargets.stopHook,
@@ -89,12 +89,12 @@ export function status(cwd, asJson, localOnly = false) {
89
89
  plan: { present: false },
90
90
  review: null,
91
91
  pr: { state: "unknown", reason: "idle task" },
92
- next: 'start a task with `stdd task start "<short name>"`',
92
+ next: "no task is required for discussion or read-only work",
93
93
  };
94
94
  if (asJson) console.log(JSON.stringify(idle, null, "\t"));
95
95
  else
96
96
  console.log(
97
- `task: idle on ${branch}\nnext: start a task with \`stdd task start "<short name>"\``,
97
+ `task: idle on ${branch}\nnext: no task is required for discussion or read-only work`,
98
98
  );
99
99
  return;
100
100
  }
@@ -225,11 +225,21 @@ export function status(cwd, asJson, localOnly = false) {
225
225
  : `enable a compatible review capability/route, then run ${reviewInvocation}${
226
226
  reviewBudgetSpent ? " deliberately" : " again"
227
227
  }`;
228
- const planReviewSatisfied = Boolean(plan.present && plan.review?.present && plan.review.done);
228
+ // The closing review rides on coordination — a plan that ordered the work, or
229
+ // a slice handed to a worker whose code the orchestrator never watched being
230
+ // written. A single slice coordinates nothing, claims no review, and is owed
231
+ // none, so nothing is named. Expectation and completion are separate
232
+ // questions: reading a missing plan as an unfinished review is what made
233
+ // `status` ask after every verified loop, whatever the change's size.
234
+ const reviewExpected = Boolean(latestReview) || plan.present || Boolean(scopeEvent);
229
235
  const recordedReviewSatisfied = latestReview?.verdict === "approved" && !reviewStale;
230
236
  // Once a ledger verdict exists it is authoritative; a checked legacy
231
237
  // heuristic item must never hide a newer failed or stale review.
232
- const reviewSatisfied = latestReview ? recordedReviewSatisfied : planReviewSatisfied;
238
+ const reviewSatisfied = !reviewExpected
239
+ ? true
240
+ : latestReview
241
+ ? recordedReviewSatisfied
242
+ : Boolean(plan.review?.done);
233
243
  const reviewNeedsAction = !reviewSatisfied;
234
244
  const reviewFailureGuidance =
235
245
  latestReview?.verdict === "changes-requested"
@@ -8,7 +8,6 @@ import { loadConfig } from "./config.mjs";
8
8
  import { checkPr, evidence } from "./evidence.mjs";
9
9
  import {
10
10
  KNOWN_CAPABILITIES,
11
- KNOWN_CI,
12
11
  KNOWN_TOOLS,
13
12
  VERSION,
14
13
  validateAdapterSelection,
@@ -300,7 +299,6 @@ async function main() {
300
299
  }
301
300
  }
302
301
  let tools = null;
303
- let ci = null;
304
302
  let baseRefArg = null;
305
303
  let prArg = null;
306
304
  let readinessOnly = false;
@@ -400,19 +398,6 @@ async function main() {
400
398
  i = parsedValueIndex;
401
399
  prArg = parsedValue;
402
400
  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
401
  } else if (parsedArg === "--tools") {
417
402
  if (command !== "init") fail(`--tools is only valid for "stdd init"`);
418
403
  i = parsedValueIndex;
@@ -439,17 +424,13 @@ async function main() {
439
424
 
440
425
  switch (command) {
441
426
  case "init": {
442
- if (
443
- interviewFlag &&
444
- (tools || ci || capabilitiesArg || hooksFlag || sessionHookFlag || stopHookFlag)
445
- ) {
427
+ if (interviewFlag && (tools || capabilitiesArg || hooksFlag || sessionHookFlag || stopHookFlag)) {
446
428
  fail("--interview replaces the other init flags — drop them and answer the questions instead");
447
429
  }
448
430
  const opts = interviewFlag
449
431
  ? await interview()
450
432
  : {
451
433
  tools: tools ?? KNOWN_TOOLS,
452
- ci: ci ?? [],
453
434
  hooks: hooksFlag,
454
435
  sessionHook: sessionHookFlag,
455
436
  stopHook: stopHookFlag,
@@ -515,7 +496,7 @@ async function main() {
515
496
  }
516
497
  console.log(
517
498
  "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] " +
499
+ "[dir|pr-body-file|pr] [--tools claude,codex,pi] [--hooks] " +
519
500
  "[--session-hook] [--interview] [--base <ref>] " +
520
501
  "[--pr <n|.>] [--watch] [--readiness] [--json] [--gate] [--local] [--reason <why>] " +
521
502
  "[--capabilities <list>] [--via subagent|codex|claude] [--review-via <route>] " +
@@ -94,6 +94,41 @@ classify → read docs → docs edit (the spec) → failing test → implement
94
94
  The base comes from `--base` or the `baseRef` key in `.stdd/config.json`;
95
95
  there is no built-in default.
96
96
 
97
+ ## Proportionality
98
+
99
+ The default route for an agreed change is one slice: the docs decision, a
100
+ failing test where one applies, the implementation, a fresh verify. A plan, a
101
+ delegated worker, and an independent review are escalations from that route,
102
+ each with a condition below. None of it touches proof — the docs decision, a
103
+ genuine red where a test applies, and a fresh verify hold at every size.
104
+ Proportionality cuts paperwork, never evidence.
105
+
106
+ A change is one slice when, at the moment of deciding, it has one agreed
107
+ observable outcome, one coherent implementation boundary, one acceptance check,
108
+ and no known dependency on another independently verifiable change. Escalate as
109
+ soon as any of four things appears: a second independent outcome, an ordering
110
+ dependency between parts, a need to hand work to another session, or a design
111
+ decision nobody has made yet. They are usually discovered mid-work. Escalating
112
+ then is the normal case, not a failed classification — the criterion is re-read
113
+ as the work goes, never declared once at the start.
114
+
115
+ Two independent axes decide which escalation applies. **Coordination
116
+ complexity** decides the plan and delegation: work that must be ordered, split,
117
+ or handed over needs a durable plan, because those artifacts exist against
118
+ memory that does not survive compaction or a handoff. **Consequence** is why a human may want an
119
+ independent review that coordination did not already require: a two-line change
120
+ to authorization or pricing can carry more of it than a two-hundred-line
121
+ rename. `stdd status` names the review from coordination alone, because
122
+ coordination is what it can observe; consequence is a judgement, and
123
+ `stdd review` is callable for it at any moment. Which surfaces carry
124
+ consequence is the adopting team's contract, not this kit's — see "What stdd
125
+ does not cover". Diff size decides neither axis; it proxies both and measures
126
+ neither.
127
+
128
+ A PR, and the CI wait that follows it, ride on the delivery boundary the user
129
+ asked for. A change requested as a local edit is complete when it is verified
130
+ locally.
131
+
97
132
  ## The frontend exception: design-first
98
133
 
99
134
  Frontend **visual** work — layout, styling, markup structure, presentation
@@ -220,10 +255,17 @@ Agent adapters have two outputs with deliberately different context costs:
220
255
  - a short, always-on instruction block carrying only repository invariants;
221
256
  - native, lazily loaded skills carrying the task workflows.
222
257
 
223
- Three routing skills make the main path explicit instead of asking an agent
224
- to infer a workflow from a flat list: `stdd-start-change` classifies first,
225
- opens a task only for repository-changing work, and routes read-only questions
226
- without writing state; `stdd-implement` runs the docs/red/green/verify loop, and
258
+ Five routing skills make the main path explicit instead of asking an agent
259
+ to infer a workflow from a flat list. `stdd-investigation` directly answers
260
+ current-state factual and diagnostic questions with evidence;
261
+ `stdd-brainstorming` directly explores opinions, future behavior, and
262
+ hypothetical implementation approaches. Both are read-only and create no task,
263
+ ledger event, persisted artifact, or repository mutation. When unknown current
264
+ facts materially affect a design, they may run in sequence as Investigation →
265
+ Brainstorming; reading docs or code while brainstorming does not itself switch
266
+ workflows. `stdd-start-change` begins only after explicit intent to persist a
267
+ work artifact or modify the repository; a hypothetical plan shown in chat stays
268
+ Brainstorming. `stdd-implement` runs the docs/red/green/verify loop, and
227
269
  `stdd-finish-change` closes review, evidence, PR checks, and any requested
228
270
  runtime verification. Specialized playbooks remain independently invocable.
229
271
 
@@ -249,7 +291,9 @@ half-written state — see
249
291
  `stdd status --json` has one stable top-level shape in every lifecycle
250
292
  state: `state`, `task`, `branch`, `loop`, `slice`, `plan`, `review`, `pr`,
251
293
  and `next` are always present. Idle state uses explicit empty/null values,
252
- so integrations never need a second response schema.
294
+ so integrations never need a second response schema. Its string-valued `next`
295
+ is neutral: no task is required for discussion or read-only work, and a task
296
+ starts only when the user chooses persisted or repository-changing action.
253
297
 
254
298
  Readers consider only the current branch's active task. A plan that was
255
299
  already present when the task started stays invisible until rewritten for
@@ -315,11 +359,14 @@ snapshot. A passing verify becomes stale after any later checkout change;
315
359
  current proof. Older ledger events without snapshots remain readable but
316
360
  are explicitly reported as legacy evidence. Timing
317
361
  leaves the prose: run `stdd status` at session start and before opening a
318
- PR. Once the loop is verified and the plan is exhausted, the closing
319
- review is the named next step ahead of the evidence line when the
320
- capability profile has a dispatch route on (`subagents` or `crossCli`),
321
- `status` says to dispatch the fresh reviewer explicitly; with both off
322
- the suggestion is omitted rather than degraded to self-review.
362
+ PR. Once the loop is verified, `status` names the closing review only when
363
+ something expects one: a plan is present, a slice was delegated (a recorded
364
+ `scope` event), or a review verdict is already recorded. With none of the
365
+ three it goes straight to the evidence line a single slice makes no review
366
+ claim and is not asked for one. Where a review is expected, it is named ahead
367
+ of the evidence line when the capability profile has a dispatch route on
368
+ (`subagents` or `crossCli`); with both off the suggestion is omitted rather
369
+ than degraded to self-review.
323
370
 
324
371
  ## The durable plan and `stdd defer`
325
372
 
@@ -358,7 +405,10 @@ delegated work alike, and its reviewer is a fresh context (a read-only
358
405
  subagent or the other CLI, per the capability profile) that sees the plan
359
406
  and the diff, never the implementing session's history. With both dispatch
360
407
  capabilities off, capability compilation omits the review item and closing
361
- review guidance entirely; it never substitutes self-review.
408
+ review guidance entirely; it never substitutes self-review. A change that
409
+ needed no plan carries no such item, makes no review claim, and is not asked
410
+ for one — the review rides on coordination, and `stdd review` stays callable
411
+ at any moment for a change whose consequence warrants it.
362
412
 
363
413
  The review item carries a `[review:]` tag, and the tag follows the same
364
414
  claim-vs-proof rule as `[red:]`: the checkbox is a claim, the ledger is
@@ -402,8 +452,8 @@ authority.
402
452
  A permission's action comes from a closed set: `merge`, `deploy`, `publish`,
403
453
  `migrate`, `force-push`, and `external-mutation`. Any other action is rejected,
404
454
  which is also why policy cannot waive a method gate — the docs edit, a genuine
405
- red, verification, the closing review, and `stdd check` are not actions the
406
- file can name. Policy widens what an agent may do without asking; it never
455
+ red, verification, a closing review the plan claims, and `stdd check` are not
456
+ actions the file can name. Policy widens what an agent may do without asking; it never
407
457
  narrows what the loop must prove.
408
458
 
409
459
  The set is enforced when the document is read, not only when `stdd policy`
@@ -98,7 +98,13 @@ formatting characters are rejected before durable state is written.
98
98
 
99
99
  A managed worker sandbox created by `stdd worker create` requires an active
100
100
  task and an already recorded docs
101
- decision. Its destination must not exist. Managed create and collect use the
101
+ decision. Its destination must not exist, must be outside the source checkout,
102
+ and must be outside any Git repository — a sandbox carries no `.git` and must
103
+ not be swept up by a surrounding one. That puts it beside the project rather
104
+ than inside it, so the convention is one hidden container,
105
+ `../.stdd-workers/<slice>`: a directory of projects then collects a single
106
+ `.stdd-workers/` however many slices are delegated, instead of one visible
107
+ sibling each. Managed create and collect use the
102
108
  native mutation helper and fail before mutation when the destination
103
109
  filesystem cannot provide the required capability guarantees. Creation copies
104
110
  the checkout's tracked and non-ignored untracked files at