@zephyrdeng/pi-review 0.13.0 → 0.15.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 (55) hide show
  1. package/README.md +61 -4
  2. package/README.zh-CN.md +57 -2
  3. package/dist/child-isolation.test.js +25 -7
  4. package/dist/child-isolation.test.js.map +1 -1
  5. package/dist/cli.js +7 -0
  6. package/dist/cli.js.map +1 -1
  7. package/dist/extensions/review.d.ts +2 -0
  8. package/dist/extensions/review.js +22 -0
  9. package/dist/extensions/rv-prompts.d.ts +19 -0
  10. package/dist/extensions/rv-prompts.js +132 -0
  11. package/dist/index.d.ts +5 -3
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +3 -2
  14. package/dist/index.js.map +1 -1
  15. package/dist/panel-cli.test.js +35 -0
  16. package/dist/panel-cli.test.js.map +1 -1
  17. package/dist/panel-config.d.ts +7 -0
  18. package/dist/panel-config.d.ts.map +1 -1
  19. package/dist/panel-config.js +12 -0
  20. package/dist/panel-config.js.map +1 -1
  21. package/dist/panel.d.ts +7 -1
  22. package/dist/panel.d.ts.map +1 -1
  23. package/dist/panel.js +44 -1
  24. package/dist/panel.js.map +1 -1
  25. package/dist/pi-config.d.ts +54 -0
  26. package/dist/pi-config.d.ts.map +1 -0
  27. package/dist/pi-config.js +87 -0
  28. package/dist/pi-config.js.map +1 -0
  29. package/dist/pi-config.test.d.ts +2 -0
  30. package/dist/pi-config.test.d.ts.map +1 -0
  31. package/dist/pi-config.test.js +78 -0
  32. package/dist/pi-config.test.js.map +1 -0
  33. package/dist/provider-probe.test.d.ts +2 -0
  34. package/dist/provider-probe.test.d.ts.map +1 -0
  35. package/dist/provider-probe.test.js +192 -0
  36. package/dist/provider-probe.test.js.map +1 -0
  37. package/dist/review.d.ts +58 -5
  38. package/dist/review.d.ts.map +1 -1
  39. package/dist/review.js +159 -9
  40. package/dist/review.js.map +1 -1
  41. package/dist/skill-protocol.test.js +13 -0
  42. package/dist/skill-protocol.test.js.map +1 -1
  43. package/dist/types.d.ts +25 -1
  44. package/dist/types.d.ts.map +1 -1
  45. package/dist/types.js.map +1 -1
  46. package/extensions/review.test.ts +8 -0
  47. package/extensions/review.ts +11 -0
  48. package/extensions/rv-config.test.ts +90 -0
  49. package/extensions/rv-config.ts +150 -0
  50. package/extensions/rv-prompts.ts +1 -1
  51. package/package.json +1 -1
  52. package/resources/providers/orcarouter.json +28 -0
  53. package/skills/pi-review/SKILL.md +14 -1
  54. package/skills/pi-review/references/codex-tools.md +2 -0
  55. package/skills/pi-review/references/model-selection.md +1 -1
@@ -0,0 +1,78 @@
1
+ import assert from "node:assert/strict";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { test } from "vitest";
6
+ import { configFilePath, loadReviewConfigFile, parseReviewConfig, resolveChildExtensions, } from "./pi-config.js";
7
+ test("parseReviewConfig accepts an empty or full valid config", () => {
8
+ assert.deepEqual(parseReviewConfig("{}"), { config: {}, warnings: [] });
9
+ assert.deepEqual(parseReviewConfig('{"childExtensions": true}'), { config: { childExtensions: true }, warnings: [] });
10
+ assert.deepEqual(parseReviewConfig('{"childExtensions": false}'), { config: { childExtensions: false }, warnings: [] });
11
+ });
12
+ test("parseReviewConfig ignores unknown keys for forward compatibility", () => {
13
+ assert.deepEqual(parseReviewConfig('{"childExtensions": true, "futureKey": 42}'), {
14
+ config: { childExtensions: true },
15
+ warnings: [],
16
+ });
17
+ });
18
+ test("parseReviewConfig treats null as explicitly unset", () => {
19
+ assert.deepEqual(parseReviewConfig('{"childExtensions": null}'), { config: {}, warnings: [] });
20
+ });
21
+ test("parseReviewConfig leniently ignores a non-boolean value with a warning", () => {
22
+ const load = parseReviewConfig('{"childExtensions": "true"}');
23
+ assert.deepEqual(load.config, {});
24
+ assert.equal(load.warnings.length, 1);
25
+ assert.match(load.warnings[0], /childExtensions/);
26
+ assert.match(load.warnings[0], /ignoring it/);
27
+ const num = parseReviewConfig('{"childExtensions": 1}');
28
+ assert.deepEqual(num.config, {});
29
+ assert.equal(num.warnings.length, 1);
30
+ });
31
+ test("parseReviewConfig leniently handles invalid JSON and non-object roots with a warning", () => {
32
+ const badJson = parseReviewConfig("not json");
33
+ assert.deepEqual(badJson.config, {});
34
+ assert.equal(badJson.warnings.length, 1);
35
+ assert.match(badJson.warnings[0], /not valid JSON/);
36
+ const arr = parseReviewConfig("[1, 2]");
37
+ assert.deepEqual(arr.config, {});
38
+ assert.equal(arr.warnings.length, 1);
39
+ const nul = parseReviewConfig("null");
40
+ assert.deepEqual(nul.config, {});
41
+ assert.equal(nul.warnings.length, 1);
42
+ });
43
+ test("loadReviewConfigFile treats a missing file as the empty config with no warnings", () => {
44
+ const missing = path.join(os.tmpdir(), `pi-review-config-missing-${Date.now()}.json`);
45
+ assert.deepEqual(loadReviewConfigFile(missing), { config: {}, warnings: [] });
46
+ });
47
+ test("loadReviewConfigFile loads and validates an existing file", () => {
48
+ const file = path.join(os.tmpdir(), `pi-review-config-${Date.now()}.json`);
49
+ fs.writeFileSync(file, '{"childExtensions": true}');
50
+ try {
51
+ assert.deepEqual(loadReviewConfigFile(file), { config: { childExtensions: true }, warnings: [] });
52
+ }
53
+ finally {
54
+ fs.rmSync(file, { force: true });
55
+ }
56
+ });
57
+ test("configFilePath defaults under the pi data dir and honors PI_REVIEW_CONFIG", () => {
58
+ assert.equal(configFilePath({}), path.join(os.homedir(), ".pi", "pi-review", "config.json"));
59
+ assert.equal(configFilePath({ PI_REVIEW_CONFIG: "/tmp/custom.json" }), "/tmp/custom.json");
60
+ });
61
+ test("resolveChildExtensions: env truthy values override config and default", () => {
62
+ for (const value of ["1", "true", "KEEP", " 1 "]) {
63
+ assert.deepEqual(resolveChildExtensions({ PI_REVIEW_CHILD_EXTENSIONS: value }, {}), { enabled: true, source: "env" });
64
+ assert.deepEqual(resolveChildExtensions({ PI_REVIEW_CHILD_EXTENSIONS: value }, { childExtensions: false }), { enabled: true, source: "env" });
65
+ }
66
+ });
67
+ test("resolveChildExtensions: env set to a non-truthy value forces isolation", () => {
68
+ assert.deepEqual(resolveChildExtensions({ PI_REVIEW_CHILD_EXTENSIONS: "0" }, { childExtensions: true }), { enabled: false, source: "env" });
69
+ assert.deepEqual(resolveChildExtensions({ PI_REVIEW_CHILD_EXTENSIONS: "no" }, { childExtensions: true }), { enabled: false, source: "env" });
70
+ });
71
+ test("resolveChildExtensions: unset or empty env falls through to config, then default", () => {
72
+ assert.deepEqual(resolveChildExtensions({}, { childExtensions: true }), { enabled: true, source: "config" });
73
+ assert.deepEqual(resolveChildExtensions({}, { childExtensions: false }), { enabled: false, source: "config" });
74
+ assert.deepEqual(resolveChildExtensions({ PI_REVIEW_CHILD_EXTENSIONS: "" }, { childExtensions: true }), { enabled: true, source: "config" });
75
+ assert.deepEqual(resolveChildExtensions({ PI_REVIEW_CHILD_EXTENSIONS: " " }, { childExtensions: true }), { enabled: true, source: "config" });
76
+ assert.deepEqual(resolveChildExtensions({}, {}), { enabled: false, source: "default" });
77
+ });
78
+ //# sourceMappingURL=pi-config.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pi-config.test.js","sourceRoot":"","sources":["../src/pi-config.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=provider-probe.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider-probe.test.d.ts","sourceRoot":"","sources":["../src/provider-probe.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,192 @@
1
+ import assert from "node:assert/strict";
2
+ import { afterEach, beforeEach, expect, test, vi } from "vitest";
3
+ vi.mock("node:child_process", () => ({
4
+ spawnSync: vi.fn(),
5
+ }));
6
+ import { spawnSync } from "node:child_process";
7
+ import { splitModelProvider } from "./panel-config.js";
8
+ import { collectPanelExtensionHints } from "./panel.js";
9
+ import { configBlockForProvider, extensionOnlyHintText, parseModelCatalog, probeProviderAvailability, unknownProviderHintText, } from "./review.js";
10
+ const CATALOG_WITHOUT_EXT = `provider model context max-out thinking images
11
+ Ai98Pro grok-4.5 500K 128K yes yes
12
+ DeepSeek deepseek-v4-flash 1M 384K yes no
13
+ openai-codex gpt-5.6-luna 272K 128K yes yes
14
+ `;
15
+ const CATALOG_WITH_EXT = `provider model context max-out thinking images
16
+ Ai98Pro grok-4.5 500K 128K yes yes
17
+ px:anthropic claude-sonnet-5 1M 128K yes yes
18
+ px:openai gpt-5.6-luna 272K 128K yes yes
19
+ `;
20
+ function catalogResult(stdout) {
21
+ return { status: 0, stdout, stderr: "", error: undefined, pid: 0, output: [null, stdout, ""], signal: null };
22
+ }
23
+ const mockedSpawnSync = vi.mocked(spawnSync);
24
+ /** Original env value captured at module load; restored after each test. */
25
+ const ORIGINAL_CHILD_EXTENSIONS = process.env.PI_REVIEW_CHILD_EXTENSIONS;
26
+ beforeEach(() => {
27
+ mockedSpawnSync.mockReset();
28
+ mockedSpawnSync.mockImplementation((_cmd, args) => {
29
+ const isolated = Array.isArray(args) && args.includes("--no-extensions");
30
+ return catalogResult(isolated ? CATALOG_WITHOUT_EXT : CATALOG_WITH_EXT);
31
+ });
32
+ // Establish the suite's default state deterministically.
33
+ delete process.env.PI_REVIEW_CHILD_EXTENSIONS;
34
+ });
35
+ afterEach(() => {
36
+ if (ORIGINAL_CHILD_EXTENSIONS === undefined) {
37
+ delete process.env.PI_REVIEW_CHILD_EXTENSIONS;
38
+ }
39
+ else {
40
+ process.env.PI_REVIEW_CHILD_EXTENSIONS = ORIGINAL_CHILD_EXTENSIONS;
41
+ }
42
+ });
43
+ test("parseModelCatalog extracts distinct provider names via header offsets", () => {
44
+ assert.deepEqual(parseModelCatalog(CATALOG_WITHOUT_EXT), ["Ai98Pro", "DeepSeek", "openai-codex"]);
45
+ });
46
+ test("parseModelCatalog keeps provider names with symbols inside a column", () => {
47
+ const catalog = `provider model
48
+ Pika(Grok) grok-4.5
49
+ px:anthropic claude-sonnet-5
50
+ `;
51
+ assert.deepEqual(parseModelCatalog(catalog), ["Pika(Grok)", "px:anthropic"]);
52
+ });
53
+ test("parseModelCatalog returns [] for non-table output", () => {
54
+ assert.deepEqual(parseModelCatalog("pi - AI coding assistant\n\nUsage: ..."), []);
55
+ });
56
+ test("probeProviderAvailability resolves available from the isolated catalog", () => {
57
+ assert.deepEqual(probeProviderAvailability("pi-avail", "DeepSeek"), { kind: "available" });
58
+ expect(mockedSpawnSync).toHaveBeenCalledTimes(1); // only the isolated catalog is consulted
59
+ });
60
+ test("probeProviderAvailability resolves extension_only from the with-extensions catalog", () => {
61
+ assert.deepEqual(probeProviderAvailability("pi-ext", "px:anthropic"), {
62
+ kind: "extension_only",
63
+ provider: "px:anthropic",
64
+ });
65
+ expect(mockedSpawnSync).toHaveBeenCalledTimes(2);
66
+ });
67
+ test("probeProviderAvailability resolves unknown when missing everywhere", () => {
68
+ assert.deepEqual(probeProviderAvailability("pi-unknown", "nonexistent"), {
69
+ kind: "unknown",
70
+ provider: "nonexistent",
71
+ });
72
+ expect(mockedSpawnSync).toHaveBeenCalledTimes(2);
73
+ });
74
+ test("probeProviderAvailability reports indeterminate and does not cache failed catalog queries", () => {
75
+ mockedSpawnSync.mockImplementation(() => ({
76
+ status: 1,
77
+ stdout: "",
78
+ stderr: "boom",
79
+ error: undefined,
80
+ pid: 0,
81
+ output: [null, "", "boom"],
82
+ signal: null,
83
+ }));
84
+ const first = probeProviderAvailability("pi-broken", "px:anthropic");
85
+ assert.deepEqual(first, { kind: "indeterminate", provider: "px:anthropic" });
86
+ // Failure is not cached: a later successful query re-runs the probe.
87
+ mockedSpawnSync.mockImplementation((_cmd, args) => {
88
+ const isolated = Array.isArray(args) && args.includes("--no-extensions");
89
+ return catalogResult(isolated ? CATALOG_WITHOUT_EXT : CATALOG_WITH_EXT);
90
+ });
91
+ const second = probeProviderAvailability("pi-broken", "px:anthropic");
92
+ assert.deepEqual(second, { kind: "extension_only", provider: "px:anthropic" });
93
+ });
94
+ test("splitModelProvider extracts the provider prefix from provider/model tokens", () => {
95
+ assert.equal(splitModelProvider("px:openai/agnes-2.0-flash"), "px:openai");
96
+ assert.equal(splitModelProvider("openai-codex/gpt-5.6-sol"), "openai-codex");
97
+ assert.equal(splitModelProvider("anthropic/*"), "anthropic");
98
+ assert.equal(splitModelProvider("gpt-5.6-sol"), undefined);
99
+ assert.equal(splitModelProvider("/leading-slash"), undefined);
100
+ assert.equal(splitModelProvider("trailing/"), undefined);
101
+ });
102
+ test("configBlockForProvider skips probing without an explicit provider", () => {
103
+ assert.equal(configBlockForProvider("pi-skip", undefined), undefined);
104
+ expect(mockedSpawnSync).not.toHaveBeenCalled();
105
+ });
106
+ test("configBlockForProvider skips probing when extensions are enabled (PI_REVIEW_CHILD_EXTENSIONS=1)", () => {
107
+ process.env.PI_REVIEW_CHILD_EXTENSIONS = "1";
108
+ assert.equal(configBlockForProvider("pi-skip", "px:anthropic"), undefined);
109
+ expect(mockedSpawnSync).not.toHaveBeenCalled();
110
+ });
111
+ test("configBlockForProvider returns undefined when the provider is isolated-available", () => {
112
+ assert.equal(configBlockForProvider("pi-avail2", "DeepSeek"), undefined);
113
+ });
114
+ test("configBlockForProvider resolves the provider from a provider/model token (F1 regression)", () => {
115
+ // No --provider; the provider must come from the model string.
116
+ const block = configBlockForProvider("pi-model-ext", undefined, "px:anthropic/claude-sonnet-5");
117
+ assert.ok(block, "expected a config block");
118
+ assert.equal(block.extensionOnly, true);
119
+ assert.match(block.structured.parseError ?? "", /PI_REVIEW_CHILD_EXTENSIONS=1/);
120
+ });
121
+ test("configBlockForProvider does not block on a failed catalog probe (F2 regression)", () => {
122
+ mockedSpawnSync.mockImplementation(() => ({
123
+ status: 1,
124
+ stdout: "",
125
+ stderr: "boom",
126
+ error: undefined,
127
+ pid: 0,
128
+ output: [null, "", "boom"],
129
+ signal: null,
130
+ }));
131
+ assert.equal(configBlockForProvider("pi-broken-f2", "px:anthropic"), undefined);
132
+ });
133
+ test("configBlockForProvider blocks with the extension hint for extension-only providers", () => {
134
+ const block = configBlockForProvider("pi-block-ext", "px:anthropic");
135
+ assert.ok(block, "expected a config block");
136
+ assert.equal(block.extensionOnly, true);
137
+ assert.equal(block.structured.status, "blocked");
138
+ assert.equal(block.structured.verdict, "blocked");
139
+ assert.equal(block.structured.verdictSource, "config_error");
140
+ assert.equal(block.structured.findings.length, 0);
141
+ assert.equal(block.structured.actionableCount, 0);
142
+ assert.match(block.structured.parseError ?? "", /PI_REVIEW_CHILD_EXTENSIONS=1/);
143
+ });
144
+ test("configBlockForProvider blocks with the unknown-provider message", () => {
145
+ const block = configBlockForProvider("pi-block-unk", "nonexistent");
146
+ assert.ok(block, "expected a config block");
147
+ assert.equal(block.extensionOnly, false);
148
+ assert.match(block.structured.parseError ?? "", /was not found/);
149
+ });
150
+ test("collectPanelExtensionHints returns undefined when no reviewer was blocked", () => {
151
+ const submissions = [
152
+ { reviewerId: "r1", model: "gpt-5.6-sol", durationMs: 1, result: { status: "clean", verdict: "approve", verdictSource: "parsed", findings: [], actionableCount: 0 } },
153
+ ];
154
+ assert.equal(collectPanelExtensionHints(submissions), undefined);
155
+ });
156
+ test("collectPanelExtensionHints keeps every provider, deduplicated (F3 regression)", () => {
157
+ const blocked = (id, provider) => ({
158
+ reviewerId: id,
159
+ model: `${provider}/m`,
160
+ durationMs: 0,
161
+ result: {
162
+ status: "blocked",
163
+ verdict: "blocked",
164
+ verdictSource: "config_error",
165
+ parseError: extensionOnlyHintText(provider),
166
+ findings: [],
167
+ actionableCount: 0,
168
+ },
169
+ extensionHint: { provider, availableViaExtension: true },
170
+ });
171
+ const hints = collectPanelExtensionHints([
172
+ blocked("r1", "px:anthropic"),
173
+ blocked("r2", "px:openai"),
174
+ blocked("r3", "px:anthropic"), // duplicate provider: collapsed
175
+ ]);
176
+ assert.deepEqual(hints, [
177
+ { provider: "px:anthropic", availableViaExtension: true },
178
+ { provider: "px:openai", availableViaExtension: true },
179
+ ]);
180
+ });
181
+ test("extensionOnlyHintText mentions the provider and the env var", () => {
182
+ const text = extensionOnlyHintText("px:anthropic");
183
+ assert.match(text, /px:anthropic/);
184
+ assert.match(text, /PI_REVIEW_CHILD_EXTENSIONS=1/);
185
+ assert.match(text, /no-extensions/);
186
+ });
187
+ test("unknownProviderHintText mentions the provider and check guidance", () => {
188
+ const text = unknownProviderHintText("foo");
189
+ assert.match(text, /foo/);
190
+ assert.match(text, /spelling/);
191
+ });
192
+ //# sourceMappingURL=provider-probe.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider-probe.test.js","sourceRoot":"","sources":["../src/provider-probe.test.ts"],"names":[],"mappings":"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"}
package/dist/review.d.ts CHANGED
@@ -1,18 +1,71 @@
1
- import type { ParsedArgs, ReviewRunResult } from "./types.js";
1
+ import type { ParsedArgs, ReviewRunResult, StructuredReviewResult } from "./types.js";
2
2
  import { type ChildRunResult } from "./child-process.js";
3
+ import type { PiReviewConfig } from "./pi-config.js";
3
4
  export declare function readReviewStdin(): string;
4
5
  export declare function childEnv(piBin: string): NodeJS.ProcessEnv;
5
6
  /**
6
- * Isolate review children from host Pi extension side effects.
7
+ * Parse `pi --list-models` table output into distinct provider names.
8
+ * Columns are space-padded; offsets are derived from the header row so
9
+ * provider names are stable even if they contain spaces.
10
+ */
11
+ export declare function parseModelCatalog(stdout: string): string[];
12
+ export type ProviderProbe = {
13
+ kind: "available";
14
+ } | {
15
+ kind: "extension_only";
16
+ provider: string;
17
+ } | {
18
+ kind: "unknown";
19
+ provider: string;
20
+ } | {
21
+ kind: "indeterminate";
22
+ provider: string;
23
+ };
24
+ /**
25
+ * Decide whether `provider` is usable by an isolated review child
26
+ * (--no-extensions, issue #8). Missing from the isolated catalog but present
27
+ * with host extensions loaded means the provider is registered by an
28
+ * extension; the caller should surface a fix hint instead of a confusing
29
+ * unknown-provider failure. A failed catalog query is `indeterminate`: the
30
+ * caller must not block a possibly-valid provider on probe trouble.
31
+ */
32
+ export declare function probeProviderAvailability(piBin: string, provider: string): ProviderProbe;
33
+ /** Human-readable fix hint for a provider that only exists via host extensions. */
34
+ export declare function extensionOnlyHintText(provider: string): string;
35
+ /** Human-readable message for a provider missing from the catalog entirely. */
36
+ export declare function unknownProviderHintText(provider: string): string;
37
+ /**
38
+ * When isolation is active and a provider is requested, block before spawning
39
+ * the child if that provider only exists via host extensions (or is unknown),
40
+ * and return a blocked review with an actionable hint. Returns undefined when
41
+ * the review should proceed.
42
+ */
43
+ export interface ProviderConfigBlock {
44
+ structured: StructuredReviewResult;
45
+ provider: string;
46
+ extensionOnly: boolean;
47
+ }
48
+ export declare function configBlockForProvider(piBin: string, provider: string | undefined, model?: string): ProviderConfigBlock | undefined;
49
+ /** Isolate review children from host Pi extension side effects.
7
50
  *
8
51
  * Host extensions (usage HUDs, auto-reload bridges, etc.) may capture extension
9
52
  * ctx and touch it after session dispose/reload. That throws Pi's stale-context
10
53
  * error and can crash an otherwise successful reviewer child (issue #8).
11
54
  *
12
- * Opt out with PI_REVIEW_CHILD_EXTENSIONS=1 when a custom provider truly requires
13
- * host extension registration inside the child.
55
+ * The effective value comes from the persistent review config
56
+ * (childExtensions, ~/.pi/pi-review/config.json), overridden per process by
57
+ * PI_REVIEW_CHILD_EXTENSIONS (1/true/keep enable, any other set value
58
+ * disables). Bare CLI stays isolated (--no-extensions) until the config says
59
+ * otherwise.
60
+ */
61
+ export declare function childIsolationArgs(env?: NodeJS.ProcessEnv, cfg?: PiReviewConfig): string[];
62
+ /**
63
+ * Build the catalog command args so the models command shows the same catalog
64
+ * an actual review child would see: the effective isolation config applies
65
+ * here too (F2: catalog and review must agree on extension-registered
66
+ * providers).
14
67
  */
15
- export declare function childIsolationArgs(env?: NodeJS.ProcessEnv): string[];
68
+ export declare function modelsArgs(args: string[], env?: NodeJS.ProcessEnv, cfg?: PiReviewConfig): string[];
16
69
  /** Prefer the larger of two captured stderr buffers; keep a readable stack tail. */
17
70
  export declare const CHILD_STDERR_DIAGNOSTIC_LIMIT = 12000;
18
71
  export declare function formatChildRuntimeDetail(runtimeError: string, stderr: string, limit?: number): string;
@@ -1 +1 @@
1
- {"version":3,"file":"review.d.ts","sourceRoot":"","sources":["../src/review.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,UAAU,EAAc,eAAe,EAAE,MAAM,YAAY,CAAC;AAC1E,OAAO,EAA2C,KAAK,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAalG,wBAAgB,eAAe,IAAI,MAAM,CAOxC;AAED,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC,UAAU,CAczD;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,EAAE,CAIjF;AAED,oFAAoF;AACpF,eAAO,MAAM,6BAA6B,QAAS,CAAC;AAEpD,wBAAgB,wBAAwB,CAAC,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,SAAgC,GAAG,MAAM,CAK5H;AAED,wFAAwF;AACxF,wBAAgB,cAAc,CAAC,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,GAAG,MAAM,CAG/E;AAED;;gFAEgF;AAChF,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,OAAO,EAAE,eAAe,EAAE,OAAO,GAAG,OAAO,CAE/F;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,IAAI,CAAC,cAAc,EAAE,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAC,GAAG,MAAM,GAAG,SAAS,CAKhH;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,KAAK,CAM9D;AAED,wBAAsB,aAAa,CAAC,MAAM,EAAE,UAAU,EAAE,SAAS,SAAoB,GAAG,OAAO,CAAC,eAAe,CAAC,CAkM/G;AAED,+CAA+C;AAC/C,wBAAsB,SAAS,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,KAAK,CAAC,CAGlE"}
1
+ {"version":3,"file":"review.d.ts","sourceRoot":"","sources":["../src/review.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,UAAU,EAAc,eAAe,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAClG,OAAO,EAA2C,KAAK,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAclG,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAErD,wBAAgB,eAAe,IAAI,MAAM,CAOxC;AAED,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC,UAAU,CAczD;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAc1D;AAkCD,MAAM,MAAM,aAAa,GACrB;IAAE,IAAI,EAAE,WAAW,CAAA;CAAE,GACrB;IAAE,IAAI,EAAE,gBAAgB,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAC5C;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GACrC;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC;AAEhD;;;;;;;GAOG;AACH,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,aAAa,CAQxF;AAED,mFAAmF;AACnF,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAS9D;AAED,+EAA+E;AAC/E,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAKhE;AAED;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,sBAAsB,CAAC;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,OAAO,CAAC;CACxB;AAED,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,KAAK,CAAC,EAAE,MAAM,GACb,mBAAmB,GAAG,SAAS,CAoBjC;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,EAAE,GAAG,CAAC,EAAE,cAAc,GAAG,MAAM,EAAE,CAGvG;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,GAAG,GAAE,MAAM,CAAC,UAAwB,EAAE,GAAG,CAAC,EAAE,cAAc,GAAG,MAAM,EAAE,CAE/G;AAED,oFAAoF;AACpF,eAAO,MAAM,6BAA6B,QAAS,CAAC;AAEpD,wBAAgB,wBAAwB,CAAC,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,SAAgC,GAAG,MAAM,CAK5H;AAED,wFAAwF;AACxF,wBAAgB,cAAc,CAAC,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,GAAG,MAAM,CAG/E;AAED;;gFAEgF;AAChF,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,OAAO,EAAE,eAAe,EAAE,OAAO,GAAG,OAAO,CAE/F;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,IAAI,CAAC,cAAc,EAAE,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAC,GAAG,MAAM,GAAG,SAAS,CAKhH;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,KAAK,CAM9D;AAED,wBAAsB,aAAa,CAAC,MAAM,EAAE,UAAU,EAAE,SAAS,SAAoB,GAAG,OAAO,CAAC,eAAe,CAAC,CAiO/G;AAED,+CAA+C;AAC/C,wBAAsB,SAAS,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,KAAK,CAAC,CAGlE"}
package/dist/review.js CHANGED
@@ -14,7 +14,9 @@ import { ProgressLogWriter } from "./progress-log.js";
14
14
  import { makeRunSessionDir, newestJsonl } from "./session.js";
15
15
  import { fail, hasPathSeparator, expandMaybeHome, normalizeTools } from "./utils.js";
16
16
  import { resolveConfig } from "./config.js";
17
+ import { splitModelProvider } from "./panel-config.js";
17
18
  import { formatReviewMetaAscii, formatReviewMetaJsonLine } from "./meta-footer.js";
19
+ import { currentConfig, resolveChildExtensions } from "./pi-config.js";
18
20
  export function readReviewStdin() {
19
21
  if (process.stdin.isTTY)
20
22
  return "";
@@ -40,20 +42,138 @@ export function childEnv(piBin) {
40
42
  };
41
43
  }
42
44
  /**
43
- * Isolate review children from host Pi extension side effects.
45
+ * Parse `pi --list-models` table output into distinct provider names.
46
+ * Columns are space-padded; offsets are derived from the header row so
47
+ * provider names are stable even if they contain spaces.
48
+ */
49
+ export function parseModelCatalog(stdout) {
50
+ const lines = stdout.split("\n");
51
+ const headerIdx = lines.findIndex((l) => l.includes("provider") && l.includes("model"));
52
+ if (headerIdx === -1)
53
+ return [];
54
+ const header = lines[headerIdx];
55
+ const providerStart = header.indexOf("provider");
56
+ const modelStart = header.indexOf("model");
57
+ const providers = new Set();
58
+ for (const line of lines.slice(headerIdx + 1)) {
59
+ if (!line.trim())
60
+ continue;
61
+ const provider = line.slice(providerStart, modelStart).trim();
62
+ if (provider)
63
+ providers.add(provider);
64
+ }
65
+ return [...providers];
66
+ }
67
+ const MODEL_CATALOG_CACHE = new Map();
68
+ function catalogCacheKey(piBin, isolated) {
69
+ return `${piBin}|${isolated ? "no-ext" : "with-ext"}`;
70
+ }
71
+ function listProviders(piBin, isolated) {
72
+ const key = catalogCacheKey(piBin, isolated);
73
+ const cached = MODEL_CATALOG_CACHE.get(key);
74
+ if (cached)
75
+ return cached;
76
+ const args = ["--list-models", ...(isolated ? ["--no-extensions"] : [])];
77
+ const result = spawnSync(piBin, args, {
78
+ env: childEnv(piBin),
79
+ encoding: "utf8",
80
+ maxBuffer: 16 * 1024 * 1024,
81
+ });
82
+ const query = result.status === 0 && result.stdout
83
+ ? { providers: new Set(parseModelCatalog(result.stdout)), ok: true }
84
+ : { providers: new Set(), ok: false };
85
+ // Only cache successful queries: a transient spawn failure (slow startup,
86
+ // config read hiccup) must not poison the probe for the rest of the run.
87
+ if (query.ok)
88
+ MODEL_CATALOG_CACHE.set(key, query);
89
+ return query;
90
+ }
91
+ /**
92
+ * Decide whether `provider` is usable by an isolated review child
93
+ * (--no-extensions, issue #8). Missing from the isolated catalog but present
94
+ * with host extensions loaded means the provider is registered by an
95
+ * extension; the caller should surface a fix hint instead of a confusing
96
+ * unknown-provider failure. A failed catalog query is `indeterminate`: the
97
+ * caller must not block a possibly-valid provider on probe trouble.
98
+ */
99
+ export function probeProviderAvailability(piBin, provider) {
100
+ const isolated = listProviders(piBin, true);
101
+ if (!isolated.ok)
102
+ return { kind: "indeterminate", provider };
103
+ if (isolated.providers.has(provider))
104
+ return { kind: "available" };
105
+ const withExtensions = listProviders(piBin, false);
106
+ if (!withExtensions.ok)
107
+ return { kind: "indeterminate", provider };
108
+ if (withExtensions.providers.has(provider))
109
+ return { kind: "extension_only", provider };
110
+ return { kind: "unknown", provider };
111
+ }
112
+ /** Human-readable fix hint for a provider that only exists via host extensions. */
113
+ export function extensionOnlyHintText(provider) {
114
+ return [
115
+ `provider "${provider}" is registered by a Pi extension, but review children`,
116
+ `run isolated with --no-extensions (issue #8).`,
117
+ "",
118
+ "Enable extension-registered providers persistently in the review config:",
119
+ ` ~/.pi/pi-review/config.json → { "childExtensions": true }`,
120
+ "(or override one run: PI_REVIEW_CHILD_EXTENSIONS=1 pi-review ...)",
121
+ ].join("\n");
122
+ }
123
+ /** Human-readable message for a provider missing from the catalog entirely. */
124
+ export function unknownProviderHintText(provider) {
125
+ return [
126
+ `provider "${provider}" was not found in the Pi model catalog (with or`,
127
+ `without host extensions loaded). Check --provider / preset provider spelling.`,
128
+ ].join("\n");
129
+ }
130
+ export function configBlockForProvider(piBin, provider, model) {
131
+ const probeProvider = provider ?? (model ? splitModelProvider(model) : undefined);
132
+ if (!probeProvider || childIsolationArgs().length === 0)
133
+ return undefined;
134
+ const probe = probeProviderAvailability(piBin, probeProvider);
135
+ // available → proceed; indeterminate (catalog query failed) → proceed and
136
+ // let the child surface its own accurate error rather than block on probe trouble.
137
+ if (probe.kind === "available" || probe.kind === "indeterminate")
138
+ return undefined;
139
+ const extensionOnly = probe.kind === "extension_only";
140
+ return {
141
+ provider: probeProvider,
142
+ extensionOnly,
143
+ structured: {
144
+ status: "blocked",
145
+ verdict: "blocked",
146
+ verdictSource: "config_error",
147
+ parseError: extensionOnly ? extensionOnlyHintText(probeProvider) : unknownProviderHintText(probeProvider),
148
+ findings: [],
149
+ actionableCount: 0,
150
+ },
151
+ };
152
+ }
153
+ /** Isolate review children from host Pi extension side effects.
44
154
  *
45
155
  * Host extensions (usage HUDs, auto-reload bridges, etc.) may capture extension
46
156
  * ctx and touch it after session dispose/reload. That throws Pi's stale-context
47
157
  * error and can crash an otherwise successful reviewer child (issue #8).
48
158
  *
49
- * Opt out with PI_REVIEW_CHILD_EXTENSIONS=1 when a custom provider truly requires
50
- * host extension registration inside the child.
159
+ * The effective value comes from the persistent review config
160
+ * (childExtensions, ~/.pi/pi-review/config.json), overridden per process by
161
+ * PI_REVIEW_CHILD_EXTENSIONS (1/true/keep enable, any other set value
162
+ * disables). Bare CLI stays isolated (--no-extensions) until the config says
163
+ * otherwise.
51
164
  */
52
- export function childIsolationArgs(env = process.env) {
53
- const raw = env.PI_REVIEW_CHILD_EXTENSIONS?.trim().toLowerCase();
54
- if (raw === "1" || raw === "true" || raw === "keep")
55
- return [];
56
- return ["--no-extensions"];
165
+ export function childIsolationArgs(env = process.env, cfg) {
166
+ const decision = resolveChildExtensions(env, cfg ?? currentConfig(env).config);
167
+ return decision.enabled ? [] : ["--no-extensions"];
168
+ }
169
+ /**
170
+ * Build the catalog command args so the models command shows the same catalog
171
+ * an actual review child would see: the effective isolation config applies
172
+ * here too (F2: catalog and review must agree on extension-registered
173
+ * providers).
174
+ */
175
+ export function modelsArgs(args, env = process.env, cfg) {
176
+ return ["--list-models", ...childIsolationArgs(env, cfg), ...args];
57
177
  }
58
178
  /** Prefer the larger of two captured stderr buffers; keep a readable stack tail. */
59
179
  export const CHILD_STDERR_DIAGNOSTIC_LIMIT = 12_000;
@@ -86,7 +206,7 @@ export function childRuntimeError(child) {
86
206
  return undefined;
87
207
  }
88
208
  export function runModels(piBin, args) {
89
- const result = spawnSync(piBin, ["--list-models", ...args], {
209
+ const result = spawnSync(piBin, modelsArgs(args), {
90
210
  stdio: "inherit",
91
211
  env: childEnv(piBin),
92
212
  });
@@ -138,6 +258,36 @@ export async function runReviewOnce(parsed, stdinText = readReviewStdin()) {
138
258
  }
139
259
  // Isolate single-review children from host extension side effects (issue #8).
140
260
  args.push(...childIsolationArgs());
261
+ // Block before spawning when the requested provider only exists via host
262
+ // extensions (issue #8): the isolated child would otherwise die with a
263
+ // confusing unknown-provider error. Surface an actionable hint so the host
264
+ // agent knows to re-run with PI_REVIEW_CHILD_EXTENSIONS=1.
265
+ const configBlock = configBlockForProvider(config.piBin, parsed.provider || preset.provider, parsed.model || preset.model);
266
+ if (configBlock) {
267
+ const hint = configBlock.structured.parseError ?? "";
268
+ const meta = {
269
+ metaVersion: REVIEW_META_VERSION,
270
+ reviewMode: parsed.mode,
271
+ ...configBlock.structured,
272
+ durationMs: 0,
273
+ model: parsed.model || preset.model || null,
274
+ ...(parsed.thinking || preset.thinking ? { thinking: parsed.thinking || preset.thinking } : {}),
275
+ ...(configBlock.extensionOnly
276
+ ? { extensionHint: { provider: configBlock.provider, availableViaExtension: true } }
277
+ : {}),
278
+ };
279
+ process.stdout.write(`${hint}\n\n`);
280
+ process.stdout.write(`${formatReviewMetaAscii(meta)}\n`);
281
+ const metaJsonDest = process.env.PI_REVIEW_META_STDOUT?.toLowerCase();
282
+ const jsonLine = formatReviewMetaJsonLine(meta);
283
+ if (metaJsonDest === "1" || metaJsonDest === "true" || metaJsonDest === "stdout") {
284
+ process.stdout.write(jsonLine);
285
+ }
286
+ else {
287
+ process.stderr.write(jsonLine);
288
+ }
289
+ return { meta, exitCode: reviewExitCode(meta.status) };
290
+ }
141
291
  args.push(...(payload.attachableFileRefs ?? payload.fileRefs), prompt);
142
292
  let progressWriter;
143
293
  if (parsed.progressLog) {