@magnusekdahl/parallix 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config/integration-pipelines.json +9 -0
- package/config/workflow.config.schema.json +12 -0
- package/docs/adr/0032-mission-refinement-state-and-usage-budget-signals.md +10 -8
- package/docs/adr/0036-mission-sizing-and-dependency-wave-heuristics.md +14 -12
- package/docs/adr/0041-integration-pipeline-gates.md +1 -1
- package/docs/adr/0047-per-mission-change-size-budget.md +161 -0
- package/docs/adr/index.md +1 -0
- package/docs/use-cases.md +44 -0
- package/lib/agents/agents.js +149 -100
- package/lib/agents/claude-telemetry.js +14 -10
- package/lib/agents/claude.js +32 -11
- package/lib/agents/codex-telemetry.js +32 -19
- package/lib/agents/codex.js +36 -11
- package/lib/agents/limit-hit.js +42 -25
- package/lib/agents/mistral-telemetry.js +1 -1
- package/lib/agents/mistral.js +13 -3
- package/lib/agents/opencode-export.js +11 -5
- package/lib/agents/opencode-telemetry.js +75 -90
- package/lib/agents/opencode.js +86 -29
- package/lib/agents/stage-telemetry.js +3 -7
- package/lib/commands/active.js +139 -94
- package/lib/commands/checkpoint.js +3 -1
- package/lib/commands/config.js +5 -3
- package/lib/commands/coverage-gate.js +17 -10
- package/lib/commands/diff.js +11 -5
- package/lib/commands/draft.js +95 -55
- package/lib/commands/handoff.js +253 -65
- package/lib/commands/integrate.js +288 -188
- package/lib/commands/mission-start.js +39 -34
- package/lib/commands/rebase.js +32 -21
- package/lib/commands/repair-handoff.js +21 -18
- package/lib/commands/resolve-conflict.js +5 -2
- package/lib/commands/review.js +1 -0
- package/lib/commands/setup-review.js +1 -0
- package/lib/commands/stats-backfill.js +50 -41
- package/lib/commands/stats.js +653 -204
- package/lib/commands/status.js +33 -28
- package/lib/core/fmt.js +58 -15
- package/lib/core/git.js +7 -5
- package/lib/core/gitignore.js +6 -4
- package/lib/core/mission-utils.js +161 -88
- package/lib/core/nels.js +199 -0
- package/lib/core/persistent-data-migration.js +55 -23
- package/lib/core/product-config.js +44 -23
- package/lib/core/runtime-matrix.js +15 -2
- package/lib/core/spawn-tee.js +40 -13
- package/lib/core/state-map.js +32 -13
- package/lib/core/storage.js +19 -5
- package/lib/core/subagent-limit.js +28 -0
- package/lib/core/verification.js +29 -12
- package/lib/review/rebase.js +11 -3
- package/lib/review/review-adapter.js +93 -6
- package/lib/review/review-artifacts.js +89 -23
- package/lib/review/review-commands.js +139 -45
- package/lib/review/review-events.js +103 -52
- package/lib/review/review-loop.js +117 -87
- package/lib/review/review-polling.js +25 -5
- package/lib/review/review-prompts.js +36 -5
- package/lib/review/review-state.js +39 -15
- package/lib/tools/backlog.js +162 -71
- package/lib/tools/forgejo.js +290 -129
- package/lib/tools/gatekeeper.js +15 -0
- package/lib/tools/redgreen.js +220 -0
- package/lib/tools/sessions.js +12 -5
- package/lib/tools/setup-review.js +170 -70
- package/package.json +11 -3
- package/prompts/draft.md +6 -0
- package/templates/mission-scaffold.md +1 -1
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
* Handles autonomous review loop orchestration.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
const os = require('os');
|
|
8
7
|
const fs = require('fs');
|
|
9
8
|
const path = require('path');
|
|
10
9
|
const fmt = require('../core/fmt');
|
|
@@ -12,20 +11,20 @@ const { git, run } = require('../core/git');
|
|
|
12
11
|
const { findMissionDir, resolveWorktree, missionBranchName } = require('../core/mission-utils');
|
|
13
12
|
const { resolveTaskFile, getTaskImplementer, getTaskStatus, enforceTaskAssignee, transitionTask, reportTaskResolution } = require('../tools/backlog');
|
|
14
13
|
const { toVirtual, transitionVirtual } = require('../core/state-map');
|
|
15
|
-
const { getPrStatus, readToken, getLatestReviewForPr, getLatestDispositionForPr, providerAvailable, getComments, postComment, postReview,
|
|
14
|
+
const { getPrStatus, readToken, getLatestReviewForPr, getLatestDispositionForPr, providerAvailable, getComments, postComment, postReview, resolveReviewUser, isProviderEnabled } = require('./review-adapter');
|
|
16
15
|
const { buildAutonomousReviewMatrix, formatMatrixSummary } = require('../core/runtime-matrix');
|
|
17
16
|
const { buildReviewPrompt, buildActOnReviewPrompt, buildCompactReviewPrompt, buildCompactActOnReviewPrompt } = require('./review-prompts');
|
|
18
17
|
const { ReviewState, readReviewState, writeReviewState, resetReviewState, VALID_PHASES } = require('./review-state');
|
|
19
|
-
|
|
18
|
+
|
|
20
19
|
const { workflowLauncherStatus, startAgent, eligibleAgentsForStep, selectAgent } = require('../agents/agents');
|
|
21
20
|
const { performHandoff } = require('../commands/handoff');
|
|
22
21
|
const { commitSafeMissionArtifacts, rebaseBeforeReviewRound } = require('./rebase');
|
|
23
22
|
const { resolveAgentModel } = require('../core/product-config');
|
|
24
23
|
|
|
25
24
|
// Import from review-polling module
|
|
26
|
-
const {
|
|
25
|
+
const { delay, resolvePollIntervalMs, resolvePollTimeoutMs, formatElapsed, isPollTimeout, pollForReview, pollForDisposition } = require('./review-polling');
|
|
27
26
|
// Import from review-artifacts module
|
|
28
|
-
const { buildMetadataFooter,
|
|
27
|
+
const { buildMetadataFooter, resolveArtifactDir, consumeReviewerArtifacts, consumeImplementerArtifacts } = require('./review-artifacts');
|
|
29
28
|
const stats = require('../commands/stats');
|
|
30
29
|
const { resolveStageTelemetry } = require('../agents/stage-telemetry');
|
|
31
30
|
|
|
@@ -40,6 +39,11 @@ const { resolveStageTelemetry } = require('../agents/stage-telemetry');
|
|
|
40
39
|
// one launch at a time, de-duped via review-state metadata, so Claude/custom rows
|
|
41
40
|
// are cumulative too. A family switch creates a separate row instead of mixing
|
|
42
41
|
// agents in one record.
|
|
42
|
+
/**
|
|
43
|
+
* @param {string} agentFamily
|
|
44
|
+
* @param {{sessionId?: string, startedAt?: string, endedAt?: string, status?: number}} result
|
|
45
|
+
* @returns {string}
|
|
46
|
+
*/
|
|
43
47
|
function stageLaunchFingerprint(agentFamily, result) {
|
|
44
48
|
return [
|
|
45
49
|
String(agentFamily || '').trim().toLowerCase(),
|
|
@@ -50,29 +54,43 @@ function stageLaunchFingerprint(agentFamily, result) {
|
|
|
50
54
|
].join('|');
|
|
51
55
|
}
|
|
52
56
|
|
|
53
|
-
|
|
54
|
-
|
|
57
|
+
/**
|
|
58
|
+
* @param {Object|null} state
|
|
59
|
+
* @param {{stage: string, agentFamily: string, result?: {[key: string]: any}, slug: string, worktree?: string, writeReviewStateFn?: Function}} opts
|
|
60
|
+
* @returns {boolean}
|
|
61
|
+
*/
|
|
62
|
+
function markStageLaunchRecorded(state, /** @type {{stage: string, agentFamily: string, result?: {[key: string]: any}, slug: string, worktree?: string, writeReviewStateFn?: Function}} */ opts) {
|
|
63
|
+
const { stage, agentFamily, result, slug, worktree, writeReviewStateFn = writeReviewState } = opts || {};
|
|
64
|
+
if (!state || !agentFamily) {return true;}
|
|
55
65
|
const key = stageWindowKey(stage, agentFamily);
|
|
56
|
-
const fingerprint = stageLaunchFingerprint(agentFamily, result);
|
|
57
|
-
|
|
58
|
-
|
|
66
|
+
const fingerprint = stageLaunchFingerprint(agentFamily, /** @type {{sessionId?: string, startedAt?: string, endedAt?: string, status?: number}} */ (result));
|
|
67
|
+
/** @type {{[key: string]: any}} */
|
|
68
|
+
const stateObj = /** @type {{[key: string]: any}} */ (state);
|
|
69
|
+
const recordedStageLaunches = stateObj.metadata && typeof stateObj.metadata === 'object'
|
|
70
|
+
? (stateObj.metadata.recordedStageLaunches || {})
|
|
59
71
|
: {};
|
|
60
72
|
const recorded = Array.isArray(recordedStageLaunches[key]) ? recordedStageLaunches[key] : [];
|
|
61
73
|
if (recorded.includes(fingerprint)) {
|
|
62
74
|
return false;
|
|
63
75
|
}
|
|
64
|
-
if (!
|
|
65
|
-
|
|
76
|
+
if (!stateObj.metadata || typeof stateObj.metadata !== 'object') {
|
|
77
|
+
stateObj.metadata = {};
|
|
66
78
|
}
|
|
67
|
-
|
|
68
|
-
...(
|
|
79
|
+
stateObj.metadata.recordedStageLaunches = {
|
|
80
|
+
...(stateObj.metadata.recordedStageLaunches || {}),
|
|
69
81
|
[key]: [...recorded, fingerprint].slice(-20),
|
|
70
82
|
};
|
|
71
83
|
writeReviewStateFn(slug, state, worktree);
|
|
72
84
|
return true;
|
|
73
85
|
}
|
|
74
86
|
|
|
75
|
-
|
|
87
|
+
/**
|
|
88
|
+
* @param {'review'|'active'} kind
|
|
89
|
+
* @param {{stage: string, slug: string, rootDir?: string, worktree?: string, implementer?: string, reviewer?: string, result?: {[key: string]: any}, sinceMs?: number, log?: Function, state?: {[key: string]: any}, writeReviewStateFn?: Function, model?: string|null}} opts
|
|
90
|
+
* @returns {void}
|
|
91
|
+
*/
|
|
92
|
+
function recordStageStatsSafe(kind, { stage, slug, rootDir, worktree, implementer, reviewer, result, sinceMs, log, state, writeReviewStateFn = writeReviewState, model = null }) {
|
|
93
|
+
/** @type {number} */
|
|
76
94
|
let durationMinutes = 0;
|
|
77
95
|
if (result && result.startedAt && result.endedAt) {
|
|
78
96
|
durationMinutes = (Date.parse(result.endedAt) - Date.parse(result.startedAt)) / 60000;
|
|
@@ -81,10 +99,10 @@ function recordStageStatsSafe(kind, { stage, slug, rootDir, worktree, implemente
|
|
|
81
99
|
// prefer the windowed Codex rollout, else the launcher-attached telemetry so
|
|
82
100
|
// Claude active/review stages record real tokens instead of all-zeros
|
|
83
101
|
// (task-1274), bounded to this stage by sinceMs (task-1285 F6).
|
|
84
|
-
const telemetry = resolveStageTelemetry({ worktree, result, sinceMs });
|
|
102
|
+
const telemetry = resolveStageTelemetry({ worktree: /** @type {string} */ (worktree || ''), result: /** @type {{[key: string]: any}} */ (result || {}), sinceMs: (/** @type {number | undefined} */ (sinceMs)) || 0 });
|
|
85
103
|
try {
|
|
86
104
|
const actorFamily = kind === 'review' ? reviewer : implementer;
|
|
87
|
-
if (state && !markStageLaunchRecorded(state, {
|
|
105
|
+
if (state && actorFamily && !markStageLaunchRecorded(state, {
|
|
88
106
|
stage,
|
|
89
107
|
agentFamily: actorFamily,
|
|
90
108
|
result,
|
|
@@ -94,16 +112,17 @@ function recordStageStatsSafe(kind, { stage, slug, rootDir, worktree, implemente
|
|
|
94
112
|
})) {
|
|
95
113
|
return;
|
|
96
114
|
}
|
|
97
|
-
stats.accumulateStageStats({ stage, slug, rootDir, implementer, reviewer, telemetry, durationMinutes, model });
|
|
98
|
-
} catch (err) {
|
|
115
|
+
/** @type {any} */ (stats).accumulateStageStats({ stage, slug, rootDir, implementer, reviewer, telemetry, durationMinutes, model });
|
|
116
|
+
} catch (/** @type {unknown} */ err) {
|
|
99
117
|
// Best-effort: never escalate to the fatal `error` channel.
|
|
100
|
-
log(fmt.status('WARN', `Could not record ${kind} stats for ${slug}: ${err.message}`));
|
|
118
|
+
log?.(fmt.status('WARN', `Could not record ${kind} stats for ${slug}: ${/** @type {Error} */(err).message}`));
|
|
101
119
|
}
|
|
102
120
|
}
|
|
103
121
|
|
|
104
122
|
const DEFAULT_MAX_ATTEMPTS = 5;
|
|
105
123
|
const CONTINUE_SKIP_CHECK_TIMEOUT_MS = 10_000;
|
|
106
124
|
|
|
125
|
+
/** @param {string} earlierIso @param {number} [nowMs] */
|
|
107
126
|
function strictlyLaterIso(earlierIso, nowMs = Date.now()) {
|
|
108
127
|
const earlierMs = Date.parse(earlierIso);
|
|
109
128
|
if (!Number.isFinite(earlierMs)) {
|
|
@@ -116,6 +135,7 @@ function strictlyLaterIso(earlierIso, nowMs = Date.now()) {
|
|
|
116
135
|
// Pre-review Setup Functions
|
|
117
136
|
// ============================================================================
|
|
118
137
|
|
|
138
|
+
/** @param {string} rootDir @param {{commandRunner?: Function, log?: Function}} [options] */
|
|
119
139
|
function maybeUpdateGraphifyBeforeReview(rootDir, { commandRunner = run, log = fmt.log.plain } = {}) {
|
|
120
140
|
const updateGraphifyKnowledgeGraph = require('../core/mission-utils').updateGraphifyKnowledgeGraph;
|
|
121
141
|
return updateGraphifyKnowledgeGraph({
|
|
@@ -137,18 +157,12 @@ function maybeUpdateGraphifyBeforeReview(rootDir, { commandRunner = run, log = f
|
|
|
137
157
|
// fallback identity selected by the launcher.
|
|
138
158
|
// This helper detects the fallback, persists the new identity to review state
|
|
139
159
|
// and the Backlog assignee, and returns the identity that should be polled for.
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
slug,
|
|
146
|
-
worktree,
|
|
147
|
-
taskResolution,
|
|
148
|
-
log = fmt.log.plain,
|
|
149
|
-
writeReviewStateFn = writeReviewState,
|
|
150
|
-
enforceTaskAssigneeFn = enforceTaskAssignee
|
|
151
|
-
} = {}) {
|
|
160
|
+
/**
|
|
161
|
+
* @param {{role: string, original: string, launchResult?: {[key: string]: any}, state: {[key: string]: any}, slug: string, worktree?: string, taskResolution?: {[key: string]: any}, log?: Function, writeReviewStateFn?: Function, enforceTaskAssigneeFn?: Function}} opts
|
|
162
|
+
* @returns {string}
|
|
163
|
+
*/
|
|
164
|
+
function applyAgentFallback(/** @type {{role: string, original: string, launchResult?: {[key: string]: any}, state: {[key: string]: any}, slug: string, worktree?: string, taskResolution?: {[key: string]: any}, log?: Function, writeReviewStateFn?: Function, enforceTaskAssigneeFn?: Function}} */ opts) {
|
|
165
|
+
const { role, original, launchResult, state, slug, worktree, taskResolution, log = fmt.log.plain, writeReviewStateFn = writeReviewState, enforceTaskAssigneeFn } = opts;
|
|
152
166
|
if (!launchResult || !launchResult.agent || launchResult.agent === original) {
|
|
153
167
|
return original;
|
|
154
168
|
}
|
|
@@ -161,13 +175,20 @@ function applyAgentFallback({
|
|
|
161
175
|
}
|
|
162
176
|
writeReviewStateFn(slug, state, worktree);
|
|
163
177
|
if (role === 'implementer' && taskResolution && taskResolution.ok) {
|
|
164
|
-
if (!enforceTaskAssigneeFn(taskResolution.taskFile, fallback)) {
|
|
178
|
+
if (enforceTaskAssigneeFn && !enforceTaskAssigneeFn(taskResolution.taskFile, fallback)) {
|
|
165
179
|
log(fmt.status('WARN', `Could not enforce fallback implementer ${fallback} in backlog task.`));
|
|
166
180
|
}
|
|
167
181
|
}
|
|
168
182
|
return fallback;
|
|
169
183
|
}
|
|
170
184
|
|
|
185
|
+
/**
|
|
186
|
+
* @param {string} slug
|
|
187
|
+
* @param {{[key: string]: any}} state
|
|
188
|
+
* @param {string} worktree
|
|
189
|
+
* @param {{log?: Function, writeReviewStateFn?: Function}} [options]
|
|
190
|
+
* @returns {void}
|
|
191
|
+
*/
|
|
171
192
|
function persistNormalizedPhaseRepair(slug, state, worktree, { log = fmt.log.plain, writeReviewStateFn = writeReviewState } = {}) {
|
|
172
193
|
if (!state || !state.phaseOriginal || VALID_PHASES.includes(state.phaseOriginal)) {
|
|
173
194
|
return;
|
|
@@ -177,6 +198,7 @@ function persistNormalizedPhaseRepair(slug, state, worktree, { log = fmt.log.pla
|
|
|
177
198
|
state.phaseOriginal = null;
|
|
178
199
|
}
|
|
179
200
|
|
|
201
|
+
/** @param {string} stage @param {string} agentFamily */
|
|
180
202
|
function stageWindowKey(stage, agentFamily) {
|
|
181
203
|
const normalizedStage = String(stage || 'default').trim().toLowerCase() || 'default';
|
|
182
204
|
const normalizedAgent = String(agentFamily || '').trim().toLowerCase();
|
|
@@ -190,6 +212,7 @@ function stageWindowKey(stage, agentFamily) {
|
|
|
190
212
|
// instead of re-summing a cumulative-since-first-launch window every round.
|
|
191
213
|
// Resume-idempotency is provided separately by the launch fingerprint in
|
|
192
214
|
// markStageLaunchRecorded, so the window itself does not need to persist.
|
|
215
|
+
/** @param {{startedAt?: string}} result */
|
|
193
216
|
function stageLaunchSinceMs(result) {
|
|
194
217
|
const startedAt = result && result.startedAt ? String(result.startedAt) : '';
|
|
195
218
|
const startedMs = startedAt ? Date.parse(startedAt) : NaN;
|
|
@@ -200,6 +223,11 @@ function stageLaunchSinceMs(result) {
|
|
|
200
223
|
// Main Review Loop
|
|
201
224
|
// ============================================================================
|
|
202
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
|
+
*/
|
|
203
231
|
async function startReviewLoop(slug, opts = {}) {
|
|
204
232
|
let {
|
|
205
233
|
implementer,
|
|
@@ -268,7 +296,7 @@ async function startReviewLoop(slug, opts = {}) {
|
|
|
268
296
|
isContinue = Boolean(isContinue || continueFlag);
|
|
269
297
|
|
|
270
298
|
const pollIntervalMs = resolvePollIntervalMs();
|
|
271
|
-
const pollTimeoutMs = resolvePollTimeoutMs(pollTimeoutSeconds);
|
|
299
|
+
const pollTimeoutMs = resolvePollTimeoutMs(/** @type {number} */ (pollTimeoutSeconds || 0));
|
|
272
300
|
|
|
273
301
|
const worktree = callerWorktree || resolveWorktree(slug) || process.cwd();
|
|
274
302
|
const resolvedProviderAvailableFn = providerAvailableFn || forgejoAvailableFn || providerAvailable;
|
|
@@ -285,7 +313,7 @@ async function startReviewLoop(slug, opts = {}) {
|
|
|
285
313
|
// slug-derived default). Absent the flag, `effectiveMissionPath` stays
|
|
286
314
|
// undefined and the prompts fall back to the standard slug-derived path.
|
|
287
315
|
const missionDir = findMissionDir(slug, worktree, { missionPath });
|
|
288
|
-
let effectiveMissionPath;
|
|
316
|
+
let effectiveMissionPath = /** @type {string|null} */ (null);
|
|
289
317
|
if (missionPath && fs.existsSync(missionPath)) {
|
|
290
318
|
effectiveMissionPath = fs.statSync(missionPath).isDirectory()
|
|
291
319
|
? path.join(missionPath, 'MISSION.md')
|
|
@@ -385,9 +413,9 @@ async function startReviewLoop(slug, opts = {}) {
|
|
|
385
413
|
// PR exists. Only fall back to guidance when self-heal cannot yield an open PR.
|
|
386
414
|
// Emits `--push` (the command self-heal attempts and the correct manual
|
|
387
415
|
// equivalent) — never the old, frequently-wrong `--submit`.
|
|
388
|
-
const fallbackGuidance = (reason) => {
|
|
416
|
+
const fallbackGuidance = (/** @type {string|null} */ reason) => {
|
|
389
417
|
error(fmt.status('FAIL', `No open review PR found for ${branch}. Create the PR before starting the review loop.`));
|
|
390
|
-
if (reason) error(` Handoff failure: ${reason}`);
|
|
418
|
+
if (reason) {error(` Handoff failure: ${reason}`);}
|
|
391
419
|
error(` Run: px review ${slug} --push`);
|
|
392
420
|
};
|
|
393
421
|
|
|
@@ -441,7 +469,7 @@ async function startReviewLoop(slug, opts = {}) {
|
|
|
441
469
|
// Resolve reviewer after the no-PR/self-heal gate so implementation-phase
|
|
442
470
|
// guidance does not depend on workstation launcher availability.
|
|
443
471
|
let reviewerSource = 'explicit';
|
|
444
|
-
let selectErr = null;
|
|
472
|
+
/** @type {Error|null} */ let selectErr = null;
|
|
445
473
|
if (!reviewer) {
|
|
446
474
|
if (persisted && persisted.reviewer) {
|
|
447
475
|
reviewer = persisted.reviewer;
|
|
@@ -450,16 +478,16 @@ async function startReviewLoop(slug, opts = {}) {
|
|
|
450
478
|
} else {
|
|
451
479
|
try {
|
|
452
480
|
reviewer = selectAgentFn('review', { exclude: new Set([implementer]) });
|
|
453
|
-
} catch (err) {
|
|
454
|
-
selectErr = err;
|
|
455
|
-
reviewer =
|
|
481
|
+
} catch (/** @type {unknown} */ err) {
|
|
482
|
+
selectErr = /** @type {Error} */ (err);
|
|
483
|
+
reviewer = undefined;
|
|
456
484
|
}
|
|
457
485
|
reviewerSource = 'auto-derived';
|
|
458
486
|
}
|
|
459
487
|
}
|
|
460
488
|
|
|
461
489
|
if (!reviewer) {
|
|
462
|
-
const anyDifferentFamilyRunnable = agents.some(a => a !== implementer && workflowLauncherStatusFn(a).supported);
|
|
490
|
+
const anyDifferentFamilyRunnable = agents.some((/** @type {string} */ a) => a !== implementer && workflowLauncherStatusFn(a).supported);
|
|
463
491
|
const implementerRunnable = agents.includes(implementer) && workflowLauncherStatusFn(implementer).supported;
|
|
464
492
|
if (!anyDifferentFamilyRunnable && implementerRunnable) {
|
|
465
493
|
log(fmt.status('WARN', `No supported different-family reviewer found for implementer "${implementer}".`));
|
|
@@ -468,7 +496,7 @@ async function startReviewLoop(slug, opts = {}) {
|
|
|
468
496
|
reviewerSource = 'single-family-fallback';
|
|
469
497
|
} else if (!anyDifferentFamilyRunnable) {
|
|
470
498
|
if (!forgejoEnabled) {
|
|
471
|
-
|
|
499
|
+
const detail = selectErr ? `: ${(/** @type {Error} */ (selectErr)).message}` : '';
|
|
472
500
|
reviewer = 'autonomous';
|
|
473
501
|
reviewerSource = 'fallback';
|
|
474
502
|
log(fmt.status('WARN', `No reviewer could be auto-derived${detail}; defaulting to "autonomous"`));
|
|
@@ -479,7 +507,7 @@ async function startReviewLoop(slug, opts = {}) {
|
|
|
479
507
|
const reason = selectErr ? `: ${selectErr.message}` : '';
|
|
480
508
|
error(fmt.status('FAIL', `No reviewer could be auto-derived${reason}.`));
|
|
481
509
|
error('\n' + fmt.status('INFO', 'Full runtime matrix:'));
|
|
482
|
-
formatMatrixSummaryFn(buildAutonomousReviewMatrixFn()).forEach(line => error(` ${line}`));
|
|
510
|
+
formatMatrixSummaryFn(buildAutonomousReviewMatrixFn()).forEach((/** @type {string} */ line) => error(` ${line}`));
|
|
483
511
|
error('\n' + fmt.status('FAIL', `No runnable reviewer route for implementer "${implementer}".`));
|
|
484
512
|
exit(1);
|
|
485
513
|
return;
|
|
@@ -512,7 +540,7 @@ async function startReviewLoop(slug, opts = {}) {
|
|
|
512
540
|
error(` Looked for: ${reviewerStatus.detail}`);
|
|
513
541
|
}
|
|
514
542
|
error('\n' + fmt.status('INFO', 'Full runtime matrix:'));
|
|
515
|
-
formatMatrixSummaryFn(buildAutonomousReviewMatrixFn()).forEach(line => error(` ${line}`));
|
|
543
|
+
formatMatrixSummaryFn(buildAutonomousReviewMatrixFn()).forEach((/** @type {string} */ line) => error(` ${line}`));
|
|
516
544
|
error('\n' + fmt.status('FAIL', `No runnable reviewer route for implementer "${implementer}".`));
|
|
517
545
|
exit(1);
|
|
518
546
|
return;
|
|
@@ -521,7 +549,7 @@ async function startReviewLoop(slug, opts = {}) {
|
|
|
521
549
|
if (!agents.includes(reviewer)) {
|
|
522
550
|
error(fmt.status('FAIL', `Unsupported reviewer: "${reviewer}" (blocked or unsupported).`));
|
|
523
551
|
error('\n' + fmt.status('INFO', 'Full runtime matrix:'));
|
|
524
|
-
formatMatrixSummaryFn(buildAutonomousReviewMatrixFn()).forEach(line => error(` ${line}`));
|
|
552
|
+
formatMatrixSummaryFn(buildAutonomousReviewMatrixFn()).forEach((/** @type {string} */ line) => error(` ${line}`));
|
|
525
553
|
error('\n' + fmt.status('FAIL', `No runnable reviewer route for implementer "${implementer}".`));
|
|
526
554
|
exit(1);
|
|
527
555
|
return;
|
|
@@ -534,9 +562,9 @@ async function startReviewLoop(slug, opts = {}) {
|
|
|
534
562
|
if (reviewerSource === 'explicit') {
|
|
535
563
|
const reason = !agents.includes(reviewer) ? 'blocked or unsupported' : 'launcher is not available';
|
|
536
564
|
error(fmt.status('FAIL', `Unsupported reviewer: "${reviewer}" (${reason}).`));
|
|
537
|
-
if (reviewerStatus.detail) error(` Looked for: ${reviewerStatus.detail}`);
|
|
565
|
+
if (reviewerStatus.detail) {error(` Looked for: ${reviewerStatus.detail}`);}
|
|
538
566
|
error('\n' + fmt.status('INFO', 'Full runtime matrix:'));
|
|
539
|
-
formatMatrixSummaryFn(buildAutonomousReviewMatrixFn()).forEach(line => error(` ${line}`));
|
|
567
|
+
formatMatrixSummaryFn(buildAutonomousReviewMatrixFn()).forEach((/** @type {string} */ line) => error(` ${line}`));
|
|
540
568
|
error('\n' + fmt.status('FAIL', `No runnable reviewer route for implementer "${implementer}".`));
|
|
541
569
|
exit(1);
|
|
542
570
|
return;
|
|
@@ -553,13 +581,13 @@ async function startReviewLoop(slug, opts = {}) {
|
|
|
553
581
|
} else {
|
|
554
582
|
fallbackStatus = workflowLauncherStatusFn(fallback);
|
|
555
583
|
}
|
|
556
|
-
} catch (err) {
|
|
584
|
+
} catch (/** @type {unknown} */ err) {
|
|
557
585
|
fallback = null;
|
|
558
586
|
fallbackStatus = null;
|
|
559
587
|
}
|
|
560
588
|
|
|
561
589
|
if (!fallback) {
|
|
562
|
-
|
|
590
|
+
const anyDifferentFamilyRunnable = agents.some((/** @type {string} */ a) => a !== implementer && workflowLauncherStatusFn(a).supported);
|
|
563
591
|
const implementerRunnable = agents.includes(implementer) && workflowLauncherStatusFn(implementer).supported;
|
|
564
592
|
if (!anyDifferentFamilyRunnable && implementerRunnable) {
|
|
565
593
|
log(fmt.status('WARN', `No supported different-family reviewer found for implementer "${implementer}".`));
|
|
@@ -576,9 +604,9 @@ async function startReviewLoop(slug, opts = {}) {
|
|
|
576
604
|
}
|
|
577
605
|
const reason = !agents.includes(reviewer) ? 'blocked or unsupported' : 'launcher is not available';
|
|
578
606
|
error(fmt.status('FAIL', `Unsupported reviewer: "${reviewer}" (${reason}) and no unblocked different-family fallback is available.`));
|
|
579
|
-
if (reviewerStatus.detail) error(` Looked for: ${reviewerStatus.detail}`);
|
|
607
|
+
if (reviewerStatus.detail) {error(` Looked for: ${reviewerStatus.detail}`);}
|
|
580
608
|
error('\n' + fmt.status('INFO', 'Full runtime matrix:'));
|
|
581
|
-
formatMatrixSummaryFn(buildAutonomousReviewMatrixFn()).forEach(line => error(` ${line}`));
|
|
609
|
+
formatMatrixSummaryFn(buildAutonomousReviewMatrixFn()).forEach((/** @type {string} */ line) => error(` ${line}`));
|
|
582
610
|
error('\n' + fmt.status('FAIL', `No runnable reviewer route for implementer "${implementer}".`));
|
|
583
611
|
exit(1);
|
|
584
612
|
return;
|
|
@@ -641,7 +669,7 @@ async function startReviewLoop(slug, opts = {}) {
|
|
|
641
669
|
if (state.phase === 'reviewing') {
|
|
642
670
|
// Check if we can skip reviewer launch
|
|
643
671
|
if (isContinue && attempt === initialRound) {
|
|
644
|
-
if (!dryRun) transitionTaskFn(slug, 'review', { rootDir: worktree, log });
|
|
672
|
+
if (!dryRun) {transitionTaskFn(slug, 'review', { rootDir: worktree, log });}
|
|
645
673
|
if (forgejoEnabled) {
|
|
646
674
|
log(fmt.status('INFO', `Round ${attempt}: checking for existing review by ${reviewer} since ${state.startedAt}...`));
|
|
647
675
|
reviewState = await pollForReviewFn(prNumber, reviewer, state.startedAt, token, {
|
|
@@ -679,7 +707,7 @@ async function startReviewLoop(slug, opts = {}) {
|
|
|
679
707
|
});
|
|
680
708
|
if (!rebaseResult.ok) { exit(1); return; }
|
|
681
709
|
|
|
682
|
-
if (!dryRun) transitionTaskFn(slug, 'review', { rootDir: worktree, log });
|
|
710
|
+
if (!dryRun) {transitionTaskFn(slug, 'review', { rootDir: worktree, log });}
|
|
683
711
|
state.phase = 'reviewing';
|
|
684
712
|
writeReviewStateFn(slug, state, worktree);
|
|
685
713
|
if (reviewer === 'autonomous' && !forgejoEnabled) {
|
|
@@ -691,11 +719,11 @@ async function startReviewLoop(slug, opts = {}) {
|
|
|
691
719
|
try {
|
|
692
720
|
reviewerLaunchResult = await startAgentFn('review', {
|
|
693
721
|
agent: reviewer,
|
|
694
|
-
prompt: (actualReviewer) => buildCompactReviewPromptFn({ reviewer, branch, implementer, focus, attempt, repoRoot: worktree, missionPath: effectiveMissionPath, actualReviewer }),
|
|
722
|
+
prompt: (/** @type {string} */ actualReviewer) => buildCompactReviewPromptFn({ reviewer, branch, implementer, focus, attempt, repoRoot: worktree, missionPath: effectiveMissionPath, actualReviewer }),
|
|
695
723
|
worktree, slug, role: 'reviewer', exclude: [implementer]
|
|
696
724
|
});
|
|
697
|
-
} catch (err) {
|
|
698
|
-
error(fmt.status('FAIL', `Could not launch reviewer agent (${reviewer}): ${err.message}`));
|
|
725
|
+
} catch (/** @type {unknown} */ err) {
|
|
726
|
+
error(fmt.status('FAIL', `Could not launch reviewer agent (${reviewer}): ${/** @type {Error} */(err).message}`));
|
|
699
727
|
exit(1); return;
|
|
700
728
|
}
|
|
701
729
|
|
|
@@ -709,12 +737,12 @@ async function startReviewLoop(slug, opts = {}) {
|
|
|
709
737
|
// The window is bounded to this round's launch so each round adds
|
|
710
738
|
// only its own usage; accumulateStageStats sums the rounds into one
|
|
711
739
|
// cumulative row per reviewer family.
|
|
712
|
-
const reviewSinceMs = stageLaunchSinceMs(
|
|
740
|
+
const reviewSinceMs = stageLaunchSinceMs((/** @type {{[key: string]: any}} */ (reviewerLaunchResult))?.result);
|
|
713
741
|
recordStageStatsSafeFn('review', {
|
|
714
|
-
stage: 'review', slug, rootDir: worktree, worktree, reviewer, implementer,
|
|
715
|
-
result:
|
|
716
|
-
sinceMs: reviewSinceMs, log, error, state, writeReviewStateFn,
|
|
717
|
-
model: resolveAgentModel(reviewer, worktree),
|
|
742
|
+
stage: 'review', slug, rootDir: worktree, worktree, reviewer: /** @type {string} */ (reviewer), implementer,
|
|
743
|
+
result: (/** @type {{[key: string]: any}} */ (reviewerLaunchResult))?.result,
|
|
744
|
+
sinceMs: /** @type {number} */ (reviewSinceMs || 0), log, error, state, writeReviewStateFn,
|
|
745
|
+
model: resolveAgentModel(/** @type {string} */ (reviewer), /** @type {string} */ (worktree)),
|
|
718
746
|
});
|
|
719
747
|
}
|
|
720
748
|
|
|
@@ -744,22 +772,23 @@ async function startReviewLoop(slug, opts = {}) {
|
|
|
744
772
|
|
|
745
773
|
if (isPollTimeout(reviewState)) {
|
|
746
774
|
// Timeout recovery loop for reviewer
|
|
747
|
-
|
|
748
|
-
|
|
775
|
+
const stateAny = /** @type {{[key: string]: any}} */ (state);
|
|
776
|
+
while ((stateAny['reviewerRetryCount'] || 0) < 2) {
|
|
777
|
+
stateAny['reviewerRetryCount'] = (stateAny['reviewerRetryCount'] || 0) + 1;
|
|
749
778
|
writeReviewStateFn(slug, state, worktree);
|
|
750
|
-
|
|
779
|
+
const elapsedStr = formatElapsed(Date.now() - Date.parse(/** @type {string} */ (stateAny['startedAt'])));
|
|
751
780
|
const recoveryPrompt = `RECOVERY: Reviewer timeout after ${elapsedStr}. Please complete the review for ${branch}.`;
|
|
752
|
-
log(fmt.status('INFO', `Round ${attempt}: relaunching reviewer (${reviewer}) with recovery prompt (retry ${
|
|
781
|
+
log(fmt.status('INFO', `Round ${attempt}: relaunching reviewer (${reviewer}) with recovery prompt (retry ${stateAny['reviewerRetryCount']}/3)...`));
|
|
753
782
|
|
|
754
783
|
let relaunchResult;
|
|
755
784
|
try {
|
|
756
785
|
relaunchResult = await startAgentFn('review', {
|
|
757
786
|
agent: reviewer,
|
|
758
|
-
prompt: (actualReviewer) => buildCompactReviewPromptFn({ reviewer, branch, implementer, focus, attempt, repoRoot: worktree, missionPath: effectiveMissionPath, actualReviewer }) + '\n\n' + recoveryPrompt,
|
|
787
|
+
prompt: (/** @type {string} */ actualReviewer) => buildCompactReviewPromptFn({ reviewer, branch, implementer, focus, attempt, repoRoot: worktree, missionPath: effectiveMissionPath, actualReviewer }) + '\n\n' + recoveryPrompt,
|
|
759
788
|
worktree, slug, role: 'reviewer', exclude: [implementer]
|
|
760
789
|
});
|
|
761
|
-
} catch (err) {
|
|
762
|
-
error(fmt.status('FAIL', `Could not relaunch reviewer agent (${reviewer}): ${err.message}`));
|
|
790
|
+
} catch (/** @type {unknown} */ err) {
|
|
791
|
+
error(fmt.status('FAIL', `Could not relaunch reviewer agent (${reviewer}): ${/** @type {Error} */(err).message}`));
|
|
763
792
|
exit(1); return;
|
|
764
793
|
}
|
|
765
794
|
|
|
@@ -791,7 +820,7 @@ async function startReviewLoop(slug, opts = {}) {
|
|
|
791
820
|
});
|
|
792
821
|
}
|
|
793
822
|
|
|
794
|
-
if (!isPollTimeout(reviewState)) break;
|
|
823
|
+
if (!isPollTimeout(reviewState)) {break;}
|
|
795
824
|
}
|
|
796
825
|
|
|
797
826
|
if (isPollTimeout(reviewState)) {
|
|
@@ -805,7 +834,7 @@ async function startReviewLoop(slug, opts = {}) {
|
|
|
805
834
|
}
|
|
806
835
|
}
|
|
807
836
|
|
|
808
|
-
if (dryRun) return;
|
|
837
|
+
if (dryRun) {return;}
|
|
809
838
|
|
|
810
839
|
if (!reviewState) {
|
|
811
840
|
error(fmt.status('FAIL', `Reviewer ${reviewer} did not submit a formal review outcome for ${branch}.`));
|
|
@@ -910,11 +939,11 @@ async function startReviewLoop(slug, opts = {}) {
|
|
|
910
939
|
try {
|
|
911
940
|
implementerLaunchResult = await startAgentFn('act-on-review', {
|
|
912
941
|
agent: implementer,
|
|
913
|
-
prompt: (actualImplementer) => buildCompactActOnReviewPromptFn({ implementer, branch, attempt, reviewOutcome: reviewState, repoRoot: worktree, missionPath: effectiveMissionPath, actualImplementer }),
|
|
942
|
+
prompt: (/** @type {string} */ actualImplementer) => buildCompactActOnReviewPromptFn({ implementer, branch, attempt, reviewOutcome: reviewState, repoRoot: worktree, missionPath: effectiveMissionPath, actualImplementer }),
|
|
914
943
|
worktree, slug, role: 'implementer', exclude: [reviewer]
|
|
915
944
|
});
|
|
916
|
-
} catch (err) {
|
|
917
|
-
error(fmt.status('FAIL', `Could not launch implementer agent (${implementer}): ${err.message}`));
|
|
945
|
+
} catch (/** @type {unknown} */ err) {
|
|
946
|
+
error(fmt.status('FAIL', `Could not launch implementer agent (${implementer}): ${/** @type {Error} */(err).message}`));
|
|
918
947
|
exit(1); return;
|
|
919
948
|
}
|
|
920
949
|
|
|
@@ -927,12 +956,12 @@ async function startReviewLoop(slug, opts = {}) {
|
|
|
927
956
|
// This is intentionally a separate stored phase from the initial
|
|
928
957
|
// execute/active launch. The window is bounded to this round's launch
|
|
929
958
|
// so each act-on-review round adds only its own usage.
|
|
930
|
-
const followUpSinceMs = stageLaunchSinceMs(
|
|
959
|
+
const followUpSinceMs = stageLaunchSinceMs((/** @type {{[key: string]: any}} */ (implementerLaunchResult))?.result);
|
|
931
960
|
recordStageStatsSafeFn('active', {
|
|
932
961
|
stage: 'follow-up', slug, rootDir: worktree, worktree, implementer, reviewer,
|
|
933
|
-
result:
|
|
934
|
-
sinceMs: followUpSinceMs, log, error, state, writeReviewStateFn,
|
|
935
|
-
model: resolveAgentModel(implementer, worktree),
|
|
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)),
|
|
936
965
|
});
|
|
937
966
|
}
|
|
938
967
|
|
|
@@ -960,22 +989,23 @@ async function startReviewLoop(slug, opts = {}) {
|
|
|
960
989
|
|
|
961
990
|
if (isPollTimeout(disposition)) {
|
|
962
991
|
// Timeout recovery loop for implementer
|
|
963
|
-
|
|
964
|
-
|
|
992
|
+
const stateAny = /** @type {{[key: string]: any}} */ (state);
|
|
993
|
+
while ((stateAny['implementerRetryCount'] || 0) < 2) {
|
|
994
|
+
stateAny['implementerRetryCount'] = (stateAny['implementerRetryCount'] || 0) + 1;
|
|
965
995
|
writeReviewStateFn(slug, state, worktree);
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
log(fmt.status('INFO', `Round ${attempt}: relaunching implementer (${implementer}) with recovery prompt (retry ${
|
|
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)...`));
|
|
969
999
|
|
|
970
1000
|
let relaunchResult;
|
|
971
1001
|
try {
|
|
972
1002
|
relaunchResult = await startAgentFn('act-on-review', {
|
|
973
1003
|
agent: implementer,
|
|
974
|
-
prompt: (actualImplementer) => buildCompactActOnReviewPromptFn({ implementer, branch, attempt, reviewOutcome: reviewState, repoRoot: worktree, missionPath: effectiveMissionPath, actualImplementer }) + '\n\n' + recoveryPrompt,
|
|
1004
|
+
prompt: (/** @type {string} */ actualImplementer) => buildCompactActOnReviewPromptFn({ implementer, branch, attempt, reviewOutcome: reviewState, repoRoot: worktree, missionPath: effectiveMissionPath, actualImplementer }) + '\n\n' + recoveryPrompt,
|
|
975
1005
|
worktree, slug, role: 'implementer', exclude: [reviewer]
|
|
976
1006
|
});
|
|
977
|
-
} catch (err) {
|
|
978
|
-
error(fmt.status('FAIL', `Could not relaunch implementer agent (${implementer}): ${err.message}`));
|
|
1007
|
+
} catch (/** @type {unknown} */ err) {
|
|
1008
|
+
error(fmt.status('FAIL', `Could not relaunch implementer agent (${implementer}): ${/** @type {Error} */(err).message}`));
|
|
979
1009
|
exit(1); return;
|
|
980
1010
|
}
|
|
981
1011
|
|
|
@@ -1006,7 +1036,7 @@ async function startReviewLoop(slug, opts = {}) {
|
|
|
1006
1036
|
});
|
|
1007
1037
|
}
|
|
1008
1038
|
|
|
1009
|
-
if (!isPollTimeout(disposition)) break;
|
|
1039
|
+
if (!isPollTimeout(disposition)) {break;}
|
|
1010
1040
|
}
|
|
1011
1041
|
|
|
1012
1042
|
if (isPollTimeout(disposition)) {
|
|
@@ -13,6 +13,7 @@ const POLL_PROGRESS_EVERY_MS = 30_000;
|
|
|
13
13
|
|
|
14
14
|
const POLL_TIMEOUT = Object.freeze({ __isPollTimeout: true });
|
|
15
15
|
|
|
16
|
+
/** @param {number} ms */
|
|
16
17
|
function delay(ms) {
|
|
17
18
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
18
19
|
}
|
|
@@ -20,29 +21,40 @@ function delay(ms) {
|
|
|
20
21
|
function resolvePollIntervalMs() {
|
|
21
22
|
const raw = process.env.AUTONOMOUS_REVIEW_POLL_INTERVAL_MS;
|
|
22
23
|
const parsed = raw ? parseInt(raw, 10) : NaN;
|
|
23
|
-
if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_POLL_INTERVAL_MS;
|
|
24
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {return DEFAULT_POLL_INTERVAL_MS;}
|
|
24
25
|
return parsed;
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
/** @param {number} [explicitSeconds] */
|
|
27
29
|
function resolvePollTimeoutMs(explicitSeconds) {
|
|
28
|
-
if (Number.isFinite(explicitSeconds) && explicitSeconds > 0) {
|
|
30
|
+
if (typeof explicitSeconds === 'number' && Number.isFinite(explicitSeconds) && explicitSeconds > 0) {
|
|
29
31
|
return explicitSeconds * 1000;
|
|
30
32
|
}
|
|
31
33
|
const raw = process.env.AUTONOMOUS_REVIEW_POLL_TIMEOUT_MS;
|
|
32
34
|
const parsed = raw ? parseInt(raw, 10) : NaN;
|
|
33
|
-
if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_POLL_MAX_WAIT_MS;
|
|
35
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {return DEFAULT_POLL_MAX_WAIT_MS;}
|
|
34
36
|
return parsed;
|
|
35
37
|
}
|
|
36
38
|
|
|
39
|
+
/** @param {number} startMs */
|
|
37
40
|
function formatElapsed(startMs) {
|
|
38
41
|
const secs = Math.round((Date.now() - startMs) / 1000);
|
|
39
42
|
return `${secs}s`;
|
|
40
43
|
}
|
|
41
44
|
|
|
45
|
+
/** @param {*} result */
|
|
42
46
|
function isPollTimeout(result) {
|
|
43
47
|
return result === POLL_TIMEOUT;
|
|
44
48
|
}
|
|
45
49
|
|
|
50
|
+
/**
|
|
51
|
+
* @param {number} prNumber
|
|
52
|
+
* @param {string} reviewerUser
|
|
53
|
+
* @param {string} sinceIso
|
|
54
|
+
* @param {string} token
|
|
55
|
+
* @param {{getLatestReviewForPrFn?: Function, sleepFn?: Function, intervalMs?: number, timeoutMs?: number, verbose?: boolean, label?: string, retryCount?: number, log?: Function}} [options]
|
|
56
|
+
* @returns {Promise<string|typeof POLL_TIMEOUT|null>}
|
|
57
|
+
*/
|
|
46
58
|
async function pollForReview(prNumber, reviewerUser, sinceIso, token, options = {}) {
|
|
47
59
|
const {
|
|
48
60
|
getLatestReviewForPrFn = getLatestReviewForPr,
|
|
@@ -67,7 +79,7 @@ async function pollForReview(prNumber, reviewerUser, sinceIso, token, options =
|
|
|
67
79
|
while (Date.now() < deadline) {
|
|
68
80
|
const review = await getLatestReviewForPrFn(prNumber, reviewerUser, sinceIso, token);
|
|
69
81
|
if (review) {
|
|
70
|
-
if (verbose) log(fmt.status('INFO', `${label}: reviewer state=${review.state} after ${formatElapsed(start)}.`));
|
|
82
|
+
if (verbose) {log(fmt.status('INFO', `${label}: reviewer state=${review.state} after ${formatElapsed(start)}.`));}
|
|
71
83
|
return review.state;
|
|
72
84
|
}
|
|
73
85
|
|
|
@@ -86,6 +98,14 @@ async function pollForReview(prNumber, reviewerUser, sinceIso, token, options =
|
|
|
86
98
|
return POLL_TIMEOUT;
|
|
87
99
|
}
|
|
88
100
|
|
|
101
|
+
/**
|
|
102
|
+
* @param {number} prNumber
|
|
103
|
+
* @param {string} implementerUser
|
|
104
|
+
* @param {string} sinceIso
|
|
105
|
+
* @param {string} token
|
|
106
|
+
* @param {{getLatestDispositionForPrFn?: Function, sleepFn?: Function, intervalMs?: number, timeoutMs?: number, verbose?: boolean, label?: string, retryCount?: number, log?: Function}} [options]
|
|
107
|
+
* @returns {Promise<string|typeof POLL_TIMEOUT|null>}
|
|
108
|
+
*/
|
|
89
109
|
async function pollForDisposition(prNumber, implementerUser, sinceIso, token, options = {}) {
|
|
90
110
|
const {
|
|
91
111
|
getLatestDispositionForPrFn = getLatestDispositionForPr,
|
|
@@ -110,7 +130,7 @@ async function pollForDisposition(prNumber, implementerUser, sinceIso, token, op
|
|
|
110
130
|
while (Date.now() < deadline) {
|
|
111
131
|
const disposition = await getLatestDispositionForPrFn(prNumber, implementerUser, sinceIso, token);
|
|
112
132
|
if (disposition) {
|
|
113
|
-
if (verbose) log(fmt.status('INFO', `${label}: disposition=${disposition} after ${formatElapsed(start)}.`));
|
|
133
|
+
if (verbose) {log(fmt.status('INFO', `${label}: disposition=${disposition} after ${formatElapsed(start)}.`));}
|
|
114
134
|
return disposition;
|
|
115
135
|
}
|
|
116
136
|
|