@fitlab-ai/agent-infra 0.11.4 → 0.11.5
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 -5
- package/dist/lib/decide.js +4 -41
- package/dist/lib/git/branch-worktree.js +45 -0
- package/dist/lib/internal/cli-route-inventory.js +0 -3
- package/dist/lib/internal/task-activity.js +7 -40
- package/dist/lib/internal/task-event.js +4 -37
- package/dist/lib/internal/task-finalization.js +9 -2
- package/dist/lib/internal/task-ledger.js +5 -43
- package/dist/lib/internal/task-lifecycle.js +1 -1
- package/dist/lib/internal/task-operation-registry.js +1 -4
- package/dist/lib/internal/task-review.js +5 -37
- package/dist/lib/internal/task-verify.js +3 -47
- package/dist/lib/internal/task-warning.js +4 -40
- package/dist/lib/platform/pr-summary.js +4 -23
- package/dist/lib/platform/verification-sync.js +12 -49
- package/dist/lib/sandbox/control/client.js +4 -1
- package/dist/lib/sandbox/control/executor.js +12 -4
- package/dist/lib/sandbox/control/protocol.js +2 -1
- package/dist/lib/sandbox/removal.js +15 -8
- package/dist/lib/task/activity-intent.js +1 -2
- package/dist/lib/task/artifact-operations.js +36 -2
- package/dist/lib/task/artifact-schema.js +7 -7
- package/dist/lib/task/control-authority.js +9 -166
- package/dist/lib/task/decision-intents.js +2 -4
- package/dist/lib/task/events.js +7 -28
- package/dist/lib/task/finalization-handoff.js +92 -0
- package/dist/lib/task/finalization.js +123 -12
- package/dist/lib/task/intermediate-cleanup.js +9 -148
- package/dist/lib/task/ledger-intents.js +3 -6
- package/dist/lib/task/lifecycle.js +50 -11
- package/dist/lib/task/local-artifact-finalization.js +13 -28
- package/dist/lib/task/review-command.js +1 -2
- package/dist/lib/task/review-finalization.js +1 -2
- package/dist/lib/task/snapshot.js +6 -6
- package/dist/lib/task/verification-engine.js +16 -11
- package/dist/lib/task/verification.js +1 -13
- package/dist/lib/task/workflow-warning-intents.js +2 -4
- package/dist/lib/task/write.js +1 -2
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/decide.ts +5 -35
- package/lib/git/branch-worktree.ts +41 -0
- package/lib/internal/cli-route-inventory.ts +0 -2
- package/lib/internal/task-activity.ts +6 -34
- package/lib/internal/task-event.ts +4 -37
- package/lib/internal/task-finalization.ts +9 -2
- package/lib/internal/task-ledger.ts +5 -38
- package/lib/internal/task-lifecycle.ts +2 -2
- package/lib/internal/task-operation-registry.ts +1 -4
- package/lib/internal/task-review.ts +5 -31
- package/lib/internal/task-verify.ts +3 -39
- package/lib/internal/task-warning.ts +3 -32
- package/lib/platform/pr-summary.ts +4 -25
- package/lib/platform/verification-sync.ts +13 -53
- package/lib/sandbox/control/client.ts +5 -1
- package/lib/sandbox/control/executor.ts +13 -4
- package/lib/sandbox/control/protocol.ts +3 -2
- package/lib/sandbox/removal.ts +23 -7
- package/lib/task/activity-intent.ts +1 -2
- package/lib/task/artifact-operations.ts +58 -2
- package/lib/task/artifact-schema.ts +9 -7
- package/lib/task/control-authority.ts +14 -188
- package/lib/task/decision-intents.ts +2 -4
- package/lib/task/events.ts +8 -27
- package/lib/task/finalization-handoff.ts +104 -0
- package/lib/task/finalization.ts +120 -12
- package/lib/task/intermediate-cleanup.ts +9 -125
- package/lib/task/ledger-intents.ts +3 -6
- package/lib/task/lifecycle.ts +45 -13
- package/lib/task/local-artifact-finalization.ts +16 -35
- package/lib/task/review-command.ts +2 -5
- package/lib/task/review-finalization.ts +1 -4
- package/lib/task/snapshot.ts +6 -6
- package/lib/task/verification-engine.ts +20 -10
- package/lib/task/verification.ts +1 -15
- package/lib/task/workflow-warning-intents.ts +2 -4
- package/lib/task/write.ts +1 -4
- package/package.json +1 -1
- package/templates/.agents/skills/analyze-task/SKILL.en.md +11 -2
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +11 -2
- package/templates/.agents/skills/code-task/reference/report-template.en.md +6 -6
- package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +6 -6
- package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +2 -2
- package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +2 -2
- package/templates/.agents/skills/complete-manual-validation/reference/report-template.en.md +1 -3
- package/templates/.agents/skills/complete-manual-validation/reference/report-template.zh-CN.md +1 -3
- package/templates/.agents/skills/complete-task/config/verify.en.json +2 -2
- package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +2 -2
- package/templates/.agents/skills/review-analysis/reference/report-template.en.md +3 -3
- package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +3 -3
- package/templates/.agents/skills/review-code/reference/report-template.en.md +3 -3
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +3 -3
- package/templates/.agents/skills/review-plan/reference/report-template.en.md +3 -3
- package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +3 -3
- package/templates/.agents/skills/review-pr/config/verify.en.json +3 -1
- package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +3 -1
- package/templates/.agents/skills/review-pr/reference/report-template.en.md +3 -3
- package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +3 -3
- package/templates/.agents/skills/run-manual-validation/config/verify.en.json +2 -2
- package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +2 -2
- package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +2 -2
- package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +2 -2
- package/dist/lib/internal/task-override.js +0 -138
- package/dist/lib/task/artifact-recovery.js +0 -476
- package/dist/lib/task/artifact-repair-intent.js +0 -181
- package/dist/lib/task/guard-override.js +0 -31
- package/dist/lib/task/human-override.js +0 -1259
- package/lib/internal/task-override.ts +0 -144
- package/lib/task/artifact-recovery.ts +0 -572
- package/lib/task/artifact-repair-intent.ts +0 -227
- package/lib/task/guard-override.ts +0 -50
- package/lib/task/human-override.ts +0 -1522
|
@@ -13,6 +13,7 @@ type ArtifactSchema = Readonly<{
|
|
|
13
13
|
family: ArtifactSchemaFamily;
|
|
14
14
|
title: Readonly<{ zh: string; en: string }>;
|
|
15
15
|
sections: readonly ArtifactSection[];
|
|
16
|
+
stateCheckPatterns: readonly string[];
|
|
16
17
|
requiredPatterns: readonly string[];
|
|
17
18
|
}>;
|
|
18
19
|
type ArtifactSkeletonInput = Readonly<{
|
|
@@ -106,21 +107,22 @@ function buildSections(family: ArtifactSchemaFamily): readonly ArtifactSection[]
|
|
|
106
107
|
}));
|
|
107
108
|
}
|
|
108
109
|
|
|
110
|
+
const TASK_SNAPSHOT_PATTERN = 'agent-infra-internal[ \\t]+task-snapshot\\b';
|
|
111
|
+
|
|
109
112
|
const ARTIFACT_SCHEMAS: readonly ArtifactSchema[] = [
|
|
110
|
-
{ family: 'analysis', title: { zh: '需求分析报告', en: 'Requirements Analysis' }, sections: buildSections('analysis'), requiredPatterns: [
|
|
111
|
-
{ family: 'review-analysis', title: { zh: '需求分析审查报告', en: 'Analysis Review Report' }, sections: buildSections('review-analysis'), requiredPatterns: ['^### (?:审查决定|Approval Decision)$'
|
|
112
|
-
{ family: 'plan', title: { zh: '技术方案', en: 'Technical Plan' }, sections: buildSections('plan'), requiredPatterns: [
|
|
113
|
-
{ family: 'review-plan', title: { zh: '技术方案审查报告', en: 'Plan Review Report' }, sections: buildSections('review-plan'), requiredPatterns: ['^### (?:审查决定|Approval Decision)$'
|
|
114
|
-
{ family: 'code', title: { zh: '实现报告', en: 'Implementation Report' }, sections: buildSections('code'), requiredPatterns: [
|
|
113
|
+
{ family: 'analysis', title: { zh: '需求分析报告', en: 'Requirements Analysis' }, sections: buildSections('analysis'), stateCheckPatterns: [TASK_SNAPSHOT_PATTERN], requiredPatterns: [] },
|
|
114
|
+
{ family: 'review-analysis', title: { zh: '需求分析审查报告', en: 'Analysis Review Report' }, sections: buildSections('review-analysis'), stateCheckPatterns: [TASK_SNAPSHOT_PATTERN], requiredPatterns: ['^### (?:审查决定|Approval Decision)$'] },
|
|
115
|
+
{ family: 'plan', title: { zh: '技术方案', en: 'Technical Plan' }, sections: buildSections('plan'), stateCheckPatterns: [TASK_SNAPSHOT_PATTERN], requiredPatterns: [] },
|
|
116
|
+
{ family: 'review-plan', title: { zh: '技术方案审查报告', en: 'Plan Review Report' }, sections: buildSections('review-plan'), stateCheckPatterns: [TASK_SNAPSHOT_PATTERN], requiredPatterns: ['^### (?:审查决定|Approval Decision)$'] },
|
|
117
|
+
{ family: 'code', title: { zh: '实现报告', en: 'Implementation Report' }, sections: buildSections('code'), stateCheckPatterns: [TASK_SNAPSHOT_PATTERN], requiredPatterns: [] },
|
|
115
118
|
{ family: 'review-code', title: { zh: '代码审查报告', en: 'Code Review Report' }, sections: buildSections('review-code'), requiredPatterns: [
|
|
116
119
|
'^### (?:审查决定|Approval Decision)$',
|
|
117
|
-
'^\\$ ',
|
|
118
120
|
'^- \\*\\*(?:总体结论|Overall Verdict)\\*\\*[::]\\s*(?:通过|需要修改|拒绝|Approved|Changes Requested|Rejected)\\s*$',
|
|
119
121
|
'^- \\*\\*(?:审查基线提交|Review Baseline Commit)\\*\\*[::]\\s*\\S',
|
|
120
122
|
'^- \\*\\*(?:审查差异基线|Reviewed Diff Base)\\*\\*[::]\\s*[0-9a-f]{40,64}\\s*$',
|
|
121
123
|
'^- \\*\\*(?:审查差异指纹|Reviewed Diff Fingerprint)\\*\\*[::]\\s*sha256:[0-9a-f]{64}\\s*$',
|
|
122
124
|
'^- \\*\\*(?:审查快照树|Reviewed Snapshot Tree)\\*\\*[::]\\s*[0-9a-f]{40,64}\\s*$'
|
|
123
|
-
] }
|
|
125
|
+
], stateCheckPatterns: [TASK_SNAPSHOT_PATTERN] }
|
|
124
126
|
];
|
|
125
127
|
|
|
126
128
|
function getArtifactSchema(family: string): ArtifactSchema | null {
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import crypto from 'node:crypto';
|
|
3
|
-
import fs from 'node:fs';
|
|
4
3
|
|
|
5
4
|
import { normalizeAgentToken } from '../agent-clients/tokens.ts';
|
|
6
5
|
import { isAgentClientId } from '../agent-clients/types.ts';
|
|
7
6
|
import type { AgentClientId } from '../agent-clients/types.ts';
|
|
8
7
|
import { getAgentClientAdapter } from '../agent-clients/registry.ts';
|
|
9
|
-
import { consumeHumanOverride, failureId, overrideDryRunConflict } from './human-override.ts';
|
|
10
8
|
import {
|
|
11
9
|
activateMatchingOrchestrationDelegation,
|
|
12
10
|
activateOrchestrationDelegation,
|
|
@@ -40,19 +38,12 @@ import type {
|
|
|
40
38
|
AgentClientLifecycleRecoveryRequest as LifecycleRecoveryRequest,
|
|
41
39
|
AgentClientLifecycleRecoveryResult as LifecycleRecoveryResult
|
|
42
40
|
} from '../agent-clients/adapter.ts';
|
|
43
|
-
import { locateActivityLog } from './activity-log.ts';
|
|
44
41
|
import { resolveTaskRef, TASK_ID_RE } from './resolve-ref.ts';
|
|
45
42
|
import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.ts';
|
|
46
43
|
import { verifyTaskEvent } from './verification.ts';
|
|
47
44
|
import { createCodexCapabilityStore } from '../agent-clients/adapters/codex-lifecycle/capability-store.ts';
|
|
48
|
-
import {
|
|
49
|
-
findArtifactRecoveryIntentsByOperation,
|
|
50
|
-
writeArtifactRecoveryIntent,
|
|
51
|
-
type ArtifactRecoveryIntent
|
|
52
|
-
} from './artifact-repair-intent.ts';
|
|
53
45
|
import {
|
|
54
46
|
computeLifecycleBuildIdentity,
|
|
55
|
-
verifyLifecycleBuildIdentity,
|
|
56
47
|
type LifecycleBuildIdentity
|
|
57
48
|
} from '../agent-clients/adapters/codex-lifecycle/build-identity.ts';
|
|
58
49
|
|
|
@@ -324,7 +315,6 @@ export function consumeLifecycleRecoveryAttestation(
|
|
|
324
315
|
export function queryLifecycleRecoveryOperation(
|
|
325
316
|
operationId: string,
|
|
326
317
|
options: Readonly<{
|
|
327
|
-
repoRoot?: string;
|
|
328
318
|
capabilityStore?: ReturnType<typeof createCodexCapabilityStore>;
|
|
329
319
|
}> = {}
|
|
330
320
|
): LifecycleRecoveryOperationQueryV1 {
|
|
@@ -334,7 +324,6 @@ export function queryLifecycleRecoveryOperation(
|
|
|
334
324
|
const store = options.capabilityStore ?? createCodexCapabilityStore();
|
|
335
325
|
const registry = authorityOperationMap.get(store);
|
|
336
326
|
const known = [...(registry?.values() ?? [])].filter((value) => value.operationId === operationId);
|
|
337
|
-
const intents = options.repoRoot ? findArtifactRecoveryIntentsByOperation(options.repoRoot, operationId) : [];
|
|
338
327
|
const capabilities = store.findByRecoveryOperation(operationId);
|
|
339
328
|
const phaseMap = new Map<string, LifecycleRecoveryOperationQueryV1['phases'][number]>();
|
|
340
329
|
for (const capability of capabilities) {
|
|
@@ -358,21 +347,10 @@ export function queryLifecycleRecoveryOperation(
|
|
|
358
347
|
? 'consumed' : 'issued'
|
|
359
348
|
});
|
|
360
349
|
}
|
|
361
|
-
for (const intent of intents) {
|
|
362
|
-
if (!intent.phase) continue;
|
|
363
|
-
const key = `${intent.phase}\0${intent.requestId}`;
|
|
364
|
-
if (phaseMap.has(key)) continue;
|
|
365
|
-
phaseMap.set(key, {
|
|
366
|
-
phase: intent.phase,
|
|
367
|
-
requestId: intent.requestId,
|
|
368
|
-
attestationId: null,
|
|
369
|
-
state: intent.state === 'consumed' ? 'consumed' : 'observed'
|
|
370
|
-
});
|
|
371
|
-
}
|
|
372
350
|
const capabilityState = capabilities.some((record) => record.recoveryState === 'consumed')
|
|
373
351
|
? 'consumed'
|
|
374
352
|
: capabilities.some((record) => record.recoveryState === 'reserved') ? 'reserved' : 'unknown';
|
|
375
|
-
const committed = capabilityState === 'consumed'
|
|
353
|
+
const committed = capabilityState === 'consumed';
|
|
376
354
|
return {
|
|
377
355
|
version: 1,
|
|
378
356
|
operationId,
|
|
@@ -382,133 +360,6 @@ export function queryLifecycleRecoveryOperation(
|
|
|
382
360
|
};
|
|
383
361
|
}
|
|
384
362
|
|
|
385
|
-
type LifecycleRecoveryCompensationOptions = Readonly<{
|
|
386
|
-
repoRoot: string;
|
|
387
|
-
controllerBinding: TaskControlControllerBinding;
|
|
388
|
-
capabilityStore?: ReturnType<typeof createCodexCapabilityStore>;
|
|
389
|
-
buildIdentity?: LifecycleBuildIdentity;
|
|
390
|
-
now?: () => number;
|
|
391
|
-
}>;
|
|
392
|
-
|
|
393
|
-
function lifecycleRecoveryCompensationError(code: string, message: string): Error {
|
|
394
|
-
const error = new Error(`${code}: ${message}`);
|
|
395
|
-
error.name = code;
|
|
396
|
-
return error;
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
function lifecycleRecoveryEventCommitted(
|
|
400
|
-
selector: LifecycleAuthorityRequestV1,
|
|
401
|
-
repoRoot: string
|
|
402
|
-
): boolean {
|
|
403
|
-
const resolved = resolveTaskRef(selector.taskId, { repoRoot });
|
|
404
|
-
if (!resolved.ok || resolved.taskId !== selector.taskId) return false;
|
|
405
|
-
let content: string;
|
|
406
|
-
try { content = fs.readFileSync(resolved.taskMdPath, 'utf8'); }
|
|
407
|
-
catch { return false; }
|
|
408
|
-
const section = locateActivityLog(content);
|
|
409
|
-
if (!section) return false;
|
|
410
|
-
const labels = { analysis: 'Analyze Task', plan: 'Plan Task', code: 'Code Task' } as const;
|
|
411
|
-
const prefix = `${labels[selector.family]} (Round ${selector.round}`;
|
|
412
|
-
return section.entries.some((entry) => {
|
|
413
|
-
if (entry.step.endsWith(' [started]') || !entry.step.startsWith(prefix)) return false;
|
|
414
|
-
const delimiter = entry.step[prefix.length];
|
|
415
|
-
return (delimiter === ',' || delimiter === ')') && entry.note.endsWith(`→ ${selector.artifact}`);
|
|
416
|
-
});
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
function lifecycleRecoveryIntentMatches(
|
|
420
|
-
intent: ArtifactRecoveryIntent,
|
|
421
|
-
selector: LifecycleAuthorityRequestV1
|
|
422
|
-
): boolean {
|
|
423
|
-
return intent.taskId === selector.taskId
|
|
424
|
-
&& intent.family === selector.family
|
|
425
|
-
&& intent.artifact === selector.artifact
|
|
426
|
-
&& (intent.state === 'commit-started' || intent.state === 'consumption-started' || intent.state === 'consumed');
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
/**
|
|
430
|
-
* Completes only the durable tail of a committed task event after a process
|
|
431
|
-
* restart. The task event itself is the commit boundary and is never replayed.
|
|
432
|
-
*/
|
|
433
|
-
export function recoverLifecycleRecoveryOperation(
|
|
434
|
-
input: LifecycleAuthorityRequestV1,
|
|
435
|
-
options: LifecycleRecoveryCompensationOptions
|
|
436
|
-
): LifecycleRecoveryOperationQueryV1 {
|
|
437
|
-
const selector = validateLifecycleAuthorityRequest(input);
|
|
438
|
-
if (selector.phase !== 'task-event.completed') {
|
|
439
|
-
throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_PHASE_UNSUPPORTED', 'only task-event.completed supports durable compensation');
|
|
440
|
-
}
|
|
441
|
-
const buildIdentity = options.buildIdentity ?? computeLifecycleBuildIdentity(options.repoRoot);
|
|
442
|
-
if (selector.expectedBuildIdentityDigest !== lifecycleAuthorityBuildDigest(buildIdentity)) {
|
|
443
|
-
throw lifecycleRecoveryCompensationError('LIFECYCLE_AUTHORITY_BUILD_MISMATCH', 'current lifecycle build does not match the recovery request');
|
|
444
|
-
}
|
|
445
|
-
if (selector.expectedControlGeneration !== options.controllerBinding.controlGeneration
|
|
446
|
-
|| selector.expectedControllerInstanceDigest !== options.controllerBinding.instanceDigest) {
|
|
447
|
-
throw lifecycleRecoveryCompensationError('LIFECYCLE_AUTHORITY_CONTROLLER_MISMATCH', 'current controller binding does not match the recovery request');
|
|
448
|
-
}
|
|
449
|
-
const store = options.capabilityStore ?? createCodexCapabilityStore();
|
|
450
|
-
const expected = {
|
|
451
|
-
taskId: selector.taskId,
|
|
452
|
-
hookDefinitionHash: selector.expectedHookDefinitionHash,
|
|
453
|
-
buildIdentity,
|
|
454
|
-
controller: options.controllerBinding
|
|
455
|
-
};
|
|
456
|
-
const capabilities = store.findByRecoveryOperation(selector.operationId);
|
|
457
|
-
if (capabilities.length !== 1 || capabilities[0]!.capabilityRefDigest !== lifecycleAuthorityDigest(selector.authorityRef)) {
|
|
458
|
-
throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_PROVENANCE_INVALID', 'durable capability provenance does not identify this operation');
|
|
459
|
-
}
|
|
460
|
-
const capability = capabilities[0]!;
|
|
461
|
-
const build = verifyLifecycleBuildIdentity(capability.buildIdentity, buildIdentity);
|
|
462
|
-
const expiredReserved = capability.status === 'expired' && capability.recoveryState === 'reserved';
|
|
463
|
-
if (!build.ok
|
|
464
|
-
|| capability.taskId !== selector.taskId
|
|
465
|
-
|| capability.hookDefinitionHash !== selector.expectedHookDefinitionHash
|
|
466
|
-
|| capability.controller?.controlGeneration !== options.controllerBinding.controlGeneration
|
|
467
|
-
|| capability.controller?.instanceDigest !== options.controllerBinding.instanceDigest
|
|
468
|
-
|| capability.recoveryState === 'unreserved'
|
|
469
|
-
|| capability.recoveryOperationId !== selector.operationId
|
|
470
|
-
|| (!['attested', 'consumed'].includes(capability.status) && !expiredReserved)) {
|
|
471
|
-
throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_PROVENANCE_INVALID', 'durable capability provenance does not match the recovery request');
|
|
472
|
-
}
|
|
473
|
-
const phase = capability.recoveryPhases.find((entry) => entry.phase === selector.phase);
|
|
474
|
-
if (!phase) {
|
|
475
|
-
throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_PHASE_MISSING', 'durable task completion phase is missing');
|
|
476
|
-
}
|
|
477
|
-
const intents = findArtifactRecoveryIntentsByOperation(options.repoRoot, selector.operationId)
|
|
478
|
-
.filter((intent) => intent.taskId === selector.taskId
|
|
479
|
-
&& intent.family === selector.family
|
|
480
|
-
&& intent.artifact === selector.artifact);
|
|
481
|
-
if (intents.length !== 1) {
|
|
482
|
-
throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_INTENT_INVALID', 'durable artifact recovery intent does not identify this task event');
|
|
483
|
-
}
|
|
484
|
-
const intent = intents[0]!;
|
|
485
|
-
if (!lifecycleRecoveryIntentMatches(intent, selector)
|
|
486
|
-
|| intent.phase !== selector.phase
|
|
487
|
-
|| intent.requestId !== selector.lifecycleRequestId
|
|
488
|
-
|| intent.authorityDigest === null) {
|
|
489
|
-
throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_INTENT_INVALID', 'durable artifact recovery intent does not match the recovery request');
|
|
490
|
-
}
|
|
491
|
-
if (!lifecycleRecoveryEventCommitted(selector, options.repoRoot)) {
|
|
492
|
-
throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_COMMIT_UNCONFIRMED', 'task completion is not durably present; compensation is not allowed');
|
|
493
|
-
}
|
|
494
|
-
if (capability.recoveryState === 'reserved') {
|
|
495
|
-
if (phase.state === 'issued') {
|
|
496
|
-
store.consumeRecoveryPhase(selector.authorityRef, selector.operationId, selector.phase, phase.requestId, expected, { allowExpiredReserved: true });
|
|
497
|
-
}
|
|
498
|
-
store.consumeReference(selector.authorityRef, selector.operationId, expected, { allowExpiredReserved: true });
|
|
499
|
-
} else if (phase.state !== 'consumed') {
|
|
500
|
-
throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_STATE_INVALID', 'consumed capability has an unconsumed completion phase');
|
|
501
|
-
}
|
|
502
|
-
if (intent.state !== 'consumed') {
|
|
503
|
-
writeArtifactRecoveryIntent(options.repoRoot, {
|
|
504
|
-
...intent,
|
|
505
|
-
state: 'consumed',
|
|
506
|
-
updatedAt: (options.now ?? Date.now)()
|
|
507
|
-
}, { expected: intent });
|
|
508
|
-
}
|
|
509
|
-
return queryLifecycleRecoveryOperation(selector.operationId, { repoRoot: options.repoRoot, capabilityStore: store });
|
|
510
|
-
}
|
|
511
|
-
|
|
512
363
|
function authorityRejected(request: LifecycleAuthorityRequestV1, code: string, message: string): LifecycleAuthorityResponseV1 {
|
|
513
364
|
return { version: 1, requestId: request.requestId, operationId: request.operationId, phase: request.phase, status: 'rejected', attestation: null, error: { code, message } };
|
|
514
365
|
}
|
|
@@ -537,6 +388,7 @@ export type TaskControlExecutionContext =
|
|
|
537
388
|
generation: string;
|
|
538
389
|
manifestPath: string;
|
|
539
390
|
requestId: string;
|
|
391
|
+
handoffDirectory: string;
|
|
540
392
|
controllerBinding?: TaskControlControllerBinding | null;
|
|
541
393
|
lifecycleRecoveryAttestation?: LifecycleRecoveryAttestationV1 | null;
|
|
542
394
|
}>;
|
|
@@ -575,11 +427,7 @@ export function taskControlOperationKey(operation: TaskControlOperation): string
|
|
|
575
427
|
: operation.intent;
|
|
576
428
|
}
|
|
577
429
|
|
|
578
|
-
export type TaskLifecycleControlRequest = TaskLifecycleRequest
|
|
579
|
-
overrideTicket?: string;
|
|
580
|
-
overrideTarget?: string;
|
|
581
|
-
overrideScope?: string;
|
|
582
|
-
}>;
|
|
430
|
+
export type TaskLifecycleControlRequest = TaskLifecycleRequest;
|
|
583
431
|
|
|
584
432
|
function invalidContext(message: string): never {
|
|
585
433
|
throw new Error(`TASK_CONTROL_CONTEXT_INVALID: ${message}`);
|
|
@@ -625,6 +473,7 @@ export function createSandboxExecutorExecutionContext(params: Readonly<{
|
|
|
625
473
|
generation: string;
|
|
626
474
|
manifestPath: string;
|
|
627
475
|
requestId: string;
|
|
476
|
+
handoffDirectory?: string;
|
|
628
477
|
controllerBinding?: TaskControlControllerBinding | null;
|
|
629
478
|
lifecycleRecoveryAttestation?: LifecycleRecoveryAttestationV1 | null;
|
|
630
479
|
}>): TaskControlExecutionContext {
|
|
@@ -645,6 +494,7 @@ export function createSandboxExecutorExecutionContext(params: Readonly<{
|
|
|
645
494
|
generation: params.generation,
|
|
646
495
|
manifestPath: absolute('manifestPath', params.manifestPath),
|
|
647
496
|
requestId: params.requestId,
|
|
497
|
+
handoffDirectory: absolute('handoffDirectory', params.handoffDirectory ?? path.join(params.worktreeRoot, '.agent-infra-handoff')),
|
|
648
498
|
...(params.controllerBinding === undefined ? {} : { controllerBinding: binding(params.controllerBinding) }),
|
|
649
499
|
...(params.lifecycleRecoveryAttestation === undefined ? {} : {
|
|
650
500
|
lifecycleRecoveryAttestation: params.lifecycleRecoveryAttestation === null
|
|
@@ -670,6 +520,7 @@ export function assertTaskControlExecutionContext(context: TaskControlExecutionC
|
|
|
670
520
|
absolute('worktreeRoot', context.worktreeRoot);
|
|
671
521
|
absolute('runtimeDir', context.runtimeDir);
|
|
672
522
|
absolute('manifestPath', context.manifestPath);
|
|
523
|
+
absolute('handoffDirectory', context.handoffDirectory);
|
|
673
524
|
if (path.resolve(context.runtimeDir) !== path.join(path.dirname(path.resolve(context.manifestPath)), 'runtime')) {
|
|
674
525
|
invalidContext('runtimeDir is not bound to the manifest control root');
|
|
675
526
|
}
|
|
@@ -706,12 +557,8 @@ function lifecycleTaskId(request: TaskLifecycleControlRequest, repoRoot: string)
|
|
|
706
557
|
async function applyLifecycleWithAuthority(
|
|
707
558
|
context: TaskControlExecutionContext,
|
|
708
559
|
request: TaskLifecycleControlRequest
|
|
709
|
-
): Promise<
|
|
710
|
-
const
|
|
711
|
-
if (conflict) {
|
|
712
|
-
return taskLifecycleFailure(request, { code: 'LIFECYCLE_PAYLOAD_INVALID', message: conflict.message });
|
|
713
|
-
}
|
|
714
|
-
const execute = async (): Promise<(TaskLifecycleResult | LifecycleRecoveryResult) & { humanOverride?: unknown }> => {
|
|
560
|
+
): Promise<TaskLifecycleResult | LifecycleRecoveryResult> {
|
|
561
|
+
const execute = async (): Promise<TaskLifecycleResult | LifecycleRecoveryResult> => {
|
|
715
562
|
if (request.intent === 'recover-started') {
|
|
716
563
|
const adapter = isAgentClientId(request.agent)
|
|
717
564
|
? getAgentClientAdapter(request.agent).orchestrationAdapter
|
|
@@ -725,27 +572,7 @@ async function applyLifecycleWithAuthority(
|
|
|
725
572
|
}
|
|
726
573
|
return adapter.recoverStarted(request as LifecycleRecoveryRequest, { repoRoot: context.repoRoot });
|
|
727
574
|
}
|
|
728
|
-
|
|
729
|
-
if (lifecycleResult.status !== 'failed' || !request.overrideTicket) return lifecycleResult;
|
|
730
|
-
if (!request.overrideTarget || !request.overrideScope) {
|
|
731
|
-
return {
|
|
732
|
-
...lifecycleResult,
|
|
733
|
-
error: { code: 'OVERRIDE_PAYLOAD_INVALID', message: 'override ticket requires --override-target and --override-scope' }
|
|
734
|
-
};
|
|
735
|
-
}
|
|
736
|
-
const override = await consumeHumanOverride({
|
|
737
|
-
taskRef: request.taskRef,
|
|
738
|
-
ticketId: request.overrideTicket,
|
|
739
|
-
failureId: failureId('lifecycle.apply', lifecycleResult.error?.code ?? 'LIFECYCLE_FAILED'),
|
|
740
|
-
target: request.overrideTarget,
|
|
741
|
-
scope: request.overrideScope,
|
|
742
|
-
intent: request.intent,
|
|
743
|
-
...('alertNumber' in request && request.alertNumber ? { alertNumber: request.alertNumber } : {}),
|
|
744
|
-
...('issueNumber' in request && request.issueNumber ? { issueNumber: request.issueNumber } : {}),
|
|
745
|
-
...('stagingDir' in request && request.stagingDir ? { stagingDir: request.stagingDir } : {})
|
|
746
|
-
}, { repoRoot: context.repoRoot });
|
|
747
|
-
if (override.status === 'failed') return { ...lifecycleResult, humanOverride: override };
|
|
748
|
-
return { ...lifecycleResult, ...override, humanOverride: override, error: null };
|
|
575
|
+
return applyTaskLifecycle(request, { repoRoot: context.repoRoot });
|
|
749
576
|
};
|
|
750
577
|
const taskId = lifecycleTaskId(request, context.repoRoot);
|
|
751
578
|
if (!taskId) return await execute();
|
|
@@ -840,7 +667,7 @@ function orchestrationFailure(error: unknown): OrchestrationResult {
|
|
|
840
667
|
export function dispatchTaskControlOperation(
|
|
841
668
|
context: TaskControlExecutionContext,
|
|
842
669
|
operation: Extract<TaskControlOperation, { family: 'task-lifecycle' }>
|
|
843
|
-
): Promise<
|
|
670
|
+
): Promise<TaskLifecycleResult | LifecycleRecoveryResult>;
|
|
844
671
|
export function dispatchTaskControlOperation(
|
|
845
672
|
context: TaskControlExecutionContext,
|
|
846
673
|
operation: Extract<TaskControlOperation, { family: 'task-finalization' }>
|
|
@@ -865,7 +692,9 @@ export function dispatchTaskControlOperation(
|
|
|
865
692
|
return finalize(operation.request, {
|
|
866
693
|
repoRoot: context.repoRoot,
|
|
867
694
|
...(context.source === 'sandbox-executor' ? {
|
|
868
|
-
controlBinding: { generation: context.generation, requestId: context.requestId }
|
|
695
|
+
controlBinding: { generation: context.generation, requestId: context.requestId },
|
|
696
|
+
handoffDirectory: context.handoffDirectory,
|
|
697
|
+
manifestPath: context.manifestPath
|
|
869
698
|
} : {}),
|
|
870
699
|
preflight: (request, options) => verifyTaskEvent(
|
|
871
700
|
{ ...request, event: 'complete-task.hard-preflight' }, options
|
|
@@ -904,7 +733,7 @@ function operationInvalid(message: string): never {
|
|
|
904
733
|
|
|
905
734
|
const LIFECYCLE_FLAGS = new Set([
|
|
906
735
|
'--agent', '--reason', '--unblock-condition', '--note', '--alert-number', '--staging-dir', '--issue-number',
|
|
907
|
-
'--auto', '--
|
|
736
|
+
'--auto', '--dry-run'
|
|
908
737
|
]);
|
|
909
738
|
|
|
910
739
|
const FINALIZATION_FLAGS = new Set(['--agent']);
|
|
@@ -979,9 +808,6 @@ export function parseTaskControlOperation(
|
|
|
979
808
|
...(value(values, '--unblock-condition') ? { unblockCondition: value(values, '--unblock-condition') } : {}),
|
|
980
809
|
...(value(values, '--note') ? { note: value(values, '--note') } : {}),
|
|
981
810
|
...(value(values, '--staging-dir') ? { stagingDir: value(values, '--staging-dir') } : {}),
|
|
982
|
-
...(value(values, '--override-ticket') ? { overrideTicket: value(values, '--override-ticket') } : {}),
|
|
983
|
-
...(value(values, '--override-target') ? { overrideTarget: value(values, '--override-target') } : {}),
|
|
984
|
-
...(value(values, '--override-scope') ? { overrideScope: value(values, '--override-scope') } : {}),
|
|
985
811
|
...(value(values, '--alert-number') ? { alertNumber: Number(value(values, '--alert-number')) } : {}),
|
|
986
812
|
...(value(values, '--issue-number') ? { issueNumber: Number(value(values, '--issue-number')) } : {}),
|
|
987
813
|
...(values['--auto'] === true ? { auto: true } : {}),
|
|
@@ -16,7 +16,6 @@ import { resolveTaskContext } from './resolve-ref.ts';
|
|
|
16
16
|
import { extractSection, extractSubSection, findSectionHeading } from './sections.ts';
|
|
17
17
|
import { captureTaskWriteMetadata, writeTask } from './write.ts';
|
|
18
18
|
import type { TaskMutation, TaskOperationSummary, TaskWriteOptions } from './write.ts';
|
|
19
|
-
import { allowsManualOverride } from './guard-override.ts';
|
|
20
19
|
import { consumeHumanDecisionReworkIntents, parseReworkIntentDocument, reworkIntentMutation } from './rework-intent.ts';
|
|
21
20
|
|
|
22
21
|
type HumanDecisionRequest = {
|
|
@@ -87,8 +86,7 @@ function applyHumanDecision(request: HumanDecisionRequest, options: TaskWriteOpt
|
|
|
87
86
|
if (!request.selector || !request.decision.trim()) return failed('DECISION_PAYLOAD_INVALID', 'selector and decision are required');
|
|
88
87
|
const resolved = resolveTaskContext(request.taskRef, { repoRoot: options.repoRoot });
|
|
89
88
|
if (!resolved.ok) return failed(resolved.code, resolved.message, resolved.taskId);
|
|
90
|
-
|
|
91
|
-
if (resolved.state !== 'active' && !stateOverride) return failed('TASK_STATE_MISMATCH', `task ${resolved.taskId} is ${resolved.state}, expected active`, resolved.taskId);
|
|
89
|
+
if (resolved.state !== 'active') return failed('TASK_STATE_MISMATCH', `task ${resolved.taskId} is ${resolved.state}, expected active`, resolved.taskId);
|
|
92
90
|
let content: string;
|
|
93
91
|
let rows: LedgerRow[];
|
|
94
92
|
try {
|
|
@@ -186,7 +184,7 @@ function applyHumanDecision(request: HumanDecisionRequest, options: TaskWriteOpt
|
|
|
186
184
|
} catch (error) {
|
|
187
185
|
return failed('DECISION_DOCUMENT_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId, row.id);
|
|
188
186
|
}
|
|
189
|
-
const writeResult = writeTask({ taskRef: resolved.taskId, expectedState:
|
|
187
|
+
const writeResult = writeTask({ taskRef: resolved.taskId, expectedState: 'active', mutations, dryRun: request.dryRun }, {
|
|
190
188
|
...options,
|
|
191
189
|
taskLocation: { repoRoot: resolved.repoRoot, taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, state: resolved.state },
|
|
192
190
|
metadataProvider: () => metadata
|
package/lib/task/events.ts
CHANGED
|
@@ -2,6 +2,8 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { execFileSync } from 'node:child_process';
|
|
4
4
|
|
|
5
|
+
import { resolveBranchWorktree } from '../git/branch-worktree.ts';
|
|
6
|
+
|
|
5
7
|
import { appendActivityEntry, locateActivityLog, pairEntries, startedBackedRows } from './activity-log.ts';
|
|
6
8
|
import {
|
|
7
9
|
buildArtifactLinkSection,
|
|
@@ -32,7 +34,6 @@ import type { OrchestrationStageCompletion } from './orchestration.ts';
|
|
|
32
34
|
import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.ts';
|
|
33
35
|
import { captureTaskWriteMetadata, writeTask } from './write.ts';
|
|
34
36
|
import type { TaskOperationSummary, TaskWriteErrorCode, TaskWriteOptions } from './write.ts';
|
|
35
|
-
import { allowsManualOverride } from './guard-override.ts';
|
|
36
37
|
import { validateLocalArtifact } from './local-artifact-finalization.ts';
|
|
37
38
|
import type { LocalArtifactFamily } from './local-artifact-finalization.ts';
|
|
38
39
|
import { buildLifecycleFacts, canStart, effectiveReworkTarget } from './capabilities.ts';
|
|
@@ -81,7 +82,6 @@ type TaskEventErrorCode =
|
|
|
81
82
|
| ArtifactErrorCode | TaskWriteErrorCode;
|
|
82
83
|
type TaskEventRequest = {
|
|
83
84
|
taskRef: string; event: TaskEventName | string; agent: string; dryRun?: boolean; orchestrated?: boolean;
|
|
84
|
-
overrideTicket?: string; overrideTarget?: string; overrideScope?: string;
|
|
85
85
|
initiator?: TriggerInitiator; requestId?: string; reasonCode?: TriggerReason;
|
|
86
86
|
sourceFinding?: string; sourceArtifact?: string; sourceSha256?: string;
|
|
87
87
|
round?: number; question?: number; artifact?: string; fixFor?: string; implementationInput?: string;
|
|
@@ -111,22 +111,7 @@ type TaskEventResult = {
|
|
|
111
111
|
|
|
112
112
|
function branchWorktree(repoRoot: string, branch: string): string | null {
|
|
113
113
|
if (!branch) return repoRoot;
|
|
114
|
-
|
|
115
|
-
const records = execFileSync('git', ['worktree', 'list', '--porcelain'], {
|
|
116
|
-
cwd: repoRoot,
|
|
117
|
-
encoding: 'utf8',
|
|
118
|
-
stdio: ['ignore', 'pipe', 'pipe']
|
|
119
|
-
}).trim().split(/\r?\n\r?\n/);
|
|
120
|
-
const expectedRef = `refs/heads/${branch}`;
|
|
121
|
-
for (const record of records) {
|
|
122
|
-
const worktree = /^worktree (.+)$/m.exec(record)?.[1];
|
|
123
|
-
const branchRef = /^branch (.+)$/m.exec(record)?.[1];
|
|
124
|
-
if (worktree && branchRef === expectedRef) return worktree;
|
|
125
|
-
}
|
|
126
|
-
} catch {
|
|
127
|
-
// A missing worktree must not anchor a review to an unrelated repository.
|
|
128
|
-
}
|
|
129
|
-
return null;
|
|
114
|
+
return resolveBranchWorktree(repoRoot, branch);
|
|
130
115
|
}
|
|
131
116
|
|
|
132
117
|
function approvedCleanReviewedCommit(reviewContent: string, verdict: Verdict | undefined, repoRoot: string, branch: string): string | null {
|
|
@@ -148,7 +133,7 @@ function approvedCleanReviewedCommit(reviewContent: string, verdict: Verdict | u
|
|
|
148
133
|
}
|
|
149
134
|
}
|
|
150
135
|
|
|
151
|
-
const BASE_FIELDS = new Set(['taskRef', 'event', 'agent', 'dryRun', '
|
|
136
|
+
const BASE_FIELDS = new Set(['taskRef', 'event', 'agent', 'dryRun', 'initiator', 'requestId', 'reasonCode', 'sourceFinding', 'sourceArtifact', 'sourceSha256']);
|
|
152
137
|
const SCHEMAS: Record<TaskEventName, { required?: string[]; optional?: string[] }> = {
|
|
153
138
|
'analyze.started': { optional: ['round'] },
|
|
154
139
|
'analyze.awaiting-input': { required: ['question'] },
|
|
@@ -758,8 +743,7 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
758
743
|
if (invalid) return failed(request, invalid);
|
|
759
744
|
const resolved = resolveTaskRef(request.taskRef, { repoRoot: options.repoRoot });
|
|
760
745
|
if (!resolved.ok) return failed(request, { code: resolved.code, message: resolved.message }, { taskId: resolved.taskId });
|
|
761
|
-
|
|
762
|
-
if (resolved.state !== 'active' && !stateOverride) return failed(request, { code: 'TASK_STATE_MISMATCH', message: `task ${resolved.taskId} is ${resolved.state}, expected active` }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
|
|
746
|
+
if (resolved.state !== 'active') return failed(request, { code: 'TASK_STATE_MISMATCH', message: `task ${resolved.taskId} is ${resolved.state}, expected active` }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
|
|
763
747
|
let content: string;
|
|
764
748
|
try { content = fs.readFileSync(resolved.taskMdPath, 'utf8'); }
|
|
765
749
|
catch (error) { return failed(request, { code: 'TASK_READ_FAILED', message: String(error) }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath }); }
|
|
@@ -851,7 +835,7 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
851
835
|
}
|
|
852
836
|
return successNoOp(normalized, resolved.taskId, resolved.taskMdPath, currentStep, eventIdentity, row.started, frontmatter, artifactContext);
|
|
853
837
|
}
|
|
854
|
-
if (eventIdentity.phase === 'completed' && !row?.started && completedRows.length === 0
|
|
838
|
+
if (eventIdentity.phase === 'completed' && !row?.started && completedRows.length === 0) return failed(normalized, { code: 'EVENT_START_MISSING', message: 'completion requires a started event or an earlier completed attempt' }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
855
839
|
if (eventIdentity.phase === 'completed') {
|
|
856
840
|
const validated = validateCompletedArtifact(resolved.taskDir, FAMILY[eventIdentity.family].artifact, normalized.artifact!, normalized.round);
|
|
857
841
|
if (!validated.ok) return failed(normalized, validated.error, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
@@ -918,12 +902,9 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
918
902
|
const facts = buildLifecycleFacts(resolved.taskDir, content, resolved.state);
|
|
919
903
|
if (!facts.ok) return failed(normalized, { code: 'EVENT_TRANSITION_INVALID', message: `${facts.code}: ${facts.message}` }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
920
904
|
const capability = canStart(lifecycleAction(eventIdentity.family), facts.facts, trigger);
|
|
921
|
-
const capabilityOverride = capability.reasonCode === 'TASK_NOT_ACTIVE'
|
|
922
|
-
? stateOverride
|
|
923
|
-
: allowsManualOverride(options.manualOverride, 'task-event', 'EVENT_TRANSITION_INVALID');
|
|
924
905
|
const safetyFailure = capability.reasonCode === 'TASK_NOT_ACTIVE'
|
|
925
906
|
|| capability.reasonCode === 'LIFECYCLE_EXECUTION_OPEN';
|
|
926
|
-
if (!capability.allowed && (normalized.initiator === 'orchestrator' || safetyFailure)
|
|
907
|
+
if (!capability.allowed && (normalized.initiator === 'orchestrator' || safetyFailure)) {
|
|
927
908
|
const code = capability.reasonCode === 'INVALIDATION_INCOMPLETE' ? 'TASK_INVALIDATION_BLOCKED' : 'EVENT_TRANSITION_INVALID';
|
|
928
909
|
return failed(normalized, { code, message: `${capability.reasonCode}: ${capability.evidence.join(', ')}` }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
929
910
|
}
|
|
@@ -1078,7 +1059,7 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
1078
1059
|
mutations.push({ kind: 'section', aliases: ['活动日志', 'Activity Log'], heading: section.heading, body });
|
|
1079
1060
|
const sourceCompletion = eventIdentity.phase === 'completed'
|
|
1080
1061
|
&& ['analyze', 'plan', 'code'].includes(eventIdentity.family);
|
|
1081
|
-
const result = writeTask({ taskRef: normalized.taskRef, expectedState:
|
|
1062
|
+
const result = writeTask({ taskRef: normalized.taskRef, expectedState: 'active', dryRun: normalized.dryRun, mutations }, { ...options, invalidationContext: sourceCompletion ? 'source-completion' : 'standard', metadataProvider: () => metadata });
|
|
1082
1063
|
if (result.status === 'failed') return failed(normalized, result.error, { taskId: result.taskId, taskMdPath: result.taskMdPath, fromStep: currentStep, toStep: step, action: eventIdentity.action, phase: eventIdentity.phase, timestamp: result.timestamp, agentInfraVersion: result.agentInfraVersion, operations: result.operations, artifactContext });
|
|
1083
1064
|
if (!normalized.dryRun && orchestrationCompletion) {
|
|
1084
1065
|
try {
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import type { TaskFinalizationReceipt } from './finalization.ts';
|
|
6
|
+
|
|
7
|
+
const HANDOFF_VERSION = 1 as const;
|
|
8
|
+
|
|
9
|
+
type TaskFinalizationHandoff = Readonly<{
|
|
10
|
+
version: typeof HANDOFF_VERSION;
|
|
11
|
+
taskId: string;
|
|
12
|
+
binding: Readonly<{ generation: string; requestId: string }>;
|
|
13
|
+
receipt: TaskFinalizationReceipt;
|
|
14
|
+
receiptSha256: string;
|
|
15
|
+
}>;
|
|
16
|
+
type ReadHandoff = Readonly<{ receipt: TaskFinalizationReceipt; cleanup: () => void }>;
|
|
17
|
+
const MAX_HANDOFF_BYTES = 1024 * 1024;
|
|
18
|
+
|
|
19
|
+
function digest(value: unknown): string {
|
|
20
|
+
return createHash('sha256').update(JSON.stringify(value)).digest('hex');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function finalizationHandoffPath(directory: string, taskId: string): string {
|
|
24
|
+
return path.join(directory, '.agent-infra', 'task-finalization', `${taskId}.json`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function publishTaskFinalizationHandoff(
|
|
28
|
+
directory: string,
|
|
29
|
+
receipt: TaskFinalizationReceipt,
|
|
30
|
+
binding: Readonly<{ generation: string; requestId: string }>
|
|
31
|
+
): string {
|
|
32
|
+
if (receipt.taskId === '' || receipt.controlBinding?.generation !== binding.generation
|
|
33
|
+
|| receipt.controlBinding.requestId !== binding.requestId) {
|
|
34
|
+
throw new Error('TASK_FINALIZATION_HANDOFF_BINDING_INVALID');
|
|
35
|
+
}
|
|
36
|
+
const envelope: TaskFinalizationHandoff = {
|
|
37
|
+
version: HANDOFF_VERSION, taskId: receipt.taskId, binding, receipt, receiptSha256: digest(receipt)
|
|
38
|
+
};
|
|
39
|
+
const target = finalizationHandoffPath(directory, receipt.taskId);
|
|
40
|
+
fs.mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 });
|
|
41
|
+
const temporary = `${target}.${process.pid}.${randomUUID()}.tmp`;
|
|
42
|
+
fs.writeFileSync(temporary, `${JSON.stringify(envelope)}\n`, { mode: 0o600, flag: 'wx' });
|
|
43
|
+
try { fs.renameSync(temporary, target); } catch (error) {
|
|
44
|
+
try { fs.unlinkSync(temporary); } catch { /* preserve the primary error */ }
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
return digest(envelope);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function readTaskFinalizationHandoff(
|
|
51
|
+
directory: string,
|
|
52
|
+
taskId: string,
|
|
53
|
+
binding: Readonly<{ generation: string; requestId: string }>,
|
|
54
|
+
handoffSha256: string
|
|
55
|
+
): ReadHandoff {
|
|
56
|
+
const file = finalizationHandoffPath(directory, taskId);
|
|
57
|
+
const initial = fs.lstatSync(file);
|
|
58
|
+
if (!initial.isFile() || initial.isSymbolicLink() || initial.size > MAX_HANDOFF_BYTES) {
|
|
59
|
+
throw new Error('TASK_FINALIZATION_HANDOFF_INVALID');
|
|
60
|
+
}
|
|
61
|
+
const descriptor = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
|
|
62
|
+
let content: string;
|
|
63
|
+
try {
|
|
64
|
+
const opened = fs.fstatSync(descriptor);
|
|
65
|
+
if (!opened.isFile() || opened.dev !== initial.dev || opened.ino !== initial.ino
|
|
66
|
+
|| opened.size > MAX_HANDOFF_BYTES) throw new Error('TASK_FINALIZATION_HANDOFF_INVALID');
|
|
67
|
+
const bytes = Buffer.alloc(MAX_HANDOFF_BYTES + 1);
|
|
68
|
+
const length = fs.readSync(descriptor, bytes, 0, bytes.length, 0);
|
|
69
|
+
if (length !== opened.size || length > MAX_HANDOFF_BYTES) throw new Error('TASK_FINALIZATION_HANDOFF_INVALID');
|
|
70
|
+
content = bytes.toString('utf8', 0, length);
|
|
71
|
+
const after = fs.fstatSync(descriptor);
|
|
72
|
+
if (after.dev !== opened.dev || after.ino !== opened.ino || after.size !== opened.size) {
|
|
73
|
+
throw new Error('TASK_FINALIZATION_HANDOFF_INVALID');
|
|
74
|
+
}
|
|
75
|
+
} finally {
|
|
76
|
+
fs.closeSync(descriptor);
|
|
77
|
+
}
|
|
78
|
+
const envelope = JSON.parse(content) as Partial<TaskFinalizationHandoff>;
|
|
79
|
+
if (!envelope || typeof envelope !== 'object' || Array.isArray(envelope)
|
|
80
|
+
|| Object.keys(envelope).sort().join('\0') !== ['binding', 'receipt', 'receiptSha256', 'taskId', 'version'].join('\0')
|
|
81
|
+
|| envelope.version !== HANDOFF_VERSION || envelope.taskId !== taskId
|
|
82
|
+
|| !envelope.binding || typeof envelope.binding !== 'object' || Array.isArray(envelope.binding)
|
|
83
|
+
|| Object.keys(envelope.binding).sort().join('\0') !== ['generation', 'requestId'].join('\0')
|
|
84
|
+
|| envelope.binding.generation !== binding.generation
|
|
85
|
+
|| envelope.binding.requestId !== binding.requestId || !envelope.receipt
|
|
86
|
+
|| envelope.receipt.controlBinding?.generation !== binding.generation
|
|
87
|
+
|| envelope.receipt.controlBinding.requestId !== binding.requestId
|
|
88
|
+
|| typeof envelope.receiptSha256 !== 'string' || envelope.receiptSha256 !== digest(envelope.receipt)
|
|
89
|
+
|| digest(envelope) !== handoffSha256) {
|
|
90
|
+
throw new Error('TASK_FINALIZATION_HANDOFF_INVALID');
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
receipt: envelope.receipt,
|
|
94
|
+
cleanup: () => {
|
|
95
|
+
try {
|
|
96
|
+
const current = fs.lstatSync(file);
|
|
97
|
+
if (current.isFile() && !current.isSymbolicLink()
|
|
98
|
+
&& current.dev === initial.dev && current.ino === initial.ino) fs.unlinkSync(file);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
}
|