@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.
- package/index.js +190 -185
- package/lib/agents/agents.js +737 -838
- package/lib/agents/agents.ts +930 -0
- package/lib/agents/claude-telemetry.js +149 -154
- package/lib/agents/claude-telemetry.ts +233 -0
- package/lib/agents/claude.js +121 -131
- package/lib/agents/claude.ts +156 -0
- package/lib/agents/codex-telemetry.js +176 -164
- package/lib/agents/codex-telemetry.ts +205 -0
- package/lib/agents/codex.js +168 -201
- package/lib/agents/codex.ts +236 -0
- package/lib/agents/limit-hit.js +216 -226
- package/lib/agents/limit-hit.ts +262 -0
- package/lib/agents/mistral-telemetry.js +7 -10
- package/lib/agents/mistral-telemetry.ts +44 -0
- package/lib/agents/mistral.js +40 -58
- package/lib/agents/mistral.ts +84 -0
- package/lib/agents/opencode-export.js +126 -127
- package/lib/agents/opencode-export.ts +160 -0
- package/lib/agents/opencode-telemetry.js +282 -261
- package/lib/agents/opencode-telemetry.ts +350 -0
- package/lib/agents/opencode.js +263 -305
- package/lib/agents/opencode.ts +370 -0
- package/lib/agents/stage-telemetry.js +23 -24
- package/lib/agents/stage-telemetry.ts +47 -0
- package/lib/commands/active.js +502 -579
- package/lib/commands/active.ts +665 -0
- package/lib/commands/checkpoint.js +98 -74
- package/lib/commands/checkpoint.ts +79 -0
- package/lib/commands/config.js +61 -31
- package/lib/commands/config.ts +47 -0
- package/lib/commands/coverage-gate.js +271 -246
- package/lib/commands/coverage-gate.ts +362 -0
- package/lib/commands/diff.js +140 -119
- package/lib/commands/diff.ts +125 -0
- package/lib/commands/draft.js +770 -923
- package/lib/commands/draft.ts +1021 -0
- package/lib/commands/handoff.js +588 -608
- package/lib/commands/handoff.ts +694 -0
- package/lib/commands/integrate.js +1332 -1463
- package/lib/commands/integrate.ts +1688 -0
- package/lib/commands/mission-start.js +279 -237
- package/lib/commands/mission-start.ts +263 -0
- package/lib/commands/rebase.js +606 -582
- package/lib/commands/rebase.ts +635 -0
- package/lib/commands/repair-handoff.js +181 -179
- package/lib/commands/repair-handoff.ts +228 -0
- package/lib/commands/resolve-conflict.js +121 -102
- package/lib/commands/resolve-conflict.ts +112 -0
- package/lib/commands/review.js +3 -4
- package/lib/commands/review.ts +14 -0
- package/lib/commands/setup-review.js +4 -5
- package/lib/commands/setup-review.ts +14 -0
- package/lib/commands/setup.js +3 -3
- package/lib/commands/setup.ts +2 -0
- package/lib/commands/stats-backfill.js +394 -359
- package/lib/commands/stats-backfill.ts +418 -0
- package/lib/commands/stats.js +1424 -1665
- package/lib/commands/stats.ts +2186 -0
- package/lib/commands/status.js +219 -200
- package/lib/commands/status.ts +219 -0
- package/lib/commands/verify.js +3 -1
- package/lib/commands/verify.ts +2 -0
- package/lib/core/fmt.js +140 -211
- package/lib/core/fmt.ts +189 -0
- package/lib/core/git.js +121 -102
- package/lib/core/git.ts +146 -0
- package/lib/core/gitignore.js +59 -106
- package/lib/core/gitignore.ts +103 -0
- package/lib/core/mission-utils.js +833 -875
- package/lib/core/mission-utils.ts +998 -0
- package/lib/core/nels.js +112 -127
- package/lib/core/nels.ts +189 -0
- package/lib/core/persistent-data-migration.js +247 -205
- package/lib/core/persistent-data-migration.ts +255 -0
- package/lib/core/product-config.js +400 -476
- package/lib/core/product-config.ts +518 -0
- package/lib/core/runtime-matrix.js +41 -87
- package/lib/core/runtime-matrix.ts +84 -0
- package/lib/core/spawn-tee.js +152 -192
- package/lib/core/spawn-tee.ts +202 -0
- package/lib/core/state-map.js +92 -92
- package/lib/core/state-map.ts +115 -0
- package/lib/core/storage.js +101 -129
- package/lib/core/storage.ts +182 -0
- package/lib/core/subagent-limit.js +15 -16
- package/lib/core/subagent-limit.ts +25 -0
- package/lib/core/verification.js +148 -138
- package/lib/core/verification.ts +200 -0
- package/lib/index.js +142 -59
- package/lib/index.ts +134 -0
- package/lib/review/rebase.js +142 -145
- package/lib/review/rebase.ts +188 -0
- package/lib/review/review-adapter.js +99 -87
- package/lib/review/review-adapter.ts +198 -0
- package/lib/review/review-artifacts.js +486 -629
- package/lib/review/review-artifacts.ts +622 -0
- package/lib/review/review-commands.js +1220 -1373
- package/lib/review/review-commands.ts +1579 -0
- package/lib/review/review-events.js +544 -886
- package/lib/review/review-events.ts +954 -0
- package/lib/review/review-loop.js +945 -1020
- package/lib/review/review-loop.ts +1174 -0
- package/lib/review/review-polling.js +86 -115
- package/lib/review/review-polling.ts +194 -0
- package/lib/review/review-prompts.js +154 -189
- package/lib/review/review-prompts.ts +182 -0
- package/lib/review/review-state.js +246 -225
- package/lib/review/review-state.ts +353 -0
- package/lib/review/review.js +63 -84
- package/lib/review/review.ts +138 -0
- package/lib/tools/backlog.js +694 -680
- package/lib/tools/backlog.ts +835 -0
- package/lib/tools/forgejo.js +1270 -1405
- package/lib/tools/forgejo.ts +1793 -0
- package/lib/tools/gatekeeper.js +106 -96
- package/lib/tools/gatekeeper.ts +124 -0
- package/lib/tools/redgreen.js +200 -181
- package/lib/tools/redgreen.ts +221 -0
- package/lib/tools/sessions.js +58 -52
- package/lib/tools/sessions.ts +81 -0
- package/lib/tools/setup-review.js +902 -975
- package/lib/tools/setup-review.ts +1152 -0
- package/package.json +7 -5
- package/prompts/draft.md +1 -0
- package/prompts/portfolio.md +1 -1
- package/px.js +226 -210
package/lib/review/rebase.js
CHANGED
|
@@ -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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
+
|