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,222 @@
1
+ /**
2
+ * The worker result sidecar: the file a worker writes its structured outcome
3
+ * into, and everything needed to find, read, canonicalise and clear it.
4
+ *
5
+ * It is a file and not the provider's final message because a provider's last
6
+ * message is prose it may decorate, truncate or repeat. A worker in an attempt
7
+ * worktree writes into that worktree, so the result has to be materialised back
8
+ * into the run directory -- which is what `materializeAttemptResult` and the
9
+ * result-materialization invocation are for.
10
+ *
11
+ * The sidecar carries no `changedFiles`: the controller measures the changed
12
+ * paths itself and records them as `state.scope.changedPaths`, so nothing here
13
+ * ever read a worker-declared list to pass along.
14
+ */
15
+ import { attemptWorkspace } from "../repo/worktree.mjs";
16
+ import { errorCode, errorMessage } from "../util.mjs";
17
+ import { existsSync, readFileSync, unlinkSync } from "node:fs";
18
+ import { extractJson } from "../harnesses/protocol.mjs";
19
+ import { invocationResult } from "./process.mjs";
20
+ import { join } from "node:path";
21
+ import { parseJudge } from "./prompts.mjs";
22
+ import { parseWorkerResult } from "../contract/worker-result.mjs";
23
+ import { readJson, writeJsonAtomic, writeTextAtomic } from "../run/store.mjs";
24
+ import { routeRuntimeForState, runtimeSnapshot } from "./failover.mjs";
25
+
26
+ /** @typedef {import("./lifecycle.mjs").Invocation} Invocation */
27
+ /** @typedef {import("../contract/index.mjs").NodeSnapshot} NodeSnapshot */
28
+ /** @typedef {import("../contract/index.mjs").ValidatedContract} ValidatedContract */
29
+ /** @typedef {import("../contract/index.mjs").ValidatedNode} ValidatedNode */
30
+ /** @typedef {import("../contract/worker-result.mjs").WorkerResult} WorkerResult */
31
+
32
+ /**
33
+ * @param {string} runDir
34
+ * @param {string} nodeId
35
+ * @returns {string}
36
+ */
37
+ export function workerResultPath(runDir, nodeId) {
38
+ return join(runDir, "results", `${nodeId}.json`);
39
+ }
40
+ /** @param {string} runDir @param {string} nodeId @param {string} workspace @returns {string} */
41
+ export function attemptWorkerResultPath(runDir, nodeId, workspace) {
42
+ return join(workspace, ".runs", "results", `${nodeId}.json`);
43
+ }
44
+ /** @param {string} workspace @param {string} nodeId */
45
+ export function clearAttemptWorkerResult(workspace, nodeId) {
46
+ try { unlinkSync(attemptWorkerResultPath("", nodeId, workspace)); } catch (error) {
47
+ if (errorCode(error) !== "ENOENT") throw error;
48
+ }
49
+ }
50
+ /** @param {string} runDir @param {NodeSnapshot} state @param {ValidatedNode} node */
51
+ export function materializeAttemptResult(runDir, state, node) {
52
+ const workspace = attemptWorkspace(state);
53
+ if (!workspace) return;
54
+ const source = attemptWorkerResultPath(runDir, node.id, workspace);
55
+ if (!existsSync(source)) return;
56
+ writeTextAtomic(workerResultPath(runDir, node.id), readFileSync(source, "utf8"));
57
+ }
58
+ /**
59
+ * The run-owned result file is the primary recovery source. The provider's
60
+ * final message is intentionally only redundant input.
61
+ *
62
+ * @param {string} runDir
63
+ * @param {string} nodeId
64
+ * @returns {WorkerResult|null}
65
+ */
66
+ export function readWorkerResultFile(runDir, nodeId) {
67
+ const path = workerResultPath(runDir, nodeId);
68
+ if (!existsSync(path)) return null;
69
+ try {
70
+ return parseWorkerResult(JSON.stringify(readJson(path)));
71
+ } catch (error) {
72
+ throw new TypeError(`canonical worker result ${path} is invalid: ${errorMessage(error)}`);
73
+ }
74
+ }
75
+ /** @param {string} runDir @param {string} nodeId @param {WorkerResult} result */
76
+ function persistWorkerResultFile(runDir, nodeId, result) {
77
+ writeJsonAtomic(workerResultPath(runDir, nodeId), result);
78
+ }
79
+ /** @param {string} runDir @param {string} nodeId */
80
+ export function clearWorkerResultFile(runDir, nodeId) {
81
+ try { unlinkSync(workerResultPath(runDir, nodeId)); } catch (error) {
82
+ if (errorCode(error) !== "ENOENT") throw error;
83
+ }
84
+ }
85
+ /** First line of the one-turn result-materialization prompt. */
86
+ export const RESULT_MATERIALIZATION_PROMPT_HEADER = "The implementation is already complete.";
87
+ /**
88
+ * The canonical result text without validation. Presence is authoritative:
89
+ * adoption decisions must surface a present-but-invalid file as an invalid
90
+ * result, never treat it as missing work.
91
+ *
92
+ * @param {string} runDir
93
+ * @param {string} nodeId
94
+ * @returns {string|null}
95
+ */
96
+ export function canonicalWorkerResultText(runDir, nodeId) {
97
+ const path = workerResultPath(runDir, nodeId);
98
+ if (!existsSync(path)) return null;
99
+ try { return JSON.stringify(readJson(path)); } catch { return readFileSync(path, "utf8"); }
100
+ }
101
+ /**
102
+ * The materialization mode must survive controller interruption, so it is
103
+ * derived from the persisted invocation prompt — the run-owned record of what
104
+ * that turn was asked to do — instead of in-memory job state.
105
+ *
106
+ * @param {Invocation|null|undefined} invocation
107
+ * @returns {boolean}
108
+ */
109
+ export function isResultMaterializationInvocation(invocation) {
110
+ if (!invocation?.promptPath) return false;
111
+ try {
112
+ return readFileSync(invocation.promptPath, "utf8").startsWith(RESULT_MATERIALIZATION_PROMPT_HEADER);
113
+ } catch {
114
+ return false;
115
+ }
116
+ }
117
+ /**
118
+ * @param {string} runDir
119
+ * @param {ValidatedNode} node
120
+ * @param {unknown} providerResult
121
+ * @returns {WorkerResult}
122
+ */
123
+ export function resolveWorkerResult(runDir, node, providerResult) {
124
+ const fromFile = readWorkerResultFile(runDir, node.id);
125
+ if (fromFile) return fromFile;
126
+ const result = parseWorkerResult(String(extractJson(providerResult) ?? providerResult ?? ""));
127
+ persistWorkerResultFile(runDir, node.id, result);
128
+ return result;
129
+ }
130
+ /**
131
+ * @param {string} prompt
132
+ * @param {string} resultPath
133
+ * @returns {string}
134
+ */
135
+ export function workerProtocolPrompt(prompt, resultPath) {
136
+ return [
137
+ prompt,
138
+ "Controller worker protocol:",
139
+ `Before your final response, write the required worker-result JSON object to this canonical result file: ${resultPath}`,
140
+ "Your final provider message is redundant; the result file is the recovery source.",
141
+ ].join("\n\n");
142
+ }
143
+ /**
144
+ * Parse a result persisted in a node checkpoint or operation settlement. The
145
+ * provider stream remains the first source of evidence; this path is used only
146
+ * after that stream is unavailable.
147
+ *
148
+ * @param {unknown} value
149
+ * @returns {string|null}
150
+ */
151
+ function parsePersistedWorkerResult(value) {
152
+ if (value === undefined || value === null) return null;
153
+ try {
154
+ const serialized = typeof value === "string" ? value : JSON.stringify(value);
155
+ parseWorkerResult(String(extractJson(serialized) ?? serialized));
156
+ return serialized;
157
+ } catch {
158
+ return null;
159
+ }
160
+ }
161
+ /**
162
+ * Durable worker-result evidence for recovery, in authority order: the run-owned
163
+ * canonical file first, then the operation settlement, then the node snapshot.
164
+ *
165
+ * @param {string} runDir
166
+ * @param {NodeSnapshot} state
167
+ * @param {Invocation} invocation
168
+ * @param {Record<string, unknown>|null} settlement
169
+ * @returns {string|null}
170
+ */
171
+ export function persistedWorkerResult(runDir, state, invocation, settlement) {
172
+ const fromFile = canonicalWorkerResultText(runDir, state.id);
173
+ if (fromFile !== null) return fromFile;
174
+ const fromSettlement = parsePersistedWorkerResult(settlement?.result);
175
+ if (fromSettlement) return fromSettlement;
176
+ if (invocation.status !== "active") return parsePersistedWorkerResult(state.result);
177
+ return null;
178
+ }
179
+ /**
180
+ * @param {NodeSnapshot} state
181
+ * @param {Invocation} invocation
182
+ * @param {Record<string, unknown>|null} settlement
183
+ * @returns {unknown|null}
184
+ */
185
+ export function persistedJudgeResult(state, invocation, settlement) {
186
+ const candidates = [settlement?.result, invocation.status !== "active" ? state.gate : null];
187
+ for (const candidate of candidates) {
188
+ if (candidate === undefined || candidate === null) continue;
189
+ try {
190
+ const serialized = typeof candidate === "string" ? candidate : JSON.stringify(candidate);
191
+ parseJudge(serialized);
192
+ return serialized;
193
+ } catch {
194
+ // Invalid candidate: skip it and try the next evidence source.
195
+ }
196
+ }
197
+ return null;
198
+ }
199
+ /**
200
+ * The worker result backing a judge-phase recovery. The canonical file is
201
+ * primary: a present-but-invalid file throws so the caller surfaces an
202
+ * invalid result, and only an absent file falls back to the transcript.
203
+ *
204
+ * @param {string} runDir
205
+ * @param {NodeSnapshot} state
206
+ * @param {ValidatedContract} contract
207
+ * @param {ValidatedNode} node
208
+ * @returns {WorkerResult|null}
209
+ */
210
+ export function recoverWorkerResult(runDir, state, contract, node) {
211
+ const fromFile = canonicalWorkerResultText(runDir, state.id);
212
+ if (fromFile !== null) {
213
+ // Presence is authoritative: an unparsable canonical file is an invalid
214
+ // result, never a license to adopt provider-derived evidence instead.
215
+ return parseWorkerResult(String(extractJson(fromFile) ?? fromFile));
216
+ }
217
+ const invocation = [...(state.invocations ?? [])].reverse().find((item) => item.phase === "worker");
218
+ if (!invocation) return null;
219
+ const result = invocationResult(invocation, invocation.runtimeId ? runtimeSnapshot(contract, invocation.runtimeId) : routeRuntimeForState(contract, node, state, "worker"));
220
+ if (result?.status !== "done") return null;
221
+ try { return parseWorkerResult(result.result ?? ""); } catch { return null; }
222
+ }