harnery 0.41.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 (269) hide show
  1. package/dist/commander.d.ts +16 -0
  2. package/dist/commander.d.ts.map +1 -1
  3. package/dist/commander.js +21 -0
  4. package/dist/commands/agents.d.ts.map +1 -1
  5. package/dist/commands/agents.js +63 -5
  6. package/dist/commands/artifacts.d.ts.map +1 -1
  7. package/dist/commands/artifacts.js +113 -12
  8. package/dist/commands/browse.d.ts +164 -0
  9. package/dist/commands/browse.d.ts.map +1 -1
  10. package/dist/commands/browse.js +263 -51
  11. package/dist/commands/claude-desktop.d.ts.map +1 -1
  12. package/dist/commands/claude-desktop.js +52 -0
  13. package/dist/commands/disk.d.ts +6 -0
  14. package/dist/commands/disk.d.ts.map +1 -0
  15. package/dist/commands/disk.js +80 -0
  16. package/dist/commands/doctor.d.ts +8 -0
  17. package/dist/commands/doctor.d.ts.map +1 -1
  18. package/dist/commands/doctor.js +48 -1
  19. package/dist/commands/files.d.ts +16 -0
  20. package/dist/commands/files.d.ts.map +1 -1
  21. package/dist/commands/files.js +54 -2
  22. package/dist/commands/init.d.ts +2 -2
  23. package/dist/commands/init.d.ts.map +1 -1
  24. package/dist/commands/init.js +43 -9
  25. package/dist/commands/rm.d.ts +4 -0
  26. package/dist/commands/rm.d.ts.map +1 -0
  27. package/dist/commands/rm.js +87 -0
  28. package/dist/commands/servers.d.ts +7 -0
  29. package/dist/commands/servers.d.ts.map +1 -0
  30. package/dist/commands/servers.js +394 -0
  31. package/dist/commands/tunnel.d.ts +65 -0
  32. package/dist/commands/tunnel.d.ts.map +1 -1
  33. package/dist/commands/tunnel.js +372 -103
  34. package/dist/commands/web-fetch.d.ts +3 -2
  35. package/dist/commands/web-fetch.d.ts.map +1 -1
  36. package/dist/commands/web-fetch.js +25 -19
  37. package/dist/commands/web.d.ts +7 -1
  38. package/dist/commands/web.d.ts.map +1 -1
  39. package/dist/commands/web.js +9 -3
  40. package/dist/core/adapters/profiles.d.ts +9 -0
  41. package/dist/core/adapters/profiles.d.ts.map +1 -1
  42. package/dist/core/adapters/profiles.js +22 -1
  43. package/dist/core/adapters/types.d.ts +9 -0
  44. package/dist/core/adapters/types.d.ts.map +1 -1
  45. package/dist/core/agents/cli.js +13 -6
  46. package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
  47. package/dist/core/agents/render/prompt-context.js +11 -5
  48. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  49. package/dist/core/agents/render/session-context.js +59 -13
  50. package/dist/core/agents/state/live-coordination-view.d.ts +20 -0
  51. package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
  52. package/dist/core/agents/state/live-coordination-view.js +60 -0
  53. package/dist/core/agents/state/names.d.ts +43 -3
  54. package/dist/core/agents/state/names.d.ts.map +1 -1
  55. package/dist/core/agents/state/names.js +87 -3
  56. package/dist/core/artifacts/delivery-card.js +9 -2
  57. package/dist/core/artifacts/index.d.ts +140 -4
  58. package/dist/core/artifacts/index.d.ts.map +1 -1
  59. package/dist/core/artifacts/index.js +522 -49
  60. package/dist/core/config.d.ts +68 -11
  61. package/dist/core/config.d.ts.map +1 -1
  62. package/dist/core/config.js +128 -3
  63. package/dist/core/diagnostics/bundle.d.ts.map +1 -1
  64. package/dist/core/diagnostics/bundle.js +2 -2
  65. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  66. package/dist/core/events/v3/producers/recorder.js +16 -0
  67. package/dist/core/events/v3/projection.js +9 -1
  68. package/dist/core/events/v3/support-storage/pack-reader.d.ts.map +1 -1
  69. package/dist/core/events/v3/support-storage/pack-reader.js +42 -16
  70. package/dist/core/hooks/adapter/events.d.ts +17 -0
  71. package/dist/core/hooks/adapter/events.d.ts.map +1 -1
  72. package/dist/core/hooks/adapter/events.js +5 -0
  73. package/dist/core/hooks/adapter/runtime-telemetry.js +3 -1
  74. package/dist/core/hooks/adapter/wiring.d.ts +37 -7
  75. package/dist/core/hooks/adapter/wiring.d.ts.map +1 -1
  76. package/dist/core/hooks/adapter/wiring.js +62 -4
  77. package/dist/core/hooks/cli.js +131 -23
  78. package/dist/core/hooks/fork-lineage.d.ts +40 -0
  79. package/dist/core/hooks/fork-lineage.d.ts.map +1 -0
  80. package/dist/core/hooks/fork-lineage.js +62 -0
  81. package/dist/core/hooks/resolve/transcript.d.ts +16 -0
  82. package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
  83. package/dist/core/hooks/resolve/transcript.js +84 -0
  84. package/dist/core/hooks/run-markers.d.ts +37 -0
  85. package/dist/core/hooks/run-markers.d.ts.map +1 -0
  86. package/dist/core/hooks/run-markers.js +109 -0
  87. package/dist/core/hooks/shell-rm.d.ts +2 -0
  88. package/dist/core/hooks/shell-rm.d.ts.map +1 -0
  89. package/dist/core/hooks/shell-rm.js +105 -0
  90. package/dist/core/hooks/shell-tokens.d.ts +12 -0
  91. package/dist/core/hooks/shell-tokens.d.ts.map +1 -0
  92. package/dist/core/hooks/shell-tokens.js +97 -0
  93. package/dist/core/hooks/shell-waiter.d.ts.map +1 -1
  94. package/dist/core/hooks/shell-waiter.js +3 -93
  95. package/dist/core/semantic/contract.d.ts +5 -5
  96. package/dist/core/servers/index.d.ts +211 -0
  97. package/dist/core/servers/index.d.ts.map +1 -0
  98. package/dist/core/servers/index.js +560 -0
  99. package/dist/core/servers/net.d.ts +58 -0
  100. package/dist/core/servers/net.d.ts.map +1 -0
  101. package/dist/core/servers/net.js +270 -0
  102. package/dist/core/servers/tunnels.d.ts +14 -0
  103. package/dist/core/servers/tunnels.d.ts.map +1 -0
  104. package/dist/core/servers/tunnels.js +33 -0
  105. package/dist/core/storage/builtins.js +1 -0
  106. package/dist/core/storage/process-log.d.ts +12 -0
  107. package/dist/core/storage/process-log.d.ts.map +1 -1
  108. package/dist/core/storage/process-log.js +10 -2
  109. package/dist/core/workflow/engine.d.ts.map +1 -1
  110. package/dist/core/workflow/engine.js +30 -1
  111. package/dist/core/workflow/sandbox-projection.d.ts +13 -2
  112. package/dist/core/workflow/sandbox-projection.d.ts.map +1 -1
  113. package/dist/core/workflow/sandbox-projection.js +23 -0
  114. package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
  115. package/dist/core/workflow/spawn-claude.js +6 -1
  116. package/dist/core/workflow/spawn-codex.d.ts +2 -0
  117. package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
  118. package/dist/core/workflow/spawn-codex.js +20 -1
  119. package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
  120. package/dist/core/workflow/spawn-cursor.js +6 -1
  121. package/dist/core/workflow/spawn-opencode.d.ts.map +1 -1
  122. package/dist/core/workflow/spawn-opencode.js +4 -1
  123. package/dist/core/workflow/types.d.ts +47 -2
  124. package/dist/core/workflow/types.d.ts.map +1 -1
  125. package/dist/core/workflow/worker-access.d.ts +92 -0
  126. package/dist/core/workflow/worker-access.d.ts.map +1 -0
  127. package/dist/core/workflow/worker-access.js +450 -0
  128. package/dist/lib/browser/batch-steps.d.ts +18 -0
  129. package/dist/lib/browser/batch-steps.d.ts.map +1 -0
  130. package/dist/lib/browser/batch-steps.js +82 -0
  131. package/dist/lib/browser/collect/collector.d.ts +99 -0
  132. package/dist/lib/browser/collect/collector.d.ts.map +1 -0
  133. package/dist/lib/browser/collect/collector.js +202 -0
  134. package/dist/lib/browser/collect/fields.d.ts +31 -0
  135. package/dist/lib/browser/collect/fields.d.ts.map +1 -0
  136. package/dist/lib/browser/collect/fields.js +75 -0
  137. package/dist/lib/browser/collect/index.d.ts +6 -0
  138. package/dist/lib/browser/collect/index.d.ts.map +1 -0
  139. package/dist/lib/browser/collect/index.js +5 -0
  140. package/dist/lib/browser/collect/page-driver.d.ts +14 -0
  141. package/dist/lib/browser/collect/page-driver.d.ts.map +1 -0
  142. package/dist/lib/browser/collect/page-driver.js +306 -0
  143. package/dist/lib/browser/collect/presets.d.ts +52 -0
  144. package/dist/lib/browser/collect/presets.d.ts.map +1 -0
  145. package/dist/lib/browser/collect/presets.js +176 -0
  146. package/dist/lib/browser/collect/scroll-plan.d.ts +85 -0
  147. package/dist/lib/browser/collect/scroll-plan.d.ts.map +1 -0
  148. package/dist/lib/browser/collect/scroll-plan.js +118 -0
  149. package/dist/lib/browser/index.d.ts +1 -1
  150. package/dist/lib/browser/index.d.ts.map +1 -1
  151. package/dist/lib/browser/page-review-judge.d.ts +2 -0
  152. package/dist/lib/browser/page-review-judge.d.ts.map +1 -1
  153. package/dist/lib/browser/page-review-judge.js +13 -0
  154. package/dist/lib/browser/qa-run-contracts.d.ts +10 -0
  155. package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -1
  156. package/dist/lib/browser/qa-run.d.ts.map +1 -1
  157. package/dist/lib/browser/qa-run.js +1 -0
  158. package/dist/lib/browser/windows-chrome.d.ts +59 -0
  159. package/dist/lib/browser/windows-chrome.d.ts.map +1 -0
  160. package/dist/lib/browser/windows-chrome.js +179 -0
  161. package/dist/lib/claude-desktop-share.d.ts +136 -0
  162. package/dist/lib/claude-desktop-share.d.ts.map +1 -0
  163. package/dist/lib/claude-desktop-share.js +394 -0
  164. package/dist/lib/disk-usage.d.ts +50 -0
  165. package/dist/lib/disk-usage.d.ts.map +1 -0
  166. package/dist/lib/disk-usage.js +303 -0
  167. package/dist/lib/docs-lint.d.ts +6 -0
  168. package/dist/lib/docs-lint.d.ts.map +1 -1
  169. package/dist/lib/docs-lint.js +8 -3
  170. package/dist/lib/guarded-remove.d.ts +34 -0
  171. package/dist/lib/guarded-remove.d.ts.map +1 -0
  172. package/dist/lib/guarded-remove.js +310 -0
  173. package/dist/lib/instructions/templates.js +8 -8
  174. package/dist/lib/tunnel/allowlist.d.ts +44 -0
  175. package/dist/lib/tunnel/allowlist.d.ts.map +1 -0
  176. package/dist/lib/tunnel/allowlist.js +204 -0
  177. package/dist/lib/tunnel/current-address.d.ts +41 -0
  178. package/dist/lib/tunnel/current-address.d.ts.map +1 -0
  179. package/dist/lib/tunnel/current-address.js +123 -0
  180. package/dist/lib/tunnel/error-page.d.ts +1 -1
  181. package/dist/lib/tunnel/error-page.d.ts.map +1 -1
  182. package/dist/lib/tunnel/error-page.js +19 -8
  183. package/dist/lib/tunnel/gate.js +33 -7
  184. package/dist/lib/tunnel/path-scope.d.ts +22 -0
  185. package/dist/lib/tunnel/path-scope.d.ts.map +1 -0
  186. package/dist/lib/tunnel/path-scope.js +62 -0
  187. package/dist/lib/tunnel/processes.d.ts +39 -0
  188. package/dist/lib/tunnel/processes.d.ts.map +1 -0
  189. package/dist/lib/tunnel/processes.js +121 -0
  190. package/dist/lib/tunnel/state.d.ts +33 -3
  191. package/dist/lib/tunnel/state.d.ts.map +1 -1
  192. package/dist/lib/tunnel/state.js +115 -30
  193. package/package.json +7 -1
  194. package/schemas/config.schema.json +91 -0
  195. package/src/commander.ts +51 -0
  196. package/src/commands/agents.ts +111 -11
  197. package/src/commands/artifacts.ts +171 -19
  198. package/src/commands/browse.ts +400 -57
  199. package/src/commands/claude-desktop.ts +61 -0
  200. package/src/commands/disk.ts +114 -0
  201. package/src/commands/doctor.ts +52 -1
  202. package/src/commands/files.ts +93 -2
  203. package/src/commands/init.ts +55 -5
  204. package/src/commands/rm.ts +115 -0
  205. package/src/commands/servers.ts +506 -0
  206. package/src/commands/tunnel.ts +494 -116
  207. package/src/commands/web-fetch.ts +30 -19
  208. package/src/commands/web.ts +9 -3
  209. package/src/core/adapters/profiles.ts +22 -1
  210. package/src/core/adapters/types.ts +10 -0
  211. package/src/core/agents/cli.ts +14 -6
  212. package/src/core/agents/render/prompt-context.ts +25 -8
  213. package/src/core/agents/render/session-context.ts +84 -16
  214. package/src/core/agents/state/live-coordination-view.ts +73 -0
  215. package/src/core/agents/state/names.ts +117 -6
  216. package/src/core/artifacts/delivery-card.ts +10 -2
  217. package/src/core/artifacts/index.ts +697 -44
  218. package/src/core/config.ts +183 -4
  219. package/src/core/diagnostics/bundle.ts +2 -1
  220. package/src/core/events/v3/producers/recorder.ts +16 -0
  221. package/src/core/events/v3/projection.ts +11 -1
  222. package/src/core/events/v3/support-storage/pack-reader.ts +45 -19
  223. package/src/core/hooks/adapter/events.ts +19 -0
  224. package/src/core/hooks/adapter/runtime-telemetry.ts +3 -1
  225. package/src/core/hooks/adapter/wiring.ts +116 -8
  226. package/src/core/hooks/cli.ts +152 -22
  227. package/src/core/hooks/fork-lineage.ts +82 -0
  228. package/src/core/hooks/resolve/transcript.ts +88 -0
  229. package/src/core/hooks/run-markers.ts +132 -0
  230. package/src/core/hooks/shell-rm.ts +103 -0
  231. package/src/core/hooks/shell-tokens.ts +96 -0
  232. package/src/core/hooks/shell-waiter.ts +4 -91
  233. package/src/core/servers/index.ts +712 -0
  234. package/src/core/servers/net.ts +283 -0
  235. package/src/core/servers/tunnels.ts +39 -0
  236. package/src/core/storage/builtins.ts +1 -0
  237. package/src/core/storage/process-log.ts +17 -2
  238. package/src/core/workflow/engine.ts +38 -1
  239. package/src/core/workflow/sandbox-projection.ts +38 -2
  240. package/src/core/workflow/spawn-claude.ts +5 -1
  241. package/src/core/workflow/spawn-codex.ts +18 -1
  242. package/src/core/workflow/spawn-cursor.ts +5 -1
  243. package/src/core/workflow/spawn-opencode.ts +4 -1
  244. package/src/core/workflow/types.ts +62 -2
  245. package/src/core/workflow/worker-access.ts +525 -0
  246. package/src/lib/browser/batch-steps.ts +79 -0
  247. package/src/lib/browser/collect/collector.ts +306 -0
  248. package/src/lib/browser/collect/fields.ts +87 -0
  249. package/src/lib/browser/collect/index.ts +47 -0
  250. package/src/lib/browser/collect/page-driver.ts +327 -0
  251. package/src/lib/browser/collect/presets.ts +237 -0
  252. package/src/lib/browser/collect/scroll-plan.ts +179 -0
  253. package/src/lib/browser/index.ts +1 -0
  254. package/src/lib/browser/page-review-judge.ts +16 -0
  255. package/src/lib/browser/qa-run-contracts.ts +11 -0
  256. package/src/lib/browser/qa-run.ts +1 -0
  257. package/src/lib/browser/windows-chrome.ts +227 -0
  258. package/src/lib/claude-desktop-share.ts +511 -0
  259. package/src/lib/disk-usage.ts +347 -0
  260. package/src/lib/docs-lint.ts +8 -3
  261. package/src/lib/guarded-remove.ts +376 -0
  262. package/src/lib/instructions/templates.ts +8 -8
  263. package/src/lib/tunnel/allowlist.ts +208 -0
  264. package/src/lib/tunnel/current-address.ts +158 -0
  265. package/src/lib/tunnel/error-page.ts +25 -9
  266. package/src/lib/tunnel/gate.ts +38 -9
  267. package/src/lib/tunnel/path-scope.ts +73 -0
  268. package/src/lib/tunnel/processes.ts +149 -0
  269. package/src/lib/tunnel/state.ts +143 -30
@@ -16,9 +16,11 @@ import type { EmitContext } from "../commander.ts";
16
16
  * harnery repo at the *matching version tag* into a cache dir and install the
17
17
  * web app's deps. Subsequent runs reuse the cache.
18
18
  *
19
- * The only thing `web/` pulls from `src/` is `harnery/core/journal` (a single,
20
- * dependency-free module), so the install is `web/` deps only: no root install,
21
- * no Playwright browser download.
19
+ * `web/` imports readers from `src/`, and `web/next.config.ts` resolves the
20
+ * root `playwright` package, so the install covers the repository root and
21
+ * then `web/`. Installing the `playwright` package downloads no browsers.
22
+ * Installing `web/` alone left `harn web up` failing with
23
+ * `Cannot find module 'playwright/package.json'`.
22
24
  */
23
25
 
24
26
  const DEFAULT_REPO_URL = "https://github.com/ryanjkelly/harnery.git";
@@ -53,8 +55,8 @@ function webCacheRoot(ref: string): string {
53
55
  }
54
56
 
55
57
  /** bun if available (preferred), else npm. Both support `<runner> install` and `<runner> run <script>`. */
56
- export function webRunner(): "bun" | "npm" {
57
- const r = spawnSync("bun", ["--version"], { stdio: "ignore" });
58
+ export function webRunner(spawn: typeof spawnSync = spawnSync): "bun" | "npm" {
59
+ const r = spawn("bun", ["--version"], { stdio: "ignore" });
58
60
  return r.status === 0 ? "bun" : "npm";
59
61
  }
60
62
 
@@ -63,14 +65,20 @@ export function webRunner(): "bun" | "npm" {
63
65
  * its path, fetching (clone + install) on first use. Returns null on failure
64
66
  * (after emitting an actionable error).
65
67
  */
66
- export function lazyFetchWebRoot(emit: EmitContext): string | null {
68
+ export function lazyFetchWebRoot(
69
+ emit: EmitContext,
70
+ spawn: typeof spawnSync = spawnSync,
71
+ ): string | null {
67
72
  const { version, repoUrl } = readPkg();
68
73
  const ref = resolveRef(version);
69
74
  const cacheRoot = webCacheRoot(ref);
70
75
  const webDir = path.join(cacheRoot, "web");
71
76
 
72
- // node_modules in the cached web/ is the "fetched + installed" marker.
73
- if (existsSync(path.join(webDir, "node_modules"))) {
77
+ // node_modules in both the cached root and web/ is the "fetched + installed" marker.
78
+ if (
79
+ existsSync(path.join(cacheRoot, "node_modules")) &&
80
+ existsSync(path.join(webDir, "node_modules"))
81
+ ) {
74
82
  emit.log(`harn web · using cached dashboard (${ref})`, "info");
75
83
  return webDir;
76
84
  }
@@ -85,7 +93,7 @@ export function lazyFetchWebRoot(emit: EmitContext): string | null {
85
93
  if (existsSync(cacheRoot)) rmSync(cacheRoot, { recursive: true, force: true });
86
94
  mkdirSync(path.dirname(cacheRoot), { recursive: true });
87
95
 
88
- const clone = spawnSync("git", ["clone", "--depth", "1", "--branch", ref, repoUrl, cacheRoot], {
96
+ const clone = spawn("git", ["clone", "--depth", "1", "--branch", ref, repoUrl, cacheRoot], {
89
97
  stdio: "inherit",
90
98
  });
91
99
  if (clone.status !== 0) {
@@ -100,16 +108,19 @@ export function lazyFetchWebRoot(emit: EmitContext): string | null {
100
108
  return null;
101
109
  }
102
110
 
103
- const runner = webRunner();
104
- emit.log(`harn web · installing dashboard deps (${runner} install in web/)`, "info");
105
- const install = spawnSync(runner, ["install"], { cwd: webDir, stdio: "inherit" });
106
- if (install.status !== 0) {
107
- emit.error({
108
- code: "web_fetch_failed",
109
- message: `${runner} install in ${webDir} failed (status ${install.status})`,
110
- hint: `Try manually: cd ${webDir} && ${runner} install`,
111
- });
112
- return null;
111
+ const runner = webRunner(spawn);
112
+ for (const dir of [cacheRoot, webDir]) {
113
+ const where = dir === webDir ? "web/" : "the harnery root";
114
+ emit.log(`harn web · installing dashboard deps (${runner} install in ${where})`, "info");
115
+ const install = spawn(runner, ["install"], { cwd: dir, stdio: "inherit" });
116
+ if (install.status !== 0) {
117
+ emit.error({
118
+ code: "web_fetch_failed",
119
+ message: `${runner} install in ${dir} failed (status ${install.status})`,
120
+ hint: `Try manually: cd ${dir} && ${runner} install`,
121
+ });
122
+ return null;
123
+ }
113
124
  }
114
125
 
115
126
  emit.log(`harn web · dashboard ready (cached at ${webDir})`, "info");
@@ -82,11 +82,17 @@ export function nodeOptionsWithHeapCap(mb: number): string | undefined {
82
82
  return `${inherited} --max-old-space-size=${mb}`.trim();
83
83
  }
84
84
 
85
- /** Preload the request diagnostics hook without replacing Next's HTTP server. */
85
+ /**
86
+ * Preload the dashboard's server hooks (keep-alive and request diagnostics)
87
+ * without replacing Next's HTTP server. NODE_OPTIONS reaches the child that
88
+ * `next dev` forks, but Next re-parses every `--import` into one value, so
89
+ * this names the single combined preload; a second, different `--import`
90
+ * would collapse into an unloadable path (web/server-preload.mjs).
91
+ */
86
92
  export function nodeOptionsWithWebDiagnostics(webDir: string, mb: number): string {
87
93
  const inherited = nodeOptionsWithHeapCap(mb) ?? "";
88
- if (inherited.includes("server-performance.mjs")) return inherited;
89
- const preload = pathToFileURL(path.join(webDir, "server-performance.mjs")).href;
94
+ if (inherited.includes("server-preload.mjs")) return inherited;
95
+ const preload = pathToFileURL(path.join(webDir, "server-preload.mjs")).href;
90
96
  return `${inherited} --import=${preload}`.trim();
91
97
  }
92
98
 
@@ -45,6 +45,18 @@ export const BUILTIN_ADAPTER_PROFILES = {
45
45
  modelFamily: "claude",
46
46
  effortValues: ["low", "medium", "high", "xhigh", "max"],
47
47
  verified: { date: "2026-08-18", version: "2.1.233 (Claude Code)" },
48
+ // Checked against 2.1.283 (ADR 0192). Claude Code has no sandbox mode flag;
49
+ // its permission mode and its optional Bash sandbox are separate, so full
50
+ // access needs both: bypass permissions, and a settings layer that turns
51
+ // the Bash sandbox off in case a user or project settings file enabled it.
52
+ fullAccess: {
53
+ argv: [
54
+ "--permission-mode",
55
+ "bypassPermissions",
56
+ "--settings",
57
+ '{"sandbox":{"enabled":false}}',
58
+ ],
59
+ },
48
60
  capabilities: capabilities({
49
61
  effortSelection: supported("Mapped to `--effort <level>`."),
50
62
  maxTurns: supported("Mapped to `--max-turns <n>`."),
@@ -78,6 +90,9 @@ export const BUILTIN_ADAPTER_PROFILES = {
78
90
  modes: { "read-only": "read-only", "workspace-write": "workspace-write" },
79
91
  writableRoots: true,
80
92
  },
93
+ // Checked against codex-cli 0.155.1 (ADR 0192): `--sandbox` accepts
94
+ // danger-full-access, and `codex exec` never prompts for approval.
95
+ fullAccess: { argv: ["--sandbox", "danger-full-access"] },
81
96
  capabilities: capabilities({
82
97
  effortSelection: supported('Mapped to `-c model_reasoning_effort="<level>"`.'),
83
98
  filesystemPolicyProjection: supported(
@@ -107,6 +122,10 @@ export const BUILTIN_ADAPTER_PROFILES = {
107
122
  modelFamily: "multi",
108
123
  effortValues: [],
109
124
  verified: { date: "2026-09-20", version: "2026.09.18-9a7762b" },
125
+ // Checked against 2026.09.26-dd393fe (ADR 0192): `--sandbox disabled`
126
+ // overrides the configured sandbox. Cursor carries effort inside the model
127
+ // id, so a full-access floor for it names exact model ids.
128
+ fullAccess: { argv: ["--sandbox", "disabled"] },
110
129
  capabilities: capabilities({
111
130
  effortSelection: unsupported(
112
131
  "Cursor embeds effort in some parameterized model ids; Harnery does not rewrite model ids.",
@@ -136,7 +155,9 @@ export const BUILTIN_ADAPTER_PROFILES = {
136
155
  authModel: "own-auth",
137
156
  modelFamily: "multi",
138
157
  // Model reasoning is selected through the `provider/model#variant` id;
139
- // OpenCode exposes no separate effort dial for Harnery to map.
158
+ // OpenCode exposes no separate effort dial for Harnery to map. It also has
159
+ // no vendor sandbox to remove, so it declares no full-access rendering and
160
+ // a worker on it is never granted one (ADR 0192).
140
161
  effortValues: [],
141
162
  verified: { date: "2026-09-20", version: "opencode v2.0.11" },
142
163
  capabilities: capabilities({
@@ -67,6 +67,16 @@ export interface AdapterProfile {
67
67
  /** How this adapter projects host filesystem policy into the vendor's own
68
68
  * sandbox (ADR 0039). Absent means it cannot project any of it. */
69
69
  sandboxProjection?: AdapterSandboxProjection;
70
+ /** Vendor-native arguments that launch a worker with no vendor sandbox
71
+ * (ADR 0192). Absent means the adapter has no distinguishable full-access
72
+ * mode, so a worker on it never qualifies for one. */
73
+ fullAccess?: AdapterFullAccessRendering;
74
+ }
75
+
76
+ /** How an adapter renders an unsandboxed launch (ADR 0192). The spawner emits
77
+ * exactly these arguments, so the declaration and the argv cannot drift. */
78
+ export interface AdapterFullAccessRendering {
79
+ argv: readonly string[];
70
80
  }
71
81
 
72
82
  /** Fully planned child invocation. `resultFile` is used by adapters such as
@@ -309,12 +309,20 @@ async function handleStateAction(root: string, action: string, rest: string[]):
309
309
  }
310
310
  } catch (error) {
311
311
  const detail = error instanceof Error ? error.message : String(error);
312
- // A missing heartbeat here means the session's generation ended
313
- // while its process lived on. Name the reopen command (ADR 0088);
314
- // set-task is usually the first thing such a session runs.
315
- const hint = detail.includes("heartbeat_missing:")
316
- ? `; run \`${resolveBinName(root)} agents lifecycle active\` to open a fresh generation for this continuing session`
317
- : "";
312
+ // A missing heartbeat usually means the session's generation ended
313
+ // while its process lived on, so name the reopen command (ADR 0088).
314
+ // A still-live generation whose authority the ledger withholds looks
315
+ // the same here; name its diagnostics instead, since reopen cannot apply.
316
+ const { describeLiveAuthorityBlockV3, liveAuthorityBlockHintV3, liveAuthorityBlockV3 } =
317
+ await import("./state/live-coordination-view.ts");
318
+ const block = detail.includes("heartbeat_missing:")
319
+ ? liveAuthorityBlockV3(root, owner)
320
+ : null;
321
+ const hint = block
322
+ ? `; ${describeLiveAuthorityBlockV3(block)}; ${liveAuthorityBlockHintV3(resolveBinName(root))}`
323
+ : detail.includes("heartbeat_missing:")
324
+ ? `; run \`${resolveBinName(root)} agents lifecycle active\` to open a fresh generation for this continuing session`
325
+ : "";
318
326
  process.stderr.write(`agent-coord set-task: V3 authority refused (${detail})${hint}\n`);
319
327
  return 1;
320
328
  }
@@ -57,6 +57,7 @@ import {
57
57
  readLiveCoordinationRow,
58
58
  readLiveCoordinationRows,
59
59
  } from "../state/live-coordination-view.ts";
60
+ import { forkRelationLabels } from "../state/names.ts";
60
61
  import type { AgentActivity, TaskState } from "../state/session-state.ts";
61
62
  import { formatPendingCouncils, recordDeliveredMessages } from "./session-context.ts";
62
63
 
@@ -441,7 +442,11 @@ function computePeerTableIfChanged(
441
442
  if (oldHash && oldHash === newHash) return "";
442
443
 
443
444
  // Render peer table via the same formatter used at SessionStart.
444
- const local = formatPeerTable(peers, mySessionId);
445
+ const local = formatPeerTable(
446
+ peers,
447
+ mySessionId,
448
+ forkRelationLabels(coordRoot, selfInstanceId, new Set(peers.map((p) => p.instance_id ?? ""))),
449
+ );
445
450
  const remoteTable = formatRemoteTable(remote);
446
451
  const table = [local, remoteTable].filter(Boolean).join("\n\n");
447
452
  if (!table) return "";
@@ -580,7 +585,11 @@ function writeHashFile(path: string, value: string): void {
580
585
  * (no cross-file imports of formatting internals). If both renderers diverge,
581
586
  * pull the shared bits into a small util module. */
582
587
 
583
- function formatPeerTable(peers: HeartbeatRow[], mySessionId: string): string {
588
+ function formatPeerTable(
589
+ peers: HeartbeatRow[],
590
+ mySessionId: string,
591
+ relations: Map<string, string> = new Map(),
592
+ ): string {
584
593
  if (peers.length === 0) return "";
585
594
  const nowSec = Math.floor(Date.now() / 1000);
586
595
 
@@ -592,16 +601,20 @@ function formatPeerTable(peers: HeartbeatRow[], mySessionId: string): string {
592
601
  }
593
602
  }
594
603
 
595
- type RowExt = HeartbeatRow & { display_files: string[] };
604
+ type RowExt = HeartbeatRow & { display_files: string[]; relation?: string };
596
605
  const rows: RowExt[] = peers
597
606
  .filter((p) => (p.kind ?? "unknown") !== "transient")
598
607
  .map((p) => {
599
608
  const folded = fold[p.instance_id ?? ""] ?? [];
600
609
  const display = Array.from(new Set([...(p.files_touched ?? []), ...folded])).sort();
601
- return { ...p, display_files: display };
610
+ const relation = relations.get(p.instance_id ?? "");
611
+ return { ...p, display_files: display, ...(relation ? { relation } : {}) };
602
612
  });
603
613
 
604
- const blocking = rows.filter((p) => p.session_id !== mySessionId).sort(byStartedAt);
614
+ // Related sessions sort first so a fork parent never hides in "+N more".
615
+ const blocking = rows
616
+ .filter((p) => p.session_id !== mySessionId)
617
+ .sort((a, b) => Number(!!b.relation) - Number(!!a.relation) || byStartedAt(a, b));
605
618
  const group = rows.filter((p) => p.session_id === mySessionId).sort(byStartedAt);
606
619
 
607
620
  const out: string[] = [];
@@ -625,7 +638,7 @@ function byStartedAt(a: HeartbeatRow, b: HeartbeatRow): number {
625
638
  }
626
639
 
627
640
  function renderSubtable(
628
- rows: Array<HeartbeatRow & { display_files: string[] }>,
641
+ rows: Array<HeartbeatRow & { display_files: string[]; relation?: string }>,
629
642
  header: string,
630
643
  nowSec: number,
631
644
  ): string {
@@ -635,7 +648,10 @@ function renderSubtable(
635
648
  return `${header}\n${first.join("\n")}${overflow}`;
636
649
  }
637
650
 
638
- function formatRow(r: HeartbeatRow & { display_files: string[] }, nowSec: number): string {
651
+ function formatRow(
652
+ r: HeartbeatRow & { display_files: string[]; relation?: string },
653
+ nowSec: number,
654
+ ): string {
639
655
  const taskPart = r.task ? ` "${r.task.slice(0, 60)}"` : "";
640
656
  const statePart = formatState(r);
641
657
  // Fall back started_at → last_heartbeat; if neither is a valid timestamp,
@@ -646,7 +662,8 @@ function formatRow(r: HeartbeatRow & { display_files: string[] }, nowSec: number
646
662
  // Prefer a short instance_id over a bare "unknown" so an incomplete row is
647
663
  // still identifiable.
648
664
  const label = r.name ?? (r.instance_id ? r.instance_id.slice(0, 8) : "unknown");
649
- return ` - agent-${label}${taskPart} (${statePart}, ${ageFrom}, ${filesPart})`;
665
+ const relation = r.relation ? ` [${r.relation}]` : "";
666
+ return ` - agent-${label}${relation}${taskPart} (${statePart}, ${ageFrom}, ${filesPart})`;
650
667
  }
651
668
 
652
669
  function formatState(r: HeartbeatRow): string {
@@ -16,7 +16,7 @@ import { appendEntry } from "../../journal/index.ts";
16
16
  import { readRemoteMachines } from "../../presence/index.ts";
17
17
  import { drainMailbox, formatMailboxDelivery, type MailboxMessageV1 } from "../mailbox.ts";
18
18
  import { readLiveCoordinationRows } from "../state/live-coordination-view.ts";
19
- import { readForkParent } from "../state/names.ts";
19
+ import { forkRelationLabels, readForkState } from "../state/names.ts";
20
20
  import type { AgentActivity, TaskState } from "../state/session-state.ts";
21
21
 
22
22
  interface HeartbeatRow {
@@ -54,7 +54,17 @@ export function renderSessionContext(opts: RenderOpts): string {
54
54
 
55
55
  // 1. Self-name line + peer table (folded if peers present)
56
56
  const peers = readActivePeers(coordRoot, instanceId);
57
- const localTable = formatPeerTable(peers, sessionId);
57
+ const forkState = readForkState(coordRoot, instanceId);
58
+ const forkParent = forkState?.parent ?? null;
59
+ const parentRow = forkParent
60
+ ? peers.find((peer) => peer.instance_id === forkParent.instance_id)
61
+ : undefined;
62
+ const relations = forkRelationLabels(
63
+ coordRoot,
64
+ instanceId,
65
+ new Set(peers.map((peer) => peer.instance_id ?? "")),
66
+ );
67
+ const localTable = formatPeerTable(peers, sessionId, relations);
58
68
  // Cross-machine presence (ADR 0016): sessions on other machines (advisory).
59
69
  const peerTable = [localTable, formatRemoteMachines(coordRoot)].filter(Boolean).join("\n\n");
60
70
  if (agentName) {
@@ -64,13 +74,11 @@ export function renderSessionContext(opts: RenderOpts): string {
64
74
  // asserts the parent's name, claims, and task. When recorded fork lineage
65
75
  // (.name-history forked_from) names the parent, say so specifically — the
66
76
  // fork is about to read a transcript full of that exact name. Otherwise
67
- // fall back to the generic clause, which needs no fork detection.
68
- const forkParent = readForkParent(coordRoot, instanceId);
69
- const authority = forkParent?.name
70
- ? `This conversation was branched from agent-${forkParent.name}'s session: earlier context showing agent-${forkParent.name}'s name, task, or file claims belongs to the pre-fork session, not to you.`
71
- : forkParent
72
- ? `This conversation was branched from another session (${forkParent.instance_id}); any agent name in earlier context belongs to that session, not to you.`
73
- : "Any different agent name in earlier context was inherited from another session; this one is authoritative.";
77
+ // fall back to the generic clause, which needs no fork detection. A live
78
+ // parent is the dangerous case (2026-10-07: a fork read its running
79
+ // parent as its own earlier self and offered to close it), so the notice
80
+ // names what the parent is doing and forbids ending it.
81
+ const authority = forkAuthorityClause(forkParent, forkState?.fork === true, parentRow);
74
82
  const selfLine = `You are agent-${agentName}${suffix}. ${authority}`;
75
83
  messages.push(peerTable ? `${selfLine}\n\n${peerTable}` : selfLine);
76
84
  } else if (peerTable) {
@@ -126,6 +134,7 @@ export function renderSessionContext(opts: RenderOpts): string {
126
134
  if (d.missing.length > 0)
127
135
  bits.push(`missing: ${d.missing.map((m) => m.subcommand).join(", ")}`);
128
136
  if (d.orphans.length > 0) bits.push(`orphaned: ${d.orphans.join(", ")}`);
137
+ if (d.stale.length > 0) bits.push(`outdated: ${d.stale.map((m) => m.subcommand).join(", ")}`);
129
138
  return ` - ${d.settingsFile} — ${bits.join("; ")}`;
130
139
  });
131
140
  messages.push(
@@ -193,13 +202,64 @@ function readActivePeers(coordRoot: string, selfInstanceId: string): HeartbeatRo
193
202
  );
194
203
  }
195
204
 
205
+ const DO_NOT_END =
206
+ "Do not end, release, or finalize that session unless the operator explicitly tells you to.";
207
+
208
+ function forkAuthorityClause(
209
+ parent: { instance_id: string; name: string | null } | null,
210
+ fork: boolean,
211
+ parentRow: HeartbeatRow | undefined,
212
+ ): string {
213
+ if (parent?.name && parentRow) {
214
+ const files = parentRow.files_touched ?? [];
215
+ const task = parentRow.task ? ` on "${parentRow.task.slice(0, 80)}"` : "";
216
+ const holds =
217
+ files.length > 0
218
+ ? ` and holds ${files.length} file${files.length === 1 ? "" : "s"} (${fmtFileList(files)})`
219
+ : "";
220
+ const owned =
221
+ files.length > 0
222
+ ? parentRow.task
223
+ ? "That name, task, and those files"
224
+ : "That name and those files"
225
+ : parentRow.task
226
+ ? "That name and task"
227
+ : "That name";
228
+ return (
229
+ `This conversation was forked from agent-${parent.name}, which is still running as a separate session${task}${holds}. ` +
230
+ `${owned} belong${owned === "That name" ? "s" : ""} to agent-${parent.name}, not to you. ${DO_NOT_END}`
231
+ );
232
+ }
233
+ if (parent?.name) {
234
+ return `This conversation was branched from agent-${parent.name}'s session: earlier context showing agent-${parent.name}'s name, task, or file claims belongs to the pre-fork session, not to you.`;
235
+ }
236
+ if (parent) {
237
+ return `This conversation was branched from another session (${parent.instance_id}); any agent name in earlier context belongs to that session, not to you.`;
238
+ }
239
+ if (fork) {
240
+ return `This conversation was forked from another session, which may still be running. Any agent name, task, or file claim in earlier context belongs to that session, not to you. ${DO_NOT_END}`;
241
+ }
242
+ return "Any different agent name in earlier context was inherited from another session; this one is authoritative.";
243
+ }
244
+
245
+ function fmtFileList(files: string[]): string {
246
+ const sorted = [...files].sort();
247
+ return sorted.length <= 3
248
+ ? sorted.join(", ")
249
+ : `${sorted.slice(0, 3).join(", ")}, +${sorted.length - 3} more`;
250
+ }
251
+
196
252
  /**
197
253
  * Renders the peer table as
198
254
  * two subsections: "Other agent groups active" (blocking) and "Your group"
199
255
  * (subagents/siblings, no mutual block). Folds transient subagents' files
200
256
  * into their parent session.
201
257
  */
202
- function formatPeerTable(peers: HeartbeatRow[], mySessionId: string): string {
258
+ function formatPeerTable(
259
+ peers: HeartbeatRow[],
260
+ mySessionId: string,
261
+ relations: Map<string, string> = new Map(),
262
+ ): string {
203
263
  if (peers.length === 0) return "";
204
264
  const nowSec = Math.floor(Date.now() / 1000);
205
265
 
@@ -213,16 +273,20 @@ function formatPeerTable(peers: HeartbeatRow[], mySessionId: string): string {
213
273
  }
214
274
 
215
275
  // Build rows with display_files (own files + folded transient files).
216
- type RowExt = HeartbeatRow & { display_files: string[] };
276
+ type RowExt = HeartbeatRow & { display_files: string[]; relation?: string };
217
277
  const rows: RowExt[] = peers
218
278
  .filter((p) => (p.kind ?? "unknown") !== "transient")
219
279
  .map((p) => {
220
280
  const folded = fold[p.instance_id ?? ""] ?? [];
221
281
  const display = Array.from(new Set([...(p.files_touched ?? []), ...folded])).sort();
222
- return { ...p, display_files: display };
282
+ const relation = relations.get(p.instance_id ?? "");
283
+ return { ...p, display_files: display, ...(relation ? { relation } : {}) };
223
284
  });
224
285
 
225
- const blocking = rows.filter((p) => p.session_id !== mySessionId).sort(byStartedAt);
286
+ // Related sessions sort first so the parent never hides in "+N more".
287
+ const blocking = rows
288
+ .filter((p) => p.session_id !== mySessionId)
289
+ .sort((a, b) => Number(!!b.relation) - Number(!!a.relation) || byStartedAt(a, b));
226
290
  const group = rows.filter((p) => p.session_id === mySessionId).sort(byStartedAt);
227
291
 
228
292
  const sections: string[] = [];
@@ -247,7 +311,7 @@ function byStartedAt(a: HeartbeatRow, b: HeartbeatRow): number {
247
311
  }
248
312
 
249
313
  function renderSubtable(
250
- rows: Array<HeartbeatRow & { display_files: string[] }>,
314
+ rows: Array<HeartbeatRow & { display_files: string[]; relation?: string }>,
251
315
  header: string,
252
316
  nowSec: number,
253
317
  ): string {
@@ -257,7 +321,10 @@ function renderSubtable(
257
321
  return `${header}\n${first.join("\n")}${overflow}`;
258
322
  }
259
323
 
260
- function formatRow(r: HeartbeatRow & { display_files: string[] }, nowSec: number): string {
324
+ function formatRow(
325
+ r: HeartbeatRow & { display_files: string[]; relation?: string },
326
+ nowSec: number,
327
+ ): string {
261
328
  const taskPart = r.task ? ` "${r.task.slice(0, 60)}"` : "";
262
329
  const activity = r.activity ?? "unknown";
263
330
  const lifecycle = r.task_state ?? "active";
@@ -265,7 +332,8 @@ function formatRow(r: HeartbeatRow & { display_files: string[] }, nowSec: number
265
332
  lifecycle === "blocked" && r.task_state_reason ? `: ${r.task_state_reason.slice(0, 80)}` : "";
266
333
  const ageFrom = fmtAge(nowSec - parseIsoSec(r.started_at));
267
334
  const filesPart = fmtFiles(r.display_files);
268
- return ` - agent-${r.name ?? "unknown"}${taskPart} (activity=${activity}, lifecycle=${lifecycle}${reason}, ${ageFrom}, ${filesPart})`;
335
+ const relation = r.relation ? ` [${r.relation}]` : "";
336
+ return ` - agent-${r.name ?? "unknown"}${relation}${taskPart} (activity=${activity}, lifecycle=${lifecycle}${reason}, ${ageFrom}, ${filesPart})`;
269
337
  }
270
338
 
271
339
  function fmtFiles(files: string[]): string {
@@ -226,3 +226,76 @@ function observedModel(generation: CoordinationGenerationViewV3): string | undef
226
226
  function isLifecycleState(value: string | undefined): value is "active" | "blocked" | "done" {
227
227
  return value === "active" || value === "blocked" || value === "done";
228
228
  }
229
+
230
+ /** Why a still-live V3 generation has lost coordination authority. */
231
+ export interface LiveAuthorityBlockV3 {
232
+ generation_id: string;
233
+ scope: "generation" | "ledger";
234
+ diagnostics: { code: string; event_id?: string }[];
235
+ }
236
+
237
+ /**
238
+ * Explain an authority refusal for a generation that is still live. Callers
239
+ * reach this after a lookup came back empty; without it the caller sees only
240
+ * "missing", which reads like an ended session and points at a reopen that
241
+ * cannot apply. Returns null when no live generation exists or nothing blocks.
242
+ */
243
+ export function liveAuthorityBlockV3(
244
+ coordRoot: string,
245
+ nativeInstanceId: string,
246
+ ): LiveAuthorityBlockV3 | null {
247
+ let view: CoordinationViewV3;
248
+ try {
249
+ if (observeLiveEventLedgerRouteV3(coordRoot).state === "blocked") return null;
250
+ view = readCoordinationViewV3(coordRoot);
251
+ } catch {
252
+ return null;
253
+ }
254
+ const generation = view.instances[liveInstanceIdV3(nativeInstanceId)];
255
+ if (generation?.phase !== "live" || generation.authority_eligible) return null;
256
+ const own = (view.diagnostics_by_generation[generation.generation_id] ?? []).filter(
257
+ (diagnostic) => diagnostic.authority_blocking,
258
+ );
259
+ const blocking = own.length
260
+ ? own
261
+ : view.global_diagnostics.filter((diagnostic) => diagnostic.authority_blocking);
262
+ if (!blocking.length) return null;
263
+ return {
264
+ generation_id: generation.generation_id,
265
+ scope: own.length ? "generation" : "ledger",
266
+ diagnostics: blocking.map((diagnostic) => ({
267
+ code: diagnostic.source_code
268
+ ? `${diagnostic.code}:${diagnostic.source_code}`
269
+ : diagnostic.code,
270
+ ...(diagnostic.event_id ? { event_id: diagnostic.event_id } : {}),
271
+ })),
272
+ };
273
+ }
274
+
275
+ /** One-line description of a live authority block, naming codes and events. */
276
+ export function describeLiveAuthorityBlockV3(block: LiveAuthorityBlockV3): string {
277
+ const byCode = new Map<string, string[]>();
278
+ for (const { code, event_id } of block.diagnostics) {
279
+ const events = byCode.get(code) ?? [];
280
+ if (event_id) events.push(event_id);
281
+ byCode.set(code, events);
282
+ }
283
+ const parts = [...byCode].map(([code, events]) => {
284
+ const shown = events.slice(0, 2).join(", ");
285
+ const more = events.length > 2 ? `, +${events.length - 2} more` : "";
286
+ return events.length ? `${code} ×${events.length} (${shown}${more})` : code;
287
+ });
288
+ const where =
289
+ block.scope === "generation"
290
+ ? `the event ledger holds ${block.diagnostics.length} authority-blocking diagnostic(s) for it`
291
+ : "the event ledger holds authority-blocking diagnostics that block every generation";
292
+ return `generation ${block.generation_id} is still live, but ${where}: ${parts.join("; ")}`;
293
+ }
294
+
295
+ /** Recovery pointer for a live authority block, in place of the reopen hint. */
296
+ export function liveAuthorityBlockHintV3(binName: string): string {
297
+ return (
298
+ `\`${binName} agents lifecycle active\` cannot help because it only reopens an ended ` +
299
+ `generation; \`${binName} agents health\` lists the diagnostics`
300
+ );
301
+ }