@tiangong-ai/cli 0.0.32 → 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 (58) hide show
  1. package/AGENTS.md +2 -2
  2. package/README.md +51 -17
  3. package/dist/research/orchestration.js +388 -31
  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.js +265 -56
  12. package/dist/research/workspace/broker.js.map +1 -1
  13. package/dist/research/workspace/discovery-planning.d.ts +25 -0
  14. package/dist/research/workspace/discovery-planning.js +106 -0
  15. package/dist/research/workspace/discovery-planning.js.map +1 -0
  16. package/dist/research/workspace/discovery-status.d.ts +46 -0
  17. package/dist/research/workspace/discovery-status.js +183 -0
  18. package/dist/research/workspace/discovery-status.js.map +1 -0
  19. package/dist/research/workspace/discovery.d.ts +25 -0
  20. package/dist/research/workspace/discovery.js +268 -0
  21. package/dist/research/workspace/discovery.js.map +1 -0
  22. package/dist/research/workspace/downloads.d.ts +76 -0
  23. package/dist/research/workspace/downloads.js +274 -0
  24. package/dist/research/workspace/downloads.js.map +1 -0
  25. package/dist/research/workspace/evidence-ledger.d.ts +52 -0
  26. package/dist/research/workspace/evidence-ledger.js +487 -0
  27. package/dist/research/workspace/evidence-ledger.js.map +1 -0
  28. package/dist/research/workspace/evidence.d.ts +1 -0
  29. package/dist/research/workspace/evidence.js +2 -0
  30. package/dist/research/workspace/evidence.js.map +1 -1
  31. package/dist/research/workspace/input-plan.js +4 -0
  32. package/dist/research/workspace/input-plan.js.map +1 -1
  33. package/dist/research/workspace/native-activity.d.ts +65 -0
  34. package/dist/research/workspace/native-activity.js +153 -0
  35. package/dist/research/workspace/native-activity.js.map +1 -0
  36. package/dist/research/workspace/preflight.d.ts +5 -0
  37. package/dist/research/workspace/preflight.js +37 -17
  38. package/dist/research/workspace/preflight.js.map +1 -1
  39. package/dist/research/workspace/projects.d.ts +3 -1
  40. package/dist/research/workspace/projects.js +346 -5
  41. package/dist/research/workspace/projects.js.map +1 -1
  42. package/dist/research/workspace/runtime.d.ts +106 -4
  43. package/dist/research/workspace/runtime.js +947 -80
  44. package/dist/research/workspace/runtime.js.map +1 -1
  45. package/dist/research/workspace/sanitization.js +28 -6
  46. package/dist/research/workspace/sanitization.js.map +1 -1
  47. package/dist/research/workspace/schemas.d.ts +3 -0
  48. package/dist/research/workspace/schemas.js +206 -33
  49. package/dist/research/workspace/schemas.js.map +1 -1
  50. package/dist/research/workspace/setup-wizard.js +10 -4
  51. package/dist/research/workspace/setup-wizard.js.map +1 -1
  52. package/dist/research/workspace/setup.d.ts +4 -1
  53. package/dist/research/workspace/setup.js +61 -11
  54. package/dist/research/workspace/setup.js.map +1 -1
  55. package/dist/research/workspace/types.d.ts +31 -3
  56. package/dist/research/workspace/workspace.js +45 -6
  57. package/dist/research/workspace/workspace.js.map +1 -1
  58. package/package.json +1 -1
package/AGENTS.md CHANGED
@@ -15,8 +15,8 @@ checkPaths:
15
15
  - .docpact/config.yaml
16
16
  - docs/agents/**
17
17
  - src/**
18
- lastReviewedAt: 2026-08-10
19
- lastReviewedCommit: bef62f8d48c42eaff14fa2bd7eba1be83a46a58b
18
+ lastReviewedAt: 2026-08-11
19
+ lastReviewedCommit: 2ce7ad7cd36c1aeba577bc72e4095ed3a81debd6
20
20
  ---
21
21
 
22
22
  # Tiangong AI CLI Contract
package/README.md CHANGED
@@ -332,8 +332,9 @@ full source. Symlinks, duplicate content, changed hashes, and context above
332
332
  `maxInputContextTokens` are rejected.
333
333
 
334
334
  The workspace stores its current protocol state under `.tiangong-research/`.
335
- Each project follows five ordered stages: evidence discovery, analysis,
336
- synthesis, independent review, and mechanical closure. Discover, analyze, and
335
+ Each project follows the evidence-first sequence: broad discovery, strict
336
+ admission, acquisition audit, immutable evidence freeze, analysis, synthesis,
337
+ independent review, and mechanical closure. Discover, acquire, analyze, and
337
338
  synthesize run in the current interactive Codex app/session or Claude Code
338
339
  session. The CLI never launches a nested producer process. Independent review
339
340
  runs through the other configured agent family's CLI, and execution is blocked
@@ -369,6 +370,31 @@ tiangong-ai research project stage submit gpu-resource-impact \
369
370
  tiangong-ai research status --workspace /absolute/path/to/workspace --json
370
371
  ```
371
372
 
373
+ The discover packet derives a bounded multi-channel plan from reviewed evidence
374
+ requirements. Required channels run first; exact repeated requests reuse the
375
+ project cache without another provider call but still consume a bounded context
376
+ view; remaining views are spent only on explicit coverage,
377
+ counterevidence, date, applicability, or full-text gaps. Native Web/Browser
378
+ leads may be registered as supplemental candidates, but they cannot be admitted
379
+ until the same canonical URL/DOI has an immutable broker occurrence. Registered
380
+ inputs are formal candidates under their own content-hash identity.
381
+ The acquire packet audits every provisional source and registers only explicit
382
+ files—never a directory or “latest download.” PDF and Office artifacts are
383
+ structurally verified and content-addressed. A registered binary full file is
384
+ review-bound but is not counted as producer-readable full text unless an
385
+ admitted UTF-8 text/JSON/HTML/CSV/Markdown derivative exists. Such a derivative
386
+ names its registered parent and inherits that parent's canonical source URL;
387
+ it does not invent a second network-download binding, and a conflicting URL is
388
+ rejected.
389
+
390
+ Successful acquisition freezes an immutable evidence snapshot before analysis.
391
+ The reviewer and mechanical closure bind and recheck the snapshot chain,
392
+ ledger, receipts, selected artifacts, excerpts, analysis, and report. Refresh a
393
+ closed result with `research project addendum SOURCE --to TARGET`; the original
394
+ closure remains unchanged, the child snapshot records a mechanical delta, and
395
+ default status hides the superseded project (`research status --all` shows full
396
+ lineage).
397
+
372
398
  Use `research run --project <id>` for an auditable project-scoped run: only
373
399
  that project is checked, scheduled, summarized, and bound to the top-level
374
400
  JSON/JSONL `projectId`, so historical blocked siblings do not alter its exit
@@ -405,8 +431,9 @@ the resolved target path and independently hashes the target executable, route
405
431
  launcher/wrapper, and internal adapter. A wrapper that performs an unpinned
406
432
  PATH lookup is not a reproducible route.
407
433
 
408
- The CLI owns the authoritative JSON Schemas for discovery, analysis,
409
- synthesis, and review. Inspect one with `research schema show <stage> --json`.
434
+ The CLI owns the authoritative JSON Schemas for discovery, acquisition,
435
+ analysis, synthesis, and review. Inspect one with
436
+ `research schema show <stage> --json`.
410
437
  Native producer preparation returns the exact schema and prompt to the current
411
438
  host; `stage submit` validates and atomically materializes its JSON. A rejected
412
439
  native submission keeps the bound session for an explicit correction and never
@@ -418,12 +445,16 @@ repair with no research tools.
418
445
  Total, per-package, output, repair, broker-response bytes, estimated broker
419
446
  context tokens, context items, wall-time, output-count, output-size, and attempt
420
447
  limits live in `.tiangong-research/config.json`.
421
- New workspaces reserve 550,000 total tokens by default, including 230,000 for
422
- discovery; the remaining package defaults are 60,000 for analysis, 70,000 for
423
- synthesis, and 175,000 for review. Primary output is bounded at 6,000 tokens
424
- and a separately invoked repair at 4,000. These are admission ceilings rather
425
- than a target spend and can be lowered only when the resulting pre-call
426
- reservations still fit.
448
+ New production workspaces use generous but finite runaway ceilings: 20,000,000
449
+ total tokens and package ceilings of 12,000,000 for discovery, 2,000,000 for
450
+ acquisition, 1,500,000 each for analysis and synthesis, and 2,500,000 for
451
+ review. Primary output is bounded at 32,000 tokens and a separately invoked
452
+ repair at 16,000. The production broker hard ceiling is 256 bounded views with
453
+ 32,000 context tokens per view; input context is bounded at 128,000 tokens.
454
+ These values are not a target spend. Coverage-derived working plans and early
455
+ stop control ordinary use, while the finite ceilings, three attempts per
456
+ package, and explicit confirmation above the cost threshold stop runaway work.
457
+ Smoke-test workspaces retain their smaller low-cost defaults.
427
458
  Before project initialization and every executable package, the control plane
428
459
  requires the complete token and conservative price reservation to fit. Native
429
460
  producer stages reserve prompt, schema, admitted context, bounded broker
@@ -438,13 +469,16 @@ Independent review uses the pre-call reservation calculator and the reviewer's
438
469
  provider-side structured-output/turn controls where available. Review admission
439
470
  reserves three maximum-size generated artifacts plus one globally bounded
440
471
  evidence-excerpt bundle, and formatting repair remains one separately budgeted,
441
- tool-free JSON correction. New workspaces also enforce a six-call broker budget
442
- mechanically; every successful native evidence fetch reports the remaining
443
- calls and excess calls are rejected before another provider request or evidence
444
- promotion. Reviewer usage records separate input, cached-input, and output
445
- tokens; configured pricing fills cost when the provider omits it. Run records
446
- and JSONL progress preserve sanitized accounting mode, event/item counts,
447
- provider turns, tool calls, reasoning tokens, and bounded provider errors.
472
+ tool-free JSON correction. Production workspaces enforce a finite 256-view
473
+ broker ceiling mechanically, while each project derives a much smaller working
474
+ budget from its reviewed coverage requirements and stops early when they are
475
+ supportable. Every successful native evidence fetch reports the remaining
476
+ working budget; excess calls are rejected before another provider request or
477
+ evidence promotion. Reviewer usage records separate input, cached-input, and
478
+ output tokens; configured pricing fills cost when the provider omits it. Run
479
+ records and JSONL progress preserve sanitized accounting mode, event/item
480
+ counts, provider turns, tool calls, reasoning tokens, and bounded provider
481
+ errors.
448
482
 
449
483
  Every evidence source must resolve to an admitted input or a completed broker
450
484
  receipt. Successful broker bodies are immutable content-addressed objects under
@@ -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
  }