one-code-extension 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/LICENSE +32 -0
  2. package/README.md +202 -0
  3. package/agents/explore.md +24 -0
  4. package/agents/general-purpose.md +21 -0
  5. package/agents/plan.md +22 -0
  6. package/docs/decisions/auto-mode.md +526 -0
  7. package/docs/decisions/branding.md +127 -0
  8. package/docs/decisions/code-review-remediation.md +106 -0
  9. package/docs/decisions/compaction.md +140 -0
  10. package/docs/decisions/distribution.md +133 -0
  11. package/docs/decisions/hooks.md +64 -0
  12. package/docs/decisions/lsp.md +58 -0
  13. package/docs/decisions/mcp.md +104 -0
  14. package/docs/decisions/memory-state.md +170 -0
  15. package/docs/decisions/model-policy.md +126 -0
  16. package/docs/decisions/modes.md +178 -0
  17. package/docs/decisions/skills-plugins.md +56 -0
  18. package/docs/decisions/subagents-workflows.md +225 -0
  19. package/docs/decisions/system-prompt.md +98 -0
  20. package/docs/decisions/tools.md +407 -0
  21. package/docs/decisions/tui.md +202 -0
  22. package/docs/decisions.md +148 -0
  23. package/extensions/ask-user/index.ts +124 -0
  24. package/extensions/ask-user/questions.ts +60 -0
  25. package/extensions/auto-mode/actions.ts +78 -0
  26. package/extensions/auto-mode/classifier.ts +388 -0
  27. package/extensions/auto-mode/config.ts +237 -0
  28. package/extensions/auto-mode/decision-log.ts +60 -0
  29. package/extensions/auto-mode/defaults.ts +65 -0
  30. package/extensions/auto-mode/instructions.ts +74 -0
  31. package/extensions/auto-mode/model-picker.ts +225 -0
  32. package/extensions/auto-mode/model-select.ts +178 -0
  33. package/extensions/auto-mode/paths.ts +109 -0
  34. package/extensions/auto-mode/pause.ts +96 -0
  35. package/extensions/auto-mode/prompt.ts +307 -0
  36. package/extensions/auto-mode/rules.ts +96 -0
  37. package/extensions/auto-mode/safety-floor.ts +100 -0
  38. package/extensions/auto-mode/sensitive.ts +137 -0
  39. package/extensions/auto-mode/shell-analysis.ts +702 -0
  40. package/extensions/background/index.ts +405 -0
  41. package/extensions/background/registry.ts +81 -0
  42. package/extensions/background/wakeup.ts +41 -0
  43. package/extensions/bash/background.ts +136 -0
  44. package/extensions/bash/index.ts +149 -0
  45. package/extensions/branding/assistant-marker.ts +53 -0
  46. package/extensions/branding/index.ts +366 -0
  47. package/extensions/branding/prompt-editor.ts +42 -0
  48. package/extensions/branding/prompt-marker.ts +29 -0
  49. package/extensions/branding/startup.ts +152 -0
  50. package/extensions/claude-compat/index.ts +50 -0
  51. package/extensions/claude-context/index.ts +79 -0
  52. package/extensions/compaction/index.ts +231 -0
  53. package/extensions/compaction/prompt.ts +191 -0
  54. package/extensions/context-management/index.ts +147 -0
  55. package/extensions/effort/index.ts +216 -0
  56. package/extensions/effort/slider.ts +253 -0
  57. package/extensions/exit/index.ts +28 -0
  58. package/extensions/file-tracker/index.ts +103 -0
  59. package/extensions/file-tracker/tracker.ts +145 -0
  60. package/extensions/hooks/debug.ts +53 -0
  61. package/extensions/hooks/executor.ts +132 -0
  62. package/extensions/hooks/index.ts +310 -0
  63. package/extensions/hooks/matcher.ts +72 -0
  64. package/extensions/hooks/plugin-hooks.ts +79 -0
  65. package/extensions/hooks/protocol.ts +151 -0
  66. package/extensions/hooks/settings.ts +163 -0
  67. package/extensions/hooks/trust.ts +151 -0
  68. package/extensions/lib/branch-restore.ts +35 -0
  69. package/extensions/lib/claude-context.ts +148 -0
  70. package/extensions/lib/deferred.ts +119 -0
  71. package/extensions/lib/git.ts +14 -0
  72. package/extensions/lib/memory.ts +219 -0
  73. package/extensions/lib/model-policy.ts +436 -0
  74. package/extensions/lib/model-tier.ts +99 -0
  75. package/extensions/lib/notifications.ts +18 -0
  76. package/extensions/lib/paths.ts +26 -0
  77. package/extensions/lib/per-cwd.ts +21 -0
  78. package/extensions/lib/persisted-output.ts +70 -0
  79. package/extensions/lib/pi-version.ts +54 -0
  80. package/extensions/lib/plugins.ts +227 -0
  81. package/extensions/lib/reminders.ts +212 -0
  82. package/extensions/lib/scratchpad.ts +68 -0
  83. package/extensions/lib/tui-render.ts +522 -0
  84. package/extensions/lsp/client.ts +278 -0
  85. package/extensions/lsp/format.ts +46 -0
  86. package/extensions/lsp/index.ts +193 -0
  87. package/extensions/lsp/protocol.ts +64 -0
  88. package/extensions/lsp/servers.ts +79 -0
  89. package/extensions/mcp/client.ts +205 -0
  90. package/extensions/mcp/config.ts +199 -0
  91. package/extensions/mcp/index.ts +373 -0
  92. package/extensions/mcp/schema.ts +161 -0
  93. package/extensions/memory/index.ts +90 -0
  94. package/extensions/notebook/index.ts +77 -0
  95. package/extensions/notebook/notebook.ts +108 -0
  96. package/extensions/permissions/index.ts +876 -0
  97. package/extensions/permissions/matcher.ts +455 -0
  98. package/extensions/permissions/modes.ts +107 -0
  99. package/extensions/permissions/protected-paths.ts +125 -0
  100. package/extensions/permissions/settings.ts +98 -0
  101. package/extensions/plan-mode/index.ts +209 -0
  102. package/extensions/plan-mode/reminder.ts +33 -0
  103. package/extensions/plan-mode/slug.ts +28 -0
  104. package/extensions/plan-mode/viewer.ts +124 -0
  105. package/extensions/plugins/index.ts +115 -0
  106. package/extensions/plugins/template.ts +42 -0
  107. package/extensions/search-tools/index.ts +0 -0
  108. package/extensions/search-tools/tools.ts +24 -0
  109. package/extensions/skill/index.ts +170 -0
  110. package/extensions/subagents/agents.ts +126 -0
  111. package/extensions/subagents/child.ts +414 -0
  112. package/extensions/subagents/default-model.ts +122 -0
  113. package/extensions/subagents/index.ts +1119 -0
  114. package/extensions/subagents/model-select.ts +346 -0
  115. package/extensions/subagents/rpc-turns.ts +123 -0
  116. package/extensions/subagents/runs.ts +93 -0
  117. package/extensions/subagents/usage.ts +53 -0
  118. package/extensions/subagents/worktree.ts +69 -0
  119. package/extensions/system-prompt/environment.ts +35 -0
  120. package/extensions/system-prompt/index.ts +54 -0
  121. package/extensions/system-prompt/template.ts +103 -0
  122. package/extensions/system-prompt/tiers/common.ts +63 -0
  123. package/extensions/system-prompt/tiers/frontier.ts +24 -0
  124. package/extensions/system-prompt/tiers/low.ts +71 -0
  125. package/extensions/system-prompt/tiers/mid.ts +56 -0
  126. package/extensions/system-reminder/index.ts +37 -0
  127. package/extensions/tasks/index.ts +140 -0
  128. package/extensions/tasks/store.ts +174 -0
  129. package/extensions/todo/index.ts +133 -0
  130. package/extensions/tool-search/index.ts +168 -0
  131. package/extensions/tool-style/index.ts +84 -0
  132. package/extensions/web/index.ts +43 -0
  133. package/extensions/web-fetch/extract.ts +139 -0
  134. package/extensions/web-fetch/index.ts +291 -0
  135. package/extensions/web-fetch/summarize.ts +78 -0
  136. package/extensions/workflow/agent-session.ts +368 -0
  137. package/extensions/workflow/globals.ts +251 -0
  138. package/extensions/workflow/index.ts +364 -0
  139. package/extensions/workflow/journal.ts +75 -0
  140. package/extensions/workflow/permission-gate.ts +71 -0
  141. package/extensions/workflow/run-manager.ts +306 -0
  142. package/extensions/workflow/saved-workflows.ts +45 -0
  143. package/extensions/workflow/script-source.ts +201 -0
  144. package/extensions/workflow/types.ts +99 -0
  145. package/extensions/workflow/vm-runtime.ts +31 -0
  146. package/extensions/workflow/widget.ts +56 -0
  147. package/extensions/worktree/index.ts +266 -0
  148. package/extensions/worktree/rewrite.ts +59 -0
  149. package/package.json +101 -0
  150. package/themes/one-code-light.json +76 -0
  151. package/themes/one-code.json +76 -0
  152. package/types/app.d.ts +5 -0
  153. package/types/community-packages.d.ts +12 -0
@@ -0,0 +1,436 @@
1
+ import type { Api, Model } from "@earendil-works/pi-ai";
2
+
3
+ /** The two automatic model-selection jobs have different capability floors. */
4
+ export type ModelRole = "classifier" | "subagent";
5
+
6
+ export interface RoleProfile {
7
+ /** Vetted, small-but-capable models. Price alone never adds to this list. */
8
+ classifier: readonly string[];
9
+ /** Economical models suitable for delegated coding/reasoning work. */
10
+ subagent: readonly string[];
11
+ }
12
+
13
+ /**
14
+ * Short family lists, not catalogs. Broad gateways apply these to their live
15
+ * authenticated catalog after containment; they never get an OpenRouter-sized
16
+ * hardcoded list. Reviewed against official provider docs on 2026-08-06.
17
+ */
18
+ export const ROLE_PROFILES: Readonly<Record<string, RoleProfile>> = {
19
+ anthropic: {
20
+ classifier: ["claude-haiku-4-5", "claude-sonnet-5", "claude-sonnet-4-6"],
21
+ subagent: ["claude-sonnet-5", "claude-haiku-4-5", "claude-sonnet-4-6"],
22
+ },
23
+ openai: {
24
+ classifier: ["gpt-5.6-luna", "gpt-5.4-mini", "gpt-5-mini", "gpt-4.1-mini", "gpt-4o-mini"],
25
+ subagent: ["gpt-5.6-luna", "gpt-5.4-mini", "gpt-5.3-codex-spark", "gpt-5.6-terra"],
26
+ },
27
+ google: {
28
+ classifier: ["gemini-3.6-flash", "gemini-3.5-flash", "gemini-3-flash-preview", "gemini-2.5-flash"],
29
+ subagent: ["gemini-3.6-flash", "gemini-3.5-flash", "gemini-3-flash-preview", "gemini-2.5-flash"],
30
+ },
31
+ xai: {
32
+ classifier: ["grok-4.3", "grok-4.5"],
33
+ subagent: ["grok-build-0.1", "grok-4.3", "grok-4.5"],
34
+ },
35
+ mistral: {
36
+ classifier: ["mistral-small-2603", "mistral-small-latest", "mistral-medium-3.5"],
37
+ subagent: ["mistral-small-2603", "mistral-medium-3.5", "codestral-latest"],
38
+ },
39
+ deepseek: {
40
+ classifier: ["deepseek-v4-flash", "deepseek-v4-pro"],
41
+ subagent: ["deepseek-v4-flash", "deepseek-v4-pro"],
42
+ },
43
+ zai: {
44
+ classifier: ["glm-5-turbo", "glm-4.7", "glm-5.2", "glm-4.5-air", "glm-4-flash"],
45
+ subagent: ["glm-5-turbo", "glm-4.7", "glm-5.2", "glm-4.5-air"],
46
+ },
47
+ moonshot: {
48
+ classifier: ["kimi-k2.6", "kimi-k2.5"],
49
+ subagent: ["kimi-k2.7-code", "kimi-k2.6", "kimi-k3"],
50
+ },
51
+ minimax: {
52
+ classifier: ["MiniMax-M2.7", "MiniMax-M3"],
53
+ subagent: ["MiniMax-M2.7", "MiniMax-M3"],
54
+ },
55
+ xiaomi: {
56
+ classifier: ["mimo-v2.5", "mimo-v2-pro"],
57
+ subagent: ["mimo-v2.5", "mimo-v2.5-pro", "mimo-v2-pro"],
58
+ },
59
+ "ant-ling": {
60
+ // The current pi catalog predates Ling 3.0. Flash remains opt-in: it has
61
+ // not been calibrated as the approval boundary.
62
+ classifier: ["Ling-2.6-1T", "Ring-2.6-1T"],
63
+ subagent: ["Ling-2.6-1T", "Ring-2.6-1T", "Ling-2.6-flash"],
64
+ },
65
+ meta: {
66
+ classifier: ["llama-3.3-70b", "llama-4-scout"],
67
+ subagent: ["llama-4-scout", "llama-3.3-70b"],
68
+ },
69
+ amazon: {
70
+ classifier: ["nova-2-lite", "nova-lite"],
71
+ subagent: ["nova-2-lite", "nova-lite", "nova-pro"],
72
+ },
73
+ "host:nvidia": {
74
+ classifier: ["nvidia/nvidia-nemotron-nano-9b-v2", "openai/gpt-oss-20b", "meta/llama-3.3-70b-instruct"],
75
+ subagent: ["poolside/laguna-xs-2.1", "nvidia/nemotron-3-super-120b-a12b", "openai/gpt-oss-20b"],
76
+ },
77
+ "host:groq": {
78
+ classifier: ["openai/gpt-oss-20b", "llama-3.3-70b-versatile"],
79
+ subagent: ["openai/gpt-oss-20b", "openai/gpt-oss-120b", "llama-3.3-70b-versatile"],
80
+ },
81
+ "host:cerebras": {
82
+ classifier: ["gpt-oss-120b", "zai-glm-4.7"],
83
+ subagent: ["gpt-oss-120b", "zai-glm-4.7"],
84
+ },
85
+ "host:fireworks": {
86
+ classifier: ["accounts/fireworks/models/deepseek-v4-flash", "accounts/fireworks/models/gpt-oss-20b"],
87
+ subagent: ["accounts/fireworks/models/kimi-k2p7-code", "accounts/fireworks/models/gpt-oss-20b"],
88
+ },
89
+ "host:together": {
90
+ classifier: ["Qwen/Qwen3.5-9B", "openai/gpt-oss-20b"],
91
+ subagent: ["openai/gpt-oss-20b", "moonshotai/Kimi-K2.7-Code", "Qwen/Qwen3.5-9B"],
92
+ },
93
+ "host:baseten": {
94
+ classifier: ["openai/gpt-oss-120b", "deepseek-ai/DeepSeek-V4-Flash"],
95
+ subagent: ["openai/gpt-oss-120b", "zai-org/GLM-5.2", "moonshotai/Kimi-K2.7-Code"],
96
+ },
97
+ "host:cloudflare-workers": {
98
+ classifier: ["@cf/openai/gpt-oss-20b", "@cf/zai-org/glm-4.7-flash"],
99
+ subagent: ["@cf/moonshotai/kimi-k2.7-code", "@cf/openai/gpt-oss-20b"],
100
+ },
101
+ "host:qwen-plan": {
102
+ classifier: ["qwen3.6-flash", "deepseek-v4-flash", "qwen3.6-plus"],
103
+ subagent: ["kimi-k2.7-code", "deepseek-v4-flash", "qwen3.7-plus", "qwen3.6-plus"],
104
+ },
105
+ "host:kimi-coding": {
106
+ classifier: ["kimi-for-coding", "k3"],
107
+ subagent: ["kimi-for-coding", "k3"],
108
+ },
109
+ };
110
+
111
+ export type ProviderPolicyKind = "direct" | "hosted" | "gateway" | "opaque";
112
+
113
+ export interface ProviderPolicy {
114
+ kind: ProviderPolicyKind;
115
+ /** Fixed profile for direct vendors and stable hosted catalogs. */
116
+ profile?: string;
117
+ /** Whether a price-ranked subagent may follow the vetted profile. */
118
+ dynamicSubagents: boolean;
119
+ }
120
+
121
+ const direct = (profile: string): ProviderPolicy => ({ kind: "direct", profile, dynamicSubagents: true });
122
+ const hosted = (profile?: string): ProviderPolicy => ({ kind: "hosted", profile, dynamicSubagents: true });
123
+ const gateway = (): ProviderPolicy => ({ kind: "gateway", dynamicSubagents: true });
124
+ const opaque = (): ProviderPolicy => ({ kind: "opaque", dynamicSubagents: false });
125
+
126
+ /**
127
+ * Every built-in language-model provider in pi 0.84.0. Unknown/custom providers
128
+ * get the opaque policy below, so a pi upgrade fails safe rather than widening
129
+ * automatic routing.
130
+ */
131
+ export const BUILTIN_PROVIDER_POLICIES: Readonly<Record<string, ProviderPolicy>> = {
132
+ "amazon-bedrock": hosted(),
133
+ "ant-ling": direct("ant-ling"),
134
+ anthropic: direct("anthropic"),
135
+ google: direct("google"),
136
+ "google-vertex": direct("google"),
137
+ openai: direct("openai"),
138
+ "azure-openai-responses": direct("openai"),
139
+ "openai-codex": direct("openai"),
140
+ radius: opaque(),
141
+ nvidia: hosted("host:nvidia"),
142
+ deepseek: direct("deepseek"),
143
+ "github-copilot": hosted(),
144
+ xai: direct("xai"),
145
+ groq: hosted("host:groq"),
146
+ cerebras: hosted("host:cerebras"),
147
+ openrouter: gateway(),
148
+ "vercel-ai-gateway": gateway(),
149
+ zai: direct("zai"),
150
+ "zai-coding-cn": direct("zai"),
151
+ mistral: direct("mistral"),
152
+ minimax: direct("minimax"),
153
+ "minimax-cn": direct("minimax"),
154
+ moonshotai: direct("moonshot"),
155
+ "moonshotai-cn": direct("moonshot"),
156
+ huggingface: opaque(),
157
+ fireworks: hosted("host:fireworks"),
158
+ together: hosted("host:together"),
159
+ baseten: hosted("host:baseten"),
160
+ opencode: opaque(),
161
+ "opencode-go": opaque(),
162
+ "kimi-coding": direct("host:kimi-coding"),
163
+ "cloudflare-workers-ai": hosted("host:cloudflare-workers"),
164
+ "cloudflare-ai-gateway": gateway(),
165
+ "qwen-token-plan": hosted("host:qwen-plan"),
166
+ "qwen-token-plan-cn": hosted("host:qwen-plan"),
167
+ xiaomi: direct("xiaomi"),
168
+ "xiaomi-token-plan-cn": direct("xiaomi"),
169
+ "xiaomi-token-plan-ams": direct("xiaomi"),
170
+ "xiaomi-token-plan-sgp": direct("xiaomi"),
171
+ };
172
+
173
+ const VENDOR_ALIASES: Readonly<Record<string, string>> = {
174
+ anthropic: "anthropic",
175
+ openai: "openai",
176
+ google: "google",
177
+ xai: "xai",
178
+ "x-ai": "xai",
179
+ mistral: "mistral",
180
+ mistralai: "mistral",
181
+ deepseek: "deepseek",
182
+ zai: "zai",
183
+ "z-ai": "zai",
184
+ qwen: "host:qwen-plan",
185
+ alibaba: "host:qwen-plan",
186
+ moonshotai: "moonshot",
187
+ minimax: "minimax",
188
+ minimaxai: "minimax",
189
+ xiaomi: "xiaomi",
190
+ meta: "meta",
191
+ "meta-llama": "meta",
192
+ amazon: "amazon",
193
+ };
194
+
195
+ export interface ModelIdentity {
196
+ /** Models may be switched automatically only when this key stays equal. */
197
+ containment: string;
198
+ /** Role profile to apply to normalizedId, when identity is reliable. */
199
+ profile?: string;
200
+ normalizedId: string;
201
+ confidence: "exact" | "family" | "opaque";
202
+ }
203
+
204
+ const providerPolicy = (provider: string): ProviderPolicy => BUILTIN_PROVIDER_POLICIES[provider] ?? opaque();
205
+
206
+ function canonicalVendor(value: string): string | undefined {
207
+ return VENDOR_ALIASES[value.toLowerCase()];
208
+ }
209
+
210
+ function prefixedIdentity(model: Model<Api>, prefix: string): ModelIdentity {
211
+ const raw = model.id.startsWith("~") ? model.id.slice(1) : model.id;
212
+ const slash = raw.indexOf("/");
213
+ if (slash <= 0) {
214
+ return { containment: `${model.provider}:opaque:${model.id}`, normalizedId: model.id, confidence: "opaque" };
215
+ }
216
+ const route = raw.slice(0, slash).toLowerCase();
217
+ const normalizedId = raw.slice(slash + 1);
218
+ return {
219
+ containment: `${model.provider}:${prefix}:${route}`,
220
+ profile: canonicalVendor(route),
221
+ normalizedId,
222
+ confidence: canonicalVendor(route) ? "family" : "opaque",
223
+ };
224
+ }
225
+
226
+ function bareFamily(id: string): string | undefined {
227
+ if (/^claude-/i.test(id)) return "anthropic";
228
+ if (/^(gpt-|o\d(?:-|$))/i.test(id)) return "openai";
229
+ if (/^(gemini-|gemma-)/i.test(id)) return "google";
230
+ if (/^grok-/i.test(id)) return "xai";
231
+ if (/^deepseek-/i.test(id)) return "deepseek";
232
+ if (/^kimi-/i.test(id)) return "moonshot";
233
+ if (/^minimax-/i.test(id)) return "minimax";
234
+ if (/^glm-/i.test(id)) return "zai";
235
+ if (/^qwen/i.test(id)) return "host:qwen-plan";
236
+ if (/^mimo-/i.test(id)) return "xiaomi";
237
+ if (/^llama-/i.test(id)) return "meta";
238
+ return undefined;
239
+ }
240
+
241
+ function bedrockIdentity(model: Model<Api>): ModelIdentity {
242
+ const geography = model.id.match(/^(us|eu|apac|au|global)\./i)?.[1]?.toLowerCase() ?? "in-region";
243
+ const raw = model.id.replace(/^(?:us|eu|apac|au|global)\./i, "");
244
+ const dot = raw.indexOf(".");
245
+ if (dot <= 0 || raw.startsWith("arn:")) {
246
+ return { containment: `${model.provider}:opaque:${model.id}`, normalizedId: model.id, confidence: "opaque" };
247
+ }
248
+ const vendor = raw.slice(0, dot).toLowerCase();
249
+ const profile = canonicalVendor(vendor);
250
+ return {
251
+ containment: `${model.provider}:geography:${geography}:family:${vendor}`,
252
+ profile,
253
+ normalizedId: raw.slice(dot + 1).replaceAll("_", "-").replace(/-v\d(?::\d+)?$/i, ""),
254
+ confidence: profile ? "family" : "opaque",
255
+ };
256
+ }
257
+
258
+ function cloudflareGatewayIdentity(model: Model<Api>): ModelIdentity {
259
+ if (model.id.startsWith("workers-ai/")) {
260
+ return {
261
+ containment: `${model.provider}:route:workers-ai`,
262
+ profile: "host:cloudflare-workers",
263
+ normalizedId: model.id.slice("workers-ai/".length),
264
+ confidence: "exact",
265
+ };
266
+ }
267
+ const api = String(model.api);
268
+ const route = api === "anthropic-messages" ? "anthropic" : api.includes("openai") ? "openai" : undefined;
269
+ if (!route) {
270
+ return { containment: `${model.provider}:opaque:${model.id}`, normalizedId: model.id, confidence: "opaque" };
271
+ }
272
+ return {
273
+ containment: `${model.provider}:route:${route}`,
274
+ profile: route,
275
+ normalizedId: model.id,
276
+ confidence: "exact",
277
+ };
278
+ }
279
+
280
+ /** Normalize privacy containment and model-family identity for one catalog row. */
281
+ export function modelIdentity(model: Model<Api>): ModelIdentity {
282
+ const policy = providerPolicy(model.provider);
283
+ if (policy.kind === "opaque") {
284
+ return { containment: `${model.provider}:opaque:${model.id}`, normalizedId: model.id, confidence: "opaque" };
285
+ }
286
+ if (model.provider === "openrouter") return prefixedIdentity(model, "author");
287
+ if (model.provider === "vercel-ai-gateway") return prefixedIdentity(model, "creator");
288
+ if (model.provider === "cloudflare-ai-gateway") return cloudflareGatewayIdentity(model);
289
+ if (model.provider === "amazon-bedrock") return bedrockIdentity(model);
290
+ if (model.provider === "github-copilot") {
291
+ const profile = bareFamily(model.id);
292
+ return profile
293
+ ? { containment: `${model.provider}:family:${profile}`, profile, normalizedId: model.id, confidence: "family" }
294
+ : { containment: `${model.provider}:opaque:${model.id}`, normalizedId: model.id, confidence: "opaque" };
295
+ }
296
+ return {
297
+ containment: model.provider,
298
+ profile: policy.profile,
299
+ normalizedId: model.id,
300
+ confidence: "exact",
301
+ };
302
+ }
303
+
304
+ /**
305
+ * Names of the known small-but-capable model families, best tier first. Too weak
306
+ * to *choose* by — Groq and xAI carry none of them — but strong enough to
307
+ * corroborate a choice price alone suggested, and to order tiers when several
308
+ * cheap models qualify (mini-class ahead of nano/lite-class).
309
+ */
310
+ const NAME_HINTS = [/haiku/i, /flash/i, /mini/i, /nano/i, /small/i, /lite/i, /instant/i, /sonnet/i];
311
+
312
+ /** Index into NAME_HINTS; NAME_HINTS.length means "no known small-model name". */
313
+ export function hintRank(model: Model<Api>): number {
314
+ const index = NAME_HINTS.findIndex((pattern) => pattern.test(model.id));
315
+ return index === -1 ? NAME_HINTS.length : index;
316
+ }
317
+
318
+ /** Whether the model's name carries one of the known small-model family words. */
319
+ export function hasSmallModelName(model: Model<Api>): boolean {
320
+ return hintRank(model) < NAME_HINTS.length;
321
+ }
322
+
323
+ const UNSUITABLE_VARIANT = /:(batch|free|online|thinking)$/i;
324
+
325
+ /** Automatic selection excludes endpoint variants with the wrong execution shape. */
326
+ export function isSelectableVariant(model: Model<Api>): boolean {
327
+ return !UNSUITABLE_VARIANT.test(model.id);
328
+ }
329
+
330
+ /** Non-positive values are catalog sentinels/unpriced, not evidence of being free. */
331
+ export function pricedInput(model: Model<Api>): number | undefined {
332
+ const input = model.cost?.input;
333
+ return typeof input === "number" && input > 0 ? input : undefined;
334
+ }
335
+
336
+ /** Models that automatic selection may consider without changing containment. */
337
+ export function modelsContainedToSession(available: Model<Api>[], sessionModel: Model<Api>): Model<Api>[] {
338
+ const sessionIdentity = modelIdentity(sessionModel);
339
+ return available.filter(
340
+ (model) =>
341
+ model.provider === sessionModel.provider &&
342
+ isSelectableVariant(model) &&
343
+ modelIdentity(model).containment === sessionIdentity.containment,
344
+ );
345
+ }
346
+
347
+ function comparableId(value: string): string {
348
+ return value.toLowerCase().replaceAll(".", "-").replaceAll("_", "-");
349
+ }
350
+
351
+ function matchesPrefix(model: Model<Api>, prefix: string): boolean {
352
+ const id = comparableId(modelIdentity(model).normalizedId);
353
+ const wanted = comparableId(prefix);
354
+ return id === wanted || id.startsWith(wanted);
355
+ }
356
+
357
+ /** First live model matching a role's reviewed family order. */
358
+ export function findRoleProfileModel(
359
+ available: Model<Api>[],
360
+ sessionModel: Model<Api>,
361
+ role: ModelRole,
362
+ accept: (model: Model<Api>) => boolean = () => true,
363
+ // A caller that already computed the containment set can pass it to avoid the
364
+ // O(catalog) recompute (classifierCandidates does, on a per-tool-call hot path).
365
+ precomputedContained?: Model<Api>[],
366
+ ): Model<Api> | undefined {
367
+ const identity = modelIdentity(sessionModel);
368
+ if (!identity.profile) return undefined;
369
+ const prefixes = ROLE_PROFILES[identity.profile]?.[role] ?? [];
370
+ const contained = precomputedContained ?? modelsContainedToSession(available, sessionModel);
371
+ for (const prefix of prefixes) {
372
+ const wanted = comparableId(prefix);
373
+ const exact = contained.find(
374
+ (model) => comparableId(modelIdentity(model).normalizedId) === wanted && accept(model),
375
+ );
376
+ if (exact) return exact;
377
+ const prefixed = contained.find((model) => matchesPrefix(model, prefix) && accept(model));
378
+ if (prefixed) return prefixed;
379
+ }
380
+ return undefined;
381
+ }
382
+
383
+ export interface EconomicalModelChoice {
384
+ model: Model<Api>;
385
+ via: "profile" | "session";
386
+ }
387
+
388
+ /**
389
+ * A vetted small-but-capable same-containment model within the session's
390
+ * price ceiling, else the session model itself. Used for low-stakes one-shot
391
+ * jobs over untrusted content (the web_fetch reader) — the content goes to
392
+ * the model, so containment applies. Not used for compaction, which runs on
393
+ * the session model to reuse the provider prompt cache.
394
+ */
395
+ export function pickEconomicalContainedModel(
396
+ available: Model<Api>[],
397
+ sessionModel: Model<Api> | undefined,
398
+ ): EconomicalModelChoice | undefined {
399
+ if (!sessionModel) return undefined;
400
+ const sessionPrice = pricedInput(sessionModel);
401
+ const withinBudget = (model: Model<Api>) => {
402
+ if (sessionPrice === undefined) return true;
403
+ const price = pricedInput(model);
404
+ return price === undefined || price <= sessionPrice;
405
+ };
406
+ const profiled = findRoleProfileModel(available, sessionModel, "classifier", withinBudget);
407
+ return profiled ? { model: profiled, via: "profile" } : { model: sessionModel, via: "session" };
408
+ }
409
+
410
+ /** Whether price ranking is an acceptable subagent fallback for this session. */
411
+ export function allowsDynamicSubagentSelection(sessionModel: Model<Api>): boolean {
412
+ const policy = providerPolicy(sessionModel.provider);
413
+ if (!policy.dynamicSubagents) return false;
414
+ return modelIdentity(sessionModel).confidence !== "opaque";
415
+ }
416
+
417
+ /** Resolve an explicit provider/id, bare id, or prefix. Explicit choices are not contained. */
418
+ export function findConfigured(available: Model<Api>[], configured: string): Model<Api> | undefined {
419
+ const wanted = configured.trim();
420
+ if (!wanted) return undefined;
421
+ const qualified = available.find((model) => `${model.provider}/${model.id}` === wanted);
422
+ if (qualified) return qualified;
423
+ const byId = available.find((model) => model.id === wanted);
424
+ if (byId) return byId;
425
+ const slash = wanted.indexOf("/");
426
+ if (slash > 0) {
427
+ const provider = wanted.slice(0, slash);
428
+ const idPrefix = wanted.slice(slash + 1);
429
+ const inProvider = available.filter((model) => model.provider === provider);
430
+ const exact = inProvider.find((model) => model.id === idPrefix);
431
+ if (exact) return exact;
432
+ const prefixed = inProvider.find((model) => model.id.startsWith(idPrefix));
433
+ if (prefixed) return prefixed;
434
+ }
435
+ return available.find((model) => model.id.startsWith(wanted));
436
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Which system-prompt register the active model gets. Claude Code ships a
3
+ * capability-tiered prompt — a terse one for frontier models, a much longer,
4
+ * more explicit one for smaller models (Anthropic removed 80%+ of the prompt for
5
+ * Claude-5-gen models with no measured loss). One Code serves every provider, so
6
+ * its non-frontier audience is dominated by models weaker than Haiku; a single
7
+ * lean prompt under-instructs them. This module maps a model to one of three
8
+ * registers; `extensions/system-prompt` picks the prompt text from the result.
9
+ *
10
+ * The classification obeys the repo convention against id-substring matching
11
+ * (see `auto-mode/model-select.ts`): the only id parsing is structural version
12
+ * parsing on first-party Anthropic ids (a namespace we control); for every other
13
+ * provider the decision rests on price + containment, with a name hint as a
14
+ * corroborating signal, never the lead.
15
+ */
16
+
17
+ import type { Api, Model } from "@earendil-works/pi-ai";
18
+ import { modelIdentity, pricedInput } from "./model-policy.ts";
19
+
20
+ export type PromptTier = "frontier" | "mid" | "low";
21
+
22
+ /**
23
+ * `CC_PROMPT_TIER=frontier|mid|low` forces a register; `auto`/unset classifies.
24
+ * Env only — a project must not be able to downgrade its own scaffolding, the
25
+ * same reasoning that keeps `autoMode` out of project settings.
26
+ */
27
+ export function tierOverride(env: NodeJS.ProcessEnv = process.env): PromptTier | undefined {
28
+ const raw = env.CC_PROMPT_TIER?.trim().toLowerCase();
29
+ return raw === "frontier" || raw === "mid" || raw === "low" ? raw : undefined;
30
+ }
31
+
32
+ /**
33
+ * Anthropic first-party frontier gate: Opus/Fable ≥ 4.8 or Sonnet ≥ 5. Adapted
34
+ * from pi-ai's `defaultSupportsToolReferences` — never Haiku, and the
35
+ * `length < 8` guard stops a dated suffix (`claude-opus-4-8-20251101`) being read
36
+ * as the minor version. Version parse, not price: `claude-opus-4-1` ($15/M) costs
37
+ * more than `opus-4-8`/`opus-5` ($5/M), so cost ranking would misclassify it.
38
+ */
39
+ function isAnthropicFrontier(model: Model<Api>): boolean {
40
+ if (model.provider !== "anthropic" || model.id.includes("haiku")) return false;
41
+ const version = model.id.match(/^claude-(opus|sonnet|fable)-(\d+)(?:-(\d+))?(?:-|$)/);
42
+ if (!version) return false;
43
+ const family = version[1];
44
+ const major = Number(version[2]);
45
+ const minor = version[3] && version[3].length < 8 ? Number(version[3]) : 0;
46
+ return family === "sonnet" ? major >= 5 : major > 4 || (major === 4 && minor >= 8);
47
+ }
48
+
49
+ /**
50
+ * Small-model name words that justify the low tier, matched as delimited tokens.
51
+ * Delimiting matters: a bare `/mini/` matches inside "geMINI", which would drag
52
+ * every Gemini model (pro included) to low. Deliberately narrower than
53
+ * model-policy's `NAME_HINTS`, which also counts "haiku"/"sonnet" — those are
54
+ * mid-class, and a gateway-proxied `…/claude-sonnet-5` reaching this branch must
55
+ * not be demoted to the weakest prompt on the strength of its name.
56
+ */
57
+ const LOW_TIER_NAME_HINT = /(?:^|[-/.])(?:flash|mini|nano|small|lite|instant)(?:[-/.]|$)/i;
58
+
59
+ /**
60
+ * A "pro"-class name marks the capable variant of a family (deepseek-v4-pro,
61
+ * gemini-3-pro, gpt-5-pro) — a capability-up signal that keeps a cheap or
62
+ * unpriced flagship in mid rather than low. Matched as a delimited token so it
63
+ * fires on `-pro`/`pro-` but not on `prometheus`, `improved`, or `proxy`.
64
+ */
65
+ const CAPABLE_NAME_HINT = /(?:^|[-/.])pro(?:[-/.]|$)/i;
66
+
67
+ /**
68
+ * Below this input price (USD per million tokens) a model with no other signal
69
+ * is treated as small. Calibrated against the live catalogs: capable flagships
70
+ * sit at ≥ $1/M (gpt-5 1.25, grok 1.25, gemini-pro 1.25–2), while mini/nano/flash
71
+ * classes sit well under $0.5. The boundary is intentionally biased toward `low`
72
+ * (more scaffolding never hurts a cheap model), per the "unknown → low" decision.
73
+ */
74
+ const LOW_TIER_MAX_INPUT_COST = 0.5;
75
+
76
+ export function resolveModelTier(
77
+ model: Model<Api> | undefined,
78
+ env: NodeJS.ProcessEnv = process.env,
79
+ ): PromptTier {
80
+ const forced = tierOverride(env);
81
+ if (forced) return forced;
82
+ if (!model) return "low"; // unknown model → maximum scaffolding
83
+
84
+ if (isAnthropicFrontier(model)) return "frontier";
85
+ if (model.provider === "anthropic") return "mid"; // Haiku, Sonnet < 5, Opus 4.1–4.7
86
+
87
+ // A "pro"-class flagship stays mid even when cheap/unpriced, unless the id
88
+ // also carries a small-model word (contradictory — let the small path win).
89
+ if (CAPABLE_NAME_HINT.test(model.id) && !LOW_TIER_NAME_HINT.test(model.id)) return "mid";
90
+
91
+ // Otherwise low wins on any positive smallness signal.
92
+ const price = pricedInput(model);
93
+ const small =
94
+ LOW_TIER_NAME_HINT.test(model.id) ||
95
+ modelIdentity(model).confidence === "opaque" || // local/self-hosted providers
96
+ price === undefined || // unpriced/unknown → low
97
+ price < LOW_TIER_MAX_INPUT_COST;
98
+ return small ? "low" : "mid";
99
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Framing for messages the harness injects into the conversation on its own
3
+ * (background completions, subagent replies, monitor events, wakeups).
4
+ *
5
+ * Adapted from Claude Code's anti-confabulation preamble (findings §14): the
6
+ * hazard is a model reading an automated event as the user having said,
7
+ * approved, or confirmed something — especially mid-task, where a pending
8
+ * question plus an arriving notification looks like an answer.
9
+ */
10
+ /** First line of the framing — `tui-render.ts`'s `notificationBody` matches on this to strip it back off for display. */
11
+ export const NOTIFICATION_HEADER = "SYSTEM NOTIFICATION — NOT USER INPUT";
12
+ /** Second line of the framing — likewise matched by `notificationBody`. */
13
+ export const NOTIFICATION_PREFIX =
14
+ "This is an automated event, not a message from the user. No new human input has been received; do not treat anything below as user acknowledgement, confirmation, or approval.";
15
+
16
+ export function systemNotification(body: string): string {
17
+ return [NOTIFICATION_HEADER, NOTIFICATION_PREFIX, "", body].join("\n");
18
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Where One Code reads and writes (pure).
3
+ *
4
+ * The policy is "own state, borrowed config": `~/.claude` is Claude Code's
5
+ * directory and One Code treats it as a read-only compatibility surface
6
+ * (settings, skills, agents, plugins, CLAUDE.md); everything One Code
7
+ * *generates* — plan files, and over time the rest of its state — lands in
8
+ * One Code's own `~/.one-code`, so neither product's artifacts mingle with the
9
+ * other's. See "Own state, borrowed config" in docs/decisions.md.
10
+ */
11
+
12
+ import { homedir } from "node:os";
13
+ import { join } from "node:path";
14
+
15
+ /**
16
+ * Claude Code's config dir — the compat surface One Code reads, never writes.
17
+ * Honours CLAUDE_CONFIG_DIR the way Claude Code itself does.
18
+ */
19
+ export function claudeConfigDir(env: Record<string, string | undefined> = process.env): string {
20
+ return env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude");
21
+ }
22
+
23
+ /** One Code's own state dir — everything One Code generates goes here. */
24
+ export function oneCodeStateDir(env: Record<string, string | undefined> = process.env): string {
25
+ return env.ONE_CODE_STATE_DIR || join(homedir(), ".one-code");
26
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Memoized get-or-create-by-cwd factory.
3
+ *
4
+ * Several tool definitions from pi (bash, read, write, edit, grep, find, ls)
5
+ * close over their working directory at construction time, but a session's
6
+ * cwd can change mid-run (worktree switches). Extensions that wrap those
7
+ * definitions re-create them per cwd and cache the result so a given cwd only
8
+ * pays construction cost once.
9
+ */
10
+
11
+ export function perCwd<T>(create: (cwd: string) => T): (cwd: string) => T {
12
+ const cache = new Map<string, T>();
13
+ return (cwd: string): T => {
14
+ let value = cache.get(cwd);
15
+ if (!value) {
16
+ value = create(cwd);
17
+ cache.set(cwd, value);
18
+ }
19
+ return value;
20
+ };
21
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Claude Code-style persistence for oversized tool results (pure apart from
3
+ * the file write).
4
+ *
5
+ * pi's built-in tools already bound their own output — bash truncates to a
6
+ * temp file, read/grep paginate — but an extension-registered tool returns
7
+ * whatever it returns, and an MCP server can hand back megabytes that would
8
+ * land verbatim in the context window. Claude Code's answer, matched here
9
+ * byte-for-byte so the model recognises the convention: write the full result
10
+ * to `<session-dir>/tool-results/<tool-call-id>.txt` and return a
11
+ * `<persisted-output>` block naming the file, its size, and a short preview.
12
+ * The model then reads or greps the file for the parts it actually needs.
13
+ */
14
+
15
+ import { mkdirSync, writeFileSync } from "node:fs";
16
+ import { join } from "node:path";
17
+
18
+ /** Claude Code persists around this size; pi's bash truncation uses 50KB too. */
19
+ export const PERSIST_MAX_BYTES = 50 * 1024;
20
+ export const PREVIEW_BYTES = 2 * 1024;
21
+
22
+ function formatSize(bytes: number): string {
23
+ return bytes < 1024 * 1024 ? `${(bytes / 1024).toFixed(1)}KB` : `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
24
+ }
25
+
26
+ export interface PersistOptions {
27
+ /** Directory the `tool-results/` folder is created under (the session dir). */
28
+ dir: string;
29
+ /** File stem — the tool call id, matching Claude Code's `toolu_*.txt` naming. */
30
+ id: string;
31
+ maxBytes?: number;
32
+ previewBytes?: number;
33
+ }
34
+
35
+ /**
36
+ * Pass text through untouched while it fits; past the limit, persist it and
37
+ * return the block the model sees instead. A failed write degrades to the
38
+ * preview with the error named — never to the full text, which is exactly the
39
+ * context blowout this exists to prevent.
40
+ */
41
+ export function persistIfLarge(text: string, options: PersistOptions): string {
42
+ const maxBytes = options.maxBytes ?? PERSIST_MAX_BYTES;
43
+ const size = Buffer.byteLength(text, "utf-8");
44
+ if (size <= maxBytes) return text;
45
+
46
+ const previewBytes = options.previewBytes ?? PREVIEW_BYTES;
47
+ // Character slice, not byte slice: close enough to the target size, and it
48
+ // cannot split a multibyte character.
49
+ const preview = text.slice(0, previewBytes);
50
+
51
+ const file = join(options.dir, "tool-results", `${options.id.replace(/[^A-Za-z0-9._-]/g, "_")}.txt`);
52
+ let saved: string;
53
+ try {
54
+ mkdirSync(join(options.dir, "tool-results"), { recursive: true });
55
+ writeFileSync(file, text);
56
+ saved = `Output too large (${formatSize(size)}). Full output saved to: ${file}`;
57
+ } catch (error) {
58
+ saved = `Output too large (${formatSize(size)}) and could not be saved to ${file}: ${(error as Error).message}. Only this preview survives — re-run with a narrower query if more is needed.`;
59
+ }
60
+
61
+ return [
62
+ "<persisted-output>",
63
+ saved,
64
+ "",
65
+ `Preview (first ${Math.round(previewBytes / 1024)}KB):`,
66
+ preview,
67
+ "...",
68
+ "</persisted-output>",
69
+ ].join("\n");
70
+ }