omegon 0.6.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 (160) hide show
  1. package/.gitattributes +3 -0
  2. package/AGENTS.md +16 -0
  3. package/LICENSE +15 -0
  4. package/README.md +289 -0
  5. package/bin/pi.mjs +30 -0
  6. package/extensions/00-secrets/index.ts +1126 -0
  7. package/extensions/01-auth/auth.ts +401 -0
  8. package/extensions/01-auth/index.ts +289 -0
  9. package/extensions/auto-compact.ts +42 -0
  10. package/extensions/bootstrap/deps.ts +291 -0
  11. package/extensions/bootstrap/index.ts +811 -0
  12. package/extensions/chronos/chronos.sh +487 -0
  13. package/extensions/chronos/index.ts +148 -0
  14. package/extensions/cleave/assessment.ts +754 -0
  15. package/extensions/cleave/bridge.ts +31 -0
  16. package/extensions/cleave/conflicts.ts +250 -0
  17. package/extensions/cleave/dispatcher.ts +808 -0
  18. package/extensions/cleave/guardrails.ts +426 -0
  19. package/extensions/cleave/index.ts +3121 -0
  20. package/extensions/cleave/lifecycle-emitter.ts +20 -0
  21. package/extensions/cleave/openspec.ts +811 -0
  22. package/extensions/cleave/planner.ts +260 -0
  23. package/extensions/cleave/review.ts +579 -0
  24. package/extensions/cleave/skills.ts +355 -0
  25. package/extensions/cleave/types.ts +261 -0
  26. package/extensions/cleave/workspace.ts +861 -0
  27. package/extensions/cleave/worktree.ts +243 -0
  28. package/extensions/core-renderers.ts +253 -0
  29. package/extensions/dashboard/context-gauge.ts +58 -0
  30. package/extensions/dashboard/file-watch.ts +14 -0
  31. package/extensions/dashboard/footer.ts +1145 -0
  32. package/extensions/dashboard/git.ts +185 -0
  33. package/extensions/dashboard/index.ts +478 -0
  34. package/extensions/dashboard/memory-audit.ts +34 -0
  35. package/extensions/dashboard/overlay-data.ts +705 -0
  36. package/extensions/dashboard/overlay.ts +365 -0
  37. package/extensions/dashboard/render-utils.ts +54 -0
  38. package/extensions/dashboard/types.ts +191 -0
  39. package/extensions/dashboard/uri-helper.ts +45 -0
  40. package/extensions/debug.ts +69 -0
  41. package/extensions/defaults.ts +282 -0
  42. package/extensions/design-tree/dashboard-state.ts +161 -0
  43. package/extensions/design-tree/design-card.ts +362 -0
  44. package/extensions/design-tree/index.ts +2130 -0
  45. package/extensions/design-tree/lifecycle-emitter.ts +41 -0
  46. package/extensions/design-tree/tree.ts +1607 -0
  47. package/extensions/design-tree/types.ts +163 -0
  48. package/extensions/distill.ts +127 -0
  49. package/extensions/effort/index.ts +395 -0
  50. package/extensions/effort/tiers.ts +146 -0
  51. package/extensions/effort/types.ts +105 -0
  52. package/extensions/lib/git-state.ts +227 -0
  53. package/extensions/lib/local-models.ts +157 -0
  54. package/extensions/lib/model-preferences.ts +51 -0
  55. package/extensions/lib/model-routing.ts +720 -0
  56. package/extensions/lib/operator-fallback.ts +205 -0
  57. package/extensions/lib/operator-profile.ts +360 -0
  58. package/extensions/lib/slash-command-bridge.ts +253 -0
  59. package/extensions/lib/typebox-helpers.ts +16 -0
  60. package/extensions/local-inference/index.ts +727 -0
  61. package/extensions/mcp-bridge/README.md +220 -0
  62. package/extensions/mcp-bridge/index.ts +951 -0
  63. package/extensions/mcp-bridge/lib.ts +365 -0
  64. package/extensions/mcp-bridge/mcp.json +3 -0
  65. package/extensions/mcp-bridge/package.json +11 -0
  66. package/extensions/model-budget.ts +752 -0
  67. package/extensions/offline-driver.ts +403 -0
  68. package/extensions/openspec/archive-gate.ts +164 -0
  69. package/extensions/openspec/branch-cleanup.ts +64 -0
  70. package/extensions/openspec/dashboard-state.ts +50 -0
  71. package/extensions/openspec/index.ts +1917 -0
  72. package/extensions/openspec/lifecycle-emitter.ts +65 -0
  73. package/extensions/openspec/lifecycle-files.ts +70 -0
  74. package/extensions/openspec/lifecycle.ts +50 -0
  75. package/extensions/openspec/reconcile.ts +187 -0
  76. package/extensions/openspec/spec.ts +1385 -0
  77. package/extensions/openspec/types.ts +98 -0
  78. package/extensions/project-memory/DESIGN-global-mind.md +198 -0
  79. package/extensions/project-memory/README.md +202 -0
  80. package/extensions/project-memory/api-types.ts +382 -0
  81. package/extensions/project-memory/compaction-policy.ts +29 -0
  82. package/extensions/project-memory/core.ts +164 -0
  83. package/extensions/project-memory/embeddings.ts +230 -0
  84. package/extensions/project-memory/extraction-v2.ts +861 -0
  85. package/extensions/project-memory/factstore.ts +2177 -0
  86. package/extensions/project-memory/index.ts +3459 -0
  87. package/extensions/project-memory/injection-metrics.ts +91 -0
  88. package/extensions/project-memory/jsonl-io.ts +12 -0
  89. package/extensions/project-memory/lifecycle.ts +331 -0
  90. package/extensions/project-memory/migration.ts +293 -0
  91. package/extensions/project-memory/package.json +9 -0
  92. package/extensions/project-memory/sci-renderers.ts +7 -0
  93. package/extensions/project-memory/template.ts +103 -0
  94. package/extensions/project-memory/triggers.ts +52 -0
  95. package/extensions/project-memory/types.ts +102 -0
  96. package/extensions/render/composition/fonts/Inter-Bold.ttf +0 -0
  97. package/extensions/render/composition/fonts/Inter-Regular.ttf +0 -0
  98. package/extensions/render/composition/fonts/Tomorrow-Bold.ttf +0 -0
  99. package/extensions/render/composition/fonts/Tomorrow-Regular.ttf +0 -0
  100. package/extensions/render/composition/package-lock.json +534 -0
  101. package/extensions/render/composition/package.json +22 -0
  102. package/extensions/render/composition/render.mjs +246 -0
  103. package/extensions/render/composition/test-comp.tsx +87 -0
  104. package/extensions/render/composition/types.ts +24 -0
  105. package/extensions/render/excalidraw/UPSTREAM.md +81 -0
  106. package/extensions/render/excalidraw/elements.ts +764 -0
  107. package/extensions/render/excalidraw/index.ts +66 -0
  108. package/extensions/render/excalidraw/types.ts +223 -0
  109. package/extensions/render/excalidraw-renderer/pyproject.toml +8 -0
  110. package/extensions/render/excalidraw-renderer/render_excalidraw.py +182 -0
  111. package/extensions/render/excalidraw-renderer/render_template.html +59 -0
  112. package/extensions/render/index.ts +830 -0
  113. package/extensions/render/native-diagrams/index.ts +57 -0
  114. package/extensions/render/native-diagrams/motifs.ts +542 -0
  115. package/extensions/render/native-diagrams/raster.ts +8 -0
  116. package/extensions/render/native-diagrams/scene.ts +75 -0
  117. package/extensions/render/native-diagrams/spec.ts +204 -0
  118. package/extensions/render/native-diagrams/svg.ts +116 -0
  119. package/extensions/sci-ui.ts +304 -0
  120. package/extensions/session-log.ts +174 -0
  121. package/extensions/shared-state.ts +146 -0
  122. package/extensions/spinner-verbs.ts +91 -0
  123. package/extensions/style.ts +281 -0
  124. package/extensions/terminal-title.ts +191 -0
  125. package/extensions/tool-profile/index.ts +291 -0
  126. package/extensions/tool-profile/profiles.ts +290 -0
  127. package/extensions/types.d.ts +9 -0
  128. package/extensions/vault/index.ts +185 -0
  129. package/extensions/version-check.ts +90 -0
  130. package/extensions/view/index.ts +859 -0
  131. package/extensions/view/uri-resolver.ts +148 -0
  132. package/extensions/web-search/index.ts +182 -0
  133. package/extensions/web-search/providers.ts +121 -0
  134. package/extensions/web-ui/index.ts +110 -0
  135. package/extensions/web-ui/server.ts +265 -0
  136. package/extensions/web-ui/state.ts +462 -0
  137. package/extensions/web-ui/static/index.html +145 -0
  138. package/extensions/web-ui/types.ts +284 -0
  139. package/package.json +76 -0
  140. package/prompts/init.md +75 -0
  141. package/prompts/new-repo.md +54 -0
  142. package/prompts/oci-login.md +56 -0
  143. package/prompts/status.md +50 -0
  144. package/settings.json +4 -0
  145. package/skills/cleave/SKILL.md +218 -0
  146. package/skills/git/SKILL.md +209 -0
  147. package/skills/git/_reference/ci-validation.md +204 -0
  148. package/skills/oci/SKILL.md +338 -0
  149. package/skills/openspec/SKILL.md +346 -0
  150. package/skills/pi-extensions/SKILL.md +191 -0
  151. package/skills/pi-tui/SKILL.md +517 -0
  152. package/skills/python/SKILL.md +189 -0
  153. package/skills/rust/SKILL.md +268 -0
  154. package/skills/security/SKILL.md +206 -0
  155. package/skills/style/SKILL.md +264 -0
  156. package/skills/typescript/SKILL.md +225 -0
  157. package/skills/vault/SKILL.md +102 -0
  158. package/themes/alpharius-legacy.json +85 -0
  159. package/themes/alpharius.conf +59 -0
  160. package/themes/alpharius.json +88 -0
@@ -0,0 +1,205 @@
1
+ import type { Model } from "@cwilson613/pi-ai";
2
+ import {
3
+ classifyUpstreamFailure,
4
+ resolveCapabilityRole,
5
+ getTierDisplayLabel,
6
+ withCandidateCooldown,
7
+ withProviderCooldown,
8
+ type CapabilityCandidate,
9
+ type CapabilityProfile,
10
+ type CapabilityRole,
11
+ type CapabilityRuntimeState,
12
+ type ModelTier,
13
+ type ProviderRoutingPolicy,
14
+ type RegistryModel,
15
+ type UpstreamFailureClassification,
16
+ } from "./model-routing.ts";
17
+ import {
18
+ fromCapabilityRuntimeState,
19
+ loadOperatorRuntimeState,
20
+ saveOperatorRuntimeState,
21
+ toCapabilityRuntimeState,
22
+ type RuntimeFallbackGuidance,
23
+ } from "./operator-profile.ts";
24
+
25
+ const ROLE_ORDER: CapabilityRole[] = ["archmagos", "magos", "adept", "servitor", "servoskull"];
26
+ const TIER_ROLE_MAP: Record<Exclude<ModelTier, "local">, CapabilityRole> = {
27
+ gloriana: "archmagos",
28
+ victory: "magos",
29
+ retribution: "adept",
30
+ };
31
+
32
+ export interface RecoveryPlan {
33
+ classification: UpstreamFailureClassification;
34
+ role?: CapabilityRole;
35
+ action: "retry-same-model" | "switch-model" | "handoff-local" | "surface" | "handled-elsewhere";
36
+ sameModelRetry: boolean;
37
+ requiresConfirmation?: boolean;
38
+ reason: string;
39
+ alternateCandidate?: {
40
+ provider: CapabilityCandidate["provider"];
41
+ id: string;
42
+ };
43
+ }
44
+
45
+ function normalizeProvider(provider: string): "anthropic" | "openai" | "local" | undefined {
46
+ if (provider === "anthropic" || provider === "openai" || provider === "local") return provider;
47
+ if (provider === "ollama") return "local";
48
+ return undefined;
49
+ }
50
+
51
+ function currentModelKey(model: Pick<Model<any>, "provider" | "id">): string | undefined {
52
+ const provider = normalizeProvider(model.provider);
53
+ if (!provider) return undefined;
54
+ return `${provider}/${model.id}`;
55
+ }
56
+
57
+ export function inferRolesForModel(model: Pick<Model<any>, "provider" | "id">, profile: CapabilityProfile): CapabilityRole[] {
58
+ const key = currentModelKey(model);
59
+ if (!key) return [];
60
+ return ROLE_ORDER.filter((role) => profile.roles[role].candidates.some((candidate) => `${candidate.provider}/${candidate.id}` === key));
61
+ }
62
+
63
+ export function planRecoveryForModel(
64
+ model: Pick<Model<any>, "provider" | "id">,
65
+ failure: unknown,
66
+ models: RegistryModel[],
67
+ policy: ProviderRoutingPolicy,
68
+ profile: CapabilityProfile,
69
+ runtimeState: CapabilityRuntimeState,
70
+ now: number = Date.now(),
71
+ ): RecoveryPlan {
72
+ const classification = classifyUpstreamFailure(failure);
73
+ const [role] = inferRolesForModel(model, profile);
74
+
75
+ if (classification.recoveryAction === "retry-same-model") {
76
+ return {
77
+ classification,
78
+ role,
79
+ action: "retry-same-model",
80
+ sameModelRetry: true,
81
+ reason: classification.reason,
82
+ };
83
+ }
84
+
85
+ if (classification.recoveryAction === "handled-elsewhere") {
86
+ return {
87
+ classification,
88
+ role,
89
+ action: "handled-elsewhere",
90
+ sameModelRetry: false,
91
+ reason: classification.reason,
92
+ };
93
+ }
94
+
95
+ if (classification.recoveryAction === "failover" && role) {
96
+ const resolution = resolveCapabilityRole(role, models, policy, profile, runtimeState, now);
97
+ if (resolution.ok && resolution.selected) {
98
+ const selected = resolution.selected.candidate;
99
+ if (selected.id !== model.id || selected.provider !== normalizeProvider(model.provider)) {
100
+ return {
101
+ classification,
102
+ role,
103
+ action: selected.provider === "local" ? "handoff-local" : "switch-model",
104
+ sameModelRetry: false,
105
+ reason: classification.reason,
106
+ alternateCandidate: {
107
+ provider: selected.provider,
108
+ id: selected.id,
109
+ },
110
+ };
111
+ }
112
+ }
113
+
114
+ return {
115
+ classification,
116
+ role,
117
+ action: "surface",
118
+ sameModelRetry: false,
119
+ requiresConfirmation: resolution.requiresConfirmation,
120
+ reason: resolution.reason ?? classification.reason,
121
+ };
122
+ }
123
+
124
+ return {
125
+ classification,
126
+ role,
127
+ action: "surface",
128
+ sameModelRetry: false,
129
+ reason: classification.reason,
130
+ };
131
+ }
132
+
133
+ export function buildFallbackGuidance(
134
+ model: Pick<Model<any>, "provider" | "id">,
135
+ models: RegistryModel[],
136
+ policy: ProviderRoutingPolicy,
137
+ profile: CapabilityProfile,
138
+ runtimeState: CapabilityRuntimeState,
139
+ now: number = Date.now(),
140
+ ): RuntimeFallbackGuidance | undefined {
141
+ const [role] = inferRolesForModel(model, profile);
142
+ if (!role) return undefined;
143
+ const resolution = resolveCapabilityRole(role, models, policy, profile, runtimeState, now);
144
+ if (resolution.ok && resolution.selected) {
145
+ const selected = resolution.selected.candidate;
146
+ if (selected.id === model.id && selected.provider === normalizeProvider(model.provider)) return undefined;
147
+ return {
148
+ role,
149
+ ok: true,
150
+ alternateCandidate: {
151
+ provider: selected.provider,
152
+ id: selected.id,
153
+ },
154
+ };
155
+ }
156
+ return {
157
+ role,
158
+ ok: false,
159
+ requiresConfirmation: resolution.requiresConfirmation,
160
+ reason: resolution.reason,
161
+ };
162
+ }
163
+
164
+ export function explainTierResolutionFailure(
165
+ tier: ModelTier,
166
+ models: RegistryModel[],
167
+ policy: ProviderRoutingPolicy,
168
+ profile: CapabilityProfile,
169
+ runtimeState: CapabilityRuntimeState,
170
+ now: number = Date.now(),
171
+ ): string | undefined {
172
+ if (tier === "local") return undefined;
173
+ const resolution = resolveCapabilityRole(TIER_ROLE_MAP[tier], models, policy, profile, runtimeState, now);
174
+ if (resolution.ok || !resolution.reason) return undefined;
175
+ return `Unable to switch to ${getTierDisplayLabel(tier)}: ${resolution.reason}`;
176
+ }
177
+
178
+ export function recordTransientFailureForModel(
179
+ root: string,
180
+ model: Pick<Model<any>, "provider" | "id">,
181
+ reason: string,
182
+ now: number = Date.now(),
183
+ ): CapabilityRuntimeState | undefined {
184
+ const classification = classifyUpstreamFailure(reason);
185
+ if (!classification.cooldownProvider && !classification.cooldownCandidate) return undefined;
186
+
187
+ const provider = normalizeProvider(model.provider);
188
+ if (!provider || provider === "local") return undefined;
189
+
190
+ let state = toCapabilityRuntimeState(loadOperatorRuntimeState(root));
191
+ if (classification.cooldownProvider) {
192
+ state = withProviderCooldown(state, provider, reason, now);
193
+ }
194
+ if (classification.cooldownCandidate) {
195
+ state = withCandidateCooldown(state, {
196
+ id: model.id,
197
+ provider,
198
+ source: "upstream",
199
+ weight: "normal",
200
+ maxThinking: "high",
201
+ }, reason, now);
202
+ }
203
+ saveOperatorRuntimeState(root, fromCapabilityRuntimeState(state));
204
+ return state;
205
+ }
@@ -0,0 +1,360 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ import type {
5
+ CapabilityProfile,
6
+ CapabilityRuntimeState,
7
+ CapabilityCandidate,
8
+ ProviderName,
9
+ CooldownEntry,
10
+ } from "./model-routing.ts";
11
+ import { loadPiConfig, savePiConfig, type PiConfig } from "./model-preferences.ts";
12
+
13
+ export const CAPABILITY_ROLES = ["archmagos", "magos", "adept", "servitor", "servoskull"] as const;
14
+
15
+ export type CapabilityRole = typeof CAPABILITY_ROLES[number];
16
+ export type CapabilityRoleAlias = "gloriana" | "victory" | "retribution" | "local" | "servo-skull";
17
+ export type CandidateSource = "upstream" | "local";
18
+ export type CandidateWeight = "light" | "normal" | "heavy";
19
+ export type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high";
20
+ export type FallbackPolicyValue = "allow" | "ask" | "deny";
21
+
22
+ export interface OperatorProfileCandidate {
23
+ id?: string;
24
+ provider?: string;
25
+ source?: CandidateSource;
26
+ weight?: CandidateWeight;
27
+ maxThinking?: ThinkingLevel;
28
+ }
29
+
30
+ export interface OperatorFallbackPolicy {
31
+ sameRoleCrossProvider: FallbackPolicyValue;
32
+ // Reserved for future values like "allow_once" and "background_only".
33
+ crossSource: FallbackPolicyValue;
34
+ // Reserved for future values like "allow_once" and "background_only".
35
+ heavyLocal: FallbackPolicyValue;
36
+ // Reserved for future values like "allow_once" and "background_only".
37
+ unknownLocalPerformance: FallbackPolicyValue;
38
+ }
39
+
40
+ export type OperatorRoleMap = Record<CapabilityRole, OperatorProfileCandidate[]>;
41
+
42
+ export interface OperatorCapabilityProfile {
43
+ roles: OperatorRoleMap;
44
+ fallback: OperatorFallbackPolicy;
45
+ setupComplete?: boolean;
46
+ }
47
+
48
+ export interface CandidateCooldownState {
49
+ until: string;
50
+ reason?: string;
51
+ }
52
+
53
+ export interface OperatorRuntimeState {
54
+ providers?: Record<string, CandidateCooldownState>;
55
+ candidates?: Record<string, CandidateCooldownState>;
56
+ }
57
+
58
+ export interface RuntimeFallbackGuidance {
59
+ role: CapabilityRole;
60
+ ok: boolean;
61
+ requiresConfirmation?: boolean;
62
+ reason?: string;
63
+ alternateCandidate?: {
64
+ provider: ProviderName;
65
+ id: string;
66
+ };
67
+ }
68
+
69
+ const DEFAULT_FALLBACK_POLICY: OperatorFallbackPolicy = {
70
+ sameRoleCrossProvider: "allow",
71
+ crossSource: "ask",
72
+ heavyLocal: "deny",
73
+ unknownLocalPerformance: "ask",
74
+ };
75
+
76
+ const DEFAULT_PROFILE: OperatorCapabilityProfile = {
77
+ roles: {
78
+ archmagos: [
79
+ { id: "claude-opus-4-6", provider: "anthropic", source: "upstream", weight: "heavy", maxThinking: "high" },
80
+ { id: "gpt-5.4", provider: "openai", source: "upstream", weight: "heavy", maxThinking: "high" },
81
+ ],
82
+ magos: [
83
+ { id: "claude-sonnet-4-6", provider: "anthropic", source: "upstream", weight: "normal", maxThinking: "medium" },
84
+ { id: "gpt-5.3-codex-spark", provider: "openai", source: "upstream", weight: "normal", maxThinking: "medium" },
85
+ ],
86
+ adept: [
87
+ { id: "claude-haiku-3-5", provider: "anthropic", source: "upstream", weight: "light", maxThinking: "low" },
88
+ { id: "gpt-5.1-codex", provider: "openai", source: "upstream", weight: "light", maxThinking: "low" },
89
+ ],
90
+ servitor: [
91
+ { id: "gpt-4o-mini", provider: "openai", source: "upstream", weight: "light", maxThinking: "minimal" },
92
+ { id: "claude-haiku-3-5", provider: "anthropic", source: "upstream", weight: "light", maxThinking: "minimal" },
93
+ ],
94
+ servoskull: [
95
+ { id: "qwen3:8b", provider: "local", source: "local", weight: "light", maxThinking: "off" },
96
+ ],
97
+ },
98
+ fallback: DEFAULT_FALLBACK_POLICY,
99
+ setupComplete: false,
100
+ };
101
+
102
+ function deepCloneDefaultProfile(): OperatorCapabilityProfile {
103
+ return JSON.parse(JSON.stringify(DEFAULT_PROFILE)) as OperatorCapabilityProfile;
104
+ }
105
+
106
+ function isRecord(value: unknown): value is Record<string, unknown> {
107
+ return !!value && typeof value === "object" && !Array.isArray(value);
108
+ }
109
+
110
+ function isCapabilityRole(value: string): value is CapabilityRole {
111
+ return (CAPABILITY_ROLES as readonly string[]).includes(value);
112
+ }
113
+
114
+ function parseCandidate(value: unknown): OperatorProfileCandidate | undefined {
115
+ if (!isRecord(value)) return undefined;
116
+ const candidate: OperatorProfileCandidate = {};
117
+ if (typeof value.id === "string") candidate.id = value.id;
118
+ if (typeof value.provider === "string") candidate.provider = value.provider;
119
+ if (value.source === "upstream" || value.source === "local") {
120
+ candidate.source = value.source;
121
+ } else if (value.source === "frontier") {
122
+ candidate.source = "upstream";
123
+ }
124
+ if (value.weight === "light" || value.weight === "normal" || value.weight === "heavy") {
125
+ candidate.weight = value.weight;
126
+ } else if (typeof value.weight === "number" && Number.isFinite(value.weight)) {
127
+ candidate.weight = value.weight >= 90 ? "heavy" : value.weight >= 50 ? "normal" : "light";
128
+ }
129
+ if (["off", "minimal", "low", "medium", "high"].includes(String(value.maxThinking))) {
130
+ candidate.maxThinking = value.maxThinking as ThinkingLevel;
131
+ }
132
+ return Object.keys(candidate).length > 0 ? candidate : undefined;
133
+ }
134
+
135
+ function parseFallbackValue(value: unknown, fallback: FallbackPolicyValue): FallbackPolicyValue {
136
+ return value === "allow" || value === "ask" || value === "deny" ? value : fallback;
137
+ }
138
+
139
+ function normalizeProviderName(provider: string | undefined): ProviderName | undefined {
140
+ if (provider === "anthropic" || provider === "openai") return provider;
141
+ if (provider === "local" || provider === "ollama") return "local";
142
+ return undefined;
143
+ }
144
+
145
+ function normalizeCandidate(candidate: OperatorProfileCandidate): CapabilityCandidate | undefined {
146
+ if (!candidate.id) return undefined;
147
+ const provider = normalizeProviderName(candidate.provider);
148
+ if (!provider) return undefined;
149
+ return {
150
+ id: candidate.id,
151
+ provider,
152
+ source: candidate.source ?? (provider === "local" ? "local" : "upstream"),
153
+ weight: candidate.weight ?? "normal",
154
+ maxThinking: candidate.maxThinking ?? "medium",
155
+ };
156
+ }
157
+
158
+ function normalizeCooldownEntry(value: CandidateCooldownState): CooldownEntry | undefined {
159
+ const until = Date.parse(value.until);
160
+ if (!Number.isFinite(until)) return undefined;
161
+ return {
162
+ until,
163
+ reason: value.reason,
164
+ };
165
+ }
166
+
167
+ export function getDefaultOperatorProfile(): OperatorCapabilityProfile {
168
+ return deepCloneDefaultProfile();
169
+ }
170
+
171
+ export function parseOperatorProfile(raw: unknown): OperatorCapabilityProfile {
172
+ const profile = deepCloneDefaultProfile();
173
+ if (!isRecord(raw)) return profile;
174
+
175
+ if (isRecord(raw.roles)) {
176
+ for (const [key, value] of Object.entries(raw.roles)) {
177
+ if (!isCapabilityRole(key) || !Array.isArray(value)) continue;
178
+ const parsed = value
179
+ .map((candidate) => parseCandidate(candidate))
180
+ .filter((candidate): candidate is OperatorProfileCandidate => !!candidate);
181
+ if (parsed.length > 0) profile.roles[key] = parsed;
182
+ }
183
+ }
184
+
185
+ if (isRecord(raw.fallback)) {
186
+ profile.fallback = {
187
+ sameRoleCrossProvider: parseFallbackValue(raw.fallback.sameRoleCrossProvider, profile.fallback.sameRoleCrossProvider),
188
+ crossSource: parseFallbackValue(raw.fallback.crossSource, profile.fallback.crossSource),
189
+ heavyLocal: parseFallbackValue(raw.fallback.heavyLocal, profile.fallback.heavyLocal),
190
+ unknownLocalPerformance: parseFallbackValue(raw.fallback.unknownLocalPerformance, profile.fallback.unknownLocalPerformance),
191
+ };
192
+ }
193
+
194
+ if (typeof raw.setupComplete === "boolean") profile.setupComplete = raw.setupComplete;
195
+ return profile;
196
+ }
197
+
198
+ export function readOperatorProfile(root: string): OperatorCapabilityProfile {
199
+ return parseOperatorProfile(loadPiConfig(root).operatorProfile);
200
+ }
201
+
202
+ export function writeOperatorProfile(root: string, profile: OperatorCapabilityProfile): void {
203
+ const config: PiConfig = loadPiConfig(root);
204
+ config.operatorProfile = parseOperatorProfile(profile);
205
+ savePiConfig(root, config);
206
+ }
207
+
208
+ function runtimeStatePath(root: string): string {
209
+ return join(root, ".pi", "runtime", "operator-profile.json");
210
+ }
211
+
212
+ export function loadOperatorRuntimeState(root: string): OperatorRuntimeState {
213
+ try {
214
+ const path = runtimeStatePath(root);
215
+ if (!existsSync(path)) return {};
216
+ const raw = JSON.parse(readFileSync(path, "utf-8"));
217
+ return parseOperatorRuntimeState(raw);
218
+ } catch {
219
+ return {};
220
+ }
221
+ }
222
+
223
+ export function saveOperatorRuntimeState(root: string, state: OperatorRuntimeState): void {
224
+ const dir = join(root, ".pi", "runtime");
225
+ mkdirSync(dir, { recursive: true });
226
+ writeFileSync(runtimeStatePath(root), JSON.stringify(parseOperatorRuntimeState(state), null, 2) + "\n", "utf-8");
227
+ }
228
+
229
+ export function parseOperatorRuntimeState(raw: unknown): OperatorRuntimeState {
230
+ if (!isRecord(raw)) return {};
231
+ const normalize = (value: unknown): Record<string, CandidateCooldownState> | undefined => {
232
+ if (!isRecord(value)) return undefined;
233
+ const entries: [string, CandidateCooldownState][] = [];
234
+ for (const [key, candidate] of Object.entries(value)) {
235
+ if (!isRecord(candidate) || typeof candidate.until !== "string") continue;
236
+ entries.push([
237
+ key,
238
+ {
239
+ until: candidate.until,
240
+ reason: typeof candidate.reason === "string" ? candidate.reason : undefined,
241
+ },
242
+ ]);
243
+ }
244
+ return entries.length > 0 ? Object.fromEntries(entries) : undefined;
245
+ };
246
+
247
+ return {
248
+ providers: normalize(raw.providers),
249
+ candidates: normalize(raw.candidates),
250
+ };
251
+ }
252
+
253
+ export function resolveRoleAlias(role: CapabilityRole | CapabilityRoleAlias): CapabilityRole {
254
+ switch (role) {
255
+ case "gloriana":
256
+ return "archmagos";
257
+ case "victory":
258
+ return "magos";
259
+ case "retribution":
260
+ return "adept";
261
+ case "local":
262
+ case "servo-skull":
263
+ return "servoskull";
264
+ default:
265
+ return role;
266
+ }
267
+ }
268
+
269
+ export function toCapabilityProfile(profile: OperatorCapabilityProfile): CapabilityProfile {
270
+ return {
271
+ roles: {
272
+ archmagos: { candidates: profile.roles.archmagos.map(normalizeCandidate).filter((c): c is CapabilityCandidate => !!c) },
273
+ magos: { candidates: profile.roles.magos.map(normalizeCandidate).filter((c): c is CapabilityCandidate => !!c) },
274
+ adept: { candidates: profile.roles.adept.map(normalizeCandidate).filter((c): c is CapabilityCandidate => !!c) },
275
+ servitor: { candidates: profile.roles.servitor.map(normalizeCandidate).filter((c): c is CapabilityCandidate => !!c) },
276
+ servoskull: { candidates: profile.roles.servoskull.map(normalizeCandidate).filter((c): c is CapabilityCandidate => !!c) },
277
+ },
278
+ internalAliases: {
279
+ gloriana: "archmagos",
280
+ victory: "magos",
281
+ retribution: "adept",
282
+ local: "servitor",
283
+ review: "archmagos",
284
+ planning: "archmagos",
285
+ compaction: "servitor",
286
+ extraction: "servitor",
287
+ "cleave.leaf": "adept",
288
+ summary: "servoskull",
289
+ background: "servoskull",
290
+ },
291
+ policy: {
292
+ sameRoleCrossProvider: profile.fallback.sameRoleCrossProvider,
293
+ crossSource: profile.fallback.crossSource,
294
+ heavyLocal: profile.fallback.heavyLocal,
295
+ unknownLocalPerformance: profile.fallback.unknownLocalPerformance,
296
+ },
297
+ };
298
+ }
299
+
300
+ export function toCapabilityRuntimeState(state: OperatorRuntimeState): CapabilityRuntimeState {
301
+ const providerCooldowns = state.providers
302
+ ? Object.fromEntries(
303
+ Object.entries(state.providers)
304
+ .map(([provider, value]) => {
305
+ const normalizedProvider = normalizeProviderName(provider);
306
+ const entry = normalizeCooldownEntry(value);
307
+ return normalizedProvider && entry ? [normalizedProvider, entry] : null;
308
+ })
309
+ .filter((entry): entry is [ProviderName, CooldownEntry] => !!entry),
310
+ )
311
+ : undefined;
312
+
313
+ const candidateCooldowns = state.candidates
314
+ ? Object.fromEntries(
315
+ Object.entries(state.candidates)
316
+ .map(([key, value]) => {
317
+ const entry = normalizeCooldownEntry(value);
318
+ if (!entry) return null;
319
+ const normalizedKey = key.replace(/^([^:/]+):/, "$1/");
320
+ return [normalizedKey, entry] as const;
321
+ })
322
+ .filter((entry): entry is readonly [string, CooldownEntry] => !!entry),
323
+ )
324
+ : undefined;
325
+
326
+ return {
327
+ providerCooldowns: providerCooldowns && Object.keys(providerCooldowns).length > 0 ? providerCooldowns : undefined,
328
+ candidateCooldowns: candidateCooldowns && Object.keys(candidateCooldowns).length > 0 ? candidateCooldowns : undefined,
329
+ };
330
+ }
331
+
332
+ export function fromCapabilityRuntimeState(state: CapabilityRuntimeState): OperatorRuntimeState {
333
+ const providerEntries: Array<[string, CandidateCooldownState]> = [];
334
+ for (const [provider, entry] of Object.entries(state.providerCooldowns ?? {})) {
335
+ if (!entry) continue;
336
+ providerEntries.push([
337
+ provider,
338
+ {
339
+ until: new Date(entry.until).toISOString(),
340
+ reason: entry.reason,
341
+ },
342
+ ]);
343
+ }
344
+
345
+ const candidateEntries: Array<[string, CandidateCooldownState]> = [];
346
+ for (const [key, entry] of Object.entries(state.candidateCooldowns ?? {})) {
347
+ candidateEntries.push([
348
+ key,
349
+ {
350
+ until: new Date(entry.until).toISOString(),
351
+ reason: entry.reason,
352
+ },
353
+ ]);
354
+ }
355
+
356
+ return {
357
+ providers: providerEntries.length > 0 ? Object.fromEntries(providerEntries) : undefined,
358
+ candidates: candidateEntries.length > 0 ? Object.fromEntries(candidateEntries) : undefined,
359
+ };
360
+ }