@magnusekdahl/parallix 1.2.0 → 1.3.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 (127) hide show
  1. package/index.js +190 -185
  2. package/lib/agents/agents.js +737 -838
  3. package/lib/agents/agents.ts +930 -0
  4. package/lib/agents/claude-telemetry.js +149 -154
  5. package/lib/agents/claude-telemetry.ts +233 -0
  6. package/lib/agents/claude.js +121 -131
  7. package/lib/agents/claude.ts +156 -0
  8. package/lib/agents/codex-telemetry.js +176 -164
  9. package/lib/agents/codex-telemetry.ts +205 -0
  10. package/lib/agents/codex.js +168 -201
  11. package/lib/agents/codex.ts +236 -0
  12. package/lib/agents/limit-hit.js +216 -226
  13. package/lib/agents/limit-hit.ts +262 -0
  14. package/lib/agents/mistral-telemetry.js +7 -10
  15. package/lib/agents/mistral-telemetry.ts +44 -0
  16. package/lib/agents/mistral.js +40 -58
  17. package/lib/agents/mistral.ts +84 -0
  18. package/lib/agents/opencode-export.js +126 -127
  19. package/lib/agents/opencode-export.ts +160 -0
  20. package/lib/agents/opencode-telemetry.js +282 -261
  21. package/lib/agents/opencode-telemetry.ts +350 -0
  22. package/lib/agents/opencode.js +263 -305
  23. package/lib/agents/opencode.ts +370 -0
  24. package/lib/agents/stage-telemetry.js +23 -24
  25. package/lib/agents/stage-telemetry.ts +47 -0
  26. package/lib/commands/active.js +502 -579
  27. package/lib/commands/active.ts +665 -0
  28. package/lib/commands/checkpoint.js +98 -74
  29. package/lib/commands/checkpoint.ts +79 -0
  30. package/lib/commands/config.js +61 -31
  31. package/lib/commands/config.ts +47 -0
  32. package/lib/commands/coverage-gate.js +271 -246
  33. package/lib/commands/coverage-gate.ts +362 -0
  34. package/lib/commands/diff.js +140 -119
  35. package/lib/commands/diff.ts +125 -0
  36. package/lib/commands/draft.js +770 -923
  37. package/lib/commands/draft.ts +1021 -0
  38. package/lib/commands/handoff.js +588 -608
  39. package/lib/commands/handoff.ts +694 -0
  40. package/lib/commands/integrate.js +1332 -1463
  41. package/lib/commands/integrate.ts +1688 -0
  42. package/lib/commands/mission-start.js +279 -237
  43. package/lib/commands/mission-start.ts +263 -0
  44. package/lib/commands/rebase.js +606 -582
  45. package/lib/commands/rebase.ts +635 -0
  46. package/lib/commands/repair-handoff.js +181 -179
  47. package/lib/commands/repair-handoff.ts +228 -0
  48. package/lib/commands/resolve-conflict.js +121 -102
  49. package/lib/commands/resolve-conflict.ts +112 -0
  50. package/lib/commands/review.js +3 -4
  51. package/lib/commands/review.ts +14 -0
  52. package/lib/commands/setup-review.js +4 -5
  53. package/lib/commands/setup-review.ts +14 -0
  54. package/lib/commands/setup.js +3 -3
  55. package/lib/commands/setup.ts +2 -0
  56. package/lib/commands/stats-backfill.js +394 -359
  57. package/lib/commands/stats-backfill.ts +418 -0
  58. package/lib/commands/stats.js +1424 -1665
  59. package/lib/commands/stats.ts +2186 -0
  60. package/lib/commands/status.js +219 -200
  61. package/lib/commands/status.ts +219 -0
  62. package/lib/commands/verify.js +3 -1
  63. package/lib/commands/verify.ts +2 -0
  64. package/lib/core/fmt.js +140 -211
  65. package/lib/core/fmt.ts +189 -0
  66. package/lib/core/git.js +121 -102
  67. package/lib/core/git.ts +146 -0
  68. package/lib/core/gitignore.js +59 -106
  69. package/lib/core/gitignore.ts +103 -0
  70. package/lib/core/mission-utils.js +833 -875
  71. package/lib/core/mission-utils.ts +998 -0
  72. package/lib/core/nels.js +112 -127
  73. package/lib/core/nels.ts +189 -0
  74. package/lib/core/persistent-data-migration.js +247 -205
  75. package/lib/core/persistent-data-migration.ts +255 -0
  76. package/lib/core/product-config.js +400 -476
  77. package/lib/core/product-config.ts +518 -0
  78. package/lib/core/runtime-matrix.js +41 -87
  79. package/lib/core/runtime-matrix.ts +84 -0
  80. package/lib/core/spawn-tee.js +152 -192
  81. package/lib/core/spawn-tee.ts +202 -0
  82. package/lib/core/state-map.js +92 -92
  83. package/lib/core/state-map.ts +115 -0
  84. package/lib/core/storage.js +101 -129
  85. package/lib/core/storage.ts +182 -0
  86. package/lib/core/subagent-limit.js +15 -16
  87. package/lib/core/subagent-limit.ts +25 -0
  88. package/lib/core/verification.js +148 -138
  89. package/lib/core/verification.ts +200 -0
  90. package/lib/index.js +142 -59
  91. package/lib/index.ts +134 -0
  92. package/lib/review/rebase.js +142 -145
  93. package/lib/review/rebase.ts +188 -0
  94. package/lib/review/review-adapter.js +99 -87
  95. package/lib/review/review-adapter.ts +198 -0
  96. package/lib/review/review-artifacts.js +486 -629
  97. package/lib/review/review-artifacts.ts +622 -0
  98. package/lib/review/review-commands.js +1220 -1373
  99. package/lib/review/review-commands.ts +1579 -0
  100. package/lib/review/review-events.js +544 -886
  101. package/lib/review/review-events.ts +954 -0
  102. package/lib/review/review-loop.js +945 -1020
  103. package/lib/review/review-loop.ts +1174 -0
  104. package/lib/review/review-polling.js +86 -115
  105. package/lib/review/review-polling.ts +194 -0
  106. package/lib/review/review-prompts.js +154 -189
  107. package/lib/review/review-prompts.ts +182 -0
  108. package/lib/review/review-state.js +246 -225
  109. package/lib/review/review-state.ts +353 -0
  110. package/lib/review/review.js +63 -84
  111. package/lib/review/review.ts +138 -0
  112. package/lib/tools/backlog.js +694 -680
  113. package/lib/tools/backlog.ts +835 -0
  114. package/lib/tools/forgejo.js +1270 -1405
  115. package/lib/tools/forgejo.ts +1793 -0
  116. package/lib/tools/gatekeeper.js +106 -96
  117. package/lib/tools/gatekeeper.ts +124 -0
  118. package/lib/tools/redgreen.js +200 -181
  119. package/lib/tools/redgreen.ts +221 -0
  120. package/lib/tools/sessions.js +58 -52
  121. package/lib/tools/sessions.ts +81 -0
  122. package/lib/tools/setup-review.js +902 -975
  123. package/lib/tools/setup-review.ts +1152 -0
  124. package/package.json +7 -5
  125. package/prompts/draft.md +1 -0
  126. package/prompts/portfolio.md +1 -1
  127. package/px.js +226 -210
@@ -0,0 +1,622 @@
1
+ /**
2
+ * Review Artifacts Module - Utility Functions
3
+ * Extracted from parallix/lib/review.js for task-1201
4
+ * Handles artifact path resolution, file I/O, and normalization.
5
+ */
6
+
7
+ import * as fs from 'fs';
8
+ import * as os from 'os';
9
+ import * as path from 'path';
10
+ import * as fmt from '../core/fmt.js';
11
+ import { missionBranchName, resolveWorktree } from '../core/mission-utils.js';
12
+ import { readReviewState, writeReviewState, reviewStateFile, ReviewState, resolveReviewIdentity } from './review-state.js';
13
+ import { readToken, postComment, postReview, getPrAuthor, isEnabled, resolveArtifactDir as resolveConfiguredArtifactDir } from './review-adapter.js';
14
+ import { createEvent, consumeHumanNotes, VALID_EVENT_TYPES, CreateEventParams, CreateEventOptions, CreateEventResult } from './review-events.js';
15
+
16
+ type CreateResult = { ok: boolean; path: string | null; error?: string | null; event?: Record<string, unknown> };
17
+
18
+ // ============================================================================
19
+ // Metadata Footer
20
+ // ============================================================================
21
+
22
+ function buildMetadataFooter(slug: string, rootDir = process.cwd()): string {
23
+ const state = readReviewState(slug, rootDir);
24
+ if (!state) { return ''; }
25
+ return `\n\n---\n\`[workflow-round:${state.round}, workflow-phase:${state.phase}]\``;
26
+ }
27
+
28
+ // ============================================================================
29
+ // Artifact Path Utilities
30
+ // ============================================================================
31
+
32
+ function reviewArtifactPath(slug: string, artifactName: string, tmpDir = os.tmpdir()): string {
33
+ return path.join(tmpDir, `${slug}-${artifactName}`);
34
+ }
35
+
36
+ /**
37
+ * Resolve artifact read path with optional /tmp fallback.
38
+ */
39
+ function resolveArtifactRead(
40
+ slug: string,
41
+ artifactName: string,
42
+ opts: { tmpDir: string; explicitTmpDir?: boolean; fallbackToTmp?: boolean; readArtifactFn: typeof readArtifactFile }
43
+ ): { path: string; value: string | null } {
44
+ const { tmpDir, explicitTmpDir, fallbackToTmp, readArtifactFn } = opts;
45
+ const primaryPath = reviewArtifactPath(slug, artifactName, tmpDir);
46
+ const primaryValue = readArtifactFn(primaryPath);
47
+ const allowTmpFallback = (!explicitTmpDir || fallbackToTmp) && tmpDir !== '/tmp';
48
+ if (primaryValue !== null || !allowTmpFallback) {
49
+ return { path: primaryPath, value: primaryValue };
50
+ }
51
+ const fallbackPath = reviewArtifactPath(slug, artifactName, '/tmp');
52
+ const fallbackValue = readArtifactFn(fallbackPath);
53
+ if (fallbackValue !== null) {
54
+ return { path: fallbackPath, value: fallbackValue };
55
+ }
56
+ return { path: primaryPath, value: null };
57
+ }
58
+
59
+ /**
60
+ * Resolve the directory where review artifacts are written and consumed.
61
+ */
62
+ function resolveArtifactDir(rootDir = process.cwd()): string {
63
+ return resolveConfiguredArtifactDir(rootDir);
64
+ }
65
+
66
+ function readArtifactFile(filePath: string, readFileSync = fs.readFileSync): string | null {
67
+ try {
68
+ const value = readFileSync(filePath, 'utf8');
69
+ return typeof value === 'string' ? value.trim() : '';
70
+ } catch {
71
+ return null;
72
+ }
73
+ }
74
+
75
+ function deleteArtifactFile(filePath: string, unlinkSync = fs.unlinkSync): void {
76
+ try {
77
+ unlinkSync(filePath);
78
+ } catch {
79
+ // Best-effort cleanup only.
80
+ }
81
+ }
82
+
83
+ // ============================================================================
84
+ // Normalization Utilities
85
+ // ============================================================================
86
+
87
+ function normalizeReviewVerdict(value: string): string | null {
88
+ const normalized = String(value || '').trim().toLowerCase();
89
+ return ['approve', 'request-changes', 'comment'].includes(normalized) ? normalized : null;
90
+ }
91
+
92
+ function normalizeDisposition(value: string): string | null {
93
+ const normalized = String(value || '').trim().toUpperCase();
94
+ return ['CHANGES_MADE', 'PUSHBACK_ALL', 'PARKED', 'BLOCKED'].includes(normalized) ? normalized : null;
95
+ }
96
+
97
+ // ============================================================================
98
+ // Workflow Comment/Review Posting
99
+ // ============================================================================
100
+
101
+ function postWorkflowComment(
102
+ slug: string,
103
+ message: string,
104
+ options: {
105
+ log?: (msg: string) => void;
106
+ error?: (msg: string) => void;
107
+ readTokenFn?: (user: string, opts: { rootDir?: string }) => string | null;
108
+ postCommentFn?: (branch: string, token: string, body: string, opts?: Record<string, unknown>) => unknown;
109
+ buildMetadataFooterFn?: (s: string, r?: string) => string;
110
+ readReviewStateFn?: (s: string, r?: string) => any;
111
+ rootDir?: string;
112
+ reviewIdentity?: string;
113
+ forgejoUser?: string;
114
+ } = {}
115
+ ): { ok: boolean; error?: string } {
116
+ const log = options.log || fmt.log.plain;
117
+ const error = options.error || fmt.log.plainError;
118
+ const readTokenFn = options.readTokenFn || readToken;
119
+ const postCommentFn = options.postCommentFn || postComment;
120
+ const buildMetadataFooterFn = options.buildMetadataFooterFn || buildMetadataFooter;
121
+ const readReviewStateFn = options.readReviewStateFn || readReviewState;
122
+ const rootDir = options.rootDir || resolveWorktree(slug) || process.cwd();
123
+ const branch = missionBranchName(slug, rootDir);
124
+ const reviewIdentity = options.reviewIdentity
125
+ || options.forgejoUser
126
+ || resolveReviewIdentity(slug, rootDir, { readReviewStateFn }).commentIdentityUser
127
+ || 'human';
128
+ const token = readTokenFn(reviewIdentity, { rootDir });
129
+ if (!token) {
130
+ error(fmt.status('FAIL', `No Forgejo token found for user "${reviewIdentity}". Cannot post comment.`));
131
+ return { ok: false };
132
+ }
133
+
134
+ const taggedMessage = message + buildMetadataFooterFn(slug, rootDir);
135
+ log(fmt.status('INFO', `Posting PR comment on ${branch} as ${reviewIdentity}...`));
136
+ const result = postCommentFn(branch, token, taggedMessage, { forgejoUser: reviewIdentity, reviewIdentity });
137
+ const r = result as Record<string, unknown>;
138
+ if (!r.ok) {
139
+ error(fmt.status('FAIL', `Could not post comment: ${(r.error as string) || 'API error'}`));
140
+ return { ok: false, error: (r.error as string) || 'API error' };
141
+ }
142
+
143
+ log(fmt.status('PASS', `Comment posted on PR for ${branch}.`));
144
+ return { ok: true };
145
+ }
146
+
147
+ /** @param {*} result */
148
+ function describeProviderFailure(result: unknown): string {
149
+ if (!result || typeof result !== 'object') { return 'API error'; }
150
+ const r = result as Record<string, unknown>;
151
+ const httpStatus = (r.statusCode !== null && Number(r.statusCode) > 0)
152
+ ? r.statusCode
153
+ : (r.status !== null && Number(r.status) >= 100 ? r.status : null);
154
+ let body: string | null = null;
155
+ if (r.data && typeof r.data === 'object') {
156
+ body = (r.data as { message?: string }).message || JSON.stringify(r.data);
157
+ } else if (typeof r.data === 'string' && r.data) {
158
+ body = r.data as string;
159
+ }
160
+ const parts: string[] = [];
161
+ if (httpStatus !== null) { parts.push(String(httpStatus)); }
162
+ if (body) { parts.push(body); }
163
+ if (parts.length) { return parts.join(' '); }
164
+ return (r.error as string) || (r.raw as string) || 'API error';
165
+ }
166
+
167
+ /**
168
+ * Record a review verdict in local review-state.json / review-events.
169
+ */
170
+ function recordLocalReviewVerdict(
171
+ slug: string,
172
+ outcome: string,
173
+ options: {
174
+ worktree?: string;
175
+ writeReviewStateFn?: (s: string, st: any, w: string) => boolean;
176
+ createEventFn?: (s: string, t: string, p: Record<string, unknown>, o: Record<string, unknown>) => CreateResult;
177
+ readReviewStateFn?: (s: string, r?: string) => any;
178
+ log?: (msg: string) => void;
179
+ error?: (msg: string) => void;
180
+ reviewer?: string;
181
+ } = {}
182
+ ): void {
183
+ const worktree = options.worktree || resolveWorktree(slug) || process.cwd();
184
+ const writeReviewStateFn = options.writeReviewStateFn || writeReviewState;
185
+ const createEventFn = options.createEventFn || createEvent;
186
+ const readReviewStateFn = options.readReviewStateFn || readReviewState;
187
+ const log = options.log || fmt.log.plain;
188
+ const error = options.error || fmt.log.plainError;
189
+
190
+ const existing = readReviewStateFn(slug, worktree);
191
+ const state = existing instanceof ReviewState
192
+ ? existing
193
+ : new ReviewState(slug, existing || {
194
+ reviewer: options.reviewer,
195
+ implementer: options.reviewer,
196
+ round: 1,
197
+ phase: 'reviewing',
198
+ });
199
+ if (outcome === 'approve') {
200
+ state.disposition = 'APPROVED';
201
+ try { state.transitionTo('approved'); } catch { /* ignore */ }
202
+ } else if (outcome === 'request-changes') {
203
+ state.disposition = 'REQUEST_CHANGES';
204
+ try { state.transitionTo('fixing'); } catch { /* ignore */ }
205
+ }
206
+ writeReviewStateFn(slug, state, worktree);
207
+
208
+ createEventFn(slug, VALID_EVENT_TYPES.REVIEWER_OUTCOME, { verdict: outcome, content: `Review verdict: ${outcome}` }, { worktree, log: log, error });
209
+ }
210
+
211
+ /**
212
+ * Submit a review outcome to the provider.
213
+ */
214
+ function postWorkflowReview(
215
+ slug: string,
216
+ outcome: string,
217
+ message: string,
218
+ options: {
219
+ log?: (msg: string) => void;
220
+ error?: (msg: string) => void;
221
+ readTokenFn?: (user: string, opts: { rootDir?: string }) => string | null;
222
+ postReviewFn?: (branch: string, token: string, outcome: string, body: string, opts?: Record<string, unknown>) => unknown;
223
+ buildMetadataFooterFn?: (s: string, r?: string) => string;
224
+ readReviewStateFn?: (s: string, r?: string) => any;
225
+ worktree?: string;
226
+ reviewIdentity?: string;
227
+ forgejoUser?: string;
228
+ getPrAuthorFn?: (branch: string, token: string, opts?: Record<string, unknown>) => unknown;
229
+ writeReviewStateFn?: (s: string, st: any, w: string) => boolean;
230
+ createEventFn?: (s: string, t: string, p: Record<string, unknown>, o: Record<string, unknown>) => CreateResult;
231
+ } = {}
232
+ ): { ok: boolean; error?: string; skipped?: boolean; reason?: string; prAuthor?: unknown } {
233
+ const log = options.log || fmt.log.plain;
234
+ const error = options.error || fmt.log.plainError;
235
+ const readTokenFn = options.readTokenFn || readToken;
236
+ const postReviewFn = options.postReviewFn || postReview;
237
+ const buildMetadataFooterFn = options.buildMetadataFooterFn || buildMetadataFooter;
238
+ const readReviewStateFn = options.readReviewStateFn || readReviewState;
239
+ const worktree = options.worktree || resolveWorktree(slug) || process.cwd();
240
+ const branch = missionBranchName(slug, worktree);
241
+ const reviewIdentity = options.reviewIdentity
242
+ || options.forgejoUser
243
+ || resolveReviewIdentity(slug, worktree, { readReviewStateFn }).identityUser
244
+ || 'human';
245
+ const token = readTokenFn(reviewIdentity, { rootDir: worktree });
246
+ if (!token) {
247
+ error(fmt.status('FAIL', `No Forgejo token found for user "${reviewIdentity}". Cannot submit review.`));
248
+ return { ok: false };
249
+ }
250
+
251
+ const getPrAuthorFn = options.getPrAuthorFn || getPrAuthor;
252
+ let prAuthor: unknown = null;
253
+ try {
254
+ prAuthor = getPrAuthorFn(branch, token, { forgejoUser: reviewIdentity, reviewIdentity, rootDir: worktree });
255
+ } catch {
256
+ prAuthor = null;
257
+ }
258
+ if (prAuthor && prAuthor === reviewIdentity) {
259
+ log(fmt.status('WARN', `Reviewer "${reviewIdentity}" is the PR author for ${branch}; skipping the provider review POST to avoid a self-approval (Forgejo rejects "approve your own pull is not allowed" with HTTP 422). Recording the "${outcome}" verdict locally in review-state.json / review-events; a different agent or a human must post the formal approval.`));
260
+ recordLocalReviewVerdict(slug, outcome, {
261
+ worktree,
262
+ reviewer: reviewIdentity,
263
+ writeReviewStateFn: options.writeReviewStateFn,
264
+ createEventFn: options.createEventFn,
265
+ readReviewStateFn,
266
+ log: log,
267
+ error,
268
+ });
269
+ return { ok: true, skipped: true, reason: 'self-author', prAuthor };
270
+ }
271
+
272
+ const taggedMessage = message + buildMetadataFooterFn(slug, worktree);
273
+ log(fmt.status('INFO', `Submitting review outcome "${outcome}" on ${branch} as ${reviewIdentity}...`));
274
+ const result = postReviewFn(branch, token, outcome, taggedMessage, { forgejoUser: reviewIdentity, reviewIdentity });
275
+ const r = result as Record<string, unknown>;
276
+ if (!r.ok) {
277
+ const failureDetail = describeProviderFailure(result);
278
+ error(fmt.status('FAIL', `Could not submit review: ${failureDetail}`));
279
+ return { ok: false, error: failureDetail };
280
+ }
281
+
282
+ log(fmt.status('PASS', `Review outcome "${outcome}" posted on PR for ${branch}.`));
283
+ return { ok: true };
284
+ }
285
+
286
+ // ============================================================================
287
+ // Artifact Consumption
288
+ // ============================================================================
289
+
290
+
291
+ async function consumeReviewerArtifacts(
292
+ slug: string,
293
+ reviewer: string,
294
+ options: {
295
+ log?: (msg: string) => void;
296
+ error?: (msg: string) => void;
297
+ readArtifactFn?: typeof readArtifactFile;
298
+ deleteArtifactFn?: typeof deleteArtifactFile;
299
+ tmpDir?: string | null;
300
+ fallbackToTmp?: boolean;
301
+ worktree?: string;
302
+ providerEnabled?: boolean | null;
303
+ forgejoEnabled?: boolean | null;
304
+ readTokenFn?: (user: string, opts?: Record<string, unknown>) => string | null;
305
+ getCommentsFn?: (branch: string, token: string) => Promise<unknown[]>;
306
+ postCommentFn?: (branch: string, token: string, body: string, opts?: Record<string, unknown>) => unknown;
307
+ postReviewFn?: (branch: string, token: string, outcome: string, body: string, opts?: Record<string, unknown>) => unknown;
308
+ buildMetadataFooterFn?: (s: string, r?: string) => string;
309
+ createEventFn?: (s: string, t: string, p: CreateEventParams, o: CreateEventOptions) => CreateEventResult;
310
+ } = {}
311
+ ): Promise<{ consumed: boolean; ok?: boolean; reviewState?: string | null }> {
312
+ const log = options.log || fmt.log.plain;
313
+ const error = options.error || fmt.log.plainError;
314
+ const readArtifactFn = options.readArtifactFn || readArtifactFile;
315
+ const deleteArtifactFn = options.deleteArtifactFn || deleteArtifactFile;
316
+ const explicitTmpDir = options.tmpDir !== null && options.tmpDir !== undefined;
317
+ const fallbackToTmp = options.fallbackToTmp === true;
318
+ const worktree = options.worktree || resolveWorktree(slug) || process.cwd();
319
+ const tmpDir = options.tmpDir || resolveArtifactDir(worktree);
320
+ const providerEnabled = options.providerEnabled !== null && options.providerEnabled !== undefined
321
+ ? options.providerEnabled
322
+ : (options.forgejoEnabled !== null && options.forgejoEnabled !== undefined ? options.forgejoEnabled : isEnabled(worktree));
323
+ const reviewStatePath = reviewStateFile(slug, worktree);
324
+
325
+ const findingsResolved = resolveArtifactRead(slug, 'review-findings.md', { tmpDir, explicitTmpDir, fallbackToTmp, readArtifactFn });
326
+ const outcomeResolved = resolveArtifactRead(slug, 'review-outcome.md', { tmpDir, explicitTmpDir, fallbackToTmp, readArtifactFn });
327
+ const verdictResolved = resolveArtifactRead(slug, 'review-verdict.txt', { tmpDir, explicitTmpDir, fallbackToTmp, readArtifactFn });
328
+ const findingsPath = findingsResolved.path;
329
+ const outcomePath = outcomeResolved.path;
330
+ const verdictPath = verdictResolved.path;
331
+
332
+ const findings = findingsResolved.value;
333
+ const outcomeMessage = outcomeResolved.value;
334
+ const verdictRaw = verdictResolved.value;
335
+
336
+ let verdict = normalizeReviewVerdict(verdictRaw || '');
337
+ if (!verdict && outcomeMessage) {
338
+ const outcomeVerdictMatch = outcomeMessage.match(/^verdict:\s*(approve|request-changes|comment)/im) ||
339
+ outcomeMessage.match(/Verdict:\s*(approve|request-changes|comment)/i);
340
+ if (outcomeVerdictMatch) {
341
+ verdict = normalizeReviewVerdict(outcomeVerdictMatch[1]);
342
+ }
343
+ }
344
+
345
+ const hasAny = findings !== null || outcomeMessage !== null || verdictRaw !== null;
346
+
347
+ if (!hasAny) {
348
+ return { consumed: false };
349
+ }
350
+ if (!findings || !outcomeMessage) {
351
+ if (!providerEnabled) {
352
+ const statePathStr = reviewStatePath ? ` local review state at ${reviewStatePath}; ` : ' ';
353
+ error(fmt.status('FAIL', `Incomplete reviewer artifacts for ${slug}. Expected ${findingsPath} and ${outcomePath}.${statePathStr}No provider review posted (provider=none); add a review-outcome.md with a Verdict line or use \`node parallix review <slug> --submit-review approve\`.`));
354
+ } else {
355
+ error(fmt.status('FAIL', `Incomplete reviewer artifacts for ${slug}. Expected ${findingsPath} and ${outcomePath}.`));
356
+ }
357
+ return { consumed: true, ok: false };
358
+ }
359
+ if (!verdict) {
360
+ if (!providerEnabled) {
361
+ const statePathStr = reviewStatePath ? ` local review state at ${reviewStatePath}; ` : ' ';
362
+ error(fmt.status('FAIL', `Reviewer artifacts for ${slug} missing verdict. Expected in ${verdictPath} or in ${outcomePath} content.${statePathStr}No provider review posted (provider=none); add a review-outcome.md with a Verdict line or use \`node parallix review <slug> --submit-review approve\`.`));
363
+ } else {
364
+ error(fmt.status('FAIL', `Reviewer artifacts for ${slug} missing verdict. Expected in ${verdictPath} or in ${outcomePath} content.`));
365
+ }
366
+ return { consumed: true, ok: false };
367
+ }
368
+
369
+ const currentState = readReviewState(slug, worktree);
370
+ const round = currentState ? currentState.round : 1;
371
+ const phase = currentState ? currentState.phase : 'reviewing';
372
+
373
+ const createEventFn = options.createEventFn || createEvent;
374
+ const findingsEventResult = createEventFn(slug, VALID_EVENT_TYPES.REVIEWER_FINDINGS, {
375
+ content: findings, round, phase, actor: reviewer
376
+ }, { worktree, skipGit: true, log: log, error });
377
+
378
+ if (!findingsEventResult.ok) {
379
+ error(fmt.status('FAIL', `Failed to persist reviewer findings to repo store: ${(findingsEventResult as { error?: string }).error}`));
380
+ return { consumed: true, ok: false };
381
+ }
382
+
383
+ const outcomeEventResult = createEventFn(slug, VALID_EVENT_TYPES.REVIEWER_OUTCOME, {
384
+ content: outcomeMessage, round, phase, actor: reviewer, verdict
385
+ }, { worktree, skipGit: true, log: log, error });
386
+
387
+ if (!outcomeEventResult.ok) {
388
+ error(fmt.status('FAIL', `Failed to persist reviewer outcome to repo store: ${(outcomeEventResult as { error?: string }).error}`));
389
+ return { consumed: true, ok: false };
390
+ }
391
+
392
+ log(fmt.status('INFO', `Persisted reviewer artifacts to repo store: ${(findingsEventResult as { path?: string }).path}, ${(outcomeEventResult as { path?: string }).path}`));
393
+
394
+ if (providerEnabled && options.readTokenFn && options.getCommentsFn) {
395
+ await consumeHumanNotes(slug, reviewer, {
396
+ getCommentsFn: options.getCommentsFn,
397
+ createEventFn: createEventFn as any,
398
+ readTokenFn: options.readTokenFn,
399
+ reviewIdentity: reviewer,
400
+ worktree,
401
+ log: log,
402
+ error
403
+ });
404
+ }
405
+
406
+ if (providerEnabled) {
407
+ const commentResult = postWorkflowComment(slug, findings, {
408
+ rootDir: worktree,
409
+ reviewIdentity: reviewer,
410
+ readTokenFn: options.readTokenFn,
411
+ postCommentFn: options.postCommentFn,
412
+ buildMetadataFooterFn: options.buildMetadataFooterFn,
413
+ log: log,
414
+ error
415
+ });
416
+ if (!commentResult.ok) {
417
+ return { consumed: true, ok: false };
418
+ }
419
+
420
+ const reviewResult = postWorkflowReview(slug, verdict, outcomeMessage, {
421
+ worktree,
422
+ reviewIdentity: reviewer,
423
+ readTokenFn: options.readTokenFn,
424
+ postReviewFn: options.postReviewFn,
425
+ buildMetadataFooterFn: options.buildMetadataFooterFn,
426
+ log: log,
427
+ error
428
+ });
429
+ if (!reviewResult.ok) {
430
+ return { consumed: true, ok: false };
431
+ }
432
+ } else {
433
+ log(fmt.status('INFO', `Review provider disabled; skipping PR mirroring for ${slug}`));
434
+ }
435
+
436
+ deleteArtifactFn(findingsPath);
437
+ deleteArtifactFn(outcomePath);
438
+ deleteArtifactFn(verdictPath);
439
+
440
+ if (verdict === 'approve') { return { consumed: true, ok: true, reviewState: 'APPROVED' }; }
441
+ if (verdict === 'request-changes') { return { consumed: true, ok: true, reviewState: 'REQUEST_CHANGES' }; }
442
+ if (!providerEnabled) {
443
+ const reviewState = verdict.toUpperCase().replace(/-/g, '_');
444
+ log(fmt.status('INFO', `Reviewer ${reviewer} produced verdict "${verdict}" with the provider disabled; normalizing to ${reviewState} for loop control.`));
445
+ return { consumed: true, ok: true, reviewState };
446
+ }
447
+ log(fmt.status('WARN', `Reviewer ${reviewer} produced verdict "${verdict}". Falling back to provider polling for loop control.`));
448
+ return { consumed: true, ok: true, reviewState: null };
449
+ }
450
+
451
+ async function consumeImplementerArtifacts(
452
+ slug: string,
453
+ implementer: string,
454
+ options: {
455
+ log?: (msg: string) => void;
456
+ error?: (msg: string) => void;
457
+ readArtifactFn?: typeof readArtifactFile;
458
+ deleteArtifactFn?: typeof deleteArtifactFile;
459
+ tmpDir?: string | null;
460
+ fallbackToTmp?: boolean;
461
+ worktree?: string;
462
+ providerEnabled?: boolean | null;
463
+ forgejoEnabled?: boolean | null;
464
+ readTokenFn?: (user: string, opts?: Record<string, unknown>) => string | null;
465
+ getCommentsFn?: (branch: string, token: string) => Promise<unknown[]>;
466
+ postCommentFn?: (branch: string, token: string, body: string, opts?: Record<string, unknown>) => unknown;
467
+ buildMetadataFooterFn?: (s: string, r?: string) => string;
468
+ createEventFn?: (s: string, t: string, p: CreateEventParams, o: CreateEventOptions) => CreateEventResult;
469
+ } = {}
470
+ ): Promise<{ consumed: boolean; ok?: boolean; disposition?: string | null }> {
471
+ const log = options.log || fmt.log.plain;
472
+ const error = options.error || fmt.log.plainError;
473
+ const readArtifactFn = options.readArtifactFn || readArtifactFile;
474
+ const deleteArtifactFn = options.deleteArtifactFn || deleteArtifactFile;
475
+ const explicitTmpDir = options.tmpDir !== null && options.tmpDir !== undefined;
476
+ const fallbackToTmp = options.fallbackToTmp === true;
477
+ const worktree = options.worktree || resolveWorktree(slug) || process.cwd();
478
+ const tmpDir = options.tmpDir || resolveArtifactDir(worktree);
479
+ const providerEnabled = options.providerEnabled !== null && options.providerEnabled !== undefined
480
+ ? options.providerEnabled
481
+ : (options.forgejoEnabled !== null && options.forgejoEnabled !== undefined ? options.forgejoEnabled : isEnabled(worktree));
482
+ const reviewStatePath = reviewStateFile(slug, worktree);
483
+
484
+ const resolutionResolved = resolveArtifactRead(slug, 'round-resolution.md', { tmpDir, explicitTmpDir, fallbackToTmp, readArtifactFn });
485
+ const dispositionResolved = resolveArtifactRead(slug, 'review-disposition.txt', { tmpDir, explicitTmpDir, fallbackToTmp, readArtifactFn });
486
+ const resolutionPath = resolutionResolved.path;
487
+ const dispositionPath = dispositionResolved.path;
488
+
489
+ const resolution = resolutionResolved.value;
490
+ const dispositionRaw = dispositionResolved.value;
491
+ const disposition = normalizeDisposition(dispositionRaw || '');
492
+ const hasAny = resolution !== null || dispositionRaw !== null;
493
+
494
+ if (!hasAny) {
495
+ return { consumed: false };
496
+ }
497
+ if (!resolution || !disposition) {
498
+ if (!providerEnabled) {
499
+ const statePathStr = reviewStatePath ? ` local review state at ${reviewStatePath}; ` : ' ';
500
+ error(fmt.status('FAIL', `Incomplete implementer artifacts for ${slug}. Expected ${resolutionPath} and ${dispositionPath}.${statePathStr}No provider review posted (provider=none); add review-disposition.txt and a round resolution, or use \`node parallix review <slug> --submit-review approve\`.`));
501
+ } else {
502
+ error(fmt.status('FAIL', `Incomplete implementer artifacts for ${slug}. Expected ${resolutionPath} and ${dispositionPath}.`));
503
+ }
504
+ return { consumed: true, ok: false };
505
+ }
506
+
507
+ const currentState = readReviewState(slug, worktree);
508
+ const round = currentState ? currentState.round : 1;
509
+ const phase = currentState ? currentState.phase : 'fixing';
510
+
511
+ const createEventFn = options.createEventFn || createEvent;
512
+
513
+ let fixedItems: unknown[] = [];
514
+ let pushedBackItems: unknown[] = [];
515
+ let parkedItems: unknown[] = [];
516
+ let blockedReason: string | null = null;
517
+
518
+ try {
519
+ const fixedMatch = resolution.match(/fixed_items:\s*(\[[^\]]*\])/i);
520
+ const pushedMatch = resolution.match(/pushed_back_items:\s*(\[[^\]]*\])/i);
521
+ const parkedMatch = resolution.match(/parked_items:\s*(\[[^\]]*\])/i);
522
+ const blockedMatch = resolution.match(/blocked_reason:\s*"([^"]*)"/i);
523
+
524
+ if (fixedMatch) { fixedItems = JSON.parse(fixedMatch[1]); }
525
+ if (pushedMatch) { pushedBackItems = JSON.parse(pushedMatch[1]); }
526
+ if (parkedMatch) { parkedItems = JSON.parse(parkedMatch[1]); }
527
+ if (blockedMatch) { blockedReason = blockedMatch[1]; }
528
+ } catch {
529
+ fixedItems = [];
530
+ pushedBackItems = [];
531
+ parkedItems = [];
532
+ }
533
+
534
+ if (disposition === 'BLOCKED' && !blockedReason) {
535
+ const match = resolution.match(/blocked.*?:\s*(.+)/i);
536
+ if (match) { blockedReason = match[1].trim(); }
537
+ }
538
+
539
+ const summaryEventResult = createEventFn(slug, VALID_EVENT_TYPES.IMPLEMENTER_ROUND_SUMMARY, {
540
+ content: resolution, round, phase, actor: implementer,
541
+ fixedItems, pushedBackItems, parkedItems,
542
+ ...(disposition === 'BLOCKED' && blockedReason ? { blockedReason } : {})
543
+ }, { worktree, skipGit: true, log: log, error });
544
+
545
+ if (!summaryEventResult.ok) {
546
+ error(fmt.status('FAIL', `Failed to persist implementer round summary to repo store: ${(summaryEventResult as { error?: string }).error}`));
547
+ return { consumed: true, ok: false };
548
+ }
549
+
550
+ const dispositionEventResult = createEventFn(slug, VALID_EVENT_TYPES.IMPLEMENTER_DISPOSITION, {
551
+ content: `Autonomous review disposition: ${disposition}`, round, phase, actor: implementer, disposition
552
+ }, { worktree, skipGit: true, log: log, error });
553
+
554
+ if (!dispositionEventResult.ok) {
555
+ error(fmt.status('FAIL', `Failed to persist implementer disposition to repo store: ${(dispositionEventResult as { error?: string }).error}`));
556
+ return { consumed: true, ok: false };
557
+ }
558
+
559
+ log(fmt.status('INFO', `Persisted implementer artifacts to repo store: ${(summaryEventResult as { path?: string }).path}, ${(dispositionEventResult as { path?: string }).path}`));
560
+
561
+ if (providerEnabled && options.readTokenFn && options.getCommentsFn) {
562
+ await consumeHumanNotes(slug, implementer, {
563
+ getCommentsFn: options.getCommentsFn,
564
+ createEventFn: createEventFn as any,
565
+ readTokenFn: options.readTokenFn,
566
+ reviewIdentity: implementer,
567
+ worktree,
568
+ log: log,
569
+ error
570
+ });
571
+ }
572
+
573
+ if (providerEnabled) {
574
+ const resolutionResult = postWorkflowComment(slug, resolution, {
575
+ rootDir: worktree,
576
+ reviewIdentity: implementer,
577
+ readTokenFn: options.readTokenFn,
578
+ postCommentFn: options.postCommentFn,
579
+ buildMetadataFooterFn: options.buildMetadataFooterFn,
580
+ log: log,
581
+ error
582
+ });
583
+ if (!resolutionResult.ok) {
584
+ return { consumed: true, ok: false };
585
+ }
586
+
587
+ const dispositionResult = postWorkflowComment(slug, `Autonomous review disposition: ${disposition}`, {
588
+ rootDir: worktree,
589
+ reviewIdentity: implementer,
590
+ readTokenFn: options.readTokenFn,
591
+ postCommentFn: options.postCommentFn,
592
+ buildMetadataFooterFn: options.buildMetadataFooterFn,
593
+ log: log,
594
+ error
595
+ });
596
+ if (!dispositionResult.ok) {
597
+ return { consumed: true, ok: false };
598
+ }
599
+ } else {
600
+ log(fmt.status('INFO', `Review provider disabled; skipping PR mirroring for ${slug}`));
601
+ }
602
+
603
+ deleteArtifactFn(resolutionPath);
604
+ deleteArtifactFn(dispositionPath);
605
+
606
+ return { consumed: true, ok: true, disposition };
607
+ }
608
+
609
+ // Module exports
610
+ export {
611
+ buildMetadataFooter,
612
+ reviewArtifactPath,
613
+ resolveArtifactDir,
614
+ readArtifactFile,
615
+ deleteArtifactFile,
616
+ normalizeReviewVerdict,
617
+ normalizeDisposition,
618
+ postWorkflowComment,
619
+ postWorkflowReview,
620
+ consumeReviewerArtifacts,
621
+ consumeImplementerArtifacts
622
+ };