@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,519 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { copyFile, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { dirname, join, resolve } from 'node:path';
|
|
5
|
+
|
|
6
|
+
const findingLimit = 8_000;
|
|
7
|
+
const maximumFindings = 20;
|
|
8
|
+
const excludedDirectories = new Set(['node_modules', 'dist']);
|
|
9
|
+
const excludedFiles = new Set(['.DS_Store']);
|
|
10
|
+
|
|
11
|
+
function record(value, name) {
|
|
12
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
13
|
+
throw new TypeError(`${name} is invalid`);
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function safeText(value, name) {
|
|
18
|
+
if (typeof value !== 'string' || value.trim() === '') throw new TypeError(`${name} is invalid`);
|
|
19
|
+
return value.trim().slice(0, findingLimit);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function secrets(environment) {
|
|
23
|
+
return Object.entries(environment)
|
|
24
|
+
.filter(
|
|
25
|
+
([name, value]) =>
|
|
26
|
+
typeof value === 'string' &&
|
|
27
|
+
value.length >= 8 &&
|
|
28
|
+
/(KEY|TOKEN|SECRET|PASSWORD)/iu.test(name),
|
|
29
|
+
)
|
|
30
|
+
.map(([, value]) => value);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function sanitize(value, environment) {
|
|
34
|
+
let output = String(value);
|
|
35
|
+
for (const secret of secrets(environment)) output = output.replaceAll(secret, '[redacted]');
|
|
36
|
+
return output
|
|
37
|
+
.replace(/Bearer\s+[^\s"']+/giu, 'Bearer [redacted]')
|
|
38
|
+
.replace(/((?:api[_-]?key|token|secret|password)\s*[=:]\s*)[^\s,"']+/giu, '$1[redacted]')
|
|
39
|
+
.slice(-findingLimit);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function sha256(path) {
|
|
43
|
+
return createHash('sha256')
|
|
44
|
+
.update(await readFile(path))
|
|
45
|
+
.digest('hex');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function filesBelow(directory, prefix = '', ignoredDirectories = new Set()) {
|
|
49
|
+
const result = [];
|
|
50
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
51
|
+
const path = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
52
|
+
if (entry.isDirectory() && !ignoredDirectories.has(entry.name))
|
|
53
|
+
result.push(...(await filesBelow(join(directory, entry.name), path, ignoredDirectories)));
|
|
54
|
+
else if (entry.isFile()) result.push(path);
|
|
55
|
+
else if (entry.isDirectory()) continue;
|
|
56
|
+
else throw new TypeError(`Unsupported package file: ${path}`);
|
|
57
|
+
}
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function sourceTreeHash(packageDirectory, protectedPaths) {
|
|
62
|
+
const files = await sourceTreeSnapshot(packageDirectory, protectedPaths);
|
|
63
|
+
return snapshotHash(files);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function snapshotHash(files) {
|
|
67
|
+
const hash = createHash('sha256');
|
|
68
|
+
for (const [path, fileHash] of files)
|
|
69
|
+
hash.update(path).update('\0').update(fileHash).update('\0');
|
|
70
|
+
return hash.digest('hex');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function sourceTreeSnapshot(packageDirectory, protectedPaths) {
|
|
74
|
+
const protectedSet = new Set(protectedPaths);
|
|
75
|
+
const paths = (await filesBelow(packageDirectory, '', excludedDirectories))
|
|
76
|
+
.filter((path) => {
|
|
77
|
+
const parts = path.split('/');
|
|
78
|
+
return !protectedSet.has(path) && !excludedFiles.has(parts.at(-1)) && !path.endsWith('.tgz');
|
|
79
|
+
})
|
|
80
|
+
.sort();
|
|
81
|
+
return new Map(
|
|
82
|
+
await Promise.all(
|
|
83
|
+
paths.map(async (path) => [path, await sha256(join(packageDirectory, path))]),
|
|
84
|
+
),
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function changedPaths(before, after) {
|
|
89
|
+
return [...new Set([...before.keys(), ...after.keys()])]
|
|
90
|
+
.filter((path) => before.get(path) !== after.get(path))
|
|
91
|
+
.sort();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function verifyInputIntegrity(inputDirectory, expected) {
|
|
95
|
+
for (const [path, expectedHash] of Object.entries(record(expected, 'Input manifest'))) {
|
|
96
|
+
const pathParts = path.split('/');
|
|
97
|
+
if (
|
|
98
|
+
!/^[a-zA-Z0-9._-]+(?:\/[a-zA-Z0-9._-]+)*$/u.test(path) ||
|
|
99
|
+
pathParts.includes('.') ||
|
|
100
|
+
pathParts.includes('..') ||
|
|
101
|
+
typeof expectedHash !== 'string' ||
|
|
102
|
+
!/^[a-f0-9]{64}$/u.test(expectedHash)
|
|
103
|
+
)
|
|
104
|
+
throw new TypeError('Input manifest is invalid');
|
|
105
|
+
const actual = await sha256(resolve(inputDirectory, path)).catch(() => '');
|
|
106
|
+
if (actual !== expectedHash) throw new TypeError(`Generation input changed: ${path}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function protectedDirectories(paths) {
|
|
111
|
+
return [
|
|
112
|
+
'src/auth',
|
|
113
|
+
'src/shared',
|
|
114
|
+
'src/lifecycle',
|
|
115
|
+
...new Set(
|
|
116
|
+
paths
|
|
117
|
+
.filter((path) => /^skills\/[a-z0-9-]+-cli-management\//u.test(path))
|
|
118
|
+
.map((path) => path.split('/').slice(0, 2).join('/')),
|
|
119
|
+
),
|
|
120
|
+
];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function verifyFoundation(packageDirectory, foundation) {
|
|
124
|
+
const paths = Object.keys(foundation).sort();
|
|
125
|
+
const directories = protectedDirectories(paths);
|
|
126
|
+
const actual = (
|
|
127
|
+
await Promise.all(
|
|
128
|
+
directories.map((path) => filesBelow(join(packageDirectory, path), path).catch(() => [])),
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
.flat()
|
|
132
|
+
.sort();
|
|
133
|
+
const expected = paths.filter((path) =>
|
|
134
|
+
directories.some((directory) => path.startsWith(`${directory}/`)),
|
|
135
|
+
);
|
|
136
|
+
if (actual.join('\0') !== expected.join('\0')) return false;
|
|
137
|
+
for (const [path, expectedHash] of Object.entries(foundation)) {
|
|
138
|
+
if ((await sha256(join(packageDirectory, path)).catch(() => '')) !== expectedHash) return false;
|
|
139
|
+
}
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function restoreFoundation(packageDirectory, backupDirectory, foundation) {
|
|
144
|
+
const paths = Object.keys(foundation).sort();
|
|
145
|
+
for (const directory of protectedDirectories(paths)) {
|
|
146
|
+
await rm(join(packageDirectory, directory), { recursive: true, force: true });
|
|
147
|
+
}
|
|
148
|
+
for (const path of paths) {
|
|
149
|
+
const backup = join(backupDirectory, path);
|
|
150
|
+
if ((await sha256(backup).catch(() => '')) !== foundation[path])
|
|
151
|
+
throw new TypeError(`Protected foundation backup is invalid: ${path}`);
|
|
152
|
+
await mkdir(dirname(join(packageDirectory, path)), { recursive: true });
|
|
153
|
+
await copyFile(backup, join(packageDirectory, path));
|
|
154
|
+
}
|
|
155
|
+
if (!(await verifyFoundation(packageDirectory, foundation)))
|
|
156
|
+
throw new TypeError('Protected foundation could not be restored');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function ensureFoundation(packageDirectory, backupDirectory, foundation) {
|
|
160
|
+
if (await verifyFoundation(packageDirectory, foundation)) return false;
|
|
161
|
+
await restoreFoundation(packageDirectory, backupDirectory, foundation);
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function run(command, args, options) {
|
|
166
|
+
const remaining = options.deadline === undefined ? undefined : options.deadline - Date.now();
|
|
167
|
+
if (remaining !== undefined && remaining <= 0) throw new TypeError('Generation deadline reached');
|
|
168
|
+
return new Promise((resolvePromise, reject) => {
|
|
169
|
+
const child = spawn(command, args, {
|
|
170
|
+
cwd: options.cwd,
|
|
171
|
+
env: options.environment,
|
|
172
|
+
signal: options.signal,
|
|
173
|
+
stdio: 'inherit',
|
|
174
|
+
});
|
|
175
|
+
let forceTimer;
|
|
176
|
+
const timer =
|
|
177
|
+
remaining === undefined
|
|
178
|
+
? undefined
|
|
179
|
+
: setTimeout(() => {
|
|
180
|
+
child.kill('SIGTERM');
|
|
181
|
+
forceTimer = setTimeout(() => child.kill('SIGKILL'), 2_000);
|
|
182
|
+
}, remaining);
|
|
183
|
+
child.once('error', (error) => {
|
|
184
|
+
clearTimeout(timer);
|
|
185
|
+
if (forceTimer) clearTimeout(forceTimer);
|
|
186
|
+
reject(error);
|
|
187
|
+
});
|
|
188
|
+
child.once('close', (code, signal) => {
|
|
189
|
+
clearTimeout(timer);
|
|
190
|
+
if (forceTimer) clearTimeout(forceTimer);
|
|
191
|
+
resolvePromise({ code: code ?? 1, signal });
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function stage(options, name, operation) {
|
|
197
|
+
await writeFile(join(options.notes, 'current-stage'), name);
|
|
198
|
+
await run(process.execPath, [options.stageTimings, 'start', options.timingPath, name], options);
|
|
199
|
+
try {
|
|
200
|
+
const result = await operation();
|
|
201
|
+
await run(
|
|
202
|
+
process.execPath,
|
|
203
|
+
[
|
|
204
|
+
options.stageTimings,
|
|
205
|
+
'finish',
|
|
206
|
+
options.timingPath,
|
|
207
|
+
name,
|
|
208
|
+
result.code === 0 ? 'passed' : 'failed',
|
|
209
|
+
],
|
|
210
|
+
options,
|
|
211
|
+
);
|
|
212
|
+
return result;
|
|
213
|
+
} catch (error) {
|
|
214
|
+
await run(
|
|
215
|
+
process.execPath,
|
|
216
|
+
[options.stageTimings, 'finish', options.timingPath, name, 'failed'],
|
|
217
|
+
options,
|
|
218
|
+
).catch(() => {});
|
|
219
|
+
throw error;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function readJson(path, name) {
|
|
224
|
+
return record(JSON.parse(await readFile(path, 'utf8')), name);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function appendHistory(options, attempt) {
|
|
228
|
+
options.history.attempts.push(attempt);
|
|
229
|
+
await writeFile(options.historyPath, `${JSON.stringify(options.history, null, 2)}\n`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function protectFoundation(options, cycle, stage) {
|
|
233
|
+
if (
|
|
234
|
+
!(await ensureFoundation(options.packageDirectory, options.backupDirectory, options.foundation))
|
|
235
|
+
)
|
|
236
|
+
return;
|
|
237
|
+
options.history.foundation_restorations.push({ cycle, stage });
|
|
238
|
+
await writeFile(options.historyPath, `${JSON.stringify(options.history, null, 2)}\n`);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function repair(options, source, findings) {
|
|
242
|
+
const attempt = options.history.attempts.length + 1;
|
|
243
|
+
const beforeFiles = await sourceTreeSnapshot(
|
|
244
|
+
options.packageDirectory,
|
|
245
|
+
Object.keys(options.foundation),
|
|
246
|
+
);
|
|
247
|
+
const before = snapshotHash(beforeFiles);
|
|
248
|
+
const request = {
|
|
249
|
+
attempt,
|
|
250
|
+
source,
|
|
251
|
+
findings: findings
|
|
252
|
+
.slice(0, maximumFindings)
|
|
253
|
+
.map((finding) => sanitize(finding, options.environment)),
|
|
254
|
+
};
|
|
255
|
+
await writeFile(options.repairRequestPath, `${JSON.stringify(request, null, 2)}\n`);
|
|
256
|
+
const started = Date.now();
|
|
257
|
+
const result = await run(
|
|
258
|
+
options.opencode,
|
|
259
|
+
[
|
|
260
|
+
'run',
|
|
261
|
+
'--session',
|
|
262
|
+
options.leadSessionId,
|
|
263
|
+
'--agent',
|
|
264
|
+
'generation-builder',
|
|
265
|
+
'--format',
|
|
266
|
+
'json',
|
|
267
|
+
'--auto',
|
|
268
|
+
'Read .generation/repair-request.json. Correct each finding across affected implementation, focused tests, README and Skills. Check sibling response variants for the same requirement, run affected focused checks, and return control.',
|
|
269
|
+
],
|
|
270
|
+
options,
|
|
271
|
+
);
|
|
272
|
+
const foundationRestored = await ensureFoundation(
|
|
273
|
+
options.packageDirectory,
|
|
274
|
+
options.backupDirectory,
|
|
275
|
+
options.foundation,
|
|
276
|
+
);
|
|
277
|
+
const afterFiles = await sourceTreeSnapshot(
|
|
278
|
+
options.packageDirectory,
|
|
279
|
+
Object.keys(options.foundation),
|
|
280
|
+
);
|
|
281
|
+
const after = snapshotHash(afterFiles);
|
|
282
|
+
const changed = changedPaths(beforeFiles, afterFiles);
|
|
283
|
+
for (const path of changed) options.changedSinceReview.add(path);
|
|
284
|
+
const outcome =
|
|
285
|
+
result.code !== 0 ? 'agent_failed' : before === after ? 'no_progress' : 'repaired';
|
|
286
|
+
await appendHistory(options, {
|
|
287
|
+
attempt,
|
|
288
|
+
source,
|
|
289
|
+
findings: request.findings,
|
|
290
|
+
source_hash_before: before,
|
|
291
|
+
source_hash_after: after,
|
|
292
|
+
changed_paths: changed,
|
|
293
|
+
duration_ms: Date.now() - started,
|
|
294
|
+
foundation_restored: foundationRestored,
|
|
295
|
+
outcome,
|
|
296
|
+
});
|
|
297
|
+
if (result.code !== 0) throw new TypeError('OpenCode repair session failed');
|
|
298
|
+
if (outcome === 'no_progress') throw new TypeError('Repair made no relevant source change');
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function reviewerFindings(summary) {
|
|
302
|
+
if (!Array.isArray(summary.reviews)) throw new TypeError('Reviewer summary is invalid');
|
|
303
|
+
const findings = [];
|
|
304
|
+
let blocked = false;
|
|
305
|
+
for (const reviewValue of summary.reviews) {
|
|
306
|
+
const review = record(reviewValue, 'Reviewer result');
|
|
307
|
+
if (review.verdict === 'blocked') blocked = true;
|
|
308
|
+
if (!Array.isArray(review.findings)) throw new TypeError('Reviewer findings are invalid');
|
|
309
|
+
for (const findingValue of review.findings) {
|
|
310
|
+
const finding = record(findingValue, 'Reviewer finding');
|
|
311
|
+
findings.push(
|
|
312
|
+
`[${safeText(finding.area, 'Reviewer finding area')}] ${safeText(finding.message, 'Reviewer finding message')}`,
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return { blocked, findings };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async function reviewContext(options) {
|
|
320
|
+
if (!options.reviewAnchor) return { mode: 'full' };
|
|
321
|
+
const anchor = options.reviewAnchor;
|
|
322
|
+
const summary = await readJson(anchor.summary_path, 'Anchored reviewer summary').catch(
|
|
323
|
+
() => undefined,
|
|
324
|
+
);
|
|
325
|
+
if (
|
|
326
|
+
!summary ||
|
|
327
|
+
!Array.isArray(summary.reviews) ||
|
|
328
|
+
summary.reviews.length === 0 ||
|
|
329
|
+
JSON.stringify(summary.reviews).length > 100_000 ||
|
|
330
|
+
summary.candidate_sha256 !== anchor.candidate_sha256 ||
|
|
331
|
+
(await sha256(anchor.summary_path).catch(() => '')) !== anchor.summary_sha256
|
|
332
|
+
)
|
|
333
|
+
return { mode: 'full' };
|
|
334
|
+
const recorded = new Set(
|
|
335
|
+
options.history.attempts
|
|
336
|
+
.slice(anchor.attempt_count)
|
|
337
|
+
.flatMap((attempt) => attempt.changed_paths ?? []),
|
|
338
|
+
);
|
|
339
|
+
const changed = [...options.changedSinceReview].sort();
|
|
340
|
+
if (changed.length > 1_000 || changed.join('\0') !== [...recorded].sort().join('\0'))
|
|
341
|
+
return { mode: 'full' };
|
|
342
|
+
return {
|
|
343
|
+
mode: 'focused',
|
|
344
|
+
anchor,
|
|
345
|
+
prior_reviews: summary.reviews,
|
|
346
|
+
outstanding_findings: options.outstandingFindings,
|
|
347
|
+
changed_paths: changed,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async function promoteCycle(options, cycle) {
|
|
352
|
+
const receiptPath = join(cycle, 'candidate-verification.json');
|
|
353
|
+
const receipt = await readJson(receiptPath, 'Candidate verification');
|
|
354
|
+
const review = await readJson(join(cycle, 'reviewer-summary.json'), 'Reviewer summary');
|
|
355
|
+
if (receipt.passed !== true || review.passed !== true || review.candidate_unchanged !== true)
|
|
356
|
+
throw new TypeError('Passing candidate cycle is invalid');
|
|
357
|
+
if (receipt.tarball !== 'candidate.tgz') throw new TypeError('Candidate tarball is invalid');
|
|
358
|
+
const tarballPath = join(cycle, receipt.tarball);
|
|
359
|
+
const tarballHash = await sha256(tarballPath);
|
|
360
|
+
if (tarballHash !== receipt.tarball_sha256)
|
|
361
|
+
throw new TypeError('Reviewed candidate tarball hash does not match');
|
|
362
|
+
await copyFile(receiptPath, join(options.notes, 'candidate-verification.json'));
|
|
363
|
+
await copyFile(
|
|
364
|
+
join(cycle, 'reviewer-summary.json'),
|
|
365
|
+
join(options.notes, 'reviewer-summary.json'),
|
|
366
|
+
);
|
|
367
|
+
await copyFile(tarballPath, join(options.notes, 'candidate.tgz'));
|
|
368
|
+
if ((await sha256(join(options.notes, 'candidate.tgz'))) !== tarballHash)
|
|
369
|
+
throw new TypeError('Promoted candidate tarball hash does not match');
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export async function runRepairLoop(input) {
|
|
373
|
+
const notes = resolve(input.cwd, '.generation');
|
|
374
|
+
const foundation = record(
|
|
375
|
+
JSON.parse(await readFile(input.foundationManifest, 'utf8')),
|
|
376
|
+
'Foundation manifest',
|
|
377
|
+
);
|
|
378
|
+
for (const [path, hash] of Object.entries(foundation)) {
|
|
379
|
+
if (typeof path !== 'string' || typeof hash !== 'string' || !/^[a-f0-9]{64}$/u.test(hash))
|
|
380
|
+
throw new TypeError('Foundation manifest is invalid');
|
|
381
|
+
}
|
|
382
|
+
const options = {
|
|
383
|
+
...input,
|
|
384
|
+
notes,
|
|
385
|
+
packageDirectory: resolve(input.cwd, input.packageDirectory),
|
|
386
|
+
backupDirectory: resolve(input.backupDirectory),
|
|
387
|
+
cyclesDirectory: join(notes, 'cycles'),
|
|
388
|
+
historyPath: join(notes, 'repair-history.json'),
|
|
389
|
+
repairRequestPath: join(notes, 'repair-request.json'),
|
|
390
|
+
timingPath: join(notes, 'stage-timings.json'),
|
|
391
|
+
foundation,
|
|
392
|
+
history: { schema_version: '1.0.0', attempts: [], foundation_restorations: [] },
|
|
393
|
+
changedSinceReview: new Set(),
|
|
394
|
+
};
|
|
395
|
+
await mkdir(options.cyclesDirectory, { recursive: true });
|
|
396
|
+
await writeFile(options.historyPath, `${JSON.stringify(options.history, null, 2)}\n`, {
|
|
397
|
+
flag: 'wx',
|
|
398
|
+
});
|
|
399
|
+
let lastFinding;
|
|
400
|
+
let repeatedFindingCount = 0;
|
|
401
|
+
for (let cycleNumber = 1; ; cycleNumber += 1) {
|
|
402
|
+
if (options.deadline !== undefined && Date.now() >= options.deadline)
|
|
403
|
+
throw new TypeError('Generation deadline reached');
|
|
404
|
+
await verifyInputIntegrity(join(options.cwd, 'inputs'), options.inputHashes);
|
|
405
|
+
await protectFoundation(options, cycleNumber, 'before_verification');
|
|
406
|
+
const cycle = join(options.cyclesDirectory, String(cycleNumber));
|
|
407
|
+
await mkdir(cycle);
|
|
408
|
+
const verificationPath = join(cycle, 'candidate-verification.json');
|
|
409
|
+
const verificationEnvironment = { ...options.environment };
|
|
410
|
+
delete verificationEnvironment.DEEPSEEK_API_KEY;
|
|
411
|
+
delete verificationEnvironment.OPENROUTER_API_KEY;
|
|
412
|
+
const verification = await stage(options, 'candidate_verification', () =>
|
|
413
|
+
run(
|
|
414
|
+
process.execPath,
|
|
415
|
+
[options.verifier, input.packageDirectory, input.requestPath, verificationPath],
|
|
416
|
+
{ ...options, environment: verificationEnvironment },
|
|
417
|
+
),
|
|
418
|
+
);
|
|
419
|
+
if (verification.code !== 0) {
|
|
420
|
+
const result = await readJson(verificationPath, 'Candidate verification failure');
|
|
421
|
+
const finding = safeText(result.finding, 'Candidate verification finding');
|
|
422
|
+
repeatedFindingCount = finding === lastFinding ? repeatedFindingCount + 1 : 1;
|
|
423
|
+
lastFinding = finding;
|
|
424
|
+
if (repeatedFindingCount >= 3)
|
|
425
|
+
throw new TypeError('The same candidate verification finding survived two repairs');
|
|
426
|
+
await repair(options, 'candidate_verification', [finding]);
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
lastFinding = undefined;
|
|
430
|
+
repeatedFindingCount = 0;
|
|
431
|
+
const reviewPath = join(cycle, 'reviewer-summary.json');
|
|
432
|
+
const reviewContextPath = join(options.notes, 'review-context.json');
|
|
433
|
+
await writeFile(
|
|
434
|
+
reviewContextPath,
|
|
435
|
+
`${JSON.stringify(await reviewContext(options), null, 2)}\n`,
|
|
436
|
+
);
|
|
437
|
+
const reviewResult = await stage(options, 'independent_review', () =>
|
|
438
|
+
run(process.execPath, [options.reviewers, verificationPath, reviewPath], options),
|
|
439
|
+
);
|
|
440
|
+
const summary = await readJson(reviewPath, 'Reviewer summary');
|
|
441
|
+
const reviewed = reviewerFindings(summary);
|
|
442
|
+
const receipt = await readJson(verificationPath, 'Candidate verification');
|
|
443
|
+
if (summary.candidate_sha256 !== receipt.tarball_sha256)
|
|
444
|
+
throw new TypeError('Reviewer summary does not match the candidate');
|
|
445
|
+
options.reviewAnchor = {
|
|
446
|
+
cycle: cycleNumber,
|
|
447
|
+
candidate_sha256: receipt.tarball_sha256,
|
|
448
|
+
summary_path: reviewPath,
|
|
449
|
+
summary_sha256: await sha256(reviewPath),
|
|
450
|
+
attempt_count: options.history.attempts.length,
|
|
451
|
+
};
|
|
452
|
+
options.outstandingFindings = reviewed.findings;
|
|
453
|
+
options.changedSinceReview.clear();
|
|
454
|
+
if (reviewed.blocked) throw new TypeError('Independent review is blocked');
|
|
455
|
+
if (reviewResult.code !== 0 || summary.passed !== true) {
|
|
456
|
+
if (reviewed.findings.length === 0) throw new TypeError('Independent review failed');
|
|
457
|
+
await repair(options, 'independent_review', reviewed.findings);
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
await protectFoundation(options, cycleNumber, 'after_review');
|
|
461
|
+
await promoteCycle(options, cycle);
|
|
462
|
+
return options.history;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function argumentsFrom(argv) {
|
|
467
|
+
const values = {};
|
|
468
|
+
for (let index = 0; index < argv.length; index += 2) {
|
|
469
|
+
const name = argv[index];
|
|
470
|
+
const value = argv[index + 1];
|
|
471
|
+
if (!name?.startsWith('--') || value === undefined)
|
|
472
|
+
throw new TypeError('Repair arguments are invalid');
|
|
473
|
+
values[name.slice(2)] = value;
|
|
474
|
+
}
|
|
475
|
+
return values;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (
|
|
479
|
+
globalThis.__INTERMESH_BUNDLED__ !== true &&
|
|
480
|
+
process.argv[1] === new URL(import.meta.url).pathname
|
|
481
|
+
) {
|
|
482
|
+
const values = argumentsFrom(process.argv.slice(2));
|
|
483
|
+
const required = [
|
|
484
|
+
'cwd',
|
|
485
|
+
'package',
|
|
486
|
+
'request',
|
|
487
|
+
'verifier',
|
|
488
|
+
'reviewers',
|
|
489
|
+
'opencode',
|
|
490
|
+
'lead-session',
|
|
491
|
+
'foundation-backup',
|
|
492
|
+
'foundation-manifest',
|
|
493
|
+
'stage-timings',
|
|
494
|
+
'input-manifest',
|
|
495
|
+
];
|
|
496
|
+
for (const name of required) safeText(values[name], name);
|
|
497
|
+
const deadline = values.deadline === undefined ? undefined : Number(values.deadline);
|
|
498
|
+
if (deadline !== undefined && (!Number.isSafeInteger(deadline) || deadline <= 0))
|
|
499
|
+
throw new TypeError('Deadline is invalid');
|
|
500
|
+
const inputHashes = record(
|
|
501
|
+
JSON.parse(await readFile(values['input-manifest'], 'utf8')),
|
|
502
|
+
'Input manifest',
|
|
503
|
+
);
|
|
504
|
+
await runRepairLoop({
|
|
505
|
+
cwd: resolve(values.cwd),
|
|
506
|
+
packageDirectory: values.package,
|
|
507
|
+
requestPath: values.request,
|
|
508
|
+
verifier: values.verifier,
|
|
509
|
+
reviewers: values.reviewers,
|
|
510
|
+
opencode: values.opencode,
|
|
511
|
+
leadSessionId: values['lead-session'],
|
|
512
|
+
...(deadline === undefined ? {} : { deadline }),
|
|
513
|
+
backupDirectory: values['foundation-backup'],
|
|
514
|
+
foundationManifest: values['foundation-manifest'],
|
|
515
|
+
stageTimings: values['stage-timings'],
|
|
516
|
+
environment: process.env,
|
|
517
|
+
inputHashes,
|
|
518
|
+
});
|
|
519
|
+
}
|