@tiangong-ai/cli 0.0.31 → 0.0.33

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 (64) hide show
  1. package/AGENTS.md +2 -2
  2. package/README.md +140 -98
  3. package/dist/research/orchestration.js +519 -21
  4. package/dist/research/orchestration.js.map +1 -1
  5. package/dist/research/workspace/acquisition.d.ts +71 -0
  6. package/dist/research/workspace/acquisition.js +593 -0
  7. package/dist/research/workspace/acquisition.js.map +1 -0
  8. package/dist/research/workspace/artifacts.d.ts +43 -0
  9. package/dist/research/workspace/artifacts.js +544 -0
  10. package/dist/research/workspace/artifacts.js.map +1 -0
  11. package/dist/research/workspace/broker.d.ts +5 -0
  12. package/dist/research/workspace/broker.js +318 -43
  13. package/dist/research/workspace/broker.js.map +1 -1
  14. package/dist/research/workspace/companion-readiness.d.ts +11 -0
  15. package/dist/research/workspace/companion-readiness.js +75 -0
  16. package/dist/research/workspace/companion-readiness.js.map +1 -0
  17. package/dist/research/workspace/discovery-planning.d.ts +25 -0
  18. package/dist/research/workspace/discovery-planning.js +106 -0
  19. package/dist/research/workspace/discovery-planning.js.map +1 -0
  20. package/dist/research/workspace/discovery-status.d.ts +46 -0
  21. package/dist/research/workspace/discovery-status.js +183 -0
  22. package/dist/research/workspace/discovery-status.js.map +1 -0
  23. package/dist/research/workspace/discovery.d.ts +25 -0
  24. package/dist/research/workspace/discovery.js +268 -0
  25. package/dist/research/workspace/discovery.js.map +1 -0
  26. package/dist/research/workspace/downloads.d.ts +76 -0
  27. package/dist/research/workspace/downloads.js +274 -0
  28. package/dist/research/workspace/downloads.js.map +1 -0
  29. package/dist/research/workspace/evidence-ledger.d.ts +52 -0
  30. package/dist/research/workspace/evidence-ledger.js +487 -0
  31. package/dist/research/workspace/evidence-ledger.js.map +1 -0
  32. package/dist/research/workspace/evidence.d.ts +1 -0
  33. package/dist/research/workspace/evidence.js +2 -0
  34. package/dist/research/workspace/evidence.js.map +1 -1
  35. package/dist/research/workspace/input-plan.js +4 -0
  36. package/dist/research/workspace/input-plan.js.map +1 -1
  37. package/dist/research/workspace/native-activity.d.ts +65 -0
  38. package/dist/research/workspace/native-activity.js +153 -0
  39. package/dist/research/workspace/native-activity.js.map +1 -0
  40. package/dist/research/workspace/preflight.d.ts +18 -0
  41. package/dist/research/workspace/preflight.js +47 -20
  42. package/dist/research/workspace/preflight.js.map +1 -1
  43. package/dist/research/workspace/projects.d.ts +3 -1
  44. package/dist/research/workspace/projects.js +357 -5
  45. package/dist/research/workspace/projects.js.map +1 -1
  46. package/dist/research/workspace/runtime.d.ts +171 -3
  47. package/dist/research/workspace/runtime.js +1582 -77
  48. package/dist/research/workspace/runtime.js.map +1 -1
  49. package/dist/research/workspace/sanitization.js +28 -6
  50. package/dist/research/workspace/sanitization.js.map +1 -1
  51. package/dist/research/workspace/schemas.d.ts +3 -0
  52. package/dist/research/workspace/schemas.js +206 -33
  53. package/dist/research/workspace/schemas.js.map +1 -1
  54. package/dist/research/workspace/setup-catalog.js +2 -2
  55. package/dist/research/workspace/setup-wizard.js +45 -17
  56. package/dist/research/workspace/setup-wizard.js.map +1 -1
  57. package/dist/research/workspace/setup.d.ts +27 -5
  58. package/dist/research/workspace/setup.js +400 -85
  59. package/dist/research/workspace/setup.js.map +1 -1
  60. package/dist/research/workspace/types.d.ts +38 -3
  61. package/dist/research/workspace/workspace.d.ts +2 -0
  62. package/dist/research/workspace/workspace.js +128 -26
  63. package/dist/research/workspace/workspace.js.map +1 -1
  64. package/package.json +1 -1
@@ -5,7 +5,9 @@ export declare function loadProject(root: string, projectId: string): Promise<Pr
5
5
  export declare function listProjects(root: string): Promise<ProjectState[]>;
6
6
  export declare function saveProject(root: string, project: ProjectState): Promise<void>;
7
7
  export declare function retryProjectPackage(root: string, projectId: string, packageId?: string): Promise<ProjectState>;
8
- export declare function forkProject(root: string, sourceProjectId: string, targetProjectId: string, resumeThrough?: "discover" | "analyze" | "synthesize"): Promise<ProjectState>;
8
+ export declare function forkProject(root: string, sourceProjectId: string, targetProjectId: string, resumeThrough?: "discover" | "acquire" | "analyze" | "synthesize"): Promise<ProjectState>;
9
+ export declare function setProjectDisposition(root: string, projectId: string, disposition: "archived" | "abandoned", reason: string): Promise<ProjectState>;
10
+ export declare function createProjectAddendum(root: string, sourceProjectId: string, targetProjectId: string): Promise<ProjectState>;
9
11
  export declare function refreshProject(project: ProjectState): ProjectState;
10
12
  export declare function nextReadyPackage(project: ProjectState): WorkPackage | undefined;
11
13
  export declare function packageById(project: ProjectState, packageId: string): WorkPackage;
@@ -5,9 +5,12 @@ import { CliError } from "../../errors.js";
5
5
  import { RESEARCH_CONTROL_DIRECTORY } from "./constants.js";
6
6
  import { appendJournalEvent } from "./journal.js";
7
7
  import { cloneProjectEvidenceReceipts } from "./evidence.js";
8
+ import { appendEvidenceLedgerEvent, cloneEvidenceLedger, registerProjectInputCandidates, } from "./evidence-ledger.js";
9
+ import { cloneProjectArtifactRecords } from "./artifacts.js";
10
+ import { freezeEvidenceSnapshot, loadCurrentEvidenceSnapshot, loadImmutableEvidenceSnapshotChain, } from "./acquisition.js";
8
11
  import { projectInputsFromPlan, reverifyProjectInputPlan } from "./input-plan.js";
9
12
  import { evaluateProjectPreflight } from "./preflight.js";
10
- import { ensureDirectory, fileRecord, fileSize, readJsonFile, sha256File, workspacePaths, writeJsonAtomic, } from "./storage.js";
13
+ import { ensureDirectory, fileRecord, fileSize, isObject, readJsonFile, sha256File, workspacePaths, writeJsonAtomic, } from "./storage.js";
11
14
  import { loadWorkspaceConfig, withWorkspaceLock } from "./workspace.js";
12
15
  const PROJECT_ID_PATTERN = /^[a-z0-9][a-z0-9-]{2,63}$/;
13
16
  export async function initializeProject(root, projectId, question, evidenceRequirements, budgetConfirmed = false, inputPlan) {
@@ -81,6 +84,9 @@ export async function initializeProject(root, projectId, question, evidenceRequi
81
84
  costUsd: 0,
82
85
  wallSeconds: 0,
83
86
  },
87
+ lineage: initialLineage("primary"),
88
+ handoff: initialHandoffState(),
89
+ evidenceState: initialEvidenceState(),
84
90
  };
85
91
  await Promise.all([
86
92
  ensureDirectory(projectRoot),
@@ -88,6 +94,7 @@ export async function initializeProject(root, projectId, question, evidenceRequi
88
94
  ensureDirectory(join(projectRoot, "runs")),
89
95
  ]);
90
96
  await writeJsonAtomic(projectPath, project);
97
+ await registerProjectInputCandidates(root, projectId, project.inputs);
91
98
  await appendJournalEvent(paths.journal, "project.initialized", projectId, {
92
99
  projectId,
93
100
  questionSha256: await hashQuestion(normalizedQuestion),
@@ -136,12 +143,17 @@ export async function addProjectInput(root, projectId, inputPath, role) {
136
143
  path: canonicalInput,
137
144
  sha256,
138
145
  bytes: await fileSize(canonicalInput),
146
+ sourceType: "primary",
147
+ dimensions: [],
148
+ fullText: true,
149
+ publicationDate: null,
139
150
  addedAt: new Date().toISOString(),
140
151
  };
141
152
  project.inputs.push(input);
142
153
  project.inputs.sort((left, right) => left.id.localeCompare(right.id));
143
154
  project.updatedAt = new Date().toISOString();
144
155
  await saveProject(root, project);
156
+ await registerProjectInputCandidates(root, projectId, [input]);
145
157
  await appendJournalEvent(workspacePaths(root).journal, "project.input.added", projectId, {
146
158
  projectId,
147
159
  inputId: input.id,
@@ -176,6 +188,19 @@ export async function retryProjectPackage(root, projectId, packageId) {
176
188
  validateProjectId(projectId);
177
189
  return withWorkspaceLock(root, "project.retry", async () => {
178
190
  const project = await loadProject(root, projectId);
191
+ if (project.status === "archived" ||
192
+ project.status === "abandoned" ||
193
+ project.lineage.supersededBy) {
194
+ throw new CliError("Historical projects cannot be retried; continue from the authoritative project.", {
195
+ code: "RESEARCH_PROJECT_NOT_AUTHORITATIVE",
196
+ exitCode: 3,
197
+ details: {
198
+ projectId,
199
+ status: project.status,
200
+ supersededBy: project.lineage.supersededBy,
201
+ },
202
+ });
203
+ }
179
204
  const selected = packageId
180
205
  ? packageById(project, packageId)
181
206
  : project.packages.find((item) => item.status === "failed" || item.status === "retry");
@@ -225,6 +250,18 @@ export async function forkProject(root, sourceProjectId, targetProjectId, resume
225
250
  }
226
251
  return withWorkspaceLock(root, "project.fork", async () => {
227
252
  const source = await loadProject(root, sourceProjectId);
253
+ if (source.lineage.supersededBy) {
254
+ throw new CliError(`Project ${sourceProjectId} is historical; fork the authoritative project ${source.lineage.supersededBy}.`, { code: "RESEARCH_PROJECT_NOT_AUTHORITATIVE", exitCode: 3 });
255
+ }
256
+ if (source.status === "archived" || source.status === "abandoned") {
257
+ throw new CliError(`Project ${sourceProjectId} is ${source.status} and cannot be forked.`, {
258
+ code: "RESEARCH_PROJECT_NOT_AUTHORITATIVE",
259
+ exitCode: 3,
260
+ });
261
+ }
262
+ if (source.handoff.state !== "agent-actionable") {
263
+ throw new CliError(`Project ${sourceProjectId} has an unresolved ${source.handoff.state} handoff.`, { code: "RESEARCH_PROJECT_HANDOFF_REQUIRED", exitCode: 3 });
264
+ }
228
265
  const targetRoot = join(workspacePaths(root).projects, targetProjectId);
229
266
  if (await lstat(targetRoot).catch(() => undefined)) {
230
267
  throw new CliError(`Research project already exists: ${targetProjectId}`, {
@@ -235,7 +272,7 @@ export async function forkProject(root, sourceProjectId, targetProjectId, resume
235
272
  const config = await loadWorkspaceConfig(root);
236
273
  const packages = defaultWorkPackages(config);
237
274
  const inheritedStages = resumeThrough
238
- ? ["discover", "analyze", "synthesize"].slice(0, ["discover", "analyze", "synthesize"].indexOf(resumeThrough) + 1)
275
+ ? ["discover", "acquire", "analyze", "synthesize"].slice(0, ["discover", "acquire", "analyze", "synthesize"].indexOf(resumeThrough) + 1)
239
276
  : [];
240
277
  for (const stage of inheritedStages) {
241
278
  const sourcePackage = source.packages.find((item) => item.stage === stage);
@@ -266,6 +303,9 @@ export async function forkProject(root, sourceProjectId, targetProjectId, resume
266
303
  ...source.evidenceRequirements,
267
304
  dimensions: [...source.evidenceRequirements.dimensions],
268
305
  sourceTypes: [...source.evidenceRequirements.sourceTypes],
306
+ requiredCapabilityIds: [...(source.evidenceRequirements.requiredCapabilityIds ?? [])],
307
+ requiredCompanionIds: [...(source.evidenceRequirements.requiredCompanionIds ?? [])],
308
+ requiredDiscoveryScopes: [...(source.evidenceRequirements.requiredDiscoveryScopes ?? [])],
269
309
  },
270
310
  packages,
271
311
  usage: {
@@ -276,6 +316,13 @@ export async function forkProject(root, sourceProjectId, targetProjectId, resume
276
316
  costUsd: 0,
277
317
  wallSeconds: 0,
278
318
  },
319
+ lineage: {
320
+ ...initialLineage("fork"),
321
+ derivedFrom: sourceProjectId,
322
+ supersedes: sourceProjectId,
323
+ },
324
+ handoff: initialHandoffState(),
325
+ evidenceState: initialEvidenceState(),
279
326
  };
280
327
  await Promise.all([
281
328
  ensureDirectory(targetRoot),
@@ -285,6 +332,7 @@ export async function forkProject(root, sourceProjectId, targetProjectId, resume
285
332
  const inheritedOutputs = [];
286
333
  const stageOutput = {
287
334
  discover: "outputs/evidence.json",
335
+ acquire: "outputs/acquisition.json",
288
336
  analyze: "outputs/analysis.json",
289
337
  synthesize: "outputs/report.md",
290
338
  };
@@ -299,20 +347,223 @@ export async function forkProject(root, sourceProjectId, targetProjectId, resume
299
347
  }
300
348
  if (inheritedStages.includes("discover")) {
301
349
  await cloneProjectEvidenceReceipts(root, sourceProjectId, targetProjectId);
350
+ await cloneEvidenceLedger(root, sourceProjectId, targetProjectId);
351
+ }
352
+ if (inheritedStages.includes("acquire")) {
353
+ await cloneProjectArtifactRecords(root, sourceProjectId, targetProjectId);
302
354
  }
303
355
  refreshProject(project);
304
356
  await writeJsonAtomic(join(targetRoot, "project.json"), project);
357
+ if (inheritedStages.includes("acquire")) {
358
+ await freezeEvidenceSnapshot(root, project);
359
+ inheritedOutputs.push(await fileRecord(join(targetRoot, "outputs", "evidence-snapshot.json"), "outputs/evidence-snapshot.json"));
360
+ await saveProject(root, project);
361
+ }
362
+ source.lineage.supersededBy = targetProjectId;
363
+ source.evidenceState.staleReason = `Superseded by recovery fork ${targetProjectId}.`;
364
+ refreshProject(source);
365
+ await saveProject(root, source);
366
+ await appendEvidenceLedgerEvent(root, sourceProjectId, "project.superseded", {
367
+ sourceProjectId,
368
+ supersededBy: targetProjectId,
369
+ reason: "recovery-fork",
370
+ });
305
371
  await appendJournalEvent(workspacePaths(root).journal, "project.forked", targetProjectId, {
306
372
  sourceProjectId,
307
373
  targetProjectId,
308
374
  resumeThrough: resumeThrough ?? null,
309
375
  inheritedOutputs,
310
376
  inheritedUsage: false,
377
+ sourceSuperseded: true,
378
+ });
379
+ return project;
380
+ });
381
+ }
382
+ export async function setProjectDisposition(root, projectId, disposition, reason) {
383
+ validateProjectId(projectId);
384
+ const normalizedReason = reason.trim();
385
+ if (normalizedReason.length < 8 || normalizedReason.length > 500) {
386
+ throw new CliError("Project disposition reason must contain 8-500 characters.", {
387
+ code: "RESEARCH_PROJECT_DISPOSITION_INVALID",
388
+ exitCode: 2,
389
+ });
390
+ }
391
+ return withWorkspaceLock(root, `project.${disposition}`, async () => {
392
+ const project = refreshProject(await loadProject(root, projectId));
393
+ if (project.status === "running" ||
394
+ project.packages.some((item) => item.status === "running")) {
395
+ throw new CliError("Abort the active native stage before archiving or abandoning this project.", { code: "RESEARCH_PROJECT_DISPOSITION_INVALID", exitCode: 3 });
396
+ }
397
+ if (project.status === "archived" || project.status === "abandoned") {
398
+ if (project.status === disposition)
399
+ return project;
400
+ throw new CliError(`Project is already ${project.status}.`, {
401
+ code: "RESEARCH_PROJECT_DISPOSITION_INVALID",
402
+ exitCode: 3,
403
+ });
404
+ }
405
+ if (disposition === "archived" && project.status !== "complete" && project.status !== "stale") {
406
+ throw new CliError("Archive is for complete or superseded history; use abandon for unfinished work.", { code: "RESEARCH_PROJECT_DISPOSITION_INVALID", exitCode: 3 });
407
+ }
408
+ if (disposition === "abandoned" &&
409
+ (project.status === "complete" || project.status === "stale")) {
410
+ throw new CliError("Abandon is for unfinished work; archive complete or superseded history instead.", { code: "RESEARCH_PROJECT_DISPOSITION_INVALID", exitCode: 3 });
411
+ }
412
+ project.status = disposition;
413
+ project.updatedAt = new Date().toISOString();
414
+ await saveProject(root, project);
415
+ await appendJournalEvent(workspacePaths(root).journal, `project.${disposition}`, projectId, {
416
+ projectId,
417
+ disposition,
418
+ reason: normalizedReason,
419
+ supersededBy: project.lineage.supersededBy,
311
420
  });
312
421
  return project;
313
422
  });
314
423
  }
424
+ export async function createProjectAddendum(root, sourceProjectId, targetProjectId) {
425
+ validateProjectId(sourceProjectId);
426
+ validateProjectId(targetProjectId);
427
+ if (sourceProjectId === targetProjectId) {
428
+ throw new CliError("Addendum target must use a different project ID.", {
429
+ code: "RESEARCH_PROJECT_ADDENDUM_INVALID",
430
+ exitCode: 2,
431
+ });
432
+ }
433
+ return withWorkspaceLock(root, "project.addendum", async () => {
434
+ const source = refreshProject(await loadProject(root, sourceProjectId));
435
+ if (source.status !== "complete" || source.packages.at(-1)?.stage !== "close") {
436
+ throw new CliError("An addendum requires a mechanically closed source project.", {
437
+ code: "RESEARCH_PROJECT_ADDENDUM_INVALID",
438
+ exitCode: 3,
439
+ });
440
+ }
441
+ if (source.lineage.supersededBy) {
442
+ throw new CliError(`Project ${sourceProjectId} is already superseded by ${source.lineage.supersededBy}.`, { code: "RESEARCH_PROJECT_ADDENDUM_INVALID", exitCode: 3 });
443
+ }
444
+ const snapshot = await loadCurrentEvidenceSnapshot(root, sourceProjectId);
445
+ const closurePath = join(workspacePaths(root).projects, sourceProjectId, "outputs", "closure.json");
446
+ const closure = await readJsonFile(closurePath, `Research closure ${sourceProjectId}`).catch(() => null);
447
+ const closureSnapshot = closure?.evidenceSnapshot &&
448
+ typeof closure.evidenceSnapshot === "object" &&
449
+ !Array.isArray(closure.evidenceSnapshot)
450
+ ? closure.evidenceSnapshot
451
+ : null;
452
+ if (closure?.projectId !== sourceProjectId ||
453
+ closure?.status !== "complete" ||
454
+ closureSnapshot?.snapshotId !== snapshot.snapshotId ||
455
+ closureSnapshot?.snapshotSha256 !== snapshot.snapshotSha256 ||
456
+ source.evidenceState.closureSnapshotId !== snapshot.snapshotId ||
457
+ source.evidenceState.currentSnapshotSha256 !== snapshot.snapshotSha256) {
458
+ throw new CliError("Source closure is not bound to its current evidence snapshot.", {
459
+ code: "RESEARCH_PROJECT_ADDENDUM_INVALID",
460
+ exitCode: 3,
461
+ });
462
+ }
463
+ const targetRoot = join(workspacePaths(root).projects, targetProjectId);
464
+ if (await lstat(targetRoot).catch(() => undefined)) {
465
+ throw new CliError(`Research project already exists: ${targetProjectId}`, {
466
+ code: "RESEARCH_PROJECT_EXISTS",
467
+ exitCode: 2,
468
+ });
469
+ }
470
+ const config = await loadWorkspaceConfig(root);
471
+ const now = new Date().toISOString();
472
+ const target = {
473
+ schemaVersion: 1,
474
+ id: targetProjectId,
475
+ question: source.question,
476
+ status: "ready",
477
+ createdAt: now,
478
+ updatedAt: now,
479
+ budgetConfirmedAt: source.budgetConfirmedAt,
480
+ inputs: source.inputs.map((input) => ({ ...input })),
481
+ evidenceRequirements: {
482
+ ...source.evidenceRequirements,
483
+ dimensions: [...source.evidenceRequirements.dimensions],
484
+ sourceTypes: [...source.evidenceRequirements.sourceTypes],
485
+ requiredCapabilityIds: [...(source.evidenceRequirements.requiredCapabilityIds ?? [])],
486
+ requiredCompanionIds: [...(source.evidenceRequirements.requiredCompanionIds ?? [])],
487
+ requiredDiscoveryScopes: [...(source.evidenceRequirements.requiredDiscoveryScopes ?? [])],
488
+ },
489
+ packages: defaultWorkPackages(config),
490
+ usage: {
491
+ tokens: 0,
492
+ inputTokens: 0,
493
+ cachedInputTokens: 0,
494
+ outputTokens: 0,
495
+ costUsd: 0,
496
+ wallSeconds: 0,
497
+ },
498
+ lineage: {
499
+ kind: "addendum",
500
+ derivedFrom: sourceProjectId,
501
+ supersedes: sourceProjectId,
502
+ supersededBy: null,
503
+ baseSnapshotId: snapshot.snapshotId,
504
+ baseSnapshotSha256: snapshot.snapshotSha256,
505
+ },
506
+ handoff: initialHandoffState(),
507
+ evidenceState: initialEvidenceState(),
508
+ };
509
+ await Promise.all([
510
+ ensureDirectory(targetRoot),
511
+ ensureDirectory(join(targetRoot, "outputs")),
512
+ ensureDirectory(join(targetRoot, "runs")),
513
+ ensureDirectory(join(targetRoot, "evidence", "snapshots")),
514
+ ]);
515
+ const inheritedOutputs = [];
516
+ for (const logicalPath of ["outputs/evidence.json", "outputs/acquisition.json"]) {
517
+ const sourcePath = join(workspacePaths(root).projects, sourceProjectId, logicalPath);
518
+ const destination = join(targetRoot, logicalPath);
519
+ await cp(sourcePath, destination, { errorOnExist: true, force: false });
520
+ inheritedOutputs.push(await fileRecord(destination, logicalPath));
521
+ }
522
+ const baseSnapshotLogicalPath = "outputs/base-evidence-snapshot.json";
523
+ const sourceSnapshotPath = join(workspacePaths(root).projects, sourceProjectId, "evidence", "snapshots", `${snapshot.snapshotSha256}.json`);
524
+ const snapshotChain = await loadImmutableEvidenceSnapshotChain(root, sourceProjectId, snapshot.snapshotSha256);
525
+ for (const chainSnapshot of snapshotChain) {
526
+ await cp(join(workspacePaths(root).projects, sourceProjectId, "evidence", "snapshots", `${chainSnapshot.snapshotSha256}.json`), join(targetRoot, "evidence", "snapshots", `${chainSnapshot.snapshotSha256}.json`), { errorOnExist: true, force: false });
527
+ }
528
+ await cp(sourceSnapshotPath, join(targetRoot, baseSnapshotLogicalPath), {
529
+ errorOnExist: true,
530
+ force: false,
531
+ });
532
+ inheritedOutputs.push(await fileRecord(join(targetRoot, baseSnapshotLogicalPath), baseSnapshotLogicalPath));
533
+ await cloneProjectEvidenceReceipts(root, sourceProjectId, targetProjectId);
534
+ await cloneEvidenceLedger(root, sourceProjectId, targetProjectId);
535
+ await cloneProjectArtifactRecords(root, sourceProjectId, targetProjectId);
536
+ await appendEvidenceLedgerEvent(root, targetProjectId, "addendum.created", {
537
+ sourceProjectId,
538
+ targetProjectId,
539
+ baseSnapshotId: snapshot.snapshotId,
540
+ baseSnapshotSha256: snapshot.snapshotSha256,
541
+ });
542
+ await writeJsonAtomic(join(targetRoot, "project.json"), target);
543
+ source.lineage.supersededBy = targetProjectId;
544
+ source.evidenceState.staleReason = `Superseded by evidence addendum ${targetProjectId}.`;
545
+ refreshProject(source);
546
+ await saveProject(root, source);
547
+ await appendEvidenceLedgerEvent(root, sourceProjectId, "project.superseded", {
548
+ sourceProjectId,
549
+ supersededBy: targetProjectId,
550
+ snapshotId: snapshot.snapshotId,
551
+ snapshotSha256: snapshot.snapshotSha256,
552
+ });
553
+ await appendJournalEvent(workspacePaths(root).journal, "project.addendum.created", targetProjectId, {
554
+ sourceProjectId,
555
+ targetProjectId,
556
+ baseSnapshotId: snapshot.snapshotId,
557
+ baseSnapshotSha256: snapshot.snapshotSha256,
558
+ inheritedOutputs,
559
+ originalClosurePreserved: true,
560
+ });
561
+ return target;
562
+ });
563
+ }
315
564
  export function refreshProject(project) {
565
+ if (project.status === "archived" || project.status === "abandoned")
566
+ return project;
316
567
  const now = Date.now();
317
568
  for (const workPackage of project.packages) {
318
569
  if (workPackage.status !== "pending" && workPackage.status !== "retry")
@@ -326,7 +577,13 @@ export function refreshProject(project) {
326
577
  if (dependenciesComplete)
327
578
  workPackage.status = "ready";
328
579
  }
329
- if (project.packages.some((item) => item.status === "failed"))
580
+ if (project.lineage.supersededBy || project.evidenceState.staleReason)
581
+ project.status = "stale";
582
+ else if (project.handoff.state === "user-action-required")
583
+ project.status = "waiting-user";
584
+ else if (project.handoff.state === "external-response-required")
585
+ project.status = "waiting-external";
586
+ else if (project.packages.some((item) => item.status === "failed"))
330
587
  project.status = "blocked";
331
588
  else if (project.packages.every((item) => item.status === "complete"))
332
589
  project.status = "complete";
@@ -339,6 +596,8 @@ export function refreshProject(project) {
339
596
  }
340
597
  export function nextReadyPackage(project) {
341
598
  refreshProject(project);
599
+ if (project.handoff.state !== "agent-actionable")
600
+ return undefined;
342
601
  return project.packages.find((workPackage) => workPackage.status === "ready");
343
602
  }
344
603
  export function packageById(project, packageId) {
@@ -355,7 +614,8 @@ function defaultWorkPackages(config) {
355
614
  const maxAttempts = config.budget.maxAttemptsPerPackage;
356
615
  return [
357
616
  workPackage("discover", "discover", "agent", "producer", [], ["outputs/evidence.json"], maxAttempts),
358
- workPackage("analyze", "analyze", "agent", "producer", ["discover"], ["outputs/analysis.json"], maxAttempts),
617
+ workPackage("acquire", "acquire", "agent", "producer", ["discover"], ["outputs/acquisition.json"], maxAttempts),
618
+ workPackage("analyze", "analyze", "agent", "producer", ["acquire"], ["outputs/analysis.json"], maxAttempts),
359
619
  workPackage("synthesize", "synthesize", "agent", "producer", ["analyze"], ["outputs/report.md"], maxAttempts),
360
620
  workPackage("review", "review", "agent", "reviewer", ["synthesize"], ["outputs/review.json"], maxAttempts),
361
621
  workPackage("close", "close", "verify", "mechanical", ["review"], ["outputs/closure.json"], 1),
@@ -383,6 +643,17 @@ function validateProjectShape(project, expectedId) {
383
643
  if (project.schemaVersion !== 1 ||
384
644
  project.id !== expectedId ||
385
645
  !PROJECT_ID_PATTERN.test(project.id) ||
646
+ ![
647
+ "ready",
648
+ "running",
649
+ "blocked",
650
+ "complete",
651
+ "stale",
652
+ "waiting-user",
653
+ "waiting-external",
654
+ "archived",
655
+ "abandoned",
656
+ ].includes(project.status) ||
386
657
  typeof project.question !== "string" ||
387
658
  (project.budgetConfirmedAt !== null && typeof project.budgetConfirmedAt !== "string") ||
388
659
  !Array.isArray(project.inputs) ||
@@ -394,7 +665,10 @@ function validateProjectShape(project, expectedId) {
394
665
  typeof project.usage.cachedInputTokens !== "number" ||
395
666
  typeof project.usage.outputTokens !== "number" ||
396
667
  typeof project.usage.costUsd !== "number" ||
397
- typeof project.usage.wallSeconds !== "number") {
668
+ typeof project.usage.wallSeconds !== "number" ||
669
+ !isProjectLineage(project.lineage) ||
670
+ !isProjectHandoff(project.handoff) ||
671
+ !isProjectEvidenceState(project.evidenceState)) {
398
672
  throw new CliError(`Research project state is invalid: ${expectedId}`, {
399
673
  code: "RESEARCH_PROJECT_INVALID",
400
674
  exitCode: 2,
@@ -412,12 +686,83 @@ function validateProjectShape(project, expectedId) {
412
686
  });
413
687
  }
414
688
  }
689
+ function initialLineage(kind) {
690
+ return {
691
+ kind,
692
+ derivedFrom: null,
693
+ supersedes: null,
694
+ supersededBy: null,
695
+ baseSnapshotId: null,
696
+ baseSnapshotSha256: null,
697
+ };
698
+ }
699
+ function initialEvidenceState() {
700
+ return {
701
+ currentSnapshotId: null,
702
+ currentSnapshotSha256: null,
703
+ closureSnapshotId: null,
704
+ staleReason: null,
705
+ };
706
+ }
707
+ function initialHandoffState() {
708
+ return {
709
+ state: "agent-actionable",
710
+ reasonCode: null,
711
+ summary: null,
712
+ requestedActions: [],
713
+ evidenceGaps: [],
714
+ requestedAt: null,
715
+ resolvedAt: null,
716
+ resolutionNote: null,
717
+ };
718
+ }
719
+ function isProjectLineage(value) {
720
+ if (!value || typeof value !== "object" || Array.isArray(value))
721
+ return false;
722
+ const lineage = value;
723
+ return (["primary", "fork", "addendum"].includes(String(lineage.kind)) &&
724
+ nullableString(lineage.derivedFrom) &&
725
+ nullableString(lineage.supersedes) &&
726
+ nullableString(lineage.supersededBy) &&
727
+ nullableString(lineage.baseSnapshotId) &&
728
+ nullableSha256(lineage.baseSnapshotSha256));
729
+ }
730
+ function isProjectEvidenceState(value) {
731
+ if (!value || typeof value !== "object" || Array.isArray(value))
732
+ return false;
733
+ const state = value;
734
+ return (nullableString(state.currentSnapshotId) &&
735
+ nullableSha256(state.currentSnapshotSha256) &&
736
+ nullableString(state.closureSnapshotId) &&
737
+ nullableString(state.staleReason));
738
+ }
739
+ function isProjectHandoff(value) {
740
+ if (!isObject(value))
741
+ return false;
742
+ return (["agent-actionable", "user-action-required", "external-response-required"].includes(String(value.state)) &&
743
+ nullableString(value.reasonCode) &&
744
+ nullableString(value.summary) &&
745
+ Array.isArray(value.requestedActions) &&
746
+ value.requestedActions.every((item) => typeof item === "string") &&
747
+ Array.isArray(value.evidenceGaps) &&
748
+ value.evidenceGaps.every((item) => typeof item === "string") &&
749
+ nullableString(value.requestedAt) &&
750
+ nullableString(value.resolvedAt) &&
751
+ nullableString(value.resolutionNote));
752
+ }
753
+ function nullableString(value) {
754
+ return value === null || typeof value === "string";
755
+ }
756
+ function nullableSha256(value) {
757
+ return value === null || (typeof value === "string" && /^[0-9a-f]{64}$/.test(value));
758
+ }
415
759
  function defaultEvidenceRequirements(config) {
416
760
  return config.mode === "production-research"
417
761
  ? {
418
762
  dimensions: ["research-question"],
419
763
  sourceTypes: ["primary"],
420
764
  requiredCapabilityIds: [],
765
+ requiredCompanionIds: [],
421
766
  requiredDiscoveryScopes: [],
422
767
  minSources: 3,
423
768
  minFullTextSources: 1,
@@ -429,6 +774,7 @@ function defaultEvidenceRequirements(config) {
429
774
  dimensions: ["research-question"],
430
775
  sourceTypes: [],
431
776
  requiredCapabilityIds: [],
777
+ requiredCompanionIds: [],
432
778
  requiredDiscoveryScopes: [],
433
779
  minSources: 1,
434
780
  minFullTextSources: 0,
@@ -444,6 +790,9 @@ export function normalizeEvidenceRequirements(value) {
444
790
  requiredCapabilityIds: [
445
791
  ...new Set((value.requiredCapabilityIds ?? []).map(normalizeRequirementId)),
446
792
  ].sort(),
793
+ requiredCompanionIds: [
794
+ ...new Set((value.requiredCompanionIds ?? []).map(normalizeRequirementId)),
795
+ ].sort(),
447
796
  requiredDiscoveryScopes: [
448
797
  ...new Set((value.requiredDiscoveryScopes ?? []).map(normalizeRequirementId)),
449
798
  ].sort(),
@@ -473,6 +822,9 @@ function isEvidenceRequirements(value) {
473
822
  (value.requiredCapabilityIds === undefined ||
474
823
  (Array.isArray(value.requiredCapabilityIds) &&
475
824
  value.requiredCapabilityIds.every(validRequirementId))) &&
825
+ (value.requiredCompanionIds === undefined ||
826
+ (Array.isArray(value.requiredCompanionIds) &&
827
+ value.requiredCompanionIds.every(validRequirementId))) &&
476
828
  (value.requiredDiscoveryScopes === undefined ||
477
829
  (Array.isArray(value.requiredDiscoveryScopes) &&
478
830
  value.requiredDiscoveryScopes.every(validRequirementId))) &&