@preapexis/piexis 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/AGENTS.md +21 -41
  2. package/README.md +73 -139
  3. package/extensions/agents.ts +205 -0
  4. package/extensions/ask-question.ts +54 -95
  5. package/extensions/recommended-tools.ts +62 -0
  6. package/extensions/ui.ts +204 -58
  7. package/lib/agent-config.ts +55 -0
  8. package/lib/agents.ts +208 -0
  9. package/lib/question-model.ts +98 -0
  10. package/lib/question-ui.ts +214 -0
  11. package/lib/recommended-tools.ts +119 -0
  12. package/lib/subagent-runner.ts +146 -0
  13. package/lib/ui-tips.json +41 -0
  14. package/package.json +5 -13
  15. package/prompts/init.md +10 -2
  16. package/prompts/summary.md +7 -6
  17. package/schemas/agent.schema.json +48 -0
  18. package/skills/{claude-api/LICENSE.txt → caveman/LICENSE} +2 -2
  19. package/skills/caveman/LICENSE-MIT +21 -0
  20. package/skills/caveman/NOTICE +10 -0
  21. package/skills/caveman/SKILL.md +89 -0
  22. package/skills/find-skills/SKILL.md +141 -0
  23. package/skills/ponytail/LICENSE +21 -0
  24. package/skills/ponytail/NOTICE +9 -0
  25. package/skills/ponytail/SKILL.md +52 -0
  26. package/skills-lock.json +6 -0
  27. package/tests/agents.test.mjs +605 -0
  28. package/tests/ask-question-pty.test.mjs +16 -0
  29. package/tests/ask-question.test.mjs +269 -0
  30. package/tests/fixtures/agents-provider.ts +55 -0
  31. package/tests/fixtures/ask-question-pty.py +91 -0
  32. package/tests/fixtures/ask-question-terminal.mjs +69 -0
  33. package/tests/fixtures/recommended-tools-terminal.mjs +103 -0
  34. package/tests/init-workflow.test.mjs +31 -0
  35. package/tests/recommended-tools-pty.test.mjs +16 -0
  36. package/tests/recommended-tools.test.mjs +423 -0
  37. package/tests/resources.test.mjs +66 -0
  38. package/tests/skills-routing.test.mjs +7 -4
  39. package/tests/ui-tip-reload.test.mjs +70 -0
  40. package/tests/ui.test.mjs +450 -0
  41. package/tsconfig.json +1 -0
  42. package/eval/README.md +0 -72
  43. package/eval/permission-cases.json +0 -28
  44. package/eval/permissions.mjs +0 -103
  45. package/extensions/background-tasks.ts +0 -433
  46. package/extensions/background-worker.ts +0 -50
  47. package/extensions/glob.ts +0 -32
  48. package/extensions/mode.ts +0 -397
  49. package/lib/bash-approvals.ts +0 -113
  50. package/lib/bash-policy.ts +0 -164
  51. package/lib/mode-policy.ts +0 -174
  52. package/lib/path-policy.ts +0 -182
  53. package/lib/permission-classifier.ts +0 -128
  54. package/lib/permission-config.ts +0 -91
  55. package/lib/permission-denials.ts +0 -32
  56. package/lib/permission-intent.ts +0 -128
  57. package/lib/permission-taxonomy.ts +0 -15
  58. package/lib/task-broker.ts +0 -65
  59. package/lib/task-checkout.ts +0 -39
  60. package/lib/task-git.ts +0 -44
  61. package/lib/task-patch.ts +0 -106
  62. package/lib/task-permissions.ts +0 -51
  63. package/lib/task-report.ts +0 -62
  64. package/lib/task-worktree-policy.ts +0 -56
  65. package/skills/claude-api/SKILL.md +0 -572
  66. package/skills/claude-api/csharp/claude-api/README.md +0 -362
  67. package/skills/claude-api/csharp/claude-api/batches.md +0 -14
  68. package/skills/claude-api/csharp/claude-api/files-api.md +0 -25
  69. package/skills/claude-api/csharp/claude-api/streaming.md +0 -28
  70. package/skills/claude-api/csharp/claude-api/tool-use.md +0 -164
  71. package/skills/claude-api/curl/examples.md +0 -256
  72. package/skills/claude-api/curl/managed-agents.md +0 -371
  73. package/skills/claude-api/go/claude-api/README.md +0 -188
  74. package/skills/claude-api/go/claude-api/files-api.md +0 -23
  75. package/skills/claude-api/go/claude-api/streaming.md +0 -43
  76. package/skills/claude-api/go/claude-api/tool-use.md +0 -220
  77. package/skills/claude-api/go/managed-agents/README.md +0 -564
  78. package/skills/claude-api/java/claude-api/README.md +0 -238
  79. package/skills/claude-api/java/claude-api/files-api.md +0 -27
  80. package/skills/claude-api/java/claude-api/streaming.md +0 -24
  81. package/skills/claude-api/java/claude-api/tool-use.md +0 -228
  82. package/skills/claude-api/java/managed-agents/README.md +0 -443
  83. package/skills/claude-api/php/claude-api/README.md +0 -174
  84. package/skills/claude-api/php/claude-api/batches.md +0 -15
  85. package/skills/claude-api/php/claude-api/files-api.md +0 -13
  86. package/skills/claude-api/php/claude-api/streaming.md +0 -27
  87. package/skills/claude-api/php/claude-api/tool-use.md +0 -253
  88. package/skills/claude-api/php/managed-agents/README.md +0 -443
  89. package/skills/claude-api/python/claude-api/README.md +0 -565
  90. package/skills/claude-api/python/claude-api/batches.md +0 -198
  91. package/skills/claude-api/python/claude-api/files-api.md +0 -170
  92. package/skills/claude-api/python/claude-api/sdk-upgrade.md +0 -286
  93. package/skills/claude-api/python/claude-api/streaming.md +0 -179
  94. package/skills/claude-api/python/claude-api/tool-use.md +0 -627
  95. package/skills/claude-api/python/managed-agents/README.md +0 -335
  96. package/skills/claude-api/ruby/claude-api/README.md +0 -134
  97. package/skills/claude-api/ruby/claude-api/streaming.md +0 -16
  98. package/skills/claude-api/ruby/claude-api/tool-use.md +0 -41
  99. package/skills/claude-api/ruby/managed-agents/README.md +0 -394
  100. package/skills/claude-api/shared/admin-api.md +0 -179
  101. package/skills/claude-api/shared/agent-design.md +0 -101
  102. package/skills/claude-api/shared/anthropic-cli.md +0 -269
  103. package/skills/claude-api/shared/claude-platform-on-aws.md +0 -59
  104. package/skills/claude-api/shared/cost-optimization.md +0 -233
  105. package/skills/claude-api/shared/error-codes.md +0 -262
  106. package/skills/claude-api/shared/live-sources.md +0 -166
  107. package/skills/claude-api/shared/managed-agents-api-reference.md +0 -455
  108. package/skills/claude-api/shared/managed-agents-client-patterns.md +0 -220
  109. package/skills/claude-api/shared/managed-agents-core.md +0 -359
  110. package/skills/claude-api/shared/managed-agents-environments.md +0 -226
  111. package/skills/claude-api/shared/managed-agents-events.md +0 -290
  112. package/skills/claude-api/shared/managed-agents-memory.md +0 -201
  113. package/skills/claude-api/shared/managed-agents-multiagent.md +0 -260
  114. package/skills/claude-api/shared/managed-agents-onboarding.md +0 -82
  115. package/skills/claude-api/shared/managed-agents-outcomes.md +0 -108
  116. package/skills/claude-api/shared/managed-agents-overview.md +0 -77
  117. package/skills/claude-api/shared/managed-agents-scheduled-deployments.md +0 -156
  118. package/skills/claude-api/shared/managed-agents-self-hosted-sandboxes.md +0 -282
  119. package/skills/claude-api/shared/managed-agents-tools.md +0 -504
  120. package/skills/claude-api/shared/managed-agents-webhooks.md +0 -143
  121. package/skills/claude-api/shared/model-migration.md +0 -1872
  122. package/skills/claude-api/shared/models.md +0 -142
  123. package/skills/claude-api/shared/platform-availability.md +0 -53
  124. package/skills/claude-api/shared/prompt-audit.md +0 -223
  125. package/skills/claude-api/shared/prompt-caching.md +0 -292
  126. package/skills/claude-api/shared/token-counting.md +0 -56
  127. package/skills/claude-api/shared/tool-use-concepts.md +0 -526
  128. package/skills/claude-api/typescript/claude-api/README.md +0 -405
  129. package/skills/claude-api/typescript/claude-api/batches.md +0 -106
  130. package/skills/claude-api/typescript/claude-api/files-api.md +0 -98
  131. package/skills/claude-api/typescript/claude-api/streaming.md +0 -178
  132. package/skills/claude-api/typescript/claude-api/tool-use.md +0 -586
  133. package/skills/claude-api/typescript/managed-agents/README.md +0 -359
  134. package/tests/auto-permissions.test.mjs +0 -315
  135. package/tests/background-tasks.test.mjs +0 -27
  136. package/tests/background-worker.test.mjs +0 -60
  137. package/tests/bash-permissions.test.mjs +0 -276
  138. package/tests/glob.test.mjs +0 -31
  139. package/tests/mode.test.mjs +0 -62
  140. package/tests/permission-evaluation.test.mjs +0 -70
  141. package/tests/permission-foundation.test.mjs +0 -194
  142. package/tests/permission-policy.test.mjs +0 -158
  143. package/tests/runtime/README.md +0 -51
  144. package/tests/runtime/harness.mjs +0 -190
  145. package/tests/runtime/permissions.test.mjs +0 -192
  146. package/tests/runtime/tui.test.mjs +0 -60
  147. package/tests/task-integration.test.mjs +0 -324
  148. package/tests/task-permissions.test.mjs +0 -178
  149. package/tests/task-worktree-policy.test.mjs +0 -53
package/extensions/ui.ts CHANGED
@@ -1,20 +1,108 @@
1
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
1
+ import { getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
 
3
- import { truncateToWidth } from "@earendil-works/pi-tui";
3
+ import { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
4
4
 
5
+ import { readFileSync } from "node:fs";
5
6
  import { basename } from "node:path";
7
+ import { agentConfigCapabilities } from "../lib/agent-config.ts";
8
+ import { hasProjectAgentConfig, loadAgentConfig } from "../lib/agents.ts";
9
+
10
+ const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
11
+
12
+ const WORDMARK = [
13
+ "██████╗ ██╗███████╗██╗ ██╗██╗███████╗",
14
+ "██╔══██╗██║██╔════╝╚██╗██╔╝██║██╔════╝",
15
+ "██████╔╝██║█████╗ ╚███╔╝ ██║███████╗",
16
+ "██╔═══╝ ██║██╔══╝ ██╔██╗ ██║╚════██║",
17
+ "██║ ██║███████╗██╔╝ ██╗██║███████║",
18
+ "╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚═╝╚══════╝"
19
+ ];
20
+ const WORDMARK_WIDTH = Math.max(...WORDMARK.map((line) => visibleWidth(line)));
21
+
22
+ const fmt = (n: number) => {
23
+ if (n < 1_000) return `${n}`;
24
+ if (n < 1_000_000) return `${(n / 1_000).toFixed(1)}k`;
25
+ return `${(n / 1_000_000).toFixed(1)}m`;
26
+ };
27
+
28
+ type Seg = { text: string; priority: number };
29
+
30
+ // Drop optional fields before shortening the last (highest-priority) field.
31
+ function justify(left: string, right: Seg[], sep: string, width: number): string {
32
+ const segs = [...right];
33
+ while (segs.length) {
34
+ const r = segs.map((s) => s.text).join(sep);
35
+ const gap = width - visibleWidth(left) - visibleWidth(r);
36
+ if (gap >= 2) return left + " ".repeat(gap) + r;
37
+ let weakest = 0;
38
+ segs.forEach((s, i) => {
39
+ if (s.priority < segs[weakest].priority) weakest = i;
40
+ });
41
+ if (segs.length === 1) {
42
+ const available = width - visibleWidth(left) - 2;
43
+ if (available > 0) {
44
+ const text = truncateToWidth(r, available, "…");
45
+ return left + " ".repeat(width - visibleWidth(left) - visibleWidth(text)) + text;
46
+ }
47
+ break;
48
+ }
49
+ segs.splice(weakest, 1);
50
+ }
51
+ return truncateToWidth(left, width, "…");
52
+ }
6
53
 
7
54
  export default function (pi: ExtensionAPI) {
55
+ // Jiti can retain imported JSON across reloads; read it once per extension runtime.
56
+ const tipList: { category: string; text: string }[] = JSON.parse(
57
+ readFileSync(new URL("../lib/ui-tips.json", import.meta.url), "utf8")
58
+ );
59
+ if (!Array.isArray(tipList) || tipList.some((tip) =>
60
+ typeof tip?.category !== "string" || !tip.category.trim() ||
61
+ typeof tip?.text !== "string" || !tip.text.trim()
62
+ )) {
63
+ throw new Error("Invalid ui-tips.json: expected tips with non-empty category and text strings.");
64
+ }
65
+
8
66
  let state = "ready";
9
67
  let activeTool = "";
10
68
  let redraw = () => {};
69
+ let remainingTips: typeof tipList = [];
70
+ let agentSetupTips: string[] = [];
11
71
 
12
72
  function update() {
13
73
  redraw();
14
74
  }
15
75
 
16
76
  pi.on("session_start", async (_event, ctx) => {
77
+ state = "ready";
78
+ activeTool = "";
79
+ redraw = () => {};
80
+ if (ctx.mode !== "tui") return;
81
+
17
82
  const project = basename(ctx.cwd);
83
+ agentSetupTips = [];
84
+ try {
85
+ if (ctx.isProjectTrusted() && await hasProjectAgentConfig(ctx.cwd)) {
86
+ const config = await loadAgentConfig(ctx.cwd, getAgentDir(), true);
87
+ for (const key of Object.keys(agentConfigCapabilities) as (keyof typeof agentConfigCapabilities)[]) {
88
+ if (config[key] === undefined) agentSetupTips.push(`Agent config: add ${key} to ${agentConfigCapabilities[key]}.`);
89
+ }
90
+ }
91
+ } catch {
92
+ agentSetupTips = ["Agent config: fix agent.json so PieXis can apply your agent settings."];
93
+ }
94
+ const selectedTips: typeof tipList = [];
95
+ for (let i = 0; i < Math.min(3, tipList.length); i++) {
96
+ if (remainingTips.length === 0) {
97
+ remainingTips = tipList.filter((tip) => !selectedTips.includes(tip));
98
+ }
99
+ const differentCategories = remainingTips.filter(
100
+ (tip) => !selectedTips.some((selected) => selected.category === tip.category)
101
+ );
102
+ const candidates = differentCategories.length > 0 ? differentCategories : remainingTips;
103
+ const tip = candidates[Math.floor(Math.random() * candidates.length)];
104
+ selectedTips.push(remainingTips.splice(remainingTips.indexOf(tip), 1)[0]);
105
+ }
18
106
 
19
107
  // Put project info in the terminal tab instead of wasting TUI rows.
20
108
  ctx.ui.setTitle(`π ${project}`);
@@ -23,82 +111,126 @@ export default function (pi: ExtensionAPI) {
23
111
  // stay attached to the editor forever, making the onboarding copy noisy.
24
112
  ctx.ui.setHeader((_tui, theme) => ({
25
113
  render(width: number) {
26
- const rule = theme.fg(
27
- "dim",
28
- "─".repeat(Math.max(0, Math.min(width, 72)))
29
- );
30
- const lines =
31
- width < 72
32
- ? [
33
- `${theme.fg("accent", "✦")} ${theme.bold("PieXis")} ${theme.fg("dim", project)}`,
34
- `${theme.fg("muted", "Try:")} explain code · find bugs · review diffs · run tests`,
35
- `${theme.fg("dim", "/plan <task> · Alt+M modes · /help for commands")}`
36
- ]
37
- : [
38
- `${theme.fg("accent", "✦")} ${theme.bold("PieXis")} ${theme.fg("dim", "permission-aware coding workspace")} ${theme.fg("muted", project)}`,
39
- rule,
40
- `${theme.fg("muted", "Try:")} explain code · find bugs · review diffs · trace behavior ${theme.fg("muted", "Build:")} edit · refactor · test`,
41
- `${theme.fg("muted", "Run:")} tests · lint · typecheck · Git ${theme.fg("muted", "Plan:")} /plan <task> · Alt+M modes ${theme.fg("dim", "Mention files, constraints, and expected behavior.")}`
42
- ];
43
- return lines.map((line) => truncateToWidth(line, width, ""));
114
+ if (width <= 0) return [];
115
+
116
+ const left = width < WORDMARK_WIDTH
117
+ ? [`${theme.fg("accent", "✦")} ${theme.bold("PieXis")}`]
118
+ : WORDMARK.map((line) => theme.fg("accent", line));
119
+
120
+ const tips = [
121
+ theme.fg("accent", theme.bold("Get started with PieXis")),
122
+ theme.fg("muted", "Questions, skills, prompts & stats."),
123
+ ...agentSetupTips.map((tip) => theme.fg("warning", tip)),
124
+ ...selectedTips.map((tip) =>
125
+ `${theme.fg("accent", `[${tip.category}]`)} ${theme.fg("muted", tip.text)}`
126
+ ),
127
+ theme.fg("dim", "/help · /summary · /commit")
128
+ ];
129
+ const tipWidth = width - WORDMARK_WIDTH - 3;
130
+ if (tipWidth >= 32) {
131
+ const right = tips.flatMap((line) => wrapTextWithAnsi(line, tipWidth));
132
+ return Array.from({ length: Math.max(left.length, right.length) }, (_, index) => {
133
+ const line = truncateToWidth(left[index] ?? "", WORDMARK_WIDTH, "");
134
+ const padding = " ".repeat(WORDMARK_WIDTH - visibleWidth(line) + 3);
135
+ return line + padding + (right[index] ?? "");
136
+ });
137
+ }
138
+
139
+ return [
140
+ ...left.map((line) => truncateToWidth(line, width, "")),
141
+ "",
142
+ ...tips.flatMap((line) => wrapTextWithAnsi(line, width))
143
+ ];
44
144
  },
45
145
  invalidate() {}
46
146
  }));
47
147
 
48
148
  ctx.ui.setFooter((tui, theme, footerData) => {
49
149
  redraw = () => tui.requestRender();
150
+ const unsubscribe = footerData.onBranchChange(() => tui.requestRender());
50
151
 
51
- const unsubscribe = footerData.onBranchChange(() => {
52
- tui.requestRender();
53
- });
152
+ // Spinner animation, only while busy.
153
+ const timer = setInterval(() => {
154
+ if (state !== "ready") tui.requestRender();
155
+ }, 100);
156
+ timer.unref?.();
54
157
 
55
158
  return {
56
159
  invalidate() {},
57
160
 
58
161
  render(width: number) {
59
- const branch = footerData.getGitBranch() ?? "—";
162
+ if (width <= 0) return [];
163
+ const dim = (s: string) => theme.fg("dim", s);
164
+ const muted = (s: string) => theme.fg("muted", s);
165
+ const sep = dim(" · ");
166
+ const prompts = ctx.sessionManager.getEntries().filter(
167
+ (entry) => entry.type === "message" && entry.message.role === "user"
168
+ ).length;
60
169
 
61
- const model = ctx.model?.id?.replace(/^gpt-/, "") ?? "—";
62
- const effort = ctx.thinkingLevel ?? "off";
63
-
64
- const usage = ctx.getContextUsage();
65
-
66
- const context =
67
- usage?.percent != null
68
- ? `ctx ${Math.round(usage.percent)}%`
69
- : "ctx —";
170
+ let input = 0, output = 0, cacheRead = 0, cacheWrite = 0, cost = 0;
171
+ for (const entry of ctx.sessionManager.getBranch()) {
172
+ if (entry.type !== "message" || entry.message.role !== "assistant") continue;
173
+ const u = entry.message.usage;
174
+ if (!u) continue;
175
+ input += u.input;
176
+ output += u.output;
177
+ cacheRead += u.cacheRead;
178
+ cacheWrite += u.cacheWrite;
179
+ cost += u.cost?.total ?? 0;
180
+ }
70
181
 
71
- let activity: string;
182
+ const frame = SPINNER[Math.floor(Date.now() / 100) % SPINNER.length];
183
+ const activity = state === "tool" ? `${frame} ${activeTool}`
184
+ : state === "thinking" ? `${frame} thinking` : "● ready";
185
+ const agent = footerData.getExtensionStatuses?.().get("piexis-agent");
186
+ const branch = footerData.getGitBranch();
187
+ // Reserve a small model slot; long tool names must not displace the profile.
188
+ const modelReserve = width >= 32 ? 9 : 0;
189
+ const profile = agent && visibleWidth(agent) + 6 + modelReserve <= width
190
+ ? theme.fg("accent", agent) + sep : "";
191
+ const activityWidth = Math.max(1, Math.min(24, width - visibleWidth(profile) - modelReserve));
192
+ const left1 = profile + theme.fg(state === "ready" ? "success" : "accent",
193
+ truncateToWidth(activity, activityWidth, "…"));
194
+ const row1 = justify(left1, [
195
+ { text: ctx.model?.id ?? "—", priority: 5 },
196
+ { text: muted(`think ${pi.getThinkingLevel()}`), priority: 4 },
197
+ ...(branch ? [{ text: dim(`⎇ ${truncateToWidth(branch, Math.max(8, Math.floor(width / 4)), "…")}`), priority: 3 }] : []),
198
+ { text: dim(`${prompts} prompts`), priority: 2 },
199
+ ...(agent ? [{ text: dim("[Alt+A]"), priority: 1 }] : [])
200
+ ], sep, width);
72
201
 
73
- if (state === "tool") {
74
- activity = theme.fg("accent", `◆ ${activeTool}`);
75
- } else if (state === "thinking") {
76
- activity = theme.fg("accent", "◆ thinking");
77
- } else {
78
- activity = theme.fg("success", "● ready");
79
- }
202
+ const usage = ctx.getContextUsage();
203
+ const pct = usage?.percent ?? null;
204
+ const tone = pct == null ? "dim" : pct >= 90 ? "error" : pct >= 70 ? "warning" : "success";
205
+ const pctText = pct == null ? "—" : `${Math.round(pct)}%`;
206
+ const costText = `$${cost < 0.01 ? cost.toFixed(4) : cost.toFixed(2)}`;
207
+ const barWidth = Math.min(12, Math.max(0,
208
+ width - visibleWidth(`ctx ${pctText}`) - visibleWidth(costText) - 2));
209
+ const filled = pct == null ? 0 : Math.round((Math.min(100, Math.max(0, pct)) / 100) * barWidth);
210
+ const bar = theme.fg(tone, "━".repeat(filled)) + dim("─".repeat(barWidth - filled));
211
+ const cap = usage
212
+ ? `${usage.tokens == null ? "—" : fmt(usage.tokens)}/${fmt(usage.contextWindow)}`
213
+ : "—/—";
214
+ const context = `${muted("ctx")} ${barWidth ? bar + " " : ""}${theme.fg(tone, pctText)}`;
215
+ const tokens = muted(`↑${fmt(input)} ↓${fmt(output)}`);
216
+ const promptTotal = input + cacheRead + cacheWrite;
217
+ const hit = promptTotal > 0 ? Math.round((cacheRead / promptTotal) * 100) : null;
218
+ // Add capacity only when it leaves room for the basic token totals and cost.
219
+ const showCapacity = visibleWidth(context + " " + cap + tokens + costText) + 7 <= width;
220
+ const left2 = context + (showCapacity ? " " + dim(cap) : "");
221
+ const row2 = justify(left2, [
222
+ { text: tokens, priority: 2 },
223
+ ...(hit == null ? [] : [{ text: dim(`cache ${hit}%`), priority: 1 }]),
224
+ { text: muted(costText), priority: 3 }
225
+ ], sep, width);
80
226
 
81
- const statuses = [...footerData.getExtensionStatuses().values()];
82
- // Keep this tolerant of theme/icon changes in the permission extension.
83
- const modeStatus =
84
- statuses.find((status) =>
85
- /(?:manual|accept edits|plan|auto|don't ask|bypass permissions).*on/i.test(
86
- status
87
- )
88
- ) ?? "";
89
- const line =
90
- `${activity}` +
91
- (modeStatus ? theme.fg("accent", ` │ ${modeStatus}`) : "") +
92
- theme.fg(
93
- "dim",
94
- ` │ ${branch}` + ` │ ${model}` + ` │ ${context}`
95
- );
96
-
97
- return [truncateToWidth(line, width, "")];
227
+ return [row1, row2];
98
228
  },
99
229
 
100
230
  dispose() {
231
+ clearInterval(timer);
101
232
  unsubscribe();
233
+ redraw = () => {};
102
234
  }
103
235
  };
104
236
  });
@@ -112,6 +244,20 @@ export default function (pi: ExtensionAPI) {
112
244
  update();
113
245
  });
114
246
 
247
+ pi.on("turn_start", async () => {
248
+ state = "thinking";
249
+ activeTool = "";
250
+ update();
251
+ });
252
+
253
+ pi.on("message_end", async () => {
254
+ update();
255
+ });
256
+
257
+ pi.on("session_compact", async () => {
258
+ update();
259
+ });
260
+
115
261
  pi.on("tool_call", async (event) => {
116
262
  state = "tool";
117
263
  activeTool = event.toolName;
@@ -0,0 +1,55 @@
1
+ export const agentNames = ["plan", "orchestrator", "debug", "ask", "code"] as const;
2
+ export type AgentName = typeof agentNames[number];
3
+ export const effortLevels = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
4
+ export type Effort = typeof effortLevels[number];
5
+
6
+ export const agentConfigSchemaUrl = "https://raw.githubusercontent.com/VarunGaikwad/piexis/main/schemas/agent.schema.json";
7
+
8
+ export interface AgentConfig {
9
+ "$schema"?: string;
10
+ model?: string;
11
+ small_model?: string;
12
+ subagent_model?: string;
13
+ delegate_exploration?: boolean;
14
+ subagent_variant_overrides?: Partial<Record<AgentName, { model?: string; effort?: Effort }>>;
15
+ }
16
+
17
+ /**
18
+ * Static values written to a newly created .pi/agent.json.
19
+ * Add new static AgentConfig properties here so project setup includes them.
20
+ */
21
+ export const initialAgentConfigDefaults = {
22
+ "$schema": agentConfigSchemaUrl,
23
+ delegate_exploration: false,
24
+ } satisfies Pick<AgentConfig, "$schema" | "delegate_exploration">;
25
+
26
+ /** Header copy for incomplete project agent configuration. */
27
+ export type AgentConfigSetting = Exclude<keyof AgentConfig, "$schema">;
28
+
29
+ export const agentConfigCapabilities = {
30
+ model: "choose the default main model",
31
+ small_model: "use a cheaper model for eligible small tasks",
32
+ subagent_model: "choose the normal model for delegated work",
33
+ delegate_exploration: "opt in to cheap, read-only codebase exploration",
34
+ subagent_variant_overrides: "customize model and effort for each agent profile",
35
+ } satisfies Record<AgentConfigSetting, string>;
36
+
37
+ export function createInitialAgentConfig(selected: string, inherited: AgentConfig): AgentConfig {
38
+ const efforts: Record<AgentName, Effort> = { plan: "high", orchestrator: "high", debug: "high", ask: "low", code: "medium" };
39
+ const overrides: NonNullable<AgentConfig["subagent_variant_overrides"]> = {};
40
+ for (const name of agentNames) {
41
+ overrides[name] = {
42
+ model: inherited.subagent_model ?? selected,
43
+ effort: efforts[name],
44
+ ...inherited.subagent_variant_overrides?.[name],
45
+ };
46
+ }
47
+ return {
48
+ "$schema": initialAgentConfigDefaults.$schema,
49
+ model: selected,
50
+ small_model: inherited.small_model ?? selected,
51
+ subagent_model: inherited.subagent_model ?? selected,
52
+ delegate_exploration: inherited.delegate_exploration ?? initialAgentConfigDefaults.delegate_exploration,
53
+ subagent_variant_overrides: overrides,
54
+ };
55
+ }
package/lib/agents.ts ADDED
@@ -0,0 +1,208 @@
1
+ import { link, lstat, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
2
+ import { basename, join } from "node:path";
3
+ import { randomUUID } from "node:crypto";
4
+ import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
5
+ import { agentNames, createInitialAgentConfig, effortLevels, type AgentConfig, type AgentName, type Effort } from "./agent-config.ts";
6
+
7
+ export { agentNames, type AgentConfig, type AgentName, effortLevels, type Effort } from "./agent-config.ts";
8
+
9
+ export const profiles: Record<AgentName, string> = {
10
+ plan: "Explore the request and relevant code, clarify important unknowns, and return an actionable plan with acceptance checks. Do not implement or delegate implementation. Stop after the plan.",
11
+ orchestrator: "Break work into focused tasks and delegate to plan, debug, ask, or code using subagent. Include all necessary context in each task; children do not inherit the conversation. Keep overlapping file changes sequential. Inspect results, resolve disagreements, and report verification evidence. Do not delegate the same orchestration task recursively.",
12
+ debug: "Reproduce or establish evidence for the failure, trace its cause before editing, and make the smallest justified fix. Verify with a regression check. Separate confirmed facts from hypotheses and report any unexecuted checks.",
13
+ ask: "Answer the user's question using relevant code or documentation as evidence. Do not change files or delegate changes. When context is missing, ask rather than invent facts. Stop when the question is answered.",
14
+ code: "Implement the requested scope using existing repository patterns. Preserve unrelated changes, add focused coverage for behavior changes, run relevant checks, and report results. Stop when acceptance conditions are met.",
15
+ };
16
+
17
+ export function isAgentName(value: unknown): value is AgentName {
18
+ return typeof value === "string" && agentNames.includes(value as AgentName);
19
+ }
20
+
21
+ function object(value: unknown, label: string): Record<string, unknown> {
22
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object`);
23
+ return value as Record<string, unknown>;
24
+ }
25
+
26
+ function model(value: unknown, label: string): string {
27
+ if (typeof value !== "string" || !/^[^\s/]+\/\S+$/.test(value)) {
28
+ throw new Error(`${label} must be an exact provider/model ID`);
29
+ }
30
+ return value;
31
+ }
32
+
33
+ export function parseAgentConfig(value: unknown): AgentConfig {
34
+ const source = object(value, "Agent configuration");
35
+ const result: AgentConfig = {};
36
+ for (const key of Object.keys(source)) {
37
+ if (!["$schema", "model", "small_model", "subagent_model", "delegate_exploration", "subagent_variant_overrides"].includes(key)) {
38
+ throw new Error(`Unknown agent configuration key: ${key}`);
39
+ }
40
+ }
41
+ if (source.$schema !== undefined) {
42
+ if (typeof source.$schema !== "string") throw new Error("$schema must be a string");
43
+ result.$schema = source.$schema;
44
+ }
45
+ for (const key of ["model", "small_model", "subagent_model"] as const) {
46
+ if (source[key] !== undefined) result[key] = model(source[key], key);
47
+ }
48
+ if (source.delegate_exploration !== undefined) {
49
+ if (typeof source.delegate_exploration !== "boolean") throw new Error("delegate_exploration must be a boolean");
50
+ result.delegate_exploration = source.delegate_exploration;
51
+ }
52
+ if (source.subagent_variant_overrides !== undefined) {
53
+ result.subagent_variant_overrides = {};
54
+ for (const [name, value] of Object.entries(object(source.subagent_variant_overrides, "subagent_variant_overrides"))) {
55
+ if (!isAgentName(name)) throw new Error(`Unknown agent variant: ${name}`);
56
+ const override = object(value, `Override ${name}`);
57
+ for (const key of Object.keys(override)) {
58
+ if (key !== "model" && key !== "effort") throw new Error(`Unknown override key: ${name}.${key}`);
59
+ }
60
+ const parsed: { model?: string; effort?: Effort } = {};
61
+ if (override.model !== undefined) parsed.model = model(override.model, `${name}.model`);
62
+ if (override.effort !== undefined) {
63
+ if (!effortLevels.includes(override.effort as Effort)) throw new Error(`Invalid effort for ${name}`);
64
+ parsed.effort = override.effort as Effort;
65
+ }
66
+ result.subagent_variant_overrides[name] = parsed;
67
+ }
68
+ }
69
+ return result;
70
+ }
71
+
72
+ export async function loadAgentConfig(cwd: string, agentDir: string, trusted: boolean): Promise<AgentConfig> {
73
+ async function load(directory: string): Promise<AgentConfig> {
74
+ for (const name of ["agent.json", "agents.json"]) {
75
+ const path = join(directory, name);
76
+ try { return parseAgentConfig(JSON.parse(await readFile(path, "utf8"))); }
77
+ catch (error) {
78
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") continue;
79
+ throw new Error(`${path}: ${error instanceof Error ? error.message : String(error)}`);
80
+ }
81
+ }
82
+ return {};
83
+ }
84
+ const global = await load(agentDir);
85
+ const project = trusted ? await load(join(cwd, ".pi")) : {};
86
+ const overrides = { ...global.subagent_variant_overrides };
87
+ for (const name of agentNames) {
88
+ if (project.subagent_variant_overrides?.[name]) {
89
+ overrides[name] = { ...overrides[name], ...project.subagent_variant_overrides[name] };
90
+ }
91
+ }
92
+ return { ...global, ...project, subagent_variant_overrides: overrides };
93
+ }
94
+
95
+ export async function hasProjectAgentConfig(cwd: string): Promise<boolean> {
96
+ for (const name of ["agent.json", "agents.json"]) {
97
+ try { await lstat(join(cwd, ".pi", name)); return true; }
98
+ catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; }
99
+ }
100
+ return false;
101
+ }
102
+
103
+ /** Create an opt-in project scaffold atomically, never replace existing or legacy configuration. */
104
+ export async function initializeAgentConfig(cwd: string, agentDir: string, defaultModel: string | undefined, signal?: AbortSignal): Promise<boolean> {
105
+ const directory = join(cwd, ".pi");
106
+ const path = join(directory, "agent.json");
107
+ return withFileMutationQueue(path, async () => {
108
+ signal?.throwIfAborted();
109
+ if (await hasProjectAgentConfig(cwd)) return false;
110
+ if (!defaultModel) throw new Error("No active model; select a model and create .pi/agent.json manually.");
111
+ const inherited = await loadAgentConfig(cwd, agentDir, false);
112
+ const selected = inherited.model ?? model(defaultModel, "Default model");
113
+ const config = createInitialAgentConfig(selected, inherited);
114
+ signal?.throwIfAborted();
115
+ await mkdir(directory, { recursive: true });
116
+ const temporary = join(directory, `.agent.json.piexis-${randomUUID()}.tmp`);
117
+ try {
118
+ await writeFile(temporary, `${JSON.stringify(config, null, 2)}\n`, { flag: "wx", mode: 0o600 });
119
+ signal?.throwIfAborted();
120
+ if (await hasProjectAgentConfig(cwd)) return false;
121
+ // Unlike rename, link fails if another writer has already created the destination.
122
+ try { await link(temporary, path); }
123
+ catch (error) { if ((error as NodeJS.ErrnoException).code === "EEXIST") return false; throw error; }
124
+ return true;
125
+ } finally { await rm(temporary, { force: true }); }
126
+ });
127
+ }
128
+
129
+ export async function completeProjectAgentConfig(cwd: string, agentDir: string, defaultModel: string | undefined, signal?: AbortSignal): Promise<"created" | "updated" | "complete"> {
130
+ const directory = join(cwd, ".pi");
131
+ let path: string | undefined;
132
+ for (const name of ["agent.json", "agents.json"]) {
133
+ const candidate = join(directory, name);
134
+ try { await lstat(candidate); path = candidate; break; }
135
+ catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; }
136
+ }
137
+ if (!path) {
138
+ if (!await initializeAgentConfig(cwd, agentDir, defaultModel, signal)) return "complete";
139
+ return "created";
140
+ }
141
+ return withFileMutationQueue(path, async () => {
142
+ signal?.throwIfAborted();
143
+ if (!(await lstat(path!)).isFile()) throw new Error(`${path} must be a regular file`);
144
+ const source = parseAgentConfig(JSON.parse(await readFile(path!, "utf8")));
145
+ const inherited = await loadAgentConfig(cwd, agentDir, false);
146
+ const selected = source.model ?? inherited.model ?? (defaultModel ? model(defaultModel, "Default model") : undefined);
147
+ if (!selected) throw new Error("No active model; select a model and configure .pi/agent.json manually.");
148
+ const defaults = createInitialAgentConfig(selected, inherited);
149
+ const overrides: NonNullable<AgentConfig["subagent_variant_overrides"]> = {};
150
+ let changed = Object.keys(defaults).some((key) => source[key as keyof AgentConfig] === undefined);
151
+ for (const name of agentNames) {
152
+ const expected = defaults.subagent_variant_overrides![name]!;
153
+ const actual = source.subagent_variant_overrides?.[name];
154
+ if (!actual?.model || actual.effort === undefined) changed = true;
155
+ overrides[name] = { ...expected, ...actual };
156
+ }
157
+ if (!changed) return "complete";
158
+ const config: AgentConfig = { ...defaults, ...source, subagent_variant_overrides: overrides };
159
+ const temporary = join(directory, `.${basename(path!)}.piexis-${randomUUID()}.tmp`);
160
+ try {
161
+ await writeFile(temporary, `${JSON.stringify(config, null, 2)}\n`, { flag: "wx", mode: 0o600 });
162
+ signal?.throwIfAborted();
163
+ await rename(temporary, path!);
164
+ return "updated";
165
+ } finally { await rm(temporary, { force: true }); }
166
+ });
167
+ }
168
+
169
+ export function isSmallTask(agent: AgentName, task: string): boolean {
170
+ // shortcut: conservative text heuristic, replace with evaluated routing if real tasks outgrow it.
171
+ if (task.length > 400 || task.split(/\s+/).length > 65) return false;
172
+ if (/\b(architect\w*|design|implement|investigate|debug|refactor|migrat\w*|security|race|performance|across|multiple|entire|comprehensive)\b/i.test(task)) return false;
173
+ if (agent === "ask") return /^\s*(find|locate|list|what (is|does|file)|where (is|are)|explain|summari[sz]e)\b/i.test(task);
174
+ if (agent === "code") return /^\s*(fix (a |the )?typo|correct (a |the )?typo|rename (a |the )?(variable|symbol)|update (a |the )?(comment|label))\b/i.test(task);
175
+ return false;
176
+ }
177
+
178
+ export function routeSubagent(config: AgentConfig, agent: AgentName, task: string, currentModel: string, currentEffort: Effort, exploration = false) {
179
+ if (exploration) {
180
+ if (!config.small_model) throw new Error("Exploration delegation requires small_model in agent.json");
181
+ return { model: config.small_model, effort: "low" as Effort, reason: "configured exploration small model" };
182
+ }
183
+ const override = config.subagent_variant_overrides?.[agent];
184
+ const small = Boolean(config.small_model && isSmallTask(agent, task) && !override?.model);
185
+ return {
186
+ model: override?.model ?? (small ? config.small_model! : config.subagent_model ?? currentModel),
187
+ effort: override?.effort ?? (small ? "low" : currentEffort),
188
+ reason: override?.model ? "variant override" : small ? "automatic small task" : "default subagent",
189
+ };
190
+ }
191
+
192
+ export function subagentTools(agent: AgentName, active: string[], depth: number, allowExplorationDelegation = false): string[] {
193
+ if (agent === "plan" || agent === "ask") {
194
+ return active.filter((name) => ["read", "grep", "find", "ls"].includes(name) || (allowExplorationDelegation && depth < 2 && name === "subagent"));
195
+ }
196
+ return active.filter((name) => name !== "AskQuestion" && (name !== "subagent" || depth < 2));
197
+ }
198
+
199
+ export function childResourceArgs(argv: string[]): string[] {
200
+ const result: string[] = [];
201
+ const values = ["--extension", "-e", "--skill", "--prompt-template", "--system-prompt", "--append-system-prompt"];
202
+ const flags = ["--no-extensions", "-ne", "--no-skills", "-ns", "--no-prompt-templates", "-np", "--no-context-files", "-nc", "--no-mcp", "--offline"];
203
+ for (let i = 0; i < argv.length && argv[i] !== "--"; i++) {
204
+ if (values.includes(argv[i]) && argv[i + 1] !== undefined) result.push(argv[i], argv[++i]);
205
+ else if (flags.includes(argv[i])) result.push(argv[i]);
206
+ }
207
+ return result;
208
+ }