@magnusekdahl/parallix 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/lib/agents/opencode-export.js +58 -15
- package/lib/core/verification.js +4 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -40,7 +40,7 @@ Each capability below is tied to a use case in [`docs/use-cases.md`](docs/use-ca
|
|
|
40
40
|
- **Publish work to a Forgejo reviewer surface without making Forgejo your branch authority** *(Confirmed mechanic).* When the review provider is enabled, Parallix syncs the local baseline to a dedicated `review` remote and opens or updates the PR there; if Forgejo is disabled, the branch/worktree flow still runs locally.
|
|
41
41
|
- **Use a repo-local Graphify knowledge graph for smaller codebase context pulls** *(Confirmed mechanic, optional, unproven payoff).* In repositories where the operator has already installed the Graphify skill, the workflow keeps `graphify-out/` isolated per worktree and refreshes it during review/integration, while the installed agent guidance steers codebase questions toward `graphify query` / `path` / `explain` before full reports or raw grep. That should reduce context bloat, but this repo does not currently claim a measured token-usage reduction.
|
|
42
42
|
- **Keep your existing verification gate instead of agent self-reporting** *(UC-5 — Confirmed).* The gate is a configured shell command with a no-op default: declare your existing `make` / `npm` / script command in `workflow.config.json` and it runs verbatim; declare nothing and verification is a documented no-op pass, not an invented gate.
|
|
43
|
-
- **See which agent family actually pays off across every repo one runtime drives** *(UC-6 — Partial).* A single operator-owned `stats.csv` accumulates per-agent telemetry across repositories. Token-cost comparison is complete today only for the families with structured telemetry (codex, claude);
|
|
43
|
+
- **See which agent family actually pays off across every repo one runtime drives** *(UC-6 — Partial).* A single operator-owned `stats.csv` accumulates per-agent usage telemetry across repositories. Token-cost comparison is complete today only for the families with structured telemetry (codex, claude, opencode/qwen); vibe/mistral record honest zeros by design.
|
|
44
44
|
|
|
45
45
|
## The core workflow
|
|
46
46
|
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const childProcess = require('child_process');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const path = require('path');
|
|
4
7
|
|
|
5
8
|
/**
|
|
6
9
|
* Capture the complete `opencode export <sessionId>` JSON document.
|
|
@@ -15,6 +18,10 @@ const childProcess = require('child_process');
|
|
|
15
18
|
* - Fails explicitly (resolves null) when output exceeds `maxBytes` rather
|
|
16
19
|
* than silently truncating into unparseable JSON.
|
|
17
20
|
*
|
|
21
|
+
* Uses a temp-file fd for stdout to avoid pipe-buffer data loss that occurs
|
|
22
|
+
* with large (>100 KB) child-process output. The child writes directly to
|
|
23
|
+
* the fd; after exit we read the file back as a string.
|
|
24
|
+
*
|
|
18
25
|
* Never rejects: any error/timeout/oversize resolves to null so telemetry
|
|
19
26
|
* capture stays best-effort and cannot break the launch.
|
|
20
27
|
*
|
|
@@ -44,13 +51,21 @@ function captureOpencodeExport(sessionId, opts = {}) {
|
|
|
44
51
|
|
|
45
52
|
let settled = false;
|
|
46
53
|
let timer = null;
|
|
47
|
-
const chunks = [];
|
|
48
54
|
let size = 0;
|
|
55
|
+
let tmpFd = null;
|
|
56
|
+
let tmpPath = null;
|
|
49
57
|
|
|
50
58
|
const finish = (value) => {
|
|
51
59
|
if (settled) return;
|
|
52
60
|
settled = true;
|
|
53
61
|
if (timer) clearTimeout(timer);
|
|
62
|
+
// Clean up temp file
|
|
63
|
+
if (tmpFd !== null) {
|
|
64
|
+
try { fs.closeSync(tmpFd); } catch (_) { /* already closed */ }
|
|
65
|
+
}
|
|
66
|
+
if (tmpPath) {
|
|
67
|
+
try { fs.unlinkSync(tmpPath); } catch (_) { /* already gone */ }
|
|
68
|
+
}
|
|
54
69
|
resolve(value);
|
|
55
70
|
};
|
|
56
71
|
|
|
@@ -58,18 +73,33 @@ function captureOpencodeExport(sessionId, opts = {}) {
|
|
|
58
73
|
try { child.kill('SIGKILL'); } catch (_) { /* already gone */ }
|
|
59
74
|
};
|
|
60
75
|
|
|
76
|
+
// Create a temp file for stdout to avoid pipe-buffer truncation
|
|
77
|
+
let tmpFileError = null;
|
|
78
|
+
try {
|
|
79
|
+
tmpPath = path.join(os.tmpdir(), `opencode-export-${process.pid}-${Date.now()}.json`);
|
|
80
|
+
tmpFd = fs.openSync(tmpPath, 'w');
|
|
81
|
+
} catch (e) {
|
|
82
|
+
tmpFileError = e;
|
|
83
|
+
}
|
|
84
|
+
|
|
61
85
|
let child;
|
|
62
86
|
try {
|
|
63
87
|
child = spawn('opencode', ['export', sessionId], {
|
|
64
88
|
cwd: worktree,
|
|
65
89
|
env: { ...process.env, ...(env || {}) },
|
|
66
|
-
stdio: ['ignore', 'pipe', '
|
|
90
|
+
stdio: ['ignore', tmpFd !== null ? tmpFd : 'pipe', 'pipe'],
|
|
67
91
|
});
|
|
68
92
|
} catch (_) {
|
|
69
93
|
finish(null);
|
|
70
94
|
return;
|
|
71
95
|
}
|
|
72
96
|
|
|
97
|
+
if (tmpFileError) {
|
|
98
|
+
killChild(child);
|
|
99
|
+
finish(null);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
73
103
|
timer = setTimeout(() => {
|
|
74
104
|
// Hung or slow export: kill it and degrade to null. Do not block.
|
|
75
105
|
killChild(child);
|
|
@@ -80,29 +110,42 @@ function captureOpencodeExport(sessionId, opts = {}) {
|
|
|
80
110
|
// ref'd keeps the timeout deterministic under Node's test runner (an unref'd
|
|
81
111
|
// timer can be skipped when the loop is otherwise idle, cancelling the test).
|
|
82
112
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
113
|
+
// Consume stderr to prevent pipe-buffer issues with the opencode binary
|
|
114
|
+
if (child.stderr) {
|
|
115
|
+
child.stderr.on('data', () => { /* drain stderr */ });
|
|
86
116
|
}
|
|
87
117
|
|
|
88
|
-
child.
|
|
118
|
+
child.on('error', () => finish(null));
|
|
119
|
+
|
|
120
|
+
child.on('close', (code) => {
|
|
89
121
|
if (settled) return;
|
|
90
|
-
|
|
122
|
+
if (tmpFd === null) {
|
|
123
|
+
finish(null);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
fs.closeSync(tmpFd);
|
|
129
|
+
} catch (_) { /* already closed */ }
|
|
130
|
+
tmpFd = null;
|
|
131
|
+
|
|
132
|
+
let content;
|
|
133
|
+
try {
|
|
134
|
+
content = fs.readFileSync(tmpPath, 'utf8');
|
|
135
|
+
} catch (_) {
|
|
136
|
+
finish(null);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
size = Buffer.byteLength(content);
|
|
91
141
|
if (size > maxBytes) {
|
|
92
142
|
// Explicit failure rather than silent truncation: a partial JSON
|
|
93
143
|
// document would parse to null/garbage and fabricate zero telemetry.
|
|
94
|
-
killChild(child);
|
|
95
144
|
finish(null);
|
|
96
145
|
return;
|
|
97
146
|
}
|
|
98
|
-
chunks.push(chunk);
|
|
99
|
-
});
|
|
100
147
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
child.on('close', () => {
|
|
104
|
-
if (settled) return;
|
|
105
|
-
finish(Buffer.concat(chunks).toString('utf8'));
|
|
148
|
+
finish(content);
|
|
106
149
|
});
|
|
107
150
|
});
|
|
108
151
|
}
|
package/lib/core/verification.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const { run } = require('./git');
|
|
1
|
+
const { git, run } = require('./git');
|
|
2
2
|
const { loadAdapterConfig } = require('./product-config');
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
|
|
@@ -49,7 +49,7 @@ function runVerificationGate(area, options = {}) {
|
|
|
49
49
|
return runFn('bash', ['-lc', command.replaceAll('{{area}}', effectiveArea)], { cwd: rootDir, stdio });
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
function readPublishedTreeState(rootDir = process.cwd(), { gitRunner =
|
|
52
|
+
function readPublishedTreeState(rootDir = process.cwd(), { gitRunner = git } = {}) {
|
|
53
53
|
const resolvedRoot = fs.realpathSync(rootDir);
|
|
54
54
|
const commitResult = gitRunner(['-C', resolvedRoot, 'rev-parse', 'HEAD']);
|
|
55
55
|
const treeResult = gitRunner(['-C', resolvedRoot, 'rev-parse', 'HEAD^{tree}']);
|
|
@@ -68,7 +68,7 @@ function readPublishedTreeState(rootDir = process.cwd(), { gitRunner = run } = {
|
|
|
68
68
|
|
|
69
69
|
function captureVerifiedTreeProof(area, rootDir = process.cwd(), options = {}) {
|
|
70
70
|
const {
|
|
71
|
-
gitRunner =
|
|
71
|
+
gitRunner = git,
|
|
72
72
|
runFn = run,
|
|
73
73
|
stdio = 'inherit'
|
|
74
74
|
} = options;
|
|
@@ -113,7 +113,7 @@ function captureVerifiedTreeProof(area, rootDir = process.cwd(), options = {}) {
|
|
|
113
113
|
};
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
function assertVerifiedTreeProof(proof, rootDir = process.cwd(), { gitRunner =
|
|
116
|
+
function assertVerifiedTreeProof(proof, rootDir = process.cwd(), { gitRunner = git } = {}) {
|
|
117
117
|
if (!proof || typeof proof !== 'object') {
|
|
118
118
|
return { ok: false, error: 'missing verification proof' };
|
|
119
119
|
}
|
package/package.json
CHANGED