@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.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/runtime/adapters/README.md +10 -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 +12 -2
- package/runtime/cli/stdd.mjs +2 -21
- package/runtime/method/README.md +49 -8
- package/runtime/method/reference-commands.md +7 -1
- package/runtime/method/reference-integration.md +33 -40
- package/runtime/package.json +1 -1
- package/runtime/playbooks/delegate-slice.md +7 -1
- package/runtime/playbooks/finish-change.md +10 -3
- package/runtime/playbooks/planning.md +5 -1
- package/runtime/playbooks/start-change.md +16 -6
- package/runtime/sdk/adapters.mjs +0 -61
- package/runtime/sdk/index.d.ts +0 -9
- package/runtime/sdk/index.mjs +0 -4
- package/skills/stdd-brainstorming/SKILL.md +1 -1
- 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 +1 -1
- package/skills/stdd-planning/SKILL.md +6 -2
- package/skills/stdd-pr-green/SKILL.md +1 -1
- package/skills/stdd-start-change/SKILL.md +16 -6
- 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
|
@@ -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
|
-
##
|
|
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.
|
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
|
@@ -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
|
|
@@ -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
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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,
|
|
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
|
|
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
|
|
92
|
-
CI reads checkout and review-request facts
|
|
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
|
|
162
|
+
## CI
|
|
163
163
|
|
|
164
|
-
CI
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
live description
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
`stdd
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
|
package/runtime/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
13
|
-
|
|
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
|
|
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
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
-
|
|
36
|
-
|
|
37
|
-
-
|
|
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
|
package/runtime/sdk/adapters.mjs
CHANGED
|
@@ -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
|
-
}
|
package/runtime/sdk/index.d.ts
CHANGED
|
@@ -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;
|
package/runtime/sdk/index.mjs
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
15
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
-
|
|
38
|
-
|
|
39
|
-
-
|
|
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.
|
|
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"
|