@oh-my-pi/pi-coding-agent 16.1.20 → 16.1.21
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 +10 -0
- package/dist/cli.js +2804 -2793
- package/dist/types/advisor/advise-tool.d.ts +3 -0
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +2 -0
- package/dist/types/mcp/oauth-flow.d.ts +42 -5
- package/dist/types/modes/components/custom-editor.d.ts +32 -0
- package/dist/types/modes/components/tool-execution.d.ts +5 -5
- package/dist/types/modes/controllers/event-controller.d.ts +10 -0
- package/dist/types/modes/controllers/input-controller.d.ts +2 -1
- package/dist/types/utils/clipboard.d.ts +10 -0
- package/package.json +12 -12
- package/scripts/generate-legacy-pi-bundled-registry.ts +10 -0
- package/src/advisor/__tests__/advisor.test.ts +44 -0
- package/src/advisor/advise-tool.ts +33 -0
- package/src/autolearn/controller.ts +17 -2
- package/src/extensibility/plugins/legacy-pi-bundled-keys.ts +1 -1
- package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +4 -4
- package/src/extensibility/plugins/legacy-pi-compat.ts +193 -5
- package/src/mcp/manager.ts +12 -3
- package/src/mcp/oauth-flow.ts +121 -7
- package/src/modes/components/chat-transcript-builder.ts +31 -0
- package/src/modes/components/custom-editor.test.ts +80 -0
- package/src/modes/components/custom-editor.ts +86 -6
- package/src/modes/components/tool-execution.ts +50 -25
- package/src/modes/controllers/event-controller.ts +57 -8
- package/src/modes/controllers/input-controller.ts +70 -27
- package/src/modes/controllers/mcp-command-controller.ts +18 -2
- package/src/modes/utils/ui-helpers.ts +40 -0
- package/src/prompts/system/autolearn-nudge-autocontinue.md +5 -0
- package/src/prompts/system/autolearn-nudge.md +4 -2
- package/src/prompts/tools/todo.md +1 -1
- package/src/session/agent-session.ts +4 -0
- package/src/tools/todo.ts +20 -10
- package/src/utils/clipboard.ts +57 -0
|
@@ -84,6 +84,7 @@ export declare function deriveAdvisorTelemetry(primaryTelemetry: AgentTelemetryC
|
|
|
84
84
|
*/
|
|
85
85
|
export declare const ADVISOR_READONLY_TOOL_NAMES: ReadonlySet<string>;
|
|
86
86
|
export declare class AdviseTool implements AgentTool<typeof adviseSchema, AdviseDetails> {
|
|
87
|
+
#private;
|
|
87
88
|
private readonly onAdvice;
|
|
88
89
|
readonly name = "advise";
|
|
89
90
|
readonly label = "Advise";
|
|
@@ -94,6 +95,8 @@ export declare class AdviseTool implements AgentTool<typeof adviseSchema, Advise
|
|
|
94
95
|
}, {}>;
|
|
95
96
|
readonly intent: "omit";
|
|
96
97
|
constructor(onAdvice: (note: string, severity?: AdviseDetails["severity"]) => void);
|
|
98
|
+
/** Clear delivered-note memory when the advisor starts a fresh conversation. */
|
|
99
|
+
resetDeliveredNotes(): void;
|
|
97
100
|
execute(_toolCallId: string, args: AdviseParams, _signal?: AbortSignal, _onUpdate?: AgentToolUpdateCallback<AdviseDetails>, _context?: AgentToolContext): Promise<AgentToolResult<AdviseDetails>>;
|
|
98
101
|
}
|
|
99
102
|
export {};
|
|
@@ -61,6 +61,8 @@ export declare function __validateLegacyPiPackageRootOverrides(candidates: Recor
|
|
|
61
61
|
* compiled-binary branch is verifiable from dev tests.
|
|
62
62
|
*/
|
|
63
63
|
export declare function __buildLegacyPiPackageRootOverrides(isCompiled: boolean): Record<string, string>;
|
|
64
|
+
/** Test seam for compiled-binary legacy extension source rewriting. */
|
|
65
|
+
export declare function __rewriteLegacyExtensionSourceForTests(source: string, importerPath: string): Promise<string>;
|
|
64
66
|
/**
|
|
65
67
|
* Load a legacy Pi extension module from its real on-disk location.
|
|
66
68
|
*
|
|
@@ -38,6 +38,14 @@ export interface MCPStoredOAuthCredential extends OAuthCredential {
|
|
|
38
38
|
clientId?: string;
|
|
39
39
|
clientSecret?: string;
|
|
40
40
|
resource?: string;
|
|
41
|
+
/**
|
|
42
|
+
* Authorization-server URL (the issuer the grant was minted against). Used
|
|
43
|
+
* to filter same-origin resource indicators on refresh: RFC 8414 lets the
|
|
44
|
+
* authorize and token endpoints sit on different origins, so refresh
|
|
45
|
+
* cannot infer the original auth-server origin from `tokenUrl` alone.
|
|
46
|
+
* Unset on legacy credentials minted before issue #3502's fix.
|
|
47
|
+
*/
|
|
48
|
+
authorizationUrl?: string;
|
|
41
49
|
}
|
|
42
50
|
export interface MCPOAuthConfig {
|
|
43
51
|
/** Authorization endpoint URL */
|
|
@@ -67,6 +75,13 @@ export interface MCPOAuthConfig {
|
|
|
67
75
|
callbackPath?: string;
|
|
68
76
|
/** MCP resource URI for RFC 8707 resource indicators */
|
|
69
77
|
resource?: string;
|
|
78
|
+
/**
|
|
79
|
+
* True when `resource` was synthesized from the server URL fallback rather
|
|
80
|
+
* than advertised by OAuth/protected-resource metadata. Fallback resources
|
|
81
|
+
* are stripped when same-origin with the authorization server; advertised
|
|
82
|
+
* path-scoped resources are preserved.
|
|
83
|
+
*/
|
|
84
|
+
stripSameOriginResource?: boolean;
|
|
70
85
|
/** Fetch implementation for token exchange and discovery requests. */
|
|
71
86
|
fetch?: FetchImpl;
|
|
72
87
|
}
|
|
@@ -93,18 +108,40 @@ export declare class MCPOAuthFlow extends OAuthCallbackFlow {
|
|
|
93
108
|
*/
|
|
94
109
|
get registeredClientSecret(): string | undefined;
|
|
95
110
|
get resource(): string | undefined;
|
|
111
|
+
/**
|
|
112
|
+
* Authorization-server URL the flow used. Persist alongside the credential
|
|
113
|
+
* so refresh can filter same-origin resource indicators against the issuer's
|
|
114
|
+
* origin even when `tokenUrl` lives on a different origin (RFC 8414 permits
|
|
115
|
+
* the split).
|
|
116
|
+
*/
|
|
117
|
+
get authorizationUrl(): string;
|
|
96
118
|
generateAuthUrl(state: string, redirectUri: string): Promise<{
|
|
97
119
|
url: string;
|
|
98
120
|
instructions?: string;
|
|
99
121
|
}>;
|
|
100
122
|
exchangeToken(code: string, _state: string, redirectUri: string): Promise<OAuthCredentials>;
|
|
101
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* Options for {@link refreshMCPOAuthToken}. Carried via the trailing object
|
|
126
|
+
* so positional callers keep working.
|
|
127
|
+
*/
|
|
128
|
+
export interface RefreshMCPOAuthTokenOptions {
|
|
129
|
+
fetch?: FetchImpl;
|
|
130
|
+
/**
|
|
131
|
+
* Authorization-server URL the original grant was minted against. Used to
|
|
132
|
+
* filter same-origin resource indicators on refresh. Defaults to `tokenUrl`'s
|
|
133
|
+
* origin when omitted for legacy credentials.
|
|
134
|
+
*/
|
|
135
|
+
authorizationUrl?: string;
|
|
136
|
+
/**
|
|
137
|
+
* True when the refresh `resource` was synthesized from the server URL
|
|
138
|
+
* fallback because the credential/auth material carried no resource.
|
|
139
|
+
* Preserved advertised resources leave this false/undefined.
|
|
140
|
+
*/
|
|
141
|
+
stripSameOriginResource?: boolean;
|
|
142
|
+
}
|
|
102
143
|
/**
|
|
103
144
|
* Refresh an MCP OAuth token using the standard refresh_token grant.
|
|
104
145
|
* Returns updated credentials; preserves the old refresh token if the server doesn't rotate it.
|
|
105
146
|
*/
|
|
106
|
-
export declare function refreshMCPOAuthToken(tokenUrl: string, refreshToken: string, clientId?: string, clientSecret?: string, resourceOrOpts?: string |
|
|
107
|
-
fetch?: FetchImpl;
|
|
108
|
-
}, opts?: {
|
|
109
|
-
fetch?: FetchImpl;
|
|
110
|
-
}): Promise<OAuthCredentials>;
|
|
147
|
+
export declare function refreshMCPOAuthToken(tokenUrl: string, refreshToken: string, clientId?: string, clientSecret?: string, resourceOrOpts?: string | RefreshMCPOAuthTokenOptions, opts?: RefreshMCPOAuthTokenOptions): Promise<OAuthCredentials>;
|
|
@@ -19,9 +19,41 @@ export declare const SPACE_HOLD_MECHANICAL_RUN = 2;
|
|
|
19
19
|
/** Idle gap (ms) after the last repeated space that counts as the space bar being released, ending
|
|
20
20
|
* the push-to-talk recording. Must comfortably exceed the OS key-repeat interval. */
|
|
21
21
|
export declare const SPACE_HOLD_RELEASE_MS = 250;
|
|
22
|
+
/**
|
|
23
|
+
* Extract image-or-other file paths from plain (un-bracketed) clipboard text.
|
|
24
|
+
* Mirrors {@link extractBracketedPastePaths} for terminals/handlers that
|
|
25
|
+
* already stripped the `\x1b[200~`…`\x1b[201~` markers (e.g. clipboard text
|
|
26
|
+
* read directly via `pbpaste`/PowerShell).
|
|
27
|
+
*/
|
|
28
|
+
export declare function extractPastePathsFromText(text: string): string[] | undefined;
|
|
22
29
|
export declare function extractBracketedPastePaths(data: string): string[] | undefined;
|
|
23
30
|
export declare function extractBracketedImagePastePaths(data: string): string[] | undefined;
|
|
24
31
|
export declare function extractBracketedImagePastePath(data: string): string | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* Return a single image file path when `text` is exactly one explicit path
|
|
34
|
+
* pointing at a supported image extension (`.png`, `.jpg`/`.jpeg`, `.gif`,
|
|
35
|
+
* `.webp`). Used by the keybind-driven clipboard image paste path so a
|
|
36
|
+
* clipboard whose only payload is an image file (e.g. Finder `Cmd+C` on
|
|
37
|
+
* macOS) attaches the image instead of pasting the path as literal text.
|
|
38
|
+
*
|
|
39
|
+
* Two-stage detection:
|
|
40
|
+
*
|
|
41
|
+
* 1. Splitter pass (shared with the bracketed-paste handler) — handles
|
|
42
|
+
* quoted paths, shell-escaped spaces, and unambiguous single tokens.
|
|
43
|
+
* Returns the single image path when it parses cleanly; explicitly
|
|
44
|
+
* returns `undefined` when the splitter found multiple segments (so
|
|
45
|
+
* ambiguous multi-path clipboard text like `/tmp/a.png /tmp/b.png`
|
|
46
|
+
* still falls through to the text fallback instead of being mis-loaded
|
|
47
|
+
* as one giant path).
|
|
48
|
+
* 2. Whole-text-as-path pass — only reached when the splitter failed
|
|
49
|
+
* (every segment must look like an explicit path; an unescaped space in
|
|
50
|
+
* a real path breaks that). Restricted to inputs anchored by
|
|
51
|
+
* {@link ABSOLUTE_PATH_PREFIX_REGEX} so prose containing a path-shaped
|
|
52
|
+
* fragment ("see /tmp/x.png") never hijacks the smart fallback. This
|
|
53
|
+
* is what recovers macOS screenshot filenames like
|
|
54
|
+
* `/Users/me/Desktop/Screenshot 2026-06-25 at 1.23.45 PM.png`.
|
|
55
|
+
*/
|
|
56
|
+
export declare function extractImagePathFromText(text: string): string | undefined;
|
|
25
57
|
/**
|
|
26
58
|
* Custom editor that handles configurable app-level shortcuts for coding-agent.
|
|
27
59
|
*/
|
|
@@ -107,13 +107,13 @@ export declare class ToolExecutionComponent extends Container implements NativeS
|
|
|
107
107
|
*/
|
|
108
108
|
seal(): void;
|
|
109
109
|
/**
|
|
110
|
-
* Whether this block is a
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
* follow-up `job` call supersedes it.
|
|
110
|
+
* Whether this block is a supersedable result snapshot that has not been
|
|
111
|
+
* sealed. Such a block never finalized, so none of its rows entered native
|
|
112
|
+
* scrollback and the whole block can be removed when a follow-up matching
|
|
113
|
+
* tool call supersedes it.
|
|
115
114
|
*/
|
|
116
115
|
isDisplaceableBlock(): boolean;
|
|
116
|
+
canBeDisplacedBy(nextToolName: string | undefined): boolean;
|
|
117
117
|
/**
|
|
118
118
|
* Stop spinner animation and cleanup resources.
|
|
119
119
|
*/
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ToolExecutionComponent } from "../../modes/components/tool-execution";
|
|
1
2
|
import type { InteractiveModeContext } from "../../modes/types";
|
|
2
3
|
import type { AgentSessionEvent } from "../../session/agent-session";
|
|
3
4
|
export declare class EventController {
|
|
@@ -14,5 +15,14 @@ export declare class EventController {
|
|
|
14
15
|
*/
|
|
15
16
|
resetTranscriptAnchors(): void;
|
|
16
17
|
handleEvent(event: AgentSessionEvent): Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* Adopt a rebuilt-tail todo snapshot as the controller's tracked live
|
|
20
|
+
* snapshot. Used by rebuild paths (settings/extensions overlay close, focus
|
|
21
|
+
* attach, /resume) to preserve displacement continuity when a turn is still
|
|
22
|
+
* active — without this, the next same-turn `todo` update would stack
|
|
23
|
+
* another panel because the controller's tracker was reset before rebuild.
|
|
24
|
+
* Drops the candidate when it is no longer a displaceable todo.
|
|
25
|
+
*/
|
|
26
|
+
inheritDisplaceableTodo(component: ToolExecutionComponent | null | undefined): void;
|
|
17
27
|
sendCompletionNotification(): void;
|
|
18
28
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type AutocompleteProvider, type SlashCommand } from "@oh-my-pi/pi-tui";
|
|
2
2
|
import type { InteractiveModeContext } from "../../modes/types";
|
|
3
|
-
import { readImageFromClipboard, readTextFromClipboard } from "../../utils/clipboard";
|
|
3
|
+
import { readImageFromClipboard, readMacFileUrlsFromClipboard, readTextFromClipboard } from "../../utils/clipboard";
|
|
4
4
|
/**
|
|
5
5
|
* Slash commands that may carry secrets in their arguments should never be
|
|
6
6
|
* persisted to history.
|
|
@@ -24,6 +24,7 @@ export declare class InputController {
|
|
|
24
24
|
clipboard?: {
|
|
25
25
|
readImage: typeof readImageFromClipboard;
|
|
26
26
|
readText: typeof readTextFromClipboard;
|
|
27
|
+
readMacFileUrls?: typeof readMacFileUrlsFromClipboard;
|
|
27
28
|
});
|
|
28
29
|
setupKeyHandlers(): void;
|
|
29
30
|
setupEditorSubmitHandler(): void;
|
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
import type { ClipboardImage } from "@oh-my-pi/pi-natives";
|
|
2
|
+
/**
|
|
3
|
+
* Read file paths from the macOS pasteboard's `public.file-url` representation.
|
|
4
|
+
*
|
|
5
|
+
* Used to reach the Finder `Cmd+C` pasteboard (which exposes only file URLs,
|
|
6
|
+
* no plain text or raw image bytes) so an image-file clipboard can be attached
|
|
7
|
+
* via {@link handleImagePathPaste} instead of falling through to "Clipboard is
|
|
8
|
+
* empty". Returns an empty array on non-darwin platforms, when AppleScript is
|
|
9
|
+
* unavailable, or when the pasteboard holds no file URLs.
|
|
10
|
+
*/
|
|
11
|
+
export declare function readMacFileUrlsFromClipboard(): Promise<string[]>;
|
|
2
12
|
/**
|
|
3
13
|
* Copy text to the system clipboard.
|
|
4
14
|
*
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "16.1.
|
|
4
|
+
"version": "16.1.21",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -48,17 +48,17 @@
|
|
|
48
48
|
"@agentclientprotocol/sdk": "0.25.0",
|
|
49
49
|
"@babel/parser": "^7.29.7",
|
|
50
50
|
"@mozilla/readability": "^0.6.0",
|
|
51
|
-
"@oh-my-pi/hashline": "16.1.
|
|
52
|
-
"@oh-my-pi/omp-stats": "16.1.
|
|
53
|
-
"@oh-my-pi/pi-agent-core": "16.1.
|
|
54
|
-
"@oh-my-pi/pi-ai": "16.1.
|
|
55
|
-
"@oh-my-pi/pi-catalog": "16.1.
|
|
56
|
-
"@oh-my-pi/pi-mnemopi": "16.1.
|
|
57
|
-
"@oh-my-pi/pi-natives": "16.1.
|
|
58
|
-
"@oh-my-pi/pi-tui": "16.1.
|
|
59
|
-
"@oh-my-pi/pi-utils": "16.1.
|
|
60
|
-
"@oh-my-pi/pi-wire": "16.1.
|
|
61
|
-
"@oh-my-pi/snapcompact": "16.1.
|
|
51
|
+
"@oh-my-pi/hashline": "16.1.21",
|
|
52
|
+
"@oh-my-pi/omp-stats": "16.1.21",
|
|
53
|
+
"@oh-my-pi/pi-agent-core": "16.1.21",
|
|
54
|
+
"@oh-my-pi/pi-ai": "16.1.21",
|
|
55
|
+
"@oh-my-pi/pi-catalog": "16.1.21",
|
|
56
|
+
"@oh-my-pi/pi-mnemopi": "16.1.21",
|
|
57
|
+
"@oh-my-pi/pi-natives": "16.1.21",
|
|
58
|
+
"@oh-my-pi/pi-tui": "16.1.21",
|
|
59
|
+
"@oh-my-pi/pi-utils": "16.1.21",
|
|
60
|
+
"@oh-my-pi/pi-wire": "16.1.21",
|
|
61
|
+
"@oh-my-pi/snapcompact": "16.1.21",
|
|
62
62
|
"@opentelemetry/api": "^1.9.1",
|
|
63
63
|
"@opentelemetry/context-async-hooks": "^2.7.1",
|
|
64
64
|
"@opentelemetry/exporter-trace-otlp-proto": "^0.218.0",
|
|
@@ -117,6 +117,15 @@ function isSafeWildcardBasename(basename: string): boolean {
|
|
|
117
117
|
return true;
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
// Worker entry modules intentionally throw when imported outside a Worker. The
|
|
121
|
+
// bundled registry loads on the main thread during legacy extension validation,
|
|
122
|
+
// so these exported subpaths must stay out of the static registry.
|
|
123
|
+
const MAIN_THREAD_UNSAFE_WILDCARD_BASENAMES = new Set(["worker-entry"]);
|
|
124
|
+
|
|
125
|
+
function isMainThreadSafeWildcardBasename(basename: string): boolean {
|
|
126
|
+
return !MAIN_THREAD_UNSAFE_WILDCARD_BASENAMES.has(basename);
|
|
127
|
+
}
|
|
128
|
+
|
|
120
129
|
interface WildcardPattern {
|
|
121
130
|
readonly exportPrefix: string;
|
|
122
131
|
readonly exportSuffix: string;
|
|
@@ -217,6 +226,7 @@ async function collectEntries(): Promise<RegistryEntry[]> {
|
|
|
217
226
|
if (!match.endsWith(pattern.sourceSuffix)) continue;
|
|
218
227
|
const basename = match.slice(0, match.length - pattern.sourceSuffix.length);
|
|
219
228
|
if (!isSafeWildcardBasename(basename)) continue;
|
|
229
|
+
if (!isMainThreadSafeWildcardBasename(basename)) continue;
|
|
220
230
|
if (basename.includes("/")) continue;
|
|
221
231
|
const subpath = `${pattern.exportPrefix}${basename}${pattern.exportSuffix}`;
|
|
222
232
|
const key = `${pkg.name}/${subpath}`;
|
|
@@ -199,6 +199,50 @@ describe("advisor", () => {
|
|
|
199
199
|
expect(result.useless).toBe(true);
|
|
200
200
|
});
|
|
201
201
|
|
|
202
|
+
it("suppresses duplicate advice notes from the same advisor session", async () => {
|
|
203
|
+
const onAdvice = vi.fn();
|
|
204
|
+
const tool = new AdviseTool(onAdvice);
|
|
205
|
+
const note = "I'll pause here and wait for the YAML revision.";
|
|
206
|
+
|
|
207
|
+
await tool.execute("tc-1", { note, severity: "nit" });
|
|
208
|
+
await tool.execute("tc-2", { note, severity: "nit" });
|
|
209
|
+
|
|
210
|
+
expect(onAdvice).toHaveBeenCalledTimes(1);
|
|
211
|
+
expect(onAdvice).toHaveBeenCalledWith(note, "nit");
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("allows the same advice after delivered-note memory resets", async () => {
|
|
215
|
+
const onAdvice = vi.fn();
|
|
216
|
+
const tool = new AdviseTool(onAdvice);
|
|
217
|
+
const note = "Acknowledged.";
|
|
218
|
+
|
|
219
|
+
await tool.execute("tc-1", { note, severity: "nit" });
|
|
220
|
+
tool.resetDeliveredNotes();
|
|
221
|
+
await tool.execute("tc-2", { note, severity: "nit" });
|
|
222
|
+
|
|
223
|
+
expect(onAdvice).toHaveBeenCalledTimes(2);
|
|
224
|
+
expect(onAdvice).toHaveBeenNthCalledWith(1, note, "nit");
|
|
225
|
+
expect(onAdvice).toHaveBeenNthCalledWith(2, note, "nit");
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("forwards escalations of an already-delivered note and suppresses downgrades", async () => {
|
|
229
|
+
const onAdvice = vi.fn();
|
|
230
|
+
const tool = new AdviseTool(onAdvice);
|
|
231
|
+
const note = "Rename collides with the existing helper.";
|
|
232
|
+
|
|
233
|
+
await tool.execute("tc-1", { note, severity: "nit" });
|
|
234
|
+
await tool.execute("tc-2", { note, severity: "concern" });
|
|
235
|
+
await tool.execute("tc-3", { note, severity: "blocker" });
|
|
236
|
+
// De-escalation back to nit or concern is treated as a duplicate.
|
|
237
|
+
await tool.execute("tc-4", { note, severity: "concern" });
|
|
238
|
+
await tool.execute("tc-5", { note, severity: "nit" });
|
|
239
|
+
|
|
240
|
+
expect(onAdvice).toHaveBeenCalledTimes(3);
|
|
241
|
+
expect(onAdvice).toHaveBeenNthCalledWith(1, note, "nit");
|
|
242
|
+
expect(onAdvice).toHaveBeenNthCalledWith(2, note, "concern");
|
|
243
|
+
expect(onAdvice).toHaveBeenNthCalledWith(3, note, "blocker");
|
|
244
|
+
});
|
|
245
|
+
|
|
202
246
|
it("validates parameters using ArkType", () => {
|
|
203
247
|
const onAdvice = vi.fn();
|
|
204
248
|
const tool = new AdviseTool(onAdvice);
|
|
@@ -139,15 +139,37 @@ export function deriveAdvisorTelemetry(
|
|
|
139
139
|
*/
|
|
140
140
|
export const ADVISOR_READONLY_TOOL_NAMES: ReadonlySet<string> = new Set(["read", "search", "find"]);
|
|
141
141
|
|
|
142
|
+
function advisorNoteDedupeKey(note: string): string {
|
|
143
|
+
return note.trim().replace(/\s+/g, " ");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Rank advisor severities so the dedupe state can detect a real escalation
|
|
147
|
+
* (nit → concern → blocker) versus a verbatim repeat. `undefined` defers to
|
|
148
|
+
* `nit` because the schema treats an omitted severity as a plain nit. */
|
|
149
|
+
const ADVISOR_SEVERITY_RANK: Record<AdvisorSeverity, number> = { nit: 1, concern: 2, blocker: 3 };
|
|
150
|
+
function advisorSeverityRank(severity: AdvisorSeverity | undefined): number {
|
|
151
|
+
return ADVISOR_SEVERITY_RANK[severity ?? "nit"];
|
|
152
|
+
}
|
|
153
|
+
|
|
142
154
|
export class AdviseTool implements AgentTool<typeof adviseSchema, AdviseDetails> {
|
|
143
155
|
readonly name = "advise";
|
|
144
156
|
readonly label = "Advise";
|
|
145
157
|
readonly description = adviseDescription;
|
|
146
158
|
readonly parameters = adviseSchema;
|
|
147
159
|
readonly intent = "omit" as const;
|
|
160
|
+
/** Highest delivered severity rank per normalized note. A new call passes
|
|
161
|
+
* through only when its rank strictly exceeds the recorded one (a real
|
|
162
|
+
* escalation: nit → concern → blocker), so an advisor cannot bypass dedupe
|
|
163
|
+
* by retagging the same text at a lower or equal severity. */
|
|
164
|
+
#deliveredNoteSeverities = new Map<string, number>();
|
|
148
165
|
|
|
149
166
|
constructor(private readonly onAdvice: (note: string, severity?: AdviseDetails["severity"]) => void) {}
|
|
150
167
|
|
|
168
|
+
/** Clear delivered-note memory when the advisor starts a fresh conversation. */
|
|
169
|
+
resetDeliveredNotes(): void {
|
|
170
|
+
this.#deliveredNoteSeverities.clear();
|
|
171
|
+
}
|
|
172
|
+
|
|
151
173
|
async execute(
|
|
152
174
|
_toolCallId: string,
|
|
153
175
|
args: AdviseParams,
|
|
@@ -155,6 +177,17 @@ export class AdviseTool implements AgentTool<typeof adviseSchema, AdviseDetails>
|
|
|
155
177
|
_onUpdate?: AgentToolUpdateCallback<AdviseDetails>,
|
|
156
178
|
_context?: AgentToolContext,
|
|
157
179
|
): Promise<AgentToolResult<AdviseDetails>> {
|
|
180
|
+
const key = advisorNoteDedupeKey(args.note);
|
|
181
|
+
const rank = advisorSeverityRank(args.severity);
|
|
182
|
+
const previousRank = this.#deliveredNoteSeverities.get(key) ?? 0;
|
|
183
|
+
if (rank <= previousRank) {
|
|
184
|
+
return {
|
|
185
|
+
content: [{ type: "text", text: "Duplicate advice ignored." }],
|
|
186
|
+
details: { note: args.note, severity: args.severity },
|
|
187
|
+
useless: true,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
this.#deliveredNoteSeverities.set(key, rank);
|
|
158
191
|
this.onAdvice(args.note, args.severity);
|
|
159
192
|
return {
|
|
160
193
|
content: [{ type: "text", text: "Recorded." }],
|
|
@@ -15,9 +15,11 @@ import type { Settings } from "../config/settings";
|
|
|
15
15
|
import autolearnGuidance from "../prompts/system/autolearn-guidance.md" with { type: "text" };
|
|
16
16
|
import autolearnGuidanceLearn from "../prompts/system/autolearn-guidance-learn.md" with { type: "text" };
|
|
17
17
|
import autolearnNudge from "../prompts/system/autolearn-nudge.md" with { type: "text" };
|
|
18
|
+
import autolearnNudgeAutoContinue from "../prompts/system/autolearn-nudge-autocontinue.md" with { type: "text" };
|
|
18
19
|
import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
|
|
19
20
|
|
|
20
|
-
const
|
|
21
|
+
const AUTOLEARN_NUDGE_PASSIVE = autolearnNudge.trim();
|
|
22
|
+
const AUTOLEARN_NUDGE_AUTOCONTINUE = autolearnNudgeAutoContinue.trim();
|
|
21
23
|
const DEFAULT_MIN_TOOL_CALLS = 5;
|
|
22
24
|
|
|
23
25
|
/**
|
|
@@ -110,7 +112,20 @@ export class AutoLearnController {
|
|
|
110
112
|
|
|
111
113
|
// Auto-run a capture turn only when explicitly enabled; otherwise the
|
|
112
114
|
// hidden reminder rides the next real turn passively.
|
|
115
|
+
//
|
|
116
|
+
// The two paths get DIFFERENT nudge text:
|
|
117
|
+
// - Auto-continue spawns a synthetic turn with the nudge as its only
|
|
118
|
+
// user-role payload, so the prompt must be terminal — it tells the
|
|
119
|
+
// agent to capture and STOP, and must not be read as the user's
|
|
120
|
+
// reply to any pending question. Without that contract, the agent
|
|
121
|
+
// conflates the synthetic prompt with user approval and resumes
|
|
122
|
+
// prior work (e.g. commits/pushes an unanswered "want me to push?"
|
|
123
|
+
// question — #3504).
|
|
124
|
+
// - Passive mode appends the nudge to the user's real next message,
|
|
125
|
+
// so the agent must answer the user normally and treat the capture
|
|
126
|
+
// as additive — not as the whole turn's job.
|
|
113
127
|
const autoContinue = this.#settings.get("autolearn.autoContinue") === true;
|
|
128
|
+
const content = autoContinue ? AUTOLEARN_NUDGE_AUTOCONTINUE : AUTOLEARN_NUDGE_PASSIVE;
|
|
114
129
|
// Arm suppression synchronously: the synthetic capture turn's agent_end
|
|
115
130
|
// fires inside sendCustomMessage (before it resolves), so the flag must be
|
|
116
131
|
// set before then. Disarm when no turn actually started — a deferred/queued
|
|
@@ -122,7 +137,7 @@ export class AutoLearnController {
|
|
|
122
137
|
.sendCustomMessage(
|
|
123
138
|
{
|
|
124
139
|
customType: "autolearn-nudge",
|
|
125
|
-
content
|
|
140
|
+
content,
|
|
126
141
|
display: false,
|
|
127
142
|
attribution: "user",
|
|
128
143
|
},
|
|
@@ -360,6 +360,7 @@ export const BUNDLED_PI_REGISTRY_KEYS: ReadonlySet<string> = new Set([
|
|
|
360
360
|
"@oh-my-pi/pi-coding-agent/config/models-config",
|
|
361
361
|
"@oh-my-pi/pi-coding-agent/config/prompt-templates",
|
|
362
362
|
"@oh-my-pi/pi-coding-agent/config/resolve-config-value",
|
|
363
|
+
"@oh-my-pi/pi-coding-agent/config/service-tier",
|
|
363
364
|
"@oh-my-pi/pi-coding-agent/config/settings-schema",
|
|
364
365
|
"@oh-my-pi/pi-coding-agent/config/settings",
|
|
365
366
|
"@oh-my-pi/pi-coding-agent/dap/client",
|
|
@@ -483,7 +484,6 @@ export const BUNDLED_PI_REGISTRY_KEYS: ReadonlySet<string> = new Set([
|
|
|
483
484
|
"@oh-my-pi/pi-coding-agent/eval/js/executor",
|
|
484
485
|
"@oh-my-pi/pi-coding-agent/eval/js/tool-bridge",
|
|
485
486
|
"@oh-my-pi/pi-coding-agent/eval/js/worker-core",
|
|
486
|
-
"@oh-my-pi/pi-coding-agent/eval/js/worker-entry",
|
|
487
487
|
"@oh-my-pi/pi-coding-agent/eval/js/worker-protocol",
|
|
488
488
|
"@oh-my-pi/pi-coding-agent/eval/py/display",
|
|
489
489
|
"@oh-my-pi/pi-coding-agent/eval/py/executor",
|
|
@@ -328,6 +328,7 @@ import * as bundledPiCodingAgentConfigModelsConfig from "@oh-my-pi/pi-coding-age
|
|
|
328
328
|
import * as bundledPiCodingAgentConfigModelsConfigSchema from "@oh-my-pi/pi-coding-agent/config/models-config-schema";
|
|
329
329
|
import * as bundledPiCodingAgentConfigPromptTemplates from "@oh-my-pi/pi-coding-agent/config/prompt-templates";
|
|
330
330
|
import * as bundledPiCodingAgentConfigResolveConfigValue from "@oh-my-pi/pi-coding-agent/config/resolve-config-value";
|
|
331
|
+
import * as bundledPiCodingAgentConfigServiceTier from "@oh-my-pi/pi-coding-agent/config/service-tier";
|
|
331
332
|
import * as bundledPiCodingAgentConfigSettings from "@oh-my-pi/pi-coding-agent/config/settings";
|
|
332
333
|
import * as bundledPiCodingAgentConfigSettingsSchema from "@oh-my-pi/pi-coding-agent/config/settings-schema";
|
|
333
334
|
import * as bundledPiCodingAgentDap from "@oh-my-pi/pi-coding-agent/dap";
|
|
@@ -385,7 +386,6 @@ import * as bundledPiCodingAgentEvalJsContextManager from "@oh-my-pi/pi-coding-a
|
|
|
385
386
|
import * as bundledPiCodingAgentEvalJsExecutor from "@oh-my-pi/pi-coding-agent/eval/js/executor";
|
|
386
387
|
import * as bundledPiCodingAgentEvalJsToolBridge from "@oh-my-pi/pi-coding-agent/eval/js/tool-bridge";
|
|
387
388
|
import * as bundledPiCodingAgentEvalJsWorkerCore from "@oh-my-pi/pi-coding-agent/eval/js/worker-core";
|
|
388
|
-
import * as bundledPiCodingAgentEvalJsWorkerEntry from "@oh-my-pi/pi-coding-agent/eval/js/worker-entry";
|
|
389
389
|
import * as bundledPiCodingAgentEvalJsWorkerProtocol from "@oh-my-pi/pi-coding-agent/eval/js/worker-protocol";
|
|
390
390
|
import * as bundledPiCodingAgentEvalPyDisplay from "@oh-my-pi/pi-coding-agent/eval/py/display";
|
|
391
391
|
import * as bundledPiCodingAgentEvalPyExecutor from "@oh-my-pi/pi-coding-agent/eval/py/executor";
|
|
@@ -1802,6 +1802,9 @@ export const BUNDLED_PI_REGISTRY: Readonly<Record<string, Readonly<Record<string
|
|
|
1802
1802
|
bundledPiCodingAgentConfigPromptTemplates as unknown as Readonly<Record<string, unknown>>,
|
|
1803
1803
|
"@oh-my-pi/pi-coding-agent/config/resolve-config-value":
|
|
1804
1804
|
bundledPiCodingAgentConfigResolveConfigValue as unknown as Readonly<Record<string, unknown>>,
|
|
1805
|
+
"@oh-my-pi/pi-coding-agent/config/service-tier": bundledPiCodingAgentConfigServiceTier as unknown as Readonly<
|
|
1806
|
+
Record<string, unknown>
|
|
1807
|
+
>,
|
|
1805
1808
|
"@oh-my-pi/pi-coding-agent/config/settings-schema": bundledPiCodingAgentConfigSettingsSchema as unknown as Readonly<
|
|
1806
1809
|
Record<string, unknown>
|
|
1807
1810
|
>,
|
|
@@ -2100,9 +2103,6 @@ export const BUNDLED_PI_REGISTRY: Readonly<Record<string, Readonly<Record<string
|
|
|
2100
2103
|
"@oh-my-pi/pi-coding-agent/eval/js/worker-core": bundledPiCodingAgentEvalJsWorkerCore as unknown as Readonly<
|
|
2101
2104
|
Record<string, unknown>
|
|
2102
2105
|
>,
|
|
2103
|
-
"@oh-my-pi/pi-coding-agent/eval/js/worker-entry": bundledPiCodingAgentEvalJsWorkerEntry as unknown as Readonly<
|
|
2104
|
-
Record<string, unknown>
|
|
2105
|
-
>,
|
|
2106
2106
|
"@oh-my-pi/pi-coding-agent/eval/js/worker-protocol": bundledPiCodingAgentEvalJsWorkerProtocol as unknown as Readonly<
|
|
2107
2107
|
Record<string, unknown>
|
|
2108
2108
|
>,
|