@ilya-lesikov/pi-pi 0.5.0 → 0.7.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 (77) hide show
  1. package/3p/pi-ask-user/index.ts +65 -49
  2. package/3p/pi-subagents/src/agent-manager.ts +8 -0
  3. package/3p/pi-subagents/src/agent-runner.ts +112 -19
  4. package/3p/pi-subagents/src/index.ts +3 -0
  5. package/extensions/orchestrator/agents/brainstorm-reviewer.ts +32 -19
  6. package/extensions/orchestrator/agents/code-reviewer.ts +31 -17
  7. package/extensions/orchestrator/agents/constraints.ts +55 -0
  8. package/extensions/orchestrator/agents/explore.ts +13 -13
  9. package/extensions/orchestrator/agents/librarian.ts +12 -9
  10. package/extensions/orchestrator/agents/plan-reviewer.ts +31 -19
  11. package/extensions/orchestrator/agents/planner.ts +28 -23
  12. package/extensions/orchestrator/agents/registry.ts +2 -1
  13. package/extensions/orchestrator/agents/repo-context.ts +11 -0
  14. package/extensions/orchestrator/agents/task.ts +14 -11
  15. package/extensions/orchestrator/agents/tool-routing.ts +17 -32
  16. package/extensions/orchestrator/ast-search.ts +2 -1
  17. package/extensions/orchestrator/cbm.test.ts +35 -0
  18. package/extensions/orchestrator/cbm.ts +43 -13
  19. package/extensions/orchestrator/command-handlers.test.ts +390 -19
  20. package/extensions/orchestrator/command-handlers.ts +68 -28
  21. package/extensions/orchestrator/commands.test.ts +255 -2
  22. package/extensions/orchestrator/commands.ts +108 -10
  23. package/extensions/orchestrator/config.test.ts +289 -68
  24. package/extensions/orchestrator/config.ts +631 -121
  25. package/extensions/orchestrator/context.test.ts +177 -10
  26. package/extensions/orchestrator/context.ts +115 -14
  27. package/extensions/orchestrator/custom-footer.ts +7 -3
  28. package/extensions/orchestrator/doctor.test.ts +561 -0
  29. package/extensions/orchestrator/doctor.ts +702 -0
  30. package/extensions/orchestrator/event-handlers.test.ts +84 -22
  31. package/extensions/orchestrator/event-handlers.ts +1220 -343
  32. package/extensions/orchestrator/exa.test.ts +46 -0
  33. package/extensions/orchestrator/exa.ts +16 -10
  34. package/extensions/orchestrator/flant-infra.test.ts +511 -0
  35. package/extensions/orchestrator/flant-infra.ts +390 -49
  36. package/extensions/orchestrator/index.ts +13 -2
  37. package/extensions/orchestrator/integration.test.ts +3065 -118
  38. package/extensions/orchestrator/log.test.ts +219 -0
  39. package/extensions/orchestrator/log.ts +153 -0
  40. package/extensions/orchestrator/model-registry.test.ts +302 -0
  41. package/extensions/orchestrator/model-registry.ts +298 -0
  42. package/extensions/orchestrator/model-version.test.ts +27 -0
  43. package/extensions/orchestrator/model-version.ts +19 -0
  44. package/extensions/orchestrator/orchestrator.test.ts +206 -56
  45. package/extensions/orchestrator/orchestrator.ts +298 -140
  46. package/extensions/orchestrator/phases/brainstorm.test.ts +10 -7
  47. package/extensions/orchestrator/phases/brainstorm.ts +41 -36
  48. package/extensions/orchestrator/phases/implementation.ts +7 -11
  49. package/extensions/orchestrator/phases/machine.test.ts +27 -8
  50. package/extensions/orchestrator/phases/machine.ts +13 -0
  51. package/extensions/orchestrator/phases/planning.ts +57 -31
  52. package/extensions/orchestrator/phases/review-task.ts +3 -7
  53. package/extensions/orchestrator/phases/review.test.ts +54 -0
  54. package/extensions/orchestrator/phases/review.ts +89 -48
  55. package/extensions/orchestrator/phases/spawn-blocking.test.ts +69 -0
  56. package/extensions/orchestrator/phases/verdict.test.ts +139 -0
  57. package/extensions/orchestrator/phases/verdict.ts +82 -0
  58. package/extensions/orchestrator/plannotator.test.ts +85 -0
  59. package/extensions/orchestrator/plannotator.ts +24 -6
  60. package/extensions/orchestrator/pp-menu.test.ts +207 -0
  61. package/extensions/orchestrator/pp-menu.ts +2741 -373
  62. package/extensions/orchestrator/repo-utils.test.ts +151 -0
  63. package/extensions/orchestrator/repo-utils.ts +67 -0
  64. package/extensions/orchestrator/spawn-cleanup.test.ts +57 -0
  65. package/extensions/orchestrator/spawn-cleanup.ts +35 -0
  66. package/extensions/orchestrator/state.test.ts +76 -6
  67. package/extensions/orchestrator/state.ts +128 -44
  68. package/extensions/orchestrator/subagent-session-marker.test.ts +36 -0
  69. package/extensions/orchestrator/test-helpers.ts +229 -0
  70. package/extensions/orchestrator/tracer.test.ts +132 -0
  71. package/extensions/orchestrator/tracer.ts +127 -0
  72. package/extensions/orchestrator/transition-controller.test.ts +207 -0
  73. package/extensions/orchestrator/transition-controller.ts +259 -0
  74. package/extensions/orchestrator/usage-tracker.test.ts +563 -0
  75. package/extensions/orchestrator/usage-tracker.ts +96 -12
  76. package/extensions/orchestrator/validate-artifacts.test.ts +83 -1
  77. package/package.json +2 -1
@@ -1,23 +1,41 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
+ import { SUB_MODEL_PREFIX, SUB_PROVIDER } from "./flant-infra.js";
5
+
6
+ /**
7
+ * A usage unit is subscription-routed (flat-rate personal Claude subscription,
8
+ * not per-token billed) when its main-turn provider is the subscription
9
+ * provider, or when the registered model id carries the `sub/` prefix. The
10
+ * prefix is the only signal available for subagents (subagents:completed has no
11
+ * provider field), so we always check it in addition to the provider.
12
+ */
13
+ export function isSubscriptionRouted(modelId?: string, provider?: string): boolean {
14
+ if (provider === SUB_PROVIDER) return true;
15
+ return typeof modelId === "string" && modelId.startsWith(SUB_MODEL_PREFIX);
16
+ }
4
17
 
5
18
  export interface ModelUsage {
6
19
  inputTokens: number;
7
20
  outputTokens: number;
8
21
  cacheReadTokens: number;
9
22
  cacheWriteTokens: number;
23
+ cacheSupported: boolean;
10
24
  turns: number;
25
+ /** Flat-rate personal subscription: dollars are excluded from cost totals, tokens are not. */
26
+ subscription: boolean;
11
27
  }
12
28
 
13
29
  export interface UsageTracker {
14
- recordTurn(modelId: string, provider: string, input: number, output: number, cacheRead: number, cacheWrite: number, cost: number): void;
30
+ recordTurn(modelId: string, provider: string, input: number, output: number, cacheRead: number, cacheWrite: number, cost: number, cacheSupported?: boolean): void;
15
31
  recordSubagentCompletion(tokens: { input?: number; output?: number; total?: number }, cost?: number, meta?: { description?: string; agentType?: string; modelId?: string; durationMs?: number; toolUses?: number }): void;
16
32
  loadFromSummary(summary: Record<string, unknown>): void;
17
33
  getTotalInputTokens(): number;
18
34
  getTotalOutputTokens(): number;
19
35
  getTotalCacheReadTokens(): number;
20
36
  getTotalCacheWriteTokens(): number;
37
+ /** Total input the model actually processed: uncached input + cache read + cache write (main + subagents). */
38
+ getTotalProcessedInputTokens(): number;
21
39
  getTotalCost(): number;
22
40
  getMainInputTokens(): number;
23
41
  getMainOutputTokens(): number;
@@ -25,6 +43,7 @@ export interface UsageTracker {
25
43
  getMainCacheWriteTokens(): number;
26
44
  getMainCost(): number;
27
45
  getCacheHitRate(): number;
46
+ isCacheSupported(): boolean;
28
47
  getPerModelUsage(): Record<string, ModelUsage>;
29
48
  getSubagentTotals(): { inputTokens: number; outputTokens: number; cost: number };
30
49
  getSubagentList(): SubagentUsage[];
@@ -40,9 +59,12 @@ export interface SubagentUsage {
40
59
  outputTokens: number;
41
60
  cacheReadTokens: number;
42
61
  cacheWriteTokens: number;
62
+ cacheSupported: boolean;
43
63
  cost: number;
44
64
  durationMs: number;
45
65
  toolUses: number;
66
+ /** Flat-rate personal subscription: dollars are excluded from cost totals, tokens are not. */
67
+ subscription: boolean;
46
68
  }
47
69
 
48
70
  interface TrackerState {
@@ -55,6 +77,8 @@ interface TrackerState {
55
77
  totalTurns: number;
56
78
  subagentInputTokens: number;
57
79
  subagentOutputTokens: number;
80
+ subagentCacheReadTokens: number;
81
+ subagentCacheWriteTokens: number;
58
82
  subagentCost: number;
59
83
  models: Map<string, ModelUsage>;
60
84
  subagents: SubagentUsage[];
@@ -86,6 +110,8 @@ function createInitialState(): TrackerState {
86
110
  totalTurns: 0,
87
111
  subagentInputTokens: 0,
88
112
  subagentOutputTokens: 0,
113
+ subagentCacheReadTokens: 0,
114
+ subagentCacheWriteTokens: 0,
89
115
  subagentCost: 0,
90
116
  models: new Map<string, ModelUsage>(),
91
117
  subagents: [],
@@ -96,12 +122,15 @@ export function createUsageTracker(): UsageTracker {
96
122
  const state = createInitialState();
97
123
 
98
124
  return {
99
- recordTurn(modelId: string, _provider: string, input: number, output: number, cacheRead: number, cacheWrite: number, cost: number): void {
125
+ recordTurn(modelId: string, provider: string, input: number, output: number, cacheRead: number, cacheWrite: number, cost: number, cacheSupported?: boolean): void {
100
126
  const safeInput = toFiniteNumber(input);
101
127
  const safeOutput = toFiniteNumber(output);
102
128
  const safeCacheRead = toFiniteNumber(cacheRead);
103
129
  const safeCacheWrite = toFiniteNumber(cacheWrite);
104
- const safeCost = toFiniteNumber(cost);
130
+ const subscription = isSubscriptionRouted(modelId, provider);
131
+ // Subscription-routed turns are flat-rate: keep the tokens but exclude the
132
+ // fictitious per-token dollars so totals stay paid-only by construction.
133
+ const safeCost = subscription ? 0 : toFiniteNumber(cost);
105
134
 
106
135
  state.totalInputTokens += safeInput;
107
136
  state.totalOutputTokens += safeOutput;
@@ -110,19 +139,27 @@ export function createUsageTracker(): UsageTracker {
110
139
  state.totalCost += safeCost;
111
140
  state.totalTurns += 1;
112
141
 
113
- const key = modelId || "unknown-model";
142
+ // Key subscription turns under the sub/ prefix even when detected only by
143
+ // provider (bare model id), so a paid and a subscription turn for the same
144
+ // underlying model never share a row and the paid dollars stay visible.
145
+ const baseKey = modelId || "unknown-model";
146
+ const key = subscription && !baseKey.startsWith(SUB_MODEL_PREFIX) ? `${SUB_MODEL_PREFIX}${baseKey}` : baseKey;
114
147
  const usage = state.models.get(key) ?? {
115
148
  inputTokens: 0,
116
149
  outputTokens: 0,
117
150
  cacheReadTokens: 0,
118
151
  cacheWriteTokens: 0,
152
+ cacheSupported: false,
119
153
  turns: 0,
154
+ subscription: false,
120
155
  };
121
156
 
122
157
  usage.inputTokens += safeInput;
123
158
  usage.outputTokens += safeOutput;
124
159
  usage.cacheReadTokens += safeCacheRead;
125
160
  usage.cacheWriteTokens += safeCacheWrite;
161
+ if (cacheSupported) usage.cacheSupported = true;
162
+ if (subscription) usage.subscription = true;
126
163
  usage.turns += 1;
127
164
  state.models.set(key, usage);
128
165
  },
@@ -137,14 +174,21 @@ export function createUsageTracker(): UsageTracker {
137
174
  const safeTotal = toFiniteNumber(tokens.total);
138
175
  const safeCacheRead = toFiniteNumber(tokens.cacheRead);
139
176
  const safeCacheWrite = toFiniteNumber(tokens.cacheWrite);
140
- const safeCost = toFiniteNumber(tokens.cost ?? cost);
177
+ // Subagents carry no provider field, so detect subscription routing from
178
+ // the registered model id prefix. Zero the dollars when subscription.
179
+ const subscription = isSubscriptionRouted(meta?.modelId);
180
+ const safeCost = subscription ? 0 : toFiniteNumber(tokens.cost ?? cost);
141
181
 
142
182
  const effectiveInput = safeInput === 0 && safeOutput === 0 ? safeTotal : safeInput;
143
183
 
144
184
  state.subagentInputTokens += effectiveInput;
145
185
  state.subagentOutputTokens += safeOutput;
186
+ state.subagentCacheReadTokens += safeCacheRead;
187
+ state.subagentCacheWriteTokens += safeCacheWrite;
146
188
  state.subagentCost += safeCost;
147
189
 
190
+ const cacheSupported = typeof tokens.cacheRead === "number" || typeof tokens.cacheWrite === "number";
191
+
148
192
  state.subagents.push({
149
193
  description: meta?.description ?? "unknown",
150
194
  agentType: meta?.agentType ?? "unknown",
@@ -153,9 +197,11 @@ export function createUsageTracker(): UsageTracker {
153
197
  outputTokens: safeOutput,
154
198
  cacheReadTokens: safeCacheRead,
155
199
  cacheWriteTokens: safeCacheWrite,
200
+ cacheSupported,
156
201
  cost: safeCost,
157
202
  durationMs: toFiniteNumber(meta?.durationMs),
158
203
  toolUses: toFiniteNumber(meta?.toolUses),
204
+ subscription,
159
205
  });
160
206
  },
161
207
 
@@ -172,22 +218,36 @@ export function createUsageTracker(): UsageTracker {
172
218
  state.totalTurns = toFiniteNumber(totals.turns);
173
219
  }
174
220
  if (Array.isArray(summary.subagents)) {
221
+ state.subagents = [];
222
+ state.subagentInputTokens = 0;
223
+ state.subagentOutputTokens = 0;
224
+ state.subagentCacheReadTokens = 0;
225
+ state.subagentCacheWriteTokens = 0;
226
+ state.subagentCost = 0;
175
227
  for (const sa of summary.subagents as Record<string, unknown>[]) {
228
+ const modelId = typeof sa.modelId === "string" ? sa.modelId : "unknown";
229
+ // Pre-change summaries lack the flag; recover it from the sub/ prefix
230
+ // so legacy subscription rows are not restored as paid.
231
+ const subscription = sa.subscription === true || isSubscriptionRouted(modelId);
176
232
  const entry: SubagentUsage = {
177
233
  description: typeof sa.description === "string" ? sa.description : "unknown",
178
234
  agentType: typeof sa.agentType === "string" ? sa.agentType : "unknown",
179
- modelId: typeof sa.modelId === "string" ? sa.modelId : "unknown",
235
+ modelId,
180
236
  inputTokens: toFiniteNumber(sa.inputTokens),
181
237
  outputTokens: toFiniteNumber(sa.outputTokens),
182
238
  cacheReadTokens: toFiniteNumber(sa.cacheReadTokens),
183
239
  cacheWriteTokens: toFiniteNumber(sa.cacheWriteTokens),
184
- cost: toFiniteNumber(sa.cost),
240
+ cacheSupported: sa.cacheSupported === true,
241
+ cost: subscription ? 0 : toFiniteNumber(sa.cost),
185
242
  durationMs: toFiniteNumber(sa.durationMs),
186
243
  toolUses: toFiniteNumber(sa.toolUses),
244
+ subscription,
187
245
  };
188
246
  state.subagents.push(entry);
189
247
  state.subagentInputTokens += entry.inputTokens;
190
248
  state.subagentOutputTokens += entry.outputTokens;
249
+ state.subagentCacheReadTokens += entry.cacheReadTokens;
250
+ state.subagentCacheWriteTokens += entry.cacheWriteTokens;
191
251
  state.subagentCost += entry.cost;
192
252
  }
193
253
  }
@@ -198,7 +258,9 @@ export function createUsageTracker(): UsageTracker {
198
258
  outputTokens: toFiniteNumber(usage.outputTokens),
199
259
  cacheReadTokens: toFiniteNumber(usage.cacheReadTokens),
200
260
  cacheWriteTokens: toFiniteNumber(usage.cacheWriteTokens),
261
+ cacheSupported: (usage as any).cacheSupported === true,
201
262
  turns: toFiniteNumber(usage.turns),
263
+ subscription: (usage as any).subscription === true || isSubscriptionRouted(modelId),
202
264
  });
203
265
  }
204
266
  }
@@ -216,11 +278,22 @@ export function createUsageTracker(): UsageTracker {
216
278
  },
217
279
 
218
280
  getTotalCacheReadTokens(): number {
219
- return state.totalCacheReadTokens;
281
+ return state.totalCacheReadTokens + state.subagentCacheReadTokens;
220
282
  },
221
283
 
222
284
  getTotalCacheWriteTokens(): number {
223
- return state.totalCacheWriteTokens;
285
+ return state.totalCacheWriteTokens + state.subagentCacheWriteTokens;
286
+ },
287
+
288
+ getTotalProcessedInputTokens(): number {
289
+ return (
290
+ state.totalInputTokens +
291
+ state.subagentInputTokens +
292
+ state.totalCacheReadTokens +
293
+ state.subagentCacheReadTokens +
294
+ state.totalCacheWriteTokens +
295
+ state.subagentCacheWriteTokens
296
+ );
224
297
  },
225
298
 
226
299
  getTotalCost(): number {
@@ -248,13 +321,22 @@ export function createUsageTracker(): UsageTracker {
248
321
  },
249
322
 
250
323
  getCacheHitRate(): number {
251
- const totalInput = state.totalInputTokens + state.subagentInputTokens;
252
- const totalCacheRead = state.totalCacheReadTokens;
253
- const denominator = totalCacheRead + totalInput;
324
+ const totalCacheRead = state.totalCacheReadTokens + state.subagentCacheReadTokens;
325
+ // Session-wide, token-weighted average: cache reads over all processed
326
+ // input (uncached input + cache read + cache write). Including cache
327
+ // writes keeps first-touch (uncached) content from inflating the rate.
328
+ const denominator = this.getTotalProcessedInputTokens();
254
329
  if (denominator <= 0) return 0;
255
330
  return totalCacheRead / denominator;
256
331
  },
257
332
 
333
+ isCacheSupported(): boolean {
334
+ for (const usage of state.models.values()) {
335
+ if (usage.cacheSupported) return true;
336
+ }
337
+ return state.subagents.some((sa) => sa.cacheSupported);
338
+ },
339
+
258
340
  getPerModelUsage(): Record<string, ModelUsage> {
259
341
  const out: Record<string, ModelUsage> = {};
260
342
  for (const [model, usage] of state.models.entries()) {
@@ -303,6 +385,8 @@ export function createUsageTracker(): UsageTracker {
303
385
  state.totalTurns = 0;
304
386
  state.subagentInputTokens = 0;
305
387
  state.subagentOutputTokens = 0;
388
+ state.subagentCacheReadTokens = 0;
389
+ state.subagentCacheWriteTokens = 0;
306
390
  state.subagentCost = 0;
307
391
  state.models.clear();
308
392
  state.subagents = [];
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from "vitest";
2
- import { validatePlan, validateUserRequest } from "./validate-artifacts.js";
2
+ import { validateArtifact, validatePlan, validateResearch, validateUserRequest } from "./validate-artifacts.js";
3
3
 
4
4
  describe("validateUserRequest", () => {
5
5
  it("rejects placeholder distillation and constraints content", () => {
@@ -86,3 +86,85 @@ Ship minimal fix.
86
86
  expect(result.errors).toContain("Section ## Scope is empty. Expected 2-4 lines summarizing scope and constraints.");
87
87
  });
88
88
  });
89
+
90
+ describe("validateResearch", () => {
91
+ it("accepts valid research content", () => {
92
+ const content = `## Affected Code
93
+ src/main.ts:run — entry point
94
+
95
+ ## Architecture Context
96
+ - Main flow calls run and dispatches handlers
97
+
98
+ ## Constraints & Edge Cases
99
+ - MUST: Keep behavior backward compatible
100
+ - RISK: Regression in startup path
101
+
102
+ ## Open Questions
103
+ Need confirmation about deprecated flag
104
+ `;
105
+
106
+ const result = validateResearch(content);
107
+
108
+ expect(result.ok).toBe(true);
109
+ });
110
+
111
+ it("fails when required sections are missing", () => {
112
+ const content = `## Affected Code
113
+ src/main.ts:run — entry point
114
+
115
+ ## Constraints & Edge Cases
116
+ - MUST: Keep behavior backward compatible
117
+ `;
118
+
119
+ const result = validateResearch(content);
120
+
121
+ expect(result.ok).toBe(false);
122
+ if (result.ok) return;
123
+ expect(result.errors.some((error) => error.includes("Missing required section: ## Architecture Context"))).toBe(true);
124
+ });
125
+
126
+ it("fails when affected code section is empty", () => {
127
+ const content = `## Affected Code
128
+ ...
129
+
130
+ ## Architecture Context
131
+ - Main flow calls run and dispatches handlers
132
+
133
+ ## Constraints & Edge Cases
134
+ - MUST: Keep behavior backward compatible
135
+ `;
136
+
137
+ const result = validateResearch(content);
138
+
139
+ expect(result.ok).toBe(false);
140
+ if (result.ok) return;
141
+ expect(result.errors).toContain("Section ## Affected Code is empty. Expected non-empty content.");
142
+ });
143
+ });
144
+
145
+ describe("validateArtifact", () => {
146
+ it("accepts valid artifact with top-level title", () => {
147
+ const content = `# Risk Analysis
148
+
149
+ - First risk
150
+ - Second risk
151
+ `;
152
+
153
+ const result = validateArtifact(content);
154
+
155
+ expect(result.ok).toBe(true);
156
+ });
157
+
158
+ it("fails when top-level title heading is missing", () => {
159
+ const content = `## Risk Analysis
160
+
161
+ - First risk
162
+ `;
163
+
164
+ const result = validateArtifact(content);
165
+
166
+ expect(result.ok).toBe(false);
167
+ if (result.ok) return;
168
+ expect(result.errors.some((error) => error.includes("Expected a top-level heading"))).toBe(true);
169
+ });
170
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ilya-lesikov/pi-pi",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "Pi-Pi Coding Agent: based on Pi Coding Agent, but twice as good",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -52,6 +52,7 @@
52
52
  "@pierre/diffs": "^1.2.0",
53
53
  "effect": "^3.0.0",
54
54
  "minimatch": "^10.0.0",
55
+ "pino": "^10.3.1",
55
56
  "proper-lockfile": "^4.1.2",
56
57
  "turndown": "^7.2.0",
57
58
  "typebox": "^1.0.0"