@tiangong-ai/cli 0.0.31 → 0.0.32
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 +97 -89
- package/dist/research/orchestration.js +144 -3
- package/dist/research/orchestration.js.map +1 -1
- package/dist/research/workspace/broker.d.ts +5 -0
- package/dist/research/workspace/broker.js +70 -4
- 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/preflight.d.ts +13 -0
- package/dist/research/workspace/preflight.js +10 -3
- package/dist/research/workspace/preflight.js.map +1 -1
- package/dist/research/workspace/projects.js +11 -0
- package/dist/research/workspace/projects.js.map +1 -1
- package/dist/research/workspace/runtime.d.ts +68 -2
- package/dist/research/workspace/runtime.js +652 -14
- package/dist/research/workspace/runtime.js.map +1 -1
- package/dist/research/workspace/setup-catalog.js +2 -2
- package/dist/research/workspace/setup-wizard.js +35 -13
- package/dist/research/workspace/setup-wizard.js.map +1 -1
- package/dist/research/workspace/setup.d.ts +23 -4
- package/dist/research/workspace/setup.js +339 -74
- package/dist/research/workspace/setup.js.map +1 -1
- package/dist/research/workspace/types.d.ts +7 -0
- package/dist/research/workspace/workspace.d.ts +2 -0
- package/dist/research/workspace/workspace.js +83 -20
- package/dist/research/workspace/workspace.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { cp, lstat, readFile, rm } from "node:fs/promises";
|
|
3
|
-
import { basename, dirname, join, relative } from "node:path";
|
|
3
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
4
4
|
import { CliError } from "../../errors.js";
|
|
5
5
|
import { loadCapabilityDeclarations, stageLockedCapabilities, verifyCapabilities, } from "./capabilities.js";
|
|
6
6
|
import { startCapabilityBroker } from "./broker.js";
|
|
@@ -16,6 +16,13 @@ import { parseStructuredStageOutput, schemaForStage, StructuredOutputError } fro
|
|
|
16
16
|
import { canonicalJson, ensureDirectory, fileRecord, isObject, pathExists, readJsonFile, regularTreeFiles, resolveContained, sha256File, sha256Text, workspacePaths, writeJsonAtomic, writeTextAtomic, } from "./storage.js";
|
|
17
17
|
import { loadWorkspaceConfig, verifyDoctorAttestation, withWorkspaceLock } from "./workspace.js";
|
|
18
18
|
export async function runResearchWorkspace(root, options, packageExecutor = executeAgent) {
|
|
19
|
+
return runResearchWorkspaceInternal(root, options, packageExecutor, false);
|
|
20
|
+
}
|
|
21
|
+
/** @internal Test-only seam for exercising deterministic package admission with fake agents. */
|
|
22
|
+
export async function runResearchWorkspaceWithInjectedProducerForTesting(root, options, packageExecutor) {
|
|
23
|
+
return runResearchWorkspaceInternal(root, options, packageExecutor, true);
|
|
24
|
+
}
|
|
25
|
+
async function runResearchWorkspaceInternal(root, options, packageExecutor, allowInjectedProducerForTesting) {
|
|
19
26
|
validateRunOptions(options);
|
|
20
27
|
const requestId = randomUUID();
|
|
21
28
|
if (options.dryRun)
|
|
@@ -36,7 +43,7 @@ export async function runResearchWorkspace(root, options, packageExecutor = exec
|
|
|
36
43
|
if (config.mode === "production-research") {
|
|
37
44
|
const verification = await verifyDoctorAttestation(root);
|
|
38
45
|
if (verification.status !== "verified" || !verification.attestation) {
|
|
39
|
-
throw new CliError("Production research requires a current successful
|
|
46
|
+
throw new CliError("Production research requires a current successful independent-reviewer doctor smoke.", {
|
|
40
47
|
code: "RESEARCH_DOCTOR_ATTESTATION_REQUIRED",
|
|
41
48
|
exitCode: 3,
|
|
42
49
|
details: { status: verification.status, errors: verification.errors },
|
|
@@ -61,6 +68,7 @@ export async function runResearchWorkspace(root, options, packageExecutor = exec
|
|
|
61
68
|
const selected = projects
|
|
62
69
|
.map((project) => ({ project, workPackage: nextReadyPackage(project) }))
|
|
63
70
|
.filter((item) => Boolean(item.workPackage) &&
|
|
71
|
+
(allowInjectedProducerForTesting || item.workPackage?.executor !== "producer") &&
|
|
64
72
|
item.project.status !== "blocked" &&
|
|
65
73
|
item.project.status !== "complete")
|
|
66
74
|
.slice(0, options.maxParallel);
|
|
@@ -78,6 +86,405 @@ export async function runResearchWorkspace(root, options, packageExecutor = exec
|
|
|
78
86
|
return result;
|
|
79
87
|
});
|
|
80
88
|
}
|
|
89
|
+
export async function prepareNativeResearchStage(input) {
|
|
90
|
+
return withWorkspaceLock(input.root, "research.native-stage.prepare", async () => {
|
|
91
|
+
await verifyJournal(workspacePaths(input.root).journal);
|
|
92
|
+
const config = await loadWorkspaceConfig(input.root);
|
|
93
|
+
assertExecutionConfiguration(config);
|
|
94
|
+
if (config.producer.agent !== input.hostAgent) {
|
|
95
|
+
throw new CliError(`This workspace requires the current native ${config.producer.agent} host, not ${input.hostAgent}.`, { code: "RESEARCH_NATIVE_HOST_MISMATCH", exitCode: 3 });
|
|
96
|
+
}
|
|
97
|
+
const capabilityVerification = await verifyCapabilities(input.root);
|
|
98
|
+
if (capabilityVerification.status !== "verified") {
|
|
99
|
+
throw new CliError("Native research requires verified capability locks.", {
|
|
100
|
+
code: "RESEARCH_CAPABILITY_DRIFT",
|
|
101
|
+
exitCode: 3,
|
|
102
|
+
details: capabilityVerification,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
if (config.mode === "production-research") {
|
|
106
|
+
const attestation = await verifyDoctorAttestation(input.root);
|
|
107
|
+
if (attestation.status !== "verified") {
|
|
108
|
+
throw new CliError("Native research requires a current independent-reviewer attestation.", {
|
|
109
|
+
code: "RESEARCH_DOCTOR_ATTESTATION_REQUIRED",
|
|
110
|
+
exitCode: 3,
|
|
111
|
+
details: { status: attestation.status, errors: attestation.errors },
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const project = await loadProject(input.root, input.projectId);
|
|
116
|
+
if (config.mode === "production-research" &&
|
|
117
|
+
config.budget.maxCostUsd > config.budget.confirmationCostUsd &&
|
|
118
|
+
!project.budgetConfirmedAt) {
|
|
119
|
+
throw new CliError("Production research budget has not been explicitly confirmed.", {
|
|
120
|
+
code: "RESEARCH_BUDGET_CONFIRMATION_REQUIRED",
|
|
121
|
+
exitCode: 3,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
const activePath = nativeStageSessionPath(input.root, project.id);
|
|
125
|
+
if (await pathExists(activePath)) {
|
|
126
|
+
const active = await readNativeStageSession(input.root, project.id);
|
|
127
|
+
if (active.packet.stage === input.stage &&
|
|
128
|
+
active.packet.hostAgent === input.hostAgent &&
|
|
129
|
+
project.packages.find((item) => item.id === active.packet.packageId)?.status === "running") {
|
|
130
|
+
await assertNativeStageBinding(input.root, project, active.packet);
|
|
131
|
+
return active.packet;
|
|
132
|
+
}
|
|
133
|
+
throw new CliError("Another native stage session is already active for this project.", {
|
|
134
|
+
code: "RESEARCH_NATIVE_STAGE_ACTIVE",
|
|
135
|
+
exitCode: 3,
|
|
136
|
+
details: { sessionId: active.packet.sessionId, stage: active.packet.stage },
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
const workPackage = nextReadyPackage(project);
|
|
140
|
+
if (!workPackage || workPackage.executor !== "producer" || workPackage.stage !== input.stage) {
|
|
141
|
+
throw new CliError(`Stage ${input.stage} is not the next native producer package.`, {
|
|
142
|
+
code: "RESEARCH_NATIVE_STAGE_NOT_READY",
|
|
143
|
+
exitCode: 3,
|
|
144
|
+
details: { readyPackage: workPackage?.id ?? null },
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
if (workPackage.attempts >= workPackage.maxAttempts) {
|
|
148
|
+
throw new CliError("Native producer package exhausted its reviewed attempt limit.", {
|
|
149
|
+
code: "RESEARCH_PACKAGE_ATTEMPTS_EXHAUSTED",
|
|
150
|
+
exitCode: 3,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
const reservation = reservePackageBudget(project, workPackage, config);
|
|
154
|
+
const sessionId = randomUUID();
|
|
155
|
+
let capsule = null;
|
|
156
|
+
let preparedStatePersisted = false;
|
|
157
|
+
try {
|
|
158
|
+
capsule = await createCapsule(input.root, project, workPackage, sessionId, config);
|
|
159
|
+
const stageContextContent = await stageContextForPackage(capsule.projectRoot, workPackage, config);
|
|
160
|
+
const declarations = await loadCapabilityDeclarations(input.root);
|
|
161
|
+
const hasBrokeredEvidence = declarations.capabilities.some((capability) => capability.permissions.includes("brokered-network"));
|
|
162
|
+
const basePrompt = packagePrompt(project, workPackage, capsule.inputManifest, capsule.stagedSkills, capsule.capabilityDocumentation, null, capsule.contextBundle, capsule.contextBundleContent, stageContextContent, config.budget.maxBrokerCalls);
|
|
163
|
+
const prompt = [
|
|
164
|
+
"Perform this producer stage in the current interactive host session. Do not launch codex exec, claude -p, or any other nested reasoning agent.",
|
|
165
|
+
input.stage === "discover" && hasBrokeredEvidence
|
|
166
|
+
? "For every internet/database request, write one non-secret request JSON file and invoke the packet's fetchEvidence argv through the CLI control plane. Use only its returned bounded context and receipt. Do not use standalone web search as evidence."
|
|
167
|
+
: "Do not acquire additional evidence in this stage.",
|
|
168
|
+
basePrompt,
|
|
169
|
+
"Save only the final schema-conforming JSON object to a new regular file, then submit it with the packet's submit command. The CLI remains the sole authority for validation and atomic promotion.",
|
|
170
|
+
].join("\n\n");
|
|
171
|
+
const preparedAt = new Date().toISOString();
|
|
172
|
+
const bindingSha256 = await nativeStageBinding(input.root, project, workPackage);
|
|
173
|
+
const packetCore = {
|
|
174
|
+
schemaVersion: 1,
|
|
175
|
+
kind: "tiangong-native-research-stage",
|
|
176
|
+
sessionId,
|
|
177
|
+
projectId: project.id,
|
|
178
|
+
packageId: workPackage.id,
|
|
179
|
+
stage: input.stage,
|
|
180
|
+
hostAgent: input.hostAgent,
|
|
181
|
+
expectedModel: config.producer.model,
|
|
182
|
+
preparedAt,
|
|
183
|
+
bindingSha256,
|
|
184
|
+
prompt,
|
|
185
|
+
outputSchema: schemaForStage(input.stage, null, input.stage === "discover" && !hasBrokeredEvidence
|
|
186
|
+
? { inputOnlyProvenanceIds: capsule.inputManifest.map((record) => record.id) }
|
|
187
|
+
: {}),
|
|
188
|
+
limits: {
|
|
189
|
+
maxOutputBytes: config.budget.maxBytesPerPackage,
|
|
190
|
+
maxOutputTokens: config.budget.maxOutputTokens,
|
|
191
|
+
reservedPackageTokens: config.budget.packageMaxTokens[input.stage],
|
|
192
|
+
reservedMaxCostUsd: reservation.costUsd,
|
|
193
|
+
maxWallSeconds: config.budget.packageMaxWallSeconds[input.stage],
|
|
194
|
+
},
|
|
195
|
+
commands: {
|
|
196
|
+
fetchEvidence: input.stage === "discover" && hasBrokeredEvidence
|
|
197
|
+
? {
|
|
198
|
+
argv: [
|
|
199
|
+
"tiangong-ai",
|
|
200
|
+
"research",
|
|
201
|
+
"project",
|
|
202
|
+
"evidence",
|
|
203
|
+
"fetch",
|
|
204
|
+
project.id,
|
|
205
|
+
"--request",
|
|
206
|
+
"<absolute-request.json>",
|
|
207
|
+
"--workspace",
|
|
208
|
+
input.root,
|
|
209
|
+
"--json",
|
|
210
|
+
],
|
|
211
|
+
requestSchema: {
|
|
212
|
+
type: "object",
|
|
213
|
+
additionalProperties: false,
|
|
214
|
+
required: ["capability_id", "url"],
|
|
215
|
+
properties: {
|
|
216
|
+
capability_id: { type: "string" },
|
|
217
|
+
credential_id: { type: "string" },
|
|
218
|
+
url: { type: "string", format: "uri" },
|
|
219
|
+
request_body: { type: "object" },
|
|
220
|
+
json_pointer: { type: "string" },
|
|
221
|
+
item_offset: { type: "integer", minimum: 0 },
|
|
222
|
+
max_items: { type: "integer", minimum: 1 },
|
|
223
|
+
cache_mode: { enum: ["prefer", "bypass"] },
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
}
|
|
227
|
+
: null,
|
|
228
|
+
submit: {
|
|
229
|
+
argv: [
|
|
230
|
+
"tiangong-ai",
|
|
231
|
+
"research",
|
|
232
|
+
"project",
|
|
233
|
+
"stage",
|
|
234
|
+
"submit",
|
|
235
|
+
project.id,
|
|
236
|
+
"--session",
|
|
237
|
+
sessionId,
|
|
238
|
+
"--output",
|
|
239
|
+
"<absolute-output.json>",
|
|
240
|
+
...(config.producer.model ? ["--confirm-model", config.producer.model] : []),
|
|
241
|
+
"--workspace",
|
|
242
|
+
input.root,
|
|
243
|
+
"--json",
|
|
244
|
+
],
|
|
245
|
+
},
|
|
246
|
+
abort: {
|
|
247
|
+
argv: [
|
|
248
|
+
"tiangong-ai",
|
|
249
|
+
"research",
|
|
250
|
+
"project",
|
|
251
|
+
"stage",
|
|
252
|
+
"abort",
|
|
253
|
+
project.id,
|
|
254
|
+
"--session",
|
|
255
|
+
sessionId,
|
|
256
|
+
"--workspace",
|
|
257
|
+
input.root,
|
|
258
|
+
"--json",
|
|
259
|
+
],
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
rules: [
|
|
263
|
+
"Current native host performs producer reasoning; the CLI does not spawn a producer.",
|
|
264
|
+
"Only broker receipts or registered immutable inputs may support discover output.",
|
|
265
|
+
"Do not place credentials, cookies, authorization data, or sensitive URL parameters in request/output files.",
|
|
266
|
+
"A file's existence is not success; submit performs schema, provenance, budget, hash, and atomic-commit checks.",
|
|
267
|
+
],
|
|
268
|
+
};
|
|
269
|
+
const packet = {
|
|
270
|
+
...packetCore,
|
|
271
|
+
packetSha256: sha256Text(canonicalJson(packetCore)),
|
|
272
|
+
};
|
|
273
|
+
const sessionCore = {
|
|
274
|
+
schemaVersion: 1,
|
|
275
|
+
kind: "tiangong-native-research-stage-session",
|
|
276
|
+
packet,
|
|
277
|
+
capsuleRoot: capsule.capsuleRoot,
|
|
278
|
+
capsuleProject: capsule.projectRoot,
|
|
279
|
+
};
|
|
280
|
+
const session = {
|
|
281
|
+
...sessionCore,
|
|
282
|
+
sessionSha256: sha256Text(canonicalJson(sessionCore)),
|
|
283
|
+
};
|
|
284
|
+
const now = new Date().toISOString();
|
|
285
|
+
workPackage.status = "running";
|
|
286
|
+
workPackage.attempts += 1;
|
|
287
|
+
workPackage.startedAt = now;
|
|
288
|
+
workPackage.completedAt = null;
|
|
289
|
+
workPackage.lastError = null;
|
|
290
|
+
workPackage.lastFailureKind = null;
|
|
291
|
+
workPackage.retryNotBefore = null;
|
|
292
|
+
refreshProject(project);
|
|
293
|
+
await ensureDirectory(dirname(activePath));
|
|
294
|
+
await writeJsonAtomic(activePath, session);
|
|
295
|
+
await saveProject(input.root, project);
|
|
296
|
+
preparedStatePersisted = true;
|
|
297
|
+
await appendJournalEvent(workspacePaths(input.root).journal, "native.stage.prepared", project.id, {
|
|
298
|
+
sessionId,
|
|
299
|
+
packetSha256: packet.packetSha256,
|
|
300
|
+
bindingSha256,
|
|
301
|
+
projectId: project.id,
|
|
302
|
+
packageId: workPackage.id,
|
|
303
|
+
stage: input.stage,
|
|
304
|
+
hostAgent: input.hostAgent,
|
|
305
|
+
expectedModel: config.producer.model,
|
|
306
|
+
accountingMode: "reserved-native-host",
|
|
307
|
+
});
|
|
308
|
+
return packet;
|
|
309
|
+
}
|
|
310
|
+
catch (error) {
|
|
311
|
+
if (!preparedStatePersisted) {
|
|
312
|
+
await rm(activePath, { force: true });
|
|
313
|
+
if (capsule)
|
|
314
|
+
await rm(capsule.capsuleRoot, { recursive: true, force: true });
|
|
315
|
+
}
|
|
316
|
+
throw error;
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
export async function submitNativeResearchStage(input) {
|
|
321
|
+
return withWorkspaceLock(input.root, "research.native-stage.submit", async () => {
|
|
322
|
+
const session = await readNativeStageSession(input.root, input.projectId);
|
|
323
|
+
if (session.packet.sessionId !== input.sessionId) {
|
|
324
|
+
throw new CliError("Native stage session ID does not match the active session.", {
|
|
325
|
+
code: "RESEARCH_NATIVE_STAGE_SESSION_MISMATCH",
|
|
326
|
+
exitCode: 3,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
const outputPath = requireNativeOutputPath(input.outputPath);
|
|
330
|
+
const outputInfo = await lstat(outputPath).catch(() => undefined);
|
|
331
|
+
if (!outputInfo?.isFile() || outputInfo.isSymbolicLink()) {
|
|
332
|
+
throw new CliError("Native stage output must be an existing regular non-symlink file.", {
|
|
333
|
+
code: "RESEARCH_NATIVE_STAGE_OUTPUT_INVALID",
|
|
334
|
+
exitCode: 2,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
if (outputInfo.size > session.packet.limits.maxOutputBytes) {
|
|
338
|
+
throw new CliError("Native stage output exceeds the reviewed byte limit.", {
|
|
339
|
+
code: "RESEARCH_NATIVE_STAGE_OUTPUT_INVALID",
|
|
340
|
+
exitCode: 3,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
const raw = await readFile(outputPath, "utf8");
|
|
344
|
+
const config = await loadWorkspaceConfig(input.root);
|
|
345
|
+
assertExecutionConfiguration(config);
|
|
346
|
+
if (input.confirmedModel !== session.packet.expectedModel) {
|
|
347
|
+
throw new CliError("The confirmed native model does not match the reviewed route.", {
|
|
348
|
+
code: "RESEARCH_NATIVE_MODEL_MISMATCH",
|
|
349
|
+
exitCode: 3,
|
|
350
|
+
details: { expectedModel: session.packet.expectedModel },
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
const project = await loadProject(input.root, input.projectId);
|
|
354
|
+
const workPackage = packageById(project, session.packet.packageId);
|
|
355
|
+
if (workPackage.status !== "running" || workPackage.executor !== "producer") {
|
|
356
|
+
throw new CliError("The bound native producer package is no longer running.", {
|
|
357
|
+
code: "RESEARCH_NATIVE_STAGE_SESSION_MISMATCH",
|
|
358
|
+
exitCode: 3,
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
await assertNativeStageBinding(input.root, project, session.packet);
|
|
362
|
+
try {
|
|
363
|
+
await materializeAndValidateStageOutput(input.root, project, session.capsuleProject, workPackage, raw, null);
|
|
364
|
+
const elapsed = Math.max(0.001, (Date.now() - Date.parse(session.packet.preparedAt)) / 1_000);
|
|
365
|
+
const outputTokens = Math.ceil(Buffer.byteLength(raw, "utf8") / RESEARCH_ESTIMATED_BYTES_PER_TOKEN);
|
|
366
|
+
const reservedTokens = config.budget.packageMaxTokens[session.packet.stage];
|
|
367
|
+
const result = {
|
|
368
|
+
exitCode: 0,
|
|
369
|
+
stdout: raw,
|
|
370
|
+
stderr: "",
|
|
371
|
+
tokens: reservedTokens,
|
|
372
|
+
inputTokens: Math.max(0, reservedTokens - outputTokens),
|
|
373
|
+
cachedInputTokens: 0,
|
|
374
|
+
outputTokens,
|
|
375
|
+
costUsd: roundMoney(reservedAgentPackageCost(config.producer, reservedTokens, config)),
|
|
376
|
+
wallSeconds: elapsed,
|
|
377
|
+
model: config.producer.model,
|
|
378
|
+
runtime: null,
|
|
379
|
+
};
|
|
380
|
+
assertActualPackageBudget(project, workPackage, config, result, config.budget.maxOutputTokens);
|
|
381
|
+
assertProjectedBudget(project, config, result);
|
|
382
|
+
if (workPackage.stage === "discover") {
|
|
383
|
+
await assertEvidenceCoverage(input.root, project, resolveContained(session.capsuleProject, "outputs/evidence.json"));
|
|
384
|
+
}
|
|
385
|
+
const outputs = await validateAndImportOutputs(input.root, project, workPackage, session.capsuleProject, config, null);
|
|
386
|
+
applyUsage(project, result);
|
|
387
|
+
workPackage.status = "complete";
|
|
388
|
+
workPackage.completedAt = new Date().toISOString();
|
|
389
|
+
workPackage.lastError = null;
|
|
390
|
+
workPackage.lastFailureKind = null;
|
|
391
|
+
workPackage.retryNotBefore = null;
|
|
392
|
+
refreshProject(project);
|
|
393
|
+
await saveProject(input.root, project);
|
|
394
|
+
await writeRunRecord(input.root, {
|
|
395
|
+
schemaVersion: 1,
|
|
396
|
+
runId: session.packet.sessionId,
|
|
397
|
+
projectId: project.id,
|
|
398
|
+
packageId: workPackage.id,
|
|
399
|
+
executor: config.producer.agent,
|
|
400
|
+
startedAt: session.packet.preparedAt,
|
|
401
|
+
completedAt: workPackage.completedAt,
|
|
402
|
+
exitCode: 0,
|
|
403
|
+
tokens: result.tokens,
|
|
404
|
+
inputTokens: result.inputTokens,
|
|
405
|
+
cachedInputTokens: result.cachedInputTokens,
|
|
406
|
+
outputTokens: result.outputTokens,
|
|
407
|
+
costUsd: result.costUsd,
|
|
408
|
+
wallSeconds: result.wallSeconds,
|
|
409
|
+
outputs,
|
|
410
|
+
stdoutSha256: sha256Text(raw),
|
|
411
|
+
stderrSha256: sha256Text(""),
|
|
412
|
+
failureKind: null,
|
|
413
|
+
failureDetails: null,
|
|
414
|
+
runtime: null,
|
|
415
|
+
accountingMode: "reserved-native-host",
|
|
416
|
+
});
|
|
417
|
+
const usage = { ...usageSlice(result), accountingMode: "reserved-native-host" };
|
|
418
|
+
await appendJournalEvent(workspacePaths(input.root).journal, "native.stage.completed", project.id, {
|
|
419
|
+
sessionId: session.packet.sessionId,
|
|
420
|
+
packetSha256: session.packet.packetSha256,
|
|
421
|
+
projectId: project.id,
|
|
422
|
+
packageId: workPackage.id,
|
|
423
|
+
stage: workPackage.stage,
|
|
424
|
+
outputs,
|
|
425
|
+
usage,
|
|
426
|
+
});
|
|
427
|
+
await rm(nativeStageSessionPath(input.root, project.id), { force: true });
|
|
428
|
+
await rm(session.capsuleRoot, { recursive: true, force: true });
|
|
429
|
+
return {
|
|
430
|
+
projectId: project.id,
|
|
431
|
+
packageId: workPackage.id,
|
|
432
|
+
stage: workPackage.stage,
|
|
433
|
+
status: "complete",
|
|
434
|
+
outputs,
|
|
435
|
+
usage,
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
catch (error) {
|
|
439
|
+
await appendJournalEvent(workspacePaths(input.root).journal, "native.stage.submit.rejected", input.projectId, {
|
|
440
|
+
sessionId: session.packet.sessionId,
|
|
441
|
+
packetSha256: session.packet.packetSha256,
|
|
442
|
+
error: bounded(sanitizeResearchText(error instanceof Error ? error.message : String(error)), 1_000),
|
|
443
|
+
});
|
|
444
|
+
throw error;
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
export async function abortNativeResearchStage(input) {
|
|
449
|
+
return withWorkspaceLock(input.root, "research.native-stage.abort", async () => {
|
|
450
|
+
const session = await readNativeStageSession(input.root, input.projectId);
|
|
451
|
+
if (session.packet.sessionId !== input.sessionId) {
|
|
452
|
+
throw new CliError("Native stage session ID does not match the active session.", {
|
|
453
|
+
code: "RESEARCH_NATIVE_STAGE_SESSION_MISMATCH",
|
|
454
|
+
exitCode: 3,
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
const project = await loadProject(input.root, input.projectId);
|
|
458
|
+
const workPackage = packageById(project, session.packet.packageId);
|
|
459
|
+
if (workPackage.status !== "running") {
|
|
460
|
+
throw new CliError("The native stage package is not running.", {
|
|
461
|
+
code: "RESEARCH_NATIVE_STAGE_SESSION_MISMATCH",
|
|
462
|
+
exitCode: 3,
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
workPackage.status = workPackage.attempts < workPackage.maxAttempts ? "retry" : "failed";
|
|
466
|
+
workPackage.completedAt = new Date().toISOString();
|
|
467
|
+
workPackage.lastError = "Native stage was explicitly aborted before submission.";
|
|
468
|
+
workPackage.lastFailureKind = "deterministic";
|
|
469
|
+
workPackage.retryNotBefore = null;
|
|
470
|
+
refreshProject(project);
|
|
471
|
+
await saveProject(input.root, project);
|
|
472
|
+
await appendJournalEvent(workspacePaths(input.root).journal, "native.stage.aborted", project.id, {
|
|
473
|
+
sessionId: session.packet.sessionId,
|
|
474
|
+
packetSha256: session.packet.packetSha256,
|
|
475
|
+
projectId: project.id,
|
|
476
|
+
packageId: workPackage.id,
|
|
477
|
+
stage: workPackage.stage,
|
|
478
|
+
});
|
|
479
|
+
await rm(nativeStageSessionPath(input.root, project.id), { force: true });
|
|
480
|
+
await rm(session.capsuleRoot, { recursive: true, force: true });
|
|
481
|
+
return {
|
|
482
|
+
projectId: project.id,
|
|
483
|
+
packageId: workPackage.id,
|
|
484
|
+
status: project.status === "blocked" ? "blocked" : "ready",
|
|
485
|
+
};
|
|
486
|
+
});
|
|
487
|
+
}
|
|
81
488
|
async function executeWorkPackage(root, projectId, packageId, config, options, requestId, packageExecutor, doctorAttestation) {
|
|
82
489
|
const project = await loadProject(root, projectId);
|
|
83
490
|
const workPackage = packageById(project, packageId);
|
|
@@ -523,11 +930,13 @@ async function writeReviewEvidenceContext(root, projectId, capsuleProject, input
|
|
|
523
930
|
"TIANGONG REVIEW EVIDENCE CONTEXT v1",
|
|
524
931
|
"The following are deterministic excerpts from hash-verified bounded views. Full objects and original bounded contexts remain bound in the review packet.",
|
|
525
932
|
].join("\n");
|
|
933
|
+
const brokerReferences = await loadBrokerReviewReferences(capsuleProject);
|
|
526
934
|
const views = [
|
|
527
935
|
{
|
|
528
936
|
prefix: "--- LOCAL INPUT CONTEXT BUNDLE ---\n--- BEGIN BOUNDED REVIEW EXCERPT ---\n",
|
|
529
|
-
content: inputContextBundle.trimEnd(),
|
|
937
|
+
content: sanitizeResearchText(inputContextBundle.trimEnd()),
|
|
530
938
|
suffix: "\n--- END BOUNDED REVIEW EXCERPT ---",
|
|
939
|
+
active: true,
|
|
531
940
|
},
|
|
532
941
|
];
|
|
533
942
|
const seen = new Set();
|
|
@@ -536,9 +945,10 @@ async function writeReviewEvidenceContext(root, projectId, capsuleProject, input
|
|
|
536
945
|
continue;
|
|
537
946
|
seen.add(receipt.contextLocator);
|
|
538
947
|
const metadata = reviewSafeReceipt(receipt);
|
|
539
|
-
const
|
|
540
|
-
|
|
541
|
-
|
|
948
|
+
const references = brokerReferences.get(receipt.attemptId) ?? [];
|
|
949
|
+
const content = references.length
|
|
950
|
+
? await citedBrokerReviewContent(capsuleProject, receipt, references)
|
|
951
|
+
: "[No admitted evidence source cites this receipt; its raw object and bounded context remain hash-bound in the review packet.]";
|
|
542
952
|
views.push({
|
|
543
953
|
prefix: [
|
|
544
954
|
`--- BROKER RECEIPT ${receipt.attemptId} ---`,
|
|
@@ -548,9 +958,13 @@ async function writeReviewEvidenceContext(root, projectId, capsuleProject, input
|
|
|
548
958
|
].join("\n"),
|
|
549
959
|
content: content.trimEnd(),
|
|
550
960
|
suffix: "\n--- END BOUNDED REVIEW EXCERPT ---",
|
|
961
|
+
active: references.length > 0,
|
|
551
962
|
});
|
|
552
963
|
}
|
|
553
|
-
const fixedContent = [
|
|
964
|
+
const fixedContent = [
|
|
965
|
+
header,
|
|
966
|
+
...views.map((view) => `${view.prefix}${view.active ? "" : view.content}${view.suffix}`),
|
|
967
|
+
].join("\n\n");
|
|
554
968
|
const fixedBytes = Buffer.byteLength(`${fixedContent}\n`, "utf8");
|
|
555
969
|
if (fixedBytes > maxBytes) {
|
|
556
970
|
throw new CliError("Review evidence metadata exceeds the configured context budget.", {
|
|
@@ -559,10 +973,11 @@ async function writeReviewEvidenceContext(root, projectId, capsuleProject, input
|
|
|
559
973
|
details: { fixedBytes, maxBytes, views: views.length },
|
|
560
974
|
});
|
|
561
975
|
}
|
|
562
|
-
const
|
|
976
|
+
const activeViews = views.filter((view) => view.active).length;
|
|
977
|
+
const contentBudgetPerView = Math.floor((maxBytes - fixedBytes) / activeViews);
|
|
563
978
|
const sections = [
|
|
564
979
|
header,
|
|
565
|
-
...views.map((view) => `${view.prefix}${boundedUtf8ReviewExcerpt(view.content, contentBudgetPerView)}${view.suffix}`),
|
|
980
|
+
...views.map((view) => `${view.prefix}${view.active ? boundedUtf8ReviewExcerpt(view.content, contentBudgetPerView) : view.content}${view.suffix}`),
|
|
566
981
|
];
|
|
567
982
|
const logicalPath = "inputs/review-evidence-context.txt";
|
|
568
983
|
const path = resolveContained(capsuleProject, logicalPath);
|
|
@@ -597,6 +1012,107 @@ async function writeReviewEvidenceContext(root, projectId, capsuleProject, input
|
|
|
597
1012
|
persistent: await fileRecord(persistentPath, persistentLogicalPath),
|
|
598
1013
|
};
|
|
599
1014
|
}
|
|
1015
|
+
async function loadBrokerReviewReferences(capsuleProject) {
|
|
1016
|
+
const evidencePath = resolveContained(capsuleProject, "outputs/evidence.json");
|
|
1017
|
+
if (!(await pathExists(evidencePath)))
|
|
1018
|
+
return new Map();
|
|
1019
|
+
const evidence = await readJsonFile(evidencePath, "Research evidence");
|
|
1020
|
+
const references = new Map();
|
|
1021
|
+
if (!Array.isArray(evidence.sources))
|
|
1022
|
+
return references;
|
|
1023
|
+
for (const source of evidence.sources) {
|
|
1024
|
+
if (!isObject(source) || !isObject(source.provenance))
|
|
1025
|
+
continue;
|
|
1026
|
+
if (source.provenance.kind !== "broker" || typeof source.provenance.id !== "string")
|
|
1027
|
+
continue;
|
|
1028
|
+
if (typeof source.id !== "string" || typeof source.title !== "string")
|
|
1029
|
+
continue;
|
|
1030
|
+
const current = references.get(source.provenance.id) ?? [];
|
|
1031
|
+
current.push({
|
|
1032
|
+
sourceId: source.id,
|
|
1033
|
+
title: source.title,
|
|
1034
|
+
jsonPointer: typeof source.jsonPointer === "string" ? source.jsonPointer : null,
|
|
1035
|
+
excerpt: typeof source.excerpt === "string" ? source.excerpt : null,
|
|
1036
|
+
});
|
|
1037
|
+
references.set(source.provenance.id, current);
|
|
1038
|
+
}
|
|
1039
|
+
for (const values of references.values()) {
|
|
1040
|
+
values.sort((left, right) => left.sourceId.localeCompare(right.sourceId));
|
|
1041
|
+
}
|
|
1042
|
+
return references;
|
|
1043
|
+
}
|
|
1044
|
+
async function citedBrokerReviewContent(capsuleProject, receipt, references) {
|
|
1045
|
+
let rawValue;
|
|
1046
|
+
if (jsonReviewContentType(receipt.contentType)) {
|
|
1047
|
+
try {
|
|
1048
|
+
rawValue = JSON.parse(await readFile(resolveContained(capsuleProject, receipt.locator), "utf8"));
|
|
1049
|
+
}
|
|
1050
|
+
catch {
|
|
1051
|
+
rawValue = undefined;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
let needsFallback = false;
|
|
1055
|
+
const sections = [
|
|
1056
|
+
"The following items are deterministic, sanitized projections selected from the hash-bound raw broker object by the JSON Pointers declared in admitted evidence.",
|
|
1057
|
+
];
|
|
1058
|
+
for (const reference of references) {
|
|
1059
|
+
const lines = [
|
|
1060
|
+
`--- CITED EVIDENCE SOURCE ${reference.sourceId} ---`,
|
|
1061
|
+
`title: ${reference.title}`,
|
|
1062
|
+
`jsonPointer: ${reference.jsonPointer ?? "unavailable"}`,
|
|
1063
|
+
];
|
|
1064
|
+
if (rawValue !== undefined && reference.jsonPointer !== null) {
|
|
1065
|
+
try {
|
|
1066
|
+
const selected = resolveReviewJsonPointer(rawValue, reference.jsonPointer);
|
|
1067
|
+
lines.push("exactItem:", JSON.stringify(selected, null, 2));
|
|
1068
|
+
}
|
|
1069
|
+
catch {
|
|
1070
|
+
needsFallback = true;
|
|
1071
|
+
lines.push("exactItem: [JSON Pointer did not resolve; bounded-context fallback follows.] ");
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
else {
|
|
1075
|
+
needsFallback = true;
|
|
1076
|
+
lines.push("exactItem: [Exact JSON item unavailable; bounded-context fallback follows.] ");
|
|
1077
|
+
}
|
|
1078
|
+
if (reference.excerpt !== null)
|
|
1079
|
+
lines.push(`declaredExcerpt: ${reference.excerpt}`);
|
|
1080
|
+
sections.push(lines.join("\n"));
|
|
1081
|
+
}
|
|
1082
|
+
if (needsFallback) {
|
|
1083
|
+
const fallback = reviewableTextContentType(receipt.contentType)
|
|
1084
|
+
? await readFile(resolveContained(capsuleProject, receipt.contextLocator), "utf8")
|
|
1085
|
+
: "[Binary bounded view omitted from model context; verify the bound file mechanically.]";
|
|
1086
|
+
sections.push(`--- BOUNDED CONTEXT FALLBACK ---\n${fallback.trimEnd()}`);
|
|
1087
|
+
}
|
|
1088
|
+
return sanitizeResearchText(sections.join("\n\n"));
|
|
1089
|
+
}
|
|
1090
|
+
function jsonReviewContentType(contentType) {
|
|
1091
|
+
return /^application\/(?:[^;]+\+)?json(?:;|$)/i.test(contentType);
|
|
1092
|
+
}
|
|
1093
|
+
function resolveReviewJsonPointer(value, pointer) {
|
|
1094
|
+
if (pointer === "")
|
|
1095
|
+
return value;
|
|
1096
|
+
if (!pointer.startsWith("/") || /~(?:[^01]|$)/.test(pointer)) {
|
|
1097
|
+
throw new Error("invalid JSON Pointer");
|
|
1098
|
+
}
|
|
1099
|
+
let selected = value;
|
|
1100
|
+
for (const rawPart of pointer.slice(1).split("/")) {
|
|
1101
|
+
const part = rawPart.replaceAll("~1", "/").replaceAll("~0", "~");
|
|
1102
|
+
if (Array.isArray(selected) &&
|
|
1103
|
+
/^(0|[1-9][0-9]*)$/.test(part) &&
|
|
1104
|
+
Number(part) < selected.length) {
|
|
1105
|
+
selected = selected[Number(part)];
|
|
1106
|
+
}
|
|
1107
|
+
else if (isObject(selected) && Object.hasOwn(selected, part)) {
|
|
1108
|
+
selected = selected[part];
|
|
1109
|
+
}
|
|
1110
|
+
else {
|
|
1111
|
+
throw new Error("JSON Pointer does not resolve");
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
return selected;
|
|
1115
|
+
}
|
|
600
1116
|
function reviewableTextContentType(contentType) {
|
|
601
1117
|
return /^(?:text\/|application\/(?:[^;]+\+)?(?:json|xml|javascript|xhtml\+xml|csv))(?:;|$)/i.test(contentType);
|
|
602
1118
|
}
|
|
@@ -1051,8 +1567,8 @@ function computeEvidenceCoverage(project, sources, declared) {
|
|
|
1051
1567
|
mechanicalGaps: gaps,
|
|
1052
1568
|
};
|
|
1053
1569
|
}
|
|
1054
|
-
async function assertEvidenceCoverage(root, project) {
|
|
1055
|
-
const path = resolveContained(projectRoot(root, project.id), "outputs/evidence.json");
|
|
1570
|
+
async function assertEvidenceCoverage(root, project, evidencePath) {
|
|
1571
|
+
const path = evidencePath ?? resolveContained(projectRoot(root, project.id), "outputs/evidence.json");
|
|
1056
1572
|
const value = JSON.parse(await readFile(path, "utf8"));
|
|
1057
1573
|
const sources = value.sources;
|
|
1058
1574
|
const declared = value.coverage;
|
|
@@ -1443,6 +1959,10 @@ function remainingWallSeconds(project, config) {
|
|
|
1443
1959
|
return Math.max(1, Math.floor(config.budget.maxWallSeconds - project.usage.wallSeconds));
|
|
1444
1960
|
}
|
|
1445
1961
|
function assertExecutionConfiguration(config) {
|
|
1962
|
+
if (config.producer.executionMode !== "native-host" ||
|
|
1963
|
+
config.reviewer.executionMode !== "headless-cli") {
|
|
1964
|
+
throw new CliError("Research requires a native-host producer and a separate headless-CLI reviewer.", { code: "RESEARCH_EXECUTION_MODE_INVALID", exitCode: 3 });
|
|
1965
|
+
}
|
|
1446
1966
|
if (config.producer.agent === config.reviewer.agent) {
|
|
1447
1967
|
throw new CliError("Research producer and reviewer must use different agent families.", {
|
|
1448
1968
|
code: "RESEARCH_REVIEW_ROUTE_INVALID",
|
|
@@ -1687,6 +2207,8 @@ async function summarizeRun(root, requestId, cycles, executed, maxCycles, projec
|
|
|
1687
2207
|
}));
|
|
1688
2208
|
const unfinished = summaries.filter((project) => project.status !== "complete");
|
|
1689
2209
|
const hasReadyPackage = summaries.some((project) => project.readyPackage !== null);
|
|
2210
|
+
const nativeStageRequired = projects.some((project) => project.packages.some((workPackage) => workPackage.executor === "producer" &&
|
|
2211
|
+
(workPackage.status === "ready" || workPackage.status === "running")));
|
|
1690
2212
|
const status = summaries.length > 0 && summaries.every((project) => project.status === "complete")
|
|
1691
2213
|
? "complete"
|
|
1692
2214
|
: unfinished.length > 0 && unfinished.every((project) => project.status === "blocked")
|
|
@@ -1698,9 +2220,11 @@ async function summarizeRun(root, requestId, cycles, executed, maxCycles, projec
|
|
|
1698
2220
|
? "all-projects-complete"
|
|
1699
2221
|
: hasReadyPackage && cycles >= maxCycles
|
|
1700
2222
|
? "cycle-limit"
|
|
1701
|
-
:
|
|
1702
|
-
? "
|
|
1703
|
-
: "
|
|
2223
|
+
: nativeStageRequired
|
|
2224
|
+
? "native-stage-required"
|
|
2225
|
+
: status === "blocked"
|
|
2226
|
+
? "project-blocked"
|
|
2227
|
+
: "no-ready-work";
|
|
1704
2228
|
return {
|
|
1705
2229
|
workspace: root,
|
|
1706
2230
|
requestId,
|
|
@@ -1737,6 +2261,120 @@ function deterministicError(message) {
|
|
|
1737
2261
|
function projectRoot(root, projectId) {
|
|
1738
2262
|
return join(workspacePaths(root).projects, projectId);
|
|
1739
2263
|
}
|
|
2264
|
+
function nativeStageSessionPath(root, projectId) {
|
|
2265
|
+
return join(projectRoot(root, projectId), "native", "active.json");
|
|
2266
|
+
}
|
|
2267
|
+
async function readNativeStageSession(root, projectId) {
|
|
2268
|
+
const path = nativeStageSessionPath(root, projectId);
|
|
2269
|
+
const info = await lstat(path).catch(() => undefined);
|
|
2270
|
+
if (!info?.isFile() || info.isSymbolicLink()) {
|
|
2271
|
+
throw new CliError("No valid native stage session is active for this project.", {
|
|
2272
|
+
code: "RESEARCH_NATIVE_STAGE_SESSION_REQUIRED",
|
|
2273
|
+
exitCode: 3,
|
|
2274
|
+
});
|
|
2275
|
+
}
|
|
2276
|
+
const value = await readJsonFile(path, "Native research stage session");
|
|
2277
|
+
if (!isObject(value) ||
|
|
2278
|
+
value.schemaVersion !== 1 ||
|
|
2279
|
+
value.kind !== "tiangong-native-research-stage-session" ||
|
|
2280
|
+
!isObject(value.packet) ||
|
|
2281
|
+
typeof value.capsuleRoot !== "string" ||
|
|
2282
|
+
typeof value.capsuleProject !== "string" ||
|
|
2283
|
+
typeof value.sessionSha256 !== "string") {
|
|
2284
|
+
throw new CliError("Native stage session has an unsupported shape.", {
|
|
2285
|
+
code: "RESEARCH_NATIVE_STAGE_SESSION_INVALID",
|
|
2286
|
+
exitCode: 3,
|
|
2287
|
+
});
|
|
2288
|
+
}
|
|
2289
|
+
const { sessionSha256, ...core } = value;
|
|
2290
|
+
if (sha256Text(canonicalJson(core)) !== sessionSha256) {
|
|
2291
|
+
throw new CliError("Native stage session failed its content hash.", {
|
|
2292
|
+
code: "RESEARCH_NATIVE_STAGE_SESSION_INVALID",
|
|
2293
|
+
exitCode: 3,
|
|
2294
|
+
});
|
|
2295
|
+
}
|
|
2296
|
+
const packet = value.packet;
|
|
2297
|
+
const { packetSha256, ...packetCore } = packet;
|
|
2298
|
+
if (packet.schemaVersion !== 1 ||
|
|
2299
|
+
packet.kind !== "tiangong-native-research-stage" ||
|
|
2300
|
+
packet.projectId !== projectId ||
|
|
2301
|
+
typeof packetSha256 !== "string" ||
|
|
2302
|
+
sha256Text(canonicalJson(packetCore)) !== packetSha256) {
|
|
2303
|
+
throw new CliError("Native stage packet failed its content hash or project binding.", {
|
|
2304
|
+
code: "RESEARCH_NATIVE_STAGE_SESSION_INVALID",
|
|
2305
|
+
exitCode: 3,
|
|
2306
|
+
});
|
|
2307
|
+
}
|
|
2308
|
+
const runtimeRoot = resolve(workspacePaths(root).runtime);
|
|
2309
|
+
const capsuleRoot = resolve(value.capsuleRoot);
|
|
2310
|
+
const capsuleProject = resolve(value.capsuleProject);
|
|
2311
|
+
if (relative(runtimeRoot, capsuleRoot).startsWith("..") ||
|
|
2312
|
+
relative(capsuleRoot, capsuleProject).startsWith("..") ||
|
|
2313
|
+
!(await lstat(capsuleRoot).catch(() => undefined))?.isDirectory() ||
|
|
2314
|
+
!(await lstat(capsuleProject).catch(() => undefined))?.isDirectory()) {
|
|
2315
|
+
throw new CliError("Native stage capsule is missing or outside the workspace runtime.", {
|
|
2316
|
+
code: "RESEARCH_NATIVE_STAGE_SESSION_INVALID",
|
|
2317
|
+
exitCode: 3,
|
|
2318
|
+
});
|
|
2319
|
+
}
|
|
2320
|
+
return value;
|
|
2321
|
+
}
|
|
2322
|
+
async function nativeStageBinding(root, project, workPackage) {
|
|
2323
|
+
const paths = workspacePaths(root);
|
|
2324
|
+
const outputRoot = join(projectRoot(root, project.id), "outputs");
|
|
2325
|
+
const outputs = [];
|
|
2326
|
+
if (await pathExists(outputRoot)) {
|
|
2327
|
+
for (const path of await regularTreeFiles(outputRoot)) {
|
|
2328
|
+
outputs.push(await fileRecord(path, relative(projectRoot(root, project.id), path).replaceAll("\\", "/")));
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
return sha256Text(canonicalJson({
|
|
2332
|
+
projectId: project.id,
|
|
2333
|
+
questionSha256: sha256Text(project.question),
|
|
2334
|
+
evidenceRequirements: project.evidenceRequirements,
|
|
2335
|
+
inputs: project.inputs.map((record) => ({
|
|
2336
|
+
id: record.id,
|
|
2337
|
+
role: record.role,
|
|
2338
|
+
sha256: record.sha256,
|
|
2339
|
+
bytes: record.bytes,
|
|
2340
|
+
contextSha256: record.contextSha256 ?? null,
|
|
2341
|
+
contextBytes: record.contextBytes ?? null,
|
|
2342
|
+
})),
|
|
2343
|
+
package: {
|
|
2344
|
+
id: workPackage.id,
|
|
2345
|
+
stage: workPackage.stage,
|
|
2346
|
+
dependencies: workPackage.dependencies,
|
|
2347
|
+
expectedOutputs: workPackage.expectedOutputs,
|
|
2348
|
+
},
|
|
2349
|
+
outputs,
|
|
2350
|
+
configSha256: await sha256File(paths.config),
|
|
2351
|
+
runtimeLockSha256: await sha256File(paths.runtimeLock),
|
|
2352
|
+
capabilityDeclarationsSha256: await sha256File(paths.capabilityDeclarations),
|
|
2353
|
+
capabilityLockSha256: (await pathExists(paths.capabilityLock))
|
|
2354
|
+
? await sha256File(paths.capabilityLock)
|
|
2355
|
+
: null,
|
|
2356
|
+
}));
|
|
2357
|
+
}
|
|
2358
|
+
async function assertNativeStageBinding(root, project, packet) {
|
|
2359
|
+
const workPackage = packageById(project, packet.packageId);
|
|
2360
|
+
const actual = await nativeStageBinding(root, project, workPackage);
|
|
2361
|
+
if (actual !== packet.bindingSha256) {
|
|
2362
|
+
throw new CliError("Native stage inputs, configuration, or admitted outputs drifted.", {
|
|
2363
|
+
code: "RESEARCH_NATIVE_STAGE_BINDING_DRIFT",
|
|
2364
|
+
exitCode: 3,
|
|
2365
|
+
details: { expectedSha256: packet.bindingSha256, actualSha256: actual },
|
|
2366
|
+
});
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
function requireNativeOutputPath(value) {
|
|
2370
|
+
if (!value || !isAbsolute(value) || /[\0\r\n]/.test(value)) {
|
|
2371
|
+
throw new CliError("Native stage output requires an explicit absolute file path.", {
|
|
2372
|
+
code: "RESEARCH_NATIVE_STAGE_OUTPUT_INVALID",
|
|
2373
|
+
exitCode: 2,
|
|
2374
|
+
});
|
|
2375
|
+
}
|
|
2376
|
+
return resolve(value);
|
|
2377
|
+
}
|
|
1740
2378
|
function validateRunOptions(options) {
|
|
1741
2379
|
if (!Number.isInteger(options.maxParallel) ||
|
|
1742
2380
|
options.maxParallel < 1 ||
|