@tokenfactory/acc-runner 0.42.0 → 0.44.0

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 (111) hide show
  1. package/README.md +52 -3
  2. package/dist/companion-run.d.ts.map +1 -1
  3. package/dist/companion-run.js +20 -0
  4. package/dist/companion-run.js.map +1 -1
  5. package/dist/config.d.ts +72 -0
  6. package/dist/config.d.ts.map +1 -1
  7. package/dist/config.js +117 -3
  8. package/dist/config.js.map +1 -1
  9. package/dist/doctor.d.ts.map +1 -1
  10. package/dist/doctor.js +13 -3
  11. package/dist/doctor.js.map +1 -1
  12. package/dist/engines/account-probe.d.ts +24 -0
  13. package/dist/engines/account-probe.d.ts.map +1 -1
  14. package/dist/engines/account-probe.js +18 -1
  15. package/dist/engines/account-probe.js.map +1 -1
  16. package/dist/engines/registry.d.ts +17 -6
  17. package/dist/engines/registry.d.ts.map +1 -1
  18. package/dist/engines/registry.js +38 -13
  19. package/dist/engines/registry.js.map +1 -1
  20. package/dist/git-auth/github-token-source.d.ts +54 -0
  21. package/dist/git-auth/github-token-source.d.ts.map +1 -0
  22. package/dist/git-auth/github-token-source.js +44 -0
  23. package/dist/git-auth/github-token-source.js.map +1 -0
  24. package/dist/git-auth/index.d.ts +4 -0
  25. package/dist/git-auth/index.d.ts.map +1 -1
  26. package/dist/git-auth/index.js +4 -0
  27. package/dist/git-auth/index.js.map +1 -1
  28. package/dist/git-auth/installation-token.d.ts +95 -0
  29. package/dist/git-auth/installation-token.d.ts.map +1 -0
  30. package/dist/git-auth/installation-token.js +192 -0
  31. package/dist/git-auth/installation-token.js.map +1 -0
  32. package/dist/local-repo/command.d.ts +28 -0
  33. package/dist/local-repo/command.d.ts.map +1 -0
  34. package/dist/local-repo/command.js +178 -0
  35. package/dist/local-repo/command.js.map +1 -0
  36. package/dist/local-repo/index.d.ts +119 -0
  37. package/dist/local-repo/index.d.ts.map +1 -0
  38. package/dist/local-repo/index.js +141 -0
  39. package/dist/local-repo/index.js.map +1 -0
  40. package/dist/machine-capabilities/index.d.ts +21 -0
  41. package/dist/machine-capabilities/index.d.ts.map +1 -0
  42. package/dist/machine-capabilities/index.js +21 -0
  43. package/dist/machine-capabilities/index.js.map +1 -0
  44. package/dist/machine-capabilities/probe.d.ts +61 -0
  45. package/dist/machine-capabilities/probe.d.ts.map +1 -0
  46. package/dist/machine-capabilities/probe.js +243 -0
  47. package/dist/machine-capabilities/probe.js.map +1 -0
  48. package/dist/machine-capabilities/redact.d.ts +42 -0
  49. package/dist/machine-capabilities/redact.d.ts.map +1 -0
  50. package/dist/machine-capabilities/redact.js +160 -0
  51. package/dist/machine-capabilities/redact.js.map +1 -0
  52. package/dist/machine-capabilities/report.d.ts +107 -0
  53. package/dist/machine-capabilities/report.d.ts.map +1 -0
  54. package/dist/machine-capabilities/report.js +211 -0
  55. package/dist/machine-capabilities/report.js.map +1 -0
  56. package/dist/machine-capabilities/types.d.ts +115 -0
  57. package/dist/machine-capabilities/types.d.ts.map +1 -0
  58. package/dist/machine-capabilities/types.js +11 -0
  59. package/dist/machine-capabilities/types.js.map +1 -0
  60. package/dist/mcp-spawn.d.ts +19 -0
  61. package/dist/mcp-spawn.d.ts.map +1 -1
  62. package/dist/mcp-spawn.js +52 -26
  63. package/dist/mcp-spawn.js.map +1 -1
  64. package/dist/migration-seq-guard.d.ts +78 -0
  65. package/dist/migration-seq-guard.d.ts.map +1 -0
  66. package/dist/migration-seq-guard.js +304 -0
  67. package/dist/migration-seq-guard.js.map +1 -0
  68. package/dist/node-version.d.ts +132 -0
  69. package/dist/node-version.d.ts.map +1 -0
  70. package/dist/node-version.js +318 -0
  71. package/dist/node-version.js.map +1 -0
  72. package/dist/prepr-gate.d.ts +120 -0
  73. package/dist/prepr-gate.d.ts.map +1 -1
  74. package/dist/prepr-gate.js +202 -3
  75. package/dist/prepr-gate.js.map +1 -1
  76. package/dist/program.d.ts.map +1 -1
  77. package/dist/program.js +34 -0
  78. package/dist/program.js.map +1 -1
  79. package/dist/provider-auth.d.ts +51 -1
  80. package/dist/provider-auth.d.ts.map +1 -1
  81. package/dist/provider-auth.js +83 -2
  82. package/dist/provider-auth.js.map +1 -1
  83. package/dist/runtime/review-chunking.d.ts +101 -0
  84. package/dist/runtime/review-chunking.d.ts.map +1 -0
  85. package/dist/runtime/review-chunking.js +192 -0
  86. package/dist/runtime/review-chunking.js.map +1 -0
  87. package/dist/runtime/review-diff.d.ts +30 -0
  88. package/dist/runtime/review-diff.d.ts.map +1 -0
  89. package/dist/runtime/review-diff.js +59 -0
  90. package/dist/runtime/review-diff.js.map +1 -0
  91. package/dist/runtime/reviewer.d.ts +40 -19
  92. package/dist/runtime/reviewer.d.ts.map +1 -1
  93. package/dist/runtime/reviewer.js +283 -68
  94. package/dist/runtime/reviewer.js.map +1 -1
  95. package/dist/serve-scope/command.d.ts +12 -0
  96. package/dist/serve-scope/command.d.ts.map +1 -0
  97. package/dist/serve-scope/command.js +89 -0
  98. package/dist/serve-scope/command.js.map +1 -0
  99. package/dist/serve-scope/index.d.ts +130 -0
  100. package/dist/serve-scope/index.d.ts.map +1 -0
  101. package/dist/serve-scope/index.js +148 -0
  102. package/dist/serve-scope/index.js.map +1 -0
  103. package/dist/task-runner.d.ts +22 -2
  104. package/dist/task-runner.d.ts.map +1 -1
  105. package/dist/task-runner.js +142 -25
  106. package/dist/task-runner.js.map +1 -1
  107. package/dist/watch.d.ts +21 -0
  108. package/dist/watch.d.ts.map +1 -1
  109. package/dist/watch.js +109 -1
  110. package/dist/watch.js.map +1 -1
  111. package/package.json +1 -1
@@ -20,7 +20,7 @@ import path from "node:path";
20
20
  import { join } from "node:path";
21
21
  import { fileURLToPath, pathToFileURL } from "node:url";
22
22
  import { execa } from "execa";
23
- import { canServeRepo, loadProfile as defaultLoadProfile, resolveBaseBranch } from "./config.js";
23
+ import { canServeRepo, loadProfile as defaultLoadProfile, localRepoPathFor, resolveBaseBranch } from "./config.js";
24
24
  import { normalizeUsage, priceUsdCents, toCliAlias, } from "./cost-pricing.js";
25
25
  import { extractResetTime, isCapacityExhausted, isGitProvisionContention, } from "./failure-classifier.js";
26
26
  import { getEngine } from "./engines/registry.js";
@@ -46,18 +46,25 @@ import { parseConflictMeta, resolveConflict, MAX_CONFLICT_ATTEMPTS, } from "./ru
46
46
  import { parseReworkMeta, renderReworkPrompt, reconcileReworkBranch as defaultReconcileReworkBranch, fetchPrHeadRef as defaultFetchPrHeadRef, } from "./runtime/rework.js";
47
47
  import { assertResolutionIntegrity, isConflictResolutionTask, summariseFailures, } from "./runtime/resolution-integrity.js";
48
48
  import { runPreprGate as defaultRunPreprGate, } from "./prepr-gate.js";
49
+ import { revalidateMigrationSeq as defaultRevalidateMigrationSeq, } from "./migration-seq-guard.js";
49
50
  import { commitGuardSkipRequested, gitConfigHooksEnv, installCommitGuardSync, } from "./commit-guard.js";
50
51
  import { readLiveAllowedGlobs } from "./locks/index.js";
51
52
  const LOG_BATCH_BYTES = 4 * 1024;
52
- // M2 (ENGINE-EXTRACT): the coding engine this runner drives. Wave 1 resolves to
53
- // the default `claude-code` engine — spawn, argv, result-parse, failure-class,
54
- // and health-probe behavior are byte-identical to the pre-refactor inline logic
55
- // now owned by src/engines/claude-code.ts.
56
- const engine = getEngine();
53
+ // M2 (ENGINE-EXTRACT): the coding engine this runner drives. Wave 1 resolved
54
+ // this to a single module-level `claude-code` constant — spawn, argv,
55
+ // result-parse, failure-class, and health-probe behavior were byte-identical
56
+ // to the pre-refactor inline logic now owned by src/engines/claude-code.ts.
57
+ //
58
+ // NS MM-2 (BATCH-ENGINE-DISPATCH): a single module-level engine can no longer
59
+ // be right — a fleet with codex/gemini flagged on runs different tasks on
60
+ // different engines. Every call site below now resolves `getEngine(task.engine)`
61
+ // per task (default param omitted ⇒ `getEngine()` ⇒ claude-code, so a task with
62
+ // no engine stamped — or a runner that never sees a non-default id — is
63
+ // byte-identical to Wave 1).
57
64
  // v0.6.0 REG-296: argv builder for `claude --print`, re-exported from the
58
65
  // claude-code engine so the historical import surface is preserved.
59
66
  export { buildClaudeArgs } from "./engines/claude-code.js";
60
- function defaultSpawnClaude(cwd, modelId) {
67
+ function defaultSpawnClaude(cwd, modelId, engineId) {
61
68
  // RR-4: install the commit-time mechanical guard into this
62
69
  // worktree and point the agent's git at it via an ephemeral core.hooksPath env
63
70
  // (never a config-file write). The agent then hits an actionable in-session
@@ -76,7 +83,7 @@ function defaultSpawnClaude(cwd, modelId) {
76
83
  baseEnv = undefined;
77
84
  }
78
85
  }
79
- return engine.spawn({ cwd, modelId, baseEnv });
86
+ return getEngine(engineId).spawn({ cwd, modelId, baseEnv });
80
87
  }
81
88
  async function defaultCheckoutBase(repoPath, baseBranch) {
82
89
  // Plain `git checkout <branch>` (not `-B`) so the integration
@@ -306,8 +313,8 @@ export async function defaultReclaimStaleConflictLocks(supabase, conflicts, nowM
306
313
  }
307
314
  return reclaimed;
308
315
  }
309
- async function defaultHealthProbe() {
310
- return engine.healthProbe();
316
+ async function defaultHealthProbe(engineId) {
317
+ return getEngine(engineId).healthProbe();
311
318
  }
312
319
  const DEFAULT_CLAUDE_UNAVAILABLE_KNOBS = {
313
320
  windowMs: 15 * 60_000,
@@ -413,9 +420,14 @@ async function streamToEvents(stream, supabase, taskId, streamName) {
413
420
  * both `--output-format=json` (extracts .result) and plain text. Falls
414
421
  * back to the entire stdout (capped) if no `## Summary` heading is
415
422
  * present — that way the PR body still tells the reviewer what happened.
423
+ *
424
+ * NS MM-2: `engineId` is optional and appended last so every pre-existing
425
+ * caller (api/github/webhook.ts, the packages/acc-runner test suite) keeps
426
+ * compiling unchanged and keeps resolving `claude-code` — only the in-file
427
+ * per-task call site now forwards `task.engine`.
416
428
  */
417
- export function extractReportFromOutput(stdout) {
418
- const parsed = engine.parseResult(stdout);
429
+ export function extractReportFromOutput(stdout, engineId) {
430
+ const parsed = getEngine(engineId).parseResult(stdout);
419
431
  const text = parsed?.result ?? stdout;
420
432
  const idx = text.indexOf("## Summary");
421
433
  if (idx === -1) {
@@ -505,9 +517,13 @@ async function emitFirstCommit(supabase, git, taskId, workdir, baseRef, branch)
505
517
  * Build a cost event from claude's stdout. Always returns a payload
506
518
  * (zero-cost when usage isn't parseable) so the cap-tracking surface
507
519
  * stays consistent: one cost_events row per attempt.
520
+ *
521
+ * NS MM-2: `engineId` is optional and appended last for the same reason as
522
+ * `extractReportFromOutput` — every pre-existing caller keeps resolving
523
+ * `claude-code` unchanged.
508
524
  */
509
- export function buildCostEvent(taskId, stdout, fallbackModel, runnerId) {
510
- const parsed = engine.parseResult(stdout);
525
+ export function buildCostEvent(taskId, stdout, fallbackModel, runnerId, engineId) {
526
+ const parsed = getEngine(engineId).parseResult(stdout);
511
527
  const model = parsed?.model ?? fallbackModel ?? "unknown";
512
528
  const usage = normalizeUsage(parsed?.usage);
513
529
  return {
@@ -713,7 +729,11 @@ async function runConflictResolutionPath(taskId, task, deps, postState_, appendE
713
729
  export function runTask(taskId, deps) {
714
730
  const git = deps.git ?? defaultGit;
715
731
  const gh = deps.gh ?? defaultGh;
716
- const spawnClaude = deps.spawnClaude ?? defaultSpawnClaude;
732
+ // NS MM-2: `spawnClaude`'s default needs the FETCHED task's `engine` column,
733
+ // which isn't known this early (claim/transition haven't run yet) — its
734
+ // binding moves below, right after `const { task } = result;`, so the
735
+ // default resolves `getEngine(task.engine)` instead of the fixed default.
736
+ // `deps.spawnClaude` (test override) is unaffected either way.
717
737
  const checkoutBase = deps.checkoutBase ?? defaultCheckoutBase;
718
738
  const acquireLock = deps.acquireLock ?? defaultAcquireTaskLock;
719
739
  const prepareWorktree = deps.prepareWorktree ?? defaultPrepareTaskWorktree;
@@ -723,10 +743,13 @@ export function runTask(taskId, deps) {
723
743
  ((args) => defaultPostRunnerStateMessage(deps.supabase, args));
724
744
  const loadProfile = deps.loadProfile ?? defaultLoadProfile;
725
745
  const loadPromptPreludeFn = deps.loadPromptPrelude ?? loadPromptPrelude;
726
- const healthProbe = deps.healthProbe ?? defaultHealthProbe;
746
+ // NS MM-2: same reason as `spawnClaude` above — `healthProbe`'s default
747
+ // binds below, after the task (and its `engine`) is fetched.
727
748
  const runPreprGate = deps.runPreprGate ??
728
749
  ((workdir, baseRef) => defaultRunPreprGate(workdir, { baseRef }));
729
750
  const reconcileRework = deps.reconcileReworkBranch ?? defaultReconcileReworkBranch;
751
+ const revalidateSeq = deps.revalidateMigrationSeq ??
752
+ ((workdir, opts) => defaultRevalidateMigrationSeq(workdir, opts));
730
753
  // FU-9: default seeds the worktree from the warm parent clone (see
731
754
  // ensureWorktreeNodeModules); logs via the runner's stderr so a doctor can see it.
732
755
  const ensureWorktreeDeps = deps.ensureWorktreeDeps ??
@@ -999,7 +1022,21 @@ export function runTask(taskId, deps) {
999
1022
  return { taskId, status: "failed", phase: "fetch", error: msg };
1000
1023
  }
1001
1024
  const result = fetched.data;
1002
- const { task } = result;
1025
+ const task = result.task;
1026
+ // NS MM-2 (BATCH-ENGINE-DISPATCH): resolve this task's build engine ONCE,
1027
+ // right after the task itself is known — the router (NS MM-1) stamps
1028
+ // `engine` alongside `model_id` on every dispatch path, defaulting to
1029
+ // 'claude-code' on the DB column. `getEngine(null)` (an unstamped/
1030
+ // pre-MM-1 row) resolves the same default Wave 1 always used, so this is
1031
+ // byte-identical for the all-claude-code fleet. Every spawn/parse/classify
1032
+ // call below drives THIS engine instead of a fixed module-level default.
1033
+ const taskEngineId = task.engine ?? null;
1034
+ const taskEngine = getEngine(taskEngineId);
1035
+ // `spawnClaude`/`healthProbe` bind here (not in the sync prologue above)
1036
+ // because their defaults need `taskEngineId`, which isn't known until now.
1037
+ const spawnClaude = deps.spawnClaude ??
1038
+ ((cwd, modelId) => defaultSpawnClaude(cwd, modelId, taskEngineId));
1039
+ const healthProbe = deps.healthProbe ?? (() => defaultHealthProbe(taskEngineId));
1003
1040
  // LOCK-LIFECYCLE-WIRE: re-read the task's CURRENT file
1004
1041
  // scope from the live row so an operator who narrowed a QUEUED task between
1005
1042
  // dispatch and claim has that narrowing honoured for the agent's scope
@@ -1139,13 +1176,27 @@ export function runTask(taskId, deps) {
1139
1176
  // guarded clone step below, inside the provision-mutex. The repo_path_hint
1140
1177
  // operator pin always wins; with the flag off this is byte-identical to
1141
1178
  // the historic `task.repo_path_hint ?? cfg.repoPath` resolution.
1179
+ //
1180
+ // LOCAL-REPO-CLI: a local repo (`local.<frag>/<name>`, migration 0318) has no
1181
+ // remote to clone from — its only copy is the working directory the operator
1182
+ // pointed `acc repo add-local` at, recorded in this machine's config.json.
1183
+ // So its path comes from cfg.localRepos and, critically, the clone cache is
1184
+ // suppressed: cloning a `local.…` slug would try to reach a GitHub repo that
1185
+ // does not exist. An explicit repo_path_hint still wins (an operator pin is
1186
+ // always the last word), and a task for a repo this machine has no local
1187
+ // entry for resolves exactly as before.
1142
1188
  const repoPathHint = task.repo_path_hint?.trim();
1143
- const useCloneCache = !repoPathHint && !!deps.cfg.cloneOnDemand && !!task.repo?.trim();
1189
+ const localRepoPath = repoPathHint
1190
+ ? null
1191
+ : localRepoPathFor(deps.cfg, task.repo);
1192
+ const useCloneCache = !repoPathHint && !localRepoPath && !!deps.cfg.cloneOnDemand && !!task.repo?.trim();
1144
1193
  const repoPath = repoPathHint
1145
1194
  ? expandHomePath(repoPathHint)
1146
- : useCloneCache
1147
- ? clonePathForRepo(task.repo.trim())
1148
- : deps.cfg.repoPath;
1195
+ : localRepoPath
1196
+ ? expandHomePath(localRepoPath)
1197
+ : useCloneCache
1198
+ ? clonePathForRepo(task.repo.trim())
1199
+ : deps.cfg.repoPath;
1149
1200
  const targetRepo = task.repo?.trim() || deps.cfg.targetRepo;
1150
1201
  // REG-301: branch base comes from task → cfg → main. cfg.integrationBranch
1151
1202
  // already defaults to "acc/integration" so the third fallback only
@@ -1318,6 +1369,62 @@ export function runTask(taskId, deps) {
1318
1369
  error: `conflict-resolution integrity guardrail rejected resolution (${summary})`,
1319
1370
  };
1320
1371
  };
1372
+ /**
1373
+ * MIG-SEQ-GUARD: last thing before `git push`, re-validate this branch's
1374
+ * NEW migration seq against a FRESHLY-FETCHED integration head. The seq was
1375
+ * chosen at worktree-fork time; by push time a sibling migration may have
1376
+ * merged and taken it, and until now that only surfaced as a migration-lint
1377
+ * failure on the PR — a whole run spent on a rename the runner can do
1378
+ * itself. When the number is gone the migration is renumbered to the next
1379
+ * free contiguous seq (filename + self-record + verify messages + every
1380
+ * test that reads it by path) and committed, so the push carries the fix.
1381
+ *
1382
+ * Never blocks: the guard is fail-open by construction and this returns
1383
+ * void. A renumber emits `migration.seq_renumbered` so the move is visible
1384
+ * on the History tab rather than being a silent rewrite.
1385
+ */
1386
+ const runMigrationSeqGuard = async (rework) => {
1387
+ let result;
1388
+ try {
1389
+ result = await revalidateSeq(workdir, {
1390
+ integrationBranch,
1391
+ baseRef: resolvedBaseRef,
1392
+ remote: "origin",
1393
+ });
1394
+ }
1395
+ catch (err) {
1396
+ // The guard already fails open internally; this catches a stub/dep that
1397
+ // throws so a seq check can never be the reason a push does not happen.
1398
+ process.stderr.write(`[acc-runner] migration seq guard crashed (push proceeds): ` +
1399
+ `${err.message?.slice(0, 200)}\n`);
1400
+ return;
1401
+ }
1402
+ if (!result.renumbered || !result.move) {
1403
+ // Quiet on the no-op path — the overwhelming majority of pushes touch
1404
+ // no migration at all, and an event per push would be pure noise.
1405
+ if (result.skipped && result.skipped !== "no-new-migrations") {
1406
+ process.stderr.write(`[acc-runner] migration seq guard: ${result.detail}\n`);
1407
+ }
1408
+ return;
1409
+ }
1410
+ await appendEvent(deps.supabase, taskId, "log", {
1411
+ phase: "push",
1412
+ stream: "stdout",
1413
+ event: "migration.seq_renumbered",
1414
+ branch,
1415
+ rework,
1416
+ integration_branch: integrationBranch,
1417
+ from_seq: result.move.fromSeq,
1418
+ to_seq: result.move.toSeq,
1419
+ file: result.move.file,
1420
+ new_file: result.move.newFile,
1421
+ remote_max_seq: result.remoteMaxSeq ?? null,
1422
+ message: result.detail,
1423
+ renumber_output: result.move.output,
1424
+ runner_id: deps.session.runnerId,
1425
+ });
1426
+ process.stderr.write(`[acc-runner] migration seq guard: ${result.detail}\n`);
1427
+ };
1321
1428
  try {
1322
1429
  // POOL-P1b: clone-on-demand source provisioning. When the flag is on and
1323
1430
  // repoPath resolved to the per-repo cache path above, clone the task's
@@ -1715,7 +1822,7 @@ export function runTask(taskId, deps) {
1715
1822
  // cancellation, retry, and non-zero exit alike. Best-effort: a
1716
1823
  // failed POST logs to stderr but never bubbles past the runner.
1717
1824
  {
1718
- const event = buildCostEvent(taskId, capturedStdout, result.model?.id, result.runner?.id);
1825
+ const event = buildCostEvent(taskId, capturedStdout, result.model?.id, result.runner?.id, taskEngineId);
1719
1826
  try {
1720
1827
  await postCostEvent(event);
1721
1828
  }
@@ -1754,7 +1861,7 @@ export function runTask(taskId, deps) {
1754
1861
  // claude_unavailable only, so this exit-0 capacity case never retries.
1755
1862
  const classifyIfFailed = (a) => {
1756
1863
  if (a.exitCode !== 0) {
1757
- return engine.classifyFailure(a.exitCode, a.stderr, a.stdout, a.durationMs);
1864
+ return taskEngine.classifyFailure(a.exitCode, a.stderr, a.stdout, a.durationMs);
1758
1865
  }
1759
1866
  if (isCapacityExhausted(a.stdout, a.stderr)) {
1760
1867
  return {
@@ -1809,7 +1916,7 @@ export function runTask(taskId, deps) {
1809
1916
  // because v0.63's health probe can upgrade a claude_unavailable exit
1810
1917
  // to env_broken when the probe proves the host is genuinely broken.
1811
1918
  let cls = classified ??
1812
- engine.classifyFailure(attempt.exitCode, attempt.stderr, attempt.stdout, attempt.durationMs);
1919
+ taskEngine.classifyFailure(attempt.exitCode, attempt.stderr, attempt.stdout, attempt.durationMs);
1813
1920
  // FH-1a: capacity exhaustion mid-task is NOT a task failure — but it
1814
1921
  // must not ABANDON the in-flight task either. v0.56 left the task
1815
1922
  // 'running' with its signal loop stopped (the outer finally) so the
@@ -2166,6 +2273,11 @@ export function runTask(taskId, deps) {
2166
2273
  return { taskId, status: "failed", phase: "push", error: msg };
2167
2274
  }
2168
2275
  }
2276
+ // MIG-SEQ-GUARD: after the no-advance check (which compares heads) and
2277
+ // immediately before the push, so the freshest possible integration head
2278
+ // decides the seq. A renumber adds a commit, which is also what makes the
2279
+ // advance check above meaningful on a rework that only moved a number.
2280
+ await runMigrationSeqGuard(true);
2169
2281
  try {
2170
2282
  await git.push(workdir, branch);
2171
2283
  }
@@ -2325,6 +2437,11 @@ export function runTask(taskId, deps) {
2325
2437
  runner_id: deps.session.runnerId,
2326
2438
  });
2327
2439
  }
2440
+ // MIG-SEQ-GUARD: the LAST thing before the push. It deliberately runs
2441
+ // after the pre-PR gate rather than inside it — the gate's own MIGFIX-01
2442
+ // renumber reads the fork-time worktree snapshot, and only a fresh fetch
2443
+ // here can see a sibling that merged while the gate was running.
2444
+ await runMigrationSeqGuard(false);
2328
2445
  // 5. Push + open PR. Both run from the worktree so the operator's
2329
2446
  // shared clone never has the task branch checked out.
2330
2447
  let prUrl = "";
@@ -2380,7 +2497,7 @@ export function runTask(taskId, deps) {
2380
2497
  branch,
2381
2498
  });
2382
2499
  try {
2383
- const report = extractReportFromOutput(capturedStdout);
2500
+ const report = extractReportFromOutput(capturedStdout, taskEngineId);
2384
2501
  // V96 (R4): generate the PR-body "Files changed" table FROM the actual
2385
2502
  // pushed diff at PR-open so the body can never contradict the diff (a
2386
2503
  // hand-listed file that drifted from reality). Best-effort — if the