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