@fitlab-ai/agent-infra 0.9.11 → 0.9.12

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 (193) hide show
  1. package/README.md +6 -6
  2. package/README.zh-CN.md +6 -6
  3. package/dist/bin/internal-cli.js +5 -0
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +71 -10
  5. package/dist/lib/defaults.json +4 -0
  6. package/dist/lib/git/workflow.js +43 -1
  7. package/dist/lib/init.js +1 -0
  8. package/dist/lib/internal/git-workflow.js +8 -1
  9. package/dist/lib/internal/platform-pr.js +8 -2
  10. package/dist/lib/internal/task-delivery.js +59 -0
  11. package/dist/lib/platform/github-client.js +7 -1
  12. package/dist/lib/platform/pr-checks.js +8 -5
  13. package/dist/lib/platform/pr-summary.js +12 -8
  14. package/dist/lib/platform/pull-requests.js +393 -91
  15. package/dist/lib/platform/verification-required.js +13 -2
  16. package/dist/lib/platform/verification-sync.js +9 -4
  17. package/dist/lib/pr-review/evidence-grading.js +12 -4
  18. package/dist/lib/sandbox/config.js +5 -0
  19. package/dist/lib/sandbox/control/lifecycle.js +29 -7
  20. package/dist/lib/server/process-state.js +2 -2
  21. package/dist/lib/task/artifact-lifecycle.js +2 -2
  22. package/dist/lib/task/commit-intent.js +100 -0
  23. package/dist/lib/task/commit-operation.js +195 -101
  24. package/dist/lib/task/create.js +33 -7
  25. package/dist/lib/task/decision-details.js +3 -48
  26. package/dist/lib/task/delivery-target.js +97 -0
  27. package/dist/lib/task/delivery.js +153 -0
  28. package/dist/lib/task/human-override.js +5 -2
  29. package/dist/lib/task/lifecycle.js +106 -9
  30. package/dist/lib/task/orchestration.js +55 -62
  31. package/dist/lib/task/pr-delivery-fact.js +182 -0
  32. package/dist/lib/task/review-finalization.js +4 -8
  33. package/dist/lib/task/review-fingerprint.js +8 -0
  34. package/dist/lib/task/verification-engine.js +48 -24
  35. package/dist/lib/task/verification-review.js +1 -1
  36. package/dist/lib/update.js +9 -0
  37. package/dist/lifecycle-build-manifest.json +3 -3
  38. package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +105 -10
  39. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +12 -1
  40. package/lib/defaults.json +4 -0
  41. package/lib/git/workflow.ts +38 -1
  42. package/lib/init.ts +3 -0
  43. package/lib/internal/git-workflow.ts +7 -1
  44. package/lib/internal/platform-pr.ts +8 -1
  45. package/lib/internal/task-delivery.ts +53 -0
  46. package/lib/platform/github-client.ts +8 -1
  47. package/lib/platform/pr-checks.ts +7 -5
  48. package/lib/platform/pr-summary.ts +10 -7
  49. package/lib/platform/pull-requests.ts +425 -87
  50. package/lib/platform/verification-required.ts +12 -2
  51. package/lib/platform/verification-sync.ts +9 -4
  52. package/lib/pr-review/evidence-grading.ts +7 -4
  53. package/lib/sandbox/config.ts +7 -0
  54. package/lib/sandbox/control/lifecycle.ts +30 -8
  55. package/lib/server/process-state.ts +2 -2
  56. package/lib/task/artifact-lifecycle.ts +2 -2
  57. package/lib/task/commit-intent.ts +137 -0
  58. package/lib/task/commit-operation.ts +174 -87
  59. package/lib/task/create.ts +34 -9
  60. package/lib/task/decision-details.ts +2 -62
  61. package/lib/task/delivery-target.ts +130 -0
  62. package/lib/task/delivery.ts +160 -0
  63. package/lib/task/human-override.ts +5 -2
  64. package/lib/task/lifecycle.ts +93 -4
  65. package/lib/task/orchestration.ts +62 -65
  66. package/lib/task/pr-delivery-fact.ts +254 -0
  67. package/lib/task/review-finalization.ts +4 -14
  68. package/lib/task/review-fingerprint.ts +10 -0
  69. package/lib/task/verification-engine.ts +45 -28
  70. package/lib/task/verification-review.ts +2 -0
  71. package/lib/update.ts +10 -0
  72. package/package.json +1 -1
  73. package/templates/.agents/rules/README.en.md +4 -2
  74. package/templates/.agents/rules/README.zh-CN.md +4 -2
  75. package/templates/.agents/rules/create-issue.en.md +11 -2
  76. package/templates/.agents/rules/create-issue.zh-CN.md +24 -2
  77. package/templates/.agents/rules/issue-pr-commands.en.md +4 -17
  78. package/templates/.agents/rules/issue-sync.en.md +26 -5
  79. package/templates/.agents/rules/issue-sync.zh-CN.md +62 -5
  80. package/templates/.agents/rules/label-milestone-setup.en.md +15 -2
  81. package/templates/.agents/rules/label-milestone-setup.zh-CN.md +15 -2
  82. package/templates/.agents/rules/local-artifact-repair.en.md +46 -0
  83. package/templates/.agents/rules/local-artifact-repair.zh-CN.md +46 -0
  84. package/templates/.agents/rules/milestone-inference.en.md +12 -2
  85. package/templates/.agents/rules/milestone-inference.zh-CN.md +25 -3
  86. package/templates/.agents/rules/pr-sync.en.md +22 -13
  87. package/templates/.agents/rules/pr-sync.zh-CN.md +22 -1
  88. package/templates/.agents/rules/release-commands.en.md +42 -3
  89. package/templates/.agents/rules/release-commands.zh-CN.md +42 -3
  90. package/templates/.agents/rules/security-alerts.en.md +17 -2
  91. package/templates/.agents/rules/security-alerts.zh-CN.md +17 -2
  92. package/templates/.agents/rules/sync-content-generation.en.md +8 -0
  93. package/templates/.agents/rules/sync-content-generation.zh-CN.md +8 -0
  94. package/templates/.agents/rules/task-management.en.md +1 -1
  95. package/templates/.agents/rules/task-management.zh-CN.md +1 -1
  96. package/templates/.agents/scripts/security-alerts.github.sh +151 -0
  97. package/templates/.agents/scripts/security-alerts.sh +31 -0
  98. package/templates/.agents/skills/analyze-task/SKILL.en.md +1 -0
  99. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +1 -0
  100. package/templates/.agents/skills/close-codescan/SKILL.en.md +2 -2
  101. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +2 -2
  102. package/templates/.agents/skills/close-dependabot/SKILL.en.md +2 -2
  103. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +2 -2
  104. package/templates/.agents/skills/code-task/SKILL.en.md +5 -2
  105. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +6 -3
  106. package/templates/.agents/skills/commit/SKILL.en.md +3 -3
  107. package/templates/.agents/skills/commit/SKILL.zh-CN.md +3 -3
  108. package/templates/.agents/skills/commit/config/verify.json +1 -1
  109. package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +3 -3
  110. package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +3 -3
  111. package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +1 -1
  112. package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +1 -1
  113. package/templates/.agents/skills/commit/reference/task-status-update.en.md +2 -2
  114. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +2 -2
  115. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +2 -1
  116. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +2 -1
  117. package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +1 -1
  118. package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +1 -1
  119. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +1 -1
  120. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +1 -1
  121. package/templates/.agents/skills/complete-task/SKILL.en.md +10 -10
  122. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +10 -10
  123. package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +2 -2
  124. package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +2 -2
  125. package/templates/.agents/skills/create-pr/SKILL.en.md +4 -4
  126. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +4 -4
  127. package/templates/.agents/skills/create-task/SKILL.en.md +1 -0
  128. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +1 -0
  129. package/templates/.agents/skills/import-codescan/SKILL.en.md +1 -1
  130. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +1 -1
  131. package/templates/.agents/skills/import-dependabot/SKILL.en.md +1 -1
  132. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +1 -1
  133. package/templates/.agents/skills/init-labels/SKILL.en.md +9 -10
  134. package/templates/.agents/skills/init-labels/SKILL.zh-CN.md +9 -10
  135. package/templates/.agents/skills/init-labels/scripts/init-labels.github.sh +57 -1
  136. package/templates/.agents/skills/init-labels/scripts/init-labels.sh +27 -1
  137. package/templates/.agents/skills/init-milestones/SKILL.en.md +3 -5
  138. package/templates/.agents/skills/init-milestones/SKILL.zh-CN.md +3 -5
  139. package/templates/.agents/skills/plan-task/SKILL.en.md +1 -0
  140. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +1 -0
  141. package/templates/.agents/skills/review-analysis/SKILL.en.md +4 -3
  142. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +4 -3
  143. package/templates/.agents/skills/review-analysis/reference/output-templates.en.md +18 -0
  144. package/templates/.agents/skills/review-analysis/reference/output-templates.zh-CN.md +18 -0
  145. package/templates/.agents/skills/review-code/SKILL.en.md +7 -6
  146. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +7 -6
  147. package/templates/.agents/skills/review-code/reference/output-templates.en.md +19 -1
  148. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +19 -1
  149. package/templates/.agents/skills/review-code/reference/report-template.en.md +4 -2
  150. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +4 -2
  151. package/templates/.agents/skills/review-plan/SKILL.en.md +4 -3
  152. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +4 -3
  153. package/templates/.agents/skills/review-plan/reference/output-templates.en.md +18 -0
  154. package/templates/.agents/skills/review-plan/reference/output-templates.zh-CN.md +18 -0
  155. package/templates/.agents/skills/review-pr/SKILL.en.md +1 -0
  156. package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +1 -0
  157. package/templates/.agents/skills/review-pr/reference/evidence-grading.en.md +2 -2
  158. package/templates/.agents/skills/review-pr/reference/evidence-grading.zh-CN.md +2 -2
  159. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +1 -0
  160. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +1 -0
  161. package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
  162. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
  163. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +4 -0
  164. package/templates/.agents/skills/watch-pr/SKILL.en.md +4 -4
  165. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +4 -4
  166. package/templates/.agents/templates/task.en.md +5 -1
  167. package/templates/.agents/templates/task.zh-CN.md +5 -1
  168. package/templates/.agents/workflows/bug-fix.en.yaml +9 -8
  169. package/templates/.agents/workflows/bug-fix.zh-CN.yaml +9 -8
  170. package/templates/.agents/workflows/feature-development.en.yaml +9 -8
  171. package/templates/.agents/workflows/feature-development.zh-CN.yaml +9 -8
  172. package/templates/.agents/workflows/refactoring.en.yaml +9 -8
  173. package/templates/.agents/workflows/refactoring.zh-CN.yaml +9 -8
  174. package/templates/.agents/rules/create-issue.github.en.md +0 -14
  175. package/templates/.agents/rules/create-issue.github.zh-CN.md +0 -27
  176. package/templates/.agents/rules/issue-pr-commands.github.en.md +0 -34
  177. package/templates/.agents/rules/issue-pr-commands.github.zh-CN.md +0 -49
  178. package/templates/.agents/rules/issue-sync.github.en.md +0 -40
  179. package/templates/.agents/rules/issue-sync.github.zh-CN.md +0 -76
  180. package/templates/.agents/rules/label-milestone-setup.github.en.md +0 -60
  181. package/templates/.agents/rules/label-milestone-setup.github.zh-CN.md +0 -60
  182. package/templates/.agents/rules/milestone-inference.github.en.md +0 -15
  183. package/templates/.agents/rules/milestone-inference.github.zh-CN.md +0 -27
  184. package/templates/.agents/rules/pr-checks-commands.github.en.md +0 -18
  185. package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +0 -34
  186. package/templates/.agents/rules/pr-sync.github.en.md +0 -43
  187. package/templates/.agents/rules/pr-sync.github.zh-CN.md +0 -69
  188. package/templates/.agents/rules/release-commands.github.en.md +0 -44
  189. package/templates/.agents/rules/release-commands.github.zh-CN.md +0 -44
  190. package/templates/.agents/rules/security-alerts.github.en.md +0 -43
  191. package/templates/.agents/rules/security-alerts.github.zh-CN.md +0 -43
  192. /package/templates/.agents/rules/{issue-fields.github.en.md → issue-fields.en.md} +0 -0
  193. /package/templates/.agents/rules/{issue-fields.github.zh-CN.md → issue-fields.zh-CN.md} +0 -0
@@ -6,6 +6,7 @@ import { hasPlatformCapability } from "./adapters.ts";
6
6
  import { inspectRequiredChecks } from "./pr-checks.ts";
7
7
  import { resolvePlatformContext } from "./context.ts";
8
8
  import { resolveReviewedHeadRelation } from "./merged-pr-equivalence.ts";
9
+ import { readPrDeliveryFact } from "../task/pr-delivery-fact.ts";
9
10
 
10
11
  const CHECK_TYPE = "required-checks";
11
12
  const SHA_PATTERN = /^[0-9a-f]{7,40}$/i;
@@ -34,7 +35,13 @@ function readHead(repoRoot: any): any {
34
35
 
35
36
  export function evaluateRequiredChecks(context: any, shared: any): any {
36
37
  const { metadata, localHead, inspection, prFlow } = context;
37
- if (prFlow === "disabled" || metadata.pr_status === "skipped" || !validPrNumber(metadata.pr_number)) {
38
+ const factRead = readPrDeliveryFact(metadata);
39
+ if (factRead.status === "invalid") {
40
+ return shared.failResult(CHECK_TYPE, factRead.error.message, "check_failed");
41
+ }
42
+ const fact = factRead.status === "valid" ? factRead.fact : null;
43
+ const prNumber = fact?.state === "bound" ? fact.identity.number : null;
44
+ if (prFlow === "disabled" || fact?.state === "skipped" || !validPrNumber(prNumber)) {
38
45
  return shared.passResult(CHECK_TYPE, "Skipped: required checks are not applicable to this task");
39
46
  }
40
47
 
@@ -87,7 +94,10 @@ export function check({ taskDir }: any, shared: any): any {
87
94
  const task = shared.loadTask(taskDir);
88
95
  if (!task.ok) return shared.failResult(CHECK_TYPE, task.message);
89
96
  const prFlow = readPrFlow(shared.repoRoot);
90
- if (prFlow === "disabled" || task.metadata.pr_status === "skipped" || !validPrNumber(task.metadata.pr_number)) {
97
+ const factRead = readPrDeliveryFact(task.metadata);
98
+ if (factRead.status === "invalid") return shared.failResult(CHECK_TYPE, factRead.error.message, "check_failed");
99
+ const bound = factRead.status === "valid" && factRead.fact.state === "bound";
100
+ if (prFlow === "disabled" || (factRead.status === "valid" && factRead.fact.state === "skipped") || !bound) {
91
101
  return evaluateRequiredChecks({ metadata: task.metadata, localHead: null, inspection: null, prFlow }, shared);
92
102
  }
93
103
  const platform = resolvePlatformContext({ cwd: shared.repoRoot });
@@ -10,6 +10,7 @@ import { inspectGitHubIssueMetadata, requirementSectionAnchors } from "./issues.
10
10
  import { listRemoteComments } from "./issue-comments.ts";
11
11
  import { taskTypeLabel } from "./metadata-labels.ts";
12
12
  import { inspectGitHubPullRequest } from "./pull-requests.ts";
13
+ import { readPrDeliveryFact } from "../task/pr-delivery-fact.ts";
13
14
 
14
15
  const CHECK_TYPE = "platform-sync";
15
16
  const VERSION_LINE_REGEX = /^[0-9]+\.[0-9]+\.x$/;
@@ -151,12 +152,16 @@ function buildSyncContext({ taskDir, config, artifactFile }: any): any {
151
152
  }
152
153
 
153
154
  const issueNumber = parseIssueNumber(task.metadata.issue_number);
154
- const prNumber = parsePrNumber(task.metadata.pr_number);
155
+ const fact = readPrDeliveryFact(task.metadata);
156
+ if (fact.status === "invalid") {
157
+ return { earlyReturn: failResult(CHECK_TYPE, fact.error.message, "check_failed") };
158
+ }
159
+ const prNumber = fact.status === "valid" && fact.fact.state === "bound" ? fact.fact.identity.number : null;
155
160
  if (config.when === "issue_number_exists" && !issueNumber) {
156
161
  return { earlyReturn: passResult(CHECK_TYPE, "Skipped: task has no issue_number") };
157
162
  }
158
- if (config.when === "pr_number_exists" && !prNumber) {
159
- return { earlyReturn: passResult(CHECK_TYPE, "Skipped: task has no pr_number") };
163
+ if (config.when === "pr_fact_bound" && !prNumber) {
164
+ return { earlyReturn: passResult(CHECK_TYPE, "Skipped: task has no verified bound pull request") };
160
165
  }
161
166
 
162
167
  if (!issueNumber) {
@@ -322,7 +327,7 @@ function fetchRemoteData(context: any): any {
322
327
  if (context.prMarker) {
323
328
  if (!context.prNumber) {
324
329
  return {
325
- earlyReturn: failResult(CHECK_TYPE, "Expected a valid pr_number for PR comment verification", "check_failed")
330
+ earlyReturn: failResult(CHECK_TYPE, "Expected a verified bound pull request for PR comment verification", "check_failed")
326
331
  };
327
332
  }
328
333
 
@@ -1,7 +1,8 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
 
4
- import { parseTaskFrontmatter } from '../task/frontmatter.ts';
4
+ import { parseTypedTaskFrontmatter } from '../task/frontmatter.ts';
5
+ import { readPrDeliveryFact } from '../task/pr-delivery-fact.ts';
5
6
 
6
7
  // ---------------------------------------------------------------------------
7
8
  // Host resolution (PL-4): PR -> unique / ambiguous / none.
@@ -62,7 +63,7 @@ export function extractClosingIssueNumbers(body: string): number[] {
62
63
  /**
63
64
  * Scan `.agents/workspace/active/{task-id}/task.md` files for tasks bound to the
64
65
  * PR number or to one of the PR's closing issues. A task is emitted at most once;
65
- * a direct `pr_number` hit takes priority over the issue-number reverse lookup.
66
+ * a verified fact identity hit takes priority over the issue-number reverse lookup.
66
67
  */
67
68
  export function collectHostCandidates(input: {
68
69
  prNumber: number;
@@ -87,9 +88,11 @@ export function collectHostCandidates(input: {
87
88
  } catch {
88
89
  continue;
89
90
  }
90
- const frontmatter = parseTaskFrontmatter(content);
91
+ let frontmatter: ReturnType<typeof parseTypedTaskFrontmatter>;
92
+ try { frontmatter = parseTypedTaskFrontmatter(content); } catch { continue; }
91
93
  const issueNumber = toPositiveNumber(frontmatter.issue_number);
92
- const prNumber = toPositiveNumber(frontmatter.pr_number);
94
+ const fact = readPrDeliveryFact(frontmatter);
95
+ const prNumber = fact.status === 'valid' && fact.fact.state === 'bound' ? fact.fact.identity.number : null;
93
96
  if (prNumber === input.prNumber) {
94
97
  candidates.push({ taskId: entry.name, taskDir, issueNumber, prNumber });
95
98
  } else if (issueNumber !== null && input.closingIssues.includes(issueNumber)) {
@@ -63,6 +63,7 @@ export type SandboxConfig = {
63
63
  tools: string[];
64
64
  customTools: SandboxTool[];
65
65
  agentClientState: AgentClientState;
66
+ delivery?: { remote: string; baseRef: string };
66
67
  refreshIntervalDays: number;
67
68
  dockerfile: string | null;
68
69
  vm: SandboxVmConfig;
@@ -73,6 +74,7 @@ type AircConfig = {
73
74
  org?: unknown;
74
75
  agentClients?: unknown;
75
76
  sandbox?: SandboxConfigInput;
77
+ delivery?: { remote?: unknown; baseRef?: unknown };
76
78
  };
77
79
 
78
80
  function detectRepoRoot(): string {
@@ -135,6 +137,7 @@ export function loadConfig({
135
137
  const agentClients = normalizeAgentClients(airc);
136
138
  const defaults = cloneDefaults();
137
139
  const sandbox = airc.sandbox ?? {};
140
+ const delivery = airc.delivery ?? {};
138
141
  const engine = validateSandboxEngine(sandbox.engine ?? defaults.engine, { platformFn });
139
142
  const project = airc.project;
140
143
 
@@ -188,6 +191,10 @@ export function loadConfig({
188
191
  tools,
189
192
  customTools,
190
193
  agentClientState: agentClients.state,
194
+ delivery: {
195
+ remote: typeof delivery.remote === 'string' ? delivery.remote : 'origin',
196
+ baseRef: typeof delivery.baseRef === 'string' ? delivery.baseRef : 'main'
197
+ },
191
198
  refreshIntervalDays: asNonNegativeIntegerOrDefault(
192
199
  sandbox.refreshIntervalDays,
193
200
  defaults.refreshIntervalDays
@@ -494,30 +494,32 @@ function readSandboxControlManifestValue(manifestPath: string): SandboxControlMa
494
494
  } catch {
495
495
  throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
496
496
  }
497
- const candidate = manifest as Partial<SandboxControlManifest> & { version?: unknown };
497
+ const candidate = manifest as Partial<SandboxControlManifest>;
498
498
  const expectedKeys = [
499
499
  'branch', 'channelDir', 'container', 'containerIdentity', 'engine', 'generation',
500
500
  'mode', 'processingDir', 'project', 'publicStatusDir', 'repoRoot', 'runtimeDir',
501
501
  'taskId', 'token', 'worktreeRoot'
502
502
  ];
503
- if ('version' in candidate || typeof candidate.runtimeDir !== 'string'
504
- || Object.keys(manifest).sort().join(',') !== expectedKeys.sort().join(',')) {
505
- throw new Error('SANDBOX_CONTROL_MANIFEST_REBUILD_REQUIRED: container-only recreation/rebuild is required');
503
+ if (Object.keys(manifest).sort().join(',') !== expectedKeys.sort().join(',')) {
504
+ throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
506
505
  }
507
506
  if (typeof candidate.containerIdentity !== 'object' || candidate.containerIdentity === null
508
507
  || Array.isArray(candidate.containerIdentity)) throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
509
508
  const containerIdentity = candidate.containerIdentity as Record<string, unknown>;
510
- if (typeof candidate.engine !== 'string' || candidate.engine.length === 0
509
+ if (Object.keys(containerIdentity).sort().join(',') !== 'id,labels'
510
+ || typeof candidate.engine !== 'string' || candidate.engine.length === 0
511
511
  || typeof candidate.repoRoot !== 'string' || typeof candidate.worktreeRoot !== 'string'
512
512
  || typeof candidate.project !== 'string' || typeof candidate.container !== 'string'
513
513
  || typeof containerIdentity.id !== 'string' || containerIdentity.id.length === 0
514
514
  || typeof containerIdentity.labels !== 'object' || containerIdentity.labels === null
515
515
  || Array.isArray(containerIdentity.labels)
516
516
  || Object.values(containerIdentity.labels).some((value) => typeof value !== 'string')
517
- || typeof candidate.branch !== 'string' || !['task-bound', 'branch-only'].includes(candidate.mode ?? '')
517
+ || typeof candidate.branch !== 'string'
518
+ || (candidate.mode !== 'task-bound' && candidate.mode !== 'branch-only')
518
519
  || (candidate.taskId !== null && typeof candidate.taskId !== 'string')
519
520
  || typeof candidate.channelDir !== 'string' || typeof candidate.publicStatusDir !== 'string'
520
- || typeof candidate.processingDir !== 'string' || typeof candidate.token !== 'string'
521
+ || typeof candidate.processingDir !== 'string' || typeof candidate.runtimeDir !== 'string'
522
+ || typeof candidate.token !== 'string'
521
523
  || typeof candidate.generation !== 'string'
522
524
  || (candidate.mode === 'task-bound' && (typeof candidate.taskId !== 'string' || candidate.taskId.length === 0))
523
525
  || (candidate.mode === 'branch-only' && candidate.taskId !== null)) throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
@@ -530,7 +532,27 @@ function readSandboxControlManifestValue(manifestPath: string): SandboxControlMa
530
532
  if (path.resolve(candidate.runtimeDir) !== path.join(root, 'runtime')) {
531
533
  throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
532
534
  }
533
- return candidate as SandboxControlManifest;
535
+ const normalized: SandboxControlManifest = {
536
+ engine: candidate.engine,
537
+ repoRoot: candidate.repoRoot,
538
+ worktreeRoot: candidate.worktreeRoot,
539
+ project: candidate.project,
540
+ container: candidate.container,
541
+ containerIdentity: {
542
+ id: containerIdentity.id as string,
543
+ labels: { ...(containerIdentity.labels as Record<string, string>) }
544
+ },
545
+ branch: candidate.branch,
546
+ mode: candidate.mode,
547
+ taskId: candidate.taskId,
548
+ token: candidate.token,
549
+ generation: candidate.generation,
550
+ channelDir: candidate.channelDir,
551
+ publicStatusDir: candidate.publicStatusDir,
552
+ processingDir: candidate.processingDir,
553
+ runtimeDir: candidate.runtimeDir
554
+ };
555
+ return normalized;
534
556
  }
535
557
 
536
558
  export function readSandboxControlManifest(manifestPath: string): SandboxControlManifest {
@@ -85,8 +85,8 @@ export function isProcessAlive(pid: number, platform: NodeJS.Platform = process.
85
85
  }
86
86
  }
87
87
 
88
- function parseDarwinStartTime(output: string): number | null {
89
- const match = output.trim().match(/^(?:Sun|Mon|Tue|Wed|Thu|Fri|Sat) ([A-Z][a-z]{2}) (\d{1,2}) (\d{2}):(\d{2}):(\d{2}) (\d{4})$/);
88
+ export function parseDarwinStartTime(output: string): number | null {
89
+ const match = output.trim().match(/^(?:Sun|Mon|Tue|Wed|Thu|Fri|Sat) ([A-Z][a-z]{2})\s+(\d{1,2}) (\d{2}):(\d{2}):(\d{2}) (\d{4})$/);
90
90
  if (!match) return null;
91
91
  const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
92
92
  const month = months.indexOf(match[1]!);
@@ -482,9 +482,9 @@ function buildArtifactLinkSection(content: string, artifact: ArtifactIdentity):
482
482
  const label = english
483
483
  ? `${spec.labels[1]} (Round ${artifact.round})`
484
484
  : `${spec.labels[0]}(Round ${artifact.round})`;
485
- const link = `[${label}](${artifact.name})`;
485
+ const link = `${label}:\`${artifact.name}\``;
486
486
  let body = extractSection(content, aliases);
487
- if (body.includes(`](${artifact.name})`)) return { aliases, heading, body };
487
+ if (body.includes(link)) return { aliases, heading, body };
488
488
  const placeholders = new Set([
489
489
  '[分析阶段的发现。哪些文件受影响?范围是什么?]',
490
490
  '[Findings from the analysis phase. Which files are affected? What is the scope?]',
@@ -0,0 +1,137 @@
1
+ import { createHash } from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+
5
+ export type CheckpointIntentState = 'prepared' | 'committed' | 'synced';
6
+
7
+ export type CheckpointIntent = Readonly<{
8
+ version: 1;
9
+ taskId: string;
10
+ branch: string;
11
+ mode: 'local';
12
+ expectedHead: string;
13
+ expectedTree: string;
14
+ paths: readonly string[];
15
+ message: string;
16
+ round: number;
17
+ digest: string;
18
+ state: CheckpointIntentState;
19
+ committedHead: string | null;
20
+ createdAt: string;
21
+ updatedAt: string;
22
+ }>;
23
+
24
+ type IntentIdentity = Readonly<{
25
+ taskId: string;
26
+ branch: string;
27
+ mode: 'local';
28
+ expectedHead: string;
29
+ expectedTree: string;
30
+ paths: readonly string[];
31
+ message: string;
32
+ round: number;
33
+ }>;
34
+
35
+ function intentRoot(repoRoot: string): string {
36
+ return path.join(repoRoot, '.agents', 'workspace', '.task-commit-intents');
37
+ }
38
+
39
+ function intentPath(repoRoot: string, taskId: string): string {
40
+ return path.join(intentRoot(repoRoot), `${taskId}.json`);
41
+ }
42
+
43
+ function checkpointIntentDigest(identity: IntentIdentity): string {
44
+ return createHash('sha256').update(JSON.stringify({
45
+ taskId: identity.taskId,
46
+ branch: identity.branch,
47
+ mode: identity.mode,
48
+ expectedHead: identity.expectedHead,
49
+ expectedTree: identity.expectedTree,
50
+ paths: [...identity.paths],
51
+ message: identity.message,
52
+ round: identity.round
53
+ })).digest('hex');
54
+ }
55
+
56
+ function isIntent(value: unknown): value is CheckpointIntent {
57
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
58
+ const intent = value as Record<string, unknown>;
59
+ return intent.version === 1
60
+ && typeof intent.taskId === 'string' && intent.taskId.length > 0
61
+ && typeof intent.branch === 'string' && intent.branch.length > 0
62
+ && intent.mode === 'local'
63
+ && typeof intent.expectedHead === 'string' && /^[a-f0-9]{40,64}$/.test(intent.expectedHead)
64
+ && typeof intent.expectedTree === 'string' && /^[a-f0-9]{40,64}$/.test(intent.expectedTree)
65
+ && Array.isArray(intent.paths) && intent.paths.every((item) => typeof item === 'string' && item.length > 0)
66
+ && typeof intent.message === 'string' && intent.message.length > 0
67
+ && Number.isSafeInteger(intent.round) && Number(intent.round) > 0
68
+ && typeof intent.digest === 'string' && /^[a-f0-9]{64}$/.test(intent.digest)
69
+ && ['prepared', 'committed', 'synced'].includes(String(intent.state))
70
+ && (intent.committedHead === null || (typeof intent.committedHead === 'string' && /^[a-f0-9]{40,64}$/.test(intent.committedHead)))
71
+ && typeof intent.createdAt === 'string' && intent.createdAt.length > 0
72
+ && typeof intent.updatedAt === 'string' && intent.updatedAt.length > 0
73
+ && intent.digest === checkpointIntentDigest(intent as unknown as IntentIdentity);
74
+ }
75
+
76
+ function readCheckpointIntent(repoRoot: string, taskId: string): CheckpointIntent | null {
77
+ const target = intentPath(repoRoot, taskId);
78
+ if (!fs.existsSync(target)) return null;
79
+ const value = JSON.parse(fs.readFileSync(target, 'utf8')) as unknown;
80
+ if (!isIntent(value)) throw new Error('COMMIT_INTENT_INVALID: checkpoint intent schema is invalid');
81
+ return value;
82
+ }
83
+
84
+ function writeCheckpointIntent(repoRoot: string, value: CheckpointIntent): void {
85
+ if (!isIntent(value)) throw new Error('COMMIT_INTENT_INVALID: checkpoint intent schema is invalid');
86
+ const directory = intentRoot(repoRoot);
87
+ fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
88
+ const target = intentPath(repoRoot, value.taskId);
89
+ const temporary = `${target}.${process.pid}.${Date.now()}.tmp`;
90
+ fs.writeFileSync(temporary, `${JSON.stringify(value)}\n`, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
91
+ try { fs.renameSync(temporary, target); }
92
+ catch (error) {
93
+ try { fs.unlinkSync(temporary); } catch { /* preserve primary error */ }
94
+ throw error;
95
+ }
96
+ }
97
+
98
+ function updateCheckpointIntent(
99
+ value: CheckpointIntent,
100
+ patch: Partial<Pick<CheckpointIntent, 'state' | 'committedHead' | 'updatedAt'>>
101
+ ): CheckpointIntent {
102
+ const next = { ...value, ...patch };
103
+ if (next.state === 'prepared' && next.committedHead !== null) throw new Error('COMMIT_INTENT_INVALID: prepared intent cannot have a committed head');
104
+ if (next.state !== 'prepared' && !next.committedHead) throw new Error('COMMIT_INTENT_INVALID: committed intent requires a committed head');
105
+ return next;
106
+ }
107
+
108
+ function removeCheckpointIntent(repoRoot: string, taskId: string): void {
109
+ const target = intentPath(repoRoot, taskId);
110
+ try { fs.unlinkSync(target); } catch (error) {
111
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
112
+ }
113
+ }
114
+
115
+ function sameCheckpointIntent(left: CheckpointIntent, right: IntentIdentity): boolean {
116
+ return left.digest === checkpointIntentDigest(right)
117
+ && left.taskId === right.taskId
118
+ && left.branch === right.branch
119
+ && left.mode === right.mode
120
+ && left.expectedHead === right.expectedHead
121
+ && left.expectedTree === right.expectedTree
122
+ && left.message === right.message
123
+ && left.round === right.round
124
+ && JSON.stringify(left.paths) === JSON.stringify(right.paths);
125
+ }
126
+
127
+ export {
128
+ checkpointIntentDigest,
129
+ intentPath,
130
+ intentRoot,
131
+ isIntent,
132
+ readCheckpointIntent,
133
+ removeCheckpointIntent,
134
+ sameCheckpointIntent,
135
+ updateCheckpointIntent,
136
+ writeCheckpointIntent
137
+ };