@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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mike Nguyen
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.
package/README.md ADDED
@@ -0,0 +1,43 @@
1
+ # @mikenguyen69/harness
2
+
3
+ The publishable npm distribution facade for the portfolio. One `harness` binary
4
+ routes operator and integration commands to `@harness/system`, `@harness/agent`,
5
+ and `@harness/orchestration` as child processes — no verification, door, run,
6
+ routing, or hook-install policy is reimplemented here.
7
+
8
+ In the development workspace, invoke the facade by path:
9
+
10
+ ```bash
11
+ node repos/npm-harness/dist/cli.js --help
12
+ ```
13
+
14
+ The workspace `node_modules/.bin/harness` shim continues to point at
15
+ `@harness/system` until the staged publish manifest adds `bin.harness` (see
16
+ `ipkg-deterministic-tarball`).
17
+
18
+ ## Cancellation (process-group-break)
19
+
20
+ A console interrupt (Ctrl+C / CTRL_BREAK) while `harness run` (or any dispatched
21
+ child) is in flight is handled as one documented path across platforms. The
22
+ facade never sends SIGINT through ChildProcess#kill — on Windows that is
23
+ TerminateProcess and does not emulate a console break.
24
+
25
+ | Platform | Spawn | Interrupt |
26
+ |----------|-------|-----------|
27
+ | POSIX | `detached: true` (new process group) | `process.kill(-pid, "SIGTERM")` |
28
+ | Windows | `CREATE_NEW_PROCESS_GROUP` (shared console; not Node `detached` / `DETACHED_PROCESS`) | `GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, groupPid)` |
29
+
30
+ The facade converts a console Ctrl+C (`SIGINT`) into that interrupt — Windows
31
+ process groups ignore CTRL_C by default, so the handler is not a no-op. In-flight
32
+ agent/orchestration grandchildren must stay in that process group (runners spawn
33
+ without `detached`). If the tree has not exited after a short grace period, the
34
+ facade reaps it (`taskkill /T /F` last resort on Windows; a Win32 job with
35
+ `KILL_ON_JOB_CLOSE` also covers grandchildren). The grace reap is not cancelled
36
+ just because the direct child closed.
37
+
38
+ Manual evidence (process tree before/after + exit code):
39
+
40
+ ```bash
41
+ node repos/npm-harness/scripts/interrupt-evidence.mjs # interactive Ctrl+C
42
+ node repos/npm-harness/scripts/interrupt-evidence.mjs --auto # POSIX / CI
43
+ ```
@@ -0,0 +1 @@
1
+ {"system":"0.0.1","agent":"0.0.1","orchestration":"0.0.1"}
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Cross-platform cancellation path for in-flight `harness` children.
3
+ *
4
+ * One operator action (console interrupt: Ctrl+C / CTRL_BREAK) maps to one
5
+ * documented mechanism and MUST NOT call ChildProcess#kill with SIGINT — on
6
+ * Windows that is TerminateProcess and does not emulate a console break.
7
+ *
8
+ * | Platform | Spawn | Interrupt |
9
+ * |----------|-------|-----------|
10
+ * | POSIX | `detached: true` (new process group) | `process.kill(-pid, "SIGTERM")` |
11
+ * | Windows | `CREATE_NEW_PROCESS_GROUP` (shared console, not `DETACHED_PROCESS`) | `GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, groupPid)` |
12
+ *
13
+ * Runners must spawn in-flight agent children **without** `detached` so they
14
+ * stay in that process group. If the tree has not exited after GRACE_MS, it is
15
+ * reaped (POSIX SIGKILL / Windows `taskkill /T /F` last resort; a Win32 job
16
+ * with KILL_ON_JOB_CLOSE also covers grandchildren).
17
+ */
18
+ import { spawn } from "node:child_process";
19
+ import { sendCtrlBreak, spawnWindowsProcessGroup } from "./win32-process-group.js";
20
+ export const CANCELLATION_PATH = {
21
+ name: "process-group-break",
22
+ posix: "detached process group + SIGTERM to -pid",
23
+ windows: "CREATE_NEW_PROCESS_GROUP + GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, groupPid)",
24
+ never: "ChildProcess kill with SIGINT",
25
+ };
26
+ /** Grace period after interrupt before a force-reap of the process tree. */
27
+ export const CANCELLATION_GRACE_MS = 5_000;
28
+ export function cancellableSpawnOptions(base = {}) {
29
+ const isWin = process.platform === "win32";
30
+ return {
31
+ ...base,
32
+ // POSIX: new process group so kill(-pid) reaches the tree.
33
+ // Windows: Node's `detached` sets DETACHED_PROCESS and breaks CTRL_BREAK
34
+ // delivery — Windows spawn goes through spawnCancellable instead.
35
+ detached: !isWin,
36
+ windowsHide: false,
37
+ };
38
+ }
39
+ /**
40
+ * Spawn a child in the documented cancellation group.
41
+ * Windows: CREATE_NEW_PROCESS_GROUP via the Win32 helper (not Node `detached`).
42
+ * POSIX: `detached: true`.
43
+ */
44
+ export function spawnCancellable(command, args, options = {}) {
45
+ if (process.platform === "win32") {
46
+ return spawnWindowsProcessGroup(command, args, options);
47
+ }
48
+ const child = spawn(command, args, cancellableSpawnOptions(options));
49
+ if (child.pid === undefined) {
50
+ throw new Error("spawnCancellable: child pid missing");
51
+ }
52
+ child.groupPid = child.pid;
53
+ return child;
54
+ }
55
+ function groupPidOf(child) {
56
+ const tagged = child;
57
+ return tagged.groupPid || child.pid;
58
+ }
59
+ /**
60
+ * Deliver the documented interrupt to `pid`'s process group.
61
+ * Windows: CTRL_BREAK via GenerateConsoleCtrlEvent. Never taskkill, never
62
+ * ChildProcess#kill("SIGINT").
63
+ */
64
+ export function interruptProcessTree(pid) {
65
+ if (process.platform === "win32") {
66
+ sendCtrlBreak(pid);
67
+ return;
68
+ }
69
+ try {
70
+ process.kill(-pid, "SIGTERM");
71
+ }
72
+ catch {
73
+ try {
74
+ process.kill(pid, "SIGTERM");
75
+ }
76
+ catch {
77
+ // already gone
78
+ }
79
+ }
80
+ }
81
+ /** Last-resort tree reap so a stuck child cannot orphan grandchildren. */
82
+ export function reapProcessTree(pid) {
83
+ if (process.platform === "win32") {
84
+ spawn("taskkill", ["/pid", String(pid), "/t", "/f"], {
85
+ stdio: "ignore",
86
+ windowsHide: true,
87
+ }).on("error", () => undefined);
88
+ return;
89
+ }
90
+ try {
91
+ process.kill(-pid, "SIGKILL");
92
+ }
93
+ catch {
94
+ try {
95
+ process.kill(pid, "SIGKILL");
96
+ }
97
+ catch {
98
+ // already gone
99
+ }
100
+ }
101
+ }
102
+ export function attachCancellation(child, onForwarded) {
103
+ let forwarded = false;
104
+ let reaper;
105
+ const forward = () => {
106
+ const pid = groupPidOf(child);
107
+ if (pid === undefined || forwarded)
108
+ return;
109
+ forwarded = true;
110
+ onForwarded?.();
111
+ interruptProcessTree(pid);
112
+ reaper = setTimeout(() => reapProcessTree(pid), CANCELLATION_GRACE_MS);
113
+ reaper.unref();
114
+ };
115
+ process.on("SIGINT", forward);
116
+ process.on("SIGTERM", forward);
117
+ if (process.platform === "win32") {
118
+ process.on("SIGBREAK", forward);
119
+ }
120
+ else {
121
+ process.on("SIGHUP", forward);
122
+ }
123
+ return () => {
124
+ // Do not clear the grace reap: the direct child (or helper) closing is not
125
+ // proof that grandchildren are gone. An unref'd reaper is harmless if the
126
+ // tree already exited; it is the only backstop if they did not.
127
+ process.removeListener("SIGINT", forward);
128
+ process.removeListener("SIGTERM", forward);
129
+ if (process.platform === "win32") {
130
+ process.removeListener("SIGBREAK", forward);
131
+ }
132
+ else {
133
+ process.removeListener("SIGHUP", forward);
134
+ }
135
+ void reaper;
136
+ };
137
+ }
138
+ //# sourceMappingURL=cancellation.js.map
package/dist/cli.js ADDED
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @mikenguyen69/harness — thin public dispatcher over the three portfolio CLIs.
4
+ *
5
+ * Owns command discovery, help/version output, package-relative internal CLI
6
+ * resolution, child-process dispatch, process-group cancellation, and
7
+ * exit-code propagation only. No verification, door, run, merge, routing, or
8
+ * hook-install policy lives here.
9
+ */
10
+ import { buildDispatchRequest, dispatch } from "./dispatch.js";
11
+ import { USAGE } from "./help.js";
12
+ import { resolveInternalClis } from "./paths.js";
13
+ import { buildInternalArgv, resolveRoute } from "./routes.js";
14
+ import { formatVersion } from "./version.js";
15
+ export async function main(argv) {
16
+ if (argv.includes("--help") || argv.includes("-h")) {
17
+ process.stdout.write(USAGE);
18
+ return 0;
19
+ }
20
+ if (argv.includes("--version") || argv.includes("-V")) {
21
+ process.stdout.write(formatVersion());
22
+ return 0;
23
+ }
24
+ const route = resolveRoute(argv);
25
+ if (route === null) {
26
+ process.stderr.write(argv.length === 0
27
+ ? USAGE
28
+ : `harness ${argv.join(" ")}: unknown command\nRun harness --help\n`);
29
+ return argv.length === 0 ? 0 : 2;
30
+ }
31
+ if (route === "status-skeleton") {
32
+ process.stderr.write("harness status: not implemented (skeleton)\n");
33
+ return 1;
34
+ }
35
+ const clis = resolveInternalClis();
36
+ const internalArgv = buildInternalArgv(route, argv);
37
+ const request = buildDispatchRequest(route.product, internalArgv, {
38
+ clis,
39
+ ...(route.injectOrchestrationBins ? { injectOrchestrationBins: true } : {}),
40
+ });
41
+ return dispatch(request);
42
+ }
43
+ const invoked = process.argv[1]?.endsWith("cli.js") ?? false;
44
+ if (invoked) {
45
+ void main(process.argv.slice(2)).then((code) => process.exit(code));
46
+ }
47
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1,70 @@
1
+ import { attachCancellation, spawnCancellable } from "./cancellation.js";
2
+ function orchestrationEnv(clis, base) {
3
+ const env = { ...base };
4
+ env.HARNESS_BIN = clis.system;
5
+ env.AGENT_HARNESS_BIN = clis.agent;
6
+ return env;
7
+ }
8
+ function commandLine(request) {
9
+ if (request.command.endsWith(".js")) {
10
+ return { command: process.execPath, args: [request.command, ...request.args] };
11
+ }
12
+ return { command: request.command, args: request.args };
13
+ }
14
+ export function dispatch(request) {
15
+ return new Promise((resolve) => {
16
+ const { command, args } = commandLine(request);
17
+ const spawnOpts = {
18
+ cwd: request.cwd ?? process.cwd(),
19
+ env: request.env ?? process.env,
20
+ stdio: "inherit",
21
+ };
22
+ const child = spawnCancellable(command, args, spawnOpts);
23
+ let settled = false;
24
+ const finish = (code) => {
25
+ if (settled)
26
+ return;
27
+ settled = true;
28
+ resolve(code);
29
+ };
30
+ const detach = attachCancellation(child);
31
+ child.on("error", () => {
32
+ detach();
33
+ finish(1);
34
+ });
35
+ child.on("close", (code) => {
36
+ detach();
37
+ finish(code ?? 1);
38
+ });
39
+ });
40
+ }
41
+ export function buildDispatchRequest(product, internalArgv, opts) {
42
+ const clis = opts.clis;
43
+ const entry = product === "system"
44
+ ? clis.system
45
+ : product === "agent"
46
+ ? clis.agent
47
+ : clis.orchestration;
48
+ let env = process.env;
49
+ if (opts.injectOrchestrationBins) {
50
+ env = orchestrationEnv(clis, process.env);
51
+ }
52
+ return { command: entry, args: internalArgv, env };
53
+ }
54
+ /** Synchronous dispatch for tests — preserves stdio pipes when configured. */
55
+ export function dispatchSync(request, spawnSync) {
56
+ const { command, args } = commandLine(request);
57
+ const child = spawnSync(command, args, {
58
+ cwd: request.cwd ?? process.cwd(),
59
+ env: request.env ?? process.env,
60
+ encoding: "utf8",
61
+ maxBuffer: 20 * 1024 * 1024,
62
+ });
63
+ return {
64
+ code: child.status ?? 1,
65
+ stdout: child.stdout ?? "",
66
+ stderr: child.stderr ?? "",
67
+ };
68
+ }
69
+ export { orchestrationEnv };
70
+ //# sourceMappingURL=dispatch.js.map
package/dist/help.js ADDED
@@ -0,0 +1,47 @@
1
+ export const USAGE = `harness — one command over the portfolio harness products
2
+
3
+ Operator journey:
4
+ harness init scaffold a target repository for harness work
5
+ harness doctor read-only preflight before a run
6
+ harness run drive the spec DAG with an agent fleet
7
+ harness resume continue after a signed door approval
8
+ harness status project run and lane status from RunState
9
+ harness report run render the end-of-run HTML report
10
+ harness report calibration aggregate verification envelopes (calibration input)
11
+
12
+ Advanced — verification and gates:
13
+ harness verify run all bound gates and emit the signed envelope
14
+ harness gate --pre ring-0 cheap gates before tool use
15
+
16
+ Advanced — specs and units:
17
+ harness spec check lint specs, dep DAG, consumes/produces
18
+ harness spec materialize write idempotent unit.planned facts
19
+ harness next list units ready to claim
20
+ harness claim atomically take a ready unit
21
+ harness heartbeat keep a claim alive
22
+ harness merge authorize or record a landed merge
23
+
24
+ Advanced — doors and ledger:
25
+ harness door list|show|approve|reject open-decision surface
26
+ harness ledger append|read|verify-chain record plane
27
+
28
+ Advanced — board and attestations:
29
+ harness board build|serve local live dashboard
30
+ harness attest provenance attestations for a merge
31
+
32
+ Advanced — calibration:
33
+ harness calibrate calibration jobs
34
+ harness digest digest surfaces
35
+
36
+ Integration (machine-facing, same binary):
37
+ harness hook pre-tool-use agent PreToolUse handler (payload on stdin)
38
+ harness session <phase> append session.<phase> evidence (stdin JSON)
39
+ harness escalate raise session.escalated for the current unit
40
+ harness route print the resolved route as JSON
41
+
42
+ Usage:
43
+ harness --help this text
44
+ harness --version facade version and internal build identity
45
+ harness <command> [args…] dispatch to the owning internal CLI
46
+ `;
47
+ //# sourceMappingURL=help.js.map
@@ -0,0 +1,53 @@
1
+ /**
2
+ * conformance — replays a recorded scenario through the adapter and returns the
3
+ * observable stream (hook exit codes + session event {type, body}). A scenario
4
+ * passes iff its replay equals its recorded expectation, field for field.
5
+ *
6
+ * The session events are compared as the adapter emits them (before the ledger
7
+ * stamps seq/ts/hash), so the comparison is deterministic.
8
+ */
9
+ import { buildSessionEvent, } from "./session.js";
10
+ import { handlePreToolUse, } from "./hook.js";
11
+ export function runScenario(s) {
12
+ const saved = {};
13
+ for (const [k, v] of Object.entries(s.env ?? {})) {
14
+ saved[k] = process.env[k];
15
+ process.env[k] = v;
16
+ }
17
+ const events = [];
18
+ const capture = (phase, body) => {
19
+ const built = buildSessionEvent(phase, body);
20
+ events.push({ type: built.type, body: built.body });
21
+ return { ok: true };
22
+ };
23
+ const exitCodes = [];
24
+ try {
25
+ for (const step of s.steps) {
26
+ if ("hook" in step) {
27
+ exitCodes.push(handlePreToolUse(step.hook, s.unit, capture).exitCode);
28
+ }
29
+ else {
30
+ capture(step.session, step.body);
31
+ }
32
+ }
33
+ }
34
+ finally {
35
+ for (const [k, v] of Object.entries(saved)) {
36
+ if (v === undefined)
37
+ delete process.env[k];
38
+ else
39
+ process.env[k] = v;
40
+ }
41
+ }
42
+ return { exitCodes, events };
43
+ }
44
+ export function checkScenario(s) {
45
+ const got = runScenario(s);
46
+ const want = { exitCodes: s.expect.exitCodes, events: s.expect.events };
47
+ const a = JSON.stringify(want, null, 2);
48
+ const b = JSON.stringify(got, null, 2);
49
+ return a === b
50
+ ? { name: s.name, ok: true }
51
+ : { name: s.name, ok: false, diff: `--- expected\n${a}\n--- actual\n${b}` };
52
+ }
53
+ //# sourceMappingURL=conformance.js.map
@@ -0,0 +1,113 @@
1
+ /**
2
+ * hook — the Claude Code PreToolUse handler.
3
+ *
4
+ * Reads the hook payload on stdin, resolves the current unit's scope, and:
5
+ * - blocks a Write/Edit outside scope (exit 2, reason on stderr) and emits
6
+ * session.gate_fired
7
+ * - otherwise, optionally runs ring-0 (`harness gate --pre`) when
8
+ * HARNESS_RING0=1; a real ring-0 failure blocks
9
+ * - on ANY internal failure (bad payload, no unit context, unreachable
10
+ * harness) it DEGRADES: warn on stderr, exit 0. The hook is the fast copy
11
+ * of the rule; verify and the substrate are the guarantee (principle 1).
12
+ */
13
+ import { spawnSync } from "node:child_process";
14
+ import { existsSync, readFileSync } from "node:fs";
15
+ import { join } from "node:path";
16
+ import { checkToolCall } from "./scope.js";
17
+ import { emitSessionEvent, } from "./session.js";
18
+ export function loadUnitContext(cwd) {
19
+ const file = join(cwd, ".harness", "current-unit.json");
20
+ if (existsSync(file)) {
21
+ try {
22
+ return JSON.parse(readFileSync(file, "utf8"));
23
+ }
24
+ catch {
25
+ /* fall through to env */
26
+ }
27
+ }
28
+ const id = process.env.HARNESS_UNIT_ID;
29
+ const component = process.env.HARNESS_UNIT_COMPONENT;
30
+ const scope = process.env.HARNESS_UNIT_SCOPE;
31
+ if (id && component && scope) {
32
+ return {
33
+ unit_id: id,
34
+ component_id: component,
35
+ scope: scope
36
+ .split(",")
37
+ .map((s) => s.trim())
38
+ .filter(Boolean),
39
+ };
40
+ }
41
+ return undefined;
42
+ }
43
+ function ring0(path, bin) {
44
+ const r = spawnSync(bin, ["gate", "--pre", "--file", path], {
45
+ encoding: "utf8",
46
+ windowsHide: true,
47
+ });
48
+ return { ok: r.status === 0, output: (r.stdout ?? "") + (r.stderr ?? "") };
49
+ }
50
+ /** Pure decision — no process.exit, for testability. */
51
+ export function handlePreToolUse(payload, ctx, emit = emitSessionEvent) {
52
+ // degrade: no unit context → let it through, warn
53
+ if (!ctx) {
54
+ return {
55
+ exitCode: 0,
56
+ message: "adapter: no current-unit context; scope not enforced at the hook (verify still will)",
57
+ };
58
+ }
59
+ const decision = checkToolCall(payload, ctx.scope, payload.cwd);
60
+ const session = payload.session_id ?? "unknown";
61
+ if (!decision.allow) {
62
+ emit("gate_fired", {
63
+ session_id: session,
64
+ unit_id: ctx.unit_id,
65
+ component_id: ctx.component_id,
66
+ ...(ctx.model ? { model: ctx.model } : {}),
67
+ gate: { name: "scope", outcome: "fail", ring: 0 },
68
+ }, { soft: true });
69
+ return decision.reason !== undefined
70
+ ? { exitCode: 2, message: decision.reason }
71
+ : { exitCode: 2 };
72
+ }
73
+ if (process.env.HARNESS_RING0 === "1" && decision.path) {
74
+ const bin = process.env.HARNESS_BIN ?? "harness";
75
+ const r = ring0(decision.path, bin);
76
+ if (!r.ok) {
77
+ emit("gate_fired", {
78
+ session_id: session,
79
+ unit_id: ctx.unit_id,
80
+ component_id: ctx.component_id,
81
+ gate: { name: "pre", outcome: "fail", ring: 0 },
82
+ }, { soft: true });
83
+ return {
84
+ exitCode: 2,
85
+ message: `ring-0 gate failed:\n${r.output.trim()}`,
86
+ };
87
+ }
88
+ }
89
+ return { exitCode: 0 };
90
+ }
91
+ /** stdin → decision → stdout/stderr + exit code. Never throws. */
92
+ export function runHookFromStdin(stdin) {
93
+ let payload;
94
+ try {
95
+ payload = JSON.parse(stdin);
96
+ }
97
+ catch {
98
+ return {
99
+ exitCode: 0,
100
+ message: "adapter: unparseable hook payload; allowing (fast-copy degrade)",
101
+ };
102
+ }
103
+ try {
104
+ return handlePreToolUse(payload, loadUnitContext(payload.cwd ?? process.cwd()));
105
+ }
106
+ catch (e) {
107
+ return {
108
+ exitCode: 0,
109
+ message: `adapter: internal error, allowing (fast-copy degrade): ${e.message}`,
110
+ };
111
+ }
112
+ }
113
+ //# sourceMappingURL=hook.js.map
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Agent-harness hook-install path.
3
+ *
4
+ * Invoked by system-harness `init` as a documented composition (spawn this
5
+ * module — do not import `@harness/agent` from system-harness). Writes
6
+ * `.claude/settings.json` with `harness hook pre-tool-use`.
7
+ */
8
+ import { fileURLToPath } from "node:url";
9
+ import { installHooks } from "./settings.js";
10
+ export { installHooks } from "./settings.js";
11
+ function flag(argv, name) {
12
+ const i = argv.indexOf(`--${name}`);
13
+ return i >= 0 && argv[i + 1] ? argv[i + 1] : undefined;
14
+ }
15
+ export function runHookInstall(argv, cwd = process.cwd()) {
16
+ const repo = flag(argv, "cwd") ?? argv[0] ?? cwd;
17
+ return installHooks(repo);
18
+ }
19
+ function invokedAsMain() {
20
+ const argv1 = process.argv[1];
21
+ if (!argv1)
22
+ return false;
23
+ const self = fileURLToPath(import.meta.url).replaceAll("\\", "/").toLowerCase();
24
+ const got = argv1.replaceAll("\\", "/").toLowerCase();
25
+ return got === self || got.endsWith("/adapter/install.js") || got.endsWith("/adapter/install.ts");
26
+ }
27
+ if (invokedAsMain()) {
28
+ process.stdout.write(`${runHookInstall(process.argv.slice(2))}\n`);
29
+ }
30
+ //# sourceMappingURL=install.js.map
@@ -0,0 +1,55 @@
1
+ /**
2
+ * scope — the adapter's fast copy of the fail-closed scope rule. A Write/Edit
3
+ * to a path outside the unit's declared `scope` globs is blocked, with the
4
+ * scope quoted back. This is the *fast* copy: the same check re-runs at
5
+ * `verify` (scope_attested) and at the merge gate, so a bypass here is caught,
6
+ * just later (principle 1).
7
+ */
8
+ import picomatch from "picomatch";
9
+ /** Tools whose file target is scope-checked. */
10
+ const WRITE_TOOLS = new Set(["Write", "Edit", "MultiEdit", "NotebookEdit"]);
11
+ function targetPath(call) {
12
+ const i = call.tool_input ?? {};
13
+ const p = i.file_path ?? i.path ?? i.notebook_path;
14
+ return typeof p === "string" ? p : undefined;
15
+ }
16
+ /** normalise to forward slashes, drop a leading ./ */
17
+ function norm(p) {
18
+ return p.replace(/\\/g, "/").replace(/^\.\//, "");
19
+ }
20
+ /**
21
+ * @param cwd the session working dir — used to make an absolute path relative
22
+ * before glob-matching against the (relative) scope globs.
23
+ */
24
+ export function checkToolCall(call, scopeGlobs, cwd) {
25
+ if (!WRITE_TOOLS.has(call.tool_name))
26
+ return { allow: true };
27
+ const raw = targetPath(call);
28
+ if (!raw)
29
+ return { allow: true };
30
+ let path = norm(raw);
31
+ if (cwd) {
32
+ const base = norm(cwd).replace(/\/$/, "") + "/";
33
+ if (path.startsWith(base))
34
+ path = path.slice(base.length);
35
+ }
36
+ if (scopeGlobs.length === 0) {
37
+ return {
38
+ allow: false,
39
+ path,
40
+ reason: "no scope declared for this unit — every unit must declare `scope`",
41
+ };
42
+ }
43
+ const isMatch = picomatch(scopeGlobs.map(norm), { dot: true });
44
+ if (isMatch(path))
45
+ return { allow: true, path };
46
+ return {
47
+ allow: false,
48
+ path,
49
+ reason: `"${path}" is outside this unit's declared scope.\n` +
50
+ ` scope: ${scopeGlobs.join(", ")}\n` +
51
+ " Touching files outside declared scope is drift. Ask to widen the scope or split a new unit.\n" +
52
+ " (This halt also re-runs at verify and at the merge gate.)",
53
+ };
54
+ }
55
+ //# sourceMappingURL=scope.js.map