@fitlab-ai/agent-infra 0.11.2 → 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 (162) hide show
  1. package/README.md +17 -8
  2. package/README.zh-CN.md +17 -8
  3. package/dist/bin/internal-cli.js +7 -2
  4. package/dist/lib/agent-clients/adapter.js +3 -0
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/recovery.js +234 -0
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +8 -35
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +1 -34
  8. package/dist/lib/agent-clients/adapters/codex.js +10 -0
  9. package/dist/lib/internal/cli-route-inventory.js +1 -1
  10. package/dist/lib/internal/manual-validation.js +22 -27
  11. package/dist/lib/internal/sandbox-control.js +1 -43
  12. package/dist/lib/internal/task-artifact.js +1 -1
  13. package/dist/lib/internal/task-finalization.js +41 -1
  14. package/dist/lib/internal/task-ledger.js +13 -5
  15. package/dist/lib/internal/task-lifecycle.js +1 -1
  16. package/dist/lib/internal/task-operation-registry.js +7 -2
  17. package/dist/lib/platform/completion-backfill.js +63 -17
  18. package/dist/lib/platform/issue-comments.js +2 -31
  19. package/dist/lib/platform/pr-summary.js +7 -2
  20. package/dist/lib/sandbox/control/client.js +3 -44
  21. package/dist/lib/sandbox/control/executor.js +14 -79
  22. package/dist/lib/sandbox/control/protocol.js +5 -64
  23. package/dist/lib/sandbox/control/server.js +49 -343
  24. package/dist/lib/sandbox/control/state.js +1 -7
  25. package/dist/lib/sandbox/control/task-view.js +1 -2
  26. package/dist/lib/sandbox/task-cleanup.js +13 -20
  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 +1 -26
  30. package/dist/lib/task/artifact-receipts.js +4 -1
  31. package/dist/lib/task/artifact-schema.js +1 -0
  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 +37 -38
  36. package/dist/lib/task/control-recovery.js +3 -42
  37. package/dist/lib/task/decision-intents.js +12 -0
  38. package/dist/lib/task/delegation-receipts.js +2 -2
  39. package/dist/lib/task/delivery.js +1 -43
  40. package/dist/lib/task/events.js +80 -64
  41. package/dist/lib/task/finalization-artifacts.js +1 -3
  42. package/dist/lib/task/finalization-state.js +1 -1
  43. package/dist/lib/task/finalization.js +319 -234
  44. package/dist/lib/task/ledger-intents.js +157 -15
  45. package/dist/lib/task/lifecycle-path.js +75 -0
  46. package/dist/lib/task/lifecycle.js +11 -1
  47. package/dist/lib/task/local-artifact-finalization.js +1 -20
  48. package/dist/lib/task/manual-validation-receipt.js +8 -3
  49. package/dist/lib/task/orchestration.js +29 -13
  50. package/dist/lib/task/review-finalization.js +3 -16
  51. package/dist/lib/task/rework-intent.js +70 -4
  52. package/dist/lib/task/verification-engine.js +20 -30
  53. package/dist/lib/task/verification.js +1 -0
  54. package/dist/lib/task/workflow-warning-intents.js +21 -6
  55. package/dist/lib/task/workspace-snapshot.js +7 -2
  56. package/dist/lifecycle-build-manifest.json +3 -3
  57. package/lib/agent-clients/adapter.ts +46 -0
  58. package/lib/agent-clients/adapters/codex-lifecycle/recovery.ts +297 -0
  59. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +13 -33
  60. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +1 -44
  61. package/lib/agent-clients/adapters/codex.ts +10 -0
  62. package/lib/internal/cli-route-inventory.ts +1 -1
  63. package/lib/internal/manual-validation.ts +22 -26
  64. package/lib/internal/sandbox-control.ts +0 -42
  65. package/lib/internal/task-artifact.ts +1 -1
  66. package/lib/internal/task-finalization.ts +40 -1
  67. package/lib/internal/task-ledger.ts +12 -5
  68. package/lib/internal/task-lifecycle.ts +2 -2
  69. package/lib/internal/task-operation-registry.ts +8 -4
  70. package/lib/platform/completion-backfill.ts +78 -16
  71. package/lib/platform/issue-comments.ts +1 -35
  72. package/lib/platform/pr-summary.ts +10 -2
  73. package/lib/sandbox/control/client.ts +5 -68
  74. package/lib/sandbox/control/executor.ts +14 -81
  75. package/lib/sandbox/control/protocol.ts +7 -83
  76. package/lib/sandbox/control/server.ts +47 -362
  77. package/lib/sandbox/control/state.ts +1 -9
  78. package/lib/sandbox/control/task-view.ts +1 -2
  79. package/lib/sandbox/task-cleanup.ts +12 -20
  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 +1 -26
  83. package/lib/task/artifact-receipts.ts +4 -1
  84. package/lib/task/artifact-schema.ts +1 -0
  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 +41 -48
  89. package/lib/task/control-recovery.ts +3 -43
  90. package/lib/task/decision-intents.ts +9 -0
  91. package/lib/task/delegation-receipts.ts +2 -2
  92. package/lib/task/delivery.ts +1 -40
  93. package/lib/task/events.ts +82 -64
  94. package/lib/task/finalization-artifacts.ts +1 -3
  95. package/lib/task/finalization-state.ts +1 -1
  96. package/lib/task/finalization.ts +310 -216
  97. package/lib/task/ledger-intents.ts +155 -15
  98. package/lib/task/lifecycle-path.ts +98 -0
  99. package/lib/task/lifecycle.ts +15 -2
  100. package/lib/task/local-artifact-finalization.ts +2 -25
  101. package/lib/task/manual-validation-receipt.ts +8 -2
  102. package/lib/task/orchestration.ts +56 -27
  103. package/lib/task/review-finalization.ts +3 -16
  104. package/lib/task/rework-intent.ts +75 -5
  105. package/lib/task/verification-engine.ts +19 -30
  106. package/lib/task/verification.ts +2 -1
  107. package/lib/task/workflow-warning-intents.ts +18 -6
  108. package/lib/task/workspace-snapshot.ts +3 -2
  109. package/package.json +1 -1
  110. package/templates/.agents/rules/lifecycle-orchestration.en.md +19 -5
  111. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +19 -5
  112. package/templates/.agents/rules/review-handshake.en.md +4 -4
  113. package/templates/.agents/rules/review-handshake.zh-CN.md +4 -4
  114. package/templates/.agents/skills/analyze-task/SKILL.en.md +11 -4
  115. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +11 -4
  116. package/templates/.agents/skills/code-task/SKILL.en.md +14 -8
  117. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +18 -13
  118. package/templates/.agents/skills/code-task/reference/code-rules.en.md +2 -2
  119. package/templates/.agents/skills/code-task/reference/code-rules.zh-CN.md +2 -2
  120. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +4 -4
  121. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +4 -4
  122. package/templates/.agents/skills/code-task/reference/report-template.en.md +1 -1
  123. package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +1 -1
  124. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +2 -2
  125. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +2 -2
  126. package/templates/.agents/skills/complete-task/SKILL.en.md +7 -8
  127. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +7 -8
  128. package/templates/.agents/skills/plan-task/SKILL.en.md +13 -10
  129. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +13 -10
  130. package/templates/.agents/skills/review-analysis/SKILL.en.md +5 -1
  131. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +5 -1
  132. package/templates/.agents/skills/review-code/SKILL.en.md +5 -1
  133. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +5 -1
  134. package/templates/.agents/skills/review-plan/SKILL.en.md +5 -1
  135. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +5 -1
  136. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +2 -2
  137. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +2 -2
  138. package/templates/.agents/skills/run-task/SKILL.en.md +9 -8
  139. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +9 -8
  140. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +18 -2
  141. package/templates/.agents/templates/task.en.md +2 -2
  142. package/templates/.agents/templates/task.zh-CN.md +2 -2
  143. package/templates/.agents/workflows/bug-fix.en.yaml +11 -6
  144. package/templates/.agents/workflows/bug-fix.zh-CN.yaml +11 -6
  145. package/templates/.agents/workflows/feature-development.en.yaml +11 -6
  146. package/templates/.agents/workflows/feature-development.zh-CN.yaml +11 -6
  147. package/templates/.agents/workflows/refactoring.en.yaml +11 -6
  148. package/templates/.agents/workflows/refactoring.zh-CN.yaml +11 -6
  149. package/dist/lib/sandbox/control/finalization-response.js +0 -35
  150. package/dist/lib/sandbox/control/task-workflow.js +0 -74
  151. package/dist/lib/sandbox/control/workflow-executor.js +0 -142
  152. package/dist/lib/task/lifecycle-recovery.js +0 -549
  153. package/dist/lib/task/recovery-warning.js +0 -19
  154. package/dist/lib/task/workflow-command.js +0 -21
  155. package/dist/lib/task/workflow-dispatch.js +0 -213
  156. package/lib/sandbox/control/finalization-response.ts +0 -37
  157. package/lib/sandbox/control/task-workflow.ts +0 -95
  158. package/lib/sandbox/control/workflow-executor.ts +0 -156
  159. package/lib/task/lifecycle-recovery.ts +0 -691
  160. package/lib/task/recovery-warning.ts +0 -21
  161. package/lib/task/workflow-command.ts +0 -22
  162. package/lib/task/workflow-dispatch.ts +0 -214
@@ -10,12 +10,6 @@ import {
10
10
  renderArtifactSkeleton
11
11
  } from './artifact-schema.ts';
12
12
  import { withTaskExecutionLock } from './task-execution-lock.ts';
13
- import {
14
- buildQualificationAudit,
15
- expectedQualificationRelations,
16
- parseTaskQualification,
17
- renderQualificationAudit
18
- } from './qualification-audit.ts';
19
13
  import type {
20
14
  ArtifactSchema,
21
15
  ArtifactSchemaFamily,
@@ -294,23 +288,6 @@ function artifactRound(family: ArtifactSchemaFamily, artifact: string): number |
294
288
  return identity?.family === family ? identity.round : null;
295
289
  }
296
290
 
297
- function qualificationAuditForInit(request: ArtifactInitRequest): { ok: true; content: string } | { ok: false; code: string; message: string } {
298
- let taskContent: string;
299
- try {
300
- taskContent = readStableFileSync(path.join(request.taskDir, 'task.md'), { maxBytes: 1024 * 1024 }).bytes.toString('utf8');
301
- } catch (error) {
302
- return { ok: false, code: 'ARTIFACT_INIT_TASK_READ_FAILED', message: error instanceof Error ? error.message : String(error) };
303
- }
304
- const qualification = parseTaskQualification(taskContent);
305
- if (!qualification.ok) return qualification;
306
- if (!qualification.qualification.present) return { ok: true, content: '' };
307
- const relations = expectedQualificationRelations(taskContent, request.family);
308
- if (!relations.ok) return relations;
309
- const audit = buildQualificationAudit(taskContent, { upstreamRelations: relations.relations ?? [] });
310
- if (!audit.ok) return audit;
311
- return { ok: true, content: `\n## 资格审计\n\n${renderQualificationAudit(audit.audit)}\n` };
312
- }
313
-
314
291
  function initializeArtifactSkeleton(request: ArtifactInitRequest): ArtifactFileResult {
315
292
  const schema = getArtifactSchema(request.family);
316
293
  if (!schema) return resultFailure('ARTIFACT_FAMILY_UNKNOWN', `unknown artifact family '${request.family}'`);
@@ -333,9 +310,7 @@ function initializeArtifactSkeleton(request: ArtifactInitRequest): ArtifactFileR
333
310
  artifact: request.artifact,
334
311
  ...(request.locale ? { locale: request.locale } : {})
335
312
  });
336
- const audit = qualificationAuditForInit(request);
337
- if (!audit.ok) return resultFailure(audit.code, audit.message);
338
- const content = `${skeleton.trimEnd()}${audit.content}`;
313
+ const content = skeleton.trimEnd();
339
314
  const tempPath = path.join(request.taskDir, `.${request.artifact}.init-${process.pid}-${Date.now()}.tmp`);
340
315
  try {
341
316
  fs.writeFileSync(tempPath, content, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
@@ -64,7 +64,10 @@ function validateReceiptShape(event: ArtifactReceiptEvent, output: string, input
64
64
  const inputIdentity = parseArtifactName(input);
65
65
  if (!outputIdentity || !inputIdentity) throw new ArtifactReceiptError(`receipt artifact identity is invalid: ${output} -> ${input}`);
66
66
  const shape = RECEIPT_SHAPES[event];
67
- if (outputIdentity.family !== shape.output || inputIdentity.family !== shape.input) {
67
+ const inputMatches = event === 'code.completed'
68
+ ? inputIdentity.family === 'analysis' || inputIdentity.family === 'plan'
69
+ : inputIdentity.family === shape.input;
70
+ if (outputIdentity.family !== shape.output || !inputMatches) {
68
71
  throw new ArtifactReceiptError(`receipt event '${event}' does not match ${output} -> ${input}`);
69
72
  }
70
73
  }
@@ -30,6 +30,7 @@ const LOCAL_SECTIONS = {
30
30
  ['impact', '影响评估', 'Impact Assessment'],
31
31
  ['risks', '技术风险', 'Technical Risks'],
32
32
  ['effort', '工作量和复杂度评估', 'Effort and Complexity Assessment'],
33
+ ['flow-decision', '流程裁定', 'Flow Decision'],
33
34
  ['state-check', '状态核对', 'State Check']
34
35
  ],
35
36
  plan: [
@@ -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
  }