@jaybeeuu/agent-cortex 1.20.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 (154) hide show
  1. package/.beads/README.md +81 -0
  2. package/.beads/config.yaml +1 -0
  3. package/.beads/issues.jsonl +236 -0
  4. package/.beads/metadata.json +7 -0
  5. package/.claude-plugin/marketplace.json +16 -0
  6. package/.github/workflows/ci.test.ts +217 -0
  7. package/.github/workflows/ci.yml +35 -0
  8. package/.github/workflows/publish.yml +36 -0
  9. package/.junie/guidelines.md +87 -0
  10. package/.junie/mcp/mcp.json +10 -0
  11. package/AGENTS.md +102 -0
  12. package/CHANGELOG.md +705 -0
  13. package/README.md +209 -0
  14. package/agents/README.md +235 -0
  15. package/agents/plan.agent.md +56 -0
  16. package/agents/ralph-plan.agent.md +119 -0
  17. package/agents/ralph.agent.md +305 -0
  18. package/agents/strategy.agent.md +109 -0
  19. package/agents-native/ralph.md +193 -0
  20. package/bin/agent-cortex.mjs +27 -0
  21. package/claude/.claude-plugin/plugin.json +11 -0
  22. package/claude/.mcp.json +18 -0
  23. package/claude/agents/plan.md +56 -0
  24. package/claude/agents/ralph-plan.md +119 -0
  25. package/claude/agents/ralph.md +193 -0
  26. package/claude/agents/strategy.md +109 -0
  27. package/claude/hooks.json +23 -0
  28. package/claude/scripts/statusline-command.sh +124 -0
  29. package/docs/ideas/auto-model-selection.md +75 -0
  30. package/docs/ideas/improve-pi-system-prompt.md +34 -0
  31. package/docs/ideas/model-selector.md +40 -0
  32. package/docs/ideas/pi-task-memory.md +34 -0
  33. package/docs/ideas/qa-extension.md +43 -0
  34. package/docs/inspirations.md +33 -0
  35. package/docs/research/gas-city-research.md +256 -0
  36. package/docs/research/skill-design-research.md +286 -0
  37. package/docs/research/skill-improvements-analysis.md +117 -0
  38. package/docs/research/skill-patterns-research.md +24 -0
  39. package/docs/skills/skill-anatomy.md +149 -0
  40. package/docs/user-preferences.md +17 -0
  41. package/extensions/agent-modes/index.ts +507 -0
  42. package/extensions/auto-name.ts +107 -0
  43. package/extensions/context-loader.md +35 -0
  44. package/extensions/context-loader.ts +65 -0
  45. package/extensions/lib/tiny-model.ts +212 -0
  46. package/extensions/notify/index.ts +293 -0
  47. package/extensions/session-start.ts +44 -0
  48. package/extensions/skill-stats/README.md +74 -0
  49. package/extensions/skill-stats/context-loader.test.ts +107 -0
  50. package/extensions/skill-stats/discover.ts +119 -0
  51. package/extensions/skill-stats/format.ts +13 -0
  52. package/extensions/skill-stats/index.test.ts +452 -0
  53. package/extensions/skill-stats/index.ts +222 -0
  54. package/extensions/skill-stats/package.json +19 -0
  55. package/extensions/skill-stats/pnpm-lock.yaml +329 -0
  56. package/extensions/skill-stats/record.ts +48 -0
  57. package/extensions/skill-stats/store.ts +104 -0
  58. package/extensions/skill-stats/tsconfig.json +12 -0
  59. package/extensions/subagent/README.md +175 -0
  60. package/extensions/subagent/agents/planner.md +37 -0
  61. package/extensions/subagent/agents/reviewer.md +35 -0
  62. package/extensions/subagent/agents/scout.md +50 -0
  63. package/extensions/subagent/agents/worker.md +24 -0
  64. package/extensions/subagent/agents.ts +126 -0
  65. package/extensions/subagent/index.ts +1138 -0
  66. package/extensions/subagent/prompts/implement-and-review.md +10 -0
  67. package/extensions/subagent/prompts/implement.md +10 -0
  68. package/extensions/subagent/prompts/scout-and-plan.md +9 -0
  69. package/hooks.json +10 -0
  70. package/lib/cli.mjs +37 -0
  71. package/package.json +39 -0
  72. package/pi/keybindings.json +3 -0
  73. package/pi/settings.json +29 -0
  74. package/plugin.json +35 -0
  75. package/pnpm-workspace.yaml +7 -0
  76. package/scripts/build-claude-agents.mjs +179 -0
  77. package/scripts/install-instructions.sh +82 -0
  78. package/skills/engineering/design-an-interface/SKILL.md +94 -0
  79. package/skills/engineering/grill-with-docs/SKILL.md +88 -0
  80. package/skills/engineering/improve-codebase-architecture/REFERENCE.md +78 -0
  81. package/skills/engineering/improve-codebase-architecture/SKILL.md +81 -0
  82. package/skills/engineering/tdd/SKILL.md +150 -0
  83. package/skills/engineering/tdd/deep-modules.md +33 -0
  84. package/skills/engineering/tdd/interface-design.md +31 -0
  85. package/skills/engineering/tdd/mocking.md +59 -0
  86. package/skills/engineering/tdd/refactoring.md +10 -0
  87. package/skills/engineering/tdd/tests.md +61 -0
  88. package/skills/engineering/technical-direction/SKILL.md +86 -0
  89. package/skills/planning/classify-bead/SKILL.md +67 -0
  90. package/skills/planning/create-task/SKILL.md +96 -0
  91. package/skills/planning/create-task/pipeline.json +33 -0
  92. package/skills/planning/create-task/scripts/create-chores.ts +112 -0
  93. package/skills/planning/create-task/scripts/package.json +14 -0
  94. package/skills/planning/create-task/scripts/pnpm-lock.yaml +342 -0
  95. package/skills/planning/create-task/scripts/tsconfig.json +12 -0
  96. package/skills/planning/create-task/templates/code.md +40 -0
  97. package/skills/planning/create-task/templates/document.md +40 -0
  98. package/skills/planning/create-task/templates/fix.md +27 -0
  99. package/skills/planning/create-task/templates/review.md +36 -0
  100. package/skills/planning/create-task/templates/verify.md +43 -0
  101. package/skills/planning/prd-to-tasks/SKILL.md +227 -0
  102. package/skills/planning/record-idea/SKILL.md +62 -0
  103. package/skills/planning/record-idea/scripts/new-idea.sh +89 -0
  104. package/skills/planning/request-refactor-plan/SKILL.md +73 -0
  105. package/skills/planning/write-a-prd/SKILL.md +74 -0
  106. package/skills/planning/write-a-ticket/SKILL.md +97 -0
  107. package/skills/productivity/bd-tool/SKILL.md +90 -0
  108. package/skills/productivity/grill-me/SKILL.md +10 -0
  109. package/skills/productivity/handoff/SKILL.md +15 -0
  110. package/skills/productivity/init-beads/SKILL.md +93 -0
  111. package/skills/productivity/teach/GLOSSARY-FORMAT.md +35 -0
  112. package/skills/productivity/teach/LEARNING-RECORD-FORMAT.md +46 -0
  113. package/skills/productivity/teach/MISSION-FORMAT.md +31 -0
  114. package/skills/productivity/teach/RESOURCES-FORMAT.md +32 -0
  115. package/skills/productivity/teach/SKILL.md +109 -0
  116. package/skills/productivity/validate-agent-dir/scripts/package.json +15 -0
  117. package/skills/productivity/validate-agent-dir/scripts/pnpm-lock.yaml +329 -0
  118. package/skills/productivity/validate-agent-dir/scripts/pnpm-workspace.yaml +2 -0
  119. package/skills/productivity/validate-agent-dir/scripts/tsconfig.json +19 -0
  120. package/skills/productivity/validate-agent-dir/scripts/validate-agent-dir.test.ts +317 -0
  121. package/skills/productivity/validate-agent-dir/scripts/validate-agent-dir.ts +127 -0
  122. package/skills/productivity/write-a-skill/SKILL.md +74 -0
  123. package/skills/review/maintain-agent-docs/REFERENCE.md +246 -0
  124. package/skills/review/maintain-agent-docs/SKILL.md +83 -0
  125. package/skills/review/refactor-skill/EXAMPLES.md +20 -0
  126. package/skills/review/refactor-skill/SKILL.md +148 -0
  127. package/skills/review/review-security/SKILL.md +107 -0
  128. package/skills/review/review-security/scripts/install-gitleaks.sh +43 -0
  129. package/skills/style/style-code/REFERENCE.md +38 -0
  130. package/skills/style/style-code/SKILL.md +113 -0
  131. package/skills/style/style-comms/SKILL.md +93 -0
  132. package/skills/style/style-documentation/SKILL.md +112 -0
  133. package/skills/style/style-tests/SKILL.md +97 -0
  134. package/skills/workflow/git-workflow/SKILL.md +85 -0
  135. package/skills/workflow/hitl-collab/SKILL.md +29 -0
  136. package/skills/workflow/plan/SKILL.md +272 -0
  137. package/skills/workflow/ralph/REFERENCE.md +202 -0
  138. package/skills/workflow/ralph/SKILL.md +105 -0
  139. package/skills/workflow/run-pipeline-stage/SKILL.md +151 -0
  140. package/skills/workflow/run-pipeline-stage/playbooks/code.md +17 -0
  141. package/skills/workflow/run-pipeline-stage/playbooks/document.md +18 -0
  142. package/skills/workflow/run-pipeline-stage/playbooks/fix.md +17 -0
  143. package/skills/workflow/run-pipeline-stage/playbooks/review.md +19 -0
  144. package/skills/workflow/run-pipeline-stage/playbooks/verify.md +14 -0
  145. package/skills/workflow/run-pipeline-stage/prompts/stage-runner.md +20 -0
  146. package/skills/workflow/run-pipeline-stage/scripts/generate-progress.test.ts +783 -0
  147. package/skills/workflow/run-pipeline-stage/scripts/generate-progress.ts +586 -0
  148. package/skills/workflow/run-pipeline-stage/scripts/package.json +15 -0
  149. package/skills/workflow/run-pipeline-stage/scripts/pnpm-lock.yaml +342 -0
  150. package/skills/workflow/run-pipeline-stage/scripts/pnpm-workspace.yaml +2 -0
  151. package/skills/workflow/run-pipeline-stage/scripts/tsconfig.json +12 -0
  152. package/skills/workflow/using-agent-skills/SKILL.md +109 -0
  153. package/skills/workflow/using-agent-skills/scripts/validate-skill-structure.test.ts +73 -0
  154. package/test/cli.test.mjs +149 -0
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Shared helpers for making cheap LLM calls from extensions.
3
+ *
4
+ * Reads a `tinyModel` config key from settings.json (global + project merged)
5
+ * and provides functions for session naming and response summarization using
6
+ * the configured model — typically the tiniest free model available.
7
+ *
8
+ * Config (in ~/.pi/agent/settings.json or .pi/settings.json):
9
+ * ```json
10
+ * {
11
+ * "tinyModel": {
12
+ * "model": "opencode/mimo-v2.5-free",
13
+ * "maxNameLength": 50,
14
+ * "maxSummaryLength": 100
15
+ * }
16
+ * }
17
+ * ```
18
+ */
19
+
20
+ import { complete } from "@earendil-works/pi-ai";
21
+ import { homedir } from "node:os";
22
+ import { join } from "node:path";
23
+ import { readFileSync, existsSync } from "node:fs";
24
+ import type { Model, Api } from "@earendil-works/pi-ai";
25
+
26
+ /* ── Types ── */
27
+
28
+ export interface TinyModelConfig {
29
+ /** Provider/id string, e.g. "opencode/mimo-v2.5-free" */
30
+ model: string;
31
+ /** Global on/off toggle */
32
+ enabled: boolean;
33
+ /** Max characters for auto-generated session names */
34
+ maxNameLength: number;
35
+ /** Max characters for response summaries in notifications */
36
+ maxSummaryLength: number;
37
+ }
38
+
39
+ const DEFAULT_CONFIG: TinyModelConfig = {
40
+ model: "opencode/mimo-v2.5-free",
41
+ enabled: true,
42
+ maxNameLength: 40,
43
+ maxSummaryLength: 100,
44
+ };
45
+
46
+ /* ── Config loading ── */
47
+
48
+ /**
49
+ * Load tiny-model config from merged global + project settings.
50
+ *
51
+ * Project .pi/settings.json overrides global ~/.pi/agent/settings.json.
52
+ */
53
+ export function loadTinyModelConfig(cwd?: string): TinyModelConfig {
54
+ const paths = [join(homedir(), ".pi", "agent", "settings.json")];
55
+ if (cwd) paths.push(join(cwd, ".pi", "settings.json"));
56
+
57
+ const merged: Record<string, any> = {};
58
+
59
+ for (const filePath of paths) {
60
+ try {
61
+ if (existsSync(filePath)) {
62
+ const raw = readFileSync(filePath, "utf-8");
63
+ const parsed = JSON.parse(raw);
64
+ if (parsed?.tinyModel && typeof parsed.tinyModel === "object") {
65
+ Object.assign(merged, parsed.tinyModel);
66
+ }
67
+ }
68
+ } catch {
69
+ // skip unreadable or invalid settings files
70
+ }
71
+ }
72
+
73
+ return { ...DEFAULT_CONFIG, ...merged } as TinyModelConfig;
74
+ }
75
+
76
+ /**
77
+ * Parse a "provider/id" string into its parts.
78
+ */
79
+ export function parseModelString(modelStr: string): {
80
+ provider: string;
81
+ id: string;
82
+ } {
83
+ const slash = modelStr.indexOf("/");
84
+ if (slash === -1) return { provider: "opencode", id: modelStr };
85
+ return {
86
+ provider: modelStr.slice(0, slash),
87
+ id: modelStr.slice(slash + 1),
88
+ };
89
+ }
90
+
91
+ /* ── LLM helpers ── */
92
+
93
+ type ModelAny = Model<Api>;
94
+ type CompleteOptions = Record<string, any>;
95
+
96
+ /**
97
+ * Generate a concise session title from the user's first prompt.
98
+ *
99
+ * Calls the configured tiny model with a system prompt tuned for naming.
100
+ * Returns null on failure (caller should fall back to truncation).
101
+ */
102
+ export async function generateNameFromPrompt(
103
+ model: ModelAny,
104
+ apiKey: string | undefined,
105
+ headers: Record<string, string> | undefined,
106
+ prompt: string,
107
+ maxLength: number,
108
+ ): Promise<string | null> {
109
+ const systemPrompt = [
110
+ "You are a session naming assistant. Generate a concise title for a coding session.",
111
+ "",
112
+ "Rules:",
113
+ "- Output ONLY the title, no quotes, no prefix, no explanation",
114
+ `- Maximum ${maxLength} characters`,
115
+ "- Summarize intent, do not copy verbatim",
116
+ "- Keep it short (3\u20135 words), capture the general theme",
117
+ "- If the message mentions files or modules, keep the key names brief",
118
+ ].join("\n");
119
+
120
+ const truncated = prompt.length > 2000 ? prompt.slice(0, 2000) + "..." : prompt;
121
+
122
+ try {
123
+ const options: CompleteOptions = { maxTokens: 64 };
124
+ if (apiKey) options.apiKey = apiKey;
125
+ if (headers) options.headers = headers;
126
+
127
+ const result = await complete(model, {
128
+ systemPrompt,
129
+ messages: [{ role: "user", content: truncated, timestamp: Date.now() }],
130
+ }, options);
131
+
132
+ const text = result.content
133
+ ?.filter((b: any): b is { type: "text"; text: string } => b.type === "text")
134
+ ?.map((b) => b.text)
135
+ ?.join("")
136
+ ?.trim() ?? "";
137
+
138
+ return cleanTitle(text, maxLength);
139
+ } catch {
140
+ return null;
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Summarize an assistant response into a one-line summary.
146
+ *
147
+ * Calls the configured tiny model. Returns null on failure.
148
+ */
149
+ export async function summarizeResponseText(
150
+ model: ModelAny,
151
+ apiKey: string | undefined,
152
+ headers: Record<string, string> | undefined,
153
+ responseText: string,
154
+ maxLength: number,
155
+ ): Promise<string | null> {
156
+ const systemPrompt = [
157
+ "Summarize what the AI assistant did in this coding session response.",
158
+ "Be concise — one short sentence or phrase.",
159
+ `Maximum ${maxLength} characters.`,
160
+ "Focus on what changed or was accomplished.",
161
+ "Output ONLY the summary, no quotes, no prefix, no explanation.",
162
+ ].join("\n");
163
+
164
+ const truncated =
165
+ responseText.length > 2000 ? responseText.slice(0, 2000) + "..." : responseText;
166
+
167
+ try {
168
+ const options: CompleteOptions = { maxTokens: 64 };
169
+ if (apiKey) options.apiKey = apiKey;
170
+ if (headers) options.headers = headers;
171
+
172
+ const result = await complete(model, {
173
+ systemPrompt,
174
+ messages: [{ role: "user", content: truncated, timestamp: Date.now() }],
175
+ }, options);
176
+
177
+ const text = result.content
178
+ ?.filter((b: any): b is { type: "text"; text: string } => b.type === "text")
179
+ ?.map((b) => b.text)
180
+ ?.join("")
181
+ ?.trim() ?? "";
182
+
183
+ return text || null;
184
+ } catch {
185
+ return null;
186
+ }
187
+ }
188
+
189
+ /* ── Internal helpers ── */
190
+
191
+ function cleanTitle(raw: string, maxLength: number): string | null {
192
+ let name = raw.trim();
193
+ if (!name) return null;
194
+
195
+ // Strip surrounding quotes if present
196
+ if (
197
+ (name.startsWith('"') && name.endsWith('"')) ||
198
+ (name.startsWith("'") && name.endsWith("'"))
199
+ ) {
200
+ name = name.slice(1, -1);
201
+ }
202
+
203
+ // Collapse newlines
204
+ name = name.replace(/\n/g, " ").trim();
205
+
206
+ // Truncate
207
+ if (name.length > maxLength) {
208
+ name = name.slice(0, maxLength - 3) + "...";
209
+ }
210
+
211
+ return name || null;
212
+ }
@@ -0,0 +1,293 @@
1
+ /**
2
+ * Desktop notification + terminal bell on task completion.
3
+ *
4
+ * Fires after multi-turn or long-running tasks (>1 turn or >30s) with a
5
+ * multiline notification showing:
6
+ * Line 1: working directory path
7
+ * Line 2: session name (if set)
8
+ * Line 3: LLM-generated summary of what the agent did
9
+ *
10
+ * The notification title is the tmux session:window.pane identifier (when
11
+ * available) or "pi".
12
+ *
13
+ * Response summarization uses the tiny model configured in settings.json
14
+ * under the `tinyModel` key.
15
+ */
16
+
17
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
18
+ import type { AgentMessage } from "@earendil-works/pi-agent-core";
19
+ import { execSync } from "node:child_process";
20
+ import { homedir } from "node:os";
21
+
22
+ import {
23
+ loadTinyModelConfig,
24
+ parseModelString,
25
+ summarizeResponseText,
26
+ } from "../lib/tiny-model.js";
27
+ import type { TinyModelConfig } from "../lib/tiny-model.js";
28
+
29
+ /* ── Context helpers ── */
30
+
31
+ /**
32
+ * Get the tmux session:window.pane identifier, or undefined if not in tmux.
33
+ */
34
+ function getTmuxLabel(): string | undefined {
35
+ if (!process.env.TMUX) return undefined;
36
+ try {
37
+ return execSync(
38
+ 'tmux display-message -p "#{session_name}:#{window_index}.#{pane_index}"',
39
+ { encoding: "utf-8", timeout: 2000 },
40
+ ).trim();
41
+ } catch {
42
+ return undefined;
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Format a working directory path for display.
48
+ *
49
+ * Replaces $HOME with ~ and truncates from the left to ~40 chars when the
50
+ * path is too long, keeping the trailing directory name visible.
51
+ */
52
+ function formatTruncatedPath(cwd: string): string {
53
+ const home = homedir();
54
+ let display = cwd;
55
+ if (display.startsWith(home)) {
56
+ display = "~" + display.slice(home.length);
57
+ }
58
+
59
+ const maxLen = 40;
60
+ if (display.length <= maxLen) return display;
61
+
62
+ const head = "~\u2026/";
63
+ const tailLen = maxLen - head.length;
64
+ if (tailLen <= 0) return display.slice(-maxLen);
65
+
66
+ return head + display.slice(-tailLen);
67
+ }
68
+
69
+ /**
70
+ * Extract plain text from the last assistant response for summarization.
71
+ *
72
+ * Returns the concatenated text content of the last assistant message,
73
+ * or empty string if none found.
74
+ */
75
+ function extractAssistantText(messages: AgentMessage[]): string {
76
+ for (let i = messages.length - 1; i >= 0; i--) {
77
+ const msg = messages[i];
78
+ if (msg.role !== "assistant") continue;
79
+
80
+ const texts = msg.content
81
+ .filter(
82
+ (b): b is { type: "text"; text: string } => b.type === "text",
83
+ )
84
+ .map((b) => b.text)
85
+ .filter(Boolean);
86
+
87
+ if (texts.length === 0) continue;
88
+
89
+ return texts.join(" ").trim();
90
+ }
91
+ return "";
92
+ }
93
+
94
+ /* ── Desktop notification helpers (OSC 777 / 99 / 9) ── */
95
+
96
+ function wrapForTmux(sequence: string): string {
97
+ if (!process.env.TMUX) return sequence;
98
+ const escaped = sequence.split("\x1b").join("\x1b\x1b");
99
+ return `\x1bPtmux;${escaped}\x1b\\`;
100
+ }
101
+
102
+ function notifyOSC777(title: string, body: string): void {
103
+ process.stdout.write(wrapForTmux(`\x1b]777;notify;${title};${body}\x07`));
104
+ }
105
+
106
+ function notifyOSC9(message: string): void {
107
+ process.stdout.write(wrapForTmux(`\x1b]9;${message}\x07`));
108
+ }
109
+
110
+ function notifyOSC99(title: string, body: string): void {
111
+ // d=1 = persistent notification (stays until dismissed)
112
+ process.stdout.write(wrapForTmux(`\x1b]99;i=1:d=1;${title}\x1b\\`));
113
+ process.stdout.write(wrapForTmux(`\x1b]99;i=1:p=body;${body}\x1b\\`));
114
+ }
115
+
116
+ function sendDesktopNotification(title: string, sections: string[]): void {
117
+ const isIterm2 =
118
+ process.env.TERM_PROGRAM === "iTerm.app" ||
119
+ Boolean(process.env.ITERM_SESSION_ID);
120
+
121
+ // Flatten sections into a single notification body for non-Windows paths
122
+ const body = sections.join(" \u00b7 ");
123
+
124
+ if (process.env.KITTY_WINDOW_ID) {
125
+ notifyOSC99(title, body);
126
+ } else if (isIterm2) {
127
+ notifyOSC9(`${title}: ${body}`);
128
+ } else if (process.env.WT_SESSION || process.platform === 'win32') {
129
+ // Windows Terminal / Windows — PowerShell toast via execFileSync
130
+ const { execFileSync } = require("node:child_process");
131
+ const script = buildWindowsToastScript(title, sections);
132
+ try {
133
+ execFileSync("powershell.exe", ["-NoProfile", "-Command", script], { timeout: 5000 });
134
+ } catch (e) {
135
+ console.warn("[notify] toast failed:", e.message);
136
+ }
137
+ } else {
138
+ notifyOSC777(title, body);
139
+ }
140
+ }
141
+
142
+ function buildWindowsToastScript(title: string, sections: string[]): string {
143
+ // Escape single quotes for PowerShell single-quoted strings: double them up
144
+ const esc = (s: string) => s.replace(/'/g, "''").replace(/\r/g, '');
145
+ const psTitle = esc(title);
146
+
147
+ // Use ToastText04 for up to 4 text elements (title + 3 body sections)
148
+ const template = [
149
+ `[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] > $null`,
150
+ `$t = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText04)`,
151
+ `$null = $t.SelectSingleNode('/toast').SetAttribute('scenario', 'persistent')`,
152
+ // Title goes to text[0], body sections to text[1-3]
153
+ `$null = $t.SelectSingleNode('//text[@id=1]').AppendChild($t.CreateTextNode('${psTitle}'))`,
154
+ ];
155
+
156
+ for (let i = 0; i < Math.min(sections.length, 3); i++) {
157
+ const escaped = esc(sections[i]);
158
+ template.push(`$null = $t.SelectSingleNode('//text[@id=${i + 2}]').AppendChild($t.CreateTextNode('${escaped}'))`);
159
+ }
160
+
161
+ template.push(`$toast = [Windows.UI.Notifications.ToastNotification]::new($t)`);
162
+ template.push(`[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('pi').Show($toast) > $null`);
163
+
164
+ return template.join("; ");
165
+ }
166
+
167
+ /* ── Bell ── */
168
+
169
+ function ringBell(): void {
170
+ try {
171
+ const { openSync, writeSync, closeSync } = require("node:fs");
172
+ const fd = openSync("/dev/tty", "w");
173
+ writeSync(fd, "\x07");
174
+ closeSync(fd);
175
+ } catch {
176
+ process.stdout.write("\x07");
177
+ }
178
+ }
179
+
180
+ export default function (pi: ExtensionAPI) {
181
+ let agentStartTime = 0;
182
+ let turnCount = 0;
183
+ let config: TinyModelConfig;
184
+
185
+ pi.on("session_start", async (_event, ctx) => {
186
+ config = loadTinyModelConfig(ctx.cwd);
187
+ });
188
+
189
+ pi.on("agent_start", async () => {
190
+ agentStartTime = Date.now();
191
+ turnCount = 0;
192
+ });
193
+
194
+ pi.on("turn_end", async () => {
195
+ turnCount++;
196
+ });
197
+
198
+ pi.on("agent_end", async (event, ctx) => {
199
+ const elapsed = Date.now() - agentStartTime;
200
+
201
+ // Only notify on multi-turn or long tasks — not on quick chat replies
202
+ if (turnCount === 1 && elapsed < 30_000) return;
203
+
204
+ // ── Title: path · tmux identifier (prominent notification header) ──
205
+ const tmuxLabel = getTmuxLabel();
206
+ const truncatedPath = formatTruncatedPath(ctx.cwd);
207
+ const title = tmuxLabel
208
+ ? `${truncatedPath} \u00b7 ${tmuxLabel}`
209
+ : truncatedPath;
210
+
211
+ // ── Body: sections separated by a blank line ──
212
+ const sections: string[] = [];
213
+
214
+ // Section 1: session name (if set)
215
+ const sessionName = pi.getSessionName();
216
+ if (sessionName) sections.push(sessionName);
217
+
218
+ // Section 2: LLM-generated summary of the assistant's response
219
+ const responseText = extractAssistantText(event.messages);
220
+ if (responseText) {
221
+ try {
222
+ const summary = await summarizeResponseWithFallback(
223
+ ctx,
224
+ responseText,
225
+ config,
226
+ );
227
+ sections.push(summary);
228
+ } catch {
229
+ // If summary fails (e.g. config not loaded), just show raw text
230
+ sections.push(responseText.slice(0, config?.maxSummaryLength ?? 100).replace(/\s+/g, ' ').trim());
231
+ }
232
+ }
233
+
234
+ // Desktop notification
235
+ sendDesktopNotification(title, sections);
236
+
237
+ // Terminal bell (tmux catches this via monitor-bell + visual-bell)
238
+ ringBell();
239
+ });
240
+ }
241
+
242
+ /**
243
+ * Summarize response text using the configured tiny model, falling back
244
+ * to simple truncation if the LLM call fails or is disabled.
245
+ */
246
+ async function summarizeResponseWithFallback(
247
+ ctx: any,
248
+ responseText: string,
249
+ config: TinyModelConfig,
250
+ ): Promise<string> {
251
+ if (!config.enabled) {
252
+ return truncateToOneLine(responseText, config.maxSummaryLength);
253
+ }
254
+
255
+ try {
256
+ const { provider, id } = parseModelString(config.model);
257
+ const model = ctx.modelRegistry.find(provider, id);
258
+ if (!model) {
259
+ return truncateToOneLine(responseText, config.maxSummaryLength);
260
+ }
261
+
262
+ const { apiKey, headers } =
263
+ await ctx.modelRegistry.getApiKeyAndHeaders(model);
264
+
265
+ const summary = await summarizeResponseText(
266
+ model,
267
+ apiKey,
268
+ headers,
269
+ responseText,
270
+ config.maxSummaryLength,
271
+ );
272
+
273
+ if (summary) return summary;
274
+ } catch (err) {
275
+ console.warn("[notify] LLM summary failed, falling back:", err);
276
+ }
277
+
278
+ return truncateToOneLine(responseText, config.maxSummaryLength);
279
+ }
280
+
281
+ /**
282
+ * Truncate text to roughly `maxLen` characters on a single line,
283
+ * breaking at a word boundary.
284
+ */
285
+ function truncateToOneLine(text: string, maxLen: number): string {
286
+ const singleLine = text.replace(/\s+/g, " ").trim();
287
+ if (singleLine.length <= maxLen) return singleLine;
288
+
289
+ const breakPoint = singleLine.lastIndexOf(" ", maxLen - 1);
290
+ const truncated =
291
+ breakPoint > 0 ? singleLine.slice(0, breakPoint) : singleLine.slice(0, maxLen);
292
+ return truncated + " \u2026";
293
+ }
@@ -0,0 +1,44 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { loadSessionContext } from "./context-loader.js";
3
+
4
+ /**
5
+ * Session-start extension.
6
+ *
7
+ * Injects baseline context at the start of every PI session:
8
+ * 1. Skill availability hints (so the agent knows they exist)
9
+ * 2. Project context files (AGENTS.md, user-preferences.md)
10
+ *
11
+ * Context files are read dynamically from the project root (cwd).
12
+ */
13
+ export default function (pi: ExtensionAPI) {
14
+ pi.on("before_agent_start", async (_event, ctx) => {
15
+ const skillHints = [
16
+ "Available skills — invoke when relevant (not pre-loaded to save context):",
17
+ "",
18
+ "• /skill:using-agent-skills — meta-skill that routes to the right skill",
19
+ " and enforces core agent behaviors. Check this first if unsure.",
20
+ "",
21
+ "• /skill:bd-tool — project context & task tracking with beads.",
22
+ " Run `bd ready` to see available work.",
23
+ "",
24
+ "• /skill:git-workflow — git discipline: feature branches,",
25
+ " worktrees for solo AFK work, PR workflow, and when to ask.",
26
+ "",
27
+ "Read the SKILL.md for full instructions when needed.",
28
+ ].join("\n");
29
+
30
+ const contextFiles = loadSessionContext(ctx.cwd);
31
+
32
+ const content = contextFiles
33
+ ? `${skillHints}\n\n---\n\n${contextFiles}`
34
+ : skillHints;
35
+
36
+ return {
37
+ message: {
38
+ customType: "session-start",
39
+ content,
40
+ display: false,
41
+ },
42
+ };
43
+ });
44
+ }
@@ -0,0 +1,74 @@
1
+ # skill-stats
2
+
3
+ A PI extension that tracks skill usage across all sessions.
4
+
5
+ ## What it tracks
6
+
7
+ | Signal | Source | What it tells you |
8
+ |---|---|---|
9
+ | **Loaded** | `before_agent_start` → `systemPromptOptions.skills` | Skill was injected into the system prompt (available to the LLM) |
10
+ | **Invoked** | `input` matching `/skill:name` | User explicitly invoked the skill |
11
+ | **Read** | `read` tool call to a SKILL.md | LLM actively loaded the skill's full instructions |
12
+
13
+ ### Misuse detection
14
+
15
+ A skill that is *loaded* frequently but *never read* may be burning context budget without being consulted — a candidate for removal or tightening.
16
+
17
+ ## Data
18
+
19
+ Usage data is stored in `~/.pi/agent-cortex/skill-usage.json` — a single file across all projects, with per-project breakdowns so you can slice by project.
20
+
21
+ ## Installation
22
+
23
+ This extension is bundled with the `agent-cortex` PI package. Install the package for
24
+ auto-discovery:
25
+
26
+ ```bash
27
+ pi install git:https://github.com/jaybeeuu/agent-cortex
28
+ pi config enable extension:skill-stats
29
+ ```
30
+
31
+ After install, `/reload` inside PI (or restart PI). The extension auto-registers.
32
+
33
+ ### Development symlink (alternative)
34
+
35
+ ```bash
36
+ ln -sf $(pwd)/extensions/skill-stats ~/.pi/agent/extensions/skill-stats
37
+ /reload
38
+ ```
39
+
40
+ ## Commands
41
+
42
+ | Command | Description |
43
+ |---|---|
44
+ | `/skill-stats` | Show usage dashboard with per-project slicing |
45
+ | `/skill-stats --project ./relative/path` | Filter to a specific project |
46
+ | `/skill-usage-reset` | Wipe all recorded data |
47
+
48
+ ## Raw data
49
+
50
+ The JSON file at `~/.pi/agent-cortex/skill-usage.json` contains the full record:
51
+
52
+ ```json
53
+ {
54
+ "version": 1,
55
+ "skills": {
56
+ "bd-tool": {
57
+ "loadedCount": 42,
58
+ "invokedCount": 5,
59
+ "readCount": 18,
60
+ "lastUsed": "2026-06-07T...",
61
+ "byProject": {
62
+ "/home/jaybeeuu/src/agent-cortex": {
63
+ "loadedCount": 40,
64
+ "invokedCount": 3,
65
+ "readCount": 16,
66
+ "lastUsed": "2026-06-07T..."
67
+ }
68
+ }
69
+ }
70
+ },
71
+ "totalTurns": 350,
72
+ "lastUpdated": "2026-06-07T..."
73
+ }
74
+ ```