@sentropic/h2a 0.80.0 → 0.82.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 (82) hide show
  1. package/README.md +5 -0
  2. package/dist/bin-routing.d.ts +13 -0
  3. package/dist/bin-routing.d.ts.map +1 -0
  4. package/dist/bin-routing.js +46 -0
  5. package/dist/bin-routing.js.map +1 -0
  6. package/dist/bin.js +62 -1
  7. package/dist/bin.js.map +1 -1
  8. package/dist/cli-contract.d.ts.map +1 -1
  9. package/dist/cli-contract.js +22 -6
  10. package/dist/cli-contract.js.map +1 -1
  11. package/dist/cli.d.ts +16 -0
  12. package/dist/cli.d.ts.map +1 -1
  13. package/dist/cli.js +197 -11
  14. package/dist/cli.js.map +1 -1
  15. package/dist/index.d.ts +1 -1
  16. package/dist/mcp.d.ts +1 -1
  17. package/dist/mcp.d.ts.map +1 -1
  18. package/dist/mcp.js +3 -1
  19. package/dist/mcp.js.map +1 -1
  20. package/dist/runtime/canevas/adapter.d.ts +11 -0
  21. package/dist/runtime/canevas/adapter.d.ts.map +1 -0
  22. package/dist/runtime/canevas/adapter.js +22 -0
  23. package/dist/runtime/canevas/adapter.js.map +1 -0
  24. package/dist/runtime/canevas/aggregate.d.ts +56 -0
  25. package/dist/runtime/canevas/aggregate.d.ts.map +1 -0
  26. package/dist/runtime/canevas/aggregate.js +84 -0
  27. package/dist/runtime/canevas/aggregate.js.map +1 -0
  28. package/dist/runtime/canevas/answer.d.ts +21 -0
  29. package/dist/runtime/canevas/answer.d.ts.map +1 -0
  30. package/dist/runtime/canevas/answer.js +62 -0
  31. package/dist/runtime/canevas/answer.js.map +1 -0
  32. package/dist/runtime/canevas/app.d.ts +19 -0
  33. package/dist/runtime/canevas/app.d.ts.map +1 -0
  34. package/dist/runtime/canevas/app.js +49 -0
  35. package/dist/runtime/canevas/app.js.map +1 -0
  36. package/dist/runtime/canevas/gather.d.ts +3 -0
  37. package/dist/runtime/canevas/gather.d.ts.map +1 -0
  38. package/dist/runtime/canevas/gather.js +39 -0
  39. package/dist/runtime/canevas/gather.js.map +1 -0
  40. package/dist/runtime/canevas/reply.d.ts +27 -0
  41. package/dist/runtime/canevas/reply.d.ts.map +1 -0
  42. package/dist/runtime/canevas/reply.js +31 -0
  43. package/dist/runtime/canevas/reply.js.map +1 -0
  44. package/dist/runtime/canevas/serve.d.ts +11 -0
  45. package/dist/runtime/canevas/serve.d.ts.map +1 -0
  46. package/dist/runtime/canevas/serve.js +42 -0
  47. package/dist/runtime/canevas/serve.js.map +1 -0
  48. package/dist/runtime/canevas/ui.d.ts +2 -0
  49. package/dist/runtime/canevas/ui.d.ts.map +1 -0
  50. package/dist/runtime/canevas/ui.js +124 -0
  51. package/dist/runtime/canevas/ui.js.map +1 -0
  52. package/dist/runtime/loop/engine/adapters.d.ts +91 -0
  53. package/dist/runtime/loop/engine/adapters.d.ts.map +1 -0
  54. package/dist/runtime/loop/engine/adapters.js +292 -0
  55. package/dist/runtime/loop/engine/adapters.js.map +1 -0
  56. package/dist/runtime/loop/engine/decision.d.ts +71 -0
  57. package/dist/runtime/loop/engine/decision.d.ts.map +1 -0
  58. package/dist/runtime/loop/engine/decision.js +141 -0
  59. package/dist/runtime/loop/engine/decision.js.map +1 -0
  60. package/dist/runtime/loop/engine/execute.d.ts +33 -0
  61. package/dist/runtime/loop/engine/execute.d.ts.map +1 -0
  62. package/dist/runtime/loop/engine/execute.js +69 -0
  63. package/dist/runtime/loop/engine/execute.js.map +1 -0
  64. package/dist/runtime/loop/engine/tick.d.ts +27 -0
  65. package/dist/runtime/loop/engine/tick.d.ts.map +1 -0
  66. package/dist/runtime/loop/engine/tick.js +72 -0
  67. package/dist/runtime/loop/engine/tick.js.map +1 -0
  68. package/dist/runtime/loop/index.d.ts +14 -0
  69. package/dist/runtime/loop/index.d.ts.map +1 -1
  70. package/dist/runtime/loop/index.js +23 -0
  71. package/dist/runtime/loop/index.js.map +1 -1
  72. package/dist/runtime/mcp/handlers.d.ts +12 -0
  73. package/dist/runtime/mcp/handlers.d.ts.map +1 -1
  74. package/dist/runtime/mcp/handlers.js +42 -0
  75. package/dist/runtime/mcp/handlers.js.map +1 -1
  76. package/dist/runtime/mcp/server.d.ts.map +1 -1
  77. package/dist/runtime/mcp/server.js +5 -1
  78. package/dist/runtime/mcp/server.js.map +1 -1
  79. package/dist/runtime/mcp/tools.d.ts.map +1 -1
  80. package/dist/runtime/mcp/tools.js +22 -0
  81. package/dist/runtime/mcp/tools.js.map +1 -1
  82. package/package.json +9 -1
package/dist/cli.js CHANGED
@@ -43,6 +43,12 @@ import { homedir, hostname } from "node:os";
43
43
  import { dirname, join, resolve as resolvePath } from "node:path";
44
44
  import { createRequire } from "node:module";
45
45
  import { fileURLToPath } from "node:url";
46
+ // Static import — `@sentropic/track` is a LIGHT library (pure reader/model, no
47
+ // pty/aws/runtime). The golden rule (lazy-only) is scoped to
48
+ // `@sentropic/h2a-runtime`/node-pty/aws-sdk, which track is NOT. Importing its
49
+ // public `runCli` lets ④ run read-only track verbs IN-PROCESS (no spawn), on the
50
+ // same `.track` under the same lock as the surviving spawn facade.
51
+ import { runCli as runTrackCli } from "@sentropic/track";
46
52
  import { H2A_ATTESTER_COMPREHENSION_RIGHT, H2A_COMPREHENSION_ATTESTATION_BODY_KIND, H2A_DECLARATION_INTERET_BODY_KIND, H2A_ORG_MANIFEST_FILENAME, H2A_ORG_PROPOSAL_BODY_KIND, H2A_ORG_RATIFIED_BODY_KIND, H2A_ROLES, H2A_WORK_STATUSES, auditNhiPosture, buildComprehensionAttestation, canAttestComprehension, checkEnvelopeFreshness, computeHash, createEnvelope, deriveWorkspaceId, diffOrgManifest, effectiveOrgInstances, isComprehensionAttestation, nhiAttestationEnvelope, nhiInventory, deriveConnectionConfidence, nhiTrustBundle, orgAssignmentEnvelope, parseOrgManifest, signCanonical, signEnvelope, subagentAddress, validateOrgManifest, verifyComprehensionAttestation } from "@sentropic/h2a";
47
53
  import { H2A_CLAUDE_HOST } from "./hosts/claude.js";
48
54
  import { H2A_CODEX_HOST } from "./hosts/codex.js";
@@ -69,6 +75,9 @@ import { appendConductorClaim } from "./runtime/governance/claims.js";
69
75
  import { conductorLaunchCheck } from "./runtime/governance/launch-check.js";
70
76
  import { lastSpawnRequestAt, recordSpawnRequest, spawnAllowed } from "./runtime/governance/spawns.js";
71
77
  import { createObjectiveLoop, listLoopEvents, listObjectiveLoops, readObjectiveLoop } from "./runtime/loop/index.js";
78
+ import { runLoopWatch, runTick } from "./runtime/loop/engine/tick.js";
79
+ import { gatherPendingDecisions } from "./runtime/canevas/gather.js";
80
+ import { runCanevasServe } from "./runtime/canevas/serve.js";
72
81
  const HERE = dirname(fileURLToPath(import.meta.url));
73
82
  // `dist/cli.js` lives in `packages/h2a-cli/dist/`; skills are at
74
83
  // `packages/h2a-cli/skills/`. Two levels up from the dist file.
@@ -1681,16 +1690,8 @@ function cmdLoop(argv, streams) {
1681
1690
  streams.stdout.write(`${JSON.stringify({ loopId, ...(agent ? { agent } : {}), events: listLoopEvents(root, loopId) }, null, 2)}\n`);
1682
1691
  return 0;
1683
1692
  }
1684
- if (sub === "tick" || sub === "watch") {
1685
- const loopId = argv[1];
1686
- if (!loopId || loopId.startsWith("--")) {
1687
- streams.stderr.write(`h2a loop ${sub}: <loopId> is required\n`);
1688
- return 1;
1689
- }
1690
- readObjectiveLoop(root, loopId);
1691
- streams.stdout.write(`${JSON.stringify({ ok: true, loopId, action: sub, supported: false, reason: "loop orchestration is not implemented in h2a loop MVP" }, null, 2)}\n`);
1692
- return 0;
1693
- }
1693
+ // `tick` / `watch` are ASYNC (lazy runtime + periodic loop) → dispatched in
1694
+ // bin.ts via `runLoopEngineCli`, not here (cmdLoop is synchronous).
1694
1695
  }
1695
1696
  catch (error) {
1696
1697
  streams.stderr.write(`h2a loop ${sub ?? ""}: ${error.message}\n`);
@@ -1699,6 +1700,83 @@ function cmdLoop(argv, streams) {
1699
1700
  streams.stderr.write("h2a loop: subcommand required (create, list, status, agents, attach, logs, tick, watch)\n");
1700
1701
  return 1;
1701
1702
  }
1703
+ /**
1704
+ * Async dispatcher for `h2a loop tick|watch` (SLICE-1: dry-run only — produces a
1705
+ * plan via the pure core + lazy adapters, executes nothing). Called from bin.ts
1706
+ * because these are async (lazy runtime import + periodic loop). `argv` is the
1707
+ * full bin argv: `["loop", "tick"|"watch", "<loopId>", ...flags]`.
1708
+ */
1709
+ export async function runLoopEngineCli(argv, streams, signal) {
1710
+ const sub = argv[1];
1711
+ const loopId = argv[2];
1712
+ const { flags } = parseFlags(argv);
1713
+ const cwd = streams.cwd ?? (() => process.cwd());
1714
+ const root = resolveRoot(flags, cwd);
1715
+ if (!loopId || loopId.startsWith("--")) {
1716
+ streams.stderr.write(`h2a loop ${sub}: <loopId> is required\n`);
1717
+ return 1;
1718
+ }
1719
+ try {
1720
+ if (sub === "watch") {
1721
+ const intervalMs = flags["interval-ms"] ? Number(flags["interval-ms"]) : undefined;
1722
+ const max = flags.max ? Number(flags.max) : undefined;
1723
+ return await runLoopWatch(root, loopId, {
1724
+ ...(intervalMs !== undefined && Number.isFinite(intervalMs) ? { intervalMs } : {}),
1725
+ ...(max !== undefined && Number.isFinite(max) ? { max } : {}),
1726
+ ...(signal ? { signal } : {}),
1727
+ stdout: streams.stdout
1728
+ });
1729
+ }
1730
+ // tick — DRY-RUN by default; `--execute` runs the plan (tranche 1: close only).
1731
+ const execute = argv.includes("--execute");
1732
+ const { plan, exec } = await runTick(root, loopId, { execute });
1733
+ const out = exec ? { ...plan, exec } : plan;
1734
+ streams.stdout.write(`${JSON.stringify(out, null, 2)}\n`);
1735
+ return plan.outcome === "failed" ? 1 : 0;
1736
+ }
1737
+ catch (error) {
1738
+ streams.stderr.write(`h2a loop ${sub}: ${error.message}\n`);
1739
+ return classifyStoreError(error.message);
1740
+ }
1741
+ }
1742
+ // Canevas ③ — read-only tranche-1 : agrège les décisions humaines en attente
1743
+ // (enveloppes `escalate` des inbox des instances live) en un JSON stable. La vue
1744
+ // session (capturePane runtime lazy), le serveur Hono et le pont-réponse
1745
+ // (reply-envelope gardé) sont des tranches ultérieures.
1746
+ function cmdCanevas(argv, streams) {
1747
+ const { command: sub, flags } = parseFlags(argv);
1748
+ const cwd = streams.cwd ?? (() => process.cwd());
1749
+ const root = resolveRoot(flags, cwd);
1750
+ if (sub === "list") {
1751
+ try {
1752
+ const decisions = gatherPendingDecisions(root);
1753
+ streams.stdout.write(`${JSON.stringify({ kind: "canevas-decisions", version: 1, decisions }, null, 2)}\n`);
1754
+ return 0;
1755
+ }
1756
+ catch (error) {
1757
+ streams.stderr.write(`h2a canevas list: ${error.message}\n`);
1758
+ return classifyStoreError(error.message);
1759
+ }
1760
+ }
1761
+ streams.stderr.write("h2a canevas: subcommand required (list, serve)\n");
1762
+ return 1;
1763
+ }
1764
+ /**
1765
+ * Async dispatcher for `h2a canevas serve` (read-only Hono server on 127.0.0.1).
1766
+ * Called from bin.ts (async + long-running). `argv` = full bin argv.
1767
+ */
1768
+ export async function runCanevasServeCli(argv, streams, signal) {
1769
+ const { flags } = parseFlags(argv);
1770
+ const cwd = streams.cwd ?? (() => process.cwd());
1771
+ const root = resolveRoot(flags, cwd);
1772
+ const port = flags.port ? Number(flags.port) : undefined;
1773
+ return runCanevasServe({
1774
+ root,
1775
+ ...(port !== undefined && Number.isFinite(port) ? { port } : {}),
1776
+ stderr: streams.stderr,
1777
+ ...(signal ? { signal } : {})
1778
+ });
1779
+ }
1702
1780
  function cmdDrumbeat(argv, streams) {
1703
1781
  // DEC-086 (D2): durable anti-stall registry — record stops, scan candidates,
1704
1782
  // clear. The long-running `watch` daemon is async (dispatched from bin.ts).
@@ -4535,10 +4613,43 @@ export async function cmdKeepalive(flags, streams) {
4535
4613
  // P3 (façade track) — les verbes track sont délégués à la CLI `@sentropic/track`
4536
4614
  // par shell-out (le store `.track` est append-only single-writer : pas de 2ᵉ writer).
4537
4615
  // Verbes spécifiques, sans namespace `track` (dissous comme host/sub).
4538
- const TRACK_FACADE_VERBS = new Set([
4616
+ export const TRACK_FACADE_VERBS = new Set([
4539
4617
  "decision", "report", "accept", "blocker", "item", "query",
4540
4618
  "consolidate", "priority", "branch", "focus", "ingest", "restructure"
4541
4619
  ]);
4620
+ // ④ de-spawn — a strict SUBSET of the facade verbs routed IN-PROCESS via
4621
+ // `@sentropic/track`'s `runCli`, no child process. Every native verb is proven
4622
+ // SYNC in the pinned track (dispatches through runCli's synchronous switch and
4623
+ // returns a `number`); the ONLY async verb, `focus` (dynamic
4624
+ // `import('@sentropic/focus')` → Promise), is deliberately excluded and keeps
4625
+ // shelling out. All paths resolve the SAME `.track` (native uses
4626
+ // `process.cwd()`, the spawn inherits it) and share track's single O_EXCL lock —
4627
+ // no split-brain, no second writer.
4628
+ //
4629
+ // The native set is split READ-ONLY vs WRITE because the throw-fallback rule
4630
+ // differs (RISK: double-write, see `delegateToTrackNative`):
4631
+ // • READ-ONLY (`query`/`report`) — a native throw wrote nothing, so falling
4632
+ // back to the spawn facade is safe.
4633
+ // • WRITE (the rest) — track's `appendCommand` is ATOMIC under the O_EXCL lock
4634
+ // (read→validate→append→writeHead→verify): it throws either BEFORE the
4635
+ // append (validation → nothing written) or AFTER a landed write (the
4636
+ // verify receipt). A spawn fallback on that throw could DOUBLE-WRITE, so a
4637
+ // write NEVER falls back on throw — it propagates the error (rc≠0 + stderr).
4638
+ export const TRACK_NATIVE_READONLY_VERBS = new Set(["query", "report"]);
4639
+ // ④ tranche-2 — the SYNC write verbs, de-spawned in-process. Each dispatches
4640
+ // through runCli's mutating switch as a plain sync `cmd…` returning a `number`.
4641
+ // `focus` is absent (async); `restructure`/`ingest` are sync file-plan applies.
4642
+ export const TRACK_NATIVE_WRITE_VERBS = new Set([
4643
+ "decision", "item", "accept", "blocker",
4644
+ "consolidate", "priority", "branch", "ingest", "restructure"
4645
+ ]);
4646
+ // Union of the in-process verbs (read-only + sync writes). `focus` (async) is
4647
+ // NOT here and keeps the spawn facade. Kept exported as the single "is this verb
4648
+ // native?" predicate used by the dispatcher and the contract tests.
4649
+ export const TRACK_NATIVE_VERBS = new Set([
4650
+ ...TRACK_NATIVE_READONLY_VERBS,
4651
+ ...TRACK_NATIVE_WRITE_VERBS
4652
+ ]);
4542
4653
  function resolveTrackBin() {
4543
4654
  // Le champ `exports` de @sentropic/track bloque l'accès à ./package.json,
4544
4655
  // donc on résout l'entrée puis on remonte jusqu'au package.json du package.
@@ -4568,12 +4679,85 @@ function delegateToTrack(argv, streams) {
4568
4679
  return 1;
4569
4680
  }
4570
4681
  }
4682
+ /**
4683
+ * ④ — run a track verb IN-PROCESS via `@sentropic/track`'s `runCli`.
4684
+ *
4685
+ * Split-brain mitigation (RISK #1): the native call resolves `.track` from
4686
+ * `process.cwd()` — the SAME cwd the spawn facade inherits (`spawnSync` with no
4687
+ * `cwd` option) — so both paths bind the identical store, and track's own
4688
+ * O_EXCL lock still serialises them. We deliberately do NOT wrap the call in any
4689
+ * h2a file-lock: track's lock is not re-entrant, and the facade never took one.
4690
+ *
4691
+ * Output is BUFFERED, then flushed only on a synchronous result.
4692
+ *
4693
+ * Two escape hatches, and the DOUBLE-WRITE rule (RISK #2) that separates them:
4694
+ * • `runCli` returns a Promise — an async verb (e.g. `focus`) that has NOT yet
4695
+ * written synchronously. The buffer is discarded and we fall back to the
4696
+ * spawn facade. Safe for reads AND writes; unreachable for the shipped set
4697
+ * (only sync verbs are routed here), so it is a defensive belt.
4698
+ * • `runCli` throws — an UNEXPECTED escape from track's own try/catch. For a
4699
+ * `readOnly` verb nothing was written, so we fall back to the spawn facade.
4700
+ * For a WRITE verb we MUST NOT re-run: track's `appendCommand` is atomic
4701
+ * (throws BEFORE the append on validation, or AFTER a landed write on the
4702
+ * verify receipt), so a spawn retry could double-write. We flush the buffered
4703
+ * native output and propagate a non-zero rc + stderr instead.
4704
+ */
4705
+ function delegateToTrackNative(argv, streams, readOnly) {
4706
+ const outBuf = [];
4707
+ const errBuf = [];
4708
+ const io = {
4709
+ // CRITICAL: same root as the spawn facade inherits. Do NOT swap in
4710
+ // `streams.cwd` here — divergence would split-brain the two paths.
4711
+ cwd: process.cwd(),
4712
+ out: (s) => {
4713
+ outBuf.push(s);
4714
+ },
4715
+ err: (s) => {
4716
+ errBuf.push(s);
4717
+ }
4718
+ };
4719
+ try {
4720
+ const rc = runTrackCli([...argv], io);
4721
+ // Async verb (Promise) — nothing written synchronously yet, so a spawn
4722
+ // fallback cannot double-write. Not representable for the shipped sync set.
4723
+ if (rc !== null && typeof rc === "object" && typeof rc.then === "function") {
4724
+ return delegateToTrack(argv, streams);
4725
+ }
4726
+ for (const s of outBuf)
4727
+ streams.stdout.write(s);
4728
+ for (const s of errBuf)
4729
+ streams.stderr.write(s);
4730
+ return rc;
4731
+ }
4732
+ catch (err) {
4733
+ // READ-ONLY throw: nothing persisted → the spawn facade may safely re-run.
4734
+ if (readOnly) {
4735
+ return delegateToTrack(argv, streams);
4736
+ }
4737
+ // WRITE throw (anti-double-write): DO NOT re-run. `appendCommand` is atomic,
4738
+ // so either nothing landed or the write already landed — a spawn retry could
4739
+ // append a second event. Flush what the native path emitted and fail loud.
4740
+ for (const s of outBuf)
4741
+ streams.stdout.write(s);
4742
+ for (const s of errBuf)
4743
+ streams.stderr.write(s);
4744
+ streams.stderr.write(`h2a ${argv[0]}: track write failed in-process (${err.message}). ` +
4745
+ `No spawn retry — the store append is atomic; a retry could double-write.\n`);
4746
+ return 1;
4747
+ }
4748
+ }
4571
4749
  export function runCli(argv = process.argv.slice(2), streams = {
4572
4750
  stdout: process.stdout,
4573
4751
  stderr: process.stderr
4574
4752
  }) {
4575
4753
  const { command, flags } = parseFlags(argv);
4576
4754
  if (command && TRACK_FACADE_VERBS.has(command)) {
4755
+ // ④: read-only + sync-write verbs run in-process; `focus` (async) keeps
4756
+ // shelling out. Same `.track`, same lock. A native WRITE never falls back to
4757
+ // spawn on throw (anti-double-write) — hence the read-only flag below.
4758
+ if (TRACK_NATIVE_VERBS.has(command)) {
4759
+ return delegateToTrackNative(argv, streams, TRACK_NATIVE_READONLY_VERBS.has(command));
4760
+ }
4577
4761
  return delegateToTrack(argv, streams);
4578
4762
  }
4579
4763
  if (!command || command === "--help" || command === "-h" || command === "help") {
@@ -4598,6 +4782,8 @@ export function runCli(argv = process.argv.slice(2), streams = {
4598
4782
  return cmdSubagent(argv.slice(1), streams);
4599
4783
  if (command === "loop")
4600
4784
  return cmdLoop(argv.slice(1), streams);
4785
+ if (command === "canevas")
4786
+ return cmdCanevas(argv.slice(1), streams);
4601
4787
  if (command === "drumbeat") {
4602
4788
  const sub = argv[1];
4603
4789
  if (sub === "watch") {