@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.
- package/LICENSE +21 -0
- package/README.md +43 -0
- package/dist/build-identity.json +1 -0
- package/dist/cancellation.js +138 -0
- package/dist/cli.js +47 -0
- package/dist/dispatch.js +70 -0
- package/dist/help.js +47 -0
- package/dist/internal/agent/adapter/conformance.js +53 -0
- package/dist/internal/agent/adapter/hook.js +113 -0
- package/dist/internal/agent/adapter/install.js +30 -0
- package/dist/internal/agent/adapter/scope.js +55 -0
- package/dist/internal/agent/adapter/session.js +63 -0
- package/dist/internal/agent/adapter/settings.js +51 -0
- package/dist/internal/agent/cli.js +79 -0
- package/dist/internal/agent/evals/index.js +111 -0
- package/dist/internal/agent/index.js +15 -0
- package/dist/internal/agent/ledger-read.js +27 -0
- package/dist/internal/agent/routing/cli.js +104 -0
- package/dist/internal/agent/routing/index.js +123 -0
- package/dist/internal/agent/telemetry/index.js +97 -0
- package/dist/internal/orchestration/activity/index.js +111 -0
- package/dist/internal/orchestration/analyze/index.js +237 -0
- package/dist/internal/orchestration/cli.js +475 -0
- package/dist/internal/orchestration/config/index.js +66 -0
- package/dist/internal/orchestration/doctor/index.js +202 -0
- package/dist/internal/orchestration/forge/index.js +121 -0
- package/dist/internal/orchestration/git/index.js +181 -0
- package/dist/internal/orchestration/harness/cli.js +378 -0
- package/dist/internal/orchestration/harness/index.js +186 -0
- package/dist/internal/orchestration/harness/shim.js +182 -0
- package/dist/internal/orchestration/index.js +29 -0
- package/dist/internal/orchestration/loop/index.js +1107 -0
- package/dist/internal/orchestration/prompts/index.js +199 -0
- package/dist/internal/orchestration/report/index.js +125 -0
- package/dist/internal/orchestration/routes/index.js +85 -0
- package/dist/internal/orchestration/run/index.js +221 -0
- package/dist/internal/orchestration/runner/index.js +90 -0
- package/dist/internal/orchestration/runners/claude/index.js +246 -0
- package/dist/internal/orchestration/runners/claude/sdk.js +198 -0
- package/dist/internal/orchestration/runners/claude/wiring.js +98 -0
- package/dist/internal/orchestration/runners/codex/exec.js +114 -0
- package/dist/internal/orchestration/runners/codex/index.js +72 -0
- package/dist/internal/orchestration/runners/conformance/index.js +169 -0
- package/dist/internal/orchestration/runners/cursor/exec.js +531 -0
- package/dist/internal/orchestration/runners/cursor/index.js +184 -0
- package/dist/internal/orchestration/runners/stub/index.js +168 -0
- package/dist/internal/orchestration/runners/telemetry.js +36 -0
- package/dist/internal/orchestration/runstate/index.js +213 -0
- package/dist/internal/orchestration/runstate/publish.js +115 -0
- package/dist/internal/orchestration/worktree/index.js +89 -0
- package/dist/internal/system/adapters/index.js +19 -0
- package/dist/internal/system/agentops/index.js +16 -0
- package/dist/internal/system/agentops/scope.js +67 -0
- package/dist/internal/system/assets.js +45 -0
- package/dist/internal/system/board/cli.js +56 -0
- package/dist/internal/system/board/index.js +276 -0
- package/dist/internal/system/board/live.js +277 -0
- package/dist/internal/system/calibration/cli.js +45 -0
- package/dist/internal/system/calibration/index.js +141 -0
- package/dist/internal/system/cli.js +174 -0
- package/dist/internal/system/corpus/replay.js +79 -0
- package/dist/internal/system/digest/cli.js +23 -0
- package/dist/internal/system/digest/index.js +145 -0
- package/dist/internal/system/doors/classify.js +94 -0
- package/dist/internal/system/doors/cli.js +184 -0
- package/dist/internal/system/doors/index.js +249 -0
- package/dist/internal/system/doors/metrics.js +47 -0
- package/dist/internal/system/escapes/cli.js +45 -0
- package/dist/internal/system/escapes/index.js +81 -0
- package/dist/internal/system/explore/cli.js +77 -0
- package/dist/internal/system/explore/index.js +91 -0
- package/dist/internal/system/gates/cli.js +43 -0
- package/dist/internal/system/gates/detectors.js +55 -0
- package/dist/internal/system/gates/pre.js +51 -0
- package/dist/internal/system/hub/cli.js +51 -0
- package/dist/internal/system/hub/index.js +146 -0
- package/dist/internal/system/init/index.js +83 -0
- package/dist/internal/system/ledger/cli.js +54 -0
- package/dist/internal/system/ledger/index.js +176 -0
- package/dist/internal/system/manifest/index.js +207 -0
- package/dist/internal/system/provenance/index.js +13 -0
- package/dist/internal/system/report/index.js +15 -0
- package/dist/internal/system/runner/index.js +74 -0
- package/dist/internal/system/runstatus/cli.js +86 -0
- package/dist/internal/system/runstatus/index.js +168 -0
- package/dist/internal/system/sequencer/briefing.js +75 -0
- package/dist/internal/system/sequencer/cli.js +170 -0
- package/dist/internal/system/sequencer/index.js +156 -0
- package/dist/internal/system/spec/cli.js +110 -0
- package/dist/internal/system/spec/index.js +212 -0
- package/dist/internal/system/spec/materialize.js +111 -0
- package/dist/internal/system/spec/openspec-parse.js +83 -0
- package/dist/internal/system/spec/project.js +208 -0
- package/dist/internal/system/substrate/index.js +14 -0
- package/dist/internal/system/system/index.js +159 -0
- package/dist/internal/system/units/index.js +87 -0
- package/dist/internal/system/verify/cli.js +103 -0
- package/dist/internal/system/verify/declined-reasons.js +61 -0
- package/dist/internal/system/verify/index.js +188 -0
- package/dist/internal/system/verify/signing.js +79 -0
- package/dist/paths.js +50 -0
- package/dist/routes.js +93 -0
- package/dist/version.js +30 -0
- package/dist/win32-process-group.js +360 -0
- package/package.json +41 -0
- package/profiles/python/bindings.toml +59 -0
- package/profiles/terraform/bindings.toml +66 -0
- package/profiles/typescript/bindings.toml +64 -0
- package/profiles/typescript/smoke/README.md +8 -0
- package/schema/baseline.schema.json +26 -0
- package/schema/decision-classes.yaml +45 -0
- package/schema/envelope.schema.json +186 -0
- package/schema/ledger-event.schema.json +235 -0
- package/schema/manifest.schema.json +119 -0
- package/schema/routes.schema.json +55 -0
- package/schema/run-status.schema.json +162 -0
- package/schema/session-event.schema.json +74 -0
- package/schema/spec.schema.json +114 -0
- package/schema/system.schema.json +142 -0
- package/templates/ci/verify.yml +23 -0
- package/templates/target-kit/README.md +78 -0
- package/templates/target-kit/component.toml +20 -0
- package/templates/target-kit/orchestration.json +9 -0
- package/templates/target-kit/profiles/target/bindings.toml +6 -0
- package/templates/target-kit/routes.toml +5 -0
- package/templates/target-kit/specs/first-unit.yaml +11 -0
- 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
|