@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
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { extractSection, findSectionHeading, parseTable } from './sections.js';
|
|
4
|
+
const RECEIPT_SECTION_ALIASES = ['产物生命周期收据', 'Artifact Lifecycle Receipts'];
|
|
5
|
+
const RECEIPT_COLUMNS = ['event', 'output', 'input', 'input_sha256', 'completed_at'];
|
|
6
|
+
const RECEIPT_EVENTS = new Set([
|
|
7
|
+
'review-analysis.completed',
|
|
8
|
+
'review-plan.completed',
|
|
9
|
+
'review-code.completed',
|
|
10
|
+
'code.completed'
|
|
11
|
+
]);
|
|
12
|
+
const SHA256_RE = /^[a-f0-9]{64}$/;
|
|
13
|
+
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$/;
|
|
14
|
+
const RECEIPT_SHAPES = {
|
|
15
|
+
'review-analysis.completed': { output: 'review-analysis', input: 'analysis' },
|
|
16
|
+
'review-plan.completed': { output: 'review-plan', input: 'plan' },
|
|
17
|
+
'review-code.completed': { output: 'review-code', input: 'code' },
|
|
18
|
+
'code.completed': { output: 'code', input: 'plan' }
|
|
19
|
+
};
|
|
20
|
+
class ArtifactReceiptError extends Error {
|
|
21
|
+
code = 'ARTIFACT_RECEIPT_INVALID';
|
|
22
|
+
constructor(message) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.name = 'ArtifactReceiptError';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function sha256Bytes(value) {
|
|
28
|
+
return createHash('sha256').update(value).digest('hex');
|
|
29
|
+
}
|
|
30
|
+
function sha256File(filePath) {
|
|
31
|
+
return sha256Bytes(fs.readFileSync(filePath));
|
|
32
|
+
}
|
|
33
|
+
function parseReceiptArtifactName(name) {
|
|
34
|
+
const match = ARTIFACT_NAME_RE.exec(name);
|
|
35
|
+
if (!match)
|
|
36
|
+
return null;
|
|
37
|
+
const roundText = match[2];
|
|
38
|
+
if (roundText !== undefined) {
|
|
39
|
+
const round = Number(roundText);
|
|
40
|
+
if (!Number.isSafeInteger(round) || String(round) !== roundText)
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
return { family: match[1] };
|
|
44
|
+
}
|
|
45
|
+
function validateReceiptShape(event, output, input) {
|
|
46
|
+
const outputIdentity = parseReceiptArtifactName(output);
|
|
47
|
+
const inputIdentity = parseReceiptArtifactName(input);
|
|
48
|
+
if (!outputIdentity || !inputIdentity)
|
|
49
|
+
throw new ArtifactReceiptError(`receipt artifact identity is invalid: ${output} -> ${input}`);
|
|
50
|
+
const shape = RECEIPT_SHAPES[event];
|
|
51
|
+
if (outputIdentity.family !== shape.output || inputIdentity.family !== shape.input) {
|
|
52
|
+
throw new ArtifactReceiptError(`receipt event '${event}' does not match ${output} -> ${input}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function parseArtifactReceipts(content) {
|
|
56
|
+
const section = extractSection(content, [...RECEIPT_SECTION_ALIASES]);
|
|
57
|
+
if (!section)
|
|
58
|
+
return { present: false, rows: [] };
|
|
59
|
+
let table;
|
|
60
|
+
try {
|
|
61
|
+
table = parseTable(content, {
|
|
62
|
+
sectionAliases: [...RECEIPT_SECTION_ALIASES],
|
|
63
|
+
columns: [...RECEIPT_COLUMNS],
|
|
64
|
+
keyColumn: 'output'
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
throw new ArtifactReceiptError(error instanceof Error ? error.message : String(error));
|
|
69
|
+
}
|
|
70
|
+
if (!table)
|
|
71
|
+
throw new ArtifactReceiptError('receipt section has no receipt table');
|
|
72
|
+
const rows = table.rows.map((row) => {
|
|
73
|
+
const event = row.values.event ?? '';
|
|
74
|
+
const output = row.values.output ?? '';
|
|
75
|
+
const input = row.values.input ?? '';
|
|
76
|
+
const inputSha256 = row.values.input_sha256 ?? '';
|
|
77
|
+
const completedAt = row.values.completed_at ?? '';
|
|
78
|
+
if (!RECEIPT_EVENTS.has(event))
|
|
79
|
+
throw new ArtifactReceiptError(`unknown receipt event '${event}'`);
|
|
80
|
+
if (!output || !input)
|
|
81
|
+
throw new ArtifactReceiptError('receipt output and input are required');
|
|
82
|
+
validateReceiptShape(event, output, input);
|
|
83
|
+
if (!SHA256_RE.test(inputSha256))
|
|
84
|
+
throw new ArtifactReceiptError(`receipt digest for '${output}' is invalid`);
|
|
85
|
+
if (!completedAt || Number.isNaN(Date.parse(completedAt.replace(' ', 'T')))) {
|
|
86
|
+
throw new ArtifactReceiptError(`receipt completion time for '${output}' is invalid`);
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
event: event,
|
|
90
|
+
output,
|
|
91
|
+
input,
|
|
92
|
+
inputSha256,
|
|
93
|
+
completedAt
|
|
94
|
+
};
|
|
95
|
+
});
|
|
96
|
+
return { present: true, rows };
|
|
97
|
+
}
|
|
98
|
+
function receiptForOutput(content, output) {
|
|
99
|
+
const parsed = parseArtifactReceipts(content);
|
|
100
|
+
return parsed.rows.find((row) => row.output === output) ?? null;
|
|
101
|
+
}
|
|
102
|
+
function upsertArtifactReceipt(content, receipt) {
|
|
103
|
+
if (!RECEIPT_EVENTS.has(receipt.event))
|
|
104
|
+
throw new ArtifactReceiptError(`unknown receipt event '${receipt.event}'`);
|
|
105
|
+
if (!receipt.output || !receipt.input)
|
|
106
|
+
throw new ArtifactReceiptError('receipt output and input are required');
|
|
107
|
+
validateReceiptShape(receipt.event, receipt.output, receipt.input);
|
|
108
|
+
if (!SHA256_RE.test(receipt.inputSha256))
|
|
109
|
+
throw new ArtifactReceiptError(`receipt digest for '${receipt.output}' is invalid`);
|
|
110
|
+
if (!receipt.completedAt || Number.isNaN(Date.parse(receipt.completedAt.replace(' ', 'T')))) {
|
|
111
|
+
throw new ArtifactReceiptError(`receipt completion time for '${receipt.output}' is invalid`);
|
|
112
|
+
}
|
|
113
|
+
const existing = parseArtifactReceipts(content).rows;
|
|
114
|
+
const previous = existing.find((row) => row.output === receipt.output);
|
|
115
|
+
if (previous && JSON.stringify(previous) !== JSON.stringify(receipt)) {
|
|
116
|
+
throw new ArtifactReceiptError(`receipt for '${receipt.output}' already exists with different evidence`);
|
|
117
|
+
}
|
|
118
|
+
const rows = previous ? existing : [...existing, receipt];
|
|
119
|
+
const heading = findSectionHeading(content, [...RECEIPT_SECTION_ALIASES]);
|
|
120
|
+
const body = [
|
|
121
|
+
`| ${RECEIPT_COLUMNS.join(' | ')} |`,
|
|
122
|
+
`| ${RECEIPT_COLUMNS.map(() => '---').join(' | ')} |`,
|
|
123
|
+
...rows.map((row) => `| ${row.event} | ${row.output} | ${row.input} | ${row.inputSha256} | ${row.completedAt} |`)
|
|
124
|
+
].join('\n');
|
|
125
|
+
return { aliases: [...RECEIPT_SECTION_ALIASES], heading, body };
|
|
126
|
+
}
|
|
127
|
+
export { ArtifactReceiptError, RECEIPT_SECTION_ALIASES, parseArtifactReceipts, receiptForOutput, sha256Bytes, sha256File, upsertArtifactReceipt };
|
|
128
|
+
//# sourceMappingURL=artifact-receipts.js.map
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
1
2
|
import { preflightCodexLifecycleEvidence, resolveCodexSpawnedChild, resolveCodexTerminal, resolveCodexThread } from '../agent-clients/adapters/codex-lifecycle/app-server.js';
|
|
2
3
|
import { createCodexLifecycleStore } from '../agent-clients/adapters/codex-lifecycle/store.js';
|
|
3
|
-
import {
|
|
4
|
+
import { createCodexCapabilityStore } from '../agent-clients/adapters/codex-lifecycle/capability-store.js';
|
|
5
|
+
import { computeLifecycleBuildIdentity } from '../agent-clients/adapters/codex-lifecycle/build-identity.js';
|
|
6
|
+
import { verifyCodexSandboxControllerContext } from '../agent-clients/adapters/codex-lifecycle/sandbox-controller.js';
|
|
7
|
+
import { resolveTaskRef } from './resolve-ref.js';
|
|
8
|
+
import { activateMatchingOrchestrationDelegation, hasActivatableOrchestrationDelegation, hasSealableOrchestrationDelegation, pauseMatchingOrchestrationDelegation, prepareOrchestrationDelegation, reconcileMatchingOrchestrationDelegation, sealMatchingOrchestrationDelegationWithHostEvidence } from './orchestration.js';
|
|
4
9
|
function coreOptions(options) {
|
|
5
10
|
return { ...options.orchestrationOptions, repoRoot: options.repoRoot ?? options.orchestrationOptions?.repoRoot };
|
|
6
11
|
}
|
|
@@ -21,19 +26,61 @@ function requiredStore(options) {
|
|
|
21
26
|
throw new Error('Codex lifecycle store is required for orchestration bridge events');
|
|
22
27
|
return options.store;
|
|
23
28
|
}
|
|
29
|
+
function controllerContext(taskId, repoRoot) {
|
|
30
|
+
const contextPath = process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT;
|
|
31
|
+
return contextPath
|
|
32
|
+
? verifyCodexSandboxControllerContext(contextPath, taskId, { repoRoot })
|
|
33
|
+
: null;
|
|
34
|
+
}
|
|
24
35
|
async function prepareCodexOrchestrationDelegation(taskRef, input, options = {}) {
|
|
25
36
|
if (input.client !== 'codex')
|
|
26
37
|
return prepareOrchestrationDelegation(taskRef, input, coreOptions(options));
|
|
27
38
|
try {
|
|
28
|
-
|
|
39
|
+
const repoRoot = options.repoRoot ?? process.cwd();
|
|
40
|
+
const resolved = resolveTaskRef(taskRef, { repoRoot });
|
|
41
|
+
if (!resolved.ok)
|
|
42
|
+
return bridgeFailure(resolved.code, resolved.message);
|
|
43
|
+
const preflight = await (options.preflight ?? preflightCodexLifecycleEvidence)(repoRoot);
|
|
44
|
+
if (!input.capabilityToken) {
|
|
45
|
+
return bridgeFailure('ORCHESTRATION_CODEX_CAPABILITY_REQUIRED', 'Codex prepare requires a current-session capability token');
|
|
46
|
+
}
|
|
47
|
+
const buildIdentity = options.buildIdentity ?? computeLifecycleBuildIdentity(repoRoot);
|
|
48
|
+
const controller = controllerContext(resolved.taskId, repoRoot);
|
|
49
|
+
const capabilityStore = options.capabilityStore ?? createCodexCapabilityStore();
|
|
50
|
+
const consumed = capabilityStore.consume(input.capabilityToken, {
|
|
51
|
+
taskId: resolved.taskId,
|
|
52
|
+
hookDefinitionHash: preflight.hookDefinitionHash,
|
|
53
|
+
buildIdentity,
|
|
54
|
+
...(controller ? { controller: {
|
|
55
|
+
instanceDigest: controller.controllerInstanceDigest,
|
|
56
|
+
controlGeneration: controller.controlGeneration
|
|
57
|
+
} } : {})
|
|
58
|
+
});
|
|
59
|
+
return prepareOrchestrationDelegation(taskRef, {
|
|
60
|
+
...input,
|
|
61
|
+
lifecycleProvenance: {
|
|
62
|
+
...consumed.buildIdentity,
|
|
63
|
+
hookDefinitionHash: preflight.hookDefinitionHash,
|
|
64
|
+
...preflight.hookProvenance,
|
|
65
|
+
capabilitySessionId: consumed.sessionId,
|
|
66
|
+
capabilityTurnId: consumed.turnId,
|
|
67
|
+
capabilityToolUseId: consumed.toolUseId,
|
|
68
|
+
controllerInstanceDigest: controller?.controllerInstanceDigest ?? null,
|
|
69
|
+
controlGeneration: controller?.controlGeneration ?? null
|
|
70
|
+
}
|
|
71
|
+
}, coreOptions(options));
|
|
29
72
|
}
|
|
30
73
|
catch (error) {
|
|
31
|
-
return bridgeFailure(
|
|
74
|
+
return bridgeFailure(error instanceof Error && error.name.startsWith('CODEX_CAPABILITY_')
|
|
75
|
+
? error.name
|
|
76
|
+
: 'ORCHESTRATION_CLIENT_PREFLIGHT_FAILED', error instanceof Error ? error.message : String(error));
|
|
32
77
|
}
|
|
33
|
-
return prepareOrchestrationDelegation(taskRef, input, coreOptions(options));
|
|
34
78
|
}
|
|
35
79
|
async function activateCodexOrchestrationDelegation(childThreadId, options = {}) {
|
|
36
80
|
try {
|
|
81
|
+
if (!hasActivatableOrchestrationDelegation('codex', childThreadId, coreOptions(options))) {
|
|
82
|
+
return bridgeFailure('ORCHESTRATION_DELEGATION_MISSING', 'No matching Codex delegation is active');
|
|
83
|
+
}
|
|
37
84
|
const store = requiredStore(options);
|
|
38
85
|
const resolved = await (options.resolveThread ?? resolveCodexThread)(childThreadId);
|
|
39
86
|
store.apply(resolved.resolution.thread);
|
|
@@ -45,6 +92,23 @@ async function activateCodexOrchestrationDelegation(childThreadId, options = {})
|
|
|
45
92
|
if (record.state.status !== 'start-ready' || !evidence) {
|
|
46
93
|
return pauseBridge('ORCHESTRATION_CODEX_START_EVIDENCE_INVALID', 'Codex lifecycle start evidence is not ready', options);
|
|
47
94
|
}
|
|
95
|
+
const repoRoot = options.repoRoot ?? process.cwd();
|
|
96
|
+
const preflight = await (options.preflight ?? preflightCodexLifecycleEvidence)(repoRoot, {
|
|
97
|
+
sessionId: evidence.parentThreadId,
|
|
98
|
+
turnId: evidence.parentTurnId,
|
|
99
|
+
toolUseId: evidence.spawnToolUseId
|
|
100
|
+
});
|
|
101
|
+
const buildIdentity = options.buildIdentity ?? computeLifecycleBuildIdentity(repoRoot);
|
|
102
|
+
const contextPath = process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT;
|
|
103
|
+
const contextTaskId = contextPath
|
|
104
|
+
? JSON.parse(fs.readFileSync(contextPath, 'utf8')).taskId
|
|
105
|
+
: null;
|
|
106
|
+
if (contextPath && typeof contextTaskId !== 'string') {
|
|
107
|
+
throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
|
|
108
|
+
}
|
|
109
|
+
const controller = contextPath
|
|
110
|
+
? verifyCodexSandboxControllerContext(contextPath, contextTaskId, { repoRoot })
|
|
111
|
+
: null;
|
|
48
112
|
return activateMatchingOrchestrationDelegation('codex', {
|
|
49
113
|
nativeAgent: evidence.nativeAgent,
|
|
50
114
|
childId: evidence.childThreadId,
|
|
@@ -57,9 +121,17 @@ async function activateCodexOrchestrationDelegation(childThreadId, options = {})
|
|
|
57
121
|
? { reasoningEffortFallbackReason: evidence.reasoningEffortFallbackReason }
|
|
58
122
|
: {}),
|
|
59
123
|
hostEvidence: {
|
|
60
|
-
kind: 'codex-lifecycle-
|
|
124
|
+
kind: 'codex-lifecycle-v2',
|
|
61
125
|
hookDefinitionHash: evidence.hookDefinitionHash,
|
|
62
|
-
startRevision: record.revision
|
|
126
|
+
startRevision: record.revision,
|
|
127
|
+
...buildIdentity,
|
|
128
|
+
...preflight.hookProvenance,
|
|
129
|
+
capabilitySessionId: evidence.parentThreadId,
|
|
130
|
+
capabilityTurnId: evidence.parentTurnId,
|
|
131
|
+
spawnToolUseId: evidence.spawnToolUseId,
|
|
132
|
+
spawnObservedAt: record.spawnObservedAt ?? undefined,
|
|
133
|
+
controllerInstanceDigest: controller?.controllerInstanceDigest ?? null,
|
|
134
|
+
controlGeneration: controller?.controlGeneration ?? null
|
|
63
135
|
}
|
|
64
136
|
}, coreOptions(options));
|
|
65
137
|
}
|
|
@@ -93,6 +165,9 @@ async function activateCodexSpawnDelegation(spawn, options = {}) {
|
|
|
93
165
|
}
|
|
94
166
|
async function sealCodexOrchestrationDelegation(childThreadId, options = {}) {
|
|
95
167
|
try {
|
|
168
|
+
if (!hasSealableOrchestrationDelegation('codex', childThreadId, coreOptions(options))) {
|
|
169
|
+
return bridgeFailure('ORCHESTRATION_DELEGATION_MISSING', 'No matching Codex delegation is active');
|
|
170
|
+
}
|
|
96
171
|
const store = requiredStore(options);
|
|
97
172
|
const existing = store.read(childThreadId);
|
|
98
173
|
if (!existing.consumer) {
|
|
@@ -168,7 +168,7 @@ function inplaceValidation(config, target, options) {
|
|
|
168
168
|
const nonce = randomUUID();
|
|
169
169
|
const issuedAt = Date.now();
|
|
170
170
|
const lease = {
|
|
171
|
-
version:
|
|
171
|
+
version: 2, generation: manifest.generation, nonce,
|
|
172
172
|
owner: { pid: process.pid, startTime }, issuedAt,
|
|
173
173
|
expiresAt: issuedAt + Math.min(options.timeoutMs + RECOVERY_GRACE_MS, MAX_TIMEOUT_MS + RECOVERY_GRACE_MS),
|
|
174
174
|
taskId: target.workspace.mode === 'task-bound' ? target.workspace.taskId : null,
|
package/dist/lib/task/create.js
CHANGED
|
@@ -181,6 +181,13 @@ function assertRealDirectory(directory, code) {
|
|
|
181
181
|
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
182
182
|
throw new Error(code);
|
|
183
183
|
}
|
|
184
|
+
function ensureRealDirectory(directory, code) {
|
|
185
|
+
if (fs.existsSync(directory)) {
|
|
186
|
+
assertRealDirectory(directory, code);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
fs.mkdirSync(directory, { mode: 0o700 });
|
|
190
|
+
}
|
|
184
191
|
function withCreateLock(repoRoot, workspaceRoot, operation) {
|
|
185
192
|
const lockRoot = path.join(workspaceRoot, '.task-create.lock');
|
|
186
193
|
fs.mkdirSync(lockRoot, { recursive: true, mode: 0o700 });
|
|
@@ -254,8 +261,9 @@ function createLocalTask(value, options) {
|
|
|
254
261
|
const repoRoot = fs.realpathSync.native(options.repoRoot);
|
|
255
262
|
const workspaceRoot = path.join(repoRoot, '.agents', 'workspace');
|
|
256
263
|
const activeRoot = path.join(workspaceRoot, 'active');
|
|
257
|
-
|
|
258
|
-
|
|
264
|
+
ensureRealDirectory(path.join(repoRoot, '.agents'), 'TASK_CREATE_WORKSPACE_INVALID');
|
|
265
|
+
ensureRealDirectory(workspaceRoot, 'TASK_CREATE_WORKSPACE_INVALID');
|
|
266
|
+
ensureRealDirectory(activeRoot, 'TASK_CREATE_WORKSPACE_INVALID');
|
|
259
267
|
const project = readProject(repoRoot);
|
|
260
268
|
const canonical = canonicalTaskCreateCandidate(candidate);
|
|
261
269
|
const candidateDigest = sha256(canonical);
|
|
@@ -11,6 +11,7 @@ function fail(code, message) {
|
|
|
11
11
|
return { ok: false, code, message };
|
|
12
12
|
}
|
|
13
13
|
function prepareDelegation(input, options = {}) {
|
|
14
|
+
const monotonic = (options.monotonicNow ?? (() => Number(process.hrtime.bigint() / 1000000n)))();
|
|
14
15
|
return Object.freeze({
|
|
15
16
|
...input,
|
|
16
17
|
id: (options.id ?? randomUUID)(),
|
|
@@ -27,17 +28,55 @@ function prepareDelegation(input, options = {}) {
|
|
|
27
28
|
afterFingerprint: null,
|
|
28
29
|
changedPaths: Object.freeze([]),
|
|
29
30
|
createdAt: (options.now ?? (() => new Date().toISOString()))(),
|
|
31
|
+
preparedMonotonicMs: monotonic,
|
|
32
|
+
spawnDispatchMonotonicMs: null,
|
|
33
|
+
activationDeadlineMonotonicMs: null,
|
|
34
|
+
spawnDispatchedAt: null,
|
|
35
|
+
activationDeadlineAt: null,
|
|
36
|
+
startEvidenceMonotonicMs: null,
|
|
37
|
+
activatedMonotonicMs: null,
|
|
30
38
|
activatedAt: null,
|
|
31
39
|
sealedAt: null,
|
|
32
40
|
consumedAt: null
|
|
33
41
|
});
|
|
34
42
|
}
|
|
43
|
+
function dispatchDelegation(receipt, options = {}) {
|
|
44
|
+
if (receipt.status !== 'prepared') {
|
|
45
|
+
return fail('DELEGATION_STATE_INVALID', `delegation ${receipt.id} is ${receipt.status}, expected prepared`);
|
|
46
|
+
}
|
|
47
|
+
if (receipt.spawnDispatchMonotonicMs != null || receipt.spawnDispatchedAt != null) {
|
|
48
|
+
return fail('DELEGATION_DISPATCH_REPLAY', `delegation ${receipt.id} was already dispatched`);
|
|
49
|
+
}
|
|
50
|
+
const monotonic = (options.monotonicNow ?? (() => Number(process.hrtime.bigint() / 1000000n)))();
|
|
51
|
+
const dispatchedAt = (options.now ?? (() => new Date().toISOString()))();
|
|
52
|
+
const window = options.activationWindowMs ?? 15_000;
|
|
53
|
+
return { ok: true, receipt: Object.freeze({
|
|
54
|
+
...receipt,
|
|
55
|
+
spawnDispatchMonotonicMs: monotonic,
|
|
56
|
+
activationDeadlineMonotonicMs: monotonic + window,
|
|
57
|
+
spawnDispatchedAt: dispatchedAt,
|
|
58
|
+
activationDeadlineAt: new Date(Date.parse(dispatchedAt) + window).toISOString()
|
|
59
|
+
}) };
|
|
60
|
+
}
|
|
35
61
|
function activateDelegation(receipt, event, options = {}) {
|
|
36
62
|
const managedRole = managedDelegationRole(event.nativeAgent);
|
|
37
63
|
if (!managedRole)
|
|
38
64
|
return fail('DELEGATION_IGNORED', `subagent '${event.nativeAgent}' is not lifecycle-managed`);
|
|
39
65
|
if (receipt.status !== 'prepared')
|
|
40
66
|
return fail('DELEGATION_STATE_INVALID', `delegation ${receipt.id} is ${receipt.status}, expected prepared`);
|
|
67
|
+
if (receipt.spawnDispatchMonotonicMs == null
|
|
68
|
+
|| receipt.spawnDispatchedAt == null
|
|
69
|
+
|| receipt.activationDeadlineAt == null
|
|
70
|
+
|| receipt.activationDeadlineMonotonicMs == null
|
|
71
|
+
|| !Number.isFinite(Date.parse(receipt.spawnDispatchedAt))
|
|
72
|
+
|| !Number.isFinite(Date.parse(receipt.activationDeadlineAt))) {
|
|
73
|
+
return fail('DELEGATION_NOT_DISPATCHED', `delegation ${receipt.id} has not reached the native spawn dispatch boundary`);
|
|
74
|
+
}
|
|
75
|
+
const monotonic = (options.monotonicNow ?? (() => Number(process.hrtime.bigint() / 1000000n)))();
|
|
76
|
+
const wallNow = Date.parse((options.now ?? (() => new Date().toISOString()))());
|
|
77
|
+
if (wallNow > Date.parse(receipt.activationDeadlineAt)) {
|
|
78
|
+
return fail('DELEGATION_ACTIVATION_TIMEOUT', `delegation ${receipt.id} activation evidence arrived after its deadline`);
|
|
79
|
+
}
|
|
41
80
|
if (managedRole !== receipt.role)
|
|
42
81
|
return fail('DELEGATION_ROLE_MISMATCH', `managed role ${managedRole} does not match ${receipt.role}`);
|
|
43
82
|
if (!event.parentId || (receipt.parentId !== null && event.parentId !== receipt.parentId) || !event.childId || event.childId === event.parentId) {
|
|
@@ -56,6 +95,9 @@ function activateDelegation(receipt, event, options = {}) {
|
|
|
56
95
|
if (!actualReasoningEffort || actualReasoningEffort.trim() !== actualReasoningEffort) {
|
|
57
96
|
return fail('DELEGATION_REASONING_EFFORT_MISSING', 'native start event must provide a non-empty actual reasoning effort');
|
|
58
97
|
}
|
|
98
|
+
if (receipt.client === 'codex'
|
|
99
|
+
&& event.hostEvidence?.kind !== 'codex-lifecycle-v2')
|
|
100
|
+
return fail('DELEGATION_HOST_EVIDENCE_REQUIRED', 'Codex activation requires trusted lifecycle-v2 host evidence');
|
|
59
101
|
if (actualReasoningEffort !== receipt.requestedReasoningEffort
|
|
60
102
|
&& (!event.reasoningEffortFallbackReason || event.reasoningEffortFallbackReason.trim() === '')) {
|
|
61
103
|
return fail('DELEGATION_REASONING_EFFORT_FALLBACK_UNRECORDED', 'actual reasoning effort differs from requested effort without a fallback reason');
|
|
@@ -72,6 +114,32 @@ function activateDelegation(receipt, event, options = {}) {
|
|
|
72
114
|
|| !Number.isSafeInteger(event.hostEvidence.startRevision)
|
|
73
115
|
|| event.hostEvidence.startRevision < 1))
|
|
74
116
|
return fail('DELEGATION_HOST_EVIDENCE_INVALID', 'Codex start evidence reference is invalid');
|
|
117
|
+
if (event.hostEvidence?.kind === 'codex-lifecycle-v2') {
|
|
118
|
+
const expected = receipt.lifecycleProvenance;
|
|
119
|
+
const spawnObservedAt = Date.parse(event.hostEvidence.spawnObservedAt ?? '');
|
|
120
|
+
const spawnDispatchedAt = Date.parse(receipt.spawnDispatchedAt);
|
|
121
|
+
const activationDeadlineAt = Date.parse(receipt.activationDeadlineAt);
|
|
122
|
+
if (!expected
|
|
123
|
+
|| event.hostEvidence.protocolVersion !== expected.protocolVersion
|
|
124
|
+
|| event.hostEvidence.packageVersion !== expected.packageVersion
|
|
125
|
+
|| event.hostEvidence.internalExecutableBuildHash !== expected.internalExecutableBuildHash
|
|
126
|
+
|| event.hostEvidence.lifecycleContractHash !== expected.lifecycleContractHash
|
|
127
|
+
|| event.hostEvidence.hookDefinitionHash !== expected.hookDefinitionHash
|
|
128
|
+
|| event.hostEvidence.hookSource !== expected.hookSource
|
|
129
|
+
|| event.hostEvidence.hookSourcePathDigest !== expected.hookSourcePathDigest
|
|
130
|
+
|| event.hostEvidence.hookSourceHash !== expected.hookSourceHash
|
|
131
|
+
|| event.hostEvidence.capabilitySessionId !== expected.capabilitySessionId
|
|
132
|
+
|| event.hostEvidence.capabilityTurnId !== expected.capabilityTurnId
|
|
133
|
+
|| !event.hostEvidence.spawnToolUseId?.trim()
|
|
134
|
+
|| event.hostEvidence.spawnToolUseId === expected.capabilityToolUseId
|
|
135
|
+
|| !Number.isFinite(spawnObservedAt)
|
|
136
|
+
|| spawnObservedAt < spawnDispatchedAt
|
|
137
|
+
|| spawnObservedAt > activationDeadlineAt
|
|
138
|
+
|| event.parentId !== expected.capabilitySessionId
|
|
139
|
+
|| (event.hostEvidence.controllerInstanceDigest ?? null) !== expected.controllerInstanceDigest
|
|
140
|
+
|| (event.hostEvidence.controlGeneration ?? null) !== expected.controlGeneration)
|
|
141
|
+
return fail('DELEGATION_HOST_EVIDENCE_INVALID', 'Codex lifecycle provenance does not match the prepared receipt');
|
|
142
|
+
}
|
|
75
143
|
return { ok: true, receipt: Object.freeze({
|
|
76
144
|
...receipt,
|
|
77
145
|
status: 'activated',
|
|
@@ -88,9 +156,20 @@ function activateDelegation(receipt, event, options = {}) {
|
|
|
88
156
|
consumer: null,
|
|
89
157
|
consumedAt: null
|
|
90
158
|
}) : receipt.hostEvidence ?? null,
|
|
91
|
-
|
|
159
|
+
startEvidenceMonotonicMs: monotonic,
|
|
160
|
+
activatedMonotonicMs: (options.monotonicNow ?? (() => Number(process.hrtime.bigint() / 1000000n)))(),
|
|
161
|
+
activatedAt: new Date(wallNow).toISOString()
|
|
92
162
|
}) };
|
|
93
163
|
}
|
|
164
|
+
function abortPreparedDelegation(receipt) {
|
|
165
|
+
if (receipt.status !== 'prepared') {
|
|
166
|
+
return fail('DELEGATION_STATE_INVALID', `delegation ${receipt.id} is ${receipt.status}, expected prepared`);
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
ok: true,
|
|
170
|
+
receipt: Object.freeze({ ...receipt, status: 'aborted' })
|
|
171
|
+
};
|
|
172
|
+
}
|
|
94
173
|
function completeDelegationStage(receipt, event) {
|
|
95
174
|
if (receipt.status !== 'activated')
|
|
96
175
|
return fail('DELEGATION_STATE_INVALID', `delegation ${receipt.id} is ${receipt.status}, expected activated`);
|
|
@@ -105,6 +184,9 @@ function completeDelegationStage(receipt, event) {
|
|
|
105
184
|
function sealDelegation(receipt, event, options = {}) {
|
|
106
185
|
if (receipt.status !== 'stage-completed')
|
|
107
186
|
return fail('DELEGATION_STATE_INVALID', `delegation ${receipt.id} is ${receipt.status}, expected stage-completed`);
|
|
187
|
+
if (receipt.client === 'codex' && options.requireHostEvidence !== false && !event.hostEvidence) {
|
|
188
|
+
return fail('DELEGATION_HOST_EVIDENCE_REQUIRED', 'Codex sealing requires consumed lifecycle-v2 host evidence');
|
|
189
|
+
}
|
|
108
190
|
if (event.childId !== receipt.childId || event.exitCode !== 0)
|
|
109
191
|
return fail('DELEGATION_STOP_INVALID', 'native stop identity or exit status is invalid');
|
|
110
192
|
if (receipt.role === 'reviewer') {
|
|
@@ -149,5 +231,5 @@ function consumeDelegation(receipt, options = {}) {
|
|
|
149
231
|
consumedAt: (options.now ?? (() => new Date().toISOString()))()
|
|
150
232
|
}) };
|
|
151
233
|
}
|
|
152
|
-
export { activateDelegation, completeDelegationStage, consumeDelegation, managedDelegationRole, prepareDelegation, sealDelegation };
|
|
234
|
+
export { activateDelegation, abortPreparedDelegation, completeDelegationStage, consumeDelegation, dispatchDelegation, managedDelegationRole, prepareDelegation, sealDelegation };
|
|
153
235
|
//# sourceMappingURL=delegation-receipts.js.map
|
package/dist/lib/task/events.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import { appendActivityEntry, locateActivityLog, pairEntries } from './activity-log.js';
|
|
3
|
-
import { buildArtifactLinkSection, artifactName, parseArtifactName, resolveArtifactContext, validateCompletedArtifact } from './artifact-lifecycle.js';
|
|
3
|
+
import { buildArtifactLinkSection, artifactName, inspectArtifactDirectory, parseReviewedInputReference, parseCodePlanInputReference, parseArtifactName, resolveArtifactContext, validateCompletedArtifact } from './artifact-lifecycle.js';
|
|
4
|
+
import { ArtifactReceiptError, sha256File, upsertArtifactReceipt } from './artifact-receipts.js';
|
|
4
5
|
import { parseTypedTaskFrontmatter } from './frontmatter.js';
|
|
5
6
|
import { consumeImplementationInput, IMPLEMENTATION_INPUT_ALIASES, parseImplementationInputs, renderImplementationInputs } from './implementation-inputs.js';
|
|
6
7
|
import { parseLedger, summarizeLedgerStage, validateLedgerRows } from './ledger.js';
|
|
@@ -237,6 +238,92 @@ function openStartedIdentity(rows, family) {
|
|
|
237
238
|
});
|
|
238
239
|
return matches.length === 1 ? matches[0] : matches.length > 1 ? { conflict: true } : null;
|
|
239
240
|
}
|
|
241
|
+
function reviewInputFamily(family) {
|
|
242
|
+
return family === 'review-analysis' ? 'analysis' : family === 'review-plan' ? 'plan' : 'code';
|
|
243
|
+
}
|
|
244
|
+
function buildCompletionReceipt(taskDir, family, artifact, completedAt, frontmatter) {
|
|
245
|
+
if (family === 'code') {
|
|
246
|
+
let content;
|
|
247
|
+
try {
|
|
248
|
+
content = fs.readFileSync(artifact.path, 'utf8');
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
return { ok: false, message: `cannot read ${artifact.name}: ${String(error)}` };
|
|
252
|
+
}
|
|
253
|
+
const input = parseCodePlanInputReference(content);
|
|
254
|
+
if (!input)
|
|
255
|
+
return { ok: false, message: `${artifact.name} does not reference a canonical plan artifact` };
|
|
256
|
+
const startedInput = typeof frontmatter.code_input_artifact === 'string' ? frontmatter.code_input_artifact : '';
|
|
257
|
+
const startedSha256 = typeof frontmatter.code_input_sha256 === 'string' ? frontmatter.code_input_sha256 : '';
|
|
258
|
+
if (!startedInput || !startedSha256)
|
|
259
|
+
return { ok: false, message: 'code.started plan input context is missing' };
|
|
260
|
+
if (input.name !== startedInput)
|
|
261
|
+
return { ok: false, message: `${artifact.name} plan input '${input.name}' does not match code.started input '${startedInput}'` };
|
|
262
|
+
const plan = inspectArtifactDirectory(taskDir, 'plan');
|
|
263
|
+
if (plan.status !== 'ready' || !plan.latest || plan.latest.name !== input.name) {
|
|
264
|
+
return { ok: false, message: `code input '${input.name}' is not the latest plan artifact` };
|
|
265
|
+
}
|
|
266
|
+
try {
|
|
267
|
+
const inputSha256 = sha256File(plan.latest.path);
|
|
268
|
+
if (inputSha256 !== startedSha256)
|
|
269
|
+
return { ok: false, message: `code input ${input.name} changed after code.started` };
|
|
270
|
+
return {
|
|
271
|
+
ok: true,
|
|
272
|
+
receipt: {
|
|
273
|
+
event: 'code.completed', output: artifact.name, input: input.name,
|
|
274
|
+
inputSha256, completedAt
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
return { ok: false, message: `cannot hash code input plan: ${error instanceof Error ? error.message : String(error)}` };
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (!family.startsWith('review-'))
|
|
283
|
+
return null;
|
|
284
|
+
const expectedFamily = reviewInputFamily(family);
|
|
285
|
+
let content;
|
|
286
|
+
try {
|
|
287
|
+
content = fs.readFileSync(artifact.path, 'utf8');
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
return { ok: false, message: `cannot read ${artifact.name}: ${String(error)}` };
|
|
291
|
+
}
|
|
292
|
+
const input = parseReviewedInputReference(content, expectedFamily);
|
|
293
|
+
if (!input)
|
|
294
|
+
return { ok: false, message: `${artifact.name} does not reference a canonical ${expectedFamily} artifact` };
|
|
295
|
+
const startedInput = typeof frontmatter.review_input_artifact === 'string' ? frontmatter.review_input_artifact : '';
|
|
296
|
+
const startedSha256 = typeof frontmatter.review_input_sha256 === 'string' ? frontmatter.review_input_sha256 : '';
|
|
297
|
+
if (!startedInput || !startedSha256)
|
|
298
|
+
return { ok: false, message: 'review started input context is missing' };
|
|
299
|
+
if (input.name !== startedInput)
|
|
300
|
+
return { ok: false, message: `${artifact.name} input '${input.name}' does not match review.started input '${startedInput}'` };
|
|
301
|
+
const current = inspectArtifactDirectory(taskDir, expectedFamily);
|
|
302
|
+
if (current.status !== 'ready' || !current.latest || current.latest.name !== input.name) {
|
|
303
|
+
return { ok: false, message: `review input '${input.name}' is not the latest ${expectedFamily} artifact` };
|
|
304
|
+
}
|
|
305
|
+
const event = family === 'review-analysis'
|
|
306
|
+
? 'review-analysis.completed'
|
|
307
|
+
: family === 'review-plan' ? 'review-plan.completed' : 'review-code.completed';
|
|
308
|
+
try {
|
|
309
|
+
const stat = fs.lstatSync(current.latest.path);
|
|
310
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
311
|
+
throw new Error('review input is not a regular file');
|
|
312
|
+
const inputSha256 = sha256File(current.latest.path);
|
|
313
|
+
if (inputSha256 !== startedSha256)
|
|
314
|
+
return { ok: false, message: `review input ${input.name} changed after review.started` };
|
|
315
|
+
return {
|
|
316
|
+
ok: true,
|
|
317
|
+
receipt: {
|
|
318
|
+
event, output: artifact.name, input: input.name,
|
|
319
|
+
inputSha256, completedAt
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
catch (error) {
|
|
324
|
+
return { ok: false, message: `cannot hash review input ${input.name}: ${error instanceof Error ? error.message : String(error)}` };
|
|
325
|
+
}
|
|
326
|
+
}
|
|
240
327
|
function applyTaskEventUnlocked(request, options = {}) {
|
|
241
328
|
const invalid = validateTaskEventRequest(request);
|
|
242
329
|
if (invalid)
|
|
@@ -363,19 +450,69 @@ function applyTaskEventUnlocked(request, options = {}) {
|
|
|
363
450
|
catch (error) {
|
|
364
451
|
return failed(normalized, { code: 'METADATA_CAPTURE_FAILED', message: String(error) }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
|
|
365
452
|
}
|
|
453
|
+
let completionReceipt = null;
|
|
454
|
+
if (eventIdentity.phase === 'completed' && completedArtifact) {
|
|
455
|
+
const receipt = buildCompletionReceipt(resolved.taskDir, eventIdentity.family, completedArtifact, metadata.timestamp, frontmatter);
|
|
456
|
+
if (receipt && !receipt.ok) {
|
|
457
|
+
return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: receipt.message }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
458
|
+
}
|
|
459
|
+
completionReceipt = receipt?.receipt ?? null;
|
|
460
|
+
}
|
|
366
461
|
const step = eventIdentity.phase === 'started' || eventIdentity.target === null ? currentStep : eventIdentity.target;
|
|
367
462
|
const logStep = eventIdentity.phase === 'started' ? `${eventIdentity.action} [started]` : eventIdentity.action;
|
|
368
463
|
const body = appendActivityEntry(section, { time: metadata.timestamp, step: logStep, agent: normalized.agent, note: eventIdentity.note });
|
|
369
464
|
const frontmatterSet = { current_step: step, assigned_to: normalized.agent };
|
|
465
|
+
let frontmatterRemove;
|
|
466
|
+
if (eventIdentity.phase === 'started' && eventIdentity.family === 'code') {
|
|
467
|
+
const planInput = artifactContext?.inputs.find((input) => input.family === 'plan');
|
|
468
|
+
if (!planInput)
|
|
469
|
+
return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: 'code.started plan input context is unavailable' }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: step, action: eventIdentity.action, phase: eventIdentity.phase, artifactContext });
|
|
470
|
+
try {
|
|
471
|
+
frontmatterSet.code_input_artifact = planInput.name;
|
|
472
|
+
frontmatterSet.code_input_sha256 = sha256File(planInput.path);
|
|
473
|
+
}
|
|
474
|
+
catch (error) {
|
|
475
|
+
return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: `cannot hash code.started plan input: ${error instanceof Error ? error.message : String(error)}` }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: step, action: eventIdentity.action, phase: eventIdentity.phase, artifactContext });
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
else if (eventIdentity.phase === 'completed' && eventIdentity.family === 'code') {
|
|
479
|
+
frontmatterRemove = ['code_input_artifact', 'code_input_sha256'];
|
|
480
|
+
}
|
|
481
|
+
else if (eventIdentity.phase === 'started' && eventIdentity.family.startsWith('review-')) {
|
|
482
|
+
const expectedFamily = reviewInputFamily(eventIdentity.family);
|
|
483
|
+
const input = artifactContext?.inputs.find((candidate) => candidate.family === expectedFamily);
|
|
484
|
+
if (!input)
|
|
485
|
+
return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: `review.started ${expectedFamily} input context is unavailable` }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: step, action: eventIdentity.action, phase: eventIdentity.phase, artifactContext });
|
|
486
|
+
try {
|
|
487
|
+
frontmatterSet.review_input_artifact = input.name;
|
|
488
|
+
frontmatterSet.review_input_sha256 = sha256File(input.path);
|
|
489
|
+
}
|
|
490
|
+
catch (error) {
|
|
491
|
+
return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: `cannot hash review.started input: ${error instanceof Error ? error.message : String(error)}` }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: step, action: eventIdentity.action, phase: eventIdentity.phase, artifactContext });
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
else if (eventIdentity.phase === 'completed' && eventIdentity.family.startsWith('review-')) {
|
|
495
|
+
frontmatterRemove = ['review_input_artifact', 'review_input_sha256'];
|
|
496
|
+
}
|
|
370
497
|
if (eventIdentity.phase === 'started' && normalized.implementationInput)
|
|
371
498
|
frontmatterSet.last_reviewed_commit = '';
|
|
372
499
|
const mutations = [
|
|
373
|
-
{ kind: 'frontmatter', set: frontmatterSet }
|
|
500
|
+
{ kind: 'frontmatter', set: frontmatterSet, remove: frontmatterRemove }
|
|
374
501
|
];
|
|
375
502
|
if (completedArtifact) {
|
|
376
503
|
const link = buildArtifactLinkSection(content, completedArtifact);
|
|
377
504
|
mutations.push({ kind: 'section', aliases: link.aliases, heading: link.heading, body: link.body });
|
|
378
505
|
}
|
|
506
|
+
if (completionReceipt) {
|
|
507
|
+
try {
|
|
508
|
+
const receiptSection = upsertArtifactReceipt(content, completionReceipt);
|
|
509
|
+
mutations.push({ kind: 'section', aliases: receiptSection.aliases, heading: receiptSection.heading, body: receiptSection.body });
|
|
510
|
+
}
|
|
511
|
+
catch (error) {
|
|
512
|
+
const message = error instanceof ArtifactReceiptError ? error.message : String(error);
|
|
513
|
+
return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
514
|
+
}
|
|
515
|
+
}
|
|
379
516
|
if (eventIdentity.phase === 'completed' && normalized.implementationInput) {
|
|
380
517
|
let implementationRows;
|
|
381
518
|
try {
|
package/dist/lib/task/ledger.js
CHANGED
|
@@ -31,8 +31,20 @@ function validateLedgerRows(rows) {
|
|
|
31
31
|
if (seen.has(row.id))
|
|
32
32
|
return { code: 'LEDGER_DUPLICATE_ID', message: `duplicate ledger id '${row.id}'` };
|
|
33
33
|
seen.add(row.id);
|
|
34
|
-
if (/^PRC-[1-9]\d*$/.test(row.id))
|
|
34
|
+
if (/^PRC-[1-9]\d*$/.test(row.id)) {
|
|
35
|
+
if (row.stage !== 'post-review-commit' ||
|
|
36
|
+
row.round !== '-' ||
|
|
37
|
+
row.severity !== '-' ||
|
|
38
|
+
row.status !== 'human-decided' ||
|
|
39
|
+
row.evidence === '' ||
|
|
40
|
+
/[\r\n]/.test(row.evidence)) {
|
|
41
|
+
return { code: 'LEDGER_DOCUMENT_INVALID', message: `post-review exemption row '${row.id}' is invalid` };
|
|
42
|
+
}
|
|
35
43
|
continue;
|
|
44
|
+
}
|
|
45
|
+
if (row.stage === 'post-review-commit') {
|
|
46
|
+
return { code: 'LEDGER_DOCUMENT_INVALID', message: `post-review exemption row '${row.id}' must use a PRC id` };
|
|
47
|
+
}
|
|
36
48
|
if (!/^(AN|PL|CD|HD)-[1-9]\d*$/.test(row.id))
|
|
37
49
|
return { code: 'LEDGER_ID_INVALID', message: `ledger id '${row.id}' is invalid` };
|
|
38
50
|
if (!['analysis', 'plan', 'code', 'post-review-commit'].includes(row.stage))
|