faberun 0.3.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.
Files changed (144) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +131 -0
  3. package/bin/faberun.mjs +25 -0
  4. package/integrations/claude-code/statusline-bench.sh +42 -0
  5. package/integrations/claude-code/statusline.sh +80 -0
  6. package/package.json +33 -0
  7. package/skills/faberun/SKILL.md +24 -0
  8. package/skills/faberun/references/contract.md +380 -0
  9. package/skills/faberun/references/engineering.md +29 -0
  10. package/skills/faberun/references/handoffs.md +26 -0
  11. package/skills/faberun/references/operations.md +184 -0
  12. package/skills/faberun/references/rules.md +35 -0
  13. package/skills/faberun/references/workflow.md +23 -0
  14. package/skills/init-agentkit/SKILL.md +108 -0
  15. package/skills/init-agentkit/scripts/install-agentkit.sh +127 -0
  16. package/skills/init-agentkit/templates/.claude/commands/create-adr.md +44 -0
  17. package/skills/init-agentkit/templates/.github/workflows/quality.yml +43 -0
  18. package/skills/init-agentkit/templates/.sentrux/baseline.json +9 -0
  19. package/skills/init-agentkit/templates/.sentrux/rules.toml +21 -0
  20. package/skills/init-agentkit/templates/AGENTS.md +110 -0
  21. package/skills/init-agentkit/templates/docs/ABSTRACTIONS.md +30 -0
  22. package/skills/init-agentkit/templates/docs/ARCHITECTURE.md +31 -0
  23. package/skills/init-agentkit/templates/docs/GETTING-STARTED.md +44 -0
  24. package/skills/init-agentkit/templates/docs/VISION.md +33 -0
  25. package/skills/init-agentkit/templates/docs/adr/0001-record-architecture-decisions.md +36 -0
  26. package/skills/init-agentkit/templates/docs/adr/0002-root-managed-ai-guidance.md +37 -0
  27. package/skills/init-agentkit/templates/docs/adr/0003-sentrux-structural-quality-gates.md +49 -0
  28. package/skills/init-agentkit/templates/docs/adr/README.md +52 -0
  29. package/skills/init-agentkit/templates/docs/sentrux.md +66 -0
  30. package/skills/init-agentkit/templates/githooks/commit-msg +22 -0
  31. package/skills/init-agentkit/templates/githooks/pre-commit +32 -0
  32. package/src/campaign/brief.mjs +394 -0
  33. package/src/campaign/chain.mjs +555 -0
  34. package/src/campaign/handoff.mjs +516 -0
  35. package/src/campaign/index.mjs +300 -0
  36. package/src/campaign/journal.mjs +347 -0
  37. package/src/campaign/layout.mjs +51 -0
  38. package/src/campaign/metrics-evals.mjs +25 -0
  39. package/src/campaign/metrics.mjs +517 -0
  40. package/src/campaign/projection.mjs +250 -0
  41. package/src/campaign/record.mjs +102 -0
  42. package/src/campaign/unpark.mjs +56 -0
  43. package/src/cli/brand.mjs +205 -0
  44. package/src/cli/campaign.mjs +730 -0
  45. package/src/cli/contract.mjs +67 -0
  46. package/src/cli/init.mjs +170 -0
  47. package/src/cli/launch.mjs +239 -0
  48. package/src/cli/seat.mjs +139 -0
  49. package/src/cli/setup.mjs +294 -0
  50. package/src/cli/skills.mjs +105 -0
  51. package/src/cli/update.mjs +216 -0
  52. package/src/cli.mjs +525 -0
  53. package/src/contract/articles.mjs +12 -0
  54. package/src/contract/assert.mjs +162 -0
  55. package/src/contract/definition-of-done.mjs +97 -0
  56. package/src/contract/final-verification.mjs +96 -0
  57. package/src/contract/index.mjs +641 -0
  58. package/src/contract/judge-envelope.mjs +25 -0
  59. package/src/contract/review-modes.mjs +151 -0
  60. package/src/contract/runtime.mjs +204 -0
  61. package/src/contract/schema-version.mjs +25 -0
  62. package/src/contract/scope-findings.mjs +77 -0
  63. package/src/contract/snapshot.mjs +639 -0
  64. package/src/contract/task-packet.mjs +495 -0
  65. package/src/contract/untrusted.mjs +75 -0
  66. package/src/contract/verification.mjs +185 -0
  67. package/src/contract/worker-result.mjs +138 -0
  68. package/src/engine/assignment.mjs +63 -0
  69. package/src/engine/backoff.mjs +492 -0
  70. package/src/engine/bulk-read.mjs +361 -0
  71. package/src/engine/cancel.mjs +177 -0
  72. package/src/engine/detach.mjs +101 -0
  73. package/src/engine/dispatch.mjs +752 -0
  74. package/src/engine/failover.mjs +192 -0
  75. package/src/engine/gate.mjs +183 -0
  76. package/src/engine/judge-gate.mjs +517 -0
  77. package/src/engine/lifecycle.mjs +772 -0
  78. package/src/engine/live-preflight.mjs +299 -0
  79. package/src/engine/mutation.mjs +146 -0
  80. package/src/engine/notify-queue.mjs +327 -0
  81. package/src/engine/process-identity.mjs +72 -0
  82. package/src/engine/process.mjs +774 -0
  83. package/src/engine/prompts.mjs +289 -0
  84. package/src/engine/recover.mjs +300 -0
  85. package/src/engine/result-file.mjs +222 -0
  86. package/src/engine/resume.mjs +635 -0
  87. package/src/engine/retry.mjs +334 -0
  88. package/src/engine/review.mjs +228 -0
  89. package/src/engine/run-command.mjs +287 -0
  90. package/src/engine/run-identity.mjs +411 -0
  91. package/src/engine/runtime-discovery.mjs +235 -0
  92. package/src/engine/scheduler.mjs +526 -0
  93. package/src/engine/scope.mjs +378 -0
  94. package/src/engine/settle.mjs +207 -0
  95. package/src/engine/state.mjs +148 -0
  96. package/src/engine/supervise.mjs +713 -0
  97. package/src/engine/verify.mjs +167 -0
  98. package/src/harnesses/agy/index.mjs +62 -0
  99. package/src/harnesses/catalogue.mjs +509 -0
  100. package/src/harnesses/claude/index.mjs +90 -0
  101. package/src/harnesses/codex/index.mjs +87 -0
  102. package/src/harnesses/dsh/closed-packet.patch.yml +42 -0
  103. package/src/harnesses/dsh/index.mjs +210 -0
  104. package/src/harnesses/dsh/runner.mjs +259 -0
  105. package/src/harnesses/exec-jsonl/index.mjs +788 -0
  106. package/src/harnesses/index.mjs +508 -0
  107. package/src/harnesses/protocol.mjs +531 -0
  108. package/src/harnesses/replay/bin.mjs +386 -0
  109. package/src/harnesses/replay/index.mjs +238 -0
  110. package/src/harnesses/zcode/index.mjs +276 -0
  111. package/src/host/config.mjs +87 -0
  112. package/src/host/home.mjs +149 -0
  113. package/src/host/package.mjs +23 -0
  114. package/src/host/preflight.mjs +520 -0
  115. package/src/host/tool-policy-decisions.mjs +341 -0
  116. package/src/host/tool-policy-hook.mjs +270 -0
  117. package/src/notify/index.mjs +359 -0
  118. package/src/notify/os-macos.mjs +81 -0
  119. package/src/repo/declared-paths.mjs +220 -0
  120. package/src/repo/integrate.mjs +546 -0
  121. package/src/repo/scope-closure.mjs +665 -0
  122. package/src/repo/signal-block.mjs +16 -0
  123. package/src/repo/signal.mjs +222 -0
  124. package/src/repo/source-identity.mjs +295 -0
  125. package/src/repo/workspace.mjs +557 -0
  126. package/src/repo/worktree.mjs +352 -0
  127. package/src/report/final.mjs +200 -0
  128. package/src/report/metrics-report.mjs +99 -0
  129. package/src/report/next.mjs +383 -0
  130. package/src/report/render.mjs +716 -0
  131. package/src/run/disk-gc.mjs +251 -0
  132. package/src/run/lock.mjs +329 -0
  133. package/src/run/node-store.mjs +62 -0
  134. package/src/run/operations.mjs +286 -0
  135. package/src/run/store.mjs +187 -0
  136. package/src/run/usage.mjs +337 -0
  137. package/src/seat/harnesses.mjs +83 -0
  138. package/src/seat/index.mjs +239 -0
  139. package/src/seat/tmux.mjs +208 -0
  140. package/src/util.mjs +0 -0
  141. package/src/web/api.mjs +371 -0
  142. package/src/web/boundary.mjs +88 -0
  143. package/src/web/index.html +299 -0
  144. package/src/web/server.mjs +552 -0
@@ -0,0 +1,287 @@
1
+ /**
2
+ * Running a verification command and bounding what comes back.
3
+ *
4
+ * The controller runs each command itself, in the attempt workspace, with a
5
+ * deliberately narrow environment (`VERIFICATION_ENV_BASE_NAMES`): a worker must
6
+ * not be able to make a suite pass by exporting something. Output is captured
7
+ * head-and-tail rather than whole, because a fuzz log will happily fill a disk,
8
+ * and the process is killed by group so a test runner's children die with it.
9
+ *
10
+ * The schema for what may be run lives in `contract/verification.mjs`; this is
11
+ * only the doing.
12
+ */
13
+ import { Buffer } from "node:buffer";
14
+ import { VERIFICATION_LIMITS, compactVerification, resolveVerificationCwd, validateVerificationCommands } from "../contract/verification.mjs";
15
+ import { mkdirSync, writeFileSync } from "node:fs";
16
+ import { processStartToken } from "../run/lock.mjs";
17
+ import { randomUUID } from "node:crypto";
18
+ import { runMutation } from "./mutation.mjs";
19
+ import { spawn } from "node:child_process";
20
+ /** @typedef {import("../contract/verification.mjs").VerificationOptions} VerificationOptions */
21
+
22
+ /** @typedef {import("node:child_process").ChildProcess} ChildProcess */
23
+ /** @typedef {import("../contract/verification.mjs").VerificationAttempt} VerificationAttempt */
24
+ /** @typedef {import("../contract/verification.mjs").VerificationAttemptResult} VerificationAttemptResult */
25
+ /** @typedef {import("../contract/verification.mjs").VerificationCommand} VerificationCommand */
26
+ /** @typedef {import("../contract/verification.mjs").VerificationCommandResult} VerificationCommandResult */
27
+ /** @typedef {import("../contract/verification.mjs").VerificationResult} VerificationResult */
28
+
29
+ // Verification children receive only the declared environment-variable names
30
+ // plus a minimal base set needed to spawn a process. Ambient controller
31
+ // variables (including secrets) must never leak into verification commands.
32
+ const VERIFICATION_ENV_BASE_NAMES = Object.freeze([
33
+ "PATH",
34
+ "HOME",
35
+ "USER",
36
+ "LOGNAME",
37
+ "SHELL",
38
+ "TMPDIR",
39
+ "TEMP",
40
+ "TMP",
41
+ "SYSTEMROOT",
42
+ "WINDIR",
43
+ "COMSPEC",
44
+ "PATHEXT",
45
+ "LANG",
46
+ "LC_ALL",
47
+ "LC_CTYPE",
48
+ "TERM",
49
+ ]);
50
+
51
+ /**
52
+ * @param {VerificationCommand} command
53
+ * @returns {Record<string, string|undefined>}
54
+ */
55
+ function verificationEnv(command) {
56
+ const names = new Set([...(command.env ?? []), ...VERIFICATION_ENV_BASE_NAMES]);
57
+ /** @type {Record<string, string|undefined>} */
58
+ const env = {};
59
+ for (const name of names) {
60
+ if (process.env[name] !== undefined) env[name] = process.env[name];
61
+ }
62
+ return env;
63
+ }
64
+ /**
65
+ * Run every declared command `repeat` times inside the workspace.
66
+ *
67
+ * @param {unknown} commands
68
+ * @param {string} baseCwd
69
+ * @param {VerificationOptions} options
70
+ * @returns {Promise<VerificationResult>}
71
+ */
72
+ export async function runVerification(commands, baseCwd, options = {}) {
73
+ const validated = validateVerificationCommands(commands);
74
+ /** @type {VerificationCommandResult[]} */
75
+ const results = [];
76
+ for (const [commandIndex, command] of validated.entries()) {
77
+ const result = command.mutation
78
+ ? await runMutationCommand(command, baseCwd, commandIndex, options)
79
+ : await runRepeatedCommand(command, baseCwd, commandIndex, options);
80
+ results.push(result);
81
+ if (options.logDir) {
82
+ mkdirSync(options.logDir, { recursive: true });
83
+ writeFileSync(`${options.logDir}/verification-${results.length}.json`, `${JSON.stringify(compactVerification({ passed: result.passed, commands: [result] }))}\n`, { mode: 0o600 });
84
+ }
85
+ }
86
+ return { passed: results.every((result) => result.passed), commands: results };
87
+ }
88
+ /**
89
+ * The ordinary case: run the argv `repeat` times and pass when every attempt does.
90
+ *
91
+ * @param {VerificationCommand} command
92
+ * @param {string} baseCwd
93
+ * @param {number} commandIndex
94
+ * @param {VerificationOptions} options
95
+ * @returns {Promise<VerificationCommandResult>}
96
+ */
97
+ async function runRepeatedCommand(command, baseCwd, commandIndex, options) {
98
+ /** @type {VerificationAttemptResult[]} */
99
+ const attempts = [];
100
+ const repeat = command.repeat ?? 1;
101
+ for (let attempt = 1; attempt <= repeat; attempt += 1) {
102
+ attempts.push(await runCommand(command, baseCwd, command.cwd ?? ".", attempt, options.signal, options, commandIndex));
103
+ }
104
+ return { ...command, cwd: resolveVerificationCwd(baseCwd, command.cwd ?? "."), passed: attempts.every((item) => item.passed), attempts };
105
+ }
106
+ /**
107
+ * The mutation case: the entry passes on the mutant-kill fraction, and every
108
+ * attempt is a real run of the same argv against one broken file.
109
+ *
110
+ * @param {VerificationCommand} command
111
+ * @param {string} baseCwd
112
+ * @param {number} commandIndex
113
+ * @param {VerificationOptions} options
114
+ * @returns {Promise<VerificationCommandResult>}
115
+ */
116
+ async function runMutationCommand(command, baseCwd, commandIndex, options) {
117
+ const commandCwd = command.cwd ?? ".";
118
+ const outcome = await runMutation(command, baseCwd, {
119
+ writeFiles: options.writeFiles ?? [],
120
+ run: (attempt) => runCommand(command, baseCwd, commandCwd, attempt, options.signal, options, commandIndex),
121
+ });
122
+ return { ...command, cwd: resolveVerificationCwd(baseCwd, commandCwd), passed: outcome.passed, attempts: outcome.attempts };
123
+ }
124
+ /**
125
+ * @param {VerificationCommand} command
126
+ * @param {string} baseCwd
127
+ * @param {string} commandCwd
128
+ * @param {number} attempt
129
+ * @param {AbortSignal|undefined} signal
130
+ * @param {VerificationOptions} options
131
+ * @param {number} commandIndex
132
+ * @returns {Promise<VerificationAttemptResult>}
133
+ */
134
+ function runCommand(command, baseCwd, commandCwd, attempt, signal, options, commandIndex) {
135
+ return new Promise((resolveResult) => {
136
+ const started = process.hrtime.bigint();
137
+ const stdout = boundedTail(VERIFICATION_LIMITS.stdoutBytes);
138
+ const stderr = boundedTail(VERIFICATION_LIMITS.stderrBytes);
139
+ let settled = false;
140
+ let timedOut = false;
141
+ /** @type {import("node:child_process").ChildProcess|null} */
142
+ let child = null;
143
+ /** @type {ReturnType<typeof setTimeout>|null} */
144
+ let timer = null;
145
+ /** @type {(() => void)|null} */
146
+ let abortHandler = null;
147
+ /** @type {VerificationAttempt|null} */
148
+ let identity = null;
149
+ let completionReported = false;
150
+ /**
151
+ * @param {number|null} exitCode
152
+ * @param {string|null} signalName
153
+ * @param {Error|null} error
154
+ */
155
+ const finish = (exitCode, signalName, error = null) => {
156
+ if (settled) return;
157
+ settled = true;
158
+ if (timer) clearTimeout(timer);
159
+ if (abortHandler) signal?.removeEventListener("abort", abortHandler);
160
+ if (child?.pid && !error && !signalName && !timedOut) terminateGroup(child);
161
+ const durationMs = Number(process.hrtime.bigint() - started) / 1e6;
162
+ const result = {
163
+ attempt,
164
+ stdout: stdout.value(),
165
+ stderr: stderr.value(),
166
+ durationMs: Math.round(durationMs * 100) / 100,
167
+ exitCode: Number.isInteger(exitCode) ? exitCode : null,
168
+ signal: signalName ?? null,
169
+ timedOut,
170
+ error: error ? String(error.message ?? error) : null,
171
+ passed: !error && !timedOut && exitCode === 0 && !signalName,
172
+ };
173
+ if (!completionReported && identity) {
174
+ completionReported = true;
175
+ try { options?.onAttemptComplete?.({ ...identity, status: result.passed ? "closed" : "failed", completedAt: new Date().toISOString(), result }); } catch {
176
+ // Notification callback: any error it throws must not change the recorded result.
177
+ }
178
+ }
179
+ resolveResult(result);
180
+ };
181
+ /**
182
+ * The timeout and abort paths settle here, never on `close`: a grandchild
183
+ * that escaped the process group can hold the stdout pipe open forever, so
184
+ * waiting for `close` would park the loop's critical path on a process that
185
+ * will never report. Kill the group, destroy the pipes, and settle now.
186
+ *
187
+ * @param {"timeout"|"abort"} reason
188
+ */
189
+ const settleFromTimer = (reason) => {
190
+ if (settled) return;
191
+ timedOut = reason === "timeout";
192
+ if (child?.pid) terminateGroup(child);
193
+ try { child?.stdout?.destroy(); } catch {
194
+ // The stream already closed; destroying it again is a no-op.
195
+ }
196
+ try { child?.stderr?.destroy(); } catch {
197
+ // The stream already closed; destroying it again is a no-op.
198
+ }
199
+ finish(null, null, reason === "abort" ? new Error("verification command aborted") : null);
200
+ };
201
+ try {
202
+ const cwd = resolveVerificationCwd(baseCwd, commandCwd);
203
+ const startedAt = new Date().toISOString();
204
+ identity = {
205
+ invocationId: randomUUID(), commandIndex, attempt, pid: null, processStartToken: null, processGroupId: null,
206
+ startedAt, deadlineAt: new Date(Date.parse(startedAt) + (command.timeoutSec ?? 120) * 1_000).toISOString(), status: "active",
207
+ };
208
+ options?.onAttemptStart?.({ ...identity });
209
+ const env = verificationEnv(command);
210
+ child = spawn(command.argv[0], command.argv.slice(1), { cwd, env, detached: process.platform !== "win32", stdio: ["ignore", "pipe", "pipe"] });
211
+ const pid = child.pid ?? null;
212
+ let paused = false;
213
+ if (process.platform !== "win32" && pid) {
214
+ try { process.kill(-pid, "SIGSTOP"); paused = true; } catch {
215
+ // ESRCH: the child may have exited between spawn and the stop; not pausing is safe.
216
+ }
217
+ }
218
+ Object.assign(identity, { pid, processStartToken: processStartToken(pid), processGroupId: process.platform === "win32" ? null : pid });
219
+ options?.onAttemptSpawn?.({ ...identity });
220
+ if (paused && pid) {
221
+ try { process.kill(-pid, "SIGCONT"); } catch {
222
+ // ESRCH: the child is already gone, so there is nothing to resume.
223
+ }
224
+ }
225
+ const childStdout = /** @type {import("node:stream").Readable} */ (child.stdout);
226
+ const childStderr = /** @type {import("node:stream").Readable} */ (child.stderr);
227
+ childStdout.on("data", (chunk) => stdout.add(chunk));
228
+ childStderr.on("data", (chunk) => stderr.add(chunk));
229
+ child.once("error", (error) => finish(null, null, error));
230
+ child.once("close", (code, signalName) => finish(code, signalName));
231
+ abortHandler = () => settleFromTimer("abort");
232
+ if (signal?.aborted) abortHandler();
233
+ else signal?.addEventListener("abort", abortHandler, { once: true });
234
+ } catch (error) {
235
+ if (child?.pid && process.platform !== "win32") {
236
+ try { process.kill(-child.pid, "SIGCONT"); } catch {
237
+ // ESRCH: best-effort resume of a paused child that may already be gone.
238
+ }
239
+ }
240
+ finish(null, null, error instanceof Error ? error : new Error(String(error)));
241
+ }
242
+ if (child && !settled) timer = setTimeout(() => settleFromTimer("timeout"), (command.timeoutSec ?? 120) * 1_000);
243
+ });
244
+ }
245
+ /**
246
+ * @param {import("node:child_process").ChildProcess} child
247
+ */
248
+ function terminateGroup(child) {
249
+ try {
250
+ if (process.platform !== "win32") process.kill(-/** @type {number} */ (child.pid), "SIGTERM");
251
+ else child.kill("SIGTERM");
252
+ } catch {
253
+ try { child.kill("SIGTERM"); } catch {
254
+ // ESRCH: the group kill failed and the leader was already gone.
255
+ }
256
+ }
257
+ setTimeout(() => {
258
+ try {
259
+ if (process.platform !== "win32") process.kill(-/** @type {number} */ (child.pid), "SIGKILL");
260
+ else child.kill("SIGKILL");
261
+ } catch {
262
+ try { child.kill("SIGKILL"); } catch {
263
+ // ESRCH: the SIGKILL fallback found no leader left to kill.
264
+ }
265
+ }
266
+ }, 100).unref();
267
+ }
268
+ /**
269
+ * @param {number} maxBytes
270
+ */
271
+ function boundedTail(maxBytes) {
272
+ let value = Buffer.alloc(0);
273
+ return {
274
+ /**
275
+ * @param {string|Buffer} chunk
276
+ */
277
+ add(chunk) {
278
+ value = Buffer.concat([value, Buffer.from(chunk)]);
279
+ if (value.length > maxBytes) {
280
+ let start = value.length - maxBytes;
281
+ while (start < value.length && (value[start] & 0xc0) === 0x80) start += 1;
282
+ value = value.subarray(start);
283
+ }
284
+ },
285
+ value: () => value.toString("utf8"),
286
+ };
287
+ }