harnery 0.23.0 → 0.25.0

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 (59) hide show
  1. package/README.md +1 -1
  2. package/dist/commands/harness.d.ts +2 -1
  3. package/dist/commands/harness.d.ts.map +1 -1
  4. package/dist/commands/harness.js +105 -3
  5. package/dist/commands/supervisor.d.ts.map +1 -1
  6. package/dist/commands/supervisor.js +3 -13
  7. package/dist/commands/work.d.ts.map +1 -1
  8. package/dist/commands/work.js +2 -7
  9. package/dist/commands/workflow.d.ts.map +1 -1
  10. package/dist/commands/workflow.js +215 -17
  11. package/dist/core/harnesses/attest.d.ts +48 -0
  12. package/dist/core/harnesses/attest.d.ts.map +1 -0
  13. package/dist/core/harnesses/attest.js +110 -0
  14. package/dist/core/harnesses/attestation.d.ts +75 -0
  15. package/dist/core/harnesses/attestation.d.ts.map +1 -0
  16. package/dist/core/harnesses/attestation.js +168 -0
  17. package/dist/core/harnesses/bench.d.ts +23 -1
  18. package/dist/core/harnesses/bench.d.ts.map +1 -1
  19. package/dist/core/harnesses/bench.js +135 -32
  20. package/dist/core/harnesses/index.d.ts +6 -2
  21. package/dist/core/harnesses/index.d.ts.map +1 -1
  22. package/dist/core/harnesses/index.js +3 -1
  23. package/dist/core/workflow/engine.js +3 -0
  24. package/dist/core/workflow/index.d.ts +1 -1
  25. package/dist/core/workflow/index.d.ts.map +1 -1
  26. package/dist/core/workflow/index.js +1 -1
  27. package/dist/core/workflow/proof.d.ts +4 -1
  28. package/dist/core/workflow/proof.d.ts.map +1 -1
  29. package/dist/core/workflow/proof.js +3 -2
  30. package/dist/core/workflow/spawn-cursor.d.ts +3 -0
  31. package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
  32. package/dist/core/workflow/spawn-cursor.js +8 -4
  33. package/dist/core/workflow/types.d.ts +14 -0
  34. package/dist/core/workflow/types.d.ts.map +1 -1
  35. package/dist/core/workflow/workspaces/index.d.ts +1 -1
  36. package/dist/core/workflow/workspaces/index.d.ts.map +1 -1
  37. package/dist/core/workflow/workspaces/index.js +1 -1
  38. package/dist/core/workflow/workspaces/integration.d.ts +8 -0
  39. package/dist/core/workflow/workspaces/integration.d.ts.map +1 -1
  40. package/dist/core/workflow/workspaces/integration.js +15 -1
  41. package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
  42. package/dist/core/workflow/workspaces/leases.js +19 -2
  43. package/package.json +1 -1
  44. package/src/commands/harness.ts +123 -2
  45. package/src/commands/supervisor.ts +11 -15
  46. package/src/commands/work.ts +8 -8
  47. package/src/commands/workflow.ts +268 -20
  48. package/src/core/harnesses/attest.ts +162 -0
  49. package/src/core/harnesses/attestation.ts +231 -0
  50. package/src/core/harnesses/bench.ts +187 -32
  51. package/src/core/harnesses/index.ts +26 -1
  52. package/src/core/workflow/engine.ts +3 -0
  53. package/src/core/workflow/index.ts +1 -0
  54. package/src/core/workflow/proof.ts +7 -1
  55. package/src/core/workflow/spawn-cursor.ts +8 -4
  56. package/src/core/workflow/types.ts +15 -0
  57. package/src/core/workflow/workspaces/index.ts +5 -1
  58. package/src/core/workflow/workspaces/integration.ts +17 -1
  59. package/src/core/workflow/workspaces/leases.ts +24 -2
package/README.md CHANGED
@@ -22,7 +22,7 @@
22
22
  - **Multi-agent coordination:** per-agent heartbeats in `.harnery/active/`, claim-time and commit-time guards, the canonical event stream, councils, a decision docket, per-agent scratchpads, and harness adapters for Claude Code / Cursor / Codex.
23
23
  - **Cross-machine presence:** sessions on every machine that shares a repo see each other, labeled by machine. Zero-config over the repo's own git remote (presence refs), with an optional live-socket upgrade via an end-to-end-encrypted relay — the public `relay.harnery.com`, or self-host with `harn relay serve` / the bundled Cloudflare Worker (`relay/worker/`). The relay sees only ciphertext in opaquely-named rooms.
24
24
  - **Coordination-aware workflows:** `harn workflow run <script>` executes bounded, schema-gated multi-subagent workflow scripts — plain JS stages fanning out to headless harness-CLI subagents that are born coordination-registered (heartbeats, events, claim guards), with deterministic code deciding the routing between stages. An optional host-owned policy gates dispatch and declared external mutations with `ALLOW`, `ASK`, and `DENY`; CLI `ASK` requests park durably for an explicit approve/deny and same-run resume, while library callers remain fail-closed by default.
25
- - **Optional isolated workspaces:** add `--isolation worktree --workspace-root <dir>` to run a workflow in a host-owned local Git worktree. Harnery freezes the allocation and provider identity, verifies the workspace before execution and resume, requires review and policy authority before fast-forward integration, and keeps cleanup conservative. `harn workflow workspace <run-id>` and the dashboard show the validated lifecycle; shared execution remains the default.
25
+ - **Optional isolated workspaces:** add `--isolation worktree --workspace-root <dir>` to run a workflow in a host-owned local Git worktree. Harnery freezes the allocation and provider identity, verifies the workspace before execution and resume, requires review and policy authority before fast-forward integration, and keeps cleanup conservative. Inspect with `harn workflow workspace`, then use the separate `workflow integration prepare`, `integration apply`, and `cleanup` phases; shared execution remains the default.
26
26
  - **Durable work above attempts:** `harn work` preserves an objective across bounded workflow attempts, dependencies, approval parks, failures, and review. Reusable workflow code receives the exact work ID, title, objective, acceptance criteria, and attempt identity as frozen typed context. A retry also receives the prior run's bounded error and unresolved proof criteria, while retry authority and attempt ceilings remain unchanged. The manifest, proof, and append-only ledger bind the same context, and the one-shot reconciler reports the next explicit action without silently spawning, retrying, or accepting work.
27
27
  - **Bounded goal supervision:** `harn supervisor` freezes a mission or existing root graph, specialist team, and safety limits, then runs scheduling cycles until reviewed mission success, human attention, no progress, or budget exhaustion. Its optional planner can create the first milestone, reassess accepted milestone proof, or recover a terminal graph using only a frozen workflow-template catalog. Plans can require independent bounded specialist review before ordinary approval or auto-apply authority, old generations remain immutable evidence, and all attempts still consume the goal budget. Use explicit foreground ticks or enroll goals in the optional heartbeat-backed background service.
28
28
  - **Standalone web UI:** `harn web up` boots a local Next.js dashboard for the coord layer, councils, per-project state, and a local coding-agent status view (Claude Code / Codex / Cursor usage, backed by `harn devtools`). Ships with the git clone, not the npm package (see [Install](#install)).
@@ -1,8 +1,9 @@
1
1
  import type { Command } from "commander";
2
2
  import type { EmitContext } from "../commander.js";
3
- import { type BenchResult, type HarnessBenchReport } from "../core/harnesses/index.js";
3
+ import { type BenchResult, type HarnessAttestationReport, type HarnessBenchReport } from "../core/harnesses/index.js";
4
4
  import type { HarnessProfile } from "../core/harnesses/types.js";
5
5
  export declare function registerHarnessCommand(program: Command, emit: EmitContext): void;
6
+ export declare function renderAttestationReport(report: HarnessAttestationReport): string;
6
7
  export declare function renderProfileTable(profiles: readonly HarnessProfile[]): string;
7
8
  export declare function renderProfile(profile: HarnessProfile): string;
8
9
  export declare function renderBenchReport(report: HarnessBenchReport): string;
@@ -1 +1 @@
1
- {"version":3,"file":"harness.d.ts","sourceRoot":"","sources":["../../src/commands/harness.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EACL,KAAK,WAAW,EAEhB,KAAK,kBAAkB,EAExB,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAYjE,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CA6DhF;AAED,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,SAAS,cAAc,EAAE,GAAG,MAAM,CAU9E;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,cAAc,GAAG,MAAM,CAkB7D;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,kBAAkB,GAAG,MAAM,CAcpE;AAgBD,mFAAmF;AACnF,YAAY,EAAE,WAAW,EAAE,CAAC"}
1
+ {"version":3,"file":"harness.d.ts","sourceRoot":"","sources":["../../src/commands/harness.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EACL,KAAK,WAAW,EAEhB,KAAK,wBAAwB,EAC7B,KAAK,kBAAkB,EAIxB,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAiBjE,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CA6IhF;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,wBAAwB,GAAG,MAAM,CAgBhF;AAED,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,SAAS,cAAc,EAAE,GAAG,MAAM,CAU9E;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,cAAc,GAAG,MAAM,CAkB7D;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,kBAAkB,GAAG,MAAM,CA6BpE;AAgBD,mFAAmF;AACnF,YAAY,EAAE,WAAW,EAAE,CAAC"}
@@ -1,4 +1,4 @@
1
- import { createBuiltinHarnessRegistry, runHarnessBench, } from "../core/harnesses/index.js";
1
+ import { createBuiltinHarnessRegistry, listAttestations, runHarnessAttestation, runHarnessBench, } from "../core/harnesses/index.js";
2
2
  const registry = createBuiltinHarnessRegistry();
3
3
  export function registerHarnessCommand(program, emit) {
4
4
  const command = program
@@ -62,6 +62,96 @@ export function registerHarnessCommand(program, emit) {
62
62
  emit.setExitCode(1);
63
63
  }
64
64
  });
65
+ command
66
+ .command("attest [harnesses...]")
67
+ .description("Record what the installed vendor CLIs actually do. Runs one real model turn each; needs --yes.")
68
+ .option("--yes", "Confirm that this spends real vendor tokens")
69
+ .option("--timeout <ms>", "Per-harness probe timeout in milliseconds")
70
+ .option("--json", "Machine-readable attestation report")
71
+ .action(async (harnesses, opts) => {
72
+ if (!opts.yes) {
73
+ emit.error({
74
+ code: "harness_attest_unconfirmed",
75
+ message: "harness attest runs one real model turn per harness and spends vendor tokens. Re-run with --yes.",
76
+ });
77
+ emit.setExitCode(1);
78
+ return;
79
+ }
80
+ const timeoutMs = opts.timeout ? Number(opts.timeout) : undefined;
81
+ if (timeoutMs !== undefined && (!Number.isFinite(timeoutMs) || timeoutMs <= 0)) {
82
+ emit.error({
83
+ code: "harness_attest_bad_timeout",
84
+ message: "--timeout must be a positive number",
85
+ });
86
+ emit.setExitCode(1);
87
+ return;
88
+ }
89
+ try {
90
+ const report = await runHarnessAttestation(registry, { harnesses, timeoutMs });
91
+ if (opts.json) {
92
+ emit.config({ format: "json" });
93
+ emit.data(report);
94
+ }
95
+ else {
96
+ emit.text(renderAttestationReport(report));
97
+ }
98
+ // A sweep that recorded nothing at all is a failure; a partial sweep is
99
+ // reported honestly and still exits 0, because a host is not required
100
+ // to install every vendor CLI.
101
+ emit.setExitCode(report.recorded === 0 ? 1 : 0);
102
+ }
103
+ catch (error) {
104
+ emit.error({ code: "harness_attest_failed", message: error.message });
105
+ emit.setExitCode(1);
106
+ }
107
+ });
108
+ command
109
+ .command("attestations")
110
+ .description("Show the recorded live attestations without running any model turns.")
111
+ .option("--json", "Machine-readable attestation list")
112
+ .action((opts) => {
113
+ try {
114
+ const records = listAttestations();
115
+ if (opts.json) {
116
+ emit.config({ format: "json" });
117
+ emit.data({ attestations: records });
118
+ return;
119
+ }
120
+ if (records.length === 0) {
121
+ emit.text("No attestations recorded. Run `harness attest --yes` to create one.");
122
+ return;
123
+ }
124
+ emit.text(renderTable(["HARNESS", "VERSION", "OBSERVED AT", "OBSERVATIONS"], records.map((record) => [
125
+ record.harness,
126
+ record.binary_version,
127
+ record.observed_at,
128
+ Object.entries(record.observations)
129
+ .map(([dimension, support]) => `${dimension}=${support}`)
130
+ .join(" "),
131
+ ])));
132
+ }
133
+ catch (error) {
134
+ emit.error({ code: "harness_attestations_failed", message: error.message });
135
+ emit.setExitCode(1);
136
+ }
137
+ });
138
+ }
139
+ export function renderAttestationReport(report) {
140
+ const rows = report.results.map((result) => [
141
+ result.harness,
142
+ result.outcome,
143
+ result.binaryVersion ?? "",
144
+ result.observations
145
+ ? Object.entries(result.observations)
146
+ .map(([dimension, support]) => `${dimension}=${support}`)
147
+ .join(" ")
148
+ : result.note,
149
+ ]);
150
+ const table = renderTable(["HARNESS", "OUTCOME", "VERSION", "OBSERVED"], rows);
151
+ const tail = report.incomplete
152
+ ? "Some harnesses were not attested; their bench rows keep an adapter basis."
153
+ : "Every selected harness was attested.";
154
+ return `${table}\n\nrecorded: ${report.recorded}/${report.results.length}\n${tail}`;
65
155
  }
66
156
  export function renderProfileTable(profiles) {
67
157
  const rows = profiles.map((profile) => [
@@ -100,13 +190,25 @@ export function renderBenchReport(report) {
100
190
  result.declared,
101
191
  result.observed,
102
192
  result.verdict,
193
+ result.basis,
103
194
  ]);
104
- const table = renderTable(["HARNESS", "DIMENSION", "DECLARED", "OBSERVED", "VERDICT"], rows);
195
+ const table = renderTable(["HARNESS", "DIMENSION", "DECLARED", "OBSERVED", "VERDICT", "BASIS"], rows);
105
196
  const summary = Object.entries(report.summary)
106
197
  .filter(([, count]) => count > 0)
107
198
  .map(([verdict, count]) => `${verdict}=${count}`)
108
199
  .join(", ");
109
- return `${table}\n\nmode: ${report.mode} (no model calls)\n${summary}`;
200
+ const basis = Object.entries(report.basisSummary)
201
+ .filter(([, count]) => count > 0)
202
+ .map(([name, count]) => `${name}=${count}`)
203
+ .join(", ");
204
+ return [
205
+ table,
206
+ "",
207
+ `mode: ${report.mode} (no model calls)`,
208
+ summary,
209
+ `basis: ${basis}`,
210
+ "adapter = checked against Harnery's planner/normalizer/fixture, not the installed CLI",
211
+ ].join("\n");
110
212
  }
111
213
  function renderTable(headers, rows) {
112
214
  const widths = headers.map((header, index) => Math.max(header.length, ...rows.map((row) => row[index]?.length ?? 0)));
@@ -1 +1 @@
1
- {"version":3,"file":"supervisor.d.ts","sourceRoot":"","sources":["../../src/commands/supervisor.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAsEnD,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CAgGnF"}
1
+ {"version":3,"file":"supervisor.d.ts","sourceRoot":"","sources":["../../src/commands/supervisor.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AA0EnD,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CAgGnF"}
@@ -1,7 +1,7 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { dirname, isAbsolute, resolve } from "node:path";
3
3
  import { workflowSubscriptionOnly } from "../core/config.js";
4
- import { createBuiltinHarnessRegistry } from "../core/harnesses/index.js";
4
+ import { createBuiltinHarnessRegistry, harnessProofInputs, probeBinaryVersion, } from "../core/harnesses/index.js";
5
5
  import { findCoordRoot } from "../core/hooks/resolve/coord-root.js";
6
6
  import { loadPolicyFile } from "../core/policy/index.js";
7
7
  import { approveSupervisorPlan, configureSupervisorService, createSupervisor, listSupervisors, readSupervisor, readSupervisorPlan, readSupervisorServiceConfig, readSupervisorServiceStatus, rejectSupervisorPlan, requestSupervisorServiceStop, retrySupervisorPlan, runSupervisor, runSupervisorServiceDaemon, spawnSupervisorService, supervisorServiceLogPath, } from "../core/supervisor/index.js";
@@ -332,12 +332,7 @@ function serviceEngine(config, registry) {
332
332
  cwd: config.engine.cwd,
333
333
  subscriptionOnly: config.engine.subscription_only,
334
334
  allowApiBilling: config.engine.allow_api_billing,
335
- harnessEvidence: Object.fromEntries(registry
336
- .list()
337
- .map((adapter) => [
338
- adapter.profile.id,
339
- { toolEvidence: adapter.profile.capabilities.toolEvidence },
340
- ])),
335
+ ...harnessProofInputs(registry.list().map((adapter) => adapter.profile), { versionProbe: probeBinaryVersion }),
341
336
  policy: config.engine.policy,
342
337
  approvalMode: "park",
343
338
  approvalAddressee: config.engine.approval_addressee,
@@ -413,12 +408,7 @@ function registerRunCommand(supervisor, mode, registry, emit) {
413
408
  cwd: opts.cwd,
414
409
  subscriptionOnly: opts.subscriptionOnly === true ? true : workflowSubscriptionOnly(coordRoot),
415
410
  allowApiBilling: opts.allowApiBilling,
416
- harnessEvidence: Object.fromEntries(registry
417
- .list()
418
- .map((adapter) => [
419
- adapter.profile.id,
420
- { toolEvidence: adapter.profile.capabilities.toolEvidence },
421
- ])),
411
+ ...harnessProofInputs(registry.list().map((adapter) => adapter.profile), { versionProbe: probeBinaryVersion }),
422
412
  policy: opts.policy ? loadPolicyFile(opts.policy) : undefined,
423
413
  approvalMode: "park",
424
414
  approvalAddressee: opts.approvalTo,
@@ -1 +1 @@
1
- {"version":3,"file":"work.d.ts","sourceRoot":"","sources":["../../src/commands/work.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAmDnD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CA8F7E"}
1
+ {"version":3,"file":"work.d.ts","sourceRoot":"","sources":["../../src/commands/work.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAuDnD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CA8F7E"}
@@ -1,5 +1,5 @@
1
1
  import { workflowSubscriptionOnly } from "../core/config.js";
2
- import { createBuiltinHarnessRegistry } from "../core/harnesses/index.js";
2
+ import { createBuiltinHarnessRegistry, harnessProofInputs, probeBinaryVersion, } from "../core/harnesses/index.js";
3
3
  import { findCoordRoot } from "../core/hooks/resolve/coord-root.js";
4
4
  import { loadPolicyFile } from "../core/policy/index.js";
5
5
  import { acceptWorkItem, cancelWorkItem, createWorkItem, listWorkItems, readWorkItem, reconcileAllWorkItems, reconcileWorkItem, reopenWorkItem, runWorkItem, } from "../core/work/index.js";
@@ -132,12 +132,7 @@ function registerRunCommand(work, name, retry, registry, harnesses, emit) {
132
132
  cwd: opts.cwd,
133
133
  subscriptionOnly: opts.subscriptionOnly === true ? true : workflowSubscriptionOnly(coordRoot),
134
134
  allowApiBilling: opts.allowApiBilling,
135
- harnessEvidence: Object.fromEntries(registry
136
- .list()
137
- .map((adapter) => [
138
- adapter.profile.id,
139
- { toolEvidence: adapter.profile.capabilities.toolEvidence },
140
- ])),
135
+ ...harnessProofInputs(registry.list().map((adapter) => adapter.profile), { versionProbe: probeBinaryVersion }),
141
136
  policy: opts.policy ? loadPolicyFile(opts.policy) : undefined,
142
137
  approvalMode: "park",
143
138
  approvalAddressee: opts.approvalTo,
@@ -1 +1 @@
1
- {"version":3,"file":"workflow.d.ts","sourceRoot":"","sources":["../../src/commands/workflow.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAiDnD,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CAmejF"}
1
+ {"version":3,"file":"workflow.d.ts","sourceRoot":"","sources":["../../src/commands/workflow.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAqEnD,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CA6pBjF"}
@@ -1,6 +1,7 @@
1
- import { resolve } from "node:path";
2
- import { workflowSubscriptionOnly } from "../core/config.js";
3
- import { createBuiltinHarnessRegistry } from "../core/harnesses/index.js";
1
+ import { existsSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { resolveBinName, workflowSubscriptionOnly } from "../core/config.js";
4
+ import { createBuiltinHarnessRegistry, harnessProofInputs, probeBinaryVersion, } from "../core/harnesses/index.js";
4
5
  import { findCoordRoot } from "../core/hooks/resolve/coord-root.js";
5
6
  import { loadPolicyFile } from "../core/policy/index.js";
6
7
  import { WorkflowParkedError } from "../core/workflow/engine.js";
@@ -87,12 +88,7 @@ export function registerWorkflowCommand(program, emit) {
87
88
  maxAgents: opts.maxAgents ? Number.parseInt(opts.maxAgents, 10) : undefined,
88
89
  concurrency: opts.concurrency ? Number.parseInt(opts.concurrency, 10) : undefined,
89
90
  cwd: opts.cwd,
90
- harnessEvidence: Object.fromEntries(registry
91
- .list()
92
- .map((adapter) => [
93
- adapter.profile.id,
94
- { toolEvidence: adapter.profile.capabilities.toolEvidence },
95
- ])),
91
+ ...harnessProofInputs(registry.list().map((adapter) => adapter.profile), { versionProbe: probeBinaryVersion }),
96
92
  policy: opts.policy ? loadPolicyFile(opts.policy) : undefined,
97
93
  approvalMode: "park",
98
94
  approvalAddressee: opts.approvalTo,
@@ -134,7 +130,7 @@ export function registerWorkflowCommand(program, emit) {
134
130
  else {
135
131
  emit.text(`run ${err.runId} parked\napproval: ${err.approvalId}\n` +
136
132
  `journal: ${err.journalPath}\n` +
137
- `resume after resolution: harn workflow resume ${err.runId}\n`);
133
+ `resume after resolution: ${resolveBinName()} workflow resume ${err.runId}\n`);
138
134
  }
139
135
  return;
140
136
  }
@@ -178,12 +174,7 @@ export function registerWorkflowCommand(program, emit) {
178
174
  writableRoots: [binding.writable_root.configured],
179
175
  }
180
176
  : undefined,
181
- harnessEvidence: Object.fromEntries(registry
182
- .list()
183
- .map((adapter) => [
184
- adapter.profile.id,
185
- { toolEvidence: adapter.profile.capabilities.toolEvidence },
186
- ])),
177
+ ...harnessProofInputs(registry.list().map((adapter) => adapter.profile), { versionProbe: probeBinaryVersion }),
187
178
  });
188
179
  if (opts.json) {
189
180
  emit.config({ format: "json" });
@@ -288,6 +279,186 @@ export function registerWorkflowCommand(program, emit) {
288
279
  : `${inspection.run_id}\tinvalid\t${inspection.error}`)
289
280
  .join("\n")}\n`);
290
281
  });
282
+ const integration = workflow
283
+ .command("integration")
284
+ .description("Prepare and apply proof-gated isolated-workspace integration.");
285
+ integration
286
+ .command("prepare <run-id>")
287
+ .description("Preview integration and write exact review and policy authority.")
288
+ .option("--policy <file>", "Current host policy JSON/JSONC for the Git mutation")
289
+ .option("--reviewer <name>", "Explicit reviewer for a standalone workflow")
290
+ .option("--reason <text>", "Reason recorded with the standalone review")
291
+ .option("--target-root <dir>", "Explicit target checkout (default: frozen source checkout)")
292
+ .option("--accept-unknown <code>", "Accept one verification unknown by code (repeatable)", collectOption, [])
293
+ .option("--approval-to <address>", "Address a durable policy ASK request")
294
+ .option("--json", "Emit the durable integration plan as JSON")
295
+ .action(async (runId, opts) => {
296
+ const coordRoot = findCoordRoot();
297
+ if (!coordRoot) {
298
+ emit.error({
299
+ code: "no_coord_root",
300
+ message: "no .harnery/ coordination root found; run `init` first",
301
+ });
302
+ process.exit(1);
303
+ }
304
+ if (!opts.policy) {
305
+ emit.error({
306
+ code: "integration_policy_required",
307
+ message: "integration prepare requires --policy <file>",
308
+ });
309
+ process.exit(1);
310
+ }
311
+ if (opts.reason && !opts.reviewer) {
312
+ emit.error({
313
+ code: "integration_reviewer_required",
314
+ message: "--reason requires --reviewer <name>",
315
+ });
316
+ process.exit(1);
317
+ }
318
+ try {
319
+ const provider = await builtInProviderForRun(coordRoot, runId);
320
+ const { IntegrationPrepareParkedError, prepareIntegration } = await import("../core/workflow/index.js");
321
+ try {
322
+ const plan = await prepareIntegration({
323
+ coordRoot,
324
+ runId,
325
+ provider,
326
+ policy: loadPolicyFile(opts.policy),
327
+ targetRoot: opts.targetRoot,
328
+ review: opts.reviewer ? { actor: opts.reviewer, reason: opts.reason } : undefined,
329
+ acceptedUnknowns: opts.acceptUnknown,
330
+ approvalAddressee: opts.approvalTo,
331
+ });
332
+ if (opts.json) {
333
+ emit.config({ format: "json" });
334
+ emit.data(plan);
335
+ return;
336
+ }
337
+ emit.text(`integration plan ${plan.plan_id} authorized\n` +
338
+ `source: ${plan.provider_preview.source_commit}\n` +
339
+ `target: ${plan.provider_preview.target_ref} at ${plan.provider_preview.target_commit}\n` +
340
+ `changes: ${plan.provider_preview.changed_paths.length}\n` +
341
+ `apply: ${resolveBinName()} workflow integration apply ${runId} --yes\n`);
342
+ }
343
+ catch (error) {
344
+ if (error instanceof IntegrationPrepareParkedError) {
345
+ if (opts.json) {
346
+ emit.config({ format: "json" });
347
+ emit.data({
348
+ status: "parked",
349
+ runId: error.runId,
350
+ planId: error.planId,
351
+ approvalId: error.approvalId,
352
+ });
353
+ }
354
+ else {
355
+ emit.text(`integration preparation parked\n` +
356
+ `run: ${error.runId}\n` +
357
+ `plan: ${error.planId}\n` +
358
+ `approval: ${error.approvalId}\n` +
359
+ `approve: ${resolveBinName()} workflow approvals approve ${error.approvalId}\n`);
360
+ }
361
+ return;
362
+ }
363
+ throw error;
364
+ }
365
+ }
366
+ catch (error) {
367
+ emit.error({
368
+ code: "workflow_integration_prepare_failed",
369
+ message: error instanceof Error ? error.message : String(error),
370
+ });
371
+ process.exit(1);
372
+ }
373
+ });
374
+ integration
375
+ .command("apply <run-id>")
376
+ .description("Apply an authorized durable integration plan.")
377
+ .option("--yes", "Confirm the target-branch mutation")
378
+ .option("--json", "Emit the durable integration receipt as JSON")
379
+ .action(async (runId, opts) => {
380
+ const coordRoot = findCoordRoot();
381
+ if (!coordRoot) {
382
+ emit.error({
383
+ code: "no_coord_root",
384
+ message: "no .harnery/ coordination root found; run `init` first",
385
+ });
386
+ process.exit(1);
387
+ }
388
+ if (!opts.yes) {
389
+ emit.error({
390
+ code: "integration_confirmation_required",
391
+ message: "integration apply changes the target branch; pass --yes to confirm",
392
+ });
393
+ process.exit(1);
394
+ }
395
+ try {
396
+ const provider = await builtInProviderForRun(coordRoot, runId);
397
+ const { applyIntegration } = await import("../core/workflow/index.js");
398
+ const receipt = await applyIntegration({ coordRoot, runId, provider });
399
+ if (opts.json) {
400
+ emit.config({ format: "json" });
401
+ emit.data(receipt);
402
+ return;
403
+ }
404
+ emit.text(`integration ${receipt.status}: ${receipt.target_ref} at ${receipt.target_commit}\n` +
405
+ `receipt: ${receipt.receipt_id}\n` +
406
+ `cleanup: ${resolveBinName()} workflow cleanup ${runId} --yes\n`);
407
+ }
408
+ catch (error) {
409
+ emit.error({
410
+ code: "workflow_integration_apply_failed",
411
+ message: error instanceof Error ? error.message : String(error),
412
+ });
413
+ process.exit(1);
414
+ }
415
+ });
416
+ workflow
417
+ .command("cleanup <run-id>")
418
+ .description("Conservatively release a provider-owned isolated workspace.")
419
+ .option("--yes", "Confirm the worktree and provider-branch removal attempt")
420
+ .option("--json", "Emit the cleanup attempt or receipt as JSON")
421
+ .action(async (runId, opts) => {
422
+ const coordRoot = findCoordRoot();
423
+ if (!coordRoot) {
424
+ emit.error({
425
+ code: "no_coord_root",
426
+ message: "no .harnery/ coordination root found; run `init` first",
427
+ });
428
+ process.exit(1);
429
+ }
430
+ if (!opts.yes) {
431
+ emit.error({
432
+ code: "cleanup_confirmation_required",
433
+ message: "workspace cleanup may remove a worktree and branch; pass --yes to confirm",
434
+ });
435
+ process.exit(1);
436
+ }
437
+ try {
438
+ const provider = await builtInProviderForRun(coordRoot, runId);
439
+ const { cleanupWorkspace } = await import("../core/workflow/index.js");
440
+ const result = await cleanupWorkspace({ coordRoot, runId, provider });
441
+ if (opts.json) {
442
+ emit.config({ format: "json" });
443
+ emit.data(result);
444
+ return;
445
+ }
446
+ const receipt = "receipt_id" in result && typeof result.receipt_id === "string"
447
+ ? `\nreceipt: ${result.receipt_id}`
448
+ : "";
449
+ const reason = "reason" in result && typeof result.reason === "string"
450
+ ? `\nreason: ${result.reason}`
451
+ : "";
452
+ emit.text(`workspace cleanup ${result.status}: ${result.binding_id}${receipt}${reason}\n`);
453
+ }
454
+ catch (error) {
455
+ emit.error({
456
+ code: "workflow_cleanup_failed",
457
+ message: error instanceof Error ? error.message : String(error),
458
+ });
459
+ process.exit(1);
460
+ }
461
+ });
291
462
  const approvals = workflow
292
463
  .command("approvals")
293
464
  .description("Inspect and resolve durable workflow policy approvals.");
@@ -379,7 +550,7 @@ export function registerWorkflowCommand(program, emit) {
379
550
  else {
380
551
  emit.text(`${renderWorkflowApproval(resolved.approval)}` +
381
552
  `${resolved.applied ? "decision recorded" : "decision already recorded"}\n` +
382
- `resume: harn workflow resume ${resolved.approval.request.run_id}\n`);
553
+ approvalNextActionHint(coordRoot, resolved.approval));
383
554
  }
384
555
  }
385
556
  catch (err) {
@@ -422,3 +593,30 @@ export function registerWorkflowCommand(program, emit) {
422
593
  }
423
594
  });
424
595
  }
596
+ async function builtInProviderForRun(coordRoot, runId) {
597
+ const { createLocalGitWorktreeProvider, readWorkflowRunManifest } = await import("../core/workflow/index.js");
598
+ const manifest = readWorkflowRunManifest(coordRoot, runId);
599
+ const binding = manifest.execution.workspace_binding;
600
+ if (!binding) {
601
+ throw new Error(`workflow run ${runId} has no isolated workspace binding`);
602
+ }
603
+ if (binding.provider.id !== "local-git-worktree") {
604
+ throw new Error(`workflow run ${runId} requires unavailable workspace provider ${binding.provider.id}`);
605
+ }
606
+ return createLocalGitWorktreeProvider({ coordRoot });
607
+ }
608
+ function collectOption(value, previous) {
609
+ return [...previous, value];
610
+ }
611
+ /** Next operator command after resolving an approval. Mid-run parks resume the
612
+ * workflow; integration prepare parks after the run is already terminal, so
613
+ * the durable plan is authorized by re-running prepare (resume would fail). */
614
+ function approvalNextActionHint(coordRoot, approval) {
615
+ const bin = resolveBinName();
616
+ const runId = approval.request.run_id;
617
+ const integrationPlanPath = join(coordRoot, ".harnery", "workflows", runId, "integration", "plan.json");
618
+ if (existsSync(integrationPlanPath)) {
619
+ return `prepare: ${bin} workflow integration prepare ${runId}\n`;
620
+ }
621
+ return `resume: ${bin} workflow resume ${runId}\n`;
622
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * The opt-in live probe that produces an attestation (ADR 0038).
3
+ *
4
+ * One bounded turn per harness, through the same `spawn` the workflow engine
5
+ * uses, so what gets attested is the path production takes rather than a
6
+ * parallel test rig.
7
+ */
8
+ import type { SpawnResult } from "../workflow/types.js";
9
+ import type { AttestableDimension, HarnessAttestation } from "./attestation.js";
10
+ import type { HarnessRegistry } from "./registry.js";
11
+ import type { CapabilitySupport, HarnessId } from "./types.js";
12
+ /** Fixed and content-free, so nothing user-supplied can reach the record and
13
+ * the turn stays as cheap as a turn can be. */
14
+ export declare const ATTESTATION_PROMPT = "Reply with the single word: ok";
15
+ export declare const DEFAULT_ATTESTATION_TIMEOUT_MS = 120000;
16
+ export type AttestationOutcome = "recorded" | "skipped" | "unreachable" | "failed";
17
+ export interface HarnessAttestationResult {
18
+ harness: HarnessId;
19
+ outcome: AttestationOutcome;
20
+ binaryVersion?: string;
21
+ observations?: Partial<Record<AttestableDimension, CapabilitySupport>>;
22
+ durationMs?: number;
23
+ note: string;
24
+ }
25
+ export interface HarnessAttestationReport {
26
+ generatedAt: string;
27
+ harnesses: HarnessId[];
28
+ results: HarnessAttestationResult[];
29
+ recorded: number;
30
+ /** True when at least one selected harness could not be attested, so a
31
+ * caller can tell a partial sweep from a complete one. */
32
+ incomplete: boolean;
33
+ }
34
+ export interface RunHarnessAttestationOptions {
35
+ harnesses?: readonly string[];
36
+ timeoutMs?: number;
37
+ cwd?: string;
38
+ coordRoot?: string;
39
+ /** Test seam and alternate host probe. A null version means unavailable. */
40
+ versionProbe?: (binary: string) => string | null;
41
+ /** Test seam. Defaults to the adapter's production spawner. */
42
+ spawn?: (harness: HarnessId, prompt: string, timeoutMs: number) => Promise<SpawnResult>;
43
+ /** Test seam. Defaults to writing under the coord root. */
44
+ persist?: (record: HarnessAttestation) => void;
45
+ now?: () => Date;
46
+ }
47
+ export declare function runHarnessAttestation(registry: HarnessRegistry, opts?: RunHarnessAttestationOptions): Promise<HarnessAttestationReport>;
48
+ //# sourceMappingURL=attest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attest.d.ts","sourceRoot":"","sources":["../../../src/core/harnesses/attest.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,KAAK,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAGhF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,KAAK,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE/D;+CAC+C;AAC/C,eAAO,MAAM,kBAAkB,mCAAmC,CAAC;AAEnE,eAAO,MAAM,8BAA8B,SAAU,CAAC;AAetD,MAAM,MAAM,kBAAkB,GAAG,UAAU,GAAG,SAAS,GAAG,aAAa,GAAG,QAAQ,CAAC;AAEnF,MAAM,WAAW,wBAAwB;IACvC,OAAO,EAAE,SAAS,CAAC;IACnB,OAAO,EAAE,kBAAkB,CAAC;IAC5B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,mBAAmB,EAAE,iBAAiB,CAAC,CAAC,CAAC;IACvE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,wBAAwB;IACvC,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,SAAS,EAAE,CAAC;IACvB,OAAO,EAAE,wBAAwB,EAAE,CAAC;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB;8DAC0D;IAC1D,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,4BAA4B;IAC3C,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4EAA4E;IAC5E,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;IACjD,+DAA+D;IAC/D,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,WAAW,CAAC,CAAC;IACxF,2DAA2D;IAC3D,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,kBAAkB,KAAK,IAAI,CAAC;IAC/C,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;CAClB;AAED,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,eAAe,EACzB,IAAI,GAAE,4BAAiC,GACtC,OAAO,CAAC,wBAAwB,CAAC,CAyFnC"}