takomi 2.1.5 → 2.1.8
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/extensions/takomi-context-manager/config.ts +42 -0
- package/.pi/extensions/takomi-context-manager/context-router.ts +57 -0
- package/.pi/extensions/takomi-context-manager/diagnostics-tools.ts +26 -0
- package/.pi/extensions/takomi-context-manager/diagnostics.ts +53 -0
- package/.pi/extensions/takomi-context-manager/extension-conflicts.ts +56 -0
- package/.pi/extensions/takomi-context-manager/index.ts +56 -0
- package/.pi/extensions/takomi-context-manager/model-policy-gate.ts +213 -0
- package/.pi/extensions/takomi-context-manager/policy-registry.ts +100 -0
- package/.pi/extensions/takomi-context-manager/policy-tools.ts +35 -0
- package/.pi/extensions/takomi-context-manager/prerequisite-gates.ts +39 -0
- package/.pi/extensions/takomi-context-manager/prompt-rewriter.ts +81 -0
- package/.pi/extensions/takomi-context-manager/skill-registry.ts +87 -0
- package/.pi/extensions/takomi-context-manager/skill-tools.ts +80 -0
- package/.pi/extensions/takomi-context-manager/state.ts +68 -0
- package/.pi/extensions/takomi-context-manager/types.ts +70 -0
- package/.pi/extensions/takomi-runtime/index.ts +8 -0
- package/.pi/extensions/takomi-runtime/ui.ts +21 -0
- package/.pi/themes/takomi-aurora.json +88 -0
- package/README.md +5 -2
- package/package.json +1 -1
- package/src/harness.js +2 -2
|
@@ -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(/"/g, '"').replace(/'/g, "'").replace(/</g, "<").replace(/>/g, ">").replace(/&/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,10 @@ 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
|
+
|
|
44
48
|
|
|
45
49
|
### Option A: Global Install (Best for Multi-IDE Users) ⭐
|
|
46
50
|
|
|
@@ -156,9 +160,8 @@ Takomi v2.0 introduces the **Global Skills Router** — install skills once, and
|
|
|
156
160
|
| **Antigravity** | `~/.gemini/antigravity/skills/` | `~/.gemini/antigravity/global_workflows/` |
|
|
157
161
|
| **KiloCode** | `~/.kilocode/skills/` | `~/.kilocode/workflows/` |
|
|
158
162
|
| **Windsurf** | `~/.codeium/windsurf/skills/` | `~/.codeium/windsurf/global_workflows/` |
|
|
159
|
-
| **
|
|
163
|
+
| **Global agents-compatible CLIs** _(e.g., Codex, Gemini CLI)_ | `~/.agents/skills/` | _(skills only)_ |
|
|
160
164
|
| **Cursor** | `~/.cursor/skills/` | _(uses rules)_ |
|
|
161
|
-
| **Gemini CLI** | `~/.agents/skills/` | _(skills only)_ |
|
|
162
165
|
|
|
163
166
|
### CLI Commands
|
|
164
167
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "takomi",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.8",
|
|
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": {
|
package/src/harness.js
CHANGED
|
@@ -80,7 +80,7 @@ export const HARNESS_MAP = {
|
|
|
80
80
|
return fs.existsSync(this.rootPath);
|
|
81
81
|
},
|
|
82
82
|
targets: {
|
|
83
|
-
skills: path.join(HOME, '.
|
|
83
|
+
skills: path.join(HOME, '.agents', 'skills'),
|
|
84
84
|
workflows: null,
|
|
85
85
|
yamls: null,
|
|
86
86
|
},
|
|
@@ -107,7 +107,7 @@ export const HARNESS_MAP = {
|
|
|
107
107
|
return fs.existsSync(this.rootPath) && !fs.existsSync(path.join(HOME, '.gemini', 'antigravity'));
|
|
108
108
|
},
|
|
109
109
|
targets: {
|
|
110
|
-
skills: path.join(HOME, '.
|
|
110
|
+
skills: path.join(HOME, '.agents', 'skills'),
|
|
111
111
|
workflows: null,
|
|
112
112
|
yamls: null,
|
|
113
113
|
},
|