@chankov/agent-skills 0.1.0 → 0.2.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 (164) hide show
  1. package/.pi/extensions/agent-skills-update-check/README.md +58 -0
  2. package/.pi/extensions/agent-skills-update-check/index.ts +161 -0
  3. package/.pi/extensions/agent-skills-update-check/package.json +6 -0
  4. package/.versions/0.2.0/.claude/commands/build.md +18 -0
  5. package/.versions/0.2.0/.claude/commands/code-simplify.md +22 -0
  6. package/.versions/0.2.0/.claude/commands/design-agent.md +14 -0
  7. package/.versions/0.2.0/.claude/commands/doctor.md +13 -0
  8. package/.versions/0.2.0/.claude/commands/plan.md +16 -0
  9. package/.versions/0.2.0/.claude/commands/prime.md +22 -0
  10. package/.versions/0.2.0/.claude/commands/review.md +16 -0
  11. package/.versions/0.2.0/.claude/commands/setup.md +19 -0
  12. package/.versions/0.2.0/.claude/commands/ship.md +17 -0
  13. package/.versions/0.2.0/.claude/commands/spec.md +15 -0
  14. package/.versions/0.2.0/.claude/commands/test.md +19 -0
  15. package/.versions/0.2.0/.opencode/commands/as-build.md +17 -0
  16. package/.versions/0.2.0/.opencode/commands/as-code-simplify.md +16 -0
  17. package/.versions/0.2.0/.opencode/commands/as-design-agent.md +15 -0
  18. package/.versions/0.2.0/.opencode/commands/as-doctor.md +11 -0
  19. package/.versions/0.2.0/.opencode/commands/as-plan.md +16 -0
  20. package/.versions/0.2.0/.opencode/commands/as-prime.md +22 -0
  21. package/.versions/0.2.0/.opencode/commands/as-review.md +15 -0
  22. package/.versions/0.2.0/.opencode/commands/as-setup.md +11 -0
  23. package/.versions/0.2.0/.opencode/commands/as-ship.md +16 -0
  24. package/.versions/0.2.0/.opencode/commands/as-spec.md +16 -0
  25. package/.versions/0.2.0/.opencode/commands/as-test.md +21 -0
  26. package/.versions/0.2.0/.pi/agents/agent-chain.yaml +49 -0
  27. package/.versions/0.2.0/.pi/agents/bowser.md +19 -0
  28. package/.versions/0.2.0/.pi/agents/pi-pi/agent-expert.md +98 -0
  29. package/.versions/0.2.0/.pi/agents/pi-pi/cli-expert.md +41 -0
  30. package/.versions/0.2.0/.pi/agents/pi-pi/config-expert.md +63 -0
  31. package/.versions/0.2.0/.pi/agents/pi-pi/ext-expert.md +43 -0
  32. package/.versions/0.2.0/.pi/agents/pi-pi/keybinding-expert.md +134 -0
  33. package/.versions/0.2.0/.pi/agents/pi-pi/pi-orchestrator.md +57 -0
  34. package/.versions/0.2.0/.pi/agents/pi-pi/prompt-expert.md +70 -0
  35. package/.versions/0.2.0/.pi/agents/pi-pi/skill-expert.md +42 -0
  36. package/.versions/0.2.0/.pi/agents/pi-pi/theme-expert.md +40 -0
  37. package/.versions/0.2.0/.pi/agents/pi-pi/tui-expert.md +85 -0
  38. package/.versions/0.2.0/.pi/agents/teams.yaml +31 -0
  39. package/.versions/0.2.0/.pi/damage-control-rules.yaml +278 -0
  40. package/.versions/0.2.0/.pi/extensions/agent-skills-update-check/README.md +58 -0
  41. package/.versions/0.2.0/.pi/extensions/agent-skills-update-check/index.ts +161 -0
  42. package/.versions/0.2.0/.pi/extensions/agent-skills-update-check/package.json +6 -0
  43. package/.versions/0.2.0/.pi/extensions/chrome-devtools-mcp/README.md +39 -0
  44. package/.versions/0.2.0/.pi/extensions/chrome-devtools-mcp/index.ts +61 -0
  45. package/.versions/0.2.0/.pi/extensions/chrome-devtools-mcp/package.json +6 -0
  46. package/.versions/0.2.0/.pi/extensions/compact-and-continue/README.md +42 -0
  47. package/.versions/0.2.0/.pi/extensions/compact-and-continue/index.ts +120 -0
  48. package/.versions/0.2.0/.pi/extensions/compact-and-continue/package.json +6 -0
  49. package/.versions/0.2.0/.pi/extensions/mcp-bridge/README.md +46 -0
  50. package/.versions/0.2.0/.pi/extensions/mcp-bridge/index.ts +206 -0
  51. package/.versions/0.2.0/.pi/extensions/mcp-bridge/package.json +6 -0
  52. package/.versions/0.2.0/.pi/extensions/package-lock.json +1143 -0
  53. package/.versions/0.2.0/.pi/extensions/package.json +9 -0
  54. package/.versions/0.2.0/.pi/harnesses/agent-chain/README.md +37 -0
  55. package/.versions/0.2.0/.pi/harnesses/agent-chain/index.ts +795 -0
  56. package/.versions/0.2.0/.pi/harnesses/agent-chain/package.json +6 -0
  57. package/.versions/0.2.0/.pi/harnesses/agent-team/README.md +38 -0
  58. package/.versions/0.2.0/.pi/harnesses/agent-team/index.ts +732 -0
  59. package/.versions/0.2.0/.pi/harnesses/agent-team/package.json +6 -0
  60. package/.versions/0.2.0/.pi/harnesses/coms/README.md +36 -0
  61. package/.versions/0.2.0/.pi/harnesses/coms/index.ts +1595 -0
  62. package/.versions/0.2.0/.pi/harnesses/coms/package.json +6 -0
  63. package/.versions/0.2.0/.pi/harnesses/coms-net/README.md +46 -0
  64. package/.versions/0.2.0/.pi/harnesses/coms-net/index.ts +1637 -0
  65. package/.versions/0.2.0/.pi/harnesses/coms-net/package.json +6 -0
  66. package/.versions/0.2.0/.pi/harnesses/damage-control/README.md +38 -0
  67. package/.versions/0.2.0/.pi/harnesses/damage-control/index.ts +207 -0
  68. package/.versions/0.2.0/.pi/harnesses/damage-control/package.json +6 -0
  69. package/.versions/0.2.0/.pi/harnesses/damage-control-continue/README.md +37 -0
  70. package/.versions/0.2.0/.pi/harnesses/damage-control-continue/index.ts +234 -0
  71. package/.versions/0.2.0/.pi/harnesses/damage-control-continue/package.json +6 -0
  72. package/.versions/0.2.0/.pi/harnesses/minimal/README.md +27 -0
  73. package/.versions/0.2.0/.pi/harnesses/minimal/index.ts +32 -0
  74. package/.versions/0.2.0/.pi/harnesses/minimal/package.json +6 -0
  75. package/.versions/0.2.0/.pi/harnesses/package-lock.json +35 -0
  76. package/.versions/0.2.0/.pi/harnesses/package.json +9 -0
  77. package/.versions/0.2.0/.pi/harnesses/pi-pi/README.md +39 -0
  78. package/.versions/0.2.0/.pi/harnesses/pi-pi/index.ts +631 -0
  79. package/.versions/0.2.0/.pi/harnesses/pi-pi/package.json +6 -0
  80. package/.versions/0.2.0/.pi/harnesses/purpose-gate/README.md +27 -0
  81. package/.versions/0.2.0/.pi/harnesses/purpose-gate/index.ts +82 -0
  82. package/.versions/0.2.0/.pi/harnesses/purpose-gate/package.json +6 -0
  83. package/.versions/0.2.0/.pi/harnesses/session-replay/README.md +28 -0
  84. package/.versions/0.2.0/.pi/harnesses/session-replay/index.ts +214 -0
  85. package/.versions/0.2.0/.pi/harnesses/session-replay/package.json +6 -0
  86. package/.versions/0.2.0/.pi/harnesses/subagent-widget/README.md +36 -0
  87. package/.versions/0.2.0/.pi/harnesses/subagent-widget/index.ts +479 -0
  88. package/.versions/0.2.0/.pi/harnesses/subagent-widget/package.json +6 -0
  89. package/.versions/0.2.0/.pi/harnesses/system-select/README.md +39 -0
  90. package/.versions/0.2.0/.pi/harnesses/system-select/index.ts +165 -0
  91. package/.versions/0.2.0/.pi/harnesses/system-select/package.json +6 -0
  92. package/.versions/0.2.0/.pi/harnesses/tilldone/README.md +35 -0
  93. package/.versions/0.2.0/.pi/harnesses/tilldone/index.ts +724 -0
  94. package/.versions/0.2.0/.pi/harnesses/tilldone/package.json +6 -0
  95. package/.versions/0.2.0/.pi/harnesses/tool-counter/README.md +31 -0
  96. package/.versions/0.2.0/.pi/harnesses/tool-counter/index.ts +100 -0
  97. package/.versions/0.2.0/.pi/harnesses/tool-counter/package.json +6 -0
  98. package/.versions/0.2.0/.pi/harnesses/tool-counter-widget/README.md +27 -0
  99. package/.versions/0.2.0/.pi/harnesses/tool-counter-widget/index.ts +66 -0
  100. package/.versions/0.2.0/.pi/harnesses/tool-counter-widget/package.json +6 -0
  101. package/.versions/0.2.0/.pi/prompts/build.md +24 -0
  102. package/.versions/0.2.0/.pi/prompts/code-simplify.md +22 -0
  103. package/.versions/0.2.0/.pi/prompts/doctor.md +13 -0
  104. package/.versions/0.2.0/.pi/prompts/plan.md +16 -0
  105. package/.versions/0.2.0/.pi/prompts/review.md +16 -0
  106. package/.versions/0.2.0/.pi/prompts/setup.md +19 -0
  107. package/.versions/0.2.0/.pi/prompts/ship.md +17 -0
  108. package/.versions/0.2.0/.pi/prompts/spec.md +15 -0
  109. package/.versions/0.2.0/.pi/prompts/test.md +19 -0
  110. package/.versions/0.2.0/.pi/skills/bowser/SKILL.md +114 -0
  111. package/.versions/0.2.0/.version +1 -0
  112. package/.versions/0.2.0/agents/builder.md +6 -0
  113. package/.versions/0.2.0/agents/code-reviewer.md +93 -0
  114. package/.versions/0.2.0/agents/documenter.md +6 -0
  115. package/.versions/0.2.0/agents/plan-reviewer.md +22 -0
  116. package/.versions/0.2.0/agents/planner.md +6 -0
  117. package/.versions/0.2.0/agents/scout.md +6 -0
  118. package/.versions/0.2.0/agents/security-auditor.md +97 -0
  119. package/.versions/0.2.0/agents/test-engineer.md +89 -0
  120. package/.versions/0.2.0/hooks/SIMPLIFY-IGNORE.md +90 -0
  121. package/.versions/0.2.0/hooks/hooks.json +14 -0
  122. package/.versions/0.2.0/hooks/session-start.sh +74 -0
  123. package/.versions/0.2.0/hooks/simplify-ignore-test.sh +247 -0
  124. package/.versions/0.2.0/hooks/simplify-ignore.sh +302 -0
  125. package/.versions/0.2.0/references/accessibility-checklist.md +159 -0
  126. package/.versions/0.2.0/references/performance-checklist.md +121 -0
  127. package/.versions/0.2.0/references/prompting-patterns.md +380 -0
  128. package/.versions/0.2.0/references/security-checklist.md +134 -0
  129. package/.versions/0.2.0/references/testing-patterns.md +236 -0
  130. package/.versions/0.2.0/skills/api-and-interface-design/SKILL.md +294 -0
  131. package/.versions/0.2.0/skills/browser-testing-with-devtools/SKILL.md +335 -0
  132. package/.versions/0.2.0/skills/ci-cd-and-automation/SKILL.md +390 -0
  133. package/.versions/0.2.0/skills/code-review-and-quality/SKILL.md +347 -0
  134. package/.versions/0.2.0/skills/code-simplification/SKILL.md +331 -0
  135. package/.versions/0.2.0/skills/context-engineering/SKILL.md +291 -0
  136. package/.versions/0.2.0/skills/debugging-and-error-recovery/SKILL.md +300 -0
  137. package/.versions/0.2.0/skills/deprecation-and-migration/SKILL.md +206 -0
  138. package/.versions/0.2.0/skills/designing-agents/SKILL.md +394 -0
  139. package/.versions/0.2.0/skills/designing-agents/pi-harness-authoring.md +213 -0
  140. package/.versions/0.2.0/skills/documentation-and-adrs/SKILL.md +278 -0
  141. package/.versions/0.2.0/skills/frontend-ui-engineering/SKILL.md +322 -0
  142. package/.versions/0.2.0/skills/git-workflow-and-versioning/SKILL.md +316 -0
  143. package/.versions/0.2.0/skills/guided-workspace-setup/SKILL.md +293 -0
  144. package/.versions/0.2.0/skills/idea-refine/SKILL.md +178 -0
  145. package/.versions/0.2.0/skills/idea-refine/examples.md +238 -0
  146. package/.versions/0.2.0/skills/idea-refine/frameworks.md +99 -0
  147. package/.versions/0.2.0/skills/idea-refine/refinement-criteria.md +113 -0
  148. package/.versions/0.2.0/skills/idea-refine/scripts/idea-refine.sh +15 -0
  149. package/.versions/0.2.0/skills/incremental-implementation/SKILL.md +279 -0
  150. package/.versions/0.2.0/skills/performance-optimization/SKILL.md +350 -0
  151. package/.versions/0.2.0/skills/planning-and-task-breakdown/SKILL.md +237 -0
  152. package/.versions/0.2.0/skills/security-and-hardening/SKILL.md +349 -0
  153. package/.versions/0.2.0/skills/shipping-and-launch/SKILL.md +309 -0
  154. package/.versions/0.2.0/skills/source-driven-development/SKILL.md +194 -0
  155. package/.versions/0.2.0/skills/spec-driven-development/SKILL.md +237 -0
  156. package/.versions/0.2.0/skills/test-driven-development/SKILL.md +379 -0
  157. package/.versions/0.2.0/skills/using-agent-skills/SKILL.md +176 -0
  158. package/CHANGELOG.md +36 -0
  159. package/bin/cli.js +42 -7
  160. package/bin/lib/update-notifier.js +195 -0
  161. package/docs/npm-install.md +60 -0
  162. package/hooks/session-start.sh +66 -12
  163. package/package.json +1 -1
  164. package/skills/guided-workspace-setup/SKILL.md +1 -1
@@ -0,0 +1,39 @@
1
+ # pi-pi
2
+
3
+ Meta-agent that builds pi agents.
4
+
5
+ > Ported from [`pi-vs-claude-code`](https://github.com/disler/pi-vs-claude-code) by [disler](https://github.com/disler) (MIT). See the [extension catalog](../../../docs/pi-extensions.md).
6
+
7
+ ## What it does
8
+
9
+ A team of domain-specific research experts — extensions, themes, skills, settings, TUI —
10
+ operate **in parallel** to gather pi documentation and patterns. The primary agent
11
+ synthesises their findings and is the only writer; the experts are read-only researchers.
12
+ Each expert fetches fresh pi documentation via Firecrawl on its first query.
13
+
14
+ Use it to scaffold new pi extensions, themes, skills, or agent definitions grounded in
15
+ current pi docs.
16
+
17
+ ## Commands & tools
18
+
19
+ - `/experts` — list the available experts and their status
20
+ - `/experts-grid N` — set the dashboard column count (default 3)
21
+
22
+ ## Requires
23
+
24
+ - `.pi/agents/pi-pi/*.md` — the expert persona definitions (shipped in this repo)
25
+ - `FIRECRAWL_API_KEY` — environment variable; experts use Firecrawl to crawl pi docs
26
+
27
+ ## Usage
28
+
29
+ ```bash
30
+ export FIRECRAWL_API_KEY=fc-...
31
+ pi -e .pi/harnesses/pi-pi/index.ts
32
+ ```
33
+
34
+ ## Upstream changes
35
+
36
+ - Theme integration removed — the `themeMap.ts` import and the `applyExtensionDefaults()`
37
+ call were stripped (this repo does not ship pi themes). Note: pi-pi still has a
38
+ `theme-expert` research persona — that is unrelated to the stripped UI theming and is
39
+ about *researching* how pi themes are authored.
@@ -0,0 +1,631 @@
1
+ /**
2
+ * Pi Pi — Meta-agent that builds Pi agents
3
+ *
4
+ * A team of domain-specific research experts (extensions, themes, skills,
5
+ * settings, TUI) operate in PARALLEL to gather documentation and patterns.
6
+ * The primary agent synthesizes their findings and WRITES the actual files.
7
+ *
8
+ * Each expert fetches fresh Pi documentation via firecrawl on first query.
9
+ * Experts are read-only researchers. The primary agent is the only writer.
10
+ *
11
+ * Commands:
12
+ * /experts — list available experts and their status
13
+ * /experts-grid N — set dashboard column count (default 3)
14
+ *
15
+ * Usage: pi -e extensions/pi-pi.ts
16
+ */
17
+
18
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
19
+ import { Type } from "@sinclair/typebox";
20
+ import { Text, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
21
+ import { spawn } from "child_process";
22
+ import { readdirSync, readFileSync, existsSync, mkdirSync } from "fs";
23
+ import { join, resolve } from "path";
24
+
25
+ // ── Types ────────────────────────────────────────
26
+
27
+ interface ExpertDef {
28
+ name: string;
29
+ description: string;
30
+ tools: string;
31
+ systemPrompt: string;
32
+ file: string;
33
+ }
34
+
35
+ interface ExpertState {
36
+ def: ExpertDef;
37
+ status: "idle" | "researching" | "done" | "error";
38
+ question: string;
39
+ elapsed: number;
40
+ lastLine: string;
41
+ queryCount: number;
42
+ timer?: ReturnType<typeof setInterval>;
43
+ }
44
+
45
+ // ── Helpers ──────────────────────────────────────
46
+
47
+ function displayName(name: string): string {
48
+ return name.split("-").map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
49
+ }
50
+
51
+ function parseAgentFile(filePath: string): ExpertDef | null {
52
+ try {
53
+ const raw = readFileSync(filePath, "utf-8");
54
+ const match = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
55
+ if (!match) return null;
56
+
57
+ const frontmatter: Record<string, string> = {};
58
+ for (const line of match[1].split("\n")) {
59
+ const idx = line.indexOf(":");
60
+ if (idx > 0) {
61
+ frontmatter[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
62
+ }
63
+ }
64
+
65
+ if (!frontmatter.name) return null;
66
+
67
+ return {
68
+ name: frontmatter.name,
69
+ description: frontmatter.description || "",
70
+ tools: frontmatter.tools || "read,grep,find,ls",
71
+ systemPrompt: match[2].trim(),
72
+ file: filePath,
73
+ };
74
+ } catch {
75
+ return null;
76
+ }
77
+ }
78
+
79
+ // ── Expert card colors ────────────────────────────
80
+ // Each expert gets a unique hue: bg fills the card interior,
81
+ // br is the matching border foreground (brighter shade of same hue).
82
+ const EXPERT_COLORS: Record<string, { bg: string; br: string }> = {
83
+ "agent-expert": { bg: "\x1b[48;2;20;30;75m", br: "\x1b[38;2;70;110;210m" }, // navy
84
+ "config-expert": { bg: "\x1b[48;2;18;65;30m", br: "\x1b[38;2;55;175;90m" }, // forest
85
+ "ext-expert": { bg: "\x1b[48;2;80;18;28m", br: "\x1b[38;2;210;65;85m" }, // crimson
86
+ "keybinding-expert": { bg: "\x1b[48;2;50;22;85m", br: "\x1b[38;2;145;80;220m" }, // violet
87
+ "prompt-expert": { bg: "\x1b[48;2;80;55;12m", br: "\x1b[38;2;215;150;40m" }, // amber
88
+ "skill-expert": { bg: "\x1b[48;2;12;65;75m", br: "\x1b[38;2;40;175;195m" }, // teal
89
+ "theme-expert": { bg: "\x1b[48;2;80;18;62m", br: "\x1b[38;2;210;55;160m" }, // rose
90
+ "tui-expert": { bg: "\x1b[48;2;28;42;80m", br: "\x1b[38;2;85;120;210m" }, // slate
91
+ "cli-expert": { bg: "\x1b[48;2;60;80;20m", br: "\x1b[38;2;160;210;55m" }, // olive/lime
92
+ };
93
+ const FG_RESET = "\x1b[39m";
94
+ const BG_RESET = "\x1b[49m";
95
+
96
+ // ── Extension ────────────────────────────────────
97
+
98
+ export default function (pi: ExtensionAPI) {
99
+ const experts: Map<string, ExpertState> = new Map();
100
+ let gridCols = 3;
101
+ let widgetCtx: any;
102
+
103
+ function loadExperts(cwd: string) {
104
+ // Pi Pi experts live in their own dedicated directory
105
+ const piPiDir = join(cwd, ".pi", "agents", "pi-pi");
106
+
107
+ experts.clear();
108
+
109
+ if (!existsSync(piPiDir)) return;
110
+ try {
111
+ for (const file of readdirSync(piPiDir)) {
112
+ if (!file.endsWith(".md")) continue;
113
+ if (file === "pi-orchestrator.md") continue;
114
+ const fullPath = resolve(piPiDir, file);
115
+ const def = parseAgentFile(fullPath);
116
+ if (def) {
117
+ const key = def.name.toLowerCase();
118
+ if (!experts.has(key)) {
119
+ experts.set(key, {
120
+ def,
121
+ status: "idle",
122
+ question: "",
123
+ elapsed: 0,
124
+ lastLine: "",
125
+ queryCount: 0,
126
+ });
127
+ }
128
+ }
129
+ }
130
+ } catch {}
131
+ }
132
+
133
+ // ── Grid Rendering ───────────────────────────
134
+
135
+ function renderCard(state: ExpertState, colWidth: number, theme: any): string[] {
136
+ const w = colWidth - 2;
137
+ const truncate = (s: string, max: number) => s.length > max ? s.slice(0, max - 3) + "..." : s;
138
+
139
+ const statusColor = state.status === "idle" ? "dim"
140
+ : state.status === "researching" ? "accent"
141
+ : state.status === "done" ? "success" : "error";
142
+ const statusIcon = state.status === "idle" ? "○"
143
+ : state.status === "researching" ? "◉"
144
+ : state.status === "done" ? "✓" : "✗";
145
+
146
+ const name = displayName(state.def.name);
147
+ const nameStr = theme.fg("accent", theme.bold(truncate(name, w)));
148
+ const nameVisible = Math.min(name.length, w);
149
+
150
+ const statusStr = `${statusIcon} ${state.status}`;
151
+ const timeStr = state.status !== "idle" ? ` ${Math.round(state.elapsed / 1000)}s` : "";
152
+ const queriesStr = state.queryCount > 0 ? ` (${state.queryCount})` : "";
153
+ const statusLine = theme.fg(statusColor, statusStr + timeStr + queriesStr);
154
+ const statusVisible = statusStr.length + timeStr.length + queriesStr.length;
155
+
156
+ const workRaw = state.question || state.def.description;
157
+ const workText = truncate(workRaw, Math.min(50, w - 1));
158
+ const workLine = theme.fg("muted", workText);
159
+ const workVisible = workText.length;
160
+
161
+ const lastRaw = state.lastLine || "";
162
+ const lastText = truncate(lastRaw, Math.min(50, w - 1));
163
+ const lastLineRendered = lastText ? theme.fg("dim", lastText) : theme.fg("dim", "—");
164
+ const lastVisible = lastText ? lastText.length : 1;
165
+
166
+ const colors = EXPERT_COLORS[state.def.name];
167
+ const bg = colors?.bg ?? "";
168
+ const br = colors?.br ?? "";
169
+ const bgr = bg ? BG_RESET : "";
170
+ const fgr = br ? FG_RESET : "";
171
+
172
+ // br colors the box-drawing characters; bg fills behind them so the
173
+ // full card — top line, side bars, bottom line — is one solid block.
174
+ const bord = (s: string) => bg + br + s + bgr + fgr;
175
+
176
+ const top = "┌" + "─".repeat(w) + "┐";
177
+ const bot = "└" + "─".repeat(w) + "┘";
178
+
179
+ // bg fills the inner content area; re-applied before padding to ensure
180
+ // the full row is colored even if theme.fg uses a full ANSI reset inside.
181
+ const border = (content: string, visLen: number) => {
182
+ const pad = " ".repeat(Math.max(0, w - visLen));
183
+ return bord("│") + bg + content + bg + pad + bgr + bord("│");
184
+ };
185
+
186
+ return [
187
+ bord(top),
188
+ border(" " + nameStr, 1 + nameVisible),
189
+ border(" " + statusLine, 1 + statusVisible),
190
+ border(" " + workLine, 1 + workVisible),
191
+ border(" " + lastLineRendered, 1 + lastVisible),
192
+ bord(bot),
193
+ ];
194
+ }
195
+
196
+ function updateWidget() {
197
+ if (!widgetCtx) return;
198
+
199
+ widgetCtx.ui.setWidget("pi-pi-grid", (_tui: any, theme: any) => {
200
+
201
+ return {
202
+ render(width: number): string[] {
203
+ if (experts.size === 0) {
204
+ return ["", theme.fg("dim", " No experts found. Add agent .md files to .pi/agents/pi-pi/")];
205
+ }
206
+
207
+ const cols = Math.min(gridCols, experts.size);
208
+ const gap = 1;
209
+ // avoid Text component's ANSI-width miscounting by returning raw lines
210
+ const colWidth = Math.floor((width - gap * (cols - 1)) / cols) - 1;
211
+ const allExperts = Array.from(experts.values());
212
+
213
+ const lines: string[] = [""]; // top margin
214
+
215
+ for (let i = 0; i < allExperts.length; i += cols) {
216
+ const rowExperts = allExperts.slice(i, i + cols);
217
+ const cards = rowExperts.map(e => renderCard(e, colWidth, theme));
218
+
219
+ while (cards.length < cols) {
220
+ cards.push(Array(6).fill(" ".repeat(colWidth)));
221
+ }
222
+
223
+ const cardHeight = cards[0].length;
224
+ for (let line = 0; line < cardHeight; line++) {
225
+ lines.push(cards.map(card => card[line] || "").join(" ".repeat(gap)));
226
+ }
227
+ }
228
+
229
+ return lines;
230
+ },
231
+ invalidate() {},
232
+ };
233
+ });
234
+ }
235
+
236
+ // ── Query Expert ─────────────────────────────
237
+
238
+ function queryExpert(
239
+ expertName: string,
240
+ question: string,
241
+ ctx: any,
242
+ ): Promise<{ output: string; exitCode: number; elapsed: number }> {
243
+ const key = expertName.toLowerCase();
244
+ const state = experts.get(key);
245
+ if (!state) {
246
+ return Promise.resolve({
247
+ output: `Expert "${expertName}" not found. Available: ${Array.from(experts.values()).map(s => s.def.name).join(", ")}`,
248
+ exitCode: 1,
249
+ elapsed: 0,
250
+ });
251
+ }
252
+
253
+ if (state.status === "researching") {
254
+ return Promise.resolve({
255
+ output: `Expert "${displayName(state.def.name)}" is already researching. Wait for it to finish.`,
256
+ exitCode: 1,
257
+ elapsed: 0,
258
+ });
259
+ }
260
+
261
+ state.status = "researching";
262
+ state.question = question;
263
+ state.elapsed = 0;
264
+ state.lastLine = "";
265
+ state.queryCount++;
266
+ updateWidget();
267
+
268
+ const startTime = Date.now();
269
+ state.timer = setInterval(() => {
270
+ state.elapsed = Date.now() - startTime;
271
+ updateWidget();
272
+ }, 1000);
273
+
274
+ const model = ctx.model
275
+ ? `${ctx.model.provider}/${ctx.model.id}`
276
+ : "openrouter/google/gemini-3-flash-preview";
277
+
278
+ const args = [
279
+ "--mode", "json",
280
+ "-p",
281
+ "--no-session",
282
+ "--no-extensions",
283
+ "--model", model,
284
+ "--tools", state.def.tools,
285
+ "--thinking", "off",
286
+ "--append-system-prompt", state.def.systemPrompt,
287
+ question,
288
+ ];
289
+
290
+ const textChunks: string[] = [];
291
+
292
+ return new Promise((resolve) => {
293
+ const proc = spawn("pi", args, {
294
+ stdio: ["ignore", "pipe", "pipe"],
295
+ env: { ...process.env },
296
+ });
297
+
298
+ let buffer = "";
299
+
300
+ proc.stdout!.setEncoding("utf-8");
301
+ proc.stdout!.on("data", (chunk: string) => {
302
+ buffer += chunk;
303
+ const lines = buffer.split("\n");
304
+ buffer = lines.pop() || "";
305
+ for (const line of lines) {
306
+ if (!line.trim()) continue;
307
+ try {
308
+ const event = JSON.parse(line);
309
+ if (event.type === "message_update") {
310
+ const delta = event.assistantMessageEvent;
311
+ if (delta?.type === "text_delta") {
312
+ textChunks.push(delta.delta || "");
313
+ const full = textChunks.join("");
314
+ const last = full.split("\n").filter((l: string) => l.trim()).pop() || "";
315
+ state.lastLine = last;
316
+ updateWidget();
317
+ }
318
+ }
319
+ } catch {}
320
+ }
321
+ });
322
+
323
+ proc.stderr!.setEncoding("utf-8");
324
+ proc.stderr!.on("data", () => {});
325
+
326
+ proc.on("close", (code) => {
327
+ if (buffer.trim()) {
328
+ try {
329
+ const event = JSON.parse(buffer);
330
+ if (event.type === "message_update") {
331
+ const delta = event.assistantMessageEvent;
332
+ if (delta?.type === "text_delta") textChunks.push(delta.delta || "");
333
+ }
334
+ } catch {}
335
+ }
336
+
337
+ clearInterval(state.timer);
338
+ state.elapsed = Date.now() - startTime;
339
+ state.status = code === 0 ? "done" : "error";
340
+
341
+ const full = textChunks.join("");
342
+ state.lastLine = full.split("\n").filter((l: string) => l.trim()).pop() || "";
343
+ updateWidget();
344
+
345
+ ctx.ui.notify(
346
+ `${displayName(state.def.name)} ${state.status} in ${Math.round(state.elapsed / 1000)}s`,
347
+ state.status === "done" ? "success" : "error"
348
+ );
349
+
350
+ resolve({
351
+ output: full,
352
+ exitCode: code ?? 1,
353
+ elapsed: state.elapsed,
354
+ });
355
+ });
356
+
357
+ proc.on("error", (err) => {
358
+ clearInterval(state.timer);
359
+ state.status = "error";
360
+ state.lastLine = `Error: ${err.message}`;
361
+ updateWidget();
362
+ resolve({
363
+ output: `Error spawning expert: ${err.message}`,
364
+ exitCode: 1,
365
+ elapsed: Date.now() - startTime,
366
+ });
367
+ });
368
+ });
369
+ }
370
+
371
+ // ── query_experts Tool (parallel) ───────────
372
+
373
+ pi.registerTool({
374
+ name: "query_experts",
375
+ label: "Query Experts",
376
+ description: `Query one or more Pi domain experts IN PARALLEL. All experts run simultaneously as concurrent subprocesses.
377
+
378
+ Pass an array of queries — each with an expert name and a specific question. All experts start at the same time and their results are returned together.
379
+
380
+ Available experts:
381
+ - ext-expert: Extensions — tools, events, commands, rendering, state management
382
+ - theme-expert: Themes — JSON format, 51 color tokens, vars, color values
383
+ - skill-expert: Skills — SKILL.md multi-file packages, scripts, references, frontmatter
384
+ - config-expert: Settings — settings.json, providers, models, packages, keybindings
385
+ - tui-expert: TUI — components, keyboard input, overlays, widgets, footers, editors
386
+ - prompt-expert: Prompt templates — single-file .md commands, arguments ($1, $@)
387
+ - agent-expert: Agent definitions — .md personas, tools, teams.yaml, orchestration
388
+ - keybinding-expert: Keyboard shortcuts — registerShortcut(), Key IDs, reserved keys, macOS terminal compatibility
389
+
390
+ Ask specific questions about what you need to BUILD. Each expert will return documentation excerpts, code patterns, and implementation guidance.`,
391
+
392
+ parameters: Type.Object({
393
+ queries: Type.Array(
394
+ Type.Object({
395
+ expert: Type.String({
396
+ description: "Expert name: ext-expert, theme-expert, skill-expert, config-expert, tui-expert, prompt-expert, or agent-expert",
397
+ }),
398
+ question: Type.String({
399
+ description: "Specific question about what you need to build. Include context about the target component.",
400
+ }),
401
+ }),
402
+ { description: "Array of expert queries to run in parallel" },
403
+ ),
404
+ }),
405
+
406
+ async execute(_toolCallId, params, _signal, onUpdate, ctx) {
407
+ const { queries } = params as { queries: { expert: string; question: string }[] };
408
+
409
+ if (!queries || queries.length === 0) {
410
+ return {
411
+ content: [{ type: "text", text: "No queries provided." }],
412
+ details: { results: [], status: "error" },
413
+ };
414
+ }
415
+
416
+ const names = queries.map(q => displayName(q.expert)).join(", ");
417
+ if (onUpdate) {
418
+ onUpdate({
419
+ content: [{ type: "text", text: `Querying ${queries.length} experts in parallel: ${names}` }],
420
+ details: { queries, status: "researching", results: [] },
421
+ });
422
+ }
423
+
424
+ // Launch ALL experts concurrently — allSettled so one failure
425
+ // never discards results from the others
426
+ const settled = await Promise.allSettled(
427
+ queries.map(async ({ expert, question }) => {
428
+ const result = await queryExpert(expert, question, ctx);
429
+ const truncated = result.output.length > 12000
430
+ ? result.output.slice(0, 12000) + "\n\n... [truncated — ask follow-up for more]"
431
+ : result.output;
432
+ const status = result.exitCode === 0 ? "done" : "error";
433
+ return {
434
+ expert,
435
+ question,
436
+ status,
437
+ elapsed: result.elapsed,
438
+ exitCode: result.exitCode,
439
+ output: truncated,
440
+ fullOutput: result.output,
441
+ };
442
+ }),
443
+ );
444
+
445
+ const results = settled.map((s, i) =>
446
+ s.status === "fulfilled"
447
+ ? s.value
448
+ : {
449
+ expert: queries[i].expert,
450
+ question: queries[i].question,
451
+ status: "error" as const,
452
+ elapsed: 0,
453
+ exitCode: 1,
454
+ output: `Error: ${(s.reason as any)?.message || s.reason}`,
455
+ fullOutput: "",
456
+ },
457
+ );
458
+
459
+ // Build combined response
460
+ const sections = results.map(r => {
461
+ const icon = r.status === "done" ? "✓" : "✗";
462
+ return `## [${icon}] ${displayName(r.expert)} (${Math.round(r.elapsed / 1000)}s)\n\n${r.output}`;
463
+ });
464
+
465
+ return {
466
+ content: [{ type: "text", text: sections.join("\n\n---\n\n") }],
467
+ details: {
468
+ results,
469
+ status: results.every(r => r.status === "done") ? "done" : "partial",
470
+ },
471
+ };
472
+ },
473
+
474
+ renderCall(args, theme) {
475
+ const queries = (args as any).queries || [];
476
+ const names = queries.map((q: any) => displayName(q.expert || "?")).join(", ");
477
+ return new Text(
478
+ theme.fg("toolTitle", theme.bold("query_experts ")) +
479
+ theme.fg("accent", `${queries.length} parallel`) +
480
+ theme.fg("dim", " — ") +
481
+ theme.fg("muted", names),
482
+ 0, 0,
483
+ );
484
+ },
485
+
486
+ renderResult(result, options, theme) {
487
+ const details = result.details as any;
488
+ if (!details?.results) {
489
+ const text = result.content[0];
490
+ return new Text(text?.type === "text" ? text.text : "", 0, 0);
491
+ }
492
+
493
+ if (options.isPartial || details.status === "researching") {
494
+ const count = details.queries?.length || "?";
495
+ return new Text(
496
+ theme.fg("accent", `◉ ${count} experts`) +
497
+ theme.fg("dim", " researching in parallel..."),
498
+ 0, 0,
499
+ );
500
+ }
501
+
502
+ const lines = (details.results as any[]).map((r: any) => {
503
+ const icon = r.status === "done" ? "✓" : "✗";
504
+ const color = r.status === "done" ? "success" : "error";
505
+ const elapsed = typeof r.elapsed === "number" ? Math.round(r.elapsed / 1000) : 0;
506
+ return theme.fg(color, `${icon} ${displayName(r.expert)}`) +
507
+ theme.fg("dim", ` ${elapsed}s`);
508
+ });
509
+
510
+ const header = lines.join(theme.fg("dim", " · "));
511
+
512
+ if (options.expanded && details.results) {
513
+ const expanded = (details.results as any[]).map((r: any) => {
514
+ const output = r.fullOutput
515
+ ? (r.fullOutput.length > 4000 ? r.fullOutput.slice(0, 4000) + "\n... [truncated]" : r.fullOutput)
516
+ : r.output || "";
517
+ return theme.fg("accent", `── ${displayName(r.expert)} ──`) + "\n" + theme.fg("muted", output);
518
+ });
519
+ return new Text(header + "\n\n" + expanded.join("\n\n"), 0, 0);
520
+ }
521
+
522
+ return new Text(header, 0, 0);
523
+ },
524
+ });
525
+
526
+ // ── Commands ─────────────────────────────────
527
+
528
+ pi.registerCommand("experts", {
529
+ description: "List available Pi Pi experts and their status",
530
+ handler: async (_args, _ctx) => {
531
+ widgetCtx = _ctx;
532
+ const lines = Array.from(experts.values())
533
+ .map(s => `${displayName(s.def.name)} (${s.status}, queries: ${s.queryCount}): ${s.def.description}`)
534
+ .join("\n");
535
+ _ctx.ui.notify(lines || "No experts loaded", "info");
536
+ },
537
+ });
538
+
539
+ pi.registerCommand("experts-grid", {
540
+ description: "Set expert grid columns: /experts-grid <1-5>",
541
+ handler: async (args, _ctx) => {
542
+ widgetCtx = _ctx;
543
+ const n = parseInt(args?.trim() || "", 10);
544
+ if (n >= 1 && n <= 5) {
545
+ gridCols = n;
546
+ _ctx.ui.notify(`Grid set to ${gridCols} columns`, "info");
547
+ updateWidget();
548
+ } else {
549
+ _ctx.ui.notify("Usage: /experts-grid <1-5>", "error");
550
+ }
551
+ },
552
+ });
553
+
554
+ // ── System Prompt ────────────────────────────
555
+
556
+ pi.on("before_agent_start", async (_event, _ctx) => {
557
+ const expertCatalog = Array.from(experts.values())
558
+ .map(s => `### ${displayName(s.def.name)}\n**Query as:** \`${s.def.name}\`\n${s.def.description}`)
559
+ .join("\n\n");
560
+
561
+ const expertNames = Array.from(experts.values()).map(s => displayName(s.def.name)).join(", ");
562
+
563
+ const orchestratorPath = join(_ctx.cwd, ".pi", "agents", "pi-pi", "pi-orchestrator.md");
564
+ let systemPrompt = "";
565
+ try {
566
+ const raw = readFileSync(orchestratorPath, "utf-8");
567
+ const match = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
568
+ const template = match ? match[2].trim() : raw;
569
+
570
+ systemPrompt = template
571
+ .replace("{{EXPERT_COUNT}}", experts.size.toString())
572
+ .replace("{{EXPERT_NAMES}}", expertNames)
573
+ .replace("{{EXPERT_CATALOG}}", expertCatalog);
574
+ } catch (err) {
575
+ systemPrompt = "Error: Could not load pi-orchestrator.md. Make sure it exists in .pi/agents/pi-pi/.";
576
+ }
577
+
578
+ return { systemPrompt };
579
+ });
580
+
581
+ // ── Session Start ────────────────────────────
582
+
583
+ pi.on("session_start", async (_event, _ctx) => {
584
+ if (widgetCtx) {
585
+ widgetCtx.ui.setWidget("pi-pi-grid", undefined);
586
+ }
587
+ widgetCtx = _ctx;
588
+
589
+ loadExperts(_ctx.cwd);
590
+ updateWidget();
591
+
592
+ const expertNames = Array.from(experts.values()).map(s => displayName(s.def.name)).join(", ");
593
+ _ctx.ui.setStatus("pi-pi", `Pi Pi (${experts.size} experts)`);
594
+ _ctx.ui.notify(
595
+ `Pi Pi loaded — ${experts.size} experts: ${expertNames}\n\n` +
596
+ `/experts List experts and status\n` +
597
+ `/experts-grid N Set grid columns (1-5)\n\n` +
598
+ `Ask me to build any Pi agent component!`,
599
+ "info",
600
+ );
601
+
602
+ // Custom footer
603
+ _ctx.ui.setFooter((_tui, theme, _footerData) => ({
604
+ dispose: () => {},
605
+ invalidate() {},
606
+ render(width: number): string[] {
607
+ const model = _ctx.model?.id || "no-model";
608
+ const usage = _ctx.getContextUsage();
609
+ const pct = usage ? usage.percent : 0;
610
+ const filled = Math.round(pct / 10);
611
+ const bar = "#".repeat(filled) + "-".repeat(10 - filled);
612
+
613
+ const active = Array.from(experts.values()).filter(e => e.status === "researching").length;
614
+ const done = Array.from(experts.values()).filter(e => e.status === "done").length;
615
+
616
+ const left = theme.fg("dim", ` ${model}`) +
617
+ theme.fg("muted", " · ") +
618
+ theme.fg("accent", "Pi Pi");
619
+ const mid = active > 0
620
+ ? theme.fg("accent", ` ◉ ${active} researching`)
621
+ : done > 0
622
+ ? theme.fg("success", ` ✓ ${done} done`)
623
+ : "";
624
+ const right = theme.fg("dim", `[${bar}] ${Math.round(pct)}% `);
625
+ const pad = " ".repeat(Math.max(1, width - visibleWidth(left) - visibleWidth(mid) - visibleWidth(right)));
626
+
627
+ return [truncateToWidth(left + mid + pad + right, width)];
628
+ },
629
+ }));
630
+ });
631
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "agent-skills-pi-pi-pi",
3
+ "private": true,
4
+ "type": "module",
5
+ "main": "index.ts"
6
+ }