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