@fitlab-ai/agent-infra 0.11.3 → 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/README.md +17 -8
- package/README.zh-CN.md +17 -8
- package/dist/bin/internal-cli.js +1 -6
- package/dist/lib/decide.js +4 -41
- package/dist/lib/git/branch-worktree.js +45 -0
- package/dist/lib/internal/cli-route-inventory.js +1 -4
- package/dist/lib/internal/task-activity.js +7 -40
- package/dist/lib/internal/task-artifact.js +1 -1
- package/dist/lib/internal/task-event.js +4 -37
- package/dist/lib/internal/task-finalization.js +48 -1
- package/dist/lib/internal/task-ledger.js +14 -44
- package/dist/lib/internal/task-lifecycle.js +1 -1
- package/dist/lib/internal/task-operation-registry.js +2 -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/completion-backfill.js +63 -17
- 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/control/server.js +26 -143
- package/dist/lib/sandbox/removal.js +15 -8
- package/dist/lib/sandbox/task-cleanup.js +13 -20
- package/dist/lib/task/activity-intent.js +1 -2
- package/dist/lib/task/artifact-command.js +11 -4
- package/dist/lib/task/artifact-lifecycle.js +217 -85
- package/dist/lib/task/artifact-operations.js +37 -28
- package/dist/lib/task/artifact-receipts.js +4 -1
- package/dist/lib/task/artifact-schema.js +8 -7
- package/dist/lib/task/artifact-selection.js +25 -0
- package/dist/lib/task/capabilities.js +158 -39
- package/dist/lib/task/commands/status.js +9 -2
- package/dist/lib/task/control-authority.js +12 -168
- package/dist/lib/task/decision-intents.js +14 -4
- package/dist/lib/task/delivery.js +1 -43
- package/dist/lib/task/events.js +68 -73
- package/dist/lib/task/finalization-handoff.js +92 -0
- package/dist/lib/task/finalization.js +423 -154
- package/dist/lib/task/intermediate-cleanup.js +9 -148
- package/dist/lib/task/ledger-intents.js +159 -20
- package/dist/lib/task/lifecycle-path.js +75 -0
- package/dist/lib/task/lifecycle.js +60 -11
- package/dist/lib/task/local-artifact-finalization.js +13 -47
- package/dist/lib/task/orchestration.js +17 -7
- package/dist/lib/task/review-command.js +1 -2
- package/dist/lib/task/review-finalization.js +4 -18
- package/dist/lib/task/rework-intent.js +70 -4
- package/dist/lib/task/snapshot.js +6 -6
- package/dist/lib/task/verification-engine.js +25 -30
- package/dist/lib/task/verification.js +2 -13
- package/dist/lib/task/workflow-warning-intents.js +23 -10
- package/dist/lib/task/workspace-snapshot.js +7 -2
- 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 +1 -3
- package/lib/internal/task-activity.ts +6 -34
- package/lib/internal/task-artifact.ts +1 -1
- package/lib/internal/task-event.ts +4 -37
- package/lib/internal/task-finalization.ts +47 -1
- package/lib/internal/task-ledger.ts +15 -41
- package/lib/internal/task-lifecycle.ts +2 -2
- package/lib/internal/task-operation-registry.ts +2 -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/completion-backfill.ts +78 -16
- 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/control/server.ts +21 -146
- package/lib/sandbox/removal.ts +23 -7
- package/lib/sandbox/task-cleanup.ts +12 -20
- package/lib/task/activity-intent.ts +1 -2
- package/lib/task/artifact-command.ts +14 -4
- package/lib/task/artifact-lifecycle.ts +236 -85
- package/lib/task/artifact-operations.ts +59 -28
- package/lib/task/artifact-receipts.ts +4 -1
- package/lib/task/artifact-schema.ts +10 -7
- package/lib/task/artifact-selection.ts +53 -0
- package/lib/task/capabilities.ts +154 -40
- package/lib/task/commands/status.ts +11 -2
- package/lib/task/control-authority.ts +17 -189
- package/lib/task/decision-intents.ts +11 -4
- package/lib/task/delivery.ts +1 -40
- package/lib/task/events.ts +73 -74
- package/lib/task/finalization-handoff.ts +104 -0
- package/lib/task/finalization.ts +388 -122
- package/lib/task/intermediate-cleanup.ts +9 -125
- package/lib/task/ledger-intents.ts +157 -20
- package/lib/task/lifecycle-path.ts +98 -0
- package/lib/task/lifecycle.ts +58 -13
- package/lib/task/local-artifact-finalization.ts +14 -56
- package/lib/task/orchestration.ts +26 -8
- package/lib/task/review-command.ts +2 -5
- package/lib/task/review-finalization.ts +4 -20
- package/lib/task/rework-intent.ts +75 -5
- package/lib/task/snapshot.ts +6 -6
- package/lib/task/verification-engine.ts +29 -30
- package/lib/task/verification.ts +3 -16
- package/lib/task/workflow-warning-intents.ts +20 -10
- package/lib/task/workspace-snapshot.ts +3 -2
- package/lib/task/write.ts +1 -4
- package/package.json +1 -1
- package/templates/.agents/rules/review-handshake.en.md +4 -4
- package/templates/.agents/rules/review-handshake.zh-CN.md +4 -4
- package/templates/.agents/skills/analyze-task/SKILL.en.md +21 -5
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +21 -5
- package/templates/.agents/skills/code-task/SKILL.en.md +14 -8
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +18 -13
- package/templates/.agents/skills/code-task/reference/code-rules.en.md +2 -2
- package/templates/.agents/skills/code-task/reference/code-rules.zh-CN.md +2 -2
- package/templates/.agents/skills/code-task/reference/dual-mode.en.md +4 -4
- package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +4 -4
- package/templates/.agents/skills/code-task/reference/report-template.en.md +7 -7
- package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +7 -7
- 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/SKILL.en.md +2 -2
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +2 -2
- 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/plan-task/SKILL.en.md +13 -10
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +13 -10
- package/templates/.agents/skills/review-analysis/SKILL.en.md +5 -1
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +5 -1
- 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/SKILL.en.md +5 -1
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +5 -1
- 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/SKILL.en.md +5 -1
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +5 -1
- 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/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +18 -2
- package/templates/.agents/templates/task.en.md +2 -2
- package/templates/.agents/templates/task.zh-CN.md +2 -2
- package/templates/.agents/workflows/bug-fix.en.yaml +11 -6
- package/templates/.agents/workflows/bug-fix.zh-CN.yaml +11 -6
- package/templates/.agents/workflows/feature-development.en.yaml +11 -6
- package/templates/.agents/workflows/feature-development.zh-CN.yaml +11 -6
- package/templates/.agents/workflows/refactoring.en.yaml +11 -6
- package/templates/.agents/workflows/refactoring.zh-CN.yaml +11 -6
- package/dist/lib/internal/task-override.js +0 -138
- package/dist/lib/sandbox/control/finalization-response.js +0 -33
- 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/sandbox/control/finalization-response.ts +0 -35
- 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
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,17 +34,17 @@ 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
|
-
import { buildLifecycleFacts, canStart } from './capabilities.ts';
|
|
39
|
+
import { buildLifecycleFacts, canStart, effectiveReworkTarget } from './capabilities.ts';
|
|
39
40
|
import type { ExplicitTrigger, LifecycleAction, TriggerInitiator, TriggerReason } from './capabilities.ts';
|
|
40
41
|
import { createInvalidationOperation, invalidationMutation, parseInvalidationDocument, targetIdFor, upsertInvalidation } from './invalidation.ts';
|
|
41
42
|
import type { InvalidationTargetKind } from './invalidation.ts';
|
|
42
43
|
import { reconcileTaskInvalidation } from './invalidation-command.ts';
|
|
43
44
|
import { consumeReworkIntents, parseReworkIntentDocument, reworkIntentMutation, supersedeReworkIntents } from './rework-intent.ts';
|
|
44
|
-
import {
|
|
45
|
-
import
|
|
45
|
+
import type { ReworkTarget } from './rework-intent.ts';
|
|
46
|
+
import { ARTIFACT_FAMILIES, parseQualificationAudit, parseTaskQualification, upstreamArtifactDigest } from './qualification-audit.ts';
|
|
47
|
+
import type { QualificationAudit } from './qualification-audit.ts';
|
|
46
48
|
import { getArtifactSchema } from './artifact-schema.ts';
|
|
47
49
|
import { canonicalSemanticDigest, inspectArtifactContract } from './artifact-operations.ts';
|
|
48
50
|
import {
|
|
@@ -52,6 +54,7 @@ import {
|
|
|
52
54
|
type LifecycleRecoveryAttestationV1
|
|
53
55
|
} from './control-authority.ts';
|
|
54
56
|
import { readManualValidationCompletion } from './manual-validation-completion.ts';
|
|
57
|
+
import { parseLifecyclePathDecision } from './lifecycle-path.ts';
|
|
55
58
|
|
|
56
59
|
const eventCatalog = [
|
|
57
60
|
'analyze.started', 'analyze.awaiting-input', 'analyze.completed',
|
|
@@ -79,7 +82,6 @@ type TaskEventErrorCode =
|
|
|
79
82
|
| ArtifactErrorCode | TaskWriteErrorCode;
|
|
80
83
|
type TaskEventRequest = {
|
|
81
84
|
taskRef: string; event: TaskEventName | string; agent: string; dryRun?: boolean; orchestrated?: boolean;
|
|
82
|
-
overrideTicket?: string; overrideTarget?: string; overrideScope?: string;
|
|
83
85
|
initiator?: TriggerInitiator; requestId?: string; reasonCode?: TriggerReason;
|
|
84
86
|
sourceFinding?: string; sourceArtifact?: string; sourceSha256?: string;
|
|
85
87
|
round?: number; question?: number; artifact?: string; fixFor?: string; implementationInput?: string;
|
|
@@ -107,24 +109,31 @@ type TaskEventResult = {
|
|
|
107
109
|
operations: readonly TaskOperationSummary[]; error: TaskEventError | null;
|
|
108
110
|
};
|
|
109
111
|
|
|
110
|
-
function
|
|
112
|
+
function branchWorktree(repoRoot: string, branch: string): string | null {
|
|
113
|
+
if (!branch) return repoRoot;
|
|
114
|
+
return resolveBranchWorktree(repoRoot, branch);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function approvedCleanReviewedCommit(reviewContent: string, verdict: Verdict | undefined, repoRoot: string, branch: string): string | null {
|
|
111
118
|
if (verdict !== 'approved') return null;
|
|
112
119
|
const reviewedHead = extractReviewedHead(reviewContent) || extractReviewBaseline(reviewContent);
|
|
113
120
|
const reviewedTree = extractReviewedSnapshotTree(reviewContent);
|
|
114
121
|
if (!/^[a-f0-9]{40}$/.test(reviewedHead) || !/^[a-f0-9]{40}$/.test(reviewedTree)) return null;
|
|
122
|
+
const worktree = branchWorktree(repoRoot, branch);
|
|
123
|
+
if (!worktree) return null;
|
|
115
124
|
try {
|
|
116
|
-
const currentHead = execFileSync('git', ['-C',
|
|
125
|
+
const currentHead = execFileSync('git', ['-C', worktree, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim();
|
|
117
126
|
if (currentHead !== reviewedHead) return null;
|
|
118
|
-
const currentTree = execFileSync('git', ['-C',
|
|
127
|
+
const currentTree = execFileSync('git', ['-C', worktree, 'rev-parse', 'HEAD^{tree}'], { encoding: 'utf8' }).trim();
|
|
119
128
|
if (currentTree !== reviewedTree) return null;
|
|
120
|
-
execFileSync('git', ['-C',
|
|
129
|
+
execFileSync('git', ['-C', worktree, 'diff-index', '--quiet', 'HEAD', '--']);
|
|
121
130
|
return reviewedHead;
|
|
122
131
|
} catch {
|
|
123
132
|
return null;
|
|
124
133
|
}
|
|
125
134
|
}
|
|
126
135
|
|
|
127
|
-
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']);
|
|
128
137
|
const SCHEMAS: Record<TaskEventName, { required?: string[]; optional?: string[] }> = {
|
|
129
138
|
'analyze.started': { optional: ['round'] },
|
|
130
139
|
'analyze.awaiting-input': { required: ['question'] },
|
|
@@ -329,7 +338,13 @@ function failed(request: TaskEventRequest, error: TaskEventError, extra: Partial
|
|
|
329
338
|
|
|
330
339
|
function normalizeStarted(request: TaskEventRequest, repoRoot: string): { request: TaskEventRequest; context: ArtifactContextResult } | { error: TaskEventError; context: ArtifactContextResult } {
|
|
331
340
|
const family = eventParts(request.event).family;
|
|
332
|
-
const context = resolveArtifactContext(request.taskRef, FAMILY[family].artifact, {
|
|
341
|
+
const context = resolveArtifactContext(request.taskRef, FAMILY[family].artifact, {
|
|
342
|
+
repoRoot,
|
|
343
|
+
reasonCode: request.reasonCode,
|
|
344
|
+
sourceFinding: request.sourceFinding,
|
|
345
|
+
sourceArtifact: request.sourceArtifact,
|
|
346
|
+
sourceSha256: request.sourceSha256
|
|
347
|
+
});
|
|
333
348
|
if (context.status !== 'ready' || !context.next) {
|
|
334
349
|
return { error: { code: context.error?.code ?? 'EVENT_ARTIFACT_CONFLICT', message: context.error?.message ?? context.codeMode?.message ?? 'artifact context is not writable' }, context };
|
|
335
350
|
}
|
|
@@ -386,25 +401,6 @@ function reviewInputFamily(family: EventFamily): ArtifactFamily {
|
|
|
386
401
|
return family === 'review-analysis' ? 'analysis' : family === 'review-plan' ? 'plan' : 'code';
|
|
387
402
|
}
|
|
388
403
|
|
|
389
|
-
function relationForStartedInput(family: EventFamily, input: ArtifactIdentity): UpstreamRelation['relation'] {
|
|
390
|
-
if (family.startsWith('review-')) {
|
|
391
|
-
return input.family === reviewInputFamily(family) ? 'reviewed-input' : 'approval-context';
|
|
392
|
-
}
|
|
393
|
-
const consumer = family === 'analyze' ? 'analysis' : family;
|
|
394
|
-
if ((consumer === 'plan' && input.family === 'analysis') || (consumer === 'code' && input.family === 'plan')) return 'required-input';
|
|
395
|
-
return 'review-context';
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
function qualificationRelationsForStarted(family: EventFamily, inputs: readonly ArtifactIdentity[]): UpstreamRelation[] {
|
|
399
|
-
return inputs.map((input) => ({
|
|
400
|
-
upstreamFamily: input.family as UpstreamRelation['upstreamFamily'],
|
|
401
|
-
upstreamArtifact: input.name,
|
|
402
|
-
upstreamRound: input.round,
|
|
403
|
-
upstreamSha256: sha256File(input.path),
|
|
404
|
-
relation: relationForStartedInput(family, input)
|
|
405
|
-
}));
|
|
406
|
-
}
|
|
407
|
-
|
|
408
404
|
function lifecycleAction(family: EventFamily): LifecycleAction {
|
|
409
405
|
return family === 'analyze' ? 'analysis' : family;
|
|
410
406
|
}
|
|
@@ -586,7 +582,20 @@ function reworkIntentMutationForCompletion(
|
|
|
586
582
|
try { return [[name, sha256File(path.join(taskDir, name))]]; }
|
|
587
583
|
catch { return []; }
|
|
588
584
|
}));
|
|
589
|
-
|
|
585
|
+
const action = family === 'analyze' ? 'analysis' : family;
|
|
586
|
+
let target: ReworkTarget = action;
|
|
587
|
+
if (family === 'analyze') {
|
|
588
|
+
const previousAnalysis = fs.readdirSync(taskDir)
|
|
589
|
+
.map((name) => parseArtifactName(name))
|
|
590
|
+
.filter((identity) => identity?.family === 'analysis' && identity.name !== artifact.name)
|
|
591
|
+
.sort((left, right) => right!.round - left!.round || left!.name.localeCompare(right!.name))[0];
|
|
592
|
+
const pathState = previousAnalysis
|
|
593
|
+
? parseLifecyclePathDecision(fs.readFileSync(path.join(taskDir, previousAnalysis.name), 'utf8'))
|
|
594
|
+
: undefined;
|
|
595
|
+
const pending = next.find((intent) => intent.status === 'pending');
|
|
596
|
+
if (pending && effectiveReworkTarget(pending.target, pathState) === action) target = pending.target;
|
|
597
|
+
}
|
|
598
|
+
next = consumeReworkIntents(next, target, hashes, timestamp).intents;
|
|
590
599
|
}
|
|
591
600
|
if (family === 'review-analysis' || family === 'review-plan' || family === 'review-code') {
|
|
592
601
|
next = supersedeReworkIntents(next, artifact.name, hash, timestamp).intents;
|
|
@@ -614,13 +623,15 @@ function buildCompletionReceipt(
|
|
|
614
623
|
? frontmatter.code_input_artifact : existing?.input ?? '';
|
|
615
624
|
const startedSha256 = typeof frontmatter.code_input_sha256 === 'string'
|
|
616
625
|
? frontmatter.code_input_sha256 : existing?.inputSha256 ?? '';
|
|
617
|
-
if (!startedInput || !startedSha256) return { ok: false, message: 'code.started
|
|
618
|
-
const
|
|
619
|
-
if (
|
|
620
|
-
|
|
626
|
+
if (!startedInput || !startedSha256) return { ok: false, message: 'code.started lifecycle input context is missing' };
|
|
627
|
+
const inputIdentity = parseArtifactName(startedInput);
|
|
628
|
+
if (!inputIdentity || !['analysis', 'plan'].includes(inputIdentity.family)) return { ok: false, message: `code input '${startedInput}' has an invalid family` };
|
|
629
|
+
const lifecycleInput = inspectArtifactDirectory(taskDir, inputIdentity.family);
|
|
630
|
+
if (lifecycleInput.status !== 'ready' || !lifecycleInput.latest || lifecycleInput.latest.name !== startedInput) {
|
|
631
|
+
return { ok: false, message: `code input '${startedInput}' is not the latest ${inputIdentity.family} artifact` };
|
|
621
632
|
}
|
|
622
633
|
try {
|
|
623
|
-
const inputSha256 = sha256File(
|
|
634
|
+
const inputSha256 = sha256File(lifecycleInput.latest.path);
|
|
624
635
|
if (inputSha256 !== startedSha256) return { ok: false, message: `code input ${startedInput} changed after code.started` };
|
|
625
636
|
return {
|
|
626
637
|
ok: true,
|
|
@@ -630,7 +641,7 @@ function buildCompletionReceipt(
|
|
|
630
641
|
}
|
|
631
642
|
};
|
|
632
643
|
} catch (error) {
|
|
633
|
-
return { ok: false, message: `cannot hash code input
|
|
644
|
+
return { ok: false, message: `cannot hash code lifecycle input: ${error instanceof Error ? error.message : String(error)}` };
|
|
634
645
|
}
|
|
635
646
|
}
|
|
636
647
|
if (!family.startsWith('review-')) return null;
|
|
@@ -732,8 +743,7 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
732
743
|
if (invalid) return failed(request, invalid);
|
|
733
744
|
const resolved = resolveTaskRef(request.taskRef, { repoRoot: options.repoRoot });
|
|
734
745
|
if (!resolved.ok) return failed(request, { code: resolved.code, message: resolved.message }, { taskId: resolved.taskId });
|
|
735
|
-
|
|
736
|
-
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 });
|
|
737
747
|
let content: string;
|
|
738
748
|
try { content = fs.readFileSync(resolved.taskMdPath, 'utf8'); }
|
|
739
749
|
catch (error) { return failed(request, { code: 'TASK_READ_FAILED', message: String(error) }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath }); }
|
|
@@ -774,6 +784,12 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
774
784
|
normalized = { ...request, round: openIdentity.round, artifact: artifactName(FAMILY[initialParts.family].artifact, openIdentity.round), fixFor: openIdentity.fixFor, implementationInput: openIdentity.implementationInput };
|
|
775
785
|
return successNoOp(normalized, resolved.taskId, resolved.taskMdPath, typeof frontmatter.current_step === 'string' ? frontmatter.current_step : '', identity(normalized), openIdentity.row.started, frontmatter, null);
|
|
776
786
|
}
|
|
787
|
+
if (rows.some((item) => item.started && !item.done)) {
|
|
788
|
+
return failed(request, {
|
|
789
|
+
code: 'EVENT_TRANSITION_INVALID',
|
|
790
|
+
message: 'EXECUTION_BUSY: another lifecycle execution is open'
|
|
791
|
+
}, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
|
|
792
|
+
}
|
|
777
793
|
const result = normalizeStarted(request, resolved.repoRoot);
|
|
778
794
|
artifactContext = result.context;
|
|
779
795
|
if ('error' in result) return failed(request, result.error, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, artifactContext });
|
|
@@ -819,7 +835,7 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
819
835
|
}
|
|
820
836
|
return successNoOp(normalized, resolved.taskId, resolved.taskMdPath, currentStep, eventIdentity, row.started, frontmatter, artifactContext);
|
|
821
837
|
}
|
|
822
|
-
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 });
|
|
823
839
|
if (eventIdentity.phase === 'completed') {
|
|
824
840
|
const validated = validateCompletedArtifact(resolved.taskDir, FAMILY[eventIdentity.family].artifact, normalized.artifact!, normalized.round);
|
|
825
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 });
|
|
@@ -836,25 +852,13 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
836
852
|
if (eventIdentity.family.startsWith('review-')) {
|
|
837
853
|
try { reviewContent = fs.readFileSync(completedArtifact.path, 'utf8'); }
|
|
838
854
|
catch (error) {
|
|
839
|
-
return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: `cannot read
|
|
855
|
+
return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: `cannot read ${completedArtifact.name}: ${error instanceof Error ? error.message : String(error)}` }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
840
856
|
}
|
|
841
857
|
const schema = getArtifactSchema(eventIdentity.family);
|
|
842
858
|
const structure = schema ? inspectArtifactContract(reviewContent, schema) : null;
|
|
843
859
|
if (structure && !structure.ok) {
|
|
844
860
|
return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: `shared artifact structure invalid: ${structure.diagnostics.map((item) => `${item.code}: ${item.message}`).join('; ')}` }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
845
861
|
}
|
|
846
|
-
const expected = expectedQualificationRelations(content, eventIdentity.family as 'review-analysis' | 'review-plan' | 'review-code');
|
|
847
|
-
if (!expected.ok) {
|
|
848
|
-
return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: `${expected.code}: ${expected.message}` }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
849
|
-
}
|
|
850
|
-
const qualification = validateQualificationAudit(content, reviewContent, {
|
|
851
|
-
family: eventIdentity.family as 'review-analysis' | 'review-plan' | 'review-code',
|
|
852
|
-
artifact: completedArtifact.name,
|
|
853
|
-
expectedUpstreamRelations: expected.relations
|
|
854
|
-
});
|
|
855
|
-
if (!qualification.ok) {
|
|
856
|
-
return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: `${qualification.code}: ${qualification.message}` }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
857
|
-
}
|
|
858
862
|
}
|
|
859
863
|
if (eventIdentity.family === 'analyze' || eventIdentity.family === 'plan' || eventIdentity.family === 'code') {
|
|
860
864
|
const localFamily = eventIdentity.family === 'analyze'
|
|
@@ -866,9 +870,7 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
866
870
|
return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: `cannot read ${completedArtifact.name}: ${String(error)}` }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
867
871
|
}
|
|
868
872
|
const local = validateLocalArtifact(artifactContent, {
|
|
869
|
-
family: localFamily
|
|
870
|
-
taskContent: content,
|
|
871
|
-
artifact: completedArtifact.name
|
|
873
|
+
family: localFamily
|
|
872
874
|
});
|
|
873
875
|
if (!local.ok) {
|
|
874
876
|
return failed(normalized, {
|
|
@@ -900,10 +902,9 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
900
902
|
const facts = buildLifecycleFacts(resolved.taskDir, content, resolved.state);
|
|
901
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 });
|
|
902
904
|
const capability = canStart(lifecycleAction(eventIdentity.family), facts.facts, trigger);
|
|
903
|
-
const
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
if (!capability.allowed && !capabilityOverride) {
|
|
905
|
+
const safetyFailure = capability.reasonCode === 'TASK_NOT_ACTIVE'
|
|
906
|
+
|| capability.reasonCode === 'LIFECYCLE_EXECUTION_OPEN';
|
|
907
|
+
if (!capability.allowed && (normalized.initiator === 'orchestrator' || safetyFailure)) {
|
|
907
908
|
const code = capability.reasonCode === 'INVALIDATION_INCOMPLETE' ? 'TASK_INVALIDATION_BLOCKED' : 'EVENT_TRANSITION_INVALID';
|
|
908
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 });
|
|
909
910
|
}
|
|
@@ -916,10 +917,11 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
916
917
|
);
|
|
917
918
|
if (findingCountError) return failed(normalized, findingCountError, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
918
919
|
let orchestrationCompletion: OrchestrationStageCompletion | null = null;
|
|
919
|
-
if (eventIdentity.phase === 'completed' &&
|
|
920
|
+
if (eventIdentity.phase === 'completed' && normalized.orchestrated
|
|
921
|
+
&& eventIdentity.family !== 'manual-validation' && eventIdentity.family !== 'validation-run') {
|
|
920
922
|
const orchestrationStage = eventIdentity.family === 'analyze' ? 'analysis' : eventIdentity.family;
|
|
921
923
|
const execution = validateLifecycleExecution(normalized.taskRef, {
|
|
922
|
-
mode:
|
|
924
|
+
mode: 'orchestrated',
|
|
923
925
|
identity: {
|
|
924
926
|
stage: orchestrationStage,
|
|
925
927
|
round: normalized.round!,
|
|
@@ -984,16 +986,9 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
984
986
|
const frontmatterSet: Record<string, string> = { current_step: step, assigned_to: normalized.agent };
|
|
985
987
|
if (currentFact) frontmatterSet.completion_facts = JSON.stringify(replaceCompletionFact(completionFacts(frontmatter), currentFact));
|
|
986
988
|
let frontmatterRemove: string[] | undefined;
|
|
987
|
-
if (eventIdentity.phase === 'started' && artifactContext) {
|
|
988
|
-
try {
|
|
989
|
-
frontmatterSet.qualification_input_relations = JSON.stringify(qualificationRelationsForStarted(eventIdentity.family, artifactContext.inputs));
|
|
990
|
-
} catch (error) {
|
|
991
|
-
return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: `cannot capture qualification input relations: ${error instanceof Error ? error.message : String(error)}` }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: step, action: eventIdentity.action, phase: eventIdentity.phase, artifactContext });
|
|
992
|
-
}
|
|
993
|
-
}
|
|
994
989
|
if (eventIdentity.phase === 'started' && eventIdentity.family === 'code') {
|
|
995
|
-
const planInput = artifactContext?.inputs.find((input) => input.family === 'plan');
|
|
996
|
-
if (!planInput) return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: 'code.started
|
|
990
|
+
const planInput = artifactContext?.inputs.find((input) => input.family === 'plan' || input.family === 'analysis');
|
|
991
|
+
if (!planInput) return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: 'code.started lifecycle input context is unavailable' }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: step, action: eventIdentity.action, phase: eventIdentity.phase, artifactContext });
|
|
997
992
|
try {
|
|
998
993
|
frontmatterSet.code_input_artifact = planInput.name;
|
|
999
994
|
frontmatterSet.code_input_sha256 = sha256File(planInput.path);
|
|
@@ -1016,11 +1011,15 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
1016
1011
|
frontmatterRemove = ['review_input_artifact', 'review_input_sha256'];
|
|
1017
1012
|
}
|
|
1018
1013
|
if (eventIdentity.phase === 'completed' && eventIdentity.family === 'review-code' && reviewContent !== null) {
|
|
1019
|
-
const reviewedCommit = approvedCleanReviewedCommit(
|
|
1014
|
+
const reviewedCommit = approvedCleanReviewedCommit(
|
|
1015
|
+
reviewContent,
|
|
1016
|
+
normalized.verdict,
|
|
1017
|
+
resolved.repoRoot,
|
|
1018
|
+
typeof frontmatter.branch === 'string' ? frontmatter.branch : ''
|
|
1019
|
+
);
|
|
1020
1020
|
if (reviewedCommit) frontmatterSet.last_reviewed_commit = reviewedCommit;
|
|
1021
1021
|
else if (normalized.verdict === 'approved') frontmatterSet.last_reviewed_commit = '';
|
|
1022
1022
|
}
|
|
1023
|
-
if (eventIdentity.phase === 'completed') frontmatterRemove = [...(frontmatterRemove ?? []), 'qualification_input_relations'];
|
|
1024
1023
|
if (eventIdentity.phase === 'started' && normalized.implementationInput) frontmatterSet.last_reviewed_commit = '';
|
|
1025
1024
|
const mutations: Parameters<typeof writeTask>[0]['mutations'][number][] = [
|
|
1026
1025
|
{ kind: 'frontmatter', set: frontmatterSet, remove: frontmatterRemove }
|
|
@@ -1060,7 +1059,7 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
1060
1059
|
mutations.push({ kind: 'section', aliases: ['活动日志', 'Activity Log'], heading: section.heading, body });
|
|
1061
1060
|
const sourceCompletion = eventIdentity.phase === 'completed'
|
|
1062
1061
|
&& ['analyze', 'plan', 'code'].includes(eventIdentity.family);
|
|
1063
|
-
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 });
|
|
1064
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 });
|
|
1065
1064
|
if (!normalized.dryRun && orchestrationCompletion) {
|
|
1066
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
|
+
}
|