jeopi-cli 16.4.2 → 16.4.4
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 +74 -0
- package/dist/cli.js +3033 -2991
- package/dist/types/advisor/advise-tool.d.ts +5 -1
- package/dist/types/advisor/config.d.ts +4 -3
- package/dist/types/advisor/runtime.d.ts +15 -0
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/update-cli.d.ts +4 -1
- package/dist/types/cli/usage-error.d.ts +4 -0
- package/dist/types/commit/agentic/agent.d.ts +1 -0
- package/dist/types/config/model-registry.d.ts +3 -3
- package/dist/types/config/model-resolver.d.ts +7 -1
- package/dist/types/config/models-config.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +3 -13
- package/dist/types/eval/py/tool-bridge.d.ts +1 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +3 -0
- package/dist/types/extensibility/extensions/runner.d.ts +3 -1
- package/dist/types/extensibility/extensions/types.d.ts +3 -0
- package/dist/types/internal-urls/memory-protocol.d.ts +6 -6
- package/dist/types/internal-urls/types.d.ts +7 -0
- package/dist/types/irc/bus.d.ts +4 -0
- package/dist/types/lsp/deferred-diagnostics.d.ts +11 -0
- package/dist/types/modes/components/move-overlay.d.ts +1 -1
- package/dist/types/modes/components/plan-review-overlay.d.ts +2 -0
- package/dist/types/modes/components/welcome.d.ts +4 -0
- package/dist/types/modes/magic-keyword-boundary.d.ts +9 -0
- package/dist/types/modes/orchestrate.d.ts +1 -1
- package/dist/types/modes/rpc/host-tools.d.ts +2 -0
- package/dist/types/modes/rpc/rpc-mode.d.ts +27 -7
- package/dist/types/modes/ultrathink.d.ts +1 -1
- package/dist/types/modes/utils/transcript-render-helpers.d.ts +9 -0
- package/dist/types/modes/workflow.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +6 -0
- package/dist/types/session/auth-broker-config.d.ts +2 -2
- package/dist/types/tools/bash-skill-urls.d.ts +3 -2
- package/dist/types/tools/browser/cmux/cmux-tab.d.ts +3 -1
- package/dist/types/tools/browser/cmux/rpc.d.ts +20 -0
- package/dist/types/tools/browser/launch.d.ts +1 -0
- package/dist/types/tools/browser/run-cancellation.d.ts +17 -2
- package/dist/types/tools/browser/run-output.d.ts +25 -0
- package/dist/types/tools/browser/tab-worker.d.ts +22 -0
- package/dist/types/utils/changelog.d.ts +35 -1
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +13 -0
- package/src/advisor/__tests__/config.test.ts +36 -0
- package/src/advisor/advise-tool.ts +6 -1
- package/src/advisor/config.ts +11 -8
- package/src/advisor/runtime.ts +127 -0
- package/src/autolearn/controller.ts +15 -3
- package/src/cli/args.ts +12 -0
- package/src/cli/flag-tables.ts +21 -7
- package/src/cli/update-cli.ts +63 -13
- package/src/cli/usage-error.ts +7 -0
- package/src/commands/acp.ts +11 -2
- package/src/commands/launch.ts +12 -3
- package/src/commit/agentic/agent.ts +4 -0
- package/src/commit/agentic/index.ts +46 -21
- package/src/config/config-file.ts +30 -12
- package/src/config/model-registry.ts +7 -7
- package/src/config/model-resolver.ts +24 -6
- package/src/config/models-config.ts +1 -1
- package/src/config/settings-schema.ts +3 -15
- package/src/edit/index.ts +10 -76
- package/src/edit/renderer.ts +13 -10
- package/src/eval/__tests__/agent-bridge.test.ts +19 -14
- package/src/eval/__tests__/bridge-timeout.test.ts +60 -0
- package/src/eval/executor-base.ts +84 -7
- package/src/eval/js/shared/rewrite-imports.ts +31 -13
- package/src/eval/py/tool-bridge.ts +13 -14
- package/src/exec/non-interactive-env.ts +0 -1
- package/src/export/ttsr.ts +0 -3
- package/src/extensibility/custom-tools/loader.ts +70 -37
- package/src/extensibility/custom-tools/types.ts +3 -0
- package/src/extensibility/extensions/runner.ts +3 -0
- package/src/extensibility/extensions/types.ts +3 -0
- package/src/extensibility/plugins/legacy-pi-bundled-keys.ts +9 -1
- package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +30 -3
- package/src/extensibility/plugins/manager.ts +21 -0
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/internal-urls/memory-protocol.ts +13 -8
- package/src/internal-urls/skill-protocol.ts +1 -1
- package/src/internal-urls/types.ts +7 -0
- package/src/irc/bus.ts +48 -18
- package/src/lsp/client.ts +7 -1
- package/src/lsp/deferred-diagnostics.ts +66 -0
- package/src/main.ts +13 -13
- package/src/mcp/tool-bridge.ts +57 -6
- package/src/mcp/transports/stdio.test.ts +45 -1
- package/src/mcp/transports/stdio.ts +6 -3
- package/src/modes/acp/acp-agent.ts +73 -2
- package/src/modes/acp/acp-event-mapper.ts +5 -0
- package/src/modes/acp/acp-mode.ts +11 -0
- package/src/modes/components/__tests__/move-overlay.test.ts +16 -1
- package/src/modes/components/advisor-config.ts +15 -7
- package/src/modes/components/chat-transcript-builder.ts +31 -11
- package/src/modes/components/move-overlay.ts +2 -3
- package/src/modes/components/plan-review-overlay.ts +7 -0
- package/src/modes/components/tips.txt +1 -1
- package/src/modes/components/welcome.ts +13 -14
- package/src/modes/controllers/command-controller.ts +8 -10
- package/src/modes/controllers/event-controller.ts +58 -42
- package/src/modes/controllers/input-controller.ts +9 -6
- package/src/modes/controllers/selector-controller.ts +3 -2
- package/src/modes/controllers/tan-command-controller.ts +40 -1
- package/src/modes/interactive-mode.ts +13 -0
- package/src/modes/magic-keyword-boundary.ts +23 -0
- package/src/modes/orchestrate.ts +6 -5
- package/src/modes/print-mode.ts +9 -0
- package/src/modes/rpc/host-tools.ts +13 -0
- package/src/modes/rpc/rpc-mode.ts +130 -56
- package/src/modes/ultrathink.ts +6 -5
- package/src/modes/utils/transcript-render-helpers.ts +44 -0
- package/src/modes/utils/ui-helpers.ts +16 -1
- package/src/modes/workflow.ts +5 -4
- package/src/prompts/system/eager-task.md +2 -2
- package/src/prompts/system/tan-context-switch.md +17 -0
- package/src/prompts/tools/browser.md +5 -5
- package/src/sdk.ts +3 -0
- package/src/session/agent-session.ts +267 -36
- package/src/session/auth-broker-config.ts +4 -4
- package/src/session/snapcompact-inline.ts +3 -19
- package/src/session/streaming-output.ts +40 -12
- package/src/slash-commands/builtin-registry.ts +8 -8
- package/src/task/executor.ts +8 -0
- package/src/task/render.ts +51 -44
- package/src/tools/bash-skill-urls.ts +7 -4
- package/src/tools/bash.ts +30 -33
- package/src/tools/browser/cmux/cmux-tab.ts +49 -50
- package/src/tools/browser/cmux/rpc.ts +50 -0
- package/src/tools/browser/launch.ts +8 -4
- package/src/tools/browser/run-cancellation.ts +44 -5
- package/src/tools/browser/run-output.ts +76 -0
- package/src/tools/browser/tab-worker.ts +280 -133
- package/src/tools/eval-render.ts +16 -14
- package/src/tools/glob.ts +20 -6
- package/src/tools/grep.ts +1 -0
- package/src/tools/image-gen.ts +3 -4
- package/src/tools/irc.ts +27 -13
- package/src/tools/path-utils.ts +6 -1
- package/src/tools/read.ts +14 -7
- package/src/tools/write.ts +22 -5
- package/src/tools/yield.ts +29 -1
- package/src/utils/changelog.ts +106 -9
- package/src/utils/git.ts +0 -1
- package/src/utils/title-generator.ts +72 -7
- package/src/web/search/index.ts +14 -6
- package/src/web/search/providers/perplexity.ts +18 -2
- package/dist/types/tools/bash-command-fixup.d.ts +0 -11
- package/src/tools/bash-command-fixup.ts +0 -37
|
@@ -13,6 +13,7 @@ export declare const DEFAULT_VIEWPORT: {
|
|
|
13
13
|
export declare const BROWSER_PROTOCOL_TIMEOUT_MS = 60000;
|
|
14
14
|
export declare function loadPuppeteer(): Promise<typeof Puppeteer>;
|
|
15
15
|
export declare function loadPuppeteerInWorker(safeDir: string): Promise<typeof Puppeteer>;
|
|
16
|
+
export declare function ensureChromiumExecutable(): Promise<string | undefined>;
|
|
16
17
|
export interface LaunchHeadlessOptions {
|
|
17
18
|
headless: boolean;
|
|
18
19
|
viewport?: {
|
|
@@ -1,4 +1,19 @@
|
|
|
1
|
-
/**
|
|
2
|
-
export
|
|
1
|
+
/** Options for the predicate form of the run-scoped `wait()` helper. */
|
|
2
|
+
export interface WaitPredicateOptions {
|
|
3
|
+
/** Max time to poll before failing, in ms (default 30_000). */
|
|
4
|
+
timeout?: number;
|
|
5
|
+
/** Poll interval in ms (default 100, floor 10). */
|
|
6
|
+
interval?: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Run-scoped `wait()` helper for evaluated browser code, honoring the owning run's
|
|
10
|
+
* cancellation signal.
|
|
11
|
+
*
|
|
12
|
+
* - `wait(ms)` sleeps for `ms` milliseconds.
|
|
13
|
+
* - `wait(fn, { timeout?, interval? })` polls `fn` (sync or async) until it returns a
|
|
14
|
+
* truthy value and resolves with that value; throws a named `ToolError` on timeout
|
|
15
|
+
* instead of stalling into the whole-cell deadline. Predicate errors propagate.
|
|
16
|
+
*/
|
|
17
|
+
export declare function waitForBrowserRun(msOrPredicate: number | (() => unknown), signal: AbortSignal, opts?: WaitPredicateOptions): Promise<unknown>;
|
|
3
18
|
/** Binds a long-lived browser facade to one evaluated run's abort signal. */
|
|
4
19
|
export declare function bindBrowserRunFacade<T extends object>(target: T, signal: AbortSignal): T;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ImageContent, TextContent } from "jeopi-ai";
|
|
2
|
+
import type { JsDisplayOutput } from "../../eval/js/shared/types";
|
|
3
|
+
/**
|
|
4
|
+
* Accumulates a browser run's result entries: explicit `display()` payloads,
|
|
5
|
+
* screenshot captions/images, and buffered stream text (`console.*`, `print`,
|
|
6
|
+
* `display()` of strings/primitives — `JsRuntime.displayValue` emits those via
|
|
7
|
+
* `onText`). Stream text is buffered and flushed as one entry before the next
|
|
8
|
+
* display/screenshot (and on `finish()`) so it reaches the tool result in
|
|
9
|
+
* order instead of vanishing into the debug log.
|
|
10
|
+
*/
|
|
11
|
+
export declare class RunOutput {
|
|
12
|
+
#private;
|
|
13
|
+
/** Buffer a stream-text chunk; it joins the entries at the next push or on finish(). */
|
|
14
|
+
pushText(chunk: string): void;
|
|
15
|
+
/** Append a `display()` payload (image/json/status), flushing buffered text first. */
|
|
16
|
+
pushDisplay(output: JsDisplayOutput): void;
|
|
17
|
+
/** Append a pre-built entry (e.g. a screenshot caption/image), flushing buffered text first. */
|
|
18
|
+
push(entry: TextContent | ImageContent): void;
|
|
19
|
+
/** Flush any remaining stream text and return the ordered entries. */
|
|
20
|
+
finish(): Array<TextContent | ImageContent>;
|
|
21
|
+
}
|
|
22
|
+
/** JSON.stringify that never throws (cycles/BigInt → String(value)). */
|
|
23
|
+
export declare function safeJsonStringify(value: unknown): string;
|
|
24
|
+
/** Pass a return value across the run boundary: structured-cloneable as-is, else JSON round-trip, else String. */
|
|
25
|
+
export declare function cloneSafe(value: unknown): unknown;
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import type { HTMLElement } from "linkedom";
|
|
2
2
|
import type { ElementHandle, ImageFormat } from "puppeteer-core";
|
|
3
3
|
import type { Transport } from "./tab-protocol";
|
|
4
|
+
declare module "puppeteer-core" {
|
|
5
|
+
interface Frame {
|
|
6
|
+
/** Puppeteer's main JavaScript realm, retained by our pinned runtime patch. */
|
|
7
|
+
mainRealm(): Realm;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
4
10
|
declare global {
|
|
5
11
|
interface Element extends HTMLElement {
|
|
6
12
|
}
|
|
@@ -37,6 +43,22 @@ interface ScreenshotOptions {
|
|
|
37
43
|
silent?: boolean;
|
|
38
44
|
}
|
|
39
45
|
export declare function normalizeSelector(selector: string): string;
|
|
46
|
+
/** ElementHandle enriched with the `fill()` the tool docs promise on handles from `tab.id()`/`tab.ref()`/`tab.waitFor()`. */
|
|
47
|
+
export type ActionableHandle = ElementHandle & {
|
|
48
|
+
fill(value: string): Promise<void>;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Attach `fill()` to a puppeteer ElementHandle before handing it to user code.
|
|
52
|
+
* Puppeteer handles expose `type()` but no `fill()`; the semantics mirror the
|
|
53
|
+
* selector-based `tab.fill()`: focus, clear any existing value, then type.
|
|
54
|
+
*/
|
|
55
|
+
export declare function toActionableHandle(handle: ElementHandle): ActionableHandle;
|
|
56
|
+
/**
|
|
57
|
+
* Hint appended to a selector op's fail-fast timeout, given the selector's current
|
|
58
|
+
* match count: a missing element (consent wall, wrong page) reads differently from
|
|
59
|
+
* a present-but-unactionable one.
|
|
60
|
+
*/
|
|
61
|
+
export declare function formatSelectorMatchHint(count: number): string;
|
|
40
62
|
export interface InflightOp {
|
|
41
63
|
label: string;
|
|
42
64
|
startedAt: number;
|
|
@@ -4,6 +4,24 @@ export interface ChangelogEntry {
|
|
|
4
4
|
patch: number;
|
|
5
5
|
content: string;
|
|
6
6
|
}
|
|
7
|
+
/** Number of changelog releases shown by automatic and default recent views. */
|
|
8
|
+
export declare const RECENT_CHANGELOG_ENTRY_LIMIT = 3;
|
|
9
|
+
/** Maximum Markdown source bytes allowed in automatic startup release notes. */
|
|
10
|
+
export declare const STARTUP_CHANGELOG_MAX_BYTES: number;
|
|
11
|
+
/** Hint appended when automatic startup release notes are truncated. */
|
|
12
|
+
export declare const STARTUP_CHANGELOG_FULL_HINT = "Use `/changelog full` to view the complete changelog.";
|
|
13
|
+
/** Markdown generated from selected changelog entries and whether it hit a size cap. */
|
|
14
|
+
export interface RenderedChangelog {
|
|
15
|
+
markdown: string;
|
|
16
|
+
truncated: boolean;
|
|
17
|
+
}
|
|
18
|
+
/** Automatic startup changelog decision, including whether the marker should advance. */
|
|
19
|
+
export interface StartupChangelogSelection {
|
|
20
|
+
markdown: string | undefined;
|
|
21
|
+
persistCurrentVersion: boolean;
|
|
22
|
+
truncated: boolean;
|
|
23
|
+
selectedEntries: number;
|
|
24
|
+
}
|
|
7
25
|
/**
|
|
8
26
|
* Parse changelog entries from the file at `changelogPath`. Scans for `## [x.y.z]`
|
|
9
27
|
* headings and collects each block until the next heading or EOF.
|
|
@@ -19,9 +37,25 @@ export declare function parseChangelog(changelogPath: string | undefined): Promi
|
|
|
19
37
|
*/
|
|
20
38
|
export declare function compareVersions(v1: ChangelogEntry, v2: ChangelogEntry): number;
|
|
21
39
|
/**
|
|
22
|
-
*
|
|
40
|
+
* Parse an omp changelog marker version into comparable parts.
|
|
41
|
+
*/
|
|
42
|
+
export declare function parseChangelogVersion(version: string | undefined): ChangelogEntry | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* Get entries newer than lastVersion.
|
|
23
45
|
*/
|
|
24
46
|
export declare function getNewEntries(entries: ChangelogEntry[], lastVersion: string): ChangelogEntry[];
|
|
47
|
+
/**
|
|
48
|
+
* Render changelog entries oldest-first by default and optionally cap the Markdown source size.
|
|
49
|
+
*/
|
|
50
|
+
export declare function renderChangelogEntries(entries: ChangelogEntry[], options?: {
|
|
51
|
+
maxBytes?: number;
|
|
52
|
+
truncationHint?: string;
|
|
53
|
+
oldestFirst?: boolean;
|
|
54
|
+
}): RenderedChangelog;
|
|
55
|
+
/**
|
|
56
|
+
* Select bounded release notes for interactive startup.
|
|
57
|
+
*/
|
|
58
|
+
export declare function selectStartupChangelog(entries: ChangelogEntry[], lastVersion: string | undefined, currentVersion: string): StartupChangelogSelection;
|
|
25
59
|
export { getChangelogPath } from "../config";
|
|
26
60
|
/**
|
|
27
61
|
* Last jeopi version whose changelog the user has seen. Stored as a plain-text
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "jeopi-cli",
|
|
4
|
-
"version": "16.4.
|
|
4
|
+
"version": "16.4.4",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://github.com/akillness/jeopi",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -57,17 +57,17 @@
|
|
|
57
57
|
"@agentclientprotocol/sdk": "0.25.0",
|
|
58
58
|
"@babel/parser": "^7.29.7",
|
|
59
59
|
"@mozilla/readability": "^0.6.0",
|
|
60
|
-
"jeopi-hashline": "16.4.
|
|
61
|
-
"jeopi-stats": "16.4.
|
|
62
|
-
"jeopi-agent-core": "16.4.
|
|
63
|
-
"jeopi-ai": "16.4.
|
|
64
|
-
"jeopi-catalog": "16.4.
|
|
65
|
-
"jeopi-mnemopi": "16.4.
|
|
66
|
-
"jeopi-natives": "16.4.
|
|
67
|
-
"jeopi-tui": "16.4.
|
|
68
|
-
"jeopi-utils": "16.4.
|
|
69
|
-
"jeopi-wire": "16.4.
|
|
70
|
-
"jeopi-snapcompact": "16.4.
|
|
60
|
+
"jeopi-hashline": "16.4.4",
|
|
61
|
+
"jeopi-stats": "16.4.4",
|
|
62
|
+
"jeopi-agent-core": "16.4.4",
|
|
63
|
+
"jeopi-ai": "16.4.4",
|
|
64
|
+
"jeopi-catalog": "16.4.4",
|
|
65
|
+
"jeopi-mnemopi": "16.4.4",
|
|
66
|
+
"jeopi-natives": "16.4.4",
|
|
67
|
+
"jeopi-tui": "16.4.4",
|
|
68
|
+
"jeopi-utils": "16.4.4",
|
|
69
|
+
"jeopi-wire": "16.4.4",
|
|
70
|
+
"jeopi-snapcompact": "16.4.4",
|
|
71
71
|
"@opentelemetry/api": "^1.9.1",
|
|
72
72
|
"@opentelemetry/context-async-hooks": "^2.7.1",
|
|
73
73
|
"@opentelemetry/exporter-trace-otlp-proto": "^0.218.0",
|
|
@@ -1839,6 +1839,19 @@ describe("advisor", () => {
|
|
|
1839
1839
|
expect(text).toContain("read, grep, glob (default)");
|
|
1840
1840
|
});
|
|
1841
1841
|
|
|
1842
|
+
it("renders an explicit no-tools advisor distinctly from the omitted default", async () => {
|
|
1843
|
+
const uiTheme = await getThemeByName("dark");
|
|
1844
|
+
if (!uiTheme) throw new Error("theme unavailable");
|
|
1845
|
+
setThemeInstance(uiTheme);
|
|
1846
|
+
const overlay = make({
|
|
1847
|
+
advisors: [{ name: "Blank", tools: [] }],
|
|
1848
|
+
});
|
|
1849
|
+
|
|
1850
|
+
const text = strip(overlay.render(200));
|
|
1851
|
+
expect(text.toLowerCase()).toContain("no tools");
|
|
1852
|
+
expect(text).not.toContain("read, grep, glob (default)");
|
|
1853
|
+
});
|
|
1854
|
+
|
|
1842
1855
|
it("moves the preview with keyboard selection and preserves an explicit tool set", async () => {
|
|
1843
1856
|
const uiTheme = await getThemeByName("dark");
|
|
1844
1857
|
if (!uiTheme) throw new Error("theme unavailable");
|
|
@@ -55,6 +55,27 @@ describe("discoverAdvisorConfigs", () => {
|
|
|
55
55
|
expect(sharedInstructions).toBe("Shared baseline for all advisors.");
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
+
it("distinguishes omitted tools, explicit no-tools, and invalid-only lists", async () => {
|
|
59
|
+
const yaml = [
|
|
60
|
+
"advisors:",
|
|
61
|
+
" - name: No Tools",
|
|
62
|
+
" tools: []",
|
|
63
|
+
" - name: Default Tools",
|
|
64
|
+
" - name: Invalid Only",
|
|
65
|
+
" tools: [reed]",
|
|
66
|
+
].join("\n");
|
|
67
|
+
await Bun.write(path.join(tmp, "WATCHDOG.yml"), yaml);
|
|
68
|
+
|
|
69
|
+
const { advisors } = await discoverAdvisorConfigs(tmp, agentDir);
|
|
70
|
+
const noTools = advisors.find(a => a.name === "No Tools");
|
|
71
|
+
const defaultTools = advisors.find(a => a.name === "Default Tools");
|
|
72
|
+
const invalidOnly = advisors.find(a => a.name === "Invalid Only");
|
|
73
|
+
|
|
74
|
+
expect(noTools?.tools).toEqual([]);
|
|
75
|
+
expect(defaultTools?.tools).toBeUndefined();
|
|
76
|
+
expect(invalidOnly?.tools).toBeUndefined();
|
|
77
|
+
});
|
|
78
|
+
|
|
58
79
|
it("ignores a malformed YAML file without throwing", async () => {
|
|
59
80
|
await Bun.write(path.join(tmp, "WATCHDOG.yml"), "advisors: [unclosed bracket");
|
|
60
81
|
const result = await discoverAdvisorConfigs(tmp, agentDir);
|
|
@@ -122,6 +143,21 @@ describe("WATCHDOG.yml file round-trip", () => {
|
|
|
122
143
|
expect(sharedInstructions).toContain("Shared baseline.");
|
|
123
144
|
});
|
|
124
145
|
|
|
146
|
+
it("round-trips an explicit empty tools list without collapsing it into the default", async () => {
|
|
147
|
+
const file = path.join(tmp, "WATCHDOG.yml");
|
|
148
|
+
const explicitNoToolsDoc: WatchdogConfigDoc = {
|
|
149
|
+
advisors: [{ name: "No Tools", tools: [] }, { name: "Default Tools" }],
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
await saveWatchdogConfigFile(file, explicitNoToolsDoc);
|
|
153
|
+
const serializedDoc = await loadWatchdogConfigFile(file);
|
|
154
|
+
expect(serializedDoc).toEqual(explicitNoToolsDoc);
|
|
155
|
+
|
|
156
|
+
const { advisors } = await discoverAdvisorConfigs(tmp, tmp);
|
|
157
|
+
expect(advisors.find(a => a.name === "No Tools")?.tools).toEqual([]);
|
|
158
|
+
expect(advisors.find(a => a.name === "Default Tools")?.tools).toBeUndefined();
|
|
159
|
+
});
|
|
160
|
+
|
|
125
161
|
it("removes the file when the doc is empty so legacy discovery resumes", async () => {
|
|
126
162
|
const file = path.join(tmp, "WATCHDOG.yml");
|
|
127
163
|
await saveWatchdogConfigFile(file, doc);
|
|
@@ -94,6 +94,9 @@ export function isAdvisorInterruptImmuneTurnActive(opts: {
|
|
|
94
94
|
* - An interrupting `concern`/`blocker` is normally steered into the agent: into
|
|
95
95
|
* the live turn while one is streaming, or (when idle) a triggered turn so the
|
|
96
96
|
* advice is acted on immediately.
|
|
97
|
+
* - If the primary tail is already a terminal text answer and there is no queued
|
|
98
|
+
* work, late interrupting advice is preserved as a visible card instead of
|
|
99
|
+
* waking the primary to restate completion.
|
|
97
100
|
* - After a deliberate user interrupt (`autoResumeSuppressed`) the advisor must
|
|
98
101
|
* not auto-resume the stopped run. While the agent is idle — or still tearing
|
|
99
102
|
* the interrupted turn down (`aborting`) — the note is preserved as a visible
|
|
@@ -103,17 +106,19 @@ export function isAdvisorInterruptImmuneTurnActive(opts: {
|
|
|
103
106
|
* run instead strands it (it never reaches the running agent) and the withheld
|
|
104
107
|
* notes dump as one burst at the next user prompt — the bug this guards.
|
|
105
108
|
* - During the post-interrupt immune-turn window, further `concern`/`blocker`
|
|
106
|
-
* notes are downgraded to asides;
|
|
109
|
+
* notes are downgraded to asides; preservation still wins.
|
|
107
110
|
*/
|
|
108
111
|
export function resolveAdvisorDeliveryChannel(opts: {
|
|
109
112
|
severity: AdvisorSeverity | undefined;
|
|
110
113
|
autoResumeSuppressed: boolean;
|
|
111
114
|
streaming: boolean;
|
|
112
115
|
aborting: boolean;
|
|
116
|
+
terminalAnswerNoQueuedWork?: boolean;
|
|
113
117
|
interruptImmuneTurnActive?: boolean;
|
|
114
118
|
}): AdvisorDeliveryChannel {
|
|
115
119
|
if (!isInterruptingSeverity(opts.severity)) return "aside";
|
|
116
120
|
if (opts.autoResumeSuppressed && (opts.aborting || !opts.streaming)) return "preserve";
|
|
121
|
+
if (opts.terminalAnswerNoQueuedWork && !opts.streaming && !opts.aborting) return "preserve";
|
|
117
122
|
if (opts.interruptImmuneTurnActive) return "aside";
|
|
118
123
|
return "steer";
|
|
119
124
|
}
|
package/src/advisor/config.ts
CHANGED
|
@@ -12,9 +12,10 @@ import { collectConfigCandidates } from "./watchdog";
|
|
|
12
12
|
* with an optional `:level` thinking suffix (e.g. `x-ai/grok-code-fast:high`),
|
|
13
13
|
* resolved exactly like any other model override; `tools` is a subset of
|
|
14
14
|
* `BUILTIN_TOOL_NAMES` — any built-in name, including mutating tools such as
|
|
15
|
-
* `edit`/`write`/`bash` (the advisor is a full agent). Omitted
|
|
16
|
-
*
|
|
17
|
-
* advisor's specialization, appended to the shared
|
|
15
|
+
* `edit`/`write`/`bash` (the advisor is a full agent). Omitted falls back to
|
|
16
|
+
* the default `read`/`grep`/`glob` subset; an explicit empty list grants no
|
|
17
|
+
* tools. `instructions` is the advisor's specialization, appended to the shared
|
|
18
|
+
* baseline.
|
|
18
19
|
*/
|
|
19
20
|
export interface AdvisorConfig {
|
|
20
21
|
name: string;
|
|
@@ -65,11 +66,13 @@ const KNOWN_TOOL_NAMES = new Set<string>(BUILTIN_TOOL_NAMES);
|
|
|
65
66
|
/**
|
|
66
67
|
* Keep only valid tool names from an advisor's `tools` list, dropping unknowns
|
|
67
68
|
* with a warning. The advisor is a full agent, so any built tool may be granted;
|
|
68
|
-
* the runtime further filters to what's actually available this session.
|
|
69
|
-
*
|
|
69
|
+
* the runtime further filters to what's actually available this session.
|
|
70
|
+
* `undefined` means "use the default subset" (read/grep/glob); only an explicit
|
|
71
|
+
* raw empty list means "no tools".
|
|
70
72
|
*/
|
|
71
73
|
function filterAdvisorTools(tools: string[] | undefined, sourcePath: string): string[] | undefined {
|
|
72
|
-
if (
|
|
74
|
+
if (tools === undefined) return undefined;
|
|
75
|
+
if (tools.length === 0) return [];
|
|
73
76
|
// Normalize legacy aliases (search→grep, find→glob) and dedupe before validating.
|
|
74
77
|
const filtered = normalizeToolNames(tools).filter(name => {
|
|
75
78
|
if (KNOWN_TOOL_NAMES.has(name)) return true;
|
|
@@ -210,7 +213,7 @@ export async function loadWatchdogConfigFile(filePath: string): Promise<Watchdog
|
|
|
210
213
|
advisors: (result.advisors ?? []).map(a => ({
|
|
211
214
|
name: a.name,
|
|
212
215
|
model: a.model?.trim() || undefined,
|
|
213
|
-
tools: a.tools
|
|
216
|
+
tools: a.tools === undefined ? undefined : [...a.tools],
|
|
214
217
|
instructions: a.instructions?.trim() ? a.instructions : undefined,
|
|
215
218
|
})),
|
|
216
219
|
};
|
|
@@ -229,7 +232,7 @@ export function serializeWatchdogConfig(doc: WatchdogConfigDoc): string {
|
|
|
229
232
|
out.advisors = doc.advisors.map(a => {
|
|
230
233
|
const entry: AdvisorConfig = { name: a.name };
|
|
231
234
|
if (a.model?.trim()) entry.model = a.model;
|
|
232
|
-
if (a.tools
|
|
235
|
+
if (a.tools !== undefined) entry.tools = [...a.tools];
|
|
233
236
|
if (a.instructions?.trim()) entry.instructions = a.instructions;
|
|
234
237
|
return entry;
|
|
235
238
|
});
|
package/src/advisor/runtime.ts
CHANGED
|
@@ -65,6 +65,94 @@ export interface AdvisorRuntimeOptions {
|
|
|
65
65
|
includeThinking?: boolean;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
const ADVISOR_QUARANTINE_PREFIX = "Advisor response quarantined";
|
|
69
|
+
|
|
70
|
+
/** Signals that an advisor response was discarded before it could become model-visible context. */
|
|
71
|
+
export class AdvisorOutputQuarantinedError extends Error {
|
|
72
|
+
constructor(message: string) {
|
|
73
|
+
super(message);
|
|
74
|
+
this.name = "AdvisorOutputQuarantinedError";
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface AdvisorOutputHazard {
|
|
79
|
+
label: string;
|
|
80
|
+
pattern: RegExp;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const ADVISOR_OUTPUT_ONLY_HAZARDS: readonly AdvisorOutputHazard[] = [
|
|
84
|
+
{ label: "account-deletion claim", pattern: /\buser\b.{0,80}\b(?:deleted|erased)\b.{0,80}\baccount\b/i },
|
|
85
|
+
{
|
|
86
|
+
label: "instruction override",
|
|
87
|
+
pattern: /\bignore\s+(?:all\s+)?(?:prior|previous|earlier)\s+(?:user\s+)?instructions\b/i,
|
|
88
|
+
},
|
|
89
|
+
{ label: "destructive shell command", pattern: /\brm\s+-[a-z]*r[a-z]*f[a-z]*(?:\s|$)/i },
|
|
90
|
+
{ label: "denial instruction", pattern: /\bdeny\s+(?:this|it|the\s+request)\s+if\s+(?:asked|questioned)\b/i },
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Replaces an advisor assistant turn that requested unavailable tools or generated
|
|
95
|
+
* output-only destructive directives with a sanitized error before dispatch.
|
|
96
|
+
*/
|
|
97
|
+
export function quarantineAdvisorUnsafeOutput(
|
|
98
|
+
message: AssistantMessage,
|
|
99
|
+
availableToolNames: ReadonlySet<string>,
|
|
100
|
+
sourceText = "",
|
|
101
|
+
): string | undefined {
|
|
102
|
+
const reasons: string[] = [];
|
|
103
|
+
const unavailableToolNames = new Set<string>();
|
|
104
|
+
const generatedParts: string[] = [];
|
|
105
|
+
for (const block of message.content) {
|
|
106
|
+
if (block.type === "toolCall" && !availableToolNames.has(block.name)) unavailableToolNames.add(block.name);
|
|
107
|
+
if (block.type === "toolCall" && block.name === "advise" && typeof block.arguments.note === "string") {
|
|
108
|
+
generatedParts.push(block.arguments.note);
|
|
109
|
+
}
|
|
110
|
+
if (block.type === "text") generatedParts.push(block.text);
|
|
111
|
+
}
|
|
112
|
+
if (unavailableToolNames.size > 0) {
|
|
113
|
+
const names = [...unavailableToolNames].sort();
|
|
114
|
+
const toolLabel = names.length === 1 ? "tool" : "tools";
|
|
115
|
+
reasons.push(`requested unavailable ${toolLabel} ${names.join(", ")}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const generatedText = generatedParts.join("\n");
|
|
119
|
+
if (generatedText) {
|
|
120
|
+
const labels: string[] = [];
|
|
121
|
+
for (const hazard of ADVISOR_OUTPUT_ONLY_HAZARDS) {
|
|
122
|
+
if (hazard.pattern.test(generatedText) && !hazard.pattern.test(sourceText)) labels.push(hazard.label);
|
|
123
|
+
}
|
|
124
|
+
if (labels.includes("destructive shell command") || labels.length >= 3) {
|
|
125
|
+
reasons.push(`generated output-only destructive directives: ${labels.join(", ")}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (reasons.length === 0) return undefined;
|
|
130
|
+
|
|
131
|
+
const messageText = `${ADVISOR_QUARANTINE_PREFIX}: ${reasons.join("; ")}`;
|
|
132
|
+
message.content = [{ type: "text", text: messageText }];
|
|
133
|
+
message.stopReason = "error";
|
|
134
|
+
message.stopDetails = undefined;
|
|
135
|
+
message.providerPayload = undefined;
|
|
136
|
+
message.errorMessage = messageText;
|
|
137
|
+
return messageText;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Builds the provenance text used to decide whether hazardous advisor output was
|
|
142
|
+
* generated by the advisor or came from model-visible primary/tool context.
|
|
143
|
+
*/
|
|
144
|
+
export function buildAdvisorQuarantineSourceText(currentInput: string, messages: readonly AgentMessage[]): string {
|
|
145
|
+
const parts: string[] = [];
|
|
146
|
+
if (currentInput) parts.push(currentInput);
|
|
147
|
+
for (const message of messages) {
|
|
148
|
+
if (message.role !== "toolResult") continue;
|
|
149
|
+
for (const block of message.content) {
|
|
150
|
+
if (block.type === "text") parts.push(block.text);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return parts.join("\n");
|
|
154
|
+
}
|
|
155
|
+
|
|
68
156
|
/**
|
|
69
157
|
* Match the stable `Refusal` / `Refusal (…)` message prefix pi-ai emits for a
|
|
70
158
|
* provider classifier refusal (`stop_details.type === "refusal"` mapped in
|
|
@@ -384,10 +472,18 @@ export class AdvisorRuntime {
|
|
|
384
472
|
// successful empty cycle.
|
|
385
473
|
const promptError = this.agent.state.error;
|
|
386
474
|
if (promptError) throw new Error(promptError);
|
|
475
|
+
const emptyResponseError = getAdvisorEmptyResponseError(
|
|
476
|
+
this.agent.state.messages.slice(messageSnapshot),
|
|
477
|
+
);
|
|
478
|
+
if (emptyResponseError) throw emptyResponseError;
|
|
387
479
|
success = true;
|
|
388
480
|
this.#consecutiveFailures = 0;
|
|
389
481
|
this.#failureNotified = false;
|
|
390
482
|
} catch (err) {
|
|
483
|
+
if (err instanceof AdvisorOutputQuarantinedError) {
|
|
484
|
+
this.#resetAdvisorContext(true, true);
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
391
487
|
// reset()/dispose() aborts the in-flight prompt; the rejection is the
|
|
392
488
|
// reset itself, not a transient advisor failure. Drop the stale batch
|
|
393
489
|
// (reset already cleared #pending and rewound the cursor) instead of
|
|
@@ -457,6 +553,37 @@ export class AdvisorRuntime {
|
|
|
457
553
|
}
|
|
458
554
|
|
|
459
555
|
type TextualContent = string | readonly (TextContent | ImageContent)[];
|
|
556
|
+
function getAdvisorEmptyResponseError(messages: readonly AgentMessage[]): Error | undefined {
|
|
557
|
+
let sawAssistant = false;
|
|
558
|
+
for (const message of messages) {
|
|
559
|
+
if (message.role !== "assistant") continue;
|
|
560
|
+
sawAssistant = true;
|
|
561
|
+
if (message.stopReason !== "stop") return undefined;
|
|
562
|
+
if (hasAdvisorResponseContent(message)) return undefined;
|
|
563
|
+
}
|
|
564
|
+
if (sawAssistant) return new Error("Advisor turn returned an empty stop response without advice");
|
|
565
|
+
if (messages.length > 0) return new Error("Advisor turn ended without an assistant response");
|
|
566
|
+
return undefined;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function hasAdvisorResponseContent(message: AssistantMessage): boolean {
|
|
570
|
+
return message.content.some(block => {
|
|
571
|
+
switch (block.type) {
|
|
572
|
+
case "text":
|
|
573
|
+
return block.text.trim().length > 0;
|
|
574
|
+
case "thinking":
|
|
575
|
+
return block.thinking.trim().length > 0;
|
|
576
|
+
case "redactedThinking":
|
|
577
|
+
return block.data.length > 0;
|
|
578
|
+
case "toolCall":
|
|
579
|
+
return block.name.trim().length > 0;
|
|
580
|
+
case "fallback":
|
|
581
|
+
return false;
|
|
582
|
+
default:
|
|
583
|
+
return false;
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
}
|
|
460
587
|
|
|
461
588
|
function obfuscateTextualContent(obfuscator: SecretObfuscator, content: TextualContent): TextualContent {
|
|
462
589
|
if (typeof content === "string") return obfuscator.obfuscate(content);
|
|
@@ -84,11 +84,11 @@ export class AutoLearnController {
|
|
|
84
84
|
return;
|
|
85
85
|
}
|
|
86
86
|
if (event.type === "agent_end") {
|
|
87
|
-
this.#onAgentEnd();
|
|
87
|
+
this.#onAgentEnd(event);
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
#onAgentEnd(): void {
|
|
91
|
+
#onAgentEnd(event: Extract<AgentSessionEvent, { type: "agent_end" }>): void {
|
|
92
92
|
// Snapshot and reset every turn: the counter describes only the
|
|
93
93
|
// just-finished turn, so below-threshold, disabled, and plan-mode stops
|
|
94
94
|
// must not let tool calls accumulate into a later turn.
|
|
@@ -107,6 +107,18 @@ export class AutoLearnController {
|
|
|
107
107
|
this.#suppressNext = false;
|
|
108
108
|
return;
|
|
109
109
|
}
|
|
110
|
+
// Never nudge a turn that ended in an abort (ESC, cancel, etc.). The
|
|
111
|
+
// abort flag on the session is unreliable by the time agent_end is
|
|
112
|
+
// deferred to subscribers; read stopReason from the event messages.
|
|
113
|
+
for (let i = event.messages.length - 1; i >= 0; i--) {
|
|
114
|
+
const message = event.messages[i];
|
|
115
|
+
if (message && typeof message === "object" && "role" in message && message.role === "assistant") {
|
|
116
|
+
if ("stopReason" in message && message.stopReason === "aborted") {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
110
122
|
// Honor a live opt-out: the subscription outlives the setting, so re-check
|
|
111
123
|
// the current flag rather than trusting install-time state.
|
|
112
124
|
if (!this.#settings.get("autolearn.enabled")) return;
|
|
@@ -148,7 +160,7 @@ export class AutoLearnController {
|
|
|
148
160
|
display: false,
|
|
149
161
|
attribution: "user",
|
|
150
162
|
},
|
|
151
|
-
{ deliverAs: "nextTurn", triggerTurn: true },
|
|
163
|
+
{ deliverAs: "nextTurn", triggerTurn: true, acceptTerminalEmptyStop: true },
|
|
152
164
|
)
|
|
153
165
|
.then(started => {
|
|
154
166
|
if (!started) this.#suppressNext = false;
|
package/src/cli/args.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
STRING_SETTERS,
|
|
15
15
|
STRING_VALUE_FLAGS,
|
|
16
16
|
} from "./flag-tables";
|
|
17
|
+
import { CliUsageError } from "./usage-error";
|
|
17
18
|
|
|
18
19
|
export type Mode = "text" | "json" | "rpc" | "acp" | "rpc-ui";
|
|
19
20
|
|
|
@@ -299,6 +300,17 @@ export function reportUnrecognizedFlags(
|
|
|
299
300
|
return true;
|
|
300
301
|
}
|
|
301
302
|
|
|
303
|
+
/** Emit a clean CLI usage error without an internal stack trace. */
|
|
304
|
+
export function reportCliUsageError(
|
|
305
|
+
error: unknown,
|
|
306
|
+
write: (text: string) => void = text => process.stderr.write(text),
|
|
307
|
+
): boolean {
|
|
308
|
+
if (!(error instanceof CliUsageError)) return false;
|
|
309
|
+
write(`${chalk.red(`Error: ${error.message}`)}\n`);
|
|
310
|
+
write(`Run \`${APP_NAME} --help\` for available flags.\n`);
|
|
311
|
+
return true;
|
|
312
|
+
}
|
|
313
|
+
|
|
302
314
|
export function getExtraHelpText(): string {
|
|
303
315
|
return `${chalk.bold("Environment Variables:")}
|
|
304
316
|
${chalk.dim("# Core Providers")}
|
package/src/cli/flag-tables.ts
CHANGED
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
|
|
33
33
|
import type { ConfiguredThinkingLevel } from "../thinking";
|
|
34
34
|
import type { Args } from "./args";
|
|
35
|
+
import { CliUsageError } from "./usage-error";
|
|
35
36
|
|
|
36
37
|
/**
|
|
37
38
|
* Runtime dependencies injected into setters that need to validate input or
|
|
@@ -85,6 +86,24 @@ const setResume: OptionalSetter = (result, value) => {
|
|
|
85
86
|
result.resume = value !== undefined ? value : true;
|
|
86
87
|
};
|
|
87
88
|
|
|
89
|
+
const MAX_TIME_DURATION_RE = /^(\d+(?:\.\d+)?)([smh])$/;
|
|
90
|
+
|
|
91
|
+
function maxTimeMultiplier(unit: string | undefined): number {
|
|
92
|
+
if (unit === "h") return 3600;
|
|
93
|
+
if (unit === "m") return 60;
|
|
94
|
+
return 1;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function parseMaxTimeSeconds(value: string): number {
|
|
98
|
+
const trimmed = value.trim();
|
|
99
|
+
const duration = MAX_TIME_DURATION_RE.exec(trimmed);
|
|
100
|
+
const seconds = duration ? Number(duration[1]) * maxTimeMultiplier(duration[2]) : Number(trimmed);
|
|
101
|
+
if (Number.isFinite(seconds) && seconds > 0) return seconds;
|
|
102
|
+
throw new CliUsageError(
|
|
103
|
+
`Invalid --max-time value: ${JSON.stringify(value)}. Expected a positive number of seconds or duration like "5s", "10m", "1h".`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
88
107
|
/**
|
|
89
108
|
* Setters for flags with string values. Most built-ins consume the next argv
|
|
90
109
|
* token even when it starts with `-`; flags listed in
|
|
@@ -121,13 +140,8 @@ export const STRING_SETTERS: Record<string, StringSetter> = {
|
|
|
121
140
|
"--plan": (result, value) => {
|
|
122
141
|
result.plan = value;
|
|
123
142
|
},
|
|
124
|
-
"--max-time": (result, value
|
|
125
|
-
|
|
126
|
-
if (Number.isFinite(seconds) && seconds > 0) {
|
|
127
|
-
result.maxTime = seconds;
|
|
128
|
-
} else {
|
|
129
|
-
deps.logger.warn("Invalid seconds passed to --max-time", { value });
|
|
130
|
-
}
|
|
143
|
+
"--max-time": (result, value) => {
|
|
144
|
+
result.maxTime = parseMaxTimeSeconds(value);
|
|
131
145
|
},
|
|
132
146
|
"--api-key": (result, value) => {
|
|
133
147
|
result.apiKey = value;
|