@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.
Files changed (172) hide show
  1. package/README.md +17 -8
  2. package/README.zh-CN.md +17 -8
  3. package/dist/bin/internal-cli.js +1 -6
  4. package/dist/lib/decide.js +4 -41
  5. package/dist/lib/git/branch-worktree.js +45 -0
  6. package/dist/lib/internal/cli-route-inventory.js +1 -4
  7. package/dist/lib/internal/task-activity.js +7 -40
  8. package/dist/lib/internal/task-artifact.js +1 -1
  9. package/dist/lib/internal/task-event.js +4 -37
  10. package/dist/lib/internal/task-finalization.js +48 -1
  11. package/dist/lib/internal/task-ledger.js +14 -44
  12. package/dist/lib/internal/task-lifecycle.js +1 -1
  13. package/dist/lib/internal/task-operation-registry.js +2 -4
  14. package/dist/lib/internal/task-review.js +5 -37
  15. package/dist/lib/internal/task-verify.js +3 -47
  16. package/dist/lib/internal/task-warning.js +4 -40
  17. package/dist/lib/platform/completion-backfill.js +63 -17
  18. package/dist/lib/platform/pr-summary.js +4 -23
  19. package/dist/lib/platform/verification-sync.js +12 -49
  20. package/dist/lib/sandbox/control/client.js +4 -1
  21. package/dist/lib/sandbox/control/executor.js +12 -4
  22. package/dist/lib/sandbox/control/protocol.js +2 -1
  23. package/dist/lib/sandbox/control/server.js +26 -143
  24. package/dist/lib/sandbox/removal.js +15 -8
  25. package/dist/lib/sandbox/task-cleanup.js +13 -20
  26. package/dist/lib/task/activity-intent.js +1 -2
  27. package/dist/lib/task/artifact-command.js +11 -4
  28. package/dist/lib/task/artifact-lifecycle.js +217 -85
  29. package/dist/lib/task/artifact-operations.js +37 -28
  30. package/dist/lib/task/artifact-receipts.js +4 -1
  31. package/dist/lib/task/artifact-schema.js +8 -7
  32. package/dist/lib/task/artifact-selection.js +25 -0
  33. package/dist/lib/task/capabilities.js +158 -39
  34. package/dist/lib/task/commands/status.js +9 -2
  35. package/dist/lib/task/control-authority.js +12 -168
  36. package/dist/lib/task/decision-intents.js +14 -4
  37. package/dist/lib/task/delivery.js +1 -43
  38. package/dist/lib/task/events.js +68 -73
  39. package/dist/lib/task/finalization-handoff.js +92 -0
  40. package/dist/lib/task/finalization.js +423 -154
  41. package/dist/lib/task/intermediate-cleanup.js +9 -148
  42. package/dist/lib/task/ledger-intents.js +159 -20
  43. package/dist/lib/task/lifecycle-path.js +75 -0
  44. package/dist/lib/task/lifecycle.js +60 -11
  45. package/dist/lib/task/local-artifact-finalization.js +13 -47
  46. package/dist/lib/task/orchestration.js +17 -7
  47. package/dist/lib/task/review-command.js +1 -2
  48. package/dist/lib/task/review-finalization.js +4 -18
  49. package/dist/lib/task/rework-intent.js +70 -4
  50. package/dist/lib/task/snapshot.js +6 -6
  51. package/dist/lib/task/verification-engine.js +25 -30
  52. package/dist/lib/task/verification.js +2 -13
  53. package/dist/lib/task/workflow-warning-intents.js +23 -10
  54. package/dist/lib/task/workspace-snapshot.js +7 -2
  55. package/dist/lib/task/write.js +1 -2
  56. package/dist/lifecycle-build-manifest.json +3 -3
  57. package/lib/decide.ts +5 -35
  58. package/lib/git/branch-worktree.ts +41 -0
  59. package/lib/internal/cli-route-inventory.ts +1 -3
  60. package/lib/internal/task-activity.ts +6 -34
  61. package/lib/internal/task-artifact.ts +1 -1
  62. package/lib/internal/task-event.ts +4 -37
  63. package/lib/internal/task-finalization.ts +47 -1
  64. package/lib/internal/task-ledger.ts +15 -41
  65. package/lib/internal/task-lifecycle.ts +2 -2
  66. package/lib/internal/task-operation-registry.ts +2 -4
  67. package/lib/internal/task-review.ts +5 -31
  68. package/lib/internal/task-verify.ts +3 -39
  69. package/lib/internal/task-warning.ts +3 -32
  70. package/lib/platform/completion-backfill.ts +78 -16
  71. package/lib/platform/pr-summary.ts +4 -25
  72. package/lib/platform/verification-sync.ts +13 -53
  73. package/lib/sandbox/control/client.ts +5 -1
  74. package/lib/sandbox/control/executor.ts +13 -4
  75. package/lib/sandbox/control/protocol.ts +3 -2
  76. package/lib/sandbox/control/server.ts +21 -146
  77. package/lib/sandbox/removal.ts +23 -7
  78. package/lib/sandbox/task-cleanup.ts +12 -20
  79. package/lib/task/activity-intent.ts +1 -2
  80. package/lib/task/artifact-command.ts +14 -4
  81. package/lib/task/artifact-lifecycle.ts +236 -85
  82. package/lib/task/artifact-operations.ts +59 -28
  83. package/lib/task/artifact-receipts.ts +4 -1
  84. package/lib/task/artifact-schema.ts +10 -7
  85. package/lib/task/artifact-selection.ts +53 -0
  86. package/lib/task/capabilities.ts +154 -40
  87. package/lib/task/commands/status.ts +11 -2
  88. package/lib/task/control-authority.ts +17 -189
  89. package/lib/task/decision-intents.ts +11 -4
  90. package/lib/task/delivery.ts +1 -40
  91. package/lib/task/events.ts +73 -74
  92. package/lib/task/finalization-handoff.ts +104 -0
  93. package/lib/task/finalization.ts +388 -122
  94. package/lib/task/intermediate-cleanup.ts +9 -125
  95. package/lib/task/ledger-intents.ts +157 -20
  96. package/lib/task/lifecycle-path.ts +98 -0
  97. package/lib/task/lifecycle.ts +58 -13
  98. package/lib/task/local-artifact-finalization.ts +14 -56
  99. package/lib/task/orchestration.ts +26 -8
  100. package/lib/task/review-command.ts +2 -5
  101. package/lib/task/review-finalization.ts +4 -20
  102. package/lib/task/rework-intent.ts +75 -5
  103. package/lib/task/snapshot.ts +6 -6
  104. package/lib/task/verification-engine.ts +29 -30
  105. package/lib/task/verification.ts +3 -16
  106. package/lib/task/workflow-warning-intents.ts +20 -10
  107. package/lib/task/workspace-snapshot.ts +3 -2
  108. package/lib/task/write.ts +1 -4
  109. package/package.json +1 -1
  110. package/templates/.agents/rules/review-handshake.en.md +4 -4
  111. package/templates/.agents/rules/review-handshake.zh-CN.md +4 -4
  112. package/templates/.agents/skills/analyze-task/SKILL.en.md +21 -5
  113. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +21 -5
  114. package/templates/.agents/skills/code-task/SKILL.en.md +14 -8
  115. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +18 -13
  116. package/templates/.agents/skills/code-task/reference/code-rules.en.md +2 -2
  117. package/templates/.agents/skills/code-task/reference/code-rules.zh-CN.md +2 -2
  118. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +4 -4
  119. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +4 -4
  120. package/templates/.agents/skills/code-task/reference/report-template.en.md +7 -7
  121. package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +7 -7
  122. package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +2 -2
  123. package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +2 -2
  124. package/templates/.agents/skills/complete-manual-validation/reference/report-template.en.md +1 -3
  125. package/templates/.agents/skills/complete-manual-validation/reference/report-template.zh-CN.md +1 -3
  126. package/templates/.agents/skills/complete-task/SKILL.en.md +2 -2
  127. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +2 -2
  128. package/templates/.agents/skills/complete-task/config/verify.en.json +2 -2
  129. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +2 -2
  130. package/templates/.agents/skills/plan-task/SKILL.en.md +13 -10
  131. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +13 -10
  132. package/templates/.agents/skills/review-analysis/SKILL.en.md +5 -1
  133. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +5 -1
  134. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +3 -3
  135. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +3 -3
  136. package/templates/.agents/skills/review-code/SKILL.en.md +5 -1
  137. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +5 -1
  138. package/templates/.agents/skills/review-code/reference/report-template.en.md +3 -3
  139. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +3 -3
  140. package/templates/.agents/skills/review-plan/SKILL.en.md +5 -1
  141. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +5 -1
  142. package/templates/.agents/skills/review-plan/reference/report-template.en.md +3 -3
  143. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +3 -3
  144. package/templates/.agents/skills/review-pr/config/verify.en.json +3 -1
  145. package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +3 -1
  146. package/templates/.agents/skills/review-pr/reference/report-template.en.md +3 -3
  147. package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +3 -3
  148. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +2 -2
  149. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +2 -2
  150. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +2 -2
  151. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +2 -2
  152. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +18 -2
  153. package/templates/.agents/templates/task.en.md +2 -2
  154. package/templates/.agents/templates/task.zh-CN.md +2 -2
  155. package/templates/.agents/workflows/bug-fix.en.yaml +11 -6
  156. package/templates/.agents/workflows/bug-fix.zh-CN.yaml +11 -6
  157. package/templates/.agents/workflows/feature-development.en.yaml +11 -6
  158. package/templates/.agents/workflows/feature-development.zh-CN.yaml +11 -6
  159. package/templates/.agents/workflows/refactoring.en.yaml +11 -6
  160. package/templates/.agents/workflows/refactoring.zh-CN.yaml +11 -6
  161. package/dist/lib/internal/task-override.js +0 -138
  162. package/dist/lib/sandbox/control/finalization-response.js +0 -33
  163. package/dist/lib/task/artifact-recovery.js +0 -476
  164. package/dist/lib/task/artifact-repair-intent.js +0 -181
  165. package/dist/lib/task/guard-override.js +0 -31
  166. package/dist/lib/task/human-override.js +0 -1259
  167. package/lib/internal/task-override.ts +0 -144
  168. package/lib/sandbox/control/finalization-response.ts +0 -35
  169. package/lib/task/artifact-recovery.ts +0 -572
  170. package/lib/task/artifact-repair-intent.ts +0 -227
  171. package/lib/task/guard-override.ts +0 -50
  172. package/lib/task/human-override.ts +0 -1522
@@ -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 { ARTIFACT_FAMILIES, expectedQualificationRelations, parseQualificationAudit, parseTaskQualification, upstreamArtifactDigest, validateQualificationAudit } from './qualification-audit.ts';
45
- import type { QualificationAudit, UpstreamRelation } from './qualification-audit.ts';
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 approvedCleanReviewedCommit(reviewContent: string, verdict: Verdict | undefined, repoRoot: string): string | null {
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', repoRoot, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim();
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', repoRoot, 'rev-parse', 'HEAD^{tree}'], { encoding: 'utf8' }).trim();
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', repoRoot, 'diff-index', '--quiet', 'HEAD', '--']);
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', 'overrideTicket', 'overrideTarget', 'overrideScope', 'initiator', 'requestId', 'reasonCode', 'sourceFinding', 'sourceArtifact', 'sourceSha256']);
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, { repoRoot });
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
- next = consumeReworkIntents(next, family === 'analyze' ? 'analysis' : family, hashes, timestamp).intents;
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 plan input context is missing' };
618
- const plan = inspectArtifactDirectory(taskDir, 'plan');
619
- if (plan.status !== 'ready' || !plan.latest || plan.latest.name !== startedInput) {
620
- return { ok: false, message: `code input '${startedInput}' is not the latest plan artifact` };
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(plan.latest.path);
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 plan: ${error instanceof Error ? error.message : String(error)}` };
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
- const stateOverride = allowsManualOverride(options.manualOverride, 'task-event', 'TASK_STATE_MISMATCH');
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 && !allowsManualOverride(options.manualOverride, 'task-event', 'EVENT_START_MISSING')) 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 });
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 qualification audit from ${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 });
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 capabilityOverride = capability.reasonCode === 'TASK_NOT_ACTIVE'
904
- ? stateOverride
905
- : allowsManualOverride(options.manualOverride, 'task-event', 'EVENT_TRANSITION_INVALID');
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' && eventIdentity.family !== 'manual-validation' && eventIdentity.family !== 'validation-run') {
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: normalized.orchestrated ? 'orchestrated' : 'standalone',
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 plan input context is unavailable' }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: step, action: eventIdentity.action, phase: eventIdentity.phase, artifactContext });
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(reviewContent, normalized.verdict, resolved.repoRoot);
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: stateOverride ? resolved.state : 'active', dryRun: normalized.dryRun, mutations }, { ...options, invalidationContext: sourceCompletion ? 'source-completion' : 'standard', metadataProvider: () => metadata });
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
+ }