@stdd/plugin 0.9.2 → 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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stdd",
3
- "version": "0.9.2",
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.2",
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.2",
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",
@@ -104,18 +104,7 @@ the extension queues one corrective follow-up model turn and then fails open;
104
104
  it never creates an unbounded continuation loop. A conflicting user-owned
105
105
  `.pi/extensions/stdd.js` is not overwritten.
106
106
 
107
- ## CI
108
-
109
- CI adapters only transport provider state into portable CLI commands:
110
-
111
- - GitHub writes `.github/workflows/stdd.yml`;
112
- - GitLab writes `.gitlab/stdd.gitlab-ci.yml`; same-project MRs authenticate
113
- with `CI_JOB_TOKEN`, while a fork source project must be on the target's
114
- CI job-token allowlist. A trusted controlled fork may instead provide a
115
- masked and hidden `STDD_GITLAB_READ_API_TOKEN` scoped to target-project
116
- `read_api`; target secrets must never be exposed to untrusted fork code;
117
- - generic prints the `check` and `check-pr` command contract without writing
118
- provider configuration.
107
+ ## Adapter composition and renderer tokens
119
108
 
120
109
  The public SDK exposes the built-in adapter registry and render functions so
121
110
  other packages can add a host without importing `cli/` internals.
@@ -137,6 +126,15 @@ capabilities (`subagents` on, `crossCli` off). Its planning skill names
137
126
  `--via subagent`; it never names a cross-CLI reviewer, emits a renderer token,
138
127
  or falls back to manual self-review.
139
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
+
140
138
  ## Plugin distribution
141
139
 
142
140
  `plugins/stdd/` is one generated distribution for Codex, Claude Code, and Pi.
@@ -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,
@@ -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
@@ -324,11 +359,14 @@ snapshot. A passing verify becomes stale after any later checkout change;
324
359
  current proof. Older ledger events without snapshots remain readable but
325
360
  are explicitly reported as legacy evidence. Timing
326
361
  leaves the prose: run `stdd status` at session start and before opening a
327
- PR. Once the loop is verified and the plan is exhausted, the closing
328
- review is the named next step ahead of the evidence line when the
329
- capability profile has a dispatch route on (`subagents` or `crossCli`),
330
- `status` says to dispatch the fresh reviewer explicitly; with both off
331
- 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.
332
370
 
333
371
  ## The durable plan and `stdd defer`
334
372
 
@@ -367,7 +405,10 @@ delegated work alike, and its reviewer is a fresh context (a read-only
367
405
  subagent or the other CLI, per the capability profile) that sees the plan
368
406
  and the diff, never the implementing session's history. With both dispatch
369
407
  capabilities off, capability compilation omits the review item and closing
370
- 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.
371
412
 
372
413
  The review item carries a `[review:]` tag, and the tag follows the same
373
414
  claim-vs-proof rule as `[red:]`: the checkbox is a claim, the ledger is
@@ -411,8 +452,8 @@ authority.
411
452
  A permission's action comes from a closed set: `merge`, `deploy`, `publish`,
412
453
  `migrate`, `force-push`, and `external-mutation`. Any other action is rejected,
413
454
  which is also why policy cannot waive a method gate — the docs edit, a genuine
414
- red, verification, the closing review, and `stdd check` are not actions the
415
- 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
416
457
  narrows what the loop must prove.
417
458
 
418
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
@@ -88,9 +88,9 @@ repository; its lazy skills remain available, while lifecycle integrations stay
88
88
  dormant outside a checkout containing `.stdd/`.
89
89
  **Shared repository contract** use runs `init` once and commits `.stdd/`, native
90
90
  agent routing, and repository policy. **Enforced contract** use explicitly adds
91
- repository-owned hooks or a CI adapter; ordinary `init` never creates CI, and
92
- CI reads checkout and review-request facts rather than the private ledger or
93
- agent state.
91
+ repository-owned hooks, or the two CI commands below to a job the repository
92
+ owns; `init` never creates CI, and CI reads checkout and review-request facts
93
+ rather than the private ledger or agent state.
94
94
 
95
95
  Repo-local generated skills remain a valid team contract and need no plugin.
96
96
  The optional universal bundle at `plugins/stdd/` distributes one generated set
@@ -159,46 +159,39 @@ The five skills named by that router (`stdd-investigation`,
159
159
  that would make one inactive. Other inactive local overrides still shadow
160
160
  their kit playbook intentionally.
161
161
 
162
- ## CI adapters
162
+ ## CI
163
163
 
164
- CI integration is an explicit, optional transport adapter around
165
- provider-neutral CLI contracts. `init` without `--ci` creates no provider file;
166
- a team may instead place the printed generic commands in an existing quality
167
- job. Every configured provider runs `stdd check`; a review pipeline pipes its
168
- live PR/MR description to `stdd check-pr - --base <ref>`. CI uses read-only
169
- repository and review-request access. It never attempts to prove the agent's
170
- reasoning, consume the ignored ledger, dispatch workers, or mutate Git: it
171
- grades only facts derivable from the checkout and review request.
164
+ CI is read-only enforcement of checkout and PR facts, composed into whatever
165
+ job the repository already runs. stdd writes no provider configuration: a
166
+ workflow is ordinary infrastructure the team owns, and generating one taught
167
+ nothing the two commands below do not already say. CI never attempts to prove
168
+ the agent's reasoning, consume the ignored ledger, dispatch workers, or mutate
169
+ Git it grades only facts derivable from the checkout and the review request.
172
170
 
173
- On GitHub, `stdd init --ci github` writes the canonical workflow for these
174
- gates and installs an explicit supported Node runtime. It fetches the PR body
175
- live from the API and re-runs on body edits —
176
- a workflow reading `github.event.pull_request.body` validates a payload
177
- frozen at trigger time, so an edited body is never re-checked and a re-run
178
- replays the stale text. The fetch uses node, not the gh CLI — node is
179
- already required to run stdd, while self-hosted runners often lack gh —
180
- and the step sets `pipefail`, so a failed fetch fails the gate as a fetch
181
- error instead of feeding check-pr an empty body that misreports as a
182
- missing evidence line. `stdd doctor` flags the frozen-payload form, and flags a PR
183
- template carrying an unquoted evidence label at the start of a line, since
184
- its placeholder residue would pass the gate on every PR.
171
+ The contract is two commands:
185
172
 
186
- On GitLab, `stdd init --ci gitlab` writes an includeable
187
- `.gitlab/stdd.gitlab-ci.yml` job. It uses the merge-request API to fetch the
188
- live description, pipes it to `check-pr -`, and passes
189
- `CI_MERGE_REQUEST_DIFF_BASE_SHA` as the base. The job enables `pipefail`, so
190
- an API failure fails the gate instead of being mistaken for an empty body.
191
- Same-project pipelines authenticate with the short-lived `CI_JOB_TOKEN`.
192
- Because fork merge-request pipelines normally run in the source project, the
193
- target must allowlist that source for job-token access. A controlled trusted
194
- fork may instead supply a masked and hidden target-project
195
- `STDD_GITLAB_READ_API_TOKEN` with only `read_api`; target credentials are
196
- never safe in an untrusted fork pipeline. Authentication failure names the
197
- required setup instead of pretending fork access is automatic.
198
- `stdd init --ci generic` writes no provider file; it prints and records the
199
- portable command contract for teams to compose into Jenkins, Buildkite, or an
200
- existing pipeline. Provider templates are adapters, never dependencies of
201
- the method or public SDK.
173
+ ```
174
+ stdd check .
175
+ <live review description> | stdd check-pr - --base <base ref>
176
+ ```
177
+
178
+ Three things a hand-written job has to get right, because they are the part
179
+ that is not obvious:
180
+
181
+ - **Fetch the description live from the provider API.** An event payload is
182
+ frozen at trigger time, so a body-only edit is never re-checked and a re-run
183
+ replays the stale text. On GitHub that means the workflow must not read
184
+ `github.event.pull_request.body`, and must re-run on the `edited` trigger;
185
+ `stdd doctor` reports a workflow that validates the frozen payload a local
186
+ diagnostic, not part of the `stdd check` gate the job itself runs.
187
+ - **Set `pipefail` on the fetch step.** Otherwise a failed fetch feeds
188
+ `check-pr` an empty body, which misreports as a missing evidence line
189
+ instead of as a fetch error.
190
+ - **Check out full history.** `check-pr --base` diffs against the base ref.
191
+
192
+ `stdd doctor` flags the frozen-payload form, and flags a PR template carrying
193
+ an unquoted evidence label at the start of a line, since its placeholder
194
+ residue would pass the gate on every PR.
202
195
 
203
196
  ## Local hooks
204
197
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stdd/cli",
3
- "version": "0.9.2",
3
+ "version": "0.10.0",
4
4
  "description": "Spec + Test Driven Development — a markdown-first methodology kit for teams building software with AI coding agents",
5
5
  "type": "module",
6
6
  "exports": {
@@ -19,11 +19,17 @@ compaction, its recorded events do.
19
19
  sandbox when the worker does not need Git authority:
20
20
 
21
21
  ```bash
22
- stdd worker create ../stdd-worker-billing \
22
+ mkdir -p ../.stdd-workers
23
+ stdd worker create ../.stdd-workers/billing \
23
24
  --frozen "docs/**,migrations/**" \
24
25
  --allowed "src/billing/**,test/billing/**"
25
26
  ```
26
27
 
28
+ A sandbox cannot live inside the checkout or inside any Git repository, so
29
+ it goes beside the project — in one hidden container, not as a visible
30
+ sibling per slice. A directory of projects collects one `.stdd-workers/`
31
+ however many slices you delegate, and deleting it removes every sandbox.
32
+
27
33
  Use `stdd slice new --frozen ... --allowed ...` only when the worker must
28
34
  operate in an existing isolated checkout. `--frozen` names globs the worker
29
35
  must not touch. `--allowed` names the only paths it may change. At least one
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: stdd-finish-change
3
- description: Close an implemented change with independent review, PR evidence, terminal CI, and runtime verification when required
3
+ description: Close an implemented change with the review, PR evidence, terminal CI, and runtime verification its delivery boundary requires
4
4
  when: Implementation is locally verified and the change is ready for review, delivery, or handoff.
5
5
  ---
6
6
 
@@ -9,8 +9,10 @@ when: Implementation is locally verified and the change is ready for review, del
9
9
  Close the current checkout in this order:
10
10
 
11
11
  1. Run the complete affected local verification.
12
- 2. Finish every plan item and run the independent closing review when the
13
- capability profile supports it.
12
+ 2. Finish every plan item. Run the independent closing review when the plan
13
+ carries a `[review:]` item or a slice was delegated, and the capability
14
+ profile supports it; a single slice makes no review claim and is not asked
15
+ for one.
14
16
  <!-- cap:crossCli -->
15
17
  `stdd review --via {{STDD_CROSS_CLI_REVIEW_VIA}}` dispatches the other CLI
16
18
  read-only and records the verdict in the ledger.
@@ -29,6 +31,11 @@ Close the current checkout in this order:
29
31
  6. Run `stdd task finish` only after the requested delivery boundary is
30
32
  actually complete.
31
33
 
34
+ Steps 3 and 4 apply when that delivery boundary is a PR. A change the user
35
+ asked for as a local edit is complete after step 2 and closes at step 6 —
36
+ opening a PR for it is work nobody requested. Step 5 is not a PR step: any
37
+ delivery carrying a runtime effect gets that verification, PR or not.
38
+
32
39
  <!-- cap:subagents|crossCli -->
33
40
  An `approved` verdict freezes the checkout. Anything you notice afterwards —
34
41
  a stale comment, a better name, one more edge case — is deferred with
@@ -1,11 +1,15 @@
1
1
  ---
2
2
  name: stdd-planning
3
3
  description: Turn an agreed behavior contract into an executable, verifiable sequence of work
4
- when: The behavior contract is agreed (docs edit drafted or committed) and the change is large enough to need ordered steps — before the first implementation edit, to fix the execution mode and delivery boundary.
4
+ when: The behavior contract is agreed (docs edit drafted or committed) and the change is more than one slice — a second independent outcome, an ordering dependency between parts, work to hand to another session, or an unresolved design decision — before the first implementation edit, to fix the execution mode and delivery boundary.
5
5
  ---
6
6
 
7
7
  # Planning
8
8
 
9
+ A single-slice change does not come here: it goes straight to
10
+ `stdd-implement`. Planning starts when a second slice appears, which is
11
+ usually mid-work rather than at classification time.
12
+
9
13
  A plan is a disposable working artifact: it guides one execution and is thrown
10
14
  away. It is never committed as a file — its home is the PR description (for
11
15
  the durable summary) and `.stdd/plan.md` (for the working copy: per checkout,
@@ -28,13 +28,23 @@ stdd status --local
28
28
  If another task is active, do not reset it silently. Finish it, continue it,
29
29
  or ask the user which task owns the checkout.
30
30
 
31
- Then classify the chosen action and route to the smallest applicable workflow:
32
-
33
- - behavior or scope is still uncertain invoke `stdd-brainstorming` within
31
+ Then route. The default is one slice: invoke `stdd-implement` directly. A
32
+ change is one slice when, at the moment of deciding, it has one agreed
33
+ observable outcome, one coherent implementation boundary, one acceptance check,
34
+ and no known dependency on another independently verifiable change.
35
+
36
+ Escalate from that default only on a named trigger:
37
+
38
+ - a second independent outcome or an ordering dependency between parts →
39
+ invoke `stdd-planning`;
40
+ - work to hand to another session → invoke `stdd-planning`, then
41
+ `stdd-delegate-slice`;
42
+ - a design decision nobody has made yet → invoke `stdd-brainstorming` within
34
43
  the active change boundary;
35
- - agreed multi-step behavior → invoke `stdd-planning`;
36
- - known defect without a diagnosis → invoke `stdd-debugging`;
37
- - small agreed change invoke `stdd-implement` directly.
44
+ - a known defect without a diagnosis → invoke `stdd-debugging`.
45
+
46
+ Any of them may appear mid-work. Escalating then is the normal case, not a
47
+ failed classification.
38
48
 
39
49
  Read `.stdd/method.md` and the canonical docs governing the touched behavior.
40
50
  The classification is a routing decision, not ceremony: skip workflows that do
@@ -3,9 +3,6 @@ import { assertPrintableSingleLine } from "./text.mjs";
3
3
 
4
4
  const SEMVER_PATTERN =
5
5
  /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/u;
6
- const CI_STAMP_PLACEHOLDER = "__STAMP__";
7
- const CI_VERSION_PLACEHOLDER = "__VERSION__";
8
- const CI_PLACEHOLDER_PATTERN = /__[A-Z][A-Z0-9_]*__/u;
9
6
  export const CROSS_CLI_REVIEW_VIA_TOKEN = "{{STDD_CROSS_CLI_REVIEW_VIA}}";
10
7
  const CROSS_CLI_REVIEW_VIAS = new Set(["claude", "codex"]);
11
8
 
@@ -92,23 +89,6 @@ export function defineAgentAdapter(adapter) {
92
89
  return deepFreeze(copy);
93
90
  }
94
91
 
95
- export function defineCiAdapter(adapter) {
96
- const id = assertSkillName(adapter?.id, "CI adapter id");
97
- const outputIsNull = adapter?.outputFile === null;
98
- const templateIsNull = adapter?.templateFile === null;
99
- if (outputIsNull !== templateIsNull) {
100
- throw new TypeError("CI adapter outputFile and templateFile must both be null or both be paths");
101
- }
102
- const copy = {
103
- id,
104
- outputFile: outputIsNull ? null : requireRepoPath(adapter?.outputFile, "CI adapter outputFile"),
105
- templateFile: templateIsNull
106
- ? null
107
- : requireRepoPath(adapter?.templateFile, "CI adapter templateFile"),
108
- };
109
- return deepFreeze(copy);
110
- }
111
-
112
92
  export const AGENT_ADAPTERS = deepFreeze({
113
93
  claude: defineAgentAdapter({
114
94
  id: "claude",
@@ -139,24 +119,6 @@ export const AGENT_ADAPTERS = deepFreeze({
139
119
  }),
140
120
  });
141
121
 
142
- export const CI_ADAPTERS = deepFreeze({
143
- github: defineCiAdapter({
144
- id: "github",
145
- outputFile: ".github/workflows/stdd.yml",
146
- templateFile: "github-stdd.yml",
147
- }),
148
- gitlab: defineCiAdapter({
149
- id: "gitlab",
150
- outputFile: ".gitlab/stdd.gitlab-ci.yml",
151
- templateFile: "gitlab-stdd.yml",
152
- }),
153
- generic: defineCiAdapter({
154
- id: "generic",
155
- outputFile: null,
156
- templateFile: null,
157
- }),
158
- });
159
-
160
122
  export function getAgentAdapter(id) {
161
123
  if (!Object.hasOwn(AGENT_ADAPTERS, id)) {
162
124
  throw new Error(`unknown agent adapter ${JSON.stringify(id)}`);
@@ -164,13 +126,6 @@ export function getAgentAdapter(id) {
164
126
  return AGENT_ADAPTERS[id];
165
127
  }
166
128
 
167
- export function getCiAdapter(id) {
168
- if (!Object.hasOwn(CI_ADAPTERS, id)) {
169
- throw new Error(`unknown CI adapter ${JSON.stringify(id)}`);
170
- }
171
- return CI_ADAPTERS[id];
172
- }
173
-
174
129
  function resolveAgentAdapter(adapter) {
175
130
  return typeof adapter === "string" ? getAgentAdapter(adapter) : defineAgentAdapter(adapter);
176
131
  }
@@ -274,19 +229,3 @@ export function renderAgentInstructions({
274
229
  "",
275
230
  ].join("\n");
276
231
  }
277
-
278
- export function renderCiTemplate(template, { stamp, version }) {
279
- const safeTemplate = requireString(template, "CI template");
280
- const safeStamp = assertPrintableSingleLine(stamp, "CI stamp");
281
- const safeVersion = assertSemanticVersion(version, "CI version");
282
- if (!safeTemplate.includes(CI_STAMP_PLACEHOLDER) || !safeTemplate.includes(CI_VERSION_PLACEHOLDER)) {
283
- throw new TypeError("CI template must contain __STAMP__ and __VERSION__ placeholders");
284
- }
285
- const rendered = safeTemplate.replace(/__STAMP__|__VERSION__/gu, (placeholder) =>
286
- placeholder === CI_STAMP_PLACEHOLDER ? safeStamp : safeVersion,
287
- );
288
- if (CI_PLACEHOLDER_PATTERN.test(rendered)) {
289
- throw new TypeError("CI template contains an unresolved placeholder");
290
- }
291
- return rendered;
292
- }
@@ -49,17 +49,9 @@ export interface AgentAdapter {
49
49
  readonly hooksFile: string;
50
50
  readonly crossCliReviewVia?: "codex" | "claude" | null;
51
51
  }
52
- export interface CiAdapter {
53
- readonly id: string;
54
- readonly outputFile: string | null;
55
- readonly templateFile: string | null;
56
- }
57
52
  export const AGENT_ADAPTERS: DeepReadonly<Record<"claude" | "codex" | "pi", AgentAdapter>>;
58
- export const CI_ADAPTERS: DeepReadonly<Record<"github" | "gitlab" | "generic", CiAdapter>>;
59
53
  export function defineAgentAdapter(adapter: AgentAdapter): DeepReadonly<AgentAdapter>;
60
- export function defineCiAdapter(adapter: CiAdapter): DeepReadonly<CiAdapter>;
61
54
  export function getAgentAdapter(id: string): AgentAdapter;
62
- export function getCiAdapter(id: string): CiAdapter;
63
55
  export function renderAgentSkill(input: {
64
56
  adapter?: string | AgentAdapter;
65
57
  name: string;
@@ -75,7 +67,6 @@ export function renderAgentInstructions(input: {
75
67
  crossCli: boolean;
76
68
  projectLogEnabled?: boolean;
77
69
  }): string;
78
- export function renderCiTemplate(template: string, input: { stamp: string; version: string }): string;
79
70
  export function assertSkillName(name: string, label?: string): string;
80
71
  export function isPrintableSingleLine(value: unknown): value is string;
81
72
  export function assertPrintableSingleLine(value: unknown, label?: string): string;
@@ -12,14 +12,10 @@ export {
12
12
  } from "../cli/lib.mjs";
13
13
  export {
14
14
  AGENT_ADAPTERS,
15
- CI_ADAPTERS,
16
15
  defineAgentAdapter,
17
- defineCiAdapter,
18
16
  getAgentAdapter,
19
- getCiAdapter,
20
17
  renderAgentInstructions,
21
18
  renderAgentSkill,
22
- renderCiTemplate,
23
19
  } from "./adapters.mjs";
24
20
  export { assertSkillName, resolveRepoPath, resolveWritableRepoPath } from "./path.mjs";
25
21
  export { assertPrintableSingleLine, isPrintableSingleLine } from "./text.mjs";
@@ -3,7 +3,7 @@ name: stdd-brainstorming
3
3
  description: "Explore future behavior and hypothetical approaches without forcing action. Use when: Asked for opinions, ideation, future behavior, or a hypothetical implementation approach, with no explicit intent to persist or modify the repository."
4
4
  ---
5
5
 
6
- <!-- generated by stdd plugin build v0.9.2 — do not edit -->
6
+ <!-- generated by stdd plugin build v0.10.0 — do not edit -->
7
7
 
8
8
 
9
9
  # Brainstorming
@@ -3,7 +3,7 @@ name: stdd-debugging
3
3
  description: "Find and fix the root cause of a defect, not its symptom. Use when: A bug, crash, failing test, or unexplained behavior is reported."
4
4
  ---
5
5
 
6
- <!-- generated by stdd plugin build v0.9.2 — do not edit -->
6
+ <!-- generated by stdd plugin build v0.10.0 — do not edit -->
7
7
 
8
8
 
9
9
  # Debugging
@@ -3,7 +3,7 @@ name: stdd-delegate-slice
3
3
  description: "Hand a slice of work to a worker session with a declared scope, a ledger handoff, and a reviewed result. Use when: Before implementing a multi-step change whose steps are independent — hand slices to worker sessions (subagent, second CLI, teammate) instead of implementing everything inline; also whenever a worker's result comes back for review."
4
4
  ---
5
5
 
6
- <!-- generated by stdd plugin build v0.9.2 — do not edit -->
6
+ <!-- generated by stdd plugin build v0.10.0 — do not edit -->
7
7
 
8
8
 
9
9
  # Delegate a Slice
@@ -21,11 +21,17 @@ compaction, its recorded events do.
21
21
  sandbox when the worker does not need Git authority:
22
22
 
23
23
  ```bash
24
- stdd worker create ../stdd-worker-billing \
24
+ mkdir -p ../.stdd-workers
25
+ stdd worker create ../.stdd-workers/billing \
25
26
  --frozen "docs/**,migrations/**" \
26
27
  --allowed "src/billing/**,test/billing/**"
27
28
  ```
28
29
 
30
+ A sandbox cannot live inside the checkout or inside any Git repository, so
31
+ it goes beside the project — in one hidden container, not as a visible
32
+ sibling per slice. A directory of projects collects one `.stdd-workers/`
33
+ however many slices you delegate, and deleting it removes every sandbox.
34
+
29
35
  Use `stdd slice new --frozen ... --allowed ...` only when the worker must
30
36
  operate in an existing isolated checkout. `--frozen` names globs the worker
31
37
  must not touch. `--allowed` names the only paths it may change. At least one
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  name: stdd-finish-change
3
- description: "Close an implemented change with independent review, PR evidence, terminal CI, and runtime verification when required. Use when: Implementation is locally verified and the change is ready for review, delivery, or handoff."
3
+ description: "Close an implemented change with the review, PR evidence, terminal CI, and runtime verification its delivery boundary requires. Use when: Implementation is locally verified and the change is ready for review, delivery, or handoff."
4
4
  ---
5
5
 
6
- <!-- generated by stdd plugin build v0.9.2 — do not edit -->
6
+ <!-- generated by stdd plugin build v0.10.0 — do not edit -->
7
7
 
8
8
 
9
9
  # Finish change
@@ -11,8 +11,10 @@ description: "Close an implemented change with independent review, PR evidence,
11
11
  Close the current checkout in this order:
12
12
 
13
13
  1. Run the complete affected local verification.
14
- 2. Finish every plan item and run the independent closing review when the
15
- capability profile supports it.
14
+ 2. Finish every plan item. Run the independent closing review when the plan
15
+ carries a `[review:]` item or a slice was delegated, and the capability
16
+ profile supports it; a single slice makes no review claim and is not asked
17
+ for one.
16
18
  `stdd review --via subagent` prints the brief path for a fresh read-only
17
19
  subagent; feed its JSON back with `stdd review --result <file>`.
18
20
  3. Generate the PR evidence with `stdd evidence`; never hand-author a claim
@@ -25,6 +27,11 @@ Close the current checkout in this order:
25
27
  6. Run `stdd task finish` only after the requested delivery boundary is
26
28
  actually complete.
27
29
 
30
+ Steps 3 and 4 apply when that delivery boundary is a PR. A change the user
31
+ asked for as a local edit is complete after step 2 and closes at step 6 —
32
+ opening a PR for it is work nobody requested. Step 5 is not a PR step: any
33
+ delivery carrying a runtime effect gets that verification, PR or not.
34
+
28
35
  An `approved` verdict freezes the checkout. Anything you notice afterwards —
29
36
  a stale comment, a better name, one more edge case — is deferred with
30
37
  `stdd defer`, not edited in. Editing discards the approval rather than
@@ -3,7 +3,7 @@ name: stdd-implement
3
3
  description: "Execute one agreed behavior slice through docs, genuine red, implementation, and fresh verification. Use when: The behavior contract is agreed and production changes are ready to begin."
4
4
  ---
5
5
 
6
- <!-- generated by stdd plugin build v0.9.2 — do not edit -->
6
+ <!-- generated by stdd plugin build v0.10.0 — do not edit -->
7
7
 
8
8
 
9
9
  # Implement
@@ -3,7 +3,7 @@ name: stdd-investigation
3
3
  description: "Read-only current-state diagnosis — evidence-backed findings, no changes. Use when: Asked a factual question about current behavior, or to diagnose or triage it, WITHOUT changing anything."
4
4
  ---
5
5
 
6
- <!-- generated by stdd plugin build v0.9.2 — do not edit -->
6
+ <!-- generated by stdd plugin build v0.10.0 — do not edit -->
7
7
 
8
8
 
9
9
  # Investigation
@@ -1,13 +1,17 @@
1
1
  ---
2
2
  name: stdd-planning
3
- description: "Turn an agreed behavior contract into an executable, verifiable sequence of work. Use when: The behavior contract is agreed (docs edit drafted or committed) and the change is large enough to need ordered steps — before the first implementation edit, to fix the execution mode and delivery boundary."
3
+ description: "Turn an agreed behavior contract into an executable, verifiable sequence of work. Use when: The behavior contract is agreed (docs edit drafted or committed) and the change is more than one slice — a second independent outcome, an ordering dependency between parts, work to hand to another session, or an unresolved design decision — before the first implementation edit, to fix the execution mode and delivery boundary."
4
4
  ---
5
5
 
6
- <!-- generated by stdd plugin build v0.9.2 — do not edit -->
6
+ <!-- generated by stdd plugin build v0.10.0 — do not edit -->
7
7
 
8
8
 
9
9
  # Planning
10
10
 
11
+ A single-slice change does not come here: it goes straight to
12
+ `stdd-implement`. Planning starts when a second slice appears, which is
13
+ usually mid-work rather than at classification time.
14
+
11
15
  A plan is a disposable working artifact: it guides one execution and is thrown
12
16
  away. It is never committed as a file — its home is the PR description (for
13
17
  the durable summary) and `.stdd/plan.md` (for the working copy: per checkout,
@@ -3,7 +3,7 @@ name: stdd-pr-green
3
3
  description: "A PR is done only when its required checks settle terminal-green on the current head. Use when: A PR/MR exists, or is about to be opened, for the current branch."
4
4
  ---
5
5
 
6
- <!-- generated by stdd plugin build v0.9.2 — do not edit -->
6
+ <!-- generated by stdd plugin build v0.10.0 — do not edit -->
7
7
 
8
8
 
9
9
  # PR Green
@@ -3,7 +3,7 @@ name: stdd-start-change
3
3
  description: "Open durable task state and route work after explicit intent to persist or modify the repository. Use when: The user explicitly wants a persisted work artifact or repository change."
4
4
  ---
5
5
 
6
- <!-- generated by stdd plugin build v0.9.2 — do not edit -->
6
+ <!-- generated by stdd plugin build v0.10.0 — do not edit -->
7
7
 
8
8
 
9
9
  # Start change
@@ -30,13 +30,23 @@ stdd status --local
30
30
  If another task is active, do not reset it silently. Finish it, continue it,
31
31
  or ask the user which task owns the checkout.
32
32
 
33
- Then classify the chosen action and route to the smallest applicable workflow:
33
+ Then route. The default is one slice: invoke `stdd-implement` directly. A
34
+ change is one slice when, at the moment of deciding, it has one agreed
35
+ observable outcome, one coherent implementation boundary, one acceptance check,
36
+ and no known dependency on another independently verifiable change.
34
37
 
35
- - behavior or scope is still uncertain invoke `stdd-brainstorming` within
38
+ Escalate from that default only on a named trigger:
39
+
40
+ - a second independent outcome or an ordering dependency between parts →
41
+ invoke `stdd-planning`;
42
+ - work to hand to another session → invoke `stdd-planning`, then
43
+ `stdd-delegate-slice`;
44
+ - a design decision nobody has made yet → invoke `stdd-brainstorming` within
36
45
  the active change boundary;
37
- - agreed multi-step behavior → invoke `stdd-planning`;
38
- - known defect without a diagnosis → invoke `stdd-debugging`;
39
- - small agreed change invoke `stdd-implement` directly.
46
+ - a known defect without a diagnosis → invoke `stdd-debugging`.
47
+
48
+ Any of them may appear mid-work. Escalating then is the normal case, not a
49
+ failed classification.
40
50
 
41
51
  Read `.stdd/method.md` and the canonical docs governing the touched behavior.
42
52
  The classification is a routing decision, not ceremony: skip workflows that do
@@ -3,7 +3,7 @@ name: stdd-worktrees
3
3
  description: "Work in an isolated workspace without fighting the platform's native isolation. Use when: Starting implementation work that should not disturb the user's current checkout."
4
4
  ---
5
5
 
6
- <!-- generated by stdd plugin build v0.9.2 — do not edit -->
6
+ <!-- generated by stdd plugin build v0.10.0 — do not edit -->
7
7
 
8
8
 
9
9
  # Isolated Workspaces
@@ -1,42 +0,0 @@
1
- # __STAMP__
2
- #
3
- # Validates the PR body fetched LIVE from the API, not the event payload —
4
- # the payload is frozen at trigger time, so a body-only edit would never be
5
- # re-checked. The `edited` trigger re-runs this workflow on body changes.
6
- name: STDD
7
- on:
8
- pull_request:
9
- types: [opened, edited, synchronize, reopened]
10
-
11
- permissions:
12
- contents: read
13
- pull-requests: read
14
-
15
- jobs:
16
- stdd:
17
- name: STDD Contract
18
- runs-on: ubuntu-latest
19
- steps:
20
- - uses: actions/checkout@v4
21
- with:
22
- fetch-depth: 0 # check-pr --base diffs against the base ref
23
- - uses: actions/setup-node@v4
24
- with:
25
- node-version: 22
26
- - name: stdd check
27
- run: npx --yes @stdd/cli@__VERSION__ check .
28
- - name: PR docs evidence (live body)
29
- env:
30
- GH_TOKEN: ${{ github.token }}
31
- PR_NUMBER: ${{ github.event.pull_request.number }}
32
- # node, not gh: node is already required for stdd, while self-hosted
33
- # runners often lack the gh CLI. pipefail: a failed fetch must fail
34
- # the gate as a fetch error, not feed check-pr an empty body.
35
- run: |
36
- set -o pipefail
37
- node --input-type=module -e '
38
- const url = "https://api.github.com/repos/" + process.env.GITHUB_REPOSITORY + "/pulls/" + process.env.PR_NUMBER;
39
- const res = await fetch(url, { headers: { authorization: "Bearer " + process.env.GH_TOKEN, accept: "application/vnd.github+json" } });
40
- if (!res.ok) { console.error("GitHub API " + res.status + " for " + url); process.exit(1); }
41
- process.stdout.write((await res.json()).body ?? "");
42
- ' | npx --yes @stdd/cli@__VERSION__ check-pr - --base "origin/$GITHUB_BASE_REF"
@@ -1,72 +0,0 @@
1
- # __STAMP__
2
- #
3
- # Include this file from the repository's root .gitlab-ci.yml:
4
- # include:
5
- # - local: .gitlab/stdd.gitlab-ci.yml
6
- #
7
- # The job defaults to GitLab's reserved .pre stage, so this include stays valid
8
- # when a consumer defines custom stages without "test". To run STDD in an
9
- # existing consumer stage, override the included job in the root .gitlab-ci.yml.
10
- # The override must name a stage declared by that pipeline:
11
- #
12
- # stdd:
13
- # stage: verify
14
- #
15
- # Same-project merge requests use CI_JOB_TOKEN. A fork pipeline runs in its
16
- # source project, so the target must allowlist that project under CI/CD job
17
- # token permissions. Only for a controlled, trusted source project, an
18
- # optional masked and hidden STDD_GITLAB_READ_API_TOKEN may carry a
19
- # target-project access token with read_api. Never expose a target token to
20
- # untrusted fork code.
21
- stdd:
22
- stage: .pre
23
- image: node:22
24
- variables:
25
- GIT_DEPTH: "0"
26
- rules:
27
- - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
28
- script:
29
- - npx --yes @stdd/cli@__VERSION__ check .
30
- - |
31
- set -o pipefail
32
- node --input-type=module -e '
33
- const targetProjectId = process.env.CI_MERGE_REQUEST_PROJECT_ID;
34
- const pipelineProjectId = process.env.CI_PROJECT_ID;
35
- const crossProject = Boolean(
36
- pipelineProjectId && targetProjectId && pipelineProjectId !== targetProjectId
37
- );
38
- const readApiToken = crossProject
39
- ? process.env.STDD_GITLAB_READ_API_TOKEN
40
- : "";
41
- const headers = readApiToken
42
- ? { "PRIVATE-TOKEN": readApiToken, accept: "application/json" }
43
- : { "JOB-TOKEN": process.env.CI_JOB_TOKEN, accept: "application/json" };
44
- const url = process.env.CI_API_V4_URL + "/projects/" +
45
- encodeURIComponent(targetProjectId) + "/merge_requests/" +
46
- process.env.CI_MERGE_REQUEST_IID;
47
- const res = await fetch(url, { headers });
48
- if (!res.ok) {
49
- console.error("GitLab API " + res.status + " for " + url);
50
- if (crossProject && !readApiToken) {
51
- console.error(
52
- "Fork or cross-project MR pipeline project " + pipelineProjectId +
53
- " cannot read target project " + targetProjectId +
54
- ". Add pipeline project " + pipelineProjectId +
55
- " to the target CI/CD job token allowlist, or only for a trusted source project " +
56
- "define masked and hidden STDD_GITLAB_READ_API_TOKEN with target-project read_api."
57
- );
58
- } else if (crossProject) {
59
- console.error(
60
- "STDD_GITLAB_READ_API_TOKEN cannot read target project " + targetProjectId +
61
- "; verify that it is a target-project token with read_api."
62
- );
63
- } else {
64
- console.error(
65
- "The same-project CI_JOB_TOKEN cannot read this merge request; " +
66
- "verify the triggering user and job-token permissions."
67
- );
68
- }
69
- process.exit(1);
70
- }
71
- process.stdout.write((await res.json()).description ?? "");
72
- ' | npx --yes @stdd/cli@__VERSION__ check-pr - --base "$CI_MERGE_REQUEST_DIFF_BASE_SHA"