@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,190 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { dedupeChecks } from "./lib.mjs";
|
|
3
|
+
import { fail, git } from "./runtime.mjs";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The branch's PR as `stdd status` reports it. Degrades, never errors:
|
|
7
|
+
* no gh on PATH or an unexpected gh failure → state "unknown".
|
|
8
|
+
*/
|
|
9
|
+
export function statusPr(cwd) {
|
|
10
|
+
let raw;
|
|
11
|
+
try {
|
|
12
|
+
raw = execFileSync("gh", ["pr", "view", "--json", "number,url,statusCheckRollup"], {
|
|
13
|
+
cwd,
|
|
14
|
+
encoding: "utf8",
|
|
15
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
16
|
+
});
|
|
17
|
+
} catch (err) {
|
|
18
|
+
if (err.code === "ENOENT") return { state: "unknown", reason: "gh unavailable" };
|
|
19
|
+
const stderr = err.stderr?.toString() ?? "";
|
|
20
|
+
if (/no pull requests found/i.test(stderr)) return { state: "none" };
|
|
21
|
+
return {
|
|
22
|
+
state: "unknown",
|
|
23
|
+
reason: stderr.trim().split("\n")[0] || "gh failed",
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
let info;
|
|
27
|
+
try {
|
|
28
|
+
info = JSON.parse(raw);
|
|
29
|
+
} catch {
|
|
30
|
+
return { state: "unknown", reason: "gh returned unparseable output" };
|
|
31
|
+
}
|
|
32
|
+
const checks = { success: 0, failure: 0, pending: 0 };
|
|
33
|
+
for (const check of info.statusCheckRollup ?? []) {
|
|
34
|
+
const conclusion = (check.conclusion ?? "").toUpperCase();
|
|
35
|
+
if (conclusion === "SUCCESS" || conclusion === "NEUTRAL" || conclusion === "SKIPPED") {
|
|
36
|
+
checks.success++;
|
|
37
|
+
} else if (conclusion === "") {
|
|
38
|
+
checks.pending++;
|
|
39
|
+
} else {
|
|
40
|
+
checks.failure++;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return { state: "open", number: info.number, url: info.url, checks };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** One rollup entry → { name, terminal, ok }, for check runs and statuses. */
|
|
47
|
+
function normalizeCheck(entry) {
|
|
48
|
+
const startedAt = entry.startedAt ?? entry.createdAt ?? "";
|
|
49
|
+
if (entry.state !== undefined && entry.status === undefined) {
|
|
50
|
+
const state = (entry.state ?? "").toUpperCase();
|
|
51
|
+
return {
|
|
52
|
+
name: entry.context ?? "unknown",
|
|
53
|
+
terminal: state !== "" && state !== "EXPECTED" && state !== "PENDING",
|
|
54
|
+
ok: state === "SUCCESS",
|
|
55
|
+
startedAt,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const conclusion = (entry.conclusion ?? "").toUpperCase();
|
|
59
|
+
return {
|
|
60
|
+
name: entry.name ?? "unknown",
|
|
61
|
+
terminal: (entry.status ?? "").toUpperCase() === "COMPLETED",
|
|
62
|
+
ok: conclusion === "SUCCESS" || conclusion === "NEUTRAL" || conclusion === "SKIPPED",
|
|
63
|
+
startedAt,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The PR's checks for its current head, straight from the forge. */
|
|
68
|
+
function ghPrChecks(pr) {
|
|
69
|
+
const args = ["pr", "view", ...(pr ? [pr] : []), "--json", "number,url,headRefOid,statusCheckRollup"];
|
|
70
|
+
let raw;
|
|
71
|
+
try {
|
|
72
|
+
raw = execFileSync("gh", args, {
|
|
73
|
+
encoding: "utf8",
|
|
74
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
75
|
+
});
|
|
76
|
+
} catch (err) {
|
|
77
|
+
if (err.code === "ENOENT") fail("stdd ci needs the GitHub CLI (gh) on PATH");
|
|
78
|
+
const stderr = err.stderr?.toString() ?? "";
|
|
79
|
+
if (/no pull requests found/i.test(stderr)) {
|
|
80
|
+
fail("no PR for the current branch — open it first, then watch it settle");
|
|
81
|
+
}
|
|
82
|
+
fail(`gh pr view failed: ${stderr.trim().split("\n")[0] || err.message}`);
|
|
83
|
+
}
|
|
84
|
+
let info;
|
|
85
|
+
try {
|
|
86
|
+
info = JSON.parse(raw);
|
|
87
|
+
} catch {
|
|
88
|
+
fail("gh returned unparseable output");
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
number: info.number,
|
|
92
|
+
url: info.url,
|
|
93
|
+
head: info.headRefOid,
|
|
94
|
+
checks: dedupeChecks((info.statusCheckRollup ?? []).map(normalizeCheck)),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* `stdd ci [pr] [--watch]` — the PR's checks on its **current head**. The
|
|
100
|
+
* watch settles only when the check set is stable (two consecutive polls
|
|
101
|
+
* with the same names) and fully terminal: a watcher attached right after
|
|
102
|
+
* a push sees a partial set — the classic early-settle trap. A terminal
|
|
103
|
+
* failure exits immediately; a head move (amend, force-push) restarts the
|
|
104
|
+
* watch on the new head.
|
|
105
|
+
*/
|
|
106
|
+
export async function ciCommand(pr, watch, intervalSec, timeoutSec) {
|
|
107
|
+
const render = (c) => ` ${c.terminal ? (c.ok ? "✓" : "✗") : "…"} ${c.name}`;
|
|
108
|
+
let localHead = null;
|
|
109
|
+
try {
|
|
110
|
+
localHead = git("rev-parse", "HEAD");
|
|
111
|
+
} catch {
|
|
112
|
+
// outside a git repo gh still resolves an explicit PR number
|
|
113
|
+
}
|
|
114
|
+
let lastHead = null;
|
|
115
|
+
let lastNames = null;
|
|
116
|
+
let lastProgress = null;
|
|
117
|
+
let stable = 0;
|
|
118
|
+
const deadline = Date.now() + timeoutSec * 1000;
|
|
119
|
+
for (;;) {
|
|
120
|
+
const view = ghPrChecks(pr);
|
|
121
|
+
if (view.head !== lastHead) {
|
|
122
|
+
if (lastHead === null) {
|
|
123
|
+
console.log(`PR #${view.number} — checks on head ${view.head.slice(0, 7)} (${view.url})`);
|
|
124
|
+
if (localHead && localHead !== view.head) {
|
|
125
|
+
console.error(
|
|
126
|
+
`stdd ci: local HEAD ${localHead.slice(0, 7)} differs from the PR head — ` +
|
|
127
|
+
"watching the PR head; push if you meant your local commit",
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
} else {
|
|
131
|
+
console.log(`head moved to ${view.head.slice(0, 7)} — restarting the watch`);
|
|
132
|
+
}
|
|
133
|
+
lastHead = view.head;
|
|
134
|
+
lastNames = null;
|
|
135
|
+
stable = 0;
|
|
136
|
+
}
|
|
137
|
+
const failed = view.checks.filter((c) => c.terminal && !c.ok);
|
|
138
|
+
const allTerminal = view.checks.every((c) => c.terminal);
|
|
139
|
+
const names = view.checks
|
|
140
|
+
.map((c) => c.name)
|
|
141
|
+
.sort()
|
|
142
|
+
.join("\n");
|
|
143
|
+
|
|
144
|
+
if (!watch) {
|
|
145
|
+
for (const c of view.checks) console.log(render(c));
|
|
146
|
+
if (view.checks.length === 0) console.log(" (no checks reported)");
|
|
147
|
+
if (failed.length > 0) {
|
|
148
|
+
console.error(`stdd ci: ${failed.length} failing on ${view.head.slice(0, 7)}`);
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
if (allTerminal && view.checks.length > 0) {
|
|
152
|
+
console.log(`stdd ci: green (${view.checks.length} checks) on ${view.head.slice(0, 7)}`);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
console.error(
|
|
156
|
+
"stdd ci: not settled — checks pending or not yet registered; `stdd ci --watch` waits it out",
|
|
157
|
+
);
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (failed.length > 0) {
|
|
162
|
+
for (const c of view.checks) console.log(render(c));
|
|
163
|
+
console.error(`stdd ci: ${failed.length} failing check(s) on ${view.head.slice(0, 7)}`);
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
stable = allTerminal && view.checks.length > 0 && names === lastNames ? stable + 1 : 0;
|
|
167
|
+
lastNames = names;
|
|
168
|
+
if (stable >= 1 && allTerminal) {
|
|
169
|
+
// second consecutive all-terminal poll with an unchanged set
|
|
170
|
+
for (const c of view.checks) console.log(render(c));
|
|
171
|
+
console.log(
|
|
172
|
+
`stdd ci: green (${view.checks.length} checks) on ${view.head.slice(0, 7)} — terminal`,
|
|
173
|
+
);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const progress = ` ${view.checks.filter((c) => c.terminal).length}/${view.checks.length} terminal`;
|
|
177
|
+
if (progress !== lastProgress) {
|
|
178
|
+
console.log(progress);
|
|
179
|
+
lastProgress = progress;
|
|
180
|
+
}
|
|
181
|
+
if (Date.now() > deadline) {
|
|
182
|
+
const open = view.checks.filter((c) => !c.terminal).map((c) => c.name);
|
|
183
|
+
fail(
|
|
184
|
+
`timed out after ${timeoutSec}s — still not terminal: ` +
|
|
185
|
+
(open.join(", ") || "check set still settling"),
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
await new Promise((resolve) => setTimeout(resolve, intervalSec * 1000));
|
|
189
|
+
}
|
|
190
|
+
}
|