@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,159 @@
1
+ /**
2
+ * system — Contract C load + ring 2 (`harness verify --system`).
3
+ *
4
+ * Pure data over the ledger + system.toml — never compiles, sub-second:
5
+ * - the component DAG is acyclic
6
+ * - every artifact a component's latest envelope CONSUMED still matches that
7
+ * artifact's latest PRODUCED hash (a stale hash is a fail-closed halt, not
8
+ * a warning)
9
+ * - no merged unit's component crosses a boundary (its envelope's
10
+ * component_id is a declared component)
11
+ *
12
+ * A failure appends `system.halted {cause}`; a clean pass appends
13
+ * `system.verified`.
14
+ */
15
+ import { readFileSync } from "node:fs";
16
+ import { parse as parseToml } from "smol-toml";
17
+ import { append, read as ledgerRead, } from "../ledger/index.js";
18
+ export class SystemError extends Error {
19
+ name = "SystemError";
20
+ }
21
+ export function loadSystem(path) {
22
+ const raw = parseToml(readFileSync(path, "utf8"));
23
+ if (!raw.system?.id)
24
+ throw new SystemError(`${path}: [system].id is required`);
25
+ const component = raw.component ?? [];
26
+ if (component.length === 0)
27
+ throw new SystemError(`${path}: at least one [[component]] is required`);
28
+ for (const c of component) {
29
+ if (!c.id || !c.profile)
30
+ throw new SystemError(`${path}: every component needs id + profile`);
31
+ if (!c.root && !c.repo)
32
+ throw new SystemError(`component "${c.id}": needs root or repo`);
33
+ }
34
+ return {
35
+ system: raw.system,
36
+ component,
37
+ artifact: raw.artifact ?? [],
38
+ door_class: raw.door_class ?? [],
39
+ scenario_ownership: raw.scenario_ownership ?? [],
40
+ };
41
+ }
42
+ function findCycle(components) {
43
+ const deps = new Map(components.map((c) => [c.id, c.deps ?? []]));
44
+ const WHITE = 0, GREY = 1, BLACK = 2;
45
+ const color = new Map();
46
+ const walk = (n) => {
47
+ color.set(n, GREY);
48
+ for (const d of deps.get(n) ?? []) {
49
+ const c = color.get(d) ?? WHITE;
50
+ if (c === GREY)
51
+ return true;
52
+ if (c === WHITE && deps.has(d) && walk(d))
53
+ return true;
54
+ }
55
+ color.set(n, BLACK);
56
+ return false;
57
+ };
58
+ for (const c of components)
59
+ if ((color.get(c.id) ?? WHITE) === WHITE && walk(c.id))
60
+ return c.id;
61
+ return null;
62
+ }
63
+ /**
64
+ * A bare `harness verify` (no `--unit`) stamps `unit_id` as `<cwd-basename>#adhoc`
65
+ * and `component_id` as that same basename. Such runs are out-of-band operator
66
+ * checks with no DAG standing — they must not drive ring-2's scope / freshness
67
+ * invariants, or an ad-hoc verify from a repo root or a worktree permanently
68
+ * halts every real unit with a phantom `scope_cross`.
69
+ */
70
+ function isAdhoc(b) {
71
+ return typeof b.unit_id === "string" && b.unit_id.endsWith("#adhoc");
72
+ }
73
+ /** Latest produced hash per artifact name, across all passing envelope events, in ledger order. */
74
+ function latestProduced(events) {
75
+ const latest = new Map();
76
+ for (const e of events) {
77
+ if (e.type !== "envelope")
78
+ continue;
79
+ const b = e.body;
80
+ if (isAdhoc(b))
81
+ continue;
82
+ for (const p of b.artifacts?.produced ?? [])
83
+ latest.set(p.name, p.hash);
84
+ }
85
+ return latest;
86
+ }
87
+ /** Latest envelope per component. */
88
+ function latestEnvelopePerComponent(events) {
89
+ const latest = new Map();
90
+ for (const e of events) {
91
+ if (e.type !== "envelope")
92
+ continue;
93
+ const b = e.body;
94
+ if (isAdhoc(b))
95
+ continue;
96
+ if (b.component_id)
97
+ latest.set(b.component_id, b);
98
+ }
99
+ return latest;
100
+ }
101
+ /** Ring 2. */
102
+ export function verifySystem(manifest, events) {
103
+ const halts = [];
104
+ const componentIds = new Set(manifest.component.map((c) => c.id));
105
+ const cycle = findCycle(manifest.component);
106
+ if (cycle)
107
+ halts.push({
108
+ cause: "dag_cycle",
109
+ detail: `cycle through component "${cycle}"`,
110
+ });
111
+ const produced = latestProduced(events);
112
+ for (const [componentId, env] of latestEnvelopePerComponent(events)) {
113
+ if (!componentIds.has(componentId)) {
114
+ halts.push({
115
+ cause: "scope_cross",
116
+ detail: `envelope names component "${componentId}" not in system.toml`,
117
+ });
118
+ }
119
+ for (const c of env.artifacts?.consumed ?? []) {
120
+ const current = produced.get(c.name);
121
+ if (current !== undefined && current !== c.hash) {
122
+ halts.push({
123
+ cause: "stale_artifact",
124
+ detail: `component "${componentId}" built against ${c.name}@${c.hash.slice(0, 8)} but the latest is @${current.slice(0, 8)}`,
125
+ });
126
+ }
127
+ }
128
+ }
129
+ return { ok: halts.length === 0, halts };
130
+ }
131
+ /** Run ring 2 and record the outcome on the ledger. */
132
+ export function runSystemVerify(opts) {
133
+ const manifest = loadSystem(opts.systemTomlPath);
134
+ const events = ledgerRead(opts.ledgerDir);
135
+ const result = verifySystem(manifest, events);
136
+ const ts = opts.now ?? new Date().toISOString();
137
+ if (result.ok) {
138
+ append(opts.ledgerDir, {
139
+ system: manifest.system.id,
140
+ actor: { kind: "harness", id: "harness" },
141
+ type: "system.verified",
142
+ ts,
143
+ body: { system: manifest.system.id },
144
+ });
145
+ }
146
+ else {
147
+ for (const h of result.halts) {
148
+ append(opts.ledgerDir, {
149
+ system: manifest.system.id,
150
+ actor: { kind: "harness", id: "harness" },
151
+ type: "system.halted",
152
+ ts,
153
+ body: { cause: h.cause, detail: h.detail },
154
+ });
155
+ }
156
+ }
157
+ return result;
158
+ }
159
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,87 @@
1
+ const TRANSITION = {
2
+ "unit.planned": "planned",
3
+ "unit.ready": "ready",
4
+ "unit.claimed": "executing",
5
+ "unit.done": "in-review",
6
+ "unit.merged": "merged",
7
+ "unit.aborted": "aborted",
8
+ };
9
+ function bareId(ref) {
10
+ return ref.includes("#") ? ref.slice(ref.indexOf("#") + 1) : ref;
11
+ }
12
+ /** Fold `unit.*` (and `session.heartbeat`) events into current per-unit state. */
13
+ export function projectUnits(events) {
14
+ const units = new Map();
15
+ for (const e of [...events].sort((a, b) => a.seq - b.seq)) {
16
+ if (e.type === "session.heartbeat" || e.type === "session.escalated") {
17
+ const uid = bareId(String(e.body.unit_id ?? ""));
18
+ const rec = units.get(uid);
19
+ if (!rec)
20
+ continue;
21
+ if (e.type === "session.heartbeat" && rec.state === "executing") {
22
+ rec.heartbeatAt = e.ts;
23
+ rec.lastSeq = e.seq;
24
+ }
25
+ else if (e.type === "session.escalated" &&
26
+ (rec.state === "executing" || rec.state === "in-review")) {
27
+ rec.escalated = true;
28
+ rec.lastSeq = e.seq;
29
+ }
30
+ continue;
31
+ }
32
+ const nextState = TRANSITION[e.type];
33
+ if (!nextState)
34
+ continue;
35
+ const body = e.body;
36
+ const id = bareId(String(body.unit_id ?? ""));
37
+ if (id === "")
38
+ continue;
39
+ const rec = units.get(id) ?? {
40
+ id,
41
+ deps: [],
42
+ state: "planned",
43
+ lastSeq: e.seq,
44
+ };
45
+ rec.state = nextState;
46
+ rec.lastSeq = e.seq;
47
+ if (body.component !== undefined)
48
+ rec.component = body.component;
49
+ if (body.mode !== undefined)
50
+ rec.mode = body.mode;
51
+ if (Array.isArray(body.deps))
52
+ rec.deps = body.deps.map(bareId);
53
+ if (nextState === "executing") {
54
+ const session = body.session ?? body.session_id ?? rec.claimedBy;
55
+ if (session !== undefined)
56
+ rec.claimedBy = session;
57
+ rec.heartbeatAt = e.ts;
58
+ delete rec.escalated; // a fresh claim (incl. --resume) clears a prior escalation
59
+ delete rec.doneAt;
60
+ delete rec.doneSeq;
61
+ }
62
+ else if (nextState === "in-review") {
63
+ rec.doneAt = e.ts;
64
+ rec.doneSeq = e.seq;
65
+ delete rec.claimedBy;
66
+ delete rec.heartbeatAt;
67
+ delete rec.escalated;
68
+ }
69
+ else {
70
+ delete rec.claimedBy;
71
+ delete rec.heartbeatAt;
72
+ delete rec.escalated;
73
+ delete rec.doneAt;
74
+ delete rec.doneSeq;
75
+ if (nextState === "merged" && typeof body.landed === "string") {
76
+ rec.landedSha = body.landed;
77
+ }
78
+ }
79
+ units.set(id, rec);
80
+ }
81
+ return units;
82
+ }
83
+ /** Convenience: the projected list, id-sorted. */
84
+ export function unitList(events) {
85
+ return [...projectUnits(events).values()].sort((a, b) => a.id.localeCompare(b.id));
86
+ }
87
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,103 @@
1
+ /**
2
+ * `harness verify` — ring 1 (default) or ring 2 (`--system <system.toml>`).
3
+ *
4
+ * harness verify [--component <dir>] [--profiles <dir>] [--component-id <id>]
5
+ * [--unit <id>] [--session <id>] [--feature <id>] [--system-id <id>]
6
+ * [--ledger <dir>] [--base <commit>] [--specs <dir>]
7
+ * [--produces name=path]... [--consumes name]...
8
+ *
9
+ * harness verify --system <system.toml> [--ledger <dir>] # ring 2
10
+ *
11
+ * --json emit `{status, envelope_hash?}` on stdout instead of the human
12
+ * report; exit codes are unchanged. `envelope_hash` is the `hash` of
13
+ * the `envelope` ledger event this run appended (ring 1 only) — a
14
+ * deterministic handle even when parallel verifies make the ledger
15
+ * tail ambiguous (G2.4).
16
+ *
17
+ * Ring 1 exit: 0 pass · 1 fail · 2 halt.
18
+ * Ring 2 exit: 0 verified · 2 halted (dag_cycle | stale_artifact | scope_cross).
19
+ */
20
+ import { basename } from "node:path";
21
+ import { runSystemVerify } from "../system/index.js";
22
+ import { verify } from "./index.js";
23
+ function flag(args, name) {
24
+ const i = args.indexOf(`--${name}`);
25
+ return i >= 0 && args[i + 1] ? args[i + 1] : undefined;
26
+ }
27
+ function multi(args, name) {
28
+ const out = [];
29
+ for (let i = 0; i < args.length; i++)
30
+ if (args[i] === `--${name}` && args[i + 1])
31
+ out.push(args[i + 1]);
32
+ return out;
33
+ }
34
+ export function verifyCommand(args) {
35
+ const json = args.includes("--json");
36
+ const systemToml = flag(args, "system");
37
+ if (systemToml && systemToml.endsWith(".toml")) {
38
+ const result = runSystemVerify({
39
+ systemTomlPath: systemToml,
40
+ ledgerDir: flag(args, "ledger") ?? ".harness/ledger",
41
+ });
42
+ if (result.ok) {
43
+ process.stdout.write(json
44
+ ? JSON.stringify({ status: "pass" }) + "\n"
45
+ : " ✓ verify --system: DAG acyclic, artifacts fresh, scope contained\n");
46
+ return 0;
47
+ }
48
+ if (json) {
49
+ process.stdout.write(JSON.stringify({ status: "halt", causes: result.halts.map((h) => h.cause) }) + "\n");
50
+ }
51
+ else {
52
+ for (const h of result.halts)
53
+ process.stderr.write(` ⛔ system.halted {${h.cause}}: ${h.detail}\n`);
54
+ }
55
+ return 2;
56
+ }
57
+ const componentDir = flag(args, "component") ?? process.cwd();
58
+ const componentId = flag(args, "component-id") ?? basename(componentDir);
59
+ const unitId = flag(args, "unit") ?? `${componentId}#adhoc`;
60
+ const produces = {};
61
+ for (const kv of multi(args, "produces")) {
62
+ const eq = kv.indexOf("=");
63
+ if (eq > 0)
64
+ produces[kv.slice(0, eq)] = kv.slice(eq + 1);
65
+ }
66
+ const consumes = multi(args, "consumes");
67
+ const result = verify({
68
+ componentDir,
69
+ componentId,
70
+ unitId,
71
+ ...(flag(args, "session") ? { sessionId: flag(args, "session") } : {}),
72
+ ...(flag(args, "ledger") ? { ledgerDir: flag(args, "ledger") } : {}),
73
+ ...(flag(args, "base") ? { baseRef: flag(args, "base") } : {}),
74
+ ...(flag(args, "specs") ? { specsDir: flag(args, "specs") } : {}),
75
+ ...(flag(args, "profiles") ? { profilesDir: flag(args, "profiles") } : {}),
76
+ ...(flag(args, "feature") ? { featureId: flag(args, "feature") } : {}),
77
+ ...(flag(args, "system-id") ? { system: flag(args, "system-id") } : {}),
78
+ ...(Object.keys(produces).length ? { produces } : {}),
79
+ ...(consumes.length ? { consumes } : {}),
80
+ agentIdentity: { kind: "human", id: "cli", attested: false },
81
+ });
82
+ const exit = result.verdict === "pass" ? 0 : result.verdict === "fail" ? 1 : 2;
83
+ if (json) {
84
+ process.stdout.write(JSON.stringify({
85
+ status: result.verdict,
86
+ envelope_hash: result.envelopeHash,
87
+ }) + "\n");
88
+ return exit;
89
+ }
90
+ const gates = result.envelope.gates;
91
+ for (const g of gates) {
92
+ const mark = g.outcome === "pass" ? "✓" : g.outcome === "declined" ? "·" : "✗";
93
+ const retry = g.retries > 0
94
+ ? ` (retries: ${g.retries} — pass-on-retry, not clean)`
95
+ : "";
96
+ process.stdout.write(` ${mark} ${g.name.padEnd(12)} ${String(g.duration_ms).padStart(6)}ms${retry}\n`);
97
+ }
98
+ process.stdout.write(`\n verdict: ${String(result.verdict).toUpperCase()}\n`);
99
+ process.stdout.write(` envelope: ${result.envelopePath}\n`);
100
+ process.stdout.write(` binding-hash: ${result.envelope.binding_hash.slice(0, 12)}…\n`);
101
+ return exit;
102
+ }
103
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Declined-gate reasons live in component.toml and are copied verbatim into
3
+ * every verify envelope. They must describe a standing property of the
4
+ * component (absent tooling, unconfigured substrate, out-of-scope concern) —
5
+ * never a specific change, unit, or diff. A false reason launders an unrun
6
+ * gate as a considered decision.
7
+ */
8
+ export class DeclinedReasonError extends Error {
9
+ name = "DeclinedReasonError";
10
+ }
11
+ /**
12
+ * Wording shapes frozen out by gate-declined-reasons-accurate. Anchored to
13
+ * the phrases that once justified skipping arch/contract/size for a long-past
14
+ * dependency refresh.
15
+ */
16
+ export const CHANGE_SPECIFIC_REASON_PATTERNS = [
17
+ /\bthe upgrade\b/i,
18
+ /\bthe dependency refresh\b/i,
19
+ /\bthis change\b/i,
20
+ /\bthe change\b/i,
21
+ /\bthis unit\b/i,
22
+ /\bthe unit's\b/i,
23
+ ];
24
+ /**
25
+ * The size gate measures diff magnitude (`size_budget`); scope is a different
26
+ * gate. Declining size because "scope bounds the change" conflates the two.
27
+ */
28
+ export const SIZE_SCOPE_CONFLATION_PATTERNS = [
29
+ /\bbounded by\b[\s\S]{0,40}\bscope\b/i,
30
+ /\bscope\b[\s\S]{0,40}\bbound/i,
31
+ /\bfile scope\b/i,
32
+ /\bscope is enforced\b/i,
33
+ /\benforced from\b[\s\S]{0,40}\bunit\b/i,
34
+ ];
35
+ export function matchingChangeSpecificPattern(reason) {
36
+ return CHANGE_SPECIFIC_REASON_PATTERNS.find((re) => re.test(reason));
37
+ }
38
+ export function matchingSizeScopeConflation(reason) {
39
+ return SIZE_SCOPE_CONFLATION_PATTERNS.find((re) => re.test(reason));
40
+ }
41
+ /**
42
+ * Fail closed when any declined reason is change-specific, or when `size` is
43
+ * declined on the grounds that scope bounds it.
44
+ */
45
+ export function assertStandingDeclinedReasons(declined) {
46
+ for (const { name, reason } of declined) {
47
+ const changeHit = matchingChangeSpecificPattern(reason);
48
+ if (changeHit) {
49
+ throw new DeclinedReasonError(`gate "${name}": declined reason must state a standing component property, ` +
50
+ `not a specific change/unit/diff (matched ${changeHit}): ${reason}`);
51
+ }
52
+ if (name === "size") {
53
+ const sizeHit = matchingSizeScopeConflation(reason);
54
+ if (sizeHit) {
55
+ throw new DeclinedReasonError(`gate "size": declined reason must not conflate size with scope ` +
56
+ `(matched ${sizeHit}); bind size_budget or give a standing size reason: ${reason}`);
57
+ }
58
+ }
59
+ }
60
+ }
61
+ //# sourceMappingURL=declined-reasons.js.map
@@ -0,0 +1,188 @@
1
+ /**
2
+ * verify — ring 1. Orchestrates: resolve the manifest, run the bound gates at
3
+ * the component root, mint a Contract B envelope, sign it with the harness
4
+ * binary key, validate it against schema/envelope.schema.json, write it, and
5
+ * append an `envelope` event to the ledger.
6
+ *
7
+ * An agent cannot fake a green result: the signature is over the canonical
8
+ * envelope body and only the harness key produces it, and verify re-runs every
9
+ * gate rather than trusting any prior claim.
10
+ */
11
+ import { createHash } from "node:crypto";
12
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
13
+ import { join, basename, isAbsolute, resolve } from "node:path";
14
+ import { packageSchemaDir, targetOwnedPaths, targetPath } from "../assets.js";
15
+ import { Ajv2020 } from "ajv/dist/2020.js";
16
+ import addFormatsDefault from "ajv-formats";
17
+ // ajv-formats ships as CJS: the default import resolves to module.exports, which
18
+ // is the plugin function at runtime. NodeNext's static view disagrees — assert it.
19
+ const addFormats = addFormatsDefault;
20
+ import { append as ledgerAppend, read as ledgerRead } from "../ledger/index.js";
21
+ import { loadManifest, loadProfile, resolveManifest, } from "../manifest/index.js";
22
+ import { runGates } from "../runner/index.js";
23
+ import { assertStandingDeclinedReasons } from "./declined-reasons.js";
24
+ import { loadOrCreateKey, signCanonical } from "./signing.js";
25
+ import { attestScope } from "../agentops/scope.js";
26
+ export { assertStandingDeclinedReasons, CHANGE_SPECIFIC_REASON_PATTERNS, DeclinedReasonError, matchingChangeSpecificPattern, matchingSizeScopeConflation, SIZE_SCOPE_CONFLATION_PATTERNS, } from "./declined-reasons.js";
27
+ const HARNESS_VERSION = "0.1.0";
28
+ const SCHEMA_ID = "https://harness.local/schema/envelope.schema.json";
29
+ function buildAjv() {
30
+ const ajv = new Ajv2020({ allErrors: true, strict: false });
31
+ addFormats(ajv);
32
+ for (const f of ["envelope.schema.json"]) {
33
+ ajv.addSchema(JSON.parse(readFileSync(join(packageSchemaDir(), f), "utf8")));
34
+ }
35
+ return ajv;
36
+ }
37
+ /** Hash produced files; look up the latest produced hash for consumed artifacts. */
38
+ function buildArtifacts(componentDir, ledgerDir, produces, consumes) {
39
+ const out = {};
40
+ if (produces && Object.keys(produces).length) {
41
+ out.produced = Object.entries(produces).map(([name, path]) => {
42
+ const full = isAbsolute(path) ? path : join(componentDir, path);
43
+ const hash = createHash("sha256")
44
+ .update(readFileSync(full))
45
+ .digest("hex");
46
+ return { name, hash, path };
47
+ });
48
+ }
49
+ if (consumes && consumes.length) {
50
+ const latest = new Map();
51
+ if (existsSync(ledgerDir)) {
52
+ for (const e of ledgerRead(ledgerDir)) {
53
+ if (e.type !== "envelope")
54
+ continue;
55
+ const b = e.body;
56
+ for (const p of b.artifacts?.produced ?? [])
57
+ latest.set(p.name, p.hash);
58
+ }
59
+ }
60
+ out.consumed = consumes.map((name) => ({
61
+ name,
62
+ hash: latest.get(name) ?? "unresolved",
63
+ }));
64
+ }
65
+ return out.produced || out.consumed ? out : undefined;
66
+ }
67
+ /**
68
+ * pass/fail policy shared by every ring: all-skip is never a pass; only
69
+ * blocking/changed-lines failures fail the verdict (advisory never does).
70
+ * `harness gate --pre` (ring 0) reuses this so it can never disagree with
71
+ * ring 1 about what a "failing" gate means.
72
+ */
73
+ export function verdictOf(gates) {
74
+ if (gates.length === 0)
75
+ return "halt"; // all-skip is never a pass
76
+ const failed = gates.some((g) => g.outcome === "fail" &&
77
+ (g.severity === "blocking" || g.severity === "changed-lines"));
78
+ return failed ? "fail" : "pass";
79
+ }
80
+ export function verify(opts) {
81
+ const componentDir = resolve(opts.componentDir ?? process.cwd());
82
+ const owned = targetOwnedPaths(componentDir);
83
+ const profilesDir = opts.profilesDir ?? targetPath(componentDir, "profiles");
84
+ const componentId = opts.componentId ?? basename(componentDir);
85
+ const keyDir = opts.keyDir ?? owned.harnessDir;
86
+ const ledgerDir = opts.ledgerDir ?? owned.ledgerDir;
87
+ const startedAt = opts.now ?? new Date().toISOString();
88
+ const identity = opts.agentIdentity ?? {
89
+ kind: "ci",
90
+ id: "harness",
91
+ attested: false,
92
+ };
93
+ const manifest = loadManifest(owned.componentToml);
94
+ const profile = loadProfile(join(profilesDir, manifest.harness.profile, "bindings.toml"));
95
+ const resolved = resolveManifest(manifest, profile, { context: "verify" });
96
+ assertStandingDeclinedReasons(resolved.declined);
97
+ const t0 = Date.now();
98
+ const gateRuns = runGates(resolved.bound, { cwd: componentDir });
99
+ const gateVerdict = verdictOf(gateRuns);
100
+ const scope = attestScope({
101
+ componentDir,
102
+ unitId: opts.unitId,
103
+ ...(opts.baseRef ? { baseRef: opts.baseRef } : {}),
104
+ ...(opts.specsDir ? { specsDir: opts.specsDir } : {}),
105
+ });
106
+ // Scope attestation HALTs verify only when it was actually attempted — a unit
107
+ // context was supplied via --base + --specs — and failed. A contextless run
108
+ // (the CI dogfood, ad-hoc `harness verify`) records the scope gate as declined
109
+ // and takes its verdict from the bound gates. The "an unattested unit may not
110
+ // merge" rule is enforced in mergeGate, not here.
111
+ const verdict = scope.attempted && !scope.attested ? "halt" : gateVerdict;
112
+ const artifacts = buildArtifacts(componentDir, ledgerDir, opts.produces, opts.consumes);
113
+ const envelope = {
114
+ envelope_version: "2.0",
115
+ harness_version: HARNESS_VERSION,
116
+ policy_version: resolved.policyVersion,
117
+ profile_version: profile.profileVersion,
118
+ binding_hash: resolved.bindingHash,
119
+ component_id: componentId,
120
+ feature_id: opts.featureId ?? null,
121
+ session_id: opts.sessionId ?? null,
122
+ unit_id: opts.unitId,
123
+ spec_hash: opts.specHash ?? "0".repeat(8),
124
+ agent_identity: identity,
125
+ started_at: startedAt,
126
+ duration_ms: Date.now() - t0,
127
+ gates: [
128
+ ...gateRuns.map((g) => ({
129
+ name: g.name,
130
+ outcome: g.outcome,
131
+ severity: g.severity,
132
+ duration_ms: g.durationMs,
133
+ retries: g.retries,
134
+ })),
135
+ {
136
+ name: "scope",
137
+ outcome: scope.attested ? "pass" : scope.attempted ? "fail" : "declined",
138
+ severity: scope.attempted ? "blocking" : "advisory",
139
+ duration_ms: 0,
140
+ retries: 0,
141
+ ...(scope.attempted
142
+ ? {}
143
+ : { declined_reason: scope.reason ?? "scope was not attested" }),
144
+ },
145
+ ...resolved.declined.map((d) => ({
146
+ name: d.name,
147
+ outcome: "declined",
148
+ severity: "advisory",
149
+ duration_ms: 0,
150
+ retries: 0,
151
+ declined_reason: d.reason,
152
+ })),
153
+ ],
154
+ ...(artifacts ? { artifacts } : {}),
155
+ scope_attested: scope.attested,
156
+ scope_evidence: {
157
+ base_ref: opts.baseRef ?? null,
158
+ changed_files: scope.changedFiles,
159
+ effective_scope: scope.effectiveScope,
160
+ violations: scope.violations,
161
+ reason: scope.reason ?? null,
162
+ },
163
+ verdict,
164
+ sarif_path: `.harness/reports/${startedAt.replace(/[:.]/g, "-")}_${opts.unitId.replace(/[#/]/g, "-")}.sarif`,
165
+ };
166
+ const { privateKey } = loadOrCreateKey(keyDir);
167
+ envelope.signature = signCanonical(envelope, privateKey);
168
+ const ajv = buildAjv();
169
+ const validate = ajv.getSchema(SCHEMA_ID);
170
+ if (!validate(envelope)) {
171
+ throw new Error("minted envelope failed schema validation:\n" +
172
+ ajv.errorsText(validate.errors, { separator: "\n" }));
173
+ }
174
+ const reportsDir = targetPath(componentDir, ".harness", "reports");
175
+ mkdirSync(reportsDir, { recursive: true });
176
+ const envelopePath = join(reportsDir, `${startedAt.replace(/[:.]/g, "-")}_${opts.unitId.replace(/[#/]/g, "-")}.json`);
177
+ writeFileSync(envelopePath, JSON.stringify(envelope, null, 2) + "\n");
178
+ const appended = ledgerAppend(ledgerDir, {
179
+ system: opts.system ?? componentId,
180
+ actor: { kind: "harness", id: `harness@${HARNESS_VERSION}` },
181
+ type: "envelope",
182
+ body: envelope,
183
+ sig: envelope.signature,
184
+ ts: startedAt,
185
+ });
186
+ return { verdict, envelope, envelopePath, envelopeHash: appended.hash };
187
+ }
188
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,79 @@
1
+ /**
2
+ * signing — the harness binary key. Ed25519, detached signatures over the
3
+ * canonical envelope body (every field except `signature`).
4
+ *
5
+ * v1 key management: one keypair per component, generated on first `verify` and
6
+ * stored at <keyDir>/signing-key.pem (private, gitignored) + signing-key.pub.
7
+ * Multi-operator key distribution is a deferred decision (solution design §13).
8
+ */
9
+ import { generateKeyPairSync, sign, verify as cryptoVerify, createPrivateKey, createPublicKey, } from "node:crypto";
10
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync, } from "node:fs";
11
+ import { join } from "node:path";
12
+ const PRIV = "signing-key.pem";
13
+ const PUB = "signing-key.pub";
14
+ /** True iff an operator key already exists (door approval requires one). */
15
+ export function operatorKeyExists(keyDir) {
16
+ return existsSync(join(keyDir, "operator-key.pem"));
17
+ }
18
+ /**
19
+ * Read-only: the operator's public key, or undefined if none has been
20
+ * initialized at `keyDir` yet. Never creates one — used by every reader that
21
+ * needs to verify a `door.decided` signature (mergeGate, door cli, metrics).
22
+ */
23
+ export function loadOperatorPublicKey(keyDir) {
24
+ const pubPath = join(keyDir, "operator-key.pub");
25
+ if (!existsSync(pubPath))
26
+ return undefined;
27
+ return createPublicKey(readFileSync(pubPath, "utf8"));
28
+ }
29
+ /** Deterministic serialization: sorted keys, no whitespace, undefined dropped. */
30
+ export function canonicalJSON(value) {
31
+ if (value === null || typeof value !== "object")
32
+ return JSON.stringify(value) ?? "null";
33
+ if (Array.isArray(value))
34
+ return "[" + value.map(canonicalJSON).join(",") + "]";
35
+ const obj = value;
36
+ const keys = Object.keys(obj)
37
+ .filter((k) => obj[k] !== undefined)
38
+ .sort();
39
+ return ("{" +
40
+ keys.map((k) => JSON.stringify(k) + ":" + canonicalJSON(obj[k])).join(",") +
41
+ "}");
42
+ }
43
+ export function loadOrCreateKey(keyDir, name = "signing-key", opts = {}) {
44
+ const create = opts.create ?? true;
45
+ const privPath = join(keyDir, name === "signing-key" ? PRIV : `${name}.pem`);
46
+ const pubPath = join(keyDir, name === "signing-key" ? PUB : `${name}.pub`);
47
+ if (existsSync(privPath)) {
48
+ const privateKey = createPrivateKey(readFileSync(privPath, "utf8"));
49
+ const publicKey = existsSync(pubPath)
50
+ ? createPublicKey(readFileSync(pubPath, "utf8"))
51
+ : createPublicKey(privateKey);
52
+ return { privateKey, publicKey };
53
+ }
54
+ if (!create) {
55
+ throw new Error(`no key at ${privPath} — it must be created explicitly, not auto-provisioned`);
56
+ }
57
+ mkdirSync(keyDir, { recursive: true });
58
+ const { privateKey, publicKey } = generateKeyPairSync("ed25519");
59
+ writeFileSync(privPath, privateKey.export({ type: "pkcs8", format: "pem" }));
60
+ writeFileSync(pubPath, publicKey.export({ type: "spki", format: "pem" }));
61
+ try {
62
+ chmodSync(privPath, 0o600);
63
+ }
64
+ catch {
65
+ /* best effort on platforms without POSIX modes */
66
+ }
67
+ return { privateKey, publicKey };
68
+ }
69
+ /** Sign the canonical form of `body` with `signature` removed. Returns base64. */
70
+ export function signCanonical(body, privateKey) {
71
+ const { signature: _drop, ...rest } = body;
72
+ return sign(null, Buffer.from(canonicalJSON(rest), "utf8"), privateKey).toString("base64");
73
+ }
74
+ /** Verify a base64 detached signature over the canonical body (minus `signature`). */
75
+ export function verifySignature(body, signature, publicKey) {
76
+ const { signature: _drop, ...rest } = body;
77
+ return cryptoVerify(null, Buffer.from(canonicalJSON(rest), "utf8"), publicKey, Buffer.from(signature, "base64"));
78
+ }
79
+ //# sourceMappingURL=signing.js.map