@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
@@ -154,16 +154,16 @@ async function releaseNoteContext(input, options = {}) {
154
154
  return { ...baseResult(platform.error.retryable ? 'blocked' : 'failed'), error: providerError(platform.error, 'PLATFORM_PROVIDER_OPERATION_FAILED') };
155
155
  const commits = commitFacts.map((commit) => {
156
156
  const seen = new Set();
157
- const authors = platform.value.history.filter((item) => item.sha === commit.oid).flatMap((item) => item.author ? [item.author] : [])
158
- .concat(platform.value.actors)
157
+ const facts = platform.value.commits.find((item) => item.sha === commit.oid);
158
+ const authors = (facts?.authors || [])
159
159
  .filter((actor) => {
160
- const key = actor.id || actor.name || '';
160
+ const key = actor.login || actor.name;
161
161
  if (seen.has(key))
162
162
  return false;
163
163
  seen.add(key);
164
164
  return true;
165
165
  });
166
- return { ...commit, authors };
166
+ return { ...commit, url: facts?.url || null, pullRequestNumbers: facts?.pullRequestNumbers || [], authors };
167
167
  });
168
168
  const pullRequests = platform.value.mergedPullRequests
169
169
  .map((item) => ({
@@ -174,14 +174,13 @@ async function releaseNoteContext(input, options = {}) {
174
174
  url: item.displayUrl || '',
175
175
  mergedAt: item.mergedAt || '',
176
176
  labels: item.labels || [],
177
- author: item.author ? { login: item.author.name || item.author.id || '', bot: false } : null,
178
- closingIssues: platform.value.closingIssues.filter((issue) => issue.id === item.id).map((issue) => ({
177
+ author: item.author,
178
+ closingIssues: item.closingIssues.map((issue) => ({
179
179
  number: issue.number || resourceIdentityNumber(issue.identity) || 0,
180
180
  ...(issue.identity ? { identity: issue.identity } : {}),
181
181
  title: issue.title,
182
182
  url: issue.displayUrl || '',
183
- labels: issue.labels || [],
184
- author: issue.author ? { login: issue.author.name || issue.author.id || '', bot: false } : null
183
+ author: issue.author
185
184
  }))
186
185
  }))
187
186
  .sort((left, right) => left.number - right.number);
@@ -154,7 +154,7 @@ export async function runInSandbox(request, options = {}) {
154
154
  allowRecreate: request.recreate,
155
155
  recreate: async () => {
156
156
  const { create } = await import('./commands/create.js');
157
- await create([request.taskRef, '--no-refresh']);
157
+ await create([request.taskRef, '--no-refresh'], { runProjectInitCommand: false });
158
158
  }
159
159
  })).container;
160
160
  }
@@ -18,6 +18,7 @@ import { createSandboxCapabilityPlan, runSandboxHooks, runBoundedSandboxHookComm
18
18
  import { resolveTools, tmpfsSeedStagingPath, tmpfsSeedTargetPath, toolConfigDirCandidates } from '../tools.js';
19
19
  import { assertFreshSandboxReady, hydrateTmpfsSeedEntries, prepareTmpfsMounts, startSandboxControlBroker } from '../recovery.js';
20
20
  import { acquireSandboxControlReplacement, assertSandboxControlCutoverSnapshot, beginSandboxControlReplacement, captureSandboxControlCutoverSnapshot, commitSandboxControlReplacement, quiesceSandboxControlRoot, recoverSandboxControlReplacement, readSandboxControlManifest } from '../control/lifecycle.js';
21
+ import { atomicWriteJson } from '../control/state.js';
21
22
  import { inspectSandboxControlContainer } from '../control/container-identity.js';
22
23
  import { hostJoin, toEnginePath, volumeArg } from '../engines/wsl2-paths.js';
23
24
  import { sandboxCoreBindMounts } from '../mounts.js';
@@ -652,7 +653,24 @@ function readImageLabels(config, engine) {
652
653
  config.imageName
653
654
  ]));
654
655
  }
655
- export async function create(args) {
656
+ function readProjectInitCommandDigest(controlRoot) {
657
+ const markerPath = path.join(controlRoot, 'project-init.json');
658
+ if (!fs.existsSync(markerPath))
659
+ return null;
660
+ try {
661
+ const marker = JSON.parse(fs.readFileSync(markerPath, 'utf8'));
662
+ return marker.version === 1 && typeof marker.commandSha256 === 'string' && /^[a-f0-9]{64}$/.test(marker.commandSha256)
663
+ ? marker.commandSha256
664
+ : null;
665
+ }
666
+ catch {
667
+ return null;
668
+ }
669
+ }
670
+ function writeProjectInitCommandDigest(controlRoot, commandSha256) {
671
+ atomicWriteJson(path.join(controlRoot, 'project-init.json'), { version: 1, commandSha256 });
672
+ }
673
+ export async function create(args, { runProjectInitCommand = true } = {}) {
656
674
  const { values, positionals } = parseArgs({
657
675
  args,
658
676
  allowPositionals: true,
@@ -720,6 +738,7 @@ export async function create(args) {
720
738
  identity: target.workspace
721
739
  });
722
740
  const worktree = worktreeCandidates.find((candidate) => fs.existsSync(candidate)) ?? worktreeCandidates[0] ?? '';
741
+ let worktreeCreated = false;
723
742
  const shareCommon = shareCommonDir(effectiveConfig);
724
743
  const shareBranch = shareBranchDir(effectiveConfig, branch);
725
744
  const preparedDockerfile = prepareDockerfile(effectiveConfig, capabilityPlan.image);
@@ -821,6 +840,7 @@ export async function create(args) {
821
840
  toEnginePath(engine, worktree),
822
841
  branch
823
842
  ]);
843
+ worktreeCreated = true;
824
844
  }
825
845
  else {
826
846
  message(`Creating branch '${branch}' from '${baseBranch}'...`);
@@ -834,6 +854,7 @@ export async function create(args) {
834
854
  toEnginePath(engine, worktree),
835
855
  baseBranch
836
856
  ]);
857
+ worktreeCreated = true;
837
858
  }
838
859
  return `Worktree ready at ${worktree}`;
839
860
  }
@@ -886,6 +907,7 @@ export async function create(args) {
886
907
  let replacementCutover = null;
887
908
  try {
888
909
  const recoveryResult = await recoverSandboxControlReplacement(controlPaths.root, replacementLease);
910
+ const previousProjectInitCommandDigest = readProjectInitCommandDigest(controlPaths.root);
889
911
  const hadExistingControlRoot = hadControlRootBeforeAcquire || recoveryResult === 'restored';
890
912
  const previousManifest = fs.existsSync(controlPaths.manifestPath)
891
913
  ? readSandboxControlManifest(controlPaths.manifestPath)
@@ -1295,6 +1317,29 @@ export async function create(args) {
1295
1317
  throw new Error(afterStartFailure.message
1296
1318
  ?? `Sandbox hook '${afterStartFailure.hookId}' failed.`);
1297
1319
  }
1320
+ const initCommand = effectiveConfig.initCommand;
1321
+ const initCommandDigest = initCommand === null ? null : createHash('sha256').update(initCommand).digest('hex');
1322
+ const shouldRunProjectInitCommand = runProjectInitCommand
1323
+ && initCommand !== null
1324
+ && initCommandDigest !== null
1325
+ && (worktreeCreated || previousProjectInitCommandDigest !== initCommandDigest);
1326
+ let projectInitCompleted = !worktreeCreated
1327
+ && initCommandDigest !== null
1328
+ && previousProjectInitCommandDigest === initCommandDigest;
1329
+ if (shouldRunProjectInitCommand && initCommand !== null) {
1330
+ try {
1331
+ runVerboseEngine(engine, 'docker', [
1332
+ 'exec', '--workdir', '/workspace', container, 'bash', '-lc', initCommand
1333
+ ]);
1334
+ projectInitCompleted = true;
1335
+ }
1336
+ catch (error) {
1337
+ throw new Error(`Project initialization failed for '${branch}': ${error instanceof Error ? error.message : String(error)}`);
1338
+ }
1339
+ }
1340
+ if (projectInitCompleted && initCommandDigest !== null) {
1341
+ writeProjectInitCommandDigest(controlPaths.root, initCommandDigest);
1342
+ }
1298
1343
  replacementLease.release();
1299
1344
  replacementLeaseHeld = false;
1300
1345
  return 'Container started';
@@ -109,7 +109,7 @@ export async function enter(args) {
109
109
  allowRecreate: parsed.recreate,
110
110
  recreate: async () => {
111
111
  const { create } = await import('./create.js');
112
- await create([target.requestedRef, '--no-refresh']);
112
+ await create([target.requestedRef, '--no-refresh'], { runProjectInitCommand: false });
113
113
  }
114
114
  });
115
115
  const container = ready.container;
@@ -65,7 +65,7 @@ export async function start(args) {
65
65
  forceRecreate: parsed.recreate,
66
66
  recreate: async () => {
67
67
  const { create } = await import('./create.js');
68
- await create([target.requestedRef, '--no-refresh']);
68
+ await create([target.requestedRef, '--no-refresh'], { runProjectInitCommand: false });
69
69
  }
70
70
  });
71
71
  const message = ready.path === 'healthy'
@@ -14,6 +14,7 @@ const DEFAULTS = Object.freeze({
14
14
  tools: ['agent-infra'],
15
15
  refreshIntervalDays: 7,
16
16
  dockerfile: null,
17
+ initCommand: null,
17
18
  vm: {
18
19
  cpu: null,
19
20
  memory: null,
@@ -44,6 +45,7 @@ function cloneDefaults() {
44
45
  tools: [...DEFAULTS.tools],
45
46
  refreshIntervalDays: DEFAULTS.refreshIntervalDays,
46
47
  dockerfile: DEFAULTS.dockerfile,
48
+ initCommand: DEFAULTS.initCommand,
47
49
  vm: { ...DEFAULTS.vm }
48
50
  };
49
51
  }
@@ -77,6 +79,13 @@ export function loadConfig({ platformFn = platform, writeStderr = (chunk) => pro
77
79
  ? [...sandbox.runtimes]
78
80
  : defaults.runtimes;
79
81
  const dockerfile = typeof sandbox.dockerfile === 'string' ? sandbox.dockerfile : defaults.dockerfile ?? null;
82
+ if (sandbox.initCommand !== undefined &&
83
+ (typeof sandbox.initCommand !== 'string' || sandbox.initCommand.trim().length === 0)) {
84
+ throw new Error('sandbox.initCommand must be a non-empty string when configured');
85
+ }
86
+ const initCommand = typeof sandbox.initCommand === 'string'
87
+ ? sandbox.initCommand
88
+ : null;
80
89
  if (!dockerfile) {
81
90
  let enginesNode;
82
91
  try {
@@ -118,6 +127,7 @@ export function loadConfig({ platformFn = platform, writeStderr = (chunk) => pro
118
127
  },
119
128
  refreshIntervalDays: asNonNegativeIntegerOrDefault(sandbox.refreshIntervalDays, defaults.refreshIntervalDays),
120
129
  dockerfile,
130
+ initCommand,
121
131
  vm: {
122
132
  cpu: asPositiveNumberOrNull(sandbox.vm?.cpu) ?? defaults.vm.cpu,
123
133
  memory: asPositiveNumberOrNull(sandbox.vm?.memory) ?? defaults.vm.memory,
@@ -1152,17 +1152,25 @@ export async function quiesceSandboxControlRoot(root, options = {}) {
1152
1152
  if (!manifest && brokerLive)
1153
1153
  throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
1154
1154
  const platform = options.platform ?? process.platform;
1155
+ const processControl = options.executionProcessControl ?? {
1156
+ isAlive: (execution) => executionAlive(execution, platform, identityProbe),
1157
+ terminate: (execution, phase) => phase === 'graceful'
1158
+ ? terminateSandboxControlExecution(execution, { platform, timeoutMs: 0, allowForce: false, identityProbe })
1159
+ : terminateSandboxControlExecution(execution, {
1160
+ platform, timeoutMs: Math.max(0, deadlineAt - Date.now()), deadlineAt, forceAt: Date.now(), identityProbe
1161
+ })
1162
+ };
1155
1163
  let executions = manifest ? readExecutions(manifest) : [];
1156
1164
  const owner = brokerLive ? broker : statusLive ? statusOwner : null;
1157
1165
  if (!owner) {
1158
1166
  if (manifest && !broker && !status)
1159
1167
  throw new Error('SANDBOX_CONTROL_OWNER_EVIDENCE_MISSING');
1160
1168
  for (const execution of executions) {
1161
- terminateSandboxControlExecution(execution, { platform, timeoutMs: 0, allowForce: false, identityProbe });
1169
+ processControl.terminate(execution, 'graceful');
1162
1170
  }
1163
1171
  const softDeadlineAt = Math.min(forceAt, Date.now() + Math.floor(Math.max(0, forceAt - Date.now()) / 2));
1164
1172
  while (Date.now() < softDeadlineAt) {
1165
- if (!executions.some((execution) => executionAlive(execution, platform, identityProbe)))
1173
+ if (!executions.some((execution) => processControl.isAlive(execution)))
1166
1174
  break;
1167
1175
  await new Promise((resolve) => setTimeout(resolve, 25));
1168
1176
  }
@@ -1171,29 +1179,26 @@ export async function quiesceSandboxControlRoot(root, options = {}) {
1171
1179
  if (Date.now() >= deadlineAt)
1172
1180
  throw new Error('SANDBOX_CONTROL_QUIESCE_DEADLINE_EXCEEDED');
1173
1181
  for (const execution of executions) {
1174
- if (!terminateSandboxControlExecution(execution, {
1175
- platform, timeoutMs: Math.max(0, deadlineAt - Date.now()), deadlineAt, forceAt: Date.now(), identityProbe
1176
- })) {
1182
+ if (!processControl.terminate(execution, 'force')) {
1177
1183
  throw new Error(`SANDBOX_CONTROL_EXECUTION_STILL_RUNNING: ${execution.requestId}`);
1178
1184
  }
1179
1185
  }
1180
- if (executions.some((execution) => executionAlive(execution, platform, identityProbe))) {
1186
+ if (executions.some((execution) => processControl.isAlive(execution))) {
1181
1187
  throw new Error('SANDBOX_CONTROL_EXECUTION_STILL_RUNNING');
1182
1188
  }
1183
1189
  return broker || status ? 'stale' : 'missing';
1184
1190
  }
1185
- const remaining = () => Math.max(0, deadlineAt - Date.now());
1186
1191
  signalOwner(owner, platform, false);
1187
1192
  for (const execution of executions) {
1188
- terminateSandboxControlExecution(execution, { platform, timeoutMs: 0, allowForce: false, identityProbe });
1193
+ processControl.terminate(execution, 'graceful');
1189
1194
  }
1190
1195
  const softDeadlineAt = Math.min(forceAt, Date.now() + Math.floor(Math.max(0, forceAt - Date.now()) / 2));
1191
1196
  while (Date.now() < softDeadlineAt) {
1192
- if (!ownerLive(owner, identityProbe) && !executions.some((execution) => executionAlive(execution, platform, identityProbe)))
1197
+ if (!ownerLive(owner, identityProbe) && !executions.some((execution) => processControl.isAlive(execution)))
1193
1198
  break;
1194
1199
  await new Promise((resolve) => setTimeout(resolve, 25));
1195
1200
  }
1196
- if (!ownerLive(owner, identityProbe) && !executions.some((execution) => executionAlive(execution, platform, identityProbe))) {
1201
+ if (!ownerLive(owner, identityProbe) && !executions.some((execution) => processControl.isAlive(execution))) {
1197
1202
  return 'stopped';
1198
1203
  }
1199
1204
  if (manifest)
@@ -1201,17 +1206,17 @@ export async function quiesceSandboxControlRoot(root, options = {}) {
1201
1206
  if (Date.now() >= deadlineAt)
1202
1207
  throw new Error('SANDBOX_CONTROL_QUIESCE_DEADLINE_EXCEEDED');
1203
1208
  for (const execution of executions) {
1204
- terminateSandboxControlExecution(execution, { platform, timeoutMs: 0, deadlineAt, forceAt: Date.now(), identityProbe });
1209
+ processControl.terminate(execution, 'force');
1205
1210
  }
1206
1211
  signalOwner(owner, platform, true);
1207
1212
  while (Date.now() < deadlineAt) {
1208
- if (!ownerLive(owner, identityProbe) && !executions.some((execution) => executionAlive(execution, platform, identityProbe)))
1213
+ if (!ownerLive(owner, identityProbe) && !executions.some((execution) => processControl.isAlive(execution)))
1209
1214
  return 'stopped';
1210
1215
  await new Promise((resolve) => setTimeout(resolve, 25));
1211
1216
  }
1212
1217
  if (ownerLive(owner, identityProbe))
1213
1218
  throw new Error('SANDBOX_CONTROL_BROKER_STILL_RUNNING');
1214
- if (executions.some((execution) => executionAlive(execution, platform, identityProbe)))
1219
+ if (executions.some((execution) => processControl.isAlive(execution)))
1215
1220
  throw new Error('SANDBOX_CONTROL_EXECUTION_STILL_RUNNING');
1216
1221
  return 'stopped';
1217
1222
  }
@@ -171,13 +171,17 @@ function taskCreateDomainEvidence(manifest, output) {
171
171
  ? output.task : null;
172
172
  const taskId = typeof task?.id === 'string' ? task.id : null;
173
173
  const shortId = typeof task?.shortId === 'string' ? task.shortId : null;
174
- if (!taskId || !shortId)
174
+ const state = task?.state;
175
+ if (!taskId || !['active', 'blocked', 'completed', 'archive'].includes(String(state)))
176
+ return { consistent: false };
177
+ if (state === 'active' ? !shortId : output.status !== 'no-op' || shortId !== null)
175
178
  return { consistent: false };
176
179
  try {
177
180
  const resolved = resolveTaskRef(taskId, { repoRoot: manifest.repoRoot });
178
181
  const shortIds = loadShortIdByTaskId(manifest.repoRoot);
179
182
  return {
180
- consistent: resolved.ok && resolved.taskId === taskId && resolved.state === 'active' && shortIds.get(taskId) === shortId,
183
+ consistent: resolved.ok && resolved.taskId === taskId && resolved.state === state
184
+ && (state === 'active' ? shortIds.get(taskId) === shortId : !shortIds.has(taskId)),
181
185
  taskId,
182
186
  shortId
183
187
  };
@@ -21,7 +21,9 @@ function stripQuotes(value) {
21
21
  }
22
22
  function readTaskContent(repoRoot, taskId) {
23
23
  for (const state of TASK_WORKSPACE_STATES) {
24
- const taskPath = path.join(repoRoot, '.agents', 'workspace', state, taskId, 'task.md');
24
+ const taskPath = state === 'archive'
25
+ ? path.join(repoRoot, '.agents', 'workspace', state, ...findArchiveTaskParts(repoRoot, taskId), 'local', 'task.md')
26
+ : path.join(repoRoot, '.agents', 'workspace', state, taskId, 'task.md');
25
27
  if (fs.existsSync(taskPath)) {
26
28
  return { content: fs.readFileSync(taskPath, 'utf8'), state, taskMd: taskPath };
27
29
  }
@@ -29,7 +31,7 @@ function readTaskContent(repoRoot, taskId) {
29
31
  throw new Error(`Task not found: ${taskId}`);
30
32
  }
31
33
  function taskWorkspaceCandidates(repoRoot, taskId) {
32
- return TASK_WORKSPACE_STATES.flatMap((state) => {
34
+ const hot = TASK_WORKSPACE_STATES.filter((state) => state !== 'archive').flatMap((state) => {
33
35
  const taskMd = path.join(repoRoot, '.agents', 'workspace', state, taskId, 'task.md');
34
36
  if (!fs.existsSync(taskMd))
35
37
  return [];
@@ -41,6 +43,37 @@ function taskWorkspaceCandidates(repoRoot, taskId) {
41
43
  taskMd
42
44
  }];
43
45
  });
46
+ const parts = findArchiveTaskParts(repoRoot, taskId);
47
+ if (parts.length === 0)
48
+ return hot;
49
+ const taskMd = path.join(repoRoot, '.agents', 'workspace', 'archive', ...parts, 'local', 'task.md');
50
+ if (!fs.existsSync(taskMd))
51
+ return hot;
52
+ const content = fs.readFileSync(taskMd, 'utf8');
53
+ return [...hot, {
54
+ taskId,
55
+ branch: resolveBranchFromTaskContent(content, taskId),
56
+ state: 'archive',
57
+ taskMd
58
+ }];
59
+ }
60
+ function findArchiveTaskParts(repoRoot, taskId) {
61
+ const archiveRoot = path.join(repoRoot, '.agents', 'workspace', 'archive');
62
+ if (!fs.existsSync(archiveRoot))
63
+ return [];
64
+ for (const year of fs.readdirSync(archiveRoot).sort().reverse()) {
65
+ if (!/^\d{4}$/.test(year))
66
+ continue;
67
+ for (const month of fs.readdirSync(path.join(archiveRoot, year)).sort().reverse()) {
68
+ if (!/^\d{2}$/.test(month))
69
+ continue;
70
+ for (const day of fs.readdirSync(path.join(archiveRoot, year, month)).sort().reverse()) {
71
+ if (/^\d{2}$/.test(day) && fs.existsSync(path.join(archiveRoot, year, month, day, taskId, 'local', 'task.md')))
72
+ return [year, month, day, taskId];
73
+ }
74
+ }
75
+ }
76
+ return [];
44
77
  }
45
78
  function resolveBranchFromTaskContent(content, taskId) {
46
79
  const frontmatterBranch = content.match(/^branch:\s*(.+)$/m);
@@ -81,6 +114,33 @@ export function resolveTaskWorkspace(taskId, repoRoot) {
81
114
  }
82
115
  export function listTaskWorkspaces(repoRoot, state) {
83
116
  const stateRoot = path.join(repoRoot, '.agents', 'workspace', state);
117
+ if (state === 'archive') {
118
+ if (!fs.existsSync(stateRoot))
119
+ return [];
120
+ const rows = [];
121
+ for (const year of fs.readdirSync(stateRoot).sort()) {
122
+ if (!/^\d{4}$/.test(year))
123
+ continue;
124
+ for (const month of fs.readdirSync(path.join(stateRoot, year)).sort()) {
125
+ if (!/^\d{2}$/.test(month))
126
+ continue;
127
+ for (const day of fs.readdirSync(path.join(stateRoot, year, month)).sort()) {
128
+ if (!/^\d{2}$/.test(day))
129
+ continue;
130
+ for (const taskId of fs.readdirSync(path.join(stateRoot, year, month, day)).sort()) {
131
+ if (!TASK_ID_RE.test(taskId))
132
+ continue;
133
+ const taskMd = path.join(stateRoot, year, month, day, taskId, 'local', 'task.md');
134
+ if (!fs.existsSync(taskMd))
135
+ continue;
136
+ const content = fs.readFileSync(taskMd, 'utf8');
137
+ rows.push({ taskId, branch: resolveBranchFromTaskContent(content, taskId), state, taskMd });
138
+ }
139
+ }
140
+ }
141
+ }
142
+ return rows;
143
+ }
84
144
  if (!fs.existsSync(stateRoot))
85
145
  return [];
86
146
  return fs.readdirSync(stateRoot, { withFileTypes: true })
@@ -1,8 +1,8 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { homedir } from 'node:os';
4
- import { createHash } from 'node:crypto';
5
4
  import { execFileSync } from 'node:child_process';
5
+ import { resolvePrimaryWorktreeRoot } from '../git/primary-worktree.js';
6
6
  const ENV_PREFIX = 'AGENT_INFRA_SERVER_';
7
7
  // Keys whose presence in the *committed* server.json is treated as a leaked
8
8
  // secret. Secrets belong in .agents/server.local.json or the environment.
@@ -16,17 +16,7 @@ export const DEFAULT_SERVER_CONFIG = {
16
16
  function isPlainObject(value) {
17
17
  return typeof value === 'object' && value !== null && !Array.isArray(value);
18
18
  }
19
- // Daemon runtime state (log + PID) lives OUTSIDE the repo, under the user's home
20
- // directory, keyed by the .airc.json "project" AND a stable hash of the repo
21
- // root path:
22
- // ~/.agent-infra/logs/<project>/<repo-hash>/server.log
23
- // ~/.agent-infra/run/<project>/<repo-hash>/server.pid
24
- // The <project> segment groups a project's checkouts for readability; the
25
- // <repo-hash> segment guarantees that two checkouts/worktrees of the same
26
- // project (same "project" but different absolute path) get ISOLATED runtime
27
- // dirs, so they never read/control each other's daemon. Using os.homedir() +
28
- // path.join keeps this correct on Windows too (C:\Users\<name>\.agent-infra\...).
29
- // An explicit log.path in server.json/.local/env still overrides the log default.
19
+ // Runtime state is shared by all worktrees through the primary checkout.
30
20
  function resolveProjectKey(repoRoot) {
31
21
  try {
32
22
  const airc = JSON.parse(fs.readFileSync(path.join(repoRoot, '.agents', '.airc.json'), 'utf8'));
@@ -39,12 +29,8 @@ function resolveProjectKey(repoRoot) {
39
29
  }
40
30
  return path.basename(repoRoot);
41
31
  }
42
- // Short, stable, filesystem-safe discriminator for a checkout's absolute path.
43
- function repoKey(repoRoot) {
44
- return createHash('sha256').update(repoRoot).digest('hex').slice(0, 12);
45
- }
46
- function runtimePath(repoRoot, projectKey, kind, file) {
47
- return path.join(homedir(), '.agent-infra', kind, projectKey, repoKey(repoRoot), file);
32
+ function runtimePath(projectKey, kind, file) {
33
+ return path.join(homedir(), '.agent-infra', kind, projectKey, file);
48
34
  }
49
35
  function detectRepoRoot() {
50
36
  try {
@@ -147,7 +133,7 @@ export function validateServerConfig(committed) {
147
133
  return { ok: true };
148
134
  }
149
135
  export function loadServerConfig({ rootDir } = {}) {
150
- const repoRoot = rootDir ?? detectRepoRoot();
136
+ const repoRoot = resolvePrimaryWorktreeRoot(rootDir ?? detectRepoRoot());
151
137
  const agentsDir = path.join(repoRoot, '.agents');
152
138
  const committed = readJsonIfPresent(path.join(agentsDir, 'server.json'));
153
139
  const validation = validateServerConfig(committed);
@@ -160,17 +146,11 @@ export function loadServerConfig({ rootDir } = {}) {
160
146
  merged = deepMerge(merged, envOverrides(process.env));
161
147
  const projectKey = resolveProjectKey(repoRoot);
162
148
  const log = isPlainObject(merged.log) ? merged.log : {};
163
- // No explicit log.path → default under ~/.agent-infra/logs/<project>/.
164
- // Explicit relative path resolves against the repo root; absolute is used as-is.
165
- const explicitPath = typeof log.path === 'string' ? log.path : null;
166
- const resolvedLogPath = explicitPath === null
167
- ? runtimePath(repoRoot, projectKey, 'logs', 'server.log')
168
- : (path.isAbsolute(explicitPath) ? explicitPath : path.join(repoRoot, explicitPath));
169
149
  return {
170
150
  repoRoot,
171
- pidFile: runtimePath(repoRoot, projectKey, 'run', 'server.pid'),
151
+ pidFile: runtimePath(projectKey, 'run', 'server.pid'),
172
152
  log: {
173
- path: resolvedLogPath,
153
+ path: runtimePath(projectKey, 'logs', 'server.log'),
174
154
  rotateAtBytes: typeof log.rotateAtBytes === 'number' ? log.rotateAtBytes : DEFAULT_ROTATE_BYTES
175
155
  },
176
156
  heartbeatMs: typeof merged.heartbeatMs === 'number' ? merged.heartbeatMs : DEFAULT_SERVER_CONFIG.heartbeatMs,
@@ -61,7 +61,7 @@ export function createMessageDispatcher(options) {
61
61
  title: `ai ${plan.argv.join(' ')}`,
62
62
  chunkChars: typeof options.config.stream?.chunkChars === 'number' ? options.config.stream.chunkChars : 4000,
63
63
  throttleMs: typeof options.config.stream?.throttleMs === 'number' ? options.config.stream.throttleMs : 1500
64
- }, (emit) => runAiImpl(plan.argv, { onChunk: emit }), (outbound) => replyOutbound(message, outbound));
64
+ }, (emit) => runAiImpl(plan.argv, { onChunk: emit, cwd: options.config.repoRoot }), (outbound) => replyOutbound(message, outbound));
65
65
  }
66
66
  };
67
67
  }
@@ -75,15 +75,16 @@ export function createMessageDispatcher(options) {
75
75
  // - runDaemon() awaits a shutdown promise that only resolves once a
76
76
  // SIGINT/SIGTERM handler has finished graceful cleanup. We never unref()
77
77
  // the only keep-alive timer (that would let the process exit immediately).
78
- export async function runDaemon() {
78
+ export async function runDaemon({ rootDir } = {}) {
79
79
  let config;
80
80
  try {
81
- config = loadServerConfig();
81
+ config = loadServerConfig({ rootDir });
82
82
  }
83
83
  catch (error) {
84
84
  process.stderr.write(`${errorMessage(error)}\n`);
85
85
  process.exit(1);
86
86
  }
87
+ process.chdir(config.repoRoot);
87
88
  const logger = createLogger(config.log);
88
89
  logger.info(`daemon starting agent-infra ${VERSION} pid=${process.pid}`);
89
90
  const ownStartTime = getProcessStartTime(process.pid);
@@ -1,4 +1,5 @@
1
1
  import fs from 'node:fs';
2
+ import path from 'node:path';
2
3
  import { spawn, execFileSync } from 'node:child_process';
3
4
  import { loadServerConfig } from './config.js';
4
5
  import { runDaemon } from './daemon.js';
@@ -65,7 +66,7 @@ export async function start({ foreground = false } = {}) {
65
66
  removePidFileIfMatches(pidPath, existing.snapshot);
66
67
  }
67
68
  if (foreground) {
68
- await runDaemon();
69
+ await runDaemon({ rootDir: config.repoRoot });
69
70
  return;
70
71
  }
71
72
  const cliEntry = process.argv[1];
@@ -75,8 +76,9 @@ export async function start({ foreground = false } = {}) {
75
76
  // Re-spawn ourselves detached. process.execArgv is forwarded so the dev path
76
77
  // (node --experimental-strip-types ./bin/cli.ts) and the built path
77
78
  // (node dist/bin/cli.js) both work.
78
- const child = spawn(process.execPath, [...process.execArgv, cliEntry, 'server', '__daemon'], {
79
+ const child = spawn(process.execPath, [...process.execArgv, path.resolve(cliEntry), 'server', '__daemon'], {
79
80
  detached: true,
81
+ cwd: config.repoRoot,
80
82
  stdio: 'ignore'
81
83
  });
82
84
  child.unref();
@@ -22,12 +22,13 @@ function resolveCommand(file) {
22
22
  function needsShell(file) {
23
23
  return process.platform === 'win32' && /\.(?:bat|cmd)$/i.test(file);
24
24
  }
25
- function spawnCapture(file, args, onChunk) {
25
+ function spawnCapture(file, args, onChunk, cwd) {
26
26
  return new Promise((resolve, reject) => {
27
27
  const resolvedFile = resolveCommand(file);
28
28
  const child = spawn(resolvedFile, args, {
29
29
  stdio: ['ignore', 'pipe', 'pipe'],
30
- shell: needsShell(resolvedFile)
30
+ shell: needsShell(resolvedFile),
31
+ ...(cwd === undefined ? {} : { cwd })
31
32
  });
32
33
  let stdout = '';
33
34
  let stderr = '';
@@ -70,6 +71,6 @@ function spawnCapture(file, args, onChunk) {
70
71
  });
71
72
  }
72
73
  export async function runAi(args, options = {}) {
73
- return (options.spawn ?? spawnCapture)('ai', args, options.onChunk);
74
+ return (options.spawn ?? spawnCapture)('ai', args, options.onChunk, options.cwd);
74
75
  }
75
76
  //# sourceMappingURL=runner.js.map
@@ -7,6 +7,8 @@ import { parseImplementationInputs, selectPendingImplementationInput } from './i
7
7
  import { parseVerdict } from './review-artifacts.js';
8
8
  import { extractSection, findSectionHeading } from './sections.js';
9
9
  import { receiptForOutput, sha256File } from './artifact-receipts.js';
10
+ import { hasArtifactCompletionFact, hasArtifactCompletionLog } from './completion-facts.js';
11
+ import { inspectReviewIdentity } from './review-identity.js';
10
12
  import { isArtifactInvalidated, parseInvalidationDocument } from './invalidation.js';
11
13
  import { parseLifecyclePathDecision } from './lifecycle-path.js';
12
14
  import { parseReworkIntentDocument } from './rework-intent.js';
@@ -149,7 +151,16 @@ function inspectArtifactDirectory(taskDir, family, identity = {}) {
149
151
  diagnostics.push(diagnostic('ROUND_GAP', family, null, `${artifactName(family, round)} is missing`));
150
152
  }
151
153
  const latest = artifacts.at(-1) ?? null;
152
- const reviewedInput = family.startsWith('review-') && latest
154
+ let hasReviewCodeReceipt = false;
155
+ if (family === 'review-code' && latest) {
156
+ try {
157
+ hasReviewCodeReceipt = Boolean(receiptForOutput(fs.readFileSync(path.join(taskDir, 'task.md'), 'utf8'), latest.name));
158
+ }
159
+ catch {
160
+ hasReviewCodeReceipt = false;
161
+ }
162
+ }
163
+ const reviewedInput = family.startsWith('review-') && latest && (family !== 'review-code' || hasReviewCodeReceipt)
153
164
  ? resolveReviewedInput(taskDir, latest, family === 'review-analysis' ? 'analysis' : family === 'review-plan' ? 'plan' : 'code', diagnostics)
154
165
  : null;
155
166
  return {
@@ -248,14 +259,16 @@ function assertWritableInventory(inventory) {
248
259
  };
249
260
  }
250
261
  const REQUIRED_INPUT = {
251
- 'review-analysis': 'analysis', plan: 'analysis', 'review-plan': 'plan', 'review-code': 'code'
262
+ 'review-analysis': 'analysis', plan: 'analysis', 'review-plan': 'plan'
252
263
  };
253
264
  const OPTIONAL_CONTEXT = {
254
- analysis: { family: 'review-analysis' },
255
- plan: { family: 'review-plan' },
256
- 'review-code': { family: 'review-plan' },
257
- 'manual-validation': { family: 'review-code' },
258
- 'validation-run': { family: 'review-code' }
265
+ analysis: ['review-analysis'],
266
+ plan: ['review-analysis', 'review-plan'],
267
+ 'review-plan': ['review-analysis'],
268
+ code: ['review-plan'],
269
+ 'review-code': ['code', 'plan', 'review-plan'],
270
+ 'manual-validation': ['review-code'],
271
+ 'validation-run': ['review-code']
259
272
  };
260
273
  function hasSelectionEvidence(content, family, options, context) {
261
274
  if (options.sourceFinding && options.sourceArtifact && options.sourceSha256)
@@ -265,6 +278,20 @@ function hasSelectionEvidence(content, family, options, context) {
265
278
  if (family.startsWith('review-')) {
266
279
  const reviewedFamily = family === 'review-analysis' ? 'analysis' : family === 'review-plan' ? 'plan' : 'code';
267
280
  const currentInput = context.inputs.find((input) => input.family === reviewedFamily);
281
+ if (family === 'review-code') {
282
+ if (!context.latest)
283
+ return false;
284
+ try {
285
+ const reportContent = fs.readFileSync(context.latest.path, 'utf8');
286
+ const identity = inspectReviewIdentity(context.taskDir, reportContent, options.repoRoot);
287
+ return identity.status !== 'matched'
288
+ || !hasArtifactCompletionFact(content, context.latest.path, 'review-code.completed')
289
+ || !hasArtifactCompletionLog(content, context.latest.name, 'review-code.completed');
290
+ }
291
+ catch {
292
+ return true;
293
+ }
294
+ }
268
295
  if (currentInput && context.reviewedInput?.name !== currentInput.name)
269
296
  return true;
270
297
  }
@@ -387,9 +414,10 @@ function resolveArtifactContext(taskRef, family, options = {}) {
387
414
  }
388
415
  const optional = OPTIONAL_CONTEXT[inventory.family];
389
416
  if (optional) {
390
- const context = inspectTaskArtifacts(taskRef, optional.family, options);
391
- if (context.status === 'ready' && context.latest) {
392
- inputs.push(context.latest);
417
+ for (const family of optional) {
418
+ const context = inspectTaskArtifacts(taskRef, family, options);
419
+ if (context.status === 'ready' && context.latest)
420
+ inputs.push(context.latest);
393
421
  }
394
422
  }
395
423
  if (inventory.family === 'manual-validation' || inventory.family === 'validation-run') {
@@ -411,6 +439,10 @@ function resolveCodeContext(inventory, options) {
411
439
  const codeMax = latestCode?.round ?? 0;
412
440
  const reviewMax = reviewCode.latest?.round ?? 0;
413
441
  const inputs = [source.latest];
442
+ const sourceReviewFamily = inputFamily === 'analysis' ? 'review-analysis' : 'review-plan';
443
+ const sourceReview = inspectTaskArtifacts(taskRef, sourceReviewFamily, options);
444
+ if (sourceReview.status === 'ready' && sourceReview.latest && sourceReview.reviewedInput?.name === source.latest.name)
445
+ inputs.push(sourceReview.latest);
414
446
  if (latestCode) {
415
447
  try {
416
448
  const taskContent = fs.readFileSync(path.join(inventory.taskDir, 'task.md'), 'utf8');