@magnusekdahl/parallix 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config/integration-pipelines.json +9 -0
- package/config/workflow.config.schema.json +12 -0
- package/docs/adr/0032-mission-refinement-state-and-usage-budget-signals.md +10 -8
- package/docs/adr/0036-mission-sizing-and-dependency-wave-heuristics.md +14 -12
- package/docs/adr/0041-integration-pipeline-gates.md +1 -1
- package/docs/adr/0047-per-mission-change-size-budget.md +161 -0
- package/docs/adr/index.md +1 -0
- package/docs/use-cases.md +44 -0
- package/lib/agents/agents.js +149 -100
- package/lib/agents/claude-telemetry.js +14 -10
- package/lib/agents/claude.js +32 -11
- package/lib/agents/codex-telemetry.js +32 -19
- package/lib/agents/codex.js +36 -11
- package/lib/agents/limit-hit.js +42 -25
- package/lib/agents/mistral-telemetry.js +1 -1
- package/lib/agents/mistral.js +13 -3
- package/lib/agents/opencode-export.js +11 -5
- package/lib/agents/opencode-telemetry.js +75 -90
- package/lib/agents/opencode.js +86 -29
- package/lib/agents/stage-telemetry.js +3 -7
- package/lib/commands/active.js +139 -94
- package/lib/commands/checkpoint.js +3 -1
- package/lib/commands/config.js +5 -3
- package/lib/commands/coverage-gate.js +17 -10
- package/lib/commands/diff.js +11 -5
- package/lib/commands/draft.js +95 -55
- package/lib/commands/handoff.js +253 -65
- package/lib/commands/integrate.js +288 -188
- package/lib/commands/mission-start.js +39 -34
- package/lib/commands/rebase.js +32 -21
- package/lib/commands/repair-handoff.js +21 -18
- package/lib/commands/resolve-conflict.js +5 -2
- package/lib/commands/review.js +1 -0
- package/lib/commands/setup-review.js +1 -0
- package/lib/commands/stats-backfill.js +50 -41
- package/lib/commands/stats.js +653 -204
- package/lib/commands/status.js +33 -28
- package/lib/core/fmt.js +58 -15
- package/lib/core/git.js +7 -5
- package/lib/core/gitignore.js +6 -4
- package/lib/core/mission-utils.js +161 -88
- package/lib/core/nels.js +199 -0
- package/lib/core/persistent-data-migration.js +55 -23
- package/lib/core/product-config.js +44 -23
- package/lib/core/runtime-matrix.js +15 -2
- package/lib/core/spawn-tee.js +40 -13
- package/lib/core/state-map.js +32 -13
- package/lib/core/storage.js +19 -5
- package/lib/core/subagent-limit.js +28 -0
- package/lib/core/verification.js +29 -12
- package/lib/review/rebase.js +11 -3
- package/lib/review/review-adapter.js +93 -6
- package/lib/review/review-artifacts.js +89 -23
- package/lib/review/review-commands.js +139 -45
- package/lib/review/review-events.js +103 -52
- package/lib/review/review-loop.js +117 -87
- package/lib/review/review-polling.js +25 -5
- package/lib/review/review-prompts.js +36 -5
- package/lib/review/review-state.js +39 -15
- package/lib/tools/backlog.js +162 -71
- package/lib/tools/forgejo.js +290 -129
- package/lib/tools/gatekeeper.js +15 -0
- package/lib/tools/redgreen.js +220 -0
- package/lib/tools/sessions.js +12 -5
- package/lib/tools/setup-review.js +170 -70
- package/package.json +11 -3
- package/prompts/draft.md +6 -0
- package/templates/mission-scaffold.md +1 -1
|
@@ -11,36 +11,37 @@ const { getPrStatus } = require('../tools/forgejo');
|
|
|
11
11
|
const { isForgejoReviewEnabled } = require('../core/product-config');
|
|
12
12
|
const stats = require('./stats');
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
const
|
|
14
|
+
/** @param {string[]} args @param {{log?: Function, error?: Function, cwdFn?: Function, getCurrentBranchFn?: Function, resolveTaskFileFn?: Function, getTaskStatusFn?: Function, toVirtualFn?: Function, findMissionDirFn?: Function, findCheckpointsFn?: Function, getFirstLineFn?: Function, inferSlugFn?: Function, getMissionYearFn?: Function, conventionalWorktreePathFn?: Function, getLastCommitFn?: Function, getPrStatusFn?: Function, evaluateRepositoryReadinessFn?: Function, evaluateReviewSetupFn?: Function, adapterChecklistFn?: Function, resolveMissionClassificationFn?: Function, isForgejoReviewEnabledFn?: Function, fsExistsSync?: Function, resolveMissionBaseBranchFn?: Function, getPrimaryBranchFn?: Function, gitFn?: Function, command?: string, returnResult?: boolean}} opts */
|
|
15
|
+
function missionStart(args, opts = {}) {
|
|
16
|
+
const log = opts.log || fmt.log.plain;
|
|
17
|
+
const error = opts.error || fmt.log.plainError;
|
|
18
|
+
const cwdFn = opts.cwdFn || (() => process.cwd());
|
|
19
|
+
const getCurrentBranchFn = opts.getCurrentBranchFn || getCurrentBranch;
|
|
20
|
+
const resolveTaskFileFn = opts.resolveTaskFileFn || resolveTaskFile;
|
|
21
|
+
const getTaskStatusFn = opts.getTaskStatusFn || getTaskStatus;
|
|
22
|
+
const toVirtualFn = opts.toVirtualFn || toVirtual;
|
|
23
|
+
const findMissionDirFn = opts.findMissionDirFn || findMissionDir;
|
|
24
|
+
const findCheckpointsFn = opts.findCheckpointsFn || findCheckpoints;
|
|
25
|
+
const getFirstLineFn = opts.getFirstLineFn || getFirstLine;
|
|
26
|
+
const inferSlugFn = opts.inferSlugFn || inferSlug;
|
|
27
|
+
const getMissionYearFn = opts.getMissionYearFn || getMissionYear;
|
|
28
|
+
const conventionalWorktreePathFn = opts.conventionalWorktreePathFn || conventionalWorktreePath;
|
|
29
|
+
const getLastCommitFn = opts.getLastCommitFn || getLastCommit;
|
|
30
|
+
const getPrStatusFn = opts.getPrStatusFn || getPrStatus;
|
|
31
|
+
const evaluateRepositoryReadinessFn = opts.evaluateRepositoryReadinessFn || evaluateRepositoryReadiness;
|
|
32
|
+
const evaluateReviewSetupFn = opts.evaluateReviewSetupFn || evaluateReviewSetup;
|
|
33
|
+
const adapterChecklistFn = opts.adapterChecklistFn || adapterChecklist;
|
|
34
|
+
const resolveMissionClassificationFn = opts.resolveMissionClassificationFn || stats.resolveMissionClassification;
|
|
35
|
+
const isForgejoReviewEnabledFn = opts.isForgejoReviewEnabledFn || isForgejoReviewEnabled;
|
|
36
|
+
const fsExistsSync = opts.fsExistsSync || fs.existsSync;
|
|
37
|
+
const resolveMissionBaseBranchFn = opts.resolveMissionBaseBranchFn || resolveMissionBaseBranch;
|
|
38
|
+
const getPrimaryBranchFn = opts.getPrimaryBranchFn || getPrimaryBranch;
|
|
39
|
+
const runFn = opts.gitFn || git;
|
|
39
40
|
|
|
40
41
|
const explicitSlug = args[0];
|
|
41
42
|
const slug = inferSlugFn(explicitSlug);
|
|
42
|
-
const isVerifyOnly =
|
|
43
|
-
const returnResult = Boolean(
|
|
43
|
+
const isVerifyOnly = opts.command === 'verify-env' || !slug;
|
|
44
|
+
const returnResult = Boolean(opts.returnResult);
|
|
44
45
|
|
|
45
46
|
if (isVerifyOnly) {
|
|
46
47
|
log(fmt.status('INFO', 'Running environment diagnostics (verify-env)...'));
|
|
@@ -49,6 +50,7 @@ function missionStart(args, options = {}) {
|
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
let overallFail = false;
|
|
53
|
+
/** @type{string[]} */
|
|
52
54
|
const remediationSteps = [];
|
|
53
55
|
|
|
54
56
|
// Check 1: PWD
|
|
@@ -150,8 +152,8 @@ function missionStart(args, options = {}) {
|
|
|
150
152
|
} else {
|
|
151
153
|
log(fmt.status('PASS', `Backlog classification: ${classification}`));
|
|
152
154
|
}
|
|
153
|
-
} catch (error) {
|
|
154
|
-
log(fmt.status('FAIL', `Backlog classification: ${error.message}`));
|
|
155
|
+
} catch (/** @type {unknown} */ error) {
|
|
156
|
+
log(fmt.status('FAIL', `Backlog classification: ${/** @type {any} */(error).message}`));
|
|
155
157
|
overallFail = true;
|
|
156
158
|
}
|
|
157
159
|
} else {
|
|
@@ -229,10 +231,13 @@ function missionStart(args, options = {}) {
|
|
|
229
231
|
}
|
|
230
232
|
|
|
231
233
|
// Extracted so callers can test the returnResult path without live git dependencies.
|
|
234
|
+
/** @param {boolean} overallFail @param {boolean} returnResult @param {{error?: Function, log?: Function, remediationSteps?: string[]}} options */
|
|
232
235
|
function completePreflightOrExit(overallFail, returnResult, options = {}) {
|
|
233
|
-
|
|
234
|
-
const
|
|
235
|
-
const
|
|
236
|
+
/** @type{{error?: Function, log?: Function, remediationSteps?: string[]}} */
|
|
237
|
+
const opts = options;
|
|
238
|
+
const error = opts.error || fmt.log.plainError;
|
|
239
|
+
const log = opts.log || fmt.log.plain;
|
|
240
|
+
const remediationSteps = opts.remediationSteps || [];
|
|
236
241
|
if (overallFail) {
|
|
237
242
|
let msg = '\n' + fmt.status('FAIL', 'Environment verdict: NOT USABLE');
|
|
238
243
|
if (remediationSteps.length > 0) {
|
|
@@ -244,11 +249,11 @@ function completePreflightOrExit(overallFail, returnResult, options = {}) {
|
|
|
244
249
|
msg += ' — fix blockers above before proceeding.';
|
|
245
250
|
}
|
|
246
251
|
error(msg);
|
|
247
|
-
if (returnResult) return { pass: false };
|
|
252
|
+
if (returnResult) {return { pass: false };}
|
|
248
253
|
process.exit(1);
|
|
249
254
|
} else {
|
|
250
255
|
log('\n' + fmt.status('PASS', 'Environment verdict: USABLE — this repository is ready for workflow commands.'));
|
|
251
|
-
if (returnResult) return { pass: true };
|
|
256
|
+
if (returnResult) {return { pass: true };}
|
|
252
257
|
process.exit(0);
|
|
253
258
|
}
|
|
254
259
|
}
|
package/lib/commands/rebase.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
2
|
const { detectRebaseState, git, getCurrentBranch } = require('../core/git');
|
|
3
3
|
const { resolveConflictsForMission } = require('./integrate');
|
|
4
|
-
const { findMissionDir, findMissionArea, inferSlug, resolveWorktree, conventionalWorktreePath, getPrimaryWorktree, getPrimaryBranch, resolveMissionBaseBranch, missionBranchName, missionDirForSlug
|
|
4
|
+
const { findMissionDir, findMissionArea, inferSlug, resolveWorktree, conventionalWorktreePath, getPrimaryWorktree, getPrimaryBranch, resolveMissionBaseBranch, missionBranchName, missionDirForSlug } = require('../core/mission-utils');
|
|
5
5
|
const { startAgent } = require('../agents/agents');
|
|
6
|
-
const { createPr, readToken, resolveForgejoUser } = require('../tools/forgejo');
|
|
6
|
+
const { createPr, readToken, resolveForgejoUser, fetchReviewBranch } = require('../tools/forgejo');
|
|
7
7
|
const { resolveTaskFile, getTaskImplementer } = require('../tools/backlog');
|
|
8
8
|
const { resolveReviewIdentity } = require('../review/review-state');
|
|
9
9
|
const { isForgejoReviewEnabled } = require('../core/product-config');
|
|
@@ -16,6 +16,7 @@ const { formatVerificationCommand } = require('../core/verification');
|
|
|
16
16
|
*
|
|
17
17
|
* Usage: px rebase [<slug>] [--push]
|
|
18
18
|
*/
|
|
19
|
+
/** @param {string[]} args @param {{inferSlugFn?: Function, findMissionDirFn?: Function, findMissionAreaFn?: Function, getCurrentBranchFn?: Function, resolveConflictsFn?: Function, startAgentFn?: Function, createPrFn?: Function, readTokenFn?: Function, resolveForgejoUserFn?: Function, resolveTaskFileFn?: Function, getTaskImplementerFn?: Function, detectRebaseStateFn?: Function, resolveMissionBaseBranchFn?: Function, gitFn?: Function, exitFn?: Function, isForgejoReviewEnabledFn?: Function, fetchReviewBranchFn?: Function}} opts */
|
|
19
20
|
async function rebase(args, {
|
|
20
21
|
inferSlugFn = inferSlug,
|
|
21
22
|
findMissionDirFn = findMissionDir,
|
|
@@ -31,8 +32,9 @@ async function rebase(args, {
|
|
|
31
32
|
detectRebaseStateFn = detectRebaseState,
|
|
32
33
|
resolveMissionBaseBranchFn = resolveMissionBaseBranch,
|
|
33
34
|
gitFn = git,
|
|
34
|
-
exitFn = (code) => process.exit(code),
|
|
35
|
+
exitFn = (/** @type{number} */ code) => process.exit(code),
|
|
35
36
|
isForgejoReviewEnabledFn = isForgejoReviewEnabled,
|
|
37
|
+
fetchReviewBranchFn = fetchReviewBranch,
|
|
36
38
|
} = {}) {
|
|
37
39
|
const flags = args.filter(a => a.startsWith('--'));
|
|
38
40
|
const params = args.filter(a => !a.startsWith('--'));
|
|
@@ -57,7 +59,7 @@ async function rebase(args, {
|
|
|
57
59
|
}
|
|
58
60
|
if (existingRebase.unmergedFiles.length > 0) {
|
|
59
61
|
fmt.log.info('Unmerged files:');
|
|
60
|
-
existingRebase.unmergedFiles.forEach(file => fmt.log.info(` - ${file}`));
|
|
62
|
+
existingRebase.unmergedFiles.forEach((/** @type {string} */ file) => fmt.log.info(` - ${file}`));
|
|
61
63
|
}
|
|
62
64
|
fmt.log.info('Recovery commands:');
|
|
63
65
|
fmt.log.info(` ${fmt.command('git rebase --continue')}`);
|
|
@@ -68,7 +70,7 @@ async function rebase(args, {
|
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
const performPush = async () => {
|
|
71
|
-
if (!isPush) return;
|
|
73
|
+
if (!isPush) {return;}
|
|
72
74
|
if (!isForgejoReviewEnabledFn(process.cwd())) {
|
|
73
75
|
fmt.log.info(`Skipping Forgejo push (review provider is not forgejo).`);
|
|
74
76
|
return;
|
|
@@ -85,13 +87,13 @@ async function rebase(args, {
|
|
|
85
87
|
}
|
|
86
88
|
forgejoUser = resolveForgejoUserFn(forgejoUser);
|
|
87
89
|
|
|
88
|
-
const token = readTokenFn(forgejoUser);
|
|
90
|
+
const token = readTokenFn(forgejoUser || 'default');
|
|
89
91
|
if (!token) {
|
|
90
|
-
fmt.log.fail(`No Forgejo token found for user "${fmt.agent(forgejoUser)}". Push failed.`);
|
|
92
|
+
fmt.log.fail(`No Forgejo token found for user "${fmt.agent(forgejoUser || 'default')}". Push failed.`);
|
|
91
93
|
exitFn(1);
|
|
92
94
|
return;
|
|
93
95
|
}
|
|
94
|
-
const result = createPrFn(branch, forgejoUser, token, { rootDir: getPrimaryWorktree(), forceWithLease: true });
|
|
96
|
+
const result = createPrFn(branch, forgejoUser || 'default', token, { rootDir: getPrimaryWorktree(), forceWithLease: true });
|
|
95
97
|
if (!result.ok) {
|
|
96
98
|
fmt.log.fail(`Push to Forgejo failed: ${result.error}`);
|
|
97
99
|
exitFn(1);
|
|
@@ -169,7 +171,9 @@ async function rebase(args, {
|
|
|
169
171
|
const rootDir = getPrimaryWorktree();
|
|
170
172
|
const worktreePath = resolveWorktree(slug) || conventionalWorktreePath(slug, rootDir);
|
|
171
173
|
|
|
172
|
-
|
|
174
|
+
/** @type{{worktreePathOverride?: string}} */
|
|
175
|
+
const conflictOpts = { worktreePathOverride: worktreePath };
|
|
176
|
+
const conflictResult = resolveConflictsFn(slug, area, conflictOpts);
|
|
173
177
|
|
|
174
178
|
// When rebase is in progress in the worktree, dry merge may fail.
|
|
175
179
|
// Fall back to parsing the rebase output directly.
|
|
@@ -200,7 +204,9 @@ async function rebase(args, {
|
|
|
200
204
|
sharedFiles,
|
|
201
205
|
});
|
|
202
206
|
} else if (!conflictResult.ok) {
|
|
203
|
-
|
|
207
|
+
/** @type{{error?: string}} */
|
|
208
|
+
const cr = conflictResult;
|
|
209
|
+
if (cr.error === 'worktree-missing') {
|
|
204
210
|
fmt.log.fail(`Mission worktree not found: ${fmt.path(worktreePath)}`);
|
|
205
211
|
fmt.log.info(`Ensure the worktree is registered: ${fmt.command(`git worktree add ${conventionalWorktreePath(slug, getPrimaryWorktree())} ${branch}`)}`);
|
|
206
212
|
exitFn(1);
|
|
@@ -221,7 +227,7 @@ async function rebase(args, {
|
|
|
221
227
|
// Only treat unclassified conflict files as shared-file conflicts
|
|
222
228
|
if (conflictResult.sharedFiles.length === 0 && conflictFiles.length > 0) {
|
|
223
229
|
const classified = new Set([...conflictResult.missionSpecificFiles]);
|
|
224
|
-
const unclassified = conflictFiles.filter(f => !classified.has(f));
|
|
230
|
+
const unclassified = conflictFiles.filter((/** @type {string} */ f) => !classified.has(f));
|
|
225
231
|
if (unclassified.length > 0) {
|
|
226
232
|
conflictResult.sharedFiles = unclassified;
|
|
227
233
|
}
|
|
@@ -232,10 +238,12 @@ async function rebase(args, {
|
|
|
232
238
|
fmt.log.info(`All ${conflictResult.missionSpecificFiles.length} conflict(s) are mission-specific. Auto-resolving...`);
|
|
233
239
|
const maxContinueAttempts = 3;
|
|
234
240
|
let continueAttempts = 0;
|
|
235
|
-
|
|
241
|
+
/** @param {{stdio?: string}} opts */
|
|
242
|
+
const continueRebase = (/** @type {{stdio?: string}} */ opts = {}) => {
|
|
236
243
|
continueAttempts += 1;
|
|
237
244
|
return gitFn(['-c', 'core.editor=true', '-c', 'merge.autoedit=no', 'rebase', '--continue'], opts);
|
|
238
245
|
};
|
|
246
|
+
/** @param {{stdout: string, stderr: string, status: number}} result */
|
|
239
247
|
const reportContinueFailure = (result) => {
|
|
240
248
|
const output = [result.stdout, result.stderr].filter(Boolean).join('\n').trim();
|
|
241
249
|
fmt.log.fail(`git rebase --continue failed (status ${result.status}).`);
|
|
@@ -251,6 +259,7 @@ async function rebase(args, {
|
|
|
251
259
|
fmt.log.fail(`Recovery: ${fmt.command('git rebase --abort')}`);
|
|
252
260
|
};
|
|
253
261
|
|
|
262
|
+
/** @param {string} branchName */
|
|
254
263
|
const failContinueBudget = (branchName) => {
|
|
255
264
|
fmt.log.fail(`Rebase still in progress on branch ${fmt.branch(branchName || slug)} after ${continueAttempts} failed --continue attempt(s).`);
|
|
256
265
|
fmt.log.fail(`Check rebase state: ${fmt.command('git status')}`);
|
|
@@ -415,7 +424,7 @@ async function rebase(args, {
|
|
|
415
424
|
|
|
416
425
|
// Shared-file conflicts exist — launch agent to resolve them
|
|
417
426
|
fmt.log.info(`${conflictResult.sharedFiles.length} shared file(s) require agent-assisted resolution:`);
|
|
418
|
-
conflictResult.sharedFiles.forEach(f => fmt.log.info(` - ${fmt.path(f)}`));
|
|
427
|
+
conflictResult.sharedFiles.forEach((/** @type {string} */ f) => fmt.log.info(` - ${fmt.path(f)}`));
|
|
419
428
|
|
|
420
429
|
const prompt = buildRebasePrompt({
|
|
421
430
|
slug,
|
|
@@ -423,7 +432,7 @@ async function rebase(args, {
|
|
|
423
432
|
worktreePath,
|
|
424
433
|
missionSpecificFiles: conflictResult.missionSpecificFiles,
|
|
425
434
|
sharedFiles: conflictResult.sharedFiles,
|
|
426
|
-
gitFn,
|
|
435
|
+
gitFn: /** @type{Function} */(gitFn),
|
|
427
436
|
});
|
|
428
437
|
|
|
429
438
|
fmt.log.info('Launching agent for conflict resolution...');
|
|
@@ -461,23 +470,24 @@ async function rebase(args, {
|
|
|
461
470
|
* when a rebase is in progress. Handles all unmerged states:
|
|
462
471
|
* UU (unmerged), DU/UD (modify/delete), AU/UA (add/add), AA (add/add).
|
|
463
472
|
*/
|
|
473
|
+
/** @param {string} worktreePath @param {Function} gitFn */
|
|
464
474
|
function parseConflictFilesFromGitStatus(worktreePath, gitFn) {
|
|
465
475
|
const statusResult = gitFn(['-C', worktreePath, 'status', '--porcelain']);
|
|
466
476
|
const files = [];
|
|
467
477
|
const unmergedStates = new Set(['UU', 'DU', 'UD', 'AU', 'UA', 'AA']);
|
|
468
478
|
for (const line of (statusResult.stdout || '').split('\n')) {
|
|
469
479
|
const trimmed = line.trim();
|
|
470
|
-
if (!trimmed) continue;
|
|
480
|
+
if (!trimmed) {continue;}
|
|
471
481
|
const match = trimmed.match(/^([A-Z]{2})\s+(.+)$/);
|
|
472
482
|
if (match) {
|
|
473
483
|
const code = match[1];
|
|
474
|
-
if (!unmergedStates.has(code)) continue;
|
|
484
|
+
if (!unmergedStates.has(code)) {continue;}
|
|
475
485
|
let file = match[2].trim();
|
|
476
486
|
// Remove surrounding quotes if present
|
|
477
487
|
if ((file.startsWith('"') && file.endsWith('"')) || (file.startsWith("'") && file.endsWith("'"))) {
|
|
478
488
|
file = file.slice(1, -1);
|
|
479
489
|
}
|
|
480
|
-
if (file) files.push(file);
|
|
490
|
+
if (file) {files.push(file);}
|
|
481
491
|
}
|
|
482
492
|
}
|
|
483
493
|
return [...new Set(files)];
|
|
@@ -487,11 +497,12 @@ function parseConflictFilesFromGitStatus(worktreePath, gitFn) {
|
|
|
487
497
|
* Parse conflict file paths from git rebase output.
|
|
488
498
|
* Similar to parseConflictFilesFromMergeOutput but handles rebase-specific output.
|
|
489
499
|
*/
|
|
500
|
+
/** @param {string} output */
|
|
490
501
|
function parseConflictFilesFromRebaseOutput(output) {
|
|
491
502
|
const seen = new Set();
|
|
492
503
|
const files = [];
|
|
493
504
|
for ( const line of output.split('\n')) {
|
|
494
|
-
if (!/CONFLICT|KONFLIKT/i.test(line)) continue;
|
|
505
|
+
if (!/CONFLICT|KONFLIKT/i.test(line)) {continue;}
|
|
495
506
|
// English: "Merge conflict in <file>"
|
|
496
507
|
const inMatch = line.match(/Merge conflict in (.+)$/);
|
|
497
508
|
if (inMatch) {
|
|
@@ -539,7 +550,7 @@ function parseConflictFilesFromRebaseOutput(output) {
|
|
|
539
550
|
const isNonPathPrefix = /^(CONFLICT|KONFLIKT|CONFLICTS|Merge conflict|Sammanslagningskonflikt|Automatic merge|Auto-merging|resolved|merged|deleted|added|changed|modified|rejected|skipped|dropped|superseded|discarded|kept|stashed|applied|already|would|both|ours|yours|their|his|her|its|your|my|us|we|they|he|she|it|a|an|the|but|and|or|for|nor|not|so|yet)\b/i.test(beforeColon);
|
|
540
551
|
if (!isNonPathPrefix) {
|
|
541
552
|
// Skip known advice/hint labels — the rest of the line is not a path.
|
|
542
|
-
if (/^(tips|hint|note)\b/i.test(beforeColon)) continue;
|
|
553
|
+
if (/^(tips|hint|note)\b/i.test(beforeColon)) {continue;}
|
|
543
554
|
const f = beforeColon;
|
|
544
555
|
if (f && !seen.has(f)) { seen.add(f); files.push(f); }
|
|
545
556
|
} else if (afterColon) {
|
|
@@ -566,12 +577,12 @@ function parseConflictFilesFromRebaseOutput(output) {
|
|
|
566
577
|
/**
|
|
567
578
|
* Build the prompt for the agent during shared-file conflict resolution.
|
|
568
579
|
*/
|
|
569
|
-
|
|
580
|
+
/** @param {{slug: string, area: string, worktreePath: string, missionSpecificFiles: string[], sharedFiles: string[], gitFn?: Function}} opts */
|
|
581
|
+
function buildRebasePrompt({ slug, area, worktreePath, missionSpecificFiles, sharedFiles, gitFn = /** @type {Function | undefined} */ (undefined) }) {
|
|
570
582
|
const missionFileCommands = missionSpecificFiles
|
|
571
583
|
.map(f => ` git checkout --theirs "${f}" && git add "${f}"`)
|
|
572
584
|
.join('\n');
|
|
573
585
|
const sharedFileList = sharedFiles.map(f => ` - ${f}`).join('\n');
|
|
574
|
-
const quotedWorktreePath = `"${worktreePath}"`;
|
|
575
586
|
|
|
576
587
|
let primaryBranch = 'main';
|
|
577
588
|
try {
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
const path = require('path');
|
|
2
1
|
const { git } = require('../core/git');
|
|
3
2
|
const missionUtils = require('../core/mission-utils');
|
|
4
3
|
const rebase = require('./rebase');
|
|
@@ -27,6 +26,7 @@ function isRelaunchableError(errorMsg) {
|
|
|
27
26
|
* @param {string} worktree - Path to the mission worktree
|
|
28
27
|
* @returns {string} The relaunch prompt
|
|
29
28
|
*/
|
|
29
|
+
/** @param {string} errorMsg @param {string} slug @param {string} worktree */
|
|
30
30
|
function buildRelaunchPrompt(errorMsg, slug, worktree) {
|
|
31
31
|
const year = missionUtils.getMissionYear(slug, worktree);
|
|
32
32
|
const missionDir = missionUtils.findMissionDir(slug, worktree) || missionUtils.missionDirForSlug(worktree, slug);
|
|
@@ -78,23 +78,26 @@ function buildRelaunchPrompt(errorMsg, slug, worktree) {
|
|
|
78
78
|
* @param {object} [options]
|
|
79
79
|
* @returns {Promise<{repaired: boolean, blocker: string|null}>} Result and optional blocker reason
|
|
80
80
|
*/
|
|
81
|
+
/** @param {string} slug @param {string} worktree @param {string} errorMsg @param {{gitFn?: Function, rebaseFn?: Function, log?: Function, error?: Function}} options */
|
|
81
82
|
async function repairHandoff(slug, worktree, errorMsg, options = {}) {
|
|
83
|
+
/** @type {{gitFn?: Function, rebaseFn?: Function, log?: Function, error?: Function}} */
|
|
84
|
+
const opts = options;
|
|
82
85
|
const {
|
|
83
|
-
taskFile = null,
|
|
84
86
|
gitFn = git,
|
|
85
87
|
rebaseFn = rebase,
|
|
86
88
|
log = fmt.log.plain,
|
|
87
89
|
error = fmt.log.plainError
|
|
88
|
-
} =
|
|
90
|
+
} = opts;
|
|
89
91
|
|
|
90
92
|
const rootDir = worktree || process.cwd();
|
|
91
93
|
let repaired = false;
|
|
92
94
|
let blocker = null;
|
|
93
95
|
|
|
96
|
+
/** @param {string} line */
|
|
94
97
|
function parsePorcelainPath(line) {
|
|
95
98
|
const xy = line.slice(0, 2);
|
|
96
99
|
const rawPath = line.slice(3).trim();
|
|
97
|
-
const pathPart = rawPath.includes('->') ? rawPath.split('->').pop().trim() : rawPath;
|
|
100
|
+
const pathPart = rawPath.includes('->') ? (rawPath.split('->').pop() || '').trim() : rawPath;
|
|
98
101
|
const cleanPath = (pathPart.startsWith('"') && pathPart.endsWith('"')) ? pathPart.slice(1, -1) : pathPart;
|
|
99
102
|
return { xy, file: cleanPath };
|
|
100
103
|
}
|
|
@@ -128,38 +131,38 @@ async function repairHandoff(slug, worktree, errorMsg, options = {}) {
|
|
|
128
131
|
const statusResult = gitFn(['-C', rootDir, 'status', '--porcelain']);
|
|
129
132
|
if (statusResult.status === 0 && statusResult.stdout) {
|
|
130
133
|
const dirtyLines = statusResult.stdout.split('\n')
|
|
131
|
-
.filter(line => line.trim().length > 0);
|
|
134
|
+
.filter(/** @param {string} line */ (line) => line.trim().length > 0);
|
|
132
135
|
|
|
133
136
|
const dirtyFilesWithStatus = dirtyLines.map(parsePorcelainPath);
|
|
134
137
|
|
|
135
|
-
const unmerged = dirtyFilesWithStatus.filter(f =>
|
|
138
|
+
const unmerged = dirtyFilesWithStatus.filter((/** @param {{xy: string, file: string}} f */ f =>
|
|
136
139
|
['DD', 'AU', 'UD', 'UA', 'DU', 'AA', 'UU'].includes(f.xy)
|
|
137
|
-
);
|
|
140
|
+
));
|
|
138
141
|
|
|
139
142
|
if (unmerged.length > 0) {
|
|
140
|
-
blocker = `Conflicted files detected:\n${unmerged.map(f => ` - ${f.file}`).join('\n')}`;
|
|
143
|
+
blocker = `Conflicted files detected:\n${unmerged.map((/** @param {{xy: string, file: string}} f */ f => ` - ${f.file}`)).join('\n')}`;
|
|
141
144
|
log(`Cannot auto-commit: ${blocker}`);
|
|
142
145
|
return { repaired: false, blocker };
|
|
143
146
|
}
|
|
144
147
|
|
|
145
|
-
const dirtyFiles = dirtyFilesWithStatus.map(f => f.file);
|
|
148
|
+
const dirtyFiles = dirtyFilesWithStatus.map((/** @param {{xy: string, file: string}} f */ f => f.file));
|
|
146
149
|
|
|
147
|
-
const isSafeToCommit = (file) =>
|
|
150
|
+
const isSafeToCommit = (/** @type{string} */ file) =>
|
|
148
151
|
missionUtils.isWorkflowGeneratedArtifact(file)
|
|
149
152
|
|| missionUtils.isMissionArtifact(file, slug, rootDir);
|
|
150
153
|
|
|
151
154
|
const safeFiles = dirtyFiles.filter(isSafeToCommit);
|
|
152
|
-
const unsafeFiles = dirtyFiles.filter(f => !isSafeToCommit(f));
|
|
155
|
+
const unsafeFiles = dirtyFiles.filter((/** @param {string} f */ f => !isSafeToCommit(f)));
|
|
153
156
|
|
|
154
157
|
if (unsafeFiles.length > 0) {
|
|
155
158
|
log(`Cannot auto-commit: dirty files include non-mission paths:`);
|
|
156
|
-
unsafeFiles.forEach(f => log(` - ${f}`));
|
|
159
|
+
unsafeFiles.forEach((/** @type {string} */ f) => log(` - ${f}`));
|
|
157
160
|
blocker = `dirty files include non-mission paths: ${unsafeFiles.join(', ')}`;
|
|
158
161
|
return { repaired: false, blocker };
|
|
159
162
|
} else if (safeFiles.length > 0) {
|
|
160
163
|
log(`Auto-committing mission artifacts:`);
|
|
161
|
-
const stageFailures = [];
|
|
162
|
-
safeFiles.forEach(f => {
|
|
164
|
+
const stageFailures = /** @type {string[]} */ ([]);
|
|
165
|
+
safeFiles.forEach((/** @type {string} */ f) => {
|
|
163
166
|
log(` - ${f}`);
|
|
164
167
|
const addResult = gitFn(['-C', rootDir, 'add', '--', f]);
|
|
165
168
|
if (addResult.status === 0) {
|
|
@@ -195,9 +198,9 @@ async function repairHandoff(slug, worktree, errorMsg, options = {}) {
|
|
|
195
198
|
try {
|
|
196
199
|
const { getCurrentBranch } = require('../core/git');
|
|
197
200
|
await rebaseFn([slug], {
|
|
198
|
-
gitFn: (args, opts) => gitFn(args, { ...opts, cwd: rootDir }),
|
|
201
|
+
gitFn: (/** @type {string[]} */ args, /** @type {{cwd?: string}} */ opts) => gitFn(args, { ...opts, cwd: rootDir }),
|
|
199
202
|
getCurrentBranchFn: () => getCurrentBranch(rootDir),
|
|
200
|
-
exitFn: (code) => {
|
|
203
|
+
exitFn: (/** @type {number} */ code) => {
|
|
201
204
|
if (code === 0) {
|
|
202
205
|
rebaseSuccess = true;
|
|
203
206
|
} else {
|
|
@@ -205,8 +208,8 @@ async function repairHandoff(slug, worktree, errorMsg, options = {}) {
|
|
|
205
208
|
}
|
|
206
209
|
}
|
|
207
210
|
});
|
|
208
|
-
} catch (err) {
|
|
209
|
-
rebaseError = err.message;
|
|
211
|
+
} catch (/** @type {unknown} */ err) {
|
|
212
|
+
rebaseError = /** @type{Error} */(err).message;
|
|
210
213
|
}
|
|
211
214
|
|
|
212
215
|
if (!rebaseSuccess) {
|
|
@@ -4,10 +4,12 @@ const { startAgent } = require('../agents/agents');
|
|
|
4
4
|
const fmt = require('../core/fmt');
|
|
5
5
|
const { formatVerificationCommand } = require('../core/verification');
|
|
6
6
|
|
|
7
|
+
/** @param {string} value */
|
|
7
8
|
function shellQuote(value) {
|
|
8
9
|
return `"${String(value).replace(/(["\\$`])/g, '\\$1')}"`;
|
|
9
10
|
}
|
|
10
11
|
|
|
12
|
+
/** @param {{slug: string, area: string, worktreePath: string, missionSpecificFiles: string[]}} opts */
|
|
11
13
|
function buildAgentResolutionPrompt({ slug, area, worktreePath, missionSpecificFiles }) {
|
|
12
14
|
const fileCommands = missionSpecificFiles
|
|
13
15
|
.map(f => ` git checkout --theirs "${f}" && git add "${f}"`)
|
|
@@ -41,10 +43,11 @@ function buildAgentResolutionPrompt({ slug, area, worktreePath, missionSpecificF
|
|
|
41
43
|
].join('\n');
|
|
42
44
|
}
|
|
43
45
|
|
|
46
|
+
/** @param {string[]} args @param {{resolveConflictsFn?: Function, startAgentFn?: Function, exitFn?: Function}} opts */
|
|
44
47
|
async function resolveConflict(args, {
|
|
45
48
|
resolveConflictsFn = resolveConflictsForMission,
|
|
46
49
|
startAgentFn = startAgent,
|
|
47
|
-
exitFn = (code) => process.exit(code),
|
|
50
|
+
exitFn = /** @type {(code: number) => void} */ ((code) => process.exit(code)),
|
|
48
51
|
} = {}) {
|
|
49
52
|
const explicitSlug = args[0];
|
|
50
53
|
const slug = inferSlug(explicitSlug);
|
|
@@ -69,7 +72,7 @@ async function resolveConflict(args, {
|
|
|
69
72
|
// Stop and require human intervention.
|
|
70
73
|
if (!result.ok && result.error === 'shared-file-conflicts') {
|
|
71
74
|
fmt.log.fail('Shared-file conflicts require human resolution. Agent cannot guess the correct merge outcome.');
|
|
72
|
-
fmt.log.info(`Shared files: ${result.sharedFiles.map(f => fmt.path(f)).join(', ')}`);
|
|
75
|
+
fmt.log.info(`Shared files: ${result.sharedFiles.map(/** @param {string} f */ (f) => fmt.path(f)).join(', ')}`);
|
|
73
76
|
fmt.log.info(`Resolve manually, then re-run: ${fmt.command(`px integrate ${slug} --dry-run`)}`);
|
|
74
77
|
exitFn(1);
|
|
75
78
|
return;
|
package/lib/commands/review.js
CHANGED