@magnusekdahl/parallix 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/index.js +190 -185
  2. package/lib/agents/agents.js +737 -838
  3. package/lib/agents/agents.ts +930 -0
  4. package/lib/agents/claude-telemetry.js +149 -154
  5. package/lib/agents/claude-telemetry.ts +233 -0
  6. package/lib/agents/claude.js +121 -131
  7. package/lib/agents/claude.ts +156 -0
  8. package/lib/agents/codex-telemetry.js +176 -164
  9. package/lib/agents/codex-telemetry.ts +205 -0
  10. package/lib/agents/codex.js +168 -201
  11. package/lib/agents/codex.ts +236 -0
  12. package/lib/agents/limit-hit.js +216 -226
  13. package/lib/agents/limit-hit.ts +262 -0
  14. package/lib/agents/mistral-telemetry.js +7 -10
  15. package/lib/agents/mistral-telemetry.ts +44 -0
  16. package/lib/agents/mistral.js +40 -58
  17. package/lib/agents/mistral.ts +84 -0
  18. package/lib/agents/opencode-export.js +126 -127
  19. package/lib/agents/opencode-export.ts +160 -0
  20. package/lib/agents/opencode-telemetry.js +282 -261
  21. package/lib/agents/opencode-telemetry.ts +350 -0
  22. package/lib/agents/opencode.js +263 -305
  23. package/lib/agents/opencode.ts +370 -0
  24. package/lib/agents/stage-telemetry.js +23 -24
  25. package/lib/agents/stage-telemetry.ts +47 -0
  26. package/lib/commands/active.js +502 -579
  27. package/lib/commands/active.ts +665 -0
  28. package/lib/commands/checkpoint.js +98 -74
  29. package/lib/commands/checkpoint.ts +79 -0
  30. package/lib/commands/config.js +61 -31
  31. package/lib/commands/config.ts +47 -0
  32. package/lib/commands/coverage-gate.js +271 -246
  33. package/lib/commands/coverage-gate.ts +362 -0
  34. package/lib/commands/diff.js +140 -119
  35. package/lib/commands/diff.ts +125 -0
  36. package/lib/commands/draft.js +770 -923
  37. package/lib/commands/draft.ts +1021 -0
  38. package/lib/commands/handoff.js +588 -608
  39. package/lib/commands/handoff.ts +694 -0
  40. package/lib/commands/integrate.js +1332 -1463
  41. package/lib/commands/integrate.ts +1688 -0
  42. package/lib/commands/mission-start.js +279 -237
  43. package/lib/commands/mission-start.ts +263 -0
  44. package/lib/commands/rebase.js +606 -582
  45. package/lib/commands/rebase.ts +635 -0
  46. package/lib/commands/repair-handoff.js +181 -179
  47. package/lib/commands/repair-handoff.ts +228 -0
  48. package/lib/commands/resolve-conflict.js +121 -102
  49. package/lib/commands/resolve-conflict.ts +112 -0
  50. package/lib/commands/review.js +3 -4
  51. package/lib/commands/review.ts +14 -0
  52. package/lib/commands/setup-review.js +4 -5
  53. package/lib/commands/setup-review.ts +14 -0
  54. package/lib/commands/setup.js +3 -3
  55. package/lib/commands/setup.ts +2 -0
  56. package/lib/commands/stats-backfill.js +394 -359
  57. package/lib/commands/stats-backfill.ts +418 -0
  58. package/lib/commands/stats.js +1424 -1665
  59. package/lib/commands/stats.ts +2186 -0
  60. package/lib/commands/status.js +219 -200
  61. package/lib/commands/status.ts +219 -0
  62. package/lib/commands/verify.js +3 -1
  63. package/lib/commands/verify.ts +2 -0
  64. package/lib/core/fmt.js +140 -211
  65. package/lib/core/fmt.ts +189 -0
  66. package/lib/core/git.js +121 -102
  67. package/lib/core/git.ts +146 -0
  68. package/lib/core/gitignore.js +59 -106
  69. package/lib/core/gitignore.ts +103 -0
  70. package/lib/core/mission-utils.js +833 -875
  71. package/lib/core/mission-utils.ts +998 -0
  72. package/lib/core/nels.js +112 -127
  73. package/lib/core/nels.ts +189 -0
  74. package/lib/core/persistent-data-migration.js +247 -205
  75. package/lib/core/persistent-data-migration.ts +255 -0
  76. package/lib/core/product-config.js +400 -476
  77. package/lib/core/product-config.ts +518 -0
  78. package/lib/core/runtime-matrix.js +41 -87
  79. package/lib/core/runtime-matrix.ts +84 -0
  80. package/lib/core/spawn-tee.js +152 -192
  81. package/lib/core/spawn-tee.ts +202 -0
  82. package/lib/core/state-map.js +92 -92
  83. package/lib/core/state-map.ts +115 -0
  84. package/lib/core/storage.js +101 -129
  85. package/lib/core/storage.ts +182 -0
  86. package/lib/core/subagent-limit.js +15 -16
  87. package/lib/core/subagent-limit.ts +25 -0
  88. package/lib/core/verification.js +148 -138
  89. package/lib/core/verification.ts +200 -0
  90. package/lib/index.js +142 -59
  91. package/lib/index.ts +134 -0
  92. package/lib/review/rebase.js +142 -145
  93. package/lib/review/rebase.ts +188 -0
  94. package/lib/review/review-adapter.js +99 -87
  95. package/lib/review/review-adapter.ts +198 -0
  96. package/lib/review/review-artifacts.js +486 -629
  97. package/lib/review/review-artifacts.ts +622 -0
  98. package/lib/review/review-commands.js +1220 -1373
  99. package/lib/review/review-commands.ts +1579 -0
  100. package/lib/review/review-events.js +544 -886
  101. package/lib/review/review-events.ts +954 -0
  102. package/lib/review/review-loop.js +945 -1020
  103. package/lib/review/review-loop.ts +1174 -0
  104. package/lib/review/review-polling.js +86 -115
  105. package/lib/review/review-polling.ts +194 -0
  106. package/lib/review/review-prompts.js +154 -189
  107. package/lib/review/review-prompts.ts +182 -0
  108. package/lib/review/review-state.js +246 -225
  109. package/lib/review/review-state.ts +353 -0
  110. package/lib/review/review.js +63 -84
  111. package/lib/review/review.ts +138 -0
  112. package/lib/tools/backlog.js +694 -680
  113. package/lib/tools/backlog.ts +835 -0
  114. package/lib/tools/forgejo.js +1270 -1405
  115. package/lib/tools/forgejo.ts +1793 -0
  116. package/lib/tools/gatekeeper.js +106 -96
  117. package/lib/tools/gatekeeper.ts +124 -0
  118. package/lib/tools/redgreen.js +200 -181
  119. package/lib/tools/redgreen.ts +221 -0
  120. package/lib/tools/sessions.js +58 -52
  121. package/lib/tools/sessions.ts +81 -0
  122. package/lib/tools/setup-review.js +902 -975
  123. package/lib/tools/setup-review.ts +1152 -0
  124. package/package.json +7 -5
  125. package/prompts/draft.md +1 -0
  126. package/prompts/portfolio.md +1 -1
  127. package/px.js +226 -210
@@ -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
+ ;