@mikenguyen69/harness 0.1.0-beta.2 → 0.1.0-beta.3

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/dist/cli.js CHANGED
@@ -8,19 +8,45 @@
8
8
  * hook-install policy lives here.
9
9
  */
10
10
  import { buildDispatchRequest, dispatch } from "./dispatch.js";
11
- import { USAGE } from "./help.js";
11
+ import { COMMAND_HELP, SESSION_HELP, SESSION_PHASES, USAGE, wantsHelp, } from "./help.js";
12
12
  import { resolveInternalClis } from "./paths.js";
13
13
  import { buildInternalArgv, resolveRoute } from "./routes.js";
14
14
  import { formatVersion } from "./version.js";
15
15
  export async function main(argv) {
16
- if (argv.includes("--help") || argv.includes("-h")) {
17
- process.stdout.write(USAGE);
18
- return 0;
19
- }
20
16
  if (argv.includes("--version") || argv.includes("-V")) {
21
17
  process.stdout.write(formatVersion());
22
18
  return 0;
23
19
  }
20
+ const first = argv[0];
21
+ const helpRequested = wantsHelp(argv);
22
+ // Bare `harness`, `harness --help`, `harness -h` — the operator-journey text.
23
+ // A help flag that is not the leading token belongs to a specific command.
24
+ if (first === undefined || first === "--help" || first === "-h") {
25
+ process.stdout.write(USAGE);
26
+ return 0;
27
+ }
28
+ // `harness session [<phase>]` — enumerate the phases, and classify an
29
+ // unrecognised one as an unknown *phase*, not an unknown command. Runs before
30
+ // the generic `<noun> --help` path so a bare `harness session` prints them too.
31
+ if (first === "session") {
32
+ const phase = argv[1];
33
+ if (phase === undefined || phase === "--help" || phase === "-h") {
34
+ process.stdout.write(SESSION_HELP + "\n");
35
+ return 0;
36
+ }
37
+ if (!SESSION_PHASES.includes(phase)) {
38
+ process.stderr.write(`harness session ${phase}: unknown phase — expected one of ${SESSION_PHASES.join(", ")}\n`);
39
+ return 2;
40
+ }
41
+ }
42
+ // `harness <noun> --help` / `harness <noun> -h` — that command's own usage.
43
+ if (helpRequested && (argv[1] === "--help" || argv[1] === "-h")) {
44
+ const synopsis = COMMAND_HELP[first];
45
+ if (synopsis !== undefined) {
46
+ process.stdout.write(synopsis + "\n");
47
+ return 0;
48
+ }
49
+ }
24
50
  const route = resolveRoute(argv);
25
51
  if (route === null) {
26
52
  process.stderr.write(argv.length === 0
@@ -28,10 +54,6 @@ export async function main(argv) {
28
54
  : `harness ${argv.join(" ")}: unknown command\nRun harness --help\n`);
29
55
  return argv.length === 0 ? 0 : 2;
30
56
  }
31
- if (route === "status-skeleton") {
32
- process.stderr.write("harness status: not implemented (skeleton)\n");
33
- return 1;
34
- }
35
57
  const clis = resolveInternalClis();
36
58
  const internalArgv = buildInternalArgv(route, argv);
37
59
  const request = buildDispatchRequest(route.product, internalArgv, {
package/dist/help.js CHANGED
@@ -5,7 +5,7 @@ Operator journey:
5
5
  harness doctor read-only preflight before a run
6
6
  harness run drive the spec DAG with an agent fleet
7
7
  harness resume continue after a signed door approval
8
- harness status project run and lane status from RunState
8
+ harness status read-only unit lanes from the ledger (+ run overlay)
9
9
  harness report run render the end-of-run HTML report
10
10
  harness report calibration aggregate verification envelopes (calibration input)
11
11
 
@@ -41,7 +41,86 @@ Integration (machine-facing, same binary):
41
41
 
42
42
  Usage:
43
43
  harness --help this text
44
+ harness <command> --help usage for one command
44
45
  harness --version facade version and internal build identity
45
46
  harness <command> [args…] dispatch to the owning internal CLI
46
47
  `;
48
+ /**
49
+ * The phases `harness session <phase>` accepts. The facade routes each to
50
+ * `agent session <phase>`; anything else is an unknown phase, not an unknown
51
+ * command.
52
+ */
53
+ export const SESSION_PHASES = [
54
+ "started",
55
+ "heartbeat",
56
+ "gate_fired",
57
+ "escalated",
58
+ "ended",
59
+ ];
60
+ export const SESSION_HELP = `harness session <phase> append session.<phase> evidence (event JSON on stdin)
61
+
62
+ phases: ${SESSION_PHASES.join(", ")}
63
+
64
+ e.g. harness session started < event.json`;
65
+ /**
66
+ * One synopsis per facade command noun, shown for `harness <noun> --help` /
67
+ * `harness <noun> -h` so subcommand help never falls through to the
68
+ * operator-journey text above. Keyed by the leading facade token.
69
+ */
70
+ export const COMMAND_HELP = {
71
+ init: `harness init scaffold component.toml, hooks, CI workflow, generated adapters
72
+ --audit read-only brownfield inventory (writes nothing)
73
+ --mode ratchet brownfield adoption against a frozen debt baseline`,
74
+ doctor: `harness doctor read-only preflight before a run
75
+ --repo <dir> --routes <routes.toml> [--spool <dir>] [--json]`,
76
+ run: `harness run drive the spec DAG with an agent fleet
77
+ --repo <dir> --config <orchestration.json> --routes <routes.toml> [--spool <dir>] [--report <html>]`,
78
+ resume: `harness resume continue a run after a signed door approval
79
+ --unit <id> [--repo <dir>] [--routes <routes.toml>]`,
80
+ status: `harness status read-only unit lanes from the ledger, with a run-status spool overlay
81
+ [--ledger <dir>] [--spool <dir>] [--json]`,
82
+ report: `harness report <run|calibration>
83
+ report run --state <state.json> end-of-run HTML report (orchestration)
84
+ report calibration aggregate verification envelopes (system)`,
85
+ verify: `harness verify run all bound gates (cheap first, fail fast) and emit the signed envelope`,
86
+ gate: `harness gate --pre --file <path> ring-0 cheap gates before tool use`,
87
+ spec: `harness spec <check|materialize|project>
88
+ check lint specs, dep DAG, consumes/produces [--specs <dir>] [--system <system.toml>]
89
+ materialize write idempotent unit.planned facts --specs <dir> --system <system.toml> [--ledger <dir>] [--json]
90
+ project emit unit YAML from OpenSpec + projection --change <dir> [--openspec <dir>] [--out <dir>] [--json]`,
91
+ next: `harness next list units ready to claim (deps merged, unclaimed or stale)
92
+ [--only <id-prefix>] [--all | --in-review | --merged | --escalated] [--json]`,
93
+ claim: `harness claim <unit-id> --session <id> atomically take a ready unit
94
+ [--specs <dir>] [--system <path>] [--ledger <dir>] [--json] [--resume | --resume-after-door]`,
95
+ heartbeat: `harness heartbeat <unit-id> --session <id> keep a claim alive [--ledger <dir>]`,
96
+ merge: `harness merge <unit-id> <--check | --landed <sha>> authorize or record a landed merge
97
+ [--system <system.toml>] [facts…]`,
98
+ door: `harness door <list|show|approve|reject> open-decision surface
99
+ list [--sweep]
100
+ show <id>
101
+ approve <id> --reason "<who>: <why>" [--who <w>]
102
+ reject <id> --reason <why>`,
103
+ ledger: `harness ledger <append|read|verify-chain> the hash-chained record plane [--dir <path>]
104
+ append full event JSON document on stdin
105
+ read print the chain
106
+ verify-chain check hash linkage`,
107
+ board: `harness board <build|serve> local live dashboard
108
+ build [--ledger <dir>] [--spool <dir>] [--out board.html]
109
+ serve [--root <dir>]… [--port 4317] [--refresh 2000]`,
110
+ attest: `harness attest write/verify provenance attestations for a merge
111
+ write --unit <id> --landed <sha> [--ledger <dir>] [--out <dir>]
112
+ verify --unit <id> [--ledger <dir>] [--dir <dir>]`,
113
+ calibrate: `harness calibrate calibration jobs`,
114
+ digest: `harness digest digest surfaces`,
115
+ route: `harness route print the resolved route as JSON`,
116
+ escalate: `harness escalate raise session.escalated for the current unit`,
117
+ hook: `harness hook pre-tool-use agent PreToolUse handler (payload on stdin)`,
118
+ session: SESSION_HELP,
119
+ };
120
+ /** True when a `-h`/`--help` token appears before any bare `--` separator. */
121
+ export function wantsHelp(argv) {
122
+ const sep = argv.indexOf("--");
123
+ const scan = sep === -1 ? argv : argv.slice(0, sep);
124
+ return scan.includes("--help") || scan.includes("-h");
125
+ }
47
126
  //# sourceMappingURL=help.js.map
@@ -186,7 +186,10 @@ export function findExploreBeforeEdit(unitEvents) {
186
186
  export function findVerifyHeavierThanImplement(unitEvents) {
187
187
  const implementTurns = unitEvents.filter((e) => IMPLEMENT_STEPS.has(e.step) && (e.kind === "tool" || e.kind === "text")).length;
188
188
  const reviewTurns = unitEvents.filter((e) => REVIEW_STEPS.has(e.step) && (e.kind === "tool" || e.kind === "text")).length;
189
- if (reviewTurns <= implementTurns || reviewTurns === 0)
189
+ // implementTurns === 0 is structural (a resume starting mid-pipeline, no
190
+ // implement step at all) — there is nothing to compare review against, not
191
+ // an imbalance. Symmetric with the reviewTurns === 0 guard below.
192
+ if (reviewTurns <= implementTurns || reviewTurns === 0 || implementTurns === 0)
190
193
  return [];
191
194
  return [
192
195
  {
@@ -238,6 +238,7 @@ export class CliHarness {
238
238
  system,
239
239
  actor: { kind: event.session ? "agent" : "harness", id: actorId },
240
240
  type: event.type,
241
+ ...(event.ts ? { ts: event.ts } : {}),
241
242
  body,
242
243
  });
243
244
  await this.callMutation(["ledger", "append", "--dir", this.opts.ledgerDir ?? join(this.opts.cwd, ".harness", "ledger")], payload);
@@ -260,6 +261,14 @@ export class CliHarness {
260
261
  ...(this.opts.specsDir ? ["--specs", workspacePath(this.opts.specsDir, workspace)] : []),
261
262
  ...(this.opts.profilesDir ? ["--profiles", workspacePath(this.opts.profilesDir, workspace)] : []),
262
263
  ...(this.opts.systemId ? ["--system-id", this.opts.systemId] : []),
264
+ // an agent-driven run must be distinguishable from an operator typing
265
+ // `harness verify` by hand (issue #50) — record this dispatched
266
+ // session's real identity rather than relying on verify's human/cli
267
+ // default.
268
+ "--agent-kind",
269
+ "agent",
270
+ "--agent-id",
271
+ session,
263
272
  "--json",
264
273
  ...this.base(),
265
274
  ]);
@@ -370,9 +379,10 @@ export function parseEnvelopeHash(stdout) {
370
379
  * `⛔ merge blocked — door door-core-a-schema opened. Decide with: …`
371
380
  * `⛔ merge blocked — door door-core-a-schema was REJECTED: …`
372
381
  * `⛔ merge blocked — door door-core-a-schema awaits a decision`
382
+ * `⛔ merge blocked — door door-core-a-schema is approved but <reason>`
373
383
  */
374
384
  export function parseDoorId(stderr) {
375
- const m = stderr.match(/door\s+(\S+?)\s+(?:opened|was REJECTED|awaits)\b/);
385
+ const m = stderr.match(/door\s+(\S+?)\s+(?:opened|was REJECTED|awaits|is approved)\b/);
376
386
  return m ? m[1] : undefined;
377
387
  }
378
388
  //# sourceMappingURL=cli.js.map
@@ -1,6 +1,8 @@
1
1
  import { reconstructSessionEvents } from "../runners/telemetry.js";
2
2
  import { NULL_PUBLISHER } from "../runstate/publish.js";
3
3
  import picomatch from "picomatch";
4
+ import { isUnparseableVerdict } from "../prompts/index.js";
5
+ export const MAX_VERIFIER_REASKS = 2;
4
6
  function isRemoteTrackingRef(ref) {
5
7
  return ref.startsWith("refs/remotes/") || ref.startsWith("origin/");
6
8
  }
@@ -155,6 +157,7 @@ const needsYouKindFor = {
155
157
  "infrastructure-failure": "infrastructure-failure",
156
158
  "config-error": "budget",
157
159
  budget: "budget",
160
+ "unparseable-verdict": "escalation",
158
161
  };
159
162
  function recordHalt(log, halt) {
160
163
  log.append({
@@ -427,11 +430,21 @@ export async function step(unit, policy, deps) {
427
430
  catch (error) {
428
431
  return mutationFailure(unit.id, "heartbeat mutation", error, log);
429
432
  }
433
+ const implStartedAt = new Date().toISOString();
430
434
  const impl = await runner.run(briefing, policy, "implementer", workspace, onActivity);
435
+ const implEndedAt = new Date().toISOString();
431
436
  log.append({ t: "lane.cost", unitId: unit.id, delta: impl.cost });
432
437
  if (impl.telemetryError)
433
438
  return mutationFailure(unit.id, "implementer session telemetry", impl.telemetryError, log);
434
- await emitReconstructedTelemetry(deps, { unitId: unit.id, session, runner: runner.name, model: policy.model, kind: "implementer" }, runner, impl);
439
+ await emitReconstructedTelemetry(deps, {
440
+ unitId: unit.id,
441
+ session,
442
+ runner: runner.name,
443
+ model: policy.model,
444
+ kind: "implementer",
445
+ startedAt: implStartedAt,
446
+ endedAt: implEndedAt,
447
+ }, runner, impl);
435
448
  if (impl.escalation) {
436
449
  log.append({
437
450
  t: "unit.escalated",
@@ -530,11 +543,21 @@ export async function step(unit, policy, deps) {
530
543
  }
531
544
  // --- review (independent verifier session, read-only) -----------
532
545
  setStep("review");
533
- const review = await runner.run(briefing, policy, "verifier", workspace, onActivity);
546
+ let reviewStartedAt = new Date().toISOString();
547
+ let review = await runner.run(briefing, policy, "verifier", workspace, onActivity);
548
+ let reviewEndedAt = new Date().toISOString();
534
549
  log.append({ t: "lane.cost", unitId: unit.id, delta: review.cost });
535
550
  if (review.telemetryError)
536
551
  return mutationFailure(unit.id, "verifier session telemetry", review.telemetryError, log);
537
- await emitReconstructedTelemetry(deps, { unitId: unit.id, session, runner: runner.name, model: policy.model, kind: "verifier" }, runner, review);
552
+ await emitReconstructedTelemetry(deps, {
553
+ unitId: unit.id,
554
+ session,
555
+ runner: runner.name,
556
+ model: policy.model,
557
+ kind: "verifier",
558
+ startedAt: reviewStartedAt,
559
+ endedAt: reviewEndedAt,
560
+ }, runner, review);
538
561
  // a verifier that touches a file voids its verdict (M2 backstop). A
539
562
  // ClaudeRunner verifier structurally cannot write (no write tools); this
540
563
  // catches a runner that reports one anyway. Isolating a degraded-tier
@@ -546,6 +569,47 @@ export async function step(unit, policy, deps) {
546
569
  continue;
547
570
  return exhausted(unit.id, config.retryBudget, log);
548
571
  }
572
+ let verifierReasks = 0;
573
+ while (isUnparseableVerdict(review.verdict)) {
574
+ if (verifierReasks >= MAX_VERIFIER_REASKS) {
575
+ const reasons = review.verdict?.reasons?.join("; ") || "no VERDICT line in the verifier output";
576
+ const halt = {
577
+ kind: "unparseable-verdict",
578
+ unitId: unit.id,
579
+ message: `${unit.id}: verifier verdict unparseable after ${verifierReasks + 1} attempts (${reasons})`,
580
+ unblock: `inspect the verifier transcript, check model output, and re-run`,
581
+ };
582
+ recordHalt(log, halt);
583
+ return { outcome: "halted", halt };
584
+ }
585
+ verifierReasks++;
586
+ log.append({
587
+ t: "lane.note",
588
+ unitId: unit.id,
589
+ note: `verifier verdict unparseable (${review.verdict?.reasons?.join("; ") || "no VERDICT line"}) — re-asking verifier session (re-ask ${verifierReasks}/${MAX_VERIFIER_REASKS})`,
590
+ });
591
+ reviewStartedAt = new Date().toISOString();
592
+ review = await runner.run(briefing, policy, "verifier", workspace, onActivity);
593
+ reviewEndedAt = new Date().toISOString();
594
+ log.append({ t: "lane.cost", unitId: unit.id, delta: review.cost });
595
+ if (review.telemetryError)
596
+ return mutationFailure(unit.id, "verifier session telemetry", review.telemetryError, log);
597
+ await emitReconstructedTelemetry(deps, {
598
+ unitId: unit.id,
599
+ session,
600
+ runner: runner.name,
601
+ model: policy.model,
602
+ kind: "verifier",
603
+ startedAt: reviewStartedAt,
604
+ endedAt: reviewEndedAt,
605
+ }, runner, review);
606
+ if (review.filesTouched.length > 0) {
607
+ const retry = registerRetryOrExhaust(unit.id, attempt, config.retryBudget, log, `verifier session wrote files (${review.filesTouched.join(", ")}) — verdict voided`);
608
+ if (retry)
609
+ continue;
610
+ return exhausted(unit.id, config.retryBudget, log);
611
+ }
612
+ }
549
613
  if (review.verdict && !review.verdict.approved) {
550
614
  const retry = registerRetryOrExhaust(unit.id, attempt, config.retryBudget, log, `verifier rejected: ${review.verdict.reasons.join("; ")}`);
551
615
  if (retry)
@@ -786,11 +850,56 @@ export async function resumeAfterDoor(unitId, deps) {
786
850
  return exhausted(unitId, 0, log, ring2.evidence);
787
851
  }
788
852
  setStep("review");
789
- const review = await runner.run(briefing, policy, "verifier", workspace, onActivity);
853
+ let reviewStartedAt = new Date().toISOString();
854
+ let review = await runner.run(briefing, policy, "verifier", workspace, onActivity);
855
+ let reviewEndedAt = new Date().toISOString();
790
856
  log.append({ t: "lane.cost", unitId, delta: review.cost });
791
857
  if (review.telemetryError)
792
858
  return mutationFailure(unitId, "door-resume verifier telemetry", review.telemetryError, log);
793
- await emitReconstructedTelemetry(deps, { unitId, session, runner: runner.name, model: policy.model, kind: "verifier" }, runner, review);
859
+ await emitReconstructedTelemetry(deps, {
860
+ unitId,
861
+ session,
862
+ runner: runner.name,
863
+ model: policy.model,
864
+ kind: "verifier",
865
+ startedAt: reviewStartedAt,
866
+ endedAt: reviewEndedAt,
867
+ }, runner, review);
868
+ let verifierReasks = 0;
869
+ while (isUnparseableVerdict(review.verdict)) {
870
+ if (verifierReasks >= MAX_VERIFIER_REASKS) {
871
+ const reasons = review.verdict?.reasons?.join("; ") || "no VERDICT line in the verifier output";
872
+ const halt = {
873
+ kind: "unparseable-verdict",
874
+ unitId,
875
+ message: `${unitId}: verifier verdict unparseable during door resume after ${verifierReasks + 1} attempts (${reasons})`,
876
+ unblock: `inspect the verifier transcript and re-run orchestrate resume --unit ${unitId}`,
877
+ };
878
+ recordHalt(log, halt);
879
+ return { outcome: "halted", halt };
880
+ }
881
+ verifierReasks++;
882
+ log.append({
883
+ t: "lane.note",
884
+ unitId,
885
+ note: `verifier verdict unparseable during door resume (${review.verdict?.reasons?.join("; ") || "no VERDICT line"}) — re-asking verifier session (re-ask ${verifierReasks}/${MAX_VERIFIER_REASKS})`,
886
+ });
887
+ reviewStartedAt = new Date().toISOString();
888
+ review = await runner.run(briefing, policy, "verifier", workspace, onActivity);
889
+ reviewEndedAt = new Date().toISOString();
890
+ log.append({ t: "lane.cost", unitId, delta: review.cost });
891
+ if (review.telemetryError)
892
+ return mutationFailure(unitId, "door-resume verifier telemetry", review.telemetryError, log);
893
+ await emitReconstructedTelemetry(deps, {
894
+ unitId,
895
+ session,
896
+ runner: runner.name,
897
+ model: policy.model,
898
+ kind: "verifier",
899
+ startedAt: reviewStartedAt,
900
+ endedAt: reviewEndedAt,
901
+ }, runner, review);
902
+ }
794
903
  if (!review.ok || review.filesTouched.length || review.verdict?.approved === false) {
795
904
  const evidence = review.filesTouched.length
796
905
  ? `verifier wrote files: ${review.filesTouched.join(", ")}`
@@ -170,7 +170,12 @@ ${diff.trim() || "(empty diff)"}
170
170
  \`\`\`
171
171
  `;
172
172
  }
173
- /** Parse the verifier's final `VERDICT:` line. Absent / malformed → reject. */
173
+ export function isUnparseableVerdict(verdict) {
174
+ if (!verdict)
175
+ return true;
176
+ return verdict.unparseable === true;
177
+ }
178
+ /** Parse the verifier's final `VERDICT:` line. Absent / malformed → unparseable. */
174
179
  export function parseVerdict(text) {
175
180
  // Strip leading markdown emphasis / quote / list / heading markers and
176
181
  // surrounding `**`/`__`/backticks — agents routinely bold the verdict line
@@ -182,18 +187,29 @@ export function parseVerdict(text) {
182
187
  .filter((l) => l.toUpperCase().startsWith("VERDICT:"))
183
188
  .at(-1);
184
189
  if (!line) {
185
- return { approved: false, reasons: ["no VERDICT line in the verifier output"] };
190
+ return {
191
+ approved: false,
192
+ reasons: ["no VERDICT line in the verifier output"],
193
+ unparseable: true,
194
+ };
186
195
  }
187
196
  const body = line.slice(line.indexOf(":") + 1).trim();
188
197
  if (/^APPROVE\b/i.test(body))
189
198
  return { approved: true, reasons: [] };
190
- const dash = body.search(/[—-]/);
191
- const reasonText = dash >= 0 ? body.slice(dash + 1).trim() : "";
199
+ if (/^REJECT\b/i.test(body)) {
200
+ const dash = body.search(/[—-]/);
201
+ const reasonText = dash >= 0 ? body.slice(dash + 1).trim() : "";
202
+ return {
203
+ approved: false,
204
+ reasons: reasonText
205
+ ? reasonText.split(",").map((r) => r.trim()).filter(Boolean)
206
+ : ["verifier rejected without stated reasons"],
207
+ };
208
+ }
192
209
  return {
193
210
  approved: false,
194
- reasons: reasonText
195
- ? reasonText.split(",").map((r) => r.trim()).filter(Boolean)
196
- : ["verifier rejected without stated reasons"],
211
+ reasons: [`unparseable VERDICT line: "${line}"`],
212
+ unparseable: true,
197
213
  };
198
214
  }
199
215
  //# sourceMappingURL=index.js.map
@@ -16,11 +16,12 @@ export function reconstructSessionEvents(p) {
16
16
  ? "ok"
17
17
  : "failed";
18
18
  return [
19
- { type: "session.started", unitId: p.unitId, ...common },
19
+ { type: "session.started", unitId: p.unitId, ts: p.startedAt, ...common },
20
20
  {
21
21
  type: "session.ended",
22
22
  unitId: p.unitId,
23
23
  session: p.session,
24
+ ts: p.endedAt,
24
25
  body: {
25
26
  ...common.body,
26
27
  outcome,
@@ -10,6 +10,23 @@ export function checkScope(changedFiles, effectiveScope) {
10
10
  const matches = picomatch(effectiveScope.map(norm), { dot: true });
11
11
  return changedFiles.map(norm).filter((path) => !matches(path));
12
12
  }
13
+ /** Path-like tokens in a gate's `run =` command — e.g. `node packages/x/gates.mjs size` → `["packages/x/gates.mjs"]`. */
14
+ function pathTokensIn(run) {
15
+ return run
16
+ .split(/\s+/)
17
+ .filter((tok) => tok.includes("/") || tok.includes("\\"))
18
+ .map(norm);
19
+ }
20
+ /**
21
+ * Which of the resolved profile's own bound-gate script paths this diff
22
+ * touches — a unit editing the referee that judges it (issue #55). Visibility
23
+ * only: this never changes the verdict by itself.
24
+ */
25
+ export function gateFilesTouched(changedFiles, boundGates) {
26
+ const scriptPaths = new Set(boundGates.flatMap((g) => pathTokensIn(g.run)));
27
+ const changed = new Set(changedFiles.map(norm));
28
+ return [...scriptPaths].filter((p) => changed.has(p)).sort();
29
+ }
13
30
  /** Derive both sides of the scope assertion from git + the accepted spec. */
14
31
  export function attestScope(opts) {
15
32
  if (!opts.baseRef || !opts.specsDir) {
@@ -35,7 +52,11 @@ export function attestScope(opts) {
35
52
  reason: `no accepted spec found for unit "${unitId}" under ${opts.specsDir}`,
36
53
  };
37
54
  }
38
- const stdout = execFileSync("git", ["diff", "--name-only", `${opts.baseRef}...HEAD`, "--"], { cwd: opts.componentDir, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
55
+ const stdout = execFileSync("git", ["diff", "--name-only", `${opts.baseRef}...HEAD`, "--"], {
56
+ cwd: opts.componentDir,
57
+ encoding: "utf8",
58
+ stdio: ["ignore", "pipe", "pipe"],
59
+ });
39
60
  const changedFiles = stdout
40
61
  .split(/\r?\n/)
41
62
  .map((path) => norm(path.trim()))
@@ -49,7 +70,9 @@ export function attestScope(opts) {
49
70
  effectiveScope,
50
71
  violations,
51
72
  ...(violations.length > 0
52
- ? { reason: `changed paths outside declared scope: ${violations.join(", ")}` }
73
+ ? {
74
+ reason: `changed paths outside declared scope: ${violations.join(", ")}`,
75
+ }
53
76
  : {}),
54
77
  };
55
78
  }
@@ -26,6 +26,8 @@ import { initCommand } from "./init/index.js";
26
26
  import { claimCommand, heartbeatCommand, nextCommand, } from "./sequencer/cli.js";
27
27
  import { specCommand } from "./spec/cli.js";
28
28
  import { verifyCommand } from "./verify/cli.js";
29
+ import { attestCommand } from "./provenance/cli.js";
30
+ import { statusCommand } from "./status/cli.js";
29
31
  const USAGE = `harness — deterministic scaffolding around nondeterministic agents
30
32
 
31
33
  Usage:
@@ -45,7 +47,11 @@ Usage:
45
47
  project emit harness unit YAML from OpenSpec + harness.projection.yaml
46
48
  --change <dir> [--openspec <dir>] [--out <dir>] [--json]
47
49
  harness next list units ready to claim (deps merged, unclaimed or stale)
50
+ harness status read-only unit lanes from the ledger, with a run-status
51
+ spool overlay [--ledger <dir>] [--spool <dir>] [--json]
48
52
  harness claim <unit-id> --session <id> atomically take a ready unit
53
+ --release hand an executing claim straight back
54
+ (refused unless --session holds it)
49
55
  harness heartbeat <unit-id> --session <id> keep a claim alive
50
56
  harness door list [--sweep] | show <id> | approve <id> --reason … | reject …
51
57
  harness merge <unit-id> <--check | --landed <sha>> [facts…] authorize/record
@@ -65,6 +71,7 @@ const COMMANDS = [
65
71
  "gate",
66
72
  "spec",
67
73
  "next",
74
+ "status",
68
75
  "claim",
69
76
  "heartbeat",
70
77
  "door",
@@ -103,6 +110,9 @@ if (cmd === "spec") {
103
110
  if (cmd === "next") {
104
111
  process.exit(nextCommand(process.argv.slice(3)));
105
112
  }
113
+ if (cmd === "status") {
114
+ process.exit(statusCommand(process.argv.slice(3)));
115
+ }
106
116
  if (cmd === "door") {
107
117
  process.exit(doorCommand(process.argv.slice(3)));
108
118
  }
@@ -169,6 +179,9 @@ if (cmd === "gate") {
169
179
  process.exit(code);
170
180
  // fall through: a bare `harness gate <name>` (not --pre) is still a skeleton
171
181
  }
182
+ if (cmd === "attest") {
183
+ process.exit(attestCommand(process.argv.slice(3)));
184
+ }
172
185
  process.stderr.write(`harness ${cmd}: not implemented (skeleton)\n`);
173
186
  process.exit(1);
174
187
  //# sourceMappingURL=cli.js.map