@magnusekdahl/parallix 1.1.0 → 1.2.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 (68) hide show
  1. package/config/integration-pipelines.json +9 -0
  2. package/config/workflow.config.schema.json +12 -0
  3. package/docs/adr/0032-mission-refinement-state-and-usage-budget-signals.md +10 -8
  4. package/docs/adr/0036-mission-sizing-and-dependency-wave-heuristics.md +14 -12
  5. package/docs/adr/0041-integration-pipeline-gates.md +1 -1
  6. package/docs/adr/0047-per-mission-change-size-budget.md +161 -0
  7. package/docs/adr/index.md +1 -0
  8. package/docs/use-cases.md +44 -0
  9. package/lib/agents/agents.js +149 -100
  10. package/lib/agents/claude-telemetry.js +14 -10
  11. package/lib/agents/claude.js +32 -11
  12. package/lib/agents/codex-telemetry.js +32 -19
  13. package/lib/agents/codex.js +36 -11
  14. package/lib/agents/limit-hit.js +42 -25
  15. package/lib/agents/mistral-telemetry.js +1 -1
  16. package/lib/agents/mistral.js +13 -3
  17. package/lib/agents/opencode-export.js +11 -5
  18. package/lib/agents/opencode-telemetry.js +75 -90
  19. package/lib/agents/opencode.js +86 -29
  20. package/lib/agents/stage-telemetry.js +3 -7
  21. package/lib/commands/active.js +139 -94
  22. package/lib/commands/checkpoint.js +3 -1
  23. package/lib/commands/config.js +5 -3
  24. package/lib/commands/coverage-gate.js +17 -10
  25. package/lib/commands/diff.js +11 -5
  26. package/lib/commands/draft.js +95 -55
  27. package/lib/commands/handoff.js +253 -65
  28. package/lib/commands/integrate.js +288 -188
  29. package/lib/commands/mission-start.js +39 -34
  30. package/lib/commands/rebase.js +32 -21
  31. package/lib/commands/repair-handoff.js +21 -18
  32. package/lib/commands/resolve-conflict.js +5 -2
  33. package/lib/commands/review.js +1 -0
  34. package/lib/commands/setup-review.js +1 -0
  35. package/lib/commands/stats-backfill.js +50 -41
  36. package/lib/commands/stats.js +653 -204
  37. package/lib/commands/status.js +33 -28
  38. package/lib/core/fmt.js +58 -15
  39. package/lib/core/git.js +7 -5
  40. package/lib/core/gitignore.js +6 -4
  41. package/lib/core/mission-utils.js +161 -88
  42. package/lib/core/nels.js +199 -0
  43. package/lib/core/persistent-data-migration.js +55 -23
  44. package/lib/core/product-config.js +44 -23
  45. package/lib/core/runtime-matrix.js +15 -2
  46. package/lib/core/spawn-tee.js +40 -13
  47. package/lib/core/state-map.js +32 -13
  48. package/lib/core/storage.js +19 -5
  49. package/lib/core/subagent-limit.js +28 -0
  50. package/lib/core/verification.js +29 -12
  51. package/lib/review/rebase.js +11 -3
  52. package/lib/review/review-adapter.js +93 -6
  53. package/lib/review/review-artifacts.js +89 -23
  54. package/lib/review/review-commands.js +139 -45
  55. package/lib/review/review-events.js +103 -52
  56. package/lib/review/review-loop.js +117 -87
  57. package/lib/review/review-polling.js +25 -5
  58. package/lib/review/review-prompts.js +36 -5
  59. package/lib/review/review-state.js +39 -15
  60. package/lib/tools/backlog.js +162 -71
  61. package/lib/tools/forgejo.js +290 -129
  62. package/lib/tools/gatekeeper.js +15 -0
  63. package/lib/tools/redgreen.js +220 -0
  64. package/lib/tools/sessions.js +12 -5
  65. package/lib/tools/setup-review.js +170 -70
  66. package/package.json +11 -3
  67. package/prompts/draft.md +6 -0
  68. package/templates/mission-scaffold.md +1 -1
@@ -7,9 +7,9 @@ const { resolveTaskFile, getTaskStatus, setTaskStatus, completeTask, getTaskAssi
7
7
  const { toVirtual, toActual } = require('../core/state-map');
8
8
  const { getPrStatus, getLatestReviewDecision, syncMerged, readToken, resolveTokenFile, resolveForgejoUser, resolveForgejoHome, isForgejoPath, listOpenPrsForSlug } = require('../tools/forgejo');
9
9
  const fmt = require('../core/fmt');
10
- const { KNOWN_AGENT_NAMES } = require('../agents/agents');
10
+
11
11
  const { buildAutonomousReviewMatrix, formatMatrixSummary } = require('../core/runtime-matrix');
12
- const { findMissionDir, resolveWorktree, findMissionArea, missionTitle, parseConflictFilesFromMergeOutput, getConflictFiles, inferSlug, updateGraphifyKnowledgeGraph, resolveMainRepo, getPrimaryWorktree, getPrimaryBranch, conventionalWorktreePath, getMissionYear, softResetTrailingBacklogNoise, findMissionDocInBranches, missionBranchName, missionBaseDir, missionDirForSlug, isMissionArtifact, resolveMissionBaseBranch, resolveBaseWorktree } = require('../core/mission-utils');
12
+ const { findMissionDir, resolveWorktree, findMissionArea, missionTitle, parseConflictFilesFromMergeOutput, getConflictFiles, inferSlug, updateGraphifyKnowledgeGraph, getPrimaryWorktree, getPrimaryBranch, conventionalWorktreePath, softResetTrailingBacklogNoise, findMissionDocInBranches, missionBranchName, missionDirForSlug, isMissionArtifact, resolveMissionBaseBranch, resolveBaseWorktree } = require('../core/mission-utils');
13
13
  const stats = require('./stats');
14
14
  const verification = require('../core/verification');
15
15
  const { formatVerificationCommand } = verification;
@@ -18,6 +18,7 @@ const { readReviewState } = require('../review/review-state');
18
18
 
19
19
  const VARIANT_B_AUTOMATION_SUMMARY = 'Variant B automation: Backlog task closeout, worktree-path rewrite, squash commit with hook-enforced validation, Forgejo sync-merged, and mission worktree cleanup.';
20
20
 
21
+ /** @type{{symptom: string, cause: string, fix: string}[]} */
21
22
  const SYNC_MERGED_DIAGNOSTICS = [
22
23
  { symptom: 'sync-merged reports generic failure; PR still open', cause: 'allow_manual_merge disabled or drifted off in Forgejo settings', fix: 'Enable "Allow manual merge" in Forgejo PR settings and retry.' },
23
24
  { symptom: 'POST /pulls/N/merge returns 405 or 500', cause: 'Forgejo API conflict or stale PR state', fix: 'px review <slug> to check if actually merged.' },
@@ -26,28 +27,34 @@ const SYNC_MERGED_DIAGNOSTICS = [
26
27
  { symptom: 'PR marked merged but remote branch still exists', cause: 'Remote branch deletion failed (permissions or network)', fix: 'px review <slug> --close (to trigger cleanup; identity resolves from review-state).' }
27
28
  ];
28
29
 
30
+ /** @extends{Error} */
29
31
  class IntegrationAbort extends Error {}
30
32
 
33
+ /** @param {{mission: string, implementer: string, pr_fix_rounds: string, classification: string, date: string}} row */
31
34
  function formatRecordedStatsRow(row) {
32
35
  return `${row.mission}: implementer=${row.implementer}, pr_fix_rounds=${row.pr_fix_rounds}, classification=${row.classification}, date=${row.date}`;
33
36
  }
34
37
 
38
+ /** @param {string} value */
35
39
  function shellQuote(value) {
36
40
  return `"${String(value).replace(/(["\\$`])/g, '\\$1')}"`;
37
41
  }
38
42
 
39
- function maybeUpdateGraphifyOnPrimary(rootDir = getPrimaryWorktree(), { commandRunner = null, log = fmt.log.plain } = {}) {
43
+ /** @param {string} rootDir @param {{commandRunner?: Function, log?: Function}} opts */
44
+ function maybeUpdateGraphifyOnPrimary(rootDir = getPrimaryWorktree(), opts = {}) {
40
45
  return updateGraphifyKnowledgeGraph({
41
46
  rootDir,
42
- commandRunner: commandRunner || ((command, args, options) => require('../core/git').run(command, args, options)),
43
- log,
47
+ commandRunner: opts.commandRunner || ((/** @type{string} */ command, /** @type{string[]} */ args, /** @type{object} */ options) => require('../core/git').run(command, args, options)),
48
+ log: opts.log,
44
49
  startMessage: `Updating graphify knowledge graph on ${getPrimaryBranch()}...`,
45
50
  failureHint: 'Continuing without blocking integration.'
46
51
  });
47
52
  }
48
53
 
49
- function prepareNoisePatchForSquash(rootDir, { gitRunner = git } = {}) {
50
- const diffResult = gitRunner(['-C', rootDir, 'diff', '--cached', '--binary']);
54
+ /** @param {string} rootDir @param {{gitRunner?: Function}} opts */
55
+ function prepareNoisePatchForSquash(rootDir, opts = {}) {
56
+ const runner = opts.gitRunner || git;
57
+ const diffResult = runner(['-C', rootDir, 'diff', '--cached', '--binary']);
51
58
  if (diffResult.status !== 0) {
52
59
  return {
53
60
  ok: false,
@@ -64,7 +71,7 @@ function prepareNoisePatchForSquash(rootDir, { gitRunner = git } = {}) {
64
71
  const patchPath = path.join(patchDir, 'backlog-noise.patch');
65
72
  fs.writeFileSync(patchPath, patch, 'utf8');
66
73
 
67
- const resetResult = gitRunner(['-C', rootDir, 'reset', '--hard', 'HEAD']);
74
+ const resetResult = runner(['-C', rootDir, 'reset', '--hard', 'HEAD']);
68
75
  if (resetResult.status !== 0) {
69
76
  return {
70
77
  ok: false,
@@ -80,9 +87,11 @@ function prepareNoisePatchForSquash(rootDir, { gitRunner = git } = {}) {
80
87
  };
81
88
  }
82
89
 
83
- function restoreNoisePatchAfterSquash(rootDir, patchPath, { gitRunner = git } = {}) {
84
- if (!patchPath) return { ok: true };
85
- const applyResult = gitRunner(['-C', rootDir, 'apply', '--index', patchPath]);
90
+ /** @param {string} rootDir @param {string|null} patchPath @param {{gitRunner?: Function}} opts */
91
+ function restoreNoisePatchAfterSquash(rootDir, patchPath, opts = {}) {
92
+ if (!patchPath) {return { ok: true };}
93
+ const runner = opts.gitRunner || git;
94
+ const applyResult = runner(['-C', rootDir, 'apply', '--index', patchPath]);
86
95
  if (applyResult.status !== 0) {
87
96
  return {
88
97
  ok: false,
@@ -92,6 +101,7 @@ function restoreNoisePatchAfterSquash(rootDir, patchPath, { gitRunner = git } =
92
101
  return { ok: true };
93
102
  }
94
103
 
104
+ /** @param {string|null} taskAssignee */
95
105
  function resolveForgejoUserForIntegration(taskAssignee) {
96
106
  // If taskAssignee is set, use it directly as the Forgejo user identity.
97
107
  // This ensures that even non-agent assignees (e.g., human users) have their
@@ -105,13 +115,15 @@ function resolveForgejoUserForIntegration(taskAssignee) {
105
115
 
106
116
  // Fall back to environment variable or default only when no assignee is available
107
117
  return {
108
- forgejoUser: resolveForgejoUser(),
118
+ forgejoUser: resolveForgejoUser(""),
109
119
  warning: null
110
120
  };
111
121
  }
112
122
 
113
- function getUnresolvedIndexConflicts(rootDir = getPrimaryWorktree(), { gitRunner = git } = {}) {
114
- const result = gitRunner(['-C', rootDir, 'ls-files', '-u']);
123
+ /** @param {string} rootDir @param {{gitRunner?: Function}} opts */
124
+ function getUnresolvedIndexConflicts(rootDir = getPrimaryWorktree(), opts = {}) {
125
+ const runner = opts.gitRunner || git;
126
+ const result = runner(['-C', rootDir, 'ls-files', '-u']);
115
127
  if (result.status !== 0) {
116
128
  return {
117
129
  ok: false,
@@ -123,9 +135,9 @@ function getUnresolvedIndexConflicts(rootDir = getPrimaryWorktree(), { gitRunner
123
135
  const files = Array.from(new Set(
124
136
  result.stdout
125
137
  .split('\n')
126
- .map(line => line.trim())
138
+ .map((/** @type{string} */ line) => line.trim())
127
139
  .filter(Boolean)
128
- .map(line => line.split('\t')[1])
140
+ .map((/** @type{string} */ line) => line.split('\t')[1])
129
141
  .filter(Boolean)
130
142
  ));
131
143
 
@@ -143,20 +155,19 @@ function parseStashPopCollisionFiles(output = '') {
143
155
  .map(line => line.replace(/\s+already exists, no checkout$/i, ''));
144
156
  }
145
157
 
146
- function reportStashPopFailure(
147
- slug,
148
- restoreResult,
149
- {
150
- rootDir = getPrimaryWorktree(),
151
- gitRunner = git,
152
- getUnresolvedIndexConflictsFn = targetRootDir => getUnresolvedIndexConflicts(targetRootDir, { gitRunner })
153
- } = {}
154
- ) {
158
+ /**
159
+ * @param {string} slug
160
+ * @param {{stdout: string, stderr: string, status: number}} restoreResult
161
+ * @param {{rootDir?: string, gitRunner?: Function, getUnresolvedIndexConflictsFn?: Function}} opts
162
+ */
163
+ function reportStashPopFailure(slug, restoreResult, opts = {}) {
155
164
  const output = [restoreResult.stdout, restoreResult.stderr].filter(Boolean).join('\n').trim();
156
- const headResult = gitRunner(['-C', rootDir, 'log', '-1', '--oneline']);
165
+ const runner = opts.gitRunner || git;
166
+ const rootDir = opts.rootDir || getPrimaryWorktree();
167
+ const headResult = runner(['-C', rootDir, 'log', '-1', '--oneline']);
157
168
  const headLine = headResult.status === 0 ? headResult.stdout.trim() : '(unavailable)';
158
169
  const integrationLanded = headLine.includes(`${missionBranchName(slug, rootDir)}:`);
159
- const indexConflicts = getUnresolvedIndexConflictsFn(rootDir);
170
+ const indexConflicts = (opts.getUnresolvedIndexConflictsFn || getUnresolvedIndexConflicts)(rootDir);
160
171
  const collisionFiles = parseStashPopCollisionFiles(output);
161
172
 
162
173
  fmt.log.fail('Could not restore the temporarily stashed local integration checkout changes.');
@@ -168,10 +179,10 @@ function reportStashPopFailure(
168
179
 
169
180
  if (indexConflicts.ok && indexConflicts.files.length > 0) {
170
181
  fmt.log.fail('Stash restore failure type: merge-conflict');
171
- indexConflicts.files.forEach(file => fmt.log.fail(` - ${file}`));
182
+ indexConflicts.files.forEach((/** @type{string} */ file) => fmt.log.fail(` - ${file}`));
172
183
  fmt.log.fail('Recovery steps:');
173
184
  fmt.log.fail(` 1. cd ${rootDir}`);
174
- indexConflicts.files.forEach(file => {
185
+ indexConflicts.files.forEach((/** @type{string} */ file) => {
175
186
  fmt.log.fail(` 2. Resolve ${file}, then run git add "${file}" or git rm "${file}"`);
176
187
  });
177
188
  fmt.log.fail(' 3. git status --short');
@@ -179,12 +190,12 @@ function reportStashPopFailure(
179
190
  } else {
180
191
  fmt.log.fail('Stash restore failure type: file-collision');
181
192
  if (collisionFiles.length > 0) {
182
- collisionFiles.forEach(file => fmt.log.fail(` - ${file}`));
193
+ collisionFiles.forEach((/** @type{string} */ file) => fmt.log.fail(` - ${file}`));
183
194
  }
184
195
  fmt.log.fail('Recovery steps:');
185
196
  fmt.log.fail(` 1. git -C ${rootDir} stash show --name-only stash@{0}`);
186
197
  if (collisionFiles.length > 0) {
187
- collisionFiles.forEach(file => {
198
+ collisionFiles.forEach((/** @type{string} */ file) => {
188
199
  fmt.log.fail(` 2. mv ${path.join(rootDir, file)} ${path.join(rootDir, `${file}.pre-stash-pop`)}`);
189
200
  });
190
201
  fmt.log.fail(` 3. git -C ${rootDir} stash pop`);
@@ -210,6 +221,7 @@ function printDiagnosticTable() {
210
221
  fmt.log.info('------------------------------------\n');
211
222
  }
212
223
 
224
+ /** @param {{statusCode?: number, error: string, raw?: string}} syncResult */
213
225
  function reportSyncMergedFailure(syncResult) {
214
226
  const detail = syncResult.statusCode ? ` (${syncResult.error}: ${syncResult.statusCode})` : ` (${syncResult.error})`;
215
227
  fmt.log.fail(`Forgejo sync-merged failed${detail}.`);
@@ -227,18 +239,20 @@ const INTEGRATION_CONFIG_PATH = path.join(getPrimaryWorktree(), 'config', 'integ
227
239
  * Detect which top-level areas have been modified in the mission branch vs primary branch
228
240
  * Returns an array of area names (e.g., ['server', 'auth-server', 'web-client'])
229
241
  */
230
- function detectChangedAreas(slug, { gitRunner = git, rootDir = getPrimaryWorktree(), baseBranch = null } = {}) {
231
- const primaryBranch = baseBranch || getPrimaryBranch();
242
+ /** @param {string} slug @param {{gitRunner?: Function, rootDir?: string, baseBranch?: string|null}} opts */
243
+ function detectChangedAreas(slug, opts = {}) {
244
+ const primaryBranch = opts.baseBranch || getPrimaryBranch();
232
245
  const branch = `mission/${slug}`;
246
+ const runner = opts.gitRunner || git;
233
247
 
234
248
  // Get changed files between primary branch and mission branch
235
- const diffResult = gitRunner(['-C', rootDir, 'diff', '--name-only', primaryBranch, branch, '--']);
249
+ const diffResult = runner(['-C', opts.rootDir || getPrimaryWorktree(), 'diff', '--name-only', primaryBranch, branch, '--']);
236
250
 
237
251
  if (diffResult.status !== 0 && !diffResult.stdout.trim()) {
238
252
  // No changes or error - fall back to checking against HEAD in the worktree
239
- const worktree = resolveWorktree(slug) || conventionalWorktreePath(slug, rootDir);
253
+ const worktree = resolveWorktree(slug) || conventionalWorktreePath(slug, opts.rootDir);
240
254
  if (fs.existsSync(worktree)) {
241
- const worktreeDiff = gitRunner(['-C', worktree, 'diff', '--name-only', primaryBranch, 'HEAD', '--']);
255
+ const worktreeDiff = runner(['-C', worktree, 'diff', '--name-only', primaryBranch, 'HEAD', '--']);
242
256
  if (worktreeDiff.status === 0 && worktreeDiff.stdout.trim()) {
243
257
  return parseFilesToAreas(worktreeDiff.stdout);
244
258
  }
@@ -252,13 +266,14 @@ function detectChangedAreas(slug, { gitRunner = git, rootDir = getPrimaryWorktre
252
266
  /**
253
267
  * Parse a list of files (one per line) and extract the top-level area directories
254
268
  */
269
+ /** @param {string} filesOutput */
255
270
  function parseFilesToAreas(filesOutput) {
256
271
  const areas = new Set();
257
- const knownAreas = ['server', 'auth-server', 'web-client', 'docs', 'workflow', 'android', 'kubernetes'];
272
+ const knownAreas = ['lib', 'server', 'auth-server', 'web-client', 'docs', 'workflow', 'android', 'kubernetes'];
258
273
 
259
- filesOutput.split('\n').forEach(file => {
274
+ filesOutput.split('\n').forEach((/** @type{string} */ file) => {
260
275
  file = file.trim();
261
- if (!file) return;
276
+ if (!file) {return;}
262
277
  const topDir = file.split('/')[0];
263
278
  if (knownAreas.includes(topDir)) {
264
279
  areas.add(topDir);
@@ -271,7 +286,9 @@ function parseFilesToAreas(filesOutput) {
271
286
  /**
272
287
  * Load integration pipelines config from repo-side file
273
288
  */
274
- function loadIntegrationConfig({ configPath = INTEGRATION_CONFIG_PATH } = {}) {
289
+ /** @param {{configPath?: string}} opts */
290
+ function loadIntegrationConfig(opts = {}) {
291
+ const configPath = opts.configPath || INTEGRATION_CONFIG_PATH;
275
292
  if (!fs.existsSync(configPath) || !fs.statSync(configPath).size) {
276
293
  return { ok: false, error: 'no config present' };
277
294
  }
@@ -280,7 +297,7 @@ function loadIntegrationConfig({ configPath = INTEGRATION_CONFIG_PATH } = {}) {
280
297
  const content = fs.readFileSync(configPath, 'utf8');
281
298
  const config = JSON.parse(content);
282
299
  return { ok: true, config };
283
- } catch (error) {
300
+ } catch (/** @type{any} */ error) {
284
301
  return { ok: false, error: `invalid JSON: ${error.message}` };
285
302
  }
286
303
  }
@@ -289,11 +306,12 @@ function loadIntegrationConfig({ configPath = INTEGRATION_CONFIG_PATH } = {}) {
289
306
  * Get the integration gate plan for changed areas
290
307
  * Returns { gates: [{key, command, run_last}], changedAreas: [...] }
291
308
  */
292
- function getIntegrationGatePlan(slug, { runIntegrationGates = true, gitRunner = git, dryRun = false, configPath = INTEGRATION_CONFIG_PATH } = {}) {
309
+ /** @param {string} slug @param {{runIntegrationGates?: boolean, gitRunner?: Function, dryRun?: boolean, configPath?: string}} opts */
310
+ function getIntegrationGatePlan(slug, opts = {}) {
293
311
  // Check config
294
- const configResult = loadIntegrationConfig({ configPath });
312
+ const configResult = loadIntegrationConfig(opts);
295
313
  if (!configResult.ok) {
296
- if (runIntegrationGates) {
314
+ if (opts.runIntegrationGates) {
297
315
  fmt.log.info(`integration-gates: ${configResult.error}, skipping`);
298
316
  }
299
317
  return { gates: [], changedAreas: [], configError: configResult.error };
@@ -301,16 +319,16 @@ function getIntegrationGatePlan(slug, { runIntegrationGates = true, gitRunner =
301
319
 
302
320
  const config = configResult.config;
303
321
  if (!config.gates || Object.keys(config.gates).length === 0) {
304
- if (runIntegrationGates) {
322
+ if (opts.runIntegrationGates) {
305
323
  fmt.log.info('integration-gates: no gates defined in config, skipping');
306
324
  }
307
325
  return { gates: [], changedAreas: [], configError: 'no gates defined' };
308
326
  }
309
327
 
310
328
  // Detect changed areas (always detect for dry-run; for real run, detect only if runIntegrationGates)
311
- const changedAreas = (runIntegrationGates || dryRun) ? detectChangedAreas(slug, { gitRunner }) : [];
329
+ const changedAreas = (opts.runIntegrationGates || opts.dryRun) ? detectChangedAreas(slug, opts) : [];
312
330
 
313
- if (runIntegrationGates && !dryRun && changedAreas.length === 0) {
331
+ if (opts.runIntegrationGates && !opts.dryRun && changedAreas.length === 0) {
314
332
  fmt.log.info('integration-gates: no area changes detected, skipping');
315
333
  return { gates: [], changedAreas: [], configError: null };
316
334
  }
@@ -351,6 +369,7 @@ function getIntegrationGatePlan(slug, { runIntegrationGates = true, gitRunner =
351
369
  /**
352
370
  * Print the integration gate plan (for --dry-run)
353
371
  */
372
+ /** @param{{key: string, command: string, order: number, run_last: boolean}[]} gates */
354
373
  function printIntegrationGatePlan(gates) {
355
374
  fmt.log.info('Integration gate plan:');
356
375
  for (const gate of gates) {
@@ -358,22 +377,21 @@ function printIntegrationGatePlan(gates) {
358
377
  }
359
378
  }
360
379
 
361
- function buildIntegrationGateEnv(slug, {
362
- dryRun = false,
363
- processEnv = process.env,
364
- gitRunner = git,
365
- baseBranch = null,
366
- baseWorktree = null
367
- } = {}) {
368
- const env = {
369
- ...processEnv,
370
- INTEGRATE_DRY_RUN: dryRun ? 'true' : 'false',
380
+ /**
381
+ * @param {string} slug
382
+ * @param {{dryRun?: boolean, processEnv?: NodeJS.ProcessEnv, gitRunner?: Function, baseBranch?: string|null, baseWorktree?: string|null}} opts
383
+ */
384
+ function buildIntegrationGateEnv(slug, opts = {}) {
385
+ /** @type {{[key: string]: string | undefined}} */
386
+ const env = /** @type {{[key: string]: string | undefined}} */ ({
387
+ ...opts.processEnv,
388
+ INTEGRATE_DRY_RUN: opts.dryRun ? 'true' : 'false',
371
389
  INTEGRATE_CHANGED_AREAS: detectChangedAreas(slug, {
372
- gitRunner,
373
- baseBranch,
374
- ...(baseWorktree ? { rootDir: baseWorktree } : {})
390
+ gitRunner: opts.gitRunner,
391
+ baseBranch: opts.baseBranch,
392
+ ...(opts.baseWorktree ? { rootDir: opts.baseWorktree } : {})
375
393
  }).join(' ')
376
- };
394
+ });
377
395
 
378
396
  // Integration must always use the repo-side config, but the changed-area set
379
397
  // should be the one resolved for this mission branch, not rediscovered later.
@@ -381,28 +399,26 @@ function buildIntegrationGateEnv(slug, {
381
399
  return env;
382
400
  }
383
401
 
384
- function resolveIntegrationVerificationWorktree(slug, {
385
- baseWorktree = getPrimaryWorktree(),
386
- resolveWorktreeFn = resolveWorktree,
387
- conventionalWorktreePathFn = conventionalWorktreePath
388
- } = {}) {
389
- return resolveWorktreeFn(slug, { cwd: baseWorktree })
390
- || conventionalWorktreePathFn(slug, baseWorktree);
402
+ /**
403
+ * @param {string} slug
404
+ * @param {{baseWorktree?: string, resolveWorktreeFn?: Function, conventionalWorktreePathFn?: Function}} opts
405
+ */
406
+ function resolveIntegrationVerificationWorktree(slug, opts = {}) {
407
+ const resolveFn = opts.resolveWorktreeFn || resolveWorktree;
408
+ const convFn = opts.conventionalWorktreePathFn || conventionalWorktreePath;
409
+ return resolveFn(slug, { cwd: opts.baseWorktree })
410
+ || convFn(slug, opts.baseWorktree);
391
411
  }
392
412
 
393
- function buildIntegrationVerificationInvocation(slug, {
394
- baseWorktree = getPrimaryWorktree(),
395
- resolveWorktreeFn = resolveWorktree,
396
- conventionalWorktreePathFn = conventionalWorktreePath,
397
- formatVerificationCommandFn = formatVerificationCommand
398
- } = {}) {
399
- const cwd = resolveIntegrationVerificationWorktree(slug, {
400
- baseWorktree,
401
- resolveWorktreeFn,
402
- conventionalWorktreePathFn
403
- });
413
+ /**
414
+ * @param {string} slug
415
+ * @param {{baseWorktree?: string, resolveWorktreeFn?: Function, conventionalWorktreePathFn?: Function, formatVerificationCommandFn?: Function}} opts
416
+ */
417
+ function buildIntegrationVerificationInvocation(slug, opts = {}) {
418
+ const cwd = resolveIntegrationVerificationWorktree(slug, opts);
419
+ const fmtFn = opts.formatVerificationCommandFn || formatVerificationCommand;
404
420
  return {
405
- command: formatVerificationCommandFn('integrate', cwd),
421
+ command: fmtFn('integrate', cwd),
406
422
  cwd
407
423
  };
408
424
  }
@@ -411,10 +427,14 @@ function buildIntegrationVerificationInvocation(slug, {
411
427
  * Execute integration gates, aborting on first failure
412
428
  * Returns { ok: boolean, failedGate: string|null, error: string|null }
413
429
  */
414
- async function executeIntegrationGates(gates, { commandRunner = null, rootDir = getPrimaryWorktree() } = {}) {
415
- const runner = commandRunner || ((cmd, args, options) => require('child_process').spawnSync(cmd, {
430
+ /**
431
+ * @param {{key: string, command: string, order: number, run_last: boolean}[]} gates
432
+ * @param {{commandRunner?: Function, rootDir?: string}} opts
433
+ */
434
+ async function executeIntegrationGates(gates, opts = {}) {
435
+ const runner = opts.commandRunner || ((/** @type{string} */ cmd) => require('child_process').spawnSync(cmd, {
416
436
  shell: true,
417
- cwd: rootDir,
437
+ cwd: opts.rootDir || getPrimaryWorktree(),
418
438
  stdio: 'inherit'
419
439
  }));
420
440
 
@@ -423,7 +443,7 @@ async function executeIntegrationGates(gates, { commandRunner = null, rootDir =
423
443
  fmt.log.info(` Command: ${gate.command}`);
424
444
 
425
445
  // Execute the command using the injected runner
426
- const result = runner(gate.command, [], { cwd: rootDir, stdio: 'inherit' });
446
+ const result = runner(gate.command, [], { cwd: opts.rootDir, stdio: 'inherit' });
427
447
 
428
448
  if (result.status !== 0) {
429
449
  fmt.log.fail(`Integration gate failed: ${gate.key}`);
@@ -438,17 +458,22 @@ async function executeIntegrationGates(gates, { commandRunner = null, rootDir =
438
458
  return { ok: true, failedGate: null, error: null };
439
459
  }
440
460
 
461
+ /** @param {string[]} args */
441
462
  async function integrate(args) {
442
463
  let exitCode = 0;
464
+ /** @type{{created?: boolean, message?: string, rootDir?: string}|null} */
443
465
  let temporaryStash = null;
444
466
  let nextActionMessage = null;
445
- const flags = args.filter(arg => arg.startsWith('--'));
446
- const params = args.filter(arg => !arg.startsWith('--'));
467
+ const flags = args.filter((/** @type{string} */ arg) => arg.startsWith('--'));
468
+ const params = args.filter((/** @type{string} */ arg) => !arg.startsWith('--'));
447
469
  const explicitSlug = params[0];
448
470
  const slug = inferSlug(explicitSlug);
449
471
  const dryRun = flags.includes('--dry-run');
450
472
  const noIntegrationGates = flags.includes('--no-integration-gates');
451
473
 
474
+ /** @type {{slug: string, branch: string, currentBranch: string, missionDir?: string, area: string, task: {ok: boolean, taskFile?: string, reason?: string, matches?: string[]}, taskStatus?: string, taskAssignee?: string|null, forgejoUser?: string|null, forgejoToken?: string|null, taskAssigneeWarning?: string|null, pr: {exists?: boolean, state?: string, number?: number, merged?: boolean, raw?: string}, siblingPrs: any[], approval: {ok?: boolean, error?: string, reviewState?: string, defaultUserApproved?: boolean, source?: string}, baseBranch?: string, baseWorktree?: string, mainBranch: string, mainDirtyEntries: string[], mainDirty: boolean}} */
475
+ let context;
476
+
452
477
  if (process.env.FORGEJO_USER === 'gemini' || process.env.WORKFLOW_AGENT === 'gemini') {
453
478
  fmt.log.fail('Gemini is not authorized to run integrate. Post a handoff comment on the PR and stop.');
454
479
  process.exit(1);
@@ -459,55 +484,70 @@ async function integrate(args) {
459
484
  process.exit(1);
460
485
  }
461
486
 
462
- if (flags.includes('--no-gate')) {
463
- fmt.log.warn('integrate ignores --no-gate. The landed squash commit relies on the local git hooks for validation.');
464
- }
465
-
466
- try {
467
- const executionDir = process.cwd();
468
- const context = buildIntegrationContext(slug);
469
- // The integration target is the mission's recorded base worktree/branch.
470
- // For legacy missions these resolve to the primary worktree/branch, keeping
471
- // the merge/commit/sync path byte-identical to today.
472
- const baseWorktree = context.baseWorktree;
473
- const baseBranch = context.baseBranch;
474
- const { failures } = printIntegrationPreflight(context);
475
-
476
- if (failures.length > 0) {
477
- fmt.log.fail('\nIntegration preflight failed. Resolve the blockers above before running integrate.');
478
- throw new IntegrationAbort();
487
+ if (flags.includes('--no-gate')) {
488
+ fmt.log.warn('integrate ignores --no-gate. The landed squash commit relies on the local git hooks for validation.');
479
489
  }
480
490
 
481
- if (!noIntegrationGates) {
482
- const verification = buildIntegrationVerificationInvocation(slug, { baseWorktree });
483
- const env = buildIntegrationGateEnv(slug, { dryRun, baseBranch, baseWorktree });
484
-
485
- if (dryRun) {
486
- fmt.log.info('Running integration gates (dry-run)...');
487
- } else {
488
- fmt.log.info('Running integration gates...');
489
- }
490
-
491
- const result = child_process.spawnSync('bash', ['-lc', verification.command], {
492
- cwd: verification.cwd,
493
- env,
494
- stdio: 'inherit'
495
- });
496
-
497
- if (result.status !== 0) {
498
- fmt.log.fail(`\nIntegration gates failed with exit code ${result.status}`);
499
- fmt.log.fail('Aborting before merge.');
491
+ try {
492
+ const executionDir = process.cwd();
493
+ // @ts-expect-error buildIntegrationContext return type mismatch
494
+ context = buildIntegrationContext(slug);
495
+ // The integration target is the mission's recorded base worktree/branch.
496
+ // For legacy missions these resolve to the primary worktree/branch, keeping
497
+ // the merge/commit/sync path byte-identical to today.
498
+ const baseWorktree = context.baseWorktree;
499
+ const baseBranch = context.baseBranch;
500
+ const { failures } = printIntegrationPreflight(context);
501
+
502
+ if (failures.length > 0) {
503
+ fmt.log.fail('\nIntegration preflight failed. Resolve the blockers above before running integrate.');
500
504
  throw new IntegrationAbort();
501
505
  }
502
-
503
- if (!dryRun) {
504
- fmt.log.pass('All integration gates passed.');
506
+
507
+ if (!noIntegrationGates) {
508
+ const verification = buildIntegrationVerificationInvocation(slug, { baseWorktree });
509
+ const env = buildIntegrationGateEnv(slug, { dryRun, baseBranch, baseWorktree });
510
+
511
+ if (dryRun) {
512
+ fmt.log.info('Running integration gates (dry-run)...');
513
+ } else {
514
+ fmt.log.info('Running integration gates...');
515
+ }
516
+
517
+ const result = child_process.spawnSync('bash', ['-lc', verification.command], {
518
+ cwd: verification.cwd,
519
+ env,
520
+ stdio: 'inherit'
521
+ });
522
+
523
+ if (result.status !== 0) {
524
+ fmt.log.fail(`\nIntegration gates failed with exit code ${result.status}`);
525
+ fmt.log.fail('Aborting before merge.');
526
+ throw new IntegrationAbort();
527
+ }
528
+
529
+ if (!dryRun) {
530
+ fmt.log.pass('All integration gates passed.');
531
+ }
532
+ } else {
533
+ fmt.log.info('Integration gates skipped via --no-integration-gates flag');
505
534
  }
506
- } else {
507
- fmt.log.info('Integration gates skipped via --no-integration-gates flag');
508
- }
509
535
 
510
- if (dryRun) {
536
+ if (dryRun) {
537
+ promoteTaskForIntegrationIfNeeded(context, { dryRun: true });
538
+ fmt.log.pass('\nDry run complete. Integration preflight passed.');
539
+ return;
540
+ }
541
+
542
+ promoteTaskForIntegrationIfNeeded(context);
543
+
544
+ temporaryStash = stashMainCheckoutIfNeeded({
545
+ slug,
546
+ dirtyEntries: context.mainDirtyEntries,
547
+ rootDir: /** @type{string} */(baseWorktree)
548
+ });
549
+
550
+ if (dryRun) {
511
551
  promoteTaskForIntegrationIfNeeded(context, { dryRun: true });
512
552
  fmt.log.pass('\nDry run complete. Integration preflight passed.');
513
553
  return;
@@ -526,6 +566,7 @@ async function integrate(args) {
526
566
  const missionWorktree = conventionalWorktreePath(slug);
527
567
  if (process.cwd() === missionWorktree || process.cwd().startsWith(missionWorktree + '/')) {
528
568
  fmt.log.info(`Moving process directory to ${baseWorktree} before mission worktree deletion.`);
569
+ // @ts-expect-error baseWorktree may be undefined
529
570
  process.chdir(baseWorktree);
530
571
  }
531
572
 
@@ -533,6 +574,7 @@ async function integrate(args) {
533
574
  const branch = missionBranchName(slug, baseWorktree);
534
575
  const mainTitle = missionTitle(slug) || slug;
535
576
  const summary = mainTitle.replace(/\s+/g, ' ').trim();
577
+ // @ts-expect-error context.task.taskFile may be undefined
536
578
  const mainTaskFile = context.task.taskFile.replace(executionDir, baseWorktree);
537
579
  const useVariantA = context.pr.merged;
538
580
  fmt.log.info(`Selecting integration variant: ${useVariantA ? 'Variant A (fast-path)' : 'Variant B (full squash-merge)'}`);
@@ -569,8 +611,11 @@ async function integrate(args) {
569
611
  fmt.log.info(`\nStep 1: Using base worktree ${baseWorktree} on ${baseBranch} as the squash-merge target...`);
570
612
 
571
613
  fmt.log.info(`Step 2: Checking merge conflicts against local ${baseBranch} in the base worktree...`);
614
+ // @ts-expect-error baseWorktree may be undefined
572
615
  const dryMerge = git(['-C', baseWorktree, 'merge', '--no-commit', '--no-ff', branch]);
616
+ // @ts-expect-error baseWorktree may be undefined
573
617
  const abortResult = git(['-C', baseWorktree, 'merge', '--abort']);
618
+ // @ts-expect-error dryMerge/abortResult type mismatch
574
619
  if (abortResult.status !== 0 && !isNoMergeToAbortResult(abortResult)) {
575
620
  fmt.log.fail('Dry-run merge could not be aborted cleanly. Inspect the local integration checkout before retrying integrate.');
576
621
  throw new IntegrationAbort();
@@ -578,6 +623,7 @@ async function integrate(args) {
578
623
  if (dryMerge.status !== 0) {
579
624
  // Before failing, check if a squash commit for this mission already landed on the primary branch
580
625
  // from a previous partial integration run (e.g. sync-merged failed after the commit was created).
626
+ // @ts-expect-error baseWorktree may be undefined
581
627
  const existingSquash = findExistingSquashCommit(baseWorktree, slug);
582
628
  if (existingSquash) {
583
629
  fmt.log.warn(`Squash commit already exists on local ${baseBranch} from a previous partial integration (${existingSquash.slice(0, 12)}). Resuming from sync-merged step.`);
@@ -591,6 +637,7 @@ async function integrate(args) {
591
637
  baseBranch: context.baseBranch
592
638
  });
593
639
  if (!syncResult.ok) {
640
+ // @ts-expect-error reportSyncMergedFailure expects {statusCode, error}
594
641
  reportSyncMergedFailure(syncResult);
595
642
  throw new IntegrationAbort();
596
643
  }
@@ -608,7 +655,7 @@ async function integrate(args) {
608
655
  fmt.log.pass('Integration completed successfully (resumed from partial state).');
609
656
  } else {
610
657
  fmt.log.fail('Merge conflicts detected. Rebase the mission branch before integrating.');
611
- const conflictOutput = [dryMerge.stdout, dryMerge.stderr].filter(Boolean).join('\n');
658
+ const conflictOutput = [/** @type {any} */ (dryMerge).stdout, /** @type {any} */ (dryMerge).stderr].filter(Boolean).join('\n');
612
659
  const conflictFiles = parseConflictFilesFromMergeOutput(conflictOutput);
613
660
  if (conflictFiles.length > 0) {
614
661
  fmt.log.info(`Conflicting files (${conflictFiles.length}):`);
@@ -618,7 +665,7 @@ async function integrate(args) {
618
665
  for (const line of formatMatrixSummary(buildAutonomousReviewMatrix())) {
619
666
  fmt.log.info(line);
620
667
  }
621
- for (const line of buildConflictResolutionPrompt(slug, context.area, { baseBranch: context.baseBranch })) {
668
+ for (const line of buildConflictResolutionPrompt(slug, context.area, { baseBranch: context.baseBranch || '' })) {
622
669
  fmt.log.info(line);
623
670
  }
624
671
  throw new IntegrationAbort();
@@ -626,7 +673,9 @@ async function integrate(args) {
626
673
  } else {
627
674
  fmt.log.info('Step 3: Squash-merging the mission branch...');
628
675
  let noisePatchState = null;
676
+ // @ts-expect-error baseWorktree may be undefined
629
677
  if (softResetTrailingBacklogNoise(baseWorktree, git)) {
678
+ // @ts-expect-error baseWorktree may be undefined
630
679
  noisePatchState = prepareNoisePatchForSquash(baseWorktree, { gitRunner: git });
631
680
  if (!noisePatchState.ok) {
632
681
  fmt.log.fail('Could not preserve trailing backlog noise before squash merge.');
@@ -636,6 +685,7 @@ async function integrate(args) {
636
685
  throw new IntegrationAbort();
637
686
  }
638
687
  }
688
+ // @ts-expect-error baseWorktree may be undefined
639
689
  const squashResult = git(['-C', baseWorktree, 'merge', '--squash', branch]);
640
690
  if (squashResult.status !== 0) {
641
691
  noisePatchState?.cleanup?.();
@@ -643,8 +693,8 @@ async function integrate(args) {
643
693
  throw new IntegrationAbort();
644
694
  }
645
695
  if (noisePatchState?.patchPath) {
646
- const restoreNoiseResult = restoreNoisePatchAfterSquash(baseWorktree, noisePatchState.patchPath, { gitRunner: git });
647
- noisePatchState.cleanup();
696
+ const restoreNoiseResult = restoreNoisePatchAfterSquash(/** @type {string} */ (baseWorktree), noisePatchState.patchPath, { gitRunner: git });
697
+ /** @type {Function} */ (noisePatchState.cleanup)();
648
698
  if (!restoreNoiseResult.ok) {
649
699
  fmt.log.fail('Could not restore trailing backlog noise after squash merge.');
650
700
  if (restoreNoiseResult.error) {
@@ -660,15 +710,17 @@ async function integrate(args) {
660
710
  // Re-resolve because it moved
661
711
  const updatedResolution = resolveTaskFile(slug, baseWorktree);
662
712
  if (updatedResolution.ok) {
713
+ // @ts-expect-error baseWorktree may be undefined
663
714
  rewriteWorktreePaths(updatedResolution.taskFile, slug, { rootDir: baseWorktree });
664
715
  }
665
716
  }
666
717
 
718
+ // @ts-expect-error baseWorktree may be undefined
667
719
  git(['-C', baseWorktree, 'add', '-A']);
668
720
  fmt.log.info('Step 5: Creating the landed squash commit in the local integration checkout...');
669
721
  const commitResult = git([
670
722
  '-C',
671
- baseWorktree,
723
+ /** @type {string} */ (baseWorktree),
672
724
  'commit',
673
725
  '-m',
674
726
  `${branch}: ${summary}`
@@ -685,17 +737,18 @@ async function integrate(args) {
685
737
  }
686
738
  const proofResult = verification.captureVerifiedTreeProof(context.area, baseWorktree, {
687
739
  gitRunner: git,
688
- runFn: child_process.spawnSync
740
+ runFn: /** @type {Function} */ (child_process.spawnSync)
689
741
  });
690
- if (!proofResult.ok) {
691
- fmt.log.fail(`Could not verify the exact tree being published: ${proofResult.error}`);
742
+ if (!/** @type {any} */ (proofResult).ok) {
743
+ fmt.log.fail(`Could not verify the exact tree being published: ${/** @type {any} */ (proofResult).error}`);
692
744
  throw new IntegrationAbort();
693
745
  }
694
- const proof = proofResult.proof;
746
+ const proof = /** @type {any} */ (proofResult).proof;
747
+ // @ts-expect-error baseWorktree may be undefined
695
748
  const mergedCommit = git(['-C', baseWorktree, 'rev-parse', 'HEAD']).stdout.trim();
696
749
  const proofCheck = verification.assertVerifiedTreeProof(proof, baseWorktree, { gitRunner: git });
697
750
  if (!proofCheck.ok) {
698
- fmt.log.fail(`Verification proof is stale for the publish tree: ${proofCheck.error}`);
751
+ fmt.log.fail(`Verification proof is stale for the publish tree: ${/** @type {any} */ (proofCheck).error}`);
699
752
  throw new IntegrationAbort();
700
753
  }
701
754
 
@@ -708,6 +761,7 @@ async function integrate(args) {
708
761
  baseBranch: context.baseBranch
709
762
  });
710
763
  if (!syncResult.ok) {
764
+ // @ts-expect-error reportSyncMergedFailure expects {statusCode, error}
711
765
  reportSyncMergedFailure(syncResult);
712
766
  throw new IntegrationAbort();
713
767
  }
@@ -731,13 +785,13 @@ async function integrate(args) {
731
785
  if (error instanceof IntegrationAbort) {
732
786
  exitCode = 1;
733
787
  } else {
734
- throw error;
788
+ throw /** @type {any} */ (error);
735
789
  }
736
790
  } finally {
737
791
  if (temporaryStash?.created) {
738
- const restoreResult = restoreMainCheckoutStash(temporaryStash);
792
+ const restoreResult = restoreMainCheckoutStash(/** @type {any} */ (temporaryStash));
739
793
  if (restoreResult.status !== 0) {
740
- reportStashPopFailure(slug, restoreResult, { rootDir: temporaryStash.rootDir });
794
+ reportStashPopFailure(slug, restoreResult, { rootDir: /** @type {any} */ (temporaryStash).rootDir });
741
795
  exitCode = 1;
742
796
  }
743
797
  }
@@ -749,6 +803,7 @@ async function integrate(args) {
749
803
  }
750
804
  }
751
805
 
806
+ /** @param {string} slug @param{{baseBranch?: string|null, baseWorktree?: string|null, isForgejoReviewEnabledFn?: Function}} opts */
752
807
  function buildIntegrationContext(slug, {
753
808
  baseBranch = null,
754
809
  baseWorktree = null,
@@ -773,15 +828,15 @@ function buildIntegrationContext(slug, {
773
828
  let task = resolveTaskFile(slug);
774
829
  if (!task.ok) {
775
830
  const worktree = resolveWorktree(slug);
776
- if (worktree) task = resolveTaskFile(slug, worktree);
831
+ if (worktree) {task = resolveTaskFile(slug, worktree);}
777
832
  }
778
- let taskStatus = task.ok ? getTaskStatus(task.taskFile) : null;
833
+ let taskStatus = task.ok ? getTaskStatus(/** @type {string} */ (task.taskFile)) : null;
779
834
  if (task.ok && taskStatus === 'backlog') {
780
835
  const worktree = resolveWorktree(slug);
781
836
  if (worktree) {
782
837
  const wtTask = resolveTaskFile(slug, worktree);
783
838
  if (wtTask.ok) {
784
- const wtStatus = getTaskStatus(wtTask.taskFile);
839
+ const wtStatus = getTaskStatus(/** @type {string} */ (wtTask.taskFile));
785
840
  if (wtStatus && wtStatus !== 'backlog') {
786
841
  task = wtTask;
787
842
  taskStatus = wtStatus;
@@ -789,22 +844,24 @@ function buildIntegrationContext(slug, {
789
844
  }
790
845
  }
791
846
  }
792
- const taskAssignee = task.ok ? getTaskAssignee(task.taskFile) : null;
793
- const forgejoEnabled = isForgejoReviewEnabledFn(resolvedBaseWorktree);
847
+ const taskAssignee = task.ok ? getTaskAssignee(/** @type {string} */ (task.taskFile)) : null;
848
+ const forgejoEnabled = isForgejoReviewEnabledFn(/** @type {string} */ (resolvedBaseWorktree));
794
849
 
795
850
  let forgejoIdentity = { forgejoUser: null, warning: null };
796
851
  let forgejoToken = null;
797
852
  let pr = { exists: false };
798
- let siblingPrs = [];
853
+ /** @type{any[]} */ let siblingPrs = [];
799
854
  let approval = { ok: false, error: 'forgejo-off', reviewState: null };
800
855
 
801
856
  if (forgejoEnabled) {
857
+ // @ts-expect-error forgejoIdentity.forgejoUser type mismatch
802
858
  forgejoIdentity = resolveForgejoUserForIntegration(taskAssignee);
803
- forgejoToken = readToken(forgejoIdentity.forgejoUser);
804
- pr = getPrStatus(branch, process.cwd(), {
805
- forgejoUser: forgejoIdentity.forgejoUser,
859
+ forgejoToken = readToken(/** @type {any} */ (forgejoIdentity.forgejoUser));
860
+ pr = /** @type {any} */ (getPrStatus(branch, process.cwd(), {
861
+ forgejoUser: /** @type {any} */ (forgejoIdentity.forgejoUser),
862
+ // @ts-expect-error forgejoToken may be null
806
863
  token: forgejoToken
807
- });
864
+ }));
808
865
 
809
866
  if (pr.exists && slug) {
810
867
  const baseSlugMatch = slug.match(/^(task-\d+)/i);
@@ -815,10 +872,10 @@ function buildIntegrationContext(slug, {
815
872
  }
816
873
  }
817
874
 
818
- approval = pr.exists ? getLatestReviewDecision(branch, {
819
- forgejoUser: forgejoIdentity.forgejoUser,
820
- token: forgejoToken
821
- }) : { ok: false, error: 'pr-missing', reviewState: null };
875
+ approval = pr.exists ? /** @type {any} */ (getLatestReviewDecision(branch, {
876
+ forgejoUser: /** @type {any} */ (forgejoIdentity.forgejoUser),
877
+ token: /** @type {any} */ (forgejoToken)
878
+ })) : /** @type {any} */ ({ ok: false, error: 'pr-missing', reviewState: undefined });
822
879
  }
823
880
 
824
881
  // Local review-state fallback: when forgejo token/API is unavailable but the
@@ -826,15 +883,15 @@ function buildIntegrationContext(slug, {
826
883
  // so that integrate can proceed without a live Forgejo connection.
827
884
  // Only applies when Forgejo was enabled but approval could not be obtained.
828
885
  if (forgejoEnabled && !approval.ok) {
829
- const localStateFallback = readReviewState(slug, resolvedBaseWorktree);
886
+ const localStateFallback = readReviewState(slug, /** @type {string} */ (resolvedBaseWorktree));
830
887
  if (localStateFallback && localStateFallback.phase === 'approved' && localStateFallback.disposition === 'APPROVED') {
831
- approval = { ok: true, reviewState: 'APPROVED', source: 'local-review-state' };
888
+ approval = /** @type {any} */ ({ ok: true, reviewState: 'APPROVED', source: 'local-review-state' });
832
889
  }
833
890
  }
834
891
 
835
- const mainBranchResult = git(['-C', resolvedBaseWorktree, 'branch', '--show-current']);
892
+ const mainBranchResult = git(['-C', /** @type {string} */ (resolvedBaseWorktree), 'branch', '--show-current']);
836
893
  const mainBranch = mainBranchResult.stdout.trim();
837
- const mainStatus = git(['-C', resolvedBaseWorktree, 'status', '--short']);
894
+ const mainStatus = git(['-C', /** @type {string} */ (resolvedBaseWorktree), 'status', '--short']);
838
895
 
839
896
  return {
840
897
  slug,
@@ -861,9 +918,11 @@ function buildIntegrationContext(slug, {
861
918
  };
862
919
  }
863
920
 
921
+ /** @param {{slug: string, branch: string, currentBranch: string, missionDir?: string, area: string, task: {ok: boolean, taskFile?: string, reason?: string, matches?: string[]}, taskStatus?: string, taskAssignee?: string|null, forgejoUser?: string|null, forgejoToken?: string|null, taskAssigneeWarning?: string|null, pr: {exists?: boolean, state?: string, number?: number, merged?: boolean, raw?: string}, siblingPrs: any[], approval: {ok?: boolean, error?: string, reviewState?: string, defaultUserApproved?: boolean, source?: string}, baseBranch?: string, baseWorktree?: string, mainBranch: string, mainDirtyEntries: string[], mainDirty: boolean}} context */
864
922
  function evaluateTaskStatusForIntegration(context) {
865
- const stateMapOptions = { rootDir: context.baseWorktree };
866
- if (toVirtual(context.taskStatus, stateMapOptions) === 'approved') {
923
+ const stateMapOptions = { rootDir: /** @type {string} */ (context.baseWorktree) };
924
+ // @ts-expect-error context.taskStatus may be undefined
925
+ if (toVirtual(context.taskStatus, /** @type {any} */ (stateMapOptions)) === 'approved') {
867
926
  return {
868
927
  ok: true,
869
928
  level: 'pass',
@@ -904,10 +963,12 @@ function evaluateTaskStatusForIntegration(context) {
904
963
  return {
905
964
  ok: false,
906
965
  level: 'fail',
966
+ // @ts-expect-error stateMapOptions type mismatch
907
967
  message: `Backlog status: expected approved, or review with an approved/merged Forgejo PR; found ${toVirtual(context.taskStatus, stateMapOptions)}`
908
968
  };
909
969
  }
910
970
 
971
+ /** @param {{slug: string, branch: string, currentBranch: string, missionDir?: string, area: string, task: {ok: boolean, taskFile?: string, reason?: string, matches?: string[]}, taskStatus?: string, taskAssignee?: string|null, forgejoUser?: string|null, forgejoToken?: string|null, taskAssigneeWarning?: string|null, pr: {exists?: boolean, state?: string, number?: number, merged?: boolean, raw?: string}, siblingPrs: any[], approval: {ok?: boolean, error?: string, reviewState?: string, defaultUserApproved?: boolean, source?: string}, baseBranch?: string, baseWorktree?: string, mainBranch: string, mainDirtyEntries: string[], mainDirty: boolean}} context */
911
972
  function promoteTaskForIntegrationIfNeeded(context, { dryRun = false } = {}) {
912
973
  const taskStatusCheck = evaluateTaskStatusForIntegration(context);
913
974
  const needsPromotion = context.task?.ok && context.taskStatus === 'review' && taskStatusCheck.ok;
@@ -921,8 +982,8 @@ function promoteTaskForIntegrationIfNeeded(context, { dryRun = false } = {}) {
921
982
  return { changed: false, dryRun: true };
922
983
  }
923
984
 
924
- const stateMapOptions = { rootDir: context.baseWorktree };
925
- if (!setTaskStatus(context.task.taskFile, toActual('approved', stateMapOptions))) {
985
+ const stateMapOptions = { rootDir: /** @type {string} */ (context.baseWorktree) };
986
+ if (!setTaskStatus(/** @type {string} */ (context.task.taskFile), toActual('approved', stateMapOptions))) {
926
987
  fmt.log.fail('Could not promote the Backlog task to approved before integration.');
927
988
  throw new IntegrationAbort();
928
989
  }
@@ -932,6 +993,9 @@ function promoteTaskForIntegrationIfNeeded(context, { dryRun = false } = {}) {
932
993
  return { changed: true, dryRun: false };
933
994
  }
934
995
 
996
+ /**
997
+ * @param{{slug: string, branch: string, missionDir?: string, area: string, task: {ok: boolean, taskFile?: string, reason?: string, matches?: string[]}, taskStatus?: string, taskAssignee?: string|null, forgejoUser?: string|null, forgejoToken?: string|null, taskAssigneeWarning?: string|null, pr: {exists?: boolean, state?: string, number?: number, merged?: boolean, raw?: string}, siblingPrs: any[], approval: {ok?: boolean, error?: string, reviewState?: string, defaultUserApproved?: boolean, source?: string}, baseBranch?: string, baseWorktree?: string, mainBranch: string, mainDirtyEntries: string[], mainDirty: boolean}} context
998
+ */
935
999
  function printIntegrationPreflight(
936
1000
  context,
937
1001
  {
@@ -956,11 +1020,14 @@ function printIntegrationPreflight(
956
1020
  log(fmt.status('INFO', `Integration preflight for ${context.slug}`));
957
1021
 
958
1022
  const branchPrefix = missionBranchName(context.slug, baseWorktree);
959
- if (context.currentBranch === context.branch || context.currentBranch.startsWith(`${branchPrefix}-`)) {
960
- log(fmt.status('PASS', `Mission branch: ${context.currentBranch}`));
1023
+ // @ts-expect-error context.currentBranch doesn't exist on type
1024
+ if ((/** @type {any} */ context).currentBranch === context.branch || (/** @type {any} */ context).currentBranch.startsWith(`${branchPrefix}-`)) {
1025
+ // @ts-expect-error context.currentBranch doesn't exist on type
1026
+ log(fmt.status('PASS', `Mission branch: ${(/** @type {any} */ context).currentBranch}`));
961
1027
  } else {
962
1028
  failures.push('branch');
963
- log(fmt.status('FAIL', `Mission branch: current branch is ${context.currentBranch}, expected ${context.branch} (or a branch with a suffix)`));
1029
+ // @ts-expect-error context.currentBranch doesn't exist on type
1030
+ log(fmt.status('FAIL', `Mission branch: current branch is ${(/** @type {any} */ context).currentBranch}, expected ${context.branch} (or a branch with a suffix)`));
964
1031
  }
965
1032
 
966
1033
  if (context.missionDir) {
@@ -987,7 +1054,7 @@ function printIntegrationPreflight(
987
1054
  }
988
1055
 
989
1056
  if (context.task.ok) {
990
- log(fmt.status('PASS', `Backlog task: ${path.basename(context.task.taskFile)} (${context.taskStatus})`));
1057
+ log(fmt.status('PASS', `Backlog task: ${path.basename(/** @type {string} */ (context.task.taskFile))} (${context.taskStatus})`));
991
1058
 
992
1059
  try {
993
1060
  const { classification, error: classificationError } = stats.resolveMissionClassification(context.slug);
@@ -997,11 +1064,12 @@ function printIntegrationPreflight(
997
1064
  } else {
998
1065
  log(fmt.status('PASS', `Backlog classification: ${classification}`));
999
1066
  }
1000
- } catch (error) {
1067
+ } catch (/** @type{any} */ error) {
1001
1068
  failures.push('classification');
1002
1069
  log(fmt.status('FAIL', `Backlog classification: ${error.message}`));
1003
1070
  }
1004
1071
 
1072
+ // @ts-expect-error context type mismatch (missing currentBranch)
1005
1073
  const taskStatusCheck = evaluateTaskStatusForIntegration(context);
1006
1074
  if (!taskStatusCheck.ok) {
1007
1075
  failures.push('task-status');
@@ -1015,7 +1083,8 @@ function printIntegrationPreflight(
1015
1083
  } else if (context.task.reason === 'ambiguous') {
1016
1084
  failures.push('task-ambiguity');
1017
1085
  log(fmt.status('FAIL', `Backlog task: ambiguous slug ${context.slug}`));
1018
- context.task.matches.forEach(match => log(` - ${match}`));
1086
+ // @ts-expect-error context.task.matches may be undefined
1087
+ context.task.matches.forEach((/** @type{string} */ match) => log(` - ${match}`));
1019
1088
  } else {
1020
1089
  log(fmt.status('WARN', `Backlog task: no task file found for ${context.slug}; continuing with synthetic/unknown task metadata.`));
1021
1090
  log(fmt.status('PASS', 'Backlog classification: unknown'));
@@ -1057,13 +1126,13 @@ function printIntegrationPreflight(
1057
1126
  const baseSlugMatch = context.slug.match(/^(task-\d+)/i);
1058
1127
  const baseSlug = baseSlugMatch ? baseSlugMatch[1].toLowerCase() : context.slug;
1059
1128
  log(fmt.status('WARN', `Multiple open PRs detected for ${baseSlug}. Close stale PRs before integrating:`));
1060
- context.siblingPrs.forEach(p => {
1129
+ context.siblingPrs.forEach((/** @type{{number: number, head: string, html_url: string}} */ p) => {
1061
1130
  log(fmt.status('INFO', ` - PR #${p.number} (${p.head}): ${p.html_url}`));
1062
1131
  });
1063
1132
  }
1064
1133
 
1065
- const tokenPath = resolveTokenFileFn(context.forgejoUser);
1066
- const token = readTokenFn(context.forgejoUser);
1134
+ const tokenPath = resolveTokenFileFn(/** @type {string} */ (context.forgejoUser));
1135
+ const token = readTokenFn(/** @type {string} */ (context.forgejoUser));
1067
1136
  if (token) {
1068
1137
  log(fmt.status('PASS', `Forgejo token: resolved for ${context.forgejoUser} (${tokenPath || 'env:FORGEJO_TOKEN'})`));
1069
1138
  } else if (localApprovalFallback) {
@@ -1130,7 +1199,7 @@ function printIntegrationPreflight(
1130
1199
  if (context.mainDirty) {
1131
1200
  warnings.push('main-dirty');
1132
1201
  log(fmt.status('WARN', `Integration checkout dirty: ${baseWorktree} has uncommitted changes that will be stashed temporarily`));
1133
- context.mainDirtyEntries.forEach(entry => log(fmt.status('INFO', ` - ${entry}`)));
1202
+ context.mainDirtyEntries.forEach((/** @type{string} */ entry) => log(fmt.status('INFO', ` - ${entry}`)));
1134
1203
  } else {
1135
1204
  log(fmt.status('PASS', 'Integration checkout dirty state: clean'));
1136
1205
  }
@@ -1156,6 +1225,7 @@ function printIntegrationPreflight(
1156
1225
  return { failures, warnings };
1157
1226
  }
1158
1227
 
1228
+ /** @param {string} taskFilePath @param {string} slug @param{{rootDir?: string}} options */
1159
1229
  function rewriteWorktreePaths(taskFilePath, slug, options = {}) {
1160
1230
  const rootDir = options.rootDir || getPrimaryWorktree();
1161
1231
  const before = fs.readFileSync(taskFilePath, 'utf8');
@@ -1169,6 +1239,7 @@ function rewriteWorktreePaths(taskFilePath, slug, options = {}) {
1169
1239
  }
1170
1240
  }
1171
1241
 
1242
+ /** @param{{slug: string, dirtyEntries?: string[], rootDir?: string, gitRunner?: Function}} params */
1172
1243
  function stashMainCheckoutIfNeeded({
1173
1244
  slug,
1174
1245
  dirtyEntries = [],
@@ -1203,6 +1274,7 @@ function stashMainCheckoutIfNeeded({
1203
1274
  };
1204
1275
  }
1205
1276
 
1277
+ /** @param{{message: string, rootDir?: string, gitRunner?: Function}} params */
1206
1278
  function restoreMainCheckoutStash({ message, rootDir = getPrimaryWorktree(), gitRunner = git }) {
1207
1279
  fmt.log.info(`Restoring temporarily stashed local integration checkout changes: ${message}`);
1208
1280
  // Pass cwd explicitly so spawnSync does not inherit the process cwd, which may have been
@@ -1210,11 +1282,16 @@ function restoreMainCheckoutStash({ message, rootDir = getPrimaryWorktree(), git
1210
1282
  return gitRunner(['-C', rootDir, 'stash', 'pop'], { cwd: rootDir });
1211
1283
  }
1212
1284
 
1285
+ /** @param{{stdout: string, stderr: string, status: number}} result */
1213
1286
  function isNoMergeToAbortResult(result) {
1214
1287
  const output = [result.stdout, result.stderr].filter(Boolean).join('\n');
1215
1288
  return /MERGE_HEAD missing|There is no merge to abort/i.test(output);
1216
1289
  }
1217
1290
 
1291
+ /**
1292
+ * @param {string} slug
1293
+ * @param{{rootDir?: string, gitRunner?: Function, recordIntegrationStatsFn?: Function}} options
1294
+ */
1218
1295
  function recordPostIntegrationStats(
1219
1296
  slug,
1220
1297
  {
@@ -1240,19 +1317,28 @@ function recordPostIntegrationStats(
1240
1317
  fmt.log.info(`Workflow stats recorded: ${formatRecordedStatsRow(outcome.row)}`);
1241
1318
  fmt.log.info('Workflow stats updated:');
1242
1319
  fmt.log.plain(outcome.report);
1320
+
1321
+ const missionRows = outcome.data?.rows || [];
1322
+ const missionReport = stats.renderMissionPhaseReport(missionRows, slug);
1323
+ const firstLine = missionReport.split('\n')[0];
1324
+ fmt.log.info(firstLine);
1325
+ fmt.log.plain(missionReport.split('\n').slice(1).join('\n'));
1326
+
1243
1327
  return outcome;
1244
1328
  }
1245
1329
 
1330
+ /** @param {string} slug @param{{rootDir?: string}} options */
1246
1331
  function recordPostIntegrationStatsOrAbort(slug, options = {}) {
1247
1332
  try {
1248
1333
  return recordPostIntegrationStats(slug, options);
1249
- } catch (error) {
1334
+ } catch (/** @type{any} */ error) {
1250
1335
  const detail = error && error.message ? error.message : String(error);
1251
1336
  fmt.log.fail(`Post-integration workflow stats failed for ${slug}: ${detail}`);
1252
1337
  throw new IntegrationAbort();
1253
1338
  }
1254
1339
  }
1255
1340
 
1341
+ /** @param{{slug: string, summary: string, mainTaskFile?: string, rootDir?: string, gitRunner?: Function, baseBranch?: string|null, verificationArea?: string|null, captureVerifiedTreeProofFn?: Function, assertVerifiedTreeProofFn?: Function}} params */
1256
1342
  function finalizeVariantACloseout({
1257
1343
  slug,
1258
1344
  summary,
@@ -1271,7 +1357,7 @@ function finalizeVariantACloseout({
1271
1357
  // Re-resolve because it moved
1272
1358
  const updatedResolution = resolveTaskFile(slug, rootDir);
1273
1359
  if (updatedResolution.ok) {
1274
- rewriteWorktreePaths(updatedResolution.taskFile, slug, { rootDir });
1360
+ rewriteWorktreePaths(/** @type {string} */ (updatedResolution.taskFile), slug, { rootDir });
1275
1361
  }
1276
1362
  }
1277
1363
 
@@ -1332,12 +1418,16 @@ function finalizeVariantACloseout({
1332
1418
  return { ok: true, changed: true };
1333
1419
  }
1334
1420
 
1421
+ /**
1422
+ * @param {string} slug
1423
+ * @param{{rootDir?: string, gitRunner?: Function, removeDir?: Function, existsSync?: Function}} options
1424
+ */
1335
1425
  function cleanupMissionWorktree(
1336
1426
  slug,
1337
1427
  {
1338
1428
  rootDir = getPrimaryWorktree(),
1339
1429
  gitRunner = git,
1340
- removeDir = target => {
1430
+ removeDir = (/** @type{string} */ target) => {
1341
1431
  if (isForgejoPath(target, { forgejoHome: resolveForgejoHome() })) {
1342
1432
  throw new Error(`CRITICAL SAFETY VIOLATION: cleanupMissionWorktree attempted to delete Forgejo home: ${target}`);
1343
1433
  }
@@ -1359,6 +1449,7 @@ function cleanupMissionWorktree(
1359
1449
  }
1360
1450
 
1361
1451
  const worktreeList = gitRunner(['-C', rootDir, 'worktree', 'list', '--porcelain']).stdout;
1452
+ // @ts-expect-error line implicitly any
1362
1453
  const isRegistered = worktreeList.split('\n').some(line => line.trim() === `worktree ${worktreePath}`);
1363
1454
 
1364
1455
  if (existsSync(worktreePath) && isRegistered) {
@@ -1389,16 +1480,17 @@ function cleanupMissionWorktree(
1389
1480
  return !existsSync(worktreePath);
1390
1481
  }
1391
1482
 
1483
+ /** @param {string} rootDir @param {string} slug */
1392
1484
  function findExistingSquashCommit(rootDir, slug) {
1393
1485
  const result = git(['-C', rootDir, 'log', '--format=%H %s', '-50']);
1394
- if (result.status !== 0) return null;
1486
+ if (result.status !== 0) {return null;}
1395
1487
  const prefix = `${missionBranchName(slug, rootDir)}:`;
1396
1488
  for (const line of result.stdout.trim().split('\n')) {
1397
1489
  const spaceIdx = line.indexOf(' ');
1398
- if (spaceIdx === -1) continue;
1490
+ if (spaceIdx === -1) {continue;}
1399
1491
  const hash = line.slice(0, spaceIdx);
1400
1492
  const subject = line.slice(spaceIdx + 1);
1401
- if (subject.startsWith(prefix)) return hash;
1493
+ if (subject.startsWith(prefix)) {return hash;}
1402
1494
  }
1403
1495
  return null;
1404
1496
  }
@@ -1414,8 +1506,14 @@ function findExistingSquashCommit(rootDir, slug) {
1414
1506
  * @param {string} slug - Backlog task key (e.g. "task-108")
1415
1507
  * @param {string} area - Mission gate area passed to the configured verification command
1416
1508
  * @param {{ getConflictFilesFn?: Function }} [options] - Overrides for testing
1417
- * @returns {{ ok: boolean, conflictFiles: string[], sharedFiles: string[], missionSpecificFiles: string[] }}
1509
+ * @returns {{ ok: boolean, conflictFiles: string[], sharedFiles: string[], missionSpecificFiles: string[], error?: string, worktreePath?: string }}
1510
+ */
1511
+ /**
1512
+ * @param {string} slug
1513
+ * @param {string} area
1514
+ * @param{{getConflictFilesFn: Function, resolveWorktreeFn?: Function, worktreePathOverride?: string|null, rootDir?: string, baseBranch?: string|null}} options
1418
1515
  */
1516
+ // @ts-expect-error resolveConflictsForMission options missing getConflictFilesFn
1419
1517
  function resolveConflictsForMission(slug, area, { getConflictFilesFn, resolveWorktreeFn = resolveWorktree, worktreePathOverride = null, rootDir = getPrimaryWorktree(), baseBranch = null } = {}) {
1420
1518
  // Resolve the actual attached worktree dynamically; fall back to the conventional
1421
1519
  // path only when git worktree list has no entry for this mission branch.
@@ -1434,7 +1532,8 @@ function resolveConflictsForMission(slug, area, { getConflictFilesFn, resolveWor
1434
1532
  try { targetBranch = resolveMissionBaseBranch(slug, rootDir); } catch (_) { targetBranch = getPrimaryBranch(); }
1435
1533
  }
1436
1534
 
1437
- const detectFn = getConflictFilesFn || ((_rootDir, _branch) => getConflictFiles(worktreePath, targetBranch));
1535
+ // @ts-expect-error getConflictFilesFn may be undefined
1536
+ const detectFn = getConflictFilesFn || (() => getConflictFiles(worktreePath, targetBranch));
1438
1537
 
1439
1538
  if (!fs.existsSync(worktreePath)) {
1440
1539
  fmt.log.fail(`Mission worktree not found: ${worktreePath}`);
@@ -1446,7 +1545,7 @@ function resolveConflictsForMission(slug, area, { getConflictFilesFn, resolveWor
1446
1545
  let conflictFiles;
1447
1546
  try {
1448
1547
  conflictFiles = detectFn(worktreePath, targetBranch);
1449
- } catch (err) {
1548
+ } catch (/** @type{any} */ err) {
1450
1549
  fmt.log.fail('Merge check failed with a non-conflict error:');
1451
1550
  fmt.log.fail(err.message);
1452
1551
  return { ok: false, error: 'merge-failed', conflictFiles: [], sharedFiles: [], missionSpecificFiles: [], worktreePath };
@@ -1459,20 +1558,20 @@ function resolveConflictsForMission(slug, area, { getConflictFilesFn, resolveWor
1459
1558
  }
1460
1559
 
1461
1560
  fmt.log.info(`Conflicting files (${conflictFiles.length}):`);
1462
- conflictFiles.forEach(f => fmt.log.info(` - ${f}`));
1561
+ conflictFiles.forEach((/** @type{string} */ f) => fmt.log.info(` - ${f}`));
1463
1562
 
1464
1563
  // Derive the mission-doc directory prefix from the actual located mission dir
1465
1564
  // so the classification works for non-standard doc paths (e.g. renamed year dir).
1466
1565
  const taskPattern = new RegExp(`backlog/(?:tasks|completed)/[^/]*${slug}`);
1467
- const missionSpecificFiles = conflictFiles.filter(f =>
1566
+ const missionSpecificFiles = conflictFiles.filter((/** @type{string} */ f) =>
1468
1567
  isMissionArtifact(f, slug, worktreePath) || taskPattern.test(f)
1469
1568
  );
1470
- const sharedFiles = conflictFiles.filter(f => !missionSpecificFiles.includes(f));
1569
+ const sharedFiles = conflictFiles.filter((/** @type{string} */ f) => !missionSpecificFiles.includes(f));
1471
1570
  const quotedWorktreePath = shellQuote(worktreePath);
1472
1571
 
1473
1572
  if (sharedFiles.length > 0) {
1474
1573
  fmt.log.warn(`Conflicts in ${sharedFiles.length} shared file(s) require manual resolution:`);
1475
- sharedFiles.forEach(f => fmt.log.plain(` - ${f}`));
1574
+ sharedFiles.forEach((/** @type{string} */ f) => fmt.log.plain(` - ${f}`));
1476
1575
  fmt.log.info('Manual resolution path:');
1477
1576
  fmt.log.info(` cd ${quotedWorktreePath}`);
1478
1577
  fmt.log.info(` git rebase ${targetBranch}`);
@@ -1489,7 +1588,7 @@ function resolveConflictsForMission(slug, area, { getConflictFilesFn, resolveWor
1489
1588
  fmt.log.info(` cd ${quotedWorktreePath}`);
1490
1589
  fmt.log.info(` git rebase ${targetBranch}`);
1491
1590
  fmt.log.info(' # after the rebase pauses on conflicts:');
1492
- missionSpecificFiles.forEach(f => fmt.log.info(` git checkout --theirs "${f}" && git add "${f}"`));
1591
+ missionSpecificFiles.forEach((/** @type{string} */ f) => fmt.log.info(` git checkout --theirs "${f}" && git add "${f}"`));
1493
1592
  fmt.log.info(' git rebase --continue');
1494
1593
  fmt.log.info(` ${formatVerificationCommand(area, worktreePath)}`);
1495
1594
  fmt.log.info(` px integrate ${slug} --dry-run`);
@@ -1497,6 +1596,7 @@ function resolveConflictsForMission(slug, area, { getConflictFilesFn, resolveWor
1497
1596
  return { ok: true, conflictFiles, sharedFiles: [], missionSpecificFiles, worktreePath };
1498
1597
  }
1499
1598
 
1599
+ /** @param{string} slug @param{string} area @param{{rootDir?: string, worktreePath?: string, baseBranch?: string|null}} options */
1500
1600
  function buildConflictResolutionPrompt(slug = '<slug>', area = '<area>', options = {}) {
1501
1601
  const rootDir = options.rootDir || getPrimaryWorktree();
1502
1602
  const worktreePath = options.worktreePath || resolveWorktree(slug) || conventionalWorktreePath(slug, rootDir);