@fitlab-ai/agent-infra 0.11.5 → 0.11.7

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 (155) hide show
  1. package/dist/lib/git/primary-worktree.js +58 -0
  2. package/dist/lib/merge.js +62 -10
  3. package/dist/lib/platform/github-provider.js +9 -14
  4. package/dist/lib/platform/github-release-notes.js +80 -11
  5. package/dist/lib/platform/none-provider.js +1 -1
  6. package/dist/lib/platform/provider-contract.d.ts +39 -16
  7. package/dist/lib/platform/provider-contract.js +1 -1
  8. package/dist/lib/platform/provider-validation.js +83 -13
  9. package/dist/lib/platform/pull-requests.js +29 -3
  10. package/dist/lib/platform/release-notes.js +7 -8
  11. package/dist/lib/sandbox/capture.js +1 -1
  12. package/dist/lib/sandbox/commands/create.js +46 -1
  13. package/dist/lib/sandbox/commands/enter.js +1 -1
  14. package/dist/lib/sandbox/commands/start.js +1 -1
  15. package/dist/lib/sandbox/config.js +10 -0
  16. package/dist/lib/sandbox/control/lifecycle.js +18 -13
  17. package/dist/lib/sandbox/control/server.js +6 -2
  18. package/dist/lib/sandbox/task-resolver.js +62 -2
  19. package/dist/lib/server/config.js +7 -27
  20. package/dist/lib/server/daemon.js +4 -3
  21. package/dist/lib/server/process-control.js +4 -2
  22. package/dist/lib/server/runner.js +4 -3
  23. package/dist/lib/task/artifact-lifecycle.js +42 -10
  24. package/dist/lib/task/artifact-receipts.js +62 -22
  25. package/dist/lib/task/capabilities.js +22 -4
  26. package/dist/lib/task/completion-facts.js +56 -0
  27. package/dist/lib/task/create-service.js +6 -2
  28. package/dist/lib/task/create.js +46 -82
  29. package/dist/lib/task/events.js +268 -195
  30. package/dist/lib/task/finalization-handoff.js +2 -1
  31. package/dist/lib/task/invalidation-command.js +4 -2
  32. package/dist/lib/task/invalidation.js +38 -9
  33. package/dist/lib/task/local-artifact-finalization.js +17 -3
  34. package/dist/lib/task/qualification-audit.js +23 -105
  35. package/dist/lib/task/resolve-ref.js +3 -3
  36. package/dist/lib/task/review-finalization.js +7 -0
  37. package/dist/lib/task/review-identity.js +69 -0
  38. package/dist/lib/task/sections.js +6 -5
  39. package/dist/lib/task/task-execution-lock.js +6 -3
  40. package/dist/lib/task/verification-engine.js +14 -63
  41. package/dist/lib/task/verification.js +35 -38
  42. package/dist/lib/task/write.js +121 -1
  43. package/dist/lifecycle-build-manifest.json +3 -3
  44. package/lib/git/primary-worktree.ts +62 -0
  45. package/lib/merge.ts +59 -10
  46. package/lib/platform/github-provider.ts +11 -18
  47. package/lib/platform/github-release-notes.ts +89 -14
  48. package/lib/platform/none-provider.ts +1 -1
  49. package/lib/platform/provider-contract.ts +38 -12
  50. package/lib/platform/provider-validation.ts +83 -12
  51. package/lib/platform/pull-requests.ts +27 -3
  52. package/lib/platform/release-notes.ts +11 -12
  53. package/lib/sandbox/capture.ts +1 -1
  54. package/lib/sandbox/commands/create.ts +52 -1
  55. package/lib/sandbox/commands/enter.ts +1 -1
  56. package/lib/sandbox/commands/start.ts +1 -1
  57. package/lib/sandbox/config.ts +12 -0
  58. package/lib/sandbox/control/lifecycle.ts +22 -13
  59. package/lib/sandbox/control/server.ts +5 -2
  60. package/lib/sandbox/task-resolver.ts +51 -2
  61. package/lib/server/config.ts +7 -29
  62. package/lib/server/daemon.ts +5 -3
  63. package/lib/server/process-control.ts +4 -2
  64. package/lib/server/runner.ts +7 -4
  65. package/lib/server/server.schema.json +0 -4
  66. package/lib/task/artifact-lifecycle.ts +35 -11
  67. package/lib/task/artifact-receipts.ts +55 -15
  68. package/lib/task/capabilities.ts +17 -3
  69. package/lib/task/completion-facts.ts +66 -0
  70. package/lib/task/create-service.ts +7 -3
  71. package/lib/task/create.ts +44 -71
  72. package/lib/task/events.ts +248 -182
  73. package/lib/task/finalization-handoff.ts +2 -1
  74. package/lib/task/invalidation-command.ts +4 -2
  75. package/lib/task/invalidation.ts +35 -10
  76. package/lib/task/local-artifact-finalization.ts +13 -3
  77. package/lib/task/qualification-audit.ts +21 -114
  78. package/lib/task/resolve-ref.ts +3 -3
  79. package/lib/task/review-finalization.ts +7 -0
  80. package/lib/task/review-identity.ts +85 -0
  81. package/lib/task/sections.ts +6 -6
  82. package/lib/task/task-execution-lock.ts +7 -3
  83. package/lib/task/verification-engine.ts +13 -86
  84. package/lib/task/verification.ts +34 -38
  85. package/lib/task/write.ts +115 -1
  86. package/package.json +1 -1
  87. package/templates/.agents/rules/README.en.md +1 -0
  88. package/templates/.agents/rules/README.zh-CN.md +1 -0
  89. package/templates/.agents/rules/decision-qualification.en.md +4 -3
  90. package/templates/.agents/rules/decision-qualification.zh-CN.md +4 -3
  91. package/templates/.agents/rules/evidence-reporting.en.md +1 -2
  92. package/templates/.agents/rules/evidence-reporting.zh-CN.md +1 -2
  93. package/templates/.agents/rules/validation-output.en.md +19 -0
  94. package/templates/.agents/rules/validation-output.zh-CN.md +19 -0
  95. package/templates/.agents/skills/analyze-task/SKILL.en.md +3 -1
  96. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +3 -3
  97. package/templates/.agents/skills/archive-tasks/SKILL.en.md +2 -2
  98. package/templates/.agents/skills/archive-tasks/SKILL.zh-CN.md +2 -2
  99. package/templates/.agents/skills/archive-tasks/scripts/archive-tasks.sh +109 -6
  100. package/templates/.agents/skills/block-task/SKILL.en.md +2 -0
  101. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +1 -1
  102. package/templates/.agents/skills/cancel-task/SKILL.en.md +2 -0
  103. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +1 -1
  104. package/templates/.agents/skills/code-task/SKILL.en.md +3 -1
  105. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +2 -2
  106. package/templates/.agents/skills/code-task/reference/report-template.en.md +4 -8
  107. package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +4 -8
  108. package/templates/.agents/skills/commit/SKILL.en.md +1 -1
  109. package/templates/.agents/skills/commit/SKILL.zh-CN.md +1 -1
  110. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +1 -1
  111. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +1 -1
  112. package/templates/.agents/skills/complete-task/SKILL.en.md +2 -0
  113. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +2 -0
  114. package/templates/.agents/skills/create-pr/SKILL.en.md +2 -0
  115. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +1 -1
  116. package/templates/.agents/skills/create-release-note/SKILL.en.md +17 -11
  117. package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +17 -11
  118. package/templates/.agents/skills/create-task/SKILL.en.md +2 -0
  119. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +1 -1
  120. package/templates/.agents/skills/import-codescan/SKILL.en.md +2 -0
  121. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +1 -1
  122. package/templates/.agents/skills/import-dependabot/SKILL.en.md +2 -0
  123. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +1 -1
  124. package/templates/.agents/skills/import-issue/SKILL.en.md +2 -0
  125. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +1 -1
  126. package/templates/.agents/skills/plan-task/SKILL.en.md +3 -1
  127. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +3 -3
  128. package/templates/.agents/skills/review-analysis/SKILL.en.md +3 -1
  129. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +3 -1
  130. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +4 -8
  131. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +4 -8
  132. package/templates/.agents/skills/review-code/SKILL.en.md +13 -12
  133. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +11 -12
  134. package/templates/.agents/skills/review-code/config/verify.en.json +1 -3
  135. package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -3
  136. package/templates/.agents/skills/review-code/reference/output-templates.en.md +23 -46
  137. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +27 -48
  138. package/templates/.agents/skills/review-code/reference/report-template.en.md +8 -11
  139. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +8 -11
  140. package/templates/.agents/skills/review-code/reference/review-criteria.en.md +2 -2
  141. package/templates/.agents/skills/review-code/reference/review-criteria.zh-CN.md +2 -2
  142. package/templates/.agents/skills/review-plan/SKILL.en.md +3 -1
  143. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +3 -1
  144. package/templates/.agents/skills/review-plan/reference/report-template.en.md +5 -9
  145. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +5 -9
  146. package/templates/.agents/skills/review-pr/SKILL.en.md +2 -0
  147. package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +2 -0
  148. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +1 -1
  149. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +1 -1
  150. package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
  151. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
  152. package/templates/.agents/skills/watch-pr/SKILL.en.md +2 -0
  153. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +1 -1
  154. package/templates/.agents/templates/task.en.md +2 -2
  155. package/templates/.agents/templates/task.zh-CN.md +2 -2
@@ -5,17 +5,21 @@ import { extractSection, findSectionHeading, parseTable } from './sections.js';
5
5
  const RECEIPT_SECTION_ALIASES = ['产物生命周期收据', 'Artifact Lifecycle Receipts'];
6
6
  const RECEIPT_COLUMNS = ['event', 'output', 'input', 'input_sha256', 'completed_at'];
7
7
  const RECEIPT_EVENTS = new Set([
8
+ 'analysis.completed',
9
+ 'plan.completed',
8
10
  'review-analysis.completed',
9
11
  'review-plan.completed',
12
+ 'code.completed',
10
13
  'review-code.completed',
11
- 'code.completed'
12
14
  ]);
13
15
  const SHA256_RE = /^[a-f0-9]{64}$/;
14
16
  const RECEIPT_SHAPES = {
17
+ 'analysis.completed': { output: 'analysis', input: 'review-analysis' },
18
+ 'plan.completed': { output: 'plan', input: 'analysis' },
15
19
  'review-analysis.completed': { output: 'review-analysis', input: 'analysis' },
16
20
  'review-plan.completed': { output: 'review-plan', input: 'plan' },
17
- 'review-code.completed': { output: 'review-code', input: 'code' },
18
- 'code.completed': { output: 'code', input: 'plan' }
21
+ 'code.completed': { output: 'code', input: 'plan' },
22
+ 'review-code.completed': { output: 'review-code', input: 'code' }
19
23
  };
20
24
  class ArtifactReceiptError extends Error {
21
25
  code = 'ARTIFACT_RECEIPT_INVALID';
@@ -37,8 +41,16 @@ function validateReceiptShape(event, output, input) {
37
41
  throw new ArtifactReceiptError(`receipt artifact identity is invalid: ${output} -> ${input}`);
38
42
  const shape = RECEIPT_SHAPES[event];
39
43
  const inputMatches = event === 'code.completed'
40
- ? inputIdentity.family === 'analysis' || inputIdentity.family === 'plan'
41
- : inputIdentity.family === shape.input;
44
+ ? inputIdentity.family === 'analysis' || inputIdentity.family === 'plan' || inputIdentity.family === 'review-analysis' || inputIdentity.family === 'review-plan' || inputIdentity.family === 'review-code'
45
+ : event === 'plan.completed'
46
+ ? inputIdentity.family === 'analysis' || inputIdentity.family === 'review-analysis' || inputIdentity.family === 'review-plan'
47
+ : event === 'analysis.completed'
48
+ ? inputIdentity.family === 'review-analysis'
49
+ : event === 'review-plan.completed'
50
+ ? inputIdentity.family === 'plan' || inputIdentity.family === 'review-analysis'
51
+ : event === 'review-code.completed'
52
+ ? inputIdentity.family === 'code' || inputIdentity.family === 'plan' || inputIdentity.family === 'review-plan'
53
+ : inputIdentity.family === shape.input;
42
54
  if (outputIdentity.family !== shape.output || !inputMatches) {
43
55
  throw new ArtifactReceiptError(`receipt event '${event}' does not match ${output} -> ${input}`);
44
56
  }
@@ -52,7 +64,9 @@ function parseArtifactReceipts(content) {
52
64
  table = parseTable(content, {
53
65
  sectionAliases: [...RECEIPT_SECTION_ALIASES],
54
66
  columns: [...RECEIPT_COLUMNS],
55
- keyColumn: 'output'
67
+ // A completed artifact can have multiple lifecycle inputs. Pair
68
+ // uniqueness is checked below because the table parser supports one key.
69
+ keyColumn: null
56
70
  });
57
71
  }
58
72
  catch (error) {
@@ -84,29 +98,55 @@ function parseArtifactReceipts(content) {
84
98
  completedAt
85
99
  };
86
100
  });
101
+ const edges = new Set();
102
+ for (const row of rows) {
103
+ const edge = `${row.output}\0${row.input}`;
104
+ if (edges.has(edge))
105
+ throw new ArtifactReceiptError(`duplicate receipt edge '${row.output}' <- '${row.input}'`);
106
+ edges.add(edge);
107
+ }
87
108
  return { present: true, rows };
88
109
  }
110
+ function receiptsForOutput(content, output) {
111
+ return parseArtifactReceipts(content).rows.filter((row) => row.output === output);
112
+ }
89
113
  function receiptForOutput(content, output) {
90
- const parsed = parseArtifactReceipts(content);
91
- return parsed.rows.find((row) => row.output === output) ?? null;
114
+ return receiptsForOutput(content, output)[0] ?? null;
92
115
  }
93
116
  function upsertArtifactReceipt(content, receipt) {
94
- if (!RECEIPT_EVENTS.has(receipt.event))
95
- throw new ArtifactReceiptError(`unknown receipt event '${receipt.event}'`);
96
- if (!receipt.output || !receipt.input)
97
- throw new ArtifactReceiptError('receipt output and input are required');
98
- validateReceiptShape(receipt.event, receipt.output, receipt.input);
99
- if (!SHA256_RE.test(receipt.inputSha256))
100
- throw new ArtifactReceiptError(`receipt digest for '${receipt.output}' is invalid`);
101
- if (!receipt.completedAt || Number.isNaN(Date.parse(receipt.completedAt.replace(' ', 'T')))) {
102
- throw new ArtifactReceiptError(`receipt completion time for '${receipt.output}' is invalid`);
117
+ return upsertArtifactReceipts(content, [receipt]);
118
+ }
119
+ function upsertArtifactReceipts(content, receiptsToAdd) {
120
+ if (receiptsToAdd.length === 0)
121
+ throw new ArtifactReceiptError('at least one receipt is required');
122
+ const additions = new Map();
123
+ for (const receipt of receiptsToAdd) {
124
+ if (!RECEIPT_EVENTS.has(receipt.event))
125
+ throw new ArtifactReceiptError(`unknown receipt event '${receipt.event}'`);
126
+ if (!receipt.output || !receipt.input)
127
+ throw new ArtifactReceiptError('receipt output and input are required');
128
+ validateReceiptShape(receipt.event, receipt.output, receipt.input);
129
+ if (!SHA256_RE.test(receipt.inputSha256))
130
+ throw new ArtifactReceiptError(`receipt digest for '${receipt.output}' is invalid`);
131
+ if (!receipt.completedAt || Number.isNaN(Date.parse(receipt.completedAt.replace(' ', 'T')))) {
132
+ throw new ArtifactReceiptError(`receipt completion time for '${receipt.output}' is invalid`);
133
+ }
134
+ const key = `${receipt.output}\0${receipt.input}`;
135
+ const duplicate = additions.get(key);
136
+ if (duplicate && JSON.stringify(duplicate) !== JSON.stringify(receipt))
137
+ throw new ArtifactReceiptError(`receipt for '${receipt.output}' from '${receipt.input}' has conflicting evidence`);
138
+ additions.set(key, receipt);
103
139
  }
104
140
  const existing = parseArtifactReceipts(content).rows;
105
- const previous = existing.find((row) => row.output === receipt.output);
106
- if (previous && JSON.stringify(previous) !== JSON.stringify(receipt)) {
107
- throw new ArtifactReceiptError(`receipt for '${receipt.output}' already exists with different evidence`);
141
+ for (const receipt of additions.values()) {
142
+ const previous = existing.find((row) => row.output === receipt.output && row.input === receipt.input);
143
+ if (previous && JSON.stringify(previous) !== JSON.stringify(receipt))
144
+ throw new ArtifactReceiptError(`receipt for '${receipt.output}' from '${receipt.input}' already exists with different evidence`);
108
145
  }
109
- const rows = previous ? existing : [...existing, receipt];
146
+ const rows = [...existing];
147
+ for (const receipt of additions.values())
148
+ if (!rows.some((row) => row.output === receipt.output && row.input === receipt.input))
149
+ rows.push(receipt);
110
150
  const heading = findSectionHeading(content, [...RECEIPT_SECTION_ALIASES]);
111
151
  const body = [
112
152
  `| ${RECEIPT_COLUMNS.join(' | ')} |`,
@@ -115,5 +155,5 @@ function upsertArtifactReceipt(content, receipt) {
115
155
  ].join('\n');
116
156
  return { aliases: [...RECEIPT_SECTION_ALIASES], heading, body };
117
157
  }
118
- export { ArtifactReceiptError, RECEIPT_SECTION_ALIASES, parseArtifactReceipts, receiptForOutput, sha256Bytes, sha256File, upsertArtifactReceipt };
158
+ export { ArtifactReceiptError, RECEIPT_SECTION_ALIASES, parseArtifactReceipts, receiptForOutput, receiptsForOutput, sha256Bytes, sha256File, upsertArtifactReceipt, upsertArtifactReceipts };
119
159
  //# sourceMappingURL=artifact-receipts.js.map
@@ -12,6 +12,8 @@ import { hasOpenLifecycleExecution } from './activity-log.js';
12
12
  import { parseQualificationAudit, parseTaskQualification } from './qualification-audit.js';
13
13
  import { parseLifecyclePathDecision, pathIncludes } from './lifecycle-path.js';
14
14
  import { parseImplementationInputs } from './implementation-inputs.js';
15
+ import { hasArtifactCompletionFact, hasArtifactCompletionLog } from './completion-facts.js';
16
+ import { inspectReviewIdentity } from './review-identity.js';
15
17
  const ARTIFACT_AUDIT_FAMILIES = new Set(['analysis', 'review-analysis', 'plan', 'review-plan', 'code', 'review-code']);
16
18
  const REASONS = new Set([
17
19
  'user-request', 'new-requirement', 'upstream-fact-doubt', 'review-finding', 'retry', 'validation-rerun'
@@ -139,13 +141,15 @@ function canStart(action, facts, trigger) {
139
141
  return allow('plan-review-approved');
140
142
  }
141
143
  if (action === 'review-code') {
142
- return hasArtifact(facts, 'code') ? allow('code-artifact') : deny('CODE_ARTIFACT_REQUIRED');
144
+ return allow(hasArtifact(facts, 'code') ? 'optional-code-context' : 'review-current-git-diff');
143
145
  }
144
146
  if (action === 'manual-validation' || action === 'validation-run') {
145
147
  if (!hasArtifact(facts, 'review-code'))
146
148
  return deny('CODE_REVIEW_REQUIRED');
147
- if (!reviewMatchesLatest(facts, 'code', 'review-code'))
149
+ if (hasArtifact(facts, 'code') && !reviewMatchesLatest(facts, 'code', 'review-code'))
148
150
  return deny('CODE_REVIEW_NOT_LATEST');
151
+ if (!facts.reviewCodeIdentityValid)
152
+ return deny('CODE_REVIEW_IDENTITY_INVALID');
149
153
  if (facts.reviews['review-code'] !== 'approved')
150
154
  return deny('CODE_REVIEW_NOT_APPROVED');
151
155
  if (facts.unresolvedLedger.code > 0)
@@ -309,7 +313,13 @@ function buildLifecycleFacts(taskDir, content, taskState = 'active', executionBu
309
313
  continue;
310
314
  }
311
315
  const audit = parseQualificationAudit(auditContent);
312
- if (!audit.ok || !audit.audit.present || !audit.audit.snapshot) {
316
+ if (!audit.ok) {
317
+ qualificationStaleArtifacts.push(name);
318
+ continue;
319
+ }
320
+ if (!audit.audit.present)
321
+ continue;
322
+ if (!audit.audit.snapshot) {
313
323
  qualificationStaleArtifacts.push(name);
314
324
  continue;
315
325
  }
@@ -324,6 +334,7 @@ function buildLifecycleFacts(taskDir, content, taskState = 'active', executionBu
324
334
  }
325
335
  const reviews = {};
326
336
  const reviewedInputs = {};
337
+ let reviewCodeIdentityValid = false;
327
338
  for (const family of ['review-analysis', 'review-plan', 'review-code']) {
328
339
  const latest = latestArtifact(artifacts[family] ?? []);
329
340
  if (!latest)
@@ -337,6 +348,12 @@ function buildLifecycleFacts(taskDir, content, taskState = 'active', executionBu
337
348
  && (artifacts[expectedFamily] ?? []).includes(input)
338
349
  && artifactHashes[input] === receipt.inputSha256)
339
350
  reviewedInputs[family] = input;
351
+ if (family === 'review-code') {
352
+ const identity = inspectReviewIdentity(taskDir, reviewContent);
353
+ reviewCodeIdentityValid = identity.status === 'matched'
354
+ && hasArtifactCompletionFact(content, path.join(taskDir, latest), 'review-code.completed')
355
+ && hasArtifactCompletionLog(content, latest, 'review-code.completed');
356
+ }
340
357
  const parsed = parseReviewSummary(reviewContent);
341
358
  if (!parsed.ok)
342
359
  continue;
@@ -407,7 +424,8 @@ function buildLifecycleFacts(taskDir, content, taskState = 'active', executionBu
407
424
  qualificationStaleArtifacts,
408
425
  pathState,
409
426
  reworkClassificationRequired,
410
- resolvedHumanDecisions
427
+ resolvedHumanDecisions,
428
+ reviewCodeIdentityValid
411
429
  };
412
430
  facts.recommendedAction = recommendNext(facts).action;
413
431
  return { ok: true, facts };
@@ -0,0 +1,56 @@
1
+ import fs from 'node:fs';
2
+ import { canonicalSemanticDigest } from './artifact-operations.js';
3
+ import { inspectActivityLog } from './activity-log.js';
4
+ import { parseTypedTaskFrontmatter } from './frontmatter.js';
5
+ import { parseArtifactName } from './artifact-name.js';
6
+ import { sha256File } from './artifact-receipts.js';
7
+ export function parseCompletionFacts(value) {
8
+ if (typeof value !== 'string')
9
+ return [];
10
+ try {
11
+ const parsed = JSON.parse(value);
12
+ if (!Array.isArray(parsed))
13
+ return [];
14
+ return parsed.filter((fact) => Boolean(fact && typeof fact === 'object' && !Array.isArray(fact)
15
+ && typeof fact.event === 'string'
16
+ && typeof fact.output === 'string'
17
+ && /^[a-f0-9]{64}$/u.test(fact.outputSha256)
18
+ && /^[a-f0-9]{64}$/u.test(fact.semanticDigest)
19
+ && typeof fact.requestId === 'string'
20
+ && typeof fact.result === 'string'));
21
+ }
22
+ catch {
23
+ return [];
24
+ }
25
+ }
26
+ export function hasArtifactCompletionFact(taskContent, artifactPath, event) {
27
+ try {
28
+ const frontmatter = parseTypedTaskFrontmatter(taskContent);
29
+ const output = artifactPath.split(/[\\/]/u).at(-1) ?? '';
30
+ const content = fs.readFileSync(artifactPath, 'utf8');
31
+ const outputSha256 = sha256File(artifactPath);
32
+ const semanticDigest = canonicalSemanticDigest(content);
33
+ return parseCompletionFacts(frontmatter.completion_facts).some((fact) => fact.event === event && fact.output === output
34
+ && fact.outputSha256 === outputSha256 && fact.semanticDigest === semanticDigest
35
+ && fact.requestId.length > 0);
36
+ }
37
+ catch {
38
+ return false;
39
+ }
40
+ }
41
+ export function hasArtifactCompletionLog(taskContent, artifact, event) {
42
+ const identity = parseArtifactName(artifact);
43
+ if (!identity)
44
+ return false;
45
+ const action = event === 'review-code.completed' ? 'Review Code'
46
+ : event === 'review-plan.completed' ? 'Review Plan'
47
+ : event === 'review-analysis.completed' ? 'Review Analysis' : null;
48
+ if (!action)
49
+ return false;
50
+ const { section, invalidEntries } = inspectActivityLog(taskContent);
51
+ if (!section || invalidEntries.length > 0)
52
+ return false;
53
+ const expected = `${action} (Round ${identity.round})`;
54
+ return section.entries.some((entry) => entry.step === expected && entry.note.includes(`→ ${artifact}`));
55
+ }
56
+ //# sourceMappingURL=completion-facts.js.map
@@ -48,9 +48,10 @@ export function parseTaskCreateResult(value) {
48
48
  if (!['applied', 'no-op', 'degraded', 'failed', 'blocked'].includes(result.status)
49
49
  || typeof result.changed !== 'boolean'
50
50
  || !result.task || typeof result.task !== 'object' || Array.isArray(result.task)
51
- || !exactKeys(result.task, ['id', 'shortId'])
51
+ || !exactKeys(result.task, ['id', 'shortId', 'state'])
52
52
  || !validTaskReference(result.task.id)
53
53
  || !validTaskReference(result.task.shortId)
54
+ || ![null, 'active', 'blocked', 'completed', 'archive'].includes(result.task.state)
54
55
  || !Array.isArray(result.operations)
55
56
  || !result.operations.every((operation) => {
56
57
  if (!operation || typeof operation !== 'object' || Array.isArray(operation))
@@ -93,7 +94,7 @@ export function taskCreateFailure(error, control) {
93
94
  return {
94
95
  status: error.retryable ? 'blocked' : 'failed',
95
96
  changed: false,
96
- task: { id: null, shortId: null },
97
+ task: { id: null, shortId: null, state: null },
97
98
  issue: null,
98
99
  operations: [],
99
100
  warnings: [],
@@ -156,6 +157,9 @@ async function createTask(value, options) {
156
157
  const operations = [{
157
158
  name: 'task:local', status: local.status, reasonCode: null
158
159
  }];
160
+ if (local.status === 'no-op' && local.task.state !== 'active') {
161
+ return { status: 'no-op', changed: false, task: local.task, issue: null, operations, warnings: [], error: null };
162
+ }
159
163
  let issue = null;
160
164
  const created = await dependencies.createIssue(local.task.id, { cwd: options.repoRoot, agent: candidate.agent });
161
165
  operations.push(...platformOperations('platform-create', created.operations));
@@ -1,5 +1,6 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
2
  import fs from 'node:fs';
3
+ import os from 'node:os';
3
4
  import path from 'node:path';
4
5
  import { isValidAgentInfraVersion, VERSION } from '../version.js';
5
6
  import { loadShortIdByTaskId, mutateShortIdRegistry } from './short-id.js';
@@ -7,6 +8,8 @@ import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-
7
8
  import { readDeliveryDefaults, validateBaseRef, validateRemote } from './delivery-target.js';
8
9
  import { buildUnboundFact, encodePrDeliveryFact } from './pr-delivery-fact.js';
9
10
  import { CANDIDATE_COLUMNS, CONSTRAINT_COLUMNS, parseTaskQualification } from './qualification-audit.js';
11
+ import { enumerateAllTaskDirs } from './resolve-ref.js';
12
+ import { parseTaskFrontmatter } from './frontmatter.js';
10
13
  const AGENTS = ['claude', 'codex', 'antigravity', 'opencode', 'cursor'];
11
14
  const TYPES = ['feature', 'bugfix', 'refactor', 'docs', 'chore'];
12
15
  const PRIORITIES = ['Urgent', 'High', 'Medium', 'Low'];
@@ -292,10 +295,27 @@ function ensureRealDirectory(directory, code) {
292
295
  }
293
296
  fs.mkdirSync(directory, { mode: 0o700 });
294
297
  }
295
- function withCreateLock(repoRoot, workspaceRoot, operation) {
296
- const lockRoot = path.join(workspaceRoot, '.task-create.lock');
297
- fs.mkdirSync(lockRoot, { recursive: true, mode: 0o700 });
298
- assertRealDirectory(lockRoot, 'TASK_CREATE_LOCK_FAILED');
298
+ function createLockRoot(project) {
299
+ const root = path.join(os.homedir(), '.agent-infra');
300
+ const directories = [root, path.join(root, 'run'), path.join(root, 'run', project), path.join(root, 'run', project, 'task-create-locks')];
301
+ for (const directory of directories) {
302
+ if (fs.existsSync(directory)) {
303
+ assertRealDirectory(directory, 'TASK_CREATE_LOCK_FAILED');
304
+ continue;
305
+ }
306
+ try {
307
+ fs.mkdirSync(directory, { mode: 0o700 });
308
+ }
309
+ catch (error) {
310
+ if (error.code !== 'EEXIST')
311
+ throw error;
312
+ assertRealDirectory(directory, 'TASK_CREATE_LOCK_FAILED');
313
+ }
314
+ }
315
+ return directories.at(-1);
316
+ }
317
+ function withCreateLock(repoRoot, project, operation) {
318
+ const lockRoot = createLockRoot(project);
299
319
  let callbackError;
300
320
  try {
301
321
  return withTaskExecutionLock(repoRoot, 'task-create', 'task-create', () => {
@@ -306,7 +326,7 @@ function withCreateLock(repoRoot, workspaceRoot, operation) {
306
326
  callbackError = error;
307
327
  throw error;
308
328
  }
309
- }, { lockRoot });
329
+ }, { lockRoot, identityScope: `task-create\0${project}` });
310
330
  }
311
331
  catch (error) {
312
332
  if (callbackError !== undefined)
@@ -318,45 +338,24 @@ function withCreateLock(repoRoot, workspaceRoot, operation) {
318
338
  throw new Error(`TASK_CREATE_LOCK_FAILED: ${message}`);
319
339
  }
320
340
  }
321
- function writeReceipt(receiptPath, value) {
322
- const temporary = `${receiptPath}.tmp.${process.pid}.${randomUUID()}`;
323
- fs.writeFileSync(temporary, `${JSON.stringify(value)}\n`, { flag: 'wx', mode: 0o600 });
324
- try {
325
- fs.renameSync(temporary, receiptPath);
326
- }
327
- catch (error) {
328
- try {
329
- fs.unlinkSync(temporary);
330
- }
331
- catch { /* best effort */ }
332
- throw error;
333
- }
334
- }
335
- function recoverPublishedTask(activeRoot, repoRoot, keyDigest, candidateDigest) {
336
- for (const name of fs.readdirSync(activeRoot)) {
337
- if (!/^TASK-\d{8}-\d{6}$/.test(name))
338
- continue;
339
- const taskDir = path.join(activeRoot, name);
340
- const stat = fs.lstatSync(taskDir);
341
- if (!stat.isDirectory() || stat.isSymbolicLink())
342
- continue;
343
- const taskMd = path.join(taskDir, 'task.md');
344
- if (!fs.existsSync(taskMd))
345
- continue;
341
+ function recoverPublishedTask(repoRoot, keyDigest, candidateDigest) {
342
+ for (const task of enumerateAllTaskDirs(repoRoot)) {
343
+ const taskMd = path.join(task.taskDir, 'task.md');
346
344
  const taskStat = fs.lstatSync(taskMd);
347
345
  if (!taskStat.isFile() || taskStat.isSymbolicLink())
346
+ throw new Error('TASK_CREATE_TASK_INVALID');
347
+ const frontmatter = parseTaskFrontmatter(fs.readFileSync(taskMd, 'utf8'));
348
+ if (frontmatter.task_create_key_digest !== keyDigest)
348
349
  continue;
349
- const content = fs.readFileSync(taskMd, 'utf8');
350
- if (!content.includes(`task_create_key_digest: ${keyDigest}`))
351
- continue;
352
- if (!content.includes(`task_create_candidate_digest: ${candidateDigest}`)) {
350
+ if (frontmatter.task_create_candidate_digest !== candidateDigest) {
353
351
  throw new Error('TASK_CREATE_IDEMPOTENCY_CONFLICT');
354
352
  }
355
- const shortId = loadShortIdByTaskId(repoRoot).get(name)
356
- ?? mutateShortIdRegistry(repoRoot, name, 'alloc').shortId;
357
- if (!shortId)
358
- throw new Error('TASK_CREATE_RECEIPT_INVALID');
359
- return { taskId: name, shortId };
353
+ const shortId = task.state === 'active'
354
+ ? loadShortIdByTaskId(repoRoot).get(task.taskId) ?? mutateShortIdRegistry(repoRoot, task.taskId, 'alloc').shortId
355
+ : null;
356
+ if (task.state === 'active' && !shortId)
357
+ throw new Error('TASK_CREATE_SHORT_ID_FAILED');
358
+ return { taskId: task.taskId, shortId, state: task.state };
360
359
  }
361
360
  return null;
362
361
  }
@@ -376,30 +375,10 @@ function createLocalTask(value, options) {
376
375
  const canonical = canonicalTaskCreateCandidate(candidate);
377
376
  const candidateDigest = sha256(canonical);
378
377
  const keyDigest = sha256(candidate.idempotencyKey);
379
- const receipts = path.join(workspaceRoot, '.task-create');
380
- return withCreateLock(repoRoot, workspaceRoot, () => {
381
- if (fs.existsSync(receipts))
382
- assertRealDirectory(receipts, 'TASK_CREATE_RECEIPT_INVALID');
383
- const receiptPath = path.join(receipts, `${keyDigest}.json`);
384
- if (fs.existsSync(receiptPath)) {
385
- const stat = fs.lstatSync(receiptPath);
386
- if (!stat.isFile() || stat.isSymbolicLink())
387
- throw new Error('TASK_CREATE_RECEIPT_INVALID');
388
- const receipt = JSON.parse(fs.readFileSync(receiptPath, 'utf8'));
389
- if (receipt.candidateDigest !== candidateDigest)
390
- throw new Error('TASK_CREATE_IDEMPOTENCY_CONFLICT');
391
- if (!receipt.taskId || !receipt.shortId || !fs.existsSync(path.join(activeRoot, receipt.taskId, 'task.md'))) {
392
- throw new Error('TASK_CREATE_RECEIPT_INVALID');
393
- }
394
- return { status: 'no-op', changed: false, task: { id: receipt.taskId, shortId: receipt.shortId } };
395
- }
396
- const recovered = recoverPublishedTask(activeRoot, repoRoot, keyDigest, candidateDigest);
378
+ return withCreateLock(repoRoot, projectConfig.project, () => {
379
+ const recovered = recoverPublishedTask(repoRoot, keyDigest, candidateDigest);
397
380
  if (recovered) {
398
- ensureRealDirectory(receipts, 'TASK_CREATE_RECEIPT_INVALID');
399
- writeReceipt(receiptPath, {
400
- version: 1, candidateDigest, taskId: recovered.taskId, shortId: recovered.shortId, status: 'recovered'
401
- });
402
- return { status: 'no-op', changed: false, task: { id: recovered.taskId, shortId: recovered.shortId } };
381
+ return { status: 'no-op', changed: false, task: { id: recovered.taskId, shortId: recovered.shortId, state: recovered.state } };
403
382
  }
404
383
  const base = options.now?.() ?? new Date();
405
384
  let taskDate = new Date(base.getTime());
@@ -418,32 +397,17 @@ function createLocalTask(value, options) {
418
397
  agentInfraVersion, keyDigest, candidateDigest
419
398
  });
420
399
  validateRenderedQualification(rendered, candidate);
421
- ensureRealDirectory(receipts, 'TASK_CREATE_RECEIPT_INVALID');
422
400
  const temporary = path.join(workspaceRoot, `.task-create.tmp.${process.pid}.${randomUUID()}`);
423
401
  fs.mkdirSync(temporary, { mode: 0o700 });
424
402
  fs.writeFileSync(path.join(temporary, 'task.md'), rendered, { flag: 'wx', mode: 0o600 });
425
403
  const destination = path.join(activeRoot, taskId);
426
404
  fs.renameSync(temporary, destination);
427
405
  let shortId;
428
- try {
429
- const allocated = mutateShortIdRegistry(repoRoot, taskId, 'alloc');
430
- if (!allocated.shortId)
431
- throw new Error('TASK_CREATE_SHORT_ID_FAILED');
432
- shortId = allocated.shortId;
433
- }
434
- catch (error) {
435
- fs.rmSync(destination, { recursive: true, force: true });
436
- throw error;
437
- }
438
- try {
439
- writeReceipt(receiptPath, { version: 1, candidateDigest, taskId, shortId, status: 'created' });
440
- }
441
- catch (error) {
442
- mutateShortIdRegistry(repoRoot, taskId, 'release');
443
- fs.rmSync(destination, { recursive: true, force: true });
444
- throw error;
445
- }
446
- return { status: 'applied', changed: true, task: { id: taskId, shortId } };
406
+ const allocated = mutateShortIdRegistry(repoRoot, taskId, 'alloc');
407
+ if (!allocated.shortId)
408
+ throw new Error('TASK_CREATE_SHORT_ID_FAILED');
409
+ shortId = allocated.shortId;
410
+ return { status: 'applied', changed: true, task: { id: taskId, shortId, state: 'active' } };
447
411
  });
448
412
  }
449
413
  export { canonicalTaskCreateCandidate, createLocalTask, validateTaskCreateCandidate };