@tiangong-ai/cli 0.0.59 → 0.0.61

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.
Files changed (104) hide show
  1. package/AGENTS.md +2 -2
  2. package/README.md +287 -41
  3. package/dist/research/orchestration.js +209 -62
  4. package/dist/research/orchestration.js.map +1 -1
  5. package/dist/research/workspace/acquisition-forecast.d.ts +3 -0
  6. package/dist/research/workspace/acquisition-forecast.js +28 -3
  7. package/dist/research/workspace/acquisition-forecast.js.map +1 -1
  8. package/dist/research/workspace/acquisition-revision.d.ts +13 -0
  9. package/dist/research/workspace/acquisition-revision.js +142 -0
  10. package/dist/research/workspace/acquisition-revision.js.map +1 -0
  11. package/dist/research/workspace/acquisition-routes.d.ts +1 -1
  12. package/dist/research/workspace/acquisition-routes.js +2 -11
  13. package/dist/research/workspace/acquisition-routes.js.map +1 -1
  14. package/dist/research/workspace/acquisition.d.ts +3 -1
  15. package/dist/research/workspace/acquisition.js +67 -35
  16. package/dist/research/workspace/acquisition.js.map +1 -1
  17. package/dist/research/workspace/analysis-run.d.ts +2 -0
  18. package/dist/research/workspace/analysis-run.js +28 -0
  19. package/dist/research/workspace/analysis-run.js.map +1 -0
  20. package/dist/research/workspace/artifact-read-audit.d.ts +16 -0
  21. package/dist/research/workspace/artifact-read-audit.js +176 -0
  22. package/dist/research/workspace/artifact-read-audit.js.map +1 -0
  23. package/dist/research/workspace/artifact-trace.d.ts +12 -0
  24. package/dist/research/workspace/artifact-trace.js +109 -0
  25. package/dist/research/workspace/artifact-trace.js.map +1 -0
  26. package/dist/research/workspace/artifact-view-mcp.d.ts +75 -0
  27. package/dist/research/workspace/artifact-view-mcp.js +214 -0
  28. package/dist/research/workspace/artifact-view-mcp.js.map +1 -0
  29. package/dist/research/workspace/artifact-views.d.ts +100 -0
  30. package/dist/research/workspace/artifact-views.js +395 -0
  31. package/dist/research/workspace/artifact-views.js.map +1 -0
  32. package/dist/research/workspace/artifacts.d.ts +3 -0
  33. package/dist/research/workspace/artifacts.js +13 -0
  34. package/dist/research/workspace/artifacts.js.map +1 -1
  35. package/dist/research/workspace/audit-bundle.d.ts +9 -0
  36. package/dist/research/workspace/audit-bundle.js +124 -8
  37. package/dist/research/workspace/audit-bundle.js.map +1 -1
  38. package/dist/research/workspace/content-evidence.d.ts +3 -0
  39. package/dist/research/workspace/content-evidence.js +149 -22
  40. package/dist/research/workspace/content-evidence.js.map +1 -1
  41. package/dist/research/workspace/executor.d.ts +5 -1
  42. package/dist/research/workspace/executor.js +106 -22
  43. package/dist/research/workspace/executor.js.map +1 -1
  44. package/dist/research/workspace/native-run.d.ts +77 -0
  45. package/dist/research/workspace/native-run.js +637 -0
  46. package/dist/research/workspace/native-run.js.map +1 -0
  47. package/dist/research/workspace/preflight.d.ts +8 -2
  48. package/dist/research/workspace/preflight.js +17 -14
  49. package/dist/research/workspace/preflight.js.map +1 -1
  50. package/dist/research/workspace/project-authority.d.ts +21 -0
  51. package/dist/research/workspace/project-authority.js +186 -0
  52. package/dist/research/workspace/project-authority.js.map +1 -0
  53. package/dist/research/workspace/project-mutations.d.ts +26 -0
  54. package/dist/research/workspace/project-mutations.js +332 -0
  55. package/dist/research/workspace/project-mutations.js.map +1 -0
  56. package/dist/research/workspace/projects.d.ts +2 -1
  57. package/dist/research/workspace/projects.js +133 -91
  58. package/dist/research/workspace/projects.js.map +1 -1
  59. package/dist/research/workspace/publication-workflow.d.ts +3 -0
  60. package/dist/research/workspace/publication-workflow.js +15 -3
  61. package/dist/research/workspace/publication-workflow.js.map +1 -1
  62. package/dist/research/workspace/request-provenance.d.ts +67 -0
  63. package/dist/research/workspace/request-provenance.js +115 -0
  64. package/dist/research/workspace/request-provenance.js.map +1 -0
  65. package/dist/research/workspace/review-executor.js +22 -11
  66. package/dist/research/workspace/review-executor.js.map +1 -1
  67. package/dist/research/workspace/runtime.d.ts +57 -0
  68. package/dist/research/workspace/runtime.js +292 -140
  69. package/dist/research/workspace/runtime.js.map +1 -1
  70. package/dist/research/workspace/sanitization.js +1 -1
  71. package/dist/research/workspace/sanitization.js.map +1 -1
  72. package/dist/research/workspace/schema-compatibility.d.ts +5 -0
  73. package/dist/research/workspace/schema-compatibility.js +55 -0
  74. package/dist/research/workspace/schema-compatibility.js.map +1 -0
  75. package/dist/research/workspace/schemas.d.ts +6 -0
  76. package/dist/research/workspace/schemas.js +13 -2
  77. package/dist/research/workspace/schemas.js.map +1 -1
  78. package/dist/research/workspace/scientific-design.js +8 -17
  79. package/dist/research/workspace/scientific-design.js.map +1 -1
  80. package/dist/research/workspace/scientific-fulfillment-audit.d.ts +3 -0
  81. package/dist/research/workspace/scientific-fulfillment-audit.js +137 -0
  82. package/dist/research/workspace/scientific-fulfillment-audit.js.map +1 -0
  83. package/dist/research/workspace/scientific-fulfillment.d.ts +121 -0
  84. package/dist/research/workspace/scientific-fulfillment.js +539 -0
  85. package/dist/research/workspace/scientific-fulfillment.js.map +1 -0
  86. package/dist/research/workspace/scientific-review-execution.js +84 -61
  87. package/dist/research/workspace/scientific-review-execution.js.map +1 -1
  88. package/dist/research/workspace/scientific-review.d.ts +8 -2
  89. package/dist/research/workspace/scientific-review.js +205 -53
  90. package/dist/research/workspace/scientific-review.js.map +1 -1
  91. package/dist/research/workspace/setup-catalog.js +2 -2
  92. package/dist/research/workspace/task-acceptance.d.ts +134 -0
  93. package/dist/research/workspace/task-acceptance.js +544 -0
  94. package/dist/research/workspace/task-acceptance.js.map +1 -0
  95. package/dist/research/workspace/task-audit.d.ts +12 -0
  96. package/dist/research/workspace/task-audit.js +231 -0
  97. package/dist/research/workspace/task-audit.js.map +1 -0
  98. package/dist/research/workspace/task-contract.d.ts +142 -0
  99. package/dist/research/workspace/task-contract.js +506 -0
  100. package/dist/research/workspace/task-contract.js.map +1 -0
  101. package/dist/research/workspace/types.d.ts +6 -3
  102. package/dist/research/workspace/workspace.js +11 -8
  103. package/dist/research/workspace/workspace.js.map +1 -1
  104. package/package.json +1 -1
@@ -1,7 +1,13 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { cp, lstat, readFile, rm } from "node:fs/promises";
2
+ import { cp, lstat, readFile, realpath, rm } from "node:fs/promises";
3
3
  import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
4
4
  import { CliError } from "../../errors.js";
5
+ import { isConsistentAnalysisRunMetadata } from "./analysis-run.js";
6
+ import { artifactPromptContext, artifactReadInstructions, openArtifactViews, persistArtifactReads, persistArtifactViewIndex, writeArtifactViewIndex, verifyPersistedArtifactViewIndex, } from "./artifact-views.js";
7
+ import { taskContext } from "./task-contract.js";
8
+ import { nativeRunArtifactRecords } from "./native-run.js";
9
+ import { compileTaskAcceptanceContext, inspectProjectTask, taskAcceptancePrompt, validateTaskReview, } from "./task-acceptance.js";
10
+ import { assertProjectAuthority, projectAuthority, projectAuthorityIndex, projectWithEffectiveAuthority, readProjectAuthorityIndex, } from "./project-authority.js";
5
11
  import { dataPublicSchemas } from "../../data/schemas.js";
6
12
  import { loadCapabilityDeclarations, stageLockedCapabilities, verifyCapabilities, } from "./capabilities.js";
7
13
  import { startCapabilityBroker } from "./broker.js";
@@ -19,9 +25,9 @@ import { createReviewExecutor } from "./review-executor.js";
19
25
  import { requiredDiscoveryCapabilityIds } from "./external-skills.js";
20
26
  import { renderInputLineContext } from "./input-plan.js";
21
27
  import { freezeClaimEvidenceGraph, freezeInferenceSnapshot, loadCurrentInferenceSnapshot, } from "./inference.js";
22
- import { appendJournalEvent, readJournal, verifyJournal } from "./journal.js";
28
+ import { appendJournalEvent, readJournal, readVerifiedJournal, verifyJournal } from "./journal.js";
23
29
  import { nativeActivityRecordSchema } from "./native-activity.js";
24
- import { calculateAgentCallTokenReservation, RESEARCH_BROKER_MAX_TURNS, RESEARCH_ESTIMATED_BYTES_PER_TOKEN, RESEARCH_MAX_REPAIR_SOURCE_BYTES, RESEARCH_REPAIR_MAX_TURNS, researchStageContextTokenLimit, researchStructuredOutputMaxTurns, reservedAgentPackageCost, } from "./preflight.js";
30
+ import { calculateAgentCallTokenReservation, RESEARCH_BROKER_MAX_TURNS, RESEARCH_ESTIMATED_BYTES_PER_TOKEN, RESEARCH_MAX_REPAIR_SOURCE_BYTES, RESEARCH_REPAIR_MAX_TURNS, RESEARCH_PACKET_READ_MAX_TURNS, RESEARCH_EXPECTED_ARTIFACT_READ_TOKENS, researchStructuredOutputMaxTurns, reservedAgentPackageCost, } from "./preflight.js";
25
31
  import { blockingScientificGate, listProjects, loadProject, nextReadyPackage, packageById, refreshProject, saveProject, scientificGateRecommendedAction, } from "./projects.js";
26
32
  import { assertResearchPolicyBinding } from "./research-policy.js";
27
33
  import { assertScientificGateForStage } from "./scientific-review.js";
@@ -177,13 +183,13 @@ async function runResearchWorkspaceInternal(root, options, packageExecutor, allo
177
183
  if (options.dryRun)
178
184
  return dryRunResult(root, requestId, options.projectId);
179
185
  return withWorkspaceLock(root, "research.run", async () => {
180
- await verifyJournal(workspacePaths(root).journal);
186
+ const authority = await readProjectAuthorityIndex(root);
181
187
  const config = await loadWorkspaceConfig(root);
182
188
  assertExecutionConfiguration(config);
183
189
  const reviewerPackageExecutor = useConfiguredReviewerExecutor
184
190
  ? createReviewExecutor({ root, execution: config.reviewerExecution }).execute
185
191
  : null;
186
- for (const project of await projectsForRun(root, options.projectId)) {
192
+ for (const project of await projectsForRun(root, options.projectId, authority)) {
187
193
  await assertProjectPublicationPolicy(root, project);
188
194
  }
189
195
  let doctorAttestation = null;
@@ -205,7 +211,7 @@ async function runResearchWorkspaceInternal(root, options, packageExecutor, allo
205
211
  });
206
212
  }
207
213
  doctorAttestation = verification.attestation;
208
- const unconfirmed = (await projectsForRun(root, options.projectId)).filter((project) => config.budget.maxCostUsd > config.budget.confirmationCostUsd &&
214
+ const unconfirmed = (await projectsForRun(root, options.projectId, authority)).filter((project) => config.budget.maxCostUsd > config.budget.confirmationCostUsd &&
209
215
  !project.budgetConfirmedAt);
210
216
  if (unconfirmed.length) {
211
217
  throw new CliError("Production research budget has not been explicitly confirmed.", {
@@ -219,7 +225,7 @@ async function runResearchWorkspaceInternal(root, options, packageExecutor, allo
219
225
  const executed = [];
220
226
  let cycles = 0;
221
227
  while (cycles < options.maxCycles) {
222
- const projects = await projectsForRun(root, options.projectId);
228
+ const projects = await projectsForRun(root, options.projectId, authority);
223
229
  const selected = projects
224
230
  .map((project) => ({ project, workPackage: nextReadyPackage(project) }))
225
231
  .filter((item) => Boolean(item.workPackage) &&
@@ -233,7 +239,7 @@ async function runResearchWorkspaceInternal(root, options, packageExecutor, allo
233
239
  const results = await Promise.all(selected.map(({ project, workPackage }) => executeWorkPackage(root, project.id, workPackage.id, config, options, requestId, packageExecutor, reviewerPackageExecutor, doctorAttestation)));
234
240
  executed.push(...results);
235
241
  }
236
- const result = await summarizeRun(root, requestId, cycles, executed, options.maxCycles, options.projectId);
242
+ const result = await summarizeRun(root, requestId, cycles, executed, options.maxCycles, options.projectId, authority);
237
243
  emitProgress(options, progressEvent("run.completed", requestId, options.projectId ?? null, null, null, {
238
244
  status: result.status,
239
245
  stopReason: result.stopReason,
@@ -304,12 +310,70 @@ export async function inspectNativeResearchStage(root, project) {
304
310
  };
305
311
  }
306
312
  }
313
+ export async function readNativeStageArtifact(input) {
314
+ return withWorkspaceLock(input.root, "research.native-stage.read", async () => {
315
+ const project = await loadProject(input.root, input.projectId);
316
+ const events = await readVerifiedJournal(workspacePaths(input.root).journal);
317
+ assertProjectAuthority(project, projectAuthorityIndex(events));
318
+ const session = await readNativeStageSession(input.root, input.projectId);
319
+ const prepared = events.findLast((event) => event.scope === project.id &&
320
+ event.type === "native.stage.prepared" &&
321
+ event.payload.sessionId === input.sessionId);
322
+ const runtimeRoot = await realpath(workspacePaths(input.root).runtime);
323
+ const capsule = await realpath(session.capsuleProject);
324
+ if (session.packet.sessionId !== input.sessionId ||
325
+ prepared?.payload.packetSha256 !== session.packet.packetSha256 ||
326
+ project.handoff.state !== "agent-actionable" ||
327
+ relative(runtimeRoot, capsule).startsWith("..") ||
328
+ isAbsolute(relative(runtimeRoot, capsule)) ||
329
+ Date.now() - Date.parse(session.packet.preparedAt) >
330
+ session.packet.limits.maxWallSeconds * 1000 ||
331
+ packageById(project, session.packet.packageId).status !== "running" ||
332
+ !session.packet.artifactViews) {
333
+ throw new CliError("Artifact reads require the exact active native stage packet.", {
334
+ code: "RESEARCH_NATIVE_STAGE_SESSION_REQUIRED",
335
+ exitCode: 3,
336
+ });
337
+ }
338
+ await assertNativeStageBinding(input.root, project, session.packet);
339
+ const views = await openArtifactViews(session.capsuleProject, session.packet.artifactViews, session.packet.packetSha256);
340
+ if (views.index.projectId !== project.id)
341
+ throw new CliError("Artifact directory belongs to another project.", {
342
+ code: "RESEARCH_ARTIFACT_VIEW_INVALID",
343
+ exitCode: 3,
344
+ });
345
+ if (!input.selection)
346
+ return views.list(input.listing);
347
+ const result = await views.read(input.selection);
348
+ await persistArtifactReads(projectRoot(input.root, project.id), session.capsuleProject, session.packet.artifactViews, session.packet.packetSha256, [result.receipt]);
349
+ await appendJournalEvent(workspacePaths(input.root).journal, "native.artifact.read", project.id, sanitizeResearchRecord({ ...result.receipt }));
350
+ return result;
351
+ });
352
+ }
353
+ /** Called under an existing short lease; observing a calculation never opens a producer. */
354
+ export async function inspectNativeCalculationScope(root, project, sessionId) {
355
+ const session = await readNativeStageSession(root, project.id);
356
+ const events = await readVerifiedJournal(workspacePaths(root).journal);
357
+ const prepared = events.findLast((event) => event.scope === project.id &&
358
+ event.type === "native.stage.prepared" &&
359
+ event.payload.sessionId === sessionId);
360
+ if (session.packet.sessionId !== sessionId ||
361
+ !["analyze", "synthesize"].includes(session.packet.stage) ||
362
+ prepared?.payload.packetSha256 !== session.packet.packetSha256 ||
363
+ packageById(project, session.packet.packageId).status !== "running" ||
364
+ Date.now() - Date.parse(session.packet.preparedAt) > session.packet.limits.maxWallSeconds * 1000) {
365
+ throw new CliError("The calculation must bind the exact active post-acquisition native packet.", { code: "RESEARCH_NATIVE_RUN_BINDING_INVALID", exitCode: 3 });
366
+ }
367
+ await assertNativeStageBinding(root, project, session.packet);
368
+ return session.packet.packetSha256;
369
+ }
307
370
  export async function prepareNativeResearchStage(input) {
308
371
  return withWorkspaceLock(input.root, "research.native-stage.prepare", async () => {
309
- await verifyJournal(workspacePaths(input.root).journal);
372
+ const authority = await readProjectAuthorityIndex(input.root);
310
373
  const config = await loadWorkspaceConfig(input.root);
311
374
  assertExecutionConfiguration(config);
312
- const project = await loadProject(input.root, input.projectId);
375
+ const project = projectWithEffectiveAuthority(await loadProject(input.root, input.projectId), authority);
376
+ assertProjectAuthority(project, authority);
313
377
  await assertScientificGateForStage(input.root, project, input.stage);
314
378
  await assertProjectPublicationPolicy(input.root, project);
315
379
  if (config.producer.agent !== input.hostAgent) {
@@ -391,15 +455,24 @@ export async function prepareNativeResearchStage(input) {
391
455
  let preparedStatePersisted = false;
392
456
  try {
393
457
  capsule = await createCapsule(input.root, project, workPackage, sessionId, config);
394
- const stageContextContent = await stageContextForPackage(capsule.projectRoot, project, workPackage, config);
458
+ const stageContextContent = await stageContextForPackage(capsule.projectRoot, project, workPackage, capsule.artifactViews, config);
395
459
  const declarations = await loadCapabilityDeclarations(input.root);
396
460
  const hasBrokeredEvidence = declarations.capabilities.some((capability) => capability.permissions.includes("brokered-network"));
397
461
  const dataCapabilities = projectResearchDataCapabilities();
398
462
  const hasDataEvidence = dataCapabilities.capabilities.length > 0;
399
- const basePrompt = packagePrompt(project, workPackage, capsule.inputManifest, capsule.stagedSkills, capsule.capabilityDocumentation, null, capsule.contextBundle, capsule.contextBundleContent, stageContextContent, discovery, await listEvidenceCandidates(input.root, project.id));
463
+ const basePrompt = packagePrompt(project, workPackage, capsule.inputManifest, capsule.stagedSkills, capsule.capabilityDocumentation, null, capsule.contextBundle, capsule.contextBundleContent, stageContextContent, discovery, await listEvidenceCandidates(input.root, project.id), "native-host");
464
+ const taskContract = await taskContext(input.root, project.id, authority.taskEvents.get(project.id) ?? []);
465
+ const taskPrompt = taskContract
466
+ ? "Original task and current authorized scope (a workflow finish is not task completion):\n" +
467
+ (await artifactPromptContext(capsule.projectRoot, capsule.artifactViews, [
468
+ "inputs/task-context.json",
469
+ ]))
470
+ : "";
400
471
  const prompt = [
401
472
  "Perform this producer stage in the current interactive host session. Do not launch codex exec, claude -p, or any other nested reasoning agent.",
402
473
  capsule.publicationPolicyDocumentation,
474
+ artifactReadInstructions(capsule.artifactViews),
475
+ taskPrompt,
403
476
  input.stage === "discover" && (hasBrokeredEvidence || hasDataEvidence)
404
477
  ? [
405
478
  hasBrokeredEvidence
@@ -416,7 +489,6 @@ export async function prepareNativeResearchStage(input) {
416
489
  ? "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."
417
490
  : "Do not acquire additional evidence in this stage.",
418
491
  basePrompt,
419
- "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.",
420
492
  ].join("\n\n");
421
493
  const preparedAt = new Date().toISOString();
422
494
  const bindingSha256 = await nativeStageBinding(input.root, project, workPackage);
@@ -436,6 +508,8 @@ export async function prepareNativeResearchStage(input) {
436
508
  ? { inputOnlyProvenanceIds: capsule.inputManifest.map((record) => record.id) }
437
509
  : {}),
438
510
  publicationPolicy: capsule.publicationPolicy,
511
+ taskContract,
512
+ artifactViews: capsule.artifactViews,
439
513
  discovery,
440
514
  limits: {
441
515
  maxOutputBytes: config.budget.maxBytesPerPackage,
@@ -446,6 +520,42 @@ export async function prepareNativeResearchStage(input) {
446
520
  maxWallSeconds: config.budget.packageMaxWallSeconds[input.stage],
447
521
  },
448
522
  commands: {
523
+ listArtifacts: {
524
+ argv: [
525
+ "tiangong-ai",
526
+ "research",
527
+ "project",
528
+ "stage",
529
+ "artifacts",
530
+ project.id,
531
+ "--session",
532
+ sessionId,
533
+ "--workspace",
534
+ input.root,
535
+ "--json",
536
+ ],
537
+ },
538
+ readArtifact: {
539
+ argv: [
540
+ "tiangong-ai",
541
+ "research",
542
+ "project",
543
+ "stage",
544
+ "read",
545
+ project.id,
546
+ "--session",
547
+ sessionId,
548
+ "--artifact",
549
+ "<objectId-from-directory>",
550
+ "--offset",
551
+ "<byte-offset>",
552
+ "--length",
553
+ "<bytes-or-all>",
554
+ "--workspace",
555
+ input.root,
556
+ "--json",
557
+ ],
558
+ },
449
559
  inspectAccess: project.scientificDesign
450
560
  ? {
451
561
  argv: [
@@ -745,6 +855,7 @@ export async function prepareNativeResearchStage(input) {
745
855
  await appendJournalEvent(workspacePaths(input.root).journal, "native.stage.prepared", project.id, {
746
856
  sessionId,
747
857
  packetSha256: packet.packetSha256,
858
+ artifactViewIndexSha256: packet.artifactViews.sha256,
748
859
  bindingSha256,
749
860
  projectId: project.id,
750
861
  packageId: workPackage.id,
@@ -792,6 +903,8 @@ export function nativeEvidenceRequestSchema(requireAcquisitionRoute) {
792
903
  }
793
904
  export async function submitNativeResearchStage(input) {
794
905
  return withWorkspaceLock(input.root, "research.native-stage.submit", async () => {
906
+ const authority = await readProjectAuthorityIndex(input.root);
907
+ assertProjectAuthority(await loadProject(input.root, input.projectId), authority);
795
908
  const session = await readNativeStageSession(input.root, input.projectId);
796
909
  if (session.packet.sessionId !== input.sessionId) {
797
910
  throw new CliError("Native stage session ID does not match the active session.", {
@@ -1264,7 +1377,11 @@ async function executeWorkPackage(root, projectId, packageId, config, options, r
1264
1377
  : null;
1265
1378
  const stageOutputTokens = discovery?.plan.outputTokenLimit ?? config.budget.maxOutputTokens;
1266
1379
  const reservation = reservePackageBudget(project, workPackage, config, discovery?.plan.reservedDiscoverTokens);
1267
- const capsule = await createCapsule(root, project, workPackage, runId, config);
1380
+ const taskAcceptance = workPackage.stage === "review" ? await compileTaskAcceptanceContext(root, project) : null;
1381
+ if (taskAcceptance?.requirements.some((row) => row.current && row.status === "unanswered")) {
1382
+ throw new CliError("Record an actual check or an honest not-run/inconclusive disposition for each current requirement before spending review budget.", { code: "RESEARCH_TASK_ACCEPTANCE_REQUIRED", exitCode: 3 });
1383
+ }
1384
+ const capsule = await createCapsule(root, project, workPackage, runId, config, taskAcceptance);
1268
1385
  capsuleRoot = capsule.capsuleRoot;
1269
1386
  capsuleDisposition = capsuleDispositionForHost(config.producer.agent);
1270
1387
  retainedCapsuleId =
@@ -1278,7 +1395,7 @@ async function executeWorkPackage(root, projectId, packageId, config, options, r
1278
1395
  packet: capsule.reviewPacketRecord,
1279
1396
  });
1280
1397
  }
1281
- const stageContextContent = await stageContextForPackage(capsule.projectRoot, project, workPackage, config);
1398
+ const stageContextContent = await stageContextForPackage(capsule.projectRoot, project, workPackage, capsule.artifactViews, config);
1282
1399
  const route = workPackage.executor === "reviewer" ? config.reviewer : config.producer;
1283
1400
  const selectedPackageExecutor = workPackage.executor === "reviewer" && reviewerPackageExecutor
1284
1401
  ? reviewerPackageExecutor
@@ -1300,9 +1417,13 @@ async function executeWorkPackage(root, projectId, packageId, config, options, r
1300
1417
  options,
1301
1418
  requestId,
1302
1419
  purpose: "primary",
1303
- prompt: packagePrompt(project, workPackage, capsule.inputManifest, capsule.stagedSkills, capsule.capabilityDocumentation, capsule.reviewPacketSha256, capsule.contextBundle, capsule.contextBundleContent, stageContextContent, discovery, await listEvidenceCandidates(root, project.id)) +
1420
+ prompt: packagePrompt(project, workPackage, capsule.inputManifest, capsule.stagedSkills, capsule.capabilityDocumentation, capsule.reviewPacketSha256, capsule.contextBundle, capsule.contextBundleContent, stageContextContent, discovery, await listEvidenceCandidates(root, project.id), "headless-cli") +
1304
1421
  (capsule.publicationPolicyDocumentation
1305
1422
  ? `\n\n${capsule.publicationPolicyDocumentation}`
1423
+ : "") +
1424
+ (capsule.taskAcceptancePrompt ? `\n\n${capsule.taskAcceptancePrompt}` : "") +
1425
+ (workPackage.stage === "review"
1426
+ ? `\n\n${artifactReadInstructions(capsule.artifactViews)}`
1306
1427
  : ""),
1307
1428
  brokerUrl: primaryBrokerUrl,
1308
1429
  inputOnlyProvenance,
@@ -1314,6 +1435,15 @@ async function executeWorkPackage(root, projectId, packageId, config, options, r
1314
1435
  assertPreCallTokenReservation(project, workPackage, config, primaryRequest, 0, true);
1315
1436
  result = await withHeartbeat(selectedPackageExecutor(primaryRequest), options, requestId, project, workPackage, config);
1316
1437
  accountedResult = result;
1438
+ if (primaryRequest.artifactViews && result.artifactReads?.length) {
1439
+ await persistArtifactReads(projectRoot(root, project.id), capsule.projectRoot, capsule.artifactViews, primaryRequest.artifactViews.packetSha256, result.artifactReads);
1440
+ await appendJournalEvent(workspacePaths(root).journal, "review.artifacts.read", project.id, sanitizeResearchRecord({
1441
+ requestId,
1442
+ packetSha256: primaryRequest.artifactViews.packetSha256,
1443
+ indexSha256: capsule.artifactViews.sha256,
1444
+ receipts: result.artifactReads.map((receipt) => receipt.receiptSha256),
1445
+ }));
1446
+ }
1317
1447
  assertExecutorSucceeded(result);
1318
1448
  assertActualPackageBudget(project, workPackage, config, result, stageOutputTokens);
1319
1449
  let acceptedRaw = result.stdout;
@@ -1554,10 +1684,17 @@ async function stagePublicationPolicy(root, project, capsuleProject) {
1554
1684
  manifestPath: resolveContained(capsuleProject, manifestPath),
1555
1685
  documents,
1556
1686
  };
1557
- await writeJsonAtomic(staged.manifestPath, staged);
1687
+ await writeJsonAtomic(staged.manifestPath, {
1688
+ ...staged,
1689
+ manifestPath,
1690
+ documents: documents.map((document) => ({
1691
+ ...document,
1692
+ path: relative(capsuleProject, document.path).replaceAll("\\", "/"),
1693
+ })),
1694
+ });
1558
1695
  return staged;
1559
1696
  }
1560
- async function createCapsule(root, project, workPackage, runId, config) {
1697
+ async function createCapsule(root, project, workPackage, runId, config, taskAcceptance = null) {
1561
1698
  const paths = workspacePaths(root);
1562
1699
  const capsuleRoot = join(paths.runtime, runId);
1563
1700
  const capsuleProject = join(capsuleRoot, "project");
@@ -1663,33 +1800,65 @@ async function createCapsule(root, project, workPackage, runId, config) {
1663
1800
  "Approved Research Policy (mandatory for every producer and reviewer decision):",
1664
1801
  `resolvedPolicySha256=${publicationPolicy.resolvedPolicySha256}`,
1665
1802
  `verdictCeiling=${publicationPolicy.verdictCeiling}`,
1666
- ...publicationPolicy.documents.map((document) => `- ${document.kind}:${document.id} sha256=${document.sha256} path=${document.path}`),
1803
+ ...publicationPolicy.documents.map((document) => `- ${document.kind}:${document.id} sha256=${document.sha256} path=${relative(capsuleProject, document.path).replaceAll("\\", "/")}`),
1667
1804
  "Read every listed policy document before reasoning. Generic defaults are explicit constraints, not evidence that a target journal will accept the manuscript.",
1668
1805
  ].join("\n")
1669
1806
  : "";
1670
1807
  const stagedSkills = await stageLockedCapabilities(root, join(capsuleProject, "skills"));
1671
- const capabilityDocumentation = await buildCapabilityDocumentation(capsuleProject, stagedSkills, config.budget.maxInputContextTokens * RESEARCH_ESTIMATED_BYTES_PER_TOKEN);
1808
+ const capabilityDocumentation = await buildCapabilityDocumentation(capsuleProject, stagedSkills);
1809
+ await writeTextAtomic(join(capsuleProject, "inputs/capability-documentation.txt"), capabilityDocumentation);
1672
1810
  const reviewEvidenceContext = workPackage.stage === "review"
1673
1811
  ? await writeReviewEvidenceContext(root, project.id, capsuleProject, contextBundleContent, evidenceReceipts, contextualEvidenceArtifacts, config.budget.maxInputContextTokens * RESEARCH_ESTIMATED_BYTES_PER_TOKEN)
1674
1812
  : null;
1813
+ const task = taskAcceptance ?? (await taskContext(root, project.id));
1814
+ if (task) {
1815
+ await writeJsonAtomic(join(capsuleProject, "inputs/task-context.json"), task);
1816
+ const source = task.requestProvenance.source;
1817
+ if (source) {
1818
+ const logical = `task/request-sources/${source.objectSha256}.json`;
1819
+ await ensureDirectory(dirname(resolveContained(capsuleProject, logical)));
1820
+ await cp(resolveContained(projectRoot(root, project.id), logical), resolveContained(capsuleProject, logical), { force: false });
1821
+ }
1822
+ }
1823
+ const taskArtifacts = new Map((taskAcceptance?.results ?? []).map((record) => [record.path, record]));
1824
+ if (task)
1825
+ for (const record of await nativeRunArtifactRecords(root, project.id))
1826
+ taskArtifacts.set(record.path, record);
1827
+ for (const result of taskArtifacts.values()) {
1828
+ await ensureDirectory(dirname(resolveContained(capsuleProject, result.path)));
1829
+ await cp(resolveContained(projectRoot(root, project.id), result.path), resolveContained(capsuleProject, result.path), { force: false });
1830
+ if ((await sha256File(resolveContained(capsuleProject, result.path))) !== result.sha256)
1831
+ throw new CliError("Native run/check artifact changed while staging the packet.", {
1832
+ code: "RESEARCH_TASK_ARTIFACT_DRIFT",
1833
+ exitCode: 3,
1834
+ });
1835
+ }
1675
1836
  const reviewPacket = reviewEvidenceContext
1676
- ? await writeReviewPacket(root, capsuleProject, project, inputManifest, evidenceReceipts, evidenceArtifacts, reviewEvidenceContext.persistent)
1837
+ ? await writeReviewPacket(root, capsuleProject, project, inputManifest, evidenceReceipts, evidenceArtifacts, reviewEvidenceContext.persistent, taskAcceptance)
1677
1838
  : null;
1839
+ const artifactViews = reviewPacket?.artifactViews ?? (await writeArtifactViewIndex(capsuleProject, project.id));
1678
1840
  return {
1679
1841
  capsuleRoot,
1680
1842
  projectRoot: capsuleProject,
1681
1843
  inputManifest,
1682
1844
  contextBundle,
1683
- contextBundleContent,
1845
+ contextBundleContent: await artifactPromptContext(capsuleProject, artifactViews, [
1846
+ "inputs/context-bundle.txt",
1847
+ ]),
1684
1848
  stagedSkills,
1685
- capabilityDocumentation,
1849
+ capabilityDocumentation: await artifactPromptContext(capsuleProject, artifactViews, [
1850
+ "inputs/capability-documentation.txt",
1851
+ ]),
1686
1852
  publicationPolicy,
1687
1853
  publicationPolicyDocumentation,
1688
1854
  reviewPacketSha256: reviewPacket?.sha256 ?? null,
1689
1855
  reviewPacketRecord: reviewPacket?.record ?? null,
1856
+ taskAcceptance,
1857
+ taskAcceptancePrompt: await taskAcceptancePrompt(taskAcceptance, capsuleProject, artifactViews),
1858
+ artifactViews,
1690
1859
  };
1691
1860
  }
1692
- async function buildCapabilityDocumentation(capsuleProject, stagedSkills, maxBytes) {
1861
+ async function buildCapabilityDocumentation(capsuleProject, stagedSkills) {
1693
1862
  const documents = [
1694
1863
  {
1695
1864
  path: "skills/manifest.json",
@@ -1705,14 +1874,6 @@ async function buildCapabilityDocumentation(capsuleProject, stagedSkills, maxByt
1705
1874
  const bundle = documents
1706
1875
  .map((document) => `--- ${document.path} ---\n${document.content.trimEnd()}`)
1707
1876
  .join("\n\n");
1708
- const bytes = Buffer.byteLength(bundle, "utf8");
1709
- if (bytes > maxBytes) {
1710
- throw new CliError("Staged external Skill documentation exceeds the input context budget.", {
1711
- code: "RESEARCH_CAPABILITY_CONTEXT_BUDGET_EXCEEDED",
1712
- exitCode: 3,
1713
- details: { bytes, maxBytes, skills: stagedSkills.map((path) => basename(path)) },
1714
- });
1715
- }
1716
1877
  return bundle;
1717
1878
  }
1718
1879
  async function buildInputContextBundle(capsuleProject, inputManifest) {
@@ -1733,7 +1894,7 @@ async function buildInputContextBundle(capsuleProject, inputManifest) {
1733
1894
  }
1734
1895
  return `${sections.join("\n\n")}\n`;
1735
1896
  }
1736
- async function writeReviewPacket(root, capsuleProject, project, inputManifest, evidenceReceipts, evidenceArtifacts, reviewEvidenceContext) {
1897
+ async function writeReviewPacket(root, capsuleProject, project, inputManifest, evidenceReceipts, evidenceArtifacts, reviewEvidenceContext, taskAcceptance) {
1737
1898
  const snapshot = await loadCurrentEvidenceSnapshot(root, project.id);
1738
1899
  const immutableSnapshots = await loadImmutableEvidenceSnapshotChain(root, project.id, snapshot.snapshotSha256);
1739
1900
  const snapshotChain = await Promise.all(immutableSnapshots.map((immutableSnapshot) => {
@@ -1778,6 +1939,8 @@ async function writeReviewPacket(root, capsuleProject, project, inputManifest, e
1778
1939
  }
1779
1940
  }
1780
1941
  await writeJsonAtomic(join(capsuleProject, "inputs", "runtime-fingerprint.json"), environment);
1942
+ const artifactViews = await writeArtifactViewIndex(capsuleProject, project.id);
1943
+ const persistedViews = await persistArtifactViewIndex(projectRoot(root, project.id), capsuleProject, artifactViews);
1781
1944
  const packet = {
1782
1945
  schemaVersion: 1,
1783
1946
  projectId: project.id,
@@ -1790,6 +1953,8 @@ async function writeReviewPacket(root, capsuleProject, project, inputManifest, e
1790
1953
  parentSnapshotSha256: snapshot.parentSnapshotSha256,
1791
1954
  },
1792
1955
  snapshotChain,
1956
+ taskAcceptance,
1957
+ artifactViews: persistedViews,
1793
1958
  inputs: inputManifest,
1794
1959
  reviewEvidenceContext,
1795
1960
  inputFiles: [...inputFiles.values()].sort((left, right) => left.path.localeCompare(right.path)),
@@ -1809,7 +1974,7 @@ async function writeReviewPacket(root, capsuleProject, project, inputManifest, e
1809
1974
  };
1810
1975
  await writeJsonAtomic(join(capsuleProject, "inputs", "review-packet.json"), completePacket);
1811
1976
  const record = await persistReviewPacket(root, project.id, completePacket, packetSha256);
1812
- return { sha256: packetSha256, record };
1977
+ return { sha256: packetSha256, record, artifactViews };
1813
1978
  }
1814
1979
  async function writeReviewEvidenceContext(root, projectId, capsuleProject, inputContextBundle, evidenceReceipts, evidenceArtifacts, maxBytes) {
1815
1980
  const header = [
@@ -1891,15 +2056,10 @@ async function writeReviewEvidenceContext(root, projectId, capsuleProject, input
1891
2056
  ...views.map((view) => `${view.prefix}${view.active ? "" : view.content}${view.suffix}`),
1892
2057
  ].join("\n\n");
1893
2058
  const fixedBytes = Buffer.byteLength(`${fixedContent}\n`, "utf8");
1894
- if (fixedBytes > maxBytes) {
1895
- throw new CliError("Review evidence metadata exceeds the configured context budget.", {
1896
- code: "RESEARCH_REVIEW_CONTEXT_BUDGET_EXCEEDED",
1897
- exitCode: 3,
1898
- details: { fixedBytes, maxBytes, views: views.length },
1899
- });
1900
- }
1901
2059
  const activeViews = views.filter((view) => view.active).length;
1902
- const contentBudgetPerView = activeViews ? Math.floor((maxBytes - fixedBytes) / activeViews) : 0;
2060
+ const contentBudgetPerView = activeViews
2061
+ ? Math.max(0, Math.floor((maxBytes - fixedBytes) / activeViews))
2062
+ : 0;
1903
2063
  const sections = [
1904
2064
  header,
1905
2065
  ...views.map((view) => `${view.prefix}${view.active ? boundedUtf8ReviewExcerpt(view.content, contentBudgetPerView) : view.content}${view.suffix}`),
@@ -1907,14 +2067,6 @@ async function writeReviewEvidenceContext(root, projectId, capsuleProject, input
1907
2067
  const logicalPath = "inputs/review-evidence-context.txt";
1908
2068
  const path = resolveContained(capsuleProject, logicalPath);
1909
2069
  const content = `${sections.join("\n\n")}\n`;
1910
- const actualBytes = Buffer.byteLength(content, "utf8");
1911
- if (actualBytes > maxBytes) {
1912
- throw new CliError("Review evidence context exceeded its deterministic byte budget.", {
1913
- code: "RESEARCH_REVIEW_CONTEXT_BUDGET_EXCEEDED",
1914
- exitCode: 3,
1915
- details: { actualBytes, maxBytes },
1916
- });
1917
- }
1918
2070
  await writeTextAtomic(path, content);
1919
2071
  const capsule = await fileRecord(path, logicalPath);
1920
2072
  const persistentLogicalPath = `review/contexts/${capsule.sha256}.txt`;
@@ -1966,15 +2118,8 @@ async function writeProducerArtifactContext(capsuleProject, evidenceArtifacts, m
1966
2118
  .map((view) => `${view.prefix}${view.active ? "" : view.content}${view.suffix}`)
1967
2119
  .join("\n\n")}\n`;
1968
2120
  const fixedBytes = Buffer.byteLength(fixed, "utf8");
1969
- if (fixedBytes > maxBytes) {
1970
- throw new CliError("Producer artifact metadata exceeds the configured context budget.", {
1971
- code: "RESEARCH_INPUT_CONTEXT_BUDGET_EXCEEDED",
1972
- exitCode: 3,
1973
- details: { fixedBytes, maxBytes, artifacts: views.length },
1974
- });
1975
- }
1976
2121
  const activeViews = views.filter((view) => view.active).length;
1977
- const perView = activeViews ? Math.floor((maxBytes - fixedBytes) / activeViews) : 0;
2122
+ const perView = activeViews ? Math.max(0, Math.floor((maxBytes - fixedBytes) / activeViews)) : 0;
1978
2123
  const content = `${header}\n\n${views
1979
2124
  .map((view) => `${view.prefix}${view.active ? boundedUtf8ReviewExcerpt(view.content, perView) : view.content}${view.suffix}`)
1980
2125
  .join("\n\n")}\n`;
@@ -2130,11 +2275,18 @@ async function persistReviewPacket(root, projectId, packet, packetSha256) {
2130
2275
  }
2131
2276
  return fileRecord(path, logicalPath);
2132
2277
  }
2133
- async function loadVerifiedReviewPacket(root, projectId, packetSha256) {
2278
+ export async function loadVerifiedReviewPacket(root, projectId, packetSha256) {
2134
2279
  const logicalPath = `review/packets/${packetSha256}.json`;
2135
2280
  const path = resolveContained(projectRoot(root, projectId), logicalPath);
2136
2281
  const packet = await readJsonFile(path, "Research review packet");
2137
2282
  verifyReviewPacketValue(packet, packetSha256);
2283
+ if (packet.projectId !== projectId)
2284
+ throw new CliError("Review packet belongs to another project.", {
2285
+ code: "RESEARCH_REVIEW_PACKET_DRIFT",
2286
+ exitCode: 3,
2287
+ });
2288
+ if (packet.artifactViews !== undefined)
2289
+ await verifyPersistedArtifactViewIndex(projectRoot(root, projectId), projectId, packet.artifactViews);
2138
2290
  const context = packet.reviewEvidenceContext;
2139
2291
  if (!isObject(context) ||
2140
2292
  typeof context.path !== "string" ||
@@ -2286,18 +2438,33 @@ function reviewSafeReceipt(receipt) {
2286
2438
  };
2287
2439
  }
2288
2440
  function agentRequest(input) {
2289
- const toolPolicy = "none";
2441
+ const packetRead = input.purpose === "primary" &&
2442
+ input.workPackage.stage === "review" &&
2443
+ input.capsule.reviewPacketSha256 !== null;
2444
+ const toolPolicy = packetRead ? "packet-read" : "none";
2290
2445
  const maxTurns = input.purpose === "repair"
2291
2446
  ? RESEARCH_REPAIR_MAX_TURNS
2292
- : input.brokerUrl
2293
- ? RESEARCH_BROKER_MAX_TURNS
2294
- : researchStructuredOutputMaxTurns(input.route);
2447
+ : packetRead
2448
+ ? RESEARCH_PACKET_READ_MAX_TURNS
2449
+ : input.brokerUrl
2450
+ ? RESEARCH_BROKER_MAX_TURNS
2451
+ : researchStructuredOutputMaxTurns(input.route);
2295
2452
  return {
2296
2453
  route: input.route,
2297
2454
  prompt: input.prompt,
2298
- outputSchema: schemaForStage(input.workPackage.stage, input.capsule.reviewPacketSha256, input.inputOnlyProvenance
2299
- ? { inputOnlyProvenanceIds: input.capsule.inputManifest.map((record) => record.id) }
2300
- : {}),
2455
+ outputSchema: schemaForStage(input.workPackage.stage, input.capsule.reviewPacketSha256, {
2456
+ ...(input.inputOnlyProvenance
2457
+ ? { inputOnlyProvenanceIds: input.capsule.inputManifest.map((record) => record.id) }
2458
+ : {}),
2459
+ ...(input.capsule.taskAcceptance
2460
+ ? {
2461
+ taskAcceptance: {
2462
+ contextSha256: input.capsule.taskAcceptance.contextSha256,
2463
+ requirementSha256s: input.capsule.taskAcceptance.requirements.map((row) => row.requirementSha256),
2464
+ },
2465
+ }
2466
+ : {}),
2467
+ }),
2301
2468
  requestId: input.requestId,
2302
2469
  purpose: input.purpose,
2303
2470
  capsuleRoot: input.capsule.capsuleRoot,
@@ -2306,11 +2473,22 @@ function agentRequest(input) {
2306
2473
  timeoutSeconds: Math.min(remainingWallSeconds(input.project, input.config), input.maxWallSeconds ??
2307
2474
  input.config.budget.packageMaxWallSeconds[input.workPackage.stage]),
2308
2475
  maxTurns,
2476
+ ...(packetRead
2477
+ ? {
2478
+ reservationTurns: researchStructuredOutputMaxTurns(input.route),
2479
+ artifactViews: {
2480
+ index: input.capsule.artifactViews,
2481
+ packetSha256: input.capsule.reviewPacketSha256,
2482
+ },
2483
+ }
2484
+ : {}),
2309
2485
  maxOutputTokens: input.maxOutputTokens,
2310
2486
  maxToolContextTokens: input.brokerUrl
2311
2487
  ? input.config.budget.maxBrokerContextTokens *
2312
2488
  (input.brokerCallBudget ?? input.config.budget.maxBrokerCalls)
2313
- : 0,
2489
+ : packetRead
2490
+ ? RESEARCH_EXPECTED_ARTIFACT_READ_TOKENS
2491
+ : 0,
2314
2492
  maxCostUsd: input.maxCostUsd,
2315
2493
  expectedRuntime: input.expectedRuntime,
2316
2494
  toolPolicy,
@@ -2400,6 +2578,7 @@ async function validateOutputShape(root, project, workPackage, path, reviewPacke
2400
2578
  await validateAnalysis(path, value);
2401
2579
  }
2402
2580
  if (workPackage.stage === "review") {
2581
+ validateTaskReview(value, await compileTaskAcceptanceContext(root, project));
2403
2582
  if (value.decision !== "pass") {
2404
2583
  throw new CliError("Independent review requested revision.", {
2405
2584
  code: "RESEARCH_REVIEW_REVISION_REQUIRED",
@@ -2514,23 +2693,10 @@ async function validateAnalysis(path, value) {
2514
2693
  inputArtifactSha256s.some((sha256) => !allowedInputSha256s.has(sha256))) {
2515
2694
  throw new StructuredOutputError("Analysis run refers to an artifact outside the inference snapshot.");
2516
2695
  }
2517
- if (run.mode === "qualitative") {
2518
- if (run.status !== "not-applicable" ||
2519
- run.command !== null ||
2520
- run.randomSeed !== null ||
2521
- implementationSha256s.length > 0 ||
2522
- environmentSha256s.length > 0) {
2523
- throw new StructuredOutputError("Qualitative analysis run metadata is inconsistent.");
2524
- }
2525
- }
2526
- else if (run.status !== "reproduced" ||
2527
- typeof run.command !== "string" ||
2528
- run.command.trim().length < 1 ||
2529
- typeof run.randomSeed !== "string" ||
2530
- run.randomSeed.trim().length < 1 ||
2531
- implementationSha256s.length < 1 ||
2532
- environmentSha256s.length < 1) {
2533
- throw new StructuredOutputError("Computational analysis requires exact reproduced run metadata.");
2696
+ if (!isConsistentAnalysisRunMetadata(run)) {
2697
+ throw new StructuredOutputError(run.mode === "qualitative"
2698
+ ? "Qualitative analysis run metadata is inconsistent."
2699
+ : "Computational analysis requires exact reproduced run metadata.");
2534
2700
  }
2535
2701
  const findings = value.findings;
2536
2702
  const findingIds = new Set();
@@ -2872,6 +3038,7 @@ async function closeProjectMechanically(root, project, workPackage) {
2872
3038
  throw deterministicError("Project review does not bind a valid review packet hash.");
2873
3039
  }
2874
3040
  const reviewPacket = await loadVerifiedReviewPacket(root, project.id, review.packetSha256);
3041
+ validateTaskReview(review, await compileTaskAcceptanceContext(root, project));
2875
3042
  await verifyReviewLedgerBinding(root, project.id, snapshot.snapshotId, review.packetSha256);
2876
3043
  const evidenceReceipts = await loadProjectEvidenceReceipts(root, project.id);
2877
3044
  const journal = await verifyJournal(workspacePaths(root).journal);
@@ -3038,7 +3205,7 @@ async function verifyProjectInputBindings(project) {
3038
3205
  async function outputRecords(root, project, logicalPaths) {
3039
3206
  return Promise.all(logicalPaths.map((logicalPath) => fileRecord(resolveContained(projectRoot(root, project.id), logicalPath), logicalPath)));
3040
3207
  }
3041
- async function stageContextForPackage(capsuleProject, project, workPackage, config) {
3208
+ async function stageContextForPackage(capsuleProject, project, workPackage, index, config) {
3042
3209
  let analyzeFallbackContext = [];
3043
3210
  if (workPackage.stage === "analyze") {
3044
3211
  const inference = JSON.parse(await readFile(resolveContained(capsuleProject, "outputs/inference-snapshot.json"), "utf8"));
@@ -3073,41 +3240,21 @@ async function stageContextForPackage(capsuleProject, project, workPackage, conf
3073
3240
  "outputs/report.md",
3074
3241
  ]
3075
3242
  : [];
3076
- const sections = [];
3077
- for (const logicalPath of logicalPaths) {
3078
- const content = await readFile(resolveContained(capsuleProject, logicalPath), "utf8");
3079
- sections.push(`### ${logicalPath}\n${content.trimEnd()}`);
3080
- }
3081
- const bundled = sections.join("\n\n");
3082
- const estimatedTokens = Math.ceil(Buffer.byteLength(bundled, "utf8") / RESEARCH_ESTIMATED_BYTES_PER_TOKEN);
3083
- const maxStageContextTokens = researchStageContextTokenLimit(config, workPackage.stage, project.lineage.kind === "addendum");
3084
- if (estimatedTokens > maxStageContextTokens) {
3085
- throw new CliError(`Admitted stage context exceeds the configured input context limit for ${workPackage.id}.`, {
3086
- code: "RESEARCH_INPUT_CONTEXT_BUDGET_EXCEEDED",
3087
- exitCode: 3,
3088
- details: {
3089
- packageId: workPackage.id,
3090
- estimatedTokens,
3091
- maxStageContextTokens,
3092
- limitField: "budget.maxInputContextTokens",
3093
- contextPaths: logicalPaths,
3094
- recommendedAction: "Keep frozen evidence unchanged. Use research project fork to regenerate a bounded context with concise metadata, or explicitly review budget.maxInputContextTokens in .tiangong-research/config.json and rerun project preflight plus workspace doctor before preparing this stage again. Do not raise output limits to accommodate input context.",
3095
- },
3096
- });
3097
- }
3098
- return bundled;
3243
+ return artifactPromptContext(capsuleProject, index, logicalPaths, Math.min(8_000, config.budget.maxInputContextTokens) * RESEARCH_ESTIMATED_BYTES_PER_TOKEN);
3099
3244
  }
3100
- function packagePrompt(project, workPackage, inputs, stagedSkills, capabilityDocumentation, reviewPacketSha256, contextBundle, contextBundleContent, stageContextContent, discovery, evidenceCandidates) {
3245
+ function packagePrompt(project, workPackage, inputs, stagedSkills, capabilityDocumentation, reviewPacketSha256, contextBundle, contextBundleContent, stageContextContent, discovery, evidenceCandidates, executionMode) {
3101
3246
  const stageInstructions = {
3102
3247
  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, broker result, and structured data 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.",
3103
3248
  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.",
3104
- analyze: "Use only the complete embedded inference snapshot below and return analysis schema v2. Bind the exact inference snapshot hash and one reproducible analysisRun. Every finding must cite admitted source IDs and exact evidenceAtomIds; a top-journal finding must also bind design claimIds. State uncertainty and applicability. Never promote a source-level citation when no exact atom supports the statement.",
3105
- synthesize: "Use only the complete embedded inference snapshot, analysis, and Claim–Evidence Graph below. Return the schema-defined object whose reportMarkdown separates supported conclusions, uncertainty, limitations, and next actions. Every material statement must remain within a graph-bound finding. Use real Markdown line breaks encoded exactly once for JSON; never place literal /n or double-escaped \\n markers in reportMarkdown.",
3106
- 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.`,
3249
+ analyze: "Use only the admitted inference snapshot and its packet-bound on-demand artifacts; return analysis schema v2. Bind the exact inference snapshot hash and one reproducible analysisRun. Every finding must cite admitted source IDs and exact evidenceAtomIds; a top-journal finding must also bind design claimIds. State uncertainty and applicability. Never promote a source-level citation when no exact atom supports the statement.",
3250
+ synthesize: "Use only the admitted inference snapshot, analysis, and Claim–Evidence Graph, reading their complete packet-bound objects on demand. Return the schema-defined object whose reportMarkdown separates supported conclusions, uncertainty, limitations, and next actions. Every material statement must remain within a graph-bound finding. Use real Markdown line breaks encoded exactly once for JSON; never place literal /n or double-escaped \\n markers in reportMarkdown.",
3251
+ review: `Independently inspect the supplied excerpts and use the packet-only artifact tools for further material, including failed checks and counterevidence. Every full object's size and SHA-256 is bound; metadata or a digest is not proof that you read it. Return the schema-defined review bound to packetSha256 ${reviewPacketSha256 ?? "unavailable"}. Use pass only after sufficient inspection supports every material claim and its stated limitations.`,
3107
3252
  close: "No agent action is allowed for mechanical closure.",
3108
3253
  };
3109
3254
  const prompt = [
3110
- "Operate only inside this isolated research capsule.",
3255
+ executionMode === "native-host"
3256
+ ? "Operate in the current native host with this project's authorized inputs and packet operations; host permissions still apply."
3257
+ : "Operate only inside this isolated research capsule.",
3111
3258
  `Project: ${project.id}`,
3112
3259
  `Question: ${project.question}`,
3113
3260
  `Stage: ${workPackage.stage}`,
@@ -3116,15 +3263,19 @@ function packagePrompt(project, workPackage, inputs, stagedSkills, capabilityDoc
3116
3263
  `Bounded input context bundle: ${JSON.stringify(contextBundle)}`,
3117
3264
  `Staged capability directories: ${JSON.stringify(stagedSkills.map((path) => `skills/${basename(path)}`))}`,
3118
3265
  workPackage.stage === "discover"
3119
- ? "The exact capability manifest and each staged external SKILL.md are embedded below. Use this documentation directly; filesystem tools are disabled."
3266
+ ? "The exact capability manifest and each staged external SKILL.md are supplied inline or through the native packet's artifact read commands. Read the required documentation; do not launch its standalone commands for broker work."
3120
3267
  : workPackage.stage === "acquire"
3121
3268
  ? "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."
3122
3269
  : "Capability files are provenance-bound but are not available as execution tools in this stage.",
3123
3270
  workPackage.stage === "discover"
3124
3271
  ? `Follow the reviewed discovery plan: ${JSON.stringify(discovery)}. The plan's max evidence-call 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. A broker receipt, structured data-runtime receipt, or immutable registered input is an admissible evidence path. Do not execute a staged Skill's curl/CLI examples or read provider environment variables. For generic broker capabilities, invoke fetch_candidate_source with the manifest capability ID and obey its exact declared HTTP method. For structured data capabilities, use the packet's dynamic catalog and describe command, then invoke runDataCapability; never call standalone data run for project evidence. Never place API keys, tokens, authorization data, cookies, or other credential-like fields in request files. The Research control plane injects declared logical credentials and persists only safe hash-bound results. Exercise every manifest capability with requiredForDiscovery=true and every project-required data capability ID, or the mechanical coverage gate will stop downstream work.`
3125
- : "Use only the complete embedded stage context; no tools or additional source reads are allowed.",
3272
+ : executionMode === "native-host" && workPackage.stage === "acquire"
3273
+ ? "Acquire files and readable derivatives only for provisionally admitted sources using the packet's bindDownload and registerArtifact commands. Do not reopen discovery, admit new sources, or access unrelated host files."
3274
+ : "Use only admitted stage context and this packet's read-only artifact channel. No new evidence acquisition or arbitrary host-file access is authorized.",
3126
3275
  stageInstructions[workPackage.stage],
3127
- "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.",
3276
+ executionMode === "native-host"
3277
+ ? "Do not write admitted output paths or control-plane files directly. 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."
3278
+ : "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.",
3128
3279
  "Do not edit project.json, input manifests, prior outputs, evidence objects, or staged capability files.",
3129
3280
  ];
3130
3281
  if (workPackage.stage === "discover") {
@@ -3136,10 +3287,10 @@ function packagePrompt(project, workPackage, inputs, stagedSkills, capabilityDoc
3136
3287
  publicationDate: candidate.publicationDate,
3137
3288
  excerpt: candidate.excerpt,
3138
3289
  originKind: candidate.origin.kind,
3139
- })))}`, "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);
3290
+ })))}`, "External capability documentation (inline or exact on-demand reference):", capabilityDocumentation, "Authorized local-input context (inline or exact on-demand reference). Full evidence files are intentionally withheld from producer packages when fullTextStaged=false; the read channel does not widen that authorization.", contextBundleContent);
3140
3291
  }
3141
3292
  if (stageContextContent) {
3142
- prompt.push("The complete admitted stage context is embedded below. Use it directly and do not re-read output files.", stageContextContent);
3293
+ prompt.push("Admitted stage context (inline or exact on-demand reference; no material is discarded to meet an embedding preference):", stageContextContent);
3143
3294
  }
3144
3295
  return prompt.join("\n\n");
3145
3296
  }
@@ -3160,7 +3311,7 @@ function assertPreCallTokenReservation(project, workPackage, config, request, al
3160
3311
  route: request.route,
3161
3312
  primaryPayloadTokens: Math.ceil((schemaBytes + promptBytes) / RESEARCH_ESTIMATED_BYTES_PER_TOKEN),
3162
3313
  repairPayloadTokens: Math.ceil((schemaBytes + RESEARCH_MAX_REPAIR_SOURCE_BYTES + 2_048) / RESEARCH_ESTIMATED_BYTES_PER_TOKEN),
3163
- maxTurns: request.maxTurns,
3314
+ maxTurns: request.reservationTurns ?? request.maxTurns,
3164
3315
  maxOutputTokens: request.maxOutputTokens,
3165
3316
  maxToolContextTokens: request.maxToolContextTokens ?? 0,
3166
3317
  maxRepairTokens: config.budget.maxRepairTokens,
@@ -3529,9 +3680,20 @@ function projectRunSummary(root, project) {
3529
3680
  usage: project.usage,
3530
3681
  };
3531
3682
  }
3532
- async function summarizeRun(root, requestId, cycles, executed, maxCycles, projectId) {
3533
- const projects = await projectsForRun(root, projectId);
3534
- const summaries = projects.map((project) => projectRunSummary(root, project));
3683
+ async function summarizeRun(root, requestId, cycles, executed, maxCycles, projectId, authority) {
3684
+ // One fresh journal view after execution includes newly committed reviewer results.
3685
+ const summaryAuthority = executed.length
3686
+ ? await readProjectAuthorityIndex(root)
3687
+ : (authority ?? (await readProjectAuthorityIndex(root)));
3688
+ const projects = await projectsForRun(root, projectId, summaryAuthority);
3689
+ const summaries = await Promise.all(projects.map(async (project) => {
3690
+ const summary = projectRunSummary(root, project);
3691
+ const events = summaryAuthority.taskEvents.get(project.id);
3692
+ if (!events)
3693
+ return summary;
3694
+ const task = await inspectProjectTask(root, project.id, events);
3695
+ return task.status === "configured" ? { ...summary, task } : summary;
3696
+ }));
3535
3697
  const unfinished = summaries.filter((project) => project.status !== "complete");
3536
3698
  const waiting = unfinished.filter((project) => project.status === "waiting-user" || project.status === "waiting-external");
3537
3699
  const hasReadyPackage = summaries.some((project) => project.readyPackage !== null);
@@ -3583,27 +3745,16 @@ async function summarizeRun(root, requestId, cycles, executed, maxCycles, projec
3583
3745
  projects: summaries,
3584
3746
  };
3585
3747
  }
3586
- async function projectsForRun(root, projectId) {
3748
+ async function projectsForRun(root, projectId, knownAuthority) {
3749
+ const authority = knownAuthority ?? (await readProjectAuthorityIndex(root));
3587
3750
  if (projectId) {
3588
- const project = await loadProject(root, projectId);
3589
- if (project.lineage.supersededBy ||
3590
- project.status === "archived" ||
3591
- project.status === "abandoned") {
3592
- throw new CliError(`Research project ${projectId} is historical and cannot be executed.`, {
3593
- code: "RESEARCH_PROJECT_NOT_AUTHORITATIVE",
3594
- exitCode: 3,
3595
- details: {
3596
- projectId,
3597
- status: project.status,
3598
- authoritativeProjectId: project.lineage.supersededBy ?? null,
3599
- },
3600
- });
3601
- }
3751
+ const project = projectWithEffectiveAuthority(await loadProject(root, projectId), authority);
3752
+ assertProjectAuthority(project, authority);
3602
3753
  return [project];
3603
3754
  }
3604
- return (await listProjects(root)).filter((project) => project.lineage.supersededBy === null &&
3605
- project.status !== "archived" &&
3606
- project.status !== "abandoned");
3755
+ return (await listProjects(root, authority))
3756
+ .filter((project) => projectAuthority(project, authority).state === "authoritative")
3757
+ .map((project) => projectWithEffectiveAuthority(project, authority));
3607
3758
  }
3608
3759
  function assertPublicEvidenceUrl(value, sourceId) {
3609
3760
  let url;
@@ -3704,6 +3855,7 @@ async function nativeStageBinding(root, project, workPackage) {
3704
3855
  return sha256Text(canonicalJson({
3705
3856
  projectId: project.id,
3706
3857
  questionSha256: sha256Text(project.question),
3858
+ taskContractSha256: (await taskContext(root, project.id))?.contractSha256 ?? null,
3707
3859
  evidenceRequirements: project.evidenceRequirements,
3708
3860
  inputs: project.inputs.map((record) => ({
3709
3861
  id: record.id,