@magnusekdahl/parallix 1.2.1 → 1.3.1

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 (81) hide show
  1. package/index.js +190 -185
  2. package/lib/agents/agents.js +34 -4
  3. package/lib/agents/agents.ts +40 -5
  4. package/lib/commands/active.js +502 -579
  5. package/lib/commands/active.ts +665 -0
  6. package/lib/commands/checkpoint.js +98 -74
  7. package/lib/commands/checkpoint.ts +79 -0
  8. package/lib/commands/config.js +61 -31
  9. package/lib/commands/config.ts +47 -0
  10. package/lib/commands/coverage-gate.js +271 -246
  11. package/lib/commands/coverage-gate.ts +362 -0
  12. package/lib/commands/diff.js +140 -119
  13. package/lib/commands/diff.ts +125 -0
  14. package/lib/commands/draft.js +770 -923
  15. package/lib/commands/draft.ts +1021 -0
  16. package/lib/commands/handoff.js +588 -608
  17. package/lib/commands/handoff.ts +694 -0
  18. package/lib/commands/integrate.js +1331 -1468
  19. package/lib/commands/integrate.ts +1688 -0
  20. package/lib/commands/mission-start.js +279 -237
  21. package/lib/commands/mission-start.ts +263 -0
  22. package/lib/commands/rebase.js +606 -582
  23. package/lib/commands/rebase.ts +635 -0
  24. package/lib/commands/repair-handoff.js +181 -179
  25. package/lib/commands/repair-handoff.ts +228 -0
  26. package/lib/commands/resolve-conflict.js +121 -102
  27. package/lib/commands/resolve-conflict.ts +112 -0
  28. package/lib/commands/review.js +3 -4
  29. package/lib/commands/review.ts +14 -0
  30. package/lib/commands/setup-review.js +4 -5
  31. package/lib/commands/setup-review.ts +14 -0
  32. package/lib/commands/setup.js +3 -3
  33. package/lib/commands/setup.ts +2 -0
  34. package/lib/commands/stats-backfill.js +394 -359
  35. package/lib/commands/stats-backfill.ts +418 -0
  36. package/lib/commands/stats.js +1424 -1665
  37. package/lib/commands/stats.ts +2186 -0
  38. package/lib/commands/status.js +219 -200
  39. package/lib/commands/status.ts +219 -0
  40. package/lib/commands/verify.js +3 -1
  41. package/lib/commands/verify.ts +2 -0
  42. package/lib/core/nels.js +112 -127
  43. package/lib/core/nels.ts +189 -0
  44. package/lib/core/subagent-limit.js +15 -16
  45. package/lib/core/subagent-limit.ts +25 -0
  46. package/lib/index.js +142 -59
  47. package/lib/index.ts +134 -0
  48. package/lib/review/rebase.js +142 -145
  49. package/lib/review/rebase.ts +188 -0
  50. package/lib/review/review-adapter.js +99 -87
  51. package/lib/review/review-adapter.ts +198 -0
  52. package/lib/review/review-artifacts.js +486 -629
  53. package/lib/review/review-artifacts.ts +622 -0
  54. package/lib/review/review-commands.js +1220 -1373
  55. package/lib/review/review-commands.ts +1579 -0
  56. package/lib/review/review-events.js +544 -886
  57. package/lib/review/review-events.ts +954 -0
  58. package/lib/review/review-loop.js +945 -1020
  59. package/lib/review/review-loop.ts +1174 -0
  60. package/lib/review/review-polling.js +86 -115
  61. package/lib/review/review-polling.ts +194 -0
  62. package/lib/review/review-prompts.js +154 -189
  63. package/lib/review/review-prompts.ts +182 -0
  64. package/lib/review/review-state.js +246 -225
  65. package/lib/review/review-state.ts +353 -0
  66. package/lib/review/review.js +63 -84
  67. package/lib/review/review.ts +138 -0
  68. package/lib/tools/backlog.js +694 -680
  69. package/lib/tools/backlog.ts +835 -0
  70. package/lib/tools/forgejo.js +1270 -1405
  71. package/lib/tools/forgejo.ts +1793 -0
  72. package/lib/tools/gatekeeper.js +106 -96
  73. package/lib/tools/gatekeeper.ts +124 -0
  74. package/lib/tools/redgreen.js +200 -181
  75. package/lib/tools/redgreen.ts +221 -0
  76. package/lib/tools/sessions.js +58 -52
  77. package/lib/tools/sessions.ts +81 -0
  78. package/lib/tools/setup-review.js +902 -975
  79. package/lib/tools/setup-review.ts +1152 -0
  80. package/package.json +4 -4
  81. package/px.js +226 -208
@@ -1,685 +1,542 @@
1
+ "use strict";
1
2
  /**
2
3
  * Review Artifacts Module - Utility Functions
3
4
  * Extracted from parallix/lib/review.js for task-1201
4
5
  * Handles artifact path resolution, file I/O, and normalization.
5
6
  */
6
-
7
- const fs = require('fs');
8
- const os = require('os');
9
- const path = require('path');
10
- const fmt = require('../core/fmt');
11
- const { missionBranchName, resolveWorktree } = require('../core/mission-utils');
12
- const { readReviewState, writeReviewState, reviewStateFile, ReviewState, resolveReviewIdentity } = require('./review-state');
13
- const { readToken, postComment, postReview, getPrAuthor, isEnabled, resolveArtifactDir: resolveConfiguredArtifactDir } = require('./review-adapter');
14
- const { createEvent, consumeHumanNotes, VALID_EVENT_TYPES } = require('./review-events');
15
-
7
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
8
+ if (k2 === undefined) k2 = k;
9
+ var desc = Object.getOwnPropertyDescriptor(m, k);
10
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
11
+ desc = { enumerable: true, get: function() { return m[k]; } };
12
+ }
13
+ Object.defineProperty(o, k2, desc);
14
+ }) : (function(o, m, k, k2) {
15
+ if (k2 === undefined) k2 = k;
16
+ o[k2] = m[k];
17
+ }));
18
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
19
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
20
+ }) : function(o, v) {
21
+ o["default"] = v;
22
+ });
23
+ var __importStar = (this && this.__importStar) || (function () {
24
+ var ownKeys = function(o) {
25
+ ownKeys = Object.getOwnPropertyNames || function (o) {
26
+ var ar = [];
27
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
28
+ return ar;
29
+ };
30
+ return ownKeys(o);
31
+ };
32
+ return function (mod) {
33
+ if (mod && mod.__esModule) return mod;
34
+ var result = {};
35
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
36
+ __setModuleDefault(result, mod);
37
+ return result;
38
+ };
39
+ })();
40
+ Object.defineProperty(exports, "__esModule", { value: true });
41
+ exports.buildMetadataFooter = buildMetadataFooter;
42
+ exports.reviewArtifactPath = reviewArtifactPath;
43
+ exports.resolveArtifactDir = resolveArtifactDir;
44
+ exports.readArtifactFile = readArtifactFile;
45
+ exports.deleteArtifactFile = deleteArtifactFile;
46
+ exports.normalizeReviewVerdict = normalizeReviewVerdict;
47
+ exports.normalizeDisposition = normalizeDisposition;
48
+ exports.postWorkflowComment = postWorkflowComment;
49
+ exports.postWorkflowReview = postWorkflowReview;
50
+ exports.consumeReviewerArtifacts = consumeReviewerArtifacts;
51
+ exports.consumeImplementerArtifacts = consumeImplementerArtifacts;
52
+ const fs = __importStar(require("fs"));
53
+ const os = __importStar(require("os"));
54
+ const path = __importStar(require("path"));
55
+ const fmt = __importStar(require("../core/fmt.js"));
56
+ const mission_utils_js_1 = require("../core/mission-utils.js");
57
+ const review_state_js_1 = require("./review-state.js");
58
+ const review_adapter_js_1 = require("./review-adapter.js");
59
+ const review_events_js_1 = require("./review-events.js");
16
60
  // ============================================================================
17
61
  // Metadata Footer
18
62
  // ============================================================================
19
-
20
- /**
21
- * @param {string} slug
22
- * @param {string} [rootDir]
23
- * @returns {string}
24
- */
25
- function buildMetadataFooter(slug, rootDir) {
26
- const state = readReviewState(slug, rootDir);
27
- if (!state) {return '';}
28
- return `\n\n---\n\`[workflow-round:${state.round}, workflow-phase:${state.phase}]\``;
63
+ function buildMetadataFooter(slug, rootDir = process.cwd()) {
64
+ const state = (0, review_state_js_1.readReviewState)(slug, rootDir);
65
+ if (!state) {
66
+ return '';
67
+ }
68
+ return `\n\n---\n\`[workflow-round:${state.round}, workflow-phase:${state.phase}]\``;
29
69
  }
30
-
31
70
  // ============================================================================
32
71
  // Artifact Path Utilities
33
72
  // ============================================================================
34
-
35
- /**
36
- * @param {string} slug
37
- * @param {string} artifactName
38
- * @param {string} [tmpDir]
39
- * @returns {string}
40
- */
41
73
  function reviewArtifactPath(slug, artifactName, tmpDir = os.tmpdir()) {
42
- return path.join(tmpDir, `${slug}-${artifactName}`);
74
+ return path.join(tmpDir, `${slug}-${artifactName}`);
43
75
  }
44
-
45
- // Agents are instructed by parallix/prompts/*.md to write review/act-on-review
46
- // artifacts to the resolved artifact dir ({{artifactDir}}). The consumers read
47
- // from os.tmpdir() by default, which diverges from /tmp whenever TMPDIR is set
48
- // (CI runners, sandboxes); under the OLD prompts that hardcoded /tmp this left
49
- // artifacts unseen and the review loop reported "no formal review outcome".
50
- // Resolve from the configured tmpDir first, then fall back to the /tmp path the
51
- // old prompts advertised. The fallback is normally skipped when a caller passes
52
- // an explicit tmpDir, so unit tests that point at their own scratch dir are
53
- // unaffected — but the review loop opts back in via `fallbackToTmp: true` so the
54
- // /tmp safety net still protects pre-existing/old-prompt artifacts even though
55
- // the loop passes the resolved default dir explicitly. This is defense-in-depth:
56
- // once the prompts substitute {{artifactDir}}, producer and consumer share one
57
- // dir by construction and this fallback is rarely exercised.
58
76
  /**
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}}
77
+ * Resolve artifact read path with optional /tmp fallback.
63
78
  */
64
- function resolveArtifactRead(slug, artifactName, { tmpDir, explicitTmpDir, fallbackToTmp, readArtifactFn }) {
65
- const primaryPath = reviewArtifactPath(slug, artifactName, tmpDir);
66
- const primaryValue = readArtifactFn(primaryPath);
67
- const allowTmpFallback = (!explicitTmpDir || fallbackToTmp) && tmpDir !== '/tmp';
68
- if (primaryValue !== null || !allowTmpFallback) {
69
- return { path: primaryPath, value: primaryValue };
70
- }
71
- const fallbackPath = reviewArtifactPath(slug, artifactName, '/tmp');
72
- const fallbackValue = readArtifactFn(fallbackPath);
73
- if (fallbackValue !== null) {
74
- return { path: fallbackPath, value: fallbackValue };
75
- }
76
- return { path: primaryPath, value: null };
79
+ function resolveArtifactRead(slug, artifactName, opts) {
80
+ const { tmpDir, explicitTmpDir, fallbackToTmp, readArtifactFn } = opts;
81
+ const primaryPath = reviewArtifactPath(slug, artifactName, tmpDir);
82
+ const primaryValue = readArtifactFn(primaryPath);
83
+ const allowTmpFallback = (!explicitTmpDir || fallbackToTmp) && tmpDir !== '/tmp';
84
+ if (primaryValue !== null || !allowTmpFallback) {
85
+ return { path: primaryPath, value: primaryValue };
86
+ }
87
+ const fallbackPath = reviewArtifactPath(slug, artifactName, '/tmp');
88
+ const fallbackValue = readArtifactFn(fallbackPath);
89
+ if (fallbackValue !== null) {
90
+ return { path: fallbackPath, value: fallbackValue };
91
+ }
92
+ return { path: primaryPath, value: null };
77
93
  }
78
-
79
94
  /**
80
- * Resolve the directory where review artifacts (review-findings.md,
81
- * review-outcome.md, review-verdict.txt, round-resolution.md,
82
- * review-disposition.txt) are written and consumed.
83
- *
84
- * Reads `adapters.review.tmpDir` from workflow.config.json when present so a
85
- * downstream repo can pin a single, portable artifact location that the review
86
- * prompt and the consumer agree on. A relative configured value is resolved
87
- * against the repo root. Falls back to `os.tmpdir()` when unconfigured, which
88
- * preserves the historical default for every existing caller.
89
- *
90
- * @param {string} [rootDir]
91
- * @returns {string} absolute artifact directory
95
+ * Resolve the directory where review artifacts are written and consumed.
92
96
  */
93
97
  function resolveArtifactDir(rootDir = process.cwd()) {
94
- return resolveConfiguredArtifactDir(rootDir);
98
+ return (0, review_adapter_js_1.resolveArtifactDir)(rootDir);
95
99
  }
96
-
97
- /**
98
- * @param {string} filePath
99
- * @param {Function} [readFileSync]
100
- * @returns {string|null}
101
- */
102
100
  function readArtifactFile(filePath, readFileSync = fs.readFileSync) {
103
- try {
104
- const value = readFileSync(filePath, 'utf8');
105
- return typeof value === 'string' ? value.trim() : '';
106
- } catch (_) {
107
- return null;
108
- }
101
+ try {
102
+ const value = readFileSync(filePath, 'utf8');
103
+ return typeof value === 'string' ? value.trim() : '';
104
+ }
105
+ catch {
106
+ return null;
107
+ }
109
108
  }
110
-
111
- /**
112
- * @param {string} filePath
113
- * @param {Function} [unlinkSync]
114
- * @returns {void}
115
- */
116
109
  function deleteArtifactFile(filePath, unlinkSync = fs.unlinkSync) {
117
- try {
118
- unlinkSync(filePath);
119
- } catch (_) {
120
- // Best-effort cleanup only.
121
- }
110
+ try {
111
+ unlinkSync(filePath);
112
+ }
113
+ catch {
114
+ // Best-effort cleanup only.
115
+ }
122
116
  }
123
-
124
117
  // ============================================================================
125
118
  // Normalization Utilities
126
119
  // ============================================================================
127
-
128
- /**
129
- * @param {string} value
130
- * @returns {string|null}
131
- */
132
120
  function normalizeReviewVerdict(value) {
133
- const normalized = String(value || '').trim().toLowerCase();
134
- return ['approve', 'request-changes', 'comment'].includes(normalized) ? normalized : null;
121
+ const normalized = String(value || '').trim().toLowerCase();
122
+ return ['approve', 'request-changes', 'comment'].includes(normalized) ? normalized : null;
135
123
  }
136
-
137
- /** @param {string} value
138
- * @returns {string|null}
139
- */
140
124
  function normalizeDisposition(value) {
141
- const normalized = String(value || '').trim().toUpperCase();
142
- return ['CHANGES_MADE', 'PUSHBACK_ALL', 'PARKED', 'BLOCKED'].includes(normalized) ? normalized : null;
125
+ const normalized = String(value || '').trim().toUpperCase();
126
+ return ['CHANGES_MADE', 'PUSHBACK_ALL', 'PARKED', 'BLOCKED'].includes(normalized) ? normalized : null;
143
127
  }
144
-
145
128
  // ============================================================================
146
129
  // Workflow Comment/Review Posting
147
130
  // ============================================================================
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
- */
155
131
  function postWorkflowComment(slug, message, options = {}) {
156
- const log = options.log || fmt.log.plain;
157
- const error = options.error || fmt.log.plainError;
158
- const readTokenFn = options.readTokenFn || readToken;
159
- const postCommentFn = options.postCommentFn || postComment;
160
- const buildMetadataFooterFn = options.buildMetadataFooterFn || buildMetadataFooter;
161
- const readReviewStateFn = options.readReviewStateFn || readReviewState;
162
- const rootDir = options.rootDir || resolveWorktree(slug) || process.cwd();
163
- const branch = missionBranchName(slug, rootDir);
164
- const reviewIdentity = options.reviewIdentity
165
- || options.forgejoUser
166
- || resolveReviewIdentity(slug, rootDir, { readReviewStateFn }).commentIdentityUser
167
- || 'human';
168
- const token = readTokenFn(reviewIdentity, { rootDir });
169
- if (!token) {
170
- error(fmt.status('FAIL', `No Forgejo token found for user "${reviewIdentity}". Cannot post comment.`));
171
- return { ok: false };
172
- }
173
-
174
- const taggedMessage = message + buildMetadataFooterFn(slug, rootDir);
175
- log(fmt.status('INFO', `Posting PR comment on ${branch} as ${reviewIdentity}...`));
176
- const result = postCommentFn(branch, token, taggedMessage, { forgejoUser: reviewIdentity, reviewIdentity });
177
- if (!result.ok) {
178
- error(fmt.status('FAIL', `Could not post comment: ${result.error || 'API error'}`));
179
- return { ok: false, error: result.error || 'API error' };
180
- }
181
-
182
- log(fmt.status('PASS', `Comment posted on PR for ${branch}.`));
183
- return { ok: true };
132
+ const log = options.log || fmt.log.plain;
133
+ const error = options.error || fmt.log.plainError;
134
+ const readTokenFn = options.readTokenFn || review_adapter_js_1.readToken;
135
+ const postCommentFn = options.postCommentFn || review_adapter_js_1.postComment;
136
+ const buildMetadataFooterFn = options.buildMetadataFooterFn || buildMetadataFooter;
137
+ const readReviewStateFn = options.readReviewStateFn || review_state_js_1.readReviewState;
138
+ const rootDir = options.rootDir || (0, mission_utils_js_1.resolveWorktree)(slug) || process.cwd();
139
+ const branch = (0, mission_utils_js_1.missionBranchName)(slug, rootDir);
140
+ const reviewIdentity = options.reviewIdentity
141
+ || options.forgejoUser
142
+ || (0, review_state_js_1.resolveReviewIdentity)(slug, rootDir, { readReviewStateFn }).commentIdentityUser
143
+ || 'human';
144
+ const token = readTokenFn(reviewIdentity, { rootDir });
145
+ if (!token) {
146
+ error(fmt.status('FAIL', `No Forgejo token found for user "${reviewIdentity}". Cannot post comment.`));
147
+ return { ok: false };
148
+ }
149
+ const taggedMessage = message + buildMetadataFooterFn(slug, rootDir);
150
+ log(fmt.status('INFO', `Posting PR comment on ${branch} as ${reviewIdentity}...`));
151
+ const result = postCommentFn(branch, token, taggedMessage, { forgejoUser: reviewIdentity, reviewIdentity });
152
+ const r = result;
153
+ if (!r.ok) {
154
+ error(fmt.status('FAIL', `Could not post comment: ${r.error || 'API error'}`));
155
+ return { ok: false, error: r.error || 'API error' };
156
+ }
157
+ log(fmt.status('PASS', `Comment posted on PR for ${branch}.`));
158
+ return { ok: true };
184
159
  }
185
-
186
- // Build a diagnosable error string from a failed provider API result, surfacing
187
- // the real HTTP status and response body (e.g. `422 approve your own pull is not
188
- // allowed`) instead of masking it behind a generic "API error". Falls back to
189
- // the result's own error/raw fields, then "API error" when nothing is present.
190
160
  /** @param {*} result */
191
161
  function describeProviderFailure(result) {
192
- if (!result || typeof result !== 'object') {return 'API error';}
193
- // Prefer the HTTP status code (statusCode) over curl's process status.
194
- const httpStatus = (result.statusCode !== null && Number(result.statusCode) > 0)
195
- ? result.statusCode
196
- : (result.status !== null && Number(result.status) >= 100 ? result.status : null);
197
- let body = null;
198
- if (result.data && typeof result.data === 'object') {
199
- body = result.data.message || JSON.stringify(result.data);
200
- } else if (typeof result.data === 'string' && result.data) {
201
- body = result.data;
202
- }
203
- const parts = [];
204
- if (httpStatus !== null) {parts.push(String(httpStatus));}
205
- if (body) {parts.push(body);}
206
- if (parts.length) {return parts.join(' ');}
207
- return result.error || result.raw || 'API error';
162
+ if (!result || typeof result !== 'object') {
163
+ return 'API error';
164
+ }
165
+ const r = result;
166
+ const httpStatus = (r.statusCode !== null && Number(r.statusCode) > 0)
167
+ ? r.statusCode
168
+ : (r.status !== null && Number(r.status) >= 100 ? r.status : null);
169
+ let body = null;
170
+ if (r.data && typeof r.data === 'object') {
171
+ body = r.data.message || JSON.stringify(r.data);
172
+ }
173
+ else if (typeof r.data === 'string' && r.data) {
174
+ body = r.data;
175
+ }
176
+ const parts = [];
177
+ if (httpStatus !== null) {
178
+ parts.push(String(httpStatus));
179
+ }
180
+ if (body) {
181
+ parts.push(body);
182
+ }
183
+ if (parts.length) {
184
+ return parts.join(' ');
185
+ }
186
+ return r.error || r.raw || 'API error';
208
187
  }
209
-
210
- // Record a review verdict in local review-state.json / review-events without
211
- // posting to the review provider. Used by the self-author skip path so the verdict is still
212
- // captured even though the formal provider approval is intentionally not posted.
213
188
  /**
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}
189
+ * Record a review verdict in local review-state.json / review-events.
218
190
  */
219
191
  function recordLocalReviewVerdict(slug, outcome, options = {}) {
220
- const worktree = options.worktree || resolveWorktree(slug) || process.cwd();
221
- const writeReviewStateFn = options.writeReviewStateFn || writeReviewState;
222
- const createEventFn = options.createEventFn || createEvent;
223
- const readReviewStateFn = options.readReviewStateFn || readReviewState;
224
- const log = options.log || fmt.log.plain;
225
- const error = options.error || fmt.log.plainError;
226
-
227
- const existing = readReviewStateFn(slug, worktree);
228
- const state = existing instanceof ReviewState
229
- ? existing
230
- : new ReviewState(slug, existing || {
231
- reviewer: options.reviewer,
232
- implementer: options.reviewer,
233
- round: 1,
234
- phase: 'reviewing',
235
- });
236
- if (outcome === 'approve') {
237
- state.disposition = 'APPROVED';
238
- try { state.transitionTo('approved'); } catch (_) {}
239
- } else if (outcome === 'request-changes') {
240
- state.disposition = 'REQUEST_CHANGES';
241
- try { state.transitionTo('fixing'); } catch (_) {}
242
- }
243
- writeReviewStateFn(slug, state, worktree);
244
-
245
- // Verdict values (approve/request-changes/comment) map 1:1 to outcome values.
246
- createEventFn(slug, VALID_EVENT_TYPES.REVIEWER_OUTCOME, { verdict: outcome }, { worktree, log, error });
192
+ const worktree = options.worktree || (0, mission_utils_js_1.resolveWorktree)(slug) || process.cwd();
193
+ const writeReviewStateFn = options.writeReviewStateFn || review_state_js_1.writeReviewState;
194
+ const createEventFn = options.createEventFn || review_events_js_1.createEvent;
195
+ const readReviewStateFn = options.readReviewStateFn || review_state_js_1.readReviewState;
196
+ const log = options.log || fmt.log.plain;
197
+ const error = options.error || fmt.log.plainError;
198
+ const existing = readReviewStateFn(slug, worktree);
199
+ const state = existing instanceof review_state_js_1.ReviewState
200
+ ? existing
201
+ : new review_state_js_1.ReviewState(slug, existing || {
202
+ reviewer: options.reviewer,
203
+ implementer: options.reviewer,
204
+ round: 1,
205
+ phase: 'reviewing',
206
+ });
207
+ if (outcome === 'approve') {
208
+ state.disposition = 'APPROVED';
209
+ try {
210
+ state.transitionTo('approved');
211
+ }
212
+ catch { /* ignore */ }
213
+ }
214
+ else if (outcome === 'request-changes') {
215
+ state.disposition = 'REQUEST_CHANGES';
216
+ try {
217
+ state.transitionTo('fixing');
218
+ }
219
+ catch { /* ignore */ }
220
+ }
221
+ writeReviewStateFn(slug, state, worktree);
222
+ createEventFn(slug, review_events_js_1.VALID_EVENT_TYPES.REVIEWER_OUTCOME, { verdict: outcome, content: `Review verdict: ${outcome}` }, { worktree, log: log, error });
247
223
  }
248
-
249
224
  /**
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}}
225
+ * Submit a review outcome to the provider.
255
226
  */
256
227
  function postWorkflowReview(slug, outcome, message, options = {}) {
257
- const log = options.log || fmt.log.plain;
258
- const error = options.error || fmt.log.plainError;
259
- const readTokenFn = options.readTokenFn || readToken;
260
- const postReviewFn = options.postReviewFn || postReview;
261
- const buildMetadataFooterFn = options.buildMetadataFooterFn || buildMetadataFooter;
262
- const readReviewStateFn = options.readReviewStateFn || readReviewState;
263
- const worktree = options.worktree || resolveWorktree(slug) || process.cwd();
264
- const branch = missionBranchName(slug, worktree);
265
- const reviewIdentity = options.reviewIdentity
266
- || options.forgejoUser
267
- || resolveReviewIdentity(slug, worktree, { readReviewStateFn }).identityUser
268
- || 'human';
269
- const token = readTokenFn(reviewIdentity, { rootDir: worktree });
270
- if (!token) {
271
- error(fmt.status('FAIL', `No Forgejo token found for user "${reviewIdentity}". Cannot submit review.`));
272
- return { ok: false };
273
- }
274
-
275
- // Self-approval guard: Forgejo rejects a formal review when the reviewer is
276
- // also the PR author (HTTP 422 "approve your own pull is not allowed"). This
277
- // arises under the legitimate same-agent-reviewer fallback. Detect it up front
278
- // and skip the POST — recording the verdict locally instead of 422-ing — and
279
- // WARN that a different agent or human must post the formal approval. Degrade
280
- // safely: if the author cannot be resolved, treat it as "not self" and POST.
281
- const getPrAuthorFn = options.getPrAuthorFn || getPrAuthor;
282
- let prAuthor = null;
283
- try {
284
- prAuthor = getPrAuthorFn(branch, token, { forgejoUser: reviewIdentity, reviewIdentity, rootDir: worktree });
285
- } catch (_) {
286
- prAuthor = null;
287
- }
288
- if (prAuthor && prAuthor === reviewIdentity) {
289
- 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.`));
290
- recordLocalReviewVerdict(slug, outcome, {
291
- worktree,
292
- reviewer: reviewIdentity,
293
- writeReviewStateFn: options.writeReviewStateFn,
294
- createEventFn: options.createEventFn,
295
- readReviewStateFn,
296
- log,
297
- error,
298
- });
299
- return { ok: true, skipped: true, reason: 'self-author', prAuthor };
300
- }
301
-
302
- const taggedMessage = message + buildMetadataFooterFn(slug, worktree);
303
- log(fmt.status('INFO', `Submitting review outcome "${outcome}" on ${branch} as ${reviewIdentity}...`));
304
- const result = postReviewFn(branch, token, outcome, taggedMessage, { forgejoUser: reviewIdentity, reviewIdentity });
305
- if (!result.ok) {
306
- const failureDetail = describeProviderFailure(result);
307
- error(fmt.status('FAIL', `Could not submit review: ${failureDetail}`));
308
- return { ok: false, error: failureDetail };
309
- }
310
-
311
- log(fmt.status('PASS', `Review outcome "${outcome}" posted on PR for ${branch}.`));
312
- return { ok: true };
228
+ const log = options.log || fmt.log.plain;
229
+ const error = options.error || fmt.log.plainError;
230
+ const readTokenFn = options.readTokenFn || review_adapter_js_1.readToken;
231
+ const postReviewFn = options.postReviewFn || review_adapter_js_1.postReview;
232
+ const buildMetadataFooterFn = options.buildMetadataFooterFn || buildMetadataFooter;
233
+ const readReviewStateFn = options.readReviewStateFn || review_state_js_1.readReviewState;
234
+ const worktree = options.worktree || (0, mission_utils_js_1.resolveWorktree)(slug) || process.cwd();
235
+ const branch = (0, mission_utils_js_1.missionBranchName)(slug, worktree);
236
+ const reviewIdentity = options.reviewIdentity
237
+ || options.forgejoUser
238
+ || (0, review_state_js_1.resolveReviewIdentity)(slug, worktree, { readReviewStateFn }).identityUser
239
+ || 'human';
240
+ const token = readTokenFn(reviewIdentity, { rootDir: worktree });
241
+ if (!token) {
242
+ error(fmt.status('FAIL', `No Forgejo token found for user "${reviewIdentity}". Cannot submit review.`));
243
+ return { ok: false };
244
+ }
245
+ const getPrAuthorFn = options.getPrAuthorFn || review_adapter_js_1.getPrAuthor;
246
+ let prAuthor = null;
247
+ try {
248
+ prAuthor = getPrAuthorFn(branch, token, { forgejoUser: reviewIdentity, reviewIdentity, rootDir: worktree });
249
+ }
250
+ catch {
251
+ prAuthor = null;
252
+ }
253
+ if (prAuthor && prAuthor === reviewIdentity) {
254
+ 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.`));
255
+ recordLocalReviewVerdict(slug, outcome, {
256
+ worktree,
257
+ reviewer: reviewIdentity,
258
+ writeReviewStateFn: options.writeReviewStateFn,
259
+ createEventFn: options.createEventFn,
260
+ readReviewStateFn,
261
+ log: log,
262
+ error,
263
+ });
264
+ return { ok: true, skipped: true, reason: 'self-author', prAuthor };
265
+ }
266
+ const taggedMessage = message + buildMetadataFooterFn(slug, worktree);
267
+ log(fmt.status('INFO', `Submitting review outcome "${outcome}" on ${branch} as ${reviewIdentity}...`));
268
+ const result = postReviewFn(branch, token, outcome, taggedMessage, { forgejoUser: reviewIdentity, reviewIdentity });
269
+ const r = result;
270
+ if (!r.ok) {
271
+ const failureDetail = describeProviderFailure(result);
272
+ error(fmt.status('FAIL', `Could not submit review: ${failureDetail}`));
273
+ return { ok: false, error: failureDetail };
274
+ }
275
+ log(fmt.status('PASS', `Review outcome "${outcome}" posted on PR for ${branch}.`));
276
+ return { ok: true };
313
277
  }
314
-
315
278
  // ============================================================================
316
279
  // Artifact Consumption
317
280
  // ============================================================================
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
- */
325
281
  async function consumeReviewerArtifacts(slug, reviewer, options = {}) {
326
- const log = options.log || fmt.log.plain;
327
- const error = options.error || fmt.log.plainError;
328
- const readArtifactFn = options.readArtifactFn || readArtifactFile;
329
- const deleteArtifactFn = options.deleteArtifactFn || deleteArtifactFile;
330
- const explicitTmpDir = options.tmpDir !== null && options.tmpDir !== undefined;
331
- const fallbackToTmp = options.fallbackToTmp === true;
332
- const worktree = options.worktree || resolveWorktree(slug) || process.cwd();
333
- const tmpDir = options.tmpDir || resolveArtifactDir(worktree);
334
- const providerEnabled = options.providerEnabled !== null && options.providerEnabled !== undefined
335
- ? options.providerEnabled
336
- : (options.forgejoEnabled !== null && options.forgejoEnabled !== undefined ? options.forgejoEnabled : isEnabled(worktree));
337
- const reviewStatePath = reviewStateFile(slug, worktree);
338
-
339
- const findingsResolved = resolveArtifactRead(slug, 'review-findings.md', { tmpDir, explicitTmpDir, fallbackToTmp, readArtifactFn });
340
- const outcomeResolved = resolveArtifactRead(slug, 'review-outcome.md', { tmpDir, explicitTmpDir, fallbackToTmp, readArtifactFn });
341
- const verdictResolved = resolveArtifactRead(slug, 'review-verdict.txt', { tmpDir, explicitTmpDir, fallbackToTmp, readArtifactFn });
342
- const findingsPath = findingsResolved.path;
343
- const outcomePath = outcomeResolved.path;
344
- const verdictPath = verdictResolved.path;
345
-
346
- const findings = findingsResolved.value;
347
- const outcomeMessage = outcomeResolved.value;
348
- const verdictRaw = verdictResolved.value;
349
-
350
- // Extract verdict from outcome message if review-verdict.txt is not provided
351
- // This allows a single review-outcome.md file to contain both the outcome and verdict
352
- let verdict = normalizeReviewVerdict(/** @type {string} */ (verdictRaw || ''));
353
- if (!verdict && outcomeMessage) {
354
- // Try to extract verdict from outcome content (e.g., "Verdict: approve" or frontmatter)
355
- const outcomeVerdictMatch = outcomeMessage.match(/^verdict:\s*(approve|request-changes|comment)/im) ||
356
- outcomeMessage.match(/Verdict:\s*(approve|request-changes|comment)/i);
357
- if (outcomeVerdictMatch) {
358
- verdict = normalizeReviewVerdict(outcomeVerdictMatch[1]);
359
- }
360
- }
361
-
362
- const hasAny = findings !== null || outcomeMessage !== null || verdictRaw !== null;
363
-
364
- if (!hasAny) {
365
- return { consumed: false };
366
- }
367
- if (!findings || !outcomeMessage) {
368
- if (!providerEnabled) {
369
- const statePathStr = reviewStatePath ? ` local review state at ${reviewStatePath}; ` : ' ';
370
- 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\`.`));
371
- } else {
372
- error(fmt.status('FAIL', `Incomplete reviewer artifacts for ${slug}. Expected ${findingsPath} and ${outcomePath}.`));
373
- }
374
- return { consumed: true, ok: false };
375
- }
376
- if (!verdict) {
282
+ const log = options.log || fmt.log.plain;
283
+ const error = options.error || fmt.log.plainError;
284
+ const readArtifactFn = options.readArtifactFn || readArtifactFile;
285
+ const deleteArtifactFn = options.deleteArtifactFn || deleteArtifactFile;
286
+ const explicitTmpDir = options.tmpDir !== null && options.tmpDir !== undefined;
287
+ const fallbackToTmp = options.fallbackToTmp === true;
288
+ const worktree = options.worktree || (0, mission_utils_js_1.resolveWorktree)(slug) || process.cwd();
289
+ const tmpDir = options.tmpDir || resolveArtifactDir(worktree);
290
+ const providerEnabled = options.providerEnabled !== null && options.providerEnabled !== undefined
291
+ ? options.providerEnabled
292
+ : (options.forgejoEnabled !== null && options.forgejoEnabled !== undefined ? options.forgejoEnabled : (0, review_adapter_js_1.isEnabled)(worktree));
293
+ const reviewStatePath = (0, review_state_js_1.reviewStateFile)(slug, worktree);
294
+ const findingsResolved = resolveArtifactRead(slug, 'review-findings.md', { tmpDir, explicitTmpDir, fallbackToTmp, readArtifactFn });
295
+ const outcomeResolved = resolveArtifactRead(slug, 'review-outcome.md', { tmpDir, explicitTmpDir, fallbackToTmp, readArtifactFn });
296
+ const verdictResolved = resolveArtifactRead(slug, 'review-verdict.txt', { tmpDir, explicitTmpDir, fallbackToTmp, readArtifactFn });
297
+ const findingsPath = findingsResolved.path;
298
+ const outcomePath = outcomeResolved.path;
299
+ const verdictPath = verdictResolved.path;
300
+ const findings = findingsResolved.value;
301
+ const outcomeMessage = outcomeResolved.value;
302
+ const verdictRaw = verdictResolved.value;
303
+ let verdict = normalizeReviewVerdict(verdictRaw || '');
304
+ if (!verdict && outcomeMessage) {
305
+ const outcomeVerdictMatch = outcomeMessage.match(/^verdict:\s*(approve|request-changes|comment)/im) ||
306
+ outcomeMessage.match(/Verdict:\s*(approve|request-changes|comment)/i);
307
+ if (outcomeVerdictMatch) {
308
+ verdict = normalizeReviewVerdict(outcomeVerdictMatch[1]);
309
+ }
310
+ }
311
+ const hasAny = findings !== null || outcomeMessage !== null || verdictRaw !== null;
312
+ if (!hasAny) {
313
+ return { consumed: false };
314
+ }
315
+ if (!findings || !outcomeMessage) {
316
+ if (!providerEnabled) {
317
+ const statePathStr = reviewStatePath ? ` local review state at ${reviewStatePath}; ` : ' ';
318
+ 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\`.`));
319
+ }
320
+ else {
321
+ error(fmt.status('FAIL', `Incomplete reviewer artifacts for ${slug}. Expected ${findingsPath} and ${outcomePath}.`));
322
+ }
323
+ return { consumed: true, ok: false };
324
+ }
325
+ if (!verdict) {
326
+ if (!providerEnabled) {
327
+ const statePathStr = reviewStatePath ? ` local review state at ${reviewStatePath}; ` : ' ';
328
+ 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\`.`));
329
+ }
330
+ else {
331
+ error(fmt.status('FAIL', `Reviewer artifacts for ${slug} missing verdict. Expected in ${verdictPath} or in ${outcomePath} content.`));
332
+ }
333
+ return { consumed: true, ok: false };
334
+ }
335
+ const currentState = (0, review_state_js_1.readReviewState)(slug, worktree);
336
+ const round = currentState ? currentState.round : 1;
337
+ const phase = currentState ? currentState.phase : 'reviewing';
338
+ const createEventFn = options.createEventFn || review_events_js_1.createEvent;
339
+ const findingsEventResult = createEventFn(slug, review_events_js_1.VALID_EVENT_TYPES.REVIEWER_FINDINGS, {
340
+ content: findings, round, phase, actor: reviewer
341
+ }, { worktree, skipGit: true, log: log, error });
342
+ if (!findingsEventResult.ok) {
343
+ error(fmt.status('FAIL', `Failed to persist reviewer findings to repo store: ${findingsEventResult.error}`));
344
+ return { consumed: true, ok: false };
345
+ }
346
+ const outcomeEventResult = createEventFn(slug, review_events_js_1.VALID_EVENT_TYPES.REVIEWER_OUTCOME, {
347
+ content: outcomeMessage, round, phase, actor: reviewer, verdict
348
+ }, { worktree, skipGit: true, log: log, error });
349
+ if (!outcomeEventResult.ok) {
350
+ error(fmt.status('FAIL', `Failed to persist reviewer outcome to repo store: ${outcomeEventResult.error}`));
351
+ return { consumed: true, ok: false };
352
+ }
353
+ log(fmt.status('INFO', `Persisted reviewer artifacts to repo store: ${findingsEventResult.path}, ${outcomeEventResult.path}`));
354
+ if (providerEnabled && options.readTokenFn && options.getCommentsFn) {
355
+ await (0, review_events_js_1.consumeHumanNotes)(slug, reviewer, {
356
+ getCommentsFn: options.getCommentsFn,
357
+ createEventFn: createEventFn,
358
+ readTokenFn: options.readTokenFn,
359
+ reviewIdentity: reviewer,
360
+ worktree,
361
+ log: log,
362
+ error
363
+ });
364
+ }
365
+ if (providerEnabled) {
366
+ const commentResult = postWorkflowComment(slug, findings, {
367
+ rootDir: worktree,
368
+ reviewIdentity: reviewer,
369
+ readTokenFn: options.readTokenFn,
370
+ postCommentFn: options.postCommentFn,
371
+ buildMetadataFooterFn: options.buildMetadataFooterFn,
372
+ log: log,
373
+ error
374
+ });
375
+ if (!commentResult.ok) {
376
+ return { consumed: true, ok: false };
377
+ }
378
+ const reviewResult = postWorkflowReview(slug, verdict, outcomeMessage, {
379
+ worktree,
380
+ reviewIdentity: reviewer,
381
+ readTokenFn: options.readTokenFn,
382
+ postReviewFn: options.postReviewFn,
383
+ buildMetadataFooterFn: options.buildMetadataFooterFn,
384
+ log: log,
385
+ error
386
+ });
387
+ if (!reviewResult.ok) {
388
+ return { consumed: true, ok: false };
389
+ }
390
+ }
391
+ else {
392
+ log(fmt.status('INFO', `Review provider disabled; skipping PR mirroring for ${slug}`));
393
+ }
394
+ deleteArtifactFn(findingsPath);
395
+ deleteArtifactFn(outcomePath);
396
+ deleteArtifactFn(verdictPath);
397
+ if (verdict === 'approve') {
398
+ return { consumed: true, ok: true, reviewState: 'APPROVED' };
399
+ }
400
+ if (verdict === 'request-changes') {
401
+ return { consumed: true, ok: true, reviewState: 'REQUEST_CHANGES' };
402
+ }
377
403
  if (!providerEnabled) {
378
- const statePathStr = reviewStatePath ? ` local review state at ${reviewStatePath}; ` : ' ';
379
- 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\`.`));
380
- } else {
381
- error(fmt.status('FAIL', `Reviewer artifacts for ${slug} missing verdict. Expected in ${verdictPath} or in ${outcomePath} content.`));
382
- }
383
- return { consumed: true, ok: false };
384
- }
385
-
386
- // Get current review state for round/phase metadata
387
- const currentState = readReviewState(slug, worktree);
388
- const round = currentState ? currentState.round : 1;
389
- const phase = currentState ? currentState.phase : 'reviewing';
390
-
391
- // SC 2: Persist to repo-owned store BEFORE provider mirroring
392
- // This ensures durable storage even if provider publication fails
393
- const createEventFn = options.createEventFn || createEvent;
394
- const findingsEventResult = createEventFn(slug, VALID_EVENT_TYPES.REVIEWER_FINDINGS, {
395
- content: findings,
396
- round,
397
- phase,
398
- actor: reviewer
399
- }, {
400
- worktree,
401
- skipGit: true, // Let the caller handle git; we just need the file persisted
402
- log,
403
- error
404
- });
405
-
406
- if (!findingsEventResult.ok) {
407
- error(fmt.status('FAIL', `Failed to persist reviewer findings to repo store: ${findingsEventResult.error}`));
408
- return { consumed: true, ok: false };
409
- }
410
-
411
- const outcomeEventResult = createEventFn(slug, VALID_EVENT_TYPES.REVIEWER_OUTCOME, {
412
- content: outcomeMessage,
413
- round,
414
- phase,
415
- actor: reviewer,
416
- verdict
417
- }, {
418
- worktree,
419
- skipGit: true,
420
- log,
421
- error
422
- });
423
-
424
- if (!outcomeEventResult.ok) {
425
- error(fmt.status('FAIL', `Failed to persist reviewer outcome to repo store: ${outcomeEventResult.error}`));
426
- return { consumed: true, ok: false };
427
- }
428
-
429
- log(fmt.status('INFO', `Persisted reviewer artifacts to repo store: ${findingsEventResult.path}, ${outcomeEventResult.path}`));
430
-
431
- // SC 6: Classify human comments before mirroring (best-effort)
432
- // Only read provider comments when a provider is enabled.
433
- if (providerEnabled && options.readTokenFn && options.getCommentsFn) {
434
- await consumeHumanNotes(slug, reviewer, {
435
- getCommentsFn: options.getCommentsFn,
436
- createEventFn,
437
- readTokenFn: options.readTokenFn,
438
- reviewIdentity: reviewer,
439
- worktree,
440
- log,
441
- error
442
- });
443
- }
444
-
445
- // Mirror to the configured review provider (SC 4) when enabled.
446
- if (providerEnabled) {
447
- // Both reviewer_findings and reviewer_outcome are mirrored
448
- const commentResult = postWorkflowComment(slug, findings, {
449
- rootDir: worktree,
450
- reviewIdentity: reviewer,
451
- readTokenFn: options.readTokenFn,
452
- postCommentFn: options.postCommentFn,
453
- buildMetadataFooterFn: options.buildMetadataFooterFn,
454
- log,
455
- error
456
- });
457
- if (!commentResult.ok) {
458
- return { consumed: true, ok: false };
459
- }
460
-
461
- const reviewResult = postWorkflowReview(slug, verdict, outcomeMessage, {
462
- worktree,
463
- reviewIdentity: reviewer,
464
- readTokenFn: options.readTokenFn,
465
- postReviewFn: options.postReviewFn,
466
- buildMetadataFooterFn: options.buildMetadataFooterFn,
467
- log,
468
- error
469
- });
470
- if (!reviewResult.ok) {
471
- return { consumed: true, ok: false };
472
- }
473
- } else {
474
- log(fmt.status('INFO', `Review provider disabled; skipping PR mirroring for ${slug}`));
475
- }
476
-
477
- deleteArtifactFn(findingsPath);
478
- deleteArtifactFn(outcomePath);
479
- deleteArtifactFn(verdictPath);
480
-
481
- if (verdict === 'approve') {return { consumed: true, ok: true, reviewState: 'APPROVED' };}
482
- if (verdict === 'request-changes') {return { consumed: true, ok: true, reviewState: 'REQUEST_CHANGES' };}
483
- // Defensive fallback: a stray `comment` verdict (prompts no longer offer it) cannot be
484
- // resolved via provider polling when the provider is disabled, so normalize it to the
485
- // disposition format and pass it through. inferPhaseFromDisposition() maps COMMENT -> fixing.
486
- if (!providerEnabled) {
487
- const reviewState = verdict.toUpperCase().replace(/-/g, '_');
488
- log(fmt.status('INFO', `Reviewer ${reviewer} produced verdict "${verdict}" with the provider disabled; normalizing to ${reviewState} for loop control.`));
489
- return { consumed: true, ok: true, reviewState };
490
- }
491
- log(fmt.status('WARN', `Reviewer ${reviewer} produced verdict "${verdict}". Falling back to provider polling for loop control.`));
492
- return { consumed: true, ok: true, reviewState: null };
404
+ const reviewState = verdict.toUpperCase().replace(/-/g, '_');
405
+ log(fmt.status('INFO', `Reviewer ${reviewer} produced verdict "${verdict}" with the provider disabled; normalizing to ${reviewState} for loop control.`));
406
+ return { consumed: true, ok: true, reviewState };
407
+ }
408
+ log(fmt.status('WARN', `Reviewer ${reviewer} produced verdict "${verdict}". Falling back to provider polling for loop control.`));
409
+ return { consumed: true, ok: true, reviewState: null };
493
410
  }
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
- */
501
411
  async function consumeImplementerArtifacts(slug, implementer, options = {}) {
502
- const log = options.log || fmt.log.plain;
503
- const error = options.error || fmt.log.plainError;
504
- const readArtifactFn = options.readArtifactFn || readArtifactFile;
505
- const deleteArtifactFn = options.deleteArtifactFn || deleteArtifactFile;
506
- const explicitTmpDir = options.tmpDir !== null && options.tmpDir !== undefined;
507
- const fallbackToTmp = options.fallbackToTmp === true;
508
- const worktree = options.worktree || resolveWorktree(slug) || process.cwd();
509
- const tmpDir = options.tmpDir || resolveArtifactDir(worktree);
510
- const providerEnabled = options.providerEnabled !== null && options.providerEnabled !== undefined
511
- ? options.providerEnabled
512
- : (options.forgejoEnabled !== null && options.forgejoEnabled !== undefined ? options.forgejoEnabled : isEnabled(worktree));
513
- const reviewStatePath = reviewStateFile(slug, worktree);
514
-
515
- const resolutionResolved = resolveArtifactRead(slug, 'round-resolution.md', { tmpDir, explicitTmpDir, fallbackToTmp, readArtifactFn });
516
- const dispositionResolved = resolveArtifactRead(slug, 'review-disposition.txt', { tmpDir, explicitTmpDir, fallbackToTmp, readArtifactFn });
517
- const resolutionPath = resolutionResolved.path;
518
- const dispositionPath = dispositionResolved.path;
519
-
520
- const resolution = resolutionResolved.value;
521
- const dispositionRaw = dispositionResolved.value;
522
- const disposition = normalizeDisposition(/** @type {string} */ (dispositionRaw || ''));
523
- const hasAny = resolution !== null || dispositionRaw !== null;
524
-
525
- if (!hasAny) {
526
- return { consumed: false };
527
- }
528
- if (!resolution || !disposition) {
529
- if (!providerEnabled) {
530
- const statePathStr = reviewStatePath ? ` local review state at ${reviewStatePath}; ` : ' ';
531
- 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\`.`));
532
- } else {
533
- error(fmt.status('FAIL', `Incomplete implementer artifacts for ${slug}. Expected ${resolutionPath} and ${dispositionPath}.`));
534
- }
535
- return { consumed: true, ok: false };
536
- }
537
-
538
- // Get current review state for round/phase metadata
539
- const currentState = readReviewState(slug, worktree);
540
- const round = currentState ? currentState.round : 1;
541
- const phase = currentState ? currentState.phase : 'fixing';
542
-
543
- // SC 2: Persist to repo-owned store BEFORE provider mirroring
544
- // This ensures durable storage even if provider publication fails
545
- const createEventFn = options.createEventFn || createEvent;
546
-
547
- // Parse resolution content to extract structured fields if present
548
- // SC 5: Validate mirrored content includes required fields
549
- let fixedItems = [];
550
- let pushedBackItems = [];
551
- let parkedItems = [];
552
- let blockedReason = null;
553
-
554
- try {
555
- // Try to extract from frontmatter or structured content
556
- const fixedMatch = resolution.match(/fixed_items:\s*(\[[^\]]*\])/i);
557
- const pushedMatch = resolution.match(/pushed_back_items:\s*(\[[^\]]*\])/i);
558
- const parkedMatch = resolution.match(/parked_items:\s*(\[[^\]]*\])/i);
559
- const blockedMatch = resolution.match(/blocked_reason:\s*"([^"]*)"/i);
560
-
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];}
565
- } catch (_) {
566
- // If parsing fails, use empty arrays
567
- fixedItems = [];
568
- pushedBackItems = [];
569
- parkedItems = [];
570
- }
571
-
572
- // For BLOCKED disposition, blockedReason is required
573
- if (disposition === 'BLOCKED' && !blockedReason) {
574
- // Check if it's in the resolution content
575
- const match = resolution.match(/blocked.*?:\s*(.+)/i);
576
- if (match) {blockedReason = match[1].trim();}
577
- }
578
-
579
- const summaryEventResult = createEventFn(slug, VALID_EVENT_TYPES.IMPLEMENTER_ROUND_SUMMARY, {
580
- content: resolution,
581
- round,
582
- phase,
583
- actor: implementer,
584
- fixedItems,
585
- pushedBackItems,
586
- parkedItems,
587
- ...(disposition === 'BLOCKED' && blockedReason ? { blockedReason } : {})
588
- }, {
589
- worktree,
590
- skipGit: true,
591
- log,
592
- error
593
- });
594
-
595
- if (!summaryEventResult.ok) {
596
- error(fmt.status('FAIL', `Failed to persist implementer round summary to repo store: ${summaryEventResult.error}`));
597
- return { consumed: true, ok: false };
598
- }
599
-
600
- const dispositionEventResult = createEventFn(slug, VALID_EVENT_TYPES.IMPLEMENTER_DISPOSITION, {
601
- content: `Autonomous review disposition: ${disposition}`,
602
- round,
603
- phase,
604
- actor: implementer,
605
- disposition
606
- }, {
607
- worktree,
608
- skipGit: true,
609
- log,
610
- error
611
- });
612
-
613
- if (!dispositionEventResult.ok) {
614
- error(fmt.status('FAIL', `Failed to persist implementer disposition to repo store: ${dispositionEventResult.error}`));
615
- return { consumed: true, ok: false };
616
- }
617
-
618
- log(fmt.status('INFO', `Persisted implementer artifacts to repo store: ${summaryEventResult.path}, ${dispositionEventResult.path}`));
619
-
620
- // SC 6: Classify human comments before mirroring (best-effort)
621
- // Only read provider comments when a provider is enabled.
622
- if (providerEnabled && options.readTokenFn && options.getCommentsFn) {
623
- await consumeHumanNotes(slug, implementer, {
624
- getCommentsFn: options.getCommentsFn,
625
- createEventFn,
626
- readTokenFn: options.readTokenFn,
627
- reviewIdentity: implementer,
628
- worktree,
629
- log,
630
- error
631
- });
632
- }
633
-
634
- // Mirror to the configured review provider (SC 4) when enabled.
635
- if (providerEnabled) {
636
- // Both implementer_round_summary and implementer_disposition are mirrored
637
- const resolutionResult = postWorkflowComment(slug, resolution, {
638
- rootDir: worktree,
639
- reviewIdentity: implementer,
640
- readTokenFn: options.readTokenFn,
641
- postCommentFn: options.postCommentFn,
642
- buildMetadataFooterFn: options.buildMetadataFooterFn,
643
- log,
644
- error
645
- });
646
- if (!resolutionResult.ok) {
647
- return { consumed: true, ok: false };
648
- }
649
-
650
- const dispositionResult = postWorkflowComment(slug, `Autonomous review disposition: ${disposition}`, {
651
- rootDir: worktree,
652
- reviewIdentity: implementer,
653
- readTokenFn: options.readTokenFn,
654
- postCommentFn: options.postCommentFn,
655
- buildMetadataFooterFn: options.buildMetadataFooterFn,
656
- log,
657
- error
658
- });
659
- if (!dispositionResult.ok) {
660
- return { consumed: true, ok: false };
661
- }
662
- } else {
663
- log(fmt.status('INFO', `Review provider disabled; skipping PR mirroring for ${slug}`));
664
- }
665
-
666
- deleteArtifactFn(resolutionPath);
667
- deleteArtifactFn(dispositionPath);
668
-
669
- return { consumed: true, ok: true, disposition };
412
+ const log = options.log || fmt.log.plain;
413
+ const error = options.error || fmt.log.plainError;
414
+ const readArtifactFn = options.readArtifactFn || readArtifactFile;
415
+ const deleteArtifactFn = options.deleteArtifactFn || deleteArtifactFile;
416
+ const explicitTmpDir = options.tmpDir !== null && options.tmpDir !== undefined;
417
+ const fallbackToTmp = options.fallbackToTmp === true;
418
+ const worktree = options.worktree || (0, mission_utils_js_1.resolveWorktree)(slug) || process.cwd();
419
+ const tmpDir = options.tmpDir || resolveArtifactDir(worktree);
420
+ const providerEnabled = options.providerEnabled !== null && options.providerEnabled !== undefined
421
+ ? options.providerEnabled
422
+ : (options.forgejoEnabled !== null && options.forgejoEnabled !== undefined ? options.forgejoEnabled : (0, review_adapter_js_1.isEnabled)(worktree));
423
+ const reviewStatePath = (0, review_state_js_1.reviewStateFile)(slug, worktree);
424
+ const resolutionResolved = resolveArtifactRead(slug, 'round-resolution.md', { tmpDir, explicitTmpDir, fallbackToTmp, readArtifactFn });
425
+ const dispositionResolved = resolveArtifactRead(slug, 'review-disposition.txt', { tmpDir, explicitTmpDir, fallbackToTmp, readArtifactFn });
426
+ const resolutionPath = resolutionResolved.path;
427
+ const dispositionPath = dispositionResolved.path;
428
+ const resolution = resolutionResolved.value;
429
+ const dispositionRaw = dispositionResolved.value;
430
+ const disposition = normalizeDisposition(dispositionRaw || '');
431
+ const hasAny = resolution !== null || dispositionRaw !== null;
432
+ if (!hasAny) {
433
+ return { consumed: false };
434
+ }
435
+ if (!resolution || !disposition) {
436
+ if (!providerEnabled) {
437
+ const statePathStr = reviewStatePath ? ` local review state at ${reviewStatePath}; ` : ' ';
438
+ 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\`.`));
439
+ }
440
+ else {
441
+ error(fmt.status('FAIL', `Incomplete implementer artifacts for ${slug}. Expected ${resolutionPath} and ${dispositionPath}.`));
442
+ }
443
+ return { consumed: true, ok: false };
444
+ }
445
+ const currentState = (0, review_state_js_1.readReviewState)(slug, worktree);
446
+ const round = currentState ? currentState.round : 1;
447
+ const phase = currentState ? currentState.phase : 'fixing';
448
+ const createEventFn = options.createEventFn || review_events_js_1.createEvent;
449
+ let fixedItems = [];
450
+ let pushedBackItems = [];
451
+ let parkedItems = [];
452
+ let blockedReason = null;
453
+ try {
454
+ const fixedMatch = resolution.match(/fixed_items:\s*(\[[^\]]*\])/i);
455
+ const pushedMatch = resolution.match(/pushed_back_items:\s*(\[[^\]]*\])/i);
456
+ const parkedMatch = resolution.match(/parked_items:\s*(\[[^\]]*\])/i);
457
+ const blockedMatch = resolution.match(/blocked_reason:\s*"([^"]*)"/i);
458
+ if (fixedMatch) {
459
+ fixedItems = JSON.parse(fixedMatch[1]);
460
+ }
461
+ if (pushedMatch) {
462
+ pushedBackItems = JSON.parse(pushedMatch[1]);
463
+ }
464
+ if (parkedMatch) {
465
+ parkedItems = JSON.parse(parkedMatch[1]);
466
+ }
467
+ if (blockedMatch) {
468
+ blockedReason = blockedMatch[1];
469
+ }
470
+ }
471
+ catch {
472
+ fixedItems = [];
473
+ pushedBackItems = [];
474
+ parkedItems = [];
475
+ }
476
+ if (disposition === 'BLOCKED' && !blockedReason) {
477
+ const match = resolution.match(/blocked.*?:\s*(.+)/i);
478
+ if (match) {
479
+ blockedReason = match[1].trim();
480
+ }
481
+ }
482
+ const summaryEventResult = createEventFn(slug, review_events_js_1.VALID_EVENT_TYPES.IMPLEMENTER_ROUND_SUMMARY, {
483
+ content: resolution, round, phase, actor: implementer,
484
+ fixedItems, pushedBackItems, parkedItems,
485
+ ...(disposition === 'BLOCKED' && blockedReason ? { blockedReason } : {})
486
+ }, { worktree, skipGit: true, log: log, error });
487
+ if (!summaryEventResult.ok) {
488
+ error(fmt.status('FAIL', `Failed to persist implementer round summary to repo store: ${summaryEventResult.error}`));
489
+ return { consumed: true, ok: false };
490
+ }
491
+ const dispositionEventResult = createEventFn(slug, review_events_js_1.VALID_EVENT_TYPES.IMPLEMENTER_DISPOSITION, {
492
+ content: `Autonomous review disposition: ${disposition}`, round, phase, actor: implementer, disposition
493
+ }, { worktree, skipGit: true, log: log, error });
494
+ if (!dispositionEventResult.ok) {
495
+ error(fmt.status('FAIL', `Failed to persist implementer disposition to repo store: ${dispositionEventResult.error}`));
496
+ return { consumed: true, ok: false };
497
+ }
498
+ log(fmt.status('INFO', `Persisted implementer artifacts to repo store: ${summaryEventResult.path}, ${dispositionEventResult.path}`));
499
+ if (providerEnabled && options.readTokenFn && options.getCommentsFn) {
500
+ await (0, review_events_js_1.consumeHumanNotes)(slug, implementer, {
501
+ getCommentsFn: options.getCommentsFn,
502
+ createEventFn: createEventFn,
503
+ readTokenFn: options.readTokenFn,
504
+ reviewIdentity: implementer,
505
+ worktree,
506
+ log: log,
507
+ error
508
+ });
509
+ }
510
+ if (providerEnabled) {
511
+ const resolutionResult = postWorkflowComment(slug, resolution, {
512
+ rootDir: worktree,
513
+ reviewIdentity: implementer,
514
+ readTokenFn: options.readTokenFn,
515
+ postCommentFn: options.postCommentFn,
516
+ buildMetadataFooterFn: options.buildMetadataFooterFn,
517
+ log: log,
518
+ error
519
+ });
520
+ if (!resolutionResult.ok) {
521
+ return { consumed: true, ok: false };
522
+ }
523
+ const dispositionResult = postWorkflowComment(slug, `Autonomous review disposition: ${disposition}`, {
524
+ rootDir: worktree,
525
+ reviewIdentity: implementer,
526
+ readTokenFn: options.readTokenFn,
527
+ postCommentFn: options.postCommentFn,
528
+ buildMetadataFooterFn: options.buildMetadataFooterFn,
529
+ log: log,
530
+ error
531
+ });
532
+ if (!dispositionResult.ok) {
533
+ return { consumed: true, ok: false };
534
+ }
535
+ }
536
+ else {
537
+ log(fmt.status('INFO', `Review provider disabled; skipping PR mirroring for ${slug}`));
538
+ }
539
+ deleteArtifactFn(resolutionPath);
540
+ deleteArtifactFn(dispositionPath);
541
+ return { consumed: true, ok: true, disposition };
670
542
  }
671
-
672
- // Module exports
673
- module.exports = {
674
- buildMetadataFooter,
675
- reviewArtifactPath,
676
- resolveArtifactDir,
677
- readArtifactFile,
678
- deleteArtifactFile,
679
- normalizeReviewVerdict,
680
- normalizeDisposition,
681
- postWorkflowComment,
682
- postWorkflowReview,
683
- consumeReviewerArtifacts,
684
- consumeImplementerArtifacts
685
- };