@stravica-ai/digital-operator-pa 0.1.54 → 0.1.56
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/dist/engineering-pa-cli/src/coordinator-hook/config.mjs +2 -2
- package/dist/engineering-pa-cli/src/environment/probes.mjs +1 -1
- package/dist/engineering-pa-cli/src/pa/cli.mjs +28 -0
- package/dist/engineering-pa-cli/src/pa/projects.mjs +7 -0
- package/dist/engineering-pa-cli/src/pa/work-cli.mjs +142 -0
- package/dist/engineering-pa-cli/src/pa/work.mjs +9 -0
- package/dist/engineering-pa-cli/src/research/engine/finalise.mjs +14 -4
- package/dist/engineering-pa-cli/src/research/workflows/research-deep.js +15 -5
- package/dist/engineering-pa-cli/src/research/workflows/research-normal.js +60 -14
- package/dist/engineering-pa-cli/src/research/wrappers/normal.mjs +46 -10
- package/dist/engineering-pa-core/docs/autonomy-envelope-tracker.md +22 -22
- package/dist/engineering-pa-core/docs/blueprints/git-identity/blueprint.md +21 -19
- package/dist/engineering-pa-core/docs/blueprints/scheduled-distillation/blueprint.md +2 -2
- package/dist/engineering-pa-core/docs/commands/init.md +1 -1
- package/dist/engineering-pa-core/docs/commands/onboard.md +6 -6
- package/dist/engineering-pa-core/docs/commands/research.md +66 -66
- package/dist/engineering-pa-core/docs/commands/upgrade.md +4 -4
- package/dist/engineering-pa-core/docs/entity-location-map.md +4 -4
- package/dist/engineering-pa-core/docs/lock.md +18 -18
- package/dist/engineering-pa-core/docs/memory-sync.md +1 -1
- package/dist/engineering-pa-core/docs/pa-operating-model.md +87 -87
- package/dist/engineering-pa-core/docs/scheduling.md +1 -1
- package/dist/engineering-pa-core/docs/standards/README.md +2 -2
- package/dist/engineering-pa-core/docs/standards/briefing-contract.md +18 -18
- package/dist/engineering-pa-core/docs/standards/tooling.md +41 -41
- package/dist/engineering-pa-core/docs/tools/distill.md +19 -19
- package/dist/engineering-pa-core/docs/tools/memory-sync.md +14 -14
- package/dist/engineering-pa-core/docs/tools/scheduler.md +15 -15
- package/dist/engineering-pa-core/docs/tools/slice.md +18 -18
- package/dist/engineering-pa-core/docs/upstream-issue-filing.md +6 -6
- package/dist/engineering-pa-core/docs/working-directory.md +3 -3
- package/dist/engineering-pa-core/docs/workspace-operations.md +11 -11
- package/dist/engineering-pa-core/package.json +3 -3
- package/dist/engineering-pa-core/roles/agentic-workflow-designer/definition.md +1 -1
- package/dist/engineering-pa-core/src/distill/health.mjs +16 -6
- package/dist/engineering-pa-core/src/instance-root/find.mjs +14 -3
- package/dist/engineering-pa-core/src/memory-sync/repo.mjs +2 -2
- package/dist/engineering-pa-core/src/scheduler/install.mjs +8 -7
- package/dist/engineering-pa-core/src/scheduler/list.mjs +2 -2
- package/dist/engineering-pa-core/src/scheduler/remove.mjs +65 -11
- package/dist/engineering-pa-core/src/scheduler/verify.mjs +2 -2
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/mirror-folder.mjs +3 -2
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/link.mjs +2 -1
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/sync-statuses.mjs +5 -1
- package/dist/engineering-pa-library-cli/package.json +1 -0
- package/dist/engineering-pa-library-cli/src/pull-clones.mjs +12 -3
- package/dist/engineering-pa-library-tunnel/package.json +3 -0
- package/dist/engineering-pa-library-tunnel/src/config.mjs +2 -2
- package/dist/engineering-pa-migrate-inspect/package.json +4 -1
- package/dist/engineering-pa-migrate-inspect/src/age-key-migrate.mjs +2 -2
- package/dist/engineering-pa-migrate-inspect/src/inspector.mjs +2 -2
- package/dist/engineering-pa-migrate-inspect/src/platforms/macos.mjs +2 -1
- package/dist/engineering-pa-scaffold/src/commands/init-workspace-extension.mjs +8 -0
- package/dist/engineering-pa-workspace/app/assets/brand/icons/app-icon-192.png +0 -0
- package/dist/engineering-pa-workspace/app/assets/brand/icons/app-icon-512.png +0 -0
- package/dist/engineering-pa-workspace/app/assets/brand/icons/apple-touch-icon.png +0 -0
- package/dist/engineering-pa-workspace/app/assets/brand/icons/favicon-16.png +0 -0
- package/dist/engineering-pa-workspace/app/assets/brand/icons/favicon-32.png +0 -0
- package/dist/engineering-pa-workspace/app/assets/brand/icons/favicon-48.png +0 -0
- package/dist/engineering-pa-workspace/app/assets/brand/icons/favicon.ico +0 -0
- package/dist/engineering-pa-workspace/app/assets/brand/icons/favicon.svg +1 -0
- package/dist/engineering-pa-workspace/app/assets/brand/icons/maskable-icon-512.png +0 -0
- package/dist/engineering-pa-workspace/app/assets/brand/icons/maskable-icon.svg +1 -0
- package/dist/engineering-pa-workspace/app/assets/brand/icons/safari-pinned-tab.svg +1 -0
- package/dist/engineering-pa-workspace/app/assets/brand/stravica-logo-dark.svg +1 -0
- package/dist/engineering-pa-workspace/app/assets/brand/stravica-logo-light.svg +1 -0
- package/dist/engineering-pa-workspace/app/assets/brand/stravica-mark.svg +1 -0
- package/dist/engineering-pa-workspace/app/assets/modal.js +17 -17
- package/dist/engineering-pa-workspace/app/assets/theme.css +332 -99
- package/dist/engineering-pa-workspace/app/assets/theme.js +259 -107
- package/dist/engineering-pa-workspace/app/banner.js +16 -12
- package/dist/engineering-pa-workspace/app/delegations.html +3 -3
- package/dist/engineering-pa-workspace/app/home.html +2 -2
- package/dist/engineering-pa-workspace/app/index.html +6 -3
- package/dist/engineering-pa-workspace/app/knowledge-map.html +2 -2
- package/dist/engineering-pa-workspace/app/knowledge.html +6 -6
- package/dist/engineering-pa-workspace/app/notes.html +2 -2
- package/dist/engineering-pa-workspace/app/onboarding.html +12 -11
- package/dist/engineering-pa-workspace/app/profile.html +4 -4
- package/dist/engineering-pa-workspace/app/project-detail.html +25 -18
- package/dist/engineering-pa-workspace/app/projects.html +4 -4
- package/dist/engineering-pa-workspace/app/repos.html +15 -14
- package/dist/engineering-pa-workspace/app/review-annotator.js +38 -38
- package/dist/engineering-pa-workspace/app/reviews.html +7 -7
- package/dist/engineering-pa-workspace/app/schedules.html +11 -11
- package/dist/engineering-pa-workspace/app/secrets.html +6 -6
- package/dist/engineering-pa-workspace/app/staging.html +15 -15
- package/dist/engineering-pa-workspace/app/tasks.html +4 -4
- package/dist/engineering-pa-workspace/app/viewer.html +14 -9
- package/dist/engineering-pa-workspace/index.mjs +2 -0
- package/dist/engineering-pa-workspace/src/install-daemon.mjs +6 -5
- package/dist/engineering-pa-workspace/src/review/api-handler.mjs +53 -2
- package/dist/engineering-pa-workspace/src/server.mjs +6 -6
- package/dist/engineering-pa-workspace/src/supervisor.mjs +5 -4
- package/dist/engineering-pa-workspace/src/workspace-page-contract.mjs +47 -19
- package/docs/onboarding-solo.md +1 -0
- package/package.json +5 -4
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
import { readFile } from 'node:fs/promises';
|
|
21
21
|
import { dirname, join, resolve } from 'node:path';
|
|
22
22
|
import { fileURLToPath } from 'node:url';
|
|
23
|
-
import {
|
|
23
|
+
import { resolveHome } from '../../../engineering-pa-core/src/home/resolve.mjs';
|
|
24
24
|
|
|
25
25
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
26
26
|
const CORE_CONFIG_PATH = resolve(__dirname, '..', '..', 'config', 'coordinator-allowlist.json');
|
|
@@ -98,7 +98,7 @@ export function mergeOverlay(config, overlay, warnings = []) {
|
|
|
98
98
|
* lives at `.claude/coordinator-overlay.json` under that directory.
|
|
99
99
|
*/
|
|
100
100
|
export function overlayPaths({ repoRoot, cwd, env = process.env }) {
|
|
101
|
-
const home = env
|
|
101
|
+
const home = resolveHome({ env });
|
|
102
102
|
const workspaceBase = repoRoot || cwd;
|
|
103
103
|
return {
|
|
104
104
|
home: home ? join(home, '.claude', 'coordinator-overlay.json') : null,
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { execFile } from 'node:child_process';
|
|
7
7
|
import { existsSync, readdirSync, statSync, readFileSync } from 'node:fs';
|
|
8
8
|
import { promisify } from 'node:util';
|
|
9
|
-
import { arch, platform, release
|
|
9
|
+
import { arch, platform, release } from 'node:os';
|
|
10
10
|
import { basename, join, resolve, dirname } from 'node:path';
|
|
11
11
|
import { resolve4 } from 'node:dns/promises';
|
|
12
12
|
import { fileURLToPath } from 'node:url';
|
|
@@ -67,6 +67,34 @@ export async function main(argv) {
|
|
|
67
67
|
return 0;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
// 0.1.55 item 4 (w-2026-09-21-dave-004): `pa --version` / `pa -v`
|
|
71
|
+
// prints the installed package version. Before this landing there
|
|
72
|
+
// was no way to ask an installed CLI which version it was, so the
|
|
73
|
+
// 0.1.54 sweep had to `cat node_modules/.../package.json` by hand.
|
|
74
|
+
if (argv[0] === '--version' || argv[0] === '-v') {
|
|
75
|
+
const { readFile } = await import('node:fs/promises');
|
|
76
|
+
const { fileURLToPath } = await import('node:url');
|
|
77
|
+
const { dirname: dn, join: jn } = await import('node:path');
|
|
78
|
+
try {
|
|
79
|
+
// packages/cli/src/pa/cli.mjs -> ../../..(three-up)/package.json
|
|
80
|
+
// works in-repo. The installed tarball layout resolves the
|
|
81
|
+
// package.json via the exports map added in this same release.
|
|
82
|
+
let pkgJson;
|
|
83
|
+
try {
|
|
84
|
+
pkgJson = await import('@stravica-ai/digital-operator-pa/package.json', { with: { type: 'json' } });
|
|
85
|
+
} catch {
|
|
86
|
+
const here = dn(fileURLToPath(import.meta.url));
|
|
87
|
+
const local = jn(here, '..', '..', '..', 'engineering-pa', 'package.json');
|
|
88
|
+
pkgJson = { default: JSON.parse(await readFile(local, 'utf8')) };
|
|
89
|
+
}
|
|
90
|
+
process.stdout.write((pkgJson.default?.version || 'unknown') + '\n');
|
|
91
|
+
return 0;
|
|
92
|
+
} catch (e) {
|
|
93
|
+
process.stderr.write(`[pa] failed to resolve version: ${e?.message || e}\n`);
|
|
94
|
+
return 1;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
70
98
|
const verb = argv[0];
|
|
71
99
|
const rest = argv.slice(1);
|
|
72
100
|
|
|
@@ -17,6 +17,7 @@ import { writeFile } from 'node:fs/promises';
|
|
|
17
17
|
import { join } from 'node:path';
|
|
18
18
|
|
|
19
19
|
import { getRepoRoot } from './repo-root.mjs';
|
|
20
|
+
import { findRepoRootSync } from '../../../engineering-pa-core/src/instance-root/find.mjs';
|
|
20
21
|
import { readJson, writeJson, ensureDir, listSubdirs, today } from './io.mjs';
|
|
21
22
|
import { addDoc, removeDocByPath, setDocStatus, DEFAULT_GROUP, VALID_DOC_STATUSES } from './doc-groups.mjs';
|
|
22
23
|
import { normaliseDocPath } from './doc-path.mjs';
|
|
@@ -111,6 +112,12 @@ export async function createProject(opts) {
|
|
|
111
112
|
validateCategory(opts.category);
|
|
112
113
|
validatePriority(opts.priority);
|
|
113
114
|
|
|
115
|
+
// 0.1.55 F1: refuse a project create from outside any operator instance
|
|
116
|
+
// with the strict findRepoRootSync error (exit 2, message naming the
|
|
117
|
+
// start directory and the markers walked). Keeps the failure shape
|
|
118
|
+
// aligned with pa work create and the scaffold write paths.
|
|
119
|
+
findRepoRootSync({ strict: true });
|
|
120
|
+
|
|
114
121
|
const repoRoot = await getRepoRoot();
|
|
115
122
|
const projDir = join(repoRoot, 'projects', opts.id);
|
|
116
123
|
const projectJsonPath = join(projDir, 'project.json');
|
|
@@ -19,10 +19,14 @@
|
|
|
19
19
|
// fall-back-to-default case. Full rationale + tests in
|
|
20
20
|
// `@stravica-ai/engineering-pa-core/src/workItemId/index.mjs`.
|
|
21
21
|
|
|
22
|
+
import { existsSync } from 'node:fs';
|
|
23
|
+
import { join } from 'node:path';
|
|
24
|
+
|
|
22
25
|
import { readPreferences } from '../../../engineering-pa-core/src/preferences.mjs';
|
|
23
26
|
|
|
24
27
|
import { createWork, setWorkStatus, listWork } from './work.mjs';
|
|
25
28
|
import { getRepoRoot } from './repo-root.mjs';
|
|
29
|
+
import { listSubdirs } from './io.mjs';
|
|
26
30
|
|
|
27
31
|
const USAGE = `pa work — manage operator-repo work items
|
|
28
32
|
|
|
@@ -126,8 +130,49 @@ async function readPreferencesPaName() {
|
|
|
126
130
|
}
|
|
127
131
|
}
|
|
128
132
|
|
|
133
|
+
// 0.1.55 item 4 (w-2026-09-21-dave-004): refuse unknown flags loudly.
|
|
134
|
+
// The pre-0.1.55 parser silently dropped anything not consumed downstream,
|
|
135
|
+
// so a typo like `--status` (only valid on `work status`) or an assumed
|
|
136
|
+
// `--description` (there is no such flag; the field is `summary`) went
|
|
137
|
+
// unnoticed and the created record was missing the value the operator
|
|
138
|
+
// meant to set. Recorded as an incident in w-2026-09-21-dave-002's
|
|
139
|
+
// summary.
|
|
140
|
+
const CREATE_ALLOWED_FLAGS = new Set([
|
|
141
|
+
'project',
|
|
142
|
+
'title',
|
|
143
|
+
'category',
|
|
144
|
+
'priority',
|
|
145
|
+
'summary',
|
|
146
|
+
'owner',
|
|
147
|
+
'assignee',
|
|
148
|
+
'author',
|
|
149
|
+
]);
|
|
150
|
+
|
|
129
151
|
async function doCreate(args) {
|
|
130
152
|
const { flags } = parseFlags(args);
|
|
153
|
+
const unknown = Object.keys(flags).filter((k) => !CREATE_ALLOWED_FLAGS.has(k));
|
|
154
|
+
if (unknown.length > 0) {
|
|
155
|
+
const list = unknown.map((k) => `--${k}`).join(', ');
|
|
156
|
+
const known = Array.from(CREATE_ALLOWED_FLAGS).map((k) => `--${k}`).sort().join(', ');
|
|
157
|
+
process.stderr.write(`[pa work create] unknown flag(s): ${list}\n`);
|
|
158
|
+
process.stderr.write(` accepted flags: ${known}\n`);
|
|
159
|
+
return 1;
|
|
160
|
+
}
|
|
161
|
+
// Project existence gate (w-2026-09-30-dave-005 estate chore). Before this gate,
|
|
162
|
+
// `pa work create --project <nonexistent>` silently minted a work item pointing at
|
|
163
|
+
// a project that did not exist; the mistake surfaced later as an orphan in listings.
|
|
164
|
+
// Refuse loudly with the nearest matches so a typo is a fast fix, not a silent
|
|
165
|
+
// corruption. Only the CLI enforces this — programmatic callers of createWork()
|
|
166
|
+
// stay permissive (test fixtures use --project 'proj-a' without a real project on
|
|
167
|
+
// disk); wider grep found no CLI caller depending on the old behaviour, so no
|
|
168
|
+
// escape hatch is exposed.
|
|
169
|
+
if (typeof flags.project === 'string') {
|
|
170
|
+
const projectErr = await validateProjectExists(flags.project);
|
|
171
|
+
if (projectErr) {
|
|
172
|
+
process.stderr.write(projectErr);
|
|
173
|
+
return 1;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
131
176
|
const priority = flags.priority != null && flags.priority !== true
|
|
132
177
|
? Number(flags.priority)
|
|
133
178
|
: null;
|
|
@@ -200,3 +245,100 @@ async function doList(args) {
|
|
|
200
245
|
for (const r of rows) process.stdout.write(cols.map((c) => pad(r[c], widths[c])).join(' ') + '\n');
|
|
201
246
|
return 0;
|
|
202
247
|
}
|
|
248
|
+
|
|
249
|
+
// ── project-existence validation (w-2026-09-30-dave-005 estate chore) ──────────
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Levenshtein distance between two short strings. Used to rank nearest project ids
|
|
253
|
+
* for a typo; not exported.
|
|
254
|
+
*/
|
|
255
|
+
function levenshtein(a, b) {
|
|
256
|
+
if (a === b) return 0;
|
|
257
|
+
if (!a.length) return b.length;
|
|
258
|
+
if (!b.length) return a.length;
|
|
259
|
+
const prev = new Array(b.length + 1);
|
|
260
|
+
const curr = new Array(b.length + 1);
|
|
261
|
+
for (let j = 0; j <= b.length; j += 1) prev[j] = j;
|
|
262
|
+
for (let i = 1; i <= a.length; i += 1) {
|
|
263
|
+
curr[0] = i;
|
|
264
|
+
for (let j = 1; j <= b.length; j += 1) {
|
|
265
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
266
|
+
curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
|
|
267
|
+
}
|
|
268
|
+
for (let j = 0; j <= b.length; j += 1) prev[j] = curr[j];
|
|
269
|
+
}
|
|
270
|
+
return prev[b.length];
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* List every projects/<id>/ whose project.json exists on disk. Names only, sorted
|
|
275
|
+
* lexicographically. Empty when the projects/ dir is absent (accepted by listSubdirs).
|
|
276
|
+
*/
|
|
277
|
+
async function listExistingProjectIds() {
|
|
278
|
+
const repoRoot = await getRepoRoot();
|
|
279
|
+
const projectsDir = join(repoRoot, 'projects');
|
|
280
|
+
const subdirs = await listSubdirs(projectsDir);
|
|
281
|
+
const ids = [];
|
|
282
|
+
for (const name of subdirs) {
|
|
283
|
+
if (existsSync(join(projectsDir, name, 'project.json'))) ids.push(name);
|
|
284
|
+
}
|
|
285
|
+
ids.sort();
|
|
286
|
+
return ids;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Nearest candidates for a missing project id: substring matches first (both
|
|
291
|
+
* directions), then the closest by Levenshtein distance (up to length/2 + 1). At
|
|
292
|
+
* most 5 suggestions; sorted deterministically.
|
|
293
|
+
*/
|
|
294
|
+
function nearestProjectMatches(target, candidates, max = 5) {
|
|
295
|
+
const seen = new Set();
|
|
296
|
+
const out = [];
|
|
297
|
+
const push = (id) => { if (!seen.has(id)) { seen.add(id); out.push(id); } };
|
|
298
|
+
for (const id of candidates) {
|
|
299
|
+
if (id.toLowerCase().includes(target.toLowerCase()) || target.toLowerCase().includes(id.toLowerCase())) {
|
|
300
|
+
push(id);
|
|
301
|
+
}
|
|
302
|
+
if (out.length >= max) break;
|
|
303
|
+
}
|
|
304
|
+
if (out.length < max) {
|
|
305
|
+
const threshold = Math.max(1, Math.floor(target.length / 2) + 1);
|
|
306
|
+
const scored = candidates
|
|
307
|
+
.filter((id) => !seen.has(id))
|
|
308
|
+
.map((id) => ({ id, d: levenshtein(target.toLowerCase(), id.toLowerCase()) }))
|
|
309
|
+
.filter((r) => r.d <= threshold)
|
|
310
|
+
.sort((a, b) => a.d - b.d || a.id.localeCompare(b.id));
|
|
311
|
+
for (const { id } of scored) {
|
|
312
|
+
push(id);
|
|
313
|
+
if (out.length >= max) break;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return out;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Returns null when the project id resolves to an existing project.json on disk, or
|
|
321
|
+
* a preformatted stderr message (with trailing newline) when it does not. Split so
|
|
322
|
+
* doCreate can print + return exit 1 without repeating the plumbing.
|
|
323
|
+
*/
|
|
324
|
+
async function validateProjectExists(projectId) {
|
|
325
|
+
const repoRoot = await getRepoRoot();
|
|
326
|
+
const path = join(repoRoot, 'projects', projectId, 'project.json');
|
|
327
|
+
if (existsSync(path)) return null;
|
|
328
|
+
let candidates = [];
|
|
329
|
+
try { candidates = await listExistingProjectIds(); } catch { candidates = []; }
|
|
330
|
+
const nearest = nearestProjectMatches(projectId, candidates);
|
|
331
|
+
const lines = [`[pa work create] unknown project: ${projectId}`];
|
|
332
|
+
lines.push(` expected projects/${projectId}/project.json on disk (not found)`);
|
|
333
|
+
if (nearest.length) {
|
|
334
|
+
lines.push(` did you mean: ${nearest.join(', ')}`);
|
|
335
|
+
} else if (candidates.length) {
|
|
336
|
+
lines.push(` existing projects: ${candidates.slice(0, 10).join(', ')}${candidates.length > 10 ? ', ...' : ''}`);
|
|
337
|
+
} else {
|
|
338
|
+
lines.push(' no projects on disk yet — create one with `pa project create` first');
|
|
339
|
+
}
|
|
340
|
+
return lines.map((l) => `${l}\n`).join('');
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Exported for tests only — pure helpers, no filesystem side-effects.
|
|
344
|
+
export const __testables = { levenshtein, nearestProjectMatches };
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
maxNForAuthorOnDate,
|
|
26
26
|
} from '../../../engineering-pa-core/src/workItemId/index.mjs';
|
|
27
27
|
|
|
28
|
+
import { findRepoRootSync } from '../../../engineering-pa-core/src/instance-root/find.mjs';
|
|
28
29
|
import { getRepoRoot } from './repo-root.mjs';
|
|
29
30
|
import {
|
|
30
31
|
readJson,
|
|
@@ -128,6 +129,14 @@ export async function createWork(opts) {
|
|
|
128
129
|
validateCategory(opts.category);
|
|
129
130
|
validatePriority(opts.priority);
|
|
130
131
|
|
|
132
|
+
// 0.1.55 F1: refuse a mint from outside any operator instance BEFORE the
|
|
133
|
+
// author-token check. Pre-fix the flow bailed with the author-token error
|
|
134
|
+
// (exit 1), which read as a config problem in the caller's terminal; the
|
|
135
|
+
// real cause is that the cwd carries none of the instance markers. Strict
|
|
136
|
+
// findRepoRootSync throws with exit 2 and names both the start directory
|
|
137
|
+
// and the markers walked, matching the sibling rule in projects.mjs.
|
|
138
|
+
findRepoRootSync({ strict: true });
|
|
139
|
+
|
|
131
140
|
let author;
|
|
132
141
|
try {
|
|
133
142
|
author = resolveAuthorToken({
|
|
@@ -134,9 +134,13 @@ export async function degradedSynthAgent(ctx, sq, reason) {
|
|
|
134
134
|
|
|
135
135
|
// Pure coverage computation. Given the planner's sub-question ids and the synth
|
|
136
136
|
// receipts, decide which SQs have a finding (any receipt with written=true), which ran
|
|
137
|
-
// as a degraded write (empty finding,
|
|
138
|
-
// uncovered (no receipt at all
|
|
139
|
-
//
|
|
137
|
+
// as a degraded write (empty finding, covered on disk but zero surviving claims), and
|
|
138
|
+
// which are wholly uncovered (no receipt at all). `complete` is true only when every
|
|
139
|
+
// planned SQ has a receipt AND every receipt carries at least one claim — a degraded
|
|
140
|
+
// zero-claim finding is a coverage gap the wrapper must surface (status='partial'),
|
|
141
|
+
// not silently mark done (w-2026-09-30-dave-005). The wrapper still distinguishes the
|
|
142
|
+
// two failure modes: uncovered aborts finalise; degraded lets the answer.md be
|
|
143
|
+
// written and returns status='partial' with the degraded[] list.
|
|
140
144
|
export function coverageReport(subQuestionIds, synthReceipts) {
|
|
141
145
|
const receiptsBySq = new Map();
|
|
142
146
|
for (const r of synthReceipts || []) {
|
|
@@ -152,7 +156,13 @@ export function coverageReport(subQuestionIds, synthReceipts) {
|
|
|
152
156
|
covered.push(id);
|
|
153
157
|
if ((r.claimCount || 0) === 0) degraded.push(id);
|
|
154
158
|
}
|
|
155
|
-
return {
|
|
159
|
+
return {
|
|
160
|
+
planned,
|
|
161
|
+
covered,
|
|
162
|
+
degraded,
|
|
163
|
+
uncovered,
|
|
164
|
+
complete: uncovered.length === 0 && degraded.length === 0,
|
|
165
|
+
};
|
|
156
166
|
}
|
|
157
167
|
|
|
158
168
|
// -- Finalise (persist gate records, write sources.json + the session answer, round-trip, mark done) --
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// GENERATED by assemble.mjs — DO NOT EDIT. Edit wrappers/deep.mjs + engine/*.mjs and re-run `pnpm assemble`.
|
|
2
2
|
// sources: wrappers/deep.mjs, engine/io.mjs, engine/plan.mjs, engine/agents.mjs, engine/finalise.mjs, engine/rank.mjs, engine/verify.mjs, engine/deepen.mjs, engine/deep.mjs
|
|
3
|
-
// contentHash: sha256:
|
|
3
|
+
// contentHash: sha256:b7c50f3547c79b4c63bb54f14d1c0675a9af3a1c871d7866050cd22ff3d28fb5
|
|
4
4
|
|
|
5
5
|
export const meta = {
|
|
6
6
|
name: 'research-deep',
|
|
@@ -511,9 +511,13 @@ async function degradedSynthAgent(ctx, sq, reason) {
|
|
|
511
511
|
|
|
512
512
|
// Pure coverage computation. Given the planner's sub-question ids and the synth
|
|
513
513
|
// receipts, decide which SQs have a finding (any receipt with written=true), which ran
|
|
514
|
-
// as a degraded write (empty finding,
|
|
515
|
-
// uncovered (no receipt at all
|
|
516
|
-
//
|
|
514
|
+
// as a degraded write (empty finding, covered on disk but zero surviving claims), and
|
|
515
|
+
// which are wholly uncovered (no receipt at all). `complete` is true only when every
|
|
516
|
+
// planned SQ has a receipt AND every receipt carries at least one claim — a degraded
|
|
517
|
+
// zero-claim finding is a coverage gap the wrapper must surface (status='partial'),
|
|
518
|
+
// not silently mark done (w-2026-09-30-dave-005). The wrapper still distinguishes the
|
|
519
|
+
// two failure modes: uncovered aborts finalise; degraded lets the answer.md be
|
|
520
|
+
// written and returns status='partial' with the degraded[] list.
|
|
517
521
|
function coverageReport(subQuestionIds, synthReceipts) {
|
|
518
522
|
const receiptsBySq = new Map();
|
|
519
523
|
for (const r of synthReceipts || []) {
|
|
@@ -529,7 +533,13 @@ function coverageReport(subQuestionIds, synthReceipts) {
|
|
|
529
533
|
covered.push(id);
|
|
530
534
|
if ((r.claimCount || 0) === 0) degraded.push(id);
|
|
531
535
|
}
|
|
532
|
-
return {
|
|
536
|
+
return {
|
|
537
|
+
planned,
|
|
538
|
+
covered,
|
|
539
|
+
degraded,
|
|
540
|
+
uncovered,
|
|
541
|
+
complete: uncovered.length === 0 && degraded.length === 0,
|
|
542
|
+
};
|
|
533
543
|
}
|
|
534
544
|
|
|
535
545
|
// -- Finalise (persist gate records, write sources.json + the session answer, round-trip, mark done) --
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// GENERATED by assemble.mjs — DO NOT EDIT. Edit wrappers/normal.mjs + engine/*.mjs and re-run `pnpm assemble`.
|
|
2
2
|
// sources: wrappers/normal.mjs, engine/io.mjs, engine/plan.mjs, engine/agents.mjs, engine/finalise.mjs, engine/rank.mjs, engine/verify.mjs, engine/deepen.mjs
|
|
3
|
-
// contentHash: sha256:
|
|
3
|
+
// contentHash: sha256:9ea786a5296dd54d14bbc662d1d47a87f437829fc5d394c3a83dd64f65ff1236
|
|
4
4
|
|
|
5
5
|
export const meta = {
|
|
6
6
|
name: 'research-normal',
|
|
@@ -510,9 +510,13 @@ async function degradedSynthAgent(ctx, sq, reason) {
|
|
|
510
510
|
|
|
511
511
|
// Pure coverage computation. Given the planner's sub-question ids and the synth
|
|
512
512
|
// receipts, decide which SQs have a finding (any receipt with written=true), which ran
|
|
513
|
-
// as a degraded write (empty finding,
|
|
514
|
-
// uncovered (no receipt at all
|
|
515
|
-
//
|
|
513
|
+
// as a degraded write (empty finding, covered on disk but zero surviving claims), and
|
|
514
|
+
// which are wholly uncovered (no receipt at all). `complete` is true only when every
|
|
515
|
+
// planned SQ has a receipt AND every receipt carries at least one claim — a degraded
|
|
516
|
+
// zero-claim finding is a coverage gap the wrapper must surface (status='partial'),
|
|
517
|
+
// not silently mark done (w-2026-09-30-dave-005). The wrapper still distinguishes the
|
|
518
|
+
// two failure modes: uncovered aborts finalise; degraded lets the answer.md be
|
|
519
|
+
// written and returns status='partial' with the degraded[] list.
|
|
516
520
|
function coverageReport(subQuestionIds, synthReceipts) {
|
|
517
521
|
const receiptsBySq = new Map();
|
|
518
522
|
for (const r of synthReceipts || []) {
|
|
@@ -528,7 +532,13 @@ function coverageReport(subQuestionIds, synthReceipts) {
|
|
|
528
532
|
covered.push(id);
|
|
529
533
|
if ((r.claimCount || 0) === 0) degraded.push(id);
|
|
530
534
|
}
|
|
531
|
-
return {
|
|
535
|
+
return {
|
|
536
|
+
planned,
|
|
537
|
+
covered,
|
|
538
|
+
degraded,
|
|
539
|
+
uncovered,
|
|
540
|
+
complete: uncovered.length === 0 && degraded.length === 0,
|
|
541
|
+
};
|
|
532
542
|
}
|
|
533
543
|
|
|
534
544
|
// -- Finalise (persist gate records, write sources.json + the session answer, round-trip, mark done) --
|
|
@@ -823,7 +833,14 @@ async function runDeepenRound(ctx, state, { lenses = VERIFY_LENSES, quorum = 2,
|
|
|
823
833
|
// the parent session's 200/200 WebSearch budget — the tools shipped in agents.mjs are
|
|
824
834
|
// the same tool surface the parent uses, so caps have to leave headroom).
|
|
825
835
|
const NORMAL_SEARCH_TOOL_CAP = 12; // matches agents.searchAgent default
|
|
826
|
-
const NORMAL_CLAIM_BUDGET = 25; // matches
|
|
836
|
+
const NORMAL_CLAIM_BUDGET = 25; // matches rankAndCapPerSq call below
|
|
837
|
+
// Per-sub-question retention floor for normal mode (w-2026-09-30-dave-005). Sessions
|
|
838
|
+
// 01M3S5PXQTCZR70MGDQRZWN6ZM (4 SQs, sq-3/sq-4 zero-claim, budgetDropped 56, 5.47M
|
|
839
|
+
// tokens) and 01M3S55WDKSYSC7Z7S8VRRJ9PC (6 SQs, sq-3/sq-4 zero-claim, budgetDropped
|
|
840
|
+
// 75, 10.97M tokens) both had later sub-questions silently starved by the earlier
|
|
841
|
+
// global rankAndCap cap while coverage.complete still reported true. Floor is the
|
|
842
|
+
// same "top-1 per SQ" guarantee deep already uses (DEEP_RETAIN_FLOOR_PER_SQ).
|
|
843
|
+
const NORMAL_RETAIN_FLOOR_PER_SQ = 1;
|
|
827
844
|
function normalWebSearchCeiling(subQuestionCount, centralClaimCount) {
|
|
828
845
|
const searchers = subQuestionCount * NORMAL_SEARCH_TOOL_CAP;
|
|
829
846
|
const verifiers = centralClaimCount * VERIFY_LENSES.length * VERIFY_TOOL_CALL_CAP_DEFAULT;
|
|
@@ -847,10 +864,19 @@ phase('Search');
|
|
|
847
864
|
const sqResults = await runSearchExtract(ctx, subQuestions);
|
|
848
865
|
|
|
849
866
|
// -- Rank + budget cap (deterministic code — §4.1) --
|
|
867
|
+
// Per-SQ floor applied BEFORE global trimming (w-2026-09-30-dave-005): reserve at
|
|
868
|
+
// least NORMAL_RETAIN_FLOOR_PER_SQ retained claim(s) per sub-question that had any
|
|
869
|
+
// claim extracted, then fill the remainder by global rank. A pure global cap starves
|
|
870
|
+
// later sub-questions silently (they end with zero retained claims, hence a degraded
|
|
871
|
+
// finding) while the wrapper still marks done.
|
|
850
872
|
const urlBySourceId = new Map();
|
|
851
873
|
const allClaims = flattenClaims(sqResults, urlBySourceId);
|
|
852
|
-
const { retained, extractedCount, budgetDropped } =
|
|
853
|
-
|
|
874
|
+
const { retained, extractedCount, budgetDropped } = rankAndCapPerSq(
|
|
875
|
+
allClaims,
|
|
876
|
+
NORMAL_CLAIM_BUDGET, // 25 (harness-skill cap)
|
|
877
|
+
{ floorPerSq: NORMAL_RETAIN_FLOOR_PER_SQ },
|
|
878
|
+
);
|
|
879
|
+
log(`extracted ${extractedCount} claims; retained ${retained.length} (budgetDropped ${budgetDropped}, floorPerSq ${NORMAL_RETAIN_FLOOR_PER_SQ})`);
|
|
854
880
|
|
|
855
881
|
// -- Verify (3-vote adversarial on central claims only — §4 normal row) --
|
|
856
882
|
phase('Verify');
|
|
@@ -877,10 +903,16 @@ phase('Synthesize');
|
|
|
877
903
|
const synthReceipts = await runSynth(ctx, subQuestions, retained, verdictByClaimId);
|
|
878
904
|
|
|
879
905
|
// -- Coverage gate --
|
|
880
|
-
//
|
|
881
|
-
//
|
|
906
|
+
// Two failure modes, handled separately (w-2026-09-30-dave-005):
|
|
907
|
+
// 1. UNCOVERED — a planned SQ has no findings receipt at all. Abort finalise; no
|
|
908
|
+
// answer.md written; status='partial'. This is the original gate.
|
|
909
|
+
// 2. DEGRADED — every SQ has a receipt but one or more carry zero surviving claims
|
|
910
|
+
// (the per-SQ floor kicks in for the extractor pool, but verify/dedupe may still
|
|
911
|
+
// leave an SQ empty). Let finalise run so answer.md is written, then override the
|
|
912
|
+
// returned status to 'partial' and surface the degraded[] list. Do NOT mark done
|
|
913
|
+
// — a partial run is not a done run.
|
|
882
914
|
const coverage = coverageReport(subQuestions.map((s) => s.id), synthReceipts);
|
|
883
|
-
if (
|
|
915
|
+
if (coverage.uncovered.length) {
|
|
884
916
|
log(`ABORT: coverage gap — ${coverage.uncovered.length} of ${coverage.planned} sub-question(s) have no findings receipt: [${coverage.uncovered.join(', ')}]. Not marking done.`);
|
|
885
917
|
return {
|
|
886
918
|
sessionId: ctx.sessionId,
|
|
@@ -891,7 +923,7 @@ if (!coverage.complete) {
|
|
|
891
923
|
};
|
|
892
924
|
}
|
|
893
925
|
if (coverage.degraded.length) {
|
|
894
|
-
log(`coverage: ${coverage.covered.length}/${coverage.planned} covered, ${coverage.degraded.length} degraded (zero-claim finding): [${coverage.degraded.join(', ')}]`);
|
|
926
|
+
log(`coverage: ${coverage.covered.length}/${coverage.planned} covered, ${coverage.degraded.length} degraded (zero-claim finding): [${coverage.degraded.join(', ')}] — run will return status='partial'`);
|
|
895
927
|
} else {
|
|
896
928
|
log(`coverage: ${coverage.covered.length}/${coverage.planned} covered, all with substantive findings`);
|
|
897
929
|
}
|
|
@@ -905,6 +937,12 @@ const unsourcedRatio = totalClaimCount ? totalUnsourced / totalClaimCount : 0;
|
|
|
905
937
|
const sourcesJson = buildSourcesJson(sqResults);
|
|
906
938
|
const gateRecords = buildGateRecords(sqResults);
|
|
907
939
|
|
|
940
|
+
// Subagent token cost — surfaced in runStats + the returned summary so an operator can
|
|
941
|
+
// see what a normal-mode session actually spent, without opening the harness manifest
|
|
942
|
+
// (w-2026-09-30-dave-005). Same shape deep uses.
|
|
943
|
+
const outputTokensSpent = (typeof budget !== 'undefined' && budget && budget.spent) ? budget.spent() : 0;
|
|
944
|
+
const budgetTotal = (typeof budget !== 'undefined' && budget && budget.total != null) ? budget.total : null;
|
|
945
|
+
|
|
908
946
|
const runStats = {
|
|
909
947
|
extracted: extractedCount, central: centralClaims.length,
|
|
910
948
|
confirmed: confirmedN, refuted: refutedN, unverifiedInfra: infraN,
|
|
@@ -913,6 +951,8 @@ const runStats = {
|
|
|
913
951
|
gatedDropped: gateRecords.length,
|
|
914
952
|
coverage,
|
|
915
953
|
webSearchCeiling: ceilingUpperBound,
|
|
954
|
+
outputTokensSpent,
|
|
955
|
+
budgetTotal,
|
|
916
956
|
};
|
|
917
957
|
|
|
918
958
|
if (unsourcedRatio > UNSOURCED_ABORT_RATIO) {
|
|
@@ -924,12 +964,18 @@ if (unsourcedRatio > UNSOURCED_ABORT_RATIO) {
|
|
|
924
964
|
phase('Finalise');
|
|
925
965
|
const finalise = await runFinalise(ctx, { gateRecords, sourcesJson, runStats });
|
|
926
966
|
|
|
927
|
-
|
|
928
|
-
|
|
967
|
+
// A degraded run reports 'partial' regardless of the finaliser's markedDone flag —
|
|
968
|
+
// the answer.md is written and the session may be marked done in the store, but the
|
|
969
|
+
// returned summary tells the caller a sub-question was not answered
|
|
970
|
+
// (w-2026-09-30-dave-005). coverage.complete is already false in that case.
|
|
971
|
+
const baseStatus = finalise.markedDone ? 'done' : 'in-progress';
|
|
972
|
+
const status = coverage.degraded.length ? 'partial' : baseStatus;
|
|
973
|
+
log(`finalise: sources=${finalise.wroteSources} answer=${finalise.wroteAnswer} roundTrip=${finalise.roundTripOk} markedDone=${finalise.markedDone} gatesAppended=${finalise.gatesAppended ?? 0}/${gateRecords.length} status=${status}`);
|
|
929
974
|
|
|
930
975
|
return {
|
|
931
976
|
sessionId: ctx.sessionId,
|
|
932
977
|
status,
|
|
978
|
+
...(coverage.degraded.length ? { reason: `${coverage.degraded.length} sub-question(s) with zero surviving claims`, degraded: coverage.degraded } : {}),
|
|
933
979
|
question: ctx.baseQuestion,
|
|
934
980
|
outputMode: ctx.baseOutputMode,
|
|
935
981
|
subQuestions: subQuestions.map((s) => ({ id: s.id, text: s.text, kind: s.kind })),
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { deriveContext } from '../engine/io.mjs';
|
|
5
5
|
import { runPlan } from '../engine/plan.mjs';
|
|
6
6
|
import { runSearchExtract } from '../engine/agents.mjs';
|
|
7
|
-
import { flattenClaims,
|
|
7
|
+
import { flattenClaims, rankAndCapPerSq } from '../engine/rank.mjs';
|
|
8
8
|
import { runVerify, VERIFY_LENSES, VERIFY_TOOL_CALL_CAP_DEFAULT } from '../engine/verify.mjs';
|
|
9
9
|
import { runDeepenRound } from '../engine/deepen.mjs';
|
|
10
10
|
import { UNSOURCED_ABORT_RATIO, buildSourcesJson, buildGateRecords, runSynth, runFinalise, coverageReport } from '../engine/finalise.mjs';
|
|
@@ -16,7 +16,14 @@ import { UNSOURCED_ABORT_RATIO, buildSourcesJson, buildGateRecords, runSynth, ru
|
|
|
16
16
|
// the parent session's 200/200 WebSearch budget — the tools shipped in agents.mjs are
|
|
17
17
|
// the same tool surface the parent uses, so caps have to leave headroom).
|
|
18
18
|
const NORMAL_SEARCH_TOOL_CAP = 12; // matches agents.searchAgent default
|
|
19
|
-
const NORMAL_CLAIM_BUDGET = 25; // matches
|
|
19
|
+
const NORMAL_CLAIM_BUDGET = 25; // matches rankAndCapPerSq call below
|
|
20
|
+
// Per-sub-question retention floor for normal mode (w-2026-09-30-dave-005). Sessions
|
|
21
|
+
// 01M3S5PXQTCZR70MGDQRZWN6ZM (4 SQs, sq-3/sq-4 zero-claim, budgetDropped 56, 5.47M
|
|
22
|
+
// tokens) and 01M3S55WDKSYSC7Z7S8VRRJ9PC (6 SQs, sq-3/sq-4 zero-claim, budgetDropped
|
|
23
|
+
// 75, 10.97M tokens) both had later sub-questions silently starved by the earlier
|
|
24
|
+
// global rankAndCap cap while coverage.complete still reported true. Floor is the
|
|
25
|
+
// same "top-1 per SQ" guarantee deep already uses (DEEP_RETAIN_FLOOR_PER_SQ).
|
|
26
|
+
const NORMAL_RETAIN_FLOOR_PER_SQ = 1;
|
|
20
27
|
function normalWebSearchCeiling(subQuestionCount, centralClaimCount) {
|
|
21
28
|
const searchers = subQuestionCount * NORMAL_SEARCH_TOOL_CAP;
|
|
22
29
|
const verifiers = centralClaimCount * VERIFY_LENSES.length * VERIFY_TOOL_CALL_CAP_DEFAULT;
|
|
@@ -54,10 +61,19 @@ phase('Search');
|
|
|
54
61
|
const sqResults = await runSearchExtract(ctx, subQuestions);
|
|
55
62
|
|
|
56
63
|
// -- Rank + budget cap (deterministic code — §4.1) --
|
|
64
|
+
// Per-SQ floor applied BEFORE global trimming (w-2026-09-30-dave-005): reserve at
|
|
65
|
+
// least NORMAL_RETAIN_FLOOR_PER_SQ retained claim(s) per sub-question that had any
|
|
66
|
+
// claim extracted, then fill the remainder by global rank. A pure global cap starves
|
|
67
|
+
// later sub-questions silently (they end with zero retained claims, hence a degraded
|
|
68
|
+
// finding) while the wrapper still marks done.
|
|
57
69
|
const urlBySourceId = new Map();
|
|
58
70
|
const allClaims = flattenClaims(sqResults, urlBySourceId);
|
|
59
|
-
const { retained, extractedCount, budgetDropped } =
|
|
60
|
-
|
|
71
|
+
const { retained, extractedCount, budgetDropped } = rankAndCapPerSq(
|
|
72
|
+
allClaims,
|
|
73
|
+
NORMAL_CLAIM_BUDGET, // 25 (harness-skill cap)
|
|
74
|
+
{ floorPerSq: NORMAL_RETAIN_FLOOR_PER_SQ },
|
|
75
|
+
);
|
|
76
|
+
log(`extracted ${extractedCount} claims; retained ${retained.length} (budgetDropped ${budgetDropped}, floorPerSq ${NORMAL_RETAIN_FLOOR_PER_SQ})`);
|
|
61
77
|
|
|
62
78
|
// -- Verify (3-vote adversarial on central claims only — §4 normal row) --
|
|
63
79
|
phase('Verify');
|
|
@@ -84,10 +100,16 @@ phase('Synthesize');
|
|
|
84
100
|
const synthReceipts = await runSynth(ctx, subQuestions, retained, verdictByClaimId);
|
|
85
101
|
|
|
86
102
|
// -- Coverage gate --
|
|
87
|
-
//
|
|
88
|
-
//
|
|
103
|
+
// Two failure modes, handled separately (w-2026-09-30-dave-005):
|
|
104
|
+
// 1. UNCOVERED — a planned SQ has no findings receipt at all. Abort finalise; no
|
|
105
|
+
// answer.md written; status='partial'. This is the original gate.
|
|
106
|
+
// 2. DEGRADED — every SQ has a receipt but one or more carry zero surviving claims
|
|
107
|
+
// (the per-SQ floor kicks in for the extractor pool, but verify/dedupe may still
|
|
108
|
+
// leave an SQ empty). Let finalise run so answer.md is written, then override the
|
|
109
|
+
// returned status to 'partial' and surface the degraded[] list. Do NOT mark done
|
|
110
|
+
// — a partial run is not a done run.
|
|
89
111
|
const coverage = coverageReport(subQuestions.map((s) => s.id), synthReceipts);
|
|
90
|
-
if (
|
|
112
|
+
if (coverage.uncovered.length) {
|
|
91
113
|
log(`ABORT: coverage gap — ${coverage.uncovered.length} of ${coverage.planned} sub-question(s) have no findings receipt: [${coverage.uncovered.join(', ')}]. Not marking done.`);
|
|
92
114
|
return {
|
|
93
115
|
sessionId: ctx.sessionId,
|
|
@@ -98,7 +120,7 @@ if (!coverage.complete) {
|
|
|
98
120
|
};
|
|
99
121
|
}
|
|
100
122
|
if (coverage.degraded.length) {
|
|
101
|
-
log(`coverage: ${coverage.covered.length}/${coverage.planned} covered, ${coverage.degraded.length} degraded (zero-claim finding): [${coverage.degraded.join(', ')}]`);
|
|
123
|
+
log(`coverage: ${coverage.covered.length}/${coverage.planned} covered, ${coverage.degraded.length} degraded (zero-claim finding): [${coverage.degraded.join(', ')}] — run will return status='partial'`);
|
|
102
124
|
} else {
|
|
103
125
|
log(`coverage: ${coverage.covered.length}/${coverage.planned} covered, all with substantive findings`);
|
|
104
126
|
}
|
|
@@ -112,6 +134,12 @@ const unsourcedRatio = totalClaimCount ? totalUnsourced / totalClaimCount : 0;
|
|
|
112
134
|
const sourcesJson = buildSourcesJson(sqResults);
|
|
113
135
|
const gateRecords = buildGateRecords(sqResults);
|
|
114
136
|
|
|
137
|
+
// Subagent token cost — surfaced in runStats + the returned summary so an operator can
|
|
138
|
+
// see what a normal-mode session actually spent, without opening the harness manifest
|
|
139
|
+
// (w-2026-09-30-dave-005). Same shape deep uses.
|
|
140
|
+
const outputTokensSpent = (typeof budget !== 'undefined' && budget && budget.spent) ? budget.spent() : 0;
|
|
141
|
+
const budgetTotal = (typeof budget !== 'undefined' && budget && budget.total != null) ? budget.total : null;
|
|
142
|
+
|
|
115
143
|
const runStats = {
|
|
116
144
|
extracted: extractedCount, central: centralClaims.length,
|
|
117
145
|
confirmed: confirmedN, refuted: refutedN, unverifiedInfra: infraN,
|
|
@@ -120,6 +148,8 @@ const runStats = {
|
|
|
120
148
|
gatedDropped: gateRecords.length,
|
|
121
149
|
coverage,
|
|
122
150
|
webSearchCeiling: ceilingUpperBound,
|
|
151
|
+
outputTokensSpent,
|
|
152
|
+
budgetTotal,
|
|
123
153
|
};
|
|
124
154
|
|
|
125
155
|
if (unsourcedRatio > UNSOURCED_ABORT_RATIO) {
|
|
@@ -131,12 +161,18 @@ if (unsourcedRatio > UNSOURCED_ABORT_RATIO) {
|
|
|
131
161
|
phase('Finalise');
|
|
132
162
|
const finalise = await runFinalise(ctx, { gateRecords, sourcesJson, runStats });
|
|
133
163
|
|
|
134
|
-
|
|
135
|
-
|
|
164
|
+
// A degraded run reports 'partial' regardless of the finaliser's markedDone flag —
|
|
165
|
+
// the answer.md is written and the session may be marked done in the store, but the
|
|
166
|
+
// returned summary tells the caller a sub-question was not answered
|
|
167
|
+
// (w-2026-09-30-dave-005). coverage.complete is already false in that case.
|
|
168
|
+
const baseStatus = finalise.markedDone ? 'done' : 'in-progress';
|
|
169
|
+
const status = coverage.degraded.length ? 'partial' : baseStatus;
|
|
170
|
+
log(`finalise: sources=${finalise.wroteSources} answer=${finalise.wroteAnswer} roundTrip=${finalise.roundTripOk} markedDone=${finalise.markedDone} gatesAppended=${finalise.gatesAppended ?? 0}/${gateRecords.length} status=${status}`);
|
|
136
171
|
|
|
137
172
|
return {
|
|
138
173
|
sessionId: ctx.sessionId,
|
|
139
174
|
status,
|
|
175
|
+
...(coverage.degraded.length ? { reason: `${coverage.degraded.length} sub-question(s) with zero surviving claims`, degraded: coverage.degraded } : {}),
|
|
140
176
|
question: ctx.baseQuestion,
|
|
141
177
|
outputMode: ctx.baseOutputMode,
|
|
142
178
|
subQuestions: subQuestions.map((s) => ({ id: s.id, text: s.text, kind: s.kind })),
|