@cmetech/otto 1.0.9 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/dist/resources/.managed-resources-content-hash +1 -1
  2. package/dist/resources/extensions/otto/commands/release-notes/_data.js +16 -0
  3. package/dist/resources/extensions/otto/commands/theme/command.js +75 -0
  4. package/dist/resources/extensions/otto/index.js +3 -0
  5. package/dist/resources/extensions/subagent/agents.js +160 -8
  6. package/dist/resources/extensions/subagent/index.js +45 -4
  7. package/dist/resources/extensions/subagent/skill-tool-stub.js +23 -0
  8. package/dist/seed-defaults.d.ts +16 -0
  9. package/dist/seed-defaults.js +69 -1
  10. package/package.json +6 -6
  11. package/packages/contracts/package.json +1 -1
  12. package/packages/daemon/package.json +3 -3
  13. package/packages/mcp-server/package.json +3 -3
  14. package/packages/native/package.json +1 -1
  15. package/packages/pi-agent-core/package.json +1 -1
  16. package/packages/pi-ai/package.json +1 -1
  17. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +3 -0
  18. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  19. package/packages/pi-coding-agent/dist/core/settings-manager.js +6 -0
  20. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  21. package/packages/pi-coding-agent/dist/core/skills.d.ts +11 -0
  22. package/packages/pi-coding-agent/dist/core/skills.d.ts.map +1 -1
  23. package/packages/pi-coding-agent/dist/core/skills.js +22 -0
  24. package/packages/pi-coding-agent/dist/core/skills.js.map +1 -1
  25. package/packages/pi-coding-agent/dist/index.d.ts +1 -1
  26. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  27. package/packages/pi-coding-agent/dist/index.js +1 -1
  28. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  29. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  30. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +13 -2
  31. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  32. package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  33. package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.js +4 -0
  34. package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.js.map +1 -1
  35. package/packages/pi-coding-agent/package.json +2 -2
  36. package/packages/pi-coding-agent/src/core/settings-manager.ts +9 -0
  37. package/packages/pi-coding-agent/src/core/skills.ts +23 -1
  38. package/packages/pi-coding-agent/src/index.ts +4 -0
  39. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +13 -2
  40. package/packages/pi-coding-agent/src/modes/interactive/theme/theme.ts +4 -0
  41. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  42. package/packages/pi-tui/dist/autocomplete.d.ts +9 -0
  43. package/packages/pi-tui/dist/autocomplete.d.ts.map +1 -1
  44. package/packages/pi-tui/dist/autocomplete.js +2 -0
  45. package/packages/pi-tui/dist/autocomplete.js.map +1 -1
  46. package/packages/pi-tui/dist/components/select-list.d.ts +10 -0
  47. package/packages/pi-tui/dist/components/select-list.d.ts.map +1 -1
  48. package/packages/pi-tui/dist/components/select-list.js +30 -17
  49. package/packages/pi-tui/dist/components/select-list.js.map +1 -1
  50. package/packages/pi-tui/package.json +1 -1
  51. package/packages/pi-tui/src/autocomplete.ts +11 -0
  52. package/packages/pi-tui/src/components/select-list.ts +41 -17
  53. package/packages/pi-tui/tsconfig.tsbuildinfo +1 -1
  54. package/packages/rpc-client/package.json +2 -2
  55. package/pkg/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  56. package/pkg/dist/modes/interactive/theme/theme.js +4 -0
  57. package/pkg/dist/modes/interactive/theme/theme.js.map +1 -1
  58. package/pkg/package.json +1 -1
  59. package/src/resources/extensions/otto/commands/release-notes/_data.ts +16 -0
  60. package/src/resources/extensions/otto/commands/theme/command.ts +89 -0
  61. package/src/resources/extensions/otto/index.ts +4 -0
  62. package/src/resources/extensions/subagent/agents.ts +166 -8
  63. package/src/resources/extensions/subagent/index.ts +46 -6
  64. package/src/resources/extensions/subagent/skill-tool-stub.ts +28 -0
  65. package/src/resources/extensions/subagent/tests/parse-agent-tools.test.ts +52 -0
  66. package/src/resources/extensions/subagent/tests/skill-tool-stub.test.ts +23 -0
@@ -1 +1 @@
1
- 0d13c40fc565ba9c
1
+ 9ee03dda8858f377
@@ -4,6 +4,22 @@
4
4
  // To add or correct release notes, edit CHANGELOG.md and rebuild. Editing this
5
5
  // file directly will be clobbered on the next build.
6
6
  export const RELEASE_NOTES = [
7
+ {
8
+ version: '1.1.0',
9
+ date: '2026-05-29',
10
+ headline: 'Harness compatibility (Claude / Codex / Kiro skills + agents), [claude] origin chips, and a `/theme` slash command.',
11
+ added: [
12
+ 'Harness skill paths (`~/.claude/skills`, `~/.codex/skills`, `~/.kiro/skills`) are now auto-seeded into `settings.skills` on launch — but only if the directory actually exists on disk. Skills loaded from those paths follow pi.dev\'s documented convention. A new `settings.seededSkillPaths` zombie-guard records every path attempted, so removing an entry from `settings.skills` keeps it removed across launches.',
13
+ 'Skill origin is now visible in the slash-command autocomplete. Skills loaded from a known harness folder (`~/.claude/skills`, `~/.codex/skills`, `~/.kiro/skills`) carry `source: "harness:<id>"`. The dropdown shows a colored chip — e.g. `[claude] skill:review-pr Review a pull request (claude)` — so the origin is glanceable. Implementation: new `HARNESS_SOURCE_PATHS` in `skills.ts`, new optional `tag` field on `AutocompleteItem` + `SelectItem`, new `SelectListTheme.tag` color hook.',
14
+ 'Harness agent discovery. OTTO\'s `subagent` tool now also resolves agent names against `~/.claude/agents/`, `~/.codex/agents/`, `~/.kiro/agents/` (user scope) and the nearest `.claude/agents/`, `.codex/agents/`, `.kiro/agents/` (project scope). A Claude skill that delegates to a companion agent now finds it without manual setup. Collision rule: OTTO\'s own agents win at each scope; project always wins over user.',
15
+ 'Tool-name normalization for harness-imported agents. Capitalized Claude/Codex/Kiro tool names in an agent\'s `tools:` frontmatter are rewritten to OTTO\'s lowercase registry names at load time — `Bash` → `bash`, `Read` → `read`, `AskUserQuestion` → `ask_user_questions`, `Task`/`Agent` → `subagent`, `WebSearch` → `web_search`, `WebFetch` → `fetch_page`, `Skill` → the stub below. Unknown names flow through as lowercase and the runtime allowlist silently drops them, so a single unknown entry no longer blocks the rest of the agent\'s toolset. MCP names (`mcp__server__*`) are preserved verbatim.',
16
+ 'Stub `skill` tool for imported Claude skills that call `Skill(name=...)`. OTTO doesn\'t support model-invoked skill execution; the stub returns a friendly message redirecting the model to either ask the user to run `/skill:<name>` from chat input or to act on the skill content inline.',
17
+ '`/subagent` listing now renders each row with embedded ANSI styling: white agent name, dim source/model metadata, dim description, and an accent `[claude]/[codex]/[kiro]` chip when the agent was discovered under a harness path. Matches the `/skills` autocomplete chip style. `AgentConfig` gains an optional `harnessSource` field.',
18
+ 'New `/theme` slash command — `/theme` opens an interactive picker over built-in themes (`otto`, `dark`, `light`, `tui-classic`, `vivid`) plus any `*.json` you drop in `~/.otto/agent/themes/`. `/theme <name>` switches directly. `/theme list` prints a non-interactive index. Switch is session-only; set `"theme": "<name>"` in `~/.otto/agent/settings.json` to persist.',
19
+ 'New `docs/HARNESS-COMPAT.md` — user-facing matrix of what\'s automatically translated when importing skills/agents from Claude/Codex/Kiro, what doesn\'t translate, and how to test.',
20
+ 'New `docs/UPSTREAM-SYNC.md` living ledger — fork baseline (gsd-pi @ 1.0.1, import commit `bb6da93`), per-package divergence status, file-level patch log, and the cherry-pick workflow for evaluating future upstream changes. Update this file in the same commit as any new vendored-package edit.',
21
+ ],
22
+ },
7
23
  {
8
24
  version: '1.0.9',
9
25
  date: '2026-05-29',
@@ -0,0 +1,75 @@
1
+ /**
2
+ * /theme — list and switch the active OTTO theme at runtime.
3
+ *
4
+ * Usage:
5
+ * /theme → interactive picker over built-ins + ~/.otto/agent/themes/*.json
6
+ * /theme <name> → switch directly (e.g. /theme cool-mint)
7
+ * /theme list → print a one-line list of available themes
8
+ *
9
+ * Switch is for the current session. To persist across launches, also set
10
+ * `"theme": "<name>"` in ~/.otto/agent/settings.json.
11
+ */
12
+ import { getAvailableThemesWithPaths, setTheme } from "@otto/pi-coding-agent";
13
+ const CUSTOM_TYPE = "otto-theme";
14
+ function postToChat(pi, content) {
15
+ pi.sendMessage({ customType: CUSTOM_TYPE, content, display: true });
16
+ }
17
+ function renderList() {
18
+ const themes = getAvailableThemesWithPaths();
19
+ const lines = themes.map((t) => {
20
+ const origin = t.path ? ` _(${t.path})_` : " _(built-in)_";
21
+ return `- \`${t.name}\`${origin}`;
22
+ });
23
+ return `**Available themes (${themes.length})**\n\n${lines.join("\n")}\n\nUse \`/theme <name>\` to switch. Persist with \`"theme": "<name>"\` in \`~/.otto/agent/settings.json\`.`;
24
+ }
25
+ function applyTheme(pi, name) {
26
+ const result = setTheme(name);
27
+ if (result.success) {
28
+ postToChat(pi, `**Theme switched to \`${name}\`** for this session.\n\nTo persist, set \`"theme": "${name}"\` in \`~/.otto/agent/settings.json\`.`);
29
+ }
30
+ else {
31
+ const available = getAvailableThemesWithPaths().map((t) => t.name).join(", ");
32
+ postToChat(pi, `**Theme switch failed:** ${result.error ?? "unknown error"}\n\nAvailable: ${available}`);
33
+ }
34
+ }
35
+ export function registerThemeCommand(pi) {
36
+ pi.registerCommand("theme", {
37
+ description: "List or switch the active OTTO theme",
38
+ handler: async (args, ctx) => {
39
+ const trimmed = args.trim();
40
+ if (trimmed === "list") {
41
+ postToChat(pi, renderList());
42
+ return;
43
+ }
44
+ if (trimmed) {
45
+ applyTheme(pi, trimmed);
46
+ return;
47
+ }
48
+ // Interactive picker
49
+ if (!ctx.hasUI || typeof ctx.ui?.select !== "function") {
50
+ postToChat(pi, renderList());
51
+ return;
52
+ }
53
+ const themes = getAvailableThemesWithPaths();
54
+ const options = themes.map((t) => (t.path ? `${t.name} (custom)` : `${t.name} (built-in)`));
55
+ let pick;
56
+ try {
57
+ pick = await ctx.ui.select(`Choose theme — ${themes.length} available`, options);
58
+ }
59
+ catch (err) {
60
+ postToChat(pi, `**theme picker error:** ${err.message}`);
61
+ return;
62
+ }
63
+ if (!pick)
64
+ return;
65
+ const picked = Array.isArray(pick) ? pick[0] : pick;
66
+ const index = options.indexOf(picked);
67
+ const chosen = index >= 0 ? themes[index] : undefined;
68
+ if (!chosen) {
69
+ postToChat(pi, `**No match for** \`${picked}\``);
70
+ return;
71
+ }
72
+ applyTheme(pi, chosen.name);
73
+ },
74
+ });
75
+ }
@@ -20,6 +20,7 @@ import { executeLangFlowTool } from "./tools/langflow.js";
20
20
  import { registerBuildFlowCommand } from "./commands/build-flow/command.js";
21
21
  import { registerPromptEngineerCommand } from "./commands/prompt-engineer/command.js";
22
22
  import { registerReleaseNotesCommand } from "./commands/release-notes/command.js";
23
+ import { registerThemeCommand } from "./commands/theme/command.js";
23
24
  import { parseLangFlowNaturalLanguage } from "./commands/langflow/natural-language.js";
24
25
  const _here = dirname(fileURLToPath(import.meta.url));
25
26
  const FLOW_TRIGGERS_DIR = join(_here, "commands", "flow-triggers");
@@ -185,6 +186,8 @@ export default function Otto(pi) {
185
186
  registerPromptEngineerCommand(pi);
186
187
  // ── Register /release-notes slash command ──
187
188
  registerReleaseNotesCommand(pi);
189
+ // ── Register /theme slash command ──
190
+ registerThemeCommand(pi);
188
191
  // ── Load and register flow-trigger slash commands ──
189
192
  // Fire-and-forget. Pi's command registry is dynamic; late registrations work.
190
193
  loadFlowTriggers(FLOW_TRIGGERS_DIR)
@@ -2,33 +2,113 @@
2
2
  * Agent discovery and configuration
3
3
  */
4
4
  import * as fs from "node:fs";
5
+ import { homedir } from "node:os";
5
6
  import * as path from "node:path";
6
7
  import { getAgentDir, parseFrontmatter } from "@otto/pi-coding-agent";
7
8
  const PROJECT_AGENT_DIR_CANDIDATES = [".otto/workflow", ".pi"];
9
+ /**
10
+ * Conventional agent folders used by other AI coding harnesses, mirroring the
11
+ * harness skill-paths support in pi-coding-agent's skills.ts. Each entry maps
12
+ * a harness id (used as a discriminator in logs and the agent's `source`
13
+ * label) to its user-scope and project-scope conventional paths.
14
+ *
15
+ * When a Claude-style skill delegates to a subagent (`subagent_type: foo` /
16
+ * `Task` tool call), OTTO's subagent tool resolves `foo` against everything
17
+ * `discoverAgents` returns. Including these harness paths is what lets a
18
+ * skill imported from `~/.claude/skills` find its companion agent in
19
+ * `~/.claude/agents` rather than failing with "unknown agent."
20
+ *
21
+ * Caveats — independent of discovery — that may affect runtime success:
22
+ * - Claude agents commonly declare `tools: [Bash, Read, ...]` (capitalized).
23
+ * OTTO's tool names tend to be lowercase. If an agent's allowlist is
24
+ * enforced strictly by the harness it was written for, capitalized
25
+ * entries won't match OTTO's tool registry. Agents without a `tools`
26
+ * field (no restriction) work without issue.
27
+ * - Agent body prompts may reference harness-specific features
28
+ * (Claude's `/compact`, MCP server names hardcoded for `claude_desktop`,
29
+ * `~/.claude/...` paths). The agent still runs; those references may
30
+ * just be ineffective.
31
+ */
32
+ const HARNESS_AGENT_PATHS = {
33
+ claude: {
34
+ userDir: path.join(homedir(), ".claude", "agents"),
35
+ projectSubdir: path.join(".claude", "agents"),
36
+ },
37
+ codex: {
38
+ userDir: path.join(homedir(), ".codex", "agents"),
39
+ projectSubdir: path.join(".codex", "agents"),
40
+ },
41
+ kiro: {
42
+ userDir: path.join(homedir(), ".kiro", "agents"),
43
+ projectSubdir: path.join(".kiro", "agents"),
44
+ },
45
+ };
8
46
  export function parseConflictsWith(value) {
9
47
  if (typeof value !== "string")
10
48
  return undefined;
11
49
  const conflicts = value.split(",").map((s) => s.trim()).filter(Boolean);
12
50
  return conflicts.length > 0 ? conflicts : undefined;
13
51
  }
14
- function parseAgentTools(value) {
52
+ /**
53
+ * Maps capitalized tool names used by other AI coding harnesses (Claude, Codex,
54
+ * Kiro) to OTTO's lowercase tool-registry names. Applied at agent-load time so
55
+ * an agent imported from ~/.claude/agents/ with `tools: [Bash, Read]` ends up
56
+ * with `tools: ["bash", "read"]` and actually has access to those tools.
57
+ *
58
+ * Tools without an explicit mapping fall through to .toLowerCase() — that's
59
+ * enough for any name that's already aligned with OTTO's case-insensitive
60
+ * convention. Tool names with no OTTO equivalent (TodoWrite, SlashCommand,
61
+ * NotebookEdit) are kept (lowercased) and silently ignored by the runtime
62
+ * allowlist check — so they don't block the rest of the agent's toolset.
63
+ *
64
+ * MCP tool patterns (mcp__server__name, mcp__server__*) are preserved
65
+ * verbatim — they're already case-sensitive identifiers, and MCP servers
66
+ * register their own tools at runtime.
67
+ */
68
+ const HARNESS_TOOL_NAME_MAP = {
69
+ Bash: "bash",
70
+ Read: "read",
71
+ Write: "write",
72
+ Edit: "edit",
73
+ Glob: "glob",
74
+ Grep: "grep",
75
+ AskUserQuestion: "ask_user_questions",
76
+ Agent: "subagent",
77
+ Task: "subagent",
78
+ WebSearch: "web_search",
79
+ WebFetch: "fetch_page",
80
+ Skill: "skill",
81
+ };
82
+ function normalizeHarnessToolName(raw) {
83
+ const trimmed = raw.trim();
84
+ if (!trimmed)
85
+ return null;
86
+ if (trimmed.startsWith("mcp__"))
87
+ return trimmed;
88
+ if (HARNESS_TOOL_NAME_MAP[trimmed])
89
+ return HARNESS_TOOL_NAME_MAP[trimmed];
90
+ return trimmed.toLowerCase();
91
+ }
92
+ export function parseAgentTools(value) {
15
93
  if (typeof value === "string") {
16
94
  const tools = value
17
95
  .split(",")
18
- .map((tool) => tool.trim())
19
- .filter(Boolean);
20
- return tools.length > 0 ? tools : undefined;
96
+ .map((tool) => normalizeHarnessToolName(tool))
97
+ .filter((tool) => Boolean(tool));
98
+ const deduped = Array.from(new Set(tools));
99
+ return deduped.length > 0 ? deduped : undefined;
21
100
  }
22
101
  if (Array.isArray(value)) {
23
102
  const tools = value
24
103
  .flatMap((tool) => typeof tool === "string" ? tool.split(",") : [])
25
- .map((tool) => tool.trim())
26
- .filter(Boolean);
27
- return tools.length > 0 ? tools : undefined;
104
+ .map((tool) => normalizeHarnessToolName(tool))
105
+ .filter((tool) => Boolean(tool));
106
+ const deduped = Array.from(new Set(tools));
107
+ return deduped.length > 0 ? deduped : undefined;
28
108
  }
29
109
  return undefined;
30
110
  }
31
- function loadAgentsFromDir(dir, source) {
111
+ function loadAgentsFromDir(dir, source, harnessSource) {
32
112
  const agents = [];
33
113
  if (!fs.existsSync(dir)) {
34
114
  return agents;
@@ -68,6 +148,7 @@ function loadAgentsFromDir(dir, source) {
68
148
  systemPrompt: body,
69
149
  source,
70
150
  filePath,
151
+ harnessSource,
71
152
  });
72
153
  }
73
154
  return agents;
@@ -96,25 +177,96 @@ function findNearestProjectAgentsDir(cwd) {
96
177
  currentDir = parentDir;
97
178
  }
98
179
  }
180
+ /**
181
+ * Walk up from cwd looking for harness project agent dirs (e.g. `.claude/agents`).
182
+ * Returns ALL matching dirs found at the first ancestor that has at least one,
183
+ * so a project root with both `.claude/agents` and `.codex/agents` surfaces
184
+ * both. Stops at the first ancestor with any matches to avoid pulling in
185
+ * stale agents from outer directories.
186
+ */
187
+ function findNearestHarnessProjectAgentsDirs(cwd) {
188
+ let currentDir = cwd;
189
+ while (true) {
190
+ const matches = [];
191
+ for (const { projectSubdir } of Object.values(HARNESS_AGENT_PATHS)) {
192
+ const candidate = path.join(currentDir, projectSubdir);
193
+ if (isDirectory(candidate))
194
+ matches.push(candidate);
195
+ }
196
+ if (matches.length > 0)
197
+ return matches;
198
+ const parentDir = path.dirname(currentDir);
199
+ if (parentDir === currentDir)
200
+ return [];
201
+ currentDir = parentDir;
202
+ }
203
+ }
99
204
  export function discoverAgents(cwd, scope) {
100
205
  const userDir = path.join(getAgentDir(), "agents");
101
206
  const projectAgentsDir = findNearestProjectAgentsDir(cwd);
207
+ const harnessProjectAgentsDirs = findNearestHarnessProjectAgentsDirs(cwd);
102
208
  const userAgents = scope === "project" ? [] : loadAgentsFromDir(userDir, "user");
103
209
  const projectAgents = scope === "user" || !projectAgentsDir ? [] : loadAgentsFromDir(projectAgentsDir, "project");
210
+ // Harness agents — user scope: ~/.claude/agents, ~/.codex/agents, ~/.kiro/agents.
211
+ // Harness agents — project scope: nearest .claude/agents etc. up the cwd tree.
212
+ // Both go into the same scope buckets as OTTO's own agents; collisions are
213
+ // resolved by the existing Map-write-wins order below (project wins user).
214
+ const harnessUserAgents = [];
215
+ if (scope !== "project") {
216
+ for (const [harnessId, { userDir: harnessUserDir }] of Object.entries(HARNESS_AGENT_PATHS)) {
217
+ harnessUserAgents.push(...loadAgentsFromDir(harnessUserDir, "user", harnessId));
218
+ }
219
+ }
220
+ const harnessProjectAgents = [];
221
+ if (scope !== "user") {
222
+ // Map each discovered project dir back to its harness id by suffix match
223
+ // (e.g. ".../.claude/agents" → "claude"). The id surfaces in /subagent as
224
+ // a glanceable chip.
225
+ for (const dir of harnessProjectAgentsDirs) {
226
+ let harnessId;
227
+ for (const [id, { projectSubdir }] of Object.entries(HARNESS_AGENT_PATHS)) {
228
+ if (dir.endsWith(projectSubdir)) {
229
+ harnessId = id;
230
+ break;
231
+ }
232
+ }
233
+ harnessProjectAgents.push(...loadAgentsFromDir(dir, "project", harnessId));
234
+ }
235
+ }
104
236
  const agentMap = new Map();
237
+ // Order: OTTO user → harness user → OTTO project → harness project.
238
+ // Later writes win on name collision, so OTTO's own agents take precedence
239
+ // at each scope, and project always overrides user. Same precedence as the
240
+ // existing logic, just with harness sources slotted in beneath.
105
241
  if (scope === "both") {
106
242
  for (const agent of userAgents)
107
243
  agentMap.set(agent.name, agent);
244
+ for (const agent of harnessUserAgents) {
245
+ if (!agentMap.has(agent.name))
246
+ agentMap.set(agent.name, agent);
247
+ }
108
248
  for (const agent of projectAgents)
109
249
  agentMap.set(agent.name, agent);
250
+ for (const agent of harnessProjectAgents) {
251
+ if (!agentMap.has(agent.name))
252
+ agentMap.set(agent.name, agent);
253
+ }
110
254
  }
111
255
  else if (scope === "user") {
112
256
  for (const agent of userAgents)
113
257
  agentMap.set(agent.name, agent);
258
+ for (const agent of harnessUserAgents) {
259
+ if (!agentMap.has(agent.name))
260
+ agentMap.set(agent.name, agent);
261
+ }
114
262
  }
115
263
  else {
116
264
  for (const agent of projectAgents)
117
265
  agentMap.set(agent.name, agent);
266
+ for (const agent of harnessProjectAgents) {
267
+ if (!agentMap.has(agent.name))
268
+ agentMap.set(agent.name, agent);
269
+ }
118
270
  }
119
271
  return { agents: Array.from(agentMap.values()), projectAgentsDir };
120
272
  }
@@ -17,12 +17,13 @@ import * as fs from "node:fs";
17
17
  import * as os from "node:os";
18
18
  import * as path from "node:path";
19
19
  import { StringEnum } from "@otto/pi-ai";
20
- import { getMarkdownTheme } from "@otto/pi-coding-agent";
20
+ import { getMarkdownTheme, getSelectListTheme } from "@otto/pi-coding-agent";
21
21
  import { Container, Markdown, Spacer, Text } from "@otto/pi-tui";
22
22
  import { Type } from "@sinclair/typebox";
23
23
  import { formatTokenCount } from "../shared/mod.js";
24
24
  import { getCurrentPhase } from "../shared/phase-state.js";
25
25
  import { discoverAgents } from "./agents.js";
26
+ import { buildSkillToolStubResponse } from "./skill-tool-stub.js";
26
27
  import { createIsolation, mergeDeltaPatches, readIsolationMode, } from "./isolation.js";
27
28
  import { registerWorker, updateWorker } from "./worker-registry.js";
28
29
  import { loadEffectiveGSDPreferences } from "../workflow/preferences.js";
@@ -643,7 +644,13 @@ export default function (pi) {
643
644
  pi.on("session_shutdown", async () => {
644
645
  await stopLiveSubagents();
645
646
  });
646
- // /subagent command - list available agents
647
+ // /subagent command - list available agents.
648
+ // Renders each row with embedded ANSI styling so the message bypasses the
649
+ // notify renderer's "dim everything" fallback (see hasAnsiStyling check in
650
+ // pi-coding-agent's renderExtensionNotifyInChat). Agent name renders in the
651
+ // terminal's default foreground (white-ish), source/model in the muted
652
+ // description tint, and the harness origin — when detected — as an accent
653
+ // chip matching the /skills autocomplete style.
647
654
  pi.registerCommand("subagent", {
648
655
  description: "List available subagents",
649
656
  handler: async (_args, ctx) => {
@@ -652,8 +659,17 @@ export default function (pi) {
652
659
  ctx.ui.notify("No agents found. Add .md files to ~/.otto/agent/agents/ or .otto/workflow/agents/", "warning");
653
660
  return;
654
661
  }
655
- const lines = discovery.agents.map((a) => ` ${a.name} [${a.source}]${a.model ? ` (${a.model})` : ""}: ${a.description}`);
656
- ctx.ui.notify(`Available agents (${discovery.agents.length}):\n${lines.join("\n")}`, "info");
662
+ const slt = getSelectListTheme();
663
+ const tag = slt.tag ?? slt.selectedText;
664
+ const dim = slt.description;
665
+ const lines = discovery.agents.map((a) => {
666
+ const chip = a.harnessSource ? `${tag(`[${a.harnessSource}]`)} ` : "";
667
+ const meta = dim(`[${a.source}]${a.model ? ` (${a.model})` : ""}`);
668
+ const desc = dim(`: ${a.description}`);
669
+ return ` ${chip}${a.name} ${meta}${desc}`;
670
+ });
671
+ const header = dim(`Available agents (${discovery.agents.length}):`);
672
+ ctx.ui.notify(`${header}\n${lines.join("\n")}`, "info");
657
673
  },
658
674
  });
659
675
  pi.registerTool({
@@ -1565,4 +1581,29 @@ export default function (pi) {
1565
1581
  return new Text(text?.type === "text" ? text.text : "(no output)", 0, 0);
1566
1582
  },
1567
1583
  });
1584
+ pi.registerTool({
1585
+ name: "skill",
1586
+ label: "Skill (stub)",
1587
+ description: [
1588
+ "Stub for the Claude-style `Skill` tool used by some imported skills.",
1589
+ "OTTO does not support invoking skills as a tool — users invoke skills via /skill:<name> from the chat input.",
1590
+ "Returns a clear message redirecting the model to use the skill content inline.",
1591
+ ].join(" "),
1592
+ parameters: Type.Object({
1593
+ name: Type.Optional(Type.String({ description: "Name of the skill the model wanted to invoke." })),
1594
+ }),
1595
+ async execute(_toolCallId, params) {
1596
+ return {
1597
+ content: [
1598
+ {
1599
+ type: "text",
1600
+ text: buildSkillToolStubResponse({
1601
+ name: typeof params.name === "string" ? params.name : undefined,
1602
+ }),
1603
+ },
1604
+ ],
1605
+ details: {},
1606
+ };
1607
+ },
1608
+ });
1568
1609
  }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Stub for the `skill` tool that Claude-style skills sometimes invoke
3
+ * (`Skill(name="foo")` in the skill body) to chain into another skill.
4
+ *
5
+ * OTTO does not support model-invoked skill execution — skills are user-
6
+ * initiated via `/skill:<name>` from the chat input. Rather than have those
7
+ * tool calls fail with "no such tool," we register a stub that returns a
8
+ * clear, actionable message. The model sees the response and can either
9
+ * fall back to acting on the skill content itself or surface the message
10
+ * to the user.
11
+ *
12
+ * Registered in src/resources/extensions/subagent/index.ts.
13
+ */
14
+ export function buildSkillToolStubResponse(input) {
15
+ const skillRef = typeof input.name === "string" && input.name.trim().length > 0
16
+ ? `/skill:${input.name.trim()}`
17
+ : "the desired /skill:<name>";
18
+ return [
19
+ "OTTO does not support invoking skills as a tool from inside an agent turn.",
20
+ `To use this skill, ask the user to run \`${skillRef}\` from the chat input — that's how OTTO surfaces skills.`,
21
+ "If you have access to the skill's body (it was loaded as a system prompt), follow its instructions inline instead.",
22
+ ].join("\n\n");
23
+ }
@@ -1,3 +1,19 @@
1
+ /**
2
+ * Conventional skill folders used by other AI coding harnesses. When the
3
+ * recommended-packages seeding runs and the directory exists on disk, the
4
+ * path string (with literal `~/` so it stays portable in settings.json) is
5
+ * appended to `settings.skills` and recorded in `settings.seededSkillPaths`
6
+ * as a zombie-resurrection guard. Users who remove an entry from
7
+ * settings.skills will not see it re-added on the next launch.
8
+ *
9
+ * Pi.dev's documented convention is for users to add these paths manually to
10
+ * settings.skills; OTTO seeds them automatically as a quality-of-life default
11
+ * for users who have these harnesses installed.
12
+ */
13
+ export declare const HARNESS_SKILL_PATHS: readonly {
14
+ setting: string;
15
+ resolved: string;
16
+ }[];
1
17
  export interface DefaultPackage {
2
18
  /** Install source as parsed by package-manager (npm:..., git:..., ./...). */
3
19
  source: string;
@@ -21,7 +21,26 @@
21
21
  // settings.seededDefaults. A subsequent `otto remove npm:foo` clears the entry
22
22
  // from settings.packages but leaves the seededDefaults marker, so we will NOT
23
23
  // re-add it on the next launch.
24
- import { mkdirSync } from 'node:fs';
24
+ import { existsSync, mkdirSync } from 'node:fs';
25
+ import { homedir } from 'node:os';
26
+ import { join } from 'node:path';
27
+ /**
28
+ * Conventional skill folders used by other AI coding harnesses. When the
29
+ * recommended-packages seeding runs and the directory exists on disk, the
30
+ * path string (with literal `~/` so it stays portable in settings.json) is
31
+ * appended to `settings.skills` and recorded in `settings.seededSkillPaths`
32
+ * as a zombie-resurrection guard. Users who remove an entry from
33
+ * settings.skills will not see it re-added on the next launch.
34
+ *
35
+ * Pi.dev's documented convention is for users to add these paths manually to
36
+ * settings.skills; OTTO seeds them automatically as a quality-of-life default
37
+ * for users who have these harnesses installed.
38
+ */
39
+ export const HARNESS_SKILL_PATHS = [
40
+ { setting: '~/.claude/skills', resolved: join(homedir(), '.claude', 'skills') },
41
+ { setting: '~/.codex/skills', resolved: join(homedir(), '.codex', 'skills') },
42
+ { setting: '~/.kiro/skills', resolved: join(homedir(), '.kiro', 'skills') },
43
+ ];
25
44
  // Add new personas here. Empty `packages: []` is allowed (the onboarding UI
26
45
  // will skip categories with no packages so users aren't shown empty lists).
27
46
  export const OTTO_DEFAULT_PACKAGE_CATEGORIES = [
@@ -178,6 +197,12 @@ export async function maybeSeedDefaultPackages(flags, agentDirPath) {
178
197
  if (seededChanged) {
179
198
  settingsManager.setSeededDefaults(newSeeded);
180
199
  }
200
+ // ── Auto-seed harness skill paths ──────────────────────────────────────────
201
+ // For each conventional harness skill folder that exists on disk, append the
202
+ // literal `~/` path to settings.skills. Skipped for paths already present in
203
+ // settings.skills, and for paths recorded in settings.seededSkillPaths (so a
204
+ // user who removed an entry is not overridden on subsequent launches).
205
+ reconcileHarnessSkillPaths(settingsManager);
181
206
  // ── Reconcile quietExtensions ──────────────────────────────────────────────
182
207
  // Packages can declare a `quietPattern` (e.g. pi-notion). When we seed such a
183
208
  // package for the first time, also append its pattern to settings.quietExtensions.
@@ -224,3 +249,46 @@ function collectQuietPatternsForSources(targetSources) {
224
249
  }
225
250
  return Array.from(patterns);
226
251
  }
252
+ /**
253
+ * Append harness skill paths (~/.claude/skills, ~/.codex/skills, ~/.kiro/skills)
254
+ * to settings.skills when:
255
+ * 1. The resolved directory actually exists on disk (no point seeding a
256
+ * phantom path for a harness the user doesn't have installed), AND
257
+ * 2. The path isn't already in settings.skills (idempotent), AND
258
+ * 3. The path isn't in settings.seededSkillPaths (zombie guard — respect
259
+ * a user who removed an entry from settings.skills).
260
+ *
261
+ * Records every attempted path in settings.seededSkillPaths regardless of
262
+ * whether it was added, so the zombie guard converges to the union of
263
+ * everything we've ever tried.
264
+ */
265
+ function reconcileHarnessSkillPaths(settingsManager) {
266
+ const seededBefore = new Set(settingsManager.getSeededSkillPaths());
267
+ const existing = new Set(settingsManager.getSkillPaths());
268
+ const newPaths = [...settingsManager.getSkillPaths()];
269
+ let pathsChanged = false;
270
+ for (const { setting, resolved } of HARNESS_SKILL_PATHS) {
271
+ if (!existsSync(resolved))
272
+ continue;
273
+ if (seededBefore.has(setting))
274
+ continue;
275
+ if (!existing.has(setting)) {
276
+ newPaths.push(setting);
277
+ existing.add(setting);
278
+ pathsChanged = true;
279
+ }
280
+ }
281
+ // seededSkillPaths tracks every path we've attempted in this run, even when
282
+ // the directory wasn't present — so a user who installs Claude later won't
283
+ // get the path auto-added (consistent with our other zombie-guard semantics).
284
+ // If you want the inverse behaviour, remove the entry from seededSkillPaths.
285
+ const attemptedPaths = HARNESS_SKILL_PATHS
286
+ .filter(({ resolved }) => existsSync(resolved))
287
+ .map(({ setting }) => setting);
288
+ const newSeeded = Array.from(new Set([...seededBefore, ...attemptedPaths]));
289
+ const seededChanged = newSeeded.length !== seededBefore.size;
290
+ if (pathsChanged)
291
+ settingsManager.setSkillPaths(newPaths);
292
+ if (seededChanged)
293
+ settingsManager.setSeededSkillPaths(newSeeded);
294
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cmetech/otto",
3
- "version": "1.0.9",
3
+ "version": "1.1.0",
4
4
  "description": "Terminal-based developer chat assistant. Permanent hard fork of gsd-pi with LangFlow flow triggers, a flow builder, and optional gateway routing for compliance environments.",
5
5
  "keywords": [
6
6
  "ai",
@@ -183,11 +183,11 @@
183
183
  "@anthropic-ai/claude-agent-sdk": "0.2.83",
184
184
  "fsevents": "~2.3.3",
185
185
  "koffi": "^2.9.0",
186
- "@cmetech/otto-engine-darwin-arm64": "1.0.9",
187
- "@cmetech/otto-engine-darwin-x64": "1.0.9",
188
- "@cmetech/otto-engine-linux-arm64-gnu": "1.0.9",
189
- "@cmetech/otto-engine-linux-x64-gnu": "1.0.9",
190
- "@cmetech/otto-engine-win32-x64-msvc": "1.0.9"
186
+ "@cmetech/otto-engine-darwin-arm64": "1.1.0",
187
+ "@cmetech/otto-engine-darwin-x64": "1.1.0",
188
+ "@cmetech/otto-engine-linux-arm64-gnu": "1.1.0",
189
+ "@cmetech/otto-engine-linux-x64-gnu": "1.1.0",
190
+ "@cmetech/otto-engine-win32-x64-msvc": "1.1.0"
191
191
  },
192
192
  "overrides": {
193
193
  "gaxios": "7.1.4",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@otto-build/contracts",
3
- "version": "1.0.9",
3
+ "version": "1.1.0",
4
4
  "description": "Shared public contracts for OTTO workspace boundaries",
5
5
  "license": "MIT",
6
6
  "otto": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@otto-build/daemon",
3
- "version": "1.0.9",
3
+ "version": "1.1.0",
4
4
  "description": "OTTO daemon — background process for project monitoring and Discord integration",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -29,8 +29,8 @@
29
29
  },
30
30
  "dependencies": {
31
31
  "@anthropic-ai/sdk": "^0.52.0",
32
- "@otto-build/contracts": "^1.0.9",
33
- "@otto-build/rpc-client": "^1.0.9",
32
+ "@otto-build/contracts": "^1.1.0",
33
+ "@otto-build/rpc-client": "^1.1.0",
34
34
  "discord.js": "^14.25.1",
35
35
  "yaml": "^2.8.0",
36
36
  "zod": "^3.24.0"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@otto-build/mcp-server",
3
- "version": "1.0.9",
3
+ "version": "1.1.0",
4
4
  "description": "MCP server exposing OTTO orchestration tools for compatible clients",
5
5
  "license": "MIT",
6
6
  "otto": {
@@ -34,8 +34,8 @@
34
34
  "test": "npm run build:test && node --test dist/mcp-server.test.js dist/remote-questions.test.js"
35
35
  },
36
36
  "dependencies": {
37
- "@otto-build/contracts": "^1.0.9",
38
- "@otto-build/rpc-client": "^1.0.9",
37
+ "@otto-build/contracts": "^1.1.0",
38
+ "@otto-build/rpc-client": "^1.1.0",
39
39
  "@modelcontextprotocol/sdk": "^1.27.1",
40
40
  "zod": "^4.0.0"
41
41
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@otto/native",
3
- "version": "1.0.9",
3
+ "version": "1.1.0",
4
4
  "description": "Native Rust bindings for OTTO — high-performance native modules via N-API",
5
5
  "type": "commonjs",
6
6
  "otto": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@otto/pi-agent-core",
3
- "version": "1.0.9",
3
+ "version": "1.1.0",
4
4
  "description": "General-purpose agent core (vendored from pi-mono)",
5
5
  "type": "module",
6
6
  "otto": {