@fro.bot/harness 0.0.0 → 1.15.13

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 ADDED
@@ -0,0 +1,76 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Fro Bot <agent@fro.bot>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ ---
24
+
25
+ This package embeds the integration method from cortexkit/orw and redistributes
26
+ a modified build of anomalyco/opencode. Both upstream projects are MIT-licensed.
27
+
28
+ ---
29
+
30
+ cortexkit/orw (integration method)
31
+ MIT License
32
+ Copyright (c) Cortexkit
33
+ https://github.com/cortexkit/orw
34
+
35
+ Permission is hereby granted, free of charge, to any person obtaining a copy
36
+ of this software and associated documentation files (the "Software"), to deal
37
+ in the Software without restriction, including without limitation the rights
38
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
39
+ copies of the Software, and to permit persons to whom the Software is
40
+ furnished to do so, subject to the following conditions:
41
+
42
+ The above copyright notice and this permission notice shall be included in all
43
+ copies or substantial portions of the Software.
44
+
45
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
46
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
47
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
48
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
49
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
50
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
51
+ SOFTWARE.
52
+
53
+ ---
54
+
55
+ anomalyco/opencode (modified binary distribution)
56
+ MIT License
57
+ Copyright (c) 2024 SST Inc.
58
+ https://github.com/anomalyco/opencode
59
+
60
+ Permission is hereby granted, free of charge, to any person obtaining a copy
61
+ of this software and associated documentation files (the "Software"), to deal
62
+ in the Software without restriction, including without limitation the rights
63
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
64
+ copies of the Software, and to permit persons to whom the Software is
65
+ furnished to do so, subject to the following conditions:
66
+
67
+ The above copyright notice and this permission notice shall be included in all
68
+ copies or substantial portions of the Software.
69
+
70
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
71
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
72
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
73
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
74
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
75
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
76
+ SOFTWARE.
package/dist/cli.d.mts ADDED
@@ -0,0 +1 @@
1
+ export { };
package/dist/cli.mjs ADDED
@@ -0,0 +1,903 @@
1
+ #!/usr/bin/env node
2
+ import { n as getHostPlatformInfo, t as binaryPathInPackage } from "./platform-CKoiiV92.mjs";
3
+ import { createRequire } from "node:module";
4
+ import { execFile, execFileSync, execSync, spawnSync } from "node:child_process";
5
+ import process from "node:process";
6
+ import { copyFileSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync } from "node:fs";
7
+ import os from "node:os";
8
+ import path from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+ import fs from "node:fs/promises";
11
+ import { promisify } from "node:util";
12
+ //#region src/sources.ts
13
+ /**
14
+ * Maps a single integration source input to a typed IntegrationSource.
15
+ *
16
+ * @param input - A PR URL, branch URL, or local branch name from config.
17
+ * @param sourceRepo - The default source repo URL (used for local branch names).
18
+ */
19
+ function parseSource(input, sourceRepo) {
20
+ const value = input.trim();
21
+ if (value.length === 0) throw new Error("Empty integration source in config branches");
22
+ if (!value.startsWith("https://github.com/")) return {
23
+ label: value,
24
+ repo: sourceRepo,
25
+ fetchRef: `refs/heads/${value}`,
26
+ fetch: `refs/remotes/watch/local/${value}`,
27
+ merge: `refs/remotes/watch/local/${value}`
28
+ };
29
+ const parts = new URL(value).pathname.split("/").filter(Boolean);
30
+ const owner = parts[0];
31
+ const repo = parts[1];
32
+ if (owner === void 0 || repo === void 0) throw new Error(`Unsupported GitHub source URL: ${value}`);
33
+ if (parts.length >= 4 && parts[2] === "tree") {
34
+ const branch = decodeURIComponent(parts.slice(3).join("/"));
35
+ const ref = `refs/remotes/watch/${watchSlug(owner, repo)}/${branch}`;
36
+ return {
37
+ label: `${owner}/${repo}:${branch}`,
38
+ repo: `https://github.com/${owner}/${repo}.git`,
39
+ fetchRef: `refs/heads/${branch}`,
40
+ fetch: ref,
41
+ merge: ref
42
+ };
43
+ }
44
+ if (parts.length >= 4 && parts[2] === "pull") {
45
+ const number = parts[3] ?? "";
46
+ if (!/^\d+$/.test(number)) throw new Error(`Unsupported GitHub pull request URL: ${value}`);
47
+ const ref = `refs/remotes/watch/${watchSlug(owner, repo)}/pr-${number}`;
48
+ return {
49
+ label: `${owner}/${repo}#${number}`,
50
+ repo: `https://github.com/${owner}/${repo}.git`,
51
+ fetchRef: `refs/pull/${number}/head`,
52
+ fetch: ref,
53
+ merge: ref
54
+ };
55
+ }
56
+ throw new Error(`Unsupported GitHub integration source URL: ${value}`);
57
+ }
58
+ /**
59
+ * Maps an array of integration source inputs to typed IntegrationSources.
60
+ *
61
+ * @param refs - Array of PR URLs, branch URLs, or local branch names.
62
+ * @param sourceRepo - The default source repo URL (used for local branch names).
63
+ */
64
+ function resolveSources(refs, sourceRepo) {
65
+ return refs.map((input) => parseSource(input, sourceRepo));
66
+ }
67
+ function watchSlug(owner, repo) {
68
+ return `${owner}-${repo}`.replaceAll(/[^\w.-]/g, "-");
69
+ }
70
+ //#endregion
71
+ //#region src/integrate.ts
72
+ const MANIFEST_FILENAME = "provenance.json";
73
+ /**
74
+ * Writes the provenance manifest to the given directory.
75
+ * This is the freeze step — called only after all integration steps succeed.
76
+ */
77
+ async function writeProvenanceManifest(dir, manifest) {
78
+ await fs.mkdir(dir, { recursive: true });
79
+ await fs.writeFile(path.join(dir, MANIFEST_FILENAME), `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
80
+ }
81
+ function integrationBranch(version) {
82
+ return `integrate/v${version}`;
83
+ }
84
+ async function renderPrompt(promptPath, workDir, baseVersion, releaseRepo, sources) {
85
+ const tag = `v${baseVersion}`;
86
+ const branch = integrationBranch(baseVersion);
87
+ const channel = "latest";
88
+ const tpl = await fs.readFile(promptPath, "utf8");
89
+ const vars = {
90
+ repo: workDir,
91
+ tag,
92
+ version: baseVersion,
93
+ channel,
94
+ branches: sources.map((s) => s.label).join(", "),
95
+ branch,
96
+ merges: sources.map((s) => s.merge).join(", then "),
97
+ sources: sources.map((s) => `${s.label} -> ${s.merge}`).join("\n- "),
98
+ release_repo: releaseRepo,
99
+ base: "dev",
100
+ release_url: `https://github.com/${releaseRepo}/releases/tag/${tag}`
101
+ };
102
+ return Object.entries(vars).reduce((text, [key, value]) => text.replaceAll(`{{${key}}}`, value), tpl);
103
+ }
104
+ const execFileAsync = promisify(execFile);
105
+ async function gitExec(args, cwd) {
106
+ const { stdout } = await execFileAsync("git", args, {
107
+ cwd,
108
+ encoding: "utf8"
109
+ });
110
+ return stdout.trim();
111
+ }
112
+ function makeRealAdapters() {
113
+ return {
114
+ cloneRepo: async (repoUrl, workDir) => {
115
+ await fs.rm(workDir, {
116
+ recursive: true,
117
+ force: true
118
+ });
119
+ await fs.mkdir(path.dirname(workDir), { recursive: true });
120
+ await gitExec([
121
+ "clone",
122
+ repoUrl,
123
+ workDir
124
+ ]);
125
+ },
126
+ fetchTags: async (workDir) => {
127
+ await gitExec([
128
+ "fetch",
129
+ "origin",
130
+ "--tags"
131
+ ], workDir);
132
+ },
133
+ fetchRef: async (workDir, remoteUrl, fetchRef, localRef) => {
134
+ await gitExec([
135
+ "fetch",
136
+ remoteUrl,
137
+ `${fetchRef}:${localRef}`
138
+ ], workDir);
139
+ },
140
+ createBranch: async (workDir, branch, tag) => {
141
+ try {
142
+ await gitExec([
143
+ "checkout",
144
+ "-B",
145
+ branch,
146
+ `refs/tags/${tag}`
147
+ ], workDir);
148
+ } catch {
149
+ await gitExec([
150
+ "checkout",
151
+ "-b",
152
+ branch,
153
+ `refs/tags/${tag}`
154
+ ], workDir);
155
+ }
156
+ },
157
+ runMerge: async (workDir, opencodeBin, agent, model, prompt) => {
158
+ await execFileAsync(opencodeBin, [
159
+ "run",
160
+ "--agent",
161
+ agent,
162
+ "--model",
163
+ model,
164
+ prompt
165
+ ], {
166
+ cwd: workDir,
167
+ encoding: "utf8",
168
+ timeout: 1800 * 1e3
169
+ });
170
+ },
171
+ buildCli: async (workDir, version, channel) => {
172
+ await execFileAsync("bun", [
173
+ "run",
174
+ "build",
175
+ "--",
176
+ "--single"
177
+ ], {
178
+ cwd: path.join(workDir, "packages", "opencode"),
179
+ encoding: "utf8",
180
+ env: {
181
+ ...process.env,
182
+ OPENCODE_CHANNEL: channel,
183
+ OPENCODE_VERSION: version
184
+ },
185
+ timeout: 1200 * 1e3
186
+ });
187
+ },
188
+ verifyVersion: async (workDir, expectedVersion) => {
189
+ const { stdout } = await execFileAsync(resolveCliPath(workDir), ["--version"], {
190
+ encoding: "utf8",
191
+ timeout: 3e4
192
+ });
193
+ const actual = stdout.trim();
194
+ if (actual !== expectedVersion) throw new Error(`Built CLI reported version ${actual}, expected ${expectedVersion}`);
195
+ },
196
+ commitIntegration: async (workDir, message) => {
197
+ await gitExec([
198
+ "-c",
199
+ "user.name=fro-bot harness integrate",
200
+ "-c",
201
+ "user.email=github-actions[bot]@users.noreply.github.com",
202
+ "add",
203
+ "-A"
204
+ ], workDir);
205
+ await gitExec([
206
+ "-c",
207
+ "user.name=fro-bot harness integrate",
208
+ "-c",
209
+ "user.email=github-actions[bot]@users.noreply.github.com",
210
+ "commit",
211
+ "--no-verify",
212
+ "-m",
213
+ message
214
+ ], workDir);
215
+ },
216
+ getCommitSha: async (workDir) => {
217
+ return gitExec(["rev-parse", "HEAD"], workDir);
218
+ }
219
+ };
220
+ }
221
+ function resolveCliPath(workDir) {
222
+ const name = `opencode-${process.platform === "win32" ? "windows" : process.platform}-${process.arch}`;
223
+ const binary = process.platform === "win32" ? "opencode.exe" : "opencode";
224
+ return path.join(workDir, "packages", "opencode", "dist", name, "bin", binary);
225
+ }
226
+ /**
227
+ * Runs the full integration pipeline:
228
+ * clone → fetch tags → fetch refs → create branch → LLM merge → build → verify → freeze
229
+ *
230
+ * On any failure: returns {ok:false, error} and writes NO manifest (fail-hard contract).
231
+ * On success: writes the provenance manifest to workDir and returns {ok:true, manifest}.
232
+ *
233
+ * @param config - Integration configuration (base version, refs, model, etc.)
234
+ * @param adapters - Injectable adapters for each step (real or stubbed for tests).
235
+ */
236
+ async function runIntegration(config, adapters) {
237
+ const { baseVersion, releaseRepo, integrationRefs, agent, model, opencodeBin, workDir, promptPath } = config;
238
+ const tag = `v${baseVersion}`;
239
+ const branch = integrationBranch(baseVersion);
240
+ const channel = "latest";
241
+ let sources;
242
+ try {
243
+ sources = resolveSources(integrationRefs, `https://github.com/${releaseRepo}.git`);
244
+ } catch (error) {
245
+ return {
246
+ ok: false,
247
+ error: `Resolve sources failed: ${errorMessage(error)}`
248
+ };
249
+ }
250
+ try {
251
+ await adapters.cloneRepo(`https://github.com/${releaseRepo}.git`, workDir);
252
+ } catch (error) {
253
+ return {
254
+ ok: false,
255
+ error: `Clone failed: ${errorMessage(error)}`
256
+ };
257
+ }
258
+ try {
259
+ await adapters.fetchTags(workDir);
260
+ } catch (error) {
261
+ return {
262
+ ok: false,
263
+ error: `Fetch tags failed: ${errorMessage(error)}`
264
+ };
265
+ }
266
+ for (const source of sources) try {
267
+ await adapters.fetchRef(workDir, source.repo, source.fetchRef, source.fetch);
268
+ } catch (error) {
269
+ return {
270
+ ok: false,
271
+ error: `Fetch ref ${source.label} failed: ${errorMessage(error)}`
272
+ };
273
+ }
274
+ try {
275
+ await adapters.createBranch(workDir, branch, tag);
276
+ } catch (error) {
277
+ return {
278
+ ok: false,
279
+ error: `Create branch ${branch} at ${tag} failed: ${errorMessage(error)}`
280
+ };
281
+ }
282
+ if (sources.length > 0) {
283
+ let prompt;
284
+ try {
285
+ prompt = await renderPrompt(promptPath, workDir, baseVersion, releaseRepo, sources);
286
+ } catch (error) {
287
+ return {
288
+ ok: false,
289
+ error: `Render merge prompt failed: ${errorMessage(error)}`
290
+ };
291
+ }
292
+ try {
293
+ await adapters.runMerge(workDir, opencodeBin, agent, model, prompt);
294
+ } catch (error) {
295
+ return {
296
+ ok: false,
297
+ error: `LLM merge failed: ${errorMessage(error)}`
298
+ };
299
+ }
300
+ try {
301
+ await adapters.commitIntegration(workDir, `integrate: apply LLM merge onto v${baseVersion}`);
302
+ } catch (error) {
303
+ return {
304
+ ok: false,
305
+ error: `Commit integration failed: ${errorMessage(error)}`
306
+ };
307
+ }
308
+ try {
309
+ await adapters.buildCli(workDir, baseVersion, channel);
310
+ } catch (error) {
311
+ return {
312
+ ok: false,
313
+ error: `Build CLI failed: ${errorMessage(error)}`
314
+ };
315
+ }
316
+ try {
317
+ await adapters.verifyVersion(workDir, baseVersion);
318
+ } catch (error) {
319
+ return {
320
+ ok: false,
321
+ error: `Version verification failed: ${errorMessage(error)}`
322
+ };
323
+ }
324
+ }
325
+ let integrationCommit;
326
+ try {
327
+ integrationCommit = await adapters.getCommitSha(workDir);
328
+ } catch (error) {
329
+ return {
330
+ ok: false,
331
+ error: `Get commit SHA failed: ${errorMessage(error)}`
332
+ };
333
+ }
334
+ const manifest = {
335
+ baseVersion,
336
+ integrationRefs: sources.map((s, i) => ({
337
+ ref: integrationRefs[i] ?? s.label,
338
+ resolvedSha: integrationCommit
339
+ })),
340
+ integrationCommit,
341
+ buildSha: "dev"
342
+ };
343
+ try {
344
+ await writeProvenanceManifest(workDir, manifest);
345
+ } catch (error) {
346
+ return {
347
+ ok: false,
348
+ error: `Write provenance manifest failed: ${errorMessage(error)}`
349
+ };
350
+ }
351
+ return {
352
+ ok: true,
353
+ manifest
354
+ };
355
+ }
356
+ function errorMessage(err) {
357
+ if (err instanceof Error) return err.message;
358
+ return String(err);
359
+ }
360
+ //#endregion
361
+ //#region src/integrate-command.ts
362
+ function isValidHarnessConfig$1(value) {
363
+ if (value === null || typeof value !== "object") return false;
364
+ const v = value;
365
+ if (typeof v.release_repo !== "string" || v.release_repo.length === 0) return false;
366
+ if (typeof v.base_version !== "string" || v.base_version.length === 0) return false;
367
+ if (!Array.isArray(v.integrationRefs)) return false;
368
+ if (!v.integrationRefs.every((el) => typeof el === "string" && el.length > 0)) return false;
369
+ if (typeof v.agent !== "string" || v.agent.length === 0) return false;
370
+ if (typeof v.model !== "string" || v.model.length === 0) return false;
371
+ if (v.opencode_bin !== void 0 && typeof v.opencode_bin !== "string") return false;
372
+ return true;
373
+ }
374
+ const packageRoot$1 = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
375
+ const DEFAULT_CONFIG_PATH = path.join(packageRoot$1, "harness.config.json");
376
+ function parseFlags(argv) {
377
+ let workDir;
378
+ let promptPath;
379
+ let out;
380
+ for (let i = 0; i < argv.length; i++) {
381
+ const arg = argv[i];
382
+ if (arg === "--work-dir" || arg === "--prompt-path" || arg === "--out") {
383
+ const next = argv[i + 1];
384
+ if (next === void 0 || next.startsWith("--")) {
385
+ console.error(`[integrate] ${arg} requires a value`);
386
+ return null;
387
+ }
388
+ if (arg === "--work-dir") workDir = next;
389
+ else if (arg === "--prompt-path") promptPath = next;
390
+ else out = next;
391
+ i++;
392
+ }
393
+ }
394
+ return {
395
+ workDir,
396
+ promptPath,
397
+ out
398
+ };
399
+ }
400
+ /**
401
+ * Packages a clean merged source snapshot plus provenance.json into a single
402
+ * tar artifact at outPath using atomic staging.
403
+ *
404
+ * Steps:
405
+ * 1. Create a temp staging dir.
406
+ * 2. Run `git archive --format=tar --output=<tmp>/source.tar <integrationCommit>` in workDir.
407
+ * 3. Extract source.tar into <tmp>/tree, copy provenance.json into <tmp>/tree.
408
+ * 4. Re-tar <tmp>/tree → <tmp>/artifact.tar.
409
+ * 5. Ensure outPath parent dir exists, then atomically rename <tmp>/artifact.tar → outPath.
410
+ * 6. Clean the temp dir in a finally block.
411
+ *
412
+ * ATOMIC: the rename only happens after the artifact is fully built. Any error
413
+ * before the rename leaves outPath untouched.
414
+ *
415
+ * @param workDir - The integration work directory (contains the git repo + provenance.json).
416
+ * @param integrationCommit - The commit SHA to archive (the frozen integration commit).
417
+ * @param outPath - Destination path for the final artifact tar.
418
+ */
419
+ async function packageArtifact(workDir, integrationCommit, outPath) {
420
+ const trackedDirtyLines = execSync("git status --porcelain", {
421
+ cwd: workDir,
422
+ encoding: "utf8"
423
+ }).split("\n").filter((line) => line.length > 0 && !line.startsWith("??"));
424
+ if (trackedDirtyLines.length > 0) throw new Error(`[integrate] Working tree has uncommitted tracked changes before git archive — these would be excluded from the artifact:\n${trackedDirtyLines.join("\n")}`);
425
+ const tmpStaging = mkdtempSync(path.join(os.tmpdir(), "harness-artifact-"));
426
+ try {
427
+ const sourceTar = path.join(tmpStaging, "source.tar");
428
+ const treeDir = path.join(tmpStaging, "tree");
429
+ const artifactTar = path.join(tmpStaging, "artifact.tar");
430
+ execFileSync("git", [
431
+ "archive",
432
+ "--format=tar",
433
+ `--output=${sourceTar}`,
434
+ integrationCommit
435
+ ], {
436
+ cwd: workDir,
437
+ stdio: [
438
+ "ignore",
439
+ "ignore",
440
+ "pipe"
441
+ ]
442
+ });
443
+ mkdirSync(treeDir, { recursive: true });
444
+ execFileSync("tar", [
445
+ "xf",
446
+ sourceTar,
447
+ "-C",
448
+ treeDir
449
+ ], { stdio: [
450
+ "ignore",
451
+ "ignore",
452
+ "pipe"
453
+ ] });
454
+ copyFileSync(path.join(workDir, "provenance.json"), path.join(treeDir, "provenance.json"));
455
+ execFileSync("tar", [
456
+ "cf",
457
+ artifactTar,
458
+ "-C",
459
+ treeDir,
460
+ "."
461
+ ], { stdio: [
462
+ "ignore",
463
+ "ignore",
464
+ "pipe"
465
+ ] });
466
+ mkdirSync(path.dirname(outPath), { recursive: true });
467
+ renameSync(artifactTar, outPath);
468
+ } finally {
469
+ try {
470
+ rmSync(tmpStaging, {
471
+ recursive: true,
472
+ force: true
473
+ });
474
+ } catch {}
475
+ }
476
+ }
477
+ /**
478
+ * Implements `harness integrate`.
479
+ *
480
+ * @param argv - CLI arguments (everything after "integrate").
481
+ * @param configPath - Path to harness.config.json (defaults to package root; injectable for tests).
482
+ * @param _packageArtifact - Injectable override for packageArtifact (for unit tests; defaults to the real impl).
483
+ * @returns Exit code: 0 on success, 1 on failure.
484
+ */
485
+ async function cmdIntegrate(argv, configPath = DEFAULT_CONFIG_PATH, _packageArtifact = packageArtifact) {
486
+ const flags = parseFlags(argv);
487
+ if (flags === null) return 1;
488
+ if (flags.workDir === void 0) {
489
+ console.error("[integrate] Missing required flag: --work-dir <dir>");
490
+ return 1;
491
+ }
492
+ if (flags.promptPath === void 0) {
493
+ console.error("[integrate] Missing required flag: --prompt-path <path>");
494
+ return 1;
495
+ }
496
+ if (flags.out === void 0) {
497
+ console.error("[integrate] Missing required flag: --out <path>");
498
+ return 1;
499
+ }
500
+ const workDir = flags.workDir;
501
+ const outPath = flags.out;
502
+ let rawConfig;
503
+ try {
504
+ const raw = readFileSync(configPath, "utf8");
505
+ rawConfig = JSON.parse(raw);
506
+ } catch (error) {
507
+ const msg = error instanceof Error ? error.message : String(error);
508
+ console.error(`[integrate] Failed to read config: ${msg}`);
509
+ return 1;
510
+ }
511
+ if (!isValidHarnessConfig$1(rawConfig)) {
512
+ console.error("[integrate] Invalid harness.config.json shape");
513
+ return 1;
514
+ }
515
+ const config = {
516
+ baseVersion: rawConfig.base_version,
517
+ releaseRepo: rawConfig.release_repo,
518
+ integrationRefs: rawConfig.integrationRefs,
519
+ agent: rawConfig.agent,
520
+ model: rawConfig.model,
521
+ opencodeBin: rawConfig.opencode_bin ?? "opencode",
522
+ workDir,
523
+ promptPath: flags.promptPath
524
+ };
525
+ try {
526
+ const result = await runIntegration(config, makeRealAdapters());
527
+ if (result.ok === true) {
528
+ await _packageArtifact(workDir, result.manifest.integrationCommit, outPath);
529
+ return 0;
530
+ }
531
+ console.error(`[integrate] ${result.error}`);
532
+ return 1;
533
+ } catch (error) {
534
+ const msg = error instanceof Error ? error.message : String(error);
535
+ console.error(`[integrate] ${msg}`);
536
+ return 1;
537
+ }
538
+ }
539
+ //#endregion
540
+ //#region src/provenance.ts
541
+ /**
542
+ * Provenance manifest for the harness binary.
543
+ *
544
+ * The manifest is the single source of truth written by the integration engine
545
+ * and read by the CLI commands (info, patches, doctor).
546
+ *
547
+ * baseVersion — the upstream OpenCode release tag this binary is based on.
548
+ * integrationRefs — ordered list of integration refs carried onto the base tag,
549
+ * each with the resolved commit SHA and optional metadata.
550
+ * integrationCommit — the frozen integration commit SHA produced by the LLM merge.
551
+ * buildSha — the git SHA of the harness build, or 'dev' in the scaffold.
552
+ */
553
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
554
+ /**
555
+ * Type guard: validates that an unknown value has the shape of a Provenance manifest.
556
+ * Treats malformed JSON as an explicit error rather than silently returning partial data.
557
+ */
558
+ function isValidProvenance(value) {
559
+ if (value === null || typeof value !== "object") return false;
560
+ const v = value;
561
+ if (typeof v.baseVersion !== "string" || v.baseVersion.length === 0) return false;
562
+ if (!Array.isArray(v.integrationRefs)) return false;
563
+ if (v.integrationCommit !== null && typeof v.integrationCommit !== "string") return false;
564
+ if (typeof v.buildSha !== "string") return false;
565
+ return true;
566
+ }
567
+ /**
568
+ * Type guard: validates that an unknown value has the shape of a harness.config.json.
569
+ */
570
+ function isValidHarnessConfig(value) {
571
+ if (value === null || typeof value !== "object") return false;
572
+ const v = value;
573
+ if (v.base_version !== void 0 && typeof v.base_version !== "string") return false;
574
+ if (v.integrationRefs !== void 0 && !Array.isArray(v.integrationRefs)) return false;
575
+ return true;
576
+ }
577
+ /**
578
+ * Returns the provenance for the current harness binary.
579
+ *
580
+ * Resolution order:
581
+ * 1. Bundled provenance.json (written by the integration engine at build time).
582
+ * 2. harness.config.json (dev scaffold — shows configured refs without a frozen commit).
583
+ * 3. Hardcoded dev placeholder (no config file present).
584
+ *
585
+ * The integration engine writes provenance.json at build time; this function reads
586
+ * it at runtime, making the manifest available without additional filesystem reads
587
+ * in production.
588
+ */
589
+ function getProvenance() {
590
+ try {
591
+ const raw = readFileSync(path.join(packageRoot, "provenance.json"), "utf8");
592
+ const parsed = JSON.parse(raw);
593
+ if (isValidProvenance(parsed)) return parsed;
594
+ } catch {}
595
+ try {
596
+ const raw = readFileSync(path.join(packageRoot, "harness.config.json"), "utf8");
597
+ const parsed = JSON.parse(raw);
598
+ if (!isValidHarnessConfig(parsed)) throw new Error("Invalid harness.config.json shape");
599
+ return {
600
+ baseVersion: parsed.base_version ?? "1.15.13",
601
+ integrationRefs: (parsed.integrationRefs ?? []).map((ref) => ({
602
+ ref,
603
+ resolvedSha: "dev"
604
+ })),
605
+ integrationCommit: null,
606
+ buildSha: "dev"
607
+ };
608
+ } catch {}
609
+ return {
610
+ baseVersion: "1.15.13",
611
+ integrationRefs: [],
612
+ integrationCommit: null,
613
+ buildSha: "dev"
614
+ };
615
+ }
616
+ /**
617
+ * Formats provenance as a human-readable string for `harness info`.
618
+ */
619
+ function formatProvenance(p) {
620
+ const lines = [
621
+ `harness (patched OpenCode)`,
622
+ ` base: ${p.baseVersion}`,
623
+ ` integration commit: ${p.integrationCommit ?? "(unbuilt/dev scaffold)"}`,
624
+ ` build sha: ${p.buildSha}`
625
+ ];
626
+ if (p.integrationRefs.length > 0) {
627
+ lines.push(` integration refs:`);
628
+ for (const r of p.integrationRefs) {
629
+ const meta = r.upstreamStatus === void 0 ? "" : ` [${r.upstreamStatus}]`;
630
+ lines.push(` - ${r.ref}${meta}`);
631
+ if (r.reason !== void 0) lines.push(` reason: ${r.reason}`);
632
+ }
633
+ } else lines.push(` integration refs: (none — dev scaffold)`);
634
+ return lines.join("\n");
635
+ }
636
+ //#endregion
637
+ //#region src/resolve-binary.ts
638
+ /**
639
+ * Resolves the patched OpenCode binary for the current host.
640
+ *
641
+ * Resolution order (precedence, highest to lowest):
642
+ * 0. OPENCODE_PATH env override — always honoured; marks isBuilt: false.
643
+ * 1. Host-platform optionalDependencies binary (the real harness-built artifact).
644
+ * Resolved via Node module resolution (createRequire) so pnpm/npm hoisting
645
+ * is handled correctly — the platform package may be hoisted outside the
646
+ * local node_modules tree.
647
+ * 2. PATH fallback (`opencode` on PATH) — ONLY when an explicit dev escape hatch
648
+ * is active: HARNESS_ALLOW_PATH_FALLBACK=1 or OPENCODE_PATH is set.
649
+ * In published/production use (no escape hatch), a missing platform binary
650
+ * is a hard error with an actionable message.
651
+ *
652
+ * The integrity check in step 1 is a basic executable-probe (--version succeeds).
653
+ * A full cryptographic integrity check (npm provenance) is enforced by the
654
+ * postinstall resolver at install time; this runtime check is a belt-and-suspenders
655
+ * guard against a corrupted or missing binary.
656
+ */
657
+ /**
658
+ * Attempts to resolve the host-platform binary from the installed
659
+ * @fro.bot/harness-<os>-<arch> optionalDependencies package.
660
+ *
661
+ * Uses Node module resolution (createRequire) to locate the package, which
662
+ * correctly handles pnpm/npm hoisting — the platform package may be installed
663
+ * outside the local node_modules tree.
664
+ *
665
+ * Returns the binary path if found and executable, or null otherwise.
666
+ */
667
+ function resolveOptionalDepBinary() {
668
+ const platformResult = getHostPlatformInfo();
669
+ if (!platformResult.ok) return null;
670
+ const info = platformResult.info;
671
+ const require = createRequire(import.meta.url);
672
+ let platformPkgRoot;
673
+ try {
674
+ const pkgJsonPath = require.resolve(`${info.packageName}/package.json`);
675
+ platformPkgRoot = path.dirname(pkgJsonPath);
676
+ } catch {
677
+ return null;
678
+ }
679
+ const binaryPath = binaryPathInPackage(platformPkgRoot, info);
680
+ try {
681
+ execFileSync(binaryPath, ["--version"], {
682
+ encoding: "utf8",
683
+ timeout: 1e4,
684
+ stdio: [
685
+ "ignore",
686
+ "pipe",
687
+ "pipe"
688
+ ]
689
+ });
690
+ return binaryPath;
691
+ } catch {
692
+ return null;
693
+ }
694
+ }
695
+ /**
696
+ * Returns true when an explicit dev escape hatch is active.
697
+ *
698
+ * The escape hatch allows PATH fallback in local/dev/unbuilt environments.
699
+ * It is NEVER active in published/production use (no env set by default).
700
+ *
701
+ * Escape hatches:
702
+ * - HARNESS_ALLOW_PATH_FALLBACK=1 — explicit opt-in for dev/CI without platform binary.
703
+ * - OPENCODE_PATH set — explicit override already provided; PATH fallback is moot.
704
+ */
705
+ function isDevEscapeHatchActive() {
706
+ return process.env.HARNESS_ALLOW_PATH_FALLBACK === "1" || process.env.OPENCODE_PATH !== void 0 && process.env.OPENCODE_PATH.length > 0;
707
+ }
708
+ /**
709
+ * Resolves the patched OpenCode binary for the current host.
710
+ *
711
+ * Resolution order:
712
+ * 0. OPENCODE_PATH env override.
713
+ * 1. Host-platform optionalDependencies binary (isBuilt: true).
714
+ * 2. PATH fallback — ONLY when HARNESS_ALLOW_PATH_FALLBACK=1 (dev escape hatch).
715
+ * In production (no escape hatch), missing platform binary → throws with remediation.
716
+ *
717
+ * @throws {Error} when no platform binary is found and no dev escape hatch is active.
718
+ */
719
+ function resolveBinary() {
720
+ const override = process.env.OPENCODE_PATH;
721
+ if (override !== void 0 && override.length > 0) return {
722
+ resolved: true,
723
+ path: override,
724
+ isBuilt: false
725
+ };
726
+ const optionalBinary = resolveOptionalDepBinary();
727
+ if (optionalBinary !== null) return {
728
+ resolved: true,
729
+ path: optionalBinary,
730
+ isBuilt: true
731
+ };
732
+ if (isDevEscapeHatchActive()) return {
733
+ resolved: true,
734
+ path: "opencode",
735
+ isBuilt: false
736
+ };
737
+ const platformResult = getHostPlatformInfo();
738
+ const expectedPkg = platformResult.ok ? platformResult.info.packageName : "@fro.bot/harness-<os>-<arch>";
739
+ throw new Error(`[harness] Platform binary not found. Expected package: ${expectedPkg}\n Remediation: ensure ${expectedPkg} is installed as an optionalDependency,\n or set OPENCODE_PATH to an explicit binary path,\n or set HARNESS_ALLOW_PATH_FALLBACK=1 to use opencode on PATH (dev only).`);
740
+ }
741
+ /**
742
+ * Checks whether the resolved binary is present and runnable by invoking it
743
+ * with `--version`. Returns the version string on success, or null on failure.
744
+ */
745
+ function probeBinary(binaryPath) {
746
+ try {
747
+ return execFileSync(binaryPath, ["--version"], {
748
+ encoding: "utf8",
749
+ timeout: 1e4,
750
+ stdio: [
751
+ "ignore",
752
+ "pipe",
753
+ "pipe"
754
+ ]
755
+ }).trim();
756
+ } catch {
757
+ return null;
758
+ }
759
+ }
760
+ //#endregion
761
+ //#region src/cli.ts
762
+ /**
763
+ * harness CLI — patched OpenCode binary with provenance/operability commands.
764
+ *
765
+ * Subcommand disambiguation:
766
+ * Reserved harness subcommands: info, patches, doctor
767
+ * --version / --help: harness-own (prints provenance / usage)
768
+ * Everything else: passed through to the resolved patched binary.
769
+ *
770
+ * This is the ONLY entry point for the @fro.bot/harness package.
771
+ * No classes; functions only; explicit boolean checks; no as-any.
772
+ */
773
+ function printUsage() {
774
+ console.log(`harness — patched OpenCode binary (Fro Bot integration)
775
+
776
+ Usage:
777
+ harness <opencode-args...> Pass through to the patched OpenCode binary
778
+ harness info Print provenance (base version, integration refs, build sha)
779
+ harness patches List configured integration refs
780
+ harness doctor Check the resolved binary is present and runnable
781
+ harness integrate Run the LLM merge integration pipeline
782
+ --work-dir <dir> (required) Working directory for the clone
783
+ --prompt-path <path> (required) Path to the merge prompt template
784
+ --out <path> (required) Artifact output path
785
+ harness --version Print harness provenance version
786
+ harness --help Print this help
787
+
788
+ Reserved subcommands (info, patches, doctor, integrate) are handled by harness itself.
789
+ All other arguments are forwarded to the patched OpenCode binary.`);
790
+ }
791
+ function cmdInfo() {
792
+ const p = getProvenance();
793
+ console.log(formatProvenance(p));
794
+ }
795
+ function cmdPatches() {
796
+ const p = getProvenance();
797
+ if (p.integrationRefs.length === 0) {
798
+ console.log("No integration refs configured (dev scaffold).");
799
+ return;
800
+ }
801
+ console.log("Integration refs:");
802
+ for (const r of p.integrationRefs) {
803
+ const status = r.upstreamStatus === void 0 ? "" : ` [${r.upstreamStatus}]`;
804
+ console.log(` - ${r.ref}${status}`);
805
+ if (r.reason !== void 0) console.log(` reason: ${r.reason}`);
806
+ }
807
+ if (p.integrationCommit !== null) console.log(`\nFrozen integration commit: ${p.integrationCommit}`);
808
+ }
809
+ function cmdDoctor() {
810
+ let binary;
811
+ try {
812
+ binary = resolveBinary();
813
+ } catch (error) {
814
+ const msg = error instanceof Error ? error.message : String(error);
815
+ console.error(`\n[FAIL] ${msg}`);
816
+ return 1;
817
+ }
818
+ const p = getProvenance();
819
+ console.log(`harness doctor`);
820
+ console.log(` base version: ${p.baseVersion}`);
821
+ console.log(` integration commit: ${p.integrationCommit ?? "(unbuilt/dev scaffold)"}`);
822
+ console.log(` build sha: ${p.buildSha}`);
823
+ console.log(` binary path: ${binary.path}`);
824
+ console.log(` is built artifact: ${binary.isBuilt}`);
825
+ if (!binary.isBuilt && process.env.HARNESS_ALLOW_PATH_FALLBACK !== "1" && process.env.OPENCODE_PATH === void 0) {
826
+ console.error("\n[FAIL] Binary is not a built harness artifact.");
827
+ console.error(" Install the platform package or set OPENCODE_PATH / HARNESS_ALLOW_PATH_FALLBACK=1 for dev use.");
828
+ return 1;
829
+ }
830
+ const version = probeBinary(binary.path);
831
+ if (version === null) {
832
+ console.error(`\n[FAIL] Binary not runnable: ${binary.path}`);
833
+ console.error(" Ensure opencode is on PATH or set OPENCODE_PATH.");
834
+ return 1;
835
+ }
836
+ console.log(` binary version: ${version}`);
837
+ if (binary.isBuilt && version !== p.baseVersion) {
838
+ console.error(`\n[FAIL] Binary version mismatch: binary reports '${version}', provenance expects '${p.baseVersion}'.`);
839
+ console.error(" Reinstall @fro.bot/harness or check the platform package version.");
840
+ return 1;
841
+ }
842
+ console.log("\n[OK] Binary is present and runnable.");
843
+ return 0;
844
+ }
845
+ function cmdPassthrough(args) {
846
+ let binary;
847
+ try {
848
+ binary = resolveBinary();
849
+ } catch (error) {
850
+ const msg = error instanceof Error ? error.message : String(error);
851
+ console.error(`[harness] ${msg}`);
852
+ return 1;
853
+ }
854
+ const result = spawnSync(binary.path, [...args], {
855
+ stdio: "inherit",
856
+ env: process.env
857
+ });
858
+ if (result.error !== void 0) {
859
+ console.error(`[harness] Failed to spawn ${binary.path}: ${result.error.message}`);
860
+ return 1;
861
+ }
862
+ return result.status ?? 1;
863
+ }
864
+ async function main() {
865
+ const args = process.argv.slice(2);
866
+ if (args[0] === "--help" || args[0] === "-h") {
867
+ printUsage();
868
+ process.exit(0);
869
+ }
870
+ if (args[0] === "--version" || args[0] === "-v") {
871
+ const p = getProvenance();
872
+ console.log(`@fro.bot/harness base:${p.baseVersion} build:${p.buildSha}`);
873
+ process.exit(0);
874
+ }
875
+ const subcommand = args[0];
876
+ if (subcommand === "info") {
877
+ cmdInfo();
878
+ process.exit(0);
879
+ }
880
+ if (subcommand === "patches") {
881
+ cmdPatches();
882
+ process.exit(0);
883
+ }
884
+ if (subcommand === "doctor") {
885
+ const code = cmdDoctor();
886
+ process.exit(code);
887
+ }
888
+ if (subcommand === "integrate") {
889
+ const code = await cmdIntegrate(args.slice(1));
890
+ process.exit(code);
891
+ }
892
+ const code = cmdPassthrough(args);
893
+ process.exit(code);
894
+ }
895
+ main().catch((error) => {
896
+ const msg = error instanceof Error ? error.message : String(error);
897
+ console.error(`[harness] Unexpected error: ${msg}`);
898
+ process.exit(1);
899
+ });
900
+ //#endregion
901
+ export {};
902
+
903
+ //# sourceMappingURL=cli.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.mjs","names":["isValidHarnessConfig","packageRoot"],"sources":["../src/sources.ts","../src/integrate.ts","../src/integrate-command.ts","../src/provenance.ts","../src/resolve-binary.ts","../src/cli.ts"],"sourcesContent":["/**\n * Integration source resolution — maps each configured ref to git fetch refs.\n *\n * Ported from cortexkit/orw src/index.ts parseSource (MIT).\n * Adapted for CI/non-interactive use: no launchd, no desktop, no interactive prompts.\n *\n * Supported input forms:\n * - Local branch name (no https:// prefix) → refs/heads/<b>\n * - GitHub branch URL (https://github.com/owner/repo/tree/<branch>) → refs/heads/<branch>\n * - GitHub PR URL (https://github.com/owner/repo/pull/N) → refs/pull/N/head\n *\n * Throws on empty input or unsupported URL forms.\n */\n\nexport interface IntegrationSource {\n /** Human-readable label for log output and the merge prompt. */\n readonly label: string\n /** Git remote URL for the source repository. */\n readonly repo: string\n /** The ref to fetch from the remote (e.g. refs/pull/N/head, refs/heads/<b>). */\n readonly fetchRef: string\n /** The local remote-tracking ref to store the fetched ref under. */\n readonly fetch: string\n /** The ref to merge (same as fetch; kept separate for prompt rendering). */\n readonly merge: string\n}\n\n/**\n * Maps a single integration source input to a typed IntegrationSource.\n *\n * @param input - A PR URL, branch URL, or local branch name from config.\n * @param sourceRepo - The default source repo URL (used for local branch names).\n */\nexport function parseSource(input: string, sourceRepo: string): IntegrationSource {\n const value = input.trim()\n if (value.length === 0) throw new Error('Empty integration source in config branches')\n\n if (!value.startsWith('https://github.com/')) {\n // Local branch name — fetch from the source repo.\n return {\n label: value,\n repo: sourceRepo,\n fetchRef: `refs/heads/${value}`,\n fetch: `refs/remotes/watch/local/${value}`,\n merge: `refs/remotes/watch/local/${value}`,\n }\n }\n\n const url = new URL(value)\n const parts = url.pathname.split('/').filter(Boolean)\n const owner = parts[0]\n const repo = parts[1]\n if (owner === undefined || repo === undefined) throw new Error(`Unsupported GitHub source URL: ${value}`)\n\n if (parts.length >= 4 && parts[2] === 'tree') {\n const branch = decodeURIComponent(parts.slice(3).join('/'))\n const slug = watchSlug(owner, repo)\n const ref = `refs/remotes/watch/${slug}/${branch}`\n return {\n label: `${owner}/${repo}:${branch}`,\n repo: `https://github.com/${owner}/${repo}.git`,\n fetchRef: `refs/heads/${branch}`,\n fetch: ref,\n merge: ref,\n }\n }\n\n if (parts.length >= 4 && parts[2] === 'pull') {\n const number = parts[3] ?? ''\n if (!/^\\d+$/.test(number)) {\n throw new Error(`Unsupported GitHub pull request URL: ${value}`)\n }\n const slug = watchSlug(owner, repo)\n const ref = `refs/remotes/watch/${slug}/pr-${number}`\n return {\n label: `${owner}/${repo}#${number}`,\n repo: `https://github.com/${owner}/${repo}.git`,\n fetchRef: `refs/pull/${number}/head`,\n fetch: ref,\n merge: ref,\n }\n }\n\n throw new Error(`Unsupported GitHub integration source URL: ${value}`)\n}\n\n/**\n * Maps an array of integration source inputs to typed IntegrationSources.\n *\n * @param refs - Array of PR URLs, branch URLs, or local branch names.\n * @param sourceRepo - The default source repo URL (used for local branch names).\n */\nexport function resolveSources(refs: readonly string[], sourceRepo: string): IntegrationSource[] {\n return refs.map(input => parseSource(input, sourceRepo))\n}\n\nfunction watchSlug(owner: string, repo: string): string {\n return `${owner}-${repo}`.replaceAll(/[^\\w.-]/g, '-')\n}\n","/**\n * Integration engine — orw-embedded LLM merge onto the release tag.\n *\n * Ported from cortexkit/orw src/index.ts check/prep/render/verifyBuild (MIT).\n * Adapted for CI/non-interactive use: no launchd, no desktop, no interactive prompts.\n *\n * The actual opencode run LLM merge is NOT unit-tested here — it requires a live\n * opencode binary + model + network. Unit tests cover the fail-hard/freeze/provenance\n * contract via injected adapters (cloneRepo, fetchRef, runMerge, buildCli, verifyVersion).\n *\n * Fail-hard contract: any failure (merge unresolved, build fail, version mismatch)\n * returns {ok:false, error} and writes NO provenance manifest. The manifest is the\n * single source of truth — it is only written after all steps succeed (freeze).\n */\n\n// IntegrationRefRecord is the canonical type — defined once in provenance.ts.\nimport type {IntegrationRefRecord} from './provenance.js'\nimport type {IntegrationSource} from './sources.js'\nimport {execFile} from 'node:child_process'\nimport fs from 'node:fs/promises'\nimport path from 'node:path'\nimport process from 'node:process'\nimport {promisify} from 'node:util'\nimport {resolveSources} from './sources.js'\n\n// Re-export so callers that previously imported from integrate.ts still work.\nexport type {IntegrationRefRecord} from './provenance.js'\n\n// ---------------------------------------------------------------------------\n// Provenance manifest types\n// ---------------------------------------------------------------------------\n\nexport interface ProvenanceManifest {\n readonly baseVersion: string\n readonly integrationRefs: readonly IntegrationRefRecord[]\n readonly integrationCommit: string\n readonly buildSha: string\n}\n\n// ---------------------------------------------------------------------------\n// Integration config\n// ---------------------------------------------------------------------------\n\nexport interface IntegrationConfig {\n readonly baseVersion: string\n readonly releaseRepo: string\n readonly integrationRefs: readonly string[]\n readonly agent: string\n readonly model: string\n readonly opencodeBin: string\n readonly workDir: string\n readonly promptPath: string\n}\n\n// ---------------------------------------------------------------------------\n// Injectable adapters (dependency injection for testability)\n// ---------------------------------------------------------------------------\n\nexport interface IntegrationAdapters {\n /** Clone the release repo into workDir. */\n cloneRepo: (repoUrl: string, workDir: string) => Promise<void>\n /** Fetch tags from origin. */\n fetchTags: (workDir: string) => Promise<void>\n /** Fetch a single integration ref into a local tracking ref. */\n fetchRef: (workDir: string, remoteUrl: string, fetchRef: string, localRef: string) => Promise<void>\n /** Create/reset the integration branch to the release tag. */\n createBranch: (workDir: string, branch: string, tag: string) => Promise<void>\n /** Run the LLM merge via opencode run. */\n runMerge: (workDir: string, opencodeBin: string, agent: string, model: string, prompt: string) => Promise<void>\n /** Build the native CLI in the work repo. */\n buildCli: (workDir: string, version: string, channel: string) => Promise<void>\n /** Verify the built CLI --version matches the expected version. */\n verifyVersion: (workDir: string, expectedVersion: string) => Promise<void>\n /** Commit the integrated working tree (after LLM merge) so HEAD contains the merge. */\n commitIntegration: (workDir: string, message: string) => Promise<void>\n /** Get the current HEAD commit SHA of the work repo. */\n getCommitSha: (workDir: string) => Promise<string>\n}\n\n// ---------------------------------------------------------------------------\n// Integration result\n// ---------------------------------------------------------------------------\n\nexport type IntegrationResult =\n | {readonly ok: true; readonly manifest: ProvenanceManifest}\n | {readonly ok: false; readonly error: string}\n\n// ---------------------------------------------------------------------------\n// Provenance manifest I/O (single source of truth)\n// ---------------------------------------------------------------------------\n\nconst MANIFEST_FILENAME = 'provenance.json'\n\n/**\n * Writes the provenance manifest to the given directory.\n * This is the freeze step — called only after all integration steps succeed.\n */\nexport async function writeProvenanceManifest(dir: string, manifest: ProvenanceManifest): Promise<void> {\n await fs.mkdir(dir, {recursive: true})\n await fs.writeFile(path.join(dir, MANIFEST_FILENAME), `${JSON.stringify(manifest, null, 2)}\\n`, 'utf8')\n}\n\n/**\n * Type guard: validates that an unknown value has the shape of a ProvenanceManifest.\n * Treats malformed/partial JSON as invalid rather than silently returning partial data.\n */\nfunction isValidProvenanceManifest(value: unknown): value is ProvenanceManifest {\n if (value === null || typeof value !== 'object') return false\n const v = value as Record<string, unknown>\n if (typeof v.baseVersion !== 'string' || v.baseVersion.length === 0) return false\n if (!Array.isArray(v.integrationRefs)) return false\n if (typeof v.integrationCommit !== 'string' || v.integrationCommit.length === 0) return false\n if (typeof v.buildSha !== 'string') return false\n return true\n}\n\n/**\n * Reads the provenance manifest from the given directory.\n * Returns null if the manifest does not exist or has an invalid shape.\n * Uses isValidProvenanceManifest to guard against malformed/partial manifests.\n */\nexport async function readProvenanceManifest(dir: string): Promise<ProvenanceManifest | null> {\n const manifestPath = path.join(dir, MANIFEST_FILENAME)\n try {\n const raw = await fs.readFile(manifestPath, 'utf8')\n const parsed: unknown = JSON.parse(raw)\n if (!isValidProvenanceManifest(parsed)) {\n return null\n }\n return parsed\n } catch {\n return null\n }\n}\n\n// ---------------------------------------------------------------------------\n// Prompt rendering (adapted from orw render())\n// ---------------------------------------------------------------------------\n\nfunction integrationBranch(version: string): string {\n return `integrate/v${version}`\n}\n\nasync function renderPrompt(\n promptPath: string,\n workDir: string,\n baseVersion: string,\n releaseRepo: string,\n sources: IntegrationSource[],\n): Promise<string> {\n const tag = `v${baseVersion}`\n const branch = integrationBranch(baseVersion)\n const channel = 'latest'\n const tpl = await fs.readFile(promptPath, 'utf8')\n const vars: Record<string, string> = {\n repo: workDir,\n tag,\n version: baseVersion,\n channel,\n branches: sources.map(s => s.label).join(', '),\n branch,\n merges: sources.map(s => s.merge).join(', then '),\n sources: sources.map(s => `${s.label} -> ${s.merge}`).join('\\n- '),\n release_repo: releaseRepo,\n base: 'dev',\n release_url: `https://github.com/${releaseRepo}/releases/tag/${tag}`,\n }\n return Object.entries(vars).reduce((text, [key, value]) => text.replaceAll(`{{${key}}}`, value), tpl)\n}\n\nconst execFileAsync = promisify(execFile)\n\nasync function gitExec(args: string[], cwd?: string): Promise<string> {\n const {stdout} = await execFileAsync('git', args, {cwd, encoding: 'utf8'})\n return stdout.trim()\n}\n\nexport function makeRealAdapters(): IntegrationAdapters {\n return {\n cloneRepo: async (repoUrl, workDir) => {\n await fs.rm(workDir, {recursive: true, force: true})\n await fs.mkdir(path.dirname(workDir), {recursive: true})\n await gitExec(['clone', repoUrl, workDir])\n },\n\n fetchTags: async workDir => {\n await gitExec(['fetch', 'origin', '--tags'], workDir)\n },\n\n fetchRef: async (workDir, remoteUrl, fetchRef, localRef) => {\n await gitExec(['fetch', remoteUrl, `${fetchRef}:${localRef}`], workDir)\n },\n\n createBranch: async (workDir, branch, tag) => {\n // Reset or create the integration branch at the release tag.\n try {\n await gitExec(['checkout', '-B', branch, `refs/tags/${tag}`], workDir)\n } catch {\n await gitExec(['checkout', '-b', branch, `refs/tags/${tag}`], workDir)\n }\n },\n\n runMerge: async (workDir, opencodeBin, agent, model, prompt) => {\n // Run opencode run synchronously — do NOT use background:true.\n // Poll to terminal state; the non-interactive tool exits when done.\n await execFileAsync(opencodeBin, ['run', '--agent', agent, '--model', model, prompt], {\n cwd: workDir,\n encoding: 'utf8',\n timeout: 30 * 60 * 1000, // 30-minute hard timeout\n })\n },\n\n buildCli: async (workDir, version, channel) => {\n await execFileAsync('bun', ['run', 'build', '--', '--single'], {\n cwd: path.join(workDir, 'packages', 'opencode'),\n encoding: 'utf8',\n env: {\n ...process.env,\n OPENCODE_CHANNEL: channel,\n OPENCODE_VERSION: version,\n },\n timeout: 20 * 60 * 1000, // 20-minute hard timeout\n })\n },\n\n verifyVersion: async (workDir, expectedVersion) => {\n const cliPath = resolveCliPath(workDir)\n const {stdout} = await execFileAsync(cliPath, ['--version'], {\n encoding: 'utf8',\n timeout: 30_000,\n })\n const actual = stdout.trim()\n if (actual !== expectedVersion) {\n throw new Error(`Built CLI reported version ${actual}, expected ${expectedVersion}`)\n }\n },\n\n commitIntegration: async (workDir, message) => {\n // Stage all changes (new, modified, deleted) from the LLM merge.\n await gitExec(\n [\n '-c',\n 'user.name=fro-bot harness integrate',\n '-c',\n 'user.email=github-actions[bot]@users.noreply.github.com',\n 'add',\n '-A',\n ],\n workDir,\n )\n // Commit with --no-verify to skip any hooks in the cloned upstream repo.\n await gitExec(\n [\n '-c',\n 'user.name=fro-bot harness integrate',\n '-c',\n 'user.email=github-actions[bot]@users.noreply.github.com',\n 'commit',\n '--no-verify',\n '-m',\n message,\n ],\n workDir,\n )\n },\n\n getCommitSha: async workDir => {\n return gitExec(['rev-parse', 'HEAD'], workDir)\n },\n }\n}\n\nfunction resolveCliPath(workDir: string): string {\n const os = process.platform === 'win32' ? 'windows' : process.platform\n const arch = process.arch\n const name = `opencode-${os}-${arch}`\n const binary = process.platform === 'win32' ? 'opencode.exe' : 'opencode'\n return path.join(workDir, 'packages', 'opencode', 'dist', name, 'bin', binary)\n}\n\n// ---------------------------------------------------------------------------\n// Core integration orchestration\n// ---------------------------------------------------------------------------\n\n/**\n * Runs the full integration pipeline:\n * clone → fetch tags → fetch refs → create branch → LLM merge → build → verify → freeze\n *\n * On any failure: returns {ok:false, error} and writes NO manifest (fail-hard contract).\n * On success: writes the provenance manifest to workDir and returns {ok:true, manifest}.\n *\n * @param config - Integration configuration (base version, refs, model, etc.)\n * @param adapters - Injectable adapters for each step (real or stubbed for tests).\n */\nexport async function runIntegration(\n config: IntegrationConfig,\n adapters: IntegrationAdapters,\n): Promise<IntegrationResult> {\n const {baseVersion, releaseRepo, integrationRefs, agent, model, opencodeBin, workDir, promptPath} = config\n const tag = `v${baseVersion}`\n const branch = integrationBranch(baseVersion)\n const channel = 'latest'\n\n // Resolve sources — wrap in try/catch so invalid refs return {ok:false} instead of throwing.\n let sources: IntegrationSource[]\n try {\n sources = resolveSources(integrationRefs, `https://github.com/${releaseRepo}.git`)\n } catch (error) {\n return {ok: false, error: `Resolve sources failed: ${errorMessage(error)}`}\n }\n\n // Step 1: Clone the release repo.\n try {\n await adapters.cloneRepo(`https://github.com/${releaseRepo}.git`, workDir)\n } catch (error) {\n return {ok: false, error: `Clone failed: ${errorMessage(error)}`}\n }\n\n // Step 2: Fetch tags.\n try {\n await adapters.fetchTags(workDir)\n } catch (error) {\n return {ok: false, error: `Fetch tags failed: ${errorMessage(error)}`}\n }\n\n // Step 3: Fetch each integration ref.\n for (const source of sources) {\n try {\n await adapters.fetchRef(workDir, source.repo, source.fetchRef, source.fetch)\n } catch (error) {\n return {ok: false, error: `Fetch ref ${source.label} failed: ${errorMessage(error)}`}\n }\n }\n\n // Step 4: Create/reset the integration branch at the release tag.\n try {\n await adapters.createBranch(workDir, branch, tag)\n } catch (error) {\n return {ok: false, error: `Create branch ${branch} at ${tag} failed: ${errorMessage(error)}`}\n }\n\n // Step 5: Run the LLM merge (only when there are refs to merge).\n if (sources.length > 0) {\n let prompt: string\n try {\n prompt = await renderPrompt(promptPath, workDir, baseVersion, releaseRepo, sources)\n } catch (error) {\n return {ok: false, error: `Render merge prompt failed: ${errorMessage(error)}`}\n }\n\n try {\n await adapters.runMerge(workDir, opencodeBin, agent, model, prompt)\n } catch (error) {\n return {ok: false, error: `LLM merge failed: ${errorMessage(error)}`}\n }\n\n // Step 5.5: Commit the integrated working tree so HEAD contains the merge.\n // Without this, getCommitSha (Step 8) returns the bare tag SHA and\n // git archive would ship the pre-merge tree.\n try {\n await adapters.commitIntegration(workDir, `integrate: apply LLM merge onto v${baseVersion}`)\n } catch (error) {\n return {ok: false, error: `Commit integration failed: ${errorMessage(error)}`}\n }\n\n // Step 6: Build the native CLI.\n try {\n await adapters.buildCli(workDir, baseVersion, channel)\n } catch (error) {\n return {ok: false, error: `Build CLI failed: ${errorMessage(error)}`}\n }\n\n // Step 7: Verify --version matches the base.\n try {\n await adapters.verifyVersion(workDir, baseVersion)\n } catch (error) {\n return {ok: false, error: `Version verification failed: ${errorMessage(error)}`}\n }\n }\n\n // Step 8: Capture the frozen integration commit SHA.\n let integrationCommit: string\n try {\n integrationCommit = await adapters.getCommitSha(workDir)\n } catch (error) {\n return {ok: false, error: `Get commit SHA failed: ${errorMessage(error)}`}\n }\n\n // Step 9: Build the provenance manifest and freeze it.\n // Per-ref SHA resolution is tracked separately; all refs share the integration commit for now.\n const manifest: ProvenanceManifest = {\n baseVersion,\n integrationRefs: sources.map((s, i) => ({\n ref: integrationRefs[i] ?? s.label,\n resolvedSha: integrationCommit,\n })),\n integrationCommit,\n buildSha: 'dev', // replaced by the per-platform build job at publish time\n }\n\n try {\n await writeProvenanceManifest(workDir, manifest)\n } catch (error) {\n return {ok: false, error: `Write provenance manifest failed: ${errorMessage(error)}`}\n }\n\n return {ok: true, manifest}\n}\n\nfunction errorMessage(err: unknown): string {\n if (err instanceof Error) return err.message\n return String(err)\n}\n","/**\n * integrate-command.ts — `harness integrate` subcommand implementation.\n *\n * Reads harness.config.json for: baseVersion, releaseRepo, integrationRefs,\n * agent, model, opencodeBin. Parses --work-dir, --prompt-path, --out from argv.\n * Assembles IntegrationConfig and calls runIntegration(config, makeRealAdapters()).\n *\n * On {ok:true}: packages a clean merged source snapshot (via git archive) plus\n * provenance.json into a single artifact at --out using atomic staging.\n *\n * Exit codes: 0 on {ok:true} + artifact written, 1 on {ok:false} or exception.\n * Error output: one-line message only — no stack traces, no secrets.\n *\n * No classes; functions only; explicit boolean checks; no as-any.\n */\n\nimport type {IntegrationConfig} from './integrate.js'\nimport {execFileSync, execSync} from 'node:child_process'\nimport {copyFileSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync} from 'node:fs'\nimport os from 'node:os'\nimport path from 'node:path'\nimport {fileURLToPath} from 'node:url'\nimport {makeRealAdapters, runIntegration} from './integrate.js'\n\n// ---------------------------------------------------------------------------\n// Config file shape\n// ---------------------------------------------------------------------------\n\ninterface HarnessConfig {\n readonly release_repo: string\n readonly base_version: string\n readonly integrationRefs: readonly string[]\n readonly agent: string\n readonly model: string\n readonly opencode_bin?: string\n}\n\nfunction isValidHarnessConfig(value: unknown): value is HarnessConfig {\n if (value === null || typeof value !== 'object') return false\n const v = value as Record<string, unknown>\n if (typeof v.release_repo !== 'string' || v.release_repo.length === 0) return false\n if (typeof v.base_version !== 'string' || v.base_version.length === 0) return false\n if (!Array.isArray(v.integrationRefs)) return false\n if (!v.integrationRefs.every((el: unknown) => typeof el === 'string' && el.length > 0)) return false\n if (typeof v.agent !== 'string' || v.agent.length === 0) return false\n if (typeof v.model !== 'string' || v.model.length === 0) return false\n if (v.opencode_bin !== undefined && typeof v.opencode_bin !== 'string') return false\n return true\n}\n\n// ---------------------------------------------------------------------------\n// Default config path (relative to this file's package root)\n// ---------------------------------------------------------------------------\n\nconst packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')\nexport const DEFAULT_CONFIG_PATH = path.join(packageRoot, 'harness.config.json')\n\n// ---------------------------------------------------------------------------\n// Flag parsing\n// ---------------------------------------------------------------------------\n\ninterface ParsedFlags {\n readonly workDir: string | undefined\n readonly promptPath: string | undefined\n readonly out: string | undefined\n}\n\nfunction parseFlags(argv: readonly string[]): ParsedFlags | null {\n let workDir: string | undefined\n let promptPath: string | undefined\n let out: string | undefined\n\n for (let i = 0; i < argv.length; i++) {\n const arg = argv[i]\n if (arg === '--work-dir' || arg === '--prompt-path' || arg === '--out') {\n const next = argv[i + 1]\n if (next === undefined || next.startsWith('--')) {\n console.error(`[integrate] ${arg} requires a value`)\n return null\n }\n if (arg === '--work-dir') {\n workDir = next\n } else if (arg === '--prompt-path') {\n promptPath = next\n } else {\n out = next\n }\n i++\n }\n }\n\n return {workDir, promptPath, out}\n}\n\n// ---------------------------------------------------------------------------\n// Artifact packaging\n// ---------------------------------------------------------------------------\n\n/**\n * Packages a clean merged source snapshot plus provenance.json into a single\n * tar artifact at outPath using atomic staging.\n *\n * Steps:\n * 1. Create a temp staging dir.\n * 2. Run `git archive --format=tar --output=<tmp>/source.tar <integrationCommit>` in workDir.\n * 3. Extract source.tar into <tmp>/tree, copy provenance.json into <tmp>/tree.\n * 4. Re-tar <tmp>/tree → <tmp>/artifact.tar.\n * 5. Ensure outPath parent dir exists, then atomically rename <tmp>/artifact.tar → outPath.\n * 6. Clean the temp dir in a finally block.\n *\n * ATOMIC: the rename only happens after the artifact is fully built. Any error\n * before the rename leaves outPath untouched.\n *\n * @param workDir - The integration work directory (contains the git repo + provenance.json).\n * @param integrationCommit - The commit SHA to archive (the frozen integration commit).\n * @param outPath - Destination path for the final artifact tar.\n */\nexport async function packageArtifact(workDir: string, integrationCommit: string, outPath: string): Promise<void> {\n // Belt-and-suspenders guard: fail loudly if the working tree has uncommitted tracked changes.\n // After FIX 1 commits the merge, tracked changes should always be committed.\n // Untracked files (e.g. provenance.json written by the harness) are intentionally excluded\n // from this check — they are copied into the artifact separately.\n // `git status --porcelain` lines starting with '??' are untracked; we only care about the rest.\n const statusOutput = execSync('git status --porcelain', {cwd: workDir, encoding: 'utf8'})\n const trackedDirtyLines = statusOutput.split('\\n').filter(line => line.length > 0 && !line.startsWith('??'))\n if (trackedDirtyLines.length > 0) {\n throw new Error(\n `[integrate] Working tree has uncommitted tracked changes before git archive — these would be excluded from the artifact:\\n${trackedDirtyLines.join('\\n')}`,\n )\n }\n\n const tmpStaging = mkdtempSync(path.join(os.tmpdir(), 'harness-artifact-'))\n try {\n const sourceTar = path.join(tmpStaging, 'source.tar')\n const treeDir = path.join(tmpStaging, 'tree')\n const artifactTar = path.join(tmpStaging, 'artifact.tar')\n\n // Step 2: Extract the clean merged source tree from the integration commit.\n execFileSync('git', ['archive', '--format=tar', `--output=${sourceTar}`, integrationCommit], {\n cwd: workDir,\n stdio: ['ignore', 'ignore', 'pipe'],\n })\n\n // Step 3a: Extract source.tar into tree dir.\n mkdirSync(treeDir, {recursive: true})\n execFileSync('tar', ['xf', sourceTar, '-C', treeDir], {\n stdio: ['ignore', 'ignore', 'pipe'],\n })\n\n // Step 3b: Copy provenance.json from workDir into the tree.\n copyFileSync(path.join(workDir, 'provenance.json'), path.join(treeDir, 'provenance.json'))\n\n // Step 4: Re-tar the tree (with provenance.json included) into artifact.tar.\n execFileSync('tar', ['cf', artifactTar, '-C', treeDir, '.'], {\n stdio: ['ignore', 'ignore', 'pipe'],\n })\n\n // Step 5: Ensure outPath parent exists, then atomically promote the artifact.\n mkdirSync(path.dirname(outPath), {recursive: true})\n renameSync(artifactTar, outPath)\n } finally {\n // Always clean the temp dir, even on error. Ignore cleanup failures.\n try {\n rmSync(tmpStaging, {recursive: true, force: true})\n } catch {\n // Intentionally swallowed — cleanup failure must not mask the real error.\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Main command\n// ---------------------------------------------------------------------------\n\n/**\n * Implements `harness integrate`.\n *\n * @param argv - CLI arguments (everything after \"integrate\").\n * @param configPath - Path to harness.config.json (defaults to package root; injectable for tests).\n * @param _packageArtifact - Injectable override for packageArtifact (for unit tests; defaults to the real impl).\n * @returns Exit code: 0 on success, 1 on failure.\n */\nexport async function cmdIntegrate(\n argv: readonly string[],\n configPath: string = DEFAULT_CONFIG_PATH,\n _packageArtifact: typeof packageArtifact = packageArtifact,\n): Promise<number> {\n // Parse flags.\n const flags = parseFlags(argv)\n if (flags === null) return 1\n\n // Validate required flags.\n if (flags.workDir === undefined) {\n console.error('[integrate] Missing required flag: --work-dir <dir>')\n return 1\n }\n if (flags.promptPath === undefined) {\n console.error('[integrate] Missing required flag: --prompt-path <path>')\n return 1\n }\n if (flags.out === undefined) {\n console.error('[integrate] Missing required flag: --out <path>')\n return 1\n }\n\n const workDir = flags.workDir\n const outPath = flags.out\n\n // Read harness.config.json.\n let rawConfig: unknown\n try {\n const raw = readFileSync(configPath, 'utf8')\n rawConfig = JSON.parse(raw)\n } catch (error) {\n const msg = error instanceof Error ? error.message : String(error)\n console.error(`[integrate] Failed to read config: ${msg}`)\n return 1\n }\n\n if (!isValidHarnessConfig(rawConfig)) {\n console.error('[integrate] Invalid harness.config.json shape')\n return 1\n }\n\n const config: IntegrationConfig = {\n baseVersion: rawConfig.base_version,\n releaseRepo: rawConfig.release_repo,\n integrationRefs: rawConfig.integrationRefs,\n agent: rawConfig.agent,\n model: rawConfig.model,\n opencodeBin: rawConfig.opencode_bin ?? 'opencode',\n workDir,\n promptPath: flags.promptPath,\n }\n\n // Run the integration and package the artifact.\n try {\n const result = await runIntegration(config, makeRealAdapters())\n if (result.ok === true) {\n await _packageArtifact(workDir, result.manifest.integrationCommit, outPath)\n return 0\n }\n console.error(`[integrate] ${result.error}`)\n return 1\n } catch (error) {\n const msg = error instanceof Error ? error.message : String(error)\n console.error(`[integrate] ${msg}`)\n return 1\n }\n}\n","/**\n * Provenance manifest for the harness binary.\n *\n * The manifest is the single source of truth written by the integration engine\n * and read by the CLI commands (info, patches, doctor).\n *\n * baseVersion — the upstream OpenCode release tag this binary is based on.\n * integrationRefs — ordered list of integration refs carried onto the base tag,\n * each with the resolved commit SHA and optional metadata.\n * integrationCommit — the frozen integration commit SHA produced by the LLM merge.\n * buildSha — the git SHA of the harness build, or 'dev' in the scaffold.\n */\nimport {readFileSync} from 'node:fs'\nimport path from 'node:path'\nimport {fileURLToPath} from 'node:url'\n\nexport interface IntegrationRefRecord {\n readonly ref: string\n readonly resolvedSha: string\n readonly reason?: string\n readonly upstreamStatus?: string\n}\n\nexport interface Provenance {\n readonly baseVersion: string\n readonly integrationRefs: readonly IntegrationRefRecord[]\n readonly integrationCommit: string | null\n readonly buildSha: string\n}\n\nconst packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')\n\n/**\n * Type guard: validates that an unknown value has the shape of a Provenance manifest.\n * Treats malformed JSON as an explicit error rather than silently returning partial data.\n */\nexport function isValidProvenance(value: unknown): value is Provenance {\n if (value === null || typeof value !== 'object') return false\n const v = value as Record<string, unknown>\n if (typeof v.baseVersion !== 'string' || v.baseVersion.length === 0) return false\n if (!Array.isArray(v.integrationRefs)) return false\n if (v.integrationCommit !== null && typeof v.integrationCommit !== 'string') return false\n if (typeof v.buildSha !== 'string') return false\n return true\n}\n\n/**\n * Type guard: validates that an unknown value has the shape of a harness.config.json.\n */\nfunction isValidHarnessConfig(value: unknown): value is {base_version?: string; integrationRefs?: string[]} {\n if (value === null || typeof value !== 'object') return false\n const v = value as Record<string, unknown>\n if (v.base_version !== undefined && typeof v.base_version !== 'string') return false\n if (v.integrationRefs !== undefined && !Array.isArray(v.integrationRefs)) return false\n return true\n}\n\n/**\n * Returns the provenance for the current harness binary.\n *\n * Resolution order:\n * 1. Bundled provenance.json (written by the integration engine at build time).\n * 2. harness.config.json (dev scaffold — shows configured refs without a frozen commit).\n * 3. Hardcoded dev placeholder (no config file present).\n *\n * The integration engine writes provenance.json at build time; this function reads\n * it at runtime, making the manifest available without additional filesystem reads\n * in production.\n */\nexport function getProvenance(): Provenance {\n // 1. Try bundled provenance.json (written by the integration engine).\n try {\n const manifestPath = path.join(packageRoot, 'provenance.json')\n const raw = readFileSync(manifestPath, 'utf8')\n const parsed: unknown = JSON.parse(raw)\n if (isValidProvenance(parsed)) {\n return parsed\n }\n // Malformed manifest — fall through to dev scaffold.\n } catch {\n // No bundled manifest — fall through.\n }\n\n // 2. Fall back to harness.config.json for the dev scaffold.\n try {\n const configPath = path.join(packageRoot, 'harness.config.json')\n const raw = readFileSync(configPath, 'utf8')\n const parsed: unknown = JSON.parse(raw)\n if (!isValidHarnessConfig(parsed)) {\n // Malformed config — fall through to hardcoded placeholder.\n throw new Error('Invalid harness.config.json shape')\n }\n const baseVersion = parsed.base_version ?? '1.15.13'\n const integrationRefs: IntegrationRefRecord[] = (parsed.integrationRefs ?? []).map(ref => ({\n ref,\n resolvedSha: 'dev',\n }))\n return {\n baseVersion,\n integrationRefs,\n integrationCommit: null,\n buildSha: 'dev',\n }\n } catch {\n // No config file — return hardcoded placeholder.\n }\n\n return {\n baseVersion: '1.15.13',\n integrationRefs: [],\n integrationCommit: null,\n buildSha: 'dev',\n }\n}\n\n/**\n * Formats provenance as a human-readable string for `harness info`.\n */\nexport function formatProvenance(p: Provenance): string {\n const lines: string[] = [\n `harness (patched OpenCode)`,\n ` base: ${p.baseVersion}`,\n ` integration commit: ${p.integrationCommit ?? '(unbuilt/dev scaffold)'}`,\n ` build sha: ${p.buildSha}`,\n ]\n if (p.integrationRefs.length > 0) {\n lines.push(` integration refs:`)\n for (const r of p.integrationRefs) {\n const meta = r.upstreamStatus === undefined ? '' : ` [${r.upstreamStatus}]`\n lines.push(` - ${r.ref}${meta}`)\n if (r.reason !== undefined) {\n lines.push(` reason: ${r.reason}`)\n }\n }\n } else {\n lines.push(` integration refs: (none — dev scaffold)`)\n }\n return lines.join('\\n')\n}\n","/**\n * Resolves the patched OpenCode binary for the current host.\n *\n * Resolution order (precedence, highest to lowest):\n * 0. OPENCODE_PATH env override — always honoured; marks isBuilt: false.\n * 1. Host-platform optionalDependencies binary (the real harness-built artifact).\n * Resolved via Node module resolution (createRequire) so pnpm/npm hoisting\n * is handled correctly — the platform package may be hoisted outside the\n * local node_modules tree.\n * 2. PATH fallback (`opencode` on PATH) — ONLY when an explicit dev escape hatch\n * is active: HARNESS_ALLOW_PATH_FALLBACK=1 or OPENCODE_PATH is set.\n * In published/production use (no escape hatch), a missing platform binary\n * is a hard error with an actionable message.\n *\n * The integrity check in step 1 is a basic executable-probe (--version succeeds).\n * A full cryptographic integrity check (npm provenance) is enforced by the\n * postinstall resolver at install time; this runtime check is a belt-and-suspenders\n * guard against a corrupted or missing binary.\n */\n\nimport {execFileSync} from 'node:child_process'\nimport {createRequire} from 'node:module'\nimport path from 'node:path'\nimport process from 'node:process'\nimport {binaryPathInPackage, getHostPlatformInfo} from './platform.js'\n\n/**\n * Result of binary resolution.\n *\n * resolved — true when a usable binary was found.\n * path — the resolved binary path or command name.\n * isBuilt — true when the binary is a real harness-built artifact.\n * false in the dev scaffold (falls back to opencode on PATH).\n */\nexport interface ResolvedBinary {\n readonly resolved: boolean\n readonly path: string\n readonly isBuilt: boolean\n}\n\n/**\n * Attempts to resolve the host-platform binary from the installed\n * @fro.bot/harness-<os>-<arch> optionalDependencies package.\n *\n * Uses Node module resolution (createRequire) to locate the package, which\n * correctly handles pnpm/npm hoisting — the platform package may be installed\n * outside the local node_modules tree.\n *\n * Returns the binary path if found and executable, or null otherwise.\n */\nfunction resolveOptionalDepBinary(): string | null {\n const platformResult = getHostPlatformInfo()\n if (!platformResult.ok) {\n // Unsupported platform — no platform binary available.\n return null\n }\n\n const info = platformResult.info\n\n // Resolve the platform package via Node module resolution.\n // This handles pnpm/npm hoisting correctly — the package may not be in\n // a local node_modules directory.\n const require = createRequire(import.meta.url)\n let platformPkgRoot: string\n try {\n const pkgJsonPath = require.resolve(`${info.packageName}/package.json`)\n platformPkgRoot = path.dirname(pkgJsonPath)\n } catch {\n // Platform package not installed (optional dependency absent).\n return null\n }\n\n const binaryPath = binaryPathInPackage(platformPkgRoot, info)\n\n // Basic executable probe — confirm the binary runs before returning it.\n try {\n execFileSync(binaryPath, ['--version'], {\n encoding: 'utf8',\n timeout: 10_000,\n stdio: ['ignore', 'pipe', 'pipe'],\n })\n return binaryPath\n } catch {\n return null\n }\n}\n\n/**\n * Returns true when an explicit dev escape hatch is active.\n *\n * The escape hatch allows PATH fallback in local/dev/unbuilt environments.\n * It is NEVER active in published/production use (no env set by default).\n *\n * Escape hatches:\n * - HARNESS_ALLOW_PATH_FALLBACK=1 — explicit opt-in for dev/CI without platform binary.\n * - OPENCODE_PATH set — explicit override already provided; PATH fallback is moot.\n */\nfunction isDevEscapeHatchActive(): boolean {\n return (\n process.env.HARNESS_ALLOW_PATH_FALLBACK === '1' ||\n (process.env.OPENCODE_PATH !== undefined && process.env.OPENCODE_PATH.length > 0)\n )\n}\n\n/**\n * Resolves the patched OpenCode binary for the current host.\n *\n * Resolution order:\n * 0. OPENCODE_PATH env override.\n * 1. Host-platform optionalDependencies binary (isBuilt: true).\n * 2. PATH fallback — ONLY when HARNESS_ALLOW_PATH_FALLBACK=1 (dev escape hatch).\n * In production (no escape hatch), missing platform binary → throws with remediation.\n *\n * @throws {Error} when no platform binary is found and no dev escape hatch is active.\n */\nexport function resolveBinary(): ResolvedBinary {\n // 0. Explicit override — always wins.\n const override = process.env.OPENCODE_PATH\n if (override !== undefined && override.length > 0) {\n return {resolved: true, path: override, isBuilt: false}\n }\n\n // 1. Host-platform optionalDependencies binary.\n const optionalBinary = resolveOptionalDepBinary()\n if (optionalBinary !== null) {\n return {resolved: true, path: optionalBinary, isBuilt: true}\n }\n\n // 2. No platform binary found.\n // In dev/unbuilt environments with an explicit escape hatch, fall back to PATH.\n // In production (no escape hatch), fail closed with an actionable error.\n if (isDevEscapeHatchActive()) {\n return {resolved: true, path: 'opencode', isBuilt: false}\n }\n\n // Determine which platform package was expected for the error message.\n const platformResult = getHostPlatformInfo()\n const expectedPkg = platformResult.ok ? platformResult.info.packageName : '@fro.bot/harness-<os>-<arch>'\n\n throw new Error(\n `[harness] Platform binary not found. Expected package: ${expectedPkg}\\n` +\n ` Remediation: ensure ${expectedPkg} is installed as an optionalDependency,\\n` +\n ` or set OPENCODE_PATH to an explicit binary path,\\n` +\n ` or set HARNESS_ALLOW_PATH_FALLBACK=1 to use opencode on PATH (dev only).`,\n )\n}\n\n/**\n * Checks whether the resolved binary is present and runnable by invoking it\n * with `--version`. Returns the version string on success, or null on failure.\n */\nexport function probeBinary(binaryPath: string): string | null {\n try {\n const output = execFileSync(binaryPath, ['--version'], {\n encoding: 'utf8',\n timeout: 10_000,\n stdio: ['ignore', 'pipe', 'pipe'],\n })\n return output.trim()\n } catch {\n return null\n }\n}\n","#!/usr/bin/env node\n/**\n * harness CLI — patched OpenCode binary with provenance/operability commands.\n *\n * Subcommand disambiguation:\n * Reserved harness subcommands: info, patches, doctor\n * --version / --help: harness-own (prints provenance / usage)\n * Everything else: passed through to the resolved patched binary.\n *\n * This is the ONLY entry point for the @fro.bot/harness package.\n * No classes; functions only; explicit boolean checks; no as-any.\n */\n\nimport {spawnSync} from 'node:child_process'\nimport process from 'node:process'\nimport {cmdIntegrate} from './integrate-command.js'\nimport {formatProvenance, getProvenance} from './provenance.js'\nimport {probeBinary, resolveBinary} from './resolve-binary.js'\n\nfunction printUsage(): void {\n console.log(`harness — patched OpenCode binary (Fro Bot integration)\n\nUsage:\n harness <opencode-args...> Pass through to the patched OpenCode binary\n harness info Print provenance (base version, integration refs, build sha)\n harness patches List configured integration refs\n harness doctor Check the resolved binary is present and runnable\n harness integrate Run the LLM merge integration pipeline\n --work-dir <dir> (required) Working directory for the clone\n --prompt-path <path> (required) Path to the merge prompt template\n --out <path> (required) Artifact output path\n harness --version Print harness provenance version\n harness --help Print this help\n\nReserved subcommands (info, patches, doctor, integrate) are handled by harness itself.\nAll other arguments are forwarded to the patched OpenCode binary.`)\n}\n\nfunction cmdInfo(): void {\n const p = getProvenance()\n console.log(formatProvenance(p))\n}\n\nfunction cmdPatches(): void {\n const p = getProvenance()\n if (p.integrationRefs.length === 0) {\n console.log('No integration refs configured (dev scaffold).')\n return\n }\n console.log('Integration refs:')\n for (const r of p.integrationRefs) {\n const status = r.upstreamStatus === undefined ? '' : ` [${r.upstreamStatus}]`\n console.log(` - ${r.ref}${status}`)\n if (r.reason !== undefined) {\n console.log(` reason: ${r.reason}`)\n }\n }\n if (p.integrationCommit !== null) {\n console.log(`\\nFrozen integration commit: ${p.integrationCommit}`)\n }\n}\n\nfunction cmdDoctor(): number {\n let binary\n try {\n binary = resolveBinary()\n } catch (error) {\n const msg = error instanceof Error ? error.message : String(error)\n console.error(`\\n[FAIL] ${msg}`)\n return 1\n }\n\n const p = getProvenance()\n\n console.log(`harness doctor`)\n console.log(` base version: ${p.baseVersion}`)\n console.log(` integration commit: ${p.integrationCommit ?? '(unbuilt/dev scaffold)'}`)\n console.log(` build sha: ${p.buildSha}`)\n console.log(` binary path: ${binary.path}`)\n console.log(` is built artifact: ${binary.isBuilt}`)\n\n // In production (isBuilt: false with no dev escape hatch), fail with remediation.\n if (!binary.isBuilt && process.env.HARNESS_ALLOW_PATH_FALLBACK !== '1' && process.env.OPENCODE_PATH === undefined) {\n console.error('\\n[FAIL] Binary is not a built harness artifact.')\n console.error(\n ' Install the platform package or set OPENCODE_PATH / HARNESS_ALLOW_PATH_FALLBACK=1 for dev use.',\n )\n return 1\n }\n\n const version = probeBinary(binary.path)\n if (version === null) {\n console.error(`\\n[FAIL] Binary not runnable: ${binary.path}`)\n console.error(' Ensure opencode is on PATH or set OPENCODE_PATH.')\n return 1\n }\n\n console.log(` binary version: ${version}`)\n\n // Verify binary version matches provenance baseVersion when we have a built artifact.\n if (binary.isBuilt && version !== p.baseVersion) {\n console.error(\n `\\n[FAIL] Binary version mismatch: binary reports '${version}', provenance expects '${p.baseVersion}'.`,\n )\n console.error(' Reinstall @fro.bot/harness or check the platform package version.')\n return 1\n }\n\n console.log('\\n[OK] Binary is present and runnable.')\n return 0\n}\n\nfunction cmdPassthrough(args: readonly string[]): number {\n let binary\n try {\n binary = resolveBinary()\n } catch (error) {\n const msg = error instanceof Error ? error.message : String(error)\n console.error(`[harness] ${msg}`)\n return 1\n }\n\n const result = spawnSync(binary.path, [...args], {\n stdio: 'inherit',\n env: process.env,\n })\n\n if (result.error !== undefined) {\n console.error(`[harness] Failed to spawn ${binary.path}: ${result.error.message}`)\n return 1\n }\n\n return result.status ?? 1\n}\n\nasync function main(): Promise<void> {\n const args = process.argv.slice(2)\n\n // --help: harness-own\n if (args[0] === '--help' || args[0] === '-h') {\n printUsage()\n process.exit(0)\n }\n\n // --version: harness-own provenance version\n if (args[0] === '--version' || args[0] === '-v') {\n const p = getProvenance()\n console.log(`@fro.bot/harness base:${p.baseVersion} build:${p.buildSha}`)\n process.exit(0)\n }\n\n const subcommand = args[0]\n\n if (subcommand === 'info') {\n cmdInfo()\n process.exit(0)\n }\n\n if (subcommand === 'patches') {\n cmdPatches()\n process.exit(0)\n }\n\n if (subcommand === 'doctor') {\n const code = cmdDoctor()\n process.exit(code)\n }\n\n if (subcommand === 'integrate') {\n const code = await cmdIntegrate(args.slice(1))\n process.exit(code)\n }\n\n // Anything not in the reserved set passes through.\n // This includes no-args (which opencode handles as its own help/default).\n const code = cmdPassthrough(args)\n process.exit(code)\n}\n\nmain().catch(error => {\n const msg = error instanceof Error ? error.message : String(error)\n console.error(`[harness] Unexpected error: ${msg}`)\n process.exit(1)\n})\n"],"mappings":"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"}
@@ -0,0 +1,65 @@
1
+ import process from "node:process";
2
+ //#region src/platform.ts
3
+ /**
4
+ * Host platform/arch detection → optionalDependencies package name.
5
+ *
6
+ * Maps the current Node.js process.platform + process.arch to the
7
+ * @fro.bot/harness-<os>-<arch> package name that ships the native binary.
8
+ *
9
+ * Mirrors OpenCode's per-platform packaging model (anomalyco/opencode script/publish.ts).
10
+ * Windows is out of scope (matrix: linux x64/arm64 + darwin x64/arm64 only).
11
+ */
12
+ /**
13
+ * Returns the platform result for the given os/arch pair.
14
+ *
15
+ * Returns {ok: false} when the platform is not in the supported matrix
16
+ * (linux/darwin × x64/arm64 only; no windows).
17
+ *
18
+ * @param os - Node.js process.platform value (e.g. 'linux', 'darwin', 'win32').
19
+ * @param arch - Node.js process.arch value (e.g. 'x64', 'arm64').
20
+ */
21
+ function getPlatformInfo(os, arch) {
22
+ if (os !== "linux" && os !== "darwin") return {
23
+ ok: false,
24
+ error: `Unsupported platform: ${os}/${arch}. @fro.bot/harness supports linux/darwin × x64/arm64 only (no windows).`
25
+ };
26
+ if (arch !== "x64" && arch !== "arm64") return {
27
+ ok: false,
28
+ error: `Unsupported platform: ${os}/${arch}. @fro.bot/harness supports linux/darwin × x64/arm64 only (no windows).`
29
+ };
30
+ const supportedOs = os;
31
+ const supportedArch = arch;
32
+ return {
33
+ ok: true,
34
+ info: {
35
+ os: supportedOs,
36
+ arch: supportedArch,
37
+ packageName: `@fro.bot/harness-${supportedOs}-${supportedArch}`,
38
+ binaryName: "opencode"
39
+ }
40
+ };
41
+ }
42
+ /**
43
+ * Returns the platform result for the current host process.
44
+ *
45
+ * Returns {ok: false} when the host platform is not in the supported matrix.
46
+ */
47
+ function getHostPlatformInfo() {
48
+ return getPlatformInfo(process.platform, process.arch);
49
+ }
50
+ /**
51
+ * Returns the expected binary path inside an installed optionalDependencies package.
52
+ *
53
+ * The per-platform packages ship the native binary at:
54
+ * <packageRoot>/bin/<binaryName>
55
+ *
56
+ * @param packageRoot - Absolute path to the installed platform package root.
57
+ * @param info - Platform info (from getPlatformInfo or getHostPlatformInfo).
58
+ */
59
+ function binaryPathInPackage(packageRoot, info) {
60
+ return `${packageRoot}/bin/${info.binaryName}`;
61
+ }
62
+ //#endregion
63
+ export { getHostPlatformInfo as n, binaryPathInPackage as t };
64
+
65
+ //# sourceMappingURL=platform-CKoiiV92.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"platform-CKoiiV92.mjs","names":[],"sources":["../src/platform.ts"],"sourcesContent":["/**\n * Host platform/arch detection → optionalDependencies package name.\n *\n * Maps the current Node.js process.platform + process.arch to the\n * @fro.bot/harness-<os>-<arch> package name that ships the native binary.\n *\n * Mirrors OpenCode's per-platform packaging model (anomalyco/opencode script/publish.ts).\n * Windows is out of scope (matrix: linux x64/arm64 + darwin x64/arm64 only).\n */\n\nimport process from 'node:process'\n\n/** Supported OS identifiers (Node.js process.platform values we handle). */\nexport type SupportedOs = 'linux' | 'darwin'\n\n/** Supported CPU arch identifiers (Node.js process.arch values we handle). */\nexport type SupportedArch = 'x64' | 'arm64'\n\n/** A resolved platform identity. */\nexport interface PlatformInfo {\n readonly os: SupportedOs\n readonly arch: SupportedArch\n /** The @fro.bot/harness-<os>-<arch> package name for this platform. */\n readonly packageName: string\n /** The binary filename inside the package (always 'opencode' on supported platforms). */\n readonly binaryName: string\n}\n\n/**\n * Discriminated result for platform detection.\n *\n * ok: true → platform is supported; info contains the resolved PlatformInfo.\n * ok: false → platform is not in the supported matrix; error contains a human-readable message.\n */\nexport type PlatformResult =\n | {readonly ok: true; readonly info: PlatformInfo}\n | {readonly ok: false; readonly error: string}\n\n/**\n * Returns the platform result for the given os/arch pair.\n *\n * Returns {ok: false} when the platform is not in the supported matrix\n * (linux/darwin × x64/arm64 only; no windows).\n *\n * @param os - Node.js process.platform value (e.g. 'linux', 'darwin', 'win32').\n * @param arch - Node.js process.arch value (e.g. 'x64', 'arm64').\n */\nexport function getPlatformInfo(os: string, arch: string): PlatformResult {\n if (os !== 'linux' && os !== 'darwin') {\n return {\n ok: false,\n error: `Unsupported platform: ${os}/${arch}. @fro.bot/harness supports linux/darwin × x64/arm64 only (no windows).`,\n }\n }\n if (arch !== 'x64' && arch !== 'arm64') {\n return {\n ok: false,\n error: `Unsupported platform: ${os}/${arch}. @fro.bot/harness supports linux/darwin × x64/arm64 only (no windows).`,\n }\n }\n\n const supportedOs: SupportedOs = os\n const supportedArch: SupportedArch = arch\n\n return {\n ok: true,\n info: {\n os: supportedOs,\n arch: supportedArch,\n packageName: `@fro.bot/harness-${supportedOs}-${supportedArch}`,\n binaryName: 'opencode',\n },\n }\n}\n\n/**\n * Returns the platform result for the current host process.\n *\n * Returns {ok: false} when the host platform is not in the supported matrix.\n */\nexport function getHostPlatformInfo(): PlatformResult {\n return getPlatformInfo(process.platform, process.arch)\n}\n\n/**\n * Returns the expected binary path inside an installed optionalDependencies package.\n *\n * The per-platform packages ship the native binary at:\n * <packageRoot>/bin/<binaryName>\n *\n * @param packageRoot - Absolute path to the installed platform package root.\n * @param info - Platform info (from getPlatformInfo or getHostPlatformInfo).\n */\nexport function binaryPathInPackage(packageRoot: string, info: PlatformInfo): string {\n return `${packageRoot}/bin/${info.binaryName}`\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AA+CA,SAAgB,gBAAgB,IAAY,MAA8B;CACxE,IAAI,OAAO,WAAW,OAAO,UAC3B,OAAO;EACL,IAAI;EACJ,OAAO,yBAAyB,GAAG,GAAG,KAAK;CAC7C;CAEF,IAAI,SAAS,SAAS,SAAS,SAC7B,OAAO;EACL,IAAI;EACJ,OAAO,yBAAyB,GAAG,GAAG,KAAK;CAC7C;CAGF,MAAM,cAA2B;CACjC,MAAM,gBAA+B;CAErC,OAAO;EACL,IAAI;EACJ,MAAM;GACJ,IAAI;GACJ,MAAM;GACN,aAAa,oBAAoB,YAAY,GAAG;GAChD,YAAY;EACd;CACF;AACF;;;;;;AAOA,SAAgB,sBAAsC;CACpD,OAAO,gBAAgB,QAAQ,UAAU,QAAQ,IAAI;AACvD;;;;;;;;;;AAWA,SAAgB,oBAAoB,aAAqB,MAA4B;CACnF,OAAO,GAAG,YAAY,OAAO,KAAK;AACpC"}
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,68 @@
1
+ import { n as getHostPlatformInfo, t as binaryPathInPackage } from "./platform-CKoiiV92.mjs";
2
+ import { createRequire } from "node:module";
3
+ import { execFileSync } from "node:child_process";
4
+ import process from "node:process";
5
+ import path from "node:path";
6
+ //#region src/postinstall.ts
7
+ /**
8
+ * postinstall.ts — integrity-verifying binary resolver run at install time.
9
+ *
10
+ * Runs after `npm install @fro.bot/harness` to:
11
+ * 1. Detect the host platform.
12
+ * 2. Locate the per-platform binary from the installed optionalDependencies package
13
+ * via Node module resolution (handles pnpm/npm hoisting correctly).
14
+ * 3. Verify the binary is executable (basic probe — npm provenance attestation
15
+ * is the cryptographic integrity gate; this is a belt-and-suspenders exec check).
16
+ * 4. Print a clear status message.
17
+ *
18
+ * Exits 0 on success or when no platform binary is available (optional dep not installed).
19
+ * The `|| true` in package.json scripts.postinstall ensures install never fails here.
20
+ *
21
+ * A missing binary is NOT a fatal install error — the harness falls back to the
22
+ * dev scaffold (opencode on PATH). The action setup fails loud if the binary is
23
+ * absent in CI, where it is required.
24
+ */
25
+ function log(msg) {
26
+ process.stderr.write(`[harness postinstall] ${msg}\n`);
27
+ }
28
+ function main() {
29
+ const platformResult = getHostPlatformInfo();
30
+ if (!platformResult.ok) {
31
+ log(`Platform not supported: ${platformResult.error}`);
32
+ log("No platform binary available. Using opencode on PATH as fallback.");
33
+ process.exit(0);
34
+ }
35
+ const info = platformResult.info;
36
+ log(`Host platform: ${info.os}/${info.arch} → ${info.packageName}`);
37
+ const require = createRequire(import.meta.url);
38
+ let platformPkgRoot;
39
+ try {
40
+ const pkgJsonPath = require.resolve(`${info.packageName}/package.json`);
41
+ platformPkgRoot = path.dirname(pkgJsonPath);
42
+ } catch {
43
+ log(`Platform package ${info.packageName} not found (optional dependency not installed).`);
44
+ log("Using opencode on PATH as fallback.");
45
+ process.exit(0);
46
+ }
47
+ const binaryPath = binaryPathInPackage(platformPkgRoot, info);
48
+ try {
49
+ log(`Binary verified: ${binaryPath} (version: ${execFileSync(binaryPath, ["--version"], {
50
+ encoding: "utf8",
51
+ timeout: 1e4,
52
+ stdio: [
53
+ "ignore",
54
+ "pipe",
55
+ "pipe"
56
+ ]
57
+ }).trim()})`);
58
+ log("Platform binary ready.");
59
+ } catch {
60
+ log(`Binary found but not executable: ${binaryPath}`);
61
+ log("Using opencode on PATH as fallback.");
62
+ }
63
+ }
64
+ main();
65
+ //#endregion
66
+ export {};
67
+
68
+ //# sourceMappingURL=postinstall.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"postinstall.mjs","names":[],"sources":["../src/postinstall.ts"],"sourcesContent":["/**\n * postinstall.ts — integrity-verifying binary resolver run at install time.\n *\n * Runs after `npm install @fro.bot/harness` to:\n * 1. Detect the host platform.\n * 2. Locate the per-platform binary from the installed optionalDependencies package\n * via Node module resolution (handles pnpm/npm hoisting correctly).\n * 3. Verify the binary is executable (basic probe — npm provenance attestation\n * is the cryptographic integrity gate; this is a belt-and-suspenders exec check).\n * 4. Print a clear status message.\n *\n * Exits 0 on success or when no platform binary is available (optional dep not installed).\n * The `|| true` in package.json scripts.postinstall ensures install never fails here.\n *\n * A missing binary is NOT a fatal install error — the harness falls back to the\n * dev scaffold (opencode on PATH). The action setup fails loud if the binary is\n * absent in CI, where it is required.\n */\n\nimport {execFileSync} from 'node:child_process'\nimport {createRequire} from 'node:module'\nimport path from 'node:path'\nimport process from 'node:process'\nimport {binaryPathInPackage, getHostPlatformInfo} from './platform.js'\n\nfunction log(msg: string): void {\n process.stderr.write(`[harness postinstall] ${msg}\\n`)\n}\n\nfunction main(): void {\n // 1. Detect host platform.\n const platformResult = getHostPlatformInfo()\n if (!platformResult.ok) {\n log(`Platform not supported: ${platformResult.error}`)\n log('No platform binary available. Using opencode on PATH as fallback.')\n process.exit(0)\n }\n\n const info = platformResult.info\n log(`Host platform: ${info.os}/${info.arch} → ${info.packageName}`)\n\n // 2. Locate the platform binary via Node module resolution.\n // createRequire resolves from this file's location, correctly handling\n // pnpm/npm hoisting (the platform package may not be in a local node_modules).\n const require = createRequire(import.meta.url)\n let platformPkgRoot: string\n try {\n const pkgJsonPath = require.resolve(`${info.packageName}/package.json`)\n platformPkgRoot = path.dirname(pkgJsonPath)\n } catch {\n log(`Platform package ${info.packageName} not found (optional dependency not installed).`)\n log('Using opencode on PATH as fallback.')\n process.exit(0)\n }\n\n const binaryPath = binaryPathInPackage(platformPkgRoot, info)\n\n // 3. Verify the binary is executable.\n try {\n const version = execFileSync(binaryPath, ['--version'], {\n encoding: 'utf8',\n timeout: 10_000,\n stdio: ['ignore', 'pipe', 'pipe'],\n }).trim()\n log(`Binary verified: ${binaryPath} (version: ${version})`)\n log('Platform binary ready.')\n } catch {\n log(`Binary found but not executable: ${binaryPath}`)\n log('Using opencode on PATH as fallback.')\n }\n}\n\nmain()\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAyBA,SAAS,IAAI,KAAmB;CAC9B,QAAQ,OAAO,MAAM,yBAAyB,IAAI,GAAG;AACvD;AAEA,SAAS,OAAa;CAEpB,MAAM,iBAAiB,oBAAoB;CAC3C,IAAI,CAAC,eAAe,IAAI;EACtB,IAAI,2BAA2B,eAAe,OAAO;EACrD,IAAI,mEAAmE;EACvE,QAAQ,KAAK,CAAC;CAChB;CAEA,MAAM,OAAO,eAAe;CAC5B,IAAI,kBAAkB,KAAK,GAAG,GAAG,KAAK,KAAK,KAAK,KAAK,aAAa;CAKlE,MAAM,UAAU,cAAc,OAAO,KAAK,GAAG;CAC7C,IAAI;CACJ,IAAI;EACF,MAAM,cAAc,QAAQ,QAAQ,GAAG,KAAK,YAAY,cAAc;EACtE,kBAAkB,KAAK,QAAQ,WAAW;CAC5C,QAAQ;EACN,IAAI,oBAAoB,KAAK,YAAY,gDAAgD;EACzF,IAAI,qCAAqC;EACzC,QAAQ,KAAK,CAAC;CAChB;CAEA,MAAM,aAAa,oBAAoB,iBAAiB,IAAI;CAG5D,IAAI;EAMF,IAAI,oBAAoB,WAAW,aALnB,aAAa,YAAY,CAAC,WAAW,GAAG;GACtD,UAAU;GACV,SAAS;GACT,OAAO;IAAC;IAAU;IAAQ;GAAM;EAClC,CAAC,EAAE,KACmD,EAAE,EAAE;EAC1D,IAAI,wBAAwB;CAC9B,QAAQ;EACN,IAAI,oCAAoC,YAAY;EACpD,IAAI,qCAAqC;CAC3C;AACF;AAEA,KAAK"}
package/package.json CHANGED
@@ -1,9 +1,47 @@
1
1
  {
2
2
  "name": "@fro.bot/harness",
3
- "version": "0.0.0",
4
- "description": "Bootstrap stub placeholder to claim the npm package name. Real releases use OIDC via harness-release.yaml.",
3
+ "version": "1.15.13",
4
+ "description": "Published patched-OpenCode binary with orw-embedded LLM-merge integration the default OpenCode for Fro Bot",
5
+ "keywords": [
6
+ "opencode",
7
+ "fro-bot",
8
+ "harness",
9
+ "cli"
10
+ ],
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/fro-bot/agent.git"
14
+ },
5
15
  "license": "MIT",
16
+ "author": "Fro Bot <agent@fro.bot>",
17
+ "type": "module",
18
+ "bin": {
19
+ "harness": "./dist/cli.mjs"
20
+ },
21
+ "files": [
22
+ "LICENSE",
23
+ "README.md",
24
+ "dist",
25
+ "provenance.json"
26
+ ],
27
+ "scripts": {
28
+ "build": "pnpm exec tsc -p tsconfig.json --noEmit && pnpm exec tsdown",
29
+ "check-types": "pnpm exec tsc -p tsconfig.json --noEmit",
30
+ "fix": "pnpm --dir ../.. exec eslint --fix packages/harness/src packages/harness/scripts",
31
+ "lint": "pnpm --dir ../.. exec eslint packages/harness/src packages/harness/scripts",
32
+ "postinstall": "node dist/postinstall.mjs || true",
33
+ "test": "pnpm exec vitest run --passWithNoTests src scripts"
34
+ },
35
+ "engines": {
36
+ "node": ">=24"
37
+ },
6
38
  "publishConfig": {
7
39
  "access": "public"
40
+ },
41
+ "optionalDependencies": {
42
+ "@fro.bot/harness-linux-x64": "1.15.13",
43
+ "@fro.bot/harness-linux-arm64": "1.15.13",
44
+ "@fro.bot/harness-darwin-x64": "1.15.13",
45
+ "@fro.bot/harness-darwin-arm64": "1.15.13"
8
46
  }
9
47
  }
@@ -0,0 +1,5 @@
1
+ {
2
+ "baseVersion": "1.15.13",
3
+ "integrationCommit": "badaaf1ba6db5a001442696c6fc60a3353a75a09",
4
+ "buildSha": "8dee96f7fb51efbb5137c151f1fbd0616fbeeabb"
5
+ }