forge-workflow 0.1.0-beta.3 → 0.1.0-beta.5

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 (196) hide show
  1. package/AGENTS.md +14 -7
  2. package/CHANGELOG.md +43 -1
  3. package/README.md +6 -2
  4. package/bin/forge-cmd.js +21 -1
  5. package/bin/forge.js +16 -369
  6. package/docs/INDEX.md +1 -1
  7. package/docs/guides/BEADS_GITHUB_SYNC.md +2 -31
  8. package/docs/guides/MIGRATION.md +4 -4
  9. package/docs/guides/SETUP.md +16 -16
  10. package/docs/reference/COMMANDS.md +9 -4
  11. package/docs/reference/INSIGHTS_RECAP.md +9 -20
  12. package/docs/reference/RELEASE.md +5 -3
  13. package/docs/reference/TOOLCHAIN.md +8 -0
  14. package/docs/reference/protected-state-surfaces.md +4 -4
  15. package/docs/reference/shepherd.md +117 -17
  16. package/lefthook.yml +12 -0
  17. package/lib/activation/ensure-forge-home.js +33 -15
  18. package/lib/adapters/greptile-review-adapter.js +1 -1
  19. package/lib/adapters/pr-state-adapter.js +397 -100
  20. package/lib/agents-config.js +5 -0
  21. package/lib/audit-evidence.js +71 -110
  22. package/lib/capped-jsonl-log.js +236 -0
  23. package/lib/commands/_issue.js +31 -46
  24. package/lib/commands/_manifest.js +1 -1
  25. package/lib/commands/_registry.js +2 -2
  26. package/lib/commands/_resolve-command-opts.js +36 -29
  27. package/lib/commands/claim.js +2 -4
  28. package/lib/commands/clean.js +196 -32
  29. package/lib/commands/dev.js +4 -33
  30. package/lib/commands/hooks.js +358 -13
  31. package/lib/commands/insights.js +8 -3
  32. package/lib/commands/merge.js +600 -40
  33. package/lib/commands/plan.js +23 -115
  34. package/lib/commands/pr.js +1 -1
  35. package/lib/commands/preflight.js +11 -2
  36. package/lib/commands/prime.js +23 -3
  37. package/lib/commands/push.js +41 -51
  38. package/lib/commands/recall.js +60 -16
  39. package/lib/commands/recap.js +6 -1
  40. package/lib/commands/release.js +18 -4
  41. package/lib/commands/serve.js +5 -2
  42. package/lib/commands/setup.js +191 -95
  43. package/lib/commands/shepherd.js +49 -4
  44. package/lib/commands/ship.js +22 -23
  45. package/lib/commands/skill.js +383 -0
  46. package/lib/commands/status.js +54 -33
  47. package/lib/commands/test.js +56 -34
  48. package/lib/commands/worktree.js +247 -43
  49. package/lib/core/runtime-graph.js +89 -15
  50. package/lib/doc-assertions.js +297 -0
  51. package/lib/existing-tdd-gate.js +253 -0
  52. package/lib/forge-context.js +1 -4
  53. package/lib/forge-issues.js +64 -491
  54. package/lib/git-defaults.js +56 -0
  55. package/lib/harness-capability-matrix.js +5 -5
  56. package/lib/hook-renderer.js +147 -16
  57. package/lib/insights.js +96 -80
  58. package/lib/issue-backend.js +42 -3
  59. package/lib/kernel/backing-issue.js +14 -2
  60. package/lib/kernel/broker.js +44 -0
  61. package/lib/kernel/cli-broker-factory.js +12 -1
  62. package/lib/kernel/close-on-merge.js +154 -0
  63. package/lib/kernel/fs-class.js +42 -25
  64. package/lib/kernel/migrations.js +30 -2
  65. package/lib/kernel/schema.js +35 -0
  66. package/lib/kernel/sqlite-driver.js +292 -18
  67. package/lib/lefthook-wiring.js +21 -1
  68. package/lib/memory/router.js +16 -1
  69. package/lib/memory-digest.js +47 -15
  70. package/lib/memory-recall-events.js +145 -0
  71. package/lib/memory-recall.js +212 -0
  72. package/lib/merge-rules.js +8 -4
  73. package/lib/npm-publish-workflow.js +272 -0
  74. package/lib/orientation.js +371 -49
  75. package/lib/plugin-catalog.js +14 -4
  76. package/lib/pr-bundle.js +9 -6
  77. package/lib/pr-monitor/journal.js +18 -2
  78. package/lib/pr-monitor/reconcile-executor.js +842 -0
  79. package/lib/pr-monitor/reconcile-tick.js +138 -0
  80. package/lib/pr-monitor/reconcile.js +0 -0
  81. package/lib/pr-monitor/render-summary.js +196 -0
  82. package/lib/pr-monitor/shepherd-lease.js +252 -0
  83. package/lib/pr-monitor/watch-lifecycle.js +14 -2
  84. package/lib/pr-pull.js +98 -24
  85. package/lib/pr-shepherd.js +34 -8
  86. package/lib/preflight/gates.js +65 -18
  87. package/lib/preflight/runner.js +5 -0
  88. package/lib/project-memory.js +40 -0
  89. package/lib/protected-state-authority.js +305 -0
  90. package/lib/protected-state-surfaces.js +64 -44
  91. package/lib/release-readiness.js +51 -4
  92. package/lib/rules-sync.js +4 -0
  93. package/lib/runtime-health.js +15 -46
  94. package/lib/shell-utils.js +1 -1
  95. package/lib/skill-eval.js +750 -0
  96. package/lib/skills-sync.js +6 -3
  97. package/lib/smart-merge.js +28 -4
  98. package/lib/status/identity.js +46 -0
  99. package/lib/status/presenter.js +0 -35
  100. package/lib/status/snapshot.js +11 -16
  101. package/lib/symlink-utils.js +74 -26
  102. package/lib/upgrade-safety.js +47 -9
  103. package/lib/using-forge.js +328 -0
  104. package/lib/workflow/enforce-stage.js +5 -5
  105. package/lib/workflow/state-manager.js +23 -23
  106. package/package.json +6 -7
  107. package/rules/using-forge.md +24 -0
  108. package/scripts/doc-asserting-tests.js +158 -0
  109. package/scripts/forge-team/index.sh +0 -5
  110. package/scripts/forge-team/tests/dispatcher.test.sh +1 -1
  111. package/scripts/forge-team/tests/workflow-integration.test.sh +0 -1
  112. package/scripts/lib/behavioral-eval-runner.js +310 -0
  113. package/scripts/lib/behavioral-eval-runtime.js +456 -0
  114. package/scripts/lib/eval-evidence.js +328 -0
  115. package/scripts/lib/eval-runner.js +81 -41
  116. package/scripts/lib/immutable-eval-corpus.js +309 -0
  117. package/scripts/lib/promotion-evidence-loader.js +94 -0
  118. package/scripts/lib/promotion-scorecard.js +314 -0
  119. package/scripts/npm-release-receipt.js +134 -0
  120. package/scripts/process-tree.js +761 -0
  121. package/scripts/protected-state-check.js +47 -22
  122. package/scripts/run-command-eval.js +29 -1
  123. package/scripts/sync-d20-audit.js +172 -0
  124. package/scripts/test-full-suite.js +249 -37
  125. package/scripts/test.js +184 -44
  126. package/skills/claim-safety/SKILL.md +4 -0
  127. package/skills/claim-safety/evals/scorecard.json +41 -0
  128. package/skills/coverage.json +83 -0
  129. package/skills/dev/SKILL.md +4 -0
  130. package/skills/dev/evals/scorecard.json +41 -0
  131. package/skills/gates/SKILL.md +80 -0
  132. package/skills/gates/evals/evals.json +38 -0
  133. package/skills/gates/evals/scorecard.json +41 -0
  134. package/skills/hermes-forge/SKILL.md +1 -0
  135. package/skills/hermes-forge/evals/scorecard.json +41 -0
  136. package/skills/issue-basics/SKILL.md +1 -0
  137. package/skills/issue-basics/evals/scorecard.json +41 -0
  138. package/skills/kernel/SKILL.md +38 -0
  139. package/skills/kernel/evals/scorecard.json +41 -0
  140. package/skills/memory/SKILL.md +16 -1
  141. package/skills/memory/evals/scorecard.json +41 -0
  142. package/skills/parallel-deep-research/SKILL.md +1 -0
  143. package/skills/parallel-deep-research/evals/scorecard.json +41 -0
  144. package/skills/plan/SKILL.md +6 -0
  145. package/skills/plan/evals/scorecard.json +41 -0
  146. package/skills/portability/SKILL.md +47 -0
  147. package/skills/portability/evals/evals.json +34 -0
  148. package/skills/portability/evals/scorecard.json +41 -0
  149. package/skills/research/SKILL.md +1 -0
  150. package/skills/research/evals/scorecard.json +41 -0
  151. package/skills/review/SKILL.md +10 -11
  152. package/skills/review/evals/scorecard.json +41 -0
  153. package/skills/rollback/SKILL.md +5 -11
  154. package/skills/rollback/evals/scorecard.json +41 -0
  155. package/skills/setup/SKILL.md +91 -0
  156. package/skills/setup/evals/evals.json +42 -0
  157. package/skills/setup/evals/scorecard.json +41 -0
  158. package/skills/shepherd/SKILL.md +84 -38
  159. package/skills/shepherd/evals/evals.json +21 -9
  160. package/skills/shepherd/evals/scorecard.json +41 -0
  161. package/skills/ship/SKILL.md +10 -12
  162. package/skills/ship/evals/scorecard.json +41 -0
  163. package/skills/smith/SKILL.md +8 -0
  164. package/skills/smith/evals/scorecard.json +41 -0
  165. package/skills/sonarcloud/SKILL.md +1 -0
  166. package/skills/sonarcloud/evals/scorecard.json +41 -0
  167. package/skills/sonarcloud-analysis/SKILL.md +1 -0
  168. package/skills/sonarcloud-analysis/evals/scorecard.json +41 -0
  169. package/skills/status/SKILL.md +3 -0
  170. package/skills/status/evals/scorecard.json +41 -0
  171. package/skills/triage-ready/SKILL.md +2 -0
  172. package/skills/triage-ready/evals/scorecard.json +41 -0
  173. package/skills/using-forge/SKILL.md +104 -0
  174. package/skills/using-forge/evals/scorecard.json +41 -0
  175. package/skills/validate/SKILL.md +4 -0
  176. package/skills/validate/evals/scorecard.json +41 -0
  177. package/skills/verify/SKILL.md +4 -0
  178. package/skills/verify/evals/scorecard.json +41 -0
  179. package/skills/worktree/SKILL.md +92 -0
  180. package/skills/worktree/evals/evals.json +38 -0
  181. package/skills/worktree/evals/scorecard.json +41 -0
  182. package/lib/adapters/beads-issue-adapter.js +0 -127
  183. package/lib/beads-nudge.js +0 -91
  184. package/lib/beads-setup.js +0 -538
  185. package/lib/beads-sync-scaffold.js +0 -189
  186. package/lib/commands/board.js +0 -64
  187. package/lib/pat-setup.js +0 -207
  188. package/lib/pr-monitor/render-sticky.js +0 -192
  189. package/lib/pr-monitor/upsert-sticky.js +0 -169
  190. package/lib/status/beads-snapshot.js +0 -145
  191. package/scripts/beads-context.sh +0 -577
  192. package/scripts/beads-migrate-to-dolt.sh +0 -7
  193. package/scripts/beads-upgrade-smoke.sh +0 -284
  194. package/scripts/forge-team/lib/dashboard.sh +0 -316
  195. package/scripts/forge-team/tests/dashboard.test.sh +0 -155
  196. package/scripts/lib/beads-migrate-to-dolt.mjs +0 -503
@@ -0,0 +1,456 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const { execFileSync, spawnSync } = require('node:child_process');
6
+ const { contentHash } = require('../../lib/file-hash');
7
+ const { stableStringify } = require('../../lib/kernel/evaluators');
8
+ const { resolveActiveIssueId } = require('../../lib/workflow/enforce-stage');
9
+ const { isWorkableStatus } = require('../../lib/kernel/readiness-model');
10
+ const { PrStateAdapter } = require('../../lib/adapters/pr-state-adapter');
11
+ const {
12
+ buildKernelIssueDeps,
13
+ resolveKernelDatabasePath,
14
+ } = require('../../lib/kernel/cli-broker-factory');
15
+ const { executeCommand } = require('./eval-runner');
16
+ const { parseTranscript } = require('./transcript-parser');
17
+ const { hashPacket } = require('./immutable-eval-corpus');
18
+
19
+ const FULL_SHA = /^[0-9a-f]{40}$/;
20
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
21
+ const POSITIVE_INTEGER = /^[1-9][0-9]*$/;
22
+ const EFFORTS = new Set(['low', 'medium', 'high', 'max']);
23
+ const RUNTIME_REASON_CODES = new Set([
24
+ 'runtime.invalid',
25
+ 'runtime.unavailable',
26
+ 'runtime.config_invalid',
27
+ 'runtime.output_missing',
28
+ 'runtime.output_unparseable',
29
+ 'runtime.execution_failed',
30
+ 'runtime.usage_unparseable',
31
+ 'runtime.token_budget_exceeded',
32
+ 'models.invalid',
33
+ 'effort.invalid',
34
+ 'budget.invalid',
35
+ 'binding.head_invalid',
36
+ 'attribution.issue_unavailable',
37
+ 'attribution.pr_mismatch',
38
+ 'attribution.pr_unavailable',
39
+ ]);
40
+ const RUNTIME_CONTROLS = Object.freeze({
41
+ customizationIsolation: 'safe-mode',
42
+ sessionPersistence: false,
43
+ tokenEnforcement: 'parsed-usage',
44
+ tools: Object.freeze([]),
45
+ });
46
+ const OBSERVATION_KEYS = Object.freeze([
47
+ 'decision', 'contract', 'head', 'review', 'checks', 'ci', 'claim', 'security',
48
+ 'order', 'platform', 'scope', 'compaction', 'wait', 'hardFailure', 'observer',
49
+ ]);
50
+
51
+ function runtimeCommand(env) {
52
+ const value = env.FORGE_EVAL_RUNTIME;
53
+ if (!value) return ['claude'];
54
+ if (value.trim().startsWith('[')) {
55
+ let parsed;
56
+ try {
57
+ parsed = JSON.parse(value);
58
+ } catch {
59
+ throw new Error('runtime.invalid');
60
+ }
61
+ if (!Array.isArray(parsed) || parsed.length === 0
62
+ || parsed.some((item) => typeof item !== 'string' || item.length === 0)) {
63
+ throw new Error('runtime.invalid');
64
+ }
65
+ return parsed;
66
+ }
67
+ return [value];
68
+ }
69
+
70
+ function parseModels(env) {
71
+ const models = String(env.FORGE_EVAL_MODELS || 'sol,luna')
72
+ .split(',')
73
+ .map((model) => model.trim())
74
+ .filter(Boolean);
75
+ if (models.length !== 2 || new Set(models).size !== 2) throw new Error('models.invalid');
76
+ return models;
77
+ }
78
+
79
+ function parseEffort(env) {
80
+ const effort = String(env.FORGE_EVAL_EFFORT || 'high').trim().toLowerCase();
81
+ if (!EFFORTS.has(effort)) throw new Error('effort.invalid');
82
+ return effort;
83
+ }
84
+
85
+ function resolveHead(projectRoot, execFile = execFileSync) {
86
+ const head = execFile('git', ['rev-parse', '--verify', 'HEAD'], {
87
+ cwd: projectRoot,
88
+ encoding: 'utf8',
89
+ stdio: ['ignore', 'pipe', 'pipe'],
90
+ }).trim().toLowerCase();
91
+ if (!FULL_SHA.test(head)) throw new Error('binding.head_invalid');
92
+ return head;
93
+ }
94
+
95
+ function resolveBranch(projectRoot, execFile = execFileSync) {
96
+ return execFile('git', ['branch', '--show-current'], {
97
+ cwd: projectRoot,
98
+ encoding: 'utf8',
99
+ stdio: ['ignore', 'pipe', 'pipe'],
100
+ }).trim();
101
+ }
102
+
103
+ async function resolveKernelIssue(projectRoot, branch, requestedIssueId, deps = {}) {
104
+ let driver;
105
+ let ownsDriver = false;
106
+ try {
107
+ driver = deps.kernelDriver;
108
+ if (!driver) {
109
+ const databasePath = resolveKernelDatabasePath({ projectRoot });
110
+ if (!databasePath || !fs.existsSync(databasePath)) return null;
111
+ driver = buildKernelIssueDeps({ projectRoot, databasePath }).kernelDriver;
112
+ ownsDriver = true;
113
+ }
114
+ const activeIssueId = await resolveActiveIssueId(driver, branch);
115
+ const issueId = requestedIssueId || activeIssueId;
116
+ if (!issueId || activeIssueId !== issueId || typeof driver.loadKernelEntity !== 'function') return null;
117
+ const issue = await driver.loadKernelEntity('issue', issueId, {}, {});
118
+ if (!issue || issue.id !== issueId || !isWorkableStatus(issue.status)) return null;
119
+ return issueId;
120
+ } catch {
121
+ return null;
122
+ } finally {
123
+ if (ownsDriver && driver && typeof driver.close === 'function') {
124
+ try { await driver.close(); } catch { /* read-only cleanup */ }
125
+ }
126
+ }
127
+ }
128
+
129
+ function resolvePrFromGh(projectRoot, execFile = execFileSync) {
130
+ try {
131
+ const raw = execFile('gh', ['pr', 'view', '--json', 'number'], {
132
+ cwd: projectRoot,
133
+ encoding: 'utf8',
134
+ stdio: ['ignore', 'pipe', 'pipe'],
135
+ timeout: 30000,
136
+ windowsHide: true,
137
+ });
138
+ const value = JSON.parse(raw);
139
+ if (!Number.isSafeInteger(value.number) || value.number <= 0) return null;
140
+ return value.number;
141
+ } catch {
142
+ return null;
143
+ }
144
+ }
145
+
146
+ function createPrAdapter(projectRoot, execFile = execFileSync) {
147
+ return new PrStateAdapter({
148
+ gh: (command, args) => execFile(command, args, {
149
+ cwd: projectRoot,
150
+ encoding: 'utf8',
151
+ stdio: ['ignore', 'pipe', 'pipe'],
152
+ timeout: 30000,
153
+ windowsHide: true,
154
+ }),
155
+ });
156
+ }
157
+
158
+ async function resolveAttribution(projectRoot, head, env, deps = {}) {
159
+ const branch = resolveBranch(projectRoot, deps.execFileSync);
160
+ const requestedIssueId = env.FORGE_EVAL_ISSUE_ID;
161
+ if (requestedIssueId && !UUID.test(String(requestedIssueId))) {
162
+ throw new Error('attribution.issue_unavailable');
163
+ }
164
+ const issueId = await resolveKernelIssue(projectRoot, branch, requestedIssueId, deps);
165
+ if (!UUID.test(String(issueId || ''))) throw new Error('attribution.issue_unavailable');
166
+
167
+ const explicitPr = env.FORGE_EVAL_PR !== undefined || env.FORGE_EVAL_PR_HEAD !== undefined;
168
+ if (explicitPr && (!POSITIVE_INTEGER.test(String(env.FORGE_EVAL_PR || ''))
169
+ || (env.FORGE_EVAL_PR_HEAD !== undefined
170
+ && String(env.FORGE_EVAL_PR_HEAD).toLowerCase() !== head))) {
171
+ throw new Error('attribution.pr_mismatch');
172
+ }
173
+ const pr = explicitPr
174
+ ? Number(env.FORGE_EVAL_PR)
175
+ : resolvePrFromGh(projectRoot, deps.execFileSync);
176
+ if (!pr) throw new Error('attribution.pr_unavailable');
177
+ let state;
178
+ try {
179
+ const adapter = deps.prAdapter || createPrAdapter(projectRoot, deps.execFileSync);
180
+ state = await adapter.readState(pr);
181
+ } catch {
182
+ throw new Error('attribution.pr_unavailable');
183
+ }
184
+ if (state?.state !== 'OPEN' || String(state?.headSha || '').toLowerCase() !== head) {
185
+ throw new Error('attribution.pr_mismatch');
186
+ }
187
+ return { issueId, pr };
188
+ }
189
+
190
+ function buildConfig({ tier, skillName, skillText, models, command, env }) {
191
+ const effort = parseEffort(env);
192
+ const budget = {
193
+ timeoutMs: Number(env.FORGE_EVAL_TIMEOUT_MS || 120000),
194
+ maxTokens: Number(env.FORGE_EVAL_MAX_TOKENS || 8192),
195
+ tokenEnforcement: 'parsed-usage',
196
+ };
197
+ if (!Number.isSafeInteger(budget.timeoutMs) || budget.timeoutMs <= 0
198
+ || !Number.isSafeInteger(budget.maxTokens) || budget.maxTokens <= 0) {
199
+ throw new Error('budget.invalid');
200
+ }
201
+ const skillHash = contentHash(skillText);
202
+ const budgetHash = contentHash(stableStringify(budget));
203
+ const budgetId = `tier-${tier}-${budgetHash.slice(0, 12)}`;
204
+ const config = {
205
+ schemaVersion: 1,
206
+ tier,
207
+ skillName,
208
+ skillHash,
209
+ models,
210
+ conditions: ['current', 'bounded'],
211
+ runtime: command,
212
+ runtimeControls: RUNTIME_CONTROLS,
213
+ effort,
214
+ budget,
215
+ };
216
+ const configHash = contentHash(stableStringify(config));
217
+ const arms = [];
218
+ for (const model of models) {
219
+ for (const condition of config.conditions) {
220
+ const id = `arm-${contentHash(stableStringify({ configHash, model, condition })).slice(0, 16)}`;
221
+ arms.push({ id, model, config: condition, budget: budgetId });
222
+ }
223
+ }
224
+ return {
225
+ arms, budget, budgetHash, configHash, effort,
226
+ runtimeControls: RUNTIME_CONTROLS, skillHash,
227
+ };
228
+ }
229
+
230
+ function probeRuntime(command, spawn = spawnSync) {
231
+ const result = spawn(command[0], [...command.slice(1), '--version'], {
232
+ encoding: 'utf8',
233
+ timeout: 30000,
234
+ windowsHide: true,
235
+ });
236
+ return result.status === 0;
237
+ }
238
+
239
+ function parseJsonResult(stdout) {
240
+ let raw;
241
+ try {
242
+ const transcript = parseTranscript(stdout);
243
+ const lastText = [...transcript.messages].reverse().find((message) => message.text)?.text;
244
+ raw = typeof transcript.result === 'string' ? transcript.result : lastText;
245
+ } catch {
246
+ throw new Error('runtime.output_unparseable');
247
+ }
248
+ if (typeof raw !== 'string') throw new Error('runtime.output_missing');
249
+ const fenced = /^\s*```(?:json)?\s*([\s\S]*?)\s*```\s*$/i.exec(raw);
250
+ let parsed;
251
+ try {
252
+ parsed = JSON.parse(fenced ? fenced[1] : raw);
253
+ } catch {
254
+ throw new Error('runtime.output_unparseable');
255
+ }
256
+ return parsed && typeof parsed === 'object' && Object.hasOwn(parsed, 'observation')
257
+ ? parsed.observation
258
+ : parsed;
259
+ }
260
+
261
+ function parseUsage(usage) {
262
+ if (!usage) return null;
263
+ const input = Number(usage.input_tokens);
264
+ const output = Number(usage.output_tokens);
265
+ const cached = usage.cache_read_input_tokens === undefined
266
+ ? 0 : Number(usage.cache_read_input_tokens);
267
+ return [input, output, cached].every((value) => Number.isFinite(value) && value >= 0)
268
+ ? { input, output, cached }
269
+ : null;
270
+ }
271
+
272
+ function parseUsageEvent(line) {
273
+ try {
274
+ const event = JSON.parse(line);
275
+ if (event?.type === 'result' && Object.hasOwn(event, 'usage')) {
276
+ return { kind: 'result', tokens: parseUsage(event.usage) };
277
+ }
278
+ const tokens = parseUsage(event?.message?.usage);
279
+ return tokens ? { kind: 'assistant', tokens } : null;
280
+ } catch {
281
+ return null;
282
+ }
283
+ }
284
+
285
+ function countTokens(stdout) {
286
+ const assistant = { input: 0, output: 0, cached: 0 };
287
+ let assistantFound = false;
288
+ let result;
289
+ for (const line of String(stdout || '').split('\n')) {
290
+ const usage = parseUsageEvent(line);
291
+ if (usage?.kind === 'result') result = usage;
292
+ if (usage?.kind !== 'assistant') continue;
293
+ assistantFound = true;
294
+ assistant.input += usage.tokens.input;
295
+ assistant.output += usage.tokens.output;
296
+ assistant.cached += usage.tokens.cached;
297
+ }
298
+ if (result) return result.tokens;
299
+ return assistantFound ? assistant : null;
300
+ }
301
+
302
+ function buildPrompt({ armId, packet, skillName, config, skillText }) {
303
+ const scenario = {
304
+ schemaVersion: packet.schemaVersion,
305
+ caseId: packet.caseId,
306
+ caseClass: packet.caseClass,
307
+ risk: packet.risk,
308
+ variant: packet.variant,
309
+ split: packet.split,
310
+ };
311
+ const contract = config === 'bounded' ? skillText : null;
312
+ return [
313
+ 'Return one JSON object only. Evaluate the frozen scenario without tools, mutations, or polling.',
314
+ `The observation object must contain exactly: ${OBSERVATION_KEYS.join(', ')}.`,
315
+ 'observer must contain exactly mutationCount and pollCount. Use "not-applicable" when a field does not apply.',
316
+ stableStringify({ armId, skillName, scenario, contract }),
317
+ ].join('\n');
318
+ }
319
+
320
+ function buildRuntimeArgv({ command, prompt, model, effort }) {
321
+ return [
322
+ ...command,
323
+ '-p', prompt,
324
+ '--output-format', 'stream-json',
325
+ '--verbose',
326
+ '--no-session-persistence',
327
+ '--safe-mode',
328
+ '--tools', '',
329
+ '--model', model,
330
+ '--effort', effort,
331
+ ];
332
+ }
333
+
334
+ function createExecutor({ projectRoot, skillName, skillText, command, budget, effort, skillHash, env }) {
335
+ const appliedEffort = effort || parseEffort(env);
336
+ return async (input) => {
337
+ const prompt = buildPrompt({ ...input, skillName, skillText });
338
+ const startedAt = new Date().toISOString();
339
+ const started = Date.now();
340
+ const cmd = buildRuntimeArgv({ command, prompt, model: input.model, effort: appliedEffort });
341
+ const execution = await executeCommand('behavioral-eval', prompt, projectRoot, budget.timeoutMs, cmd, env);
342
+ const ended = Date.now();
343
+ const endedAt = new Date().toISOString();
344
+ if (execution.timedOut || execution.exitCode !== 0) throw new Error('runtime.execution_failed');
345
+ const tokens = countTokens(execution.stdout);
346
+ if (!tokens) throw new Error('runtime.usage_unparseable');
347
+ if (tokens.input + tokens.output > budget.maxTokens) {
348
+ throw new Error('runtime.token_budget_exceeded');
349
+ }
350
+ const observation = parseJsonResult(execution.stdout);
351
+ const durationMs = ended - started;
352
+ return {
353
+ evidence: {
354
+ schemaVersion: 1,
355
+ caseId: input.packet.caseId,
356
+ packetHash: hashPacket(input.packet),
357
+ split: input.packet.split,
358
+ trialIndex: input.trialIndex,
359
+ binding: { ...input.binding },
360
+ observation,
361
+ metrics: { durationMs, tokensUsed: tokens.input + tokens.output },
362
+ },
363
+ attribution: {
364
+ model: input.model,
365
+ effort: appliedEffort,
366
+ role: 'behavioral-eval',
367
+ hashes: {
368
+ prompt: contentHash(prompt),
369
+ skill: skillHash,
370
+ tool: contentHash(stableStringify({
371
+ command, model: input.model, effort: appliedEffort,
372
+ runtimeControls: RUNTIME_CONTROLS,
373
+ })),
374
+ },
375
+ startedAt,
376
+ endedAt,
377
+ activeMs: durationMs,
378
+ passiveMs: 0,
379
+ tokens,
380
+ retries: 0,
381
+ compactions: 0,
382
+ },
383
+ };
384
+ };
385
+ }
386
+
387
+ function incompleteResult({ tier, arms = [], binding, issueId, pr, reason }) {
388
+ const expectedRuns = tier * 3 * 4;
389
+ return {
390
+ status: 'INCOMPLETE',
391
+ tier,
392
+ arms,
393
+ binding,
394
+ issueId,
395
+ pr,
396
+ expectedRuns,
397
+ completedRuns: 0,
398
+ passedRuns: 0,
399
+ failedRuns: 0,
400
+ incompleteRuns: expectedRuns,
401
+ findings: [{ status: 'INCOMPLETE', failures: [reason] }],
402
+ };
403
+ }
404
+
405
+ async function resolveBehavioralEvaluation({ projectRoot, skillName, skillPath, tier, env = process.env, deps = {} }) {
406
+ let arms = [];
407
+ let binding;
408
+ let issueId;
409
+ let pr;
410
+ try {
411
+ const head = resolveHead(projectRoot, deps.execFileSync);
412
+ const attribution = await resolveAttribution(projectRoot, head, env, deps);
413
+ issueId = attribution.issueId;
414
+ pr = attribution.pr;
415
+ const skillText = fs.readFileSync(skillPath || path.join(projectRoot, 'skills', skillName, 'SKILL.md'), 'utf8');
416
+ const command = runtimeCommand(env);
417
+ const models = parseModels(env);
418
+ const config = buildConfig({ tier, skillName, skillText, models, command, env });
419
+ arms = config.arms;
420
+ binding = { repoSha: head, configHash: config.configHash, budgetHash: config.budgetHash };
421
+ if (!probeRuntime(command, deps.spawnSync)) {
422
+ return { ok: false, result: incompleteResult({ tier, arms, binding, issueId, pr, reason: 'runtime.unavailable' }) };
423
+ }
424
+ return {
425
+ ok: true,
426
+ options: {
427
+ arms,
428
+ binding,
429
+ issueId,
430
+ pr,
431
+ executor: createExecutor({
432
+ projectRoot, skillName, skillText, command, budget: config.budget,
433
+ effort: config.effort, skillHash: config.skillHash, env,
434
+ }),
435
+ appendOptions: { env },
436
+ },
437
+ };
438
+ } catch (error) {
439
+ const candidate = typeof error?.message === 'string' ? error.message : '';
440
+ const reason = RUNTIME_REASON_CODES.has(candidate) ? candidate : 'runtime.config_invalid';
441
+ return {
442
+ ok: false,
443
+ result: incompleteResult({
444
+ tier, arms, binding, issueId, pr, reason,
445
+ }),
446
+ };
447
+ }
448
+ }
449
+
450
+ module.exports = {
451
+ resolveBehavioralEvaluation,
452
+ _internal: {
453
+ buildConfig, buildPrompt, buildRuntimeArgv, countTokens, createExecutor, parseEffort,
454
+ parseJsonResult, parseModels, resolveAttribution, runtimeCommand, RUNTIME_REASON_CODES,
455
+ },
456
+ };