@fitlab-ai/agent-infra 0.11.1 → 0.11.3
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 +7 -2
- package/dist/lib/agent-clients/adapter.js +3 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/recovery.js +234 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +8 -35
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +1 -34
- package/dist/lib/agent-clients/adapters/codex.js +10 -0
- package/dist/lib/internal/cli-route-inventory.js +1 -1
- package/dist/lib/internal/manual-validation.js +22 -27
- package/dist/lib/internal/platform-comment.js +59 -0
- package/dist/lib/internal/sandbox-control.js +2 -171
- package/dist/lib/internal/task-artifact.js +1 -1
- package/dist/lib/internal/task-lifecycle.js +1 -1
- package/dist/lib/internal/task-operation-registry.js +7 -2
- package/dist/lib/internal/task-review.js +5 -5
- package/dist/lib/internal/task-validate.js +0 -6
- package/dist/lib/platform/comment-safety.js +7 -1
- package/dist/lib/platform/github-provider.js +13 -3
- package/dist/lib/platform/issue-comments.js +93 -15
- package/dist/lib/platform/pr-summary.js +7 -2
- package/dist/lib/platform/provider-contract.d.ts +2 -0
- package/dist/lib/platform/provider-validation.js +7 -2
- package/dist/lib/platform/verification-sync.js +19 -2
- package/dist/lib/sandbox/control/client.js +3 -44
- package/dist/lib/sandbox/control/executor.js +14 -117
- package/dist/lib/sandbox/control/finalization-response.js +8 -2
- package/dist/lib/sandbox/control/protocol.js +5 -64
- package/dist/lib/sandbox/control/server.js +29 -206
- package/dist/lib/sandbox/control/state.js +1 -7
- package/dist/lib/sandbox/control/task-view.js +2 -1
- package/dist/lib/task/artifact-command.js +6 -6
- package/dist/lib/task/codex-orchestration.js +10 -208
- package/dist/lib/task/commit-operation.js +3 -18
- package/dist/lib/task/control-authority.js +35 -44
- package/dist/lib/task/control-recovery.js +3 -42
- package/dist/lib/task/delegation-receipts.js +16 -78
- package/dist/lib/task/events.js +89 -237
- package/dist/lib/task/finalization-artifacts.js +1 -3
- package/dist/lib/task/finalization-state.js +2 -1
- package/dist/lib/task/finalization.js +208 -57
- package/dist/lib/task/lifecycle-execution.js +2 -11
- package/dist/lib/task/lifecycle.js +1 -1
- package/dist/lib/task/local-artifact-finalization.js +25 -242
- package/dist/lib/task/manual-validation-receipt.js +8 -3
- package/dist/lib/task/orchestration.js +12 -24
- package/dist/lib/task/review-command.js +0 -1
- package/dist/lib/task/review-finalization.js +46 -211
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapter.ts +46 -0
- package/lib/agent-clients/adapters/codex-lifecycle/recovery.ts +297 -0
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +13 -33
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +1 -44
- package/lib/agent-clients/adapters/codex.ts +10 -0
- package/lib/internal/cli-route-inventory.ts +1 -1
- package/lib/internal/manual-validation.ts +22 -26
- package/lib/internal/platform-comment.ts +38 -0
- package/lib/internal/sandbox-control.ts +1 -185
- package/lib/internal/task-artifact.ts +1 -1
- package/lib/internal/task-lifecycle.ts +2 -2
- package/lib/internal/task-operation-registry.ts +8 -4
- package/lib/internal/task-review.ts +5 -5
- package/lib/internal/task-validate.ts +0 -5
- package/lib/platform/comment-safety.ts +8 -0
- package/lib/platform/github-provider.ts +11 -2
- package/lib/platform/issue-comments.ts +102 -17
- package/lib/platform/pr-summary.ts +10 -2
- package/lib/platform/provider-contract.ts +2 -0
- package/lib/platform/provider-validation.ts +6 -2
- package/lib/platform/verification-sync.ts +19 -2
- package/lib/sandbox/control/client.ts +5 -68
- package/lib/sandbox/control/executor.ts +14 -125
- package/lib/sandbox/control/finalization-response.ts +8 -2
- package/lib/sandbox/control/protocol.ts +7 -83
- package/lib/sandbox/control/server.ts +30 -220
- package/lib/sandbox/control/state.ts +1 -9
- package/lib/sandbox/control/task-view.ts +2 -1
- package/lib/task/artifact-command.ts +6 -7
- package/lib/task/codex-orchestration.ts +8 -241
- package/lib/task/commit-operation.ts +3 -23
- package/lib/task/control-authority.ts +39 -55
- package/lib/task/control-recovery.ts +3 -43
- package/lib/task/delegation-receipts.ts +14 -81
- package/lib/task/events.ts +106 -255
- package/lib/task/finalization-artifacts.ts +1 -3
- package/lib/task/finalization-state.ts +2 -1
- package/lib/task/finalization.ts +221 -65
- package/lib/task/lifecycle-execution.ts +10 -21
- package/lib/task/lifecycle.ts +2 -2
- package/lib/task/local-artifact-finalization.ts +23 -281
- package/lib/task/manual-validation-receipt.ts +8 -2
- package/lib/task/orchestration.ts +30 -53
- package/lib/task/review-command.ts +0 -1
- package/lib/task/review-finalization.ts +45 -193
- package/package.json +2 -2
- package/templates/.agents/rules/lifecycle-orchestration.en.md +23 -9
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +23 -9
- package/templates/.agents/rules/local-artifact-repair.en.md +14 -60
- package/templates/.agents/rules/local-artifact-repair.zh-CN.md +13 -60
- package/templates/.agents/skills/analyze-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/code-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +2 -2
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/complete-task/SKILL.en.md +7 -8
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +7 -8
- package/templates/.agents/skills/plan-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/review-analysis/SKILL.en.md +2 -2
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/review-code/SKILL.en.md +3 -3
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/review-plan/SKILL.en.md +2 -2
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +2 -2
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/run-task/SKILL.en.md +10 -9
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +10 -9
- package/templates/.agents/skills/run-task/reference/host-validation.en.md +2 -2
- package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +2 -2
- package/dist/lib/sandbox/control/task-workflow.js +0 -74
- package/dist/lib/sandbox/control/workflow-executor.js +0 -157
- package/dist/lib/task/lifecycle-recovery.js +0 -549
- package/dist/lib/task/recovery-warning.js +0 -19
- package/dist/lib/task/workflow-command.js +0 -18
- package/dist/lib/task/workflow-dispatch.js +0 -161
- package/lib/sandbox/control/task-workflow.ts +0 -95
- package/lib/sandbox/control/workflow-executor.ts +0 -183
- package/lib/task/lifecycle-recovery.ts +0 -691
- package/lib/task/recovery-warning.ts +0 -21
- package/lib/task/workflow-command.ts +0 -19
- package/lib/task/workflow-dispatch.ts +0 -171
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
|
|
3
|
+
import { createCodexLifecycleStore } from './store.ts';
|
|
4
|
+
import type { StoredCodexLifecycle } from './store.ts';
|
|
5
|
+
import { managedDelegationRole } from '../../../task/delegation-receipts.ts';
|
|
6
|
+
import type { DelegationReceipt } from '../../../task/delegation-receipts.ts';
|
|
7
|
+
import { normalizeAgentToken } from '../../tokens.ts';
|
|
8
|
+
import { locateActivityLog, pairEntries, appendActivityEntry } from '../../../task/activity-log.ts';
|
|
9
|
+
import type { ActivityLogSection, StepRow } from '../../../task/activity-log.ts';
|
|
10
|
+
import { captureTaskWriteMetadata, writeTask } from '../../../task/write.ts';
|
|
11
|
+
import { parseTypedTaskFrontmatter } from '../../../task/frontmatter.ts';
|
|
12
|
+
import { resolveTaskRef } from '../../../task/resolve-ref.ts';
|
|
13
|
+
import { resolveAgentRuntimeStoreRoot } from '../../../runtime/agent-runtime.ts';
|
|
14
|
+
import {
|
|
15
|
+
recoverActivatedOrchestrationDelegationUnderLock,
|
|
16
|
+
readRun
|
|
17
|
+
} from '../../../task/orchestration.ts';
|
|
18
|
+
import type { OrchestrationOptions, OrchestrationRun } from '../../../task/orchestration.ts';
|
|
19
|
+
import type {
|
|
20
|
+
AgentClientLifecycleRecoveryRequest,
|
|
21
|
+
AgentClientLifecycleRecoveryResult
|
|
22
|
+
} from '../../adapter.ts';
|
|
23
|
+
|
|
24
|
+
const ACTIONS = {
|
|
25
|
+
analysis: 'Analyze Task',
|
|
26
|
+
'review-analysis': 'Review Analysis',
|
|
27
|
+
plan: 'Plan Task',
|
|
28
|
+
'review-plan': 'Review Plan',
|
|
29
|
+
code: 'Code Task',
|
|
30
|
+
'review-code': 'Review Code'
|
|
31
|
+
} as const;
|
|
32
|
+
|
|
33
|
+
type RecoveryStage = keyof typeof ACTIONS;
|
|
34
|
+
type LifecycleRecoveryRequest = AgentClientLifecycleRecoveryRequest;
|
|
35
|
+
type LifecycleRecoveryResult = AgentClientLifecycleRecoveryResult;
|
|
36
|
+
type LifecycleRecoveryOptions = Readonly<{
|
|
37
|
+
repoRoot?: string;
|
|
38
|
+
now?: () => string;
|
|
39
|
+
orchestration?: Pick<OrchestrationOptions, 'diagnosticLog' | 'now'>;
|
|
40
|
+
lifecycleStore?: ReturnType<typeof createCodexLifecycleStore>;
|
|
41
|
+
writeTask?: typeof writeTask;
|
|
42
|
+
}>;
|
|
43
|
+
|
|
44
|
+
type RecoveryCandidate = Readonly<{
|
|
45
|
+
receipt: DelegationReceipt;
|
|
46
|
+
row: StepRow;
|
|
47
|
+
}>;
|
|
48
|
+
|
|
49
|
+
function result(
|
|
50
|
+
request: LifecycleRecoveryRequest,
|
|
51
|
+
status: LifecycleRecoveryResult['status'],
|
|
52
|
+
extra: Partial<LifecycleRecoveryResult> = {}
|
|
53
|
+
): LifecycleRecoveryResult {
|
|
54
|
+
return {
|
|
55
|
+
status,
|
|
56
|
+
changed: status === 'applied',
|
|
57
|
+
targetState: 'active',
|
|
58
|
+
requestRef: request.taskRef,
|
|
59
|
+
intent: 'recover-started',
|
|
60
|
+
taskId: null,
|
|
61
|
+
receiptId: null,
|
|
62
|
+
childId: null,
|
|
63
|
+
error: null,
|
|
64
|
+
...extra
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function failure(
|
|
69
|
+
request: LifecycleRecoveryRequest,
|
|
70
|
+
status: 'owner-unknown' | 'conflict',
|
|
71
|
+
code: string,
|
|
72
|
+
message: string,
|
|
73
|
+
extra: Partial<LifecycleRecoveryResult> = {}
|
|
74
|
+
): LifecycleRecoveryResult {
|
|
75
|
+
return result(request, status, { error: { code, message }, ...extra });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function actionPattern(receipt: Pick<DelegationReceipt, 'stage' | 'round'>): RegExp | null {
|
|
79
|
+
const action = ACTIONS[receipt.stage as RecoveryStage];
|
|
80
|
+
if (!action) return null;
|
|
81
|
+
const escaped = action.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
|
|
82
|
+
return new RegExp(`^${escaped} \\(Round ${receipt.round}(?:, (?:fix for review-code(?:-r[2-9]|-r[1-9]\\d+)?.md|decision II-[1-9]\\d*))?\\)$`, 'u');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function openRows(section: ActivityLogSection): StepRow[] {
|
|
86
|
+
return pairEntries([...section.entries]).filter((row) => row.started !== '' && row.done === '');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function rowForReceipt(rows: readonly StepRow[], receipt: DelegationReceipt): StepRow[] {
|
|
90
|
+
const pattern = actionPattern(receipt);
|
|
91
|
+
return pattern ? rows.filter((row) => pattern.test(row.step)) : [];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function readLifecycleStore(
|
|
95
|
+
options: LifecycleRecoveryOptions,
|
|
96
|
+
repoRoot: string
|
|
97
|
+
): ReturnType<typeof createCodexLifecycleStore> {
|
|
98
|
+
return options.lifecycleStore ?? createCodexLifecycleStore({
|
|
99
|
+
root: resolveAgentRuntimeStoreRoot({ repoRoot, store: 'lifecycle' }),
|
|
100
|
+
cliVersion: 'recovery',
|
|
101
|
+
now: options.now
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function readStoredEvidence(
|
|
106
|
+
store: ReturnType<typeof createCodexLifecycleStore>,
|
|
107
|
+
childId: string
|
|
108
|
+
): StoredCodexLifecycle | { missing: true } | { error: Error } {
|
|
109
|
+
try {
|
|
110
|
+
return store.read(childId);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
113
|
+
if (message.includes('was not found uniquely')) return { missing: true };
|
|
114
|
+
return { error: error instanceof Error ? error : new Error(message) };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function validateStopRecord(
|
|
119
|
+
record: StoredCodexLifecycle,
|
|
120
|
+
receipt: DelegationReceipt
|
|
121
|
+
): { ok: true } | { ok: false; code: string; message: string } {
|
|
122
|
+
const start = record.state.startEvidence;
|
|
123
|
+
const stop = record.state.stopEvidence;
|
|
124
|
+
const provenance = receipt.lifecycleProvenance;
|
|
125
|
+
const host = receipt.hostEvidence;
|
|
126
|
+
if (
|
|
127
|
+
receipt.client !== 'codex'
|
|
128
|
+
|| !provenance
|
|
129
|
+
|| host?.kind !== 'codex-lifecycle-v2'
|
|
130
|
+
|| !start
|
|
131
|
+
|| !stop
|
|
132
|
+
|| record.state.status !== 'stop-ready'
|
|
133
|
+
|| stop.terminalStatus !== 'completed'
|
|
134
|
+
|| stop.hookStopObserved !== true
|
|
135
|
+
|| start.childThreadId !== receipt.childId
|
|
136
|
+
|| start.parentThreadId !== receipt.parentId
|
|
137
|
+
|| managedDelegationRole(start.nativeAgent) !== receipt.role
|
|
138
|
+
|| start.hookDefinitionHash !== provenance.hookDefinitionHash
|
|
139
|
+
|| host.hookDefinitionHash !== provenance.hookDefinitionHash
|
|
140
|
+
|| host.capabilitySessionId !== provenance.capabilitySessionId
|
|
141
|
+
|| host.capabilityTurnId !== provenance.capabilityTurnId
|
|
142
|
+
|| host.capabilityToolUseId !== provenance.capabilityToolUseId
|
|
143
|
+
|| host.spawnToolUseId !== start.spawnToolUseId
|
|
144
|
+
|| !host.spawnObservedAt
|
|
145
|
+
|| host.spawnObservedAt !== record.spawnObservedAt
|
|
146
|
+
|| host.controllerInstanceDigest !== provenance.controllerInstanceDigest
|
|
147
|
+
|| host.controlGeneration !== provenance.controlGeneration
|
|
148
|
+
|| typeof host.controllerInstanceDigest !== 'string'
|
|
149
|
+
|| typeof host.controlGeneration !== 'string'
|
|
150
|
+
|| host.startRevision < 1
|
|
151
|
+
|| record.revision <= host.startRevision
|
|
152
|
+
|| record.state.child?.childThreadId !== receipt.childId
|
|
153
|
+
|| record.state.stop?.childThreadId !== receipt.childId
|
|
154
|
+
) {
|
|
155
|
+
return { ok: false, code: 'RECOVERY_STOP_EVIDENCE_INVALID', message: 'Codex stop evidence does not match the activated receipt' };
|
|
156
|
+
}
|
|
157
|
+
if (record.consumer !== null && record.consumer !== receipt.id) {
|
|
158
|
+
return { ok: false, code: 'RECOVERY_CONSUMER_CONFLICT', message: `Codex lifecycle evidence was consumed by '${record.consumer}'` };
|
|
159
|
+
}
|
|
160
|
+
return { ok: true };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function abortedCandidates(run: OrchestrationRun, rows: readonly StepRow[]): RecoveryCandidate[] {
|
|
164
|
+
const candidates = run.receipts.flatMap((receipt) => {
|
|
165
|
+
if (receipt.status !== 'aborted' || receipt.activatedAt === null || receipt.client !== 'codex') return [];
|
|
166
|
+
const matches = rowForReceipt(rows, receipt);
|
|
167
|
+
return matches.length === 1 ? [{ receipt, row: matches[0]! }] : [];
|
|
168
|
+
});
|
|
169
|
+
return candidates;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function appendRecoveryLog(
|
|
173
|
+
request: LifecycleRecoveryRequest,
|
|
174
|
+
taskId: string,
|
|
175
|
+
section: ActivityLogSection,
|
|
176
|
+
candidate: RecoveryCandidate,
|
|
177
|
+
repoRoot: string,
|
|
178
|
+
writer: typeof writeTask
|
|
179
|
+
): LifecycleRecoveryResult {
|
|
180
|
+
const metadata = captureTaskWriteMetadata();
|
|
181
|
+
const body = appendActivityEntry(section, {
|
|
182
|
+
time: metadata.timestamp,
|
|
183
|
+
step: `${candidate.row.step} [aborted]`,
|
|
184
|
+
agent: request.agent,
|
|
185
|
+
note: `terminated Codex delegation; receipt=${candidate.receipt.id}; child=${candidate.receipt.childId}`
|
|
186
|
+
});
|
|
187
|
+
const written = writer({
|
|
188
|
+
taskRef: taskId,
|
|
189
|
+
expectedState: 'active',
|
|
190
|
+
mutations: [{ kind: 'section', aliases: ['活动日志', 'Activity Log'], heading: section.heading, body }]
|
|
191
|
+
}, { repoRoot, metadataProvider: () => metadata });
|
|
192
|
+
if (written.status === 'failed') {
|
|
193
|
+
return failure(request, 'owner-unknown', 'RECOVERY_LOG_WRITE_FAILED', written.error.message, {
|
|
194
|
+
taskId, receiptId: candidate.receipt.id, childId: candidate.receipt.childId
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
return result(request, 'applied', {
|
|
198
|
+
taskId, receiptId: candidate.receipt.id, childId: candidate.receipt.childId
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function recoverStartedLifecycleUnderLock(
|
|
203
|
+
request: LifecycleRecoveryRequest,
|
|
204
|
+
options: LifecycleRecoveryOptions = {}
|
|
205
|
+
): LifecycleRecoveryResult {
|
|
206
|
+
if (!request || request.intent !== 'recover-started' || request.auto !== true || normalizeAgentToken(request.agent) !== 'codex') {
|
|
207
|
+
return failure(request, 'conflict', 'RECOVERY_PAYLOAD_INVALID', 'automatic recovery requires a task and the Codex agent');
|
|
208
|
+
}
|
|
209
|
+
const resolved = resolveTaskRef(request.taskRef, { repoRoot: options.repoRoot });
|
|
210
|
+
if (!resolved.ok) return failure(request, 'conflict', resolved.code, resolved.message);
|
|
211
|
+
const taskId = resolved.taskId;
|
|
212
|
+
let content: string;
|
|
213
|
+
try {
|
|
214
|
+
content = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
215
|
+
parseTypedTaskFrontmatter(content);
|
|
216
|
+
} catch (error) {
|
|
217
|
+
return failure(request, 'conflict', 'RECOVERY_TASK_INVALID', error instanceof Error ? error.message : String(error), { taskId });
|
|
218
|
+
}
|
|
219
|
+
const section = locateActivityLog(content);
|
|
220
|
+
if (!section) return failure(request, 'conflict', 'RECOVERY_LOG_INVALID', 'task has no unique Activity Log section', { taskId });
|
|
221
|
+
let run: OrchestrationRun | null;
|
|
222
|
+
try {
|
|
223
|
+
run = readRun(resolved.taskDir, options.orchestration);
|
|
224
|
+
} catch (error) {
|
|
225
|
+
return failure(request, 'owner-unknown', 'RECOVERY_ORCHESTRATION_UNKNOWN', error instanceof Error ? error.message : String(error), { taskId });
|
|
226
|
+
}
|
|
227
|
+
if (!run) return result(request, 'no-op', { taskId });
|
|
228
|
+
|
|
229
|
+
const rows = openRows(section);
|
|
230
|
+
const pending = run.pendingDelegation;
|
|
231
|
+
if (!pending) {
|
|
232
|
+
const candidates = abortedCandidates(run, rows);
|
|
233
|
+
if (candidates.length === 0) return result(request, 'no-op', { taskId });
|
|
234
|
+
if (candidates.length > 1) {
|
|
235
|
+
return failure(request, 'conflict', 'RECOVERY_CANDIDATE_AMBIGUOUS', 'more than one aborted delegation has an open lifecycle row', { taskId });
|
|
236
|
+
}
|
|
237
|
+
return appendRecoveryLog(request, taskId, section, candidates[0]!, resolved.repoRoot, options.writeTask ?? writeTask);
|
|
238
|
+
}
|
|
239
|
+
if (pending.status !== 'activated') return result(request, 'no-op', { taskId });
|
|
240
|
+
if (pending.client !== 'codex' || run.status !== 'running' || run.pause !== null) {
|
|
241
|
+
return failure(request, 'conflict', 'RECOVERY_ORCHESTRATION_INVALID', 'activated recovery requires one running Codex delegation', {
|
|
242
|
+
taskId, receiptId: pending.id, childId: pending.childId
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
const matchingRows = rowForReceipt(rows, pending);
|
|
246
|
+
if (matchingRows.length !== 1 || normalizeAgentToken(matchingRows[0]!.agent) !== 'codex' || matchingRows[0]!.note !== 'started') {
|
|
247
|
+
return failure(request, 'conflict', 'RECOVERY_LOG_CONFLICT', 'activated delegation does not have one matching open lifecycle row', {
|
|
248
|
+
taskId, receiptId: pending.id, childId: pending.childId
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
if (!pending.childId) {
|
|
252
|
+
return failure(request, 'conflict', 'RECOVERY_DELEGATION_INVALID', 'activated delegation has no child identity', { taskId, receiptId: pending.id });
|
|
253
|
+
}
|
|
254
|
+
const store = readLifecycleStore(options, resolved.repoRoot);
|
|
255
|
+
const stored = readStoredEvidence(store, pending.childId);
|
|
256
|
+
if ('error' in stored) return failure(request, 'owner-unknown', 'RECOVERY_STORE_UNKNOWN', stored.error.message, { taskId, receiptId: pending.id, childId: pending.childId });
|
|
257
|
+
if ('missing' in stored) return failure(request, 'owner-unknown', 'RECOVERY_STOP_EVIDENCE_MISSING', 'matching Codex lifecycle stop evidence is missing', { taskId, receiptId: pending.id, childId: pending.childId });
|
|
258
|
+
const evidence = validateStopRecord(stored, pending);
|
|
259
|
+
if (!evidence.ok) return failure(request, evidence.code === 'RECOVERY_CONSUMER_CONFLICT' ? 'conflict' : 'owner-unknown', evidence.code, evidence.message, { taskId, receiptId: pending.id, childId: pending.childId });
|
|
260
|
+
|
|
261
|
+
let consumed = stored;
|
|
262
|
+
if (stored.consumer === null) {
|
|
263
|
+
try {
|
|
264
|
+
consumed = store.consume(pending.childId, pending.id, pending.hostEvidence?.hookDefinitionHash);
|
|
265
|
+
} catch (error) {
|
|
266
|
+
return failure(request, 'owner-unknown', 'RECOVERY_EVIDENCE_CONSUME_FAILED', error instanceof Error ? error.message : String(error), { taskId, receiptId: pending.id, childId: pending.childId });
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (!consumed.consumedAt) {
|
|
270
|
+
return failure(request, 'owner-unknown', 'RECOVERY_EVIDENCE_INVALID', 'consumed stop evidence has no timestamp', { taskId, receiptId: pending.id, childId: pending.childId });
|
|
271
|
+
}
|
|
272
|
+
const recovered = recoverActivatedOrchestrationDelegationUnderLock(taskId, {
|
|
273
|
+
receiptId: pending.id,
|
|
274
|
+
stage: pending.stage,
|
|
275
|
+
round: pending.round,
|
|
276
|
+
artifact: pending.artifact,
|
|
277
|
+
startedAgent: request.agent,
|
|
278
|
+
childId: pending.childId,
|
|
279
|
+
stopRevision: consumed.revision,
|
|
280
|
+
consumer: pending.id,
|
|
281
|
+
consumedAt: consumed.consumedAt
|
|
282
|
+
}, { ...options.orchestration, repoRoot: resolved.repoRoot, now: options.now });
|
|
283
|
+
if (recovered.status === 'failed' || !recovered.run) {
|
|
284
|
+
return failure(request, 'owner-unknown', recovered.error?.code ?? 'RECOVERY_ORCHESTRATION_FAILED', recovered.error?.message ?? 'orchestration recovery failed', { taskId, receiptId: pending.id, childId: pending.childId });
|
|
285
|
+
}
|
|
286
|
+
return appendRecoveryLog(request, taskId, section, { receipt: recovered.run.receipts.at(-1)!, row: matchingRows[0]! }, resolved.repoRoot, options.writeTask ?? writeTask);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function recoverStartedLifecycleFromAdapter(
|
|
290
|
+
request: LifecycleRecoveryRequest,
|
|
291
|
+
options: LifecycleRecoveryOptions = {}
|
|
292
|
+
): LifecycleRecoveryResult {
|
|
293
|
+
return recoverStartedLifecycleUnderLock(request, options);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export { recoverStartedLifecycleFromAdapter, recoverStartedLifecycleUnderLock };
|
|
297
|
+
export type { LifecycleRecoveryOptions, LifecycleRecoveryRequest, LifecycleRecoveryResult };
|
|
@@ -9,10 +9,8 @@ import * as toml from 'smol-toml';
|
|
|
9
9
|
import {
|
|
10
10
|
requestCodexControllerClose,
|
|
11
11
|
requestCodexControllerOpen,
|
|
12
|
-
requestCodexControllerVerify
|
|
13
|
-
requestSandboxControl
|
|
12
|
+
requestCodexControllerVerify
|
|
14
13
|
} from '../../../sandbox/control/client.ts';
|
|
15
|
-
import { readSandboxControlStatus } from '../../../sandbox/control/state.ts';
|
|
16
14
|
import { getProcessStartTime, type ProcessIdentity } from '../../../server/process-state.ts';
|
|
17
15
|
import { LIFECYCLE_PROTOCOL_VERSION, type LifecycleIdentityWarning } from './build-identity.ts';
|
|
18
16
|
import {
|
|
@@ -46,7 +44,7 @@ type ControllerOptions = Readonly<{
|
|
|
46
44
|
control?: ControllerControl;
|
|
47
45
|
now?: () => number;
|
|
48
46
|
codexVersion?: () => string;
|
|
49
|
-
|
|
47
|
+
verifyController?: typeof requestCodexControllerVerify;
|
|
50
48
|
openController?: typeof requestCodexControllerOpen;
|
|
51
49
|
closeController?: typeof requestCodexControllerClose;
|
|
52
50
|
environment?: NodeJS.ProcessEnv;
|
|
@@ -117,40 +115,22 @@ function verifyRuntime(runtimeDir: string): void {
|
|
|
117
115
|
}
|
|
118
116
|
}
|
|
119
117
|
|
|
120
|
-
function
|
|
118
|
+
function verifyControllerBinding(
|
|
119
|
+
context: CodexSandboxControllerContextV2,
|
|
120
|
+
control: ControllerControl,
|
|
121
|
+
requestControllerVerify: typeof requestCodexControllerVerify
|
|
122
|
+
): void {
|
|
121
123
|
verifyRuntime(control.runtimeDir);
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
for (let attempt = 0; attempt < 5; attempt += 1) {
|
|
125
|
-
try {
|
|
126
|
-
status = readSandboxControlStatus(control.statusDir);
|
|
127
|
-
break;
|
|
128
|
-
} catch (error) {
|
|
129
|
-
lastError = error;
|
|
130
|
-
if (attempt < 4) Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
if (!status) {
|
|
134
|
-
throw new Error(`CODEX_SANDBOX_CONTROLLER_CONTROL_UNAVAILABLE: ${String(lastError)}`);
|
|
135
|
-
}
|
|
136
|
-
if (status.generation !== control.generation
|
|
137
|
-
|| status.state !== 'healthy'
|
|
138
|
-
|| Date.now() - status.updatedAt > 1_500) {
|
|
139
|
-
throw new Error('CODEX_SANDBOX_CONTROLLER_CONTROL_UNAVAILABLE');
|
|
140
|
-
}
|
|
141
|
-
const response = requestSandboxControl({
|
|
142
|
-
family: 'task-orchestration',
|
|
143
|
-
args: [taskId, 'status'],
|
|
124
|
+
const verified = requestControllerVerify({
|
|
125
|
+
controllerProof: controllerProofFromContext(context),
|
|
144
126
|
...control,
|
|
145
127
|
timeoutMs: 30_000
|
|
146
128
|
});
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
} catch {
|
|
129
|
+
if (verified.binding.taskId !== context.taskId
|
|
130
|
+
|| verified.binding.controlGeneration !== context.controlGeneration
|
|
131
|
+
|| verified.binding.controllerInstanceDigest !== context.controllerInstanceDigest) {
|
|
151
132
|
throw new Error('CODEX_SANDBOX_CONTROLLER_TASK_BINDING_INVALID');
|
|
152
133
|
}
|
|
153
|
-
if (payload.taskId !== taskId) throw new Error('CODEX_SANDBOX_CONTROLLER_TASK_BINDING_INVALID');
|
|
154
134
|
}
|
|
155
135
|
|
|
156
136
|
function verifyCodexSandboxControllerContextWithWarnings(
|
|
@@ -349,7 +329,7 @@ function prepareCodexSandboxController(
|
|
|
349
329
|
|| opened.lease.controllerProcess.startTime !== parentStartTime) {
|
|
350
330
|
throw new Error('SANDBOX_CONTROL_RESULT_INVALID');
|
|
351
331
|
}
|
|
352
|
-
(options.
|
|
332
|
+
verifyControllerBinding(context, control, options.verifyController ?? requestCodexControllerVerify);
|
|
353
333
|
const contextPath = path.join(home, 'controller-context.json');
|
|
354
334
|
writeCodexSandboxControllerContext(contextPath, context);
|
|
355
335
|
|
|
@@ -43,23 +43,10 @@ type ActiveCodexLifecycleEvidenceQuery = Readonly<{
|
|
|
43
43
|
const LOCK_RETRY_MS = 10;
|
|
44
44
|
const LOCK_TIMEOUT_MS = 1_000;
|
|
45
45
|
const LOCK_STALE_MS = 30_000;
|
|
46
|
-
const RECOVERY_CONSUMER_RE = /^lifecycle-recovery:([^:\r\n]+):([^:\r\n]+)$/u;
|
|
47
|
-
|
|
48
46
|
function digest(value: string): string {
|
|
49
47
|
return crypto.createHash('sha256').update(value).digest('hex');
|
|
50
48
|
}
|
|
51
49
|
|
|
52
|
-
function isRecoveryConsumer(value: string): boolean {
|
|
53
|
-
return RECOVERY_CONSUMER_RE.test(value);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function recoveryConsumer(taskId: string, receiptId: string): string {
|
|
57
|
-
if (!/^[^:\r\n]+$/u.test(taskId) || !/^[^:\r\n]+$/u.test(receiptId)) {
|
|
58
|
-
throw new Error('Codex lifecycle recovery claim identity is invalid');
|
|
59
|
-
}
|
|
60
|
-
return `lifecycle-recovery:${taskId}:${receiptId}`;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
50
|
function readRecord(file: string): StoredCodexLifecycle {
|
|
64
51
|
const value = JSON.parse(fs.readFileSync(file, 'utf8')) as StoredCodexLifecycle;
|
|
65
52
|
if (
|
|
@@ -310,45 +297,15 @@ function createCodexLifecycleStore(options: CodexLifecycleStoreOptions) {
|
|
|
310
297
|
expectedHookDefinitionHash?: string
|
|
311
298
|
): StoredCodexLifecycle {
|
|
312
299
|
if (!consumer.trim()) throw new Error('Codex lifecycle consumer is required');
|
|
313
|
-
if (consumer.startsWith('lifecycle-recovery:')) {
|
|
314
|
-
throw new Error('Codex lifecycle recovery claims require claimRecovery');
|
|
315
|
-
}
|
|
316
300
|
return consumeInternal(childThreadId, consumer, expectedHookDefinitionHash);
|
|
317
301
|
}
|
|
318
302
|
|
|
319
|
-
function claimRecovery(
|
|
320
|
-
childThreadId: string,
|
|
321
|
-
taskId: string,
|
|
322
|
-
receiptId: string,
|
|
323
|
-
expectedHookDefinitionHash?: string
|
|
324
|
-
): StoredCodexLifecycle {
|
|
325
|
-
return consumeInternal(
|
|
326
|
-
childThreadId,
|
|
327
|
-
recoveryConsumer(taskId, receiptId),
|
|
328
|
-
expectedHookDefinitionHash
|
|
329
|
-
);
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
function releaseRecovery(childThreadId: string, consumer: string): boolean {
|
|
333
|
-
if (!/^lifecycle-recovery:[^:\r\n]+:[^:\r\n]+$/.test(consumer)) return false;
|
|
334
|
-
return withWriteLock(() => {
|
|
335
|
-
const matches = findByChild(childThreadId);
|
|
336
|
-
if (matches.length !== 1) return false;
|
|
337
|
-
const file = matches[0]!;
|
|
338
|
-
const current = readRecord(file);
|
|
339
|
-
if (current.state.status !== 'stop-ready' || current.consumer !== consumer || !current.consumedAt) return false;
|
|
340
|
-
fs.unlinkSync(file);
|
|
341
|
-
return true;
|
|
342
|
-
});
|
|
343
|
-
}
|
|
344
|
-
|
|
345
303
|
function expireBefore(cutoff: string): number {
|
|
346
304
|
return withWriteLock(() => {
|
|
347
305
|
let changed = 0;
|
|
348
306
|
for (const file of recordFiles(root)) {
|
|
349
307
|
const current = readRecord(file);
|
|
350
308
|
if (current.updatedAt >= cutoff) continue;
|
|
351
|
-
if (current.consumer && isRecoveryConsumer(current.consumer)) continue;
|
|
352
309
|
if (current.consumer || ['invalid', 'expired', 'stop-ready'].includes(current.state.status)) {
|
|
353
310
|
fs.unlinkSync(file);
|
|
354
311
|
changed += 1;
|
|
@@ -368,7 +325,7 @@ function createCodexLifecycleStore(options: CodexLifecycleStoreOptions) {
|
|
|
368
325
|
});
|
|
369
326
|
}
|
|
370
327
|
|
|
371
|
-
return Object.freeze({ root, apply, applyToSpawn, consume,
|
|
328
|
+
return Object.freeze({ root, apply, applyToSpawn, consume, expireBefore, findByParent, read });
|
|
372
329
|
}
|
|
373
330
|
|
|
374
331
|
export { createCodexLifecycleStore, hasActiveCodexLifecycleEvidence };
|
|
@@ -28,6 +28,16 @@ const codexAdapter = defineAgentClientAdapter({
|
|
|
28
28
|
actualModel: 'app-server',
|
|
29
29
|
actualReasoningEffort: 'app-server'
|
|
30
30
|
},
|
|
31
|
+
orchestrationAdapter: {
|
|
32
|
+
prepareDelegation: async (...args) => {
|
|
33
|
+
const { prepareCodexOrchestrationDelegation } = await import('../../task/codex-orchestration.ts');
|
|
34
|
+
return prepareCodexOrchestrationDelegation(...args);
|
|
35
|
+
},
|
|
36
|
+
recoverStarted: async (...args) => {
|
|
37
|
+
const { recoverStartedLifecycleFromAdapter } = await import('./codex-lifecycle/recovery.ts');
|
|
38
|
+
return recoverStartedLifecycleFromAdapter(...args);
|
|
39
|
+
}
|
|
40
|
+
},
|
|
31
41
|
project: {
|
|
32
42
|
ownedPathPrefixes: ['.codex/'],
|
|
33
43
|
managed: ['.codex/hooks.json', '.codex/agents/'],
|
|
@@ -46,7 +46,7 @@ export const INTERNAL_HANDLER_ROUTE_SELECTORS = Object.freeze({
|
|
|
46
46
|
'platform-security': ['read', 'dismiss'],
|
|
47
47
|
'platform-metadata': ['init-labels', 'init-milestones'],
|
|
48
48
|
'platform-context': ['resolve'],
|
|
49
|
-
'platform-comment': ['list', 'recover', 'owner', 'backfill', 'sync'],
|
|
49
|
+
'platform-comment': ['list', 'recover', 'owner', 'backfill', 'stage-summary', 'sync'],
|
|
50
50
|
'platform-issue': ['inspect', 'create', 'bind', 'sync'],
|
|
51
51
|
'platform-pr': ['inspect', 'summary-context', 'resolve-external', 'create', 'bind', 'skip', 'sync', 'change-report', 'summary-sync', 'sync-in-labels'],
|
|
52
52
|
'platform-pr-review': ['inspect', 'list', 'publish-pr', 'publish-task'],
|
|
@@ -6,7 +6,7 @@ import { resolveTaskRef } from '../task/resolve-ref.ts';
|
|
|
6
6
|
import { applyTaskEvent } from '../task/events.ts';
|
|
7
7
|
import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.ts';
|
|
8
8
|
import { normalizeAgentToken } from '../agent-clients/tokens.ts';
|
|
9
|
-
import { createManualValidationReceipt, manualValidationFinalSummaryDigest, manualValidationFinalSummaryProjectionMatches, readManualValidationReceipt, writeManualValidationReceiptAtomic } from '../task/manual-validation-receipt.ts';
|
|
9
|
+
import { MANUAL_VALIDATION_RECEIPT_PLACEHOLDER, createManualValidationReceipt, manualValidationFinalSummaryDigest, manualValidationFinalSummaryProjectionMatches, readManualValidationReceipt, writeManualValidationReceiptAtomic } from '../task/manual-validation-receipt.ts';
|
|
10
10
|
import { archiveManualValidationGeneration, createManualValidationTransaction, readManualValidationTransaction, retryManualValidationTransaction, summaryPreimageDigest, transitionManualValidationTransaction, validateManualValidationGenerationArchive, writeManualValidationTransactionAtomic } from '../task/manual-validation-transaction.ts';
|
|
11
11
|
import type { ManualValidationTransaction } from '../task/manual-validation-transaction.ts';
|
|
12
12
|
import type { ManualValidationReceipt } from '../task/manual-validation-receipt.ts';
|
|
@@ -43,28 +43,23 @@ function printFailure(format: OutputFormat, error: { code: string; message: stri
|
|
|
43
43
|
process.exitCode = 1;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
const
|
|
47
|
-
const TRAILING_CANONICAL_REPORT_PLACEHOLDER = /\n*<!--\s*canonical-pr-change-report\s*-->\s*$/u;
|
|
46
|
+
const MANUAL_VALIDATION_STATUS_HEADING = /^###\s+(?:⚠️\s+(?:需人工校验|Manual Validation Required)|✅\s+(?:人工验证已通过|无需人工校验|Manual Validation Passed|No Manual Validation Required)|⏳\s+(?:人工验证待收尾|Manual Validation Pending))\s*$/mu;
|
|
48
47
|
|
|
49
|
-
function manualSummaryBody(body: string, phase: 'pending' | 'final'
|
|
48
|
+
function manualSummaryBody(body: string, phase: 'pending' | 'final'): string {
|
|
50
49
|
const chinese = /###\s+(?:⚠️\s+需人工校验|✅\s+(?:人工验证已通过|无需人工校验)|⏳\s+人工验证待收尾)\s*$/mu.test(body);
|
|
51
|
-
const
|
|
52
|
-
? chinese
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
? `${trailingPlaceholder.trim()}\n\n${section}\n\n`
|
|
65
|
-
: `${section}\n\n`;
|
|
66
|
-
});
|
|
67
|
-
return inserted ? `${updated.replace(/\s+$/u, '')}\n` : `${body.replace(/\s+$/u, '')}\n\n${section}\n`;
|
|
50
|
+
const heading = phase === 'pending'
|
|
51
|
+
? chinese ? '### ⏳ 人工验证待收尾' : '### ⏳ Manual Validation Pending'
|
|
52
|
+
: chinese ? '### ✅ 人工验证已通过' : '### ✅ Manual Validation Passed';
|
|
53
|
+
const marker = phase === 'final'
|
|
54
|
+
? `\n\n${MANUAL_VALIDATION_RECEIPT_PLACEHOLDER}`
|
|
55
|
+
: '';
|
|
56
|
+
if (MANUAL_VALIDATION_STATUS_HEADING.test(body)) {
|
|
57
|
+
return `${body.replace(MANUAL_VALIDATION_STATUS_HEADING, `${heading}${marker}`).replace(/\s+$/u, '')}\n`;
|
|
58
|
+
}
|
|
59
|
+
const fallback = phase === 'pending'
|
|
60
|
+
? chinese ? '人工验证正在等待最终完成。' : 'Manual validation is awaiting final promotion.'
|
|
61
|
+
: chinese ? '人工验证已通过。' : 'Manual validation passed.';
|
|
62
|
+
return `${body.replace(/\s+$/u, '')}\n\n${heading}${marker}\n\n${fallback}\n`;
|
|
68
63
|
}
|
|
69
64
|
|
|
70
65
|
function openManualValidationStarted(taskMdPath: string): { present: boolean; transactionId: string | null } {
|
|
@@ -126,7 +121,9 @@ async function executeManualValidationTransactionLocked(
|
|
|
126
121
|
const pullRequest = currentState.pullRequest;
|
|
127
122
|
const preimageBody = currentState.comment?.body ?? '';
|
|
128
123
|
const preimage = { commentId: currentState.comment?.id ?? null, body: preimageBody, digest: summaryPreimageDigest(preimageBody) };
|
|
129
|
-
const
|
|
124
|
+
const summaryPath = path.resolve(cwd, values.summaryFile!);
|
|
125
|
+
const finalBodyWithoutReceipt = fs.readFileSync(summaryPath, 'utf8');
|
|
126
|
+
const evidenceDigest = sha256File(summaryPath);
|
|
130
127
|
const archiveGeneration = options.archiveGeneration ?? archiveManualValidationGeneration;
|
|
131
128
|
let transactionResult = readManualValidationTransaction(resolved.taskDir, { taskId: resolved.taskId, prNumber: pullRequest.number, prHeadSha: pullRequest.head.sha, evidenceDigest, artifact: values.artifact! });
|
|
132
129
|
let previousTransaction: ManualValidationTransaction | null = null;
|
|
@@ -151,8 +148,7 @@ async function executeManualValidationTransactionLocked(
|
|
|
151
148
|
const transactionId = transactionResult.ok
|
|
152
149
|
? transactionResult.value.transactionId
|
|
153
150
|
: (openStarted.transactionId ?? values.transactionId ?? `mv-${Date.now()}`);
|
|
154
|
-
const
|
|
155
|
-
const pendingBody = manualSummaryBody(finalBodyWithoutReceipt, 'pending', transactionId);
|
|
151
|
+
const pendingBody = manualSummaryBody(finalBodyWithoutReceipt, 'pending');
|
|
156
152
|
const createTransaction = (): ManualValidationTransaction => createManualValidationTransaction({
|
|
157
153
|
transactionId,
|
|
158
154
|
taskId: resolved.taskId,
|
|
@@ -161,7 +157,7 @@ async function executeManualValidationTransactionLocked(
|
|
|
161
157
|
evidenceDigest,
|
|
162
158
|
summaryPreimage: preimage,
|
|
163
159
|
pendingSummaryDigest: summaryPreimageDigest(pendingBody),
|
|
164
|
-
finalSummaryDigest: manualValidationFinalSummaryDigest(manualSummaryBody(finalBodyWithoutReceipt, 'final'
|
|
160
|
+
finalSummaryDigest: manualValidationFinalSummaryDigest(manualSummaryBody(finalBodyWithoutReceipt, 'final')),
|
|
165
161
|
artifact: values.artifact!,
|
|
166
162
|
attempt: 1,
|
|
167
163
|
createdAt: new Date().toISOString(),
|
|
@@ -254,7 +250,7 @@ async function executeManualValidationTransactionLocked(
|
|
|
254
250
|
transitionAndPersist('final-promotion-in-progress');
|
|
255
251
|
}
|
|
256
252
|
const promoted = await syncPullRequestSummary(taskRef, {
|
|
257
|
-
cwd, client: options.client, agent, body: manualSummaryBody(finalBodyWithoutReceipt, 'final'
|
|
253
|
+
cwd, client: options.client, agent, body: manualSummaryBody(finalBodyWithoutReceipt, 'final'), changeReportFile: path.resolve(cwd, values.changeReportFile!), primaryResult, strict: true,
|
|
258
254
|
manualValidation: { phase: 'final', transactionId: transaction.transactionId, receiptDigest: receipt.receiptDigest, prHeadSha: receipt.prHeadSha, authority: 'coordinator' }, lockAlreadyHeld: true
|
|
259
255
|
});
|
|
260
256
|
if (!['applied', 'no-op'].includes(promoted.status)) transactionFailure(promoted.error ?? { code: 'MANUAL_VALIDATION_TRANSACTION_RECOVERY_REQUIRED', message: 'final summary promotion failed' });
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
4
|
|
|
4
5
|
import { normalizeAgentToken, AGENT_USAGE_HINT } from '../agent-clients/tokens.ts';
|
|
5
6
|
import {
|
|
@@ -8,15 +9,19 @@ import {
|
|
|
8
9
|
syncPlatformComment
|
|
9
10
|
} from '../platform/issue-comments.ts';
|
|
10
11
|
import type { CommentKind } from '../platform/issue-comments.ts';
|
|
12
|
+
import { canonicalizeSummaryBody } from '../platform/comment-safety.ts';
|
|
13
|
+
import { platformResult } from '../platform/types.ts';
|
|
11
14
|
import type { PlatformResult } from '../platform/types.ts';
|
|
12
15
|
import { recoverTaskFromComments } from '../platform/task-recovery.ts';
|
|
13
16
|
import { backfillCompletionComments } from '../platform/completion-backfill.ts';
|
|
14
17
|
import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.ts';
|
|
18
|
+
import { resolveTaskRef } from '../task/resolve-ref.ts';
|
|
15
19
|
|
|
16
20
|
const USAGE = `Usage: agent-infra-internal platform-comment list --issue <token> [--cwd <path>]
|
|
17
21
|
agent-infra-internal platform-comment recover --issue <token> --task-id <TASK-id> --output <staging-task.md> [--cwd <path>]
|
|
18
22
|
agent-infra-internal platform-comment owner <task-ref> [--cwd <path>]
|
|
19
23
|
agent-infra-internal platform-comment backfill <task-ref> --agent <agent> [--cwd <path>]
|
|
24
|
+
agent-infra-internal platform-comment stage-summary <task-ref> --body-file <path|-> [--cwd <path>]
|
|
20
25
|
agent-infra-internal platform-comment sync <task-ref> --kind <kind> --agent <agent> [--artifact <file>] [--body-file <path|->] [--status-label <label>] [--backfill] [--cwd <path>]
|
|
21
26
|
`;
|
|
22
27
|
|
|
@@ -64,6 +69,7 @@ async function platformComment(args: string[] = []): Promise<void> {
|
|
|
64
69
|
internalHandlerRoute('platform-comment', 'recover', operation),
|
|
65
70
|
internalHandlerRoute('platform-comment', 'owner', operation),
|
|
66
71
|
internalHandlerRoute('platform-comment', 'backfill', operation),
|
|
72
|
+
internalHandlerRoute('platform-comment', 'stage-summary', operation),
|
|
67
73
|
internalHandlerRoute('platform-comment', 'sync', operation)
|
|
68
74
|
].some(Boolean)) { fail('a valid operation is required'); return; }
|
|
69
75
|
const hasTaskRef = operation !== 'list' && operation !== 'recover';
|
|
@@ -144,6 +150,38 @@ async function platformComment(args: string[] = []): Promise<void> {
|
|
|
144
150
|
finish(await backfillCompletionComments(taskRef!, { cwd, agent: normalizedAgent }));
|
|
145
151
|
return;
|
|
146
152
|
}
|
|
153
|
+
if (internalHandlerRoute('platform-comment', 'stage-summary', operation)) {
|
|
154
|
+
const unexpected = Object.keys(parsed.values).find((key) => !['cwd', 'bodyFile'].includes(key));
|
|
155
|
+
if (unexpected) { fail(`stage-summary does not accept '--${unexpected}'`); return; }
|
|
156
|
+
if (typeof parsed.values.bodyFile !== 'string') { fail('stage-summary requires --body-file'); return; }
|
|
157
|
+
let body: string;
|
|
158
|
+
try { body = readBodyFile(parsed.values.bodyFile, cwd); }
|
|
159
|
+
catch (error) { fail(`unable to read body file: ${error instanceof Error ? error.message : String(error)}`); return; }
|
|
160
|
+
const canonical = canonicalizeSummaryBody(body);
|
|
161
|
+
if (!canonical.ok) { fail(`unable to stage summary: ${canonical.error.code}: ${canonical.error.message}`); return; }
|
|
162
|
+
body = canonical.value;
|
|
163
|
+
const resolved = resolveTaskRef(taskRef!, { repoRoot: cwd });
|
|
164
|
+
if (!resolved.ok || resolved.state !== 'active') { fail('stage-summary requires an active task'); return; }
|
|
165
|
+
const target = path.join(resolved.taskDir, '.delivery-summary.json');
|
|
166
|
+
const value = { taskId: resolved.taskId, body, sha256: createHash('sha256').update(body).digest('hex') };
|
|
167
|
+
const previous = fs.existsSync(target) ? fs.readFileSync(target, 'utf8') : null;
|
|
168
|
+
const next = `${JSON.stringify(value)}\n`;
|
|
169
|
+
if (previous === next) {
|
|
170
|
+
finish(platformResult('no-op'));
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const temporary = `${target}.${process.pid}.${randomUUID()}.tmp`;
|
|
174
|
+
try {
|
|
175
|
+
fs.writeFileSync(temporary, next, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
|
176
|
+
fs.renameSync(temporary, target);
|
|
177
|
+
} catch (error) {
|
|
178
|
+
try { fs.unlinkSync(temporary); } catch { /* preserve primary error */ }
|
|
179
|
+
fail(`unable to stage summary: ${error instanceof Error ? error.message : String(error)}`);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
finish(platformResult('applied', { operations: [{ name: 'delivery-summary', status: 'applied', reasonCode: null }] }));
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
147
185
|
if (!internalHandlerRoute('platform-comment', 'sync', operation)) { fail('operation is not registered'); return; }
|
|
148
186
|
const kind = parsed.values.kind;
|
|
149
187
|
const agent = parsed.values.agent;
|