@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
|
@@ -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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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,
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
134
|
-
|
|
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
|
-
|
|
142
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
body =
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
}
|
|
383
|
-
return { consumed: true, ok:
|
|
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
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
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
|
-
};
|