@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,7 +4,6 @@
4
4
  * Handles autonomous review loop orchestration.
5
5
  */
6
6
 
7
- const os = require('os');
8
7
  const fs = require('fs');
9
8
  const path = require('path');
10
9
  const fmt = require('../core/fmt');
@@ -12,20 +11,20 @@ const { git, run } = require('../core/git');
12
11
  const { findMissionDir, resolveWorktree, missionBranchName } = require('../core/mission-utils');
13
12
  const { resolveTaskFile, getTaskImplementer, getTaskStatus, enforceTaskAssignee, transitionTask, reportTaskResolution } = require('../tools/backlog');
14
13
  const { toVirtual, transitionVirtual } = require('../core/state-map');
15
- const { getPrStatus, readToken, getLatestReviewForPr, getLatestDispositionForPr, providerAvailable, getComments, postComment, postReview, closePr, resolveReviewUser, isProviderEnabled } = require('./review-adapter');
14
+ const { getPrStatus, readToken, getLatestReviewForPr, getLatestDispositionForPr, providerAvailable, getComments, postComment, postReview, resolveReviewUser, isProviderEnabled } = require('./review-adapter');
16
15
  const { buildAutonomousReviewMatrix, formatMatrixSummary } = require('../core/runtime-matrix');
17
16
  const { buildReviewPrompt, buildActOnReviewPrompt, buildCompactReviewPrompt, buildCompactActOnReviewPrompt } = require('./review-prompts');
18
17
  const { ReviewState, readReviewState, writeReviewState, resetReviewState, VALID_PHASES } = require('./review-state');
19
- const { createEvent, consumeHumanNotes, VALID_EVENT_TYPES } = require('./review-events');
18
+
20
19
  const { workflowLauncherStatus, startAgent, eligibleAgentsForStep, selectAgent } = require('../agents/agents');
21
20
  const { performHandoff } = require('../commands/handoff');
22
21
  const { commitSafeMissionArtifacts, rebaseBeforeReviewRound } = require('./rebase');
23
22
  const { resolveAgentModel } = require('../core/product-config');
24
23
 
25
24
  // Import from review-polling module
26
- const { POLL_TIMEOUT, delay, resolvePollIntervalMs, resolvePollTimeoutMs, formatElapsed, isPollTimeout, pollForReview, pollForDisposition } = require('./review-polling');
25
+ const { delay, resolvePollIntervalMs, resolvePollTimeoutMs, formatElapsed, isPollTimeout, pollForReview, pollForDisposition } = require('./review-polling');
27
26
  // Import from review-artifacts module
28
- const { buildMetadataFooter, reviewArtifactPath, resolveArtifactDir, readArtifactFile, deleteArtifactFile, normalizeReviewVerdict, normalizeDisposition, postWorkflowComment, postWorkflowReview, consumeReviewerArtifacts, consumeImplementerArtifacts } = require('./review-artifacts');
27
+ const { buildMetadataFooter, resolveArtifactDir, consumeReviewerArtifacts, consumeImplementerArtifacts } = require('./review-artifacts');
29
28
  const stats = require('../commands/stats');
30
29
  const { resolveStageTelemetry } = require('../agents/stage-telemetry');
31
30
 
@@ -40,6 +39,11 @@ const { resolveStageTelemetry } = require('../agents/stage-telemetry');
40
39
  // one launch at a time, de-duped via review-state metadata, so Claude/custom rows
41
40
  // are cumulative too. A family switch creates a separate row instead of mixing
42
41
  // agents in one record.
42
+ /**
43
+ * @param {string} agentFamily
44
+ * @param {{sessionId?: string, startedAt?: string, endedAt?: string, status?: number}} result
45
+ * @returns {string}
46
+ */
43
47
  function stageLaunchFingerprint(agentFamily, result) {
44
48
  return [
45
49
  String(agentFamily || '').trim().toLowerCase(),
@@ -50,29 +54,43 @@ function stageLaunchFingerprint(agentFamily, result) {
50
54
  ].join('|');
51
55
  }
52
56
 
53
- function markStageLaunchRecorded(state, { stage, agentFamily, result, slug, worktree, writeReviewStateFn = writeReviewState } = {}) {
54
- if (!state || !agentFamily) return true;
57
+ /**
58
+ * @param {Object|null} state
59
+ * @param {{stage: string, agentFamily: string, result?: {[key: string]: any}, slug: string, worktree?: string, writeReviewStateFn?: Function}} opts
60
+ * @returns {boolean}
61
+ */
62
+ function markStageLaunchRecorded(state, /** @type {{stage: string, agentFamily: string, result?: {[key: string]: any}, slug: string, worktree?: string, writeReviewStateFn?: Function}} */ opts) {
63
+ const { stage, agentFamily, result, slug, worktree, writeReviewStateFn = writeReviewState } = opts || {};
64
+ if (!state || !agentFamily) {return true;}
55
65
  const key = stageWindowKey(stage, agentFamily);
56
- const fingerprint = stageLaunchFingerprint(agentFamily, result);
57
- const recordedStageLaunches = state.metadata && typeof state.metadata === 'object'
58
- ? (state.metadata.recordedStageLaunches || {})
66
+ const fingerprint = stageLaunchFingerprint(agentFamily, /** @type {{sessionId?: string, startedAt?: string, endedAt?: string, status?: number}} */ (result));
67
+ /** @type {{[key: string]: any}} */
68
+ const stateObj = /** @type {{[key: string]: any}} */ (state);
69
+ const recordedStageLaunches = stateObj.metadata && typeof stateObj.metadata === 'object'
70
+ ? (stateObj.metadata.recordedStageLaunches || {})
59
71
  : {};
60
72
  const recorded = Array.isArray(recordedStageLaunches[key]) ? recordedStageLaunches[key] : [];
61
73
  if (recorded.includes(fingerprint)) {
62
74
  return false;
63
75
  }
64
- if (!state.metadata || typeof state.metadata !== 'object') {
65
- state.metadata = {};
76
+ if (!stateObj.metadata || typeof stateObj.metadata !== 'object') {
77
+ stateObj.metadata = {};
66
78
  }
67
- state.metadata.recordedStageLaunches = {
68
- ...(state.metadata.recordedStageLaunches || {}),
79
+ stateObj.metadata.recordedStageLaunches = {
80
+ ...(stateObj.metadata.recordedStageLaunches || {}),
69
81
  [key]: [...recorded, fingerprint].slice(-20),
70
82
  };
71
83
  writeReviewStateFn(slug, state, worktree);
72
84
  return true;
73
85
  }
74
86
 
75
- function recordStageStatsSafe(kind, { stage, slug, rootDir, worktree, implementer, reviewer, result, sinceMs, log, error, state, writeReviewStateFn = writeReviewState, model = null }) {
87
+ /**
88
+ * @param {'review'|'active'} kind
89
+ * @param {{stage: string, slug: string, rootDir?: string, worktree?: string, implementer?: string, reviewer?: string, result?: {[key: string]: any}, sinceMs?: number, log?: Function, state?: {[key: string]: any}, writeReviewStateFn?: Function, model?: string|null}} opts
90
+ * @returns {void}
91
+ */
92
+ function recordStageStatsSafe(kind, { stage, slug, rootDir, worktree, implementer, reviewer, result, sinceMs, log, state, writeReviewStateFn = writeReviewState, model = null }) {
93
+ /** @type {number} */
76
94
  let durationMinutes = 0;
77
95
  if (result && result.startedAt && result.endedAt) {
78
96
  durationMinutes = (Date.parse(result.endedAt) - Date.parse(result.startedAt)) / 60000;
@@ -81,10 +99,10 @@ function recordStageStatsSafe(kind, { stage, slug, rootDir, worktree, implemente
81
99
  // prefer the windowed Codex rollout, else the launcher-attached telemetry so
82
100
  // Claude active/review stages record real tokens instead of all-zeros
83
101
  // (task-1274), bounded to this stage by sinceMs (task-1285 F6).
84
- const telemetry = resolveStageTelemetry({ worktree, result, sinceMs });
102
+ const telemetry = resolveStageTelemetry({ worktree: /** @type {string} */ (worktree || ''), result: /** @type {{[key: string]: any}} */ (result || {}), sinceMs: (/** @type {number | undefined} */ (sinceMs)) || 0 });
85
103
  try {
86
104
  const actorFamily = kind === 'review' ? reviewer : implementer;
87
- if (state && !markStageLaunchRecorded(state, {
105
+ if (state && actorFamily && !markStageLaunchRecorded(state, {
88
106
  stage,
89
107
  agentFamily: actorFamily,
90
108
  result,
@@ -94,16 +112,17 @@ function recordStageStatsSafe(kind, { stage, slug, rootDir, worktree, implemente
94
112
  })) {
95
113
  return;
96
114
  }
97
- stats.accumulateStageStats({ stage, slug, rootDir, implementer, reviewer, telemetry, durationMinutes, model });
98
- } catch (err) {
115
+ /** @type {any} */ (stats).accumulateStageStats({ stage, slug, rootDir, implementer, reviewer, telemetry, durationMinutes, model });
116
+ } catch (/** @type {unknown} */ err) {
99
117
  // Best-effort: never escalate to the fatal `error` channel.
100
- log(fmt.status('WARN', `Could not record ${kind} stats for ${slug}: ${err.message}`));
118
+ log?.(fmt.status('WARN', `Could not record ${kind} stats for ${slug}: ${/** @type {Error} */(err).message}`));
101
119
  }
102
120
  }
103
121
 
104
122
  const DEFAULT_MAX_ATTEMPTS = 5;
105
123
  const CONTINUE_SKIP_CHECK_TIMEOUT_MS = 10_000;
106
124
 
125
+ /** @param {string} earlierIso @param {number} [nowMs] */
107
126
  function strictlyLaterIso(earlierIso, nowMs = Date.now()) {
108
127
  const earlierMs = Date.parse(earlierIso);
109
128
  if (!Number.isFinite(earlierMs)) {
@@ -116,6 +135,7 @@ function strictlyLaterIso(earlierIso, nowMs = Date.now()) {
116
135
  // Pre-review Setup Functions
117
136
  // ============================================================================
118
137
 
138
+ /** @param {string} rootDir @param {{commandRunner?: Function, log?: Function}} [options] */
119
139
  function maybeUpdateGraphifyBeforeReview(rootDir, { commandRunner = run, log = fmt.log.plain } = {}) {
120
140
  const updateGraphifyKnowledgeGraph = require('../core/mission-utils').updateGraphifyKnowledgeGraph;
121
141
  return updateGraphifyKnowledgeGraph({
@@ -137,18 +157,12 @@ function maybeUpdateGraphifyBeforeReview(rootDir, { commandRunner = run, log = f
137
157
  // fallback identity selected by the launcher.
138
158
  // This helper detects the fallback, persists the new identity to review state
139
159
  // and the Backlog assignee, and returns the identity that should be polled for.
140
- function applyAgentFallback({
141
- role,
142
- original,
143
- launchResult,
144
- state,
145
- slug,
146
- worktree,
147
- taskResolution,
148
- log = fmt.log.plain,
149
- writeReviewStateFn = writeReviewState,
150
- enforceTaskAssigneeFn = enforceTaskAssignee
151
- } = {}) {
160
+ /**
161
+ * @param {{role: string, original: string, launchResult?: {[key: string]: any}, state: {[key: string]: any}, slug: string, worktree?: string, taskResolution?: {[key: string]: any}, log?: Function, writeReviewStateFn?: Function, enforceTaskAssigneeFn?: Function}} opts
162
+ * @returns {string}
163
+ */
164
+ function applyAgentFallback(/** @type {{role: string, original: string, launchResult?: {[key: string]: any}, state: {[key: string]: any}, slug: string, worktree?: string, taskResolution?: {[key: string]: any}, log?: Function, writeReviewStateFn?: Function, enforceTaskAssigneeFn?: Function}} */ opts) {
165
+ const { role, original, launchResult, state, slug, worktree, taskResolution, log = fmt.log.plain, writeReviewStateFn = writeReviewState, enforceTaskAssigneeFn } = opts;
152
166
  if (!launchResult || !launchResult.agent || launchResult.agent === original) {
153
167
  return original;
154
168
  }
@@ -161,13 +175,20 @@ function applyAgentFallback({
161
175
  }
162
176
  writeReviewStateFn(slug, state, worktree);
163
177
  if (role === 'implementer' && taskResolution && taskResolution.ok) {
164
- if (!enforceTaskAssigneeFn(taskResolution.taskFile, fallback)) {
178
+ if (enforceTaskAssigneeFn && !enforceTaskAssigneeFn(taskResolution.taskFile, fallback)) {
165
179
  log(fmt.status('WARN', `Could not enforce fallback implementer ${fallback} in backlog task.`));
166
180
  }
167
181
  }
168
182
  return fallback;
169
183
  }
170
184
 
185
+ /**
186
+ * @param {string} slug
187
+ * @param {{[key: string]: any}} state
188
+ * @param {string} worktree
189
+ * @param {{log?: Function, writeReviewStateFn?: Function}} [options]
190
+ * @returns {void}
191
+ */
171
192
  function persistNormalizedPhaseRepair(slug, state, worktree, { log = fmt.log.plain, writeReviewStateFn = writeReviewState } = {}) {
172
193
  if (!state || !state.phaseOriginal || VALID_PHASES.includes(state.phaseOriginal)) {
173
194
  return;
@@ -177,6 +198,7 @@ function persistNormalizedPhaseRepair(slug, state, worktree, { log = fmt.log.pla
177
198
  state.phaseOriginal = null;
178
199
  }
179
200
 
201
+ /** @param {string} stage @param {string} agentFamily */
180
202
  function stageWindowKey(stage, agentFamily) {
181
203
  const normalizedStage = String(stage || 'default').trim().toLowerCase() || 'default';
182
204
  const normalizedAgent = String(agentFamily || '').trim().toLowerCase();
@@ -190,6 +212,7 @@ function stageWindowKey(stage, agentFamily) {
190
212
  // instead of re-summing a cumulative-since-first-launch window every round.
191
213
  // Resume-idempotency is provided separately by the launch fingerprint in
192
214
  // markStageLaunchRecorded, so the window itself does not need to persist.
215
+ /** @param {{startedAt?: string}} result */
193
216
  function stageLaunchSinceMs(result) {
194
217
  const startedAt = result && result.startedAt ? String(result.startedAt) : '';
195
218
  const startedMs = startedAt ? Date.parse(startedAt) : NaN;
@@ -200,6 +223,11 @@ function stageLaunchSinceMs(result) {
200
223
  // Main Review Loop
201
224
  // ============================================================================
202
225
 
226
+ /**
227
+ * @param {string} slug
228
+ * @param {{implementer?: string, reviewer?: string, focus?: string, maxAttempts?: number, dryRun?: boolean, reset?: boolean, continue?: boolean, isContinue?: boolean, verbose?: boolean, pollTimeoutSeconds?: number|null, worktree?: string, missionPath?: string, resetReviewStateFn?: Function, maybeUpdateGraphifyBeforeReviewFn?: Function, readReviewStateFn?: Function, resolveTaskFileFn?: Function, getTaskImplementerFn?: Function, getTaskStatusFn?: Function, transitionTaskFn?: Function, toVirtualFn?: Function, transitionVirtualFn?: Function, workflowLauncherStatusFn?: Function, buildAutonomousReviewMatrixFn?: Function, formatMatrixSummaryFn?: Function, selectAgentFn?: Function, providerAvailableFn?: Function, runFn?: Function, getPrStatusFn?: Function, enforceTaskAssigneeFn?: Function, resolveReviewUserFn?: Function, forgejoAvailableFn?: Function, resolveForgejoUserFn?: Function, readTokenFn?: Function, getCommentsFn?: Function, postCommentFn?: Function, postReviewFn?: Function, writeReviewStateFn?: Function, startAgentFn?: Function, pollForReviewFn?: Function, pollForDispositionFn?: Function, applyAgentFallbackFn?: Function, buildReviewPromptFn?: Function, buildActOnReviewPromptFn?: Function, buildCompactReviewPromptFn?: Function, buildCompactActOnReviewPromptFn?: Function, consumeReviewerArtifactsFn?: Function, consumeImplementerArtifactsFn?: Function, rebaseBeforeReviewRoundFn?: Function, eligibleAgentsForStepFn?: Function, performHandoffFn?: Function, log?: Function, error?: Function, getLatestReviewForPrFn?: Function, getLatestDispositionForPrFn?: Function, sleepFn?: Function, exit?: Function, gitFn?: Function, isReviewProviderEnabledFn?: Function, legacyIsForgejoReviewEnabledFn?: Function, isForgejoReviewEnabledFn?: Function, recordStageStatsSafeFn?: Function}} [opts]
229
+ * @returns {Promise<void>}
230
+ */
203
231
  async function startReviewLoop(slug, opts = {}) {
204
232
  let {
205
233
  implementer,
@@ -268,7 +296,7 @@ async function startReviewLoop(slug, opts = {}) {
268
296
  isContinue = Boolean(isContinue || continueFlag);
269
297
 
270
298
  const pollIntervalMs = resolvePollIntervalMs();
271
- const pollTimeoutMs = resolvePollTimeoutMs(pollTimeoutSeconds);
299
+ const pollTimeoutMs = resolvePollTimeoutMs(/** @type {number} */ (pollTimeoutSeconds || 0));
272
300
 
273
301
  const worktree = callerWorktree || resolveWorktree(slug) || process.cwd();
274
302
  const resolvedProviderAvailableFn = providerAvailableFn || forgejoAvailableFn || providerAvailable;
@@ -285,7 +313,7 @@ async function startReviewLoop(slug, opts = {}) {
285
313
  // slug-derived default). Absent the flag, `effectiveMissionPath` stays
286
314
  // undefined and the prompts fall back to the standard slug-derived path.
287
315
  const missionDir = findMissionDir(slug, worktree, { missionPath });
288
- let effectiveMissionPath;
316
+ let effectiveMissionPath = /** @type {string|null} */ (null);
289
317
  if (missionPath && fs.existsSync(missionPath)) {
290
318
  effectiveMissionPath = fs.statSync(missionPath).isDirectory()
291
319
  ? path.join(missionPath, 'MISSION.md')
@@ -385,9 +413,9 @@ async function startReviewLoop(slug, opts = {}) {
385
413
  // PR exists. Only fall back to guidance when self-heal cannot yield an open PR.
386
414
  // Emits `--push` (the command self-heal attempts and the correct manual
387
415
  // equivalent) — never the old, frequently-wrong `--submit`.
388
- const fallbackGuidance = (reason) => {
416
+ const fallbackGuidance = (/** @type {string|null} */ reason) => {
389
417
  error(fmt.status('FAIL', `No open review PR found for ${branch}. Create the PR before starting the review loop.`));
390
- if (reason) error(` Handoff failure: ${reason}`);
418
+ if (reason) {error(` Handoff failure: ${reason}`);}
391
419
  error(` Run: px review ${slug} --push`);
392
420
  };
393
421
 
@@ -441,7 +469,7 @@ async function startReviewLoop(slug, opts = {}) {
441
469
  // Resolve reviewer after the no-PR/self-heal gate so implementation-phase
442
470
  // guidance does not depend on workstation launcher availability.
443
471
  let reviewerSource = 'explicit';
444
- let selectErr = null;
472
+ /** @type {Error|null} */ let selectErr = null;
445
473
  if (!reviewer) {
446
474
  if (persisted && persisted.reviewer) {
447
475
  reviewer = persisted.reviewer;
@@ -450,16 +478,16 @@ async function startReviewLoop(slug, opts = {}) {
450
478
  } else {
451
479
  try {
452
480
  reviewer = selectAgentFn('review', { exclude: new Set([implementer]) });
453
- } catch (err) {
454
- selectErr = err;
455
- reviewer = null;
481
+ } catch (/** @type {unknown} */ err) {
482
+ selectErr = /** @type {Error} */ (err);
483
+ reviewer = undefined;
456
484
  }
457
485
  reviewerSource = 'auto-derived';
458
486
  }
459
487
  }
460
488
 
461
489
  if (!reviewer) {
462
- const anyDifferentFamilyRunnable = agents.some(a => a !== implementer && workflowLauncherStatusFn(a).supported);
490
+ const anyDifferentFamilyRunnable = agents.some((/** @type {string} */ a) => a !== implementer && workflowLauncherStatusFn(a).supported);
463
491
  const implementerRunnable = agents.includes(implementer) && workflowLauncherStatusFn(implementer).supported;
464
492
  if (!anyDifferentFamilyRunnable && implementerRunnable) {
465
493
  log(fmt.status('WARN', `No supported different-family reviewer found for implementer "${implementer}".`));
@@ -468,7 +496,7 @@ async function startReviewLoop(slug, opts = {}) {
468
496
  reviewerSource = 'single-family-fallback';
469
497
  } else if (!anyDifferentFamilyRunnable) {
470
498
  if (!forgejoEnabled) {
471
- const detail = selectErr ? `: ${selectErr.message}` : '';
499
+ const detail = selectErr ? `: ${(/** @type {Error} */ (selectErr)).message}` : '';
472
500
  reviewer = 'autonomous';
473
501
  reviewerSource = 'fallback';
474
502
  log(fmt.status('WARN', `No reviewer could be auto-derived${detail}; defaulting to "autonomous"`));
@@ -479,7 +507,7 @@ async function startReviewLoop(slug, opts = {}) {
479
507
  const reason = selectErr ? `: ${selectErr.message}` : '';
480
508
  error(fmt.status('FAIL', `No reviewer could be auto-derived${reason}.`));
481
509
  error('\n' + fmt.status('INFO', 'Full runtime matrix:'));
482
- formatMatrixSummaryFn(buildAutonomousReviewMatrixFn()).forEach(line => error(` ${line}`));
510
+ formatMatrixSummaryFn(buildAutonomousReviewMatrixFn()).forEach((/** @type {string} */ line) => error(` ${line}`));
483
511
  error('\n' + fmt.status('FAIL', `No runnable reviewer route for implementer "${implementer}".`));
484
512
  exit(1);
485
513
  return;
@@ -512,7 +540,7 @@ async function startReviewLoop(slug, opts = {}) {
512
540
  error(` Looked for: ${reviewerStatus.detail}`);
513
541
  }
514
542
  error('\n' + fmt.status('INFO', 'Full runtime matrix:'));
515
- formatMatrixSummaryFn(buildAutonomousReviewMatrixFn()).forEach(line => error(` ${line}`));
543
+ formatMatrixSummaryFn(buildAutonomousReviewMatrixFn()).forEach((/** @type {string} */ line) => error(` ${line}`));
516
544
  error('\n' + fmt.status('FAIL', `No runnable reviewer route for implementer "${implementer}".`));
517
545
  exit(1);
518
546
  return;
@@ -521,7 +549,7 @@ async function startReviewLoop(slug, opts = {}) {
521
549
  if (!agents.includes(reviewer)) {
522
550
  error(fmt.status('FAIL', `Unsupported reviewer: "${reviewer}" (blocked or unsupported).`));
523
551
  error('\n' + fmt.status('INFO', 'Full runtime matrix:'));
524
- formatMatrixSummaryFn(buildAutonomousReviewMatrixFn()).forEach(line => error(` ${line}`));
552
+ formatMatrixSummaryFn(buildAutonomousReviewMatrixFn()).forEach((/** @type {string} */ line) => error(` ${line}`));
525
553
  error('\n' + fmt.status('FAIL', `No runnable reviewer route for implementer "${implementer}".`));
526
554
  exit(1);
527
555
  return;
@@ -534,9 +562,9 @@ async function startReviewLoop(slug, opts = {}) {
534
562
  if (reviewerSource === 'explicit') {
535
563
  const reason = !agents.includes(reviewer) ? 'blocked or unsupported' : 'launcher is not available';
536
564
  error(fmt.status('FAIL', `Unsupported reviewer: "${reviewer}" (${reason}).`));
537
- if (reviewerStatus.detail) error(` Looked for: ${reviewerStatus.detail}`);
565
+ if (reviewerStatus.detail) {error(` Looked for: ${reviewerStatus.detail}`);}
538
566
  error('\n' + fmt.status('INFO', 'Full runtime matrix:'));
539
- formatMatrixSummaryFn(buildAutonomousReviewMatrixFn()).forEach(line => error(` ${line}`));
567
+ formatMatrixSummaryFn(buildAutonomousReviewMatrixFn()).forEach((/** @type {string} */ line) => error(` ${line}`));
540
568
  error('\n' + fmt.status('FAIL', `No runnable reviewer route for implementer "${implementer}".`));
541
569
  exit(1);
542
570
  return;
@@ -553,13 +581,13 @@ async function startReviewLoop(slug, opts = {}) {
553
581
  } else {
554
582
  fallbackStatus = workflowLauncherStatusFn(fallback);
555
583
  }
556
- } catch (err) {
584
+ } catch (/** @type {unknown} */ err) {
557
585
  fallback = null;
558
586
  fallbackStatus = null;
559
587
  }
560
588
 
561
589
  if (!fallback) {
562
- const anyDifferentFamilyRunnable = agents.some(a => a !== implementer && workflowLauncherStatusFn(a).supported);
590
+ const anyDifferentFamilyRunnable = agents.some((/** @type {string} */ a) => a !== implementer && workflowLauncherStatusFn(a).supported);
563
591
  const implementerRunnable = agents.includes(implementer) && workflowLauncherStatusFn(implementer).supported;
564
592
  if (!anyDifferentFamilyRunnable && implementerRunnable) {
565
593
  log(fmt.status('WARN', `No supported different-family reviewer found for implementer "${implementer}".`));
@@ -576,9 +604,9 @@ async function startReviewLoop(slug, opts = {}) {
576
604
  }
577
605
  const reason = !agents.includes(reviewer) ? 'blocked or unsupported' : 'launcher is not available';
578
606
  error(fmt.status('FAIL', `Unsupported reviewer: "${reviewer}" (${reason}) and no unblocked different-family fallback is available.`));
579
- if (reviewerStatus.detail) error(` Looked for: ${reviewerStatus.detail}`);
607
+ if (reviewerStatus.detail) {error(` Looked for: ${reviewerStatus.detail}`);}
580
608
  error('\n' + fmt.status('INFO', 'Full runtime matrix:'));
581
- formatMatrixSummaryFn(buildAutonomousReviewMatrixFn()).forEach(line => error(` ${line}`));
609
+ formatMatrixSummaryFn(buildAutonomousReviewMatrixFn()).forEach((/** @type {string} */ line) => error(` ${line}`));
582
610
  error('\n' + fmt.status('FAIL', `No runnable reviewer route for implementer "${implementer}".`));
583
611
  exit(1);
584
612
  return;
@@ -641,7 +669,7 @@ async function startReviewLoop(slug, opts = {}) {
641
669
  if (state.phase === 'reviewing') {
642
670
  // Check if we can skip reviewer launch
643
671
  if (isContinue && attempt === initialRound) {
644
- if (!dryRun) transitionTaskFn(slug, 'review', { rootDir: worktree, log });
672
+ if (!dryRun) {transitionTaskFn(slug, 'review', { rootDir: worktree, log });}
645
673
  if (forgejoEnabled) {
646
674
  log(fmt.status('INFO', `Round ${attempt}: checking for existing review by ${reviewer} since ${state.startedAt}...`));
647
675
  reviewState = await pollForReviewFn(prNumber, reviewer, state.startedAt, token, {
@@ -679,7 +707,7 @@ async function startReviewLoop(slug, opts = {}) {
679
707
  });
680
708
  if (!rebaseResult.ok) { exit(1); return; }
681
709
 
682
- if (!dryRun) transitionTaskFn(slug, 'review', { rootDir: worktree, log });
710
+ if (!dryRun) {transitionTaskFn(slug, 'review', { rootDir: worktree, log });}
683
711
  state.phase = 'reviewing';
684
712
  writeReviewStateFn(slug, state, worktree);
685
713
  if (reviewer === 'autonomous' && !forgejoEnabled) {
@@ -691,11 +719,11 @@ async function startReviewLoop(slug, opts = {}) {
691
719
  try {
692
720
  reviewerLaunchResult = await startAgentFn('review', {
693
721
  agent: reviewer,
694
- prompt: (actualReviewer) => buildCompactReviewPromptFn({ reviewer, branch, implementer, focus, attempt, repoRoot: worktree, missionPath: effectiveMissionPath, actualReviewer }),
722
+ prompt: (/** @type {string} */ actualReviewer) => buildCompactReviewPromptFn({ reviewer, branch, implementer, focus, attempt, repoRoot: worktree, missionPath: effectiveMissionPath, actualReviewer }),
695
723
  worktree, slug, role: 'reviewer', exclude: [implementer]
696
724
  });
697
- } catch (err) {
698
- error(fmt.status('FAIL', `Could not launch reviewer agent (${reviewer}): ${err.message}`));
725
+ } catch (/** @type {unknown} */ err) {
726
+ error(fmt.status('FAIL', `Could not launch reviewer agent (${reviewer}): ${/** @type {Error} */(err).message}`));
699
727
  exit(1); return;
700
728
  }
701
729
 
@@ -709,12 +737,12 @@ async function startReviewLoop(slug, opts = {}) {
709
737
  // The window is bounded to this round's launch so each round adds
710
738
  // only its own usage; accumulateStageStats sums the rounds into one
711
739
  // cumulative row per reviewer family.
712
- const reviewSinceMs = stageLaunchSinceMs(reviewerLaunchResult && reviewerLaunchResult.result);
740
+ const reviewSinceMs = stageLaunchSinceMs((/** @type {{[key: string]: any}} */ (reviewerLaunchResult))?.result);
713
741
  recordStageStatsSafeFn('review', {
714
- stage: 'review', slug, rootDir: worktree, worktree, reviewer, implementer,
715
- result: reviewerLaunchResult && reviewerLaunchResult.result,
716
- sinceMs: reviewSinceMs, log, error, state, writeReviewStateFn,
717
- model: resolveAgentModel(reviewer, worktree),
742
+ stage: 'review', slug, rootDir: worktree, worktree, reviewer: /** @type {string} */ (reviewer), implementer,
743
+ result: (/** @type {{[key: string]: any}} */ (reviewerLaunchResult))?.result,
744
+ sinceMs: /** @type {number} */ (reviewSinceMs || 0), log, error, state, writeReviewStateFn,
745
+ model: resolveAgentModel(/** @type {string} */ (reviewer), /** @type {string} */ (worktree)),
718
746
  });
719
747
  }
720
748
 
@@ -744,22 +772,23 @@ async function startReviewLoop(slug, opts = {}) {
744
772
 
745
773
  if (isPollTimeout(reviewState)) {
746
774
  // Timeout recovery loop for reviewer
747
- while (state.reviewerRetryCount < 2) {
748
- state.reviewerRetryCount++;
775
+ const stateAny = /** @type {{[key: string]: any}} */ (state);
776
+ while ((stateAny['reviewerRetryCount'] || 0) < 2) {
777
+ stateAny['reviewerRetryCount'] = (stateAny['reviewerRetryCount'] || 0) + 1;
749
778
  writeReviewStateFn(slug, state, worktree);
750
- const elapsedStr = formatElapsed(Date.now() - new Date(state.startedAt));
779
+ const elapsedStr = formatElapsed(Date.now() - Date.parse(/** @type {string} */ (stateAny['startedAt'])));
751
780
  const recoveryPrompt = `RECOVERY: Reviewer timeout after ${elapsedStr}. Please complete the review for ${branch}.`;
752
- log(fmt.status('INFO', `Round ${attempt}: relaunching reviewer (${reviewer}) with recovery prompt (retry ${state.reviewerRetryCount}/3)...`));
781
+ log(fmt.status('INFO', `Round ${attempt}: relaunching reviewer (${reviewer}) with recovery prompt (retry ${stateAny['reviewerRetryCount']}/3)...`));
753
782
 
754
783
  let relaunchResult;
755
784
  try {
756
785
  relaunchResult = await startAgentFn('review', {
757
786
  agent: reviewer,
758
- prompt: (actualReviewer) => buildCompactReviewPromptFn({ reviewer, branch, implementer, focus, attempt, repoRoot: worktree, missionPath: effectiveMissionPath, actualReviewer }) + '\n\n' + recoveryPrompt,
787
+ prompt: (/** @type {string} */ actualReviewer) => buildCompactReviewPromptFn({ reviewer, branch, implementer, focus, attempt, repoRoot: worktree, missionPath: effectiveMissionPath, actualReviewer }) + '\n\n' + recoveryPrompt,
759
788
  worktree, slug, role: 'reviewer', exclude: [implementer]
760
789
  });
761
- } catch (err) {
762
- error(fmt.status('FAIL', `Could not relaunch reviewer agent (${reviewer}): ${err.message}`));
790
+ } catch (/** @type {unknown} */ err) {
791
+ error(fmt.status('FAIL', `Could not relaunch reviewer agent (${reviewer}): ${/** @type {Error} */(err).message}`));
763
792
  exit(1); return;
764
793
  }
765
794
 
@@ -791,7 +820,7 @@ async function startReviewLoop(slug, opts = {}) {
791
820
  });
792
821
  }
793
822
 
794
- if (!isPollTimeout(reviewState)) break;
823
+ if (!isPollTimeout(reviewState)) {break;}
795
824
  }
796
825
 
797
826
  if (isPollTimeout(reviewState)) {
@@ -805,7 +834,7 @@ async function startReviewLoop(slug, opts = {}) {
805
834
  }
806
835
  }
807
836
 
808
- if (dryRun) return;
837
+ if (dryRun) {return;}
809
838
 
810
839
  if (!reviewState) {
811
840
  error(fmt.status('FAIL', `Reviewer ${reviewer} did not submit a formal review outcome for ${branch}.`));
@@ -910,11 +939,11 @@ async function startReviewLoop(slug, opts = {}) {
910
939
  try {
911
940
  implementerLaunchResult = await startAgentFn('act-on-review', {
912
941
  agent: implementer,
913
- prompt: (actualImplementer) => buildCompactActOnReviewPromptFn({ implementer, branch, attempt, reviewOutcome: reviewState, repoRoot: worktree, missionPath: effectiveMissionPath, actualImplementer }),
942
+ prompt: (/** @type {string} */ actualImplementer) => buildCompactActOnReviewPromptFn({ implementer, branch, attempt, reviewOutcome: reviewState, repoRoot: worktree, missionPath: effectiveMissionPath, actualImplementer }),
914
943
  worktree, slug, role: 'implementer', exclude: [reviewer]
915
944
  });
916
- } catch (err) {
917
- error(fmt.status('FAIL', `Could not launch implementer agent (${implementer}): ${err.message}`));
945
+ } catch (/** @type {unknown} */ err) {
946
+ error(fmt.status('FAIL', `Could not launch implementer agent (${implementer}): ${/** @type {Error} */(err).message}`));
918
947
  exit(1); return;
919
948
  }
920
949
 
@@ -927,12 +956,12 @@ async function startReviewLoop(slug, opts = {}) {
927
956
  // This is intentionally a separate stored phase from the initial
928
957
  // execute/active launch. The window is bounded to this round's launch
929
958
  // so each act-on-review round adds only its own usage.
930
- const followUpSinceMs = stageLaunchSinceMs(implementerLaunchResult && implementerLaunchResult.result);
959
+ const followUpSinceMs = stageLaunchSinceMs((/** @type {{[key: string]: any}} */ (implementerLaunchResult))?.result);
931
960
  recordStageStatsSafeFn('active', {
932
961
  stage: 'follow-up', slug, rootDir: worktree, worktree, implementer, reviewer,
933
- result: implementerLaunchResult && implementerLaunchResult.result,
934
- sinceMs: followUpSinceMs, log, error, state, writeReviewStateFn,
935
- model: resolveAgentModel(implementer, worktree),
962
+ result: (/** @type {{[key: string]: any}} */ (implementerLaunchResult))?.result,
963
+ sinceMs: /** @type {number} */ (followUpSinceMs || 0), log, error, state, writeReviewStateFn,
964
+ model: resolveAgentModel(/** @type {string} */ (implementer), /** @type {string} */ (worktree)),
936
965
  });
937
966
  }
938
967
 
@@ -960,22 +989,23 @@ async function startReviewLoop(slug, opts = {}) {
960
989
 
961
990
  if (isPollTimeout(disposition)) {
962
991
  // Timeout recovery loop for implementer
963
- while (state.implementerRetryCount < 2) {
964
- state.implementerRetryCount++;
992
+ const stateAny = /** @type {{[key: string]: any}} */ (state);
993
+ while ((stateAny['implementerRetryCount'] || 0) < 2) {
994
+ stateAny['implementerRetryCount'] = (stateAny['implementerRetryCount'] || 0) + 1;
965
995
  writeReviewStateFn(slug, state, worktree);
966
- const elapsedStr = formatElapsed(Date.now() - new Date(state.startedAt));
967
- const recoveryPrompt = `RECOVERY: Implementer disposition timeout after ${elapsedStr}. Please provide a disposition (PUSHBACK_ALL, BLOCKED, PARKED, or continue with fixes) for ${branch}.`;
968
- log(fmt.status('INFO', `Round ${attempt}: relaunching implementer (${implementer}) with recovery prompt (retry ${state.implementerRetryCount}/3)...`));
996
+ const elapsedStr = formatElapsed(Date.now() - Date.parse(/** @type {string} */ (stateAny['startedAt'])));
997
+ const recoveryPrompt = `RECOVERY: Implementer disposition timeout after ${elapsedStr}. Please provide a disposition (PUSHBACK_ALL, BLOCKED, PARKED, or continue with fixes) for ${branch}.`;
998
+ log(fmt.status('INFO', `Round ${attempt}: relaunching implementer (${implementer}) with recovery prompt (retry ${stateAny['implementerRetryCount']}/3)...`));
969
999
 
970
1000
  let relaunchResult;
971
1001
  try {
972
1002
  relaunchResult = await startAgentFn('act-on-review', {
973
1003
  agent: implementer,
974
- prompt: (actualImplementer) => buildCompactActOnReviewPromptFn({ implementer, branch, attempt, reviewOutcome: reviewState, repoRoot: worktree, missionPath: effectiveMissionPath, actualImplementer }) + '\n\n' + recoveryPrompt,
1004
+ prompt: (/** @type {string} */ actualImplementer) => buildCompactActOnReviewPromptFn({ implementer, branch, attempt, reviewOutcome: reviewState, repoRoot: worktree, missionPath: effectiveMissionPath, actualImplementer }) + '\n\n' + recoveryPrompt,
975
1005
  worktree, slug, role: 'implementer', exclude: [reviewer]
976
1006
  });
977
- } catch (err) {
978
- error(fmt.status('FAIL', `Could not relaunch implementer agent (${implementer}): ${err.message}`));
1007
+ } catch (/** @type {unknown} */ err) {
1008
+ error(fmt.status('FAIL', `Could not relaunch implementer agent (${implementer}): ${/** @type {Error} */(err).message}`));
979
1009
  exit(1); return;
980
1010
  }
981
1011
 
@@ -1006,7 +1036,7 @@ async function startReviewLoop(slug, opts = {}) {
1006
1036
  });
1007
1037
  }
1008
1038
 
1009
- if (!isPollTimeout(disposition)) break;
1039
+ if (!isPollTimeout(disposition)) {break;}
1010
1040
  }
1011
1041
 
1012
1042
  if (isPollTimeout(disposition)) {
@@ -13,6 +13,7 @@ const POLL_PROGRESS_EVERY_MS = 30_000;
13
13
 
14
14
  const POLL_TIMEOUT = Object.freeze({ __isPollTimeout: true });
15
15
 
16
+ /** @param {number} ms */
16
17
  function delay(ms) {
17
18
  return new Promise(resolve => setTimeout(resolve, ms));
18
19
  }
@@ -20,29 +21,40 @@ function delay(ms) {
20
21
  function resolvePollIntervalMs() {
21
22
  const raw = process.env.AUTONOMOUS_REVIEW_POLL_INTERVAL_MS;
22
23
  const parsed = raw ? parseInt(raw, 10) : NaN;
23
- if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_POLL_INTERVAL_MS;
24
+ if (!Number.isFinite(parsed) || parsed <= 0) {return DEFAULT_POLL_INTERVAL_MS;}
24
25
  return parsed;
25
26
  }
26
27
 
28
+ /** @param {number} [explicitSeconds] */
27
29
  function resolvePollTimeoutMs(explicitSeconds) {
28
- if (Number.isFinite(explicitSeconds) && explicitSeconds > 0) {
30
+ if (typeof explicitSeconds === 'number' && Number.isFinite(explicitSeconds) && explicitSeconds > 0) {
29
31
  return explicitSeconds * 1000;
30
32
  }
31
33
  const raw = process.env.AUTONOMOUS_REVIEW_POLL_TIMEOUT_MS;
32
34
  const parsed = raw ? parseInt(raw, 10) : NaN;
33
- if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_POLL_MAX_WAIT_MS;
35
+ if (!Number.isFinite(parsed) || parsed <= 0) {return DEFAULT_POLL_MAX_WAIT_MS;}
34
36
  return parsed;
35
37
  }
36
38
 
39
+ /** @param {number} startMs */
37
40
  function formatElapsed(startMs) {
38
41
  const secs = Math.round((Date.now() - startMs) / 1000);
39
42
  return `${secs}s`;
40
43
  }
41
44
 
45
+ /** @param {*} result */
42
46
  function isPollTimeout(result) {
43
47
  return result === POLL_TIMEOUT;
44
48
  }
45
49
 
50
+ /**
51
+ * @param {number} prNumber
52
+ * @param {string} reviewerUser
53
+ * @param {string} sinceIso
54
+ * @param {string} token
55
+ * @param {{getLatestReviewForPrFn?: Function, sleepFn?: Function, intervalMs?: number, timeoutMs?: number, verbose?: boolean, label?: string, retryCount?: number, log?: Function}} [options]
56
+ * @returns {Promise<string|typeof POLL_TIMEOUT|null>}
57
+ */
46
58
  async function pollForReview(prNumber, reviewerUser, sinceIso, token, options = {}) {
47
59
  const {
48
60
  getLatestReviewForPrFn = getLatestReviewForPr,
@@ -67,7 +79,7 @@ async function pollForReview(prNumber, reviewerUser, sinceIso, token, options =
67
79
  while (Date.now() < deadline) {
68
80
  const review = await getLatestReviewForPrFn(prNumber, reviewerUser, sinceIso, token);
69
81
  if (review) {
70
- if (verbose) log(fmt.status('INFO', `${label}: reviewer state=${review.state} after ${formatElapsed(start)}.`));
82
+ if (verbose) {log(fmt.status('INFO', `${label}: reviewer state=${review.state} after ${formatElapsed(start)}.`));}
71
83
  return review.state;
72
84
  }
73
85
 
@@ -86,6 +98,14 @@ async function pollForReview(prNumber, reviewerUser, sinceIso, token, options =
86
98
  return POLL_TIMEOUT;
87
99
  }
88
100
 
101
+ /**
102
+ * @param {number} prNumber
103
+ * @param {string} implementerUser
104
+ * @param {string} sinceIso
105
+ * @param {string} token
106
+ * @param {{getLatestDispositionForPrFn?: Function, sleepFn?: Function, intervalMs?: number, timeoutMs?: number, verbose?: boolean, label?: string, retryCount?: number, log?: Function}} [options]
107
+ * @returns {Promise<string|typeof POLL_TIMEOUT|null>}
108
+ */
89
109
  async function pollForDisposition(prNumber, implementerUser, sinceIso, token, options = {}) {
90
110
  const {
91
111
  getLatestDispositionForPrFn = getLatestDispositionForPr,
@@ -110,7 +130,7 @@ async function pollForDisposition(prNumber, implementerUser, sinceIso, token, op
110
130
  while (Date.now() < deadline) {
111
131
  const disposition = await getLatestDispositionForPrFn(prNumber, implementerUser, sinceIso, token);
112
132
  if (disposition) {
113
- if (verbose) log(fmt.status('INFO', `${label}: disposition=${disposition} after ${formatElapsed(start)}.`));
133
+ if (verbose) {log(fmt.status('INFO', `${label}: disposition=${disposition} after ${formatElapsed(start)}.`));}
114
134
  return disposition;
115
135
  }
116
136