@stdd/plugin 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +9 -0
- package/.codex-plugin/plugin.json +21 -0
- package/LICENSE +21 -0
- package/README.md +47 -0
- package/extensions/stdd.mjs +77 -0
- package/hooks/claude-hooks.json +28 -0
- package/hooks/codex-hooks.json +28 -0
- package/package.json +38 -0
- package/runtime/adapters/README.md +158 -0
- package/runtime/cli/check.mjs +555 -0
- package/runtime/cli/ci.mjs +190 -0
- package/runtime/cli/claude-hooks.mjs +689 -0
- package/runtime/cli/config.mjs +27 -0
- package/runtime/cli/evidence.mjs +249 -0
- package/runtime/cli/generated-files.mjs +1693 -0
- package/runtime/cli/held-fs.mjs +415 -0
- package/runtime/cli/init.mjs +883 -0
- package/runtime/cli/ledger.mjs +1470 -0
- package/runtime/cli/lib.mjs +909 -0
- package/runtime/cli/path-bytes.mjs +83 -0
- package/runtime/cli/policy.mjs +112 -0
- package/runtime/cli/recorders.mjs +188 -0
- package/runtime/cli/review-fs.mjs +825 -0
- package/runtime/cli/review.mjs +1065 -0
- package/runtime/cli/runtime.mjs +32 -0
- package/runtime/cli/scope.mjs +185 -0
- package/runtime/cli/snapshot.mjs +897 -0
- package/runtime/cli/state-validation.mjs +168 -0
- package/runtime/cli/status.mjs +580 -0
- package/runtime/cli/stdd.mjs +536 -0
- package/runtime/cli/worker-fs.mjs +971 -0
- package/runtime/cli/worker-metadata.mjs +139 -0
- package/runtime/cli/worker.mjs +779 -0
- package/runtime/method/README.md +634 -0
- package/runtime/method/reference-commands.md +147 -0
- package/runtime/method/reference-generated-state.md +151 -0
- package/runtime/method/reference-integration.md +233 -0
- package/runtime/package.json +65 -0
- package/runtime/playbooks/brainstorming.md +46 -0
- package/runtime/playbooks/debugging.md +36 -0
- package/runtime/playbooks/delegate-slice.md +129 -0
- package/runtime/playbooks/finish-change.md +46 -0
- package/runtime/playbooks/implement.md +26 -0
- package/runtime/playbooks/investigation.md +33 -0
- package/runtime/playbooks/managed-playbooks.json +14 -0
- package/runtime/playbooks/planning.md +177 -0
- package/runtime/playbooks/pr-green.md +50 -0
- package/runtime/playbooks/start-change.md +37 -0
- package/runtime/playbooks/worktrees.md +45 -0
- package/runtime/prebuilds/stdd-fs/darwin-arm64/stdd-fs +0 -0
- package/runtime/prebuilds/stdd-fs/darwin-x64/stdd-fs +0 -0
- package/runtime/prebuilds/stdd-fs/linux-arm64/stdd-fs +0 -0
- package/runtime/prebuilds/stdd-fs/linux-x64/stdd-fs +0 -0
- package/runtime/prebuilds/stdd-fs/manifest.json +47 -0
- package/runtime/prebuilds/stdd-fs/win32-arm64/stdd-fs.exe +0 -0
- package/runtime/prebuilds/stdd-fs/win32-x64/stdd-fs.exe +0 -0
- package/runtime/sdk/adapters.mjs +279 -0
- package/runtime/sdk/file-observation.mjs +12 -0
- package/runtime/sdk/index.d.ts +140 -0
- package/runtime/sdk/index.mjs +31 -0
- package/runtime/sdk/native-fs.mjs +1235 -0
- package/runtime/sdk/path.mjs +71 -0
- package/runtime/sdk/text.mjs +42 -0
- package/runtime/sdk/workflow.mjs +294 -0
- package/runtime/templates/deferred-design.md +47 -0
- package/runtime/templates/github-stdd.yml +42 -0
- package/runtime/templates/gitlab-stdd.yml +72 -0
- package/runtime/templates/pr-description.md +35 -0
- package/scripts/adopting-root.mjs +42 -0
- package/scripts/stdd-hook.mjs +72 -0
- package/skills/stdd-brainstorming/SKILL.md +48 -0
- package/skills/stdd-debugging/SKILL.md +38 -0
- package/skills/stdd-delegate-slice/SKILL.md +118 -0
- package/skills/stdd-finish-change/SKILL.md +40 -0
- package/skills/stdd-implement/SKILL.md +28 -0
- package/skills/stdd-investigation/SKILL.md +35 -0
- package/skills/stdd-planning/SKILL.md +165 -0
- package/skills/stdd-pr-green/SKILL.md +52 -0
- package/skills/stdd-start-change/SKILL.md +39 -0
- package/skills/stdd-worktrees/SKILL.md +46 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// The repository's stdd configuration. Lower state modules read it through
|
|
2
|
+
// this owner so they never import the entry module.
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import { resolveWritableRepoPath } from "../sdk/path.mjs";
|
|
5
|
+
import { DEFAULT_CONFIG, mergeConfig } from "./lib.mjs";
|
|
6
|
+
import { fail } from "./runtime.mjs";
|
|
7
|
+
|
|
8
|
+
export function loadConfig(targetDir) {
|
|
9
|
+
let configPath;
|
|
10
|
+
try {
|
|
11
|
+
configPath = resolveWritableRepoPath(targetDir, ".stdd/config.json", "config path");
|
|
12
|
+
} catch (err) {
|
|
13
|
+
fail(err.message);
|
|
14
|
+
}
|
|
15
|
+
if (!fs.existsSync(configPath)) return DEFAULT_CONFIG;
|
|
16
|
+
let parsed;
|
|
17
|
+
try {
|
|
18
|
+
parsed = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
19
|
+
} catch (err) {
|
|
20
|
+
fail(`.stdd/config.json is not valid JSON: ${err.message}`);
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
return mergeConfig(parsed);
|
|
24
|
+
} catch (err) {
|
|
25
|
+
fail(`.stdd/config.json: ${err.message}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import { resolveRepoPath } from "../sdk/path.mjs";
|
|
4
|
+
import { loadConfig } from "./config.mjs";
|
|
5
|
+
import {
|
|
6
|
+
currentBranch,
|
|
7
|
+
gitChangedPaths,
|
|
8
|
+
LABEL_TO_DECISION,
|
|
9
|
+
loadLedger,
|
|
10
|
+
resolveRepoDir,
|
|
11
|
+
} from "./ledger.mjs";
|
|
12
|
+
import {
|
|
13
|
+
extractDocPaths,
|
|
14
|
+
findEvidenceLines,
|
|
15
|
+
globToRegExp,
|
|
16
|
+
nearMissEvidenceLines,
|
|
17
|
+
sentinelSuggestion,
|
|
18
|
+
} from "./lib.mjs";
|
|
19
|
+
import { fail, git } from "./runtime.mjs";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* With --pr: fetch the live PR (body, base, head) from the forge, so the
|
|
23
|
+
* validation matches what CI will see — never a diverged local checkout.
|
|
24
|
+
* Returns `{ body, baseRef, headRef, number }`.
|
|
25
|
+
*/
|
|
26
|
+
function resolveLivePr(pr) {
|
|
27
|
+
const args = [
|
|
28
|
+
"pr",
|
|
29
|
+
"view",
|
|
30
|
+
...(pr === "." ? [] : [pr]),
|
|
31
|
+
"--json",
|
|
32
|
+
"body,baseRefName,headRefOid,number",
|
|
33
|
+
];
|
|
34
|
+
let info;
|
|
35
|
+
try {
|
|
36
|
+
info = JSON.parse(
|
|
37
|
+
execFileSync("gh", args, {
|
|
38
|
+
encoding: "utf8",
|
|
39
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
40
|
+
}),
|
|
41
|
+
);
|
|
42
|
+
} catch (err) {
|
|
43
|
+
if (err.code === "ENOENT") fail("--pr needs the GitHub CLI (gh) on PATH");
|
|
44
|
+
fail(`gh pr view failed: ${err.stderr?.toString().trim().split("\n")[0] || err.message}`);
|
|
45
|
+
}
|
|
46
|
+
const baseRef = `origin/${info.baseRefName}`;
|
|
47
|
+
try {
|
|
48
|
+
git("fetch", "-q", "origin", info.baseRefName);
|
|
49
|
+
} catch (err) {
|
|
50
|
+
fail(
|
|
51
|
+
`could not fetch origin/${info.baseRefName}: ${err.stderr?.toString().trim().split("\n")[0] || err.message}`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
let headRef = "HEAD";
|
|
55
|
+
if (git("rev-parse", "HEAD") !== info.headRefOid) {
|
|
56
|
+
try {
|
|
57
|
+
git("cat-file", "-e", `${info.headRefOid}^{commit}`);
|
|
58
|
+
} catch {
|
|
59
|
+
try {
|
|
60
|
+
git("fetch", "-q", "origin", `pull/${info.number}/head`);
|
|
61
|
+
git("cat-file", "-e", `${info.headRefOid}^{commit}`);
|
|
62
|
+
} catch {
|
|
63
|
+
fail(
|
|
64
|
+
`local HEAD differs from PR head ${info.headRefOid.slice(0, 7)} and it could not ` +
|
|
65
|
+
"be fetched — push or check out the PR branch",
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
headRef = info.headRefOid;
|
|
70
|
+
}
|
|
71
|
+
return { body: info.body, baseRef, headRef, number: info.number };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Draft the docs evidence line from the actual diff instead of recall.
|
|
76
|
+
* Canonical docs changed → the finished line on stdout (substitution-safe);
|
|
77
|
+
* none changed → the authored-sentinel templates on stderr, nonzero exit.
|
|
78
|
+
*/
|
|
79
|
+
export function evidence(targetDir, baseRefFlag) {
|
|
80
|
+
const config = loadConfig(targetDir);
|
|
81
|
+
const base = baseRefFlag ?? config.baseRef;
|
|
82
|
+
if (!base) {
|
|
83
|
+
fail('evidence needs a base ref — pass --base <ref> or set "baseRef" in .stdd/config.json');
|
|
84
|
+
}
|
|
85
|
+
let changed;
|
|
86
|
+
try {
|
|
87
|
+
changed = gitChangedPaths(targetDir, `${base}...HEAD`);
|
|
88
|
+
} catch (err) {
|
|
89
|
+
fail(`--base ${base}: git diff failed: ${err.stderr?.toString().trim() || err.message}`);
|
|
90
|
+
}
|
|
91
|
+
const canonical = config.canonicalDocs.map(globToRegExp);
|
|
92
|
+
const docs = changed.filter((file) => canonical.some((re) => re.test(file)));
|
|
93
|
+
// The ledger's recorded decision is read first; the diff is the
|
|
94
|
+
// cross-check, and on contradiction the diff wins and the conflict is
|
|
95
|
+
// reported. loadLedger needs a branch — outside a git repo there is no
|
|
96
|
+
// ledger to consult, so degrade to diff-only.
|
|
97
|
+
const evidenceBranch = currentBranch(targetDir);
|
|
98
|
+
const recorded = evidenceBranch
|
|
99
|
+
? (loadLedger(targetDir, evidenceBranch)
|
|
100
|
+
.filter((e) => e.event === "docs")
|
|
101
|
+
.at(-1) ?? null)
|
|
102
|
+
: null;
|
|
103
|
+
if (docs.length > 0) {
|
|
104
|
+
if (recorded && recorded.decision !== "updated-first") {
|
|
105
|
+
console.error(
|
|
106
|
+
`stdd evidence: the ledger records "${recorded.decision}" but the diff changes ` +
|
|
107
|
+
`canonical docs — the diff wins; the ledger claim is contradicted`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
console.log(`Docs updated first: ${docs.map(evidencePath).join(", ")}`);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (recorded?.decision === "checked") {
|
|
114
|
+
console.log(`Docs checked, no change needed: ${recorded.paths.join(", ")} — ${recorded.reason}`);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (recorded?.decision === "not-applicable") {
|
|
118
|
+
console.log(`Docs not applicable: ${recorded.reason}`);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
fail(
|
|
122
|
+
`no canonical docs changed against ${base}${
|
|
123
|
+
recorded ? ` — the ledger claims "updated-first" but the diff is contradicted` : ""
|
|
124
|
+
} — author the evidence line yourself:\n` +
|
|
125
|
+
" Docs checked, no change needed: <docs + reason>\n" +
|
|
126
|
+
" Docs not applicable: <why implementation-only>",
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function checkPr(prBodyFile, baseRef, pr) {
|
|
131
|
+
let body;
|
|
132
|
+
let headRef = "HEAD";
|
|
133
|
+
let okSuffix = "";
|
|
134
|
+
if (pr) {
|
|
135
|
+
if (prBodyFile) fail(`--pr replaces the <file|-> argument — drop "${prBodyFile}"`);
|
|
136
|
+
if (baseRef) fail("--pr derives the base from the PR — drop --base");
|
|
137
|
+
const live = resolveLivePr(pr);
|
|
138
|
+
body = live.body;
|
|
139
|
+
baseRef = live.baseRef;
|
|
140
|
+
headRef = live.headRef;
|
|
141
|
+
const shortHead = (headRef === "HEAD" ? git("rev-parse", "HEAD") : headRef).slice(0, 7);
|
|
142
|
+
okSuffix = ` (PR #${live.number} body, base ${baseRef}, head ${shortHead})`;
|
|
143
|
+
} else {
|
|
144
|
+
body =
|
|
145
|
+
prBodyFile === "-" || !prBodyFile
|
|
146
|
+
? fs.readFileSync(0, "utf8")
|
|
147
|
+
: fs.readFileSync(prBodyFile, "utf8");
|
|
148
|
+
}
|
|
149
|
+
const matches = findEvidenceLines(body);
|
|
150
|
+
if (matches.length === 0) {
|
|
151
|
+
let message =
|
|
152
|
+
"PR body has no docs evidence line. Add exactly one of:\n" +
|
|
153
|
+
" Docs updated first: <docs>\n" +
|
|
154
|
+
" Docs checked, no change needed: <docs + reason>\n" +
|
|
155
|
+
" Docs not applicable: <why implementation-only>";
|
|
156
|
+
for (const near of nearMissEvidenceLines(body)) {
|
|
157
|
+
message +=
|
|
158
|
+
`\n\nline ${near.line} is a near-miss:\n` +
|
|
159
|
+
` found: ${near.raw.trim()}\n` +
|
|
160
|
+
` fix: ${near.suggestion}\n` +
|
|
161
|
+
" (the line must start at column 0 with the exact label, no markdown formatting)";
|
|
162
|
+
}
|
|
163
|
+
fail(message);
|
|
164
|
+
}
|
|
165
|
+
if (matches.length > 1) {
|
|
166
|
+
fail(
|
|
167
|
+
`PR body has ${matches.length} docs evidence lines ` +
|
|
168
|
+
`(lines ${matches.map((m) => m.line).join(", ")}) — keep exactly one.`,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
if (matches[0].content === "") {
|
|
172
|
+
fail(`"${matches[0].label}:" names no evidence — list the docs or the reason after the colon.`);
|
|
173
|
+
}
|
|
174
|
+
const repoDir = resolveRepoDir(process.cwd());
|
|
175
|
+
if (baseRef) verifyEvidenceAgainstDiff(matches[0], baseRef, headRef, repoDir, Boolean(pr));
|
|
176
|
+
// Advisory only — a ledger disagreement never changes the pass condition.
|
|
177
|
+
const advisoryBranch = currentBranch(repoDir);
|
|
178
|
+
const recorded = advisoryBranch
|
|
179
|
+
? (loadLedger(repoDir, advisoryBranch)
|
|
180
|
+
.filter((e) => e.event === "docs")
|
|
181
|
+
.at(-1) ?? null)
|
|
182
|
+
: null;
|
|
183
|
+
if (recorded && LABEL_TO_DECISION[matches[0].label] !== recorded.decision) {
|
|
184
|
+
console.error(
|
|
185
|
+
`stdd check-pr: advisory — the ledger records the docs decision as ` +
|
|
186
|
+
`"${recorded.decision}" but the PR body says "${matches[0].label}"`,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
console.log(`stdd check-pr: OK${okSuffix}`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** With --base: the evidence claim must be backed by the actual git diff. */
|
|
193
|
+
function verifyEvidenceAgainstDiff(
|
|
194
|
+
{ label, content },
|
|
195
|
+
baseRef,
|
|
196
|
+
headRef = "HEAD",
|
|
197
|
+
repoDir = process.cwd(),
|
|
198
|
+
fromPr = false,
|
|
199
|
+
) {
|
|
200
|
+
const paths = extractDocPaths(content);
|
|
201
|
+
for (const docPath of paths) {
|
|
202
|
+
try {
|
|
203
|
+
resolveRepoPath(repoDir, docPath, `evidence path ${JSON.stringify(docPath)}`);
|
|
204
|
+
} catch (err) {
|
|
205
|
+
fail(err.message);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (label === "Docs updated first") {
|
|
209
|
+
if (paths.length === 0) {
|
|
210
|
+
const suggestion = sentinelSuggestion(content);
|
|
211
|
+
fail(
|
|
212
|
+
`"Docs updated first:" names no doc paths — list the changed docs.` +
|
|
213
|
+
(suggestion ? `\nDid you mean:\n ${suggestion}` : ""),
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
let changed;
|
|
217
|
+
try {
|
|
218
|
+
changed = gitChangedPaths(repoDir, `${baseRef}...${headRef}`);
|
|
219
|
+
} catch (err) {
|
|
220
|
+
fail(`--base ${baseRef}: git diff failed: ${err.stderr?.toString().trim() || err.message}`);
|
|
221
|
+
}
|
|
222
|
+
const missing = paths.filter((p) => !changed.includes(p));
|
|
223
|
+
if (missing.length > 0) {
|
|
224
|
+
fail(`claimed as updated but not changed against ${baseRef}: ${missing.join(", ")}`);
|
|
225
|
+
}
|
|
226
|
+
} else if (label === "Docs checked, no change needed") {
|
|
227
|
+
const absent = paths.filter((p) => {
|
|
228
|
+
if (!fromPr) return !fs.existsSync(resolveRepoPath(repoDir, p, "evidence path"));
|
|
229
|
+
try {
|
|
230
|
+
execFileSync("git", ["-C", repoDir, "cat-file", "-e", `${headRef}:${p}`], {
|
|
231
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
232
|
+
});
|
|
233
|
+
return false;
|
|
234
|
+
} catch {
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
if (absent.length > 0) {
|
|
239
|
+
fail(
|
|
240
|
+
`claimed as checked but does not exist in ${fromPr ? "the PR head" : "the tree"}: ${absent.join(", ")}`,
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Evidence paths with whitespace are backticked so extraction is reversible. */
|
|
247
|
+
function evidencePath(docPath) {
|
|
248
|
+
return /\s/.test(docPath) ? `\`${docPath}\`` : docPath;
|
|
249
|
+
}
|