@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
@@ -11,36 +11,37 @@ const { getPrStatus } = require('../tools/forgejo');
11
11
  const { isForgejoReviewEnabled } = require('../core/product-config');
12
12
  const stats = require('./stats');
13
13
 
14
- function missionStart(args, options = {}) {
15
- const log = options.log || fmt.log.plain;
16
- const error = options.error || fmt.log.plainError;
17
- const cwdFn = options.cwdFn || (() => process.cwd());
18
- const getCurrentBranchFn = options.getCurrentBranchFn || getCurrentBranch;
19
- const resolveTaskFileFn = options.resolveTaskFileFn || resolveTaskFile;
20
- const getTaskStatusFn = options.getTaskStatusFn || getTaskStatus;
21
- const toVirtualFn = options.toVirtualFn || toVirtual;
22
- const findMissionDirFn = options.findMissionDirFn || findMissionDir;
23
- const findCheckpointsFn = options.findCheckpointsFn || findCheckpoints;
24
- const getFirstLineFn = options.getFirstLineFn || getFirstLine;
25
- const inferSlugFn = options.inferSlugFn || inferSlug;
26
- const getMissionYearFn = options.getMissionYearFn || getMissionYear;
27
- const conventionalWorktreePathFn = options.conventionalWorktreePathFn || conventionalWorktreePath;
28
- const getLastCommitFn = options.getLastCommitFn || getLastCommit;
29
- const getPrStatusFn = options.getPrStatusFn || getPrStatus;
30
- const evaluateRepositoryReadinessFn = options.evaluateRepositoryReadinessFn || evaluateRepositoryReadiness;
31
- const evaluateReviewSetupFn = options.evaluateReviewSetupFn || evaluateReviewSetup;
32
- const adapterChecklistFn = options.adapterChecklistFn || adapterChecklist;
33
- const resolveMissionClassificationFn = options.resolveMissionClassificationFn || stats.resolveMissionClassification;
34
- const isForgejoReviewEnabledFn = options.isForgejoReviewEnabledFn || isForgejoReviewEnabled;
35
- const fsExistsSync = options.fsExistsSync || fs.existsSync;
36
- const resolveMissionBaseBranchFn = options.resolveMissionBaseBranchFn || resolveMissionBaseBranch;
37
- const getPrimaryBranchFn = options.getPrimaryBranchFn || getPrimaryBranch;
38
- const runFn = options.gitFn || git;
14
+ /** @param {string[]} args @param {{log?: Function, error?: Function, cwdFn?: Function, getCurrentBranchFn?: Function, resolveTaskFileFn?: Function, getTaskStatusFn?: Function, toVirtualFn?: Function, findMissionDirFn?: Function, findCheckpointsFn?: Function, getFirstLineFn?: Function, inferSlugFn?: Function, getMissionYearFn?: Function, conventionalWorktreePathFn?: Function, getLastCommitFn?: Function, getPrStatusFn?: Function, evaluateRepositoryReadinessFn?: Function, evaluateReviewSetupFn?: Function, adapterChecklistFn?: Function, resolveMissionClassificationFn?: Function, isForgejoReviewEnabledFn?: Function, fsExistsSync?: Function, resolveMissionBaseBranchFn?: Function, getPrimaryBranchFn?: Function, gitFn?: Function, command?: string, returnResult?: boolean}} opts */
15
+ function missionStart(args, opts = {}) {
16
+ const log = opts.log || fmt.log.plain;
17
+ const error = opts.error || fmt.log.plainError;
18
+ const cwdFn = opts.cwdFn || (() => process.cwd());
19
+ const getCurrentBranchFn = opts.getCurrentBranchFn || getCurrentBranch;
20
+ const resolveTaskFileFn = opts.resolveTaskFileFn || resolveTaskFile;
21
+ const getTaskStatusFn = opts.getTaskStatusFn || getTaskStatus;
22
+ const toVirtualFn = opts.toVirtualFn || toVirtual;
23
+ const findMissionDirFn = opts.findMissionDirFn || findMissionDir;
24
+ const findCheckpointsFn = opts.findCheckpointsFn || findCheckpoints;
25
+ const getFirstLineFn = opts.getFirstLineFn || getFirstLine;
26
+ const inferSlugFn = opts.inferSlugFn || inferSlug;
27
+ const getMissionYearFn = opts.getMissionYearFn || getMissionYear;
28
+ const conventionalWorktreePathFn = opts.conventionalWorktreePathFn || conventionalWorktreePath;
29
+ const getLastCommitFn = opts.getLastCommitFn || getLastCommit;
30
+ const getPrStatusFn = opts.getPrStatusFn || getPrStatus;
31
+ const evaluateRepositoryReadinessFn = opts.evaluateRepositoryReadinessFn || evaluateRepositoryReadiness;
32
+ const evaluateReviewSetupFn = opts.evaluateReviewSetupFn || evaluateReviewSetup;
33
+ const adapterChecklistFn = opts.adapterChecklistFn || adapterChecklist;
34
+ const resolveMissionClassificationFn = opts.resolveMissionClassificationFn || stats.resolveMissionClassification;
35
+ const isForgejoReviewEnabledFn = opts.isForgejoReviewEnabledFn || isForgejoReviewEnabled;
36
+ const fsExistsSync = opts.fsExistsSync || fs.existsSync;
37
+ const resolveMissionBaseBranchFn = opts.resolveMissionBaseBranchFn || resolveMissionBaseBranch;
38
+ const getPrimaryBranchFn = opts.getPrimaryBranchFn || getPrimaryBranch;
39
+ const runFn = opts.gitFn || git;
39
40
 
40
41
  const explicitSlug = args[0];
41
42
  const slug = inferSlugFn(explicitSlug);
42
- const isVerifyOnly = options.command === 'verify-env' || !slug;
43
- const returnResult = Boolean(options.returnResult);
43
+ const isVerifyOnly = opts.command === 'verify-env' || !slug;
44
+ const returnResult = Boolean(opts.returnResult);
44
45
 
45
46
  if (isVerifyOnly) {
46
47
  log(fmt.status('INFO', 'Running environment diagnostics (verify-env)...'));
@@ -49,6 +50,7 @@ function missionStart(args, options = {}) {
49
50
  }
50
51
 
51
52
  let overallFail = false;
53
+ /** @type{string[]} */
52
54
  const remediationSteps = [];
53
55
 
54
56
  // Check 1: PWD
@@ -150,8 +152,8 @@ function missionStart(args, options = {}) {
150
152
  } else {
151
153
  log(fmt.status('PASS', `Backlog classification: ${classification}`));
152
154
  }
153
- } catch (error) {
154
- log(fmt.status('FAIL', `Backlog classification: ${error.message}`));
155
+ } catch (/** @type {unknown} */ error) {
156
+ log(fmt.status('FAIL', `Backlog classification: ${/** @type {any} */(error).message}`));
155
157
  overallFail = true;
156
158
  }
157
159
  } else {
@@ -229,10 +231,13 @@ function missionStart(args, options = {}) {
229
231
  }
230
232
 
231
233
  // Extracted so callers can test the returnResult path without live git dependencies.
234
+ /** @param {boolean} overallFail @param {boolean} returnResult @param {{error?: Function, log?: Function, remediationSteps?: string[]}} options */
232
235
  function completePreflightOrExit(overallFail, returnResult, options = {}) {
233
- const error = options.error || fmt.log.plainError;
234
- const log = options.log || fmt.log.plain;
235
- const remediationSteps = options.remediationSteps || [];
236
+ /** @type{{error?: Function, log?: Function, remediationSteps?: string[]}} */
237
+ const opts = options;
238
+ const error = opts.error || fmt.log.plainError;
239
+ const log = opts.log || fmt.log.plain;
240
+ const remediationSteps = opts.remediationSteps || [];
236
241
  if (overallFail) {
237
242
  let msg = '\n' + fmt.status('FAIL', 'Environment verdict: NOT USABLE');
238
243
  if (remediationSteps.length > 0) {
@@ -244,11 +249,11 @@ function completePreflightOrExit(overallFail, returnResult, options = {}) {
244
249
  msg += ' — fix blockers above before proceeding.';
245
250
  }
246
251
  error(msg);
247
- if (returnResult) return { pass: false };
252
+ if (returnResult) {return { pass: false };}
248
253
  process.exit(1);
249
254
  } else {
250
255
  log('\n' + fmt.status('PASS', 'Environment verdict: USABLE — this repository is ready for workflow commands.'));
251
- if (returnResult) return { pass: true };
256
+ if (returnResult) {return { pass: true };}
252
257
  process.exit(0);
253
258
  }
254
259
  }
@@ -1,9 +1,9 @@
1
1
  const path = require('path');
2
2
  const { detectRebaseState, git, getCurrentBranch } = require('../core/git');
3
3
  const { resolveConflictsForMission } = require('./integrate');
4
- const { findMissionDir, findMissionArea, inferSlug, resolveWorktree, conventionalWorktreePath, getPrimaryWorktree, getPrimaryBranch, resolveMissionBaseBranch, missionBranchName, missionDirForSlug, getMissionYear } = require('../core/mission-utils');
4
+ const { findMissionDir, findMissionArea, inferSlug, resolveWorktree, conventionalWorktreePath, getPrimaryWorktree, getPrimaryBranch, resolveMissionBaseBranch, missionBranchName, missionDirForSlug } = require('../core/mission-utils');
5
5
  const { startAgent } = require('../agents/agents');
6
- const { createPr, readToken, resolveForgejoUser } = require('../tools/forgejo');
6
+ const { createPr, readToken, resolveForgejoUser, fetchReviewBranch } = require('../tools/forgejo');
7
7
  const { resolveTaskFile, getTaskImplementer } = require('../tools/backlog');
8
8
  const { resolveReviewIdentity } = require('../review/review-state');
9
9
  const { isForgejoReviewEnabled } = require('../core/product-config');
@@ -16,6 +16,7 @@ const { formatVerificationCommand } = require('../core/verification');
16
16
  *
17
17
  * Usage: px rebase [<slug>] [--push]
18
18
  */
19
+ /** @param {string[]} args @param {{inferSlugFn?: Function, findMissionDirFn?: Function, findMissionAreaFn?: Function, getCurrentBranchFn?: Function, resolveConflictsFn?: Function, startAgentFn?: Function, createPrFn?: Function, readTokenFn?: Function, resolveForgejoUserFn?: Function, resolveTaskFileFn?: Function, getTaskImplementerFn?: Function, detectRebaseStateFn?: Function, resolveMissionBaseBranchFn?: Function, gitFn?: Function, exitFn?: Function, isForgejoReviewEnabledFn?: Function, fetchReviewBranchFn?: Function}} opts */
19
20
  async function rebase(args, {
20
21
  inferSlugFn = inferSlug,
21
22
  findMissionDirFn = findMissionDir,
@@ -31,8 +32,9 @@ async function rebase(args, {
31
32
  detectRebaseStateFn = detectRebaseState,
32
33
  resolveMissionBaseBranchFn = resolveMissionBaseBranch,
33
34
  gitFn = git,
34
- exitFn = (code) => process.exit(code),
35
+ exitFn = (/** @type{number} */ code) => process.exit(code),
35
36
  isForgejoReviewEnabledFn = isForgejoReviewEnabled,
37
+ fetchReviewBranchFn = fetchReviewBranch,
36
38
  } = {}) {
37
39
  const flags = args.filter(a => a.startsWith('--'));
38
40
  const params = args.filter(a => !a.startsWith('--'));
@@ -57,7 +59,7 @@ async function rebase(args, {
57
59
  }
58
60
  if (existingRebase.unmergedFiles.length > 0) {
59
61
  fmt.log.info('Unmerged files:');
60
- existingRebase.unmergedFiles.forEach(file => fmt.log.info(` - ${file}`));
62
+ existingRebase.unmergedFiles.forEach((/** @type {string} */ file) => fmt.log.info(` - ${file}`));
61
63
  }
62
64
  fmt.log.info('Recovery commands:');
63
65
  fmt.log.info(` ${fmt.command('git rebase --continue')}`);
@@ -68,7 +70,7 @@ async function rebase(args, {
68
70
  }
69
71
 
70
72
  const performPush = async () => {
71
- if (!isPush) return;
73
+ if (!isPush) {return;}
72
74
  if (!isForgejoReviewEnabledFn(process.cwd())) {
73
75
  fmt.log.info(`Skipping Forgejo push (review provider is not forgejo).`);
74
76
  return;
@@ -85,13 +87,13 @@ async function rebase(args, {
85
87
  }
86
88
  forgejoUser = resolveForgejoUserFn(forgejoUser);
87
89
 
88
- const token = readTokenFn(forgejoUser);
90
+ const token = readTokenFn(forgejoUser || 'default');
89
91
  if (!token) {
90
- fmt.log.fail(`No Forgejo token found for user "${fmt.agent(forgejoUser)}". Push failed.`);
92
+ fmt.log.fail(`No Forgejo token found for user "${fmt.agent(forgejoUser || 'default')}". Push failed.`);
91
93
  exitFn(1);
92
94
  return;
93
95
  }
94
- const result = createPrFn(branch, forgejoUser, token, { rootDir: getPrimaryWorktree(), forceWithLease: true });
96
+ const result = createPrFn(branch, forgejoUser || 'default', token, { rootDir: getPrimaryWorktree(), forceWithLease: true });
95
97
  if (!result.ok) {
96
98
  fmt.log.fail(`Push to Forgejo failed: ${result.error}`);
97
99
  exitFn(1);
@@ -169,7 +171,9 @@ async function rebase(args, {
169
171
  const rootDir = getPrimaryWorktree();
170
172
  const worktreePath = resolveWorktree(slug) || conventionalWorktreePath(slug, rootDir);
171
173
 
172
- const conflictResult = resolveConflictsFn(slug, area, { worktreePathOverride: worktreePath });
174
+ /** @type{{worktreePathOverride?: string}} */
175
+ const conflictOpts = { worktreePathOverride: worktreePath };
176
+ const conflictResult = resolveConflictsFn(slug, area, conflictOpts);
173
177
 
174
178
  // When rebase is in progress in the worktree, dry merge may fail.
175
179
  // Fall back to parsing the rebase output directly.
@@ -200,7 +204,9 @@ async function rebase(args, {
200
204
  sharedFiles,
201
205
  });
202
206
  } else if (!conflictResult.ok) {
203
- if (conflictResult.error === 'worktree-missing') {
207
+ /** @type{{error?: string}} */
208
+ const cr = conflictResult;
209
+ if (cr.error === 'worktree-missing') {
204
210
  fmt.log.fail(`Mission worktree not found: ${fmt.path(worktreePath)}`);
205
211
  fmt.log.info(`Ensure the worktree is registered: ${fmt.command(`git worktree add ${conventionalWorktreePath(slug, getPrimaryWorktree())} ${branch}`)}`);
206
212
  exitFn(1);
@@ -221,7 +227,7 @@ async function rebase(args, {
221
227
  // Only treat unclassified conflict files as shared-file conflicts
222
228
  if (conflictResult.sharedFiles.length === 0 && conflictFiles.length > 0) {
223
229
  const classified = new Set([...conflictResult.missionSpecificFiles]);
224
- const unclassified = conflictFiles.filter(f => !classified.has(f));
230
+ const unclassified = conflictFiles.filter((/** @type {string} */ f) => !classified.has(f));
225
231
  if (unclassified.length > 0) {
226
232
  conflictResult.sharedFiles = unclassified;
227
233
  }
@@ -232,10 +238,12 @@ async function rebase(args, {
232
238
  fmt.log.info(`All ${conflictResult.missionSpecificFiles.length} conflict(s) are mission-specific. Auto-resolving...`);
233
239
  const maxContinueAttempts = 3;
234
240
  let continueAttempts = 0;
235
- const continueRebase = (opts) => {
241
+ /** @param {{stdio?: string}} opts */
242
+ const continueRebase = (/** @type {{stdio?: string}} */ opts = {}) => {
236
243
  continueAttempts += 1;
237
244
  return gitFn(['-c', 'core.editor=true', '-c', 'merge.autoedit=no', 'rebase', '--continue'], opts);
238
245
  };
246
+ /** @param {{stdout: string, stderr: string, status: number}} result */
239
247
  const reportContinueFailure = (result) => {
240
248
  const output = [result.stdout, result.stderr].filter(Boolean).join('\n').trim();
241
249
  fmt.log.fail(`git rebase --continue failed (status ${result.status}).`);
@@ -251,6 +259,7 @@ async function rebase(args, {
251
259
  fmt.log.fail(`Recovery: ${fmt.command('git rebase --abort')}`);
252
260
  };
253
261
 
262
+ /** @param {string} branchName */
254
263
  const failContinueBudget = (branchName) => {
255
264
  fmt.log.fail(`Rebase still in progress on branch ${fmt.branch(branchName || slug)} after ${continueAttempts} failed --continue attempt(s).`);
256
265
  fmt.log.fail(`Check rebase state: ${fmt.command('git status')}`);
@@ -415,7 +424,7 @@ async function rebase(args, {
415
424
 
416
425
  // Shared-file conflicts exist — launch agent to resolve them
417
426
  fmt.log.info(`${conflictResult.sharedFiles.length} shared file(s) require agent-assisted resolution:`);
418
- conflictResult.sharedFiles.forEach(f => fmt.log.info(` - ${fmt.path(f)}`));
427
+ conflictResult.sharedFiles.forEach((/** @type {string} */ f) => fmt.log.info(` - ${fmt.path(f)}`));
419
428
 
420
429
  const prompt = buildRebasePrompt({
421
430
  slug,
@@ -423,7 +432,7 @@ async function rebase(args, {
423
432
  worktreePath,
424
433
  missionSpecificFiles: conflictResult.missionSpecificFiles,
425
434
  sharedFiles: conflictResult.sharedFiles,
426
- gitFn,
435
+ gitFn: /** @type{Function} */(gitFn),
427
436
  });
428
437
 
429
438
  fmt.log.info('Launching agent for conflict resolution...');
@@ -461,23 +470,24 @@ async function rebase(args, {
461
470
  * when a rebase is in progress. Handles all unmerged states:
462
471
  * UU (unmerged), DU/UD (modify/delete), AU/UA (add/add), AA (add/add).
463
472
  */
473
+ /** @param {string} worktreePath @param {Function} gitFn */
464
474
  function parseConflictFilesFromGitStatus(worktreePath, gitFn) {
465
475
  const statusResult = gitFn(['-C', worktreePath, 'status', '--porcelain']);
466
476
  const files = [];
467
477
  const unmergedStates = new Set(['UU', 'DU', 'UD', 'AU', 'UA', 'AA']);
468
478
  for (const line of (statusResult.stdout || '').split('\n')) {
469
479
  const trimmed = line.trim();
470
- if (!trimmed) continue;
480
+ if (!trimmed) {continue;}
471
481
  const match = trimmed.match(/^([A-Z]{2})\s+(.+)$/);
472
482
  if (match) {
473
483
  const code = match[1];
474
- if (!unmergedStates.has(code)) continue;
484
+ if (!unmergedStates.has(code)) {continue;}
475
485
  let file = match[2].trim();
476
486
  // Remove surrounding quotes if present
477
487
  if ((file.startsWith('"') && file.endsWith('"')) || (file.startsWith("'") && file.endsWith("'"))) {
478
488
  file = file.slice(1, -1);
479
489
  }
480
- if (file) files.push(file);
490
+ if (file) {files.push(file);}
481
491
  }
482
492
  }
483
493
  return [...new Set(files)];
@@ -487,11 +497,12 @@ function parseConflictFilesFromGitStatus(worktreePath, gitFn) {
487
497
  * Parse conflict file paths from git rebase output.
488
498
  * Similar to parseConflictFilesFromMergeOutput but handles rebase-specific output.
489
499
  */
500
+ /** @param {string} output */
490
501
  function parseConflictFilesFromRebaseOutput(output) {
491
502
  const seen = new Set();
492
503
  const files = [];
493
504
  for ( const line of output.split('\n')) {
494
- if (!/CONFLICT|KONFLIKT/i.test(line)) continue;
505
+ if (!/CONFLICT|KONFLIKT/i.test(line)) {continue;}
495
506
  // English: "Merge conflict in <file>"
496
507
  const inMatch = line.match(/Merge conflict in (.+)$/);
497
508
  if (inMatch) {
@@ -539,7 +550,7 @@ function parseConflictFilesFromRebaseOutput(output) {
539
550
  const isNonPathPrefix = /^(CONFLICT|KONFLIKT|CONFLICTS|Merge conflict|Sammanslagningskonflikt|Automatic merge|Auto-merging|resolved|merged|deleted|added|changed|modified|rejected|skipped|dropped|superseded|discarded|kept|stashed|applied|already|would|both|ours|yours|their|his|her|its|your|my|us|we|they|he|she|it|a|an|the|but|and|or|for|nor|not|so|yet)\b/i.test(beforeColon);
540
551
  if (!isNonPathPrefix) {
541
552
  // Skip known advice/hint labels — the rest of the line is not a path.
542
- if (/^(tips|hint|note)\b/i.test(beforeColon)) continue;
553
+ if (/^(tips|hint|note)\b/i.test(beforeColon)) {continue;}
543
554
  const f = beforeColon;
544
555
  if (f && !seen.has(f)) { seen.add(f); files.push(f); }
545
556
  } else if (afterColon) {
@@ -566,12 +577,12 @@ function parseConflictFilesFromRebaseOutput(output) {
566
577
  /**
567
578
  * Build the prompt for the agent during shared-file conflict resolution.
568
579
  */
569
- function buildRebasePrompt({ slug, area, worktreePath, missionSpecificFiles, sharedFiles, gitFn = null }) {
580
+ /** @param {{slug: string, area: string, worktreePath: string, missionSpecificFiles: string[], sharedFiles: string[], gitFn?: Function}} opts */
581
+ function buildRebasePrompt({ slug, area, worktreePath, missionSpecificFiles, sharedFiles, gitFn = /** @type {Function | undefined} */ (undefined) }) {
570
582
  const missionFileCommands = missionSpecificFiles
571
583
  .map(f => ` git checkout --theirs "${f}" && git add "${f}"`)
572
584
  .join('\n');
573
585
  const sharedFileList = sharedFiles.map(f => ` - ${f}`).join('\n');
574
- const quotedWorktreePath = `"${worktreePath}"`;
575
586
 
576
587
  let primaryBranch = 'main';
577
588
  try {
@@ -1,4 +1,3 @@
1
- const path = require('path');
2
1
  const { git } = require('../core/git');
3
2
  const missionUtils = require('../core/mission-utils');
4
3
  const rebase = require('./rebase');
@@ -27,6 +26,7 @@ function isRelaunchableError(errorMsg) {
27
26
  * @param {string} worktree - Path to the mission worktree
28
27
  * @returns {string} The relaunch prompt
29
28
  */
29
+ /** @param {string} errorMsg @param {string} slug @param {string} worktree */
30
30
  function buildRelaunchPrompt(errorMsg, slug, worktree) {
31
31
  const year = missionUtils.getMissionYear(slug, worktree);
32
32
  const missionDir = missionUtils.findMissionDir(slug, worktree) || missionUtils.missionDirForSlug(worktree, slug);
@@ -78,23 +78,26 @@ function buildRelaunchPrompt(errorMsg, slug, worktree) {
78
78
  * @param {object} [options]
79
79
  * @returns {Promise<{repaired: boolean, blocker: string|null}>} Result and optional blocker reason
80
80
  */
81
+ /** @param {string} slug @param {string} worktree @param {string} errorMsg @param {{gitFn?: Function, rebaseFn?: Function, log?: Function, error?: Function}} options */
81
82
  async function repairHandoff(slug, worktree, errorMsg, options = {}) {
83
+ /** @type {{gitFn?: Function, rebaseFn?: Function, log?: Function, error?: Function}} */
84
+ const opts = options;
82
85
  const {
83
- taskFile = null,
84
86
  gitFn = git,
85
87
  rebaseFn = rebase,
86
88
  log = fmt.log.plain,
87
89
  error = fmt.log.plainError
88
- } = options;
90
+ } = opts;
89
91
 
90
92
  const rootDir = worktree || process.cwd();
91
93
  let repaired = false;
92
94
  let blocker = null;
93
95
 
96
+ /** @param {string} line */
94
97
  function parsePorcelainPath(line) {
95
98
  const xy = line.slice(0, 2);
96
99
  const rawPath = line.slice(3).trim();
97
- const pathPart = rawPath.includes('->') ? rawPath.split('->').pop().trim() : rawPath;
100
+ const pathPart = rawPath.includes('->') ? (rawPath.split('->').pop() || '').trim() : rawPath;
98
101
  const cleanPath = (pathPart.startsWith('"') && pathPart.endsWith('"')) ? pathPart.slice(1, -1) : pathPart;
99
102
  return { xy, file: cleanPath };
100
103
  }
@@ -128,38 +131,38 @@ async function repairHandoff(slug, worktree, errorMsg, options = {}) {
128
131
  const statusResult = gitFn(['-C', rootDir, 'status', '--porcelain']);
129
132
  if (statusResult.status === 0 && statusResult.stdout) {
130
133
  const dirtyLines = statusResult.stdout.split('\n')
131
- .filter(line => line.trim().length > 0);
134
+ .filter(/** @param {string} line */ (line) => line.trim().length > 0);
132
135
 
133
136
  const dirtyFilesWithStatus = dirtyLines.map(parsePorcelainPath);
134
137
 
135
- const unmerged = dirtyFilesWithStatus.filter(f =>
138
+ const unmerged = dirtyFilesWithStatus.filter((/** @param {{xy: string, file: string}} f */ f =>
136
139
  ['DD', 'AU', 'UD', 'UA', 'DU', 'AA', 'UU'].includes(f.xy)
137
- );
140
+ ));
138
141
 
139
142
  if (unmerged.length > 0) {
140
- blocker = `Conflicted files detected:\n${unmerged.map(f => ` - ${f.file}`).join('\n')}`;
143
+ blocker = `Conflicted files detected:\n${unmerged.map((/** @param {{xy: string, file: string}} f */ f => ` - ${f.file}`)).join('\n')}`;
141
144
  log(`Cannot auto-commit: ${blocker}`);
142
145
  return { repaired: false, blocker };
143
146
  }
144
147
 
145
- const dirtyFiles = dirtyFilesWithStatus.map(f => f.file);
148
+ const dirtyFiles = dirtyFilesWithStatus.map((/** @param {{xy: string, file: string}} f */ f => f.file));
146
149
 
147
- const isSafeToCommit = (file) =>
150
+ const isSafeToCommit = (/** @type{string} */ file) =>
148
151
  missionUtils.isWorkflowGeneratedArtifact(file)
149
152
  || missionUtils.isMissionArtifact(file, slug, rootDir);
150
153
 
151
154
  const safeFiles = dirtyFiles.filter(isSafeToCommit);
152
- const unsafeFiles = dirtyFiles.filter(f => !isSafeToCommit(f));
155
+ const unsafeFiles = dirtyFiles.filter((/** @param {string} f */ f => !isSafeToCommit(f)));
153
156
 
154
157
  if (unsafeFiles.length > 0) {
155
158
  log(`Cannot auto-commit: dirty files include non-mission paths:`);
156
- unsafeFiles.forEach(f => log(` - ${f}`));
159
+ unsafeFiles.forEach((/** @type {string} */ f) => log(` - ${f}`));
157
160
  blocker = `dirty files include non-mission paths: ${unsafeFiles.join(', ')}`;
158
161
  return { repaired: false, blocker };
159
162
  } else if (safeFiles.length > 0) {
160
163
  log(`Auto-committing mission artifacts:`);
161
- const stageFailures = [];
162
- safeFiles.forEach(f => {
164
+ const stageFailures = /** @type {string[]} */ ([]);
165
+ safeFiles.forEach((/** @type {string} */ f) => {
163
166
  log(` - ${f}`);
164
167
  const addResult = gitFn(['-C', rootDir, 'add', '--', f]);
165
168
  if (addResult.status === 0) {
@@ -195,9 +198,9 @@ async function repairHandoff(slug, worktree, errorMsg, options = {}) {
195
198
  try {
196
199
  const { getCurrentBranch } = require('../core/git');
197
200
  await rebaseFn([slug], {
198
- gitFn: (args, opts) => gitFn(args, { ...opts, cwd: rootDir }),
201
+ gitFn: (/** @type {string[]} */ args, /** @type {{cwd?: string}} */ opts) => gitFn(args, { ...opts, cwd: rootDir }),
199
202
  getCurrentBranchFn: () => getCurrentBranch(rootDir),
200
- exitFn: (code) => {
203
+ exitFn: (/** @type {number} */ code) => {
201
204
  if (code === 0) {
202
205
  rebaseSuccess = true;
203
206
  } else {
@@ -205,8 +208,8 @@ async function repairHandoff(slug, worktree, errorMsg, options = {}) {
205
208
  }
206
209
  }
207
210
  });
208
- } catch (err) {
209
- rebaseError = err.message;
211
+ } catch (/** @type {unknown} */ err) {
212
+ rebaseError = /** @type{Error} */(err).message;
210
213
  }
211
214
 
212
215
  if (!rebaseSuccess) {
@@ -4,10 +4,12 @@ const { startAgent } = require('../agents/agents');
4
4
  const fmt = require('../core/fmt');
5
5
  const { formatVerificationCommand } = require('../core/verification');
6
6
 
7
+ /** @param {string} value */
7
8
  function shellQuote(value) {
8
9
  return `"${String(value).replace(/(["\\$`])/g, '\\$1')}"`;
9
10
  }
10
11
 
12
+ /** @param {{slug: string, area: string, worktreePath: string, missionSpecificFiles: string[]}} opts */
11
13
  function buildAgentResolutionPrompt({ slug, area, worktreePath, missionSpecificFiles }) {
12
14
  const fileCommands = missionSpecificFiles
13
15
  .map(f => ` git checkout --theirs "${f}" && git add "${f}"`)
@@ -41,10 +43,11 @@ function buildAgentResolutionPrompt({ slug, area, worktreePath, missionSpecificF
41
43
  ].join('\n');
42
44
  }
43
45
 
46
+ /** @param {string[]} args @param {{resolveConflictsFn?: Function, startAgentFn?: Function, exitFn?: Function}} opts */
44
47
  async function resolveConflict(args, {
45
48
  resolveConflictsFn = resolveConflictsForMission,
46
49
  startAgentFn = startAgent,
47
- exitFn = (code) => process.exit(code),
50
+ exitFn = /** @type {(code: number) => void} */ ((code) => process.exit(code)),
48
51
  } = {}) {
49
52
  const explicitSlug = args[0];
50
53
  const slug = inferSlug(explicitSlug);
@@ -69,7 +72,7 @@ async function resolveConflict(args, {
69
72
  // Stop and require human intervention.
70
73
  if (!result.ok && result.error === 'shared-file-conflicts') {
71
74
  fmt.log.fail('Shared-file conflicts require human resolution. Agent cannot guess the correct merge outcome.');
72
- fmt.log.info(`Shared files: ${result.sharedFiles.map(f => fmt.path(f)).join(', ')}`);
75
+ fmt.log.info(`Shared files: ${result.sharedFiles.map(/** @param {string} f */ (f) => fmt.path(f)).join(', ')}`);
73
76
  fmt.log.info(`Resolve manually, then re-run: ${fmt.command(`px integrate ${slug} --dry-run`)}`);
74
77
  exitFn(1);
75
78
  return;
@@ -6,6 +6,7 @@ const review = require('../review/review-commands').review;
6
6
  *
7
7
  * Usage: node parallix review [<slug>] [--verify] [--submit] [--push] ...
8
8
  */
9
+ /** @param {string[]} args */
9
10
  async function reviewCommand(args) {
10
11
  await review(args);
11
12
  }
@@ -6,6 +6,7 @@ const setupReview = require('../tools/setup-review');
6
6
  *
7
7
  * Usage: node parallix setup-review
8
8
  */
9
+ /** @param {string[]} args @param {object} options */
9
10
  async function setupReviewCommand(args, options) {
10
11
  await setupReview(args, options);
11
12
  }