takomi 2.1.13 → 2.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.pi/agents/architect.md +73 -73
- package/.pi/agents/coder.md +70 -70
- package/.pi/agents/designer.md +72 -72
- package/.pi/agents/orchestrator.md +122 -122
- package/.pi/agents/reviewer.md +71 -71
- package/.pi/extensions/oauth-router/provider.ts +3 -1
- package/.pi/extensions/takomi-context-manager/config.ts +48 -48
- package/.pi/extensions/takomi-context-manager/context-router.ts +57 -57
- package/.pi/extensions/takomi-context-manager/diagnostics-tools.ts +28 -28
- package/.pi/extensions/takomi-context-manager/diagnostics.ts +55 -55
- package/.pi/extensions/takomi-context-manager/extension-conflicts.ts +56 -56
- package/.pi/extensions/takomi-context-manager/index.ts +56 -56
- package/.pi/extensions/takomi-context-manager/model-policy-gate.ts +228 -206
- package/.pi/extensions/takomi-context-manager/policy-registry.ts +97 -97
- package/.pi/extensions/takomi-context-manager/policy-tools.ts +35 -35
- package/.pi/extensions/takomi-context-manager/prerequisite-gates.ts +39 -39
- package/.pi/extensions/takomi-context-manager/prompt-rewriter.ts +100 -100
- package/.pi/extensions/takomi-context-manager/skill-registry.ts +87 -87
- package/.pi/extensions/takomi-context-manager/skill-tools.ts +80 -80
- package/.pi/extensions/takomi-context-manager/state.ts +68 -68
- package/.pi/extensions/takomi-context-manager/types.ts +77 -77
- package/.pi/extensions/takomi-runtime/command-text.ts +10 -2
- package/.pi/extensions/takomi-runtime/commands.ts +78 -5
- package/.pi/extensions/takomi-runtime/routing-policy.ts +187 -145
- package/.pi/extensions/takomi-subagents/native-render.ts +41 -41
- package/.pi/extensions/takomi-subagents/pi-subagents-internal.ts +35 -32
- package/.pi/extensions/takomi-subagents/run-types.ts +25 -25
- package/.pi/prompts/build-prompt.md +259 -259
- package/.pi/prompts/design-prompt.md +95 -95
- package/.pi/prompts/genesis-prompt.md +140 -140
- package/.pi/prompts/prime-prompt.md +110 -110
- package/.pi/themes/takomi-aurora.json +88 -88
- package/README.md +2 -4
- package/assets/.agent/skills/21st-dev-components/SKILL.md +244 -244
- package/assets/.agent/skills/anti-gravity/SKILL.md +112 -0
- package/assets/.agent/skills/gemini/SKILL.md +14 -223
- package/assets/.agent/skills/git-commit-generation/SKILL.md +195 -0
- package/package.json +1 -1
- package/src/pi-takomi-core/validation.ts +135 -135
|
@@ -1,206 +1,228 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import type {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
return /
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
return
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
5
|
+
import type { ContextManagerState } from "./state";
|
|
6
|
+
import { recordBlocked } from "./state";
|
|
7
|
+
import { resolveTakomiRoutingPolicy } from "../takomi-runtime/routing-policy";
|
|
8
|
+
|
|
9
|
+
type Settings = {
|
|
10
|
+
takomi?: { modelRoutingPolicyFile?: string };
|
|
11
|
+
subagents?: { agentOverrides?: Record<string, unknown> };
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type ModelPolicySnapshot = {
|
|
15
|
+
approvedModels: string[];
|
|
16
|
+
preferredModels: string[];
|
|
17
|
+
sourceFiles: string[];
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function asRecord(value: unknown): Record<string, unknown> {
|
|
21
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function readSettingsFile(filePath: string): Promise<Settings> {
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(await readFile(filePath, "utf8")) as Settings;
|
|
27
|
+
} catch {
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function mergeSettings(globalSettings: Settings, projectSettings: Settings): Settings {
|
|
33
|
+
const globalOverrides = asRecord(globalSettings.subagents?.agentOverrides);
|
|
34
|
+
const projectOverrides = asRecord(projectSettings.subagents?.agentOverrides);
|
|
35
|
+
return {
|
|
36
|
+
...globalSettings,
|
|
37
|
+
...projectSettings,
|
|
38
|
+
takomi: { ...(globalSettings.takomi ?? {}), ...(projectSettings.takomi ?? {}) },
|
|
39
|
+
subagents: {
|
|
40
|
+
...(globalSettings.subagents ?? {}),
|
|
41
|
+
...(projectSettings.subagents ?? {}),
|
|
42
|
+
agentOverrides: { ...globalOverrides, ...projectOverrides },
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function readSettings(cwd: string): Promise<Settings> {
|
|
48
|
+
const globalSettings = await readSettingsFile(path.join(os.homedir(), ".pi", "agent", "settings.json"));
|
|
49
|
+
const projectSettings = await readSettingsFile(path.resolve(cwd, ".pi/settings.json"));
|
|
50
|
+
return mergeSettings(globalSettings, projectSettings);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function modelFamily(model: string): string {
|
|
54
|
+
return model.split("/").at(-1)?.toLowerCase() ?? model.toLowerCase();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function unique(values: string[]): string[] {
|
|
58
|
+
return [...new Set(values.filter(Boolean))];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function collectModelsFromSettings(settings: Settings): string[] {
|
|
62
|
+
const overrides = asRecord(settings.subagents?.agentOverrides);
|
|
63
|
+
const models: string[] = [];
|
|
64
|
+
for (const value of Object.values(overrides)) {
|
|
65
|
+
const record = asRecord(value);
|
|
66
|
+
if (typeof record.model === "string") models.push(record.model);
|
|
67
|
+
if (Array.isArray(record.fallbackModels)) {
|
|
68
|
+
for (const fallback of record.fallbackModels) if (typeof fallback === "string") models.push(fallback.split(":")[0]);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return models;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function isModelLike(value: string): boolean {
|
|
75
|
+
const lower = value.toLowerCase();
|
|
76
|
+
return /(^|\/)(gpt|claude|gemini|o[0-9]|qwen|deepseek|llama|mistral|kimi|grok|sonnet|haiku|opus|codex|mini|max)/i.test(lower)
|
|
77
|
+
|| lower.includes("oauth-router/")
|
|
78
|
+
|| lower.includes("openai-codex/")
|
|
79
|
+
|| lower.includes("lmstudio/");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function collectModelsFromPolicy(text: string): string[] {
|
|
83
|
+
const explicit = (text.match(/[a-z0-9-]+\/[a-z0-9._-]+/gi) ?? []).filter(isModelLike);
|
|
84
|
+
const inferred: string[] = [];
|
|
85
|
+
if (/gpt[- ]?5\.5/i.test(text)) inferred.push("oauth-router/gpt-5.5");
|
|
86
|
+
if (/gpt[- ]?5\.4(?!\s*mini)/i.test(text)) inferred.push("oauth-router/gpt-5.4");
|
|
87
|
+
if (/gpt[- ]?5\.4\s*mini/i.test(text)) inferred.push("oauth-router/gpt-5.4-mini");
|
|
88
|
+
return unique([...explicit, ...inferred]);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function loadSnapshot(cwd: string): Promise<ModelPolicySnapshot> {
|
|
92
|
+
const settings = await readSettings(cwd);
|
|
93
|
+
const settingsModels = collectModelsFromSettings(settings);
|
|
94
|
+
const resolvedPolicy = await resolveTakomiRoutingPolicy(cwd);
|
|
95
|
+
const sourceFiles = resolvedPolicy.policyPath ? [resolvedPolicy.policyPath] : [];
|
|
96
|
+
const policyModels = resolvedPolicy.text ? collectModelsFromPolicy(resolvedPolicy.text) : [];
|
|
97
|
+
const approvedModels = unique([...settingsModels, ...policyModels]);
|
|
98
|
+
return { approvedModels, preferredModels: settingsModels.length ? unique(settingsModels) : approvedModels, sourceFiles };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function collectRequestedModelRefs(input: unknown): Array<{ holder: Record<string, unknown>; key: string; value: string }> {
|
|
102
|
+
const refs: Array<{ holder: Record<string, unknown>; key: string; value: string }> = [];
|
|
103
|
+
function visit(value: unknown): void {
|
|
104
|
+
if (!value || typeof value !== "object") return;
|
|
105
|
+
if (Array.isArray(value)) {
|
|
106
|
+
for (const item of value) visit(item);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const record = value as Record<string, unknown>;
|
|
110
|
+
for (const key of ["model", "preferredModel"]) {
|
|
111
|
+
if (typeof record[key] === "string") refs.push({ holder: record, key, value: record[key] });
|
|
112
|
+
}
|
|
113
|
+
if (Array.isArray(record.fallbackModels)) {
|
|
114
|
+
record.fallbackModels = record.fallbackModels.filter((item) => typeof item === "string");
|
|
115
|
+
}
|
|
116
|
+
for (const key of ["tasks", "chain"]) visit(record[key]);
|
|
117
|
+
}
|
|
118
|
+
visit(input);
|
|
119
|
+
return refs;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function approvedEquivalent(requested: string, approved: string[]): string | undefined {
|
|
123
|
+
const requestedFamily = modelFamily(requested);
|
|
124
|
+
return approved.find((candidate) => modelFamily(candidate) === requestedFamily);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function isModelFailure(text: string): boolean {
|
|
128
|
+
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);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function renderPolicyViolation(requested: string, approved: string[]): string {
|
|
132
|
+
return [
|
|
133
|
+
"Blocked by Takomi routing policy.",
|
|
134
|
+
"",
|
|
135
|
+
"Requested model:",
|
|
136
|
+
requested,
|
|
137
|
+
"",
|
|
138
|
+
"Allowed/preferred models include:",
|
|
139
|
+
...(approved.length ? approved.map((model) => `- ${model}`) : ["- none discovered; run /takomi routing <policy text> first"]),
|
|
140
|
+
"",
|
|
141
|
+
"The subagent did not run. Ask the user how to proceed or retry with an approved model.",
|
|
142
|
+
].join("\n");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
type RecoveryChoice =
|
|
146
|
+
| { action: "retry"; model: string }
|
|
147
|
+
| { action: "stop" };
|
|
148
|
+
|
|
149
|
+
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> {
|
|
150
|
+
if (approved.length === 0) return { action: "stop" };
|
|
151
|
+
const options = [
|
|
152
|
+
...approved.map((model) => `Retry with ${model}`),
|
|
153
|
+
"Stop and let me send a new prompt",
|
|
154
|
+
];
|
|
155
|
+
const choice = await ctx.ui.select(
|
|
156
|
+
`takomi_subagent requested a model outside your routing policy: ${requested}`,
|
|
157
|
+
options,
|
|
158
|
+
);
|
|
159
|
+
if (choice?.startsWith("Retry with ")) return { action: "retry", model: choice.replace("Retry with ", "") };
|
|
160
|
+
return { action: "stop" };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function installModelPolicyGate(pi: ExtensionAPI, state: ContextManagerState): void {
|
|
164
|
+
pi.on("tool_call", async (event, ctx) => {
|
|
165
|
+
if (event.toolName !== "takomi_subagent") return;
|
|
166
|
+
const snapshot = await loadSnapshot(ctx.cwd);
|
|
167
|
+
const approved = snapshot.approvedModels;
|
|
168
|
+
if (approved.length === 0) return;
|
|
169
|
+
|
|
170
|
+
const refs = collectRequestedModelRefs(event.input);
|
|
171
|
+
const corrections: string[] = [];
|
|
172
|
+
for (const ref of refs) {
|
|
173
|
+
if (approved.includes(ref.value)) continue;
|
|
174
|
+
const equivalent = approvedEquivalent(ref.value, approved);
|
|
175
|
+
if (equivalent) {
|
|
176
|
+
ref.holder[ref.key] = equivalent;
|
|
177
|
+
corrections.push(`${ref.value} -> ${equivalent}`);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const recovery = await askForInvalidModelRecovery(ctx, ref.value, approved);
|
|
181
|
+
if (recovery.action === "retry") {
|
|
182
|
+
ref.holder[ref.key] = recovery.model;
|
|
183
|
+
corrections.push(`${ref.value} -> ${recovery.model} (user selected recovery)`);
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
const reason = [
|
|
187
|
+
renderPolicyViolation(ref.value, approved),
|
|
188
|
+
"",
|
|
189
|
+
"Human selected stop. The agent turn has been aborted; wait for the user's next prompt.",
|
|
190
|
+
].join("\n");
|
|
191
|
+
recordBlocked(state, event.toolName, reason);
|
|
192
|
+
ctx.abort?.();
|
|
193
|
+
return { block: true, reason };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (corrections.length > 0) {
|
|
197
|
+
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");
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
pi.on("tool_result", async (event, ctx) => {
|
|
202
|
+
if (event.toolName !== "takomi_subagent" || !event.isError) return;
|
|
203
|
+
const content = JSON.stringify(event.content ?? "");
|
|
204
|
+
if (!isModelFailure(content)) return;
|
|
205
|
+
|
|
206
|
+
const snapshot = await loadSnapshot(ctx.cwd);
|
|
207
|
+
const options = [
|
|
208
|
+
...snapshot.approvedModels.map((model) => `Retry with ${model}`),
|
|
209
|
+
"Stop and let me send a new prompt",
|
|
210
|
+
];
|
|
211
|
+
const choice = await ctx.ui.select("Takomi subagent model/provider failure. How do you want to continue?", options);
|
|
212
|
+
const retryModel = choice?.startsWith("Retry with ") ? choice.replace("Retry with ", "") : undefined;
|
|
213
|
+
const stopped = !retryModel;
|
|
214
|
+
const guidance = [
|
|
215
|
+
"Takomi subagent failed with a model/provider-related error.",
|
|
216
|
+
"",
|
|
217
|
+
`Policy source: ${snapshot.sourceFiles.join(", ") || "not found"}`,
|
|
218
|
+
"Policy-approved models:",
|
|
219
|
+
...(snapshot.approvedModels.length ? snapshot.approvedModels.map((model) => `- ${model}`) : ["- none discovered"]),
|
|
220
|
+
"",
|
|
221
|
+
stopped
|
|
222
|
+
? "Human selected stop/no retry. The agent turn has been aborted; wait for the user's next prompt."
|
|
223
|
+
: `User selected retry with ${retryModel}. Retry takomi_subagent with model ${retryModel}.`,
|
|
224
|
+
].join("\n");
|
|
225
|
+
if (stopped) ctx.abort?.();
|
|
226
|
+
return { content: [{ type: "text", text: guidance }], isError: true, terminate: stopped };
|
|
227
|
+
});
|
|
228
|
+
}
|
|
@@ -1,97 +1,97 @@
|
|
|
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
|
-
import { resolveTakomiRoutingPolicy } from "../takomi-runtime/routing-policy";
|
|
6
|
-
|
|
7
|
-
function descriptionFromMarkdown(content: string): string {
|
|
8
|
-
const lines = content.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
9
|
-
const firstBody = lines.find((line) => !line.startsWith("#"));
|
|
10
|
-
return firstBody?.slice(0, 240) ?? "Policy pack";
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
async function readPolicyFile(name: string, filePath: string): Promise<PolicyPack | undefined> {
|
|
14
|
-
try {
|
|
15
|
-
const content = await readFile(filePath, "utf8");
|
|
16
|
-
return {
|
|
17
|
-
name,
|
|
18
|
-
description: descriptionFromMarkdown(content),
|
|
19
|
-
content,
|
|
20
|
-
path: filePath,
|
|
21
|
-
};
|
|
22
|
-
} catch {
|
|
23
|
-
return undefined;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function addPolicy(policies: Map<string, PolicyPack>, policy: PolicyPack | undefined, override = false): void {
|
|
28
|
-
if (!policy) return;
|
|
29
|
-
const key = normalizeName(policy.name);
|
|
30
|
-
if (!override && policies.has(key)) return;
|
|
31
|
-
policies.set(key, policy);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
async function discoverResolvedModelRoutingPolicy(cwd: string): Promise<PolicyPack | undefined> {
|
|
35
|
-
const resolved = await resolveTakomiRoutingPolicy(cwd);
|
|
36
|
-
if (!resolved.text) return undefined;
|
|
37
|
-
return {
|
|
38
|
-
name: "model-routing",
|
|
39
|
-
description: descriptionFromMarkdown(resolved.text),
|
|
40
|
-
content: resolved.text,
|
|
41
|
-
path: resolved.policyPath,
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
async function discoverExplicitPolicyFiles(cwd: string, config: ContextManagerConfig): Promise<PolicyPack[]> {
|
|
46
|
-
const entries = Object.entries(config.policyFiles ?? {});
|
|
47
|
-
const policies = await Promise.all(entries.map(([name, filePath]) => readPolicyFile(name, path.resolve(cwd, filePath))));
|
|
48
|
-
return policies.filter((policy): policy is PolicyPack => Boolean(policy));
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export async function discoverPolicies(cwd: string, config: ContextManagerConfig): Promise<Map<string, PolicyPack>> {
|
|
52
|
-
const policies = new Map<string, PolicyPack>();
|
|
53
|
-
|
|
54
|
-
// Source-of-truth priority 1: explicit context-manager policy file mappings.
|
|
55
|
-
for (const policy of await discoverExplicitPolicyFiles(cwd, config)) {
|
|
56
|
-
addPolicy(policies, policy, true);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Source-of-truth priority 2: Takomi routing policy resolved from project settings or the bundled harness default.
|
|
60
|
-
addPolicy(policies, await discoverResolvedModelRoutingPolicy(cwd), true);
|
|
61
|
-
|
|
62
|
-
// Source-of-truth priority 3: discovered markdown policy packs. These are supplemental/default packs.
|
|
63
|
-
for (const policyPath of config.policyPaths) {
|
|
64
|
-
const absolute = path.resolve(cwd, policyPath);
|
|
65
|
-
try {
|
|
66
|
-
const entries = await readdir(absolute, { withFileTypes: true });
|
|
67
|
-
for (const entry of entries) {
|
|
68
|
-
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
69
|
-
const name = path.basename(entry.name, ".md");
|
|
70
|
-
addPolicy(policies, await readPolicyFile(name, path.join(absolute, entry.name)), false);
|
|
71
|
-
}
|
|
72
|
-
} catch {
|
|
73
|
-
// Optional policy directories may not exist.
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return policies;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export function renderPolicyManifest(policies: Map<string, PolicyPack>, names: string[]): string {
|
|
81
|
-
const selected = names.length > 0 ? names : [...policies.keys()];
|
|
82
|
-
return selected.map((name) => {
|
|
83
|
-
const policy = policies.get(normalizeName(name));
|
|
84
|
-
if (!policy) return `Policy not found: ${name}`;
|
|
85
|
-
return [`Policy: ${policy.name}`, `Description: ${policy.description}`, `Location: ${policy.path ?? "(generated/default)"}`].join("\n");
|
|
86
|
-
}).join("\n\n");
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export function renderPolicies(policies: Map<string, PolicyPack>, loaded: Set<string>, names: string[]): string {
|
|
90
|
-
if (names.length === 0) return "No policies requested.";
|
|
91
|
-
return names.map((name) => {
|
|
92
|
-
const policy = policies.get(normalizeName(name));
|
|
93
|
-
if (!policy) return `Policy not found: ${name}`;
|
|
94
|
-
loaded.add(policy.name);
|
|
95
|
-
return [`Policy: ${policy.name}`, `Description: ${policy.description}`, `Location: ${policy.path ?? "(generated/default)"}`, "", policy.content].join("\n");
|
|
96
|
-
}).join("\n\n---\n\n");
|
|
97
|
-
}
|
|
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
|
+
import { resolveTakomiRoutingPolicy } from "../takomi-runtime/routing-policy";
|
|
6
|
+
|
|
7
|
+
function descriptionFromMarkdown(content: string): string {
|
|
8
|
+
const lines = content.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
9
|
+
const firstBody = lines.find((line) => !line.startsWith("#"));
|
|
10
|
+
return firstBody?.slice(0, 240) ?? "Policy pack";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function readPolicyFile(name: string, filePath: string): Promise<PolicyPack | undefined> {
|
|
14
|
+
try {
|
|
15
|
+
const content = await readFile(filePath, "utf8");
|
|
16
|
+
return {
|
|
17
|
+
name,
|
|
18
|
+
description: descriptionFromMarkdown(content),
|
|
19
|
+
content,
|
|
20
|
+
path: filePath,
|
|
21
|
+
};
|
|
22
|
+
} catch {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function addPolicy(policies: Map<string, PolicyPack>, policy: PolicyPack | undefined, override = false): void {
|
|
28
|
+
if (!policy) return;
|
|
29
|
+
const key = normalizeName(policy.name);
|
|
30
|
+
if (!override && policies.has(key)) return;
|
|
31
|
+
policies.set(key, policy);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function discoverResolvedModelRoutingPolicy(cwd: string): Promise<PolicyPack | undefined> {
|
|
35
|
+
const resolved = await resolveTakomiRoutingPolicy(cwd);
|
|
36
|
+
if (!resolved.text) return undefined;
|
|
37
|
+
return {
|
|
38
|
+
name: "model-routing",
|
|
39
|
+
description: descriptionFromMarkdown(resolved.text),
|
|
40
|
+
content: resolved.text,
|
|
41
|
+
path: resolved.policyPath,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function discoverExplicitPolicyFiles(cwd: string, config: ContextManagerConfig): Promise<PolicyPack[]> {
|
|
46
|
+
const entries = Object.entries(config.policyFiles ?? {});
|
|
47
|
+
const policies = await Promise.all(entries.map(([name, filePath]) => readPolicyFile(name, path.resolve(cwd, filePath))));
|
|
48
|
+
return policies.filter((policy): policy is PolicyPack => Boolean(policy));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function discoverPolicies(cwd: string, config: ContextManagerConfig): Promise<Map<string, PolicyPack>> {
|
|
52
|
+
const policies = new Map<string, PolicyPack>();
|
|
53
|
+
|
|
54
|
+
// Source-of-truth priority 1: explicit context-manager policy file mappings.
|
|
55
|
+
for (const policy of await discoverExplicitPolicyFiles(cwd, config)) {
|
|
56
|
+
addPolicy(policies, policy, true);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Source-of-truth priority 2: Takomi routing policy resolved from project settings or the bundled harness default.
|
|
60
|
+
addPolicy(policies, await discoverResolvedModelRoutingPolicy(cwd), true);
|
|
61
|
+
|
|
62
|
+
// Source-of-truth priority 3: discovered markdown policy packs. These are supplemental/default packs.
|
|
63
|
+
for (const policyPath of config.policyPaths) {
|
|
64
|
+
const absolute = path.resolve(cwd, policyPath);
|
|
65
|
+
try {
|
|
66
|
+
const entries = await readdir(absolute, { withFileTypes: true });
|
|
67
|
+
for (const entry of entries) {
|
|
68
|
+
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
69
|
+
const name = path.basename(entry.name, ".md");
|
|
70
|
+
addPolicy(policies, await readPolicyFile(name, path.join(absolute, entry.name)), false);
|
|
71
|
+
}
|
|
72
|
+
} catch {
|
|
73
|
+
// Optional policy directories may not exist.
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return policies;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function renderPolicyManifest(policies: Map<string, PolicyPack>, names: string[]): string {
|
|
81
|
+
const selected = names.length > 0 ? names : [...policies.keys()];
|
|
82
|
+
return selected.map((name) => {
|
|
83
|
+
const policy = policies.get(normalizeName(name));
|
|
84
|
+
if (!policy) return `Policy not found: ${name}`;
|
|
85
|
+
return [`Policy: ${policy.name}`, `Description: ${policy.description}`, `Location: ${policy.path ?? "(generated/default)"}`].join("\n");
|
|
86
|
+
}).join("\n\n");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function renderPolicies(policies: Map<string, PolicyPack>, loaded: Set<string>, names: string[]): string {
|
|
90
|
+
if (names.length === 0) return "No policies requested.";
|
|
91
|
+
return names.map((name) => {
|
|
92
|
+
const policy = policies.get(normalizeName(name));
|
|
93
|
+
if (!policy) return `Policy not found: ${name}`;
|
|
94
|
+
loaded.add(policy.name);
|
|
95
|
+
return [`Policy: ${policy.name}`, `Description: ${policy.description}`, `Location: ${policy.path ?? "(generated/default)"}`, "", policy.content].join("\n");
|
|
96
|
+
}).join("\n\n---\n\n");
|
|
97
|
+
}
|