takomi 2.1.13 → 2.1.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.
Files changed (39) hide show
  1. package/.pi/agents/architect.md +73 -73
  2. package/.pi/agents/coder.md +70 -70
  3. package/.pi/agents/designer.md +72 -72
  4. package/.pi/agents/orchestrator.md +122 -122
  5. package/.pi/agents/reviewer.md +71 -71
  6. package/.pi/extensions/oauth-router/provider.ts +3 -1
  7. package/.pi/extensions/takomi-context-manager/config.ts +48 -48
  8. package/.pi/extensions/takomi-context-manager/context-router.ts +57 -57
  9. package/.pi/extensions/takomi-context-manager/diagnostics-tools.ts +28 -28
  10. package/.pi/extensions/takomi-context-manager/diagnostics.ts +55 -55
  11. package/.pi/extensions/takomi-context-manager/extension-conflicts.ts +56 -56
  12. package/.pi/extensions/takomi-context-manager/index.ts +56 -56
  13. package/.pi/extensions/takomi-context-manager/model-policy-gate.ts +228 -206
  14. package/.pi/extensions/takomi-context-manager/policy-registry.ts +97 -97
  15. package/.pi/extensions/takomi-context-manager/policy-tools.ts +35 -35
  16. package/.pi/extensions/takomi-context-manager/prerequisite-gates.ts +39 -39
  17. package/.pi/extensions/takomi-context-manager/prompt-rewriter.ts +100 -100
  18. package/.pi/extensions/takomi-context-manager/skill-registry.ts +87 -87
  19. package/.pi/extensions/takomi-context-manager/skill-tools.ts +80 -80
  20. package/.pi/extensions/takomi-context-manager/state.ts +68 -68
  21. package/.pi/extensions/takomi-context-manager/types.ts +77 -77
  22. package/.pi/extensions/takomi-runtime/command-text.ts +10 -2
  23. package/.pi/extensions/takomi-runtime/commands.ts +78 -5
  24. package/.pi/extensions/takomi-runtime/routing-policy.ts +187 -145
  25. package/.pi/extensions/takomi-subagents/native-render.ts +41 -41
  26. package/.pi/extensions/takomi-subagents/pi-subagents-internal.ts +35 -32
  27. package/.pi/extensions/takomi-subagents/run-types.ts +25 -25
  28. package/.pi/prompts/build-prompt.md +259 -259
  29. package/.pi/prompts/design-prompt.md +95 -95
  30. package/.pi/prompts/genesis-prompt.md +140 -140
  31. package/.pi/prompts/prime-prompt.md +110 -110
  32. package/.pi/themes/takomi-aurora.json +88 -88
  33. package/README.md +2 -4
  34. package/assets/.agent/skills/21st-dev-components/SKILL.md +244 -244
  35. package/assets/.agent/skills/anti-gravity/SKILL.md +112 -0
  36. package/assets/.agent/skills/gemini/SKILL.md +14 -223
  37. package/assets/.agent/skills/git-commit-generation/SKILL.md +195 -0
  38. package/package.json +1 -1
  39. package/src/pi-takomi-core/validation.ts +135 -135
@@ -1,80 +1,80 @@
1
- import { readFile } from "node:fs/promises";
2
- import path from "node:path";
3
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
4
- import { Type } from "typebox";
5
- import type { ContextManagerState } from "./state";
6
- import { findSkill, normalizeName, sortedSkills } from "./skill-registry";
7
-
8
- function renderSkillIndex(state: ContextManagerState): string {
9
- const skills = sortedSkills(state.skills);
10
- if (skills.length === 0) return "Available skills (names only): none discovered.";
11
- return ["Available skills (names only):", ...skills.map((skill) => `- ${skill.name}`)].join("\n");
12
- }
13
-
14
- function renderManifest(state: ContextManagerState, names: string[]): string {
15
- if (names.length === 0) return "No skills requested.";
16
- return names.map((name) => {
17
- const skill = findSkill(state.skills, name);
18
- if (!skill) {
19
- const close = sortedSkills(state.skills).filter((candidate) => normalizeName(candidate.name).includes(normalizeName(name).slice(0, 4))).slice(0, 5).map((candidate) => candidate.name);
20
- return [`Skill not found: ${name}`, close.length ? `Known close matches: ${close.join(", ")}` : ""].filter(Boolean).join("\n");
21
- }
22
- return [`Skill: ${skill.name}`, `Description: ${skill.description ?? "(no description discovered)"}`, `Location: ${skill.location ?? "(no location discovered)"}`].join("\n");
23
- }).join("\n\n");
24
- }
25
-
26
- async function loadSkillContent(location: string): Promise<string> {
27
- const fileName = path.basename(location).toLowerCase();
28
- if (fileName !== "skill.md" && !location.toLowerCase().endsWith(".md")) throw new Error(`Refusing to load non-markdown skill location: ${location}`);
29
- return readFile(location, "utf8");
30
- }
31
-
32
- export function registerSkillTools(pi: ExtensionAPI, state: ContextManagerState): void {
33
- pi.registerTool({
34
- name: "skill_index",
35
- label: "Skill Index",
36
- description: "Return the available skill names only. Use this to inspect capability names without loading descriptions or full instructions.",
37
- promptSnippet: "List available skill names only for progressive skill loading",
38
- parameters: Type.Object({}),
39
- async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
40
- state.report.cwd = ctx.cwd;
41
- state.report.toolCalls.skillIndex += 1;
42
- return { content: [{ type: "text", text: renderSkillIndex(state) }], details: { skillCount: state.skills.size } };
43
- },
44
- });
45
-
46
- pi.registerTool({
47
- name: "skill_manifest",
48
- label: "Skill Manifest",
49
- description: "Return descriptions and locations for selected skills without loading full SKILL.md instructions.",
50
- promptSnippet: "Show selected skill descriptions and locations without full instructions",
51
- parameters: Type.Object({ skills: Type.Array(Type.String({ description: "Skill name to inspect" })) }),
52
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
53
- state.report.cwd = ctx.cwd;
54
- state.report.toolCalls.skillManifest += 1;
55
- return { content: [{ type: "text", text: renderManifest(state, params.skills) }], details: { requested: params.skills } };
56
- },
57
- });
58
-
59
- pi.registerTool({
60
- name: "skill_load",
61
- label: "Skill Load",
62
- description: "Load the full SKILL.md content for one selected skill that will actually be used.",
63
- promptSnippet: "Load full SKILL.md instructions for one selected skill",
64
- parameters: Type.Object({ skill: Type.String({ description: "Exact skill name to load" }) }),
65
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
66
- state.report.cwd = ctx.cwd;
67
- state.report.toolCalls.skillLoad += 1;
68
- const skill = findSkill(state.skills, params.skill);
69
- if (!skill?.location) return { content: [{ type: "text", text: renderManifest(state, [params.skill]) }], details: { found: false, requested: params.skill }, isError: true };
70
- try {
71
- const content = await loadSkillContent(skill.location);
72
- state.report.loadedByTool.push(skill.name);
73
- return { content: [{ type: "text", text: [`Skill: ${skill.name}`, `Location: ${skill.location}`, "", content].join("\n") }], details: { found: true, skill: skill.name, location: skill.location } };
74
- } catch (error) {
75
- const message = error instanceof Error ? error.message : String(error);
76
- return { content: [{ type: "text", text: message }], details: { found: true, skill: skill.name, error: message }, isError: true };
77
- }
78
- },
79
- });
80
- }
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
4
+ import { Type } from "typebox";
5
+ import type { ContextManagerState } from "./state";
6
+ import { findSkill, normalizeName, sortedSkills } from "./skill-registry";
7
+
8
+ function renderSkillIndex(state: ContextManagerState): string {
9
+ const skills = sortedSkills(state.skills);
10
+ if (skills.length === 0) return "Available skills (names only): none discovered.";
11
+ return ["Available skills (names only):", ...skills.map((skill) => `- ${skill.name}`)].join("\n");
12
+ }
13
+
14
+ function renderManifest(state: ContextManagerState, names: string[]): string {
15
+ if (names.length === 0) return "No skills requested.";
16
+ return names.map((name) => {
17
+ const skill = findSkill(state.skills, name);
18
+ if (!skill) {
19
+ const close = sortedSkills(state.skills).filter((candidate) => normalizeName(candidate.name).includes(normalizeName(name).slice(0, 4))).slice(0, 5).map((candidate) => candidate.name);
20
+ return [`Skill not found: ${name}`, close.length ? `Known close matches: ${close.join(", ")}` : ""].filter(Boolean).join("\n");
21
+ }
22
+ return [`Skill: ${skill.name}`, `Description: ${skill.description ?? "(no description discovered)"}`, `Location: ${skill.location ?? "(no location discovered)"}`].join("\n");
23
+ }).join("\n\n");
24
+ }
25
+
26
+ async function loadSkillContent(location: string): Promise<string> {
27
+ const fileName = path.basename(location).toLowerCase();
28
+ if (fileName !== "skill.md" && !location.toLowerCase().endsWith(".md")) throw new Error(`Refusing to load non-markdown skill location: ${location}`);
29
+ return readFile(location, "utf8");
30
+ }
31
+
32
+ export function registerSkillTools(pi: ExtensionAPI, state: ContextManagerState): void {
33
+ pi.registerTool({
34
+ name: "skill_index",
35
+ label: "Skill Index",
36
+ description: "Return the available skill names only. Use this to inspect capability names without loading descriptions or full instructions.",
37
+ promptSnippet: "List available skill names only for progressive skill loading",
38
+ parameters: Type.Object({}),
39
+ async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
40
+ state.report.cwd = ctx.cwd;
41
+ state.report.toolCalls.skillIndex += 1;
42
+ return { content: [{ type: "text", text: renderSkillIndex(state) }], details: { skillCount: state.skills.size } };
43
+ },
44
+ });
45
+
46
+ pi.registerTool({
47
+ name: "skill_manifest",
48
+ label: "Skill Manifest",
49
+ description: "Return descriptions and locations for selected skills without loading full SKILL.md instructions.",
50
+ promptSnippet: "Show selected skill descriptions and locations without full instructions",
51
+ parameters: Type.Object({ skills: Type.Array(Type.String({ description: "Skill name to inspect" })) }),
52
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
53
+ state.report.cwd = ctx.cwd;
54
+ state.report.toolCalls.skillManifest += 1;
55
+ return { content: [{ type: "text", text: renderManifest(state, params.skills) }], details: { requested: params.skills } };
56
+ },
57
+ });
58
+
59
+ pi.registerTool({
60
+ name: "skill_load",
61
+ label: "Skill Load",
62
+ description: "Load the full SKILL.md content for one selected skill that will actually be used.",
63
+ promptSnippet: "Load full SKILL.md instructions for one selected skill",
64
+ parameters: Type.Object({ skill: Type.String({ description: "Exact skill name to load" }) }),
65
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
66
+ state.report.cwd = ctx.cwd;
67
+ state.report.toolCalls.skillLoad += 1;
68
+ const skill = findSkill(state.skills, params.skill);
69
+ if (!skill?.location) return { content: [{ type: "text", text: renderManifest(state, [params.skill]) }], details: { found: false, requested: params.skill }, isError: true };
70
+ try {
71
+ const content = await loadSkillContent(skill.location);
72
+ state.report.loadedByTool.push(skill.name);
73
+ return { content: [{ type: "text", text: [`Skill: ${skill.name}`, `Location: ${skill.location}`, "", content].join("\n") }], details: { found: true, skill: skill.name, location: skill.location } };
74
+ } catch (error) {
75
+ const message = error instanceof Error ? error.message : String(error);
76
+ return { content: [{ type: "text", text: message }], details: { found: true, skill: skill.name, error: message }, isError: true };
77
+ }
78
+ },
79
+ });
80
+ }
@@ -1,68 +1,68 @@
1
- import type { ContextReport, PolicyPack, SkillRecord } from "./types";
2
-
3
- export type ContextManagerState = {
4
- skills: Map<string, SkillRecord>;
5
- policies: Map<string, PolicyPack>;
6
- loadedPolicies: Set<string>;
7
- readFiles: Set<string>;
8
- editedFiles: Set<string>;
9
- writtenFiles: Set<string>;
10
- report: ContextReport;
11
- };
12
-
13
- export function createEmptyReport(): ContextReport {
14
- return {
15
- timestamp: new Date().toISOString(),
16
- cwd: "",
17
- userPrompt: "",
18
- skillCount: 0,
19
- candidates: [],
20
- loadedByTool: [],
21
- loadedPolicies: [],
22
- readFiles: [],
23
- editedFiles: [],
24
- writtenFiles: [],
25
- blockedActions: [],
26
- duplicateExtensionWarnings: [],
27
- promptRewrite: {
28
- attempted: false,
29
- changed: false,
30
- originalLength: 0,
31
- rewrittenLength: 0,
32
- removedSections: [],
33
- warnings: [],
34
- },
35
- toolCalls: {
36
- skillIndex: 0,
37
- skillManifest: 0,
38
- skillLoad: 0,
39
- policyManifest: 0,
40
- policyLoad: 0,
41
- contextReport: 0,
42
- },
43
- };
44
- }
45
-
46
- export function createState(): ContextManagerState {
47
- return {
48
- skills: new Map(),
49
- policies: new Map(),
50
- loadedPolicies: new Set(),
51
- readFiles: new Set(),
52
- editedFiles: new Set(),
53
- writtenFiles: new Set(),
54
- report: createEmptyReport(),
55
- };
56
- }
57
-
58
- export function syncReportLedger(state: ContextManagerState): void {
59
- state.report.loadedPolicies = [...state.loadedPolicies].sort();
60
- state.report.readFiles = [...state.readFiles].sort();
61
- state.report.editedFiles = [...state.editedFiles].sort();
62
- state.report.writtenFiles = [...state.writtenFiles].sort();
63
- }
64
-
65
- export function recordBlocked(state: ContextManagerState, toolName: string, reason: string): void {
66
- state.report.blockedActions.push({ toolName, reason, timestamp: new Date().toISOString() });
67
- syncReportLedger(state);
68
- }
1
+ import type { ContextReport, PolicyPack, SkillRecord } from "./types";
2
+
3
+ export type ContextManagerState = {
4
+ skills: Map<string, SkillRecord>;
5
+ policies: Map<string, PolicyPack>;
6
+ loadedPolicies: Set<string>;
7
+ readFiles: Set<string>;
8
+ editedFiles: Set<string>;
9
+ writtenFiles: Set<string>;
10
+ report: ContextReport;
11
+ };
12
+
13
+ export function createEmptyReport(): ContextReport {
14
+ return {
15
+ timestamp: new Date().toISOString(),
16
+ cwd: "",
17
+ userPrompt: "",
18
+ skillCount: 0,
19
+ candidates: [],
20
+ loadedByTool: [],
21
+ loadedPolicies: [],
22
+ readFiles: [],
23
+ editedFiles: [],
24
+ writtenFiles: [],
25
+ blockedActions: [],
26
+ duplicateExtensionWarnings: [],
27
+ promptRewrite: {
28
+ attempted: false,
29
+ changed: false,
30
+ originalLength: 0,
31
+ rewrittenLength: 0,
32
+ removedSections: [],
33
+ warnings: [],
34
+ },
35
+ toolCalls: {
36
+ skillIndex: 0,
37
+ skillManifest: 0,
38
+ skillLoad: 0,
39
+ policyManifest: 0,
40
+ policyLoad: 0,
41
+ contextReport: 0,
42
+ },
43
+ };
44
+ }
45
+
46
+ export function createState(): ContextManagerState {
47
+ return {
48
+ skills: new Map(),
49
+ policies: new Map(),
50
+ loadedPolicies: new Set(),
51
+ readFiles: new Set(),
52
+ editedFiles: new Set(),
53
+ writtenFiles: new Set(),
54
+ report: createEmptyReport(),
55
+ };
56
+ }
57
+
58
+ export function syncReportLedger(state: ContextManagerState): void {
59
+ state.report.loadedPolicies = [...state.loadedPolicies].sort();
60
+ state.report.readFiles = [...state.readFiles].sort();
61
+ state.report.editedFiles = [...state.editedFiles].sort();
62
+ state.report.writtenFiles = [...state.writtenFiles].sort();
63
+ }
64
+
65
+ export function recordBlocked(state: ContextManagerState, toolName: string, reason: string): void {
66
+ state.report.blockedActions.push({ toolName, reason, timestamp: new Date().toISOString() });
67
+ syncReportLedger(state);
68
+ }
@@ -1,77 +1,77 @@
1
- export type SkillRecord = {
2
- name: string;
3
- description?: string;
4
- location?: string;
5
- source: "systemPromptOptions" | "xml" | "tool";
6
- };
7
-
8
- export type CandidateContext = {
9
- name: string;
10
- score: number;
11
- confidence: "high" | "medium";
12
- suggestedAction: "skill_load" | "skill_manifest";
13
- reasons: string[];
14
- };
15
-
16
- export type PolicyPack = {
17
- name: string;
18
- description: string;
19
- content: string;
20
- path?: string;
21
- };
22
-
23
- export type Prerequisite = { type: "policies"; policies: string[] };
24
-
25
- export type SkillIndexDisplayMode = "hidden" | "candidates" | "all-names" | "auto";
26
-
27
- export type ContextManagerConfig = {
28
- skillDisplay: {
29
- mode: SkillIndexDisplayMode;
30
- maxVisibleSkillNames: number;
31
- alwaysShowToolInstructions: boolean;
32
- };
33
- candidateRouter: {
34
- maxCandidates: number;
35
- highConfidence: number;
36
- mediumConfidence: number;
37
- };
38
- policyPaths: string[];
39
- policyFiles?: Record<string, string>;
40
- toolPrerequisites: Record<string, Prerequisite[]>;
41
- promptCompaction: {
42
- compactModelRouting: boolean;
43
- compactModelRegistry: boolean;
44
- compactSkillDescriptions: boolean;
45
- };
46
- };
47
-
48
- export type ContextReport = {
49
- timestamp: string;
50
- cwd: string;
51
- userPrompt: string;
52
- skillCount: number;
53
- candidates: CandidateContext[];
54
- loadedByTool: string[];
55
- loadedPolicies: string[];
56
- readFiles: string[];
57
- editedFiles: string[];
58
- writtenFiles: string[];
59
- blockedActions: Array<{ toolName: string; reason: string; timestamp: string }>;
60
- duplicateExtensionWarnings: Array<{ toolName: string; paths: string[] }>;
61
- promptRewrite: {
62
- attempted: boolean;
63
- changed: boolean;
64
- originalLength: number;
65
- rewrittenLength: number;
66
- removedSections: string[];
67
- warnings: string[];
68
- };
69
- toolCalls: {
70
- skillIndex: number;
71
- skillManifest: number;
72
- skillLoad: number;
73
- policyManifest: number;
74
- policyLoad: number;
75
- contextReport: number;
76
- };
77
- };
1
+ export type SkillRecord = {
2
+ name: string;
3
+ description?: string;
4
+ location?: string;
5
+ source: "systemPromptOptions" | "xml" | "tool";
6
+ };
7
+
8
+ export type CandidateContext = {
9
+ name: string;
10
+ score: number;
11
+ confidence: "high" | "medium";
12
+ suggestedAction: "skill_load" | "skill_manifest";
13
+ reasons: string[];
14
+ };
15
+
16
+ export type PolicyPack = {
17
+ name: string;
18
+ description: string;
19
+ content: string;
20
+ path?: string;
21
+ };
22
+
23
+ export type Prerequisite = { type: "policies"; policies: string[] };
24
+
25
+ export type SkillIndexDisplayMode = "hidden" | "candidates" | "all-names" | "auto";
26
+
27
+ export type ContextManagerConfig = {
28
+ skillDisplay: {
29
+ mode: SkillIndexDisplayMode;
30
+ maxVisibleSkillNames: number;
31
+ alwaysShowToolInstructions: boolean;
32
+ };
33
+ candidateRouter: {
34
+ maxCandidates: number;
35
+ highConfidence: number;
36
+ mediumConfidence: number;
37
+ };
38
+ policyPaths: string[];
39
+ policyFiles?: Record<string, string>;
40
+ toolPrerequisites: Record<string, Prerequisite[]>;
41
+ promptCompaction: {
42
+ compactModelRouting: boolean;
43
+ compactModelRegistry: boolean;
44
+ compactSkillDescriptions: boolean;
45
+ };
46
+ };
47
+
48
+ export type ContextReport = {
49
+ timestamp: string;
50
+ cwd: string;
51
+ userPrompt: string;
52
+ skillCount: number;
53
+ candidates: CandidateContext[];
54
+ loadedByTool: string[];
55
+ loadedPolicies: string[];
56
+ readFiles: string[];
57
+ editedFiles: string[];
58
+ writtenFiles: string[];
59
+ blockedActions: Array<{ toolName: string; reason: string; timestamp: string }>;
60
+ duplicateExtensionWarnings: Array<{ toolName: string; paths: string[] }>;
61
+ promptRewrite: {
62
+ attempted: boolean;
63
+ changed: boolean;
64
+ originalLength: number;
65
+ rewrittenLength: number;
66
+ removedSections: string[];
67
+ warnings: string[];
68
+ };
69
+ toolCalls: {
70
+ skillIndex: number;
71
+ skillManifest: number;
72
+ skillLoad: number;
73
+ policyManifest: number;
74
+ policyLoad: number;
75
+ contextReport: number;
76
+ };
77
+ };
@@ -16,7 +16,7 @@ const ROOT_COMPLETIONS: TakomiCompletion[] = [
16
16
  { value: "mode", label: "mode", description: "Set direct, orchestrate, or review mode" },
17
17
  { value: "gate", label: "gate", description: "Set auto or review-gated execution" },
18
18
  { value: "subagents", label: "subagents", description: "Control subagent usage and view" },
19
- { value: "routing", label: "routing", description: "Install/update Takomi model routing policy" },
19
+ { value: "routing", label: "routing", description: "Show or update Takomi model routing policy" },
20
20
  ];
21
21
 
22
22
  const SUBCOMMAND_COMPLETIONS: Record<string, TakomiCompletion[]> = {
@@ -37,6 +37,12 @@ const SUBCOMMAND_COMPLETIONS: Record<string, TakomiCompletion[]> = {
37
37
  { value: "collapse", label: "collapse", description: "Collapse native tool results" },
38
38
  { value: "toggle", label: "toggle", description: "Toggle native tool result expansion" },
39
39
  ],
40
+ routing: [
41
+ { value: "show", label: "show", description: "Show active routing policy source, path, and contents" },
42
+ { value: "global", label: "global", description: "Save following policy text globally" },
43
+ { value: "local", label: "local", description: "Save following policy text as a project override" },
44
+ { value: "where", label: "where", description: "Show where the active routing policy is loaded from" },
45
+ ],
40
46
  };
41
47
 
42
48
  const SUBCOMMAND_ALIASES: Record<string, string> = {
@@ -71,7 +77,9 @@ export function commandHelp(): string {
71
77
  "/takomi mode <direct|orchestrate|review>",
72
78
  "/takomi gate <auto|review>",
73
79
  "/takomi subagents <on|off|status|expand|collapse|toggle>",
74
- "/takomi routing <policy text>",
80
+ "/takomi routing [show|where]",
81
+ "/takomi routing <policy text> # updates global policy",
82
+ "/takomi routing local <policy text> # project override",
75
83
  "/takomi-status",
76
84
  "/takomi-reset",
77
85
  ].join("\n");
@@ -7,7 +7,7 @@ import type {
7
7
  } from "../../../src/pi-takomi-core";
8
8
  import { commandHelp, completions, statusText, workflowPrompt } from "./command-text";
9
9
  import type { TakomiSubagentController } from "./subagent-types";
10
- import { installTakomiRoutingPolicy } from "./routing-policy";
10
+ import { installTakomiRoutingPolicy, resolveTakomiRoutingPolicy, type RoutingPolicyInstallScope } from "./routing-policy";
11
11
 
12
12
  export type TakomiRuntimeCommandState = {
13
13
  enabled: boolean;
@@ -82,14 +82,87 @@ export function registerTakomiCommands(pi: ExtensionAPI, options: RegisterTakomi
82
82
  }
83
83
 
84
84
  async function handleRouting(ctx: ExtensionCommandContext, body?: string): Promise<void> {
85
- if (!body?.trim()) {
86
- ctx.ui.notify("Usage: /takomi routing <policy text>\nTip: paste the policy after the command, or say: Update Takomi routing logic: \"\"\"...\"\"\"", "warning");
85
+ const trimmed = body?.trim() ?? "";
86
+
87
+ const usage = [
88
+ "Usage:",
89
+ "/takomi routing show Print the full active policy",
90
+ "/takomi routing where Show source/path only",
91
+ "/takomi routing <policy text> Update the global policy",
92
+ "/takomi routing local <policy text> Create/update this project's override",
93
+ ].join("\n");
94
+
95
+ async function showRoutingHelp(): Promise<void> {
96
+ const resolved = await resolveTakomiRoutingPolicy(ctx.cwd);
97
+ ctx.ui.notify([
98
+ "Takomi routing options",
99
+ "",
100
+ `Active source: ${resolved.source}`,
101
+ `Active path: ${resolved.policyPath ?? "not found"}`,
102
+ "",
103
+ usage,
104
+ "",
105
+ "Resolution order: project .pi/takomi/model-routing.md → global ~/.pi/takomi/model-routing.md → bundled fallback.",
106
+ ].join("\n"), "warning");
107
+ }
108
+
109
+ async function showRoutingLocation(): Promise<void> {
110
+ const resolved = await resolveTakomiRoutingPolicy(ctx.cwd);
111
+ ctx.ui.notify([
112
+ "Active Takomi routing policy location",
113
+ "",
114
+ `Source: ${resolved.source}`,
115
+ `Path: ${resolved.policyPath ?? "not found"}`,
116
+ "",
117
+ usage,
118
+ ].join("\n"), resolved.source === "missing" ? "warning" : "info");
119
+ }
120
+
121
+ async function showActivePolicy(): Promise<void> {
122
+ const resolved = await resolveTakomiRoutingPolicy(ctx.cwd);
123
+ const text = resolved.text ?? "No Takomi routing policy found.";
124
+ const clipped = text.length > 6000 ? `${text.slice(0, 6000)}\n\n…truncated; open the file above for the full policy.` : text;
125
+ ctx.ui.notify([
126
+ "Active Takomi routing policy",
127
+ "",
128
+ `Source: ${resolved.source}`,
129
+ `Path: ${resolved.policyPath ?? "not found"}`,
130
+ "",
131
+ clipped,
132
+ "",
133
+ usage,
134
+ ].join("\n"), resolved.source === "missing" ? "warning" : "info");
135
+ }
136
+
137
+ if (!trimmed) {
138
+ await showRoutingHelp();
87
139
  return;
88
140
  }
141
+ if (/^(where|path|status)$/i.test(trimmed)) {
142
+ await showRoutingLocation();
143
+ return;
144
+ }
145
+ if (/^(show|view)$/i.test(trimmed)) {
146
+ await showActivePolicy();
147
+ return;
148
+ }
149
+
150
+ if (/^(global|local|project|set)$/i.test(trimmed)) {
151
+ ctx.ui.notify(usage, "warning");
152
+ return;
153
+ }
154
+
155
+ const scopeMatch = trimmed.match(/^(global|local|project)\s+([\s\S]+)$/i);
156
+ const scope: RoutingPolicyInstallScope = scopeMatch?.[1]?.toLowerCase() === "local" || scopeMatch?.[1]?.toLowerCase() === "project" ? "project" : "global";
157
+ const policyText = scopeMatch?.[2] ?? trimmed.replace(/^set\s+/i, "");
158
+
89
159
  try {
90
- const result = await installTakomiRoutingPolicy(ctx.cwd, body);
160
+ const result = await installTakomiRoutingPolicy(ctx.cwd, policyText, { scope });
91
161
  const detected = result.detectedDefaults.length ? `\n\nDetected defaults:\n- ${result.detectedDefaults.join("\n- ")}` : "\n\nNo model names were auto-detected; saved policy only.";
92
- ctx.ui.notify(`Takomi routing policy updated.\n\nPolicy: ${result.policyPath}\nSettings: ${result.settingsPath}${detected}`, "info");
162
+ const overrideNote = scope === "global"
163
+ ? "\n\nThis will be used by every project unless that project has its own .pi/takomi/model-routing.md override."
164
+ : "\n\nThis project-local policy overrides the global policy for the current project.";
165
+ ctx.ui.notify(`Takomi routing policy updated (${scope}).\n\nPolicy: ${result.policyPath}\nSettings: ${result.settingsPath}${detected}${overrideNote}`, "info");
93
166
  } catch (error) {
94
167
  ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
95
168
  }