@fitlab-ai/agent-infra 0.11.3 → 0.11.4

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 (102) hide show
  1. package/README.md +17 -8
  2. package/README.zh-CN.md +17 -8
  3. package/dist/bin/internal-cli.js +1 -1
  4. package/dist/lib/internal/cli-route-inventory.js +1 -1
  5. package/dist/lib/internal/task-artifact.js +1 -1
  6. package/dist/lib/internal/task-finalization.js +41 -1
  7. package/dist/lib/internal/task-ledger.js +13 -5
  8. package/dist/lib/internal/task-operation-registry.js +1 -0
  9. package/dist/lib/platform/completion-backfill.js +63 -17
  10. package/dist/lib/sandbox/control/server.js +26 -143
  11. package/dist/lib/sandbox/task-cleanup.js +13 -20
  12. package/dist/lib/task/artifact-command.js +11 -4
  13. package/dist/lib/task/artifact-lifecycle.js +217 -85
  14. package/dist/lib/task/artifact-operations.js +1 -26
  15. package/dist/lib/task/artifact-receipts.js +4 -1
  16. package/dist/lib/task/artifact-schema.js +1 -0
  17. package/dist/lib/task/artifact-selection.js +25 -0
  18. package/dist/lib/task/capabilities.js +158 -39
  19. package/dist/lib/task/commands/status.js +9 -2
  20. package/dist/lib/task/control-authority.js +3 -2
  21. package/dist/lib/task/decision-intents.js +12 -0
  22. package/dist/lib/task/delivery.js +1 -43
  23. package/dist/lib/task/events.js +80 -64
  24. package/dist/lib/task/finalization.js +301 -143
  25. package/dist/lib/task/ledger-intents.js +157 -15
  26. package/dist/lib/task/lifecycle-path.js +75 -0
  27. package/dist/lib/task/lifecycle.js +10 -0
  28. package/dist/lib/task/local-artifact-finalization.js +1 -20
  29. package/dist/lib/task/orchestration.js +17 -7
  30. package/dist/lib/task/review-finalization.js +3 -16
  31. package/dist/lib/task/rework-intent.js +70 -4
  32. package/dist/lib/task/verification-engine.js +20 -30
  33. package/dist/lib/task/verification.js +1 -0
  34. package/dist/lib/task/workflow-warning-intents.js +21 -6
  35. package/dist/lib/task/workspace-snapshot.js +7 -2
  36. package/dist/lifecycle-build-manifest.json +3 -3
  37. package/lib/internal/cli-route-inventory.ts +1 -1
  38. package/lib/internal/task-artifact.ts +1 -1
  39. package/lib/internal/task-finalization.ts +40 -1
  40. package/lib/internal/task-ledger.ts +12 -5
  41. package/lib/internal/task-operation-registry.ts +1 -0
  42. package/lib/platform/completion-backfill.ts +78 -16
  43. package/lib/sandbox/control/server.ts +21 -146
  44. package/lib/sandbox/task-cleanup.ts +12 -20
  45. package/lib/task/artifact-command.ts +14 -4
  46. package/lib/task/artifact-lifecycle.ts +236 -85
  47. package/lib/task/artifact-operations.ts +1 -26
  48. package/lib/task/artifact-receipts.ts +4 -1
  49. package/lib/task/artifact-schema.ts +1 -0
  50. package/lib/task/artifact-selection.ts +53 -0
  51. package/lib/task/capabilities.ts +154 -40
  52. package/lib/task/commands/status.ts +11 -2
  53. package/lib/task/control-authority.ts +3 -1
  54. package/lib/task/decision-intents.ts +9 -0
  55. package/lib/task/delivery.ts +1 -40
  56. package/lib/task/events.ts +82 -64
  57. package/lib/task/finalization.ts +269 -111
  58. package/lib/task/ledger-intents.ts +155 -15
  59. package/lib/task/lifecycle-path.ts +98 -0
  60. package/lib/task/lifecycle.ts +13 -0
  61. package/lib/task/local-artifact-finalization.ts +2 -25
  62. package/lib/task/orchestration.ts +26 -8
  63. package/lib/task/review-finalization.ts +3 -16
  64. package/lib/task/rework-intent.ts +75 -5
  65. package/lib/task/verification-engine.ts +19 -30
  66. package/lib/task/verification.ts +2 -1
  67. package/lib/task/workflow-warning-intents.ts +18 -6
  68. package/lib/task/workspace-snapshot.ts +3 -2
  69. package/package.json +1 -1
  70. package/templates/.agents/rules/review-handshake.en.md +4 -4
  71. package/templates/.agents/rules/review-handshake.zh-CN.md +4 -4
  72. package/templates/.agents/skills/analyze-task/SKILL.en.md +11 -4
  73. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +11 -4
  74. package/templates/.agents/skills/code-task/SKILL.en.md +14 -8
  75. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +18 -13
  76. package/templates/.agents/skills/code-task/reference/code-rules.en.md +2 -2
  77. package/templates/.agents/skills/code-task/reference/code-rules.zh-CN.md +2 -2
  78. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +4 -4
  79. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +4 -4
  80. package/templates/.agents/skills/code-task/reference/report-template.en.md +1 -1
  81. package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +1 -1
  82. package/templates/.agents/skills/complete-task/SKILL.en.md +2 -2
  83. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +2 -2
  84. package/templates/.agents/skills/plan-task/SKILL.en.md +13 -10
  85. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +13 -10
  86. package/templates/.agents/skills/review-analysis/SKILL.en.md +5 -1
  87. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +5 -1
  88. package/templates/.agents/skills/review-code/SKILL.en.md +5 -1
  89. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +5 -1
  90. package/templates/.agents/skills/review-plan/SKILL.en.md +5 -1
  91. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +5 -1
  92. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +18 -2
  93. package/templates/.agents/templates/task.en.md +2 -2
  94. package/templates/.agents/templates/task.zh-CN.md +2 -2
  95. package/templates/.agents/workflows/bug-fix.en.yaml +11 -6
  96. package/templates/.agents/workflows/bug-fix.zh-CN.yaml +11 -6
  97. package/templates/.agents/workflows/feature-development.en.yaml +11 -6
  98. package/templates/.agents/workflows/feature-development.zh-CN.yaml +11 -6
  99. package/templates/.agents/workflows/refactoring.en.yaml +11 -6
  100. package/templates/.agents/workflows/refactoring.zh-CN.yaml +11 -6
  101. package/dist/lib/sandbox/control/finalization-response.js +0 -33
  102. package/lib/sandbox/control/finalization-response.ts +0 -35
@@ -35,14 +35,15 @@ import type { TaskOperationSummary, TaskWriteErrorCode, TaskWriteOptions } from
35
35
  import { allowsManualOverride } from './guard-override.ts';
36
36
  import { validateLocalArtifact } from './local-artifact-finalization.ts';
37
37
  import type { LocalArtifactFamily } from './local-artifact-finalization.ts';
38
- import { buildLifecycleFacts, canStart } from './capabilities.ts';
38
+ import { buildLifecycleFacts, canStart, effectiveReworkTarget } from './capabilities.ts';
39
39
  import type { ExplicitTrigger, LifecycleAction, TriggerInitiator, TriggerReason } from './capabilities.ts';
40
40
  import { createInvalidationOperation, invalidationMutation, parseInvalidationDocument, targetIdFor, upsertInvalidation } from './invalidation.ts';
41
41
  import type { InvalidationTargetKind } from './invalidation.ts';
42
42
  import { reconcileTaskInvalidation } from './invalidation-command.ts';
43
43
  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';
44
+ import type { ReworkTarget } from './rework-intent.ts';
45
+ import { ARTIFACT_FAMILIES, parseQualificationAudit, parseTaskQualification, upstreamArtifactDigest } from './qualification-audit.ts';
46
+ import type { QualificationAudit } from './qualification-audit.ts';
46
47
  import { getArtifactSchema } from './artifact-schema.ts';
47
48
  import { canonicalSemanticDigest, inspectArtifactContract } from './artifact-operations.ts';
48
49
  import {
@@ -52,6 +53,7 @@ import {
52
53
  type LifecycleRecoveryAttestationV1
53
54
  } from './control-authority.ts';
54
55
  import { readManualValidationCompletion } from './manual-validation-completion.ts';
56
+ import { parseLifecyclePathDecision } from './lifecycle-path.ts';
55
57
 
56
58
  const eventCatalog = [
57
59
  'analyze.started', 'analyze.awaiting-input', 'analyze.completed',
@@ -107,17 +109,39 @@ 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
+ try {
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;
130
+ }
131
+
132
+ function approvedCleanReviewedCommit(reviewContent: string, verdict: Verdict | undefined, repoRoot: string, branch: string): string | null {
111
133
  if (verdict !== 'approved') return null;
112
134
  const reviewedHead = extractReviewedHead(reviewContent) || extractReviewBaseline(reviewContent);
113
135
  const reviewedTree = extractReviewedSnapshotTree(reviewContent);
114
136
  if (!/^[a-f0-9]{40}$/.test(reviewedHead) || !/^[a-f0-9]{40}$/.test(reviewedTree)) return null;
137
+ const worktree = branchWorktree(repoRoot, branch);
138
+ if (!worktree) return null;
115
139
  try {
116
- const currentHead = execFileSync('git', ['-C', repoRoot, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim();
140
+ const currentHead = execFileSync('git', ['-C', worktree, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim();
117
141
  if (currentHead !== reviewedHead) return null;
118
- const currentTree = execFileSync('git', ['-C', repoRoot, 'rev-parse', 'HEAD^{tree}'], { encoding: 'utf8' }).trim();
142
+ const currentTree = execFileSync('git', ['-C', worktree, 'rev-parse', 'HEAD^{tree}'], { encoding: 'utf8' }).trim();
119
143
  if (currentTree !== reviewedTree) return null;
120
- execFileSync('git', ['-C', repoRoot, 'diff-index', '--quiet', 'HEAD', '--']);
144
+ execFileSync('git', ['-C', worktree, 'diff-index', '--quiet', 'HEAD', '--']);
121
145
  return reviewedHead;
122
146
  } catch {
123
147
  return null;
@@ -329,7 +353,13 @@ function failed(request: TaskEventRequest, error: TaskEventError, extra: Partial
329
353
 
330
354
  function normalizeStarted(request: TaskEventRequest, repoRoot: string): { request: TaskEventRequest; context: ArtifactContextResult } | { error: TaskEventError; context: ArtifactContextResult } {
331
355
  const family = eventParts(request.event).family;
332
- const context = resolveArtifactContext(request.taskRef, FAMILY[family].artifact, { repoRoot });
356
+ const context = resolveArtifactContext(request.taskRef, FAMILY[family].artifact, {
357
+ repoRoot,
358
+ reasonCode: request.reasonCode,
359
+ sourceFinding: request.sourceFinding,
360
+ sourceArtifact: request.sourceArtifact,
361
+ sourceSha256: request.sourceSha256
362
+ });
333
363
  if (context.status !== 'ready' || !context.next) {
334
364
  return { error: { code: context.error?.code ?? 'EVENT_ARTIFACT_CONFLICT', message: context.error?.message ?? context.codeMode?.message ?? 'artifact context is not writable' }, context };
335
365
  }
@@ -386,25 +416,6 @@ function reviewInputFamily(family: EventFamily): ArtifactFamily {
386
416
  return family === 'review-analysis' ? 'analysis' : family === 'review-plan' ? 'plan' : 'code';
387
417
  }
388
418
 
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
419
  function lifecycleAction(family: EventFamily): LifecycleAction {
409
420
  return family === 'analyze' ? 'analysis' : family;
410
421
  }
@@ -586,7 +597,20 @@ function reworkIntentMutationForCompletion(
586
597
  try { return [[name, sha256File(path.join(taskDir, name))]]; }
587
598
  catch { return []; }
588
599
  }));
589
- next = consumeReworkIntents(next, family === 'analyze' ? 'analysis' : family, hashes, timestamp).intents;
600
+ const action = family === 'analyze' ? 'analysis' : family;
601
+ let target: ReworkTarget = action;
602
+ if (family === 'analyze') {
603
+ const previousAnalysis = fs.readdirSync(taskDir)
604
+ .map((name) => parseArtifactName(name))
605
+ .filter((identity) => identity?.family === 'analysis' && identity.name !== artifact.name)
606
+ .sort((left, right) => right!.round - left!.round || left!.name.localeCompare(right!.name))[0];
607
+ const pathState = previousAnalysis
608
+ ? parseLifecyclePathDecision(fs.readFileSync(path.join(taskDir, previousAnalysis.name), 'utf8'))
609
+ : undefined;
610
+ const pending = next.find((intent) => intent.status === 'pending');
611
+ if (pending && effectiveReworkTarget(pending.target, pathState) === action) target = pending.target;
612
+ }
613
+ next = consumeReworkIntents(next, target, hashes, timestamp).intents;
590
614
  }
591
615
  if (family === 'review-analysis' || family === 'review-plan' || family === 'review-code') {
592
616
  next = supersedeReworkIntents(next, artifact.name, hash, timestamp).intents;
@@ -614,13 +638,15 @@ function buildCompletionReceipt(
614
638
  ? frontmatter.code_input_artifact : existing?.input ?? '';
615
639
  const startedSha256 = typeof frontmatter.code_input_sha256 === 'string'
616
640
  ? 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` };
641
+ if (!startedInput || !startedSha256) return { ok: false, message: 'code.started lifecycle input context is missing' };
642
+ const inputIdentity = parseArtifactName(startedInput);
643
+ if (!inputIdentity || !['analysis', 'plan'].includes(inputIdentity.family)) return { ok: false, message: `code input '${startedInput}' has an invalid family` };
644
+ const lifecycleInput = inspectArtifactDirectory(taskDir, inputIdentity.family);
645
+ if (lifecycleInput.status !== 'ready' || !lifecycleInput.latest || lifecycleInput.latest.name !== startedInput) {
646
+ return { ok: false, message: `code input '${startedInput}' is not the latest ${inputIdentity.family} artifact` };
621
647
  }
622
648
  try {
623
- const inputSha256 = sha256File(plan.latest.path);
649
+ const inputSha256 = sha256File(lifecycleInput.latest.path);
624
650
  if (inputSha256 !== startedSha256) return { ok: false, message: `code input ${startedInput} changed after code.started` };
625
651
  return {
626
652
  ok: true,
@@ -630,7 +656,7 @@ function buildCompletionReceipt(
630
656
  }
631
657
  };
632
658
  } catch (error) {
633
- return { ok: false, message: `cannot hash code input plan: ${error instanceof Error ? error.message : String(error)}` };
659
+ return { ok: false, message: `cannot hash code lifecycle input: ${error instanceof Error ? error.message : String(error)}` };
634
660
  }
635
661
  }
636
662
  if (!family.startsWith('review-')) return null;
@@ -774,6 +800,12 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
774
800
  normalized = { ...request, round: openIdentity.round, artifact: artifactName(FAMILY[initialParts.family].artifact, openIdentity.round), fixFor: openIdentity.fixFor, implementationInput: openIdentity.implementationInput };
775
801
  return successNoOp(normalized, resolved.taskId, resolved.taskMdPath, typeof frontmatter.current_step === 'string' ? frontmatter.current_step : '', identity(normalized), openIdentity.row.started, frontmatter, null);
776
802
  }
803
+ if (rows.some((item) => item.started && !item.done)) {
804
+ return failed(request, {
805
+ code: 'EVENT_TRANSITION_INVALID',
806
+ message: 'EXECUTION_BUSY: another lifecycle execution is open'
807
+ }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
808
+ }
777
809
  const result = normalizeStarted(request, resolved.repoRoot);
778
810
  artifactContext = result.context;
779
811
  if ('error' in result) return failed(request, result.error, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, artifactContext });
@@ -836,25 +868,13 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
836
868
  if (eventIdentity.family.startsWith('review-')) {
837
869
  try { reviewContent = fs.readFileSync(completedArtifact.path, 'utf8'); }
838
870
  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 });
871
+ 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
872
  }
841
873
  const schema = getArtifactSchema(eventIdentity.family);
842
874
  const structure = schema ? inspectArtifactContract(reviewContent, schema) : null;
843
875
  if (structure && !structure.ok) {
844
876
  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
877
  }
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
878
  }
859
879
  if (eventIdentity.family === 'analyze' || eventIdentity.family === 'plan' || eventIdentity.family === 'code') {
860
880
  const localFamily = eventIdentity.family === 'analyze'
@@ -866,9 +886,7 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
866
886
  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
887
  }
868
888
  const local = validateLocalArtifact(artifactContent, {
869
- family: localFamily,
870
- taskContent: content,
871
- artifact: completedArtifact.name
889
+ family: localFamily
872
890
  });
873
891
  if (!local.ok) {
874
892
  return failed(normalized, {
@@ -903,7 +921,9 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
903
921
  const capabilityOverride = capability.reasonCode === 'TASK_NOT_ACTIVE'
904
922
  ? stateOverride
905
923
  : allowsManualOverride(options.manualOverride, 'task-event', 'EVENT_TRANSITION_INVALID');
906
- if (!capability.allowed && !capabilityOverride) {
924
+ const safetyFailure = capability.reasonCode === 'TASK_NOT_ACTIVE'
925
+ || capability.reasonCode === 'LIFECYCLE_EXECUTION_OPEN';
926
+ if (!capability.allowed && (normalized.initiator === 'orchestrator' || safetyFailure) && !capabilityOverride) {
907
927
  const code = capability.reasonCode === 'INVALIDATION_INCOMPLETE' ? 'TASK_INVALIDATION_BLOCKED' : 'EVENT_TRANSITION_INVALID';
908
928
  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
929
  }
@@ -916,10 +936,11 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
916
936
  );
917
937
  if (findingCountError) return failed(normalized, findingCountError, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
918
938
  let orchestrationCompletion: OrchestrationStageCompletion | null = null;
919
- if (eventIdentity.phase === 'completed' && eventIdentity.family !== 'manual-validation' && eventIdentity.family !== 'validation-run') {
939
+ if (eventIdentity.phase === 'completed' && normalized.orchestrated
940
+ && eventIdentity.family !== 'manual-validation' && eventIdentity.family !== 'validation-run') {
920
941
  const orchestrationStage = eventIdentity.family === 'analyze' ? 'analysis' : eventIdentity.family;
921
942
  const execution = validateLifecycleExecution(normalized.taskRef, {
922
- mode: normalized.orchestrated ? 'orchestrated' : 'standalone',
943
+ mode: 'orchestrated',
923
944
  identity: {
924
945
  stage: orchestrationStage,
925
946
  round: normalized.round!,
@@ -984,16 +1005,9 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
984
1005
  const frontmatterSet: Record<string, string> = { current_step: step, assigned_to: normalized.agent };
985
1006
  if (currentFact) frontmatterSet.completion_facts = JSON.stringify(replaceCompletionFact(completionFacts(frontmatter), currentFact));
986
1007
  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
1008
  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 });
1009
+ const planInput = artifactContext?.inputs.find((input) => input.family === 'plan' || input.family === 'analysis');
1010
+ 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
1011
  try {
998
1012
  frontmatterSet.code_input_artifact = planInput.name;
999
1013
  frontmatterSet.code_input_sha256 = sha256File(planInput.path);
@@ -1016,11 +1030,15 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
1016
1030
  frontmatterRemove = ['review_input_artifact', 'review_input_sha256'];
1017
1031
  }
1018
1032
  if (eventIdentity.phase === 'completed' && eventIdentity.family === 'review-code' && reviewContent !== null) {
1019
- const reviewedCommit = approvedCleanReviewedCommit(reviewContent, normalized.verdict, resolved.repoRoot);
1033
+ const reviewedCommit = approvedCleanReviewedCommit(
1034
+ reviewContent,
1035
+ normalized.verdict,
1036
+ resolved.repoRoot,
1037
+ typeof frontmatter.branch === 'string' ? frontmatter.branch : ''
1038
+ );
1020
1039
  if (reviewedCommit) frontmatterSet.last_reviewed_commit = reviewedCommit;
1021
1040
  else if (normalized.verdict === 'approved') frontmatterSet.last_reviewed_commit = '';
1022
1041
  }
1023
- if (eventIdentity.phase === 'completed') frontmatterRemove = [...(frontmatterRemove ?? []), 'qualification_input_relations'];
1024
1042
  if (eventIdentity.phase === 'started' && normalized.implementationInput) frontmatterSet.last_reviewed_commit = '';
1025
1043
  const mutations: Parameters<typeof writeTask>[0]['mutations'][number][] = [
1026
1044
  { kind: 'frontmatter', set: frontmatterSet, remove: frontmatterRemove }