@magnusekdahl/parallix 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/index.js +190 -185
  2. package/lib/agents/agents.js +737 -838
  3. package/lib/agents/agents.ts +930 -0
  4. package/lib/agents/claude-telemetry.js +149 -154
  5. package/lib/agents/claude-telemetry.ts +233 -0
  6. package/lib/agents/claude.js +121 -131
  7. package/lib/agents/claude.ts +156 -0
  8. package/lib/agents/codex-telemetry.js +176 -164
  9. package/lib/agents/codex-telemetry.ts +205 -0
  10. package/lib/agents/codex.js +168 -201
  11. package/lib/agents/codex.ts +236 -0
  12. package/lib/agents/limit-hit.js +216 -226
  13. package/lib/agents/limit-hit.ts +262 -0
  14. package/lib/agents/mistral-telemetry.js +7 -10
  15. package/lib/agents/mistral-telemetry.ts +44 -0
  16. package/lib/agents/mistral.js +40 -58
  17. package/lib/agents/mistral.ts +84 -0
  18. package/lib/agents/opencode-export.js +126 -127
  19. package/lib/agents/opencode-export.ts +160 -0
  20. package/lib/agents/opencode-telemetry.js +282 -261
  21. package/lib/agents/opencode-telemetry.ts +350 -0
  22. package/lib/agents/opencode.js +263 -305
  23. package/lib/agents/opencode.ts +370 -0
  24. package/lib/agents/stage-telemetry.js +23 -24
  25. package/lib/agents/stage-telemetry.ts +47 -0
  26. package/lib/commands/active.js +502 -579
  27. package/lib/commands/active.ts +665 -0
  28. package/lib/commands/checkpoint.js +98 -74
  29. package/lib/commands/checkpoint.ts +79 -0
  30. package/lib/commands/config.js +61 -31
  31. package/lib/commands/config.ts +47 -0
  32. package/lib/commands/coverage-gate.js +271 -246
  33. package/lib/commands/coverage-gate.ts +362 -0
  34. package/lib/commands/diff.js +140 -119
  35. package/lib/commands/diff.ts +125 -0
  36. package/lib/commands/draft.js +770 -923
  37. package/lib/commands/draft.ts +1021 -0
  38. package/lib/commands/handoff.js +588 -608
  39. package/lib/commands/handoff.ts +694 -0
  40. package/lib/commands/integrate.js +1332 -1463
  41. package/lib/commands/integrate.ts +1688 -0
  42. package/lib/commands/mission-start.js +279 -237
  43. package/lib/commands/mission-start.ts +263 -0
  44. package/lib/commands/rebase.js +606 -582
  45. package/lib/commands/rebase.ts +635 -0
  46. package/lib/commands/repair-handoff.js +181 -179
  47. package/lib/commands/repair-handoff.ts +228 -0
  48. package/lib/commands/resolve-conflict.js +121 -102
  49. package/lib/commands/resolve-conflict.ts +112 -0
  50. package/lib/commands/review.js +3 -4
  51. package/lib/commands/review.ts +14 -0
  52. package/lib/commands/setup-review.js +4 -5
  53. package/lib/commands/setup-review.ts +14 -0
  54. package/lib/commands/setup.js +3 -3
  55. package/lib/commands/setup.ts +2 -0
  56. package/lib/commands/stats-backfill.js +394 -359
  57. package/lib/commands/stats-backfill.ts +418 -0
  58. package/lib/commands/stats.js +1424 -1665
  59. package/lib/commands/stats.ts +2186 -0
  60. package/lib/commands/status.js +219 -200
  61. package/lib/commands/status.ts +219 -0
  62. package/lib/commands/verify.js +3 -1
  63. package/lib/commands/verify.ts +2 -0
  64. package/lib/core/fmt.js +140 -211
  65. package/lib/core/fmt.ts +189 -0
  66. package/lib/core/git.js +121 -102
  67. package/lib/core/git.ts +146 -0
  68. package/lib/core/gitignore.js +59 -106
  69. package/lib/core/gitignore.ts +103 -0
  70. package/lib/core/mission-utils.js +833 -875
  71. package/lib/core/mission-utils.ts +998 -0
  72. package/lib/core/nels.js +112 -127
  73. package/lib/core/nels.ts +189 -0
  74. package/lib/core/persistent-data-migration.js +247 -205
  75. package/lib/core/persistent-data-migration.ts +255 -0
  76. package/lib/core/product-config.js +400 -476
  77. package/lib/core/product-config.ts +518 -0
  78. package/lib/core/runtime-matrix.js +41 -87
  79. package/lib/core/runtime-matrix.ts +84 -0
  80. package/lib/core/spawn-tee.js +152 -192
  81. package/lib/core/spawn-tee.ts +202 -0
  82. package/lib/core/state-map.js +92 -92
  83. package/lib/core/state-map.ts +115 -0
  84. package/lib/core/storage.js +101 -129
  85. package/lib/core/storage.ts +182 -0
  86. package/lib/core/subagent-limit.js +15 -16
  87. package/lib/core/subagent-limit.ts +25 -0
  88. package/lib/core/verification.js +148 -138
  89. package/lib/core/verification.ts +200 -0
  90. package/lib/index.js +142 -59
  91. package/lib/index.ts +134 -0
  92. package/lib/review/rebase.js +142 -145
  93. package/lib/review/rebase.ts +188 -0
  94. package/lib/review/review-adapter.js +99 -87
  95. package/lib/review/review-adapter.ts +198 -0
  96. package/lib/review/review-artifacts.js +486 -629
  97. package/lib/review/review-artifacts.ts +622 -0
  98. package/lib/review/review-commands.js +1220 -1373
  99. package/lib/review/review-commands.ts +1579 -0
  100. package/lib/review/review-events.js +544 -886
  101. package/lib/review/review-events.ts +954 -0
  102. package/lib/review/review-loop.js +945 -1020
  103. package/lib/review/review-loop.ts +1174 -0
  104. package/lib/review/review-polling.js +86 -115
  105. package/lib/review/review-polling.ts +194 -0
  106. package/lib/review/review-prompts.js +154 -189
  107. package/lib/review/review-prompts.ts +182 -0
  108. package/lib/review/review-state.js +246 -225
  109. package/lib/review/review-state.ts +353 -0
  110. package/lib/review/review.js +63 -84
  111. package/lib/review/review.ts +138 -0
  112. package/lib/tools/backlog.js +694 -680
  113. package/lib/tools/backlog.ts +835 -0
  114. package/lib/tools/forgejo.js +1270 -1405
  115. package/lib/tools/forgejo.ts +1793 -0
  116. package/lib/tools/gatekeeper.js +106 -96
  117. package/lib/tools/gatekeeper.ts +124 -0
  118. package/lib/tools/redgreen.js +200 -181
  119. package/lib/tools/redgreen.ts +221 -0
  120. package/lib/tools/sessions.js +58 -52
  121. package/lib/tools/sessions.ts +81 -0
  122. package/lib/tools/setup-review.js +902 -975
  123. package/lib/tools/setup-review.ts +1152 -0
  124. package/package.json +7 -5
  125. package/prompts/draft.md +1 -0
  126. package/prompts/portfolio.md +1 -1
  127. package/px.js +226 -210
@@ -1,1471 +1,1318 @@
1
+ "use strict";
1
2
  /**
2
3
  * Review Commands Module
3
4
  * Extracted from parallix/lib/review.js for task-1201
4
5
  * Handles command dispatching and CLI command implementations.
5
6
  */
6
-
7
- const fs = require('fs');
8
- const path = require('path');
9
- const os = require('os');
10
- const fmt = require('../core/fmt');
11
- const { run, getCurrentBranch } = require('../core/git');
12
- const { findMissionDir, findMissionArea, resolveWorktree, inferSlug, findCheckpoints, missionBranchName, missionBaseDir } = require('../core/mission-utils');
13
- const { resolveTaskFile, getTaskStatus, getAcceptanceCriteria, getTaskAssignee, getTaskImplementer, reportTaskResolution, transitionTask } = require('../tools/backlog');
14
- const { toVirtual } = require('../core/state-map');
15
- const { getPrStatus, readToken, postComment, createPr, getComments, closePr, resolveReviewUser, isProviderEnabled } = require('./review-adapter');
16
- const { buildAutonomousReviewMatrix, formatMatrixSummary } = require('../core/runtime-matrix');
17
-
18
- const { readReviewState, writeReviewState, resolveReviewIdentity } = require('./review-state');
19
- const { createEvent, importAllLegacyArtifacts, ALL_EVENT_TYPES,
20
- isValidEventType, shouldMirrorToProvider, readAllEvents } = require('./review-events');
21
- const { startAgent } = require('../agents/agents');
22
- const { performHandoff } = require('../commands/handoff');
23
- const { formatVerificationCommand, runVerificationGate } = require('../core/verification');
24
- const { bootstrapReviewSurface } = require('../tools/setup-review');
25
- const { resolveReviewAdapter } = require('../core/product-config');
26
-
27
- // Import from review-artifacts module
28
- const { buildMetadataFooter, reviewArtifactPath, postWorkflowComment, postWorkflowReview, consumeReviewerArtifacts } = require('./review-artifacts');
29
-
7
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
8
+ if (k2 === undefined) k2 = k;
9
+ var desc = Object.getOwnPropertyDescriptor(m, k);
10
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
11
+ desc = { enumerable: true, get: function() { return m[k]; } };
12
+ }
13
+ Object.defineProperty(o, k2, desc);
14
+ }) : (function(o, m, k, k2) {
15
+ if (k2 === undefined) k2 = k;
16
+ o[k2] = m[k];
17
+ }));
18
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
19
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
20
+ }) : function(o, v) {
21
+ o["default"] = v;
22
+ });
23
+ var __importStar = (this && this.__importStar) || (function () {
24
+ var ownKeys = function(o) {
25
+ ownKeys = Object.getOwnPropertyNames || function (o) {
26
+ var ar = [];
27
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
28
+ return ar;
29
+ };
30
+ return ownKeys(o);
31
+ };
32
+ return function (mod) {
33
+ if (mod && mod.__esModule) return mod;
34
+ var result = {};
35
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
36
+ __setModuleDefault(result, mod);
37
+ return result;
38
+ };
39
+ })();
40
+ Object.defineProperty(exports, "__esModule", { value: true });
41
+ exports.flagValue = flagValue;
42
+ exports.readTextFlag = readTextFlag;
43
+ exports.formatStaticReviewFindings = formatStaticReviewFindings;
44
+ exports.formatStaticReviewSuccess = formatStaticReviewSuccess;
45
+ exports.postStaticReviewComment = postStaticReviewComment;
46
+ exports.performStaticReview = performStaticReview;
47
+ exports.verifyReview = verifyReview;
48
+ exports.submitForReview = submitForReview;
49
+ exports.readComments = readComments;
50
+ exports.pushRound = pushRound;
51
+ exports.showReviewStatus = showReviewStatus;
52
+ exports.commentRound = commentRound;
53
+ exports.consumeArtifacts = consumeArtifacts;
54
+ exports.submitReviewRound = submitReviewRound;
55
+ exports.closeMissionPr = closeMissionPr;
56
+ exports.createEventHandler = createEventHandler;
57
+ exports.importLegacyHandler = importLegacyHandler;
58
+ exports.review = review;
59
+ const fs = __importStar(require("fs"));
60
+ const path = __importStar(require("path"));
61
+ const os = __importStar(require("os"));
62
+ const fmt = __importStar(require("../core/fmt.js"));
63
+ const git_js_1 = require("../core/git.js");
64
+ const mission_utils_js_1 = require("../core/mission-utils.js");
65
+ const backlog_js_1 = require("../tools/backlog.js");
66
+ const state_map_js_1 = require("../core/state-map.js");
67
+ const review_adapter_js_1 = require("./review-adapter.js");
68
+ const runtime_matrix_js_1 = require("../core/runtime-matrix.js");
69
+ const review_state_js_1 = require("./review-state.js");
70
+ const review_events_js_1 = require("./review-events.js");
71
+ const agents_js_1 = require("../agents/agents.js");
72
+ const verification_js_1 = require("../core/verification.js");
73
+ const setup_review_js_1 = require("../tools/setup-review.js");
74
+ const product_config_js_1 = require("../core/product-config.js");
75
+ const review_artifacts_js_1 = require("./review-artifacts.js");
76
+ const review_loop_js_1 = require("./review-loop.js");
77
+ /** Lazily loaded handoff module. */
78
+ let _handoff = null;
79
+ async function getHandoff() {
80
+ if (!_handoff) {
81
+ _handoff = await Promise.resolve().then(() => __importStar(require('../commands/handoff.js')));
82
+ }
83
+ return _handoff;
84
+ }
30
85
  const DEFAULT_MAX_ATTEMPTS = 5;
31
-
32
86
  // ============================================================================
33
87
  // Internal Helpers
34
88
  // ============================================================================
35
-
36
- /** @param {string[]} args @param {string} flag */
37
89
  function flagValue(args, flag) {
38
- const idx = args.indexOf(flag);
39
- if (idx === -1) {return null;}
40
- const val = args[idx + 1];
41
- if (!val || val.startsWith('--')) {return null;}
42
- return val;
90
+ const idx = args.indexOf(flag);
91
+ if (idx === -1) {
92
+ return null;
93
+ }
94
+ const val = args[idx + 1];
95
+ if (!val || val.startsWith('--')) {
96
+ return null;
97
+ }
98
+ return val;
43
99
  }
44
-
45
- /**
46
- * @param {string[]} args
47
- * @param {string} inlineFlag
48
- * @param {string} fileFlag
49
- * @param {string} label
50
- * @param {{readFileSync?: Function, error?: Function, exit?: Function}} [options]
51
- * @returns {string|null}
52
- */
53
100
  function readTextFlag(args, inlineFlag, fileFlag, label, options = {}) {
54
- const readFileSync = options.readFileSync || fs.readFileSync;
55
- const error = options.error || fmt.log.plainError;
56
- const exit = options.exit || process.exit;
57
- const filePath = flagValue(args, fileFlag);
58
- if (filePath) {
59
- try {
60
- return readFileSync(filePath, 'utf8').trimEnd();
61
- } catch (err) {
62
- error(`Could not read ${label} from ${fmt.path(filePath)}: ${/** @type {Error} */(err).message}`);
63
- exit(1);
64
- return null;
65
- }
66
- }
67
-
68
- return flagValue(args, inlineFlag);
101
+ const readFileSync = options.readFileSync || fs.readFileSync;
102
+ const error = options.error || fmt.log.plainError;
103
+ const exit = options.exit || process.exit;
104
+ const filePath = flagValue(args, fileFlag);
105
+ if (filePath) {
106
+ try {
107
+ return readFileSync(filePath, 'utf8').trimEnd();
108
+ }
109
+ catch (err) {
110
+ error(`Could not read ${label} from ${fmt.path(filePath)}: ${err.message}`);
111
+ exit(1);
112
+ return null;
113
+ }
114
+ }
115
+ return flagValue(args, inlineFlag);
69
116
  }
70
-
71
- /** @param {string[]} findings */
72
117
  function formatStaticReviewFindings(findings) {
73
- const lines = [
74
- 'Static review found the following issue(s) before autonomous review:',
75
- ''
76
- ];
77
- findings.forEach((finding, index) => {
78
- lines.push(`${index + 1}. ${finding}`);
79
- });
80
- lines.push('', 'Auto-launching the act-on-review loop for follow-up.');
81
- return lines.join('\n');
118
+ const lines = [
119
+ 'Static review found the following issue(s) before autonomous review:',
120
+ ''
121
+ ];
122
+ findings.forEach((finding, index) => {
123
+ lines.push(`${index + 1}. ${finding}`);
124
+ });
125
+ lines.push('', 'Auto-launching the act-on-review loop for follow-up.');
126
+ return lines.join('\n');
82
127
  }
83
-
84
- /** @param {string} slug */
85
128
  function formatStaticReviewSuccess(slug) {
86
- return [
87
- `Static review for ${slug} found zero issues.`,
88
- '',
89
- 'Checked:',
90
- '- mission diff against the primary branch',
91
- '- checkpoint presence',
92
- '- final checkpoint Goal Check evidence',
93
- '',
94
- 'Mission remains in `review` status awaiting an actual autonomous or peer review verdict.'
95
- ].join('\n');
129
+ return [
130
+ `Static review for ${slug} found zero issues.`,
131
+ '',
132
+ 'Checked:',
133
+ '- mission diff against the primary branch',
134
+ '- checkpoint presence',
135
+ '- final checkpoint Goal Check evidence',
136
+ '',
137
+ 'Mission remains in `review` status awaiting an actual autonomous or peer review verdict.'
138
+ ].join('\n');
96
139
  }
97
-
98
- /**
99
- * @param {string} slug
100
- * @param {{log?: Function, error?: Function, getTaskStatusFn?: Function, resolveTaskFileFn?: Function, transitionTaskFn?: Function, rootDir?: string}} [options]
101
- * @returns {{repaired: boolean, skipped?: boolean, currentStatus?: string}}
102
- */
103
140
  function repairStaleActiveTaskAfterReview(slug, options = {}) {
104
- const log = options.log || fmt.log.plain;
105
- const error = options.error || fmt.log.plainError;
106
- const getTaskStatusFn = options.getTaskStatusFn || getTaskStatus;
107
- const resolveTaskFileFn = options.resolveTaskFileFn || resolveTaskFile;
108
- const transitionTaskFn = options.transitionTaskFn || transitionTask;
109
- const rootDir = options.rootDir || process.cwd();
110
-
111
- const taskResolution = resolveTaskFileFn(slug, rootDir);
112
- if (!taskResolution.ok) {
113
- return { repaired: false, skipped: true };
114
- }
115
-
116
- const currentStatus = getTaskStatusFn(taskResolution.taskFile);
117
- if (toVirtual(currentStatus) !== 'active') {
118
- return { repaired: false, skipped: true, currentStatus };
119
- }
120
-
121
- if (!transitionTaskFn(slug, 'review', { rootDir, log })) {
122
- error(fmt.status('WARN', `Could not transition backlog task ${slug} to review after recording the review outcome.`));
123
- return { repaired: false, skipped: false, currentStatus };
124
- }
125
-
126
- return { repaired: true, currentStatus };
141
+ const log = options.log || fmt.log.plain;
142
+ const error = options.error || fmt.log.plainError;
143
+ const getTaskStatusFn = options.getTaskStatusFn || backlog_js_1.getTaskStatus;
144
+ const resolveTaskFileFn = options.resolveTaskFileFn || backlog_js_1.resolveTaskFile;
145
+ const transitionTaskFn = options.transitionTaskFn || backlog_js_1.transitionTask;
146
+ const rootDir = options.rootDir || process.cwd();
147
+ const taskResolution = resolveTaskFileFn(slug, rootDir);
148
+ if (!taskResolution.ok) {
149
+ return { repaired: false, skipped: true };
150
+ }
151
+ const currentStatus = getTaskStatusFn(taskResolution.taskFile);
152
+ if ((0, state_map_js_1.toVirtual)(currentStatus || '') !== 'active') {
153
+ return { repaired: false, skipped: true, currentStatus: currentStatus ?? undefined };
154
+ }
155
+ if (!transitionTaskFn(slug, 'review', { rootDir, log })) {
156
+ error(fmt.status('WARN', `Could not transition backlog task ${slug} to review after recording the review outcome.`));
157
+ return { repaired: false, skipped: false, currentStatus: currentStatus ?? undefined };
158
+ }
159
+ return { repaired: true, currentStatus: currentStatus ?? undefined };
127
160
  }
128
-
129
- /**
130
- * @param {string} slug
131
- * @param {{worktree?: string, taskFile?: string|null, log?: Function, error?: Function}} options
132
- * @returns {Promise<{ok: boolean, dirty?: boolean, unsafe?: boolean}>}
133
- */
134
161
  async function commitPersistedReviewOutputs(slug, options = {}) {
135
- const { commitSafeMissionArtifacts } = require('./review-loop');
136
- return commitSafeMissionArtifacts(slug, options.worktree || process.cwd(), {
137
- taskFile: options.taskFile || null,
138
- log: options.log || fmt.log.plain,
139
- error: options.error || fmt.log.plainError,
140
- });
162
+ return (0, review_loop_js_1.commitSafeMissionArtifacts)(slug, options.worktree || process.cwd(), {
163
+ taskFile: options.taskFile || null,
164
+ log: options.log || fmt.log.plain,
165
+ error: options.error || fmt.log.plainError,
166
+ });
141
167
  }
142
-
143
- /**
144
- * @param {string} slug
145
- * @param {string} message
146
- * @param {{log?: Function, error?: Function, resolveTaskFileFn?: Function, getTaskImplementerFn?: Function, resolveWorktreeFn?: Function, readTokenFn?: Function, postCommentFn?: Function, resolveReviewUserFn?: Function, resolveForgejoUserFn?: Function, readReviewStateFn?: Function, buildMetadataFooterFn?: Function, rootDir?: string}} [options]
147
- * @returns {{ok: boolean, error?: string}}
148
- */
149
168
  function postStaticReviewComment(slug, message, options = {}) {
150
- const log = options.log || fmt.log.plain;
151
- const error = options.error || fmt.log.plainError;
152
- const resolveTaskFileFn = options.resolveTaskFileFn || resolveTaskFile;
153
- const getTaskImplementerFn = options.getTaskImplementerFn || getTaskImplementer;
154
- const resolveWorktreeFn = options.resolveWorktreeFn || resolveWorktree;
155
- const readTokenFn = options.readTokenFn || readToken;
156
- const postCommentFn = options.postCommentFn || postComment;
157
- const resolveReviewUserFn = options.resolveReviewUserFn || options.resolveForgejoUserFn || resolveReviewUser;
158
- const readReviewStateFn = options.readReviewStateFn || readReviewState;
159
- const buildMetadataFooterFn = options.buildMetadataFooterFn || buildMetadataFooter;
160
-
161
- const rootDir = options.rootDir || resolveWorktreeFn(slug) || process.cwd();
162
- const branch = missionBranchName(slug, rootDir);
163
- const { identityUser } = resolveReviewIdentity(slug, rootDir, {
164
- readReviewStateFn,
165
- });
166
- let resolvedUser = identityUser;
167
-
168
- if (!resolvedUser) {
169
- const taskResolution = resolveTaskFileFn(slug, rootDir);
170
- if (taskResolution.ok) {
171
- resolvedUser = getTaskImplementerFn(taskResolution.taskFile);
172
- }
173
- }
174
- if (!resolvedUser) {
175
- error('Cannot determine review identity. Set FORGEJO_USER, persist review-state.json, or assign the task implementer.');
176
- return { ok: false, error: 'missing-user' };
177
- }
178
- resolvedUser = resolveReviewUserFn(resolvedUser);
179
- const token = readTokenFn(resolvedUser, { rootDir });
180
- if (!token) {
181
- error(`No Forgejo token found for user "${resolvedUser}". Cannot post static review comment.`);
182
- return { ok: false, error: 'missing-token' };
183
- }
184
-
185
- const taggedMessage = message + buildMetadataFooterFn(slug, rootDir);
186
- log(`Posting static review comment on ${fmt.branch(branch)} as ${resolvedUser}...`);
187
- const result = postCommentFn(branch, token, taggedMessage, { reviewIdentity: resolvedUser, forgejoUser: resolvedUser });
188
-
189
- if (!result.ok) {
190
- error(`Could not post static review comment: ${result.error || 'API error'}`);
169
+ const log = options.log || fmt.log.plain;
170
+ const error = options.error || fmt.log.plainError;
171
+ const resolveTaskFileFn = options.resolveTaskFileFn || backlog_js_1.resolveTaskFile;
172
+ const getTaskImplementerFn = options.getTaskImplementerFn || backlog_js_1.getTaskImplementer;
173
+ const resolveWorktreeFn = options.resolveWorktreeFn || mission_utils_js_1.resolveWorktree;
174
+ const readTokenFn = options.readTokenFn || review_adapter_js_1.readToken;
175
+ const postCommentFn = options.postCommentFn || review_adapter_js_1.postComment;
176
+ const resolveReviewUserFn = options.resolveReviewUserFn || options.resolveForgejoUserFn || review_adapter_js_1.resolveReviewUser;
177
+ const readReviewStateFn = options.readReviewStateFn || review_state_js_1.readReviewState;
178
+ const buildMetadataFooterFn = options.buildMetadataFooterFn || review_artifacts_js_1.buildMetadataFooter;
179
+ const rootDir = options.rootDir || resolveWorktreeFn(slug) || process.cwd();
180
+ const branch = (0, mission_utils_js_1.missionBranchName)(slug, rootDir);
181
+ const { identityUser } = (0, review_state_js_1.resolveReviewIdentity)(slug, rootDir, {
182
+ readReviewStateFn,
183
+ });
184
+ let resolvedUser = identityUser;
185
+ if (!resolvedUser) {
186
+ const taskResolution = resolveTaskFileFn(slug, rootDir);
187
+ if (taskResolution.ok) {
188
+ resolvedUser = getTaskImplementerFn(taskResolution.taskFile);
189
+ }
190
+ }
191
+ if (!resolvedUser) {
192
+ error('Cannot determine review identity. Set FORGEJO_USER, persist review-state.json, or assign the task implementer.');
193
+ return { ok: false, error: 'missing-user' };
194
+ }
195
+ resolvedUser = resolveReviewUserFn(resolvedUser);
196
+ const token = readTokenFn(resolvedUser, { rootDir });
197
+ if (!token) {
198
+ error(`No Forgejo token found for user "${resolvedUser}". Cannot post static review comment.`);
199
+ return { ok: false, error: 'missing-token' };
200
+ }
201
+ const taggedMessage = message + buildMetadataFooterFn(slug, rootDir);
202
+ log(`Posting static review comment on ${fmt.branch(branch)} as ${resolvedUser}...`);
203
+ const result = postCommentFn(branch, token, taggedMessage, { reviewIdentity: resolvedUser, forgejoUser: resolvedUser });
204
+ if (!result.ok) {
205
+ error(`Could not post static review comment: ${result.error || 'API error'}`);
206
+ return result;
207
+ }
208
+ log(fmt.status('PASS', `Static review comment posted on PR for ${fmt.branch(branch)}.`));
191
209
  return result;
192
- }
193
-
194
- log(fmt.status('PASS', `Static review comment posted on PR for ${fmt.branch(branch)}.`));
195
- return result;
196
210
  }
197
-
198
211
  // ============================================================================
199
212
  // Command: performStaticReview
200
213
  // ============================================================================
201
-
202
- /**
203
- * Perform a static review of the mission branch when no PR exists.
204
- * Inspects git diff for changed files, checks the final checkpoint for
205
- * a Goal Check table, and returns findings.
206
- */
207
- /**
208
- * @param {string} slug
209
- * @param {{log?: Function, findMissionDir?: Function, findCheckpoints?: Function, readFileSync?: Function, run?: Function, resolveWorktree?: Function, missionPath?: string, rootDir?: string}} [options]
210
- * @returns {{ok: boolean, findings: string[]}}
211
- */
212
214
  function performStaticReview(slug, options = {}) {
213
- const log = options.log || fmt.log.plain;
214
- const findMissionDirFn = options.findMissionDir || findMissionDir;
215
- const findCheckpointsFn = options.findCheckpoints || findCheckpoints;
216
- const readFileSyncFn = options.readFileSync || fs.readFileSync;
217
- const runFn = options.run || run;
218
- const resolveWorktreeFn = options.resolveWorktree || resolveWorktree;
219
- const findings = [];
220
-
221
- log(`Performing static review for mission: ${fmt.slug(slug)}`);
222
-
223
- // Resolve the worktree root early so mission dir and checkpoint lookups use the same base
224
- const worktreeRoot = resolveWorktreeFn(slug);
225
- const rootDir = worktreeRoot || process.cwd();
226
-
227
- // Check if mission directory exists (with optional --mission path override)
228
- const missionDir = findMissionDirFn(slug, rootDir, { missionPath: options.missionPath });
229
- if (!missionDir) {
230
- findings.push(`Mission directory not found for slug: ${slug}`);
231
- return { ok: false, findings };
232
- }
233
- log(fmt.status('PASS', `Mission directory found: ${fmt.path(missionDir)}`));
234
-
235
- // Check if checkpoint documents exist
236
- const checkpoints = findCheckpointsFn(missionDir);
237
- if (checkpoints.length === 0) {
238
- findings.push('No checkpoint documents found. Implementation evidence is required.');
239
- return { ok: false, findings };
240
- }
241
- log(fmt.status('PASS', `Found ${checkpoints.length} checkpoint document(s).`));
242
-
243
- // Check the final checkpoint for a Goal Check table
244
- const finalCheckpoint = checkpoints[checkpoints.length - 1];
245
- try {
246
- const checkpointContent = readFileSyncFn(finalCheckpoint, 'utf8');
247
- const goalCheckMatch = checkpointContent.match(/^## Goal Check(?: Table)?\s*$/m);
248
- if (!goalCheckMatch) {
249
- findings.push(`Final checkpoint ${path.basename(finalCheckpoint)} is missing a "## Goal Check" section.`);
250
- } else {
251
- log(fmt.status('PASS', 'Final checkpoint contains "## Goal Check" section.'));
252
-
253
- // Verify goal-check table has at least one evidence row
254
- const afterHeader = checkpointContent.slice(goalCheckMatch.index + goalCheckMatch[0].length);
255
- const separatorPattern = /^\|\s*:?-+:?\s*\|/;
256
- const headerPattern = /^\| .+\| .+\| .+\|$/;
257
- const evidenceLinePattern = /^\| .+\| .+\| .+\|$/;
258
- const linesAfterHeader = afterHeader.split('\n');
259
- let foundEvidence = false;
260
- let pastHeader = false;
261
- for (const line of linesAfterHeader) {
262
- const trimmed = line.trim();
263
- if (trimmed === '') {continue;}
264
- if (!pastHeader && headerPattern.test(trimmed)) {
265
- pastHeader = true;
266
- continue;
215
+ const log = options.log || fmt.log.plain;
216
+ const findMissionDirFn = options.findMissionDir || mission_utils_js_1.findMissionDir;
217
+ const findCheckpointsFn = options.findCheckpoints || mission_utils_js_1.findCheckpoints;
218
+ const readFileSyncFn = options.readFileSync || fs.readFileSync;
219
+ const runFn = options.run || git_js_1.run;
220
+ const resolveWorktreeFn = options.resolveWorktree || mission_utils_js_1.resolveWorktree;
221
+ const findings = [];
222
+ log(`Performing static review for mission: ${fmt.slug(slug)}`);
223
+ // Resolve the worktree root early so mission dir and checkpoint lookups use the same base
224
+ const worktreeRoot = resolveWorktreeFn(slug);
225
+ const rootDir = worktreeRoot || process.cwd();
226
+ // Check if mission directory exists (with optional --mission path override)
227
+ const missionDir = findMissionDirFn(slug, rootDir, { missionPath: options.missionPath });
228
+ if (!missionDir) {
229
+ findings.push(`Mission directory not found for slug: ${slug}`);
230
+ return { ok: false, findings };
231
+ }
232
+ log(fmt.status('PASS', `Mission directory found: ${fmt.path(missionDir)}`));
233
+ // Check if checkpoint documents exist
234
+ const checkpoints = findCheckpointsFn(missionDir);
235
+ if (checkpoints.length === 0) {
236
+ findings.push('No checkpoint documents found. Implementation evidence is required.');
237
+ return { ok: false, findings };
238
+ }
239
+ log(fmt.status('PASS', `Found ${checkpoints.length} checkpoint document(s).`));
240
+ // Check the final checkpoint for a Goal Check table
241
+ const finalCheckpoint = checkpoints[checkpoints.length - 1];
242
+ try {
243
+ const checkpointContent = readFileSyncFn(finalCheckpoint, 'utf8');
244
+ const goalCheckMatch = checkpointContent.match(/^## Goal Check(?: Table)?\s*$/m);
245
+ if (!goalCheckMatch) {
246
+ findings.push(`Final checkpoint ${path.basename(finalCheckpoint)} is missing a "## Goal Check" section.`);
267
247
  }
268
- if (separatorPattern.test(trimmed)) {continue;}
269
- if (pastHeader && evidenceLinePattern.test(trimmed)) {
270
- foundEvidence = true;
271
- break;
248
+ else {
249
+ log(fmt.status('PASS', 'Final checkpoint contains "## Goal Check" section.'));
250
+ // Verify goal-check table has at least one evidence row
251
+ const afterHeader = checkpointContent.slice(goalCheckMatch.index + goalCheckMatch[0].length);
252
+ const separatorPattern = /^\|\s*:?-+:?\s*\|/;
253
+ const headerPattern = /^\| .+\| .+\| .+\|$/;
254
+ const evidenceLinePattern = /^\| .+\| .+\| .+\|$/;
255
+ const linesAfterHeader = afterHeader.split('\n');
256
+ let foundEvidence = false;
257
+ let pastHeader = false;
258
+ for (const line of linesAfterHeader) {
259
+ const trimmed = line.trim();
260
+ if (trimmed === '') {
261
+ continue;
262
+ }
263
+ if (!pastHeader && headerPattern.test(trimmed)) {
264
+ pastHeader = true;
265
+ continue;
266
+ }
267
+ if (separatorPattern.test(trimmed)) {
268
+ continue;
269
+ }
270
+ if (pastHeader && evidenceLinePattern.test(trimmed)) {
271
+ foundEvidence = true;
272
+ break;
273
+ }
274
+ break;
275
+ }
276
+ if (!foundEvidence) {
277
+ findings.push(`Final checkpoint ${path.basename(finalCheckpoint)} has "## Goal Check" section but no evidence rows.`);
278
+ }
279
+ else {
280
+ log(fmt.status('PASS', 'Goal Check table contains evidence rows.'));
281
+ }
272
282
  }
273
- break;
274
- }
275
- if (!foundEvidence) {
276
- findings.push(`Final checkpoint ${path.basename(finalCheckpoint)} has "## Goal Check" section but no evidence rows.`);
277
- } else {
278
- log(fmt.status('PASS', 'Goal Check table contains evidence rows.'));
279
- }
280
- }
281
- } catch (err) {
282
- findings.push(`Could not read final checkpoint ${path.basename(finalCheckpoint)}: ${/** @type {Error} */(err).message}`);
283
- }
284
-
285
- // Inspect git diff for changed files
286
- const baseBranch = require('../core/mission-utils').getPrimaryBranch(rootDir);
287
- const diffResult = runFn('git', ['diff', `${baseBranch}..HEAD`, '--name-only'], { cwd: rootDir });
288
- if (diffResult.status === 0 && diffResult.stdout) {
289
- const changedFiles = diffResult.stdout.trim().split('\n').filter((/** @type {string} */ f) => f.trim());
290
- const missionDirPrefix = path.relative(rootDir, missionBaseDir(rootDir)).split(path.sep).join('/') + '/';
291
- log(`Changed files in branch: ${changedFiles.join(', ')}`);
292
- // Check for unexpected areas — missions may legitimately touch many surfaces.
293
- // Derive allowed set from known workflow areas plus common repo structures,
294
- // plus any file with a recognized extension or inside the mission directory.
295
- const knownAreas = [
296
- 'parallix/', 'docs/', 'scripts/', 'config/', 'backlog/', 'forgejo/',
297
- '.agents/', '.github/', '.vscode/', '.graphifyignore',
298
- ];
299
- const knownExtensions = ['.sh', '.csv', '.json', '.yaml', '.yml', '.toml', '.lock', '.cfg', '.ini', '.env', '.txt', '.properties', '.sql', '.css', '.html', '.xml', '.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico', '.pdf', '.tar', '.gz', '.zip'];
300
- const unexpectedFiles = changedFiles.filter((/** @type {string} */ f) =>
301
- !knownAreas.some((/** @type {string} */ area) => f.startsWith(area)) &&
302
- !f.startsWith(missionDirPrefix) &&
303
- !f.endsWith('.md') &&
304
- !knownExtensions.some((/** @type {string} */ ext) => f.toLowerCase().endsWith(ext))
305
- );
306
- if (unexpectedFiles.length > 0) {
307
- log(fmt.status('WARN', `Changed files outside known areas (may be intentional): ${unexpectedFiles.join(', ')}`));
308
- } else {
309
- log(fmt.status('PASS', 'All changed files are within expected areas.'));
310
- }
311
- } else {
312
- log(fmt.status('WARN', `git diff ${baseBranch}..HEAD returned no output or failed — branch may be up to date with ${baseBranch}.`));
313
- }
314
-
315
- return { ok: findings.length === 0, findings };
283
+ }
284
+ catch (err) {
285
+ findings.push(`Could not read final checkpoint ${path.basename(finalCheckpoint)}: ${err.message}`);
286
+ }
287
+ // Inspect git diff for changed files
288
+ const baseBranch = (0, mission_utils_js_1.getPrimaryBranch)(rootDir);
289
+ const diffResult = runFn('git', ['diff', `${baseBranch}..HEAD`, '--name-only'], { cwd: rootDir });
290
+ if (diffResult.status === 0 && diffResult.stdout) {
291
+ const changedFiles = diffResult.stdout.trim().split('\n').filter((f) => f.trim());
292
+ const missionDirPrefix = path.relative(rootDir, (0, mission_utils_js_1.missionBaseDir)(rootDir)).split(path.sep).join('/') + '/';
293
+ log(`Changed files in branch: ${changedFiles.join(', ')}`);
294
+ // Check for unexpected areas — missions may legitimately touch many surfaces.
295
+ // Derive allowed set from known workflow areas plus common repo structures,
296
+ // plus any file with a recognized extension or inside the mission directory.
297
+ const knownAreas = [
298
+ 'parallix/', 'docs/', 'scripts/', 'config/', 'backlog/', 'forgejo/',
299
+ '.agents/', '.github/', '.vscode/', '.graphifyignore',
300
+ ];
301
+ const knownExtensions = ['.sh', '.csv', '.json', '.yaml', '.yml', '.toml', '.lock', '.cfg', '.ini', '.env', '.txt', '.properties', '.sql', '.css', '.html', '.xml', '.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico', '.pdf', '.tar', '.gz', '.zip'];
302
+ const unexpectedFiles = changedFiles.filter((f) => !knownAreas.some((area) => f.startsWith(area)) &&
303
+ !f.startsWith(missionDirPrefix) &&
304
+ !f.endsWith('.md') &&
305
+ !knownExtensions.some((ext) => f.toLowerCase().endsWith(ext)));
306
+ if (unexpectedFiles.length > 0) {
307
+ log(fmt.status('WARN', `Changed files outside known areas (may be intentional): ${unexpectedFiles.join(', ')}`));
308
+ }
309
+ else {
310
+ log(fmt.status('PASS', 'All changed files are within expected areas.'));
311
+ }
312
+ }
313
+ else {
314
+ log(fmt.status('WARN', `git diff ${baseBranch}..HEAD returned no output or failed — branch may be up to date with ${baseBranch}.`));
315
+ }
316
+ return { ok: findings.length === 0, findings };
316
317
  }
317
-
318
318
  // ============================================================================
319
319
  // Command: verifyReview
320
320
  // ============================================================================
321
-
322
- /**
323
- * @param {string} slug
324
- * @param {boolean|string} skipGate
325
- * @param {{log?: Function, error?: Function, exit?: Function, resolveWorktreeFn?: Function, findMissionDirFn?: Function, getCurrentBranchFn?: Function, resolveTaskFileFn?: Function, getPrStatusFn?: Function, getTaskStatusFn?: Function, toVirtualFn?: Function, findMissionAreaFn?: Function, runFn?: Function, getAcceptanceCriteriaFn?: Function, formatMatrixSummaryFn?: Function, buildAutonomousReviewMatrixFn?: Function, readReviewStateFn?: Function, isReviewProviderEnabledFn?: Function, isForgejoReviewEnabledFn?: Function, cwdFn?: Function, missionPath?: string, skipGate?: boolean}} [options]
326
- * @returns {void}
327
- */
328
321
  function verifyReview(slug, skipGate, options = {}) {
329
- const log = options.log || fmt.log.plain;
330
- const error = options.error || fmt.log.plainError;
331
- const exit = options.exit || process.exit;
332
- const resolveWorktreeFn = options.resolveWorktreeFn || resolveWorktree;
333
- const findMissionDirFn = options.findMissionDirFn || findMissionDir;
334
- const getCurrentBranchFn = options.getCurrentBranchFn || getCurrentBranch;
335
- const resolveTaskFileFn = options.resolveTaskFileFn || resolveTaskFile;
336
- const getPrStatusFn = options.getPrStatusFn || getPrStatus;
337
- const getTaskStatusFn = options.getTaskStatusFn || getTaskStatus;
338
- const toVirtualFn = options.toVirtualFn || toVirtual;
339
- const findMissionAreaFn = options.findMissionAreaFn || findMissionArea;
340
- const runFn = options.runFn || run;
341
- const getAcceptanceCriteriaFn = options.getAcceptanceCriteriaFn || getAcceptanceCriteria;
342
- const formatMatrixSummaryFn = options.formatMatrixSummaryFn || formatMatrixSummary;
343
- const buildAutonomousReviewMatrixFn = options.buildAutonomousReviewMatrixFn || buildAutonomousReviewMatrix;
344
- const readReviewStateFn = options.readReviewStateFn || readReviewState;
345
- const isReviewProviderEnabledFn = options.isReviewProviderEnabledFn || options.isForgejoReviewEnabledFn || isProviderEnabled;
346
- const cwdFn = options.cwdFn || (() => process.cwd());
347
-
348
- const worktree = resolveWorktreeFn(slug);
349
- const rootDir = worktree || cwdFn();
350
-
351
- const missionDir = findMissionDirFn(slug, rootDir, { missionPath: options.missionPath });
352
- const branch = missionBranchName(slug, rootDir);
353
- const current = getCurrentBranchFn(rootDir);
354
- const taskResolution = resolveTaskFileFn(slug, rootDir);
355
- const providerEnabled = isReviewProviderEnabledFn(rootDir);
356
- const pr = providerEnabled ? getPrStatusFn(branch, rootDir) : { exists: false };
357
- const failures = [];
358
- const warnings = [];
359
-
360
- log(`Reviewer verification for mission: ${fmt.slug(slug)}`);
361
- if (worktree) {
362
- log(`Found dedicated worktree: ${fmt.path(worktree)}`);
363
- } else {
364
- log('Using current directory as mission root.');
365
- }
366
-
367
- if (!missionDir) {
368
- failures.push('mission-dir');
369
- log(fmt.status('FAIL', `Mission directory not found for slug: ${fmt.slug(slug)}`));
370
- } else {
371
- log(fmt.status('PASS', `Mission doc: ${fmt.path(path.join(missionDir, 'MISSION.md'))}`));
372
- }
373
-
374
- if (current !== branch) {
375
- failures.push('branch');
376
- log(fmt.status('FAIL', `Branch: current branch is ${fmt.branch(current)}, expected ${fmt.branch(branch)}`));
377
- } else {
378
- log(fmt.status('PASS', `Branch: ${fmt.branch(current)}`));
379
- }
380
-
381
- let taskStatus = null;
382
- let virtualStatus = null;
383
- if (!taskResolution.ok) {
384
- failures.push('task');
385
- reportTaskResolution(taskResolution, slug, log);
386
- } else {
387
- taskStatus = getTaskStatusFn(taskResolution.taskFile);
388
- virtualStatus = toVirtualFn(taskStatus);
389
- if (taskStatus === 'review' || virtualStatus === 'approved') {
390
- log(fmt.status('PASS', `Backlog task: ${path.basename(taskResolution.taskFile)} (${taskStatus})`));
391
- } else if (taskStatus === 'done') {
392
- failures.push('task-status');
393
- log(fmt.status('FAIL', 'Backlog task: task is already done/integrated'));
394
- } else if (taskStatus === 'active' || virtualStatus === 'active') {
395
- warnings.push('task-still-active');
396
- log(fmt.status('WARN', `Backlog task: ${path.basename(taskResolution.taskFile)} is still ${taskStatus}`));
397
- } else {
398
- failures.push('task-status');
399
- log(fmt.status('FAIL', `Backlog task: unexpected status ${taskStatus}`));
400
- }
401
- }
402
-
403
- if (providerEnabled) {
404
- if (pr.exists && pr.state === 'open' && !pr.merged) {
405
- log(fmt.status('PASS', `Review PR: PR #${pr.number} is open`));
406
- } else if (pr.exists) {
407
- failures.push('pr-state');
408
- log(fmt.status('FAIL', `Review PR: expected an open PR, got state=${pr.state} merged=${pr.merged}`));
409
- } else {
410
- // No PR exists - check if task is in implementation phase
411
- if (taskResolution.ok) {
412
- const isImplementationPhase = taskStatus === 'active' || virtualStatus === 'active';
413
- if (isImplementationPhase) {
414
- // Task is still in implementation - emit warning instead of failure
415
- warnings.push('no-pr-yet');
416
- log(fmt.status('WARN', `Review PR: no PR found for ${branch}. Task is still ${taskStatus} — complete implementation and submit first: px review ${slug} --push`));
417
- } else {
418
- // Task is in post-implementation state or ambiguous - hard fail
419
- failures.push('pr-missing');
420
- log(fmt.status('FAIL', `Review PR: ${pr.raw || 'no PR found'}`));
322
+ const log = options.log || fmt.log.plain;
323
+ const error = options.error || fmt.log.plainError;
324
+ const exit = options.exit || process.exit;
325
+ const resolveWorktreeFn = options.resolveWorktreeFn || mission_utils_js_1.resolveWorktree;
326
+ const findMissionDirFn = options.findMissionDirFn || mission_utils_js_1.findMissionDir;
327
+ const getCurrentBranchFn = options.getCurrentBranchFn || git_js_1.getCurrentBranch;
328
+ const resolveTaskFileFn = options.resolveTaskFileFn || backlog_js_1.resolveTaskFile;
329
+ const getPrStatusFn = options.getPrStatusFn || review_adapter_js_1.getPrStatus;
330
+ const getTaskStatusFn = options.getTaskStatusFn || backlog_js_1.getTaskStatus;
331
+ const toVirtualFn = options.toVirtualFn || state_map_js_1.toVirtual;
332
+ const findMissionAreaFn = options.findMissionAreaFn || mission_utils_js_1.findMissionArea;
333
+ const runFn = options.runFn || git_js_1.run;
334
+ const getAcceptanceCriteriaFn = options.getAcceptanceCriteriaFn || backlog_js_1.getAcceptanceCriteria;
335
+ const formatMatrixSummaryFn = options.formatMatrixSummaryFn || runtime_matrix_js_1.formatMatrixSummary;
336
+ const buildAutonomousReviewMatrixFn = options.buildAutonomousReviewMatrixFn || runtime_matrix_js_1.buildAutonomousReviewMatrix;
337
+ const readReviewStateFn = options.readReviewStateFn || review_state_js_1.readReviewState;
338
+ const isReviewProviderEnabledFn = options.isReviewProviderEnabledFn || options.isForgejoReviewEnabledFn || review_adapter_js_1.isProviderEnabled;
339
+ const cwdFn = options.cwdFn || (() => process.cwd());
340
+ const worktree = resolveWorktreeFn(slug);
341
+ const rootDir = worktree || cwdFn();
342
+ const missionDir = findMissionDirFn(slug, rootDir, { missionPath: options.missionPath });
343
+ const branch = (0, mission_utils_js_1.missionBranchName)(slug, rootDir);
344
+ const current = getCurrentBranchFn(rootDir);
345
+ const taskResolution = resolveTaskFileFn(slug, rootDir);
346
+ const providerEnabled = isReviewProviderEnabledFn(rootDir);
347
+ const pr = providerEnabled ? getPrStatusFn(branch, rootDir) : { exists: false };
348
+ const failures = [];
349
+ const warnings = [];
350
+ log(`Reviewer verification for mission: ${fmt.slug(slug)}`);
351
+ if (worktree) {
352
+ log(`Found dedicated worktree: ${fmt.path(worktree)}`);
353
+ }
354
+ else {
355
+ log('Using current directory as mission root.');
356
+ }
357
+ if (!missionDir) {
358
+ failures.push('mission-dir');
359
+ log(fmt.status('FAIL', `Mission directory not found for slug: ${fmt.slug(slug)}`));
360
+ }
361
+ else {
362
+ log(fmt.status('PASS', `Mission doc: ${fmt.path(path.join(missionDir, 'MISSION.md'))}`));
363
+ }
364
+ if (current !== branch) {
365
+ failures.push('branch');
366
+ log(fmt.status('FAIL', `Branch: current branch is ${fmt.branch(current)}, expected ${fmt.branch(branch)}`));
367
+ }
368
+ else {
369
+ log(fmt.status('PASS', `Branch: ${fmt.branch(current)}`));
370
+ }
371
+ let taskStatus = null;
372
+ let virtualStatus = null;
373
+ if (!taskResolution.ok) {
374
+ failures.push('task');
375
+ (0, backlog_js_1.reportTaskResolution)(taskResolution, slug, log);
376
+ }
377
+ else {
378
+ taskStatus = getTaskStatusFn(taskResolution.taskFile);
379
+ virtualStatus = toVirtualFn(taskStatus || '');
380
+ if (taskStatus === 'review' || virtualStatus === 'approved') {
381
+ log(fmt.status('PASS', `Backlog task: ${path.basename(taskResolution.taskFile)} (${taskStatus})`));
421
382
  }
422
- } else {
423
- // Cannot determine task status - safe default to hard fail
424
- failures.push('pr-missing');
425
- log(fmt.status('FAIL', `Review PR: ${pr.raw || 'no PR found'}`));
426
- }
427
- }
428
- } else {
429
- log(fmt.status('INFO', 'Forgejo PR: skipped (review provider is not forgejo).'));
430
- }
431
-
432
- if (missionDir) {
433
- const area = findMissionAreaFn(missionDir);
434
- const skipGateFlag = Boolean(skipGate || options.skipGate);
435
- if (skipGateFlag) {
436
- log(fmt.status('WARN', `Verification gate skipped (--no-gate) for area ${area}`));
437
- } else {
438
- log(`Running reviewer gate: ${fmt.command(formatVerificationCommand(area, process.cwd()))}`);
439
- const verifyResult = runVerificationGate(area, { rootDir: process.cwd(), stdio: 'inherit', runFn });
440
- if (verifyResult.status !== 0) {
441
- failures.push('gate');
442
- log(fmt.status('FAIL', 'Reviewer gate failed.'));
443
- } else {
444
- log(fmt.status('PASS', 'Reviewer gate passed.'));
445
- }
446
- }
447
- }
448
-
449
- if (taskResolution.ok) {
450
- const acceptanceCriteria = getAcceptanceCriteriaFn(taskResolution.taskFile);
451
- log(fmt.status('INFO', 'Acceptance evidence checklist:'));
452
- if (acceptanceCriteria.length === 0) {
453
- log(' - No Acceptance Criteria found on the Backlog task.');
454
- } else {
455
- acceptanceCriteria.forEach((/** @type {string} */ line) => log(` ${line}`));
456
- }
457
- }
458
-
459
- log(fmt.status('INFO', 'Autonomous review runtime matrix:'));
460
- formatMatrixSummaryFn(buildAutonomousReviewMatrixFn()).forEach((/** @type {string} */ line) => log(line));
461
-
462
- // Show persisted reviewer state if present
463
- const persisted = readReviewStateFn(slug);
464
- if (persisted) {
465
- log(`Persisted reviewer state: reviewer=${fmt.agent(persisted.reviewer)} implementer=${fmt.agent(persisted.implementer)} round=${persisted.round} startedAt=${persisted.startedAt}`);
466
- }
467
-
468
- if (warnings.length > 0) {
469
- log(fmt.status('WARN', `Review verification warnings: ${warnings.join(', ')}`));
470
- }
471
-
472
- if (failures.length > 0) {
473
- error('\n' + fmt.status('INFO', 'Review verification failed. Resolve the blockers above before starting review.'));
474
- exit(1);
475
- return;
476
- }
477
-
478
- log('\n' + fmt.status('PASS', 'Review verification complete.'));
383
+ else if (taskStatus === 'done') {
384
+ failures.push('task-status');
385
+ log(fmt.status('FAIL', 'Backlog task: task is already done/integrated'));
386
+ }
387
+ else if (taskStatus === 'active' || virtualStatus === 'active') {
388
+ warnings.push('task-still-active');
389
+ log(fmt.status('WARN', `Backlog task: ${path.basename(taskResolution.taskFile)} is still ${taskStatus}`));
390
+ }
391
+ else {
392
+ failures.push('task-status');
393
+ log(fmt.status('FAIL', `Backlog task: unexpected status ${taskStatus}`));
394
+ }
395
+ }
396
+ const prAny = pr;
397
+ if (providerEnabled) {
398
+ if (prAny.exists && prAny.state === 'open' && !prAny.merged) {
399
+ log(fmt.status('PASS', `Review PR: PR #${prAny.number} is open`));
400
+ }
401
+ else if (prAny.exists) {
402
+ failures.push('pr-state');
403
+ log(fmt.status('FAIL', `Review PR: expected an open PR, got state=${prAny.state} merged=${prAny.merged}`));
404
+ }
405
+ else {
406
+ // No PR exists - check if task is in implementation phase
407
+ if (taskResolution.ok) {
408
+ const isImplementationPhase = taskStatus === 'active' || virtualStatus === 'active';
409
+ if (isImplementationPhase) {
410
+ // Task is still in implementation - emit warning instead of failure
411
+ warnings.push('no-pr-yet');
412
+ log(fmt.status('WARN', `Review PR: no PR found for ${branch}. Task is still ${taskStatus} — complete implementation and submit first: px review ${slug} --push`));
413
+ }
414
+ else {
415
+ // Task is in post-implementation state or ambiguous - hard fail
416
+ failures.push('pr-missing');
417
+ log(fmt.status('FAIL', `Review PR: ${prAny.raw || 'no PR found'}`));
418
+ }
419
+ }
420
+ else {
421
+ // Cannot determine task status - safe default to hard fail
422
+ failures.push('pr-missing');
423
+ log(fmt.status('FAIL', `Review PR: ${prAny.raw || 'no PR found'}`));
424
+ }
425
+ }
426
+ }
427
+ else {
428
+ log(fmt.status('INFO', 'Forgejo PR: skipped (review provider is not forgejo).'));
429
+ }
430
+ if (missionDir) {
431
+ const area = findMissionAreaFn(missionDir);
432
+ const skipGateFlag = Boolean(skipGate || options.skipGate);
433
+ if (skipGateFlag) {
434
+ log(fmt.status('WARN', `Verification gate skipped (--no-gate) for area ${area}`));
435
+ }
436
+ else {
437
+ log(`Running reviewer gate: ${fmt.command((0, verification_js_1.formatVerificationCommand)(area, process.cwd()))}`);
438
+ const verifyResult = (0, verification_js_1.runVerificationGate)(area, { rootDir: process.cwd(), stdio: 'inherit', runFn });
439
+ if (verifyResult.status !== 0) {
440
+ failures.push('gate');
441
+ log(fmt.status('FAIL', 'Reviewer gate failed.'));
442
+ }
443
+ else {
444
+ log(fmt.status('PASS', 'Reviewer gate passed.'));
445
+ }
446
+ }
447
+ }
448
+ if (taskResolution.ok) {
449
+ const acceptanceCriteria = getAcceptanceCriteriaFn(taskResolution.taskFile);
450
+ log(fmt.status('INFO', 'Acceptance evidence checklist:'));
451
+ if (acceptanceCriteria.length === 0) {
452
+ log(' - No Acceptance Criteria found on the Backlog task.');
453
+ }
454
+ else {
455
+ acceptanceCriteria.forEach((line) => log(` ${line}`));
456
+ }
457
+ }
458
+ log(fmt.status('INFO', 'Autonomous review runtime matrix:'));
459
+ formatMatrixSummaryFn(buildAutonomousReviewMatrixFn()).forEach((line) => log(line));
460
+ // Show persisted reviewer state if present
461
+ const persisted = readReviewStateFn(slug);
462
+ if (persisted) {
463
+ log(`Persisted reviewer state: reviewer=${fmt.agent(persisted.reviewer ?? '')} implementer=${fmt.agent(persisted.implementer ?? '')} round=${persisted.round} startedAt=${persisted.startedAt}`);
464
+ }
465
+ if (warnings.length > 0) {
466
+ log(fmt.status('WARN', `Review verification warnings: ${warnings.join(', ')}`));
467
+ }
468
+ if (failures.length > 0) {
469
+ error('\n' + fmt.status('INFO', 'Review verification failed. Resolve the blockers above before starting review.'));
470
+ exit(1);
471
+ return;
472
+ }
473
+ log('\n' + fmt.status('PASS', 'Review verification complete.'));
479
474
  }
480
-
481
475
  // ============================================================================
482
476
  // Command: submitForReview
483
477
  // ============================================================================
484
-
485
- /**
486
- * @param {string} slug
487
- * @param {boolean|string} skipGate
488
- * @param {{exit?: Function, resolveTaskFileFn?: Function, getTaskImplementerFn?: Function, resolveWorktreeFn?: Function, performHandoffFn?: Function, readReviewStateFn?: Function, isReviewProviderEnabledFn?: Function, isForgejoReviewEnabledFn?: Function, log?: Function}} [options]
489
- * @returns {Promise<void>}
490
- */
491
478
  async function submitForReview(slug, skipGate, options = {}) {
492
- const exit = options.exit || process.exit;
493
- const resolveTaskFileFn = options.resolveTaskFileFn || resolveTaskFile;
494
- const getTaskImplementerFn = options.getTaskImplementerFn || getTaskImplementer;
495
- const resolveWorktreeFn = options.resolveWorktreeFn || resolveWorktree;
496
- const performHandoffFn = options.performHandoffFn || performHandoff;
497
- const readReviewStateFn = options.readReviewStateFn || readReviewState;
498
- const isReviewProviderEnabledFn = options.isReviewProviderEnabledFn || options.isForgejoReviewEnabledFn || isProviderEnabled;
499
- const log = options.log || fmt.log.plain;
500
-
501
- const worktree = resolveWorktreeFn(slug) || process.cwd();
502
- const providerEnabled = isReviewProviderEnabledFn(worktree);
503
-
504
- const { identityUser: reviewStateUser } = resolveReviewIdentity(slug, worktree, {
505
- readReviewStateFn,
506
- });
507
- let reviewIdentity = reviewStateUser;
508
-
509
- // 2. Check backlog task assignee
510
- if (!reviewIdentity) {
511
- const taskResolution = resolveTaskFileFn(slug, worktree);
512
- if (taskResolution.ok) {
513
- reviewIdentity = getTaskImplementerFn(taskResolution.taskFile);
479
+ const exit = options.exit || process.exit;
480
+ const resolveTaskFileFn = options.resolveTaskFileFn || backlog_js_1.resolveTaskFile;
481
+ const getTaskImplementerFn = options.getTaskImplementerFn || backlog_js_1.getTaskImplementer;
482
+ const resolveWorktreeFn = options.resolveWorktreeFn || mission_utils_js_1.resolveWorktree;
483
+ const performHandoffFn = options.performHandoffFn || (await getHandoff()).performHandoff;
484
+ const readReviewStateFn = options.readReviewStateFn || review_state_js_1.readReviewState;
485
+ const isReviewProviderEnabledFn = options.isReviewProviderEnabledFn || options.isForgejoReviewEnabledFn || review_adapter_js_1.isProviderEnabled;
486
+ const log = options.log || fmt.log.plain;
487
+ const worktree = resolveWorktreeFn(slug) || process.cwd();
488
+ const providerEnabled = isReviewProviderEnabledFn(worktree);
489
+ const { identityUser: reviewStateUser } = (0, review_state_js_1.resolveReviewIdentity)(slug, worktree, {
490
+ readReviewStateFn,
491
+ });
492
+ let reviewIdentity = reviewStateUser;
493
+ // 2. Check backlog task assignee
494
+ if (!reviewIdentity) {
495
+ const taskResolution = resolveTaskFileFn(slug, worktree);
496
+ if (taskResolution.ok) {
497
+ reviewIdentity = getTaskImplementerFn(taskResolution.taskFile);
498
+ }
499
+ }
500
+ // 3. Mode-specific final fallback: named identity (provider-backed) vs "autonomous" (provider=none) (SC 6)
501
+ if (!reviewIdentity) {
502
+ if (providerEnabled) {
503
+ log(fmt.status('FAIL', `No review identity resolved for ${slug}. Persist review-state.json or set the task implementer before submitting for review.`));
504
+ exit(1);
505
+ return;
506
+ }
507
+ else {
508
+ reviewIdentity = 'autonomous';
509
+ log(fmt.status('WARN', `No reviewer/implementer identity resolved for ${slug}; defaulting to "autonomous"`));
510
+ }
511
+ }
512
+ const result = await performHandoffFn(slug, { skipGate, reviewIdentity, forgejoUser: reviewIdentity, worktree });
513
+ if (!result.ok) {
514
+ exit(1);
514
515
  }
515
- }
516
-
517
- // 3. Mode-specific final fallback: named identity (provider-backed) vs "autonomous" (provider=none) (SC 6)
518
- if (!reviewIdentity) {
519
- if (providerEnabled) {
520
- log(fmt.status('FAIL', `No review identity resolved for ${slug}. Persist review-state.json or set the task implementer before submitting for review.`));
521
- exit(1);
522
- return;
523
- } else {
524
- reviewIdentity = 'autonomous';
525
- log(fmt.status('WARN', `No reviewer/implementer identity resolved for ${slug}; defaulting to "autonomous"`));
526
- }
527
- }
528
-
529
- const result = await performHandoffFn(slug, { skipGate, reviewIdentity, forgejoUser: reviewIdentity, worktree });
530
- if (!result.ok) {
531
- exit(1);
532
- }
533
516
  }
534
-
535
517
  // ============================================================================
536
518
  // Command: readComments
537
519
  // ============================================================================
538
-
539
- /**
540
- * @param {string} slug
541
- * @param {{log?: Function, error?: Function, exit?: Function, readTokenFn?: Function, readReviewStateFn?: Function, resolveReviewUserFn?: Function, resolveForgejoUserFn?: Function, getCommentsFn?: Function, readAllEventsFn?: Function, isReviewProviderEnabledFn?: Function, isForgejoReviewEnabledFn?: Function, worktree?: string}} [options]
542
- * @returns {Promise<void>}
543
- */
544
520
  async function readComments(slug, options = {}) {
545
- const log = options.log || fmt.log.plain;
546
- const error = options.error || fmt.log.plainError;
547
- const exit = options.exit || process.exit;
548
- const readTokenFn = options.readTokenFn || readToken;
549
- const readReviewStateFn = options.readReviewStateFn || readReviewState;
550
- const resolveReviewUserFn = options.resolveReviewUserFn || options.resolveForgejoUserFn || resolveReviewUser;
551
- const getCommentsFn = options.getCommentsFn || getComments;
552
- const readAllEventsFn = options.readAllEventsFn || readAllEvents;
553
- const isReviewProviderEnabledFn = options.isReviewProviderEnabledFn || options.isForgejoReviewEnabledFn || isProviderEnabled;
554
- const worktree = options.worktree || resolveWorktree(slug) || process.cwd();
555
- const branch = missionBranchName(slug, worktree);
556
-
557
- // Provider-disabled fallback: read persisted local review events instead of polling the PR.
558
- // Avoids resolving a provider token (which would FAIL/exit) when the provider is off.
559
- if (!isReviewProviderEnabledFn(worktree)) {
560
- log(fmt.status('INFO', `Review provider disabled; reading local review events for ${slug}...`));
561
- const events = readAllEventsFn(slug, { rootDir: worktree, log, error });
562
- if (!events || events.length === 0) {
563
- log('no comments');
564
- return;
565
- }
566
- for (const e of events) {
567
- let label = e.event_type || 'event';
568
- if (e.round !== null) {label += ` round ${e.round}`;}
569
- log(`--- ${label} | ${e.actor || 'unknown'} (${e.timestamp || e.fileCreated || ''}) ---`);
570
- log(e.content || '(no body)');
571
- log('');
572
- }
573
- return;
574
- }
575
-
576
- let reviewIdentity = resolveReviewIdentity(slug, worktree, {
577
- readReviewStateFn,
578
- }).identityUser;
579
-
580
- if (!reviewIdentity) {
581
- error(fmt.status('FAIL', `Cannot determine review identity for ${slug}. Persist review-state.json or set FORGEJO_USER.`));
582
- exit(1);
583
- return;
584
- }
585
- reviewIdentity = resolveReviewUserFn(reviewIdentity);
586
-
587
- const token = readTokenFn(reviewIdentity, { rootDir: worktree });
588
- if (!token) {
589
- error(fmt.status('FAIL', `No Forgejo token found for user "${reviewIdentity}".`));
590
- exit(1);
591
- return;
592
- }
593
-
594
- log(fmt.status('INFO', `Reading PR comments on ${branch} as ${reviewIdentity}...`));
595
- const comments = await getCommentsFn(branch, token);
596
-
597
- if (comments === null) {
598
- error(fmt.status('FAIL', `Could not fetch PR comments for ${branch}. The review provider may be unreachable or the PR is missing.`));
599
- exit(1);
600
- return;
601
- }
602
-
603
- if (comments.length === 0) {
604
- log('no comments');
605
- return;
606
- }
607
-
608
- for (const c of comments) {
609
- let label = c.kind;
610
- if (c.location) {label += ` ${c.location}`;}
611
- log(`--- ${label} | ${c.user} (${c.created}) ---`);
612
- log(c.body || '(no body)');
613
- log('');
614
- }
521
+ const log = options.log || fmt.log.plain;
522
+ const error = options.error || fmt.log.plainError;
523
+ const exit = options.exit || process.exit;
524
+ const readTokenFn = options.readTokenFn || review_adapter_js_1.readToken;
525
+ const readReviewStateFn = options.readReviewStateFn || review_state_js_1.readReviewState;
526
+ const resolveReviewUserFn = options.resolveReviewUserFn || options.resolveForgejoUserFn || review_adapter_js_1.resolveReviewUser;
527
+ const getCommentsFn = options.getCommentsFn || review_adapter_js_1.getComments;
528
+ const readAllEventsFn = options.readAllEventsFn || review_events_js_1.readAllEvents;
529
+ const isReviewProviderEnabledFn = options.isReviewProviderEnabledFn || options.isForgejoReviewEnabledFn || review_adapter_js_1.isProviderEnabled;
530
+ const worktree = options.worktree || (0, mission_utils_js_1.resolveWorktree)(slug) || process.cwd();
531
+ const branch = (0, mission_utils_js_1.missionBranchName)(slug, worktree);
532
+ // Provider-disabled fallback: read persisted local review events instead of polling the PR.
533
+ // Avoids resolving a provider token (which would FAIL/exit) when the provider is off.
534
+ if (!isReviewProviderEnabledFn(worktree)) {
535
+ log(fmt.status('INFO', `Review provider disabled; reading local review events for ${slug}...`));
536
+ const events = readAllEventsFn(slug, { rootDir: worktree, error });
537
+ if (!events || events.length === 0) {
538
+ log('no comments');
539
+ return;
540
+ }
541
+ for (const e of events) {
542
+ let label = e.event_type || 'event';
543
+ if (e.round !== null) {
544
+ label += ` round ${e.round}`;
545
+ }
546
+ log(`--- ${label} | ${e.actor || 'unknown'} (${e.timestamp || e.fileCreated || ''}) ---`);
547
+ log(e.content || '(no body)');
548
+ log('');
549
+ }
550
+ return;
551
+ }
552
+ let reviewIdentity = (0, review_state_js_1.resolveReviewIdentity)(slug, worktree, {
553
+ readReviewStateFn,
554
+ }).identityUser;
555
+ if (!reviewIdentity) {
556
+ error(fmt.status('FAIL', `Cannot determine review identity for ${slug}. Persist review-state.json or set FORGEJO_USER.`));
557
+ exit(1);
558
+ return;
559
+ }
560
+ reviewIdentity = resolveReviewUserFn(reviewIdentity);
561
+ const token = readTokenFn(reviewIdentity, { rootDir: worktree });
562
+ if (!token) {
563
+ error(fmt.status('FAIL', `No Forgejo token found for user "${reviewIdentity}".`));
564
+ exit(1);
565
+ return;
566
+ }
567
+ log(fmt.status('INFO', `Reading PR comments on ${branch} as ${reviewIdentity}...`));
568
+ const comments = await getCommentsFn(branch, token);
569
+ if (comments === null) {
570
+ error(fmt.status('FAIL', `Could not fetch PR comments for ${branch}. The review provider may be unreachable or the PR is missing.`));
571
+ exit(1);
572
+ return;
573
+ }
574
+ if (comments.length === 0) {
575
+ log('no comments');
576
+ return;
577
+ }
578
+ for (const c of comments) {
579
+ let label = c.kind;
580
+ if (c.location) {
581
+ label += ` ${c.location}`;
582
+ }
583
+ log(`--- ${label} | ${c.user} (${c.created}) ---`);
584
+ log(c.body || '(no body)');
585
+ log('');
586
+ }
615
587
  }
616
-
617
588
  // ============================================================================
618
589
  // Command: pushRound
619
590
  // ============================================================================
620
-
621
- /**
622
- * @param {string} slug
623
- * @param {{log?: Function, error?: Function, exit?: Function, resolveWorktreeFn?: Function, resolveTaskFileFn?: Function, getTaskImplementerFn?: Function, transitionTaskFn?: Function, resolveReviewUserFn?: Function, resolveForgejoUserFn?: Function, isReviewProviderEnabledFn?: Function, isForgejoReviewEnabledFn?: Function, readTokenFn?: Function, readReviewStateFn?: Function, createPrFn?: Function, bootstrapReviewSurfaceFn?: Function, resolveReviewAdapterFn?: Function, cwdFn?: Function, force?: boolean}} [options]
624
- * @returns {Promise<void>}
625
- */
626
591
  async function pushRound(slug, options = {}) {
627
- const log = options.log || fmt.log.plain;
628
- const error = options.error || fmt.log.plainError;
629
- const exit = options.exit || process.exit;
630
- const resolveWorktreeFn = options.resolveWorktreeFn || resolveWorktree;
631
- const resolveTaskFileFn = options.resolveTaskFileFn || resolveTaskFile;
632
- const getTaskImplementerFn = options.getTaskImplementerFn || getTaskImplementer;
633
- const transitionTaskFn = options.transitionTaskFn || transitionTask;
634
- const resolveReviewUserFn = options.resolveReviewUserFn || options.resolveForgejoUserFn || resolveReviewUser;
635
- const isReviewProviderEnabledFn = options.isReviewProviderEnabledFn || options.isForgejoReviewEnabledFn || isProviderEnabled;
636
- const readTokenFn = options.readTokenFn || readToken;
637
- const readReviewStateFn = options.readReviewStateFn || readReviewState;
638
- const createPrFn = options.createPrFn || createPr;
639
- const bootstrapReviewSurfaceFn = options.bootstrapReviewSurfaceFn || bootstrapReviewSurface;
640
- const resolveReviewAdapterFn = options.resolveReviewAdapterFn || resolveReviewAdapter;
641
- const cwdFn = options.cwdFn || (() => process.cwd());
642
- const force = options.force || false;
643
- const worktree = resolveWorktreeFn(slug);
644
- const rootDir = worktree || cwdFn();
645
- const branch = missionBranchName(slug, rootDir);
646
- const providerEnabled = isReviewProviderEnabledFn(rootDir);
647
-
648
- const { identityUser: reviewStateUser } = resolveReviewIdentity(slug, rootDir, {
649
- readReviewStateFn,
650
- });
651
- let reviewIdentity = reviewStateUser;
652
-
653
- if (!reviewIdentity) {
654
- // 1. Check backlog task assignee
655
- const taskResolution = resolveTaskFileFn(slug, rootDir);
656
- if (taskResolution.ok) {
657
- reviewIdentity = getTaskImplementerFn(taskResolution.taskFile);
658
- }
659
- }
660
-
661
- // 2. Mode-specific final fallback: named identity (provider-backed) vs "autonomous" (provider=none) (SC 6)
662
- if (!reviewIdentity) {
663
- if (providerEnabled) {
664
- error(fmt.status('FAIL', `No review identity resolved for --push on ${slug}. Persist review-state.json or set the task implementer.`));
665
- exit(1);
666
- return;
667
- } else {
668
- reviewIdentity = 'autonomous';
669
- log(fmt.status('WARN', `No reviewer/implementer identity resolved for --push; defaulting to "autonomous"`));
670
- }
671
- }
672
-
673
- if (!reviewIdentity || (providerEnabled && reviewIdentity === 'autonomous')) {
674
- error(fmt.status('FAIL', 'No review identity resolved for push.'));
675
- exit(1);
676
- return;
677
- }
678
-
679
- reviewIdentity = resolveReviewUserFn(reviewIdentity);
680
- const token = readTokenFn(reviewIdentity, { rootDir: worktree });
681
- if (!token) {
682
- error(fmt.status('FAIL', `No Forgejo token found for user "${reviewIdentity}".`));
683
- exit(1);
684
- return;
685
- }
686
-
687
- // Transition to review before pushing so the state change is included in the PR update
688
- transitionTaskFn(slug, 'review', { rootDir, log });
689
-
690
- log(fmt.status('INFO', `Pushing ${branch} to the review provider as ${reviewIdentity}...${force ? ' (force-with-lease)' : ''}`));
691
- if (worktree) {
692
- log(fmt.status('INFO', `Found dedicated worktree: ${worktree}`));
693
- }
694
-
695
- let result = createPrFn(branch, reviewIdentity, token, { rootDir, forceWithLease: true });
696
- if (!result.ok && /Repository not found/i.test(result.error || '')) {
697
- const reviewAdapter = resolveReviewAdapterFn(rootDir);
698
- const ownerLogin = (reviewAdapter.repo && reviewAdapter.repo.split('/')[0]) || 'magnus';
699
- const bootstrap = await bootstrapReviewSurfaceFn(rootDir, {
700
- baseUrl: reviewAdapter.baseUrl,
701
- repo: reviewAdapter.repo,
702
- ownerLogin,
703
- ownerPassword: '',
704
- agentPasswords: [],
705
- }, {
706
- interactive: false,
707
- log,
708
- error,
592
+ const log = options.log || fmt.log.plain;
593
+ const error = options.error || fmt.log.plainError;
594
+ const exit = options.exit || process.exit;
595
+ const resolveWorktreeFn = options.resolveWorktreeFn || mission_utils_js_1.resolveWorktree;
596
+ const resolveTaskFileFn = options.resolveTaskFileFn || backlog_js_1.resolveTaskFile;
597
+ const getTaskImplementerFn = options.getTaskImplementerFn || backlog_js_1.getTaskImplementer;
598
+ const transitionTaskFn = options.transitionTaskFn || backlog_js_1.transitionTask;
599
+ const resolveReviewUserFn = options.resolveReviewUserFn || options.resolveForgejoUserFn || review_adapter_js_1.resolveReviewUser;
600
+ const isReviewProviderEnabledFn = options.isReviewProviderEnabledFn || options.isForgejoReviewEnabledFn || review_adapter_js_1.isProviderEnabled;
601
+ const readTokenFn = options.readTokenFn || review_adapter_js_1.readToken;
602
+ const readReviewStateFn = options.readReviewStateFn || review_state_js_1.readReviewState;
603
+ const createPrFn = options.createPrFn || review_adapter_js_1.createPr;
604
+ const bootstrapReviewSurfaceFn = options.bootstrapReviewSurfaceFn || setup_review_js_1.bootstrapReviewSurface;
605
+ const resolveReviewAdapterFn = options.resolveReviewAdapterFn || product_config_js_1.resolveReviewAdapter;
606
+ const cwdFn = options.cwdFn || (() => process.cwd());
607
+ const force = options.force || false;
608
+ const worktree = resolveWorktreeFn(slug);
609
+ const rootDir = worktree || cwdFn();
610
+ const branch = (0, mission_utils_js_1.missionBranchName)(slug, rootDir);
611
+ const providerEnabled = isReviewProviderEnabledFn(rootDir);
612
+ const { identityUser: reviewStateUser } = (0, review_state_js_1.resolveReviewIdentity)(slug, rootDir, {
613
+ readReviewStateFn,
709
614
  });
710
- if (bootstrap.ok) {
711
- result = createPrFn(branch, reviewIdentity, token, { rootDir, forceWithLease: true });
712
- }
713
- }
714
-
715
- if (!result.ok) {
716
- error(fmt.status('FAIL', `Push to review provider failed: ${result.error}`));
717
- exit(1);
718
- return;
719
- }
720
-
721
- log(fmt.status('PASS', `Branch pushed and PR updated for ${branch}.`));
615
+ let reviewIdentity = reviewStateUser;
616
+ if (!reviewIdentity) {
617
+ // 1. Check backlog task assignee
618
+ const taskResolution = resolveTaskFileFn(slug, rootDir);
619
+ if (taskResolution.ok) {
620
+ reviewIdentity = getTaskImplementerFn(taskResolution.taskFile);
621
+ }
622
+ }
623
+ // 2. Mode-specific final fallback: named identity (provider-backed) vs "autonomous" (provider=none) (SC 6)
624
+ if (!reviewIdentity) {
625
+ if (providerEnabled) {
626
+ error(fmt.status('FAIL', `No review identity resolved for --push on ${slug}. Persist review-state.json or set the task implementer.`));
627
+ exit(1);
628
+ return;
629
+ }
630
+ else {
631
+ reviewIdentity = 'autonomous';
632
+ log(fmt.status('WARN', `No reviewer/implementer identity resolved for --push; defaulting to "autonomous"`));
633
+ }
634
+ }
635
+ if (!reviewIdentity || (providerEnabled && reviewIdentity === 'autonomous')) {
636
+ error(fmt.status('FAIL', 'No review identity resolved for push.'));
637
+ exit(1);
638
+ return;
639
+ }
640
+ reviewIdentity = resolveReviewUserFn(reviewIdentity);
641
+ const token = readTokenFn(reviewIdentity, { rootDir: worktree || undefined });
642
+ if (!token) {
643
+ error(fmt.status('FAIL', `No Forgejo token found for user "${reviewIdentity}".`));
644
+ exit(1);
645
+ return;
646
+ }
647
+ // Transition to review before pushing so the state change is included in the PR update
648
+ transitionTaskFn(slug, 'review', { rootDir, log });
649
+ log(fmt.status('INFO', `Pushing ${branch} to the review provider as ${reviewIdentity}...${force ? ' (force-with-lease)' : ''}`));
650
+ if (worktree) {
651
+ log(fmt.status('INFO', `Found dedicated worktree: ${worktree}`));
652
+ }
653
+ let result = createPrFn(branch, reviewIdentity, token, { rootDir, forceWithLease: true });
654
+ if (!result.ok && /Repository not found/i.test(result.error || '')) {
655
+ const reviewAdapter = resolveReviewAdapterFn(rootDir);
656
+ const ownerLogin = (reviewAdapter.repo && reviewAdapter.repo.split('/')[0]) || 'magnus';
657
+ const bootstrap = await bootstrapReviewSurfaceFn(rootDir, {
658
+ baseUrl: reviewAdapter.baseUrl,
659
+ repo: reviewAdapter.repo,
660
+ ownerLogin,
661
+ ownerPassword: '',
662
+ agentPasswords: [],
663
+ }, {
664
+ interactive: false,
665
+ log,
666
+ error,
667
+ });
668
+ if (bootstrap.ok) {
669
+ result = createPrFn(branch, reviewIdentity, token, { rootDir, forceWithLease: true });
670
+ }
671
+ }
672
+ if (!result.ok) {
673
+ error(fmt.status('FAIL', `Push to review provider failed: ${result.error}`));
674
+ exit(1);
675
+ return;
676
+ }
677
+ log(fmt.status('PASS', `Branch pushed and PR updated for ${branch}.`));
722
678
  }
723
-
724
679
  // ============================================================================
725
680
  // Command: showReviewStatus
726
681
  // ============================================================================
727
-
728
- /**
729
- * @param {string} slug
730
- * @param {{log?: Function, readReviewStateFn?: Function, resolveWorktreeFn?: Function}} [options]
731
- * @returns {void}
732
- */
733
682
  function showReviewStatus(slug, options = {}) {
734
- const log = options.log || fmt.log.plain;
735
- const readReviewStateFn = options.readReviewStateFn || readReviewState;
736
- const resolveWorktreeFn = options.resolveWorktreeFn || resolveWorktree;
737
-
738
- const worktree = resolveWorktreeFn(slug) || process.cwd();
739
- const state = readReviewStateFn(slug, worktree);
740
-
741
- log(fmt.status('INFO', `Review status for mission: ${fmt.slug(slug)}`));
742
-
743
- if (!state) {
744
- log(fmt.status('INFO', 'No persisted review state found.'));
745
- return;
746
- }
747
-
748
- log(` Round: ${state.round}`);
749
- log(` Phase: ${state.phase}`);
750
- log(` Reviewer: ${fmt.agent(state.reviewer)}`);
751
- log(` Implementer: ${fmt.agent(state.implementer)}`);
752
- log(` Started at: ${state.startedAt}`);
753
- if (state.disposition) {
754
- log(` Disposition: ${state.disposition}`);
755
- }
756
- if (state.reviewerRetryCount > 0) {
757
- log(` Reviewer retries: ${state.reviewerRetryCount}`);
758
- }
759
- if (state.implementerRetryCount > 0) {
760
- log(` Implementer retries: ${state.implementerRetryCount}`);
761
- }
683
+ const log = options.log || fmt.log.plain;
684
+ const readReviewStateFn = options.readReviewStateFn || review_state_js_1.readReviewState;
685
+ const resolveWorktreeFn = options.resolveWorktreeFn || mission_utils_js_1.resolveWorktree;
686
+ const worktree = resolveWorktreeFn(slug) || process.cwd();
687
+ const state = readReviewStateFn(slug, worktree);
688
+ log(fmt.status('INFO', `Review status for mission: ${fmt.slug(slug)}`));
689
+ if (!state) {
690
+ log(fmt.status('INFO', 'No persisted review state found.'));
691
+ return;
692
+ }
693
+ log(` Round: ${state.round}`);
694
+ log(` Phase: ${state.phase}`);
695
+ log(` Reviewer: ${fmt.agent(state.reviewer ?? '')}`);
696
+ log(` Implementer: ${fmt.agent(state.implementer ?? '')}`);
697
+ log(` Started at: ${state.startedAt}`);
698
+ if (state.disposition) {
699
+ log(` Disposition: ${state.disposition}`);
700
+ }
701
+ if (state.reviewerRetryCount > 0) {
702
+ log(` Reviewer retries: ${state.reviewerRetryCount}`);
703
+ }
704
+ if (state.implementerRetryCount > 0) {
705
+ log(` Implementer retries: ${state.implementerRetryCount}`);
706
+ }
762
707
  }
763
-
764
708
  // ============================================================================
765
709
  // Command: commentRound
766
710
  // ============================================================================
767
-
768
- /**
769
- * @param {string} slug
770
- * @param {string} message
771
- * @param {{log?: Function, error?: Function, exit?: Function, readReviewStateFn?: Function, writeReviewStateFn?: Function, resolveReviewUserFn?: Function, resolveForgejoUserFn?: Function, rootDir?: string, readTokenFn?: Function, postCommentFn?: Function, buildMetadataFooterFn?: Function}} [options]
772
- * @returns {void}
773
- */
774
711
  function commentRound(slug, message, options = {}) {
775
- const log = options.log || fmt.log.plain;
776
- const error = options.error || fmt.log.plainError;
777
- const exit = options.exit || process.exit;
778
- const readReviewStateFn = options.readReviewStateFn || readReviewState;
779
- const writeReviewStateFn = options.writeReviewStateFn || writeReviewState;
780
- const resolveReviewUserFn = options.resolveReviewUserFn || options.resolveForgejoUserFn || resolveReviewUser;
781
- const rootDir = options.rootDir || resolveWorktree(slug) || process.cwd();
782
-
783
- let reviewIdentity = resolveReviewIdentity(slug, rootDir, {
784
- readReviewStateFn,
785
- }).identityUser;
786
-
787
- if (!reviewIdentity) {
788
- error(fmt.status('FAIL', `Cannot determine review identity for ${slug}. Persist review-state.json or set FORGEJO_USER.`));
789
- exit(1);
790
- return;
791
- }
792
- reviewIdentity = resolveReviewUserFn(reviewIdentity);
793
- const result = postWorkflowComment(slug, message, {
794
- rootDir,
795
- reviewIdentity: reviewIdentity || undefined,
796
- readTokenFn: options.readTokenFn,
797
- postCommentFn: options.postCommentFn,
798
- buildMetadataFooterFn: options.buildMetadataFooterFn,
799
- log,
800
- error
801
- });
802
-
803
- if (!result.ok) {
804
- exit(1);
805
- return;
806
- }
807
-
808
- const currentState = readReviewStateFn(slug, rootDir);
809
- if (currentState) {
810
- writeReviewStateFn(slug, currentState, rootDir);
811
- }
712
+ const log = options.log || fmt.log.plain;
713
+ const error = options.error || fmt.log.plainError;
714
+ const exit = options.exit || process.exit;
715
+ const readReviewStateFn = options.readReviewStateFn || review_state_js_1.readReviewState;
716
+ const writeReviewStateFn = options.writeReviewStateFn || review_state_js_1.writeReviewState;
717
+ const resolveReviewUserFn = options.resolveReviewUserFn || options.resolveForgejoUserFn || review_adapter_js_1.resolveReviewUser;
718
+ const rootDir = options.rootDir || (0, mission_utils_js_1.resolveWorktree)(slug) || process.cwd();
719
+ let reviewIdentity = (0, review_state_js_1.resolveReviewIdentity)(slug, rootDir, {
720
+ readReviewStateFn,
721
+ }).identityUser;
722
+ if (!reviewIdentity) {
723
+ error(fmt.status('FAIL', `Cannot determine review identity for ${slug}. Persist review-state.json or set FORGEJO_USER.`));
724
+ exit(1);
725
+ return;
726
+ }
727
+ reviewIdentity = resolveReviewUserFn(reviewIdentity);
728
+ const result = (0, review_artifacts_js_1.postWorkflowComment)(slug, message, {
729
+ rootDir,
730
+ reviewIdentity: reviewIdentity || undefined,
731
+ readTokenFn: options.readTokenFn,
732
+ postCommentFn: options.postCommentFn,
733
+ buildMetadataFooterFn: options.buildMetadataFooterFn,
734
+ log,
735
+ error
736
+ });
737
+ if (!result.ok) {
738
+ exit(1);
739
+ return;
740
+ }
741
+ const currentState = readReviewStateFn(slug, rootDir);
742
+ if (currentState) {
743
+ writeReviewStateFn(slug, currentState, rootDir);
744
+ }
812
745
  }
813
-
814
746
  // ============================================================================
815
747
  // Command: consumeArtifacts
816
748
  // ============================================================================
817
-
818
- /**
819
- * @param {string} slug
820
- * @param {{log?: Function, error?: Function, resolveWorktreeFn?: Function, transitionTaskFn?: Function, consumeReviewerArtifactsFn?: Function, resolveTaskFileFn?: Function, getTaskAssigneeFn?: Function, getTaskStatusFn?: Function, resolveArtifactDirFn?: Function, readReviewStateFn?: Function, createEventFn?: Function, readArtifactFn?: Function, deleteArtifactFn?: Function}} [options]
821
- * @returns {Promise<{ok: boolean, consumed: boolean, reviewState?: string|null}>}
822
- */
823
749
  async function consumeArtifacts(slug, options = {}) {
824
- const log = options.log || fmt.log.plain;
825
- const error = options.error || fmt.log.plainError;
826
- const resolveWorktreeFn = options.resolveWorktreeFn || resolveWorktree;
827
- const transitionTaskFn = options.transitionTaskFn || transitionTask;
828
- const consumeReviewerArtifactsFn = options.consumeReviewerArtifactsFn || consumeReviewerArtifacts;
829
- const resolveTaskFileFn = options.resolveTaskFileFn || resolveTaskFile;
830
- const getTaskAssigneeFn = options.getTaskAssigneeFn || getTaskAssignee;
831
- const getTaskStatusFn = options.getTaskStatusFn || getTaskStatus;
832
- const resolveArtifactDirFn = options.resolveArtifactDirFn || require('./review-artifacts').resolveArtifactDir;
833
-
834
- const worktree = resolveWorktreeFn(slug) || process.cwd();
835
- const rootDir = worktree;
836
- const taskResolution = resolveTaskFileFn(slug, rootDir);
837
-
838
- // Resolve artifact directory
839
- const artifactDir = resolveArtifactDirFn(rootDir);
840
- log(fmt.status('INFO', `Consuming reviewer artifacts for ${slug} from ${artifactDir}`));
841
-
842
- // Determine reviewer identity from review-state first, then task assignee.
843
- const { identityUser: stateReviewer } = resolveReviewIdentity(slug, worktree, {
844
- readReviewStateFn: options.readReviewStateFn || readReviewState,
845
- });
846
- let reviewer = stateReviewer;
847
- if (!reviewer && taskResolution.ok) {
848
- reviewer = getTaskAssigneeFn(taskResolution.taskFile);
849
- }
850
- if (!reviewer) {
851
- reviewer = 'autonomous';
852
- log(fmt.status('WARN', `No reviewer identity resolved; defaulting to "${reviewer}"`));
853
- }
854
-
855
- // Consume artifacts - this will create reviewer_findings and reviewer_outcome events
856
- const result = await consumeReviewerArtifactsFn(slug, reviewer, {
857
- rootDir,
858
- worktree,
859
- tmpDir: artifactDir,
860
- log,
861
- error,
862
- providerEnabled: false,
863
- createEventFn: options.createEventFn,
864
- readArtifactFn: options.readArtifactFn,
865
- deleteArtifactFn: options.deleteArtifactFn,
866
- });
867
-
868
- if (!result.consumed) {
869
- log(fmt.status('WARN', `No reviewer artifact files found at ${artifactDir} for ${slug}.`));
870
- return { ok: false, consumed: false };
871
- }
872
-
873
- if (!result.ok) {
874
- error(fmt.status('FAIL', `Failed to consume reviewer artifacts for ${slug}: ${result.ok === false ? 'missing required fields (findings, outcome, verdict)' : 'unknown failure'}`));
875
- return { ok: false, consumed: true };
876
- }
877
-
878
- // Persist the artifact location to review-state.json if it doesn't exist yet
879
- const persisted = readReviewState(slug, worktree);
880
- if (!persisted) {
881
- const { ReviewState } = require('./review-state');
882
- writeReviewState(slug, new ReviewState(slug, {
883
- reviewer,
884
- round: 1,
885
- phase: 'reviewing',
886
- }), worktree);
887
- log(fmt.status('INFO', 'Created review-state.json for artifact consumption.'));
888
- }
889
-
890
- // Transition backlog task to review status
891
- if (taskResolution.ok) {
892
- const currentStatus = getTaskStatusFn ? getTaskStatusFn(taskResolution.taskFile) : null;
893
- if (!currentStatus || currentStatus !== 'review') {
894
- transitionTaskFn(slug, 'review', { rootDir, log });
895
- } else {
896
- log(fmt.status('INFO', `Backlog task for ${slug} already at review status.`));
897
- }
898
- }
899
-
900
- const cleanup = await commitPersistedReviewOutputs(slug, {
901
- worktree,
902
- taskFile: taskResolution.ok ? taskResolution.taskFile : null,
903
- log,
904
- error
905
- });
906
- if (!cleanup.ok) {
907
- error(fmt.status('FAIL', `Consumed reviewer artifacts for ${slug}, but could not commit the persisted mission artifacts.`));
908
- return { ok: false, consumed: true };
909
- }
910
-
911
- log(fmt.status('PASS', `Reviewer artifacts consumed for ${slug}. Backlog task set to review.`));
912
- return { ok: true, consumed: true, reviewState: result.reviewState };
750
+ const log = options.log || fmt.log.plain;
751
+ const error = options.error || fmt.log.plainError;
752
+ const resolveWorktreeFn = options.resolveWorktreeFn || mission_utils_js_1.resolveWorktree;
753
+ const transitionTaskFn = options.transitionTaskFn || backlog_js_1.transitionTask;
754
+ const consumeReviewerArtifactsFn = options.consumeReviewerArtifactsFn || review_artifacts_js_1.consumeReviewerArtifacts;
755
+ const resolveTaskFileFn = options.resolveTaskFileFn || backlog_js_1.resolveTaskFile;
756
+ const getTaskAssigneeFn = options.getTaskAssigneeFn || backlog_js_1.getTaskAssignee;
757
+ const getTaskStatusFn = options.getTaskStatusFn || backlog_js_1.getTaskStatus;
758
+ const resolveArtifactDirFn = options.resolveArtifactDirFn || review_artifacts_js_1.resolveArtifactDir;
759
+ const worktree = resolveWorktreeFn(slug) || process.cwd();
760
+ const rootDir = worktree;
761
+ const taskResolution = resolveTaskFileFn(slug, rootDir);
762
+ // Resolve artifact directory
763
+ const artifactDir = resolveArtifactDirFn(rootDir);
764
+ log(fmt.status('INFO', `Consuming reviewer artifacts for ${slug} from ${artifactDir}`));
765
+ // Determine reviewer identity from review-state first, then task assignee.
766
+ const { identityUser: stateReviewer } = (0, review_state_js_1.resolveReviewIdentity)(slug, worktree, {
767
+ readReviewStateFn: options.readReviewStateFn || review_state_js_1.readReviewState,
768
+ });
769
+ let reviewer = stateReviewer;
770
+ if (!reviewer && taskResolution.ok) {
771
+ reviewer = getTaskAssigneeFn(taskResolution.taskFile);
772
+ }
773
+ if (!reviewer) {
774
+ reviewer = 'autonomous';
775
+ log(fmt.status('WARN', `No reviewer identity resolved; defaulting to "${reviewer}"`));
776
+ }
777
+ // Consume artifacts - this will create reviewer_findings and reviewer_outcome events
778
+ const result = await consumeReviewerArtifactsFn(slug, reviewer, {
779
+ worktree,
780
+ tmpDir: artifactDir,
781
+ log,
782
+ error,
783
+ providerEnabled: false,
784
+ createEventFn: options.createEventFn,
785
+ readArtifactFn: options.readArtifactFn,
786
+ deleteArtifactFn: options.deleteArtifactFn,
787
+ });
788
+ if (!result.consumed) {
789
+ log(fmt.status('WARN', `No reviewer artifact files found at ${artifactDir} for ${slug}.`));
790
+ return { ok: false, consumed: false };
791
+ }
792
+ if (!result.ok) {
793
+ error(fmt.status('FAIL', `Failed to consume reviewer artifacts for ${slug}: ${result.ok === false ? 'missing required fields (findings, outcome, verdict)' : 'unknown failure'}`));
794
+ return { ok: false, consumed: true };
795
+ }
796
+ // Persist the artifact location to review-state.json if it doesn't exist yet
797
+ const persisted = (0, review_state_js_1.readReviewState)(slug, worktree);
798
+ if (!persisted) {
799
+ (0, review_state_js_1.writeReviewState)(slug, new review_state_js_1.ReviewState(slug, {
800
+ reviewer,
801
+ round: 1,
802
+ phase: 'reviewing',
803
+ }), worktree);
804
+ log(fmt.status('INFO', 'Created review-state.json for artifact consumption.'));
805
+ }
806
+ // Transition backlog task to review status
807
+ if (taskResolution.ok) {
808
+ const currentStatus = getTaskStatusFn ? getTaskStatusFn(taskResolution.taskFile) : null;
809
+ if (!currentStatus || currentStatus !== 'review') {
810
+ transitionTaskFn(slug, 'review', { rootDir, log });
811
+ }
812
+ else {
813
+ log(fmt.status('INFO', `Backlog task for ${slug} already at review status.`));
814
+ }
815
+ }
816
+ const cleanup = await commitPersistedReviewOutputs(slug, {
817
+ worktree,
818
+ taskFile: taskResolution.ok ? taskResolution.taskFile : null,
819
+ log,
820
+ error
821
+ });
822
+ if (!cleanup.ok) {
823
+ error(fmt.status('FAIL', `Consumed reviewer artifacts for ${slug}, but could not commit the persisted mission artifacts.`));
824
+ return { ok: false, consumed: true };
825
+ }
826
+ log(fmt.status('PASS', `Reviewer artifacts consumed for ${slug}. Backlog task set to review.`));
827
+ return { ok: true, consumed: true, reviewState: result.reviewState };
913
828
  }
914
-
915
829
  // ============================================================================
916
830
  // Command: submitReviewRound
917
831
  // ============================================================================
918
-
919
- /**
920
- * @param {string} slug
921
- * @param {string} outcome
922
- * @param {string} message
923
- * @param {{log?: Function, error?: Function, exit?: Function, transitionTaskFn?: Function, readReviewStateFn?: Function, writeReviewStateFn?: Function, isReviewProviderEnabledFn?: Function, isForgejoReviewEnabledFn?: Function, resolveReviewUserFn?: Function, resolveForgejoUserFn?: Function, resolveTaskFileFn?: Function, getTaskStatusFn?: Function, worktree?: string, readTokenFn?: Function, postReviewFn?: Function, getPrAuthorFn?: Function, createEventFn?: Function, buildMetadataFooterFn?: Function}} [options]
924
- * @returns {void}
925
- */
926
832
  function submitReviewRound(slug, outcome, message, options = {}) {
927
- const log = options.log || fmt.log.plain;
928
- const error = options.error || fmt.log.plainError;
929
- const exit = options.exit || process.exit;
930
- const transitionTaskFn = options.transitionTaskFn || transitionTask;
931
- const readReviewStateFn = options.readReviewStateFn || readReviewState;
932
- const writeReviewStateFn = options.writeReviewStateFn || writeReviewState;
933
- const isReviewProviderEnabledFn = options.isReviewProviderEnabledFn || options.isForgejoReviewEnabledFn || isProviderEnabled;
934
- const resolveReviewUserFn = options.resolveReviewUserFn || options.resolveForgejoUserFn || resolveReviewUser;
935
- const resolveTaskFileFn = options.resolveTaskFileFn || resolveTaskFile;
936
- const getTaskStatusFn = options.getTaskStatusFn || getTaskStatus;
937
- const VALID_OUTCOMES = ['approve', 'request-changes', 'comment'];
938
- if (!VALID_OUTCOMES.includes(outcome)) {
939
- error(fmt.status('FAIL', `Unknown review outcome "${outcome}". Valid: ${VALID_OUTCOMES.join(', ')}.`));
940
- exit(1);
941
- return;
942
- }
943
-
944
- const worktree = options.worktree || resolveWorktree(slug) || process.cwd();
945
- const providerEnabled = isReviewProviderEnabledFn(worktree);
946
-
947
- // For provider=none (standalone), skip provider posting and only update review-state
948
- if (!providerEnabled) {
949
- log(fmt.status('INFO', `Review provider is none — skipping provider posting, updating review-state only for ${slug}.`));
950
- const { ReviewState } = require('./review-state');
951
- const transitionTaskFn = options.transitionTaskFn || transitionTask;
833
+ const log = options.log || fmt.log.plain;
834
+ const error = options.error || fmt.log.plainError;
835
+ const exit = options.exit || process.exit;
836
+ const transitionTaskFn = options.transitionTaskFn || backlog_js_1.transitionTask;
837
+ const readReviewStateFn = options.readReviewStateFn || review_state_js_1.readReviewState;
838
+ const writeReviewStateFn = options.writeReviewStateFn || review_state_js_1.writeReviewState;
839
+ const isReviewProviderEnabledFn = options.isReviewProviderEnabledFn || options.isForgejoReviewEnabledFn || review_adapter_js_1.isProviderEnabled;
840
+ const resolveReviewUserFn = options.resolveReviewUserFn || options.resolveForgejoUserFn || review_adapter_js_1.resolveReviewUser;
841
+ const resolveTaskFileFn = options.resolveTaskFileFn || backlog_js_1.resolveTaskFile;
842
+ const getTaskStatusFn = options.getTaskStatusFn || backlog_js_1.getTaskStatus;
843
+ const VALID_OUTCOMES = ['approve', 'request-changes', 'comment'];
844
+ if (!VALID_OUTCOMES.includes(outcome)) {
845
+ error(fmt.status('FAIL', `Unknown review outcome "${outcome}". Valid: ${VALID_OUTCOMES.join(', ')}.`));
846
+ exit(1);
847
+ return;
848
+ }
849
+ const worktree = options.worktree || (0, mission_utils_js_1.resolveWorktree)(slug) || process.cwd();
850
+ const providerEnabled = isReviewProviderEnabledFn(worktree);
851
+ // For provider=none (standalone), skip provider posting and only update review-state
852
+ if (!providerEnabled) {
853
+ log(fmt.status('INFO', `Review provider is none — skipping provider posting, updating review-state only for ${slug}.`));
854
+ const currentState = readReviewStateFn(slug, worktree);
855
+ let stateToWrite;
856
+ if (currentState) {
857
+ stateToWrite = currentState;
858
+ if (outcome === 'approve') {
859
+ stateToWrite.disposition = 'APPROVED';
860
+ try {
861
+ stateToWrite.transitionTo('approved');
862
+ }
863
+ catch (_) { /* ignore */ }
864
+ }
865
+ else if (outcome === 'request-changes') {
866
+ stateToWrite.disposition = 'REQUEST_CHANGES';
867
+ try {
868
+ stateToWrite.transitionTo('fixing');
869
+ }
870
+ catch (_) { /* ignore */ }
871
+ }
872
+ }
873
+ else {
874
+ // No existing state, create minimal state for tracking
875
+ const phaseForOutcome = outcome === 'approve' ? 'approved' : 'fixing';
876
+ stateToWrite = new review_state_js_1.ReviewState(slug, {
877
+ disposition: outcome === 'approve' ? 'APPROVED' : outcome === 'request-changes' ? 'REQUEST_CHANGES' : undefined,
878
+ reviewer: 'autonomous',
879
+ implementer: process.env.WORKFLOW_AGENT || 'autonomous',
880
+ round: 1,
881
+ phase: phaseForOutcome,
882
+ });
883
+ }
884
+ writeReviewStateFn(slug, stateToWrite, worktree);
885
+ // Also transition the backlog task for provider=none so integrate preflight passes
886
+ const backlogStatusMap = {
887
+ 'approve': 'approved',
888
+ 'request-changes': 'review',
889
+ 'comment': 'review'
890
+ };
891
+ const backlogStatus = backlogStatusMap[outcome];
892
+ if (backlogStatus && !transitionTaskFn(slug, backlogStatus, { rootDir: worktree, log })) {
893
+ log(fmt.status('WARN', `Could not transition backlog task ${slug} to ${backlogStatus}.`));
894
+ }
895
+ log(fmt.status('PASS', `Review outcome "${outcome}" recorded locally for ${slug}.`));
896
+ return;
897
+ }
898
+ // For provider-backed reviews, post through the adapter. Review-state is the normal source.
899
+ let reviewIdentity = (0, review_state_js_1.resolveReviewIdentity)(slug, worktree, {
900
+ readReviewStateFn,
901
+ }).identityUser;
902
+ if (!reviewIdentity) {
903
+ error(fmt.status('FAIL', `Cannot determine review identity for ${slug}. Persist review-state.json or set FORGEJO_USER.`));
904
+ exit(1);
905
+ return;
906
+ }
907
+ reviewIdentity = resolveReviewUserFn(reviewIdentity);
908
+ const result = (0, review_artifacts_js_1.postWorkflowReview)(slug, outcome, message, {
909
+ worktree,
910
+ reviewIdentity: reviewIdentity || undefined,
911
+ readTokenFn: options.readTokenFn,
912
+ postReviewFn: options.postReviewFn,
913
+ getPrAuthorFn: options.getPrAuthorFn,
914
+ writeReviewStateFn: options.writeReviewStateFn,
915
+ createEventFn: options.createEventFn,
916
+ readReviewStateFn: options.readReviewStateFn,
917
+ buildMetadataFooterFn: options.buildMetadataFooterFn,
918
+ log,
919
+ error
920
+ });
921
+ // Self-author skip: postWorkflowReview intentionally did not POST to the provider
922
+ // (reviewer == PR author) and already persisted the verdict locally. This is
923
+ // a legitimate same-agent-reviewer fallback, not a failure — do NOT exit(1).
924
+ if (result.ok && result.skipped) {
925
+ repairStaleActiveTaskAfterReview(slug, {
926
+ rootDir: worktree,
927
+ resolveTaskFileFn,
928
+ getTaskStatusFn,
929
+ transitionTaskFn,
930
+ log,
931
+ error
932
+ });
933
+ log(fmt.status('WARN', `Review outcome "${outcome}" recorded locally for ${slug} (self-approval POST skipped). A different agent or a human must post the formal provider approval.`));
934
+ return;
935
+ }
936
+ if (!result.ok) {
937
+ exit(1);
938
+ return;
939
+ }
952
940
  const currentState = readReviewStateFn(slug, worktree);
953
-
954
- let stateToWrite;
955
941
  if (currentState) {
956
- stateToWrite = currentState;
957
- if (outcome === 'approve') {
958
- stateToWrite.disposition = 'APPROVED';
959
- try { stateToWrite.transitionTo('approved'); } catch (_) {}
960
- } else if (outcome === 'request-changes') {
961
- stateToWrite.disposition = 'REQUEST_CHANGES';
962
- try { stateToWrite.transitionTo('fixing'); } catch (_) {}
963
- }
964
- } else {
965
- // No existing state, create minimal state for tracking
966
- const phaseForOutcome = outcome === 'approve' ? 'approved' : 'fixing';
967
- stateToWrite = new ReviewState(slug, {
968
- disposition: outcome === 'approve' ? 'APPROVED' : outcome === 'request-changes' ? 'REQUEST_CHANGES' : undefined,
969
- reviewer: 'autonomous',
970
- implementer: process.env.WORKFLOW_AGENT || 'autonomous',
971
- round: 1,
972
- phase: phaseForOutcome,
973
- });
974
- }
975
- writeReviewStateFn(slug, stateToWrite, worktree);
976
-
977
- // Also transition the backlog task for provider=none so integrate preflight passes
978
- const backlogStatusMap = {
979
- 'approve': 'approved',
980
- 'request-changes': 'review',
981
- 'comment': 'review'
982
- };
983
- const backlogStatus = backlogStatusMap[/** @type {keyof typeof backlogStatusMap} */ (outcome)];
984
- if (backlogStatus && !transitionTaskFn(slug, backlogStatus, { rootDir: worktree, log })) {
985
- log(fmt.status('WARN', `Could not transition backlog task ${slug} to ${backlogStatus}.`));
986
- }
987
-
988
- log(fmt.status('PASS', `Review outcome "${outcome}" recorded locally for ${slug}.`));
989
- return;
990
- }
991
-
992
- // For provider-backed reviews, post through the adapter. Review-state is the normal source.
993
- let reviewIdentity = resolveReviewIdentity(slug, worktree, {
994
- readReviewStateFn,
995
- }).identityUser;
996
-
997
- if (!reviewIdentity) {
998
- error(fmt.status('FAIL', `Cannot determine review identity for ${slug}. Persist review-state.json or set FORGEJO_USER.`));
999
- exit(1);
1000
- return;
1001
- }
1002
- reviewIdentity = resolveReviewUserFn(reviewIdentity);
1003
- const result = postWorkflowReview(slug, outcome, message, {
1004
- worktree,
1005
- reviewIdentity: reviewIdentity || undefined,
1006
- readTokenFn: options.readTokenFn,
1007
- postReviewFn: options.postReviewFn,
1008
- getPrAuthorFn: options.getPrAuthorFn,
1009
- writeReviewStateFn: options.writeReviewStateFn,
1010
- createEventFn: options.createEventFn,
1011
- readReviewStateFn: options.readReviewStateFn,
1012
- buildMetadataFooterFn: options.buildMetadataFooterFn,
1013
- log,
1014
- error
1015
- });
1016
-
1017
- // Self-author skip: postWorkflowReview intentionally did not POST to the provider
1018
- // (reviewer == PR author) and already persisted the verdict locally. This is
1019
- // a legitimate same-agent-reviewer fallback, not a failure — do NOT exit(1).
1020
- if (result.ok && result.skipped) {
1021
- repairStaleActiveTaskAfterReview(slug, {
1022
- rootDir: worktree,
1023
- resolveTaskFileFn,
1024
- getTaskStatusFn,
1025
- transitionTaskFn,
1026
- log,
1027
- error
1028
- });
1029
- log(fmt.status('WARN', `Review outcome "${outcome}" recorded locally for ${slug} (self-approval POST skipped). A different agent or a human must post the formal provider approval.`));
1030
- return;
1031
- }
1032
-
1033
- if (!result.ok) {
1034
- exit(1);
1035
- return;
1036
- }
1037
-
1038
- const currentState = readReviewStateFn(slug, worktree);
1039
- if (currentState) {
942
+ if (outcome === 'approve') {
943
+ currentState.disposition = 'APPROVED';
944
+ try {
945
+ currentState.transitionTo('approved');
946
+ }
947
+ catch (_) { /* ignore */ }
948
+ }
949
+ else if (outcome === 'request-changes') {
950
+ currentState.disposition = 'REQUEST_CHANGES';
951
+ try {
952
+ currentState.transitionTo('fixing');
953
+ }
954
+ catch (_) { /* ignore */ }
955
+ }
956
+ writeReviewStateFn(slug, currentState, worktree);
957
+ }
958
+ const taskResolution = resolveTaskFileFn(slug, worktree);
959
+ const currentStatus = taskResolution.ok ? getTaskStatusFn(taskResolution.taskFile) : null;
960
+ let backlogStatus = null;
1040
961
  if (outcome === 'approve') {
1041
- currentState.disposition = 'APPROVED';
1042
- try { currentState.transitionTo('approved'); } catch (_) {}
1043
- } else if (outcome === 'request-changes') {
1044
- currentState.disposition = 'REQUEST_CHANGES';
1045
- try { currentState.transitionTo('fixing'); } catch (_) {}
1046
- }
1047
- writeReviewStateFn(slug, currentState, worktree);
1048
- }
1049
-
1050
- const taskResolution = resolveTaskFileFn(slug, worktree);
1051
- const currentStatus = taskResolution.ok ? getTaskStatusFn(taskResolution.taskFile) : null;
1052
- let backlogStatus = null;
1053
-
1054
- if (outcome === 'approve') {
1055
- backlogStatus = currentStatus === 'active' ? 'review' : 'approved';
1056
- } else if (outcome === 'request-changes' || outcome === 'comment') {
1057
- backlogStatus = 'review';
1058
- }
1059
-
1060
- if (backlogStatus && !transitionTaskFn(slug, backlogStatus, { rootDir: worktree, log })) {
1061
- log(fmt.status('WARN', `Could not transition backlog task ${slug} to ${backlogStatus}.`));
1062
- }
962
+ backlogStatus = currentStatus === 'active' ? 'review' : 'approved';
963
+ }
964
+ else if (outcome === 'request-changes' || outcome === 'comment') {
965
+ backlogStatus = 'review';
966
+ }
967
+ if (backlogStatus && !transitionTaskFn(slug, backlogStatus, { rootDir: worktree, log })) {
968
+ log(fmt.status('WARN', `Could not transition backlog task ${slug} to ${backlogStatus}.`));
969
+ }
1063
970
  }
1064
-
1065
971
  // ============================================================================
1066
972
  // Command: closeMissionPr
1067
973
  // ============================================================================
1068
-
1069
- /**
1070
- * @param {string} slug
1071
- * @param {{log?: Function, error?: Function, exit?: Function, readTokenFn?: Function, readReviewStateFn?: Function, resolveReviewUserFn?: Function, resolveForgejoUserFn?: Function, closePrFn?: Function, worktree?: string}} [options]
1072
- * @returns {Promise<void>}
1073
- */
1074
974
  async function closeMissionPr(slug, options = {}) {
1075
- const log = options.log || fmt.log.plain;
1076
- const error = options.error || fmt.log.plainError;
1077
- const exit = options.exit || process.exit;
1078
- const readTokenFn = options.readTokenFn || readToken;
1079
- const readReviewStateFn = options.readReviewStateFn || readReviewState;
1080
- const resolveReviewUserFn = options.resolveReviewUserFn || options.resolveForgejoUserFn || resolveReviewUser;
1081
- const closePrFn = options.closePrFn || closePr;
1082
- const worktree = options.worktree || resolveWorktree(slug) || process.cwd();
1083
- const branch = missionBranchName(slug, worktree);
1084
-
1085
- let reviewIdentity = resolveReviewIdentity(slug, worktree, {
1086
- readReviewStateFn,
1087
- }).identityUser;
1088
-
1089
- if (!reviewIdentity) {
1090
- error(fmt.status('FAIL', `Cannot determine review identity for ${slug}. Persist review-state.json or set FORGEJO_USER.`));
1091
- exit(1);
1092
- return;
1093
- }
1094
- reviewIdentity = resolveReviewUserFn(reviewIdentity);
1095
-
1096
- const token = readTokenFn(reviewIdentity, { rootDir: worktree });
1097
- if (!token) {
1098
- error(fmt.status('FAIL', `No Forgejo token found for user "${reviewIdentity}".`));
1099
- exit(1);
1100
- return;
1101
- }
1102
-
1103
- log(fmt.status('INFO', `Closing PR for ${branch} as ${reviewIdentity}...`));
1104
- const result = await closePrFn(branch, token, reviewIdentity);
1105
-
1106
- if (!result.ok) {
1107
- exit(1);
1108
- }
975
+ const log = options.log || fmt.log.plain;
976
+ const error = options.error || fmt.log.plainError;
977
+ const exit = options.exit || process.exit;
978
+ const readTokenFn = options.readTokenFn || review_adapter_js_1.readToken;
979
+ const readReviewStateFn = options.readReviewStateFn || review_state_js_1.readReviewState;
980
+ const resolveReviewUserFn = options.resolveReviewUserFn || options.resolveForgejoUserFn || review_adapter_js_1.resolveReviewUser;
981
+ const closePrFn = options.closePrFn || review_adapter_js_1.closePr;
982
+ const worktree = options.worktree || (0, mission_utils_js_1.resolveWorktree)(slug) || process.cwd();
983
+ const branch = (0, mission_utils_js_1.missionBranchName)(slug, worktree);
984
+ let reviewIdentity = (0, review_state_js_1.resolveReviewIdentity)(slug, worktree, {
985
+ readReviewStateFn,
986
+ }).identityUser;
987
+ if (!reviewIdentity) {
988
+ error(fmt.status('FAIL', `Cannot determine review identity for ${slug}. Persist review-state.json or set FORGEJO_USER.`));
989
+ exit(1);
990
+ return;
991
+ }
992
+ reviewIdentity = resolveReviewUserFn(reviewIdentity);
993
+ const token = readTokenFn(reviewIdentity, { rootDir: worktree });
994
+ if (!token) {
995
+ error(fmt.status('FAIL', `No Forgejo token found for user "${reviewIdentity}".`));
996
+ exit(1);
997
+ return;
998
+ }
999
+ log(fmt.status('INFO', `Closing PR for ${branch} as ${reviewIdentity}...`));
1000
+ const result = await closePrFn(branch, token, reviewIdentity);
1001
+ if (!result.ok) {
1002
+ exit(1);
1003
+ }
1109
1004
  }
1110
-
1111
1005
  // ============================================================================
1112
1006
  // Event CLI Handlers
1113
1007
  // ============================================================================
1114
-
1115
- /**
1116
- * @param {string} slug
1117
- * @param {string[]} args
1118
- * @param {{log?: Function, error?: Function, exit?: Function, resolveWorktreeFn?: Function, readReviewStateFn?: Function}} [options]
1119
- * @returns {void}
1120
- */
1121
1008
  function createEventHandler(slug, args, options = {}) {
1122
- const log = options.log || fmt.log.plain;
1123
- const error = options.error || fmt.log.plainError;
1124
- const exit = options.exit || process.exit;
1125
- const resolveWorktreeFn = options.resolveWorktreeFn || resolveWorktree;
1126
-
1127
- const eventType = flagValue(args, '--type');
1128
- const inputFile = flagValue(args, '--input-file');
1129
- const actor = flagValue(args, '--actor');
1130
- const roundRaw = flagValue(args, '--round');
1131
- const phase = flagValue(args, '--phase');
1132
- const disposition = flagValue(args, '--disposition');
1133
- const verdict = flagValue(args, '--verdict');
1134
-
1135
- if (!eventType) {
1136
- error(fmt.status('FAIL', '--create-event requires --type <classification>'));
1137
- exit(1);
1138
- return;
1139
- }
1140
-
1141
- // Validate event type first
1142
- if (!isValidEventType(eventType)) {
1143
- error(fmt.status('FAIL', `Invalid event type "${eventType}". Valid: ${ALL_EVENT_TYPES.join(', ')}`));
1144
- exit(1);
1145
- return;
1146
- }
1147
-
1148
- // Read content from input file or stdin
1149
- let content = '';
1150
- if (inputFile) {
1151
- try {
1152
- content = fs.readFileSync(inputFile, 'utf8');
1153
- log(fmt.status('INFO', `Read event content from: ${inputFile}`));
1154
- } catch (err) {
1155
- error(fmt.status('FAIL', `Failed to read input file: ${/** @type {Error} */(err).message}`));
1156
- exit(1);
1157
- return;
1158
- }
1159
- }
1160
-
1161
- const round = roundRaw ? parseInt(roundRaw, 10) : undefined;
1162
- if (roundRaw && (isNaN(/** @type {number} */ (round))) ) {
1163
- error(fmt.status('FAIL', `--round must be a number, got "${roundRaw}"`));
1164
- exit(1);
1165
- return;
1166
- }
1167
-
1168
- const worktree = resolveWorktreeFn(slug) || process.cwd();
1169
-
1170
- // Build params
1171
- /** @type {{content: string, round?: number, phase?: string, actor?: string, disposition?: string, verdict?: string, fixedItems?: string[], pushedBackItems?: string[], parkedItems?: string[], blockedReason?: string}} */
1172
- const params = { content };
1173
- if (round !== undefined) {params.round = round;}
1174
- if (phase) {params.phase = phase;}
1175
- if (actor) {params.actor = actor;}
1176
- if (disposition) {params.disposition = disposition;}
1177
- if (verdict) {params.verdict = verdict;}
1178
-
1179
- const readReviewStateFn = options.readReviewStateFn || readReviewState;
1180
- const { identityUser: stateReviewIdentity } = resolveReviewIdentity(slug, worktree, {
1181
- readReviewStateFn,
1182
- });
1183
- const reviewIdentity = actor || stateReviewIdentity;
1184
-
1185
- // SC 4: For mirrored event types, a provider identity is required before creating the event.
1186
- if (shouldMirrorToProvider(eventType) && !reviewIdentity) {
1187
- error(fmt.status('FAIL', 'Cannot determine review identity for a mirrored event. Validate review-state.json or use --actor.'));
1188
- exit(1);
1189
- return;
1190
- }
1191
-
1192
- // Extract fields from content if structured
1193
- if (content.includes('fixed_items:') || content.includes('fixedItems:')) {
1194
- try {
1195
- const frontmatterMatch = content.match(/fixed_items:\s*(\[[^\]]*\])/i);
1196
- if (frontmatterMatch) {
1197
- params.fixedItems = JSON.parse(frontmatterMatch[1]);
1198
- }
1199
- } catch (_) {}
1200
- try {
1201
- const frontmatterMatch = content.match(/pushed_back_items:\s*(\[[^\]]*\])/i);
1202
- if (frontmatterMatch) {
1203
- params.pushedBackItems = JSON.parse(frontmatterMatch[1]);
1204
- }
1205
- } catch (_) {}
1206
- try {
1207
- const frontmatterMatch = content.match(/parked_items:\s*(\[[^\]]*\])/i);
1208
- if (frontmatterMatch) {
1209
- params.parkedItems = JSON.parse(frontmatterMatch[1]);
1210
- }
1211
- } catch (_) {}
1212
- try {
1213
- const frontmatterMatch = content.match(/blocked_reason:\s*"([^"]*)"/i);
1214
- if (frontmatterMatch) {
1215
- params.blockedReason = frontmatterMatch[1];
1216
- }
1217
- } catch (_) {}
1218
- }
1219
-
1220
- // Create the event
1221
- const result = createEvent(slug, eventType, params, {
1222
- worktree,
1223
- skipGit: false,
1224
- log,
1225
- error
1226
- });
1227
-
1228
- if (!result.ok) {
1229
- error(fmt.status('FAIL', `Could not create event: ${result.error}`));
1230
- exit(1);
1231
- return;
1232
- }
1233
-
1234
- log(fmt.status('PASS', `Created review event at: ${result.path}`));
1009
+ const log = options.log || fmt.log.plain;
1010
+ const error = options.error || fmt.log.plainError;
1011
+ const exit = options.exit || process.exit;
1012
+ const resolveWorktreeFn = options.resolveWorktreeFn || mission_utils_js_1.resolveWorktree;
1013
+ const eventType = flagValue(args, '--type');
1014
+ const inputFile = flagValue(args, '--input-file');
1015
+ const actor = flagValue(args, '--actor');
1016
+ const roundRaw = flagValue(args, '--round');
1017
+ const phase = flagValue(args, '--phase');
1018
+ const disposition = flagValue(args, '--disposition');
1019
+ const verdict = flagValue(args, '--verdict');
1020
+ if (!eventType) {
1021
+ error(fmt.status('FAIL', '--create-event requires --type <classification>'));
1022
+ exit(1);
1023
+ return;
1024
+ }
1025
+ // Validate event type first
1026
+ if (!(0, review_events_js_1.isValidEventType)(eventType)) {
1027
+ error(fmt.status('FAIL', `Invalid event type "${eventType}". Valid: ${review_events_js_1.ALL_EVENT_TYPES.join(', ')}`));
1028
+ exit(1);
1029
+ return;
1030
+ }
1031
+ // Read content from input file or stdin
1032
+ let content = '';
1033
+ if (inputFile) {
1034
+ try {
1035
+ content = fs.readFileSync(inputFile, 'utf8');
1036
+ log(fmt.status('INFO', `Read event content from: ${inputFile}`));
1037
+ }
1038
+ catch (err) {
1039
+ error(fmt.status('FAIL', `Failed to read input file: ${err.message}`));
1040
+ exit(1);
1041
+ return;
1042
+ }
1043
+ }
1044
+ const round = roundRaw ? parseInt(roundRaw, 10) : undefined;
1045
+ if (roundRaw && isNaN(round)) {
1046
+ error(fmt.status('FAIL', `--round must be a number, got "${roundRaw}"`));
1047
+ exit(1);
1048
+ return;
1049
+ }
1050
+ const worktree = resolveWorktreeFn(slug) || process.cwd();
1051
+ const params = { content };
1052
+ if (round !== undefined) {
1053
+ params.round = round;
1054
+ }
1055
+ if (phase) {
1056
+ params.phase = phase;
1057
+ }
1058
+ if (actor) {
1059
+ params.actor = actor;
1060
+ }
1061
+ if (disposition) {
1062
+ params.disposition = disposition;
1063
+ }
1064
+ if (verdict) {
1065
+ params.verdict = verdict;
1066
+ }
1067
+ const readReviewStateFn = options.readReviewStateFn || review_state_js_1.readReviewState;
1068
+ const { identityUser: stateReviewIdentity } = (0, review_state_js_1.resolveReviewIdentity)(slug, worktree, {
1069
+ readReviewStateFn,
1070
+ });
1071
+ const reviewIdentity = actor || stateReviewIdentity;
1072
+ // SC 4: For mirrored event types, a provider identity is required before creating the event.
1073
+ if ((0, review_events_js_1.shouldMirrorToProvider)(eventType) && !reviewIdentity) {
1074
+ error(fmt.status('FAIL', 'Cannot determine review identity for a mirrored event. Validate review-state.json or use --actor.'));
1075
+ exit(1);
1076
+ return;
1077
+ }
1078
+ // Extract fields from content if structured
1079
+ if (content.includes('fixed_items:') || content.includes('fixedItems:')) {
1080
+ try {
1081
+ const frontmatterMatch = content.match(/fixed_items:\s*(\[[^\]]*\])/i);
1082
+ if (frontmatterMatch) {
1083
+ params.fixedItems = JSON.parse(frontmatterMatch[1]);
1084
+ }
1085
+ }
1086
+ catch (_) { /* ignore */ }
1087
+ try {
1088
+ const frontmatterMatch = content.match(/pushed_back_items:\s*(\[[^\]]*\])/i);
1089
+ if (frontmatterMatch) {
1090
+ params.pushedBackItems = JSON.parse(frontmatterMatch[1]);
1091
+ }
1092
+ }
1093
+ catch (_) { /* ignore */ }
1094
+ try {
1095
+ const frontmatterMatch = content.match(/parked_items:\s*(\[[^\]]*\])/i);
1096
+ if (frontmatterMatch) {
1097
+ params.parkedItems = JSON.parse(frontmatterMatch[1]);
1098
+ }
1099
+ }
1100
+ catch (_) { /* ignore */ }
1101
+ try {
1102
+ const frontmatterMatch = content.match(/blocked_reason:\s*"([^"]*)"/i);
1103
+ if (frontmatterMatch) {
1104
+ params.blockedReason = frontmatterMatch[1];
1105
+ }
1106
+ }
1107
+ catch (_) { /* ignore */ }
1108
+ }
1109
+ // Create the event
1110
+ const result = (0, review_events_js_1.createEvent)(slug, eventType, params, {
1111
+ worktree,
1112
+ skipGit: false,
1113
+ log,
1114
+ error
1115
+ });
1116
+ if (!result.ok) {
1117
+ error(fmt.status('FAIL', `Could not create event: ${result.error}`));
1118
+ exit(1);
1119
+ return;
1120
+ }
1121
+ log(fmt.status('PASS', `Created review event at: ${result.path}`));
1235
1122
  }
1236
-
1237
- /**
1238
- * @param {string} slug
1239
- * @param {string[]} args
1240
- * @param {{log?: Function, error?: Function, exit?: Function, resolveWorktreeFn?: Function}} [options]
1241
- * @returns {void}
1242
- */
1243
1123
  function importLegacyHandler(slug, args, options = {}) {
1244
- const log = options.log || fmt.log.plain;
1245
- const error = options.error || fmt.log.plainError;
1246
- const exit = options.exit || process.exit;
1247
- const resolveWorktreeFn = options.resolveWorktreeFn || resolveWorktree;
1248
-
1249
- const tmpDir = flagValue(args, '--tmp-dir') || process.env.WORKFLOW_TMP_DIR || os.tmpdir();
1250
- const worktree = resolveWorktreeFn(slug) || process.cwd();
1251
-
1252
- const result = importAllLegacyArtifacts(slug, { tmpDir, worktree, log, error });
1253
-
1254
- const errors = result.errors || [];
1255
- if (!result.ok) {
1256
- error(fmt.status('FAIL', `Legacy import failed: ${errors.join(', ')}`));
1257
- exit(1);
1258
- return;
1259
- }
1260
-
1261
- log(fmt.status('PASS', `Imported ${result.imported ? result.imported.length : 0} legacy artifacts for ${slug}`));
1124
+ const log = options.log || fmt.log.plain;
1125
+ const error = options.error || fmt.log.plainError;
1126
+ const exit = options.exit || process.exit;
1127
+ const resolveWorktreeFn = options.resolveWorktreeFn || mission_utils_js_1.resolveWorktree;
1128
+ const tmpDir = flagValue(args, '--tmp-dir') || process.env.WORKFLOW_TMP_DIR || os.tmpdir();
1129
+ const worktree = resolveWorktreeFn(slug) || process.cwd();
1130
+ const result = (0, review_events_js_1.importAllLegacyArtifacts)(slug, { tmpDir, worktree, log, error });
1131
+ const errors = result.errors || [];
1132
+ if (!result.ok) {
1133
+ error(fmt.status('FAIL', `Legacy import failed: ${errors.join(', ')}`));
1134
+ exit(1);
1135
+ return;
1136
+ }
1137
+ log(fmt.status('PASS', `Imported ${result.imported ? result.imported.length : 0} legacy artifacts for ${slug}`));
1262
1138
  }
1263
-
1264
1139
  // ============================================================================
1265
1140
  // Main Dispatcher
1266
1141
  // ============================================================================
1267
-
1268
- /**
1269
- * @param {string[]} args
1270
- * @param {{inferSlugFn?: Function, log?: Function, error?: Function, exit?: Function, verifyReviewFn?: Function, submitForReviewFn?: Function, consumeArtifactsFn?: Function, pushRoundFn?: Function, readCommentsFn?: Function, commentRoundFn?: Function, submitReviewRoundFn?: Function, closeMissionPrFn?: Function, startReviewLoopFn?: Function, recordStageStatsSafeFn?: Function, startAgentFn?: Function, resolveTaskFileFn?: Function, getTaskImplementerFn?: Function, getPrStatusFn?: Function, readReviewStateFn?: Function, performStaticReviewFn?: Function, postStaticReviewCommentFn?: Function, resolveWorktreeFn?: Function, run?: Function, missionPath?: string}} [options]
1271
- * @returns {Promise<void>}
1272
- */
1273
1142
  async function review(args, options = {}) {
1274
- const inferSlugFn = options.inferSlugFn || inferSlug;
1275
- const log = options.log || fmt.log.plain;
1276
- const error = options.error || fmt.log.plainError;
1277
- const exit = options.exit || process.exit;
1278
- const verifyReviewFn = options.verifyReviewFn || verifyReview;
1279
- const submitForReviewFn = options.submitForReviewFn || submitForReview;
1280
- const consumeArtifactsFn = options.consumeArtifactsFn || consumeArtifacts;
1281
- const pushRoundFn = options.pushRoundFn || pushRound;
1282
- const readCommentsFn = options.readCommentsFn || readComments;
1283
- const commentRoundFn = options.commentRoundFn || commentRound;
1284
- const submitReviewRoundFn = options.submitReviewRoundFn || submitReviewRound;
1285
- const closeMissionPrFn = options.closeMissionPrFn || closeMissionPr;
1286
- const startReviewLoopFn = options.startReviewLoopFn || require('./review-loop').startReviewLoop;
1287
- const recordStageStatsSafeFn = options.recordStageStatsSafeFn || require('./review-loop').recordStageStatsSafe;
1288
- const startAgentFn = options.startAgentFn || startAgent;
1289
- const resolveTaskFileFn = options.resolveTaskFileFn || resolveTaskFile;
1290
- const getTaskImplementerFn = options.getTaskImplementerFn || getTaskImplementer;
1291
- const getPrStatusFn = options.getPrStatusFn || getPrStatus;
1292
- const readReviewStateFn = options.readReviewStateFn || readReviewState;
1293
- const performStaticReviewFn = options.performStaticReviewFn || performStaticReview;
1294
- const postStaticReviewCommentFn = options.postStaticReviewCommentFn || postStaticReviewComment;
1295
- const resolveWorktreeFn = options.resolveWorktreeFn || resolveWorktree;
1296
- const runFn = options.run || run;
1297
-
1298
- const flags = args.filter(a => a.startsWith('--'));
1299
- const params = args.filter(a => !a.startsWith('--'));
1300
-
1301
- const explicitSlug = params[0];
1302
- const slug = inferSlugFn(explicitSlug);
1303
- const isSubmit = flags.includes('--submit');
1304
- const isVerify = flags.includes('--verify');
1305
- const isStart = flags.includes('--start');
1306
- const isContinue = flags.includes('--continue');
1307
- const isPush = flags.includes('--push');
1308
- const isForce = flags.includes('--force');
1309
- const isComment = flags.includes('--comment') || flags.includes('--comment-file');
1310
- const isComments = flags.includes('--comments');
1311
- const isSubmitReview = flags.includes('--submit-review');
1312
- const isClose = flags.includes('--close');
1313
- const isStatus = flags.includes('--status');
1314
- const skipGate = flags.includes('--no-gate');
1315
- const isDryRun = flags.includes('--dry-run');
1316
- const isReset = flags.includes('--reset');
1317
- const isCreateEvent = flags.includes('--create-event');
1318
- const isImportLegacy = flags.includes('--import-legacy');
1319
- const isConsumeArtifacts = flags.includes('--consume-artifacts');
1320
- const missionPath = flagValue(args, '--mission');
1321
-
1322
- if (!slug) {
1323
- error('Usage: px review [<slug>] [--verify] [--submit] [--push] [--force] [--start] [--continue] [--no-gate] [--status] [--comments] [--comment "<msg>"|--comment-file <path>] [--submit-review <outcome> [--message "<summary>"|--message-file <path>] [--close] [--create-event --type <classification> [--input-file <path>] [--actor <name>] [--round <n>] [--phase <phase>] [--mission <path>]] [--import-legacy [--tmp-dir <dir>]] [--consume-artifacts]');
1324
- exit(1);
1325
- return;
1326
- }
1327
-
1328
- if (isStatus) {
1329
- showReviewStatus(slug, { ...options, readReviewStateFn });
1330
- return;
1331
- } else if (isVerify) {
1332
- verifyReviewFn(slug, skipGate, { ...options, missionPath });
1333
- } else if (isSubmit) {
1334
- // Pre-check: inspect the configured artifact directory for unprocessed files
1335
- // and emit a warning if artifacts are found but --submit-review was not used.
1336
- const artifactDir = require('./review-artifacts').resolveArtifactDir(resolveWorktreeFn(slug) || process.cwd());
1337
- const findingsPath = reviewArtifactPath(slug, 'review-findings.md', artifactDir);
1338
- const outcomePath = reviewArtifactPath(slug, 'review-outcome.md', artifactDir);
1339
- const verdictPath = reviewArtifactPath(slug, 'review-verdict.txt', artifactDir);
1340
- if (fs.existsSync(findingsPath) || fs.existsSync(outcomePath) || fs.existsSync(verdictPath)) {
1341
- log(fmt.status('WARN', `Unprocessed review artifacts found at ${artifactDir} for ${slug}. Consider using --consume-artifacts to persist them before handoff, or use --submit-review to post a verdict.`));
1342
- }
1343
- await submitForReviewFn(slug, skipGate, options);
1344
- } else if (isConsumeArtifacts) {
1345
- await consumeArtifactsFn(slug, options);
1346
- } else if (isPush) {
1347
- await pushRoundFn(slug, { ...options, force: isForce });
1348
- } else if (isComments) {
1349
- await readCommentsFn(slug, options);
1350
- } else if (isComment) {
1351
- const message = readTextFlag(args, '--comment', '--comment-file', 'comment', options);
1352
- if (!message) {
1353
- error(fmt.status('FAIL', '--comment requires text via --comment "<text>" or --comment-file <path>.'));
1354
- exit(1);
1355
- return;
1356
- }
1357
- commentRoundFn(slug, message, options);
1358
- } else if (isSubmitReview) {
1359
- const outcome = flagValue(args, '--submit-review');
1360
- const message = readTextFlag(args, '--message', '--message-file', 'review message', options) || '';
1361
- if (!outcome) {
1362
- error(fmt.status('FAIL', '--submit-review requires an outcome: px review <slug> --submit-review <approve|request-changes|comment> [--message "<summary>"|--message-file <path>]'));
1363
- exit(1);
1364
- return;
1365
- }
1366
- submitReviewRoundFn(slug, outcome, message, options);
1367
- } else if (isClose) {
1368
- await closeMissionPrFn(slug, options);
1369
- } else if (isCreateEvent) {
1370
- createEventHandler(slug, args, options);
1371
- return;
1372
- } else if (isImportLegacy) {
1373
- importLegacyHandler(slug, args, options);
1374
- return;
1375
- } else if (isStart || isContinue) {
1376
- const implementer = flagValue(args, '--implementer');
1377
- const reviewer = flagValue(args, '--reviewer');
1378
- const focus = flagValue(args, '--focus') || 'all';
1379
- const maxAttempts = parseInt(flagValue(args, '--max-attempts') || String(DEFAULT_MAX_ATTEMPTS), 10);
1380
- const verbose = flags.includes('--verbose');
1381
- const pollTimeoutRaw = flagValue(args, '--poll-timeout-seconds');
1382
- const pollTimeoutSeconds = pollTimeoutRaw ? parseInt(pollTimeoutRaw, 10) : null;
1383
- await startReviewLoopFn(slug, {
1384
- implementer,
1385
- reviewer,
1386
- focus,
1387
- maxAttempts,
1388
- dryRun: isDryRun,
1389
- reset: isReset,
1390
- isContinue,
1391
- verbose,
1392
- pollTimeoutSeconds,
1393
- missionPath,
1394
- recordStageStatsSafeFn
1395
- });
1396
- } else {
1397
- const pr = getPrStatusFn(missionBranchName(slug, process.cwd()), process.cwd());
1398
- log(fmt.status('INFO', `Review status for mission: ${fmt.slug(slug)}`));
1399
- if (pr.exists) {
1400
- log(pr.raw);
1401
- } else {
1402
- log(fmt.status('INFO', 'No active PR found for this mission.'));
1403
- // Static review: when branch exists but no PR is open, inspect the diff
1404
- // and check checkpoint evidence. Auto-trigger review loop if findings exist.
1405
- const worktreeForStatic = resolveWorktreeFn(slug) || process.cwd();
1406
- const staticResult = performStaticReviewFn(slug, { log, error, findMissionDir: findMissionDir, findCheckpoints: findCheckpoints, readFileSync: fs.readFileSync, run: runFn, resolveWorktree: resolveWorktreeFn, rootDir: worktreeForStatic, missionPath });
1407
- if (staticResult.findings && staticResult.findings.length > 0) {
1408
- // Trivial structural findings (missing Goal Check, no evidence rows, mission
1409
- // dir absent) are self-fixable, so re-launch the implementer with a targeted
1410
- // prompt instead of burning a full reviewer round via the review loop.
1411
- const taskResolution = resolveTaskFileFn(slug, worktreeForStatic);
1412
- const implementer = taskResolution && taskResolution.taskFile
1413
- ? getTaskImplementerFn(taskResolution.taskFile)
1414
- : null;
1415
- if (!implementer) {
1416
- log(fmt.status('WARN', `Static review found ${staticResult.findings.length} finding(s) but the implementer could not be resolved for ${fmt.slug(slug)} — not re-launching the implementer and not starting the review loop.`));
1417
- } else {
1418
- log(`\nStatic review found ${staticResult.findings.length} finding(s). Re-launching implementer (${fmt.agent(implementer)}) with a targeted fix prompt...`);
1419
- const findingLines = staticResult.findings.map((/** @type {string} */ f) => `- ${f}`).join('\n');
1420
- const prompt = `Static review of your mission branch found the following issue(s). Fix them and commit, then stop:\n${findingLines}`;
1421
- await startAgentFn('active', { prompt, worktree: worktreeForStatic, agent: implementer, slug });
1143
+ const inferSlugFn = options.inferSlugFn || mission_utils_js_1.inferSlug;
1144
+ const log = options.log || fmt.log.plain;
1145
+ const error = options.error || fmt.log.plainError;
1146
+ const exit = options.exit || process.exit;
1147
+ const verifyReviewFn = options.verifyReviewFn || verifyReview;
1148
+ const submitForReviewFn = options.submitForReviewFn || submitForReview;
1149
+ const consumeArtifactsFn = options.consumeArtifactsFn || consumeArtifacts;
1150
+ const pushRoundFn = options.pushRoundFn || pushRound;
1151
+ const readCommentsFn = options.readCommentsFn || readComments;
1152
+ const commentRoundFn = options.commentRoundFn || commentRound;
1153
+ const submitReviewRoundFn = options.submitReviewRoundFn || submitReviewRound;
1154
+ const closeMissionPrFn = options.closeMissionPrFn || closeMissionPr;
1155
+ const startReviewLoopFn = options.startReviewLoopFn || review_loop_js_1.startReviewLoop;
1156
+ const recordStageStatsSafeFn = options.recordStageStatsSafeFn || review_loop_js_1.recordStageStatsSafe;
1157
+ const startAgentFn = options.startAgentFn || agents_js_1.startAgent;
1158
+ const resolveTaskFileFn = options.resolveTaskFileFn || backlog_js_1.resolveTaskFile;
1159
+ const getTaskImplementerFn = options.getTaskImplementerFn || backlog_js_1.getTaskImplementer;
1160
+ const getPrStatusFn = options.getPrStatusFn || review_adapter_js_1.getPrStatus;
1161
+ const readReviewStateFn = options.readReviewStateFn || review_state_js_1.readReviewState;
1162
+ const performStaticReviewFn = options.performStaticReviewFn || performStaticReview;
1163
+ const postStaticReviewCommentFn = options.postStaticReviewCommentFn || postStaticReviewComment;
1164
+ const resolveWorktreeFn = options.resolveWorktreeFn || mission_utils_js_1.resolveWorktree;
1165
+ const runFn = options.run || git_js_1.run;
1166
+ const flags = args.filter(a => a.startsWith('--'));
1167
+ const params = args.filter(a => !a.startsWith('--'));
1168
+ const explicitSlug = params[0];
1169
+ const slug = inferSlugFn(explicitSlug);
1170
+ const isSubmit = flags.includes('--submit');
1171
+ const isVerify = flags.includes('--verify');
1172
+ const isStart = flags.includes('--start');
1173
+ const isContinue = flags.includes('--continue');
1174
+ const isPush = flags.includes('--push');
1175
+ const isForce = flags.includes('--force');
1176
+ const isComment = flags.includes('--comment') || flags.includes('--comment-file');
1177
+ const isComments = flags.includes('--comments');
1178
+ const isSubmitReview = flags.includes('--submit-review');
1179
+ const isClose = flags.includes('--close');
1180
+ const isStatus = flags.includes('--status');
1181
+ const skipGate = flags.includes('--no-gate');
1182
+ const isDryRun = flags.includes('--dry-run');
1183
+ const isReset = flags.includes('--reset');
1184
+ const isCreateEvent = flags.includes('--create-event');
1185
+ const isImportLegacy = flags.includes('--import-legacy');
1186
+ const isConsumeArtifacts = flags.includes('--consume-artifacts');
1187
+ const missionPath = flagValue(args, '--mission');
1188
+ if (!slug) {
1189
+ error('Usage: px review [<slug>] [--verify] [--submit] [--push] [--force] [--start] [--continue] [--no-gate] [--status] [--comments] [--comment "<msg>"|--comment-file <path>] [--submit-review <outcome> [--message "<summary>"|--message-file <path>] [--close] [--create-event --type <classification> [--input-file <path>] [--actor <name>] [--round <n>] [--phase <phase>] [--mission <path>]] [--import-legacy [--tmp-dir <dir>]] [--consume-artifacts]');
1190
+ exit(1);
1191
+ return;
1192
+ }
1193
+ if (isStatus) {
1194
+ showReviewStatus(slug, { ...options, readReviewStateFn });
1195
+ return;
1196
+ }
1197
+ else if (isVerify) {
1198
+ verifyReviewFn(slug, skipGate, { ...options, missionPath: missionPath || undefined });
1199
+ }
1200
+ else if (isSubmit) {
1201
+ // Pre-check: inspect the configured artifact directory for unprocessed files
1202
+ // and emit a warning if artifacts are found but --submit-review was not used.
1203
+ const artifactDir = (0, review_artifacts_js_1.resolveArtifactDir)(resolveWorktreeFn(slug) || process.cwd());
1204
+ const findingsPath = (0, review_artifacts_js_1.reviewArtifactPath)(slug, 'review-findings.md', artifactDir);
1205
+ const outcomePath = (0, review_artifacts_js_1.reviewArtifactPath)(slug, 'review-outcome.md', artifactDir);
1206
+ const verdictPath = (0, review_artifacts_js_1.reviewArtifactPath)(slug, 'review-verdict.txt', artifactDir);
1207
+ if (fs.existsSync(findingsPath) || fs.existsSync(outcomePath) || fs.existsSync(verdictPath)) {
1208
+ log(fmt.status('WARN', `Unprocessed review artifacts found at ${artifactDir} for ${slug}. Consider using --consume-artifacts to persist them before handoff, or use --submit-review to post a verdict.`));
1209
+ }
1210
+ await submitForReviewFn(slug, skipGate, options);
1211
+ }
1212
+ else if (isConsumeArtifacts) {
1213
+ await consumeArtifactsFn(slug, options);
1214
+ }
1215
+ else if (isPush) {
1216
+ await pushRoundFn(slug, { ...options, force: isForce });
1217
+ }
1218
+ else if (isComments) {
1219
+ await readCommentsFn(slug, options);
1220
+ }
1221
+ else if (isComment) {
1222
+ const message = readTextFlag(args, '--comment', '--comment-file', 'comment', options);
1223
+ if (!message) {
1224
+ error(fmt.status('FAIL', '--comment requires text via --comment "<text>" or --comment-file <path>.'));
1225
+ exit(1);
1226
+ return;
1422
1227
  }
1423
- } else if (staticResult.ok) {
1424
- log(fmt.status('INFO', 'Static review passed — no findings. Mission branch is clean.'));
1425
- const prStatus = getPrStatusFn(missionBranchName(slug, process.cwd()), process.cwd());
1426
- if (!prStatus.exists || prStatus.state !== 'open') {
1427
- log(fmt.status('INFO', 'No open PR found — submitting for review before finalizing static review...'));
1428
- await submitForReviewFn(slug, true, options);
1228
+ commentRoundFn(slug, message, options);
1229
+ }
1230
+ else if (isSubmitReview) {
1231
+ const outcome = flagValue(args, '--submit-review');
1232
+ const message = readTextFlag(args, '--message', '--message-file', 'review message', options) || '';
1233
+ if (!outcome) {
1234
+ error(fmt.status('FAIL', '--submit-review requires an outcome: px review <slug> --submit-review <approve|request-changes|comment> [--message "<summary>"|--message-file <path>]'));
1235
+ exit(1);
1236
+ return;
1429
1237
  }
1430
- postStaticReviewCommentFn(
1431
- slug,
1432
- formatStaticReviewSuccess(slug),
1433
- { ...options, rootDir: worktreeForStatic, log, error }
1434
- );
1435
- }
1238
+ submitReviewRoundFn(slug, outcome, message, options);
1436
1239
  }
1437
- const persisted = readReviewStateFn(slug);
1438
- if (persisted) {
1439
- log(fmt.status('INFO', `Persisted reviewer state: reviewer=${fmt.agent(persisted.reviewer)} implementer=${fmt.agent(persisted.implementer)} round=${persisted.round}`));
1240
+ else if (isClose) {
1241
+ await closeMissionPrFn(slug, options);
1242
+ }
1243
+ else if (isCreateEvent) {
1244
+ createEventHandler(slug, args, options);
1245
+ return;
1246
+ }
1247
+ else if (isImportLegacy) {
1248
+ importLegacyHandler(slug, args, options);
1249
+ return;
1250
+ }
1251
+ else if (isStart || isContinue) {
1252
+ const implementer = flagValue(args, '--implementer');
1253
+ const reviewer = flagValue(args, '--reviewer');
1254
+ const focus = flagValue(args, '--focus') || 'all';
1255
+ const maxAttempts = parseInt(flagValue(args, '--max-attempts') || String(DEFAULT_MAX_ATTEMPTS), 10);
1256
+ const verbose = flags.includes('--verbose');
1257
+ const pollTimeoutRaw = flagValue(args, '--poll-timeout-seconds');
1258
+ const pollTimeoutSeconds = pollTimeoutRaw ? parseInt(pollTimeoutRaw, 10) : null;
1259
+ await startReviewLoopFn(slug, {
1260
+ implementer: implementer || undefined,
1261
+ reviewer: reviewer || undefined,
1262
+ focus,
1263
+ maxAttempts,
1264
+ dryRun: isDryRun,
1265
+ reset: isReset,
1266
+ isContinue,
1267
+ verbose,
1268
+ pollTimeoutSeconds,
1269
+ missionPath: missionPath || undefined,
1270
+ recordStageStatsSafeFn
1271
+ });
1272
+ }
1273
+ else {
1274
+ const pr = getPrStatusFn((0, mission_utils_js_1.missionBranchName)(slug, process.cwd()), process.cwd());
1275
+ log(fmt.status('INFO', `Review status for mission: ${fmt.slug(slug)}`));
1276
+ if (pr.exists) {
1277
+ log(pr.raw);
1278
+ }
1279
+ else {
1280
+ log(fmt.status('INFO', 'No active PR found for this mission.'));
1281
+ // Static review: when branch exists but no PR is open, inspect the diff
1282
+ // and check checkpoint evidence. Auto-trigger review loop if findings exist.
1283
+ const worktreeForStatic = resolveWorktreeFn(slug) || process.cwd();
1284
+ const staticResult = performStaticReviewFn(slug, { log, findMissionDir: mission_utils_js_1.findMissionDir, findCheckpoints: mission_utils_js_1.findCheckpoints, readFileSync: fs.readFileSync, run: runFn, resolveWorktree: resolveWorktreeFn, rootDir: worktreeForStatic, missionPath: missionPath || undefined });
1285
+ if (staticResult.findings && staticResult.findings.length > 0) {
1286
+ // Trivial structural findings (missing Goal Check, no evidence rows, mission
1287
+ // dir absent) are self-fixable, so re-launch the implementer with a targeted
1288
+ // prompt instead of burning a full reviewer round via the review loop.
1289
+ const taskResolution = resolveTaskFileFn(slug, worktreeForStatic);
1290
+ const implementer = taskResolution && taskResolution.taskFile
1291
+ ? getTaskImplementerFn(taskResolution.taskFile)
1292
+ : null;
1293
+ if (!implementer) {
1294
+ log(fmt.status('WARN', `Static review found ${staticResult.findings.length} finding(s) but the implementer could not be resolved for ${fmt.slug(slug)} — not re-launching the implementer and not starting the review loop.`));
1295
+ }
1296
+ else {
1297
+ log(`\nStatic review found ${staticResult.findings.length} finding(s). Re-launching implementer (${fmt.agent(implementer)}) with a targeted fix prompt...`);
1298
+ const findingLines = staticResult.findings.map((f) => `- ${f}`).join('\n');
1299
+ const prompt = `Static review of your mission branch found the following issue(s). Fix them and commit, then stop:\n${findingLines}`;
1300
+ await startAgentFn('active', { prompt, worktree: worktreeForStatic, agent: implementer, slug });
1301
+ }
1302
+ }
1303
+ else if (staticResult.ok) {
1304
+ log(fmt.status('INFO', 'Static review passed — no findings. Mission branch is clean.'));
1305
+ const prStatus = getPrStatusFn((0, mission_utils_js_1.missionBranchName)(slug, process.cwd()), process.cwd());
1306
+ if (!prStatus.exists || prStatus.state !== 'open') {
1307
+ log(fmt.status('INFO', 'No open PR found — submitting for review before finalizing static review...'));
1308
+ await submitForReviewFn(slug, true, options);
1309
+ }
1310
+ postStaticReviewCommentFn(slug, formatStaticReviewSuccess(slug), { ...options, rootDir: worktreeForStatic, log, error });
1311
+ }
1312
+ }
1313
+ const persisted = readReviewStateFn(slug);
1314
+ if (persisted) {
1315
+ log(fmt.status('INFO', `Persisted reviewer state: reviewer=${fmt.agent(persisted.reviewer ?? '')} implementer=${fmt.agent(persisted.implementer ?? '')} round=${persisted.round}`));
1316
+ }
1440
1317
  }
1441
- }
1442
1318
  }
1443
-
1444
- // ============================================================================
1445
- // Module Exports
1446
- // ============================================================================
1447
-
1448
- module.exports = {
1449
- // Internal helpers
1450
- flagValue,
1451
- readTextFlag,
1452
- formatStaticReviewFindings,
1453
- formatStaticReviewSuccess,
1454
- postStaticReviewComment,
1455
- performStaticReview,
1456
- // Commands
1457
- verifyReview,
1458
- submitForReview,
1459
- consumeArtifacts,
1460
- readComments,
1461
- pushRound,
1462
- showReviewStatus,
1463
- commentRound,
1464
- submitReviewRound,
1465
- closeMissionPr,
1466
- // Event handlers
1467
- createEventHandler,
1468
- importLegacyHandler,
1469
- // Main dispatcher
1470
- review
1471
- };