@gajae-code/coding-agent 0.16.3 → 0.16.6

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 (277) hide show
  1. package/CHANGELOG.md +113 -0
  2. package/dist/types/capability/types.d.ts +4 -0
  3. package/dist/types/cli/args.d.ts +2 -2
  4. package/dist/types/cli/customize-doctor.d.ts +22 -0
  5. package/dist/types/cli/root-flags.d.ts +4 -2
  6. package/dist/types/cli.d.ts +1 -1
  7. package/dist/types/commands/launch.d.ts +1 -1
  8. package/dist/types/config/autorouting-tier-map.d.ts +3 -0
  9. package/dist/types/config/file-lock.d.ts +31 -3
  10. package/dist/types/config/model-registry.d.ts +4 -0
  11. package/dist/types/config/model-resolver.d.ts +9 -0
  12. package/dist/types/config/settings-schema.d.ts +29 -1
  13. package/dist/types/coordinator-mcp/question-state.d.ts +7 -0
  14. package/dist/types/coordinator-mcp/server.d.ts +7 -0
  15. package/dist/types/discovery/claude-plugins.d.ts +3 -1
  16. package/dist/types/discovery/helpers.d.ts +35 -0
  17. package/dist/types/exec/bash-executor.d.ts +17 -1
  18. package/dist/types/exec/bash-shell-guardian.d.ts +15 -0
  19. package/dist/types/exec/bash-shell-runtime-entry.d.ts +2 -0
  20. package/dist/types/exec/bash-shell-signals.d.ts +2 -0
  21. package/dist/types/exec/bash-shell-supervisor-entry.d.ts +1 -0
  22. package/dist/types/exec/bash-shell-supervisor.d.ts +10 -0
  23. package/dist/types/exec/bash-shell-worker-entry.d.ts +2 -0
  24. package/dist/types/exec/bash-shell-worker-protocol.d.ts +63 -0
  25. package/dist/types/exec/bash-shell-worker.d.ts +1 -0
  26. package/dist/types/exec/isolated-shell.d.ts +19 -0
  27. package/dist/types/extensibility/extensions/function-hooks-internal.d.ts +9 -0
  28. package/dist/types/extensibility/extensions/function-hooks.d.ts +169 -0
  29. package/dist/types/extensibility/extensions/index.d.ts +1 -0
  30. package/dist/types/extensibility/extensions/runner.d.ts +46 -6
  31. package/dist/types/extensibility/extensions/types.d.ts +16 -0
  32. package/dist/types/extensibility/extensions/wrapper.d.ts +3 -1
  33. package/dist/types/extensibility/gjc-plugins/constrained-hooks.d.ts +35 -2
  34. package/dist/types/extensibility/gjc-plugins/mcp-policy.d.ts +16 -0
  35. package/dist/types/extensibility/gjc-plugins/paths.d.ts +2 -0
  36. package/dist/types/extensibility/gjc-plugins/types.d.ts +25 -0
  37. package/dist/types/extensibility/hooks/types.d.ts +4 -0
  38. package/dist/types/extensibility/runtime-skill-discovery.d.ts +5 -2
  39. package/dist/types/extensibility/shared-events.d.ts +1 -1
  40. package/dist/types/extensibility/skill-management.d.ts +9 -4
  41. package/dist/types/extensibility/skills.d.ts +2 -0
  42. package/dist/types/gjc-runtime/empty-delete-gc.d.ts +4 -1
  43. package/dist/types/gjc-runtime/linux-proc.d.ts +2 -0
  44. package/dist/types/gjc-runtime/session-state-lock.d.ts +14 -0
  45. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +41 -3
  46. package/dist/types/gjc-runtime/ultragoal-succession.d.ts +251 -0
  47. package/dist/types/modes/components/assistant-message.d.ts +1 -2
  48. package/dist/types/modes/components/bash-execution.d.ts +1 -0
  49. package/dist/types/modes/components/execution-shared.d.ts +1 -0
  50. package/dist/types/modes/components/settings-selector.d.ts +6 -1
  51. package/dist/types/modes/components/status-line/command.d.ts +35 -0
  52. package/dist/types/modes/components/status-line/index.d.ts +1 -0
  53. package/dist/types/modes/components/status-line/types.d.ts +5 -0
  54. package/dist/types/modes/components/tool-status-header.d.ts +14 -0
  55. package/dist/types/modes/controllers/event-controller.d.ts +6 -0
  56. package/dist/types/modes/interactive-mode.d.ts +3 -0
  57. package/dist/types/modes/types.d.ts +7 -0
  58. package/dist/types/runtime-mcp/client.d.ts +9 -1
  59. package/dist/types/runtime-mcp/manager.d.ts +7 -1
  60. package/dist/types/runtime-mcp/pool.d.ts +22 -3
  61. package/dist/types/runtime-mcp/transports/stdio.d.ts +0 -4
  62. package/dist/types/runtime-mcp/types.d.ts +22 -0
  63. package/dist/types/sdk/broker/ensure.d.ts +32 -0
  64. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +0 -1
  65. package/dist/types/sdk/broker/lifecycle.d.ts +6 -0
  66. package/dist/types/sdk/broker/startup-failure.d.ts +4 -1
  67. package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -0
  68. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +12 -0
  69. package/dist/types/sdk/bus/chat-effect-journal.d.ts +17 -2
  70. package/dist/types/sdk/bus/conversation-store.d.ts +11 -0
  71. package/dist/types/sdk/bus/lifecycle-commands.d.ts +8 -8
  72. package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
  73. package/dist/types/sdk/bus/slack-live-provider.d.ts +6 -0
  74. package/dist/types/sdk/bus/slack-provider.d.ts +12 -0
  75. package/dist/types/sdk/bus/topic-registry.d.ts +8 -0
  76. package/dist/types/sdk/client/client.d.ts +6 -0
  77. package/dist/types/sdk/host/host.d.ts +3 -1
  78. package/dist/types/sdk/host/session-runtime.d.ts +26 -1
  79. package/dist/types/sdk/lifecycle/service.d.ts +1 -0
  80. package/dist/types/sdk/router/session-router.d.ts +10 -2
  81. package/dist/types/sdk/session-api-key.d.ts +26 -0
  82. package/dist/types/sdk/session.d.ts +5 -1
  83. package/dist/types/sdk/transport/serve-cli.d.ts +1 -1
  84. package/dist/types/session/agent-session.d.ts +11 -0
  85. package/dist/types/session/fallback-chain-controller.d.ts +15 -0
  86. package/dist/types/session/internal/managed-session-storage.d.ts +7 -0
  87. package/dist/types/session/messages.d.ts +1 -0
  88. package/dist/types/session/session-manager.d.ts +3 -1
  89. package/dist/types/setup/paseo/announce.d.ts +112 -0
  90. package/dist/types/setup/paseo/setup-deps.d.ts +2 -1
  91. package/dist/types/skill-state/canonical-skills.d.ts +4 -0
  92. package/dist/types/system-prompt.d.ts +10 -0
  93. package/dist/types/task/executor.d.ts +5 -1
  94. package/dist/types/tools/bash.d.ts +1 -0
  95. package/dist/types/tools/fetch.d.ts +7 -0
  96. package/dist/types/tools/github-cache.d.ts +2 -1
  97. package/dist/types/tools/index.d.ts +10 -0
  98. package/dist/types/tools/path-utils.d.ts +2 -0
  99. package/dist/types/tools/search.d.ts +6 -0
  100. package/package.json +9 -7
  101. package/scripts/capture-status-line-custom-editor-showcase.ts +151 -0
  102. package/scripts/generate-sdk-operation-inventory.ts +11 -0
  103. package/scripts/verify-status-line-custom-editor-showcase.ts +309 -0
  104. package/src/capability/index.ts +14 -2
  105. package/src/capability/types.ts +4 -0
  106. package/src/cli/args.ts +20 -9
  107. package/src/cli/customize-doctor.ts +155 -12
  108. package/src/cli/root-flags.ts +5 -3
  109. package/src/cli.ts +22 -0
  110. package/src/commands/sdk.ts +74 -31
  111. package/src/config/autorouting-tier-map.ts +1 -0
  112. package/src/config/file-lock-gc.ts +40 -2
  113. package/src/config/file-lock.ts +379 -31
  114. package/src/config/model-preset-registry.ts +120 -19
  115. package/src/config/model-profiles.ts +40 -0
  116. package/src/config/model-registry.ts +73 -23
  117. package/src/config/model-resolver.ts +48 -0
  118. package/src/config/settings-schema.ts +33 -1
  119. package/src/config/settings.ts +1 -0
  120. package/src/coordinator-mcp/question-state.ts +33 -2
  121. package/src/coordinator-mcp/server.ts +477 -101
  122. package/src/crash/sanitize.ts +1 -1
  123. package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +11 -11
  124. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +13 -2
  125. package/src/defaults/gjc/skills/ultragoal/SKILL.md +22 -0
  126. package/src/discovery/builtin.ts +52 -55
  127. package/src/discovery/claude-plugins.ts +12 -4
  128. package/src/discovery/helpers.ts +307 -34
  129. package/src/exec/bash-executor.ts +330 -50
  130. package/src/exec/bash-shell-guardian.ts +388 -0
  131. package/src/exec/bash-shell-runtime-entry.ts +5 -0
  132. package/src/exec/bash-shell-signals.ts +29 -0
  133. package/src/exec/bash-shell-supervisor-entry.ts +3 -0
  134. package/src/exec/bash-shell-supervisor.ts +359 -0
  135. package/src/exec/bash-shell-worker-entry.ts +5 -0
  136. package/src/exec/bash-shell-worker-protocol.ts +36 -0
  137. package/src/exec/bash-shell-worker.ts +116 -0
  138. package/src/exec/isolated-shell.ts +393 -0
  139. package/src/extensibility/extensions/function-hooks-internal.ts +73 -0
  140. package/src/extensibility/extensions/function-hooks.ts +959 -0
  141. package/src/extensibility/extensions/index.ts +22 -0
  142. package/src/extensibility/extensions/loader.ts +53 -2
  143. package/src/extensibility/extensions/runner.ts +895 -264
  144. package/src/extensibility/extensions/types.ts +42 -0
  145. package/src/extensibility/extensions/wrapper.ts +56 -15
  146. package/src/extensibility/gjc-plugins/compiler.ts +228 -60
  147. package/src/extensibility/gjc-plugins/constrained-hooks.ts +321 -23
  148. package/src/extensibility/gjc-plugins/installer.ts +13 -12
  149. package/src/extensibility/gjc-plugins/mcp-policy.ts +81 -52
  150. package/src/extensibility/gjc-plugins/paths.ts +13 -0
  151. package/src/extensibility/gjc-plugins/registry.ts +113 -1
  152. package/src/extensibility/gjc-plugins/runtime-adapters.ts +758 -16
  153. package/src/extensibility/gjc-plugins/runtime-quarantine.ts +20 -2
  154. package/src/extensibility/gjc-plugins/schema.ts +121 -4
  155. package/src/extensibility/gjc-plugins/types.ts +25 -0
  156. package/src/extensibility/hooks/loader.ts +133 -16
  157. package/src/extensibility/hooks/types.ts +28 -0
  158. package/src/extensibility/runtime-skill-discovery.ts +149 -190
  159. package/src/extensibility/shared-events.ts +1 -1
  160. package/src/extensibility/skill-management.ts +200 -52
  161. package/src/extensibility/skills.ts +73 -25
  162. package/src/gjc-runtime/empty-delete-gc.ts +94 -4
  163. package/src/gjc-runtime/gc-runtime.ts +29 -0
  164. package/src/gjc-runtime/linux-proc.ts +14 -0
  165. package/src/gjc-runtime/repository-binding.ts +1 -1
  166. package/src/gjc-runtime/session-state-lock.ts +143 -10
  167. package/src/gjc-runtime/session-state-sidecar.ts +27 -11
  168. package/src/gjc-runtime/state-writer.ts +30 -29
  169. package/src/gjc-runtime/ultragoal-runtime.ts +189 -47
  170. package/src/gjc-runtime/ultragoal-succession.ts +1667 -0
  171. package/src/gjc-runtime/workflow-manifest.generated.json +52 -1
  172. package/src/gjc-runtime/workflow-manifest.ts +16 -0
  173. package/src/harness-control-plane/session-lease.ts +2 -2
  174. package/src/internal-urls/docs-index.generated.ts +12 -12
  175. package/src/lsp/config.ts +10 -3
  176. package/src/lsp/index.ts +1 -1
  177. package/src/lsp/path-trust.ts +69 -14
  178. package/src/main.ts +78 -35
  179. package/src/modes/DESIGN.md +54 -0
  180. package/src/modes/acp/acp-agent.ts +16 -3
  181. package/src/modes/components/assistant-message.ts +22 -9
  182. package/src/modes/components/bash-execution.ts +4 -1
  183. package/src/modes/components/execution-shared.ts +4 -1
  184. package/src/modes/components/model-selector.ts +166 -33
  185. package/src/modes/components/session-observer-overlay.ts +1 -1
  186. package/src/modes/components/settings-selector.ts +1075 -235
  187. package/src/modes/components/status-line/command.ts +190 -0
  188. package/src/modes/components/status-line/index.ts +1 -0
  189. package/src/modes/components/status-line/segments.ts +32 -5
  190. package/src/modes/components/status-line/types.ts +5 -0
  191. package/src/modes/components/tool-status-header.ts +396 -22
  192. package/src/modes/controllers/command-controller.ts +3 -0
  193. package/src/modes/controllers/event-controller.ts +151 -7
  194. package/src/modes/controllers/extension-ui-controller.ts +1 -0
  195. package/src/modes/controllers/input-controller.ts +6 -3
  196. package/src/modes/controllers/selector-controller.ts +21 -0
  197. package/src/modes/interactive-mode.ts +53 -6
  198. package/src/modes/print-mode.ts +5 -1
  199. package/src/modes/prompt-action-autocomplete.ts +54 -27
  200. package/src/modes/runtime-init.ts +2 -0
  201. package/src/modes/types.ts +17 -3
  202. package/src/modes/utils/ui-helpers.ts +2 -0
  203. package/src/prompts/system/system-prompt.md +8 -0
  204. package/src/prompts/tools/search.md +2 -0
  205. package/src/prompts/tools/subagent.md +1 -0
  206. package/src/runtime-mcp/client.ts +60 -17
  207. package/src/runtime-mcp/manager.ts +189 -14
  208. package/src/runtime-mcp/pool.test.ts +218 -0
  209. package/src/runtime-mcp/pool.ts +224 -23
  210. package/src/runtime-mcp/transports/stdio.ts +158 -24
  211. package/src/runtime-mcp/types.ts +24 -0
  212. package/src/sdk/broker/broker.ts +124 -32
  213. package/src/sdk/broker/ensure.ts +248 -41
  214. package/src/sdk/broker/lifecycle-ledger.ts +7 -17
  215. package/src/sdk/broker/lifecycle.ts +56 -6
  216. package/src/sdk/broker/startup-failure.ts +15 -6
  217. package/src/sdk/bus/chat-daemon-control.ts +3 -1
  218. package/src/sdk/bus/chat-daemon-runtime.ts +72 -6
  219. package/src/sdk/bus/chat-effect-journal.ts +132 -7
  220. package/src/sdk/bus/conversation-store.ts +29 -0
  221. package/src/sdk/bus/lifecycle-commands.ts +17 -11
  222. package/src/sdk/bus/slack-conversation.ts +1 -0
  223. package/src/sdk/bus/slack-daemon.ts +478 -49
  224. package/src/sdk/bus/slack-live-provider.ts +12 -0
  225. package/src/sdk/bus/slack-provider.ts +5 -0
  226. package/src/sdk/bus/telegram-daemon.ts +16 -2
  227. package/src/sdk/bus/topic-registry.ts +19 -0
  228. package/src/sdk/cli/master-cli.ts +1 -1
  229. package/src/sdk/cli/session-cli.ts +11 -8
  230. package/src/sdk/client/client.ts +66 -17
  231. package/src/sdk/host/host.ts +29 -9
  232. package/src/sdk/host/query/revision-store.ts +19 -13
  233. package/src/sdk/host/reverse-leases.ts +7 -4
  234. package/src/sdk/host/session-runtime.test.ts +52 -10
  235. package/src/sdk/host/session-runtime.ts +383 -44
  236. package/src/sdk/lifecycle/service.ts +4 -0
  237. package/src/sdk/protocol/operation-inventory.generated.json +77 -0
  238. package/src/sdk/router/session-router.ts +89 -38
  239. package/src/sdk/session-api-key.ts +94 -0
  240. package/src/sdk/session.ts +228 -55
  241. package/src/sdk/transport/relay.ts +35 -13
  242. package/src/sdk/transport/serve-cli.ts +3 -3
  243. package/src/session/agent-session.ts +240 -90
  244. package/src/session/contribution-prep.ts +214 -21
  245. package/src/session/fallback-chain-controller.ts +38 -0
  246. package/src/session/internal/managed-session-storage.ts +19 -5
  247. package/src/session/messages.ts +5 -0
  248. package/src/session/session-manager.ts +29 -5
  249. package/src/session-import/redact.ts +5 -1
  250. package/src/setup/paseo/announce.ts +456 -0
  251. package/src/setup/paseo/setup-deps.ts +15 -3
  252. package/src/skill-state/canonical-skills.ts +12 -0
  253. package/src/skills/skills.test.ts +41 -21
  254. package/src/slash-commands/builtin-registry.ts +1 -0
  255. package/src/system-prompt.ts +29 -3
  256. package/src/task/executor.ts +7 -1
  257. package/src/task/index.ts +21 -4
  258. package/src/tools/bash.ts +63 -19
  259. package/src/tools/cron.ts +1 -1
  260. package/src/tools/fetch.ts +29 -1
  261. package/src/tools/gh.ts +26 -3
  262. package/src/tools/github-cache.ts +68 -16
  263. package/src/tools/index.ts +10 -0
  264. package/src/tools/job.ts +21 -4
  265. package/src/tools/monitor.ts +8 -2
  266. package/src/tools/path-utils.ts +7 -0
  267. package/src/tools/read.ts +14 -2
  268. package/src/tools/render-utils.ts +1 -3
  269. package/src/tools/resource-gc.ts +28 -8
  270. package/src/tools/search.ts +116 -58
  271. package/src/tools/skill-discovery.ts +5 -0
  272. package/src/tools/skill.ts +37 -2
  273. package/src/tools/subagent.ts +6 -3
  274. package/src/tools/tool-catalog.generated.ts +10 -3
  275. package/src/tools/ultragoal-ask-guard.ts +2 -2
  276. package/src/web/insane/bridge.ts +113 -23
  277. package/src/web/search/providers/perplexity.ts +102 -41
@@ -38,6 +38,8 @@ import { initializeWithSettings, loadCapability } from "../discovery";
38
38
  import { inspectClaudeConvention } from "../discovery/claude";
39
39
  import { inspectCodexConvention } from "../discovery/codex";
40
40
  import { scanSkillsFromDir } from "../discovery/helpers";
41
+ import { functionHookGrantHash, normalizeFunctionHookGrant } from "../extensibility/extensions/function-hooks";
42
+ import { compileGjcPluginBundle } from "../extensibility/gjc-plugins/compiler";
41
43
  import { summarizeGjcPluginObservability } from "../extensibility/gjc-plugins/observability";
42
44
  import { loadEffectiveGjcPluginRegistry } from "../extensibility/gjc-plugins/registry";
43
45
  import { getEnabledPlugins } from "../extensibility/plugins/loader";
@@ -47,7 +49,10 @@ import { loadAllMCPConfigs } from "../runtime-mcp/config";
47
49
  import { readDisabledServers } from "../runtime-mcp/config-writer";
48
50
  import { canonicalizeMCPEndpoint } from "../runtime-mcp/pool-key";
49
51
  import { redactMCPEndpoint } from "../runtime-mcp/redaction";
50
- import { CANONICAL_GJC_WORKFLOW_SKILLS } from "../skill-state/canonical-skills";
52
+ import {
53
+ getSkillFilesystemIdentity,
54
+ isCanonicalGjcWorkflowSkillFilesystemAlias,
55
+ } from "../skill-state/canonical-skills";
51
56
  import { expandTilde } from "../tools/path-utils";
52
57
 
53
58
  // =============================================================================
@@ -153,6 +158,29 @@ export interface CustomizeDoctorItem {
153
158
  mcp?: McpSafeSummary;
154
159
  /** Present only for quarantined plugin-bundle surfaces. */
155
160
  quarantineCode?: string;
161
+ /** Metadata-only Function Hook policy projection; hook code is never executed. */
162
+ functionHooks?: CustomizeFunctionHookSummary[];
163
+ }
164
+
165
+ export interface CustomizeFunctionHookSummary {
166
+ extensionId: string;
167
+ order: number;
168
+ event: string;
169
+ target?: string;
170
+ phase?: "before" | "after";
171
+ scope: "user" | "project";
172
+ plugin: string;
173
+ relativePath: string;
174
+ status: "disabled" | "quarantined" | "blocked-isolate" | "legacy-active";
175
+ requestedCapabilities: string[];
176
+ effectiveCapabilities: string[];
177
+ attenuateDownstream: string[];
178
+ networkDestinations: string[];
179
+ filesystemRoots: string[];
180
+ capabilityHash?: string;
181
+ implementationHash?: string;
182
+ quarantineCode?: string;
183
+ remediation: string;
156
184
  }
157
185
 
158
186
  export interface CustomizeDoctorSurface {
@@ -200,8 +228,6 @@ export interface CustomizeDoctorCommandOptions {
200
228
 
201
229
  const REDACTED = "<redacted>";
202
230
 
203
- const BUILT_IN_SKILL_NAMES = new Set<string>(CANONICAL_GJC_WORKFLOW_SKILLS);
204
-
205
231
  const SENSITIVE_KEY_PATTERN =
206
232
  /(?:token|secret|key|credential|password|passwd|pwd|authorization|auth|bearer|cookie|session|apikey)/i;
207
233
 
@@ -655,7 +681,7 @@ async function collectSkills(cwd: string, activeSettings: SettingsInstance): Pro
655
681
  cwd,
656
682
  disabledExtensions: activeSettings.get("disabledExtensions"),
657
683
  });
658
- for (const skill of result.skills) loadedNames.add(skill.name);
684
+ for (const skill of result.skills) loadedNames.add(getSkillFilesystemIdentity(skill.name));
659
685
  }
660
686
 
661
687
  const skillScopeNotes: string[] = [];
@@ -730,7 +756,7 @@ async function collectSkills(cwd: string, activeSettings: SettingsInstance): Pro
730
756
  [`Move the skill under .gjc/skills/<name>/SKILL.md to make it loadable`],
731
757
  );
732
758
  }
733
- if (BUILT_IN_SKILL_NAMES.has(entry.name)) {
759
+ if (isCanonicalGjcWorkflowSkillFilesystemAlias(entry.name)) {
734
760
  return finalizeItem(
735
761
  base,
736
762
  "shadowed",
@@ -740,7 +766,7 @@ async function collectSkills(cwd: string, activeSettings: SettingsInstance): Pro
740
766
  { precedence: { priority: entry.priority, shadowedBy: { provider: "bundled", scope: "native" } } },
741
767
  );
742
768
  }
743
- if (loadedNames.has(entry.name)) {
769
+ if (loadedNames.has(getSkillFilesystemIdentity(entry.name))) {
744
770
  return finalizeItem(base, "loaded", "loaded", "Loaded by session startup.", []);
745
771
  }
746
772
  if (ignoredPatterns.length > 0 || includePatterns.length > 0) {
@@ -809,7 +835,7 @@ async function collectSkills(cwd: string, activeSettings: SettingsInstance): Pro
809
835
  );
810
836
  continue;
811
837
  }
812
- if (BUILT_IN_SKILL_NAMES.has(skill.name)) {
838
+ if (isCanonicalGjcWorkflowSkillFilesystemAlias(skill.name)) {
813
839
  items.push(
814
840
  finalizeItem(
815
841
  base,
@@ -828,7 +854,10 @@ async function collectSkills(cwd: string, activeSettings: SettingsInstance): Pro
828
854
  // in loadedNames even though it loses the name collision to an
829
855
  // earlier-priority source. The collision must report shadowed-by-
830
856
  // precedence, not a second "loaded" item.
831
- const collisionWinner = items.find(i => i.name === skill.name && i.status === "loaded");
857
+ const skillIdentity = getSkillFilesystemIdentity(skill.name);
858
+ const collisionWinner = items.find(
859
+ i => getSkillFilesystemIdentity(i.name) === skillIdentity && i.status === "loaded",
860
+ );
832
861
  if (collisionWinner) {
833
862
  items.push(
834
863
  finalizeItem(
@@ -847,7 +876,7 @@ async function collectSkills(cwd: string, activeSettings: SettingsInstance): Pro
847
876
  );
848
877
  continue;
849
878
  }
850
- if (loadedNames.has(skill.name)) {
879
+ if (loadedNames.has(skillIdentity)) {
851
880
  items.push(
852
881
  finalizeItem(base, "loaded", "loaded", "Loaded by session startup from skills.customDirectories.", []),
853
882
  );
@@ -910,8 +939,11 @@ async function collectHooks(cwd: string, activeSettings: SettingsInstance): Prom
910
939
  nameOf: hook => hook.name,
911
940
  pathOf: hook => hook.path,
912
941
  notExecutedDetail:
913
- "Discovered and shown in the extension dashboard, but standalone sessions do not execute hook files. Runtime hooks come from validated GJC plugin bundles.",
914
- notExecutedRemediation: ["See `gjc plugin list` for plugin bundles that contribute runtime hooks"],
942
+ "Trusted filesystem hook module discovered for session-start activation. Doctor does not execute the module, so dynamic Function Hook registrations and effective runtime order remain opaque until a session loads it.",
943
+ notExecutedRemediation: [
944
+ "Review the trusted hook source before session startup",
945
+ "Use plugin-bundle Function Hooks for declarative grants, hashes, quarantine, and metadata-only policy inspection",
946
+ ],
915
947
  });
916
948
  }
917
949
 
@@ -1140,7 +1172,101 @@ async function collectPluginBundles(cwd: string): Promise<CustomizeDoctorSurface
1140
1172
  const bundleEntries = await loadEffectiveGjcPluginRegistry(cwd, { migrate: false });
1141
1173
  const observability = await summarizeGjcPluginObservability(cwd, { migrate: false });
1142
1174
  const items: CustomizeDoctorItem[] = [...npmItems];
1175
+ let globalFunctionHookOrder = 0;
1143
1176
  for (const entry of bundleEntries) {
1177
+ const surfaces = observability.surfaces.filter(s => s.plugin === entry.name && s.scope === entry.scope);
1178
+ let compiledHooks = new Map<
1179
+ string,
1180
+ Awaited<ReturnType<typeof compileGjcPluginBundle>>["surfaces"]["hooks"][number]
1181
+ >();
1182
+ try {
1183
+ const compiled = await compileGjcPluginBundle(entry.pluginRoot);
1184
+ compiledHooks = new Map(compiled.surfaces.hooks.map(hook => [hook.extensionId, hook]));
1185
+ } catch {
1186
+ // Existing observability reports unreadable/hash-drift bundles. Keep
1187
+ // declarative rows visible and mark unmatched metadata quarantined.
1188
+ }
1189
+ const functionHooks: CustomizeFunctionHookSummary[] = entry.surfaces.hooks.map(hook => {
1190
+ const observed = surfaces.find(surface => surface.extensionId === hook.extensionId);
1191
+ const compiled = compiledHooks.get(hook.extensionId);
1192
+ const hasGrantMetadata =
1193
+ hook.capabilities !== undefined ||
1194
+ hook.networkDestinations !== undefined ||
1195
+ hook.filesystemRoots !== undefined ||
1196
+ hook.capabilityHash !== undefined;
1197
+ let metadataValid =
1198
+ compiled !== undefined &&
1199
+ ((hook.functionHook === true && hasGrantMetadata) ||
1200
+ (hook.functionHook !== true && !hasGrantMetadata && compiled.functionHook !== true));
1201
+ try {
1202
+ const grant = normalizeFunctionHookGrant({
1203
+ capabilities: hook.capabilities,
1204
+ networkDestinations: hook.networkDestinations,
1205
+ filesystemRoots: hook.filesystemRoots,
1206
+ });
1207
+ metadataValid =
1208
+ metadataValid &&
1209
+ hook.functionHook === compiled?.functionHook &&
1210
+ hook.event === compiled?.event &&
1211
+ hook.target === compiled?.target &&
1212
+ hook.phase === compiled?.phase &&
1213
+ hook.relativePath === compiled?.relativePath &&
1214
+ hook.implementationHash === compiled?.implementationHash &&
1215
+ (hook.functionHook !== true ||
1216
+ (hook.capabilityHash?.toLowerCase() === functionHookGrantHash(grant).toLowerCase() &&
1217
+ hook.capabilityHash?.toLowerCase() === compiled?.capabilityHash?.toLowerCase() &&
1218
+ JSON.stringify(grant.capabilities) === JSON.stringify(compiled?.capabilities ?? []) &&
1219
+ JSON.stringify(grant.networkDestinations) ===
1220
+ JSON.stringify(compiled?.networkDestinations ?? []) &&
1221
+ JSON.stringify(grant.filesystemRoots) === JSON.stringify(compiled?.filesystemRoots ?? [])));
1222
+ } catch {
1223
+ metadataValid = false;
1224
+ }
1225
+ const status: CustomizeFunctionHookSummary["status"] =
1226
+ !metadataValid || observed?.status === "quarantined"
1227
+ ? "quarantined"
1228
+ : !entry.enabled || entry.disabledSurfaceIds.includes(hook.extensionId)
1229
+ ? "disabled"
1230
+ : hook.functionHook === true
1231
+ ? "blocked-isolate"
1232
+ : "legacy-active";
1233
+ return {
1234
+ extensionId: hook.extensionId,
1235
+ order: globalFunctionHookOrder++,
1236
+ event: hook.event,
1237
+ ...(hook.target === undefined ? {} : { target: hook.target }),
1238
+ ...(hook.phase === undefined ? {} : { phase: hook.phase }),
1239
+ scope: entry.scope,
1240
+ plugin: entry.name,
1241
+ relativePath: hook.relativePath,
1242
+ status,
1243
+ requestedCapabilities: [...(hook.capabilities ?? [])],
1244
+ effectiveCapabilities: status === "legacy-active" ? [...(hook.capabilities ?? [])] : [],
1245
+ attenuateDownstream: [],
1246
+ networkDestinations: (hook.networkDestinations ?? []).map(destination => {
1247
+ try {
1248
+ const url = new URL(destination);
1249
+ return url.protocol === "https:" && !url.username && !url.password ? url.origin : "[redacted]";
1250
+ } catch {
1251
+ return "[redacted]";
1252
+ }
1253
+ }),
1254
+ filesystemRoots: [...(hook.filesystemRoots ?? [])],
1255
+ ...(hook.capabilityHash === undefined ? {} : { capabilityHash: hook.capabilityHash }),
1256
+ ...(hook.implementationHash === undefined ? {} : { implementationHash: hook.implementationHash }),
1257
+ ...(observed?.quarantineCode === undefined && metadataValid
1258
+ ? {}
1259
+ : { quarantineCode: observed?.quarantineCode ?? "security_policy" }),
1260
+ remediation:
1261
+ status === "blocked-isolate"
1262
+ ? "Function Hook remains disabled until a capability-enforcing plugin isolate is available."
1263
+ : status === "quarantined"
1264
+ ? "Reinstall the bundle and verify installed metadata and file hashes."
1265
+ : status === "disabled"
1266
+ ? `Enable surface ${hook.extensionId} only after reviewing its declared policy.`
1267
+ : "Legacy constrained hook executes under the existing trusted compatibility policy.",
1268
+ };
1269
+ });
1144
1270
  const base: Omit<CustomizeDoctorItem, "status" | "reason" | "detail" | "remediation"> = {
1145
1271
  name: entry.name,
1146
1272
  kind: "plugin-bundle",
@@ -1153,6 +1279,7 @@ async function collectPluginBundles(cwd: string): Promise<CustomizeDoctorSurface
1153
1279
  trust: TRUST_BY_KIND["plugin-bundle"],
1154
1280
  restartRequired: true,
1155
1281
  precedence: { priority: 0 },
1282
+ functionHooks,
1156
1283
  };
1157
1284
  if (!entry.enabled) {
1158
1285
  items.push(
@@ -1166,7 +1293,6 @@ async function collectPluginBundles(cwd: string): Promise<CustomizeDoctorSurface
1166
1293
  );
1167
1294
  continue;
1168
1295
  }
1169
- const surfaces = observability.surfaces.filter(s => s.plugin === entry.name && s.scope === entry.scope);
1170
1296
  const quarantined = surfaces.filter(s => s.status === "quarantined");
1171
1297
  if (quarantined.length > 0) {
1172
1298
  const codes = Array.from(new Set(quarantined.map(s => s.quarantineCode ?? "unknown"))).join(", ");
@@ -1574,6 +1700,23 @@ export function renderCustomizeDoctorText(report: CustomizeDoctorReport): string
1574
1700
  lines.push(` reason: ${item.reason} — ${item.detail}`);
1575
1701
  lines.push(` source: ${item.path}`);
1576
1702
  if (item.quarantineCode) lines.push(` quarantine code: ${item.quarantineCode}`);
1703
+ for (const hook of item.functionHooks ?? []) {
1704
+ lines.push(
1705
+ ` function hook[${hook.order}]: ${hook.extensionId} event=${hook.event}${hook.target ? ` target=${hook.target}` : ""} status=${hook.status}`,
1706
+ );
1707
+ lines.push(` provenance: scope=${hook.scope} plugin=${hook.plugin} path=${hook.relativePath}`);
1708
+ lines.push(
1709
+ ` grants: requested=${hook.requestedCapabilities.join(",") || "none"} effective=${hook.effectiveCapabilities.join(",") || "none"} attenuate=${hook.attenuateDownstream.join(",") || "none"}`,
1710
+ );
1711
+ if (hook.networkDestinations.length > 0)
1712
+ lines.push(` network destinations: ${hook.networkDestinations.join(",")}`);
1713
+ if (hook.filesystemRoots.length > 0)
1714
+ lines.push(` filesystem roots: ${hook.filesystemRoots.join(",")}`);
1715
+ if (hook.capabilityHash) lines.push(` capability hash: ${hook.capabilityHash}`);
1716
+ if (hook.implementationHash) lines.push(` implementation hash: ${hook.implementationHash}`);
1717
+ if (hook.quarantineCode) lines.push(` quarantine code: ${hook.quarantineCode}`);
1718
+ lines.push(` remediation: ${hook.remediation}`);
1719
+ }
1577
1720
  for (const fix of item.remediation) lines.push(` fix: ${fix}`);
1578
1721
  lines.push(` trust: ${item.trust}`);
1579
1722
  lines.push(` restart required: ${item.restartRequired ? "yes (new session)" : "no"}`);
@@ -1,6 +1,8 @@
1
+ import { THINKING_EFFORTS } from "@gajae-code/ai/core";
1
2
  import { type FlagDescriptor, Flags } from "@gajae-code/utils/cli";
2
3
 
3
- const ROOT_THINKING_EFFORTS = ["minimal", "low", "medium", "high", "xhigh", "max"] as const;
4
+ export const ROOT_THINKING_LEVELS = ["off", ...THINKING_EFFORTS] as const;
5
+ export type RootThinkingLevel = (typeof ROOT_THINKING_LEVELS)[number];
4
6
 
5
7
  /** Public launch flags shared by root help, completion, and the launch command. */
6
8
  export const ROOT_LAUNCH_FLAGS = {
@@ -63,8 +65,8 @@ export const ROOT_LAUNCH_FLAGS = {
63
65
  tmux: Flags.boolean({ description: "Launch interactive startup inside tmux" }),
64
66
  tools: Flags.string({ description: "Comma-separated list of tools to enable (default: all)" }),
65
67
  thinking: Flags.string({
66
- description: `Set thinking level: ${ROOT_THINKING_EFFORTS.join(", ")}`,
67
- options: ROOT_THINKING_EFFORTS,
68
+ description: `Set thinking level: ${ROOT_THINKING_LEVELS.join(", ")}`,
69
+ options: ROOT_THINKING_LEVELS,
68
70
  }),
69
71
  "no-rules": Flags.boolean({ description: "Disable rules discovery and loading" }),
70
72
  export: Flags.string({ description: "Export session file to HTML and exit" }),
package/src/cli.ts CHANGED
@@ -10,6 +10,15 @@ import { APP_NAME, formatBunRuntimeError, MIN_BUN_VERSION, VERSION } from "@gaja
10
10
  import { runFixtureReport } from "./cli/fixture-report";
11
11
  import { ROOT_LAUNCH_FLAGS } from "./cli/root-flags";
12
12
  import QuickLane from "./commands/quick-lane";
13
+ import { runBashShellGuardian } from "./exec/bash-shell-guardian";
14
+ import { runBashShellSupervisor } from "./exec/bash-shell-supervisor";
15
+ import { runBashShellWorker } from "./exec/bash-shell-worker";
16
+ import {
17
+ BASH_SHELL_RUNTIME_ARG,
18
+ BASH_SHELL_SUPERVISOR_ARG,
19
+ BASH_SHELL_WORKER_ARG,
20
+ } from "./exec/bash-shell-worker-protocol";
21
+ import { smokeTestIsolatedShell } from "./exec/isolated-shell";
13
22
  import { smokeTestTabWorker } from "./tools/browser/tab-worker-smoke";
14
23
 
15
24
  if (Bun.semver.order(Bun.version, MIN_BUN_VERSION) < 0) {
@@ -340,6 +349,7 @@ async function runSmokeTest(): Promise<void> {
340
349
  const { runNativeSmokeTest } = await import("./cli/native-smoke");
341
350
  await runNativeSmokeTest();
342
351
  await smokeTestTabWorker();
352
+ await smokeTestIsolatedShell();
343
353
  process.stdout.write("smoke-test: ok\n");
344
354
  }
345
355
 
@@ -392,6 +402,18 @@ export function routeRootArgv(argv: readonly string[]): string[] {
392
402
 
393
403
  /** Run the CLI with the given argv (no `process.argv` prefix). */
394
404
  export async function runCli(argv: string[]): Promise<void> {
405
+ if (argv.length === 1 && argv[0] === BASH_SHELL_WORKER_ARG) {
406
+ await runBashShellGuardian();
407
+ return;
408
+ }
409
+ if (argv.length === 1 && argv[0] === BASH_SHELL_SUPERVISOR_ARG) {
410
+ await runBashShellSupervisor();
411
+ return;
412
+ }
413
+ if (argv.length === 1 && argv[0] === BASH_SHELL_RUNTIME_ARG) {
414
+ await runBashShellWorker();
415
+ return;
416
+ }
395
417
  // macOS malloc-env launch boundary. Re-exec once with a scrubbed environment
396
418
  // BEFORE any fast path or subprocess spawn, so the startup env snapshot Bun hands
397
419
  // to every child lane (Bun.spawn defaults, node:child_process, native PTY, tmux
@@ -13,6 +13,7 @@ import { initTheme } from "../modes/theme/theme";
13
13
  import { ACP_MCP_REQUEST_TIMEOUT_MS, ACP_MCP_STARTUP_HEADROOM_MS } from "../sdk/acp/mcp";
14
14
  import { Broker } from "../sdk/broker/broker";
15
15
  import { readBrokerDiscovery } from "../sdk/broker/discovery";
16
+ import { reconcileBrokerGenerationForStartup, withBrokerStartupLock } from "../sdk/broker/ensure";
16
17
  import { completeBrokerProcess } from "../sdk/broker/internal";
17
18
  import {
18
19
  type LifecycleTranscriptEvidence,
@@ -1086,59 +1087,101 @@ export default class Sdk extends Command {
1086
1087
  await runSessionHost();
1087
1088
  return;
1088
1089
  }
1089
- const agentDir = internal.agentDir;
1090
- const broker = new Broker({
1091
- agentDir,
1092
- masterOrphanGraceMs: (await Settings.loadForScope({ cwd: process.cwd(), agentDir })).get(
1093
- "sdk.masterOrphanGraceMs",
1094
- ),
1095
- resolveDirectoryMigration: async cwd => {
1096
- const settings = await Settings.loadForScope({ cwd, agentDir });
1090
+ const agentDir = path.resolve(internal.agentDir);
1091
+ let broker: Broker | undefined;
1092
+ try {
1093
+ broker = await withBrokerStartupLock(agentDir, async deadline => {
1094
+ const remainingMs = Math.max(1, deadline - Date.now());
1095
+ const testWatchdogMs = Number(process.env.GJC_SDK_TEST_BROKER_STARTUP_WATCHDOG_MS ?? 0);
1096
+ const watchdogMs =
1097
+ Number.isSafeInteger(testWatchdogMs) && testWatchdogMs > 0 && testWatchdogMs <= remainingMs
1098
+ ? testWatchdogMs
1099
+ : remainingMs;
1100
+ const startupWatchdog = setTimeout(() => {
1101
+ process.stderr.write(`SDK broker startup exceeded its ${watchdogMs}ms fence deadline.\n`);
1102
+ process.exit(1);
1103
+ }, watchdogMs);
1097
1104
  try {
1098
- const policy = settings.get("session.directoryMigration");
1099
- return policy === "disabled" ? "disabled" : "copy-retain";
1105
+ if (await reconcileBrokerGenerationForStartup({ agentDir }, deadline)) return undefined;
1106
+ if (process.env.GJC_SDK_TEST_BROKER_STARTUP_STALL === "1") {
1107
+ const stalled = Promise.withResolvers<void>();
1108
+ await stalled.promise;
1109
+ }
1110
+ const startupDelayMs = Number(process.env.GJC_SDK_TEST_BROKER_STARTUP_DELAY_MS ?? 0);
1111
+ if (Number.isSafeInteger(startupDelayMs) && startupDelayMs > 0 && startupDelayMs <= 10_000)
1112
+ await Bun.sleep(startupDelayMs);
1113
+ const candidate = new Broker({
1114
+ agentDir,
1115
+ masterOrphanGraceMs: (await Settings.loadForScope({ cwd: process.cwd(), agentDir })).get(
1116
+ "sdk.masterOrphanGraceMs",
1117
+ ),
1118
+ resolveDirectoryMigration: async cwd => {
1119
+ const settings = await Settings.loadForScope({ cwd, agentDir });
1120
+ try {
1121
+ const policy = settings.get("session.directoryMigration");
1122
+ return policy === "disabled" ? "disabled" : "copy-retain";
1123
+ } finally {
1124
+ await settings.close();
1125
+ }
1126
+ },
1127
+ });
1128
+ broker = candidate;
1129
+ await candidate.start();
1130
+ return candidate;
1100
1131
  } finally {
1101
- await settings.close();
1132
+ clearTimeout(startupWatchdog);
1102
1133
  }
1103
- },
1104
- });
1105
- try {
1106
- await broker.start();
1134
+ });
1107
1135
  } catch (error) {
1136
+ if (broker) {
1137
+ try {
1138
+ await broker.stop();
1139
+ } catch {
1140
+ // Preserve the startup/fence failure that made this bootstrap unusable.
1141
+ }
1142
+ }
1108
1143
  // This process spawns detached with stdio ignored (see ensure.ts), so the
1109
1144
  // durable marker is the only channel the caller has to see why start()
1110
1145
  // failed instead of a bare exit code (#3963).
1111
- await writeBrokerStartupFailureMarker(agentDir, {
1112
- reason: error instanceof Error ? error.message : String(error),
1113
- exitCode: 1,
1114
- signal: null,
1115
- pid: process.pid,
1116
- });
1146
+ const incarnation = processIncarnation(process.pid);
1147
+ if (incarnation)
1148
+ await writeBrokerStartupFailureMarker(agentDir, {
1149
+ reason: error instanceof Error ? error.message : String(error),
1150
+ exitCode: 1,
1151
+ signal: null,
1152
+ pid: process.pid,
1153
+ incarnation,
1154
+ });
1117
1155
  throw error;
1118
1156
  }
1119
- if (!broker.ownsDiscovery) {
1157
+ if (!broker) return;
1158
+ const runningBroker = broker;
1159
+ if (!runningBroker.ownsDiscovery) {
1120
1160
  // Another broker owns discovery; this process exits cleanly (code 0) as
1121
1161
  // the race loser. Record why so a caller polling for a winner that never
1122
1162
  // appears can diagnose the loss instead of seeing only a bare exit 0.
1123
- await writeBrokerStartupFailureMarker(agentDir, {
1124
- reason: "Another broker owns the lock/discovery; this broker exited as the race loser.",
1125
- exitCode: 0,
1126
- signal: null,
1127
- pid: process.pid,
1128
- });
1163
+ const incarnation = processIncarnation(process.pid);
1164
+ if (incarnation)
1165
+ await writeBrokerStartupFailureMarker(agentDir, {
1166
+ reason: "Another broker owns the lock/discovery; this broker exited as the race loser.",
1167
+ exitCode: 0,
1168
+ signal: null,
1169
+ pid: process.pid,
1170
+ incarnation,
1171
+ });
1129
1172
  return;
1130
1173
  }
1131
1174
  // A live broker must not keep advertising sessions whose host process is
1132
1175
  // gone; the sweep is the broker-side half of the host reaping bound.
1133
- const stopSweep = startBrokerDeadRegistrationSweep(broker);
1176
+ const stopSweep = startBrokerDeadRegistrationSweep(runningBroker);
1134
1177
  const stop = () => {
1135
1178
  stopSweep();
1136
- void broker.stop();
1179
+ void runningBroker.stop();
1137
1180
  };
1138
1181
  process.once("SIGTERM", stop);
1139
1182
  process.once("SIGINT", stop);
1140
1183
  try {
1141
- await completeBrokerProcess(broker);
1184
+ await completeBrokerProcess(runningBroker);
1142
1185
  } finally {
1143
1186
  stopSweep();
1144
1187
  }
@@ -359,6 +359,7 @@ export const TIER_MAP_SKIP_LIST = {
359
359
  "nvidia/poolside/laguna-xs-2.1": { rationale: "post-rebase catalog addition from dev; not yet curated" },
360
360
  "nvidia/thinkingmachines/inkling": { rationale: "post-rebase catalog addition from dev; not yet curated" },
361
361
  "nvidia/upstage/solar-10.7b-instruct": { rationale: "post-rebase catalog addition from dev; not yet curated" },
362
+ "openai-codex/gpt-6-astra": { rationale: "GPT-6 Astra catalog addition (2026-09-03 release); not yet curated" },
362
363
  "openai-codex/gpt-daybreak-blue-latest": { rationale: "post-rebase catalog addition from dev; not yet curated" },
363
364
  "opencode-go/deepseek-v4-flash-vision-exp": {
364
365
  rationale: "post-rebase catalog addition from dev; not yet curated",
@@ -17,7 +17,12 @@ import type {
17
17
  } from "../gjc-runtime/gc-runtime";
18
18
  import { gcPidStatusLabel } from "../gjc-runtime/gc-runtime";
19
19
  import { resolveReceiptSpoolDir } from "../harness-control-plane/receipt-spool";
20
- import { readFileLockObservationForGc, removeFileLockDirForGc } from "./file-lock";
20
+ import {
21
+ fileLockStagingOwnerPid,
22
+ inspectFileLockStagingDir,
23
+ readFileLockObservationForGc,
24
+ removeFileLockDirForGc,
25
+ } from "./file-lock";
21
26
 
22
27
  const MAX_WALK_DEPTH = 6;
23
28
  /** Default per-root walk budget. Truncation is a warning, not a hard error. */
@@ -67,7 +72,27 @@ function keptMalformedRecord(lockDir: string): GcRecord {
67
72
  };
68
73
  }
69
74
 
75
+ function stagingOwnerStatus(ctx: GcContext, pid: number): "alive" | "dead" | "unknown" {
76
+ const status = gcPidStatusLabel(ctx.probe(pid));
77
+ return status === "eperm" ? "unknown" : status;
78
+ }
79
+
70
80
  async function collectLockRecord(lockDir: string, ctx: GcContext): Promise<GcRecord> {
81
+ if (fileLockStagingOwnerPid(path.basename(lockDir)) !== null) {
82
+ const staging = await inspectFileLockStagingDir(lockDir, pid => stagingOwnerStatus(ctx, pid));
83
+ return {
84
+ store: "file_locks",
85
+ id: lockDir,
86
+ path: lockDir,
87
+ pid: staging.pid,
88
+ pid_status: staging.status,
89
+ status: "file_lock_staging_orphan",
90
+ stale: staging.status === "dead",
91
+ removable: staging.status === "dead",
92
+ action: "none",
93
+ reason: staging.reason,
94
+ };
95
+ }
71
96
  const observation = await readFileLockObservationForGc(lockDir);
72
97
  const info = observation?.info;
73
98
  if (!info) return keptMalformedRecord(lockDir);
@@ -131,7 +156,7 @@ async function walkForLockDirs(
131
156
  state.entries++;
132
157
  if (!stat.isDirectory() || stat.isSymbolicLink()) return;
133
158
 
134
- if (path.basename(dir).endsWith(".lock")) {
159
+ if (path.basename(dir).endsWith(".lock") || fileLockStagingOwnerPid(path.basename(dir)) !== null) {
135
160
  lockDirs.add(dir);
136
161
  return;
137
162
  }
@@ -152,6 +177,11 @@ async function walkForLockDirs(
152
177
  state.truncated = true;
153
178
  return;
154
179
  }
180
+ // Session index locks live directly in sessions/, but session payload trees remain pruned.
181
+ if (entry === "sessions") {
182
+ await walkForLockDirs(path.join(dir, entry), MAX_WALK_DEPTH - 1, state, lockDirs, errors, maxWalkEntries);
183
+ continue;
184
+ }
155
185
  if (PRUNED_DIR_NAMES.has(entry)) continue;
156
186
  await walkForLockDirs(path.join(dir, entry), depth + 1, state, lockDirs, errors, maxWalkEntries);
157
187
  }
@@ -204,6 +234,14 @@ export const fileLocksGcAdapter: GcStoreAdapter = {
204
234
  },
205
235
  async prune(record: GcRecord, ctx: GcContext): Promise<GcPruneOutcome> {
206
236
  const lockDir = record.path ?? record.id;
237
+ if (fileLockStagingOwnerPid(path.basename(lockDir)) !== null) {
238
+ try {
239
+ const staging = await inspectFileLockStagingDir(lockDir, pid => stagingOwnerStatus(ctx, pid), true);
240
+ return staging.removed ? { removed: true } : { removed: false, skipped: staging.reason };
241
+ } catch (error) {
242
+ return { removed: false, error: errorMessage(error) };
243
+ }
244
+ }
207
245
  const observation = await readFileLockObservationForGc(lockDir);
208
246
  const info = observation?.info;
209
247
  if (!info) return { removed: false, skipped: "lock_no_longer_dead_or_missing" };