@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
@@ -4,6 +4,7 @@ const { git } = require('../core/git');
4
4
  const { getPrimaryBranch, missionBranchName, inferSlug, resolveWorktree } = require('../core/mission-utils');
5
5
  const fmt = require('../core/fmt');
6
6
 
7
+ /** @param {string[]} args @param {{gitFn?: Function, spawnSyncFn?: Function, getPrimaryBranchFn?: Function, missionBranchNameFn?: Function, inferSlugFn?: Function, resolveWorktreeFn?: Function, exitFn?: Function, failFn?: Function}} opts */
7
8
  async function diff(args, {
8
9
  gitFn = git,
9
10
  spawnSyncFn = childProcess.spawnSync,
@@ -14,6 +15,7 @@ async function diff(args, {
14
15
  exitFn = process.exit,
15
16
  failFn = fmt.log.fail,
16
17
  } = {}) {
18
+ /** @param {string} a */
17
19
  const explicitSlug = args.filter(a => !a.startsWith('--'))[0];
18
20
  const slug = inferSlugFn(explicitSlug);
19
21
 
@@ -34,7 +36,7 @@ async function diff(args, {
34
36
  try {
35
37
  primary = getPrimaryBranchFn(worktree);
36
38
  } catch (err) {
37
- failFn(err.message);
39
+ failFn(/** @type{Error} */(err).message);
38
40
  exitFn(1);
39
41
  return;
40
42
  }
@@ -43,30 +45,34 @@ async function diff(args, {
43
45
  const target = `${primary}..HEAD`;
44
46
 
45
47
  // Detect configured diff tool/pager
48
+ /** @param {string} key */
46
49
  const getGitConfig = (key) => {
47
50
  const result = gitFn(['-C', worktree, 'config', '--get', key]);
48
51
  return result.status === 0 ? result.stdout.trim() : null;
49
52
  };
50
53
 
54
+ /** @param {string} cmd */
51
55
  const isSpecializedTool = (cmd) => {
52
- if (!cmd) return false;
56
+ if (!cmd) {return false;}
53
57
  const parts = cmd.split(/\s+/);
54
58
  // Handle cases like "LESS=FRX less" or "/usr/bin/delta"
59
+ /** @param {string} p */
55
60
  const exePart = parts.find(p => !p.includes('='));
56
- if (!exePart) return false;
61
+ if (!exePart) {return false;}
57
62
  const exe = path.basename(exePart).toLowerCase();
58
63
 
59
64
  // Explicitly reject known default pagers
60
- if (['less', 'cat', 'more'].includes(exe)) return false;
65
+ if (['less', 'cat', 'more'].includes(exe)) {return false;}
61
66
 
62
67
  // Accept known specialized tools documented in review-tooling.md
63
- if (['delta', 'difft', 'diff-so-fancy'].includes(exe)) return true;
68
+ if (['delta', 'difft', 'diff-so-fancy'].includes(exe)) {return true;}
64
69
 
65
70
  // Fallback: if it's not a known default, and we have a value, treat it as specialized
66
71
  // for now to avoid breaking custom setups like "vimdiff", but we've rejected plain "less".
67
72
  return true;
68
73
  };
69
74
 
75
+ /** @param {childProcess.SpawnSyncReturns<string>} result @param {string} toolName */
70
76
  const handleLaunchResult = (result, toolName) => {
71
77
  if (result.error) {
72
78
  failFn(`Failed to launch ${toolName}: ${result.error.message}`);
@@ -4,7 +4,7 @@ const crypto = require('crypto');
4
4
  const fmt = require('../core/fmt');
5
5
  const { git, getWorktreeStatus } = require('../core/git');
6
6
  const { startDraftAgent, selectAgent, readAgentConfigOrExit } = require('../agents/agents');
7
- const { resolveTaskFile, enforceTaskAssignee, reportTaskResolution, checkBacklogIntegrity, transitionTask, getTaskClassification, getTaskStorage } = require('../tools/backlog');
7
+ const { resolveTaskFile, enforceTaskAssignee, reportTaskResolution, checkBacklogIntegrity, transitionTask, getTaskStorage } = require('../tools/backlog');
8
8
  const { findMissionArea, findMissionDir, inferSlug, getMissionYear, resolveMainRepo, conventionalWorktreePath, squashTrailingBacklogNoiseIntoPreviousMission, resolveWorktree, getPrimaryBranch, missionBranchName, missionDirForSlug, detectLaunchBaseBranch } = require('../core/mission-utils');
9
9
  const { transitionVirtual } = require('../core/state-map');
10
10
  const stats = require('./stats');
@@ -17,7 +17,7 @@ const DRAFT_PROMPT_PATH = path.join(__dirname, '..', '..', 'prompts', 'draft.md'
17
17
  const MISSION_SCAFFOLD_PATH = path.join(__dirname, '..', '..', 'templates', 'mission-scaffold.md');
18
18
  const SYNTHETIC_SLUG_PREFIX = 'adhoc-';
19
19
 
20
- function slugifyDraftIntent(value) {
20
+ function slugifyDraftIntent(/** @type {string} */ value) {
21
21
  return String(value || '')
22
22
  .trim()
23
23
  .toLowerCase()
@@ -28,7 +28,7 @@ function slugifyDraftIntent(value) {
28
28
  .slice(0, 64);
29
29
  }
30
30
 
31
- function syntheticTaskId(slug, seed) {
31
+ function syntheticTaskId(/** @type {string} */ slug, /** @type {string} */ seed) {
32
32
  const hash = crypto.createHash('sha1').update(String(seed || slug)).digest('hex').slice(0, 8).toUpperCase();
33
33
  const prefix = slug.startsWith(SYNTHETIC_SLUG_PREFIX) ? 'ADHOC' : 'TASK';
34
34
  const base = slug
@@ -38,9 +38,9 @@ function syntheticTaskId(slug, seed) {
38
38
  return `${prefix}-${base}-${hash}`;
39
39
  }
40
40
 
41
- function resolveDraftTarget(rawInput, cwd = process.cwd()) {
41
+ function resolveDraftTarget(/** @type {string} */ rawInput, cwd = process.cwd()) {
42
42
  const explicit = String(rawInput || '').trim();
43
- if (!explicit) return null;
43
+ if (!explicit) {return null;}
44
44
 
45
45
  if (explicit.toLowerCase().startsWith('task-')) {
46
46
  return {
@@ -90,7 +90,7 @@ function resolveDraftTarget(rawInput, cwd = process.cwd()) {
90
90
  };
91
91
  }
92
92
 
93
- async function runDraftCommand(args, {
93
+ async function runDraftCommand(/** @type {string[]} */ args, {
94
94
  inferSlugFn = inferSlug,
95
95
  resolveMainRepoFn = resolveMainRepo,
96
96
  conventionalWorktreePathFn = conventionalWorktreePath,
@@ -136,9 +136,9 @@ async function runDraftCommand(args, {
136
136
  const syntheticTask = draftTarget.syntheticTask;
137
137
 
138
138
  // Allow operators to pin the agent family via CLI flag instead of WORKFLOW_AGENT env var.
139
- function flagValue(arr, flag, name) {
139
+ function flagValue(/** @type {string[]} */ arr, /** @type {string} */ flag, /** @type {string} */ name) {
140
140
  const i = arr.indexOf(flag);
141
- if (i === -1) return null;
141
+ if (i === -1) {return null;}
142
142
  const v = arr[i + 1];
143
143
  if (!v || v.startsWith('--')) {
144
144
  errorFn(fmt.status('FAIL', `Missing value for --${name}. Usage: px draft <slug> --${name} <family>`));
@@ -178,7 +178,7 @@ async function runDraftCommand(args, {
178
178
  try {
179
179
  launchBase = detectLaunchBaseBranchFn(process.cwd());
180
180
  } catch (error) {
181
- errorFn(fmt.status('FAIL', error.message));
181
+ errorFn(fmt.status('FAIL', /** @type {any} */ (error).message));
182
182
  exitFn(1);
183
183
  return;
184
184
  }
@@ -208,9 +208,9 @@ async function runDraftCommand(args, {
208
208
  errorFn(fmt.status('FAIL', `Backlog integrity issues detected for ${normalizedSlug}:`));
209
209
  relevantIssues.forEach(issue => {
210
210
  if (issue.type === 'duplicate-completed') {
211
- logFn(` - ${fmt.path(issue.file)}: task ${fmt.bold(issue.taskId)} already has a canonical copy in ${fmt.path(issue.canonicalFile)}; this backlog/tasks copy is stale.`);
211
+ logFn(` - ${fmt.path(/** @type {any} */ (issue).file)}: task ${fmt.bold(/** @type {any} */ (issue).taskId)} already has a canonical copy in ${fmt.path(/** @type {any} */ (issue).canonicalFile)}; this backlog/tasks copy is stale.`);
212
212
  } else {
213
- logFn(` - ${fmt.path(issue.file)}: filename ID (${fmt.bold(issue.filenameId)}) does not match frontmatter ID (${fmt.bold(issue.frontmatterId)})`);
213
+ logFn(` - ${fmt.path(/** @type {any} */ (issue).file)}: filename ID (${fmt.bold(/** @type {any} */ (issue).filenameId)}) does not match frontmatter ID (${fmt.bold(/** @type {any} */ (issue).frontmatterId)})`);
214
214
  }
215
215
  });
216
216
  logFn('Repair: Fix filename/id mismatch, or remove the stale backlog/tasks copy of a completed/archived task, before drafting.');
@@ -244,7 +244,7 @@ async function runDraftCommand(args, {
244
244
  ensureMissionBaseBranchRecordedFn(missionFile, recordedBase, { logFn });
245
245
 
246
246
  logFn(fmt.bold('Step 4: Ensuring Backlog task exists in worktree...'));
247
- if (!bootstrapBacklogTaskFn(targetWorktree, mainRepo, normalizedSlug, { logFn, errorFn, syntheticTask })) {
247
+ if (!bootstrapBacklogTaskFn(targetWorktree, mainRepo, normalizedSlug, { logFn, errorFn, syntheticTask: /** @type {null | undefined} */ (syntheticTask) })) {
248
248
  const { tasksDir } = getTaskStorage(targetWorktree);
249
249
  const taskDirHint = path.relative(targetWorktree, tasksDir).split(path.sep).join('/');
250
250
  errorFn(fmt.status('FAIL', `Backlog task for ${normalizedSlug} could not be prepared in the mission worktree.`));
@@ -276,23 +276,25 @@ async function runDraftCommand(args, {
276
276
  const agentConfig = readAgentConfigOrExitFn();
277
277
  const agent = preselectedAgent || selectAgentFn('draft', { config: agentConfig });
278
278
 
279
- const prompt = buildDraftPrompt(normalizedSlug, { rootDir: mainRepo, worktree: targetWorktree });
279
+ // @ts-expect-error buildDraftPrompt type mismatch
280
+ const prompt = buildDraftPrompt(normalizedSlug, { rootDir: mainRepo, worktree: targetWorktree || '' });
280
281
  logFn('Launching draft agent...');
281
282
  const { agent: actualAgent, result } = await startDraftAgentFn({
282
283
  prompt,
284
+ // @ts-expect-error worktree not in type
283
285
  worktree: targetWorktree,
284
286
  agent
285
287
  });
286
- logFn(`Draft agent family: ${fmt.agent(actualAgent)}`);
288
+ logFn(`Draft agent family: ${fmt.agent(/** @type {any} */ (actualAgent))}`);
287
289
 
288
290
  if (result.error) {
289
- errorFn(fmt.status('FAIL', `Could not start draft agent (${fmt.agent(actualAgent)}): ${result.error.message}`));
291
+ errorFn(fmt.status('FAIL', `Could not start draft agent (${fmt.agent(/** @type {any} */ (actualAgent))}): ${/** @type {any} */ (result.error).message}`));
290
292
  exitFn(1);
291
293
  return;
292
294
  }
293
295
 
294
- if (typeof result.status === 'number' && result.status !== 0) {
295
- errorFn(fmt.status('FAIL', `Draft agent (${fmt.agent(actualAgent)}) exited with status ${result.status}.`));
296
+ if (typeof /** @type {any} */ (result).status === 'number' && /** @type {any} */ (result).status !== 0) {
297
+ errorFn(fmt.status('FAIL', `Draft agent (${fmt.agent(/** @type {any} */ (actualAgent))}) exited with status ${/** @type {any} */ (result).status}.`));
296
298
  exitFn(result.status || 1);
297
299
  return;
298
300
  }
@@ -315,6 +317,7 @@ async function runDraftCommand(args, {
315
317
  agentFamily: actualAgent,
316
318
  result,
317
319
  log: logFn,
320
+ // @ts-expect-error reportTaskResolutionFn accepts extra properties
318
321
  error: errorFn
319
322
  });
320
323
 
@@ -328,6 +331,7 @@ async function runDraftCommand(args, {
328
331
  const restartOk = await restartDraftAgentFn(normalizedSlug, targetWorktree, {
329
332
  logFn,
330
333
  errorFn,
334
+ // @ts-expect-error restartDraftAgentFn accepts extra properties
331
335
  exitFn
332
336
  });
333
337
  if (!restartOk) {
@@ -355,12 +359,12 @@ async function runDraftCommand(args, {
355
359
  try {
356
360
  enforceDraftCommitSafetyFn({ slug: normalizedSlug, worktree: targetWorktree, logFn, errorFn });
357
361
  } catch (error) {
358
- errorFn(fmt.status('FAIL', error.message));
362
+ errorFn(fmt.status('FAIL', /** @type {any} */ (error).message));
359
363
  exitFn(1);
360
364
  return;
361
365
  }
362
366
 
363
- if (!transitionVirtualFn(transitionTaskFn, normalizedSlug, 'ready', { rootDir: targetWorktree, log: logFn })) {
367
+ if (!transitionVirtualFn(transitionTaskFn, normalizedSlug, 'ready', /** @type {{ rootDir: string, log: Function }} */ ({ rootDir: targetWorktree, log: logFn }))) {
364
368
  errorFn(fmt.status('FAIL', `Could not transition task ${normalizedSlug} to ready status.`));
365
369
  exitFn(1);
366
370
  return;
@@ -369,18 +373,24 @@ async function runDraftCommand(args, {
369
373
  logFn('\n' + fmt.status('INFO', `Next: ${fmt.command(`cd ${targetWorktree}`)}`));
370
374
  }
371
375
 
376
+ // @ts-expect-error implicit any on args/deps
372
377
  async function draft(args, deps) {
373
378
  return runDraftCommand(args, deps);
374
379
  }
375
380
 
376
381
  function recordDraftImplementer({
382
+ // @ts-expect-error implicit any binding elements
377
383
  selected,
384
+ // @ts-expect-error implicit any binding elements
378
385
  actual,
386
+ // @ts-expect-error implicit any binding elements
379
387
  taskResolution,
380
388
  log = fmt.log.plain,
381
389
  enforceTaskAssigneeFn = enforceTaskAssignee,
382
390
  gitFn = git,
391
+ // @ts-expect-error implicit any binding elements
383
392
  slug,
393
+ // @ts-expect-error implicit any binding elements
384
394
  worktree
385
395
  }) {
386
396
  if (!taskResolution || !taskResolution.ok || !actual) {
@@ -408,6 +418,7 @@ function recordDraftImplementer({
408
418
  return actual;
409
419
  }
410
420
 
421
+ // @ts-expect-error implicit any on mainRepo/branchName
411
422
  function ensureMissionBranch(mainRepo, branchName, {
412
423
  gitFn = git,
413
424
  logFn = fmt.log.plain,
@@ -436,6 +447,7 @@ function ensureMissionBranch(mainRepo, branchName, {
436
447
  * detached-HEAD launch) or when the correct line is already present. Replaces a
437
448
  * stale line in place, otherwise inserts the line just under the title.
438
449
  */
450
+ // @ts-expect-error implicit any on missionFile/baseBranch
439
451
  function ensureMissionBaseBranchRecorded(missionFile, baseBranch, { logFn = fmt.log.plain } = {}) {
440
452
  if (!baseBranch || !missionFile || !fs.existsSync(missionFile)) {
441
453
  return false;
@@ -463,6 +475,7 @@ function ensureMissionBaseBranchRecorded(missionFile, baseBranch, { logFn = fmt.
463
475
  return true;
464
476
  }
465
477
 
478
+ // @ts-expect-error implicit any on mainRepo/targetWorktree/branchName
466
479
  function ensureWorktree(mainRepo, targetWorktree, branchName, {
467
480
  existsFn = fs.existsSync,
468
481
  gitFn = git,
@@ -484,11 +497,12 @@ function ensureWorktree(mainRepo, targetWorktree, branchName, {
484
497
  gitFn(['-C', mainRepo, 'worktree', 'add', targetWorktree, branchName]);
485
498
  logFn(fmt.status('PASS', `Created worktree at ${fmt.path(targetWorktree)}.`));
486
499
  } catch (error) {
487
- errorFn(fmt.status('FAIL', `Could not create worktree: ${error.message}`));
500
+ errorFn(fmt.status('FAIL', `Could not create worktree: ${/** @type {any} */ (error).message}`));
488
501
  exitFn(1);
489
502
  }
490
503
  }
491
504
 
505
+ // @ts-expect-error implicit any on targetWorktree
492
506
  function ensureGraphifyWorkspace(targetWorktree, { logFn = fmt.log.plain } = {}) {
493
507
  const targetPath = path.join(targetWorktree, 'graphify-out');
494
508
 
@@ -511,6 +525,7 @@ function ensureGraphifyWorkspace(targetWorktree, { logFn = fmt.log.plain } = {})
511
525
  return true;
512
526
  }
513
527
 
528
+ // @ts-expect-error implicit any on targetWorktree
514
529
  function ensureGraphifyIgnore(targetWorktree, { gitFn = git, logFn = fmt.log.plain } = {}) {
515
530
  const targetPath = path.join(targetWorktree, '.graphifyignore');
516
531
 
@@ -538,12 +553,13 @@ function ensureGraphifyIgnore(targetWorktree, { gitFn = git, logFn = fmt.log.pla
538
553
  gitFn(['-C', gitRoot, 'commit', '-m', 'workflow: add .graphifyignore to exclude .workflow/ from graphify']);
539
554
  logFn(fmt.status('PASS', `Created and committed .graphifyignore in ${fmt.path(gitRoot)}.`));
540
555
  } catch (error) {
541
- logFn(fmt.status('WARN', `Created .graphifyignore but could not commit: ${error.message}. It will be picked up by the draft safety harness.`));
556
+ logFn(fmt.status('WARN', `Created .graphifyignore but could not commit: ${/** @type {any} */ (error).message}. It will be picked up by the draft safety harness.`));
542
557
  }
543
558
 
544
559
  return true;
545
560
  }
546
561
 
562
+ // @ts-expect-error implicit any on targetWorktree/slug
547
563
  function ensureMissionFile(targetWorktree, slug, { logFn = fmt.log.plain } = {}) {
548
564
  const missionDir = missionDirForSlug(targetWorktree, slug);
549
565
  if (!fs.existsSync(missionDir)) {
@@ -563,6 +579,7 @@ function ensureMissionFile(targetWorktree, slug, { logFn = fmt.log.plain } = {})
563
579
  return missionFile;
564
580
  }
565
581
 
582
+ // @ts-expect-error implicit any on mainRepo
566
583
  function ensureDraftRepoConfigCommitted(mainRepo, {
567
584
  getWorktreeStatusFn = getWorktreeStatus,
568
585
  errorFn = fmt.log.plainError
@@ -593,6 +610,7 @@ function ensureDraftRepoConfigCommitted(mainRepo, {
593
610
  return false;
594
611
  }
595
612
 
613
+ // @ts-expect-error implicit any on targetWorktree/mainRepo/slug
596
614
  function bootstrapBacklogTask(targetWorktree, mainRepo, slug, {
597
615
  resolveTaskFileFn = resolveTaskFile,
598
616
  reportTaskResolutionFn = reportTaskResolution,
@@ -614,11 +632,11 @@ function bootstrapBacklogTask(targetWorktree, mainRepo, slug, {
614
632
 
615
633
  if (syntheticTask) {
616
634
  const { tasksDir } = getTaskStorage(targetWorktree);
617
- const taskPath = path.join(tasksDir, `${slug} - ${slugifyDraftIntent(syntheticTask.title || slug) || 'mission'}.md`);
635
+ const taskPath = path.join(tasksDir, `${slug} - ${slugifyDraftIntent(/** @type {any} */ (syntheticTask).title || slug) || 'mission'}.md`);
618
636
  const body = [
619
637
  '---',
620
- `id: ${syntheticTask.id || syntheticTaskId(slug, syntheticTask.intent || slug)}`,
621
- `title: ${syntheticTask.title || slug}`,
638
+ `id: ${/** @type {any} */ (syntheticTask).id || syntheticTaskId(slug, /** @type {any} */ (syntheticTask).intent || slug)}`,
639
+ `title: ${/** @type {any} */ (syntheticTask).title || slug}`,
622
640
  'status: backlog',
623
641
  'assignee: []',
624
642
  "created_date: '" + new Date().toISOString().slice(0, 16).replace('T', ' ') + "'",
@@ -629,7 +647,7 @@ function bootstrapBacklogTask(targetWorktree, mainRepo, slug, {
629
647
  '',
630
648
  '## Description',
631
649
  '',
632
- syntheticTask.intent || `Synthetic task created for ${slug}.`,
650
+ /** @type {any} */ (syntheticTask).intent || `Synthetic task created for ${slug}.`,
633
651
  ''
634
652
  ].join('\n');
635
653
 
@@ -643,7 +661,7 @@ function bootstrapBacklogTask(targetWorktree, mainRepo, slug, {
643
661
  gitFn(['-C', targetWorktree, 'commit', '-m', `backlog(${slug}): create synthetic task`]);
644
662
  logFn(fmt.status('PASS', 'Committed synthetic task in worktree.'));
645
663
  } catch (error) {
646
- errorFn(fmt.status('FAIL', `Could not commit synthetic task: ${error.message}`));
664
+ errorFn(fmt.status('FAIL', `Could not commit synthetic task: ${/** @type {any} */ (error).message}`));
647
665
  return false;
648
666
  }
649
667
  return true;
@@ -656,10 +674,12 @@ function bootstrapBacklogTask(targetWorktree, mainRepo, slug, {
656
674
  return false;
657
675
  }
658
676
 
677
+ // @ts-expect-error mainResolution.taskFile may be undefined
659
678
  const relativePath = path.relative(mainRepo, mainResolution.taskFile);
660
- const targetPath = path.join(targetWorktree, relativePath);
679
+ const targetPath = path.join(targetWorktree || '', relativePath);
661
680
 
662
681
  fs.mkdirSync(path.dirname(targetPath), { recursive: true });
682
+ // @ts-expect-error mainResolution.taskFile may be undefined
663
683
  fs.copyFileSync(mainResolution.taskFile, targetPath);
664
684
 
665
685
  logFn(fmt.status('PASS', `Bootstrapped ${fmt.path(relativePath)} from main repo.`));
@@ -669,17 +689,19 @@ function bootstrapBacklogTask(targetWorktree, mainRepo, slug, {
669
689
  gitFn(['-C', targetWorktree, 'commit', '-m', `backlog(${slug}): bootstrap task from ${getPrimaryBranch()}`]);
670
690
  logFn(fmt.status('PASS', 'Committed bootstrapped task in worktree.'));
671
691
  } catch (error) {
672
- errorFn(fmt.status('FAIL', `Could not commit bootstrapped task: ${error.message}`));
692
+ errorFn(fmt.status('FAIL', `Could not commit bootstrapped task: ${/** @type {any} */ (error).message}`));
673
693
  return false;
674
694
  }
675
695
 
676
696
  return true;
677
697
  }
678
698
 
699
+ // @ts-expect-error implicit any on rootDir
679
700
  function resolveVerifyCmd(rootDir) {
680
- return formatVerificationCommand(null, rootDir);
701
+ return formatVerificationCommand(undefined, rootDir);
681
702
  }
682
703
 
704
+ // @ts-expect-error implicit any on slug/promptRoot
683
705
  function resolveTaskPath(slug, promptRoot) {
684
706
  const resolution = resolveTaskFile(slug, promptRoot);
685
707
  if (resolution && resolution.ok && resolution.taskFile) {
@@ -688,6 +710,7 @@ function resolveTaskPath(slug, promptRoot) {
688
710
  return path.join(promptRoot, 'backlog', 'tasks', `<${slug}>.md`);
689
711
  }
690
712
 
713
+ // @ts-expect-error implicit any on taskPath
691
714
  function resolveClassificationInstructions(taskPath) {
692
715
  if (taskPath && fs.existsSync(taskPath)) {
693
716
  const content = fs.readFileSync(taskPath, 'utf8');
@@ -695,9 +718,10 @@ function resolveClassificationInstructions(taskPath) {
695
718
  return 'because this task was synthesized by the harness, preserve the `unknown` label unless you have concrete repo-specific evidence to replace it. Do not add a separate frontmatter field for mission type.';
696
719
  }
697
720
  }
698
- return 'set exactly one of `ai_sdlc` or `user_value` in the Backlog task labels — no other value. Use `ai_sdlc` for workflow, prompt, or agent-fix work; use `user_value` for everything else (including code tech debt). Do not add a separate frontmatter field for mission type.';
721
+ return 'set exactly one of `ai_sdlc` or `user_value` in the Backlog task labels — plus optionally `bug` for bug-fix missions. Use `ai_sdlc` for workflow, prompt, or agent-fix work; use `user_value` for everything else (including code tech debt). Do not add a separate frontmatter field for mission type.';
699
722
  }
700
723
 
724
+ // @ts-expect-error implicit any on slug/rootDir/worktree
701
725
  function buildDraftPrompt(slug, { rootDir = process.cwd(), worktree = null } = {}) {
702
726
  const template = fs.readFileSync(DRAFT_PROMPT_PATH, 'utf8');
703
727
  const promptRoot = worktree || rootDir;
@@ -715,10 +739,12 @@ function buildDraftPrompt(slug, { rootDir = process.cwd(), worktree = null } = {
715
739
  .replaceAll('{{verifyCmd}}', resolveVerifyCmd(promptRoot));
716
740
  }
717
741
 
742
+ // @ts-expect-error implicit any on slug
718
743
  function fallbackDraftCommitMessage(slug) {
719
744
  return `draft(${slug}): capture agent output`;
720
745
  }
721
746
 
747
+ // @ts-expect-error implicit any on slug/worktree
722
748
  function validateDraftClassification(slug, worktree, {
723
749
  resolveMissionClassificationFn = stats.resolveMissionClassification,
724
750
  errorFn = fmt.log.plainError
@@ -726,19 +752,20 @@ function validateDraftClassification(slug, worktree, {
726
752
  try {
727
753
  const { classification, error: classificationError } = resolveMissionClassificationFn(slug, worktree);
728
754
  if (!classification) {
729
- if (classificationError) errorFn(fmt.status('FAIL', classificationError));
755
+ if (classificationError) {errorFn(fmt.status('FAIL', classificationError));}
730
756
  return { ok: true, classification: null };
731
757
  }
732
758
  return { ok: true, classification };
733
759
  } catch (error) {
734
- if (error.message.includes('Missing or invalid classification')) {
760
+ if (/** @type {any} */ (error).message.includes('Missing or invalid classification')) {
735
761
  return { ok: true, classification: null };
736
762
  }
737
- errorFn(fmt.status('FAIL', error.message));
763
+ errorFn(fmt.status('FAIL', /** @type {any} */ (error).message));
738
764
  return { ok: false, reason: 'invalid-classification' };
739
765
  }
740
766
  }
741
767
 
768
+ // @ts-expect-error implicit any on slug/worktree
742
769
  function normalizeDraftClassification(slug, worktree, {
743
770
  resolveMissionClassificationFn = stats.resolveMissionClassification,
744
771
  errorFn = fmt.log.plainError
@@ -746,59 +773,62 @@ function normalizeDraftClassification(slug, worktree, {
746
773
  try {
747
774
  const { classification, error: classificationError } = resolveMissionClassificationFn(slug, worktree);
748
775
  if (!classification) {
749
- if (classificationError) errorFn(fmt.status('FAIL', classificationError));
776
+ if (classificationError) {errorFn(fmt.status('FAIL', classificationError));}
750
777
  return { ok: false, reason: 'missing-classification' };
751
778
  }
752
779
  return { ok: true, classification };
753
780
  } catch (error) {
754
- if (error.message.includes('Missing or invalid classification')) {
781
+ if (/** @type {any} */ (error).message.includes('Missing or invalid classification')) {
755
782
  return { ok: false, reason: 'missing-classification' };
756
783
  }
757
- errorFn(fmt.status('FAIL', error.message));
784
+ errorFn(fmt.status('FAIL', /** @type {any} */ (error).message));
758
785
  return { ok: false, reason: 'invalid-classification' };
759
786
  }
760
787
  }
761
788
 
789
+ // @ts-expect-error implicit any on slug/rootDir/worktree
762
790
  function buildRestartPrompt(slug, { rootDir = process.cwd(), worktree = null } = {}) {
763
791
  return `${buildDraftPrompt(slug, { rootDir, worktree })}
764
792
 
765
793
  Focused repair:
766
- - update the backlog task so labels contain exactly one of \`ai_sdlc\` or \`user_value\`
794
+ - update the backlog task so labels contain exactly one of \`ai_sdlc\` or \`user_value\` (plus optionally \`bug\` if this is a bug fix)
767
795
  - use \`ai_sdlc\` for workflow, prompt, or agent-fix work; use \`user_value\` for everything else, including standard code tech debt
768
796
  - do not add a separate frontmatter field for mission type
769
- - if both labels are present, keep only the correct one
797
+ - if both classification labels are present, keep only the correct one
770
798
  `;
771
799
  }
772
800
 
801
+ // @ts-expect-error implicit any on slug/worktree
773
802
  async function restartDraftAgent(slug, worktree, {
774
803
  selectAgentFn = selectAgent,
775
804
  startDraftAgentFn = startDraftAgent,
776
805
  readAgentConfigOrExitFn = readAgentConfigOrExit,
777
806
  logFn = fmt.log.plain,
778
- errorFn = fmt.log.plainError,
779
- exitFn = process.exit
807
+ errorFn = fmt.log.plainError
780
808
  } = {}) {
781
809
  const agentConfig = readAgentConfigOrExitFn();
782
810
  const agent = selectAgentFn('draft', { config: agentConfig });
783
811
 
784
812
  const prompt = buildRestartPrompt(slug, { rootDir: worktree, worktree });
785
813
  logFn('Relaunching draft agent to repair mission type labels...');
814
+ // @ts-expect-error startDraftAgentFn accepts extra properties
786
815
  const { agent: actualAgent, result } = await startDraftAgentFn({ prompt, worktree, agent });
787
- logFn(`Restart draft agent family: ${fmt.agent(actualAgent)}`);
816
+ logFn(`Restart draft agent family: ${fmt.agent(/** @type {any} */ (actualAgent))}`);
788
817
 
789
818
  if (result.error) {
790
- errorFn(fmt.status('FAIL', `Could not restart draft agent (${fmt.agent(actualAgent)}): ${result.error.message}`));
819
+ errorFn(fmt.status('FAIL', `Could not restart draft agent (${fmt.agent(/** @type {any} */ (actualAgent))}): ${/** @type {any} */ (result.error).message}`));
791
820
  return false;
792
821
  }
793
822
 
794
- if (typeof result.status === 'number' && result.status !== 0) {
795
- errorFn(fmt.status('FAIL', `Restart draft agent (${fmt.agent(actualAgent)}) exited with status ${result.status}.`));
823
+ if (typeof /** @type {any} */ (result).status === 'number' && /** @type {any} */ (result).status !== 0) {
824
+ errorFn(fmt.status('FAIL', `Restart draft agent (${fmt.agent(/** @type {any} */ (actualAgent))}) exited with status ${/** @type {any} */ (result).status}.`));
796
825
  return false;
797
826
  }
798
827
 
799
828
  return true;
800
829
  }
801
830
 
831
+ // @ts-expect-error implicit any on entry
802
832
  function parseDirtyEntry(entry) {
803
833
  const match = entry.match(/^(.{1,2})\s+(.*)$/);
804
834
  const status = (match ? match[1] : entry.slice(0, 2)).padEnd(2, ' ');
@@ -816,20 +846,24 @@ function parseDirtyEntry(entry) {
816
846
  return { status, filePath, sourcePath };
817
847
  }
818
848
 
849
+ // @ts-expect-error implicit any on status
819
850
  function isUnmergedStatus(status) {
820
851
  return ['DD', 'AU', 'UD', 'UA', 'DU', 'AA', 'UU'].includes(status);
821
852
  }
822
853
 
854
+ // @ts-expect-error implicit any on status
823
855
  function isDeletedStatus(status) {
824
856
  return status.includes('D') && !isUnmergedStatus(status);
825
857
  }
826
858
 
859
+ // @ts-expect-error implicit any on filePath/slug
827
860
  function isMissionTaskPath(filePath, slug) {
828
- if (!filePath) return false;
861
+ if (!filePath) {return false;}
829
862
  const taskPattern = new RegExp(`^backlog/(?:tasks|completed)/[^/]*${slug}(?:\\b|[^/]*$)`);
830
863
  return taskPattern.test(filePath);
831
864
  }
832
865
 
866
+ // @ts-expect-error implicit any on filePath/slug/worktree
833
867
  function isExpectedDraftPath(filePath, slug, worktree) {
834
868
  const missionDir = findMissionDir(slug, worktree);
835
869
  const missionPrefix = missionDir
@@ -838,21 +872,23 @@ function isExpectedDraftPath(filePath, slug, worktree) {
838
872
  return filePath.startsWith(missionPrefix) || isMissionTaskPath(filePath, slug);
839
873
  }
840
874
 
875
+ // @ts-expect-error implicit any on dirtyEntries/slug/worktree
841
876
  function classifyDraftEntries(dirtyEntries, slug, worktree) {
842
877
  const parsedEntries = dirtyEntries.map(parseDirtyEntry);
843
- const conflictEntries = parsedEntries.filter(entry => isUnmergedStatus(entry.status));
844
- const stagedEntries = parsedEntries.filter(entry => !isUnmergedStatus(entry.status));
845
- const expectedEntries = stagedEntries.filter(entry => isExpectedDraftPath(entry.filePath, slug, worktree));
846
- const unexpectedEntries = stagedEntries.filter(entry => !isExpectedDraftPath(entry.filePath, slug, worktree));
878
+ const conflictEntries = parsedEntries.filter((/** @type {any} */ entry) => isUnmergedStatus(entry.status));
879
+ const stagedEntries = parsedEntries.filter((/** @type {any} */ entry) => !isUnmergedStatus(entry.status));
880
+ const expectedEntries = stagedEntries.filter((/** @type {any} */ entry) => isExpectedDraftPath(entry.filePath, slug, worktree));
881
+ const unexpectedEntries = stagedEntries.filter((/** @type {any} */ entry) => !isExpectedDraftPath(entry.filePath, slug, worktree));
847
882
 
848
883
  return { conflictEntries, expectedEntries, unexpectedEntries };
849
884
  }
850
885
 
851
- function resolveMissionSpecificDraftConflicts({ slug, worktree, conflictEntries, gitImpl = git, logFn = fmt.log.plain, errorFn = fmt.log.plainError }) {
852
- const sharedConflicts = conflictEntries.filter(entry => !isExpectedDraftPath(entry.filePath, slug, worktree));
886
+ // @ts-expect-error implicit any on slug/worktree/conflictEntries
887
+ function resolveMissionSpecificDraftConflicts({ slug, worktree, conflictEntries, gitImpl = git, logFn = fmt.log.plain }) {
888
+ const sharedConflicts = conflictEntries.filter((/** @type {any} */ entry) => !isExpectedDraftPath(entry.filePath, slug, worktree));
853
889
  if (sharedConflicts.length > 0) {
854
890
  const area = findMissionArea(findMissionDir(slug, worktree) || missionDirForSlug(worktree, slug));
855
- const sharedFiles = sharedConflicts.map(entry => entry.filePath);
891
+ const sharedFiles = sharedConflicts.map((/** @type {any} */ entry) => entry.filePath);
856
892
  throw new Error(
857
893
  `Draft safety harness found shared-file conflicts: ${sharedFiles.join(', ')}. ` +
858
894
  `Run "px resolve-conflict ${slug}" from ${worktree}, then re-run ${formatVerificationCommand(area, worktree)}.`
@@ -871,6 +907,7 @@ function resolveMissionSpecificDraftConflicts({ slug, worktree, conflictEntries,
871
907
  }
872
908
  }
873
909
 
910
+ // @ts-expect-error implicit any on slug/worktree/dirtyEntries
874
911
  function enforceDraftCommitSafety({ slug, worktree, dirtyEntries = getWorktreeStatus(worktree), gitImpl = git, logFn = fmt.log.plain, errorFn = fmt.log.plainError }) {
875
912
  if (dirtyEntries.length === 0) {
876
913
  logFn(fmt.status('PASS', 'Draft safety harness: no uncommitted changes left behind.'));
@@ -883,6 +920,7 @@ function enforceDraftCommitSafety({ slug, worktree, dirtyEntries = getWorktreeSt
883
920
  }
884
921
 
885
922
  const { conflictEntries, expectedEntries, unexpectedEntries } = classifyDraftEntries(dirtyEntries, slug, worktree);
923
+ // @ts-expect-error errorFn not in type
886
924
  resolveMissionSpecificDraftConflicts({ slug, worktree, conflictEntries, gitImpl, logFn, errorFn });
887
925
 
888
926
  const deletedTaskEntries = [...expectedEntries, ...unexpectedEntries].filter(entry =>
@@ -937,6 +975,7 @@ function enforceDraftCommitSafety({ slug, worktree, dirtyEntries = getWorktreeSt
937
975
  return true;
938
976
  }
939
977
 
978
+ // @ts-expect-error implicit any on mainRepo
940
979
  function ensureRepoExists(mainRepo, exitFn = process.exit, errorFn = fmt.log.fail) {
941
980
  if (!fs.existsSync(mainRepo)) {
942
981
  errorFn(`Main repository not found at ${mainRepo}. Please ensure it exists or set PRIMARY_WORKTREE.`);
@@ -952,8 +991,9 @@ function ensureRepoExists(mainRepo, exitFn = process.exit, errorFn = fmt.log.fai
952
991
  * families record honest zeros with provider/model set to the family name.
953
992
  * Best-effort: a failure here must never fail the draft.
954
993
  */
955
- function recordDraftStats({ slug, rootDir, agentFamily, result, log = fmt.log.plain, error = fmt.log.plainError }) {
956
- if (!result) return;
994
+ // @ts-expect-error implicit any on slug/rootDir/agentFamily/result
995
+ function recordDraftStats({ slug, rootDir, agentFamily, result, log = fmt.log.plain }) {
996
+ if (!result) {return;}
957
997
  let durationMinutes = 0;
958
998
  if (result.startedAt && result.endedAt) {
959
999
  durationMinutes = (Date.parse(result.endedAt) - Date.parse(result.startedAt)) / 60000;
@@ -971,7 +1011,7 @@ function recordDraftStats({ slug, rootDir, agentFamily, result, log = fmt.log.pl
971
1011
  log(fmt.status('INFO', `Draft stats recorded: ${slug} stage=draft provider=${row.provider} model=${row.model} input_tokens=${row.input_tokens} tool_calls=${row.tool_calls}`));
972
1012
  } catch (err) {
973
1013
  // Best-effort: never escalate to the fatal `error` channel.
974
- log(fmt.status('WARN', `Could not record draft stats for ${slug}: ${err.message}`));
1014
+ log(fmt.status('WARN', `Could not record draft stats for ${slug}: ${/** @type {any} */ (err).message}`));
975
1015
  }
976
1016
  }
977
1017