@tiangong-ai/cli 0.0.32 → 0.0.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/AGENTS.md +7 -2
  2. package/README.md +72 -19
  3. package/dist/research/commands.js +58 -1
  4. package/dist/research/commands.js.map +1 -1
  5. package/dist/research/orchestration.js +388 -31
  6. package/dist/research/orchestration.js.map +1 -1
  7. package/dist/research/workspace/acquisition.d.ts +71 -0
  8. package/dist/research/workspace/acquisition.js +593 -0
  9. package/dist/research/workspace/acquisition.js.map +1 -0
  10. package/dist/research/workspace/artifacts.d.ts +43 -0
  11. package/dist/research/workspace/artifacts.js +544 -0
  12. package/dist/research/workspace/artifacts.js.map +1 -0
  13. package/dist/research/workspace/broker.js +265 -56
  14. package/dist/research/workspace/broker.js.map +1 -1
  15. package/dist/research/workspace/constants.d.ts +1 -0
  16. package/dist/research/workspace/constants.js +4 -2
  17. package/dist/research/workspace/constants.js.map +1 -1
  18. package/dist/research/workspace/context.js +106 -11
  19. package/dist/research/workspace/context.js.map +1 -1
  20. package/dist/research/workspace/discovery-planning.d.ts +25 -0
  21. package/dist/research/workspace/discovery-planning.js +106 -0
  22. package/dist/research/workspace/discovery-planning.js.map +1 -0
  23. package/dist/research/workspace/discovery-status.d.ts +46 -0
  24. package/dist/research/workspace/discovery-status.js +183 -0
  25. package/dist/research/workspace/discovery-status.js.map +1 -0
  26. package/dist/research/workspace/discovery.d.ts +25 -0
  27. package/dist/research/workspace/discovery.js +268 -0
  28. package/dist/research/workspace/discovery.js.map +1 -0
  29. package/dist/research/workspace/downloads.d.ts +76 -0
  30. package/dist/research/workspace/downloads.js +274 -0
  31. package/dist/research/workspace/downloads.js.map +1 -0
  32. package/dist/research/workspace/evidence-ledger.d.ts +52 -0
  33. package/dist/research/workspace/evidence-ledger.js +487 -0
  34. package/dist/research/workspace/evidence-ledger.js.map +1 -0
  35. package/dist/research/workspace/evidence.d.ts +1 -0
  36. package/dist/research/workspace/evidence.js +2 -0
  37. package/dist/research/workspace/evidence.js.map +1 -1
  38. package/dist/research/workspace/input-plan.js +4 -0
  39. package/dist/research/workspace/input-plan.js.map +1 -1
  40. package/dist/research/workspace/native-activity.d.ts +65 -0
  41. package/dist/research/workspace/native-activity.js +153 -0
  42. package/dist/research/workspace/native-activity.js.map +1 -0
  43. package/dist/research/workspace/preflight.d.ts +5 -0
  44. package/dist/research/workspace/preflight.js +37 -17
  45. package/dist/research/workspace/preflight.js.map +1 -1
  46. package/dist/research/workspace/projects.d.ts +3 -1
  47. package/dist/research/workspace/projects.js +346 -5
  48. package/dist/research/workspace/projects.js.map +1 -1
  49. package/dist/research/workspace/runtime.d.ts +106 -4
  50. package/dist/research/workspace/runtime.js +947 -80
  51. package/dist/research/workspace/runtime.js.map +1 -1
  52. package/dist/research/workspace/sanitization.js +28 -6
  53. package/dist/research/workspace/sanitization.js.map +1 -1
  54. package/dist/research/workspace/schemas.d.ts +3 -0
  55. package/dist/research/workspace/schemas.js +206 -33
  56. package/dist/research/workspace/schemas.js.map +1 -1
  57. package/dist/research/workspace/setup-catalog.js +5 -5
  58. package/dist/research/workspace/setup-invocation.d.ts +11 -0
  59. package/dist/research/workspace/setup-invocation.js +34 -0
  60. package/dist/research/workspace/setup-invocation.js.map +1 -0
  61. package/dist/research/workspace/setup-wizard.js +10 -4
  62. package/dist/research/workspace/setup-wizard.js.map +1 -1
  63. package/dist/research/workspace/setup.d.ts +55 -7
  64. package/dist/research/workspace/setup.js +603 -32
  65. package/dist/research/workspace/setup.js.map +1 -1
  66. package/dist/research/workspace/types.d.ts +52 -3
  67. package/dist/research/workspace/workspace.js +45 -6
  68. package/dist/research/workspace/workspace.js.map +1 -1
  69. package/package.json +4 -2
@@ -1,5 +1,5 @@
1
1
  import { lstat, readFile } from "node:fs/promises";
2
- import { isAbsolute, resolve } from "node:path";
2
+ import { isAbsolute, join, resolve } from "node:path";
3
3
  import { CliError } from "../errors.js";
4
4
  import { stringifyJson, write } from "../io.js";
5
5
  import { parseStrictArgs, strictBoolean, strictString } from "../strict-args.js";
@@ -10,12 +10,19 @@ import { setCapabilityCredentialFromEnvironment } from "./workspace/credentials.
10
10
  import { configureExternalSkillProfile, doctorExternalCapabilities, EXTERNAL_SKILL_CONTEXT_PROFILE, EXTERNAL_SKILL_MEDIA_PROFILE, EXTERNAL_SKILL_PROFILE, importExternalCapability, inspectExternalSkillCatalog, } from "./workspace/external-skills.js";
11
11
  import { appendJournalEvent } from "./workspace/journal.js";
12
12
  import { fetchNativeCandidateSource } from "./workspace/broker.js";
13
+ import { registerEvidenceArtifact } from "./workspace/artifacts.js";
14
+ import { loadCurrentEvidenceSnapshot } from "./workspace/acquisition.js";
15
+ import { inspectDiscoveryProgress } from "./workspace/discovery-status.js";
16
+ import { recordDiscoveryAssessmentBatch } from "./workspace/discovery.js";
17
+ import { bindEvidenceDownload } from "./workspace/downloads.js";
18
+ import { registerNativeDiscoveryCandidate } from "./workspace/evidence-ledger.js";
19
+ import { recordNativeResearchActivity } from "./workspace/native-activity.js";
13
20
  import { readAndVerifyProjectInputPlan } from "./workspace/input-plan.js";
14
- import { addProjectInput, initializeProject, forkProject, listProjects, loadProject, nextReadyPackage, normalizeEvidenceRequirements, refreshProject, retryProjectPackage, } from "./workspace/projects.js";
21
+ import { addProjectInput, createProjectAddendum, initializeProject, forkProject, listProjects, loadProject, nextReadyPackage, normalizeEvidenceRequirements, refreshProject, retryProjectPackage, setProjectDisposition, } from "./workspace/projects.js";
15
22
  import { evaluateProjectPreflight } from "./workspace/preflight.js";
16
- import { abortNativeResearchStage, prepareNativeResearchStage, runResearchWorkspace, submitNativeResearchStage, } from "./workspace/runtime.js";
23
+ import { abortNativeResearchStage, inspectNativeResearchStage, prepareNativeResearchStage, requestResearchHandoff, resolveResearchHandoff, runResearchWorkspace, submitNativeResearchStage, } from "./workspace/runtime.js";
17
24
  import { schemaForStage } from "./workspace/schemas.js";
18
- import { sha256Text, workspacePaths } from "./workspace/storage.js";
25
+ import { pathExists, sha256Text, workspacePaths } from "./workspace/storage.js";
19
26
  import { doctorResearchWorkspace, initializeResearchWorkspace, requireResearchWorkspace, withWorkspaceLock, } from "./workspace/workspace.js";
20
27
  const COMMON_OPTIONS = { help: "boolean", json: "boolean" };
21
28
  const WORKSPACE_OPTIONS = { ...COMMON_OPTIONS, workspace: "string" };
@@ -56,13 +63,23 @@ export function researchOrchestrationHelp() {
56
63
  tiangong-ai research project preflight --question <question> [--requirements <absolute-json>] [--input-plan <absolute-json>] [--workspace <path>] [--json]
57
64
  tiangong-ai research project input add <project-id> --path <absolute-file> [--role primary|reference|replication] [--workspace <path>] [--json]
58
65
  tiangong-ai research project retry <project-id> [--package <package-id>] [--workspace <path>] [--json]
59
- tiangong-ai research project fork <source-project-id> --to <target-project-id> [--resume-through discover|analyze|synthesize] [--workspace <path>] [--json]
60
- tiangong-ai research project stage prepare <project-id> --stage discover|analyze|synthesize --host-agent codex|claude [--workspace <path>] [--json]
66
+ tiangong-ai research project fork <source-project-id> --to <target-project-id> [--resume-through discover|acquire|analyze|synthesize] [--workspace <path>] [--json]
67
+ tiangong-ai research project addendum <closed-project-id> --to <target-project-id> [--workspace <path>] [--json]
68
+ tiangong-ai research project archive <project-id> --reason <text> [--workspace <path>] [--json]
69
+ tiangong-ai research project abandon <project-id> --reason <text> [--workspace <path>] [--json]
70
+ tiangong-ai research project handoff request <project-id> --record <absolute-json> [--workspace <path>] [--json]
71
+ tiangong-ai research project handoff resolve <project-id> --note <text> [--workspace <path>] [--json]
72
+ tiangong-ai research project stage prepare <project-id> --stage discover|acquire|analyze|synthesize --host-agent codex|claude [--workspace <path>] [--json]
61
73
  tiangong-ai research project stage submit <project-id> --session <id> --output <absolute-json> [--confirm-model <id>] [--workspace <path>] [--json]
62
74
  tiangong-ai research project stage abort <project-id> --session <id> [--workspace <path>] [--json]
63
75
  tiangong-ai research project evidence fetch <project-id> --request <absolute-json> [--workspace <path>] [--json]
64
- tiangong-ai research schema show <discover|analyze|synthesize|review|doctor> [--json]
65
- tiangong-ai research status [--project <project-id>] [--workspace <absolute-path>] [--json]
76
+ tiangong-ai research project evidence activity record <project-id> --record <absolute-json> [--workspace <path>] [--json]
77
+ tiangong-ai research project evidence candidate register <project-id> --record <absolute-json> [--workspace <path>] [--json]
78
+ tiangong-ai research project evidence assessment record <project-id> --record <absolute-json> [--workspace <path>] [--json]
79
+ tiangong-ai research project evidence download bind <project-id> --candidate <id> --record <absolute-json> [--workspace <path>] [--json]
80
+ tiangong-ai research project evidence artifact register <project-id> --candidate <id> --path <absolute-file> [--download-binding <id> | --derived-from-artifact <id>] [--media-type <type>] [--source-url <https-url>] [--license <declared-license>] [--license-url <https-url>] [--host-type <type>] [--article-version <version>] [--workspace <path>] [--json]
81
+ tiangong-ai research schema show <discover|acquire|analyze|synthesize|review|doctor> [--json]
82
+ tiangong-ai research status [--project <project-id>] [--all] [--workspace <absolute-path>] [--json]
66
83
  tiangong-ai research run [--project <project-id>] [--max-parallel <1-8>] [--max-cycles <1-100>] [--dry-run] [--progress-jsonl] [--workspace <absolute-path>] [--json]
67
84
 
68
85
  ${researchSetupHelp()}
@@ -79,6 +96,7 @@ async function runSchema(argv, io) {
79
96
  return writeHelp(io);
80
97
  const stage = onePositional(args.positionals, "research schema show");
81
98
  if (stage !== "discover" &&
99
+ stage !== "acquire" &&
82
100
  stage !== "analyze" &&
83
101
  stage !== "synthesize" &&
84
102
  stage !== "review" &&
@@ -281,6 +299,45 @@ async function runProject(argv, io) {
281
299
  const [action, ...rest] = argv;
282
300
  if (!action || action === "--help" || action === "-h")
283
301
  return writeHelp(io);
302
+ if (action === "handoff") {
303
+ const [handoffAction, ...handoffRest] = rest;
304
+ if (handoffAction === "request") {
305
+ const args = parseStrictArgs(handoffRest, { ...WORKSPACE_OPTIONS, record: "string" }, "research project handoff request");
306
+ if (strictBoolean(args, "help"))
307
+ return writeHelp(io);
308
+ const projectId = onePositional(args.positionals, "research project handoff request");
309
+ const recordPath = strictString(args, "record");
310
+ if (!recordPath) {
311
+ throw new CliError("handoff request requires --record.", {
312
+ code: "RESEARCH_PROJECT_HANDOFF_INVALID",
313
+ exitCode: 2,
314
+ });
315
+ }
316
+ const root = await workspaceFromArgs(args);
317
+ const record = await readBoundedJsonRecord(recordPath, "--record", "RESEARCH_PROJECT_HANDOFF_INVALID");
318
+ const result = await requestResearchHandoff({ root, projectId, value: record });
319
+ writeJson(io, result, args);
320
+ return 0;
321
+ }
322
+ if (handoffAction === "resolve") {
323
+ const args = parseStrictArgs(handoffRest, { ...WORKSPACE_OPTIONS, note: "string" }, "research project handoff resolve");
324
+ if (strictBoolean(args, "help"))
325
+ return writeHelp(io);
326
+ const projectId = onePositional(args.positionals, "research project handoff resolve");
327
+ const note = strictString(args, "note");
328
+ if (!note) {
329
+ throw new CliError("handoff resolve requires --note.", {
330
+ code: "RESEARCH_PROJECT_HANDOFF_INVALID",
331
+ exitCode: 2,
332
+ });
333
+ }
334
+ const root = await workspaceFromArgs(args);
335
+ const result = await resolveResearchHandoff({ root, projectId, note });
336
+ writeJson(io, result, args);
337
+ return 0;
338
+ }
339
+ throw unknownAction("research project handoff", handoffAction ?? "");
340
+ }
284
341
  if (action === "stage") {
285
342
  const [stageAction, ...stageRest] = rest;
286
343
  if (stageAction === "prepare") {
@@ -345,6 +402,156 @@ async function runProject(argv, io) {
345
402
  }
346
403
  if (action === "evidence") {
347
404
  const [evidenceAction, ...evidenceRest] = rest;
405
+ if (evidenceAction === "activity") {
406
+ const [activityAction, ...activityRest] = evidenceRest;
407
+ if (activityAction !== "record") {
408
+ throw unknownAction("research project evidence activity", activityAction ?? "");
409
+ }
410
+ const args = parseStrictArgs(activityRest, { ...WORKSPACE_OPTIONS, record: "string" }, "research project evidence activity record");
411
+ if (strictBoolean(args, "help"))
412
+ return writeHelp(io);
413
+ const projectId = onePositional(args.positionals, "research project evidence activity record");
414
+ const recordPath = strictString(args, "record");
415
+ if (!recordPath) {
416
+ throw new CliError("activity record requires --record.", {
417
+ code: "RESEARCH_NATIVE_ACTIVITY_INVALID",
418
+ exitCode: 2,
419
+ });
420
+ }
421
+ const root = await workspaceFromArgs(args);
422
+ const record = await readBoundedJsonRecord(recordPath, "--record", "RESEARCH_NATIVE_ACTIVITY_INVALID");
423
+ const result = await withWorkspaceLock(root, "research.native-activity.record", () => recordNativeResearchActivity({ root, projectId, value: record }));
424
+ writeJson(io, result, args);
425
+ return 0;
426
+ }
427
+ if (evidenceAction === "candidate") {
428
+ const [candidateAction, ...candidateRest] = evidenceRest;
429
+ if (candidateAction !== "register") {
430
+ throw unknownAction("research project evidence candidate", candidateAction ?? "");
431
+ }
432
+ const args = parseStrictArgs(candidateRest, { ...WORKSPACE_OPTIONS, record: "string" }, "research project evidence candidate register");
433
+ if (strictBoolean(args, "help"))
434
+ return writeHelp(io);
435
+ const projectId = onePositional(args.positionals, "research project evidence candidate register");
436
+ const recordPath = strictString(args, "record");
437
+ if (!recordPath) {
438
+ throw new CliError("candidate register requires --record.", {
439
+ code: "RESEARCH_NATIVE_CANDIDATE_INVALID",
440
+ exitCode: 2,
441
+ });
442
+ }
443
+ const root = await workspaceFromArgs(args);
444
+ const record = await readBoundedJsonRecord(recordPath, "--record", "RESEARCH_NATIVE_CANDIDATE_INVALID");
445
+ const result = await withWorkspaceLock(root, "research.native-candidate.register", () => registerNativeDiscoveryCandidate({ root, projectId, value: record }));
446
+ writeJson(io, result, args);
447
+ return 0;
448
+ }
449
+ if (evidenceAction === "assessment") {
450
+ const [assessmentAction, ...assessmentRest] = evidenceRest;
451
+ if (assessmentAction !== "record") {
452
+ throw unknownAction("research project evidence assessment", assessmentAction ?? "");
453
+ }
454
+ const args = parseStrictArgs(assessmentRest, { ...WORKSPACE_OPTIONS, record: "string" }, "research project evidence assessment record");
455
+ if (strictBoolean(args, "help"))
456
+ return writeHelp(io);
457
+ const projectId = onePositional(args.positionals, "research project evidence assessment record");
458
+ const recordPath = strictString(args, "record");
459
+ if (!recordPath) {
460
+ throw new CliError("assessment record requires --record.", {
461
+ code: "RESEARCH_DISCOVERY_ASSESSMENT_INVALID",
462
+ exitCode: 2,
463
+ });
464
+ }
465
+ const root = await workspaceFromArgs(args);
466
+ const record = await readBoundedJsonRecord(recordPath, "--record", "RESEARCH_DISCOVERY_ASSESSMENT_INVALID");
467
+ const result = await withWorkspaceLock(root, "research.discovery-assessment.record", () => recordDiscoveryAssessmentBatch({ root, projectId, value: record }));
468
+ writeJson(io, result, args);
469
+ return 0;
470
+ }
471
+ if (evidenceAction === "download") {
472
+ const [downloadAction, ...downloadRest] = evidenceRest;
473
+ if (downloadAction !== "bind") {
474
+ throw unknownAction("research project evidence download", downloadAction ?? "");
475
+ }
476
+ const args = parseStrictArgs(downloadRest, { ...WORKSPACE_OPTIONS, candidate: "string", record: "string" }, "research project evidence download bind");
477
+ if (strictBoolean(args, "help"))
478
+ return writeHelp(io);
479
+ const projectId = onePositional(args.positionals, "research project evidence download bind");
480
+ const candidateId = strictString(args, "candidate");
481
+ const recordPath = strictString(args, "record");
482
+ if (!candidateId || !recordPath) {
483
+ throw new CliError("download bind requires --candidate and --record.", {
484
+ code: "RESEARCH_DOWNLOAD_BINDING_INVALID",
485
+ exitCode: 2,
486
+ });
487
+ }
488
+ const root = await workspaceFromArgs(args);
489
+ const record = await readBoundedJsonRecord(recordPath, "--record", "RESEARCH_DOWNLOAD_BINDING_INVALID");
490
+ const result = await withWorkspaceLock(root, "research.download.bind", () => bindEvidenceDownload({ root, projectId, candidateId, value: record }));
491
+ writeJson(io, result, args);
492
+ return result.status === "completed" ? 0 : 3;
493
+ }
494
+ if (evidenceAction === "artifact") {
495
+ const [artifactAction, ...artifactRest] = evidenceRest;
496
+ if (artifactAction !== "register") {
497
+ throw unknownAction("research project evidence artifact", artifactAction ?? "");
498
+ }
499
+ const args = parseStrictArgs(artifactRest, {
500
+ ...WORKSPACE_OPTIONS,
501
+ candidate: "string",
502
+ path: "string",
503
+ "media-type": "string",
504
+ "source-url": "string",
505
+ license: "string",
506
+ "license-url": "string",
507
+ "host-type": "string",
508
+ "article-version": "string",
509
+ "download-binding": "string",
510
+ "derived-from-artifact": "string",
511
+ }, "research project evidence artifact register");
512
+ if (strictBoolean(args, "help"))
513
+ return writeHelp(io);
514
+ const projectId = onePositional(args.positionals, "research project evidence artifact register");
515
+ const candidateId = strictString(args, "candidate");
516
+ const path = strictString(args, "path");
517
+ if (!candidateId || !path) {
518
+ throw new CliError("artifact register requires --candidate and --path.", {
519
+ code: "RESEARCH_ARTIFACT_PATH_INVALID",
520
+ exitCode: 2,
521
+ });
522
+ }
523
+ const root = await workspaceFromArgs(args);
524
+ const result = await withWorkspaceLock(root, "research.artifact.register", () => registerEvidenceArtifact({
525
+ root,
526
+ projectId,
527
+ candidateId,
528
+ path,
529
+ ...(strictString(args, "media-type")
530
+ ? { mediaType: strictString(args, "media-type") }
531
+ : {}),
532
+ ...(strictString(args, "source-url")
533
+ ? { sourceUrl: strictString(args, "source-url") }
534
+ : {}),
535
+ ...(strictString(args, "license") ? { license: strictString(args, "license") } : {}),
536
+ ...(strictString(args, "license-url")
537
+ ? { licenseUrl: strictString(args, "license-url") }
538
+ : {}),
539
+ ...(strictString(args, "host-type")
540
+ ? { hostType: strictString(args, "host-type") }
541
+ : {}),
542
+ ...(strictString(args, "article-version")
543
+ ? { articleVersion: strictString(args, "article-version") }
544
+ : {}),
545
+ ...(strictString(args, "download-binding")
546
+ ? { downloadBindingId: strictString(args, "download-binding") }
547
+ : {}),
548
+ ...(strictString(args, "derived-from-artifact")
549
+ ? { derivedFromArtifactId: strictString(args, "derived-from-artifact") }
550
+ : {}),
551
+ }));
552
+ writeJson(io, result, args);
553
+ return 0;
554
+ }
348
555
  if (evidenceAction !== "fetch") {
349
556
  throw unknownAction("research project evidence", evidenceAction ?? "");
350
557
  }
@@ -466,33 +673,180 @@ async function runProject(argv, io) {
466
673
  writeJson(io, project, args);
467
674
  return 0;
468
675
  }
676
+ if (action === "addendum") {
677
+ const args = parseStrictArgs(rest, { ...WORKSPACE_OPTIONS, to: "string" }, "research project addendum");
678
+ if (strictBoolean(args, "help"))
679
+ return writeHelp(io);
680
+ const sourceProjectId = onePositional(args.positionals, "research project addendum");
681
+ const targetProjectId = strictString(args, "to");
682
+ if (!targetProjectId) {
683
+ throw new CliError("research project addendum requires --to.", {
684
+ code: "RESEARCH_PROJECT_ADDENDUM_INVALID",
685
+ exitCode: 2,
686
+ });
687
+ }
688
+ const root = await workspaceFromArgs(args);
689
+ const project = await createProjectAddendum(root, sourceProjectId, targetProjectId);
690
+ writeJson(io, project, args);
691
+ return 0;
692
+ }
693
+ if (action === "archive" || action === "abandon") {
694
+ const args = parseStrictArgs(rest, { ...WORKSPACE_OPTIONS, reason: "string" }, `research project ${action}`);
695
+ if (strictBoolean(args, "help"))
696
+ return writeHelp(io);
697
+ const projectId = onePositional(args.positionals, `research project ${action}`);
698
+ const reason = strictString(args, "reason");
699
+ if (!reason) {
700
+ throw new CliError(`research project ${action} requires --reason.`, {
701
+ code: "RESEARCH_PROJECT_DISPOSITION_INVALID",
702
+ exitCode: 2,
703
+ });
704
+ }
705
+ const root = await workspaceFromArgs(args);
706
+ const project = await setProjectDisposition(root, projectId, action === "archive" ? "archived" : "abandoned", reason);
707
+ writeJson(io, project, args);
708
+ return 0;
709
+ }
469
710
  throw unknownAction("research project", action);
470
711
  }
471
712
  async function runStatus(argv, io) {
472
- const args = parseStrictArgs(argv, { ...WORKSPACE_OPTIONS, project: "string" }, "research status");
713
+ const args = parseStrictArgs(argv, { ...WORKSPACE_OPTIONS, project: "string", all: "boolean" }, "research status");
473
714
  if (strictBoolean(args, "help"))
474
715
  return writeHelp(io);
475
716
  rejectPositionals(args.positionals, "research status");
476
717
  const root = await workspaceFromArgs(args);
477
718
  const selectedProject = strictString(args, "project");
478
- const projects = selectedProject
479
- ? [await loadProject(root, selectedProject)]
480
- : await listProjects(root);
719
+ const workspaceProjects = await listProjects(root);
720
+ const allProjects = selectedProject
721
+ ? [
722
+ workspaceProjects.find((project) => project.id === selectedProject) ??
723
+ (await loadProject(root, selectedProject)),
724
+ ]
725
+ : workspaceProjects;
726
+ const projects = selectedProject || strictBoolean(args, "all")
727
+ ? allProjects
728
+ : allProjects.filter((project) => project.lineage.supersededBy === null &&
729
+ project.status !== "archived" &&
730
+ project.status !== "abandoned");
481
731
  const result = {
482
732
  workspace: root,
483
- projects: projects.map((project) => ({
484
- id: project.id,
485
- question: project.question,
486
- status: refreshProject(project).status,
487
- readyPackage: nextReadyPackage(project)?.id ?? null,
488
- usage: project.usage,
489
- inputs: project.inputs,
490
- packages: project.packages,
733
+ hiddenSupersededProjects: selectedProject || strictBoolean(args, "all")
734
+ ? 0
735
+ : workspaceProjects.filter((project) => project.lineage.supersededBy !== null).length,
736
+ hiddenArchivedProjects: selectedProject || strictBoolean(args, "all")
737
+ ? 0
738
+ : workspaceProjects.filter((project) => project.status === "archived").length,
739
+ hiddenAbandonedProjects: selectedProject || strictBoolean(args, "all")
740
+ ? 0
741
+ : workspaceProjects.filter((project) => project.status === "abandoned").length,
742
+ projects: await Promise.all(projects.map(async (project) => {
743
+ const current = refreshProject(project);
744
+ const nativeStage = await inspectNativeResearchStage(root, current);
745
+ const snapshot = await inspectSnapshotForStatus(root, current.id);
746
+ const readyPackage = nextReadyPackage(current)?.id ?? null;
747
+ return {
748
+ id: current.id,
749
+ question: current.question,
750
+ status: current.status,
751
+ authority: projectAuthority(current, workspaceProjects),
752
+ lineage: current.lineage,
753
+ handoff: current.handoff,
754
+ evidenceState: current.evidenceState,
755
+ snapshot,
756
+ nativeStage,
757
+ readyPackage,
758
+ recommendedAction: projectRecommendedAction(root, current, readyPackage, nativeStage),
759
+ usage: current.usage,
760
+ inputs: current.inputs,
761
+ packages: current.packages,
762
+ discovery: await inspectDiscoveryProgress(root, current),
763
+ };
491
764
  })),
492
765
  };
493
766
  writeJson(io, result, args);
494
767
  return 0;
495
768
  }
769
+ function projectAuthority(project, projects) {
770
+ const byId = new Map(projects.map((candidate) => [candidate.id, candidate]));
771
+ const visited = new Set([project.id]);
772
+ let current = project;
773
+ while (current.lineage.supersededBy) {
774
+ if (visited.has(current.lineage.supersededBy))
775
+ break;
776
+ visited.add(current.lineage.supersededBy);
777
+ const next = byId.get(current.lineage.supersededBy);
778
+ if (!next)
779
+ return { state: "superseded", projectId: current.lineage.supersededBy };
780
+ current = next;
781
+ }
782
+ if (project.status === "archived")
783
+ return { state: "archived", projectId: current.id };
784
+ if (project.status === "abandoned")
785
+ return { state: "abandoned", projectId: current.id };
786
+ return {
787
+ state: project.id === current.id ? "authoritative" : "superseded",
788
+ projectId: current.id,
789
+ };
790
+ }
791
+ async function inspectSnapshotForStatus(root, projectId) {
792
+ const currentPath = join(workspacePaths(root).projects, projectId, "outputs", "evidence-snapshot.json");
793
+ if (!(await pathExists(currentPath)))
794
+ return { status: "absent" };
795
+ try {
796
+ const snapshot = await loadCurrentEvidenceSnapshot(root, projectId);
797
+ return {
798
+ status: "verified",
799
+ snapshotId: snapshot.snapshotId,
800
+ snapshotSha256: snapshot.snapshotSha256,
801
+ parentSnapshotId: snapshot.parentSnapshotId,
802
+ sourceCount: snapshot.sources.length,
803
+ artifactCount: snapshot.artifacts.length,
804
+ delta: snapshot.delta,
805
+ };
806
+ }
807
+ catch (error) {
808
+ const code = error instanceof CliError ? error.code : "RESEARCH_EVIDENCE_SNAPSHOT_INVALID";
809
+ return {
810
+ status: "invalid",
811
+ code,
812
+ };
813
+ }
814
+ }
815
+ function projectRecommendedAction(root, project, readyPackage, nativeStage) {
816
+ if (project.lineage.supersededBy) {
817
+ return `Continue with superseding project ${project.lineage.supersededBy}.`;
818
+ }
819
+ if (project.status === "archived" || project.status === "abandoned") {
820
+ return `Project is ${project.status}; inspect with research status --all and continue only from an authoritative project.`;
821
+ }
822
+ if (project.handoff.state === "user-action-required") {
823
+ return `User action required: ${project.handoff.summary ?? "review the requested action"} Resolve only after completion: tiangong-ai research project handoff resolve ${project.id} --note <resolution-note> --workspace ${root}`;
824
+ }
825
+ if (project.handoff.state === "external-response-required") {
826
+ return `Waiting for an external response: ${project.handoff.summary ?? "external evidence is pending"} Do not continue substitute searching; resolve after the response is registered.`;
827
+ }
828
+ if (nativeStage.status === "stale" || nativeStage.status === "invalid") {
829
+ return nativeStage.recommendedAction ?? "Recover the stale native session explicitly.";
830
+ }
831
+ if (nativeStage.status === "active") {
832
+ return nativeStage.recommendedAction ?? "Resume the active native stage.";
833
+ }
834
+ if (project.status === "complete") {
835
+ return `Create an immutable evidence addendum only when new evidence exists: tiangong-ai research project addendum ${project.id} --to <new-project-id> --workspace ${root}`;
836
+ }
837
+ if (project.status === "blocked") {
838
+ const failed = project.packages.find((workPackage) => workPackage.status === "failed");
839
+ return failed
840
+ ? `Review ${failed.lastFailureKind ?? "deterministic"} failure for ${failed.id}, then run the explicit project retry command if corrected.`
841
+ : "Inspect the blocking package and use explicit retry or fork recovery.";
842
+ }
843
+ if (readyPackage && ["discover", "acquire", "analyze", "synthesize"].includes(readyPackage)) {
844
+ return `Prepare native ${readyPackage}: tiangong-ai research project stage prepare ${project.id} --stage ${readyPackage} --host-agent <codex|claude> --workspace ${root}`;
845
+ }
846
+ return readyPackage === "review"
847
+ ? `Run the independent reviewer package: tiangong-ai research run --project ${project.id} --workspace ${root}`
848
+ : "Continue the next ready package.";
849
+ }
496
850
  async function runWorkspaceExecution(argv, io) {
497
851
  const args = parseStrictArgs(argv, {
498
852
  ...WORKSPACE_OPTIONS,
@@ -582,7 +936,7 @@ function researchMode(value) {
582
936
  function resumeStage(value) {
583
937
  if (!value)
584
938
  return undefined;
585
- if (value === "discover" || value === "analyze" || value === "synthesize")
939
+ if (value === "discover" || value === "acquire" || value === "analyze" || value === "synthesize")
586
940
  return value;
587
941
  throw new CliError(`Unsupported --resume-through stage: ${value}`, {
588
942
  code: "RESEARCH_PROJECT_FORK_INVALID",
@@ -590,9 +944,9 @@ function resumeStage(value) {
590
944
  });
591
945
  }
592
946
  function nativeProducerStage(value) {
593
- if (value === "discover" || value === "analyze" || value === "synthesize")
947
+ if (value === "discover" || value === "acquire" || value === "analyze" || value === "synthesize")
594
948
  return value;
595
- throw new CliError("--stage must be discover, analyze, or synthesize.", {
949
+ throw new CliError("--stage must be discover, acquire, analyze, or synthesize.", {
596
950
  code: "RESEARCH_NATIVE_STAGE_INVALID",
597
951
  exitCode: 2,
598
952
  });
@@ -606,17 +960,20 @@ function nativeHostAgent(value) {
606
960
  });
607
961
  }
608
962
  async function readNativeEvidenceRequest(path) {
963
+ return readBoundedJsonRecord(path, "--request", "RESEARCH_BROKER_REQUEST_INVALID");
964
+ }
965
+ async function readBoundedJsonRecord(path, label, code) {
609
966
  if (!isAbsolute(path)) {
610
- throw new CliError("--request must be an absolute JSON file path.", {
611
- code: "RESEARCH_BROKER_REQUEST_INVALID",
967
+ throw new CliError(`${label} must be an absolute JSON file path.`, {
968
+ code,
612
969
  exitCode: 2,
613
970
  });
614
971
  }
615
972
  const selected = resolve(path);
616
973
  const info = await lstat(selected).catch(() => undefined);
617
974
  if (!info?.isFile() || info.isSymbolicLink() || info.size > 1024 * 1024) {
618
- throw new CliError("--request must be a bounded regular non-symlink JSON file.", {
619
- code: "RESEARCH_BROKER_REQUEST_INVALID",
975
+ throw new CliError(`${label} must be a bounded regular non-symlink JSON file.`, {
976
+ code,
620
977
  exitCode: 2,
621
978
  });
622
979
  }
@@ -625,14 +982,14 @@ async function readNativeEvidenceRequest(path) {
625
982
  value = JSON.parse(await readFile(selected, "utf8"));
626
983
  }
627
984
  catch {
628
- throw new CliError("--request contains invalid JSON.", {
629
- code: "RESEARCH_BROKER_REQUEST_INVALID",
985
+ throw new CliError(`${label} contains invalid JSON.`, {
986
+ code,
630
987
  exitCode: 2,
631
988
  });
632
989
  }
633
990
  if (!value || typeof value !== "object" || Array.isArray(value)) {
634
- throw new CliError("--request must contain one JSON object.", {
635
- code: "RESEARCH_BROKER_REQUEST_INVALID",
991
+ throw new CliError(`${label} must contain one JSON object.`, {
992
+ code,
636
993
  exitCode: 2,
637
994
  });
638
995
  }