@gajae-code/coding-agent 0.16.3 → 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.
- package/CHANGELOG.md +59 -0
- package/dist/types/cli/args.d.ts +2 -2
- package/dist/types/cli/root-flags.d.ts +4 -2
- package/dist/types/cli.d.ts +1 -1
- package/dist/types/commands/launch.d.ts +1 -1
- package/dist/types/config/autorouting-tier-map.d.ts +3 -0
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/model-resolver.d.ts +9 -0
- package/dist/types/config/settings-schema.d.ts +29 -1
- package/dist/types/discovery/claude-plugins.d.ts +3 -1
- package/dist/types/discovery/helpers.d.ts +2 -0
- package/dist/types/extensibility/gjc-plugins/mcp-policy.d.ts +16 -0
- package/dist/types/extensibility/gjc-plugins/paths.d.ts +2 -0
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +1 -1
- package/dist/types/extensibility/shared-events.d.ts +1 -1
- package/dist/types/gjc-runtime/session-state-lock.d.ts +2 -0
- package/dist/types/modes/components/assistant-message.d.ts +1 -2
- package/dist/types/modes/components/settings-selector.d.ts +6 -1
- package/dist/types/modes/components/status-line/command.d.ts +35 -0
- package/dist/types/modes/components/status-line/index.d.ts +1 -0
- package/dist/types/modes/components/status-line/types.d.ts +5 -0
- package/dist/types/modes/components/tool-status-header.d.ts +14 -0
- package/dist/types/modes/controllers/event-controller.d.ts +6 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/runtime-mcp/client.d.ts +9 -1
- package/dist/types/runtime-mcp/manager.d.ts +7 -1
- package/dist/types/runtime-mcp/pool.d.ts +22 -3
- package/dist/types/runtime-mcp/transports/stdio.d.ts +0 -4
- package/dist/types/runtime-mcp/types.d.ts +22 -0
- package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +12 -0
- package/dist/types/sdk/bus/slack-live-provider.d.ts +6 -0
- package/dist/types/sdk/bus/slack-provider.d.ts +12 -0
- package/dist/types/sdk/host/session-runtime.d.ts +23 -0
- package/dist/types/sdk/session-api-key.d.ts +26 -0
- package/dist/types/sdk/transport/serve-cli.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +9 -0
- package/dist/types/setup/paseo/announce.d.ts +111 -0
- package/dist/types/setup/paseo/setup-deps.d.ts +2 -1
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/task/executor.d.ts +2 -1
- package/dist/types/tools/fetch.d.ts +7 -0
- package/dist/types/tools/index.d.ts +8 -0
- package/dist/types/tools/path-utils.d.ts +2 -0
- package/dist/types/tools/search.d.ts +6 -0
- package/package.json +7 -7
- package/scripts/capture-status-line-custom-editor-showcase.ts +151 -0
- package/scripts/generate-sdk-operation-inventory.ts +9 -0
- package/scripts/verify-status-line-custom-editor-showcase.ts +309 -0
- package/src/cli/args.ts +20 -9
- package/src/cli/root-flags.ts +5 -3
- package/src/config/autorouting-tier-map.ts +1 -0
- package/src/config/file-lock.ts +181 -17
- package/src/config/model-preset-registry.ts +5 -3
- package/src/config/model-profiles.ts +40 -0
- package/src/config/model-registry.ts +57 -11
- package/src/config/model-resolver.ts +48 -0
- package/src/config/settings-schema.ts +33 -1
- package/src/config/settings.ts +1 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +11 -11
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +13 -2
- package/src/discovery/builtin.ts +13 -4
- package/src/discovery/claude-plugins.ts +12 -4
- package/src/discovery/helpers.ts +10 -2
- package/src/extensibility/gjc-plugins/compiler.ts +208 -58
- package/src/extensibility/gjc-plugins/installer.ts +13 -12
- package/src/extensibility/gjc-plugins/mcp-policy.ts +81 -52
- package/src/extensibility/gjc-plugins/paths.ts +13 -0
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +758 -16
- package/src/extensibility/runtime-skill-discovery.ts +77 -151
- package/src/extensibility/shared-events.ts +1 -1
- package/src/extensibility/skills.ts +44 -16
- package/src/gjc-runtime/repository-binding.ts +1 -1
- package/src/gjc-runtime/session-state-lock.ts +92 -2
- package/src/internal-urls/docs-index.generated.ts +7 -7
- package/src/main.ts +72 -34
- package/src/modes/DESIGN.md +54 -0
- package/src/modes/components/assistant-message.ts +22 -9
- package/src/modes/components/model-selector.ts +166 -33
- package/src/modes/components/settings-selector.ts +1075 -235
- package/src/modes/components/status-line/command.ts +190 -0
- package/src/modes/components/status-line/index.ts +1 -0
- package/src/modes/components/status-line/segments.ts +32 -5
- package/src/modes/components/status-line/types.ts +5 -0
- package/src/modes/components/tool-status-header.ts +393 -21
- package/src/modes/controllers/command-controller.ts +2 -0
- package/src/modes/controllers/event-controller.ts +148 -5
- package/src/modes/controllers/selector-controller.ts +21 -0
- package/src/modes/interactive-mode.ts +47 -1
- package/src/modes/types.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +1 -0
- package/src/prompts/system/system-prompt.md +8 -0
- package/src/prompts/tools/search.md +2 -0
- package/src/runtime-mcp/client.ts +60 -17
- package/src/runtime-mcp/manager.ts +171 -14
- package/src/runtime-mcp/pool.test.ts +209 -0
- package/src/runtime-mcp/pool.ts +224 -23
- package/src/runtime-mcp/transports/stdio.ts +158 -24
- package/src/runtime-mcp/types.ts +24 -0
- package/src/sdk/broker/broker.ts +5 -4
- package/src/sdk/bus/chat-daemon-control.ts +1 -1
- package/src/sdk/bus/chat-daemon-runtime.ts +37 -1
- package/src/sdk/bus/slack-daemon.ts +92 -2
- package/src/sdk/bus/slack-live-provider.ts +12 -0
- package/src/sdk/bus/slack-provider.ts +5 -0
- package/src/sdk/cli/session-cli.ts +9 -6
- package/src/sdk/host/session-runtime.test.ts +2 -2
- package/src/sdk/host/session-runtime.ts +275 -43
- package/src/sdk/protocol/operation-inventory.generated.json +66 -0
- package/src/sdk/session-api-key.ts +94 -0
- package/src/sdk/session.ts +73 -29
- package/src/sdk/transport/serve-cli.ts +3 -3
- package/src/session/agent-session.ts +123 -38
- package/src/session/contribution-prep.ts +214 -21
- package/src/setup/paseo/announce.ts +450 -0
- package/src/setup/paseo/setup-deps.ts +15 -3
- package/src/slash-commands/builtin-registry.ts +1 -0
- package/src/system-prompt.ts +4 -0
- package/src/task/executor.ts +2 -1
- package/src/task/index.ts +17 -4
- package/src/tools/bash.ts +35 -8
- package/src/tools/cron.ts +1 -1
- package/src/tools/fetch.ts +29 -1
- package/src/tools/index.ts +8 -0
- package/src/tools/job.ts +8 -2
- package/src/tools/monitor.ts +8 -2
- package/src/tools/path-utils.ts +7 -0
- package/src/tools/search.ts +116 -58
- package/src/tools/skill.ts +22 -2
- package/src/tools/subagent.ts +3 -1
- package/src/tools/tool-catalog.generated.ts +8 -1
- package/src/web/insane/bridge.ts +113 -23
- 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 {
|
|
4
|
-
import { findRepoRoot
|
|
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,
|
|
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
|
|
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
|
|
226
|
+
async function collectPluginSkills(
|
|
238
227
|
home: string,
|
|
239
228
|
cwd: string,
|
|
240
|
-
|
|
241
|
-
|
|
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
|
|
247
|
-
return
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
359
|
-
seenNames.
|
|
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
|
|
427
|
-
//
|
|
428
|
-
//
|
|
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
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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
|
-
|
|
524
|
-
const
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
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;
|
|
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.
|
|
120
|
-
*
|
|
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
|
|
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
|
|
166
|
-
provider =>
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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 =
|
|
201
|
-
.
|
|
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: [
|
|
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 === ""
|
|
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);
|
|
@@ -413,6 +449,8 @@ export const SessionStateLockTestHooks: {
|
|
|
413
449
|
forcedQuarantineName?: string;
|
|
414
450
|
/** @internal Observes bounded acquisition retries without changing their timing. */
|
|
415
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;
|
|
416
454
|
/** @internal Runs after transition mkdir contention and before stale-claim inspection. */
|
|
417
455
|
afterTransitionClaimContention?: (transitionDir: string) => void | Promise<void>;
|
|
418
456
|
} = {};
|
|
@@ -1993,7 +2031,14 @@ async function recoverPendingTransitionRelease(
|
|
|
1993
2031
|
}
|
|
1994
2032
|
}
|
|
1995
2033
|
|
|
1996
|
-
/**
|
|
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
|
+
*/
|
|
1997
2042
|
async function withLockPathTransition<T>(
|
|
1998
2043
|
lockFile: string,
|
|
1999
2044
|
transition: () => Promise<T>,
|
|
@@ -2003,10 +2048,52 @@ async function withLockPathTransition<T>(
|
|
|
2003
2048
|
if (ownerAccessStrategy() === "unsupported")
|
|
2004
2049
|
throw new SessionStateLockUnavailableError(new Error("Safe transition ownership is unsupported."));
|
|
2005
2050
|
const transitionDir = `${lockFile}${LOCK_TRANSITION_RESOURCE_SUFFIX}`;
|
|
2006
|
-
const ownerFile = `${transitionDir}.owner`;
|
|
2007
2051
|
const recoveryKey = await transitionRecoveryKey(transitionDir);
|
|
2052
|
+
const localKey = recoveryKey;
|
|
2008
2053
|
const owner = await newLockOwner();
|
|
2009
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
|
+
|
|
2010
2097
|
for (;;) {
|
|
2011
2098
|
if (ownerAccessStrategy() === "unsupported" && fsSync.existsSync(transitionDir))
|
|
2012
2099
|
throw new SessionStateLockUnavailableError(new Error("Safe transition ownership is unsupported."));
|
|
@@ -2014,12 +2101,15 @@ async function withLockPathTransition<T>(
|
|
|
2014
2101
|
// The claim this process stranded in an earlier failed release is gone;
|
|
2015
2102
|
// fall through and retry the mkdir immediately.
|
|
2016
2103
|
}
|
|
2104
|
+
if (await joinLocalTurn()) continue;
|
|
2017
2105
|
try {
|
|
2018
2106
|
await fs.mkdir(transitionDir, { mode: 0o700 });
|
|
2107
|
+
claimLocalTurn();
|
|
2019
2108
|
await fs.chmod(transitionDir, 0o700);
|
|
2020
2109
|
} catch (error) {
|
|
2021
2110
|
const code = (error as NodeJS.ErrnoException).code;
|
|
2022
2111
|
if (code !== "EEXIST" && !isTransientLockError(error)) throw new SessionStateLockUnavailableError(error);
|
|
2112
|
+
if (await joinLocalTurn()) continue;
|
|
2023
2113
|
await SessionStateLockTestHooks.afterTransitionClaimContention?.(transitionDir);
|
|
2024
2114
|
if (await reclaimStaleTransitionClaim(transitionDir, quarantineName)) {
|
|
2025
2115
|
if (lockRetryExhausted(budget))
|