@gajae-code/coding-agent 0.12.0 → 0.12.2

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 (247) hide show
  1. package/CHANGELOG.md +111 -0
  2. package/dist/types/async/job-manager.d.ts +17 -3
  3. package/dist/types/config/settings-schema.d.ts +56 -2
  4. package/dist/types/exa/mcp-client.d.ts +10 -1
  5. package/dist/types/exec/bash-executor.d.ts +45 -12
  6. package/dist/types/extensibility/extensions/types.d.ts +16 -2
  7. package/dist/types/extensibility/gjc-plugins/index.d.ts +12 -2
  8. package/dist/types/extensibility/gjc-plugins/installer.d.ts +63 -7
  9. package/dist/types/extensibility/gjc-plugins/lifecycle-reconciliation.d.ts +55 -0
  10. package/dist/types/extensibility/gjc-plugins/lifecycle.d.ts +64 -0
  11. package/dist/types/extensibility/gjc-plugins/runtime-quarantine.d.ts +67 -0
  12. package/dist/types/extensibility/gjc-plugins/session-validation.d.ts +3 -1
  13. package/dist/types/extensibility/gjc-plugins/types.d.ts +124 -0
  14. package/dist/types/gjc-runtime/launch-tmux.d.ts +1 -0
  15. package/dist/types/gjc-runtime/ralplan-review-conflicts.d.ts +114 -0
  16. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +23 -3
  17. package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
  18. package/dist/types/gjc-runtime/team-runtime.d.ts +25 -0
  19. package/dist/types/gjc-runtime/team-worker-heartbeat.d.ts +43 -0
  20. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +10 -0
  21. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +37 -80
  22. package/dist/types/modes/acp/acp-agent.d.ts +2 -19
  23. package/dist/types/modes/components/gjc-bundle-settings.d.ts +34 -0
  24. package/dist/types/modes/components/irc-sidebar.d.ts +16 -0
  25. package/dist/types/modes/components/notifications-settings-editor.d.ts +1 -0
  26. package/dist/types/modes/components/session-observer-overlay.d.ts +18 -0
  27. package/dist/types/modes/components/settings-selector.d.ts +9 -0
  28. package/dist/types/modes/components/status-line/gh.d.ts +5 -0
  29. package/dist/types/modes/components/transcript-viewer-overlay.d.ts +18 -1
  30. package/dist/types/modes/controllers/command-controller.d.ts +1 -1
  31. package/dist/types/modes/controllers/event-controller.d.ts +1 -1
  32. package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -1
  33. package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
  34. package/dist/types/modes/interactive-mode.d.ts +9 -0
  35. package/dist/types/modes/theme/theme.d.ts +3 -1
  36. package/dist/types/modes/types.d.ts +32 -0
  37. package/dist/types/reminders/star-reminder.d.ts +3 -13
  38. package/dist/types/runtime-mcp/manager.d.ts +1 -0
  39. package/dist/types/sdk/acp/adapter.d.ts +3 -1
  40. package/dist/types/sdk/broker/transport.d.ts +2 -0
  41. package/dist/types/sdk/bus/attachment-registry.d.ts +1 -0
  42. package/dist/types/sdk/bus/chat-daemon-control.d.ts +5 -1
  43. package/dist/types/sdk/bus/config.d.ts +6 -2
  44. package/dist/types/sdk/bus/index.d.ts +29 -0
  45. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +8 -1
  46. package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
  47. package/dist/types/sdk/bus/reconciliation-store.d.ts +7 -3
  48. package/dist/types/sdk/bus/rich-draft.d.ts +1 -0
  49. package/dist/types/sdk/bus/rich-render.d.ts +2 -0
  50. package/dist/types/sdk/bus/telegram-adoption-intent.d.ts +165 -0
  51. package/dist/types/sdk/bus/telegram-cli.d.ts +12 -0
  52. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +12 -4
  53. package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -1
  54. package/dist/types/sdk/bus/telegram-reference.d.ts +11 -1
  55. package/dist/types/sdk/bus/threaded-render.d.ts +3 -2
  56. package/dist/types/sdk/bus/topic-registry.d.ts +11 -1
  57. package/dist/types/sdk/host/query/revision-store.d.ts +2 -0
  58. package/dist/types/sdk/host/reverse-leases.d.ts +1 -1
  59. package/dist/types/sdk/prompt-status.d.ts +23 -3
  60. package/dist/types/sdk/session.d.ts +6 -0
  61. package/dist/types/session/agent-session.d.ts +40 -2
  62. package/dist/types/session/artifacts.d.ts +8 -0
  63. package/dist/types/session/blob-store.d.ts +57 -4
  64. package/dist/types/session/internal/managed-session-storage.d.ts +30 -4
  65. package/dist/types/session/session-manager.d.ts +23 -6
  66. package/dist/types/session/session-storage.d.ts +4 -0
  67. package/dist/types/session/streaming-output.d.ts +36 -7
  68. package/dist/types/setup/credential-import.d.ts +12 -0
  69. package/dist/types/skill-state/active-state.d.ts +12 -0
  70. package/dist/types/skill-state/workflow-hud.d.ts +6 -0
  71. package/dist/types/task/executor.d.ts +3 -2
  72. package/dist/types/task/index.d.ts +7 -1
  73. package/dist/types/task/output-manager.d.ts +1 -0
  74. package/dist/types/task/provider-retry-status.d.ts +8 -0
  75. package/dist/types/task/receipt.d.ts +9 -1
  76. package/dist/types/task/types.d.ts +23 -0
  77. package/dist/types/tools/ask-answer-registry.d.ts +2 -2
  78. package/dist/types/tools/bash-interactive.d.ts +4 -1
  79. package/dist/types/tools/bash.d.ts +3 -1
  80. package/dist/types/tools/image-gen.d.ts +4 -0
  81. package/dist/types/tools/index.d.ts +6 -0
  82. package/dist/types/tools/output-meta.d.ts +23 -2
  83. package/dist/types/tools/path-utils.d.ts +1 -0
  84. package/dist/types/tools/read.d.ts +0 -6
  85. package/dist/types/tools/review.d.ts +5 -7
  86. package/dist/types/tools/subagent.d.ts +2 -0
  87. package/dist/types/utils/gh.d.ts +13 -0
  88. package/dist/types/utils/image-resize.d.ts +1 -0
  89. package/dist/types/web/search/providers/kimi.d.ts +2 -0
  90. package/dist/types/web/search/providers/searxng.d.ts +7 -0
  91. package/dist/types/web/search/providers/xai.d.ts +2 -0
  92. package/package.json +10 -8
  93. package/scripts/acp-conformance-agent.ts +218 -0
  94. package/scripts/capture-gjc-bundle-settings.ts +306 -0
  95. package/scripts/dogfood-ralplan-review-conflicts.ts +272 -0
  96. package/scripts/generate-sdk-operation-inventory.ts +5 -0
  97. package/scripts/resident-memory-bench.ts +1341 -0
  98. package/scripts/run-acp-conformance.ts +290 -0
  99. package/src/async/job-manager.ts +99 -39
  100. package/src/cli/fast-help.ts +1 -1
  101. package/src/cli/plugin-cli.ts +176 -18
  102. package/src/commands/team.ts +19 -1
  103. package/src/config/file-lock.ts +11 -2
  104. package/src/config/settings-schema.ts +53 -2
  105. package/src/coordinator-mcp/question-gate-codec.ts +1 -1
  106. package/src/coordinator-mcp/server.ts +5 -12
  107. package/src/debug/index.ts +42 -17
  108. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -0
  109. package/src/defaults/gjc/skills/ralplan/SKILL.md +25 -20
  110. package/src/defaults/gjc/skills/team/SKILL.md +2 -0
  111. package/src/defaults/gjc/skills/ultragoal/SKILL.md +39 -26
  112. package/src/defaults/gjc/skills/ultragoal/validation-batch-contracts.md +12 -0
  113. package/src/defaults/gjc-defaults.ts +4 -4
  114. package/src/exa/mcp-client.ts +12 -3
  115. package/src/exec/bash-executor.ts +144 -27
  116. package/src/extensibility/extensions/runner.ts +15 -0
  117. package/src/extensibility/extensions/types.ts +18 -1
  118. package/src/extensibility/gjc-plugins/constrained-hooks.ts +8 -2
  119. package/src/extensibility/gjc-plugins/index.ts +19 -2
  120. package/src/extensibility/gjc-plugins/installer.ts +189 -50
  121. package/src/extensibility/gjc-plugins/lifecycle-reconciliation.ts +199 -0
  122. package/src/extensibility/gjc-plugins/lifecycle.ts +626 -0
  123. package/src/extensibility/gjc-plugins/observability.ts +3 -3
  124. package/src/extensibility/gjc-plugins/prompt-appendix.ts +5 -1
  125. package/src/extensibility/gjc-plugins/runtime-adapters.ts +12 -2
  126. package/src/extensibility/gjc-plugins/runtime-quarantine.ts +121 -0
  127. package/src/extensibility/gjc-plugins/schema.ts +84 -12
  128. package/src/extensibility/gjc-plugins/session-validation.ts +9 -3
  129. package/src/extensibility/gjc-plugins/types.ts +141 -0
  130. package/src/extensibility/gjc-plugins/validation.ts +9 -0
  131. package/src/gjc-runtime/launch-tmux.ts +25 -6
  132. package/src/gjc-runtime/launch-worktree.ts +103 -1
  133. package/src/gjc-runtime/ledger-event-renderer.ts +1 -0
  134. package/src/gjc-runtime/managed-owner-admission.ts +1 -1
  135. package/src/gjc-runtime/ralplan-review-conflicts.ts +472 -0
  136. package/src/gjc-runtime/ralplan-runtime.ts +491 -27
  137. package/src/gjc-runtime/team-launch.ts +8 -0
  138. package/src/gjc-runtime/team-runtime.ts +140 -32
  139. package/src/gjc-runtime/team-worker-heartbeat.ts +149 -0
  140. package/src/gjc-runtime/tmux-owner-isolation.ts +10 -0
  141. package/src/gjc-runtime/tmux-sessions.ts +58 -13
  142. package/src/gjc-runtime/ultragoal-evidence.ts +6 -74
  143. package/src/gjc-runtime/ultragoal-runtime.ts +635 -612
  144. package/src/gjc-runtime/workflow-manifest.generated.json +37 -99
  145. package/src/gjc-runtime/workflow-manifest.ts +21 -70
  146. package/src/internal-urls/docs-index.generated.ts +16 -16
  147. package/src/modes/DESIGN.md +104 -0
  148. package/src/modes/acp/acp-agent.ts +298 -186
  149. package/src/modes/components/gjc-bundle-settings.ts +508 -0
  150. package/src/modes/components/irc-sidebar.ts +30 -1
  151. package/src/modes/components/notifications-settings-editor.ts +12 -1
  152. package/src/modes/components/oauth-selector.ts +68 -30
  153. package/src/modes/components/session-observer-overlay.ts +305 -86
  154. package/src/modes/components/settings-selector.ts +56 -5
  155. package/src/modes/components/status-line/gh.ts +16 -0
  156. package/src/modes/components/tool-status-header.ts +3 -15
  157. package/src/modes/components/transcript-viewer-overlay.ts +99 -24
  158. package/src/modes/controllers/command-controller.ts +42 -34
  159. package/src/modes/controllers/event-controller.ts +27 -16
  160. package/src/modes/controllers/extension-ui-controller.ts +193 -34
  161. package/src/modes/controllers/selector-controller.ts +160 -22
  162. package/src/modes/interactive-mode.ts +189 -62
  163. package/src/modes/jobs-observer.ts +1 -1
  164. package/src/modes/runtime-init.ts +3 -0
  165. package/src/modes/theme/theme.ts +11 -9
  166. package/src/modes/types.ts +88 -0
  167. package/src/prompts/agent-fragments/ralplan-persistence.md +4 -3
  168. package/src/prompts/system/eager-todo.md +8 -4
  169. package/src/prompts/system/system-prompt.md +11 -4
  170. package/src/prompts/tools/bash.md +5 -5
  171. package/src/prompts/tools/search-tool-bm25.md +5 -0
  172. package/src/prompts/tools/task.md +1 -1
  173. package/src/reminders/star-reminder.ts +4 -42
  174. package/src/runtime-mcp/manager.ts +19 -4
  175. package/src/sdk/acp/adapter.ts +22 -6
  176. package/src/sdk/broker/lifecycle.ts +18 -13
  177. package/src/sdk/broker/transport.ts +35 -3
  178. package/src/sdk/bus/attachment-registry.ts +5 -1
  179. package/src/sdk/bus/chat-daemon-cli.ts +4 -39
  180. package/src/sdk/bus/chat-daemon-control.ts +7 -3
  181. package/src/sdk/bus/config.ts +18 -2
  182. package/src/sdk/bus/index.ts +661 -126
  183. package/src/sdk/bus/kind-aware-reconciliation.ts +148 -73
  184. package/src/sdk/bus/lifecycle-commands.ts +4 -1
  185. package/src/sdk/bus/lifecycle-control-runtime.ts +403 -88
  186. package/src/sdk/bus/reconciliation-store.ts +95 -16
  187. package/src/sdk/bus/rich-draft.ts +1 -1
  188. package/src/sdk/bus/rich-render.ts +2 -2
  189. package/src/sdk/bus/telegram-adoption-intent.ts +574 -0
  190. package/src/sdk/bus/telegram-cli.ts +33 -8
  191. package/src/sdk/bus/telegram-daemon-cli.ts +1 -0
  192. package/src/sdk/bus/telegram-daemon-contract.ts +12 -4
  193. package/src/sdk/bus/telegram-daemon.ts +916 -23
  194. package/src/sdk/bus/telegram-reference.ts +40 -3
  195. package/src/sdk/bus/threaded-render.ts +48 -3
  196. package/src/sdk/bus/topic-registry.ts +20 -0
  197. package/src/sdk/host/query/revision-store.ts +36 -2
  198. package/src/sdk/host/reverse-leases.ts +44 -7
  199. package/src/sdk/prompt-status.ts +16 -3
  200. package/src/sdk/protocol/operation-inventory.generated.json +33 -0
  201. package/src/sdk/session.ts +89 -9
  202. package/src/session/agent-session.ts +844 -173
  203. package/src/session/artifacts.ts +22 -0
  204. package/src/session/auth-broker-config.ts +10 -3
  205. package/src/session/blob-store.ts +915 -24
  206. package/src/session/internal/managed-session-scope.ts +200 -111
  207. package/src/session/internal/managed-session-storage.ts +492 -62
  208. package/src/session/session-manager.ts +1614 -435
  209. package/src/session/session-storage.ts +38 -45
  210. package/src/session/streaming-output.ts +238 -68
  211. package/src/setup/credential-import.ts +75 -13
  212. package/src/skill-state/active-state.ts +32 -0
  213. package/src/skill-state/workflow-hud.ts +17 -0
  214. package/src/skill-state/workflow-mutation-guard.ts +496 -2
  215. package/src/task/executor.ts +122 -39
  216. package/src/task/index.ts +280 -45
  217. package/src/task/output-manager.ts +24 -14
  218. package/src/task/provider-retry-status.ts +16 -0
  219. package/src/task/receipt.ts +34 -8
  220. package/src/task/render.ts +6 -2
  221. package/src/task/types.ts +108 -0
  222. package/src/tools/ask-answer-registry.ts +12 -6
  223. package/src/tools/ask.ts +86 -55
  224. package/src/tools/ast-edit.ts +1 -0
  225. package/src/tools/ast-grep.ts +1 -0
  226. package/src/tools/bash-interactive.ts +8 -3
  227. package/src/tools/bash.ts +503 -60
  228. package/src/tools/find.ts +1 -0
  229. package/src/tools/image-gen.ts +29 -5
  230. package/src/tools/index.ts +6 -0
  231. package/src/tools/output-meta.ts +108 -14
  232. package/src/tools/path-utils.ts +3 -2
  233. package/src/tools/read.ts +18 -0
  234. package/src/tools/review.ts +5 -7
  235. package/src/tools/search.ts +1 -0
  236. package/src/tools/subagent-render.ts +6 -2
  237. package/src/tools/subagent.ts +6 -0
  238. package/src/tools/telegram-send.ts +6 -3
  239. package/src/utils/gh.ts +39 -0
  240. package/src/utils/image-resize.ts +3 -0
  241. package/src/web/search/providers/anthropic.ts +4 -4
  242. package/src/web/search/providers/kimi.ts +15 -5
  243. package/src/web/search/providers/searxng.ts +47 -4
  244. package/src/web/search/providers/xai.ts +7 -2
  245. package/dist/types/gjc-runtime/ultragoal-pipeline.d.ts +0 -23
  246. package/src/defaults/gjc/skills/ultragoal/pipeline-validation-contracts.md +0 -30
  247. package/src/gjc-runtime/ultragoal-pipeline.ts +0 -306
@@ -8,7 +8,7 @@ import {
8
8
  verifyOwnerOnlyPathSecurity,
9
9
  verifyOwnerOnlyPathSecurityExpected,
10
10
  } from "@gajae-code/natives";
11
- import { logger, pathIsWithin } from "@gajae-code/utils";
11
+ import { hasFsCode, logger, pathIsWithin } from "@gajae-code/utils";
12
12
  import type { ResumeSessionIdentity } from "../session-manager";
13
13
  import {
14
14
  FileSessionStorage,
@@ -31,8 +31,10 @@ import {
31
31
  type ManagedFileSnapshot,
32
32
  ManagedPublishError,
33
33
  ManagedSessionDescendantStore,
34
+ ManagedSessionSecurityError,
34
35
  type ManagedSessionSecurityPolicy,
35
36
  type ManagedStorageLock,
37
+ managedSecurityFailureClassification,
36
38
  prepareManagedDirectoryRoot,
37
39
  publishManagedFileNoReplace,
38
40
  publishManagedTombstone,
@@ -126,7 +128,7 @@ function configuredRootPath(scope: ManagedScope): string {
126
128
  const canonical = fs.realpathSync.native(candidate);
127
129
  return suffix.length === 0 ? canonical : path.join(canonical, ...suffix);
128
130
  } catch (error) {
129
- if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
131
+ if (!hasFsCode(error, "ENOENT")) throw error;
130
132
  const parent = path.dirname(candidate);
131
133
  if (parent === candidate) throw new Error("Configured managed root is unavailable.");
132
134
  suffix.unshift(path.basename(candidate));
@@ -176,6 +178,23 @@ export type ManagedScopeResolution =
176
178
  cause?: { readonly classification: string; readonly diagnostic?: string };
177
179
  };
178
180
 
181
+ function managedScopeFailureCause(error: unknown): { readonly classification: string } {
182
+ return { classification: managedSecurityFailureClassification(error) ?? "binding_invalid" };
183
+ }
184
+
185
+ const managedScopeFailureCodes = new Set([
186
+ "atomic_unavailable",
187
+ "invalid_request",
188
+ "durability_failed",
189
+ "durability_not_provable",
190
+ ]);
191
+
192
+ function managedScopeFailureMessage(error: unknown, fallback: string): string {
193
+ const classification = managedSecurityFailureClassification(error);
194
+ if (classification) return classification;
195
+ return error instanceof Error && managedScopeFailureCodes.has(error.message) ? error.message : fallback;
196
+ }
197
+
179
198
  export interface ManagedCandidate {
180
199
  sessionId: string;
181
200
  path: string;
@@ -276,7 +295,7 @@ function identityFor(cwd: string): NativeIdentity {
276
295
  function verifyExistingManagedScopeDirectory(pathname: string) {
277
296
  if (process.platform !== "win32") return verifyOwnerOnlyPathSecurity(pathname, "directory");
278
297
  const expected = fs.lstatSync(pathname, { bigint: true });
279
- if (!expected.isDirectory() || expected.isSymbolicLink()) throw new Error("Unsafe managed directory");
298
+ if (!expected.isDirectory() || expected.isSymbolicLink()) throw new ManagedSessionSecurityError("reparse_point");
280
299
  const verified = verifyOwnerOnlyPathSecurityExpected(pathname, "directory", expected.dev, expected.ino);
281
300
  const current = fs.lstatSync(pathname, { bigint: true });
282
301
  if (
@@ -285,7 +304,7 @@ function verifyExistingManagedScopeDirectory(pathname: string) {
285
304
  current.dev !== expected.dev ||
286
305
  current.ino !== expected.ino
287
306
  )
288
- throw new Error("Managed session directory changed");
307
+ throw new ManagedSessionSecurityError("identity_mismatch");
289
308
  return verified;
290
309
  }
291
310
 
@@ -404,8 +423,18 @@ function validateExistingBinding(scope: ManagedScope): ManagedScopeResolution |
404
423
  try {
405
424
  raw = captureManagedFileNoFollow(bindingPath).bytes.toString("utf8");
406
425
  } catch (error) {
407
- if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
408
- return { kind: "error", code: "binding_invalid", message: "The managed scope binding is invalid JSON." };
426
+ if (hasFsCode(error, "ENOENT")) return undefined;
427
+ const classification = hasFsCode(error, "EACCES")
428
+ ? "EACCES"
429
+ : hasFsCode(error, "EPERM")
430
+ ? "EPERM"
431
+ : "binding_invalid";
432
+ return {
433
+ kind: "error",
434
+ code: "binding_invalid",
435
+ message: "The managed scope binding is invalid JSON.",
436
+ cause: { classification },
437
+ };
409
438
  }
410
439
  return validateBindingRaw(scope, raw);
411
440
  }
@@ -428,14 +457,16 @@ function resolveManagedScopeInternal(
428
457
  kind: "error",
429
458
  code: "sessions_root_unavailable",
430
459
  message: "The sessions root is not a safe directory.",
460
+ cause: { classification: "reparse_point" },
431
461
  };
432
462
  }
433
463
  } catch (error) {
434
- if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
464
+ if (!hasFsCode(error, "ENOENT")) {
435
465
  return {
436
466
  kind: "error",
437
467
  code: "sessions_root_unavailable",
438
468
  message: "The sessions root could not be inspected.",
469
+ cause: managedScopeFailureCause(error),
439
470
  };
440
471
  }
441
472
  }
@@ -461,21 +492,28 @@ function resolveManagedScopeInternal(
461
492
  kind: "error",
462
493
  code: "sessions_root_unavailable",
463
494
  message: "The sessions root is not a safe directory.",
495
+ cause: { classification: "reparse_point" },
464
496
  };
465
497
  }
466
498
  } catch (error) {
467
- if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
499
+ if (!hasFsCode(error, "ENOENT")) {
468
500
  return {
469
501
  kind: "error",
470
502
  code: "sessions_root_unavailable",
471
503
  message: "The sessions root could not be inspected.",
504
+ cause: managedScopeFailureCause(error),
472
505
  };
473
506
  }
474
507
  }
475
508
  try {
476
509
  const directory = fs.lstatSync(scope.directoryPath);
477
510
  if (!directory.isDirectory() || directory.isSymbolicLink()) {
478
- return { kind: "error", code: "binding_invalid", message: "The managed scope path is not a safe directory." };
511
+ return {
512
+ kind: "error",
513
+ code: "binding_invalid",
514
+ message: "The managed scope path is not a safe directory.",
515
+ cause: { classification: "reparse_point" },
516
+ };
479
517
  }
480
518
  const security = validateNativeSecurityResult(
481
519
  verifyExistingManagedScopeDirectory(scope.directoryPath),
@@ -487,11 +525,17 @@ function resolveManagedScopeInternal(
487
525
  kind: "error",
488
526
  code: "binding_invalid",
489
527
  message: "The managed scope security could not be verified.",
528
+ cause: { classification: security.code ?? "acl_verify_failed" },
490
529
  };
491
530
  }
492
531
  } catch (error) {
493
- if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
494
- return { kind: "error", code: "binding_invalid", message: "The managed scope path could not be inspected." };
532
+ if (!hasFsCode(error, "ENOENT")) {
533
+ return {
534
+ kind: "error",
535
+ code: "binding_invalid",
536
+ message: "The managed scope path could not be inspected.",
537
+ cause: managedScopeFailureCause(error),
538
+ };
495
539
  }
496
540
  }
497
541
  return validateExistingBinding(scope) ?? { kind: "resolved", scope };
@@ -556,7 +600,7 @@ function fsyncManagedParent(pathname: string): void {
556
600
  try {
557
601
  descriptor = fs.openSync(parent, fs.constants.O_RDONLY | fs.constants.O_DIRECTORY | fs.constants.O_NOFOLLOW);
558
602
  } catch (error) {
559
- if ((error as NodeJS.ErrnoException).code === "ENOENT" && path.dirname(parent) !== parent) {
603
+ if (hasFsCode(error, "ENOENT") && path.dirname(parent) !== parent) {
560
604
  parent = path.dirname(parent);
561
605
  continue;
562
606
  }
@@ -778,7 +822,7 @@ export async function ensureManagedScope(
778
822
  try {
779
823
  await publishManagedFileNoReplace(bindingPath, new TextEncoder().encode(binding), undefined, root, policy);
780
824
  } catch (error) {
781
- if ((error as Error).message !== "destination_conflict") throw error;
825
+ if (!(error instanceof Error) || error.message !== "destination_conflict") throw error;
782
826
  bindingCollision = true;
783
827
  }
784
828
  const validated = validateExistingBinding(scope);
@@ -787,14 +831,14 @@ export async function ensureManagedScope(
787
831
 
788
832
  const preparedDirectory = fs.lstatSync(scope.directoryPath, { bigint: true });
789
833
  if (!preparedDirectory.isDirectory() || preparedDirectory.isSymbolicLink())
790
- throw new Error("Managed session directory changed");
834
+ throw new ManagedSessionSecurityError("reparse_point");
791
835
  managedDirectoryIdentities.set(scope, { dev: preparedDirectory.dev, ino: preparedDirectory.ino });
792
836
  return { kind: "resolved", scope };
793
837
  } catch (error) {
794
838
  const publication = error instanceof ManagedPublishError ? error : undefined;
795
839
  const message =
796
840
  publication?.classification ??
797
- (error instanceof Error ? error.message : "The managed scope could not be initialized.");
841
+ managedScopeFailureMessage(error, "The managed scope could not be initialized.");
798
842
  const code =
799
843
  message === "atomic_unavailable" ||
800
844
  message === "invalid_request" ||
@@ -802,14 +846,13 @@ export async function ensureManagedScope(
802
846
  message === "durability_not_provable"
803
847
  ? message
804
848
  : "binding_invalid";
805
-
806
849
  return {
807
850
  kind: "error",
808
851
  code,
809
852
  message,
810
- ...(publication
811
- ? { cause: { classification: publication.classification, diagnostic: publication.diagnostic } }
812
- : { cause: { classification: code } }),
853
+ cause: publication
854
+ ? { classification: publication.classification, diagnostic: publication.diagnostic }
855
+ : managedScopeFailureCause(error),
813
856
  };
814
857
  }
815
858
  }
@@ -873,21 +916,44 @@ function isRecoverableOwnerOnlyModeDrift(error: unknown): boolean {
873
916
  return message === "mode_mismatch" || message.endsWith(": mode_mismatch");
874
917
  }
875
918
 
919
+ type ManagedScopePrepareStage =
920
+ | "retained_identity"
921
+ | "root_authority"
922
+ | "sessions_root"
923
+ | "scope_directory"
924
+ | "scope_identity"
925
+ | "retained_authority"
926
+ | "store"
927
+ | "binding_publish"
928
+ | "binding_read"
929
+ | "binding_validate"
930
+ | "scope_revalidate"
931
+ | "internal_directory"
932
+ | "locks_directory"
933
+ | "receipts_directory"
934
+ | "tombstones_directory";
935
+
876
936
  /** Synchronously create and validate the v2 binding before a default session writer exists. */
877
937
  export function prepareManagedSessionScopeForWriteSync(
878
938
  scope: ManagedScope,
879
939
  policy: ManagedSessionSecurityPolicy = "default",
880
940
  authority?: ManagedCandidateWriteAuthority,
881
941
  ): ManagedScopeResolution {
942
+ let stage: ManagedScopePrepareStage = "retained_identity";
882
943
  try {
883
944
  assertRetainedManagedDirectoryIdentity(scope);
945
+ stage = "root_authority";
884
946
  const root = authority?.rootAuthority ?? scopeRoot(scope, policy);
885
947
  if (authority) bindManagedWriteAuthority(scope, authority);
948
+ stage = "sessions_root";
886
949
  ensureManagedDirectory(scope.sessionsRoot, root, policy);
950
+ stage = "scope_directory";
887
951
  ensureManagedDirectory(scope.directoryPath, root, policy);
952
+ stage = "scope_identity";
888
953
  const preparedDirectory = fs.lstatSync(scope.directoryPath, { bigint: true });
889
954
  if (!preparedDirectory.isDirectory() || preparedDirectory.isSymbolicLink())
890
955
  throw new Error("Managed session directory changed");
956
+ stage = "retained_authority";
891
957
  const retainedAuthority =
892
958
  authority?.retainedAuthority &&
893
959
  authority.retainedDirectory !== undefined &&
@@ -904,6 +970,7 @@ export function prepareManagedSessionScopeForWriteSync(
904
970
  retainedAuthority ? { authority: retainedAuthority, authorityBaseDir: scope.directoryPath } : undefined,
905
971
  policy,
906
972
  );
973
+ stage = "store";
907
974
  let store: ManagedSessionDescendantStore;
908
975
  try {
909
976
  store = buildStore();
@@ -917,15 +984,19 @@ export function prepareManagedSessionScopeForWriteSync(
917
984
  store = buildStore();
918
985
  }
919
986
  const binding = new TextEncoder().encode(`${JSON.stringify(bindingFor(scope))}\n`);
987
+ stage = "binding_publish";
920
988
  try {
921
989
  store.publishNoReplaceSync(MANAGED_SESSION_BINDING_FILE, binding);
922
990
  } catch (error) {
923
- if ((error as Error).message !== "destination_conflict") throw error;
991
+ if (!(error instanceof Error) || error.message !== "destination_conflict") throw error;
924
992
  }
993
+ stage = "binding_read";
925
994
  const capturedBinding = store.readExpected(MANAGED_SESSION_BINDING_FILE);
926
995
  if (!capturedBinding) throw new Error("Managed scope binding is unavailable");
996
+ stage = "binding_validate";
927
997
  const validated = validateBindingRaw(scope, capturedBinding.bytes.toString("utf8"));
928
998
  managedDirectoryAuthorities.set(scope, retainedAuthority);
999
+ stage = "scope_revalidate";
929
1000
  const directoryStat = fs.lstatSync(scope.directoryPath, { bigint: true });
930
1001
  if (
931
1002
  !directoryStat.isDirectory() ||
@@ -935,18 +1006,27 @@ export function prepareManagedSessionScopeForWriteSync(
935
1006
  )
936
1007
  throw new Error("Managed session directory changed");
937
1008
  managedDirectoryIdentities.set(scope, { dev: preparedDirectory.dev, ino: preparedDirectory.ino });
938
- if (validated) return validated;
1009
+ if (validated) {
1010
+ if (validated.kind !== "error") throw new Error("Unexpected managed scope binding result");
1011
+ return {
1012
+ ...validated,
1013
+ cause: { classification: validated.code, diagnostic: "prepare:binding_validate" },
1014
+ };
1015
+ }
939
1016
  const internal = managedInternalDirectory(scope);
1017
+ stage = "internal_directory";
940
1018
  ensureManagedDirectory(internal, root, policy);
1019
+ stage = "locks_directory";
941
1020
  ensureManagedDirectory(path.join(internal, MANAGED_LOCKS_DIRECTORY), root, policy);
1021
+ stage = "receipts_directory";
942
1022
  ensureManagedDirectory(path.join(internal, MANAGED_RECEIPTS_DIRECTORY), root, policy);
1023
+ stage = "tombstones_directory";
943
1024
  ensureManagedDirectory(path.join(internal, MANAGED_TOMBSTONES_DIRECTORY), root, policy);
944
1025
  return { kind: "resolved", scope };
945
1026
  } catch (error) {
946
1027
  const publication = error instanceof ManagedPublishError ? error : undefined;
947
1028
  const message =
948
- publication?.classification ??
949
- (error instanceof Error ? error.message : "Managed write protocol setup failed.");
1029
+ publication?.classification ?? managedScopeFailureMessage(error, "Managed write protocol setup failed.");
950
1030
  const code =
951
1031
  message === "atomic_unavailable" ||
952
1032
  message === "invalid_request" ||
@@ -954,15 +1034,14 @@ export function prepareManagedSessionScopeForWriteSync(
954
1034
  message === "durability_not_provable"
955
1035
  ? message
956
1036
  : "binding_invalid";
1037
+
957
1038
  return {
958
1039
  kind: "error",
959
1040
  code,
960
1041
  message,
961
- ...(publication
962
- ? { cause: { classification: publication.classification, diagnostic: publication.diagnostic } }
963
- : code === "binding_invalid"
964
- ? {}
965
- : { cause: { classification: code } }),
1042
+ cause: publication
1043
+ ? { classification: publication.classification, diagnostic: publication.diagnostic }
1044
+ : { ...managedScopeFailureCause(error), diagnostic: `prepare:${stage}` },
966
1045
  };
967
1046
  }
968
1047
  }
@@ -1725,6 +1804,14 @@ function sameArtifactTreeSnapshot(left: NativeDirectoryTreeSnapshot, right: Nati
1725
1804
  return leftEntries.every((entry, index) => entry === rightEntries[index]);
1726
1805
  }
1727
1806
 
1807
+ function artifactTreePayloadAbsent(snapshot: NativeDirectoryTreeSnapshot): boolean {
1808
+ return (
1809
+ snapshot.entries.length === 1 &&
1810
+ snapshot.entries[0]?.relativePath === "" &&
1811
+ snapshot.entries[0].kind === "directory"
1812
+ );
1813
+ }
1814
+
1728
1815
  function assertRetainedArtifactsAuthority(pending: CleanupReceipt): void {
1729
1816
  if (!pending.detachedArtifactsPath) return;
1730
1817
  if (!pending.expectedArtifactsIdentity || !pending.expectedArtifactsTree) throw new Error("durability_failed");
@@ -1735,10 +1822,12 @@ function assertRetainedArtifactsAuthority(pending: CleanupReceipt): void {
1735
1822
  throw new Error("durability_failed");
1736
1823
  }
1737
1824
  const observed = artifactIdentityAt(pending.detachedArtifactsPath);
1825
+ const observedTree = snapshotArtifactTree(pending.detachedArtifactsPath);
1738
1826
  if (
1739
1827
  !observed ||
1740
1828
  !sameArtifactRootIdentity(observed, pending.expectedArtifactsIdentity) ||
1741
- !sameArtifactTreeSnapshot(snapshotArtifactTree(pending.detachedArtifactsPath), pending.expectedArtifactsTree)
1829
+ (!sameArtifactTreeSnapshot(observedTree, pending.expectedArtifactsTree) &&
1830
+ !artifactTreePayloadAbsent(observedTree))
1742
1831
  )
1743
1832
  throw new Error("binding_invalid");
1744
1833
  }
@@ -1964,6 +2053,15 @@ function snapshotArtifactTree(pathname: string): NativeDirectoryTreeSnapshot {
1964
2053
  return result.snapshot;
1965
2054
  }
1966
2055
 
2056
+ function retainedArtifactPayloadAbsent(pathname: string): boolean {
2057
+ try {
2058
+ return artifactTreePayloadAbsent(snapshotArtifactTree(pathname));
2059
+ } catch (error) {
2060
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return true;
2061
+ throw error;
2062
+ }
2063
+ }
2064
+
1967
2065
  function nextCleanupReceipt(target: RetiredTarget, pending: CleanupReceipt | undefined): CleanupReceipt {
1968
2066
  const attempt = (pending?.attempt ?? 0) + 1;
1969
2067
  const directory = path.dirname(target.path);
@@ -2924,53 +3022,44 @@ export async function reconcileManagedTombstones(
2924
3022
  }
2925
3023
  if (deletion.kind === "cleanup_pending") {
2926
3024
  const retry = nextCleanupReceipt(target, active);
2927
- await publishCleanupPending(
2928
- scope,
2929
- tombstone,
2930
- {
2931
- ...retry,
2932
- expectedArtifactsIdentity:
2933
- deletion.phase === "artifacts"
2934
- ? deletion.artifactsIdentity
2935
- : active.expectedArtifactsIdentity,
2936
- expectedArtifactsTree:
2937
- deletion.phase === "artifacts" ? deletion.artifactsTree : active.expectedArtifactsTree,
2938
- detachedArtifactsPath:
2939
- deletion.phase === "artifacts"
2940
- ? deletion.detachedArtifactsPath
2941
- : active.detachedArtifactsPath,
3025
+ const pendingEvidence = cleanupPendingEvidence(retry, active, deletion);
3026
+ await publishCleanupPending(scope, tombstone, pendingEvidence, lock);
3027
+ if (deletion.phase === "artifacts") {
3028
+ if (!retainedArtifactPayloadAbsent(deletion.detachedArtifactsPath)) continue;
3029
+ await publishCleanupArtifactsRemoved(scope, tombstone, pendingEvidence, lock);
3030
+ deletion = await new FileSessionStorage().deleteSessionVerified({
3031
+ sessionsRoot: scope.sessionsRoot,
3032
+ transcriptPath: target.path,
3033
+ sessionId: target.sessionId,
3034
+ cwd: target.cwd,
3035
+ transcriptIdentity: target.identity,
3036
+ plannedArtifactsPath: pendingEvidence.plannedArtifactsPath,
3037
+ plannedTranscriptPath: pendingEvidence.plannedTranscriptPath,
2942
3038
  detachedTranscriptPath:
2943
- deletion.phase === "transcript"
2944
- ? deletion.detachedTranscriptPath
2945
- : active.detachedTranscriptPath,
2946
- retainedArtifactsSuccessorPath:
2947
- deletion.phase === "artifacts"
2948
- ? deletion.retainedSuccessorPath
2949
- : active.retainedArtifactsSuccessorPath,
2950
- retainedArtifactsPlaceholderPath:
2951
- deletion.phase === "artifacts"
2952
- ? deletion.retainedPlaceholderPath
2953
- : active.retainedArtifactsPlaceholderPath,
2954
- retainedArtifactsUnknownPath:
2955
- deletion.phase === "artifacts"
2956
- ? deletion.retainedUnknownPath
2957
- : active.retainedArtifactsUnknownPath,
2958
- retainedTranscriptSuccessorPath:
2959
- deletion.phase === "transcript"
2960
- ? deletion.retainedSuccessorPath
2961
- : active.retainedTranscriptSuccessorPath,
2962
- retainedTranscriptPlaceholderPath:
2963
- deletion.phase === "transcript"
2964
- ? deletion.retainedPlaceholderPath
2965
- : active.retainedTranscriptPlaceholderPath,
2966
- retainedTranscriptUnknownPath:
2967
- deletion.phase === "transcript"
2968
- ? deletion.retainedUnknownPath
2969
- : active.retainedTranscriptUnknownPath,
2970
- },
2971
- lock,
2972
- );
2973
- continue;
3039
+ pendingEvidence.detachedTranscriptPath ?? observedPending?.detachedTranscriptPath,
3040
+ retainedArtifactsSuccessorPath: pendingEvidence.retainedArtifactsSuccessorPath,
3041
+ retainedArtifactsPlaceholderPath: pendingEvidence.retainedArtifactsPlaceholderPath,
3042
+ retainedArtifactsUnknownPath: pendingEvidence.retainedArtifactsUnknownPath,
3043
+ retainedTranscriptSuccessorPath: pendingEvidence.retainedTranscriptSuccessorPath,
3044
+ retainedTranscriptPlaceholderPath: pendingEvidence.retainedTranscriptPlaceholderPath,
3045
+ retainedTranscriptUnknownPath: pendingEvidence.retainedTranscriptUnknownPath,
3046
+ artifactsRemoved: true,
3047
+ });
3048
+ if (deletion.kind === "cleanup_pending") {
3049
+ if (
3050
+ deletion.phase !== "transcript" ||
3051
+ deletion.detachedTranscriptPath !== pendingEvidence.plannedTranscriptPath
3052
+ )
3053
+ throw new Error("durability_failed");
3054
+ const followup = nextCleanupReceipt(target, pendingEvidence);
3055
+ await publishCleanupPending(
3056
+ scope,
3057
+ tombstone,
3058
+ cleanupPendingEvidence(followup, pendingEvidence, deletion),
3059
+ lock,
3060
+ );
3061
+ }
3062
+ }
2974
3063
  }
2975
3064
  fsyncManagedParent(target.path);
2976
3065
  await publishCleanupCompleted(scope, tombstone, target, lock);
@@ -2984,7 +3073,7 @@ export async function reconcileManagedTombstones(
2984
3073
  }
2985
3074
  }
2986
3075
  } finally {
2987
- if (lock) await lock.release().catch(() => undefined);
3076
+ if (lock) await lock.release();
2988
3077
  }
2989
3078
  }
2990
3079
  }
@@ -3013,8 +3102,7 @@ export async function prepareManagedSessionScopeForWrite(
3013
3102
  } catch (error) {
3014
3103
  const publication = error instanceof ManagedPublishError ? error : undefined;
3015
3104
  const message =
3016
- publication?.classification ??
3017
- (error instanceof Error ? error.message : "Managed write protocol setup failed.");
3105
+ publication?.classification ?? managedScopeFailureMessage(error, "Managed write protocol setup failed.");
3018
3106
  const code =
3019
3107
  message === "atomic_unavailable" ||
3020
3108
  message === "invalid_request" ||
@@ -3026,9 +3114,9 @@ export async function prepareManagedSessionScopeForWrite(
3026
3114
  kind: "error",
3027
3115
  code,
3028
3116
  message,
3029
- ...(publication
3030
- ? { cause: { classification: publication.classification, diagnostic: publication.diagnostic } }
3031
- : { cause: { classification: code } }),
3117
+ cause: publication
3118
+ ? { classification: publication.classification, diagnostic: publication.diagnostic }
3119
+ : managedScopeFailureCause(error),
3032
3120
  };
3033
3121
  }
3034
3122
  }
@@ -3358,7 +3446,7 @@ export async function openManagedCandidateForWrite(
3358
3446
  : "Managed migration failed.",
3359
3447
  };
3360
3448
  } finally {
3361
- if (lock) await lock.release().catch(() => undefined);
3449
+ if (lock) await lock.release();
3362
3450
  }
3363
3451
  }
3364
3452
 
@@ -3510,51 +3598,52 @@ export async function deleteManagedSessionCandidate(
3510
3598
  )
3511
3599
  throw new Error("durability_failed");
3512
3600
  const retry = nextCleanupReceipt(target, active);
3513
- await publishCleanupPending(scope, tombstone, cleanupPendingEvidence(retry, active, deletion), lock);
3601
+ const pendingEvidence = cleanupPendingEvidence(retry, active, deletion);
3602
+ await publishCleanupPending(scope, tombstone, pendingEvidence, lock);
3514
3603
  if (deletion.phase === "artifacts") {
3515
- // A typed retained artifact root proves durable canonical absence; advance
3516
- // to the transcript phase instead of stalling on the retained evidence.
3517
- await publishCleanupArtifactsRemoved(scope, tombstone, active, lock);
3604
+ if (!retainedArtifactPayloadAbsent(deletion.detachedArtifactsPath))
3605
+ return {
3606
+ kind: "cleanup_pending",
3607
+ tombstonePath: tombstone,
3608
+ phase: deletion.phase,
3609
+ message: "Exact cleanup remains pending because descriptor-bound final deletion is unavailable.",
3610
+ };
3611
+ await publishCleanupArtifactsRemoved(scope, tombstone, pendingEvidence, lock);
3518
3612
  deletion = await new FileSessionStorage().deleteSessionVerified({
3519
3613
  sessionsRoot: scope.sessionsRoot,
3520
3614
  transcriptPath: target.path,
3521
3615
  sessionId: target.sessionId,
3522
3616
  cwd: target.cwd,
3523
3617
  transcriptIdentity: target.identity,
3524
- plannedArtifactsPath: active.plannedArtifactsPath,
3525
- plannedTranscriptPath: active.plannedTranscriptPath,
3526
- detachedTranscriptPath: active.detachedTranscriptPath ?? observedPending?.detachedTranscriptPath,
3527
- retainedArtifactsSuccessorPath: active.retainedArtifactsSuccessorPath,
3528
- retainedArtifactsPlaceholderPath: active.retainedArtifactsPlaceholderPath,
3529
- retainedArtifactsUnknownPath: active.retainedArtifactsUnknownPath,
3530
- retainedTranscriptSuccessorPath: active.retainedTranscriptSuccessorPath,
3531
- retainedTranscriptPlaceholderPath: active.retainedTranscriptPlaceholderPath,
3532
- retainedTranscriptUnknownPath: active.retainedTranscriptUnknownPath,
3618
+ plannedArtifactsPath: pendingEvidence.plannedArtifactsPath,
3619
+ plannedTranscriptPath: pendingEvidence.plannedTranscriptPath,
3620
+ detachedTranscriptPath:
3621
+ pendingEvidence.detachedTranscriptPath ?? observedPending?.detachedTranscriptPath,
3622
+ retainedArtifactsSuccessorPath: pendingEvidence.retainedArtifactsSuccessorPath,
3623
+ retainedArtifactsPlaceholderPath: pendingEvidence.retainedArtifactsPlaceholderPath,
3624
+ retainedArtifactsUnknownPath: pendingEvidence.retainedArtifactsUnknownPath,
3625
+ retainedTranscriptSuccessorPath: pendingEvidence.retainedTranscriptSuccessorPath,
3626
+ retainedTranscriptPlaceholderPath: pendingEvidence.retainedTranscriptPlaceholderPath,
3627
+ retainedTranscriptUnknownPath: pendingEvidence.retainedTranscriptUnknownPath,
3533
3628
  artifactsRemoved: true,
3534
3629
  });
3535
3630
  if (deletion.kind === "cleanup_pending") {
3536
- const followup = nextCleanupReceipt(target, active);
3631
+ if (
3632
+ deletion.phase !== "transcript" ||
3633
+ deletion.detachedTranscriptPath !== pendingEvidence.plannedTranscriptPath
3634
+ )
3635
+ throw new Error("durability_failed");
3636
+ const followup = nextCleanupReceipt(target, pendingEvidence);
3537
3637
  await publishCleanupPending(
3538
3638
  scope,
3539
3639
  tombstone,
3540
- cleanupPendingEvidence(followup, active, deletion),
3640
+ cleanupPendingEvidence(followup, pendingEvidence, deletion),
3541
3641
  lock,
3542
3642
  );
3543
- if (
3544
- deletion.phase !== "transcript" ||
3545
- deletion.detachedTranscriptPath !== active.plannedTranscriptPath
3546
- )
3547
- return {
3548
- kind: "cleanup_pending",
3549
- tombstonePath: tombstone,
3550
- phase: deletion.phase,
3551
- message:
3552
- "Exact cleanup remains pending because descriptor-bound final deletion is unavailable.",
3553
- };
3554
3643
  }
3555
3644
  }
3556
- // A typed retained transcript quarantine is durable canonical-absence
3557
- // evidence — never a terminal byte-deletion claim; complete the receipt.
3645
+ // A retained transcript quarantine proves canonical absence and remains
3646
+ // identity-bound in the durable pending receipt.
3558
3647
  }
3559
3648
  fsyncManagedParent(target.path);
3560
3649
  await publishCleanupCompleted(scope, tombstone, target, lock);
@@ -3564,6 +3653,6 @@ export async function deleteManagedSessionCandidate(
3564
3653
  const code = expectedFailure(error);
3565
3654
  return { kind: "error", code, message: error instanceof Error ? error.message : "Managed deletion failed." };
3566
3655
  } finally {
3567
- if (lock) await lock.release().catch(() => undefined);
3656
+ if (lock) await lock.release();
3568
3657
  }
3569
3658
  }