@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
@@ -12,16 +12,32 @@
12
12
  * writing a skill file into a scope, and per-skill enable/disable. The import
13
13
  * preview/apply transaction and the UI itself are owned by #4291.
14
14
  */
15
+ import * as nodeFs from "node:fs";
15
16
  import * as fs from "node:fs/promises";
16
17
  import * as path from "node:path";
17
- import { getTrustedHomeDir, parseFrontmatter } from "@gajae-code/utils";
18
+ import {
19
+ getAgentDir,
20
+ getAgentProfileAuthority,
21
+ getTrustedHomeDir,
22
+ normalizePathForComparison,
23
+ parseFrontmatter,
24
+ } from "@gajae-code/utils";
18
25
  import { findRepoRoot } from "../capability/fs";
19
26
  import type { Skill as CapabilitySkill } from "../capability/skill";
20
27
  import { resolveSkillScopeTrust } from "../config/skill-settings-defaults";
21
28
  import { scanClaudeProjectSkills, scanClaudeUserSkills } from "../discovery/claude";
22
29
  import { scanCodexProjectSkills, scanCodexUserSkills } from "../discovery/codex";
23
- import { compareSkillOrder, SOURCE_PATHS, scanSkillsFromDir } from "../discovery/helpers";
24
- import { CANONICAL_GJC_WORKFLOW_SKILLS } from "../skill-state/canonical-skills";
30
+ import {
31
+ compareSkillOrder,
32
+ getAncestorDirs,
33
+ getUserSkillScanDirs,
34
+ resolveUserAgentDir,
35
+ scanSkillsFromDir,
36
+ } from "../discovery/helpers";
37
+ import {
38
+ CANONICAL_GJC_WORKFLOW_SKILLS,
39
+ isCanonicalGjcWorkflowSkillFilesystemAlias,
40
+ } from "../skill-state/canonical-skills";
25
41
  export type SkillScope = "project" | "user";
26
42
  export type ConventionSkillHost = "claude" | "codex";
27
43
 
@@ -61,6 +77,7 @@ export interface ConventionSkillImportSource {
61
77
  export interface WriteNativeSkillInput {
62
78
  cwd: string;
63
79
  home?: string;
80
+ agentDir?: string;
64
81
  scope: SkillScope;
65
82
  name: string;
66
83
  content: string;
@@ -93,28 +110,127 @@ export class SkillFrontmatterError extends Error {
93
110
  }
94
111
  }
95
112
 
96
- const BUILT_IN_SKILL_NAMES = new Set<string>(CANONICAL_GJC_WORKFLOW_SKILLS);
113
+ function isProtectedSkillName(name: string): boolean {
114
+ return isCanonicalGjcWorkflowSkillFilesystemAlias(name);
115
+ }
97
116
 
98
- function getRuntimeHome(): string {
99
- return getTrustedHomeDir();
117
+ function assertSafeSkillName(name: string): void {
118
+ if (
119
+ name === "." ||
120
+ name === ".." ||
121
+ /[. ]$/u.test(name) ||
122
+ name.includes("/") ||
123
+ name.includes("\\") ||
124
+ name.includes("\0") ||
125
+ path.basename(name) !== name
126
+ ) {
127
+ throw new SkillFrontmatterError(`skill name must be a single path segment: ${name}`);
128
+ }
129
+ }
130
+
131
+ function isPathWithin(root: string, candidate: string): boolean {
132
+ const relative = path.relative(root, candidate);
133
+ return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
134
+ }
135
+
136
+ async function ensureSafeDescendantDirectory(root: string, segments: readonly string[]): Promise<string> {
137
+ try {
138
+ const rootStat = await fs.lstat(root);
139
+ if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
140
+ throw new SkillFrontmatterError(`skill authority root is not a safe directory: ${root}`);
141
+ }
142
+ } catch (error) {
143
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
144
+ await fs.mkdir(root, { recursive: true });
145
+ const rootStat = await fs.lstat(root);
146
+ if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
147
+ throw new SkillFrontmatterError(`skill authority root is not a safe directory: ${root}`);
148
+ }
149
+ }
150
+ const realRoot = await fs.realpath(root);
151
+ let current = root;
152
+ for (const segment of segments) {
153
+ current = path.join(current, segment);
154
+ try {
155
+ const stat = await fs.lstat(current);
156
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
157
+ throw new SkillFrontmatterError(`skill destination is not a safe directory: ${current}`);
158
+ }
159
+ } catch (error) {
160
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
161
+ try {
162
+ await fs.mkdir(current);
163
+ } catch (mkdirError) {
164
+ if ((mkdirError as NodeJS.ErrnoException).code !== "EEXIST") throw mkdirError;
165
+ const stat = await fs.lstat(current);
166
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
167
+ throw new SkillFrontmatterError(`skill destination is not a safe directory: ${current}`);
168
+ }
169
+ }
170
+ }
171
+ const realCurrent = await fs.realpath(current);
172
+ if (!isPathWithin(realRoot, realCurrent)) {
173
+ throw new SkillFrontmatterError(`skill destination escapes its scope root: ${current}`);
174
+ }
175
+ }
176
+ return current;
100
177
  }
101
178
 
102
- /** Ancestor directories from `cwd` (inclusive) up to `stop` (inclusive), excluding `home`. */
103
- function ancestorDirs(cwd: string, stop: string, home: string): string[] {
104
- const dirs: string[] = [];
105
- let current = path.resolve(cwd);
106
- const resolvedStop = path.resolve(stop);
107
- const resolvedHome = path.resolve(home);
108
- while (true) {
109
- if (current !== resolvedHome) {
110
- dirs.push(current);
179
+ async function writeSkillFileSafely(skillDir: string, scopeRoot: string, content: string): Promise<string> {
180
+ const [canonicalSkillDir, canonicalScopeRoot] = await Promise.all([fs.realpath(skillDir), fs.realpath(scopeRoot)]);
181
+ if (!isPathWithin(canonicalScopeRoot, canonicalSkillDir)) {
182
+ throw new SkillFrontmatterError(`skill destination escapes its scope root: ${skillDir}`);
183
+ }
184
+ const filePath = path.join(skillDir, "SKILL.md");
185
+ let destinationExists = false;
186
+ try {
187
+ const destination = await fs.lstat(filePath);
188
+ if (destination.isSymbolicLink() || !destination.isFile()) {
189
+ throw new SkillFrontmatterError(`skill destination is not a safe file: ${filePath}`);
111
190
  }
112
- if (current === resolvedStop) break;
113
- const parent = path.dirname(current);
114
- if (parent === current) break;
115
- current = parent;
191
+ destinationExists = true;
192
+ } catch (error) {
193
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
194
+ }
195
+ const noFollowFlags =
196
+ process.platform === "win32" ? 0 : (nodeFs.constants.O_NOFOLLOW ?? 0) | (nodeFs.constants.O_NONBLOCK ?? 0);
197
+ const flags = destinationExists
198
+ ? nodeFs.constants.O_WRONLY | noFollowFlags
199
+ : nodeFs.constants.O_WRONLY | nodeFs.constants.O_CREAT | nodeFs.constants.O_EXCL | noFollowFlags;
200
+ let handle: fs.FileHandle;
201
+ try {
202
+ handle = await fs.open(filePath, flags, 0o600);
203
+ } catch (error) {
204
+ throw new SkillFrontmatterError(`skill destination is not a safe file: ${filePath} (${String(error)})`);
116
205
  }
117
- return dirs;
206
+ try {
207
+ const [opened, observed, currentSkillDir] = await Promise.all([
208
+ handle.stat({ bigint: true }),
209
+ fs.lstat(filePath, { bigint: true }),
210
+ fs.realpath(skillDir),
211
+ ]);
212
+ if (
213
+ !opened.isFile() ||
214
+ opened.nlink !== 1n ||
215
+ observed.isSymbolicLink() ||
216
+ !observed.isFile() ||
217
+ opened.dev !== observed.dev ||
218
+ opened.ino !== observed.ino ||
219
+ normalizePathForComparison(currentSkillDir) !== normalizePathForComparison(canonicalSkillDir) ||
220
+ !isPathWithin(canonicalScopeRoot, currentSkillDir)
221
+ ) {
222
+ throw new SkillFrontmatterError(`skill destination changed during write: ${filePath}`);
223
+ }
224
+ await handle.truncate(0);
225
+ await handle.writeFile(content);
226
+ } finally {
227
+ await handle.close();
228
+ }
229
+ return filePath;
230
+ }
231
+
232
+ function getRuntimeHome(): string {
233
+ return getTrustedHomeDir();
118
234
  }
119
235
 
120
236
  /**
@@ -126,34 +242,42 @@ export async function getProjectSkillDirs(
126
242
  home: string,
127
243
  ): Promise<{ dirs: string[]; repoRoot: string | null }> {
128
244
  const repoRoot = await findRepoRoot(cwd);
129
- const walkDirs = ancestorDirs(cwd, path.resolve(repoRoot ?? cwd), home);
130
- return { dirs: walkDirs.map(dir => path.join(dir, ".gjc", "skills")), repoRoot };
245
+ const walkDirs = getAncestorDirs(cwd, path.resolve(repoRoot ?? cwd), home);
246
+ return { dirs: walkDirs.map(({ dir }) => path.join(dir, ".gjc", "skills")), repoRoot };
131
247
  }
132
248
 
133
249
  /** Canonical user skill directories in precedence order (same resolution as runtime discovery). */
134
- export function getUserSkillDirs(home: string): string[] {
135
- return [
136
- ...new Set([
137
- path.join(home, SOURCE_PATHS.native.userAgent, "skills"),
138
- path.join(home, SOURCE_PATHS.native.userBase, "skills"),
139
- path.join(home, ".gjc", "skills"),
140
- ]),
141
- ];
250
+ export function getUserSkillDirs(home: string, agentDir?: string, profileAuthority?: "default" | "custom"): string[] {
251
+ return getUserSkillScanDirs(home, agentDir, profileAuthority);
142
252
  }
143
253
 
144
254
  /**
145
255
  * The canonical directory a write targets for a scope: the repo root (or `cwd`)
146
- * `.gjc/skills` for project scope, the canonical `<config>/agent/skills` user
147
- * root for user scope (honoring `GJC_CONFIG_DIR` / `PI_CONFIG_DIR`).
256
+ * `.gjc/skills` for project scope, the agent directory's `skills` root for user
257
+ * scope — the same directory every reader scans first (`gjc config path` prints
258
+ * it; `--agent-dir` / `GJC_CODING_AGENT_DIR` / `setAgentDir()` move it).
148
259
  */
149
260
  export async function resolveNativeSkillScopeDir(
150
261
  cwd: string,
151
262
  scope: SkillScope,
152
- home = getRuntimeHome(),
263
+ _home?: string,
264
+ agentDir?: string,
153
265
  ): Promise<string> {
154
- if (scope === "user") return path.join(home, SOURCE_PATHS.native.userAgent, "skills");
266
+ const home = _home ?? getRuntimeHome();
267
+ if (scope === "user") {
268
+ const resolvedAgentDir = agentDir ?? (_home === undefined ? getAgentDir() : resolveUserAgentDir(home));
269
+ return path.join(path.resolve(resolvedAgentDir), "skills");
270
+ }
155
271
  const repoRoot = await findRepoRoot(cwd);
156
- return path.join(repoRoot ?? path.resolve(cwd), ".gjc", "skills");
272
+ const resolvedCwd = path.resolve(cwd);
273
+ const projectRoot = repoRoot ?? resolvedCwd;
274
+ if (normalizePathForComparison(projectRoot) === normalizePathForComparison(home)) {
275
+ if (normalizePathForComparison(resolvedCwd) === normalizePathForComparison(home)) {
276
+ throw new SkillFrontmatterError("project skill scope is unavailable at the user home boundary");
277
+ }
278
+ return path.join(resolvedCwd, ".gjc", "skills");
279
+ }
280
+ return path.join(projectRoot, ".gjc", "skills");
157
281
  }
158
282
 
159
283
  function matchesPattern(name: string, patterns: string[] | undefined): boolean {
@@ -174,14 +298,31 @@ function isDisabledByExtension(name: string, disabledExtensions: string[] | unde
174
298
  export async function listNativeSkillsForManagement(options: {
175
299
  cwd: string;
176
300
  home?: string;
301
+ agentDir?: string;
302
+ /** Resolver-owned profile classification; unlike path comparison, this survives HOME refreshes. */
303
+ profileAuthority?: "default" | "custom";
177
304
  policy?: SkillManagementPolicy;
178
305
  }): Promise<ManagedSkillRecord[]> {
306
+ const homeWasInjected = options.home !== undefined;
179
307
  const home = options.home ?? getRuntimeHome();
308
+ const agentDir = options.agentDir ?? (homeWasInjected ? resolveUserAgentDir(home) : getAgentDir());
309
+ const profileAuthority =
310
+ options.profileAuthority ??
311
+ (options.agentDir !== undefined
312
+ ? normalizePathForComparison(options.agentDir) ===
313
+ normalizePathForComparison(homeWasInjected ? resolveUserAgentDir(home) : getAgentDir())
314
+ ? homeWasInjected
315
+ ? "default"
316
+ : getAgentProfileAuthority()
317
+ : "custom"
318
+ : !homeWasInjected
319
+ ? getAgentProfileAuthority()
320
+ : undefined);
180
321
  const policy = options.policy;
181
322
  const projectTrusted = resolveSkillScopeTrust(policy ?? {}, "project");
182
323
  const userTrusted = resolveSkillScopeTrust(policy ?? {}, "user");
183
324
 
184
- const scanJobs: Array<Promise<{ dir: string; items: CapabilitySkill[] }>> = [];
325
+ const scanJobs: Array<Promise<{ dir: string; scope: SkillScope; items: CapabilitySkill[] }>> = [];
185
326
  const projectDirs = await getProjectSkillDirs(options.cwd, home);
186
327
  if (projectTrusted) {
187
328
  for (const dir of projectDirs.dirs) {
@@ -189,17 +330,23 @@ export async function listNativeSkillsForManagement(options: {
189
330
  scanSkillsFromDir(
190
331
  { cwd: options.cwd, home, repoRoot: projectDirs.repoRoot },
191
332
  { dir, providerId: "runtime", level: "project", requireDescription: true },
192
- ).then(result => ({ dir, items: result.items })),
333
+ ).then(result => ({ dir, scope: "project" as const, items: result.items })),
193
334
  );
194
335
  }
195
336
  }
196
337
  if (userTrusted) {
197
- for (const dir of getUserSkillDirs(home)) {
338
+ for (const dir of getUserSkillScanDirs(home, agentDir, profileAuthority)) {
198
339
  scanJobs.push(
199
340
  scanSkillsFromDir(
200
341
  { cwd: options.cwd, home, repoRoot: home },
201
- { dir, providerId: "runtime", level: "user", requireDescription: true },
202
- ).then(result => ({ dir, items: result.items })),
342
+ {
343
+ dir,
344
+ authorityRoot: path.resolve(dir) === path.resolve(agentDir, "skills") ? agentDir : undefined,
345
+ providerId: "runtime",
346
+ level: "user",
347
+ requireDescription: true,
348
+ },
349
+ ).then(result => ({ dir, scope: "user" as const, items: result.items })),
203
350
  );
204
351
  }
205
352
  }
@@ -208,8 +355,7 @@ export async function listNativeSkillsForManagement(options: {
208
355
  const seenNames = new Set<string>();
209
356
  const seenPaths = new Set<string>();
210
357
 
211
- for (const { dir, items } of await Promise.all(scanJobs)) {
212
- const scope: SkillScope = path.resolve(dir).startsWith(`${path.resolve(home)}${path.sep}`) ? "user" : "project";
358
+ for (const { dir, scope, items } of await Promise.all(scanJobs)) {
213
359
  const source = scope === "project" ? "project .gjc/skills" : `user ${dir}`;
214
360
  for (const skill of items) {
215
361
  const realPath = await safeRealpath(skill.path);
@@ -217,7 +363,7 @@ export async function listNativeSkillsForManagement(options: {
217
363
 
218
364
  let disabledReason: SkillDisabledReason | undefined;
219
365
  let enabled = true;
220
- if (BUILT_IN_SKILL_NAMES.has(skill.name)) {
366
+ if (isProtectedSkillName(skill.name)) {
221
367
  enabled = false;
222
368
  disabledReason = "protected";
223
369
  } else if (!(scope === "project" ? projectTrusted : userTrusted)) {
@@ -272,19 +418,21 @@ export async function writeNativeSkill(input: WriteNativeSkillInput): Promise<Wr
272
418
 
273
419
  const effectiveName =
274
420
  typeof frontmatter.name === "string" && frontmatter.name.trim() ? frontmatter.name.trim() : name;
275
- if (BUILT_IN_SKILL_NAMES.has(effectiveName)) throw new SkillNameProtectedError(effectiveName);
276
-
277
- const directory = await resolveNativeSkillScopeDir(input.cwd, input.scope, input.home ?? getRuntimeHome());
278
- const skillDir = path.join(directory, effectiveName);
279
- await fs.mkdir(skillDir, { recursive: true });
280
- const filePath = path.join(skillDir, "SKILL.md");
281
- await Bun.write(filePath, `${input.content.trimEnd()}\n`);
421
+ if (isProtectedSkillName(effectiveName)) throw new SkillNameProtectedError(effectiveName);
422
+ assertSafeSkillName(effectiveName);
423
+
424
+ const directory = await resolveNativeSkillScopeDir(input.cwd, input.scope, input.home, input.agentDir);
425
+ const scopeRoot = input.scope === "user" ? path.dirname(directory) : path.dirname(path.dirname(directory));
426
+ const relativeDirectory = path.relative(scopeRoot, directory);
427
+ const directorySegments = relativeDirectory.split(path.sep).filter(Boolean);
428
+ const skillDir = await ensureSafeDescendantDirectory(scopeRoot, [...directorySegments, effectiveName]);
429
+ const filePath = await writeSkillFileSafely(skillDir, scopeRoot, `${input.content.trimEnd()}\n`);
282
430
  return { name: effectiveName, scope: input.scope, directory, path: filePath };
283
431
  }
284
432
 
285
433
  /** Whether a skill is enabled under the current policy (disabledExtensions/ignored/scope trust). */
286
434
  export function isNativeSkillEnabled(name: string, policy: SkillManagementPolicy | undefined): boolean {
287
- if (BUILT_IN_SKILL_NAMES.has(name)) return true;
435
+ if (isProtectedSkillName(name)) return true;
288
436
  if (isDisabledByExtension(name, policy?.disabledExtensions)) return false;
289
437
  if (matchesPattern(name, policy?.ignoredSkills)) return false;
290
438
  if (policy?.includeSkills?.length && !matchesPattern(name, policy.includeSkills)) return false;
@@ -299,7 +447,7 @@ export function isNativeSkillEnabled(name: string, policy: SkillManagementPolicy
299
447
  export function setNativeSkillEnabled(name: string, enabled: boolean, disabledExtensions: string[]): string[] {
300
448
  const id = `skill:${name}`;
301
449
  const next = new Set(disabledExtensions);
302
- if (BUILT_IN_SKILL_NAMES.has(name)) return [...next];
450
+ if (isProtectedSkillName(name)) return [...next];
303
451
  if (enabled) {
304
452
  next.delete(id);
305
453
  } else {
@@ -1,14 +1,24 @@
1
1
  import * as fs from "node:fs/promises";
2
- import { getProjectDir, getTrustedHomeDir } from "@gajae-code/utils";
2
+ import {
3
+ getAgentDir,
4
+ getAgentProfileAuthority,
5
+ getProjectDir,
6
+ getTrustedHomeDir,
7
+ normalizePathForComparison,
8
+ } from "@gajae-code/utils";
3
9
  import { findRepoRoot } from "../capability/fs";
4
10
  import { skillCapability } from "../capability/skill";
5
11
  import type { SourceMeta } from "../capability/types";
6
12
  import type { SkillsSettings } from "../config/settings";
7
13
  import { resolveSkillScopeTrust } from "../config/skill-settings-defaults";
8
14
  import { type Skill as CapabilitySkill, getCapability } from "../discovery";
9
- import { compareSkillOrder, scanSkillsFromDir } from "../discovery/helpers";
15
+ import { loadMarketplaceSkills } from "../discovery/claude-plugins";
16
+ import { compareSkillOrder, resolveUserAgentDir, scanSkillsFromDir } from "../discovery/helpers";
10
17
  import type { SkillPromptDetails } from "../session/messages";
11
- import { CANONICAL_GJC_WORKFLOW_SKILLS } from "../skill-state/canonical-skills";
18
+ import {
19
+ getSkillFilesystemIdentity,
20
+ isCanonicalGjcWorkflowSkillFilesystemAlias,
21
+ } from "../skill-state/canonical-skills";
12
22
  import { expandTilde } from "../tools/path-utils";
13
23
  import type { LoadedSubskillActivation } from "./gjc-plugins";
14
24
  import { buildSubskillInjection } from "./gjc-plugins/injection";
@@ -113,16 +123,17 @@ export interface LoadSkillsOptions extends SkillsSettings {
113
123
  home?: string;
114
124
  /** Explicit user agent directory for native user-scope skill discovery. */
115
125
  agentDir?: string;
126
+ /** Resolver-owned classification for the selected agent directory. */
127
+ profileAuthority?: "default" | "custom";
116
128
  }
117
129
 
118
130
  /**
119
- * Skill providers loaded into sessions. Only native `.gjc` skills are live
120
- * filesystem skills; Claude/Codex convention skills are explicit import
131
+ * Skill providers loaded into sessions. Native `.gjc` skills and installed
132
+ * marketplace plugin skills are both live filesystem skills, so both are
133
+ * enumerated into sessions; Claude/Codex convention skills are explicit import
121
134
  * sources into `.gjc` (see skill-management.ts) and are never loaded directly.
122
135
  */
123
- const LOADABLE_SKILL_PROVIDERS = new Set(["native"]);
124
-
125
- const BUILT_IN_SKILL_NAMES = new Set<string>(CANONICAL_GJC_WORKFLOW_SKILLS.map(name => name.toLowerCase()));
136
+ const LOADABLE_SKILL_PROVIDERS = new Set(["native", "claude-plugins"]);
126
137
 
127
138
  /**
128
139
  * Load skills from all configured locations.
@@ -142,14 +153,25 @@ export async function loadSkills(options: LoadSkillsOptions = {}): Promise<LoadS
142
153
  if (!enabled) {
143
154
  return { skills: [], warnings: [] };
144
155
  }
156
+ const homeWasInjected = options.home !== undefined;
145
157
  const home = options.home ?? getTrustedHomeDir();
158
+ const agentDir = options.agentDir ?? (homeWasInjected ? resolveUserAgentDir(home) : getAgentDir());
159
+ const profileAuthority =
160
+ options.profileAuthority ??
161
+ (!homeWasInjected
162
+ ? options.agentDir !== undefined &&
163
+ normalizePathForComparison(options.agentDir) !== normalizePathForComparison(getAgentDir())
164
+ ? "custom"
165
+ : getAgentProfileAuthority()
166
+ : undefined);
146
167
 
147
168
  const projectTrusted = resolveSkillScopeTrust(options, "project");
148
169
  const userTrusted = resolveSkillScopeTrust(options, "user");
149
170
 
150
171
  // Skill scope trust decides which canonical locations are loaded: project
151
172
  // scope covers `.gjc/skills` (walk-up), user scope covers `~/.gjc/agent/skills`
152
- // and the legacy user roots. Claude/Codex convention skills are import
173
+ // and the legacy user roots, and installed marketplace plugins are gated by the
174
+ // scope they were installed into. Claude/Codex convention skills are import
153
175
  // sources into `.gjc`, never loaded directly.
154
176
  function isSourceEnabled(source: SourceMeta): boolean {
155
177
  const { provider, level } = source;
@@ -162,16 +184,34 @@ export async function loadSkills(options: LoadSkillsOptions = {}): Promise<LoadS
162
184
  // Use capability API to load all skills. `all` (rather than `items`) keeps
163
185
  // shadowed duplicates so this function can apply the documented precedence
164
186
  // itself: project scope beats user scope.
165
- const nativeProvider = getCapability<CapabilitySkill>(skillCapability.id)?.providers.find(
166
- provider => provider.id === "native",
167
- );
168
- if (!nativeProvider) throw new Error("Native skill provider is unavailable");
169
- const result = await nativeProvider.load({
187
+ const providers =
188
+ getCapability<CapabilitySkill>(skillCapability.id)?.providers.filter(provider =>
189
+ LOADABLE_SKILL_PROVIDERS.has(provider.id),
190
+ ) ?? [];
191
+ if (!providers.some(provider => provider.id === "native")) {
192
+ throw new Error("Native skill provider is unavailable");
193
+ }
194
+ const loadContext = {
170
195
  cwd,
171
196
  home,
172
- userAgentDir: options.agentDir,
197
+ userAgentDir: agentDir,
198
+ profileAuthority,
173
199
  repoRoot: await findRepoRoot(cwd),
174
- });
200
+ };
201
+ const providerResults = await Promise.all(
202
+ providers.map(async provider => {
203
+ const allowedLevels = new Set<"user" | "project">();
204
+ if (projectTrusted) allowedLevels.add("project");
205
+ if (userTrusted) allowedLevels.add("user");
206
+ return {
207
+ provider,
208
+ result:
209
+ provider.id === "claude-plugins"
210
+ ? await loadMarketplaceSkills(loadContext, allowedLevels)
211
+ : await provider.load(loadContext),
212
+ };
213
+ }),
214
+ );
175
215
 
176
216
  const skillMap = new Map<string, Skill>();
177
217
  const realPathSet = new Set<string>();
@@ -197,8 +237,11 @@ export async function loadSkills(options: LoadSkillsOptions = {}): Promise<LoadS
197
237
  // precedence. `all` is already in native provider order with project dirs
198
238
  // before user dirs; a stable sort by level lifts every project item above
199
239
  // every user item, giving: project `.gjc/skills` > user roots.
200
- const filteredSkills = result.items
201
- .filter(capSkill => {
240
+ const filteredSkills = providerResults
241
+ .flatMap(({ provider, result }) =>
242
+ result.items.map(capSkill => ({ capSkill, providerPriority: provider.priority })),
243
+ )
244
+ .filter(({ capSkill }) => {
202
245
  if (!isSourceEnabled(capSkill._source)) return false;
203
246
  if (disabledSkillNames.has(capSkill.name)) return false;
204
247
  if (matchesIgnorePatterns(capSkill.name)) return false;
@@ -207,12 +250,12 @@ export async function loadSkills(options: LoadSkillsOptions = {}): Promise<LoadS
207
250
  })
208
251
  .sort((a, b) => {
209
252
  const levelOrder = { project: 0, user: 1 } as const;
210
- return levelOrder[a.level] - levelOrder[b.level];
253
+ return levelOrder[a.capSkill.level] - levelOrder[b.capSkill.level] || b.providerPriority - a.providerPriority;
211
254
  });
212
255
 
213
256
  // Batch resolve all real paths in parallel
214
257
  const realPaths = await Promise.all(
215
- filteredSkills.map(async capSkill => {
258
+ filteredSkills.map(async ({ capSkill }) => {
216
259
  try {
217
260
  return await fs.realpath(capSkill.path);
218
261
  } catch {
@@ -223,7 +266,7 @@ export async function loadSkills(options: LoadSkillsOptions = {}): Promise<LoadS
223
266
 
224
267
  // Process skills with resolved paths
225
268
  for (let i = 0; i < filteredSkills.length; i++) {
226
- const capSkill = filteredSkills[i];
269
+ const { capSkill } = filteredSkills[i];
227
270
  const resolvedPath = realPaths[i];
228
271
 
229
272
  // Skip silently if we've already loaded this exact file (via symlink)
@@ -235,14 +278,14 @@ export async function loadSkills(options: LoadSkillsOptions = {}): Promise<LoadS
235
278
  // workflow-routing hijack: warn, and let the session merge keep the bundled
236
279
  // definition authoritative (sdk/session.ts). User-scope copies are not
237
280
  // warned about: installed defaults legitimately mirror bundled skills.
238
- if (capSkill.level === "project" && BUILT_IN_SKILL_NAMES.has(capSkill.name.toLowerCase())) {
281
+ if (capSkill.level === "project" && isCanonicalGjcWorkflowSkillFilesystemAlias(capSkill.name)) {
239
282
  collisionWarnings.push({
240
283
  skillPath: capSkill.path,
241
284
  message: `name collision: "${capSkill.name}" is a bundled GJC workflow skill; the bundled definition takes precedence in sessions and this project copy is never used`,
242
285
  });
243
286
  }
244
287
 
245
- const skillKey = capSkill.name.toLowerCase();
288
+ const skillKey = getSkillFilesystemIdentity(capSkill.name);
246
289
  const existing = skillMap.get(skillKey);
247
290
  if (existing) {
248
291
  collisionWarnings.push({
@@ -319,7 +362,7 @@ export async function loadSkills(options: LoadSkillsOptions = {}): Promise<LoadS
319
362
  const resolvedPath = customRealPaths[i];
320
363
  if (realPathSet.has(resolvedPath)) continue;
321
364
 
322
- const skillKey = skill.name.toLowerCase();
365
+ const skillKey = getSkillFilesystemIdentity(skill.name);
323
366
  const existing = skillMap.get(skillKey);
324
367
  if (existing) {
325
368
  collisionWarnings.push({
@@ -338,7 +381,12 @@ export async function loadSkills(options: LoadSkillsOptions = {}): Promise<LoadS
338
381
 
339
382
  return {
340
383
  skills,
341
- warnings: [...(result.warnings ?? []).map(w => ({ skillPath: "", message: w })), ...collisionWarnings],
384
+ warnings: [
385
+ ...providerResults.flatMap(({ result }) =>
386
+ (result.warnings ?? []).map(message => ({ skillPath: "", message })),
387
+ ),
388
+ ...collisionWarnings,
389
+ ],
342
390
  };
343
391
  }
344
392