@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
@@ -7,17 +7,10 @@ import { checkpointCommitMatches } from './commit-identity.js';
7
7
  import { readCheckpointIntent } from './commit-intent.js';
8
8
  import { parseTaskFrontmatter } from './frontmatter.js';
9
9
  import { readTaskFinalizationReceipt } from './finalization.js';
10
- import { semanticDigest, sha256Content } from './local-artifact-finalization.js';
11
- import { artifactRecoveryRoot, readArtifactRecoveryIntent } from './artifact-repair-intent.js';
12
- import { parseArtifactName } from './artifact-name.js';
13
10
  import { enumerateAllTaskDirs } from './resolve-ref.js';
14
11
  const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
15
- const LOCAL_INTENT_RE = /^(TASK-\d{8}-\d{6})-(analysis|review-analysis|plan|review-plan|code|review-code)-(.+\.md)\.json$/;
16
12
  const COMMIT_INTENT_RE = /^(TASK-\d{8}-\d{6})\.json$/;
17
- const AUXILIARY_ROOTS = [
18
- '.local-artifact-finalization-intents',
19
- '.task-commit-intents'
20
- ];
13
+ const AUXILIARY_ROOTS = ['.task-commit-intents'];
21
14
  function auxiliaryRoot(repoRoot, name) {
22
15
  return path.join(repoRoot, '.agents', 'workspace', name);
23
16
  }
@@ -98,46 +91,6 @@ function receiptGate(repoRoot, task, options) {
98
91
  }
99
92
  return null;
100
93
  }
101
- function canonicalLocalIntentPath(taskId, family, artifact) {
102
- return `${taskId}-${family}-${artifact}.json`;
103
- }
104
- function localIntentCandidate(repoRoot, task, filePath, taskId, family, artifact, identity, options) {
105
- const protectedReason = taskGate(task);
106
- if (protectedReason)
107
- return item('LFAI-CONSUMED', taskId, filePath, 'protected', protectedReason, identity);
108
- if (path.basename(filePath) !== canonicalLocalIntentPath(taskId, family, artifact)
109
- || !parseArtifactName(artifact)?.family || parseArtifactName(artifact).family !== family) {
110
- return item('LFAI-CONSUMED', taskId, filePath, 'protected', 'PATH_IDENTITY_MISMATCH', identity);
111
- }
112
- let intent;
113
- try {
114
- intent = readArtifactRecoveryIntent(repoRoot, taskId, family, artifact);
115
- }
116
- catch {
117
- return item('LFAI-CONSUMED', taskId, filePath, 'protected', 'LFAI_SCHEMA_INVALID', identity);
118
- }
119
- if (!intent || intent.state !== 'consumed') {
120
- return item('LFAI-CONSUMED', taskId, filePath, 'protected', 'LFAI_STATE_PROTECTED', identity);
121
- }
122
- const artifactPath = path.join(task.taskDir, artifact);
123
- if (safeLstat(task.taskDir, artifactPath, 'file') === null) {
124
- return item('LFAI-CONSUMED', taskId, filePath, 'protected', 'ARTIFACT_IDENTITY_MISMATCH', identity);
125
- }
126
- let content;
127
- try {
128
- content = fs.readFileSync(artifactPath, 'utf8');
129
- }
130
- catch {
131
- return item('LFAI-CONSUMED', taskId, filePath, 'protected', 'ARTIFACT_UNREADABLE', identity);
132
- }
133
- if (intent.finalArtifactSha256 !== sha256Content(content) || intent.finalSemanticDigest !== semanticDigest(content)) {
134
- return item('LFAI-CONSUMED', taskId, filePath, 'protected', 'ARTIFACT_DIGEST_MISMATCH', identity);
135
- }
136
- const receiptReason = receiptGate(repoRoot, task, options);
137
- if (receiptReason)
138
- return item('LFAI-CONSUMED', taskId, filePath, 'protected', receiptReason, identity);
139
- return item('LFAI-CONSUMED', taskId, filePath, 'planned', 'LFAI_CONSUMED_AND_VERIFIED', identity);
140
- }
141
94
  function commitIntentCandidate(repoRoot, task, filePath, taskId, identity, options) {
142
95
  const protectedReason = taskGate(task);
143
96
  if (protectedReason)
@@ -209,24 +162,13 @@ function readAuxiliaryCandidates(repoRoot, tasks, options) {
209
162
  candidates.push(item('AUXILIARY-UNKNOWN', null, filePath, 'protected', 'PATH_IDENTITY_MISMATCH'));
210
163
  continue;
211
164
  }
212
- if (rootName === '.local-artifact-finalization-intents') {
213
- const match = LOCAL_INTENT_RE.exec(entry.name);
214
- if (!match || (selected && !selected.has(match[1]))) {
215
- if (!selected || !match)
216
- candidates.push(item('AUXILIARY-UNKNOWN', null, filePath, 'protected', 'PATH_IDENTITY_MISMATCH', identity));
217
- continue;
218
- }
219
- candidates.push(localIntentCandidate(repoRoot, tasks.get(match[1]), filePath, match[1], match[2], match[3], identity, options));
220
- }
221
- else {
222
- const match = COMMIT_INTENT_RE.exec(entry.name);
223
- if (!match || (selected && !selected.has(match[1]))) {
224
- if (!selected || !match)
225
- candidates.push(item('AUXILIARY-UNKNOWN', null, filePath, 'protected', 'PATH_IDENTITY_MISMATCH', identity));
226
- continue;
227
- }
228
- candidates.push(commitIntentCandidate(repoRoot, tasks.get(match[1]), filePath, match[1], identity, options));
165
+ const match = COMMIT_INTENT_RE.exec(entry.name);
166
+ if (!match || (selected && !selected.has(match[1]))) {
167
+ if (!selected || !match)
168
+ candidates.push(item('AUXILIARY-UNKNOWN', null, filePath, 'protected', 'PATH_IDENTITY_MISMATCH', identity));
169
+ continue;
229
170
  }
171
+ candidates.push(commitIntentCandidate(repoRoot, tasks.get(match[1]), filePath, match[1], identity, options));
230
172
  }
231
173
  if (entries.length === 0) {
232
174
  candidates.push(item('EMPTY-AUX-PARENT', null, root, 'planned', 'EMPTY_CANONICAL_AUXILIARY_ROOT', rootIdentity));
@@ -255,85 +197,7 @@ function scanIntermediateCleanup(repoRoot, options = {}) {
255
197
  const candidates = scanInternal(repoRoot, options);
256
198
  return buildIntermediateCleanupReport(candidates, true);
257
199
  }
258
- function cleanupConsumedRecoveryFiles(repoRoot, task, candidate) {
259
- if (candidate.kind !== 'LFAI-CONSUMED' || !candidate.taskId)
260
- return true;
261
- if (!task)
262
- return false;
263
- const match = LOCAL_INTENT_RE.exec(path.basename(candidate.path));
264
- if (!match)
265
- return false;
266
- let intent;
267
- try {
268
- intent = readArtifactRecoveryIntent(repoRoot, candidate.taskId, match[2], match[3]);
269
- }
270
- catch {
271
- return false;
272
- }
273
- if (!intent || intent.state !== 'consumed')
274
- return false;
275
- const recovery = artifactRecoveryRoot(task.taskDir, intent.recoveryOperationId);
276
- let stat = null;
277
- try {
278
- stat = fs.lstatSync(recovery);
279
- }
280
- catch (error) {
281
- if (errorCode(error) === 'ENOENT')
282
- return true;
283
- return false;
284
- }
285
- if (!stat.isDirectory() || stat.isSymbolicLink())
286
- return false;
287
- let entries;
288
- try {
289
- entries = fs.readdirSync(recovery, { withFileTypes: true });
290
- }
291
- catch {
292
- return false;
293
- }
294
- for (const entry of entries) {
295
- if (entry.isSymbolicLink())
296
- return false;
297
- if (entry.isFile()) {
298
- if (!['baseline.md', 'candidate.md', 'final.md', 'publish.md'].includes(entry.name))
299
- return false;
300
- continue;
301
- }
302
- if (!entry.isDirectory() || entry.name !== 'generations')
303
- return false;
304
- let generations;
305
- try {
306
- generations = fs.readdirSync(path.join(recovery, entry.name), { withFileTypes: true });
307
- }
308
- catch {
309
- return false;
310
- }
311
- if (generations.some((generation) => generation.isSymbolicLink()
312
- || !generation.isFile() || !/^[a-f0-9]{64}\.md$/u.test(generation.name)))
313
- return false;
314
- }
315
- try {
316
- for (const entry of entries) {
317
- const target = path.join(recovery, entry.name);
318
- if (entry.isFile())
319
- fs.unlinkSync(target);
320
- else {
321
- for (const generation of fs.readdirSync(target, { withFileTypes: true }))
322
- fs.unlinkSync(path.join(target, generation.name));
323
- fs.rmdirSync(target);
324
- }
325
- }
326
- fs.rmdirSync(recovery);
327
- const recoveryRoot = path.dirname(recovery);
328
- if (fs.readdirSync(recoveryRoot).length === 0)
329
- fs.rmdirSync(recoveryRoot);
330
- return true;
331
- }
332
- catch (error) {
333
- return errorCode(error) === 'ENOENT';
334
- }
335
- }
336
- function removeCandidate(repoRoot, candidate, task) {
200
+ function removeCandidate(repoRoot, candidate) {
337
201
  if (candidate.disposition !== 'planned')
338
202
  return candidate;
339
203
  const root = candidate.kind === 'EMPTY-AUX-PARENT'
@@ -358,9 +222,6 @@ function removeCandidate(repoRoot, candidate, task) {
358
222
  return { ...candidate, disposition: 'protected', reason: 'PATH_IDENTITY_CHANGED' };
359
223
  if (!sameFileIdentity(expected, actual))
360
224
  return { ...candidate, disposition: 'protected', reason: 'PATH_IDENTITY_CHANGED' };
361
- if (!cleanupConsumedRecoveryFiles(repoRoot, task, candidate)) {
362
- return { ...candidate, disposition: 'protected', reason: 'RECOVERY_STAGING_NOT_SAFE_TO_REMOVE' };
363
- }
364
225
  try {
365
226
  if (candidate.kind === 'EMPTY-AUX-PARENT') {
366
227
  if (fs.readdirSync(candidate.path).length !== 0)
@@ -413,7 +274,7 @@ function cleanupIntermediateUnderRemovalCoordinator(repoRoot, options = {}) {
413
274
  continue;
414
275
  const current = refreshedByKey.get(intermediateCleanupKey(initialCandidate));
415
276
  items[index] = current
416
- ? removeCandidate(repoRoot, current, tasks.get(taskId))
277
+ ? removeCandidate(repoRoot, current)
417
278
  : { ...initialCandidate, disposition: 'protected', reason: 'CANDIDATE_NO_LONGER_ELIGIBLE' };
418
279
  }
419
280
  };
@@ -6,8 +6,10 @@ import { parseArtifactName } from './artifact-name.js';
6
6
  import { LEDGER_COLUMNS, LEDGER_HEADINGS, LEDGER_SECTION_MISSING_CODE, LEDGER_SECTION_MISSING_MESSAGE, nextHdId, parseLedgerDocument, validateLedgerRows } from './ledger.js';
7
7
  import { resolveTaskRef } from './resolve-ref.js';
8
8
  import { writeTask } from './write.js';
9
- import { allowsManualOverride } from './guard-override.js';
10
- import { parseReworkIntentDocument, reworkIntentMutation, upsertReworkIntent } from './rework-intent.js';
9
+ import { parseLegacyReworkIntentDocument, parseReworkIntentDocument, reworkIntentMutation, upsertReworkIntent } from './rework-intent.js';
10
+ import { extractSection, extractSubSection } from './sections.js';
11
+ import { parseLifecyclePathDecision } from './lifecycle-path.js';
12
+ import { scanVisibleMarkdown } from './markdown.js';
11
13
  const PREFIX = { analysis: 'AN', plan: 'PL', code: 'CD' };
12
14
  const RESPONSE = new Set(['accepted', 'adjusted', 'refuted', 'cannot-judge']);
13
15
  const DISPOSITION = new Set(['confirmed', 'closed', 'open', 'needs-human-decision']);
@@ -23,15 +25,50 @@ function ledgerReadErrorCode(error) {
23
25
  return code;
24
26
  return code === 'TABLE_DUPLICATE_KEY' ? 'LEDGER_DUPLICATE_ID' : 'LEDGER_DOCUMENT_INVALID';
25
27
  }
26
- function maxHandshakeRounds(repoRoot) {
27
- try {
28
- const parsed = JSON.parse(fs.readFileSync(path.join(repoRoot, '.agents', '.airc.json'), 'utf8'));
29
- const value = parsed.review?.maxHandshakeRounds;
30
- return Number.isInteger(value) && Number(value) > 0 ? Number(value) : 3;
31
- }
32
- catch {
33
- return 3;
28
+ const CLASSIFICATION_TARGET = {
29
+ 'scope-or-requirement': 'analysis', design: 'plan', implementation: 'code',
30
+ 'human-decision': 'pause', 'insufficient-evidence': 'pause'
31
+ };
32
+ function semanticDigest(value) {
33
+ return createHash('sha256').update(value.normalize('NFKC').replace(/\s+/g, ' ').trim()).digest('hex');
34
+ }
35
+ function normalizedTaskFact(value) {
36
+ return value.normalize('NFKC').replace(/\s+/g, ' ').trim();
37
+ }
38
+ function semanticFindingEvidence(reviewContent, heading, end) {
39
+ const title = heading.text.replace(/^\d+[.、::]\s*/, '').trim();
40
+ return `${title}\n${reviewContent.slice(heading.end, end)}`;
41
+ }
42
+ function findingEvidence(reviewContent, evidence) {
43
+ const anchor = evidence.split('#')[1];
44
+ if (!anchor)
45
+ return null;
46
+ const markdown = scanVisibleMarkdown(reviewContent);
47
+ const explicit = markdown.anchors.find((candidate) => candidate.id === anchor);
48
+ if (explicit) {
49
+ const heading = markdown.headings.find((candidate) => candidate.start > explicit.start);
50
+ const next = heading && markdown.headings.find((candidate) => candidate.start > heading.start && candidate.level <= heading.level);
51
+ const end = next?.start ?? reviewContent.length;
52
+ return heading ? semanticFindingEvidence(reviewContent, heading, end) : reviewContent.slice(explicit.end, end);
34
53
  }
54
+ const heading = markdown.headings.find((candidate) => {
55
+ const escapedAnchor = anchor.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
56
+ return candidate.text === anchor || new RegExp(`^${escapedAnchor}(?:\\s|[.、::])`).test(candidate.text);
57
+ });
58
+ if (!heading)
59
+ return null;
60
+ const next = markdown.headings.find((candidate) => candidate.start > heading.start && candidate.level <= heading.level);
61
+ return semanticFindingEvidence(reviewContent, heading, next?.start ?? reviewContent.length);
62
+ }
63
+ function taskFactDigest(taskDir, content) {
64
+ const analysisNames = fs.readdirSync(taskDir).filter((name) => /^analysis(?:-r\d+)?\.md$/.test(name));
65
+ const latest = analysisNames.map((name) => ({ name, round: parseArtifactName(name)?.round ?? 0 })).sort((a, b) => b.round - a.round)[0];
66
+ const flow = latest ? parseLifecyclePathDecision(fs.readFileSync(path.join(taskDir, latest.name), 'utf8')) : null;
67
+ return semanticDigest(JSON.stringify({
68
+ taskInput: normalizedTaskFact(extractSection(content, ['任务输入', 'Task Input'])),
69
+ requirements: normalizedTaskFact(extractSection(content, ['需求', 'Requirements'])),
70
+ flow: flow?.status === 'valid' ? flow.decision.semanticDigest : flow?.status ?? 'missing'
71
+ }));
35
72
  }
36
73
  function nextFindingId(rows, stage) {
37
74
  const prefix = PREFIX[stage];
@@ -67,8 +104,7 @@ function applyLedgerIntent(intent, options = {}) {
67
104
  const resolved = resolveTaskRef(intent.taskRef, { repoRoot: options.repoRoot });
68
105
  if (!resolved.ok)
69
106
  return failed(intent, resolved.code, resolved.message, resolved.taskId);
70
- const stateOverride = allowsManualOverride(options.manualOverride, 'ledger-intent', 'TASK_STATE_MISMATCH');
71
- if (resolved.state !== 'active' && !stateOverride)
107
+ if (resolved.state !== 'active')
72
108
  return failed(intent, 'TASK_STATE_MISMATCH', `task ${resolved.taskId} is ${resolved.state}, expected active`, resolved.taskId);
73
109
  let content;
74
110
  let rows;
@@ -89,12 +125,15 @@ function applyLedgerIntent(intent, options = {}) {
89
125
  return { status: 'no-op', changed: false, intent: intent.kind, taskId: resolved.taskId, entityId: nextHdId(rows), before: null, after: null, operations: [], error: null };
90
126
  }
91
127
  if (intent.kind === 'rework-intent-upsert') {
92
- if (!/^RI-[1-9]\d*$/.test(intent.intentId) || !/^(AN|PL|CD)-[1-9]\d*$/.test(intent.findingId) || !['analysis', 'plan', 'code'].includes(intent.target) || !/^[a-f0-9]{64}$/.test(intent.sourceSha256)) {
93
- return failed(intent, 'LEDGER_PAYLOAD_INVALID', 'rework intent identity, target, or source hash is invalid', resolved.taskId, intent.intentId);
128
+ if (!/^RI-[1-9]\d*$/.test(intent.intentId) || !/^(AN|PL|CD)-[1-9]\d*$/.test(intent.findingId) || !(intent.classification in CLASSIFICATION_TARGET) || !/^[a-f0-9]{64}$/.test(intent.sourceSha256)) {
129
+ return failed(intent, 'LEDGER_PAYLOAD_INVALID', 'rework intent identity, classification, or source hash is invalid', resolved.taskId, intent.intentId);
94
130
  }
95
131
  const finding = rows.find((row) => row.id === intent.findingId);
96
132
  if (!finding || finding.severity === 'decision')
97
133
  return failed(intent, 'LEDGER_NOT_FOUND', `finding '${intent.findingId}' was not found`, resolved.taskId, intent.findingId);
134
+ if (intent.classification === 'human-decision' && finding.status !== 'needs-human-decision') {
135
+ return failed(intent, 'LEDGER_TRANSITION_INVALID', 'human-decision classification requires a needs-human-decision finding', resolved.taskId, intent.findingId);
136
+ }
98
137
  const sourceFromEvidence = finding.evidence.split('#')[0];
99
138
  if (sourceFromEvidence !== intent.sourceArtifact || parseArtifactName(intent.sourceArtifact)?.family !== `review-${finding.stage}`) {
100
139
  return failed(intent, 'LEDGER_IDENTITY_CONFLICT', 'rework intent source artifact does not match finding evidence', resolved.taskId, intent.intentId);
@@ -106,12 +145,35 @@ function applyLedgerIntent(intent, options = {}) {
106
145
  const parsed = parseReworkIntentDocument(content);
107
146
  if (!parsed.ok)
108
147
  return failed(intent, parsed.code, parsed.message, resolved.taskId, intent.intentId);
109
- const nextIntent = {
148
+ const reviewContent = fs.readFileSync(path.join(resolved.taskDir, intent.sourceArtifact), 'utf8');
149
+ const evidenceBlock = findingEvidence(reviewContent, finding.evidence);
150
+ if (!evidenceBlock)
151
+ return failed(intent, 'LEDGER_EVIDENCE_INVALID', `finding evidence anchor '${finding.evidence}' was not found`, resolved.taskId, intent.intentId);
152
+ const evidenceDigest = semanticDigest(evidenceBlock);
153
+ const factsDigest = taskFactDigest(resolved.taskDir, content);
154
+ const requestedIntent = {
110
155
  intentId: intent.intentId, findingId: intent.findingId, sourceArtifact: intent.sourceArtifact,
111
- sourceSha256: intent.sourceSha256, target: intent.target, status: 'pending',
156
+ sourceSha256: intent.sourceSha256, target: CLASSIFICATION_TARGET[intent.classification], classification: intent.classification,
157
+ evidenceDigest, taskFactDigest: factsDigest, status: 'pending',
112
158
  declaredAt: new Date().toISOString(), consumedAt: ''
113
159
  };
114
- const next = upsertReworkIntent(parsed.intents, nextIntent);
160
+ if (parsed.intents.some((candidate) => candidate.intentId === intent.intentId)) {
161
+ const replay = upsertReworkIntent(parsed.intents, requestedIntent);
162
+ if (!replay.changed) {
163
+ return { status: 'no-op', changed: false, intent: intent.kind, taskId: resolved.taskId, entityId: intent.intentId, before: null, after: null, operations: [], error: null };
164
+ }
165
+ }
166
+ const same = parsed.intents.find((candidate) => candidate.findingId === intent.findingId
167
+ && candidate.evidenceDigest === evidenceDigest && candidate.taskFactDigest === factsDigest
168
+ && candidate.classification === intent.classification && candidate.target === CLASSIFICATION_TARGET[intent.classification]);
169
+ const classification = same ? 'insufficient-evidence' : intent.classification;
170
+ const nextIntent = {
171
+ ...requestedIntent, target: CLASSIFICATION_TARGET[classification], classification
172
+ };
173
+ const now = nextIntent.declaredAt;
174
+ const superseded = parsed.intents.map((candidate) => candidate.status === 'pending'
175
+ ? { ...candidate, status: 'superseded', consumedAt: now } : candidate);
176
+ const next = upsertReworkIntent(superseded, nextIntent);
115
177
  if (!next.changed)
116
178
  return { status: 'no-op', changed: false, intent: intent.kind, taskId: resolved.taskId, entityId: intent.intentId, before: null, after: null, operations: [], error: null };
117
179
  const result = writeTask({ taskRef: intent.taskRef, expectedState: 'active', dryRun: intent.dryRun, mutations: [reworkIntentMutation(content, next.intents)] }, { ...options, taskLocation: { repoRoot: resolved.repoRoot, taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, state: resolved.state } });
@@ -123,6 +185,84 @@ function applyLedgerIntent(intent, options = {}) {
123
185
  return failed(intent, 'LEDGER_IDENTITY_CONFLICT', error instanceof Error ? error.message : String(error), resolved.taskId, intent.intentId);
124
186
  }
125
187
  }
188
+ if (intent.kind === 'rework-intent-rebuild') {
189
+ const beforeSha256 = createHash('sha256').update(content).digest('hex');
190
+ const supplied = [intent.findingId, intent.sourceArtifact, intent.sourceSha256, intent.classification].filter((value) => value !== undefined).length;
191
+ if (supplied !== 0 && supplied !== 4)
192
+ return failed(intent, 'LEDGER_PAYLOAD_INVALID', 'rebuild binding options must be provided together', resolved.taskId);
193
+ const current = parseReworkIntentDocument(content);
194
+ if (current.ok && current.present) {
195
+ if (supplied !== 0)
196
+ return failed(intent, 'LEDGER_PAYLOAD_INVALID', 'current rework intent format does not accept rebuild binding options', resolved.taskId);
197
+ return {
198
+ status: 'no-op', changed: false, intent: intent.kind, taskId: resolved.taskId, entityId: null,
199
+ before: null, after: null, operations: [], error: null,
200
+ rebuild: { mode: 'format-only', beforeSha256, afterSha256: beforeSha256, converted: current.intents.length, newIntentId: null }
201
+ };
202
+ }
203
+ const legacy = parseLegacyReworkIntentDocument(content);
204
+ if (!legacy.ok)
205
+ return failed(intent, legacy.code, legacy.message, resolved.taskId);
206
+ const pending = legacy.intents.filter((row) => row.status === 'pending');
207
+ if (pending.length === 0 && supplied !== 0)
208
+ return failed(intent, 'LEDGER_PAYLOAD_INVALID', 'format-only rebuild does not accept finding or classification options', resolved.taskId);
209
+ if (pending.length > 0 && supplied !== 4)
210
+ return failed(intent, 'LEDGER_PAYLOAD_INVALID', 'legacy pending intents require finding, review, hash, and classification', resolved.taskId);
211
+ const now = new Date().toISOString();
212
+ let next = legacy.intents.map((row) => row.status === 'pending' ? { ...row, status: 'superseded', consumedAt: now } : row);
213
+ let entityId = null;
214
+ if (pending.length > 0) {
215
+ const finding = rows.find((row) => row.id === intent.findingId);
216
+ if (!finding || finding.severity === 'decision' || finding.evidence.split('#')[0] !== intent.sourceArtifact)
217
+ return failed(intent, 'LEDGER_IDENTITY_CONFLICT', 'rebuild binding does not match a review finding', resolved.taskId);
218
+ if (intent.classification === 'human-decision' && finding.status !== 'needs-human-decision') {
219
+ return failed(intent, 'LEDGER_TRANSITION_INVALID', 'human-decision classification requires a needs-human-decision finding', resolved.taskId);
220
+ }
221
+ const sourceIdentity = parseArtifactName(intent.sourceArtifact);
222
+ const expectedFamily = `review-${finding.stage}`;
223
+ const latestSource = fs.readdirSync(resolved.taskDir)
224
+ .map((name) => parseArtifactName(name))
225
+ .filter((identity) => identity?.family === expectedFamily)
226
+ .sort((left, right) => right.round - left.round || left.name.localeCompare(right.name))[0];
227
+ if (!sourceIdentity || sourceIdentity.family !== expectedFamily || latestSource?.name !== sourceIdentity.name) {
228
+ return failed(intent, 'LEDGER_IDENTITY_CONFLICT', 'rebuild source artifact is not the latest review for the finding stage', resolved.taskId);
229
+ }
230
+ const sourcePath = path.join(resolved.taskDir, intent.sourceArtifact);
231
+ const actualHash = createHash('sha256').update(fs.readFileSync(sourcePath)).digest('hex');
232
+ if (actualHash !== intent.sourceSha256)
233
+ return failed(intent, 'LEDGER_IDENTITY_CONFLICT', 'rebuild source artifact hash does not match', resolved.taskId);
234
+ const max = next.reduce((value, row) => Math.max(value, Number(row.intentId.slice(3))), 0);
235
+ entityId = `RI-${max + 1}`;
236
+ const reviewContent = fs.readFileSync(sourcePath, 'utf8');
237
+ const classification = intent.classification;
238
+ if (!(classification in CLASSIFICATION_TARGET))
239
+ return failed(intent, 'LEDGER_PAYLOAD_INVALID', 'rebuild classification is invalid', resolved.taskId);
240
+ const evidenceBlock = findingEvidence(reviewContent, finding.evidence);
241
+ if (!evidenceBlock)
242
+ return failed(intent, 'LEDGER_EVIDENCE_INVALID', `finding evidence anchor '${finding.evidence}' was not found`, resolved.taskId);
243
+ next = [...next, {
244
+ intentId: entityId, findingId: intent.findingId, sourceArtifact: intent.sourceArtifact, sourceSha256: intent.sourceSha256,
245
+ target: CLASSIFICATION_TARGET[classification], classification,
246
+ evidenceDigest: semanticDigest(evidenceBlock),
247
+ taskFactDigest: taskFactDigest(resolved.taskDir, content), status: 'pending', declaredAt: now, consumedAt: ''
248
+ }];
249
+ }
250
+ const result = writeTask({ taskRef: intent.taskRef, expectedState: 'active', dryRun: intent.dryRun, mutations: [reworkIntentMutation(content, next)] }, { ...options, taskLocation: { repoRoot: resolved.repoRoot, taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, state: resolved.state } });
251
+ if (result.status === 'failed')
252
+ return failed(intent, result.error.code, result.error.message, result.taskId, entityId);
253
+ const afterContent = result.status === 'applied' || result.status === 'no-op' ? fs.readFileSync(resolved.taskMdPath, 'utf8') : content;
254
+ return {
255
+ status: result.status, changed: result.changed, intent: intent.kind, taskId: result.taskId, entityId,
256
+ before: null, after: null, operations: result.operations, error: null,
257
+ rebuild: {
258
+ mode: pending.length > 0 ? 'pending-reclassification' : 'format-only',
259
+ beforeSha256,
260
+ afterSha256: createHash('sha256').update(afterContent).digest('hex'),
261
+ converted: legacy.intents.length,
262
+ newIntentId: entityId
263
+ }
264
+ };
265
+ }
126
266
  let before = null;
127
267
  let after;
128
268
  if (intent.kind === 'finding-upsert') {
@@ -199,8 +339,7 @@ function applyLedgerIntent(intent, options = {}) {
199
339
  ? new Set(['confirmed', 'open', 'needs-human-decision'])
200
340
  : before.status === 'cannot-judge' ? new Set(['open', 'needs-human-decision']) : new Set();
201
341
  const sameRoundMinorClose = before.status === 'open' && before.severity === 'minor' && intent.status === 'closed';
202
- if (((!sameRoundMinorClose && !allowed.has(intent.status)) || (intent.status === 'open' && Number(before.round) >= maxHandshakeRounds(resolved.repoRoot)))
203
- && !allowsManualOverride(options.manualOverride, 'ledger-intent', 'LEDGER_TRANSITION_INVALID')) {
342
+ if (!sameRoundMinorClose && !allowed.has(intent.status)) {
204
343
  return failed(intent, 'LEDGER_TRANSITION_INVALID', `finding '${intent.id}' cannot transition from ${before.status} to ${intent.status}`, resolved.taskId, intent.id);
205
344
  }
206
345
  after = { ...before, status: intent.status, evidence };
@@ -233,7 +372,7 @@ function applyLedgerIntent(intent, options = {}) {
233
372
  }
234
373
  }
235
374
  const result = writeTask({
236
- taskRef: intent.taskRef, expectedState: stateOverride ? resolved.state : 'active',
375
+ taskRef: intent.taskRef, expectedState: 'active',
237
376
  mutations: [rowMutation(after), ...(implementationMutation ? [implementationMutation] : [])],
238
377
  dryRun: 'dryRun' in intent ? intent.dryRun : false
239
378
  }, { ...options, taskLocation: { repoRoot: resolved.repoRoot, taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, state: resolved.state } });
@@ -0,0 +1,75 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { extractSection } from './sections.js';
3
+ const PATH_STAGES = {
4
+ streamlined: ['analysis', 'code', 'review-code'],
5
+ standard: ['analysis', 'plan', 'code', 'review-code'],
6
+ full: ['analysis', 'review-analysis', 'plan', 'review-plan', 'code', 'review-code']
7
+ };
8
+ const FIELD_ALIASES = {
9
+ path: ['本任务路径', 'Path'],
10
+ basis: ['判定依据', 'Basis'],
11
+ unmetHigherConditions: ['未满足的更高路径条件', '未满足的较低路径条件', 'Unmet Higher-path Conditions'],
12
+ upgradeTriggers: ['升级触发条件', 'Upgrade Triggers']
13
+ };
14
+ function normalizeText(value) {
15
+ return value.normalize('NFKC').replace(/\s+/g, ' ').trim().replace(/[。.]$/, '');
16
+ }
17
+ function digest(value) {
18
+ return createHash('sha256').update(JSON.stringify(value)).digest('hex');
19
+ }
20
+ function pathToken(value) {
21
+ const normalized = normalizeText(value).toLowerCase().replace(/[`*_]/g, '');
22
+ if (/^(?:streamlined|精简路径|精简)$/.test(normalized))
23
+ return 'streamlined';
24
+ if (/^(?:standard|标准路径|标准)$/.test(normalized))
25
+ return 'standard';
26
+ if (/^(?:full|完整路径|完整)$/.test(normalized))
27
+ return 'full';
28
+ return null;
29
+ }
30
+ function fieldValues(body, aliases) {
31
+ const wanted = new Set(aliases);
32
+ const values = [];
33
+ for (const line of body.split(/\r?\n/)) {
34
+ const match = /^\s*[-*]\s+\*\*([^*]+)\*\*[::]\s*(.*?)\s*$/.exec(line);
35
+ if (match && wanted.has(match[1].trim()))
36
+ values.push(match[2].trim());
37
+ }
38
+ return values;
39
+ }
40
+ function parseLifecyclePathDecision(analysisContent, sourceArtifact = '', sourceSha256 = '') {
41
+ const body = extractSection(analysisContent, ['流程裁定', 'Flow Decision']);
42
+ if (!body)
43
+ return { status: 'missing', decision: null, message: 'analysis flow decision section is missing' };
44
+ const fields = Object.fromEntries(Object.entries(FIELD_ALIASES).map(([key, aliases]) => [key, fieldValues(body, aliases)]));
45
+ const invalid = Object.entries(fields).find(([, values]) => values.length !== 1);
46
+ if (invalid)
47
+ return { status: 'invalid', decision: null, message: `flow decision field '${invalid[0]}' must appear exactly once` };
48
+ const path = pathToken(fields.path[0]);
49
+ if (!path)
50
+ return { status: 'invalid', decision: null, message: `unknown lifecycle path '${fields.path[0]}'` };
51
+ const basis = normalizeText(fields.basis[0]);
52
+ const unmetHigherConditions = normalizeText(fields.unmetHigherConditions[0]);
53
+ const upgradeTriggers = normalizeText(fields.upgradeTriggers[0]);
54
+ if (!basis || !unmetHigherConditions || !upgradeTriggers)
55
+ return { status: 'invalid', decision: null, message: 'flow decision fields must be non-empty' };
56
+ return {
57
+ status: 'valid',
58
+ message: null,
59
+ decision: {
60
+ path,
61
+ stages: PATH_STAGES[path],
62
+ basis,
63
+ unmetHigherConditions,
64
+ upgradeTriggers,
65
+ semanticDigest: digest({ path, basis, unmetHigherConditions, upgradeTriggers }),
66
+ sourceArtifact,
67
+ sourceSha256
68
+ }
69
+ };
70
+ }
71
+ function pathIncludes(state, stage) {
72
+ return state?.status === 'valid' && state.decision.stages.includes(stage);
73
+ }
74
+ export { PATH_STAGES, parseLifecyclePathDecision, pathIncludes };
75
+ //# sourceMappingURL=lifecycle-path.js.map