@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.
- package/AGENTS.md +2 -2
- package/README.md +51 -17
- package/dist/research/orchestration.js +388 -31
- package/dist/research/orchestration.js.map +1 -1
- package/dist/research/workspace/acquisition.d.ts +71 -0
- package/dist/research/workspace/acquisition.js +593 -0
- package/dist/research/workspace/acquisition.js.map +1 -0
- package/dist/research/workspace/artifacts.d.ts +43 -0
- package/dist/research/workspace/artifacts.js +544 -0
- package/dist/research/workspace/artifacts.js.map +1 -0
- package/dist/research/workspace/broker.js +265 -56
- package/dist/research/workspace/broker.js.map +1 -1
- package/dist/research/workspace/discovery-planning.d.ts +25 -0
- package/dist/research/workspace/discovery-planning.js +106 -0
- package/dist/research/workspace/discovery-planning.js.map +1 -0
- package/dist/research/workspace/discovery-status.d.ts +46 -0
- package/dist/research/workspace/discovery-status.js +183 -0
- package/dist/research/workspace/discovery-status.js.map +1 -0
- package/dist/research/workspace/discovery.d.ts +25 -0
- package/dist/research/workspace/discovery.js +268 -0
- package/dist/research/workspace/discovery.js.map +1 -0
- package/dist/research/workspace/downloads.d.ts +76 -0
- package/dist/research/workspace/downloads.js +274 -0
- package/dist/research/workspace/downloads.js.map +1 -0
- package/dist/research/workspace/evidence-ledger.d.ts +52 -0
- package/dist/research/workspace/evidence-ledger.js +487 -0
- package/dist/research/workspace/evidence-ledger.js.map +1 -0
- package/dist/research/workspace/evidence.d.ts +1 -0
- package/dist/research/workspace/evidence.js +2 -0
- package/dist/research/workspace/evidence.js.map +1 -1
- package/dist/research/workspace/input-plan.js +4 -0
- package/dist/research/workspace/input-plan.js.map +1 -1
- package/dist/research/workspace/native-activity.d.ts +65 -0
- package/dist/research/workspace/native-activity.js +153 -0
- package/dist/research/workspace/native-activity.js.map +1 -0
- package/dist/research/workspace/preflight.d.ts +5 -0
- package/dist/research/workspace/preflight.js +37 -17
- package/dist/research/workspace/preflight.js.map +1 -1
- package/dist/research/workspace/projects.d.ts +3 -1
- package/dist/research/workspace/projects.js +346 -5
- package/dist/research/workspace/projects.js.map +1 -1
- package/dist/research/workspace/runtime.d.ts +106 -4
- package/dist/research/workspace/runtime.js +947 -80
- package/dist/research/workspace/runtime.js.map +1 -1
- package/dist/research/workspace/sanitization.js +28 -6
- package/dist/research/workspace/sanitization.js.map +1 -1
- package/dist/research/workspace/schemas.d.ts +3 -0
- package/dist/research/workspace/schemas.js +206 -33
- package/dist/research/workspace/schemas.js.map +1 -1
- package/dist/research/workspace/setup-wizard.js +10 -4
- package/dist/research/workspace/setup-wizard.js.map +1 -1
- package/dist/research/workspace/setup.d.ts +4 -1
- package/dist/research/workspace/setup.js +61 -11
- package/dist/research/workspace/setup.js.map +1 -1
- package/dist/research/workspace/types.d.ts +31 -3
- package/dist/research/workspace/workspace.js +45 -6
- package/dist/research/workspace/workspace.js.map +1 -1
- 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-
|
|
19
|
-
lastReviewedCommit:
|
|
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
|
|
336
|
-
|
|
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,
|
|
409
|
-
synthesis, and review. Inspect one with
|
|
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
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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.
|
|
442
|
-
mechanically
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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
|
|
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
|
|
65
|
-
tiangong-ai research
|
|
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
|
|
479
|
-
|
|
480
|
-
|
|
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
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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(
|
|
611
|
-
code
|
|
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(
|
|
619
|
-
code
|
|
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(
|
|
629
|
-
code
|
|
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(
|
|
635
|
-
code
|
|
991
|
+
throw new CliError(`${label} must contain one JSON object.`, {
|
|
992
|
+
code,
|
|
636
993
|
exitCode: 2,
|
|
637
994
|
});
|
|
638
995
|
}
|