@fitlab-ai/agent-infra 0.11.9 → 0.11.10
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 +0 -6
- package/dist/lib/git/workflow.js +1 -1
- package/dist/lib/internal/cli-route-inventory.js +0 -3
- package/dist/lib/internal/sandbox-control.js +30 -5
- package/dist/lib/internal/task-finalization.js +14 -23
- package/dist/lib/internal/task-operation-registry.js +2 -3
- package/dist/lib/internal/task-platform-recovery.js +9 -4
- package/dist/lib/platform/completion-backfill.js +2 -1
- package/dist/lib/platform/context.js +4 -2
- package/dist/lib/platform/github-client.js +20 -17
- package/dist/lib/platform/github-provider.js +42 -22
- package/dist/lib/platform/github-release-notes.js +4 -2
- package/dist/lib/platform/in-label-sync.js +2 -1
- package/dist/lib/platform/issue-comments.js +13 -9
- package/dist/lib/platform/operation-coordinator.js +1 -1
- package/dist/lib/platform/pr-review.js +2 -1
- package/dist/lib/platform/provider-bridge.js +4 -5
- package/dist/lib/platform/provider-contract.d.ts +3 -6
- package/dist/lib/platform/provider-contract.js +3 -15
- package/dist/lib/platform/provider-validation.js +9 -4
- package/dist/lib/platform/pull-requests.js +4 -2
- package/dist/lib/sandbox/control/client.js +54 -6
- package/dist/lib/sandbox/control/executor.js +8 -3
- package/dist/lib/sandbox/removal.js +0 -1
- package/dist/lib/task/artifact-command.js +11 -1
- package/dist/lib/task/artifact-lifecycle.js +119 -62
- package/dist/lib/task/artifact-selection.js +1 -1
- package/dist/lib/task/capabilities.js +55 -21
- package/dist/lib/task/commands/status.js +0 -27
- package/dist/lib/task/commit-identity.js +22 -7
- package/dist/lib/task/commit-intent.js +67 -26
- package/dist/lib/task/commit-operation.js +19 -8
- package/dist/lib/task/completion-facts.js +32 -10
- package/dist/lib/task/control-authority.js +2 -4
- package/dist/lib/task/current-contract.js +1 -5
- package/dist/lib/task/events.js +8 -274
- package/dist/lib/task/execution-inputs.js +14 -0
- package/dist/lib/task/finalization-envelope.js +4 -0
- package/dist/lib/task/finalization.js +10 -36
- package/dist/lib/task/frontmatter.js +1 -1
- package/dist/lib/task/ledger-intents.js +4 -7
- package/dist/lib/task/orchestration.js +11 -10
- package/dist/lib/task/platform-operation-journal.js +17 -10
- package/dist/lib/task/platform-operation-recovery.js +95 -28
- package/dist/lib/task/review-fingerprint.js +4 -26
- package/dist/lib/task/verification-engine.js +95 -6
- package/dist/lib/task/write.js +0 -11
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/git/workflow.ts +1 -1
- package/lib/internal/cli-route-inventory.ts +0 -2
- package/lib/internal/sandbox-control.ts +39 -6
- package/lib/internal/task-finalization.ts +16 -24
- package/lib/internal/task-operation-registry.ts +2 -3
- package/lib/internal/task-platform-recovery.ts +9 -4
- package/lib/platform/completion-backfill.ts +2 -1
- package/lib/platform/context.ts +8 -4
- package/lib/platform/github-client.ts +24 -20
- package/lib/platform/github-provider.ts +47 -26
- package/lib/platform/github-release-notes.ts +5 -3
- package/lib/platform/in-label-sync.ts +2 -1
- package/lib/platform/issue-comments.ts +14 -9
- package/lib/platform/operation-coordinator.ts +1 -1
- package/lib/platform/pr-review.ts +2 -1
- package/lib/platform/provider-bridge.ts +10 -9
- package/lib/platform/provider-contract.ts +6 -21
- package/lib/platform/provider-loader.ts +7 -6
- package/lib/platform/provider-validation.ts +30 -8
- package/lib/platform/pull-requests.ts +4 -2
- package/lib/platform/repository-metadata.ts +3 -2
- package/lib/platform/security-alerts.ts +1 -1
- package/lib/platform/types.ts +2 -0
- package/lib/sandbox/control/client.ts +57 -6
- package/lib/sandbox/control/executor.ts +8 -3
- package/lib/sandbox/removal.ts +2 -10
- package/lib/task/artifact-command.ts +10 -1
- package/lib/task/artifact-lifecycle.ts +100 -52
- package/lib/task/artifact-selection.ts +1 -1
- package/lib/task/capabilities.ts +47 -24
- package/lib/task/commands/status.ts +0 -31
- package/lib/task/commit-identity.ts +24 -7
- package/lib/task/commit-intent.ts +58 -24
- package/lib/task/commit-operation.ts +17 -8
- package/lib/task/completion-facts.ts +31 -9
- package/lib/task/control-authority.ts +1 -4
- package/lib/task/current-contract.ts +1 -6
- package/lib/task/events.ts +8 -247
- package/lib/task/execution-inputs.ts +25 -0
- package/lib/task/finalization-envelope.ts +21 -0
- package/lib/task/finalization.ts +10 -35
- package/lib/task/frontmatter.ts +1 -1
- package/lib/task/ledger-intents.ts +4 -7
- package/lib/task/orchestration.ts +11 -17
- package/lib/task/platform-operation-journal.ts +18 -11
- package/lib/task/platform-operation-recovery.ts +90 -31
- package/lib/task/review-fingerprint.ts +4 -26
- package/lib/task/verification-engine.ts +103 -9
- package/lib/task/write.ts +0 -19
- package/package.json +1 -1
- package/templates/.agents/rules/commit-and-pr.en.md +5 -5
- package/templates/.agents/rules/commit-and-pr.zh-CN.md +5 -5
- package/templates/.agents/rules/decision-qualification.en.md +1 -1
- package/templates/.agents/rules/decision-qualification.zh-CN.md +1 -1
- package/templates/.agents/skills/code-task/SKILL.en.md +25 -9
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +26 -9
- package/templates/.agents/skills/code-task/reference/code-rules.en.md +1 -1
- package/templates/.agents/skills/code-task/reference/code-rules.zh-CN.md +1 -1
- package/templates/.agents/skills/code-task/reference/fix-mode.en.md +5 -16
- package/templates/.agents/skills/code-task/reference/fix-mode.zh-CN.md +5 -16
- package/templates/.agents/skills/code-task/reference/report-template.en.md +1 -1
- package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +1 -1
- package/templates/.agents/skills/complete-task/SKILL.en.md +15 -12
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +11 -10
- package/templates/.agents/skills/update-agent-infra/SKILL.en.md +1 -1
- package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +1 -1
- package/templates/.agents/templates/task.zh-CN.md +0 -12
- package/templates/.github/workflows/pr-label.yml +1 -2
- package/dist/lib/internal/task-invalidation.js +0 -63
- package/dist/lib/task/intermediate-cleanup.js +0 -327
- package/dist/lib/task/invalidation-command.js +0 -88
- package/dist/lib/task/invalidation.js +0 -242
- package/lib/internal/task-invalidation.ts +0 -41
- package/lib/task/intermediate-cleanup.ts +0 -393
- package/lib/task/invalidation-command.ts +0 -88
- package/lib/task/invalidation.ts +0 -313
|
@@ -4,7 +4,6 @@ import { createHash, randomUUID } from 'node:crypto';
|
|
|
4
4
|
import { isResourceIdentity } from '../platform/resource-identity.js';
|
|
5
5
|
import { resolveTaskRef } from './resolve-ref.js';
|
|
6
6
|
const JOURNAL_FILE = '.platform-operations.json';
|
|
7
|
-
const MAX_ATTEMPTS = 3;
|
|
8
7
|
function operationId(input) {
|
|
9
8
|
return createHash('sha256')
|
|
10
9
|
.update(`${input.kind}\0${input.target}\0${input.expectedDigest}`)
|
|
@@ -36,8 +35,7 @@ function parseJournal(file, taskId) {
|
|
|
36
35
|
|| !/^[a-f0-9]{64}$/u.test(item.expectedDigest)
|
|
37
36
|
|| !['deferred', 'required'].includes(item.dependency)
|
|
38
37
|
|| !['queued', 'pending', 'unknown', 'succeeded', 'failed'].includes(item.state)
|
|
39
|
-
|| !Number.isSafeInteger(item.attempts) || item.attempts < 0
|
|
40
|
-
|| item.maxAttempts !== MAX_ATTEMPTS
|
|
38
|
+
|| !Number.isSafeInteger(item.attempts) || item.attempts < 0
|
|
41
39
|
|| !(item.lastCode === null || typeof item.lastCode === 'string')
|
|
42
40
|
|| (item.kind === 'issue-metadata'
|
|
43
41
|
? !item.issueMetadata || typeof item.issueMetadata.requirements !== 'boolean'
|
|
@@ -120,7 +118,13 @@ function recordPlatformOperation(input) {
|
|
|
120
118
|
const { taskId, file } = resolveJournal(input.taskRef, input.cwd);
|
|
121
119
|
const journal = parseJournal(file, taskId);
|
|
122
120
|
const id = operationId(input);
|
|
123
|
-
const
|
|
121
|
+
const targetIndex = journal.operations.findIndex((item) => item.id === id);
|
|
122
|
+
const replacementIndex = input.replaceOperationId
|
|
123
|
+
? journal.operations.findIndex((item) => item.id === input.replaceOperationId)
|
|
124
|
+
: -1;
|
|
125
|
+
const target = targetIndex >= 0 ? journal.operations[targetIndex] : undefined;
|
|
126
|
+
const replacement = replacementIndex >= 0 ? journal.operations[replacementIndex] : undefined;
|
|
127
|
+
const previousAttempts = Math.max(target?.attempts ?? 0, replacement?.attempts ?? 0);
|
|
124
128
|
const next = {
|
|
125
129
|
id,
|
|
126
130
|
kind: input.kind,
|
|
@@ -128,8 +132,7 @@ function recordPlatformOperation(input) {
|
|
|
128
132
|
expectedDigest: input.expectedDigest,
|
|
129
133
|
dependency: input.dependency,
|
|
130
134
|
state: input.state,
|
|
131
|
-
attempts:
|
|
132
|
-
maxAttempts: MAX_ATTEMPTS,
|
|
135
|
+
attempts: previousAttempts + (input.state === 'pending' ? 1 : 0),
|
|
133
136
|
lastCode: input.lastCode ?? null,
|
|
134
137
|
...(input.issueMetadata ? { issueMetadata: input.issueMetadata } : {}),
|
|
135
138
|
...(input.issueCreate ? { issueCreate: input.issueCreate } : {}),
|
|
@@ -138,9 +141,13 @@ function recordPlatformOperation(input) {
|
|
|
138
141
|
...(input.pullRequestReview ? { pullRequestReview: input.pullRequestReview } : {}),
|
|
139
142
|
updatedAt: new Date().toISOString()
|
|
140
143
|
};
|
|
141
|
-
const operations =
|
|
142
|
-
? journal.operations.
|
|
143
|
-
|
|
144
|
+
const operations = replacementIndex >= 0
|
|
145
|
+
? journal.operations.flatMap((item, index) => index === replacementIndex
|
|
146
|
+
? [next]
|
|
147
|
+
: index === targetIndex ? [] : [item])
|
|
148
|
+
: targetIndex >= 0
|
|
149
|
+
? journal.operations.map((item, index) => index === targetIndex ? next : item)
|
|
150
|
+
: [...journal.operations, next];
|
|
144
151
|
writeJournal(file, { version: 1, taskId, operations });
|
|
145
152
|
return next;
|
|
146
153
|
}
|
|
@@ -148,5 +155,5 @@ function readPlatformOperationJournal(taskRef, cwd) {
|
|
|
148
155
|
const { taskId, file } = resolveJournal(taskRef, cwd);
|
|
149
156
|
return parseJournal(file, taskId);
|
|
150
157
|
}
|
|
151
|
-
export { JOURNAL_FILE as PLATFORM_OPERATION_JOURNAL_FILE,
|
|
158
|
+
export { JOURNAL_FILE as PLATFORM_OPERATION_JOURNAL_FILE, operationId, recordPlatformOperation, readPlatformOperationJournal };
|
|
152
159
|
//# sourceMappingURL=platform-operation-journal.js.map
|
|
@@ -18,6 +18,8 @@ import { canonicalizeSummaryBody } from '../platform/comment-safety.js';
|
|
|
18
18
|
import { platformResult } from '../platform/types.js';
|
|
19
19
|
import { resolveTaskRef } from './resolve-ref.js';
|
|
20
20
|
import { readPlatformOperationJournal, recordPlatformOperation } from './platform-operation-journal.js';
|
|
21
|
+
import { providerErrorRetryable } from '../platform/provider-validation.js';
|
|
22
|
+
const DEFAULT_RECOVERY_ATTEMPTS = 3;
|
|
21
23
|
function labelsMatchOwnedPrefix(actual, expected, prefix) {
|
|
22
24
|
return actual.filter((label) => label.startsWith(prefix)).sort().join('\0')
|
|
23
25
|
=== expected.filter((label) => label.startsWith(prefix)).sort().join('\0');
|
|
@@ -26,7 +28,7 @@ function fieldsMatchExpected(actual, expected) {
|
|
|
26
28
|
return Object.entries(expected).every(([name, value]) => actual[name] === value);
|
|
27
29
|
}
|
|
28
30
|
function result(status, recovered, pending, error = null) {
|
|
29
|
-
return { status, changed: recovered.length > 0, recovered, pending, error };
|
|
31
|
+
return { status, changed: recovered.length > 0, recovered, pending, error: error ? { code: error.code, message: error.message } : null };
|
|
30
32
|
}
|
|
31
33
|
function summaryPayload(taskDir, taskId) {
|
|
32
34
|
const file = path.join(taskDir, '.delivery-summary.json');
|
|
@@ -198,7 +200,7 @@ async function replayPullRequestSummary(taskId, operation, agent, cwd, client) {
|
|
|
198
200
|
? await resolved.value.provider.comments.list({ context: providerOperationContext(resolved.value), parent: identity })
|
|
199
201
|
: unsupportedProviderOperation(resolved.value.provider, 'comments.list');
|
|
200
202
|
if (!listed.ok)
|
|
201
|
-
return platformResult(listed.error.
|
|
203
|
+
return platformResult(providerErrorRetryable(listed.error.code) ? 'blocked' : 'failed', { error: providerError(listed.error, 'PLATFORM_PROVIDER_OPERATION_FAILED') });
|
|
202
204
|
const marker = `<!-- sync-pr:${taskId}:summary -->`;
|
|
203
205
|
const summaries = listed.value.filter((comment) => normalizeCommentContent(comment.body).split('\n', 1)[0] === marker);
|
|
204
206
|
if (summaries.length > 1)
|
|
@@ -293,7 +295,7 @@ async function replayPullRequest(taskId, operation, agent, cwd) {
|
|
|
293
295
|
}
|
|
294
296
|
return recoverCreatedPullRequest(taskId, { agent, cwd, base: intent.baseRef || '', head: intent.headRef || '', skipQueue: true });
|
|
295
297
|
}
|
|
296
|
-
async function
|
|
298
|
+
async function recoverPlatformOperationsOnce(taskRef, selection, options) {
|
|
297
299
|
const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
|
|
298
300
|
if (!resolved.ok)
|
|
299
301
|
return result('failed', [], [], { code: resolved.code, message: resolved.message, retryable: false });
|
|
@@ -307,34 +309,23 @@ async function recoverPlatformOperations(taskRef, selection, options) {
|
|
|
307
309
|
}
|
|
308
310
|
const candidates = journal.operations.filter((operation) => operation.id !== options.excludeId
|
|
309
311
|
&& (operation.state === 'queued' || operation.state === 'pending' || operation.state === 'unknown'
|
|
310
|
-
|| (operation.state === 'failed' && operation.lastCode !== 'PLATFORM_OPERATION_SUPERSEDED')))
|
|
311
|
-
.filter((operation) => selection === 'all' || operation.dependency === selection)
|
|
312
|
-
.sort((left, right) => left.updatedAt.localeCompare(right.updatedAt));
|
|
312
|
+
|| (operation.state === 'failed' && operation.lastCode !== 'PLATFORM_OPERATION_SUPERSEDED')));
|
|
313
313
|
if (!candidates.length)
|
|
314
314
|
return result('no-op', [], []);
|
|
315
|
+
const head = candidates[0];
|
|
316
|
+
if (selection !== 'all' && head.dependency !== selection) {
|
|
317
|
+
return result('blocked', [], candidates.map((operation) => operation.id), {
|
|
318
|
+
code: 'PLATFORM_OPERATION_RECOVERY_PENDING',
|
|
319
|
+
message: 'The first unresolved platform operation belongs to another recovery selection',
|
|
320
|
+
retryable: true
|
|
321
|
+
});
|
|
322
|
+
}
|
|
315
323
|
const limit = options.limit === undefined ? candidates.length : Math.max(1, options.limit);
|
|
316
324
|
const recovered = [];
|
|
317
325
|
const pending = [];
|
|
318
326
|
const selected = candidates.slice(0, limit);
|
|
319
327
|
for (const [index, operation] of selected.entries()) {
|
|
320
|
-
|
|
321
|
-
pending.push(operation.id);
|
|
322
|
-
return result('blocked', recovered, pending, {
|
|
323
|
-
code: operation.lastCode || 'PLATFORM_OPERATION_FAILED',
|
|
324
|
-
message: 'A previous platform operation failed permanently; later writes remain queued',
|
|
325
|
-
retryable: false
|
|
326
|
-
});
|
|
327
|
-
}
|
|
328
|
-
if (operation.attempts >= operation.maxAttempts) {
|
|
329
|
-
pending.push(operation.id, ...selected.slice(index + 1).map((next) => next.id));
|
|
330
|
-
pending.push(...candidates.slice(limit).map((next) => next.id));
|
|
331
|
-
return result('blocked', recovered, pending, {
|
|
332
|
-
code: operation.lastCode || 'PLATFORM_OPERATION_RETRY_LIMIT_REACHED',
|
|
333
|
-
message: 'A previous platform operation exhausted its recovery attempts; later writes remain queued',
|
|
334
|
-
retryable: false
|
|
335
|
-
});
|
|
336
|
-
}
|
|
337
|
-
if (operation.kind !== 'issue-create') {
|
|
328
|
+
{
|
|
338
329
|
try {
|
|
339
330
|
recordPlatformOperation({
|
|
340
331
|
taskRef: resolved.taskId, cwd: resolved.repoRoot, kind: operation.kind,
|
|
@@ -368,8 +359,8 @@ async function recoverPlatformOperations(taskRef, selection, options) {
|
|
|
368
359
|
if (current.id !== operation.id) {
|
|
369
360
|
try {
|
|
370
361
|
recordPlatformOperation({ taskRef: resolved.taskId, cwd: resolved.repoRoot, kind: operation.kind, target: operation.target,
|
|
371
|
-
expectedDigest:
|
|
372
|
-
state: '
|
|
362
|
+
expectedDigest: current.expectedDigest, issueMetadata: current.issueMetadata, dependency: operation.dependency,
|
|
363
|
+
state: 'queued', replaceOperationId: operation.id });
|
|
373
364
|
targetOperation = { ...operation, expectedDigest: current.expectedDigest, issueMetadata: current.issueMetadata, id: current.id };
|
|
374
365
|
}
|
|
375
366
|
catch (error) {
|
|
@@ -396,7 +387,7 @@ async function recoverPlatformOperations(taskRef, selection, options) {
|
|
|
396
387
|
if (current.id !== operation.id) {
|
|
397
388
|
try {
|
|
398
389
|
recordPlatformOperation({ taskRef: resolved.taskId, cwd: resolved.repoRoot, kind: operation.kind, target: operation.target,
|
|
399
|
-
expectedDigest:
|
|
390
|
+
expectedDigest: current.expectedDigest, dependency: operation.dependency, state: 'queued', replaceOperationId: operation.id });
|
|
400
391
|
targetOperation = { ...operation, expectedDigest: current.expectedDigest, id: current.id };
|
|
401
392
|
}
|
|
402
393
|
catch (error) {
|
|
@@ -437,7 +428,7 @@ async function recoverPlatformOperations(taskRef, selection, options) {
|
|
|
437
428
|
...(operation.pullRequestSummary ? { pullRequestSummary: operation.pullRequestSummary } : {}),
|
|
438
429
|
...(operation.pullRequestReview ? { pullRequestReview: operation.pullRequestReview } : {}),
|
|
439
430
|
dependency: operation.dependency,
|
|
440
|
-
state: succeeded ? 'succeeded' :
|
|
431
|
+
state: succeeded ? 'succeeded' : 'unknown',
|
|
441
432
|
lastCode: remote.error?.code ?? null
|
|
442
433
|
});
|
|
443
434
|
}
|
|
@@ -455,5 +446,81 @@ async function recoverPlatformOperations(taskRef, selection, options) {
|
|
|
455
446
|
pending.push(...remaining);
|
|
456
447
|
return result(pending.length ? 'blocked' : recovered.length ? 'applied' : 'no-op', recovered, pending, pending.length ? { code: 'PLATFORM_OPERATION_RECOVERY_PENDING', message: 'Platform operations remain pending after the recovery budget', retryable: true } : null);
|
|
457
448
|
}
|
|
449
|
+
async function recoverPlatformOperations(taskRef, selection, options) {
|
|
450
|
+
const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
|
|
451
|
+
if (!resolved.ok)
|
|
452
|
+
return result('failed', [], [], { code: resolved.code, message: resolved.message, retryable: false });
|
|
453
|
+
let initial;
|
|
454
|
+
try {
|
|
455
|
+
initial = readPlatformOperationJournal(resolved.taskId, resolved.repoRoot);
|
|
456
|
+
}
|
|
457
|
+
catch (error) {
|
|
458
|
+
const value = error;
|
|
459
|
+
return result('failed', [], [], { code: value.code || 'PLATFORM_OPERATION_JOURNAL_INVALID', message: value.message || String(error), retryable: false });
|
|
460
|
+
}
|
|
461
|
+
const eligible = (operation) => operation.id !== options.excludeId
|
|
462
|
+
&& (operation.state === 'queued' || operation.state === 'pending' || operation.state === 'unknown'
|
|
463
|
+
|| (operation.state === 'failed' && operation.lastCode !== 'PLATFORM_OPERATION_SUPERSEDED'));
|
|
464
|
+
const first = initial.operations.filter(eligible);
|
|
465
|
+
if (!first.length)
|
|
466
|
+
return result('no-op', [], []);
|
|
467
|
+
const operationLimit = options.limit === undefined ? first.length : Math.max(1, options.limit);
|
|
468
|
+
const attemptLimit = options.attempts ?? DEFAULT_RECOVERY_ATTEMPTS;
|
|
469
|
+
if (!Number.isSafeInteger(attemptLimit) || attemptLimit <= 0) {
|
|
470
|
+
return result('failed', [], [], { code: 'PLATFORM_OPERATION_RECOVERY_PAYLOAD_INVALID', message: 'Recovery attempts must be a positive safe integer', retryable: false });
|
|
471
|
+
}
|
|
472
|
+
const recovered = [];
|
|
473
|
+
for (let operationIndex = 0; operationIndex < operationLimit; operationIndex += 1) {
|
|
474
|
+
let latest;
|
|
475
|
+
try {
|
|
476
|
+
latest = readPlatformOperationJournal(resolved.taskId, resolved.repoRoot);
|
|
477
|
+
}
|
|
478
|
+
catch (error) {
|
|
479
|
+
const value = error;
|
|
480
|
+
return result('failed', recovered, [], { code: value.code || 'PLATFORM_OPERATION_JOURNAL_INVALID', message: value.message || String(error), retryable: false });
|
|
481
|
+
}
|
|
482
|
+
const head = latest.operations.find(eligible);
|
|
483
|
+
if (!head)
|
|
484
|
+
break;
|
|
485
|
+
if (selection !== 'all' && head.dependency !== selection) {
|
|
486
|
+
const pending = latest.operations.filter(eligible).map((operation) => operation.id);
|
|
487
|
+
return result('blocked', recovered, pending, {
|
|
488
|
+
code: 'PLATFORM_OPERATION_RECOVERY_PENDING',
|
|
489
|
+
message: 'The first unresolved platform operation belongs to another recovery selection',
|
|
490
|
+
retryable: true
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
let last = null;
|
|
494
|
+
for (let attempt = 0; attempt < attemptLimit; attempt += 1) {
|
|
495
|
+
last = await recoverPlatformOperationsOnce(taskRef, selection, { ...options, limit: 1 });
|
|
496
|
+
recovered.push(...last.recovered.filter((id) => !recovered.includes(id)));
|
|
497
|
+
if (last.recovered.length > 0 || last.status === 'applied' || last.status === 'no-op')
|
|
498
|
+
break;
|
|
499
|
+
}
|
|
500
|
+
if (!last || (last.recovered.length === 0 && last.status !== 'applied' && last.status !== 'no-op')) {
|
|
501
|
+
let current;
|
|
502
|
+
try {
|
|
503
|
+
current = readPlatformOperationJournal(resolved.taskId, resolved.repoRoot);
|
|
504
|
+
}
|
|
505
|
+
catch {
|
|
506
|
+
return result('blocked', recovered, [head.id], last?.error ?? null);
|
|
507
|
+
}
|
|
508
|
+
const pending = current.operations.filter(eligible).map((operation) => operation.id);
|
|
509
|
+
return result(last?.status === 'failed' ? 'failed' : 'blocked', recovered, pending, last?.error ?? {
|
|
510
|
+
code: 'PLATFORM_OPERATION_RECOVERY_PENDING',
|
|
511
|
+
message: 'The first queued platform operation remains unresolved after this recovery run',
|
|
512
|
+
retryable: true
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
let remaining;
|
|
517
|
+
try {
|
|
518
|
+
remaining = readPlatformOperationJournal(resolved.taskId, resolved.repoRoot).operations.filter(eligible).map((operation) => operation.id);
|
|
519
|
+
}
|
|
520
|
+
catch {
|
|
521
|
+
remaining = [];
|
|
522
|
+
}
|
|
523
|
+
return result(remaining.length ? 'blocked' : recovered.length ? 'applied' : 'no-op', recovered, remaining, remaining.length ? { code: 'PLATFORM_OPERATION_RECOVERY_PENDING', message: 'Platform operations remain queued after this recovery run', retryable: true } : null);
|
|
524
|
+
}
|
|
458
525
|
export { fieldsMatchExpected, labelsMatchOwnedPrefix, recoverPlatformOperations };
|
|
459
526
|
//# sourceMappingURL=platform-operation-recovery.js.map
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
import { invalidationBlocks, isArtifactInvalidated, parseInvalidationDocument } from "./invalidation.js";
|
|
3
|
+
import { inspectArtifactDirectory } from "./artifact-lifecycle.js";
|
|
5
4
|
const REVIEWED_TREE_RE = /^[a-f0-9]{40,64}$/;
|
|
6
5
|
export function parseReviewedGitTree(value) {
|
|
7
6
|
return REVIEWED_TREE_RE.test(value) ? value : null;
|
|
@@ -28,32 +27,11 @@ export function resolvePostReviewGlobs(config = {}, reviewConfig = {}) {
|
|
|
28
27
|
return [":/", ...excludes.map((p) => `:(top,exclude)${p}`)];
|
|
29
28
|
}
|
|
30
29
|
export function findAuthoritativeReviewCodeArtifact(taskDir) {
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
if (round === 0) {
|
|
30
|
+
const inventory = inspectArtifactDirectory(taskDir, 'review-code');
|
|
31
|
+
if (inventory.status !== 'ready' || !inventory.latest) {
|
|
34
32
|
return { ok: false, round: 0, fileName: null, path: null, error: null };
|
|
35
33
|
}
|
|
36
|
-
const fileName =
|
|
37
|
-
let taskContent;
|
|
38
|
-
try {
|
|
39
|
-
taskContent = fs.readFileSync(path.join(taskDir, "task.md"), "utf8");
|
|
40
|
-
}
|
|
41
|
-
catch (error) {
|
|
42
|
-
return {
|
|
43
|
-
ok: false, round, fileName, path: null,
|
|
44
|
-
error: error instanceof Error ? error.message : String(error)
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
const invalidation = parseInvalidationDocument(taskContent);
|
|
48
|
-
if (!invalidation.ok) {
|
|
49
|
-
return { ok: false, round, fileName, path: null, error: invalidation.message };
|
|
50
|
-
}
|
|
51
|
-
if (invalidationBlocks(invalidation.document)) {
|
|
52
|
-
return { ok: false, round, fileName, path: null, error: "artifact invalidation is incomplete; reconcile task.md before using review-code" };
|
|
53
|
-
}
|
|
54
|
-
if (isArtifactInvalidated(invalidation.document, "review-code", fileName)) {
|
|
55
|
-
return { ok: false, round, fileName, path: null, error: `${fileName} is invalidated; re-run review-code` };
|
|
56
|
-
}
|
|
34
|
+
const { round, name: fileName } = inventory.latest;
|
|
57
35
|
return {
|
|
58
36
|
ok: true,
|
|
59
37
|
round,
|
|
@@ -19,7 +19,7 @@ import { inspectDecisionDetailDuplicates } from "./decision-details.js";
|
|
|
19
19
|
import { parseImplementationInputs } from "./implementation-inputs.js";
|
|
20
20
|
import { scanVisibleMarkdown } from "./markdown.js";
|
|
21
21
|
import { loadVerificationConfig } from "./verification-config.js";
|
|
22
|
-
import { inspectActivityLog } from "./activity-log.js";
|
|
22
|
+
import { inspectActivityLog, pairEntries } from "./activity-log.js";
|
|
23
23
|
import { parseWorkflowWarnings } from "./workflow-warnings.js";
|
|
24
24
|
import { OrchestrationStateError, readRun } from "./orchestration.js";
|
|
25
25
|
import { readPrDeliveryFact } from "./pr-delivery-fact.js";
|
|
@@ -30,9 +30,11 @@ import { normalizeVerificationRecord } from './gate-policy.js';
|
|
|
30
30
|
import { manualValidationFinalSummaryProjectionMatches } from "./manual-validation-receipt.js";
|
|
31
31
|
import { readManualValidationCompletion } from "./manual-validation-completion.js";
|
|
32
32
|
import { sha256File } from "./artifact-receipts.js";
|
|
33
|
+
import { inspectArtifactDirectory } from "./artifact-lifecycle.js";
|
|
34
|
+
import { parseArtifactName } from "./artifact-name.js";
|
|
33
35
|
import { summaryCommentState } from "../platform/pr-summary.js";
|
|
34
36
|
import { taskIssueIdentity } from "../platform/task-identities.js";
|
|
35
|
-
import { hasArtifactCompletionFact, hasArtifactCompletionLog } from './completion-facts.js';
|
|
37
|
+
import { hasArtifactCompletionFact, hasArtifactCompletionLog, parseCompletionFacts } from './completion-facts.js';
|
|
36
38
|
import { inspectReviewIdentity } from './review-identity.js';
|
|
37
39
|
const TASK_ENUMS = {
|
|
38
40
|
type: ["feature", "bugfix", "refactor", "docs", "chore"],
|
|
@@ -264,7 +266,7 @@ function checkOrchestrationEvidence({ taskDir }) {
|
|
|
264
266
|
return passResult('orchestration-evidence', 'Persisted orchestration model and delegation evidence is internally consistent');
|
|
265
267
|
}
|
|
266
268
|
// === Check Functions ===
|
|
267
|
-
function checkTaskMeta({ taskDir, config, repositoryRoot }) {
|
|
269
|
+
function checkTaskMeta({ taskDir, config, repositoryRoot, skillName, artifactFile }) {
|
|
268
270
|
const task = loadTask(taskDir);
|
|
269
271
|
if (!task.ok) {
|
|
270
272
|
return failResult("task-meta", task.message);
|
|
@@ -306,7 +308,22 @@ function checkTaskMeta({ taskDir, config, repositoryRoot }) {
|
|
|
306
308
|
}
|
|
307
309
|
const expectedStep = config.expected_step;
|
|
308
310
|
if (expectedStep && metadata.current_step !== expectedStep) {
|
|
309
|
-
|
|
311
|
+
let validLifecycleArtifact = false;
|
|
312
|
+
if (currentTask) {
|
|
313
|
+
const activityConfig = loadVerificationConfig(repositoryRoot, skillName).checks?.["activity-log"];
|
|
314
|
+
const { section, invalidEntries } = inspectActivityLog(task.content);
|
|
315
|
+
if (activityConfig && section && invalidEntries.length === 0) {
|
|
316
|
+
let latestAction = "";
|
|
317
|
+
for (const entry of section.entries) {
|
|
318
|
+
if (!ACTIVITY_LOG_STARTED_RE.test(entry.step))
|
|
319
|
+
latestAction = entry.step;
|
|
320
|
+
}
|
|
321
|
+
validLifecycleArtifact = checkArtifactLifecycleActivityLog(taskDir, artifactFile, skillName, activityConfig, task, section.entries, latestAction)?.status === "pass";
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (!validLifecycleArtifact) {
|
|
325
|
+
return failResult("task-meta", `Expected current_step '${expectedStep}', got '${metadata.current_step || "(empty)"}'`);
|
|
326
|
+
}
|
|
310
327
|
}
|
|
311
328
|
const expectedStatus = config.expected_status;
|
|
312
329
|
if (expectedStatus && metadata.status !== expectedStatus) {
|
|
@@ -604,7 +621,73 @@ function checkImplementationInput({ taskDir, artifactFile }) {
|
|
|
604
621
|
}
|
|
605
622
|
return passResult("implementation-input", `${actionDecision} matches Activity Log, report, and task table`);
|
|
606
623
|
}
|
|
607
|
-
|
|
624
|
+
const ARTIFACT_ACTIVITY_STAGES = {
|
|
625
|
+
"analyze-task": { family: "analysis", event: "analyze.completed" },
|
|
626
|
+
"plan-task": { family: "plan", event: "plan.completed" },
|
|
627
|
+
"review-analysis": { family: "review-analysis", event: "review-analysis.completed" },
|
|
628
|
+
"review-plan": { family: "review-plan", event: "review-plan.completed" },
|
|
629
|
+
"code-task": { family: "code", event: "code.completed" },
|
|
630
|
+
"review-code": { family: "review-code", event: "review-code.completed" }
|
|
631
|
+
};
|
|
632
|
+
function checkArtifactLifecycleActivityLog(taskDir, artifactFile, skillName, config, task, entries, latestAction) {
|
|
633
|
+
const stage = ARTIFACT_ACTIVITY_STAGES[skillName];
|
|
634
|
+
if (!stage)
|
|
635
|
+
return null;
|
|
636
|
+
const inventory = inspectArtifactDirectory(taskDir, stage.family);
|
|
637
|
+
if (inventory.status !== "ready" || !inventory.latest) {
|
|
638
|
+
return failResult("activity-log", `Current ${stage.family} artifact is unavailable: ${inventory.error?.message ?? "no artifact exists"}`);
|
|
639
|
+
}
|
|
640
|
+
const output = artifactFile || inventory.latest.name;
|
|
641
|
+
if (output !== inventory.latest.name) {
|
|
642
|
+
return failResult("activity-log", `Artifact '${output}' is not the current ${stage.family} artifact; rerun ${skillName} for '${inventory.latest.name}'`);
|
|
643
|
+
}
|
|
644
|
+
const identity = parseArtifactName(output);
|
|
645
|
+
if (!identity || identity.family !== stage.family) {
|
|
646
|
+
return failResult("activity-log", `Artifact '${output}' does not belong to ${stage.family}`);
|
|
647
|
+
}
|
|
648
|
+
if (!hasArtifactCompletionFact(task.content, path.join(taskDir, output), stage.event)) {
|
|
649
|
+
return failResult("activity-log", `Completion fact for '${output}' is missing or stale; rerun ${skillName} to record a current completion`);
|
|
650
|
+
}
|
|
651
|
+
const frontmatter = parseTypedTaskFrontmatter(task.content);
|
|
652
|
+
const fact = parseCompletionFacts(frontmatter.completion_facts).find((item) => item.event === stage.event && item.output === output);
|
|
653
|
+
if (!fact)
|
|
654
|
+
return failResult("activity-log", `Completion fact for '${output}' is missing; rerun ${skillName}`);
|
|
655
|
+
if (!Array.isArray(fact.lifecycleInputs)) {
|
|
656
|
+
return failResult("activity-log", `Completion fact for '${output}' has no input snapshot; rerun ${skillName}`);
|
|
657
|
+
}
|
|
658
|
+
for (const input of fact.lifecycleInputs) {
|
|
659
|
+
if (!input || typeof input.name !== "string" || !/^[a-f0-9]{64}$/u.test(input.sha256)) {
|
|
660
|
+
return failResult("activity-log", `Completion fact for '${output}' has an invalid input snapshot; rerun ${skillName}`);
|
|
661
|
+
}
|
|
662
|
+
const inputIdentity = parseArtifactName(input.name);
|
|
663
|
+
if (!inputIdentity)
|
|
664
|
+
return failResult("activity-log", `Completion fact for '${output}' has invalid input '${input.name}'; rerun ${skillName}`);
|
|
665
|
+
const inputInventory = inspectArtifactDirectory(taskDir, inputIdentity.family);
|
|
666
|
+
if (inputInventory.status !== "ready" || inputInventory.latest?.name !== input.name
|
|
667
|
+
|| sha256File(path.join(taskDir, input.name)) !== input.sha256) {
|
|
668
|
+
return failResult("activity-log", `Input '${input.name}' changed or is no longer current; rerun ${skillName} to refresh '${output}'`);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
const expectedAction = new RegExp(config.expected_action_pattern);
|
|
672
|
+
const roundAction = entries.find((entry) => {
|
|
673
|
+
if (ACTIVITY_LOG_STARTED_RE.test(entry.step))
|
|
674
|
+
return false;
|
|
675
|
+
if (!expectedAction.test(entry.step))
|
|
676
|
+
return false;
|
|
677
|
+
expectedAction.lastIndex = 0;
|
|
678
|
+
const actionRound = /\(Round (\d+)/.exec(entry.step)?.[1];
|
|
679
|
+
return actionRound === String(identity.round) && entry.note.includes(`→ ${output}`);
|
|
680
|
+
});
|
|
681
|
+
if (!roundAction) {
|
|
682
|
+
return failResult("activity-log", `No completed Activity Log row records '${output}' for ${skillName} Round ${identity.round}`);
|
|
683
|
+
}
|
|
684
|
+
if (skillName === "plan-task" && /^(?:Human Decision|Decision)\b/.test(latestAction)
|
|
685
|
+
&& expectedAction.test(latestAction)) {
|
|
686
|
+
return passResult("activity-log", `Plan completion '${output}' is current; latest action '${latestAction}' is a human decision`);
|
|
687
|
+
}
|
|
688
|
+
return passResult("activity-log", `Current ${stage.family} completion '${output}' is valid for Round ${identity.round}`);
|
|
689
|
+
}
|
|
690
|
+
function checkActivityLog({ taskDir, config, skillName, artifactFile }) {
|
|
608
691
|
const task = loadTask(taskDir);
|
|
609
692
|
if (!task.ok) {
|
|
610
693
|
return failResult("activity-log", task.message);
|
|
@@ -638,6 +721,9 @@ function checkActivityLog({ taskDir, config }) {
|
|
|
638
721
|
doneActions.push(action);
|
|
639
722
|
}
|
|
640
723
|
}
|
|
724
|
+
const artifactLifecycleResult = checkArtifactLifecycleActivityLog(taskDir, artifactFile, skillName, config, task, entries, latestAction);
|
|
725
|
+
if (artifactLifecycleResult)
|
|
726
|
+
return artifactLifecycleResult;
|
|
641
727
|
if (config.expected_action_pattern && !new RegExp(config.expected_action_pattern).test(latestAction)) {
|
|
642
728
|
const expected = new RegExp(config.expected_action_pattern);
|
|
643
729
|
let index = doneActions.length - 1;
|
|
@@ -799,7 +885,10 @@ async function checkManualValidation({ taskDir, repositoryRoot }) {
|
|
|
799
885
|
const inspectedLog = inspectActivityLog(task.content);
|
|
800
886
|
if (!inspectedLog.section)
|
|
801
887
|
return failResult("manual-validation", "Activity Log is missing or invalid");
|
|
802
|
-
const
|
|
888
|
+
const reviewRow = pairEntries(inspectedLog.section.entries).find((row) => row.step === `Review Code (Round ${review.round})` && row.started !== '' && row.done !== '');
|
|
889
|
+
const reviewDoneIndex = reviewRow
|
|
890
|
+
? inspectedLog.section.entries.findIndex((entry) => entry.step === reviewRow.step && entry.time === reviewRow.done)
|
|
891
|
+
: -1;
|
|
803
892
|
const completedIndex = inspectedLog.section.entries.findIndex((entry) => entry.step === "Complete Manual Validation"
|
|
804
893
|
&& entry.note.includes(`Manual validation passed → ${artifactName};`)
|
|
805
894
|
&& entry.note.includes(`transaction=${receipt.transactionId};`)
|
package/dist/lib/task/write.js
CHANGED
|
@@ -6,7 +6,6 @@ import { parseTypedTaskFrontmatter, updateTaskFrontmatter } from './frontmatter.
|
|
|
6
6
|
import { resolveTaskRef } from './resolve-ref.js';
|
|
7
7
|
import { mutateTableRow, upsertSection } from './sections.js';
|
|
8
8
|
import { validateCurrentTaskContract } from './current-contract.js';
|
|
9
|
-
import { invalidationBlocks, parseInvalidationDocument } from './invalidation.js';
|
|
10
9
|
const DEFAULT_FILE_SYSTEM = {
|
|
11
10
|
readFileSync: (file) => fs.readFileSync(file, 'utf8'),
|
|
12
11
|
statModeSync: (file) => fs.statSync(file).mode,
|
|
@@ -91,16 +90,6 @@ function writeTaskCore(request, options = {}) {
|
|
|
91
90
|
if (!contract.ok) {
|
|
92
91
|
return failure(request, identity, contract.code, contract.message);
|
|
93
92
|
}
|
|
94
|
-
if (invalidationBlocks(contract.invalidation)
|
|
95
|
-
&& options.invalidationContext !== 'source-completion'
|
|
96
|
-
&& options.invalidationContext !== 'reconcile') {
|
|
97
|
-
return failure(request, identity, 'TASK_INVALIDATION_BLOCKED', 'task has an incomplete artifact invalidation operation; reconcile it before writing downstream state');
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
else {
|
|
101
|
-
const invalidation = parseInvalidationDocument(original);
|
|
102
|
-
if (!invalidation.ok)
|
|
103
|
-
return failure(request, identity, 'TASK_INVALIDATION_INVALID', invalidation.message);
|
|
104
93
|
}
|
|
105
94
|
let candidate = original;
|
|
106
95
|
const operations = [];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"protocolVersion": 3,
|
|
3
|
-
"packageVersion": "0.11.
|
|
4
|
-
"sourceInputHash": "
|
|
5
|
-
"internalExecutableBuildHash": "
|
|
3
|
+
"packageVersion": "0.11.10",
|
|
4
|
+
"sourceInputHash": "3500e8eb7fac8bbad3d95124002f8042af3d160185a5b635eb9b4d8f88a991cb",
|
|
5
|
+
"internalExecutableBuildHash": "51a4c472b6a54b3ca3c28ab049683175d3ea4730fe560868f853cd603b5a98f1"
|
|
6
6
|
}
|
package/lib/git/workflow.ts
CHANGED
|
@@ -100,7 +100,7 @@ function commitExplicitPaths(input: { cwd: string; paths: readonly string[]; mes
|
|
|
100
100
|
const tree = value(runner, input.cwd, ['write-tree']);
|
|
101
101
|
if (tree !== input.expectedTree) return { status: 'failed' as const, changed: false, snapshot: inspectGitWorkflow(input.cwd, runner).snapshot, operations: [{ name: 'stage', status: 'applied' as const }], error: { code: 'GIT_TREE_MISMATCH', message: `Expected staged tree ${input.expectedTree}, received ${tree ?? 'unavailable'}` } };
|
|
102
102
|
}
|
|
103
|
-
const commit = run(runner, input.cwd, ['commit', '-m', input.message]);
|
|
103
|
+
const commit = run(runner, input.cwd, ['commit', '--cleanup=verbatim', '-m', input.message]);
|
|
104
104
|
const after = inspectGitWorkflow(input.cwd, runner);
|
|
105
105
|
if (commit.status !== 0) return { status: 'failed' as const, changed: false, snapshot: after.snapshot, operations: [{ name: 'commit', status: 'failed' as const, message: commit.stderr.trim() }], error: { code: 'GIT_COMMIT_FAILED', message: commit.stderr.trim() } };
|
|
106
106
|
return { status: 'applied' as const, changed: true, snapshot: after.snapshot, operations: [{ name: 'stage', status: 'applied' as const }, { name: 'commit', status: 'applied' as const }], error: null };
|
|
@@ -60,7 +60,6 @@ export const INTERNAL_HANDLER_ROUTE_SELECTORS = Object.freeze({
|
|
|
60
60
|
'task-orchestration': ['status', 'progress'],
|
|
61
61
|
'task-review': ['preflight', 'finalize-summary'],
|
|
62
62
|
'task-event': ['event'],
|
|
63
|
-
'task-invalidation': ['reconcile'],
|
|
64
63
|
'task-lifecycle': ['intent'],
|
|
65
64
|
'task-finalization': ['complete'],
|
|
66
65
|
'task-platform-recovery': ['inspect', 'recover'],
|
|
@@ -98,7 +97,6 @@ export function internalRouteSelector(command: string, args: readonly string[]):
|
|
|
98
97
|
if (command === 'task-short-id') return first(args) === 'list' && args.includes('--verify') ? 'list-verify' : first(args);
|
|
99
98
|
if (command === 'task-orchestration') return args[1] === 'status' ? 'status' : args[1] ? 'progress' : '';
|
|
100
99
|
if (command === 'task-review') return args[1] ?? '';
|
|
101
|
-
if (command === 'task-invalidation') return args[1] ?? '';
|
|
102
100
|
if (command === 'task-artifact') return args[1] ?? '';
|
|
103
101
|
if (command === 'platform-pr-review' && first(args) === 'publish') {
|
|
104
102
|
const scope = optionValue(args, '--scope') ?? '';
|
|
@@ -9,6 +9,7 @@ import { serveSandboxControl } from '../sandbox/control/server.ts';
|
|
|
9
9
|
import { runSandboxControlExecutor } from '../sandbox/control/executor.ts';
|
|
10
10
|
import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.ts';
|
|
11
11
|
import { parseTaskCreateResult, taskCreateExitCode } from '../task/create-service.ts';
|
|
12
|
+
import { serializeTaskFinalizationEnvelope, type TaskFinalizationEnvelope } from '../task/finalization-envelope.ts';
|
|
12
13
|
|
|
13
14
|
type FinalizationStatus = 'completed' | 'failed' | 'blocked' | 'unknown';
|
|
14
15
|
|
|
@@ -16,10 +17,11 @@ function writeFinalizationEnvelope(
|
|
|
16
17
|
status: FinalizationStatus,
|
|
17
18
|
accepted: boolean,
|
|
18
19
|
error: { code: string; message: string; retryable: boolean },
|
|
19
|
-
changed = false,
|
|
20
|
-
result: unknown = null
|
|
20
|
+
changed: boolean | null = false,
|
|
21
|
+
result: unknown = null,
|
|
22
|
+
requestId: string | null = null
|
|
21
23
|
): void {
|
|
22
|
-
process.stdout.write(
|
|
24
|
+
process.stdout.write(serializeTaskFinalizationEnvelope({ status, changed, accepted, requestId, result, error }));
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
function finalizationErrorStatus(error: { retryable: boolean; code: string }): FinalizationStatus {
|
|
@@ -43,6 +45,28 @@ function recoveredExitCode(response: Awaited<ReturnType<typeof recoverSandboxCon
|
|
|
43
45
|
return response.exitCode ?? 1;
|
|
44
46
|
}
|
|
45
47
|
|
|
48
|
+
function finalizationResponseOutput(response: Awaited<ReturnType<typeof requestSandboxTaskFinalization>>): string {
|
|
49
|
+
try {
|
|
50
|
+
const value = JSON.parse(response.stdout) as Partial<TaskFinalizationEnvelope>;
|
|
51
|
+
if (value.version === 2
|
|
52
|
+
&& value.status
|
|
53
|
+
&& ['completed', 'failed', 'blocked', 'unknown'].includes(value.status)
|
|
54
|
+
&& (typeof value.changed === 'boolean' || value.changed === null)) {
|
|
55
|
+
return serializeTaskFinalizationEnvelope({
|
|
56
|
+
status: value.status,
|
|
57
|
+
changed: value.changed,
|
|
58
|
+
accepted: true,
|
|
59
|
+
requestId: response.id,
|
|
60
|
+
result: value.result ?? null,
|
|
61
|
+
error: value.error ?? null
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
} catch {
|
|
65
|
+
// Preserve non-envelope output for diagnostics from the executor.
|
|
66
|
+
}
|
|
67
|
+
return response.stdout;
|
|
68
|
+
}
|
|
69
|
+
|
|
46
70
|
function sandboxFinalizationClient(args: string[]): void {
|
|
47
71
|
if (args.length !== 4 || !args[0] || args[1] !== 'complete' || args[2] !== '--agent' || !args[3]) {
|
|
48
72
|
const error = { code: 'TASK_FINALIZATION_PAYLOAD_INVALID', message: 'task ref, complete intent, and --agent are required', retryable: false };
|
|
@@ -63,7 +87,8 @@ function sandboxFinalizationClient(args: string[]): void {
|
|
|
63
87
|
response = requestSandboxTaskFinalization({ agent });
|
|
64
88
|
} catch (error) {
|
|
65
89
|
if (!(error instanceof SandboxControlClientError)) throw error;
|
|
66
|
-
|
|
90
|
+
const status = finalizationErrorStatus(error.detail);
|
|
91
|
+
writeFinalizationEnvelope(status, error.accepted, error.detail, status === 'unknown' ? null : false, null, error.requestId);
|
|
67
92
|
writeClientError(error);
|
|
68
93
|
process.exitCode = error.detail.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN' ? 1 : error.detail.retryable ? 2 : 1;
|
|
69
94
|
return;
|
|
@@ -74,12 +99,20 @@ function sandboxFinalizationClient(args: string[]): void {
|
|
|
74
99
|
message: 'sandbox control rejected the finalization request',
|
|
75
100
|
retryable: false
|
|
76
101
|
};
|
|
77
|
-
|
|
102
|
+
const status = finalizationErrorStatus(error);
|
|
103
|
+
writeFinalizationEnvelope(
|
|
104
|
+
status,
|
|
105
|
+
error.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN',
|
|
106
|
+
error,
|
|
107
|
+
status === 'unknown' ? null : false,
|
|
108
|
+
null,
|
|
109
|
+
response.id
|
|
110
|
+
);
|
|
78
111
|
process.stderr.write(`${error.message}\n`);
|
|
79
112
|
process.exitCode = error.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN' ? 1 : error.retryable ? 2 : 1;
|
|
80
113
|
return;
|
|
81
114
|
}
|
|
82
|
-
process.stdout.write(response
|
|
115
|
+
process.stdout.write(finalizationResponseOutput(response));
|
|
83
116
|
process.stderr.write(response.stderr);
|
|
84
117
|
process.exitCode = response.exitCode ?? 1;
|
|
85
118
|
}
|