@aefree/pi-plastic 0.4.0 → 0.5.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 (54) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +8 -7
  3. package/extensions/bash-cm-diff-guard.ts +2 -1
  4. package/extensions/file-discovery-filter.ts +67 -0
  5. package/index.ts +12 -0
  6. package/package.json +7 -8
  7. package/skills/_shared/references/conventional-commits.md +198 -198
  8. package/skills/using-plastic/SKILL.md +2 -2
  9. package/skills/using-plastic/references/changeset-operations.md +2 -1
  10. package/skills/using-plastic/references/code-review-creation.md +30 -30
  11. package/skills/using-plastic/references/integration.md +2 -1
  12. package/skills/using-plastic/references/quick-reference.md +2 -1
  13. package/skills/using-plastic/references/resources.md +7 -7
  14. package/skills/using-plastic/references/reviewing-changes.md +4 -3
  15. package/skills/using-plastic/references/shelving.md +25 -25
  16. package/skills/using-plastic/references/troubleshooting.md +2 -1
  17. package/skills/using-plastic/references/workspaces.md +33 -33
  18. package/src/file-discovery-filter-rendezvous.ts +104 -0
  19. package/src/{repository-search-provider.ts → file-discovery-filter.ts} +21 -17
  20. package/src/plastic-core.ts +438 -152
  21. package/src/plastic-tool-loading.ts +2 -1
  22. package/evals/tool-loading/README.md +0 -38
  23. package/evals/tool-loading/RESULTS.md +0 -57
  24. package/evals/tool-loading/baseline.json +0 -34
  25. package/evals/tool-loading/cases.json +0 -70
  26. package/evals/tool-loading/config.json +0 -19
  27. package/evals/tool-loading/mutation-guard.ts +0 -30
  28. package/evals/tool-loading/provider-capture.ts +0 -22
  29. package/evals/tool-loading/results/.gitignore +0 -3
  30. package/evals/tool-loading/run-eval.ts +0 -372
  31. package/extensions/repository-search-provider.ts +0 -66
  32. package/tests/bash-cm-diff-guard-validation.ts +0 -55
  33. package/tests/fixtures/plastic.selector +0 -3
  34. package/tests/fixtures/status-compact.txt +0 -1
  35. package/tests/fixtures/status-normal.txt +0 -1
  36. package/tests/packed-copy-owner-conflict.test.ts +0 -28
  37. package/tests/pi-tool-harness.ts +0 -116
  38. package/tests/plastic-branch-create-live.test.ts +0 -78
  39. package/tests/plastic-branch-query.test.ts +0 -26
  40. package/tests/plastic-branch-status.test.ts +0 -241
  41. package/tests/plastic-checkin-path-resolution.ts +0 -219
  42. package/tests/plastic-constrained-sampling.test.ts +0 -77
  43. package/tests/plastic-diff-workflow.test.ts +0 -86
  44. package/tests/plastic-dynamic-tool-loading.test.ts +0 -224
  45. package/tests/plastic-extension-registration.test.ts +0 -34
  46. package/tests/plastic-live-smoke.test.ts +0 -37
  47. package/tests/plastic-mutation-execution.test.ts +0 -61
  48. package/tests/plastic-patch-command.test.ts +0 -118
  49. package/tests/plastic-process-lifecycle.test.ts +0 -148
  50. package/tests/plastic-tool-rendering.test.ts +0 -16
  51. package/tests/plastic-tool-validation.ts +0 -153
  52. package/tests/plastic-workspace.test.ts +0 -62
  53. package/tests/repository-search-provider-registration.test.ts +0 -37
  54. package/tests/repository-search-provider.test.ts +0 -27
@@ -1,372 +0,0 @@
1
- import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
- import { tmpdir } from "node:os";
3
- import { dirname, join, resolve } from "node:path";
4
- import { spawn } from "node:child_process";
5
- import { fileURLToPath } from "node:url";
6
-
7
- type Condition = "all-active" | "balanced" | "loader-only";
8
- type JsonObject = Record<string, unknown>;
9
- type EvalCase = { id: string; prompt: string; requiredTools: string[]; minimalActivatedTools: string[]; forbiddenTools: string[]; requiredPreflightTools?: string[] };
10
- type EvalConfig = {
11
- piVersionPrefix: string;
12
- sandboxCwdEnv: string;
13
- sandboxMarkerFile: string;
14
- sandboxMarkerContent: string;
15
- sandboxAuthorizationEnv: string;
16
- sandboxAuthorizationValue: string;
17
- timeoutMs: number;
18
- maxToolCalls: number;
19
- maxOutputChars: number;
20
- conditions: Condition[];
21
- destructiveTools: string[];
22
- };
23
-
24
- const HERE = dirname(fileURLToPath(import.meta.url));
25
- const PACKAGE_ROOT = resolve(HERE, "../..");
26
- const CONFIG_PATH = join(HERE, "config.json");
27
- const CASES_PATH = join(HERE, "cases.json");
28
- const BASELINE_PATH = join(HERE, "baseline.json");
29
- const RESULTS_DIR = join(HERE, "results");
30
- const MODEL_PATTERN = /^openai-codex\/gpt-5\.6-(luna|terra|sol)(:(off|minimal|low|medium|high|xhigh|max))?$/;
31
- const LOADER = "plastic_tool_search";
32
- const ACTIVE_BY_CONDITION: Record<Condition, readonly string[]> = {
33
- "all-active": ["*"],
34
- balanced: [LOADER, "plastic_status", "plastic_currentBranch"],
35
- "loader-only": [LOADER],
36
- };
37
-
38
- type Options = { trials: number; conditions: Condition[]; caseIds: string[]; model?: string; keep: boolean; includeEvents: boolean; dryRun: boolean };
39
-
40
- function readJson<T>(path: string): T {
41
- return JSON.parse(readFileSync(path, "utf8")) as T;
42
- }
43
-
44
- function fail(message: string): never {
45
- throw new Error(message);
46
- }
47
-
48
- function usage(): string {
49
- return `Usage: npx tsx evals/tool-loading/run-eval.ts --model openai-codex/gpt-5.6-luna[:thinking] [options]
50
-
51
- Options:
52
- --trials <1..5> Sequential fresh subprocess trials (default: 1)
53
- --condition <name[,name]> all-active, balanced, loader-only (default: all)
54
- --cases <id[,id]> Case IDs from cases.json (default: all)
55
- --model <provider/model> Required; approved GPT-5.6 Codex model, optionally :<thinking-level>
56
- --keep Keep raw provider capture files in the system temp directory
57
- --include-events Include sanitized event crumbs in the JSON summary
58
- --dry-run Validate config and print planned subprocesses without model calls
59
- --help Show this help`;
60
- }
61
-
62
- function parseCsv(value: string): string[] {
63
- return value.split(",").map((item) => item.trim()).filter(Boolean);
64
- }
65
-
66
- function parseOptions(argv: string[]): Options {
67
- const options: Options = { trials: 1, conditions: ["all-active", "balanced", "loader-only"], caseIds: [], keep: false, includeEvents: false, dryRun: false };
68
- for (let i = 0; i < argv.length; i += 1) {
69
- const arg = argv[i];
70
- const value = () => argv[++i] ?? fail(`Missing value for ${arg}`);
71
- if (arg === "--help" || arg === "-h") { console.log(usage()); process.exit(0); }
72
- if (arg === "--trials") options.trials = Number(value());
73
- else if (arg === "--condition") options.conditions = parseCsv(value()) as Condition[];
74
- else if (arg === "--cases") options.caseIds = parseCsv(value());
75
- else if (arg === "--model") options.model = value();
76
- else if (arg === "--keep") options.keep = true;
77
- else if (arg === "--include-events") options.includeEvents = true;
78
- else if (arg === "--dry-run") options.dryRun = true;
79
- else fail(`Unknown option: ${arg}`);
80
- }
81
- if (!Number.isInteger(options.trials) || options.trials < 1 || options.trials > 5) fail("--trials must be an integer from 1 through 5");
82
- return options;
83
- }
84
-
85
- function validate(config: EvalConfig, cases: EvalCase[], baseline: JsonObject, options: Options): void {
86
- if (config.piVersionPrefix !== "0.83.") fail("config piVersionPrefix must select the validated Pi 0.83 patch line");
87
- if (config.sandboxCwdEnv !== "PI_PLASTIC_EVAL_SANDBOX") fail("config sandboxCwdEnv must use the dedicated eval environment variable");
88
- if (config.sandboxMarkerFile !== ".pi-plastic-eval-sandbox" || config.sandboxMarkerContent !== "pi-plastic-eval-sandbox\n") fail("config sandbox marker attestation is invalid");
89
- if (config.sandboxAuthorizationEnv !== "PI_PLASTIC_EVAL_ALLOW" || config.sandboxAuthorizationValue !== "dedicated-sandbox") fail("config sandbox authorization attestation is invalid");
90
- if (!Array.isArray(config.conditions) || !config.conditions.every((value) => ["all-active", "balanced", "loader-only"].includes(value))) fail("Invalid conditions in config.json");
91
- if (!Number.isInteger(config.timeoutMs) || config.timeoutMs <= 0 || !Number.isInteger(config.maxToolCalls) || config.maxToolCalls <= 0 || !Number.isInteger(config.maxOutputChars) || config.maxOutputChars <= 0) fail("Invalid numeric bounds in config.json");
92
- const ids = new Set<string>();
93
- for (const testCase of cases) {
94
- if (!testCase.id || ids.has(testCase.id) || !testCase.prompt || !Array.isArray(testCase.requiredTools) || !Array.isArray(testCase.minimalActivatedTools) || !Array.isArray(testCase.forbiddenTools) || (testCase.requiredPreflightTools !== undefined && !Array.isArray(testCase.requiredPreflightTools))) fail("Invalid or duplicate case in cases.json");
95
- if ((testCase.requiredPreflightTools ?? []).some((tool) => !testCase.requiredTools.includes(tool))) fail(`Preflight tool must also be required in case ${testCase.id}`);
96
- if (new Set(testCase.minimalActivatedTools).size !== testCase.minimalActivatedTools.length || testCase.minimalActivatedTools.some((tool) => !testCase.requiredTools.includes(tool)) || testCase.requiredTools.some((tool) => !testCase.minimalActivatedTools.includes(tool))) fail(`minimalActivatedTools must be the exact smallest sufficient required-tool set in case ${testCase.id}`);
97
- ids.add(testCase.id);
98
- }
99
- for (const condition of options.conditions) if (!config.conditions.includes(condition)) fail(`Unknown condition: ${condition}`);
100
- for (const id of options.caseIds) if (!ids.has(id)) fail(`Unknown case: ${id}`);
101
- const legacy = baseline.legacyBaseline as JsonObject | undefined;
102
- const conditions = baseline.implementedConditions as JsonObject | undefined;
103
- if (legacy?.toolCount !== 29 || legacy.serializedSchemaChars !== 21192 || legacy.estimatedTokens !== 5298 || legacy.promptMetadataChars !== 0) fail("baseline.json legacy measurement does not match the untouched baseline");
104
- if (!conditions || (conditions.balanced as JsonObject)?.toolCount !== 3 || (conditions["loader-only"] as JsonObject)?.toolCount !== 1 || (conditions["all-active"] as JsonObject)?.toolCount !== 28) fail("baseline.json implemented condition metadata is incomplete");
105
- }
106
-
107
- function isInitiallyActive(condition: Condition, tool: string): boolean {
108
- return ACTIVE_BY_CONDITION[condition].includes("*") || ACTIVE_BY_CONDITION[condition].includes(tool);
109
- }
110
-
111
- function expectedLoaderCalls(condition: Condition, testCase: EvalCase): number {
112
- if (testCase.id === "negative-no-operation") return 0;
113
- return testCase.minimalActivatedTools.some((tool) => !isInitiallyActive(condition, tool)) ? 1 : 0;
114
- }
115
-
116
- function sameToolSet(left: readonly string[], right: readonly string[]): boolean {
117
- return left.length === right.length && left.every((tool) => right.includes(tool));
118
- }
119
-
120
- function numeric(value: unknown): number {
121
- return typeof value === "number" && Number.isFinite(value) ? value : 0;
122
- }
123
-
124
- function readUsage(message: JsonObject): Record<string, number> {
125
- const usage = (message.usage ?? {}) as JsonObject;
126
- const input = numeric(usage.input ?? usage.inputTokens);
127
- const output = numeric(usage.output ?? usage.outputTokens);
128
- const cacheRead = numeric(usage.cacheRead ?? usage.cache_read_tokens);
129
- const cacheWrite = numeric(usage.cacheWrite ?? usage.cache_write_tokens);
130
- return { input, output, cacheRead, cacheWrite, total: numeric(usage.totalTokens) || input + output + cacheRead + cacheWrite };
131
- }
132
-
133
- function findTools(payload: unknown): unknown[] | undefined {
134
- if (!payload || typeof payload !== "object") return undefined;
135
- if (Array.isArray(payload)) {
136
- for (const item of payload) { const found = findTools(item); if (found) return found; }
137
- return undefined;
138
- }
139
- const object = payload as JsonObject;
140
- if (Array.isArray(object.tools)) return object.tools;
141
- for (const value of Object.values(object)) { const found = findTools(value); if (found) return found; }
142
- return undefined;
143
- }
144
-
145
- function nativeMarkers(value: unknown, markers = { toolSearchCall: 0, toolSearchOutput: 0 }): typeof markers {
146
- if (Array.isArray(value)) {
147
- for (const item of value) nativeMarkers(item, markers);
148
- } else if (value && typeof value === "object") {
149
- const object = value as JsonObject;
150
- if (object.type === "tool_search_call") markers.toolSearchCall += 1;
151
- if (object.type === "tool_search_output") markers.toolSearchOutput += 1;
152
- for (const item of Object.values(object)) nativeMarkers(item, markers);
153
- }
154
- return markers;
155
- }
156
-
157
- function sanitizeEvent(event: JsonObject): JsonObject {
158
- const clean: JsonObject = { type: event.type };
159
- if (typeof event.toolName === "string") clean.toolName = event.toolName;
160
- if (typeof event.isError === "boolean") clean.isError = event.isError;
161
- const message = event.message as JsonObject | undefined;
162
- if (message && typeof message.role === "string") {
163
- clean.role = message.role;
164
- clean.hasError = typeof message.errorMessage === "string" && message.errorMessage.length > 0;
165
- }
166
- return clean;
167
- }
168
-
169
- async function runProcess(command: string, args: string[], cwd: string, env: NodeJS.ProcessEnv, timeoutMs: number, maxOutputChars: number): Promise<{ stdout: string; stderr: string; exitCode: number | null; timedOut: boolean; outputLimitExceeded: boolean; wallTimeMs: number }> {
170
- const started = performance.now();
171
- return await new Promise((resolveProcess, reject) => {
172
- const child = spawn(command, args, { cwd, env, stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
173
- let stdout = "";
174
- let stderr = "";
175
- let timedOut = false;
176
- let outputLimitExceeded = false;
177
- let settled = false;
178
- const finish = (exitCode: number | null) => {
179
- if (settled) return;
180
- settled = true;
181
- clearTimeout(timer);
182
- resolveProcess({ stdout, stderr, exitCode, timedOut, outputLimitExceeded, wallTimeMs: Math.round(performance.now() - started) });
183
- };
184
- const terminate = () => {
185
- if (process.platform === "win32" && child.pid) {
186
- spawn("taskkill.exe", ["/PID", String(child.pid), "/T", "/F"], { windowsHide: true, stdio: "ignore" });
187
- } else child.kill("SIGTERM");
188
- };
189
- const stopForOutput = () => {
190
- if (outputLimitExceeded) return;
191
- outputLimitExceeded = true;
192
- terminate();
193
- };
194
- const timer = setTimeout(() => { timedOut = true; terminate(); }, timeoutMs);
195
- child.stdout.on("data", (chunk: Buffer) => { stdout += chunk.toString("utf8"); if (stdout.length + stderr.length > maxOutputChars) stopForOutput(); });
196
- child.stderr.on("data", (chunk: Buffer) => { stderr += chunk.toString("utf8"); if (stdout.length + stderr.length > maxOutputChars) stopForOutput(); });
197
- // Treat launch failures as failed trial results so raw capture cleanup and the
198
- // sanitized aggregate still happen; never surface child output verbatim.
199
- child.once("error", () => finish(null));
200
- child.once("close", finish);
201
- });
202
- }
203
-
204
- function parseCapture(path: string): { providerRequests: number; initialToolCount?: number; initialToolSchemaChars?: number; nativeToolSearch: { toolSearchCall: number; toolSearchOutput: number }; captureParseErrors: number } {
205
- const answer = { providerRequests: 0, initialToolCount: undefined as number | undefined, initialToolSchemaChars: undefined as number | undefined, nativeToolSearch: { toolSearchCall: 0, toolSearchOutput: 0 }, captureParseErrors: 0 };
206
- if (!existsSync(path)) return answer;
207
- for (const line of readFileSync(path, "utf8").split(/\r?\n/).filter(Boolean)) {
208
- try {
209
- const record = JSON.parse(line) as JsonObject;
210
- answer.providerRequests += 1;
211
- const payload = record.payload;
212
- if (answer.initialToolCount === undefined) {
213
- const tools = findTools(payload);
214
- if (tools) {
215
- answer.initialToolCount = tools.length;
216
- answer.initialToolSchemaChars = JSON.stringify(tools).length;
217
- }
218
- }
219
- const markers = nativeMarkers(payload);
220
- answer.nativeToolSearch.toolSearchCall += markers.toolSearchCall;
221
- answer.nativeToolSearch.toolSearchOutput += markers.toolSearchOutput;
222
- } catch { answer.captureParseErrors += 1; }
223
- }
224
- return answer;
225
- }
226
-
227
- function parseTrial(stdout: string, stderr: string, processResult: Awaited<ReturnType<typeof runProcess>>, capturePath: string, condition: Condition, testCase: EvalCase, config: EvalConfig, includeEvents: boolean): JsonObject {
228
- const events: JsonObject[] = [];
229
- let invalidJsonLines = 0;
230
- const toolCalls: string[] = [];
231
- const toolCallRecords: Array<{ name: string; args: JsonObject }> = [];
232
- const toolErrors: string[] = [];
233
- const activated: string[] = [];
234
- const usage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 };
235
- let assistantErrors = 0;
236
- let assistantMessages = 0;
237
-
238
- for (const line of stdout.split(/\r?\n/).filter(Boolean)) {
239
- let event: JsonObject;
240
- try { event = JSON.parse(line) as JsonObject; } catch { invalidJsonLines += 1; continue; }
241
- if (includeEvents) events.push(sanitizeEvent(event));
242
- if (event.type === "tool_execution_start" && typeof event.toolName === "string") {
243
- toolCalls.push(event.toolName);
244
- toolCallRecords.push({ name: event.toolName, args: event.args && typeof event.args === "object" ? event.args as JsonObject : {} });
245
- }
246
- if (event.type === "tool_execution_end") {
247
- if (event.isError === true && typeof event.toolName === "string") toolErrors.push(event.toolName);
248
- const result = event.result as JsonObject | undefined;
249
- const details = result?.details as JsonObject | undefined;
250
- if (Array.isArray(details?.added)) activated.push(...details.added.filter((name): name is string => typeof name === "string"));
251
- }
252
- if (event.type === "message_end") {
253
- const message = event.message as JsonObject | undefined;
254
- if (message?.role === "assistant") {
255
- assistantMessages += 1;
256
- const current = readUsage(message);
257
- for (const key of Object.keys(usage) as Array<keyof typeof usage>) usage[key] += current[key];
258
- if (typeof message.errorMessage === "string" && message.errorMessage.length > 0) assistantErrors += 1;
259
- }
260
- }
261
- }
262
-
263
- const capture = parseCapture(capturePath);
264
- const counts = Object.fromEntries([...new Set(toolCalls)].map((tool) => [tool, toolCalls.filter((entry) => entry === tool).length]));
265
- const checks: Array<{ name: string; pass: boolean; detail?: string }> = [];
266
- const loaderExpected = expectedLoaderCalls(condition, testCase);
267
- for (const tool of testCase.requiredTools) checks.push({ name: `required:${tool}`, pass: counts[tool] === 1, detail: `observed ${counts[tool] ?? 0}` });
268
- for (const tool of testCase.requiredPreflightTools ?? []) {
269
- const records = toolCallRecords.filter((call) => call.name === tool);
270
- checks.push({ name: `preflight:${tool}`, pass: records.length === 1 && records[0].args.preflight === true, detail: records.length === 1 ? `preflight=${String(records[0].args.preflight)}` : `observed ${records.length} calls` });
271
- }
272
- for (const tool of testCase.forbiddenTools) checks.push({ name: `forbidden:${tool}`, pass: !counts[tool], detail: `observed ${counts[tool] ?? 0}` });
273
- checks.push({ name: "loader-count", pass: (counts[LOADER] ?? 0) === loaderExpected, detail: `expected ${loaderExpected}, observed ${counts[LOADER] ?? 0}` });
274
- if (loaderExpected > 0) {
275
- const expectedActivated = testCase.minimalActivatedTools.filter((tool) => !isInitiallyActive(condition, tool));
276
- const actualActivated = [...new Set(activated)];
277
- checks.push({ name: "exact-smallest-sufficient-activation", pass: sameToolSet(actualActivated, expectedActivated), detail: `expected ${expectedActivated.join(", ") || "none"}; observed ${actualActivated.join(", ") || "none"}` });
278
- }
279
- const nativeMarkerPairs = Math.min(capture.nativeToolSearch.toolSearchCall, capture.nativeToolSearch.toolSearchOutput);
280
- checks.push({
281
- name: "native-deferred-markers",
282
- pass: loaderExpected > 0 ? nativeMarkerPairs >= 1 : nativeMarkerPairs === 0,
283
- detail: `expected ${loaderExpected > 0 ? "at least one" : "zero"}; call=${capture.nativeToolSearch.toolSearchCall}, output=${capture.nativeToolSearch.toolSearchOutput}`,
284
- });
285
- checks.push({ name: "max-four-activated", pass: activated.length <= 4, detail: `observed ${activated.length}` });
286
- if (testCase.id === "negative-no-operation" || testCase.id === "ambiguous-branch-discovery") {
287
- const destructive = new Set(config.destructiveTools);
288
- const unsafeActivations = activated.filter((tool) => destructive.has(tool));
289
- const unsafeCalls = toolCalls.filter((tool) => destructive.has(tool));
290
- checks.push({ name: "no-destructive-activation", pass: unsafeActivations.length === 0, detail: unsafeActivations.join(", ") || "none" });
291
- checks.push({ name: "no-destructive-calls", pass: unsafeCalls.length === 0, detail: unsafeCalls.join(", ") || "none" });
292
- if (testCase.id === "negative-no-operation") checks.push({ name: "no-plastic-operation", pass: toolCalls.length === 0, detail: toolCalls.join(", ") || "none" });
293
- }
294
- checks.push({ name: "no-tool-errors", pass: toolErrors.length === 0, detail: toolErrors.join(", ") || "none" });
295
- checks.push({ name: "no-assistant-errors", pass: assistantErrors === 0, detail: String(assistantErrors) });
296
- checks.push({ name: "bounded-tool-calls", pass: toolCalls.length <= config.maxToolCalls, detail: `${toolCalls.length}/${config.maxToolCalls}` });
297
- checks.push({ name: "bounded-output", pass: !processResult.outputLimitExceeded, detail: `${stdout.length + stderr.length}/${config.maxOutputChars}` });
298
- checks.push({ name: "not-timed-out", pass: !processResult.timedOut, detail: `${processResult.wallTimeMs}ms` });
299
- checks.push({ name: "process-exit", pass: processResult.exitCode === 0, detail: String(processResult.exitCode) });
300
- checks.push({ name: "clean-jsonl", pass: invalidJsonLines === 0, detail: String(invalidJsonLines) });
301
-
302
- return {
303
- condition, caseId: testCase.id, pass: checks.every((check) => check.pass), checks,
304
- wallTimeMs: processResult.wallTimeMs, exitCode: processResult.exitCode, timedOut: processResult.timedOut,
305
- outputChars: stdout.length + stderr.length, toolCalls, toolCallCounts: counts,
306
- loaderRequests: toolCallRecords.filter((call) => call.name === LOADER).map((call) => ({ query: call.args.query, toolNames: call.args.toolNames, limit: call.args.limit })),
307
- activated: [...new Set(activated)], toolErrors,
308
- assistantMessages, providerRequests: capture.providerRequests,
309
- assistantUsage: usage, initialToolSchema: { toolCount: capture.initialToolCount, serializedChars: capture.initialToolSchemaChars },
310
- nativeToolSearch: capture.nativeToolSearch, captureParseErrors: capture.captureParseErrors,
311
- ...(includeEvents ? { events } : {}),
312
- };
313
- }
314
-
315
- function timestamp(): string { return new Date().toISOString().replace(/[:.]/g, "-"); }
316
-
317
- async function main(): Promise<void> {
318
- const options = parseOptions(process.argv.slice(2));
319
- const config = readJson<EvalConfig>(CONFIG_PATH);
320
- const cases = readJson<{ cases: EvalCase[] }>(CASES_PATH).cases;
321
- const baseline = readJson<JsonObject>(BASELINE_PATH);
322
- validate(config, cases, baseline, options);
323
- const selectedCases = options.caseIds.length ? cases.filter((testCase) => options.caseIds.includes(testCase.id)) : cases;
324
- if (!options.dryRun && !options.model) fail("--model is required for a live model evaluation");
325
- if (options.model && !MODEL_PATTERN.test(options.model)) fail("--model must match openai-codex/gpt-5.6-(luna|terra|sol), optionally followed by a supported thinking level");
326
- const configuredSandbox = process.env[config.sandboxCwdEnv]?.trim();
327
- if (!options.dryRun && !configuredSandbox) fail(`Set ${config.sandboxCwdEnv} to the dedicated Plastic sandbox path`);
328
- const sandboxCwd = configuredSandbox ? resolve(configuredSandbox) : `<${config.sandboxCwdEnv}>`;
329
- if (!options.dryRun) {
330
- if (!existsSync(sandboxCwd)) fail(`Dedicated sandbox does not exist: ${sandboxCwd}`);
331
- const markerPath = join(sandboxCwd, config.sandboxMarkerFile);
332
- if (!existsSync(markerPath) || readFileSync(markerPath, "utf8") !== config.sandboxMarkerContent) fail(`Dedicated sandbox must contain ${config.sandboxMarkerFile} with the documented exact content`);
333
- if (process.env[config.sandboxAuthorizationEnv] !== config.sandboxAuthorizationValue) fail(`Set ${config.sandboxAuthorizationEnv}=${config.sandboxAuthorizationValue} to attest the dedicated sandbox`);
334
- }
335
-
336
- const piPackagePath = join(PACKAGE_ROOT, "node_modules", "@earendil-works", "pi-coding-agent", "package.json");
337
- const piVersion = existsSync(piPackagePath) ? (readJson<{ version?: string }>(piPackagePath).version ?? "unknown") : "missing";
338
- if (!piVersion.startsWith(config.piVersionPrefix)) fail(`This eval requires the Pi ${config.piVersionPrefix.slice(0, -1)} patch line; found ${piVersion}`);
339
- const piCli = join(PACKAGE_ROOT, "node_modules", "@earendil-works", "pi-coding-agent", "dist", "cli.js");
340
- const baseArgs = ["--mode", "json", "--no-session", "--no-approve", "--no-context-files", "--no-extensions", "-e", join(PACKAGE_ROOT, "index.ts"), "-e", join(HERE, "mutation-guard.ts"), "-e", join(HERE, "provider-capture.ts"), "--no-skills", "--no-prompt-templates", "--no-builtin-tools"];
341
- if (options.dryRun) {
342
- console.log(`VALID: ${selectedCases.length} cases × ${options.conditions.length} conditions × ${options.trials} trial(s)`);
343
- console.log(`Pi ${config.piVersionPrefix.slice(0, -1)} subprocess: ${process.execPath} ${piCli} ${baseArgs.join(" ")} --model <approved-model> <case-prompt>`);
344
- console.log(`cwd: ${sandboxCwd}`);
345
- return;
346
- }
347
-
348
- mkdirSync(RESULTS_DIR, { recursive: true });
349
- const trials: JsonObject[] = [];
350
- for (const condition of options.conditions) {
351
- for (const testCase of selectedCases) {
352
- for (let trial = 1; trial <= options.trials; trial += 1) {
353
- const capturePath = join(tmpdir(), `pi-plastic-tool-loading-${process.pid}-${Date.now()}-${condition}-${testCase.id}-${trial}.jsonl`);
354
- const args = [...baseArgs, "--model", options.model!, testCase.prompt];
355
- const result = await runProcess(process.execPath, [piCli, ...args], sandboxCwd, { ...process.env, PI_PLASTIC_TOOL_LOADING_MODE: condition, PI_PLASTIC_EVAL_PROVIDER_CAPTURE: capturePath }, config.timeoutMs, config.maxOutputChars);
356
- const summary = parseTrial(result.stdout, result.stderr, result, capturePath, condition, testCase, config, options.includeEvents);
357
- if (options.keep) summary.rawCapturePath = capturePath;
358
- else rmSync(capturePath, { force: true });
359
- trials.push({ trial, ...summary });
360
- console.log(`${condition} ${testCase.id} #${trial}: ${summary.pass ? "PASS" : "FAIL"} (${summary.wallTimeMs}ms)`);
361
- }
362
- }
363
- }
364
- const passed = trials.filter((trial) => trial.pass === true).length;
365
- const report = { schemaVersion: 2, generatedAt: new Date().toISOString(), sandbox: "attested dedicated sandbox", model: options.model, options: { trials: options.trials, conditions: options.conditions, cases: selectedCases.map((testCase) => testCase.id), includeEvents: options.includeEvents, rawCapturesKept: options.keep }, baseline, aggregate: { trials: trials.length, passed, failed: trials.length - passed }, trials };
366
- const reportPath = join(RESULTS_DIR, `tool-loading-${timestamp()}.json`);
367
- writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
368
- console.log(`Aggregate: ${passed}/${trials.length} passed; sanitized summary: ${reportPath}`);
369
- if (passed !== trials.length) process.exitCode = 1;
370
- }
371
-
372
- void main().catch((error: unknown) => { console.error(error instanceof Error ? `ERROR: ${error.message}` : "ERROR: evaluation failed"); process.exitCode = 1; });
@@ -1,66 +0,0 @@
1
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import { createPlasticRepositorySearchPolicyV1, loadPlasticOwnerV1 } from "../src/repository-search-provider.ts";
3
-
4
- const REPO_SEARCH_CONTRACT_RUNTIME = "@aefree/pi-repo-search/contracts/" + "v1";
5
-
6
- type PolicyToken = { readonly id: string; readonly nonce: string };
7
- type RepositoryPolicyRegistry = {
8
- register(scope: object, policy: ReturnType<typeof createPlasticRepositorySearchPolicyV1>): PolicyToken;
9
- unregister(token: PolicyToken): boolean;
10
- };
11
- type RepoSearchContractRuntime = {
12
- createRepositoryPolicyRegistryV1?: () => RepositoryPolicyRegistry;
13
- };
14
- type RuntimeLoader = () => Promise<RepoSearchContractRuntime>;
15
- type ScopeRegistrations = Readonly<{ policyToken: PolicyToken; registry: RepositoryPolicyRegistry }>;
16
-
17
- /** Accept only failures resolving the optional package itself; installed-package defects remain visible. */
18
- export function isMissingRepoSearchRuntime(error: unknown): boolean {
19
- if (typeof error !== "object" || error === null) return false;
20
- const candidate = error as { code?: unknown; message?: unknown };
21
- if (candidate.code !== "ERR_MODULE_NOT_FOUND" && candidate.code !== "MODULE_NOT_FOUND") return false;
22
- if (typeof candidate.message !== "string") return false;
23
- return candidate.message.includes("Cannot find package '@aefree/pi-repo-search'") ||
24
- candidate.message.includes(`'${REPO_SEARCH_CONTRACT_RUNTIME}'`) ||
25
- candidate.message.includes(`\"${REPO_SEARCH_CONTRACT_RUNTIME}\"`);
26
- }
27
-
28
- const loadRepoSearchRuntime: RuntimeLoader = async () => import(REPO_SEARCH_CONTRACT_RUNTIME) as Promise<RepoSearchContractRuntime>;
29
-
30
- /** Session-scoped optional Plastic policy registration when pi-repo-search is installed. */
31
- export function createPlasticRepositoryProviderExtension(loadRuntime: RuntimeLoader = loadRepoSearchRuntime): (pi: ExtensionAPI) => void {
32
- return (pi) => {
33
- const registrations = new WeakMap<object, ScopeRegistrations>();
34
- const unregisterScope = (current: ScopeRegistrations | undefined): void => {
35
- if (current !== undefined) current.registry.unregister(current.policyToken);
36
- };
37
-
38
- pi.on("session_start", async (_event, ctx) => {
39
- let runtime: RepoSearchContractRuntime;
40
- try {
41
- runtime = await loadRuntime();
42
- } catch (error) {
43
- if (isMissingRepoSearchRuntime(error)) return;
44
- throw error;
45
- }
46
- if (typeof runtime.createRepositoryPolicyRegistryV1 !== "function") return;
47
- const registry = runtime.createRepositoryPolicyRegistryV1();
48
- const owner = await loadPlasticOwnerV1(import.meta.url);
49
- const scope = ctx.sessionManager;
50
- unregisterScope(registrations.get(scope));
51
- const policyToken = registry.register(scope, createPlasticRepositorySearchPolicyV1(owner));
52
- registrations.set(scope, Object.freeze({ policyToken, registry }));
53
- });
54
- pi.on("session_shutdown", (_event, ctx) => {
55
- const scope = ctx.sessionManager;
56
- const current = registrations.get(scope);
57
- if (current === undefined) return;
58
- unregisterScope(current);
59
- registrations.delete(scope);
60
- });
61
- };
62
- }
63
-
64
- export default function registerPlasticRepositoryProviders(pi: ExtensionAPI): void {
65
- createPlasticRepositoryProviderExtension()(pi);
66
- }
@@ -1,55 +0,0 @@
1
- import { __bashCmDiffGuardInternals } from "../extensions/bash-cm-diff-guard.ts";
2
-
3
- const assert = (condition: boolean, message: string): void =>
4
- {
5
- if (!condition)
6
- {
7
- throw new Error(message);
8
- }
9
- };
10
-
11
- const expectBlocked = (command: string): void =>
12
- {
13
- assert(
14
- __bashCmDiffGuardInternals.commandRunsCmDiff(command),
15
- `Expected command to be blocked: ${command}`,
16
- );
17
- };
18
-
19
- const expectAllowed = (command: string): void =>
20
- {
21
- assert(
22
- !__bashCmDiffGuardInternals.commandRunsCmDiff(command),
23
- `Expected command to be allowed: ${command}`,
24
- );
25
- };
26
-
27
- const main = (): void =>
28
- {
29
- expectBlocked("cm diff cs:123");
30
- expectBlocked("env FOO=bar cm diff cs:123");
31
- expectBlocked("command cm diff cs:123");
32
- expectBlocked("sudo cm diff cs:123");
33
- expectBlocked("time cm diff cs:123");
34
- expectBlocked("nice -n 5 cm diff cs:123");
35
- expectBlocked("zsh -lc \"cm diff cs:123\"");
36
- expectBlocked("zsh -c \"cm diff cs:123\"");
37
- expectBlocked("zsh -lc \"command cm diff cs:123\"");
38
- expectBlocked("bash -lc \"env FOO=bar cm diff cs:123\"");
39
- expectBlocked("cmd /c \"cm diff cs:123\"");
40
- expectBlocked("cmd /c cm diff cs:123");
41
- expectBlocked("pwsh -command \"cm diff cs:123\"");
42
- expectBlocked("powershell -command cm diff cs:123");
43
- expectBlocked("\"C:\\Program Files\\PlasticSCM5\\client\\cm.exe\" diff cs:123");
44
- expectBlocked("cm status & cm diff cs:123");
45
- expectBlocked("cm status\ncm diff cs:123");
46
-
47
- expectAllowed("cm status");
48
- expectAllowed("cm merge br:/main");
49
- expectAllowed("git diff --stat");
50
- expectAllowed("zsh -lc \"cm status\"");
51
-
52
- console.log("PASS: bash cm diff guard validation succeeded");
53
- };
54
-
55
- main();
@@ -1,3 +0,0 @@
1
- repository sanitized-repository@sanitized-server
2
- path /
3
- smartbranch /main/feature/footer
@@ -1 +0,0 @@
1
- Workspace status for cs:42@br:/main/feature/footer@sanitized-repository@sanitized-server
@@ -1 +0,0 @@
1
- /main/feature/footer@sanitized-repository@sanitized-server (cs:42 - head)
@@ -1,28 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { mkdtemp, mkdir, rm } from "node:fs/promises";
3
- import { tmpdir } from "node:os";
4
- import path from "node:path";
5
- import { pathToFileURL } from "node:url";
6
- import { spawnSync } from "node:child_process";
7
- import { createPlasticRepositorySearchPolicyV1, loadPlasticOwnerV1 } from "../src/repository-search-provider.ts";
8
-
9
- const temp = await mkdtemp(path.join(tmpdir(), "pi-plastic-packed-copies-"));
10
- try {
11
- const npmCli = process.env.npm_execpath ?? (process.platform === "win32" ? path.join(process.env.APPDATA!, "npm", "node_modules", "npm", "bin", "npm-cli.js") : "/usr/lib/node_modules/npm/bin/npm-cli.js");
12
- const packed = spawnSync(process.execPath, [npmCli, "pack", "--json", "--pack-destination", temp], { cwd: path.join(import.meta.dirname, ".."), encoding: "utf8", shell: false });
13
- assert.equal(packed.status, 0, packed.stderr);
14
- const filename = JSON.parse(packed.stdout)[0].filename as string;
15
- const first = path.join(temp, "first"); const second = path.join(temp, "second");
16
- await mkdir(first); await mkdir(second);
17
- for (const destination of [first, second]) {
18
- const extracted = spawnSync("tar", ["-xf", filename, "-C", path.basename(destination), "package/package.json", "package/extensions/repository-search-provider.ts"], { cwd: temp, encoding: "utf8", shell: false });
19
- assert.equal(extracted.status, 0, extracted.stderr);
20
- }
21
- const entry = (root: string) => pathToFileURL(path.join(root, "package", "extensions", "repository-search-provider.ts")).href;
22
- const ownerOne = await loadPlasticOwnerV1(entry(first));
23
- const ownerTwo = await loadPlasticOwnerV1(entry(second));
24
- assert.notEqual(ownerOne.packageRoot, ownerTwo.packageRoot);
25
- assert.equal(ownerOne.packageVersion, ownerTwo.packageVersion);
26
- assert.equal(createPlasticRepositorySearchPolicyV1(ownerOne).id, createPlasticRepositorySearchPolicyV1(ownerTwo).id);
27
- } finally { await rm(temp, { recursive: true, force: true }); }
28
- console.log("PASS: two packed physical pi-plastic copies retain distinct owner roots");
@@ -1,116 +0,0 @@
1
- import { fileURLToPath } from "node:url";
2
-
3
- export type ToolSourceInfo = {
4
- path: string;
5
- source: string;
6
- scope: string;
7
- origin: string;
8
- baseDir?: string;
9
- };
10
-
11
- export type RegisteredTool = {
12
- name: string;
13
- label?: string;
14
- description?: string;
15
- promptSnippet?: string;
16
- promptGuidelines?: string[];
17
- sourceInfo?: ToolSourceInfo;
18
- parameters?: unknown;
19
- constrainedSampling?: false | {
20
- type: "json_schema";
21
- strict: "prefer" | "require";
22
- } | {
23
- type: "grammar";
24
- variants: {
25
- openai_lark?: string;
26
- openai_regex?: string;
27
- };
28
- };
29
- prepareArguments?: (args: unknown) => Record<string, unknown>;
30
- execute: (...args: any[]) => Promise<any> | any;
31
- };
32
-
33
- type SessionStartHandler = (event: unknown, ctx: { sessionManager: { getBranch: () => unknown[] } }) => Promise<void> | void;
34
-
35
- const DEFAULT_EXTENSION_SOURCE: ToolSourceInfo = {
36
- path: fileURLToPath(new URL("../index.ts", import.meta.url)),
37
- source: "extension",
38
- scope: "project",
39
- origin: "package",
40
- };
41
-
42
- export class PiToolHarness {
43
- readonly registry = new Map<string, RegisteredTool>();
44
- readonly setActiveToolsCalls: string[][] = [];
45
- private readonly sessionStartHandlers: SessionStartHandler[] = [];
46
- private readonly sourceInfoAvailable: boolean;
47
- private readonly extensionSourceInfo: ToolSourceInfo;
48
- private activeTools: string[];
49
- private branchEntries: unknown[];
50
-
51
- constructor(options: {
52
- activeTools?: string[];
53
- branchEntries?: unknown[];
54
- foreignTools?: RegisteredTool[];
55
- sourceInfoAvailable?: boolean;
56
- extensionSourceInfo?: ToolSourceInfo;
57
- } = {}) {
58
- this.activeTools = [...(options.activeTools ?? [])];
59
- this.branchEntries = [...(options.branchEntries ?? [])];
60
- // Default to Pi 0.82 behavior; individual compatibility tests explicitly
61
- // opt out to model older runtimes without canonical sourceInfo.
62
- this.sourceInfoAvailable = options.sourceInfoAvailable ?? true;
63
- this.extensionSourceInfo = options.extensionSourceInfo ?? DEFAULT_EXTENSION_SOURCE;
64
- for (const tool of options.foreignTools ?? []) this.registry.set(tool.name, { ...tool });
65
- }
66
-
67
- readonly api = {
68
- registerTool: (tool: RegisteredTool) => {
69
- // Pi keeps the first effective definition for a colliding extension name.
70
- if (!this.registry.has(tool.name)) {
71
- this.registry.set(tool.name, this.sourceInfoAvailable ? { ...tool, sourceInfo: this.extensionSourceInfo } : { ...tool });
72
- }
73
- },
74
- on: (event: string, handler: SessionStartHandler) => {
75
- if (event === "session_start") this.sessionStartHandlers.push(handler);
76
- },
77
- getActiveTools: () => [...this.activeTools],
78
- getAllTools: () => [...this.registry.values()],
79
- setActiveTools: (names: string[]) => {
80
- this.activeTools = [...new Set(names)];
81
- this.setActiveToolsCalls.push([...this.activeTools]);
82
- },
83
- };
84
-
85
- async load(modulePath = "../index.ts"): Promise<void> {
86
- const extensionModule = await import(modulePath);
87
- const extension = extensionModule.default as ((pi: any) => void) | undefined;
88
- if (typeof extension !== "function") throw new Error(`Extension module '${modulePath}' has no default function export.`);
89
- extension(this.api);
90
- }
91
-
92
- async startSession(branchEntries = this.branchEntries): Promise<void> {
93
- this.branchEntries = [...branchEntries];
94
- const ctx = { sessionManager: { getBranch: () => [...this.branchEntries] } };
95
- for (const handler of this.sessionStartHandlers) await handler({ reason: "startup" }, ctx);
96
- }
97
-
98
- getActiveTools(): string[] {
99
- return [...this.activeTools];
100
- }
101
- }
102
-
103
- export async function loadRegisteredTools(modulePath = "../index.ts"): Promise<Map<string, RegisteredTool>> {
104
- const harness = new PiToolHarness();
105
- await harness.load(modulePath);
106
- return harness.registry;
107
- }
108
-
109
- export async function invokeRegisteredTool(name: string, args: Record<string, unknown>, cwd = process.cwd(), modulePath = "../index.ts"): Promise<string> {
110
- const tools = await loadRegisteredTools(modulePath);
111
- const tool = tools.get(name);
112
- if (!tool) throw new Error(`Tool '${name}' is not registered.`);
113
- const result = await tool.execute("test-tool-call", args, undefined, undefined, { cwd });
114
- const textParts = Array.isArray(result?.content) ? result.content.filter((entry: any) => entry?.type === "text") : [];
115
- return textParts.map((entry: any) => String(entry.text ?? "")).join("\n");
116
- }