@fitlab-ai/agent-infra 0.11.1 → 0.11.2
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/lib/internal/cli-route-inventory.js +1 -1
- package/dist/lib/internal/platform-comment.js +59 -0
- package/dist/lib/internal/sandbox-control.js +4 -131
- package/dist/lib/internal/task-artifact.js +1 -1
- package/dist/lib/internal/task-operation-registry.js +1 -0
- 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 +120 -13
- 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/executor.js +0 -38
- package/dist/lib/sandbox/control/finalization-response.js +10 -2
- package/dist/lib/sandbox/control/task-view.js +3 -1
- package/dist/lib/sandbox/control/workflow-executor.js +28 -43
- 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 +1 -8
- package/dist/lib/task/delegation-receipts.js +14 -76
- package/dist/lib/task/events.js +89 -237
- package/dist/lib/task/finalization-state.js +2 -1
- package/dist/lib/task/finalization.js +273 -49
- package/dist/lib/task/lifecycle-execution.js +2 -11
- package/dist/lib/task/local-artifact-finalization.js +25 -242
- package/dist/lib/task/orchestration.js +0 -18
- package/dist/lib/task/review-command.js +0 -1
- package/dist/lib/task/review-finalization.js +46 -211
- package/dist/lib/task/workflow-command.js +4 -1
- package/dist/lib/task/workflow-dispatch.js +55 -3
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/internal/cli-route-inventory.ts +1 -1
- package/lib/internal/platform-comment.ts +38 -0
- package/lib/internal/sandbox-control.ts +3 -145
- package/lib/internal/task-artifact.ts +1 -1
- package/lib/internal/task-operation-registry.ts +1 -0
- 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 +132 -13
- 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/executor.ts +0 -44
- package/lib/sandbox/control/finalization-response.ts +10 -2
- package/lib/sandbox/control/task-view.ts +3 -1
- package/lib/sandbox/control/workflow-executor.ts +23 -50
- 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 +1 -8
- package/lib/task/delegation-receipts.ts +12 -79
- package/lib/task/events.ts +106 -255
- package/lib/task/finalization-state.ts +2 -1
- package/lib/task/finalization.ts +277 -57
- package/lib/task/lifecycle-execution.ts +10 -21
- package/lib/task/local-artifact-finalization.ts +23 -281
- package/lib/task/orchestration.ts +0 -34
- package/lib/task/review-command.ts +0 -1
- package/lib/task/review-finalization.ts +45 -193
- package/lib/task/workflow-command.ts +4 -1
- package/lib/task/workflow-dispatch.ts +46 -3
- package/package.json +2 -2
- package/templates/.agents/rules/lifecycle-orchestration.en.md +7 -7
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +7 -7
- 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-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +3 -3
- 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-task/SKILL.en.md +6 -6
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +6 -6
- 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
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
2
4
|
|
|
3
5
|
import { parseArtifactName } from './artifact-name.ts';
|
|
4
|
-
import { validateCompletedArtifact,
|
|
6
|
+
import { validateCompletedArtifact, validateArtifactPublication } from './artifact-lifecycle.ts';
|
|
5
7
|
import { LEDGER_SECTION_MISSING_CODE, LEDGER_SECTION_MISSING_MESSAGE, parseLedgerDocument, summarizeLedgerStage, validateLedgerRows } from './ledger.ts';
|
|
6
8
|
import type { LedgerStageStatus, ReviewStage } from './ledger.ts';
|
|
7
9
|
import { finalizeReviewSummaryContent } from './review-artifacts.ts';
|
|
@@ -15,17 +17,6 @@ import type { ManualOverrideCapability } from './guard-override.ts';
|
|
|
15
17
|
import { getArtifactSchema } from './artifact-schema.ts';
|
|
16
18
|
import { canonicalSemanticDigest, inspectArtifactContract, sha256Content } from './artifact-operations.ts';
|
|
17
19
|
import { expectedQualificationRelations, validateQualificationAudit } from './qualification-audit.ts';
|
|
18
|
-
import { readArtifactRecoveryIntent } from './artifact-repair-intent.ts';
|
|
19
|
-
import {
|
|
20
|
-
beginArtifactRecovery,
|
|
21
|
-
commitArtifactRecovery,
|
|
22
|
-
prepareArtifactRecoveryFinal,
|
|
23
|
-
prepareArtifactRecoveryCommit,
|
|
24
|
-
reconcileArtifactRecovery,
|
|
25
|
-
recoveryContextFromIntent,
|
|
26
|
-
stageArtifactCandidate
|
|
27
|
-
} from './artifact-recovery.ts';
|
|
28
|
-
import type { ArtifactRecoveryContext } from './artifact-recovery.ts';
|
|
29
20
|
|
|
30
21
|
type ReviewFinalizationErrorCode =
|
|
31
22
|
| ResolveTaskRefErrorCode
|
|
@@ -51,7 +42,6 @@ type ReviewFinalizationRequest = {
|
|
|
51
42
|
artifact: string;
|
|
52
43
|
orchestrated?: boolean;
|
|
53
44
|
dryRun?: boolean;
|
|
54
|
-
recoveryId?: string;
|
|
55
45
|
};
|
|
56
46
|
type ReviewFinalizationResult = {
|
|
57
47
|
status: 'planned' | 'applied' | 'no-op' | 'failed';
|
|
@@ -64,12 +54,6 @@ type ReviewFinalizationResult = {
|
|
|
64
54
|
stageStatus: LedgerStageStatus | null;
|
|
65
55
|
artifactSha256: string | null;
|
|
66
56
|
semanticDigest: string | null;
|
|
67
|
-
recovery?: Readonly<{
|
|
68
|
-
recoveryId: string;
|
|
69
|
-
candidatePath: string;
|
|
70
|
-
baselineSha256: string;
|
|
71
|
-
baselineSemanticDigest: string;
|
|
72
|
-
}>;
|
|
73
57
|
operations: readonly {
|
|
74
58
|
kind: 'artifact';
|
|
75
59
|
artifact: string;
|
|
@@ -90,7 +74,6 @@ type ReviewFinalizationOptions = {
|
|
|
90
74
|
fileSystem?: Partial<ReviewFileSystem>;
|
|
91
75
|
manualOverride?: ManualOverrideCapability;
|
|
92
76
|
lockAlreadyHeld?: boolean;
|
|
93
|
-
startRecovery?: boolean;
|
|
94
77
|
};
|
|
95
78
|
|
|
96
79
|
const STAGES: Record<ReviewStage, { family: 'review-analysis' | 'review-plan' | 'review-code' }> = {
|
|
@@ -143,7 +126,7 @@ function preflightFailed(
|
|
|
143
126
|
};
|
|
144
127
|
}
|
|
145
128
|
|
|
146
|
-
/** Validate
|
|
129
|
+
/** Validate the current review draft before any finding-ledger write. */
|
|
147
130
|
function preflightReviewSummaryUnlocked(
|
|
148
131
|
request: ReviewFinalizationRequest,
|
|
149
132
|
options: ReviewFinalizationOptions = {}
|
|
@@ -162,28 +145,13 @@ function preflightReviewSummaryUnlocked(
|
|
|
162
145
|
|
|
163
146
|
let taskContent: string;
|
|
164
147
|
let content: string;
|
|
165
|
-
let recovery: ArtifactRecoveryContext | undefined;
|
|
166
148
|
try {
|
|
167
149
|
taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
if (!['awaiting-preflight-recovery', 'preflight-ready'].includes(intent.state)) return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', `recovery journal is in '${intent.state}' state`, resolved.taskId);
|
|
172
|
-
recovery = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, intent);
|
|
173
|
-
content = fs.existsSync(recovery.stagingPath)
|
|
174
|
-
? fs.readFileSync(recovery.stagingPath, 'utf8')
|
|
175
|
-
: fs.readFileSync(recovery.formalPath, 'utf8');
|
|
176
|
-
} else {
|
|
177
|
-
const validated = validateCompletedArtifact(resolved.taskDir, spec.family, request.artifact);
|
|
178
|
-
if (!validated.ok) return preflightFailed(request, validated.error.code === 'ARTIFACT_NOT_REGULAR' ? 'REVIEW_ARTIFACT_NOT_REGULAR' : 'REVIEW_ARTIFACT_IDENTITY_INVALID', validated.error.message, resolved.taskId);
|
|
179
|
-
content = fs.readFileSync(validated.artifact.path, 'utf8');
|
|
180
|
-
const intent = readArtifactRecoveryIntent(resolved.repoRoot, resolved.taskId, spec.family, request.artifact);
|
|
181
|
-
if (intent?.state === 'preflight-ready') recovery = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, intent);
|
|
182
|
-
if (intent?.state === 'consumed') return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', 'review artifact was already finalized', resolved.taskId);
|
|
183
|
-
}
|
|
150
|
+
const validated = validateCompletedArtifact(resolved.taskDir, spec.family, request.artifact);
|
|
151
|
+
if (!validated.ok) return preflightFailed(request, validated.error.code === 'ARTIFACT_NOT_REGULAR' ? 'REVIEW_ARTIFACT_NOT_REGULAR' : 'REVIEW_ARTIFACT_IDENTITY_INVALID', validated.error.message, resolved.taskId);
|
|
152
|
+
content = fs.readFileSync(validated.artifact.path, 'utf8');
|
|
184
153
|
} catch (error) { return preflightFailed(request, 'REVIEW_ARTIFACT_NOT_REGULAR', String(error), resolved.taskId); }
|
|
185
154
|
|
|
186
|
-
if (!hasOpenArtifactRound(taskContent, spec.family, parsed.round)) return preflightFailed(request, 'REVIEW_ARTIFACT_IDENTITY_INVALID', `${request.artifact} does not have one matching open started review event`, resolved.taskId);
|
|
187
155
|
const execution = validateLifecycleExecution(request.taskRef, {
|
|
188
156
|
mode: request.orchestrated ? 'orchestrated' : 'standalone',
|
|
189
157
|
identity: { stage: spec.family, round: parsed.round, artifact: request.artifact, role: 'reviewer' },
|
|
@@ -204,14 +172,6 @@ function preflightReviewSummaryUnlocked(
|
|
|
204
172
|
});
|
|
205
173
|
const qualificationError = qualification.ok ? null : qualification;
|
|
206
174
|
if (!detail.ok || !structure.ok || qualificationError) {
|
|
207
|
-
if (!recovery && !request.dryRun) {
|
|
208
|
-
try {
|
|
209
|
-
recovery = beginArtifactRecovery({
|
|
210
|
-
taskId: resolved.taskId, family: spec.family, artifact: request.artifact, round: parsed.round,
|
|
211
|
-
requestId: `review-preflight:${resolved.taskId}:${stage}:${parsed.round}`
|
|
212
|
-
}, Buffer.from(content, 'utf8'), { repoRoot: resolved.repoRoot, taskDir: resolved.taskDir, lockAlreadyHeld: options.lockAlreadyHeld });
|
|
213
|
-
} catch (error) { return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', String(error), resolved.taskId); }
|
|
214
|
-
}
|
|
215
175
|
const diagnostics = [
|
|
216
176
|
...(!detail.ok ? [`${detail.code}: ${detail.message}`] : []),
|
|
217
177
|
...structure.diagnostics.map((item) => `${item.code}: ${item.message}`),
|
|
@@ -219,27 +179,18 @@ function preflightReviewSummaryUnlocked(
|
|
|
219
179
|
].join('; ');
|
|
220
180
|
return preflightFailed(request, !detail.ok ? 'REVIEW_DECISION_DETAIL_INVALID' : !structure.ok ? 'REVIEW_ARTIFACT_STRUCTURE_INVALID' : 'REVIEW_ARTIFACT_QUALIFICATION_INVALID', diagnostics, resolved.taskId, {
|
|
221
181
|
artifactSha256, semanticDigest,
|
|
222
|
-
...(recovery ? { recovery: recoveryInfo(recovery) } : {})
|
|
223
182
|
});
|
|
224
183
|
}
|
|
225
|
-
if (request.dryRun
|
|
184
|
+
if (request.dryRun) {
|
|
226
185
|
return {
|
|
227
|
-
...preflightFailed(request, 'REVIEW_ARTIFACT_CONFLICT', '', resolved.taskId, { artifactSha256, semanticDigest
|
|
186
|
+
...preflightFailed(request, 'REVIEW_ARTIFACT_CONFLICT', '', resolved.taskId, { artifactSha256, semanticDigest }),
|
|
228
187
|
status: 'passed', changed: false, error: null
|
|
229
188
|
};
|
|
230
189
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
}, Buffer.from(content, 'utf8'), { repoRoot: resolved.repoRoot, taskDir: resolved.taskDir, lockAlreadyHeld: options.lockAlreadyHeld });
|
|
236
|
-
const staged = stageArtifactCandidate(recovery, Buffer.from(content, 'utf8'), { lockAlreadyHeld: options.lockAlreadyHeld });
|
|
237
|
-
prepareArtifactRecoveryCommit(recovery, staged.candidateSha256, staged.semanticDigest, { lockAlreadyHeld: options.lockAlreadyHeld });
|
|
238
|
-
return {
|
|
239
|
-
...preflightFailed(request, 'REVIEW_ARTIFACT_CONFLICT', '', resolved.taskId, { artifactSha256, semanticDigest, recovery: recoveryInfo(recovery) }),
|
|
240
|
-
status: 'passed', changed: false, error: null
|
|
241
|
-
};
|
|
242
|
-
} catch (error) { return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', String(error), resolved.taskId, { artifactSha256, semanticDigest }); }
|
|
190
|
+
return {
|
|
191
|
+
...preflightFailed(request, 'REVIEW_ARTIFACT_CONFLICT', '', resolved.taskId, { artifactSha256, semanticDigest }),
|
|
192
|
+
status: 'passed', changed: false, error: null
|
|
193
|
+
};
|
|
243
194
|
}
|
|
244
195
|
|
|
245
196
|
function preflightReviewSummary(
|
|
@@ -265,19 +216,9 @@ function preflightReviewSummary(
|
|
|
265
216
|
type ReviewSummaryCandidatePreparation = Readonly<{
|
|
266
217
|
result: ReviewFinalizationResult;
|
|
267
218
|
content: string;
|
|
268
|
-
recovery?: ArtifactRecoveryContext;
|
|
269
219
|
lockAlreadyHeld?: boolean;
|
|
270
220
|
}>;
|
|
271
221
|
|
|
272
|
-
function recoveryInfo(recovery: ArtifactRecoveryContext): NonNullable<ReviewFinalizationResult['recovery']> {
|
|
273
|
-
return {
|
|
274
|
-
recoveryId: recovery.recoveryId,
|
|
275
|
-
candidatePath: recovery.stagingPath,
|
|
276
|
-
baselineSha256: recovery.baselineSha256,
|
|
277
|
-
baselineSemanticDigest: recovery.baselineSemanticDigest
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
|
-
|
|
281
222
|
/** Prepare domain validation, summary bytes and provenance before any publication. */
|
|
282
223
|
function prepareReviewSummaryCandidate(
|
|
283
224
|
request: ReviewFinalizationRequest,
|
|
@@ -306,36 +247,9 @@ function prepareReviewSummaryCandidate(
|
|
|
306
247
|
let taskContent: string;
|
|
307
248
|
try { taskContent = (options.fileSystem?.readFileSync ?? DEFAULT_FILE_SYSTEM.readFileSync)(resolved.taskMdPath); }
|
|
308
249
|
catch (error) { return reject('REVIEW_ARTIFACT_NOT_REGULAR', String(error)); }
|
|
309
|
-
let recovery: ArtifactRecoveryContext | undefined;
|
|
310
|
-
if (request.recoveryId) {
|
|
311
|
-
try {
|
|
312
|
-
const intent = readArtifactRecoveryIntent(resolved.repoRoot, taskId!, spec.family, request.artifact);
|
|
313
|
-
if (!intent || intent.recoveryOperationId !== request.recoveryId) return reject('REVIEW_PROVENANCE_INVALID', 'recovery id does not match the artifact journal');
|
|
314
|
-
if (!['awaiting-preflight-recovery', 'preflight-ready', 'preflight-commit-started', 'preflight-passed', 'full-finalizer-ready', 'commit-started'].includes(intent.state)) return reject('REVIEW_PROVENANCE_INVALID', `recovery journal is in '${intent.state}' state`);
|
|
315
|
-
recovery = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, intent);
|
|
316
|
-
artifactContent = fs.existsSync(recovery.stagingPath)
|
|
317
|
-
? fs.readFileSync(recovery.stagingPath, 'utf8')
|
|
318
|
-
: fs.readFileSync(recovery.formalPath, 'utf8');
|
|
319
|
-
} catch (error) { return reject('REVIEW_PROVENANCE_INVALID', String(error)); }
|
|
320
|
-
}
|
|
321
250
|
const artifactSha256 = sha256Content(artifactContent);
|
|
322
251
|
const semanticDigest = canonicalSemanticDigest(artifactContent);
|
|
323
252
|
const digests = { artifactSha256, semanticDigest };
|
|
324
|
-
let repairIntent;
|
|
325
|
-
try { repairIntent = readArtifactRecoveryIntent(resolved.repoRoot, taskId!, spec.family, request.artifact); }
|
|
326
|
-
catch (error) { return reject('REVIEW_PROVENANCE_INVALID', String(error), digests); }
|
|
327
|
-
if (repairIntent?.state === 'consumed') {
|
|
328
|
-
if (repairIntent.finalArtifactSha256 !== artifactSha256 || repairIntent.finalSemanticDigest !== semanticDigest) {
|
|
329
|
-
return reject('REVIEW_PROVENANCE_INVALID', 'the review artifact changed after its finalization provenance was recorded', digests);
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
if (repairIntent?.state === 'passed'
|
|
333
|
-
&& (repairIntent.finalArtifactSha256 !== artifactSha256 || repairIntent.finalSemanticDigest !== semanticDigest)) {
|
|
334
|
-
repairIntent = undefined;
|
|
335
|
-
}
|
|
336
|
-
if (!hasOpenArtifactRound(taskContent, spec.family, parsed.round)) {
|
|
337
|
-
return reject('REVIEW_ARTIFACT_IDENTITY_INVALID', `${request.artifact} does not have one matching open started review event`);
|
|
338
|
-
}
|
|
339
253
|
const execution = validateLifecycleExecution(request.taskRef, {
|
|
340
254
|
mode: request.orchestrated ? 'orchestrated' : 'standalone',
|
|
341
255
|
identity: { stage: spec.family, round: parsed.round, artifact: request.artifact, role: 'reviewer' },
|
|
@@ -356,59 +270,15 @@ function prepareReviewSummaryCandidate(
|
|
|
356
270
|
const schema = getArtifactSchema(spec.family)!;
|
|
357
271
|
const structure = inspectArtifactContract(artifactContent, schema);
|
|
358
272
|
if (!structure.ok) {
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
taskId: taskId!, family: spec.family, artifact: request.artifact, round: parsed.round,
|
|
363
|
-
requestId: `review-finalize:${taskId!}:${stage}:${parsed.round}`
|
|
364
|
-
}, Buffer.from(artifactContent, 'utf8'), { repoRoot: resolved.repoRoot, taskDir: resolved.taskDir, lockAlreadyHeld: options.lockAlreadyHeld });
|
|
365
|
-
} catch (error) { return reject('REVIEW_PROVENANCE_INVALID', String(error), digests); }
|
|
366
|
-
}
|
|
367
|
-
return {
|
|
368
|
-
...reject('REVIEW_ARTIFACT_STRUCTURE_INVALID', structure.diagnostics.map((item) => `${item.code}: ${item.message}`).join('; '), {
|
|
369
|
-
artifactSha256, semanticDigest: structure.semanticDigest,
|
|
370
|
-
...(recovery ? { recovery: {
|
|
371
|
-
recoveryId: recovery.recoveryId,
|
|
372
|
-
candidatePath: recovery.stagingPath,
|
|
373
|
-
baselineSha256: recovery.baselineSha256,
|
|
374
|
-
baselineSemanticDigest: recovery.baselineSemanticDigest
|
|
375
|
-
} } : {})
|
|
376
|
-
}),
|
|
377
|
-
...(recovery ? { recovery } : {})
|
|
378
|
-
};
|
|
273
|
+
return reject('REVIEW_ARTIFACT_STRUCTURE_INVALID', structure.diagnostics.map((item) => `${item.code}: ${item.message}`).join('; '), {
|
|
274
|
+
artifactSha256, semanticDigest: structure.semanticDigest
|
|
275
|
+
});
|
|
379
276
|
}
|
|
380
277
|
const transformed = finalizeReviewSummaryContent(artifactContent, stageStatus.unresolvedFindingCounts, {
|
|
381
|
-
refreshNumericCounts:
|
|
278
|
+
refreshNumericCounts: true
|
|
382
279
|
});
|
|
383
280
|
if (!transformed.ok) return reject(transformed.code, transformed.message);
|
|
384
281
|
const finalDigests = { artifactSha256: sha256Content(transformed.content), semanticDigest: canonicalSemanticDigest(transformed.content) };
|
|
385
|
-
if (!recovery && repairIntent?.state === 'commit-started') {
|
|
386
|
-
if (repairIntent.finalArtifactSha256 !== finalDigests.artifactSha256 || repairIntent.finalSemanticDigest !== finalDigests.semanticDigest) {
|
|
387
|
-
return reject('REVIEW_PROVENANCE_INVALID', 'formal review artifact does not match the interrupted recovery journal', finalDigests);
|
|
388
|
-
}
|
|
389
|
-
recovery = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, repairIntent);
|
|
390
|
-
}
|
|
391
|
-
if (!recovery && (repairIntent?.state === 'passed' || repairIntent?.state === 'consumed')) {
|
|
392
|
-
if (transformed.changed) return reject('REVIEW_PROVENANCE_INVALID', 'the review artifact requires changes after its completed recovery was consumed', finalDigests);
|
|
393
|
-
return {
|
|
394
|
-
content: transformed.content,
|
|
395
|
-
result: {
|
|
396
|
-
...failed(request, 'REVIEW_ARTIFACT_CONFLICT', '', taskId, stageStatus, finalDigests),
|
|
397
|
-
status: 'no-op',
|
|
398
|
-
changed: false,
|
|
399
|
-
error: null
|
|
400
|
-
},
|
|
401
|
-
lockAlreadyHeld: options.lockAlreadyHeld
|
|
402
|
-
};
|
|
403
|
-
}
|
|
404
|
-
if (!recovery && !request.dryRun && options.startRecovery) {
|
|
405
|
-
try {
|
|
406
|
-
recovery = beginArtifactRecovery({
|
|
407
|
-
taskId: taskId!, family: spec.family, artifact: request.artifact, round: parsed.round,
|
|
408
|
-
requestId: `review-finalize:${taskId!}:${stage}:${parsed.round}`
|
|
409
|
-
}, Buffer.from(artifactContent, 'utf8'), { repoRoot: resolved.repoRoot, taskDir: resolved.taskDir, lockAlreadyHeld: options.lockAlreadyHeld });
|
|
410
|
-
} catch (error) { return reject('REVIEW_PROVENANCE_INVALID', String(error), finalDigests); }
|
|
411
|
-
}
|
|
412
282
|
return {
|
|
413
283
|
content: transformed.content,
|
|
414
284
|
result: {
|
|
@@ -418,7 +288,6 @@ function prepareReviewSummaryCandidate(
|
|
|
418
288
|
operations: transformed.changed ? [{ kind: 'artifact', artifact: request.artifact, operation: 'update' }] : [],
|
|
419
289
|
error: null
|
|
420
290
|
},
|
|
421
|
-
...(recovery ? { recovery } : {}),
|
|
422
291
|
lockAlreadyHeld: options.lockAlreadyHeld
|
|
423
292
|
};
|
|
424
293
|
}
|
|
@@ -428,51 +297,18 @@ function commitReviewSummaryProvenance(
|
|
|
428
297
|
repoRoot: string,
|
|
429
298
|
options: Readonly<{ afterPublish?: () => void }> = {}
|
|
430
299
|
): ReviewFinalizationResult {
|
|
431
|
-
|
|
300
|
+
return prepared.result;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function replaceCurrentArtifactAtomically(file: string, content: string): void {
|
|
304
|
+
const temporary = path.join(path.dirname(file), `.${path.basename(file)}.${process.pid}.${randomUUID()}.tmp`);
|
|
432
305
|
try {
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
...prepared.result,
|
|
439
|
-
status: 'no-op',
|
|
440
|
-
changed: false,
|
|
441
|
-
artifactSha256: intent.finalArtifactSha256,
|
|
442
|
-
semanticDigest: intent.finalSemanticDigest,
|
|
443
|
-
error: null
|
|
444
|
-
};
|
|
445
|
-
} else if (intent.state === 'commit-started') {
|
|
446
|
-
const reconciled = reconcileArtifactRecovery(prepared.recovery, {
|
|
447
|
-
lockAlreadyHeld: prepared.lockAlreadyHeld,
|
|
448
|
-
afterPublish: options.afterPublish
|
|
449
|
-
});
|
|
450
|
-
committed = reconciled.intent;
|
|
451
|
-
} else if (intent.state === 'awaiting-preflight-recovery') {
|
|
452
|
-
const staged = stageArtifactCandidate(prepared.recovery, Buffer.from(prepared.content, 'utf8'), { lockAlreadyHeld: prepared.lockAlreadyHeld });
|
|
453
|
-
prepareArtifactRecoveryCommit(prepared.recovery, staged.candidateSha256, staged.semanticDigest, { lockAlreadyHeld: prepared.lockAlreadyHeld });
|
|
454
|
-
committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld });
|
|
455
|
-
} else if (intent.state === 'preflight-ready' || intent.state === 'preflight-commit-started') {
|
|
456
|
-
committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld });
|
|
306
|
+
fs.writeFileSync(temporary, content, { encoding: 'utf8', flag: 'wx' });
|
|
307
|
+
fs.renameSync(temporary, file);
|
|
308
|
+
} finally {
|
|
309
|
+
try { fs.unlinkSync(temporary); } catch (error) {
|
|
310
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
|
|
457
311
|
}
|
|
458
|
-
if (committed?.state === 'preflight-passed' || intent.state === 'preflight-passed') {
|
|
459
|
-
prepareArtifactRecoveryFinal(prepared.recovery, Buffer.from(prepared.content, 'utf8'), { lockAlreadyHeld: prepared.lockAlreadyHeld });
|
|
460
|
-
committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld, afterPublish: options.afterPublish });
|
|
461
|
-
} else if (intent.state === 'full-finalizer-ready') {
|
|
462
|
-
committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld, afterPublish: options.afterPublish });
|
|
463
|
-
}
|
|
464
|
-
if (!committed || committed.state !== 'passed') throw new Error(`ARTIFACT_RECOVERY_STATE_INVALID: commit ended in '${committed?.state ?? intent.state}'`);
|
|
465
|
-
return {
|
|
466
|
-
...prepared.result,
|
|
467
|
-
artifactSha256: committed.finalArtifactSha256,
|
|
468
|
-
semanticDigest: committed.finalSemanticDigest,
|
|
469
|
-
error: null
|
|
470
|
-
};
|
|
471
|
-
} catch (error) {
|
|
472
|
-
if (error instanceof Error && error.message.startsWith('TASK_WORKFLOW_FAULT_INJECTED:')) throw error;
|
|
473
|
-
return { ...prepared.result, status: 'failed', error: {
|
|
474
|
-
code: 'REVIEW_RECOVERY_COMMIT_FAILED', message: `cannot publish review candidate: ${String(error)}`
|
|
475
|
-
} };
|
|
476
312
|
}
|
|
477
313
|
}
|
|
478
314
|
|
|
@@ -494,14 +330,30 @@ function finalizeReviewSummaryUnlocked(
|
|
|
494
330
|
catch (error) { return failed(request, 'REVIEW_ARTIFACT_NOT_REGULAR', String(error), resolved.taskId); }
|
|
495
331
|
const prepared = prepareReviewSummaryCandidate(request, content, options);
|
|
496
332
|
if (request.dryRun) return prepared.result;
|
|
497
|
-
|
|
333
|
+
if (prepared.result.status === 'failed' || !prepared.result.changed) return prepared.result;
|
|
334
|
+
try {
|
|
335
|
+
replaceCurrentArtifactAtomically(validated.artifact.path, prepared.content);
|
|
336
|
+
return {
|
|
337
|
+
...prepared.result,
|
|
338
|
+
status: 'applied',
|
|
339
|
+
artifactSha256: sha256Content(prepared.content),
|
|
340
|
+
semanticDigest: canonicalSemanticDigest(prepared.content)
|
|
341
|
+
};
|
|
342
|
+
} catch (error) {
|
|
343
|
+
return failed(
|
|
344
|
+
request,
|
|
345
|
+
'REVIEW_RECOVERY_COMMIT_FAILED',
|
|
346
|
+
`cannot update current review artifact: ${error instanceof Error ? error.message : String(error)}`,
|
|
347
|
+
resolved.taskId
|
|
348
|
+
);
|
|
349
|
+
}
|
|
498
350
|
}
|
|
499
351
|
|
|
500
352
|
function finalizeReviewSummary(
|
|
501
353
|
request: ReviewFinalizationRequest,
|
|
502
354
|
options: ReviewFinalizationOptions = {}
|
|
503
355
|
): ReviewFinalizationResult {
|
|
504
|
-
const effectiveOptions =
|
|
356
|
+
const effectiveOptions = options;
|
|
505
357
|
if (request.dryRun || options.lockAlreadyHeld) return finalizeReviewSummaryUnlocked(request, effectiveOptions);
|
|
506
358
|
const resolved = resolveTaskRef(request.taskRef, { repoRoot: options.repoRoot });
|
|
507
359
|
if (!resolved.ok) return finalizeReviewSummaryUnlocked(request, effectiveOptions);
|
|
@@ -9,10 +9,13 @@ export const TASK_WORKFLOW_COMMANDS = Object.freeze({
|
|
|
9
9
|
'ledger-finding-response': ['task-ledger', 'finding-respond'],
|
|
10
10
|
'ledger-finding-review': ['task-ledger', 'finding-review'],
|
|
11
11
|
'ledger-finding-upsert': ['task-ledger', 'finding-upsert'],
|
|
12
|
+
'ledger-stage-status': ['task-ledger', 'stage-status'],
|
|
12
13
|
'decision-next-id': ['task-ledger', 'decision-next-id'],
|
|
13
14
|
'decision-upsert': ['task-ledger', 'decision-upsert'],
|
|
14
15
|
'invalidation-reconcile': ['task-invalidation', 'reconcile'],
|
|
15
|
-
'warning-add': ['task-warning', 'add']
|
|
16
|
+
'warning-add': ['task-warning', 'add'],
|
|
17
|
+
'warning-list': ['task-warning', 'list'],
|
|
18
|
+
'warning-set-status': ['task-warning', 'set-status']
|
|
16
19
|
} as const);
|
|
17
20
|
export type TaskWorkflowOperation = keyof typeof TASK_WORKFLOW_COMMANDS;
|
|
18
21
|
export const TASK_WORKFLOW_OPERATIONS = Object.freeze(Object.keys(TASK_WORKFLOW_COMMANDS) as TaskWorkflowOperation[]);
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
|
|
1
3
|
import { reconcileTaskInvalidation } from './invalidation-command.ts';
|
|
2
4
|
import { applyLedgerIntent } from './ledger-intents.ts';
|
|
3
5
|
import type { LedgerIntent } from './ledger-intents.ts';
|
|
6
|
+
import { isReviewStage, parseLedgerDocument, summarizeLedgerStage, validateLedgerRows } from './ledger.ts';
|
|
7
|
+
import { resolveTaskRef } from './resolve-ref.ts';
|
|
4
8
|
import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.ts';
|
|
5
9
|
import type { TaskWorkflowOperation } from './workflow-command.ts';
|
|
6
10
|
import { applyWorkflowWarningIntent } from './workflow-warning-intents.ts';
|
|
@@ -116,7 +120,24 @@ function ledgerIntent(operation: TaskWorkflowOperation, args: readonly string[])
|
|
|
116
120
|
|
|
117
121
|
function warningIntent(args: readonly string[]): WorkflowWarningIntent | DispatchFailure {
|
|
118
122
|
const [taskRef, kind] = args;
|
|
119
|
-
if (!taskRef ||
|
|
123
|
+
if (!taskRef || !['add', 'list', 'set-status'].includes(kind ?? '')) {
|
|
124
|
+
return failed('TASK_WORKFLOW_PAYLOAD_INVALID', 'task workflow warning request is invalid');
|
|
125
|
+
}
|
|
126
|
+
if (kind === 'list') {
|
|
127
|
+
const values = options(args, { '--status': 'status' });
|
|
128
|
+
if (isFailure(values)) return values;
|
|
129
|
+
return { kind, taskRef, ...(value(values, '--status') ? { status: value(values, '--status') as never } : {}) };
|
|
130
|
+
}
|
|
131
|
+
if (kind === 'set-status') {
|
|
132
|
+
const values = options(args, { '--id': 'id', '--status': 'status', '--resolution': 'resolution', '--dry-run': 'dryRun' });
|
|
133
|
+
if (isFailure(values)) return values;
|
|
134
|
+
const missing = required(values, ['--id', '--status', '--resolution']);
|
|
135
|
+
if (missing) return missing;
|
|
136
|
+
return {
|
|
137
|
+
kind, taskRef, id: value(values, '--id')!, status: value(values, '--status') as never,
|
|
138
|
+
resolution: value(values, '--resolution')!, ...(values['--dry-run'] === true ? { dryRun: true } : {})
|
|
139
|
+
};
|
|
140
|
+
}
|
|
120
141
|
const values = options(args, {
|
|
121
142
|
'--step': 'step', '--severity': 'severity', '--code': 'code', '--target': 'target',
|
|
122
143
|
'--message': 'message', '--action': 'action', '--dry-run': 'dryRun'
|
|
@@ -131,6 +152,26 @@ function warningIntent(args: readonly string[]): WorkflowWarningIntent | Dispatc
|
|
|
131
152
|
} as WorkflowWarningIntent;
|
|
132
153
|
}
|
|
133
154
|
|
|
155
|
+
function stageStatus(repoRoot: string, args: readonly string[]): WorkflowDispatchResult {
|
|
156
|
+
const [taskRef, kind] = args;
|
|
157
|
+
if (!taskRef || kind !== 'stage-status') return failed('TASK_WORKFLOW_PAYLOAD_INVALID', 'task workflow ledger request is invalid');
|
|
158
|
+
const values = options(args, { '--stage': 'stage' });
|
|
159
|
+
if (isFailure(values)) return values;
|
|
160
|
+
const stage = value(values, '--stage');
|
|
161
|
+
if (!stage || !isReviewStage(stage)) return failed('TASK_WORKFLOW_PAYLOAD_INVALID', 'stage-status requires a valid review stage');
|
|
162
|
+
const resolved = resolveTaskRef(taskRef, { repoRoot });
|
|
163
|
+
if (!resolved.ok) return failed(resolved.code, resolved.message);
|
|
164
|
+
try {
|
|
165
|
+
const ledger = parseLedgerDocument(fs.readFileSync(resolved.taskMdPath, 'utf8'));
|
|
166
|
+
if (!ledger.present) return failed('LEDGER_SECTION_MISSING', 'review disagreement ledger section is missing');
|
|
167
|
+
const invalid = validateLedgerRows(ledger.rows);
|
|
168
|
+
if (invalid) return failed(invalid.code, invalid.message);
|
|
169
|
+
return { status: 'ready', changed: false, taskId: resolved.taskId, stageStatus: summarizeLedgerStage(ledger.rows, stage), error: null };
|
|
170
|
+
} catch (error) {
|
|
171
|
+
return failed('LEDGER_DOCUMENT_INVALID', error instanceof Error ? error.message : String(error));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
134
175
|
/**
|
|
135
176
|
* The caller has already selected and authorized the execution boundary.
|
|
136
177
|
* This function deliberately invokes task domains in that process: starting a
|
|
@@ -157,10 +198,12 @@ export function dispatchWorkflowCommand(
|
|
|
157
198
|
...(values['--dry-run'] === true ? { dryRun: true } : {})
|
|
158
199
|
}));
|
|
159
200
|
}
|
|
160
|
-
if (operation === '
|
|
201
|
+
if (operation === 'ledger-stage-status') return stageStatus(repoRoot, args);
|
|
202
|
+
if (operation === 'warning-add' || operation === 'warning-list' || operation === 'warning-set-status') {
|
|
161
203
|
const intent = warningIntent(args);
|
|
162
204
|
if (isFailure(intent)) return intent;
|
|
163
|
-
|
|
205
|
+
if (intent.kind === 'list') return applyWorkflowWarningIntent(intent, { repoRoot });
|
|
206
|
+
return withTaskExecutionLock(repoRoot, taskRef, `task-warning.${intent.kind}`, () => applyWorkflowWarningIntent(intent, { repoRoot }));
|
|
164
207
|
}
|
|
165
208
|
const intent = ledgerIntent(operation, args);
|
|
166
209
|
if (isFailure(intent)) return intent;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fitlab-ai/agent-infra",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.2",
|
|
4
4
|
"description": "Bootstrap tool for AI multi-tool collaboration infrastructure — works with Claude Code, Codex, Antigravity CLI, and OpenCode",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"installer"
|
|
53
53
|
],
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"@clack/prompts": "1.
|
|
55
|
+
"@clack/prompts": "1.8.1",
|
|
56
56
|
"@larksuiteoapi/node-sdk": "^1.68.0",
|
|
57
57
|
"cross-spawn": "^7.0.6",
|
|
58
58
|
"fs-native-extensions": "1.5.1",
|
|
@@ -5,18 +5,18 @@
|
|
|
5
5
|
- The orchestrator only routes and delegates. Stage skills remain the single source of truth for behavior and artifacts.
|
|
6
6
|
- Every stage and rework round uses a fresh executor. Every review uses a fresh reviewer; follow-up reuse is forbidden.
|
|
7
7
|
- A reviewer may only write its review artifact and core-generated task metadata. Business-code, HEAD, or index changes invalidate the receipt.
|
|
8
|
-
- An active run has one pending delegation.
|
|
8
|
+
- An active run has one pending delegation. Keep the stage associated with the observed child and report actual start and terminal failures.
|
|
9
9
|
- The first release ends after one existing safely gated `commit`; it does not create a PR, monitor checks, or complete the task.
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## Current Execution Records
|
|
12
12
|
|
|
13
|
-
`orchestration.json`
|
|
13
|
+
`orchestration.json` records stages, model policy, child identity, and actual outcomes. Records use the current structure; trusted local operators may correct them and rerun validation. Existing task write locks and atomic writes protect updates.
|
|
14
14
|
|
|
15
|
-
## Codex Host
|
|
15
|
+
## Codex Host
|
|
16
16
|
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
17
|
+
- Use native spawn, wait, and App Server results from the current host; forward task operations through the existing broker.
|
|
18
|
+
- Prepare validates the current task, model policy, and host preflight. Start and terminal records associate observed parent/child identity; failure must not be recorded as success.
|
|
19
|
+
- Local execution does not require capability, controller attestation, or one-use consumption authority. Do not add automatic child discovery, spawn under a new lock protocol, orphan recovery, or a dedicated recovery protocol.
|
|
20
20
|
|
|
21
21
|
## Model Policy
|
|
22
22
|
|
|
@@ -5,18 +5,18 @@
|
|
|
5
5
|
- 总控只路由和委派;阶段技能仍是业务规则与产物格式的单一事实源。
|
|
6
6
|
- 每个阶段和每轮返工都创建 fresh executor;每轮审查创建 fresh reviewer,禁止 follow-up 复用。
|
|
7
7
|
- reviewer 只能写当前审查产物和核心生成的任务元数据。业务代码、HEAD 或暂存区变化会使 receipt 失效。
|
|
8
|
-
- active run 中只有一个 pending delegation
|
|
8
|
+
- active run 中只有一个 pending delegation;阶段与实际 child 身份保持一致,启动和终态失败如实记录。
|
|
9
9
|
- 首版成功终点是一次通过既有安全门禁的 `commit`;不创建 PR、不监控 checks、不执行 `complete-task`。
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## 当前执行记录
|
|
12
12
|
|
|
13
|
-
`orchestration.json`
|
|
13
|
+
`orchestration.json` 记录阶段、模型策略、child 身份及实际执行结果。记录必须符合当前结构;本地操作者可直接修正后重新校验。已有任务写锁与原子写入继续保护写操作。
|
|
14
14
|
|
|
15
|
-
## Codex
|
|
15
|
+
## Codex 宿主
|
|
16
16
|
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
17
|
+
- 使用当前宿主的原生 spawn、wait 与 App Server 结果;任务操作通过现有 broker 转发。
|
|
18
|
+
- prepare 校验当前任务、模型策略与宿主 preflight。启动和完成记录关联实际 parent/child 身份,失败不得写成成功。
|
|
19
|
+
- 不要求 capability、controller attestation 或一次性消费授权本地实现;不新增 child 自动发现、锁内启动交接、孤立 child 恢复或专用恢复协议。
|
|
20
20
|
|
|
21
21
|
## 模型策略
|
|
22
22
|
|