@magnusekdahl/parallix 1.2.1 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/index.js +190 -185
  2. package/lib/agents/agents.js +34 -4
  3. package/lib/agents/agents.ts +40 -5
  4. package/lib/commands/active.js +502 -579
  5. package/lib/commands/active.ts +665 -0
  6. package/lib/commands/checkpoint.js +98 -74
  7. package/lib/commands/checkpoint.ts +79 -0
  8. package/lib/commands/config.js +61 -31
  9. package/lib/commands/config.ts +47 -0
  10. package/lib/commands/coverage-gate.js +271 -246
  11. package/lib/commands/coverage-gate.ts +362 -0
  12. package/lib/commands/diff.js +140 -119
  13. package/lib/commands/diff.ts +125 -0
  14. package/lib/commands/draft.js +770 -923
  15. package/lib/commands/draft.ts +1021 -0
  16. package/lib/commands/handoff.js +588 -608
  17. package/lib/commands/handoff.ts +694 -0
  18. package/lib/commands/integrate.js +1331 -1468
  19. package/lib/commands/integrate.ts +1688 -0
  20. package/lib/commands/mission-start.js +279 -237
  21. package/lib/commands/mission-start.ts +263 -0
  22. package/lib/commands/rebase.js +606 -582
  23. package/lib/commands/rebase.ts +635 -0
  24. package/lib/commands/repair-handoff.js +181 -179
  25. package/lib/commands/repair-handoff.ts +228 -0
  26. package/lib/commands/resolve-conflict.js +121 -102
  27. package/lib/commands/resolve-conflict.ts +112 -0
  28. package/lib/commands/review.js +3 -4
  29. package/lib/commands/review.ts +14 -0
  30. package/lib/commands/setup-review.js +4 -5
  31. package/lib/commands/setup-review.ts +14 -0
  32. package/lib/commands/setup.js +3 -3
  33. package/lib/commands/setup.ts +2 -0
  34. package/lib/commands/stats-backfill.js +394 -359
  35. package/lib/commands/stats-backfill.ts +418 -0
  36. package/lib/commands/stats.js +1424 -1665
  37. package/lib/commands/stats.ts +2186 -0
  38. package/lib/commands/status.js +219 -200
  39. package/lib/commands/status.ts +219 -0
  40. package/lib/commands/verify.js +3 -1
  41. package/lib/commands/verify.ts +2 -0
  42. package/lib/core/nels.js +112 -127
  43. package/lib/core/nels.ts +189 -0
  44. package/lib/core/subagent-limit.js +15 -16
  45. package/lib/core/subagent-limit.ts +25 -0
  46. package/lib/index.js +142 -59
  47. package/lib/index.ts +134 -0
  48. package/lib/review/rebase.js +142 -145
  49. package/lib/review/rebase.ts +188 -0
  50. package/lib/review/review-adapter.js +99 -87
  51. package/lib/review/review-adapter.ts +198 -0
  52. package/lib/review/review-artifacts.js +486 -629
  53. package/lib/review/review-artifacts.ts +622 -0
  54. package/lib/review/review-commands.js +1220 -1373
  55. package/lib/review/review-commands.ts +1579 -0
  56. package/lib/review/review-events.js +544 -886
  57. package/lib/review/review-events.ts +954 -0
  58. package/lib/review/review-loop.js +945 -1020
  59. package/lib/review/review-loop.ts +1174 -0
  60. package/lib/review/review-polling.js +86 -115
  61. package/lib/review/review-polling.ts +194 -0
  62. package/lib/review/review-prompts.js +154 -189
  63. package/lib/review/review-prompts.ts +182 -0
  64. package/lib/review/review-state.js +246 -225
  65. package/lib/review/review-state.ts +353 -0
  66. package/lib/review/review.js +63 -84
  67. package/lib/review/review.ts +138 -0
  68. package/lib/tools/backlog.js +694 -680
  69. package/lib/tools/backlog.ts +835 -0
  70. package/lib/tools/forgejo.js +1270 -1405
  71. package/lib/tools/forgejo.ts +1793 -0
  72. package/lib/tools/gatekeeper.js +106 -96
  73. package/lib/tools/gatekeeper.ts +124 -0
  74. package/lib/tools/redgreen.js +200 -181
  75. package/lib/tools/redgreen.ts +221 -0
  76. package/lib/tools/sessions.js +58 -52
  77. package/lib/tools/sessions.ts +81 -0
  78. package/lib/tools/setup-review.js +902 -975
  79. package/lib/tools/setup-review.ts +1152 -0
  80. package/package.json +4 -4
  81. package/px.js +226 -208
@@ -1,59 +1,99 @@
1
- const fs = require('fs');
2
- const os = require('os');
3
- const path = require('path');
4
- const fmt = require('../core/fmt');
5
- const { findMissionDir, findCheckpoints, missionBranchName, resolveMissionBaseBranch } = require('../core/mission-utils');
6
- const { git, run } = require('../core/git');
7
-
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.findReproTestPath = findReproTestPath;
40
+ exports.resolveMissionParentCommit = resolveMissionParentCommit;
41
+ exports.runReproAtRef = runReproAtRef;
42
+ exports.verifyRedGreenProof = verifyRedGreenProof;
43
+ const fs_1 = __importDefault(require("fs"));
44
+ const os_1 = __importDefault(require("os"));
45
+ const path_1 = __importDefault(require("path"));
46
+ const fmt = __importStar(require("../core/fmt.js"));
47
+ const mission_utils_js_1 = require("../core/mission-utils.js");
48
+ const git_js_1 = require("../core/git.js");
8
49
  const REPRO_TEST_MARKER = /^Reproduction-Test:\s*(.+?)\s*$/m;
9
-
10
50
  /**
11
51
  * Locate the reproduction test path from a `Reproduction-Test: <path>` line in
12
52
  * MISSION.md or any checkpoint document. Returns null when none is declared.
13
53
  */
14
54
  function findReproTestPath(slug, rootDir, options = {}) {
15
- const {
16
- findMissionDirFn = findMissionDir,
17
- findCheckpointsFn = findCheckpoints
18
- } = options;
19
-
20
- const missionDir = findMissionDirFn(slug, rootDir);
21
- if (!missionDir) {return null;}
22
-
23
- const candidates = [path.join(missionDir, 'MISSION.md')];
24
- try {
25
- candidates.push(...findCheckpointsFn(missionDir));
26
- } catch (_) {
27
- // missionDir may not be readable; MISSION.md alone is enough to try
28
- }
29
-
30
- for (const file of candidates) {
31
- if (!file || !fs.existsSync(file)) {continue;}
32
- const match = fs.readFileSync(file, 'utf8').match(REPRO_TEST_MARKER);
33
- if (match) {return match[1].trim();}
34
- }
35
- return null;
55
+ const { findMissionDirFn = mission_utils_js_1.findMissionDir, findCheckpointsFn = mission_utils_js_1.findCheckpoints } = options;
56
+ const missionDir = findMissionDirFn(slug, rootDir);
57
+ if (!missionDir) {
58
+ return null;
59
+ }
60
+ const candidates = [path_1.default.join(missionDir, 'MISSION.md')];
61
+ try {
62
+ candidates.push(...findCheckpointsFn(missionDir));
63
+ }
64
+ catch (_) {
65
+ // missionDir may not be readable; MISSION.md alone is enough to try
66
+ }
67
+ for (const file of candidates) {
68
+ if (!file || !fs_1.default.existsSync(file)) {
69
+ continue;
70
+ }
71
+ const match = fs_1.default.readFileSync(file, 'utf8').match(REPRO_TEST_MARKER);
72
+ if (match) {
73
+ return match[1].trim();
74
+ }
75
+ }
76
+ return null;
36
77
  }
37
-
38
78
  /**
39
79
  * Resolve the commit where the mission branch diverged from its base branch.
40
80
  * Uses merge-base so a rebased/advanced base still yields the true fork point.
41
81
  */
42
82
  function resolveMissionParentCommit(slug, rootDir, options = {}) {
43
- const { gitFn = null } = options;
44
- const runner = gitFn || git;
45
- const branch = options.branch || missionBranchName(slug, rootDir);
46
- let base;
47
- try {
48
- base = resolveMissionBaseBranch(slug, rootDir, { gitFn: runner });
49
- } catch (_) {
50
- return null;
51
- }
52
- const result = runner(['-C', rootDir, 'merge-base', base, branch]);
53
- const sha = ((result && result.stdout) || '').trim();
54
- return result && result.status === 0 && sha ? sha : null;
83
+ const { gitFn = null } = options;
84
+ const runner = gitFn || git_js_1.git;
85
+ const branch = options.branch || (0, mission_utils_js_1.missionBranchName)(slug, rootDir);
86
+ let base;
87
+ try {
88
+ base = (0, mission_utils_js_1.resolveMissionBaseBranch)(slug, rootDir, { gitFn: runner });
89
+ }
90
+ catch (_) {
91
+ return null;
92
+ }
93
+ const result = runner(['-C', rootDir, 'merge-base', base, branch]);
94
+ const sha = ((result && result.stdout) || '').trim();
95
+ return result && result.status === 0 && sha ? sha : null;
55
96
  }
56
-
57
97
  /**
58
98
  * Run a test against the source at `ref`, overlaying the test file from
59
99
  * `headRef` so the test exists even at the parent commit (where it was not
@@ -61,40 +101,38 @@ function resolveMissionParentCommit(slug, rootDir, options = {}) {
61
101
  * worktree is never mutated.
62
102
  */
63
103
  function runReproAtRef(ref, testPath, options = {}) {
64
- const {
65
- rootDir = process.cwd(),
66
- headRef = 'HEAD',
67
- gitFn = null,
68
- runCommandFn = null
69
- } = options;
70
- const gitRunner = gitFn || git;
71
- const cmdRunner = runCommandFn || run;
72
-
73
- const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'redgreen-'));
74
- const worktreePath = path.join(tmp, 'wt');
75
- try {
76
- const add = gitRunner(['-C', rootDir, 'worktree', 'add', '--detach', worktreePath, ref]);
77
- if (!add || add.status !== 0) {
78
- return { status: null, skipped: true, reason: 'worktree-add-failed' };
79
- }
80
-
81
- // Overlay the reproduction test from the branch HEAD so the parent ref runs
82
- // the same test against pre-fix source.
83
- const show = gitRunner(['-C', rootDir, 'show', `${headRef}:${testPath}`]);
84
- if (show && show.status === 0) {
85
- const dest = path.join(worktreePath, testPath);
86
- fs.mkdirSync(path.dirname(dest), { recursive: true });
87
- fs.writeFileSync(dest, show.stdout);
88
- }
89
-
90
- const result = cmdRunner('node', ['--test', testPath], { cwd: worktreePath });
91
- return { status: result && typeof result.status === 'number' ? result.status : 1 };
92
- } finally {
93
- try { gitRunner(['-C', rootDir, 'worktree', 'remove', '--force', worktreePath]); } catch (_) {}
94
- try { fs.rmSync(tmp, { recursive: true, force: true }); } catch (_) {}
95
- }
104
+ const { rootDir = process.cwd(), headRef = 'HEAD', gitFn = null, runCommandFn = null } = options;
105
+ const gitRunner = gitFn || git_js_1.git;
106
+ const cmdRunner = runCommandFn || git_js_1.run;
107
+ const tmp = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'redgreen-'));
108
+ const worktreePath = path_1.default.join(tmp, 'wt');
109
+ try {
110
+ const add = gitRunner(['-C', rootDir, 'worktree', 'add', '--detach', worktreePath, ref]);
111
+ if (!add || add.status !== 0) {
112
+ return { status: null, skipped: true, reason: 'worktree-add-failed' };
113
+ }
114
+ // Overlay the reproduction test from the branch HEAD so the parent ref runs
115
+ // the same test against pre-fix source.
116
+ const show = gitRunner(['-C', rootDir, 'show', `${headRef}:${testPath}`]);
117
+ if (show && show.status === 0) {
118
+ const dest = path_1.default.join(worktreePath, testPath);
119
+ fs_1.default.mkdirSync(path_1.default.dirname(dest), { recursive: true });
120
+ fs_1.default.writeFileSync(dest, show.stdout);
121
+ }
122
+ const result = cmdRunner('node', ['--test', testPath], { cwd: worktreePath });
123
+ return { status: result && typeof result.status === 'number' ? result.status : 1 };
124
+ }
125
+ finally {
126
+ try {
127
+ gitRunner(['-C', rootDir, 'worktree', 'remove', '--force', worktreePath]);
128
+ }
129
+ catch (_) { }
130
+ try {
131
+ fs_1.default.rmSync(tmp, { recursive: true, force: true });
132
+ }
133
+ catch (_) { }
134
+ }
96
135
  }
97
-
98
136
  /**
99
137
  * Verify the red→green proof for a mission that declares a reproduction test.
100
138
  * Returns:
@@ -103,118 +141,99 @@ function runReproAtRef(ref, testPath, options = {}) {
103
141
  * { ok: false, ... , error } — repro missing, passed at parent, or failed at HEAD
104
142
  */
105
143
  function verifyRedGreenProof(slug, options = {}) {
106
- const {
107
- rootDir = process.cwd(),
108
- log = fmt.log.plain,
109
- headRef = 'HEAD',
110
- findReproTestPathFn = findReproTestPath,
111
- resolveMissionParentCommitFn = resolveMissionParentCommit,
112
- runReproAtRefFn = runReproAtRef,
113
- branch: branchOverride
114
- } = options;
115
- const branch = branchOverride || missionBranchName(slug, rootDir);
116
-
117
- // Strip injected-function keys from options before passing to child calls
118
- const childOpts = { ...options };
119
- delete childOpts.findReproTestPathFn;
120
- delete childOpts.resolveMissionParentCommitFn;
121
- delete childOpts.runReproAtRefFn;
122
- delete childOpts.branch;
123
-
124
- // 1. Locate the reproduction test declared by the mission.
125
- const testPath = findReproTestPathFn(slug, rootDir, childOpts);
126
- if (!testPath) {
127
- const errMsg = `Mission ${slug} declares no \`Reproduction-Test:\` line in MISSION.md or any checkpoint document.`;
128
- log(fmt.status('FAIL', errMsg));
129
- return { ok: false, skipped: false, reason: 'repro-not-declared', error: errMsg };
130
- }
131
-
132
- // 2. Resolve the parent commit (fork point) of the mission branch.
133
- const parentCommit = resolveMissionParentCommitFn(slug, rootDir, { branch, ...childOpts });
134
- if (!parentCommit) {
135
- const errMsg = `Could not resolve the parent commit for mission ${slug} (ambiguous or missing merge base).`;
136
- log(fmt.status('FAIL', errMsg));
137
- return { ok: false, skipped: false, reason: 'parent-unresolved', error: errMsg };
138
- }
139
-
140
- // 3. Red: the reproduction test must FAIL at the parent commit.
141
- const red = runReproAtRefFn(parentCommit, testPath, { rootDir, headRef, ...childOpts });
142
- if (red && red.skipped) {
143
- log(fmt.status('WARN', `Red→green gate: no usable test runner for ${slug} (${red.reason || 'unknown'}); skipping.`));
144
- return { ok: true, skipped: true, reason: red.reason || 'no-test-runner' };
145
- }
146
- if (red.status === 0) {
147
- const errMsg = `Reproduction test ${testPath} PASSED at parent commit ${parentCommit.slice(0, 12)} — it must fail (red) to lock the bug.`;
148
- log(fmt.status('FAIL', errMsg));
149
- return { ok: false, skipped: false, reason: 'not-red', error: errMsg, testPath, parentCommit };
150
- }
151
-
152
- // 4. Green: the reproduction test must PASS at HEAD.
153
- const green = runReproAtRefFn(headRef, testPath, { rootDir, headRef, ...childOpts });
154
- if (green && green.skipped) {
155
- log(fmt.status('WARN', `Red→green gate: could not run repro at HEAD for ${slug} (${green.reason || 'unknown'}); skipping.`));
156
- return { ok: true, skipped: true, reason: green.reason || 'no-test-runner' };
157
- }
158
- if (green.status !== 0) {
159
- const errMsg = `Reproduction test ${testPath} FAILED at HEAD — the fix must make it pass (green).`;
160
- log(fmt.status('FAIL', errMsg));
161
- return { ok: false, skipped: false, reason: 'not-green', error: errMsg, testPath, parentCommit };
162
- }
163
-
164
- log(fmt.status('INFO', `Red→green gate: ${slug} repro ${testPath} red at ${parentCommit.slice(0, 12)}, green at HEAD.`));
165
- return { ok: true, skipped: false, reason: 'red-green-verified', testPath, parentCommit };
144
+ const { rootDir = process.cwd(), log = fmt.log.plain, headRef = 'HEAD', findReproTestPathFn = findReproTestPath, resolveMissionParentCommitFn = resolveMissionParentCommit, runReproAtRefFn = runReproAtRef, branch: branchOverride } = options;
145
+ const branch = branchOverride || (0, mission_utils_js_1.missionBranchName)(slug, rootDir);
146
+ // Strip injected-function keys from options before passing to child calls
147
+ const childOpts = { ...options };
148
+ delete childOpts.findReproTestPathFn;
149
+ delete childOpts.resolveMissionParentCommitFn;
150
+ delete childOpts.runReproAtRefFn;
151
+ delete childOpts.branch;
152
+ // 1. Locate the reproduction test declared by the mission.
153
+ const testPath = findReproTestPathFn(slug, rootDir, childOpts);
154
+ if (!testPath) {
155
+ const errMsg = `Mission ${slug} declares no \`Reproduction-Test:\` line in MISSION.md or any checkpoint document.`;
156
+ log(fmt.status('FAIL', errMsg));
157
+ return { ok: false, skipped: false, reason: 'repro-not-declared', error: errMsg };
158
+ }
159
+ // 2. Resolve the parent commit (fork point) of the mission branch.
160
+ const parentCommit = resolveMissionParentCommitFn(slug, rootDir, { branch, ...childOpts });
161
+ if (!parentCommit) {
162
+ const errMsg = `Could not resolve the parent commit for mission ${slug} (ambiguous or missing merge base).`;
163
+ log(fmt.status('FAIL', errMsg));
164
+ return { ok: false, skipped: false, reason: 'parent-unresolved', error: errMsg };
165
+ }
166
+ // 3. Red: the reproduction test must FAIL at the parent commit.
167
+ const red = runReproAtRefFn(parentCommit, testPath, { rootDir, headRef, ...childOpts });
168
+ if (red && red.skipped) {
169
+ log(fmt.status('WARN', `Red→green gate: no usable test runner for ${slug} (${red.reason || 'unknown'}); skipping.`));
170
+ return { ok: true, skipped: true, reason: red.reason || 'no-test-runner' };
171
+ }
172
+ if (red.status === 0) {
173
+ const errMsg = `Reproduction test ${testPath} PASSED at parent commit ${parentCommit.slice(0, 12)} — it must fail (red) to lock the bug.`;
174
+ log(fmt.status('FAIL', errMsg));
175
+ return { ok: false, skipped: false, reason: 'not-red', error: errMsg, testPath, parentCommit };
176
+ }
177
+ // 4. Green: the reproduction test must PASS at HEAD.
178
+ const green = runReproAtRefFn(headRef, testPath, { rootDir, headRef, ...childOpts });
179
+ if (green && green.skipped) {
180
+ log(fmt.status('WARN', `Red→green gate: could not run repro at HEAD for ${slug} (${green.reason || 'unknown'}); skipping.`));
181
+ return { ok: true, skipped: true, reason: green.reason || 'no-test-runner' };
182
+ }
183
+ if (green.status !== 0) {
184
+ const errMsg = `Reproduction test ${testPath} FAILED at HEAD — the fix must make it pass (green).`;
185
+ log(fmt.status('FAIL', errMsg));
186
+ return { ok: false, skipped: false, reason: 'not-green', error: errMsg, testPath, parentCommit };
187
+ }
188
+ log(fmt.status('INFO', `Red→green gate: ${slug} repro ${testPath} red at ${parentCommit.slice(0, 12)}, green at HEAD.`));
189
+ return { ok: true, skipped: false, reason: 'red-green-verified', testPath, parentCommit };
166
190
  }
167
-
168
191
  /**
169
192
  * CLI entry point: px verify-repro --test <path> --slug <slug>
170
193
  * Standalone command for verifying red→green proof outside the handoff flow.
171
194
  */
172
195
  function main() {
173
- const args = process.argv.slice(2);
174
- let slug = null;
175
- let testPath = null;
176
- let rootDir = process.cwd();
177
-
178
- for (let i = 0; i < args.length; i++) {
179
- if (args[i] === '--slug' && args[i + 1]) { slug = args[++i]; }
180
- else if (args[i] === '--test' && args[i + 1]) { testPath = args[++i]; }
181
- else if (args[i] === '--root' && args[i + 1]) { rootDir = args[++i]; }
182
- }
183
-
184
- if (!slug) {
185
- process.stderr.write('Usage: node lib/tools/redgreen.js --slug <slug> --test <path>\n');
186
- process.exit(1);
187
- }
188
-
189
- if (!testPath) {
190
- // Try to find it from MISSION.md
191
- testPath = findReproTestPath(slug, rootDir);
196
+ const args = process.argv.slice(2);
197
+ let slug = null;
198
+ let testPath = null;
199
+ let rootDir = process.cwd();
200
+ for (let i = 0; i < args.length; i++) {
201
+ if (args[i] === '--slug' && args[i + 1]) {
202
+ slug = args[++i];
203
+ }
204
+ else if (args[i] === '--test' && args[i + 1]) {
205
+ testPath = args[++i];
206
+ }
207
+ else if (args[i] === '--root' && args[i + 1]) {
208
+ rootDir = args[++i];
209
+ }
210
+ }
211
+ if (!slug) {
212
+ process.stderr.write('Usage: node lib/tools/redgreen.js --slug <slug> --test <path>\n');
213
+ process.exit(1);
214
+ }
192
215
  if (!testPath) {
193
- process.stderr.write(`No --test specified and no Reproduction-Test: line found for ${slug}\n`);
194
- process.exit(1);
195
- }
196
- }
197
-
198
- const result = verifyRedGreenProof(slug, { rootDir, log: process.stdout.write.bind(process.stdout), testPath });
199
- if (!result.ok) {
200
- process.stderr.write(`FAIL: ${result.error}\n`);
201
- process.exit(1);
202
- }
203
- if (result.skipped) {
204
- process.stdout.write(`SKIP: ${result.reason}\n`);
216
+ // Try to find it from MISSION.md
217
+ testPath = findReproTestPath(slug, rootDir);
218
+ if (!testPath) {
219
+ process.stderr.write(`No --test specified and no Reproduction-Test: line found for ${slug}\n`);
220
+ process.exit(1);
221
+ }
222
+ }
223
+ const result = verifyRedGreenProof(slug, { rootDir, log: process.stdout.write.bind(process.stdout), testPath });
224
+ if (!result.ok) {
225
+ process.stderr.write(`FAIL: ${result.error}\n`);
226
+ process.exit(1);
227
+ }
228
+ if (result.skipped) {
229
+ process.stdout.write(`SKIP: ${result.reason}\n`);
230
+ process.exit(0);
231
+ }
232
+ process.stdout.write(`PASS: ${result.reason} (test: ${result.testPath})\n`);
205
233
  process.exit(0);
206
- }
207
- process.stdout.write(`PASS: ${result.reason} (test: ${result.testPath})\n`);
208
- process.exit(0);
209
234
  }
210
-
211
- if (require.main === module) {
212
- main();
235
+ // CLI entry point when run directly
236
+ if (typeof require !== 'undefined' && require.main === module) {
237
+ main();
213
238
  }
214
-
215
- module.exports = {
216
- findReproTestPath,
217
- resolveMissionParentCommit,
218
- runReproAtRef,
219
- verifyRedGreenProof
220
- };
239
+ ;
@@ -0,0 +1,221 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import * as fmt from '../core/fmt.js';
5
+ import { findMissionDir, findCheckpoints, missionBranchName, resolveMissionBaseBranch } from '../core/mission-utils.js';
6
+ import { git, run } from '../core/git.js';
7
+
8
+ const REPRO_TEST_MARKER = /^Reproduction-Test:\s*(.+?)\s*$/m;
9
+
10
+ /**
11
+ * Locate the reproduction test path from a `Reproduction-Test: <path>` line in
12
+ * MISSION.md or any checkpoint document. Returns null when none is declared.
13
+ */
14
+ function findReproTestPath(slug: string, rootDir: string, options: { findMissionDirFn?: Function, findCheckpointsFn?: Function } = {} as any) {
15
+ const {
16
+ findMissionDirFn = findMissionDir,
17
+ findCheckpointsFn = findCheckpoints
18
+ } = options;
19
+
20
+ const missionDir = findMissionDirFn(slug, rootDir);
21
+ if (!missionDir) {return null;}
22
+
23
+ const candidates = [path.join(missionDir, 'MISSION.md')];
24
+ try {
25
+ candidates.push(...findCheckpointsFn(missionDir));
26
+ } catch (_) {
27
+ // missionDir may not be readable; MISSION.md alone is enough to try
28
+ }
29
+
30
+ for (const file of candidates) {
31
+ if (!file || !fs.existsSync(file)) {continue;}
32
+ const match = fs.readFileSync(file, 'utf8').match(REPRO_TEST_MARKER);
33
+ if (match) {return match[1].trim();}
34
+ }
35
+ return null;
36
+ }
37
+
38
+ /**
39
+ * Resolve the commit where the mission branch diverged from its base branch.
40
+ * Uses merge-base so a rebased/advanced base still yields the true fork point.
41
+ */
42
+ function resolveMissionParentCommit(slug: string, rootDir: string, options: { gitFn?: Function | null, branch?: string } = {} as any) {
43
+ const { gitFn = null } = options;
44
+ const runner = gitFn || git;
45
+ const branch = options.branch || missionBranchName(slug, rootDir);
46
+ let base;
47
+ try {
48
+ base = resolveMissionBaseBranch(slug, rootDir, { gitFn: runner });
49
+ } catch (_) {
50
+ return null;
51
+ }
52
+ const result = runner(['-C', rootDir, 'merge-base', base, branch]);
53
+ const sha = ((result && result.stdout) || '').trim();
54
+ return result && result.status === 0 && sha ? sha : null;
55
+ }
56
+
57
+ /**
58
+ * Run a test against the source at `ref`, overlaying the test file from
59
+ * `headRef` so the test exists even at the parent commit (where it was not
60
+ * yet authored). Runs inside a throwaway detached worktree so the live
61
+ * worktree is never mutated.
62
+ */
63
+ function runReproAtRef(ref: string, testPath: string, options: { rootDir?: string, headRef?: string, gitFn?: Function | null, runCommandFn?: Function | null } = {} as any) {
64
+ const {
65
+ rootDir = process.cwd(),
66
+ headRef = 'HEAD',
67
+ gitFn = null,
68
+ runCommandFn = null
69
+ } = options;
70
+ const gitRunner = gitFn || git;
71
+ const cmdRunner = runCommandFn || run;
72
+
73
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'redgreen-'));
74
+ const worktreePath = path.join(tmp, 'wt');
75
+ try {
76
+ const add = gitRunner(['-C', rootDir, 'worktree', 'add', '--detach', worktreePath, ref]);
77
+ if (!add || add.status !== 0) {
78
+ return { status: null, skipped: true, reason: 'worktree-add-failed' };
79
+ }
80
+
81
+ // Overlay the reproduction test from the branch HEAD so the parent ref runs
82
+ // the same test against pre-fix source.
83
+ const show = gitRunner(['-C', rootDir, 'show', `${headRef}:${testPath}`]);
84
+ if (show && show.status === 0) {
85
+ const dest = path.join(worktreePath, testPath);
86
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
87
+ fs.writeFileSync(dest, show.stdout);
88
+ }
89
+
90
+ const result = cmdRunner('node', ['--test', testPath], { cwd: worktreePath });
91
+ return { status: result && typeof result.status === 'number' ? result.status : 1 };
92
+ } finally {
93
+ try { gitRunner(['-C', rootDir, 'worktree', 'remove', '--force', worktreePath]); } catch (_) {}
94
+ try { fs.rmSync(tmp, { recursive: true, force: true }); } catch (_) {}
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Verify the red→green proof for a mission that declares a reproduction test.
100
+ * Returns:
101
+ * { ok: true, skipped: true } — no usable test runner
102
+ * { ok: true, skipped: false } — repro failed at parent and passed at HEAD
103
+ * { ok: false, ... , error } — repro missing, passed at parent, or failed at HEAD
104
+ */
105
+ function verifyRedGreenProof(slug: string, options: { rootDir?: string, log?: Function, headRef?: string, findReproTestPathFn?: Function, resolveMissionParentCommitFn?: Function, runReproAtRefFn?: Function, branch?: string, testPath?: string } = {} as any) {
106
+ const {
107
+ rootDir = process.cwd(),
108
+ log = fmt.log.plain,
109
+ headRef = 'HEAD',
110
+ findReproTestPathFn = findReproTestPath,
111
+ resolveMissionParentCommitFn = resolveMissionParentCommit,
112
+ runReproAtRefFn = runReproAtRef,
113
+ branch: branchOverride
114
+ } = options;
115
+ const branch = branchOverride || missionBranchName(slug, rootDir);
116
+
117
+ // Strip injected-function keys from options before passing to child calls
118
+ const childOpts = { ...options };
119
+ delete childOpts.findReproTestPathFn;
120
+ delete childOpts.resolveMissionParentCommitFn;
121
+ delete childOpts.runReproAtRefFn;
122
+ delete childOpts.branch;
123
+
124
+ // 1. Locate the reproduction test declared by the mission.
125
+ const testPath = findReproTestPathFn(slug, rootDir, childOpts);
126
+ if (!testPath) {
127
+ const errMsg = `Mission ${slug} declares no \`Reproduction-Test:\` line in MISSION.md or any checkpoint document.`;
128
+ log(fmt.status('FAIL', errMsg));
129
+ return { ok: false, skipped: false, reason: 'repro-not-declared', error: errMsg };
130
+ }
131
+
132
+ // 2. Resolve the parent commit (fork point) of the mission branch.
133
+ const parentCommit = resolveMissionParentCommitFn(slug, rootDir, { branch, ...childOpts });
134
+ if (!parentCommit) {
135
+ const errMsg = `Could not resolve the parent commit for mission ${slug} (ambiguous or missing merge base).`;
136
+ log(fmt.status('FAIL', errMsg));
137
+ return { ok: false, skipped: false, reason: 'parent-unresolved', error: errMsg };
138
+ }
139
+
140
+ // 3. Red: the reproduction test must FAIL at the parent commit.
141
+ const red = runReproAtRefFn(parentCommit, testPath, { rootDir, headRef, ...childOpts });
142
+ if (red && red.skipped) {
143
+ log(fmt.status('WARN', `Red→green gate: no usable test runner for ${slug} (${red.reason || 'unknown'}); skipping.`));
144
+ return { ok: true, skipped: true, reason: red.reason || 'no-test-runner' };
145
+ }
146
+ if (red.status === 0) {
147
+ const errMsg = `Reproduction test ${testPath} PASSED at parent commit ${parentCommit.slice(0, 12)} — it must fail (red) to lock the bug.`;
148
+ log(fmt.status('FAIL', errMsg));
149
+ return { ok: false, skipped: false, reason: 'not-red', error: errMsg, testPath, parentCommit };
150
+ }
151
+
152
+ // 4. Green: the reproduction test must PASS at HEAD.
153
+ const green = runReproAtRefFn(headRef, testPath, { rootDir, headRef, ...childOpts });
154
+ if (green && green.skipped) {
155
+ log(fmt.status('WARN', `Red→green gate: could not run repro at HEAD for ${slug} (${green.reason || 'unknown'}); skipping.`));
156
+ return { ok: true, skipped: true, reason: green.reason || 'no-test-runner' };
157
+ }
158
+ if (green.status !== 0) {
159
+ const errMsg = `Reproduction test ${testPath} FAILED at HEAD — the fix must make it pass (green).`;
160
+ log(fmt.status('FAIL', errMsg));
161
+ return { ok: false, skipped: false, reason: 'not-green', error: errMsg, testPath, parentCommit };
162
+ }
163
+
164
+ log(fmt.status('INFO', `Red→green gate: ${slug} repro ${testPath} red at ${parentCommit.slice(0, 12)}, green at HEAD.`));
165
+ return { ok: true, skipped: false, reason: 'red-green-verified', testPath, parentCommit };
166
+ }
167
+
168
+ /**
169
+ * CLI entry point: px verify-repro --test <path> --slug <slug>
170
+ * Standalone command for verifying red→green proof outside the handoff flow.
171
+ */
172
+ function main() {
173
+ const args = process.argv.slice(2);
174
+ let slug = null;
175
+ let testPath = null;
176
+ let rootDir = process.cwd();
177
+
178
+ for (let i = 0; i < args.length; i++) {
179
+ if (args[i] === '--slug' && args[i + 1]) { slug = args[++i]; }
180
+ else if (args[i] === '--test' && args[i + 1]) { testPath = args[++i]; }
181
+ else if (args[i] === '--root' && args[i + 1]) { rootDir = args[++i]; }
182
+ }
183
+
184
+ if (!slug) {
185
+ process.stderr.write('Usage: node lib/tools/redgreen.js --slug <slug> --test <path>\n');
186
+ process.exit(1);
187
+ }
188
+
189
+ if (!testPath) {
190
+ // Try to find it from MISSION.md
191
+ testPath = findReproTestPath(slug, rootDir);
192
+ if (!testPath) {
193
+ process.stderr.write(`No --test specified and no Reproduction-Test: line found for ${slug}\n`);
194
+ process.exit(1);
195
+ }
196
+ }
197
+
198
+ const result = verifyRedGreenProof(slug, { rootDir, log: process.stdout.write.bind(process.stdout), testPath });
199
+ if (!result.ok) {
200
+ process.stderr.write(`FAIL: ${result.error}\n`);
201
+ process.exit(1);
202
+ }
203
+ if (result.skipped) {
204
+ process.stdout.write(`SKIP: ${result.reason}\n`);
205
+ process.exit(0);
206
+ }
207
+ process.stdout.write(`PASS: ${result.reason} (test: ${result.testPath})\n`);
208
+ process.exit(0);
209
+ }
210
+
211
+ // CLI entry point when run directly
212
+ if (typeof require !== 'undefined' && (require as { main?: unknown }).main === module) {
213
+ main();
214
+ }
215
+
216
+
217
+ export { findReproTestPath };
218
+ export { resolveMissionParentCommit };
219
+ export { runReproAtRef };
220
+ export { verifyRedGreenProof };
221
+ ;