@sentropic/h2a 0.88.0 → 0.90.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 (63) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/dist/bin.js +0 -12
  4. package/dist/bin.js.map +1 -1
  5. package/dist/cli-command-map.d.ts.map +1 -1
  6. package/dist/cli-command-map.js +1 -2
  7. package/dist/cli-command-map.js.map +1 -1
  8. package/dist/cli-contract.d.ts.map +1 -1
  9. package/dist/cli-contract.js +10 -18
  10. package/dist/cli-contract.js.map +1 -1
  11. package/dist/cli.d.ts +7 -0
  12. package/dist/cli.d.ts.map +1 -1
  13. package/dist/cli.js +105 -60
  14. package/dist/cli.js.map +1 -1
  15. package/dist/index.d.ts +1 -1
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +1 -1
  18. package/dist/index.js.map +1 -1
  19. package/dist/runtime/local-files/index.d.ts +1 -1
  20. package/dist/runtime/local-files/index.d.ts.map +1 -1
  21. package/dist/runtime/local-files/index.js.map +1 -1
  22. package/dist/runtime/local-files/paths.d.ts +27 -9
  23. package/dist/runtime/local-files/paths.d.ts.map +1 -1
  24. package/dist/runtime/local-files/paths.js +78 -18
  25. package/dist/runtime/local-files/paths.js.map +1 -1
  26. package/dist/runtime/local-files/store.js +1 -1
  27. package/dist/runtime/local-files/store.js.map +1 -1
  28. package/dist/runtime/loop/engine/adapters.js +1 -1
  29. package/dist/runtime/loop/engine/adapters.js.map +1 -1
  30. package/dist/runtime/loop/index.d.ts +2 -0
  31. package/dist/runtime/loop/index.d.ts.map +1 -1
  32. package/dist/runtime/loop/index.js +25 -0
  33. package/dist/runtime/loop/index.js.map +1 -1
  34. package/dist/runtime/mcp/handlers.d.ts +2 -1
  35. package/dist/runtime/mcp/handlers.d.ts.map +1 -1
  36. package/dist/runtime/mcp/handlers.js +81 -33
  37. package/dist/runtime/mcp/handlers.js.map +1 -1
  38. package/dist/runtime/mcp/server.js +1 -1
  39. package/dist/runtime/mcp/server.js.map +1 -1
  40. package/dist/runtime/mcp/tools.d.ts.map +1 -1
  41. package/dist/runtime/mcp/tools.js +2 -1
  42. package/dist/runtime/mcp/tools.js.map +1 -1
  43. package/dist/runtime/mirror/serve.d.ts.map +1 -1
  44. package/dist/runtime/mirror/serve.js +4 -12
  45. package/dist/runtime/mirror/serve.js.map +1 -1
  46. package/dist/runtime/reporting/index.d.ts +0 -2
  47. package/dist/runtime/reporting/index.d.ts.map +1 -1
  48. package/dist/runtime/reporting/index.js +0 -2
  49. package/dist/runtime/reporting/index.js.map +1 -1
  50. package/package.json +3 -2
  51. package/skills/harness/track-report/SKILL.md +22 -12
  52. package/dist/runtime/reporting/config.d.ts +0 -21
  53. package/dist/runtime/reporting/config.d.ts.map +0 -1
  54. package/dist/runtime/reporting/config.js +0 -76
  55. package/dist/runtime/reporting/config.js.map +0 -1
  56. package/dist/runtime/reporting/report-ai.d.ts +0 -49
  57. package/dist/runtime/reporting/report-ai.d.ts.map +0 -1
  58. package/dist/runtime/reporting/report-ai.js +0 -326
  59. package/dist/runtime/reporting/report-ai.js.map +0 -1
  60. package/dist/runtime/reporting/stdin.d.ts +0 -2
  61. package/dist/runtime/reporting/stdin.d.ts.map +0 -1
  62. package/dist/runtime/reporting/stdin.js +0 -8
  63. package/dist/runtime/reporting/stdin.js.map +0 -1
package/dist/cli.js CHANGED
@@ -74,7 +74,7 @@ import { renderK8sSidecar } from "./runtime/deploy/k8s-sidecar.js";
74
74
  import { renderK8sTenant } from "./runtime/deploy/k8s-tenant.js";
75
75
  import { remoteServerForStore, sendRemoteEnvelope } from "./runtime/remote/index.js";
76
76
  import { buildInstanceMirror, isPushableHttpUrl, mirrorPushGloballyDisabled, mirrorServerForStore, redactEndpoint, runMirrorPushDaemon, MIN_MIRROR_PUSH_INTERVAL_MS, MIRROR_PUSH_INVALID_URL_MESSAGE, MIRROR_PUSH_OFF_ENV } from "./runtime/mirror/index.js";
77
- import { recordStop, scanDrumbeat, clearDrumbeatEntry, runDrumbeatWatch as runDrumbeatWatchLoop, loggingRelauncher, localTmuxRelauncher, headlessRelauncher, remoteRelauncher, chainRelauncher, relanceFromInbox, loggingDecider, subagentDecider, H2A_DEFAULT_MAX_RELANCES } from "./runtime/drumbeat/index.js";
77
+ import { recordStop, scanDrumbeat, listDrumbeat, clearDrumbeatEntry, runDrumbeatWatch as runDrumbeatWatchLoop, loggingRelauncher, localTmuxRelauncher, headlessRelauncher, remoteRelauncher, chainRelauncher, relanceFromInbox, loggingDecider, subagentDecider, H2A_DEFAULT_MAX_RELANCES } from "./runtime/drumbeat/index.js";
78
78
  import { gatherNhiSnapshot } from "./runtime/nhi.js";
79
79
  import { raiseBlockage, listBlockages, resolveBlockage } from "./runtime/blockage/index.js";
80
80
  import { recordEscalation, listEscalations, clearEscalation } from "./runtime/escalation/index.js";
@@ -87,12 +87,12 @@ import { conductorFor } from "./runtime/governance/conductor.js";
87
87
  import { appendConductorClaim } from "./runtime/governance/claims.js";
88
88
  import { conductorLaunchCheck } from "./runtime/governance/launch-check.js";
89
89
  import { lastSpawnRequestAt, recordSpawnRequest, spawnAllowed } from "./runtime/governance/spawns.js";
90
- import { createObjectiveLoop, declareObjectiveLoopDone, joinObjectiveLoop, listLoopEvents, listObjectiveLoops, readObjectiveLoop, reportObjectiveLoop, stopObjectiveLoop, validateLoopLaunchSpec } from "./runtime/loop/index.js";
90
+ import { createObjectiveLoop, declareObjectiveLoopDone, enableObjectiveLoopAutoTick, joinObjectiveLoop, listLoopEvents, listObjectiveLoops, readObjectiveLoop, reportObjectiveLoop, stopObjectiveLoop, validateLoopLaunchSpec } from "./runtime/loop/index.js";
91
91
  import { runLoopWatch, runTick } from "./runtime/loop/engine/tick.js";
92
92
  import { runLoopSupervisor } from "./runtime/loop/supervisor.js";
93
93
  import { gatherPendingDecisions } from "./runtime/canevas/gather.js";
94
94
  import { runCanevasServe } from "./runtime/canevas/serve.js";
95
- import { installTrackReportAiConfig, readH2AReportContext, TrackReportAiConfigConflictError } from "./runtime/reporting/index.js";
95
+ import { readH2AReportContext } from "./runtime/reporting/index.js";
96
96
  const HERE = dirname(fileURLToPath(import.meta.url));
97
97
  // `dist/cli.js` lives in `packages/h2a-cli/dist/`; skills are at
98
98
  // `packages/h2a-cli/skills/`. Two levels up from the dist file.
@@ -246,7 +246,8 @@ export function renderCliHelp() {
246
246
  " h2a install-skills --host <claude|codex|gemini|agy> [--scope user|project] [--force]",
247
247
  " h2a deploy k8s-sidecar [--instance <id>] [--host <h>] [--root <path>] [--image <ref>] [--cli-version <ver>] [--write <file>]",
248
248
  " h2a deploy k8s-tenant [--namespace <ns>] [--root <path>] [--replicas <n>] [--storage <size>] [--storage-class <sc>] [--lease-ms <ms>] [--image <ref>] [--cli-version <ver>] [--write <file>]",
249
- " h2a loop create --name <n> --goal <text> [--repo <path[:role]>] [--track <json>] [--agent <host:role:placement> [--launch-stdin]] [--root <path>]",
249
+ " h2a loop create --name <n> --goal <text> [--auto-tick] [--repo <path[:role]>] [--track <json>] [--agent <host:role:placement> [--launch-stdin]] [--root <path>]",
250
+ " h2a loop enable-auto-tick <loopId> [--root <path>]",
250
251
  " h2a loop join <loopId> --instance <id> [--agent-id <id>] [--role <role>] [--launch-stdin] [--root <path>]",
251
252
  " h2a loop report <loopId> --note <text> [--instance <id>] [--agent-id <id>] [--auto-join] [--root <path>]",
252
253
  " h2a loop done <loopId> [--note <text>] [--instance <id>] [--agent-id <id>] [--root <path>]",
@@ -258,10 +259,8 @@ export function renderCliHelp() {
258
259
  " h2a loop logs <loopId> [--agent <selector>] [--root <path>]",
259
260
  " h2a loop supervise [--interval-ms <n>] [--root <path>]",
260
261
  "",
261
- "Track AI adapters (leaf commands; never call Track report/snapshot):",
262
+ "Legacy Track context projection (leaf command; never calls Track report/snapshot):",
262
263
  " h2a report-context --workspace-root <absolute-path> [--root <h2a-store>]",
263
- " h2a report-ai --model <model> --effort <low|medium|high|xhigh> --gateway required (Track context envelope on stdin; one no-tools local-gateway request)",
264
- " h2a report-ai install-track-config [--force] (atomic user XDG config, mode 0600; preserves overrides by default)",
265
264
  "",
266
265
  "Focus Web (packaged production app):",
267
266
  " h2a focus serve [--repo <path>] [--track-events <path>] [--host <host>] [--port <0-65535>]",
@@ -415,6 +414,11 @@ function cmdRegister(flags, streams) {
415
414
  const cwd = streams.cwd ?? (() => process.cwd());
416
415
  const root = resolveRoot(flags, cwd);
417
416
  const store = createLocalStore({ root });
417
+ if (store.findInstance(registration.id)) {
418
+ const message = `Instance already registered: ${registration.id}`;
419
+ streams.stderr.write(`h2a register: ${message}\n`);
420
+ return classifyStoreError(message);
421
+ }
418
422
  try {
419
423
  store.registerInstance(registration);
420
424
  }
@@ -561,32 +565,37 @@ function cmdMailbox(argv, mailbox, streams) {
561
565
  streams.stderr.write(`h2a ${mailbox} put: invalid JSON (${error.message})\n`);
562
566
  return 1;
563
567
  }
568
+ let inboxRecipient = flags.instance;
569
+ let inboxResolution;
564
570
  if (mailbox === "inbox") {
565
- try {
566
- assertHostQualifiedAddress(flags.instance, "recipient");
567
- }
568
- catch (error) {
569
- streams.stderr.write(`\n${error.message}\n`);
570
- return 1;
571
- }
572
- // WP-2: resolve-before-send — legibility gate (does NOT change destination).
571
+ // WP-2/DOC-03: a display name resolves only to the presence's full
572
+ // instance; a bare display name is never an inbox destination.
573
573
  const root = resolveRoot(flags, streams.cwd ?? (() => process.cwd()));
574
- const liveSessions = listPresence(root).map((s) => s.instance);
574
+ const liveSessions = listPresence(root);
575
575
  const registeredIds = store.listInstances().map((i) => i.instance ?? i.id);
576
- const resolution = resolveRecipient({
576
+ inboxResolution = resolveRecipient({
577
577
  target: flags.instance,
578
578
  liveInstances: liveSessions,
579
579
  registeredInstances: registeredIds
580
580
  });
581
- if (resolution.kind === "refuse") {
582
- streams.stderr.write(`\nh2a: ${resolution.reason}${resolution.candidates ? `\ncandidates: ${resolution.candidates.join(", ")}` : ""}\n`);
581
+ if (inboxResolution.kind === "refuse") {
582
+ streams.stderr.write(`\nh2a: ${inboxResolution.reason}${inboxResolution.candidates ? `\ncandidates: ${inboxResolution.candidates.join(", ")}` : ""}\n`);
583
+ return 1;
584
+ }
585
+ if (inboxResolution.kind === "deliver-resolved") {
586
+ inboxRecipient = inboxResolution.recipient;
587
+ }
588
+ try {
589
+ assertHostQualifiedAddress(inboxRecipient, "recipient");
590
+ }
591
+ catch (error) {
592
+ streams.stderr.write(`\n${error.message}\n`);
583
593
  return 1;
584
594
  }
585
- // deliver / deliver-dormant / deliver-hint → proceed with put below.
586
595
  }
587
596
  try {
588
597
  if (mailbox === "inbox") {
589
- store.putInboxMessage(flags.instance, envelope);
598
+ store.putInboxMessage(inboxRecipient, envelope);
590
599
  }
591
600
  else {
592
601
  store.putOutboxMessage(flags.instance, envelope);
@@ -594,26 +603,19 @@ function cmdMailbox(argv, mailbox, streams) {
594
603
  // Bug-2 backstop: tell the caller whether the recipient is actually live
595
604
  // (the write always succeeds — a dormant deposit-for-wake is legitimate).
596
605
  const recipientLive = mailbox === "inbox"
597
- ? listPresence(store.paths.root).some((s) => canonicalAddress(s.instance) === canonicalAddress(flags.instance))
606
+ ? listPresence(store.paths.root).some((s) => canonicalAddress(s.instance) === canonicalAddress(inboxRecipient))
598
607
  : undefined;
599
608
  // WP-2: enrich stdout with resolution metadata.
600
609
  let resolutionMeta = {};
601
- if (mailbox === "inbox") {
602
- const root2 = resolveRoot(flags, streams.cwd ?? (() => process.cwd()));
603
- const liveSessions2 = listPresence(root2).map((s) => s.instance);
604
- const registeredIds2 = store.listInstances().map((i) => i.instance ?? i.id);
605
- const resolution2 = resolveRecipient({
606
- target: flags.instance,
607
- liveInstances: liveSessions2,
608
- registeredInstances: registeredIds2
609
- });
610
+ if (inboxResolution) {
610
611
  resolutionMeta = {
611
- resolution: resolution2.kind,
612
- ...(resolution2.kind === "deliver-hint" ? { liveCandidate: resolution2.liveCandidate, reason: resolution2.reason } : {}),
613
- ...(resolution2.kind === "deliver-dormant" ? { reason: resolution2.reason, dormant: true } : {})
612
+ resolution: inboxResolution.kind,
613
+ ...(inboxResolution.kind === "deliver-hint" ? { liveCandidate: inboxResolution.liveCandidate, reason: inboxResolution.reason } : {}),
614
+ ...(inboxResolution.kind === "deliver-resolved" ? { recipient: inboxResolution.recipient, reason: inboxResolution.reason } : {}),
615
+ ...(inboxResolution.kind === "deliver-dormant" ? { reason: inboxResolution.reason, dormant: true } : {})
614
616
  };
615
617
  }
616
- streams.stdout.write(`${JSON.stringify({ ok: true, id: envelope.id, mailbox, instance: flags.instance, ...(recipientLive !== undefined ? { recipientLive } : {}), ...resolutionMeta }, null, 2)}\n`);
618
+ streams.stdout.write(`${JSON.stringify({ ok: true, id: envelope.id, mailbox, instance: mailbox === "inbox" ? inboxRecipient : flags.instance, ...(recipientLive !== undefined ? { recipientLive } : {}), ...resolutionMeta }, null, 2)}\n`);
617
619
  return 0;
618
620
  }
619
621
  catch (error) {
@@ -626,7 +628,29 @@ function cmdMailbox(argv, mailbox, streams) {
626
628
  }
627
629
  }
628
630
  if (sub === "read") {
629
- const messages = mailbox === "inbox" ? store.readInbox(flags.instance) : store.readOutbox(flags.instance);
631
+ let recipient = flags.instance;
632
+ if (mailbox === "inbox") {
633
+ const liveSessions = listPresence(root);
634
+ const registeredIds = store.listInstances().map((i) => i.instance ?? i.id);
635
+ const resolution = resolveRecipient({
636
+ target: flags.instance,
637
+ liveInstances: liveSessions,
638
+ registeredInstances: registeredIds,
639
+ operation: "read"
640
+ });
641
+ if (resolution.kind === "list") {
642
+ streams.stdout.write(`${JSON.stringify({ candidates: resolution.candidates, reason: resolution.reason }, null, 2)}\n`);
643
+ return 0;
644
+ }
645
+ if (resolution.kind === "refuse") {
646
+ streams.stderr.write(`\nh2a: ${resolution.reason}${resolution.candidates ? `\ncandidates: ${resolution.candidates.join(", ")}` : ""}\n`);
647
+ return 1;
648
+ }
649
+ if (resolution.kind === "deliver-resolved") {
650
+ recipient = resolution.recipient;
651
+ }
652
+ }
653
+ const messages = mailbox === "inbox" ? store.readInbox(recipient) : store.readOutbox(flags.instance);
630
654
  streams.stdout.write(`${JSON.stringify(messages, null, 2)}\n`);
631
655
  return 0;
632
656
  }
@@ -635,7 +659,19 @@ function cmdMailbox(argv, mailbox, streams) {
635
659
  streams.stderr.write("h2a inbox pop: --envelope <id> required\n");
636
660
  return 1;
637
661
  }
638
- const popped = store.popInboxMessage(flags.instance, flags.envelope);
662
+ const liveSessions = listPresence(root);
663
+ const registeredIds = store.listInstances().map((i) => i.instance ?? i.id);
664
+ const resolution = resolveRecipient({
665
+ target: flags.instance,
666
+ liveInstances: liveSessions,
667
+ registeredInstances: registeredIds
668
+ });
669
+ if (resolution.kind === "refuse") {
670
+ streams.stderr.write(`\nh2a: ${resolution.reason}${resolution.candidates ? `\ncandidates: ${resolution.candidates.join(", ")}` : ""}\n`);
671
+ return 1;
672
+ }
673
+ const recipient = resolution.kind === "deliver-resolved" ? resolution.recipient : flags.instance;
674
+ const popped = store.popInboxMessage(recipient, flags.envelope);
639
675
  if (!popped) {
640
676
  streams.stderr.write(`h2a inbox pop: no such envelope ${flags.envelope}\n`);
641
677
  // State conflict against the local store (the envelope is not where the
@@ -1877,11 +1913,22 @@ function cmdLoop(argv, streams) {
1877
1913
  goal: flags.goal,
1878
1914
  repos,
1879
1915
  refs,
1880
- agents
1916
+ agents,
1917
+ ...(flags["auto-tick"] === "true" ? { policy: { autoTick: true } } : {})
1881
1918
  });
1882
1919
  streams.stdout.write(`${JSON.stringify(loop, null, 2)}\n`);
1883
1920
  return 0;
1884
1921
  }
1922
+ if (sub === "enable-auto-tick") {
1923
+ const loopId = argv[1];
1924
+ if (!loopId || loopId.startsWith("--")) {
1925
+ streams.stderr.write("h2a loop enable-auto-tick: <loopId> is required\n");
1926
+ return 1;
1927
+ }
1928
+ const loop = enableObjectiveLoopAutoTick(root, loopId);
1929
+ streams.stdout.write(`${JSON.stringify(loop, null, 2)}\n`);
1930
+ return 0;
1931
+ }
1885
1932
  if (sub === "list") {
1886
1933
  streams.stdout.write(`${JSON.stringify(listObjectiveLoops(root), null, 2)}\n`);
1887
1934
  return 0;
@@ -1999,7 +2046,7 @@ function cmdLoop(argv, streams) {
1999
2046
  streams.stderr.write(`h2a loop ${sub ?? ""}: ${error.message}\n`);
2000
2047
  return classifyStoreError(error.message);
2001
2048
  }
2002
- streams.stderr.write("h2a loop: subcommand required (create, join, report, done, stop, list, status, agents, attach, logs, tick, watch/run, supervise)\n");
2049
+ streams.stderr.write("h2a loop: subcommand required (create, enable-auto-tick, join, report, done, stop, list, status, agents, attach, logs, tick, watch/run, supervise)\n");
2003
2050
  return 1;
2004
2051
  }
2005
2052
  /**
@@ -2848,6 +2895,16 @@ export async function runDrumbeatRelanceInbox(flags, io = {
2848
2895
  io.stdout.write(`${JSON.stringify({ ok: true, ...result }, null, 2)}\n`);
2849
2896
  return 0;
2850
2897
  }
2898
+ /**
2899
+ * D4 resume messages can only act on a durable, non-terminal stop record.
2900
+ * Keeping this projection bounded prevents one huge historical registry from
2901
+ * starving every drumbeat tick before its anti-stall scan runs.
2902
+ */
2903
+ export function drumbeatResumeInboxTargets(entries) {
2904
+ return entries
2905
+ .filter((entry) => entry.workStatus !== "done" && entry.terminal === undefined)
2906
+ .map((entry) => entry.instance);
2907
+ }
2851
2908
  /**
2852
2909
  * `h2a drumbeat watch` (DEC-086): long-running anti-stall daemon. Async +
2853
2910
  * blocking, dispatched from bin.ts like mcp-serve. Uses the logging relauncher
@@ -2951,7 +3008,16 @@ export async function runDrumbeatWatch(flags, io = {
2951
3008
  ...(flags.instance !== undefined ? { selfInstance: flags.instance } : {}),
2952
3009
  log,
2953
3010
  beforeScan: async () => {
3011
+ // A D4 `drumbeat.resume` is actionable only for an already-recorded,
3012
+ // non-terminal stop entry. Scanning every registered instance here was
3013
+ // O(registry × inbox) on every beat: a shared bus with tens of thousands
3014
+ // of historical identities never reached its anti-stall scan at all.
3015
+ // Restrict the inbox pass to the bounded stop registry; messages for an
3016
+ // actor without a stop entry cannot be safely relanced and remain in its
3017
+ // inbox for ordinary handling instead of starving the watchdog.
3018
+ const resumeTargets = drumbeatResumeInboxTargets(listDrumbeat(root));
2954
3019
  const result = await relanceFromInbox(root, {
3020
+ instances: resumeTargets,
2955
3021
  relauncher: buildLocalInboxRelauncher(kind, log)
2956
3022
  });
2957
3023
  if (result.relanced.length || result.skipped.length) {
@@ -5471,21 +5537,6 @@ function cmdReportContext(flags, streams) {
5471
5537
  return 3;
5472
5538
  }
5473
5539
  }
5474
- function cmdInstallTrackReportAiConfig(flags, streams) {
5475
- try {
5476
- const result = installTrackReportAiConfig({ force: flags.force === "true" });
5477
- streams.stdout.write(`${JSON.stringify({ ok: true, ...result })}\n`);
5478
- return 0;
5479
- }
5480
- catch (err) {
5481
- if (err instanceof TrackReportAiConfigConflictError) {
5482
- streams.stderr.write(`h2a report-ai install-track-config: ${err.message}\n`);
5483
- return 2;
5484
- }
5485
- streams.stderr.write(`h2a report-ai install-track-config: ${err.message}\n`);
5486
- return 3;
5487
- }
5488
- }
5489
5540
  function resolveTrackBin() {
5490
5541
  // Le champ `exports` de @sentropic/track bloque l'accès à ./package.json,
5491
5542
  // donc on résout l'entrée puis on remonte jusqu'au package.json du package.
@@ -5645,12 +5696,6 @@ export function runCli(argv = process.argv.slice(2), streams = {
5645
5696
  }
5646
5697
  if (command === "report-context")
5647
5698
  return cmdReportContext(flags, streams);
5648
- if (command === "report-ai") {
5649
- if (argv[1] === "install-track-config")
5650
- return cmdInstallTrackReportAiConfig(flags, streams);
5651
- streams.stderr.write("h2a report-ai: async adapter command — run via the h2a binary\n");
5652
- return 1;
5653
- }
5654
5699
  if (command === "init")
5655
5700
  return cmdInit(flags, streams);
5656
5701
  if (command === "register")