@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,6 +1,8 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
2
4
|
import { parseArtifactName } from './artifact-name.js';
|
|
3
|
-
import { validateCompletedArtifact,
|
|
5
|
+
import { validateCompletedArtifact, validateArtifactPublication } from './artifact-lifecycle.js';
|
|
4
6
|
import { LEDGER_SECTION_MISSING_CODE, LEDGER_SECTION_MISSING_MESSAGE, parseLedgerDocument, summarizeLedgerStage, validateLedgerRows } from './ledger.js';
|
|
5
7
|
import { finalizeReviewSummaryContent } from './review-artifacts.js';
|
|
6
8
|
import { inspectDecisionDetailDuplicates } from './decision-details.js';
|
|
@@ -11,8 +13,6 @@ import { allowsManualOverride } from './guard-override.js';
|
|
|
11
13
|
import { getArtifactSchema } from './artifact-schema.js';
|
|
12
14
|
import { canonicalSemanticDigest, inspectArtifactContract, sha256Content } from './artifact-operations.js';
|
|
13
15
|
import { expectedQualificationRelations, validateQualificationAudit } from './qualification-audit.js';
|
|
14
|
-
import { readArtifactRecoveryIntent } from './artifact-repair-intent.js';
|
|
15
|
-
import { beginArtifactRecovery, commitArtifactRecovery, prepareArtifactRecoveryFinal, prepareArtifactRecoveryCommit, reconcileArtifactRecovery, recoveryContextFromIntent, stageArtifactCandidate } from './artifact-recovery.js';
|
|
16
16
|
const STAGES = {
|
|
17
17
|
analysis: { family: 'review-analysis' },
|
|
18
18
|
plan: { family: 'review-plan' },
|
|
@@ -47,7 +47,7 @@ function preflightFailed(request, code, message, taskId = null, extra = {}) {
|
|
|
47
47
|
stageStatus: null
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
|
-
/** Validate
|
|
50
|
+
/** Validate the current review draft before any finding-ledger write. */
|
|
51
51
|
function preflightReviewSummaryUnlocked(request, options = {}) {
|
|
52
52
|
const stage = request.stage;
|
|
53
53
|
const spec = STAGES[stage];
|
|
@@ -68,37 +68,16 @@ function preflightReviewSummaryUnlocked(request, options = {}) {
|
|
|
68
68
|
return preflightFailed(request, 'REVIEW_ARTIFACT_IDENTITY_INVALID', publicationError.message, resolved.taskId);
|
|
69
69
|
let taskContent;
|
|
70
70
|
let content;
|
|
71
|
-
let recovery;
|
|
72
71
|
try {
|
|
73
72
|
taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (!['awaiting-preflight-recovery', 'preflight-ready'].includes(intent.state))
|
|
79
|
-
return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', `recovery journal is in '${intent.state}' state`, resolved.taskId);
|
|
80
|
-
recovery = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, intent);
|
|
81
|
-
content = fs.existsSync(recovery.stagingPath)
|
|
82
|
-
? fs.readFileSync(recovery.stagingPath, 'utf8')
|
|
83
|
-
: fs.readFileSync(recovery.formalPath, 'utf8');
|
|
84
|
-
}
|
|
85
|
-
else {
|
|
86
|
-
const validated = validateCompletedArtifact(resolved.taskDir, spec.family, request.artifact);
|
|
87
|
-
if (!validated.ok)
|
|
88
|
-
return preflightFailed(request, validated.error.code === 'ARTIFACT_NOT_REGULAR' ? 'REVIEW_ARTIFACT_NOT_REGULAR' : 'REVIEW_ARTIFACT_IDENTITY_INVALID', validated.error.message, resolved.taskId);
|
|
89
|
-
content = fs.readFileSync(validated.artifact.path, 'utf8');
|
|
90
|
-
const intent = readArtifactRecoveryIntent(resolved.repoRoot, resolved.taskId, spec.family, request.artifact);
|
|
91
|
-
if (intent?.state === 'preflight-ready')
|
|
92
|
-
recovery = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, intent);
|
|
93
|
-
if (intent?.state === 'consumed')
|
|
94
|
-
return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', 'review artifact was already finalized', resolved.taskId);
|
|
95
|
-
}
|
|
73
|
+
const validated = validateCompletedArtifact(resolved.taskDir, spec.family, request.artifact);
|
|
74
|
+
if (!validated.ok)
|
|
75
|
+
return preflightFailed(request, validated.error.code === 'ARTIFACT_NOT_REGULAR' ? 'REVIEW_ARTIFACT_NOT_REGULAR' : 'REVIEW_ARTIFACT_IDENTITY_INVALID', validated.error.message, resolved.taskId);
|
|
76
|
+
content = fs.readFileSync(validated.artifact.path, 'utf8');
|
|
96
77
|
}
|
|
97
78
|
catch (error) {
|
|
98
79
|
return preflightFailed(request, 'REVIEW_ARTIFACT_NOT_REGULAR', String(error), resolved.taskId);
|
|
99
80
|
}
|
|
100
|
-
if (!hasOpenArtifactRound(taskContent, spec.family, parsed.round))
|
|
101
|
-
return preflightFailed(request, 'REVIEW_ARTIFACT_IDENTITY_INVALID', `${request.artifact} does not have one matching open started review event`, resolved.taskId);
|
|
102
81
|
const execution = validateLifecycleExecution(request.taskRef, {
|
|
103
82
|
mode: request.orchestrated ? 'orchestrated' : 'standalone',
|
|
104
83
|
identity: { stage: spec.family, round: parsed.round, artifact: request.artifact, role: 'reviewer' },
|
|
@@ -120,17 +99,6 @@ function preflightReviewSummaryUnlocked(request, options = {}) {
|
|
|
120
99
|
});
|
|
121
100
|
const qualificationError = qualification.ok ? null : qualification;
|
|
122
101
|
if (!detail.ok || !structure.ok || qualificationError) {
|
|
123
|
-
if (!recovery && !request.dryRun) {
|
|
124
|
-
try {
|
|
125
|
-
recovery = beginArtifactRecovery({
|
|
126
|
-
taskId: resolved.taskId, family: spec.family, artifact: request.artifact, round: parsed.round,
|
|
127
|
-
requestId: `review-preflight:${resolved.taskId}:${stage}:${parsed.round}`
|
|
128
|
-
}, Buffer.from(content, 'utf8'), { repoRoot: resolved.repoRoot, taskDir: resolved.taskDir, lockAlreadyHeld: options.lockAlreadyHeld });
|
|
129
|
-
}
|
|
130
|
-
catch (error) {
|
|
131
|
-
return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', String(error), resolved.taskId);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
102
|
const diagnostics = [
|
|
135
103
|
...(!detail.ok ? [`${detail.code}: ${detail.message}`] : []),
|
|
136
104
|
...structure.diagnostics.map((item) => `${item.code}: ${item.message}`),
|
|
@@ -138,30 +106,18 @@ function preflightReviewSummaryUnlocked(request, options = {}) {
|
|
|
138
106
|
].join('; ');
|
|
139
107
|
return preflightFailed(request, !detail.ok ? 'REVIEW_DECISION_DETAIL_INVALID' : !structure.ok ? 'REVIEW_ARTIFACT_STRUCTURE_INVALID' : 'REVIEW_ARTIFACT_QUALIFICATION_INVALID', diagnostics, resolved.taskId, {
|
|
140
108
|
artifactSha256, semanticDigest,
|
|
141
|
-
...(recovery ? { recovery: recoveryInfo(recovery) } : {})
|
|
142
109
|
});
|
|
143
110
|
}
|
|
144
|
-
if (request.dryRun
|
|
111
|
+
if (request.dryRun) {
|
|
145
112
|
return {
|
|
146
|
-
...preflightFailed(request, 'REVIEW_ARTIFACT_CONFLICT', '', resolved.taskId, { artifactSha256, semanticDigest
|
|
113
|
+
...preflightFailed(request, 'REVIEW_ARTIFACT_CONFLICT', '', resolved.taskId, { artifactSha256, semanticDigest }),
|
|
147
114
|
status: 'passed', changed: false, error: null
|
|
148
115
|
};
|
|
149
116
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}, Buffer.from(content, 'utf8'), { repoRoot: resolved.repoRoot, taskDir: resolved.taskDir, lockAlreadyHeld: options.lockAlreadyHeld });
|
|
155
|
-
const staged = stageArtifactCandidate(recovery, Buffer.from(content, 'utf8'), { lockAlreadyHeld: options.lockAlreadyHeld });
|
|
156
|
-
prepareArtifactRecoveryCommit(recovery, staged.candidateSha256, staged.semanticDigest, { lockAlreadyHeld: options.lockAlreadyHeld });
|
|
157
|
-
return {
|
|
158
|
-
...preflightFailed(request, 'REVIEW_ARTIFACT_CONFLICT', '', resolved.taskId, { artifactSha256, semanticDigest, recovery: recoveryInfo(recovery) }),
|
|
159
|
-
status: 'passed', changed: false, error: null
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
catch (error) {
|
|
163
|
-
return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', String(error), resolved.taskId, { artifactSha256, semanticDigest });
|
|
164
|
-
}
|
|
117
|
+
return {
|
|
118
|
+
...preflightFailed(request, 'REVIEW_ARTIFACT_CONFLICT', '', resolved.taskId, { artifactSha256, semanticDigest }),
|
|
119
|
+
status: 'passed', changed: false, error: null
|
|
120
|
+
};
|
|
165
121
|
}
|
|
166
122
|
function preflightReviewSummary(request, options = {}) {
|
|
167
123
|
if (request.dryRun || options.lockAlreadyHeld)
|
|
@@ -178,14 +134,6 @@ function preflightReviewSummary(request, options = {}) {
|
|
|
178
134
|
return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', `${error.code}: ${error.message}`, resolved.taskId);
|
|
179
135
|
}
|
|
180
136
|
}
|
|
181
|
-
function recoveryInfo(recovery) {
|
|
182
|
-
return {
|
|
183
|
-
recoveryId: recovery.recoveryId,
|
|
184
|
-
candidatePath: recovery.stagingPath,
|
|
185
|
-
baselineSha256: recovery.baselineSha256,
|
|
186
|
-
baselineSemanticDigest: recovery.baselineSemanticDigest
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
137
|
/** Prepare domain validation, summary bytes and provenance before any publication. */
|
|
190
138
|
function prepareReviewSummaryCandidate(request, artifactContent, options = {}) {
|
|
191
139
|
let taskId = null;
|
|
@@ -219,45 +167,9 @@ function prepareReviewSummaryCandidate(request, artifactContent, options = {}) {
|
|
|
219
167
|
catch (error) {
|
|
220
168
|
return reject('REVIEW_ARTIFACT_NOT_REGULAR', String(error));
|
|
221
169
|
}
|
|
222
|
-
let recovery;
|
|
223
|
-
if (request.recoveryId) {
|
|
224
|
-
try {
|
|
225
|
-
const intent = readArtifactRecoveryIntent(resolved.repoRoot, taskId, spec.family, request.artifact);
|
|
226
|
-
if (!intent || intent.recoveryOperationId !== request.recoveryId)
|
|
227
|
-
return reject('REVIEW_PROVENANCE_INVALID', 'recovery id does not match the artifact journal');
|
|
228
|
-
if (!['awaiting-preflight-recovery', 'preflight-ready', 'preflight-commit-started', 'preflight-passed', 'full-finalizer-ready', 'commit-started'].includes(intent.state))
|
|
229
|
-
return reject('REVIEW_PROVENANCE_INVALID', `recovery journal is in '${intent.state}' state`);
|
|
230
|
-
recovery = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, intent);
|
|
231
|
-
artifactContent = fs.existsSync(recovery.stagingPath)
|
|
232
|
-
? fs.readFileSync(recovery.stagingPath, 'utf8')
|
|
233
|
-
: fs.readFileSync(recovery.formalPath, 'utf8');
|
|
234
|
-
}
|
|
235
|
-
catch (error) {
|
|
236
|
-
return reject('REVIEW_PROVENANCE_INVALID', String(error));
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
170
|
const artifactSha256 = sha256Content(artifactContent);
|
|
240
171
|
const semanticDigest = canonicalSemanticDigest(artifactContent);
|
|
241
172
|
const digests = { artifactSha256, semanticDigest };
|
|
242
|
-
let repairIntent;
|
|
243
|
-
try {
|
|
244
|
-
repairIntent = readArtifactRecoveryIntent(resolved.repoRoot, taskId, spec.family, request.artifact);
|
|
245
|
-
}
|
|
246
|
-
catch (error) {
|
|
247
|
-
return reject('REVIEW_PROVENANCE_INVALID', String(error), digests);
|
|
248
|
-
}
|
|
249
|
-
if (repairIntent?.state === 'consumed') {
|
|
250
|
-
if (repairIntent.finalArtifactSha256 !== artifactSha256 || repairIntent.finalSemanticDigest !== semanticDigest) {
|
|
251
|
-
return reject('REVIEW_PROVENANCE_INVALID', 'the review artifact changed after its finalization provenance was recorded', digests);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
if (repairIntent?.state === 'passed'
|
|
255
|
-
&& (repairIntent.finalArtifactSha256 !== artifactSha256 || repairIntent.finalSemanticDigest !== semanticDigest)) {
|
|
256
|
-
repairIntent = undefined;
|
|
257
|
-
}
|
|
258
|
-
if (!hasOpenArtifactRound(taskContent, spec.family, parsed.round)) {
|
|
259
|
-
return reject('REVIEW_ARTIFACT_IDENTITY_INVALID', `${request.artifact} does not have one matching open started review event`);
|
|
260
|
-
}
|
|
261
173
|
const execution = validateLifecycleExecution(request.taskRef, {
|
|
262
174
|
mode: request.orchestrated ? 'orchestrated' : 'standalone',
|
|
263
175
|
identity: { stage: spec.family, round: parsed.round, artifact: request.artifact, role: 'reviewer' },
|
|
@@ -284,67 +196,16 @@ function prepareReviewSummaryCandidate(request, artifactContent, options = {}) {
|
|
|
284
196
|
const schema = getArtifactSchema(spec.family);
|
|
285
197
|
const structure = inspectArtifactContract(artifactContent, schema);
|
|
286
198
|
if (!structure.ok) {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
taskId: taskId, family: spec.family, artifact: request.artifact, round: parsed.round,
|
|
291
|
-
requestId: `review-finalize:${taskId}:${stage}:${parsed.round}`
|
|
292
|
-
}, Buffer.from(artifactContent, 'utf8'), { repoRoot: resolved.repoRoot, taskDir: resolved.taskDir, lockAlreadyHeld: options.lockAlreadyHeld });
|
|
293
|
-
}
|
|
294
|
-
catch (error) {
|
|
295
|
-
return reject('REVIEW_PROVENANCE_INVALID', String(error), digests);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
return {
|
|
299
|
-
...reject('REVIEW_ARTIFACT_STRUCTURE_INVALID', structure.diagnostics.map((item) => `${item.code}: ${item.message}`).join('; '), {
|
|
300
|
-
artifactSha256, semanticDigest: structure.semanticDigest,
|
|
301
|
-
...(recovery ? { recovery: {
|
|
302
|
-
recoveryId: recovery.recoveryId,
|
|
303
|
-
candidatePath: recovery.stagingPath,
|
|
304
|
-
baselineSha256: recovery.baselineSha256,
|
|
305
|
-
baselineSemanticDigest: recovery.baselineSemanticDigest
|
|
306
|
-
} } : {})
|
|
307
|
-
}),
|
|
308
|
-
...(recovery ? { recovery } : {})
|
|
309
|
-
};
|
|
199
|
+
return reject('REVIEW_ARTIFACT_STRUCTURE_INVALID', structure.diagnostics.map((item) => `${item.code}: ${item.message}`).join('; '), {
|
|
200
|
+
artifactSha256, semanticDigest: structure.semanticDigest
|
|
201
|
+
});
|
|
310
202
|
}
|
|
311
203
|
const transformed = finalizeReviewSummaryContent(artifactContent, stageStatus.unresolvedFindingCounts, {
|
|
312
|
-
refreshNumericCounts:
|
|
204
|
+
refreshNumericCounts: true
|
|
313
205
|
});
|
|
314
206
|
if (!transformed.ok)
|
|
315
207
|
return reject(transformed.code, transformed.message);
|
|
316
208
|
const finalDigests = { artifactSha256: sha256Content(transformed.content), semanticDigest: canonicalSemanticDigest(transformed.content) };
|
|
317
|
-
if (!recovery && repairIntent?.state === 'commit-started') {
|
|
318
|
-
if (repairIntent.finalArtifactSha256 !== finalDigests.artifactSha256 || repairIntent.finalSemanticDigest !== finalDigests.semanticDigest) {
|
|
319
|
-
return reject('REVIEW_PROVENANCE_INVALID', 'formal review artifact does not match the interrupted recovery journal', finalDigests);
|
|
320
|
-
}
|
|
321
|
-
recovery = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, repairIntent);
|
|
322
|
-
}
|
|
323
|
-
if (!recovery && (repairIntent?.state === 'passed' || repairIntent?.state === 'consumed')) {
|
|
324
|
-
if (transformed.changed)
|
|
325
|
-
return reject('REVIEW_PROVENANCE_INVALID', 'the review artifact requires changes after its completed recovery was consumed', finalDigests);
|
|
326
|
-
return {
|
|
327
|
-
content: transformed.content,
|
|
328
|
-
result: {
|
|
329
|
-
...failed(request, 'REVIEW_ARTIFACT_CONFLICT', '', taskId, stageStatus, finalDigests),
|
|
330
|
-
status: 'no-op',
|
|
331
|
-
changed: false,
|
|
332
|
-
error: null
|
|
333
|
-
},
|
|
334
|
-
lockAlreadyHeld: options.lockAlreadyHeld
|
|
335
|
-
};
|
|
336
|
-
}
|
|
337
|
-
if (!recovery && !request.dryRun && options.startRecovery) {
|
|
338
|
-
try {
|
|
339
|
-
recovery = beginArtifactRecovery({
|
|
340
|
-
taskId: taskId, family: spec.family, artifact: request.artifact, round: parsed.round,
|
|
341
|
-
requestId: `review-finalize:${taskId}:${stage}:${parsed.round}`
|
|
342
|
-
}, Buffer.from(artifactContent, 'utf8'), { repoRoot: resolved.repoRoot, taskDir: resolved.taskDir, lockAlreadyHeld: options.lockAlreadyHeld });
|
|
343
|
-
}
|
|
344
|
-
catch (error) {
|
|
345
|
-
return reject('REVIEW_PROVENANCE_INVALID', String(error), finalDigests);
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
209
|
return {
|
|
349
210
|
content: transformed.content,
|
|
350
211
|
result: {
|
|
@@ -354,65 +215,26 @@ function prepareReviewSummaryCandidate(request, artifactContent, options = {}) {
|
|
|
354
215
|
operations: transformed.changed ? [{ kind: 'artifact', artifact: request.artifact, operation: 'update' }] : [],
|
|
355
216
|
error: null
|
|
356
217
|
},
|
|
357
|
-
...(recovery ? { recovery } : {}),
|
|
358
218
|
lockAlreadyHeld: options.lockAlreadyHeld
|
|
359
219
|
};
|
|
360
220
|
}
|
|
361
221
|
function commitReviewSummaryProvenance(prepared, repoRoot, options = {}) {
|
|
362
|
-
|
|
363
|
-
|
|
222
|
+
return prepared.result;
|
|
223
|
+
}
|
|
224
|
+
function replaceCurrentArtifactAtomically(file, content) {
|
|
225
|
+
const temporary = path.join(path.dirname(file), `.${path.basename(file)}.${process.pid}.${randomUUID()}.tmp`);
|
|
364
226
|
try {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
...prepared.result,
|
|
372
|
-
status: 'no-op',
|
|
373
|
-
changed: false,
|
|
374
|
-
artifactSha256: intent.finalArtifactSha256,
|
|
375
|
-
semanticDigest: intent.finalSemanticDigest,
|
|
376
|
-
error: null
|
|
377
|
-
};
|
|
378
|
-
}
|
|
379
|
-
else if (intent.state === 'commit-started') {
|
|
380
|
-
const reconciled = reconcileArtifactRecovery(prepared.recovery, {
|
|
381
|
-
lockAlreadyHeld: prepared.lockAlreadyHeld,
|
|
382
|
-
afterPublish: options.afterPublish
|
|
383
|
-
});
|
|
384
|
-
committed = reconciled.intent;
|
|
385
|
-
}
|
|
386
|
-
else if (intent.state === 'awaiting-preflight-recovery') {
|
|
387
|
-
const staged = stageArtifactCandidate(prepared.recovery, Buffer.from(prepared.content, 'utf8'), { lockAlreadyHeld: prepared.lockAlreadyHeld });
|
|
388
|
-
prepareArtifactRecoveryCommit(prepared.recovery, staged.candidateSha256, staged.semanticDigest, { lockAlreadyHeld: prepared.lockAlreadyHeld });
|
|
389
|
-
committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld });
|
|
390
|
-
}
|
|
391
|
-
else if (intent.state === 'preflight-ready' || intent.state === 'preflight-commit-started') {
|
|
392
|
-
committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld });
|
|
393
|
-
}
|
|
394
|
-
if (committed?.state === 'preflight-passed' || intent.state === 'preflight-passed') {
|
|
395
|
-
prepareArtifactRecoveryFinal(prepared.recovery, Buffer.from(prepared.content, 'utf8'), { lockAlreadyHeld: prepared.lockAlreadyHeld });
|
|
396
|
-
committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld, afterPublish: options.afterPublish });
|
|
227
|
+
fs.writeFileSync(temporary, content, { encoding: 'utf8', flag: 'wx' });
|
|
228
|
+
fs.renameSync(temporary, file);
|
|
229
|
+
}
|
|
230
|
+
finally {
|
|
231
|
+
try {
|
|
232
|
+
fs.unlinkSync(temporary);
|
|
397
233
|
}
|
|
398
|
-
|
|
399
|
-
|
|
234
|
+
catch (error) {
|
|
235
|
+
if (error.code !== 'ENOENT')
|
|
236
|
+
throw error;
|
|
400
237
|
}
|
|
401
|
-
if (!committed || committed.state !== 'passed')
|
|
402
|
-
throw new Error(`ARTIFACT_RECOVERY_STATE_INVALID: commit ended in '${committed?.state ?? intent.state}'`);
|
|
403
|
-
return {
|
|
404
|
-
...prepared.result,
|
|
405
|
-
artifactSha256: committed.finalArtifactSha256,
|
|
406
|
-
semanticDigest: committed.finalSemanticDigest,
|
|
407
|
-
error: null
|
|
408
|
-
};
|
|
409
|
-
}
|
|
410
|
-
catch (error) {
|
|
411
|
-
if (error instanceof Error && error.message.startsWith('TASK_WORKFLOW_FAULT_INJECTED:'))
|
|
412
|
-
throw error;
|
|
413
|
-
return { ...prepared.result, status: 'failed', error: {
|
|
414
|
-
code: 'REVIEW_RECOVERY_COMMIT_FAILED', message: `cannot publish review candidate: ${String(error)}`
|
|
415
|
-
} };
|
|
416
238
|
}
|
|
417
239
|
}
|
|
418
240
|
function finalizeReviewSummaryUnlocked(request, options = {}) {
|
|
@@ -437,10 +259,23 @@ function finalizeReviewSummaryUnlocked(request, options = {}) {
|
|
|
437
259
|
const prepared = prepareReviewSummaryCandidate(request, content, options);
|
|
438
260
|
if (request.dryRun)
|
|
439
261
|
return prepared.result;
|
|
440
|
-
|
|
262
|
+
if (prepared.result.status === 'failed' || !prepared.result.changed)
|
|
263
|
+
return prepared.result;
|
|
264
|
+
try {
|
|
265
|
+
replaceCurrentArtifactAtomically(validated.artifact.path, prepared.content);
|
|
266
|
+
return {
|
|
267
|
+
...prepared.result,
|
|
268
|
+
status: 'applied',
|
|
269
|
+
artifactSha256: sha256Content(prepared.content),
|
|
270
|
+
semanticDigest: canonicalSemanticDigest(prepared.content)
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
catch (error) {
|
|
274
|
+
return failed(request, 'REVIEW_RECOVERY_COMMIT_FAILED', `cannot update current review artifact: ${error instanceof Error ? error.message : String(error)}`, resolved.taskId);
|
|
275
|
+
}
|
|
441
276
|
}
|
|
442
277
|
function finalizeReviewSummary(request, options = {}) {
|
|
443
|
-
const effectiveOptions =
|
|
278
|
+
const effectiveOptions = options;
|
|
444
279
|
if (request.dryRun || options.lockAlreadyHeld)
|
|
445
280
|
return finalizeReviewSummaryUnlocked(request, effectiveOptions);
|
|
446
281
|
const resolved = resolveTaskRef(request.taskRef, { repoRoot: options.repoRoot });
|
|
@@ -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
|
});
|
|
17
20
|
export const TASK_WORKFLOW_OPERATIONS = Object.freeze(Object.keys(TASK_WORKFLOW_COMMANDS));
|
|
18
21
|
//# sourceMappingURL=workflow-command.js.map
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
1
2
|
import { reconcileTaskInvalidation } from './invalidation-command.js';
|
|
2
3
|
import { applyLedgerIntent } from './ledger-intents.js';
|
|
4
|
+
import { isReviewStage, parseLedgerDocument, summarizeLedgerStage, validateLedgerRows } from './ledger.js';
|
|
5
|
+
import { resolveTaskRef } from './resolve-ref.js';
|
|
3
6
|
import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.js';
|
|
4
7
|
import { applyWorkflowWarningIntent } from './workflow-warning-intents.js';
|
|
5
8
|
function failed(code, message) {
|
|
@@ -102,8 +105,27 @@ function ledgerIntent(operation, args) {
|
|
|
102
105
|
}
|
|
103
106
|
function warningIntent(args) {
|
|
104
107
|
const [taskRef, kind] = args;
|
|
105
|
-
if (!taskRef || kind
|
|
108
|
+
if (!taskRef || !['add', 'list', 'set-status'].includes(kind ?? '')) {
|
|
106
109
|
return failed('TASK_WORKFLOW_PAYLOAD_INVALID', 'task workflow warning request is invalid');
|
|
110
|
+
}
|
|
111
|
+
if (kind === 'list') {
|
|
112
|
+
const values = options(args, { '--status': 'status' });
|
|
113
|
+
if (isFailure(values))
|
|
114
|
+
return values;
|
|
115
|
+
return { kind, taskRef, ...(value(values, '--status') ? { status: value(values, '--status') } : {}) };
|
|
116
|
+
}
|
|
117
|
+
if (kind === 'set-status') {
|
|
118
|
+
const values = options(args, { '--id': 'id', '--status': 'status', '--resolution': 'resolution', '--dry-run': 'dryRun' });
|
|
119
|
+
if (isFailure(values))
|
|
120
|
+
return values;
|
|
121
|
+
const missing = required(values, ['--id', '--status', '--resolution']);
|
|
122
|
+
if (missing)
|
|
123
|
+
return missing;
|
|
124
|
+
return {
|
|
125
|
+
kind, taskRef, id: value(values, '--id'), status: value(values, '--status'),
|
|
126
|
+
resolution: value(values, '--resolution'), ...(values['--dry-run'] === true ? { dryRun: true } : {})
|
|
127
|
+
};
|
|
128
|
+
}
|
|
107
129
|
const values = options(args, {
|
|
108
130
|
'--step': 'step', '--severity': 'severity', '--code': 'code', '--target': 'target',
|
|
109
131
|
'--message': 'message', '--action': 'action', '--dry-run': 'dryRun'
|
|
@@ -119,6 +141,32 @@ function warningIntent(args) {
|
|
|
119
141
|
action: value(values, '--action'), ...(values['--dry-run'] === true ? { dryRun: true } : {})
|
|
120
142
|
};
|
|
121
143
|
}
|
|
144
|
+
function stageStatus(repoRoot, args) {
|
|
145
|
+
const [taskRef, kind] = args;
|
|
146
|
+
if (!taskRef || kind !== 'stage-status')
|
|
147
|
+
return failed('TASK_WORKFLOW_PAYLOAD_INVALID', 'task workflow ledger request is invalid');
|
|
148
|
+
const values = options(args, { '--stage': 'stage' });
|
|
149
|
+
if (isFailure(values))
|
|
150
|
+
return values;
|
|
151
|
+
const stage = value(values, '--stage');
|
|
152
|
+
if (!stage || !isReviewStage(stage))
|
|
153
|
+
return failed('TASK_WORKFLOW_PAYLOAD_INVALID', 'stage-status requires a valid review stage');
|
|
154
|
+
const resolved = resolveTaskRef(taskRef, { repoRoot });
|
|
155
|
+
if (!resolved.ok)
|
|
156
|
+
return failed(resolved.code, resolved.message);
|
|
157
|
+
try {
|
|
158
|
+
const ledger = parseLedgerDocument(fs.readFileSync(resolved.taskMdPath, 'utf8'));
|
|
159
|
+
if (!ledger.present)
|
|
160
|
+
return failed('LEDGER_SECTION_MISSING', 'review disagreement ledger section is missing');
|
|
161
|
+
const invalid = validateLedgerRows(ledger.rows);
|
|
162
|
+
if (invalid)
|
|
163
|
+
return failed(invalid.code, invalid.message);
|
|
164
|
+
return { status: 'ready', changed: false, taskId: resolved.taskId, stageStatus: summarizeLedgerStage(ledger.rows, stage), error: null };
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
return failed('LEDGER_DOCUMENT_INVALID', error instanceof Error ? error.message : String(error));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
122
170
|
/**
|
|
123
171
|
* The caller has already selected and authorized the execution boundary.
|
|
124
172
|
* This function deliberately invokes task domains in that process: starting a
|
|
@@ -143,11 +191,15 @@ export function dispatchWorkflowCommand(repoRoot, operation, args) {
|
|
|
143
191
|
...(values['--dry-run'] === true ? { dryRun: true } : {})
|
|
144
192
|
}));
|
|
145
193
|
}
|
|
146
|
-
if (operation === '
|
|
194
|
+
if (operation === 'ledger-stage-status')
|
|
195
|
+
return stageStatus(repoRoot, args);
|
|
196
|
+
if (operation === 'warning-add' || operation === 'warning-list' || operation === 'warning-set-status') {
|
|
147
197
|
const intent = warningIntent(args);
|
|
148
198
|
if (isFailure(intent))
|
|
149
199
|
return intent;
|
|
150
|
-
|
|
200
|
+
if (intent.kind === 'list')
|
|
201
|
+
return applyWorkflowWarningIntent(intent, { repoRoot });
|
|
202
|
+
return withTaskExecutionLock(repoRoot, taskRef, `task-warning.${intent.kind}`, () => applyWorkflowWarningIntent(intent, { repoRoot }));
|
|
151
203
|
}
|
|
152
204
|
const intent = ledgerIntent(operation, args);
|
|
153
205
|
if (isFailure(intent))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"protocolVersion": 3,
|
|
3
|
-
"packageVersion": "0.11.
|
|
4
|
-
"sourceInputHash": "
|
|
5
|
-
"internalExecutableBuildHash": "
|
|
3
|
+
"packageVersion": "0.11.2",
|
|
4
|
+
"sourceInputHash": "0a1ffa8b90a0d157f9b2aabea6a43f946d68a622bde337befe71059f63b3812b",
|
|
5
|
+
"internalExecutableBuildHash": "b6742e1b5d1b997bf7e21e644bfde2bafaa608f7ede7ca308ff61c2cd19da831"
|
|
6
6
|
}
|
|
@@ -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'],
|
|
@@ -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;
|