@oh-my-pi/pi-coding-agent 16.1.22 → 16.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +99 -1
- package/dist/cli.js +4387 -4164
- package/dist/types/advisor/__tests__/emission-guard.test.d.ts +1 -0
- package/dist/types/advisor/emission-guard.d.ts +73 -0
- package/dist/types/advisor/index.d.ts +1 -0
- package/dist/types/advisor/runtime.d.ts +7 -0
- package/dist/types/advisor/watchdog.d.ts +2 -0
- package/dist/types/cli/flag-tables.d.ts +1 -0
- package/dist/types/cli/gallery-fixtures/search.d.ts +1 -1
- package/dist/types/cli/gc-cli.d.ts +58 -0
- package/dist/types/collab/display-name.d.ts +3 -0
- package/dist/types/collab/host.d.ts +0 -2
- package/dist/types/commands/gc.d.ts +37 -0
- package/dist/types/commands/worktree.d.ts +0 -3
- package/dist/types/config/inline-tool-descriptors-mode.d.ts +13 -0
- package/dist/types/config/model-discovery.d.ts +6 -1
- package/dist/types/config/model-registry.d.ts +6 -2
- package/dist/types/config/model-resolver.d.ts +12 -0
- package/dist/types/config/models-config-schema.d.ts +29 -2
- package/dist/types/config/models-config.d.ts +22 -1
- package/dist/types/config/settings-schema.d.ts +145 -21
- package/dist/types/config/settings.d.ts +13 -0
- package/dist/types/dap/config.d.ts +1 -1
- package/dist/types/edit/hashline/filesystem.d.ts +4 -2
- package/dist/types/edit/renderer.d.ts +4 -0
- package/dist/types/extensibility/extensions/types.d.ts +17 -17
- package/dist/types/extensibility/hooks/types.d.ts +10 -10
- package/dist/types/extensibility/plugins/marketplace/types.d.ts +1 -0
- package/dist/types/internal-urls/__tests__/ssh-protocol.test.d.ts +1 -0
- package/dist/types/internal-urls/index.d.ts +1 -0
- package/dist/types/internal-urls/registry-helpers.d.ts +2 -0
- package/dist/types/internal-urls/router.d.ts +1 -1
- package/dist/types/internal-urls/ssh-protocol.d.ts +10 -0
- package/dist/types/internal-urls/types.d.ts +19 -2
- package/dist/types/irc/bus.d.ts +6 -0
- package/dist/types/mcp/transports/stdio.d.ts +25 -1
- package/dist/types/modes/components/__tests__/move-overlay.test.d.ts +1 -0
- package/dist/types/modes/components/custom-editor.d.ts +7 -0
- package/dist/types/modes/components/move-overlay.d.ts +23 -0
- package/dist/types/modes/components/plugin-selector.d.ts +2 -1
- package/dist/types/modes/components/queue-mode-selector.d.ts +2 -1
- package/dist/types/modes/components/select-list-mouse-routing.d.ts +2 -0
- package/dist/types/modes/components/settings-defs.d.ts +4 -1
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/modes/components/show-images-selector.d.ts +2 -1
- package/dist/types/modes/components/status-line/component.d.ts +2 -0
- package/dist/types/modes/components/status-line/types.d.ts +2 -0
- package/dist/types/modes/components/theme-selector.d.ts +2 -1
- package/dist/types/modes/components/thinking-selector.d.ts +2 -1
- package/dist/types/modes/controllers/command-controller.d.ts +10 -1
- package/dist/types/modes/controllers/streaming-reveal.d.ts +6 -0
- package/dist/types/modes/interactive-mode.d.ts +10 -1
- package/dist/types/modes/internal-url-autocomplete.d.ts +1 -1
- package/dist/types/modes/running-subagent-badge.d.ts +6 -0
- package/dist/types/modes/theme/mermaid-rendering.test.d.ts +1 -0
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/modes/types.d.ts +9 -1
- package/dist/types/sdk.d.ts +2 -2
- package/dist/types/session/agent-session.d.ts +2 -1
- package/dist/types/session/session-listing.d.ts +10 -1
- package/dist/types/session/session-manager.d.ts +13 -0
- package/dist/types/slash-commands/builtin-registry.d.ts +1 -1
- package/dist/types/ssh/__tests__/connection-manager-args.test.d.ts +1 -0
- package/dist/types/ssh/__tests__/file-transfer-posix-guard.test.d.ts +1 -0
- package/dist/types/ssh/connection-manager.d.ts +2 -0
- package/dist/types/ssh/file-transfer.d.ts +79 -0
- package/dist/types/ssh/utils.d.ts +6 -0
- package/dist/types/system-prompt.d.ts +5 -0
- package/dist/types/task/executor.d.ts +16 -0
- package/dist/types/tiny/text.d.ts +1 -1
- package/dist/types/tools/builtin-names.d.ts +5 -1
- package/dist/types/tools/{find.d.ts → glob.d.ts} +15 -15
- package/dist/types/tools/{search.d.ts → grep.d.ts} +14 -14
- package/dist/types/tools/index.d.ts +3 -3
- package/dist/types/tools/path-utils.d.ts +29 -0
- package/dist/types/tools/plan-mode-guard.d.ts +7 -0
- package/dist/types/tools/read.d.ts +2 -2
- package/dist/types/tools/render-utils.d.ts +8 -0
- package/dist/types/tools/renderers.d.ts +11 -0
- package/dist/types/tools/ssh.d.ts +2 -0
- package/dist/types/tools/todo.d.ts +0 -16
- package/dist/types/tools/write.d.ts +2 -2
- package/dist/types/utils/active-repo-context.d.ts +8 -0
- package/dist/types/utils/image-resize.d.ts +1 -0
- package/dist/types/utils/markit-cache.d.ts +23 -0
- package/dist/types/utils/markit.d.ts +5 -1
- package/dist/types/utils/prompt-path.d.ts +1 -0
- package/dist/types/web/search/providers/duckduckgo.d.ts +14 -0
- package/dist/types/web/search/providers/firecrawl.d.ts +28 -0
- package/dist/types/web/search/providers/tinyfish.d.ts +29 -0
- package/dist/types/web/search/providers/xai.d.ts +13 -0
- package/dist/types/web/search/types.d.ts +18 -2
- package/package.json +30 -15
- package/scripts/bench-guard.ts +1 -1
- package/scripts/build-binary.ts +9 -9
- package/scripts/bundle-dist.ts +2 -2
- package/src/advisor/__tests__/advisor.test.ts +40 -4
- package/src/advisor/__tests__/emission-guard.test.ts +147 -0
- package/src/advisor/advise-tool.ts +1 -1
- package/src/advisor/emission-guard.ts +172 -0
- package/src/advisor/index.ts +1 -0
- package/src/advisor/runtime.ts +11 -0
- package/src/advisor/watchdog.ts +12 -1
- package/src/cli/args.ts +5 -2
- package/src/cli/auth-broker-cli.ts +17 -0
- package/src/cli/config-cli.ts +4 -0
- package/src/cli/flag-tables.ts +7 -4
- package/src/cli/gallery-cli.ts +14 -2
- package/src/cli/gallery-fixtures/edit.ts +60 -0
- package/src/cli/gallery-fixtures/fs.ts +17 -17
- package/src/cli/gallery-fixtures/search.ts +4 -4
- package/src/cli/gc-cli.ts +939 -0
- package/src/cli/usage-cli.ts +20 -1
- package/src/cli/web-search-cli.ts +1 -1
- package/src/cli-commands.ts +1 -0
- package/src/collab/display-name.ts +13 -0
- package/src/collab/guest.ts +5 -1
- package/src/collab/host.ts +2 -13
- package/src/commands/gc.ts +46 -0
- package/src/commands/worktree.ts +6 -0
- package/src/config/inline-tool-descriptors-mode.ts +27 -0
- package/src/config/model-discovery.ts +77 -8
- package/src/config/model-registry.ts +89 -11
- package/src/config/model-resolver.ts +39 -1
- package/src/config/models-config-schema.ts +41 -5
- package/src/config/models-config.ts +4 -1
- package/src/config/settings-schema.ts +130 -27
- package/src/config/settings.ts +216 -7
- package/src/cursor.ts +1 -1
- package/src/dap/config.ts +152 -8
- package/src/dap/session.ts +1 -1
- package/src/discovery/helpers.ts +3 -1
- package/src/edit/hashline/diff.ts +14 -3
- package/src/edit/hashline/execute.ts +42 -6
- package/src/edit/hashline/filesystem.ts +49 -8
- package/src/edit/index.ts +1 -0
- package/src/edit/modes/patch.ts +11 -1
- package/src/edit/renderer.ts +140 -13
- package/src/eval/__tests__/agent-bridge.test.ts +101 -0
- package/src/eval/__tests__/julia-prelude.test.ts +18 -0
- package/src/eval/agent-bridge.ts +26 -3
- package/src/eval/jl/runner.jl +7 -1
- package/src/eval/js/tool-bridge.ts +2 -2
- package/src/export/html/index.ts +1 -1
- package/src/export/html/template.js +3 -1
- package/src/export/html/tool-views.generated.js +20 -20
- package/src/extensibility/extensions/types.ts +22 -22
- package/src/extensibility/hooks/types.ts +11 -11
- package/src/extensibility/plugins/legacy-pi-bundled-keys.ts +10 -3
- package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +31 -10
- package/src/extensibility/plugins/marketplace/manager.ts +22 -0
- package/src/extensibility/plugins/marketplace/types.ts +1 -0
- package/src/internal-urls/__tests__/ssh-protocol.test.ts +331 -0
- package/src/internal-urls/docs-index.generated.txt +2 -2
- package/src/internal-urls/docs-index.ts +2 -3
- package/src/internal-urls/index.ts +1 -0
- package/src/internal-urls/registry-helpers.ts +19 -4
- package/src/internal-urls/router.ts +5 -3
- package/src/internal-urls/ssh-protocol.ts +367 -0
- package/src/internal-urls/types.ts +19 -2
- package/src/irc/bus.ts +11 -3
- package/src/lsp/index.ts +8 -1
- package/src/mcp/oauth-discovery.ts +20 -20
- package/src/mcp/tool-bridge.ts +32 -2
- package/src/mcp/transports/stdio.test.ts +20 -3
- package/src/mcp/transports/stdio.ts +45 -14
- package/src/memories/index.ts +1 -1
- package/src/modes/acp/acp-event-mapper.ts +2 -2
- package/src/modes/components/__tests__/move-overlay.test.ts +166 -0
- package/src/modes/components/agent-hub.ts +3 -0
- package/src/modes/components/agent-transcript-viewer.ts +9 -7
- package/src/modes/components/custom-editor.ts +90 -5
- package/src/modes/components/move-overlay.ts +282 -0
- package/src/modes/components/plan-review-overlay.ts +35 -35
- package/src/modes/components/plugin-selector.ts +6 -1
- package/src/modes/components/queue-mode-selector.ts +6 -1
- package/src/modes/components/select-list-mouse-routing.ts +35 -0
- package/src/modes/components/session-selector.ts +11 -10
- package/src/modes/components/settings-defs.ts +14 -1
- package/src/modes/components/settings-selector.ts +196 -29
- package/src/modes/components/show-images-selector.ts +6 -1
- package/src/modes/components/status-line/component.ts +108 -28
- package/src/modes/components/status-line/segments.ts +5 -1
- package/src/modes/components/status-line/types.ts +2 -0
- package/src/modes/components/theme-selector.ts +6 -1
- package/src/modes/components/thinking-selector.ts +6 -1
- package/src/modes/components/tool-execution.ts +25 -9
- package/src/modes/components/tree-selector.ts +5 -5
- package/src/modes/controllers/command-controller.ts +140 -47
- package/src/modes/controllers/event-controller.ts +59 -3
- package/src/modes/controllers/input-controller.ts +70 -4
- package/src/modes/controllers/selector-controller.ts +15 -2
- package/src/modes/controllers/streaming-reveal.ts +17 -0
- package/src/modes/controllers/tool-args-reveal.ts +1 -1
- package/src/modes/interactive-mode.ts +142 -59
- package/src/modes/internal-url-autocomplete.ts +17 -2
- package/src/modes/prompt-action-autocomplete.ts +3 -1
- package/src/modes/running-subagent-badge.ts +13 -0
- package/src/modes/setup-wizard/scenes/glyph.ts +2 -13
- package/src/modes/setup-wizard/scenes/providers.ts +2 -1
- package/src/modes/setup-wizard/scenes/theme.ts +6 -12
- package/src/modes/setup-wizard/scenes/web-search.ts +8 -13
- package/src/modes/setup-wizard/wizard-overlay.ts +6 -5
- package/src/modes/theme/mermaid-rendering.test.ts +53 -0
- package/src/modes/theme/theme.ts +42 -15
- package/src/modes/types.ts +9 -1
- package/src/modes/utils/interactive-context-helpers.ts +1 -1
- package/src/priority.json +15 -0
- package/src/prompts/advisor/active-repo-watchdog.md +6 -0
- package/src/prompts/advisor/system.md +21 -7
- package/src/prompts/agents/designer.md +1 -1
- package/src/prompts/agents/explore.md +1 -1
- package/src/prompts/agents/librarian.md +2 -2
- package/src/prompts/agents/plan.md +2 -2
- package/src/prompts/agents/reviewer.md +1 -1
- package/src/prompts/agents/task.md +2 -2
- package/src/prompts/system/active-repo-context.md +4 -0
- package/src/prompts/system/gemini-tool-call-reminder.md +9 -0
- package/src/prompts/system/plan-mode-active.md +12 -3
- package/src/prompts/system/project-prompt.md +2 -2
- package/src/prompts/system/system-prompt.md +8 -6
- package/src/prompts/tools/bash.md +2 -2
- package/src/prompts/tools/checkpoint.md +1 -1
- package/src/prompts/tools/{find.md → glob.md} +1 -1
- package/src/prompts/tools/{search.md → grep.md} +3 -3
- package/src/prompts/tools/read.md +4 -2
- package/src/sdk.ts +98 -29
- package/src/session/agent-session.ts +828 -191
- package/src/session/session-history-format.ts +2 -2
- package/src/session/session-listing.ts +35 -2
- package/src/session/session-manager.ts +46 -0
- package/src/slash-commands/builtin-registry.ts +158 -22
- package/src/slash-commands/helpers/usage-report.ts +23 -2
- package/src/ssh/__tests__/connection-manager-args.test.ts +69 -0
- package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +68 -0
- package/src/ssh/connection-manager.ts +12 -15
- package/src/ssh/file-transfer.ts +209 -0
- package/src/ssh/utils.ts +24 -0
- package/src/system-prompt.ts +60 -28
- package/src/task/executor.ts +96 -36
- package/src/task/index.ts +3 -3
- package/src/task/render.ts +14 -13
- package/src/task/worktree.ts +38 -1
- package/src/tiny/text.ts +49 -4
- package/src/tiny/worker.ts +3 -3
- package/src/tools/acp-bridge.ts +6 -1
- package/src/tools/bash.ts +2 -2
- package/src/tools/builtin-names.ts +26 -2
- package/src/tools/{find.ts → glob.ts} +48 -42
- package/src/tools/{search.ts → grep.ts} +311 -129
- package/src/tools/index.ts +13 -14
- package/src/tools/irc.ts +6 -2
- package/src/tools/path-utils.ts +64 -1
- package/src/tools/plan-mode-guard.ts +26 -13
- package/src/tools/read.ts +35 -6
- package/src/tools/render-utils.ts +14 -0
- package/src/tools/renderers.ts +15 -4
- package/src/tools/ssh.ts +17 -2
- package/src/tools/todo.ts +4 -26
- package/src/tools/write.ts +25 -9
- package/src/utils/active-repo-context.ts +143 -0
- package/src/utils/edit-mode.ts +19 -2
- package/src/utils/image-resize.ts +88 -7
- package/src/utils/lang-from-path.ts +3 -3
- package/src/utils/markit-cache.ts +166 -0
- package/src/utils/markit.ts +86 -18
- package/src/utils/prompt-path.ts +3 -0
- package/src/utils/shell-snapshot.ts +1 -1
- package/src/utils/title-generator.ts +1 -1
- package/src/web/search/provider.ts +54 -34
- package/src/web/search/providers/duckduckgo.ts +140 -0
- package/src/web/search/providers/firecrawl.ts +144 -0
- package/src/web/search/providers/tinyfish.ts +159 -0
- package/src/web/search/providers/xai.ts +292 -0
- package/src/web/search/providers/zai.ts +142 -56
- package/src/web/search/types.ts +6 -2
package/scripts/build-binary.ts
CHANGED
|
@@ -49,8 +49,8 @@ async function main(): Promise<void> {
|
|
|
49
49
|
// Generate inside the try so the finally always restores the empty checked-in
|
|
50
50
|
// placeholders (stats client archive, docs index) even on failure.
|
|
51
51
|
try {
|
|
52
|
-
await runCommand(["bun", "--cwd=../stats", "
|
|
53
|
-
await runCommand(["bun", "
|
|
52
|
+
await runCommand(["bun", "--cwd=../stats", "run", "gen:stats"]);
|
|
53
|
+
await runCommand(["bun", "run", "gen:docs"]);
|
|
54
54
|
// `legacy-pi-bundled-registry.ts` static-imports
|
|
55
55
|
// `@oh-my-pi/pi-coding-agent/export/html` (one of pi-coding-agent's
|
|
56
56
|
// named subpath exports, see scripts/generate-legacy-pi-bundled-registry.ts),
|
|
@@ -61,14 +61,14 @@ async function main(): Promise<void> {
|
|
|
61
61
|
// resolving the missing generated bundle. Rebuilding the tool views
|
|
62
62
|
// here makes the compile self-contained and matches what `prepack`
|
|
63
63
|
// does for the npm bundle.
|
|
64
|
-
await runCommand(["bun", "--cwd=../collab-web", "run", "
|
|
64
|
+
await runCommand(["bun", "--cwd=../collab-web", "run", "gen:tool-views"]);
|
|
65
65
|
await runCommand(
|
|
66
|
-
["bun", "--cwd=../natives", "run", "
|
|
66
|
+
["bun", "--cwd=../natives", "run", "gen:native"],
|
|
67
67
|
crossTarget
|
|
68
68
|
? { ...Bun.env, TARGET_PLATFORM: crossPlatform as string, TARGET_ARCH: crossArch as string }
|
|
69
69
|
: Bun.env,
|
|
70
70
|
);
|
|
71
|
-
await runCommand(["bun", "
|
|
71
|
+
await runCommand(["bun", "run", "gen:mupdf"]);
|
|
72
72
|
// Regenerate the bundled-pi registry + key set before the compile so any
|
|
73
73
|
// new pi-* subpath export added under `packages/*/package.json` is served
|
|
74
74
|
// from the host's in-process copy. Without this, `bun build --compile`
|
|
@@ -123,12 +123,12 @@ async function main(): Promise<void> {
|
|
|
123
123
|
await runCommand(["codesign", "--force", "--sign", "-", outputPath]);
|
|
124
124
|
}
|
|
125
125
|
} finally {
|
|
126
|
-
await runCommand(["bun", "
|
|
127
|
-
await runCommand(["bun", "--cwd=../natives", "run", "
|
|
126
|
+
await runCommand(["bun", "run", "gen:mupdf:reset"]);
|
|
127
|
+
await runCommand(["bun", "--cwd=../natives", "run", "gen:native:reset"]);
|
|
128
128
|
}
|
|
129
129
|
} finally {
|
|
130
|
-
await runCommand(["bun", "--cwd=../stats", "
|
|
131
|
-
await runCommand(["bun", "
|
|
130
|
+
await runCommand(["bun", "--cwd=../stats", "run", "gen:stats:reset"]);
|
|
131
|
+
await runCommand(["bun", "run", "gen:docs:reset"]);
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
134
|
|
package/scripts/bundle-dist.ts
CHANGED
|
@@ -79,7 +79,7 @@ async function main(): Promise<void> {
|
|
|
79
79
|
// so embed the dashboard archive the same way compiled binaries do
|
|
80
80
|
// (scripts/build-binary.ts). Reset afterwards to keep the checked-in
|
|
81
81
|
// placeholder empty.
|
|
82
|
-
await runCommand(["bun", "--cwd=../stats", "
|
|
82
|
+
await runCommand(["bun", "--cwd=../stats", "run", "gen:stats"]);
|
|
83
83
|
try {
|
|
84
84
|
await runCommand([
|
|
85
85
|
"bun",
|
|
@@ -97,7 +97,7 @@ async function main(): Promise<void> {
|
|
|
97
97
|
"./src/cli.ts",
|
|
98
98
|
]);
|
|
99
99
|
} finally {
|
|
100
|
-
await runCommand(["bun", "--cwd=../stats", "
|
|
100
|
+
await runCommand(["bun", "--cwd=../stats", "run", "gen:stats:reset"]);
|
|
101
101
|
}
|
|
102
102
|
await ensureShebang();
|
|
103
103
|
const stat = await fs.stat(cliPath);
|
|
@@ -3,6 +3,7 @@ import type { AgentMessage, AgentTelemetryConfig } from "@oh-my-pi/pi-agent-core
|
|
|
3
3
|
import { type } from "arktype";
|
|
4
4
|
import { createAdvisorMessageCard } from "../../modes/components/advisor-message";
|
|
5
5
|
import { getThemeByName } from "../../modes/theme/theme";
|
|
6
|
+
import advisorSystemPrompt from "../../prompts/advisor/system.md" with { type: "text" };
|
|
6
7
|
import { SecretObfuscator } from "../../secrets/obfuscator";
|
|
7
8
|
import { formatSessionHistoryMarkdown } from "../../session/session-history-format";
|
|
8
9
|
import { YieldQueue } from "../../session/yield-queue";
|
|
@@ -21,6 +22,41 @@ import {
|
|
|
21
22
|
} from "..";
|
|
22
23
|
|
|
23
24
|
describe("advisor", () => {
|
|
25
|
+
describe("advisor system prompt", () => {
|
|
26
|
+
it("forbids concrete claims about tool arguments hidden from the advisor transcript", () => {
|
|
27
|
+
const messages = [
|
|
28
|
+
{
|
|
29
|
+
role: "assistant",
|
|
30
|
+
content: [
|
|
31
|
+
{
|
|
32
|
+
type: "toolCall",
|
|
33
|
+
id: "search-timeout",
|
|
34
|
+
name: "grep",
|
|
35
|
+
arguments: { pattern: "needle", paths: ["packages/coding-agent/src"] },
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
timestamp: 1,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
role: "toolResult",
|
|
42
|
+
toolCallId: "search-timeout",
|
|
43
|
+
toolName: "grep",
|
|
44
|
+
content: [{ type: "text", text: "timed out after 30s" }],
|
|
45
|
+
isError: true,
|
|
46
|
+
timestamp: 2,
|
|
47
|
+
},
|
|
48
|
+
] as unknown as AgentMessage[];
|
|
49
|
+
|
|
50
|
+
const rendered = formatSessionHistoryMarkdown(messages);
|
|
51
|
+
|
|
52
|
+
expect(rendered).toContain("→ grep(needle @ packages/coding-agent/src) ⇒ error");
|
|
53
|
+
expect(rendered).not.toContain("paths[0]");
|
|
54
|
+
expect(advisorSystemPrompt).toContain("Arguments absent from the rendered transcript are UNKNOWN");
|
|
55
|
+
expect(advisorSystemPrompt).toContain("NEVER assert concrete values, array indexes");
|
|
56
|
+
expect(advisorSystemPrompt).toContain("NEVER claim `paths[0]`, array flattening, or malformed `paths`");
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
24
60
|
describe("formatSessionHistoryMarkdown includeThinking", () => {
|
|
25
61
|
it("includes thinking text when includeThinking is true", () => {
|
|
26
62
|
const thinking = "I should check the edge case first.";
|
|
@@ -675,13 +711,13 @@ describe("advisor", () => {
|
|
|
675
711
|
} as AgentMessage,
|
|
676
712
|
{
|
|
677
713
|
role: "assistant",
|
|
678
|
-
content: [{ type: "toolCall", id: "b", name: "
|
|
714
|
+
content: [{ type: "toolCall", id: "b", name: "grep", arguments: { pattern: "y" } }],
|
|
679
715
|
timestamp: 4,
|
|
680
716
|
} as unknown as AgentMessage,
|
|
681
717
|
{
|
|
682
718
|
role: "toolResult",
|
|
683
719
|
toolCallId: "b",
|
|
684
|
-
toolName: "
|
|
720
|
+
toolName: "grep",
|
|
685
721
|
content: [{ type: "text", text: "ok" }],
|
|
686
722
|
isError: false,
|
|
687
723
|
timestamp: 5,
|
|
@@ -1008,9 +1044,9 @@ describe("advisor", () => {
|
|
|
1008
1044
|
|
|
1009
1045
|
describe("read-only tool allowlist", () => {
|
|
1010
1046
|
it("selects only the investigation tools from a mixed toolset", () => {
|
|
1011
|
-
const toolset = ["read", "edit", "
|
|
1047
|
+
const toolset = ["read", "edit", "grep", "bash", "glob", "write", "advise"];
|
|
1012
1048
|
const selected = toolset.filter(name => ADVISOR_READONLY_TOOL_NAMES.has(name));
|
|
1013
|
-
expect(selected).toEqual(["read", "
|
|
1049
|
+
expect(selected).toEqual(["read", "grep", "glob"]);
|
|
1014
1050
|
expect(ADVISOR_READONLY_TOOL_NAMES.has("edit")).toBe(false);
|
|
1015
1051
|
expect(ADVISOR_READONLY_TOOL_NAMES.has("bash")).toBe(false);
|
|
1016
1052
|
expect(ADVISOR_READONLY_TOOL_NAMES.has("write")).toBe(false);
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { AdvisorEmissionGuard, normalizeAdvisorNote } from "../emission-guard";
|
|
3
|
+
|
|
4
|
+
describe("normalizeAdvisorNote", () => {
|
|
5
|
+
it("collapses punctuation, casing, and surrounding whitespace into one canonical key", () => {
|
|
6
|
+
// The reporter's three top duplicates all key to the same canonical form
|
|
7
|
+
// regardless of trailing punctuation or casing — that's what makes the
|
|
8
|
+
// dedupe + suppression checks single-membership.
|
|
9
|
+
expect(normalizeAdvisorNote("Stop.")).toBe("stop");
|
|
10
|
+
expect(normalizeAdvisorNote(" STOP! ")).toBe("stop");
|
|
11
|
+
expect(normalizeAdvisorNote("*Stop*")).toBe("stop");
|
|
12
|
+
expect(normalizeAdvisorNote("Done.")).toBe("done");
|
|
13
|
+
expect(normalizeAdvisorNote("No issue; continue.")).toBe("no issue continue");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("returns empty string for whitespace-only input so callers can short-circuit", () => {
|
|
17
|
+
expect(normalizeAdvisorNote("")).toBe("");
|
|
18
|
+
expect(normalizeAdvisorNote(" ")).toBe("");
|
|
19
|
+
expect(normalizeAdvisorNote("...")).toBe("");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("preserves internal letters/digits but folds non-alphanumeric runs to one space", () => {
|
|
23
|
+
expect(normalizeAdvisorNote("Refactor `auth-flow.ts`: drop legacy branch.")).toBe(
|
|
24
|
+
"refactor auth flow ts drop legacy branch",
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("AdvisorEmissionGuard", () => {
|
|
30
|
+
it("drops the exact content-free filler the reporter observed flooding the chat", () => {
|
|
31
|
+
// Issue #3520: 114× "Stop.", 52× "No issue; continue.", 41× "Done." —
|
|
32
|
+
// none of these carry a concrete reason and they cannot be acted on, so
|
|
33
|
+
// the guard suppresses them regardless of severity.
|
|
34
|
+
const guard = new AdvisorEmissionGuard();
|
|
35
|
+
expect(guard.accept("Stop.")).toBe(false);
|
|
36
|
+
expect(guard.accept("Done.")).toBe(false);
|
|
37
|
+
expect(guard.accept("No issue; continue.")).toBe(false);
|
|
38
|
+
expect(guard.accept("LGTM")).toBe(false);
|
|
39
|
+
expect(guard.accept("No further watcher input needed.")).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("dedupes by normalized text across the session, ignoring casing and trailing punctuation", () => {
|
|
43
|
+
const guard = new AdvisorEmissionGuard();
|
|
44
|
+
expect(guard.accept("Move retries into the queue, not the request path.")).toBe(true);
|
|
45
|
+
// Same advice with different casing and trailing punctuation must NOT
|
|
46
|
+
// land twice in the primary transcript.
|
|
47
|
+
expect(guard.accept("move retries into the queue, not the request path")).toBe(false);
|
|
48
|
+
expect(guard.accept("Move retries into the queue, not the request path!")).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("rate-limits to one accepted advise per advisor update cycle", () => {
|
|
52
|
+
// The advisor system prompt says "at most one `advise` per update". Real
|
|
53
|
+
// models violate this; the guard enforces it at the boundary so the
|
|
54
|
+
// primary transcript never receives two advisories from one model cycle.
|
|
55
|
+
const guard = new AdvisorEmissionGuard();
|
|
56
|
+
expect(guard.accept("First concern: missing await in #handleRetry.")).toBe(true);
|
|
57
|
+
expect(guard.accept("Second concern: wrong env var name.")).toBe(false);
|
|
58
|
+
guard.beginUpdate();
|
|
59
|
+
// New cycle: budget reset.
|
|
60
|
+
expect(guard.accept("Second concern: wrong env var name.")).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("does not let a suppressed call consume the per-update budget", () => {
|
|
64
|
+
// A noise call like "Stop." must never displace a real concern that
|
|
65
|
+
// follows in the same advisor model cycle.
|
|
66
|
+
const guard = new AdvisorEmissionGuard();
|
|
67
|
+
expect(guard.accept("Stop.")).toBe(false);
|
|
68
|
+
expect(guard.accept("Concrete: read race in #handleRetry.")).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("does not let a deduped call consume the per-update budget", () => {
|
|
72
|
+
// A repeat of a prior session note is dropped, but the model can still
|
|
73
|
+
// follow it with a fresh concrete concern in the same cycle.
|
|
74
|
+
const guard = new AdvisorEmissionGuard();
|
|
75
|
+
expect(guard.accept("Concrete: read race in #handleRetry.")).toBe(true);
|
|
76
|
+
guard.beginUpdate();
|
|
77
|
+
expect(guard.accept("Concrete: read race in #handleRetry.")).toBe(false);
|
|
78
|
+
expect(guard.accept("New concern: cache eviction never fires.")).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("reset clears dedupe and the per-update gate so a re-primed advisor can re-raise old issues", () => {
|
|
82
|
+
// Compaction / session-switch rewrites the primary transcript. The
|
|
83
|
+
// advisor is re-primed from scratch and may legitimately re-raise the
|
|
84
|
+
// same concerns — they're new context for a freshly-primed reviewer.
|
|
85
|
+
const guard = new AdvisorEmissionGuard();
|
|
86
|
+
expect(guard.accept("Race in #handleRetry.")).toBe(true);
|
|
87
|
+
expect(guard.accept("Race in #handleRetry.")).toBe(false);
|
|
88
|
+
guard.reset();
|
|
89
|
+
expect(guard.accept("Race in #handleRetry.")).toBe(true);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("evicts oldest entries when dedupe history exceeds capacity", () => {
|
|
93
|
+
// Bounded so very long sessions cannot grow the dedupe state without
|
|
94
|
+
// bound. Pre-eviction unique notes are remembered; post-eviction the
|
|
95
|
+
// oldest one is forgotten and can resurface.
|
|
96
|
+
const guard = new AdvisorEmissionGuard({ capacity: 3 });
|
|
97
|
+
expect(guard.accept("first")).toBe(true);
|
|
98
|
+
guard.beginUpdate();
|
|
99
|
+
expect(guard.accept("second")).toBe(true);
|
|
100
|
+
guard.beginUpdate();
|
|
101
|
+
expect(guard.accept("third")).toBe(true);
|
|
102
|
+
guard.beginUpdate();
|
|
103
|
+
// "first" still in history.
|
|
104
|
+
expect(guard.accept("first")).toBe(false);
|
|
105
|
+
guard.beginUpdate();
|
|
106
|
+
// Fourth unique entry evicts "first".
|
|
107
|
+
expect(guard.accept("fourth")).toBe(true);
|
|
108
|
+
guard.beginUpdate();
|
|
109
|
+
expect(guard.accept("first")).toBe(true);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("rejects empty / whitespace-only notes without consuming the budget", () => {
|
|
113
|
+
const guard = new AdvisorEmissionGuard();
|
|
114
|
+
expect(guard.accept("")).toBe(false);
|
|
115
|
+
expect(guard.accept(" ")).toBe(false);
|
|
116
|
+
expect(guard.accept("Concrete advice.")).toBe(true);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("end-to-end: the reporter's 309-call spam log produces ≤1 accepted note across many updates", () => {
|
|
120
|
+
// Mimic the issue's distribution: 114× "Stop.", 52× "No issue; continue.",
|
|
121
|
+
// 41× "Done.", plus 102 copies of one concrete-but-repeated nit. Spread
|
|
122
|
+
// the calls across 50 advisor update cycles. Each cycle is allowed at
|
|
123
|
+
// most one accepted note, and identical-text repeats never escape the
|
|
124
|
+
// guard. After all calls, exactly the concrete nit has been accepted
|
|
125
|
+
// — and only once.
|
|
126
|
+
const guard = new AdvisorEmissionGuard();
|
|
127
|
+
const accepted: string[] = [];
|
|
128
|
+
const stream: string[] = [
|
|
129
|
+
...Array(114).fill("Stop."),
|
|
130
|
+
...Array(52).fill("No issue; continue."),
|
|
131
|
+
...Array(41).fill("Done."),
|
|
132
|
+
...Array(102).fill("Concrete-but-repeated nit: x"),
|
|
133
|
+
];
|
|
134
|
+
// Interleave across 50 update cycles.
|
|
135
|
+
const cycles = 50;
|
|
136
|
+
const perCycle = Math.ceil(stream.length / cycles);
|
|
137
|
+
for (let c = 0; c < cycles; c++) {
|
|
138
|
+
guard.beginUpdate();
|
|
139
|
+
for (let i = 0; i < perCycle; i++) {
|
|
140
|
+
const note = stream[c * perCycle + i];
|
|
141
|
+
if (note === undefined) break;
|
|
142
|
+
if (guard.accept(note)) accepted.push(note);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
expect(accepted).toEqual(["Concrete-but-repeated nit: x"]);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
@@ -137,7 +137,7 @@ export function deriveAdvisorTelemetry(
|
|
|
137
137
|
* inspect the workspace before weighing in. Names match the primary session's
|
|
138
138
|
* tool instances, which the advisor reuses.
|
|
139
139
|
*/
|
|
140
|
-
export const ADVISOR_READONLY_TOOL_NAMES: ReadonlySet<string> = new Set(["read", "
|
|
140
|
+
export const ADVISOR_READONLY_TOOL_NAMES: ReadonlySet<string> = new Set(["read", "grep", "glob"]);
|
|
141
141
|
|
|
142
142
|
function advisorNoteDedupeKey(note: string): string {
|
|
143
143
|
return note.trim().replace(/\s+/g, " ");
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-session policy gate for advisor `advise()` calls.
|
|
3
|
+
*
|
|
4
|
+
* The advisor system prompt tells the watcher model:
|
|
5
|
+
*
|
|
6
|
+
* > at most one `advise` per update
|
|
7
|
+
* > NEVER repeat advice you already gave, and NEVER send the same advice twice
|
|
8
|
+
*
|
|
9
|
+
* Real advisor models violate this. Issue #3520 captured a session where
|
|
10
|
+
* `__advisor.jsonl` recorded 309 `advise` calls covering 92 unique notes —
|
|
11
|
+
* 114× `Stop.`, 52× `No issue; continue.`, 41× `Done.` — flooding the primary
|
|
12
|
+
* transcript with `<advisory severity="blocker">Stop.</advisory>` after the
|
|
13
|
+
* task was already complete. The fix is to make the rules load-bearing in code
|
|
14
|
+
* instead of prose: silently drop duplicates, content-free self-talk, and
|
|
15
|
+
* over-budget calls at the `enqueueAdvice` boundary so the primary stays
|
|
16
|
+
* clean even when the advisor misbehaves.
|
|
17
|
+
*
|
|
18
|
+
* The gate is intentionally invisible to the advisor model — `AdviseTool`
|
|
19
|
+
* still returns `Recorded.` for a suppressed call. Surfacing "suppressed"
|
|
20
|
+
* back into advisor context risks the model rephrasing the same useless note
|
|
21
|
+
* to bypass the dedupe ("Stop.", then "Halt." then "Stop now.").
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Case-insensitive, punctuation-folded normalization. Collapses every run of
|
|
26
|
+
* non-letter / non-digit characters into a single space and trims, so
|
|
27
|
+
* `"Stop."`, `"*Stop*"`, and `" stop "` all key to `stop`, while
|
|
28
|
+
* `"No issue; continue."` keys to `no issue continue`.
|
|
29
|
+
*
|
|
30
|
+
* Exported for tests.
|
|
31
|
+
*/
|
|
32
|
+
export function normalizeAdvisorNote(note: string): string {
|
|
33
|
+
return note
|
|
34
|
+
.toLowerCase()
|
|
35
|
+
.normalize("NFKC")
|
|
36
|
+
.replace(/[^\p{L}\p{N}]+/gu, " ")
|
|
37
|
+
.trim();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Normalized phrases the advisor occasionally emits that carry no concrete
|
|
42
|
+
* actionable content. Each must be the output of {@link normalizeAdvisorNote}
|
|
43
|
+
* so a single membership check covers every punctuation/casing variant
|
|
44
|
+
* (`"Stop."`, `"stop"`, `"STOP!"`).
|
|
45
|
+
*
|
|
46
|
+
* The list is conservative — only short, content-free filler the reporter
|
|
47
|
+
* observed driving primary-transcript pollution. A genuine `blocker` like
|
|
48
|
+
* `"Stop: 'await' missing on writeStream.end() will lose buffered writes."`
|
|
49
|
+
* does not match.
|
|
50
|
+
*/
|
|
51
|
+
const SUPPRESSED_NORMALIZED_PHRASES: Record<string, true> = {
|
|
52
|
+
// Self-stop noise — telling the agent to "stop" without a reason is useless.
|
|
53
|
+
stop: true,
|
|
54
|
+
"stop here": true,
|
|
55
|
+
"stop now": true,
|
|
56
|
+
halt: true,
|
|
57
|
+
abort: true,
|
|
58
|
+
// Completion self-talk — the agent already finished the task.
|
|
59
|
+
done: true,
|
|
60
|
+
"task done": true,
|
|
61
|
+
"task complete": true,
|
|
62
|
+
complete: true,
|
|
63
|
+
finished: true,
|
|
64
|
+
ok: true,
|
|
65
|
+
okay: true,
|
|
66
|
+
"ok done": true,
|
|
67
|
+
// "Nothing to flag" — silence is the correct expression of "no concerns".
|
|
68
|
+
"no issue": true,
|
|
69
|
+
"no issues": true,
|
|
70
|
+
"no issue continue": true,
|
|
71
|
+
"no concerns": true,
|
|
72
|
+
"no concern": true,
|
|
73
|
+
"nothing to add": true,
|
|
74
|
+
"nothing to flag": true,
|
|
75
|
+
"nothing to report": true,
|
|
76
|
+
"no notes": true,
|
|
77
|
+
"no further input": true,
|
|
78
|
+
"no further input needed": true,
|
|
79
|
+
"no further input required": true,
|
|
80
|
+
"no further watcher input": true,
|
|
81
|
+
"no further watcher input needed": true,
|
|
82
|
+
"no further advice": true,
|
|
83
|
+
"no further advice needed": true,
|
|
84
|
+
// Endorsements — equivalent to silence.
|
|
85
|
+
lgtm: true,
|
|
86
|
+
"looks good": true,
|
|
87
|
+
"all good": true,
|
|
88
|
+
"agent is on track": true,
|
|
89
|
+
"agent on track": true,
|
|
90
|
+
"on track": true,
|
|
91
|
+
continue: true,
|
|
92
|
+
"carry on": true,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Bounds the dedupe history. Sessions with very long advisor activity could
|
|
97
|
+
* otherwise grow the set without bound. The reporter's pathological session
|
|
98
|
+
* had 92 unique notes; 4096 leaves headroom while staying tiny (≤ ~256 KB of
|
|
99
|
+
* normalized strings even at long max).
|
|
100
|
+
*/
|
|
101
|
+
const DEFAULT_HISTORY_CAPACITY = 4096;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Decides whether an advisor `advise()` call should reach the primary agent.
|
|
105
|
+
*
|
|
106
|
+
* Enforces — in this order — the noise filter, session-scoped exact-text
|
|
107
|
+
* dedupe (FIFO-evicted at {@link DEFAULT_HISTORY_CAPACITY}), and a per-update
|
|
108
|
+
* rate limit of one accepted note per advisor model prompt. Suppressed calls
|
|
109
|
+
* never consume the per-update budget — a noise call doesn't burn the slot
|
|
110
|
+
* for a real concern that follows in the same update.
|
|
111
|
+
*
|
|
112
|
+
* Reset on advisor reset (compaction, session switch, `/new`) via
|
|
113
|
+
* {@link reset}. Per-update gate is cleared at the start of every advisor
|
|
114
|
+
* `agent.prompt()` cycle via {@link beginUpdate}.
|
|
115
|
+
*/
|
|
116
|
+
export class AdvisorEmissionGuard {
|
|
117
|
+
#seen = new Set<string>();
|
|
118
|
+
/** Insertion-order log to drive FIFO eviction without an extra Map. */
|
|
119
|
+
#seenOrder: string[] = [];
|
|
120
|
+
#consumedThisUpdate = false;
|
|
121
|
+
readonly #capacity: number;
|
|
122
|
+
|
|
123
|
+
constructor(opts: { capacity?: number } = {}) {
|
|
124
|
+
this.#capacity = opts.capacity ?? DEFAULT_HISTORY_CAPACITY;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Drop all dedupe and per-update state. Called from
|
|
129
|
+
* `AgentSession#resetAdvisorSessionState()` whenever the advisor runtime is
|
|
130
|
+
* reset — same boundary as `yieldQueue.clear("advisor")`, so a re-primed
|
|
131
|
+
* advisor can re-raise old issues (the primary transcript was rewritten).
|
|
132
|
+
*/
|
|
133
|
+
reset(): void {
|
|
134
|
+
this.#seen.clear();
|
|
135
|
+
this.#seenOrder.length = 0;
|
|
136
|
+
this.#consumedThisUpdate = false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Clear the per-update rate-limit gate. Called by `AdvisorRuntime` right
|
|
141
|
+
* before each `agent.prompt(batch)` invocation so the next advisor model
|
|
142
|
+
* cycle starts with a fresh budget of one advise.
|
|
143
|
+
*/
|
|
144
|
+
beginUpdate(): void {
|
|
145
|
+
this.#consumedThisUpdate = false;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Whether the proposed note should reach the primary. On `true` the gate
|
|
150
|
+
* has already recorded the note (consumed the per-update budget and added
|
|
151
|
+
* it to the dedupe history) — caller delivers the note. On `false` the
|
|
152
|
+
* caller drops it.
|
|
153
|
+
*
|
|
154
|
+
* Empty / whitespace-only notes are suppressed; the model's
|
|
155
|
+
* tool-args contract still requires a non-empty string but defense-in-depth.
|
|
156
|
+
*/
|
|
157
|
+
accept(note: string): boolean {
|
|
158
|
+
const key = normalizeAdvisorNote(note);
|
|
159
|
+
if (!key) return false;
|
|
160
|
+
if (SUPPRESSED_NORMALIZED_PHRASES[key]) return false;
|
|
161
|
+
if (this.#seen.has(key)) return false;
|
|
162
|
+
if (this.#consumedThisUpdate) return false;
|
|
163
|
+
this.#consumedThisUpdate = true;
|
|
164
|
+
this.#seen.add(key);
|
|
165
|
+
this.#seenOrder.push(key);
|
|
166
|
+
if (this.#seenOrder.length > this.#capacity) {
|
|
167
|
+
const stale = this.#seenOrder.shift();
|
|
168
|
+
if (stale !== undefined) this.#seen.delete(stale);
|
|
169
|
+
}
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
}
|
package/src/advisor/index.ts
CHANGED
package/src/advisor/runtime.ts
CHANGED
|
@@ -30,6 +30,13 @@ export interface AdvisorRuntimeHost {
|
|
|
30
30
|
* the primary's next compaction triggers {@link AdvisorRuntime.reset}).
|
|
31
31
|
*/
|
|
32
32
|
maintainContext?(incomingTokens: number): Promise<boolean>;
|
|
33
|
+
/**
|
|
34
|
+
* Called immediately before each `agent.prompt(batch)` cycle. Lets the host
|
|
35
|
+
* clear per-update advisor state — currently the one-advise-per-update gate
|
|
36
|
+
* in {@link AdvisorEmissionGuard}, which the host owns because it is the
|
|
37
|
+
* one that routes `advise()` results back to the primary.
|
|
38
|
+
*/
|
|
39
|
+
beginAdvisorUpdate?(): void;
|
|
33
40
|
}
|
|
34
41
|
|
|
35
42
|
interface PendingDelta {
|
|
@@ -275,6 +282,10 @@ export class AdvisorRuntime {
|
|
|
275
282
|
|
|
276
283
|
let success = false;
|
|
277
284
|
try {
|
|
285
|
+
// Reset the host's per-update advisor state (one-advise-per-update
|
|
286
|
+
// gate) before each model cycle, so the new batch starts with a
|
|
287
|
+
// fresh budget. Dedupe history persists across cycles.
|
|
288
|
+
this.host.beginAdvisorUpdate?.();
|
|
278
289
|
await this.agent.prompt(batch);
|
|
279
290
|
success = true;
|
|
280
291
|
this.#consecutiveFailures = 0;
|
package/src/advisor/watchdog.ts
CHANGED
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
import * as os from "node:os";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { getAgentDir, isEnoent, logger } from "@oh-my-pi/pi-utils";
|
|
3
|
+
import { getAgentDir, isEnoent, logger, prompt } from "@oh-my-pi/pi-utils";
|
|
4
4
|
import { expandAtImports } from "../discovery/at-imports";
|
|
5
|
+
import activeRepoWatchdogTemplate from "../prompts/advisor/active-repo-watchdog.md" with { type: "text" };
|
|
6
|
+
import type { ActiveRepoContext } from "../utils/active-repo-context";
|
|
5
7
|
import { repo } from "../utils/git";
|
|
8
|
+
import { normalizePromptPath } from "../utils/prompt-path";
|
|
9
|
+
|
|
10
|
+
export function formatActiveRepoWatchdogPrompt(activeRepoContext: ActiveRepoContext): string {
|
|
11
|
+
return prompt
|
|
12
|
+
.render(activeRepoWatchdogTemplate, {
|
|
13
|
+
relativeRepoRoot: normalizePromptPath(activeRepoContext.relativeRepoRoot),
|
|
14
|
+
})
|
|
15
|
+
.trim();
|
|
16
|
+
}
|
|
6
17
|
|
|
7
18
|
/**
|
|
8
19
|
* Discover and load WATCHDOG.md files walking up from cwd, project .omp folder, and user agent dir.
|
package/src/cli/args.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { APP_NAME, CONFIG_DIR_NAME, logger } from "@oh-my-pi/pi-utils";
|
|
5
5
|
import chalk from "chalk";
|
|
6
6
|
import { CLI_THINKING_LEVELS, type ConfiguredThinkingLevel, parseCliThinkingLevel } from "../thinking";
|
|
7
|
-
import { BUILTIN_TOOL_NAMES } from "../tools/builtin-names";
|
|
7
|
+
import { BUILTIN_TOOL_NAMES, normalizeToolNames } from "../tools/builtin-names";
|
|
8
8
|
import {
|
|
9
9
|
OPTIONAL_FLAGS,
|
|
10
10
|
OPTIONAL_VALUE_FLAGS,
|
|
@@ -90,6 +90,7 @@ const PARSE_DEPS: ParseDeps = {
|
|
|
90
90
|
logger,
|
|
91
91
|
parseThinking: parseCliThinkingLevel,
|
|
92
92
|
builtinToolNames: BUILTIN_TOOL_NAMES,
|
|
93
|
+
normalizeToolNames,
|
|
93
94
|
thinkingEfforts: CLI_THINKING_LEVELS,
|
|
94
95
|
};
|
|
95
96
|
|
|
@@ -310,6 +311,8 @@ export function getExtraHelpText(): string {
|
|
|
310
311
|
PERPLEXITY_API_KEY - Perplexity web search API key (optional; anonymous fallback)
|
|
311
312
|
PERPLEXITY_COOKIES - Perplexity web search (session cookie)
|
|
312
313
|
TAVILY_API_KEY - Tavily web search
|
|
314
|
+
TINYFISH_API_KEY - TinyFish web search
|
|
315
|
+
FIRECRAWL_API_KEY - Firecrawl web search
|
|
313
316
|
ANTHROPIC_SEARCH_API_KEY - Anthropic web search (override; isolates search from main ANTHROPIC_API_KEY)
|
|
314
317
|
ANTHROPIC_SEARCH_BASE_URL - Anthropic web search base URL (override; pairs with ANTHROPIC_SEARCH_API_KEY)
|
|
315
318
|
|
|
@@ -330,7 +333,7 @@ ${chalk.bold("Available Tools (default-enabled unless noted):")}
|
|
|
330
333
|
edit - Edit files with find/replace
|
|
331
334
|
write - Write files (creates/overwrites)
|
|
332
335
|
grep - Search file contents
|
|
333
|
-
|
|
336
|
+
glob - Find files by glob pattern
|
|
334
337
|
lsp - Language server protocol (code intelligence)
|
|
335
338
|
python - Execute Python code (requires: ${APP_NAME} setup python)
|
|
336
339
|
notebook - Edit Jupyter notebooks
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
type OAuthCredential,
|
|
29
29
|
type OAuthProvider,
|
|
30
30
|
type OAuthProviderInfo,
|
|
31
|
+
PASTE_CODE_LOGIN_PROVIDERS,
|
|
31
32
|
PROVIDER_REGISTRY,
|
|
32
33
|
SqliteAuthCredentialStore,
|
|
33
34
|
} from "@oh-my-pi/pi-ai";
|
|
@@ -211,6 +212,15 @@ async function runLocalLogin(provider: OAuthProvider): Promise<void> {
|
|
|
211
212
|
const storage = new AuthStorage(store);
|
|
212
213
|
await storage.reload();
|
|
213
214
|
try {
|
|
215
|
+
// Only paste-code providers (fixed non-loopback redirect, e.g. GitLab Duo
|
|
216
|
+
// Agent's vscode:// URI) get the manual paste fallback. An explicit
|
|
217
|
+
// `onManualCodeInput` is honored for ANY provider (the storage escape hatch),
|
|
218
|
+
// so for loopback providers we must not pass it: it would make
|
|
219
|
+
// `OAuthCallbackFlow` race a readline prompt against the HTTP callback and, if
|
|
220
|
+
// the callback wins, leave that prompt outstanding (dirty/blocked terminal).
|
|
221
|
+
// `AuthStorage.login` independently refuses to synthesize the default prompt
|
|
222
|
+
// for non-paste-code providers, so this is defense-in-depth on the same gate.
|
|
223
|
+
const usesManualInput = PASTE_CODE_LOGIN_PROVIDERS.has(provider);
|
|
214
224
|
await storage.login(provider, {
|
|
215
225
|
onAuth({ url, instructions }) {
|
|
216
226
|
process.stdout.write(`\nOpen this URL in your browser:\n${url}\n`);
|
|
@@ -223,6 +233,13 @@ async function runLocalLogin(provider: OAuthProvider): Promise<void> {
|
|
|
223
233
|
onPrompt(p) {
|
|
224
234
|
return ask(`${p.message}${p.placeholder ? ` (${p.placeholder})` : ""}:`);
|
|
225
235
|
},
|
|
236
|
+
...(usesManualInput
|
|
237
|
+
? {
|
|
238
|
+
onManualCodeInput() {
|
|
239
|
+
return ask("Paste the authorization code (or full redirect URL):");
|
|
240
|
+
},
|
|
241
|
+
}
|
|
242
|
+
: undefined),
|
|
226
243
|
});
|
|
227
244
|
process.stdout.write(`\nCredentials saved to ${getAgentDbPath()}\n`);
|
|
228
245
|
} finally {
|
package/src/cli/config-cli.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
Settings,
|
|
17
17
|
type SettingValue,
|
|
18
18
|
settings,
|
|
19
|
+
validateProviderMaxInFlightRequests,
|
|
19
20
|
} from "../config/settings";
|
|
20
21
|
import { SETTINGS_SCHEMA } from "../config/settings-schema";
|
|
21
22
|
import { theme } from "../modes/theme/theme";
|
|
@@ -218,6 +219,9 @@ function parseAndSetValue(path: SettingPath, rawValue: string): void {
|
|
|
218
219
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
219
220
|
throw new Error(`Invalid record JSON: ${rawValue}`);
|
|
220
221
|
}
|
|
222
|
+
if (path === "providers.maxInFlightRequests") {
|
|
223
|
+
parsed = validateProviderMaxInFlightRequests(parsed);
|
|
224
|
+
}
|
|
221
225
|
parsedValue = parsed;
|
|
222
226
|
break;
|
|
223
227
|
}
|
package/src/cli/flag-tables.ts
CHANGED
|
@@ -46,6 +46,7 @@ export interface ParseDeps {
|
|
|
46
46
|
logger: { warn: (message: string, meta?: Record<string, unknown>) => void };
|
|
47
47
|
parseThinking: (value: string | null | undefined) => ConfiguredThinkingLevel | undefined;
|
|
48
48
|
builtinToolNames: readonly string[];
|
|
49
|
+
normalizeToolNames: (values: Iterable<string>) => string[];
|
|
49
50
|
thinkingEfforts: readonly string[];
|
|
50
51
|
}
|
|
51
52
|
|
|
@@ -147,10 +148,12 @@ export const STRING_SETTERS: Record<string, StringSetter> = {
|
|
|
147
148
|
result.models = value.split(",").map(s => s.trim());
|
|
148
149
|
},
|
|
149
150
|
"--tools": (result, value, deps) => {
|
|
150
|
-
const names =
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
151
|
+
const names = deps.normalizeToolNames(
|
|
152
|
+
value
|
|
153
|
+
.split(",")
|
|
154
|
+
.map(s => s.trim())
|
|
155
|
+
.filter(Boolean),
|
|
156
|
+
);
|
|
154
157
|
const valid: string[] = [];
|
|
155
158
|
for (const name of names) {
|
|
156
159
|
if (deps.builtinToolNames.includes(name)) {
|