takomi 2.1.6 → 2.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.pi/README.md CHANGED
@@ -8,6 +8,7 @@ It is intentionally separate from the existing cross-harness assets under `asset
8
8
 
9
9
  - `extensions/takomi-runtime/` - Pi runtime glue, embedded workflow playbooks, and orchestrator board tools
10
10
  - `extensions/takomi-subagents/` - Takomi-facing subagent wrapper over Pi-style execution semantics with resumable conversation IDs
11
+ - `extensions/takomi-context-manager/` - Progressive context loading, skill manifests, policy packs, model-routing gates, and context diagnostics
11
12
  - `prompts/` - Pi-native prompt shortcuts
12
13
  - `agents/` - Pi-native specialist agent definitions, including a design-stage agent
13
14
 
@@ -39,6 +40,7 @@ Inside Pi, use:
39
40
  - `/takomi subagents status|expand|collapse|fullscreen|next|prev|toggle` to inspect or reshape the active subagent stack
40
41
  - `/takomi-status` to show lifecycle, gate, session, and active subagent state
41
42
  - `/takomi-reset` to reset session-local Takomi runtime state
43
+ - `/context-report` to inspect prompt compaction, skill loading, policy gates, model routing corrections, and duplicate extension diagnostics
42
44
  - `takomi_board` actions now include stage expansion, task updates, multi-task dispatch, and redispatch support for review loops
43
45
  - The old standalone commands (`/takomi-genesis`, `/takomi-design`, `/takomi-build`, `/takomi-kickoff`, `/autoorch`, `/orch`, `/architect`, `/code`, `/review`, and the `/takomi-subagent*` variants) are folded into `/takomi` subcommands so slash autocomplete stays small.
44
46
  - prompt shortcuts are suffixed with `-prompt` to avoid collisions with runtime commands, e.g. `/orch-prompt`, `/build-prompt`, `/design-prompt`, `/genesis-prompt`, `/takomi-prompt`, `/prime-prompt`
@@ -67,6 +69,7 @@ Bundled with Takomi now:
67
69
 
68
70
  - `.pi/extensions/takomi-runtime/`
69
71
  - `.pi/extensions/takomi-subagents/`
72
+ - `.pi/extensions/takomi-context-manager/`
70
73
  - `.pi/prompts/`
71
74
  - `.pi/agents/`
72
75
  - `.pi/themes/`
@@ -112,6 +115,11 @@ So when working on packaging, agents should distinguish between:
112
115
  - Active Takomi subagent work now streams through the native Pi-style result UI instead of Takomi's older below-editor stack.
113
116
  - Use Pi's native result expansion, `Alt+T`, or `/takomi subagents expand` to inspect detailed subagent output.
114
117
  - Takomi still tracks active runs internally for status, review continuity, and board synchronization, but it no longer opens a custom subagent fullscreen overlay.
118
+ - `takomi-context-manager` reduces prompt bloat by replacing the always-on skill description dump with a names-only Skill Index plus progressive `skill_manifest`/`skill_load` tools.
119
+ - `takomi-context-manager` treats `/takomi routing` as the source of truth for model-routing policy via `.pi/settings.json -> takomi.modelRoutingPolicyFile`.
120
+ - `takomi-context-manager` gates `takomi_subagent` when model-routing context has not been loaded, provides the routing policy, and tells the agent to retry.
121
+ - `takomi-context-manager` can correct safe wrong-provider model requests, block or pause on policy violations, and ask the user whether to retry with an approved model or stop.
122
+ - `takomi-context-manager` detects known duplicate global/project Takomi extension paths in `context_report` to help diagnose tool registration conflicts.
115
123
  - `takomi_board` can:
116
124
  - create a Genesis-first lifecycle session by default
117
125
  - expand a lifecycle stage into additional tasks
@@ -0,0 +1,42 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import type { ContextManagerConfig } from "./types";
4
+
5
+ export const DEFAULT_CONFIG: ContextManagerConfig = {
6
+ candidateRouter: {
7
+ maxCandidates: 5,
8
+ highConfidence: 100,
9
+ mediumConfidence: 40,
10
+ },
11
+ policyPaths: [".pi/takomi", ".pi/takomi/policies"],
12
+ toolPrerequisites: {
13
+ takomi_subagent: [{ type: "policies", policies: ["model-routing"] }],
14
+ },
15
+ promptCompaction: {
16
+ compactModelRouting: true,
17
+ compactModelRegistry: true,
18
+ compactSkillDescriptions: true,
19
+ },
20
+ };
21
+
22
+ function mergeConfig(value: Partial<ContextManagerConfig>): ContextManagerConfig {
23
+ return {
24
+ ...DEFAULT_CONFIG,
25
+ ...value,
26
+ candidateRouter: { ...DEFAULT_CONFIG.candidateRouter, ...value.candidateRouter },
27
+ promptCompaction: { ...DEFAULT_CONFIG.promptCompaction, ...value.promptCompaction },
28
+ policyPaths: value.policyPaths ?? DEFAULT_CONFIG.policyPaths,
29
+ policyFiles: value.policyFiles,
30
+ toolPrerequisites: value.toolPrerequisites ?? DEFAULT_CONFIG.toolPrerequisites,
31
+ };
32
+ }
33
+
34
+ export async function loadConfig(cwd: string): Promise<ContextManagerConfig> {
35
+ const configPath = path.resolve(cwd, ".pi/takomi/context-manager/config.json");
36
+ try {
37
+ const raw = await readFile(configPath, "utf8");
38
+ return mergeConfig(JSON.parse(raw) as Partial<ContextManagerConfig>);
39
+ } catch {
40
+ return DEFAULT_CONFIG;
41
+ }
42
+ }
@@ -0,0 +1,57 @@
1
+ import type { CandidateContext, ContextManagerConfig, SkillRecord } from "./types";
2
+ import { normalizeName, normalizeText, sortedSkills } from "./skill-registry";
3
+
4
+ const STOPWORDS = new Set(["a", "an", "and", "are", "as", "for", "from", "in", "is", "it", "of", "on", "or", "the", "this", "to", "with", "when", "you"]);
5
+
6
+ function meaningfulWords(value: string): string[] {
7
+ return normalizeText(value).split(" ").filter((word) => word.length >= 3 && !STOPWORDS.has(word));
8
+ }
9
+
10
+ function scoreSkill(prompt: string, skill: SkillRecord, config: ContextManagerConfig): CandidateContext | undefined {
11
+ const promptNorm = normalizeText(prompt);
12
+ if (!promptNorm) return undefined;
13
+ const reasons: string[] = [];
14
+ let score = 0;
15
+ const nameNorm = normalizeText(skill.name);
16
+ if (nameNorm && promptNorm.includes(nameNorm)) {
17
+ score += 100;
18
+ reasons.push("exact skill name match");
19
+ }
20
+ const nameWords = meaningfulWords(skill.name);
21
+ const matchedNameWords = nameWords.filter((word) => promptNorm.includes(word));
22
+ if (matchedNameWords.length > 0 && matchedNameWords.length === nameWords.length) {
23
+ score += 50;
24
+ reasons.push(`matched skill name words: ${matchedNameWords.join(", ")}`);
25
+ } else if (matchedNameWords.length > 0) {
26
+ score += 15 * matchedNameWords.length;
27
+ reasons.push(`partial skill name words: ${matchedNameWords.join(", ")}`);
28
+ }
29
+ const descriptionWords = meaningfulWords(skill.description ?? "").slice(0, 50);
30
+ const matchedDescriptionWords = [...new Set(descriptionWords.filter((word) => promptNorm.includes(word)))];
31
+ if (matchedDescriptionWords.length > 0) {
32
+ score += Math.min(50, matchedDescriptionWords.length * 10);
33
+ reasons.push(`description keywords: ${matchedDescriptionWords.slice(0, 5).join(", ")}`);
34
+ }
35
+ if (score < config.candidateRouter.mediumConfidence) return undefined;
36
+ const confidence = score >= config.candidateRouter.highConfidence ? "high" : "medium";
37
+ return {
38
+ name: skill.name,
39
+ score,
40
+ confidence,
41
+ suggestedAction: confidence === "high" ? "skill_load" : "skill_manifest",
42
+ reasons,
43
+ };
44
+ }
45
+
46
+ export function findCandidates(prompt: string, skills: Map<string, SkillRecord>, config: ContextManagerConfig): CandidateContext[] {
47
+ return sortedSkills(skills)
48
+ .map((skill) => scoreSkill(prompt, skill, config))
49
+ .filter((candidate): candidate is CandidateContext => Boolean(candidate))
50
+ .sort((a, b) => b.score - a.score || a.name.localeCompare(b.name))
51
+ .slice(0, config.candidateRouter.maxCandidates);
52
+ }
53
+
54
+ export function renderCandidateHint(candidates: CandidateContext[]): string {
55
+ if (candidates.length === 0) return "";
56
+ return ["Potentially relevant skills:", ...candidates.map((candidate) => `- ${candidate.name} — use ${candidate.suggestedAction} if relevant`)].join("\n");
57
+ }
@@ -0,0 +1,26 @@
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ import { Type } from "typebox";
3
+ import type { ContextManagerState } from "./state";
4
+ import { renderReport } from "./diagnostics";
5
+
6
+ export function registerDiagnostics(pi: ExtensionAPI, state: ContextManagerState): void {
7
+ pi.registerTool({
8
+ name: "context_report",
9
+ label: "Context Report",
10
+ description: "Show takomi-context-manager diagnostics for prompt rewriting, candidates, policies, gates, and progressive skill loading.",
11
+ promptSnippet: "Show context manager diagnostics and prompt composition decisions",
12
+ parameters: Type.Object({ verbose: Type.Optional(Type.Boolean({ description: "Include skill and policy indexes in the report" })) }),
13
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
14
+ state.report.cwd = ctx.cwd;
15
+ state.report.toolCalls.contextReport += 1;
16
+ return { content: [{ type: "text", text: renderReport(state, params.verbose ?? false) }], details: state.report };
17
+ },
18
+ });
19
+
20
+ pi.registerCommand("context-report", {
21
+ description: "Show takomi-context-manager diagnostics",
22
+ handler: async (_args, ctx) => {
23
+ ctx.ui.notify(renderReport(state, true), "info");
24
+ },
25
+ });
26
+ }
@@ -0,0 +1,53 @@
1
+ import type { ContextManagerState } from "./state";
2
+ import { sortedSkills } from "./skill-registry";
3
+ import { syncReportLedger } from "./state";
4
+ import { renderDuplicateExtensionGuidance } from "./extension-conflicts";
5
+
6
+ export function renderReport(state: ContextManagerState, verbose = false): string {
7
+ syncReportLedger(state);
8
+ const report = state.report;
9
+ const reduction = report.promptRewrite.originalLength > 0
10
+ ? Math.round((1 - report.promptRewrite.rewrittenLength / report.promptRewrite.originalLength) * 1000) / 10
11
+ : 0;
12
+ const lines = [
13
+ "Takomi Context Manager Report",
14
+ `- Timestamp: ${report.timestamp}`,
15
+ `- CWD: ${report.cwd || "(unknown)"}`,
16
+ `- Skill count: ${report.skillCount}`,
17
+ `- Prompt rewrite attempted: ${report.promptRewrite.attempted ? "yes" : "no"}`,
18
+ `- Prompt changed: ${report.promptRewrite.changed ? "yes" : "no"}`,
19
+ `- Original prompt length: ${report.promptRewrite.originalLength} chars`,
20
+ `- Rewritten prompt length: ${report.promptRewrite.rewrittenLength} chars`,
21
+ `- Reduction: ${reduction}%`,
22
+ `- Removed sections: ${report.promptRewrite.removedSections.join(", ") || "none"}`,
23
+ `- Loaded skills: ${report.loadedByTool.join(", ") || "none"}`,
24
+ `- Loaded policies: ${report.loadedPolicies.join(", ") || "none"}`,
25
+ `- Read files: ${report.readFiles.length}`,
26
+ `- Edited files: ${report.editedFiles.length}`,
27
+ `- Written files: ${report.writtenFiles.length}`,
28
+ `- Blocked actions: ${report.blockedActions.length}`,
29
+ `- Duplicate extension warnings: ${report.duplicateExtensionWarnings.length}`,
30
+ `- Tool calls: skill_index=${report.toolCalls.skillIndex}, skill_manifest=${report.toolCalls.skillManifest}, skill_load=${report.toolCalls.skillLoad}, policy_manifest=${report.toolCalls.policyManifest}, policy_load=${report.toolCalls.policyLoad}, context_report=${report.toolCalls.contextReport}`,
31
+ `- Warnings: ${report.promptRewrite.warnings.join("; ") || "none"}`,
32
+ ];
33
+ if (report.candidates.length > 0) {
34
+ lines.push("- Candidates:");
35
+ for (const candidate of report.candidates) {
36
+ lines.push(` - ${candidate.name} (${candidate.confidence}, ${candidate.score}): ${candidate.reasons.join("; ")}`);
37
+ }
38
+ } else {
39
+ lines.push("- Candidates: none");
40
+ }
41
+ if (report.duplicateExtensionWarnings.length > 0 || verbose) {
42
+ lines.push("", "Extension Conflict Diagnostics:", ...renderDuplicateExtensionGuidance(report.duplicateExtensionWarnings));
43
+ }
44
+ if (verbose) {
45
+ lines.push("", "Skill Index:", ...sortedSkills(state.skills).map((skill) => `- ${skill.name}`));
46
+ lines.push("", "Policy Packs:", ...[...state.policies.values()].map((policy) => `- ${policy.name}: ${policy.description}`));
47
+ if (report.blockedActions.length > 0) {
48
+ lines.push("", "Blocked Actions:");
49
+ for (const action of report.blockedActions.slice(-10)) lines.push(`- ${action.timestamp} ${action.toolName}: ${action.reason}`);
50
+ }
51
+ }
52
+ return lines.join("\n");
53
+ }
@@ -0,0 +1,56 @@
1
+ import { access } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import type { ContextManagerState } from "./state";
5
+
6
+ type KnownTakomiExtension = {
7
+ toolName: string;
8
+ relativePath: string;
9
+ };
10
+
11
+ const KNOWN_TAKOMI_EXTENSIONS: KnownTakomiExtension[] = [
12
+ { toolName: "takomi_workflow", relativePath: path.join("takomi-runtime", "index.ts") },
13
+ { toolName: "takomi_board", relativePath: path.join("takomi-runtime", "index.ts") },
14
+ { toolName: "takomi_subagent", relativePath: path.join("takomi-subagents", "index.ts") },
15
+ ];
16
+
17
+ async function exists(filePath: string): Promise<boolean> {
18
+ try {
19
+ await access(filePath);
20
+ return true;
21
+ } catch {
22
+ return false;
23
+ }
24
+ }
25
+
26
+ function homePiExtensionsRoot(): string {
27
+ return path.join(os.homedir(), ".pi", "agent", "extensions");
28
+ }
29
+
30
+ export async function detectDuplicateTakomiExtensions(cwd: string): Promise<Array<{ toolName: string; paths: string[] }>> {
31
+ const globalRoot = homePiExtensionsRoot();
32
+ const projectRoot = path.resolve(cwd, ".pi", "extensions");
33
+ const warnings: Array<{ toolName: string; paths: string[] }> = [];
34
+
35
+ for (const known of KNOWN_TAKOMI_EXTENSIONS) {
36
+ const globalPath = path.join(globalRoot, known.relativePath);
37
+ const projectPath = path.join(projectRoot, known.relativePath);
38
+ if (await exists(globalPath) && await exists(projectPath)) {
39
+ warnings.push({ toolName: known.toolName, paths: [globalPath, projectPath] });
40
+ }
41
+ }
42
+
43
+ return warnings;
44
+ }
45
+
46
+ export function renderDuplicateExtensionGuidance(warnings: Array<{ toolName: string; paths: string[] }>): string[] {
47
+ if (warnings.length === 0) return ["- Duplicate Takomi extensions: none detected"];
48
+ const lines = ["- Duplicate Takomi extensions detected:"];
49
+ for (const warning of warnings) {
50
+ lines.push(` - ${warning.toolName}`);
51
+ for (const filePath of warning.paths) lines.push(` - ${filePath}`);
52
+ }
53
+ lines.push("- Recommended dev command: use scripts/pi-dev.ps1, which starts Pi with --no-extensions and explicit project-local Takomi extensions.");
54
+ lines.push("- For global Pi sessions, disable/remove one duplicate source or prefer explicit extension loading to avoid tool registration conflicts.");
55
+ return lines;
56
+ }
@@ -0,0 +1,56 @@
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ import { loadConfig, DEFAULT_CONFIG } from "./config";
3
+ import { createState } from "./state";
4
+ import { collectSkillsFromOptions, collectSkillsFromXml, mergeSkills } from "./skill-registry";
5
+ import { discoverPolicies } from "./policy-registry";
6
+ import { findCandidates } from "./context-router";
7
+ import { rewritePrompt } from "./prompt-rewriter";
8
+ import { registerSkillTools } from "./skill-tools";
9
+ import { registerPolicyTools } from "./policy-tools";
10
+ import { registerDiagnostics } from "./diagnostics-tools";
11
+ import { installPrerequisiteGates } from "./prerequisite-gates";
12
+ import { installModelPolicyGate } from "./model-policy-gate";
13
+ import { detectDuplicateTakomiExtensions } from "./extension-conflicts";
14
+ import type { ContextManagerConfig } from "./types";
15
+
16
+ export default function takomiContextManager(pi: ExtensionAPI) {
17
+ const state = createState();
18
+ let config: ContextManagerConfig = DEFAULT_CONFIG;
19
+
20
+ registerSkillTools(pi, state);
21
+ registerPolicyTools(pi, state);
22
+ registerDiagnostics(pi, state);
23
+ installPrerequisiteGates(pi, state, () => config);
24
+ installModelPolicyGate(pi, state);
25
+
26
+ pi.on("before_agent_start", async (event, ctx) => {
27
+ config = await loadConfig(ctx.cwd);
28
+ state.policies = await discoverPolicies(ctx.cwd, config);
29
+ const duplicateExtensionWarnings = await detectDuplicateTakomiExtensions(ctx.cwd);
30
+ const optionSkills = collectSkillsFromOptions(event.systemPromptOptions);
31
+ const xmlSkills = collectSkillsFromXml(event.systemPrompt);
32
+ state.skills = mergeSkills([...optionSkills, ...xmlSkills]);
33
+
34
+ const candidates = findCandidates(event.prompt, state.skills, config);
35
+ const rewrite = rewritePrompt(event.systemPrompt, state.skills, candidates, config);
36
+ state.report = {
37
+ ...state.report,
38
+ timestamp: new Date().toISOString(),
39
+ cwd: ctx.cwd,
40
+ userPrompt: event.prompt,
41
+ skillCount: state.skills.size,
42
+ candidates,
43
+ duplicateExtensionWarnings,
44
+ promptRewrite: {
45
+ attempted: true,
46
+ changed: rewrite.changed,
47
+ originalLength: event.systemPrompt.length,
48
+ rewrittenLength: rewrite.prompt.length,
49
+ removedSections: rewrite.removedSections,
50
+ warnings: rewrite.warnings,
51
+ },
52
+ };
53
+
54
+ return { systemPrompt: rewrite.prompt };
55
+ });
56
+ }
@@ -0,0 +1,213 @@
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 { ContextManagerState } from "./state";
5
+ import { recordBlocked } from "./state";
6
+
7
+ type Settings = {
8
+ takomi?: { modelRoutingPolicyFile?: string };
9
+ subagents?: { agentOverrides?: Record<string, unknown> };
10
+ };
11
+
12
+ type ModelPolicySnapshot = {
13
+ approvedModels: string[];
14
+ preferredModels: string[];
15
+ sourceFiles: string[];
16
+ };
17
+
18
+ function asRecord(value: unknown): Record<string, unknown> {
19
+ return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
20
+ }
21
+
22
+ async function readSettings(cwd: string): Promise<Settings> {
23
+ try {
24
+ return JSON.parse(await readFile(path.resolve(cwd, ".pi/settings.json"), "utf8")) as Settings;
25
+ } catch {
26
+ return {};
27
+ }
28
+ }
29
+
30
+ function modelFamily(model: string): string {
31
+ return model.split("/").at(-1)?.toLowerCase() ?? model.toLowerCase();
32
+ }
33
+
34
+ function unique(values: string[]): string[] {
35
+ return [...new Set(values.filter(Boolean))];
36
+ }
37
+
38
+ function collectModelsFromSettings(settings: Settings): string[] {
39
+ const overrides = asRecord(settings.subagents?.agentOverrides);
40
+ const models: string[] = [];
41
+ for (const value of Object.values(overrides)) {
42
+ const record = asRecord(value);
43
+ if (typeof record.model === "string") models.push(record.model);
44
+ if (Array.isArray(record.fallbackModels)) {
45
+ for (const fallback of record.fallbackModels) if (typeof fallback === "string") models.push(fallback.split(":")[0]);
46
+ }
47
+ }
48
+ return models;
49
+ }
50
+
51
+ function isModelLike(value: string): boolean {
52
+ const lower = value.toLowerCase();
53
+ return /(^|\/)(gpt|claude|gemini|o[0-9]|qwen|deepseek|llama|mistral|kimi|grok|sonnet|haiku|opus|codex|mini|max)/i.test(lower)
54
+ || lower.includes("oauth-router/")
55
+ || lower.includes("openai-codex/")
56
+ || lower.includes("lmstudio/");
57
+ }
58
+
59
+ function collectModelsFromPolicy(text: string): string[] {
60
+ const explicit = (text.match(/[a-z0-9-]+\/[a-z0-9._-]+/gi) ?? []).filter(isModelLike);
61
+ const inferred: string[] = [];
62
+ if (/gpt[- ]?5\.5/i.test(text)) inferred.push("oauth-router/gpt-5.5");
63
+ if (/gpt[- ]?5\.4(?!\s*mini)/i.test(text)) inferred.push("oauth-router/gpt-5.4");
64
+ if (/gpt[- ]?5\.4\s*mini/i.test(text)) inferred.push("oauth-router/gpt-5.4-mini");
65
+ return unique([...explicit, ...inferred]);
66
+ }
67
+
68
+ async function loadSnapshot(cwd: string): Promise<ModelPolicySnapshot> {
69
+ const settings = await readSettings(cwd);
70
+ const settingsModels = collectModelsFromSettings(settings);
71
+ const sourceFiles: string[] = [];
72
+ let policyModels: string[] = [];
73
+ const configured = settings.takomi?.modelRoutingPolicyFile ?? ".pi/takomi/model-routing.md";
74
+ const policyPath = path.isAbsolute(configured) ? configured : path.resolve(cwd, configured);
75
+ try {
76
+ const text = await readFile(policyPath, "utf8");
77
+ sourceFiles.push(policyPath);
78
+ policyModels = collectModelsFromPolicy(text);
79
+ } catch {
80
+ // Routing policy may not exist yet.
81
+ }
82
+ const approvedModels = unique([...settingsModels, ...policyModels]);
83
+ return { approvedModels, preferredModels: settingsModels.length ? unique(settingsModels) : approvedModels, sourceFiles };
84
+ }
85
+
86
+ function collectRequestedModelRefs(input: unknown): Array<{ holder: Record<string, unknown>; key: string; value: string }> {
87
+ const refs: Array<{ holder: Record<string, unknown>; key: string; value: string }> = [];
88
+ function visit(value: unknown): void {
89
+ if (!value || typeof value !== "object") return;
90
+ if (Array.isArray(value)) {
91
+ for (const item of value) visit(item);
92
+ return;
93
+ }
94
+ const record = value as Record<string, unknown>;
95
+ for (const key of ["model", "preferredModel"]) {
96
+ if (typeof record[key] === "string") refs.push({ holder: record, key, value: record[key] });
97
+ }
98
+ if (Array.isArray(record.fallbackModels)) {
99
+ record.fallbackModels = record.fallbackModels.filter((item) => typeof item === "string");
100
+ }
101
+ for (const key of ["tasks", "chain"]) visit(record[key]);
102
+ }
103
+ visit(input);
104
+ return refs;
105
+ }
106
+
107
+ function approvedEquivalent(requested: string, approved: string[]): string | undefined {
108
+ const requestedFamily = modelFamily(requested);
109
+ return approved.find((candidate) => modelFamily(candidate) === requestedFamily);
110
+ }
111
+
112
+ function isModelFailure(text: string): boolean {
113
+ return /(unknown provider|provider.*not.*found|model.*not.*found|model.*unavailable|invalid model|unsupported model|auth|unauthorized|forbidden|rate limit|429|quota|context window|maximum context)/i.test(text);
114
+ }
115
+
116
+ function renderPolicyViolation(requested: string, approved: string[]): string {
117
+ return [
118
+ "Blocked by Takomi routing policy.",
119
+ "",
120
+ "Requested model:",
121
+ requested,
122
+ "",
123
+ "Allowed/preferred models include:",
124
+ ...(approved.length ? approved.map((model) => `- ${model}`) : ["- none discovered; run /takomi routing <policy text> first"]),
125
+ "",
126
+ "The subagent did not run. Ask the user how to proceed or retry with an approved model.",
127
+ ].join("\n");
128
+ }
129
+
130
+ type RecoveryChoice =
131
+ | { action: "retry"; model: string }
132
+ | { action: "stop" };
133
+
134
+ async function askForInvalidModelRecovery(ctx: { ui: { select(title: string, options: string[]): Promise<string | undefined>; notify(message: string, level?: string): void }; abort?: () => void }, requested: string, approved: string[]): Promise<RecoveryChoice> {
135
+ if (approved.length === 0) return { action: "stop" };
136
+ const options = [
137
+ ...approved.map((model) => `Retry with ${model}`),
138
+ "Stop and let me send a new prompt",
139
+ ];
140
+ const choice = await ctx.ui.select(
141
+ `takomi_subagent requested a model outside your routing policy: ${requested}`,
142
+ options,
143
+ );
144
+ if (choice?.startsWith("Retry with ")) return { action: "retry", model: choice.replace("Retry with ", "") };
145
+ return { action: "stop" };
146
+ }
147
+
148
+ export function installModelPolicyGate(pi: ExtensionAPI, state: ContextManagerState): void {
149
+ pi.on("tool_call", async (event, ctx) => {
150
+ if (event.toolName !== "takomi_subagent") return;
151
+ const snapshot = await loadSnapshot(ctx.cwd);
152
+ const approved = snapshot.approvedModels;
153
+ if (approved.length === 0) return;
154
+
155
+ const refs = collectRequestedModelRefs(event.input);
156
+ const corrections: string[] = [];
157
+ for (const ref of refs) {
158
+ if (approved.includes(ref.value)) continue;
159
+ const equivalent = approvedEquivalent(ref.value, approved);
160
+ if (equivalent) {
161
+ ref.holder[ref.key] = equivalent;
162
+ corrections.push(`${ref.value} -> ${equivalent}`);
163
+ continue;
164
+ }
165
+ const recovery = await askForInvalidModelRecovery(ctx, ref.value, approved);
166
+ if (recovery.action === "retry") {
167
+ ref.holder[ref.key] = recovery.model;
168
+ corrections.push(`${ref.value} -> ${recovery.model} (user selected recovery)`);
169
+ continue;
170
+ }
171
+ const reason = [
172
+ renderPolicyViolation(ref.value, approved),
173
+ "",
174
+ "Human selected stop. The agent turn has been aborted; wait for the user's next prompt.",
175
+ ].join("\n");
176
+ recordBlocked(state, event.toolName, reason);
177
+ ctx.abort?.();
178
+ return { block: true, reason };
179
+ }
180
+
181
+ if (corrections.length > 0) {
182
+ ctx.ui.notify(`Takomi context manager corrected subagent model routing:\n- ${corrections.join("\n- ")}\n\nBe careful to follow /takomi routing policy next time.`, "warning");
183
+ }
184
+ });
185
+
186
+ pi.on("tool_result", async (event, ctx) => {
187
+ if (event.toolName !== "takomi_subagent" || !event.isError) return;
188
+ const content = JSON.stringify(event.content ?? "");
189
+ if (!isModelFailure(content)) return;
190
+
191
+ const snapshot = await loadSnapshot(ctx.cwd);
192
+ const options = [
193
+ ...snapshot.approvedModels.map((model) => `Retry with ${model}`),
194
+ "Stop and let me send a new prompt",
195
+ ];
196
+ const choice = await ctx.ui.select("Takomi subagent model/provider failure. How do you want to continue?", options);
197
+ const retryModel = choice?.startsWith("Retry with ") ? choice.replace("Retry with ", "") : undefined;
198
+ const stopped = !retryModel;
199
+ const guidance = [
200
+ "Takomi subagent failed with a model/provider-related error.",
201
+ "",
202
+ `Policy source: ${snapshot.sourceFiles.join(", ") || "not found"}`,
203
+ "Policy-approved models:",
204
+ ...(snapshot.approvedModels.length ? snapshot.approvedModels.map((model) => `- ${model}`) : ["- none discovered"]),
205
+ "",
206
+ stopped
207
+ ? "Human selected stop/no retry. The agent turn has been aborted; wait for the user's next prompt."
208
+ : `User selected retry with ${retryModel}. Retry takomi_subagent with model ${retryModel}.`,
209
+ ].join("\n");
210
+ if (stopped) ctx.abort?.();
211
+ return { content: [{ type: "text", text: guidance }], isError: true, terminate: stopped };
212
+ });
213
+ }
@@ -0,0 +1,100 @@
1
+ import { readdir, readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import type { ContextManagerConfig, PolicyPack } from "./types";
4
+ import { normalizeName } from "./skill-registry";
5
+
6
+ function descriptionFromMarkdown(content: string): string {
7
+ const lines = content.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
8
+ const firstBody = lines.find((line) => !line.startsWith("#"));
9
+ return firstBody?.slice(0, 240) ?? "Policy pack";
10
+ }
11
+
12
+ async function readPolicyFile(name: string, filePath: string): Promise<PolicyPack | undefined> {
13
+ try {
14
+ const content = await readFile(filePath, "utf8");
15
+ return {
16
+ name,
17
+ description: descriptionFromMarkdown(content),
18
+ content,
19
+ path: filePath,
20
+ };
21
+ } catch {
22
+ return undefined;
23
+ }
24
+ }
25
+
26
+ function addPolicy(policies: Map<string, PolicyPack>, policy: PolicyPack | undefined, override = false): void {
27
+ if (!policy) return;
28
+ const key = normalizeName(policy.name);
29
+ if (!override && policies.has(key)) return;
30
+ policies.set(key, policy);
31
+ }
32
+
33
+ async function discoverTakomiSettingsPolicy(cwd: string): Promise<PolicyPack | undefined> {
34
+ try {
35
+ const settingsPath = path.resolve(cwd, ".pi/settings.json");
36
+ const settings = JSON.parse(await readFile(settingsPath, "utf8")) as {
37
+ takomi?: { modelRoutingPolicyFile?: string };
38
+ };
39
+ const modelRoutingPolicyFile = settings.takomi?.modelRoutingPolicyFile;
40
+ if (!modelRoutingPolicyFile) return undefined;
41
+ return readPolicyFile("model-routing", path.resolve(cwd, modelRoutingPolicyFile));
42
+ } catch {
43
+ return undefined;
44
+ }
45
+ }
46
+
47
+ async function discoverExplicitPolicyFiles(cwd: string, config: ContextManagerConfig): Promise<PolicyPack[]> {
48
+ const entries = Object.entries(config.policyFiles ?? {});
49
+ const policies = await Promise.all(entries.map(([name, filePath]) => readPolicyFile(name, path.resolve(cwd, filePath))));
50
+ return policies.filter((policy): policy is PolicyPack => Boolean(policy));
51
+ }
52
+
53
+ export async function discoverPolicies(cwd: string, config: ContextManagerConfig): Promise<Map<string, PolicyPack>> {
54
+ const policies = new Map<string, PolicyPack>();
55
+
56
+ // Source-of-truth priority 1: explicit context-manager policy file mappings.
57
+ for (const policy of await discoverExplicitPolicyFiles(cwd, config)) {
58
+ addPolicy(policies, policy, true);
59
+ }
60
+
61
+ // Source-of-truth priority 2: Takomi's own routing artifact created by `/takomi routing`.
62
+ // The context manager must consume this file, not invent/own model routing policy text.
63
+ addPolicy(policies, await discoverTakomiSettingsPolicy(cwd), true);
64
+
65
+ // Source-of-truth priority 3: discovered markdown policy packs. These are supplemental/default packs.
66
+ for (const policyPath of config.policyPaths) {
67
+ const absolute = path.resolve(cwd, policyPath);
68
+ try {
69
+ const entries = await readdir(absolute, { withFileTypes: true });
70
+ for (const entry of entries) {
71
+ if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
72
+ const name = path.basename(entry.name, ".md");
73
+ addPolicy(policies, await readPolicyFile(name, path.join(absolute, entry.name)), false);
74
+ }
75
+ } catch {
76
+ // Optional policy directories may not exist.
77
+ }
78
+ }
79
+
80
+ return policies;
81
+ }
82
+
83
+ export function renderPolicyManifest(policies: Map<string, PolicyPack>, names: string[]): string {
84
+ const selected = names.length > 0 ? names : [...policies.keys()];
85
+ return selected.map((name) => {
86
+ const policy = policies.get(normalizeName(name));
87
+ if (!policy) return `Policy not found: ${name}`;
88
+ return [`Policy: ${policy.name}`, `Description: ${policy.description}`, `Location: ${policy.path ?? "(generated/default)"}`].join("\n");
89
+ }).join("\n\n");
90
+ }
91
+
92
+ export function renderPolicies(policies: Map<string, PolicyPack>, loaded: Set<string>, names: string[]): string {
93
+ if (names.length === 0) return "No policies requested.";
94
+ return names.map((name) => {
95
+ const policy = policies.get(normalizeName(name));
96
+ if (!policy) return `Policy not found: ${name}`;
97
+ loaded.add(policy.name);
98
+ return [`Policy: ${policy.name}`, `Description: ${policy.description}`, `Location: ${policy.path ?? "(generated/default)"}`, "", policy.content].join("\n");
99
+ }).join("\n\n---\n\n");
100
+ }
@@ -0,0 +1,35 @@
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ import { Type } from "typebox";
3
+ import type { ContextManagerState } from "./state";
4
+ import { syncReportLedger } from "./state";
5
+ import { renderPolicies, renderPolicyManifest } from "./policy-registry";
6
+
7
+ export function registerPolicyTools(pi: ExtensionAPI, state: ContextManagerState): void {
8
+ pi.registerTool({
9
+ name: "policy_manifest",
10
+ label: "Policy Manifest",
11
+ description: "Return descriptions for available context policy packs without loading full policy content.",
12
+ promptSnippet: "Show available context policy pack descriptions",
13
+ parameters: Type.Object({ policies: Type.Optional(Type.Array(Type.String({ description: "Policy name to inspect" }))) }),
14
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
15
+ state.report.cwd = ctx.cwd;
16
+ state.report.toolCalls.policyManifest += 1;
17
+ return { content: [{ type: "text", text: renderPolicyManifest(state.policies, params.policies ?? []) }], details: { requested: params.policies ?? [...state.policies.keys()] } };
18
+ },
19
+ });
20
+
21
+ pi.registerTool({
22
+ name: "policy_load",
23
+ label: "Policy Load",
24
+ description: "Load one or more context policy packs required before sensitive tools such as takomi_subagent.",
25
+ promptSnippet: "Load policy packs required before sensitive tool calls",
26
+ parameters: Type.Object({ policies: Type.Array(Type.String({ description: "Policy pack name to load" })) }),
27
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
28
+ state.report.cwd = ctx.cwd;
29
+ state.report.toolCalls.policyLoad += 1;
30
+ const text = renderPolicies(state.policies, state.loadedPolicies, params.policies);
31
+ syncReportLedger(state);
32
+ return { content: [{ type: "text", text }], details: { requested: params.policies, loadedPolicies: [...state.loadedPolicies].sort() } };
33
+ },
34
+ });
35
+ }
@@ -0,0 +1,39 @@
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ import type { ContextManagerConfig } from "./types";
3
+ import type { ContextManagerState } from "./state";
4
+ import { recordBlocked, syncReportLedger } from "./state";
5
+ import { renderPolicies } from "./policy-registry";
6
+
7
+ function renderPolicyGateBlock(toolName: string, missing: string[], policyText: string): string {
8
+ return [
9
+ `Blocked ${toolName}: required policy context had not been loaded yet.`,
10
+ "",
11
+ "The required policy context is provided below and has now been marked as loaded for this session.",
12
+ "Retry the original tool call now, following the policy.",
13
+ "",
14
+ "Required policies:",
15
+ ...missing.map((policy) => `- ${policy}`),
16
+ "",
17
+ "Loaded policy context:",
18
+ policyText,
19
+ ].join("\n");
20
+ }
21
+
22
+ export function installPrerequisiteGates(pi: ExtensionAPI, state: ContextManagerState, getConfig: () => ContextManagerConfig): void {
23
+ pi.on("tool_call", async (event, ctx) => {
24
+ state.report.cwd = ctx.cwd;
25
+ const prereqs = getConfig().toolPrerequisites[event.toolName] ?? [];
26
+
27
+ for (const prereq of prereqs) {
28
+ if (prereq.type !== "policies") continue;
29
+ const missing = prereq.policies.filter((policy) => !state.loadedPolicies.has(policy));
30
+ if (missing.length === 0) continue;
31
+
32
+ const policyText = renderPolicies(state.policies, state.loadedPolicies, missing);
33
+ syncReportLedger(state);
34
+ const reason = renderPolicyGateBlock(event.toolName, missing, policyText);
35
+ recordBlocked(state, event.toolName, reason);
36
+ return { block: true, reason };
37
+ }
38
+ });
39
+ }
@@ -0,0 +1,81 @@
1
+ import type { CandidateContext, ContextManagerConfig, SkillRecord } from "./types";
2
+ import { renderCandidateHint } from "./context-router";
3
+ import { sortedSkills } from "./skill-registry";
4
+
5
+ function renderSkillIndex(skills: SkillRecord[]): string {
6
+ if (skills.length === 0) return "Skills: none discovered.";
7
+ return `Skills: ${skills.length} discovered. Use skill_index only when the task may need a skill.`;
8
+ }
9
+
10
+ function renderProgressiveRule(): string {
11
+ return [
12
+ "Skill loading:",
13
+ "- Skills are optional capability packs that give you special instructions/tools for specialized, repetitive tasks.",
14
+ "- Do not preload skill descriptions into the prompt.",
15
+ "- When doing specialized work, you may check whether a suited skill exists with skill_index.",
16
+ "- For uncertain matches, request skill_manifest for likely skills; manifests include descriptions and locations.",
17
+ "- If a skill is clearly relevant or the user names it directly, use skill_load without requesting a manifest first.",
18
+ "- Load full skill instructions only for skills you will actually use.",
19
+ "",
20
+ "Policy loading:",
21
+ "- Model/subagent/lifecycle policies are lazy-loaded policy packs.",
22
+ "- Use policy_manifest to inspect available policies.",
23
+ "- Use policy_load before sensitive tools such as takomi_subagent.",
24
+ ].join("\n");
25
+ }
26
+
27
+ function compactHeavyPolicyBlocks(prompt: string, config: ContextManagerConfig): { prompt: string; removedSections: string[] } {
28
+ let next = prompt;
29
+ const removedSections: string[] = [];
30
+ if (config.promptCompaction.compactModelRouting) {
31
+ const modelRoutingRegex = /Project Takomi model routing policy is active\. Apply it when choosing parent\/subagent models and escalation levels:\s*\n\n# Takomi Model Routing Policy[\s\S]*?(?=\nAvailable model context from Pi registry:)/;
32
+ if (modelRoutingRegex.test(next)) {
33
+ next = next.replace(modelRoutingRegex, [
34
+ "Project Takomi model routing policy is available as a lazy-loaded policy pack.",
35
+ "The subagent prerequisite gate can provide this policy automatically on first blocked takomi_subagent attempt, then the agent should retry.",
36
+ "",
37
+ ].join("\n"));
38
+ removedSections.push("full model routing policy");
39
+ }
40
+ }
41
+ if (config.promptCompaction.compactModelRegistry) {
42
+ const registryRegex = /Available model context from Pi registry:[^\n]*(?:\n|$)/;
43
+ if (registryRegex.test(next)) {
44
+ next = next.replace(registryRegex, "Available model registry context exists. The subagent policy gate will provide model routing context if needed.\n");
45
+ removedSections.push("verbose model registry list");
46
+ }
47
+ }
48
+ return { prompt: next, removedSections };
49
+ }
50
+
51
+ export function rewritePrompt(systemPrompt: string, skills: Map<string, SkillRecord>, candidates: CandidateContext[], config: ContextManagerConfig): {
52
+ prompt: string;
53
+ changed: boolean;
54
+ removedSections: string[];
55
+ warnings: string[];
56
+ } {
57
+ const warnings: string[] = [];
58
+ const removedSections: string[] = [];
59
+ let next = systemPrompt;
60
+ let changed = false;
61
+
62
+ if (config.promptCompaction.compactSkillDescriptions) {
63
+ const replacement = [renderSkillIndex(sortedSkills(skills)), renderProgressiveRule(), renderCandidateHint(candidates)].filter(Boolean).join("\n\n");
64
+ const skillBlockRegex = /<available_skills>[\s\S]*?<\/available_skills>/i;
65
+ if (!skillBlockRegex.test(next)) {
66
+ warnings.push("No <available_skills> block found; appended progressive skill guidance instead.");
67
+ next = `${next}\n\n${replacement}`;
68
+ changed = true;
69
+ } else {
70
+ next = next.replace(skillBlockRegex, replacement);
71
+ changed = true;
72
+ removedSections.push("available_skills descriptions");
73
+ }
74
+ }
75
+
76
+ const compacted = compactHeavyPolicyBlocks(next, config);
77
+ next = compacted.prompt;
78
+ removedSections.push(...compacted.removedSections);
79
+ changed = changed || compacted.removedSections.length > 0;
80
+ return { prompt: next, changed, removedSections, warnings };
81
+ }
@@ -0,0 +1,87 @@
1
+ import type { SkillRecord } from "./types";
2
+
3
+ export function normalizeText(value: string): string {
4
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, " ").replace(/\s+/g, " ").trim();
5
+ }
6
+
7
+ export function normalizeName(name: string): string {
8
+ return name.trim().toLowerCase();
9
+ }
10
+
11
+ function getString(input: unknown, keys: string[]): string | undefined {
12
+ if (!input || typeof input !== "object") return undefined;
13
+ const record = input as Record<string, unknown>;
14
+ for (const key of keys) {
15
+ const value = record[key];
16
+ if (typeof value === "string" && value.trim()) return value.trim();
17
+ }
18
+ return undefined;
19
+ }
20
+
21
+ export function collectSkillsFromOptions(options: unknown): SkillRecord[] {
22
+ if (!options || typeof options !== "object") return [];
23
+ const skills = (options as { skills?: unknown }).skills;
24
+ const rawList = Array.isArray(skills)
25
+ ? skills
26
+ : skills && typeof skills === "object"
27
+ ? Object.values(skills as Record<string, unknown>)
28
+ : [];
29
+ return rawList.flatMap((item): SkillRecord[] => {
30
+ const name = getString(item, ["name", "id", "title"]);
31
+ if (!name) return [];
32
+ return [{
33
+ name,
34
+ description: getString(item, ["description", "summary"]),
35
+ location: getString(item, ["location", "path", "file", "skillPath"]),
36
+ source: "systemPromptOptions",
37
+ }];
38
+ });
39
+ }
40
+
41
+ function decodeXmlEntities(value: string): string {
42
+ return value.replace(/&quot;/g, '"').replace(/&apos;/g, "'").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
43
+ }
44
+
45
+ function extractTag(block: string, tag: string): string | undefined {
46
+ const match = block.match(new RegExp(`<${tag}>([\\s\\S]*?)</${tag}>`, "i"));
47
+ return match?.[1]?.trim() ? decodeXmlEntities(match[1].trim()) : undefined;
48
+ }
49
+
50
+ export function collectSkillsFromXml(systemPrompt: string): SkillRecord[] {
51
+ const root = systemPrompt.match(/<available_skills>([\s\S]*?)<\/available_skills>/i);
52
+ if (!root) return [];
53
+ const skills: SkillRecord[] = [];
54
+ for (const match of root[1].matchAll(/<skill>([\s\S]*?)<\/skill>/gi)) {
55
+ const name = extractTag(match[1], "name");
56
+ if (!name) continue;
57
+ skills.push({ name, description: extractTag(match[1], "description"), location: extractTag(match[1], "location"), source: "xml" });
58
+ }
59
+ return skills;
60
+ }
61
+
62
+ export function mergeSkills(records: SkillRecord[]): Map<string, SkillRecord> {
63
+ const merged = new Map<string, SkillRecord>();
64
+ for (const skill of records) {
65
+ const key = normalizeName(skill.name);
66
+ const existing = merged.get(key);
67
+ merged.set(key, existing ? {
68
+ name: existing.name,
69
+ description: existing.description ?? skill.description,
70
+ location: existing.location ?? skill.location,
71
+ source: existing.source === "systemPromptOptions" ? existing.source : skill.source,
72
+ } : skill);
73
+ }
74
+ return merged;
75
+ }
76
+
77
+ export function sortedSkills(skills: Map<string, SkillRecord>): SkillRecord[] {
78
+ return [...skills.values()].sort((a, b) => a.name.localeCompare(b.name));
79
+ }
80
+
81
+ export function findSkill(skills: Map<string, SkillRecord>, name: string): SkillRecord | undefined {
82
+ const key = normalizeName(name);
83
+ const exact = skills.get(key);
84
+ if (exact) return exact;
85
+ const matches = sortedSkills(skills).filter((skill) => normalizeName(skill.name).includes(key));
86
+ return matches.length === 1 ? matches[0] : undefined;
87
+ }
@@ -0,0 +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
+ }
@@ -0,0 +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
+ }
@@ -0,0 +1,70 @@
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 ContextManagerConfig = {
26
+ candidateRouter: {
27
+ maxCandidates: number;
28
+ highConfidence: number;
29
+ mediumConfidence: number;
30
+ };
31
+ policyPaths: string[];
32
+ policyFiles?: Record<string, string>;
33
+ toolPrerequisites: Record<string, Prerequisite[]>;
34
+ promptCompaction: {
35
+ compactModelRouting: boolean;
36
+ compactModelRegistry: boolean;
37
+ compactSkillDescriptions: boolean;
38
+ };
39
+ };
40
+
41
+ export type ContextReport = {
42
+ timestamp: string;
43
+ cwd: string;
44
+ userPrompt: string;
45
+ skillCount: number;
46
+ candidates: CandidateContext[];
47
+ loadedByTool: string[];
48
+ loadedPolicies: string[];
49
+ readFiles: string[];
50
+ editedFiles: string[];
51
+ writtenFiles: string[];
52
+ blockedActions: Array<{ toolName: string; reason: string; timestamp: string }>;
53
+ duplicateExtensionWarnings: Array<{ toolName: string; paths: string[] }>;
54
+ promptRewrite: {
55
+ attempted: boolean;
56
+ changed: boolean;
57
+ originalLength: number;
58
+ rewrittenLength: number;
59
+ removedSections: string[];
60
+ warnings: string[];
61
+ };
62
+ toolCalls: {
63
+ skillIndex: number;
64
+ skillManifest: number;
65
+ skillLoad: number;
66
+ policyManifest: number;
67
+ policyLoad: number;
68
+ contextReport: number;
69
+ };
70
+ };
@@ -37,6 +37,7 @@ import { executeTakomiSubagentTool } from "../takomi-subagents/tool-runner";
37
37
  import {
38
38
  renderRuntimeStatus,
39
39
  renderRuntimeWidget,
40
+ renderTakomiHeader,
40
41
  TakomiFooterComponent,
41
42
  } from "./ui";
42
43
  import { getTakomiSubagentController } from "./subagent-controller";
@@ -443,6 +444,13 @@ const footerStateRef: { current: TakomiState; installed: boolean } = { current:
443
444
 
444
445
  async function refreshUi(ctx: ExtensionContext, state: TakomiState) {
445
446
  if (!ctx.hasUI) return;
447
+ ctx.ui.setTitle("Takomi");
448
+ ctx.ui.setHeader((_tui, theme) => ({
449
+ invalidate() {},
450
+ render() {
451
+ return renderTakomiHeader(theme);
452
+ },
453
+ }));
446
454
  footerStateRef.current = state;
447
455
  ctx.ui.setStatus("takomi-runtime", renderRuntimeStatus(ctx.ui.theme, state));
448
456
  const widget = renderRuntimeWidget(ctx.ui.theme, state);
@@ -31,6 +31,27 @@ export type RuntimeHudState = {
31
31
 
32
32
  type Tone = "accent" | "warning" | "success" | "error" | "muted" | "dim" | "thinkingMinimal";
33
33
 
34
+ export function renderTakomiHeader(theme: Theme): string[] {
35
+ const accent = (text: string) => theme.fg("accent", text);
36
+ const violet = (text: string) => theme.fg("thinkingMinimal", text);
37
+ const muted = (text: string) => theme.fg("muted", text);
38
+ const dim = (text: string) => theme.fg("dim", text);
39
+ const line = dim("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
40
+
41
+ return [
42
+ "",
43
+ accent("████████╗ █████╗ ██╗ ██╗ ██████╗ ███╗ ███╗██╗"),
44
+ accent("╚══██╔══╝██╔══██╗██║ ██╔╝██╔═══██╗████╗ ████║██║"),
45
+ violet(" ██║ ███████║█████╔╝ ██║ ██║██╔████╔██║██║"),
46
+ violet(" ██║ ██╔══██║██╔═██╗ ██║ ██║██║╚██╔╝██║██║"),
47
+ accent(" ██║ ██║ ██║██║ ██╗╚██████╔╝██║ ╚═╝ ██║██║"),
48
+ accent(" ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝"),
49
+ line,
50
+ `${muted(" Genesis")} ${dim("→")} ${muted("Design")} ${dim("→")} ${muted("Build")} ${dim("| custom Pi harness runtime")}`,
51
+ "",
52
+ ];
53
+ }
54
+
34
55
  function stageTone(stage?: string): Tone {
35
56
  switch (stage) {
36
57
  case "genesis":
@@ -0,0 +1,88 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/badlogic/pi-mono/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
3
+ "name": "takomi-aurora",
4
+ "vars": {
5
+ "void": "#070A13",
6
+ "obsidian": "#0B1020",
7
+ "deep": "#11162A",
8
+ "glass": "#18213A",
9
+ "glassHot": "#251633",
10
+ "glassCool": "#0D2430",
11
+ "line": "#2B3558",
12
+ "lineGlow": "#3D4B7A",
13
+ "snow": "#EAF2FF",
14
+ "mist": "#AAB8D6",
15
+ "haze": "#6F7EA3",
16
+ "neonCyan": "#2EF3FF",
17
+ "electricBlue": "#4D8DFF",
18
+ "plasmaViolet": "#B56CFF",
19
+ "hotMagenta": "#FF4FD8",
20
+ "solarGold": "#FFD166",
21
+ "acidGreen": "#6DFFB8",
22
+ "ember": "#FF6B6B"
23
+ },
24
+ "colors": {
25
+ "accent": "neonCyan",
26
+ "border": "line",
27
+ "borderAccent": "hotMagenta",
28
+ "borderMuted": "lineGlow",
29
+ "success": "acidGreen",
30
+ "error": "ember",
31
+ "warning": "solarGold",
32
+ "muted": "mist",
33
+ "dim": "haze",
34
+ "text": "snow",
35
+ "thinkingText": "mist",
36
+
37
+ "selectedBg": "glassHot",
38
+ "userMessageBg": "deep",
39
+ "userMessageText": "snow",
40
+ "customMessageBg": "glassCool",
41
+ "customMessageText": "snow",
42
+ "customMessageLabel": "hotMagenta",
43
+
44
+ "toolPendingBg": "glassCool",
45
+ "toolSuccessBg": "#0D2C24",
46
+ "toolErrorBg": "#321622",
47
+ "toolTitle": "neonCyan",
48
+ "toolOutput": "snow",
49
+
50
+ "mdHeading": "hotMagenta",
51
+ "mdLink": "neonCyan",
52
+ "mdLinkUrl": "electricBlue",
53
+ "mdCode": "solarGold",
54
+ "mdCodeBlock": "snow",
55
+ "mdCodeBlockBorder": "plasmaViolet",
56
+ "mdQuote": "mist",
57
+ "mdQuoteBorder": "hotMagenta",
58
+ "mdHr": "lineGlow",
59
+ "mdListBullet": "neonCyan",
60
+
61
+ "toolDiffAdded": "acidGreen",
62
+ "toolDiffRemoved": "ember",
63
+ "toolDiffContext": "mist",
64
+
65
+ "syntaxComment": "haze",
66
+ "syntaxKeyword": "hotMagenta",
67
+ "syntaxFunction": "neonCyan",
68
+ "syntaxVariable": "snow",
69
+ "syntaxString": "acidGreen",
70
+ "syntaxNumber": "solarGold",
71
+ "syntaxType": "electricBlue",
72
+ "syntaxOperator": "plasmaViolet",
73
+ "syntaxPunctuation": "mist",
74
+
75
+ "thinkingOff": "lineGlow",
76
+ "thinkingMinimal": "plasmaViolet",
77
+ "thinkingLow": "electricBlue",
78
+ "thinkingMedium": "neonCyan",
79
+ "thinkingHigh": "hotMagenta",
80
+ "thinkingXhigh": "ember",
81
+ "bashMode": "solarGold"
82
+ },
83
+ "export": {
84
+ "pageBg": "#060813",
85
+ "cardBg": "#0E1426",
86
+ "infoBg": "#1A1730"
87
+ }
88
+ }
package/README.md CHANGED
@@ -41,6 +41,20 @@ takomi install all
41
41
  takomi init
42
42
  ```
43
43
 
44
+ ### Subagent execution credit
45
+
46
+ Takomi's Pi-native subagent execution and terminal UI build on **[`pi-subagents`](https://github.com/nicobailon/pi-subagents)** by **Nico Bailon**. That package provides the underlying Pi extension for delegated subagent runs, including the native subagent result renderer, live progress/status display, single/parallel/chain execution support, session/artifact handling, and related subagent tooling. Takomi adds its own lifecycle orchestration, model-routing policy, workflow metadata, board/checklist context, and agent conventions on top of that foundation.
47
+
48
+ ### Context Manager
49
+
50
+ Takomi now ships a Pi-native `takomi-context-manager` extension. It reduces prompt bloat with progressive context loading:
51
+
52
+ - skill names are always visible, while descriptions and full `SKILL.md` files load only when needed
53
+ - `skill_manifest`, `skill_load`, `policy_manifest`, `policy_load`, and `context_report` tools expose context on demand
54
+ - `/takomi routing` remains the source of truth for model-routing policy
55
+ - `takomi_subagent` is guarded by routing-policy context and can recover from wrong-provider model choices
56
+ - `/context-report` shows prompt compaction, loaded skills/policies, blocked actions, model-routing corrections, and duplicate extension diagnostics
57
+
44
58
 
45
59
  ### Option A: Global Install (Best for Multi-IDE Users) ⭐
46
60
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "takomi",
3
- "version": "2.1.6",
3
+ "version": "2.1.9",
4
4
  "description": "🎯 Stop wrestling with AI. Start building with purpose. The artisan's toolkit for agent workflows, Codex skills, and original Takomi capabilities like 21st.dev integration.",
5
5
  "type": "module",
6
6
  "bin": {