@phi-code-admin/phi-code 0.86.0 → 0.88.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 (35) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/docs/adr/0001-phase-contract.md +57 -0
  3. package/docs/adr/0002-independent-review.md +47 -0
  4. package/docs/fork-policy.md +18 -0
  5. package/extensions/phi/agents.ts +7 -4
  6. package/extensions/phi/benchmark.ts +129 -49
  7. package/extensions/phi/browser.ts +10 -37
  8. package/extensions/phi/btw/btw.ts +1 -7
  9. package/extensions/phi/chrome/index.ts +1283 -741
  10. package/extensions/phi/commit.ts +9 -14
  11. package/extensions/phi/goal/index.ts +10 -33
  12. package/extensions/phi/init.ts +37 -47
  13. package/extensions/phi/keys.ts +2 -7
  14. package/extensions/phi/mcp/callback-server.ts +162 -165
  15. package/extensions/phi/mcp/config.ts +122 -136
  16. package/extensions/phi/mcp/errors.ts +18 -23
  17. package/extensions/phi/mcp/index.ts +322 -355
  18. package/extensions/phi/mcp/oauth-provider.ts +289 -289
  19. package/extensions/phi/mcp/server-manager.ts +390 -413
  20. package/extensions/phi/mcp/tool-bridge.ts +381 -415
  21. package/extensions/phi/memory.ts +2 -2
  22. package/extensions/phi/models.ts +27 -26
  23. package/extensions/phi/orchestrator.ts +451 -237
  24. package/extensions/phi/productivity.ts +4 -2
  25. package/extensions/phi/providers/agent-def.ts +1 -1
  26. package/extensions/phi/providers/alibaba.ts +56 -7
  27. package/extensions/phi/providers/context-window.ts +4 -1
  28. package/extensions/phi/providers/live-models.ts +5 -20
  29. package/extensions/phi/providers/orchestrator-helpers.ts +31 -5
  30. package/extensions/phi/providers/phase-machine.ts +283 -0
  31. package/extensions/phi/setup.ts +196 -169
  32. package/extensions/phi/skill-loader.ts +14 -17
  33. package/extensions/phi/smart-router.ts +6 -6
  34. package/extensions/phi/web-search.ts +90 -50
  35. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,109 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.88.0] - 2026-07-10
4
+
5
+ ### Changed
6
+
7
+ - **The /plan phase contract is now structured-primary with a text fallback**
8
+ (see `docs/adr/0001-phase-contract.md`). A phase agent CALLS a new
9
+ `phase_result` tool to emit its verdict / blocking / handoff as data;
10
+ `resolvePhaseOutcome` merges that structured emission field-by-field with the
11
+ regex-scraped markdown report, preferring the structured value. TEST and
12
+ REVIEW are instructed to call it. When a model does not call the tool,
13
+ behavior is identical to the previous text-only path — the change cannot make
14
+ any run worse. This replaces "regex luck" with an exact machine-read path for
15
+ the control-flow-critical signals (verdict, BLOCKED, the review-fix cycle).
16
+
17
+ ### Added
18
+
19
+ - **End-to-end integration test of the orchestrator.** The real orchestrator
20
+ extension is now driven through a full multi-phase run against a simulated Pi
21
+ runtime (`test/orchestrator-integration.test.ts`): phase progression, handoff
22
+ propagation, the review-FAIL → fix → re-review cycle, and the BLOCKED pause
23
+ are all exercised together, not just the pure decision function. (Scripted
24
+ phase outcomes — deterministic, no live model.)
25
+ - **An eval harness** (`evals/`) — the measurement infrastructure that did not
26
+ exist. Tasks with deterministic pass/fail verifiers, a runnable baseline
27
+ strategy (`npx tsx evals/run.ts`), unit-tested scoring/aggregation
28
+ (`test/evals-lib.test.ts`), and a demonstrated real run (2/2 baseline tasks on
29
+ a live model). `evals/README.md` documents the methodology and is honest that
30
+ the /plan-vs-baseline head-to-head is not yet a single number.
31
+ - **Two ADRs** documenting the phase-contract decision and the independent-review
32
+ release gate (`docs/adr/`).
33
+
34
+ ### Fixed
35
+
36
+ - Bugs found by an **independent adversarial review** of the phase-contract
37
+ change (see `docs/adr/0002-independent-review.md`), each now pinned by a
38
+ regression test:
39
+ - **State leak between runs**: `currentPhaseResult` was reset only in
40
+ `sendNextPhase`, but the first phase of each `/plan` run launches directly —
41
+ a stale structured result (e.g. a BLOCKED verdict) from a previous run could
42
+ abort a fresh run at phase 1. Reset on every run start + a phase-identity
43
+ stamp so a stale/late result is never mistaken for the current phase.
44
+ - **Field erasure on multi-call**: a second `phase_result` call replaced the
45
+ whole object, wiping a verdict set by the first; it now merges only the
46
+ fields each call sets.
47
+ - **Dead text fallback for PLAN/CODE**: `readPhaseReport` looked for
48
+ `<key>-<ts>.md` but PLAN writes `todo-<ts>.md` and CODE `progress-<ts>.md`,
49
+ so their handoff blocks were never read. Mapped key → report file.
50
+ - **`extractSection` over-matching**: a prose line starting with a section
51
+ word (e.g. "Blocking issues remain: 2") could be read as that section's
52
+ header. The plain-label form now requires `:` or end-of-line.
53
+ - A TEST/REVIEW phase that finished with tool work but emitted no verdict at all
54
+ (neither structured nor a VERDICT line) is now surfaced instead of silently
55
+ passing.
56
+ - **The eval runner** had two Windows bugs its first real run caught: temp-dir
57
+ cleanup raced `EPERM` (now retries), and `shell:true` with an args array
58
+ mangled the prompt (now a single quoted command string).
59
+
60
+ ### Tooling
61
+
62
+ - Biome now also lints `evals/`.
63
+
64
+ ## [0.87.0] - 2026-07-10
65
+
66
+ ### Added
67
+
68
+ - **The /plan orchestrator's phase state machine is now unit-tested.** Its
69
+ decision core — what to do after each phase given the messages and the report
70
+ (user abort > auth error > transient retry > BLOCKED pause > review-FAIL fix
71
+ cycle > continue) — is extracted into a pure module
72
+ (`extensions/phi/providers/phase-machine.ts`) and covered by 32 tests,
73
+ including the cases where a model deviates from the text contract: a missing
74
+ or unparseable `VERDICT:` line is now surfaced to the user instead of being
75
+ silently treated as a pass, and zero-tool-call / queue-race paths are pinned.
76
+ The `agent_end` hook keeps the exact same behavior but now only interprets
77
+ the decision (I/O), it no longer decides.
78
+ - **Section parsing tolerates the header shapes models actually emit.**
79
+ `extractSection` (HANDOFF / BLOCKING) now handles a markdown heading, a
80
+ standalone bold label (`**HANDOFF**`) or a plain `LABEL:` line, and splits a
81
+ report written entirely with bold labels — previously a bold-only `**HANDOFF**`
82
+ returned nothing. Added regression tests for these deviations.
83
+
84
+ ### Changed
85
+
86
+ - **extensions/ and the sigma-\* packages are now linted.** Biome previously
87
+ skipped them entirely (typecheck covered them, lint did not); they are in the
88
+ linted set and the whole repo is clean (`biome ci .` passes on 752 files,
89
+ zero warnings). Idiomatic-but-flagged regex-exec loops in web-search were
90
+ rewritten through an `execAll` generator rather than silenced with rule
91
+ overrides.
92
+ - **Releases are reproducible.** `npm run build` no longer regenerates the
93
+ model catalog from models.dev — the committed `packages/ai/src/*.generated.ts`
94
+ is the source of truth and `build` just compiles it, so a publish ships
95
+ exactly what is in git. Regeneration is an explicit `npm run generate
96
+ --workspace=packages/ai` step; a scheduled `refresh-models.yml` workflow
97
+ opens a PR weekly so the catalog still stays fresh with a human in the loop.
98
+
99
+ ### CI / process
100
+
101
+ - **CI now gates lint, format, types and catalog-cleanliness on every PR**
102
+ (`biome ci`, `tsgo --noEmit`, and `git diff --exit-code` on the generated
103
+ catalog), not just build + test — fork PRs that bypass the local pre-commit
104
+ hook are covered. A pull-request template makes the discipline checklist
105
+ explicit, and `docs/fork-policy.md` documents the reproducible-catalog flow.
106
+
3
107
  ## [0.86.0] - 2026-07-10
4
108
 
5
109
  ### Changed
@@ -0,0 +1,57 @@
1
+ # ADR 0001 — Structured-primary phase contract for /plan
2
+
3
+ Status: accepted (2026-07-10)
4
+
5
+ ## Context
6
+
7
+ The /plan orchestrator chains five agent phases (explore → plan → code → test
8
+ → review). Each phase must report two things the orchestrator acts on:
9
+
10
+ - a **verdict** (TEST/REVIEW): PASS / FAIL / BLOCKED / SKIP, and
11
+ - a **handoff** (+ BLOCKING findings for REVIEW) carried to the next phase.
12
+
13
+ Originally this was communicated **only** as markdown the model wrote to
14
+ `.phi/plans/<phase>-<ts>.md`, which the orchestrator scraped with regexes
15
+ (`## VERDICT:`, `## HANDOFF`, `## BLOCKING`). That is fragile: models phrase
16
+ headers inconsistently (`**HANDOFF**`, `HANDOFF:`, mid-sentence "verdict"), and
17
+ a mis-scrape silently degrades control flow — a missed REVIEW FAIL skips the fix
18
+ cycle; a missed BLOCKED keeps a doomed run going. The original justification for
19
+ text-only was that the upstream proxy did not guarantee valid structured tool
20
+ output.
21
+
22
+ ## Decision
23
+
24
+ Keep the markdown report (it is the human-readable artifact) but make the
25
+ **machine-read path structured and primary**:
26
+
27
+ - The orchestrator registers a `phase_result` tool. TEST and REVIEW phases are
28
+ instructed to call it with `{verdict, blocking, handoff}`; any phase may call
29
+ it to hand off.
30
+ - `resolvePhaseOutcome(structured, reportText)` (pure, unit-tested) merges the
31
+ two sources **field by field**, preferring the structured value and falling
32
+ back to the regex-scraped report per field. When the model calls the tool the
33
+ outcome is exact; when it does not, behavior is byte-for-byte the pre-existing
34
+ text path.
35
+ - The text parser was hardened anyway (`extractSection` accepts heading, bold,
36
+ and plain-label forms) so the fallback is as robust as possible.
37
+
38
+ ## Why not go structured-only
39
+
40
+ Two reasons the text path stays as a fallback rather than being removed:
41
+
42
+ 1. **Provider variance.** Not every provider/proxy reliably emits tool calls on
43
+ every turn; a model that writes a good report but forgets the tool call must
44
+ still drive the pipeline correctly.
45
+ 2. **Zero-regression migration.** Making structured additive means the change
46
+ cannot make any existing run worse — the worst case equals the old behavior.
47
+
48
+ ## Consequences
49
+
50
+ - Robustness of control flow (verdict/BLOCKED/fix-cycle) now depends on a
51
+ structured emission when available, not on regex luck.
52
+ - Two code paths must stay in sync; `resolvePhaseOutcome` centralizes the merge
53
+ and is covered by unit tests, and `orchestrator-integration.test.ts` drives
54
+ the whole chain via the structured path.
55
+ - Follow-up (not done here): once telemetry shows the structured path is taken
56
+ reliably across the providers phi ships, the text fallback can be demoted to a
57
+ warning-only safety net.
@@ -0,0 +1,47 @@
1
+ # ADR 0002 — Independent adversarial review as a release gate
2
+
3
+ Status: accepted (2026-07-10)
4
+
5
+ ## Context
6
+
7
+ phi-code is largely built by a single author, who is also the only reviewer.
8
+ Self-review misses the bugs the author's mental model is blind to — the code
9
+ does what the author *thinks* it does, and they test that. There is no external
10
+ human reviewer on hand for every change.
11
+
12
+ ## Decision
13
+
14
+ Before shipping a non-trivial change to a load-bearing subsystem (the /plan
15
+ orchestrator, the model/provider layer, compaction, the extension runtime), run
16
+ an **independent adversarial review**: a reviewer with fresh context that did
17
+ not write the code, prompted to *refute* — to find state leaks, races, contract
18
+ mismatches, and edge cases — not to approve.
19
+
20
+ Findings are triaged (verify each against the code, discard false positives),
21
+ the real ones are fixed, and each fix is pinned with a regression test before
22
+ the change ships.
23
+
24
+ ## Evidence this works
25
+
26
+ The structured-phase-contract change (ADR 0001) passed the author's own unit
27
+ and integration tests. An independent review of that change then found four
28
+ real defects the author's tests missed:
29
+
30
+ - a structured result leaking from one `/plan` run into the next (a stale
31
+ BLOCKED verdict could abort a fresh run at phase 1);
32
+ - a second `phase_result` call erasing fields set by the first;
33
+ - no phase-identity guard against a late tool call landing after a transition;
34
+ - the text HANDOFF fallback being dead for two phases due to a report-file name
35
+ mismatch.
36
+
37
+ All four are now fixed and covered by regression tests
38
+ (`orchestrator-integration.test.ts`, `phase-machine.test.ts`).
39
+
40
+ ## Consequences
41
+
42
+ - "Green tests" is necessary but not sufficient for a load-bearing change; an
43
+ independent refutation pass is part of the definition of done.
44
+ - The reviewer need not be human to be useful — it needs to be *independent of
45
+ the authoring context* and prompted adversarially. This is not a substitute
46
+ for real external users, whose absence remains an honest limitation of the
47
+ project's validation.
@@ -61,3 +61,21 @@ use these constants, never a hardcoded "pi" or "phi".**
61
61
  `agents/`, `skills/`, `config/`, the `sigma-*` packages, and the browser and
62
62
  camoufox packages are phi-code territory: normal engineering rules apply,
63
63
  no merge constraints.
64
+
65
+ ## Reproducible releases: the generated model catalog
66
+
67
+ `packages/ai/src/models.generated.ts` (and `image-models.generated.ts`) are
68
+ **committed source files and the source of truth for every build**. `npm run
69
+ build` compiles them with `tsgo`; it does NOT fetch models.dev, so a build/
70
+ publish always ships exactly what is in git — releases are reproducible.
71
+
72
+ Refreshing the catalog is a separate, explicit action:
73
+
74
+ - `npm run generate --workspace=packages/ai` regenerates from models.dev.
75
+ - The maintainer reviews the diff, commits it, and bumps `packages/ai`.
76
+ - `.github/workflows/refresh-models.yml` does this weekly and opens a PR, so the
77
+ catalog never rots without a human in the loop.
78
+
79
+ CI enforces the invariant: the `check` job fails if the committed catalog
80
+ differs from a clean checkout (`git diff --exit-code`), catching both a stray
81
+ hand-edit and any build that regenerated it.
@@ -23,15 +23,18 @@ export default function agentsExtension(pi: ExtensionAPI) {
23
23
  const arg = args.trim().toLowerCase();
24
24
 
25
25
  if (agents.length === 0) {
26
- ctx.ui.notify("No agent definitions found.\n\nCreate agent files in:\n- `.phi/agents/` (project)\n- `~/.phi/agent/agents/` (global)\n\nFormat: Markdown with YAML frontmatter (name, description, tools, model).", "info");
26
+ ctx.ui.notify(
27
+ "No agent definitions found.\n\nCreate agent files in:\n- `.phi/agents/` (project)\n- `~/.phi/agent/agents/` (global)\n\nFormat: Markdown with YAML frontmatter (name, description, tools, model).",
28
+ "info",
29
+ );
27
30
  return;
28
31
  }
29
32
 
30
33
  // Show specific agent details
31
34
  if (arg && arg !== "list") {
32
- const agent = agents.find(a => a.name.toLowerCase() === arg);
35
+ const agent = agents.find((a) => a.name.toLowerCase() === arg);
33
36
  if (!agent) {
34
- ctx.ui.notify(`Agent "${arg}" not found. Available: ${agents.map(a => a.name).join(", ")}`, "warning");
37
+ ctx.ui.notify(`Agent "${arg}" not found. Available: ${agents.map((a) => a.name).join(", ")}`, "warning");
35
38
  return;
36
39
  }
37
40
 
@@ -39,7 +42,7 @@ export default function agentsExtension(pi: ExtensionAPI) {
39
42
 
40
43
  📝 ${agent.description}
41
44
  🤖 Model: \`${agent.model}\`
42
- 🔧 Tools: ${agent.tools.map(t => `\`${t}\``).join(", ")}
45
+ 🔧 Tools: ${agent.tools.map((t) => `\`${t}\``).join(", ")}
43
46
  📁 Source: ${agent.source} (\`${agent.filePath}\`)
44
47
 
45
48
  **System Prompt:**
@@ -17,10 +17,10 @@
17
17
  * - /benchmark clear — Clear all results
18
18
  */
19
19
 
20
- import type { ExtensionAPI, ExtensionContext } from "phi-code";
21
- import { writeFile, mkdir, readFile, access } from "node:fs/promises";
22
- import { join } from "node:path";
20
+ import { access, mkdir, readFile, writeFile } from "node:fs/promises";
23
21
  import { homedir } from "node:os";
22
+ import { join } from "node:path";
23
+ import type { ExtensionAPI, ExtensionContext } from "phi-code";
24
24
 
25
25
  // ─── Types ───────────────────────────────────────────────────────────────
26
26
 
@@ -88,12 +88,12 @@ Respond with ONLY the function code, no explanations.`,
88
88
  { test: /(<=\s*0|===?\s*0|<\s*1)/.test(code), detail: "Handles edge case n=0" },
89
89
  { test: /(===?\s*1|<=\s*1)/.test(code), detail: "Handles edge case n=1" },
90
90
  ];
91
- const passed = checks.filter(c => c.test).length;
91
+ const passed = checks.filter((c) => c.test).length;
92
92
  const total = checks.length;
93
93
  return {
94
94
  passed: passed >= 5,
95
95
  score: Math.round((passed / total) * 100),
96
- details: checks.map(c => `${c.test ? "✅" : "❌"} ${c.detail}`).join("\n"),
96
+ details: checks.map((c) => `${c.test ? "✅" : "❌"} ${c.detail}`).join("\n"),
97
97
  };
98
98
  },
99
99
  },
@@ -125,16 +125,29 @@ Explain the bug and provide the fixed code.`,
125
125
  validate: (response: string) => {
126
126
  const lower = response.toLowerCase();
127
127
  const checks = [
128
- { test: /reference|shallow|copy|spread|\[\.\.\./.test(lower), detail: "Identifies reference/copy issue" },
129
- { test: /\[\.\.\.arr1\]|\[\.\.\.arr1,|Array\.from|\.slice\(\)|structuredClone|concat/.test(response), detail: "Uses spread/copy/concat fix" },
128
+ {
129
+ test: /reference|shallow|copy|spread|\[\.\.\./.test(lower),
130
+ detail: "Identifies reference/copy issue",
131
+ },
132
+ {
133
+ test: /\[\.\.\.arr1\]|\[\.\.\.arr1,|Array\.from|\.slice\(\)|structuredClone|concat/.test(response),
134
+ detail: "Uses spread/copy/concat fix",
135
+ },
130
136
  { test: /mutate|modify|original|side.?effect/.test(lower), detail: "Explains the mutation problem" },
131
- { test: /const result\s*=\s*\[/.test(response) || /\.slice\(/.test(response) || /\.concat\(/.test(response) || /Array\.from/.test(response), detail: "Creates new array in fix" },
137
+ {
138
+ test:
139
+ /const result\s*=\s*\[/.test(response) ||
140
+ /\.slice\(/.test(response) ||
141
+ /\.concat\(/.test(response) ||
142
+ /Array\.from/.test(response),
143
+ detail: "Creates new array in fix",
144
+ },
132
145
  ];
133
- const passed = checks.filter(c => c.test).length;
146
+ const passed = checks.filter((c) => c.test).length;
134
147
  return {
135
148
  passed: passed >= 3,
136
149
  score: Math.round((passed / checks.length) * 100),
137
- details: checks.map(c => `${c.test ? "✅" : "❌"} ${c.detail}`).join("\n"),
150
+ details: checks.map((c) => `${c.test ? "✅" : "❌"} ${c.detail}`).join("\n"),
138
151
  };
139
152
  },
140
153
  },
@@ -170,11 +183,11 @@ Provide a structured plan with specific files to create/modify, dependencies to
170
183
  { test: /env|secret|config/.test(lower), detail: "Addresses secret management" },
171
184
  { test: /step|phase|\d\.|create|modify|add/.test(lower), detail: "Provides structured steps" },
172
185
  ];
173
- const passed = checks.filter(c => c.test).length;
186
+ const passed = checks.filter((c) => c.test).length;
174
187
  return {
175
188
  passed: passed >= 6,
176
189
  score: Math.round((passed / checks.length) * 100),
177
- details: checks.map(c => `${c.test ? "✅" : "❌"} ${c.detail}`).join("\n"),
190
+ details: checks.map((c) => `${c.test ? "✅" : "❌"} ${c.detail}`).join("\n"),
178
191
  };
179
192
  },
180
193
  },
@@ -219,9 +232,15 @@ Output ONLY the JSON.`,
219
232
  checks.push({ test: obj?.name?.first === "Alice", detail: 'name.first = "Alice"' });
220
233
  checks.push({ test: obj?.name?.last === "Smith", detail: 'name.last = "Smith"' });
221
234
  checks.push({ test: obj?.email === "alice@example.com", detail: "Correct email" });
222
- checks.push({ test: typeof obj?.profile?.age === "number" && obj.profile.age === 28, detail: "Age is number 28" });
235
+ checks.push({
236
+ test: typeof obj?.profile?.age === "number" && obj.profile.age === 28,
237
+ detail: "Age is number 28",
238
+ });
223
239
  checks.push({ test: obj?.preferences?.theme === "dark", detail: 'theme = "dark"' });
224
- checks.push({ test: obj?.preferences?.notifications?.email === true, detail: "email notifications = true" });
240
+ checks.push({
241
+ test: obj?.preferences?.notifications?.email === true,
242
+ detail: "email notifications = true",
243
+ });
225
244
  } catch {
226
245
  checks.push({ test: false, detail: "Valid JSON (parse failed)" });
227
246
  checks.push({ test: false, detail: "name.first" });
@@ -232,11 +251,11 @@ Output ONLY the JSON.`,
232
251
  checks.push({ test: false, detail: "notifications" });
233
252
  }
234
253
 
235
- const passed = checks.filter(c => c.test).length;
254
+ const passed = checks.filter((c) => c.test).length;
236
255
  return {
237
256
  passed: passed >= 5,
238
257
  score: Math.round((passed / checks.length) * 100),
239
- details: checks.map(c => `${c.test ? "✅" : "❌"} ${c.detail}`).join("\n"),
258
+ details: checks.map((c) => `${c.test ? "✅" : "❌"} ${c.detail}`).join("\n"),
240
259
  };
241
260
  },
242
261
  },
@@ -248,7 +267,11 @@ Output ONLY the JSON.`,
248
267
  weight: 1,
249
268
  prompt: `Reply with exactly this text and nothing else: "Hello, World!"`,
250
269
  validate: (response: string) => {
251
- const trimmed = response.trim().replace(/^["']|["']$/g, "").replace(/```\w*\n?/g, "").trim();
270
+ const trimmed = response
271
+ .trim()
272
+ .replace(/^["']|["']$/g, "")
273
+ .replace(/```\w*\n?/g, "")
274
+ .trim();
252
275
  const exact = trimmed === "Hello, World!";
253
276
  const close = trimmed.toLowerCase().includes("hello, world");
254
277
  return {
@@ -287,15 +310,23 @@ Be specific and technical.`,
287
310
  { test: /memory.?leak|leak/.test(lower), detail: "Identifies memory leak" },
288
311
  { test: /stream|buffer|file|upload|temp|cleanup/.test(lower), detail: "Links to file upload handling" },
289
312
  { test: /close|destroy|cleanup|dispose|gc|garbage/.test(lower), detail: "Suggests resource cleanup" },
290
- { test: /event.?listener|handler|remove|off/.test(lower) || /stream|pipe/.test(lower), detail: "Checks for handler/stream leaks" },
291
- { test: /heapdump|heap.?snapshot|inspect|profile|--max-old-space/.test(lower) || /process\.memoryUsage/.test(lower), detail: "Suggests debugging tools" },
313
+ {
314
+ test: /event.?listener|handler|remove|off/.test(lower) || /stream|pipe/.test(lower),
315
+ detail: "Checks for handler/stream leaks",
316
+ },
317
+ {
318
+ test:
319
+ /heapdump|heap.?snapshot|inspect|profile|--max-old-space/.test(lower) ||
320
+ /process\.memoryUsage/.test(lower),
321
+ detail: "Suggests debugging tools",
322
+ },
292
323
  { test: /monitor|alert|metric|prometheus|grafana|threshold/.test(lower), detail: "Suggests monitoring" },
293
324
  ];
294
- const passed = checks.filter(c => c.test).length;
325
+ const passed = checks.filter((c) => c.test).length;
295
326
  return {
296
327
  passed: passed >= 4,
297
328
  score: Math.round((passed / checks.length) * 100),
298
- details: checks.map(c => `${c.test ? "✅" : "❌"} ${c.detail}`).join("\n"),
329
+ details: checks.map((c) => `${c.test ? "✅" : "❌"} ${c.detail}`).join("\n"),
299
330
  };
300
331
  },
301
332
  },
@@ -322,7 +353,16 @@ function getProviderConfigs(): ProviderConfig[] {
322
353
  name: "alibaba-codingplan",
323
354
  envVar: "ALIBABA_CODING_PLAN_KEY",
324
355
  baseUrl: "https://coding-intl.dashscope.aliyuncs.com/v1",
325
- models: ["qwen3.5-plus", "qwen3-max-2026-01-23", "qwen3-coder-plus", "qwen3-coder-next", "kimi-k2.5", "glm-5", "glm-4.7", "MiniMax-M2.5"],
356
+ models: [
357
+ "qwen3.5-plus",
358
+ "qwen3-max-2026-01-23",
359
+ "qwen3-coder-plus",
360
+ "qwen3-coder-next",
361
+ "kimi-k2.5",
362
+ "glm-5",
363
+ "glm-4.7",
364
+ "MiniMax-M2.5",
365
+ ],
326
366
  },
327
367
  {
328
368
  name: "openai",
@@ -386,14 +426,16 @@ async function getAvailableModels(): Promise<Array<{ id: string; provider: strin
386
426
  for (const m of providerConfig.models) {
387
427
  const modelId = typeof m === "string" ? m : m.id;
388
428
  // Skip if already added from env vars
389
- if (!models.some(existing => existing.id === modelId && existing.baseUrl === baseUrl)) {
429
+ if (!models.some((existing) => existing.id === modelId && existing.baseUrl === baseUrl)) {
390
430
  models.push({ id: modelId, provider: id, baseUrl, apiKey });
391
431
  }
392
432
  }
393
433
  }
394
434
  }
395
435
  }
396
- } catch { /* ignore parse errors */ }
436
+ } catch {
437
+ /* ignore parse errors */
438
+ }
397
439
  }
398
440
 
399
441
  // 3. Try to detect LM Studio (port 1234) and Ollama (port 11434) directly
@@ -402,7 +444,7 @@ async function getAvailableModels(): Promise<Array<{ id: string; provider: strin
402
444
  { name: "ollama", port: 11434, baseUrl: "http://localhost:11434/v1" },
403
445
  ]) {
404
446
  // Skip if already discovered via models.json
405
- if (models.some(m => m.baseUrl === local.baseUrl)) continue;
447
+ if (models.some((m) => m.baseUrl === local.baseUrl)) continue;
406
448
 
407
449
  try {
408
450
  const controller = new AbortController();
@@ -411,16 +453,18 @@ async function getAvailableModels(): Promise<Array<{ id: string; provider: strin
411
453
  clearTimeout(timeout);
412
454
 
413
455
  if (resp.ok) {
414
- const data = await resp.json() as any;
456
+ const data = (await resp.json()) as any;
415
457
  const modelList = data?.data || [];
416
458
  for (const m of modelList) {
417
459
  const modelId = m.id || m.name;
418
- if (modelId && !models.some(existing => existing.id === modelId)) {
460
+ if (modelId && !models.some((existing) => existing.id === modelId)) {
419
461
  models.push({ id: modelId, provider: local.name, baseUrl: local.baseUrl, apiKey: "local" });
420
462
  }
421
463
  }
422
464
  }
423
- } catch { /* not running */ }
465
+ } catch {
466
+ /* not running */
467
+ }
424
468
  }
425
469
 
426
470
  return models;
@@ -514,7 +558,7 @@ export default function benchmarkExtension(pi: ExtensionAPI) {
514
558
  for (let testIdx = 0; testIdx < tests.length; testIdx++) {
515
559
  const test = tests[testIdx];
516
560
  // Rate limiting: 1.5s between API calls to avoid throttling
517
- if (testIdx > 0) await new Promise(r => setTimeout(r, 1500));
561
+ if (testIdx > 0) await new Promise((r) => setTimeout(r, 1500));
518
562
  ctx.ui.notify(` ⏳ ${test.category}: ${test.name}...`, "info");
519
563
 
520
564
  try {
@@ -586,15 +630,24 @@ export default function benchmarkExtension(pi: ExtensionAPI) {
586
630
  report += "**Leaderboard:**\n";
587
631
  sorted.forEach((r, i) => {
588
632
  const medal = i === 0 ? "🥇" : i === 1 ? "🥈" : i === 2 ? "🥉" : `${i + 1}.`;
589
- const tier = r.totalScore >= 80 ? "S" : r.totalScore >= 65 ? "A" : r.totalScore >= 50 ? "B" : r.totalScore >= 35 ? "C" : "D";
633
+ const tier =
634
+ r.totalScore >= 80
635
+ ? "S"
636
+ : r.totalScore >= 65
637
+ ? "A"
638
+ : r.totalScore >= 50
639
+ ? "B"
640
+ : r.totalScore >= 35
641
+ ? "C"
642
+ : "D";
590
643
  report += `${medal} **${r.modelId}** — ${r.totalScore}/100 [${tier}] (avg ${r.avgTimeMs}ms)\n`;
591
644
  });
592
645
 
593
646
  // Category breakdown
594
647
  report += "\n**Category Breakdown:**\n```\n";
595
- const header = "Model".padEnd(25) + categories.map(c => c.substring(0, 8).padEnd(10)).join("") + "TOTAL\n";
648
+ const header = `${"Model".padEnd(25) + categories.map((c) => c.substring(0, 8).padEnd(10)).join("")}TOTAL\n`;
596
649
  report += header;
597
- report += "-".repeat(header.length) + "\n";
650
+ report += `${"-".repeat(header.length)}\n`;
598
651
 
599
652
  for (const r of sorted) {
600
653
  let line = r.modelId.substring(0, 24).padEnd(25);
@@ -603,7 +656,7 @@ export default function benchmarkExtension(pi: ExtensionAPI) {
603
656
  line += String(score).padEnd(10);
604
657
  }
605
658
  line += String(r.totalScore);
606
- report += line + "\n";
659
+ report += `${line}\n`;
607
660
  }
608
661
  report += "```\n";
609
662
 
@@ -627,7 +680,8 @@ export default function benchmarkExtension(pi: ExtensionAPI) {
627
680
  // ─── Command ─────────────────────────────────────────────────────────
628
681
 
629
682
  pi.registerCommand("benchmark", {
630
- description: "Run AI model benchmarks (6 categories: code-gen, debug, planning, tool-calling, speed, orchestration)",
683
+ description:
684
+ "Run AI model benchmarks (6 categories: code-gen, debug, planning, tool-calling, speed, orchestration)",
631
685
  handler: async (args, ctx) => {
632
686
  const arg = args.trim().toLowerCase();
633
687
 
@@ -658,7 +712,8 @@ export default function benchmarkExtension(pi: ExtensionAPI) {
658
712
 
659
713
  // Help
660
714
  if (arg === "help" || arg === "?") {
661
- ctx.ui.notify(`**Phi Code Benchmark** — 6 categories, real API calls
715
+ ctx.ui.notify(
716
+ `**Phi Code Benchmark** — 6 categories, real API calls
662
717
 
663
718
  Commands:
664
719
  /benchmark Run on current model
@@ -676,7 +731,9 @@ Categories tested (weighted):
676
731
  ⏱️ speed (×1) — Response latency
677
732
  🧩 orchestration (×2) — Multi-step analysis
678
733
 
679
- Scoring: S (80+), A (65+), B (50+), C (35+), D (<35)`, "info");
734
+ Scoring: S (80+), A (65+), B (50+), C (35+), D (<35)`,
735
+ "info",
736
+ );
680
737
  return;
681
738
  }
682
739
 
@@ -684,8 +741,13 @@ Scoring: S (80+), A (65+), B (50+), C (35+), D (<35)`, "info");
684
741
  const available = await getAvailableModels();
685
742
  if (available.length === 0) {
686
743
  const providers = getProviderConfigs();
687
- const hint = providers.map(p => ` ${p.envVar}: ${process.env[p.envVar] ? "set but no models configured" : "not set"}`).join("\n");
688
- ctx.ui.notify(`❌ No benchmarkable models found.\n\nProvider status:\n${hint}\n\nSet at least one API key with known models.`, "warning");
744
+ const hint = providers
745
+ .map((p) => ` ${p.envVar}: ${process.env[p.envVar] ? "set but no models configured" : "not set"}`)
746
+ .join("\n");
747
+ ctx.ui.notify(
748
+ `❌ No benchmarkable models found.\n\nProvider status:\n${hint}\n\nSet at least one API key with known models.`,
749
+ "warning",
750
+ );
689
751
  return;
690
752
  }
691
753
 
@@ -700,7 +762,7 @@ Scoring: S (80+), A (65+), B (50+), C (35+), D (<35)`, "info");
700
762
  const result = await benchmarkModel(model.id, model.provider, model.baseUrl, model.apiKey, ctx);
701
763
 
702
764
  // Replace existing result for this model
703
- store.results = store.results.filter(r => r.modelId !== model.id);
765
+ store.results = store.results.filter((r) => r.modelId !== model.id);
704
766
  store.results.push(result);
705
767
  await saveStore(store);
706
768
  }
@@ -713,21 +775,27 @@ Scoring: S (80+), A (65+), B (50+), C (35+), D (<35)`, "info");
713
775
  if (arg) {
714
776
  // Benchmark specific model: prefer exact id match, then require an
715
777
  // unambiguous substring match to avoid benchmarking the wrong model.
716
- const exact = available.find(m => m.id.toLowerCase() === arg);
717
- const matches = available.filter(m => m.id.toLowerCase().includes(arg));
778
+ const exact = available.find((m) => m.id.toLowerCase() === arg);
779
+ const matches = available.filter((m) => m.id.toLowerCase().includes(arg));
718
780
  const model = exact ?? (matches.length === 1 ? matches[0] : undefined);
719
781
  if (!model) {
720
782
  if (!exact && matches.length > 1) {
721
- ctx.ui.notify(`Model "${arg}" is ambiguous. Did you mean:\n${matches.map(m => ` - ${m.id} (${m.provider})`).join("\n")}`, "warning");
783
+ ctx.ui.notify(
784
+ `Model "${arg}" is ambiguous. Did you mean:\n${matches.map((m) => ` - ${m.id} (${m.provider})`).join("\n")}`,
785
+ "warning",
786
+ );
722
787
  return;
723
788
  }
724
- ctx.ui.notify(`Model "${arg}" not found or no API key. Available:\n${available.map(m => ` - ${m.id} (${m.provider})`).join("\n")}`, "warning");
789
+ ctx.ui.notify(
790
+ `Model "${arg}" not found or no API key. Available:\n${available.map((m) => ` - ${m.id} (${m.provider})`).join("\n")}`,
791
+ "warning",
792
+ );
725
793
  return;
726
794
  }
727
795
 
728
796
  ctx.ui.notify(`🧪 Benchmarking **${model.id}** (6 categories)...\n`, "info");
729
797
  const result = await benchmarkModel(model.id, model.provider, model.baseUrl, model.apiKey, ctx);
730
- store.results = store.results.filter(r => r.modelId !== model.id);
798
+ store.results = store.results.filter((r) => r.modelId !== model.id);
731
799
  store.results.push(result);
732
800
  await saveStore(store);
733
801
 
@@ -739,11 +807,17 @@ Scoring: S (80+), A (65+), B (50+), C (35+), D (<35)`, "info");
739
807
  // Try to find current model in available list
740
808
  const currentModel = ctx.model;
741
809
  if (currentModel) {
742
- const modelConfig = available.find(m => m.id === currentModel.id);
810
+ const modelConfig = available.find((m) => m.id === currentModel.id);
743
811
  if (modelConfig) {
744
812
  ctx.ui.notify(`🧪 Benchmarking current model **${currentModel.id}** (6 categories)...\n`, "info");
745
- const result = await benchmarkModel(modelConfig.id, modelConfig.provider, modelConfig.baseUrl, modelConfig.apiKey, ctx);
746
- store.results = store.results.filter(r => r.modelId !== modelConfig.id);
813
+ const result = await benchmarkModel(
814
+ modelConfig.id,
815
+ modelConfig.provider,
816
+ modelConfig.baseUrl,
817
+ modelConfig.apiKey,
818
+ ctx,
819
+ );
820
+ store.results = store.results.filter((r) => r.modelId !== modelConfig.id);
747
821
  store.results.push(result);
748
822
  await saveStore(store);
749
823
  ctx.ui.notify(`\n✅ **${currentModel.id}** — Total: ${result.totalScore}/100`, "info");
@@ -752,7 +826,10 @@ Scoring: S (80+), A (65+), B (50+), C (35+), D (<35)`, "info");
752
826
  }
753
827
 
754
828
  // Fallback: show available models
755
- ctx.ui.notify(`Available models for benchmark:\n${available.map(m => ` - ${m.id} (${m.provider})`).join("\n")}\n\nUsage: /benchmark <model-id> or /benchmark all`, "info");
829
+ ctx.ui.notify(
830
+ `Available models for benchmark:\n${available.map((m) => ` - ${m.id} (${m.provider})`).join("\n")}\n\nUsage: /benchmark <model-id> or /benchmark all`,
831
+ "info",
832
+ );
756
833
  },
757
834
  });
758
835
 
@@ -761,7 +838,10 @@ Scoring: S (80+), A (65+), B (50+), C (35+), D (<35)`, "info");
761
838
  try {
762
839
  const store = await loadStore();
763
840
  if (store.results.length > 0) {
764
- ctx.ui.notify(`🧪 ${store.results.length} benchmark results available. /benchmark results to view.`, "info");
841
+ ctx.ui.notify(
842
+ `🧪 ${store.results.length} benchmark results available. /benchmark results to view.`,
843
+ "info",
844
+ );
765
845
  }
766
846
  } catch {
767
847
  // ignore