@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.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/runtime/adapters/README.md +23 -12
- package/runtime/cli/check.mjs +1 -1
- package/runtime/cli/generated-files.mjs +37 -13
- package/runtime/cli/init.mjs +5 -59
- package/runtime/cli/status.mjs +14 -4
- package/runtime/cli/stdd.mjs +2 -21
- package/runtime/method/README.md +63 -13
- package/runtime/method/reference-commands.md +7 -1
- package/runtime/method/reference-integration.md +55 -50
- package/runtime/package.json +1 -1
- package/runtime/playbooks/brainstorming.md +56 -37
- package/runtime/playbooks/delegate-slice.md +7 -1
- package/runtime/playbooks/finish-change.md +10 -3
- package/runtime/playbooks/investigation.md +11 -5
- package/runtime/playbooks/planning.md +5 -1
- package/runtime/playbooks/start-change.md +30 -16
- package/runtime/sdk/adapters.mjs +18 -66
- package/runtime/sdk/index.d.ts +0 -9
- package/runtime/sdk/index.mjs +0 -4
- package/skills/stdd-brainstorming/SKILL.md +56 -37
- package/skills/stdd-debugging/SKILL.md +1 -1
- package/skills/stdd-delegate-slice/SKILL.md +8 -2
- package/skills/stdd-finish-change/SKILL.md +11 -4
- package/skills/stdd-implement/SKILL.md +1 -1
- package/skills/stdd-investigation/SKILL.md +11 -5
- package/skills/stdd-planning/SKILL.md +6 -2
- package/skills/stdd-pr-green/SKILL.md +1 -1
- package/skills/stdd-start-change/SKILL.md +30 -16
- package/skills/stdd-worktrees/SKILL.md +1 -1
- package/runtime/templates/github-stdd.yml +0 -42
- package/runtime/templates/gitlab-stdd.yml +0 -72
package/package.json
CHANGED
|
@@ -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
|
-
##
|
|
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.
|
package/runtime/cli/check.mjs
CHANGED
|
@@ -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;
|
|
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", "
|
|
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(
|
|
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
|
-
|
|
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
|
-
...
|
|
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
|
package/runtime/cli/init.mjs
CHANGED
|
@@ -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)
|
|
305
|
-
//
|
|
306
|
-
//
|
|
307
|
-
//
|
|
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,
|
|
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,
|
package/runtime/cli/status.mjs
CHANGED
|
@@ -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:
|
|
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:
|
|
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
|
-
|
|
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 =
|
|
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"
|
package/runtime/cli/stdd.mjs
CHANGED
|
@@ -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] [--
|
|
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>] " +
|
package/runtime/method/README.md
CHANGED
|
@@ -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
|
-
|
|
224
|
-
to infer a workflow from a flat list
|
|
225
|
-
|
|
226
|
-
|
|
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
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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,
|
|
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
|
|
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
|