@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,12 +7,41 @@ type DelegationRole = 'executor' | 'reviewer';
|
|
|
7
7
|
type DelegationStage = 'analysis' | 'review-analysis' | 'plan' | 'review-plan' | 'code' | 'review-code' | 'commit';
|
|
8
8
|
type DelegationStatus = 'prepared' | 'activated' | 'stage-completed' | 'sealed' | 'consumed' | 'aborted' | 'expired';
|
|
9
9
|
type DelegationHostEvidence = Readonly<{
|
|
10
|
-
kind: 'codex-lifecycle-v1';
|
|
10
|
+
kind: 'codex-lifecycle-v1' | 'codex-lifecycle-v2';
|
|
11
11
|
hookDefinitionHash: string;
|
|
12
12
|
startRevision: number;
|
|
13
13
|
stopRevision: number | null;
|
|
14
14
|
consumer: string | null;
|
|
15
15
|
consumedAt: string | null;
|
|
16
|
+
protocolVersion?: number;
|
|
17
|
+
packageVersion?: string;
|
|
18
|
+
internalExecutableBuildHash?: string;
|
|
19
|
+
lifecycleContractHash?: string;
|
|
20
|
+
hookSource?: 'project' | 'managed' | 'isolated-user';
|
|
21
|
+
hookSourcePathDigest?: string;
|
|
22
|
+
hookSourceHash?: string;
|
|
23
|
+
capabilitySessionId?: string;
|
|
24
|
+
capabilityTurnId?: string;
|
|
25
|
+
capabilityToolUseId?: string;
|
|
26
|
+
spawnToolUseId?: string;
|
|
27
|
+
spawnObservedAt?: string;
|
|
28
|
+
controllerInstanceDigest?: string | null;
|
|
29
|
+
controlGeneration?: string | null;
|
|
30
|
+
}>;
|
|
31
|
+
type DelegationLifecycleProvenance = Readonly<{
|
|
32
|
+
protocolVersion: number;
|
|
33
|
+
packageVersion: string;
|
|
34
|
+
internalExecutableBuildHash: string;
|
|
35
|
+
lifecycleContractHash: string;
|
|
36
|
+
hookDefinitionHash: string;
|
|
37
|
+
hookSource: 'project' | 'managed' | 'isolated-user';
|
|
38
|
+
hookSourcePathDigest: string;
|
|
39
|
+
hookSourceHash: string;
|
|
40
|
+
capabilitySessionId: string;
|
|
41
|
+
capabilityTurnId: string;
|
|
42
|
+
capabilityToolUseId: string;
|
|
43
|
+
controllerInstanceDigest: string | null;
|
|
44
|
+
controlGeneration: string | null;
|
|
16
45
|
}>;
|
|
17
46
|
|
|
18
47
|
type DelegationReceipt = Readonly<{
|
|
@@ -36,11 +65,19 @@ type DelegationReceipt = Readonly<{
|
|
|
36
65
|
agent: string | null;
|
|
37
66
|
status: DelegationStatus;
|
|
38
67
|
workspaceSnapshotScope?: 'task';
|
|
68
|
+
lifecycleProvenance?: DelegationLifecycleProvenance | null;
|
|
39
69
|
hostEvidence?: DelegationHostEvidence | null;
|
|
40
70
|
beforeFingerprint: string;
|
|
41
71
|
afterFingerprint: string | null;
|
|
42
72
|
changedPaths: readonly string[];
|
|
43
73
|
createdAt: string;
|
|
74
|
+
preparedMonotonicMs: number;
|
|
75
|
+
spawnDispatchMonotonicMs: number | null;
|
|
76
|
+
activationDeadlineMonotonicMs: number | null;
|
|
77
|
+
spawnDispatchedAt: string | null;
|
|
78
|
+
activationDeadlineAt: string | null;
|
|
79
|
+
startEvidenceMonotonicMs: number | null;
|
|
80
|
+
activatedMonotonicMs: number | null;
|
|
44
81
|
activatedAt: string | null;
|
|
45
82
|
sealedAt: string | null;
|
|
46
83
|
consumedAt: string | null;
|
|
@@ -64,9 +101,10 @@ function fail(code: string, message: string): ReceiptFailure {
|
|
|
64
101
|
}
|
|
65
102
|
|
|
66
103
|
function prepareDelegation(
|
|
67
|
-
input: Omit<DelegationReceipt, 'id' | 'requestedModel' | 'requestedReasoningEffort' | 'actualModel' | 'actualReasoningEffort' | 'modelFallbackReason' | 'reasoningEffortFallbackReason' | 'parentId' | 'childId' | 'spawnMode' | 'agent' | 'status' | 'hostEvidence' | 'afterFingerprint' | 'changedPaths' | 'createdAt' | 'activatedAt' | 'sealedAt' | 'consumedAt'> & Readonly<{ requestedModel: string; requestedReasoningEffort: string }>,
|
|
68
|
-
options: { id?: () => string; now?: () => string } = {}
|
|
104
|
+
input: Omit<DelegationReceipt, 'id' | 'requestedModel' | 'requestedReasoningEffort' | 'actualModel' | 'actualReasoningEffort' | 'modelFallbackReason' | 'reasoningEffortFallbackReason' | 'parentId' | 'childId' | 'spawnMode' | 'agent' | 'status' | 'hostEvidence' | 'afterFingerprint' | 'changedPaths' | 'createdAt' | 'preparedMonotonicMs' | 'spawnDispatchMonotonicMs' | 'activationDeadlineMonotonicMs' | 'spawnDispatchedAt' | 'activationDeadlineAt' | 'startEvidenceMonotonicMs' | 'activatedMonotonicMs' | 'activatedAt' | 'sealedAt' | 'consumedAt'> & Readonly<{ requestedModel: string; requestedReasoningEffort: string }>,
|
|
105
|
+
options: { id?: () => string; now?: () => string; monotonicNow?: () => number } = {}
|
|
69
106
|
): DelegationReceipt {
|
|
107
|
+
const monotonic = (options.monotonicNow ?? (() => Number(process.hrtime.bigint() / 1_000_000n)))();
|
|
70
108
|
return Object.freeze({
|
|
71
109
|
...input,
|
|
72
110
|
id: (options.id ?? randomUUID)(),
|
|
@@ -83,12 +121,41 @@ function prepareDelegation(
|
|
|
83
121
|
afterFingerprint: null,
|
|
84
122
|
changedPaths: Object.freeze([]),
|
|
85
123
|
createdAt: (options.now ?? (() => new Date().toISOString()))(),
|
|
124
|
+
preparedMonotonicMs: monotonic,
|
|
125
|
+
spawnDispatchMonotonicMs: null,
|
|
126
|
+
activationDeadlineMonotonicMs: null,
|
|
127
|
+
spawnDispatchedAt: null,
|
|
128
|
+
activationDeadlineAt: null,
|
|
129
|
+
startEvidenceMonotonicMs: null,
|
|
130
|
+
activatedMonotonicMs: null,
|
|
86
131
|
activatedAt: null,
|
|
87
132
|
sealedAt: null,
|
|
88
133
|
consumedAt: null
|
|
89
134
|
});
|
|
90
135
|
}
|
|
91
136
|
|
|
137
|
+
function dispatchDelegation(
|
|
138
|
+
receipt: DelegationReceipt,
|
|
139
|
+
options: { now?: () => string; monotonicNow?: () => number; activationWindowMs?: number } = {}
|
|
140
|
+
): ReceiptResult {
|
|
141
|
+
if (receipt.status !== 'prepared') {
|
|
142
|
+
return fail('DELEGATION_STATE_INVALID', `delegation ${receipt.id} is ${receipt.status}, expected prepared`);
|
|
143
|
+
}
|
|
144
|
+
if (receipt.spawnDispatchMonotonicMs != null || receipt.spawnDispatchedAt != null) {
|
|
145
|
+
return fail('DELEGATION_DISPATCH_REPLAY', `delegation ${receipt.id} was already dispatched`);
|
|
146
|
+
}
|
|
147
|
+
const monotonic = (options.monotonicNow ?? (() => Number(process.hrtime.bigint() / 1_000_000n)))();
|
|
148
|
+
const dispatchedAt = (options.now ?? (() => new Date().toISOString()))();
|
|
149
|
+
const window = options.activationWindowMs ?? 15_000;
|
|
150
|
+
return { ok: true, receipt: Object.freeze({
|
|
151
|
+
...receipt,
|
|
152
|
+
spawnDispatchMonotonicMs: monotonic,
|
|
153
|
+
activationDeadlineMonotonicMs: monotonic + window,
|
|
154
|
+
spawnDispatchedAt: dispatchedAt,
|
|
155
|
+
activationDeadlineAt: new Date(Date.parse(dispatchedAt) + window).toISOString()
|
|
156
|
+
}) };
|
|
157
|
+
}
|
|
158
|
+
|
|
92
159
|
function activateDelegation(
|
|
93
160
|
receipt: DelegationReceipt,
|
|
94
161
|
event: Readonly<{
|
|
@@ -101,16 +168,45 @@ function activateDelegation(
|
|
|
101
168
|
modelFallbackReason?: string;
|
|
102
169
|
reasoningEffortFallbackReason?: string;
|
|
103
170
|
hostEvidence?: Readonly<{
|
|
104
|
-
kind: 'codex-lifecycle-v1';
|
|
171
|
+
kind: 'codex-lifecycle-v1' | 'codex-lifecycle-v2';
|
|
105
172
|
hookDefinitionHash: string;
|
|
106
173
|
startRevision: number;
|
|
174
|
+
protocolVersion?: number;
|
|
175
|
+
packageVersion?: string;
|
|
176
|
+
internalExecutableBuildHash?: string;
|
|
177
|
+
lifecycleContractHash?: string;
|
|
178
|
+
hookSource?: 'project' | 'managed' | 'isolated-user';
|
|
179
|
+
hookSourcePathDigest?: string;
|
|
180
|
+
hookSourceHash?: string;
|
|
181
|
+
capabilitySessionId?: string;
|
|
182
|
+
capabilityTurnId?: string;
|
|
183
|
+
capabilityToolUseId?: string;
|
|
184
|
+
spawnToolUseId?: string;
|
|
185
|
+
spawnObservedAt?: string;
|
|
186
|
+
controllerInstanceDigest?: string | null;
|
|
187
|
+
controlGeneration?: string | null;
|
|
107
188
|
}>;
|
|
108
189
|
}>,
|
|
109
|
-
options: { now?: () => string } = {}
|
|
190
|
+
options: { now?: () => string; monotonicNow?: () => number } = {}
|
|
110
191
|
): ReceiptResult {
|
|
111
192
|
const managedRole = managedDelegationRole(event.nativeAgent);
|
|
112
193
|
if (!managedRole) return fail('DELEGATION_IGNORED', `subagent '${event.nativeAgent}' is not lifecycle-managed`);
|
|
113
194
|
if (receipt.status !== 'prepared') return fail('DELEGATION_STATE_INVALID', `delegation ${receipt.id} is ${receipt.status}, expected prepared`);
|
|
195
|
+
if (
|
|
196
|
+
receipt.spawnDispatchMonotonicMs == null
|
|
197
|
+
|| receipt.spawnDispatchedAt == null
|
|
198
|
+
|| receipt.activationDeadlineAt == null
|
|
199
|
+
|| receipt.activationDeadlineMonotonicMs == null
|
|
200
|
+
|| !Number.isFinite(Date.parse(receipt.spawnDispatchedAt))
|
|
201
|
+
|| !Number.isFinite(Date.parse(receipt.activationDeadlineAt))
|
|
202
|
+
) {
|
|
203
|
+
return fail('DELEGATION_NOT_DISPATCHED', `delegation ${receipt.id} has not reached the native spawn dispatch boundary`);
|
|
204
|
+
}
|
|
205
|
+
const monotonic = (options.monotonicNow ?? (() => Number(process.hrtime.bigint() / 1_000_000n)))();
|
|
206
|
+
const wallNow = Date.parse((options.now ?? (() => new Date().toISOString()))());
|
|
207
|
+
if (wallNow > Date.parse(receipt.activationDeadlineAt)) {
|
|
208
|
+
return fail('DELEGATION_ACTIVATION_TIMEOUT', `delegation ${receipt.id} activation evidence arrived after its deadline`);
|
|
209
|
+
}
|
|
114
210
|
if (managedRole !== receipt.role) return fail('DELEGATION_ROLE_MISMATCH', `managed role ${managedRole} does not match ${receipt.role}`);
|
|
115
211
|
if (!event.parentId || (receipt.parentId !== null && event.parentId !== receipt.parentId) || !event.childId || event.childId === event.parentId) {
|
|
116
212
|
return fail('DELEGATION_IDENTITY_INVALID', 'native parent/child identity does not match the prepared delegation');
|
|
@@ -127,6 +223,10 @@ function activateDelegation(
|
|
|
127
223
|
if (!actualReasoningEffort || actualReasoningEffort.trim() !== actualReasoningEffort) {
|
|
128
224
|
return fail('DELEGATION_REASONING_EFFORT_MISSING', 'native start event must provide a non-empty actual reasoning effort');
|
|
129
225
|
}
|
|
226
|
+
if (
|
|
227
|
+
receipt.client === 'codex'
|
|
228
|
+
&& event.hostEvidence?.kind !== 'codex-lifecycle-v2'
|
|
229
|
+
) return fail('DELEGATION_HOST_EVIDENCE_REQUIRED', 'Codex activation requires trusted lifecycle-v2 host evidence');
|
|
130
230
|
if (
|
|
131
231
|
actualReasoningEffort !== receipt.requestedReasoningEffort
|
|
132
232
|
&& (!event.reasoningEffortFallbackReason || event.reasoningEffortFallbackReason.trim() === '')
|
|
@@ -148,6 +248,33 @@ function activateDelegation(
|
|
|
148
248
|
|| event.hostEvidence.startRevision < 1
|
|
149
249
|
)
|
|
150
250
|
) return fail('DELEGATION_HOST_EVIDENCE_INVALID', 'Codex start evidence reference is invalid');
|
|
251
|
+
if (event.hostEvidence?.kind === 'codex-lifecycle-v2') {
|
|
252
|
+
const expected = receipt.lifecycleProvenance;
|
|
253
|
+
const spawnObservedAt = Date.parse(event.hostEvidence.spawnObservedAt ?? '');
|
|
254
|
+
const spawnDispatchedAt = Date.parse(receipt.spawnDispatchedAt);
|
|
255
|
+
const activationDeadlineAt = Date.parse(receipt.activationDeadlineAt);
|
|
256
|
+
if (
|
|
257
|
+
!expected
|
|
258
|
+
|| event.hostEvidence.protocolVersion !== expected.protocolVersion
|
|
259
|
+
|| event.hostEvidence.packageVersion !== expected.packageVersion
|
|
260
|
+
|| event.hostEvidence.internalExecutableBuildHash !== expected.internalExecutableBuildHash
|
|
261
|
+
|| event.hostEvidence.lifecycleContractHash !== expected.lifecycleContractHash
|
|
262
|
+
|| event.hostEvidence.hookDefinitionHash !== expected.hookDefinitionHash
|
|
263
|
+
|| event.hostEvidence.hookSource !== expected.hookSource
|
|
264
|
+
|| event.hostEvidence.hookSourcePathDigest !== expected.hookSourcePathDigest
|
|
265
|
+
|| event.hostEvidence.hookSourceHash !== expected.hookSourceHash
|
|
266
|
+
|| event.hostEvidence.capabilitySessionId !== expected.capabilitySessionId
|
|
267
|
+
|| event.hostEvidence.capabilityTurnId !== expected.capabilityTurnId
|
|
268
|
+
|| !event.hostEvidence.spawnToolUseId?.trim()
|
|
269
|
+
|| event.hostEvidence.spawnToolUseId === expected.capabilityToolUseId
|
|
270
|
+
|| !Number.isFinite(spawnObservedAt)
|
|
271
|
+
|| spawnObservedAt < spawnDispatchedAt
|
|
272
|
+
|| spawnObservedAt > activationDeadlineAt
|
|
273
|
+
|| event.parentId !== expected.capabilitySessionId
|
|
274
|
+
|| (event.hostEvidence.controllerInstanceDigest ?? null) !== expected.controllerInstanceDigest
|
|
275
|
+
|| (event.hostEvidence.controlGeneration ?? null) !== expected.controlGeneration
|
|
276
|
+
) return fail('DELEGATION_HOST_EVIDENCE_INVALID', 'Codex lifecycle provenance does not match the prepared receipt');
|
|
277
|
+
}
|
|
151
278
|
return { ok: true, receipt: Object.freeze({
|
|
152
279
|
...receipt,
|
|
153
280
|
status: 'activated',
|
|
@@ -164,10 +291,22 @@ function activateDelegation(
|
|
|
164
291
|
consumer: null,
|
|
165
292
|
consumedAt: null
|
|
166
293
|
}) : receipt.hostEvidence ?? null,
|
|
167
|
-
|
|
294
|
+
startEvidenceMonotonicMs: monotonic,
|
|
295
|
+
activatedMonotonicMs: (options.monotonicNow ?? (() => Number(process.hrtime.bigint() / 1_000_000n)))(),
|
|
296
|
+
activatedAt: new Date(wallNow).toISOString()
|
|
168
297
|
}) };
|
|
169
298
|
}
|
|
170
299
|
|
|
300
|
+
function abortPreparedDelegation(receipt: DelegationReceipt): ReceiptResult {
|
|
301
|
+
if (receipt.status !== 'prepared') {
|
|
302
|
+
return fail('DELEGATION_STATE_INVALID', `delegation ${receipt.id} is ${receipt.status}, expected prepared`);
|
|
303
|
+
}
|
|
304
|
+
return {
|
|
305
|
+
ok: true,
|
|
306
|
+
receipt: Object.freeze({ ...receipt, status: 'aborted' as const })
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
171
310
|
function completeDelegationStage(
|
|
172
311
|
receipt: DelegationReceipt,
|
|
173
312
|
event: Readonly<{ stage: DelegationStage; round: number; artifact: string; agent: string }>
|
|
@@ -191,9 +330,12 @@ function sealDelegation(
|
|
|
191
330
|
changedPaths: readonly string[];
|
|
192
331
|
hostEvidence?: Readonly<{ stopRevision: number; consumer: string; consumedAt: string }>;
|
|
193
332
|
}>,
|
|
194
|
-
options: { now?: () => string } = {}
|
|
333
|
+
options: { now?: () => string; requireHostEvidence?: boolean } = {}
|
|
195
334
|
): ReceiptResult {
|
|
196
335
|
if (receipt.status !== 'stage-completed') return fail('DELEGATION_STATE_INVALID', `delegation ${receipt.id} is ${receipt.status}, expected stage-completed`);
|
|
336
|
+
if (receipt.client === 'codex' && options.requireHostEvidence !== false && !event.hostEvidence) {
|
|
337
|
+
return fail('DELEGATION_HOST_EVIDENCE_REQUIRED', 'Codex sealing requires consumed lifecycle-v2 host evidence');
|
|
338
|
+
}
|
|
197
339
|
if (event.childId !== receipt.childId || event.exitCode !== 0) return fail('DELEGATION_STOP_INVALID', 'native stop identity or exit status is invalid');
|
|
198
340
|
if (receipt.role === 'reviewer') {
|
|
199
341
|
const taskRoot = `.agents/workspace/active/${receipt.taskId}/`;
|
|
@@ -239,10 +381,20 @@ function consumeDelegation(receipt: DelegationReceipt, options: { now?: () => st
|
|
|
239
381
|
|
|
240
382
|
export {
|
|
241
383
|
activateDelegation,
|
|
384
|
+
abortPreparedDelegation,
|
|
242
385
|
completeDelegationStage,
|
|
243
386
|
consumeDelegation,
|
|
387
|
+
dispatchDelegation,
|
|
244
388
|
managedDelegationRole,
|
|
245
389
|
prepareDelegation,
|
|
246
390
|
sealDelegation
|
|
247
391
|
};
|
|
248
|
-
export type {
|
|
392
|
+
export type {
|
|
393
|
+
DelegationHostEvidence,
|
|
394
|
+
DelegationLifecycleProvenance,
|
|
395
|
+
DelegationReceipt,
|
|
396
|
+
DelegationRole,
|
|
397
|
+
DelegationStage,
|
|
398
|
+
DelegationStatus,
|
|
399
|
+
ReceiptResult
|
|
400
|
+
};
|
package/lib/task/events.ts
CHANGED
|
@@ -4,11 +4,16 @@ import { appendActivityEntry, locateActivityLog, pairEntries } from './activity-
|
|
|
4
4
|
import {
|
|
5
5
|
buildArtifactLinkSection,
|
|
6
6
|
artifactName,
|
|
7
|
+
inspectArtifactDirectory,
|
|
8
|
+
parseReviewedInputReference,
|
|
9
|
+
parseCodePlanInputReference,
|
|
7
10
|
parseArtifactName,
|
|
8
11
|
resolveArtifactContext,
|
|
9
12
|
validateCompletedArtifact
|
|
10
13
|
} from './artifact-lifecycle.ts';
|
|
11
14
|
import type { ArtifactContextResult, ArtifactErrorCode, ArtifactFamily, ArtifactIdentity } from './artifact-lifecycle.ts';
|
|
15
|
+
import { ArtifactReceiptError, sha256File, upsertArtifactReceipt } from './artifact-receipts.ts';
|
|
16
|
+
import type { ArtifactReceipt } from './artifact-receipts.ts';
|
|
12
17
|
import { parseTypedTaskFrontmatter } from './frontmatter.ts';
|
|
13
18
|
import {
|
|
14
19
|
consumeImplementationInput,
|
|
@@ -273,6 +278,80 @@ function openStartedIdentity(rows: ReturnType<typeof pairEntries>, family: Event
|
|
|
273
278
|
return matches.length === 1 ? matches[0] : matches.length > 1 ? { conflict: true as const } : null;
|
|
274
279
|
}
|
|
275
280
|
|
|
281
|
+
function reviewInputFamily(family: EventFamily): ArtifactFamily {
|
|
282
|
+
return family === 'review-analysis' ? 'analysis' : family === 'review-plan' ? 'plan' : 'code';
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function buildCompletionReceipt(
|
|
286
|
+
taskDir: string,
|
|
287
|
+
family: EventFamily,
|
|
288
|
+
artifact: ArtifactIdentity,
|
|
289
|
+
completedAt: string,
|
|
290
|
+
frontmatter: Record<string, unknown>
|
|
291
|
+
): { ok: true; receipt: ArtifactReceipt } | { ok: false; message: string } | null {
|
|
292
|
+
if (family === 'code') {
|
|
293
|
+
let content: string;
|
|
294
|
+
try { content = fs.readFileSync(artifact.path, 'utf8'); }
|
|
295
|
+
catch (error) { return { ok: false, message: `cannot read ${artifact.name}: ${String(error)}` }; }
|
|
296
|
+
const input = parseCodePlanInputReference(content);
|
|
297
|
+
if (!input) return { ok: false, message: `${artifact.name} does not reference a canonical plan artifact` };
|
|
298
|
+
const startedInput = typeof frontmatter.code_input_artifact === 'string' ? frontmatter.code_input_artifact : '';
|
|
299
|
+
const startedSha256 = typeof frontmatter.code_input_sha256 === 'string' ? frontmatter.code_input_sha256 : '';
|
|
300
|
+
if (!startedInput || !startedSha256) return { ok: false, message: 'code.started plan input context is missing' };
|
|
301
|
+
if (input.name !== startedInput) return { ok: false, message: `${artifact.name} plan input '${input.name}' does not match code.started input '${startedInput}'` };
|
|
302
|
+
const plan = inspectArtifactDirectory(taskDir, 'plan');
|
|
303
|
+
if (plan.status !== 'ready' || !plan.latest || plan.latest.name !== input.name) {
|
|
304
|
+
return { ok: false, message: `code input '${input.name}' is not the latest plan artifact` };
|
|
305
|
+
}
|
|
306
|
+
try {
|
|
307
|
+
const inputSha256 = sha256File(plan.latest.path);
|
|
308
|
+
if (inputSha256 !== startedSha256) return { ok: false, message: `code input ${input.name} changed after code.started` };
|
|
309
|
+
return {
|
|
310
|
+
ok: true,
|
|
311
|
+
receipt: {
|
|
312
|
+
event: 'code.completed', output: artifact.name, input: input.name,
|
|
313
|
+
inputSha256, completedAt
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
} catch (error) {
|
|
317
|
+
return { ok: false, message: `cannot hash code input plan: ${error instanceof Error ? error.message : String(error)}` };
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
if (!family.startsWith('review-')) return null;
|
|
321
|
+
const expectedFamily = reviewInputFamily(family);
|
|
322
|
+
let content: string;
|
|
323
|
+
try { content = fs.readFileSync(artifact.path, 'utf8'); }
|
|
324
|
+
catch (error) { return { ok: false, message: `cannot read ${artifact.name}: ${String(error)}` }; }
|
|
325
|
+
const input = parseReviewedInputReference(content, expectedFamily);
|
|
326
|
+
if (!input) return { ok: false, message: `${artifact.name} does not reference a canonical ${expectedFamily} artifact` };
|
|
327
|
+
const startedInput = typeof frontmatter.review_input_artifact === 'string' ? frontmatter.review_input_artifact : '';
|
|
328
|
+
const startedSha256 = typeof frontmatter.review_input_sha256 === 'string' ? frontmatter.review_input_sha256 : '';
|
|
329
|
+
if (!startedInput || !startedSha256) return { ok: false, message: 'review started input context is missing' };
|
|
330
|
+
if (input.name !== startedInput) return { ok: false, message: `${artifact.name} input '${input.name}' does not match review.started input '${startedInput}'` };
|
|
331
|
+
const current = inspectArtifactDirectory(taskDir, expectedFamily);
|
|
332
|
+
if (current.status !== 'ready' || !current.latest || current.latest.name !== input.name) {
|
|
333
|
+
return { ok: false, message: `review input '${input.name}' is not the latest ${expectedFamily} artifact` };
|
|
334
|
+
}
|
|
335
|
+
const event = family === 'review-analysis'
|
|
336
|
+
? 'review-analysis.completed' as const
|
|
337
|
+
: family === 'review-plan' ? 'review-plan.completed' as const : 'review-code.completed' as const;
|
|
338
|
+
try {
|
|
339
|
+
const stat = fs.lstatSync(current.latest.path);
|
|
340
|
+
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('review input is not a regular file');
|
|
341
|
+
const inputSha256 = sha256File(current.latest.path);
|
|
342
|
+
if (inputSha256 !== startedSha256) return { ok: false, message: `review input ${input.name} changed after review.started` };
|
|
343
|
+
return {
|
|
344
|
+
ok: true,
|
|
345
|
+
receipt: {
|
|
346
|
+
event, output: artifact.name, input: input.name,
|
|
347
|
+
inputSha256, completedAt
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
} catch (error) {
|
|
351
|
+
return { ok: false, message: `cannot hash review input ${input.name}: ${error instanceof Error ? error.message : String(error)}` };
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
276
355
|
function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOptions = {}): TaskEventResult {
|
|
277
356
|
const invalid = validateTaskEventRequest(request);
|
|
278
357
|
if (invalid) return failed(request, invalid);
|
|
@@ -371,18 +450,60 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
371
450
|
let metadata;
|
|
372
451
|
try { metadata = (options.metadataProvider ?? captureTaskWriteMetadata)(); }
|
|
373
452
|
catch (error) { return failed(normalized, { code: 'METADATA_CAPTURE_FAILED', message: String(error) }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath }); }
|
|
453
|
+
let completionReceipt: ArtifactReceipt | null = 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
|
+
}
|
|
374
461
|
const step = eventIdentity.phase === 'started' || eventIdentity.target === null ? currentStep : eventIdentity.target;
|
|
375
462
|
const logStep = eventIdentity.phase === 'started' ? `${eventIdentity.action} [started]` : eventIdentity.action;
|
|
376
463
|
const body = appendActivityEntry(section, { time: metadata.timestamp, step: logStep, agent: normalized.agent, note: eventIdentity.note });
|
|
377
464
|
const frontmatterSet: Record<string, string> = { current_step: step, assigned_to: normalized.agent };
|
|
465
|
+
let frontmatterRemove: string[] | undefined;
|
|
466
|
+
if (eventIdentity.phase === 'started' && eventIdentity.family === 'code') {
|
|
467
|
+
const planInput = artifactContext?.inputs.find((input) => input.family === 'plan');
|
|
468
|
+
if (!planInput) 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 });
|
|
469
|
+
try {
|
|
470
|
+
frontmatterSet.code_input_artifact = planInput.name;
|
|
471
|
+
frontmatterSet.code_input_sha256 = sha256File(planInput.path);
|
|
472
|
+
} catch (error) {
|
|
473
|
+
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 });
|
|
474
|
+
}
|
|
475
|
+
} else if (eventIdentity.phase === 'completed' && eventIdentity.family === 'code') {
|
|
476
|
+
frontmatterRemove = ['code_input_artifact', 'code_input_sha256'];
|
|
477
|
+
} else if (eventIdentity.phase === 'started' && eventIdentity.family.startsWith('review-')) {
|
|
478
|
+
const expectedFamily = reviewInputFamily(eventIdentity.family);
|
|
479
|
+
const input = artifactContext?.inputs.find((candidate) => candidate.family === expectedFamily);
|
|
480
|
+
if (!input) 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 });
|
|
481
|
+
try {
|
|
482
|
+
frontmatterSet.review_input_artifact = input.name;
|
|
483
|
+
frontmatterSet.review_input_sha256 = sha256File(input.path);
|
|
484
|
+
} catch (error) {
|
|
485
|
+
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 });
|
|
486
|
+
}
|
|
487
|
+
} else if (eventIdentity.phase === 'completed' && eventIdentity.family.startsWith('review-')) {
|
|
488
|
+
frontmatterRemove = ['review_input_artifact', 'review_input_sha256'];
|
|
489
|
+
}
|
|
378
490
|
if (eventIdentity.phase === 'started' && normalized.implementationInput) frontmatterSet.last_reviewed_commit = '';
|
|
379
491
|
const mutations: Parameters<typeof writeTask>[0]['mutations'][number][] = [
|
|
380
|
-
{ kind: 'frontmatter', set: frontmatterSet }
|
|
492
|
+
{ kind: 'frontmatter', set: frontmatterSet, remove: frontmatterRemove }
|
|
381
493
|
];
|
|
382
494
|
if (completedArtifact) {
|
|
383
495
|
const link = buildArtifactLinkSection(content, completedArtifact);
|
|
384
496
|
mutations.push({ kind: 'section', aliases: link.aliases, heading: link.heading, body: link.body });
|
|
385
497
|
}
|
|
498
|
+
if (completionReceipt) {
|
|
499
|
+
try {
|
|
500
|
+
const receiptSection = upsertArtifactReceipt(content, completionReceipt);
|
|
501
|
+
mutations.push({ kind: 'section', aliases: receiptSection.aliases, heading: receiptSection.heading, body: receiptSection.body });
|
|
502
|
+
} catch (error) {
|
|
503
|
+
const message = error instanceof ArtifactReceiptError ? error.message : String(error);
|
|
504
|
+
return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
505
|
+
}
|
|
506
|
+
}
|
|
386
507
|
if (eventIdentity.phase === 'completed' && normalized.implementationInput) {
|
|
387
508
|
let implementationRows;
|
|
388
509
|
try {
|
package/lib/task/ledger.ts
CHANGED
|
@@ -61,7 +61,22 @@ function validateLedgerRows(rows: readonly LedgerRow[]): LedgerValidationError |
|
|
|
61
61
|
for (const row of rows) {
|
|
62
62
|
if (seen.has(row.id)) return { code: 'LEDGER_DUPLICATE_ID', message: `duplicate ledger id '${row.id}'` };
|
|
63
63
|
seen.add(row.id);
|
|
64
|
-
if (/^PRC-[1-9]\d*$/.test(row.id))
|
|
64
|
+
if (/^PRC-[1-9]\d*$/.test(row.id)) {
|
|
65
|
+
if (
|
|
66
|
+
row.stage !== 'post-review-commit' ||
|
|
67
|
+
row.round !== '-' ||
|
|
68
|
+
row.severity !== '-' ||
|
|
69
|
+
row.status !== 'human-decided' ||
|
|
70
|
+
row.evidence === '' ||
|
|
71
|
+
/[\r\n]/.test(row.evidence)
|
|
72
|
+
) {
|
|
73
|
+
return { code: 'LEDGER_DOCUMENT_INVALID', message: `post-review exemption row '${row.id}' is invalid` };
|
|
74
|
+
}
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (row.stage === 'post-review-commit') {
|
|
78
|
+
return { code: 'LEDGER_DOCUMENT_INVALID', message: `post-review exemption row '${row.id}' must use a PRC id` };
|
|
79
|
+
}
|
|
65
80
|
if (!/^(AN|PL|CD|HD)-[1-9]\d*$/.test(row.id)) return { code: 'LEDGER_ID_INVALID', message: `ledger id '${row.id}' is invalid` };
|
|
66
81
|
if (!['analysis', 'plan', 'code', 'post-review-commit'].includes(row.stage)) return { code: 'LEDGER_STAGE_INVALID', message: `ledger stage '${row.stage}' is invalid` };
|
|
67
82
|
if (!FINDING_STATUSES.has(row.status)) return { code: 'LEDGER_STATUS_INVALID', message: `ledger status '${row.status}' is invalid` };
|