omegon 0.6.3 → 0.6.4
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/README.md +12 -10
- package/bin/omegon.mjs +40 -0
- package/bin/pi.mjs +5 -26
- package/extensions/00-secrets/index.ts +146 -39
- package/extensions/01-auth/auth.ts +1 -1
- package/extensions/01-auth/index.ts +3 -3
- package/extensions/auto-compact.ts +1 -1
- package/extensions/bootstrap/deps.ts +42 -0
- package/extensions/bootstrap/index.ts +326 -110
- package/extensions/chronos/index.ts +1 -1
- package/extensions/cleave/dispatcher.ts +6 -6
- package/extensions/cleave/index.ts +6 -6
- package/extensions/cleave/planner.ts +1 -1
- package/extensions/cleave/worktree.ts +1 -1
- package/extensions/core-renderers.ts +24 -84
- package/extensions/dashboard/footer.ts +184 -40
- package/extensions/dashboard/git.ts +2 -2
- package/extensions/dashboard/index.ts +4 -4
- package/extensions/dashboard/overlay-data.ts +5 -5
- package/extensions/dashboard/overlay.ts +5 -5
- package/extensions/dashboard/render-utils.ts +1 -1
- package/extensions/dashboard/types.ts +15 -0
- package/extensions/defaults.ts +4 -12
- package/extensions/design-tree/dashboard-state.ts +6 -6
- package/extensions/design-tree/design-card.ts +3 -3
- package/extensions/design-tree/index.ts +64 -44
- package/extensions/design-tree/types.ts +4 -2
- package/extensions/distill.ts +1 -1
- package/extensions/effort/index.ts +137 -10
- package/extensions/lib/model-routing.ts +304 -32
- package/extensions/lib/operator-fallback.ts +1 -1
- package/extensions/lib/operator-profile.ts +1 -1
- package/extensions/lib/provider-env.ts +163 -0
- package/extensions/{sci-ui.ts → lib/sci-ui.ts} +119 -2
- package/extensions/{shared-state.ts → lib/shared-state.ts} +13 -9
- package/extensions/lib/slash-command-bridge.ts +1 -1
- package/extensions/{types.d.ts → lib/types.d.ts} +3 -3
- package/extensions/local-inference/index.ts +1 -1
- package/extensions/mcp-bridge/index.ts +1 -1
- package/extensions/model-budget.ts +10 -10
- package/extensions/offline-driver.ts +11 -4
- package/extensions/openspec/archive-gate.ts +1 -1
- package/extensions/openspec/branch-cleanup.ts +1 -1
- package/extensions/openspec/dashboard-state.ts +3 -3
- package/extensions/openspec/index.ts +5 -5
- package/extensions/project-memory/factstore.ts +5 -11
- package/extensions/project-memory/index.ts +48 -34
- package/extensions/project-memory/package.json +1 -1
- package/extensions/project-memory/sci-renderers.ts +1 -1
- package/extensions/render/index.ts +1 -1
- package/extensions/session-log.ts +1 -1
- package/extensions/spinner-verbs.ts +1 -1
- package/extensions/style.ts +1 -1
- package/extensions/terminal-title.ts +3 -3
- package/extensions/tool-profile/index.ts +1 -1
- package/extensions/vault/index.ts +1 -1
- package/extensions/version-check.ts +13 -9
- package/extensions/view/index.ts +4 -4
- package/extensions/web-search/index.ts +5 -2
- package/extensions/web-ui/index.ts +1 -1
- package/extensions/web-ui/state.ts +1 -1
- package/package.json +8 -7
- package/scripts/preinstall.sh +19 -3
- package/scripts/publish-pi-mono.sh +92 -0
- package/skills/pi-extensions/SKILL.md +2 -2
- package/skills/pi-tui/SKILL.md +17 -17
- package/skills/typescript/SKILL.md +1 -1
- package/themes/alpharius.json +7 -6
- /package/extensions/{debug.ts → lib/debug.ts} +0 -0
|
@@ -17,18 +17,18 @@
|
|
|
17
17
|
* ## Overview | ## Research | ## Decisions | ## Open Questions | ## Implementation Notes
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import type { ExtensionAPI, ExtensionContext } from "@
|
|
20
|
+
import type { ExtensionAPI, ExtensionContext } from "@styrene-lab/pi-coding-agent";
|
|
21
21
|
import { Type } from "@sinclair/typebox";
|
|
22
22
|
import { StringEnum } from "../lib/typebox-helpers.ts";
|
|
23
|
-
import { Text } from "@
|
|
23
|
+
import { Text } from "@styrene-lab/pi-tui";
|
|
24
24
|
import * as fs from "node:fs";
|
|
25
25
|
import * as path from "node:path";
|
|
26
26
|
import { execFileSync } from "node:child_process";
|
|
27
27
|
import { shouldRefreshDesignTreeForPath } from "../dashboard/file-watch.ts";
|
|
28
|
-
import { sharedState } from "../shared-state.ts";
|
|
28
|
+
import { sharedState } from "../lib/shared-state.ts";
|
|
29
29
|
|
|
30
30
|
import { emitDesignTreeState } from "./dashboard-state.ts";
|
|
31
|
-
import { sciCall, sciLoading, sciOk, sciErr, sciExpanded, sciBanner } from "../sci-ui.ts";
|
|
31
|
+
import { sciCall, sciLoading, sciOk, sciErr, sciExpanded, sciBanner } from "../lib/sci-ui.ts";
|
|
32
32
|
import { SciDesignCard, buildCardDetails } from "./design-card.ts";
|
|
33
33
|
import type { DesignCardDetails } from "./design-card.ts";
|
|
34
34
|
import { emitConstraintCandidates, emitDecisionCandidates } from "./lifecycle-emitter.ts";
|
|
@@ -338,7 +338,7 @@ export default function designTreeExtension(pi: ExtensionAPI): void {
|
|
|
338
338
|
boundToOpenSpec: binding.bound,
|
|
339
339
|
// Normalized binding status from canonical resolver
|
|
340
340
|
bindingStatus: lifecycleSummary?.bindingStatus ?? (binding.bound ? "bound" : "unbound"),
|
|
341
|
-
canImplement: node.status === "decided",
|
|
341
|
+
canImplement: node.status === "decided" || node.status === "resolved",
|
|
342
342
|
isImplementationPhase: node.status === "implementing" || node.status === "implemented",
|
|
343
343
|
reopenSignalTarget: binding.changeName ?? node.openspec_change ?? node.id,
|
|
344
344
|
// Canonical lifecycle fields from resolveLifecycleSummary when available
|
|
@@ -435,7 +435,7 @@ export default function designTreeExtension(pi: ExtensionAPI): void {
|
|
|
435
435
|
// AND design-phase OpenSpec change is archived.
|
|
436
436
|
const readyNodes = Array.from(tree.nodes.values())
|
|
437
437
|
.filter((n) => {
|
|
438
|
-
if (n.status !== "decided") return false;
|
|
438
|
+
if (n.status !== "decided" && n.status !== "resolved") return false;
|
|
439
439
|
// Hard gate: design spec must be archived
|
|
440
440
|
const specBinding = resolveDesignSpecBinding(ctx.cwd, n.id);
|
|
441
441
|
if (!specBinding.archived) return false;
|
|
@@ -668,7 +668,7 @@ export default function designTreeExtension(pi: ExtensionAPI): void {
|
|
|
668
668
|
"set focus, or bridge to OpenSpec for implementation.\n\n" +
|
|
669
669
|
"Actions:\n" +
|
|
670
670
|
"- create: Create a new design node (id, title required; parent, status, tags, overview optional)\n" +
|
|
671
|
-
"- set_status: Change node status (seed/exploring/decided/blocked/deferred)\n" +
|
|
671
|
+
"- set_status: Change node status (seed/exploring/resolved/decided/blocked/deferred)\n" +
|
|
672
672
|
"- add_question: Add an open question to a node\n" +
|
|
673
673
|
"- remove_question: Remove an open question by text\n" +
|
|
674
674
|
"- add_research: Add a research entry (heading + content)\n" +
|
|
@@ -686,7 +686,7 @@ export default function designTreeExtension(pi: ExtensionAPI): void {
|
|
|
686
686
|
"Mutate the design tree — create nodes, set status, add research/decisions/questions, branch, implement",
|
|
687
687
|
promptGuidelines: [
|
|
688
688
|
"Use 'create' to start a new design exploration. Status defaults to 'seed'.",
|
|
689
|
-
"Use 'set_status' to transition nodes: seed → exploring → decided. Use 'blocked' or 'deferred' as needed.",
|
|
689
|
+
"Use 'set_status' to transition nodes: seed → exploring → resolved → decided. Use 'resolved' when design questions are answered but the formal lifecycle gate hasn't cleared. Use 'blocked' or 'deferred' as needed.",
|
|
690
690
|
"Use 'add_question' when discussion reveals unknowns. Use 'remove_question' when questions are answered.",
|
|
691
691
|
"Use 'add_research' to record findings with a heading and content.",
|
|
692
692
|
"Use 'add_decision' to crystallize choices with title, status (exploring/decided/rejected), and rationale.",
|
|
@@ -799,21 +799,31 @@ export default function designTreeExtension(pi: ExtensionAPI): void {
|
|
|
799
799
|
const oldStatus = node.status;
|
|
800
800
|
|
|
801
801
|
// Hard gate: set_status(decided) requires archived design spec.
|
|
802
|
+
// Exception: bug/chore/task nodes with no open questions can skip
|
|
803
|
+
// the design-phase ceremony — the decision IS the diagnosis.
|
|
802
804
|
if (newStatus === "decided") {
|
|
803
|
-
const
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
805
|
+
const lightweightTypes = new Set(["bug", "chore", "task"]);
|
|
806
|
+
const isLightweight = node.issue_type && lightweightTypes.has(node.issue_type);
|
|
807
|
+
const hasOpenQuestions = (node.open_questions?.length ?? 0) > 0;
|
|
808
|
+
|
|
809
|
+
// Lightweight nodes: allow decided if no open questions remain,
|
|
810
|
+
// regardless of design-phase spec state.
|
|
811
|
+
if (!(isLightweight && !hasOpenQuestions)) {
|
|
812
|
+
const designSpec = resolveDesignSpecBinding(ctx.cwd, node.id);
|
|
813
|
+
if (designSpec.missing) {
|
|
814
|
+
return {
|
|
815
|
+
content: [{ type: "text", text: `Cannot mark '${node.title}' decided: scaffold design spec first via set_status(exploring).` }],
|
|
816
|
+
details: { id: node.id, blockedBy: "design-openspec-missing" },
|
|
817
|
+
isError: true,
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
if (designSpec.active && !designSpec.archived) {
|
|
821
|
+
return {
|
|
822
|
+
content: [{ type: "text", text: `Cannot mark '${node.title}' decided: run \`/assess design ${node.id}\` then archive the design change before marking decided.` }],
|
|
823
|
+
details: { id: node.id, blockedBy: "design-openspec-not-archived" },
|
|
824
|
+
isError: true,
|
|
825
|
+
};
|
|
826
|
+
}
|
|
817
827
|
}
|
|
818
828
|
}
|
|
819
829
|
|
|
@@ -1125,12 +1135,12 @@ export default function designTreeExtension(pi: ExtensionAPI): void {
|
|
|
1125
1135
|
if (!node) {
|
|
1126
1136
|
return { content: [{ type: "text", text: `Node '${params.node_id}' not found` }], details: {}, isError: true };
|
|
1127
1137
|
}
|
|
1128
|
-
if (node.status !== "decided") {
|
|
1138
|
+
if (node.status !== "decided" && node.status !== "resolved") {
|
|
1129
1139
|
return {
|
|
1130
1140
|
content: [{
|
|
1131
1141
|
type: "text",
|
|
1132
|
-
text: `Node '${node.title}' is '${node.status}', not 'decided'. ` +
|
|
1133
|
-
`Resolve open questions and set status to 'decided' before implementing.`,
|
|
1142
|
+
text: `Node '${node.title}' is '${node.status}', not 'decided' or 'resolved'. ` +
|
|
1143
|
+
`Resolve open questions and set status to 'decided' (or 'resolved') before implementing.`,
|
|
1134
1144
|
}],
|
|
1135
1145
|
details: {},
|
|
1136
1146
|
isError: true,
|
|
@@ -1138,21 +1148,31 @@ export default function designTreeExtension(pi: ExtensionAPI): void {
|
|
|
1138
1148
|
}
|
|
1139
1149
|
|
|
1140
1150
|
// Hard gate: design-phase spec must be archived before implementation.
|
|
1151
|
+
// Exception: bug/chore/task nodes that are already decided with no
|
|
1152
|
+
// open questions can proceed directly — they don't need a design-phase
|
|
1153
|
+
// spec because the decision is the bug diagnosis itself.
|
|
1141
1154
|
{
|
|
1142
|
-
const
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
}
|
|
1155
|
+
const lightweightTypes = new Set(["bug", "chore", "task"]);
|
|
1156
|
+
const isLightweight = node.issue_type && lightweightTypes.has(node.issue_type);
|
|
1157
|
+
const hasOpenQuestions = (node.open_questions?.length ?? 0) > 0;
|
|
1158
|
+
const skipDesignGate = isLightweight && !hasOpenQuestions && (node.status === "decided" || node.status === "resolved");
|
|
1159
|
+
|
|
1160
|
+
if (!skipDesignGate) {
|
|
1161
|
+
const designSpec = resolveDesignSpecBinding(ctx.cwd, node.id);
|
|
1162
|
+
if (designSpec.missing) {
|
|
1163
|
+
return {
|
|
1164
|
+
content: [{ type: "text", text: "Scaffold design spec first via set_status(exploring)" }],
|
|
1165
|
+
details: { id: node.id, blockedBy: "design-openspec-missing" },
|
|
1166
|
+
isError: true,
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
if (designSpec.active && !designSpec.archived) {
|
|
1170
|
+
return {
|
|
1171
|
+
content: [{ type: "text", text: `Cannot implement '${node.title}': archive the design change first (\`/opsx:archive\` on the design change).` }],
|
|
1172
|
+
details: { id: node.id, blockedBy: "design-openspec-not-archived" },
|
|
1173
|
+
isError: true,
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1156
1176
|
}
|
|
1157
1177
|
}
|
|
1158
1178
|
|
|
@@ -1367,7 +1387,7 @@ export default function designTreeExtension(pi: ExtensionAPI): void {
|
|
|
1367
1387
|
return;
|
|
1368
1388
|
}
|
|
1369
1389
|
const total = tree.nodes.size;
|
|
1370
|
-
const decided = Array.from(tree.nodes.values()).filter((n) => n.status === "decided").length;
|
|
1390
|
+
const decided = Array.from(tree.nodes.values()).filter((n) => n.status === "decided" || n.status === "resolved").length;
|
|
1371
1391
|
const exploring = Array.from(tree.nodes.values()).filter(
|
|
1372
1392
|
(n) => n.status === "exploring" || n.status === "seed",
|
|
1373
1393
|
).length;
|
|
@@ -1658,9 +1678,9 @@ export default function designTreeExtension(pi: ExtensionAPI): void {
|
|
|
1658
1678
|
ctx.ui.notify(`Node '${id}' not found`, "error");
|
|
1659
1679
|
return;
|
|
1660
1680
|
}
|
|
1661
|
-
if (node.status !== "decided") {
|
|
1681
|
+
if (node.status !== "decided" && node.status !== "resolved") {
|
|
1662
1682
|
ctx.ui.notify(
|
|
1663
|
-
`'${node.title}' is '${node.status}', not 'decided'. Resolve questions first.`,
|
|
1683
|
+
`'${node.title}' is '${node.status}', not 'decided'/'resolved'. Resolve questions first.`,
|
|
1664
1684
|
"warning",
|
|
1665
1685
|
);
|
|
1666
1686
|
return;
|
|
@@ -1765,7 +1785,7 @@ export default function designTreeExtension(pi: ExtensionAPI): void {
|
|
|
1765
1785
|
|
|
1766
1786
|
const implemented = Array.from(tree.nodes.values()).filter((n) => n.status === "implemented").length;
|
|
1767
1787
|
const implementing = Array.from(tree.nodes.values()).filter((n) => n.status === "implementing").length;
|
|
1768
|
-
const decided = Array.from(tree.nodes.values()).filter((n) => n.status === "decided").length;
|
|
1788
|
+
const decided = Array.from(tree.nodes.values()).filter((n) => n.status === "decided" || n.status === "resolved").length;
|
|
1769
1789
|
const exploring = Array.from(tree.nodes.values()).filter(
|
|
1770
1790
|
(n) => n.status === "exploring" || n.status === "seed",
|
|
1771
1791
|
).length;
|
|
@@ -1956,7 +1976,7 @@ export default function designTreeExtension(pi: ExtensionAPI): void {
|
|
|
1956
1976
|
if (status === "implemented" || status === "deferred") {
|
|
1957
1977
|
toMigrate.push(entry);
|
|
1958
1978
|
} else {
|
|
1959
|
-
// seed, exploring, decided, blocked — leave in docs/
|
|
1979
|
+
// seed, exploring, resolved, decided, blocked — leave in docs/
|
|
1960
1980
|
activeExplorations.push(entry);
|
|
1961
1981
|
}
|
|
1962
1982
|
}
|
|
@@ -4,13 +4,14 @@
|
|
|
4
4
|
|
|
5
5
|
// ─── Node Status ─────────────────────────────────────────────────────────────
|
|
6
6
|
|
|
7
|
-
export type NodeStatus = "seed" | "exploring" | "decided" | "implementing" | "implemented" | "blocked" | "deferred";
|
|
7
|
+
export type NodeStatus = "seed" | "exploring" | "resolved" | "decided" | "implementing" | "implemented" | "blocked" | "deferred";
|
|
8
8
|
|
|
9
|
-
export const VALID_STATUSES: NodeStatus[] = ["seed", "exploring", "decided", "implementing", "implemented", "blocked", "deferred"];
|
|
9
|
+
export const VALID_STATUSES: NodeStatus[] = ["seed", "exploring", "resolved", "decided", "implementing", "implemented", "blocked", "deferred"];
|
|
10
10
|
|
|
11
11
|
export const STATUS_ICONS: Record<NodeStatus, string> = {
|
|
12
12
|
seed: "◌",
|
|
13
13
|
exploring: "◐",
|
|
14
|
+
resolved: "◉",
|
|
14
15
|
decided: "●",
|
|
15
16
|
implementing: "⚙",
|
|
16
17
|
implemented: "✓",
|
|
@@ -21,6 +22,7 @@ export const STATUS_ICONS: Record<NodeStatus, string> = {
|
|
|
21
22
|
export const STATUS_COLORS: Record<NodeStatus, string> = {
|
|
22
23
|
seed: "muted",
|
|
23
24
|
exploring: "accent",
|
|
25
|
+
resolved: "success",
|
|
24
26
|
decided: "success",
|
|
25
27
|
implementing: "accent",
|
|
26
28
|
implemented: "success",
|
package/extensions/distill.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import { existsSync, mkdirSync } from "node:fs";
|
|
13
13
|
import { join, basename } from "node:path";
|
|
14
|
-
import type { ExtensionAPI } from "@
|
|
14
|
+
import type { ExtensionAPI } from "@styrene-lab/pi-coding-agent";
|
|
15
15
|
|
|
16
16
|
export default function distillExtension(pi: ExtensionAPI) {
|
|
17
17
|
|
|
@@ -16,24 +16,28 @@
|
|
|
16
16
|
* /effort uncap — Remove ceiling lock
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import type { ExtensionAPI, ExtensionContext } from "@
|
|
19
|
+
import type { ExtensionAPI, ExtensionContext } from "@styrene-lab/pi-coding-agent";
|
|
20
20
|
import { readFileSync, existsSync } from "node:fs";
|
|
21
21
|
import { join } from "node:path";
|
|
22
22
|
|
|
23
23
|
import type { EffortLevel, EffortState, EffortModelTier, ThinkingLevel } from "./types.ts";
|
|
24
24
|
import { EFFORT_NAMES } from "./types.ts";
|
|
25
25
|
import { tierConfig, parseTierName, DEFAULT_EFFORT_LEVEL, TIER_NAMES } from "./tiers.ts";
|
|
26
|
-
import { sharedState, DASHBOARD_UPDATE_EVENT } from "../shared-state.ts";
|
|
26
|
+
import { sharedState, DASHBOARD_UPDATE_EVENT } from "../lib/shared-state.ts";
|
|
27
27
|
import {
|
|
28
28
|
resolveTier,
|
|
29
29
|
getTierDisplayLabel,
|
|
30
30
|
getDefaultPolicy,
|
|
31
|
+
getViableModels,
|
|
32
|
+
buildProviderSummary,
|
|
33
|
+
matchTierUniversal as matchTierUniversalExport,
|
|
31
34
|
clampThinkingLevel,
|
|
32
35
|
type ModelTier,
|
|
33
36
|
type RegistryModel,
|
|
34
37
|
} from "../lib/model-routing.ts";
|
|
35
38
|
import { readLastUsedModel, writeLastUsedModel } from "../lib/model-preferences.ts";
|
|
36
39
|
import { readOperatorProfile, loadOperatorRuntimeState, toCapabilityProfile, toCapabilityRuntimeState } from "../lib/operator-profile.ts";
|
|
40
|
+
import { PROVIDER_ENV_VARS, getProviderRemediationHint } from "../lib/provider-env.ts";
|
|
37
41
|
|
|
38
42
|
// ─── Constants ───────────────────────────────────────────────
|
|
39
43
|
|
|
@@ -72,7 +76,7 @@ async function switchDriverModel(
|
|
|
72
76
|
driver: EffortModelTier,
|
|
73
77
|
): Promise<{ model: RegistryModel; maxThinking?: ThinkingLevel } | null> {
|
|
74
78
|
// Snapshot the registry once; both resolveTier and the model lookup use it
|
|
75
|
-
const all = ctx.modelRegistry
|
|
79
|
+
const all = getViableModels(ctx.modelRegistry);
|
|
76
80
|
// Build O(1) index over the same snapshot — no second linear scan (C3)
|
|
77
81
|
const byKey = new Map(all.map((m) => [`${m.provider}/${m.id}`, m]));
|
|
78
82
|
const { policy, profile, runtimeState } = getResolverInputs(ctx);
|
|
@@ -113,7 +117,7 @@ function resolveExtractionTier(
|
|
|
113
117
|
ctx: ExtensionContext,
|
|
114
118
|
): { displayTier: string; resolvedModelId?: string } {
|
|
115
119
|
const { policy, profile, runtimeState } = getResolverInputs(ctx);
|
|
116
|
-
const all = ctx.modelRegistry
|
|
120
|
+
const all = getViableModels(ctx.modelRegistry);
|
|
117
121
|
|
|
118
122
|
// Determine effective tier: upgrade local→retribution when policy prefers cheap cloud
|
|
119
123
|
const effectiveTier: ModelTier =
|
|
@@ -247,7 +251,25 @@ export default function (pi: ExtensionAPI) {
|
|
|
247
251
|
// current startup model rather than warning about an unusable session when
|
|
248
252
|
// a working driver is already present.
|
|
249
253
|
const restoredModel = await restoreLastUsedModel(pi, ctx);
|
|
250
|
-
|
|
254
|
+
let switchedDriver = restoredModel ? null : await switchDriverModel(pi, ctx, state.driver);
|
|
255
|
+
|
|
256
|
+
// Degradation cascade: if the preferred tier failed, try adjacent tiers
|
|
257
|
+
// before settling on whatever pi defaulted to (which may be deprecated).
|
|
258
|
+
// Order: victory→gloriana→retribution, gloriana→victory, retribution→victory.
|
|
259
|
+
if (!restoredModel && !switchedDriver) {
|
|
260
|
+
const DEGRADATION_CASCADE: Record<string, ModelTier[]> = {
|
|
261
|
+
victory: ["gloriana", "retribution"],
|
|
262
|
+
gloriana: ["victory"],
|
|
263
|
+
retribution: ["victory"],
|
|
264
|
+
local: [],
|
|
265
|
+
};
|
|
266
|
+
const fallbacks = DEGRADATION_CASCADE[state.driver] ?? [];
|
|
267
|
+
for (const fallbackTier of fallbacks) {
|
|
268
|
+
switchedDriver = await switchDriverModel(pi, ctx, fallbackTier);
|
|
269
|
+
if (switchedDriver) break;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
251
273
|
const retainedModel = !restoredModel && !switchedDriver && ctx.model ? ctx.model : null;
|
|
252
274
|
|
|
253
275
|
// Set thinking level, respecting candidate ceilings when the effort-driven
|
|
@@ -259,8 +281,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
259
281
|
: state.thinking;
|
|
260
282
|
pi.setThinkingLevel(effectiveThinking as any);
|
|
261
283
|
|
|
262
|
-
// Notify operator
|
|
284
|
+
// Notify operator — suppress the "no model" warning during first-run
|
|
285
|
+
// (bootstrap handles consolidated guidance), but always show when a model
|
|
286
|
+
// is resolved so the operator knows what's driving their session.
|
|
263
287
|
const icon = TIER_ICONS[state.level];
|
|
288
|
+
const hasModel = !!(restoredModel || switchedDriver || retainedModel);
|
|
264
289
|
const modelNote = restoredModel
|
|
265
290
|
? ` → restored ${restoredModel.provider}/${restoredModel.id}`
|
|
266
291
|
: switchedDriver
|
|
@@ -268,10 +293,112 @@ export default function (pi: ExtensionAPI) {
|
|
|
268
293
|
: retainedModel
|
|
269
294
|
? ` → kept ${retainedModel.provider}/${retainedModel.id} (preferred ${state.driver} unavailable)`
|
|
270
295
|
: " (driver model unavailable)";
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
296
|
+
if (hasModel || !sharedState.bootstrapPending) {
|
|
297
|
+
ctx.ui.notify(
|
|
298
|
+
`${icon} Effort: ${state.name} (${state.driver}/${effectiveThinking})${modelNote}`,
|
|
299
|
+
hasModel ? "info" : "warning",
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Provider summary — show what tiers are available
|
|
304
|
+
try {
|
|
305
|
+
const allModels = ctx.modelRegistry.getAll() as unknown as RegistryModel[];
|
|
306
|
+
const viable = getViableModels(ctx.modelRegistry);
|
|
307
|
+
const policy = sharedState.routingPolicy ?? getDefaultPolicy();
|
|
308
|
+
const summary = buildProviderSummary(allModels, viable, policy);
|
|
309
|
+
|
|
310
|
+
if (summary.level === 0 && !sharedState.bootstrapPending) {
|
|
311
|
+
ctx.ui.notify("⚠ No providers configured. Run /bootstrap or /providers for setup hints.", "warning");
|
|
312
|
+
} else if (summary.level < 3) {
|
|
313
|
+
const parts: string[] = [];
|
|
314
|
+
for (const t of summary.tiers) {
|
|
315
|
+
const icon = t.status === "operational" ? "●" : t.status === "degraded" ? "◐" : "○";
|
|
316
|
+
const detail = t.topCandidate ? ` ${t.topCandidate.provider}/${t.topCandidate.modelId}` : "";
|
|
317
|
+
parts.push(`${icon} ${getTierDisplayLabel(t.tier)}${detail}`);
|
|
318
|
+
}
|
|
319
|
+
ctx.ui.notify(`Routing: ${parts.join(" ")}`, "info");
|
|
320
|
+
}
|
|
321
|
+
// level 3 (all operational) = silent — no need to clutter startup
|
|
322
|
+
} catch {
|
|
323
|
+
// Non-critical — don't break startup
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// ── /providers command ──
|
|
328
|
+
|
|
329
|
+
pi.registerCommand("providers", {
|
|
330
|
+
description: "Show provider auth status and tier routing summary",
|
|
331
|
+
handler: async (_args, ctx) => {
|
|
332
|
+
const allModels = ctx.modelRegistry.getAll() as unknown as RegistryModel[];
|
|
333
|
+
const viable = getViableModels(ctx.modelRegistry);
|
|
334
|
+
const policy = sharedState.routingPolicy ?? getDefaultPolicy();
|
|
335
|
+
const summary = buildProviderSummary(allModels, viable, policy);
|
|
336
|
+
|
|
337
|
+
const lines: string[] = [];
|
|
338
|
+
|
|
339
|
+
// Auth status
|
|
340
|
+
if (summary.authProviders.length > 0) {
|
|
341
|
+
lines.push(`**Auth configured:** ${summary.authProviders.join(", ")}`);
|
|
342
|
+
}
|
|
343
|
+
if (summary.unauthProviders.length > 0) {
|
|
344
|
+
const top = summary.unauthProviders.slice(0, 8);
|
|
345
|
+
const more = summary.unauthProviders.length > 8 ? ` (+${summary.unauthProviders.length - 8} more)` : "";
|
|
346
|
+
lines.push(`**No auth:** ${top.join(", ")}${more}`);
|
|
347
|
+
}
|
|
348
|
+
lines.push("");
|
|
349
|
+
|
|
350
|
+
// Tier table
|
|
351
|
+
lines.push("| Tier | Status | Provider | Model |");
|
|
352
|
+
lines.push("|------|--------|----------|-------|");
|
|
353
|
+
for (const t of summary.tiers) {
|
|
354
|
+
const icon = t.status === "operational" ? "●" : t.status === "degraded" ? "◐" : "○";
|
|
355
|
+
const status = `${icon} ${t.status}`;
|
|
356
|
+
const provider = t.topCandidate?.provider ?? "—";
|
|
357
|
+
const model = t.topCandidate?.modelId ?? "—";
|
|
358
|
+
lines.push(`| ${getTierDisplayLabel(t.tier)} | ${status} | ${provider} | ${model} |`);
|
|
359
|
+
}
|
|
360
|
+
lines.push("");
|
|
361
|
+
|
|
362
|
+
// Candidate detail
|
|
363
|
+
for (const t of summary.tiers) {
|
|
364
|
+
if (t.candidateCount > 1) {
|
|
365
|
+
const matches = matchTierUniversalExport(viable, t.tier);
|
|
366
|
+
const candidateList = matches.slice(0, 5).map((m) => `${m.model.provider}/${m.model.id}`).join(", ");
|
|
367
|
+
const more = matches.length > 5 ? ` (+${matches.length - 5} more)` : "";
|
|
368
|
+
lines.push(`**${getTierDisplayLabel(t.tier)} candidates:** ${candidateList}${more}`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Remediation hints for unconfigured providers that could improve coverage
|
|
373
|
+
const impairedTiers = summary.tiers.filter(t => t.status === "unavailable" || t.status === "degraded");
|
|
374
|
+
if (impairedTiers.length > 0 && summary.unauthProviders.length > 0) {
|
|
375
|
+
const hintLines: string[] = [];
|
|
376
|
+
const shown = new Set<string>();
|
|
377
|
+
for (const provider of summary.unauthProviders) {
|
|
378
|
+
if (shown.size >= 5) break;
|
|
379
|
+
const hint = getProviderRemediationHint(provider);
|
|
380
|
+
if (hint && !shown.has(provider)) {
|
|
381
|
+
shown.add(provider);
|
|
382
|
+
const entry = PROVIDER_ENV_VARS[provider];
|
|
383
|
+
const desc = entry?.description ?? provider;
|
|
384
|
+
hintLines.push(` ${provider} (${desc}): ${hint}`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
if (hintLines.length > 0) {
|
|
388
|
+
lines.push("**To configure providers:**");
|
|
389
|
+
lines.push(...hintLines);
|
|
390
|
+
lines.push("");
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
lines.push(`**Headline:** ${summary.headline}`);
|
|
395
|
+
|
|
396
|
+
pi.sendMessage({
|
|
397
|
+
customType: "provider-summary",
|
|
398
|
+
content: lines.join("\n"),
|
|
399
|
+
display: true,
|
|
400
|
+
});
|
|
401
|
+
},
|
|
275
402
|
});
|
|
276
403
|
|
|
277
404
|
// ── /effort command ──
|