@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.
- package/index.js +190 -185
- package/lib/agents/agents.js +737 -838
- package/lib/agents/agents.ts +930 -0
- package/lib/agents/claude-telemetry.js +149 -154
- package/lib/agents/claude-telemetry.ts +233 -0
- package/lib/agents/claude.js +121 -131
- package/lib/agents/claude.ts +156 -0
- package/lib/agents/codex-telemetry.js +176 -164
- package/lib/agents/codex-telemetry.ts +205 -0
- package/lib/agents/codex.js +168 -201
- package/lib/agents/codex.ts +236 -0
- package/lib/agents/limit-hit.js +216 -226
- package/lib/agents/limit-hit.ts +262 -0
- package/lib/agents/mistral-telemetry.js +7 -10
- package/lib/agents/mistral-telemetry.ts +44 -0
- package/lib/agents/mistral.js +40 -58
- package/lib/agents/mistral.ts +84 -0
- package/lib/agents/opencode-export.js +126 -127
- package/lib/agents/opencode-export.ts +160 -0
- package/lib/agents/opencode-telemetry.js +282 -261
- package/lib/agents/opencode-telemetry.ts +350 -0
- package/lib/agents/opencode.js +263 -305
- package/lib/agents/opencode.ts +370 -0
- package/lib/agents/stage-telemetry.js +23 -24
- package/lib/agents/stage-telemetry.ts +47 -0
- package/lib/commands/active.js +502 -579
- package/lib/commands/active.ts +665 -0
- package/lib/commands/checkpoint.js +98 -74
- package/lib/commands/checkpoint.ts +79 -0
- package/lib/commands/config.js +61 -31
- package/lib/commands/config.ts +47 -0
- package/lib/commands/coverage-gate.js +271 -246
- package/lib/commands/coverage-gate.ts +362 -0
- package/lib/commands/diff.js +140 -119
- package/lib/commands/diff.ts +125 -0
- package/lib/commands/draft.js +770 -923
- package/lib/commands/draft.ts +1021 -0
- package/lib/commands/handoff.js +588 -608
- package/lib/commands/handoff.ts +694 -0
- package/lib/commands/integrate.js +1332 -1463
- package/lib/commands/integrate.ts +1688 -0
- package/lib/commands/mission-start.js +279 -237
- package/lib/commands/mission-start.ts +263 -0
- package/lib/commands/rebase.js +606 -582
- package/lib/commands/rebase.ts +635 -0
- package/lib/commands/repair-handoff.js +181 -179
- package/lib/commands/repair-handoff.ts +228 -0
- package/lib/commands/resolve-conflict.js +121 -102
- package/lib/commands/resolve-conflict.ts +112 -0
- package/lib/commands/review.js +3 -4
- package/lib/commands/review.ts +14 -0
- package/lib/commands/setup-review.js +4 -5
- package/lib/commands/setup-review.ts +14 -0
- package/lib/commands/setup.js +3 -3
- package/lib/commands/setup.ts +2 -0
- package/lib/commands/stats-backfill.js +394 -359
- package/lib/commands/stats-backfill.ts +418 -0
- package/lib/commands/stats.js +1424 -1665
- package/lib/commands/stats.ts +2186 -0
- package/lib/commands/status.js +219 -200
- package/lib/commands/status.ts +219 -0
- package/lib/commands/verify.js +3 -1
- package/lib/commands/verify.ts +2 -0
- package/lib/core/fmt.js +140 -211
- package/lib/core/fmt.ts +189 -0
- package/lib/core/git.js +121 -102
- package/lib/core/git.ts +146 -0
- package/lib/core/gitignore.js +59 -106
- package/lib/core/gitignore.ts +103 -0
- package/lib/core/mission-utils.js +833 -875
- package/lib/core/mission-utils.ts +998 -0
- package/lib/core/nels.js +112 -127
- package/lib/core/nels.ts +189 -0
- package/lib/core/persistent-data-migration.js +247 -205
- package/lib/core/persistent-data-migration.ts +255 -0
- package/lib/core/product-config.js +400 -476
- package/lib/core/product-config.ts +518 -0
- package/lib/core/runtime-matrix.js +41 -87
- package/lib/core/runtime-matrix.ts +84 -0
- package/lib/core/spawn-tee.js +152 -192
- package/lib/core/spawn-tee.ts +202 -0
- package/lib/core/state-map.js +92 -92
- package/lib/core/state-map.ts +115 -0
- package/lib/core/storage.js +101 -129
- package/lib/core/storage.ts +182 -0
- package/lib/core/subagent-limit.js +15 -16
- package/lib/core/subagent-limit.ts +25 -0
- package/lib/core/verification.js +148 -138
- package/lib/core/verification.ts +200 -0
- package/lib/index.js +142 -59
- package/lib/index.ts +134 -0
- package/lib/review/rebase.js +142 -145
- package/lib/review/rebase.ts +188 -0
- package/lib/review/review-adapter.js +99 -87
- package/lib/review/review-adapter.ts +198 -0
- package/lib/review/review-artifacts.js +486 -629
- package/lib/review/review-artifacts.ts +622 -0
- package/lib/review/review-commands.js +1220 -1373
- package/lib/review/review-commands.ts +1579 -0
- package/lib/review/review-events.js +544 -886
- package/lib/review/review-events.ts +954 -0
- package/lib/review/review-loop.js +945 -1020
- package/lib/review/review-loop.ts +1174 -0
- package/lib/review/review-polling.js +86 -115
- package/lib/review/review-polling.ts +194 -0
- package/lib/review/review-prompts.js +154 -189
- package/lib/review/review-prompts.ts +182 -0
- package/lib/review/review-state.js +246 -225
- package/lib/review/review-state.ts +353 -0
- package/lib/review/review.js +63 -84
- package/lib/review/review.ts +138 -0
- package/lib/tools/backlog.js +694 -680
- package/lib/tools/backlog.ts +835 -0
- package/lib/tools/forgejo.js +1270 -1405
- package/lib/tools/forgejo.ts +1793 -0
- package/lib/tools/gatekeeper.js +106 -96
- package/lib/tools/gatekeeper.ts +124 -0
- package/lib/tools/redgreen.js +200 -181
- package/lib/tools/redgreen.ts +221 -0
- package/lib/tools/sessions.js +58 -52
- package/lib/tools/sessions.ts +81 -0
- package/lib/tools/setup-review.js +902 -975
- package/lib/tools/setup-review.ts +1152 -0
- package/package.json +7 -5
- package/prompts/draft.md +1 -0
- package/prompts/portfolio.md +1 -1
- 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
|
+
};
|