@malmhq/intermesh-cli 0.1.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/LICENSE +198 -0
- package/README.md +77 -0
- package/dist/generation/.opencode/AGENTS.md +38 -0
- package/dist/generation/.opencode/agents/generation-builder.md +9 -0
- package/dist/generation/.opencode/agents/generation-fidelity.md +9 -0
- package/dist/generation/.opencode/agents/generation-source.md +13 -0
- package/dist/generation/.opencode/prompts/analyze-capabilities.md +61 -0
- package/dist/generation/.opencode/prompts/analyze-source.md +41 -0
- package/dist/generation/.opencode/prompts/generate.md +9 -0
- package/dist/generation/.opencode/prompts/repair-capability-proposal.md +5 -0
- package/dist/generation/.opencode/skills/generation-building/SKILL.md +32 -0
- package/dist/generation/.opencode/skills/generation-package-review/SKILL.md +36 -0
- package/dist/generation/.opencode/skills/generation-source-analysis/SKILL.md +36 -0
- package/dist/generation/AGENTS.md +38 -0
- package/dist/generation/agent-skill-installer/index.ts +4 -0
- package/dist/generation/agent-skill-installer/install.ts +503 -0
- package/dist/generation/agent-skill-installer/lock.ts +128 -0
- package/dist/generation/agent-skill-installer/manifest.ts +271 -0
- package/dist/generation/agent-skill-installer/targets.ts +169 -0
- package/dist/generation/runtime-entrypoint.js +39980 -0
- package/dist/generation/scripts/check-package.mjs +45 -0
- package/dist/generation/scripts/installed-help.mjs +98 -0
- package/dist/generation/scripts/lead-input.mjs +584 -0
- package/dist/generation/scripts/prepare-opencode.mjs +33 -0
- package/dist/generation/scripts/prepared-work.mjs +197 -0
- package/dist/generation/scripts/repair-loop.mjs +519 -0
- package/dist/generation/scripts/run-reviewers.mjs +306 -0
- package/dist/generation/scripts/runtime-capture.mjs +119 -0
- package/dist/generation/scripts/stage-timings.mjs +53 -0
- package/dist/generation/scripts/validate-source-brief.mjs +220 -0
- package/dist/generation/scripts/verify-package.mjs +365 -0
- package/dist/generation/templates/npm-cli/CONTRIBUTING.md +20 -0
- package/dist/generation/templates/npm-cli/GENERATION-PATTERNS.md +143 -0
- package/dist/generation/templates/npm-cli/README.md +67 -0
- package/dist/generation/templates/npm-cli/capabilities.json +1 -0
- package/dist/generation/templates/npm-cli/mock/routes.ts +3 -0
- package/dist/generation/templates/npm-cli/mock/server.ts +15 -0
- package/dist/generation/templates/npm-cli/mock/types.ts +7 -0
- package/dist/generation/templates/npm-cli/package.json +36 -0
- package/dist/generation/templates/npm-cli/pnpm-lock.yaml +495 -0
- package/dist/generation/templates/npm-cli/pnpm-workspace.yaml +2 -0
- package/dist/generation/templates/npm-cli/skills/customer-cli-management/SKILL.md +22 -0
- package/dist/generation/templates/npm-cli/skills/customer-workflow/SKILL.md +22 -0
- package/dist/generation/templates/npm-cli/src/auth/commands.ts +77 -0
- package/dist/generation/templates/npm-cli/src/auth/credentials.ts +66 -0
- package/dist/generation/templates/npm-cli/src/auth/prompt.ts +27 -0
- package/dist/generation/templates/npm-cli/src/cli.ts +53 -0
- package/dist/generation/templates/npm-cli/src/commands.ts +6 -0
- package/dist/generation/templates/npm-cli/src/customer.ts +16 -0
- package/dist/generation/templates/npm-cli/src/index.ts +11 -0
- package/dist/generation/templates/npm-cli/src/lifecycle/commands.ts +344 -0
- package/dist/generation/templates/npm-cli/src/lifecycle/config.ts +32 -0
- package/dist/generation/templates/npm-cli/src/shared/dry-run.ts +32 -0
- package/dist/generation/templates/npm-cli/src/shared/errors.ts +162 -0
- package/dist/generation/templates/npm-cli/src/shared/files.ts +45 -0
- package/dist/generation/templates/npm-cli/src/shared/http.ts +125 -0
- package/dist/generation/templates/npm-cli/src/shared/output.ts +33 -0
- package/dist/generation/templates/npm-cli/test/auth.test.ts +113 -0
- package/dist/generation/templates/npm-cli/test/fixture-cli.ts +51 -0
- package/dist/generation/templates/npm-cli/test/foundation.test.ts +316 -0
- package/dist/generation/templates/npm-cli/test/helpers.ts +34 -0
- package/dist/generation/templates/npm-cli/test/mock-lifecycle.test.ts +86 -0
- package/dist/generation/templates/npm-cli/test/run-with-mock.ts +142 -0
- package/dist/generation/templates/npm-cli/tsconfig.build.json +5 -0
- package/dist/generation/templates/npm-cli/tsconfig.json +11 -0
- package/dist/generation/templates/opencode-bootstrap/package-lock.json +402 -0
- package/dist/generation/templates/opencode-bootstrap/package.json +7 -0
- package/dist/index.js +51965 -0
- package/dist/runtime/candidate-inspection/inspect.Dockerfile +4 -0
- package/dist/runtime/candidate-inspection/install.Dockerfile +5 -0
- package/dist/runtime/candidate-inspection/registry-proxy.mjs +37 -0
- package/dist/runtime/harbor/bootstrap/package-lock.json +402 -0
- package/dist/runtime/harbor/bootstrap/package.json +7 -0
- package/dist/runtime/harbor/evaluation.Dockerfile +31 -0
- package/dist/runtime/harbor/harbor_launcher.py +62 -0
- package/dist/runtime/harbor/intermesh_opencode.py +44 -0
- package/dist/runtime/harbor/model-forwarder.mjs +52 -0
- package/dist/runtime/live-task.md +13 -0
- package/package.json +44 -0
- package/skills/inter-cli-management/SKILL.md +38 -0
- package/skills/inter-cli-management/evals/evals.json +40 -0
- package/skills/inter-context/SKILL.md +72 -0
- package/skills/inter-context/evals/cases.json +82 -0
- package/skills/inter-eval-authoring/SKILL.md +70 -0
- package/skills/inter-eval-authoring/evals/evals.json +69 -0
- package/skills/inter-eval-authoring/evals/trigger-queries.json +34 -0
- package/skills/inter-eval-authoring/references/authoring-guide.md +70 -0
- package/skills/inter-eval-authoring/references/eval-plan.example.json +92 -0
- package/skills/inter-eval-authoring/references/eval-plan.schema.json +304 -0
- package/skills/inter-evaluation/SKILL.md +51 -0
- package/skills/inter-evaluation/evals/evals.json +49 -0
- package/skills/inter-evaluation/evals/trigger-queries.json +30 -0
- package/skills/inter-generation/SKILL.md +42 -0
- package/skills/inter-generation/evals/evals.json +81 -0
- package/skills/inter-generation/evals/trigger-queries.json +58 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { dirname, join, resolve } from 'node:path';
|
|
5
|
+
import { prepareOpenCodeDependencies } from './prepare-opencode.mjs';
|
|
6
|
+
import { startRuntimeCapture } from './runtime-capture.mjs';
|
|
7
|
+
|
|
8
|
+
const agents = ['generation-fidelity'];
|
|
9
|
+
const resultPattern = /REVIEW_RESULT:\s*(\{[^\n]+\})/gu;
|
|
10
|
+
const outputLimit = 10 * 1024 * 1024;
|
|
11
|
+
const retainedReportLimit = 100_000;
|
|
12
|
+
const findingLimit = 8_000;
|
|
13
|
+
const areas = new Set(['cli', 'skill', 'cross-cutting', 'unknown']);
|
|
14
|
+
|
|
15
|
+
function positiveInteger(value, fallback, name) {
|
|
16
|
+
const parsed = value === undefined ? fallback : Number(value);
|
|
17
|
+
if (!Number.isSafeInteger(parsed) || parsed < 1) throw new TypeError(`${name} is invalid`);
|
|
18
|
+
return parsed;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function redact(value, secrets) {
|
|
22
|
+
return secrets.reduce((text, secret) => text.replaceAll(secret, '[REDACTED]'), value);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function resultText(output) {
|
|
26
|
+
return output
|
|
27
|
+
.split('\n')
|
|
28
|
+
.map((line) => {
|
|
29
|
+
try {
|
|
30
|
+
const event = JSON.parse(line);
|
|
31
|
+
return typeof event?.part?.text === 'string' ? event.part.text : line;
|
|
32
|
+
} catch {
|
|
33
|
+
return line;
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
.join('\n');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function parseResult(output) {
|
|
40
|
+
const source = [...resultText(output).matchAll(resultPattern)].at(-1)?.[1];
|
|
41
|
+
if (!source) return;
|
|
42
|
+
try {
|
|
43
|
+
const value = JSON.parse(source);
|
|
44
|
+
if (!value || !['passed', 'needs-fix', 'blocked'].includes(value.verdict)) return;
|
|
45
|
+
if (!Array.isArray(value.findings)) return;
|
|
46
|
+
const findings = value.findings.map((finding) => {
|
|
47
|
+
if (!finding || !areas.has(finding.area) || typeof finding.message !== 'string')
|
|
48
|
+
throw new TypeError('Invalid finding');
|
|
49
|
+
const message = finding.message.trim().slice(0, findingLimit);
|
|
50
|
+
if (!message) throw new TypeError('Invalid finding');
|
|
51
|
+
return { area: finding.area, message };
|
|
52
|
+
});
|
|
53
|
+
if (value.verdict === 'passed' && findings.length !== 0) return;
|
|
54
|
+
if (value.verdict === 'needs-fix' && findings.length === 0) return;
|
|
55
|
+
const reason =
|
|
56
|
+
typeof value.reason === 'string' ? value.reason.trim().slice(0, findingLimit) : '';
|
|
57
|
+
if (value.verdict === 'blocked' && !reason) return;
|
|
58
|
+
return { verdict: value.verdict, findings, ...(reason ? { reason } : {}) };
|
|
59
|
+
} catch {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function sha256(path) {
|
|
65
|
+
return createHash('sha256')
|
|
66
|
+
.update(await readFile(path))
|
|
67
|
+
.digest('hex');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function matchesHash(path, expected) {
|
|
71
|
+
return sha256(path)
|
|
72
|
+
.then((actual) => actual === expected)
|
|
73
|
+
.catch(() => false);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function attemptReview(options, agent, attempt) {
|
|
77
|
+
const chunks = [];
|
|
78
|
+
let bytes = 0;
|
|
79
|
+
let lastActivity = Date.now();
|
|
80
|
+
let reason;
|
|
81
|
+
let forceTimer;
|
|
82
|
+
const state = join(options.cwd, '.generation', 'harness-data', 'reviews', agent);
|
|
83
|
+
await mkdir(state, { recursive: true });
|
|
84
|
+
const environment = {
|
|
85
|
+
...options.environment,
|
|
86
|
+
XDG_CONFIG_HOME: join(state, 'config'),
|
|
87
|
+
XDG_DATA_HOME: join(state, 'data'),
|
|
88
|
+
XDG_CACHE_HOME: join(state, 'cache'),
|
|
89
|
+
XDG_STATE_HOME: join(state, 'state'),
|
|
90
|
+
};
|
|
91
|
+
await (options.prepareOpenCodeDependencies ?? prepareOpenCodeDependencies)([
|
|
92
|
+
join(environment.XDG_CONFIG_HOME, 'opencode'),
|
|
93
|
+
]);
|
|
94
|
+
const child = spawn(options.opencode, ['run', '--agent', agent, '--format', 'json', '--auto'], {
|
|
95
|
+
cwd: options.cwd,
|
|
96
|
+
env: environment,
|
|
97
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
98
|
+
});
|
|
99
|
+
const capture =
|
|
100
|
+
environment.INTERMESH_DEBUG_RUNTIME === '1'
|
|
101
|
+
? startRuntimeCapture(
|
|
102
|
+
join(options.cwd, '.generation/runtime-debug', `${agent}-${attempt}`),
|
|
103
|
+
options.secrets,
|
|
104
|
+
)
|
|
105
|
+
: undefined;
|
|
106
|
+
child.stderr.on('data', (chunk) => capture?.stderr(chunk));
|
|
107
|
+
child.once('close', (code, signal) => {
|
|
108
|
+
capture?.finish({ code, signal });
|
|
109
|
+
if (capture?.errors.length) process.stderr.write('Diagnostic capture incomplete\n');
|
|
110
|
+
});
|
|
111
|
+
const stop = (nextReason) => {
|
|
112
|
+
if (reason) return;
|
|
113
|
+
reason = nextReason;
|
|
114
|
+
child.kill('SIGTERM');
|
|
115
|
+
forceTimer = setTimeout(() => child.kill('SIGKILL'), 2_000);
|
|
116
|
+
};
|
|
117
|
+
const cancel = () => stop('cancelled');
|
|
118
|
+
options.signal?.addEventListener('abort', cancel, { once: true });
|
|
119
|
+
if (options.signal?.aborted) cancel();
|
|
120
|
+
const collect = (chunk) => {
|
|
121
|
+
lastActivity = Date.now();
|
|
122
|
+
bytes += chunk.length;
|
|
123
|
+
if (bytes <= outputLimit) chunks.push(chunk);
|
|
124
|
+
else stop('output-limit');
|
|
125
|
+
};
|
|
126
|
+
child.stdout.on('data', collect);
|
|
127
|
+
child.stderr.on('data', collect);
|
|
128
|
+
child.stdin.on('error', () => {});
|
|
129
|
+
child.stdin.end(options.prompt);
|
|
130
|
+
const hardTimer = setTimeout(() => stop('hard-timeout'), options.hardTimeoutMs);
|
|
131
|
+
const idleTimer = setInterval(() => {
|
|
132
|
+
if (Date.now() - lastActivity >= options.idleMs) stop('idle-timeout');
|
|
133
|
+
}, options.pollMs);
|
|
134
|
+
const exit = await new Promise((resolveExit) => {
|
|
135
|
+
child.once('error', () => resolveExit({ code: 1, spawnError: true }));
|
|
136
|
+
child.once('close', (code) => resolveExit({ code: code ?? 1, spawnError: false }));
|
|
137
|
+
}).finally(() => {
|
|
138
|
+
clearTimeout(hardTimer);
|
|
139
|
+
clearInterval(idleTimer);
|
|
140
|
+
if (forceTimer) clearTimeout(forceTimer);
|
|
141
|
+
options.signal?.removeEventListener('abort', cancel);
|
|
142
|
+
});
|
|
143
|
+
const output = redact(Buffer.concat(chunks).toString('utf8'), options.secrets);
|
|
144
|
+
await writeFile(
|
|
145
|
+
join(options.cwd, '.generation', 'reviews', `${agent}-attempt-${attempt}.jsonl`),
|
|
146
|
+
output,
|
|
147
|
+
);
|
|
148
|
+
const result = parseResult(output);
|
|
149
|
+
return {
|
|
150
|
+
exit_code: exit.code,
|
|
151
|
+
reason:
|
|
152
|
+
reason ??
|
|
153
|
+
(exit.spawnError
|
|
154
|
+
? 'spawn-error'
|
|
155
|
+
: exit.code === 0 && result
|
|
156
|
+
? 'completed'
|
|
157
|
+
: 'invalid-result'),
|
|
158
|
+
result,
|
|
159
|
+
report: output.slice(-retainedReportLimit),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function review(options, agent) {
|
|
164
|
+
const started = Date.now();
|
|
165
|
+
for (let attempt = 1; attempt <= 2; attempt += 1) {
|
|
166
|
+
const result = await attemptReview(options, agent, attempt);
|
|
167
|
+
if (result.exit_code === 0 && result.result) {
|
|
168
|
+
return {
|
|
169
|
+
agent,
|
|
170
|
+
...result.result,
|
|
171
|
+
...(result.result.reason ? { blocked_reason: result.result.reason } : {}),
|
|
172
|
+
attempts: attempt,
|
|
173
|
+
reason: result.reason,
|
|
174
|
+
duration_ms: Date.now() - started,
|
|
175
|
+
report: result.report,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
if (attempt === 2)
|
|
179
|
+
return {
|
|
180
|
+
agent,
|
|
181
|
+
verdict: 'blocked',
|
|
182
|
+
findings: [],
|
|
183
|
+
attempts: attempt,
|
|
184
|
+
reason: result.reason,
|
|
185
|
+
duration_ms: Date.now() - started,
|
|
186
|
+
report: result.report,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
throw new Error('Unreachable reviewer state');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export async function runReviewers({
|
|
193
|
+
cwd,
|
|
194
|
+
opencode = 'opencode',
|
|
195
|
+
prompt,
|
|
196
|
+
environment = process.env,
|
|
197
|
+
idleMs = 600_000,
|
|
198
|
+
hardTimeoutMs = 1_200_000,
|
|
199
|
+
pollMs = 1_000,
|
|
200
|
+
signal,
|
|
201
|
+
authority,
|
|
202
|
+
candidate,
|
|
203
|
+
prepareOpenCodeDependencies: prepareDependencies,
|
|
204
|
+
}) {
|
|
205
|
+
const options = {
|
|
206
|
+
cwd: resolve(cwd),
|
|
207
|
+
opencode,
|
|
208
|
+
prompt,
|
|
209
|
+
environment,
|
|
210
|
+
idleMs: positiveInteger(idleMs, 600_000, 'Idle timeout'),
|
|
211
|
+
hardTimeoutMs: positiveInteger(hardTimeoutMs, 1_200_000, 'Hard timeout'),
|
|
212
|
+
pollMs: positiveInteger(pollMs, 1_000, 'Poll interval'),
|
|
213
|
+
signal,
|
|
214
|
+
authority,
|
|
215
|
+
candidate,
|
|
216
|
+
prepareOpenCodeDependencies: prepareDependencies,
|
|
217
|
+
secrets: ['OPENROUTER_API_KEY', 'DEEPSEEK_API_KEY']
|
|
218
|
+
.map((name) => environment[name])
|
|
219
|
+
.filter((value) => typeof value === 'string' && value.length > 0),
|
|
220
|
+
};
|
|
221
|
+
await mkdir(join(options.cwd, '.generation', 'reviews'), { recursive: true });
|
|
222
|
+
const expectedHash = candidate?.sha256;
|
|
223
|
+
const candidatePath = candidate?.path && resolve(options.cwd, candidate.path);
|
|
224
|
+
const candidateWasValid = candidatePath ? await matchesHash(candidatePath, expectedHash) : true;
|
|
225
|
+
const authorityHash = options.authority?.sha256;
|
|
226
|
+
const authorityPath = options.authority?.path && resolve(options.cwd, options.authority.path);
|
|
227
|
+
const authorityWasValid = authorityPath ? await matchesHash(authorityPath, authorityHash) : true;
|
|
228
|
+
const reviews = await Promise.all(agents.map((agent) => review(options, agent)));
|
|
229
|
+
const candidateUnchanged = candidatePath
|
|
230
|
+
? candidateWasValid && (await matchesHash(candidatePath, expectedHash))
|
|
231
|
+
: true;
|
|
232
|
+
const authorityUnchanged = authorityPath
|
|
233
|
+
? authorityWasValid && (await matchesHash(authorityPath, authorityHash))
|
|
234
|
+
: true;
|
|
235
|
+
return {
|
|
236
|
+
passed:
|
|
237
|
+
candidateUnchanged &&
|
|
238
|
+
authorityUnchanged &&
|
|
239
|
+
reviews.every(({ verdict }) => verdict === 'passed'),
|
|
240
|
+
candidate_unchanged: candidateUnchanged,
|
|
241
|
+
authority_unchanged: authorityUnchanged,
|
|
242
|
+
reviews,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function main() {
|
|
247
|
+
const [verificationPath, reportPath] = process.argv.slice(2);
|
|
248
|
+
if (!verificationPath || !reportPath)
|
|
249
|
+
throw new TypeError('Expected verification and report paths');
|
|
250
|
+
const verification = JSON.parse(await readFile(verificationPath, 'utf8'));
|
|
251
|
+
if (
|
|
252
|
+
verification.passed !== true ||
|
|
253
|
+
typeof verification.tarball !== 'string' ||
|
|
254
|
+
typeof verification.tarball_sha256 !== 'string'
|
|
255
|
+
)
|
|
256
|
+
throw new TypeError('Candidate verification is invalid');
|
|
257
|
+
const tarball = join(dirname(verificationPath), verification.tarball);
|
|
258
|
+
const approvedProposal = await readFile('.generation/approved-proposal.json', 'utf8')
|
|
259
|
+
.then(() => ' Read .generation/approved-proposal.json as the exact human-approved authority.')
|
|
260
|
+
.catch(() => '');
|
|
261
|
+
const reviewContext = await readFile('.generation/review-context.json', 'utf8').catch(
|
|
262
|
+
() => '{"mode":"full"}',
|
|
263
|
+
);
|
|
264
|
+
const prompt = `Review the exact candidate tarball at ${tarball} with SHA-256 ${verification.tarball_sha256}. Its submitted package files are available read-only at package/. Read inputs/run-request.json and its original source paths.${approvedProposal} Read the passing verification receipt at ${verificationPath}; accept its build, tests, packaging, install, help and structural results without repeating them. Review context: ${reviewContext} If mode is focused, verify every outstanding finding and changed path plus concrete affected dependencies; otherwise perform one full API fidelity, business meaning and safety review. Target three minutes and execute only focused checks for named suspected mismatches. Report insufficient evidence as blocked. Do not change the submitted package. Return findings and evidence, then end with exactly one line: REVIEW_RESULT: {"verdict":"passed","findings":[]}. Verdict may be passed, needs-fix, or blocked. Finding area must be cli, skill, cross-cutting, or unknown. needs-fix requires findings; blocked requires a reason.`;
|
|
265
|
+
const controller = new AbortController();
|
|
266
|
+
const cancel = () => controller.abort();
|
|
267
|
+
process.once('SIGINT', cancel);
|
|
268
|
+
process.once('SIGTERM', cancel);
|
|
269
|
+
try {
|
|
270
|
+
const result = await runReviewers({
|
|
271
|
+
cwd: process.cwd(),
|
|
272
|
+
opencode: process.env.OPENCODE_BIN ?? 'opencode',
|
|
273
|
+
prompt,
|
|
274
|
+
idleMs: process.env.REVIEW_IDLE_MS,
|
|
275
|
+
hardTimeoutMs: process.env.REVIEW_HARD_TIMEOUT_MS,
|
|
276
|
+
pollMs: process.env.REVIEW_POLL_MS,
|
|
277
|
+
signal: controller.signal,
|
|
278
|
+
candidate: { path: tarball, sha256: verification.tarball_sha256 },
|
|
279
|
+
...(approvedProposal
|
|
280
|
+
? {
|
|
281
|
+
authority: {
|
|
282
|
+
path: '.generation/approved-proposal.json',
|
|
283
|
+
sha256:
|
|
284
|
+
process.env.INTERMESH_APPROVED_PROPOSAL_HASH ??
|
|
285
|
+
(await sha256('.generation/approved-proposal.json')),
|
|
286
|
+
},
|
|
287
|
+
}
|
|
288
|
+
: {}),
|
|
289
|
+
});
|
|
290
|
+
await writeFile(
|
|
291
|
+
reportPath,
|
|
292
|
+
JSON.stringify({ ...result, candidate_sha256: verification.tarball_sha256 }, null, 2),
|
|
293
|
+
{ flag: 'wx' },
|
|
294
|
+
);
|
|
295
|
+
if (!result.passed) process.exitCode = 1;
|
|
296
|
+
} finally {
|
|
297
|
+
process.off('SIGINT', cancel);
|
|
298
|
+
process.off('SIGTERM', cancel);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (
|
|
303
|
+
globalThis.__INTERMESH_BUNDLED__ !== true &&
|
|
304
|
+
process.argv[1] === new URL(import.meta.url).pathname
|
|
305
|
+
)
|
|
306
|
+
main();
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
4
|
+
|
|
5
|
+
const limit = 256 * 1024;
|
|
6
|
+
|
|
7
|
+
export function startRuntimeCapture(directory, secrets) {
|
|
8
|
+
const errors = [];
|
|
9
|
+
const startedAt = new Date().toISOString();
|
|
10
|
+
const decoder = new StringDecoder('utf8');
|
|
11
|
+
let pending = '';
|
|
12
|
+
let dropping = false;
|
|
13
|
+
let tail = '';
|
|
14
|
+
let truncated = false;
|
|
15
|
+
let resourceBytes = 0;
|
|
16
|
+
let finished = false;
|
|
17
|
+
const attempt = (operation) => {
|
|
18
|
+
try {
|
|
19
|
+
operation();
|
|
20
|
+
} catch {
|
|
21
|
+
if (!errors.includes('Diagnostic capture incomplete'))
|
|
22
|
+
errors.push('Diagnostic capture incomplete');
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
const redact = (text) => {
|
|
26
|
+
for (const secret of secrets.filter(Boolean)) text = text.replaceAll(secret, '[redacted]');
|
|
27
|
+
return text
|
|
28
|
+
.replace(/Bearer\s+[^\s"']+/giu, 'Bearer [redacted]')
|
|
29
|
+
.replace(/((?:api[_-]?key|token|secret|password)\s*[=:]\s*)[^\s,"']+/giu, '$1[redacted]');
|
|
30
|
+
};
|
|
31
|
+
const retain = (line) => {
|
|
32
|
+
tail += redact(line);
|
|
33
|
+
if (Buffer.byteLength(tail) > limit) {
|
|
34
|
+
tail = Buffer.from(tail).subarray(-limit).toString('utf8');
|
|
35
|
+
tail = tail.slice(tail.indexOf('\n') + 1);
|
|
36
|
+
truncated = true;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
const consume = (text) => {
|
|
40
|
+
for (const fragment of text.split(/(?<=\n)/u)) {
|
|
41
|
+
const ended = fragment.endsWith('\n');
|
|
42
|
+
if (!dropping) pending += fragment;
|
|
43
|
+
if (Buffer.byteLength(pending) > 64 * 1024) {
|
|
44
|
+
pending = '';
|
|
45
|
+
dropping = true;
|
|
46
|
+
truncated = true;
|
|
47
|
+
}
|
|
48
|
+
if (ended) {
|
|
49
|
+
retain(dropping ? '[oversized stderr line omitted]\n' : pending);
|
|
50
|
+
pending = '';
|
|
51
|
+
dropping = false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
const sample = () => {
|
|
56
|
+
attempt(() => writeFileSync(join(directory, 'opencode-stderr.log'), tail, { mode: 0o600 }));
|
|
57
|
+
const counters = {};
|
|
58
|
+
for (const name of [
|
|
59
|
+
'memory.current',
|
|
60
|
+
'memory.peak',
|
|
61
|
+
'memory.max',
|
|
62
|
+
'memory.events',
|
|
63
|
+
'pids.current',
|
|
64
|
+
'pids.max',
|
|
65
|
+
'pids.events',
|
|
66
|
+
'cpu.stat',
|
|
67
|
+
]) {
|
|
68
|
+
try {
|
|
69
|
+
const value = readFileSync(`/sys/fs/cgroup/${name}`, 'utf8').trim();
|
|
70
|
+
counters[name] = /^(?:[a-z_]+ )?(?:\d+|max)(?:\n[a-z_]+ \d+)*$/u.test(value) ? value : null;
|
|
71
|
+
} catch {
|
|
72
|
+
counters[name] = null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const line = `${JSON.stringify({ at: new Date().toISOString(), counters })}\n`;
|
|
76
|
+
if (resourceBytes + Buffer.byteLength(line) <= limit) {
|
|
77
|
+
attempt(() => appendFileSync(join(directory, 'resources.jsonl'), line, { mode: 0o600 }));
|
|
78
|
+
resourceBytes += Buffer.byteLength(line);
|
|
79
|
+
}
|
|
80
|
+
return counters;
|
|
81
|
+
};
|
|
82
|
+
attempt(() => mkdirSync(directory, { recursive: true, mode: 0o700 }));
|
|
83
|
+
sample();
|
|
84
|
+
const timer = setInterval(sample, 5000);
|
|
85
|
+
timer.unref();
|
|
86
|
+
return {
|
|
87
|
+
errors,
|
|
88
|
+
stderr(chunk) {
|
|
89
|
+
if (!finished) consume(typeof chunk === 'string' ? chunk : decoder.write(chunk));
|
|
90
|
+
},
|
|
91
|
+
finish(exit) {
|
|
92
|
+
if (finished) return;
|
|
93
|
+
finished = true;
|
|
94
|
+
clearInterval(timer);
|
|
95
|
+
consume(decoder.end());
|
|
96
|
+
if (pending) retain(pending);
|
|
97
|
+
const counters = sample();
|
|
98
|
+
attempt(() => writeFileSync(join(directory, 'opencode-stderr.log'), tail, { mode: 0o600 }));
|
|
99
|
+
attempt(() =>
|
|
100
|
+
writeFileSync(
|
|
101
|
+
join(directory, 'process.json'),
|
|
102
|
+
JSON.stringify(
|
|
103
|
+
{
|
|
104
|
+
started_at: startedAt,
|
|
105
|
+
finished_at: new Date().toISOString(),
|
|
106
|
+
...exit,
|
|
107
|
+
truncated,
|
|
108
|
+
final_counters: counters,
|
|
109
|
+
capture_errors: errors,
|
|
110
|
+
},
|
|
111
|
+
null,
|
|
112
|
+
2,
|
|
113
|
+
),
|
|
114
|
+
{ mode: 0o600 },
|
|
115
|
+
),
|
|
116
|
+
);
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
|
|
3
|
+
const NAMES = new Set([
|
|
4
|
+
'starter_verification',
|
|
5
|
+
'source_analysis',
|
|
6
|
+
'source_validation',
|
|
7
|
+
'lead_input_preparation',
|
|
8
|
+
'lead_execution',
|
|
9
|
+
'generator_input_preparation',
|
|
10
|
+
'generator_execution',
|
|
11
|
+
'preliminary_help',
|
|
12
|
+
'skill_finalization',
|
|
13
|
+
'candidate_verification',
|
|
14
|
+
'independent_review',
|
|
15
|
+
]);
|
|
16
|
+
const OUTCOMES = new Set(['passed', 'failed', 'cancelled', 'timed_out']);
|
|
17
|
+
|
|
18
|
+
async function read(path) {
|
|
19
|
+
try {
|
|
20
|
+
const value = JSON.parse(await readFile(path, 'utf8'));
|
|
21
|
+
if (!value || !Array.isArray(value.stages)) throw new TypeError('Stage timings are invalid');
|
|
22
|
+
return value;
|
|
23
|
+
} catch (error) {
|
|
24
|
+
if (error && error.code === 'ENOENT') return { schema_version: '1.0.0', stages: [] };
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function update(path, stage, outcome) {
|
|
30
|
+
if (!NAMES.has(stage)) throw new TypeError('Stage name is invalid');
|
|
31
|
+
const value = await read(path);
|
|
32
|
+
const now = new Date();
|
|
33
|
+
if (!outcome) {
|
|
34
|
+
if (value.stages.some((item) => item.name === stage && !item.finished_at))
|
|
35
|
+
throw new TypeError('Stage is already running');
|
|
36
|
+
value.stages.push({ name: stage, started_at: now.toISOString() });
|
|
37
|
+
} else {
|
|
38
|
+
if (!OUTCOMES.has(outcome)) throw new TypeError('Stage outcome is invalid');
|
|
39
|
+
const item = [...value.stages]
|
|
40
|
+
.reverse()
|
|
41
|
+
.find((entry) => entry.name === stage && !entry.finished_at);
|
|
42
|
+
if (!item) throw new TypeError('Stage has not started');
|
|
43
|
+
item.finished_at = now.toISOString();
|
|
44
|
+
item.duration_ms = Math.max(0, now.getTime() - Date.parse(item.started_at));
|
|
45
|
+
item.outcome = outcome;
|
|
46
|
+
}
|
|
47
|
+
await writeFile(path, `${JSON.stringify(value, null, 2)}\n`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const [command, path, stage, outcome] = process.argv.slice(2);
|
|
51
|
+
if (!path || !stage || (command !== 'start' && command !== 'finish'))
|
|
52
|
+
throw new TypeError('Expected start|finish, timings path and stage');
|
|
53
|
+
await update(path, stage, command === 'finish' ? outcome : undefined);
|