@ilya-lesikov/pi-pi 0.7.0 → 0.9.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 (75) hide show
  1. package/3p/pi-plannotator/apps/pi-extension/README.md +6 -5
  2. package/3p/pi-plannotator/apps/pi-extension/plannotator-browser.ts +25 -10
  3. package/3p/pi-plannotator/apps/pi-extension/plannotator-events.code-review.test.ts +172 -0
  4. package/3p/pi-plannotator/apps/pi-extension/plannotator-events.ts +50 -12
  5. package/3p/pi-plannotator/apps/pi-extension/server/project.test.ts +45 -0
  6. package/3p/pi-plannotator/apps/pi-extension/server/project.ts +7 -6
  7. package/3p/pi-plannotator/apps/pi-extension/server/serverReview.ts +41 -25
  8. package/3p/pi-subagents/src/agent-manager.ts +34 -1
  9. package/3p/pi-subagents/src/agent-runner.ts +66 -33
  10. package/3p/pi-subagents/src/index.ts +3 -38
  11. package/3p/pi-subagents/src/types.ts +4 -0
  12. package/extensions/orchestrator/agents/advisor.ts +35 -0
  13. package/extensions/orchestrator/agents/brainstorm-reviewer.ts +7 -7
  14. package/extensions/orchestrator/agents/code-reviewer.ts +27 -11
  15. package/extensions/orchestrator/agents/constraints.test.ts +82 -0
  16. package/extensions/orchestrator/agents/constraints.ts +66 -2
  17. package/extensions/orchestrator/agents/deep-debugger.ts +35 -0
  18. package/extensions/orchestrator/agents/plan-reviewer.ts +7 -7
  19. package/extensions/orchestrator/agents/planner.ts +9 -8
  20. package/extensions/orchestrator/agents/prompts.test.ts +120 -0
  21. package/extensions/orchestrator/agents/reviewer.ts +48 -0
  22. package/extensions/orchestrator/agents/task.ts +6 -20
  23. package/extensions/orchestrator/agents/tool-routing.ts +39 -1
  24. package/extensions/orchestrator/ai-comment-cleanup.test.ts +89 -0
  25. package/extensions/orchestrator/ai-comment-cleanup.ts +73 -0
  26. package/extensions/orchestrator/command-handlers.test.ts +47 -0
  27. package/extensions/orchestrator/command-handlers.ts +44 -28
  28. package/extensions/orchestrator/config.test.ts +40 -1
  29. package/extensions/orchestrator/config.ts +18 -2
  30. package/extensions/orchestrator/context.test.ts +54 -0
  31. package/extensions/orchestrator/context.ts +81 -2
  32. package/extensions/orchestrator/custom-footer.test.ts +91 -0
  33. package/extensions/orchestrator/custom-footer.ts +20 -20
  34. package/extensions/orchestrator/event-handlers.test.ts +412 -2
  35. package/extensions/orchestrator/event-handlers.ts +556 -227
  36. package/extensions/orchestrator/flant-infra.test.ts +25 -0
  37. package/extensions/orchestrator/flant-infra.ts +91 -0
  38. package/extensions/orchestrator/index.ts +1 -1
  39. package/extensions/orchestrator/integration.test.ts +411 -20
  40. package/extensions/orchestrator/messages.test.ts +30 -0
  41. package/extensions/orchestrator/messages.ts +6 -0
  42. package/extensions/orchestrator/model-registry.test.ts +48 -1
  43. package/extensions/orchestrator/model-registry.ts +43 -1
  44. package/extensions/orchestrator/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  45. package/extensions/orchestrator/orchestrator.test.ts +113 -0
  46. package/extensions/orchestrator/orchestrator.ts +197 -60
  47. package/extensions/orchestrator/phases/brainstorm.ts +20 -27
  48. package/extensions/orchestrator/phases/implementation.test.ts +11 -0
  49. package/extensions/orchestrator/phases/implementation.ts +4 -6
  50. package/extensions/orchestrator/phases/machine.test.ts +36 -0
  51. package/extensions/orchestrator/phases/machine.ts +11 -1
  52. package/extensions/orchestrator/phases/planning.test.ts +16 -0
  53. package/extensions/orchestrator/phases/planning.ts +11 -4
  54. package/extensions/orchestrator/phases/review-task.test.ts +20 -0
  55. package/extensions/orchestrator/phases/review-task.ts +47 -22
  56. package/extensions/orchestrator/phases/review.test.ts +34 -0
  57. package/extensions/orchestrator/phases/review.ts +44 -6
  58. package/extensions/orchestrator/plannotator.ts +9 -6
  59. package/extensions/orchestrator/pp-menu.test.ts +207 -1
  60. package/extensions/orchestrator/pp-menu.ts +514 -402
  61. package/extensions/orchestrator/pp-state-tools.test.ts +192 -0
  62. package/extensions/orchestrator/pp-state-tools.ts +249 -0
  63. package/extensions/orchestrator/rate-limit-fallback-flow.test.ts +128 -0
  64. package/extensions/orchestrator/rate-limit-fallback.test.ts +98 -0
  65. package/extensions/orchestrator/rate-limit-fallback.ts +243 -0
  66. package/extensions/orchestrator/state.test.ts +9 -0
  67. package/extensions/orchestrator/state.ts +15 -0
  68. package/extensions/orchestrator/test-helpers.ts +4 -1
  69. package/extensions/orchestrator/transition-controller.test.ts +100 -0
  70. package/extensions/orchestrator/transition-controller.ts +61 -3
  71. package/extensions/orchestrator/usage-tracker.ts +5 -1
  72. package/extensions/orchestrator/validate-artifacts.test.ts +20 -0
  73. package/extensions/orchestrator/validate-artifacts.ts +2 -2
  74. package/package.json +1 -1
  75. package/AGENTS.md +0 -28
@@ -427,6 +427,7 @@ describe("flant-infra", () => {
427
427
  enabled: false,
428
428
  autoUpdate: true,
429
429
  cacheTTLDays: 7,
430
+ switchBackIntervalMinutes: 30,
430
431
  subscription: false,
431
432
  lastUpdated: null,
432
433
  cachedFlantModels: null,
@@ -465,6 +466,7 @@ describe("flant-infra", () => {
465
466
  enabled: true,
466
467
  autoUpdate: false,
467
468
  cacheTTLDays: 3,
469
+ switchBackIntervalMinutes: 30,
468
470
  subscription: false,
469
471
  lastUpdated: "2026-01-02T03:04:05.000Z",
470
472
  cachedFlantModels: ["gpt-5-4", "claude-opus-4-6"],
@@ -488,6 +490,7 @@ describe("flant-infra", () => {
488
490
  enabled: true,
489
491
  autoUpdate: true,
490
492
  cacheTTLDays: 14,
493
+ switchBackIntervalMinutes: 30,
491
494
  subscription: true,
492
495
  lastUpdated: "2026-02-01T00:00:00.000Z",
493
496
  cachedFlantModels: ["claude-opus-4-6", "gpt-5-4"],
@@ -508,4 +511,26 @@ describe("flant-infra", () => {
508
511
  expect(loaded).toEqual(settings);
509
512
  expect(existsSync(join(dir, "extensions", "pp", "cache", "flant-models.json"))).toBe(true);
510
513
  });
514
+
515
+ it("normalizes switchBackIntervalMinutes: default when missing, parsed, floored to >=1", async () => {
516
+ const dir = makeTempDir();
517
+ const mod = await loadFlantInfraModule(dir);
518
+ const settingsDir = join(dir, "extensions", "pp", "cache");
519
+ const settingsPath = join(settingsDir, "flant-models.json");
520
+ mkdirSync(settingsDir, { recursive: true });
521
+
522
+ // Missing -> default 30.
523
+ writeFileSync(settingsPath, JSON.stringify({ enabled: true }), "utf-8");
524
+ expect(mod.loadFlantSettings().switchBackIntervalMinutes).toBe(30);
525
+
526
+ // String numeric -> parsed and rounded.
527
+ writeFileSync(settingsPath, JSON.stringify({ enabled: true, switchBackIntervalMinutes: "45" }), "utf-8");
528
+ expect(mod.loadFlantSettings().switchBackIntervalMinutes).toBe(45);
529
+
530
+ // Invalid / <1 -> floored to 1.
531
+ writeFileSync(settingsPath, JSON.stringify({ enabled: true, switchBackIntervalMinutes: 0 }), "utf-8");
532
+ expect(mod.loadFlantSettings().switchBackIntervalMinutes).toBe(1);
533
+ writeFileSync(settingsPath, JSON.stringify({ enabled: true, switchBackIntervalMinutes: "nonsense" }), "utf-8");
534
+ expect(mod.loadFlantSettings().switchBackIntervalMinutes).toBe(30);
535
+ });
511
536
  });
@@ -35,6 +35,13 @@ export interface FlantSettings {
35
35
  * Claude OAuth token (billed against their personal Claude subscription).
36
36
  */
37
37
  subscription: boolean;
38
+ /**
39
+ * Minutes between out-of-band "is the subscription limit cleared yet?" probes
40
+ * while the sub→non-sub rate-limit fallback is active. On each interval a
41
+ * cheap probe hits the sub model; on success the user is asked to switch back.
42
+ * Default 30.
43
+ */
44
+ switchBackIntervalMinutes: number;
38
45
  }
39
46
 
40
47
  const GEMINI_MAP: Record<string, string> = {
@@ -64,6 +71,7 @@ const DEFAULT_SETTINGS: FlantSettings = {
64
71
  cachedFlantModels: null,
65
72
  cachedOpenRouterData: null,
66
73
  subscription: false,
74
+ switchBackIntervalMinutes: 30,
67
75
  };
68
76
 
69
77
  /** Provider name for the personal-subscription Claude routing. */
@@ -245,10 +253,15 @@ function normalizeSettings(raw: unknown): FlantSettings {
245
253
  if (!raw || typeof raw !== "object") return { ...DEFAULT_SETTINGS };
246
254
  const value = raw as Record<string, unknown>;
247
255
  const cacheTTLDays = Math.max(1, Math.round(toNumber(value.cacheTTLDays, DEFAULT_SETTINGS.cacheTTLDays)));
256
+ const switchBackIntervalMinutes = Math.max(
257
+ 1,
258
+ Math.round(toNumber(value.switchBackIntervalMinutes, DEFAULT_SETTINGS.switchBackIntervalMinutes)),
259
+ );
248
260
  return {
249
261
  enabled: !!value.enabled,
250
262
  autoUpdate: value.autoUpdate === undefined ? true : !!value.autoUpdate,
251
263
  cacheTTLDays,
264
+ switchBackIntervalMinutes,
252
265
  subscription: !!value.subscription,
253
266
  lastUpdated: typeof value.lastUpdated === "string" ? value.lastUpdated : null,
254
267
  cachedFlantModels: Array.isArray(value.cachedFlantModels)
@@ -347,6 +360,81 @@ function mapFlantToOpenRouterId(modelId: string): string | null {
347
360
  return null;
348
361
  }
349
362
 
363
+ /**
364
+ * Out-of-band probe: is the personal Claude subscription limit cleared yet?
365
+ * Sends a tiny, fully throwaway request (NOT part of the session, so it cannot
366
+ * pollute conversation/context/cache) to the gateway's Anthropic endpoint using
367
+ * the personal Claude OAuth token. Returns:
368
+ * "ok" — 200: capacity is back (offer switch-back)
369
+ * "rate_limited" — 429: still limited (stay on non-sub, retry later)
370
+ * "error" — credentials missing / network / other status (treat as not-back)
371
+ *
372
+ * The OAuth token is refreshed first so a failure is a genuine 429 and not an
373
+ * expired-token false negative. `max_tokens: 1` + an explicit "just respond hi"
374
+ * instruction keep output at ~1 token.
375
+ */
376
+ // Derive the gateway probe model id (`sub/<bare-claude-id>`) from any stored
377
+ // form: `pp-flant-anthropic-sub/sub/<m>`, `sub/<m>`, or a bare `<m>`. Exported
378
+ // for testing the derivation without a network call.
379
+ export function subProbeModelId(modelId: string): string {
380
+ let bare = modelId;
381
+ if (bare.startsWith(`${SUB_PROVIDER}/`)) bare = bare.slice(`${SUB_PROVIDER}/`.length);
382
+ if (bare.startsWith(SUB_MODEL_PREFIX)) bare = bare.slice(SUB_MODEL_PREFIX.length);
383
+ return `${SUB_MODEL_PREFIX}${bare}`;
384
+ }
385
+
386
+ export async function probeSubscriptionCleared(
387
+ modelId: string,
388
+ ): Promise<"ok" | "rate_limited" | "error"> {
389
+ const log = getLogger();
390
+ await refreshClaudeOAuthToken();
391
+ const oauthToken = readClaudeOAuthToken();
392
+ const gatewayKey = readGatewayApiKey();
393
+ if (!oauthToken || !gatewayKey) {
394
+ log.debug({ s: "flant", hasOAuth: !!oauthToken, hasGatewayKey: !!gatewayKey }, "probe skipped: missing credentials");
395
+ return "error";
396
+ }
397
+ // The gateway expects the bare claude-* id under the `sub/` prefix.
398
+ const probeModel = subProbeModelId(modelId);
399
+ try {
400
+ const res = await fetch("https://llm-api.flant.ru/v1/messages", {
401
+ method: "POST",
402
+ headers: {
403
+ "content-type": "application/json",
404
+ "anthropic-version": "2023-06-01",
405
+ // Claude Code OAuth identity headers — the gateway requires these for
406
+ // subscription (sub/) routing; without them it can reject the probe and
407
+ // we would never detect that the limit cleared. Mirrors doctor.ts.
408
+ "anthropic-beta": "claude-code-20250219,oauth-2025-04-20",
409
+ "user-agent": "claude-cli/1.0.0",
410
+ "x-app": "cli",
411
+ Authorization: `Bearer ${oauthToken}`,
412
+ "x-litellm-api-key": `Bearer ${gatewayKey}`,
413
+ },
414
+ body: JSON.stringify({
415
+ model: probeModel,
416
+ max_tokens: 1,
417
+ temperature: 0,
418
+ messages: [{ role: "user", content: "just respond hi" }],
419
+ }),
420
+ signal: AbortSignal.timeout(30000),
421
+ });
422
+ if (res.status === 429) {
423
+ log.debug({ s: "flant", model: probeModel }, "probe: still rate limited");
424
+ return "rate_limited";
425
+ }
426
+ if (res.ok) {
427
+ log.debug({ s: "flant", model: probeModel }, "probe: capacity back");
428
+ return "ok";
429
+ }
430
+ log.debug({ s: "flant", model: probeModel, status: res.status }, "probe: unexpected status");
431
+ return "error";
432
+ } catch (err: any) {
433
+ log.debug({ s: "flant", err: err?.message }, "probe failed");
434
+ return "error";
435
+ }
436
+ }
437
+
350
438
  export async function discoverFlantModels(apiKey: string): Promise<string[]> {
351
439
  const res = await fetch("https://llm-api.flant.ru/v1/models", {
352
440
  headers: { Authorization: `Bearer ${apiKey}` },
@@ -655,6 +743,9 @@ export function generateFlantConfig(models: string[], subscriptionActive = false
655
743
  explore: { model: modelSpec(fastModel, sub), thinking: "low" },
656
744
  librarian: { model: modelSpec(fastModel, sub), thinking: "medium" },
657
745
  task: { model: modelSpec(taskModel, sub), thinking: "medium" },
746
+ advisor: { model: modelSpec(latestGpt ?? fallback, sub), thinking: "high" },
747
+ "deep-debugger": { model: modelSpec(latestGpt ?? fallback, sub), thinking: "high" },
748
+ reviewer: { model: modelSpec(latestGpt ?? fallback, sub), thinking: "high" },
658
749
  },
659
750
  presetGroups: {
660
751
  planners: buildPresetGroup({
@@ -65,7 +65,7 @@ function registerSubagentTools(pi: ExtensionAPI): void {
65
65
  ...event.content,
66
66
  {
67
67
  type: "text" as const,
68
- text: `\n\n<validation-error>\nPlan structure is invalid:\n${result.errors.map((e) => `- ${e}`).join("\n")}\n\nFix immediately. Required structure:\n# Plan\n## Scope\n<2-4 lines>\n## Checklist\n- [ ] <outcome> — Done when: <observable condition>\n## Blockers (optional)\n<issues>\n\nRewrite the file now.\n</validation-error>`,
68
+ text: `\n\n<validation-error>\nPlan structure is invalid:\n${result.errors.map((e) => `- ${e}`).join("\n")}\n\nFix immediately. Required structure:\n# Plan\n## Scope\n<2-4 lines>\n## Checklist\n- [ ] <outcome> — Done when: <observable condition>\n## Pattern constraints (optional; include when adding a type/function/user-facing value)\n<closest existing analog + conventions to mirror>\n## Blockers (optional)\n<issues>\n\nRewrite the file now.\n</validation-error>`,
69
69
  },
70
70
  ],
71
71
  };