@gajae-code/coding-agent 0.16.1 → 0.16.4

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 (146) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/dist/types/cli/args.d.ts +2 -2
  3. package/dist/types/cli/root-flags.d.ts +4 -2
  4. package/dist/types/cli.d.ts +1 -1
  5. package/dist/types/commands/launch.d.ts +1 -1
  6. package/dist/types/config/autorouting-tier-map.d.ts +3 -0
  7. package/dist/types/config/model-registry.d.ts +4 -0
  8. package/dist/types/config/model-resolver.d.ts +9 -0
  9. package/dist/types/config/settings-schema.d.ts +29 -1
  10. package/dist/types/discovery/claude-plugins.d.ts +3 -1
  11. package/dist/types/discovery/helpers.d.ts +2 -0
  12. package/dist/types/extensibility/gjc-plugins/mcp-policy.d.ts +16 -0
  13. package/dist/types/extensibility/gjc-plugins/paths.d.ts +2 -0
  14. package/dist/types/extensibility/runtime-skill-discovery.d.ts +1 -1
  15. package/dist/types/extensibility/shared-events.d.ts +1 -1
  16. package/dist/types/gjc-runtime/session-state-lock.d.ts +2 -0
  17. package/dist/types/modes/components/assistant-message.d.ts +1 -2
  18. package/dist/types/modes/components/settings-selector.d.ts +6 -1
  19. package/dist/types/modes/components/status-line/command.d.ts +35 -0
  20. package/dist/types/modes/components/status-line/index.d.ts +1 -0
  21. package/dist/types/modes/components/status-line/types.d.ts +5 -0
  22. package/dist/types/modes/components/tool-status-header.d.ts +14 -0
  23. package/dist/types/modes/controllers/event-controller.d.ts +6 -0
  24. package/dist/types/modes/interactive-mode.d.ts +1 -0
  25. package/dist/types/modes/types.d.ts +1 -0
  26. package/dist/types/runtime-mcp/client.d.ts +9 -1
  27. package/dist/types/runtime-mcp/manager.d.ts +7 -1
  28. package/dist/types/runtime-mcp/pool.d.ts +22 -3
  29. package/dist/types/runtime-mcp/transports/stdio.d.ts +0 -4
  30. package/dist/types/runtime-mcp/types.d.ts +22 -0
  31. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +12 -0
  32. package/dist/types/sdk/bus/slack-live-provider.d.ts +6 -0
  33. package/dist/types/sdk/bus/slack-provider.d.ts +12 -0
  34. package/dist/types/sdk/cli/rows.d.ts +7 -0
  35. package/dist/types/sdk/host/events.d.ts +1 -0
  36. package/dist/types/sdk/host/host.d.ts +2 -0
  37. package/dist/types/sdk/host/session-runtime.d.ts +23 -0
  38. package/dist/types/sdk/session-api-key.d.ts +26 -0
  39. package/dist/types/sdk/transport/serve-cli.d.ts +1 -1
  40. package/dist/types/session/agent-session.d.ts +9 -0
  41. package/dist/types/setup/paseo/announce.d.ts +111 -0
  42. package/dist/types/setup/paseo/setup-deps.d.ts +2 -1
  43. package/dist/types/system-prompt.d.ts +2 -0
  44. package/dist/types/task/executor.d.ts +2 -1
  45. package/dist/types/tools/fetch.d.ts +7 -0
  46. package/dist/types/tools/index.d.ts +8 -2
  47. package/dist/types/tools/path-utils.d.ts +2 -0
  48. package/dist/types/tools/search.d.ts +6 -0
  49. package/dist/types/utils/external-editor.d.ts +5 -0
  50. package/package.json +7 -7
  51. package/scripts/capture-status-line-custom-editor-showcase.ts +151 -0
  52. package/scripts/generate-sdk-operation-inventory.ts +9 -0
  53. package/scripts/verify-status-line-custom-editor-showcase.ts +309 -0
  54. package/src/cli/args.ts +20 -9
  55. package/src/cli/root-flags.ts +5 -3
  56. package/src/config/autorouting-tier-map.ts +1 -0
  57. package/src/config/file-lock.ts +181 -17
  58. package/src/config/model-preset-registry.ts +5 -3
  59. package/src/config/model-profiles.ts +40 -0
  60. package/src/config/model-registry.ts +57 -11
  61. package/src/config/model-resolver.ts +48 -0
  62. package/src/config/settings-schema.ts +33 -1
  63. package/src/config/settings.ts +1 -0
  64. package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +11 -11
  65. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +13 -2
  66. package/src/discovery/builtin.ts +13 -4
  67. package/src/discovery/claude-plugins.ts +12 -4
  68. package/src/discovery/helpers.ts +10 -2
  69. package/src/extensibility/gjc-plugins/compiler.ts +208 -58
  70. package/src/extensibility/gjc-plugins/installer.ts +13 -12
  71. package/src/extensibility/gjc-plugins/mcp-policy.ts +81 -52
  72. package/src/extensibility/gjc-plugins/paths.ts +13 -0
  73. package/src/extensibility/gjc-plugins/runtime-adapters.ts +758 -16
  74. package/src/extensibility/runtime-skill-discovery.ts +77 -151
  75. package/src/extensibility/shared-events.ts +1 -1
  76. package/src/extensibility/skills.ts +44 -16
  77. package/src/gjc-runtime/repository-binding.ts +1 -1
  78. package/src/gjc-runtime/session-state-lock.ts +94 -6
  79. package/src/internal-urls/docs-index.generated.ts +10 -10
  80. package/src/main.ts +72 -34
  81. package/src/modes/DESIGN.md +54 -0
  82. package/src/modes/acp/acp-agent.ts +29 -14
  83. package/src/modes/components/assistant-message.ts +22 -9
  84. package/src/modes/components/model-selector.ts +166 -33
  85. package/src/modes/components/settings-selector.ts +1075 -235
  86. package/src/modes/components/status-line/command.ts +190 -0
  87. package/src/modes/components/status-line/index.ts +1 -0
  88. package/src/modes/components/status-line/segments.ts +32 -5
  89. package/src/modes/components/status-line/types.ts +5 -0
  90. package/src/modes/components/tool-status-header.ts +393 -21
  91. package/src/modes/controllers/command-controller.ts +2 -0
  92. package/src/modes/controllers/event-controller.ts +148 -5
  93. package/src/modes/controllers/selector-controller.ts +21 -0
  94. package/src/modes/interactive-mode.ts +47 -1
  95. package/src/modes/types.ts +1 -0
  96. package/src/modes/utils/ui-helpers.ts +1 -0
  97. package/src/prompts/system/system-prompt.md +8 -0
  98. package/src/prompts/tools/bash.md +1 -1
  99. package/src/prompts/tools/job.md +1 -1
  100. package/src/prompts/tools/search.md +2 -0
  101. package/src/runtime-mcp/client.ts +60 -17
  102. package/src/runtime-mcp/manager.ts +171 -14
  103. package/src/runtime-mcp/pool.test.ts +209 -0
  104. package/src/runtime-mcp/pool.ts +224 -23
  105. package/src/runtime-mcp/transports/stdio.ts +158 -24
  106. package/src/runtime-mcp/types.ts +24 -0
  107. package/src/sdk/acp/adapter.ts +1 -1
  108. package/src/sdk/broker/broker.ts +5 -4
  109. package/src/sdk/bus/chat-daemon-control.ts +1 -1
  110. package/src/sdk/bus/chat-daemon-runtime.ts +44 -1
  111. package/src/sdk/bus/slack-daemon.ts +92 -2
  112. package/src/sdk/bus/slack-live-provider.ts +12 -0
  113. package/src/sdk/bus/slack-provider.ts +5 -0
  114. package/src/sdk/cli/rows.ts +47 -2
  115. package/src/sdk/cli/session-cli.ts +47 -23
  116. package/src/sdk/host/events.ts +25 -2
  117. package/src/sdk/host/host.ts +4 -1
  118. package/src/sdk/host/session-runtime.test.ts +36 -25
  119. package/src/sdk/host/session-runtime.ts +279 -43
  120. package/src/sdk/protocol/operation-inventory.generated.json +66 -0
  121. package/src/sdk/router/session-router.ts +60 -37
  122. package/src/sdk/session-api-key.ts +94 -0
  123. package/src/sdk/session.ts +73 -30
  124. package/src/sdk/transport/serve-cli.ts +3 -3
  125. package/src/session/agent-session.ts +123 -38
  126. package/src/session/contribution-prep.ts +214 -21
  127. package/src/setup/paseo/announce.ts +450 -0
  128. package/src/setup/paseo/setup-deps.ts +15 -3
  129. package/src/slash-commands/builtin-registry.ts +1 -0
  130. package/src/system-prompt.ts +4 -0
  131. package/src/task/executor.ts +2 -1
  132. package/src/task/index.ts +17 -4
  133. package/src/tools/bash.ts +50 -21
  134. package/src/tools/cron.ts +1 -1
  135. package/src/tools/fetch.ts +29 -1
  136. package/src/tools/index.ts +8 -2
  137. package/src/tools/job.ts +8 -2
  138. package/src/tools/monitor.ts +8 -2
  139. package/src/tools/path-utils.ts +7 -0
  140. package/src/tools/search.ts +116 -58
  141. package/src/tools/skill.ts +22 -2
  142. package/src/tools/subagent.ts +3 -1
  143. package/src/tools/tool-catalog.generated.ts +9 -2
  144. package/src/utils/external-editor.ts +16 -2
  145. package/src/web/insane/bridge.ts +113 -23
  146. package/src/web/search/providers/perplexity.ts +102 -41
@@ -1,16 +1,17 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
- import { getPluginsDir, getTrustedHomeDir } from "@gajae-code/utils";
4
- import { findRepoRoot, invalidate as invalidateFsCache } from "../capability/fs";
3
+ import { getTrustedHomeDir } from "@gajae-code/utils";
4
+ import { findRepoRoot } from "../capability/fs";
5
5
  import type { Skill as CapabilitySkill } from "../capability/skill";
6
6
  import type { SkillsSettings } from "../config/settings-schema";
7
7
  import { resolveSkillScopeTrust } from "../config/skill-settings-defaults";
8
8
  import { scanClaudeProjectSkills, scanClaudeUserSkills } from "../discovery/claude";
9
+ import { loadMarketplaceSkills } from "../discovery/claude-plugins";
9
10
  import { scanCodexProjectSkills, scanCodexUserSkills } from "../discovery/codex";
10
- import { compareSkillOrder, listClaudePluginRoots, SOURCE_PATHS, scanSkillsFromDir } from "../discovery/helpers";
11
+ import { compareSkillOrder, SOURCE_PATHS, scanSkillsFromDir } from "../discovery/helpers";
11
12
  import { CANONICAL_GJC_WORKFLOW_SKILLS } from "../skill-state/canonical-skills";
12
13
  import { expandTilde } from "../tools/path-utils";
13
- import type { Skill } from "./skills";
14
+ import { loadSkills, type Skill } from "./skills";
14
15
 
15
16
  export type RuntimeSkillDiscoverySource = "project" | "user";
16
17
 
@@ -152,18 +153,6 @@ function getUseWhen(skill: CapabilitySkill): string[] | undefined {
152
153
  return values.length > 0 ? values : undefined;
153
154
  }
154
155
 
155
- function toRuntimeSkill(skill: CapabilitySkill, source: RuntimeSkillDiscoverySource): Skill {
156
- return {
157
- name: skill.name,
158
- description: typeof skill.frontmatter?.description === "string" ? skill.frontmatter.description : "",
159
- filePath: skill.path,
160
- baseDir: skill.path.replace(/[\\/]SKILL\.md$/, ""),
161
- source: `runtime:${source}`,
162
- hide: skill.frontmatter?.hide === true,
163
- _source: { ...skill._source, providerName: "Runtime skill discovery" },
164
- };
165
- }
166
-
167
156
  function sourceEnabled(source: RuntimeSkillDiscoverySource, policy: SkillsSettings | undefined): boolean {
168
157
  if (policy?.enabled !== true) return false;
169
158
  return resolveSkillScopeTrust(policy, source);
@@ -202,7 +191,7 @@ async function realPathOrSelf(filePath: string): Promise<string> {
202
191
  }
203
192
 
204
193
  interface ScanJobResult {
205
- items: Array<{ skill: CapabilitySkill; source: RuntimeSkillDiscoverySource }>;
194
+ items: Array<{ skill: CapabilitySkill; source: RuntimeSkillDiscoverySource; providerPriority: number }>;
206
195
  warnings: string[];
207
196
  label: string;
208
197
  }
@@ -234,23 +223,16 @@ function pushDiagnostic(diagnostics: string[], message: string): void {
234
223
  if (diagnostics.length < MAX_DIAGNOSTICS) diagnostics.push(message);
235
224
  }
236
225
 
237
- async function collectPluginSkillDirs(
226
+ async function collectPluginSkills(
238
227
  home: string,
239
228
  cwd: string,
240
- ): Promise<Array<{ dir: string; pluginName: string; level: "user" | "project" }>> {
241
- // Ensure the underlying installed_plugins.json fs cache is current — install/upgrade
242
- // writers already invalidate via clearPluginRootsAndCaches; this is a defensive
243
- // fallback so a stale in-memory cache cannot mask a disabled/removed plugin.
244
- invalidateFsCache(`${getPluginsDir(home)}/installed_plugins.json`);
229
+ allowedLevels: ReadonlySet<"user" | "project">,
230
+ ): Promise<{ items: CapabilitySkill[]; warnings: string[] }> {
245
231
  try {
246
- const { roots } = await listClaudePluginRoots(home, cwd);
247
- return roots.map(root => ({
248
- dir: `${root.path}/skills`,
249
- pluginName: root.plugin,
250
- level: root.scope,
251
- }));
252
- } catch {
253
- return [];
232
+ const result = await loadMarketplaceSkills({ cwd, home, repoRoot: await findRepoRoot(cwd) }, allowedLevels);
233
+ return { items: result.items, warnings: result.warnings ?? [] };
234
+ } catch (error) {
235
+ return { items: [], warnings: [`marketplace skill scan failed: ${String(error)}`] };
254
236
  }
255
237
  }
256
238
 
@@ -287,6 +269,7 @@ async function scanProjectOrUserDir(
287
269
  level: "project" | "user",
288
270
  label: string,
289
271
  source: RuntimeSkillDiscoverySource,
272
+ providerPriority: number,
290
273
  ): Promise<ScanJobResult> {
291
274
  const result = await scanSkillsFromDir(ctx, {
292
275
  dir,
@@ -295,7 +278,7 @@ async function scanProjectOrUserDir(
295
278
  requireDescription: true,
296
279
  });
297
280
  return {
298
- items: result.items.map(skill => ({ skill, source })),
281
+ items: result.items.map(skill => ({ skill, source, providerPriority })),
299
282
  warnings: result.warnings ?? [],
300
283
  label,
301
284
  };
@@ -355,8 +338,9 @@ function reportConventionImportCandidates(
355
338
  ): void {
356
339
  for (const scan of scans.sort((a, b) => a.host.localeCompare(b.host) || a.dir.localeCompare(b.dir))) {
357
340
  for (const skill of scan.skills) {
358
- if (seenNames.has(skill.name)) continue;
359
- seenNames.add(skill.name);
341
+ const normalizedName = skill.name.toLowerCase();
342
+ if (seenNames.has(normalizedName)) continue;
343
+ seenNames.add(normalizedName);
360
344
  pushDiagnostic(
361
345
  diagnostics,
362
346
  `skill "${skill.name}" found at ${skill.path} (${scan.host} convention): import sources are not loaded directly; copy it into a trusted .gjc/skills directory to enable it, e.g. mkdir -p .gjc/skills/${skill.name} && cp ${skill.path} .gjc/skills/${skill.name}/SKILL.md`,
@@ -405,55 +389,46 @@ export async function discoverRuntimeSkills(
405
389
  const projectContext = { cwd: options.cwd, home, repoRoot: projectDirs.repoRoot };
406
390
  if ((source === "all" || source === "project") && sourceEnabled("project", policy)) {
407
391
  for (const { dir, label } of projectDirs.scans) {
408
- scanJobs.push(scanProjectOrUserDir(projectContext, dir, "project", label, "project"));
392
+ scanJobs.push(scanProjectOrUserDir(projectContext, dir, "project", label, "project", 100));
409
393
  }
410
394
  }
411
395
  if ((source === "all" || source === "user") && sourceEnabled("user", policy)) {
412
396
  for (const dir of getUserSkillDirs(home)) {
413
397
  scanJobs.push(
414
- scanProjectOrUserDir({ cwd: options.cwd, home, repoRoot: home }, dir, "user", `user ${dir}`, "user"),
398
+ scanProjectOrUserDir({ cwd: options.cwd, home, repoRoot: home }, dir, "user", `user ${dir}`, "user", 100),
415
399
  );
416
400
  }
417
401
  }
418
402
  if ((source === "all" || source === "user") && policy?.enabled === true) {
419
403
  for (const dir of getCustomSkillDirs(policy, home)) {
420
404
  scanJobs.push(
421
- scanProjectOrUserDir({ cwd: options.cwd, home, repoRoot: home }, dir, "user", `custom ${dir}`, "user"),
405
+ scanProjectOrUserDir({ cwd: options.cwd, home, repoRoot: home }, dir, "user", `custom ${dir}`, "user", 0),
422
406
  );
423
407
  }
424
408
  }
425
409
 
426
- // Marketplace plugin skills: version-stable, enabled-only.
427
- // Uses the same trust gate as native user/project scopes and the same
428
- // discovery root (<installPath>/skills) the capability provider uses,
429
- // namespaced as `plugin:skillName`.
410
+ // Marketplace plugin skills come from the same provider used to populate
411
+ // session.skills, so discovery, slash commands, SDK state, and exact-name
412
+ // invocation cannot drift on manifest paths, namespacing, or enabled roots.
430
413
  if (policy?.enabled === true) {
431
- const pluginDirs = await collectPluginSkillDirs(home, options.cwd);
432
- for (const entry of pluginDirs) {
433
- if (entry.level === "project" && !(source === "all" || source === "project")) continue;
434
- if (entry.level === "user" && !(source === "all" || source === "user")) continue;
435
- if (!sourceEnabled(entry.level, policy)) continue;
436
- const label = `plugin ${entry.pluginName} skills`;
437
- scanJobs.push(
438
- (async () => {
439
- const pluginCtx = { cwd: options.cwd, home, repoRoot: home };
440
- const result = await scanSkillsFromDir(pluginCtx, {
441
- dir: entry.dir,
442
- providerId: "plugin",
443
- level: entry.level,
444
- requireDescription: true,
445
- });
446
- for (const skill of result.items) {
447
- skill.name = `${entry.pluginName}:${skill.name}`;
448
- }
449
- return {
450
- items: result.items.map(skill => ({ skill, source: entry.level as RuntimeSkillDiscoverySource })),
451
- warnings: result.warnings ?? [],
452
- label,
453
- };
454
- })(),
455
- );
414
+ const allowedLevels = new Set<"user" | "project">();
415
+ if ((source === "all" || source === "project") && sourceEnabled("project", policy)) {
416
+ allowedLevels.add("project");
417
+ }
418
+ if ((source === "all" || source === "user") && sourceEnabled("user", policy)) {
419
+ allowedLevels.add("user");
456
420
  }
421
+ scanJobs.push(
422
+ collectPluginSkills(home, options.cwd, allowedLevels).then(result => ({
423
+ items: result.items.map(skill => ({
424
+ skill,
425
+ source: skill.level as RuntimeSkillDiscoverySource,
426
+ providerPriority: 70,
427
+ })),
428
+ warnings: result.warnings,
429
+ label: "marketplace plugin skills",
430
+ })),
431
+ );
457
432
  }
458
433
  if (policy?.enabled === true && policy.customDirectories && policy.customDirectories.length > 0) {
459
434
  for (const dir of getCustomSkillDirs(policy, home)) {
@@ -466,6 +441,7 @@ export async function discoverRuntimeSkills(
466
441
  const seenNames = new Set<string>();
467
442
  const seenPaths = new Set<string>();
468
443
  const candidates: RuntimeSkillDiscoveryCandidate[] = [];
444
+ const orderedItems: ScanJobResult["items"] = [];
469
445
  for (const entry of settled) {
470
446
  if ("error" in entry) {
471
447
  pushDiagnostic(diagnostics, `skill scan failed: ${entry.error}`);
@@ -476,29 +452,34 @@ export async function discoverRuntimeSkills(
476
452
  pushDiagnostic(diagnostics, `${entry.label}: ${warning}`);
477
453
  }
478
454
  }
479
- for (const item of entry.items) {
480
- if (!isAllowedByPolicy(item.skill, policy, diagnostics)) continue;
481
- const realPath = await realPathOrSelf(item.skill.path);
482
- if (seenPaths.has(realPath) || seenNames.has(item.skill.name)) {
483
- pushDiagnostic(
484
- diagnostics,
485
- `skill "${item.skill.name}" already resolved from a higher-precedence location; ignoring ${item.skill.path}`,
486
- );
487
- continue;
488
- }
489
- seenPaths.add(realPath);
490
- seenNames.add(item.skill.name);
491
-
492
- const candidate: RuntimeSkillDiscoveryCandidate = {
493
- name: item.skill.name,
494
- description:
495
- typeof item.skill.frontmatter?.description === "string" ? item.skill.frontmatter.description : "",
496
- source: item.source,
497
- path: item.skill.path,
498
- useWhen: getUseWhen(item.skill),
499
- };
500
- if (matchesQuery(candidate, options.query ?? "")) candidates.push(candidate);
455
+ orderedItems.push(...entry.items);
456
+ }
457
+ orderedItems.sort((a, b) => {
458
+ const levelOrder = { project: 0, user: 1 } as const;
459
+ return levelOrder[a.source] - levelOrder[b.source] || b.providerPriority - a.providerPriority;
460
+ });
461
+ for (const item of orderedItems) {
462
+ if (!isAllowedByPolicy(item.skill, policy, diagnostics)) continue;
463
+ const realPath = await realPathOrSelf(item.skill.path);
464
+ const normalizedName = item.skill.name.toLowerCase();
465
+ if (seenPaths.has(realPath) || seenNames.has(normalizedName)) {
466
+ pushDiagnostic(
467
+ diagnostics,
468
+ `skill "${item.skill.name}" already resolved from a higher-precedence location; ignoring ${item.skill.path}`,
469
+ );
470
+ continue;
501
471
  }
472
+ seenPaths.add(realPath);
473
+ seenNames.add(normalizedName);
474
+
475
+ const candidate: RuntimeSkillDiscoveryCandidate = {
476
+ name: item.skill.name,
477
+ description: typeof item.skill.frontmatter?.description === "string" ? item.skill.frontmatter.description : "",
478
+ source: item.source,
479
+ path: item.skill.path,
480
+ useWhen: getUseWhen(item.skill),
481
+ };
482
+ if (matchesQuery(candidate, options.query ?? "")) candidates.push(candidate);
502
483
  }
503
484
  candidates.sort((a, b) => compareSkillOrder(a.name, a.path, b.name, b.path));
504
485
  reportConventionImportCandidates(
@@ -520,67 +501,12 @@ export async function findRuntimeSkillByName(
520
501
  ): Promise<Skill | undefined> {
521
502
  const normalized = name.trim();
522
503
  if (!normalized) return undefined;
523
- const scanJobs: Array<Promise<{ skill: CapabilitySkill; source: RuntimeSkillDiscoverySource }[]>> = [];
524
- const projectDirs = await getProjectSkillDirs(cwd, home);
525
- const projectContext = { cwd, home, repoRoot: projectDirs.repoRoot };
526
- if (sourceEnabled("project", policy)) {
527
- scanJobs.push(
528
- ...projectDirs.scans.map(scan =>
529
- scanSkillsFromDir(projectContext, {
530
- dir: scan.dir,
531
- providerId: "runtime",
532
- level: "project",
533
- requireDescription: true,
534
- }).then(result => result.items.map(skill => ({ skill, source: "project" as const }))),
535
- ),
536
- );
537
- }
538
- if (sourceEnabled("user", policy)) {
539
- for (const dir of getUserSkillDirs(home)) {
540
- scanJobs.push(
541
- scanSkillsFromDir(
542
- { cwd, home, repoRoot: home },
543
- { dir, providerId: "runtime", level: "user", requireDescription: true },
544
- ).then(result => result.items.map(skill => ({ skill, source: "user" as const }))),
545
- );
546
- }
547
- }
548
- if (policy?.enabled === true) {
549
- for (const dir of getCustomSkillDirs(policy, home)) {
550
- scanJobs.push(
551
- scanSkillsFromDir(
552
- { cwd, home, repoRoot: home },
553
- { dir, providerId: "runtime", level: "user", requireDescription: true },
554
- ).then(result => result.items.map(skill => ({ skill, source: "user" as const }))),
555
- );
556
- }
557
- }
558
- if (policy?.enabled === true) {
559
- try {
560
- const pluginDirs = await collectPluginSkillDirs(home, cwd);
561
- for (const entry of pluginDirs) {
562
- if (!sourceEnabled(entry.level, policy)) continue;
563
- scanJobs.push(
564
- scanSkillsFromDir(
565
- { cwd, home, repoRoot: home },
566
- {
567
- dir: entry.dir,
568
- providerId: "plugin",
569
- level: entry.level,
570
- requireDescription: true,
571
- },
572
- ).then(result => {
573
- for (const skill of result.items) skill.name = `${entry.pluginName}:${skill.name}`;
574
- return result.items.map(skill => ({ skill, source: entry.level as RuntimeSkillDiscoverySource }));
575
- }),
576
- );
577
- }
578
- } catch {}
579
- }
580
- for (const entry of (await Promise.all(scanJobs)).flat()) {
581
- if (entry.skill.name === normalized && isAllowedByPolicy(entry.skill, policy, [])) {
582
- return toRuntimeSkill(entry.skill, entry.source);
583
- }
584
- }
585
- return undefined;
504
+ if (BUILT_IN_SKILL_NAMES.has(normalized)) return undefined;
505
+ const loaded = await loadSkills({
506
+ ...policy,
507
+ cwd,
508
+ home,
509
+ agentDir: path.join(home, SOURCE_PATHS.native.userAgent),
510
+ });
511
+ return loaded.skills.find(skill => skill.name === normalized);
586
512
  }
@@ -188,7 +188,7 @@ export interface AgentEndEvent {
188
188
  sdkRunToken?: string;
189
189
  /** Indicates whether the loop ended normally, suspended, cancelled, or entered maintenance. */
190
190
  stopReason?: "completed" | "paused" | "cancelled" | "maintenance";
191
- /** Present for maintenance checkpoints; non-aborted checkpoints are not terminal. */
191
+ /** Present for maintenance checkpoints; only committed maintenance outcomes are non-terminal. */
192
192
  maintenanceOutcome?: MidRunMaintenanceOutcome;
193
193
  }
194
194
 
@@ -6,6 +6,7 @@ import type { SourceMeta } from "../capability/types";
6
6
  import type { SkillsSettings } from "../config/settings";
7
7
  import { resolveSkillScopeTrust } from "../config/skill-settings-defaults";
8
8
  import { type Skill as CapabilitySkill, getCapability } from "../discovery";
9
+ import { loadMarketplaceSkills } from "../discovery/claude-plugins";
9
10
  import { compareSkillOrder, scanSkillsFromDir } from "../discovery/helpers";
10
11
  import type { SkillPromptDetails } from "../session/messages";
11
12
  import { CANONICAL_GJC_WORKFLOW_SKILLS } from "../skill-state/canonical-skills";
@@ -116,11 +117,12 @@ export interface LoadSkillsOptions extends SkillsSettings {
116
117
  }
117
118
 
118
119
  /**
119
- * Skill providers loaded into sessions. Only native `.gjc` skills are live
120
- * filesystem skills; Claude/Codex convention skills are explicit import
120
+ * Skill providers loaded into sessions. Native `.gjc` skills and installed
121
+ * marketplace plugin skills are both live filesystem skills, so both are
122
+ * enumerated into sessions; Claude/Codex convention skills are explicit import
121
123
  * sources into `.gjc` (see skill-management.ts) and are never loaded directly.
122
124
  */
123
- const LOADABLE_SKILL_PROVIDERS = new Set(["native"]);
125
+ const LOADABLE_SKILL_PROVIDERS = new Set(["native", "claude-plugins"]);
124
126
 
125
127
  const BUILT_IN_SKILL_NAMES = new Set<string>(CANONICAL_GJC_WORKFLOW_SKILLS.map(name => name.toLowerCase()));
126
128
 
@@ -149,7 +151,8 @@ export async function loadSkills(options: LoadSkillsOptions = {}): Promise<LoadS
149
151
 
150
152
  // Skill scope trust decides which canonical locations are loaded: project
151
153
  // scope covers `.gjc/skills` (walk-up), user scope covers `~/.gjc/agent/skills`
152
- // and the legacy user roots. Claude/Codex convention skills are import
154
+ // and the legacy user roots, and installed marketplace plugins are gated by the
155
+ // scope they were installed into. Claude/Codex convention skills are import
153
156
  // sources into `.gjc`, never loaded directly.
154
157
  function isSourceEnabled(source: SourceMeta): boolean {
155
158
  const { provider, level } = source;
@@ -162,16 +165,33 @@ export async function loadSkills(options: LoadSkillsOptions = {}): Promise<LoadS
162
165
  // Use capability API to load all skills. `all` (rather than `items`) keeps
163
166
  // shadowed duplicates so this function can apply the documented precedence
164
167
  // 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({
168
+ const providers =
169
+ getCapability<CapabilitySkill>(skillCapability.id)?.providers.filter(provider =>
170
+ LOADABLE_SKILL_PROVIDERS.has(provider.id),
171
+ ) ?? [];
172
+ if (!providers.some(provider => provider.id === "native")) {
173
+ throw new Error("Native skill provider is unavailable");
174
+ }
175
+ const loadContext = {
170
176
  cwd,
171
177
  home,
172
178
  userAgentDir: options.agentDir,
173
179
  repoRoot: await findRepoRoot(cwd),
174
- });
180
+ };
181
+ const providerResults = await Promise.all(
182
+ providers.map(async provider => {
183
+ const allowedLevels = new Set<"user" | "project">();
184
+ if (projectTrusted) allowedLevels.add("project");
185
+ if (userTrusted) allowedLevels.add("user");
186
+ return {
187
+ provider,
188
+ result:
189
+ provider.id === "claude-plugins"
190
+ ? await loadMarketplaceSkills(loadContext, allowedLevels)
191
+ : await provider.load(loadContext),
192
+ };
193
+ }),
194
+ );
175
195
 
176
196
  const skillMap = new Map<string, Skill>();
177
197
  const realPathSet = new Set<string>();
@@ -197,8 +217,11 @@ export async function loadSkills(options: LoadSkillsOptions = {}): Promise<LoadS
197
217
  // precedence. `all` is already in native provider order with project dirs
198
218
  // before user dirs; a stable sort by level lifts every project item above
199
219
  // every user item, giving: project `.gjc/skills` > user roots.
200
- const filteredSkills = result.items
201
- .filter(capSkill => {
220
+ const filteredSkills = providerResults
221
+ .flatMap(({ provider, result }) =>
222
+ result.items.map(capSkill => ({ capSkill, providerPriority: provider.priority })),
223
+ )
224
+ .filter(({ capSkill }) => {
202
225
  if (!isSourceEnabled(capSkill._source)) return false;
203
226
  if (disabledSkillNames.has(capSkill.name)) return false;
204
227
  if (matchesIgnorePatterns(capSkill.name)) return false;
@@ -207,12 +230,12 @@ export async function loadSkills(options: LoadSkillsOptions = {}): Promise<LoadS
207
230
  })
208
231
  .sort((a, b) => {
209
232
  const levelOrder = { project: 0, user: 1 } as const;
210
- return levelOrder[a.level] - levelOrder[b.level];
233
+ return levelOrder[a.capSkill.level] - levelOrder[b.capSkill.level] || b.providerPriority - a.providerPriority;
211
234
  });
212
235
 
213
236
  // Batch resolve all real paths in parallel
214
237
  const realPaths = await Promise.all(
215
- filteredSkills.map(async capSkill => {
238
+ filteredSkills.map(async ({ capSkill }) => {
216
239
  try {
217
240
  return await fs.realpath(capSkill.path);
218
241
  } catch {
@@ -223,7 +246,7 @@ export async function loadSkills(options: LoadSkillsOptions = {}): Promise<LoadS
223
246
 
224
247
  // Process skills with resolved paths
225
248
  for (let i = 0; i < filteredSkills.length; i++) {
226
- const capSkill = filteredSkills[i];
249
+ const { capSkill } = filteredSkills[i];
227
250
  const resolvedPath = realPaths[i];
228
251
 
229
252
  // Skip silently if we've already loaded this exact file (via symlink)
@@ -338,7 +361,12 @@ export async function loadSkills(options: LoadSkillsOptions = {}): Promise<LoadS
338
361
 
339
362
  return {
340
363
  skills,
341
- warnings: [...(result.warnings ?? []).map(w => ({ skillPath: "", message: w })), ...collisionWarnings],
364
+ warnings: [
365
+ ...providerResults.flatMap(({ result }) =>
366
+ (result.warnings ?? []).map(message => ({ skillPath: "", message })),
367
+ ),
368
+ ...collisionWarnings,
369
+ ],
342
370
  };
343
371
  }
344
372
 
@@ -106,7 +106,7 @@ function normalizeRelativeSubdir(relative: string): string {
106
106
  .replaceAll("\\", "/")
107
107
  .replace(/^\.\/+/u, "")
108
108
  .replace(/\/+$/u, "");
109
- if (normalized === "" || normalized === ".") {
109
+ if (normalized === "") {
110
110
  throw new RepositoryBindingError("invalid_binding", "relativeSubdir must be a non-empty relative path");
111
111
  }
112
112
  if (path.isAbsolute(normalized) || normalized.split("/").includes("..")) {
@@ -146,7 +146,43 @@ interface PendingTransitionRelease {
146
146
  recovery?: Promise<boolean>;
147
147
  }
148
148
 
149
+ interface LocalTransitionQueue {
150
+ waiters: Array<() => void>;
151
+ }
152
+
149
153
  const pendingTransitionReleases = new Map<string, PendingTransitionRelease>();
154
+ const localTransitionQueues = new Map<string, LocalTransitionQueue>();
155
+
156
+ async function joinLocalTransitionQueue(
157
+ key: string,
158
+ transitionDir: string,
159
+ retryBudget?: LockRetryBudget,
160
+ ): Promise<LocalTransitionQueue | undefined> {
161
+ const queue = localTransitionQueues.get(key);
162
+ if (!queue) return undefined;
163
+ const ready = Promise.withResolvers<void>();
164
+ const queuedAt = performance.now();
165
+ SessionStateLockTestHooks.afterLocalTransitionQueued?.(transitionDir);
166
+ queue.waiters.push(ready.resolve);
167
+ await ready.promise;
168
+ if (retryBudget) retryBudget.startedAt += Math.max(0, performance.now() - queuedAt);
169
+ return queue;
170
+ }
171
+
172
+ function claimLocalTransitionQueue(key: string): LocalTransitionQueue {
173
+ const queue: LocalTransitionQueue = { waiters: [] };
174
+ localTransitionQueues.set(key, queue);
175
+ return queue;
176
+ }
177
+
178
+ function releaseLocalTransitionQueue(key: string, queue: LocalTransitionQueue): void {
179
+ const next = queue.waiters.shift();
180
+ if (next) {
181
+ next();
182
+ return;
183
+ }
184
+ if (localTransitionQueues.get(key) === queue) localTransitionQueues.delete(key);
185
+ }
150
186
 
151
187
  function clearPendingTransitionRelease(key: string, pending?: PendingTransitionRelease): void {
152
188
  const current = pendingTransitionReleases.get(key);
@@ -241,8 +277,7 @@ function removeOwnedTransitionClaim(nativePath: string, generation: TransitionDi
241
277
  if (!captured.ok || !captured.snapshot) return captured.code === "not_found";
242
278
  const root = captured.snapshot.entries.find(entry => entry.relativePath === "");
243
279
  if (
244
- !root ||
245
- root.kind !== "directory" ||
280
+ root?.kind !== "directory" ||
246
281
  root.dev !== String(generation.dev) ||
247
282
  root.ino !== String(generation.ino) ||
248
283
  root.nlink !== String(generation.nlink) ||
@@ -414,6 +449,8 @@ export const SessionStateLockTestHooks: {
414
449
  forcedQuarantineName?: string;
415
450
  /** @internal Observes bounded acquisition retries without changing their timing. */
416
451
  afterAcquireContention?: (lockFile: string, attempt: number, elapsedMs: number) => void;
452
+ /** @internal Observes process-local admission without altering on-disk ownership. */
453
+ afterLocalTransitionQueued?: (transitionDir: string) => void;
417
454
  /** @internal Runs after transition mkdir contention and before stale-claim inspection. */
418
455
  afterTransitionClaimContention?: (transitionDir: string) => void | Promise<void>;
419
456
  } = {};
@@ -1728,8 +1765,7 @@ async function reclaimStaleTransitionClaim(transitionDir: string, quarantineName
1728
1765
  if (!captured.ok || !captured.snapshot) return false;
1729
1766
  const root = captured.snapshot.entries.find(entry => entry.relativePath === "");
1730
1767
  if (
1731
- !root ||
1732
- root.kind !== "directory" ||
1768
+ root?.kind !== "directory" ||
1733
1769
  root.dev !== String(generation.dev) ||
1734
1770
  root.ino !== String(generation.ino) ||
1735
1771
  root.nlink !== String(generation.nlink) ||
@@ -1995,7 +2031,14 @@ async function recoverPendingTransitionRelease(
1995
2031
  }
1996
2032
  }
1997
2033
 
1998
- /** Run one pathname transition under an atomic `mkdir`/`rmdir` claim. */
2034
+ /**
2035
+ * Run one pathname transition under an atomic `mkdir`/`rmdir` claim.
2036
+ *
2037
+ * Same-process contenders join the current claim owner's lifetime before consuming their
2038
+ * bounded filesystem-contention budget. The on-disk protocol remains authoritative across
2039
+ * processes; this queue only prevents a descheduled local owner from timing out its own
2040
+ * successor while it still legitimately holds the claim.
2041
+ */
1999
2042
  async function withLockPathTransition<T>(
2000
2043
  lockFile: string,
2001
2044
  transition: () => Promise<T>,
@@ -2005,10 +2048,52 @@ async function withLockPathTransition<T>(
2005
2048
  if (ownerAccessStrategy() === "unsupported")
2006
2049
  throw new SessionStateLockUnavailableError(new Error("Safe transition ownership is unsupported."));
2007
2050
  const transitionDir = `${lockFile}${LOCK_TRANSITION_RESOURCE_SUFFIX}`;
2008
- const ownerFile = `${transitionDir}.owner`;
2009
2051
  const recoveryKey = await transitionRecoveryKey(transitionDir);
2052
+ const localKey = recoveryKey;
2010
2053
  const owner = await newLockOwner();
2011
2054
  const budget = retryBudget ?? lockRetryBudget();
2055
+ let localTurn = await joinLocalTransitionQueue(localKey, transitionDir, budget);
2056
+ const claimLocalTurn = (): void => {
2057
+ localTurn ??= claimLocalTransitionQueue(localKey);
2058
+ };
2059
+ const joinLocalTurn = async (): Promise<boolean> => {
2060
+ if (localTurn) return false;
2061
+ const joined = await joinLocalTransitionQueue(localKey, transitionDir, budget);
2062
+ if (!joined) return false;
2063
+ localTurn = joined;
2064
+ return true;
2065
+ };
2066
+ try {
2067
+ return await runLockPathTransition(
2068
+ lockFile,
2069
+ recoveryKey,
2070
+ owner,
2071
+ budget,
2072
+ claimLocalTurn,
2073
+ joinLocalTurn,
2074
+ transition,
2075
+ quarantineName,
2076
+ );
2077
+ } finally {
2078
+ if (localTurn) releaseLocalTransitionQueue(localKey, localTurn);
2079
+ }
2080
+ }
2081
+
2082
+ async function runLockPathTransition<T>(
2083
+ lockFile: string,
2084
+ recoveryKey: string,
2085
+ owner: SessionStateLockOwner,
2086
+ budget: LockRetryBudget,
2087
+ claimLocalTurn: () => void,
2088
+ joinLocalTurn: () => Promise<boolean>,
2089
+ transition: () => Promise<T>,
2090
+ quarantineName = lockQuarantineName(),
2091
+ ): Promise<T> {
2092
+ if (ownerAccessStrategy() === "unsupported")
2093
+ throw new SessionStateLockUnavailableError(new Error("Safe transition ownership is unsupported."));
2094
+ const transitionDir = `${lockFile}${LOCK_TRANSITION_RESOURCE_SUFFIX}`;
2095
+ const ownerFile = `${transitionDir}.owner`;
2096
+
2012
2097
  for (;;) {
2013
2098
  if (ownerAccessStrategy() === "unsupported" && fsSync.existsSync(transitionDir))
2014
2099
  throw new SessionStateLockUnavailableError(new Error("Safe transition ownership is unsupported."));
@@ -2016,12 +2101,15 @@ async function withLockPathTransition<T>(
2016
2101
  // The claim this process stranded in an earlier failed release is gone;
2017
2102
  // fall through and retry the mkdir immediately.
2018
2103
  }
2104
+ if (await joinLocalTurn()) continue;
2019
2105
  try {
2020
2106
  await fs.mkdir(transitionDir, { mode: 0o700 });
2107
+ claimLocalTurn();
2021
2108
  await fs.chmod(transitionDir, 0o700);
2022
2109
  } catch (error) {
2023
2110
  const code = (error as NodeJS.ErrnoException).code;
2024
2111
  if (code !== "EEXIST" && !isTransientLockError(error)) throw new SessionStateLockUnavailableError(error);
2112
+ if (await joinLocalTurn()) continue;
2025
2113
  await SessionStateLockTestHooks.afterTransitionClaimContention?.(transitionDir);
2026
2114
  if (await reclaimStaleTransitionClaim(transitionDir, quarantineName)) {
2027
2115
  if (lockRetryExhausted(budget))