@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
@@ -37,6 +37,7 @@ function isForgejoProvider(rootDir = process.cwd()) {
37
37
  return isEnabled(rootDir);
38
38
  }
39
39
 
40
+ /** @param {string} [rootDir] */
40
41
  function resolveArtifactDir(rootDir = process.cwd()) {
41
42
  const review = loadAdapterConfig(rootDir).review || {};
42
43
  const configured = typeof review.tmpDir === 'string' && review.tmpDir.trim()
@@ -48,65 +49,151 @@ function resolveArtifactDir(rootDir = process.cwd()) {
48
49
  return path.isAbsolute(configured) ? configured : path.resolve(rootDir, configured);
49
50
  }
50
51
 
52
+ /**
53
+ * @param {string} rootDir
54
+ * @param {Function} liveFn
55
+ * @param {*} noopValue
56
+ * @returns {*}
57
+ */
51
58
  function withForgejo(rootDir, liveFn, noopValue) {
52
59
  return isEnabled(rootDir) ? liveFn() : noopValue;
53
60
  }
54
61
 
62
+ /**
63
+ * @param {string} branch
64
+ * @param {string} [rootDir]
65
+ * @param {{[key: string]: any}} [options]
66
+ * @returns {*}
67
+ */
55
68
  function getPrStatus(branch, rootDir = process.cwd(), options = {}) {
56
69
  return withForgejo(rootDir, () => forgejo.getPrStatus(branch, rootDir, options), NOOP_PR_STATUS);
57
70
  }
58
71
 
72
+ /**
73
+ * @param {string} user
74
+ * @param {{rootDir?: string}} [options]
75
+ * @returns {string|null}
76
+ */
59
77
  function readToken(user, options = {}) {
60
78
  const rootDir = options.rootDir || process.cwd();
61
79
  return isEnabled(rootDir) ? forgejo.readToken(user) : null;
62
80
  }
63
81
 
82
+ /**
83
+ * @param {number} prNumber
84
+ * @param {string} reviewerUser
85
+ * @param {string} sinceIso
86
+ * @param {string} token
87
+ * @param {{rootDir?: string, [key: string]: any}} [options]
88
+ * @returns {Promise<*|null>}
89
+ */
64
90
  async function getLatestReviewForPr(prNumber, reviewerUser, sinceIso, token, options = {}) {
65
- if (!token) return null;
91
+ if (!token) {return null;}
66
92
  return withForgejo(options.rootDir || process.cwd(), () => forgejo.getLatestReviewForPr(prNumber, reviewerUser, sinceIso, token, options), null);
67
93
  }
68
94
 
95
+ /**
96
+ * @param {number} prNumber
97
+ * @param {string} implementerUser
98
+ * @param {string} sinceIso
99
+ * @param {string} token
100
+ * @param {{rootDir?: string, [key: string]: any}} [options]
101
+ * @returns {Promise<*|null>}
102
+ */
69
103
  async function getLatestDispositionForPr(prNumber, implementerUser, sinceIso, token, options = {}) {
70
- if (!token) return null;
104
+ if (!token) {return null;}
71
105
  return withForgejo(options.rootDir || process.cwd(), () => forgejo.getLatestDispositionForPr(prNumber, implementerUser, sinceIso, token, options), null);
72
106
  }
73
107
 
108
+ /**
109
+ * @param {string} branch
110
+ * @param {string} token
111
+ * @param {string} body
112
+ * @param {{rootDir?: string, [key: string]: any}} [options]
113
+ * @returns {*}
114
+ */
74
115
  function postComment(branch, token, body, options = {}) {
75
116
  return withForgejo(options.rootDir || process.cwd(), () => forgejo.postComment(branch, token, body, options), { ok: true, skipped: true, reason: 'review-provider-disabled' });
76
117
  }
77
118
 
119
+ /**
120
+ * @param {string} branch
121
+ * @param {string} token
122
+ * @param {string} outcome
123
+ * @param {string} summary
124
+ * @param {{rootDir?: string, [key: string]: any}} [options]
125
+ * @returns {*}
126
+ */
78
127
  function postReview(branch, token, outcome, summary, options = {}) {
79
128
  return withForgejo(options.rootDir || process.cwd(), () => forgejo.postReview(branch, token, outcome, summary, options), { ok: true, skipped: true, reason: 'review-provider-disabled' });
80
129
  }
81
130
 
131
+ /**
132
+ * @param {string} branch
133
+ * @param {string} user
134
+ * @param {string} token
135
+ * @param {{rootDir?: string, [key: string]: any}} [options]
136
+ * @returns {*}
137
+ */
82
138
  function createPr(branch, user, token, options = {}) {
83
139
  return withForgejo(options.rootDir || process.cwd(), () => forgejo.createPr(branch, user, token, options), { ok: true, skipped: true, url: null });
84
140
  }
85
141
 
86
- function forgejoAvailable(url = process.env.FORGEJO_URL || 'http://localhost:3300', options = {}) {
87
- return withForgejo(options.rootDir || process.cwd(), () => forgejo.forgejoAvailable(url, options), false);
142
+ /**
143
+ * @param {string} [url]
144
+ * @param {{rootDir?: string, [key: string]: any}} [options]
145
+ * @returns {*}
146
+ */
147
+ function forgejoAvailable(url = process.env.FORGEJO_URL || 'http://localhost:3300', /** @type {{rootDir?: string, [key: string]: any}} */ options = {}) {
148
+ return withForgejo(options.rootDir || process.cwd(), () => forgejo.forgejoAvailable(url, /** @type {{request?: Function, timeout?: number}} */ (options)), false);
88
149
  }
89
150
 
151
+ /**
152
+ * @param {string} [url]
153
+ * @param {{rootDir?: string, [key: string]: any}} [options]
154
+ * @returns {*}
155
+ */
90
156
  function providerAvailable(url = process.env.FORGEJO_URL || 'http://localhost:3300', options = {}) {
91
157
  return forgejoAvailable(url, options);
92
158
  }
93
159
 
160
+ /**
161
+ * @param {string} branch
162
+ * @param {string} token
163
+ * @param {{rootDir?: string, [key: string]: any}} [options]
164
+ * @returns {Promise<*>}
165
+ */
94
166
  async function getComments(branch, token, options = {}) {
95
167
  return withForgejo(options.rootDir || process.cwd(), () => forgejo.getComments(branch, token, options), []);
96
168
  }
97
169
 
98
- function closePr(branch, token, user, options = {}) {
99
- return withForgejo(options.rootDir || process.cwd(), () => forgejo.closePr(branch, token, user, options), { ok: true, skipped: true, reason: 'review-provider-disabled' });
170
+ /**
171
+ * @param {string} branch
172
+ * @param {string} token
173
+ * @param {string} user
174
+ * @param {{rootDir?: string, [key: string]: any}} [options]
175
+ * @returns {*}
176
+ */
177
+ function closePr(branch, token, user, /** @type {{rootDir?: string, [key: string]: any}} */ options = {}) {
178
+ return withForgejo(options.rootDir || process.cwd(), () => (/** @type {any} */ (forgejo).closePr)(branch, token, user, options), { ok: true, skipped: true, reason: 'review-provider-disabled' });
100
179
  }
101
180
 
181
+ /** @param {string} explicitUser */
102
182
  function resolveForgejoUser(explicitUser) {
103
183
  return explicitUser || process.env.FORGEJO_USER || null;
104
184
  }
105
185
 
186
+ /** @param {string} explicitUser */
106
187
  function resolveReviewUser(explicitUser) {
107
188
  return resolveForgejoUser(explicitUser);
108
189
  }
109
190
 
191
+ /**
192
+ * @param {string} branch
193
+ * @param {string} token
194
+ * @param {{rootDir?: string, [key: string]: any}} [options]
195
+ * @returns {*|null}
196
+ */
110
197
  function getPrAuthor(branch, token, options = {}) {
111
198
  return withForgejo(options.rootDir || process.cwd(), () => forgejo.getPrAuthor(branch, token, options), null);
112
199
  }
@@ -10,16 +10,21 @@ const path = require('path');
10
10
  const fmt = require('../core/fmt');
11
11
  const { missionBranchName, resolveWorktree } = require('../core/mission-utils');
12
12
  const { readReviewState, writeReviewState, reviewStateFile, ReviewState, resolveReviewIdentity } = require('./review-state');
13
- const { readToken, postComment, postReview, getComments, getPrAuthor, isEnabled, resolveArtifactDir: resolveConfiguredArtifactDir } = require('./review-adapter');
13
+ const { readToken, postComment, postReview, getPrAuthor, isEnabled, resolveArtifactDir: resolveConfiguredArtifactDir } = require('./review-adapter');
14
14
  const { createEvent, consumeHumanNotes, VALID_EVENT_TYPES } = require('./review-events');
15
15
 
16
16
  // ============================================================================
17
17
  // Metadata Footer
18
18
  // ============================================================================
19
19
 
20
+ /**
21
+ * @param {string} slug
22
+ * @param {string} [rootDir]
23
+ * @returns {string}
24
+ */
20
25
  function buildMetadataFooter(slug, rootDir) {
21
26
  const state = readReviewState(slug, rootDir);
22
- if (!state) return '';
27
+ if (!state) {return '';}
23
28
  return `\n\n---\n\`[workflow-round:${state.round}, workflow-phase:${state.phase}]\``;
24
29
  }
25
30
 
@@ -27,6 +32,12 @@ function buildMetadataFooter(slug, rootDir) {
27
32
  // Artifact Path Utilities
28
33
  // ============================================================================
29
34
 
35
+ /**
36
+ * @param {string} slug
37
+ * @param {string} artifactName
38
+ * @param {string} [tmpDir]
39
+ * @returns {string}
40
+ */
30
41
  function reviewArtifactPath(slug, artifactName, tmpDir = os.tmpdir()) {
31
42
  return path.join(tmpDir, `${slug}-${artifactName}`);
32
43
  }
@@ -44,6 +55,12 @@ function reviewArtifactPath(slug, artifactName, tmpDir = os.tmpdir()) {
44
55
  // the loop passes the resolved default dir explicitly. This is defense-in-depth:
45
56
  // once the prompts substitute {{artifactDir}}, producer and consumer share one
46
57
  // dir by construction and this fallback is rarely exercised.
58
+ /**
59
+ * @param {string} slug
60
+ * @param {string} artifactName
61
+ * @param {{tmpDir: string, explicitTmpDir?: boolean, fallbackToTmp?: boolean, readArtifactFn: Function}} opts
62
+ * @returns {{path: string, value: string|null}}
63
+ */
47
64
  function resolveArtifactRead(slug, artifactName, { tmpDir, explicitTmpDir, fallbackToTmp, readArtifactFn }) {
48
65
  const primaryPath = reviewArtifactPath(slug, artifactName, tmpDir);
49
66
  const primaryValue = readArtifactFn(primaryPath);
@@ -77,6 +94,11 @@ function resolveArtifactDir(rootDir = process.cwd()) {
77
94
  return resolveConfiguredArtifactDir(rootDir);
78
95
  }
79
96
 
97
+ /**
98
+ * @param {string} filePath
99
+ * @param {Function} [readFileSync]
100
+ * @returns {string|null}
101
+ */
80
102
  function readArtifactFile(filePath, readFileSync = fs.readFileSync) {
81
103
  try {
82
104
  const value = readFileSync(filePath, 'utf8');
@@ -86,6 +108,11 @@ function readArtifactFile(filePath, readFileSync = fs.readFileSync) {
86
108
  }
87
109
  }
88
110
 
111
+ /**
112
+ * @param {string} filePath
113
+ * @param {Function} [unlinkSync]
114
+ * @returns {void}
115
+ */
89
116
  function deleteArtifactFile(filePath, unlinkSync = fs.unlinkSync) {
90
117
  try {
91
118
  unlinkSync(filePath);
@@ -98,11 +125,18 @@ function deleteArtifactFile(filePath, unlinkSync = fs.unlinkSync) {
98
125
  // Normalization Utilities
99
126
  // ============================================================================
100
127
 
128
+ /**
129
+ * @param {string} value
130
+ * @returns {string|null}
131
+ */
101
132
  function normalizeReviewVerdict(value) {
102
133
  const normalized = String(value || '').trim().toLowerCase();
103
134
  return ['approve', 'request-changes', 'comment'].includes(normalized) ? normalized : null;
104
135
  }
105
136
 
137
+ /** @param {string} value
138
+ * @returns {string|null}
139
+ */
106
140
  function normalizeDisposition(value) {
107
141
  const normalized = String(value || '').trim().toUpperCase();
108
142
  return ['CHANGES_MADE', 'PUSHBACK_ALL', 'PARKED', 'BLOCKED'].includes(normalized) ? normalized : null;
@@ -112,6 +146,12 @@ function normalizeDisposition(value) {
112
146
  // Workflow Comment/Review Posting
113
147
  // ============================================================================
114
148
 
149
+ /**
150
+ * @param {string} slug
151
+ * @param {string} message
152
+ * @param {{log?: Function, error?: Function, readTokenFn?: Function, postCommentFn?: Function, buildMetadataFooterFn?: Function, readReviewStateFn?: Function, rootDir?: string, reviewIdentity?: string, forgejoUser?: string}} [options]
153
+ * @returns {{ok: boolean, error?: string}}
154
+ */
115
155
  function postWorkflowComment(slug, message, options = {}) {
116
156
  const log = options.log || fmt.log.plain;
117
157
  const error = options.error || fmt.log.plainError;
@@ -147,12 +187,13 @@ function postWorkflowComment(slug, message, options = {}) {
147
187
  // the real HTTP status and response body (e.g. `422 approve your own pull is not
148
188
  // allowed`) instead of masking it behind a generic "API error". Falls back to
149
189
  // the result's own error/raw fields, then "API error" when nothing is present.
190
+ /** @param {*} result */
150
191
  function describeProviderFailure(result) {
151
- if (!result || typeof result !== 'object') return 'API error';
192
+ if (!result || typeof result !== 'object') {return 'API error';}
152
193
  // Prefer the HTTP status code (statusCode) over curl's process status.
153
- const httpStatus = (result.statusCode != null && Number(result.statusCode) > 0)
194
+ const httpStatus = (result.statusCode !== null && Number(result.statusCode) > 0)
154
195
  ? result.statusCode
155
- : (result.status != null && Number(result.status) >= 100 ? result.status : null);
196
+ : (result.status !== null && Number(result.status) >= 100 ? result.status : null);
156
197
  let body = null;
157
198
  if (result.data && typeof result.data === 'object') {
158
199
  body = result.data.message || JSON.stringify(result.data);
@@ -160,15 +201,21 @@ function describeProviderFailure(result) {
160
201
  body = result.data;
161
202
  }
162
203
  const parts = [];
163
- if (httpStatus != null) parts.push(String(httpStatus));
164
- if (body) parts.push(body);
165
- if (parts.length) return parts.join(' ');
204
+ if (httpStatus !== null) {parts.push(String(httpStatus));}
205
+ if (body) {parts.push(body);}
206
+ if (parts.length) {return parts.join(' ');}
166
207
  return result.error || result.raw || 'API error';
167
208
  }
168
209
 
169
210
  // Record a review verdict in local review-state.json / review-events without
170
211
  // posting to the review provider. Used by the self-author skip path so the verdict is still
171
212
  // captured even though the formal provider approval is intentionally not posted.
213
+ /**
214
+ * @param {string} slug
215
+ * @param {string} outcome
216
+ * @param {{worktree?: string, writeReviewStateFn?: Function, createEventFn?: Function, readReviewStateFn?: Function, log?: Function, error?: Function, reviewer?: string}} [options]
217
+ * @returns {void}
218
+ */
172
219
  function recordLocalReviewVerdict(slug, outcome, options = {}) {
173
220
  const worktree = options.worktree || resolveWorktree(slug) || process.cwd();
174
221
  const writeReviewStateFn = options.writeReviewStateFn || writeReviewState;
@@ -199,6 +246,13 @@ function recordLocalReviewVerdict(slug, outcome, options = {}) {
199
246
  createEventFn(slug, VALID_EVENT_TYPES.REVIEWER_OUTCOME, { verdict: outcome }, { worktree, log, error });
200
247
  }
201
248
 
249
+ /**
250
+ * @param {string} slug
251
+ * @param {string} outcome
252
+ * @param {string} message
253
+ * @param {{log?: Function, error?: Function, readTokenFn?: Function, postReviewFn?: Function, buildMetadataFooterFn?: Function, readReviewStateFn?: Function, worktree?: string, reviewIdentity?: string, forgejoUser?: string, getPrAuthorFn?: Function, writeReviewStateFn?: Function, createEventFn?: Function}} [options]
254
+ * @returns {{ok: boolean, error?: string, skipped?: boolean, reason?: string, prAuthor?: string}}
255
+ */
202
256
  function postWorkflowReview(slug, outcome, message, options = {}) {
203
257
  const log = options.log || fmt.log.plain;
204
258
  const error = options.error || fmt.log.plainError;
@@ -262,18 +316,24 @@ function postWorkflowReview(slug, outcome, message, options = {}) {
262
316
  // Artifact Consumption
263
317
  // ============================================================================
264
318
 
319
+ /**
320
+ * @param {string} slug
321
+ * @param {string} reviewer
322
+ * @param {{log?: Function, error?: Function, readArtifactFn?: Function, deleteArtifactFn?: Function, tmpDir?: string|null, fallbackToTmp?: boolean, worktree?: string, providerEnabled?: boolean|null, forgejoEnabled?: boolean|null, readTokenFn?: Function, getCommentsFn?: Function, postCommentFn?: Function, postReviewFn?: Function, buildMetadataFooterFn?: Function, createEventFn?: Function}} [options]
323
+ * @returns {Promise<{consumed: boolean, ok?: boolean, reviewState?: string|null}>}
324
+ */
265
325
  async function consumeReviewerArtifacts(slug, reviewer, options = {}) {
266
326
  const log = options.log || fmt.log.plain;
267
327
  const error = options.error || fmt.log.plainError;
268
328
  const readArtifactFn = options.readArtifactFn || readArtifactFile;
269
329
  const deleteArtifactFn = options.deleteArtifactFn || deleteArtifactFile;
270
- const explicitTmpDir = options.tmpDir != null;
330
+ const explicitTmpDir = options.tmpDir !== null && options.tmpDir !== undefined;
271
331
  const fallbackToTmp = options.fallbackToTmp === true;
272
332
  const worktree = options.worktree || resolveWorktree(slug) || process.cwd();
273
333
  const tmpDir = options.tmpDir || resolveArtifactDir(worktree);
274
- const providerEnabled = options.providerEnabled != null
334
+ const providerEnabled = options.providerEnabled !== null && options.providerEnabled !== undefined
275
335
  ? options.providerEnabled
276
- : (options.forgejoEnabled != null ? options.forgejoEnabled : isEnabled(worktree));
336
+ : (options.forgejoEnabled !== null && options.forgejoEnabled !== undefined ? options.forgejoEnabled : isEnabled(worktree));
277
337
  const reviewStatePath = reviewStateFile(slug, worktree);
278
338
 
279
339
  const findingsResolved = resolveArtifactRead(slug, 'review-findings.md', { tmpDir, explicitTmpDir, fallbackToTmp, readArtifactFn });
@@ -289,7 +349,7 @@ async function consumeReviewerArtifacts(slug, reviewer, options = {}) {
289
349
 
290
350
  // Extract verdict from outcome message if review-verdict.txt is not provided
291
351
  // This allows a single review-outcome.md file to contain both the outcome and verdict
292
- let verdict = normalizeReviewVerdict(verdictRaw);
352
+ let verdict = normalizeReviewVerdict(/** @type {string} */ (verdictRaw || ''));
293
353
  if (!verdict && outcomeMessage) {
294
354
  // Try to extract verdict from outcome content (e.g., "Verdict: approve" or frontmatter)
295
355
  const outcomeVerdictMatch = outcomeMessage.match(/^verdict:\s*(approve|request-changes|comment)/im) ||
@@ -418,8 +478,8 @@ async function consumeReviewerArtifacts(slug, reviewer, options = {}) {
418
478
  deleteArtifactFn(outcomePath);
419
479
  deleteArtifactFn(verdictPath);
420
480
 
421
- if (verdict === 'approve') return { consumed: true, ok: true, reviewState: 'APPROVED' };
422
- if (verdict === 'request-changes') return { consumed: true, ok: true, reviewState: 'REQUEST_CHANGES' };
481
+ if (verdict === 'approve') {return { consumed: true, ok: true, reviewState: 'APPROVED' };}
482
+ if (verdict === 'request-changes') {return { consumed: true, ok: true, reviewState: 'REQUEST_CHANGES' };}
423
483
  // Defensive fallback: a stray `comment` verdict (prompts no longer offer it) cannot be
424
484
  // resolved via provider polling when the provider is disabled, so normalize it to the
425
485
  // disposition format and pass it through. inferPhaseFromDisposition() maps COMMENT -> fixing.
@@ -432,18 +492,24 @@ async function consumeReviewerArtifacts(slug, reviewer, options = {}) {
432
492
  return { consumed: true, ok: true, reviewState: null };
433
493
  }
434
494
 
495
+ /**
496
+ * @param {string} slug
497
+ * @param {string} implementer
498
+ * @param {{log?: Function, error?: Function, readArtifactFn?: Function, deleteArtifactFn?: Function, tmpDir?: string|null, fallbackToTmp?: boolean, worktree?: string, providerEnabled?: boolean|null, forgejoEnabled?: boolean|null, readTokenFn?: Function, getCommentsFn?: Function, postCommentFn?: Function, buildMetadataFooterFn?: Function, createEventFn?: Function}} [options]
499
+ * @returns {Promise<{consumed: boolean, ok?: boolean, disposition?: string|null}>}
500
+ */
435
501
  async function consumeImplementerArtifacts(slug, implementer, options = {}) {
436
502
  const log = options.log || fmt.log.plain;
437
503
  const error = options.error || fmt.log.plainError;
438
504
  const readArtifactFn = options.readArtifactFn || readArtifactFile;
439
505
  const deleteArtifactFn = options.deleteArtifactFn || deleteArtifactFile;
440
- const explicitTmpDir = options.tmpDir != null;
506
+ const explicitTmpDir = options.tmpDir !== null && options.tmpDir !== undefined;
441
507
  const fallbackToTmp = options.fallbackToTmp === true;
442
508
  const worktree = options.worktree || resolveWorktree(slug) || process.cwd();
443
509
  const tmpDir = options.tmpDir || resolveArtifactDir(worktree);
444
- const providerEnabled = options.providerEnabled != null
510
+ const providerEnabled = options.providerEnabled !== null && options.providerEnabled !== undefined
445
511
  ? options.providerEnabled
446
- : (options.forgejoEnabled != null ? options.forgejoEnabled : isEnabled(worktree));
512
+ : (options.forgejoEnabled !== null && options.forgejoEnabled !== undefined ? options.forgejoEnabled : isEnabled(worktree));
447
513
  const reviewStatePath = reviewStateFile(slug, worktree);
448
514
 
449
515
  const resolutionResolved = resolveArtifactRead(slug, 'round-resolution.md', { tmpDir, explicitTmpDir, fallbackToTmp, readArtifactFn });
@@ -453,7 +519,7 @@ async function consumeImplementerArtifacts(slug, implementer, options = {}) {
453
519
 
454
520
  const resolution = resolutionResolved.value;
455
521
  const dispositionRaw = dispositionResolved.value;
456
- const disposition = normalizeDisposition(dispositionRaw);
522
+ const disposition = normalizeDisposition(/** @type {string} */ (dispositionRaw || ''));
457
523
  const hasAny = resolution !== null || dispositionRaw !== null;
458
524
 
459
525
  if (!hasAny) {
@@ -492,10 +558,10 @@ async function consumeImplementerArtifacts(slug, implementer, options = {}) {
492
558
  const parkedMatch = resolution.match(/parked_items:\s*(\[[^\]]*\])/i);
493
559
  const blockedMatch = resolution.match(/blocked_reason:\s*"([^"]*)"/i);
494
560
 
495
- if (fixedMatch) fixedItems = JSON.parse(fixedMatch[1]);
496
- if (pushedMatch) pushedBackItems = JSON.parse(pushedMatch[1]);
497
- if (parkedMatch) parkedItems = JSON.parse(parkedMatch[1]);
498
- if (blockedMatch) blockedReason = blockedMatch[1];
561
+ if (fixedMatch) {fixedItems = JSON.parse(fixedMatch[1]);}
562
+ if (pushedMatch) {pushedBackItems = JSON.parse(pushedMatch[1]);}
563
+ if (parkedMatch) {parkedItems = JSON.parse(parkedMatch[1]);}
564
+ if (blockedMatch) {blockedReason = blockedMatch[1];}
499
565
  } catch (_) {
500
566
  // If parsing fails, use empty arrays
501
567
  fixedItems = [];
@@ -507,7 +573,7 @@ async function consumeImplementerArtifacts(slug, implementer, options = {}) {
507
573
  if (disposition === 'BLOCKED' && !blockedReason) {
508
574
  // Check if it's in the resolution content
509
575
  const match = resolution.match(/blocked.*?:\s*(.+)/i);
510
- if (match) blockedReason = match[1].trim();
576
+ if (match) {blockedReason = match[1].trim();}
511
577
  }
512
578
 
513
579
  const summaryEventResult = createEventFn(slug, VALID_EVENT_TYPES.IMPLEMENTER_ROUND_SUMMARY, {