@mikenguyen69/harness 0.1.0-beta.1

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.
Files changed (127) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +43 -0
  3. package/dist/build-identity.json +1 -0
  4. package/dist/cancellation.js +138 -0
  5. package/dist/cli.js +47 -0
  6. package/dist/dispatch.js +70 -0
  7. package/dist/help.js +47 -0
  8. package/dist/internal/agent/adapter/conformance.js +53 -0
  9. package/dist/internal/agent/adapter/hook.js +113 -0
  10. package/dist/internal/agent/adapter/install.js +30 -0
  11. package/dist/internal/agent/adapter/scope.js +55 -0
  12. package/dist/internal/agent/adapter/session.js +63 -0
  13. package/dist/internal/agent/adapter/settings.js +51 -0
  14. package/dist/internal/agent/cli.js +79 -0
  15. package/dist/internal/agent/evals/index.js +111 -0
  16. package/dist/internal/agent/index.js +15 -0
  17. package/dist/internal/agent/ledger-read.js +27 -0
  18. package/dist/internal/agent/routing/cli.js +104 -0
  19. package/dist/internal/agent/routing/index.js +123 -0
  20. package/dist/internal/agent/telemetry/index.js +97 -0
  21. package/dist/internal/orchestration/activity/index.js +111 -0
  22. package/dist/internal/orchestration/analyze/index.js +237 -0
  23. package/dist/internal/orchestration/cli.js +475 -0
  24. package/dist/internal/orchestration/config/index.js +66 -0
  25. package/dist/internal/orchestration/doctor/index.js +202 -0
  26. package/dist/internal/orchestration/forge/index.js +121 -0
  27. package/dist/internal/orchestration/git/index.js +181 -0
  28. package/dist/internal/orchestration/harness/cli.js +378 -0
  29. package/dist/internal/orchestration/harness/index.js +186 -0
  30. package/dist/internal/orchestration/harness/shim.js +182 -0
  31. package/dist/internal/orchestration/index.js +29 -0
  32. package/dist/internal/orchestration/loop/index.js +1107 -0
  33. package/dist/internal/orchestration/prompts/index.js +199 -0
  34. package/dist/internal/orchestration/report/index.js +125 -0
  35. package/dist/internal/orchestration/routes/index.js +85 -0
  36. package/dist/internal/orchestration/run/index.js +221 -0
  37. package/dist/internal/orchestration/runner/index.js +90 -0
  38. package/dist/internal/orchestration/runners/claude/index.js +246 -0
  39. package/dist/internal/orchestration/runners/claude/sdk.js +198 -0
  40. package/dist/internal/orchestration/runners/claude/wiring.js +98 -0
  41. package/dist/internal/orchestration/runners/codex/exec.js +114 -0
  42. package/dist/internal/orchestration/runners/codex/index.js +72 -0
  43. package/dist/internal/orchestration/runners/conformance/index.js +169 -0
  44. package/dist/internal/orchestration/runners/cursor/exec.js +531 -0
  45. package/dist/internal/orchestration/runners/cursor/index.js +184 -0
  46. package/dist/internal/orchestration/runners/stub/index.js +168 -0
  47. package/dist/internal/orchestration/runners/telemetry.js +36 -0
  48. package/dist/internal/orchestration/runstate/index.js +213 -0
  49. package/dist/internal/orchestration/runstate/publish.js +115 -0
  50. package/dist/internal/orchestration/worktree/index.js +89 -0
  51. package/dist/internal/system/adapters/index.js +19 -0
  52. package/dist/internal/system/agentops/index.js +16 -0
  53. package/dist/internal/system/agentops/scope.js +67 -0
  54. package/dist/internal/system/assets.js +45 -0
  55. package/dist/internal/system/board/cli.js +56 -0
  56. package/dist/internal/system/board/index.js +276 -0
  57. package/dist/internal/system/board/live.js +277 -0
  58. package/dist/internal/system/calibration/cli.js +45 -0
  59. package/dist/internal/system/calibration/index.js +141 -0
  60. package/dist/internal/system/cli.js +174 -0
  61. package/dist/internal/system/corpus/replay.js +79 -0
  62. package/dist/internal/system/digest/cli.js +23 -0
  63. package/dist/internal/system/digest/index.js +145 -0
  64. package/dist/internal/system/doors/classify.js +94 -0
  65. package/dist/internal/system/doors/cli.js +184 -0
  66. package/dist/internal/system/doors/index.js +249 -0
  67. package/dist/internal/system/doors/metrics.js +47 -0
  68. package/dist/internal/system/escapes/cli.js +45 -0
  69. package/dist/internal/system/escapes/index.js +81 -0
  70. package/dist/internal/system/explore/cli.js +77 -0
  71. package/dist/internal/system/explore/index.js +91 -0
  72. package/dist/internal/system/gates/cli.js +43 -0
  73. package/dist/internal/system/gates/detectors.js +55 -0
  74. package/dist/internal/system/gates/pre.js +51 -0
  75. package/dist/internal/system/hub/cli.js +51 -0
  76. package/dist/internal/system/hub/index.js +146 -0
  77. package/dist/internal/system/init/index.js +83 -0
  78. package/dist/internal/system/ledger/cli.js +54 -0
  79. package/dist/internal/system/ledger/index.js +176 -0
  80. package/dist/internal/system/manifest/index.js +207 -0
  81. package/dist/internal/system/provenance/index.js +13 -0
  82. package/dist/internal/system/report/index.js +15 -0
  83. package/dist/internal/system/runner/index.js +74 -0
  84. package/dist/internal/system/runstatus/cli.js +86 -0
  85. package/dist/internal/system/runstatus/index.js +168 -0
  86. package/dist/internal/system/sequencer/briefing.js +75 -0
  87. package/dist/internal/system/sequencer/cli.js +170 -0
  88. package/dist/internal/system/sequencer/index.js +156 -0
  89. package/dist/internal/system/spec/cli.js +110 -0
  90. package/dist/internal/system/spec/index.js +212 -0
  91. package/dist/internal/system/spec/materialize.js +111 -0
  92. package/dist/internal/system/spec/openspec-parse.js +83 -0
  93. package/dist/internal/system/spec/project.js +208 -0
  94. package/dist/internal/system/substrate/index.js +14 -0
  95. package/dist/internal/system/system/index.js +159 -0
  96. package/dist/internal/system/units/index.js +87 -0
  97. package/dist/internal/system/verify/cli.js +103 -0
  98. package/dist/internal/system/verify/declined-reasons.js +61 -0
  99. package/dist/internal/system/verify/index.js +188 -0
  100. package/dist/internal/system/verify/signing.js +79 -0
  101. package/dist/paths.js +50 -0
  102. package/dist/routes.js +93 -0
  103. package/dist/version.js +30 -0
  104. package/dist/win32-process-group.js +360 -0
  105. package/package.json +41 -0
  106. package/profiles/python/bindings.toml +59 -0
  107. package/profiles/terraform/bindings.toml +66 -0
  108. package/profiles/typescript/bindings.toml +64 -0
  109. package/profiles/typescript/smoke/README.md +8 -0
  110. package/schema/baseline.schema.json +26 -0
  111. package/schema/decision-classes.yaml +45 -0
  112. package/schema/envelope.schema.json +186 -0
  113. package/schema/ledger-event.schema.json +235 -0
  114. package/schema/manifest.schema.json +119 -0
  115. package/schema/routes.schema.json +55 -0
  116. package/schema/run-status.schema.json +162 -0
  117. package/schema/session-event.schema.json +74 -0
  118. package/schema/spec.schema.json +114 -0
  119. package/schema/system.schema.json +142 -0
  120. package/templates/ci/verify.yml +23 -0
  121. package/templates/target-kit/README.md +78 -0
  122. package/templates/target-kit/component.toml +20 -0
  123. package/templates/target-kit/orchestration.json +9 -0
  124. package/templates/target-kit/profiles/target/bindings.toml +6 -0
  125. package/templates/target-kit/routes.toml +5 -0
  126. package/templates/target-kit/specs/first-unit.yaml +11 -0
  127. package/templates/target-kit/system.toml +7 -0
@@ -0,0 +1,115 @@
1
+ /**
2
+ * publish (D1) — serialise a `RunState` to a `run-status` document and push it
3
+ * to the hub spool via `harness runstatus publish`, so the system-harness board
4
+ * can overlay this live run on portfolio truth.
5
+ *
6
+ * The document shape is system-harness's (`run-status.schema.json`, P7.1) — one
7
+ * schema, N emitters. This product never writes spool files directly, exactly
8
+ * as it appends the ledger only through `harness ledger append`.
9
+ *
10
+ * Publishing is best-effort: a failure degrades to a warning on the RunLog and
11
+ * NEVER stops the loop (staleness rules on the board cover a silent machine).
12
+ */
13
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
14
+ import { tmpdir } from "node:os";
15
+ import { join } from "node:path";
16
+ import { callProcess } from "../harness/cli.js";
17
+ const iso = (ms, fallback = Date.now()) => new Date(ms > 0 ? ms : fallback).toISOString();
18
+ export function runStateToDocument(state) {
19
+ const doc = {
20
+ machine_id: state.machineId || "local",
21
+ run_id: state.runId || "unknown",
22
+ system_id: state.systemId || "unknown",
23
+ spec_ref: state.specRef || "unknown",
24
+ started_at: iso(state.startedAt),
25
+ heartbeat_at: iso(state.heartbeatAt, Date.now()),
26
+ lanes: state.lanes.map((l) => ({
27
+ unit_id: l.unitId,
28
+ step: l.step,
29
+ runner: l.runner,
30
+ model: l.model,
31
+ attempt: l.attempt,
32
+ retry_budget: l.retryBudget,
33
+ cost: l.cost,
34
+ emitter: l.emitter,
35
+ settled: l.settled,
36
+ ...(l.envelopeHash ? { envelope_hash: l.envelopeHash } : {}),
37
+ ...(l.doorId ? { door_id: l.doorId } : {}),
38
+ ...(l.activity ? { activity: l.activity } : {}),
39
+ ...(l.activityAt ? { activity_at: iso(l.activityAt) } : {}),
40
+ ...(l.turn !== undefined ? { turn: l.turn } : {}),
41
+ ...(l.tokens ? { tokens: l.tokens } : {}),
42
+ ...(l.recent?.length ? { recent: l.recent } : {}),
43
+ })),
44
+ needs_you: state.needsYou.map((n) => ({
45
+ kind: n.kind,
46
+ ...(n.unitId ? { unit_id: n.unitId } : {}),
47
+ message: n.message,
48
+ unblock: n.unblock,
49
+ })),
50
+ counts: {
51
+ merged: state.counts.merged,
52
+ escalated: state.counts.escalated,
53
+ parked: state.counts.parked,
54
+ in_flight: state.counts.inFlight,
55
+ needs_you: state.counts.needsYou,
56
+ dag_size: state.dagSize,
57
+ },
58
+ };
59
+ if (state.outcome)
60
+ doc.outcome = state.outcome;
61
+ return doc;
62
+ }
63
+ /** A no-op publisher — the default when nothing is wired (dry loop, tests). */
64
+ export const NULL_PUBLISHER = { async publish() { } };
65
+ /** Publishes by shelling `harness runstatus publish` with the doc on a temp file. */
66
+ export class CliRunPublisher {
67
+ opts;
68
+ constructor(opts) {
69
+ this.opts = opts;
70
+ }
71
+ async publish(state) {
72
+ const doc = runStateToDocument(state);
73
+ let tmp;
74
+ try {
75
+ const dir = mkdtempSync(join(tmpdir(), "orch-runstatus-"));
76
+ tmp = join(dir, "doc.json");
77
+ writeFileSync(tmp, JSON.stringify(doc));
78
+ const args = [
79
+ "runstatus",
80
+ "publish",
81
+ "--file",
82
+ tmp,
83
+ "--spool",
84
+ this.opts.spoolDir,
85
+ ...(this.opts.machineId ? ["--machine", this.opts.machineId] : []),
86
+ ];
87
+ const result = await callProcess({
88
+ command: this.opts.harnessBin,
89
+ args: [...(this.opts.harnessBinArgs ?? []), ...args],
90
+ cwd: this.opts.cwd,
91
+ });
92
+ if (result.code !== 0)
93
+ throw new Error((result.stderr || result.stdout).trim() || `exit ${result.code}`);
94
+ }
95
+ catch (e) {
96
+ // degrade to a warning — never stop the loop.
97
+ this.opts.log.append({
98
+ t: "lane.note",
99
+ unitId: state.runId || "run",
100
+ note: `run-status publish failed (${e.message.trim()}) — the board may show this run as stale`,
101
+ });
102
+ }
103
+ finally {
104
+ if (tmp) {
105
+ try {
106
+ rmSync(join(tmp, ".."), { recursive: true, force: true });
107
+ }
108
+ catch {
109
+ /* ignore */
110
+ }
111
+ }
112
+ }
113
+ }
114
+ }
115
+ //# sourceMappingURL=publish.js.map
@@ -0,0 +1,89 @@
1
+ /** Per-unit Git worktrees: the control checkout owns orchestration state while
2
+ * every implementation lane gets an isolated branch checkout. */
3
+ import { execFile } from "node:child_process";
4
+ import { createHash } from "node:crypto";
5
+ import { mkdir } from "node:fs/promises";
6
+ import { basename, join, resolve } from "node:path";
7
+ import { promisify } from "node:util";
8
+ const run = promisify(execFile);
9
+ export class GitWorktreeManager {
10
+ controlRepo;
11
+ root;
12
+ gitBin;
13
+ constructor(opts) {
14
+ this.controlRepo = resolve(opts.controlRepo);
15
+ this.root = resolve(opts.root);
16
+ this.gitBin = opts.gitBin ?? "git";
17
+ }
18
+ async acquire(request) {
19
+ await mkdir(this.root, { recursive: true });
20
+ const path = join(this.root, laneName(request.unitId));
21
+ const existing = await this.registeredPath(request.branch);
22
+ if (existing) {
23
+ if (resolve(existing) !== resolve(path)) {
24
+ throw new Error(`branch ${request.branch} is already checked out at ${existing}`);
25
+ }
26
+ return { unitId: request.unitId, branch: request.branch, path, controlRepo: this.controlRepo };
27
+ }
28
+ const branchExists = (await this.git(["show-ref", "--verify", "--quiet", `refs/heads/${request.branch}`], true)).code === 0;
29
+ const args = branchExists
30
+ ? ["worktree", "add", path, request.branch]
31
+ : ["worktree", "add", "-b", request.branch, path, request.baseBranch];
32
+ await this.git(args);
33
+ return { unitId: request.unitId, branch: request.branch, path, controlRepo: this.controlRepo };
34
+ }
35
+ async release(workspace) {
36
+ const status = await this.git(["-C", workspace.path, "status", "--porcelain"]);
37
+ if (status.stdout.trim()) {
38
+ throw new Error(`refusing to remove dirty worktree for ${workspace.unitId}: ${workspace.path}`);
39
+ }
40
+ await this.git(["worktree", "remove", workspace.path]);
41
+ }
42
+ async registeredPath(branch) {
43
+ const result = await this.git(["worktree", "list", "--porcelain"]);
44
+ let path;
45
+ for (const line of result.stdout.split(/\r?\n/)) {
46
+ if (line.startsWith("worktree "))
47
+ path = line.slice("worktree ".length);
48
+ if (line === `branch refs/heads/${branch}`)
49
+ return path;
50
+ if (!line)
51
+ path = undefined;
52
+ }
53
+ return undefined;
54
+ }
55
+ async git(args, allowFailure = false) {
56
+ try {
57
+ const { stdout, stderr } = await run(this.gitBin, args, { cwd: this.controlRepo });
58
+ return { code: 0, stdout, stderr };
59
+ }
60
+ catch (error) {
61
+ const detail = error;
62
+ const result = {
63
+ code: typeof detail.code === "number" ? detail.code : 1,
64
+ stdout: detail.stdout ?? "",
65
+ stderr: detail.stderr ?? "",
66
+ };
67
+ if (allowFailure)
68
+ return result;
69
+ throw new Error(`git ${args.join(" ")} failed: ${(result.stderr || result.stdout).trim()}`, { cause: error });
70
+ }
71
+ }
72
+ }
73
+ /** Test/backward-compatibility seam for pure loops that do not touch a checkout. */
74
+ export class InPlaceWorkspaceManager {
75
+ path;
76
+ constructor(path = process.cwd()) {
77
+ this.path = path;
78
+ }
79
+ async acquire(request) {
80
+ return { unitId: request.unitId, branch: request.branch, path: this.path, controlRepo: this.path };
81
+ }
82
+ async release(_workspace) { }
83
+ }
84
+ function laneName(unitId) {
85
+ const readable = basename(unitId.replaceAll("\\", "/")).replace(/[^a-zA-Z0-9._-]+/g, "-");
86
+ const hash = createHash("sha256").update(unitId).digest("hex").slice(0, 10);
87
+ return `${readable || "unit"}-${hash}`;
88
+ }
89
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,19 @@
1
+ /**
2
+ * adapters — generates the per-agent surface from ONE protocol template:
3
+ * AGENTS.md (vendor-neutral, Linux Foundation-stewarded), a CLAUDE.md that
4
+ * imports it, Claude Code hooks calling `harness gate --pre`, Cursor
5
+ * allowlists, Codex sandbox config, git hooks, CI workflow.
6
+ *
7
+ * Rules:
8
+ * - generated files carry a content hash; the `gen:fresh` gate fails on
9
+ * hand-edits or staleness — adapters may never fork from policy
10
+ * - adapters are UX (prompt) and fast feedback (hooks); the substrate is
11
+ * the guarantee and must be sufficient alone
12
+ */
13
+ export function generate(_target, _projectRoot) {
14
+ throw new Error("not implemented (skeleton)");
15
+ }
16
+ export function checkFresh(_projectRoot) {
17
+ throw new Error("not implemented (skeleton)");
18
+ }
19
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,16 @@
1
+ /**
2
+ * agentops — the layer most harnesses forget: who is this session, what may
3
+ * it touch, what may it spend, and what happens when it dies.
4
+ *
5
+ * - identity: session id + model id, threaded into every envelope
6
+ * - scope enforcement: out-of-scope file touches halt (fail closed)
7
+ * - budgets: token/cost/time caps per unit (exceeded = halt, not warn)
8
+ * - lifecycle: a dead session holding a unit `executing` is reclaimable
9
+ *
10
+ * Threat model source: OWASP Top 10 for Agentic Applications 2026 —
11
+ * goal hijack (ASI01), tool misuse (ASI02), privilege abuse (ASI03).
12
+ */
13
+ export function checkScope(_changedFiles, _scopeGlobs) {
14
+ throw new Error("not implemented (skeleton)");
15
+ }
16
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,67 @@
1
+ /** Authoritative verify-time scope attestation (independent of agent hooks). */
2
+ import { execFileSync } from "node:child_process";
3
+ import picomatch from "picomatch";
4
+ import { canonicalUnitId, loadSpecs } from "../spec/index.js";
5
+ const norm = (path) => path.replaceAll("\\", "/").replace(/^\.\//, "");
6
+ /** Return every changed path outside the effective scope. Empty scope fails closed. */
7
+ export function checkScope(changedFiles, effectiveScope) {
8
+ if (effectiveScope.length === 0)
9
+ return changedFiles.map(norm);
10
+ const matches = picomatch(effectiveScope.map(norm), { dot: true });
11
+ return changedFiles.map(norm).filter((path) => !matches(path));
12
+ }
13
+ /** Derive both sides of the scope assertion from git + the accepted spec. */
14
+ export function attestScope(opts) {
15
+ if (!opts.baseRef || !opts.specsDir) {
16
+ return {
17
+ attested: false,
18
+ attempted: false,
19
+ changedFiles: [],
20
+ effectiveScope: [],
21
+ violations: [],
22
+ reason: "no unit context: verify was called without --base and --specs, so scope was not attested",
23
+ };
24
+ }
25
+ try {
26
+ const unitId = canonicalUnitId(opts.unitId);
27
+ const loaded = loadSpecs(opts.specsDir).find(({ spec }) => canonicalUnitId(spec.id) === unitId);
28
+ if (!loaded) {
29
+ return {
30
+ attested: false,
31
+ attempted: true,
32
+ changedFiles: [],
33
+ effectiveScope: [],
34
+ violations: [],
35
+ reason: `no accepted spec found for unit "${unitId}" under ${opts.specsDir}`,
36
+ };
37
+ }
38
+ const stdout = execFileSync("git", ["diff", "--name-only", `${opts.baseRef}...HEAD`, "--"], { cwd: opts.componentDir, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
39
+ const changedFiles = stdout
40
+ .split(/\r?\n/)
41
+ .map((path) => norm(path.trim()))
42
+ .filter(Boolean);
43
+ const effectiveScope = loaded.spec.scope.map(norm);
44
+ const violations = checkScope(changedFiles, effectiveScope);
45
+ return {
46
+ attested: violations.length === 0,
47
+ attempted: true,
48
+ changedFiles,
49
+ effectiveScope,
50
+ violations,
51
+ ...(violations.length > 0
52
+ ? { reason: `changed paths outside declared scope: ${violations.join(", ")}` }
53
+ : {}),
54
+ };
55
+ }
56
+ catch (error) {
57
+ return {
58
+ attested: false,
59
+ attempted: true,
60
+ changedFiles: [],
61
+ effectiveScope: [],
62
+ violations: [],
63
+ reason: `scope attestation failed: ${error.message}`,
64
+ };
65
+ }
66
+ }
67
+ //# sourceMappingURL=scope.js.map
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Package-owned asset resolution from `import.meta.url` (workspace or staged).
3
+ */
4
+ import { existsSync } from "node:fs";
5
+ import { dirname, join, resolve } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ export function resolvePackageAsset(fromUrl, ...markers) {
8
+ let dir = dirname(fileURLToPath(fromUrl));
9
+ while (true) {
10
+ for (const marker of markers) {
11
+ const candidate = join(dir, marker);
12
+ if (existsSync(candidate))
13
+ return resolve(candidate);
14
+ }
15
+ const parent = dirname(dir);
16
+ if (parent === dir) {
17
+ throw new Error(`package asset not found (looked for ${markers.join(" | ")} from ${fileURLToPath(fromUrl)})`);
18
+ }
19
+ dir = parent;
20
+ }
21
+ }
22
+ export function packageSchemaDir(fromUrl = import.meta.url) {
23
+ return resolvePackageAsset(fromUrl, "schema");
24
+ }
25
+ export function packageProfilesDir(fromUrl = import.meta.url) {
26
+ return resolvePackageAsset(fromUrl, "profiles");
27
+ }
28
+ /** Workspace: `ci-templates/`; staged package: `templates/`. */
29
+ export function packageTemplatesDir(fromUrl = import.meta.url) {
30
+ return resolvePackageAsset(fromUrl, "templates", "ci-templates");
31
+ }
32
+ export function targetPath(repoRoot, ...segments) {
33
+ return resolve(repoRoot, ...segments);
34
+ }
35
+ export function targetOwnedPaths(repoRoot) {
36
+ const root = resolve(repoRoot);
37
+ return {
38
+ specs: targetPath(root, "specs"),
39
+ systemToml: targetPath(root, "system.toml"),
40
+ harnessDir: targetPath(root, ".harness"),
41
+ componentToml: targetPath(root, "component.toml"),
42
+ ledgerDir: targetPath(root, ".harness", "ledger"),
43
+ };
44
+ }
45
+ //# sourceMappingURL=assets.js.map
@@ -0,0 +1,56 @@
1
+ /**
2
+ * `harness board build` — render the operations board from the ledger.
3
+ *
4
+ * harness board build [--ledger <dir>] [--spool <dir>] [--out board.html] [--json]
5
+ *
6
+ * Every panel is a filter over the in-memory projection (sub-millisecond);
7
+ * empty panels render "insufficient data", never a fake zero. `--spool` adds
8
+ * the live "runs" overlay from the run-status spool (P7.3).
9
+ */
10
+ import { writeFileSync } from "node:fs";
11
+ import { resolve } from "node:path";
12
+ import { projectLedger } from "../hub/index.js";
13
+ import { projectRuns } from "../runstatus/index.js";
14
+ import { buildBoard, renderBoardHtml } from "./index.js";
15
+ import { startBoardServer } from "./live.js";
16
+ function flag(args, name) {
17
+ const i = args.indexOf(`--${name}`);
18
+ return i >= 0 && args[i + 1] ? args[i + 1] : undefined;
19
+ }
20
+ export function boardCommand(args) {
21
+ if (args[0] !== "build") {
22
+ process.stderr.write("harness board build [--ledger <dir>] [--spool <dir>] [--out board.html] [--json]\n");
23
+ return 2;
24
+ }
25
+ const spool = flag(args, "spool");
26
+ const board = buildBoard(projectLedger(flag(args, "ledger") ?? ".harness/ledger"), new Date().toISOString(), spool ? projectRuns(spool) : undefined);
27
+ if (args.includes("--json")) {
28
+ process.stdout.write(JSON.stringify(board, null, 2) + "\n");
29
+ return 0;
30
+ }
31
+ const out = flag(args, "out") ?? "board.html";
32
+ writeFileSync(out, renderBoardHtml(board));
33
+ const insufficient = Object.values(board.panels).filter((p) => p.status === "insufficient-data").length;
34
+ process.stdout.write(`wrote ${out} — ${Object.keys(board.panels).length} panels (${insufficient} insufficient-data)\n`);
35
+ return 0;
36
+ }
37
+ export function parseBoardServeOptions(args, cwd = process.cwd()) {
38
+ const roots = [];
39
+ for (let i = 0; i < args.length; i++) {
40
+ if (args[i] === "--root" && args[i + 1])
41
+ roots.push(resolve(cwd, args[++i]));
42
+ }
43
+ const portText = flag(args, "port");
44
+ const refreshText = flag(args, "refresh");
45
+ const port = portText === undefined ? 4317 : Number(portText);
46
+ const refreshMs = refreshText === undefined ? 2_000 : Number(refreshText);
47
+ if (!Number.isInteger(port) || port < 0 || port > 65_535)
48
+ throw new Error(`invalid --port: ${portText}`);
49
+ if (!Number.isFinite(refreshMs) || refreshMs < 250)
50
+ throw new Error(`invalid --refresh: ${refreshText}`);
51
+ return { roots: roots.length ? roots : [resolve(cwd)], host: "127.0.0.1", port, refreshMs };
52
+ }
53
+ export function boardServeCommand(args, cwd = process.cwd()) {
54
+ return startBoardServer(parseBoardServeOptions(args, cwd));
55
+ }
56
+ //# sourceMappingURL=cli.js.map