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.
- package/.gitattributes +3 -0
- package/AGENTS.md +16 -0
- package/LICENSE +15 -0
- package/README.md +289 -0
- package/bin/pi.mjs +30 -0
- package/extensions/00-secrets/index.ts +1126 -0
- package/extensions/01-auth/auth.ts +401 -0
- package/extensions/01-auth/index.ts +289 -0
- package/extensions/auto-compact.ts +42 -0
- package/extensions/bootstrap/deps.ts +291 -0
- package/extensions/bootstrap/index.ts +811 -0
- package/extensions/chronos/chronos.sh +487 -0
- package/extensions/chronos/index.ts +148 -0
- package/extensions/cleave/assessment.ts +754 -0
- package/extensions/cleave/bridge.ts +31 -0
- package/extensions/cleave/conflicts.ts +250 -0
- package/extensions/cleave/dispatcher.ts +808 -0
- package/extensions/cleave/guardrails.ts +426 -0
- package/extensions/cleave/index.ts +3121 -0
- package/extensions/cleave/lifecycle-emitter.ts +20 -0
- package/extensions/cleave/openspec.ts +811 -0
- package/extensions/cleave/planner.ts +260 -0
- package/extensions/cleave/review.ts +579 -0
- package/extensions/cleave/skills.ts +355 -0
- package/extensions/cleave/types.ts +261 -0
- package/extensions/cleave/workspace.ts +861 -0
- package/extensions/cleave/worktree.ts +243 -0
- package/extensions/core-renderers.ts +253 -0
- package/extensions/dashboard/context-gauge.ts +58 -0
- package/extensions/dashboard/file-watch.ts +14 -0
- package/extensions/dashboard/footer.ts +1145 -0
- package/extensions/dashboard/git.ts +185 -0
- package/extensions/dashboard/index.ts +478 -0
- package/extensions/dashboard/memory-audit.ts +34 -0
- package/extensions/dashboard/overlay-data.ts +705 -0
- package/extensions/dashboard/overlay.ts +365 -0
- package/extensions/dashboard/render-utils.ts +54 -0
- package/extensions/dashboard/types.ts +191 -0
- package/extensions/dashboard/uri-helper.ts +45 -0
- package/extensions/debug.ts +69 -0
- package/extensions/defaults.ts +282 -0
- package/extensions/design-tree/dashboard-state.ts +161 -0
- package/extensions/design-tree/design-card.ts +362 -0
- package/extensions/design-tree/index.ts +2130 -0
- package/extensions/design-tree/lifecycle-emitter.ts +41 -0
- package/extensions/design-tree/tree.ts +1607 -0
- package/extensions/design-tree/types.ts +163 -0
- package/extensions/distill.ts +127 -0
- package/extensions/effort/index.ts +395 -0
- package/extensions/effort/tiers.ts +146 -0
- package/extensions/effort/types.ts +105 -0
- package/extensions/lib/git-state.ts +227 -0
- package/extensions/lib/local-models.ts +157 -0
- package/extensions/lib/model-preferences.ts +51 -0
- package/extensions/lib/model-routing.ts +720 -0
- package/extensions/lib/operator-fallback.ts +205 -0
- package/extensions/lib/operator-profile.ts +360 -0
- package/extensions/lib/slash-command-bridge.ts +253 -0
- package/extensions/lib/typebox-helpers.ts +16 -0
- package/extensions/local-inference/index.ts +727 -0
- package/extensions/mcp-bridge/README.md +220 -0
- package/extensions/mcp-bridge/index.ts +951 -0
- package/extensions/mcp-bridge/lib.ts +365 -0
- package/extensions/mcp-bridge/mcp.json +3 -0
- package/extensions/mcp-bridge/package.json +11 -0
- package/extensions/model-budget.ts +752 -0
- package/extensions/offline-driver.ts +403 -0
- package/extensions/openspec/archive-gate.ts +164 -0
- package/extensions/openspec/branch-cleanup.ts +64 -0
- package/extensions/openspec/dashboard-state.ts +50 -0
- package/extensions/openspec/index.ts +1917 -0
- package/extensions/openspec/lifecycle-emitter.ts +65 -0
- package/extensions/openspec/lifecycle-files.ts +70 -0
- package/extensions/openspec/lifecycle.ts +50 -0
- package/extensions/openspec/reconcile.ts +187 -0
- package/extensions/openspec/spec.ts +1385 -0
- package/extensions/openspec/types.ts +98 -0
- package/extensions/project-memory/DESIGN-global-mind.md +198 -0
- package/extensions/project-memory/README.md +202 -0
- package/extensions/project-memory/api-types.ts +382 -0
- package/extensions/project-memory/compaction-policy.ts +29 -0
- package/extensions/project-memory/core.ts +164 -0
- package/extensions/project-memory/embeddings.ts +230 -0
- package/extensions/project-memory/extraction-v2.ts +861 -0
- package/extensions/project-memory/factstore.ts +2177 -0
- package/extensions/project-memory/index.ts +3459 -0
- package/extensions/project-memory/injection-metrics.ts +91 -0
- package/extensions/project-memory/jsonl-io.ts +12 -0
- package/extensions/project-memory/lifecycle.ts +331 -0
- package/extensions/project-memory/migration.ts +293 -0
- package/extensions/project-memory/package.json +9 -0
- package/extensions/project-memory/sci-renderers.ts +7 -0
- package/extensions/project-memory/template.ts +103 -0
- package/extensions/project-memory/triggers.ts +52 -0
- package/extensions/project-memory/types.ts +102 -0
- package/extensions/render/composition/fonts/Inter-Bold.ttf +0 -0
- package/extensions/render/composition/fonts/Inter-Regular.ttf +0 -0
- package/extensions/render/composition/fonts/Tomorrow-Bold.ttf +0 -0
- package/extensions/render/composition/fonts/Tomorrow-Regular.ttf +0 -0
- package/extensions/render/composition/package-lock.json +534 -0
- package/extensions/render/composition/package.json +22 -0
- package/extensions/render/composition/render.mjs +246 -0
- package/extensions/render/composition/test-comp.tsx +87 -0
- package/extensions/render/composition/types.ts +24 -0
- package/extensions/render/excalidraw/UPSTREAM.md +81 -0
- package/extensions/render/excalidraw/elements.ts +764 -0
- package/extensions/render/excalidraw/index.ts +66 -0
- package/extensions/render/excalidraw/types.ts +223 -0
- package/extensions/render/excalidraw-renderer/pyproject.toml +8 -0
- package/extensions/render/excalidraw-renderer/render_excalidraw.py +182 -0
- package/extensions/render/excalidraw-renderer/render_template.html +59 -0
- package/extensions/render/index.ts +830 -0
- package/extensions/render/native-diagrams/index.ts +57 -0
- package/extensions/render/native-diagrams/motifs.ts +542 -0
- package/extensions/render/native-diagrams/raster.ts +8 -0
- package/extensions/render/native-diagrams/scene.ts +75 -0
- package/extensions/render/native-diagrams/spec.ts +204 -0
- package/extensions/render/native-diagrams/svg.ts +116 -0
- package/extensions/sci-ui.ts +304 -0
- package/extensions/session-log.ts +174 -0
- package/extensions/shared-state.ts +146 -0
- package/extensions/spinner-verbs.ts +91 -0
- package/extensions/style.ts +281 -0
- package/extensions/terminal-title.ts +191 -0
- package/extensions/tool-profile/index.ts +291 -0
- package/extensions/tool-profile/profiles.ts +290 -0
- package/extensions/types.d.ts +9 -0
- package/extensions/vault/index.ts +185 -0
- package/extensions/version-check.ts +90 -0
- package/extensions/view/index.ts +859 -0
- package/extensions/view/uri-resolver.ts +148 -0
- package/extensions/web-search/index.ts +182 -0
- package/extensions/web-search/providers.ts +121 -0
- package/extensions/web-ui/index.ts +110 -0
- package/extensions/web-ui/server.ts +265 -0
- package/extensions/web-ui/state.ts +462 -0
- package/extensions/web-ui/static/index.html +145 -0
- package/extensions/web-ui/types.ts +284 -0
- package/package.json +76 -0
- package/prompts/init.md +75 -0
- package/prompts/new-repo.md +54 -0
- package/prompts/oci-login.md +56 -0
- package/prompts/status.md +50 -0
- package/settings.json +4 -0
- package/skills/cleave/SKILL.md +218 -0
- package/skills/git/SKILL.md +209 -0
- package/skills/git/_reference/ci-validation.md +204 -0
- package/skills/oci/SKILL.md +338 -0
- package/skills/openspec/SKILL.md +346 -0
- package/skills/pi-extensions/SKILL.md +191 -0
- package/skills/pi-tui/SKILL.md +517 -0
- package/skills/python/SKILL.md +189 -0
- package/skills/rust/SKILL.md +268 -0
- package/skills/security/SKILL.md +206 -0
- package/skills/style/SKILL.md +264 -0
- package/skills/typescript/SKILL.md +225 -0
- package/skills/vault/SKILL.md +102 -0
- package/themes/alpharius-legacy.json +85 -0
- package/themes/alpharius.conf +59 -0
- package/themes/alpharius.json +88 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* design-tree/types — Shared type definitions for the design tree extension.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// ─── Node Status ─────────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
export type NodeStatus = "seed" | "exploring" | "decided" | "implementing" | "implemented" | "blocked" | "deferred";
|
|
8
|
+
|
|
9
|
+
export const VALID_STATUSES: NodeStatus[] = ["seed", "exploring", "decided", "implementing", "implemented", "blocked", "deferred"];
|
|
10
|
+
|
|
11
|
+
export const STATUS_ICONS: Record<NodeStatus, string> = {
|
|
12
|
+
seed: "◌",
|
|
13
|
+
exploring: "◐",
|
|
14
|
+
decided: "●",
|
|
15
|
+
implementing: "⚙",
|
|
16
|
+
implemented: "✓",
|
|
17
|
+
blocked: "✕",
|
|
18
|
+
deferred: "◑",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const STATUS_COLORS: Record<NodeStatus, string> = {
|
|
22
|
+
seed: "muted",
|
|
23
|
+
exploring: "accent",
|
|
24
|
+
decided: "success",
|
|
25
|
+
implementing: "accent",
|
|
26
|
+
implemented: "success",
|
|
27
|
+
blocked: "error",
|
|
28
|
+
deferred: "warning",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// ─── Structured Sections ─────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
/** A decision recorded in the ## Decisions section */
|
|
34
|
+
export interface DesignDecision {
|
|
35
|
+
title: string;
|
|
36
|
+
status: "exploring" | "decided" | "rejected";
|
|
37
|
+
rationale: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** A research entry recorded in the ## Research section */
|
|
41
|
+
export interface ResearchEntry {
|
|
42
|
+
heading: string;
|
|
43
|
+
content: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** File scope entry from ## Implementation Notes */
|
|
47
|
+
export interface FileScope {
|
|
48
|
+
path: string;
|
|
49
|
+
description: string;
|
|
50
|
+
action?: "new" | "modified" | "deleted";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ─── Acceptance Criteria ─────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
/** A Given/When/Then scenario from ## Acceptance Criteria → ### Scenarios */
|
|
56
|
+
export interface AcceptanceCriteriaScenario {
|
|
57
|
+
title: string;
|
|
58
|
+
given: string;
|
|
59
|
+
when: string;
|
|
60
|
+
then: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** A falsifiability condition from ## Acceptance Criteria → ### Falsifiability */
|
|
64
|
+
export interface AcceptanceCriteriaFalsifiability {
|
|
65
|
+
condition: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** A checkbox constraint from ## Acceptance Criteria → ### Constraints */
|
|
69
|
+
export interface AcceptanceCriteriaConstraint {
|
|
70
|
+
text: string;
|
|
71
|
+
checked: boolean;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Parsed ## Acceptance Criteria section */
|
|
75
|
+
export interface AcceptanceCriteria {
|
|
76
|
+
scenarios: AcceptanceCriteriaScenario[];
|
|
77
|
+
falsifiability: AcceptanceCriteriaFalsifiability[];
|
|
78
|
+
constraints: AcceptanceCriteriaConstraint[];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Parsed structured sections from the document body */
|
|
82
|
+
export interface DocumentSections {
|
|
83
|
+
overview: string;
|
|
84
|
+
research: ResearchEntry[];
|
|
85
|
+
decisions: DesignDecision[];
|
|
86
|
+
openQuestions: string[];
|
|
87
|
+
implementationNotes: {
|
|
88
|
+
fileScope: FileScope[];
|
|
89
|
+
constraints: string[];
|
|
90
|
+
rawContent: string;
|
|
91
|
+
};
|
|
92
|
+
acceptanceCriteria: AcceptanceCriteria;
|
|
93
|
+
/** Any content not in a recognized section */
|
|
94
|
+
extraSections: Array<{ heading: string; content: string }>;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ─── Issue Type ──────────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
export type IssueType = "epic" | "feature" | "task" | "bug" | "chore";
|
|
100
|
+
|
|
101
|
+
export const VALID_ISSUE_TYPES: IssueType[] = ["epic", "feature", "task", "bug", "chore"];
|
|
102
|
+
|
|
103
|
+
export const ISSUE_TYPE_ICONS: Record<IssueType, string> = {
|
|
104
|
+
epic: "⬡",
|
|
105
|
+
feature: "★",
|
|
106
|
+
task: "◻",
|
|
107
|
+
bug: "✖",
|
|
108
|
+
chore: "⟳",
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// ─── Priority ────────────────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
export type Priority = 1 | 2 | 3 | 4 | 5;
|
|
114
|
+
|
|
115
|
+
export const PRIORITY_LABELS: Record<Priority, string> = {
|
|
116
|
+
1: "critical",
|
|
117
|
+
2: "high",
|
|
118
|
+
3: "medium",
|
|
119
|
+
4: "low",
|
|
120
|
+
5: "trivial",
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// ─── Design Node ─────────────────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
export interface DesignNode {
|
|
126
|
+
id: string;
|
|
127
|
+
title: string;
|
|
128
|
+
status: NodeStatus;
|
|
129
|
+
parent?: string;
|
|
130
|
+
dependencies: string[];
|
|
131
|
+
related: string[];
|
|
132
|
+
tags: string[];
|
|
133
|
+
/** Open questions — synced from ## Open Questions body section */
|
|
134
|
+
open_questions: string[];
|
|
135
|
+
/** Explicit branch name override (D1). When set, used instead of feature/<node-id> */
|
|
136
|
+
branch?: string;
|
|
137
|
+
/** Git branches associated with this node — history of all branches that carried this work */
|
|
138
|
+
branches: string[];
|
|
139
|
+
/** OpenSpec change name linked to this node */
|
|
140
|
+
openspec_change?: string;
|
|
141
|
+
/** Issue type classification (epic/feature/task/bug/chore) */
|
|
142
|
+
issue_type?: IssueType;
|
|
143
|
+
/** Priority from 1 (critical) to 5 (trivial) */
|
|
144
|
+
priority?: Priority;
|
|
145
|
+
filePath: string;
|
|
146
|
+
lastModified: number;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface DesignTree {
|
|
150
|
+
nodes: Map<string, DesignNode>;
|
|
151
|
+
docsDir: string;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ─── Document Template ───────────────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
export const SECTION_HEADINGS = {
|
|
157
|
+
overview: "## Overview",
|
|
158
|
+
research: "## Research",
|
|
159
|
+
decisions: "## Decisions",
|
|
160
|
+
openQuestions: "## Open Questions",
|
|
161
|
+
implementationNotes: "## Implementation Notes",
|
|
162
|
+
acceptanceCriteria: "## Acceptance Criteria",
|
|
163
|
+
} as const;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* distill — Context distillation for session handoff
|
|
3
|
+
*
|
|
4
|
+
* Registers `/distill` command that analyzes the current conversation context
|
|
5
|
+
* and produces a portable distillation summary for bootstrapping a fresh session.
|
|
6
|
+
*
|
|
7
|
+
* The extension handles the mechanical parts (directory creation, file writing,
|
|
8
|
+
* git status collection) and delegates the summarization to the LLM via
|
|
9
|
+
* sendUserMessage.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
13
|
+
import { join, basename } from "node:path";
|
|
14
|
+
import type { ExtensionAPI } from "@cwilson613/pi-coding-agent";
|
|
15
|
+
|
|
16
|
+
export default function distillExtension(pi: ExtensionAPI) {
|
|
17
|
+
|
|
18
|
+
pi.registerCommand("distill", {
|
|
19
|
+
description: "Create a portable session distillation for fresh context bootstrap",
|
|
20
|
+
handler: async (_args, ctx) => {
|
|
21
|
+
// Gather mechanical context the LLM will need
|
|
22
|
+
const cwd = ctx.cwd;
|
|
23
|
+
const repoName = basename(cwd);
|
|
24
|
+
|
|
25
|
+
// Git status
|
|
26
|
+
let branch = "unknown";
|
|
27
|
+
let recentCommits = "";
|
|
28
|
+
let uncommitted = "";
|
|
29
|
+
try {
|
|
30
|
+
const branchResult = await pi.exec("git", ["branch", "--show-current"], { timeout: 5_000, cwd });
|
|
31
|
+
branch = branchResult.stdout.trim() || "HEAD detached";
|
|
32
|
+
} catch { /* ignore */ }
|
|
33
|
+
try {
|
|
34
|
+
const logResult = await pi.exec(
|
|
35
|
+
"git", ["log", "--oneline", "-10", "--no-decorate"],
|
|
36
|
+
{ timeout: 5_000, cwd },
|
|
37
|
+
);
|
|
38
|
+
recentCommits = logResult.stdout.trim();
|
|
39
|
+
} catch { /* ignore */ }
|
|
40
|
+
try {
|
|
41
|
+
const statusResult = await pi.exec("git", ["status", "--porcelain"], { timeout: 5_000, cwd });
|
|
42
|
+
uncommitted = statusResult.stdout.trim() || "(clean)";
|
|
43
|
+
} catch { /* ignore */ }
|
|
44
|
+
|
|
45
|
+
// Ensure distillation directory exists
|
|
46
|
+
const distillDir = join(cwd, ".pi", "distillations");
|
|
47
|
+
mkdirSync(distillDir, { recursive: true });
|
|
48
|
+
|
|
49
|
+
// Generate a timestamp-based filename
|
|
50
|
+
const now = new Date();
|
|
51
|
+
const ts = now.toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
52
|
+
|
|
53
|
+
// Build the prompt for the LLM to do the actual summarization
|
|
54
|
+
const prompt = [
|
|
55
|
+
"Analyze the full conversation context and create a session distillation.",
|
|
56
|
+
"",
|
|
57
|
+
"**Repository context (already gathered):**",
|
|
58
|
+
`- Working directory: ${cwd}`,
|
|
59
|
+
`- Repository: ${repoName}`,
|
|
60
|
+
`- Branch: ${branch}`,
|
|
61
|
+
`- Recent commits:`,
|
|
62
|
+
"```",
|
|
63
|
+
recentCommits || "(no commits)",
|
|
64
|
+
"```",
|
|
65
|
+
`- Uncommitted changes:`,
|
|
66
|
+
"```",
|
|
67
|
+
uncommitted,
|
|
68
|
+
"```",
|
|
69
|
+
"",
|
|
70
|
+
"**Instructions:**",
|
|
71
|
+
"",
|
|
72
|
+
`Write a distillation file to \`${distillDir}/${ts}-<slug>.md\` where <slug> is a 2-3 word kebab-case description of the session topic.`,
|
|
73
|
+
"",
|
|
74
|
+
"Use this structure:",
|
|
75
|
+
"",
|
|
76
|
+
"```markdown",
|
|
77
|
+
"# Session Distillation: <brief-title>",
|
|
78
|
+
"",
|
|
79
|
+
"Generated: <timestamp>",
|
|
80
|
+
`Working Directory: ${cwd}`,
|
|
81
|
+
`Repository: ${repoName}`,
|
|
82
|
+
"",
|
|
83
|
+
"## Session Overview",
|
|
84
|
+
"<2-3 sentence summary of what was accomplished>",
|
|
85
|
+
"",
|
|
86
|
+
"## Technical State",
|
|
87
|
+
"### Repository Status",
|
|
88
|
+
`- Branch: ${branch}`,
|
|
89
|
+
"- Recent commits: <summarize>",
|
|
90
|
+
"- Uncommitted changes: <summarize>",
|
|
91
|
+
"",
|
|
92
|
+
"### Key Changes This Session",
|
|
93
|
+
"<bulleted list of significant modifications>",
|
|
94
|
+
"",
|
|
95
|
+
"## Decisions Made",
|
|
96
|
+
"<numbered list with brief rationale>",
|
|
97
|
+
"",
|
|
98
|
+
"## Pending Items",
|
|
99
|
+
"### Incomplete Work",
|
|
100
|
+
"### Known Issues",
|
|
101
|
+
"### Planned Next Steps",
|
|
102
|
+
"",
|
|
103
|
+
"## Critical Context",
|
|
104
|
+
"<information that would be difficult to reconstruct>",
|
|
105
|
+
"",
|
|
106
|
+
"## File Reference",
|
|
107
|
+
"Key files for continuation:",
|
|
108
|
+
"- <path>: <purpose>",
|
|
109
|
+
"```",
|
|
110
|
+
"",
|
|
111
|
+
"After writing the file, output a handoff block like:",
|
|
112
|
+
"",
|
|
113
|
+
"```",
|
|
114
|
+
"Distillation saved to: <path>",
|
|
115
|
+
"",
|
|
116
|
+
"To continue in a fresh session, copy this prompt:",
|
|
117
|
+
"---",
|
|
118
|
+
"Continue from distillation: <path>",
|
|
119
|
+
"Read the distillation file and confirm you understand the context before proceeding.",
|
|
120
|
+
"---",
|
|
121
|
+
"```",
|
|
122
|
+
].join("\n");
|
|
123
|
+
|
|
124
|
+
pi.sendUserMessage(prompt, { deliverAs: "followUp" });
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
}
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* effort — Global inference cost control extension.
|
|
3
|
+
*
|
|
4
|
+
* Provides a single `/effort` command to switch between 7 named tiers
|
|
5
|
+
* (Servitor → Omnissiah), each controlling the driver model, thinking level,
|
|
6
|
+
* and downstream settings for cleave dispatch, extraction, and compaction.
|
|
7
|
+
*
|
|
8
|
+
* On session_start: resolves the active tier from PI_EFFORT env var,
|
|
9
|
+
* .pi/config.json, or default (Substantial), writes to sharedState.effort,
|
|
10
|
+
* and switches the driver model + thinking level accordingly.
|
|
11
|
+
*
|
|
12
|
+
* Commands:
|
|
13
|
+
* /effort — Show current tier info
|
|
14
|
+
* /effort <name> — Switch to named tier
|
|
15
|
+
* /effort cap — Lock ceiling at current tier
|
|
16
|
+
* /effort uncap — Remove ceiling lock
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { ExtensionAPI, ExtensionContext } from "@cwilson613/pi-coding-agent";
|
|
20
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
21
|
+
import { join } from "node:path";
|
|
22
|
+
|
|
23
|
+
import type { EffortLevel, EffortState, EffortModelTier, ThinkingLevel } from "./types.ts";
|
|
24
|
+
import { EFFORT_NAMES } from "./types.ts";
|
|
25
|
+
import { tierConfig, parseTierName, DEFAULT_EFFORT_LEVEL, TIER_NAMES } from "./tiers.ts";
|
|
26
|
+
import { sharedState, DASHBOARD_UPDATE_EVENT } from "../shared-state.ts";
|
|
27
|
+
import {
|
|
28
|
+
resolveTier,
|
|
29
|
+
getTierDisplayLabel,
|
|
30
|
+
getDefaultPolicy,
|
|
31
|
+
clampThinkingLevel,
|
|
32
|
+
type ModelTier,
|
|
33
|
+
type RegistryModel,
|
|
34
|
+
} from "../lib/model-routing.ts";
|
|
35
|
+
import { readLastUsedModel, writeLastUsedModel } from "../lib/model-preferences.ts";
|
|
36
|
+
import { readOperatorProfile, loadOperatorRuntimeState, toCapabilityProfile, toCapabilityRuntimeState } from "../lib/operator-profile.ts";
|
|
37
|
+
|
|
38
|
+
// ─── Constants ───────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
/** Tier icons indexed by level. */
|
|
41
|
+
const TIER_ICONS: Record<EffortLevel, string> = {
|
|
42
|
+
1: "○",
|
|
43
|
+
2: "●",
|
|
44
|
+
3: "†",
|
|
45
|
+
4: "⚔",
|
|
46
|
+
5: "☠",
|
|
47
|
+
6: "💀",
|
|
48
|
+
7: "🤖",
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
function getResolverInputs(ctx: ExtensionContext) {
|
|
52
|
+
const policy = sharedState.routingPolicy ?? getDefaultPolicy();
|
|
53
|
+
const profile = toCapabilityProfile(readOperatorProfile(ctx.cwd));
|
|
54
|
+
const runtimeState = toCapabilityRuntimeState(loadOperatorRuntimeState(ctx.cwd));
|
|
55
|
+
return { policy, profile, runtimeState };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ─── Model Switching ─────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Switch the driver model to match the effort tier's driver setting.
|
|
62
|
+
* Uses the shared resolveTier() resolver with the current session policy.
|
|
63
|
+
* Returns true if the switch succeeded.
|
|
64
|
+
*
|
|
65
|
+
* C3: `all` is fetched once and indexed as a Map so the post-resolution lookup
|
|
66
|
+
* is O(1) with no second linear scan. Both resolveTier and the model lookup
|
|
67
|
+
* operate on the same snapshot.
|
|
68
|
+
*/
|
|
69
|
+
async function switchDriverModel(
|
|
70
|
+
pi: ExtensionAPI,
|
|
71
|
+
ctx: ExtensionContext,
|
|
72
|
+
driver: EffortModelTier,
|
|
73
|
+
): Promise<{ model: RegistryModel; maxThinking?: ThinkingLevel } | null> {
|
|
74
|
+
// Snapshot the registry once; both resolveTier and the model lookup use it
|
|
75
|
+
const all = ctx.modelRegistry.getAll() as unknown as RegistryModel[];
|
|
76
|
+
// Build O(1) index over the same snapshot — no second linear scan (C3)
|
|
77
|
+
const byKey = new Map(all.map((m) => [`${m.provider}/${m.id}`, m]));
|
|
78
|
+
const { policy, profile, runtimeState } = getResolverInputs(ctx);
|
|
79
|
+
const resolved = resolveTier(driver, all, policy, runtimeState, profile);
|
|
80
|
+
if (!resolved) return null;
|
|
81
|
+
// Direct map lookup — no second linear scan of `all`
|
|
82
|
+
const model = byKey.get(`${resolved.provider}/${resolved.modelId}`);
|
|
83
|
+
if (!model) return null;
|
|
84
|
+
const success = await pi.setModel(model as any);
|
|
85
|
+
return success ? { model, maxThinking: resolved.maxThinking as ThinkingLevel | undefined } : null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function restoreLastUsedModel(
|
|
89
|
+
pi: ExtensionAPI,
|
|
90
|
+
ctx: ExtensionContext,
|
|
91
|
+
): Promise<RegistryModel | null> {
|
|
92
|
+
const persisted = readLastUsedModel(ctx.cwd);
|
|
93
|
+
if (!persisted) return null;
|
|
94
|
+
const model = ctx.modelRegistry.find(persisted.provider, persisted.modelId) as unknown as RegistryModel | undefined;
|
|
95
|
+
if (!model) return null;
|
|
96
|
+
const success = await pi.setModel(model as any);
|
|
97
|
+
return success ? model : null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Resolve the effective extraction tier, honoring the session routing policy.
|
|
102
|
+
*
|
|
103
|
+
* When cheapCloudPreferredOverLocal is true and the effort tier's extraction
|
|
104
|
+
* setting is "local", we upgrade to "retribution" (cheapest cloud tier) so that
|
|
105
|
+
* background extraction work uses a cost-effective cloud model when available.
|
|
106
|
+
* If no cloud model satisfies retribution, falls back to "local" transparently.
|
|
107
|
+
*
|
|
108
|
+
* Spec: "Extraction prefers cheap cloud when configured"
|
|
109
|
+
* "Offline or unavailable cloud falls back safely"
|
|
110
|
+
*/
|
|
111
|
+
function resolveExtractionTier(
|
|
112
|
+
extraction: EffortModelTier,
|
|
113
|
+
ctx: ExtensionContext,
|
|
114
|
+
): { displayTier: string; resolvedModelId?: string } {
|
|
115
|
+
const { policy, profile, runtimeState } = getResolverInputs(ctx);
|
|
116
|
+
const all = ctx.modelRegistry.getAll() as unknown as RegistryModel[];
|
|
117
|
+
|
|
118
|
+
// Determine effective tier: upgrade local→retribution when policy prefers cheap cloud
|
|
119
|
+
const effectiveTier: ModelTier =
|
|
120
|
+
policy.cheapCloudPreferredOverLocal && extraction === "local" ? "retribution" : extraction;
|
|
121
|
+
|
|
122
|
+
const resolved = resolveTier(effectiveTier, all, policy, runtimeState, profile);
|
|
123
|
+
|
|
124
|
+
// If cloud preferred but no cloud model matched, fall back to local.
|
|
125
|
+
// We call resolveTier("local") rather than matchLocalTier() directly because
|
|
126
|
+
// resolveTier is the public API. The cloud-preferring policy is passed through
|
|
127
|
+
// intentionally — resolveTier's "local" path ignores policy entirely and goes
|
|
128
|
+
// straight to matchLocalTier(), so the policy has no effect here. This is
|
|
129
|
+
// safe and avoids importing the private matchLocalTier function.
|
|
130
|
+
const final =
|
|
131
|
+
resolved ?? (effectiveTier !== "local" ? resolveTier("local", all, policy, runtimeState, profile) : undefined);
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
displayTier: final ? getTierDisplayLabel(final.tier) : getTierDisplayLabel(effectiveTier),
|
|
135
|
+
resolvedModelId: final?.modelId,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ─── Config Resolution ───────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Read the effort tier from .pi/config.json in the project root.
|
|
143
|
+
* Returns undefined if file doesn't exist or has no effort key.
|
|
144
|
+
*/
|
|
145
|
+
function readConfigEffort(cwd: string): string | undefined {
|
|
146
|
+
try {
|
|
147
|
+
const configPath = join(cwd, ".pi", "config.json");
|
|
148
|
+
if (!existsSync(configPath)) return undefined;
|
|
149
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
150
|
+
const parsed = JSON.parse(raw);
|
|
151
|
+
return typeof parsed.effort === "string" ? parsed.effort : undefined;
|
|
152
|
+
} catch {
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Resolve the initial effort level from (in priority order):
|
|
159
|
+
* 1. PI_EFFORT environment variable
|
|
160
|
+
* 2. .pi/config.json effort field
|
|
161
|
+
* 3. Default (Substantial, level 3)
|
|
162
|
+
*/
|
|
163
|
+
function resolveInitialLevel(cwd: string): EffortLevel {
|
|
164
|
+
// 1. Environment variable
|
|
165
|
+
const envValue = process.env.PI_EFFORT;
|
|
166
|
+
if (envValue) {
|
|
167
|
+
const level = parseTierName(envValue);
|
|
168
|
+
if (level !== undefined) return level;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 2. Config file
|
|
172
|
+
const configValue = readConfigEffort(cwd);
|
|
173
|
+
if (configValue) {
|
|
174
|
+
const level = parseTierName(configValue);
|
|
175
|
+
if (level !== undefined) return level;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 3. Default
|
|
179
|
+
return DEFAULT_EFFORT_LEVEL;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Build an EffortState from a tier level.
|
|
184
|
+
* Preserves existing cap state if provided.
|
|
185
|
+
* resolvedExtractionModelId is always initialized to undefined here;
|
|
186
|
+
* callers must invoke resolveExtractionTier() and populate it before
|
|
187
|
+
* writing to sharedState.effort (W2).
|
|
188
|
+
*/
|
|
189
|
+
function buildEffortState(
|
|
190
|
+
level: EffortLevel,
|
|
191
|
+
capped: boolean = false,
|
|
192
|
+
capLevel?: EffortLevel,
|
|
193
|
+
): EffortState {
|
|
194
|
+
const config = tierConfig(level);
|
|
195
|
+
return {
|
|
196
|
+
...config,
|
|
197
|
+
capped,
|
|
198
|
+
capLevel,
|
|
199
|
+
resolvedExtractionModelId: undefined,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ─── Display Helpers ─────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
function formatTierInfo(state: EffortState): string {
|
|
206
|
+
const icon = TIER_ICONS[state.level];
|
|
207
|
+
const capIndicator = state.capped && state.capLevel
|
|
208
|
+
? ` [CAPPED at ${EFFORT_NAMES[state.capLevel]}]`
|
|
209
|
+
: "";
|
|
210
|
+
const driverLabel = getTierDisplayLabel(state.driver);
|
|
211
|
+
const extractionLabel = getTierDisplayLabel(state.extraction);
|
|
212
|
+
const compactionLabel = getTierDisplayLabel(state.compaction);
|
|
213
|
+
const reviewLabel = getTierDisplayLabel(state.reviewModel);
|
|
214
|
+
const floorLabel = getTierDisplayLabel(state.cleaveFloor);
|
|
215
|
+
const lines = [
|
|
216
|
+
`${icon} **${state.name}** (level ${state.level}/7)${capIndicator}`,
|
|
217
|
+
` Driver: ${driverLabel} (${state.driver}) | Thinking: ${state.thinking}`,
|
|
218
|
+
` Extraction: ${extractionLabel} (${state.extraction}) | Compaction: ${compactionLabel} (${state.compaction})`,
|
|
219
|
+
` Cleave: preferLocal=${state.cleavePreferLocal}, floor=${floorLabel} (${state.cleaveFloor})`,
|
|
220
|
+
` Review: ${reviewLabel} (${state.reviewModel})`,
|
|
221
|
+
];
|
|
222
|
+
return lines.join("\n");
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ─── Extension Entry Point ───────────────────────────────────
|
|
226
|
+
|
|
227
|
+
export default function (pi: ExtensionAPI) {
|
|
228
|
+
// ── Session Start: resolve and apply effort tier ──
|
|
229
|
+
|
|
230
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
231
|
+
const level = resolveInitialLevel(ctx.cwd);
|
|
232
|
+
const state = buildEffortState(level);
|
|
233
|
+
|
|
234
|
+
// Resolve extraction tier under current routing policy (C1: spec compliance).
|
|
235
|
+
// When cheapCloudPreferredOverLocal is true this upgrades local→retribution and
|
|
236
|
+
// falls back to local if no cloud model is available.
|
|
237
|
+
const extractionResolution = resolveExtractionTier(state.extraction, ctx);
|
|
238
|
+
state.resolvedExtractionModelId = extractionResolution.resolvedModelId;
|
|
239
|
+
|
|
240
|
+
// Write to shared state
|
|
241
|
+
sharedState.effort = state;
|
|
242
|
+
pi.events.emit(DASHBOARD_UPDATE_EVENT, { source: "effort" });
|
|
243
|
+
|
|
244
|
+
// Restore the operator's last explicit model choice when possible.
|
|
245
|
+
// If none is persisted (or it is no longer available), fall back to the
|
|
246
|
+
// current effort tier's resolved driver. As a final guard, keep pi's
|
|
247
|
+
// current startup model rather than warning about an unusable session when
|
|
248
|
+
// a working driver is already present.
|
|
249
|
+
const restoredModel = await restoreLastUsedModel(pi, ctx);
|
|
250
|
+
const switchedDriver = restoredModel ? null : await switchDriverModel(pi, ctx, state.driver);
|
|
251
|
+
const retainedModel = !restoredModel && !switchedDriver && ctx.model ? ctx.model : null;
|
|
252
|
+
|
|
253
|
+
// Set thinking level, respecting candidate ceilings when the effort-driven
|
|
254
|
+
// model switch produced a structured resolver result.
|
|
255
|
+
const effectiveThinking: ThinkingLevel = switchedDriver?.maxThinking
|
|
256
|
+
? clampThinkingLevel(state.thinking, switchedDriver.maxThinking)
|
|
257
|
+
: restoredModel || retainedModel
|
|
258
|
+
? state.thinking
|
|
259
|
+
: state.thinking;
|
|
260
|
+
pi.setThinkingLevel(effectiveThinking as any);
|
|
261
|
+
|
|
262
|
+
// Notify operator
|
|
263
|
+
const icon = TIER_ICONS[state.level];
|
|
264
|
+
const modelNote = restoredModel
|
|
265
|
+
? ` → restored ${restoredModel.provider}/${restoredModel.id}`
|
|
266
|
+
: switchedDriver
|
|
267
|
+
? ` → ${switchedDriver.model.provider}/${switchedDriver.model.id}`
|
|
268
|
+
: retainedModel
|
|
269
|
+
? ` → kept ${retainedModel.provider}/${retainedModel.id} (preferred ${state.driver} unavailable)`
|
|
270
|
+
: " (driver model unavailable)";
|
|
271
|
+
ctx.ui.notify(
|
|
272
|
+
`${icon} Effort: ${state.name} (${state.driver}/${effectiveThinking})${modelNote}`,
|
|
273
|
+
restoredModel || switchedDriver || retainedModel ? "info" : "warning",
|
|
274
|
+
);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// ── /effort command ──
|
|
278
|
+
|
|
279
|
+
pi.registerCommand("effort", {
|
|
280
|
+
description: "View or change effort tier. Usage: /effort [tier|cap|uncap]",
|
|
281
|
+
getArgumentCompletions: (prefix: string) => {
|
|
282
|
+
const options = [...TIER_NAMES, "cap", "uncap"];
|
|
283
|
+
const lower = prefix.toLowerCase();
|
|
284
|
+
const matches = options.filter((o) => o.toLowerCase().startsWith(lower));
|
|
285
|
+
return matches.map((name) => ({
|
|
286
|
+
label: name,
|
|
287
|
+
value: name,
|
|
288
|
+
}));
|
|
289
|
+
},
|
|
290
|
+
handler: async (args, ctx) => {
|
|
291
|
+
const arg = args.trim();
|
|
292
|
+
|
|
293
|
+
// No args → show current tier
|
|
294
|
+
if (!arg) {
|
|
295
|
+
const state = sharedState.effort;
|
|
296
|
+
if (!state) {
|
|
297
|
+
ctx.ui.notify("⚠️ Effort state not initialized", "warning");
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
ctx.ui.notify(formatTierInfo(state), "info");
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// /effort cap
|
|
305
|
+
if (arg.toLowerCase() === "cap") {
|
|
306
|
+
const state = sharedState.effort;
|
|
307
|
+
if (!state) {
|
|
308
|
+
ctx.ui.notify("⚠️ Effort state not initialized", "warning");
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
const icon = TIER_ICONS[state.level];
|
|
312
|
+
const newState = buildEffortState(state.level, true, state.level);
|
|
313
|
+
// Resolve extraction tier and populate before writing to sharedState (W1)
|
|
314
|
+
const extractionResolution = resolveExtractionTier(newState.extraction, ctx);
|
|
315
|
+
newState.resolvedExtractionModelId = extractionResolution.resolvedModelId;
|
|
316
|
+
sharedState.effort = newState;
|
|
317
|
+
pi.events.emit(DASHBOARD_UPDATE_EVENT, { source: "effort" });
|
|
318
|
+
ctx.ui.notify(
|
|
319
|
+
`${icon} Effort capped at ${state.name} (level ${state.level}) — agent cannot upgrade past this tier`,
|
|
320
|
+
"info",
|
|
321
|
+
);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// /effort uncap
|
|
326
|
+
if (arg.toLowerCase() === "uncap") {
|
|
327
|
+
const state = sharedState.effort;
|
|
328
|
+
if (!state) {
|
|
329
|
+
ctx.ui.notify("⚠️ Effort state not initialized", "warning");
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
const icon = TIER_ICONS[state.level];
|
|
333
|
+
const newState = buildEffortState(state.level, false);
|
|
334
|
+
// Resolve extraction tier and populate before writing to sharedState (W1)
|
|
335
|
+
const extractionResolution = resolveExtractionTier(newState.extraction, ctx);
|
|
336
|
+
newState.resolvedExtractionModelId = extractionResolution.resolvedModelId;
|
|
337
|
+
sharedState.effort = newState;
|
|
338
|
+
pi.events.emit(DASHBOARD_UPDATE_EVENT, { source: "effort" });
|
|
339
|
+
ctx.ui.notify(
|
|
340
|
+
`${icon} Effort cap removed — agent can freely upgrade`,
|
|
341
|
+
"info",
|
|
342
|
+
);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// /effort <tier name>
|
|
347
|
+
const level = parseTierName(arg);
|
|
348
|
+
if (level === undefined) {
|
|
349
|
+
const valid = TIER_NAMES.map(
|
|
350
|
+
(name, i) => `${TIER_ICONS[(i + 1) as EffortLevel]} ${name}`,
|
|
351
|
+
).join(", ");
|
|
352
|
+
ctx.ui.notify(
|
|
353
|
+
`❌ Unknown tier "${arg}". Valid tiers: ${valid}`,
|
|
354
|
+
"error",
|
|
355
|
+
);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Preserve cap state on switch
|
|
360
|
+
const prev = sharedState.effort;
|
|
361
|
+
const capped = prev?.capped ?? false;
|
|
362
|
+
const capLevel = prev?.capLevel;
|
|
363
|
+
const state = buildEffortState(level, capped, capLevel);
|
|
364
|
+
|
|
365
|
+
// Resolve extraction tier before writing to sharedState (C1, C2)
|
|
366
|
+
const extractionResolution = resolveExtractionTier(state.extraction, ctx);
|
|
367
|
+
state.resolvedExtractionModelId = extractionResolution.resolvedModelId;
|
|
368
|
+
|
|
369
|
+
// Write to shared state only after all fields are populated
|
|
370
|
+
sharedState.effort = state;
|
|
371
|
+
pi.events.emit(DASHBOARD_UPDATE_EVENT, { source: "effort" });
|
|
372
|
+
|
|
373
|
+
// Switch driver model
|
|
374
|
+
const driverModel = await switchDriverModel(pi, ctx, state.driver);
|
|
375
|
+
if (driverModel) {
|
|
376
|
+
writeLastUsedModel(ctx.cwd, { provider: driverModel.model.provider, modelId: driverModel.model.id });
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Set thinking level
|
|
380
|
+
const effectiveThinking: ThinkingLevel = driverModel?.maxThinking
|
|
381
|
+
? clampThinkingLevel(state.thinking, driverModel.maxThinking)
|
|
382
|
+
: state.thinking;
|
|
383
|
+
pi.setThinkingLevel(effectiveThinking as any);
|
|
384
|
+
|
|
385
|
+
const icon = TIER_ICONS[state.level];
|
|
386
|
+
const modelNote = driverModel
|
|
387
|
+
? ` → ${driverModel.model.provider}/${driverModel.model.id}`
|
|
388
|
+
: " (driver model unavailable)";
|
|
389
|
+
ctx.ui.notify(
|
|
390
|
+
`${icon} Switched to ${state.name} (${state.driver}/${effectiveThinking})${modelNote}`,
|
|
391
|
+
driverModel ? "info" : "warning",
|
|
392
|
+
);
|
|
393
|
+
},
|
|
394
|
+
});
|
|
395
|
+
}
|