@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,3 +1,4 @@
1
+ "use strict";
1
2
  /**
2
3
  * Rebase helpers for pre-review workflow.
3
4
  *
@@ -6,15 +7,50 @@
6
7
  *
7
8
  * Both handoff.js and review-loop.js import from this module instead.
8
9
  */
9
-
10
- const path = require('path');
11
- const fmt = require('../core/fmt');
12
- const { git } = require('../core/git');
13
- const { resolveWorktree } = require('../core/mission-utils');
14
- const { isProviderEnabled } = require('./review-adapter');
15
-
10
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ var desc = Object.getOwnPropertyDescriptor(m, k);
13
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
14
+ desc = { enumerable: true, get: function() { return m[k]; } };
15
+ }
16
+ Object.defineProperty(o, k2, desc);
17
+ }) : (function(o, m, k, k2) {
18
+ if (k2 === undefined) k2 = k;
19
+ o[k2] = m[k];
20
+ }));
21
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
22
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
23
+ }) : function(o, v) {
24
+ o["default"] = v;
25
+ });
26
+ var __importStar = (this && this.__importStar) || (function () {
27
+ var ownKeys = function(o) {
28
+ ownKeys = Object.getOwnPropertyNames || function (o) {
29
+ var ar = [];
30
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
31
+ return ar;
32
+ };
33
+ return ownKeys(o);
34
+ };
35
+ return function (mod) {
36
+ if (mod && mod.__esModule) return mod;
37
+ var result = {};
38
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
39
+ __setModuleDefault(result, mod);
40
+ return result;
41
+ };
42
+ })();
43
+ Object.defineProperty(exports, "__esModule", { value: true });
44
+ exports.commitSafeMissionArtifacts = commitSafeMissionArtifacts;
45
+ exports.rebaseBeforeReviewRound = rebaseBeforeReviewRound;
46
+ const path = __importStar(require("path"));
47
+ const fmt = __importStar(require("../core/fmt.js"));
48
+ const git_js_1 = require("../core/git.js");
49
+ const mission_utils_js_1 = require("../core/mission-utils.js");
50
+ const review_adapter_js_1 = require("./review-adapter.js");
51
+ const child_process_1 = require("child_process");
52
+ const _spawnSync = child_process_1.spawnSync;
16
53
  const SHARED_FILE_REBASE_CONFLICT_RE = /shared file(?:\(s\))? require agent-assisted resolution|shared-file-conflicts/i;
17
-
18
54
  /**
19
55
  * Auto-commit safe mission artifacts before rebase.
20
56
  * Only touches mission-generated files, task files, and stats CSV.
@@ -23,149 +59,110 @@ const SHARED_FILE_REBASE_CONFLICT_RE = /shared file(?:\(s\))? require agent-assi
23
59
  * @param {{taskFile?: string|null, gitFn?: Function, log?: Function, error?: Function, isMissionArtifactFn?: Function, isWorkflowGeneratedArtifactFn?: Function, resolveStatsRelPathFn?: Function}} [options]
24
60
  * @returns {Promise<{ok: boolean, dirty: boolean, unsafe?: boolean}>}
25
61
  */
26
- async function commitSafeMissionArtifacts(slug, worktree, {
27
- taskFile = null,
28
- gitFn = git,
29
- log = fmt.log.plain,
30
- error = fmt.log.plainError,
31
- isMissionArtifactFn = require('../core/mission-utils').isMissionArtifact,
32
- isWorkflowGeneratedArtifactFn = require('../core/mission-utils').isWorkflowGeneratedArtifact,
33
- resolveStatsRelPathFn = () => null
34
- } = {}) {
35
- const rootDir = worktree || process.cwd();
36
- const statusResult = gitFn(['-C', rootDir, 'status', '--porcelain=v1', '-z']);
37
- if (statusResult.status !== 0 || !statusResult.stdout) {
38
- return { ok: true, dirty: false };
39
- }
40
-
41
- const parsePorcelainZ = (/** @type {string|Buffer} */ stdout) => {
42
- const entries = String(stdout).split('\0').filter(Boolean);
43
- const dirty = [];
44
- for (let i = 0; i < entries.length; i += 1) {
45
- const entry = entries[i];
46
- const xy = entry.slice(0, 2);
47
- const file = entry.slice(3);
48
- /** @type {{xy: string, file: string, paths: string[], source?: string}} */
49
- const record = { xy, file, paths: [file] };
50
- if ((xy[0] === 'R' || xy[0] === 'C') && i + 1 < entries.length) {
51
- const source = entries[i + 1];
52
- record.source = source;
53
- record.paths.push(source);
54
- i += 1;
55
- }
56
- dirty.push(record);
62
+ async function commitSafeMissionArtifacts(slug, worktree, { taskFile = null, gitFn = git_js_1.git, log = fmt.log.plain, error = fmt.log.plainError, isMissionArtifactFn = mission_utils_js_1.isMissionArtifact, isWorkflowGeneratedArtifactFn = mission_utils_js_1.isWorkflowGeneratedArtifact, resolveStatsRelPathFn = () => null } = {}) {
63
+ const rootDir = worktree || process.cwd();
64
+ const statusResult = gitFn(['-C', rootDir, 'status', '--porcelain=v1', '-z']);
65
+ if (statusResult.status !== 0 || !statusResult.stdout) {
66
+ return { ok: true, dirty: false };
67
+ }
68
+ const parsePorcelainZ = (stdout) => {
69
+ const entries = String(stdout).split('\0').filter(Boolean);
70
+ const dirty = [];
71
+ for (let i = 0; i < entries.length; i += 1) {
72
+ const entry = entries[i];
73
+ const xy = entry.slice(0, 2);
74
+ const file = entry.slice(3);
75
+ const record = { xy, file, paths: [file] };
76
+ if ((xy[0] === 'R' || xy[0] === 'C') && i + 1 < entries.length) {
77
+ const source = entries[i + 1];
78
+ record.source = source;
79
+ record.paths.push(source);
80
+ i += 1;
81
+ }
82
+ dirty.push(record);
83
+ }
84
+ return dirty;
85
+ };
86
+ const dirtyFiles = parsePorcelainZ(statusResult.stdout);
87
+ const unmerged = dirtyFiles.filter(f => ['DD', 'AU', 'UD', 'UA', 'DU', 'AA', 'UU'].includes(f.xy));
88
+ if (unmerged.length > 0) {
89
+ error(fmt.status('FAIL', `Cannot auto-commit: unmerged/conflicting files detected:\n${unmerged.map((f) => ` - ${f.file}`).join('\n')}`));
90
+ return { ok: false, dirty: true, unsafe: true };
91
+ }
92
+ const resolvedTaskFile = taskFile ? path.relative(rootDir, taskFile) : null;
93
+ const statsRelPath = resolveStatsRelPathFn(rootDir);
94
+ const isSafeToCommit = (file) => isWorkflowGeneratedArtifactFn(file)
95
+ || isMissionArtifactFn(file, slug, rootDir)
96
+ || !!(resolvedTaskFile && file === resolvedTaskFile)
97
+ || !!(statsRelPath && file === statsRelPath);
98
+ const unsafeFiles = dirtyFiles
99
+ .flatMap((f) => f.paths)
100
+ .filter((file) => !isSafeToCommit(file));
101
+ if (unsafeFiles.length > 0) {
102
+ error(fmt.status('FAIL', `Cannot auto-commit: dirty files include non-mission paths:`));
103
+ unsafeFiles.forEach((file) => error(` - ${file}`));
104
+ return { ok: false, dirty: true, unsafe: true };
105
+ }
106
+ log(`Auto-committing safe mission artifacts for ${fmt.branch(`mission/${slug}`)} before rebase...`);
107
+ dirtyFiles.forEach((f) => {
108
+ log(` - ${f.file}`);
109
+ gitFn(['-C', rootDir, 'add', '--', f.file]);
110
+ });
111
+ const commitRes = gitFn(['-C', rootDir, 'commit', '-m', `workflow(${slug}): auto-commit mission artifacts before pre-review rebase`]);
112
+ if (commitRes.status === 0) {
113
+ log(fmt.status('PASS', 'Mission artifacts committed.'));
114
+ return { ok: true, dirty: true };
115
+ }
116
+ else {
117
+ const failureMsg = [commitRes.stderr, commitRes.stdout].filter(Boolean).join('\n').trim();
118
+ error(fmt.status('FAIL', `Failed to commit mission artifacts: ${failureMsg}`));
119
+ return { ok: false, dirty: true };
57
120
  }
58
- return dirty;
59
- };
60
-
61
- const dirtyFiles = parsePorcelainZ(statusResult.stdout);
62
-
63
- const unmerged = dirtyFiles.filter(f => ['DD', 'AU', 'UD', 'UA', 'DU', 'AA', 'UU'].includes(f.xy));
64
- if (unmerged.length > 0) {
65
- error(fmt.status('FAIL', `Cannot auto-commit: unmerged/conflicting files detected:\n${unmerged.map(f => ` - ${f.file}`).join('\n')}`));
66
- return { ok: false, dirty: true, unsafe: true };
67
- }
68
-
69
- const resolvedTaskFile = taskFile ? path.relative(rootDir, taskFile) : null;
70
- const statsRelPath = resolveStatsRelPathFn(rootDir);
71
- const isSafeToCommit = (/** @type {string} */ file) =>
72
- isWorkflowGeneratedArtifactFn(file)
73
- || isMissionArtifactFn(file, slug, rootDir)
74
- || (resolvedTaskFile && file === resolvedTaskFile)
75
- || (statsRelPath && file === statsRelPath);
76
-
77
- const unsafeFiles = dirtyFiles
78
- .flatMap(f => f.paths)
79
- .filter((/** @type {string} */ file) => !isSafeToCommit(file));
80
- if (unsafeFiles.length > 0) {
81
- error(fmt.status('FAIL', `Cannot auto-commit: dirty files include non-mission paths:`));
82
- unsafeFiles.forEach(file => error(` - ${file}`));
83
- return { ok: false, dirty: true, unsafe: true };
84
- }
85
-
86
- log(`Auto-committing safe mission artifacts for ${fmt.branch(`mission/${slug}`)} before rebase...`);
87
- dirtyFiles.forEach(f => {
88
- log(` - ${f.file}`);
89
- gitFn(['-C', rootDir, 'add', '--', f.file]);
90
- });
91
-
92
- const commitRes = gitFn(['-C', rootDir, 'commit', '-m', `workflow(${slug}): auto-commit mission artifacts before pre-review rebase`]);
93
- if (commitRes.status === 0) {
94
- log(fmt.status('PASS', 'Mission artifacts committed.'));
95
- return { ok: true, dirty: true };
96
- } else {
97
- const failureMsg = [commitRes.stderr, commitRes.stdout].filter(Boolean).join('\n').trim();
98
- error(fmt.status('FAIL', `Failed to commit mission artifacts: ${failureMsg}`));
99
- return { ok: false, dirty: true };
100
- }
101
121
  }
102
-
122
+ /** @typedef {{ok: boolean, sharedFileConflicts: boolean}} RebaseResult */
103
123
  /**
104
124
  * Rebase the mission branch onto the latest primary/main branch before launching a reviewer.
105
- *
106
- * Always commits safe worktree state first, then rebases via the workflow CLI.
107
- * In standalone (provider-disabled) mode, skips the rebase after committing.
108
125
  * @param {string} slug
109
126
  * @param {{runFn?: Function, gitFn?: Function, taskFile?: string|null, worktree?: string, log?: Function, error?: Function, isReviewProviderEnabledFn?: Function|null, legacyIsForgejoReviewEnabledFn?: Function|null, isForgejoReviewEnabledFn?: Function|null}} [options]
110
127
  * @returns {Promise<{ok: boolean, sharedFileConflicts: boolean}>}
111
128
  */
112
- async function rebaseBeforeReviewRound(slug, {
113
- runFn = require('child_process').spawnSync,
114
- gitFn = git,
115
- taskFile = null,
116
- worktree = resolveWorktree(slug) || process.cwd(),
117
- log = fmt.log.plain,
118
- error = fmt.log.plainError,
119
- isReviewProviderEnabledFn = undefined,
120
- legacyIsForgejoReviewEnabledFn = null,
121
- isForgejoReviewEnabledFn = null
122
- } = {}) {
123
- const cleanup = await commitSafeMissionArtifacts(slug, worktree, { taskFile, gitFn, log, error });
124
- if (!cleanup.ok) {
125
- error(fmt.status('WARN', 'Worktree is dirty with unsafe or conflicted files. Rebase may fail.'));
126
- return { ok: false, sharedFileConflicts: false };
127
- }
128
-
129
- const forgejoEnabledFn = isReviewProviderEnabledFn
130
- || legacyIsForgejoReviewEnabledFn
131
- || isForgejoReviewEnabledFn
132
- || isProviderEnabled;
133
- const forgejoEnabled = forgejoEnabledFn(worktree);
134
- if (!forgejoEnabled) {
135
- log(fmt.status('INFO', `Review provider disabled; committed worktree state and skipping pre-review rebase for ${fmt.branch(`mission/${slug}`)}.`));
136
- return { ok: true, sharedFileConflicts: false };
137
- }
138
-
139
- const workflowCli = path.resolve(__dirname, '..', 'index.js');
140
-
141
- log(`Rebasing ${fmt.branch(`mission/${slug}`)} onto the latest primary branch before reviewer launch...`);
142
-
143
- const result = runFn(process.execPath, [workflowCli, 'rebase', slug, '--push'], {
144
- cwd: worktree,
145
- encoding: 'utf8',
146
- stdio: ['ignore', 'pipe', 'pipe']
147
- });
148
- const output = [result.stdout, result.stderr].filter(Boolean).join('\n').trim();
149
-
150
- if (result.status === 0) {
151
- log(fmt.status('PASS', `Pre-review rebase completed for ${fmt.branch(`mission/${slug}`)}.`));
152
- return { ok: true, sharedFileConflicts: false };
153
- }
154
-
155
- if (output) {
156
- error(output);
157
- }
158
- const sharedFileConflicts = SHARED_FILE_REBASE_CONFLICT_RE.test(output);
159
- if (sharedFileConflicts) {
160
- error(fmt.status('FAIL', 'Shared-file rebase conflicts detected. Autonomous review loop cannot continue safely.'));
161
- log(fmt.status('INFO', `Resolve the conflicts in the worktree, then re-run: px review ${slug} --start`));
162
- } else {
163
- error(fmt.status('FAIL', `Rebase failed before launching reviewer for ${fmt.branch(`mission/${slug}`)}.`));
164
- }
165
- return { ok: false, sharedFileConflicts };
129
+ async function rebaseBeforeReviewRound(slug, { runFn = _spawnSync, gitFn = git_js_1.git, taskFile = null, worktree = (0, mission_utils_js_1.resolveWorktree)(slug) || process.cwd(), log = fmt.log.plain, error = fmt.log.plainError, isReviewProviderEnabledFn = undefined, legacyIsForgejoReviewEnabledFn = null, isForgejoReviewEnabledFn = null } = {}) {
130
+ const cleanup = await commitSafeMissionArtifacts(slug, worktree, { taskFile, gitFn, log, error });
131
+ if (!cleanup.ok) {
132
+ error(fmt.status('WARN', 'Worktree is dirty with unsafe or conflicted files. Rebase may fail.'));
133
+ return { ok: false, sharedFileConflicts: false };
134
+ }
135
+ const forgejoEnabledFn = isReviewProviderEnabledFn
136
+ || legacyIsForgejoReviewEnabledFn
137
+ || isForgejoReviewEnabledFn
138
+ || review_adapter_js_1.isProviderEnabled;
139
+ const forgejoEnabled = forgejoEnabledFn(worktree);
140
+ if (!forgejoEnabled) {
141
+ log(fmt.status('INFO', `Review provider disabled; committed worktree state and skipping pre-review rebase for ${fmt.branch(`mission/${slug}`)}.`));
142
+ return { ok: true, sharedFileConflicts: false };
143
+ }
144
+ const workflowCli = path.resolve(__dirname, '..', 'index.js');
145
+ log(`Rebasing ${fmt.branch(`mission/${slug}`)} onto the latest primary branch before reviewer launch...`);
146
+ const result = runFn(process.execPath, [workflowCli, 'rebase', slug, '--push'], {
147
+ cwd: worktree,
148
+ encoding: 'utf8',
149
+ stdio: ['ignore', 'pipe', 'pipe']
150
+ });
151
+ const output = [result.stdout, result.stderr].filter(Boolean).join('\n').trim();
152
+ if (result.status === 0) {
153
+ log(fmt.status('PASS', `Pre-review rebase completed for ${fmt.branch(`mission/${slug}`)}.`));
154
+ return { ok: true, sharedFileConflicts: false };
155
+ }
156
+ if (output) {
157
+ error(output);
158
+ }
159
+ const sharedFileConflicts = SHARED_FILE_REBASE_CONFLICT_RE.test(output);
160
+ if (sharedFileConflicts) {
161
+ error(fmt.status('FAIL', 'Shared-file rebase conflicts detected. Autonomous review loop cannot continue safely.'));
162
+ log(fmt.status('INFO', `Resolve the conflicts in the worktree, then re-run: px review ${slug} --start`));
163
+ }
164
+ else {
165
+ error(fmt.status('FAIL', `Rebase failed before launching reviewer for ${fmt.branch(`mission/${slug}`)}.`));
166
+ }
167
+ return { ok: false, sharedFileConflicts };
166
168
  }
167
-
168
- module.exports = {
169
- commitSafeMissionArtifacts,
170
- rebaseBeforeReviewRound,
171
- };
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Rebase helpers for pre-review workflow.
3
+ *
4
+ * Extracted from review-loop.js to break the circular dependency:
5
+ * handoff.js -> review-loop.js -> handoff.js
6
+ *
7
+ * Both handoff.js and review-loop.js import from this module instead.
8
+ */
9
+
10
+ import * as path from 'path';
11
+ import * as fmt from '../core/fmt.js';
12
+ import { git } from '../core/git.js';
13
+ import { resolveWorktree, isMissionArtifact, isWorkflowGeneratedArtifact } from '../core/mission-utils.js';
14
+ import { isProviderEnabled } from './review-adapter.js';
15
+ import { spawnSync } from 'child_process';
16
+
17
+ type RunFn = (cmd: string, args: string[], opts: { cwd?: string; encoding?: string; stdio?: unknown[] }) => { status: number | null; stdout?: string; stderr?: string };
18
+
19
+ const _spawnSync = spawnSync as unknown as RunFn;
20
+
21
+ const SHARED_FILE_REBASE_CONFLICT_RE = /shared file(?:\(s\))? require agent-assisted resolution|shared-file-conflicts/i;
22
+
23
+ /**
24
+ * Auto-commit safe mission artifacts before rebase.
25
+ * Only touches mission-generated files, task files, and stats CSV.
26
+ * @param {string} slug
27
+ * @param {string} worktree
28
+ * @param {{taskFile?: string|null, gitFn?: Function, log?: Function, error?: Function, isMissionArtifactFn?: Function, isWorkflowGeneratedArtifactFn?: Function, resolveStatsRelPathFn?: Function}} [options]
29
+ * @returns {Promise<{ok: boolean, dirty: boolean, unsafe?: boolean}>}
30
+ */
31
+ export async function commitSafeMissionArtifacts(slug: string, worktree: string, {
32
+ taskFile = null,
33
+ gitFn = git,
34
+ log = fmt.log.plain,
35
+ error = fmt.log.plainError,
36
+ isMissionArtifactFn = isMissionArtifact,
37
+ isWorkflowGeneratedArtifactFn = isWorkflowGeneratedArtifact,
38
+ resolveStatsRelPathFn = () => null
39
+ }: {
40
+ taskFile?: string | null;
41
+ gitFn?: typeof git;
42
+ log?: (msg: string) => void;
43
+ error?: (msg: string) => void;
44
+ isMissionArtifactFn?: (file: string, slug: string, rootDir: string) => boolean;
45
+ isWorkflowGeneratedArtifactFn?: (file: string) => boolean;
46
+ resolveStatsRelPathFn?: (rootDir: string) => string | null;
47
+ } = {}): Promise<{ ok: boolean; dirty: boolean; unsafe?: boolean }> {
48
+ const rootDir = worktree || process.cwd();
49
+ const statusResult = gitFn(['-C', rootDir, 'status', '--porcelain=v1', '-z']);
50
+ if (statusResult.status !== 0 || !statusResult.stdout) {
51
+ return { ok: true, dirty: false };
52
+ }
53
+
54
+ const parsePorcelainZ = (stdout: string | Buffer) => {
55
+ const entries = String(stdout).split('\0').filter(Boolean);
56
+ const dirty: { xy: string; file: string; paths: string[]; source?: string }[] = [];
57
+ for (let i = 0; i < entries.length; i += 1) {
58
+ const entry = entries[i];
59
+ const xy = entry.slice(0, 2);
60
+ const file = entry.slice(3);
61
+ const record: { xy: string; file: string; paths: string[]; source?: string } = { xy, file, paths: [file] };
62
+ if ((xy[0] === 'R' || xy[0] === 'C') && i + 1 < entries.length) {
63
+ const source = entries[i + 1];
64
+ record.source = source;
65
+ record.paths.push(source);
66
+ i += 1;
67
+ }
68
+ dirty.push(record);
69
+ }
70
+ return dirty;
71
+ };
72
+
73
+ const dirtyFiles = parsePorcelainZ(statusResult.stdout);
74
+
75
+ const unmerged = dirtyFiles.filter(f => ['DD', 'AU', 'UD', 'UA', 'DU', 'AA', 'UU'].includes(f.xy));
76
+ if (unmerged.length > 0) {
77
+ error(fmt.status('FAIL', `Cannot auto-commit: unmerged/conflicting files detected:\n${unmerged.map((f: { file: string }) => ` - ${f.file}`).join('\n')}`));
78
+ return { ok: false, dirty: true, unsafe: true };
79
+ }
80
+
81
+ const resolvedTaskFile = taskFile ? path.relative(rootDir, taskFile) : null;
82
+ const statsRelPath = resolveStatsRelPathFn(rootDir);
83
+ const isSafeToCommit = (file: string) =>
84
+ isWorkflowGeneratedArtifactFn(file)
85
+ || isMissionArtifactFn(file, slug, rootDir)
86
+ || !!(resolvedTaskFile && file === resolvedTaskFile)
87
+ || !!(statsRelPath && file === statsRelPath);
88
+
89
+ const unsafeFiles = dirtyFiles
90
+ .flatMap((f: { paths: string[] }) => f.paths)
91
+ .filter((file: string) => !isSafeToCommit(file));
92
+ if (unsafeFiles.length > 0) {
93
+ error(fmt.status('FAIL', `Cannot auto-commit: dirty files include non-mission paths:`));
94
+ unsafeFiles.forEach((file: string) => error(` - ${file}`));
95
+ return { ok: false, dirty: true, unsafe: true };
96
+ }
97
+
98
+ log(`Auto-committing safe mission artifacts for ${fmt.branch(`mission/${slug}`)} before rebase...`);
99
+ dirtyFiles.forEach((f: { file: string }) => {
100
+ log(` - ${f.file}`);
101
+ gitFn(['-C', rootDir, 'add', '--', f.file]);
102
+ });
103
+
104
+ const commitRes = gitFn(['-C', rootDir, 'commit', '-m', `workflow(${slug}): auto-commit mission artifacts before pre-review rebase`]);
105
+ if (commitRes.status === 0) {
106
+ log(fmt.status('PASS', 'Mission artifacts committed.'));
107
+ return { ok: true, dirty: true };
108
+ } else {
109
+ const failureMsg = [commitRes.stderr, commitRes.stdout].filter(Boolean).join('\n').trim();
110
+ error(fmt.status('FAIL', `Failed to commit mission artifacts: ${failureMsg}`));
111
+ return { ok: false, dirty: true };
112
+ }
113
+ }
114
+
115
+ /** @typedef {{ok: boolean, sharedFileConflicts: boolean}} RebaseResult */
116
+
117
+ /**
118
+ * Rebase the mission branch onto the latest primary/main branch before launching a reviewer.
119
+ * @param {string} slug
120
+ * @param {{runFn?: Function, gitFn?: Function, taskFile?: string|null, worktree?: string, log?: Function, error?: Function, isReviewProviderEnabledFn?: Function|null, legacyIsForgejoReviewEnabledFn?: Function|null, isForgejoReviewEnabledFn?: Function|null}} [options]
121
+ * @returns {Promise<{ok: boolean, sharedFileConflicts: boolean}>}
122
+ */
123
+ export async function rebaseBeforeReviewRound(slug: string, {
124
+ runFn = _spawnSync,
125
+ gitFn = git,
126
+ taskFile = null,
127
+ worktree = resolveWorktree(slug) || process.cwd(),
128
+ log = fmt.log.plain,
129
+ error = fmt.log.plainError,
130
+ isReviewProviderEnabledFn = undefined,
131
+ legacyIsForgejoReviewEnabledFn = null,
132
+ isForgejoReviewEnabledFn = null
133
+ }: {
134
+ runFn?: (cmd: string, args: string[], opts: { cwd?: string; encoding?: string; stdio?: unknown[] }) => { status: number | null; stdout?: string; stderr?: string };
135
+ gitFn?: typeof git;
136
+ taskFile?: string | null;
137
+ worktree?: string;
138
+ log?: (msg: string) => void;
139
+ error?: (msg: string) => void;
140
+ isReviewProviderEnabledFn?: ((wt: string) => boolean) | undefined | null;
141
+ legacyIsForgejoReviewEnabledFn?: ((wt: string) => boolean) | null;
142
+ isForgejoReviewEnabledFn?: ((wt: string) => boolean) | null;
143
+ } = {}): Promise<{ ok: boolean; sharedFileConflicts: boolean }> {
144
+ const cleanup = await commitSafeMissionArtifacts(slug, worktree, { taskFile, gitFn, log, error });
145
+ if (!cleanup.ok) {
146
+ error(fmt.status('WARN', 'Worktree is dirty with unsafe or conflicted files. Rebase may fail.'));
147
+ return { ok: false, sharedFileConflicts: false };
148
+ }
149
+
150
+ const forgejoEnabledFn = isReviewProviderEnabledFn
151
+ || legacyIsForgejoReviewEnabledFn
152
+ || isForgejoReviewEnabledFn
153
+ || isProviderEnabled;
154
+ const forgejoEnabled = forgejoEnabledFn(worktree);
155
+ if (!forgejoEnabled) {
156
+ log(fmt.status('INFO', `Review provider disabled; committed worktree state and skipping pre-review rebase for ${fmt.branch(`mission/${slug}`)}.`));
157
+ return { ok: true, sharedFileConflicts: false };
158
+ }
159
+
160
+ const workflowCli = path.resolve(__dirname, '..', 'index.js');
161
+
162
+ log(`Rebasing ${fmt.branch(`mission/${slug}`)} onto the latest primary branch before reviewer launch...`);
163
+
164
+ const result = runFn(process.execPath, [workflowCli, 'rebase', slug, '--push'], {
165
+ cwd: worktree,
166
+ encoding: 'utf8',
167
+ stdio: ['ignore', 'pipe', 'pipe']
168
+ });
169
+ const output = [result.stdout, result.stderr].filter(Boolean).join('\n').trim();
170
+
171
+ if (result.status === 0) {
172
+ log(fmt.status('PASS', `Pre-review rebase completed for ${fmt.branch(`mission/${slug}`)}.`));
173
+ return { ok: true, sharedFileConflicts: false };
174
+ }
175
+
176
+ if (output) {
177
+ error(output);
178
+ }
179
+ const sharedFileConflicts = SHARED_FILE_REBASE_CONFLICT_RE.test(output);
180
+ if (sharedFileConflicts) {
181
+ error(fmt.status('FAIL', 'Shared-file rebase conflicts detected. Autonomous review loop cannot continue safely.'));
182
+ log(fmt.status('INFO', `Resolve the conflicts in the worktree, then re-run: px review ${slug} --start`));
183
+ } else {
184
+ error(fmt.status('FAIL', `Rebase failed before launching reviewer for ${fmt.branch(`mission/${slug}`)}.`));
185
+ }
186
+ return { ok: false, sharedFileConflicts };
187
+ }
188
+