@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 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); two families record honest zeros by design.
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
 
@@ -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', 'ignore'],
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
- if (!child || !child.stdout) {
84
- finish(null);
85
- return;
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.stdout.on('data', (chunk) => {
118
+ child.on('error', () => finish(null));
119
+
120
+ child.on('close', (code) => {
89
121
  if (settled) return;
90
- size += chunk.length;
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
- child.on('error', () => finish(null));
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');
@@ -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())) {
@@ -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 either from persisted state or fresh
597
- const state = persisted && persisted.reviewer === reviewer && persisted.implementer === implementer
598
- ? ReviewState.from(slug, persisted)
599
- : new ReviewState(slug, { reviewer, implementer });
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) {
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magnusekdahl/parallix",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "AI mission workflow toolkit with a px CLI — local-first, human-in-the-loop multi-agent development",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "private": false,
package/prompts/draft.md CHANGED
@@ -1,10 +1,22 @@
1
- Mode: draft. No execution.
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
- Mode: review. No code changes, no repo-state edits.
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 the diff with `git diff {{primaryBranch}}..HEAD`. Do a detailed review, since agents tend to miss stuff and just check off boxes in the checkpoints, which is not the intent here.
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 post to Forgejo directly; `px review {{slug}} --start` or `--submit` publishes the artifacts.
15
- - Do not edit repo files; do not switch into implementer behavior.
16
- - If workflow state, prompts, or PR history are inconsistent, report that inconsistency as a finding rather than fixing it.
17
- - Graphify-first: before reviewing, check if `graphify-out/graph.json` exists. If it does, run `graphify query "review {{slug}} for correctness and completeness"` to get a graph-based view of the mission scope before examining the diff.
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