@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
@@ -107,13 +107,11 @@ type TaskLifecycleOptions = {
107
107
  fileSystem?: Partial<LifecycleFileSystem>;
108
108
  taskFileSystem?: Partial<TaskFileSystem>;
109
109
  directoryRenameSync?: (source: string, target: string) => void;
110
- manualOverride?: Readonly<{ failureId: string; operator: string; reason: string }>;
110
+ /** Write the terminal task document and durable journal, but leave the move
111
+ * and short-id mutation for a separately authorized commit. */
112
+ prepareOnly?: boolean;
111
113
  };
112
114
 
113
- function allowsManualOverride(options: TaskLifecycleOptions, code: string): boolean {
114
- return options.manualOverride?.failureId === `lifecycle.apply:${code}`;
115
- }
116
-
117
115
  const STEPS: readonly LifecycleStep[] = ['task-written', 'directory-moved', 'registry-committed'];
118
116
  const DEFAULT_IO: LifecycleFileSystem = {
119
117
  renameSync: (source, target) => fs.renameSync(source, target),
@@ -359,6 +357,44 @@ export function readLifecycleJournalEvidence(repoRoot: string, taskId: string):
359
357
  } catch { return { exists: true, completedSteps: [], failure: 'SANDBOX_CONTROL_LIFECYCLE_JOURNAL_INVALID' }; }
360
358
  }
361
359
 
360
+ export function inspectTaskLifecycleProgress(
361
+ repoRoot: string, taskId: string, agent: string
362
+ ): 'not-started' | 'started-recoverable' | 'unknown' {
363
+ try {
364
+ const hot = locateHotTaskDirs(repoRoot, taskId);
365
+ if (hot.length !== 1 || (hot[0]!.state !== 'active' && hot[0]!.state !== 'completed')) return 'unknown';
366
+ const { state, taskDir } = hot[0]!;
367
+ const content = fs.readFileSync(path.join(taskDir, 'task.md'), 'utf8');
368
+ const frontmatter = parseTypedTaskFrontmatter(content);
369
+ if (frontmatter.id !== taskId) return 'unknown';
370
+ const journalPath = path.join(taskDir, '.task-lifecycle.json');
371
+ if (!fs.existsSync(journalPath)) {
372
+ if (state === 'active') return frontmatter.status === 'active' ? 'not-started' : 'unknown';
373
+ return matchingCompletion(content, { taskRef: taskId, intent: 'complete', agent })
374
+ ? 'started-recoverable' : 'unknown';
375
+ }
376
+ const journal = parseJournal(fs.readFileSync(journalPath, 'utf8'));
377
+ const request: TaskLifecycleRequest = { taskRef: taskId, intent: 'complete', agent };
378
+ if (journal.taskId !== taskId || journal.intent !== 'complete'
379
+ || journal.intentDigest !== intentIdentity(request, taskId)
380
+ || journal.sourceState !== 'active' || journal.targetState !== 'completed') return 'unknown';
381
+ if (journal.completedSteps.some((step, index) => STEPS[index] !== step)) return 'unknown';
382
+ const directoryMoved = journal.completedSteps.includes('directory-moved');
383
+ const registryCommitted = journal.completedSteps.includes('registry-committed');
384
+ const hasShortId = loadShortIdByTaskId(repoRoot).has(taskId);
385
+ if (state === 'active' && (directoryMoved || !hasShortId)) return 'unknown';
386
+ if (state === 'completed' && (!journal.completedSteps.includes('task-written')
387
+ || !matchingCompletion(content, request) || (registryCommitted && hasShortId))) return 'unknown';
388
+ if (frontmatter.status !== state
389
+ && !(state === 'active' && frontmatter.status === 'completed' && matchingCompletion(content, request))) {
390
+ return 'unknown';
391
+ }
392
+ return 'started-recoverable';
393
+ } catch {
394
+ return 'unknown';
395
+ }
396
+ }
397
+
362
398
  type DirectoryEntry = Readonly<{ relativePath: string; kind: 'file' | 'directory'; size: number; digest: string }>;
363
399
 
364
400
  function directoryManifest(root: string): DirectoryEntry[] {
@@ -465,7 +501,7 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
465
501
  }
466
502
  return failed(request, { code: 'LIFECYCLE_INTENT_CONFLICT', message: `task ${taskId} is already ${spec.target} with a different lifecycle intent` }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
467
503
  }
468
- if (!spec.sources.includes(sourceState) && !existingJournal && !allowsManualOverride(options, 'LIFECYCLE_SOURCE_INVALID')) {
504
+ if (!spec.sources.includes(sourceState) && !existingJournal) {
469
505
  return failed(request, { code: 'LIFECYCLE_SOURCE_INVALID', message: `${request.intent} is not allowed from ${sourceState}` }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
470
506
  }
471
507
  if (sourcePath !== targetPath && fs.existsSync(targetPath) && !existingJournal) {
@@ -476,7 +512,7 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
476
512
  try { frontmatter = parseTypedTaskFrontmatter(content); }
477
513
  catch (error) { return failed(request, { code: 'LIFECYCLE_DOCUMENT_INVALID', message: error instanceof Error ? error.message : String(error) }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath }); }
478
514
  if (frontmatter.id !== taskId) return failed(request, { code: 'LIFECYCLE_IDENTITY_INVALID', message: 'task.md id does not match its lifecycle task id' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
479
- if (!locateActivityLog(content) && !allowsManualOverride(options, 'LIFECYCLE_LOG_MISSING')) return failed(request, { code: 'LIFECYCLE_LOG_MISSING', message: 'task has no unique Activity Log section' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
515
+ if (!locateActivityLog(content)) return failed(request, { code: 'LIFECYCLE_LOG_MISSING', message: 'task has no unique Activity Log section' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
480
516
  if (spec.target === 'active') {
481
517
  const contract = validateCurrentTaskContract(content);
482
518
  if (!contract.ok) return failed(request, { code: 'LIFECYCLE_DOCUMENT_INVALID', message: contract.message }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
@@ -490,10 +526,10 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
490
526
  }
491
527
  const registry = JSON.parse(registryRequest.output || '{"version":1,"ids":{}}') as { ids: Record<string, string> };
492
528
  const registeredKey = Object.entries(registry.ids).find(([, candidate]) => candidate === taskId)?.[0];
493
- if (spec.registry === 'release' && sourceState === 'active' && !registeredKey && !allowsManualOverride(options, 'LIFECYCLE_SHORT_ID_PRECONDITION')) {
529
+ if (spec.registry === 'release' && sourceState === 'active' && !registeredKey) {
494
530
  return failed(request, { code: 'LIFECYCLE_SHORT_ID_PRECONDITION', message: 'active source has no short-id registry entry' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
495
531
  }
496
- if (spec.registry === 'alloc' && !registeredKey && Object.keys(registry.ids).length >= 10 ** configuredShortIdLength(repoRoot) - 1 && !allowsManualOverride(options, 'SHORT_ID_CAPACITY_EXCEEDED')) {
532
+ if (spec.registry === 'alloc' && !registeredKey && Object.keys(registry.ids).length >= 10 ** configuredShortIdLength(repoRoot) - 1) {
497
533
  return failed(request, { code: 'SHORT_ID_CAPACITY_EXCEEDED', message: 'short-id registry has no free slot' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
498
534
  }
499
535
  const digest = intentIdentity(request, taskId);
@@ -511,8 +547,7 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
511
547
  if (!journal.failure) return failed(request, { code: 'LIFECYCLE_JOURNAL_INVALID', message: 'existing lifecycle journal has no durable failure evidence for a read-only probe' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath: initialJournalPath });
512
548
  return failed(request, journal.failure, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath: initialJournalPath, completedSteps: journal.completedSteps, pendingSteps: STEPS.filter((step) => !journal.completedSteps.includes(step)) });
513
549
  }
514
- const overrideResumesRecordedFailure = journal.failure && allowsManualOverride(options, journal.failure.code);
515
- if (journal.intentDigest !== digest && !overrideResumesRecordedFailure) return failed(request, { code: 'LIFECYCLE_INTENT_CONFLICT', message: 'an in-progress lifecycle journal belongs to a different request' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath: initialJournalPath });
550
+ if (journal.intentDigest !== digest) return failed(request, { code: 'LIFECYCLE_INTENT_CONFLICT', message: 'an in-progress lifecycle journal belongs to a different request' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath: initialJournalPath });
516
551
  } else {
517
552
  let metadata: TaskWriteMetadata;
518
553
  try { metadata = (options.metadataProvider ?? captureTaskWriteMetadata)(); }
@@ -522,7 +557,7 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
522
557
  }
523
558
  journal = { version: 1, taskId, intent: request.intent, intentDigest: digest, sourceState, targetState: spec.target, metadata, completedSteps: [] };
524
559
  if (request.dryRun) {
525
- const plannedMutations = mutationsFor(request, content, metadata, restoredFiles, allowsManualOverride(options, 'LIFECYCLE_LOG_MISSING'));
560
+ const plannedMutations = mutationsFor(request, content, metadata, restoredFiles, false);
526
561
  if ('code' in plannedMutations) return failed(request, plannedMutations, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
527
562
  const taskPlan = writeTask(
528
563
  { taskRef: taskId, expectedState: sourceState === 'blocked' ? 'blocked' : 'active', mutations: plannedMutations, dryRun: true },
@@ -558,7 +593,7 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
558
593
  if (matchingCompletion(io.readFileSync(path.join(sourcePath, 'task.md')), request, restoredFiles)) completed.add('task-written');
559
594
  if (!completed.has('task-written')) {
560
595
  const current = io.readFileSync(path.join(sourcePath, 'task.md'));
561
- const mutations = mutationsFor(request, current, journal.metadata, restoredFiles, allowsManualOverride(options, 'LIFECYCLE_LOG_MISSING'));
596
+ const mutations = mutationsFor(request, current, journal.metadata, restoredFiles, false);
562
597
  if ('code' in mutations) return failed(request, mutations, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath, completedSteps: [...completed], changed: completed.size > 0 });
563
598
  const writeResult = writeTask(
564
599
  { taskRef: taskId, expectedState: sourceState === 'blocked' ? 'blocked' : 'active', mutations },
@@ -574,6 +609,16 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
574
609
  try { writeJournal(journalPath, journal, io); }
575
610
  catch (error) { return failed(request, { code: 'LIFECYCLE_JOURNAL_WRITE_FAILED', message: String(error) }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath, task: { operations: taskOperations }, changed: true, completedSteps: [...completed], timestamp: journal.metadata.timestamp, agentInfraVersion: journal.metadata.agentInfraVersion }); }
576
611
  }
612
+ if (options.prepareOnly && !completed.has('directory-moved')) {
613
+ return {
614
+ ...failed(request, { code: '', message: '' }), status: 'applied', changed: true, error: null,
615
+ taskId, sourceState, targetState: spec.target, sourcePath, targetPath,
616
+ task: { operations: taskOperations }, directory: { effect: 'unchanged', changed: false },
617
+ shortId: { effect: 'unchanged', shortId: null, changed: false },
618
+ timestamp: journal.metadata.timestamp, agentInfraVersion: journal.metadata.agentInfraVersion,
619
+ journalPath, completedSteps: [...completed], pendingSteps: STEPS.filter((step) => !completed.has(step))
620
+ };
621
+ }
577
622
  if (fs.existsSync(targetPath) && !fs.existsSync(sourcePath)) completed.add('directory-moved');
578
623
  if (!completed.has('directory-moved')) {
579
624
  fs.mkdirSync(path.dirname(targetPath), { recursive: true });
@@ -2,14 +2,10 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
 
4
4
  import { inspectDecisionDetailDuplicates } from './decision-details.ts';
5
- import { scanVisibleMarkdown } from './markdown.ts';
6
5
  import { parseArtifactName } from './artifact-name.ts';
7
- import {
8
- hasOpenArtifactRound,
9
- } from './artifact-lifecycle.ts';
6
+ import { parseLifecyclePathDecision } from './lifecycle-path.ts';
10
7
  import { resolveTaskRef } from './resolve-ref.ts';
11
- import { expectedQualificationRelations, validateQualificationAudit } from './qualification-audit.ts';
12
- import { canonicalSemanticDigest, inspectArtifactPatterns, inspectArtifactStructure, sha256Content } from './artifact-operations.ts';
8
+ import { canonicalSemanticDigest, inspectArtifactContract, sha256Content } from './artifact-operations.ts';
13
9
  import { getArtifactSchema } from './artifact-schema.ts';
14
10
 
15
11
  type LocalArtifactFamily = 'analysis' | 'plan' | 'code';
@@ -29,7 +25,7 @@ type LocalArtifactDiagnosticCode =
29
25
  | 'LOCAL_REQUIRED_PATTERN_MISSING'
30
26
  | 'LOCAL_STRUCTURAL_INVALID'
31
27
  | 'LOCAL_SECTION_HEADING_TRAILING_PUNCTUATION'
32
- | 'LOCAL_QUALIFICATION_AUDIT_INVALID';
28
+ | 'LOCAL_FLOW_DECISION_INVALID';
33
29
 
34
30
  type LocalArtifactDiagnostic = {
35
31
  code: LocalArtifactDiagnosticCode;
@@ -40,8 +36,6 @@ type LocalArtifactDiagnostic = {
40
36
  type LocalArtifactValidationOptions = {
41
37
  family: LocalArtifactFamily;
42
38
  requiredSections?: readonly string[];
43
- taskContent?: string;
44
- artifact?: string;
45
39
  };
46
40
 
47
41
  type LocalArtifactValidationResult = {
@@ -78,7 +72,9 @@ function localDiagnosticCode(code: string): LocalArtifactDiagnosticCode {
78
72
  ARTIFACT_EMPTY: 'LOCAL_ARTIFACT_EMPTY',
79
73
  ARTIFACT_MISSING_SECTION: 'LOCAL_ARTIFACT_MISSING_SECTION',
80
74
  ARTIFACT_DUPLICATE_SECTION: 'LOCAL_ARTIFACT_DUPLICATE_SECTION',
81
- ARTIFACT_HEADING_TRAILING_PUNCTUATION: 'LOCAL_SECTION_HEADING_TRAILING_PUNCTUATION'
75
+ ARTIFACT_HEADING_TRAILING_PUNCTUATION: 'LOCAL_SECTION_HEADING_TRAILING_PUNCTUATION',
76
+ ARTIFACT_STATE_CHECK_PATTERN_MISSING: 'LOCAL_STATUS_COMMAND_MISSING',
77
+ ARTIFACT_REQUIRED_PATTERN_MISSING: 'LOCAL_REQUIRED_PATTERN_MISSING'
82
78
  };
83
79
  return map[code] ?? 'LOCAL_STRUCTURAL_INVALID';
84
80
  }
@@ -87,59 +83,23 @@ function localDiagnostic(item: { code: string; message: string; line: number | n
87
83
  return { code: localDiagnosticCode(item.code), message: item.message, line: item.line };
88
84
  }
89
85
 
90
- function isStatusPattern(pattern: string): boolean {
91
- return pattern === '^\\$ ';
92
- }
93
-
94
86
  function validateLocalArtifact(
95
87
  content: string,
96
88
  options: LocalArtifactValidationOptions
97
89
  ): LocalArtifactValidationResult {
98
90
  const schema = getArtifactSchema(options.family)!;
99
91
  const diagnostics: LocalArtifactDiagnostic[] = [];
100
- const scanned = scanVisibleMarkdown(content);
101
- const structure = inspectArtifactStructure(content, schema);
102
- for (const item of structure.diagnostics) diagnostics.push(localDiagnostic(item));
103
-
104
- const statusSection = schema.sections.find((section) => section.id === 'state-check');
105
- if (statusSection) {
106
- const statusHeading = scanned.headings.find((heading) => (
107
- heading.level === 2 && [statusSection.headings.zh, statusSection.headings.en].some((name) => (
108
- heading.text === name || heading.text === `${name}:` || heading.text === `${name}:`
109
- ))
110
- ));
111
- if (statusHeading) {
112
- const next = scanned.headings.find((candidate) => candidate.start > statusHeading.start && candidate.level <= 2);
113
- const body = content.slice(statusHeading.end, next?.start ?? content.length);
114
- for (const pattern of schema.requiredPatterns.filter(isStatusPattern)) {
115
- if (!new RegExp(pattern, 'm').test(body)) {
116
- diagnostics.push({ code: 'LOCAL_STATUS_COMMAND_MISSING', message: `status section '${statusHeading.text}' is missing required command output`, line: content.slice(0, statusHeading.start).split('\n').length });
117
- }
118
- }
119
- }
120
- }
121
-
122
- const patternInspection = inspectArtifactPatterns(content, {
123
- ...schema,
124
- requiredPatterns: schema.requiredPatterns.filter((item) => !isStatusPattern(item))
125
- });
126
- for (const item of patternInspection.diagnostics) diagnostics.push({ code: 'LOCAL_REQUIRED_PATTERN_MISSING', message: item.message, line: null });
92
+ const inspection = inspectArtifactContract(content, schema, { scopeStateCheckSection: true });
93
+ for (const item of inspection.diagnostics) diagnostics.push(localDiagnostic(item));
127
94
 
128
95
  const decisionDetails = inspectDecisionDetailDuplicates(content);
129
96
  if (!decisionDetails.ok) diagnostics.push({ code: 'LOCAL_DECISION_DETAIL_DUPLICATE', message: decisionDetails.message, line: null });
130
97
 
131
- if (options.taskContent !== undefined) {
132
- const identity = options.artifact ? parseArtifactName(options.artifact) : null;
133
- const expected = identity?.family === options.family && hasOpenArtifactRound(options.taskContent, options.family, identity.round)
134
- ? expectedQualificationRelations(options.taskContent, options.family)
135
- : undefined;
136
- if (expected && !expected.ok) diagnostics.push({ code: 'LOCAL_QUALIFICATION_AUDIT_INVALID', message: `${expected.code}: ${expected.message}`, line: null });
137
- const qualification = validateQualificationAudit(options.taskContent, content, {
138
- family: options.family,
139
- artifact: options.artifact,
140
- expectedUpstreamRelations: expected?.ok ? expected.relations : undefined
141
- });
142
- if (!qualification.ok) diagnostics.push({ code: 'LOCAL_QUALIFICATION_AUDIT_INVALID', message: `${qualification.code}: ${qualification.message}`, line: null });
98
+ if (options.family === 'analysis') {
99
+ const flowDecision = parseLifecyclePathDecision(content);
100
+ if (flowDecision.status !== 'valid') {
101
+ diagnostics.push({ code: 'LOCAL_FLOW_DECISION_INVALID', message: flowDecision.message, line: null });
102
+ }
143
103
  }
144
104
 
145
105
  return {
@@ -182,10 +142,8 @@ function prepareLocalArtifact(
182
142
  if (!resolved.ok) return failed(resolved.code, resolved.message);
183
143
  const parsed = parseArtifactName(request.artifact);
184
144
  if (!parsed || parsed.family !== request.family) return failed('ARTIFACT_IDENTITY_INVALID', `artifact '${request.artifact}' does not match ${request.family}`);
185
- let taskContent: string;
186
145
  let content: string;
187
146
  try {
188
- taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
189
147
  const artifactPath = path.join(resolved.taskDir, request.artifact);
190
148
  if (!fs.statSync(artifactPath).isFile()) return failed('ARTIFACT_NOT_REGULAR', `artifact '${request.artifact}' is not a regular file`);
191
149
  content = candidate ?? fs.readFileSync(artifactPath, 'utf8');
@@ -195,7 +153,7 @@ function prepareLocalArtifact(
195
153
  return failed('ARTIFACT_IDENTITY_INVALID', 'artifact finalization requires an active task');
196
154
  }
197
155
  const validation = validateLocalArtifact(content, {
198
- family: request.family, requiredSections: request.requiredSections, taskContent, artifact: request.artifact
156
+ family: request.family, requiredSections: request.requiredSections
199
157
  });
200
158
  const artifactSha256 = sha256Content(content);
201
159
  const result = failedFinalization(request, {
@@ -59,6 +59,7 @@ import { hasOpenLifecycleExecution } from './activity-log.ts';
59
59
  import { reconcileTaskInvalidation } from './invalidation-command.ts';
60
60
  import type { LifecycleAction, LifecycleFacts } from './capabilities.ts';
61
61
  import { normalizeAgentToken } from '../agent-clients/tokens.ts';
62
+ import { resolveArtifactContext } from './artifact-lifecycle.ts';
62
63
 
63
64
  type OrchestrationStatus = 'running' | 'paused' | 'completed';
64
65
  type ModelPolicySource = Readonly<{
@@ -642,18 +643,14 @@ function validateSavedCurrentDiffBase(
642
643
  return null;
643
644
  }
644
645
 
645
- function routeFromFacts(facts: LifecycleFacts): Omit<OrchestrationNext, 'requestedModel' | 'requestedReasoningEffort'> | { completion: true } | null {
646
+ function routeFromFacts(facts: LifecycleFacts): Omit<OrchestrationNext, 'requestedModel' | 'requestedReasoningEffort' | 'round' | 'artifact'> | { completion: true } | null {
646
647
  const recommendation = recommendNext(facts);
647
- if (!recommendation.action) return { completion: true };
648
+ if (!recommendation.action) return recommendation.reasonCode === 'LIFECYCLE_REVIEWED' ? { completion: true } : null;
648
649
  const action = recommendation.action;
649
650
  if (action === 'manual-validation' || action === 'validation-run') return null;
650
- const family = action === 'analysis' ? 'analysis' : action;
651
- const round = maxArtifactRound([
652
- ...(facts.artifacts[family] ?? []), ...(facts.staleArtifacts?.[family] ?? [])
653
- ], family) + 1;
654
651
  const command = action === 'analysis' ? 'analyze-task' : action === 'plan' ? 'plan-task' : action === 'code' ? 'code-task' : action;
655
652
  const role = action.startsWith('review-') ? 'reviewer' : 'executor';
656
- return { action: command, role, stage: action, round, artifact: artifactName(family, round) };
653
+ return { action: command, role, stage: action };
657
654
  }
658
655
 
659
656
  function routeOrchestration(taskRef: string, options: OrchestrationOptions = {}): OrchestrationResult {
@@ -786,14 +783,35 @@ function routeOrchestration(taskRef: string, options: OrchestrationOptions = {})
786
783
  return { status: 'completed', changed: true, taskId: resolved.taskId, run: completed, next: null, error: null };
787
784
  }
788
785
  const action = routed.stage as LifecycleAction;
786
+ const family = action === 'analysis' ? 'analysis' : action;
787
+ const artifactContext = resolveArtifactContext(taskRef, family, {
788
+ repoRoot: resolved.repoRoot
789
+ });
790
+ if (artifactContext.status !== 'ready' || !artifactContext.selection) {
791
+ return failed(
792
+ artifactContext.error?.code ?? 'ORCHESTRATION_ARTIFACT_SELECTION_FAILED',
793
+ artifactContext.error?.message ?? 'cannot determine artifact selection',
794
+ resolved.taskId
795
+ );
796
+ }
797
+ if (artifactContext.selection.disposition === 'reuse') {
798
+ return failed(
799
+ 'ORCHESTRATION_REUSE_STALLED',
800
+ `lifecycle still routes to ${action} after reusing ${artifactContext.selection.artifact.name}`,
801
+ resolved.taskId
802
+ );
803
+ }
804
+ const selected = artifactContext.selection.artifact;
789
805
  const capability = canStart(action, facts.facts, {
790
- initiator: 'orchestrator', requestId: `orchestration:${resolved.taskId}:${routed.stage}:${routed.round}`,
806
+ initiator: 'orchestrator', requestId: `orchestration:${resolved.taskId}:${routed.stage}:${selected.round}`,
791
807
  requestedAction: action, reasonCode: 'user-request', explicitRequest: true
792
808
  });
793
809
  if (!capability.allowed) return failed('ORCHESTRATION_CAPABILITY_DENIED', `${capability.reasonCode}: ${capability.evidence.join(', ')}`, resolved.taskId);
794
810
  const policy = run ? rolePolicy(run, routed.role) : null;
795
811
  const next: OrchestrationNext = {
796
812
  ...routed,
813
+ round: selected.round,
814
+ artifact: selected.name,
797
815
  requestedModel: policy?.model ?? null,
798
816
  requestedReasoningEffort: policy?.reasoningEffort ?? null
799
817
  };
@@ -1,13 +1,10 @@
1
1
  import type { ReviewFinalizationRequest } from './review-finalization.ts';
2
2
 
3
- export function parseReviewCommand(args: readonly string[]): ReviewFinalizationRequest & {
4
- overrideTicket?: string; overrideTarget?: string; overrideScope?: string;
5
- } {
3
+ export function parseReviewCommand(args: readonly string[]): ReviewFinalizationRequest {
6
4
  if (!args[0] || !['preflight', 'finalize-summary'].includes(args[1] ?? '')) throw new Error('task ref and a supported review operation are required');
7
5
  const values: Record<string, string | boolean> = {};
8
6
  const flags = {
9
- '--stage': 'stage', '--artifact': 'artifact', '--orchestrated': 'orchestrated', '--dry-run': 'dryRun',
10
- '--override-ticket': 'overrideTicket', '--override-target': 'overrideTarget', '--override-scope': 'overrideScope'
7
+ '--stage': 'stage', '--artifact': 'artifact', '--orchestrated': 'orchestrated', '--dry-run': 'dryRun'
11
8
  } as const;
12
9
  for (let index = 2; index < args.length; index += 1) {
13
10
  const flag = args[index]!;
@@ -12,11 +12,8 @@ import { resolveTaskRef } from './resolve-ref.ts';
12
12
  import { validateLifecycleExecution } from './lifecycle-execution.ts';
13
13
  import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.ts';
14
14
  import type { ResolveTaskRefErrorCode } from './resolve-ref.ts';
15
- import { allowsManualOverride } from './guard-override.ts';
16
- import type { ManualOverrideCapability } from './guard-override.ts';
17
15
  import { getArtifactSchema } from './artifact-schema.ts';
18
16
  import { canonicalSemanticDigest, inspectArtifactContract, sha256Content } from './artifact-operations.ts';
19
- import { expectedQualificationRelations, validateQualificationAudit } from './qualification-audit.ts';
20
17
 
21
18
  type ReviewFinalizationErrorCode =
22
19
  | ResolveTaskRefErrorCode
@@ -30,7 +27,6 @@ type ReviewFinalizationErrorCode =
30
27
  | 'REVIEW_SUMMARY_PLACEHOLDER_INVALID'
31
28
  | 'REVIEW_SUMMARY_COUNT_MISMATCH'
32
29
  | 'REVIEW_ARTIFACT_STRUCTURE_INVALID'
33
- | 'REVIEW_ARTIFACT_QUALIFICATION_INVALID'
34
30
  | 'REVIEW_DECISION_DETAIL_INVALID'
35
31
  | 'REVIEW_ARTIFACT_CONFLICT'
36
32
  | 'REVIEW_PROVENANCE_INVALID'
@@ -72,7 +68,6 @@ type ReviewFileSystem = {
72
68
  type ReviewFinalizationOptions = {
73
69
  repoRoot?: string;
74
70
  fileSystem?: Partial<ReviewFileSystem>;
75
- manualOverride?: ManualOverrideCapability;
76
71
  lockAlreadyHeld?: boolean;
77
72
  };
78
73
 
@@ -143,10 +138,8 @@ function preflightReviewSummaryUnlocked(
143
138
  const publicationError = validateArtifactPublication(resolved.taskDir, spec.family, request.artifact);
144
139
  if (publicationError) return preflightFailed(request, 'REVIEW_ARTIFACT_IDENTITY_INVALID', publicationError.message, resolved.taskId);
145
140
 
146
- let taskContent: string;
147
141
  let content: string;
148
142
  try {
149
- taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
150
143
  const validated = validateCompletedArtifact(resolved.taskDir, spec.family, request.artifact);
151
144
  if (!validated.ok) return preflightFailed(request, validated.error.code === 'ARTIFACT_NOT_REGULAR' ? 'REVIEW_ARTIFACT_NOT_REGULAR' : 'REVIEW_ARTIFACT_IDENTITY_INVALID', validated.error.message, resolved.taskId);
152
145
  content = fs.readFileSync(validated.artifact.path, 'utf8');
@@ -163,21 +156,12 @@ function preflightReviewSummaryUnlocked(
163
156
  const semanticDigest = canonicalSemanticDigest(content);
164
157
  const detail = inspectDecisionDetailDuplicates(content);
165
158
  const structure = inspectArtifactContract(content, getArtifactSchema(spec.family)!);
166
- const expected = expectedQualificationRelations(taskContent, spec.family);
167
- if (!expected.ok) return preflightFailed(request, 'REVIEW_ARTIFACT_QUALIFICATION_INVALID', `${expected.code}: ${expected.message}`, resolved.taskId, { artifactSha256, semanticDigest });
168
- const qualification = validateQualificationAudit(taskContent, content, {
169
- family: spec.family,
170
- artifact: request.artifact,
171
- expectedUpstreamRelations: expected.relations
172
- });
173
- const qualificationError = qualification.ok ? null : qualification;
174
- if (!detail.ok || !structure.ok || qualificationError) {
159
+ if (!detail.ok || !structure.ok) {
175
160
  const diagnostics = [
176
161
  ...(!detail.ok ? [`${detail.code}: ${detail.message}`] : []),
177
- ...structure.diagnostics.map((item) => `${item.code}: ${item.message}`),
178
- ...(qualificationError ? [`${qualificationError.code}: ${qualificationError.message}`] : [])
162
+ ...structure.diagnostics.map((item) => `${item.code}: ${item.message}`)
179
163
  ].join('; ');
180
- return preflightFailed(request, !detail.ok ? 'REVIEW_DECISION_DETAIL_INVALID' : !structure.ok ? 'REVIEW_ARTIFACT_STRUCTURE_INVALID' : 'REVIEW_ARTIFACT_QUALIFICATION_INVALID', diagnostics, resolved.taskId, {
164
+ return preflightFailed(request, !detail.ok ? 'REVIEW_DECISION_DETAIL_INVALID' : 'REVIEW_ARTIFACT_STRUCTURE_INVALID', diagnostics, resolved.taskId, {
181
165
  artifactSha256, semanticDigest,
182
166
  });
183
167
  }
@@ -237,7 +221,7 @@ function prepareReviewSummaryCandidate(
237
221
  const resolved = resolveTaskRef(request.taskRef, { repoRoot: options.repoRoot });
238
222
  taskId = resolved.taskId ?? null;
239
223
  if (!resolved.ok) return reject(resolved.code, resolved.message);
240
- if (resolved.state !== 'active' && !allowsManualOverride(options.manualOverride, 'review-finalization', 'TASK_STATE_MISMATCH')) {
224
+ if (resolved.state !== 'active') {
241
225
  return reject('TASK_STATE_MISMATCH', `task ${taskId} is ${resolved.state}, expected active`);
242
226
  }
243
227
  const parsed = parseArtifactName(request.artifact);
@@ -1,9 +1,14 @@
1
1
  const REWORK_INTENT_HEADINGS = ['返工意图', 'Rework Intent'] as const;
2
2
  const REWORK_INTENT_COLUMNS = [
3
+ 'intent_id', 'finding_id', 'source_artifact', 'source_sha256', 'target',
4
+ 'classification', 'evidence_digest', 'task_fact_digest', 'status', 'declared_at', 'consumed_at'
5
+ ] as const;
6
+ const LEGACY_REWORK_INTENT_COLUMNS = [
3
7
  'intent_id', 'finding_id', 'source_artifact', 'source_sha256', 'target',
4
8
  'status', 'declared_at', 'consumed_at'
5
9
  ] as const;
6
- type ReworkTarget = 'analysis' | 'plan' | 'code';
10
+ type ReworkTarget = 'analysis' | 'plan' | 'code' | 'pause';
11
+ type ReworkClassification = 'scope-or-requirement' | 'design' | 'implementation' | 'human-decision' | 'insufficient-evidence';
7
12
  type ReworkIntentStatus = 'pending' | 'consumed' | 'superseded';
8
13
  type ReworkIntent = {
9
14
  intentId: string;
@@ -11,6 +16,9 @@ type ReworkIntent = {
11
16
  sourceArtifact: string;
12
17
  sourceSha256: string;
13
18
  target: ReworkTarget;
19
+ classification?: ReworkClassification;
20
+ evidenceDigest?: string;
21
+ taskFactDigest?: string;
14
22
  status: ReworkIntentStatus;
15
23
  declaredAt: string;
16
24
  consumedAt: string;
@@ -66,15 +74,19 @@ function parseReworkIntentDocument(content: string): ReworkIntentParseResult {
66
74
  const row = Object.fromEntries(REWORK_INTENT_COLUMNS.map((column, index) => [column, values[index]!])) as Record<string, string>;
67
75
  if (!/^RI-[1-9]\d*$/.test(row.intent_id!)) throw new Error(`invalid intent id '${row.intent_id}'`);
68
76
  if (!/^(AN|PL|CD)-[1-9]\d*$/.test(row.finding_id!)) throw new Error(`invalid finding id '${row.finding_id}'`);
69
- if (!['analysis', 'plan', 'code'].includes(row.target!)) throw new Error(`invalid rework target '${row.target}'`);
77
+ if (!['analysis', 'plan', 'code', 'pause'].includes(row.target!)) throw new Error(`invalid rework target '${row.target}'`);
78
+ if (!['scope-or-requirement', 'design', 'implementation', 'human-decision', 'insufficient-evidence'].includes(row.classification!)) throw new Error(`invalid rework classification '${row.classification}'`);
70
79
  if (!['pending', 'consumed', 'superseded'].includes(row.status!)) throw new Error(`invalid rework status '${row.status}'`);
71
80
  if (!/^[a-f0-9]{64}$/.test(row.source_sha256!)) throw new Error(`invalid source hash for '${row.intent_id}'`);
81
+ if (!/^[a-f0-9]{64}$/.test(row.evidence_digest!) || !/^[a-f0-9]{64}$/.test(row.task_fact_digest!)) throw new Error(`invalid semantic digest for '${row.intent_id}'`);
72
82
  if (!row.source_artifact || !row.declared_at) throw new Error(`rework intent '${row.intent_id}' has invalid required fields`);
73
83
  const terminal = row.status === 'consumed' || row.status === 'superseded';
74
84
  if (terminal !== Boolean(row.consumed_at)) throw new Error(`rework intent '${row.intent_id}' has invalid consumed_at`);
75
85
  return {
76
86
  intentId: row.intent_id!, findingId: row.finding_id!, sourceArtifact: row.source_artifact!,
77
87
  sourceSha256: row.source_sha256!, target: row.target as ReworkTarget,
88
+ classification: row.classification as ReworkClassification,
89
+ evidenceDigest: row.evidence_digest!, taskFactDigest: row.task_fact_digest!,
78
90
  status: row.status as ReworkIntentStatus, declaredAt: row.declared_at!, consumedAt: row.consumed_at!
79
91
  } satisfies ReworkIntent;
80
92
  });
@@ -96,7 +108,10 @@ function renderReworkIntents(intents: readonly ReworkIntent[]): string {
96
108
  `| ${REWORK_INTENT_COLUMNS.map(() => '---').join(' | ')} |`,
97
109
  ...intents.map((intent) => `| ${[
98
110
  intent.intentId, intent.findingId, intent.sourceArtifact, intent.sourceSha256,
99
- intent.target, intent.status, intent.declaredAt, intent.consumedAt
111
+ intent.target, intent.classification ?? (intent.target === 'analysis' ? 'scope-or-requirement' : intent.target === 'plan' ? 'design' : intent.target === 'code' ? 'implementation' : 'insufficient-evidence'),
112
+ intent.evidenceDigest ?? requireDigest(`${intent.findingId}|${intent.sourceArtifact}|${intent.sourceSha256}`),
113
+ intent.taskFactDigest ?? requireDigest(`current|${intent.findingId}|${intent.target}`),
114
+ intent.status, intent.declaredAt, intent.consumedAt
100
115
  ].map(escape).join(' | ')} |`)
101
116
  ].join('\n');
102
117
  }
@@ -104,11 +119,63 @@ function renderReworkIntents(intents: readonly ReworkIntent[]): string {
104
119
  function upsertReworkIntent(intents: readonly ReworkIntent[], intent: ReworkIntent): { changed: boolean; intents: readonly ReworkIntent[] } {
105
120
  const existing = intents.find((candidate) => candidate.intentId === intent.intentId);
106
121
  if (!existing) return { changed: true, intents: [...intents, intent] };
107
- const identity = (value: ReworkIntent) => [value.intentId, value.findingId, value.sourceArtifact, value.sourceSha256, value.target];
122
+ const identity = (value: ReworkIntent) => [value.intentId, value.findingId, value.sourceArtifact, value.sourceSha256, value.target, value.classification, value.evidenceDigest, value.taskFactDigest];
108
123
  if (JSON.stringify(identity(existing)) !== JSON.stringify(identity(intent))) throw new Error(`intent '${intent.intentId}' identity conflicts with existing state`);
109
124
  return { changed: false, intents };
110
125
  }
111
126
 
127
+ function consumeHumanDecisionReworkIntents(
128
+ intents: readonly ReworkIntent[],
129
+ findingId: string,
130
+ consumedAt: string
131
+ ): { changed: boolean; intents: readonly ReworkIntent[] } {
132
+ let changed = false;
133
+ const next = intents.map((intent) => {
134
+ if (intent.status !== 'pending' || intent.target !== 'pause' || intent.classification !== 'human-decision' || intent.findingId !== findingId) return intent;
135
+ changed = true;
136
+ return { ...intent, status: 'consumed' as const, consumedAt };
137
+ });
138
+ return { changed, intents: next };
139
+ }
140
+
141
+ function parseLegacyReworkIntentDocument(content: string): ReworkIntentParseResult {
142
+ // TODO(compat): Remove this explicit decoder once no task contains the eight-column rework-intent header.
143
+ const body = sectionBody(content);
144
+ if (body === null) return { ok: false, code: 'TASK_REWORK_INTENT_INVALID', message: 'legacy rework intent section is missing' };
145
+ const lines = body.split(/\r?\n/).filter((line) => line.trim());
146
+ const header = lines[0] ? cells(lines[0]) : null;
147
+ const separator = lines[1] ? cells(lines[1]) : null;
148
+ if (!header || !separator || header.length !== LEGACY_REWORK_INTENT_COLUMNS.length || !header.every((value, index) => value === LEGACY_REWORK_INTENT_COLUMNS[index])) {
149
+ return { ok: false, code: 'TASK_REWORK_INTENT_INVALID', message: 'rework intent section is not the known legacy table' };
150
+ }
151
+ try {
152
+ const intents = lines.slice(2).map((line) => {
153
+ const values = cells(line);
154
+ if (!values || values.length !== LEGACY_REWORK_INTENT_COLUMNS.length) throw new Error('legacy rework intent row has the wrong column count');
155
+ const row = Object.fromEntries(LEGACY_REWORK_INTENT_COLUMNS.map((column, index) => [column, values[index]!])) as Record<string, string>;
156
+ if (!/^RI-[1-9]\d*$/.test(row.intent_id!) || !/^(AN|PL|CD)-[1-9]\d*$/.test(row.finding_id!)) throw new Error('legacy rework intent identity is invalid');
157
+ if (!['analysis', 'plan', 'code'].includes(row.target!) || !['pending', 'consumed', 'superseded'].includes(row.status!)) throw new Error('legacy rework intent state is invalid');
158
+ if (!/^[a-f0-9]{64}$/.test(row.source_sha256!)) throw new Error('legacy rework intent source hash is invalid');
159
+ const legacyDigest = (value: string) => requireDigest(value);
160
+ const classification: ReworkClassification = row.target === 'analysis' ? 'scope-or-requirement' : row.target === 'plan' ? 'design' : 'implementation';
161
+ return {
162
+ intentId: row.intent_id!, findingId: row.finding_id!, sourceArtifact: row.source_artifact!, sourceSha256: row.source_sha256!,
163
+ target: row.target as ReworkTarget, classification,
164
+ evidenceDigest: legacyDigest(`${row.finding_id}|${row.source_artifact}|${row.source_sha256}`),
165
+ taskFactDigest: legacyDigest(`legacy|${row.finding_id}|${row.target}`),
166
+ status: row.status as ReworkIntentStatus, declaredAt: row.declared_at!, consumedAt: row.consumed_at!
167
+ } satisfies ReworkIntent;
168
+ });
169
+ return { ok: true, present: true, intents };
170
+ } catch (error) {
171
+ return { ok: false, code: 'TASK_REWORK_INTENT_INVALID', message: error instanceof Error ? error.message : String(error) };
172
+ }
173
+ }
174
+
175
+ function requireDigest(value: string): string {
176
+ return createHash('sha256').update(value).digest('hex');
177
+ }
178
+
112
179
  function consumeReworkIntents(
113
180
  intents: readonly ReworkIntent[],
114
181
  target: ReworkTarget,
@@ -161,5 +228,8 @@ export {
161
228
  consumeReworkIntents,
162
229
  supersedeReworkIntents,
163
230
  upsertReworkIntent
231
+ , consumeHumanDecisionReworkIntents
232
+ , parseLegacyReworkIntentDocument
164
233
  };
165
- export type { ReworkIntent, ReworkIntentParseResult, ReworkIntentStatus, ReworkTarget };
234
+ export type { ReworkClassification, ReworkIntent, ReworkIntentParseResult, ReworkIntentStatus, ReworkTarget };
235
+ import { createHash } from 'node:crypto';
@@ -97,22 +97,22 @@ function collectTaskSnapshot(taskRef: string, options: SnapshotOptions = {}): Ta
97
97
  const observations: SnapshotObservation[] = [];
98
98
  try {
99
99
  const output = stripOneTrailingNewline((options.gitStatus ?? defaultGitStatus)(resolved.repoRoot));
100
- observations.push({ name: 'git', status: 'ready', command: '$ git status -s', output, error: null });
100
+ observations.push({ name: 'git', status: 'ready', command: 'git status --short --untracked-files=all', output, error: null });
101
101
  } catch (error) {
102
- return failedSnapshot(resolved, taskRef, observations, 'git', '$ git status -s', 'SNAPSHOT_GIT_FAILED', error);
102
+ return failedSnapshot(resolved, taskRef, observations, 'git', 'git status --short --untracked-files=all', 'SNAPSHOT_GIT_FAILED', error);
103
103
  }
104
104
  try {
105
105
  const entries = (options.readDirectory ?? ((dir) => fs.readdirSync(dir, { withFileTypes: true })))(resolved.taskDir);
106
106
  const output = renderDirectory(entries, resolved.taskDir, options.lstat ?? fs.lstatSync);
107
- observations.push({ name: 'task-directory', status: 'ready', command: `$ ls -la ${taskDirPath}/`, output, error: null });
107
+ observations.push({ name: 'task-directory', status: 'ready', command: `fs.readdirSync ${taskDirPath}/`, output, error: null });
108
108
  } catch (error) {
109
- return failedSnapshot(resolved, taskRef, observations, 'task-directory', `$ ls -la ${taskDirPath}/`, 'SNAPSHOT_DIRECTORY_READ_FAILED', error);
109
+ return failedSnapshot(resolved, taskRef, observations, 'task-directory', `fs.readdirSync ${taskDirPath}/`, 'SNAPSHOT_DIRECTORY_READ_FAILED', error);
110
110
  }
111
111
  try {
112
112
  const content = (options.readTask ?? ((file) => fs.readFileSync(file, 'utf8')))(resolved.taskMdPath);
113
- observations.push({ name: 'task-tail', status: 'ready', command: `$ tail ${taskPath}`, output: renderTail(content), error: null });
113
+ observations.push({ name: 'task-tail', status: 'ready', command: `fs.readFileSync ${taskPath} (last 10 lines)`, output: renderTail(content), error: null });
114
114
  } catch (error) {
115
- return failedSnapshot(resolved, taskRef, observations, 'task-tail', `$ tail ${taskPath}`, 'SNAPSHOT_TASK_READ_FAILED', error);
115
+ return failedSnapshot(resolved, taskRef, observations, 'task-tail', `fs.readFileSync ${taskPath} (last 10 lines)`, 'SNAPSHOT_TASK_READ_FAILED', error);
116
116
  }
117
117
  return {
118
118
  status: 'ready', changed: false, requestRef: taskRef, taskId: resolved.taskId,