@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
@@ -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
  }
@@ -26,6 +26,7 @@ import {
26
26
  import type { OrchestrationOptions, OrchestrationResult } from './orchestration.ts';
27
27
  import {
28
28
  applyTaskFinalization,
29
+ commitPreparedTaskFinalization,
29
30
  type TaskFinalizationRequest,
30
31
  type TaskFinalizationResult
31
32
  } from './finalization.ts';
@@ -860,7 +861,8 @@ export function dispatchTaskControlOperation(
860
861
  }
861
862
  if (operation.family === 'task-finalization') {
862
863
  operationTaskId(context, operation.request.taskRef);
863
- return applyTaskFinalization(operation.request, {
864
+ const finalize = context.source === 'sandbox-executor' ? commitPreparedTaskFinalization : applyTaskFinalization;
865
+ return finalize(operation.request, {
864
866
  repoRoot: context.repoRoot,
865
867
  ...(context.source === 'sandbox-executor' ? {
866
868
  controlBinding: { generation: context.generation, requestId: context.requestId }
@@ -17,6 +17,7 @@ import { extractSection, extractSubSection, findSectionHeading } from './section
17
17
  import { captureTaskWriteMetadata, writeTask } from './write.ts';
18
18
  import type { TaskMutation, TaskOperationSummary, TaskWriteOptions } from './write.ts';
19
19
  import { allowsManualOverride } from './guard-override.ts';
20
+ import { consumeHumanDecisionReworkIntents, parseReworkIntentDocument, reworkIntentMutation } from './rework-intent.ts';
20
21
 
21
22
  type HumanDecisionRequest = {
22
23
  taskRef?: string;
@@ -177,6 +178,14 @@ function applyHumanDecision(request: HumanDecisionRequest, options: TaskWriteOpt
177
178
  kind: 'section', aliases: ACTIVITY_ALIASES, heading: activity.heading,
178
179
  body: appendActivityEntry(activity, { time: metadata.timestamp, step: 'Human Decision', agent: 'human', note: `${row.id} decided → ${recordId}` })
179
180
  });
181
+ try {
182
+ const parsedRework = parseReworkIntentDocument(content);
183
+ if (!parsedRework.ok) return failed('DECISION_DOCUMENT_INVALID', parsedRework.message, resolved.taskId, row.id);
184
+ const consumed = consumeHumanDecisionReworkIntents(parsedRework.intents, row.id, metadata.timestamp);
185
+ if (consumed.changed) mutations.push(reworkIntentMutation(content, consumed.intents));
186
+ } catch (error) {
187
+ return failed('DECISION_DOCUMENT_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId, row.id);
188
+ }
180
189
  const writeResult = writeTask({ taskRef: resolved.taskId, expectedState: stateOverride ? resolved.state : 'active', mutations, dryRun: request.dryRun }, {
181
190
  ...options,
182
191
  taskLocation: { repoRoot: resolved.repoRoot, taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, state: resolved.state },
@@ -4,8 +4,7 @@ import path from 'node:path';
4
4
 
5
5
  import { parseTaskFrontmatter } from './frontmatter.ts';
6
6
  import { resolveTaskRef } from './resolve-ref.ts';
7
- import { resolveDeliveryTarget, resolveDiffBase, resolveTargetHead, validateBaseRef, validateRemote } from './delivery-target.ts';
8
- import { extractReviewBaseline, extractReviewDiffBase, extractReviewTargetHead, extractReviewedHead, findAuthoritativeReviewCodeArtifact } from './review-fingerprint.ts';
7
+ import { resolveDeliveryTarget, validateBaseRef, validateRemote } from './delivery-target.ts';
9
8
  import { captureTaskWriteMetadata, writeTask } from './write.ts';
10
9
  import { withRepositoryMutationLock, withTaskExecutionLock } from './task-execution-lock.ts';
11
10
 
@@ -47,40 +46,6 @@ function validateBranch(cwd: string, branch: string): boolean {
47
46
  return git(cwd, ['check-ref-format', `refs/heads/${branch}`]).status === 0;
48
47
  }
49
48
 
50
- function validateReviewTargetCompatibility(
51
- repoRoot: string,
52
- taskDir: string,
53
- target: { remote: string; baseRef: string },
54
- reviewedHead: string
55
- ): { ok: true } | { ok: false; code: string; message: string } {
56
- const artifact = findAuthoritativeReviewCodeArtifact(taskDir);
57
- if (!artifact.ok || !artifact.path) return { ok: false, code: 'DELIVERY_REVIEW_REQUIRED', message: 'A current review-code artifact is required before delivery' };
58
- let content: string;
59
- try { content = fs.readFileSync(artifact.path, 'utf8'); }
60
- catch (error) { return { ok: false, code: 'DELIVERY_REVIEW_REQUIRED', message: error instanceof Error ? error.message : String(error) }; }
61
- const savedReviewedHead = extractReviewedHead(content) || extractReviewBaseline(content);
62
- const savedTargetHead = extractReviewTargetHead(content);
63
- const savedDiffBase = extractReviewDiffBase(content);
64
- if (!savedReviewedHead || !savedTargetHead || !savedDiffBase) {
65
- return { ok: false, code: 'DELIVERY_REVIEW_TARGET_INVALID', message: 'review-code must save reviewed head, target head, and diff base before delivery' };
66
- }
67
- if (savedReviewedHead !== reviewedHead) {
68
- return { ok: false, code: 'DELIVERY_REVIEW_REQUIRED', message: 'review-code reviewed head does not match last_reviewed_commit' };
69
- }
70
- const saved = resolveDiffBase(repoRoot, reviewedHead, savedTargetHead);
71
- if (!saved.ok || saved.diffBase !== savedDiffBase) {
72
- return { ok: false, code: 'DELIVERY_REVIEW_TARGET_INVALID', message: saved.ok ? 'saved review diff base does not match saved target head' : saved.message };
73
- }
74
- const currentTarget = resolveTargetHead(repoRoot, target);
75
- if (!currentTarget.ok) return { ok: false, code: currentTarget.code, message: currentTarget.message };
76
- const current = resolveDiffBase(repoRoot, reviewedHead, currentTarget.head!);
77
- if (!current.ok) return { ok: false, code: current.code, message: current.message };
78
- if (current.diffBase !== savedDiffBase) {
79
- return { ok: false, code: 'DELIVERY_REVIEW_TARGET_CHANGED', message: 'current delivery target changed the reviewed diff base; re-run review-code' };
80
- }
81
- return { ok: true };
82
- }
83
-
84
49
  function failure(taskId: string | null, code: string, message: string, retryable = false, status: 'failed' | 'blocked' = retryable ? 'blocked' : 'failed'): TaskBranchDeliveryResult {
85
50
  return { status, changed: false, taskId, remoteHead: null, localHead: null, state: null, error: { code, message, retryable } };
86
51
  }
@@ -115,10 +80,6 @@ function deliverUnlocked(
115
80
  const status = git(repoRoot, ['status', '--porcelain=v1']);
116
81
  if (status.status !== 0) return failure(taskId, 'DELIVERY_GIT_INSPECT_FAILED', status.stderr.trim() || 'Unable to inspect Git state');
117
82
  if (status.stdout.trim()) return failure(taskId, 'DELIVERY_WORKTREE_DIRTY', 'Working tree must be clean before branch delivery');
118
- const reviewed = frontmatter.last_reviewed_commit ?? '';
119
- if (reviewed !== localHead) return failure(taskId, 'DELIVERY_REVIEW_REQUIRED', 'Local HEAD must equal last_reviewed_commit before delivery');
120
- const reviewTarget = validateReviewTargetCompatibility(repoRoot, taskDir, target.value, reviewed);
121
- if (!reviewTarget.ok) return failure(taskId, reviewTarget.code, reviewTarget.message, reviewTarget.code.includes('UNAVAILABLE'), reviewTarget.code.includes('UNAVAILABLE') ? 'blocked' : 'failed');
122
83
  const ref = `refs/heads/${branch}`;
123
84
  const remoteOutput = git(repoRoot, ['ls-remote', '--refs', target.value.remote, ref]);
124
85
  if (remoteOutput.status !== 0) return failure(taskId, 'DELIVERY_REMOTE_UNAVAILABLE', remoteOutput.stderr.trim() || 'Unable to inspect the delivery remote', true);