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.
- package/AGENTS.md +14 -7
- package/CHANGELOG.md +43 -1
- package/README.md +6 -2
- package/bin/forge-cmd.js +21 -1
- package/bin/forge.js +16 -369
- package/docs/INDEX.md +1 -1
- package/docs/guides/BEADS_GITHUB_SYNC.md +2 -31
- package/docs/guides/MIGRATION.md +4 -4
- package/docs/guides/SETUP.md +16 -16
- package/docs/reference/COMMANDS.md +9 -4
- package/docs/reference/INSIGHTS_RECAP.md +9 -20
- package/docs/reference/RELEASE.md +5 -3
- package/docs/reference/TOOLCHAIN.md +8 -0
- package/docs/reference/protected-state-surfaces.md +4 -4
- package/docs/reference/shepherd.md +117 -17
- package/lefthook.yml +12 -0
- package/lib/activation/ensure-forge-home.js +33 -15
- package/lib/adapters/greptile-review-adapter.js +1 -1
- package/lib/adapters/pr-state-adapter.js +397 -100
- package/lib/agents-config.js +5 -0
- package/lib/audit-evidence.js +71 -110
- package/lib/capped-jsonl-log.js +236 -0
- package/lib/commands/_issue.js +31 -46
- package/lib/commands/_manifest.js +1 -1
- package/lib/commands/_registry.js +2 -2
- package/lib/commands/_resolve-command-opts.js +36 -29
- package/lib/commands/claim.js +2 -4
- package/lib/commands/clean.js +196 -32
- package/lib/commands/dev.js +4 -33
- package/lib/commands/hooks.js +358 -13
- package/lib/commands/insights.js +8 -3
- package/lib/commands/merge.js +600 -40
- package/lib/commands/plan.js +23 -115
- package/lib/commands/pr.js +1 -1
- package/lib/commands/preflight.js +11 -2
- package/lib/commands/prime.js +23 -3
- package/lib/commands/push.js +41 -51
- package/lib/commands/recall.js +60 -16
- package/lib/commands/recap.js +6 -1
- package/lib/commands/release.js +18 -4
- package/lib/commands/serve.js +5 -2
- package/lib/commands/setup.js +191 -95
- package/lib/commands/shepherd.js +49 -4
- package/lib/commands/ship.js +22 -23
- package/lib/commands/skill.js +383 -0
- package/lib/commands/status.js +54 -33
- package/lib/commands/test.js +56 -34
- package/lib/commands/worktree.js +247 -43
- package/lib/core/runtime-graph.js +89 -15
- package/lib/doc-assertions.js +297 -0
- package/lib/existing-tdd-gate.js +253 -0
- package/lib/forge-context.js +1 -4
- package/lib/forge-issues.js +64 -491
- package/lib/git-defaults.js +56 -0
- package/lib/harness-capability-matrix.js +5 -5
- package/lib/hook-renderer.js +147 -16
- package/lib/insights.js +96 -80
- package/lib/issue-backend.js +42 -3
- package/lib/kernel/backing-issue.js +14 -2
- package/lib/kernel/broker.js +44 -0
- package/lib/kernel/cli-broker-factory.js +12 -1
- package/lib/kernel/close-on-merge.js +154 -0
- package/lib/kernel/fs-class.js +42 -25
- package/lib/kernel/migrations.js +30 -2
- package/lib/kernel/schema.js +35 -0
- package/lib/kernel/sqlite-driver.js +292 -18
- package/lib/lefthook-wiring.js +21 -1
- package/lib/memory/router.js +16 -1
- package/lib/memory-digest.js +47 -15
- package/lib/memory-recall-events.js +145 -0
- package/lib/memory-recall.js +212 -0
- package/lib/merge-rules.js +8 -4
- package/lib/npm-publish-workflow.js +272 -0
- package/lib/orientation.js +371 -49
- package/lib/plugin-catalog.js +14 -4
- package/lib/pr-bundle.js +9 -6
- package/lib/pr-monitor/journal.js +18 -2
- package/lib/pr-monitor/reconcile-executor.js +842 -0
- package/lib/pr-monitor/reconcile-tick.js +138 -0
- package/lib/pr-monitor/reconcile.js +0 -0
- package/lib/pr-monitor/render-summary.js +196 -0
- package/lib/pr-monitor/shepherd-lease.js +252 -0
- package/lib/pr-monitor/watch-lifecycle.js +14 -2
- package/lib/pr-pull.js +98 -24
- package/lib/pr-shepherd.js +34 -8
- package/lib/preflight/gates.js +65 -18
- package/lib/preflight/runner.js +5 -0
- package/lib/project-memory.js +40 -0
- package/lib/protected-state-authority.js +305 -0
- package/lib/protected-state-surfaces.js +64 -44
- package/lib/release-readiness.js +51 -4
- package/lib/rules-sync.js +4 -0
- package/lib/runtime-health.js +15 -46
- package/lib/shell-utils.js +1 -1
- package/lib/skill-eval.js +750 -0
- package/lib/skills-sync.js +6 -3
- package/lib/smart-merge.js +28 -4
- package/lib/status/identity.js +46 -0
- package/lib/status/presenter.js +0 -35
- package/lib/status/snapshot.js +11 -16
- package/lib/symlink-utils.js +74 -26
- package/lib/upgrade-safety.js +47 -9
- package/lib/using-forge.js +328 -0
- package/lib/workflow/enforce-stage.js +5 -5
- package/lib/workflow/state-manager.js +23 -23
- package/package.json +6 -7
- package/rules/using-forge.md +24 -0
- package/scripts/doc-asserting-tests.js +158 -0
- package/scripts/forge-team/index.sh +0 -5
- package/scripts/forge-team/tests/dispatcher.test.sh +1 -1
- package/scripts/forge-team/tests/workflow-integration.test.sh +0 -1
- package/scripts/lib/behavioral-eval-runner.js +310 -0
- package/scripts/lib/behavioral-eval-runtime.js +456 -0
- package/scripts/lib/eval-evidence.js +328 -0
- package/scripts/lib/eval-runner.js +81 -41
- package/scripts/lib/immutable-eval-corpus.js +309 -0
- package/scripts/lib/promotion-evidence-loader.js +94 -0
- package/scripts/lib/promotion-scorecard.js +314 -0
- package/scripts/npm-release-receipt.js +134 -0
- package/scripts/process-tree.js +761 -0
- package/scripts/protected-state-check.js +47 -22
- package/scripts/run-command-eval.js +29 -1
- package/scripts/sync-d20-audit.js +172 -0
- package/scripts/test-full-suite.js +249 -37
- package/scripts/test.js +184 -44
- package/skills/claim-safety/SKILL.md +4 -0
- package/skills/claim-safety/evals/scorecard.json +41 -0
- package/skills/coverage.json +83 -0
- package/skills/dev/SKILL.md +4 -0
- package/skills/dev/evals/scorecard.json +41 -0
- package/skills/gates/SKILL.md +80 -0
- package/skills/gates/evals/evals.json +38 -0
- package/skills/gates/evals/scorecard.json +41 -0
- package/skills/hermes-forge/SKILL.md +1 -0
- package/skills/hermes-forge/evals/scorecard.json +41 -0
- package/skills/issue-basics/SKILL.md +1 -0
- package/skills/issue-basics/evals/scorecard.json +41 -0
- package/skills/kernel/SKILL.md +38 -0
- package/skills/kernel/evals/scorecard.json +41 -0
- package/skills/memory/SKILL.md +16 -1
- package/skills/memory/evals/scorecard.json +41 -0
- package/skills/parallel-deep-research/SKILL.md +1 -0
- package/skills/parallel-deep-research/evals/scorecard.json +41 -0
- package/skills/plan/SKILL.md +6 -0
- package/skills/plan/evals/scorecard.json +41 -0
- package/skills/portability/SKILL.md +47 -0
- package/skills/portability/evals/evals.json +34 -0
- package/skills/portability/evals/scorecard.json +41 -0
- package/skills/research/SKILL.md +1 -0
- package/skills/research/evals/scorecard.json +41 -0
- package/skills/review/SKILL.md +10 -11
- package/skills/review/evals/scorecard.json +41 -0
- package/skills/rollback/SKILL.md +5 -11
- package/skills/rollback/evals/scorecard.json +41 -0
- package/skills/setup/SKILL.md +91 -0
- package/skills/setup/evals/evals.json +42 -0
- package/skills/setup/evals/scorecard.json +41 -0
- package/skills/shepherd/SKILL.md +84 -38
- package/skills/shepherd/evals/evals.json +21 -9
- package/skills/shepherd/evals/scorecard.json +41 -0
- package/skills/ship/SKILL.md +10 -12
- package/skills/ship/evals/scorecard.json +41 -0
- package/skills/smith/SKILL.md +8 -0
- package/skills/smith/evals/scorecard.json +41 -0
- package/skills/sonarcloud/SKILL.md +1 -0
- package/skills/sonarcloud/evals/scorecard.json +41 -0
- package/skills/sonarcloud-analysis/SKILL.md +1 -0
- package/skills/sonarcloud-analysis/evals/scorecard.json +41 -0
- package/skills/status/SKILL.md +3 -0
- package/skills/status/evals/scorecard.json +41 -0
- package/skills/triage-ready/SKILL.md +2 -0
- package/skills/triage-ready/evals/scorecard.json +41 -0
- package/skills/using-forge/SKILL.md +104 -0
- package/skills/using-forge/evals/scorecard.json +41 -0
- package/skills/validate/SKILL.md +4 -0
- package/skills/validate/evals/scorecard.json +41 -0
- package/skills/verify/SKILL.md +4 -0
- package/skills/verify/evals/scorecard.json +41 -0
- package/skills/worktree/SKILL.md +92 -0
- package/skills/worktree/evals/evals.json +38 -0
- package/skills/worktree/evals/scorecard.json +41 -0
- package/lib/adapters/beads-issue-adapter.js +0 -127
- package/lib/beads-nudge.js +0 -91
- package/lib/beads-setup.js +0 -538
- package/lib/beads-sync-scaffold.js +0 -189
- package/lib/commands/board.js +0 -64
- package/lib/pat-setup.js +0 -207
- package/lib/pr-monitor/render-sticky.js +0 -192
- package/lib/pr-monitor/upsert-sticky.js +0 -169
- package/lib/status/beads-snapshot.js +0 -145
- package/scripts/beads-context.sh +0 -577
- package/scripts/beads-migrate-to-dolt.sh +0 -7
- package/scripts/beads-upgrade-smoke.sh +0 -284
- package/scripts/forge-team/lib/dashboard.sh +0 -316
- package/scripts/forge-team/tests/dashboard.test.sh +0 -155
- 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
|
+
};
|