@tiangong-ai/cli 0.0.32 → 0.0.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/AGENTS.md +7 -2
- package/README.md +72 -19
- package/dist/research/commands.js +58 -1
- package/dist/research/commands.js.map +1 -1
- package/dist/research/orchestration.js +388 -31
- package/dist/research/orchestration.js.map +1 -1
- package/dist/research/workspace/acquisition.d.ts +71 -0
- package/dist/research/workspace/acquisition.js +593 -0
- package/dist/research/workspace/acquisition.js.map +1 -0
- package/dist/research/workspace/artifacts.d.ts +43 -0
- package/dist/research/workspace/artifacts.js +544 -0
- package/dist/research/workspace/artifacts.js.map +1 -0
- package/dist/research/workspace/broker.js +265 -56
- package/dist/research/workspace/broker.js.map +1 -1
- package/dist/research/workspace/constants.d.ts +1 -0
- package/dist/research/workspace/constants.js +4 -2
- package/dist/research/workspace/constants.js.map +1 -1
- package/dist/research/workspace/context.js +106 -11
- package/dist/research/workspace/context.js.map +1 -1
- package/dist/research/workspace/discovery-planning.d.ts +25 -0
- package/dist/research/workspace/discovery-planning.js +106 -0
- package/dist/research/workspace/discovery-planning.js.map +1 -0
- package/dist/research/workspace/discovery-status.d.ts +46 -0
- package/dist/research/workspace/discovery-status.js +183 -0
- package/dist/research/workspace/discovery-status.js.map +1 -0
- package/dist/research/workspace/discovery.d.ts +25 -0
- package/dist/research/workspace/discovery.js +268 -0
- package/dist/research/workspace/discovery.js.map +1 -0
- package/dist/research/workspace/downloads.d.ts +76 -0
- package/dist/research/workspace/downloads.js +274 -0
- package/dist/research/workspace/downloads.js.map +1 -0
- package/dist/research/workspace/evidence-ledger.d.ts +52 -0
- package/dist/research/workspace/evidence-ledger.js +487 -0
- package/dist/research/workspace/evidence-ledger.js.map +1 -0
- package/dist/research/workspace/evidence.d.ts +1 -0
- package/dist/research/workspace/evidence.js +2 -0
- package/dist/research/workspace/evidence.js.map +1 -1
- package/dist/research/workspace/input-plan.js +4 -0
- package/dist/research/workspace/input-plan.js.map +1 -1
- package/dist/research/workspace/native-activity.d.ts +65 -0
- package/dist/research/workspace/native-activity.js +153 -0
- package/dist/research/workspace/native-activity.js.map +1 -0
- package/dist/research/workspace/preflight.d.ts +5 -0
- package/dist/research/workspace/preflight.js +37 -17
- package/dist/research/workspace/preflight.js.map +1 -1
- package/dist/research/workspace/projects.d.ts +3 -1
- package/dist/research/workspace/projects.js +346 -5
- package/dist/research/workspace/projects.js.map +1 -1
- package/dist/research/workspace/runtime.d.ts +106 -4
- package/dist/research/workspace/runtime.js +947 -80
- package/dist/research/workspace/runtime.js.map +1 -1
- package/dist/research/workspace/sanitization.js +28 -6
- package/dist/research/workspace/sanitization.js.map +1 -1
- package/dist/research/workspace/schemas.d.ts +3 -0
- package/dist/research/workspace/schemas.js +206 -33
- package/dist/research/workspace/schemas.js.map +1 -1
- package/dist/research/workspace/setup-catalog.js +5 -5
- package/dist/research/workspace/setup-invocation.d.ts +11 -0
- package/dist/research/workspace/setup-invocation.js +34 -0
- package/dist/research/workspace/setup-invocation.js.map +1 -0
- package/dist/research/workspace/setup-wizard.js +10 -4
- package/dist/research/workspace/setup-wizard.js.map +1 -1
- package/dist/research/workspace/setup.d.ts +55 -7
- package/dist/research/workspace/setup.js +603 -32
- package/dist/research/workspace/setup.js.map +1 -1
- package/dist/research/workspace/types.d.ts +52 -3
- package/dist/research/workspace/workspace.js +45 -6
- package/dist/research/workspace/workspace.js.map +1 -1
- package/package.json +4 -2
|
@@ -4,17 +4,53 @@ import { basename, dirname, isAbsolute, join, relative, resolve } from "node:pat
|
|
|
4
4
|
import { CliError } from "../../errors.js";
|
|
5
5
|
import { loadCapabilityDeclarations, stageLockedCapabilities, verifyCapabilities, } from "./capabilities.js";
|
|
6
6
|
import { startCapabilityBroker } from "./broker.js";
|
|
7
|
+
import { commitAcquisitionAssessments, freezeEvidenceSnapshot, loadCurrentEvidenceSnapshot, loadImmutableEvidenceSnapshotChain, materializeAcquisitionAudit, parseMaterializedAcquisitionAudit, } from "./acquisition.js";
|
|
8
|
+
import { stageEvidenceArtifacts } from "./artifacts.js";
|
|
9
|
+
import { commitDiscoveryDecisions, materializeDiscoveryEvidence } from "./discovery.js";
|
|
10
|
+
import { inspectDiscoveryProgress } from "./discovery-status.js";
|
|
11
|
+
import { downloadBindingRecordSchema } from "./downloads.js";
|
|
7
12
|
import { loadProjectEvidenceReceipts, stageProjectEvidence } from "./evidence.js";
|
|
13
|
+
import { appendEvidenceLedgerEvent, evidenceLedgerPath, listEvidenceCandidates, registerProjectInputCandidates, } from "./evidence-ledger.js";
|
|
8
14
|
import { executeAgent } from "./executor.js";
|
|
9
15
|
import { requiredDiscoveryCapabilityIds } from "./external-skills.js";
|
|
10
16
|
import { renderInputLineContext } from "./input-plan.js";
|
|
11
17
|
import { appendJournalEvent, readJournal, verifyJournal } from "./journal.js";
|
|
18
|
+
import { nativeActivityRecordSchema } from "./native-activity.js";
|
|
12
19
|
import { calculateAgentCallTokenReservation, RESEARCH_BROKER_MAX_TURNS, RESEARCH_ESTIMATED_BYTES_PER_TOKEN, RESEARCH_MAX_REPAIR_SOURCE_BYTES, RESEARCH_REPAIR_MAX_TURNS, researchStructuredOutputMaxTurns, reservedAgentPackageCost, } from "./preflight.js";
|
|
13
20
|
import { listProjects, loadProject, nextReadyPackage, packageById, refreshProject, saveProject, } from "./projects.js";
|
|
14
21
|
import { configuredResearchSecrets, sanitizeResearchRecord, sanitizeResearchText, } from "./sanitization.js";
|
|
15
|
-
import { parseStructuredStageOutput, schemaForStage, StructuredOutputError } from "./schemas.js";
|
|
22
|
+
import { parseEvidenceRecord, schemaForDiscoveryAssessmentBatch, parseStructuredStageOutput, schemaForStage, StructuredOutputError, } from "./schemas.js";
|
|
16
23
|
import { canonicalJson, ensureDirectory, fileRecord, isObject, pathExists, readJsonFile, regularTreeFiles, resolveContained, sha256File, sha256Text, workspacePaths, writeJsonAtomic, writeTextAtomic, } from "./storage.js";
|
|
17
24
|
import { loadWorkspaceConfig, verifyDoctorAttestation, withWorkspaceLock } from "./workspace.js";
|
|
25
|
+
export const researchHandoffRecordSchema = {
|
|
26
|
+
$id: "https://schemas.tiangong.ai/research/handoff-request-v1.json",
|
|
27
|
+
type: "object",
|
|
28
|
+
additionalProperties: false,
|
|
29
|
+
required: ["schemaVersion", "state", "reasonCode", "summary", "requestedActions", "evidenceGaps"],
|
|
30
|
+
properties: {
|
|
31
|
+
schemaVersion: { type: "integer", const: 1 },
|
|
32
|
+
state: {
|
|
33
|
+
type: "string",
|
|
34
|
+
enum: ["user-action-required", "external-response-required"],
|
|
35
|
+
},
|
|
36
|
+
reasonCode: { type: "string", pattern: "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$" },
|
|
37
|
+
summary: { type: "string", minLength: 8, maxLength: 1_000 },
|
|
38
|
+
requestedActions: {
|
|
39
|
+
type: "array",
|
|
40
|
+
minItems: 1,
|
|
41
|
+
maxItems: 10,
|
|
42
|
+
uniqueItems: true,
|
|
43
|
+
items: { type: "string", minLength: 1, maxLength: 500 },
|
|
44
|
+
},
|
|
45
|
+
evidenceGaps: {
|
|
46
|
+
type: "array",
|
|
47
|
+
minItems: 1,
|
|
48
|
+
maxItems: 50,
|
|
49
|
+
uniqueItems: true,
|
|
50
|
+
items: { type: "string", minLength: 1, maxLength: 500 },
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
};
|
|
18
54
|
export async function runResearchWorkspace(root, options, packageExecutor = executeAgent) {
|
|
19
55
|
return runResearchWorkspaceInternal(root, options, packageExecutor, false);
|
|
20
56
|
}
|
|
@@ -86,6 +122,53 @@ async function runResearchWorkspaceInternal(root, options, packageExecutor, allo
|
|
|
86
122
|
return result;
|
|
87
123
|
});
|
|
88
124
|
}
|
|
125
|
+
export async function inspectNativeResearchStage(root, project) {
|
|
126
|
+
const path = nativeStageSessionPath(root, project.id);
|
|
127
|
+
if (!(await pathExists(path))) {
|
|
128
|
+
return {
|
|
129
|
+
status: "none",
|
|
130
|
+
sessionId: null,
|
|
131
|
+
stage: null,
|
|
132
|
+
preparedAt: null,
|
|
133
|
+
reasonCode: null,
|
|
134
|
+
recommendedAction: null,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
const session = await readNativeStageSession(root, project.id);
|
|
139
|
+
const workPackage = packageById(project, session.packet.packageId);
|
|
140
|
+
const actualBinding = await nativeStageBinding(root, project, workPackage);
|
|
141
|
+
const config = await loadWorkspaceConfig(root);
|
|
142
|
+
const elapsedSeconds = Math.max(0, (Date.now() - Date.parse(session.packet.preparedAt)) / 1_000);
|
|
143
|
+
const reasonCode = workPackage.status !== "running"
|
|
144
|
+
? "package-not-running"
|
|
145
|
+
: actualBinding !== session.packet.bindingSha256
|
|
146
|
+
? "binding-drift"
|
|
147
|
+
: elapsedSeconds > config.budget.packageMaxWallSeconds[session.packet.stage]
|
|
148
|
+
? "wall-time-expired"
|
|
149
|
+
: null;
|
|
150
|
+
return {
|
|
151
|
+
status: reasonCode ? "stale" : "active",
|
|
152
|
+
sessionId: session.packet.sessionId,
|
|
153
|
+
stage: session.packet.stage,
|
|
154
|
+
preparedAt: session.packet.preparedAt,
|
|
155
|
+
reasonCode,
|
|
156
|
+
recommendedAction: reasonCode
|
|
157
|
+
? `tiangong-ai research project stage abort ${project.id} --session ${session.packet.sessionId} --workspace ${root}`
|
|
158
|
+
: `Resume the current native ${session.packet.stage} stage and submit with the packet command.`,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
return {
|
|
163
|
+
status: "invalid",
|
|
164
|
+
sessionId: null,
|
|
165
|
+
stage: null,
|
|
166
|
+
preparedAt: null,
|
|
167
|
+
reasonCode: error instanceof CliError ? error.code : "RESEARCH_NATIVE_STAGE_SESSION_INVALID",
|
|
168
|
+
recommendedAction: "Inspect the invalid native session and use the explicit abort/retry recovery path; do not delete control files manually.",
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
}
|
|
89
172
|
export async function prepareNativeResearchStage(input) {
|
|
90
173
|
return withWorkspaceLock(input.root, "research.native-stage.prepare", async () => {
|
|
91
174
|
await verifyJournal(workspacePaths(input.root).journal);
|
|
@@ -113,6 +196,12 @@ export async function prepareNativeResearchStage(input) {
|
|
|
113
196
|
}
|
|
114
197
|
}
|
|
115
198
|
const project = await loadProject(input.root, input.projectId);
|
|
199
|
+
if (input.stage === "discover") {
|
|
200
|
+
await registerProjectInputCandidates(input.root, project.id, project.inputs);
|
|
201
|
+
}
|
|
202
|
+
if (input.stage === "analyze" || input.stage === "synthesize") {
|
|
203
|
+
await loadCurrentEvidenceSnapshot(input.root, project.id);
|
|
204
|
+
}
|
|
116
205
|
if (config.mode === "production-research" &&
|
|
117
206
|
config.budget.maxCostUsd > config.budget.confirmationCostUsd &&
|
|
118
207
|
!project.budgetConfirmedAt) {
|
|
@@ -150,21 +239,28 @@ export async function prepareNativeResearchStage(input) {
|
|
|
150
239
|
exitCode: 3,
|
|
151
240
|
});
|
|
152
241
|
}
|
|
153
|
-
const
|
|
242
|
+
const discovery = input.stage === "discover"
|
|
243
|
+
? await inspectDiscoveryProgress(input.root, project, config)
|
|
244
|
+
: null;
|
|
245
|
+
const reservedPackageTokens = discovery?.plan.reservedDiscoverTokens ?? config.budget.packageMaxTokens[input.stage];
|
|
246
|
+
const stageOutputTokens = discovery?.plan.outputTokenLimit ?? config.budget.maxOutputTokens;
|
|
247
|
+
const reservation = reservePackageBudget(project, workPackage, config, reservedPackageTokens);
|
|
154
248
|
const sessionId = randomUUID();
|
|
155
249
|
let capsule = null;
|
|
156
250
|
let preparedStatePersisted = false;
|
|
157
251
|
try {
|
|
158
252
|
capsule = await createCapsule(input.root, project, workPackage, sessionId, config);
|
|
159
|
-
const stageContextContent = await stageContextForPackage(capsule.projectRoot, workPackage, config);
|
|
253
|
+
const stageContextContent = await stageContextForPackage(capsule.projectRoot, project, workPackage, config);
|
|
160
254
|
const declarations = await loadCapabilityDeclarations(input.root);
|
|
161
255
|
const hasBrokeredEvidence = declarations.capabilities.some((capability) => capability.permissions.includes("brokered-network"));
|
|
162
|
-
const basePrompt = packagePrompt(project, workPackage, capsule.inputManifest, capsule.stagedSkills, capsule.capabilityDocumentation, null, capsule.contextBundle, capsule.contextBundleContent, stageContextContent,
|
|
256
|
+
const basePrompt = packagePrompt(project, workPackage, capsule.inputManifest, capsule.stagedSkills, capsule.capabilityDocumentation, null, capsule.contextBundle, capsule.contextBundleContent, stageContextContent, discovery, await listEvidenceCandidates(input.root, project.id));
|
|
163
257
|
const prompt = [
|
|
164
258
|
"Perform this producer stage in the current interactive host session. Do not launch codex exec, claude -p, or any other nested reasoning agent.",
|
|
165
259
|
input.stage === "discover" && hasBrokeredEvidence
|
|
166
|
-
? "
|
|
167
|
-
:
|
|
260
|
+
? "Use native Web/Browser broadly for discovery when useful, but record every native search/navigation with recordActivity and register its candidates. Before admitting any native lead, formalize the same URL or DOI through fetchEvidence so it receives an immutable broker receipt. Assess candidates in bounded batches with recordAssessment as they arrive; the final output is only a small coverage closeout. Native results without broker/input provenance are discovery leads, never evidence."
|
|
261
|
+
: input.stage === "acquire"
|
|
262
|
+
? "Acquire the provisionally admitted sources with the installed external acquisition/document Skills or an explicitly selected user-authorized browser. Capture the exact browser/adapter Download object, save it to the planned unique staging path, and call bindDownload before registerArtifact. Record browser/download/file-inspection activity with recordActivity. Failed or cancelled downloads cannot create bindings or artifacts. Never scan a download directory or infer success from file existence."
|
|
263
|
+
: "Do not acquire additional evidence in this stage.",
|
|
168
264
|
basePrompt,
|
|
169
265
|
"Save only the final schema-conforming JSON object to a new regular file, then submit it with the packet's submit command. The CLI remains the sole authority for validation and atomic promotion.",
|
|
170
266
|
].join("\n\n");
|
|
@@ -185,14 +281,50 @@ export async function prepareNativeResearchStage(input) {
|
|
|
185
281
|
outputSchema: schemaForStage(input.stage, null, input.stage === "discover" && !hasBrokeredEvidence
|
|
186
282
|
? { inputOnlyProvenanceIds: capsule.inputManifest.map((record) => record.id) }
|
|
187
283
|
: {}),
|
|
284
|
+
discovery,
|
|
188
285
|
limits: {
|
|
189
286
|
maxOutputBytes: config.budget.maxBytesPerPackage,
|
|
190
|
-
maxOutputTokens:
|
|
191
|
-
reservedPackageTokens
|
|
287
|
+
maxOutputTokens: stageOutputTokens,
|
|
288
|
+
reservedPackageTokens,
|
|
192
289
|
reservedMaxCostUsd: reservation.costUsd,
|
|
193
290
|
maxWallSeconds: config.budget.packageMaxWallSeconds[input.stage],
|
|
194
291
|
},
|
|
195
292
|
commands: {
|
|
293
|
+
requestHandoff: {
|
|
294
|
+
argv: [
|
|
295
|
+
"tiangong-ai",
|
|
296
|
+
"research",
|
|
297
|
+
"project",
|
|
298
|
+
"handoff",
|
|
299
|
+
"request",
|
|
300
|
+
project.id,
|
|
301
|
+
"--record",
|
|
302
|
+
"<absolute-handoff-record.json>",
|
|
303
|
+
"--workspace",
|
|
304
|
+
input.root,
|
|
305
|
+
"--json",
|
|
306
|
+
],
|
|
307
|
+
recordSchema: structuredClone(researchHandoffRecordSchema),
|
|
308
|
+
},
|
|
309
|
+
recordActivity: input.stage === "discover" || input.stage === "acquire"
|
|
310
|
+
? {
|
|
311
|
+
argv: [
|
|
312
|
+
"tiangong-ai",
|
|
313
|
+
"research",
|
|
314
|
+
"project",
|
|
315
|
+
"evidence",
|
|
316
|
+
"activity",
|
|
317
|
+
"record",
|
|
318
|
+
project.id,
|
|
319
|
+
"--record",
|
|
320
|
+
"<absolute-activity.json>",
|
|
321
|
+
"--workspace",
|
|
322
|
+
input.root,
|
|
323
|
+
"--json",
|
|
324
|
+
],
|
|
325
|
+
recordSchema: structuredClone(nativeActivityRecordSchema),
|
|
326
|
+
}
|
|
327
|
+
: null,
|
|
196
328
|
fetchEvidence: input.stage === "discover" && hasBrokeredEvidence
|
|
197
329
|
? {
|
|
198
330
|
argv: [
|
|
@@ -225,6 +357,122 @@ export async function prepareNativeResearchStage(input) {
|
|
|
225
357
|
},
|
|
226
358
|
}
|
|
227
359
|
: null,
|
|
360
|
+
registerArtifact: input.stage === "acquire"
|
|
361
|
+
? {
|
|
362
|
+
argv: [
|
|
363
|
+
"tiangong-ai",
|
|
364
|
+
"research",
|
|
365
|
+
"project",
|
|
366
|
+
"evidence",
|
|
367
|
+
"artifact",
|
|
368
|
+
"register",
|
|
369
|
+
project.id,
|
|
370
|
+
"--candidate",
|
|
371
|
+
"<candidate-id>",
|
|
372
|
+
"--path",
|
|
373
|
+
"<absolute-file>",
|
|
374
|
+
"--download-binding",
|
|
375
|
+
"<binding-id-for-network-file>",
|
|
376
|
+
"--media-type",
|
|
377
|
+
"<media-type>",
|
|
378
|
+
"--workspace",
|
|
379
|
+
input.root,
|
|
380
|
+
"--json",
|
|
381
|
+
],
|
|
382
|
+
supportedMediaTypes: [
|
|
383
|
+
"application/pdf",
|
|
384
|
+
"application/json",
|
|
385
|
+
"text/plain",
|
|
386
|
+
"text/markdown",
|
|
387
|
+
"text/csv",
|
|
388
|
+
"text/html",
|
|
389
|
+
"application/zip",
|
|
390
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
391
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
392
|
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
393
|
+
],
|
|
394
|
+
optionalMetadataFields: [
|
|
395
|
+
"download-binding",
|
|
396
|
+
"derived-from-artifact",
|
|
397
|
+
"source-url",
|
|
398
|
+
"license",
|
|
399
|
+
"license-url",
|
|
400
|
+
"host-type",
|
|
401
|
+
"article-version",
|
|
402
|
+
],
|
|
403
|
+
}
|
|
404
|
+
: null,
|
|
405
|
+
bindDownload: input.stage === "acquire"
|
|
406
|
+
? {
|
|
407
|
+
argv: [
|
|
408
|
+
"tiangong-ai",
|
|
409
|
+
"research",
|
|
410
|
+
"project",
|
|
411
|
+
"evidence",
|
|
412
|
+
"download",
|
|
413
|
+
"bind",
|
|
414
|
+
project.id,
|
|
415
|
+
"--candidate",
|
|
416
|
+
"<candidate-id>",
|
|
417
|
+
"--record",
|
|
418
|
+
"<absolute-download-record.json>",
|
|
419
|
+
"--workspace",
|
|
420
|
+
input.root,
|
|
421
|
+
"--json",
|
|
422
|
+
],
|
|
423
|
+
recordSchema: structuredClone(downloadBindingRecordSchema),
|
|
424
|
+
}
|
|
425
|
+
: null,
|
|
426
|
+
recordAssessment: input.stage === "discover"
|
|
427
|
+
? {
|
|
428
|
+
argv: [
|
|
429
|
+
"tiangong-ai",
|
|
430
|
+
"research",
|
|
431
|
+
"project",
|
|
432
|
+
"evidence",
|
|
433
|
+
"assessment",
|
|
434
|
+
"record",
|
|
435
|
+
project.id,
|
|
436
|
+
"--record",
|
|
437
|
+
"<absolute-assessment-batch.json>",
|
|
438
|
+
"--workspace",
|
|
439
|
+
input.root,
|
|
440
|
+
"--json",
|
|
441
|
+
],
|
|
442
|
+
recordSchema: schemaForDiscoveryAssessmentBatch(),
|
|
443
|
+
}
|
|
444
|
+
: null,
|
|
445
|
+
registerCandidate: input.stage === "discover"
|
|
446
|
+
? {
|
|
447
|
+
argv: [
|
|
448
|
+
"tiangong-ai",
|
|
449
|
+
"research",
|
|
450
|
+
"project",
|
|
451
|
+
"evidence",
|
|
452
|
+
"candidate",
|
|
453
|
+
"register",
|
|
454
|
+
project.id,
|
|
455
|
+
"--record",
|
|
456
|
+
"<absolute-candidate.json>",
|
|
457
|
+
"--workspace",
|
|
458
|
+
input.root,
|
|
459
|
+
"--json",
|
|
460
|
+
],
|
|
461
|
+
recordSchema: {
|
|
462
|
+
type: "object",
|
|
463
|
+
additionalProperties: false,
|
|
464
|
+
required: ["title"],
|
|
465
|
+
anyOf: [{ required: ["url"] }, { required: ["doi"] }],
|
|
466
|
+
properties: {
|
|
467
|
+
title: { type: "string" },
|
|
468
|
+
url: { type: "string", format: "uri" },
|
|
469
|
+
doi: { type: "string" },
|
|
470
|
+
publicationDate: { type: "string" },
|
|
471
|
+
excerpt: { type: "string" },
|
|
472
|
+
},
|
|
473
|
+
},
|
|
474
|
+
}
|
|
475
|
+
: null,
|
|
228
476
|
submit: {
|
|
229
477
|
argv: [
|
|
230
478
|
"tiangong-ai",
|
|
@@ -261,7 +509,10 @@ export async function prepareNativeResearchStage(input) {
|
|
|
261
509
|
},
|
|
262
510
|
rules: [
|
|
263
511
|
"Current native host performs producer reasoning; the CLI does not spawn a producer.",
|
|
512
|
+
"Native Web/Browser activity is visible in the evidence ledger but becomes admissible only after broker/input formalization and strict assessment.",
|
|
513
|
+
"When the next material step requires user authorization or an external response, request a durable handoff and stop; do not keep searching low-yield substitutes.",
|
|
264
514
|
"Only broker receipts or registered immutable inputs may support discover output.",
|
|
515
|
+
"Only exact, structurally validated, content-addressed artifacts may support full-text acquisition claims.",
|
|
265
516
|
"Do not place credentials, cookies, authorization data, or sensitive URL parameters in request/output files.",
|
|
266
517
|
"A file's existence is not success; submit performs schema, provenance, budget, hash, and atomic-commit checks.",
|
|
267
518
|
],
|
|
@@ -351,6 +602,14 @@ export async function submitNativeResearchStage(input) {
|
|
|
351
602
|
});
|
|
352
603
|
}
|
|
353
604
|
const project = await loadProject(input.root, input.projectId);
|
|
605
|
+
if (project.handoff.state !== "agent-actionable") {
|
|
606
|
+
throw new CliError("Native stage is paused for an unresolved project handoff.", {
|
|
607
|
+
code: "RESEARCH_PROJECT_HANDOFF_REQUIRED",
|
|
608
|
+
exitCode: 3,
|
|
609
|
+
details: { state: project.handoff.state, reasonCode: project.handoff.reasonCode },
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
await assertNoUnresolvedNativeChallenge(input.root, project.id);
|
|
354
613
|
const workPackage = packageById(project, session.packet.packageId);
|
|
355
614
|
if (workPackage.status !== "running" || workPackage.executor !== "producer") {
|
|
356
615
|
throw new CliError("The bound native producer package is no longer running.", {
|
|
@@ -363,7 +622,7 @@ export async function submitNativeResearchStage(input) {
|
|
|
363
622
|
await materializeAndValidateStageOutput(input.root, project, session.capsuleProject, workPackage, raw, null);
|
|
364
623
|
const elapsed = Math.max(0.001, (Date.now() - Date.parse(session.packet.preparedAt)) / 1_000);
|
|
365
624
|
const outputTokens = Math.ceil(Buffer.byteLength(raw, "utf8") / RESEARCH_ESTIMATED_BYTES_PER_TOKEN);
|
|
366
|
-
const reservedTokens =
|
|
625
|
+
const reservedTokens = session.packet.limits.reservedPackageTokens;
|
|
367
626
|
const result = {
|
|
368
627
|
exitCode: 0,
|
|
369
628
|
stdout: raw,
|
|
@@ -377,12 +636,22 @@ export async function submitNativeResearchStage(input) {
|
|
|
377
636
|
model: config.producer.model,
|
|
378
637
|
runtime: null,
|
|
379
638
|
};
|
|
380
|
-
assertActualPackageBudget(project, workPackage, config, result,
|
|
639
|
+
assertActualPackageBudget(project, workPackage, config, result, session.packet.limits.maxOutputTokens);
|
|
381
640
|
assertProjectedBudget(project, config, result);
|
|
382
641
|
if (workPackage.stage === "discover") {
|
|
383
|
-
await
|
|
642
|
+
await assertDiscoveryCoverage(input.root, project, resolveContained(session.capsuleProject, "outputs/evidence.json"));
|
|
384
643
|
}
|
|
385
644
|
const outputs = await validateAndImportOutputs(input.root, project, workPackage, session.capsuleProject, config, null);
|
|
645
|
+
if (workPackage.stage === "discover") {
|
|
646
|
+
await commitDiscoveryDecisions(input.root, project.id, parseStructuredStageOutput("discover", raw).value);
|
|
647
|
+
}
|
|
648
|
+
if (workPackage.stage === "acquire") {
|
|
649
|
+
const audit = parseMaterializedAcquisitionAudit(JSON.parse(await readFile(join(projectRoot(input.root, project.id), "outputs", "acquisition.json"), "utf8")));
|
|
650
|
+
await commitAcquisitionAssessments(input.root, project.id, audit);
|
|
651
|
+
await freezeEvidenceSnapshot(input.root, project);
|
|
652
|
+
outputs.push(await fileRecord(join(projectRoot(input.root, project.id), "outputs", "evidence-snapshot.json"), "outputs/evidence-snapshot.json"));
|
|
653
|
+
}
|
|
654
|
+
await commitStageEvidenceBindings(input.root, project, workPackage);
|
|
386
655
|
applyUsage(project, result);
|
|
387
656
|
workPackage.status = "complete";
|
|
388
657
|
workPackage.completedAt = new Date().toISOString();
|
|
@@ -445,6 +714,22 @@ export async function submitNativeResearchStage(input) {
|
|
|
445
714
|
}
|
|
446
715
|
});
|
|
447
716
|
}
|
|
717
|
+
async function assertNoUnresolvedNativeChallenge(root, projectId) {
|
|
718
|
+
const events = await readJournal(evidenceLedgerPath(root, projectId));
|
|
719
|
+
const lastChallengeIndex = events.findLastIndex((event) => event.type === "activity.recorded" &&
|
|
720
|
+
event.payload.status === "blocked" &&
|
|
721
|
+
event.payload.challenge !== "none");
|
|
722
|
+
if (lastChallengeIndex < 0)
|
|
723
|
+
return;
|
|
724
|
+
const lastResolutionIndex = events.findLastIndex((event) => event.type === "handoff.resolved");
|
|
725
|
+
if (lastResolutionIndex < lastChallengeIndex) {
|
|
726
|
+
throw new CliError("A login, MFA, CAPTCHA, paywall, security, or authorization challenge requires a durable user handoff before stage submission.", {
|
|
727
|
+
code: "RESEARCH_PROJECT_HANDOFF_REQUIRED",
|
|
728
|
+
exitCode: 3,
|
|
729
|
+
details: { challenge: events[lastChallengeIndex]?.payload.challenge ?? "unknown" },
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
}
|
|
448
733
|
export async function abortNativeResearchStage(input) {
|
|
449
734
|
return withWorkspaceLock(input.root, "research.native-stage.abort", async () => {
|
|
450
735
|
const session = await readNativeStageSession(input.root, input.projectId);
|
|
@@ -485,6 +770,159 @@ export async function abortNativeResearchStage(input) {
|
|
|
485
770
|
};
|
|
486
771
|
});
|
|
487
772
|
}
|
|
773
|
+
export async function requestResearchHandoff(input) {
|
|
774
|
+
return withWorkspaceLock(input.root, "research.handoff.request", async () => {
|
|
775
|
+
const value = parseHandoffRequest(input.value);
|
|
776
|
+
const project = refreshProject(await loadProject(input.root, input.projectId));
|
|
777
|
+
if (project.lineage.supersededBy ||
|
|
778
|
+
["complete", "stale", "archived", "abandoned"].includes(project.status)) {
|
|
779
|
+
throw new CliError("Historical or closed projects require an immutable addendum, not an in-place handoff.", { code: "RESEARCH_PROJECT_HANDOFF_INVALID", exitCode: 3 });
|
|
780
|
+
}
|
|
781
|
+
if (project.handoff.state !== "agent-actionable") {
|
|
782
|
+
throw new CliError("This project already has an unresolved handoff.", {
|
|
783
|
+
code: "RESEARCH_PROJECT_HANDOFF_REQUIRED",
|
|
784
|
+
exitCode: 3,
|
|
785
|
+
details: { state: project.handoff.state },
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
const activePath = nativeStageSessionPath(input.root, input.projectId);
|
|
789
|
+
const session = (await pathExists(activePath))
|
|
790
|
+
? await readNativeStageSession(input.root, input.projectId)
|
|
791
|
+
: null;
|
|
792
|
+
if (session) {
|
|
793
|
+
const workPackage = packageById(project, session.packet.packageId);
|
|
794
|
+
if (workPackage.status !== "running") {
|
|
795
|
+
throw new CliError("Active native session is not bound to a running package.", {
|
|
796
|
+
code: "RESEARCH_NATIVE_STAGE_SESSION_MISMATCH",
|
|
797
|
+
exitCode: 3,
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
workPackage.status = "ready";
|
|
801
|
+
workPackage.attempts = Math.max(0, workPackage.attempts - 1);
|
|
802
|
+
workPackage.startedAt = null;
|
|
803
|
+
workPackage.completedAt = null;
|
|
804
|
+
workPackage.lastError = null;
|
|
805
|
+
workPackage.lastFailureKind = null;
|
|
806
|
+
workPackage.retryNotBefore = null;
|
|
807
|
+
}
|
|
808
|
+
const requestedAt = new Date().toISOString();
|
|
809
|
+
project.handoff = {
|
|
810
|
+
state: value.state,
|
|
811
|
+
reasonCode: value.reasonCode,
|
|
812
|
+
summary: value.summary,
|
|
813
|
+
requestedActions: value.requestedActions,
|
|
814
|
+
evidenceGaps: value.evidenceGaps,
|
|
815
|
+
requestedAt,
|
|
816
|
+
resolvedAt: null,
|
|
817
|
+
resolutionNote: null,
|
|
818
|
+
};
|
|
819
|
+
refreshProject(project);
|
|
820
|
+
await saveProject(input.root, project);
|
|
821
|
+
const eventPayload = {
|
|
822
|
+
state: value.state,
|
|
823
|
+
reasonCode: value.reasonCode,
|
|
824
|
+
summary: value.summary,
|
|
825
|
+
requestedActions: value.requestedActions,
|
|
826
|
+
evidenceGaps: value.evidenceGaps,
|
|
827
|
+
requestedAt,
|
|
828
|
+
interruptedSessionId: session?.packet.sessionId ?? null,
|
|
829
|
+
interruptedPackageId: session?.packet.packageId ?? null,
|
|
830
|
+
};
|
|
831
|
+
await appendEvidenceLedgerEvent(input.root, project.id, "handoff.requested", eventPayload);
|
|
832
|
+
await appendJournalEvent(workspacePaths(input.root).journal, "project.handoff.requested", project.id, { projectId: project.id, ...eventPayload });
|
|
833
|
+
if (session) {
|
|
834
|
+
await rm(activePath, { force: true });
|
|
835
|
+
await rm(session.capsuleRoot, { recursive: true, force: true });
|
|
836
|
+
}
|
|
837
|
+
return {
|
|
838
|
+
projectId: project.id,
|
|
839
|
+
status: project.status === "waiting-external" ? "waiting-external" : "waiting-user",
|
|
840
|
+
handoff: project.handoff,
|
|
841
|
+
resolveCommand: `tiangong-ai research project handoff resolve ${project.id} --note <resolution-note> --workspace ${input.root}`,
|
|
842
|
+
};
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
export async function resolveResearchHandoff(input) {
|
|
846
|
+
return withWorkspaceLock(input.root, "research.handoff.resolve", async () => {
|
|
847
|
+
const project = await loadProject(input.root, input.projectId);
|
|
848
|
+
if (project.handoff.state === "agent-actionable" || !project.handoff.requestedAt) {
|
|
849
|
+
throw new CliError("Project has no unresolved handoff.", {
|
|
850
|
+
code: "RESEARCH_PROJECT_HANDOFF_INVALID",
|
|
851
|
+
exitCode: 2,
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
const note = sanitizeResearchText(input.note, configuredResearchSecrets(process.env)).trim();
|
|
855
|
+
if (note.length < 8 || note.length > 1_000) {
|
|
856
|
+
throw new CliError("Handoff resolution note must contain 8-1000 safe characters.", {
|
|
857
|
+
code: "RESEARCH_PROJECT_HANDOFF_INVALID",
|
|
858
|
+
exitCode: 2,
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
const previousState = project.handoff.state;
|
|
862
|
+
const resolvedAt = new Date().toISOString();
|
|
863
|
+
project.handoff = {
|
|
864
|
+
...project.handoff,
|
|
865
|
+
state: "agent-actionable",
|
|
866
|
+
resolvedAt,
|
|
867
|
+
resolutionNote: note,
|
|
868
|
+
};
|
|
869
|
+
refreshProject(project);
|
|
870
|
+
await saveProject(input.root, project);
|
|
871
|
+
const eventPayload = {
|
|
872
|
+
previousState,
|
|
873
|
+
reasonCode: project.handoff.reasonCode,
|
|
874
|
+
requestedAt: project.handoff.requestedAt,
|
|
875
|
+
resolvedAt,
|
|
876
|
+
resolutionNote: note,
|
|
877
|
+
};
|
|
878
|
+
await appendEvidenceLedgerEvent(input.root, project.id, "handoff.resolved", eventPayload);
|
|
879
|
+
await appendJournalEvent(workspacePaths(input.root).journal, "project.handoff.resolved", project.id, { projectId: project.id, ...eventPayload });
|
|
880
|
+
return { projectId: project.id, status: project.status, handoff: project.handoff };
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
function parseHandoffRequest(value) {
|
|
884
|
+
const sanitized = sanitizeResearchRecord(value, configuredResearchSecrets(process.env));
|
|
885
|
+
const allowed = new Set([
|
|
886
|
+
"schemaVersion",
|
|
887
|
+
"state",
|
|
888
|
+
"reasonCode",
|
|
889
|
+
"summary",
|
|
890
|
+
"requestedActions",
|
|
891
|
+
"evidenceGaps",
|
|
892
|
+
]);
|
|
893
|
+
const requestedActions = Array.isArray(sanitized.requestedActions)
|
|
894
|
+
? sanitized.requestedActions
|
|
895
|
+
: [];
|
|
896
|
+
const evidenceGaps = Array.isArray(sanitized.evidenceGaps) ? sanitized.evidenceGaps : [];
|
|
897
|
+
if (Object.keys(sanitized).some((key) => !allowed.has(key)) ||
|
|
898
|
+
sanitized.schemaVersion !== 1 ||
|
|
899
|
+
!["user-action-required", "external-response-required"].includes(String(sanitized.state)) ||
|
|
900
|
+
typeof sanitized.reasonCode !== "string" ||
|
|
901
|
+
!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(sanitized.reasonCode) ||
|
|
902
|
+
typeof sanitized.summary !== "string" ||
|
|
903
|
+
sanitized.summary.trim().length < 8 ||
|
|
904
|
+
sanitized.summary.length > 1_000 ||
|
|
905
|
+
requestedActions.length < 1 ||
|
|
906
|
+
requestedActions.length > 10 ||
|
|
907
|
+
requestedActions.some((item) => typeof item !== "string" || item.length < 1 || item.length > 500) ||
|
|
908
|
+
new Set(requestedActions).size !== requestedActions.length ||
|
|
909
|
+
evidenceGaps.length < 1 ||
|
|
910
|
+
evidenceGaps.length > 50 ||
|
|
911
|
+
evidenceGaps.some((item) => typeof item !== "string" || item.length < 1 || item.length > 500) ||
|
|
912
|
+
new Set(evidenceGaps).size !== evidenceGaps.length) {
|
|
913
|
+
throw new CliError("Handoff request failed validation.", {
|
|
914
|
+
code: "RESEARCH_PROJECT_HANDOFF_INVALID",
|
|
915
|
+
exitCode: 2,
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
return {
|
|
919
|
+
state: sanitized.state,
|
|
920
|
+
reasonCode: sanitized.reasonCode,
|
|
921
|
+
summary: sanitized.summary.trim(),
|
|
922
|
+
requestedActions: requestedActions,
|
|
923
|
+
evidenceGaps: evidenceGaps,
|
|
924
|
+
};
|
|
925
|
+
}
|
|
488
926
|
async function executeWorkPackage(root, projectId, packageId, config, options, requestId, packageExecutor, doctorAttestation) {
|
|
489
927
|
const project = await loadProject(root, projectId);
|
|
490
928
|
const workPackage = packageById(project, packageId);
|
|
@@ -521,7 +959,14 @@ async function executeWorkPackage(root, projectId, packageId, config, options, r
|
|
|
521
959
|
promotedOutputs = await outputRecords(root, project, workPackage.expectedOutputs);
|
|
522
960
|
}
|
|
523
961
|
else {
|
|
524
|
-
|
|
962
|
+
if (["analyze", "synthesize", "review"].includes(workPackage.stage)) {
|
|
963
|
+
await loadCurrentEvidenceSnapshot(root, project.id);
|
|
964
|
+
}
|
|
965
|
+
const discovery = workPackage.stage === "discover"
|
|
966
|
+
? await inspectDiscoveryProgress(root, project, config)
|
|
967
|
+
: null;
|
|
968
|
+
const stageOutputTokens = discovery?.plan.outputTokenLimit ?? config.budget.maxOutputTokens;
|
|
969
|
+
const reservation = reservePackageBudget(project, workPackage, config, discovery?.plan.reservedDiscoverTokens);
|
|
525
970
|
const capsule = await createCapsule(root, project, workPackage, runId, config);
|
|
526
971
|
capsuleRoot = capsule.capsuleRoot;
|
|
527
972
|
if (capsule.reviewPacketRecord) {
|
|
@@ -533,7 +978,7 @@ async function executeWorkPackage(root, projectId, packageId, config, options, r
|
|
|
533
978
|
packet: capsule.reviewPacketRecord,
|
|
534
979
|
});
|
|
535
980
|
}
|
|
536
|
-
const stageContextContent = await stageContextForPackage(capsule.projectRoot, workPackage, config);
|
|
981
|
+
const stageContextContent = await stageContextForPackage(capsule.projectRoot, project, workPackage, config);
|
|
537
982
|
const route = workPackage.executor === "reviewer" ? config.reviewer : config.producer;
|
|
538
983
|
executor = route.agent;
|
|
539
984
|
broker =
|
|
@@ -552,10 +997,11 @@ async function executeWorkPackage(root, projectId, packageId, config, options, r
|
|
|
552
997
|
options,
|
|
553
998
|
requestId,
|
|
554
999
|
purpose: "primary",
|
|
555
|
-
prompt: packagePrompt(project, workPackage, capsule.inputManifest, capsule.stagedSkills, capsule.capabilityDocumentation, capsule.reviewPacketSha256, capsule.contextBundle, capsule.contextBundleContent, stageContextContent,
|
|
1000
|
+
prompt: packagePrompt(project, workPackage, capsule.inputManifest, capsule.stagedSkills, capsule.capabilityDocumentation, capsule.reviewPacketSha256, capsule.contextBundle, capsule.contextBundleContent, stageContextContent, discovery, await listEvidenceCandidates(root, project.id)),
|
|
556
1001
|
brokerUrl: primaryBrokerUrl,
|
|
557
1002
|
inputOnlyProvenance,
|
|
558
|
-
maxOutputTokens: Math.min(
|
|
1003
|
+
maxOutputTokens: Math.min(stageOutputTokens, reservation.tokens),
|
|
1004
|
+
...(discovery ? { brokerCallBudget: discovery.plan.maxCalls } : {}),
|
|
559
1005
|
maxCostUsd: reservation.costUsd,
|
|
560
1006
|
expectedRuntime: runtimeForRoute(doctorAttestation, route),
|
|
561
1007
|
});
|
|
@@ -563,7 +1009,8 @@ async function executeWorkPackage(root, projectId, packageId, config, options, r
|
|
|
563
1009
|
result = await withHeartbeat(packageExecutor(primaryRequest), options, requestId, project, workPackage, config);
|
|
564
1010
|
accountedResult = result;
|
|
565
1011
|
assertExecutorSucceeded(result);
|
|
566
|
-
assertActualPackageBudget(project, workPackage, config, result,
|
|
1012
|
+
assertActualPackageBudget(project, workPackage, config, result, stageOutputTokens);
|
|
1013
|
+
let acceptedRaw = result.stdout;
|
|
567
1014
|
try {
|
|
568
1015
|
await materializeAndValidateStageOutput(root, project, capsule.projectRoot, workPackage, result.stdout, capsule.reviewPacketSha256);
|
|
569
1016
|
}
|
|
@@ -596,14 +1043,23 @@ async function executeWorkPackage(root, projectId, packageId, config, options, r
|
|
|
596
1043
|
const repair = await withHeartbeat(packageExecutor(repairRequest), options, requestId, project, workPackage, config);
|
|
597
1044
|
accountedResult = combineExecutionResults(result, repair);
|
|
598
1045
|
assertExecutorSucceeded(repair);
|
|
599
|
-
assertActualPackageBudget(project, workPackage, config, accountedResult,
|
|
1046
|
+
assertActualPackageBudget(project, workPackage, config, accountedResult, stageOutputTokens + config.budget.maxRepairTokens);
|
|
1047
|
+
acceptedRaw = repair.stdout;
|
|
600
1048
|
await materializeAndValidateStageOutput(root, project, capsule.projectRoot, workPackage, repair.stdout, capsule.reviewPacketSha256);
|
|
601
1049
|
}
|
|
602
1050
|
assertProjectedBudget(project, config, accountedResult);
|
|
603
1051
|
promotedOutputs = await validateAndImportOutputs(root, project, workPackage, capsule.projectRoot, config, capsule.reviewPacketSha256);
|
|
604
1052
|
if (workPackage.stage === "discover") {
|
|
605
|
-
await
|
|
1053
|
+
await assertDiscoveryCoverage(root, project);
|
|
1054
|
+
await commitDiscoveryDecisions(root, project.id, parseStructuredStageOutput("discover", acceptedRaw).value);
|
|
606
1055
|
}
|
|
1056
|
+
if (workPackage.stage === "acquire") {
|
|
1057
|
+
const audit = parseMaterializedAcquisitionAudit(JSON.parse(await readFile(join(projectRoot(root, project.id), "outputs", "acquisition.json"), "utf8")));
|
|
1058
|
+
await commitAcquisitionAssessments(root, project.id, audit);
|
|
1059
|
+
await freezeEvidenceSnapshot(root, project);
|
|
1060
|
+
promotedOutputs.push(await fileRecord(join(projectRoot(root, project.id), "outputs", "evidence-snapshot.json"), "outputs/evidence-snapshot.json"));
|
|
1061
|
+
}
|
|
1062
|
+
await commitStageEvidenceBindings(root, project, workPackage);
|
|
607
1063
|
}
|
|
608
1064
|
const completedAt = new Date().toISOString();
|
|
609
1065
|
applyUsage(project, accountedResult);
|
|
@@ -809,6 +1265,16 @@ async function createCapsule(root, project, workPackage, runId, config) {
|
|
|
809
1265
|
await writeTextAtomic(contextBundlePath, contextBundleContent);
|
|
810
1266
|
const contextBundle = await fileRecord(contextBundlePath, "inputs/context-bundle.txt");
|
|
811
1267
|
const evidenceReceipts = await stageProjectEvidence(root, project.id, capsuleProject);
|
|
1268
|
+
const frozenSnapshot = ["analyze", "synthesize", "review"].includes(workPackage.stage)
|
|
1269
|
+
? await loadCurrentEvidenceSnapshot(root, project.id)
|
|
1270
|
+
: null;
|
|
1271
|
+
const evidenceArtifacts = frozenSnapshot
|
|
1272
|
+
? await stageEvidenceArtifacts(root, project.id, capsuleProject, new Set(frozenSnapshot.artifacts.map((artifact) => artifact.artifactId)))
|
|
1273
|
+
: [];
|
|
1274
|
+
if ((workPackage.stage === "analyze" || workPackage.stage === "synthesize") &&
|
|
1275
|
+
evidenceArtifacts.length) {
|
|
1276
|
+
await writeProducerArtifactContext(capsuleProject, evidenceArtifacts, Math.floor((config.budget.maxInputContextTokens * RESEARCH_ESTIMATED_BYTES_PER_TOKEN) / 2));
|
|
1277
|
+
}
|
|
812
1278
|
await writeJsonAtomic(join(capsuleProject, "inputs", "evidence-receipts.json"), evidenceReceipts.map(reviewSafeReceipt));
|
|
813
1279
|
await writeJsonAtomic(join(capsuleProject, "project.json"), {
|
|
814
1280
|
...project,
|
|
@@ -821,10 +1287,10 @@ async function createCapsule(root, project, workPackage, runId, config) {
|
|
|
821
1287
|
const stagedSkills = await stageLockedCapabilities(root, join(capsuleProject, "skills"));
|
|
822
1288
|
const capabilityDocumentation = await buildCapabilityDocumentation(capsuleProject, stagedSkills, config.budget.maxInputContextTokens * RESEARCH_ESTIMATED_BYTES_PER_TOKEN);
|
|
823
1289
|
const reviewEvidenceContext = workPackage.stage === "review"
|
|
824
|
-
? await writeReviewEvidenceContext(root, project.id, capsuleProject, contextBundleContent, evidenceReceipts, config.budget.maxInputContextTokens * RESEARCH_ESTIMATED_BYTES_PER_TOKEN)
|
|
1290
|
+
? await writeReviewEvidenceContext(root, project.id, capsuleProject, contextBundleContent, evidenceReceipts, evidenceArtifacts, config.budget.maxInputContextTokens * RESEARCH_ESTIMATED_BYTES_PER_TOKEN)
|
|
825
1291
|
: null;
|
|
826
1292
|
const reviewPacket = reviewEvidenceContext
|
|
827
|
-
? await writeReviewPacket(root, capsuleProject, project, inputManifest, evidenceReceipts, reviewEvidenceContext.persistent)
|
|
1293
|
+
? await writeReviewPacket(root, capsuleProject, project, inputManifest, evidenceReceipts, evidenceArtifacts, reviewEvidenceContext.persistent)
|
|
828
1294
|
: null;
|
|
829
1295
|
return {
|
|
830
1296
|
capsuleRoot,
|
|
@@ -882,8 +1348,21 @@ async function buildInputContextBundle(capsuleProject, inputManifest) {
|
|
|
882
1348
|
}
|
|
883
1349
|
return `${sections.join("\n\n")}\n`;
|
|
884
1350
|
}
|
|
885
|
-
async function writeReviewPacket(root, capsuleProject, project, inputManifest, evidenceReceipts, reviewEvidenceContext) {
|
|
886
|
-
const
|
|
1351
|
+
async function writeReviewPacket(root, capsuleProject, project, inputManifest, evidenceReceipts, evidenceArtifacts, reviewEvidenceContext) {
|
|
1352
|
+
const snapshot = await loadCurrentEvidenceSnapshot(root, project.id);
|
|
1353
|
+
const immutableSnapshots = await loadImmutableEvidenceSnapshotChain(root, project.id, snapshot.snapshotSha256);
|
|
1354
|
+
const snapshotChain = await Promise.all(immutableSnapshots.map((immutableSnapshot) => {
|
|
1355
|
+
const sha256 = immutableSnapshot.snapshotSha256;
|
|
1356
|
+
const logicalPath = `evidence/snapshots/${sha256}.json`;
|
|
1357
|
+
return fileRecord(resolveContained(projectRoot(root, project.id), logicalPath), logicalPath);
|
|
1358
|
+
}));
|
|
1359
|
+
const artifactPaths = [
|
|
1360
|
+
"outputs/evidence.json",
|
|
1361
|
+
"outputs/acquisition.json",
|
|
1362
|
+
"outputs/evidence-snapshot.json",
|
|
1363
|
+
"outputs/analysis.json",
|
|
1364
|
+
"outputs/report.md",
|
|
1365
|
+
];
|
|
887
1366
|
const evidenceFiles = new Map();
|
|
888
1367
|
for (const receipt of evidenceReceipts) {
|
|
889
1368
|
for (const locator of [receipt.locator, receipt.contextLocator]) {
|
|
@@ -892,6 +1371,11 @@ async function writeReviewPacket(root, capsuleProject, project, inputManifest, e
|
|
|
892
1371
|
}
|
|
893
1372
|
}
|
|
894
1373
|
}
|
|
1374
|
+
for (const artifact of evidenceArtifacts) {
|
|
1375
|
+
if (!evidenceFiles.has(artifact.locator)) {
|
|
1376
|
+
evidenceFiles.set(artifact.locator, await fileRecord(resolveContained(capsuleProject, artifact.locator), artifact.locator));
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
895
1379
|
const environment = await reviewEnvironmentPacket(root, project.id);
|
|
896
1380
|
const inputFiles = new Map();
|
|
897
1381
|
for (const input of inputManifest) {
|
|
@@ -907,10 +1391,18 @@ async function writeReviewPacket(root, capsuleProject, project, inputManifest, e
|
|
|
907
1391
|
projectId: project.id,
|
|
908
1392
|
questionSha256: sha256Text(project.question),
|
|
909
1393
|
evidenceRequirements: project.evidenceRequirements,
|
|
1394
|
+
evidenceSnapshot: {
|
|
1395
|
+
snapshotId: snapshot.snapshotId,
|
|
1396
|
+
snapshotSha256: snapshot.snapshotSha256,
|
|
1397
|
+
parentSnapshotId: snapshot.parentSnapshotId,
|
|
1398
|
+
parentSnapshotSha256: snapshot.parentSnapshotSha256,
|
|
1399
|
+
},
|
|
1400
|
+
snapshotChain,
|
|
910
1401
|
inputs: inputManifest,
|
|
911
1402
|
reviewEvidenceContext,
|
|
912
1403
|
inputFiles: [...inputFiles.values()].sort((left, right) => left.path.localeCompare(right.path)),
|
|
913
1404
|
evidenceReceipts: evidenceReceipts.map(reviewSafeReceipt),
|
|
1405
|
+
evidenceArtifacts,
|
|
914
1406
|
evidenceFiles: [...evidenceFiles.values()].sort((left, right) => left.path.localeCompare(right.path)),
|
|
915
1407
|
environment,
|
|
916
1408
|
environmentFile: await fileRecord(join(capsuleProject, "inputs", "runtime-fingerprint.json"), "inputs/runtime-fingerprint.json"),
|
|
@@ -925,7 +1417,7 @@ async function writeReviewPacket(root, capsuleProject, project, inputManifest, e
|
|
|
925
1417
|
const record = await persistReviewPacket(root, project.id, completePacket, packetSha256);
|
|
926
1418
|
return { sha256: packetSha256, record };
|
|
927
1419
|
}
|
|
928
|
-
async function writeReviewEvidenceContext(root, projectId, capsuleProject, inputContextBundle, evidenceReceipts, maxBytes) {
|
|
1420
|
+
async function writeReviewEvidenceContext(root, projectId, capsuleProject, inputContextBundle, evidenceReceipts, evidenceArtifacts, maxBytes) {
|
|
929
1421
|
const header = [
|
|
930
1422
|
"TIANGONG REVIEW EVIDENCE CONTEXT v1",
|
|
931
1423
|
"The following are deterministic excerpts from hash-verified bounded views. Full objects and original bounded contexts remain bound in the review packet.",
|
|
@@ -939,13 +1431,25 @@ async function writeReviewEvidenceContext(root, projectId, capsuleProject, input
|
|
|
939
1431
|
active: true,
|
|
940
1432
|
},
|
|
941
1433
|
];
|
|
942
|
-
const
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
1434
|
+
const seenUnreferencedContexts = new Set();
|
|
1435
|
+
const orderedReceipts = [...evidenceReceipts].sort((left, right) => {
|
|
1436
|
+
const leftReferenced = (brokerReferences.get(left.attemptId)?.length ?? 0) > 0;
|
|
1437
|
+
const rightReferenced = (brokerReferences.get(right.attemptId)?.length ?? 0) > 0;
|
|
1438
|
+
if (leftReferenced !== rightReferenced)
|
|
1439
|
+
return leftReferenced ? -1 : 1;
|
|
1440
|
+
return left.attemptId.localeCompare(right.attemptId);
|
|
1441
|
+
});
|
|
1442
|
+
for (const receipt of orderedReceipts) {
|
|
947
1443
|
const metadata = reviewSafeReceipt(receipt);
|
|
948
1444
|
const references = brokerReferences.get(receipt.attemptId) ?? [];
|
|
1445
|
+
// Every admitted receipt gets its own exact projection even when another
|
|
1446
|
+
// request produced identical bounded bytes. Only uncited duplicate views
|
|
1447
|
+
// may be collapsed; otherwise random receipt UUID ordering can hide the
|
|
1448
|
+
// one receipt actually cited by evidence.json.
|
|
1449
|
+
if (references.length === 0 && seenUnreferencedContexts.has(receipt.contextLocator))
|
|
1450
|
+
continue;
|
|
1451
|
+
if (references.length === 0)
|
|
1452
|
+
seenUnreferencedContexts.add(receipt.contextLocator);
|
|
949
1453
|
const content = references.length
|
|
950
1454
|
? await citedBrokerReviewContent(capsuleProject, receipt, references)
|
|
951
1455
|
: "[No admitted evidence source cites this receipt; its raw object and bounded context remain hash-bound in the review packet.]";
|
|
@@ -961,6 +1465,33 @@ async function writeReviewEvidenceContext(root, projectId, capsuleProject, input
|
|
|
961
1465
|
active: references.length > 0,
|
|
962
1466
|
});
|
|
963
1467
|
}
|
|
1468
|
+
for (const artifact of [...evidenceArtifacts].sort((left, right) => left.artifactId.localeCompare(right.artifactId))) {
|
|
1469
|
+
const textArtifact = reviewableTextContentType(artifact.mediaType);
|
|
1470
|
+
const content = textArtifact
|
|
1471
|
+
? sanitizeResearchText((await readFile(resolveContained(capsuleProject, artifact.locator), "utf8")).trimEnd())
|
|
1472
|
+
: "[Binary artifact omitted from model context; the complete structurally validated file remains hash-bound in the persistent review packet.]";
|
|
1473
|
+
const safeMetadata = {
|
|
1474
|
+
artifactId: artifact.artifactId,
|
|
1475
|
+
candidateId: artifact.candidateId,
|
|
1476
|
+
sha256: artifact.sha256,
|
|
1477
|
+
bytes: artifact.bytes,
|
|
1478
|
+
mediaType: artifact.mediaType,
|
|
1479
|
+
originalFilename: artifact.originalFilename,
|
|
1480
|
+
locator: artifact.locator,
|
|
1481
|
+
validation: artifact.validation,
|
|
1482
|
+
};
|
|
1483
|
+
views.push({
|
|
1484
|
+
prefix: [
|
|
1485
|
+
`--- FROZEN EVIDENCE ARTIFACT ${artifact.artifactId} ---`,
|
|
1486
|
+
`metadata: ${JSON.stringify(safeMetadata)}`,
|
|
1487
|
+
"--- BEGIN BOUNDED REVIEW EXCERPT ---",
|
|
1488
|
+
"",
|
|
1489
|
+
].join("\n"),
|
|
1490
|
+
content,
|
|
1491
|
+
suffix: "\n--- END BOUNDED REVIEW EXCERPT ---",
|
|
1492
|
+
active: textArtifact,
|
|
1493
|
+
});
|
|
1494
|
+
}
|
|
964
1495
|
const fixedContent = [
|
|
965
1496
|
header,
|
|
966
1497
|
...views.map((view) => `${view.prefix}${view.active ? "" : view.content}${view.suffix}`),
|
|
@@ -974,7 +1505,7 @@ async function writeReviewEvidenceContext(root, projectId, capsuleProject, input
|
|
|
974
1505
|
});
|
|
975
1506
|
}
|
|
976
1507
|
const activeViews = views.filter((view) => view.active).length;
|
|
977
|
-
const contentBudgetPerView = Math.floor((maxBytes - fixedBytes) / activeViews);
|
|
1508
|
+
const contentBudgetPerView = activeViews ? Math.floor((maxBytes - fixedBytes) / activeViews) : 0;
|
|
978
1509
|
const sections = [
|
|
979
1510
|
header,
|
|
980
1511
|
...views.map((view) => `${view.prefix}${view.active ? boundedUtf8ReviewExcerpt(view.content, contentBudgetPerView) : view.content}${view.suffix}`),
|
|
@@ -1012,6 +1543,52 @@ async function writeReviewEvidenceContext(root, projectId, capsuleProject, input
|
|
|
1012
1543
|
persistent: await fileRecord(persistentPath, persistentLogicalPath),
|
|
1013
1544
|
};
|
|
1014
1545
|
}
|
|
1546
|
+
async function writeProducerArtifactContext(capsuleProject, evidenceArtifacts, maxBytes) {
|
|
1547
|
+
const header = [
|
|
1548
|
+
"TIANGONG PRODUCER ARTIFACT CONTEXT v1",
|
|
1549
|
+
"Only deterministic sanitized excerpts from producer-visible text artifacts are included. Binary files remain hash-bound but are not treated as read full text.",
|
|
1550
|
+
].join("\n");
|
|
1551
|
+
const views = await Promise.all([...evidenceArtifacts]
|
|
1552
|
+
.sort((left, right) => left.artifactId.localeCompare(right.artifactId))
|
|
1553
|
+
.map(async (artifact) => {
|
|
1554
|
+
const active = reviewableTextContentType(artifact.mediaType);
|
|
1555
|
+
return {
|
|
1556
|
+
prefix: [
|
|
1557
|
+
`--- FROZEN ARTIFACT ${artifact.artifactId} ---`,
|
|
1558
|
+
`candidateId: ${artifact.candidateId}`,
|
|
1559
|
+
`sha256: ${artifact.sha256}`,
|
|
1560
|
+
`mediaType: ${artifact.mediaType}`,
|
|
1561
|
+
"--- BEGIN BOUNDED ARTIFACT CONTEXT ---",
|
|
1562
|
+
"",
|
|
1563
|
+
].join("\n"),
|
|
1564
|
+
content: active
|
|
1565
|
+
? sanitizeResearchText((await readFile(resolveContained(capsuleProject, artifact.locator), "utf8")).trimEnd())
|
|
1566
|
+
: "[Binary artifact is hash-bound but omitted from producer context. It is not counted as producer-visible full text.]",
|
|
1567
|
+
suffix: "\n--- END BOUNDED ARTIFACT CONTEXT ---",
|
|
1568
|
+
active,
|
|
1569
|
+
};
|
|
1570
|
+
}));
|
|
1571
|
+
const fixed = `${header}\n\n${views
|
|
1572
|
+
.map((view) => `${view.prefix}${view.active ? "" : view.content}${view.suffix}`)
|
|
1573
|
+
.join("\n\n")}\n`;
|
|
1574
|
+
const fixedBytes = Buffer.byteLength(fixed, "utf8");
|
|
1575
|
+
if (fixedBytes > maxBytes) {
|
|
1576
|
+
throw new CliError("Producer artifact metadata exceeds the configured context budget.", {
|
|
1577
|
+
code: "RESEARCH_INPUT_CONTEXT_BUDGET_EXCEEDED",
|
|
1578
|
+
exitCode: 3,
|
|
1579
|
+
details: { fixedBytes, maxBytes, artifacts: views.length },
|
|
1580
|
+
});
|
|
1581
|
+
}
|
|
1582
|
+
const activeViews = views.filter((view) => view.active).length;
|
|
1583
|
+
const perView = activeViews ? Math.floor((maxBytes - fixedBytes) / activeViews) : 0;
|
|
1584
|
+
const content = `${header}\n\n${views
|
|
1585
|
+
.map((view) => `${view.prefix}${view.active ? boundedUtf8ReviewExcerpt(view.content, perView) : view.content}${view.suffix}`)
|
|
1586
|
+
.join("\n\n")}\n`;
|
|
1587
|
+
const logicalPath = "inputs/evidence-artifact-context.txt";
|
|
1588
|
+
const path = resolveContained(capsuleProject, logicalPath);
|
|
1589
|
+
await writeTextAtomic(path, content);
|
|
1590
|
+
return fileRecord(path, logicalPath);
|
|
1591
|
+
}
|
|
1015
1592
|
async function loadBrokerReviewReferences(capsuleProject) {
|
|
1016
1593
|
const evidencePath = resolveContained(capsuleProject, "outputs/evidence.json");
|
|
1017
1594
|
if (!(await pathExists(evidencePath)))
|
|
@@ -1114,7 +1691,7 @@ function resolveReviewJsonPointer(value, pointer) {
|
|
|
1114
1691
|
return selected;
|
|
1115
1692
|
}
|
|
1116
1693
|
function reviewableTextContentType(contentType) {
|
|
1117
|
-
return /^(?:text
|
|
1694
|
+
return /^(?:text\/[^;]+|application\/(?:[^;]+\+)?(?:json|xml|javascript|xhtml\+xml|csv))(?:;|$)/i.test(contentType);
|
|
1118
1695
|
}
|
|
1119
1696
|
function boundedUtf8ReviewExcerpt(value, maxBytes) {
|
|
1120
1697
|
if (Buffer.byteLength(value, "utf8") <= maxBytes)
|
|
@@ -1191,6 +1768,41 @@ async function loadVerifiedReviewPacket(root, projectId, packetSha256) {
|
|
|
1191
1768
|
exitCode: 3,
|
|
1192
1769
|
});
|
|
1193
1770
|
}
|
|
1771
|
+
const snapshotChain = packet.snapshotChain;
|
|
1772
|
+
if (!Array.isArray(snapshotChain)) {
|
|
1773
|
+
throw new CliError("Persistent review packet has no evidence snapshot chain.", {
|
|
1774
|
+
code: "RESEARCH_REVIEW_PACKET_DRIFT",
|
|
1775
|
+
exitCode: 3,
|
|
1776
|
+
});
|
|
1777
|
+
}
|
|
1778
|
+
for (const record of snapshotChain) {
|
|
1779
|
+
if (!isObject(record) ||
|
|
1780
|
+
typeof record.path !== "string" ||
|
|
1781
|
+
typeof record.sha256 !== "string" ||
|
|
1782
|
+
!Number.isInteger(record.bytes) ||
|
|
1783
|
+
!/^evidence\/snapshots\/[0-9a-f]{64}\.json$/.test(record.path)) {
|
|
1784
|
+
throw new CliError("Persistent review packet has an invalid snapshot-chain record.", {
|
|
1785
|
+
code: "RESEARCH_REVIEW_PACKET_DRIFT",
|
|
1786
|
+
exitCode: 3,
|
|
1787
|
+
});
|
|
1788
|
+
}
|
|
1789
|
+
let actual;
|
|
1790
|
+
try {
|
|
1791
|
+
actual = await fileRecord(resolveContained(projectRoot(root, projectId), record.path), record.path);
|
|
1792
|
+
}
|
|
1793
|
+
catch {
|
|
1794
|
+
throw new CliError("Persistent review snapshot chain is missing.", {
|
|
1795
|
+
code: "RESEARCH_REVIEW_PACKET_DRIFT",
|
|
1796
|
+
exitCode: 3,
|
|
1797
|
+
});
|
|
1798
|
+
}
|
|
1799
|
+
if (actual.sha256 !== record.sha256 || actual.bytes !== record.bytes) {
|
|
1800
|
+
throw new CliError("Persistent review snapshot chain failed hash verification.", {
|
|
1801
|
+
code: "RESEARCH_REVIEW_PACKET_DRIFT",
|
|
1802
|
+
exitCode: 3,
|
|
1803
|
+
});
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1194
1806
|
return fileRecord(path, logicalPath);
|
|
1195
1807
|
}
|
|
1196
1808
|
function verifyReviewPacketValue(packet, packetSha256) {
|
|
@@ -1299,7 +1911,8 @@ function agentRequest(input) {
|
|
|
1299
1911
|
maxTurns,
|
|
1300
1912
|
maxOutputTokens: input.maxOutputTokens,
|
|
1301
1913
|
maxToolContextTokens: input.brokerUrl
|
|
1302
|
-
? input.config.budget.maxBrokerContextTokens *
|
|
1914
|
+
? input.config.budget.maxBrokerContextTokens *
|
|
1915
|
+
(input.brokerCallBudget ?? input.config.budget.maxBrokerCalls)
|
|
1303
1916
|
: 0,
|
|
1304
1917
|
maxCostUsd: input.maxCostUsd,
|
|
1305
1918
|
expectedRuntime: input.expectedRuntime,
|
|
@@ -1330,8 +1943,10 @@ async function materializeAndValidateStageOutput(root, project, capsuleProject,
|
|
|
1330
1943
|
const parsed = parseStructuredStageOutput(workPackage.stage, raw, reviewPacketSha256);
|
|
1331
1944
|
const destination = resolveContained(capsuleProject, workPackage.expectedOutputs[0]);
|
|
1332
1945
|
const fileContent = workPackage.stage === "discover"
|
|
1333
|
-
? `${JSON.stringify(normalizeEvidenceCoverage(project, parsed.value), null, 2)}\n`
|
|
1334
|
-
:
|
|
1946
|
+
? `${JSON.stringify(normalizeEvidenceCoverage(project, await materializeDiscoveryEvidence(root, project, parsed.value)), null, 2)}\n`
|
|
1947
|
+
: workPackage.stage === "acquire"
|
|
1948
|
+
? `${JSON.stringify(await materializeAcquisitionAudit(root, project, parsed.value), null, 2)}\n`
|
|
1949
|
+
: parsed.fileContent;
|
|
1335
1950
|
await writeTextAtomic(destination, fileContent);
|
|
1336
1951
|
await validateOutputShape(root, project, workPackage, destination, reviewPacketSha256);
|
|
1337
1952
|
if (parsed.normalizations.length > 0) {
|
|
@@ -1370,12 +1985,20 @@ async function validateOutputShape(root, project, workPackage, path, reviewPacke
|
|
|
1370
1985
|
const content = await readFile(path, "utf8");
|
|
1371
1986
|
if (!content.trim())
|
|
1372
1987
|
throw deterministicError(`${workPackage.expectedOutputs[0]} is empty.`);
|
|
1373
|
-
if (workPackage.stage === "synthesize")
|
|
1988
|
+
if (workPackage.stage === "synthesize") {
|
|
1989
|
+
await validateSynthesisDocument(path, content);
|
|
1374
1990
|
return;
|
|
1375
|
-
|
|
1991
|
+
}
|
|
1376
1992
|
if (workPackage.stage === "discover") {
|
|
1993
|
+
const value = parseEvidenceRecord(content);
|
|
1377
1994
|
await validateEvidenceSources(root, project, value.sources);
|
|
1995
|
+
return;
|
|
1996
|
+
}
|
|
1997
|
+
if (workPackage.stage === "acquire") {
|
|
1998
|
+
parseMaterializedAcquisitionAudit(JSON.parse(content));
|
|
1999
|
+
return;
|
|
1378
2000
|
}
|
|
2001
|
+
const { value } = parseStructuredStageOutput(workPackage.stage, content, reviewPacketSha256);
|
|
1379
2002
|
if (workPackage.stage === "analyze") {
|
|
1380
2003
|
await validateFindings(path, value.findings);
|
|
1381
2004
|
}
|
|
@@ -1449,10 +2072,10 @@ async function validateEvidenceSources(root, project, sources) {
|
|
|
1449
2072
|
}
|
|
1450
2073
|
}
|
|
1451
2074
|
async function validateFindings(path, findings) {
|
|
1452
|
-
const evidencePath = join(dirname(path), "evidence.json");
|
|
2075
|
+
const evidencePath = join(dirname(path), "evidence-snapshot.json");
|
|
1453
2076
|
const evidence = JSON.parse(await readFile(evidencePath, "utf8"));
|
|
1454
2077
|
if (!isObject(evidence) || !Array.isArray(evidence.sources)) {
|
|
1455
|
-
throw deterministicError("Analysis requires
|
|
2078
|
+
throw deterministicError("Analysis requires a frozen evidence-snapshot.json.");
|
|
1456
2079
|
}
|
|
1457
2080
|
const sourceIds = new Set(evidence.sources
|
|
1458
2081
|
.filter((source) => isObject(source))
|
|
@@ -1475,6 +2098,105 @@ async function validateFindings(path, findings) {
|
|
|
1475
2098
|
findingIds.add(finding.id);
|
|
1476
2099
|
}
|
|
1477
2100
|
}
|
|
2101
|
+
async function validateSynthesisDocument(path, content) {
|
|
2102
|
+
const validation = [];
|
|
2103
|
+
if ([...content.matchAll(/`([^`\n]*)`/g)].some((match) => /https?:\/\//i.test(match[1] ?? ""))) {
|
|
2104
|
+
validation.push("URLs must be real Markdown links or bare links, not inline-code literals");
|
|
2105
|
+
}
|
|
2106
|
+
if (/%(?:60|0a|0d)|\\u0060|https?:\/\/[^\s<>"'`)\]]*`/i.test(content)) {
|
|
2107
|
+
validation.push("URLs contain encoded or literal backtick/newline contamination");
|
|
2108
|
+
}
|
|
2109
|
+
const markdownTargets = [];
|
|
2110
|
+
for (const match of content.matchAll(/!?\[[^\]\n]*\]\(([^)\s]+)(?:\s+["'][^"']*["'])?\)/g)) {
|
|
2111
|
+
if (match[1])
|
|
2112
|
+
markdownTargets.push(stripMarkdownDestination(match[1]));
|
|
2113
|
+
}
|
|
2114
|
+
const referenceIds = new Set();
|
|
2115
|
+
for (const match of content.matchAll(/^\s*\[([^\]\n]+)\]:\s*(\S+)/gm)) {
|
|
2116
|
+
const id = match[1].trim().toLowerCase().replace(/\s+/g, " ");
|
|
2117
|
+
if (referenceIds.has(id))
|
|
2118
|
+
validation.push(`duplicate Markdown reference ID: ${id}`);
|
|
2119
|
+
referenceIds.add(id);
|
|
2120
|
+
markdownTargets.push(stripMarkdownDestination(match[2]));
|
|
2121
|
+
}
|
|
2122
|
+
for (const target of markdownTargets) {
|
|
2123
|
+
await validateReportTarget(path, target, validation);
|
|
2124
|
+
}
|
|
2125
|
+
const urls = new Set([...content.matchAll(/https?:\/\/[^\s<>"'`)\]]+/gi)]
|
|
2126
|
+
.map((match) => stripTrailingUrlPunctuation(match[0]))
|
|
2127
|
+
.filter(Boolean));
|
|
2128
|
+
for (const value of urls)
|
|
2129
|
+
validateReportHttpsUrl(value, validation);
|
|
2130
|
+
if (validation.length) {
|
|
2131
|
+
throw new StructuredOutputError("Synthesis output failed mechanical link QA.", {
|
|
2132
|
+
validation: [...new Set(validation)].slice(0, 20),
|
|
2133
|
+
});
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
async function validateReportTarget(reportPath, target, validation) {
|
|
2137
|
+
if (!target || target.startsWith("#"))
|
|
2138
|
+
return;
|
|
2139
|
+
if (/^https?:\/\//i.test(target)) {
|
|
2140
|
+
validateReportHttpsUrl(target, validation);
|
|
2141
|
+
return;
|
|
2142
|
+
}
|
|
2143
|
+
if (/^mailto:/i.test(target))
|
|
2144
|
+
return;
|
|
2145
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(target)) {
|
|
2146
|
+
validation.push(`unsupported Markdown link scheme: ${target.split(":", 1)[0]}`);
|
|
2147
|
+
return;
|
|
2148
|
+
}
|
|
2149
|
+
let decoded;
|
|
2150
|
+
try {
|
|
2151
|
+
decoded = decodeURIComponent(target.split(/[?#]/, 1)[0] ?? "");
|
|
2152
|
+
}
|
|
2153
|
+
catch {
|
|
2154
|
+
validation.push("Markdown link contains invalid percent encoding");
|
|
2155
|
+
return;
|
|
2156
|
+
}
|
|
2157
|
+
if (!decoded || isAbsolute(decoded)) {
|
|
2158
|
+
validation.push("Markdown local links must be relative files inside the research capsule");
|
|
2159
|
+
return;
|
|
2160
|
+
}
|
|
2161
|
+
const capsuleProject = dirname(dirname(reportPath));
|
|
2162
|
+
const selected = resolve(dirname(reportPath), decoded);
|
|
2163
|
+
if (relative(capsuleProject, selected).startsWith("..")) {
|
|
2164
|
+
validation.push("Markdown local link escapes the research capsule");
|
|
2165
|
+
return;
|
|
2166
|
+
}
|
|
2167
|
+
const info = await lstat(selected).catch(() => undefined);
|
|
2168
|
+
if (!info?.isFile() || info.isSymbolicLink()) {
|
|
2169
|
+
validation.push(`Markdown local link does not resolve to a regular file: ${decoded}`);
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
function validateReportHttpsUrl(value, validation) {
|
|
2173
|
+
if (/%(?:60|0a|0d)|`/i.test(value)) {
|
|
2174
|
+
validation.push("URL contains encoded or literal backtick/newline contamination");
|
|
2175
|
+
return;
|
|
2176
|
+
}
|
|
2177
|
+
let url;
|
|
2178
|
+
try {
|
|
2179
|
+
url = new URL(value);
|
|
2180
|
+
}
|
|
2181
|
+
catch {
|
|
2182
|
+
validation.push("report contains an invalid URL");
|
|
2183
|
+
return;
|
|
2184
|
+
}
|
|
2185
|
+
if (url.protocol !== "https:")
|
|
2186
|
+
validation.push(`report URL must use HTTPS: ${url.host}`);
|
|
2187
|
+
if (url.username || url.password)
|
|
2188
|
+
validation.push("report URL contains embedded credentials");
|
|
2189
|
+
const sensitive = /^(access_token|api[_-]?key|apikey|auth|authorization|code|cookie|key|password|secret|session|sig|signature|token)$/i;
|
|
2190
|
+
if ([...url.searchParams.keys()].some((key) => sensitive.test(key))) {
|
|
2191
|
+
validation.push("report URL contains sensitive query parameters");
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
function stripMarkdownDestination(value) {
|
|
2195
|
+
return value.startsWith("<") && value.endsWith(">") ? value.slice(1, -1) : value;
|
|
2196
|
+
}
|
|
2197
|
+
function stripTrailingUrlPunctuation(value) {
|
|
2198
|
+
return value.replace(/[),.;!?`]+$/g, "");
|
|
2199
|
+
}
|
|
1478
2200
|
function normalizeEvidenceCoverage(project, value) {
|
|
1479
2201
|
const inputIds = new Set(project.inputs.map((input) => input.id));
|
|
1480
2202
|
const sources = (value.sources ?? []).map((source) => {
|
|
@@ -1567,13 +2289,16 @@ function computeEvidenceCoverage(project, sources, declared) {
|
|
|
1567
2289
|
mechanicalGaps: gaps,
|
|
1568
2290
|
};
|
|
1569
2291
|
}
|
|
1570
|
-
async function
|
|
2292
|
+
async function assertDiscoveryCoverage(root, project, evidencePath) {
|
|
1571
2293
|
const path = evidencePath ?? resolveContained(projectRoot(root, project.id), "outputs/evidence.json");
|
|
1572
2294
|
const value = JSON.parse(await readFile(path, "utf8"));
|
|
1573
2295
|
const sources = value.sources;
|
|
1574
2296
|
const declared = value.coverage;
|
|
1575
2297
|
const computed = computeEvidenceCoverage(project, sources, declared);
|
|
1576
|
-
|
|
2298
|
+
// Full-text acquisition is intentionally the next phase. Discovery must
|
|
2299
|
+
// establish breadth, dates, source types and dimensions, but cannot claim
|
|
2300
|
+
// that a search-result receipt is already acquired full text.
|
|
2301
|
+
const gaps = computed.mechanicalGaps.filter((gap) => !/full-text source\(s\)/.test(gap));
|
|
1577
2302
|
const requiredCapabilities = requiredDiscoveryCapabilityIds(await loadCapabilityDeclarations(root));
|
|
1578
2303
|
const exercisedCapabilities = new Set((await loadProjectEvidenceReceipts(root, project.id)).map((receipt) => receipt.capabilityId));
|
|
1579
2304
|
const journalEvents = await readJournal(workspacePaths(root).journal);
|
|
@@ -1619,9 +2344,6 @@ async function assertEvidenceCoverage(root, project, evidencePath) {
|
|
|
1619
2344
|
canonicalJson(declared.publicationDateRange) !== canonicalJson(computed.publicationDateRange)) {
|
|
1620
2345
|
gaps.push("coverage summary does not match admitted sources");
|
|
1621
2346
|
}
|
|
1622
|
-
if (declared.decision !== computed.decision) {
|
|
1623
|
-
gaps.push(`coverage decision must be ${computed.decision}`);
|
|
1624
|
-
}
|
|
1625
2347
|
if (gaps.length) {
|
|
1626
2348
|
throw new CliError("Evidence coverage is insufficient; downstream packages were not started.", {
|
|
1627
2349
|
code: "RESEARCH_EVIDENCE_INSUFFICIENT",
|
|
@@ -1658,9 +2380,14 @@ function publicationDateInterval(value) {
|
|
|
1658
2380
|
return { earliest: `${match[1]}-01-01`, latest: `${match[1]}-12-31` };
|
|
1659
2381
|
}
|
|
1660
2382
|
async function closeProjectMechanically(root, project, workPackage) {
|
|
1661
|
-
await
|
|
2383
|
+
const snapshot = await loadCurrentEvidenceSnapshot(root, project.id);
|
|
2384
|
+
project.evidenceState.currentSnapshotId = snapshot.snapshotId;
|
|
2385
|
+
project.evidenceState.currentSnapshotSha256 = snapshot.snapshotSha256;
|
|
2386
|
+
project.evidenceState.closureSnapshotId = snapshot.snapshotId;
|
|
1662
2387
|
const required = [
|
|
1663
2388
|
"outputs/evidence.json",
|
|
2389
|
+
"outputs/acquisition.json",
|
|
2390
|
+
"outputs/evidence-snapshot.json",
|
|
1664
2391
|
"outputs/analysis.json",
|
|
1665
2392
|
"outputs/report.md",
|
|
1666
2393
|
"outputs/review.json",
|
|
@@ -1675,6 +2402,7 @@ async function closeProjectMechanically(root, project, workPackage) {
|
|
|
1675
2402
|
throw deterministicError("Project review does not bind a valid review packet hash.");
|
|
1676
2403
|
}
|
|
1677
2404
|
const reviewPacket = await loadVerifiedReviewPacket(root, project.id, review.packetSha256);
|
|
2405
|
+
await verifyReviewLedgerBinding(root, project.id, snapshot.snapshotId, review.packetSha256);
|
|
1678
2406
|
const evidenceReceipts = await loadProjectEvidenceReceipts(root, project.id);
|
|
1679
2407
|
const journal = await verifyJournal(workspacePaths(root).journal);
|
|
1680
2408
|
const closure = {
|
|
@@ -1684,6 +2412,12 @@ async function closeProjectMechanically(root, project, workPackage) {
|
|
|
1684
2412
|
closedAt: new Date().toISOString(),
|
|
1685
2413
|
questionSha256: sha256Text(project.question),
|
|
1686
2414
|
evidenceRequirements: project.evidenceRequirements,
|
|
2415
|
+
evidenceSnapshot: {
|
|
2416
|
+
snapshotId: snapshot.snapshotId,
|
|
2417
|
+
snapshotSha256: snapshot.snapshotSha256,
|
|
2418
|
+
parentSnapshotId: snapshot.parentSnapshotId,
|
|
2419
|
+
parentSnapshotSha256: snapshot.parentSnapshotSha256,
|
|
2420
|
+
},
|
|
1687
2421
|
inputs: project.inputs.map((input) => ({
|
|
1688
2422
|
id: input.id,
|
|
1689
2423
|
role: input.role,
|
|
@@ -1707,6 +2441,75 @@ async function closeProjectMechanically(root, project, workPackage) {
|
|
|
1707
2441
|
await writeJsonAtomic(closurePath, closure);
|
|
1708
2442
|
return zeroExecutionResult();
|
|
1709
2443
|
}
|
|
2444
|
+
async function commitStageEvidenceBindings(root, project, workPackage) {
|
|
2445
|
+
if (workPackage.stage !== "analyze" && workPackage.stage !== "review")
|
|
2446
|
+
return;
|
|
2447
|
+
const snapshot = await loadCurrentEvidenceSnapshot(root, project.id);
|
|
2448
|
+
const events = await readJournal(evidenceLedgerPath(root, project.id));
|
|
2449
|
+
if (workPackage.stage === "analyze") {
|
|
2450
|
+
const path = join(projectRoot(root, project.id), "outputs", "analysis.json");
|
|
2451
|
+
const value = JSON.parse(await readFile(path, "utf8"));
|
|
2452
|
+
if (!isObject(value) || !Array.isArray(value.findings)) {
|
|
2453
|
+
throw deterministicError("Analysis claim bindings require a valid analysis output.");
|
|
2454
|
+
}
|
|
2455
|
+
const committed = new Set(events
|
|
2456
|
+
.filter((event) => event.type === "claim.used")
|
|
2457
|
+
.map((event) => String(event.payload.bindingSha256)));
|
|
2458
|
+
for (const finding of value.findings) {
|
|
2459
|
+
if (!isObject(finding) ||
|
|
2460
|
+
typeof finding.id !== "string" ||
|
|
2461
|
+
!Array.isArray(finding.evidence) ||
|
|
2462
|
+
finding.evidence.some((sourceId) => typeof sourceId !== "string")) {
|
|
2463
|
+
throw deterministicError("Analysis contains an invalid claim binding.");
|
|
2464
|
+
}
|
|
2465
|
+
const binding = {
|
|
2466
|
+
claimId: finding.id,
|
|
2467
|
+
snapshotId: snapshot.snapshotId,
|
|
2468
|
+
sourceIds: [...finding.evidence].sort(),
|
|
2469
|
+
claimSha256: sha256Text(canonicalJson(finding)),
|
|
2470
|
+
};
|
|
2471
|
+
const bindingSha256 = sha256Text(canonicalJson(binding));
|
|
2472
|
+
if (committed.has(bindingSha256))
|
|
2473
|
+
continue;
|
|
2474
|
+
await appendEvidenceLedgerEvent(root, project.id, "claim.used", {
|
|
2475
|
+
...binding,
|
|
2476
|
+
bindingSha256,
|
|
2477
|
+
});
|
|
2478
|
+
}
|
|
2479
|
+
return;
|
|
2480
|
+
}
|
|
2481
|
+
const path = join(projectRoot(root, project.id), "outputs", "review.json");
|
|
2482
|
+
const value = JSON.parse(await readFile(path, "utf8"));
|
|
2483
|
+
if (!isObject(value) || value.decision !== "pass" || typeof value.packetSha256 !== "string") {
|
|
2484
|
+
throw deterministicError("Review binding requires a passing schema-valid review.");
|
|
2485
|
+
}
|
|
2486
|
+
const binding = {
|
|
2487
|
+
snapshotId: snapshot.snapshotId,
|
|
2488
|
+
packetSha256: value.packetSha256,
|
|
2489
|
+
reviewSha256: await sha256File(path),
|
|
2490
|
+
decision: value.decision,
|
|
2491
|
+
};
|
|
2492
|
+
const bindingSha256 = sha256Text(canonicalJson(binding));
|
|
2493
|
+
if (events.some((event) => event.type === "review.bound" && event.payload.bindingSha256 === bindingSha256)) {
|
|
2494
|
+
return;
|
|
2495
|
+
}
|
|
2496
|
+
await appendEvidenceLedgerEvent(root, project.id, "review.bound", {
|
|
2497
|
+
...binding,
|
|
2498
|
+
bindingSha256,
|
|
2499
|
+
});
|
|
2500
|
+
}
|
|
2501
|
+
async function verifyReviewLedgerBinding(root, projectId, snapshotId, packetSha256) {
|
|
2502
|
+
const reviewPath = join(projectRoot(root, projectId), "outputs", "review.json");
|
|
2503
|
+
const reviewSha256 = await sha256File(reviewPath);
|
|
2504
|
+
const events = await readJournal(evidenceLedgerPath(root, projectId));
|
|
2505
|
+
const bound = events.some((event) => event.type === "review.bound" &&
|
|
2506
|
+
event.payload.snapshotId === snapshotId &&
|
|
2507
|
+
event.payload.packetSha256 === packetSha256 &&
|
|
2508
|
+
event.payload.reviewSha256 === reviewSha256);
|
|
2509
|
+
if (!bound) {
|
|
2510
|
+
throw deterministicError("Project review is not bound to the current evidence snapshot ledger.");
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
1710
2513
|
async function verifyProjectInputBindings(project) {
|
|
1711
2514
|
for (const input of project.inputs) {
|
|
1712
2515
|
const info = await lstat(input.path).catch(() => undefined);
|
|
@@ -1746,19 +2549,37 @@ async function verifyProjectInputBindings(project) {
|
|
|
1746
2549
|
async function outputRecords(root, project, logicalPaths) {
|
|
1747
2550
|
return Promise.all(logicalPaths.map((logicalPath) => fileRecord(resolveContained(projectRoot(root, project.id), logicalPath), logicalPath)));
|
|
1748
2551
|
}
|
|
1749
|
-
async function stageContextForPackage(capsuleProject, workPackage, config) {
|
|
1750
|
-
const logicalPaths = workPackage.stage === "
|
|
1751
|
-
? ["outputs/evidence.json"]
|
|
1752
|
-
: workPackage.stage === "
|
|
1753
|
-
? [
|
|
1754
|
-
|
|
2552
|
+
async function stageContextForPackage(capsuleProject, project, workPackage, config) {
|
|
2553
|
+
const logicalPaths = workPackage.stage === "discover" && project.lineage.kind === "addendum"
|
|
2554
|
+
? ["outputs/base-evidence-snapshot.json"]
|
|
2555
|
+
: workPackage.stage === "acquire"
|
|
2556
|
+
? [
|
|
2557
|
+
"outputs/evidence.json",
|
|
2558
|
+
...(project.lineage.kind === "addendum" ? ["outputs/base-evidence-snapshot.json"] : []),
|
|
2559
|
+
]
|
|
2560
|
+
: workPackage.stage === "analyze"
|
|
1755
2561
|
? [
|
|
1756
|
-
"
|
|
1757
|
-
"
|
|
1758
|
-
|
|
1759
|
-
|
|
2562
|
+
"outputs/evidence-snapshot.json",
|
|
2563
|
+
...((await pathExists(resolveContained(capsuleProject, "inputs/evidence-artifact-context.txt")))
|
|
2564
|
+
? ["inputs/evidence-artifact-context.txt"]
|
|
2565
|
+
: []),
|
|
1760
2566
|
]
|
|
1761
|
-
:
|
|
2567
|
+
: workPackage.stage === "synthesize"
|
|
2568
|
+
? [
|
|
2569
|
+
"outputs/evidence-snapshot.json",
|
|
2570
|
+
"outputs/analysis.json",
|
|
2571
|
+
...((await pathExists(resolveContained(capsuleProject, "inputs/evidence-artifact-context.txt")))
|
|
2572
|
+
? ["inputs/evidence-artifact-context.txt"]
|
|
2573
|
+
: []),
|
|
2574
|
+
]
|
|
2575
|
+
: workPackage.stage === "review"
|
|
2576
|
+
? [
|
|
2577
|
+
"inputs/review-evidence-context.txt",
|
|
2578
|
+
"outputs/evidence-snapshot.json",
|
|
2579
|
+
"outputs/analysis.json",
|
|
2580
|
+
"outputs/report.md",
|
|
2581
|
+
]
|
|
2582
|
+
: [];
|
|
1762
2583
|
const sections = [];
|
|
1763
2584
|
for (const logicalPath of logicalPaths) {
|
|
1764
2585
|
const content = await readFile(resolveContained(capsuleProject, logicalPath), "utf8");
|
|
@@ -1766,13 +2587,17 @@ async function stageContextForPackage(capsuleProject, workPackage, config) {
|
|
|
1766
2587
|
}
|
|
1767
2588
|
const bundled = sections.join("\n\n");
|
|
1768
2589
|
const estimatedTokens = Math.ceil(Buffer.byteLength(bundled, "utf8") / RESEARCH_ESTIMATED_BYTES_PER_TOKEN);
|
|
1769
|
-
const maxStageContextTokens = workPackage.stage === "
|
|
1770
|
-
? config.budget.maxInputContextTokens
|
|
1771
|
-
: workPackage.stage === "
|
|
1772
|
-
? config.budget.maxOutputTokens *
|
|
1773
|
-
: workPackage.stage === "
|
|
1774
|
-
? config.budget.
|
|
1775
|
-
:
|
|
2590
|
+
const maxStageContextTokens = workPackage.stage === "discover" && project.lineage.kind === "addendum"
|
|
2591
|
+
? config.budget.maxInputContextTokens
|
|
2592
|
+
: workPackage.stage === "review"
|
|
2593
|
+
? config.budget.maxInputContextTokens + config.budget.maxOutputTokens * 4
|
|
2594
|
+
: workPackage.stage === "synthesize"
|
|
2595
|
+
? config.budget.maxInputContextTokens
|
|
2596
|
+
: workPackage.stage === "analyze"
|
|
2597
|
+
? config.budget.maxInputContextTokens
|
|
2598
|
+
: workPackage.stage === "acquire"
|
|
2599
|
+
? config.budget.maxOutputTokens
|
|
2600
|
+
: 0;
|
|
1776
2601
|
if (estimatedTokens > maxStageContextTokens) {
|
|
1777
2602
|
throw new CliError(`Admitted stage context exceeds the configured input context limit for ${workPackage.id}.`, {
|
|
1778
2603
|
code: "RESEARCH_INPUT_CONTEXT_BUDGET_EXCEEDED",
|
|
@@ -1786,10 +2611,11 @@ async function stageContextForPackage(capsuleProject, workPackage, config) {
|
|
|
1786
2611
|
}
|
|
1787
2612
|
return bundled;
|
|
1788
2613
|
}
|
|
1789
|
-
function packagePrompt(project, workPackage, inputs, stagedSkills, capabilityDocumentation, reviewPacketSha256, contextBundle, contextBundleContent, stageContextContent,
|
|
2614
|
+
function packagePrompt(project, workPackage, inputs, stagedSkills, capabilityDocumentation, reviewPacketSha256, contextBundle, contextBundleContent, stageContextContent, discovery, evidenceCandidates) {
|
|
1790
2615
|
const stageInstructions = {
|
|
1791
|
-
discover: "
|
|
1792
|
-
|
|
2616
|
+
discover: "Assess candidates incrementally through the packet's recordAssessment command; do not accumulate a source-sized final response. The control plane has already assigned every immutable input and broker result a candidateId and retains its title, URL, DOI, dates, receipt, locator, JSON Pointer, hashes, and retrieval metadata. Reference candidateId; never repeat or invent those deterministic fields. Give each admitted candidate a concise sourceId plus source type, relevance, quality, applicability, coverage dimensions, and limitations. Record meaningful explicit rejections; omitted candidates remain unassessed for later gap filling. Native Web or Browser discoveries are supplemental candidates only and cannot be admitted until an immutable broker receipt is attached to the same canonical URL or DOI. After broad search, strict assessment, and focused gap filling, return only the small closeout object with one judgment for every reviewed dimension plus limitations and remaining gaps. The CLI mechanically joins the latest recorded assessments to provenance, derives counts/date range/coverage, and rejects unknown or unformalized candidates.",
|
|
2617
|
+
acquire: "Audit every provisionally admitted source exactly once. For each source, bind its ledger candidateId, list only artifactIds returned by the exact artifact registration command, and choose accepted, limited, or rejected with a concise rationale and explicit limitations. A broker receipt is an immutable discovery record but is not full text. Use an empty artifactIds array only when intentionally retaining a source as metadata/abstract evidence or when the source is an already registered local input. Put unresolved blocking acquisition or coverage deficiencies in gaps; put honest non-blocking scope constraints in limitations. Do not invent file paths, hashes, URLs, artifact IDs, or successful downloads.",
|
|
2618
|
+
analyze: "Use only the complete embedded frozen evidence snapshot below and return the schema-defined analysis object. Every finding must cite source IDs present in that snapshot and state uncertainty and applicability. When the snapshot has a parent, use its mechanical delta to focus re-analysis on added, changed, and removed evidence while still returning one complete current analysis.",
|
|
1793
2619
|
synthesize: "Use only the complete embedded admitted evidence and findings below. Return the schema-defined object whose reportMarkdown separates supported conclusions, uncertainty, limitations, and next actions. Use real Markdown line breaks encoded exactly once for JSON; never place literal /n or double-escaped \\n markers in reportMarkdown. When the admitted publication-date range is narrower than the requested range, state the exact admitted range and missing interval prominently in the opening summary.",
|
|
1794
2620
|
review: `Independently inspect the complete embedded artifacts and globally bounded evidence excerpts. The CLI has already verified every bound full evidence object's size and SHA-256 and persistently stored the complete review packet; its hash is schema-bound even though the packet metadata is not duplicated in model context. Do not claim to have read beyond the embedded excerpts. Return the schema-defined review bound to packetSha256 ${reviewPacketSha256 ?? "unavailable"}. Use pass only when every material claim is traceable within the admitted evidence and clearly scoped to its limitations.`,
|
|
1795
2621
|
close: "No agent action is allowed for mechanical closure.",
|
|
@@ -1805,16 +2631,26 @@ function packagePrompt(project, workPackage, inputs, stagedSkills, capabilityDoc
|
|
|
1805
2631
|
`Staged capability directories: ${JSON.stringify(stagedSkills.map((path) => `skills/${basename(path)}`))}`,
|
|
1806
2632
|
workPackage.stage === "discover"
|
|
1807
2633
|
? "The exact capability manifest and each staged external SKILL.md are embedded below. Use this documentation directly; filesystem tools are disabled."
|
|
1808
|
-
:
|
|
2634
|
+
: workPackage.stage === "acquire"
|
|
2635
|
+
? "Use the installed external acquisition/document Skills in the current native host, but treat the CLI artifact registry and acquisition schema as the only authority for durable evidence."
|
|
2636
|
+
: "Capability files are provenance-bound but are not available as execution tools in this stage.",
|
|
1809
2637
|
workPackage.stage === "discover"
|
|
1810
|
-
? `
|
|
2638
|
+
? `Follow the reviewed discovery plan: ${JSON.stringify(discovery)}. The plan's max broker-view count is a hard working ceiling, not a target to exhaust. Execute required first-pass channels before supplemental channels, prefer broad high-yield queries, assess registered candidates between batches, and use the next gap-fill batch only for explicit uncovered dimensions, source types, date ranges, full text, limitations, or counterevidence. Stop fetching as soon as the declared coverage minimums are supportable. Native Web/Browser may broaden lead discovery, but every such action must be recorded through recordActivity, every useful result must be registered as a candidate, and the same URL/DOI must then be formalized through the broker before admission. The broker or an immutable registered input is the sole admissible evidence path. Do not execute a staged Skill's curl/CLI examples or read provider environment variables. Invoke fetch_candidate_source with the manifest capability ID and obey its exact declared HTTP method. GET capabilities use only declared query parameters; POST capabilities require request_body containing only the documented non-secret request JSON. Never place API keys, tokens, authorization data, cookies, or other credential-like fields in request_body. The broker injects the sole declared logical credential, disables caching for credentialed requests, and never persists the POST body (only its hash). The tool result includes exact bounded context, registered candidate IDs, its receipt, whether a network call was avoided, and remaining view budget. Exercise every manifest capability with requiredForDiscovery=true, or the mechanical coverage gate will stop downstream work.`
|
|
1811
2639
|
: "Use only the complete embedded stage context; no tools or additional source reads are allowed.",
|
|
1812
2640
|
stageInstructions[workPackage.stage],
|
|
1813
2641
|
"Do not write stage output files directly. Your final response must be only the JSON object required by the supplied output schema; the CLI will validate and atomically materialize it.",
|
|
1814
2642
|
"Do not edit project.json, input manifests, prior outputs, evidence objects, or staged capability files.",
|
|
1815
2643
|
];
|
|
1816
2644
|
if (workPackage.stage === "discover") {
|
|
1817
|
-
prompt.push(
|
|
2645
|
+
prompt.push(`Candidate index already registered at stage start: ${JSON.stringify(evidenceCandidates.map((candidate) => ({
|
|
2646
|
+
candidateId: candidate.id,
|
|
2647
|
+
title: candidate.title,
|
|
2648
|
+
url: candidate.url,
|
|
2649
|
+
doi: candidate.doi,
|
|
2650
|
+
publicationDate: candidate.publicationDate,
|
|
2651
|
+
excerpt: candidate.excerpt,
|
|
2652
|
+
originKind: candidate.origin.kind,
|
|
2653
|
+
})))}`, "The complete external capability documentation bundle is embedded below:", capabilityDocumentation, "The complete authorized local-input context is embedded below. Use it directly and do not re-read individual local input files. Full evidence files are intentionally withheld from producer packages when fullTextStaged=false.", contextBundleContent);
|
|
1818
2654
|
}
|
|
1819
2655
|
if (stageContextContent) {
|
|
1820
2656
|
prompt.push("The complete admitted stage context is embedded below. Use it directly and do not re-read output files.", stageContextContent);
|
|
@@ -1861,11 +2697,12 @@ function assertPreCallTokenReservation(project, workPackage, config, request, al
|
|
|
1861
2697
|
});
|
|
1862
2698
|
}
|
|
1863
2699
|
}
|
|
1864
|
-
function reservePackageBudget(project, workPackage, config) {
|
|
2700
|
+
function reservePackageBudget(project, workPackage, config, requestedTokens) {
|
|
1865
2701
|
if (workPackage.stage === "close")
|
|
1866
2702
|
return { tokens: 0, costUsd: 0 };
|
|
1867
2703
|
const route = workPackage.executor === "reviewer" ? config.reviewer : config.producer;
|
|
1868
|
-
const
|
|
2704
|
+
const packageMaximum = config.budget.packageMaxTokens[workPackage.stage];
|
|
2705
|
+
const tokens = Math.min(packageMaximum, requestedTokens ?? packageMaximum);
|
|
1869
2706
|
const costUsd = roundMoney(reservedAgentPackageCost(route, tokens, config));
|
|
1870
2707
|
const wallSeconds = config.budget.packageMaxWallSeconds[workPackage.stage];
|
|
1871
2708
|
const remaining = remainingBudget(project, config);
|
|
@@ -2206,25 +3043,33 @@ async function summarizeRun(root, requestId, cycles, executed, maxCycles, projec
|
|
|
2206
3043
|
usage: project.usage,
|
|
2207
3044
|
}));
|
|
2208
3045
|
const unfinished = summaries.filter((project) => project.status !== "complete");
|
|
3046
|
+
const waiting = unfinished.filter((project) => project.status === "waiting-user" || project.status === "waiting-external");
|
|
2209
3047
|
const hasReadyPackage = summaries.some((project) => project.readyPackage !== null);
|
|
2210
3048
|
const nativeStageRequired = projects.some((project) => project.packages.some((workPackage) => workPackage.executor === "producer" &&
|
|
2211
3049
|
(workPackage.status === "ready" || workPackage.status === "running")));
|
|
2212
3050
|
const status = summaries.length > 0 && summaries.every((project) => project.status === "complete")
|
|
2213
3051
|
? "complete"
|
|
2214
|
-
:
|
|
2215
|
-
|
|
2216
|
-
|
|
3052
|
+
: waiting.length > 0 &&
|
|
3053
|
+
unfinished.every((project) => project.status === "blocked" ||
|
|
3054
|
+
project.status === "waiting-user" ||
|
|
3055
|
+
project.status === "waiting-external")
|
|
3056
|
+
? "waiting"
|
|
3057
|
+
: unfinished.length > 0 && unfinished.every((project) => project.status === "blocked")
|
|
3058
|
+
? "blocked"
|
|
3059
|
+
: "ready";
|
|
2217
3060
|
const stopReason = summaries.length === 0
|
|
2218
3061
|
? "no-projects"
|
|
2219
3062
|
: status === "complete"
|
|
2220
3063
|
? "all-projects-complete"
|
|
2221
|
-
:
|
|
2222
|
-
? "
|
|
2223
|
-
:
|
|
2224
|
-
? "
|
|
2225
|
-
:
|
|
2226
|
-
? "
|
|
2227
|
-
: "
|
|
3064
|
+
: status === "waiting"
|
|
3065
|
+
? "handoff-required"
|
|
3066
|
+
: hasReadyPackage && cycles >= maxCycles
|
|
3067
|
+
? "cycle-limit"
|
|
3068
|
+
: nativeStageRequired
|
|
3069
|
+
? "native-stage-required"
|
|
3070
|
+
: status === "blocked"
|
|
3071
|
+
? "project-blocked"
|
|
3072
|
+
: "no-ready-work";
|
|
2228
3073
|
return {
|
|
2229
3074
|
workspace: root,
|
|
2230
3075
|
requestId,
|
|
@@ -2237,7 +3082,26 @@ async function summarizeRun(root, requestId, cycles, executed, maxCycles, projec
|
|
|
2237
3082
|
};
|
|
2238
3083
|
}
|
|
2239
3084
|
async function projectsForRun(root, projectId) {
|
|
2240
|
-
|
|
3085
|
+
if (projectId) {
|
|
3086
|
+
const project = await loadProject(root, projectId);
|
|
3087
|
+
if (project.lineage.supersededBy ||
|
|
3088
|
+
project.status === "archived" ||
|
|
3089
|
+
project.status === "abandoned") {
|
|
3090
|
+
throw new CliError(`Research project ${projectId} is historical and cannot be executed.`, {
|
|
3091
|
+
code: "RESEARCH_PROJECT_NOT_AUTHORITATIVE",
|
|
3092
|
+
exitCode: 3,
|
|
3093
|
+
details: {
|
|
3094
|
+
projectId,
|
|
3095
|
+
status: project.status,
|
|
3096
|
+
authoritativeProjectId: project.lineage.supersededBy ?? null,
|
|
3097
|
+
},
|
|
3098
|
+
});
|
|
3099
|
+
}
|
|
3100
|
+
return [project];
|
|
3101
|
+
}
|
|
3102
|
+
return (await listProjects(root)).filter((project) => project.lineage.supersededBy === null &&
|
|
3103
|
+
project.status !== "archived" &&
|
|
3104
|
+
project.status !== "abandoned");
|
|
2241
3105
|
}
|
|
2242
3106
|
function assertPublicEvidenceUrl(value, sourceId) {
|
|
2243
3107
|
let url;
|
|
@@ -2247,6 +3111,9 @@ function assertPublicEvidenceUrl(value, sourceId) {
|
|
|
2247
3111
|
catch {
|
|
2248
3112
|
throw deterministicError(`Evidence source ${sourceId} contains an invalid URL.`);
|
|
2249
3113
|
}
|
|
3114
|
+
if (url.protocol !== "https:") {
|
|
3115
|
+
throw deterministicError(`Evidence source ${sourceId} URL must use HTTPS.`);
|
|
3116
|
+
}
|
|
2250
3117
|
if (url.username || url.password) {
|
|
2251
3118
|
throw deterministicError(`Evidence source ${sourceId} URL contains credentials.`);
|
|
2252
3119
|
}
|