@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
@@ -1,5 +1,5 @@
1
- import { readFile } from "node:fs/promises";
2
- import { isAbsolute } from "node:path";
1
+ import { lstat, readFile } from "node:fs/promises";
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";
@@ -9,12 +9,20 @@ import { inspectResearchContext } from "./workspace/context.js";
9
9
  import { setCapabilityCredentialFromEnvironment } from "./workspace/credentials.js";
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
+ 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";
12
20
  import { readAndVerifyProjectInputPlan } from "./workspace/input-plan.js";
13
- 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";
14
22
  import { evaluateProjectPreflight } from "./workspace/preflight.js";
15
- import { runResearchWorkspace } from "./workspace/runtime.js";
23
+ import { abortNativeResearchStage, inspectNativeResearchStage, prepareNativeResearchStage, requestResearchHandoff, resolveResearchHandoff, runResearchWorkspace, submitNativeResearchStage, } from "./workspace/runtime.js";
16
24
  import { schemaForStage } from "./workspace/schemas.js";
17
- import { sha256Text, workspacePaths } from "./workspace/storage.js";
25
+ import { pathExists, sha256Text, workspacePaths } from "./workspace/storage.js";
18
26
  import { doctorResearchWorkspace, initializeResearchWorkspace, requireResearchWorkspace, withWorkspaceLock, } from "./workspace/workspace.js";
19
27
  const COMMON_OPTIONS = { help: "boolean", json: "boolean" };
20
28
  const WORKSPACE_OPTIONS = { ...COMMON_OPTIONS, workspace: "string" };
@@ -55,9 +63,23 @@ export function researchOrchestrationHelp() {
55
63
  tiangong-ai research project preflight --question <question> [--requirements <absolute-json>] [--input-plan <absolute-json>] [--workspace <path>] [--json]
56
64
  tiangong-ai research project input add <project-id> --path <absolute-file> [--role primary|reference|replication] [--workspace <path>] [--json]
57
65
  tiangong-ai research project retry <project-id> [--package <package-id>] [--workspace <path>] [--json]
58
- tiangong-ai research project fork <source-project-id> --to <target-project-id> [--resume-through discover|analyze|synthesize] [--workspace <path>] [--json]
59
- tiangong-ai research schema show <discover|analyze|synthesize|review|doctor> [--json]
60
- tiangong-ai research status [--project <project-id>] [--workspace <absolute-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]
73
+ tiangong-ai research project stage submit <project-id> --session <id> --output <absolute-json> [--confirm-model <id>] [--workspace <path>] [--json]
74
+ tiangong-ai research project stage abort <project-id> --session <id> [--workspace <path>] [--json]
75
+ tiangong-ai research project evidence fetch <project-id> --request <absolute-json> [--workspace <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]
61
83
  tiangong-ai research run [--project <project-id>] [--max-parallel <1-8>] [--max-cycles <1-100>] [--dry-run] [--progress-jsonl] [--workspace <absolute-path>] [--json]
62
84
 
63
85
  ${researchSetupHelp()}
@@ -74,6 +96,7 @@ async function runSchema(argv, io) {
74
96
  return writeHelp(io);
75
97
  const stage = onePositional(args.positionals, "research schema show");
76
98
  if (stage !== "discover" &&
99
+ stage !== "acquire" &&
77
100
  stage !== "analyze" &&
78
101
  stage !== "synthesize" &&
79
102
  stage !== "review" &&
@@ -276,6 +299,279 @@ async function runProject(argv, io) {
276
299
  const [action, ...rest] = argv;
277
300
  if (!action || action === "--help" || action === "-h")
278
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
+ }
341
+ if (action === "stage") {
342
+ const [stageAction, ...stageRest] = rest;
343
+ if (stageAction === "prepare") {
344
+ const args = parseStrictArgs(stageRest, { ...WORKSPACE_OPTIONS, stage: "string", "host-agent": "string" }, "research project stage prepare");
345
+ if (strictBoolean(args, "help"))
346
+ return writeHelp(io);
347
+ const projectId = onePositional(args.positionals, "research project stage prepare");
348
+ const stage = nativeProducerStage(strictString(args, "stage"));
349
+ const hostAgent = nativeHostAgent(strictString(args, "host-agent"));
350
+ const root = await workspaceFromArgs(args);
351
+ const result = await prepareNativeResearchStage({ root, projectId, stage, hostAgent });
352
+ writeJson(io, result, args);
353
+ return 0;
354
+ }
355
+ if (stageAction === "submit") {
356
+ const args = parseStrictArgs(stageRest, {
357
+ ...WORKSPACE_OPTIONS,
358
+ session: "string",
359
+ output: "string",
360
+ "confirm-model": "string",
361
+ }, "research project stage submit");
362
+ if (strictBoolean(args, "help"))
363
+ return writeHelp(io);
364
+ const projectId = onePositional(args.positionals, "research project stage submit");
365
+ const sessionId = strictString(args, "session");
366
+ const outputPath = strictString(args, "output");
367
+ if (!sessionId || !outputPath) {
368
+ throw new CliError("stage submit requires --session and --output.", {
369
+ code: "RESEARCH_NATIVE_STAGE_OUTPUT_INVALID",
370
+ exitCode: 2,
371
+ });
372
+ }
373
+ const root = await workspaceFromArgs(args);
374
+ const result = await submitNativeResearchStage({
375
+ root,
376
+ projectId,
377
+ sessionId,
378
+ outputPath,
379
+ confirmedModel: strictString(args, "confirm-model") ?? null,
380
+ });
381
+ writeJson(io, result, args);
382
+ return 0;
383
+ }
384
+ if (stageAction === "abort") {
385
+ const args = parseStrictArgs(stageRest, { ...WORKSPACE_OPTIONS, session: "string" }, "research project stage abort");
386
+ if (strictBoolean(args, "help"))
387
+ return writeHelp(io);
388
+ const projectId = onePositional(args.positionals, "research project stage abort");
389
+ const sessionId = strictString(args, "session");
390
+ if (!sessionId) {
391
+ throw new CliError("stage abort requires --session.", {
392
+ code: "RESEARCH_NATIVE_STAGE_SESSION_REQUIRED",
393
+ exitCode: 2,
394
+ });
395
+ }
396
+ const root = await workspaceFromArgs(args);
397
+ const result = await abortNativeResearchStage({ root, projectId, sessionId });
398
+ writeJson(io, result, args);
399
+ return 0;
400
+ }
401
+ throw unknownAction("research project stage", stageAction ?? "");
402
+ }
403
+ if (action === "evidence") {
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
+ }
555
+ if (evidenceAction !== "fetch") {
556
+ throw unknownAction("research project evidence", evidenceAction ?? "");
557
+ }
558
+ const args = parseStrictArgs(evidenceRest, { ...WORKSPACE_OPTIONS, request: "string" }, "research project evidence fetch");
559
+ if (strictBoolean(args, "help"))
560
+ return writeHelp(io);
561
+ const projectId = onePositional(args.positionals, "research project evidence fetch");
562
+ const requestPath = strictString(args, "request");
563
+ if (!requestPath) {
564
+ throw new CliError("evidence fetch requires --request.", {
565
+ code: "RESEARCH_BROKER_REQUEST_INVALID",
566
+ exitCode: 2,
567
+ });
568
+ }
569
+ const request = await readNativeEvidenceRequest(requestPath);
570
+ const root = await workspaceFromArgs(args);
571
+ const result = await withWorkspaceLock(root, "research.native-evidence.fetch", () => fetchNativeCandidateSource({ root, projectId, request }));
572
+ writeJson(io, result, args);
573
+ return 0;
574
+ }
279
575
  if (action === "init") {
280
576
  const args = parseStrictArgs(rest, {
281
577
  ...WORKSPACE_OPTIONS,
@@ -377,33 +673,180 @@ async function runProject(argv, io) {
377
673
  writeJson(io, project, args);
378
674
  return 0;
379
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
+ }
380
710
  throw unknownAction("research project", action);
381
711
  }
382
712
  async function runStatus(argv, io) {
383
- const args = parseStrictArgs(argv, { ...WORKSPACE_OPTIONS, project: "string" }, "research status");
713
+ const args = parseStrictArgs(argv, { ...WORKSPACE_OPTIONS, project: "string", all: "boolean" }, "research status");
384
714
  if (strictBoolean(args, "help"))
385
715
  return writeHelp(io);
386
716
  rejectPositionals(args.positionals, "research status");
387
717
  const root = await workspaceFromArgs(args);
388
718
  const selectedProject = strictString(args, "project");
389
- const projects = selectedProject
390
- ? [await loadProject(root, selectedProject)]
391
- : 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");
392
731
  const result = {
393
732
  workspace: root,
394
- projects: projects.map((project) => ({
395
- id: project.id,
396
- question: project.question,
397
- status: refreshProject(project).status,
398
- readyPackage: nextReadyPackage(project)?.id ?? null,
399
- usage: project.usage,
400
- inputs: project.inputs,
401
- 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
+ };
402
764
  })),
403
765
  };
404
766
  writeJson(io, result, args);
405
767
  return 0;
406
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
+ }
407
850
  async function runWorkspaceExecution(argv, io) {
408
851
  const args = parseStrictArgs(argv, {
409
852
  ...WORKSPACE_OPTIONS,
@@ -457,6 +900,8 @@ async function readEvidenceRequirements(path) {
457
900
  !Array.isArray(value.sourceTypes) ||
458
901
  (value.requiredCapabilityIds !== undefined &&
459
902
  !Array.isArray(value.requiredCapabilityIds)) ||
903
+ (value.requiredCompanionIds !== undefined &&
904
+ !Array.isArray(value.requiredCompanionIds)) ||
460
905
  (value.requiredDiscoveryScopes !== undefined &&
461
906
  !Array.isArray(value.requiredDiscoveryScopes)) ||
462
907
  typeof value.minSources !== "number" ||
@@ -474,6 +919,7 @@ async function readEvidenceRequirements(path) {
474
919
  return normalizeEvidenceRequirements({
475
920
  ...value,
476
921
  requiredCapabilityIds: value.requiredCapabilityIds ?? [],
922
+ requiredCompanionIds: value.requiredCompanionIds ?? [],
477
923
  requiredDiscoveryScopes: value.requiredDiscoveryScopes ?? [],
478
924
  });
479
925
  }
@@ -490,13 +936,65 @@ function researchMode(value) {
490
936
  function resumeStage(value) {
491
937
  if (!value)
492
938
  return undefined;
493
- if (value === "discover" || value === "analyze" || value === "synthesize")
939
+ if (value === "discover" || value === "acquire" || value === "analyze" || value === "synthesize")
494
940
  return value;
495
941
  throw new CliError(`Unsupported --resume-through stage: ${value}`, {
496
942
  code: "RESEARCH_PROJECT_FORK_INVALID",
497
943
  exitCode: 2,
498
944
  });
499
945
  }
946
+ function nativeProducerStage(value) {
947
+ if (value === "discover" || value === "acquire" || value === "analyze" || value === "synthesize")
948
+ return value;
949
+ throw new CliError("--stage must be discover, acquire, analyze, or synthesize.", {
950
+ code: "RESEARCH_NATIVE_STAGE_INVALID",
951
+ exitCode: 2,
952
+ });
953
+ }
954
+ function nativeHostAgent(value) {
955
+ if (value === "codex" || value === "claude")
956
+ return value;
957
+ throw new CliError("--host-agent must be codex or claude.", {
958
+ code: "RESEARCH_NATIVE_HOST_INVALID",
959
+ exitCode: 2,
960
+ });
961
+ }
962
+ async function readNativeEvidenceRequest(path) {
963
+ return readBoundedJsonRecord(path, "--request", "RESEARCH_BROKER_REQUEST_INVALID");
964
+ }
965
+ async function readBoundedJsonRecord(path, label, code) {
966
+ if (!isAbsolute(path)) {
967
+ throw new CliError(`${label} must be an absolute JSON file path.`, {
968
+ code,
969
+ exitCode: 2,
970
+ });
971
+ }
972
+ const selected = resolve(path);
973
+ const info = await lstat(selected).catch(() => undefined);
974
+ if (!info?.isFile() || info.isSymbolicLink() || info.size > 1024 * 1024) {
975
+ throw new CliError(`${label} must be a bounded regular non-symlink JSON file.`, {
976
+ code,
977
+ exitCode: 2,
978
+ });
979
+ }
980
+ let value;
981
+ try {
982
+ value = JSON.parse(await readFile(selected, "utf8"));
983
+ }
984
+ catch {
985
+ throw new CliError(`${label} contains invalid JSON.`, {
986
+ code,
987
+ exitCode: 2,
988
+ });
989
+ }
990
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
991
+ throw new CliError(`${label} must contain one JSON object.`, {
992
+ code,
993
+ exitCode: 2,
994
+ });
995
+ }
996
+ return value;
997
+ }
500
998
  async function workspaceFromArgs(args) {
501
999
  return requireResearchWorkspace(strictString(args, "workspace") ?? process.cwd());
502
1000
  }