@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,378 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli — the real `Harness`, shelling the `@harness/system` binary. Portfolio
|
|
3
|
+
* rule 1 forbids importing system-harness source, so every method is a
|
|
4
|
+
* subprocess call.
|
|
5
|
+
*
|
|
6
|
+
* This replaces the v0 text-parsing shim: `harness next --json` and
|
|
7
|
+
* `harness claim --json` (SH P7.4) now return structured data, so `next` /
|
|
8
|
+
* `claim` / `escalated` parse JSON directly and the briefing carries the
|
|
9
|
+
* spec-declared merge facts. `next --all --json` supplies truthful DAG sizing
|
|
10
|
+
* and both startup crash-window checks; `claim --resume-after-door` is the
|
|
11
|
+
* signed-approval-only recovery path.
|
|
12
|
+
*/
|
|
13
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
14
|
+
import { existsSync } from "node:fs";
|
|
15
|
+
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
16
|
+
/** Resolve an executable or JS entrypoint once; doctor prints this exact spec. */
|
|
17
|
+
export function resolveCommandSpec(bin, cwd) {
|
|
18
|
+
const candidate = isAbsolute(bin) ? bin : resolve(cwd, bin);
|
|
19
|
+
if (bin.endsWith(".js") && existsSync(candidate)) {
|
|
20
|
+
return { command: process.execPath, args: [candidate] };
|
|
21
|
+
}
|
|
22
|
+
if (existsSync(candidate))
|
|
23
|
+
return { command: candidate, args: [] };
|
|
24
|
+
try {
|
|
25
|
+
const locator = process.platform === "win32" ? "where.exe" : "which";
|
|
26
|
+
const found = execFileSync(locator, [bin], { cwd, encoding: "utf8" })
|
|
27
|
+
.split(/\r?\n/)
|
|
28
|
+
.map((line) => line.trim())
|
|
29
|
+
.find(Boolean);
|
|
30
|
+
if (found)
|
|
31
|
+
return { command: found, args: [] };
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// handled by the explicit error below
|
|
35
|
+
}
|
|
36
|
+
throw new Error(`executable not found: ${bin}`);
|
|
37
|
+
}
|
|
38
|
+
/** A failed state-changing CLI call, retaining the child diagnostics. */
|
|
39
|
+
export class CliMutationError extends Error {
|
|
40
|
+
request;
|
|
41
|
+
result;
|
|
42
|
+
name = "CliMutationError";
|
|
43
|
+
constructor(request, result) {
|
|
44
|
+
const detail = (result.stderr || result.stdout).trim();
|
|
45
|
+
super(`${request.command} ${request.args.join(" ")} exited ${result.code}` +
|
|
46
|
+
(detail ? `: ${detail}` : ""));
|
|
47
|
+
this.request = request;
|
|
48
|
+
this.result = result;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Cross-platform child-process primitive. Unlike execFile it supports stdin,
|
|
53
|
+
* never rejects for an ordinary non-zero exit, and preserves both output
|
|
54
|
+
* streams for the caller's policy decision.
|
|
55
|
+
*/
|
|
56
|
+
export function callProcess(request) {
|
|
57
|
+
return new Promise((resolve) => {
|
|
58
|
+
let stdout = "";
|
|
59
|
+
let stderr = "";
|
|
60
|
+
let settled = false;
|
|
61
|
+
const finish = (result) => {
|
|
62
|
+
if (settled)
|
|
63
|
+
return;
|
|
64
|
+
settled = true;
|
|
65
|
+
resolve(result);
|
|
66
|
+
};
|
|
67
|
+
const isWindowsShim = process.platform === "win32" && /\.(?:cmd|bat)$/i.test(request.command);
|
|
68
|
+
const command = isWindowsShim ? (process.env.ComSpec ?? "cmd.exe") : request.command;
|
|
69
|
+
const args = isWindowsShim
|
|
70
|
+
? ["/d", "/s", "/c", windowsCommandLine(request.command, request.args)]
|
|
71
|
+
: request.args;
|
|
72
|
+
const child = spawn(command, args, {
|
|
73
|
+
cwd: request.cwd,
|
|
74
|
+
env: request.env ?? process.env,
|
|
75
|
+
windowsHide: true,
|
|
76
|
+
windowsVerbatimArguments: isWindowsShim,
|
|
77
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
78
|
+
});
|
|
79
|
+
child.stdout.setEncoding("utf8");
|
|
80
|
+
child.stderr.setEncoding("utf8");
|
|
81
|
+
child.stdout.on("data", (chunk) => (stdout += chunk));
|
|
82
|
+
child.stderr.on("data", (chunk) => (stderr += chunk));
|
|
83
|
+
// A child that exits before consuming stdin can close the pipe early. Its
|
|
84
|
+
// exit/error event is the authoritative result; avoid an unhandled EPIPE.
|
|
85
|
+
child.stdin.on("error", () => undefined);
|
|
86
|
+
child.on("error", (error) => {
|
|
87
|
+
finish({ code: 1, stdout, stderr: stderr + error.message });
|
|
88
|
+
});
|
|
89
|
+
child.on("close", (code) => finish({ code: code ?? 1, stdout, stderr }));
|
|
90
|
+
if (request.stdin !== undefined)
|
|
91
|
+
child.stdin.end(request.stdin);
|
|
92
|
+
else
|
|
93
|
+
child.stdin.end();
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
function windowsCommandLine(command, args) {
|
|
97
|
+
const quote = (value) => `"${value.replaceAll("%", "%%").replaceAll("\"", "\"\"")}"`;
|
|
98
|
+
return `"${[command, ...args].map(quote).join(" ")}"`;
|
|
99
|
+
}
|
|
100
|
+
/** Map a `harness claim --json` payload onto this product's `Briefing`. Pure. */
|
|
101
|
+
export function briefingFromClaimJson(j) {
|
|
102
|
+
const facts = {
|
|
103
|
+
addsDependency: j.merge_facts.adds_dependency,
|
|
104
|
+
schemaChange: j.merge_facts.schema_change,
|
|
105
|
+
security: j.merge_facts.security,
|
|
106
|
+
gateExternal: j.merge_facts.gate_external,
|
|
107
|
+
artifactProducerChange: j.merge_facts.artifact_producer_change,
|
|
108
|
+
};
|
|
109
|
+
if (j.door)
|
|
110
|
+
facts.door = j.door;
|
|
111
|
+
const briefing = {
|
|
112
|
+
unitId: j.unit_id,
|
|
113
|
+
intent: j.intent,
|
|
114
|
+
acceptanceCriteria: j.acceptance_criteria,
|
|
115
|
+
scope: j.scope,
|
|
116
|
+
branch: j.branch,
|
|
117
|
+
mergeFacts: facts,
|
|
118
|
+
};
|
|
119
|
+
if (j.component)
|
|
120
|
+
briefing.component = j.component;
|
|
121
|
+
if (j.profile)
|
|
122
|
+
briefing.profile = j.profile;
|
|
123
|
+
return briefing;
|
|
124
|
+
}
|
|
125
|
+
export class CliHarness {
|
|
126
|
+
opts;
|
|
127
|
+
constructor(opts) {
|
|
128
|
+
this.opts = opts;
|
|
129
|
+
}
|
|
130
|
+
base() {
|
|
131
|
+
return this.opts.ledgerDir ? ["--ledger", this.opts.ledgerDir] : [];
|
|
132
|
+
}
|
|
133
|
+
callRead(args, stdin) {
|
|
134
|
+
return (this.opts.processCall ?? callProcess)({
|
|
135
|
+
command: this.opts.bin,
|
|
136
|
+
args: [...(this.opts.binArgs ?? []), ...args],
|
|
137
|
+
cwd: this.opts.cwd,
|
|
138
|
+
env: this.opts.env ?? process.env,
|
|
139
|
+
...(stdin !== undefined ? { stdin } : {}),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
async callMutation(args, stdin) {
|
|
143
|
+
const request = {
|
|
144
|
+
command: this.opts.bin,
|
|
145
|
+
args: [...(this.opts.binArgs ?? []), ...args],
|
|
146
|
+
cwd: this.opts.cwd,
|
|
147
|
+
env: this.opts.env ?? process.env,
|
|
148
|
+
...(stdin !== undefined ? { stdin } : {}),
|
|
149
|
+
};
|
|
150
|
+
const result = await (this.opts.processCall ?? callProcess)(request);
|
|
151
|
+
if (result.code !== 0)
|
|
152
|
+
throw new CliMutationError(request, result);
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
155
|
+
toReadyUnit(u) {
|
|
156
|
+
const r = { id: u.id, deps: u.deps, mode: u.mode };
|
|
157
|
+
if (u.component)
|
|
158
|
+
r.component = u.component;
|
|
159
|
+
return r;
|
|
160
|
+
}
|
|
161
|
+
async next(opts) {
|
|
162
|
+
const args = ["next", "--json", ...this.base()];
|
|
163
|
+
if (opts?.only)
|
|
164
|
+
args.push("--only", opts.only);
|
|
165
|
+
const { stdout } = await this.callRead(args);
|
|
166
|
+
return JSON.parse(stdout || "[]").map((u) => this.toReadyUnit(u));
|
|
167
|
+
}
|
|
168
|
+
async escalated() {
|
|
169
|
+
const { stdout } = await this.callRead([
|
|
170
|
+
"next",
|
|
171
|
+
"--escalated",
|
|
172
|
+
"--json",
|
|
173
|
+
...this.base(),
|
|
174
|
+
]);
|
|
175
|
+
return JSON.parse(stdout || "[]").map((u) => this.toReadyUnit(u));
|
|
176
|
+
}
|
|
177
|
+
async units() {
|
|
178
|
+
const { stdout } = await this.callRead(["next", "--all", "--json", ...this.base()]);
|
|
179
|
+
const out = [];
|
|
180
|
+
for (const u of JSON.parse(stdout || "[]")) {
|
|
181
|
+
const snap = { id: u.id, state: u.state ?? "planned" };
|
|
182
|
+
if (u.component)
|
|
183
|
+
snap.component = u.component;
|
|
184
|
+
if (u.done_at)
|
|
185
|
+
snap.heartbeatAt = u.done_at;
|
|
186
|
+
if (u.landed)
|
|
187
|
+
snap.landedSha = u.landed;
|
|
188
|
+
out.push(snap);
|
|
189
|
+
}
|
|
190
|
+
return out;
|
|
191
|
+
}
|
|
192
|
+
async claim(unitId, session) {
|
|
193
|
+
return this.claimWith(unitId, session, []);
|
|
194
|
+
}
|
|
195
|
+
async resumeAfterDoor(unitId, session) {
|
|
196
|
+
return this.claimWith(unitId, session, ["--resume-after-door", "--keys", join(this.opts.cwd, ".harness")]);
|
|
197
|
+
}
|
|
198
|
+
async claimWith(unitId, session, extra) {
|
|
199
|
+
const args = [
|
|
200
|
+
"claim",
|
|
201
|
+
unitId,
|
|
202
|
+
"--session",
|
|
203
|
+
session,
|
|
204
|
+
"--json",
|
|
205
|
+
...extra,
|
|
206
|
+
...this.base(),
|
|
207
|
+
];
|
|
208
|
+
if (this.opts.specsDir)
|
|
209
|
+
args.push("--specs", this.opts.specsDir);
|
|
210
|
+
if (this.opts.systemToml)
|
|
211
|
+
args.push("--system", this.opts.systemToml);
|
|
212
|
+
const { code, stdout } = await this.callRead(args);
|
|
213
|
+
if (code !== 0)
|
|
214
|
+
return null;
|
|
215
|
+
const briefing = briefingFromClaimJson(JSON.parse(stdout));
|
|
216
|
+
if (briefing.component) {
|
|
217
|
+
const descriptor = this.opts.components?.get(briefing.component);
|
|
218
|
+
if (descriptor)
|
|
219
|
+
briefing.componentRoot = descriptor.root;
|
|
220
|
+
}
|
|
221
|
+
return briefing;
|
|
222
|
+
}
|
|
223
|
+
async heartbeat(unitId, session) {
|
|
224
|
+
await this.callMutation(["heartbeat", unitId, "--session", session, ...this.base()]);
|
|
225
|
+
}
|
|
226
|
+
async ledgerAppend(event) {
|
|
227
|
+
const system = this.opts.systemId;
|
|
228
|
+
const actorId = event.session ?? this.opts.actorId;
|
|
229
|
+
if (!system || !actorId) {
|
|
230
|
+
throw new Error("CliHarness ledger append requires systemId and actorId context");
|
|
231
|
+
}
|
|
232
|
+
const body = {
|
|
233
|
+
unit_id: event.unitId,
|
|
234
|
+
...(event.session ? { session: event.session, session_id: event.session } : {}),
|
|
235
|
+
...event.body,
|
|
236
|
+
};
|
|
237
|
+
const payload = JSON.stringify({
|
|
238
|
+
system,
|
|
239
|
+
actor: { kind: event.session ? "agent" : "harness", id: actorId },
|
|
240
|
+
type: event.type,
|
|
241
|
+
body,
|
|
242
|
+
});
|
|
243
|
+
await this.callMutation(["ledger", "append", "--dir", this.opts.ledgerDir ?? join(this.opts.cwd, ".harness", "ledger")], payload);
|
|
244
|
+
}
|
|
245
|
+
async verify(briefing, session, baseRef, workspace) {
|
|
246
|
+
const componentDir = workspacePath(briefing.componentRoot ?? this.opts.cwd, workspace);
|
|
247
|
+
const componentId = briefing.component ?? "unknown";
|
|
248
|
+
const { code, stdout, stderr } = await this.callRead([
|
|
249
|
+
"verify",
|
|
250
|
+
"--component",
|
|
251
|
+
componentDir,
|
|
252
|
+
"--component-id",
|
|
253
|
+
componentId,
|
|
254
|
+
"--unit",
|
|
255
|
+
briefing.unitId,
|
|
256
|
+
"--session",
|
|
257
|
+
session,
|
|
258
|
+
"--base",
|
|
259
|
+
baseRef,
|
|
260
|
+
...(this.opts.specsDir ? ["--specs", workspacePath(this.opts.specsDir, workspace)] : []),
|
|
261
|
+
...(this.opts.profilesDir ? ["--profiles", workspacePath(this.opts.profilesDir, workspace)] : []),
|
|
262
|
+
...(this.opts.systemId ? ["--system-id", this.opts.systemId] : []),
|
|
263
|
+
"--json",
|
|
264
|
+
...this.base(),
|
|
265
|
+
]);
|
|
266
|
+
// `harness verify --json` (SH G2.4) emits `{status, envelope_hash?}` on
|
|
267
|
+
// stdout — a deterministic handle to THIS verify's `envelope` event even
|
|
268
|
+
// when parallel verifies make the ledger tail ambiguous.
|
|
269
|
+
if (code === 0) {
|
|
270
|
+
const hash = parseEnvelopeHash(stdout);
|
|
271
|
+
return hash ? { status: "pass", envelopeHash: hash } : { status: "pass" };
|
|
272
|
+
}
|
|
273
|
+
if (code === 2)
|
|
274
|
+
return { status: "halt", evidence: stderr || stdout };
|
|
275
|
+
return { status: "fail", evidence: stderr || stdout };
|
|
276
|
+
}
|
|
277
|
+
async verifySystem(unitId, systemToml, workspace) {
|
|
278
|
+
const { code, stdout, stderr } = await this.callRead([
|
|
279
|
+
"verify",
|
|
280
|
+
"--unit",
|
|
281
|
+
unitId,
|
|
282
|
+
"--system",
|
|
283
|
+
workspacePath(systemToml, workspace),
|
|
284
|
+
...this.base(),
|
|
285
|
+
]);
|
|
286
|
+
if (code === 0)
|
|
287
|
+
return { status: "pass" };
|
|
288
|
+
if (code === 2)
|
|
289
|
+
return { status: "halt", evidence: stderr || stdout };
|
|
290
|
+
return { status: "fail", evidence: stderr || stdout };
|
|
291
|
+
}
|
|
292
|
+
async authorizeMerge(unitId, facts, workspace) {
|
|
293
|
+
const { code, stdout, stderr } = await this.callRead([
|
|
294
|
+
"merge",
|
|
295
|
+
unitId,
|
|
296
|
+
"--check",
|
|
297
|
+
...(this.opts.systemToml ? ["--system", workspacePath(this.opts.systemToml, workspace)] : []),
|
|
298
|
+
...mergeFactFlags(facts),
|
|
299
|
+
...this.base(),
|
|
300
|
+
]);
|
|
301
|
+
if (code === 0)
|
|
302
|
+
return { status: "authorized" };
|
|
303
|
+
return blockedMerge(unitId, stderr || stdout);
|
|
304
|
+
}
|
|
305
|
+
async recordMerge(unitId, landedSha, facts, workspace) {
|
|
306
|
+
const { code, stdout, stderr } = await this.callRead([
|
|
307
|
+
"merge",
|
|
308
|
+
unitId,
|
|
309
|
+
"--landed",
|
|
310
|
+
landedSha,
|
|
311
|
+
...(this.opts.systemToml ? ["--system", workspacePath(this.opts.systemToml, workspace)] : []),
|
|
312
|
+
...mergeFactFlags(facts),
|
|
313
|
+
...this.base(),
|
|
314
|
+
]);
|
|
315
|
+
if (code === 0)
|
|
316
|
+
return { status: "merged", landedSha };
|
|
317
|
+
return blockedMerge(unitId, stderr || stdout);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
function mergeFactFlags(facts) {
|
|
321
|
+
const flags = [];
|
|
322
|
+
if (facts.door === "one-way")
|
|
323
|
+
flags.push("--one-way");
|
|
324
|
+
if (facts.door === "two-way")
|
|
325
|
+
flags.push("--two-way");
|
|
326
|
+
if (facts.addsDependency)
|
|
327
|
+
flags.push("--adds-dependency");
|
|
328
|
+
if (facts.schemaChange)
|
|
329
|
+
flags.push("--schema-change");
|
|
330
|
+
if (facts.security)
|
|
331
|
+
flags.push("--security");
|
|
332
|
+
if (facts.gateExternal)
|
|
333
|
+
flags.push("--gate-external");
|
|
334
|
+
if (facts.artifactProducerChange)
|
|
335
|
+
flags.push("--artifact-producer-change");
|
|
336
|
+
if (facts.mode)
|
|
337
|
+
flags.push("--mode", facts.mode);
|
|
338
|
+
return flags;
|
|
339
|
+
}
|
|
340
|
+
function blockedMerge(unitId, output) {
|
|
341
|
+
const blurb = output.trim();
|
|
342
|
+
return {
|
|
343
|
+
status: "door-open",
|
|
344
|
+
doorId: parseDoorId(blurb) ?? `${unitId}#door`,
|
|
345
|
+
unblock: blurb || `harness door approve ${unitId} --reason "<why>" --who <operator>`,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
function workspacePath(path, workspace) {
|
|
349
|
+
if (!workspace)
|
|
350
|
+
return path;
|
|
351
|
+
const rel = relative(workspace.controlRepo, resolve(path));
|
|
352
|
+
if (rel.startsWith("..") || isAbsolute(rel))
|
|
353
|
+
return path;
|
|
354
|
+
return resolve(workspace.path, rel);
|
|
355
|
+
}
|
|
356
|
+
/** `{"status":"pass","envelope_hash":"<hex>"}` → the hash, or undefined. */
|
|
357
|
+
export function parseEnvelopeHash(stdout) {
|
|
358
|
+
try {
|
|
359
|
+
const j = JSON.parse(stdout.trim());
|
|
360
|
+
return typeof j.envelope_hash === "string" && j.envelope_hash
|
|
361
|
+
? j.envelope_hash
|
|
362
|
+
: undefined;
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
return undefined;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Pull the door id out of `harness merge`'s blocked-merge stderr, e.g.
|
|
370
|
+
* `⛔ merge blocked — door door-core-a-schema opened. Decide with: …`
|
|
371
|
+
* `⛔ merge blocked — door door-core-a-schema was REJECTED: …`
|
|
372
|
+
* `⛔ merge blocked — door door-core-a-schema awaits a decision`
|
|
373
|
+
*/
|
|
374
|
+
export function parseDoorId(stderr) {
|
|
375
|
+
const m = stderr.match(/door\s+(\S+?)\s+(?:opened|was REJECTED|awaits)\b/);
|
|
376
|
+
return m ? m[1] : undefined;
|
|
377
|
+
}
|
|
378
|
+
//# sourceMappingURL=cli.js.map
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* harness — the typed wrapper over the `@harness/system` CLI surface.
|
|
3
|
+
*
|
|
4
|
+
* This product reimplements NONE of the pipeline it drives. Every method here
|
|
5
|
+
* is a call to a `harness` subcommand: verify (rings 1 + 2), door classification
|
|
6
|
+
* (folded into `merge`), the `unit.merged` fact, provenance appends. The loop
|
|
7
|
+
* talks only to this interface — never to `child_process` directly, never to a
|
|
8
|
+
* system-harness source import (portfolio rule 1).
|
|
9
|
+
*
|
|
10
|
+
* Two implementations ship in v0:
|
|
11
|
+
* - `FakeHarness` (this file) — an in-memory unit table the dry loop (M1) and
|
|
12
|
+
* the test suite drive against. No process, no ledger, no git.
|
|
13
|
+
* - `CliHarness` (./cli.ts) — shells the real `harness` binary, parsing the
|
|
14
|
+
* `--json` surface from SH P7.4 (`next` / `claim` / `escalated`) plus the
|
|
15
|
+
* separate pre-land merge check and post-land merge record calls. The v0
|
|
16
|
+
* text-parsing shim it replaced is gone.
|
|
17
|
+
*/
|
|
18
|
+
export class FakeHarness {
|
|
19
|
+
table = new Map();
|
|
20
|
+
appends = [];
|
|
21
|
+
heartbeats = [];
|
|
22
|
+
constructor(specs = []) {
|
|
23
|
+
for (const s of specs)
|
|
24
|
+
this.add(s);
|
|
25
|
+
}
|
|
26
|
+
add(s) {
|
|
27
|
+
const u = {
|
|
28
|
+
id: s.id,
|
|
29
|
+
deps: s.deps ?? [],
|
|
30
|
+
mode: s.mode ?? "build",
|
|
31
|
+
intent: s.intent ?? `implement ${s.id} to its acceptance criteria`,
|
|
32
|
+
acceptanceCriteria: s.acceptanceCriteria ?? [
|
|
33
|
+
{ statement: `${s.id} MUST satisfy its intent`, verifier: "test" },
|
|
34
|
+
],
|
|
35
|
+
scope: s.scope ?? [`src/${s.id}/**`],
|
|
36
|
+
mergeFacts: s.mergeFacts ?? { door: "two-way" },
|
|
37
|
+
state: s.state ?? "planned",
|
|
38
|
+
};
|
|
39
|
+
if (s.component !== undefined)
|
|
40
|
+
u.component = s.component;
|
|
41
|
+
if (s.escalated !== undefined)
|
|
42
|
+
u.escalated = s.escalated;
|
|
43
|
+
if (s.landedSha !== undefined)
|
|
44
|
+
u.landedSha = s.landedSha;
|
|
45
|
+
if (s.stale !== undefined)
|
|
46
|
+
u.stale = s.stale;
|
|
47
|
+
if (s.verify !== undefined)
|
|
48
|
+
u.verify = s.verify;
|
|
49
|
+
if (s.verifySystem !== undefined)
|
|
50
|
+
u.verifySystem = s.verifySystem;
|
|
51
|
+
this.table.set(s.id, u);
|
|
52
|
+
}
|
|
53
|
+
ready(u) {
|
|
54
|
+
if (u.escalated)
|
|
55
|
+
return false;
|
|
56
|
+
// planned, or executing-but-stale (the sequencer re-offers a dead session's unit).
|
|
57
|
+
if (u.state !== "planned" && !(u.state === "executing" && u.stale))
|
|
58
|
+
return false;
|
|
59
|
+
return u.deps.every((d) => this.table.get(bareId(d))?.state === "merged");
|
|
60
|
+
}
|
|
61
|
+
toReadyUnit(u) {
|
|
62
|
+
const r = { id: u.id, deps: u.deps, mode: u.mode };
|
|
63
|
+
if (u.component !== undefined)
|
|
64
|
+
r.component = u.component;
|
|
65
|
+
return r;
|
|
66
|
+
}
|
|
67
|
+
async next(opts) {
|
|
68
|
+
return [...this.table.values()]
|
|
69
|
+
.filter((u) => this.ready(u))
|
|
70
|
+
.filter((u) => !opts?.only || u.id.startsWith(opts.only))
|
|
71
|
+
.sort((a, b) => a.id.localeCompare(b.id))
|
|
72
|
+
.map((u) => this.toReadyUnit(u));
|
|
73
|
+
}
|
|
74
|
+
async escalated() {
|
|
75
|
+
return [...this.table.values()]
|
|
76
|
+
.filter((u) => u.escalated)
|
|
77
|
+
.sort((a, b) => a.id.localeCompare(b.id))
|
|
78
|
+
.map((u) => this.toReadyUnit(u));
|
|
79
|
+
}
|
|
80
|
+
async units() {
|
|
81
|
+
return [...this.table.values()]
|
|
82
|
+
.sort((a, b) => a.id.localeCompare(b.id))
|
|
83
|
+
.map((u) => {
|
|
84
|
+
const snap = { id: u.id, state: u.state };
|
|
85
|
+
if (u.component !== undefined)
|
|
86
|
+
snap.component = u.component;
|
|
87
|
+
if (u.landedSha !== undefined)
|
|
88
|
+
snap.landedSha = u.landedSha;
|
|
89
|
+
return snap;
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
async claim(unitId, session) {
|
|
93
|
+
const u = this.table.get(unitId);
|
|
94
|
+
if (!u || !this.ready(u))
|
|
95
|
+
return null;
|
|
96
|
+
u.state = "executing";
|
|
97
|
+
u.claimedBy = session;
|
|
98
|
+
delete u.stale; // a fresh claim takes ownership from the dead session
|
|
99
|
+
return this.briefing(u);
|
|
100
|
+
}
|
|
101
|
+
async resumeAfterDoor(unitId, session) {
|
|
102
|
+
const u = this.table.get(unitId);
|
|
103
|
+
if (!u || u.state !== "in-review")
|
|
104
|
+
return null;
|
|
105
|
+
u.state = "executing";
|
|
106
|
+
u.claimedBy = session;
|
|
107
|
+
return this.briefing(u);
|
|
108
|
+
}
|
|
109
|
+
briefing(u) {
|
|
110
|
+
const b = {
|
|
111
|
+
unitId: u.id,
|
|
112
|
+
intent: u.intent,
|
|
113
|
+
acceptanceCriteria: u.acceptanceCriteria,
|
|
114
|
+
scope: u.scope,
|
|
115
|
+
branch: `unit/${u.id}`,
|
|
116
|
+
mergeFacts: u.mergeFacts,
|
|
117
|
+
};
|
|
118
|
+
if (u.component !== undefined)
|
|
119
|
+
b.component = u.component;
|
|
120
|
+
return b;
|
|
121
|
+
}
|
|
122
|
+
async heartbeat(unitId, session) {
|
|
123
|
+
this.heartbeats.push({ unitId, session });
|
|
124
|
+
}
|
|
125
|
+
async ledgerAppend(event) {
|
|
126
|
+
this.appends.push(event);
|
|
127
|
+
if (event.type === "unit.done") {
|
|
128
|
+
const u = this.table.get(event.unitId);
|
|
129
|
+
// in-review → in-review is a documented no-op in the fold (M3).
|
|
130
|
+
if (u && u.state === "executing")
|
|
131
|
+
u.state = "in-review";
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
async verify(briefing, _session, _baseRef) {
|
|
135
|
+
return (this.table.get(briefing.unitId)?.verify ?? {
|
|
136
|
+
status: "pass",
|
|
137
|
+
// a deterministic stand-in for the ring-1 `envelope` event hash (G4).
|
|
138
|
+
envelopeHash: `env-${briefing.unitId}`,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
async verifySystem(unitId, _systemToml) {
|
|
142
|
+
return this.table.get(unitId)?.verifySystem ?? { status: "pass" };
|
|
143
|
+
}
|
|
144
|
+
async authorizeMerge(unitId, facts) {
|
|
145
|
+
const u = this.table.get(unitId);
|
|
146
|
+
if (!u)
|
|
147
|
+
return {
|
|
148
|
+
status: "door-open",
|
|
149
|
+
doorId: `${unitId}#missing`,
|
|
150
|
+
unblock: "unit not found",
|
|
151
|
+
};
|
|
152
|
+
// classifyMerge: an absent door is one-way (safety default — v0-plan calibration note).
|
|
153
|
+
const oneWay = facts.door === "one-way" ||
|
|
154
|
+
facts.door === undefined ||
|
|
155
|
+
facts.schemaChange === true ||
|
|
156
|
+
facts.security === true;
|
|
157
|
+
if (oneWay) {
|
|
158
|
+
return {
|
|
159
|
+
status: "door-open",
|
|
160
|
+
doorId: `${unitId}#door`,
|
|
161
|
+
unblock: `harness door approve ${unitId} --reason "<why>" --who <operator>`,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
return { status: "authorized" };
|
|
165
|
+
}
|
|
166
|
+
async recordMerge(unitId, landedSha, _facts) {
|
|
167
|
+
const u = this.table.get(unitId);
|
|
168
|
+
if (!u)
|
|
169
|
+
return {
|
|
170
|
+
status: "door-open",
|
|
171
|
+
doorId: `${unitId}#missing`,
|
|
172
|
+
unblock: "unit not found",
|
|
173
|
+
};
|
|
174
|
+
u.state = "merged";
|
|
175
|
+
u.landedSha = landedSha;
|
|
176
|
+
return { status: "merged", landedSha };
|
|
177
|
+
}
|
|
178
|
+
/** test helper: current state of a unit. */
|
|
179
|
+
stateOf(unitId) {
|
|
180
|
+
return this.table.get(unitId)?.state;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
export function bareId(ref) {
|
|
184
|
+
return ref.includes("#") ? ref.slice(ref.indexOf("#") + 1) : ref;
|
|
185
|
+
}
|
|
186
|
+
//# sourceMappingURL=index.js.map
|