@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,91 @@
1
+ /**
2
+ * explore — the exploration lane (D7). A `mode: explore` unit runs ring-0 only
3
+ * and has no edge to `merged` (enforced by mergeGate). Its only exits:
4
+ *
5
+ * - abort: unit.aborted. The spike is thrown away.
6
+ * - graduate: emit a draft `spec.authored` from what was learned (intent,
7
+ * discovered scope, discovered acceptance criteria) and plan a
8
+ * normal `build` unit for it.
9
+ */
10
+ import { append, read as ledgerRead, } from "../ledger/index.js";
11
+ import { projectUnits } from "../units/index.js";
12
+ export class ExploreError extends Error {
13
+ name = "ExploreError";
14
+ }
15
+ function bareId(ref) {
16
+ return ref.includes("#") ? ref.slice(ref.indexOf("#") + 1) : ref;
17
+ }
18
+ function assertExplore(unitId, events) {
19
+ const u = projectUnits(events).get(bareId(unitId));
20
+ if (!u)
21
+ throw new ExploreError(`no such unit: ${unitId}`);
22
+ if (u.mode !== "explore")
23
+ throw new ExploreError(`unit "${unitId}" is not mode: explore`);
24
+ if (u.state === "merged" || u.state === "aborted")
25
+ throw new ExploreError(`unit "${unitId}" is already ${u.state}`);
26
+ return u;
27
+ }
28
+ export function abortExplore(unitId, opts) {
29
+ const events = ledgerRead(opts.ledgerDir);
30
+ const u = assertExplore(unitId, events);
31
+ return append(opts.ledgerDir, {
32
+ system: opts.system ?? events[0]?.system ?? unitId,
33
+ actor: { kind: "human", id: "operator" },
34
+ type: "unit.aborted",
35
+ ts: opts.now ?? new Date().toISOString(),
36
+ body: {
37
+ unit_id: unitId,
38
+ ...(u.component ? { component: u.component } : {}),
39
+ reason: "explore: aborted",
40
+ },
41
+ });
42
+ }
43
+ /** Emit a draft build spec from the spike's learnings and plan the build unit. */
44
+ export function graduateExplore(unitId, discovered, opts) {
45
+ const events = ledgerRead(opts.ledgerDir);
46
+ const u = assertExplore(unitId, events);
47
+ const ts = opts.now ?? new Date().toISOString();
48
+ const sys = opts.system ?? events[0]?.system ?? unitId;
49
+ const draftSpecId = `${u.component ?? "core"}#${bareId(unitId)}-graduated`;
50
+ const spec = {
51
+ id: draftSpecId,
52
+ component: u.component ?? "core",
53
+ mode: "build",
54
+ intent: discovered.intent,
55
+ acceptance_criteria: discovered.acceptanceCriteria,
56
+ scope: discovered.scope,
57
+ };
58
+ const out = [];
59
+ out.push(append(opts.ledgerDir, {
60
+ system: sys,
61
+ actor: { kind: "human", id: "operator" },
62
+ type: "unit.aborted",
63
+ ts,
64
+ body: {
65
+ unit_id: unitId,
66
+ reason: "explore: graduated",
67
+ graduated_to: draftSpecId,
68
+ },
69
+ }));
70
+ out.push(append(opts.ledgerDir, {
71
+ system: sys,
72
+ actor: { kind: "human", id: "operator" },
73
+ type: "spec.authored",
74
+ ts,
75
+ body: { spec_id: draftSpecId, draft: true, from_explore: unitId, spec },
76
+ }));
77
+ out.push(append(opts.ledgerDir, {
78
+ system: sys,
79
+ actor: { kind: "harness", id: "harness" },
80
+ type: "unit.planned",
81
+ ts,
82
+ body: {
83
+ unit_id: draftSpecId,
84
+ component: u.component ?? "core",
85
+ mode: "build",
86
+ from_explore: unitId,
87
+ },
88
+ }));
89
+ return { draftSpecId, events: out };
90
+ }
91
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,43 @@
1
+ /**
2
+ * `harness gate --pre` — see src/gates/pre.ts for the policy. Any other
3
+ * `harness gate <name>` invocation (a single built-in gate run, e.g. the
4
+ * `spec`/`size`/`gen:fresh` gates a profile's `run =` line shells out to) is
5
+ * not implemented yet — that is a separate, larger piece of work than ring 0
6
+ * and stays a tracked skeleton (see docs/implement/plan.md).
7
+ *
8
+ * harness gate --pre --file <path> [--component <dir>] [--profiles <dir>]
9
+ *
10
+ * Exit: 0 pass (including "no cheap gates bound") · 1 a cheap gate failed ·
11
+ * 2 halt (component.toml / profile could not be resolved).
12
+ */
13
+ import { runGatePre } from "./pre.js";
14
+ function flag(args, name) {
15
+ const i = args.indexOf(`--${name}`);
16
+ return i >= 0 && args[i + 1] ? args[i + 1] : undefined;
17
+ }
18
+ export function gateCommand(args) {
19
+ if (!args.includes("--pre"))
20
+ return undefined; // let the caller fall through
21
+ const result = runGatePre({
22
+ ...(flag(args, "component") ? { componentDir: flag(args, "component") } : {}),
23
+ ...(flag(args, "profiles") ? { profilesDir: flag(args, "profiles") } : {}),
24
+ ...(flag(args, "file") ? { file: flag(args, "file") } : {}),
25
+ });
26
+ if (result.verdict === "halt") {
27
+ process.stderr.write(`⛔ gate --pre halted: ${result.haltReason}\n`);
28
+ return 2;
29
+ }
30
+ if (result.gates.length === 0) {
31
+ process.stdout.write("· no cheap gates bound; nothing to check at ring 0\n");
32
+ return 0;
33
+ }
34
+ for (const g of result.gates) {
35
+ const mark = g.outcome === "pass" ? "✓" : "✗";
36
+ process.stdout.write(` ${mark} ${g.name.padEnd(12)} ${String(g.durationMs).padStart(6)}ms\n`);
37
+ if (g.outcome !== "pass" && g.output.trim()) {
38
+ process.stderr.write(g.output.trim() + "\n");
39
+ }
40
+ }
41
+ return result.verdict === "pass" ? 0 : 1;
42
+ }
43
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1,55 @@
1
+ /**
2
+ * gate detectors — the small, replay-testable checks behind a few named gates.
3
+ * Each takes plain data (no filesystem, no tools) so a corpus fixture can drive
4
+ * it deterministically.
5
+ */
6
+ import picomatch from "picomatch";
7
+ const SUPPRESSION_RE = /\b(eslint-disable(?:-next-line|-line)?|@ts-(?:ignore|expect-error|nocheck)|noqa|type:\s*ignore|nosec|prettier-ignore)\b/;
8
+ /** `gate:lint` — net-new suppression comments introduced by a diff. */
9
+ export function suppressionDelta(before, after) {
10
+ const count = (s) => s.split("\n").filter((l) => SUPPRESSION_RE.test(l)).length;
11
+ const beforeLines = new Set(before
12
+ .split("\n")
13
+ .map((l) => l.trim())
14
+ .filter((l) => SUPPRESSION_RE.test(l)));
15
+ const added = [];
16
+ for (const l of after.split("\n")) {
17
+ const t = l.trim();
18
+ if (SUPPRESSION_RE.test(t) && !beforeLines.has(t))
19
+ added.push(t);
20
+ }
21
+ // also catch a raw increase even if lines look identical (duplication)
22
+ if (added.length === 0 && count(after) > count(before))
23
+ added.push("(suppression count increased)");
24
+ return added;
25
+ }
26
+ /** `gate:size`/scope — changed files that fall outside the unit's declared scope globs. */
27
+ export function scopeCross(changedFiles, scopeGlobs) {
28
+ if (scopeGlobs.length === 0)
29
+ return changedFiles.slice();
30
+ const isMatch = picomatch(scopeGlobs.map((g) => g.replace(/\\/g, "/")), { dot: true });
31
+ return changedFiles
32
+ .map((f) => f.replace(/\\/g, "/"))
33
+ .filter((f) => !isMatch(f));
34
+ }
35
+ /** `gate:arch` — a test file weakened in the same unit as the code it covers. */
36
+ export function testWeakening(input) {
37
+ const isTest = (p) => /\.(?:test|spec)\.[tj]sx?$/.test(p) ||
38
+ /(^|\/)(?:tests?|__tests__)\//.test(p);
39
+ const stemOf = (p) => p
40
+ .split(/[/\\]/)
41
+ .pop()
42
+ .replace(/\.(?:test|spec)\.[tj]sx?$/, "")
43
+ .replace(/\.[tj]sx?$/, "");
44
+ const subjectStems = new Set(input.changed.filter((p) => !isTest(p)).map(stemOf));
45
+ const flags = [];
46
+ for (const [testFile, removed] of Object.entries(input.assertionsRemoved)) {
47
+ if (removed <= 0 || !input.changed.includes(testFile))
48
+ continue;
49
+ if (subjectStems.has(stemOf(testFile))) {
50
+ flags.push(`${testFile}: ${removed} assertion(s) removed while its subject (${stemOf(testFile)}) changed in the same unit`);
51
+ }
52
+ }
53
+ return flags;
54
+ }
55
+ //# sourceMappingURL=detectors.js.map
@@ -0,0 +1,51 @@
1
+ /**
2
+ * gate --pre — ring 0, the seconds-loop actuator (mission `docs/mission.md`
3
+ * §"the five feedback loops", loop 1). Runs only the *cheap*-cost gates bound
4
+ * for this component, at the component root, and reports pass/fail using the
5
+ * exact same policy `harness verify` (ring 1) uses (`verdictOf`) — so ring 0
6
+ * can give fast feedback but never disagree with ring 1 about what "failing"
7
+ * means. It never mints an envelope and never touches the ledger: it is the
8
+ * fast copy of the rule, not the guarantee (adapter docstring, principle 1).
9
+ *
10
+ * Deliberately not (yet) file-scoped: `--file <path>` is accepted (the
11
+ * PreToolUse hook always passes it) but not used to filter which gates run —
12
+ * the manifest carries no per-file gate mapping today. Every bound cheap gate
13
+ * runs at the component root on every call. This is honest, not silently
14
+ * fake precision: it is the caller's own `cost: cheap` declaration in the
15
+ * component manifest and the profile bindings that decides whether a gate is
16
+ * fast enough to belong here — calibration (`src/calibration`) is the
17
+ * long-run mechanism for re-labeling a gate that turns out too slow. Ring 0
18
+ * only ever runs when the caller opts in (`HARNESS_RING0=1`); it is never the
19
+ * default path.
20
+ */
21
+ import { join, resolve } from "node:path";
22
+ import { targetPath } from "../assets.js";
23
+ import { loadManifest, loadProfile, resolveManifest, ManifestError, } from "../manifest/index.js";
24
+ import { runGates } from "../runner/index.js";
25
+ import { verdictOf } from "../verify/index.js";
26
+ export function runGatePre(opts) {
27
+ const componentDir = resolve(opts.componentDir ?? process.cwd());
28
+ const profilesDir = opts.profilesDir ?? targetPath(componentDir, "profiles");
29
+ let bound;
30
+ try {
31
+ const manifest = loadManifest(targetPath(componentDir, "component.toml"));
32
+ const profile = loadProfile(join(profilesDir, manifest.harness.profile, "bindings.toml"));
33
+ bound = resolveManifest(manifest, profile, { context: "verify" }).bound;
34
+ }
35
+ catch (e) {
36
+ return {
37
+ verdict: "halt",
38
+ gates: [],
39
+ haltReason: e instanceof ManifestError
40
+ ? e.message
41
+ : `could not resolve gates: ${e.message}`,
42
+ };
43
+ }
44
+ const cheap = bound.filter((g) => g.cost === "cheap");
45
+ if (cheap.length === 0) {
46
+ return { verdict: "pass", gates: [] };
47
+ }
48
+ const gateRuns = runGates(cheap, { cwd: componentDir });
49
+ return { verdict: verdictOf(gateRuns), gates: gateRuns };
50
+ }
51
+ //# sourceMappingURL=pre.js.map
@@ -0,0 +1,51 @@
1
+ /**
2
+ * `harness hub` — the operator hub.
3
+ *
4
+ * harness hub sync --from <ledger-dir> --to <hub-ledger-dir>
5
+ * [--spool-from <dir> --spool-to <dir>]
6
+ * harness hub project [--ledger <dir>] [--redact]
7
+ *
8
+ * `sync` also copies the run-status spool (P7.2) when both `--spool-*` are
9
+ * given — the same pull channel, one command.
10
+ */
11
+ import { syncSpool } from "../runstatus/index.js";
12
+ import { HubError, projectLedger, syncLedger } from "./index.js";
13
+ function flag(args, name) {
14
+ const i = args.indexOf(`--${name}`);
15
+ return i >= 0 && args[i + 1] ? args[i + 1] : undefined;
16
+ }
17
+ export function hubCommand(args) {
18
+ const sub = args[0];
19
+ if (sub === "sync") {
20
+ const from = flag(args, "from");
21
+ const to = flag(args, "to");
22
+ if (!from || !to) {
23
+ process.stderr.write("harness hub sync --from <ledger-dir> --to <hub-ledger-dir>\n");
24
+ return 2;
25
+ }
26
+ try {
27
+ const r = syncLedger(from, to);
28
+ let extra = "";
29
+ const spoolFrom = flag(args, "spool-from");
30
+ const spoolTo = flag(args, "spool-to");
31
+ if (spoolFrom && spoolTo) {
32
+ const files = syncSpool(spoolFrom, spoolTo);
33
+ extra = `; ${files.length} run-status document(s)`;
34
+ }
35
+ process.stdout.write(`synced ${r.segments.length} segment(s), ${r.events} event(s); chain intact${extra}\n`);
36
+ return 0;
37
+ }
38
+ catch (e) {
39
+ process.stderr.write(`${e.message}\n`);
40
+ return e instanceof HubError ? 1 : 2;
41
+ }
42
+ }
43
+ if (sub === "project") {
44
+ const state = projectLedger(flag(args, "ledger") ?? ".harness/ledger", args.includes("--redact") ? { dropReasonText: true } : undefined);
45
+ process.stdout.write(JSON.stringify(state, null, 2) + "\n");
46
+ return 0;
47
+ }
48
+ process.stderr.write("harness hub <sync --from … --to … | project [--ledger …] [--redact]>\n");
49
+ return 2;
50
+ }
51
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1,146 @@
1
+ /**
2
+ * hub — the operator side. Pulls read-only ledger replicas, runs a
3
+ * deterministic projection over them, and can redact reason-text on export.
4
+ *
5
+ * - sync: copy JSONL segments, then verify the chain — a divergent replica
6
+ * fails loudly (HubError), never silently stale.
7
+ * - project: a pure reduction to a derived state. Re-projecting from scratch
8
+ * yields a byte-identical object (the golden property).
9
+ * - redact: strip reason / detail / context text for clients that require it.
10
+ */
11
+ import { cpSync, existsSync, mkdirSync, readdirSync } from "node:fs";
12
+ import { join } from "node:path";
13
+ import { read as ledgerRead, verifyChain, } from "../ledger/index.js";
14
+ import { projectUnits } from "../units/index.js";
15
+ import { projectDoors } from "../doors/index.js";
16
+ export class HubError extends Error {
17
+ name = "HubError";
18
+ }
19
+ const SEGMENT_RE = /^\d{6}\.jsonl$/;
20
+ /** Copy ledger segments from a source dir into the hub, then verify the chain. */
21
+ export function syncLedger(fromDir, toDir) {
22
+ if (!existsSync(fromDir))
23
+ throw new HubError(`no ledger at ${fromDir}`);
24
+ mkdirSync(toDir, { recursive: true });
25
+ const segments = readdirSync(fromDir)
26
+ .filter((n) => SEGMENT_RE.test(n))
27
+ .sort();
28
+ for (const seg of segments)
29
+ cpSync(join(fromDir, seg), join(toDir, seg));
30
+ const chain = verifyChain(toDir);
31
+ if (!chain.ok) {
32
+ throw new HubError(`divergent replica: chain BROKEN at seq ${chain.seq} — ${chain.reason}`);
33
+ }
34
+ return { segments, events: chain.length };
35
+ }
36
+ const REDACT_KEYS = ["reason", "detail", "context", "why"];
37
+ /** Return copies of the events with free-text fields blanked per config. */
38
+ export function redact(events, config) {
39
+ if (!config.dropReasonText)
40
+ return events;
41
+ return events.map((e) => {
42
+ const body = { ...e.body };
43
+ for (const k of REDACT_KEYS)
44
+ if (k in body)
45
+ body[k] = "[redacted]";
46
+ return { ...e, body };
47
+ });
48
+ }
49
+ /** Deterministic projection — sorted throughout, order-independent, re-runnable. */
50
+ export function project(events) {
51
+ const sorted = [...events].sort((a, b) => a.seq - b.seq);
52
+ const units = [...projectUnits(sorted).values()].sort((a, b) => a.id.localeCompare(b.id));
53
+ const doors = [...projectDoors(sorted).values()].sort((a, b) => a.id.localeCompare(b.id));
54
+ const unitCounts = {};
55
+ for (const u of units)
56
+ unitCounts[u.state] = (unitCounts[u.state] ?? 0) + 1;
57
+ const sessions = new Map();
58
+ for (const e of sorted) {
59
+ if (!e.type.startsWith("session."))
60
+ continue;
61
+ const b = e.body;
62
+ const id = b.session_id ?? "unknown";
63
+ const s = sessions.get(id) ?? {
64
+ id,
65
+ unit: b.unit_id ?? "",
66
+ model: null,
67
+ costUsd: 0,
68
+ events: 0,
69
+ };
70
+ s.events++;
71
+ if (b.unit_id)
72
+ s.unit = b.unit_id;
73
+ if (b.model)
74
+ s.model = b.model;
75
+ if (typeof b.cost_usd === "number")
76
+ s.costUsd += b.cost_usd;
77
+ sessions.set(id, s);
78
+ }
79
+ const escapes = [];
80
+ for (const e of sorted) {
81
+ if (e.type !== "escape.routed")
82
+ continue;
83
+ const b = e.body;
84
+ escapes.push({
85
+ scenario: b.scenario ?? "",
86
+ owner: b.owner ?? null,
87
+ newUnit: b.new_unit ?? null,
88
+ });
89
+ }
90
+ const systemHalts = [];
91
+ for (const e of sorted) {
92
+ if (e.type !== "system.halted")
93
+ continue;
94
+ const b = e.body;
95
+ systemHalts.push({ cause: b.cause ?? "", detail: b.detail ?? "" });
96
+ }
97
+ const runAnalyses = [];
98
+ for (const e of sorted) {
99
+ if (e.type !== "run.analyzed")
100
+ continue;
101
+ const b = e.body;
102
+ runAnalyses.push({
103
+ runId: b.run_id ?? "",
104
+ ts: e.ts,
105
+ units: (b.units ?? []).map((u) => ({
106
+ unitId: u.unit_id ?? "",
107
+ turnsTotal: u.turns_total ?? 0,
108
+ toolFailures: u.tool_failures ?? 0,
109
+ attempts: u.attempts ?? 0,
110
+ wallMs: u.wall_ms ?? 0,
111
+ agentMs: u.agent_ms ?? 0,
112
+ costUsd: typeof u.cost_usd === "number" ? u.cost_usd : null,
113
+ runner: u.runner ?? "",
114
+ model: u.model ?? "",
115
+ })),
116
+ findings: (b.findings ?? []).map((f) => ({
117
+ rule: f.rule ?? "",
118
+ unitId: f.unit_id ?? "",
119
+ severity: f.severity ?? "info",
120
+ count: f.count ?? 0,
121
+ })),
122
+ logRef: b.log_ref ?? "",
123
+ });
124
+ }
125
+ return {
126
+ system: sorted[0]?.system ?? null,
127
+ counts: {
128
+ events: sorted.length,
129
+ units: unitCounts,
130
+ doorsOpen: doors.filter((d) => d.state === "opened").length,
131
+ escapes: escapes.length,
132
+ systemHalts: systemHalts.length,
133
+ },
134
+ units,
135
+ doors,
136
+ sessions: [...sessions.values()].sort((a, b) => a.id.localeCompare(b.id)),
137
+ escapes,
138
+ systemHalts,
139
+ runAnalyses,
140
+ };
141
+ }
142
+ export function projectLedger(ledgerDir, redaction) {
143
+ const events = ledgerRead(ledgerDir);
144
+ return project(redaction ? redact(events, redaction) : events);
145
+ }
146
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,83 @@
1
+ /**
2
+ * init — scaffold hooks (`harness hook pre-tool-use`).
3
+ *
4
+ * Named owner of init-composes-hook-install: system-harness `init` invokes the
5
+ * agent-harness hook-install path (`adapter/install`) directly as a subprocess
6
+ * composition. system-harness does not import `@harness/agent`. The published
7
+ * facade only routes `harness init` here and adds no hook-install logic.
8
+ */
9
+ import { spawnSync } from "node:child_process";
10
+ import { existsSync, mkdirSync } from "node:fs";
11
+ import { dirname, join, resolve } from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+ import { packageProfilesDir, packageTemplatesDir, targetOwnedPaths, targetPath, } from "../assets.js";
14
+ /** Public facade hook command — never an internal binary path. */
15
+ export const HOOK_COMMAND = "harness hook pre-tool-use";
16
+ /** Documented composition: system init → agent-harness adapter/install. */
17
+ export const HOOK_INSTALL_OWNER = "system-harness init composes agent-harness adapter/install";
18
+ function resolveAgentHookInstall() {
19
+ const here = dirname(fileURLToPath(import.meta.url));
20
+ const fromEnv = process.env.AGENT_HARNESS_BIN;
21
+ const candidates = [
22
+ // staged / published layout: dist/internal/{system,agent}/
23
+ join(here, "..", "..", "agent", "adapter", "install.js"),
24
+ // workspace layout: repos/{system,agent}-harness/{src,dist}/
25
+ join(here, "..", "..", "..", "agent-harness", "dist", "adapter", "install.js"),
26
+ ...(fromEnv ? [join(dirname(fromEnv), "adapter", "install.js")] : []),
27
+ ];
28
+ for (const candidate of candidates) {
29
+ if (existsSync(candidate))
30
+ return candidate;
31
+ }
32
+ throw new Error(`agent-harness hook-install path not found (looked in ${candidates.join(", ")}); build @harness/agent`);
33
+ }
34
+ /**
35
+ * Invoke the agent-harness hook-install path. `command` is accepted for call-site
36
+ * compatibility; the install path always writes `HOOK_COMMAND`.
37
+ */
38
+ export function writeInitHooks(repoRoot, _command = HOOK_COMMAND) {
39
+ const install = resolveAgentHookInstall();
40
+ const result = spawnSync(process.execPath, [install, "--cwd", resolve(repoRoot)], {
41
+ encoding: "utf8",
42
+ windowsHide: true,
43
+ });
44
+ if (result.status !== 0) {
45
+ const detail = (result.stderr || result.stdout || "").trim();
46
+ throw new Error(`hook-install failed (${result.status}): ${detail || "no output"}`);
47
+ }
48
+ const written = (result.stdout ?? "").trim();
49
+ return written || targetPath(repoRoot, ".claude", "settings.json");
50
+ }
51
+ export function runInit(repoRoot) {
52
+ const root = resolve(repoRoot);
53
+ return {
54
+ repoRoot: root,
55
+ hooksPath: writeInitHooks(root),
56
+ profilesDir: packageProfilesDir(),
57
+ templatesDir: packageTemplatesDir(),
58
+ };
59
+ }
60
+ function flag(args, name) {
61
+ const i = args.indexOf(`--${name}`);
62
+ return i >= 0 && args[i + 1] ? args[i + 1] : undefined;
63
+ }
64
+ export function initCommand(args, cwd = process.cwd()) {
65
+ if (args.includes("--help") || args.includes("-h")) {
66
+ process.stdout.write("harness init [--cwd <dir>] scaffold hooks (harness hook pre-tool-use)\n");
67
+ return 0;
68
+ }
69
+ const repo = resolve(flag(args, "cwd") ?? cwd);
70
+ mkdirSync(targetOwnedPaths(repo).harnessDir, { recursive: true });
71
+ try {
72
+ const result = runInit(repo);
73
+ process.stdout.write(`✓ hooks ${result.hooksPath}\n`);
74
+ process.stdout.write(` (profiles from ${result.profilesDir})\n`);
75
+ process.stdout.write(` (templates from ${result.templatesDir})\n`);
76
+ return 0;
77
+ }
78
+ catch (error) {
79
+ process.stderr.write(`harness init: ${error.message}\n`);
80
+ return 1;
81
+ }
82
+ }
83
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,54 @@
1
+ /**
2
+ * `harness ledger <sub>` — the single write path into the record plane.
3
+ *
4
+ * harness ledger append < event.json append one event (stdin JSON), print it
5
+ * harness ledger verify-chain recompute the chain; exit 1 on any break
6
+ * harness ledger read print every event as JSONL
7
+ *
8
+ * Common flag: --dir <path> (default .harness/ledger)
9
+ *
10
+ * agent-harness appends its session and eval events through this same command,
11
+ * so chaining (and later, signing) live in one implementation.
12
+ */
13
+ import { readFileSync } from "node:fs";
14
+ import { append, read, verifyChain } from "./index.js";
15
+ const DEFAULT_DIR = ".harness/ledger";
16
+ function dirFrom(args) {
17
+ const i = args.indexOf("--dir");
18
+ return i >= 0 && args[i + 1] ? args[i + 1] : DEFAULT_DIR;
19
+ }
20
+ export function ledgerCommand(args) {
21
+ const sub = args[0];
22
+ const dir = dirFrom(args);
23
+ switch (sub) {
24
+ case "append": {
25
+ const raw = readFileSync(0, "utf8").trim();
26
+ if (raw === "") {
27
+ process.stderr.write("harness ledger append: expected an event JSON on stdin\n");
28
+ return 2;
29
+ }
30
+ const ev = JSON.parse(raw);
31
+ const written = append(dir, ev);
32
+ process.stdout.write(JSON.stringify(written) + "\n");
33
+ return 0;
34
+ }
35
+ case "read": {
36
+ for (const e of read(dir))
37
+ process.stdout.write(JSON.stringify(e) + "\n");
38
+ return 0;
39
+ }
40
+ case "verify-chain": {
41
+ const result = verifyChain(dir);
42
+ if (result.ok) {
43
+ process.stdout.write(`ok — ${result.length} event(s), chain intact\n`);
44
+ return 0;
45
+ }
46
+ process.stderr.write(`BROKEN at seq ${result.seq}: ${result.reason}\n`);
47
+ return 1;
48
+ }
49
+ default:
50
+ process.stderr.write("harness ledger <append|read|verify-chain> [--dir <path>]\n");
51
+ return 2;
52
+ }
53
+ }
54
+ //# sourceMappingURL=cli.js.map