@selesai/code 0.13.35 → 0.13.38
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 +21 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +2 -0
- package/dist/core/remote-catalog-provider.js +2 -2
- package/dist/core/remote-catalog-provider.test.js +20 -1
- package/dist/extensions/jev/decisions.test.ts +18 -0
- package/dist/extensions/jev/decisions.ts +54 -6
- package/dist/extensions/jev-ask-tool.test.ts +371 -25
- package/dist/extensions/jev-ask-tool.ts +826 -167
- package/dist/extensions/jev-find-source.ts +190 -0
- package/dist/extensions/package.json +1 -1
- package/dist/extensions/pi-subagents/agents/researcher.md +4 -5
- package/dist/extensions/pi-subagents/docs/agents.md +1 -7
- package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +3 -4
- package/dist/extensions/pi-web-agent/src/extension.ts +11 -0
- package/dist/extensions/ponytail/index.js +7 -4
- package/dist/extensions/ponytail/ponytail-config.cjs +52 -26
- package/dist/extensions/ponytail/ponytail-instructions.cjs +3 -17
- package/dist/extensions/ponytail/test/extension.test.js +37 -16
- package/dist/extensions/ponytail/test/helpers.test.js +54 -51
- package/dist/skills/ponytail/SKILL.md +5 -7
- package/dist/skills/ponytail-help/SKILL.md +10 -14
- package/docs/settings.md +8 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@selesai/code` will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.13.38] - 2026-10-01
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
- **`jev_find` returns source evidence, not just file pointers.** Large trees are narrowed through directory, file, and source-unit judging; results include ranked files, reading leads, and accepted source verbatim with original line numbers, capped at 12 Jev requests and 16 KiB of source per call. JavaScript and TypeScript are split into declarations when the parser is available; other files and installs without it use bounded text chunks.
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **Jev Find remains useful when Jev cannot judge.** It returns ripgrep-ranked candidates with local keyword-window source, and provider diagnostics expose only a failure category and optional HTTP status, never raw provider error text.
|
|
12
|
+
|
|
13
|
+
## [0.13.37] - 2026-09-30
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- **Ponytail starts in `lite` by default.** With no `PONYTAIL_DEFAULT_MODE` and no `ponytailMode` in the agent `settings.json`, new sessions and handoffs now start in `lite` instead of `off`. A saved choice still wins: `/ponytail off` (or `/ponytail default off`) keeps it off.
|
|
17
|
+
|
|
18
|
+
## [0.13.36] - 2026-09-29
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- **Remote model catalogs load again.** The pi.dev catalog request identified itself as `pi/<selesai version>`, and pi.dev answers an unknown Pi version with 404 ("No compatible model catalog is available for this Pi version"). Selesai cached that as an empty catalog, so models newer than the bundled data — such as `openai-codex/gpt-6-sol`, `openai-codex/gpt-6-luna`, and `github-copilot/gpt-6-luna` — showed as `[unavailable]` in the model list even when logged in. The request now carries the upstream Pi platform version (`PI_PLATFORM_VERSION`, the pinned `@earendil-works/pi-ai` release). An existing empty entry in `models-store.json` is refetched after its 4-hour window, or immediately if you delete it.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
- **Ponytail is off by default and remembers your mode.** Opt in with `/ponytail`. `/ponytail <mode>` saves the choice as `ponytailMode` in the agent `settings.json`, so new sessions and handoffs start in it; other keys are preserved and an unparseable `settings.json` is never overwritten. Modes are `off`, `lite`, and `full` (`ultra` was removed), and every mode now gets upstream's full ponytail prompt.
|
|
25
|
+
|
|
5
26
|
## [0.13.35] - 2026-09-29
|
|
6
27
|
|
|
7
28
|
### Added
|
package/dist/config.d.ts
CHANGED
|
@@ -92,6 +92,8 @@ export declare const APP_NAME: string;
|
|
|
92
92
|
export declare const APP_TITLE: string;
|
|
93
93
|
export declare const CONFIG_DIR_NAME: string;
|
|
94
94
|
export declare const VERSION: string;
|
|
95
|
+
/** Upstream Pi platform version (the pinned pi-ai release). pi.dev gates model catalogs on this, not on VERSION. */
|
|
96
|
+
export declare const PI_PLATFORM_VERSION: string;
|
|
95
97
|
export declare const ENV_AGENT_DIR: string;
|
|
96
98
|
export declare const ENV_SESSION_DIR: string;
|
|
97
99
|
export declare function expandTildePath(path: string): string;
|
package/dist/config.js
CHANGED
|
@@ -463,6 +463,8 @@ export const APP_NAME = piConfigName || "pi";
|
|
|
463
463
|
export const APP_TITLE = piConfigName ? APP_NAME : "π";
|
|
464
464
|
export const CONFIG_DIR_NAME = pkg.piConfig?.configDir || ".pi";
|
|
465
465
|
export const VERSION = pkg.version || "0.0.0";
|
|
466
|
+
/** Upstream Pi platform version (the pinned pi-ai release). pi.dev gates model catalogs on this, not on VERSION. */
|
|
467
|
+
export const PI_PLATFORM_VERSION = pkg.dependencies?.["@earendil-works/pi-ai"]?.replace(/^[^\d]*/, "") || VERSION;
|
|
466
468
|
// e.g., PI_CODING_AGENT_DIR or TAU_CODING_AGENT_DIR
|
|
467
469
|
export const ENV_AGENT_DIR = `${APP_NAME.toUpperCase()}_CODING_AGENT_DIR`;
|
|
468
470
|
export const ENV_SESSION_DIR = `${APP_NAME.toUpperCase()}_CODING_AGENT_SESSION_DIR`;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { PI_PLATFORM_VERSION } from "../config.js";
|
|
2
2
|
import { getPiUserAgent } from "../utils/pi-user-agent.js";
|
|
3
3
|
const DEFAULT_CATALOG_BASE_URL = "https://pi.dev";
|
|
4
4
|
export const REMOTE_CATALOG_REFRESH_INTERVAL_MS = 4 * 60 * 60 * 1000;
|
|
@@ -70,7 +70,7 @@ export function withRemoteCatalog(provider, catalogBaseUrl = DEFAULT_CATALOG_BAS
|
|
|
70
70
|
const response = await fetch(url, {
|
|
71
71
|
headers: {
|
|
72
72
|
accept: "application/json",
|
|
73
|
-
"User-Agent": getPiUserAgent(
|
|
73
|
+
"User-Agent": getPiUserAgent(PI_PLATFORM_VERSION),
|
|
74
74
|
...(validator ? { "if-none-match": validator } : {}),
|
|
75
75
|
},
|
|
76
76
|
signal: context.signal,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { PI_PLATFORM_VERSION, VERSION } from "../config.js";
|
|
2
3
|
import { withRemoteCatalog } from "./remote-catalog-provider.js";
|
|
3
4
|
const cachedModel = {
|
|
4
5
|
id: "gpt-6-sol",
|
|
@@ -26,4 +27,22 @@ describe("remote model catalog cache", () => {
|
|
|
26
27
|
await wrapped.refreshModels?.(refreshContext({ models: [cachedModel], checkedAt: localGeneratedAt + 1, lastModified: 0 }));
|
|
27
28
|
expect(wrapped.getModels().map((model) => model.id)).toEqual(["gpt-6-sol"]);
|
|
28
29
|
});
|
|
30
|
+
// pi.dev 404s catalogs for unknown Pi versions; selesai's own VERSION is not a Pi version.
|
|
31
|
+
it("identifies as the upstream Pi platform version, not the selesai version", async () => {
|
|
32
|
+
expect(PI_PLATFORM_VERSION).not.toBe(VERSION);
|
|
33
|
+
const fetchMock = vi.fn(async () => new Response("{}", { status: 200 }));
|
|
34
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
35
|
+
try {
|
|
36
|
+
const provider = { id: "openai-codex", getModels: () => [] };
|
|
37
|
+
await withRemoteCatalog(provider, "https://example.test").refreshModels?.({
|
|
38
|
+
...refreshContext(undefined),
|
|
39
|
+
allowNetwork: true,
|
|
40
|
+
});
|
|
41
|
+
const headers = fetchMock.mock.calls[0][1].headers;
|
|
42
|
+
expect(headers["User-Agent"]).toMatch(new RegExp(`^pi/${PI_PLATFORM_VERSION.replaceAll(".", "\\.")} `));
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
vi.unstubAllGlobals();
|
|
46
|
+
}
|
|
47
|
+
});
|
|
29
48
|
});
|
|
@@ -17,6 +17,7 @@ const state = vi.hoisted(() => ({ settingsPath: "" }));
|
|
|
17
17
|
// layer owns the non-streaming decisions request), so a suite stubs that.
|
|
18
18
|
import {
|
|
19
19
|
askJev,
|
|
20
|
+
askJevAnswers,
|
|
20
21
|
buildConversation,
|
|
21
22
|
buildJevPayload,
|
|
22
23
|
confidenceBucket,
|
|
@@ -278,6 +279,23 @@ describe("askJev", () => {
|
|
|
278
279
|
expect(await ask()).toMatchObject({ failure: "malformed" });
|
|
279
280
|
});
|
|
280
281
|
|
|
282
|
+
it("reports safe transport diagnostics without returning raw provider errors", async () => {
|
|
283
|
+
const error = Object.assign(new Error("secret=must-not-escape"), { status: 503 });
|
|
284
|
+
completeMock.mockRejectedValue(error);
|
|
285
|
+
|
|
286
|
+
const result = await askJevAnswers(runtime() as never, CONNECTION, { payload: {}, maxBytes: 1_024 });
|
|
287
|
+
expect(result).toMatchObject({ failure: "transport", diagnostic: { kind: "provider-error", httpStatus: 503 } });
|
|
288
|
+
expect(JSON.stringify(result)).not.toContain("must-not-escape");
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("extracts only the HTTP status from TokenIn's error completion", async () => {
|
|
292
|
+
completeMock.mockResolvedValue(jevResponse("", { stopReason: "error", errorMessage: "HTTP 429: private response body" }));
|
|
293
|
+
|
|
294
|
+
const result = await askJevAnswers(runtime() as never, CONNECTION, { payload: {}, maxBytes: 1_024 });
|
|
295
|
+
expect(result).toMatchObject({ failure: "transport", diagnostic: { kind: "provider-error", httpStatus: 429 } });
|
|
296
|
+
expect(JSON.stringify(result)).not.toContain("private response body");
|
|
297
|
+
});
|
|
298
|
+
|
|
281
299
|
it("abstains per question on an unlisted, low-confidence, or missing answer", async () => {
|
|
282
300
|
completeMock.mockResolvedValue(jevResponse(jevAnswers({ repository_context: { choice: "maybe", confidence: 1 } })));
|
|
283
301
|
expect(await ask()).toMatchObject({ choices: {}, failure: "unknown-choice" });
|
|
@@ -426,10 +426,41 @@ function responseText(response: { content: readonly { type: string; text?: strin
|
|
|
426
426
|
}
|
|
427
427
|
|
|
428
428
|
function failureReason(error: unknown): JevAbstainReason {
|
|
429
|
-
|
|
429
|
+
try {
|
|
430
|
+
if (isRecord(error) && (error.name === "TimeoutError" || error.name === "AbortError")) return "timeout";
|
|
431
|
+
} catch {
|
|
432
|
+
// A hostile error object is still just a transport failure.
|
|
433
|
+
}
|
|
430
434
|
return "transport";
|
|
431
435
|
}
|
|
432
436
|
|
|
437
|
+
function failureDiagnostic(error: unknown, failure: JevAbstainReason): JevFailureDiagnostic {
|
|
438
|
+
if (failure === "timeout") return { kind: "timeout" };
|
|
439
|
+
try {
|
|
440
|
+
const response = isRecord(error) && isRecord(error.response) ? error.response : undefined;
|
|
441
|
+
const message =
|
|
442
|
+
typeof error === "string"
|
|
443
|
+
? error
|
|
444
|
+
: error instanceof Error
|
|
445
|
+
? error.message
|
|
446
|
+
: isRecord(error) && typeof error.errorMessage === "string"
|
|
447
|
+
? error.errorMessage
|
|
448
|
+
: "";
|
|
449
|
+
const messageStatus = /^HTTP (\d{3}):/.exec(message)?.[1];
|
|
450
|
+
const httpStatus =
|
|
451
|
+
(isRecord(error) ? (error.status ?? error.statusCode ?? response?.status) : undefined) ??
|
|
452
|
+
(messageStatus ? Number(messageStatus) : undefined);
|
|
453
|
+
return {
|
|
454
|
+
kind: "provider-error",
|
|
455
|
+
...(typeof httpStatus === "number" && Number.isInteger(httpStatus) && httpStatus >= 100 && httpStatus <= 599
|
|
456
|
+
? { httpStatus }
|
|
457
|
+
: {}),
|
|
458
|
+
};
|
|
459
|
+
} catch {
|
|
460
|
+
return { kind: "provider-error" };
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
433
464
|
interface JevAuth {
|
|
434
465
|
ok: boolean;
|
|
435
466
|
apiKey?: string;
|
|
@@ -494,10 +525,17 @@ export async function askJev(
|
|
|
494
525
|
return { choices, rejected, elapsedMs: result.elapsedMs };
|
|
495
526
|
}
|
|
496
527
|
|
|
528
|
+
/** Safe, content-free context for a failed Jev request. Never contains provider error text. */
|
|
529
|
+
export interface JevFailureDiagnostic {
|
|
530
|
+
kind: "timeout" | "provider-error" | "malformed-response";
|
|
531
|
+
httpStatus?: number;
|
|
532
|
+
}
|
|
533
|
+
|
|
497
534
|
/** Jev's raw `answers` envelope, or why the request produced none. */
|
|
498
535
|
export interface JevAnswers {
|
|
499
536
|
answers?: Record<string, unknown>;
|
|
500
537
|
failure?: JevAbstainReason;
|
|
538
|
+
diagnostic?: JevFailureDiagnostic;
|
|
501
539
|
elapsedMs: number;
|
|
502
540
|
}
|
|
503
541
|
|
|
@@ -510,7 +548,8 @@ export interface JevAnswers {
|
|
|
510
548
|
* was asked. Nothing beyond the envelope is validated here, so a caller that
|
|
511
549
|
* expects typed answers owns interpreting them. Every failure — no provider
|
|
512
550
|
* template, no credential, an oversized request, a timeout, a rejected call, or
|
|
513
|
-
* malformed JSON — is an absent decision, and this never throws.
|
|
551
|
+
* malformed JSON — is an absent decision, and this never throws. Failure diagnostics
|
|
552
|
+
* contain only a category and optional HTTP status, never provider error text.
|
|
514
553
|
*/
|
|
515
554
|
export async function askJevAnswers(
|
|
516
555
|
ctx: JevRuntime,
|
|
@@ -518,7 +557,11 @@ export async function askJevAnswers(
|
|
|
518
557
|
request: { payload: unknown; maxBytes: number },
|
|
519
558
|
): Promise<JevAnswers> {
|
|
520
559
|
const started = Date.now();
|
|
521
|
-
const abstained = (failure: JevAbstainReason): JevAnswers => ({
|
|
560
|
+
const abstained = (failure: JevAbstainReason, diagnostic?: JevFailureDiagnostic): JevAnswers => ({
|
|
561
|
+
failure,
|
|
562
|
+
...(diagnostic ? { diagnostic } : {}),
|
|
563
|
+
elapsedMs: Date.now() - started,
|
|
564
|
+
});
|
|
522
565
|
|
|
523
566
|
const access = await jevAccess(ctx, connection);
|
|
524
567
|
if ("failure" in access) return abstained(access.failure);
|
|
@@ -539,16 +582,21 @@ export async function askJevAnswers(
|
|
|
539
582
|
{ apiKey: auth.apiKey, headers: auth.headers, maxTokens: JEV_MAX_TOKENS, signal },
|
|
540
583
|
);
|
|
541
584
|
} catch (error) {
|
|
542
|
-
|
|
585
|
+
const failure = failureReason(error);
|
|
586
|
+
return abstained(failure, failureDiagnostic(error, failure));
|
|
543
587
|
}
|
|
544
588
|
// A provider failure or a deadline arrives as a message with an error stop
|
|
545
589
|
// reason rather than as a thrown error.
|
|
546
590
|
if (completion.stopReason === "error" || completion.stopReason === "aborted") {
|
|
547
|
-
|
|
591
|
+
const failure = signal.aborted ? "timeout" : "transport";
|
|
592
|
+
return abstained(
|
|
593
|
+
failure,
|
|
594
|
+
failure === "timeout" ? { kind: "timeout" } : failureDiagnostic(completion.errorMessage, failure),
|
|
595
|
+
);
|
|
548
596
|
}
|
|
549
597
|
|
|
550
598
|
const answers = parseAnswers(responseText(completion));
|
|
551
|
-
if (!answers) return abstained("malformed");
|
|
599
|
+
if (!answers) return abstained("malformed", { kind: "malformed-response" });
|
|
552
600
|
return { answers, elapsedMs: Date.now() - started };
|
|
553
601
|
}
|
|
554
602
|
|