@fitlab-ai/agent-infra 0.9.6 → 0.9.7
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/dist/bin/internal-cli.js +5 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +45 -4
- package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +162 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +244 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +370 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +21 -5
- package/dist/lib/defaults.json +10 -2
- package/dist/lib/git/workflow.js +30 -3
- package/dist/lib/internal/codex-lifecycle.js +69 -1
- package/dist/lib/internal/codex-sandbox-controller.js +106 -0
- package/dist/lib/internal/task-orchestration.js +37 -5
- package/dist/lib/platform/context.js +6 -4
- package/dist/lib/platform/github-client.js +55 -2
- package/dist/lib/process-data/checkpoint.js +319 -0
- package/dist/lib/process-data/index.js +128 -60
- package/dist/lib/process-data/materialize.js +84 -0
- package/dist/lib/process-data/normalize.js +34 -1
- package/dist/lib/process-data/reconcile.js +55 -1
- package/dist/lib/process-data/sources.js +422 -1
- package/dist/lib/process-data/store.js +341 -11
- package/dist/lib/sandbox/commands/create.js +103 -51
- package/dist/lib/sandbox/commands/rm.js +64 -15
- package/dist/lib/sandbox/commands/start.js +4 -3
- package/dist/lib/sandbox/control/container-identity.js +64 -0
- package/dist/lib/sandbox/control/executor.js +13 -7
- package/dist/lib/sandbox/control/lifecycle.js +238 -29
- package/dist/lib/sandbox/control/protocol.js +7 -0
- package/dist/lib/sandbox/control/server.js +202 -26
- package/dist/lib/sandbox/control/state.js +25 -15
- package/dist/lib/sandbox/control/timing.js +8 -0
- package/dist/lib/sandbox/mounts.js +5 -13
- package/dist/lib/sandbox/recovery.js +59 -42
- package/dist/lib/sandbox/workspace-view.js +57 -40
- package/dist/lib/server/daemon.js +1 -1
- package/dist/lib/server/process-control.js +11 -1
- package/dist/lib/server/process-state.js +57 -20
- package/dist/lib/task/artifact-lifecycle.js +82 -11
- package/dist/lib/task/artifact-receipts.js +128 -0
- package/dist/lib/task/codex-orchestration.js +81 -6
- package/dist/lib/task/commands/validate.js +1 -1
- package/dist/lib/task/create.js +10 -2
- package/dist/lib/task/delegation-receipts.js +84 -2
- package/dist/lib/task/events.js +139 -2
- package/dist/lib/task/ledger.js +13 -1
- package/dist/lib/task/orchestration.js +235 -14
- package/dist/lib/task/sections.js +5 -1
- package/dist/lib/task/task-execution-lock.js +5 -4
- package/dist/lib/task/verification-engine.js +40 -8
- package/dist/lifecycle-build-manifest.json +6 -0
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +60 -3
- package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +227 -0
- package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +321 -0
- package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +26 -0
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +453 -0
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +33 -4
- package/lib/defaults.json +10 -2
- package/lib/git/workflow.ts +25 -2
- package/lib/internal/codex-lifecycle.ts +68 -1
- package/lib/internal/codex-sandbox-controller.ts +109 -0
- package/lib/internal/task-orchestration.ts +31 -4
- package/lib/platform/context.ts +8 -5
- package/lib/platform/github-client.ts +61 -3
- package/lib/process-data/checkpoint.ts +335 -0
- package/lib/process-data/index.ts +112 -60
- package/lib/process-data/materialize.ts +83 -0
- package/lib/process-data/normalize.ts +29 -1
- package/lib/process-data/reconcile.ts +64 -2
- package/lib/process-data/sources.ts +449 -2
- package/lib/process-data/store.ts +304 -11
- package/lib/process-data/types.ts +124 -2
- package/lib/sandbox/commands/create.ts +56 -7
- package/lib/sandbox/commands/rm.ts +64 -14
- package/lib/sandbox/commands/start.ts +4 -3
- package/lib/sandbox/control/container-identity.ts +93 -0
- package/lib/sandbox/control/executor.ts +14 -10
- package/lib/sandbox/control/lifecycle.ts +264 -33
- package/lib/sandbox/control/protocol.ts +31 -5
- package/lib/sandbox/control/server.ts +184 -24
- package/lib/sandbox/control/state.ts +34 -17
- package/lib/sandbox/control/timing.ts +12 -0
- package/lib/sandbox/mounts.ts +5 -15
- package/lib/sandbox/recovery.ts +71 -42
- package/lib/sandbox/workspace-view.ts +61 -40
- package/lib/server/daemon.ts +1 -1
- package/lib/server/process-control.ts +13 -3
- package/lib/server/process-state.ts +82 -27
- package/lib/task/artifact-lifecycle.ts +73 -10
- package/lib/task/artifact-receipts.ts +161 -0
- package/lib/task/codex-orchestration.ts +91 -5
- package/lib/task/commands/validate.ts +1 -1
- package/lib/task/create.ts +11 -2
- package/lib/task/delegation-receipts.ts +160 -8
- package/lib/task/events.ts +122 -1
- package/lib/task/ledger.ts +16 -1
- package/lib/task/orchestration.ts +356 -17
- package/lib/task/sections.ts +6 -2
- package/lib/task/task-execution-lock.ts +11 -9
- package/lib/task/verification-engine.ts +51 -16
- package/package.json +1 -1
- package/templates/.agents/README.en.md +7 -0
- package/templates/.agents/README.zh-CN.md +5 -0
- package/templates/.agents/hooks/lifecycle-delegation.js +25 -1
- package/templates/.agents/rules/lifecycle-orchestration.en.md +8 -2
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +8 -2
- package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
- package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +1 -1
- package/templates/.agents/rules/review-handshake.en.md +3 -1
- package/templates/.agents/rules/review-handshake.zh-CN.md +3 -1
- package/templates/.agents/skills/code-task/reference/dual-mode.en.md +3 -1
- package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +3 -1
- package/templates/.agents/skills/complete-task/SKILL.en.md +4 -0
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +4 -0
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +6 -6
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +6 -6
- package/templates/.agents/skills/run-manual-validation/config/verify.en.json +14 -3
- package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +14 -3
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +67 -0
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +67 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +25 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +25 -0
- package/templates/.agents/skills/run-task/SKILL.en.md +9 -7
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +9 -7
- package/templates/.agents/skills/run-task/reference/host-validation.en.md +2 -0
- package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +2 -0
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +75 -3
- package/templates/.agents/workspace/README.en.md +0 -26
- package/templates/.agents/workspace/README.zh-CN.md +0 -26
|
@@ -7,6 +7,7 @@ import { locateActivityLog } from './activity-log.ts';
|
|
|
7
7
|
import { parseImplementationInputs, selectPendingImplementationInput } from './implementation-inputs.ts';
|
|
8
8
|
import { parseVerdict } from './review-artifacts.ts';
|
|
9
9
|
import { extractSection, findSectionHeading } from './sections.ts';
|
|
10
|
+
import { receiptForOutput, sha256File } from './artifact-receipts.ts';
|
|
10
11
|
|
|
11
12
|
const artifactFamilyCatalog = [
|
|
12
13
|
{ family: 'analysis', sectionAliases: ['分析', 'Analysis'], heading: '分析', labels: ['需求分析报告', 'Requirements Analysis'] },
|
|
@@ -218,11 +219,7 @@ function resolveReviewedInput(
|
|
|
218
219
|
diagnostics.push(diagnostic('BROKEN_REFERENCE', review.family, review.name, String(error)));
|
|
219
220
|
return null;
|
|
220
221
|
}
|
|
221
|
-
const
|
|
222
|
-
const header = lines.findIndex((line) => /\*\*(?:审查输入|Review Input)\*\*[::]/.test(line));
|
|
223
|
-
const referenceBlock = header >= 0 ? lines.slice(header, header + 12).join('\n') : '';
|
|
224
|
-
const candidates = [...referenceBlock.matchAll(/`([^`]+\.md)`/g)].map((match) => match[1]!);
|
|
225
|
-
const parsed = candidates.map((name) => parseArtifactName(name)).find((item) => item?.family === expectedFamily);
|
|
222
|
+
const parsed = parseReviewedInputReference(content, expectedFamily);
|
|
226
223
|
if (!parsed) {
|
|
227
224
|
diagnostics.push(diagnostic('BROKEN_REFERENCE', review.family, review.name, `${review.name} does not reference a canonical ${expectedFamily} artifact`));
|
|
228
225
|
return null;
|
|
@@ -231,9 +228,16 @@ function resolveReviewedInput(
|
|
|
231
228
|
try {
|
|
232
229
|
const stat = fs.lstatSync(abs);
|
|
233
230
|
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('referenced input is not a regular file');
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
231
|
+
const taskContent = fs.readFileSync(path.join(taskDir, 'task.md'), 'utf8');
|
|
232
|
+
const receipt = receiptForOutput(taskContent, review.name);
|
|
233
|
+
if (!receipt) throw new Error(`receipt for ${review.name} is missing`);
|
|
234
|
+
const expectedEvent = reviewEventName(review.family);
|
|
235
|
+
if (receipt.event !== expectedEvent || receipt.input !== parsed.name) {
|
|
236
|
+
throw new Error(`receipt for ${review.name} does not match ${parsed.name}`);
|
|
237
|
+
}
|
|
238
|
+
const actualSha256 = sha256File(abs);
|
|
239
|
+
if (actualSha256 !== receipt.inputSha256) {
|
|
240
|
+
throw new Error(`${parsed.name} content digest does not match receipt`);
|
|
237
241
|
}
|
|
238
242
|
return { ...parsed, path: abs, size: stat.size, mtimeMs: stat.mtimeMs };
|
|
239
243
|
} catch (error) {
|
|
@@ -242,6 +246,54 @@ function resolveReviewedInput(
|
|
|
242
246
|
}
|
|
243
247
|
}
|
|
244
248
|
|
|
249
|
+
function parseReviewedInputReference(content: string, expectedFamily: ArtifactFamily): { family: ArtifactFamily; round: number; name: string } | null {
|
|
250
|
+
const lines = content.split(/\r?\n/);
|
|
251
|
+
const header = lines.findIndex((line) => /\*\*(?:审查输入|Review Input)\*\*[::]/.test(line));
|
|
252
|
+
const referenceBlock = header >= 0 ? lines.slice(header, header + 12).join('\n') : '';
|
|
253
|
+
const candidates = [...referenceBlock.matchAll(/`([^`]+\.md)`/g)].map((match) => match[1]!);
|
|
254
|
+
return candidates
|
|
255
|
+
.map((name) => parseArtifactName(name))
|
|
256
|
+
.find((item) => item?.family === expectedFamily) ?? null;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function parseCodePlanInputReference(content: string): { family: 'plan'; round: number; name: string } | null {
|
|
260
|
+
const lines = content.split(/\r?\n/);
|
|
261
|
+
const sectionStart = lines.findIndex((line) => /^##\s+(?:实现输入|Implementation Input)\s*$/.test(line.trim()));
|
|
262
|
+
if (sectionStart < 0) return null;
|
|
263
|
+
const bodyLines = lines.slice(sectionStart + 1);
|
|
264
|
+
const nextSection = bodyLines.findIndex((line) => /^##\s+/.test(line.trim()));
|
|
265
|
+
const body = (nextSection < 0 ? bodyLines : bodyLines.slice(0, nextSection)).join('\n');
|
|
266
|
+
const match = body.match(/\*\*(?:方案输入|Plan Input)\*\*[::]\s*`([^`]+\.md)`/);
|
|
267
|
+
if (!match) return null;
|
|
268
|
+
const parsed = parseArtifactName(match[1]!);
|
|
269
|
+
return parsed?.family === 'plan' ? { ...parsed, family: 'plan' } : null;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function reviewEventName(family: ArtifactFamily): 'review-analysis.completed' | 'review-plan.completed' | 'review-code.completed' {
|
|
273
|
+
if (family === 'review-analysis') return 'review-analysis.completed';
|
|
274
|
+
if (family === 'review-plan') return 'review-plan.completed';
|
|
275
|
+
if (family === 'review-code') return 'review-code.completed';
|
|
276
|
+
throw new Error(`unsupported review family '${family}'`);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function resolveCodeInputReceipt(taskDir: string, code: ArtifactIdentity): { input: ArtifactIdentity; inputSha256: string } | null {
|
|
280
|
+
let taskContent: string;
|
|
281
|
+
try { taskContent = fs.readFileSync(path.join(taskDir, 'task.md'), 'utf8'); }
|
|
282
|
+
catch { return null; }
|
|
283
|
+
let receipt;
|
|
284
|
+
try { receipt = receiptForOutput(taskContent, code.name); }
|
|
285
|
+
catch { return null; }
|
|
286
|
+
if (!receipt || receipt.event !== 'code.completed') return null;
|
|
287
|
+
const plan = inspectArtifactDirectory(taskDir, 'plan');
|
|
288
|
+
if (plan.status !== 'ready') return null;
|
|
289
|
+
const input = plan.artifacts.find((artifact) => artifact.name === receipt.input);
|
|
290
|
+
return input ? { input, inputSha256: receipt.inputSha256 } : null;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function resolveCodePlanInput(taskDir: string, code: ArtifactIdentity): { input: ArtifactIdentity; inputSha256: string } | null {
|
|
294
|
+
return resolveCodeInputReceipt(taskDir, code);
|
|
295
|
+
}
|
|
296
|
+
|
|
245
297
|
function assertWritableInventory(inventory: ArtifactInventoryResult): ArtifactError | null {
|
|
246
298
|
if (inventory.status === 'failed') return inventory.error;
|
|
247
299
|
const blockers = inventory.diagnostics.filter((item) => BLOCKING_DIAGNOSTICS.has(item.code));
|
|
@@ -312,11 +364,21 @@ function resolveCodeContext(inventory: ArtifactInventoryResult, options: Inspect
|
|
|
312
364
|
return withCodeMode(inventory, inputs, 'ready', 'init', codeMax, reviewMax, null, null,
|
|
313
365
|
'No prior code artifact. Starting initial implementation (round 1 -> code.md).');
|
|
314
366
|
}
|
|
367
|
+
const codePlanInput = resolveCodePlanInput(inventory.taskDir!, latestCode);
|
|
368
|
+
if (!codePlanInput) {
|
|
369
|
+
return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', `code completion receipt for ${latestCode.name} is missing or does not match the current plan`, latestCode.name);
|
|
370
|
+
}
|
|
371
|
+
if (reviewPlan.latest && !reviewPlan.reviewedInput) {
|
|
372
|
+
return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', `${reviewPlan.latest.name} has no valid reviewed input`, reviewPlan.latest.name);
|
|
373
|
+
}
|
|
315
374
|
if (reviewPlan.latest && reviewPlan.reviewedInput?.name === plan.latest.name) {
|
|
316
375
|
const verdict = parseVerdict(reviewPlan.latest.path);
|
|
317
|
-
|
|
376
|
+
const reviewedPlanSha256 = sha256File(reviewPlan.reviewedInput.path);
|
|
377
|
+
if (verdict.ok && verdict.verdict === 'Approved' && (
|
|
378
|
+
codePlanInput.input.name !== plan.latest.name || codePlanInput.inputSha256 !== reviewedPlanSha256
|
|
379
|
+
)) {
|
|
318
380
|
return withCodeMode(inventory, inputs, 'ready', 'init', codeMax, reviewMax, verdict.verdict, reviewPlan.latest.name,
|
|
319
|
-
`Latest ${reviewPlan.latest.name}
|
|
381
|
+
`Latest ${reviewPlan.latest.name} approves plan content not captured by the latest code input receipt. Entering replan-driven init.`);
|
|
320
382
|
}
|
|
321
383
|
}
|
|
322
384
|
if (reviewMax < codeMax) {
|
|
@@ -440,6 +502,7 @@ function buildArtifactLinkSection(content: string, artifact: ArtifactIdentity):
|
|
|
440
502
|
export {
|
|
441
503
|
artifactFamilyCatalog, artifactName, parseArtifactName, familySpec,
|
|
442
504
|
inspectTaskArtifacts, inspectArtifactDirectory, resolveArtifactContext,
|
|
505
|
+
parseReviewedInputReference, parseCodePlanInputReference, resolveCodePlanInput, reviewEventName,
|
|
443
506
|
assertWritableInventory, validateCompletedArtifact, buildArtifactLinkSection
|
|
444
507
|
};
|
|
445
508
|
export type {
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
|
|
4
|
+
import { extractSection, findSectionHeading, parseTable } from './sections.ts';
|
|
5
|
+
|
|
6
|
+
const RECEIPT_SECTION_ALIASES = ['产物生命周期收据', 'Artifact Lifecycle Receipts'] as const;
|
|
7
|
+
const RECEIPT_COLUMNS = ['event', 'output', 'input', 'input_sha256', 'completed_at'] as const;
|
|
8
|
+
const RECEIPT_EVENTS = new Set([
|
|
9
|
+
'review-analysis.completed',
|
|
10
|
+
'review-plan.completed',
|
|
11
|
+
'review-code.completed',
|
|
12
|
+
'code.completed'
|
|
13
|
+
]);
|
|
14
|
+
const SHA256_RE = /^[a-f0-9]{64}$/;
|
|
15
|
+
const ARTIFACT_NAME_RE = /^(analysis|review-analysis|plan|review-plan|code|review-code|manual-validation|validation-run|pr-review)(?:-r([2-9]|[1-9]\d+))?\.md$/;
|
|
16
|
+
const RECEIPT_SHAPES = {
|
|
17
|
+
'review-analysis.completed': { output: 'review-analysis', input: 'analysis' },
|
|
18
|
+
'review-plan.completed': { output: 'review-plan', input: 'plan' },
|
|
19
|
+
'review-code.completed': { output: 'review-code', input: 'code' },
|
|
20
|
+
'code.completed': { output: 'code', input: 'plan' }
|
|
21
|
+
} as const;
|
|
22
|
+
|
|
23
|
+
type ArtifactReceiptEvent =
|
|
24
|
+
| 'review-analysis.completed'
|
|
25
|
+
| 'review-plan.completed'
|
|
26
|
+
| 'review-code.completed'
|
|
27
|
+
| 'code.completed';
|
|
28
|
+
type ArtifactReceipt = {
|
|
29
|
+
event: ArtifactReceiptEvent;
|
|
30
|
+
output: string;
|
|
31
|
+
input: string;
|
|
32
|
+
inputSha256: string;
|
|
33
|
+
completedAt: string;
|
|
34
|
+
};
|
|
35
|
+
type ArtifactReceiptParseResult = {
|
|
36
|
+
present: boolean;
|
|
37
|
+
rows: readonly ArtifactReceipt[];
|
|
38
|
+
};
|
|
39
|
+
type ReceiptSectionMutation = {
|
|
40
|
+
aliases: readonly string[];
|
|
41
|
+
heading: string;
|
|
42
|
+
body: string;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
class ArtifactReceiptError extends Error {
|
|
46
|
+
readonly code = 'ARTIFACT_RECEIPT_INVALID';
|
|
47
|
+
|
|
48
|
+
constructor(message: string) {
|
|
49
|
+
super(message);
|
|
50
|
+
this.name = 'ArtifactReceiptError';
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function sha256Bytes(value: Buffer): string {
|
|
55
|
+
return createHash('sha256').update(value).digest('hex');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function sha256File(filePath: string): string {
|
|
59
|
+
return sha256Bytes(fs.readFileSync(filePath));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function parseReceiptArtifactName(name: string): { family: string } | null {
|
|
63
|
+
const match = ARTIFACT_NAME_RE.exec(name);
|
|
64
|
+
if (!match) return null;
|
|
65
|
+
const roundText = match[2];
|
|
66
|
+
if (roundText !== undefined) {
|
|
67
|
+
const round = Number(roundText);
|
|
68
|
+
if (!Number.isSafeInteger(round) || String(round) !== roundText) return null;
|
|
69
|
+
}
|
|
70
|
+
return { family: match[1]! };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function validateReceiptShape(event: ArtifactReceiptEvent, output: string, input: string): void {
|
|
74
|
+
const outputIdentity = parseReceiptArtifactName(output);
|
|
75
|
+
const inputIdentity = parseReceiptArtifactName(input);
|
|
76
|
+
if (!outputIdentity || !inputIdentity) throw new ArtifactReceiptError(`receipt artifact identity is invalid: ${output} -> ${input}`);
|
|
77
|
+
const shape = RECEIPT_SHAPES[event];
|
|
78
|
+
if (outputIdentity.family !== shape.output || inputIdentity.family !== shape.input) {
|
|
79
|
+
throw new ArtifactReceiptError(`receipt event '${event}' does not match ${output} -> ${input}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function parseArtifactReceipts(content: string): ArtifactReceiptParseResult {
|
|
84
|
+
const section = extractSection(content, [...RECEIPT_SECTION_ALIASES]);
|
|
85
|
+
if (!section) return { present: false, rows: [] };
|
|
86
|
+
|
|
87
|
+
let table;
|
|
88
|
+
try {
|
|
89
|
+
table = parseTable(content, {
|
|
90
|
+
sectionAliases: [...RECEIPT_SECTION_ALIASES],
|
|
91
|
+
columns: [...RECEIPT_COLUMNS],
|
|
92
|
+
keyColumn: 'output'
|
|
93
|
+
});
|
|
94
|
+
} catch (error) {
|
|
95
|
+
throw new ArtifactReceiptError(error instanceof Error ? error.message : String(error));
|
|
96
|
+
}
|
|
97
|
+
if (!table) throw new ArtifactReceiptError('receipt section has no receipt table');
|
|
98
|
+
|
|
99
|
+
const rows = table.rows.map((row) => {
|
|
100
|
+
const event = row.values.event ?? '';
|
|
101
|
+
const output = row.values.output ?? '';
|
|
102
|
+
const input = row.values.input ?? '';
|
|
103
|
+
const inputSha256 = row.values.input_sha256 ?? '';
|
|
104
|
+
const completedAt = row.values.completed_at ?? '';
|
|
105
|
+
if (!RECEIPT_EVENTS.has(event)) throw new ArtifactReceiptError(`unknown receipt event '${event}'`);
|
|
106
|
+
if (!output || !input) throw new ArtifactReceiptError('receipt output and input are required');
|
|
107
|
+
validateReceiptShape(event as ArtifactReceiptEvent, output, input);
|
|
108
|
+
if (!SHA256_RE.test(inputSha256)) throw new ArtifactReceiptError(`receipt digest for '${output}' is invalid`);
|
|
109
|
+
if (!completedAt || Number.isNaN(Date.parse(completedAt.replace(' ', 'T')))) {
|
|
110
|
+
throw new ArtifactReceiptError(`receipt completion time for '${output}' is invalid`);
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
event: event as ArtifactReceiptEvent,
|
|
114
|
+
output,
|
|
115
|
+
input,
|
|
116
|
+
inputSha256,
|
|
117
|
+
completedAt
|
|
118
|
+
};
|
|
119
|
+
});
|
|
120
|
+
return { present: true, rows };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function receiptForOutput(content: string, output: string): ArtifactReceipt | null {
|
|
124
|
+
const parsed = parseArtifactReceipts(content);
|
|
125
|
+
return parsed.rows.find((row) => row.output === output) ?? null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function upsertArtifactReceipt(content: string, receipt: ArtifactReceipt): ReceiptSectionMutation {
|
|
129
|
+
if (!RECEIPT_EVENTS.has(receipt.event)) throw new ArtifactReceiptError(`unknown receipt event '${receipt.event}'`);
|
|
130
|
+
if (!receipt.output || !receipt.input) throw new ArtifactReceiptError('receipt output and input are required');
|
|
131
|
+
validateReceiptShape(receipt.event, receipt.output, receipt.input);
|
|
132
|
+
if (!SHA256_RE.test(receipt.inputSha256)) throw new ArtifactReceiptError(`receipt digest for '${receipt.output}' is invalid`);
|
|
133
|
+
if (!receipt.completedAt || Number.isNaN(Date.parse(receipt.completedAt.replace(' ', 'T')))) {
|
|
134
|
+
throw new ArtifactReceiptError(`receipt completion time for '${receipt.output}' is invalid`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const existing = parseArtifactReceipts(content).rows;
|
|
138
|
+
const previous = existing.find((row) => row.output === receipt.output);
|
|
139
|
+
if (previous && JSON.stringify(previous) !== JSON.stringify(receipt)) {
|
|
140
|
+
throw new ArtifactReceiptError(`receipt for '${receipt.output}' already exists with different evidence`);
|
|
141
|
+
}
|
|
142
|
+
const rows = previous ? existing : [...existing, receipt];
|
|
143
|
+
const heading = findSectionHeading(content, [...RECEIPT_SECTION_ALIASES]);
|
|
144
|
+
const body = [
|
|
145
|
+
`| ${RECEIPT_COLUMNS.join(' | ')} |`,
|
|
146
|
+
`| ${RECEIPT_COLUMNS.map(() => '---').join(' | ')} |`,
|
|
147
|
+
...rows.map((row) => `| ${row.event} | ${row.output} | ${row.input} | ${row.inputSha256} | ${row.completedAt} |`)
|
|
148
|
+
].join('\n');
|
|
149
|
+
return { aliases: [...RECEIPT_SECTION_ALIASES], heading, body };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export {
|
|
153
|
+
ArtifactReceiptError,
|
|
154
|
+
RECEIPT_SECTION_ALIASES,
|
|
155
|
+
parseArtifactReceipts,
|
|
156
|
+
receiptForOutput,
|
|
157
|
+
sha256Bytes,
|
|
158
|
+
sha256File,
|
|
159
|
+
upsertArtifactReceipt
|
|
160
|
+
};
|
|
161
|
+
export type { ArtifactReceipt, ArtifactReceiptEvent, ArtifactReceiptParseResult, ReceiptSectionMutation };
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
|
|
1
3
|
import {
|
|
2
4
|
preflightCodexLifecycleEvidence,
|
|
3
5
|
resolveCodexSpawnedChild,
|
|
@@ -5,9 +7,16 @@ import {
|
|
|
5
7
|
resolveCodexThread
|
|
6
8
|
} from '../agent-clients/adapters/codex-lifecycle/app-server.ts';
|
|
7
9
|
import { createCodexLifecycleStore } from '../agent-clients/adapters/codex-lifecycle/store.ts';
|
|
10
|
+
import { createCodexCapabilityStore } from '../agent-clients/adapters/codex-lifecycle/capability-store.ts';
|
|
11
|
+
import { computeLifecycleBuildIdentity } from '../agent-clients/adapters/codex-lifecycle/build-identity.ts';
|
|
12
|
+
import type { LifecycleBuildIdentity } from '../agent-clients/adapters/codex-lifecycle/build-identity.ts';
|
|
13
|
+
import { verifyCodexSandboxControllerContext } from '../agent-clients/adapters/codex-lifecycle/sandbox-controller.ts';
|
|
8
14
|
import type { AgentClientId } from '../agent-clients/types.ts';
|
|
15
|
+
import { resolveTaskRef } from './resolve-ref.ts';
|
|
9
16
|
import {
|
|
10
17
|
activateMatchingOrchestrationDelegation,
|
|
18
|
+
hasActivatableOrchestrationDelegation,
|
|
19
|
+
hasSealableOrchestrationDelegation,
|
|
11
20
|
pauseMatchingOrchestrationDelegation,
|
|
12
21
|
prepareOrchestrationDelegation,
|
|
13
22
|
reconcileMatchingOrchestrationDelegation,
|
|
@@ -16,12 +25,15 @@ import {
|
|
|
16
25
|
import type { OrchestrationOptions, OrchestrationResult } from './orchestration.ts';
|
|
17
26
|
|
|
18
27
|
type LifecycleStore = ReturnType<typeof createCodexLifecycleStore>;
|
|
28
|
+
type CapabilityStore = ReturnType<typeof createCodexCapabilityStore>;
|
|
19
29
|
type CodexBridgeOptions = Readonly<{
|
|
20
30
|
repoRoot?: string;
|
|
21
31
|
store?: LifecycleStore;
|
|
22
32
|
preflight?: typeof preflightCodexLifecycleEvidence;
|
|
23
33
|
resolveThread?: typeof resolveCodexThread;
|
|
24
34
|
resolveTerminal?: typeof resolveCodexTerminal;
|
|
35
|
+
capabilityStore?: CapabilityStore;
|
|
36
|
+
buildIdentity?: LifecycleBuildIdentity;
|
|
25
37
|
orchestrationOptions?: OrchestrationOptions;
|
|
26
38
|
}>;
|
|
27
39
|
|
|
@@ -59,25 +71,68 @@ function requiredStore(options: CodexBridgeOptions): LifecycleStore {
|
|
|
59
71
|
return options.store;
|
|
60
72
|
}
|
|
61
73
|
|
|
74
|
+
function controllerContext(taskId: string, repoRoot: string) {
|
|
75
|
+
const contextPath = process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT;
|
|
76
|
+
return contextPath
|
|
77
|
+
? verifyCodexSandboxControllerContext(contextPath, taskId, { repoRoot })
|
|
78
|
+
: null;
|
|
79
|
+
}
|
|
80
|
+
|
|
62
81
|
async function prepareCodexOrchestrationDelegation(
|
|
63
82
|
taskRef: string,
|
|
64
83
|
input: Readonly<{
|
|
65
84
|
client: AgentClientId;
|
|
66
85
|
requestedModel?: string;
|
|
67
86
|
requestedReasoningEffort?: string;
|
|
87
|
+
capabilityToken?: string;
|
|
68
88
|
}>,
|
|
69
89
|
options: CodexBridgeOptions = {}
|
|
70
90
|
): Promise<OrchestrationResult> {
|
|
71
91
|
if (input.client !== 'codex') return prepareOrchestrationDelegation(taskRef, input, coreOptions(options));
|
|
72
92
|
try {
|
|
73
|
-
|
|
93
|
+
const repoRoot = options.repoRoot ?? process.cwd();
|
|
94
|
+
const resolved = resolveTaskRef(taskRef, { repoRoot });
|
|
95
|
+
if (!resolved.ok) return bridgeFailure(resolved.code, resolved.message);
|
|
96
|
+
const preflight = await (options.preflight ?? preflightCodexLifecycleEvidence)(repoRoot);
|
|
97
|
+
if (!input.capabilityToken) {
|
|
98
|
+
return bridgeFailure(
|
|
99
|
+
'ORCHESTRATION_CODEX_CAPABILITY_REQUIRED',
|
|
100
|
+
'Codex prepare requires a current-session capability token'
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
const buildIdentity = options.buildIdentity ?? computeLifecycleBuildIdentity(repoRoot);
|
|
104
|
+
const controller = controllerContext(resolved.taskId, repoRoot);
|
|
105
|
+
const capabilityStore = options.capabilityStore ?? createCodexCapabilityStore();
|
|
106
|
+
const consumed = capabilityStore.consume(input.capabilityToken, {
|
|
107
|
+
taskId: resolved.taskId,
|
|
108
|
+
hookDefinitionHash: preflight.hookDefinitionHash,
|
|
109
|
+
buildIdentity,
|
|
110
|
+
...(controller ? { controller: {
|
|
111
|
+
instanceDigest: controller.controllerInstanceDigest,
|
|
112
|
+
controlGeneration: controller.controlGeneration
|
|
113
|
+
} } : {})
|
|
114
|
+
});
|
|
115
|
+
return prepareOrchestrationDelegation(taskRef, {
|
|
116
|
+
...input,
|
|
117
|
+
lifecycleProvenance: {
|
|
118
|
+
...consumed.buildIdentity,
|
|
119
|
+
hookDefinitionHash: preflight.hookDefinitionHash,
|
|
120
|
+
...preflight.hookProvenance,
|
|
121
|
+
capabilitySessionId: consumed.sessionId!,
|
|
122
|
+
capabilityTurnId: consumed.turnId!,
|
|
123
|
+
capabilityToolUseId: consumed.toolUseId!,
|
|
124
|
+
controllerInstanceDigest: controller?.controllerInstanceDigest ?? null,
|
|
125
|
+
controlGeneration: controller?.controlGeneration ?? null
|
|
126
|
+
}
|
|
127
|
+
}, coreOptions(options));
|
|
74
128
|
} catch (error) {
|
|
75
129
|
return bridgeFailure(
|
|
76
|
-
'
|
|
130
|
+
error instanceof Error && error.name.startsWith('CODEX_CAPABILITY_')
|
|
131
|
+
? error.name
|
|
132
|
+
: 'ORCHESTRATION_CLIENT_PREFLIGHT_FAILED',
|
|
77
133
|
error instanceof Error ? error.message : String(error)
|
|
78
134
|
);
|
|
79
135
|
}
|
|
80
|
-
return prepareOrchestrationDelegation(taskRef, input, coreOptions(options));
|
|
81
136
|
}
|
|
82
137
|
|
|
83
138
|
async function activateCodexOrchestrationDelegation(
|
|
@@ -85,6 +140,9 @@ async function activateCodexOrchestrationDelegation(
|
|
|
85
140
|
options: CodexBridgeOptions = {}
|
|
86
141
|
): Promise<OrchestrationResult> {
|
|
87
142
|
try {
|
|
143
|
+
if (!hasActivatableOrchestrationDelegation('codex', childThreadId, coreOptions(options))) {
|
|
144
|
+
return bridgeFailure('ORCHESTRATION_DELEGATION_MISSING', 'No matching Codex delegation is active');
|
|
145
|
+
}
|
|
88
146
|
const store = requiredStore(options);
|
|
89
147
|
const resolved = await (options.resolveThread ?? resolveCodexThread)(childThreadId);
|
|
90
148
|
store.apply(resolved.resolution.thread);
|
|
@@ -95,6 +153,23 @@ async function activateCodexOrchestrationDelegation(
|
|
|
95
153
|
if (record.state.status !== 'start-ready' || !evidence) {
|
|
96
154
|
return pauseBridge('ORCHESTRATION_CODEX_START_EVIDENCE_INVALID', 'Codex lifecycle start evidence is not ready', options);
|
|
97
155
|
}
|
|
156
|
+
const repoRoot = options.repoRoot ?? process.cwd();
|
|
157
|
+
const preflight = await (options.preflight ?? preflightCodexLifecycleEvidence)(repoRoot, {
|
|
158
|
+
sessionId: evidence.parentThreadId,
|
|
159
|
+
turnId: evidence.parentTurnId,
|
|
160
|
+
toolUseId: evidence.spawnToolUseId
|
|
161
|
+
});
|
|
162
|
+
const buildIdentity = options.buildIdentity ?? computeLifecycleBuildIdentity(repoRoot);
|
|
163
|
+
const contextPath = process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT;
|
|
164
|
+
const contextTaskId = contextPath
|
|
165
|
+
? (JSON.parse(fs.readFileSync(contextPath, 'utf8')) as { taskId?: unknown }).taskId
|
|
166
|
+
: null;
|
|
167
|
+
if (contextPath && typeof contextTaskId !== 'string') {
|
|
168
|
+
throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
|
|
169
|
+
}
|
|
170
|
+
const controller = contextPath
|
|
171
|
+
? verifyCodexSandboxControllerContext(contextPath, contextTaskId as string, { repoRoot })
|
|
172
|
+
: null;
|
|
98
173
|
return activateMatchingOrchestrationDelegation('codex', {
|
|
99
174
|
nativeAgent: evidence.nativeAgent,
|
|
100
175
|
childId: evidence.childThreadId,
|
|
@@ -107,9 +182,17 @@ async function activateCodexOrchestrationDelegation(
|
|
|
107
182
|
? { reasoningEffortFallbackReason: evidence.reasoningEffortFallbackReason }
|
|
108
183
|
: {}),
|
|
109
184
|
hostEvidence: {
|
|
110
|
-
kind: 'codex-lifecycle-
|
|
185
|
+
kind: 'codex-lifecycle-v2',
|
|
111
186
|
hookDefinitionHash: evidence.hookDefinitionHash,
|
|
112
|
-
startRevision: record.revision
|
|
187
|
+
startRevision: record.revision,
|
|
188
|
+
...buildIdentity,
|
|
189
|
+
...preflight.hookProvenance,
|
|
190
|
+
capabilitySessionId: evidence.parentThreadId,
|
|
191
|
+
capabilityTurnId: evidence.parentTurnId,
|
|
192
|
+
spawnToolUseId: evidence.spawnToolUseId,
|
|
193
|
+
spawnObservedAt: record.spawnObservedAt ?? undefined,
|
|
194
|
+
controllerInstanceDigest: controller?.controllerInstanceDigest ?? null,
|
|
195
|
+
controlGeneration: controller?.controlGeneration ?? null
|
|
113
196
|
}
|
|
114
197
|
}, coreOptions(options));
|
|
115
198
|
} catch (error) {
|
|
@@ -149,6 +232,9 @@ async function sealCodexOrchestrationDelegation(
|
|
|
149
232
|
options: CodexBridgeOptions = {}
|
|
150
233
|
): Promise<OrchestrationResult> {
|
|
151
234
|
try {
|
|
235
|
+
if (!hasSealableOrchestrationDelegation('codex', childThreadId, coreOptions(options))) {
|
|
236
|
+
return bridgeFailure('ORCHESTRATION_DELEGATION_MISSING', 'No matching Codex delegation is active');
|
|
237
|
+
}
|
|
152
238
|
const store = requiredStore(options);
|
|
153
239
|
const existing = store.read(childThreadId);
|
|
154
240
|
if (!existing.consumer) {
|
|
@@ -169,7 +169,7 @@ function inplaceValidation(config: ReturnType<typeof loadConfig>, target: Return
|
|
|
169
169
|
const nonce = randomUUID();
|
|
170
170
|
const issuedAt = Date.now();
|
|
171
171
|
const lease: SandboxControlLease = {
|
|
172
|
-
version:
|
|
172
|
+
version: 2, generation: manifest.generation, nonce,
|
|
173
173
|
owner: { pid: process.pid, startTime }, issuedAt,
|
|
174
174
|
expiresAt: issuedAt + Math.min(options.timeoutMs + RECOVERY_GRACE_MS, MAX_TIMEOUT_MS + RECOVERY_GRACE_MS),
|
|
175
175
|
taskId: target.workspace.mode === 'task-bound' ? target.workspace.taskId : null,
|
package/lib/task/create.ts
CHANGED
|
@@ -231,6 +231,14 @@ function assertRealDirectory(directory: string, code: string): void {
|
|
|
231
231
|
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error(code);
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
+
function ensureRealDirectory(directory: string, code: string): void {
|
|
235
|
+
if (fs.existsSync(directory)) {
|
|
236
|
+
assertRealDirectory(directory, code);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
fs.mkdirSync(directory, { mode: 0o700 });
|
|
240
|
+
}
|
|
241
|
+
|
|
234
242
|
function withCreateLock<T>(repoRoot: string, workspaceRoot: string, operation: () => T): T {
|
|
235
243
|
const lockRoot = path.join(workspaceRoot, '.task-create.lock');
|
|
236
244
|
fs.mkdirSync(lockRoot, { recursive: true, mode: 0o700 });
|
|
@@ -289,8 +297,9 @@ function createLocalTask(value: unknown, options: LocalTaskCreateOptions): Local
|
|
|
289
297
|
const repoRoot = fs.realpathSync.native(options.repoRoot);
|
|
290
298
|
const workspaceRoot = path.join(repoRoot, '.agents', 'workspace');
|
|
291
299
|
const activeRoot = path.join(workspaceRoot, 'active');
|
|
292
|
-
|
|
293
|
-
|
|
300
|
+
ensureRealDirectory(path.join(repoRoot, '.agents'), 'TASK_CREATE_WORKSPACE_INVALID');
|
|
301
|
+
ensureRealDirectory(workspaceRoot, 'TASK_CREATE_WORKSPACE_INVALID');
|
|
302
|
+
ensureRealDirectory(activeRoot, 'TASK_CREATE_WORKSPACE_INVALID');
|
|
294
303
|
const project = readProject(repoRoot);
|
|
295
304
|
const canonical = canonicalTaskCreateCandidate(candidate);
|
|
296
305
|
const candidateDigest = sha256(canonical);
|