@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
@@ -13,6 +13,7 @@ type ArtifactSchema = Readonly<{
13
13
  family: ArtifactSchemaFamily;
14
14
  title: Readonly<{ zh: string; en: string }>;
15
15
  sections: readonly ArtifactSection[];
16
+ stateCheckPatterns: readonly string[];
16
17
  requiredPatterns: readonly string[];
17
18
  }>;
18
19
  type ArtifactSkeletonInput = Readonly<{
@@ -30,6 +31,7 @@ const LOCAL_SECTIONS = {
30
31
  ['impact', '影响评估', 'Impact Assessment'],
31
32
  ['risks', '技术风险', 'Technical Risks'],
32
33
  ['effort', '工作量和复杂度评估', 'Effort and Complexity Assessment'],
34
+ ['flow-decision', '流程裁定', 'Flow Decision'],
33
35
  ['state-check', '状态核对', 'State Check']
34
36
  ],
35
37
  plan: [
@@ -105,21 +107,22 @@ function buildSections(family: ArtifactSchemaFamily): readonly ArtifactSection[]
105
107
  }));
106
108
  }
107
109
 
110
+ const TASK_SNAPSHOT_PATTERN = 'agent-infra-internal[ \\t]+task-snapshot\\b';
111
+
108
112
  const ARTIFACT_SCHEMAS: readonly ArtifactSchema[] = [
109
- { family: 'analysis', title: { zh: '需求分析报告', en: 'Requirements Analysis' }, sections: buildSections('analysis'), requiredPatterns: ['^\\$ '] },
110
- { family: 'review-analysis', title: { zh: '需求分析审查报告', en: 'Analysis Review Report' }, sections: buildSections('review-analysis'), requiredPatterns: ['^### (?:审查决定|Approval Decision)$', '^\\$ '] },
111
- { family: 'plan', title: { zh: '技术方案', en: 'Technical Plan' }, sections: buildSections('plan'), requiredPatterns: ['^\\$ '] },
112
- { family: 'review-plan', title: { zh: '技术方案审查报告', en: 'Plan Review Report' }, sections: buildSections('review-plan'), requiredPatterns: ['^### (?:审查决定|Approval Decision)$', '^\\$ '] },
113
- { family: 'code', title: { zh: '实现报告', en: 'Implementation Report' }, sections: buildSections('code'), requiredPatterns: ['^\\$ '] },
113
+ { family: 'analysis', title: { zh: '需求分析报告', en: 'Requirements Analysis' }, sections: buildSections('analysis'), stateCheckPatterns: [TASK_SNAPSHOT_PATTERN], requiredPatterns: [] },
114
+ { family: 'review-analysis', title: { zh: '需求分析审查报告', en: 'Analysis Review Report' }, sections: buildSections('review-analysis'), stateCheckPatterns: [TASK_SNAPSHOT_PATTERN], requiredPatterns: ['^### (?:审查决定|Approval Decision)$'] },
115
+ { family: 'plan', title: { zh: '技术方案', en: 'Technical Plan' }, sections: buildSections('plan'), stateCheckPatterns: [TASK_SNAPSHOT_PATTERN], requiredPatterns: [] },
116
+ { family: 'review-plan', title: { zh: '技术方案审查报告', en: 'Plan Review Report' }, sections: buildSections('review-plan'), stateCheckPatterns: [TASK_SNAPSHOT_PATTERN], requiredPatterns: ['^### (?:审查决定|Approval Decision)$'] },
117
+ { family: 'code', title: { zh: '实现报告', en: 'Implementation Report' }, sections: buildSections('code'), stateCheckPatterns: [TASK_SNAPSHOT_PATTERN], requiredPatterns: [] },
114
118
  { family: 'review-code', title: { zh: '代码审查报告', en: 'Code Review Report' }, sections: buildSections('review-code'), requiredPatterns: [
115
119
  '^### (?:审查决定|Approval Decision)$',
116
- '^\\$ ',
117
120
  '^- \\*\\*(?:总体结论|Overall Verdict)\\*\\*[::]\\s*(?:通过|需要修改|拒绝|Approved|Changes Requested|Rejected)\\s*$',
118
121
  '^- \\*\\*(?:审查基线提交|Review Baseline Commit)\\*\\*[::]\\s*\\S',
119
122
  '^- \\*\\*(?:审查差异基线|Reviewed Diff Base)\\*\\*[::]\\s*[0-9a-f]{40,64}\\s*$',
120
123
  '^- \\*\\*(?:审查差异指纹|Reviewed Diff Fingerprint)\\*\\*[::]\\s*sha256:[0-9a-f]{64}\\s*$',
121
124
  '^- \\*\\*(?:审查快照树|Reviewed Snapshot Tree)\\*\\*[::]\\s*[0-9a-f]{40,64}\\s*$'
122
- ] }
125
+ ], stateCheckPatterns: [TASK_SNAPSHOT_PATTERN] }
123
126
  ];
124
127
 
125
128
  function getArtifactSchema(family: string): ArtifactSchema | null {
@@ -0,0 +1,53 @@
1
+ import type { ArtifactFamily } from './artifact-name.ts';
2
+
3
+ type SelectionArtifact = Readonly<{
4
+ family: ArtifactFamily;
5
+ round: number;
6
+ name: string;
7
+ }>;
8
+
9
+ type ArtifactDisposition = 'create' | 'resume' | 'reuse';
10
+
11
+ type ArtifactSelection = Readonly<{
12
+ disposition: ArtifactDisposition;
13
+ reasonCode: string;
14
+ artifact: SelectionArtifact;
15
+ writeRequired: boolean;
16
+ }>;
17
+
18
+ type ArtifactSelectionRequest = Readonly<{
19
+ next: SelectionArtifact;
20
+ latest: SelectionArtifact | null;
21
+ open: boolean;
22
+ hasChangeEvidence: boolean;
23
+ }>;
24
+
25
+ function selectArtifactDisposition(request: ArtifactSelectionRequest): ArtifactSelection {
26
+ if (request.open) {
27
+ return {
28
+ disposition: 'resume', reasonCode: 'open-round', artifact: request.next,
29
+ writeRequired: true
30
+ };
31
+ }
32
+ if (!request.latest) {
33
+ return {
34
+ disposition: 'create',
35
+ reasonCode: request.next.round === 1 ? 'no-history' : 'invalidated-history',
36
+ artifact: request.next, writeRequired: true
37
+ };
38
+ }
39
+ const reasonCode = request.hasChangeEvidence ? 'change-evidence' : 'history-matched';
40
+ const reuse = reasonCode === 'history-matched';
41
+ return {
42
+ disposition: reuse ? 'reuse' : 'create',
43
+ reasonCode,
44
+ artifact: reuse ? request.latest : request.next,
45
+ writeRequired: !reuse
46
+ };
47
+ }
48
+
49
+ export { selectArtifactDisposition };
50
+ export type {
51
+ ArtifactDisposition,
52
+ ArtifactSelection,
53
+ };
@@ -4,16 +4,19 @@ import path from 'node:path';
4
4
  import { parseArtifactName } from './artifact-name.ts';
5
5
  import type { InvalidationDocument } from './invalidation.ts';
6
6
  import { invalidationBlocks, isArtifactInvalidated, parseInvalidationDocument } from './invalidation.ts';
7
- import { receiptForOutput } from './artifact-receipts.ts';
7
+ import { parseArtifactReceipts, receiptForOutput } from './artifact-receipts.ts';
8
8
  import { parseTypedTaskFrontmatter } from './frontmatter.ts';
9
9
  import { parseLedgerDocument, summarizeLedgerStage, validateLedgerRows } from './ledger.ts';
10
10
  import { parseReviewSummary, resolveCanonicalVerdict } from './review-artifacts.ts';
11
11
  import { parseReworkIntentDocument } from './rework-intent.ts';
12
- import type { ReworkIntent } from './rework-intent.ts';
12
+ import type { ReworkIntent, ReworkTarget } from './rework-intent.ts';
13
13
  import { sha256File } from './artifact-receipts.ts';
14
14
  import { hasOpenLifecycleExecution } from './activity-log.ts';
15
15
  import { parseQualificationAudit, parseTaskQualification } from './qualification-audit.ts';
16
16
  import type { QualificationAudit, TaskQualification } from './qualification-audit.ts';
17
+ import { parseLifecyclePathDecision, pathIncludes } from './lifecycle-path.ts';
18
+ import type { LifecyclePathState } from './lifecycle-path.ts';
19
+ import { parseImplementationInputs } from './implementation-inputs.ts';
17
20
 
18
21
  const ARTIFACT_AUDIT_FAMILIES = new Set(['analysis', 'review-analysis', 'plan', 'review-plan', 'code', 'review-code']);
19
22
 
@@ -49,6 +52,9 @@ type LifecycleFacts = {
49
52
  recommendedAction?: LifecycleAction | null;
50
53
  qualificationStale?: boolean;
51
54
  qualificationStaleArtifacts?: readonly string[];
55
+ pathState?: LifecyclePathState;
56
+ reworkClassificationRequired?: readonly ('analysis' | 'plan' | 'code')[];
57
+ resolvedHumanDecisions?: Partial<Record<'analysis' | 'plan' | 'code', 'review' | 'implementation'>>;
52
58
  };
53
59
  type CapabilityResult = {
54
60
  allowed: boolean;
@@ -80,6 +86,11 @@ function hasArtifact(facts: LifecycleFacts, action: LifecycleAction): boolean {
80
86
  return (facts.artifacts[action]?.length ?? 0) > 0;
81
87
  }
82
88
 
89
+ function effectiveReworkTarget(target: ReworkTarget, pathState: LifecyclePathState | undefined): ReworkTarget {
90
+ if (target === 'plan' && pathState?.status === 'valid' && !pathIncludes(pathState, 'plan')) return 'analysis';
91
+ return target;
92
+ }
93
+
83
94
  function canStart(action: LifecycleAction, facts: LifecycleFacts, trigger: ExplicitTrigger): CapabilityResult {
84
95
  if (!trigger || trigger.requestedAction !== action) return deny('TRIGGER_ACTION_MISMATCH', `requested=${trigger?.requestedAction ?? 'missing'}`);
85
96
  if (!trigger.requestId || !trigger.requestId.trim()) return deny('TRIGGER_REQUEST_ID_REQUIRED');
@@ -93,11 +104,32 @@ function canStart(action: LifecycleAction, facts: LifecycleFacts, trigger: Expli
93
104
  }
94
105
  if (facts.taskState !== 'active') return deny('TASK_NOT_ACTIVE', `state=${facts.taskState}`);
95
106
  if (invalidationBlocks(facts.invalidation)) return deny('INVALIDATION_INCOMPLETE');
96
- if (facts.qualificationStale && facts.recommendedAction !== action) {
97
- return deny('QUALIFICATION_STALE', ...(facts.qualificationStaleArtifacts ?? ['qualification audit is stale']));
98
- }
99
107
  if (facts.executionBusy) return deny('EXECUTION_BUSY');
100
108
 
109
+ const pendingIntent = (facts.reworkIntents ?? []).find((intent) => intent.status === 'pending');
110
+ if (pendingIntent) {
111
+ const target = effectiveReworkTarget(pendingIntent.target, facts.pathState);
112
+ const requirementRestart = action === 'analysis' && trigger.reasonCode === 'new-requirement';
113
+ if (!requirementRestart && (target === 'pause' || target !== action)) {
114
+ return deny(target === 'pause' ? 'REWORK_PAUSED' : 'REWORK_INTENT_TARGET_MISMATCH', pendingIntent.intentId);
115
+ }
116
+ }
117
+
118
+ if (facts.pathState && facts.pathState.status !== 'valid') {
119
+ return action === 'analysis'
120
+ ? allow(`lifecycle-path-${facts.pathState.status}`)
121
+ : deny('LIFECYCLE_PATH_INVALID', facts.pathState.message);
122
+ }
123
+ if (facts.pathState?.status === 'valid'
124
+ && ['analysis', 'review-analysis', 'plan', 'review-plan', 'code', 'review-code'].includes(action)
125
+ && !pathIncludes(facts.pathState, action as never)) {
126
+ return deny('ARTIFACT_STAGE_NOT_SELECTED', `path=${facts.pathState.decision.path}`, `stage=${action}`);
127
+ }
128
+ if (facts.reworkClassificationRequired?.includes(action as 'analysis' | 'plan' | 'code')
129
+ && !(facts.reworkIntents ?? []).some((intent) => intent.status === 'pending')) {
130
+ return deny('REWORK_CLASSIFICATION_REQUIRED', `stage=${action}`);
131
+ }
132
+
101
133
  const implicit = trigger.explicitRequest === false;
102
134
  if (action === 'analysis') {
103
135
  if (!implicit || hasArtifact(facts, 'analysis') || facts.recommendedAction === 'analysis') return allow('analysis-capability');
@@ -107,6 +139,9 @@ function canStart(action: LifecycleAction, facts: LifecycleFacts, trigger: Expli
107
139
  return hasArtifact(facts, 'analysis') ? allow('analysis-artifact') : deny('ANALYSIS_ARTIFACT_REQUIRED');
108
140
  }
109
141
  if (action === 'plan') {
142
+ if (facts.pathState?.status === 'valid' && facts.pathState.decision.path === 'standard') {
143
+ return hasArtifact(facts, 'analysis') ? allow('analysis-artifact') : deny('ANALYSIS_ARTIFACT_REQUIRED');
144
+ }
110
145
  if (!hasArtifact(facts, 'review-analysis')) return deny('ANALYSIS_REVIEW_REQUIRED');
111
146
  if (!reviewMatchesLatest(facts, 'analysis', 'review-analysis')) return deny('ANALYSIS_REVIEW_NOT_LATEST');
112
147
  if (facts.reviews['review-analysis'] !== 'approved') return deny('ANALYSIS_REVIEW_NOT_APPROVED');
@@ -118,11 +153,22 @@ function canStart(action: LifecycleAction, facts: LifecycleFacts, trigger: Expli
118
153
  }
119
154
  if (action === 'code') {
120
155
  if (trigger.implementationInput) {
156
+ if (facts.resolvedHumanDecisions?.code === 'implementation') return allow('human-decision-implementation-input');
121
157
  if (!hasArtifact(facts, 'review-code')) return deny('CODE_REVIEW_REQUIRED');
122
158
  if (!reviewMatchesLatest(facts, 'code', 'review-code')) return deny('CODE_REVIEW_NOT_LATEST');
123
159
  if (facts.reviews['review-code'] !== 'approved') return deny('CODE_REVIEW_NOT_APPROVED');
124
160
  return allow('implementation-input');
125
161
  }
162
+ if (facts.pathState?.status === 'valid' && facts.pathState.decision.path === 'streamlined') {
163
+ if (!hasArtifact(facts, 'analysis')) return deny('ANALYSIS_ARTIFACT_REQUIRED');
164
+ if (facts.unresolvedLedger.analysis > 0) return deny('ANALYSIS_LEDGER_BLOCKED', `unresolved=${facts.unresolvedLedger.analysis}`);
165
+ return allow('analysis-path-approved');
166
+ }
167
+ if (facts.pathState?.status === 'valid' && facts.pathState.decision.path === 'standard') {
168
+ if (!hasArtifact(facts, 'plan')) return deny('PLAN_ARTIFACT_REQUIRED');
169
+ if (facts.unresolvedLedger.plan > 0) return deny('PLAN_LEDGER_BLOCKED', `unresolved=${facts.unresolvedLedger.plan}`);
170
+ return allow('plan-path-approved');
171
+ }
126
172
  if (!hasArtifact(facts, 'review-plan')) return deny('PLAN_REVIEW_REQUIRED');
127
173
  if (!reviewMatchesLatest(facts, 'plan', 'review-plan')) return deny('PLAN_REVIEW_NOT_LATEST');
128
174
  if (facts.reviews['review-plan'] !== 'approved') return deny('PLAN_REVIEW_NOT_APPROVED');
@@ -147,14 +193,6 @@ function latestArtifact(names: readonly string[]): string | null {
147
193
  .sort((left, right) => right.round - left.round || left.name.localeCompare(right.name))[0]?.name ?? null;
148
194
  }
149
195
 
150
- function reviewedInputName(content: string, expectedFamily: 'analysis' | 'plan' | 'code'): string | null {
151
- const header = content.split(/\r?\n/).findIndex((line) => /\*\*(?:审查输入|Review Input)\*\*[::]/.test(line));
152
- if (header < 0) return null;
153
- const referenceBlock = content.split(/\r?\n/).slice(header, header + 12).join('\n');
154
- const expected = new RegExp('`' + expectedFamily + '(?:-r[2-9]|-r[1-9]\\d+)?\\.md`');
155
- return expected.exec(referenceBlock)?.[0].slice(1, -1) ?? null;
156
- }
157
-
158
196
  function reviewMatchesLatest(facts: LifecycleFacts, source: 'analysis' | 'plan' | 'code', review: 'review-analysis' | 'review-plan' | 'review-code'): boolean {
159
197
  const latestSource = latestArtifact(facts.artifacts[source] ?? []);
160
198
  return Boolean(latestSource && facts.reviewedInputs?.[review] === latestSource);
@@ -172,31 +210,53 @@ function qualificationCandidateSnapshotMatches(
172
210
  });
173
211
  }
174
212
 
175
- const QUALIFICATION_RECOVERY_ORDER: ReadonlyArray<{ action: LifecycleAction; pattern: RegExp }> = [
176
- { action: 'analysis', pattern: /^analysis(?:-r\d+)?\.md$/ },
177
- { action: 'review-analysis', pattern: /^review-analysis(?:-r\d+)?\.md$/ },
178
- { action: 'plan', pattern: /^plan(?:-r\d+)?\.md$/ },
179
- { action: 'review-plan', pattern: /^review-plan(?:-r\d+)?\.md$/ },
180
- { action: 'code', pattern: /^code(?:-r\d+)?\.md$/ },
181
- { action: 'review-code', pattern: /^review-code(?:-r\d+)?\.md$/ }
182
- ];
183
-
184
- function qualificationRecoveryAction(facts: LifecycleFacts): LifecycleAction | null {
185
- const stale = facts.qualificationStaleArtifacts ?? [];
186
- return QUALIFICATION_RECOVERY_ORDER.find(({ pattern }) => stale.some((name) => pattern.test(name)))?.action ?? null;
187
- }
188
-
189
213
  function recommendNext(facts: LifecycleFacts): LifecycleRecommendation {
190
- if (facts.qualificationStale) {
191
- const action = qualificationRecoveryAction(facts);
192
- return {
193
- action,
194
- reasonCode: 'QUALIFICATION_RECOVERY_REQUIRED',
195
- evidence: facts.qualificationStaleArtifacts ?? ['qualification audit is stale']
196
- };
214
+ if ((facts.reworkClassificationRequired?.length ?? 0) > 0
215
+ && !(facts.reworkIntents ?? []).some((intent) => intent.status === 'pending')) {
216
+ return { action: null, reasonCode: 'REWORK_CLASSIFICATION_REQUIRED', evidence: facts.reworkClassificationRequired ?? [] };
197
217
  }
198
218
  const pendingIntent = (facts.reworkIntents ?? []).find((intent) => intent.status === 'pending');
199
- if (pendingIntent) return { action: pendingIntent.target, reasonCode: 'REWORK_INTENT_PENDING', evidence: [pendingIntent.intentId, pendingIntent.findingId] };
219
+ if (pendingIntent) {
220
+ const target = effectiveReworkTarget(pendingIntent.target, facts.pathState);
221
+ return { action: target === 'pause' ? null : target, reasonCode: target === 'pause' ? 'REWORK_PAUSED' : 'REWORK_INTENT_PENDING', evidence: [pendingIntent.intentId, pendingIntent.findingId] };
222
+ }
223
+ if (facts.pathState && facts.pathState.status !== 'valid') {
224
+ return { action: 'analysis', reasonCode: facts.pathState.status === 'missing' ? 'LIFECYCLE_PATH_MISSING' : 'LIFECYCLE_PATH_INVALID', evidence: [facts.pathState.message] };
225
+ }
226
+ if (facts.pathState?.status === 'valid') {
227
+ for (const stage of facts.pathState.decision.stages) {
228
+ if (stage === 'analysis' && !hasArtifact(facts, stage)) return { action: stage, reasonCode: 'ANALYSIS_ARTIFACT_MISSING', evidence: ['analysis artifact is absent'] };
229
+ if (stage === 'review-analysis' && (!reviewMatchesLatest(facts, 'analysis', stage) || facts.reviews[stage] !== 'approved')) {
230
+ if (facts.reviews[stage] === 'changes-requested' && facts.resolvedHumanDecisions?.analysis === 'review') {
231
+ return { action: stage, reasonCode: 'HUMAN_DECISION_REVIEW_REQUIRED', evidence: ['analysis decision was resolved'] };
232
+ }
233
+ return facts.reviews[stage] === 'changes-requested'
234
+ ? { action: 'analysis', reasonCode: 'ANALYSIS_REWORK_REQUIRED', evidence: ['analysis review is not approved'] }
235
+ : { action: stage, reasonCode: 'ANALYSIS_REVIEW_MISSING', evidence: ['analysis review does not bind the latest analysis artifact'] };
236
+ }
237
+ if (stage === 'plan' && !hasArtifact(facts, stage)) return { action: stage, reasonCode: 'PLAN_ARTIFACT_MISSING', evidence: ['plan artifact is absent'] };
238
+ if (stage === 'review-plan' && (!reviewMatchesLatest(facts, 'plan', stage) || facts.reviews[stage] !== 'approved')) {
239
+ if (facts.reviews[stage] === 'changes-requested' && facts.resolvedHumanDecisions?.plan === 'review') {
240
+ return { action: stage, reasonCode: 'HUMAN_DECISION_REVIEW_REQUIRED', evidence: ['plan decision was resolved'] };
241
+ }
242
+ return facts.reviews[stage] === 'changes-requested'
243
+ ? { action: 'plan', reasonCode: 'PLAN_REWORK_REQUIRED', evidence: ['plan review is not approved'] }
244
+ : { action: stage, reasonCode: 'PLAN_REVIEW_MISSING', evidence: ['plan review does not bind the latest plan artifact'] };
245
+ }
246
+ if (stage === 'code' && !hasArtifact(facts, stage)) return { action: stage, reasonCode: 'CODE_ARTIFACT_MISSING', evidence: ['code artifact is absent'] };
247
+ if (stage === 'review-code' && (!reviewMatchesLatest(facts, 'code', stage) || facts.reviews[stage] !== 'approved')) {
248
+ if (facts.reviews[stage] === 'changes-requested' && facts.resolvedHumanDecisions?.code) {
249
+ return facts.resolvedHumanDecisions.code === 'implementation'
250
+ ? { action: 'code', reasonCode: 'HUMAN_DECISION_IMPLEMENTATION_REQUIRED', evidence: ['code decision requires implementation'] }
251
+ : { action: stage, reasonCode: 'HUMAN_DECISION_REVIEW_REQUIRED', evidence: ['code decision requires no implementation'] };
252
+ }
253
+ return facts.reviews[stage] === 'changes-requested'
254
+ ? { action: 'code', reasonCode: 'CODE_REWORK_REQUIRED', evidence: ['code review is not approved'] }
255
+ : { action: stage, reasonCode: 'CODE_REVIEW_MISSING', evidence: ['code review does not bind the latest code artifact'] };
256
+ }
257
+ }
258
+ return { action: null, reasonCode: 'LIFECYCLE_REVIEWED', evidence: ['selected lifecycle path is complete'] };
259
+ }
200
260
  if (!hasArtifact(facts, 'analysis')) return { action: 'analysis', reasonCode: 'ANALYSIS_ARTIFACT_MISSING', evidence: ['analysis artifact is absent'] };
201
261
  if (!hasArtifact(facts, 'review-analysis') || !reviewMatchesLatest(facts, 'analysis', 'review-analysis')) {
202
262
  return { action: 'review-analysis', reasonCode: 'ANALYSIS_REVIEW_MISSING', evidence: ['analysis review does not bind the latest analysis artifact'] };
@@ -247,8 +307,10 @@ function buildLifecycleFacts(taskDir: string, content: string, taskState = 'acti
247
307
  const family = familyFor(name);
248
308
  return !family || !isArtifactInvalidated(invalidation.document, family, name);
249
309
  });
310
+ const allArtifactHashes: Record<string, string> = {};
311
+ for (const name of files) allArtifactHashes[name] = sha256File(path.join(taskDir, name));
250
312
  const artifactHashes: Record<string, string> = {};
251
- for (const name of activeFiles) artifactHashes[name] = sha256File(path.join(taskDir, name));
313
+ for (const name of activeFiles) artifactHashes[name] = allArtifactHashes[name]!;
252
314
  const artifacts: Partial<Record<LifecycleAction, readonly string[]>> = Object.fromEntries(
253
315
  artifactFamilies.map((family) => [
254
316
  family, activeFiles.filter((name) => familyFor(name) === family)
@@ -288,18 +350,67 @@ function buildLifecycleFacts(taskDir: string, content: string, taskState = 'acti
288
350
  if (!latest) continue;
289
351
  const reviewContent = fs.readFileSync(path.join(taskDir, latest), 'utf8');
290
352
  const expectedFamily = family === 'review-analysis' ? 'analysis' : family === 'review-plan' ? 'plan' : 'code';
291
- const input = reviewedInputName(reviewContent, expectedFamily);
292
- const receipt = input ? receiptForOutput(content, latest) : null;
293
- if (input && receipt?.input === input && artifactHashes[input] === receipt.inputSha256) reviewedInputs[family] = input;
353
+ const receipt = receiptForOutput(content, latest);
354
+ const input = receipt?.input;
355
+ if (
356
+ input
357
+ && parseArtifactName(input)?.family === expectedFamily
358
+ && (artifacts[expectedFamily] ?? []).includes(input)
359
+ && artifactHashes[input] === receipt.inputSha256
360
+ ) reviewedInputs[family] = input;
294
361
  const parsed = parseReviewSummary(reviewContent);
295
362
  if (!parsed.ok) continue;
296
363
  const verdict = resolveCanonicalVerdict(parsed.summary);
297
364
  if (verdict.ok) reviews[family] = verdict.verdict === 'Approved' ? 'approved' : verdict.verdict === 'Changes Requested' ? 'changes-requested' : 'rejected';
298
365
  }
366
+ const reworkClassificationRequired: Array<'analysis' | 'plan' | 'code'> = [];
367
+ const receipts = parseArtifactReceipts(content).rows;
368
+ for (const [reviewFamily, stage] of [['review-analysis', 'analysis'], ['review-plan', 'plan'], ['review-code', 'code']] as const) {
369
+ const cycles = receipts.filter((receipt) => receipt.event === `${reviewFamily}.completed` && files.includes(receipt.output)
370
+ && allArtifactHashes[receipt.input] === receipt.inputSha256).map((receipt) => {
371
+ const parsed = parseReviewSummary(fs.readFileSync(path.join(taskDir, receipt.output), 'utf8'));
372
+ const verdict = parsed.ok ? resolveCanonicalVerdict(parsed.summary) : null;
373
+ return {
374
+ input: receipt.input,
375
+ output: receipt.output,
376
+ outputRound: parseArtifactName(receipt.output)?.round ?? 0,
377
+ inputRound: parseArtifactName(receipt.input)?.round ?? 0,
378
+ verdict: verdict?.ok ? verdict.verdict : null
379
+ };
380
+ }).sort((left, right) => left.inputRound - right.inputRound || left.outputRound - right.outputRound || left.output.localeCompare(right.output));
381
+ const latest = cycles.at(-1);
382
+ const trailingInputRounds = new Set<number>();
383
+ for (let index = cycles.length - 1; index >= 0 && cycles[index]!.verdict === 'Changes Requested'; index -= 1) {
384
+ trailingInputRounds.add(cycles[index]!.inputRound);
385
+ }
386
+ const handled = latest && (rework.intents ?? []).some((intent) => intent.sourceArtifact === latest.output
387
+ && intent.sourceSha256 === allArtifactHashes[latest.output]);
388
+ const latestActiveReview = latestArtifact(artifacts[reviewFamily] ?? []);
389
+ const latestActiveInput = latestArtifact(artifacts[stage] ?? []);
390
+ const latestCycleIsActive = latest?.output === latestActiveReview && latest.input === latestActiveInput;
391
+ if (latestCycleIsActive && latest.verdict === 'Changes Requested' && trailingInputRounds.size >= 2 && !handled) reworkClassificationRequired.push(stage);
392
+ }
299
393
  const unresolvedLedger = { analysis: 0, plan: 0, code: 0 };
300
394
  if (ledger.present) {
301
395
  for (const stage of ['analysis', 'plan', 'code'] as const) unresolvedLedger[stage] = summarizeLedgerStage(ledger.rows, stage).unresolved.length;
302
396
  }
397
+ const latestAnalysis = latestArtifact(artifacts.analysis ?? []);
398
+ const pathState = latestAnalysis
399
+ ? parseLifecyclePathDecision(fs.readFileSync(path.join(taskDir, latestAnalysis), 'utf8'), latestAnalysis, artifactHashes[latestAnalysis] ?? '')
400
+ : { status: 'missing' as const, decision: null, message: 'analysis artifact is missing' };
401
+ const resolvedHumanDecisions: NonNullable<LifecycleFacts['resolvedHumanDecisions']> = {};
402
+ const implementationInputs = parseImplementationInputs(content).rows;
403
+ for (const stage of ['analysis', 'plan', 'code'] as const) {
404
+ const reviewFamily = `review-${stage}` as 'review-analysis' | 'review-plan' | 'review-code';
405
+ const latestReview = latestArtifact(artifacts[reviewFamily] ?? []);
406
+ if (!latestReview || reviews[reviewFamily] !== 'changes-requested') continue;
407
+ const latestHash = artifactHashes[latestReview];
408
+ const decision = rework.intents.find((intent) => intent.status === 'consumed'
409
+ && intent.classification === 'human-decision' && intent.sourceArtifact === latestReview && intent.sourceSha256 === latestHash);
410
+ if (!decision) continue;
411
+ const implementationInput = implementationInputs.find((input) => input.ledgerId === decision.findingId);
412
+ resolvedHumanDecisions[stage] = stage === 'code' && implementationInput?.status === 'pending' ? 'implementation' : 'review';
413
+ }
303
414
  const facts: LifecycleFacts = {
304
415
  taskState, currentStep: String(metadata.current_step ?? ''), artifacts, reviews,
305
416
  staleArtifacts, reviewedInputs, artifactHashes,
@@ -309,6 +420,9 @@ function buildLifecycleFacts(taskDir: string, content: string, taskState = 'acti
309
420
  recommendedAction: null,
310
421
  qualificationStale: qualificationStaleArtifacts.length > 0,
311
422
  qualificationStaleArtifacts
423
+ , pathState
424
+ , reworkClassificationRequired
425
+ , resolvedHumanDecisions
312
426
  };
313
427
  facts.recommendedAction = recommendNext(facts).action;
314
428
  return { ok: true, facts };
@@ -317,5 +431,5 @@ function buildLifecycleFacts(taskDir: string, content: string, taskState = 'acti
317
431
  }
318
432
  }
319
433
 
320
- export { buildLifecycleFacts, canStart, recommendNext };
434
+ export { buildLifecycleFacts, canStart, effectiveReworkTarget, recommendNext };
321
435
  export type { CapabilityResult, ExplicitTrigger, LifecycleAction, LifecycleFacts, LifecycleFactsResult, LifecycleRecommendation, TriggerInitiator, TriggerReason };
@@ -386,6 +386,8 @@ type StatusModel = {
386
386
  orchestration: OrchestrationInfo;
387
387
  invalidation?: { status: string; processed: number; total: number; pendingIntents: number };
388
388
  recommendation?: string;
389
+ recommendationReason?: string;
390
+ lifecyclePath?: string;
389
391
  git: GitInfo;
390
392
  };
391
393
 
@@ -444,7 +446,11 @@ function renderStatus(model: StatusModel): string[] {
444
446
  ])
445
447
  );
446
448
 
447
- lines.push('', 'Recommendation', ...renderPairs([['action', model.recommendation ?? DASH]]));
449
+ lines.push('', 'Recommendation', ...renderPairs([
450
+ ['path', model.lifecyclePath ?? DASH],
451
+ ['action', model.recommendation ?? DASH],
452
+ ['reason', model.recommendationReason ?? DASH]
453
+ ]));
448
454
 
449
455
  lines.push(
450
456
  '',
@@ -546,7 +552,8 @@ function buildFromResolved(input: BuildStatusModelInput): StatusModel {
546
552
  pendingIntents
547
553
  };
548
554
  const facts = buildLifecycleFacts(input.taskDir, content, fm.status ?? 'active');
549
- const recommendation = facts.ok ? (recommendNext(facts.facts).action ?? DASH) : DASH;
555
+ const next = facts.ok ? recommendNext(facts.facts) : null;
556
+ const recommendation = next?.action ?? DASH;
550
557
 
551
558
  return {
552
559
  taskId: input.taskId,
@@ -560,6 +567,8 @@ function buildFromResolved(input: BuildStatusModelInput): StatusModel {
560
567
  orchestration: collectOrchestration(input.taskDir),
561
568
  invalidation: invalidationInfo,
562
569
  recommendation,
570
+ recommendationReason: next?.reasonCode ?? 'TASK_CAPABILITY_FACTS_INVALID',
571
+ lifecyclePath: facts.ok && facts.facts.pathState?.status === 'valid' ? facts.facts.pathState.decision.path : facts.ok ? facts.facts.pathState?.status ?? DASH : DASH,
563
572
  git: collectGit(fm.branch ?? '', run)
564
573
  };
565
574
  }