jeo-code 0.7.17 → 0.7.18

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 CHANGED
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
  The README mirrors the latest 5 entries — regenerate with `bun run changelog:sync`.
8
8
 
9
9
 
10
+ ## [0.7.18] - 2026-06-26
11
+ _Slash-command discovery and the `/model` flow reach gajae-code parity. The slash palette/autocomplete now fuzzy-matches command names (with a description fallback for intent-style queries), resolved skills can contribute their own `/aliases` as real dispatchable commands, and `/model` runs gjc's two-menu target → reasoning flow so a picked model can be assigned to the default or any subagent role with its own thinking budget._
12
+
13
+ ### Added
14
+ - **Slash completion gains gjc §2.1 fuzzy name matching with a description fallback.** `src/tui/components/autocomplete.ts` adds pure `fuzzyMatch`/`fuzzyScore` (exact 100 > startsWith 80 > includes 60 > subsequence `40 − gaps×5`, min 1) and a `fuzzyCommandHits` ranker, so `/mdl` → `/model` now completes while prefix-exact behaviour (`/mod` → `/model`) is preserved. When nothing matches a command name, both the autocomplete dropdown and `matchSlash` (`slash.ts`) fall back to a description substring match — `/oauth` → `/login`, `/clipboard` → `/dump` — gated to ≥2-char queries with a real substring hit to avoid flooding. New `SLASH_COMMAND_DESCRIPTIONS` map backs the fallback.
15
+ - **Resolved skills can contribute `/` slash commands, not just `$<name>`.** Every slash alias a resolved skill declares (frontmatter `aliases:`/`slash:`) becomes a real, dispatchable palette command grouped under "skills" — extending the slash menu, autocomplete, and previews. `runLaunchCommand` (`src/commands/launch.ts`) wires `skillSlashAliases`/`parseSkillSlashInvocation` so both the interactive REPL (`/obsidian-capture note`) and one-shot mode (`jeo "/obsidian-capture note"`) dispatch the owning skill, with built-in slash handlers always winning and first-declarer-wins alias scoping.
16
+ - **`buildThinkingLevelChoices` extracts the reasoning menu (gjc `#renderThinkingMenu`).** `src/tui/components/live-model-picker.ts` adds `THINKING_LEVEL_ORDER` and a pure `buildThinkingLevelChoices(current, { inheritLabel, tokenHint })` that renders `<level> — <description> (<tokens>)` with the current level flagged and an optional leading `inherit` row for role targets — fully unit-testable.
17
+
18
+ ### Changed
19
+ - **`/model` runs gjc's two-menu target → reasoning flow.** After a model is picked, `applyPickedModelWithTarget` now shows `applyTargetChoices` ("Set as DEFAULT / EXECUTOR / …", which target uses the model) followed by the per-target reasoning menu. DEFAULT updates the active session model and default thinking; a subagent role writes `~/.jeo/config.json` (model + thinking, or inherit) without switching the chat model. Replaces the prior default-only assignment.
20
+
21
+ ### Verified
22
+ - `bun run typecheck` clean; full suite green — 2009 pass / 0 fail across 234 files — including new `test/autocomplete.test.ts` cases (`fuzzyMatch` subsequence, `fuzzyScore` ranking, non-prefix subsequence completion, prefix-exact preservation, description-only fallback), `test/slash.test.ts` (`matchSlash` description fallback for intent queries), and `test/live-model-picker.test.ts` (`buildThinkingLevelChoices` ordering, token hints skipping minimal, `inherit` row only with a label, explicit role level outranking inherit).
23
+
24
+
10
25
  ## [0.7.17] - 2026-06-25
11
26
  _Developer workflow parity (gjc `dev:link`/`dev:doctor`, adapted for jeo's zero-native-dep Bun runtime): the global `jeo` command can be linked to run this checkout's source hot to every edit, with a drift doctor that flags when `jeo` resolves to a compiled binary or an installed copy instead. README gains "Skill migration and bundled skill inspection" + "Development" sections. Also ships OKF concept-memory search/scoring with budget-aware injection and a round of workflow-prompt hardening (anti-punt, todo-first planning, verdict discipline) that keeps every loop escape hatch intact._
12
27
 
package/README.ja.md CHANGED
@@ -205,11 +205,11 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
205
205
  ## 変更履歴 (Changelog)
206
206
 
207
207
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
208
+ - **[0.7.18]** (2026-06-26) — Slash-command discovery and the `/model` flow reach gajae-code parity. The slash palette/autocomplete now fuzzy-matches command names (with a description fallback for intent-style queries), resolved skills can contribute their own `/aliases` as real dispatchable commands, and `/model` runs gjc's two-menu target → reasoning flow so a picked model can be assigned to the default or any subagent role with its own thinking budget.
208
209
  - **[0.7.17]** (2026-06-25) — Developer workflow parity (gjc `dev:link`/`dev:doctor`, adapted for jeo's zero-native-dep Bun runtime): the global `jeo` command can be linked to run this checkout's source hot to every edit, with a drift doctor that flags when `jeo` resolves to a compiled binary or an installed copy instead. README gains "Skill migration and bundled skill inspection" + "Development" sections. Also ships OKF concept-memory search/scoring with budget-aware injection and a round of workflow-prompt hardening (anti-punt, todo-first planning, verdict discipline) that keeps every loop escape hatch intact.
209
210
  - **[0.7.16]** (2026-06-25) — Bundled skills get a safe installer: `jeo skills sync` materializes the built-in workflow skills into `~/.jeo/skills`, preserving local edits by default, with a CI-friendly `--check` drift report and a `--force` overwrite.
210
211
  - **[0.7.15]** (2026-06-25) — Rollback release: the runtime source and test suite are restored to the **0.7.9** state. The intervening 0.7.10–0.7.14 runtime changes (quiet-mode/`-q`, `/resume` transcript rework, `mutatedFiles` plumbing, Ollama `num_ctx`, tmux REPL reaping, and related engine/provider/TUI edits) are reverted. Published as a new version because npm cannot re-issue an existing version number; `latest` now serves code identical to 0.7.9.
211
212
  - **[0.7.14]** (2026-06-25) — Quieter, cleaner output: a new `-q`/`--quiet` flag strips launch banners and courtesy logs, `/resume` no longer dumps walls of raw escaped JSON into scrollback, and a turn that edits files now reliably surfaces those paths to orchestrators.
212
- - **[0.7.9]** (2026-06-24) — Local model providers actually run, and OAuth/login hygiene: Ollama requests now carry an explicit `num_ctx` so jeo's large system prompt no longer overflows Ollama's small default window (the "ollama 모델이 안 돌아간다" blocker), LM Studio reasoning models that emit the whole reply on the reasoning channel are recovered instead of dying empty, GGUF chat templates that can't render the `tools` array are retried with native tools stripped, the OAuth success tab auto-closes with a clear hint, the effective-credential picker prefers a verified OAuth login over a stray API key, and abandoned idle `jeo` tmux REPL sessions are reaped on launch.
213
213
 
214
214
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
215
215
  <!-- CHANGELOG:END -->
package/README.ko.md CHANGED
@@ -205,11 +205,11 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
205
205
  ## 변경 이력 (Changelog)
206
206
 
207
207
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
208
+ - **[0.7.18]** (2026-06-26) — Slash-command discovery and the `/model` flow reach gajae-code parity. The slash palette/autocomplete now fuzzy-matches command names (with a description fallback for intent-style queries), resolved skills can contribute their own `/aliases` as real dispatchable commands, and `/model` runs gjc's two-menu target → reasoning flow so a picked model can be assigned to the default or any subagent role with its own thinking budget.
208
209
  - **[0.7.17]** (2026-06-25) — Developer workflow parity (gjc `dev:link`/`dev:doctor`, adapted for jeo's zero-native-dep Bun runtime): the global `jeo` command can be linked to run this checkout's source hot to every edit, with a drift doctor that flags when `jeo` resolves to a compiled binary or an installed copy instead. README gains "Skill migration and bundled skill inspection" + "Development" sections. Also ships OKF concept-memory search/scoring with budget-aware injection and a round of workflow-prompt hardening (anti-punt, todo-first planning, verdict discipline) that keeps every loop escape hatch intact.
209
210
  - **[0.7.16]** (2026-06-25) — Bundled skills get a safe installer: `jeo skills sync` materializes the built-in workflow skills into `~/.jeo/skills`, preserving local edits by default, with a CI-friendly `--check` drift report and a `--force` overwrite.
210
211
  - **[0.7.15]** (2026-06-25) — Rollback release: the runtime source and test suite are restored to the **0.7.9** state. The intervening 0.7.10–0.7.14 runtime changes (quiet-mode/`-q`, `/resume` transcript rework, `mutatedFiles` plumbing, Ollama `num_ctx`, tmux REPL reaping, and related engine/provider/TUI edits) are reverted. Published as a new version because npm cannot re-issue an existing version number; `latest` now serves code identical to 0.7.9.
211
212
  - **[0.7.14]** (2026-06-25) — Quieter, cleaner output: a new `-q`/`--quiet` flag strips launch banners and courtesy logs, `/resume` no longer dumps walls of raw escaped JSON into scrollback, and a turn that edits files now reliably surfaces those paths to orchestrators.
212
- - **[0.7.9]** (2026-06-24) — Local model providers actually run, and OAuth/login hygiene: Ollama requests now carry an explicit `num_ctx` so jeo's large system prompt no longer overflows Ollama's small default window (the "ollama 모델이 안 돌아간다" blocker), LM Studio reasoning models that emit the whole reply on the reasoning channel are recovered instead of dying empty, GGUF chat templates that can't render the `tools` array are retried with native tools stripped, the OAuth success tab auto-closes with a clear hint, the effective-credential picker prefers a verified OAuth login over a stray API key, and abandoned idle `jeo` tmux REPL sessions are reaped on launch.
213
213
 
214
214
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
215
215
  <!-- CHANGELOG:END -->
package/README.md CHANGED
@@ -232,11 +232,11 @@ Huge thanks to [gajae-code](https://github.com/Yeachan-Heo/gajae-code) for the i
232
232
  ## Changelog
233
233
 
234
234
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
235
+ - **[0.7.18]** (2026-06-26) — Slash-command discovery and the `/model` flow reach gajae-code parity. The slash palette/autocomplete now fuzzy-matches command names (with a description fallback for intent-style queries), resolved skills can contribute their own `/aliases` as real dispatchable commands, and `/model` runs gjc's two-menu target → reasoning flow so a picked model can be assigned to the default or any subagent role with its own thinking budget.
235
236
  - **[0.7.17]** (2026-06-25) — Developer workflow parity (gjc `dev:link`/`dev:doctor`, adapted for jeo's zero-native-dep Bun runtime): the global `jeo` command can be linked to run this checkout's source hot to every edit, with a drift doctor that flags when `jeo` resolves to a compiled binary or an installed copy instead. README gains "Skill migration and bundled skill inspection" + "Development" sections. Also ships OKF concept-memory search/scoring with budget-aware injection and a round of workflow-prompt hardening (anti-punt, todo-first planning, verdict discipline) that keeps every loop escape hatch intact.
236
237
  - **[0.7.16]** (2026-06-25) — Bundled skills get a safe installer: `jeo skills sync` materializes the built-in workflow skills into `~/.jeo/skills`, preserving local edits by default, with a CI-friendly `--check` drift report and a `--force` overwrite.
237
238
  - **[0.7.15]** (2026-06-25) — Rollback release: the runtime source and test suite are restored to the **0.7.9** state. The intervening 0.7.10–0.7.14 runtime changes (quiet-mode/`-q`, `/resume` transcript rework, `mutatedFiles` plumbing, Ollama `num_ctx`, tmux REPL reaping, and related engine/provider/TUI edits) are reverted. Published as a new version because npm cannot re-issue an existing version number; `latest` now serves code identical to 0.7.9.
238
239
  - **[0.7.14]** (2026-06-25) — Quieter, cleaner output: a new `-q`/`--quiet` flag strips launch banners and courtesy logs, `/resume` no longer dumps walls of raw escaped JSON into scrollback, and a turn that edits files now reliably surfaces those paths to orchestrators.
239
- - **[0.7.9]** (2026-06-24) — Local model providers actually run, and OAuth/login hygiene: Ollama requests now carry an explicit `num_ctx` so jeo's large system prompt no longer overflows Ollama's small default window (the "ollama 모델이 안 돌아간다" blocker), LM Studio reasoning models that emit the whole reply on the reasoning channel are recovered instead of dying empty, GGUF chat templates that can't render the `tools` array are retried with native tools stripped, the OAuth success tab auto-closes with a clear hint, the effective-credential picker prefers a verified OAuth login over a stray API key, and abandoned idle `jeo` tmux REPL sessions are reaped on launch.
240
240
 
241
241
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
242
242
  <!-- CHANGELOG:END -->
package/README.zh.md CHANGED
@@ -205,11 +205,11 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
205
205
  ## 更新日志 (Changelog)
206
206
 
207
207
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
208
+ - **[0.7.18]** (2026-06-26) — Slash-command discovery and the `/model` flow reach gajae-code parity. The slash palette/autocomplete now fuzzy-matches command names (with a description fallback for intent-style queries), resolved skills can contribute their own `/aliases` as real dispatchable commands, and `/model` runs gjc's two-menu target → reasoning flow so a picked model can be assigned to the default or any subagent role with its own thinking budget.
208
209
  - **[0.7.17]** (2026-06-25) — Developer workflow parity (gjc `dev:link`/`dev:doctor`, adapted for jeo's zero-native-dep Bun runtime): the global `jeo` command can be linked to run this checkout's source hot to every edit, with a drift doctor that flags when `jeo` resolves to a compiled binary or an installed copy instead. README gains "Skill migration and bundled skill inspection" + "Development" sections. Also ships OKF concept-memory search/scoring with budget-aware injection and a round of workflow-prompt hardening (anti-punt, todo-first planning, verdict discipline) that keeps every loop escape hatch intact.
209
210
  - **[0.7.16]** (2026-06-25) — Bundled skills get a safe installer: `jeo skills sync` materializes the built-in workflow skills into `~/.jeo/skills`, preserving local edits by default, with a CI-friendly `--check` drift report and a `--force` overwrite.
210
211
  - **[0.7.15]** (2026-06-25) — Rollback release: the runtime source and test suite are restored to the **0.7.9** state. The intervening 0.7.10–0.7.14 runtime changes (quiet-mode/`-q`, `/resume` transcript rework, `mutatedFiles` plumbing, Ollama `num_ctx`, tmux REPL reaping, and related engine/provider/TUI edits) are reverted. Published as a new version because npm cannot re-issue an existing version number; `latest` now serves code identical to 0.7.9.
211
212
  - **[0.7.14]** (2026-06-25) — Quieter, cleaner output: a new `-q`/`--quiet` flag strips launch banners and courtesy logs, `/resume` no longer dumps walls of raw escaped JSON into scrollback, and a turn that edits files now reliably surfaces those paths to orchestrators.
212
- - **[0.7.9]** (2026-06-24) — Local model providers actually run, and OAuth/login hygiene: Ollama requests now carry an explicit `num_ctx` so jeo's large system prompt no longer overflows Ollama's small default window (the "ollama 모델이 안 돌아간다" blocker), LM Studio reasoning models that emit the whole reply on the reasoning channel are recovered instead of dying empty, GGUF chat templates that can't render the `tools` array are retried with native tools stripped, the OAuth success tab auto-closes with a clear hint, the effective-credential picker prefers a verified OAuth login over a stray API key, and abandoned idle `jeo` tmux REPL sessions are reaped on launch.
213
213
 
214
214
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
215
215
  <!-- CHANGELOG:END -->
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jeo-code",
3
- "version": "0.7.17",
3
+ "version": "0.7.18",
4
4
  "description": "Clean, highly optimized AI coding agent using spec-first loop",
5
5
  "type": "module",
6
6
  "main": "src/cli.ts",
@@ -14,7 +14,7 @@ import { runDeepInterviewEngine, type DeepInterviewEngineOptions } from "./deep-
14
14
  import { runRalplanEngine, type RalplanEngineOptions } from "./ralplan";
15
15
  import { runTeamEngine, type TeamEngineOptions } from "./team";
16
16
  import { runUltragoalEngine, type UltragoalEngineOptions } from "./ultragoal";
17
- import { skillsPromptSection, loadSkills, buildSkillTask, workflowSkillsForPrompt, parseSkillInvocation, parseSkillChain, looksLikeSkillEcho, skillInvocationCard, type SkillDoc, type SkillInvocation } from "../skills/catalog";
17
+ import { skillsPromptSection, loadSkills, buildSkillTask, workflowSkillsForPrompt, parseSkillInvocation, parseSkillChain, parseSkillSlashInvocation, skillSlashAliases, looksLikeSkillEcho, skillInvocationCard, type SkillDoc, type SkillInvocation } from "../skills/catalog";
18
18
  import { formatForgeBox, scaleForgeWidth } from "../tui/components/forge";
19
19
  import { interactiveOAuthLogin } from "./auth";
20
20
  import { logoutOAuth, OAUTH_PROVIDERS, API_KEY_ONLY_PROVIDERS, setApiKey } from "../auth";
@@ -43,7 +43,7 @@ import { readGoalState, writeGoalState, clearGoalState, verifyGoal } from "../ag
43
43
  import { listAliases } from "../ai/model-registry";
44
44
  import { openaiCompatDef, SUBSCRIPTION_PROVIDER_NAMES } from "../ai/providers/openai-compatible-catalog";
45
45
 
46
- import { allSubagentRoles, getSubagentRole, resolveSubagentModel, resolveSubagentMaxSteps, resolveSubagentThinking, parseMaxSteps, withSubagentSetting, clearSubagentSetting } from "../agent/subagents";
46
+ import { allSubagentRoles, getSubagentRole, resolveSubagentModel, resolveSubagentMaxSteps, resolveSubagentThinking, parseMaxSteps, withSubagentSetting, clearSubagentSetting, applyTargetChoices } from "../agent/subagents";
47
47
  import { SelectList, renderSelectList, type SelectItem } from "../tui/components/select-list";
48
48
  import { SessionPicker, renderSessionPicker } from "../tui/components/session-picker";
49
49
  import {
@@ -57,7 +57,7 @@ import {
57
57
  formatPickListWithCapabilities,
58
58
  formatCapabilityLine,
59
59
  } from "../tui/components/config-panel";
60
- import { liveModelPicker, renderLiveModelPicker, type ModelAssignmentBadge } from "../tui/components/live-model-picker";
60
+ import { liveModelPicker, renderLiveModelPicker, buildThinkingLevelChoices, type ModelAssignmentBadge } from "../tui/components/live-model-picker";
61
61
 
62
62
  import { loginPicker, renderLoginPicker, onboardingPicker, renderOnboardingPicker, apiKeyPicker, renderApiKeyPicker, subscriptionLoginPicker, type OnboardingAction } from "../tui/components/provider-picker";
63
63
  import { sanitizeForTerminal } from "../tui/components/code-view";
@@ -522,10 +522,27 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
522
522
 
523
523
  const workflowSkills = workflowSkillsForPrompt(resolvedSkills);
524
524
  const resolvedSkillNames = resolvedSkills.map(s => s.name);
525
- // Skills are invoked ONLY via the `$<name>` entrypoint, never `/`. The `/` menu therefore
526
- // advertises NO skill slash commands — keeping `skillSlashDetails` empty leaves the slash
527
- // palette, autocomplete, and previews free of skill entries.
525
+ // Skills can ADD `/` commands: every slash alias a resolved skill declares (frontmatter
526
+ // `aliases:`/`slash:`) becomes a real, dispatchable palette command grouped under "skills",
527
+ // so installing a skill extends the slash menu, autocomplete, and previews — not just `$<name>`.
528
+ // First declarer of an alias wins (aliases are owner-scoped, so cross-skill collisions are rare).
528
529
  const skillSlashDetails: SlashCommandInfo[] = [];
530
+ {
531
+ const seenAlias = new Set<string>();
532
+ for (const s of resolvedSkills) {
533
+ for (const alias of skillSlashAliases(s)) {
534
+ const key = alias.toLowerCase();
535
+ if (seenAlias.has(key)) continue;
536
+ seenAlias.add(key);
537
+ skillSlashDetails.push({
538
+ command: alias,
539
+ usage: `${alias} [intent]`,
540
+ description: `${s.name} skill — ${s.summary}`,
541
+ group: "skills",
542
+ });
543
+ }
544
+ }
545
+ }
529
546
 
530
547
  const protocol = buildToolProtocol(allowedTools);
531
548
  const preamble = flags.systemPrompt ?? "You are the jeo, an interactive coding agent.\nAccomplish the user's request by calling tools and verifying your work.";
@@ -1260,12 +1277,18 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
1260
1277
  }
1261
1278
  return;
1262
1279
  }
1263
- // Slash / file-path / `/skill:` entrypoints (non-`$`) still resolve to a single skill.
1280
+ // A `$skill` invocation resolves to a single skill.
1264
1281
  const skillInvocation = parseSkillInvocation(messageContent, resolvedSkills);
1265
1282
  if (skillInvocation) {
1266
1283
  await runOneSkillShot(skillInvocation);
1267
1284
  return;
1268
1285
  }
1286
+ // A declared skill `/alias` (e.g. `jeo "/obsidian-capture note"`) dispatches the owning skill.
1287
+ const slashSkill = parseSkillSlashInvocation(messageContent, resolvedSkills);
1288
+ if (slashSkill) {
1289
+ await runOneSkillShot(slashSkill);
1290
+ return;
1291
+ }
1269
1292
  try {
1270
1293
  const { reply, rendered, usage } = await runTurn(messageContent, shouldUseOneShotTui(flags.noTui));
1271
1294
  if (!rendered) console.log(stripMarkdown(renderMarkdownTables(reply)) + usage);
@@ -2449,15 +2472,10 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
2449
2472
  current: ThinkLevel | undefined,
2450
2473
  inheritLabel?: string,
2451
2474
  ): Promise<ThinkLevel | "inherit" | undefined> => {
2452
- const mark = (lvl: string): string => (current === lvl ? "current" : "");
2453
- const levels: { value: string; label: string; hint?: string }[] = [
2454
- ...(inheritLabel ? [{ value: "inherit", label: inheritLabel, hint: current === undefined ? "current" : "" }] : []),
2455
- { value: "minimal", label: "minimal — lightest reasoning", hint: mark("minimal") },
2456
- { value: "low", label: `low — light reasoning (~${Math.round(thinkingMaxTokens("low") / 1000)}k tokens)`, hint: mark("low") },
2457
- { value: "medium", label: `medium — moderate reasoning (~${Math.round(thinkingMaxTokens("medium") / 1000)}k tokens)`, hint: mark("medium") },
2458
- { value: "high", label: `high — deep reasoning (~${Math.round(thinkingMaxTokens("high") / 1000)}k tokens)`, hint: mark("high") },
2459
- { value: "xhigh", label: `xhigh — maximum reasoning (~${Math.round(thinkingMaxTokens("xhigh") / 1000)}k tokens)`, hint: mark("xhigh") },
2460
- ];
2475
+ const levels = buildThinkingLevelChoices(current, {
2476
+ inheritLabel,
2477
+ tokenHint: lvl => (lvl === "minimal" ? undefined : `~${Math.round(thinkingMaxTokens(lvl as ThinkLevel) / 1000)}k tokens`),
2478
+ });
2461
2479
  const picked = await pickFromOptions(title, levels);
2462
2480
  return picked === "inherit" || isThinkingLevel(picked) ? picked : undefined;
2463
2481
  };
@@ -2485,57 +2503,63 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
2485
2503
  return leaf.replace(/^gpt/i, "GPT");
2486
2504
  };
2487
2505
 
2488
- const modelActionChoices = (config: Awaited<ReturnType<typeof readGlobalConfig>>): SelectItem<string>[] => {
2489
- const levels: ThinkLevel[] = ["minimal", "low", "medium", "high", "xhigh"];
2490
- const choices: SelectItem<string>[] = [];
2491
- const currentDefaultThinking = sessionThinking ?? config.thinkingLevel ?? "medium";
2492
- const appendChildren = (children: Array<Omit<SelectItem<string>, "depth" | "branch">>) => {
2493
- children.forEach((child, index) => {
2494
- choices.push({ ...child, depth: 1, branch: index === children.length - 1 ? "last" : "mid" });
2495
- });
2496
- };
2497
-
2498
- choices.push({
2499
- value: "heading:default",
2500
- label: "Set as DEFAULT (Default)",
2501
- hint: `${config.defaultModel} (${currentDefaultThinking}) · roles → /agents`,
2502
- disabled: true,
2503
- });
2504
- appendChildren([
2505
- { value: "default:keep", label: "Set model only", hint: `keep thinking ${currentDefaultThinking}` },
2506
- ...levels.map(level => ({
2507
- value: `default:${level}`,
2508
- label: `thinking ${level}`,
2509
- hint: level === currentDefaultThinking ? "current" : `~${Math.round(thinkingMaxTokens(level) / 1000)}k tokens`,
2510
- })),
2511
- ]);
2512
-
2513
- return choices;
2514
- };
2515
-
2516
-
2517
-
2518
-
2506
+ // gajae-code `/model` parity: after a model is picked, the interactive flow
2507
+ // runs TWO menus — gjc's `#renderActionMenu` ("Set as DEFAULT / EXECUTOR / …",
2508
+ // i.e. WHICH target uses this model) followed by its `#renderThinkingMenu`
2509
+ // ("Reasoning for <target>", the per-target reasoning budget). DEFAULT updates
2510
+ // the active session model; a subagent role writes ~/.jeo/config.json without
2511
+ // switching the chat model.
2519
2512
  const applyPickedModelWithTarget = async (target: string): Promise<boolean> => {
2520
2513
  if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
2521
2514
  const cfgForPick = await readGlobalConfig();
2522
- // `/model` only assigns the DEFAULT model + (optionally) the default thinking.
2523
- // Per-role model and thinking are configured in /agents (and /agents edit).
2524
- const choice = await pickFromOptions(`Model Name: ${displayModelName(target)}\n\nAction for: ${target}`, modelActionChoices(cfgForPick)) ?? "default:keep";
2525
- const [, action = "keep"] = choice.split(":", 2);
2526
- const { resolved, provider } = await describeModel(target);
2527
- const st = (await describeAllProviders(cfgForPick)).find(s => s.name === provider);
2528
- sessionModel = target;
2529
- await persistSessionModel();
2530
- const defaultThinking = isThinkingLevel(action) ? action : undefined;
2531
- if (defaultThinking) {
2532
- sessionThinking = defaultThinking;
2515
+ const targetId = await pickFromOptions(
2516
+ `Model Name: ${displayModelName(target)}\n\nAction for: ${target}`,
2517
+ applyTargetChoices(cfgForPick).map(c => ({ value: c.value, label: c.label, hint: c.hint })),
2518
+ );
2519
+ if (!targetId) {
2520
+ console.log("(cancelled)");
2521
+ return true;
2522
+ }
2523
+
2524
+ if (targetId === "default") {
2525
+ const currentDefaultThinking = (sessionThinking ?? cfgForPick.thinkingLevel ?? "medium") as ThinkLevel;
2526
+ const level = await pickThinkingLevel(`Reasoning for default: ${target}`, currentDefaultThinking);
2527
+ if (level === undefined) {
2528
+ console.log("(cancelled)");
2529
+ return true;
2530
+ }
2531
+ const { resolved, provider } = await describeModel(target);
2532
+ const st = (await describeAllProviders(cfgForPick)).find(s => s.name === provider);
2533
+ sessionModel = target;
2534
+ await persistSessionModel();
2535
+ const defaultThinking = isThinkingLevel(level) ? level : undefined;
2536
+ if (defaultThinking) {
2537
+ sessionThinking = defaultThinking;
2538
+ }
2539
+ await saveConfigPatch(raw => ({
2540
+ ...rememberModelPatch(raw, target),
2541
+ ...(defaultThinking ? { thinkingLevel: defaultThinking } : {}),
2542
+ }));
2543
+ console.log(`Model set to ${formatModelLine({ label: target, resolved, provider, ready: st?.ready })}${defaultThinking ? ` · thinking ${defaultThinking}` : ""} — saved as default.`);
2544
+ return true;
2545
+ }
2546
+
2547
+ const role = getSubagentRole(targetId, cfgForPick);
2548
+ if (!role) return false;
2549
+ const currentRoleThinking = resolveSubagentThinking(role.id, cfgForPick) as ThinkLevel | undefined;
2550
+ const level = await pickThinkingLevel(
2551
+ `Reasoning for ${role.title}: ${target}`,
2552
+ currentRoleThinking,
2553
+ `inherit (default ${cfgForPick.thinkingLevel ?? "medium"})`,
2554
+ );
2555
+ if (level === undefined) {
2556
+ console.log("(cancelled)");
2557
+ return true;
2533
2558
  }
2534
- await saveConfigPatch(raw => ({
2535
- ...rememberModelPatch(raw, target),
2536
- ...(defaultThinking ? { thinkingLevel: defaultThinking } : {}),
2537
- }));
2538
- console.log(`Model set to ${formatModelLine({ label: target, resolved, provider, ready: st?.ready })}${defaultThinking ? ` · thinking ${defaultThinking}` : ""} — saved as default. Role models/thinking: /agents`);
2559
+ const thinking = level === "inherit" ? undefined : (isThinkingLevel(level) ? level : undefined);
2560
+ await saveConfigPatch(raw => ({ subagents: withSubagentSetting(raw, role.id, { model: target, thinking }) }));
2561
+ const { provider } = await describeModel(target);
2562
+ console.log(`${role.title} model set to ${target} (${provider})${level === "inherit" ? " · thinking inherits default" : ` · thinking ${level}`} — saved to ~/.jeo/config.json.`);
2539
2563
  return true;
2540
2564
  };
2541
2565
 
@@ -4419,6 +4443,20 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
4419
4443
  continue;
4420
4444
  }
4421
4445
  }
4446
+ // A declared skill slash alias (e.g. `/obsidian-capture note`) dispatches the owning skill,
4447
+ // passing the alias as the routing signal and the rest of the line as the intent. Placed
4448
+ // after every built-in slash handler (built-ins win) and before the unhandled-slash fallback.
4449
+ if (!flags.noSkills && input.startsWith("/")) {
4450
+ const slashSkill = parseSkillSlashInvocation(input, resolvedSkills);
4451
+ if (slashSkill) {
4452
+ try {
4453
+ await runSkillInvocation(slashSkill.skill, slashSkill.intent, slashSkill.invokedAs);
4454
+ } catch (err) {
4455
+ console.log(`! ${(err as Error).message}`);
4456
+ }
4457
+ continue;
4458
+ }
4459
+ }
4422
4460
  // Unhandled slash attempt → suggest, don't send the typo to the model.
4423
4461
  if (isSlashAttempt(input)) {
4424
4462
  const m = matchSlash(input, [...completionContext().slashCommands]);
@@ -521,6 +521,20 @@ export function getSkillBySlash(skills: SkillDoc[], command: string): SkillDoc |
521
521
  return skills.find(s => skillSlashAliases(s).some(a => a.toLowerCase() === q));
522
522
  }
523
523
 
524
+ /** Resolve a leading `/alias` slash command to the skill that DECLARES it, e.g.
525
+ * `/obsidian-capture note text` → { skill, intent: "note text", invokedAs: "/obsidian-capture" }.
526
+ * Returns null when the first token is not a declared skill slash alias. This is what lets an
527
+ * installed skill add real, dispatchable `/` commands (not just routing-hint metadata): only the
528
+ * FIRST token is matched, and the rest of the line becomes the intent passed to the skill. */
529
+ export function parseSkillSlashInvocation(input: string, skills: SkillDoc[]): SkillInvocation | null {
530
+ const trimmed = input.trim();
531
+ if (!trimmed.startsWith("/")) return null;
532
+ const command = trimmed.split(/\s+/, 1)[0] ?? "";
533
+ const skill = getSkillBySlash(skills, command);
534
+ if (!skill) return null;
535
+ return { skill, intent: trimmed.slice(command.length).trim(), invokedAs: command };
536
+ }
537
+
524
538
  export interface SkillInvocation {
525
539
  skill: SkillDoc;
526
540
  intent: string;
@@ -13,7 +13,7 @@
13
13
  * completer never blocks on the network. Static data (slash names, catalog ids,
14
14
  * provider names, role ids) is filled by `staticCompletionContext()`.
15
15
  */
16
- import { SLASH_COMMANDS } from "./slash";
16
+ import { SLASH_COMMANDS, SLASH_COMMAND_DESCRIPTIONS } from "./slash";
17
17
  import { catalogIds } from "../../ai/model-catalog-compat";
18
18
  import { PROVIDER_NAMES } from "../../ai/provider-status";
19
19
  import { SUBAGENT_ROLES } from "../../agent/subagents";
@@ -84,6 +84,82 @@ function prefixHits(pool: string[], token: string): string[] {
84
84
  return pool.filter(c => c.toLowerCase().startsWith(q));
85
85
  }
86
86
 
87
+ /**
88
+ * Fuzzy subsequence test (gjc parity): every char of `query` appears in
89
+ * `target` in order (not necessarily adjacent). Empty query matches everything.
90
+ */
91
+ export function fuzzyMatch(query: string, target: string): boolean {
92
+ if (!query) return true;
93
+ let qi = 0;
94
+ for (let ti = 0; ti < target.length && qi < query.length; ti++) {
95
+ if (target[ti] === query[qi]) qi++;
96
+ }
97
+ return qi === query.length;
98
+ }
99
+
100
+ /**
101
+ * Score a match (gjc parity): exact=100 > startsWith=80 > includes=60 >
102
+ * subsequence(40 − gaps×5, min 1). Returns 0 when `query` is not a subsequence.
103
+ */
104
+ export function fuzzyScore(query: string, target: string): number {
105
+ if (!query) return 80;
106
+ if (query === target) return 100;
107
+ if (target.startsWith(query)) return 80;
108
+ if (target.includes(query)) return 60;
109
+ let qi = 0;
110
+ let gaps = 0;
111
+ let lastMatch = -1;
112
+ for (let ti = 0; ti < target.length && qi < query.length; ti++) {
113
+ if (target[ti] === query[qi]) {
114
+ if (lastMatch !== -1 && ti !== lastMatch + 1) gaps++;
115
+ lastMatch = ti;
116
+ qi++;
117
+ }
118
+ }
119
+ if (qi < query.length) return 0;
120
+ return Math.max(1, 40 - gaps * 5);
121
+ }
122
+
123
+ /**
124
+ * Fuzzy command-name matches, ranked best-first (gjc §2.1): higher fuzzyScore
125
+ * first, ties broken by registration order so the list stays stable. Prefix
126
+ * matches always outrank looser subsequence hits, so `/mod`→`/model` is kept
127
+ * while `/mdl`→`/model` now also completes.
128
+ */
129
+ function fuzzyHits(pool: string[], token: string): string[] {
130
+ const q = token.toLowerCase();
131
+ return pool
132
+ .map((c, index) => ({ c, score: fuzzyScore(q, c.toLowerCase()), index }))
133
+ .filter(x => x.score > 0)
134
+ .sort((a, b) => b.score - a.score || a.index - b.index)
135
+ .map(x => x.c);
136
+ }
137
+
138
+ /**
139
+ * Command-name completion with a description fallback (gjc §2.1). Name matches
140
+ * win outright (so `/mod`→`/model` stays precise); only when NOTHING matches the
141
+ * name do we fall back to substring-or-better description hits, so intent-style
142
+ * queries with no literal name match still resolve — e.g. `/oauth`→`/login`,
143
+ * `/transcript`→`/dump`. The description fallback requires ≥2 query chars and a
144
+ * real substring (not a loose subsequence) to avoid flooding the dropdown.
145
+ */
146
+ function fuzzyCommandHits(pool: string[], token: string): string[] {
147
+ const nameHits = fuzzyHits(pool, token);
148
+ if (nameHits.length > 0) return nameHits;
149
+
150
+ const q = token.replace(/^\//, "").toLowerCase();
151
+ if (q.length < 2) return [];
152
+ return pool
153
+ .map((c, index) => {
154
+ const desc = SLASH_COMMAND_DESCRIPTIONS.get(c);
155
+ const hit = desc?.includes(q) ?? false;
156
+ return { c, score: hit ? fuzzyScore(q, desc!) * 0.5 : 0, index };
157
+ })
158
+ .filter(x => x.score > 0)
159
+ .sort((a, b) => b.score - a.score || a.index - b.index)
160
+ .map(x => x.c);
161
+ }
162
+
87
163
  /** De-duplicate (case-insensitive, first wins), preserving order, capped. */
88
164
  function dedupeCap(items: string[], cap = MAX_COMPLETIONS): string[] {
89
165
  const seen = new Set<string>();
@@ -129,7 +205,7 @@ export function complete(line: string, ctx: CompletionContext): CompletionResult
129
205
  // `/command` mention completion mid-line (the leading-token case is the
130
206
  // dedicated command branch below, which also completes arguments).
131
207
  if (token.startsWith("/")) {
132
- return { completions: dedupeCap(prefixHits(ctx.slashCommands, token)), token, kind: "command" };
208
+ return { completions: dedupeCap(fuzzyCommandHits(ctx.slashCommands, token)), token, kind: "command" };
133
209
  }
134
210
  return { completions: [], token: line, kind: "none" };
135
211
  }
@@ -137,7 +213,7 @@ export function complete(line: string, ctx: CompletionContext): CompletionResult
137
213
  // Completing the command name itself (single token, still typing it).
138
214
  if (tokens.length <= 1 && !trailingSpace) {
139
215
  const token = tokens[0] ?? "/";
140
- return { completions: dedupeCap(prefixHits(ctx.slashCommands, token)), token, kind: "command" };
216
+ return { completions: dedupeCap(fuzzyCommandHits(ctx.slashCommands, token)), token, kind: "command" };
141
217
  }
142
218
 
143
219
  const cmd = tokens[0]!.toLowerCase();
@@ -106,3 +106,43 @@ export function liveModelPicker(entries: PickEntry[], opts: LiveModelPickerOptio
106
106
  export function renderLiveModelPicker(list: SelectList<PickEntry>, opts: RenderSelectOptions = {}): string[] {
107
107
  return renderSelectList(list, { title: "Select a live model", rows: 12, ...opts });
108
108
  }
109
+
110
+ /** The five reasoning levels in display order, lightest → heaviest. */
111
+ export const THINKING_LEVEL_ORDER = ["minimal", "low", "medium", "high", "xhigh"] as const;
112
+
113
+ const THINKING_LEVEL_DESCRIPTION: Record<string, string> = {
114
+ minimal: "lightest reasoning",
115
+ low: "light reasoning",
116
+ medium: "moderate reasoning",
117
+ high: "deep reasoning",
118
+ xhigh: "maximum reasoning",
119
+ };
120
+
121
+ export interface ThinkingLevelChoiceOptions {
122
+ /** Prepend an "inherit" row (role targets only, which can follow the default). */
123
+ inheritLabel?: string;
124
+ /** Per-level reasoning-budget hint, e.g. "~32k tokens". Return undefined to omit. */
125
+ tokenHint?: (level: string) => string | undefined;
126
+ }
127
+
128
+ /**
129
+ * gajae-code-parity reasoning menu (gjc's `#renderThinkingMenu`): the per-target
130
+ * "Reasoning for …" choices shown after a model is picked. Each level renders as
131
+ * `<level> — <description> (<tokens>)` with the current level flagged. Role
132
+ * targets gain a leading `inherit` row. Pure — fully unit-testable.
133
+ */
134
+ export function buildThinkingLevelChoices(
135
+ current: string | undefined,
136
+ opts: ThinkingLevelChoiceOptions = {},
137
+ ): SelectItem<string>[] {
138
+ const items: SelectItem<string>[] = [];
139
+ if (opts.inheritLabel) {
140
+ items.push({ value: "inherit", label: opts.inheritLabel, hint: current === undefined ? "current" : "" });
141
+ }
142
+ for (const level of THINKING_LEVEL_ORDER) {
143
+ const tokens = opts.tokenHint?.(level);
144
+ const label = `${level} — ${THINKING_LEVEL_DESCRIPTION[level]}${tokens ? ` (${tokens})` : ""}`;
145
+ items.push({ value: level, label, hint: current === level ? "current" : "" });
146
+ }
147
+ return items;
148
+ }
@@ -65,6 +65,11 @@ export const SLASH_COMMAND_DETAILS: readonly SlashCommandInfo[] = [
65
65
 
66
66
  export const SLASH_COMMANDS = SLASH_COMMAND_DETAILS.map(c => c.command);
67
67
 
68
+ /** Bundled command name → lowercased description (gjc §2.1 description matching). */
69
+ export const SLASH_COMMAND_DESCRIPTIONS = new Map<string, string>(
70
+ SLASH_COMMAND_DETAILS.map(c => [c.command, c.description.toLowerCase()]),
71
+ );
72
+
68
73
  export function mergeSlashCommandDetails(extra: readonly SlashCommandInfo[] = []): SlashCommandInfo[] {
69
74
  const byCommand = new Map<string, SlashCommandInfo>();
70
75
  for (const d of [...SLASH_COMMAND_DETAILS, ...extra]) byCommand.set(d.command, d);
@@ -73,8 +78,10 @@ export function mergeSlashCommandDetails(extra: readonly SlashCommandInfo[] = []
73
78
 
74
79
  /** Return the slash commands that match `input` (case-insensitive). Prefix matches
75
80
  * come first; a fuzzy subsequence fallback (gjc-style, e.g. `/expt` → `/export`)
76
- * follows so typos / abbreviations still surface a command. A bare `/` lists every
77
- * command. Empty for non-slash input. */
81
+ * follows so typos / abbreviations still surface a command. When NOTHING matches
82
+ * the name, fall back to a description substring match (gjc §2.1) so intent-style
83
+ * queries with no literal name match still resolve — e.g. `/oauth` → `/login`,
84
+ * `/clipboard` → `/dump`. A bare `/` lists every command. Empty for non-slash input. */
78
85
  export function matchSlash(input: string, commands: string[] = SLASH_COMMANDS): string[] {
79
86
  if (!input.startsWith("/")) return [];
80
87
  const q = input.toLowerCase();
@@ -82,7 +89,12 @@ export function matchSlash(input: string, commands: string[] = SLASH_COMMANDS):
82
89
  if (body === "") return [...commands];
83
90
  const starts = commands.filter(c => c.toLowerCase().startsWith(q));
84
91
  const fuzzy = commands.filter(c => !starts.includes(c) && subsequence(body, c.slice(1).toLowerCase()));
85
- return [...starts, ...fuzzy];
92
+ const named = [...starts, ...fuzzy];
93
+ if (named.length > 0) return named;
94
+ // Description fallback: only when no name matches, and only for queries of ≥2
95
+ // chars with a real substring hit (not a loose subsequence), to avoid flooding.
96
+ if (body.length < 2) return [];
97
+ return commands.filter(c => SLASH_COMMAND_DESCRIPTIONS.get(c)?.includes(body) ?? false);
86
98
  }
87
99
 
88
100
  /** True when `input` looks like a slash command (starts with "/" and has no space). */