@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.
- package/CHANGELOG.md +104 -0
- package/docs/adr/0001-phase-contract.md +57 -0
- package/docs/adr/0002-independent-review.md +47 -0
- package/docs/fork-policy.md +18 -0
- package/extensions/phi/agents.ts +7 -4
- package/extensions/phi/benchmark.ts +129 -49
- package/extensions/phi/browser.ts +10 -37
- package/extensions/phi/btw/btw.ts +1 -7
- package/extensions/phi/chrome/index.ts +1283 -741
- package/extensions/phi/commit.ts +9 -14
- package/extensions/phi/goal/index.ts +10 -33
- package/extensions/phi/init.ts +37 -47
- package/extensions/phi/keys.ts +2 -7
- package/extensions/phi/mcp/callback-server.ts +162 -165
- package/extensions/phi/mcp/config.ts +122 -136
- package/extensions/phi/mcp/errors.ts +18 -23
- package/extensions/phi/mcp/index.ts +322 -355
- package/extensions/phi/mcp/oauth-provider.ts +289 -289
- package/extensions/phi/mcp/server-manager.ts +390 -413
- package/extensions/phi/mcp/tool-bridge.ts +381 -415
- package/extensions/phi/memory.ts +2 -2
- package/extensions/phi/models.ts +27 -26
- package/extensions/phi/orchestrator.ts +451 -237
- package/extensions/phi/productivity.ts +4 -2
- package/extensions/phi/providers/agent-def.ts +1 -1
- package/extensions/phi/providers/alibaba.ts +56 -7
- package/extensions/phi/providers/context-window.ts +4 -1
- package/extensions/phi/providers/live-models.ts +5 -20
- package/extensions/phi/providers/orchestrator-helpers.ts +31 -5
- package/extensions/phi/providers/phase-machine.ts +283 -0
- package/extensions/phi/setup.ts +196 -169
- package/extensions/phi/skill-loader.ts +14 -17
- package/extensions/phi/smart-router.ts +6 -6
- package/extensions/phi/web-search.ts +90 -50
- 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.
|
package/docs/fork-policy.md
CHANGED
|
@@ -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.
|
package/extensions/phi/agents.ts
CHANGED
|
@@ -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(
|
|
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
|
|
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
|
-
{
|
|
129
|
-
|
|
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
|
-
{
|
|
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({
|
|
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({
|
|
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
|
|
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
|
-
{
|
|
291
|
-
|
|
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: [
|
|
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 {
|
|
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 {
|
|
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 =
|
|
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("")
|
|
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)
|
|
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
|
|
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:
|
|
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(
|
|
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)`,
|
|
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
|
|
688
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
746
|
-
|
|
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(
|
|
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(
|
|
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
|