@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,184 @@
1
+ /**
2
+ * `harness door` / `harness merge` — the operator's decision surface.
3
+ *
4
+ * harness door list open doors, highest decision value first
5
+ * harness door show <id>
6
+ * harness door approve <id> --reason <r> [--who <w>]
7
+ * harness door reject <id> --reason <r> [--who <w>]
8
+ * harness door init-key one-time: create the operator key
9
+ * harness merge <unit-id> --check [--system <system.toml>] [facts…]
10
+ * harness merge <unit-id> --landed <sha> [--system <system.toml>] [facts…]
11
+ *
12
+ * merge facts: --one-way | --two-way | --adds-dependency | --schema-change
13
+ * --security | --gate-external | --artifact-producer-change
14
+ * --component <id> | --mode <name>
15
+ *
16
+ * `--check` authorizes without writing `unit.merged`; the actuator lands Git,
17
+ * then calls the `--landed` form to record the actual object id. A one-way /
18
+ * propose-only class opens a door and blocks until an operator approves it.
19
+ * `unit.merged` is written nowhere else. Every view of
20
+ * door state here re-verifies `door.decided` against the operator public key
21
+ * (ADR 0002) — an unsigned or forged decision displays and merges as if it
22
+ * never happened.
23
+ */
24
+ import { read as ledgerRead } from "../ledger/index.js";
25
+ import { loadOperatorPublicKey, loadOrCreateKey, operatorKeyExists } from "../verify/signing.js";
26
+ import { classifyMerge } from "./classify.js";
27
+ import { DoorError, bareUnitId, decideDoor, mergeGate, mergeUnit, openDoor, projectDoors, sweepExpired, } from "./index.js";
28
+ import { doorMetricsFromLedger } from "./metrics.js";
29
+ function flag(args, name) {
30
+ const i = args.indexOf(`--${name}`);
31
+ return i >= 0 && args[i + 1] ? args[i + 1] : undefined;
32
+ }
33
+ const has = (args, name) => args.includes(`--${name}`);
34
+ function opts(args) {
35
+ return {
36
+ ledgerDir: flag(args, "ledger") ?? ".harness/ledger",
37
+ keyDir: flag(args, "keys") ?? ".harness",
38
+ };
39
+ }
40
+ export function doorCommand(args) {
41
+ const sub = args[0];
42
+ if (sub === "init-key") {
43
+ const keyDir = opts(args).keyDir;
44
+ const existed = operatorKeyExists(keyDir);
45
+ loadOrCreateKey(keyDir, "operator-key");
46
+ process.stdout.write(existed
47
+ ? `operator key already exists at ${keyDir}/operator-key.pem\n`
48
+ : `operator key created at ${keyDir}/operator-key.pem — keep this private; only a human should hold it\n`);
49
+ return 0;
50
+ }
51
+ if (sub === "list") {
52
+ if (has(args, "sweep")) {
53
+ const gone = sweepExpired(opts(args));
54
+ if (gone.length)
55
+ process.stderr.write(`expired: ${gone.join(", ")}\n`);
56
+ }
57
+ const pub = loadOperatorPublicKey(opts(args).keyDir);
58
+ const doors = [
59
+ ...projectDoors(ledgerRead(opts(args).ledgerDir), pub).values(),
60
+ ]
61
+ .filter((d) => d.state === "opened")
62
+ .sort((a, b) => Date.parse(a.openedAt) - Date.parse(b.openedAt));
63
+ for (const d of doors)
64
+ process.stdout.write(`${d.id} ${d.unitId} [${d.classes.join(", ")}] opened ${d.openedAt}\n`);
65
+ if (doors.length === 0)
66
+ process.stderr.write("no open doors\n");
67
+ return 0;
68
+ }
69
+ if (sub === "metrics") {
70
+ const window = Number(flag(args, "window") ?? 30);
71
+ const m = doorMetricsFromLedger(opts(args).ledgerDir, Date.now(), window, opts(args).keyDir);
72
+ process.stdout.write(JSON.stringify(m, null, 2) + "\n");
73
+ if (m.rubberStampFlag) {
74
+ process.stderr.write(`⚠ rubber-stamp: approval ${(m.approvalRate * 100).toFixed(0)}% at ${m.medianDecisionSeconds}s median — below ${m.thresholds.approvalRateThreshold * 100}% / ${m.thresholds.minDeliberationSeconds}s\n`);
75
+ return 1;
76
+ }
77
+ return 0;
78
+ }
79
+ if (sub === "show") {
80
+ const pub = loadOperatorPublicKey(opts(args).keyDir);
81
+ const d = projectDoors(ledgerRead(opts(args).ledgerDir), pub).get(args[1] ?? "");
82
+ if (!d) {
83
+ process.stderr.write("no such door\n");
84
+ return 1;
85
+ }
86
+ process.stdout.write(JSON.stringify(d, null, 2) + "\n");
87
+ return 0;
88
+ }
89
+ if (sub === "approve" || sub === "reject") {
90
+ const id = args[1];
91
+ const reason = flag(args, "reason");
92
+ if (!id || !reason) {
93
+ process.stderr.write(`harness door ${sub} <id> --reason "<why>" [--who <w>]\n`);
94
+ return 2;
95
+ }
96
+ try {
97
+ const e = decideDoor(id, sub === "approve" ? "approved" : "rejected", reason, flag(args, "who") ?? "operator", opts(args));
98
+ process.stdout.write(`${sub === "approve" ? "approved" : "rejected"} ${id} (seq ${e.seq})\n`);
99
+ return 0;
100
+ }
101
+ catch (e) {
102
+ process.stderr.write(`${e.message}\n`);
103
+ return e instanceof DoorError ? 1 : 2;
104
+ }
105
+ }
106
+ process.stderr.write("harness door <list [--sweep] | show <id> | metrics [--window N] | approve <id> --reason … | reject <id> --reason … | init-key>\n");
107
+ return 2;
108
+ }
109
+ function factsFrom(args) {
110
+ const f = {};
111
+ if (has(args, "one-way"))
112
+ f.unitDoor = "one-way";
113
+ if (has(args, "two-way"))
114
+ f.unitDoor = "two-way";
115
+ if (has(args, "adds-dependency"))
116
+ f.addsDependency = true;
117
+ if (has(args, "schema-change"))
118
+ f.touchesSchemaOrApi = true;
119
+ if (has(args, "security"))
120
+ f.touchesSecurity = true;
121
+ if (has(args, "gate-external"))
122
+ f.gateExternal = true;
123
+ if (has(args, "artifact-producer-change"))
124
+ f.artifactProducerChange = true;
125
+ const component = flag(args, "component");
126
+ if (component)
127
+ f.component = component;
128
+ const mode = flag(args, "mode");
129
+ if (mode)
130
+ f.mode = mode;
131
+ return f;
132
+ }
133
+ export function mergeCommand(args) {
134
+ const unitId = args[0];
135
+ if (!unitId || unitId.startsWith("--")) {
136
+ process.stderr.write("harness merge <unit-id> <--check | --landed <sha>> [--system <system.toml>] [facts…]\n");
137
+ return 2;
138
+ }
139
+ const o = {
140
+ ...opts(args),
141
+ ...(flag(args, "system") ? { systemTomlPath: flag(args, "system") } : {}),
142
+ };
143
+ const facts = factsFrom(args);
144
+ const landed = flag(args, "landed") ?? "0000000";
145
+ if (has(args, "check")) {
146
+ const gate = mergeGate(unitId, facts, o);
147
+ if (gate.ok) {
148
+ process.stdout.write(`merge authorized ${unitId}\n`);
149
+ return 0;
150
+ }
151
+ return reportBlockedMerge(unitId, facts, o, gate.blockedBy);
152
+ }
153
+ const result = mergeUnit(unitId, facts, landed, o);
154
+ if (result.merged) {
155
+ process.stdout.write(`merged ${unitId}\n`);
156
+ return 0;
157
+ }
158
+ return reportBlockedMerge(unitId, facts, o, result.blockedBy);
159
+ }
160
+ function reportBlockedMerge(unitId, facts, o, blockedBy) {
161
+ // blocked — if it's a door situation, make sure one is open
162
+ const classification = classifyMerge(facts, o.systemTomlPath);
163
+ if (classification.requiresHuman) {
164
+ const pub = loadOperatorPublicKey(o.keyDir);
165
+ const existing = [
166
+ ...projectDoors(ledgerRead(o.ledgerDir), pub).values(),
167
+ ].find((d) => bareUnitId(d.unitId) === bareUnitId(unitId) &&
168
+ (d.state === "opened" || d.state === "rejected"));
169
+ if (!existing) {
170
+ const id = openDoor(unitId, classification, o);
171
+ process.stderr.write(`⛔ merge blocked — door ${id} opened. Decide with: harness door approve ${id} --reason "…"\n`);
172
+ }
173
+ else if (existing.state === "rejected") {
174
+ process.stderr.write(`⛔ merge blocked — door ${existing.id} was REJECTED: ${existing.reason}\n`);
175
+ }
176
+ else {
177
+ process.stderr.write(`⛔ merge blocked — door ${existing.id} awaits a decision\n`);
178
+ }
179
+ return 2;
180
+ }
181
+ process.stderr.write(`⛔ merge blocked — ${blockedBy}\n`);
182
+ return 2;
183
+ }
184
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1,249 @@
1
+ /**
2
+ * doors — the halt-and-decide gate on the transition to `merged`.
3
+ *
4
+ * A door is a ledger fact (door.opened → door.decided | door.expired), not a
5
+ * stored record. A one-way (or propose-only) class on a merge means the unit
6
+ * CANNOT reach `merged` until an operator decides the door — enforced by
7
+ * `mergeGate`; `mergeUnit` is the only writer of `unit.merged` and is called
8
+ * only after the actuator has actually landed Git.
9
+ *
10
+ * `door.decided` is signed by the operator key (ADR 0002) and every reader
11
+ * that treats a door as approved or rejected — `mergeGate` above all —
12
+ * re-verifies that signature against the public key at `opts.keyDir`. A
13
+ * `door.decided` event with no key material to check against, a missing
14
+ * `sig`, or a `sig` that doesn't verify is never applied: the door simply
15
+ * stays in its prior state, so an unsigned or forged decision cannot unblock
16
+ * a merge. The key itself is never auto-provisioned on the decide path — see
17
+ * `harness door init-key`.
18
+ */
19
+ import { append, read as ledgerRead, } from "../ledger/index.js";
20
+ import { projectUnits } from "../units/index.js";
21
+ import { canonicalJSON, loadOperatorPublicKey, loadOrCreateKey, verifySignature, } from "../verify/signing.js";
22
+ import { sign } from "node:crypto";
23
+ import { classifyMerge, } from "./classify.js";
24
+ const DEFAULT_EXPIRY_MS = 24 * 60 * 60 * 1000;
25
+ export function doorId(unitId, openSeq) {
26
+ return `door-${unitId.replace(/[#/]/g, "-")}-${openSeq}`;
27
+ }
28
+ /** Strip a `system#` prefix, if any — units and doors may be addressed either way. */
29
+ export function bareUnitId(id) {
30
+ return id.includes("#") ? id.slice(id.indexOf("#") + 1) : id;
31
+ }
32
+ /**
33
+ * Fold door.* events into current per-door state.
34
+ *
35
+ * `operatorPublicKey`, when given, gates every `door.decided`: an event with
36
+ * a missing or invalid signature is skipped entirely (the door stays
37
+ * "opened") instead of being trusted. Omitting the key preserves the old,
38
+ * unverified projection — used only by read-only, cross-machine aggregation
39
+ * (the hub) that does not hold the operator's key material and never writes
40
+ * `unit.merged`; every writer/decider path always passes the key.
41
+ */
42
+ export function projectDoors(events, operatorPublicKey) {
43
+ const doors = new Map();
44
+ for (const e of [...events].sort((a, b) => a.seq - b.seq)) {
45
+ const b = e.body;
46
+ if (e.type === "door.opened" && b.door_id && b.unit_id) {
47
+ doors.set(b.door_id, {
48
+ id: b.door_id,
49
+ unitId: b.unit_id,
50
+ classes: b.classes ?? [],
51
+ state: "opened",
52
+ openedAt: e.ts,
53
+ expiresAt: b.expires_at ??
54
+ new Date(Date.parse(e.ts) + DEFAULT_EXPIRY_MS).toISOString(),
55
+ lastSeq: e.seq,
56
+ });
57
+ }
58
+ else if (e.type === "door.decided" && b.door_id) {
59
+ const d = doors.get(b.door_id);
60
+ if (d && d.state === "opened") {
61
+ if (operatorPublicKey) {
62
+ const verified = e.sig !== undefined &&
63
+ verifySignature({ id: b.door_id, verdict: b.verdict, reason: b.reason, who: b.who }, e.sig, operatorPublicKey);
64
+ if (!verified)
65
+ continue; // unsigned or forged — never applied
66
+ }
67
+ d.state = b.verdict === "approved" ? "approved" : "rejected";
68
+ if (b.reason !== undefined)
69
+ d.reason = b.reason;
70
+ if (b.who !== undefined)
71
+ d.who = b.who;
72
+ d.decidedAt = e.ts;
73
+ d.lastSeq = e.seq;
74
+ }
75
+ }
76
+ else if (e.type === "door.expired" && b.door_id) {
77
+ const d = doors.get(b.door_id);
78
+ if (d && d.state === "opened") {
79
+ d.state = "expired";
80
+ d.lastSeq = e.seq;
81
+ }
82
+ }
83
+ }
84
+ return doors;
85
+ }
86
+ /** The operator public key for these options, if one has been initialized. */
87
+ function operatorKeyFor(opts) {
88
+ return loadOperatorPublicKey(opts.keyDir ?? ".harness");
89
+ }
90
+ /** Open a door for a unit whose merge classification requires one. Returns the door id, or null if no human is required. */
91
+ export function openDoor(unitId, classification, opts) {
92
+ if (!classification.requiresHuman)
93
+ return null;
94
+ const events = ledgerRead(opts.ledgerDir);
95
+ const seq = (events.at(-1)?.seq ?? -1) + 1;
96
+ const id = doorId(unitId, seq);
97
+ const ts = opts.now ?? new Date().toISOString();
98
+ const ev = {
99
+ system: opts.system ?? events[0]?.system ?? unitId,
100
+ actor: { kind: "harness", id: "harness" },
101
+ type: "door.opened",
102
+ ts,
103
+ body: {
104
+ door_id: id,
105
+ unit_id: unitId,
106
+ classes: classification.fired.map((f) => `${f.source}:${f.match}(${f.class})`),
107
+ expires_at: new Date(Date.parse(ts) + DEFAULT_EXPIRY_MS).toISOString(),
108
+ },
109
+ };
110
+ append(opts.ledgerDir, ev);
111
+ return id;
112
+ }
113
+ export class DoorError extends Error {
114
+ name = "DoorError";
115
+ }
116
+ /**
117
+ * Decide an open door. Requires a reason and a pre-existing operator key —
118
+ * this never creates one (`harness door init-key` does that, deliberately,
119
+ * once, before any decision is possible).
120
+ */
121
+ export function decideDoor(id, verdict, reason, who, opts) {
122
+ if (reason.trim() === "")
123
+ throw new DoorError("a decision needs a reason");
124
+ const keyDir = opts.keyDir ?? ".harness";
125
+ let sig;
126
+ try {
127
+ const { privateKey } = loadOrCreateKey(keyDir, "operator-key", {
128
+ create: false,
129
+ });
130
+ sig = sign(null, Buffer.from(canonicalJSON({ id, verdict, reason, who }), "utf8"), privateKey).toString("base64");
131
+ }
132
+ catch {
133
+ throw new DoorError(`no operator key at ${keyDir} — run "harness door init-key" first (a human, not an agent, should hold this key)`);
134
+ }
135
+ const events = ledgerRead(opts.ledgerDir);
136
+ const door = projectDoors(events, operatorKeyFor(opts)).get(id);
137
+ if (!door)
138
+ throw new DoorError(`no such door: ${id}`);
139
+ if (door.state !== "opened")
140
+ throw new DoorError(`door ${id} is already ${door.state}`);
141
+ return append(opts.ledgerDir, {
142
+ system: opts.system ?? events[0]?.system ?? door.unitId,
143
+ actor: { kind: "human", id: who },
144
+ type: "door.decided",
145
+ ts: opts.now ?? new Date().toISOString(),
146
+ sig,
147
+ body: { door_id: id, unit_id: door.unitId, verdict, reason, who },
148
+ });
149
+ }
150
+ /**
151
+ * `scope_attested` from a unit's most recent `envelope` event — `undefined` if
152
+ * it has none. Scope attestation is a merge precondition (it is NOT a HALT
153
+ * inside verify): a unit whose latest verify envelope did not attest its diff
154
+ * against a declared scope may not reach `merged`.
155
+ */
156
+ function latestEnvelopeScopeAttested(events, bareId) {
157
+ let attested;
158
+ for (const e of [...events].sort((a, b) => a.seq - b.seq)) {
159
+ if (e.type !== "envelope")
160
+ continue;
161
+ const b = e.body;
162
+ if (bareUnitId(b.unit_id ?? "") === bareId)
163
+ attested = b.scope_attested === true;
164
+ }
165
+ return attested;
166
+ }
167
+ /**
168
+ * Can this unit reach `merged`? It must be in-review, its latest verify
169
+ * envelope must carry `scope_attested: true`, and every human-required door
170
+ * class must have a matching APPROVED door. This function is read-only; the
171
+ * actuator uses it before Git land, then `mergeUnit` rechecks it when
172
+ * recording the post-land fact.
173
+ */
174
+ export function mergeGate(unitId, facts, opts) {
175
+ const events = ledgerRead(opts.ledgerDir);
176
+ const bare = bareUnitId(unitId);
177
+ const unit = projectUnits(events).get(bare);
178
+ const classification = classifyMerge(facts, opts.systemTomlPath);
179
+ if (unit?.mode === "explore") {
180
+ throw new DoorError(`unit "${unitId}" is mode: explore — explore units have NO edge to merged (D7). Exit via abort or graduate.`);
181
+ }
182
+ if (!unit || unit.state !== "in-review") {
183
+ return {
184
+ ok: false,
185
+ blockedBy: `unit is ${unit?.state ?? "unknown"}, not in-review`,
186
+ classification,
187
+ };
188
+ }
189
+ const scopeAttested = latestEnvelopeScopeAttested(events, bare);
190
+ if (scopeAttested !== true) {
191
+ return {
192
+ ok: false,
193
+ blockedBy: scopeAttested === undefined
194
+ ? "no verify envelope for this unit — scope attestation is required before merge (run `harness verify --base <ref> --specs <dir>`)"
195
+ : "latest verify envelope has scope_attested: false — re-run `harness verify --base <ref> --specs <dir>` with the diff inside the unit's declared scope",
196
+ classification,
197
+ };
198
+ }
199
+ if (!classification.requiresHuman)
200
+ return { ok: true, classification };
201
+ const approved = [
202
+ ...projectDoors(events, operatorKeyFor(opts)).values(),
203
+ ].some((d) => bareUnitId(d.unitId) === bare && d.state === "approved");
204
+ return approved
205
+ ? { ok: true, classification }
206
+ : {
207
+ ok: false,
208
+ blockedBy: "a one-way/propose-only door class fired and no approved door covers this unit",
209
+ classification,
210
+ };
211
+ }
212
+ /** Perform the merge — only through the gate. */
213
+ export function mergeUnit(unitId, facts, landedSha, opts) {
214
+ const gate = mergeGate(unitId, facts, opts);
215
+ if (!gate.ok)
216
+ return {
217
+ merged: false,
218
+ ...(gate.blockedBy ? { blockedBy: gate.blockedBy } : {}),
219
+ };
220
+ const events = ledgerRead(opts.ledgerDir);
221
+ append(opts.ledgerDir, {
222
+ system: opts.system ?? events[0]?.system ?? unitId,
223
+ actor: { kind: "harness", id: "harness" },
224
+ type: "unit.merged",
225
+ ts: opts.now ?? new Date().toISOString(),
226
+ body: { unit_id: unitId, landed: landedSha },
227
+ });
228
+ return { merged: true };
229
+ }
230
+ /** Sweep: append door.expired for every open door past its expiry. */
231
+ export function sweepExpired(opts) {
232
+ const now = Date.parse(opts.now ?? new Date().toISOString());
233
+ const events = ledgerRead(opts.ledgerDir);
234
+ const expired = [];
235
+ for (const d of projectDoors(events, operatorKeyFor(opts)).values()) {
236
+ if (d.state === "opened" && Date.parse(d.expiresAt) <= now) {
237
+ append(opts.ledgerDir, {
238
+ system: opts.system ?? events[0]?.system ?? d.unitId,
239
+ actor: { kind: "harness", id: "harness" },
240
+ type: "door.expired",
241
+ ts: new Date(now).toISOString(),
242
+ body: { door_id: d.id, unit_id: d.unitId },
243
+ });
244
+ expired.push(d.id);
245
+ }
246
+ }
247
+ return expired;
248
+ }
249
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,47 @@
1
+ import { read as ledgerRead } from "../ledger/index.js";
2
+ import { loadOperatorPublicKey } from "../verify/signing.js";
3
+ import { classifyMerge } from "./classify.js";
4
+ import { projectDoors } from "./index.js";
5
+ function median(xs) {
6
+ if (xs.length === 0)
7
+ return null;
8
+ const s = [...xs].sort((a, b) => a - b);
9
+ const m = Math.floor(s.length / 2);
10
+ return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
11
+ }
12
+ export function doorMetrics(events, now = Date.now(), windowDays = 30, operatorPublicKey) {
13
+ const cutoff = now - windowDays * 24 * 60 * 60 * 1000;
14
+ const doors = [...projectDoors(events, operatorPublicKey).values()];
15
+ const thresholds = classifyMerge({}).antiRubberStamp;
16
+ const inWindow = doors.filter((d) => Date.parse(d.decidedAt ?? d.openedAt) >= cutoff);
17
+ const decidedDoors = inWindow.filter((d) => d.state === "approved" || d.state === "rejected");
18
+ const approved = decidedDoors.filter((d) => d.state === "approved").length;
19
+ const rejected = decidedDoors.filter((d) => d.state === "rejected").length;
20
+ const expired = inWindow.filter((d) => d.state === "expired").length;
21
+ const open = doors.filter((d) => d.state === "opened").length;
22
+ const decisionSecs = decidedDoors
23
+ .filter((d) => d.decidedAt)
24
+ .map((d) => (Date.parse(d.decidedAt) - Date.parse(d.openedAt)) / 1000);
25
+ const med = median(decisionSecs);
26
+ const rate = decidedDoors.length ? approved / decidedDoors.length : null;
27
+ const rubberStampFlag = rate !== null &&
28
+ rate > thresholds.approvalRateThreshold &&
29
+ med !== null &&
30
+ med < thresholds.minDeliberationSeconds;
31
+ return {
32
+ windowDays,
33
+ decided: decidedDoors.length,
34
+ approved,
35
+ rejected,
36
+ expired,
37
+ open,
38
+ approvalRate: rate,
39
+ medianDecisionSeconds: med,
40
+ rubberStampFlag,
41
+ thresholds,
42
+ };
43
+ }
44
+ export function doorMetricsFromLedger(ledgerDir, now, windowDays, keyDir) {
45
+ return doorMetrics(ledgerRead(ledgerDir), now, windowDays, keyDir ? loadOperatorPublicKey(keyDir) : undefined);
46
+ }
47
+ //# sourceMappingURL=metrics.js.map
@@ -0,0 +1,45 @@
1
+ /**
2
+ * `harness escape route <scenario-id>` — route a failed integration scenario to
3
+ * its owning component as a new fix unit.
4
+ *
5
+ * harness escape route <scenario-id> --system <system.toml>
6
+ * [--ledger <dir>] [--context "<failing context>"]
7
+ */
8
+ import { integrationReady, routeEscape } from "./index.js";
9
+ import { loadSystem } from "../system/index.js";
10
+ import { read as ledgerRead } from "../ledger/index.js";
11
+ function flag(args, name) {
12
+ const i = args.indexOf(`--${name}`);
13
+ return i >= 0 && args[i + 1] ? args[i + 1] : undefined;
14
+ }
15
+ export function escapeCommand(args) {
16
+ const sub = args[0];
17
+ const ledgerDir = flag(args, "ledger") ?? ".harness/ledger";
18
+ const systemTomlPath = flag(args, "system");
19
+ if (sub === "ready") {
20
+ if (!systemTomlPath) {
21
+ process.stderr.write("harness escape ready <component-id> --system <system.toml>\n");
22
+ return 2;
23
+ }
24
+ const ok = integrationReady(args[1] ?? "", loadSystem(systemTomlPath), ledgerRead(ledgerDir));
25
+ process.stdout.write(ok ? "ready\n" : "not ready (a dep is not green)\n");
26
+ return ok ? 0 : 1;
27
+ }
28
+ if (sub === "route") {
29
+ const scenario = args[1];
30
+ if (!scenario || !systemTomlPath) {
31
+ process.stderr.write('harness escape route <scenario-id> --system <system.toml> [--context "…"]\n');
32
+ return 2;
33
+ }
34
+ const r = routeEscape(scenario, {
35
+ ledgerDir,
36
+ systemTomlPath,
37
+ ...(flag(args, "context") ? { failContext: flag(args, "context") } : {}),
38
+ });
39
+ process.stdout.write(`routed ${r.scenario} → ${r.owners.join(", ") || "(unrouted)"}; new unit ${r.newUnit}\n`);
40
+ return 0;
41
+ }
42
+ process.stderr.write("harness escape <route <scenario-id> | ready <component-id>> --system <system.toml>\n");
43
+ return 2;
44
+ }
45
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1,81 @@
1
+ /**
2
+ * escapes — ring 3 routing. A failing integration scenario is matched against
3
+ * `scenario_ownership` in system.toml, routed to the owning component(s) as a
4
+ * new scoped fix unit, and recorded as escape.detected → escape.routed. The
5
+ * failing context travels with the new unit.
6
+ */
7
+ import picomatch from "picomatch";
8
+ import { append, read as ledgerRead, } from "../ledger/index.js";
9
+ import { loadSystem } from "../system/index.js";
10
+ /** Which component(s) own failures matching `scenarioId`. */
11
+ export function ownersOf(scenarioId, manifest) {
12
+ const owners = new Set();
13
+ for (const so of manifest.scenario_ownership) {
14
+ if (picomatch(so.match)(scenarioId))
15
+ for (const o of so.owns)
16
+ owners.add(o);
17
+ }
18
+ return [...owners];
19
+ }
20
+ /**
21
+ * Detect + route an escaped integration failure. Appends escape.detected,
22
+ * escape.routed, and unit.planned for the fix unit (deps = the owning
23
+ * components' latest work is implied; the unit carries the failing context).
24
+ */
25
+ export function routeEscape(scenarioId, opts) {
26
+ const manifest = loadSystem(opts.systemTomlPath);
27
+ const owners = ownersOf(scenarioId, manifest);
28
+ const primary = owners[0] ?? "unrouted";
29
+ const events = ledgerRead(opts.ledgerDir);
30
+ const seq = (events.at(-1)?.seq ?? -1) + 1;
31
+ const newUnit = `${primary}#escape-${scenarioId.replace(/[^a-z0-9]+/gi, "-")}-${seq}`;
32
+ const ts = opts.now ?? new Date().toISOString();
33
+ const sys = manifest.system.id;
34
+ const actor = { kind: "harness", id: "integration-ring" };
35
+ append(opts.ledgerDir, {
36
+ system: sys,
37
+ actor,
38
+ type: "escape.detected",
39
+ ts,
40
+ body: {
41
+ scenario: scenarioId,
42
+ ...(opts.failContext ? { context: opts.failContext } : {}),
43
+ },
44
+ });
45
+ append(opts.ledgerDir, {
46
+ system: sys,
47
+ actor,
48
+ type: "escape.routed",
49
+ ts,
50
+ body: { scenario: scenarioId, owner: primary, owners, new_unit: newUnit },
51
+ });
52
+ append(opts.ledgerDir, {
53
+ system: sys,
54
+ actor,
55
+ type: "unit.planned",
56
+ ts,
57
+ body: {
58
+ unit_id: newUnit,
59
+ component: primary,
60
+ from_escape: scenarioId,
61
+ ...(opts.failContext ? { context: opts.failContext } : {}),
62
+ },
63
+ });
64
+ return { scenario: scenarioId, owners, newUnit };
65
+ }
66
+ /** Whether an integration component may run: every dep has a passing latest envelope. */
67
+ export function integrationReady(componentId, manifest, events) {
68
+ const comp = manifest.component.find((c) => c.id === componentId);
69
+ if (!comp || comp.role !== "integration")
70
+ return false;
71
+ const latestVerdict = new Map();
72
+ for (const e of events) {
73
+ if (e.type !== "envelope")
74
+ continue;
75
+ const b = e.body;
76
+ if (b.component_id)
77
+ latestVerdict.set(b.component_id, b.verdict ?? "");
78
+ }
79
+ return (comp.deps ?? []).every((d) => latestVerdict.get(d) === "pass");
80
+ }
81
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,77 @@
1
+ /**
2
+ * `harness explore` — the exploration lane's two exits.
3
+ *
4
+ * harness explore abort <unit-id> [--ledger <dir>]
5
+ * harness explore graduate <unit-id> --intent "…" --scope "glob,glob"
6
+ * [--criterion "stmt|verifier"]... [--ledger <dir>]
7
+ */
8
+ import { abortExplore, ExploreError, graduateExplore, } from "./index.js";
9
+ function flag(args, name) {
10
+ const i = args.indexOf(`--${name}`);
11
+ return i >= 0 && args[i + 1] ? args[i + 1] : undefined;
12
+ }
13
+ function multi(args, name) {
14
+ const out = [];
15
+ for (let i = 0; i < args.length; i++)
16
+ if (args[i] === `--${name}` && args[i + 1])
17
+ out.push(args[i + 1]);
18
+ return out;
19
+ }
20
+ export function exploreCommand(args) {
21
+ const sub = args[0];
22
+ const unitId = args[1];
23
+ const ledgerDir = flag(args, "ledger") ?? ".harness/ledger";
24
+ if (!unitId || unitId.startsWith("--")) {
25
+ process.stderr.write("harness explore <abort|graduate> <unit-id> …\n");
26
+ return 2;
27
+ }
28
+ try {
29
+ if (sub === "abort") {
30
+ const e = abortExplore(unitId, { ledgerDir });
31
+ process.stdout.write(`aborted ${unitId} (seq ${e.seq})\n`);
32
+ return 0;
33
+ }
34
+ if (sub === "graduate") {
35
+ const intent = flag(args, "intent");
36
+ const scope = (flag(args, "scope") ?? "")
37
+ .split(",")
38
+ .map((s) => s.trim())
39
+ .filter(Boolean);
40
+ if (!intent || scope.length === 0) {
41
+ process.stderr.write('harness explore graduate <unit-id> --intent "…" --scope "glob,glob" [--criterion "stmt|verifier"]…\n');
42
+ return 2;
43
+ }
44
+ const acceptanceCriteria = multi(args, "criterion").map((c) => {
45
+ const [statement, v] = c.split("|");
46
+ return v
47
+ ? { statement: statement.trim(), verifier: v.trim() }
48
+ : {
49
+ statement: statement.trim(),
50
+ manual: "carried from the spike; sharpen before build",
51
+ };
52
+ });
53
+ const discovered = {
54
+ intent,
55
+ scope,
56
+ acceptanceCriteria: acceptanceCriteria.length
57
+ ? acceptanceCriteria
58
+ : [
59
+ {
60
+ statement: "TODO from the spike",
61
+ manual: "sharpen before build",
62
+ },
63
+ ],
64
+ };
65
+ const r = graduateExplore(unitId, discovered, { ledgerDir });
66
+ process.stdout.write(`graduated ${unitId} → draft build spec ${r.draftSpecId}\n`);
67
+ return 0;
68
+ }
69
+ }
70
+ catch (e) {
71
+ process.stderr.write(`${e.message}\n`);
72
+ return e instanceof ExploreError ? 1 : 2;
73
+ }
74
+ process.stderr.write("harness explore <abort|graduate> <unit-id> …\n");
75
+ return 2;
76
+ }
77
+ //# sourceMappingURL=cli.js.map