@phi-code-admin/phi-code 0.90.0 → 0.92.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 CHANGED
@@ -1,5 +1,59 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.92.0] - 2026-07-11
4
+
5
+ ### Fixed — the two defects the n=13 SWE-bench measurement exposed
6
+
7
+ The measured head-to-head (baseline 7/13 vs /debug 6/13, official harness) traced
8
+ /debug's two lost points to concrete, reparable defects — both fixed here.
9
+
10
+ - **Hasty BLOCKED (sympy-11897, halted at 51s).** The generic /debug//build
11
+ driver now routes phase completion through the same pure decision machine as
12
+ /plan (`decidePhaseTransition`), gaining what it measurably lacked: a fatal
13
+ 401 stops the run, a transient provider error (timeout / 5xx / 429 / broken
14
+ stream) retries the SAME phase once on the fallback model instead of being
15
+ swallowed, and — new — a BLOCKED verdict gets ONE second chance on the
16
+ fallback model (a different family re-checks the reproduction) before the
17
+ halt. A confirmed second BLOCKED still halts honestly: no fabricated fix.
18
+ REPRODUCE's instructions now state BLOCKED is a last resort — adjust and
19
+ retry once before concluding.
20
+ - **Timeout-blind VERIFY (sympy-11870, fix passed its repro but hung the real
21
+ suite for 900s).** VERIFY's instructions now treat a TIMEOUT of the
22
+ reproduction or the suite as a FAILURE of the candidate — a massive slowdown
23
+ or infinite loop IS a regression — with verdict BLOCKED, never FIXED. A
24
+ dramatic duration increase below the timeout is flagged as a red flag too.
25
+
26
+ 8 integration tests cover the new behaviours (BLOCKED retry → honest halt,
27
+ BLOCKED retry → proceed on success, transient-error retry); full suite green
28
+ (1439).
29
+
30
+ ## [0.91.0] - 2026-07-11
31
+
32
+ ### Added — Alibaba Cloud Coding Plan, first-class and one-command
33
+
34
+ The Alibaba Coding Plan support (endpoints, live catalog, ping, wizard) already
35
+ existed but was only reachable through the interactive `/setup` wizard — a user
36
+ could not simply add the provider. Verified against the official docs
37
+ (https://www.alibabacloud.com/help/en/model-studio/coding-plan) and a live API
38
+ round-trip (catalog listing + a real `qwen3.7-plus` completion).
39
+
40
+ - **`/keys set` now bootstraps known providers.** `providers/provider-bootstrap.ts`
41
+ (pure, 11 tests) builds a complete first-time models.json entry — baseUrl, api
42
+ protocol, bundled model list — from a single API key, with each provider's own
43
+ key-format validation applied up front (`sk-sp-` prefix for Alibaba). One
44
+ command configures the provider end to end:
45
+ `/keys set alibaba-codingplan sk-sp-…` — then the session-start refresh
46
+ replaces the bundled list with the live catalog. Bootstrappable ids:
47
+ `alibaba-codingplan`, `alibaba-codingplan-anthropic`, `opencode-go`,
48
+ `opencode-go-anthropic`. Unknown providers keep the previous /setup guidance.
49
+ - **`/keys test` now actually works.** `registerProviderPing` existed but nothing
50
+ ever called it; keys.ts now registers the built-in pings (Alibaba, OpenCode Go)
51
+ at load, so `/keys test alibaba-codingplan` validates the key against the real
52
+ `/v1/models` endpoint.
53
+ - **Provider display names.** `alibaba-codingplan`, `alibaba-codingplan-anthropic`
54
+ and `opencode-go-anthropic` now render with proper names in the /model picker
55
+ (they fell back to raw ids).
56
+
3
57
  ## [0.90.0] - 2026-07-11
4
58
 
5
59
  ### Added — the execution sandbox (the guaranteed oracle)
@@ -1 +1 @@
1
- {"version":3,"file":"provider-display-names.d.ts","sourceRoot":"","sources":["../../src/core/provider-display-names.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,+BAA+B,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CA+BlE,CAAC","sourcesContent":["export const BUILT_IN_PROVIDER_DISPLAY_NAMES: Record<string, string> = {\n\tanthropic: \"Anthropic\",\n\t\"amazon-bedrock\": \"Amazon Bedrock\",\n\t\"azure-openai-responses\": \"Azure OpenAI Responses\",\n\tcerebras: \"Cerebras\",\n\t\"cloudflare-ai-gateway\": \"Cloudflare AI Gateway\",\n\t\"cloudflare-workers-ai\": \"Cloudflare Workers AI\",\n\tdeepseek: \"DeepSeek\",\n\tfireworks: \"Fireworks\",\n\tgoogle: \"Google Gemini\",\n\t\"google-vertex\": \"Google Vertex AI\",\n\tgroq: \"Groq\",\n\thuggingface: \"Hugging Face\",\n\t\"kimi-coding\": \"Kimi For Coding\",\n\tmistral: \"Mistral\",\n\tminimax: \"MiniMax\",\n\t\"minimax-cn\": \"MiniMax (China)\",\n\tmoonshotai: \"Moonshot AI\",\n\t\"moonshotai-cn\": \"Moonshot AI (China)\",\n\topencode: \"OpenCode Zen\",\n\t\"opencode-go\": \"OpenCode Go\",\n\topenai: \"OpenAI\",\n\topenrouter: \"OpenRouter\",\n\ttogether: \"Together AI\",\n\t\"vercel-ai-gateway\": \"Vercel AI Gateway\",\n\txai: \"xAI\",\n\tzai: \"ZAI\",\n\txiaomi: \"Xiaomi MiMo\",\n\t\"xiaomi-token-plan-cn\": \"Xiaomi MiMo Token Plan (China)\",\n\t\"xiaomi-token-plan-ams\": \"Xiaomi MiMo Token Plan (Amsterdam)\",\n\t\"xiaomi-token-plan-sgp\": \"Xiaomi MiMo Token Plan (Singapore)\",\n};\n"]}
1
+ {"version":3,"file":"provider-display-names.d.ts","sourceRoot":"","sources":["../../src/core/provider-display-names.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,+BAA+B,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAkClE,CAAC","sourcesContent":["export const BUILT_IN_PROVIDER_DISPLAY_NAMES: Record<string, string> = {\n\t\"alibaba-codingplan\": \"Alibaba Coding Plan\",\n\t\"alibaba-codingplan-anthropic\": \"Alibaba Coding Plan (Anthropic-compat)\",\n\tanthropic: \"Anthropic\",\n\t\"amazon-bedrock\": \"Amazon Bedrock\",\n\t\"azure-openai-responses\": \"Azure OpenAI Responses\",\n\tcerebras: \"Cerebras\",\n\t\"cloudflare-ai-gateway\": \"Cloudflare AI Gateway\",\n\t\"cloudflare-workers-ai\": \"Cloudflare Workers AI\",\n\tdeepseek: \"DeepSeek\",\n\tfireworks: \"Fireworks\",\n\tgoogle: \"Google Gemini\",\n\t\"google-vertex\": \"Google Vertex AI\",\n\tgroq: \"Groq\",\n\thuggingface: \"Hugging Face\",\n\t\"kimi-coding\": \"Kimi For Coding\",\n\tmistral: \"Mistral\",\n\tminimax: \"MiniMax\",\n\t\"minimax-cn\": \"MiniMax (China)\",\n\tmoonshotai: \"Moonshot AI\",\n\t\"moonshotai-cn\": \"Moonshot AI (China)\",\n\topencode: \"OpenCode Zen\",\n\t\"opencode-go\": \"OpenCode Go\",\n\t\"opencode-go-anthropic\": \"OpenCode Go (Anthropic-compat)\",\n\topenai: \"OpenAI\",\n\topenrouter: \"OpenRouter\",\n\ttogether: \"Together AI\",\n\t\"vercel-ai-gateway\": \"Vercel AI Gateway\",\n\txai: \"xAI\",\n\tzai: \"ZAI\",\n\txiaomi: \"Xiaomi MiMo\",\n\t\"xiaomi-token-plan-cn\": \"Xiaomi MiMo Token Plan (China)\",\n\t\"xiaomi-token-plan-ams\": \"Xiaomi MiMo Token Plan (Amsterdam)\",\n\t\"xiaomi-token-plan-sgp\": \"Xiaomi MiMo Token Plan (Singapore)\",\n};\n"]}
@@ -1,4 +1,6 @@
1
1
  export const BUILT_IN_PROVIDER_DISPLAY_NAMES = {
2
+ "alibaba-codingplan": "Alibaba Coding Plan",
3
+ "alibaba-codingplan-anthropic": "Alibaba Coding Plan (Anthropic-compat)",
2
4
  anthropic: "Anthropic",
3
5
  "amazon-bedrock": "Amazon Bedrock",
4
6
  "azure-openai-responses": "Azure OpenAI Responses",
@@ -19,6 +21,7 @@ export const BUILT_IN_PROVIDER_DISPLAY_NAMES = {
19
21
  "moonshotai-cn": "Moonshot AI (China)",
20
22
  opencode: "OpenCode Zen",
21
23
  "opencode-go": "OpenCode Go",
24
+ "opencode-go-anthropic": "OpenCode Go (Anthropic-compat)",
22
25
  openai: "OpenAI",
23
26
  openrouter: "OpenRouter",
24
27
  together: "Together AI",
@@ -1 +1 @@
1
- {"version":3,"file":"provider-display-names.js","sourceRoot":"","sources":["../../src/core/provider-display-names.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,+BAA+B,GAA2B;IACtE,SAAS,EAAE,WAAW;IACtB,gBAAgB,EAAE,gBAAgB;IAClC,wBAAwB,EAAE,wBAAwB;IAClD,QAAQ,EAAE,UAAU;IACpB,uBAAuB,EAAE,uBAAuB;IAChD,uBAAuB,EAAE,uBAAuB;IAChD,QAAQ,EAAE,UAAU;IACpB,SAAS,EAAE,WAAW;IACtB,MAAM,EAAE,eAAe;IACvB,eAAe,EAAE,kBAAkB;IACnC,IAAI,EAAE,MAAM;IACZ,WAAW,EAAE,cAAc;IAC3B,aAAa,EAAE,iBAAiB;IAChC,OAAO,EAAE,SAAS;IAClB,OAAO,EAAE,SAAS;IAClB,YAAY,EAAE,iBAAiB;IAC/B,UAAU,EAAE,aAAa;IACzB,eAAe,EAAE,qBAAqB;IACtC,QAAQ,EAAE,cAAc;IACxB,aAAa,EAAE,aAAa;IAC5B,MAAM,EAAE,QAAQ;IAChB,UAAU,EAAE,YAAY;IACxB,QAAQ,EAAE,aAAa;IACvB,mBAAmB,EAAE,mBAAmB;IACxC,GAAG,EAAE,KAAK;IACV,GAAG,EAAE,KAAK;IACV,MAAM,EAAE,aAAa;IACrB,sBAAsB,EAAE,gCAAgC;IACxD,uBAAuB,EAAE,oCAAoC;IAC7D,uBAAuB,EAAE,oCAAoC;CAC7D,CAAC","sourcesContent":["export const BUILT_IN_PROVIDER_DISPLAY_NAMES: Record<string, string> = {\n\tanthropic: \"Anthropic\",\n\t\"amazon-bedrock\": \"Amazon Bedrock\",\n\t\"azure-openai-responses\": \"Azure OpenAI Responses\",\n\tcerebras: \"Cerebras\",\n\t\"cloudflare-ai-gateway\": \"Cloudflare AI Gateway\",\n\t\"cloudflare-workers-ai\": \"Cloudflare Workers AI\",\n\tdeepseek: \"DeepSeek\",\n\tfireworks: \"Fireworks\",\n\tgoogle: \"Google Gemini\",\n\t\"google-vertex\": \"Google Vertex AI\",\n\tgroq: \"Groq\",\n\thuggingface: \"Hugging Face\",\n\t\"kimi-coding\": \"Kimi For Coding\",\n\tmistral: \"Mistral\",\n\tminimax: \"MiniMax\",\n\t\"minimax-cn\": \"MiniMax (China)\",\n\tmoonshotai: \"Moonshot AI\",\n\t\"moonshotai-cn\": \"Moonshot AI (China)\",\n\topencode: \"OpenCode Zen\",\n\t\"opencode-go\": \"OpenCode Go\",\n\topenai: \"OpenAI\",\n\topenrouter: \"OpenRouter\",\n\ttogether: \"Together AI\",\n\t\"vercel-ai-gateway\": \"Vercel AI Gateway\",\n\txai: \"xAI\",\n\tzai: \"ZAI\",\n\txiaomi: \"Xiaomi MiMo\",\n\t\"xiaomi-token-plan-cn\": \"Xiaomi MiMo Token Plan (China)\",\n\t\"xiaomi-token-plan-ams\": \"Xiaomi MiMo Token Plan (Amsterdam)\",\n\t\"xiaomi-token-plan-sgp\": \"Xiaomi MiMo Token Plan (Singapore)\",\n};\n"]}
1
+ {"version":3,"file":"provider-display-names.js","sourceRoot":"","sources":["../../src/core/provider-display-names.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,+BAA+B,GAA2B;IACtE,oBAAoB,EAAE,qBAAqB;IAC3C,8BAA8B,EAAE,wCAAwC;IACxE,SAAS,EAAE,WAAW;IACtB,gBAAgB,EAAE,gBAAgB;IAClC,wBAAwB,EAAE,wBAAwB;IAClD,QAAQ,EAAE,UAAU;IACpB,uBAAuB,EAAE,uBAAuB;IAChD,uBAAuB,EAAE,uBAAuB;IAChD,QAAQ,EAAE,UAAU;IACpB,SAAS,EAAE,WAAW;IACtB,MAAM,EAAE,eAAe;IACvB,eAAe,EAAE,kBAAkB;IACnC,IAAI,EAAE,MAAM;IACZ,WAAW,EAAE,cAAc;IAC3B,aAAa,EAAE,iBAAiB;IAChC,OAAO,EAAE,SAAS;IAClB,OAAO,EAAE,SAAS;IAClB,YAAY,EAAE,iBAAiB;IAC/B,UAAU,EAAE,aAAa;IACzB,eAAe,EAAE,qBAAqB;IACtC,QAAQ,EAAE,cAAc;IACxB,aAAa,EAAE,aAAa;IAC5B,uBAAuB,EAAE,gCAAgC;IACzD,MAAM,EAAE,QAAQ;IAChB,UAAU,EAAE,YAAY;IACxB,QAAQ,EAAE,aAAa;IACvB,mBAAmB,EAAE,mBAAmB;IACxC,GAAG,EAAE,KAAK;IACV,GAAG,EAAE,KAAK;IACV,MAAM,EAAE,aAAa;IACrB,sBAAsB,EAAE,gCAAgC;IACxD,uBAAuB,EAAE,oCAAoC;IAC7D,uBAAuB,EAAE,oCAAoC;CAC7D,CAAC","sourcesContent":["export const BUILT_IN_PROVIDER_DISPLAY_NAMES: Record<string, string> = {\n\t\"alibaba-codingplan\": \"Alibaba Coding Plan\",\n\t\"alibaba-codingplan-anthropic\": \"Alibaba Coding Plan (Anthropic-compat)\",\n\tanthropic: \"Anthropic\",\n\t\"amazon-bedrock\": \"Amazon Bedrock\",\n\t\"azure-openai-responses\": \"Azure OpenAI Responses\",\n\tcerebras: \"Cerebras\",\n\t\"cloudflare-ai-gateway\": \"Cloudflare AI Gateway\",\n\t\"cloudflare-workers-ai\": \"Cloudflare Workers AI\",\n\tdeepseek: \"DeepSeek\",\n\tfireworks: \"Fireworks\",\n\tgoogle: \"Google Gemini\",\n\t\"google-vertex\": \"Google Vertex AI\",\n\tgroq: \"Groq\",\n\thuggingface: \"Hugging Face\",\n\t\"kimi-coding\": \"Kimi For Coding\",\n\tmistral: \"Mistral\",\n\tminimax: \"MiniMax\",\n\t\"minimax-cn\": \"MiniMax (China)\",\n\tmoonshotai: \"Moonshot AI\",\n\t\"moonshotai-cn\": \"Moonshot AI (China)\",\n\topencode: \"OpenCode Zen\",\n\t\"opencode-go\": \"OpenCode Go\",\n\t\"opencode-go-anthropic\": \"OpenCode Go (Anthropic-compat)\",\n\topenai: \"OpenAI\",\n\topenrouter: \"OpenRouter\",\n\ttogether: \"Together AI\",\n\t\"vercel-ai-gateway\": \"Vercel AI Gateway\",\n\txai: \"xAI\",\n\tzai: \"ZAI\",\n\txiaomi: \"Xiaomi MiMo\",\n\t\"xiaomi-token-plan-cn\": \"Xiaomi MiMo Token Plan (China)\",\n\t\"xiaomi-token-plan-ams\": \"Xiaomi MiMo Token Plan (Amsterdam)\",\n\t\"xiaomi-token-plan-sgp\": \"Xiaomi MiMo Token Plan (Singapore)\",\n};\n"]}
@@ -21,6 +21,9 @@
21
21
  */
22
22
 
23
23
  import { ApiKeyStore, type ExtensionAPI, getApiKeyStore, getConfigWatcher } from "phi-code";
24
+ import { pingAlibaba } from "./providers/alibaba.js";
25
+ import { pingOpenCodeGo } from "./providers/opencode-go.js";
26
+ import { bootstrapProviderConfig, isBootstrappableProvider } from "./providers/provider-bootstrap.js";
24
27
 
25
28
  type ProviderPingFn = (key: string, timeoutMs?: number) => Promise<{ ok: boolean; error?: string }>;
26
29
 
@@ -38,6 +41,13 @@ export default function keysExtension(pi: ExtensionAPI) {
38
41
  const store = getApiKeyStore();
39
42
  const watcher = getConfigWatcher();
40
43
 
44
+ // Built-in pings so `/keys test <id>` works out of the box for the providers
45
+ // phi ships builders for (previously nothing ever registered a ping).
46
+ registerProviderPing("alibaba-codingplan", pingAlibaba);
47
+ registerProviderPing("alibaba-codingplan-anthropic", pingAlibaba);
48
+ registerProviderPing("opencode-go", pingOpenCodeGo);
49
+ registerProviderPing("opencode-go-anthropic", pingOpenCodeGo);
50
+
41
51
  pi.registerCommand("keys", {
42
52
  description: "Manage API keys (list / set / remove / test / reload)",
43
53
  handler: async (args, ctx) => {
@@ -75,15 +85,35 @@ export default function keysExtension(pi: ExtensionAPI) {
75
85
  return;
76
86
  }
77
87
  // A bare `set` only updates the key; it cannot supply baseUrl/api/models.
78
- // For a never-configured id this would persist an unusable provider
79
- // (apiKey but no endpoint), so require an existing entry with a baseUrl.
88
+ // For a never-configured id: bootstrap the full entry when phi ships a
89
+ // builder for it (Alibaba Coding Plan, OpenCode Go); otherwise keep the
90
+ // old behaviour and point at /setup.
80
91
  const existing = store.getProvider(id);
81
92
  if (!existing?.baseUrl) {
93
+ if (!isBootstrappableProvider(id)) {
94
+ ctx.ui.notify(
95
+ `\`${id}\` is not a configured provider (no baseUrl on file). ` +
96
+ `Run \`/setup\` to add it with an endpoint and models first; ` +
97
+ `\`/keys set\` only updates the key of an already-configured provider.`,
98
+ "warning",
99
+ );
100
+ return;
101
+ }
102
+ const boot = bootstrapProviderConfig(id, key);
103
+ if (!boot.ok) {
104
+ ctx.ui.notify(`Cannot configure \`${id}\`: ${boot.error}`, "warning");
105
+ return;
106
+ }
107
+ watcher.muteForWrite("models_json_changed");
108
+ store.setKey(id, key, {
109
+ baseUrl: boot.config.baseUrl,
110
+ api: boot.config.api,
111
+ models: boot.config.models,
112
+ });
82
113
  ctx.ui.notify(
83
- `\`${id}\` is not a configured provider (no baseUrl on file). ` +
84
- `Run \`/setup\` to add it with an endpoint and models first; ` +
85
- `\`/keys set\` only updates the key of an already-configured provider.`,
86
- "warning",
114
+ `Configured \`${id}\` (\`${ApiKeyStore.maskKey(key)}\`) ${boot.note}\n` +
115
+ `Stored in ${store.configPath}. Use \`/keys test ${id}\` to validate, then pick a model with \`/model\`.`,
116
+ "info",
87
117
  );
88
118
  return;
89
119
  }
@@ -284,6 +284,11 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
284
284
  // Set true once this phase has been retried once (transient proxy failure /
285
285
  // timeout / 0-tool-call). Hard cap of one retry per phase.
286
286
  retried?: boolean;
287
+ // Generic driver (/debug, /build) only: set true once this phase was re-run
288
+ // after a BLOCKED verdict. One second chance on the fallback model — a hasty
289
+ // BLOCKED (measured: 51s on sympy-11897) is cheaper to re-check than to
290
+ // trust — then the halt is honest.
291
+ blockedRetried?: boolean;
287
292
  // When true, switchModelForPhase resolves the fallback model first (used on retry
288
293
  // to swap to a different model family rather than re-hit the one that just failed).
289
294
  useFallback?: boolean;
@@ -1323,13 +1328,63 @@ Tag the note with relevant keywords for vector search.
1323
1328
  phaseTimeoutId = null;
1324
1329
  }
1325
1330
 
1326
- // A BLOCKED verdict (REPRODUCE cannot reproduce / VERIFY could not confirm)
1327
- // halts the pipeline honestly no fabricated success.
1331
+ // Route through the SAME pure decision machine as /plan so the generic
1332
+ // driver gets the protections it was measured to lack (sympy-11897, a 51s
1333
+ // hasty halt): fatal 401 stop, one retry on a transient provider error.
1328
1334
  const structuredForThisPhase = currentPhaseResultKey === (currentPhase?.key ?? null) ? currentPhaseResult : null;
1329
1335
  const outcome = resolvePhaseOutcome(structuredForThisPhase, null);
1330
- if (outcome.verdict === "BLOCKED" && currentPhase) {
1336
+ const decision = decidePhaseTransition({
1337
+ analysis,
1338
+ phase: currentPhase ? { key: currentPhase.key, retried: currentPhase.retried } : null,
1339
+ verdict: outcome.verdict,
1340
+ // The generic driver has no review-fix cycle; pass the cap as spent.
1341
+ reviewFixRounds: 1,
1342
+ maxToolCallsPerPhase: MAX_TOOL_CALLS_PER_PHASE,
1343
+ });
1344
+
1345
+ if (decision.action === "stop") {
1346
+ ctx.ui.notify(
1347
+ `\n❌ **/${orchestrationMode} aborted:** API authentication error (401). Check your API key.`,
1348
+ "error",
1349
+ );
1350
+ finishGenericOrchestration(ctx, `❌ **/${orchestrationMode} aborted (auth error).**`);
1351
+ return;
1352
+ }
1353
+
1354
+ if (decision.action === "retry-fallback" && currentPhase) {
1355
+ // Transient provider failure (timeout / 5xx / 429 / broken stream): retry
1356
+ // the SAME phase once on the fallback model before treating any of its
1357
+ // output as a real outcome.
1358
+ currentPhase.retried = true;
1359
+ currentPhase.useFallback = true;
1360
+ phaseQueue.unshift(currentPhase);
1361
+ phasePending = false;
1362
+ ctx.ui.notify(
1363
+ `\n🔁 **Transient provider error** in ${currentPhase.label} — retrying once on the fallback model.`,
1364
+ "warning",
1365
+ );
1366
+ sendNextGenericPhase(ctx);
1367
+ return;
1368
+ }
1369
+
1370
+ // BLOCKED (REPRODUCE cannot reproduce / VERIFY could not confirm): give the
1371
+ // phase ONE second chance on the fallback model — a hasty BLOCKED is cheaper
1372
+ // to re-check than to trust — then halt honestly. No fabricated success.
1373
+ if (decision.action === "pause-blocked" && currentPhase) {
1374
+ if (!currentPhase.blockedRetried) {
1375
+ currentPhase.blockedRetried = true;
1376
+ currentPhase.useFallback = true;
1377
+ phaseQueue.unshift(currentPhase);
1378
+ phasePending = false;
1379
+ ctx.ui.notify(
1380
+ `\n🔁 **${currentPhase.label} reported BLOCKED on the first attempt** — retrying once on the fallback model before halting. ${outcome.handoff || ""}`,
1381
+ "warning",
1382
+ );
1383
+ sendNextGenericPhase(ctx);
1384
+ return;
1385
+ }
1331
1386
  ctx.ui.notify(
1332
- `\n⏸️ **${currentPhase.label} reported BLOCKED.** ${outcome.handoff || "No reproducible/verifiable result."}`,
1387
+ `\n⏸️ **${currentPhase.label} reported BLOCKED (confirmed on retry).** ${outcome.handoff || "No reproducible/verifiable result."}`,
1333
1388
  "warning",
1334
1389
  );
1335
1390
  finishGenericOrchestration(ctx, `⏸️ **/${orchestrationMode} stopped: BLOCKED at ${currentPhase.label}.**`);
@@ -59,7 +59,8 @@ ${failing}
59
59
  - If it PASSES → STOP. Write \`BLOCKED: cannot reproduce — passes on current code\` with the run pasted. Do not invent a bug.
60
60
  - If \`sandbox_run\` reports \`SANDBOX UNAVAILABLE\` → STOP. Write \`BLOCKED: no executable environment\`.
61
61
  4. Do NOT edit any source yet. This phase only observes.
62
- 5. **Last action:** call \`phase_result\` \`verdict: PASS\` if it reproduced (proceed to LOCALIZE), or \`verdict: BLOCKED\` with the reason if you stopped.` +
62
+ 5. **BLOCKED is a last resort, not a first reaction.** If a run fails for an infrastructure-looking reason (tool error, missing file you guessed wrong, transient failure), adjust and retry at least once — e.g. locate the real test paths, try an alternative reproduction — before concluding. Only report BLOCKED after a genuine attempt showed the state is not reproducible or not runnable.
63
+ 6. **Last action:** call \`phase_result\` — \`verdict: PASS\` if it reproduced (proceed to LOCALIZE), or \`verdict: BLOCKED\` with the reason if you stopped.` +
63
64
  DEBUG_RULES,
64
65
 
65
66
  localize: `You are the LOCALIZE agent (phase 2 of /debug). Drive from the REAL symptom the previous phase captured.
@@ -96,6 +97,7 @@ ${failing}
96
97
  3. Verdict (from what \`sandbox_run\` returned, not from inspection):
97
98
  - Both green → \`FIXED\`, and paste the before(fail)/after(pass) reproduction runs and the green suite as evidence.
98
99
  - Reproduction still fails, or the suite regresses → \`BLOCKED\` with the closest diagnostic. Do NOT ship the least-bad patch; a wrong fix is worse than an honest BLOCKED.
100
+ - **A TIMEOUT counts as a FAILURE.** If the reproduction or the suite TIMES OUT with the fix applied (\`sandbox_run\` verdict TIMEOUT), the fix has introduced a massive slowdown or an infinite loop — that IS a regression (measured: a fix that passed its repro but made the real test suite hang for 900s). Verdict \`BLOCKED\`, never \`FIXED\`. Compare rough durations before/after: a check that got dramatically slower is a red flag even below the timeout.
99
101
  4. Write the final verdict block, then call \`phase_result\` with \`verdict: PASS\` (FIXED) or \`verdict: BLOCKED\`, plus a one-line handoff:
100
102
  \`\`\`
101
103
  VERDICT: FIXED | BLOCKED
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Provider bootstrap — first-time, non-interactive provider configuration.
3
+ *
4
+ * `/keys set <id> <key>` used to refuse a provider that had no models.json
5
+ * entry yet ("run /setup first"), which made adding a known provider (e.g. the
6
+ * Alibaba Coding Plan) impossible outside the interactive wizard. This module
7
+ * closes that gap: for providers phi ships a config builder for, it produces a
8
+ * complete models.json entry (baseUrl + api + model list) from a single API
9
+ * key, with the provider's own key-format validation applied first.
10
+ *
11
+ * Pure (no fs, no network) so every branch is unit-tested; keys.ts is the only
12
+ * call site.
13
+ */
14
+
15
+ import { ALIBABA_PROVIDERS, buildAlibabaProviderConfig, validateAlibabaApiKey } from "./alibaba.js";
16
+ import {
17
+ buildOpenCodeGoAnthropicProviderConfig,
18
+ buildOpenCodeGoProviderConfig,
19
+ OPENCODE_GO_FALLBACK_MODELS,
20
+ type OpenCodeGoModel,
21
+ validateOpenCodeGoApiKey,
22
+ } from "./opencode-go.js";
23
+
24
+ export interface BootstrapModel {
25
+ id: string;
26
+ name: string;
27
+ reasoning: boolean;
28
+ input: readonly ("text" | "image")[];
29
+ contextWindow: number;
30
+ maxTokens: number;
31
+ compat?: Record<string, unknown>;
32
+ }
33
+
34
+ export interface BootstrapConfig {
35
+ baseUrl: string;
36
+ api: string;
37
+ apiKey: string;
38
+ models: BootstrapModel[];
39
+ }
40
+
41
+ export type BootstrapResult = { ok: true; config: BootstrapConfig; note: string } | { ok: false; error: string };
42
+
43
+ /** Provider ids `/keys set` can fully configure from just an API key. */
44
+ export const BOOTSTRAPPABLE_PROVIDERS = [
45
+ "alibaba-codingplan",
46
+ "alibaba-codingplan-anthropic",
47
+ "opencode-go",
48
+ "opencode-go-anthropic",
49
+ ] as const;
50
+
51
+ export function isBootstrappableProvider(id: string): boolean {
52
+ return (BOOTSTRAPPABLE_PROVIDERS as readonly string[]).includes(id);
53
+ }
54
+
55
+ /**
56
+ * Build a complete first-time provider config from an API key. Validates the
57
+ * key FORMAT (prefix/length) per provider; liveness is `/keys test`'s job.
58
+ * The bundled model list seeds the entry — the session-start refresh and
59
+ * `/models refresh` replace it with the live catalog afterwards.
60
+ */
61
+ export function bootstrapProviderConfig(id: string, apiKey: string): BootstrapResult {
62
+ const key = apiKey.trim();
63
+
64
+ switch (id) {
65
+ case "alibaba-codingplan":
66
+ case "alibaba-codingplan-anthropic": {
67
+ const invalid = validateAlibabaApiKey(key);
68
+ if (invalid) return { ok: false, error: invalid };
69
+ const variant = id === "alibaba-codingplan-anthropic" ? "anthropic" : "openai";
70
+ const config: BootstrapConfig = buildAlibabaProviderConfig(variant, key);
71
+ return {
72
+ ok: true,
73
+ config,
74
+ note: `${ALIBABA_PROVIDERS[variant].displayName} — ${config.models.length} bundled model(s); a live refresh follows on next session start (or run /models refresh ${id}).`,
75
+ };
76
+ }
77
+ case "opencode-go":
78
+ case "opencode-go-anthropic": {
79
+ const invalid = validateOpenCodeGoApiKey(key);
80
+ if (invalid) return { ok: false, error: invalid };
81
+ const models: OpenCodeGoModel[] = [...OPENCODE_GO_FALLBACK_MODELS];
82
+ const config: BootstrapConfig =
83
+ id === "opencode-go-anthropic"
84
+ ? buildOpenCodeGoAnthropicProviderConfig(key, models)
85
+ : buildOpenCodeGoProviderConfig(key, models);
86
+ return {
87
+ ok: true,
88
+ config,
89
+ note: `OpenCode Go — ${config.models.length} bundled model(s); a live refresh follows on next session start (or run /models refresh ${id}).`,
90
+ };
91
+ }
92
+ default:
93
+ return {
94
+ ok: false,
95
+ error: `\`${id}\` has no bootstrap builder. Run \`/setup\` to configure it interactively, or add it to ~/.phi/agent/models.json. Bootstrappable ids: ${BOOTSTRAPPABLE_PROVIDERS.join(", ")}.`,
96
+ };
97
+ }
98
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phi-code-admin/phi-code",
3
- "version": "0.90.0",
3
+ "version": "0.92.0",
4
4
  "description": "Coding agent CLI with persistent memory, sub-agents, intelligent routing, and orchestration",
5
5
  "type": "module",
6
6
  "piConfig": {