harnery 0.42.0 → 0.43.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 (77) hide show
  1. package/dist/commander.d.ts +16 -0
  2. package/dist/commander.d.ts.map +1 -1
  3. package/dist/commander.js +18 -0
  4. package/dist/commands/agents.d.ts.map +1 -1
  5. package/dist/commands/agents.js +54 -4
  6. package/dist/commands/tunnel.d.ts +65 -0
  7. package/dist/commands/tunnel.d.ts.map +1 -1
  8. package/dist/commands/tunnel.js +290 -101
  9. package/dist/commands/web-fetch.d.ts +3 -2
  10. package/dist/commands/web-fetch.d.ts.map +1 -1
  11. package/dist/commands/web-fetch.js +25 -19
  12. package/dist/commands/web.d.ts +7 -1
  13. package/dist/commands/web.d.ts.map +1 -1
  14. package/dist/commands/web.js +9 -3
  15. package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
  16. package/dist/core/agents/render/prompt-context.js +11 -5
  17. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  18. package/dist/core/agents/render/session-context.js +57 -13
  19. package/dist/core/agents/state/names.d.ts +43 -3
  20. package/dist/core/agents/state/names.d.ts.map +1 -1
  21. package/dist/core/agents/state/names.js +87 -3
  22. package/dist/core/events/v3/support-storage/pack-reader.d.ts.map +1 -1
  23. package/dist/core/events/v3/support-storage/pack-reader.js +42 -16
  24. package/dist/core/hooks/cli.js +87 -18
  25. package/dist/core/hooks/fork-lineage.d.ts +40 -0
  26. package/dist/core/hooks/fork-lineage.d.ts.map +1 -0
  27. package/dist/core/hooks/fork-lineage.js +62 -0
  28. package/dist/core/hooks/resolve/transcript.d.ts +16 -0
  29. package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
  30. package/dist/core/hooks/resolve/transcript.js +84 -0
  31. package/dist/core/hooks/shell-rm.d.ts +2 -0
  32. package/dist/core/hooks/shell-rm.d.ts.map +1 -0
  33. package/dist/core/hooks/shell-rm.js +105 -0
  34. package/dist/core/hooks/shell-tokens.d.ts +12 -0
  35. package/dist/core/hooks/shell-tokens.d.ts.map +1 -0
  36. package/dist/core/hooks/shell-tokens.js +97 -0
  37. package/dist/core/hooks/shell-waiter.d.ts.map +1 -1
  38. package/dist/core/hooks/shell-waiter.js +3 -93
  39. package/dist/core/semantic/contract.d.ts +5 -5
  40. package/dist/core/storage/process-log.d.ts +12 -0
  41. package/dist/core/storage/process-log.d.ts.map +1 -1
  42. package/dist/core/storage/process-log.js +10 -2
  43. package/dist/lib/tunnel/allowlist.d.ts +44 -0
  44. package/dist/lib/tunnel/allowlist.d.ts.map +1 -0
  45. package/dist/lib/tunnel/allowlist.js +204 -0
  46. package/dist/lib/tunnel/current-address.d.ts +41 -0
  47. package/dist/lib/tunnel/current-address.d.ts.map +1 -0
  48. package/dist/lib/tunnel/current-address.js +123 -0
  49. package/dist/lib/tunnel/gate.js +13 -6
  50. package/dist/lib/tunnel/processes.d.ts +39 -0
  51. package/dist/lib/tunnel/processes.d.ts.map +1 -0
  52. package/dist/lib/tunnel/processes.js +121 -0
  53. package/dist/lib/tunnel/state.d.ts +28 -3
  54. package/dist/lib/tunnel/state.d.ts.map +1 -1
  55. package/dist/lib/tunnel/state.js +98 -27
  56. package/package.json +2 -1
  57. package/src/commander.ts +32 -0
  58. package/src/commands/agents.ts +97 -11
  59. package/src/commands/tunnel.ts +394 -116
  60. package/src/commands/web-fetch.ts +30 -19
  61. package/src/commands/web.ts +9 -3
  62. package/src/core/agents/render/prompt-context.ts +25 -8
  63. package/src/core/agents/render/session-context.ts +83 -16
  64. package/src/core/agents/state/names.ts +117 -6
  65. package/src/core/events/v3/support-storage/pack-reader.ts +45 -19
  66. package/src/core/hooks/cli.ts +106 -17
  67. package/src/core/hooks/fork-lineage.ts +82 -0
  68. package/src/core/hooks/resolve/transcript.ts +88 -0
  69. package/src/core/hooks/shell-rm.ts +103 -0
  70. package/src/core/hooks/shell-tokens.ts +96 -0
  71. package/src/core/hooks/shell-waiter.ts +4 -91
  72. package/src/core/storage/process-log.ts +17 -2
  73. package/src/lib/tunnel/allowlist.ts +208 -0
  74. package/src/lib/tunnel/current-address.ts +158 -0
  75. package/src/lib/tunnel/gate.ts +16 -8
  76. package/src/lib/tunnel/processes.ts +149 -0
  77. package/src/lib/tunnel/state.ts +123 -27
@@ -84,6 +84,7 @@ import {
84
84
  readLiveCoordinationRow,
85
85
  readLiveCoordinationRows,
86
86
  } from "../core/agents/state/live-coordination-view.ts";
87
+ import { forkRelationLabels, resolveForkAncestry } from "../core/agents/state/names.ts";
87
88
  import {
88
89
  type AgentActivity,
89
90
  foldSessionState,
@@ -107,6 +108,7 @@ import {
107
108
  projectCoordinationViewV3,
108
109
  readCoordinationViewV3,
109
110
  } from "../core/events/v3/coordination-view.ts";
111
+ import { nativeInstanceIdV3 } from "../core/events/v3/live-route-observer.ts";
110
112
  import { liveInstanceIdV3 } from "../core/events/v3/live-routing.ts";
111
113
  import {
112
114
  countSummarizedSinceV3,
@@ -437,9 +439,15 @@ export function registerAgentsCommand(
437
439
  "--session-id <id>",
438
440
  "Target the heartbeat with this session_id directly, bypassing the ppid walk.",
439
441
  )
440
- .action((state: string, opts: { reason?: string; sessionId?: string }) => {
441
- runLifecycle(state, opts);
442
- });
442
+ .option(
443
+ "--force-ancestor",
444
+ "Allow a fork to change its fork ancestor's lifecycle (only on explicit operator instruction)",
445
+ )
446
+ .action(
447
+ (state: string, opts: { reason?: string; sessionId?: string; forceAncestor?: boolean }) => {
448
+ runLifecycle(state, opts);
449
+ },
450
+ );
443
451
 
444
452
  cmd
445
453
  .command("end")
@@ -453,9 +461,20 @@ export function registerAgentsCommand(
453
461
  "succeeded | failed | cancelled | timed_out | denied | interrupted | unknown",
454
462
  "succeeded",
455
463
  )
456
- .action((opts: { sessionId?: string; instanceId?: string; outcome: string }) => {
457
- runEndSession(opts);
458
- });
464
+ .option(
465
+ "--force-ancestor",
466
+ "Allow a fork to finalize its fork ancestor's session (only on explicit operator instruction)",
467
+ )
468
+ .action(
469
+ (opts: {
470
+ sessionId?: string;
471
+ instanceId?: string;
472
+ outcome: string;
473
+ forceAncestor?: boolean;
474
+ }) => {
475
+ runEndSession(opts);
476
+ },
477
+ );
459
478
 
460
479
  cmd
461
480
  .command("reconcile")
@@ -666,7 +685,40 @@ const SESSION_OUTCOMES = new Set([
666
685
  "unknown",
667
686
  ]);
668
687
 
669
- function runEndSession(opts: { sessionId?: string; instanceId?: string; outcome: string }) {
688
+ /**
689
+ * Refuse when the calling session targets one of its recorded fork ancestors.
690
+ * A fork's transcript is full of its parent's name, task, and claims, so a
691
+ * fork that "cleans up its earlier self" is usually cutting off a live parent
692
+ * (2026-10-07: a fork offered to close its running parent). Returns the
693
+ * refusal message, or null when the call may proceed.
694
+ */
695
+ function forkAncestorRefusal(
696
+ root: string,
697
+ targetIds: Array<string | undefined>,
698
+ force: boolean | undefined,
699
+ action: string,
700
+ ): string | null {
701
+ if (force) return null;
702
+ const ids = new Set(targetIds.filter((id): id is string => !!id));
703
+ if (ids.size === 0) return null;
704
+ const self = resolveOwner() ?? sessionIdentityFromEnv();
705
+ if (!self || ids.has(self)) return null;
706
+ const ancestor = resolveForkAncestry(root, self).find((a) => ids.has(a.instance_id));
707
+ if (!ancestor) return null;
708
+ const label = ancestor.name ? `agent-${ancestor.name}` : ancestor.instance_id;
709
+ return (
710
+ `${label} is this session's fork ancestor: this conversation was branched from it, so its ` +
711
+ `name, task, and files in your context are its own, and it may still be running. Refusing to ` +
712
+ `${action} it. Rerun with --force-ancestor only when the operator explicitly asked for that.`
713
+ );
714
+ }
715
+
716
+ function runEndSession(opts: {
717
+ sessionId?: string;
718
+ instanceId?: string;
719
+ outcome: string;
720
+ forceAncestor?: boolean;
721
+ }) {
670
722
  const root = monorepoRoot();
671
723
  if (!root) return failCommand("not_in_repo", "not in an agent session");
672
724
  if (!SESSION_OUTCOMES.has(opts.outcome)) {
@@ -698,6 +750,15 @@ function runEndSession(opts: { sessionId?: string; instanceId?: string; outcome:
698
750
  return failCommand("session_identity_missing", "live V3 generation disappeared");
699
751
  }
700
752
  const state = record.state;
753
+ if (opts.instanceId || opts.sessionId) {
754
+ const refusal = forkAncestorRefusal(
755
+ root,
756
+ [target, state.session_id, nativeInstanceIdV3(state.instance_id)],
757
+ opts.forceAncestor,
758
+ "finalize",
759
+ );
760
+ if (refusal) return failCommand("session_is_fork_ancestor", refusal);
761
+ }
701
762
  if (state.delegations.length > 0) {
702
763
  return failCommand(
703
764
  "session_work_open",
@@ -1992,7 +2053,10 @@ function runSetTask(task: string, opts?: { sessionId?: string }): void {
1992
2053
  });
1993
2054
  }
1994
2055
 
1995
- function runLifecycle(rawState: string, opts: { reason?: string; sessionId?: string }): void {
2056
+ function runLifecycle(
2057
+ rawState: string,
2058
+ opts: { reason?: string; sessionId?: string; forceAncestor?: boolean },
2059
+ ): void {
1996
2060
  const state = rawState.trim().toLowerCase();
1997
2061
  if (state !== "active" && state !== "blocked" && state !== "done") {
1998
2062
  emit.error({
@@ -2013,6 +2077,19 @@ function runLifecycle(rawState: string, opts: { reason?: string; sessionId?: str
2013
2077
  return;
2014
2078
  }
2015
2079
  if (!opts.sessionId) ensureAdapterSession(root);
2080
+ if (opts.sessionId) {
2081
+ const refusal = forkAncestorRefusal(
2082
+ root,
2083
+ [opts.sessionId],
2084
+ opts.forceAncestor,
2085
+ "change the lifecycle of",
2086
+ );
2087
+ if (refusal) {
2088
+ emit.error({ code: "session_is_fork_ancestor", message: refusal });
2089
+ process.exitCode = 1;
2090
+ return;
2091
+ }
2092
+ }
2016
2093
  const myOwner = opts.sessionId ?? resolveOwner() ?? sessionIdentityFromEnv();
2017
2094
  if (!myOwner) {
2018
2095
  emit.error(
@@ -2390,8 +2467,15 @@ function runStatus(opts: {
2390
2467
 
2391
2468
  const { livePeers, stale: peersStale } = collectStatusPeerHealth(root, myOwner);
2392
2469
 
2393
- // Sort: file-holders first (by file count desc), then idle peers by recency.
2470
+ // Recorded fork relations label the parent and forks so a fork cannot read
2471
+ // its live parent as its own earlier session; they sort first.
2472
+ const relations = forkRelationLabels(root, myOwner, new Set(livePeers.map((p) => p.instance_id)));
2473
+ // Sort: related sessions, then file-holders (by file count desc), then idle
2474
+ // peers by recency.
2394
2475
  livePeers.sort((a, b) => {
2476
+ const ar = relations.has(a.instance_id) ? 1 : 0;
2477
+ const br = relations.has(b.instance_id) ? 1 : 0;
2478
+ if (ar !== br) return br - ar;
2395
2479
  const af = a.files_touched?.length ?? 0;
2396
2480
  const bf = b.files_touched?.length ?? 0;
2397
2481
  if (af !== bf) return bf - af;
@@ -2406,7 +2490,7 @@ function runStatus(opts: {
2406
2490
  );
2407
2491
  // Cross-machine presence (ADR 0016): sessions on other machines, advisory.
2408
2492
  const remoteMachines = readRemoteMachines(root);
2409
- const peersStr = formatPeers(livePeers, 4, peersStale, remoteMachines);
2493
+ const peersStr = formatPeers(livePeers, 4, peersStale, remoteMachines, relations);
2410
2494
 
2411
2495
  const ctxUsage = readContextUsage(hb.native_session_id ?? hb.session_id, hb.platform);
2412
2496
  let ctxStr: string;
@@ -2600,13 +2684,15 @@ function formatPeers(
2600
2684
  cap: number,
2601
2685
  staleCount: number,
2602
2686
  remoteMachines: RemoteMachine[] = [],
2687
+ relations: Map<string, string> = new Map(),
2603
2688
  ): string {
2604
2689
  if (peers.length === 0 && staleCount === 0 && remoteMachines.length === 0) return "none";
2605
2690
  const labels = peers.map((p) => {
2606
2691
  const name = p.name || "unnamed";
2607
2692
  const plat = formatPlatformLabel(p.platform);
2608
2693
  const files = p.files_touched?.length ?? 0;
2609
- const base = `${name} (${plat})`;
2694
+ const relation = relations.get(p.instance_id);
2695
+ const base = relation ? `${name} (${plat}, ${relation})` : `${name} (${plat})`;
2610
2696
  return files > 0 ? `${base}, ${files} files` : base;
2611
2697
  });
2612
2698
  let main: string;