@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,83 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { escapeNonPrintableSingleLine, isPrintableSingleLine } from "../sdk/text.mjs";
|
|
4
|
+
import { globToRegExp } from "./lib.mjs";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Split a Buffer of NUL-delimited records into per-record Buffers. Git's
|
|
8
|
+
* `-z` output is raw pathname bytes (a path is any byte sequence but NUL),
|
|
9
|
+
* so records must be sliced on the byte, never decoded first.
|
|
10
|
+
*/
|
|
11
|
+
export function splitNul(buf) {
|
|
12
|
+
const out = [];
|
|
13
|
+
let start = 0;
|
|
14
|
+
for (let i = 0; i < buf.length; i++) {
|
|
15
|
+
if (buf[i] === 0) {
|
|
16
|
+
out.push(buf.subarray(start, i));
|
|
17
|
+
start = i + 1;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
if (start < buf.length) out.push(buf.subarray(start));
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// A git path is arbitrary bytes. Across the review subsystem the byte-exact
|
|
25
|
+
// latin1 decode (a bijection: distinct paths never collapse, ASCII structure
|
|
26
|
+
// — the `/`, `.md`, directory names a glob keys on — is preserved) is the
|
|
27
|
+
// match/dedupe/snapshot key; the UTF-8 view, escaped by displayPath, is what
|
|
28
|
+
// a human reads. A glob is source text (Unicode), so it too is encoded to
|
|
29
|
+
// its byte form before compiling, or a non-ASCII glob literal (docs/über/**)
|
|
30
|
+
// would never match its latin1 pathname.
|
|
31
|
+
export const pathForMatch = (buf) => buf.toString("latin1");
|
|
32
|
+
export const pathForView = (latin1) => Buffer.from(latin1, "latin1").toString("utf8");
|
|
33
|
+
export const latinGlob = (glob) => globToRegExp(Buffer.from(glob, "utf8").toString("latin1"));
|
|
34
|
+
export const absPathBuf = (cwd, latin1) =>
|
|
35
|
+
Buffer.concat([Buffer.from(`${cwd}/`), Buffer.from(latin1, "latin1")]);
|
|
36
|
+
|
|
37
|
+
export function parentPathBuf(absolute) {
|
|
38
|
+
const separator = path.sep.charCodeAt(0);
|
|
39
|
+
const index = absolute.lastIndexOf(separator);
|
|
40
|
+
return index === 0 ? absolute.subarray(0, 1) : absolute.subarray(0, index);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function realPathBuf(value) {
|
|
44
|
+
return fs.realpathSync(value, { encoding: "buffer" });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function bufferPathIsWithin(root, candidate) {
|
|
48
|
+
if (candidate.equals(root)) return true;
|
|
49
|
+
const separator = Buffer.from(path.sep);
|
|
50
|
+
const prefix =
|
|
51
|
+
root.length === separator.length && root.equals(separator) ? root : Buffer.concat([root, separator]);
|
|
52
|
+
return candidate.length > prefix.length && candidate.subarray(0, prefix.length).equals(prefix);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Present a path as a quoted literal whenever it contains syntax or a scalar
|
|
56
|
+
// that the shared single-line boundary rejects. Every unsafe scalar is made
|
|
57
|
+
// visible, so a filename cannot split, repaint, hide, or reorder the brief.
|
|
58
|
+
export function displayPath(p) {
|
|
59
|
+
const escaped = escapeNonPrintableSingleLine(p);
|
|
60
|
+
if (escaped === p && isPrintableSingleLine(p) && !p.includes('"') && !p.includes("\\")) return p;
|
|
61
|
+
let quoted = '"';
|
|
62
|
+
for (const scalar of p) {
|
|
63
|
+
if (scalar === '"' || scalar === "\\") quoted += `\\${scalar}`;
|
|
64
|
+
else quoted += escapeNonPrintableSingleLine(scalar);
|
|
65
|
+
}
|
|
66
|
+
return `${quoted}"`;
|
|
67
|
+
}
|
|
68
|
+
// The human view of a latin1 (byte-exact) path. A valid-UTF-8 path renders
|
|
69
|
+
// as its text; a path that is not valid UTF-8 is shown byte-escaped and
|
|
70
|
+
// quoted (`"…\xff.md"`), so distinct invalid byte sequences stay
|
|
71
|
+
// distinguishable in the brief instead of both collapsing to U+FFFD.
|
|
72
|
+
export function viewPath(latin1) {
|
|
73
|
+
const buf = Buffer.from(latin1, "latin1");
|
|
74
|
+
const utf8 = pathForView(latin1);
|
|
75
|
+
if (Buffer.from(utf8, "utf8").equals(buf)) return displayPath(utf8);
|
|
76
|
+
let out = '"';
|
|
77
|
+
for (const b of buf) {
|
|
78
|
+
if (b === 0x22 || b === 0x5c) out += `\\${String.fromCharCode(b)}`;
|
|
79
|
+
else if (b >= 0x20 && b < 0x7f) out += String.fromCharCode(b);
|
|
80
|
+
else out += `\\x${b.toString(16).padStart(2, "0")}`;
|
|
81
|
+
}
|
|
82
|
+
return `${out}"`;
|
|
83
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// --- the project policy document: standing decisions, bounded grants ---
|
|
2
|
+
// Policy is durable repository state, not task state: it needs no active task
|
|
3
|
+
// and never touches the ledger. It is tracked, so it is published the way every
|
|
4
|
+
// other tracked file is — identity-bound and atomic, never written in place.
|
|
5
|
+
// Callers translate these errors into CLI diagnostics; the module itself stays
|
|
6
|
+
// testable in process.
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import { resolveWritableRepoPath } from "../sdk/path.mjs";
|
|
9
|
+
import {
|
|
10
|
+
openNativeRepoMutation,
|
|
11
|
+
publishNativeRepoFile,
|
|
12
|
+
readOptionalNativeRepoFile,
|
|
13
|
+
} from "./held-fs.mjs";
|
|
14
|
+
import {
|
|
15
|
+
appendPolicyNote,
|
|
16
|
+
appendPolicyPermission,
|
|
17
|
+
assertPolicyAction,
|
|
18
|
+
POLICY_ACTIONS,
|
|
19
|
+
parsePolicy,
|
|
20
|
+
} from "./lib.mjs";
|
|
21
|
+
import { readWorkerMetadata } from "./worker-metadata.mjs";
|
|
22
|
+
|
|
23
|
+
const POLICY_REL = ".stdd/policy.md";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Apply one append to the policy document. The document is read and republished
|
|
27
|
+
* inside a single native session: the destination is bound to the identity and
|
|
28
|
+
* bytes observed at read time, so a concurrent edit or a target swapped after
|
|
29
|
+
* resolution fails the publication instead of silently losing the other write.
|
|
30
|
+
*/
|
|
31
|
+
async function mutatePolicy(cwd, transform) {
|
|
32
|
+
// Proven state, not a bare marker: a malformed `.stdd/worker.json` in an
|
|
33
|
+
// owning checkout must surface as invalid metadata rather than as the false
|
|
34
|
+
// claim that this is a sandbox.
|
|
35
|
+
if (readWorkerMetadata(cwd)) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
"policy is owned by the source checkout — a managed worker cannot record standing decisions",
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
resolveWritableRepoPath(cwd, POLICY_REL, "policy path");
|
|
41
|
+
const context = await openNativeRepoMutation(cwd, "native filesystem helper for policy");
|
|
42
|
+
try {
|
|
43
|
+
const state = await readOptionalNativeRepoFile(context, POLICY_REL, { label: "policy path" });
|
|
44
|
+
const next = transform(state ? state.bytes.toString("utf8") : "");
|
|
45
|
+
await publishNativeRepoFile(context, POLICY_REL, next, {
|
|
46
|
+
mode: 0o644,
|
|
47
|
+
tempPrefix: ".policy-",
|
|
48
|
+
directoryMode: 0o755,
|
|
49
|
+
expectedTarget: state ? state.file.observation.identity : null,
|
|
50
|
+
...(state ? { expectedBytes: state.bytes } : {}),
|
|
51
|
+
});
|
|
52
|
+
} finally {
|
|
53
|
+
await context.close();
|
|
54
|
+
}
|
|
55
|
+
return POLICY_REL;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* `stdd policy show` — the enforcing read of the policy document.
|
|
60
|
+
*
|
|
61
|
+
* A session must consult policy through this command rather than reading the
|
|
62
|
+
* markdown itself: the closed action set, the printable-line rule and the
|
|
63
|
+
* section boundary live in `parsePolicy`, and a reader that skips it is back to
|
|
64
|
+
* trusting whatever the file happens to say.
|
|
65
|
+
*/
|
|
66
|
+
export function policyShow(cwd) {
|
|
67
|
+
const target = resolveWritableRepoPath(cwd, POLICY_REL, "policy path");
|
|
68
|
+
if (!fs.existsSync(target)) {
|
|
69
|
+
console.log("no project policy recorded — stdd policy add <text> starts one");
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
const policy = parsePolicy(fs.readFileSync(target, "utf8"));
|
|
73
|
+
if (policy.permissions.length === 0) console.log("permissions: none");
|
|
74
|
+
else {
|
|
75
|
+
console.log("permissions (verify the condition before acting):");
|
|
76
|
+
for (const { action, condition } of policy.permissions) {
|
|
77
|
+
console.log(` ${action} — when: ${condition}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (policy.notes.length > 0) {
|
|
81
|
+
console.log("notes (advisory — they grant nothing):");
|
|
82
|
+
for (const note of policy.notes) console.log(` ${note}`);
|
|
83
|
+
}
|
|
84
|
+
if (policy.rejected.length > 0) {
|
|
85
|
+
console.log(
|
|
86
|
+
`ignored — ${policy.rejected.length} entr${policy.rejected.length === 1 ? "y names" : "ies name"} an action this kit does not know:`,
|
|
87
|
+
);
|
|
88
|
+
for (const entry of policy.rejected) console.log(` ${entry}`);
|
|
89
|
+
console.log(` known actions: ${POLICY_ACTIONS.join(", ")}`);
|
|
90
|
+
}
|
|
91
|
+
return 0;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** `stdd policy add <text>` — record project nuance. Grants nothing. */
|
|
95
|
+
export async function policyAdd(cwd, text) {
|
|
96
|
+
return await mutatePolicy(cwd, (content) => appendPolicyNote(content, text));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* `stdd policy allow <action> --when <condition>` — pre-authorize one outward
|
|
101
|
+
* effect. The action must come from the closed set, and the condition is
|
|
102
|
+
* mandatory: a grant the session cannot verify is not a grant.
|
|
103
|
+
*/
|
|
104
|
+
export async function policyAllow(cwd, action, condition) {
|
|
105
|
+
assertPolicyAction(action);
|
|
106
|
+
if (typeof condition !== "string" || condition.trim() === "") {
|
|
107
|
+
throw new Error(
|
|
108
|
+
`every permission names what to verify: policy allow <action> --when "<verifiable condition>"`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
return await mutatePolicy(cwd, (content) => appendPolicyPermission(content, action, condition.trim()));
|
|
112
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// Checkout docs/run recorders; intentionally independent of the CLI entry module.
|
|
2
|
+
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { resolveRepoPath } from "../sdk/path.mjs";
|
|
5
|
+
import { assertPrintableSingleLine } from "../sdk/text.mjs";
|
|
6
|
+
import { loadConfig } from "./config.mjs";
|
|
7
|
+
import {
|
|
8
|
+
appendLedger,
|
|
9
|
+
DOCS_DECISIONS,
|
|
10
|
+
ledgerAppendContext,
|
|
11
|
+
withCapturedLedgerIdentity,
|
|
12
|
+
} from "./ledger.mjs";
|
|
13
|
+
import { redGenuine } from "./lib.mjs";
|
|
14
|
+
import { fail, MAX_SUBPROCESS_BUFFER } from "./runtime.mjs";
|
|
15
|
+
import { checkoutSnapshot } from "./snapshot.mjs";
|
|
16
|
+
|
|
17
|
+
const EXCERPT_LIMIT = 2000;
|
|
18
|
+
|
|
19
|
+
/** `stdd docs <decision> [paths…] [--reason <why>]` — record the docs decision. */
|
|
20
|
+
export function recordDocs(cwd, decision, paths, reason) {
|
|
21
|
+
if (!DOCS_DECISIONS.includes(decision)) {
|
|
22
|
+
// Free text is the recurring mistake — answer with the exact forms
|
|
23
|
+
// and, when a decision word is buried in the prose, the corrected call.
|
|
24
|
+
const joined = [decision ?? "", ...paths].join(" ");
|
|
25
|
+
const stem = /not[- ]applicable/i.test(joined)
|
|
26
|
+
? "not-applicable"
|
|
27
|
+
: /updated[- ]first/i.test(joined)
|
|
28
|
+
? "updated-first"
|
|
29
|
+
: /\bchecked\b/i.test(joined)
|
|
30
|
+
? "checked"
|
|
31
|
+
: null;
|
|
32
|
+
fail(
|
|
33
|
+
`unknown docs decision "${decision ?? ""}" — the decision is one word, then its arguments:\n` +
|
|
34
|
+
" stdd docs updated-first <paths…>\n" +
|
|
35
|
+
" stdd docs checked <paths…> --reason <why>\n" +
|
|
36
|
+
" stdd docs not-applicable --reason <why>" +
|
|
37
|
+
(stem ? `\ndid you mean: stdd docs ${stem} …` : ""),
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
if (reason !== null) {
|
|
41
|
+
try {
|
|
42
|
+
assertPrintableSingleLine(reason, "docs reason");
|
|
43
|
+
} catch {
|
|
44
|
+
fail("docs reason must be a non-empty single printable line without control characters");
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (decision === "not-applicable") {
|
|
48
|
+
if (paths.length > 0) {
|
|
49
|
+
fail(
|
|
50
|
+
"not-applicable takes no paths — put the why into --reason:\n" +
|
|
51
|
+
' stdd docs not-applicable --reason "<why implementation-only>"',
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
if (!reason) fail("not-applicable needs --reason <why implementation-only>");
|
|
55
|
+
} else if (paths.length === 0) {
|
|
56
|
+
fail(`${decision} needs at least one docs path`);
|
|
57
|
+
}
|
|
58
|
+
if (decision === "checked" && !reason) {
|
|
59
|
+
fail("checked needs --reason <why no change is needed>");
|
|
60
|
+
}
|
|
61
|
+
for (const docPath of paths) {
|
|
62
|
+
try {
|
|
63
|
+
resolveRepoPath(cwd, docPath, `docs path ${JSON.stringify(docPath)}`);
|
|
64
|
+
} catch (err) {
|
|
65
|
+
fail(err.message);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const docsContext = ledgerAppendContext(cwd, { event: "docs" });
|
|
69
|
+
const event = {
|
|
70
|
+
event: "docs",
|
|
71
|
+
decision,
|
|
72
|
+
paths,
|
|
73
|
+
snapshot: checkoutSnapshot(cwd),
|
|
74
|
+
...(reason ? { reason } : {}),
|
|
75
|
+
...(docsContext.task ? { taskId: docsContext.task.id } : {}),
|
|
76
|
+
};
|
|
77
|
+
try {
|
|
78
|
+
withCapturedLedgerIdentity(
|
|
79
|
+
cwd,
|
|
80
|
+
{
|
|
81
|
+
expectedBranch: docsContext.branch,
|
|
82
|
+
expectedTaskState: docsContext.taskState,
|
|
83
|
+
subject: "docs evidence",
|
|
84
|
+
retry: "stdd docs",
|
|
85
|
+
},
|
|
86
|
+
() =>
|
|
87
|
+
appendLedger(cwd, event, {
|
|
88
|
+
preserveTaskScope: true,
|
|
89
|
+
lockHeld: true,
|
|
90
|
+
expectedBranch: docsContext.branch,
|
|
91
|
+
}),
|
|
92
|
+
);
|
|
93
|
+
} catch (err) {
|
|
94
|
+
fail(err.message);
|
|
95
|
+
}
|
|
96
|
+
console.log(`stdd docs: recorded ${decision}${paths.length ? ` (${paths.join(", ")})` : ""}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* `stdd red|verify -- <cmd>` — run the command, record {cmd, exit, excerpt}
|
|
101
|
+
* verbatim, pass the exit code through. Output flows to the caller unchanged.
|
|
102
|
+
*/
|
|
103
|
+
export function recordRun(cwd, kind, argv) {
|
|
104
|
+
// A single whitespace-carrying word after -- is a description, not a
|
|
105
|
+
// command — it can never spawn. Reject with the corrected form and
|
|
106
|
+
// record nothing: prose in the ledger verifies nothing.
|
|
107
|
+
if (argv.length === 1 && /\s/.test(argv[0])) {
|
|
108
|
+
fail(
|
|
109
|
+
`${kind} takes the command and its arguments, never prose — nothing was recorded\n` +
|
|
110
|
+
` e.g.: stdd ${kind} -- pnpm --filter api test\n` +
|
|
111
|
+
` shell constructs: stdd ${kind} -- sh -c "<cmd>"`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
// A recorder must know it can persist the result before it launches a
|
|
115
|
+
// command with arbitrary side effects.
|
|
116
|
+
const runContext = ledgerAppendContext(cwd, { event: kind });
|
|
117
|
+
const config = loadConfig(cwd);
|
|
118
|
+
const result = spawnSync(argv[0], argv.slice(1), {
|
|
119
|
+
encoding: "utf8",
|
|
120
|
+
maxBuffer: MAX_SUBPROCESS_BUFFER,
|
|
121
|
+
});
|
|
122
|
+
let exit = result.status ?? 1;
|
|
123
|
+
let output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
|
|
124
|
+
if (result.error) {
|
|
125
|
+
exit = 127;
|
|
126
|
+
output += `${result.error.message}\n`;
|
|
127
|
+
}
|
|
128
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
129
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
130
|
+
if (result.error) {
|
|
131
|
+
console.error(
|
|
132
|
+
`stdd ${kind}: ${result.error.message}` +
|
|
133
|
+
(result.error.code === "ENOENT"
|
|
134
|
+
? " — command not found; is the worktree ready (stdd doctor --readiness)? " +
|
|
135
|
+
"Shell constructs need sh -c"
|
|
136
|
+
: ""),
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const event = {
|
|
141
|
+
event: kind,
|
|
142
|
+
cmd: argv.join(" "),
|
|
143
|
+
exit,
|
|
144
|
+
excerpt: output.slice(-EXCERPT_LIMIT),
|
|
145
|
+
snapshot: checkoutSnapshot(cwd),
|
|
146
|
+
};
|
|
147
|
+
if (kind === "red") {
|
|
148
|
+
event.genuine = redGenuine(exit, output, config.redPattern ?? null);
|
|
149
|
+
if (exit === 0) {
|
|
150
|
+
console.error('stdd red: the command exited 0 — that is green, not red (recorded genuine: "no")');
|
|
151
|
+
} else if (event.genuine === "unknown") {
|
|
152
|
+
console.error(
|
|
153
|
+
'stdd red: no redPattern in .stdd/config.json — cannot assert genuine-red (recorded genuine: "unknown")',
|
|
154
|
+
);
|
|
155
|
+
} else if (event.genuine === "no") {
|
|
156
|
+
console.error(
|
|
157
|
+
'stdd red: output does not match redPattern — this looks like an environment error, not a genuine red (recorded genuine: "no")',
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
withCapturedLedgerIdentity(
|
|
163
|
+
cwd,
|
|
164
|
+
{
|
|
165
|
+
expectedBranch: runContext.branch,
|
|
166
|
+
expectedTaskState: runContext.taskState,
|
|
167
|
+
subject: `${kind} result`,
|
|
168
|
+
retry: `stdd ${kind}`,
|
|
169
|
+
},
|
|
170
|
+
() =>
|
|
171
|
+
appendLedger(
|
|
172
|
+
cwd,
|
|
173
|
+
{
|
|
174
|
+
...event,
|
|
175
|
+
...(runContext.task ? { taskId: runContext.task.id } : {}),
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
preserveTaskScope: true,
|
|
179
|
+
lockHeld: true,
|
|
180
|
+
expectedBranch: runContext.branch,
|
|
181
|
+
},
|
|
182
|
+
),
|
|
183
|
+
);
|
|
184
|
+
} catch (err) {
|
|
185
|
+
fail(err.message);
|
|
186
|
+
}
|
|
187
|
+
process.exit(exit);
|
|
188
|
+
}
|