@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.
- package/index.js +190 -185
- package/lib/agents/agents.js +737 -838
- package/lib/agents/agents.ts +930 -0
- package/lib/agents/claude-telemetry.js +149 -154
- package/lib/agents/claude-telemetry.ts +233 -0
- package/lib/agents/claude.js +121 -131
- package/lib/agents/claude.ts +156 -0
- package/lib/agents/codex-telemetry.js +176 -164
- package/lib/agents/codex-telemetry.ts +205 -0
- package/lib/agents/codex.js +168 -201
- package/lib/agents/codex.ts +236 -0
- package/lib/agents/limit-hit.js +216 -226
- package/lib/agents/limit-hit.ts +262 -0
- package/lib/agents/mistral-telemetry.js +7 -10
- package/lib/agents/mistral-telemetry.ts +44 -0
- package/lib/agents/mistral.js +40 -58
- package/lib/agents/mistral.ts +84 -0
- package/lib/agents/opencode-export.js +126 -127
- package/lib/agents/opencode-export.ts +160 -0
- package/lib/agents/opencode-telemetry.js +282 -261
- package/lib/agents/opencode-telemetry.ts +350 -0
- package/lib/agents/opencode.js +263 -305
- package/lib/agents/opencode.ts +370 -0
- package/lib/agents/stage-telemetry.js +23 -24
- package/lib/agents/stage-telemetry.ts +47 -0
- package/lib/commands/active.js +502 -579
- package/lib/commands/active.ts +665 -0
- package/lib/commands/checkpoint.js +98 -74
- package/lib/commands/checkpoint.ts +79 -0
- package/lib/commands/config.js +61 -31
- package/lib/commands/config.ts +47 -0
- package/lib/commands/coverage-gate.js +271 -246
- package/lib/commands/coverage-gate.ts +362 -0
- package/lib/commands/diff.js +140 -119
- package/lib/commands/diff.ts +125 -0
- package/lib/commands/draft.js +770 -923
- package/lib/commands/draft.ts +1021 -0
- package/lib/commands/handoff.js +588 -608
- package/lib/commands/handoff.ts +694 -0
- package/lib/commands/integrate.js +1332 -1463
- package/lib/commands/integrate.ts +1688 -0
- package/lib/commands/mission-start.js +279 -237
- package/lib/commands/mission-start.ts +263 -0
- package/lib/commands/rebase.js +606 -582
- package/lib/commands/rebase.ts +635 -0
- package/lib/commands/repair-handoff.js +181 -179
- package/lib/commands/repair-handoff.ts +228 -0
- package/lib/commands/resolve-conflict.js +121 -102
- package/lib/commands/resolve-conflict.ts +112 -0
- package/lib/commands/review.js +3 -4
- package/lib/commands/review.ts +14 -0
- package/lib/commands/setup-review.js +4 -5
- package/lib/commands/setup-review.ts +14 -0
- package/lib/commands/setup.js +3 -3
- package/lib/commands/setup.ts +2 -0
- package/lib/commands/stats-backfill.js +394 -359
- package/lib/commands/stats-backfill.ts +418 -0
- package/lib/commands/stats.js +1424 -1665
- package/lib/commands/stats.ts +2186 -0
- package/lib/commands/status.js +219 -200
- package/lib/commands/status.ts +219 -0
- package/lib/commands/verify.js +3 -1
- package/lib/commands/verify.ts +2 -0
- package/lib/core/fmt.js +140 -211
- package/lib/core/fmt.ts +189 -0
- package/lib/core/git.js +121 -102
- package/lib/core/git.ts +146 -0
- package/lib/core/gitignore.js +59 -106
- package/lib/core/gitignore.ts +103 -0
- package/lib/core/mission-utils.js +833 -875
- package/lib/core/mission-utils.ts +998 -0
- package/lib/core/nels.js +112 -127
- package/lib/core/nels.ts +189 -0
- package/lib/core/persistent-data-migration.js +247 -205
- package/lib/core/persistent-data-migration.ts +255 -0
- package/lib/core/product-config.js +400 -476
- package/lib/core/product-config.ts +518 -0
- package/lib/core/runtime-matrix.js +41 -87
- package/lib/core/runtime-matrix.ts +84 -0
- package/lib/core/spawn-tee.js +152 -192
- package/lib/core/spawn-tee.ts +202 -0
- package/lib/core/state-map.js +92 -92
- package/lib/core/state-map.ts +115 -0
- package/lib/core/storage.js +101 -129
- package/lib/core/storage.ts +182 -0
- package/lib/core/subagent-limit.js +15 -16
- package/lib/core/subagent-limit.ts +25 -0
- package/lib/core/verification.js +148 -138
- package/lib/core/verification.ts +200 -0
- package/lib/index.js +142 -59
- package/lib/index.ts +134 -0
- package/lib/review/rebase.js +142 -145
- package/lib/review/rebase.ts +188 -0
- package/lib/review/review-adapter.js +99 -87
- package/lib/review/review-adapter.ts +198 -0
- package/lib/review/review-artifacts.js +486 -629
- package/lib/review/review-artifacts.ts +622 -0
- package/lib/review/review-commands.js +1220 -1373
- package/lib/review/review-commands.ts +1579 -0
- package/lib/review/review-events.js +544 -886
- package/lib/review/review-events.ts +954 -0
- package/lib/review/review-loop.js +945 -1020
- package/lib/review/review-loop.ts +1174 -0
- package/lib/review/review-polling.js +86 -115
- package/lib/review/review-polling.ts +194 -0
- package/lib/review/review-prompts.js +154 -189
- package/lib/review/review-prompts.ts +182 -0
- package/lib/review/review-state.js +246 -225
- package/lib/review/review-state.ts +353 -0
- package/lib/review/review.js +63 -84
- package/lib/review/review.ts +138 -0
- package/lib/tools/backlog.js +694 -680
- package/lib/tools/backlog.ts +835 -0
- package/lib/tools/forgejo.js +1270 -1405
- package/lib/tools/forgejo.ts +1793 -0
- package/lib/tools/gatekeeper.js +106 -96
- package/lib/tools/gatekeeper.ts +124 -0
- package/lib/tools/redgreen.js +200 -181
- package/lib/tools/redgreen.ts +221 -0
- package/lib/tools/sessions.js +58 -52
- package/lib/tools/sessions.ts +81 -0
- package/lib/tools/setup-review.js +902 -975
- package/lib/tools/setup-review.ts +1152 -0
- package/package.json +7 -5
- package/prompts/draft.md +1 -0
- package/prompts/portfolio.md +1 -1
- package/px.js +226 -210
package/lib/tools/redgreen.js
CHANGED
|
@@ -1,59 +1,99 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
findReproTestPathFn
|
|
111
|
-
resolveMissionParentCommitFn
|
|
112
|
-
runReproAtRefFn
|
|
113
|
-
branch
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
process.
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
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
|
+
;
|