@magnusekdahl/parallix 1.0.3 → 1.0.5
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/README.md +1 -1
- package/lib/agents/agents.js +13 -1
- package/lib/agents/opencode-export.js +58 -15
- package/lib/commands/integrate.js +19 -1
- package/lib/commands/rebase.js +20 -1
- package/lib/commands/status.js +33 -1
- package/lib/core/git.js +46 -0
- package/lib/review/review-loop.js +13 -4
- package/lib/review/review.js +1 -0
- package/package.json +1 -1
- package/prompts/draft.md +13 -1
- package/prompts/review-verbose.md +17 -1
- package/prompts/review.md +94 -7
package/README.md
CHANGED
|
@@ -40,7 +40,7 @@ Each capability below is tied to a use case in [`docs/use-cases.md`](docs/use-ca
|
|
|
40
40
|
- **Publish work to a Forgejo reviewer surface without making Forgejo your branch authority** *(Confirmed mechanic).* When the review provider is enabled, Parallix syncs the local baseline to a dedicated `review` remote and opens or updates the PR there; if Forgejo is disabled, the branch/worktree flow still runs locally.
|
|
41
41
|
- **Use a repo-local Graphify knowledge graph for smaller codebase context pulls** *(Confirmed mechanic, optional, unproven payoff).* In repositories where the operator has already installed the Graphify skill, the workflow keeps `graphify-out/` isolated per worktree and refreshes it during review/integration, while the installed agent guidance steers codebase questions toward `graphify query` / `path` / `explain` before full reports or raw grep. That should reduce context bloat, but this repo does not currently claim a measured token-usage reduction.
|
|
42
42
|
- **Keep your existing verification gate instead of agent self-reporting** *(UC-5 — Confirmed).* The gate is a configured shell command with a no-op default: declare your existing `make` / `npm` / script command in `workflow.config.json` and it runs verbatim; declare nothing and verification is a documented no-op pass, not an invented gate.
|
|
43
|
-
- **See which agent family actually pays off across every repo one runtime drives** *(UC-6 — Partial).* A single operator-owned `stats.csv` accumulates per-agent telemetry across repositories. Token-cost comparison is complete today only for the families with structured telemetry (codex, claude);
|
|
43
|
+
- **See which agent family actually pays off across every repo one runtime drives** *(UC-6 — Partial).* A single operator-owned `stats.csv` accumulates per-agent usage telemetry across repositories. Token-cost comparison is complete today only for the families with structured telemetry (codex, claude, opencode/qwen); vibe/mistral record honest zeros by design.
|
|
44
44
|
|
|
45
45
|
## The core workflow
|
|
46
46
|
|
package/lib/agents/agents.js
CHANGED
|
@@ -6,7 +6,7 @@ const { startCodexDraftAgent, resolveCodexCommand } = require('./codex');
|
|
|
6
6
|
const { startClaudeAgent, resolveClaudeCommand } = require('./claude');
|
|
7
7
|
const { startMistralAgent, resolveMistralCommand } = require('./mistral');
|
|
8
8
|
const { startOpencodeAgent, resolveOpencodeCommand } = require('./opencode');
|
|
9
|
-
const { detectLimitHit } = require('./limit-hit');
|
|
9
|
+
const { detectLimitHit, formatBlockUntil, DEFAULT_FALLBACK_HOURS } = require('./limit-hit');
|
|
10
10
|
const sessions = require('../tools/sessions');
|
|
11
11
|
const storage = require('../core/storage');
|
|
12
12
|
const { resolveAgentModel } = require('../core/product-config');
|
|
@@ -820,6 +820,18 @@ async function startAgent(step, opts = {}) {
|
|
|
820
820
|
});
|
|
821
821
|
tried.add(chosen);
|
|
822
822
|
launched.add(chosen);
|
|
823
|
+
// Block non-qwen agents on non-limit failures so selectAgent excludes them
|
|
824
|
+
// on the next retry iteration, and the review-loop fallback path can activate.
|
|
825
|
+
// qwen (opencode/local AI) is excluded — exit 1 is a temporary local error.
|
|
826
|
+
if (chosen !== 'qwen') {
|
|
827
|
+
const blockUntil = formatBlockUntil(new Date(Date.now() + DEFAULT_FALLBACK_HOURS * 60 * 60 * 1000));
|
|
828
|
+
try {
|
|
829
|
+
const blockResult = updateAgentBlockFn(chosen, blockUntil);
|
|
830
|
+
log(fmt.status('INFO', `Wrote blocklist entry for ${fmt.agent(chosen)} -> ${fmt.path(blockResult.path)} (${DEFAULT_FALLBACK_HOURS}h block)`));
|
|
831
|
+
} catch (err) {
|
|
832
|
+
log(fmt.status('WARN', `Could not persist blocklist entry for ${fmt.agent(chosen)}: ${err.message}`));
|
|
833
|
+
}
|
|
834
|
+
}
|
|
823
835
|
chosen = null;
|
|
824
836
|
continue;
|
|
825
837
|
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const childProcess = require('child_process');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const path = require('path');
|
|
4
7
|
|
|
5
8
|
/**
|
|
6
9
|
* Capture the complete `opencode export <sessionId>` JSON document.
|
|
@@ -15,6 +18,10 @@ const childProcess = require('child_process');
|
|
|
15
18
|
* - Fails explicitly (resolves null) when output exceeds `maxBytes` rather
|
|
16
19
|
* than silently truncating into unparseable JSON.
|
|
17
20
|
*
|
|
21
|
+
* Uses a temp-file fd for stdout to avoid pipe-buffer data loss that occurs
|
|
22
|
+
* with large (>100 KB) child-process output. The child writes directly to
|
|
23
|
+
* the fd; after exit we read the file back as a string.
|
|
24
|
+
*
|
|
18
25
|
* Never rejects: any error/timeout/oversize resolves to null so telemetry
|
|
19
26
|
* capture stays best-effort and cannot break the launch.
|
|
20
27
|
*
|
|
@@ -44,13 +51,21 @@ function captureOpencodeExport(sessionId, opts = {}) {
|
|
|
44
51
|
|
|
45
52
|
let settled = false;
|
|
46
53
|
let timer = null;
|
|
47
|
-
const chunks = [];
|
|
48
54
|
let size = 0;
|
|
55
|
+
let tmpFd = null;
|
|
56
|
+
let tmpPath = null;
|
|
49
57
|
|
|
50
58
|
const finish = (value) => {
|
|
51
59
|
if (settled) return;
|
|
52
60
|
settled = true;
|
|
53
61
|
if (timer) clearTimeout(timer);
|
|
62
|
+
// Clean up temp file
|
|
63
|
+
if (tmpFd !== null) {
|
|
64
|
+
try { fs.closeSync(tmpFd); } catch (_) { /* already closed */ }
|
|
65
|
+
}
|
|
66
|
+
if (tmpPath) {
|
|
67
|
+
try { fs.unlinkSync(tmpPath); } catch (_) { /* already gone */ }
|
|
68
|
+
}
|
|
54
69
|
resolve(value);
|
|
55
70
|
};
|
|
56
71
|
|
|
@@ -58,18 +73,33 @@ function captureOpencodeExport(sessionId, opts = {}) {
|
|
|
58
73
|
try { child.kill('SIGKILL'); } catch (_) { /* already gone */ }
|
|
59
74
|
};
|
|
60
75
|
|
|
76
|
+
// Create a temp file for stdout to avoid pipe-buffer truncation
|
|
77
|
+
let tmpFileError = null;
|
|
78
|
+
try {
|
|
79
|
+
tmpPath = path.join(os.tmpdir(), `opencode-export-${process.pid}-${Date.now()}.json`);
|
|
80
|
+
tmpFd = fs.openSync(tmpPath, 'w');
|
|
81
|
+
} catch (e) {
|
|
82
|
+
tmpFileError = e;
|
|
83
|
+
}
|
|
84
|
+
|
|
61
85
|
let child;
|
|
62
86
|
try {
|
|
63
87
|
child = spawn('opencode', ['export', sessionId], {
|
|
64
88
|
cwd: worktree,
|
|
65
89
|
env: { ...process.env, ...(env || {}) },
|
|
66
|
-
stdio: ['ignore', 'pipe', '
|
|
90
|
+
stdio: ['ignore', tmpFd !== null ? tmpFd : 'pipe', 'pipe'],
|
|
67
91
|
});
|
|
68
92
|
} catch (_) {
|
|
69
93
|
finish(null);
|
|
70
94
|
return;
|
|
71
95
|
}
|
|
72
96
|
|
|
97
|
+
if (tmpFileError) {
|
|
98
|
+
killChild(child);
|
|
99
|
+
finish(null);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
73
103
|
timer = setTimeout(() => {
|
|
74
104
|
// Hung or slow export: kill it and degrade to null. Do not block.
|
|
75
105
|
killChild(child);
|
|
@@ -80,29 +110,42 @@ function captureOpencodeExport(sessionId, opts = {}) {
|
|
|
80
110
|
// ref'd keeps the timeout deterministic under Node's test runner (an unref'd
|
|
81
111
|
// timer can be skipped when the loop is otherwise idle, cancelling the test).
|
|
82
112
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
113
|
+
// Consume stderr to prevent pipe-buffer issues with the opencode binary
|
|
114
|
+
if (child.stderr) {
|
|
115
|
+
child.stderr.on('data', () => { /* drain stderr */ });
|
|
86
116
|
}
|
|
87
117
|
|
|
88
|
-
child.
|
|
118
|
+
child.on('error', () => finish(null));
|
|
119
|
+
|
|
120
|
+
child.on('close', (code) => {
|
|
89
121
|
if (settled) return;
|
|
90
|
-
|
|
122
|
+
if (tmpFd === null) {
|
|
123
|
+
finish(null);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
fs.closeSync(tmpFd);
|
|
129
|
+
} catch (_) { /* already closed */ }
|
|
130
|
+
tmpFd = null;
|
|
131
|
+
|
|
132
|
+
let content;
|
|
133
|
+
try {
|
|
134
|
+
content = fs.readFileSync(tmpPath, 'utf8');
|
|
135
|
+
} catch (_) {
|
|
136
|
+
finish(null);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
size = Buffer.byteLength(content);
|
|
91
141
|
if (size > maxBytes) {
|
|
92
142
|
// Explicit failure rather than silent truncation: a partial JSON
|
|
93
143
|
// document would parse to null/garbage and fabricate zero telemetry.
|
|
94
|
-
killChild(child);
|
|
95
144
|
finish(null);
|
|
96
145
|
return;
|
|
97
146
|
}
|
|
98
|
-
chunks.push(chunk);
|
|
99
|
-
});
|
|
100
147
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
child.on('close', () => {
|
|
104
|
-
if (settled) return;
|
|
105
|
-
finish(Buffer.concat(chunks).toString('utf8'));
|
|
148
|
+
finish(content);
|
|
106
149
|
});
|
|
107
150
|
});
|
|
108
151
|
}
|
|
@@ -2,7 +2,7 @@ const fs = require('fs');
|
|
|
2
2
|
const os = require('os');
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const child_process = require('child_process');
|
|
5
|
-
const { git, getCurrentBranch } = require('../core/git');
|
|
5
|
+
const { detectRebaseState, git, getCurrentBranch } = require('../core/git');
|
|
6
6
|
const { resolveTaskFile, getTaskStatus, setTaskStatus, completeTask, getTaskAssignee } = require('../tools/backlog');
|
|
7
7
|
const { toVirtual, toActual } = require('../core/state-map');
|
|
8
8
|
const { getPrStatus, getLatestReviewDecision, syncMerged, readToken, resolveTokenFile, resolveForgejoUser, resolveForgejoHome, isForgejoPath, listOpenPrsForSlug } = require('../tools/forgejo');
|
|
@@ -935,6 +935,7 @@ function printIntegrationPreflight(
|
|
|
935
935
|
{
|
|
936
936
|
readTokenFn = readToken,
|
|
937
937
|
resolveTokenFileFn = resolveTokenFile,
|
|
938
|
+
detectRebaseStateFn = detectRebaseState,
|
|
938
939
|
getUnresolvedIndexConflictsFn = getUnresolvedIndexConflicts,
|
|
939
940
|
findMissionDocInBranchesFn = findMissionDocInBranches,
|
|
940
941
|
isForgejoReviewEnabledFn = isForgejoReviewEnabled,
|
|
@@ -1088,6 +1089,23 @@ function printIntegrationPreflight(
|
|
|
1088
1089
|
log(fmt.status('INFO', `Retry with: git -C ${baseWorktree} checkout ${expectedPrimaryBranch}`));
|
|
1089
1090
|
}
|
|
1090
1091
|
|
|
1092
|
+
const rebaseState = detectRebaseStateFn(baseWorktree);
|
|
1093
|
+
if (rebaseState.inProgress) {
|
|
1094
|
+
failures.push('rebase-in-progress');
|
|
1095
|
+
log(fmt.status('FAIL', `Integration checkout rebase: rebase in progress in ${baseWorktree}`));
|
|
1096
|
+
if (rebaseState.rebaseHead) {
|
|
1097
|
+
log(fmt.status('INFO', `Current rebase head: ${rebaseState.rebaseHead}`));
|
|
1098
|
+
}
|
|
1099
|
+
if (rebaseState.unmergedFiles.length > 0) {
|
|
1100
|
+
rebaseState.unmergedFiles.forEach(file => log(fmt.status('INFO', ` - ${file}`)));
|
|
1101
|
+
}
|
|
1102
|
+
log(fmt.status('INFO', 'Finish or abort the existing rebase before retrying:'));
|
|
1103
|
+
log(fmt.status('INFO', ` git -C ${baseWorktree} rebase --continue`));
|
|
1104
|
+
log(fmt.status('INFO', ` git -C ${baseWorktree} rebase --abort`));
|
|
1105
|
+
log(fmt.status('INFO', ` git -C ${baseWorktree} rebase --skip`));
|
|
1106
|
+
log(fmt.status('INFO', `Retry with: px integrate ${context.slug} --dry-run`));
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1091
1109
|
const indexConflicts = getUnresolvedIndexConflictsFn(baseWorktree);
|
|
1092
1110
|
if (!indexConflicts.ok) {
|
|
1093
1111
|
failures.push('main-index-conflict-check');
|
package/lib/commands/rebase.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
|
-
const { git, getCurrentBranch } = require('../core/git');
|
|
2
|
+
const { detectRebaseState, git, getCurrentBranch } = require('../core/git');
|
|
3
3
|
const { resolveConflictsForMission } = require('./integrate');
|
|
4
4
|
const { findMissionDir, findMissionArea, inferSlug, resolveWorktree, conventionalWorktreePath, getPrimaryWorktree, getPrimaryBranch, missionBranchName, missionDirForSlug, getMissionYear } = require('../core/mission-utils');
|
|
5
5
|
const { startAgent } = require('../agents/agents');
|
|
@@ -28,6 +28,7 @@ async function rebase(args, {
|
|
|
28
28
|
resolveForgejoUserFn = resolveForgejoUser,
|
|
29
29
|
resolveTaskFileFn = resolveTaskFile,
|
|
30
30
|
getTaskImplementerFn = getTaskImplementer,
|
|
31
|
+
detectRebaseStateFn = detectRebaseState,
|
|
31
32
|
gitFn = git,
|
|
32
33
|
exitFn = (code) => process.exit(code),
|
|
33
34
|
isForgejoReviewEnabledFn = isForgejoReviewEnabled,
|
|
@@ -47,6 +48,24 @@ async function rebase(args, {
|
|
|
47
48
|
const area = missionDir ? findMissionAreaFn(missionDir) : 'docs';
|
|
48
49
|
const branch = missionBranchName(slug);
|
|
49
50
|
|
|
51
|
+
const existingRebase = detectRebaseStateFn(process.cwd());
|
|
52
|
+
if (existingRebase.inProgress) {
|
|
53
|
+
fmt.log.fail(`Rebase already in progress for ${fmt.branch(branch)}.`);
|
|
54
|
+
if (existingRebase.rebaseHead) {
|
|
55
|
+
fmt.log.info(`Current rebase head: ${existingRebase.rebaseHead}`);
|
|
56
|
+
}
|
|
57
|
+
if (existingRebase.unmergedFiles.length > 0) {
|
|
58
|
+
fmt.log.info('Unmerged files:');
|
|
59
|
+
existingRebase.unmergedFiles.forEach(file => fmt.log.info(` - ${file}`));
|
|
60
|
+
}
|
|
61
|
+
fmt.log.info('Recovery commands:');
|
|
62
|
+
fmt.log.info(` ${fmt.command('git rebase --continue')}`);
|
|
63
|
+
fmt.log.info(` ${fmt.command('git rebase --abort')}`);
|
|
64
|
+
fmt.log.info(` ${fmt.command('git rebase --skip')}`);
|
|
65
|
+
exitFn(1);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
50
69
|
const performPush = async () => {
|
|
51
70
|
if (!isPush) return;
|
|
52
71
|
if (!isForgejoReviewEnabledFn(process.cwd())) {
|
package/lib/commands/status.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const { getCurrentBranch, getUncommittedCount, getLastThreeCommits, run } = require('../core/git');
|
|
1
|
+
const { detectRebaseState, getCurrentBranch, getUncommittedCount, getLastThreeCommits, run } = require('../core/git');
|
|
2
2
|
const { findTaskFile, getTaskStatus } = require('../tools/backlog');
|
|
3
3
|
const { findMissionDir, findCheckpoints, getFirstLine, inferSlug, missionBranchPrefix, missionBranchName } = require('../core/mission-utils');
|
|
4
4
|
const { WORKFLOW_AGENT_NAMES, eligibleAgentsForStep, readAgentConfigOrExit, workflowLauncherStatus } = require('../agents/agents');
|
|
@@ -77,6 +77,7 @@ function findStaleMissionWorktrees({
|
|
|
77
77
|
return {
|
|
78
78
|
slug,
|
|
79
79
|
path: entry.path,
|
|
80
|
+
branch: branchRef,
|
|
80
81
|
taskStatus: taskStatus || 'missing',
|
|
81
82
|
cleanupCommand: taskStatus === 'done'
|
|
82
83
|
? `scripts/cleanup-mission-worktree.sh ${slug}`
|
|
@@ -86,6 +87,19 @@ function findStaleMissionWorktrees({
|
|
|
86
87
|
.filter(Boolean);
|
|
87
88
|
}
|
|
88
89
|
|
|
90
|
+
function formatWorktreeBranch(ref) {
|
|
91
|
+
if (!ref) return '(detached HEAD)';
|
|
92
|
+
return ref.replace(/^refs\/heads\//, '');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function logRebaseDiagnostics(log, label, rebaseState) {
|
|
96
|
+
const detachedText = rebaseState.detached ? 'detached HEAD, ' : '';
|
|
97
|
+
log(`${label}: ${detachedText}${rebaseState.unmergedFiles.length} unmerged file(s)`);
|
|
98
|
+
rebaseState.unmergedFiles.forEach(file => {
|
|
99
|
+
log(` - ${file}`);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
89
103
|
function status(args, options = {}) {
|
|
90
104
|
const exit = options.exit || process.exit;
|
|
91
105
|
const log = options.log || fmt.log.plain;
|
|
@@ -104,6 +118,7 @@ function status(args, options = {}) {
|
|
|
104
118
|
const workflowLauncherStatusFn = options.workflowLauncherStatusFn || workflowLauncherStatus;
|
|
105
119
|
const getLastThreeCommitsFn = options.getLastThreeCommitsFn || getLastThreeCommits;
|
|
106
120
|
const getUncommittedCountFn = options.getUncommittedCountFn || getUncommittedCount;
|
|
121
|
+
const detectRebaseStateFn = options.detectRebaseStateFn || detectRebaseState;
|
|
107
122
|
|
|
108
123
|
const explicitSlug = args[0];
|
|
109
124
|
const slug = inferSlugFn(explicitSlug);
|
|
@@ -112,6 +127,15 @@ function status(args, options = {}) {
|
|
|
112
127
|
log(`Branch: ${fmt.branch(getCurrentBranchFn())}`);
|
|
113
128
|
log(`Worktree: ${fmt.path(process.cwd())}`);
|
|
114
129
|
|
|
130
|
+
try {
|
|
131
|
+
const rebaseState = detectRebaseStateFn(process.cwd());
|
|
132
|
+
if (rebaseState.inProgress && rebaseState.detached) {
|
|
133
|
+
logRebaseDiagnostics(log, 'Detached HEAD: rebase in progress', rebaseState);
|
|
134
|
+
}
|
|
135
|
+
} catch (_) {
|
|
136
|
+
// Ignore transient worktree/git-state failures in status output.
|
|
137
|
+
}
|
|
138
|
+
|
|
115
139
|
if (slug) {
|
|
116
140
|
const taskFile = findTaskFileFn(slug);
|
|
117
141
|
const taskStatus = getTaskStatusFn(taskFile);
|
|
@@ -146,6 +170,14 @@ function status(args, options = {}) {
|
|
|
146
170
|
const staleWorktrees = findStaleMissionWorktreesFn();
|
|
147
171
|
staleWorktrees.forEach(entry => {
|
|
148
172
|
log(`Stale worktree: ${fmt.path(entry.path)} (task: ${entry.taskStatus})`);
|
|
173
|
+
try {
|
|
174
|
+
const rebaseState = detectRebaseStateFn(entry.path);
|
|
175
|
+
if (rebaseState.inProgress) {
|
|
176
|
+
logRebaseDiagnostics(log, `Rebase in progress on ${formatWorktreeBranch(entry.branch)}`, rebaseState);
|
|
177
|
+
}
|
|
178
|
+
} catch (_) {
|
|
179
|
+
// Ignore stale worktrees that disappear during inspection.
|
|
180
|
+
}
|
|
149
181
|
log(` Cleanup: ${fmt.command(entry.cleanupCommand)}`);
|
|
150
182
|
});
|
|
151
183
|
}
|
package/lib/core/git.js
CHANGED
|
@@ -50,6 +50,51 @@ function getUncommittedCount(cwd = process.cwd()) {
|
|
|
50
50
|
return result.stdout.trim().split('\n').length;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
function parseUnmergedFiles(output = '') {
|
|
54
|
+
return Array.from(new Set(
|
|
55
|
+
output
|
|
56
|
+
.split('\n')
|
|
57
|
+
.map(line => line.trim())
|
|
58
|
+
.filter(Boolean)
|
|
59
|
+
.map(line => line.split('\t')[1])
|
|
60
|
+
.filter(Boolean)
|
|
61
|
+
));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function detectRebaseState(cwd = process.cwd(), { gitRunner = git, fsModule = require('fs'), pathModule = require('path') } = {}) {
|
|
65
|
+
const gitDirResult = gitRunner(['-C', cwd, 'rev-parse', '--git-dir']);
|
|
66
|
+
const resolvedGitDir = gitDirResult.status === 0
|
|
67
|
+
? gitDirResult.stdout.trim()
|
|
68
|
+
: '.git';
|
|
69
|
+
const gitDir = pathModule.isAbsolute(resolvedGitDir)
|
|
70
|
+
? resolvedGitDir
|
|
71
|
+
: pathModule.join(cwd, resolvedGitDir);
|
|
72
|
+
const rebaseMergeDir = pathModule.join(gitDir, 'rebase-merge');
|
|
73
|
+
const rebaseApplyDir = pathModule.join(gitDir, 'rebase-apply');
|
|
74
|
+
const rebaseDir = fsModule.existsSync(rebaseMergeDir)
|
|
75
|
+
? rebaseMergeDir
|
|
76
|
+
: (fsModule.existsSync(rebaseApplyDir) ? rebaseApplyDir : null);
|
|
77
|
+
|
|
78
|
+
const headResult = gitRunner(['-C', cwd, 'symbolic-ref', '--quiet', '--short', 'HEAD']);
|
|
79
|
+
const detached = headResult.status !== 0;
|
|
80
|
+
|
|
81
|
+
const showCurrentResult = gitRunner(['-C', cwd, 'rebase', '--show-current']);
|
|
82
|
+
const rebaseHead = showCurrentResult.status === 0 ? showCurrentResult.stdout.trim() : '';
|
|
83
|
+
|
|
84
|
+
const unmergedResult = gitRunner(['-C', cwd, 'ls-files', '-u']);
|
|
85
|
+
const unmergedFiles = unmergedResult.status === 0 ? parseUnmergedFiles(unmergedResult.stdout) : [];
|
|
86
|
+
|
|
87
|
+
const inProgress = Boolean(rebaseDir || rebaseHead || unmergedFiles.length > 0);
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
inProgress,
|
|
91
|
+
rebaseHead,
|
|
92
|
+
detached,
|
|
93
|
+
unmergedFiles,
|
|
94
|
+
rebaseDir
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
53
98
|
function getLastCommit() {
|
|
54
99
|
const result = git(['log', '-1', '--format=%H|%ad|%s']);
|
|
55
100
|
const [sha, date, subject] = result.stdout.trim().split('|');
|
|
@@ -68,6 +113,7 @@ module.exports = {
|
|
|
68
113
|
getWorktreeStatus,
|
|
69
114
|
isDirty,
|
|
70
115
|
getUncommittedCount,
|
|
116
|
+
detectRebaseState,
|
|
71
117
|
getLastCommit,
|
|
72
118
|
getLastThreeCommits
|
|
73
119
|
};
|
|
@@ -593,10 +593,19 @@ async function startReviewLoop(slug, opts = {}) {
|
|
|
593
593
|
|
|
594
594
|
log(fmt.status('INFO', `Selected reviewer: ${reviewer} (${reviewerSource})`));
|
|
595
595
|
|
|
596
|
-
// Build the canonical ReviewState instance
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
596
|
+
// Build the canonical ReviewState instance. When persisted state exists we
|
|
597
|
+
// resume from it — preserving round, startedAt, phase, disposition, retry
|
|
598
|
+
// counts, and metadata — and only overwrite the identities with the ones
|
|
599
|
+
// selected for this launch (which may differ after a reviewer/implementer
|
|
600
|
+
// fallback). Constructing fresh would silently reset all persisted progress.
|
|
601
|
+
let state;
|
|
602
|
+
if (persisted) {
|
|
603
|
+
state = ReviewState.from(slug, persisted);
|
|
604
|
+
state.reviewer = reviewer;
|
|
605
|
+
state.implementer = implementer;
|
|
606
|
+
} else {
|
|
607
|
+
state = new ReviewState(slug, { reviewer, implementer });
|
|
608
|
+
}
|
|
600
609
|
persistNormalizedPhaseRepair(slug, state, worktree, { log, writeReviewStateFn });
|
|
601
610
|
|
|
602
611
|
if (persisted && state.round > 1 && !dryRun) {
|
package/lib/review/review.js
CHANGED
|
@@ -70,6 +70,7 @@ module.exports.consumeReviewerArtifacts = consumeReviewerArtifacts;
|
|
|
70
70
|
module.exports.consumeImplementerArtifacts = consumeImplementerArtifacts;
|
|
71
71
|
|
|
72
72
|
// From review-loop
|
|
73
|
+
module.exports.recordStageStatsSafe = recordStageStatsSafe;
|
|
73
74
|
module.exports.maybeUpdateGraphifyBeforeReview = maybeUpdateGraphifyBeforeReview;
|
|
74
75
|
module.exports.commitSafeMissionArtifacts = commitSafeMissionArtifacts;
|
|
75
76
|
module.exports.rebaseBeforeReviewRound = rebaseBeforeReviewRound;
|
package/package.json
CHANGED
package/prompts/draft.md
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
|
-
Mode: draft.
|
|
1
|
+
Mode: draft. Do not implement the mission — produce the mission contract document only.
|
|
2
2
|
Mission slug: {{slug}}
|
|
3
3
|
Mission path: {{missionPath}}
|
|
4
4
|
Backlog task: {{taskPath}}
|
|
5
5
|
|
|
6
6
|
The harness has already created the mission branch, worktree, scaffolded `{{missionPath}}`, and ensured the backlog task exists. Your job is to read the user's intent from `{{taskPath}}` and fill `{{missionPath}}` with a real mission contract.
|
|
7
7
|
|
|
8
|
+
Allowed actions:
|
|
9
|
+
- read files (backlog task, MISSION.md scaffold, graphify index if present)
|
|
10
|
+
- write/edit files (MISSION.md, backlog task labels)
|
|
11
|
+
- run graphify queries and updates
|
|
12
|
+
- run `{{verifyCmd}}` to verify the draft
|
|
13
|
+
|
|
14
|
+
Forbidden actions:
|
|
15
|
+
- implement any feature or fix described in the mission
|
|
16
|
+
- modify source code outside MISSION.md and the backlog task file
|
|
17
|
+
- run tests beyond the single `{{verifyCmd}}` gate
|
|
18
|
+
- start a review, execute, or integrate phase
|
|
19
|
+
|
|
8
20
|
Drafting requirements:
|
|
9
21
|
- fill every scaffolded section in `{{missionPath}}` with concrete, non-generic content (no placeholders, no "TBD")
|
|
10
22
|
- include a Goal, Why now, Scope, Out of scope, Success criteria, Risks/assumptions, Checkpoints, Gates, Restricted areas, and Stop rules
|
|
@@ -16,5 +16,21 @@ Requirements:
|
|
|
16
16
|
- write the formal review message to `{{artifactDir}}/{{slug}}-review-outcome.md`
|
|
17
17
|
- write the formal review verdict (`approve` or `request-changes`) to `{{artifactDir}}/{{slug}}-review-verdict.txt`; `comment` is not a valid outcome — if you have findings but the criteria pass, use `request-changes`
|
|
18
18
|
- do not post to Forgejo directly; `px` will consume the artifact files, publish them, and advance review state
|
|
19
|
-
- do not edit repo files
|
|
20
19
|
- stop once the artifact files are written
|
|
20
|
+
|
|
21
|
+
Separation of duties — you are the reviewer, not the implementer. Stay in review-only mode:
|
|
22
|
+
|
|
23
|
+
You MUST NOT:
|
|
24
|
+
- edit, create, or delete any repo source, config, test, or doc file to fix a problem — report it as a finding instead of touching the file
|
|
25
|
+
- fix bugs, refactor, complete unfinished work, or otherwise "improve" the diff under review; reviewing is not implementing
|
|
26
|
+
- run branch-history operations: no rebase, squash, amend, `git reset`, force-push, or branch deletion
|
|
27
|
+
- run merge or PR operations: no merge, push, opening/closing/merging PRs, or posting to Forgejo directly
|
|
28
|
+
- mutate workflow state: do not write or edit checkpoint documents, mission artifacts, act-on-review files, or any review-loop/review-state files
|
|
29
|
+
|
|
30
|
+
You MUST:
|
|
31
|
+
- review the full mission diff, confirm the final checkpoint's goal-check evidence, and write the findings, outcome, and verdict artifacts
|
|
32
|
+
- report any inconsistency (workflow state, prompts, PR history) as a finding rather than resolving it yourself
|
|
33
|
+
|
|
34
|
+
You MAY (these writes are the sole exceptions to "no repo edits"):
|
|
35
|
+
- write to the artifact directory `{{artifactDir}}` (findings, outcome, verdict)
|
|
36
|
+
- create temporary diagnostic files under `/tmp`
|
package/prompts/review.md
CHANGED
|
@@ -1,17 +1,104 @@
|
|
|
1
|
-
|
|
1
|
+
<<<<<<< Updated upstream
|
|
2
|
+
Mode: review. No code changes, commits, repo-state edits, or implementer behavior.
|
|
3
|
+
=======
|
|
4
|
+
Mode: review. No code changes, , commits, repo-state edits, or implementer behavior.
|
|
5
|
+
>>>>>>> Stashed changes
|
|
2
6
|
Mission: {{missionPath}}
|
|
3
7
|
Attempt: {{attempt}}. Focus: {{focus}}.
|
|
4
|
-
|
|
5
8
|
Entrypoint: {{review_entrypoint}}
|
|
6
9
|
|
|
10
|
+
Load before reviewing:
|
|
11
|
+
- `AGENTS.md`
|
|
12
|
+
- locked mission at `{{missionPath}}`
|
|
13
|
+
- final checkpoint document, if present
|
|
14
|
+
- diff: `git diff {{primaryBranch}}..HEAD`
|
|
15
|
+
|
|
7
16
|
Minimum loop contract:
|
|
8
17
|
- Load the locked mission at `{{missionPath}}` and `AGENTS.md` before reviewing.
|
|
9
18
|
- Run `px review {{slug}} --verify`.
|
|
10
|
-
- Review
|
|
19
|
+
- Review as an independent senior engineer. Approve only if the mission is satisfied, verification is credible for the risk level, and the diff is safe to integrate.
|
|
20
|
+
- Request changes for actionable issues introduced or materially worsened by this mission.
|
|
11
21
|
- Confirm the final checkpoint document in the mission directory contains a Goal Check table citing real evidence (file:line, test names).
|
|
22
|
+
Check:
|
|
23
|
+
- mission scope and acceptance criteria
|
|
24
|
+
- final checkpoint claims vs actual diff
|
|
25
|
+
- correctness and regressions
|
|
26
|
+
- tests / gates / verification evidence
|
|
27
|
+
- security and unsafe operations
|
|
28
|
+
- integration with existing code, config, APIs, schemas, docs, or workflows
|
|
29
|
+
- maintainability issues that materially affect future work
|
|
30
|
+
|
|
12
31
|
- Write findings to `{{artifactDir}}/{{slug}}-review-findings.md`.
|
|
13
32
|
- Write the formal outcome to `{{artifactDir}}/{{slug}}-review-outcome.md` and the legacy verdict (`approve` | `request-changes`) to `{{artifactDir}}/{{slug}}-review-verdict.txt`. `comment` is not a valid outcome: if you have findings but the criteria pass, use `request-changes`.
|
|
14
|
-
- Do not
|
|
15
|
-
|
|
16
|
-
-
|
|
17
|
-
|
|
33
|
+
- Do not call px directly, the workflow will do that for you
|
|
34
|
+
<<<<<<< Updated upstream
|
|
35
|
+
- Do not post to Forgejo directly; `px` will consume the artifact files, publish them, and advance review state
|
|
36
|
+
|
|
37
|
+
Separation of duties — you are the reviewer, not the implementer. Stay in review-only mode:
|
|
38
|
+
|
|
39
|
+
You MUST NOT:
|
|
40
|
+
- Edit, create, or delete any repo source, config, test, or doc file to fix a problem — report it as a finding instead of touching the file
|
|
41
|
+
- Fix bugs, refactor, complete unfinished work, or otherwise "improve" the diff under review; reviewing is not implementing
|
|
42
|
+
- Run branch-history operations: no rebase, squash, amend, `git reset`, force-push, or branch deletion
|
|
43
|
+
- Run merge or PR operations: no merge, push, opening/closing/merging PRs, or posting to Forgejo directly
|
|
44
|
+
- Mutate workflow state: do not write or edit checkpoint documents, mission artifacts, act-on-review files, or any review-loop/review-state files
|
|
45
|
+
|
|
46
|
+
You MAY (these writes are the sole exceptions to "no repo edits"):
|
|
47
|
+
- Write to the artifact directory `{{artifactDir}}` (findings, outcome, verdict)
|
|
48
|
+
- Create temporary diagnostic files under `/tmp`.
|
|
49
|
+
=======
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
Do not report:
|
|
53
|
+
- unrelated pre-existing issues
|
|
54
|
+
- speculative risks
|
|
55
|
+
- style preferences
|
|
56
|
+
- alternative designs that are merely different
|
|
57
|
+
- improvements outside mission scope
|
|
58
|
+
|
|
59
|
+
Findings must cite specific files/lines where possible and explain impact plus suggested fix.
|
|
60
|
+
|
|
61
|
+
Write:
|
|
62
|
+
|
|
63
|
+
1. `{{artifactDir}}/{{slug}}-review-findings.md`
|
|
64
|
+
|
|
65
|
+
Include either:
|
|
66
|
+
|
|
67
|
+
- actionable findings with file references, impact, and suggested fix
|
|
68
|
+
|
|
69
|
+
or:
|
|
70
|
+
|
|
71
|
+
- `No actionable findings`
|
|
72
|
+
- explicit evidence checked:
|
|
73
|
+
- mission reviewed
|
|
74
|
+
- diff reviewed
|
|
75
|
+
- final checkpoint reviewed or not present
|
|
76
|
+
- `px review {{slug}} --verify` result or limitation
|
|
77
|
+
- main changed areas inspected
|
|
78
|
+
|
|
79
|
+
2. `{{artifactDir}}/{{slug}}-review-outcome.md`
|
|
80
|
+
|
|
81
|
+
Include:
|
|
82
|
+
|
|
83
|
+
- verdict: `approve` or `request-changes`
|
|
84
|
+
- short rationale
|
|
85
|
+
- required changes, or `None`
|
|
86
|
+
- verification result or limitation
|
|
87
|
+
- non-blocking notes, if any
|
|
88
|
+
|
|
89
|
+
3. `{{artifactDir}}/{{slug}}-review-verdict.txt`
|
|
90
|
+
|
|
91
|
+
Write exactly one word:
|
|
92
|
+
|
|
93
|
+
`approve`
|
|
94
|
+
|
|
95
|
+
or
|
|
96
|
+
|
|
97
|
+
`request-changes`
|
|
98
|
+
|
|
99
|
+
Rules:
|
|
100
|
+
- `comment` is not valid.
|
|
101
|
+
- If there are actionable findings that should be fixed before integration, verdict is `request-changes`.
|
|
102
|
+
- If workflow state, prompt state, PR history, checkpoint evidence, or artifacts are inconsistent, report the inconsistency as a finding instead of fixing it.
|
|
103
|
+
- Do not post to Forgejo directly; Parallix publishes the artifacts.
|
|
104
|
+
>>>>>>> Stashed changes
|