@ilya-lesikov/pi-pi 0.6.0 → 0.8.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 (66) hide show
  1. package/3p/pi-ask-user/index.ts +1 -1
  2. package/3p/pi-plannotator/apps/pi-extension/README.md +6 -5
  3. package/3p/pi-plannotator/apps/pi-extension/plannotator-browser.ts +25 -10
  4. package/3p/pi-plannotator/apps/pi-extension/plannotator-events.code-review.test.ts +172 -0
  5. package/3p/pi-plannotator/apps/pi-extension/plannotator-events.ts +50 -12
  6. package/3p/pi-plannotator/apps/pi-extension/server/project.test.ts +45 -0
  7. package/3p/pi-plannotator/apps/pi-extension/server/project.ts +7 -6
  8. package/3p/pi-plannotator/apps/pi-extension/server/serverReview.ts +41 -25
  9. package/3p/pi-subagents/src/agent-manager.ts +34 -1
  10. package/3p/pi-subagents/src/agent-runner.ts +66 -33
  11. package/3p/pi-subagents/src/index.ts +3 -38
  12. package/3p/pi-subagents/src/types.ts +4 -0
  13. package/extensions/orchestrator/agents/advisor.ts +35 -0
  14. package/extensions/orchestrator/agents/brainstorm-reviewer.ts +7 -7
  15. package/extensions/orchestrator/agents/code-reviewer.ts +7 -7
  16. package/extensions/orchestrator/agents/constraints.test.ts +44 -0
  17. package/extensions/orchestrator/agents/constraints.ts +3 -0
  18. package/extensions/orchestrator/agents/deep-debugger.ts +35 -0
  19. package/extensions/orchestrator/agents/plan-reviewer.ts +7 -7
  20. package/extensions/orchestrator/agents/planner.ts +9 -8
  21. package/extensions/orchestrator/agents/prompts.test.ts +120 -0
  22. package/extensions/orchestrator/agents/reviewer.ts +48 -0
  23. package/extensions/orchestrator/agents/task.ts +6 -20
  24. package/extensions/orchestrator/agents/tool-routing.ts +23 -1
  25. package/extensions/orchestrator/command-handlers.ts +1 -1
  26. package/extensions/orchestrator/config.ts +8 -4
  27. package/extensions/orchestrator/context.test.ts +54 -0
  28. package/extensions/orchestrator/context.ts +65 -2
  29. package/extensions/orchestrator/custom-footer.ts +5 -2
  30. package/extensions/orchestrator/doctor.test.ts +3 -1
  31. package/extensions/orchestrator/doctor.ts +40 -2
  32. package/extensions/orchestrator/event-handlers.test.ts +97 -1
  33. package/extensions/orchestrator/event-handlers.ts +222 -48
  34. package/extensions/orchestrator/flant-infra.test.ts +312 -0
  35. package/extensions/orchestrator/flant-infra.ts +407 -44
  36. package/extensions/orchestrator/index.ts +1 -1
  37. package/extensions/orchestrator/integration.test.ts +312 -18
  38. package/extensions/orchestrator/messages.test.ts +30 -0
  39. package/extensions/orchestrator/messages.ts +6 -0
  40. package/extensions/orchestrator/model-registry.test.ts +124 -13
  41. package/extensions/orchestrator/model-registry.ts +91 -33
  42. package/extensions/orchestrator/orchestrator.test.ts +113 -0
  43. package/extensions/orchestrator/orchestrator.ts +163 -3
  44. package/extensions/orchestrator/phases/brainstorm.ts +9 -20
  45. package/extensions/orchestrator/phases/implementation.test.ts +11 -0
  46. package/extensions/orchestrator/phases/implementation.ts +4 -6
  47. package/extensions/orchestrator/phases/planning.test.ts +16 -0
  48. package/extensions/orchestrator/phases/planning.ts +11 -4
  49. package/extensions/orchestrator/phases/review-task.ts +1 -4
  50. package/extensions/orchestrator/phases/review.test.ts +62 -0
  51. package/extensions/orchestrator/phases/review.ts +58 -15
  52. package/extensions/orchestrator/plannotator.ts +9 -6
  53. package/extensions/orchestrator/pp-menu.test.ts +74 -1
  54. package/extensions/orchestrator/pp-menu.ts +366 -94
  55. package/extensions/orchestrator/pp-state-tools.test.ts +192 -0
  56. package/extensions/orchestrator/pp-state-tools.ts +249 -0
  57. package/extensions/orchestrator/rate-limit-fallback-flow.test.ts +128 -0
  58. package/extensions/orchestrator/rate-limit-fallback.test.ts +98 -0
  59. package/extensions/orchestrator/rate-limit-fallback.ts +243 -0
  60. package/extensions/orchestrator/state.ts +41 -20
  61. package/extensions/orchestrator/test-helpers.ts +18 -3
  62. package/extensions/orchestrator/usage-tracker.test.ts +131 -3
  63. package/extensions/orchestrator/usage-tracker.ts +78 -11
  64. package/extensions/orchestrator/validate-artifacts.test.ts +20 -0
  65. package/extensions/orchestrator/validate-artifacts.ts +2 -2
  66. package/package.json +1 -1
@@ -1,6 +1,23 @@
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
+ if (typeof modelId !== "string") return false;
16
+ // Accept a bare `sub/<m>` id OR a full provider-prefixed spec
17
+ // `pp-flant-anthropic-sub/sub/<m>` passed as a single id (the form callers
18
+ // store/pass without a separate provider field).
19
+ return modelId.startsWith(SUB_MODEL_PREFIX) || modelId.startsWith(`${SUB_PROVIDER}/`);
20
+ }
4
21
 
5
22
  export interface ModelUsage {
6
23
  inputTokens: number;
@@ -9,6 +26,8 @@ export interface ModelUsage {
9
26
  cacheWriteTokens: number;
10
27
  cacheSupported: boolean;
11
28
  turns: number;
29
+ /** Flat-rate personal subscription: dollars are excluded from cost totals, tokens are not. */
30
+ subscription: boolean;
12
31
  }
13
32
 
14
33
  export interface UsageTracker {
@@ -19,6 +38,8 @@ export interface UsageTracker {
19
38
  getTotalOutputTokens(): number;
20
39
  getTotalCacheReadTokens(): number;
21
40
  getTotalCacheWriteTokens(): number;
41
+ /** Total input the model actually processed: uncached input + cache read + cache write (main + subagents). */
42
+ getTotalProcessedInputTokens(): number;
22
43
  getTotalCost(): number;
23
44
  getMainInputTokens(): number;
24
45
  getMainOutputTokens(): number;
@@ -46,6 +67,8 @@ export interface SubagentUsage {
46
67
  cost: number;
47
68
  durationMs: number;
48
69
  toolUses: number;
70
+ /** Flat-rate personal subscription: dollars are excluded from cost totals, tokens are not. */
71
+ subscription: boolean;
49
72
  }
50
73
 
51
74
  interface TrackerState {
@@ -58,6 +81,8 @@ interface TrackerState {
58
81
  totalTurns: number;
59
82
  subagentInputTokens: number;
60
83
  subagentOutputTokens: number;
84
+ subagentCacheReadTokens: number;
85
+ subagentCacheWriteTokens: number;
61
86
  subagentCost: number;
62
87
  models: Map<string, ModelUsage>;
63
88
  subagents: SubagentUsage[];
@@ -89,6 +114,8 @@ function createInitialState(): TrackerState {
89
114
  totalTurns: 0,
90
115
  subagentInputTokens: 0,
91
116
  subagentOutputTokens: 0,
117
+ subagentCacheReadTokens: 0,
118
+ subagentCacheWriteTokens: 0,
92
119
  subagentCost: 0,
93
120
  models: new Map<string, ModelUsage>(),
94
121
  subagents: [],
@@ -99,12 +126,15 @@ export function createUsageTracker(): UsageTracker {
99
126
  const state = createInitialState();
100
127
 
101
128
  return {
102
- recordTurn(modelId: string, _provider: string, input: number, output: number, cacheRead: number, cacheWrite: number, cost: number, cacheSupported?: boolean): void {
129
+ recordTurn(modelId: string, provider: string, input: number, output: number, cacheRead: number, cacheWrite: number, cost: number, cacheSupported?: boolean): void {
103
130
  const safeInput = toFiniteNumber(input);
104
131
  const safeOutput = toFiniteNumber(output);
105
132
  const safeCacheRead = toFiniteNumber(cacheRead);
106
133
  const safeCacheWrite = toFiniteNumber(cacheWrite);
107
- const safeCost = toFiniteNumber(cost);
134
+ const subscription = isSubscriptionRouted(modelId, provider);
135
+ // Subscription-routed turns are flat-rate: keep the tokens but exclude the
136
+ // fictitious per-token dollars so totals stay paid-only by construction.
137
+ const safeCost = subscription ? 0 : toFiniteNumber(cost);
108
138
 
109
139
  state.totalInputTokens += safeInput;
110
140
  state.totalOutputTokens += safeOutput;
@@ -113,7 +143,11 @@ export function createUsageTracker(): UsageTracker {
113
143
  state.totalCost += safeCost;
114
144
  state.totalTurns += 1;
115
145
 
116
- const key = modelId || "unknown-model";
146
+ // Key subscription turns under the sub/ prefix even when detected only by
147
+ // provider (bare model id), so a paid and a subscription turn for the same
148
+ // underlying model never share a row and the paid dollars stay visible.
149
+ const baseKey = modelId || "unknown-model";
150
+ const key = subscription && !baseKey.startsWith(SUB_MODEL_PREFIX) ? `${SUB_MODEL_PREFIX}${baseKey}` : baseKey;
117
151
  const usage = state.models.get(key) ?? {
118
152
  inputTokens: 0,
119
153
  outputTokens: 0,
@@ -121,6 +155,7 @@ export function createUsageTracker(): UsageTracker {
121
155
  cacheWriteTokens: 0,
122
156
  cacheSupported: false,
123
157
  turns: 0,
158
+ subscription: false,
124
159
  };
125
160
 
126
161
  usage.inputTokens += safeInput;
@@ -128,6 +163,7 @@ export function createUsageTracker(): UsageTracker {
128
163
  usage.cacheReadTokens += safeCacheRead;
129
164
  usage.cacheWriteTokens += safeCacheWrite;
130
165
  if (cacheSupported) usage.cacheSupported = true;
166
+ if (subscription) usage.subscription = true;
131
167
  usage.turns += 1;
132
168
  state.models.set(key, usage);
133
169
  },
@@ -142,12 +178,17 @@ export function createUsageTracker(): UsageTracker {
142
178
  const safeTotal = toFiniteNumber(tokens.total);
143
179
  const safeCacheRead = toFiniteNumber(tokens.cacheRead);
144
180
  const safeCacheWrite = toFiniteNumber(tokens.cacheWrite);
145
- const safeCost = toFiniteNumber(tokens.cost ?? cost);
181
+ // Subagents carry no provider field, so detect subscription routing from
182
+ // the registered model id prefix. Zero the dollars when subscription.
183
+ const subscription = isSubscriptionRouted(meta?.modelId);
184
+ const safeCost = subscription ? 0 : toFiniteNumber(tokens.cost ?? cost);
146
185
 
147
186
  const effectiveInput = safeInput === 0 && safeOutput === 0 ? safeTotal : safeInput;
148
187
 
149
188
  state.subagentInputTokens += effectiveInput;
150
189
  state.subagentOutputTokens += safeOutput;
190
+ state.subagentCacheReadTokens += safeCacheRead;
191
+ state.subagentCacheWriteTokens += safeCacheWrite;
151
192
  state.subagentCost += safeCost;
152
193
 
153
194
  const cacheSupported = typeof tokens.cacheRead === "number" || typeof tokens.cacheWrite === "number";
@@ -164,6 +205,7 @@ export function createUsageTracker(): UsageTracker {
164
205
  cost: safeCost,
165
206
  durationMs: toFiniteNumber(meta?.durationMs),
166
207
  toolUses: toFiniteNumber(meta?.toolUses),
208
+ subscription,
167
209
  });
168
210
  },
169
211
 
@@ -183,24 +225,33 @@ export function createUsageTracker(): UsageTracker {
183
225
  state.subagents = [];
184
226
  state.subagentInputTokens = 0;
185
227
  state.subagentOutputTokens = 0;
228
+ state.subagentCacheReadTokens = 0;
229
+ state.subagentCacheWriteTokens = 0;
186
230
  state.subagentCost = 0;
187
231
  for (const sa of summary.subagents as Record<string, unknown>[]) {
232
+ const modelId = typeof sa.modelId === "string" ? sa.modelId : "unknown";
233
+ // Pre-change summaries lack the flag; recover it from the sub/ prefix
234
+ // so legacy subscription rows are not restored as paid.
235
+ const subscription = sa.subscription === true || isSubscriptionRouted(modelId);
188
236
  const entry: SubagentUsage = {
189
237
  description: typeof sa.description === "string" ? sa.description : "unknown",
190
238
  agentType: typeof sa.agentType === "string" ? sa.agentType : "unknown",
191
- modelId: typeof sa.modelId === "string" ? sa.modelId : "unknown",
239
+ modelId,
192
240
  inputTokens: toFiniteNumber(sa.inputTokens),
193
241
  outputTokens: toFiniteNumber(sa.outputTokens),
194
242
  cacheReadTokens: toFiniteNumber(sa.cacheReadTokens),
195
243
  cacheWriteTokens: toFiniteNumber(sa.cacheWriteTokens),
196
244
  cacheSupported: sa.cacheSupported === true,
197
- cost: toFiniteNumber(sa.cost),
245
+ cost: subscription ? 0 : toFiniteNumber(sa.cost),
198
246
  durationMs: toFiniteNumber(sa.durationMs),
199
247
  toolUses: toFiniteNumber(sa.toolUses),
248
+ subscription,
200
249
  };
201
250
  state.subagents.push(entry);
202
251
  state.subagentInputTokens += entry.inputTokens;
203
252
  state.subagentOutputTokens += entry.outputTokens;
253
+ state.subagentCacheReadTokens += entry.cacheReadTokens;
254
+ state.subagentCacheWriteTokens += entry.cacheWriteTokens;
204
255
  state.subagentCost += entry.cost;
205
256
  }
206
257
  }
@@ -213,6 +264,7 @@ export function createUsageTracker(): UsageTracker {
213
264
  cacheWriteTokens: toFiniteNumber(usage.cacheWriteTokens),
214
265
  cacheSupported: (usage as any).cacheSupported === true,
215
266
  turns: toFiniteNumber(usage.turns),
267
+ subscription: (usage as any).subscription === true || isSubscriptionRouted(modelId),
216
268
  });
217
269
  }
218
270
  }
@@ -230,11 +282,22 @@ export function createUsageTracker(): UsageTracker {
230
282
  },
231
283
 
232
284
  getTotalCacheReadTokens(): number {
233
- return state.totalCacheReadTokens;
285
+ return state.totalCacheReadTokens + state.subagentCacheReadTokens;
234
286
  },
235
287
 
236
288
  getTotalCacheWriteTokens(): number {
237
- return state.totalCacheWriteTokens;
289
+ return state.totalCacheWriteTokens + state.subagentCacheWriteTokens;
290
+ },
291
+
292
+ getTotalProcessedInputTokens(): number {
293
+ return (
294
+ state.totalInputTokens +
295
+ state.subagentInputTokens +
296
+ state.totalCacheReadTokens +
297
+ state.subagentCacheReadTokens +
298
+ state.totalCacheWriteTokens +
299
+ state.subagentCacheWriteTokens
300
+ );
238
301
  },
239
302
 
240
303
  getTotalCost(): number {
@@ -262,9 +325,11 @@ export function createUsageTracker(): UsageTracker {
262
325
  },
263
326
 
264
327
  getCacheHitRate(): number {
265
- const totalInput = state.totalInputTokens + state.subagentInputTokens;
266
- const totalCacheRead = state.totalCacheReadTokens;
267
- const denominator = totalCacheRead + totalInput;
328
+ const totalCacheRead = state.totalCacheReadTokens + state.subagentCacheReadTokens;
329
+ // Session-wide, token-weighted average: cache reads over all processed
330
+ // input (uncached input + cache read + cache write). Including cache
331
+ // writes keeps first-touch (uncached) content from inflating the rate.
332
+ const denominator = this.getTotalProcessedInputTokens();
268
333
  if (denominator <= 0) return 0;
269
334
  return totalCacheRead / denominator;
270
335
  },
@@ -324,6 +389,8 @@ export function createUsageTracker(): UsageTracker {
324
389
  state.totalTurns = 0;
325
390
  state.subagentInputTokens = 0;
326
391
  state.subagentOutputTokens = 0;
392
+ state.subagentCacheReadTokens = 0;
393
+ state.subagentCacheWriteTokens = 0;
327
394
  state.subagentCost = 0;
328
395
  state.models.clear();
329
396
  state.subagents = [];
@@ -41,6 +41,26 @@ Ship minimal fix.
41
41
  expect(result.ok).toBe(true);
42
42
  });
43
43
 
44
+ it("accepts a Pattern constraints section", () => {
45
+ const content = `# Plan
46
+
47
+ ## Scope
48
+ Add a new annotation.
49
+
50
+ ## Checklist
51
+ - [ ] Add annotation — Done when: tests pass
52
+
53
+ ## Pattern constraints
54
+ - Mirror deletePolicies: one typed slice, kebab-case values.
55
+
56
+ ## Blockers
57
+ - none
58
+ `;
59
+
60
+ const result = validatePlan(content);
61
+ expect(result.ok).toBe(true);
62
+ });
63
+
44
64
  it("accepts multiline Done when continuation", () => {
45
65
  const content = `# Plan
46
66
 
@@ -5,7 +5,7 @@ const USER_REQUEST_ALLOWED_SECTIONS = ["Problem", "Constraints"] as const;
5
5
  const RESEARCH_REQUIRED_SECTIONS = ["Affected Code", "Architecture Context", "Constraints & Edge Cases"] as const;
6
6
  const RESEARCH_ALLOWED_SECTIONS = ["Affected Code", "Architecture Context", "Constraints & Edge Cases", "Open Questions"] as const;
7
7
  const PLAN_REQUIRED_SECTIONS = ["Scope", "Checklist"] as const;
8
- const PLAN_ALLOWED_SECTIONS = ["Scope", "Checklist", "Blockers"] as const;
8
+ const PLAN_ALLOWED_SECTIONS = ["Scope", "Checklist", "Pattern constraints", "Blockers"] as const;
9
9
  const PLACEHOLDER_PATTERNS = /^(?:[-*.…—]|tbd|todo|n\/a|na|none|\.{2,})$/i;
10
10
 
11
11
  function splitLines(content: string): string[] {
@@ -186,7 +186,7 @@ export function validatePlan(content: string): ValidationResult {
186
186
  const errors: string[] = [];
187
187
  const h1 = getH1(content);
188
188
  const firstHeading = getFirstHeading(content);
189
- const expectedSections = formatSectionList(PLAN_REQUIRED_SECTIONS, ["Blockers"]);
189
+ const expectedSections = formatSectionList(PLAN_REQUIRED_SECTIONS, ["Pattern constraints", "Blockers"]);
190
190
 
191
191
  if (!firstHeading) {
192
192
  errors.push("Missing required heading: # Plan. Expected first heading: # Plan");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ilya-lesikov/pi-pi",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "Pi-Pi Coding Agent: based on Pi Coding Agent, but twice as good",
5
5
  "keywords": [
6
6
  "pi-package",