jeo-code 0.5.13 → 0.5.15

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.
@@ -1,7 +1,7 @@
1
1
  import { createInterface } from "node:readline/promises";
2
2
  import { emitKeypressEvents } from "node:readline";
3
3
  import { PassThrough } from "node:stream";
4
- import { runAgentLoop, executorSystemPrompt, DEFAULT_TOOLS, TOOL_PROTOCOL, WORKING_DISCIPLINE, OUTPUT_DISCIPLINE, type AgentLoopEvents } from "../agent/engine";
4
+ import { runAgentLoop, DEFAULT_TOOLS, TOOL_PROTOCOL, WORKING_DISCIPLINE, OUTPUT_DISCIPLINE, type AgentLoopEvents } from "../agent/engine";
5
5
  import { createOpikTracer, wrapEvents } from "../agent/opik-tracer";
6
6
  import { initialDynamicStepLimit } from "../agent/step-budget";
7
7
  import { memoryPromptSection, spawnDetachedDistill } from "../agent/memory";
@@ -10,11 +10,11 @@ import { createSubagentTool, SUBAGENT_TOOL_PROTOCOL_LINE } from "../agent/subage
10
10
  import { SubagentRegistry } from "../agent/subagent-registry";
11
11
  import { createTodoTool, TODO_TOOL_PROTOCOL_LINE } from "../agent/todo-tool";
12
12
  import { LaunchTui } from "../tui/app";
13
- import { runDeepInterviewEngine } from "./deep-interview";
14
- import { runRalplanEngine } from "./ralplan";
15
- import { runTeamEngine } from "./team";
16
- import { runUltragoalEngine } from "./ultragoal";
17
- import { skillsPromptSection, loadSkills, formatSkill, buildSkillTask, getSkillFrom, skillSlashAliases, workflowSkillsForPrompt, parseSkillInvocation, parseSkillChain, looksLikeSkillEcho, skillInvocationCard, type SkillDoc, type SkillInvocation } from "../skills/catalog";
13
+ import { runDeepInterviewEngine, type DeepInterviewEngineOptions } from "./deep-interview";
14
+ import { runRalplanEngine, type RalplanEngineOptions } from "./ralplan";
15
+ import { runTeamEngine, type TeamEngineOptions } from "./team";
16
+ import { runUltragoalEngine, type UltragoalEngineOptions } from "./ultragoal";
17
+ import { skillsPromptSection, loadSkills, buildSkillTask, workflowSkillsForPrompt, parseSkillInvocation, parseSkillChain, looksLikeSkillEcho, skillInvocationCard, type SkillDoc, type SkillInvocation } from "../skills/catalog";
18
18
  import { formatForgeBox } from "../tui/components/forge";
19
19
  import { interactiveOAuthLogin } from "./auth";
20
20
  import { logoutOAuth } from "../auth";
@@ -54,7 +54,7 @@ import {
54
54
  formatCapabilityLine,
55
55
  } from "../tui/components/config-panel";
56
56
  import { liveModelPicker, renderLiveModelPicker, type ModelAssignmentBadge } from "../tui/components/live-model-picker";
57
- import { skillPicker, renderSkillPicker } from "../tui/components/skill-picker";
57
+
58
58
  import { providerPicker, renderProviderPicker } from "../tui/components/provider-picker";
59
59
  import { detectLanguage, languageLabel, parseLineRange, sliceLines, formatCodeBlock, formatDiff, sanitizeForTerminal } from "../tui/components/code-view";
60
60
  import { categoryBadge } from "../tui/components/category-index";
@@ -509,6 +509,18 @@ export function isStandaloneBackspace(chunk: string): boolean {
509
509
  return chunk.length > 0 && /^[\x7f\b]+$/.test(chunk);
510
510
  }
511
511
 
512
+ /** gjc-parity slash-command aliases, applied once before dispatch so each real command
513
+ * keeps a SINGLE handler: `/login`→`/provider login`, `/settings`→`/config`,
514
+ * `/subagent(s)`→`/agents`. Pure rewrite that preserves any trailing args. */
515
+ export function normalizeSlashAlias(input: string): string {
516
+ if (input === "/login" || input.startsWith("/login ")) return `/provider login${input.slice("/login".length)}`;
517
+ if (input === "/settings") return "/config";
518
+ if (input === "/subagent" || input.startsWith("/subagent ")) return `/agents${input.slice("/subagent".length)}`;
519
+ if (input === "/subagents" || input.startsWith("/subagents ")) return `/agents${input.slice("/subagents".length)}`;
520
+ if (input === "/resume" || input.startsWith("/resume ")) return `/session resume${input.slice("/resume".length)}`;
521
+ return input;
522
+ }
523
+
512
524
  export interface PromptInputQueue {
513
525
  pendingLines: string[];
514
526
  partial: string;
@@ -1013,6 +1025,28 @@ function resolveWorktree(cwd: string, wt: string): string {
1013
1025
  return abs;
1014
1026
  }
1015
1027
 
1028
+ /** The bundled workflow skills that run through a dedicated engine (deep-interview /
1029
+ * ralplan / team / ultragoal), not the ordinary agent loop. Single source of truth —
1030
+ * the menu listing, the dispatch guards, and the engine switch all read from here. */
1031
+ export const WORKFLOW_NAMES = ["deep-interview", "ralplan", "team", "ultragoal"] as const;
1032
+
1033
+ /** True when a skill name is one of the bundled workflow engines. */
1034
+ export function isWorkflowSkill(name: string): boolean {
1035
+ return (WORKFLOW_NAMES as readonly string[]).includes(name);
1036
+ }
1037
+
1038
+ /** Dispatch a bundled workflow by name to its engine. Keeps the name→engine mapping in
1039
+ * ONE place so the one-shot and interactive skill runners can't drift apart. */
1040
+ export function runWorkflowEngine(
1041
+ name: string,
1042
+ opts: DeepInterviewEngineOptions & RalplanEngineOptions & TeamEngineOptions & UltragoalEngineOptions,
1043
+ ): Promise<{ ok: boolean; reason?: string }> {
1044
+ if (name === "deep-interview") return runDeepInterviewEngine(opts);
1045
+ if (name === "ralplan") return runRalplanEngine(opts);
1046
+ if (name === "team") return runTeamEngine(opts);
1047
+ return runUltragoalEngine(opts);
1048
+ }
1049
+
1016
1050
  export async function runLaunchCommand(args: string[]): Promise<void> {
1017
1051
  let cwd = process.cwd();
1018
1052
  const flags = parseFlags(args, cwd);
@@ -1192,28 +1226,10 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
1192
1226
 
1193
1227
  const workflowSkills = workflowSkillsForPrompt(resolvedSkills);
1194
1228
  const resolvedSkillNames = resolvedSkills.map(s => s.name);
1195
- // Bundled workflows are first-class `/name` commands (deep-interview/ralplan/team/
1196
- // ultragoal), surfaced in the `/` menu even when their SKILL.md self-references no
1197
- // slash token `parseSkillInvocation` dispatches `/name` by skill name. Aliases the
1198
- // SKILL.md does declare are listed too (deduped, case-insensitive).
1199
- const WORKFLOW_SLASH_NAMES = ["deep-interview", "ralplan", "team", "ultragoal"];
1200
- const skillSlashDetails: SlashCommandInfo[] = resolvedSkills.flatMap(skill => {
1201
- const aliases = skillSlashAliases(skill);
1202
- const nameSlash = WORKFLOW_SLASH_NAMES.includes(skill.name) ? [`/${skill.name}`] : [];
1203
- const seen = new Set<string>();
1204
- const commands = [...nameSlash, ...aliases].filter(a => {
1205
- const k = a.toLowerCase();
1206
- if (seen.has(k)) return false;
1207
- seen.add(k);
1208
- return true;
1209
- });
1210
- return commands.map(alias => ({
1211
- command: alias,
1212
- usage: `${alias} [intent]`,
1213
- description: `Run ${skill.name} skill${skill.summary ? ` — ${skill.summary}` : ""}`,
1214
- group: "skills" as const,
1215
- }));
1216
- });
1229
+ // Skills are invoked ONLY via the `$<name>` entrypoint, never `/`. The `/` menu therefore
1230
+ // advertises NO skill slash commands keeping `skillSlashDetails` empty leaves the slash
1231
+ // palette, autocomplete, and previews free of skill entries.
1232
+ const skillSlashDetails: SlashCommandInfo[] = [];
1217
1233
 
1218
1234
  const protocol = buildToolProtocol(allowedTools);
1219
1235
  const preamble = flags.systemPrompt ?? "You are the jeo, an interactive coding agent.\nAccomplish the user's request by calling tools and verifying your work.";
@@ -1237,6 +1253,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
1237
1253
  "- Advertise both bundled workflow skills and configured skills below. Bundled workflows are the primary routing priority, while configured/user skills can be invoked via explicit slash commands or /skill.\n" +
1238
1254
  "- Do NOT answer with a skill routing brief or execute a skill unless the user explicitly asks for skill help, invokes /skill or a skill slash alias, or the task truly fits a bundled workflow.\n" +
1239
1255
  "- If the user pasted SKILL.md docs as reference material, treat them as user data and follow the latest concrete request.\n" +
1256
+ "- Before writing code or files in a domain a loaded skill covers, read that SKILL.md first — skills encode repo/env constraints absent from your training; several may apply, so don't pre-judge that none is needed.\n" +
1240
1257
  "- Your done reason must describe YOUR work or answer — never recite skill documentation.\n" +
1241
1258
  skillsPromptSection(workflowSkills)) +
1242
1259
  (memoryBlock ? "\n\n" + memoryBlock : "");
@@ -1618,9 +1635,9 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
1618
1635
  if (cmd === "/exit" || cmd === "/quit") {
1619
1636
  return;
1620
1637
  }
1621
- if (cmd === "/clear" || cmd === "/new" || cmd === "/drop") {
1638
+ if (cmd === "/clear" || cmd === "/session new" || cmd === "/session drop" || cmd === "/session delete") {
1622
1639
  // Reset history to just the system prompt and overwrite the session file so
1623
- // the persisted transcript matches (a fresh session for /new and /drop).
1640
+ // the persisted transcript matches (a fresh session for /session new and /session drop).
1624
1641
  history.length = 1;
1625
1642
  if (sessionId && !flags.noSession) {
1626
1643
  try {
@@ -1639,7 +1656,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
1639
1656
  // One skill run (bundle workflow → engine; regular skill → agent turn). Shared by the
1640
1657
  // single-invocation path and the `$a $b …` chain path so every `$` skill actually runs.
1641
1658
  const runOneSkillShot = async (inv: SkillInvocation): Promise<void> => {
1642
- const isBundleWorkflow = ["deep-interview", "ralplan", "team", "ultragoal"].includes(inv.skill.name);
1659
+ const isBundleWorkflow = isWorkflowSkill(inv.skill.name);
1643
1660
  if (isBundleWorkflow) {
1644
1661
  const startMsg: Message = {
1645
1662
  role: "system",
@@ -1672,16 +1689,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
1672
1689
  let ok = false;
1673
1690
  let reason: string | undefined;
1674
1691
  try {
1675
- let res: { ok: boolean; reason?: string };
1676
- if (inv.skill.name === "deep-interview") {
1677
- res = await runDeepInterviewEngine(opts);
1678
- } else if (inv.skill.name === "ralplan") {
1679
- res = await runRalplanEngine(opts);
1680
- } else if (inv.skill.name === "team") {
1681
- res = await runTeamEngine(opts);
1682
- } else {
1683
- res = await runUltragoalEngine(opts);
1684
- }
1692
+ const res: { ok: boolean; reason?: string } = await runWorkflowEngine(inv.skill.name, opts);
1685
1693
  ok = res.ok;
1686
1694
  reason = res.reason;
1687
1695
  } catch (err: any) {
@@ -1821,7 +1829,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
1821
1829
  );
1822
1830
  logLines(card);
1823
1831
  }
1824
- const isBundleWorkflow = ["deep-interview", "ralplan", "team", "ultragoal"].includes(skill.name);
1832
+ const isBundleWorkflow = isWorkflowSkill(skill.name);
1825
1833
  if (isBundleWorkflow) {
1826
1834
  const startMsg: Message = {
1827
1835
  role: "system",
@@ -1871,16 +1879,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
1871
1879
  let ok = false;
1872
1880
  let reason: string | undefined;
1873
1881
  try {
1874
- let res: { ok: boolean; reason?: string };
1875
- if (skill.name === "deep-interview") {
1876
- res = await runDeepInterviewEngine(opts);
1877
- } else if (skill.name === "ralplan") {
1878
- res = await runRalplanEngine(opts);
1879
- } else if (skill.name === "team") {
1880
- res = await runTeamEngine(opts);
1881
- } else {
1882
- res = await runUltragoalEngine(opts);
1883
- }
1882
+ const res: { ok: boolean; reason?: string } = await runWorkflowEngine(skill.name, opts);
1884
1883
  ok = res.ok;
1885
1884
  reason = res.reason;
1886
1885
  } catch (err: any) {
@@ -2830,54 +2829,6 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
2830
2829
  return true;
2831
2830
  };
2832
2831
 
2833
- const pickSkillFromList = async (skills: SkillDoc[]): Promise<SkillDoc | undefined> => {
2834
- if (!process.stdin.isTTY || skills.length === 0) return undefined;
2835
- const list = skillPicker(skills);
2836
- let chosen: SkillDoc | undefined;
2837
- await runSelectPicker(
2838
- (cols, rows) =>
2839
- renderSkillPicker(list, {
2840
- cols,
2841
- rows: Math.max(4, Math.min(rows, 12)),
2842
- unicode: true,
2843
- color: true,
2844
- }),
2845
- (ch, key) => {
2846
- if (key?.name === "up") {
2847
- list.up();
2848
- return false;
2849
- }
2850
- if (key?.name === "down") {
2851
- list.down();
2852
- return false;
2853
- }
2854
- if (key?.name === "pageup") {
2855
- list.page(-1, 6);
2856
- return false;
2857
- }
2858
- if (key?.name === "pagedown") {
2859
- list.page(1, 6);
2860
- return false;
2861
- }
2862
- if (key?.name === "backspace") {
2863
- list.backspace();
2864
- return false;
2865
- }
2866
- if (key?.name === "escape" || (key?.ctrl && key.name === "c")) {
2867
- return true;
2868
- }
2869
- if (key?.name === "return" || key?.name === "enter") {
2870
- chosen = list.selected()?.value;
2871
- return true;
2872
- }
2873
- if (ch && ch >= " " && !key?.ctrl && !key?.meta) {
2874
- list.typeChar(ch);
2875
- }
2876
- return false;
2877
- },
2878
- );
2879
- return chosen;
2880
- };
2881
2832
 
2882
2833
  const pickCloudProvider = async (statuses: Awaited<ReturnType<typeof describeAllProviders>>): Promise<AuthProvider | undefined> => {
2883
2834
  const cloud = new Set(["anthropic", "openai", "gemini", "antigravity"]);
@@ -3128,13 +3079,9 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
3128
3079
  let input = pendingSelection && trigger && pendingSelection.startsWith(trigger.token)
3129
3080
  ? raw.slice(0, trigger.start) + pendingSelection
3130
3081
  : raw;
3131
- // gjc-parity command aliases (full behavior reuse, no duplicated handlers).
3132
- if (input === "/login" || input.startsWith("/login ")) input = `/provider login${input.slice("/login".length)}`;
3133
- else if (input === "/settings") input = "/config";
3134
- // `/subagent`(s) → the /agents roster/editor (view + change the current
3135
- // subagent composition: per-role model · thinking · steps).
3136
- else if (input === "/subagent" || input.startsWith("/subagent ")) input = `/agents${input.slice("/subagent".length)}`;
3137
- else if (input === "/subagents" || input.startsWith("/subagents ")) input = `/agents${input.slice("/subagents".length)}`;
3082
+ // gjc-parity command aliases (full behavior reuse, no duplicated handlers):
3083
+ // /login→/provider login, /settings→/config, /subagent(s)→/agents.
3084
+ input = normalizeSlashAlias(input);
3138
3085
  pendingSelection = undefined;
3139
3086
  navMatches = [];
3140
3087
  navIdx = -1;
@@ -3199,156 +3146,169 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
3199
3146
  }
3200
3147
  continue;
3201
3148
  }
3202
- if (input === "/sessions") {
3203
- const sessions = await listSessions(cwd);
3204
- if (sessions.length === 0) console.log("(no saved sessions)");
3205
- for (const s of sessions) {
3206
- const marker = s.id === sessionId ? "*" : " ";
3207
- const title = s.title ? `[${s.title}] ` : "";
3208
- console.log(` ${marker}${s.id} (${s.messageCount} msgs) ${title}${s.preview}`);
3209
- }
3210
- continue;
3211
- }
3212
- // ---- gjc-parity session management ------------------------------------
3213
- const startFreshSession = async (verb: string): Promise<void> => {
3214
- history.length = 1;
3215
- if (!flags.noSession) {
3216
- sessionId = (await createSession(cwd)).id;
3217
- advanceSessionBoxColor(); // distinct input-box hue per newly opened session
3218
- console.log(`(${verb} — new session ${sessionId})`);
3219
- } else {
3220
- sessionId = undefined;
3221
- console.log(`(${verb} — sessions disabled)`);
3222
- }
3223
- };
3224
- if (input === "/new") {
3225
- await startFreshSession("started fresh");
3226
- continue;
3227
- }
3228
- if (input === "/drop") {
3229
- if (sessionId) {
3230
- const removed = await deleteSession(sessionId, cwd);
3231
- console.log(removed ? `(deleted session ${sessionId})` : `(session ${sessionId} already gone)`);
3232
- }
3233
- await startFreshSession("dropped");
3234
- continue;
3235
- }
3236
3149
  if (input === "/session" || input.startsWith("/session ")) {
3237
- const sub = input.substring(8).trim().toLowerCase();
3238
- if (sub === "delete") {
3239
- if (!sessionId) {
3240
- console.log("(sessions are disabled nothing to delete)");
3241
- continue;
3150
+ const tokens = input.substring(8).trim().split(/\s+/).filter(Boolean);
3151
+ const sub = (tokens[0] ?? "").toLowerCase();
3152
+
3153
+ const startFreshSession = async (verb: string): Promise<void> => {
3154
+ history.length = 1;
3155
+ if (!flags.noSession) {
3156
+ sessionId = (await createSession(cwd)).id;
3157
+ advanceSessionBoxColor(); // distinct input-box hue per newly opened session
3158
+ console.log(`(${verb} — new session ${sessionId})`);
3159
+ } else {
3160
+ sessionId = undefined;
3161
+ console.log(`(${verb} — sessions disabled)`);
3242
3162
  }
3243
- const removed = await deleteSession(sessionId, cwd);
3244
- console.log(removed ? `(deleted session ${sessionId})` : `(session ${sessionId} already gone)`);
3245
- await startFreshSession("dropped");
3163
+ };
3164
+
3165
+ if (sub === "new") {
3166
+ await startFreshSession("started fresh");
3246
3167
  continue;
3247
3168
  }
3248
- if (sub && sub !== "info") {
3249
- console.log("Usage: /session [info|delete]");
3169
+ if (sub === "drop" || sub === "delete") {
3170
+ if (sessionId) {
3171
+ const removed = await deleteSession(sessionId, cwd);
3172
+ console.log(removed ? `(deleted session ${sessionId})` : `(session ${sessionId} already gone)`);
3173
+ }
3174
+ await startFreshSession("dropped");
3250
3175
  continue;
3251
3176
  }
3252
- if (!sessionId) {
3253
- console.log("Session: disabled (--no-session)");
3177
+ if (sub === "rename") {
3178
+ const title = tokens.slice(1).join(" ").trim();
3179
+ if (!title) {
3180
+ console.log("Usage: /session rename <title>");
3181
+ continue;
3182
+ }
3183
+ if (!sessionId) {
3184
+ console.log("(sessions are disabled — nothing to rename)");
3185
+ continue;
3186
+ }
3187
+ try {
3188
+ await renameSession(sessionId, title, cwd);
3189
+ console.log(`(session renamed to '${title}')`);
3190
+ } catch (err) {
3191
+ console.log(`! rename failed: ${(err as Error).message}`);
3192
+ }
3254
3193
  continue;
3255
3194
  }
3256
- const all = await listSessions(cwd);
3257
- const current = all.find(s => s.id === sessionId);
3258
- console.log("Session info:");
3259
- console.log(` id ${sessionId}`);
3260
- if (current?.title) console.log(` title ${current.title}`);
3261
- console.log(` file ${sessionPath(sessionId, cwd)}`);
3262
- console.log(` started ${current?.timestamp ?? "(this run)"}`);
3263
- console.log(` messages ${current?.messageCount ?? Math.max(0, history.length - 1)} persisted · ${history.length - 1} in context`);
3264
- console.log(` workspace ${cwd}`);
3265
- continue;
3266
- }
3267
- if (input === "/rename" || input.startsWith("/rename ")) {
3268
- const title = input.substring(7).trim();
3269
- if (!title) {
3270
- console.log("Usage: /rename <title>");
3195
+ if (sub === "resume") {
3196
+ const arg = tokens.slice(1).join(" ").trim();
3197
+ const applyResume = async (rid: string): Promise<void> => {
3198
+ try {
3199
+ const { messages } = await loadSession(rid, cwd);
3200
+ history.length = 1;
3201
+ for (const m of messages) history.push(m);
3202
+ sessionId = rid;
3203
+ // Seed /retry + reply marker from the last user/assistant turn.
3204
+ lastUserInput = ""; lastReply = "";
3205
+ for (let k = history.length - 1; k >= 1; k--) {
3206
+ if (history[k]!.role === "user" && !lastUserInput) lastUserInput = String(history[k]!.content ?? "");
3207
+ if (history[k]!.role === "assistant" && !lastReply) lastReply = String(history[k]!.content ?? "");
3208
+ if (lastUserInput && lastReply) break;
3209
+ }
3210
+ // Seed readline's input history so ↑ in the prompt recalls THIS session's
3211
+ // prior prompts (not just lines typed in the current run). readline history
3212
+ // is newest-first; unshift in chronological order so the session's newest
3213
+ // prompt lands at the front (first ↑). Skip injected/framed messages.
3214
+ const rli = rl as unknown as { history?: string[] };
3215
+ if (Array.isArray(rli.history)) {
3216
+ const priorPrompts = history
3217
+ .filter(m => m.role === "user")
3218
+ .map(m => String(m.content ?? "").trim())
3219
+ .filter(c => c && !c.startsWith("Tool [") && !c.startsWith("[mid-turn steering") && !c.startsWith("[Earlier conversation summary]"));
3220
+ for (const p of priorPrompts) {
3221
+ if (rli.history[0] !== p) rli.history.unshift(p);
3222
+ }
3223
+ }
3224
+ const sep = "─".repeat(Math.min(48, Math.max(20, (process.stdout.columns ?? 80) - 1)));
3225
+ logLines([
3226
+ sep,
3227
+ `resumed session ${rid} · ${messages.length} message(s) (/history all for the full transcript)`,
3228
+ sep,
3229
+ ...formatTranscript(history, { maxTurns: 6, color: true, unicode: true }),
3230
+ sep,
3231
+ ]);
3232
+ } catch (err) {
3233
+ console.log(`! ${(err as Error).message}`);
3234
+ }
3235
+ };
3236
+ if (arg) { await applyResume(arg); continue; }
3237
+ // No id → only sessions with a real conversation are resumable (every launch
3238
+ // creates an empty session; those are noise).
3239
+ const sessions = (await listSessions(cwd)).filter(s => s.messageCount > 0);
3240
+ if (sessions.length === 0) {
3241
+ console.log("(no saved sessions with history)");
3242
+ continue;
3243
+ }
3244
+ // Interactive arrow-key picker on a TTY: ↑↓ to move, Enter to resume, Esc cancels.
3245
+ if (process.stdin.isTTY && process.stdout.isTTY) {
3246
+ const items: SelectItem<string>[] = sessions.slice(0, 50).map(s => ({
3247
+ value: s.id,
3248
+ label: `${s.title ? `[${s.title}] ` : ""}${(s.preview || s.id).replace(/\s+/g, " ")}`.slice(0, 76) || s.id,
3249
+ hint: `${s.messageCount} msgs${s.id === sessionId ? " · current" : ""}`,
3250
+ }));
3251
+ const picked = await pickFromOptions("Resume a session ↑↓ move · Enter resume · Esc cancel", items);
3252
+ if (picked) await applyResume(picked);
3253
+ else console.log("(resume cancelled)");
3254
+ continue;
3255
+ }
3256
+ // Non-TTY fallback: static list (resume with /session resume <id>).
3257
+ console.log("Saved sessions — resume with /session resume <id>:");
3258
+ for (const s of sessions.slice(0, 15)) {
3259
+ const marker = s.id === sessionId ? "*" : " ";
3260
+ console.log(` ${marker}${s.id} (${s.messageCount} msgs) ${s.title ? `[${s.title}] ` : ""}${s.preview}`);
3261
+ }
3271
3262
  continue;
3272
3263
  }
3273
- if (!sessionId) {
3274
- console.log("(sessions are disabled — nothing to rename)");
3264
+ if (sub === "list") {
3265
+ const sessions = await listSessions(cwd);
3266
+ if (sessions.length === 0) console.log("(no saved sessions)");
3267
+ for (const s of sessions) {
3268
+ const marker = s.id === sessionId ? "*" : " ";
3269
+ const title = s.title ? `[${s.title}] ` : "";
3270
+ console.log(` ${marker}${s.id} (${s.messageCount} msgs) ${title}${s.preview}`);
3271
+ }
3275
3272
  continue;
3276
3273
  }
3277
- try {
3278
- await renameSession(sessionId, title, cwd);
3279
- console.log(`(session renamed to '${title}')`);
3280
- } catch (err) {
3281
- console.log(`! rename failed: ${(err as Error).message}`);
3282
- }
3283
- continue;
3284
- }
3285
- if (input === "/resume" || input.startsWith("/resume ")) {
3286
- const arg = input.substring(7).trim();
3287
- // Load a session into history and print its transcript so the resume is visible.
3288
- const applyResume = async (rid: string): Promise<void> => {
3289
- try {
3290
- const { messages } = await loadSession(rid, cwd);
3291
- history.length = 1;
3292
- for (const m of messages) history.push(m);
3293
- sessionId = rid;
3294
- // Seed /retry + reply marker from the last user/assistant turn.
3295
- lastUserInput = ""; lastReply = "";
3296
- for (let k = history.length - 1; k >= 1; k--) {
3297
- if (history[k]!.role === "user" && !lastUserInput) lastUserInput = String(history[k]!.content ?? "");
3298
- if (history[k]!.role === "assistant" && !lastReply) lastReply = String(history[k]!.content ?? "");
3299
- if (lastUserInput && lastReply) break;
3300
- }
3301
- // Seed readline's input history so ↑ in the prompt recalls THIS session's
3302
- // prior prompts (not just lines typed in the current run). readline history
3303
- // is newest-first; unshift in chronological order so the session's newest
3304
- // prompt lands at the front (first ↑). Skip injected/framed messages.
3305
- const rli = rl as unknown as { history?: string[] };
3306
- if (Array.isArray(rli.history)) {
3307
- const priorPrompts = history
3308
- .filter(m => m.role === "user")
3309
- .map(m => String(m.content ?? "").trim())
3310
- .filter(c => c && !c.startsWith("Tool [") && !c.startsWith("[mid-turn steering") && !c.startsWith("[Earlier conversation summary]"));
3311
- for (const p of priorPrompts) {
3312
- if (rli.history[0] !== p) rli.history.unshift(p);
3313
- }
3314
- }
3315
- const sep = "─".repeat(Math.min(48, Math.max(20, (process.stdout.columns ?? 80) - 1)));
3316
- logLines([
3317
- sep,
3318
- `resumed session ${rid} · ${messages.length} message(s) (/history all for the full transcript)`,
3319
- sep,
3320
- ...formatTranscript(history, { maxTurns: 6, color: true, unicode: true }),
3321
- sep,
3322
- ]);
3323
- } catch (err) {
3324
- console.log(`! ${(err as Error).message}`);
3274
+ if (sub === "info") {
3275
+ if (!sessionId) {
3276
+ console.log("Session: disabled (--no-session)");
3277
+ continue;
3325
3278
  }
3326
- };
3327
- if (arg) { await applyResume(arg); continue; }
3328
- // No id → only sessions with a real conversation are resumable (every launch
3329
- // creates an empty session; those are noise).
3330
- const sessions = (await listSessions(cwd)).filter(s => s.messageCount > 0);
3331
- if (sessions.length === 0) {
3332
- console.log("(no saved sessions with history)");
3279
+ const all = await listSessions(cwd);
3280
+ const current = all.find(s => s.id === sessionId);
3281
+ console.log("Session info:");
3282
+ console.log(` id ${sessionId}`);
3283
+ if (current?.title) console.log(` title ${current.title}`);
3284
+ console.log(` file ${sessionPath(sessionId, cwd)}`);
3285
+ console.log(` started ${current?.timestamp ?? "(this run)"}`);
3286
+ console.log(` messages ${current?.messageCount ?? Math.max(0, history.length - 1)} persisted · ${history.length - 1} in context`);
3287
+ console.log(` workspace ${cwd}`);
3333
3288
  continue;
3334
3289
  }
3335
- // Interactive arrow-key picker on a TTY: ↑↓ to move, Enter to resume, Esc cancels.
3336
- if (process.stdin.isTTY && process.stdout.isTTY) {
3337
- const items: SelectItem<string>[] = sessions.slice(0, 50).map(s => ({
3338
- value: s.id,
3339
- label: `${s.title ? `[${s.title}] ` : ""}${(s.preview || s.id).replace(/\s+/g, " ")}`.slice(0, 76) || s.id,
3340
- hint: `${s.messageCount} msgs${s.id === sessionId ? " · current" : ""}`,
3341
- }));
3342
- const picked = await pickFromOptions("Resume a session ↑↓ move · Enter resume · Esc cancel", items);
3343
- if (picked) await applyResume(picked);
3344
- else console.log("(resume cancelled)");
3290
+ if (sub && sub !== "info") {
3291
+ console.log("Usage: /session [list|info|new|drop|rename <title>|resume [id]]");
3345
3292
  continue;
3346
3293
  }
3347
- // Non-TTY fallback: static list (resume with /resume <id>).
3348
- console.log("Saved sessions resume with /resume <id>:");
3349
- for (const s of sessions.slice(0, 15)) {
3350
- const marker = s.id === sessionId ? "*" : " ";
3351
- console.log(` ${marker}${s.id} (${s.messageCount} msgs) ${s.title ? `[${s.title}] ` : ""}${s.preview}`);
3294
+
3295
+ // Default: list sessions AND show current session info
3296
+ const sessions = await listSessions(cwd);
3297
+ if (sessions.length === 0) {
3298
+ console.log("(no saved sessions)");
3299
+ } else {
3300
+ console.log("Saved sessions:");
3301
+ for (const s of sessions) {
3302
+ const marker = s.id === sessionId ? "*" : " ";
3303
+ const title = s.title ? `[${s.title}] ` : "";
3304
+ console.log(` ${marker}${s.id} (${s.messageCount} msgs) ${title}${s.preview}`);
3305
+ }
3306
+ }
3307
+ if (sessionId) {
3308
+ const current = sessions.find(s => s.id === sessionId);
3309
+ console.log(`\nCurrent session: ${sessionId}${current?.title ? ` [${current.title}]` : ""}`);
3310
+ } else {
3311
+ console.log("\nCurrent session: disabled (--no-session)");
3352
3312
  }
3353
3313
  continue;
3354
3314
  }
@@ -4249,46 +4209,23 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
4249
4209
  console.log(res.success ? (res.output || "(no matches)") : `! ${res.error}`);
4250
4210
  continue;
4251
4211
  }
4212
+
4252
4213
  const skillEntrypoint = input.startsWith("/skill:") ? "/skill:" : input.startsWith("/skill") && (input === "/skill" || input[6] === " ") ? "/skill" : "";
4253
4214
  if (skillEntrypoint) {
4254
4215
  if (flags.noSkills) {
4255
4216
  console.log("Skills are disabled.");
4256
4217
  continue;
4257
4218
  }
4258
- const rest = skillEntrypoint === "/skill:" ? input.substring(7).trim() : input.substring(6).trim();
4219
+ // `/` never LOADS or RUNS a skill file skills are invoked ONLY via `$<name>`.
4220
+ // `/skill[:...]` is a read-only listing that points the user at the `$` entrypoint.
4259
4221
  let skills = await loadSkills(cwd);
4260
4222
  if (flags.skills) {
4261
4223
  const patterns = flags.skills.split(",").map(p => p.trim()).filter(Boolean);
4262
4224
  skills = skills.filter(s => patterns.some(p => matchSkillGlob(p, s.name)));
4263
4225
  }
4264
- if (!rest) {
4265
- if (process.stdin.isTTY && process.stdout.isTTY) {
4266
- const picked = await pickSkillFromList(skills);
4267
- if (!picked) {
4268
- console.log("(cancelled)");
4269
- continue;
4270
- }
4271
- await runSkillInvocation(picked, "");
4272
- continue;
4273
- }
4274
- console.log("Skills (bundled + configured docs) — run with /skill <name> [intent] or a skill slash alias:");
4275
- for (const s of skills) {
4276
- const aliases = skillSlashAliases(s);
4277
- console.log(` ${s.name.padEnd(16)} ${s.summary}${aliases.length ? ` (${aliases.join(", ")})` : ""}`);
4278
- }
4279
- continue;
4280
- }
4281
- const [nm, ...intentParts] = rest.split(/\s+/);
4282
- const skill = getSkillFrom(skills, nm);
4283
- if (!skill) {
4284
- console.log(`Unknown skill: ${nm}. Available: ${skills.map(s => s.name).join(", ")}`);
4285
- continue;
4286
- }
4287
- const intent = intentParts.join(" ").trim();
4288
- try {
4289
- await runSkillInvocation(skill, intent);
4290
- } catch (err) {
4291
- console.log(`! ${(err as Error).message}`);
4226
+ console.log("Skills are invoked with $<name> [intent] (e.g. $team build auth). Available:");
4227
+ for (const s of skills) {
4228
+ console.log(` $${s.name.padEnd(16)} ${s.summary}`);
4292
4229
  }
4293
4230
  continue;
4294
4231
  }