@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,507 @@
1
+ /**
2
+ * Agent Modes Extension
3
+ *
4
+ * Makes the agent-cortex agents (ralph, ralph-plan, strategy) available as
5
+ * switchable PI modes. Each mode:
6
+ * - Injects the agent's full system prompt into the running context
7
+ * - Restricts tools based on the agent's declared tool set
8
+ * - Shows a status indicator with the active agent name
9
+ *
10
+ * Modeled on Pi's built-in preset.ts example and R-Dson/pi-modes.
11
+ *
12
+ * Commands:
13
+ * /agent — show mode selector
14
+ * /agent <name> — switch to agent mode directly
15
+ * Ctrl+Shift+A — cycle agents
16
+ * --agent <name> — start in a specific agent mode
17
+ */
18
+
19
+ import { readFileSync, readdirSync, statSync, existsSync } from "node:fs";
20
+ import { join, dirname } from "node:path";
21
+ import { fileURLToPath } from "node:url";
22
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
23
+ import { Key } from "@earendil-works/pi-tui";
24
+
25
+ // ─── Types ───────────────────────────────────────────────────────────────────
26
+
27
+ interface AgentDef {
28
+ /** Lowercase id derived from filename (e.g. "ralph", "ralph-plan") */
29
+ id: string;
30
+ /** Display name from frontmatter or prettified id */
31
+ name: string;
32
+ /** Description from frontmatter */
33
+ description: string;
34
+ /** Colour for status indicator */
35
+ color: string;
36
+ /** PI tool names to enable (empty = all tools) */
37
+ tools: string[];
38
+ /** Full agent body (the system prompt to inject) */
39
+ prompt: string;
40
+ }
41
+
42
+ interface OriginalState {
43
+ tools: string[];
44
+ }
45
+
46
+ // ─── Discovery ───────────────────────────────────────────────────────────────
47
+
48
+ const __dirname = dirname(fileURLToPath(import.meta.url));
49
+ // Agents dir is <extension-root>/../../agents/ — two levels up from extensions/agent-modes/
50
+ const AGENTS_DIR = join(__dirname, "..", "..", "agents");
51
+
52
+ /**
53
+ * Copilot CLI tool names to PI built-in tool names.
54
+ * Tools not in this map are left as-is (they may be custom tools).
55
+ */
56
+ const COPILOT_TO_PI: Record<string, string[]> = {
57
+ bash: ["bash"],
58
+ view: ["read"],
59
+ read: ["read"],
60
+ edit: ["edit"],
61
+ create: ["write"],
62
+ write: ["write"],
63
+ grep: ["grep"],
64
+ rg: ["grep", "find"],
65
+ glob: ["find", "ls"],
66
+ find: ["find"],
67
+ ls: ["ls"],
68
+ };
69
+
70
+ /**
71
+ * Mapping from agent id → colour for the status indicator.
72
+ */
73
+ const AGENT_COLORS: Record<string, string> = {
74
+ ralph: "accent",
75
+ "ralph-plan": "warning",
76
+ strategy: "accent",
77
+ };
78
+
79
+ const PERSIST_KEY = "agent-modes-state";
80
+
81
+ /** Modes with no tool restriction gets the full default set. */
82
+ const DEFAULT_TOOLS = ["read", "bash", "edit", "write", "grep", "find", "ls"];
83
+
84
+ // ─── Agent file parsing ──────────────────────────────────────────────────────
85
+
86
+ function parseFrontmatter(raw: string): {
87
+ name?: string;
88
+ description?: string;
89
+ tools?: string[];
90
+ prompt: string;
91
+ } {
92
+ const lines = raw.split(/\r?\n/);
93
+ if (lines[0] !== "---") return { prompt: raw };
94
+
95
+ // Find closing ---
96
+ let sep = -1;
97
+ for (let i = 1; i < lines.length; i++) {
98
+ if (lines[i].trim() === "---") {
99
+ sep = i;
100
+ break;
101
+ }
102
+ }
103
+ if (sep === -1) return { prompt: raw };
104
+
105
+ const yamlLines = lines.slice(1, sep);
106
+ const prompt = lines.slice(sep + 1).join("\n").trim();
107
+
108
+ let name: string | undefined;
109
+ let description: string | undefined;
110
+ let tools: string[] | undefined;
111
+
112
+ for (const line of yamlLines) {
113
+ const trimmed = line.trim();
114
+ const colonIdx = trimmed.indexOf(":");
115
+ if (colonIdx === -1) continue;
116
+
117
+ const key = trimmed.slice(0, colonIdx).trim().toLowerCase();
118
+ const val = trimmed.slice(colonIdx + 1).trim();
119
+
120
+ if (key === "name") {
121
+ // Strip surrounding quotes
122
+ name = val.replace(/^["']|["']$/g, "");
123
+ } else if (key === "description") {
124
+ description = val.replace(/^["']|["']$/g, "");
125
+ }
126
+ }
127
+
128
+ // Parse tools from the tools frontmatter.
129
+ // Supports two formats:
130
+ // tools: ["bash", "view"] (JSON array, single line)
131
+ // tools: (YAML list, subsequent lines)
132
+ // - bash
133
+ // - view
134
+ let inToolsBlock = false;
135
+ for (const line of yamlLines) {
136
+ const trimmed = line.trim();
137
+
138
+ // Check for inline JSON array: tools: ["foo", "bar"]
139
+ const colonIdx = trimmed.indexOf(":");
140
+ if (colonIdx !== -1) {
141
+ const key = trimmed.slice(0, colonIdx).trim().toLowerCase();
142
+ const val = trimmed.slice(colonIdx + 1).trim();
143
+ if (key === "tools") {
144
+ if (val.startsWith("[")) {
145
+ // Inline JSON array
146
+ try {
147
+ tools = JSON.parse(val);
148
+ } catch {
149
+ // Malformed JSON — fall through
150
+ }
151
+ continue;
152
+ }
153
+ if (val === "[]" || val === "") {
154
+ // Empty tools — explicit
155
+ tools = [];
156
+ continue;
157
+ }
158
+ }
159
+ }
160
+
161
+ // YAML block format: "tools:" on its own line, items on subsequent lines
162
+ if (trimmed === "tools:") {
163
+ inToolsBlock = true;
164
+ if (!tools) tools = [];
165
+ continue;
166
+ }
167
+ if (inToolsBlock) {
168
+ // Check if this line is still part of the tools block (indented or starts with -)
169
+ if (line.startsWith(" ") || line.startsWith("\t") || trimmed.startsWith("- ")) {
170
+ const item = trimmed.replace(/^- /, "").trim().replace(/^["']|["']$/g, "");
171
+ if (item) {
172
+ if (!tools) tools = [];
173
+ tools.push(item);
174
+ }
175
+ } else if (trimmed !== "") {
176
+ // Non-empty, non-indented line → tools block ended
177
+ inToolsBlock = false;
178
+ }
179
+ }
180
+ }
181
+
182
+ return { name, description, tools, prompt };
183
+ }
184
+
185
+ function mapCopilotToolsToPi(copilotTools: string[] | undefined): string[] | undefined {
186
+ if (!copilotTools || copilotTools.length === 0) return undefined;
187
+
188
+ const piTools = new Set<string>();
189
+ for (const ct of copilotTools) {
190
+ const mapped = COPILOT_TO_PI[ct];
191
+ if (mapped) {
192
+ for (const t of mapped) piTools.add(t);
193
+ } else {
194
+ // Pass through unknown tool names (could be custom tools)
195
+ piTools.add(ct);
196
+ }
197
+ }
198
+ return [...piTools].sort();
199
+ }
200
+
201
+ function discoverAgents(): AgentDef[] {
202
+ if (!existsSync(AGENTS_DIR)) {
203
+ console.warn(`[agent-modes] Agents directory not found: ${AGENTS_DIR}`);
204
+ return [];
205
+ }
206
+
207
+ const files = readdirSync(AGENTS_DIR)
208
+ .filter((f) => f.endsWith(".agent.md"))
209
+ .sort();
210
+
211
+ const agents: AgentDef[] = [];
212
+
213
+ for (const file of files) {
214
+ const filePath = join(AGENTS_DIR, file);
215
+ if (!statSync(filePath).isFile()) continue;
216
+
217
+ let raw: string;
218
+ try {
219
+ raw = readFileSync(filePath, "utf-8");
220
+ } catch (err) {
221
+ console.warn(`[agent-modes] Failed to read ${file}: ${err}`);
222
+ continue;
223
+ }
224
+
225
+ const parsed = parseFrontmatter(raw);
226
+ const id = file.replace(/\.agent\.md$/i, "").toLowerCase();
227
+ const name = parsed.name || prettifyId(id);
228
+ const tools = mapCopilotToolsToPi(parsed.tools);
229
+ const color = AGENT_COLORS[id] || "accent";
230
+
231
+ agents.push({
232
+ id,
233
+ name,
234
+ description: parsed.description || "",
235
+ color,
236
+ tools: tools || [...DEFAULT_TOOLS],
237
+ prompt: parsed.prompt || `You are ${name}. ${parsed.description || ""}`,
238
+ });
239
+ }
240
+
241
+ return agents;
242
+ }
243
+
244
+ function prettifyId(id: string): string {
245
+ return id
246
+ .split("-")
247
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
248
+ .join(" ");
249
+ }
250
+
251
+ // ─── Extension ───────────────────────────────────────────────────────────────
252
+
253
+ export default function agentModesExtension(pi: ExtensionAPI): void {
254
+ const agents = discoverAgents();
255
+
256
+ if (agents.length === 0) {
257
+ console.warn("[agent-modes] No agents found. Extension disabled.");
258
+ return;
259
+ }
260
+
261
+ let activeAgentIndex = agents.length; // default mode is beyond the last agent
262
+ let originalState: OriginalState | undefined;
263
+ let baselineTools: string[] = [];
264
+ let registeredCommands = false;
265
+
266
+ // ── Helpers ──────────────────────────────────────────────────────────────
267
+
268
+ function getActiveModeLabel(): string {
269
+ if (activeAgentIndex >= agents.length || activeAgentIndex < 0) return "default";
270
+ return agents[activeAgentIndex].name;
271
+ }
272
+
273
+ function getActiveColor(): string {
274
+ if (activeAgentIndex >= agents.length || activeAgentIndex < 0) return "accent";
275
+ return agents[activeAgentIndex].color;
276
+ }
277
+
278
+ function isDefaultMode(): boolean {
279
+ return activeAgentIndex >= agents.length || activeAgentIndex < 0;
280
+ }
281
+
282
+ function getActiveAgent(): AgentDef | undefined {
283
+ if (activeAgentIndex >= agents.length || activeAgentIndex < 0) return undefined;
284
+ return agents[activeAgentIndex];
285
+ }
286
+
287
+ function setAgentMode(ctx: ExtensionContext, index: number): boolean {
288
+ if (index < 0) index = agents.length; // default mode
289
+ if (index > agents.length) return false;
290
+
291
+ // Snapshot before first switch
292
+ if (originalState === undefined) {
293
+ originalState = { tools: [...baselineTools] };
294
+ }
295
+
296
+ if (index >= agents.length) {
297
+ // Default mode: restore baseline tools
298
+ activeAgentIndex = agents.length;
299
+ try {
300
+ if (originalState) {
301
+ pi.setActiveTools(originalState.tools);
302
+ } else if (baselineTools.length > 0) {
303
+ pi.setActiveTools(baselineTools);
304
+ }
305
+ } catch (err) {
306
+ console.warn(`[agent-modes] Failed to restore tools: ${err}`);
307
+ return false;
308
+ }
309
+ } else {
310
+ // Agent mode: set agent-specific tools
311
+ const agent = agents[index];
312
+ activeAgentIndex = index;
313
+ try {
314
+ pi.setActiveTools(agent.tools);
315
+ } catch (err) {
316
+ console.warn(`[agent-modes] Failed to set tools for ${agent.id}: ${err}`);
317
+ return false;
318
+ }
319
+ }
320
+
321
+ updateUI(ctx);
322
+ return true;
323
+ }
324
+
325
+ function updateUI(ctx: ExtensionContext): void {
326
+ const label = getActiveModeLabel();
327
+ if (label === "default") {
328
+ ctx.ui.setStatus("agent-mode", undefined);
329
+ } else {
330
+ ctx.ui.setStatus("agent-mode", ctx.ui.theme.fg(getActiveColor(), `agent:${label}`));
331
+ }
332
+ }
333
+
334
+ function persistState(): void {
335
+ try {
336
+ pi.appendEntry(PERSIST_KEY, {
337
+ agentIndex: activeAgentIndex,
338
+ agentId: activeAgentIndex < agents.length ? agents[activeAgentIndex]?.id : undefined,
339
+ });
340
+ } catch {
341
+ // Non-critical
342
+ }
343
+ }
344
+
345
+ // ── Register commands & shortcuts (once) ────────────────────────────────
346
+
347
+ function registerCommands(): void {
348
+ if (registeredCommands) return;
349
+ registeredCommands = true;
350
+
351
+ // /agent command
352
+ pi.registerCommand("agent", {
353
+ description: `Switch agent mode (${agents.map((a) => a.id).join(", ")})`,
354
+ handler: async (args, ctx) => {
355
+ if (!args?.trim()) {
356
+ await showAgentSelector(ctx);
357
+ return;
358
+ }
359
+
360
+ const input = args.trim().toLowerCase();
361
+
362
+ // "default" returns to default mode
363
+ if (input === "default" || input === "none") {
364
+ if (setAgentMode(ctx, agents.length)) {
365
+ persistState();
366
+ ctx.ui.notify("Switched to default mode (full access)", "info");
367
+ }
368
+ return;
369
+ }
370
+
371
+ const index = agents.findIndex((a) => a.id === input);
372
+ if (index === -1) {
373
+ ctx.ui.notify(
374
+ `Unknown agent "${input}". Available: ${agents.map((a) => a.id).join(", ")}`,
375
+ "error",
376
+ );
377
+ return;
378
+ }
379
+
380
+ if (setAgentMode(ctx, index)) {
381
+ persistState();
382
+ ctx.ui.notify(`Switched to agent: ${agents[index].name}`, "info");
383
+ }
384
+ },
385
+ });
386
+
387
+ // Ctrl+Shift+A to cycle agents (A = Agent)
388
+ pi.registerShortcut(Key.ctrlShift("a"), {
389
+ description: "Next agent mode",
390
+ handler: async (ctx) => {
391
+ const next = (activeAgentIndex + 1) % (agents.length + 1);
392
+ if (setAgentMode(ctx, next)) {
393
+ persistState();
394
+ const label = getActiveModeLabel();
395
+ ctx.ui.notify(`Mode: ${label}`, "info");
396
+ }
397
+ },
398
+ });
399
+
400
+ // --agent CLI flag
401
+ pi.registerFlag("agent", {
402
+ description: `Start in a specific agent mode (${agents.map((a) => a.id).join(", ")})`,
403
+ type: "string",
404
+ });
405
+
406
+ // inject agent prompt before each agent start
407
+ pi.on("before_agent_start", async (event) => {
408
+ const agent = getActiveAgent();
409
+ if (!agent || !agent.prompt) return;
410
+
411
+ return {
412
+ systemPrompt: `${event.systemPrompt}\n\n---\n${agent.prompt}`,
413
+ message: {
414
+ customType: "agent-mode-context",
415
+ content: `[AGENT MODE: ${agent.name.toUpperCase()}]\n${agent.description || "No description"}`,
416
+ display: false,
417
+ },
418
+ };
419
+ });
420
+ }
421
+
422
+ // ── Agent selector UI ────────────────────────────────────────────────────
423
+
424
+ async function showAgentSelector(ctx: ExtensionContext): Promise<void> {
425
+ const choices: string[] = [
426
+ "(default) — Full tool access, no agent prompt",
427
+ ...agents.map((agent) => {
428
+ const active = activeAgentIndex >= 0 && agents[activeAgentIndex]?.id === agent.id
429
+ ? " (active)" : "";
430
+ return `${agent.name}${active} — ${agent.description.slice(0, 60)}`;
431
+ }),
432
+ ];
433
+
434
+ const result = await ctx.ui.select("Select agent mode:", choices);
435
+ if (!result) return;
436
+
437
+ if (result.startsWith("(default)")) {
438
+ if (setAgentMode(ctx, agents.length)) {
439
+ persistState();
440
+ ctx.ui.notify("Switched to default mode (full access)", "info");
441
+ }
442
+ return;
443
+ }
444
+
445
+ // Match the agent name from the choice label
446
+ const matchedAgent = agents.find((a) => result.startsWith(a.name));
447
+ if (matchedAgent) {
448
+ const index = agents.indexOf(matchedAgent);
449
+ if (setAgentMode(ctx, index)) {
450
+ persistState();
451
+ ctx.ui.notify(`Switched to agent: ${matchedAgent.name}`, "info");
452
+ }
453
+ }
454
+ }
455
+
456
+ // ── Bootstrap ────────────────────────────────────────────────────────────
457
+
458
+ pi.on("session_start", async (_event, ctx) => {
459
+ // Capture baseline tools once all extensions are loaded
460
+ try {
461
+ baselineTools = pi.getActiveTools();
462
+ } catch {
463
+ baselineTools = [...DEFAULT_TOOLS];
464
+ }
465
+
466
+ registerCommands();
467
+
468
+ // Restore persisted state
469
+ const entries = ctx.sessionManager.getEntries();
470
+ let restoredIndex: number | undefined;
471
+
472
+ for (let i = entries.length - 1; i >= 0; i--) {
473
+ const entry = entries[i] as { type: string; customType?: string; data?: { agentIndex?: number } };
474
+ if (entry.type === "custom" && entry.customType === PERSIST_KEY) {
475
+ if (typeof entry.data?.agentIndex === "number") {
476
+ restoredIndex = entry.data.agentIndex;
477
+ break;
478
+ }
479
+ }
480
+ }
481
+
482
+ // --agent CLI flag takes priority
483
+ const agentFlag = pi.getFlag("agent");
484
+ let targetIndex: number;
485
+
486
+ if (typeof agentFlag === "string" && agentFlag) {
487
+ const flagIdx = agents.findIndex((a) => a.id === agentFlag.toLowerCase());
488
+ if (flagIdx !== -1) {
489
+ targetIndex = flagIdx;
490
+ } else {
491
+ console.warn(`[agent-modes] Unknown --agent "${agentFlag}".`);
492
+ targetIndex = restoredIndex ?? agents.length;
493
+ }
494
+ } else if (restoredIndex !== undefined) {
495
+ targetIndex = restoredIndex;
496
+ } else {
497
+ targetIndex = agents.length; // default mode
498
+ }
499
+
500
+ if (setAgentMode(ctx, targetIndex)) {
501
+ const label = getActiveModeLabel();
502
+ if (label !== "default") {
503
+ ctx.ui.notify(`Agent: ${label}`, "info");
504
+ }
505
+ }
506
+ });
507
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Auto-name sessions using a tiny side-agent LLM.
3
+ *
4
+ * On the first user prompt of a new session, calls a cheap configured model
5
+ * (default: opencode/mimo-v2.5-free) to generate a concise session title,
6
+ * then sets it via pi.setSessionName(). Falls back to simple truncation if
7
+ * the LLM call fails.
8
+ *
9
+ * Config (in settings.json under `tinyModel`):
10
+ * ```json
11
+ * {
12
+ * "tinyModel": {
13
+ * "model": "opencode/mimo-v2.5-free",
14
+ * "maxNameLength": 50
15
+ * }
16
+ * }
17
+ * ```
18
+ *
19
+ * Existing names (set via --name or /name) are never overwritten.
20
+ */
21
+
22
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
23
+ import {
24
+ loadTinyModelConfig,
25
+ parseModelString,
26
+ generateNameFromPrompt,
27
+ } from "./lib/tiny-model.js";
28
+ import type { TinyModelConfig } from "./lib/tiny-model.js";
29
+
30
+ export default function (pi: ExtensionAPI) {
31
+ let config: TinyModelConfig;
32
+ let hasNamed = false;
33
+
34
+ pi.on("session_start", async (_event, ctx) => {
35
+ config = loadTinyModelConfig(ctx.cwd);
36
+ hasNamed = false;
37
+
38
+ // If session already has a name (resume, fork, or --name), skip
39
+ if (pi.getSessionName()) {
40
+ hasNamed = true;
41
+ }
42
+ });
43
+
44
+ pi.on("input", async (event, ctx) => {
45
+ if (!config.enabled || hasNamed) return;
46
+ if (event.source === "rpc") return;
47
+ if (pi.getSessionName()) {
48
+ hasNamed = true;
49
+ return;
50
+ }
51
+ if (!event.text?.trim()) return;
52
+
53
+ hasNamed = true;
54
+
55
+ // Fire the LLM call asynchronously — don't block the agent
56
+ (async () => {
57
+ try {
58
+ const { provider, id } = parseModelString(config.model);
59
+ const model = ctx.modelRegistry.find(provider, id);
60
+ if (!model) {
61
+ // Model not found; fall back to truncation
62
+ fallbackName(event.text, config.maxNameLength, pi);
63
+ return;
64
+ }
65
+
66
+ const { apiKey, headers } =
67
+ await ctx.modelRegistry.getApiKeyAndHeaders(model);
68
+
69
+ const name = await generateNameFromPrompt(
70
+ model,
71
+ apiKey,
72
+ headers,
73
+ event.text,
74
+ config.maxNameLength,
75
+ );
76
+
77
+ if (name) {
78
+ pi.setSessionName(name);
79
+ } else {
80
+ fallbackName(event.text, config.maxNameLength, pi);
81
+ }
82
+ } catch (err) {
83
+ console.warn("[auto-name] LLM call failed, falling back:", err);
84
+ fallbackName(event.text, config.maxNameLength, pi);
85
+ }
86
+ })();
87
+ });
88
+ }
89
+
90
+ /**
91
+ * Fallback: truncate the first line of the prompt to use as a session name.
92
+ */
93
+ function fallbackName(
94
+ text: string,
95
+ maxLength: number,
96
+ pi: ExtensionAPI,
97
+ ): void {
98
+ const firstLine = text.split("\n")[0]?.trim() ?? "";
99
+ if (!firstLine) return;
100
+
101
+ const name =
102
+ firstLine.length > maxLength
103
+ ? firstLine.slice(0, maxLength - 1) + "\u2026"
104
+ : firstLine;
105
+
106
+ pi.setSessionName(name);
107
+ }
@@ -0,0 +1,35 @@
1
+ # context-loader
2
+
3
+ Utility module for loading project context files at session start.
4
+
5
+ ## Purpose
6
+
7
+ Reads context files (AGENTS.md, user-preferences.md) from the project root and
8
+ formats them for injection into the agent session. This gives the agent baseline
9
+ project context without requiring manual file reads.
10
+
11
+ ## Usage
12
+
13
+ ```typescript
14
+ import { loadSessionContext } from "./context-loader.js";
15
+
16
+ // Load default context files from project root
17
+ const context = loadSessionContext(cwd);
18
+
19
+ // Load custom file list
20
+ const context = loadSessionContext(cwd, ["AGENTS.md", "docs/custom.md"]);
21
+ ```
22
+
23
+ ## Default Context Files
24
+
25
+ - `AGENTS.md` — Project instructions for agents
26
+ - `docs/user-preferences.md` — User tooling preferences
27
+
28
+ ## Testing
29
+
30
+ Tests are in `extensions/skill-stats/context-loader.test.ts`:
31
+
32
+ ```bash
33
+ cd extensions/skill-stats
34
+ node --test --import tsx/esm context-loader.test.ts
35
+ ```
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Context file loader for session-start extension.
3
+ *
4
+ * Reads project context files (AGENTS.md, user-preferences.md, etc.) and
5
+ * formats them for injection into the agent session at startup.
6
+ */
7
+
8
+ import fs from "node:fs";
9
+ import path from "node:path";
10
+
11
+ export interface ContextFile {
12
+ path: string;
13
+ content: string;
14
+ }
15
+
16
+ /**
17
+ * Default context files to load at session start.
18
+ * Paths are relative to the project root (cwd).
19
+ */
20
+ export const DEFAULT_CONTEXT_FILES = [
21
+ "AGENTS.md",
22
+ "docs/user-preferences.md",
23
+ ];
24
+
25
+ /**
26
+ * Load a single context file. Returns null if the file doesn't exist or is empty.
27
+ */
28
+ export function loadContextFile(filePath: string): ContextFile | null {
29
+ if (!filePath) return null;
30
+
31
+ try {
32
+ const content = fs.readFileSync(filePath, "utf-8").trim();
33
+ if (!content) return null;
34
+ return { path: filePath, content };
35
+ } catch {
36
+ return null;
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Load all context files from the given paths (relative to cwd).
42
+ * Returns a formatted string ready for injection into the session.
43
+ */
44
+ export function loadSessionContext(
45
+ cwd: string,
46
+ filePaths?: string[],
47
+ ): string {
48
+ const files = filePaths ?? DEFAULT_CONTEXT_FILES;
49
+ const loaded: ContextFile[] = [];
50
+
51
+ for (const relPath of files) {
52
+ const absPath = path.resolve(cwd, relPath);
53
+ const file = loadContextFile(absPath);
54
+ if (file) loaded.push(file);
55
+ }
56
+
57
+ if (loaded.length === 0) return "";
58
+
59
+ return loaded
60
+ .map((f) => {
61
+ const name = path.basename(f.path);
62
+ return `--- ${name} ---\n${f.content}`;
63
+ })
64
+ .join("\n\n");
65
+ }