@magnusekdahl/parallix 1.2.1 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +190 -185
- package/lib/agents/agents.js +34 -4
- package/lib/agents/agents.ts +40 -5
- 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 +1331 -1468
- 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/nels.js +112 -127
- package/lib/core/nels.ts +189 -0
- package/lib/core/subagent-limit.js +15 -16
- package/lib/core/subagent-limit.ts +25 -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 +4 -4
- package/px.js +226 -208
|
@@ -1,33 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
1
2
|
/**
|
|
2
3
|
* Review Loop Module
|
|
3
4
|
* Extracted from parallix/lib/review.js for task-1201
|
|
4
5
|
* Handles autonomous review loop orchestration.
|
|
5
6
|
*/
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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.rebaseBeforeReviewRound = exports.commitSafeMissionArtifacts = void 0;
|
|
42
|
+
exports.recordStageStatsSafe = recordStageStatsSafe;
|
|
43
|
+
exports.maybeUpdateGraphifyBeforeReview = maybeUpdateGraphifyBeforeReview;
|
|
44
|
+
exports.applyAgentFallback = applyAgentFallback;
|
|
45
|
+
exports.persistNormalizedPhaseRepair = persistNormalizedPhaseRepair;
|
|
46
|
+
exports.stageLaunchSinceMs = stageLaunchSinceMs;
|
|
47
|
+
exports.startReviewLoop = startReviewLoop;
|
|
48
|
+
const fs = __importStar(require("fs"));
|
|
49
|
+
const path = __importStar(require("path"));
|
|
50
|
+
const fmt = __importStar(require("../core/fmt.js"));
|
|
51
|
+
const git_js_1 = require("../core/git.js");
|
|
52
|
+
const mission_utils_js_1 = require("../core/mission-utils.js");
|
|
53
|
+
const backlog_js_1 = require("../tools/backlog.js");
|
|
54
|
+
const state_map_js_1 = require("../core/state-map.js");
|
|
55
|
+
const review_adapter_js_1 = require("./review-adapter.js");
|
|
56
|
+
const runtime_matrix_js_1 = require("../core/runtime-matrix.js");
|
|
57
|
+
const review_prompts_js_1 = require("./review-prompts.js");
|
|
58
|
+
const review_state_js_1 = require("./review-state.js");
|
|
59
|
+
const agents_js_1 = require("../agents/agents.js");
|
|
60
|
+
const rebase_js_1 = require("./rebase.js");
|
|
61
|
+
Object.defineProperty(exports, "commitSafeMissionArtifacts", { enumerable: true, get: function () { return rebase_js_1.commitSafeMissionArtifacts; } });
|
|
62
|
+
Object.defineProperty(exports, "rebaseBeforeReviewRound", { enumerable: true, get: function () { return rebase_js_1.rebaseBeforeReviewRound; } });
|
|
63
|
+
const product_config_js_1 = require("../core/product-config.js");
|
|
64
|
+
const review_polling_js_1 = require("./review-polling.js");
|
|
65
|
+
const review_artifacts_js_1 = require("./review-artifacts.js");
|
|
66
|
+
const stage_telemetry_js_1 = require("../agents/stage-telemetry.js");
|
|
67
|
+
/** Lazily loaded stats module — loaded on first use to avoid circular dependency. */
|
|
68
|
+
let _stats = null;
|
|
69
|
+
async function getStats() {
|
|
70
|
+
if (!_stats) {
|
|
71
|
+
_stats = await Promise.resolve().then(() => __importStar(require('../commands/stats.js')));
|
|
72
|
+
}
|
|
73
|
+
return _stats;
|
|
74
|
+
}
|
|
75
|
+
/** Lazily loaded handoff module. */
|
|
76
|
+
let _handoff = null;
|
|
77
|
+
async function getHandoff() {
|
|
78
|
+
if (!_handoff) {
|
|
79
|
+
_handoff = await Promise.resolve().then(() => __importStar(require('../commands/handoff.js')));
|
|
80
|
+
}
|
|
81
|
+
return _handoff;
|
|
82
|
+
}
|
|
31
83
|
// Stage telemetry recording is best-effort: a failure must never break the
|
|
32
84
|
// review loop. Token columns are populated only for the codex role (the C2 rule
|
|
33
85
|
// guarantees at most one of implementer/reviewer is codex); other families
|
|
@@ -39,172 +91,129 @@ const { resolveStageTelemetry } = require('../agents/stage-telemetry');
|
|
|
39
91
|
// one launch at a time, de-duped via review-state metadata, so Claude/custom rows
|
|
40
92
|
// are cumulative too. A family switch creates a separate row instead of mixing
|
|
41
93
|
// agents in one record.
|
|
42
|
-
/**
|
|
43
|
-
* @param {string} agentFamily
|
|
44
|
-
* @param {{sessionId?: string, startedAt?: string, endedAt?: string, status?: number}} result
|
|
45
|
-
* @returns {string}
|
|
46
|
-
*/
|
|
47
94
|
function stageLaunchFingerprint(agentFamily, result) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
95
|
+
return [
|
|
96
|
+
String(agentFamily || '').trim().toLowerCase(),
|
|
97
|
+
result && result.sessionId ? String(result.sessionId) : '',
|
|
98
|
+
result && result.startedAt ? String(result.startedAt) : '',
|
|
99
|
+
result && result.endedAt ? String(result.endedAt) : '',
|
|
100
|
+
result && result.status !== undefined && result.status !== null ? String(result.status) : '',
|
|
101
|
+
].join('|');
|
|
55
102
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
stateObj.metadata.recordedStageLaunches = {
|
|
80
|
-
...(stateObj.metadata.recordedStageLaunches || {}),
|
|
81
|
-
[key]: [...recorded, fingerprint].slice(-20),
|
|
82
|
-
};
|
|
83
|
-
writeReviewStateFn(slug, state, worktree);
|
|
84
|
-
return true;
|
|
103
|
+
function markStageLaunchRecorded(state, opts) {
|
|
104
|
+
const { stage, agentFamily, result, slug, worktree, writeReviewStateFn = review_state_js_1.writeReviewState } = opts || {};
|
|
105
|
+
if (!state || !agentFamily) {
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
const key = stageWindowKey(stage, agentFamily);
|
|
109
|
+
const fingerprint = stageLaunchFingerprint(agentFamily, result || null);
|
|
110
|
+
const recordedStageLaunches = state.metadata && typeof state.metadata === 'object'
|
|
111
|
+
? (state.metadata.recordedStageLaunches || {})
|
|
112
|
+
: {};
|
|
113
|
+
const recorded = Array.isArray(recordedStageLaunches[key]) ? recordedStageLaunches[key] : [];
|
|
114
|
+
if (recorded.includes(fingerprint)) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
if (!state.metadata || typeof state.metadata !== 'object') {
|
|
118
|
+
state.metadata = {};
|
|
119
|
+
}
|
|
120
|
+
state.metadata.recordedStageLaunches = {
|
|
121
|
+
...(state.metadata.recordedStageLaunches || {}),
|
|
122
|
+
[key]: [...recorded, fingerprint].slice(-20),
|
|
123
|
+
};
|
|
124
|
+
writeReviewStateFn(slug, state, worktree || process.cwd());
|
|
125
|
+
return true;
|
|
85
126
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
slug,
|
|
110
|
-
worktree,
|
|
111
|
-
writeReviewStateFn
|
|
112
|
-
})) {
|
|
113
|
-
return;
|
|
127
|
+
function recordStageStatsSafe(kind, opts) {
|
|
128
|
+
const { stage, slug, rootDir, worktree, implementer, reviewer, result, sinceMs, log, state, writeReviewStateFn = review_state_js_1.writeReviewState, model = null } = opts;
|
|
129
|
+
let durationMinutes = 0;
|
|
130
|
+
if (result && result.startedAt && result.endedAt) {
|
|
131
|
+
durationMinutes = (Date.parse(result.endedAt) - Date.parse(result.startedAt)) / 60000;
|
|
132
|
+
}
|
|
133
|
+
const telemetry = (0, stage_telemetry_js_1.resolveStageTelemetry)({ worktree: worktree || '', result: result || {}, sinceMs: sinceMs || 0 });
|
|
134
|
+
try {
|
|
135
|
+
const actorFamily = kind === 'review' ? reviewer : implementer;
|
|
136
|
+
if (state && actorFamily && !markStageLaunchRecorded(state, {
|
|
137
|
+
stage,
|
|
138
|
+
agentFamily: actorFamily,
|
|
139
|
+
result,
|
|
140
|
+
slug,
|
|
141
|
+
worktree,
|
|
142
|
+
writeReviewStateFn
|
|
143
|
+
})) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
getStats().then(s => s.accumulateStageStats({ stage, slug, rootDir, implementer, reviewer, telemetry, durationMinutes, model })).catch(() => { });
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
log?.(fmt.status('WARN', `Could not record ${kind} stats for ${slug}: ${err.message}`));
|
|
114
150
|
}
|
|
115
|
-
/** @type {any} */ (stats).accumulateStageStats({ stage, slug, rootDir, implementer, reviewer, telemetry, durationMinutes, model });
|
|
116
|
-
} catch (/** @type {unknown} */ err) {
|
|
117
|
-
// Best-effort: never escalate to the fatal `error` channel.
|
|
118
|
-
log?.(fmt.status('WARN', `Could not record ${kind} stats for ${slug}: ${/** @type {Error} */(err).message}`));
|
|
119
|
-
}
|
|
120
151
|
}
|
|
121
|
-
|
|
122
152
|
const DEFAULT_MAX_ATTEMPTS = 5;
|
|
123
153
|
const CONTINUE_SKIP_CHECK_TIMEOUT_MS = 10_000;
|
|
124
|
-
|
|
125
|
-
/** @param {string} earlierIso @param {number} [nowMs] */
|
|
126
154
|
function strictlyLaterIso(earlierIso, nowMs = Date.now()) {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
155
|
+
const earlierMs = Date.parse(earlierIso);
|
|
156
|
+
if (!Number.isFinite(earlierMs)) {
|
|
157
|
+
return new Date(nowMs).toISOString();
|
|
158
|
+
}
|
|
159
|
+
return new Date(Math.max(nowMs, earlierMs + 1)).toISOString();
|
|
132
160
|
}
|
|
133
|
-
|
|
134
161
|
// ============================================================================
|
|
135
162
|
// Pre-review Setup Functions
|
|
136
163
|
// ============================================================================
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
});
|
|
164
|
+
function maybeUpdateGraphifyBeforeReview(rootDir, { commandRunner = git_js_1.run, log = fmt.log.plain } = {}) {
|
|
165
|
+
// Lazy require to break circular dependency with core/mission-utils
|
|
166
|
+
const { updateGraphifyKnowledgeGraph } = require('../core/mission-utils.js');
|
|
167
|
+
return updateGraphifyKnowledgeGraph({
|
|
168
|
+
rootDir,
|
|
169
|
+
commandRunner,
|
|
170
|
+
log,
|
|
171
|
+
startMessage: 'Updating graphify knowledge graph...',
|
|
172
|
+
failureHint: 'Continuing without blocking review start.'
|
|
173
|
+
});
|
|
148
174
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
175
|
// ============================================================================
|
|
152
176
|
// Agent Fallback Handling
|
|
153
177
|
// ============================================================================
|
|
154
|
-
|
|
155
178
|
// If startAgent fell back to a different family after a limit hit, the comments
|
|
156
179
|
// that gate the loop (review outcome / disposition) will be authored by the
|
|
157
180
|
// fallback identity selected by the launcher.
|
|
158
181
|
// This helper detects the fallback, persists the new identity to review state
|
|
159
182
|
// and the Backlog assignee, and returns the identity that should be polled for.
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if (enforceTaskAssigneeFn && !enforceTaskAssigneeFn(taskResolution.taskFile, fallback)) {
|
|
179
|
-
log(fmt.status('WARN', `Could not enforce fallback implementer ${fallback} in backlog task.`));
|
|
183
|
+
function applyAgentFallback(opts) {
|
|
184
|
+
const { role, original, launchResult, state, slug, worktree, taskResolution, log = fmt.log.plain, writeReviewStateFn = review_state_js_1.writeReviewState, enforceTaskAssigneeFn } = opts;
|
|
185
|
+
if (!launchResult || !launchResult.agent || launchResult.agent === original) {
|
|
186
|
+
return original;
|
|
187
|
+
}
|
|
188
|
+
const fallback = launchResult.agent;
|
|
189
|
+
log(fmt.status('INFO', `${role} fell back from ${original} to ${fallback}; updating identity before polling.`));
|
|
190
|
+
if (role === 'reviewer') {
|
|
191
|
+
state.reviewer = fallback;
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
state.implementer = fallback;
|
|
195
|
+
}
|
|
196
|
+
writeReviewStateFn(slug, state, worktree || process.cwd());
|
|
197
|
+
if (role === 'implementer' && taskResolution && taskResolution.ok) {
|
|
198
|
+
if (enforceTaskAssigneeFn && !enforceTaskAssigneeFn(taskResolution.taskFile, fallback)) {
|
|
199
|
+
log(fmt.status('WARN', `Could not enforce fallback implementer ${fallback} in backlog task.`));
|
|
200
|
+
}
|
|
180
201
|
}
|
|
181
|
-
|
|
182
|
-
return fallback;
|
|
202
|
+
return fallback;
|
|
183
203
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
*/
|
|
192
|
-
function persistNormalizedPhaseRepair(slug, state, worktree, { log = fmt.log.plain, writeReviewStateFn = writeReviewState } = {}) {
|
|
193
|
-
if (!state || !state.phaseOriginal || VALID_PHASES.includes(state.phaseOriginal)) {
|
|
194
|
-
return;
|
|
195
|
-
}
|
|
196
|
-
log(fmt.status('WARN', `Persisted review phase "${state.phaseOriginal}" is invalid. Repairing to "${state.phase}".`));
|
|
197
|
-
writeReviewStateFn(slug, state, worktree);
|
|
198
|
-
state.phaseOriginal = null;
|
|
204
|
+
function persistNormalizedPhaseRepair(slug, state, worktree, { log = fmt.log.plain, writeReviewStateFn = review_state_js_1.writeReviewState } = {}) {
|
|
205
|
+
if (!state || !state.phaseOriginal || review_state_js_1.VALID_PHASES.includes(state.phaseOriginal)) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
log(fmt.status('WARN', `Persisted review phase "${state.phaseOriginal}" is invalid. Repairing to "${state.phase}".`));
|
|
209
|
+
writeReviewStateFn(slug, state, worktree);
|
|
210
|
+
state.phaseOriginal = null;
|
|
199
211
|
}
|
|
200
|
-
|
|
201
|
-
/** @param {string} stage @param {string} agentFamily */
|
|
202
212
|
function stageWindowKey(stage, agentFamily) {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
213
|
+
const normalizedStage = String(stage || 'default').trim().toLowerCase() || 'default';
|
|
214
|
+
const normalizedAgent = String(agentFamily || '').trim().toLowerCase();
|
|
215
|
+
return `${normalizedStage}:${normalizedAgent}`;
|
|
206
216
|
}
|
|
207
|
-
|
|
208
217
|
// Window the Codex telemetry read to the CURRENT launch's start so each round
|
|
209
218
|
// contributes only its own rollouts. Earlier rounds' rollouts have an mtime
|
|
210
219
|
// strictly before this launch's startedAt and are excluded, so the per-round
|
|
@@ -212,901 +221,817 @@ function stageWindowKey(stage, agentFamily) {
|
|
|
212
221
|
// instead of re-summing a cumulative-since-first-launch window every round.
|
|
213
222
|
// Resume-idempotency is provided separately by the launch fingerprint in
|
|
214
223
|
// markStageLaunchRecorded, so the window itself does not need to persist.
|
|
215
|
-
/** @param {{startedAt?: string}} result */
|
|
216
224
|
function stageLaunchSinceMs(result) {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
225
|
+
const startedAt = result && result.startedAt ? String(result.startedAt) : '';
|
|
226
|
+
const startedMs = startedAt ? Date.parse(startedAt) : NaN;
|
|
227
|
+
return Number.isFinite(startedMs) ? startedMs : 0;
|
|
220
228
|
}
|
|
221
|
-
|
|
222
229
|
// ============================================================================
|
|
223
230
|
// Main Review Loop
|
|
224
231
|
// ============================================================================
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* @param {string} slug
|
|
228
|
-
* @param {{implementer?: string, reviewer?: string, focus?: string, maxAttempts?: number, dryRun?: boolean, reset?: boolean, continue?: boolean, isContinue?: boolean, verbose?: boolean, pollTimeoutSeconds?: number|null, worktree?: string, missionPath?: string, resetReviewStateFn?: Function, maybeUpdateGraphifyBeforeReviewFn?: Function, readReviewStateFn?: Function, resolveTaskFileFn?: Function, getTaskImplementerFn?: Function, getTaskStatusFn?: Function, transitionTaskFn?: Function, toVirtualFn?: Function, transitionVirtualFn?: Function, workflowLauncherStatusFn?: Function, buildAutonomousReviewMatrixFn?: Function, formatMatrixSummaryFn?: Function, selectAgentFn?: Function, providerAvailableFn?: Function, runFn?: Function, getPrStatusFn?: Function, enforceTaskAssigneeFn?: Function, resolveReviewUserFn?: Function, forgejoAvailableFn?: Function, resolveForgejoUserFn?: Function, readTokenFn?: Function, getCommentsFn?: Function, postCommentFn?: Function, postReviewFn?: Function, writeReviewStateFn?: Function, startAgentFn?: Function, pollForReviewFn?: Function, pollForDispositionFn?: Function, applyAgentFallbackFn?: Function, buildReviewPromptFn?: Function, buildActOnReviewPromptFn?: Function, buildCompactReviewPromptFn?: Function, buildCompactActOnReviewPromptFn?: Function, consumeReviewerArtifactsFn?: Function, consumeImplementerArtifactsFn?: Function, rebaseBeforeReviewRoundFn?: Function, eligibleAgentsForStepFn?: Function, performHandoffFn?: Function, log?: Function, error?: Function, getLatestReviewForPrFn?: Function, getLatestDispositionForPrFn?: Function, sleepFn?: Function, exit?: Function, gitFn?: Function, isReviewProviderEnabledFn?: Function, legacyIsForgejoReviewEnabledFn?: Function, isForgejoReviewEnabledFn?: Function, recordStageStatsSafeFn?: Function}} [opts]
|
|
229
|
-
* @returns {Promise<void>}
|
|
230
|
-
*/
|
|
231
232
|
async function startReviewLoop(slug, opts = {}) {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
providerAvailableFn = undefined,
|
|
259
|
-
runFn = run,
|
|
260
|
-
getPrStatusFn = getPrStatus,
|
|
261
|
-
enforceTaskAssigneeFn = enforceTaskAssignee,
|
|
262
|
-
resolveReviewUserFn = undefined,
|
|
263
|
-
forgejoAvailableFn = null,
|
|
264
|
-
resolveForgejoUserFn = null,
|
|
265
|
-
readTokenFn = readToken,
|
|
266
|
-
getCommentsFn = getComments,
|
|
267
|
-
postCommentFn = postComment,
|
|
268
|
-
postReviewFn = postReview,
|
|
269
|
-
writeReviewStateFn = writeReviewState,
|
|
270
|
-
startAgentFn = startAgent,
|
|
271
|
-
pollForReviewFn = pollForReview,
|
|
272
|
-
pollForDispositionFn = pollForDisposition,
|
|
273
|
-
applyAgentFallbackFn = applyAgentFallback,
|
|
274
|
-
buildReviewPromptFn = buildReviewPrompt,
|
|
275
|
-
buildActOnReviewPromptFn = buildActOnReviewPrompt,
|
|
276
|
-
buildCompactReviewPromptFn = buildCompactReviewPrompt,
|
|
277
|
-
buildCompactActOnReviewPromptFn = buildCompactActOnReviewPrompt,
|
|
278
|
-
consumeReviewerArtifactsFn = consumeReviewerArtifacts,
|
|
279
|
-
consumeImplementerArtifactsFn = consumeImplementerArtifacts,
|
|
280
|
-
rebaseBeforeReviewRoundFn = rebaseBeforeReviewRound,
|
|
281
|
-
eligibleAgentsForStepFn = eligibleAgentsForStep,
|
|
282
|
-
performHandoffFn = performHandoff,
|
|
283
|
-
log = fmt.log.plain,
|
|
284
|
-
error = fmt.log.plainError,
|
|
285
|
-
getLatestReviewForPrFn = getLatestReviewForPr,
|
|
286
|
-
getLatestDispositionForPrFn = getLatestDispositionForPr,
|
|
287
|
-
sleepFn = delay,
|
|
288
|
-
exit = process.exit,
|
|
289
|
-
gitFn = git,
|
|
290
|
-
isReviewProviderEnabledFn = undefined,
|
|
291
|
-
legacyIsForgejoReviewEnabledFn = null,
|
|
292
|
-
isForgejoReviewEnabledFn = null,
|
|
293
|
-
recordStageStatsSafeFn = () => {}
|
|
294
|
-
} = opts;
|
|
295
|
-
let prNumber = null;
|
|
296
|
-
isContinue = Boolean(isContinue || continueFlag);
|
|
297
|
-
|
|
298
|
-
const pollIntervalMs = resolvePollIntervalMs();
|
|
299
|
-
const pollTimeoutMs = resolvePollTimeoutMs(/** @type {number} */ (pollTimeoutSeconds || 0));
|
|
300
|
-
|
|
301
|
-
const worktree = callerWorktree || resolveWorktree(slug) || process.cwd();
|
|
302
|
-
const resolvedProviderAvailableFn = providerAvailableFn || forgejoAvailableFn || providerAvailable;
|
|
303
|
-
const resolvedReviewUserFn = resolveReviewUserFn || resolveForgejoUserFn || resolveReviewUser;
|
|
304
|
-
const branch = missionBranchName(slug, worktree);
|
|
305
|
-
// Resolve the artifact directory once so reviewer/implementer artifact reads
|
|
306
|
-
// and writes use the adapter-configured location instead of os.tmpdir().
|
|
307
|
-
const artifactDir = resolveArtifactDir(worktree);
|
|
308
|
-
|
|
309
|
-
// `--mission <path>` override (task-1272 SC7): when the caller points the
|
|
310
|
-
// review loop at a mission contract in a non-standard location, resolve the
|
|
311
|
-
// mission directory via the override and thread the contract file path into
|
|
312
|
-
// the reviewer/implementer prompts so launched agents read it (not the
|
|
313
|
-
// slug-derived default). Absent the flag, `effectiveMissionPath` stays
|
|
314
|
-
// undefined and the prompts fall back to the standard slug-derived path.
|
|
315
|
-
const missionDir = findMissionDir(slug, worktree, { missionPath });
|
|
316
|
-
let effectiveMissionPath = /** @type {string|null} */ (null);
|
|
317
|
-
if (missionPath && fs.existsSync(missionPath)) {
|
|
318
|
-
effectiveMissionPath = fs.statSync(missionPath).isDirectory()
|
|
319
|
-
? path.join(missionPath, 'MISSION.md')
|
|
320
|
-
: missionPath;
|
|
321
|
-
log(fmt.status('INFO', `Using mission contract from --mission override: ${effectiveMissionPath}`));
|
|
322
|
-
} else if (missionPath) {
|
|
323
|
-
log(fmt.status('WARN', `--mission path not found: ${missionPath}; falling back to slug-derived mission location${missionDir ? ` (${missionDir})` : ''}.`));
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
const taskResolution = resolveTaskFileFn(slug, worktree);
|
|
327
|
-
|
|
328
|
-
if (!dryRun) {
|
|
329
|
-
maybeUpdateGraphifyBeforeReviewFn(worktree, { commandRunner: runFn, log });
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
// --reset: clear persisted state before starting
|
|
333
|
-
if (reset) {
|
|
334
|
-
if (resetReviewStateFn(slug, worktree)) {
|
|
335
|
-
log(fmt.status('INFO', `Review state reset for ${slug}.`));
|
|
233
|
+
let { implementer, reviewer, focus = 'all', maxAttempts = DEFAULT_MAX_ATTEMPTS, dryRun = false, reset = false, continue: continueFlag = false, isContinue = false, verbose = false, pollTimeoutSeconds = null, worktree: callerWorktree, missionPath, resetReviewStateFn = review_state_js_1.resetReviewState, maybeUpdateGraphifyBeforeReviewFn = maybeUpdateGraphifyBeforeReview, readReviewStateFn = review_state_js_1.readReviewState, resolveTaskFileFn = backlog_js_1.resolveTaskFile, getTaskImplementerFn = backlog_js_1.getTaskImplementer, getTaskStatusFn = backlog_js_1.getTaskStatus, transitionTaskFn = backlog_js_1.transitionTask, toVirtualFn = state_map_js_1.toVirtual, transitionVirtualFn = state_map_js_1.transitionVirtual, workflowLauncherStatusFn = agents_js_1.workflowLauncherStatus, buildAutonomousReviewMatrixFn = runtime_matrix_js_1.buildAutonomousReviewMatrix, formatMatrixSummaryFn = runtime_matrix_js_1.formatMatrixSummary, selectAgentFn = agents_js_1.selectAgent, providerAvailableFn = undefined, runFn = git_js_1.run, getPrStatusFn = review_adapter_js_1.getPrStatus, enforceTaskAssigneeFn = backlog_js_1.enforceTaskAssignee, resolveReviewUserFn = undefined, forgejoAvailableFn = null, resolveForgejoUserFn = null, readTokenFn = review_adapter_js_1.readToken, getCommentsFn = review_adapter_js_1.getComments, postCommentFn = review_adapter_js_1.postComment, postReviewFn = review_adapter_js_1.postReview, writeReviewStateFn = review_state_js_1.writeReviewState, startAgentFn = agents_js_1.startAgent, pollForReviewFn = review_polling_js_1.pollForReview, pollForDispositionFn = review_polling_js_1.pollForDisposition, applyAgentFallbackFn = applyAgentFallback, buildReviewPromptFn = review_prompts_js_1.buildReviewPrompt, buildActOnReviewPromptFn = review_prompts_js_1.buildActOnReviewPrompt, buildCompactReviewPromptFn = review_prompts_js_1.buildCompactReviewPrompt, buildCompactActOnReviewPromptFn = review_prompts_js_1.buildCompactActOnReviewPrompt, consumeReviewerArtifactsFn = review_artifacts_js_1.consumeReviewerArtifacts, consumeImplementerArtifactsFn = review_artifacts_js_1.consumeImplementerArtifacts, rebaseBeforeReviewRoundFn = rebase_js_1.rebaseBeforeReviewRound, eligibleAgentsForStepFn = agents_js_1.eligibleAgentsForStep, log = fmt.log.plain, error = fmt.log.plainError, getLatestReviewForPrFn = review_adapter_js_1.getLatestReviewForPr, getLatestDispositionForPrFn = review_adapter_js_1.getLatestDispositionForPr, sleepFn = review_polling_js_1.delay, exit = process.exit, gitFn = git_js_1.git, isReviewProviderEnabledFn = undefined, legacyIsForgejoReviewEnabledFn = null, isForgejoReviewEnabledFn = null, recordStageStatsSafeFn = () => { } } = opts;
|
|
234
|
+
const performHandoffFn = opts.performHandoffFn || (await getHandoff()).performHandoff;
|
|
235
|
+
let prNumber = null;
|
|
236
|
+
isContinue = Boolean(isContinue || continueFlag);
|
|
237
|
+
const pollIntervalMs = (0, review_polling_js_1.resolvePollIntervalMs)();
|
|
238
|
+
const pollTimeoutMs = (0, review_polling_js_1.resolvePollTimeoutMs)(pollTimeoutSeconds || 0);
|
|
239
|
+
const worktree = callerWorktree || (0, mission_utils_js_1.resolveWorktree)(slug) || process.cwd();
|
|
240
|
+
const resolvedProviderAvailableFn = providerAvailableFn || forgejoAvailableFn || review_adapter_js_1.providerAvailable;
|
|
241
|
+
const resolvedReviewUserFn = resolveReviewUserFn || resolveForgejoUserFn || review_adapter_js_1.resolveReviewUser;
|
|
242
|
+
const branch = (0, mission_utils_js_1.missionBranchName)(slug, worktree);
|
|
243
|
+
// Resolve the artifact directory once so reviewer/implementer artifact reads
|
|
244
|
+
// and writes use the adapter-configured location instead of os.tmpdir().
|
|
245
|
+
const artifactDir = (0, review_artifacts_js_1.resolveArtifactDir)(worktree);
|
|
246
|
+
// `--mission <path>` override (task-1272 SC7): when the caller points the
|
|
247
|
+
// review loop at a mission contract in a non-standard location, resolve the
|
|
248
|
+
// mission directory via the override and thread the contract file path into
|
|
249
|
+
// the reviewer/implementer prompts so launched agents read it (not the
|
|
250
|
+
// slug-derived default). Absent the flag, `effectiveMissionPath` stays
|
|
251
|
+
// undefined and the prompts fall back to the standard slug-derived path.
|
|
252
|
+
const missionDir = (0, mission_utils_js_1.findMissionDir)(slug, worktree, { missionPath });
|
|
253
|
+
let effectiveMissionPath = null;
|
|
254
|
+
if (missionPath && fs.existsSync(missionPath)) {
|
|
255
|
+
effectiveMissionPath = fs.statSync(missionPath).isDirectory()
|
|
256
|
+
? path.join(missionPath, 'MISSION.md')
|
|
257
|
+
: missionPath;
|
|
258
|
+
log(fmt.status('INFO', `Using mission contract from --mission override: ${effectiveMissionPath}`));
|
|
336
259
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
const agents = eligibleAgentsForStepFn('review');
|
|
340
|
-
|
|
341
|
-
const persisted = readReviewStateFn(slug, worktree);
|
|
342
|
-
|
|
343
|
-
if (!implementer) {
|
|
344
|
-
// Try to resume from persisted state
|
|
345
|
-
if (persisted) {
|
|
346
|
-
implementer = persisted.implementer;
|
|
347
|
-
log(fmt.status('INFO', `Resuming persisted implementer: ${implementer}`));
|
|
348
|
-
} else {
|
|
349
|
-
// Try to resolve from backlog task
|
|
350
|
-
if (taskResolution.ok) {
|
|
351
|
-
implementer = getTaskImplementerFn(taskResolution.taskFile);
|
|
352
|
-
if (implementer) {
|
|
353
|
-
log(fmt.status('INFO', `Auto-derived implementer from backlog task: ${implementer}`));
|
|
354
|
-
}
|
|
355
|
-
}
|
|
260
|
+
else if (missionPath) {
|
|
261
|
+
log(fmt.status('WARN', `--mission path not found: ${missionPath}; falling back to slug-derived mission location${missionDir ? ` (${missionDir})` : ''}.`));
|
|
356
262
|
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
implementer = 'autonomous';
|
|
361
|
-
log(fmt.status('WARN', `No implementer identity resolved for ${slug}; defaulting to "autonomous"`));
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
if (!taskResolution.ok) {
|
|
365
|
-
reportTaskResolution(taskResolution, slug, error);
|
|
366
|
-
exit(1);
|
|
367
|
-
return;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
// Validate remote review state only when a review provider is enabled.
|
|
371
|
-
const forgejoEnabledFn = isReviewProviderEnabledFn
|
|
372
|
-
|| legacyIsForgejoReviewEnabledFn
|
|
373
|
-
|| isForgejoReviewEnabledFn
|
|
374
|
-
|| isProviderEnabled;
|
|
375
|
-
const forgejoEnabled = forgejoEnabledFn(worktree);
|
|
376
|
-
if (!dryRun && forgejoEnabled) {
|
|
377
|
-
const forgejoUrl = process.env.FORGEJO_URL || 'http://localhost:3300';
|
|
378
|
-
const bootstrapScript = path.resolve(__dirname, '../../scripts/bootstrap.sh');
|
|
379
|
-
log(fmt.status('INFO', `Checking review-provider availability at ${forgejoUrl}...`));
|
|
380
|
-
if (!await resolvedProviderAvailableFn(forgejoUrl)) {
|
|
381
|
-
error(fmt.status('FAIL', `Review provider not reachable at ${forgejoUrl}`));
|
|
382
|
-
error(' Attempting to bootstrap provider containers...');
|
|
383
|
-
const bootstrapResult = runFn('bash', [bootstrapScript], { stdio: 'inherit' });
|
|
384
|
-
if (bootstrapResult.status === 0) {
|
|
385
|
-
log(fmt.status('INFO', 'Bootstrap succeeded. Continuing with review loop.'));
|
|
386
|
-
} else {
|
|
387
|
-
error(fmt.status('FAIL', 'Bootstrap failed. Please run \'scripts/bootstrap.sh\' manually and retry.'));
|
|
388
|
-
exit(1);
|
|
389
|
-
return;
|
|
390
|
-
}
|
|
391
|
-
} else {
|
|
392
|
-
log(fmt.status('INFO', `Review provider is running and reachable at ${forgejoUrl}.`));
|
|
263
|
+
const taskResolution = resolveTaskFileFn(slug, worktree);
|
|
264
|
+
if (!dryRun) {
|
|
265
|
+
maybeUpdateGraphifyBeforeReviewFn(worktree, { commandRunner: runFn, log });
|
|
393
266
|
}
|
|
394
|
-
|
|
395
|
-
if (
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
// Implementation phase states: 'active' or any virtual state mapping to it
|
|
401
|
-
const isImplementationPhase = taskStatus === 'active' || virtualStatus === 'active';
|
|
402
|
-
|
|
403
|
-
if (isImplementationPhase) {
|
|
404
|
-
// Task is still in implementation phase - provide guidance, don't hard-fail
|
|
405
|
-
log(fmt.status('INFO', `PR not found for ${branch}. Task is in ${taskStatus} — create the PR first: px review ${slug} --push`));
|
|
406
|
-
return;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
// Task is in a post-implementation state (review/approved/ready-for-integration)
|
|
410
|
-
// or the status is ambiguous, yet no open PR exists. Rather than dead-ending
|
|
411
|
-
// with manual guidance, self-heal: run the canonical handoff (push branch +
|
|
412
|
-
// create PR, reliable since task-1317), re-check, and continue the loop once a
|
|
413
|
-
// PR exists. Only fall back to guidance when self-heal cannot yield an open PR.
|
|
414
|
-
// Emits `--push` (the command self-heal attempts and the correct manual
|
|
415
|
-
// equivalent) — never the old, frequently-wrong `--submit`.
|
|
416
|
-
const fallbackGuidance = (/** @type {string|null} */ reason) => {
|
|
417
|
-
error(fmt.status('FAIL', `No open review PR found for ${branch}. Create the PR before starting the review loop.`));
|
|
418
|
-
if (reason) {error(` Handoff failure: ${reason}`);}
|
|
419
|
-
error(` Run: px review ${slug} --push`);
|
|
420
|
-
};
|
|
421
|
-
|
|
422
|
-
if (dryRun) {
|
|
423
|
-
// Dry-run never self-heals (no agents/side effects); emit guidance and exit.
|
|
424
|
-
fallbackGuidance(null);
|
|
425
|
-
exit(1);
|
|
426
|
-
return;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
log(fmt.status('INFO', `No open review PR for ${branch} (task in ${taskStatus}) — attempting automatic handoff (px review ${slug} --push)...`));
|
|
430
|
-
const handoff = await performHandoffFn(slug, { forgejoUser: implementer, worktree });
|
|
431
|
-
|
|
432
|
-
if (handoff && handoff.gatekeeperPushedBack) {
|
|
433
|
-
// Mandatory artifacts are missing: don't spin the loop. The gatekeeper
|
|
434
|
-
// kept the task in its current state; surface that and stop.
|
|
435
|
-
error(fmt.status('FAIL', `Handoff blocked for ${branch}: mandatory mission artifacts are missing. Task stays in ${taskStatus}; supply the required artifacts and retry.`));
|
|
436
|
-
exit(1);
|
|
437
|
-
return;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
if (!handoff || !handoff.ok) {
|
|
441
|
-
fallbackGuidance(handoff && handoff.error ? handoff.error : null);
|
|
442
|
-
exit(1);
|
|
443
|
-
return;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
// Handoff succeeded — re-check whether an open PR now exists.
|
|
447
|
-
const healedPr = getPrStatusFn(branch, worktree);
|
|
448
|
-
if (!healedPr.exists || healedPr.state !== 'open') {
|
|
449
|
-
fallbackGuidance(null);
|
|
450
|
-
exit(1);
|
|
451
|
-
return;
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
log(fmt.status('INFO', `Self-heal succeeded: review PR #${healedPr.number} confirmed open for ${branch}. Continuing review loop.`));
|
|
455
|
-
prNumber = healedPr.number;
|
|
456
|
-
// Fall through into the normal loop with the recovered PR number.
|
|
457
|
-
} else {
|
|
458
|
-
log(fmt.status('INFO', `Review PR #${pr.number} confirmed open for ${branch}.`));
|
|
459
|
-
prNumber = pr.number;
|
|
267
|
+
// --reset: clear persisted state before starting
|
|
268
|
+
if (reset) {
|
|
269
|
+
if (resetReviewStateFn(slug, worktree)) {
|
|
270
|
+
log(fmt.status('INFO', `Review state reset for ${slug}.`));
|
|
271
|
+
}
|
|
460
272
|
}
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
selectErr = /** @type {Error} */ (err);
|
|
483
|
-
reviewer = undefined;
|
|
484
|
-
}
|
|
485
|
-
reviewerSource = 'auto-derived';
|
|
273
|
+
const agents = eligibleAgentsForStepFn('review');
|
|
274
|
+
const persisted = readReviewStateFn(slug, worktree);
|
|
275
|
+
if (!implementer) {
|
|
276
|
+
// Try to resume from persisted state
|
|
277
|
+
if (persisted) {
|
|
278
|
+
implementer = persisted.implementer;
|
|
279
|
+
log(fmt.status('INFO', `Resuming persisted implementer: ${implementer}`));
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
// Try to resolve from backlog task
|
|
283
|
+
if (taskResolution.ok) {
|
|
284
|
+
implementer = (getTaskImplementerFn(taskResolution.taskFile) || undefined);
|
|
285
|
+
if (implementer) {
|
|
286
|
+
log(fmt.status('INFO', `Auto-derived implementer from backlog task: ${implementer}`));
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (!implementer) {
|
|
292
|
+
implementer = 'autonomous';
|
|
293
|
+
log(fmt.status('WARN', `No implementer identity resolved for ${slug}; defaulting to "autonomous"`));
|
|
486
294
|
}
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
if (!reviewer) {
|
|
490
|
-
const anyDifferentFamilyRunnable = agents.some((/** @type {string} */ a) => a !== implementer && workflowLauncherStatusFn(a).supported);
|
|
491
|
-
const implementerRunnable = agents.includes(implementer) && workflowLauncherStatusFn(implementer).supported;
|
|
492
|
-
if (!anyDifferentFamilyRunnable && implementerRunnable) {
|
|
493
|
-
log(fmt.status('WARN', `No supported different-family reviewer found for implementer "${implementer}".`));
|
|
494
|
-
log(fmt.status('WARN', `Single-family fallback: reviewer="${implementer}" (same as implementer) — no different-family agent is runnable or unblocked on this workstation.`));
|
|
495
|
-
reviewer = implementer;
|
|
496
|
-
reviewerSource = 'single-family-fallback';
|
|
497
|
-
} else if (!anyDifferentFamilyRunnable) {
|
|
498
|
-
if (!forgejoEnabled) {
|
|
499
|
-
const detail = selectErr ? `: ${(/** @type {Error} */ (selectErr)).message}` : '';
|
|
500
|
-
reviewer = 'autonomous';
|
|
501
|
-
reviewerSource = 'fallback';
|
|
502
|
-
log(fmt.status('WARN', `No reviewer could be auto-derived${detail}; defaulting to "autonomous"`));
|
|
503
|
-
// Provider=none mode can complete the loop with workflow-owned artifacts only.
|
|
504
|
-
// Do not hard-fail just because no local reviewer launcher is runnable.
|
|
505
|
-
log(fmt.status('INFO', 'Review provider disabled and no runnable reviewer route available; using autonomous workflow-owned review surfaces.'));
|
|
506
|
-
} else {
|
|
507
|
-
const reason = selectErr ? `: ${selectErr.message}` : '';
|
|
508
|
-
error(fmt.status('FAIL', `No reviewer could be auto-derived${reason}.`));
|
|
509
|
-
error('\n' + fmt.status('INFO', 'Full runtime matrix:'));
|
|
510
|
-
formatMatrixSummaryFn(buildAutonomousReviewMatrixFn()).forEach((/** @type {string} */ line) => error(` ${line}`));
|
|
511
|
-
error('\n' + fmt.status('FAIL', `No runnable reviewer route for implementer "${implementer}".`));
|
|
295
|
+
if (!taskResolution.ok) {
|
|
296
|
+
(0, backlog_js_1.reportTaskResolution)(taskResolution, slug, error);
|
|
512
297
|
exit(1);
|
|
513
298
|
return;
|
|
514
|
-
}
|
|
515
|
-
} else {
|
|
516
|
-
const detail = selectErr ? `: ${selectErr.message}` : '';
|
|
517
|
-
reviewer = 'autonomous';
|
|
518
|
-
reviewerSource = 'fallback';
|
|
519
|
-
log(fmt.status('WARN', `No reviewer could be auto-derived${detail}; defaulting to "autonomous"`));
|
|
520
299
|
}
|
|
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
|
-
|
|
300
|
+
// Validate remote review state only when a review provider is enabled.
|
|
301
|
+
const forgejoEnabledFn = isReviewProviderEnabledFn
|
|
302
|
+
|| legacyIsForgejoReviewEnabledFn
|
|
303
|
+
|| isForgejoReviewEnabledFn
|
|
304
|
+
|| review_adapter_js_1.isProviderEnabled;
|
|
305
|
+
const forgejoEnabled = forgejoEnabledFn(worktree);
|
|
306
|
+
if (!dryRun && forgejoEnabled) {
|
|
307
|
+
const forgejoUrl = process.env.FORGEJO_URL || 'http://localhost:3300';
|
|
308
|
+
const bootstrapScript = path.resolve(__dirname, '../../scripts/bootstrap.sh');
|
|
309
|
+
log(fmt.status('INFO', `Checking review-provider availability at ${forgejoUrl}...`));
|
|
310
|
+
if (!await resolvedProviderAvailableFn(forgejoUrl)) {
|
|
311
|
+
error(fmt.status('FAIL', `Review provider not reachable at ${forgejoUrl}`));
|
|
312
|
+
error(' Attempting to bootstrap provider containers...');
|
|
313
|
+
const bootstrapResult = runFn('bash', [bootstrapScript], { stdio: 'inherit' });
|
|
314
|
+
if (bootstrapResult.status === 0) {
|
|
315
|
+
log(fmt.status('INFO', 'Bootstrap succeeded. Continuing with review loop.'));
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
error(fmt.status('FAIL', 'Bootstrap failed. Please run \'scripts/bootstrap.sh\' manually and retry.'));
|
|
319
|
+
exit(1);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
log(fmt.status('INFO', `Review provider is running and reachable at ${forgejoUrl}.`));
|
|
325
|
+
}
|
|
326
|
+
const pr = getPrStatusFn(branch, worktree);
|
|
327
|
+
if (!pr.exists || pr.state !== 'open') {
|
|
328
|
+
// Check task status to detect pre-review (implementation) phase
|
|
329
|
+
const taskStatus = taskResolution.ok ? getTaskStatusFn(taskResolution.taskFile) : null;
|
|
330
|
+
const virtualStatus = taskStatus ? toVirtualFn(taskStatus) : null;
|
|
331
|
+
// Implementation phase states: 'active' or any virtual state mapping to it
|
|
332
|
+
const isImplementationPhase = taskStatus === 'active' || virtualStatus === 'active';
|
|
333
|
+
if (isImplementationPhase) {
|
|
334
|
+
// Task is still in implementation phase - provide guidance, don't hard-fail
|
|
335
|
+
log(fmt.status('INFO', `PR not found for ${branch}. Task is in ${taskStatus} — create the PR first: px review ${slug} --push`));
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
// Task is in a post-implementation state (review/approved/ready-for-integration)
|
|
339
|
+
// or the status is ambiguous, yet no open PR exists. Rather than dead-ending
|
|
340
|
+
// with manual guidance, self-heal: run the canonical handoff (push branch +
|
|
341
|
+
// create PR, reliable since task-1317), re-check, and continue the loop once a
|
|
342
|
+
// PR exists. Only fall back to guidance when self-heal cannot yield an open PR.
|
|
343
|
+
// Emits `--push` (the command self-heal attempts and the correct manual
|
|
344
|
+
// equivalent) — never the old, frequently-wrong `--submit`.
|
|
345
|
+
const fallbackGuidance = (reason) => {
|
|
346
|
+
error(fmt.status('FAIL', `No open review PR found for ${branch}. Create the PR before starting the review loop.`));
|
|
347
|
+
if (reason) {
|
|
348
|
+
error(` Handoff failure: ${reason}`);
|
|
349
|
+
}
|
|
350
|
+
error(` Run: px review ${slug} --push`);
|
|
351
|
+
};
|
|
352
|
+
if (dryRun) {
|
|
353
|
+
// Dry-run never self-heals (no agents/side effects); emit guidance and exit.
|
|
354
|
+
fallbackGuidance(null);
|
|
355
|
+
exit(1);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
log(fmt.status('INFO', `No open review PR for ${branch} (task in ${taskStatus}) — attempting automatic handoff (px review ${slug} --push)...`));
|
|
359
|
+
const handoff = await performHandoffFn(slug, { forgejoUser: implementer, worktree });
|
|
360
|
+
if (handoff && handoff.gatekeeperPushedBack) {
|
|
361
|
+
// Mandatory artifacts are missing: don't spin the loop. The gatekeeper
|
|
362
|
+
// kept the task in its current state; surface that and stop.
|
|
363
|
+
error(fmt.status('FAIL', `Handoff blocked for ${branch}: mandatory mission artifacts are missing. Task stays in ${taskStatus}; supply the required artifacts and retry.`));
|
|
364
|
+
exit(1);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
if (!handoff || !handoff.ok) {
|
|
368
|
+
fallbackGuidance(handoff && handoff.error ? String(handoff.error) : null);
|
|
369
|
+
exit(1);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
// Handoff succeeded — re-check whether an open PR now exists.
|
|
373
|
+
const healedPr = getPrStatusFn(branch, worktree);
|
|
374
|
+
if (!healedPr.exists || healedPr.state !== 'open') {
|
|
375
|
+
fallbackGuidance(null);
|
|
376
|
+
exit(1);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
log(fmt.status('INFO', `Self-heal succeeded: review PR #${healedPr.number} confirmed open for ${branch}. Continuing review loop.`));
|
|
380
|
+
prNumber = healedPr.number;
|
|
381
|
+
// Fall through into the normal loop with the recovered PR number.
|
|
382
|
+
}
|
|
383
|
+
else {
|
|
384
|
+
log(fmt.status('INFO', `Review PR #${pr.number} confirmed open for ${branch}.`));
|
|
385
|
+
prNumber = pr.number;
|
|
386
|
+
}
|
|
547
387
|
}
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
error(fmt.status('FAIL', `Unsupported reviewer: "${reviewer}" (blocked or unsupported).`));
|
|
551
|
-
error('\n' + fmt.status('INFO', 'Full runtime matrix:'));
|
|
552
|
-
formatMatrixSummaryFn(buildAutonomousReviewMatrixFn()).forEach((/** @type {string} */ line) => error(` ${line}`));
|
|
553
|
-
error('\n' + fmt.status('FAIL', `No runnable reviewer route for implementer "${implementer}".`));
|
|
554
|
-
exit(1);
|
|
555
|
-
return;
|
|
388
|
+
else if (!dryRun && !forgejoEnabled) {
|
|
389
|
+
log(fmt.status('INFO', 'Forgejo validation skipped (review provider is not forgejo). Using workflow-owned review surfaces.'));
|
|
556
390
|
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
if (
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
exit(1);
|
|
570
|
-
return;
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
const excludeSet = new Set([...triedReviewers, implementer]);
|
|
574
|
-
let fallback;
|
|
575
|
-
let fallbackStatus;
|
|
576
|
-
try {
|
|
577
|
-
fallback = selectAgentFn('review', { exclude: excludeSet });
|
|
578
|
-
if (excludeSet.has(fallback)) {
|
|
579
|
-
fallback = null;
|
|
580
|
-
fallbackStatus = null;
|
|
581
|
-
} else {
|
|
582
|
-
fallbackStatus = workflowLauncherStatusFn(fallback);
|
|
391
|
+
// Note: The implementer may not be in the agents list (eligible for review step)
|
|
392
|
+
// if its launcher is unavailable. This is OK - we'll use fallback logic for the reviewer.
|
|
393
|
+
// The strict implementer eligibility check was removed to allow fallback reviewer selection (SC 4).
|
|
394
|
+
// Resolve reviewer after the no-PR/self-heal gate so implementation-phase
|
|
395
|
+
// guidance does not depend on workstation launcher availability.
|
|
396
|
+
let reviewerSource = 'explicit';
|
|
397
|
+
let selectErr = null;
|
|
398
|
+
if (!reviewer) {
|
|
399
|
+
if (persisted && persisted.reviewer) {
|
|
400
|
+
reviewer = persisted.reviewer;
|
|
401
|
+
reviewerSource = 'persisted';
|
|
402
|
+
log(fmt.status('INFO', `Resuming persisted reviewer: ${reviewer} (round ${persisted.round})`));
|
|
583
403
|
}
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
404
|
+
else {
|
|
405
|
+
try {
|
|
406
|
+
reviewer = selectAgentFn('review', { exclude: new Set([implementer]) });
|
|
407
|
+
}
|
|
408
|
+
catch (err) {
|
|
409
|
+
selectErr = err;
|
|
410
|
+
reviewer = undefined;
|
|
411
|
+
}
|
|
412
|
+
reviewerSource = 'auto-derived';
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
if (!reviewer) {
|
|
416
|
+
const anyDifferentFamilyRunnable = agents.some((a) => a !== implementer && workflowLauncherStatusFn(a).supported);
|
|
591
417
|
const implementerRunnable = agents.includes(implementer) && workflowLauncherStatusFn(implementer).supported;
|
|
592
418
|
if (!anyDifferentFamilyRunnable && implementerRunnable) {
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
break;
|
|
419
|
+
log(fmt.status('WARN', `No supported different-family reviewer found for implementer "${implementer}".`));
|
|
420
|
+
log(fmt.status('WARN', `Single-family fallback: reviewer="${implementer}" (same as implementer) — no different-family agent is runnable or unblocked on this workstation.`));
|
|
421
|
+
reviewer = implementer;
|
|
422
|
+
reviewerSource = 'single-family-fallback';
|
|
598
423
|
}
|
|
599
|
-
if (
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
424
|
+
else if (!anyDifferentFamilyRunnable) {
|
|
425
|
+
if (!forgejoEnabled) {
|
|
426
|
+
const detail = selectErr ? `: ${selectErr.message}` : '';
|
|
427
|
+
reviewer = 'autonomous';
|
|
428
|
+
reviewerSource = 'fallback';
|
|
429
|
+
log(fmt.status('WARN', `No reviewer could be auto-derived${detail}; defaulting to "autonomous"`));
|
|
430
|
+
// Provider=none mode can complete the loop with workflow-owned artifacts only.
|
|
431
|
+
// Do not hard-fail just because no local reviewer launcher is runnable.
|
|
432
|
+
log(fmt.status('INFO', 'Review provider disabled and no runnable reviewer route available; using autonomous workflow-owned review surfaces.'));
|
|
433
|
+
}
|
|
434
|
+
else {
|
|
435
|
+
const reason = selectErr ? `: ${selectErr.message}` : '';
|
|
436
|
+
error(fmt.status('FAIL', `No reviewer could be auto-derived${reason}.`));
|
|
437
|
+
error('\n' + fmt.status('INFO', 'Full runtime matrix:'));
|
|
438
|
+
formatMatrixSummaryFn(buildAutonomousReviewMatrixFn()).forEach((line) => error(` ${line}`));
|
|
439
|
+
error('\n' + fmt.status('FAIL', `No runnable reviewer route for implementer "${implementer}".`));
|
|
440
|
+
exit(1);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
const detail = selectErr ? `: ${selectErr.message}` : '';
|
|
446
|
+
reviewer = 'autonomous';
|
|
447
|
+
reviewerSource = 'fallback';
|
|
448
|
+
log(fmt.status('WARN', `No reviewer could be auto-derived${detail}; defaulting to "autonomous"`));
|
|
604
449
|
}
|
|
605
|
-
const reason = !agents.includes(reviewer) ? 'blocked or unsupported' : 'launcher is not available';
|
|
606
|
-
error(fmt.status('FAIL', `Unsupported reviewer: "${reviewer}" (${reason}) and no unblocked different-family fallback is available.`));
|
|
607
|
-
if (reviewerStatus.detail) {error(` Looked for: ${reviewerStatus.detail}`);}
|
|
608
|
-
error('\n' + fmt.status('INFO', 'Full runtime matrix:'));
|
|
609
|
-
formatMatrixSummaryFn(buildAutonomousReviewMatrixFn()).forEach((/** @type {string} */ line) => error(` ${line}`));
|
|
610
|
-
error('\n' + fmt.status('FAIL', `No runnable reviewer route for implementer "${implementer}".`));
|
|
611
|
-
exit(1);
|
|
612
|
-
return;
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
const reason = !agents.includes(reviewer) ? 'blocked or unsupported' : 'launcher is not available';
|
|
616
|
-
log(fmt.status('WARN', `Unsupported reviewer: "${reviewer}" (${reason}); trying fallback "${fallback}".`));
|
|
617
|
-
reviewer = fallback;
|
|
618
|
-
reviewerStatus = fallbackStatus;
|
|
619
|
-
reviewerSource = reviewerSource === 'persisted' ? 'persisted-fallback' : 'auto-derived-fallback';
|
|
620
450
|
}
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
// selected for this launch (which may differ after a reviewer/implementer
|
|
629
|
-
// fallback). Constructing fresh would silently reset all persisted progress.
|
|
630
|
-
let state;
|
|
631
|
-
if (persisted) {
|
|
632
|
-
state = ReviewState.from(slug, persisted);
|
|
633
|
-
state.reviewer = reviewer;
|
|
634
|
-
state.implementer = implementer;
|
|
635
|
-
} else {
|
|
636
|
-
state = new ReviewState(slug, { reviewer, implementer });
|
|
637
|
-
}
|
|
638
|
-
persistNormalizedPhaseRepair(slug, state, worktree, { log, writeReviewStateFn });
|
|
639
|
-
|
|
640
|
-
if (persisted && state.round > 1 && !dryRun) {
|
|
641
|
-
log(fmt.status('INFO', `Resuming review loop from round ${state.round} (${state.phase}).`));
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
log(fmt.status('INFO', `Starting autonomous review loop for mission: ${slug}`));
|
|
645
|
-
log(fmt.status('INFO', `Branch: ${branch}`));
|
|
646
|
-
log(fmt.status('INFO', `Implementer: ${implementer} | Reviewer: ${reviewer} (${reviewerSource})`));
|
|
647
|
-
log(fmt.status('INFO', `Focus: ${focus} | Max attempts: ${maxAttempts}`));
|
|
648
|
-
log(fmt.status('INFO', `Poll interval: ${Math.round(pollIntervalMs / 1000)}s | Poll timeout: ${Math.round(pollTimeoutMs / 1000)}s${verbose ? ' | Verbose: on' : ''}`));
|
|
649
|
-
|
|
650
|
-
if (dryRun) {
|
|
651
|
-
log(fmt.status('DRY-RUN', 'No agents will be launched.'));
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
// Only resolve a provider identity and read a token when a provider is enabled.
|
|
655
|
-
const pollingUser = forgejoEnabled ? resolvedReviewUserFn() : null;
|
|
656
|
-
const token = dryRun || !forgejoEnabled ? null : readTokenFn(pollingUser, { rootDir: worktree });
|
|
657
|
-
const initialRound = state.round;
|
|
658
|
-
|
|
659
|
-
for (let attempt = initialRound; attempt <= maxAttempts; attempt++) {
|
|
660
|
-
log('\n' + fmt.status('INFO', `========== Round ${attempt} / ${maxAttempts} ==========`));
|
|
661
|
-
|
|
662
|
-
// On rounds after the first, advance the state machine
|
|
663
|
-
if (attempt > state.round) {
|
|
664
|
-
state.advanceRound();
|
|
451
|
+
const willLaunchRounds = maxAttempts >= ((persisted && persisted.round) || 1);
|
|
452
|
+
const resumesInFixingPhase = persisted && persisted.phase === 'fixing' && !dryRun;
|
|
453
|
+
// Dry-run validates the reviewer identity but only explicit reviewers bypass
|
|
454
|
+
// launcher support checks; auto-derived/persisted reviewers still exercise
|
|
455
|
+
// fallback routing so dry-run logs reflect the real selection path.
|
|
456
|
+
if (reviewerSource === 'fallback') {
|
|
457
|
+
log(fmt.status('INFO', `Reviewer identity defaulted to autonomous; skipping launcher availability check.`));
|
|
665
458
|
}
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
if (
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
if (!
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
// When the provider is disabled, use local review state instead of polling.
|
|
684
|
-
log(fmt.status('INFO', `Round ${attempt}: review provider disabled; using workflow-owned review state.`));
|
|
685
|
-
reviewState = null;
|
|
459
|
+
else if (resumesInFixingPhase) {
|
|
460
|
+
log(fmt.status('INFO', `Resuming in fixing phase with reviewer "${reviewer}"; skipping launcher availability check until a new review launch is needed.`));
|
|
461
|
+
}
|
|
462
|
+
else if (dryRun && reviewerSource === 'explicit') {
|
|
463
|
+
const reviewerStatus = workflowLauncherStatusFn(reviewer);
|
|
464
|
+
const hasInjectedLauncherStatus = workflowLauncherStatusFn !== agents_js_1.workflowLauncherStatus;
|
|
465
|
+
if (!agents.includes(reviewer) || (hasInjectedLauncherStatus && !reviewerStatus.supported)) {
|
|
466
|
+
const reason = !agents.includes(reviewer) ? 'blocked or unsupported' : 'launcher is not available';
|
|
467
|
+
error(fmt.status('FAIL', `Unsupported reviewer: "${reviewer}" (${reason}).`));
|
|
468
|
+
if (reason === 'launcher is not available' && reviewerStatus.detail) {
|
|
469
|
+
error(` Looked for: ${reviewerStatus.detail}`);
|
|
470
|
+
}
|
|
471
|
+
error('\n' + fmt.status('INFO', 'Full runtime matrix:'));
|
|
472
|
+
formatMatrixSummaryFn(buildAutonomousReviewMatrixFn()).forEach((line) => error(` ${line}`));
|
|
473
|
+
error('\n' + fmt.status('FAIL', `No runnable reviewer route for implementer "${implementer}".`));
|
|
474
|
+
exit(1);
|
|
475
|
+
return;
|
|
686
476
|
}
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
log(buildReviewPromptFn({ reviewer, branch, implementer, focus, attempt, repoRoot: worktree, missionPath: effectiveMissionPath, actualReviewer: '{{AGENT_NAME}}' }));
|
|
697
|
-
}
|
|
477
|
+
}
|
|
478
|
+
else if (!willLaunchRounds) {
|
|
479
|
+
if (!agents.includes(reviewer)) {
|
|
480
|
+
error(fmt.status('FAIL', `Unsupported reviewer: "${reviewer}" (blocked or unsupported).`));
|
|
481
|
+
error('\n' + fmt.status('INFO', 'Full runtime matrix:'));
|
|
482
|
+
formatMatrixSummaryFn(buildAutonomousReviewMatrixFn()).forEach((line) => error(` ${line}`));
|
|
483
|
+
error('\n' + fmt.status('FAIL', `No runnable reviewer route for implementer "${implementer}".`));
|
|
484
|
+
exit(1);
|
|
485
|
+
return;
|
|
698
486
|
}
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
let
|
|
487
|
+
}
|
|
488
|
+
else {
|
|
489
|
+
let reviewerStatus = workflowLauncherStatusFn(reviewer);
|
|
490
|
+
const triedReviewers = new Set();
|
|
491
|
+
while (!agents.includes(reviewer) || !reviewerStatus.supported) {
|
|
492
|
+
triedReviewers.add(reviewer);
|
|
493
|
+
if (reviewerSource === 'explicit') {
|
|
494
|
+
const reason = !agents.includes(reviewer) ? 'blocked or unsupported' : 'launcher is not available';
|
|
495
|
+
error(fmt.status('FAIL', `Unsupported reviewer: "${reviewer}" (${reason}).`));
|
|
496
|
+
if (reviewerStatus.detail) {
|
|
497
|
+
error(` Looked for: ${reviewerStatus.detail}`);
|
|
498
|
+
}
|
|
499
|
+
error('\n' + fmt.status('INFO', 'Full runtime matrix:'));
|
|
500
|
+
formatMatrixSummaryFn(buildAutonomousReviewMatrixFn()).forEach((line) => error(` ${line}`));
|
|
501
|
+
error('\n' + fmt.status('FAIL', `No runnable reviewer route for implementer "${implementer}".`));
|
|
502
|
+
exit(1);
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
const excludeSet = new Set([...triedReviewers, implementer]);
|
|
506
|
+
let fallback;
|
|
507
|
+
let fallbackStatus;
|
|
719
508
|
try {
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
509
|
+
fallback = selectAgentFn('review', { exclude: excludeSet });
|
|
510
|
+
if (excludeSet.has(fallback)) {
|
|
511
|
+
fallback = undefined;
|
|
512
|
+
fallbackStatus = null;
|
|
513
|
+
}
|
|
514
|
+
else {
|
|
515
|
+
fallbackStatus = workflowLauncherStatusFn(fallback);
|
|
516
|
+
}
|
|
728
517
|
}
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
518
|
+
catch {
|
|
519
|
+
fallback = undefined;
|
|
520
|
+
fallbackStatus = null;
|
|
521
|
+
}
|
|
522
|
+
if (!fallback) {
|
|
523
|
+
const anyDifferentFamilyRunnable = agents.some((a) => a !== implementer && workflowLauncherStatusFn(a).supported);
|
|
524
|
+
const implementerRunnable = agents.includes(implementer) && workflowLauncherStatusFn(implementer).supported;
|
|
525
|
+
if (!anyDifferentFamilyRunnable && implementerRunnable) {
|
|
526
|
+
log(fmt.status('WARN', `No supported different-family reviewer found for implementer "${implementer}".`));
|
|
527
|
+
log(fmt.status('WARN', `Single-family fallback: reviewer="${implementer}" (same as implementer) — no different-family agent is runnable or unblocked on this workstation.`));
|
|
528
|
+
reviewer = implementer;
|
|
529
|
+
reviewerSource = 'single-family-fallback';
|
|
530
|
+
break;
|
|
531
|
+
}
|
|
532
|
+
if (dryRun && implementer === 'autonomous') {
|
|
533
|
+
reviewer = 'autonomous';
|
|
534
|
+
reviewerSource = 'fallback';
|
|
535
|
+
log(fmt.status('WARN', `No runnable reviewer launcher available in dry-run; defaulting reviewer identity to "autonomous".`));
|
|
536
|
+
break;
|
|
537
|
+
}
|
|
538
|
+
const reason = !agents.includes(reviewer) ? 'blocked or unsupported' : 'launcher is not available';
|
|
539
|
+
error(fmt.status('FAIL', `Unsupported reviewer: "${reviewer}" (${reason}) and no unblocked different-family fallback is available.`));
|
|
540
|
+
if (reviewerStatus.detail) {
|
|
541
|
+
error(` Looked for: ${reviewerStatus.detail}`);
|
|
542
|
+
}
|
|
543
|
+
error('\n' + fmt.status('INFO', 'Full runtime matrix:'));
|
|
544
|
+
formatMatrixSummaryFn(buildAutonomousReviewMatrixFn()).forEach((line) => error(` ${line}`));
|
|
545
|
+
error('\n' + fmt.status('FAIL', `No runnable reviewer route for implementer "${implementer}".`));
|
|
546
|
+
exit(1);
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
const reason = !agents.includes(reviewer) ? 'blocked or unsupported' : 'launcher is not available';
|
|
550
|
+
log(fmt.status('WARN', `Unsupported reviewer: "${reviewer}" (${reason}); trying fallback "${fallback}".`));
|
|
551
|
+
reviewer = fallback;
|
|
552
|
+
reviewerStatus = fallbackStatus;
|
|
553
|
+
reviewerSource = reviewerSource === 'persisted' ? 'persisted-fallback' : 'auto-derived-fallback';
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
log(fmt.status('INFO', `Selected reviewer: ${reviewer} (${reviewerSource})`));
|
|
557
|
+
// Build the canonical ReviewState instance. When persisted state exists we
|
|
558
|
+
// resume from it — preserving round, startedAt, phase, disposition, retry
|
|
559
|
+
// counts, and metadata — and only overwrite the identities with the ones
|
|
560
|
+
// selected for this launch (which may differ after a reviewer/implementer
|
|
561
|
+
// fallback). Constructing fresh would silently reset all persisted progress.
|
|
562
|
+
let state;
|
|
563
|
+
if (persisted) {
|
|
564
|
+
state = review_state_js_1.ReviewState.from(slug, persisted);
|
|
565
|
+
state.reviewer = reviewer;
|
|
566
|
+
state.implementer = implementer;
|
|
567
|
+
}
|
|
568
|
+
else {
|
|
569
|
+
state = new review_state_js_1.ReviewState(slug, { reviewer, implementer });
|
|
570
|
+
}
|
|
571
|
+
persistNormalizedPhaseRepair(slug, state, worktree, { log, writeReviewStateFn });
|
|
572
|
+
if (persisted && state.round > 1 && !dryRun) {
|
|
573
|
+
log(fmt.status('INFO', `Resuming review loop from round ${state.round} (${state.phase}).`));
|
|
574
|
+
}
|
|
575
|
+
log(fmt.status('INFO', `Starting autonomous review loop for mission: ${slug}`));
|
|
576
|
+
log(fmt.status('INFO', `Branch: ${branch}`));
|
|
577
|
+
log(fmt.status('INFO', `Implementer: ${implementer} | Reviewer: ${reviewer} (${reviewerSource})`));
|
|
578
|
+
log(fmt.status('INFO', `Focus: ${focus} | Max attempts: ${maxAttempts}`));
|
|
579
|
+
log(fmt.status('INFO', `Poll interval: ${Math.round(pollIntervalMs / 1000)}s | Poll timeout: ${Math.round(pollTimeoutMs / 1000)}s${verbose ? ' | Verbose: on' : ''}`));
|
|
580
|
+
if (dryRun) {
|
|
581
|
+
log(fmt.status('DRY-RUN', 'No agents will be launched.'));
|
|
582
|
+
}
|
|
583
|
+
// Only resolve a provider identity and read a token when a provider is enabled.
|
|
584
|
+
const pollingUser = forgejoEnabled ? resolvedReviewUserFn() : null;
|
|
585
|
+
const token = dryRun || !forgejoEnabled ? null : readTokenFn(pollingUser, { rootDir: worktree });
|
|
586
|
+
const initialRound = state.round;
|
|
587
|
+
for (let attempt = initialRound; attempt <= maxAttempts; attempt++) {
|
|
588
|
+
log('\n' + fmt.status('INFO', `========== Round ${attempt} / ${maxAttempts} ==========`));
|
|
589
|
+
// On rounds after the first, advance the state machine
|
|
590
|
+
if (attempt > state.round) {
|
|
591
|
+
state.advanceRound();
|
|
592
|
+
}
|
|
593
|
+
let reviewState;
|
|
594
|
+
if (state.phase === 'reviewing') {
|
|
595
|
+
// Check if we can skip reviewer launch
|
|
596
|
+
if (isContinue && attempt === initialRound) {
|
|
597
|
+
if (!dryRun) {
|
|
598
|
+
transitionTaskFn(slug, 'review', { rootDir: worktree, log });
|
|
599
|
+
}
|
|
600
|
+
if (forgejoEnabled) {
|
|
601
|
+
log(fmt.status('INFO', `Round ${attempt}: checking for existing review by ${reviewer} since ${state.startedAt}...`));
|
|
602
|
+
reviewState = await pollForReviewFn(prNumber, reviewer, state.startedAt, token, {
|
|
603
|
+
getLatestReviewForPrFn, sleepFn, intervalMs: 1000, timeoutMs: 2000, retryCount: state.reviewerRetryCount, verbose, label: `round ${attempt} skip-check`, log
|
|
604
|
+
});
|
|
605
|
+
// Skip-check timeout means no existing review - treat as null to proceed with initial launch
|
|
606
|
+
if ((0, review_polling_js_1.isPollTimeout)(reviewState)) {
|
|
607
|
+
reviewState = null;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
else {
|
|
611
|
+
// When the provider is disabled, use local review state instead of polling.
|
|
612
|
+
log(fmt.status('INFO', `Round ${attempt}: review provider disabled; using workflow-owned review state.`));
|
|
613
|
+
reviewState = null;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
// Handle reviewState: could be a valid state, null (no existing review or hard failure), or undefined (first time)
|
|
617
|
+
if (dryRun) {
|
|
618
|
+
if (!reviewState) {
|
|
619
|
+
if (reviewer === 'autonomous') {
|
|
620
|
+
log(fmt.status('INFO', `Round ${attempt}: reviewer identity is autonomous; skipping dry-run reviewer prompt and using local review artifacts only.`));
|
|
621
|
+
}
|
|
622
|
+
else {
|
|
623
|
+
log(`\n--- DRY-RUN: reviewer (${reviewer}) prompt ---`);
|
|
624
|
+
log(buildReviewPromptFn({ reviewer: reviewer, branch, implementer: implementer, focus, attempt, repoRoot: worktree, missionPath: effectiveMissionPath || undefined, actualReviewer: '{{AGENT_NAME}}' }));
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
else {
|
|
629
|
+
if (!reviewState) {
|
|
630
|
+
// First launch or hard failure (null)
|
|
631
|
+
const rebaseResult = await rebaseBeforeReviewRoundFn(slug, {
|
|
632
|
+
worktree, runFn: runFn, log, error,
|
|
633
|
+
taskFile: taskResolution.taskFile,
|
|
634
|
+
gitFn,
|
|
635
|
+
isReviewProviderEnabledFn: forgejoEnabledFn
|
|
636
|
+
});
|
|
637
|
+
if (!rebaseResult.ok) {
|
|
638
|
+
exit(1);
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
if (!dryRun) {
|
|
642
|
+
transitionTaskFn(slug, 'review', { rootDir: worktree, log });
|
|
643
|
+
}
|
|
644
|
+
state.phase = 'reviewing';
|
|
645
|
+
writeReviewStateFn(slug, state, worktree);
|
|
646
|
+
if (reviewer === 'autonomous' && !forgejoEnabled) {
|
|
647
|
+
log(fmt.status('INFO', `Round ${attempt}: reviewer identity is autonomous; skipping reviewer launch and using local review artifacts only.`));
|
|
648
|
+
}
|
|
649
|
+
else {
|
|
650
|
+
log(fmt.status('INFO', `Round ${attempt}: launching reviewer (${reviewer})...`));
|
|
651
|
+
let reviewerLaunchResult;
|
|
652
|
+
try {
|
|
653
|
+
reviewerLaunchResult = await startAgentFn('review', {
|
|
654
|
+
agent: reviewer,
|
|
655
|
+
prompt: (actualReviewer) => buildCompactReviewPromptFn({ reviewer: reviewer, branch, implementer: implementer, focus, attempt, repoRoot: worktree, missionPath: effectiveMissionPath || undefined, actualReviewer }),
|
|
656
|
+
worktree, slug, role: 'reviewer', exclude: [implementer]
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
catch (err) {
|
|
660
|
+
error(fmt.status('FAIL', `Could not launch reviewer agent (${reviewer}): ${err.message}`));
|
|
661
|
+
exit(1);
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
reviewer = applyAgentFallbackFn({
|
|
665
|
+
role: 'reviewer', original: reviewer, launchResult: reviewerLaunchResult,
|
|
666
|
+
state: state, slug, worktree, taskResolution, log, writeReviewStateFn, enforceTaskAssigneeFn
|
|
667
|
+
});
|
|
668
|
+
// Record review-stage telemetry immediately after the reviewer runs,
|
|
669
|
+
// so the worktree's newest codex rollout is this reviewer's session.
|
|
670
|
+
// The window is bounded to this round's launch so each round adds
|
|
671
|
+
// only its own usage; accumulateStageStats sums the rounds into one
|
|
672
|
+
// cumulative row per reviewer family.
|
|
673
|
+
const reviewSinceMs = stageLaunchSinceMs(reviewerLaunchResult?.result);
|
|
674
|
+
recordStageStatsSafeFn('review', {
|
|
675
|
+
stage: 'review', slug, rootDir: worktree, worktree, reviewer, implementer,
|
|
676
|
+
result: reviewerLaunchResult?.result,
|
|
677
|
+
sinceMs: reviewSinceMs || 0, log, error, state, writeReviewStateFn,
|
|
678
|
+
model: (0, product_config_js_1.resolveAgentModel)(reviewer, worktree),
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
const reviewerArtifacts = await consumeReviewerArtifactsFn(slug, reviewer, {
|
|
682
|
+
worktree,
|
|
683
|
+
tmpDir: artifactDir,
|
|
684
|
+
fallbackToTmp: true,
|
|
685
|
+
readTokenFn,
|
|
686
|
+
getCommentsFn: getCommentsFn,
|
|
687
|
+
postCommentFn,
|
|
688
|
+
postReviewFn,
|
|
689
|
+
buildMetadataFooterFn: review_artifacts_js_1.buildMetadataFooter,
|
|
690
|
+
forgejoEnabled,
|
|
691
|
+
log,
|
|
692
|
+
error
|
|
693
|
+
});
|
|
694
|
+
if (reviewerArtifacts.consumed) {
|
|
695
|
+
if (!reviewerArtifacts.ok) {
|
|
696
|
+
exit(1);
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
reviewState = reviewerArtifacts.reviewState;
|
|
700
|
+
}
|
|
701
|
+
if (!reviewState && forgejoEnabled) {
|
|
702
|
+
reviewState = await pollForReviewFn(prNumber, reviewer, state.startedAt, token, {
|
|
703
|
+
getLatestReviewForPrFn, sleepFn, intervalMs: pollIntervalMs, timeoutMs: pollTimeoutMs, retryCount: 0, verbose, label: `round ${attempt} review`, log
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
if ((0, review_polling_js_1.isPollTimeout)(reviewState)) {
|
|
708
|
+
// Timeout recovery loop for reviewer
|
|
709
|
+
const stateAny = state;
|
|
710
|
+
while ((stateAny['reviewerRetryCount'] || 0) < 2) {
|
|
711
|
+
stateAny['reviewerRetryCount'] = (stateAny['reviewerRetryCount'] || 0) + 1;
|
|
712
|
+
writeReviewStateFn(slug, state, worktree);
|
|
713
|
+
const elapsedStr = (0, review_polling_js_1.formatElapsed)(Date.now() - Date.parse(stateAny['startedAt']));
|
|
714
|
+
const recoveryPrompt = `RECOVERY: Reviewer timeout after ${elapsedStr}. Please complete the review for ${branch}.`;
|
|
715
|
+
log(fmt.status('INFO', `Round ${attempt}: relaunching reviewer (${reviewer}) with recovery prompt (retry ${stateAny['reviewerRetryCount']}/3)...`));
|
|
716
|
+
let relaunchResult;
|
|
717
|
+
try {
|
|
718
|
+
relaunchResult = await startAgentFn('review', {
|
|
719
|
+
agent: reviewer,
|
|
720
|
+
prompt: (actualReviewer) => buildCompactReviewPromptFn({ reviewer: reviewer, branch, implementer: implementer, focus, attempt, repoRoot: worktree, missionPath: effectiveMissionPath || undefined, actualReviewer }) + '\n\n' + recoveryPrompt,
|
|
721
|
+
worktree, slug, role: 'reviewer', exclude: [implementer]
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
catch (err) {
|
|
725
|
+
error(fmt.status('FAIL', `Could not relaunch reviewer agent (${reviewer}): ${err.message}`));
|
|
726
|
+
exit(1);
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
reviewer = applyAgentFallbackFn({
|
|
730
|
+
role: 'reviewer', original: reviewer, launchResult: relaunchResult,
|
|
731
|
+
state: state, slug, worktree, taskResolution, log, writeReviewStateFn, enforceTaskAssigneeFn
|
|
732
|
+
});
|
|
733
|
+
const retryArtifacts = await consumeReviewerArtifactsFn(slug, reviewer, {
|
|
734
|
+
worktree,
|
|
735
|
+
tmpDir: artifactDir,
|
|
736
|
+
fallbackToTmp: true,
|
|
737
|
+
readTokenFn,
|
|
738
|
+
getCommentsFn: getCommentsFn,
|
|
739
|
+
postCommentFn,
|
|
740
|
+
postReviewFn,
|
|
741
|
+
buildMetadataFooterFn: review_artifacts_js_1.buildMetadataFooter,
|
|
742
|
+
forgejoEnabled,
|
|
743
|
+
log,
|
|
744
|
+
error
|
|
745
|
+
});
|
|
746
|
+
if (retryArtifacts.consumed) {
|
|
747
|
+
if (!retryArtifacts.ok) {
|
|
748
|
+
exit(1);
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
reviewState = retryArtifacts.reviewState;
|
|
752
|
+
}
|
|
753
|
+
if (!reviewState && forgejoEnabled) {
|
|
754
|
+
reviewState = await pollForReviewFn(prNumber, reviewer, state.startedAt, token, {
|
|
755
|
+
getLatestReviewForPrFn, sleepFn, intervalMs: pollIntervalMs, timeoutMs: pollTimeoutMs, retryCount: state.reviewerRetryCount, verbose, label: `round ${attempt} review retry ${state.reviewerRetryCount}`, log
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
if (!(0, review_polling_js_1.isPollTimeout)(reviewState)) {
|
|
759
|
+
break;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
if ((0, review_polling_js_1.isPollTimeout)(reviewState)) {
|
|
763
|
+
log(fmt.status('INFO', `Autonomous review stopped: excessive reviewer timeout retries`));
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
else if (!reviewState) {
|
|
768
|
+
error(fmt.status('FAIL', `Reviewer ${reviewer} did not submit a formal review outcome for ${branch}.`));
|
|
769
|
+
error(' The reviewer agent may have exited without posting to the review PR.');
|
|
770
|
+
exit(1);
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
if (dryRun) {
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
if (!reviewState) {
|
|
778
|
+
error(fmt.status('FAIL', `Reviewer ${reviewer} did not submit a formal review outcome for ${branch}.`));
|
|
779
|
+
error(' The reviewer agent may have exited without posting to the review PR.');
|
|
780
|
+
exit(1);
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
if ((0, review_polling_js_1.isPollTimeout)(reviewState)) {
|
|
784
|
+
log(fmt.status('INFO', `Autonomous review stopped: excessive reviewer timeout retries`));
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
log(fmt.status('INFO', `Round ${attempt}: reviewer outcome = ${reviewState}`));
|
|
788
|
+
if (reviewState === 'APPROVED') {
|
|
789
|
+
state.transitionTo('approved');
|
|
790
|
+
state.disposition = reviewState;
|
|
791
|
+
writeReviewStateFn(slug, state, worktree);
|
|
792
|
+
log(fmt.status('PASS', 'Autonomous review stopped: reviewer approved the PR. Hand off to human review/integration.'));
|
|
793
|
+
transitionVirtualFn(transitionTaskFn, slug, 'approved', { log });
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
state.transitionTo('fixing');
|
|
797
|
+
state.disposition = reviewState;
|
|
771
798
|
}
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
799
|
+
else {
|
|
800
|
+
// Resume in fixing phase, need reviewState
|
|
801
|
+
if (forgejoEnabled) {
|
|
802
|
+
const latestReview = await getLatestReviewForPrFn(prNumber, reviewer, state.startedAt, token);
|
|
803
|
+
reviewState = latestReview ? latestReview.state : null;
|
|
804
|
+
if (!reviewState) {
|
|
805
|
+
// Restore retry path: missing review on resume is recoverable
|
|
806
|
+
// (e.g., reviewer identity changed, state is stale, or artifact not yet
|
|
807
|
+
// discoverable). Treat as request-changes so the fixing phase proceeds
|
|
808
|
+
// and can re-trigger the review on the next attempt.
|
|
809
|
+
log(fmt.status('WARN', `No review found for ${reviewer} since ${state.startedAt}; treating as request-changes and proceeding to fixing phase.`));
|
|
810
|
+
reviewState = 'request-changes';
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
else {
|
|
814
|
+
// When the provider is disabled, use persisted review state.
|
|
815
|
+
reviewState = state.disposition || null;
|
|
816
|
+
if (!reviewState) {
|
|
817
|
+
// Restore retry path: missing local review state on resume is recoverable.
|
|
818
|
+
// Treat as request-changes so the fixing phase proceeds.
|
|
819
|
+
log(fmt.status('WARN', `No local review state found for ${reviewer}; treating as request-changes and proceeding to fixing phase.`));
|
|
820
|
+
reviewState = 'request-changes';
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
log(fmt.status('INFO', `Round ${attempt}: resuming in fixing phase with review outcome = ${reviewState}`));
|
|
824
|
+
}
|
|
825
|
+
// Fixing phase
|
|
826
|
+
let disposition;
|
|
827
|
+
let reLaunch;
|
|
828
|
+
let sinceIso = state.startedAt;
|
|
829
|
+
if (isContinue && attempt === state.round) {
|
|
830
|
+
if (forgejoEnabled) {
|
|
831
|
+
log(fmt.status('INFO', `Round ${attempt}: checking for existing disposition by ${implementer} since ${state.startedAt}...`));
|
|
832
|
+
disposition = await pollForDispositionFn(prNumber, implementer, state.startedAt, token, {
|
|
833
|
+
getLatestDispositionForPrFn, sleepFn, intervalMs: 1000, timeoutMs: CONTINUE_SKIP_CHECK_TIMEOUT_MS, verbose, label: `round ${attempt} skip-check`, log
|
|
834
|
+
});
|
|
835
|
+
// Skip-check timeout means no existing disposition - treat as null to proceed with initial launch
|
|
836
|
+
if ((0, review_polling_js_1.isPollTimeout)(disposition)) {
|
|
837
|
+
disposition = null;
|
|
838
|
+
}
|
|
839
|
+
else if (isContinue && (disposition === 'BLOCKED' || disposition === 'PARKED')) {
|
|
840
|
+
// Stale BLOCKED/PARKED from a previous round: re-launch the implementer
|
|
841
|
+
// so they can post a fresh disposition reflecting the current state of the branch.
|
|
842
|
+
log(fmt.status('INFO', `Round ${attempt}: implementer disposition found (${disposition}). Re-launching implementer to assess whether blocker is resolved...`));
|
|
843
|
+
reLaunch = true;
|
|
844
|
+
// Capture a fresh sinceIso so the post-relaunch poll doesn't
|
|
845
|
+
// immediately return the same stale disposition. Use a strictly
|
|
846
|
+
// newer timestamp than the persisted round start to avoid same-ms
|
|
847
|
+
// collisions during fast local test runs.
|
|
848
|
+
sinceIso = strictlyLaterIso(state.startedAt);
|
|
849
|
+
disposition = null;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
else {
|
|
853
|
+
// When the provider is disabled, use local review state.
|
|
854
|
+
log(fmt.status('INFO', `Round ${attempt}: review provider disabled; using workflow-owned disposition state.`));
|
|
855
|
+
disposition = null;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
if (!disposition) {
|
|
859
|
+
// First launch or re-launch after stale BLOCKED/PARKED
|
|
860
|
+
if (dryRun) {
|
|
861
|
+
log(`\n--- DRY-RUN: implementer (${implementer}) act-on-review prompt ---`);
|
|
862
|
+
log(buildActOnReviewPromptFn({ implementer: implementer, branch, attempt, repoRoot: worktree, missionPath: effectiveMissionPath || undefined, actualImplementer: '{{AGENT_NAME}}' }));
|
|
863
|
+
if (reLaunch) {
|
|
864
|
+
log(fmt.status('INFO', `Round ${attempt}: stale BLOCKED/PARKED disposition replaced by fresh implementer action.`));
|
|
865
|
+
}
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
778
868
|
writeReviewStateFn(slug, state, worktree);
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
let relaunchResult;
|
|
784
|
-
try {
|
|
785
|
-
relaunchResult = await startAgentFn('review', {
|
|
786
|
-
agent: reviewer,
|
|
787
|
-
prompt: (/** @type {string} */ actualReviewer) => buildCompactReviewPromptFn({ reviewer, branch, implementer, focus, attempt, repoRoot: worktree, missionPath: effectiveMissionPath, actualReviewer }) + '\n\n' + recoveryPrompt,
|
|
788
|
-
worktree, slug, role: 'reviewer', exclude: [implementer]
|
|
789
|
-
});
|
|
790
|
-
} catch (/** @type {unknown} */ err) {
|
|
791
|
-
error(fmt.status('FAIL', `Could not relaunch reviewer agent (${reviewer}): ${/** @type {Error} */(err).message}`));
|
|
792
|
-
exit(1); return;
|
|
869
|
+
transitionTaskFn(slug, 'active', { implementer, rootDir: worktree, log });
|
|
870
|
+
if (implementer === 'autonomous' && !forgejoEnabled) {
|
|
871
|
+
log(fmt.status('INFO', `Round ${attempt}: implementer identity is autonomous; skipping implementer launch and using local review artifacts only.`));
|
|
793
872
|
}
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
873
|
+
else {
|
|
874
|
+
log(fmt.status('INFO', `Round ${attempt}: launching implementer (${implementer}) for act-on-review...`));
|
|
875
|
+
let implementerLaunchResult;
|
|
876
|
+
try {
|
|
877
|
+
implementerLaunchResult = await startAgentFn('act-on-review', {
|
|
878
|
+
agent: implementer,
|
|
879
|
+
prompt: (actualImplementer) => buildCompactActOnReviewPromptFn({ implementer: implementer, branch, attempt, reviewOutcome: reviewState, repoRoot: worktree, missionPath: effectiveMissionPath || undefined, actualImplementer }),
|
|
880
|
+
worktree, slug, role: 'implementer', exclude: [reviewer]
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
catch (err) {
|
|
884
|
+
error(fmt.status('FAIL', `Could not launch implementer agent (${implementer}): ${err.message}`));
|
|
885
|
+
exit(1);
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
implementer = applyAgentFallbackFn({
|
|
889
|
+
role: 'implementer', original: implementer, launchResult: implementerLaunchResult,
|
|
890
|
+
state: state, slug, worktree, taskResolution, log, writeReviewStateFn, enforceTaskAssigneeFn
|
|
891
|
+
});
|
|
892
|
+
// Record follow-up telemetry immediately after the implementer runs.
|
|
893
|
+
// This is intentionally a separate stored phase from the initial
|
|
894
|
+
// execute/active launch. The window is bounded to this round's launch
|
|
895
|
+
// so each act-on-review round adds only its own usage.
|
|
896
|
+
const followUpSinceMs = stageLaunchSinceMs(implementerLaunchResult?.result);
|
|
897
|
+
recordStageStatsSafeFn('active', {
|
|
898
|
+
stage: 'follow-up', slug, rootDir: worktree, worktree, implementer, reviewer,
|
|
899
|
+
result: implementerLaunchResult?.result,
|
|
900
|
+
sinceMs: followUpSinceMs || 0, log, error, state, writeReviewStateFn,
|
|
901
|
+
model: (0, product_config_js_1.resolveAgentModel)(implementer, worktree),
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
const implementerArtifacts = await consumeImplementerArtifactsFn(slug, implementer, {
|
|
905
|
+
worktree,
|
|
906
|
+
tmpDir: artifactDir,
|
|
907
|
+
fallbackToTmp: true,
|
|
908
|
+
readTokenFn,
|
|
909
|
+
getCommentsFn: getCommentsFn,
|
|
910
|
+
postCommentFn,
|
|
911
|
+
buildMetadataFooterFn: review_artifacts_js_1.buildMetadataFooter,
|
|
912
|
+
forgejoEnabled,
|
|
913
|
+
log,
|
|
914
|
+
error
|
|
812
915
|
});
|
|
813
|
-
if (
|
|
814
|
-
|
|
815
|
-
|
|
916
|
+
if (implementerArtifacts.consumed) {
|
|
917
|
+
if (!implementerArtifacts.ok) {
|
|
918
|
+
exit(1);
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
disposition = implementerArtifacts.disposition;
|
|
816
922
|
}
|
|
817
|
-
if (!
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
923
|
+
if (!disposition && forgejoEnabled) {
|
|
924
|
+
disposition = await pollForDispositionFn(prNumber, implementer, sinceIso, token, {
|
|
925
|
+
getLatestDispositionForPrFn, sleepFn, intervalMs: pollIntervalMs, timeoutMs: pollTimeoutMs, retryCount: 0, verbose, label: `round ${attempt} disposition`, log
|
|
926
|
+
});
|
|
821
927
|
}
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
928
|
+
if ((0, review_polling_js_1.isPollTimeout)(disposition)) {
|
|
929
|
+
// Timeout recovery loop for implementer
|
|
930
|
+
const stateAny = state;
|
|
931
|
+
while ((stateAny['implementerRetryCount'] || 0) < 2) {
|
|
932
|
+
stateAny['implementerRetryCount'] = (stateAny['implementerRetryCount'] || 0) + 1;
|
|
933
|
+
writeReviewStateFn(slug, state, worktree);
|
|
934
|
+
const elapsedStr = (0, review_polling_js_1.formatElapsed)(Date.now() - Date.parse(stateAny['startedAt']));
|
|
935
|
+
const recoveryPrompt = `RECOVERY: Implementer disposition timeout after ${elapsedStr}. Please provide a disposition (PUSHBACK_ALL, BLOCKED, PARKED, or continue with fixes) for ${branch}.`;
|
|
936
|
+
log(fmt.status('INFO', `Round ${attempt}: relaunching implementer (${implementer}) with recovery prompt (retry ${stateAny['implementerRetryCount']}/3)...`));
|
|
937
|
+
let relaunchResult;
|
|
938
|
+
try {
|
|
939
|
+
relaunchResult = await startAgentFn('act-on-review', {
|
|
940
|
+
agent: implementer,
|
|
941
|
+
prompt: (actualImplementer) => buildCompactActOnReviewPromptFn({ implementer: implementer, branch, attempt, reviewOutcome: reviewState, repoRoot: worktree, missionPath: effectiveMissionPath || undefined, actualImplementer }) + '\n\n' + recoveryPrompt,
|
|
942
|
+
worktree, slug, role: 'implementer', exclude: [reviewer]
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
catch (err) {
|
|
946
|
+
error(fmt.status('FAIL', `Could not relaunch implementer agent (${implementer}): ${err.message}`));
|
|
947
|
+
exit(1);
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
implementer = applyAgentFallbackFn({
|
|
951
|
+
role: 'implementer', original: implementer, launchResult: relaunchResult,
|
|
952
|
+
state: state, slug, worktree, taskResolution, log, writeReviewStateFn, enforceTaskAssigneeFn
|
|
953
|
+
});
|
|
954
|
+
const retryArtifacts = await consumeImplementerArtifactsFn(slug, implementer, {
|
|
955
|
+
worktree,
|
|
956
|
+
tmpDir: artifactDir,
|
|
957
|
+
fallbackToTmp: true,
|
|
958
|
+
readTokenFn,
|
|
959
|
+
getCommentsFn: getCommentsFn,
|
|
960
|
+
postCommentFn,
|
|
961
|
+
buildMetadataFooterFn: review_artifacts_js_1.buildMetadataFooter,
|
|
962
|
+
forgejoEnabled,
|
|
963
|
+
log,
|
|
964
|
+
error
|
|
965
|
+
});
|
|
966
|
+
if (retryArtifacts.consumed) {
|
|
967
|
+
if (!retryArtifacts.ok) {
|
|
968
|
+
exit(1);
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
disposition = retryArtifacts.disposition;
|
|
972
|
+
}
|
|
973
|
+
if (!disposition && forgejoEnabled) {
|
|
974
|
+
disposition = await pollForDispositionFn(prNumber, implementer, sinceIso, token, {
|
|
975
|
+
getLatestDispositionForPrFn, sleepFn, intervalMs: pollIntervalMs, timeoutMs: pollTimeoutMs, retryCount: state.implementerRetryCount, verbose, label: `round ${attempt} disposition retry ${state.implementerRetryCount}`, log
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
if (!(0, review_polling_js_1.isPollTimeout)(disposition)) {
|
|
979
|
+
break;
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
if ((0, review_polling_js_1.isPollTimeout)(disposition)) {
|
|
983
|
+
writeReviewStateFn(slug, state, worktree);
|
|
984
|
+
log(fmt.status('INFO', `Autonomous review stopped: excessive implementer timeout retries`));
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
else if (!disposition) {
|
|
989
|
+
error(fmt.status('FAIL', `Implementer ${implementer} did not post an autonomous review disposition comment.`));
|
|
990
|
+
exit(1);
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
// Suppress the duplicate message after re-launch
|
|
994
|
+
reLaunch = null;
|
|
834
995
|
}
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
if (!reviewState) {
|
|
840
|
-
error(fmt.status('FAIL', `Reviewer ${reviewer} did not submit a formal review outcome for ${branch}.`));
|
|
841
|
-
error(' The reviewer agent may have exited without posting to the review PR.');
|
|
842
|
-
exit(1); return;
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
if (isPollTimeout(reviewState)) {
|
|
846
|
-
log(fmt.status('INFO', `Autonomous review stopped: excessive reviewer timeout retries`));
|
|
847
|
-
return;
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
log(fmt.status('INFO', `Round ${attempt}: reviewer outcome = ${reviewState}`));
|
|
851
|
-
|
|
852
|
-
if (reviewState === 'APPROVED') {
|
|
853
|
-
state.transitionTo('approved');
|
|
854
|
-
state.disposition = reviewState;
|
|
855
|
-
writeReviewStateFn(slug, state, worktree);
|
|
856
|
-
log(fmt.status('PASS', 'Autonomous review stopped: reviewer approved the PR. Hand off to human review/integration.'));
|
|
857
|
-
transitionVirtualFn(transitionTaskFn, slug, 'approved', { rootDir: worktree, log });
|
|
858
|
-
return;
|
|
859
|
-
}
|
|
860
|
-
state.transitionTo('fixing');
|
|
861
|
-
state.disposition = reviewState;
|
|
862
|
-
} else {
|
|
863
|
-
// Resume in fixing phase, need reviewState
|
|
864
|
-
if (forgejoEnabled) {
|
|
865
|
-
const latestReview = await getLatestReviewForPrFn(prNumber, reviewer, state.startedAt, token);
|
|
866
|
-
reviewState = latestReview ? latestReview.state : null;
|
|
867
|
-
if (!reviewState) {
|
|
868
|
-
// Restore retry path: missing review on resume is recoverable
|
|
869
|
-
// (e.g., reviewer identity changed, state is stale, or artifact not yet
|
|
870
|
-
// discoverable). Treat as request-changes so the fixing phase proceeds
|
|
871
|
-
// and can re-trigger the review on the next attempt.
|
|
872
|
-
log(fmt.status('WARN', `No review found for ${reviewer} since ${state.startedAt}; treating as request-changes and proceeding to fixing phase.`));
|
|
873
|
-
reviewState = 'request-changes';
|
|
996
|
+
else if (reLaunch) {
|
|
997
|
+
// Re-launch path: stale BLOCKED/PARKED was replaced by fresh implementation.
|
|
998
|
+
// The disposition was set by the re-launch above (lines 767-785).
|
|
999
|
+
// Fall through to disposition handling below.
|
|
874
1000
|
}
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
if (!reviewState) {
|
|
879
|
-
// Restore retry path: missing local review state on resume is recoverable.
|
|
880
|
-
// Treat as request-changes so the fixing phase proceeds.
|
|
881
|
-
log(fmt.status('WARN', `No local review state found for ${reviewer}; treating as request-changes and proceeding to fixing phase.`));
|
|
882
|
-
reviewState = 'request-changes';
|
|
1001
|
+
else {
|
|
1002
|
+
// Skip-check found an existing disposition that is not BLOCKED/PARKED (or reLaunch flag).
|
|
1003
|
+
log(fmt.status('INFO', `Round ${attempt}: implementer disposition found (${disposition}). Skipping implementer launch.`));
|
|
883
1004
|
}
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
// Fixing phase
|
|
889
|
-
let disposition;
|
|
890
|
-
let reLaunch;
|
|
891
|
-
let sinceIso = state.startedAt;
|
|
892
|
-
if (isContinue && attempt === state.round) {
|
|
893
|
-
if (forgejoEnabled) {
|
|
894
|
-
log(fmt.status('INFO', `Round ${attempt}: checking for existing disposition by ${implementer} since ${state.startedAt}...`));
|
|
895
|
-
disposition = await pollForDispositionFn(prNumber, implementer, state.startedAt, token, {
|
|
896
|
-
getLatestDispositionForPrFn, sleepFn, intervalMs: 1000, timeoutMs: CONTINUE_SKIP_CHECK_TIMEOUT_MS, verbose, label: `round ${attempt} skip-check`, log
|
|
897
|
-
});
|
|
898
|
-
// Skip-check timeout means no existing disposition - treat as null to proceed with initial launch
|
|
899
|
-
if (isPollTimeout(disposition)) {
|
|
900
|
-
disposition = null;
|
|
901
|
-
} else if (isContinue && (disposition === 'BLOCKED' || disposition === 'PARKED')) {
|
|
902
|
-
// Stale BLOCKED/PARKED from a previous round: re-launch the implementer
|
|
903
|
-
// so they can post a fresh disposition reflecting the current state of the branch.
|
|
904
|
-
log(fmt.status('INFO', `Round ${attempt}: implementer disposition found (${disposition}). Re-launching implementer to assess whether blocker is resolved...`));
|
|
905
|
-
reLaunch = true;
|
|
906
|
-
// Capture a fresh sinceIso so the post-relaunch poll doesn't
|
|
907
|
-
// immediately return the same stale disposition. Use a strictly
|
|
908
|
-
// newer timestamp than the persisted round start to avoid same-ms
|
|
909
|
-
// collisions during fast local test runs.
|
|
910
|
-
sinceIso = strictlyLaterIso(state.startedAt);
|
|
911
|
-
disposition = null;
|
|
1005
|
+
if (!disposition) {
|
|
1006
|
+
error(fmt.status('FAIL', `Implementer ${implementer} did not post an autonomous review disposition comment.`));
|
|
1007
|
+
exit(1);
|
|
1008
|
+
return;
|
|
912
1009
|
}
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
if (!disposition) {
|
|
921
|
-
// First launch or re-launch after stale BLOCKED/PARKED
|
|
922
|
-
if (dryRun) {
|
|
923
|
-
log(`\n--- DRY-RUN: implementer (${implementer}) act-on-review prompt ---`);
|
|
924
|
-
log(buildActOnReviewPromptFn({ implementer, branch, attempt, repoRoot: worktree, missionPath: effectiveMissionPath, actualImplementer: '{{AGENT_NAME}}' }));
|
|
925
|
-
if (reLaunch) {
|
|
926
|
-
log(fmt.status('INFO', `Round ${attempt}: stale BLOCKED/PARKED disposition replaced by fresh implementer action.`));
|
|
1010
|
+
if ((0, review_polling_js_1.isPollTimeout)(disposition)) {
|
|
1011
|
+
writeReviewStateFn(slug, state, worktree);
|
|
1012
|
+
log(fmt.status('INFO', `Autonomous review stopped: excessive implementer timeout retries`));
|
|
1013
|
+
return;
|
|
927
1014
|
}
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
log(fmt.status('INFO', `Round ${attempt}: implementer identity is autonomous; skipping implementer launch and using local review artifacts only.`));
|
|
935
|
-
} else {
|
|
936
|
-
log(fmt.status('INFO', `Round ${attempt}: launching implementer (${implementer}) for act-on-review...`));
|
|
937
|
-
|
|
938
|
-
let implementerLaunchResult;
|
|
939
|
-
try {
|
|
940
|
-
implementerLaunchResult = await startAgentFn('act-on-review', {
|
|
941
|
-
agent: implementer,
|
|
942
|
-
prompt: (/** @type {string} */ actualImplementer) => buildCompactActOnReviewPromptFn({ implementer, branch, attempt, reviewOutcome: reviewState, repoRoot: worktree, missionPath: effectiveMissionPath, actualImplementer }),
|
|
943
|
-
worktree, slug, role: 'implementer', exclude: [reviewer]
|
|
944
|
-
});
|
|
945
|
-
} catch (/** @type {unknown} */ err) {
|
|
946
|
-
error(fmt.status('FAIL', `Could not launch implementer agent (${implementer}): ${/** @type {Error} */(err).message}`));
|
|
947
|
-
exit(1); return;
|
|
1015
|
+
log(fmt.status('INFO', `Round ${attempt}: implementer disposition = ${disposition}`));
|
|
1016
|
+
if (disposition === 'PUSHBACK_ALL') {
|
|
1017
|
+
state.disposition = disposition;
|
|
1018
|
+
writeReviewStateFn(slug, state, worktree);
|
|
1019
|
+
log(fmt.status('INFO', 'Autonomous review stopped: implementer pushed back on all remaining comments. Hand off to human review.'));
|
|
1020
|
+
return;
|
|
948
1021
|
}
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
// Record follow-up telemetry immediately after the implementer runs.
|
|
956
|
-
// This is intentionally a separate stored phase from the initial
|
|
957
|
-
// execute/active launch. The window is bounded to this round's launch
|
|
958
|
-
// so each act-on-review round adds only its own usage.
|
|
959
|
-
const followUpSinceMs = stageLaunchSinceMs((/** @type {{[key: string]: any}} */ (implementerLaunchResult))?.result);
|
|
960
|
-
recordStageStatsSafeFn('active', {
|
|
961
|
-
stage: 'follow-up', slug, rootDir: worktree, worktree, implementer, reviewer,
|
|
962
|
-
result: (/** @type {{[key: string]: any}} */ (implementerLaunchResult))?.result,
|
|
963
|
-
sinceMs: /** @type {number} */ (followUpSinceMs || 0), log, error, state, writeReviewStateFn,
|
|
964
|
-
model: resolveAgentModel(/** @type {string} */ (implementer), /** @type {string} */ (worktree)),
|
|
965
|
-
});
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
const implementerArtifacts = await consumeImplementerArtifactsFn(slug, implementer, {
|
|
969
|
-
worktree,
|
|
970
|
-
tmpDir: artifactDir,
|
|
971
|
-
fallbackToTmp: true,
|
|
972
|
-
readTokenFn,
|
|
973
|
-
getCommentsFn,
|
|
974
|
-
postCommentFn,
|
|
975
|
-
buildMetadataFooterFn: buildMetadataFooter,
|
|
976
|
-
forgejoEnabled,
|
|
977
|
-
log,
|
|
978
|
-
error
|
|
979
|
-
});
|
|
980
|
-
if (implementerArtifacts.consumed) {
|
|
981
|
-
if (!implementerArtifacts.ok) { exit(1); return; }
|
|
982
|
-
disposition = implementerArtifacts.disposition;
|
|
983
|
-
}
|
|
984
|
-
if (!disposition && forgejoEnabled) {
|
|
985
|
-
disposition = await pollForDispositionFn(prNumber, implementer, sinceIso, token, {
|
|
986
|
-
getLatestDispositionForPrFn, sleepFn, intervalMs: pollIntervalMs, timeoutMs: pollTimeoutMs, retryCount: 0, verbose, label: `round ${attempt} disposition`, log
|
|
987
|
-
});
|
|
988
|
-
}
|
|
989
|
-
|
|
990
|
-
if (isPollTimeout(disposition)) {
|
|
991
|
-
// Timeout recovery loop for implementer
|
|
992
|
-
const stateAny = /** @type {{[key: string]: any}} */ (state);
|
|
993
|
-
while ((stateAny['implementerRetryCount'] || 0) < 2) {
|
|
994
|
-
stateAny['implementerRetryCount'] = (stateAny['implementerRetryCount'] || 0) + 1;
|
|
995
|
-
writeReviewStateFn(slug, state, worktree);
|
|
996
|
-
const elapsedStr = formatElapsed(Date.now() - Date.parse(/** @type {string} */ (stateAny['startedAt'])));
|
|
997
|
-
const recoveryPrompt = `RECOVERY: Implementer disposition timeout after ${elapsedStr}. Please provide a disposition (PUSHBACK_ALL, BLOCKED, PARKED, or continue with fixes) for ${branch}.`;
|
|
998
|
-
log(fmt.status('INFO', `Round ${attempt}: relaunching implementer (${implementer}) with recovery prompt (retry ${stateAny['implementerRetryCount']}/3)...`));
|
|
999
|
-
|
|
1000
|
-
let relaunchResult;
|
|
1001
|
-
try {
|
|
1002
|
-
relaunchResult = await startAgentFn('act-on-review', {
|
|
1003
|
-
agent: implementer,
|
|
1004
|
-
prompt: (/** @type {string} */ actualImplementer) => buildCompactActOnReviewPromptFn({ implementer, branch, attempt, reviewOutcome: reviewState, repoRoot: worktree, missionPath: effectiveMissionPath, actualImplementer }) + '\n\n' + recoveryPrompt,
|
|
1005
|
-
worktree, slug, role: 'implementer', exclude: [reviewer]
|
|
1006
|
-
});
|
|
1007
|
-
} catch (/** @type {unknown} */ err) {
|
|
1008
|
-
error(fmt.status('FAIL', `Could not relaunch implementer agent (${implementer}): ${/** @type {Error} */(err).message}`));
|
|
1009
|
-
exit(1); return;
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
implementer = applyAgentFallbackFn({
|
|
1013
|
-
role: 'implementer', original: implementer, launchResult: relaunchResult,
|
|
1014
|
-
state, slug, worktree, taskResolution, log, writeReviewStateFn, enforceTaskAssigneeFn
|
|
1015
|
-
});
|
|
1016
|
-
|
|
1017
|
-
const retryArtifacts = await consumeImplementerArtifactsFn(slug, implementer, {
|
|
1018
|
-
worktree,
|
|
1019
|
-
tmpDir: artifactDir,
|
|
1020
|
-
fallbackToTmp: true,
|
|
1021
|
-
readTokenFn,
|
|
1022
|
-
getCommentsFn,
|
|
1023
|
-
postCommentFn,
|
|
1024
|
-
buildMetadataFooterFn: buildMetadataFooter,
|
|
1025
|
-
forgejoEnabled,
|
|
1026
|
-
log,
|
|
1027
|
-
error
|
|
1028
|
-
});
|
|
1029
|
-
if (retryArtifacts.consumed) {
|
|
1030
|
-
if (!retryArtifacts.ok) { exit(1); return; }
|
|
1031
|
-
disposition = retryArtifacts.disposition;
|
|
1032
|
-
}
|
|
1033
|
-
if (!disposition && forgejoEnabled) {
|
|
1034
|
-
disposition = await pollForDispositionFn(prNumber, implementer, sinceIso, token, {
|
|
1035
|
-
getLatestDispositionForPrFn, sleepFn, intervalMs: pollIntervalMs, timeoutMs: pollTimeoutMs, retryCount: state.implementerRetryCount, verbose, label: `round ${attempt} disposition retry ${state.implementerRetryCount}`, log
|
|
1036
|
-
});
|
|
1037
|
-
}
|
|
1038
|
-
|
|
1039
|
-
if (!isPollTimeout(disposition)) {break;}
|
|
1022
|
+
if (disposition === 'BLOCKED' || disposition === 'PARKED') {
|
|
1023
|
+
state.disposition = disposition;
|
|
1024
|
+
writeReviewStateFn(slug, state, worktree);
|
|
1025
|
+
log(fmt.status('INFO', `Autonomous review stopped: implementer reported ${disposition}. Hand off to human review.`));
|
|
1026
|
+
return;
|
|
1040
1027
|
}
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
log(fmt.status('INFO', `Autonomous review stopped: excessive implementer timeout retries`));
|
|
1045
|
-
return;
|
|
1028
|
+
state.disposition = disposition;
|
|
1029
|
+
try {
|
|
1030
|
+
state.transitionTo('reviewing');
|
|
1046
1031
|
}
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
}
|
|
1051
|
-
|
|
1052
|
-
// Suppress the duplicate message after re-launch
|
|
1053
|
-
reLaunch = null;
|
|
1054
|
-
} else if (reLaunch) {
|
|
1055
|
-
// Re-launch path: stale BLOCKED/PARKED was replaced by fresh implementation.
|
|
1056
|
-
// The disposition was set by the re-launch above (lines 767-785).
|
|
1057
|
-
// Fall through to disposition handling below.
|
|
1058
|
-
} else {
|
|
1059
|
-
// Skip-check found an existing disposition that is not BLOCKED/PARKED (or reLaunch flag).
|
|
1060
|
-
log(fmt.status('INFO', `Round ${attempt}: implementer disposition found (${disposition}). Skipping implementer launch.`));
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
|
-
if (!disposition) {
|
|
1064
|
-
error(fmt.status('FAIL', `Implementer ${implementer} did not post an autonomous review disposition comment.`));
|
|
1065
|
-
exit(1); return;
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
|
-
if (isPollTimeout(disposition)) {
|
|
1069
|
-
writeReviewStateFn(slug, state, worktree);
|
|
1070
|
-
log(fmt.status('INFO', `Autonomous review stopped: excessive implementer timeout retries`));
|
|
1071
|
-
return;
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
|
-
log(fmt.status('INFO', `Round ${attempt}: implementer disposition = ${disposition}`));
|
|
1075
|
-
|
|
1076
|
-
if (disposition === 'PUSHBACK_ALL') {
|
|
1077
|
-
state.disposition = disposition;
|
|
1078
|
-
writeReviewStateFn(slug, state, worktree);
|
|
1079
|
-
log(fmt.status('INFO', 'Autonomous review stopped: implementer pushed back on all remaining comments. Hand off to human review.'));
|
|
1080
|
-
return;
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
|
-
if (disposition === 'BLOCKED' || disposition === 'PARKED') {
|
|
1084
|
-
state.disposition = disposition;
|
|
1085
|
-
writeReviewStateFn(slug, state, worktree);
|
|
1086
|
-
log(fmt.status('INFO', `Autonomous review stopped: implementer reported ${disposition}. Hand off to human review.`));
|
|
1087
|
-
return;
|
|
1032
|
+
catch (_) { /* ignore */ }
|
|
1033
|
+
writeReviewStateFn(slug, state, worktree);
|
|
1034
|
+
log(fmt.status('INFO', `Round ${attempt}: implementer made changes. Continuing to round ${attempt + 1}.`));
|
|
1088
1035
|
}
|
|
1089
|
-
|
|
1090
|
-
state.disposition = disposition;
|
|
1091
|
-
try { state.transitionTo('reviewing'); } catch (_) {}
|
|
1092
|
-
writeReviewStateFn(slug, state, worktree);
|
|
1093
|
-
log(fmt.status('INFO', `Round ${attempt}: implementer made changes. Continuing to round ${attempt + 1}.`));
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
|
-
log(fmt.status('INFO', `Autonomous review stopped: reached ${maxAttempts} attempts. Hand off to human review.`));
|
|
1036
|
+
log(fmt.status('INFO', `Autonomous review stopped: reached ${maxAttempts} attempts. Hand off to human review.`));
|
|
1097
1037
|
}
|
|
1098
|
-
|
|
1099
|
-
// ============================================================================
|
|
1100
|
-
// Module Exports
|
|
1101
|
-
// ============================================================================
|
|
1102
|
-
|
|
1103
|
-
module.exports = {
|
|
1104
|
-
maybeUpdateGraphifyBeforeReview,
|
|
1105
|
-
commitSafeMissionArtifacts,
|
|
1106
|
-
rebaseBeforeReviewRound,
|
|
1107
|
-
applyAgentFallback,
|
|
1108
|
-
persistNormalizedPhaseRepair,
|
|
1109
|
-
stageLaunchSinceMs,
|
|
1110
|
-
startReviewLoop,
|
|
1111
|
-
recordStageStatsSafe
|
|
1112
|
-
};
|