@magnusekdahl/parallix 1.0.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/CHANGELOG.md +140 -0
- package/LICENSE +661 -0
- package/README.md +196 -0
- package/config/agents.json +25 -0
- package/config/agents.local.json.template +8 -0
- package/config/state-map.json +4 -0
- package/config/state-map.json.template +31 -0
- package/config/workflow.config.schema.json +98 -0
- package/data/.gitkeep +0 -0
- package/docs/adr/0031-ai-agent-instruction-boundary-and-command-floor.md +114 -0
- package/docs/adr/0032-mission-refinement-state-and-usage-budget-signals.md +135 -0
- package/docs/adr/0034-module-and-skill-invocation-model.md +202 -0
- package/docs/adr/0036-mission-sizing-and-dependency-wave-heuristics.md +79 -0
- package/docs/adr/0037-ai-workflow-coordination-architecture.md +162 -0
- package/docs/adr/0041-integration-pipeline-gates.md +165 -0
- package/docs/adr/0042-workflow-cli-color-rendering-approach.md +106 -0
- package/docs/adr/0043-git-target-resolution-strategy.md +185 -0
- package/docs/adr/0044-workflow-distribution-model.md +277 -0
- package/docs/adr/0045-parallax-branch-model.md +182 -0
- package/docs/adr/0046-npm-publish-process-and-security.md +138 -0
- package/docs/adr/index.md +20 -0
- package/docs/agents.md +212 -0
- package/docs/authority-reference.md +298 -0
- package/docs/forgejo-setup.md +31 -0
- package/docs/migration/extraction.md +61 -0
- package/docs/migration/task-classification.md +36 -0
- package/docs/operator-setup.md +76 -0
- package/docs/readme-rewrite-benchmark.md +188 -0
- package/docs/use-cases.md +105 -0
- package/examples/README.md +62 -0
- package/examples/run-enterprise-tarball-workflow-smoke.sh +257 -0
- package/examples/run-verify-env-smoke.sh +40 -0
- package/index.js +250 -0
- package/lib/README.md +13 -0
- package/lib/agents/agents.js +867 -0
- package/lib/agents/claude-telemetry.js +233 -0
- package/lib/agents/claude.js +139 -0
- package/lib/agents/codex-telemetry.js +202 -0
- package/lib/agents/codex.js +219 -0
- package/lib/agents/limit-hit.js +252 -0
- package/lib/agents/mistral-telemetry.js +44 -0
- package/lib/agents/mistral.js +68 -0
- package/lib/agents/opencode-export.js +110 -0
- package/lib/agents/opencode-telemetry.js +356 -0
- package/lib/agents/opencode.js +218 -0
- package/lib/agents/stage-telemetry.js +37 -0
- package/lib/commands/active.js +625 -0
- package/lib/commands/checkpoint.js +76 -0
- package/lib/commands/config.js +39 -0
- package/lib/commands/coverage-gate.js +358 -0
- package/lib/commands/diff.js +119 -0
- package/lib/commands/draft.js +854 -0
- package/lib/commands/handoff.js +501 -0
- package/lib/commands/integrate.js +1528 -0
- package/lib/commands/mission-start.js +246 -0
- package/lib/commands/rebase.js +597 -0
- package/lib/commands/repair-handoff.js +227 -0
- package/lib/commands/resolve-conflict.js +109 -0
- package/lib/commands/review.js +13 -0
- package/lib/commands/setup-review.js +13 -0
- package/lib/commands/setup.js +3 -0
- package/lib/commands/stats-backfill.js +395 -0
- package/lib/commands/stats.js +1601 -0
- package/lib/commands/status.js +183 -0
- package/lib/commands/verify.js +1 -0
- package/lib/core/fmt.js +202 -0
- package/lib/core/git.js +73 -0
- package/lib/core/gitignore.js +110 -0
- package/lib/core/mission-utils.js +1017 -0
- package/lib/core/persistent-data-migration.js +201 -0
- package/lib/core/product-config.js +508 -0
- package/lib/core/runtime-matrix.js +82 -0
- package/lib/core/spawn-tee.js +173 -0
- package/lib/core/state-map.js +89 -0
- package/lib/core/storage.js +165 -0
- package/lib/core/verification.js +149 -0
- package/lib/index.js +77 -0
- package/lib/review/rebase.js +163 -0
- package/lib/review/review-adapter.js +135 -0
- package/lib/review/review-artifacts.js +619 -0
- package/lib/review/review-commands.js +1375 -0
- package/lib/review/review-events.js +1007 -0
- package/lib/review/review-loop.js +1004 -0
- package/lib/review/review-polling.js +141 -0
- package/lib/review/review-prompts.js +212 -0
- package/lib/review/review-state.js +280 -0
- package/lib/review/review.js +96 -0
- package/lib/tools/backlog.js +680 -0
- package/lib/tools/forgejo.js +1585 -0
- package/lib/tools/gatekeeper.js +106 -0
- package/lib/tools/sessions.js +74 -0
- package/lib/tools/setup-review.js +1053 -0
- package/package.json +56 -0
- package/prompts/act-on-review-verbose.md +20 -0
- package/prompts/act-on-review.md +22 -0
- package/prompts/draft.md +20 -0
- package/prompts/execute.md +24 -0
- package/prompts/portfolio.md +30 -0
- package/prompts/review-verbose.md +20 -0
- package/prompts/review.md +17 -0
- package/px.js +236 -0
- package/templates/AGENTS-snippet.md +14 -0
- package/templates/AGENTS.md.template +34 -0
- package/templates/CLAUDE.md.template +27 -0
- package/templates/CODEX.md.template +38 -0
- package/templates/MISTRAL.md.template +24 -0
- package/templates/claude-commands/act-on-review.md +3 -0
- package/templates/claude-commands/area-review.md +3 -0
- package/templates/claude-commands/draft.md +6 -0
- package/templates/claude-commands/execute.md +6 -0
- package/templates/claude-commands/integrate.md +4 -0
- package/templates/claude-commands/portfolio.md +5 -0
- package/templates/claude-commands/review.md +4 -0
- package/templates/codex/config.toml +6 -0
- package/templates/mission-scaffold.md +39 -0
- package/templates/vibe/skills/act-on-review/SKILL.md +16 -0
- package/templates/vibe/skills/area-review/SKILL.md +16 -0
- package/templates/vibe/skills/draft/SKILL.md +16 -0
- package/templates/vibe/skills/execute/SKILL.md +16 -0
- package/templates/vibe/skills/integrate/SKILL.md +16 -0
- package/templates/vibe/skills/portfolio/SKILL.md +21 -0
- package/templates/vibe/skills/review/SKILL.md +16 -0
- package/tools/setup-forgejo-docker.sh +84 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fmt = require('../core/fmt');
|
|
4
|
+
const { loadEffectiveConfig, loadWorkflowConfig, validateWorkflowConfig } = require('../core/product-config');
|
|
5
|
+
|
|
6
|
+
// `node parallix config` — read-only. Prints the effective configuration:
|
|
7
|
+
// code-owned defaults merged with the optional workflow.config.json override.
|
|
8
|
+
// This replaces the deleted workflow.config.json.example as the way to discover
|
|
9
|
+
// the configurable surface, with no copy-paste footgun and no second source of
|
|
10
|
+
// truth to drift from (task-1233 Scope Amendment).
|
|
11
|
+
async function config(args = [], opts = {}) {
|
|
12
|
+
const logFn = opts.logFn || fmt.log.plain;
|
|
13
|
+
const errorFn = opts.errorFn || fmt.log.plainError;
|
|
14
|
+
const exitFn = opts.exitFn || (code => { process.exitCode = code; });
|
|
15
|
+
const rootDir = opts.rootDir || process.cwd();
|
|
16
|
+
|
|
17
|
+
const loaded = loadWorkflowConfig(rootDir);
|
|
18
|
+
if (!loaded.found) {
|
|
19
|
+
logFn(fmt.status('INFO', 'No workflow.config.json found — showing built-in defaults.'));
|
|
20
|
+
} else if (loaded.parseError) {
|
|
21
|
+
errorFn(fmt.status('FAIL', `workflow.config.json is invalid JSON (${loaded.parseError.message}); showing fallback built-in defaults.`));
|
|
22
|
+
logFn(JSON.stringify(loadEffectiveConfig(rootDir), null, 2));
|
|
23
|
+
exitFn(1);
|
|
24
|
+
return;
|
|
25
|
+
} else {
|
|
26
|
+
const issues = validateWorkflowConfig(loaded.config);
|
|
27
|
+
if (issues.length > 0) {
|
|
28
|
+
errorFn(fmt.status('FAIL', `workflow.config.json is structurally invalid: ${issues.join('; ')}; showing fallback built-in defaults.`));
|
|
29
|
+
logFn(JSON.stringify(loadEffectiveConfig(rootDir), null, 2));
|
|
30
|
+
exitFn(1);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
logFn(fmt.status('INFO', `Effective config (built-in defaults + ${loaded.configPath}):`));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
logFn(JSON.stringify(loadEffectiveConfig(rootDir), null, 2));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = config;
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* coverage-gate.js - run parallix tests with Node's built-in coverage support
|
|
5
|
+
* and enforce a configurable line-coverage threshold (default 90%).
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* node parallix/lib/commands/coverage-gate.js [--threshold <pct>] [--dry-run]
|
|
9
|
+
*
|
|
10
|
+
* Exit 0 when tests pass and coverage >= threshold.
|
|
11
|
+
* Exit 1 otherwise.
|
|
12
|
+
*
|
|
13
|
+
* Denominator: parallix/index.js plus all nested .js files under parallix/lib
|
|
14
|
+
* Excludes: parallix/test/*, parallix/prompts/*, parallix/config/*.json,
|
|
15
|
+
* coverage output dirs, node_modules, generated/temp files.
|
|
16
|
+
*
|
|
17
|
+
* /tmp write inventory (SC-1): every /tmp or os.tmpdir() write location used by
|
|
18
|
+
* coverage-gate.js and the workflow test infrastructure it runs:
|
|
19
|
+
*
|
|
20
|
+
* 1. PER_RUN_SCRATCH array (line 58) — tracks per-process scratch dirs created
|
|
21
|
+
* by this process. Each entry is a path returned by fs.mkdtempSync().
|
|
22
|
+
* 2. Node --experimental-test-coverage scratch (line 146) — Node creates
|
|
23
|
+
* coverage-*.json files under NODE_V8_COVERAGE. coverage-gate.js sets
|
|
24
|
+
* NODE_V8_COVERAGE to a per-process unique dir via createPerRunScratchDirs()
|
|
25
|
+
* (line 176), so Node writes to a unique path, never /tmp/node-coverage-*.
|
|
26
|
+
* 3. parallix/coverage/ directory (line 153) — created via fs.mkdirSync for
|
|
27
|
+
* lcov output; lives under REPO_ROOT, not /tmp.
|
|
28
|
+
* 4. TEMP_DIR_PREFIXES (lines 26-44) — prefix-based cleanup allowlist used by
|
|
29
|
+
* cleanupNewTempDirs() (lines 94-110). Covers dirs created by test files
|
|
30
|
+
* under parallix/test/*. These prefixes are intentionally NOT modified per SC-8.
|
|
31
|
+
* 5. os.tmpdir() default parameter (lines 85, 94) — listTempEntries() and
|
|
32
|
+
* cleanupNewTempDirs() accept os.tmpdir() as default root. runTests() passes
|
|
33
|
+
* a per-run TMPDIR to the child test process and cleans only that root.
|
|
34
|
+
* 6. Per-process mkdtempSync dirs (line 113) — createPerRunScratchDirs() creates
|
|
35
|
+
* a unique dir via fs.mkdtempSync(path.join(os.tmpdir(), 'node-coverage-'))
|
|
36
|
+
* and records it in PER_RUN_SCRATCH for targeted cleanup.
|
|
37
|
+
* 7. Test files (37 files, ~168 mkdtempSync calls) — all use
|
|
38
|
+
* fs.mkdtempSync(path.join(os.tmpdir(), 'prefix-')) pattern; already
|
|
39
|
+
* per-process unique. No fixed-path /tmp writes in test files. Generic
|
|
40
|
+
* runtime-* dirs are intentionally excluded because runtime-matrix launcher
|
|
41
|
+
* tests keep executable probes there while parallel coverage processes run.
|
|
42
|
+
* 8. /tmp/test-main — not produced by any in-scope code; orphaned 1.1 GB dir
|
|
43
|
+
* on disk from manual integration test (pre-existing, outside parallix/ scope).
|
|
44
|
+
* 9. Prompt templates (review.md, review-verbose.md, act-on-review.md, etc.) —
|
|
45
|
+
* /tmp/{{slug}}-*.md patterns are templates, not actual writes.
|
|
46
|
+
*
|
|
47
|
+
* Cleanup strategy: per-process inventory (PER_RUN_SCRATCH) + exit/SIGINT/SIGTERM
|
|
48
|
+
* handlers (lines 131-141) that remove only tracked dirs. No blanket rm -rf.
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
const { spawnSync } = require('child_process');
|
|
52
|
+
const fs = require('fs');
|
|
53
|
+
const os = require('os');
|
|
54
|
+
const path = require('path');
|
|
55
|
+
const fmt = require('../core/fmt');
|
|
56
|
+
|
|
57
|
+
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
58
|
+
const SELF_TEST_FILE = 'coverage-gate.test.js';
|
|
59
|
+
const TEMP_DIR_PREFIXES = [
|
|
60
|
+
'agents-',
|
|
61
|
+
'sessions-',
|
|
62
|
+
'review-',
|
|
63
|
+
'forgejo-',
|
|
64
|
+
'workflow-',
|
|
65
|
+
'gatekeeper-',
|
|
66
|
+
'graphify-',
|
|
67
|
+
'claude-',
|
|
68
|
+
'codex-',
|
|
69
|
+
'gemini-',
|
|
70
|
+
'opencode-',
|
|
71
|
+
'startagent-',
|
|
72
|
+
'test-',
|
|
73
|
+
'mission-task-',
|
|
74
|
+
'visualBoard-task-',
|
|
75
|
+
'backlog-'
|
|
76
|
+
];
|
|
77
|
+
const COVERAGE_INCLUDES = [
|
|
78
|
+
'index.js',
|
|
79
|
+
'lib/**/*.js'
|
|
80
|
+
];
|
|
81
|
+
const COVERAGE_EXCLUDES = [
|
|
82
|
+
'test/**',
|
|
83
|
+
'prompts/**',
|
|
84
|
+
'config/*.json',
|
|
85
|
+
'.workflow/**',
|
|
86
|
+
'node_modules/**'
|
|
87
|
+
];
|
|
88
|
+
// The full suite regularly exceeds 10 minutes in this repository, especially
|
|
89
|
+
// under cold caches. Keep the gate generous so it can finish without a manual
|
|
90
|
+
// override while still failing on real hangs.
|
|
91
|
+
const DEFAULT_TEST_TIMEOUT_MS = 3_600_000;
|
|
92
|
+
const PER_RUN_SCRATCH = [];
|
|
93
|
+
let threshold = 90;
|
|
94
|
+
let dryRun = false;
|
|
95
|
+
let lcov = false;
|
|
96
|
+
let cleanupDone = false;
|
|
97
|
+
|
|
98
|
+
for (let i = 2; i < process.argv.length; i++) {
|
|
99
|
+
if (process.argv[i] === '--threshold' && process.argv[i + 1]) {
|
|
100
|
+
threshold = parseFloat(process.argv[i + 1]);
|
|
101
|
+
i++;
|
|
102
|
+
} else if (process.argv[i] === '--dry-run') {
|
|
103
|
+
dryRun = true;
|
|
104
|
+
} else if (process.argv[i] === '--lcov') {
|
|
105
|
+
lcov = true;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function discoverTestFiles() {
|
|
110
|
+
const testDir = path.join(REPO_ROOT, 'test');
|
|
111
|
+
if (!fs.existsSync(testDir)) return [];
|
|
112
|
+
return fs.readdirSync(testDir)
|
|
113
|
+
.filter(file => file.endsWith('.test.js'))
|
|
114
|
+
.filter(file => file !== SELF_TEST_FILE)
|
|
115
|
+
.map(file => path.join(testDir, file))
|
|
116
|
+
.sort();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function listTempEntries(tmpRoot = os.tmpdir()) {
|
|
120
|
+
if (!fs.existsSync(tmpRoot)) return new Set();
|
|
121
|
+
return new Set(fs.readdirSync(tmpRoot));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function shouldCleanTempDir(name) {
|
|
125
|
+
return TEMP_DIR_PREFIXES.some(prefix => name.startsWith(prefix));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function cleanupNewTempDirs(beforeEntries, tmpRoot = os.tmpdir()) {
|
|
129
|
+
if (!fs.existsSync(tmpRoot)) return;
|
|
130
|
+
for (const name of fs.readdirSync(tmpRoot)) {
|
|
131
|
+
if (beforeEntries.has(name) || !shouldCleanTempDir(name)) continue;
|
|
132
|
+
const fullPath = path.join(tmpRoot, name);
|
|
133
|
+
let stat = null;
|
|
134
|
+
try {
|
|
135
|
+
stat = fs.statSync(fullPath);
|
|
136
|
+
} catch (_) {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (!stat.isDirectory()) continue;
|
|
140
|
+
try {
|
|
141
|
+
fs.rmSync(fullPath, { recursive: true, force: true });
|
|
142
|
+
} catch (_) {}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function createPerRunScratchDirs() {
|
|
147
|
+
const coverageDir = fs.mkdtempSync(path.join(os.tmpdir(), 'node-coverage-'));
|
|
148
|
+
PER_RUN_SCRATCH.push(coverageDir);
|
|
149
|
+
return coverageDir;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function createPerRunTmpRoot() {
|
|
153
|
+
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'coverage-gate-tmp-'));
|
|
154
|
+
PER_RUN_SCRATCH.push(tmpRoot);
|
|
155
|
+
return tmpRoot;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function createMockGraphifyBin() {
|
|
159
|
+
const graphifyDir = fs.mkdtempSync(path.join(os.tmpdir(), 'graphify-'));
|
|
160
|
+
PER_RUN_SCRATCH.push(graphifyDir);
|
|
161
|
+
const graphifyBin = path.join(graphifyDir, 'graphify');
|
|
162
|
+
fs.writeFileSync(graphifyBin, `#!/usr/bin/env bash
|
|
163
|
+
set -euo pipefail
|
|
164
|
+
if [[ "\${1:-}" == "--help" ]]; then
|
|
165
|
+
echo "mock graphify"
|
|
166
|
+
exit 0
|
|
167
|
+
fi
|
|
168
|
+
if [[ "\${1:-}" == "update" ]]; then
|
|
169
|
+
mkdir -p graphify-out
|
|
170
|
+
echo '{"nodes":[],"edges":[]}' > graphify-out/graph.json
|
|
171
|
+
echo '# Mock Graph Report' > graphify-out/GRAPH_REPORT.md
|
|
172
|
+
echo "[mock graphify] update $*"
|
|
173
|
+
exit 0
|
|
174
|
+
fi
|
|
175
|
+
echo "Unhandled graphify command: $*" >&2
|
|
176
|
+
exit 1
|
|
177
|
+
`, { mode: 0o755 });
|
|
178
|
+
return graphifyBin;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function cleanupPerRunScratch() {
|
|
182
|
+
if (cleanupDone) return;
|
|
183
|
+
cleanupDone = true;
|
|
184
|
+
for (const dir of PER_RUN_SCRATCH) {
|
|
185
|
+
try {
|
|
186
|
+
if (fs.existsSync(dir)) {
|
|
187
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
188
|
+
}
|
|
189
|
+
} catch (_) {}
|
|
190
|
+
}
|
|
191
|
+
PER_RUN_SCRATCH.length = 0;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function registerExitHandlers() {
|
|
195
|
+
process.on('exit', cleanupPerRunScratch);
|
|
196
|
+
process.on('SIGINT', () => {
|
|
197
|
+
cleanupPerRunScratch();
|
|
198
|
+
process.exit(130);
|
|
199
|
+
});
|
|
200
|
+
process.on('SIGTERM', () => {
|
|
201
|
+
cleanupPerRunScratch();
|
|
202
|
+
process.exit(143);
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function resetPerRunScratchState() {
|
|
207
|
+
PER_RUN_SCRATCH.length = 0;
|
|
208
|
+
cleanupDone = false;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function buildCoverageArgs(testFiles, coverageThreshold = threshold, useLcov = lcov) {
|
|
212
|
+
const args = [
|
|
213
|
+
'--test',
|
|
214
|
+
'--experimental-test-coverage',
|
|
215
|
+
`--test-coverage-lines=${coverageThreshold}`,
|
|
216
|
+
...COVERAGE_INCLUDES.flatMap(pattern => ['--test-coverage-include', pattern]),
|
|
217
|
+
...COVERAGE_EXCLUDES.flatMap(pattern => ['--test-coverage-exclude', pattern]),
|
|
218
|
+
];
|
|
219
|
+
|
|
220
|
+
if (useLcov) {
|
|
221
|
+
const coverageDir = path.join(REPO_ROOT, 'coverage');
|
|
222
|
+
if (!fs.existsSync(coverageDir)) {
|
|
223
|
+
fs.mkdirSync(coverageDir, { recursive: true });
|
|
224
|
+
}
|
|
225
|
+
args.push('--test-reporter=lcov');
|
|
226
|
+
args.push(`--test-reporter-destination=${path.join(coverageDir, 'lcov.info')}`);
|
|
227
|
+
args.push('--test-reporter=spec');
|
|
228
|
+
args.push('--test-reporter-destination=stdout');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
args.push(...testFiles);
|
|
232
|
+
return args;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function resolveTestTimeoutMs(env = process.env) {
|
|
236
|
+
const raw = env.WORKFLOW_COVERAGE_GATE_TIMEOUT_MS;
|
|
237
|
+
if (raw === undefined || raw === '') return DEFAULT_TEST_TIMEOUT_MS;
|
|
238
|
+
const parsed = Number(raw);
|
|
239
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_TEST_TIMEOUT_MS;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function runTests(testFiles, coverageThreshold = threshold, _spawnSync = spawnSync) {
|
|
243
|
+
const tmpRoot = createPerRunTmpRoot();
|
|
244
|
+
const tmpEntriesBefore = listTempEntries(tmpRoot);
|
|
245
|
+
const nodeCoverageDir = createPerRunScratchDirs();
|
|
246
|
+
const childEnv = { ...process.env };
|
|
247
|
+
delete childEnv.NODE_TEST_CONTEXT;
|
|
248
|
+
delete childEnv.NODE_OPTIONS;
|
|
249
|
+
childEnv.TMPDIR = tmpRoot;
|
|
250
|
+
childEnv.NODE_V8_COVERAGE = nodeCoverageDir;
|
|
251
|
+
childEnv.GRAPHIFY_BIN = createMockGraphifyBin();
|
|
252
|
+
|
|
253
|
+
const result = _spawnSync(process.execPath, buildCoverageArgs(testFiles, coverageThreshold), {
|
|
254
|
+
encoding: 'utf8',
|
|
255
|
+
cwd: REPO_ROOT,
|
|
256
|
+
env: childEnv,
|
|
257
|
+
stdio: ['pipe', 'inherit', 'inherit'],
|
|
258
|
+
timeout: resolveTestTimeoutMs(),
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
cleanupNewTempDirs(tmpEntriesBefore, tmpRoot);
|
|
262
|
+
cleanupPerRunScratch();
|
|
263
|
+
|
|
264
|
+
if (result.error) {
|
|
265
|
+
fmt.log.error(`[coverage-gate] failed to spawn test runner: ${result.error.message}`);
|
|
266
|
+
return 1;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (result.signal) {
|
|
270
|
+
fmt.log.error(`[coverage-gate] test runner killed by signal ${result.signal}`);
|
|
271
|
+
return 1;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return result.status === null ? 1 : result.status;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function main() {
|
|
278
|
+
const testFiles = discoverTestFiles();
|
|
279
|
+
if (testFiles.length === 0) {
|
|
280
|
+
fmt.log.fail('no parallix test files found under test/');
|
|
281
|
+
process.exit(1);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
fmt.log.info(`Found ${testFiles.length} test file(s)`);
|
|
285
|
+
|
|
286
|
+
if (dryRun) {
|
|
287
|
+
fmt.log.info(`DRY-RUN mode — threshold=${threshold}%`);
|
|
288
|
+
fmt.log.info('Denominator: index.js + lib/**/*.js');
|
|
289
|
+
fmt.log.info(`Include globs: ${COVERAGE_INCLUDES.join(', ')}`);
|
|
290
|
+
fmt.log.info(`Exclude globs: ${COVERAGE_EXCLUDES.join(', ')}`);
|
|
291
|
+
fmt.log.info(`Would run: ${fmt.command(`${process.execPath} ${buildCoverageArgs(testFiles, threshold).join(' ')}`)}`);
|
|
292
|
+
process.exit(0);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
registerExitHandlers();
|
|
296
|
+
process.exit(runTests(testFiles, threshold));
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (require.main === module) {
|
|
300
|
+
main();
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function run(args, options = {}) {
|
|
304
|
+
const exitFn = options.exitFn || process.exit;
|
|
305
|
+
let threshold_ = 90;
|
|
306
|
+
let dryRun_ = false;
|
|
307
|
+
for (let i = 0; i < args.length; i++) {
|
|
308
|
+
if (args[i] === '--threshold' && args[i + 1]) {
|
|
309
|
+
threshold_ = parseFloat(args[i + 1]);
|
|
310
|
+
i++;
|
|
311
|
+
} else if (args[i] === '--dry-run') {
|
|
312
|
+
dryRun_ = true;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
const savedThreshold = threshold;
|
|
316
|
+
const savedDryRun = dryRun;
|
|
317
|
+
threshold = threshold_;
|
|
318
|
+
dryRun = dryRun_;
|
|
319
|
+
try {
|
|
320
|
+
if (dryRun_) {
|
|
321
|
+
const testFiles = discoverTestFiles();
|
|
322
|
+
if (testFiles.length === 0) {
|
|
323
|
+
if (typeof exitFn === 'function') exitFn(1);
|
|
324
|
+
} else {
|
|
325
|
+
fmt.log.info(`Found ${testFiles.length} test file(s)`);
|
|
326
|
+
fmt.log.info(`DRY-RUN mode — threshold=${threshold}%`);
|
|
327
|
+
fmt.log.info('Denominator: index.js + lib/**/*.js');
|
|
328
|
+
fmt.log.info(`Include globs: ${COVERAGE_INCLUDES.join(', ')}`);
|
|
329
|
+
fmt.log.info(`Exclude globs: ${COVERAGE_EXCLUDES.join(', ')}`);
|
|
330
|
+
fmt.log.info(`Would run: ${fmt.command(`${process.execPath} ${buildCoverageArgs(testFiles, threshold).join(' ')}`)}`);
|
|
331
|
+
if (typeof exitFn === 'function') exitFn(0);
|
|
332
|
+
else process.exit(0);
|
|
333
|
+
}
|
|
334
|
+
} else {
|
|
335
|
+
registerExitHandlers();
|
|
336
|
+
exitFn(runTests(discoverTestFiles(), threshold));
|
|
337
|
+
}
|
|
338
|
+
} finally {
|
|
339
|
+
threshold = savedThreshold;
|
|
340
|
+
dryRun = savedDryRun;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
module.exports = run;
|
|
345
|
+
module.exports.buildCoverageArgs = buildCoverageArgs;
|
|
346
|
+
module.exports.cleanupNewTempDirs = cleanupNewTempDirs;
|
|
347
|
+
module.exports.cleanupPerRunScratch = cleanupPerRunScratch;
|
|
348
|
+
module.exports.createPerRunScratchDirs = createPerRunScratchDirs;
|
|
349
|
+
module.exports.COVERAGE_EXCLUDES = COVERAGE_EXCLUDES;
|
|
350
|
+
module.exports.COVERAGE_INCLUDES = COVERAGE_INCLUDES;
|
|
351
|
+
module.exports.DEFAULT_TEST_TIMEOUT_MS = DEFAULT_TEST_TIMEOUT_MS;
|
|
352
|
+
module.exports.discoverTestFiles = discoverTestFiles;
|
|
353
|
+
module.exports.listTempEntries = listTempEntries;
|
|
354
|
+
module.exports.registerExitHandlers = registerExitHandlers;
|
|
355
|
+
module.exports.resetPerRunScratchState = resetPerRunScratchState;
|
|
356
|
+
module.exports.resolveTestTimeoutMs = resolveTestTimeoutMs;
|
|
357
|
+
module.exports.runTests = runTests;
|
|
358
|
+
module.exports.shouldCleanTempDir = shouldCleanTempDir;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
const childProcess = require('child_process');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { git } = require('../core/git');
|
|
4
|
+
const { getPrimaryBranch, missionBranchName, inferSlug, resolveWorktree } = require('../core/mission-utils');
|
|
5
|
+
const fmt = require('../core/fmt');
|
|
6
|
+
|
|
7
|
+
async function diff(args, {
|
|
8
|
+
gitFn = git,
|
|
9
|
+
spawnSyncFn = childProcess.spawnSync,
|
|
10
|
+
getPrimaryBranchFn = getPrimaryBranch,
|
|
11
|
+
missionBranchNameFn = missionBranchName,
|
|
12
|
+
inferSlugFn = inferSlug,
|
|
13
|
+
resolveWorktreeFn = resolveWorktree,
|
|
14
|
+
exitFn = process.exit,
|
|
15
|
+
failFn = fmt.log.fail,
|
|
16
|
+
} = {}) {
|
|
17
|
+
const explicitSlug = args.filter(a => !a.startsWith('--'))[0];
|
|
18
|
+
const slug = inferSlugFn(explicitSlug);
|
|
19
|
+
|
|
20
|
+
if (!slug) {
|
|
21
|
+
failFn('Usage: node parallix diff [<slug>]');
|
|
22
|
+
exitFn(1);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const worktree = resolveWorktreeFn(slug);
|
|
27
|
+
if (!worktree) {
|
|
28
|
+
failFn(`Mission worktree not found for ${fmt.slug(slug)}. Run this from the mission worktree or create it first.`);
|
|
29
|
+
exitFn(1);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let primary;
|
|
34
|
+
try {
|
|
35
|
+
primary = getPrimaryBranchFn(worktree);
|
|
36
|
+
} catch (err) {
|
|
37
|
+
failFn(err.message);
|
|
38
|
+
exitFn(1);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const branch = missionBranchNameFn(slug);
|
|
43
|
+
const target = `${primary}..HEAD`;
|
|
44
|
+
|
|
45
|
+
// Detect configured diff tool/pager
|
|
46
|
+
const getGitConfig = (key) => {
|
|
47
|
+
const result = gitFn(['-C', worktree, 'config', '--get', key]);
|
|
48
|
+
return result.status === 0 ? result.stdout.trim() : null;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const isSpecializedTool = (cmd) => {
|
|
52
|
+
if (!cmd) return false;
|
|
53
|
+
const parts = cmd.split(/\s+/);
|
|
54
|
+
// Handle cases like "LESS=FRX less" or "/usr/bin/delta"
|
|
55
|
+
const exePart = parts.find(p => !p.includes('='));
|
|
56
|
+
if (!exePart) return false;
|
|
57
|
+
const exe = path.basename(exePart).toLowerCase();
|
|
58
|
+
|
|
59
|
+
// Explicitly reject known default pagers
|
|
60
|
+
if (['less', 'cat', 'more'].includes(exe)) return false;
|
|
61
|
+
|
|
62
|
+
// Accept known specialized tools documented in review-tooling.md
|
|
63
|
+
if (['delta', 'difft', 'diff-so-fancy'].includes(exe)) return true;
|
|
64
|
+
|
|
65
|
+
// Fallback: if it's not a known default, and we have a value, treat it as specialized
|
|
66
|
+
// for now to avoid breaking custom setups like "vimdiff", but we've rejected plain "less".
|
|
67
|
+
return true;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const handleLaunchResult = (result, toolName) => {
|
|
71
|
+
if (result.error) {
|
|
72
|
+
failFn(`Failed to launch ${toolName}: ${result.error.message}`);
|
|
73
|
+
exitFn(1);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (result.signal) {
|
|
77
|
+
failFn(`${toolName} terminated by signal: ${result.signal}`);
|
|
78
|
+
exitFn(1);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (result.status === null) {
|
|
82
|
+
failFn(`${toolName} exited with unknown status.`);
|
|
83
|
+
exitFn(1);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
exitFn(result.status);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const diffTool = getGitConfig('diff.tool');
|
|
90
|
+
if (diffTool) {
|
|
91
|
+
fmt.log.info(`Launching ${fmt.command('git difftool')} for ${fmt.branch(branch)} (tool: ${fmt.bold(diffTool)})...`);
|
|
92
|
+
const result = spawnSyncFn('git', ['difftool', '-d', '--no-prompt', target], { cwd: worktree, stdio: 'inherit' });
|
|
93
|
+
handleLaunchResult(result, 'git difftool');
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const pagerDiff = getGitConfig('pager.diff');
|
|
98
|
+
if (isSpecializedTool(pagerDiff)) {
|
|
99
|
+
fmt.log.info(`Launching ${fmt.command('git diff')} for ${fmt.branch(branch)} (pager: ${fmt.bold(pagerDiff)})...`);
|
|
100
|
+
const result = spawnSyncFn('git', ['diff', target], { cwd: worktree, stdio: 'inherit' });
|
|
101
|
+
handleLaunchResult(result, 'git diff');
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const corePager = getGitConfig('core.pager');
|
|
106
|
+
if (isSpecializedTool(corePager)) {
|
|
107
|
+
fmt.log.info(`Launching ${fmt.command('git diff')} for ${fmt.branch(branch)} (core.pager: ${fmt.bold(corePager)})...`);
|
|
108
|
+
const result = spawnSyncFn('git', ['diff', target], { cwd: worktree, stdio: 'inherit' });
|
|
109
|
+
handleLaunchResult(result, 'git diff');
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
failFn('No specialized local diff tool (e.g., delta, difftastic) is configured in Git.');
|
|
114
|
+
fmt.log.info('The workflow diff command requires an explicitly configured diff.tool or a non-default core.pager.');
|
|
115
|
+
fmt.log.info(`Refer to ${fmt.path('docs/developer-setup/review-tooling.md')} for setup guidance.`);
|
|
116
|
+
exitFn(1);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = diff;
|