@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
@@ -0,0 +1,456 @@
1
+ /**
2
+ * Announce a live interactive session to Paseo so it appears there as a
3
+ * controllable ACP agent.
4
+ *
5
+ * `gjc setup paseo` registers GJC as an ACP provider, but Paseo only ever knows
6
+ * about the sessions it started itself. Everything else is already in place: an
7
+ * interactive `gjc` hosts a broker-registered SDK endpoint, and GJC's own ACP
8
+ * surface resolves a *live* index entry straight to that running host -- a
9
+ * `session/load` attaches to it instead of resuming a second copy. The one
10
+ * missing step is telling the Paseo daemon that the session exists, which is
11
+ * exactly what `paseo import` does.
12
+ *
13
+ * Two hard constraints shape this module:
14
+ *
15
+ * - The Paseo CLI blocks indefinitely when its daemon is not running (measured:
16
+ * no output and no exit after two minutes). It is therefore NEVER spawned
17
+ * before a cheap socket probe proves something is listening.
18
+ * - ACP `session/load` only attaches to a session the broker index reports as
19
+ * live; for anything else it falls back to `session.resume`, which would start
20
+ * a *second* host for a session that is actually running. Liveness is
21
+ * confirmed before the import, never assumed.
22
+ *
23
+ * Everything here is best-effort: no Paseo, a stopped daemon, or a failed import
24
+ * must never slow down or break an interactive launch. It is also opt-in: the
25
+ * caller checks `paseo.autoImport` before any of this runs, so an installation
26
+ * that never asked for it pays nothing at all.
27
+ */
28
+ import * as fs from "node:fs/promises";
29
+ import * as net from "node:net";
30
+ import * as path from "node:path";
31
+ import { $which, logger } from "@gajae-code/utils";
32
+ import { AgentDirSessionLifecycleClient } from "../../sdk/lifecycle/broker-client";
33
+ import { sessionListPageFromResponse, traverseSessionList } from "../../sdk/session-list";
34
+ import { PROVIDER_EXTENDS, PROVIDER_KEY, resolvePaseoHome } from "./setup-deps";
35
+
36
+ /** Cheap liveness probe of the daemon socket. Only gates the expensive CLI spawn. */
37
+ const DAEMON_PROBE_TIMEOUT_MS = 750;
38
+ /** Bound on the broker `session.list` traversal that proves our own liveness. */
39
+ const BROKER_QUERY_TIMEOUT_MS = 5_000;
40
+ /** `paseo import` spawns Electron, probes the provider, and replays history. */
41
+ const IMPORT_TIMEOUT_MS = 90_000;
42
+ /** Cadence of the retry loop while the daemon is simply not up yet. */
43
+ const RETRY_INTERVAL_MS = 30_000;
44
+ /** Retries stop after ten minutes so a launch never keeps a timer alive forever. */
45
+ const MAX_ATTEMPTS = 20;
46
+
47
+ /** Paseo's own default when nothing configures a listener. */
48
+ const DEFAULT_DAEMON_TARGET: PaseoDaemonTarget = { kind: "tcp", host: "localhost", port: 6767 };
49
+
50
+ export type PaseoDaemonTarget =
51
+ | { readonly kind: "ipc"; readonly socketPath: string }
52
+ | { readonly kind: "tcp"; readonly host: string; readonly port: number };
53
+
54
+ /** Why an announcement did nothing. Each reason is a distinct, actionable state. */
55
+ export type PaseoAnnounceSkip =
56
+ | "no-paseo-config"
57
+ | "no-provider"
58
+ | "cli-missing"
59
+ | "daemon-unreachable"
60
+ | "unsupported-daemon-target"
61
+ | "daemon-auth-required"
62
+ | "session-not-live";
63
+
64
+ export type PaseoAnnounceOutcome =
65
+ | { readonly kind: "imported"; readonly providerKey: string }
66
+ | { readonly kind: "already-imported"; readonly providerKey: string }
67
+ | { readonly kind: "skipped"; readonly reason: PaseoAnnounceSkip }
68
+ | { readonly kind: "failed"; readonly detail: string };
69
+
70
+ export interface PaseoAnnounceDependencies {
71
+ /** `~/.paseo/config.json` (or the `$PASEO_HOME` equivalent). */
72
+ readonly configJson: string;
73
+ /** `~/.paseo` (or `$PASEO_HOME`). */
74
+ readonly paseoHome: string;
75
+ readonly env: NodeJS.ProcessEnv;
76
+ /** Parsed JSON, or `undefined` when the file is absent or unreadable. */
77
+ readJson(file: string): Promise<unknown>;
78
+ /** Absolute path of the `paseo` CLI, or `undefined` when it is not installed. */
79
+ resolveCli(): string | undefined;
80
+ probeDaemon(target: PaseoDaemonTarget): Promise<boolean>;
81
+ /** True only when the broker index reports this session id as live in `cwd`. */
82
+ isSessionLive(sessionId: string, cwd: string): Promise<boolean>;
83
+ runImport(input: {
84
+ readonly cli: string;
85
+ readonly providerKey: string;
86
+ readonly cwd: string;
87
+ readonly sessionId: string;
88
+ readonly daemonTarget: PaseoDaemonTarget;
89
+ readonly daemonHost: string;
90
+ readonly signal?: AbortSignal;
91
+ }): Promise<PaseoAnnounceOutcome>;
92
+ }
93
+
94
+ interface ResolvedDaemonSelection {
95
+ readonly target: PaseoDaemonTarget;
96
+ readonly host: string;
97
+ }
98
+
99
+ function asRecord(value: unknown): Record<string, unknown> | undefined {
100
+ return value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : undefined;
101
+ }
102
+
103
+ /**
104
+ * Pick the Paseo provider entry to import under.
105
+ *
106
+ * `gjc setup paseo` writes `gjc`, and `--mpreset <name>` writes `gjc-<name>`, so
107
+ * the base key is preferred and a preset provider is only used when it is the
108
+ * only one installed. A disabled entry is never selected: Paseo would refuse to
109
+ * launch it.
110
+ */
111
+ export function selectProviderKey(config: unknown): string | undefined {
112
+ const providers = asRecord(asRecord(asRecord(config)?.agents)?.providers);
113
+ if (!providers) return undefined;
114
+ const usable = (key: string): boolean => {
115
+ const entry = asRecord(providers[key]);
116
+ if (entry === undefined || entry.enabled === false || entry.extends !== PROVIDER_EXTENDS) return false;
117
+ if (typeof entry.label !== "string" || !entry.label.startsWith("Gajae Code")) return false;
118
+ const command = entry.command;
119
+ return Array.isArray(command) && command.every(value => typeof value === "string") && command.includes("acp");
120
+ };
121
+ if (usable(PROVIDER_KEY)) return PROVIDER_KEY;
122
+ return Object.keys(providers)
123
+ .filter(key => key.startsWith(`${PROVIDER_KEY}-`) && usable(key))
124
+ .sort()[0];
125
+ }
126
+
127
+ /**
128
+ * Parse one Paseo listener spelling into a connectable target.
129
+ *
130
+ * Mirrors the spellings Paseo's own CLI accepts: `unix://`/`pipe://` prefixes, a
131
+ * bare absolute path, a bare port, `tcp://host:port`, and plain `host:port`.
132
+ */
133
+ export function parseDaemonListen(raw: string): PaseoDaemonTarget | undefined {
134
+ const trimmed = raw.trim();
135
+ if (!trimmed) return undefined;
136
+ if (trimmed.startsWith("unix://")) {
137
+ const socketPath = trimmed.slice("unix://".length).trim();
138
+ return socketPath ? { kind: "ipc", socketPath } : undefined;
139
+ }
140
+ if (trimmed.startsWith("pipe://") || trimmed.startsWith("\\\\.\\pipe\\")) {
141
+ const socketPath = trimmed.startsWith("pipe://") ? trimmed.slice("pipe://".length).trim() : trimmed;
142
+ return socketPath ? { kind: "ipc", socketPath } : undefined;
143
+ }
144
+ if (trimmed.startsWith("/")) return { kind: "ipc", socketPath: trimmed };
145
+ if (/^\d+$/.test(trimmed)) {
146
+ const port = Number(trimmed);
147
+ return Number.isSafeInteger(port) && port >= 1 && port <= 65_535
148
+ ? { kind: "tcp", host: "127.0.0.1", port }
149
+ : undefined;
150
+ }
151
+ const authority = trimmed.startsWith("tcp://") ? trimmed.slice("tcp://".length) : trimmed;
152
+ const withoutQuery = authority.split("?")[0] ?? "";
153
+ const separator = withoutQuery.lastIndexOf(":");
154
+ if (separator <= 0) return undefined;
155
+ const port = Number(withoutQuery.slice(separator + 1));
156
+ if (!Number.isSafeInteger(port) || port < 1 || port > 65_535) return undefined;
157
+ const host = withoutQuery.slice(0, separator).replace(/^\[|]$/g, "");
158
+ return host ? { kind: "tcp", host, port } : undefined;
159
+ }
160
+
161
+ /**
162
+ * Resolve where the daemon listens, in the same precedence Paseo's CLI uses:
163
+ * environment override, then the running daemon's own pid file, then the
164
+ * configured listener, then Paseo's default.
165
+ */
166
+ export async function resolveDaemonTarget(
167
+ config: unknown,
168
+ deps: Pick<PaseoAnnounceDependencies, "env" | "paseoHome" | "readJson">,
169
+ ): Promise<PaseoDaemonTarget | undefined> {
170
+ return (await resolveDaemonSelection(config, deps))?.target;
171
+ }
172
+
173
+ async function resolveDaemonSelection(
174
+ config: unknown,
175
+ deps: Pick<PaseoAnnounceDependencies, "env" | "paseoHome" | "readJson">,
176
+ ): Promise<ResolvedDaemonSelection | undefined> {
177
+ const explicitHost = deps.env.PASEO_HOST?.trim();
178
+ if (explicitHost) {
179
+ const target = parseDaemonListen(explicitHost);
180
+ return target ? { target, host: explicitHost } : undefined;
181
+ }
182
+ const explicitListen = deps.env.PASEO_LISTEN?.trim();
183
+ if (explicitListen) {
184
+ const target = parseDaemonListen(explicitListen);
185
+ return target ? { target, host: explicitListen } : undefined;
186
+ }
187
+
188
+ const pidFile = asRecord(await deps.readJson(path.join(deps.paseoHome, "paseo.pid")));
189
+ const pidListen = typeof pidFile?.listen === "string" ? pidFile.listen : pidFile?.sockPath;
190
+ const pidTarget = typeof pidListen === "string" ? parseDaemonListen(pidListen) : undefined;
191
+ if (pidTarget?.kind === "ipc") return { target: pidTarget, host: pidListen as string };
192
+
193
+ const parsed = asRecord(config);
194
+ const configured = asRecord(parsed?.daemon)?.listen ?? parsed?.listen;
195
+ if (typeof configured !== "string") return { target: DEFAULT_DAEMON_TARGET, host: "localhost:6767" };
196
+ const configuredTarget = parseDaemonListen(configured);
197
+ if (!configuredTarget) return { target: DEFAULT_DAEMON_TARGET, host: "localhost:6767" };
198
+ if (configured.trim() === "127.0.0.1:6767") {
199
+ return { target: DEFAULT_DAEMON_TARGET, host: "localhost:6767" };
200
+ }
201
+ return { target: configuredTarget, host: configured };
202
+ }
203
+
204
+ /**
205
+ * Turn a non-zero `paseo import` into an outcome.
206
+ *
207
+ * Two of Paseo's refusals are not failures:
208
+ *
209
+ * - an already-known session is exactly the state we wanted to reach;
210
+ * - a password-protected daemon (`Cannot connect to daemon ...: Password
211
+ * required`) is a configuration the user chose. The socket probe cannot see
212
+ * it -- the TCP connect succeeds -- so it can only be recognized here, and
213
+ * reporting it would put an error on screen at every single launch for
214
+ * something GJC must not fix on its own. Exporting `PASEO_PASSWORD` makes it
215
+ * work, since the CLI inherits this process's environment.
216
+ */
217
+ export function classifyImportFailure(providerKey: string, detail: string): PaseoAnnounceOutcome {
218
+ if (/already imported/i.test(detail)) return { kind: "already-imported", providerKey };
219
+ if (/cannot connect to daemon[^\n]*password required/i.test(detail)) {
220
+ return { kind: "skipped", reason: "daemon-auth-required" };
221
+ }
222
+ return { kind: "failed", detail: "paseo import failed; inspect the Paseo daemon log" };
223
+ }
224
+
225
+ /**
226
+ * Run the full announcement once.
227
+ *
228
+ * The order is load-bearing: every cheap filesystem check runs before the socket
229
+ * probe, and the socket probe runs before anything spawns the Paseo CLI.
230
+ */
231
+ export async function announceSessionToPaseo(
232
+ input: { readonly sessionId: string; readonly cwd: string },
233
+ deps: PaseoAnnounceDependencies,
234
+ signal?: AbortSignal,
235
+ ): Promise<PaseoAnnounceOutcome> {
236
+ if (signal?.aborted) return { kind: "failed", detail: "Paseo announcement cancelled" };
237
+ const config = await deps.readJson(deps.configJson);
238
+ if (signal?.aborted) return { kind: "failed", detail: "Paseo announcement cancelled" };
239
+ if (config === undefined) return { kind: "skipped", reason: "no-paseo-config" };
240
+
241
+ const providerKey = selectProviderKey(config);
242
+ if (!providerKey) return { kind: "skipped", reason: "no-provider" };
243
+
244
+ const cli = deps.resolveCli();
245
+ if (!cli) return { kind: "skipped", reason: "cli-missing" };
246
+
247
+ const selection = await resolveDaemonSelection(config, deps);
248
+ if ((deps.env.PASEO_HOST?.trim() || deps.env.PASEO_LISTEN?.trim()) && selection === undefined)
249
+ return { kind: "skipped", reason: "unsupported-daemon-target" };
250
+ if (!selection || !(await deps.probeDaemon(selection.target)))
251
+ return { kind: "skipped", reason: "daemon-unreachable" };
252
+ if (signal?.aborted) return { kind: "failed", detail: "Paseo announcement cancelled" };
253
+
254
+ // Importing a session the broker does not yet report as live would make ACP
255
+ // `session/load` resume a copy instead of attaching to the running host.
256
+ if (!(await deps.isSessionLive(input.sessionId, input.cwd))) return { kind: "skipped", reason: "session-not-live" };
257
+
258
+ return await deps.runImport({
259
+ cli,
260
+ providerKey,
261
+ cwd: input.cwd,
262
+ sessionId: input.sessionId,
263
+ daemonTarget: selection.target,
264
+ daemonHost: selection.host,
265
+ signal,
266
+ });
267
+ }
268
+
269
+ /** Only a daemon that is not up yet, or a registration that has not landed yet, is worth retrying. */
270
+ export function isRetryableOutcome(outcome: PaseoAnnounceOutcome): boolean {
271
+ return (
272
+ outcome.kind === "skipped" && (outcome.reason === "daemon-unreachable" || outcome.reason === "session-not-live")
273
+ );
274
+ }
275
+
276
+ export interface PaseoAnnouncementHandle {
277
+ cancel(): void;
278
+ }
279
+
280
+ /**
281
+ * Announce the session, retrying only while Paseo is not ready to hear it.
282
+ *
283
+ * The user routinely opens Paseo after their terminal, so a single attempt at
284
+ * launch would silently never land. Retries are socket probes, not CLI spawns,
285
+ * and every timer is unref'd so the loop can never hold the process open.
286
+ */
287
+ export function startPaseoAnnouncement(
288
+ input: {
289
+ readonly sessionId: string;
290
+ readonly cwd: string;
291
+ isEnabled?: () => boolean;
292
+ onOutcome?: (outcome: PaseoAnnounceOutcome) => void;
293
+ },
294
+ deps: PaseoAnnounceDependencies,
295
+ ): PaseoAnnouncementHandle {
296
+ let cancelled = false;
297
+ let timer: ReturnType<typeof setTimeout> | undefined;
298
+ const controller = new AbortController();
299
+
300
+ const attempt = async (remaining: number): Promise<void> => {
301
+ if (cancelled || input.isEnabled?.() === false) return;
302
+ let outcome: PaseoAnnounceOutcome;
303
+ try {
304
+ outcome = await announceSessionToPaseo(input, deps, controller.signal);
305
+ } catch (error) {
306
+ outcome = { kind: "failed", detail: error instanceof Error ? error.message : String(error) };
307
+ }
308
+ if (cancelled || input.isEnabled?.() === false) return;
309
+ if (isRetryableOutcome(outcome) && remaining > 1) {
310
+ // Logged per attempt: a retried outcome is otherwise completely silent,
311
+ // which is the exact state a user reports as "it never showed up".
312
+ logger.debug("Paseo session announcement retrying", { outcome, remaining: remaining - 1 });
313
+ timer = setTimeout(() => void attempt(remaining - 1), RETRY_INTERVAL_MS);
314
+ timer.unref?.();
315
+ return;
316
+ }
317
+ logger.debug("Paseo session announcement settled", { outcome });
318
+ try {
319
+ input.onOutcome?.(outcome);
320
+ } catch (error) {
321
+ logger.debug("Paseo session announcement outcome handler failed", {
322
+ error: error instanceof Error ? error.message : String(error),
323
+ });
324
+ }
325
+ };
326
+
327
+ void attempt(MAX_ATTEMPTS);
328
+ return {
329
+ cancel(): void {
330
+ cancelled = true;
331
+ controller.abort();
332
+ if (timer) clearTimeout(timer);
333
+ },
334
+ };
335
+ }
336
+
337
+ async function readJson(file: string): Promise<unknown> {
338
+ try {
339
+ return JSON.parse(await Bun.file(file).text());
340
+ } catch {
341
+ return undefined;
342
+ }
343
+ }
344
+
345
+ function probeDaemon(target: PaseoDaemonTarget): Promise<boolean> {
346
+ return new Promise(resolve => {
347
+ let settled = false;
348
+ const settle = (reachable: boolean): void => {
349
+ if (settled) return;
350
+ settled = true;
351
+ socket.destroy();
352
+ resolve(reachable);
353
+ };
354
+ const socket =
355
+ target.kind === "ipc"
356
+ ? net.connect({ path: target.socketPath })
357
+ : net.connect({ host: target.host, port: target.port });
358
+ socket.setTimeout(DAEMON_PROBE_TIMEOUT_MS);
359
+ socket.once("connect", () => settle(true));
360
+ socket.once("timeout", () => settle(false));
361
+ socket.once("error", () => settle(false));
362
+ });
363
+ }
364
+
365
+ function isSessionLive(agentDir: string): (sessionId: string, cwd: string) => Promise<boolean> {
366
+ return async (sessionId, cwd) => {
367
+ const canonicalCwd = await fs.realpath(cwd).catch(() => path.resolve(cwd));
368
+ const client = new AgentDirSessionLifecycleClient(agentDir);
369
+ const pages = await traverseSessionList(
370
+ {},
371
+ pageInput => client.global("session.list", pageInput, { timeoutMs: BROKER_QUERY_TIMEOUT_MS }),
372
+ response => sessionListPageFromResponse(response),
373
+ );
374
+ for (const { sessions } of pages) {
375
+ if (sessionListContainsLiveSession(sessions, sessionId, canonicalCwd)) return true;
376
+ }
377
+ return false;
378
+ };
379
+ }
380
+
381
+ export function sessionListContainsLiveSession(sessions: readonly unknown[], sessionId: string, cwd: string): boolean {
382
+ for (const value of sessions) {
383
+ const session = asRecord(value);
384
+ if (session?.sessionId !== sessionId || session.live !== true) continue;
385
+ const locatorCwd = asRecord(session.locator)?.cwd;
386
+ if (typeof locatorCwd === "string" && path.resolve(locatorCwd) === path.resolve(cwd)) return true;
387
+ }
388
+ return false;
389
+ }
390
+
391
+ /**
392
+ * Run `paseo import` with the session's own environment.
393
+ *
394
+ * GJC never reads a Paseo credential and has no setting of its own for one. The
395
+ * CLI authenticates exactly as it would if the user ran it by hand: if
396
+ * `PASEO_PASSWORD` is exported it is inherited, and if it is not, a
397
+ * password-protected daemon refuses and the announcement becomes a quiet skip.
398
+ */
399
+ function runImport(env: NodeJS.ProcessEnv) {
400
+ return async (input: {
401
+ readonly cli: string;
402
+ readonly providerKey: string;
403
+ readonly cwd: string;
404
+ readonly sessionId: string;
405
+ readonly daemonTarget: PaseoDaemonTarget;
406
+ readonly daemonHost: string;
407
+ readonly signal?: AbortSignal;
408
+ }): Promise<PaseoAnnounceOutcome> => {
409
+ const timeoutController = new AbortController();
410
+ const signal = input.signal
411
+ ? AbortSignal.any([input.signal, timeoutController.signal])
412
+ : timeoutController.signal;
413
+ const timer = setTimeout(() => timeoutController.abort(), IMPORT_TIMEOUT_MS);
414
+ try {
415
+ const child = Bun.spawn(
416
+ [input.cli, "import", "--provider", input.providerKey, "--cwd", input.cwd, input.sessionId],
417
+ {
418
+ stdout: "ignore",
419
+ stderr: "pipe",
420
+ signal,
421
+ env: { ...env, PASEO_HOST: input.daemonHost },
422
+ },
423
+ );
424
+ const [exitCode, stderr] = await Promise.all([child.exited, new Response(child.stderr).text()]);
425
+ if (input.signal?.aborted) return { kind: "failed", detail: "Paseo announcement cancelled" };
426
+ if (timeoutController.signal.aborted)
427
+ return { kind: "failed", detail: `paseo import timed out after ${IMPORT_TIMEOUT_MS}ms` };
428
+ if (exitCode === 0) return { kind: "imported", providerKey: input.providerKey };
429
+ return classifyImportFailure(input.providerKey, stderr.trim() || `paseo import exited with ${exitCode}`);
430
+ } catch (error) {
431
+ return { kind: "failed", detail: error instanceof Error ? error.message : String(error) };
432
+ } finally {
433
+ clearTimeout(timer);
434
+ }
435
+ };
436
+ }
437
+
438
+ export function createDefaultPaseoAnnounceDependencies(
439
+ agentDir: string,
440
+ env: NodeJS.ProcessEnv = process.env,
441
+ ): PaseoAnnounceDependencies {
442
+ const paseoHome = resolvePaseoHome(env);
443
+ return {
444
+ configJson: path.join(paseoHome, "config.json"),
445
+ paseoHome,
446
+ env,
447
+ readJson,
448
+ // Resolve against the injected environment, not the ambient one: the env is a
449
+ // dependency here, and a lookup that ignores it would resolve a different
450
+ // executable than every other step in this module reasons about.
451
+ resolveCli: () => $which("paseo", env.PATH === undefined ? undefined : { PATH: env.PATH }) ?? undefined,
452
+ probeDaemon,
453
+ isSessionLive: isSessionLive(agentDir),
454
+ runImport: runImport(env),
455
+ };
456
+ }
@@ -76,8 +76,20 @@ export interface PaseoSetupDependencies {
76
76
  now(): Date;
77
77
  }
78
78
 
79
- export function createDefaultPaseoPaths(agentDir: string = getAgentDir(), home: string = os.homedir()): PaseoPaths {
80
- const paseoHome = path.join(home, ".paseo");
79
+ export function resolvePaseoHome(env: NodeJS.ProcessEnv = process.env, home: string = os.homedir()): string {
80
+ const configured = env.PASEO_HOME?.trim();
81
+ if (!configured) return path.join(home, ".paseo");
82
+ const expanded =
83
+ configured === "~" ? home : configured.startsWith("~/") ? path.join(home, configured.slice(2)) : configured;
84
+ return path.resolve(expanded);
85
+ }
86
+
87
+ export function createDefaultPaseoPaths(
88
+ agentDir: string = getAgentDir(),
89
+ home: string = os.homedir(),
90
+ env: NodeJS.ProcessEnv = process.env,
91
+ ): PaseoPaths {
92
+ const paseoHome = resolvePaseoHome(env, home);
81
93
  return {
82
94
  configJson: path.join(paseoHome, "config.json"),
83
95
  orchestrationPreferences: path.join(paseoHome, "orchestration-preferences.json"),
@@ -168,7 +180,7 @@ export function parseProviderLs(stdout: string): PaseoLsOutcome {
168
180
 
169
181
  export function createDefaultPaseoSetupDependencies(): PaseoSetupDependencies {
170
182
  return {
171
- paths: createDefaultPaseoPaths(),
183
+ paths: createDefaultPaseoPaths(getAgentDir(), os.homedir(), process.env),
172
184
  runProviderLs,
173
185
  now: () => new Date(),
174
186
  };
@@ -2,3 +2,15 @@
2
2
  export const CANONICAL_GJC_WORKFLOW_SKILLS = ["deep-interview", "ralplan", "ultragoal", "autoresearch"] as const;
3
3
 
4
4
  export type CanonicalGjcWorkflowSkill = (typeof CANONICAL_GJC_WORKFLOW_SKILLS)[number];
5
+
6
+ const CANONICAL_GJC_WORKFLOW_SKILL_SET = new Set<string>(CANONICAL_GJC_WORKFLOW_SKILLS);
7
+
8
+ /** Case-insensitive, Win32-path-equivalent key used for skill collision checks. */
9
+ export function getSkillFilesystemIdentity(name: string): string {
10
+ return name.replace(/[. ]+$/u, "").toLowerCase();
11
+ }
12
+
13
+ /** Whether a name resolves to a bundled workflow under portable filesystem semantics. */
14
+ export function isCanonicalGjcWorkflowSkillFilesystemAlias(name: string): boolean {
15
+ return CANONICAL_GJC_WORKFLOW_SKILL_SET.has(getSkillFilesystemIdentity(name));
16
+ }
@@ -1,12 +1,14 @@
1
- import { describe, expect, test } from "bun:test";
1
+ import { describe, expect, test, vi } from "bun:test";
2
+ import * as nodeFs from "node:fs";
2
3
  import * as fs from "node:fs/promises";
3
4
  import * as path from "node:path";
5
+ import type { LoadContext } from "../capability/types";
4
6
  import { getEmbeddedDefaultGjcSkills } from "../defaults/gjc-defaults";
5
7
  import { buildSkillPromptMessage } from "../extensibility/skills";
6
8
  import { SKILL_FRONTMATTER_SCAN_BYTES, SKILL_FRONTMATTER_SCAN_TOTAL_BYTES, scanSkillDescriptorsFromDir } from "./index";
7
9
 
8
- function makeContext(): any {
9
- return { cwd: process.cwd(), home: process.env.HOME ?? process.cwd(), repoRoot: null };
10
+ function makeContext(root: string): LoadContext {
11
+ return { cwd: root, home: root, repoRoot: null };
10
12
  }
11
13
 
12
14
  describe("skill descriptors", () => {
@@ -22,22 +24,31 @@ describe("skill descriptors", () => {
22
24
  `---\nname: bounded\ndescription: bounded scan\n---\n${body}`,
23
25
  );
24
26
 
25
- const originalFile = Bun.file;
26
- const sliceEnds: number[] = [];
27
- (Bun as any).file = (filePath: string) => {
28
- const file = originalFile(filePath);
29
- return new Proxy(file, {
30
- get(target, property, receiver) {
31
- if (property !== "slice") return Reflect.get(target, property, receiver);
32
- return (start?: number, end?: number) => {
33
- sliceEnds.push(end ?? -1);
34
- return target.slice(start, end);
35
- };
36
- },
37
- });
38
- };
27
+ // Discovery reads the validated descriptor rather than reopening with Bun.file.
28
+ // Observe only the handles this scan opens: a FileHandle prototype spy is
29
+ // process-wide, so unrelated reads from concurrently running tests
30
+ // interleave and make global counts flaky.
31
+ const reads: Array<unknown[]> = [];
32
+ const probe = await fs.open(path.join(skillDir, "SKILL.md"), "r");
33
+ const fileHandlePrototype = Object.getPrototypeOf(probe) as fs.FileHandle;
34
+ await probe.close();
35
+ const originalOpen = nodeFs.promises.open;
36
+ type OpenArgs = Parameters<typeof nodeFs.promises.open>;
37
+ const openSpy = vi.spyOn(nodeFs.promises, "open").mockImplementation((async (...args: OpenArgs) => {
38
+ const handle = await originalOpen(...args);
39
+ const skillPath = args[0];
40
+ if (typeof skillPath === "string" && skillPath.endsWith("SKILL.md")) {
41
+ const originalRead = handle.read.bind(handle);
42
+ handle.read = (async (...readArgs: never[]) => {
43
+ reads.push(readArgs as unknown[]);
44
+ return originalRead(...readArgs);
45
+ }) as typeof handle.read;
46
+ }
47
+ return handle;
48
+ }) as typeof nodeFs.promises.open);
49
+ const readFileSpy = vi.spyOn(fileHandlePrototype, "readFile");
39
50
  try {
40
- const result = await scanSkillDescriptorsFromDir(makeContext(), {
51
+ const result = await scanSkillDescriptorsFromDir(makeContext(root), {
41
52
  dir: root,
42
53
  providerId: "test",
43
54
  level: "project",
@@ -45,9 +56,18 @@ describe("skill descriptors", () => {
45
56
  expect(result.items).toHaveLength(1);
46
57
  expect(Object.hasOwn(result.items[0]?.metadata ?? {}, "content")).toBe(false);
47
58
  expect(JSON.stringify(result.items[0]?.metadata)).not.toContain(bodyMarker);
48
- expect(sliceEnds).toContain(SKILL_FRONTMATTER_SCAN_BYTES);
59
+ const boundedReads = reads.filter(
60
+ call => call[1] === 0 && call[2] === SKILL_FRONTMATTER_SCAN_BYTES && call[3] === 0,
61
+ );
62
+ expect(boundedReads).toHaveLength(1);
63
+ const oversizedReads = reads.filter(
64
+ call => typeof call[2] === "number" && call[2] > SKILL_FRONTMATTER_SCAN_BYTES,
65
+ );
66
+ expect(oversizedReads).toHaveLength(0);
67
+ expect(readFileSpy).not.toHaveBeenCalled();
49
68
  } finally {
50
- (Bun as any).file = originalFile;
69
+ openSpy.mockRestore();
70
+ readFileSpy.mockRestore();
51
71
  }
52
72
  } finally {
53
73
  await fs.rm(root, { recursive: true, force: true });
@@ -63,7 +83,7 @@ describe("skill descriptors", () => {
63
83
  path.join(skillDir, "SKILL.md"),
64
84
  `---\nname: unterminated\ndescription: no closing delimiter\n${"x".repeat(SKILL_FRONTMATTER_SCAN_TOTAL_BYTES * 32)}`,
65
85
  );
66
- const result = await scanSkillDescriptorsFromDir(makeContext(), {
86
+ const result = await scanSkillDescriptorsFromDir(makeContext(root), {
67
87
  dir: root,
68
88
  providerId: "test",
69
89
  level: "project",
@@ -127,6 +127,7 @@ function toSlashCommandRuntime(runtime: TuiSlashCommandRuntime): SlashCommandRun
127
127
  reloadPlugins: async () => {
128
128
  const projectPath = await resolveActiveProjectRegistryPath(ctx.sessionManager.getCwd());
129
129
  clearPluginRootsAndCaches(projectPath ? [projectPath] : undefined);
130
+ await ctx.session.reloadSkills();
130
131
  await ctx.refreshSlashCommandState();
131
132
  await ctx.session.refreshSshTool({ activateIfAvailable: true });
132
133
  },