@stravica-ai/digital-operator-pa 0.1.33 → 0.1.34
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/bin/estate-sync +0 -0
- package/dist/engineering-pa-cli/bin/launch +0 -0
- package/dist/engineering-pa-cli/bin/pa +0 -0
- package/dist/engineering-pa-cli/bin/pa-coordinator-hook +0 -0
- package/dist/engineering-pa-cli/bin/research +0 -0
- package/dist/engineering-pa-cli/bin/scheduler +0 -0
- package/dist/engineering-pa-cli/bin/secrets +0 -0
- package/dist/engineering-pa-cli/bin/smoke +0 -0
- package/dist/engineering-pa-cli/bin/sync +0 -0
- package/dist/engineering-pa-cli/docs/secrets.md +34 -0
- package/dist/engineering-pa-cli/scripts/migrate-doc-paths.mjs +0 -0
- package/dist/engineering-pa-cli/src/research/briefs/deep-verifier.md +7 -2
- package/dist/engineering-pa-cli/src/research/briefs/verifier.md +6 -2
- package/dist/engineering-pa-cli/src/research/engine/agents.mjs +3 -0
- package/dist/engineering-pa-cli/src/research/engine/deep.mjs +5 -50
- package/dist/engineering-pa-cli/src/research/engine/finalise.mjs +80 -4
- package/dist/engineering-pa-cli/src/research/engine/verify.mjs +7 -0
- package/dist/engineering-pa-cli/src/research/workflows/research-deep.js +93 -53
- package/dist/engineering-pa-cli/src/research/workflows/research-normal.js +137 -5
- package/dist/engineering-pa-cli/src/research/wrappers/normal.mjs +49 -3
- package/dist/engineering-pa-cli/src/secrets/cli.mjs +96 -3
- package/dist/engineering-pa-cli/src/secrets/commands/get.mjs +40 -9
- package/dist/engineering-pa-cli/src/secrets/vault.mjs +2 -1
- package/dist/engineering-pa-core/bin/desk +0 -0
- package/dist/engineering-pa-core/bin/distill +0 -0
- package/dist/engineering-pa-core/bin/memory-sync +0 -0
- package/dist/engineering-pa-core/bin/slice +0 -0
- package/dist/engineering-pa-core/src/scheduler/install.mjs +110 -4
- package/dist/engineering-pa-library-cli/bin/library +0 -0
- package/dist/engineering-pa-library-cli/package.json +1 -1
- package/dist/engineering-pa-library-cli/src/config.mjs +26 -4
- package/dist/engineering-pa-library-cli/src/verbs/status.mjs +40 -2
- package/dist/engineering-pa-library-tunnel/bin/library-tunnel +0 -0
- package/dist/engineering-pa-migrate-inspect/bin/migrate-inspect +0 -0
- package/dist/engineering-pa-scaffold/bin/pa-init +0 -0
- package/dist/engineering-pa-workspace/src/ws.mjs +0 -0
- package/package.json +5 -6
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -85,6 +85,40 @@ node core/tools/secrets/bin/secrets set API_KEY "sk-abc123" --no-canonical
|
|
|
85
85
|
node core/tools/secrets/bin/secrets verify --values --against /path/to/other/vault/scope.enc.json
|
|
86
86
|
```
|
|
87
87
|
|
|
88
|
+
### Full-scope reference form (get/set/delete/find)
|
|
89
|
+
|
|
90
|
+
Barry ruling 2026-07-15 (`w-2026-07-15-023`): every secret is referred
|
|
91
|
+
to by its full scope path at every non-interactive reference site. The
|
|
92
|
+
scope is named at the call site so provenance is visible and same-name
|
|
93
|
+
keys in different scopes cannot silently collide (a real problem today,
|
|
94
|
+
`secrets find CLOUDFLARE_WORKERS_ACCOUNT_ID_STRAVICA` returns matches
|
|
95
|
+
in both `hq-estate` and `wsd`).
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
secrets get hq-estate/CLERK_AUTH_KEY
|
|
99
|
+
secrets set clients-clive/BUNNY_API_KEY_CLIVE -
|
|
100
|
+
secrets delete seo-ops/OPENAI_API_KEY
|
|
101
|
+
secrets find hq-estate/GITHUB_PAT_MAX_READ
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Each is equivalent to the corresponding `--scope <name> <verb> KEY`
|
|
105
|
+
form. The two may be combined when they agree; a mismatch is a hard
|
|
106
|
+
error (exit 1). `--file` is not combinable with the reference form (use
|
|
107
|
+
one or the other). Bare-key calls still work and remain the default for
|
|
108
|
+
interactive use, but emit a one-line deprecation notice on stderr
|
|
109
|
+
naming the preferred `scope/KEY` form for the target vault.
|
|
110
|
+
|
|
111
|
+
### Presence check (`get KEY --exists`)
|
|
112
|
+
|
|
113
|
+
`secrets get KEY --exists` (alias `--dry-run`) probes for the key
|
|
114
|
+
without ever decrypting. Prints `present` and exits 0 when the key
|
|
115
|
+
resolves in the target vault; prints `absent` and exits 1 when it does
|
|
116
|
+
not. Combines with `--scope`, `--file`, and the `scope/KEY` reference
|
|
117
|
+
form. Use it as the migration proof primitive when moving a reference
|
|
118
|
+
site to the full-scope form: run `secrets get scope/KEY --exists` and
|
|
119
|
+
compare against the pre-migration equivalent to prove identity, without
|
|
120
|
+
ever handling the secret value itself.
|
|
121
|
+
|
|
88
122
|
### Canonical-first writes
|
|
89
123
|
|
|
90
124
|
Canonical-first dual-write is the DEFAULT for scoped `set` since 0.1.20 —
|
|
File without changes
|
|
@@ -57,8 +57,13 @@ Return one of three outcomes: `confirmed`, `refuted`, `unverified-infra`.
|
|
|
57
57
|
## 4. Tools & sources
|
|
58
58
|
|
|
59
59
|
- `WebSearch` + `WebFetch` to seek contradiction, rival explanations, and counter-
|
|
60
|
-
examples. Keep it lean —
|
|
61
|
-
|
|
60
|
+
examples. Keep it lean — you are testing one synthesis, not rebuilding the sub-
|
|
61
|
+
question.
|
|
62
|
+
- **Hard tool-call cap: {{verifyToolCallCap}} calls total** (searches + fetches
|
|
63
|
+
combined). This is a real budget, not a suggestion: workflow WebSearch calls draw
|
|
64
|
+
from the PARENT session's shared budget, and a fan-out of verifiers can drain it.
|
|
65
|
+
Reach the cap without a decisive check → vote per the adversarial default (§3, §8),
|
|
66
|
+
noting in `note` that the cap was reached.
|
|
62
67
|
|
|
63
68
|
## 5. Boundaries
|
|
64
69
|
|
|
@@ -47,8 +47,12 @@ Return one of three outcomes: `confirmed`, `refuted`, `unverified-infra`.
|
|
|
47
47
|
## 4. Tools & sources
|
|
48
48
|
|
|
49
49
|
- `WebSearch` + `WebFetch` to seek contradiction and corroboration. Keep it lean —
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
you are testing one claim, not rebuilding the sub-question.
|
|
51
|
+
- **Hard tool-call cap: {{verifyToolCallCap}} calls total** (searches + fetches
|
|
52
|
+
combined). This is a real budget, not a suggestion: workflow WebSearch calls draw
|
|
53
|
+
from the PARENT session's shared budget, and a fan-out of verifiers can drain it.
|
|
54
|
+
Reach the cap without a decisive check → vote per the adversarial default (§3,
|
|
55
|
+
§8), noting in `note` that the cap was reached.
|
|
52
56
|
|
|
53
57
|
## 5. Boundaries
|
|
54
58
|
|
|
@@ -121,6 +121,9 @@ export function verifyAgent(ctx, claim, lens, model = 'sonnet', effort = 'high',
|
|
|
121
121
|
return agent(brief(ctx, role, {
|
|
122
122
|
sessionId: ctx.sessionId, claimText: claim.text, quote: claim.quote, sourceUrl: claim.url || claim.sourceId,
|
|
123
123
|
subQuestionText: claim.sqText, lens,
|
|
124
|
+
// Verifier tool-call cap: shared WebSearch budget with the parent session, so keep
|
|
125
|
+
// per-verifier calls tight (w-2026-09-01-dave-010). Defaults to 2; deep can lift.
|
|
126
|
+
verifyToolCallCap: ctx.verifyToolCallCap ?? 2,
|
|
124
127
|
}), { model, effort, phase: 'Verify', label: `verify:${claim.claimId}:${lens}`, schema: VERIFY_SCHEMA });
|
|
125
128
|
}
|
|
126
129
|
|
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
// stop, and testOverrides so a mechanical smoke run is self-identifying.
|
|
16
16
|
import { flattenClaims, rankAndCapPerSq } from './rank.mjs';
|
|
17
17
|
import { adjudicate } from './verify.mjs';
|
|
18
|
-
import { verifyAgent
|
|
18
|
+
import { verifyAgent } from './agents.mjs';
|
|
19
19
|
import { runDeepenRound, deepenStopReason } from './deepen.mjs';
|
|
20
|
-
import { buildSourcesJson, buildGateRecords, bucketsFor } from './finalise.mjs';
|
|
20
|
+
import { buildSourcesJson, buildGateRecords, bucketsFor, synthOrDegrade } from './finalise.mjs';
|
|
21
21
|
|
|
22
22
|
// Guaranteed floor of top claims kept per sub-question when the claim budget is capped
|
|
23
23
|
// (deep-mode fix for SQ starvation). 1 = every planned sub-question keeps its single best
|
|
@@ -316,60 +316,15 @@ export async function recordRunStart(ctx) {
|
|
|
316
316
|
// claim, but a searcher/extractor that found nothing still leaves an empty SQ). Such an
|
|
317
317
|
// SQ gets a DEGRADED finding: schema-valid, confidence 'low', zero claims, and a summary
|
|
318
318
|
// explaining the absence. This closes the deep-judge's "only N of M sub-questions
|
|
319
|
-
// answered" gap and keeps the finding count == the plan's sub-question count.
|
|
319
|
+
// answered" gap and keeps the finding count == the plan's sub-question count. The
|
|
320
|
+
// synthOrDegrade/degradedFinding/degradedSynthAgent primitives now live in finalise.mjs
|
|
321
|
+
// (shared with normal — w-2026-09-01-dave-010 fix).
|
|
320
322
|
export async function runDeepSynth(ctx, subQuestions, retained, verdictByClaimId) {
|
|
321
323
|
return (await parallel(
|
|
322
324
|
subQuestions.map((sq) => () => synthOrDegrade(ctx, sq, bucketsFor(retained, verdictByClaimId, sq.id))),
|
|
323
325
|
)).filter(Boolean);
|
|
324
326
|
}
|
|
325
327
|
|
|
326
|
-
// Synthesize one sub-question, degrading to a zero-claim finding when there is nothing to
|
|
327
|
-
// synthesize or the synthesizer fails to write a schema-valid finding — either way the SQ
|
|
328
|
-
// keeps its file, so the invariant holds.
|
|
329
|
-
async function synthOrDegrade(ctx, sq, buckets) {
|
|
330
|
-
const hasClaims = buckets.verified.length || buckets.supporting.length || buckets.refuted.length;
|
|
331
|
-
if (hasClaims) {
|
|
332
|
-
const receipt = await synthAgent(ctx, sq, buckets);
|
|
333
|
-
if (receipt && receipt.written === true) return receipt;
|
|
334
|
-
log(`synth: ${sq.id} did not write a valid finding — writing a degraded finding to preserve the finding-per-SQ invariant`);
|
|
335
|
-
return degradedSynthAgent(ctx, sq, 'the synthesizer did not produce a schema-valid finding');
|
|
336
|
-
}
|
|
337
|
-
log(`synth: ${sq.id} had no retained claims — writing a degraded (zero-claim) finding to preserve the finding-per-SQ invariant`);
|
|
338
|
-
return degradedSynthAgent(ctx, sq, 'no claims survived ranking/verification for this sub-question');
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
// Pure: the exact degraded finding for a sub-question (fits the EXISTING finding schema —
|
|
342
|
-
// no store change). Empty quotes/sources/votes, confidence 'low', a self-describing
|
|
343
|
-
// summary, unsourced:false.
|
|
344
|
-
export function degradedFinding(sq, reason) {
|
|
345
|
-
return {
|
|
346
|
-
subQuestionId: sq.id,
|
|
347
|
-
summary: `No verifiable findings were produced for this sub-question (${sq.text}). Reason: ${reason}. `
|
|
348
|
-
+ `This degraded finding is written to preserve the one-finding-per-sub-question invariant; treat the `
|
|
349
|
-
+ `sub-question as unanswered and re-run research for it if a complete answer is required.`,
|
|
350
|
-
quotes: [],
|
|
351
|
-
sources: [],
|
|
352
|
-
confidence: 'low',
|
|
353
|
-
verificationVotes: [],
|
|
354
|
-
unsourced: false,
|
|
355
|
-
};
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
// Have an agent write the degraded finding verbatim and round-trip it. Returns a synth
|
|
359
|
-
// receipt (claimCount 0) so the caller's aggregation (unsourced ratio, run stats) is
|
|
360
|
-
// unaffected. No brief needed — this is a mechanical verbatim write.
|
|
361
|
-
async function degradedSynthAgent(ctx, sq, reason) {
|
|
362
|
-
const finding = degradedFinding(sq, reason);
|
|
363
|
-
const path = `${ctx.sessionDir}/findings/${sq.id}.json`;
|
|
364
|
-
return await agent([
|
|
365
|
-
`You are writing a **degraded finding** for deep-research session \`${ctx.sessionId}\`, sub-question \`${sq.id}\`.`,
|
|
366
|
-
`Write this EXACT JSON (verbatim, no edits) to \`${path}\`:`,
|
|
367
|
-
'```json', JSON.stringify(finding, null, 2), '```',
|
|
368
|
-
`Then round-trip: run \`${ctx.cli} show ${ctx.sessionId} --json\` and confirm exit 0 (it validates findings).`,
|
|
369
|
-
`Return { subQuestionId: "${sq.id}", written: true, path: "${path}", confidence: "low", claimCount: 0,`,
|
|
370
|
-
`centralConfirmed: 0, centralRefuted: 0, unsourcedCount: 0 } once the round-trip is green.`,
|
|
371
|
-
].join('\n'), { model: ctx.modelOverride || 'haiku', effort: 'low', phase: 'Synthesize', label: `synth-degraded:${sq.id}`, schema: SYNTH_SCHEMA });
|
|
372
|
-
}
|
|
373
328
|
|
|
374
329
|
// ── judge phase (advisory — spec §2.3, arch §4.2 phase 7) ──────────────────────
|
|
375
330
|
export const JUDGE_RECEIPT_SCHEMA = {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Shared research engine — synthesis bucketing, deterministic aggregation
|
|
2
2
|
// (sources.json / gate records), and the synth + finalise phases.
|
|
3
|
-
import { synthAgent } from './agents.mjs';
|
|
3
|
+
import { synthAgent, SYNTH_SCHEMA } from './agents.mjs';
|
|
4
4
|
|
|
5
5
|
export const UNSOURCED_ABORT_RATIO = 0.10; // §8 guardrail: >10% unsourced fails the run
|
|
6
6
|
|
|
@@ -71,14 +71,90 @@ export function buildGateRecords(sqResults) {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
// -- Synthesize (per-SQ finding files — §4.1 lead tier) --
|
|
74
|
+
// Every planned sub-question gets a finding, even when no claim survived ranking or
|
|
75
|
+
// verification. Normal's global CLAIM_BUDGET cap (rankAndCap in wrappers/normal.mjs)
|
|
76
|
+
// concentrates retention on the highest-ranked SQs; without the degrade fallback the
|
|
77
|
+
// starved SQs silently vanish from the answer while the workflow still reports
|
|
78
|
+
// status='done' (see w-2026-09-01-dave-010 — 4-SQ plan, sq-3/sq-4 dropped, 5 findings
|
|
79
|
+
// presented as complete). runDeepSynth carries the same invariant for deep mode.
|
|
74
80
|
export async function runSynth(ctx, subQuestions, retained, verdictByClaimId) {
|
|
75
81
|
return (await parallel(
|
|
76
|
-
subQuestions
|
|
77
|
-
.filter((sq) => retained.some((c) => c.sqId === sq.id))
|
|
78
|
-
.map((sq) => () => synthAgent(ctx, sq, bucketsFor(retained, verdictByClaimId, sq.id))),
|
|
82
|
+
subQuestions.map((sq) => () => synthOrDegrade(ctx, sq, bucketsFor(retained, verdictByClaimId, sq.id))),
|
|
79
83
|
)).filter(Boolean);
|
|
80
84
|
}
|
|
81
85
|
|
|
86
|
+
// Synthesize one sub-question, degrading to a zero-claim finding when there is nothing
|
|
87
|
+
// to synthesize or the synthesizer fails to write a schema-valid finding — either way
|
|
88
|
+
// the SQ keeps its file, so the finding-per-SQ invariant holds and the coverage gate in
|
|
89
|
+
// the wrapper can see the full picture.
|
|
90
|
+
export async function synthOrDegrade(ctx, sq, buckets) {
|
|
91
|
+
const hasClaims = buckets.verified.length || buckets.supporting.length || buckets.refuted.length;
|
|
92
|
+
if (hasClaims) {
|
|
93
|
+
const receipt = await synthAgent(ctx, sq, buckets);
|
|
94
|
+
if (receipt && receipt.written === true) return receipt;
|
|
95
|
+
log(`synth: ${sq.id} did not write a valid finding — writing a degraded finding to preserve the finding-per-SQ invariant`);
|
|
96
|
+
return degradedSynthAgent(ctx, sq, 'the synthesizer did not produce a schema-valid finding');
|
|
97
|
+
}
|
|
98
|
+
log(`synth: ${sq.id} had no retained claims — writing a degraded (zero-claim) finding to preserve the finding-per-SQ invariant`);
|
|
99
|
+
return degradedSynthAgent(ctx, sq, 'no claims survived ranking/verification for this sub-question');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Pure: the exact degraded finding for a sub-question (fits the EXISTING finding schema —
|
|
103
|
+
// no store change). Empty quotes/sources/votes, confidence 'low', a self-describing
|
|
104
|
+
// summary, unsourced:false.
|
|
105
|
+
export function degradedFinding(sq, reason) {
|
|
106
|
+
return {
|
|
107
|
+
subQuestionId: sq.id,
|
|
108
|
+
summary: `No verifiable findings were produced for this sub-question (${sq.text}). Reason: ${reason}. `
|
|
109
|
+
+ `This degraded finding is written to preserve the one-finding-per-sub-question invariant; treat the `
|
|
110
|
+
+ `sub-question as unanswered and re-run research for it if a complete answer is required.`,
|
|
111
|
+
quotes: [],
|
|
112
|
+
sources: [],
|
|
113
|
+
confidence: 'low',
|
|
114
|
+
verificationVotes: [],
|
|
115
|
+
unsourced: false,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Have an agent write the degraded finding verbatim and round-trip it. Returns a synth
|
|
120
|
+
// receipt (claimCount 0) so the caller's aggregation (unsourced ratio, coverage stats)
|
|
121
|
+
// is unaffected. No brief needed — this is a mechanical verbatim write.
|
|
122
|
+
export async function degradedSynthAgent(ctx, sq, reason) {
|
|
123
|
+
const finding = degradedFinding(sq, reason);
|
|
124
|
+
const path = `${ctx.sessionDir}/findings/${sq.id}.json`;
|
|
125
|
+
return await agent([
|
|
126
|
+
`You are writing a **degraded finding** for research session \`${ctx.sessionId}\`, sub-question \`${sq.id}\`.`,
|
|
127
|
+
`Write this EXACT JSON (verbatim, no edits) to \`${path}\`:`,
|
|
128
|
+
'```json', JSON.stringify(finding, null, 2), '```',
|
|
129
|
+
`Then round-trip: run \`${ctx.cli} show ${ctx.sessionId} --json\` and confirm exit 0 (it validates findings).`,
|
|
130
|
+
`Return { subQuestionId: "${sq.id}", written: true, path: "${path}", confidence: "low", claimCount: 0,`,
|
|
131
|
+
`centralConfirmed: 0, centralRefuted: 0, unsourcedCount: 0 } once the round-trip is green.`,
|
|
132
|
+
].join('\n'), { model: ctx.modelOverride || 'haiku', effort: 'low', phase: 'Synthesize', label: `synth-degraded:${sq.id}`, schema: SYNTH_SCHEMA });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Pure coverage computation. Given the planner's sub-question ids and the synth
|
|
136
|
+
// receipts, decide which SQs have a finding (any receipt with written=true), which ran
|
|
137
|
+
// as a degraded write (empty finding, still counted as covered), and which are wholly
|
|
138
|
+
// uncovered (no receipt at all — the failure this gate exists to catch). Used by the
|
|
139
|
+
// wrapper to fail loudly instead of silently reporting status='done' on a partial run.
|
|
140
|
+
export function coverageReport(subQuestionIds, synthReceipts) {
|
|
141
|
+
const receiptsBySq = new Map();
|
|
142
|
+
for (const r of synthReceipts || []) {
|
|
143
|
+
if (r && r.subQuestionId) receiptsBySq.set(r.subQuestionId, r);
|
|
144
|
+
}
|
|
145
|
+
const planned = subQuestionIds.length;
|
|
146
|
+
const covered = [];
|
|
147
|
+
const degraded = [];
|
|
148
|
+
const uncovered = [];
|
|
149
|
+
for (const id of subQuestionIds) {
|
|
150
|
+
const r = receiptsBySq.get(id);
|
|
151
|
+
if (!r || r.written !== true) { uncovered.push(id); continue; }
|
|
152
|
+
covered.push(id);
|
|
153
|
+
if ((r.claimCount || 0) === 0) degraded.push(id);
|
|
154
|
+
}
|
|
155
|
+
return { planned, covered, degraded, uncovered, complete: uncovered.length === 0 };
|
|
156
|
+
}
|
|
157
|
+
|
|
82
158
|
// -- Finalise (persist gate records, write sources.json + the session answer, round-trip, mark done) --
|
|
83
159
|
export async function runFinalise(ctx, { gateRecords, sourcesJson, runStats }) {
|
|
84
160
|
// The gate-persistence step is present only when searchers actually dropped gated
|
|
@@ -6,6 +6,13 @@ import { verifyAgent } from './agents.mjs';
|
|
|
6
6
|
|
|
7
7
|
export const VERIFY_LENSES = ['quote-support', 'contradiction-search', 'recency-source-fit'];
|
|
8
8
|
|
|
9
|
+
// Hard per-verifier WebSearch/WebFetch cap. Workflow searchers observed to drain the
|
|
10
|
+
// PARENT session's shared WebSearch budget (w-2026-09-01-dave-010: 200/200 consumed
|
|
11
|
+
// on a normal run). The verifier brief already asks for "a few targeted calls"; this
|
|
12
|
+
// makes it a schema-enforceable ceiling the brief interpolates. Two calls per lens is
|
|
13
|
+
// enough to test one claim without re-researching the sub-question.
|
|
14
|
+
export const VERIFY_TOOL_CALL_CAP_DEFAULT = 2;
|
|
15
|
+
|
|
9
16
|
// Decide one claim's verdict from its votes.
|
|
10
17
|
// - `unverified-infra` votes are infrastructure failures, not evidence — they
|
|
11
18
|
// count toward neither confirmation nor refutation.
|
|
@@ -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:cbdbff5d260df77a3b56a60d37bd75a58f0fa38968dbd0f0181c71fb6d8b3d29
|
|
4
4
|
|
|
5
5
|
export const meta = {
|
|
6
6
|
name: 'research-deep',
|
|
@@ -331,6 +331,9 @@ function verifyAgent(ctx, claim, lens, model = 'sonnet', effort = 'high', role =
|
|
|
331
331
|
return agent(brief(ctx, role, {
|
|
332
332
|
sessionId: ctx.sessionId, claimText: claim.text, quote: claim.quote, sourceUrl: claim.url || claim.sourceId,
|
|
333
333
|
subQuestionText: claim.sqText, lens,
|
|
334
|
+
// Verifier tool-call cap: shared WebSearch budget with the parent session, so keep
|
|
335
|
+
// per-verifier calls tight (w-2026-09-01-dave-010). Defaults to 2; deep can lift.
|
|
336
|
+
verifyToolCallCap: ctx.verifyToolCallCap ?? 2,
|
|
334
337
|
}), { model, effort, phase: 'Verify', label: `verify:${claim.claimId}:${lens}`, schema: VERIFY_SCHEMA });
|
|
335
338
|
}
|
|
336
339
|
|
|
@@ -445,14 +448,90 @@ function buildGateRecords(sqResults) {
|
|
|
445
448
|
}
|
|
446
449
|
|
|
447
450
|
// -- Synthesize (per-SQ finding files — §4.1 lead tier) --
|
|
451
|
+
// Every planned sub-question gets a finding, even when no claim survived ranking or
|
|
452
|
+
// verification. Normal's global CLAIM_BUDGET cap (rankAndCap in wrappers/normal.mjs)
|
|
453
|
+
// concentrates retention on the highest-ranked SQs; without the degrade fallback the
|
|
454
|
+
// starved SQs silently vanish from the answer while the workflow still reports
|
|
455
|
+
// status='done' (see w-2026-09-01-dave-010 — 4-SQ plan, sq-3/sq-4 dropped, 5 findings
|
|
456
|
+
// presented as complete). runDeepSynth carries the same invariant for deep mode.
|
|
448
457
|
async function runSynth(ctx, subQuestions, retained, verdictByClaimId) {
|
|
449
458
|
return (await parallel(
|
|
450
|
-
subQuestions
|
|
451
|
-
.filter((sq) => retained.some((c) => c.sqId === sq.id))
|
|
452
|
-
.map((sq) => () => synthAgent(ctx, sq, bucketsFor(retained, verdictByClaimId, sq.id))),
|
|
459
|
+
subQuestions.map((sq) => () => synthOrDegrade(ctx, sq, bucketsFor(retained, verdictByClaimId, sq.id))),
|
|
453
460
|
)).filter(Boolean);
|
|
454
461
|
}
|
|
455
462
|
|
|
463
|
+
// Synthesize one sub-question, degrading to a zero-claim finding when there is nothing
|
|
464
|
+
// to synthesize or the synthesizer fails to write a schema-valid finding — either way
|
|
465
|
+
// the SQ keeps its file, so the finding-per-SQ invariant holds and the coverage gate in
|
|
466
|
+
// the wrapper can see the full picture.
|
|
467
|
+
async function synthOrDegrade(ctx, sq, buckets) {
|
|
468
|
+
const hasClaims = buckets.verified.length || buckets.supporting.length || buckets.refuted.length;
|
|
469
|
+
if (hasClaims) {
|
|
470
|
+
const receipt = await synthAgent(ctx, sq, buckets);
|
|
471
|
+
if (receipt && receipt.written === true) return receipt;
|
|
472
|
+
log(`synth: ${sq.id} did not write a valid finding — writing a degraded finding to preserve the finding-per-SQ invariant`);
|
|
473
|
+
return degradedSynthAgent(ctx, sq, 'the synthesizer did not produce a schema-valid finding');
|
|
474
|
+
}
|
|
475
|
+
log(`synth: ${sq.id} had no retained claims — writing a degraded (zero-claim) finding to preserve the finding-per-SQ invariant`);
|
|
476
|
+
return degradedSynthAgent(ctx, sq, 'no claims survived ranking/verification for this sub-question');
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Pure: the exact degraded finding for a sub-question (fits the EXISTING finding schema —
|
|
480
|
+
// no store change). Empty quotes/sources/votes, confidence 'low', a self-describing
|
|
481
|
+
// summary, unsourced:false.
|
|
482
|
+
function degradedFinding(sq, reason) {
|
|
483
|
+
return {
|
|
484
|
+
subQuestionId: sq.id,
|
|
485
|
+
summary: `No verifiable findings were produced for this sub-question (${sq.text}). Reason: ${reason}. `
|
|
486
|
+
+ `This degraded finding is written to preserve the one-finding-per-sub-question invariant; treat the `
|
|
487
|
+
+ `sub-question as unanswered and re-run research for it if a complete answer is required.`,
|
|
488
|
+
quotes: [],
|
|
489
|
+
sources: [],
|
|
490
|
+
confidence: 'low',
|
|
491
|
+
verificationVotes: [],
|
|
492
|
+
unsourced: false,
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Have an agent write the degraded finding verbatim and round-trip it. Returns a synth
|
|
497
|
+
// receipt (claimCount 0) so the caller's aggregation (unsourced ratio, coverage stats)
|
|
498
|
+
// is unaffected. No brief needed — this is a mechanical verbatim write.
|
|
499
|
+
async function degradedSynthAgent(ctx, sq, reason) {
|
|
500
|
+
const finding = degradedFinding(sq, reason);
|
|
501
|
+
const path = `${ctx.sessionDir}/findings/${sq.id}.json`;
|
|
502
|
+
return await agent([
|
|
503
|
+
`You are writing a **degraded finding** for research session \`${ctx.sessionId}\`, sub-question \`${sq.id}\`.`,
|
|
504
|
+
`Write this EXACT JSON (verbatim, no edits) to \`${path}\`:`,
|
|
505
|
+
'```json', JSON.stringify(finding, null, 2), '```',
|
|
506
|
+
`Then round-trip: run \`${ctx.cli} show ${ctx.sessionId} --json\` and confirm exit 0 (it validates findings).`,
|
|
507
|
+
`Return { subQuestionId: "${sq.id}", written: true, path: "${path}", confidence: "low", claimCount: 0,`,
|
|
508
|
+
`centralConfirmed: 0, centralRefuted: 0, unsourcedCount: 0 } once the round-trip is green.`,
|
|
509
|
+
].join('\n'), { model: ctx.modelOverride || 'haiku', effort: 'low', phase: 'Synthesize', label: `synth-degraded:${sq.id}`, schema: SYNTH_SCHEMA });
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Pure coverage computation. Given the planner's sub-question ids and the synth
|
|
513
|
+
// receipts, decide which SQs have a finding (any receipt with written=true), which ran
|
|
514
|
+
// as a degraded write (empty finding, still counted as covered), and which are wholly
|
|
515
|
+
// uncovered (no receipt at all — the failure this gate exists to catch). Used by the
|
|
516
|
+
// wrapper to fail loudly instead of silently reporting status='done' on a partial run.
|
|
517
|
+
function coverageReport(subQuestionIds, synthReceipts) {
|
|
518
|
+
const receiptsBySq = new Map();
|
|
519
|
+
for (const r of synthReceipts || []) {
|
|
520
|
+
if (r && r.subQuestionId) receiptsBySq.set(r.subQuestionId, r);
|
|
521
|
+
}
|
|
522
|
+
const planned = subQuestionIds.length;
|
|
523
|
+
const covered = [];
|
|
524
|
+
const degraded = [];
|
|
525
|
+
const uncovered = [];
|
|
526
|
+
for (const id of subQuestionIds) {
|
|
527
|
+
const r = receiptsBySq.get(id);
|
|
528
|
+
if (!r || r.written !== true) { uncovered.push(id); continue; }
|
|
529
|
+
covered.push(id);
|
|
530
|
+
if ((r.claimCount || 0) === 0) degraded.push(id);
|
|
531
|
+
}
|
|
532
|
+
return { planned, covered, degraded, uncovered, complete: uncovered.length === 0 };
|
|
533
|
+
}
|
|
534
|
+
|
|
456
535
|
// -- Finalise (persist gate records, write sources.json + the session answer, round-trip, mark done) --
|
|
457
536
|
async function runFinalise(ctx, { gateRecords, sourcesJson, runStats }) {
|
|
458
537
|
// The gate-persistence step is present only when searchers actually dropped gated
|
|
@@ -584,6 +663,13 @@ function rankAndCapPerSq(claims, budget, { floorPerSq = 1 } = {}) {
|
|
|
584
663
|
|
|
585
664
|
const VERIFY_LENSES = ['quote-support', 'contradiction-search', 'recency-source-fit'];
|
|
586
665
|
|
|
666
|
+
// Hard per-verifier WebSearch/WebFetch cap. Workflow searchers observed to drain the
|
|
667
|
+
// PARENT session's shared WebSearch budget (w-2026-09-01-dave-010: 200/200 consumed
|
|
668
|
+
// on a normal run). The verifier brief already asks for "a few targeted calls"; this
|
|
669
|
+
// makes it a schema-enforceable ceiling the brief interpolates. Two calls per lens is
|
|
670
|
+
// enough to test one claim without re-researching the sub-question.
|
|
671
|
+
const VERIFY_TOOL_CALL_CAP_DEFAULT = 2;
|
|
672
|
+
|
|
587
673
|
// Decide one claim's verdict from its votes.
|
|
588
674
|
// - `unverified-infra` votes are infrastructure failures, not evidence — they
|
|
589
675
|
// count toward neither confirmation nor refutation.
|
|
@@ -1040,61 +1126,15 @@ async function recordRunStart(ctx) {
|
|
|
1040
1126
|
// claim, but a searcher/extractor that found nothing still leaves an empty SQ). Such an
|
|
1041
1127
|
// SQ gets a DEGRADED finding: schema-valid, confidence 'low', zero claims, and a summary
|
|
1042
1128
|
// explaining the absence. This closes the deep-judge's "only N of M sub-questions
|
|
1043
|
-
// answered" gap and keeps the finding count == the plan's sub-question count.
|
|
1129
|
+
// answered" gap and keeps the finding count == the plan's sub-question count. The
|
|
1130
|
+
// synthOrDegrade/degradedFinding/degradedSynthAgent primitives now live in finalise.mjs
|
|
1131
|
+
// (shared with normal — w-2026-09-01-dave-010 fix).
|
|
1044
1132
|
async function runDeepSynth(ctx, subQuestions, retained, verdictByClaimId) {
|
|
1045
1133
|
return (await parallel(
|
|
1046
1134
|
subQuestions.map((sq) => () => synthOrDegrade(ctx, sq, bucketsFor(retained, verdictByClaimId, sq.id))),
|
|
1047
1135
|
)).filter(Boolean);
|
|
1048
1136
|
}
|
|
1049
1137
|
|
|
1050
|
-
// Synthesize one sub-question, degrading to a zero-claim finding when there is nothing to
|
|
1051
|
-
// synthesize or the synthesizer fails to write a schema-valid finding — either way the SQ
|
|
1052
|
-
// keeps its file, so the invariant holds.
|
|
1053
|
-
async function synthOrDegrade(ctx, sq, buckets) {
|
|
1054
|
-
const hasClaims = buckets.verified.length || buckets.supporting.length || buckets.refuted.length;
|
|
1055
|
-
if (hasClaims) {
|
|
1056
|
-
const receipt = await synthAgent(ctx, sq, buckets);
|
|
1057
|
-
if (receipt && receipt.written === true) return receipt;
|
|
1058
|
-
log(`synth: ${sq.id} did not write a valid finding — writing a degraded finding to preserve the finding-per-SQ invariant`);
|
|
1059
|
-
return degradedSynthAgent(ctx, sq, 'the synthesizer did not produce a schema-valid finding');
|
|
1060
|
-
}
|
|
1061
|
-
log(`synth: ${sq.id} had no retained claims — writing a degraded (zero-claim) finding to preserve the finding-per-SQ invariant`);
|
|
1062
|
-
return degradedSynthAgent(ctx, sq, 'no claims survived ranking/verification for this sub-question');
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
// Pure: the exact degraded finding for a sub-question (fits the EXISTING finding schema —
|
|
1066
|
-
// no store change). Empty quotes/sources/votes, confidence 'low', a self-describing
|
|
1067
|
-
// summary, unsourced:false.
|
|
1068
|
-
function degradedFinding(sq, reason) {
|
|
1069
|
-
return {
|
|
1070
|
-
subQuestionId: sq.id,
|
|
1071
|
-
summary: `No verifiable findings were produced for this sub-question (${sq.text}). Reason: ${reason}. `
|
|
1072
|
-
+ `This degraded finding is written to preserve the one-finding-per-sub-question invariant; treat the `
|
|
1073
|
-
+ `sub-question as unanswered and re-run research for it if a complete answer is required.`,
|
|
1074
|
-
quotes: [],
|
|
1075
|
-
sources: [],
|
|
1076
|
-
confidence: 'low',
|
|
1077
|
-
verificationVotes: [],
|
|
1078
|
-
unsourced: false,
|
|
1079
|
-
};
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
// Have an agent write the degraded finding verbatim and round-trip it. Returns a synth
|
|
1083
|
-
// receipt (claimCount 0) so the caller's aggregation (unsourced ratio, run stats) is
|
|
1084
|
-
// unaffected. No brief needed — this is a mechanical verbatim write.
|
|
1085
|
-
async function degradedSynthAgent(ctx, sq, reason) {
|
|
1086
|
-
const finding = degradedFinding(sq, reason);
|
|
1087
|
-
const path = `${ctx.sessionDir}/findings/${sq.id}.json`;
|
|
1088
|
-
return await agent([
|
|
1089
|
-
`You are writing a **degraded finding** for deep-research session \`${ctx.sessionId}\`, sub-question \`${sq.id}\`.`,
|
|
1090
|
-
`Write this EXACT JSON (verbatim, no edits) to \`${path}\`:`,
|
|
1091
|
-
'```json', JSON.stringify(finding, null, 2), '```',
|
|
1092
|
-
`Then round-trip: run \`${ctx.cli} show ${ctx.sessionId} --json\` and confirm exit 0 (it validates findings).`,
|
|
1093
|
-
`Return { subQuestionId: "${sq.id}", written: true, path: "${path}", confidence: "low", claimCount: 0,`,
|
|
1094
|
-
`centralConfirmed: 0, centralRefuted: 0, unsourcedCount: 0 } once the round-trip is green.`,
|
|
1095
|
-
].join('\n'), { model: ctx.modelOverride || 'haiku', effort: 'low', phase: 'Synthesize', label: `synth-degraded:${sq.id}`, schema: SYNTH_SCHEMA });
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
1138
|
// ── judge phase (advisory — spec §2.3, arch §4.2 phase 7) ──────────────────────
|
|
1099
1139
|
const JUDGE_RECEIPT_SCHEMA = {
|
|
1100
1140
|
type: 'object', additionalProperties: false,
|