@bongos/core 1.19.332 → 1.19.334
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/.bongos-core.json +173 -43
- package/docs/adr/0187-oauth-handshake-cookie-lifetime.md +84 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +2 -2
- package/docs/api-reference.md +1 -1
- package/docs/copy-inventory.md +202 -202
- package/docs/copy-registry.json +384 -384
- package/modules/grading/grader-config.js +98 -0
- package/modules/grading/grader-model.js +150 -0
- package/modules/grading/grader-prompt.js +139 -0
- package/modules/grading/grader-score.js +706 -0
- package/modules/grading/grader-subagent.js +328 -0
- package/modules/grading/grader.js +5 -1287
- package/modules/hall-ui/public/builders.js +19 -1182
- package/modules/hall-ui/public/goals-page.js +11 -997
- package/modules/hall-ui/public/goals-render.js +1032 -0
- package/modules/hall-ui/public/goals.html +4 -0
- package/modules/hall-ui/public/hall-render.js +1211 -0
- package/modules/hall-ui/public/index.html +6 -0
- package/modules/lifecycle/routes/goal-route-authz.js +400 -0
- package/modules/lifecycle/routes/goals.js +1 -367
- package/modules/lifecycle/routes/task-route-helpers.js +219 -0
- package/modules/lifecycle/routes/task-write-routes.js +542 -0
- package/modules/lifecycle/routes/tasks.js +3 -696
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/box-infra.js +280 -0
- package/scripts/gds/box.js +1 -245
- package/scripts/gds/fitness-checks-identity.js +320 -0
- package/scripts/gds/fitness-checks-vocabulary.js +230 -0
- package/scripts/gds/fitness-checks-write-validation.js +144 -0
- package/scripts/gds/fitness-lib.js +82 -0
- package/scripts/gds/fitness.js +55 -668
- package/scripts/gds/provision-config.js +204 -0
- package/scripts/gds/provision-net.js +221 -0
- package/scripts/gds/provision-repo.js +849 -0
- package/scripts/gds/provision-units.js +262 -0
- package/scripts/gds/provision.js +4 -1395
- package/src/bongos/auth-admission.js +264 -0
- package/src/bongos/auth-config.js +151 -0
- package/src/bongos/auth-discord.js +198 -0
- package/src/bongos/auth-github.js +336 -0
- package/src/bongos/auth-idp.js +330 -0
- package/src/bongos/auth.js +5 -1100
- package/src/bongos/routes/auth.js +54 -10
- package/src/module-api.js +5 -1
- package/tests/auth_admission_gate.mjs +3 -1
- package/tests/auth_web_recovery.mjs +63 -0
- package/tests/est_advisory.mjs +3 -1
- package/tests/goal_advisory.mjs +3 -2
- package/tests/goal_criterion_confirm_ui.mjs +6 -0
- package/tests/goal_subtitle.mjs +5 -2
- package/tests/goals_card_view.mjs +10 -3
- package/tests/grader_generated_provenance.mjs +3 -1
- package/tests/hall_palette.mjs +3 -1
- package/tests/helpers.mjs +58 -25
- package/tests/lib_sh_resolution.mjs +3 -1
- package/tests/membership_kind_lockstep.mjs +3 -1
- package/tests/migration_allocation.mjs +5 -2
- package/tests/patch_gate_ordering.mjs +3 -1
- package/tests/rank_tier_single_source.mjs +16 -3
- package/tests/reward_gate.mjs +3 -2
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
// modules/grading/grader-subagent.js — actually running a subagent: the spawn,
|
|
2
|
+
// the read-context bundle it is given, the cached and one-shot wrappers.
|
|
3
|
+
//
|
|
4
|
+
// Carved out of modules/grading/grader.js by task 1003206 (goal 1000079, the R03
|
|
5
|
+
// de-monolith plan) to bring the grader under the 1,500-line ratchet. A STRUCTURAL
|
|
6
|
+
// move: bodies unchanged, grader.js requires every name back, so the grade port
|
|
7
|
+
// surface and the panel's behaviour are exactly as before.
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const path = require('node:path');
|
|
12
|
+
const fs = require('node:fs');
|
|
13
|
+
const { spawn } = require('node:child_process');
|
|
14
|
+
const { DEFAULT_GRADER_MODEL, THRESHOLD, VERSION, api } = require('./grader-config.js');
|
|
15
|
+
const { validateAndScore } = require('./grader-score.js');
|
|
16
|
+
|
|
17
|
+
function runSubagent({ prompt, opts = {} }) {
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
const timeoutMs = opts.timeoutMs ?? 240_000;
|
|
20
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
21
|
+
const model = opts.model ?? DEFAULT_GRADER_MODEL;
|
|
22
|
+
// Env escape hatch (OTB_CLAUDE_BIN / CLAUDE_BIN) for installs where the CLI
|
|
23
|
+
// isn't named `claude` on PATH — covers any OS, not just Windows.
|
|
24
|
+
const bin = opts.claudeBin ?? process.env.OTB_CLAUDE_BIN ?? process.env.CLAUDE_BIN ?? 'claude';
|
|
25
|
+
|
|
26
|
+
const args = [
|
|
27
|
+
'-p',
|
|
28
|
+
'--print',
|
|
29
|
+
'--model', model,
|
|
30
|
+
'--output-format', 'json',
|
|
31
|
+
'--tools', 'Read',
|
|
32
|
+
'--disable-slash-commands',
|
|
33
|
+
'--permission-mode', 'bypassPermissions',
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
const startedAt = Date.now();
|
|
37
|
+
const child = spawn(bin, args, {
|
|
38
|
+
cwd,
|
|
39
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
40
|
+
// On Windows the `claude` CLI is a `claude.cmd` shim that Node's spawn
|
|
41
|
+
// can't launch without a shell (ENOENT -> SUBAGENT_SPAWN_FAILED, observed
|
|
42
|
+
// on #662). Route through cmd.exe on win32 only; Mac/Linux keep
|
|
43
|
+
// shell:false (the default, and the proven-working path). args carry no
|
|
44
|
+
// user input (the prompt goes in via stdin), so shell injection is a
|
|
45
|
+
// non-issue. windowsHide suppresses the transient console window.
|
|
46
|
+
shell: process.platform === 'win32',
|
|
47
|
+
windowsHide: true,
|
|
48
|
+
// MAS Phase 3 (#427): mark this child as a subagent so the Conductor
|
|
49
|
+
// UserPromptSubmit hook (.claude/hooks/conductor.js) bails instead of
|
|
50
|
+
// injecting advisory context into the grader worker's prompt. Any
|
|
51
|
+
// subagent we spawn inherits this flag and is likewise suppressed.
|
|
52
|
+
env: { ...process.env, OTB_SUBAGENT: '1' },
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
let stdout = '';
|
|
56
|
+
let stderr = '';
|
|
57
|
+
let timedOut = false;
|
|
58
|
+
|
|
59
|
+
const killTimer = setTimeout(() => {
|
|
60
|
+
timedOut = true;
|
|
61
|
+
try { child.kill('SIGTERM'); } catch (_) {}
|
|
62
|
+
setTimeout(() => { try { child.kill('SIGKILL'); } catch (_) {} }, 5_000);
|
|
63
|
+
}, timeoutMs);
|
|
64
|
+
|
|
65
|
+
child.stdout.on('data', (b) => { stdout += b.toString('utf8'); });
|
|
66
|
+
child.stderr.on('data', (b) => { stderr += b.toString('utf8'); });
|
|
67
|
+
|
|
68
|
+
child.on('error', (err) => {
|
|
69
|
+
clearTimeout(killTimer);
|
|
70
|
+
reject(Object.assign(new Error(`subagent spawn failed: ${err.message}`), {
|
|
71
|
+
code: 'SUBAGENT_SPAWN_FAILED',
|
|
72
|
+
cause: err,
|
|
73
|
+
}));
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
child.on('close', (exitCode) => {
|
|
77
|
+
clearTimeout(killTimer);
|
|
78
|
+
const runMs = Date.now() - startedAt;
|
|
79
|
+
if (timedOut) {
|
|
80
|
+
return reject(Object.assign(new Error(`subagent timed out after ${timeoutMs}ms`), {
|
|
81
|
+
code: 'SUBAGENT_TIMEOUT',
|
|
82
|
+
run_ms: runMs,
|
|
83
|
+
stdout,
|
|
84
|
+
stderr,
|
|
85
|
+
}));
|
|
86
|
+
}
|
|
87
|
+
if (exitCode !== 0) {
|
|
88
|
+
return reject(Object.assign(new Error(`subagent exited with code ${exitCode}`), {
|
|
89
|
+
code: 'SUBAGENT_NONZERO_EXIT',
|
|
90
|
+
exit_code: exitCode,
|
|
91
|
+
run_ms: runMs,
|
|
92
|
+
stdout,
|
|
93
|
+
stderr,
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
96
|
+
resolve({ stdout, stderr, run_ms: runMs, exit_code: exitCode, cost_usd: extractCostUsd(stdout) });
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
child.stdin.write(prompt, 'utf8');
|
|
100
|
+
child.stdin.end();
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ---------- task 1002660: shared context bundle ----------
|
|
105
|
+
//
|
|
106
|
+
// ~90% of panel cost was fixed re-exploration: four subprocesses each spending
|
|
107
|
+
// agentic Read turns re-opening the same changed files. Read them ONCE here
|
|
108
|
+
// and hand every worker the post-change contents inline (fenced, untrusted),
|
|
109
|
+
// so a worker's Read tool is only needed for files beyond the bundle.
|
|
110
|
+
const CONTEXT_BUNDLE_MAX_FILES = 24;
|
|
111
|
+
const CONTEXT_BUNDLE_MAX_TOTAL_CHARS = 120_000;
|
|
112
|
+
const CONTEXT_BUNDLE_MAX_FILE_CHARS = 16_000;
|
|
113
|
+
// Binary-ish extensions never worth inlining (the diff already names them).
|
|
114
|
+
const CONTEXT_BUNDLE_SKIP_RE = /\.(?:png|jpe?g|gif|webp|ico|svgz|woff2?|ttf|eot|zip|gz|tgz|tar|pdf|mp[34]|wav|ogg|bin|exe|dll|node)$/i;
|
|
115
|
+
|
|
116
|
+
// Added lines per file, from the unified diff. Only lines long enough to be a
|
|
117
|
+
// meaningful fingerprint (a bare `}` or `});` matches anywhere) — used to prove
|
|
118
|
+
// a truncated bundle prefix actually SHOWS the change (task 1002905).
|
|
119
|
+
const BUNDLE_EVIDENCE_MIN_CHARS = 12;
|
|
120
|
+
function diffAddedLinesByFile(diff) {
|
|
121
|
+
const byFile = new Map();
|
|
122
|
+
let cur = null;
|
|
123
|
+
let prevWasOldHeader = false;
|
|
124
|
+
for (const line of String(diff || '').split('\n')) {
|
|
125
|
+
// A '+++ ' header only counts right after its '--- ' partner: a content line
|
|
126
|
+
// reading '++ x' arrives as '+++ x' and must not be mistaken for a header.
|
|
127
|
+
if (prevWasOldHeader && line.startsWith('+++ ')) {
|
|
128
|
+
prevWasOldHeader = false;
|
|
129
|
+
const p = line.slice(4).trim().replace(/^b\//, '');
|
|
130
|
+
cur = p === '/dev/null' ? null : p;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
prevWasOldHeader = line.startsWith('--- ');
|
|
134
|
+
if (prevWasOldHeader || line.startsWith('diff --git')) continue;
|
|
135
|
+
if (!cur || !line.startsWith('+')) continue;
|
|
136
|
+
const t = line.slice(1).trim();
|
|
137
|
+
if (t.length < BUNDLE_EVIDENCE_MIN_CHARS) continue;
|
|
138
|
+
if (!byFile.has(cur)) byFile.set(cur, []);
|
|
139
|
+
byFile.get(cur).push(t);
|
|
140
|
+
}
|
|
141
|
+
return byFile;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Pure-ish (fs reads only, injectable): returns { files: [{file, content,
|
|
145
|
+
// truncated, chars_total}], skipped: [file...], total_chars } for the post-change
|
|
146
|
+
// contents of changedFiles under cwd. Deleted/unreadable files land in skipped —
|
|
147
|
+
// their absence from the bundle is visible, never silent.
|
|
148
|
+
//
|
|
149
|
+
// Security (panel hacker finding, task 1002660): changedFiles is builder-
|
|
150
|
+
// controlled and git commits symlinks, so a naive readFileSync would follow a
|
|
151
|
+
// committed link OUT of the repo (SSH keys, session tokens) and inject the
|
|
152
|
+
// target into worker prompts + the stored grade. lstat-refuse symlinks and
|
|
153
|
+
// non-files, and require the resolved path to stay under cwd (containment) —
|
|
154
|
+
// a refused entry lands in skipped, visible like every other exclusion.
|
|
155
|
+
function buildContextBundle({ changedFiles, cwd, diff, fsImpl = fs } = {}) {
|
|
156
|
+
const files = [];
|
|
157
|
+
const skipped = [];
|
|
158
|
+
let total = 0;
|
|
159
|
+
const root = path.resolve(cwd || process.cwd());
|
|
160
|
+
// task 1002905: the fabricated all-blocker FAIL. A file over the per-file cap
|
|
161
|
+
// is truncated from the HEAD, so on a large file (ship.js: 250k chars) a change
|
|
162
|
+
// at char 62k lands entirely outside the bundle — and the prompt presents what
|
|
163
|
+
// IS bundled as "the post-change contents". A worker then searches the prefix,
|
|
164
|
+
// doesn't find the new code, and reports it as absent (5 blockers, every claim
|
|
165
|
+
// exactly the pre-change state). A prefix that contains none of the change is
|
|
166
|
+
// worse than no bundle at all, so refuse it: skipped[] already tells the worker
|
|
167
|
+
// to Read the file.
|
|
168
|
+
const addedByFile = diffAddedLinesByFile(diff);
|
|
169
|
+
for (const rel of (Array.isArray(changedFiles) ? changedFiles : [])) {
|
|
170
|
+
if (files.length >= CONTEXT_BUNDLE_MAX_FILES || total >= CONTEXT_BUNDLE_MAX_TOTAL_CHARS) {
|
|
171
|
+
skipped.push(rel);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (CONTEXT_BUNDLE_SKIP_RE.test(rel)) { skipped.push(rel); continue; }
|
|
175
|
+
let content;
|
|
176
|
+
try {
|
|
177
|
+
const abs = path.resolve(root, rel);
|
|
178
|
+
if (abs !== root && !abs.startsWith(root + path.sep)) { skipped.push(rel); continue; }
|
|
179
|
+
const st = fsImpl.lstatSync(abs);
|
|
180
|
+
if (!st.isFile() || st.isSymbolicLink()) { skipped.push(rel); continue; }
|
|
181
|
+
content = fsImpl.readFileSync(abs, 'utf8');
|
|
182
|
+
} catch (_) { skipped.push(rel); continue; } // deleted by the diff, or unreadable
|
|
183
|
+
const charsTotal = content.length;
|
|
184
|
+
let truncated = false;
|
|
185
|
+
if (content.length > CONTEXT_BUNDLE_MAX_FILE_CHARS) {
|
|
186
|
+
content = content.slice(0, CONTEXT_BUNDLE_MAX_FILE_CHARS);
|
|
187
|
+
truncated = true;
|
|
188
|
+
}
|
|
189
|
+
if (truncated) {
|
|
190
|
+
const added = addedByFile.get(rel);
|
|
191
|
+
if (added && added.length && !added.some((a) => content.includes(a))) { skipped.push(rel); continue; }
|
|
192
|
+
}
|
|
193
|
+
if (total + content.length > CONTEXT_BUNDLE_MAX_TOTAL_CHARS) { skipped.push(rel); continue; }
|
|
194
|
+
total += content.length;
|
|
195
|
+
files.push({ file: rel, content, truncated, chars_total: charsTotal });
|
|
196
|
+
}
|
|
197
|
+
return { files, skipped, total_chars: total };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// System 5 (task 1359): an OPT-IN cached path around runSubagent. When the caller
|
|
201
|
+
// supplies a `cache` spec (from llm-cache.makeCacheSpec), a hit replays the stored
|
|
202
|
+
// result at cost_usd:0 (so $0 flows naturally into extractCostUsd → session-cost);
|
|
203
|
+
// a miss runs the untouched runSubagent and stores a clean (exit 0) result. With
|
|
204
|
+
// no cache spec it's exactly runSubagent — the default path is never altered. The
|
|
205
|
+
// llm-cache reaches us through the doorway (BV1.R73: it's shared infra core also
|
|
206
|
+
// uses, so a module can't own it); if it's somehow absent the path degrades to an
|
|
207
|
+
// ordinary uncached run, exactly as the old lazy-require catch did.
|
|
208
|
+
async function runSubagentCached({ prompt, opts = {}, cache = null }) {
|
|
209
|
+
if (!cache || !cache.key) return runSubagent({ prompt, opts });
|
|
210
|
+
const llmCache = api.llmCache;
|
|
211
|
+
if (!llmCache) return runSubagent({ prompt, opts });
|
|
212
|
+
return llmCache.withCache(cache, () => runSubagent({ prompt, opts }));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// #486 (ADR 0028 path 1): the `claude -p --output-format json` envelope reports
|
|
216
|
+
// the subprocess's own dollar cost in `total_cost_usd` (verified present:
|
|
217
|
+
// {type, ..., total_cost_usd, usage, modelUsage}). We were parsing only
|
|
218
|
+
// `env.result` (extractAssistantResult) and silently discarding that cost — the
|
|
219
|
+
// single biggest reason cost_log's `api` category sat at $0.60 across the whole
|
|
220
|
+
// project (#482 audit F1). Pull it out so the panel's spend can be logged
|
|
221
|
+
// (category=api, source=grader). Returns null on non-JSON / partial output, so
|
|
222
|
+
// a cost signal never fabricates a number it doesn't have.
|
|
223
|
+
function extractCostUsd(rawStdout) {
|
|
224
|
+
try {
|
|
225
|
+
const env = JSON.parse(rawStdout);
|
|
226
|
+
if (env && typeof env.total_cost_usd === 'number' && env.total_cost_usd >= 0) {
|
|
227
|
+
return env.total_cost_usd;
|
|
228
|
+
}
|
|
229
|
+
} catch (_) { /* non-JSON or partial stdout — no cost signal */ }
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function extractAssistantResult(rawStdout) {
|
|
234
|
+
try {
|
|
235
|
+
const env = JSON.parse(rawStdout);
|
|
236
|
+
if (env && typeof env.result === 'string') return env.result;
|
|
237
|
+
if (env && typeof env === 'object' && 'functional_fidelity' in env) return env;
|
|
238
|
+
return rawStdout;
|
|
239
|
+
} catch (_) {
|
|
240
|
+
return rawStdout;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Run one shot end-to-end: build the prompt (caller did this), spawn the
|
|
245
|
+
// subagent at the requested model, parse + validate. Returns a per-shot
|
|
246
|
+
// grade-result; subprocess failures become passed=false per-shot results so
|
|
247
|
+
// aggregateShots can still produce a final verdict.
|
|
248
|
+
async function runOneShot({ prompt, model, shotIndex, runOpts }) {
|
|
249
|
+
try {
|
|
250
|
+
const { stdout, run_ms, exit_code, cost_usd } = await runSubagent({
|
|
251
|
+
prompt,
|
|
252
|
+
opts: { ...runOpts, model },
|
|
253
|
+
});
|
|
254
|
+
const assistantResult = extractAssistantResult(stdout);
|
|
255
|
+
const scored = validateAndScore(assistantResult, {
|
|
256
|
+
run_ms,
|
|
257
|
+
exit_code,
|
|
258
|
+
raw_chars: stdout.length,
|
|
259
|
+
model,
|
|
260
|
+
shot_index: shotIndex,
|
|
261
|
+
});
|
|
262
|
+
// #486 (ADR 0028 path 1): carry this shot's own subprocess cost so the
|
|
263
|
+
// trivial-diff path (gradeSubagentV3MultiShot → aggregateShots) can sum it
|
|
264
|
+
// into panel_cost_usd just like the full panel does.
|
|
265
|
+
scored.cost_usd = cost_usd ?? null;
|
|
266
|
+
return scored;
|
|
267
|
+
} catch (err) {
|
|
268
|
+
return {
|
|
269
|
+
score: 0,
|
|
270
|
+
passed: false,
|
|
271
|
+
threshold: THRESHOLD,
|
|
272
|
+
grader_kind: 'subagent',
|
|
273
|
+
grader_version: VERSION,
|
|
274
|
+
rubric_scores: {},
|
|
275
|
+
signals: {
|
|
276
|
+
grader_kind: 'subagent',
|
|
277
|
+
grader_version: VERSION,
|
|
278
|
+
model,
|
|
279
|
+
threshold: THRESHOLD,
|
|
280
|
+
shot_index: shotIndex,
|
|
281
|
+
run_ms: err.run_ms ?? null,
|
|
282
|
+
exit_code: err.exit_code ?? null,
|
|
283
|
+
subprocess_error: err.code || 'UNKNOWN',
|
|
284
|
+
stderr_tail: (err.stderr || '').slice(-500),
|
|
285
|
+
},
|
|
286
|
+
notes_md: `Shot ${shotIndex}: subprocess failed (${err.code || 'UNKNOWN'}: ${err.message}).`,
|
|
287
|
+
issues: [],
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ---------- v4: panel aggregation (MAS Phase 2.0) ----------
|
|
293
|
+
|
|
294
|
+
// Collapses N per-worker WorkerResults into the final v4 grade-result.
|
|
295
|
+
//
|
|
296
|
+
// Gate logic:
|
|
297
|
+
// - Any worker `fail` → overall FAIL (a real finding blocks).
|
|
298
|
+
// - Else any worker errored → overall FALLTHROUGH (manual-confirm) —
|
|
299
|
+
// a crash is missing signal, not a
|
|
300
|
+
// finding, so it must not hard-block
|
|
301
|
+
// on infra flakiness (#126).
|
|
302
|
+
// - Else any worker `concerns` → overall FALLTHROUGH (manual-confirm).
|
|
303
|
+
// - All workers `pass` → overall PASS.
|
|
304
|
+
//
|
|
305
|
+
// Server-compat forwarding:
|
|
306
|
+
// The grade POST shape (ff, cq, economy, confidence, issues) is preserved
|
|
307
|
+
// so the server's existing validateAndScore re-validation aligns with the
|
|
308
|
+
// panel's verdict. We achieve this by setting the forwarded confidence:
|
|
309
|
+
// pass → median worker confidence (server's gate also passes)
|
|
310
|
+
// concerns → FALLTHROUGH_CONFIDENCE (below LOW_CONFIDENCE → server fails)
|
|
311
|
+
// fail → 0 + a failing axis preserved at < threshold (server fails)
|
|
312
|
+
//
|
|
313
|
+
// rubric_scores:
|
|
314
|
+
// Each axis is the median across workers that reported it (Narc reports
|
|
315
|
+
// functional_fidelity; Quality reports code_quality + economy). Preserves
|
|
316
|
+
// the trend dashboard's one-number-per-task contract.
|
|
317
|
+
//
|
|
318
|
+
// signals.workers[] carries the full per-worker audit trail.
|
|
319
|
+
|
|
320
|
+
module.exports = {
|
|
321
|
+
buildContextBundle,
|
|
322
|
+
diffAddedLinesByFile,
|
|
323
|
+
extractAssistantResult,
|
|
324
|
+
extractCostUsd,
|
|
325
|
+
runOneShot,
|
|
326
|
+
runSubagent,
|
|
327
|
+
runSubagentCached,
|
|
328
|
+
};
|