@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.
Files changed (34) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/cli.js +2804 -2793
  3. package/dist/types/advisor/advise-tool.d.ts +3 -0
  4. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +2 -0
  5. package/dist/types/mcp/oauth-flow.d.ts +42 -5
  6. package/dist/types/modes/components/custom-editor.d.ts +32 -0
  7. package/dist/types/modes/components/tool-execution.d.ts +5 -5
  8. package/dist/types/modes/controllers/event-controller.d.ts +10 -0
  9. package/dist/types/modes/controllers/input-controller.d.ts +2 -1
  10. package/dist/types/utils/clipboard.d.ts +10 -0
  11. package/package.json +12 -12
  12. package/scripts/generate-legacy-pi-bundled-registry.ts +10 -0
  13. package/src/advisor/__tests__/advisor.test.ts +44 -0
  14. package/src/advisor/advise-tool.ts +33 -0
  15. package/src/autolearn/controller.ts +17 -2
  16. package/src/extensibility/plugins/legacy-pi-bundled-keys.ts +1 -1
  17. package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +4 -4
  18. package/src/extensibility/plugins/legacy-pi-compat.ts +193 -5
  19. package/src/mcp/manager.ts +12 -3
  20. package/src/mcp/oauth-flow.ts +121 -7
  21. package/src/modes/components/chat-transcript-builder.ts +31 -0
  22. package/src/modes/components/custom-editor.test.ts +80 -0
  23. package/src/modes/components/custom-editor.ts +86 -6
  24. package/src/modes/components/tool-execution.ts +50 -25
  25. package/src/modes/controllers/event-controller.ts +57 -8
  26. package/src/modes/controllers/input-controller.ts +70 -27
  27. package/src/modes/controllers/mcp-command-controller.ts +18 -2
  28. package/src/modes/utils/ui-helpers.ts +40 -0
  29. package/src/prompts/system/autolearn-nudge-autocontinue.md +5 -0
  30. package/src/prompts/system/autolearn-nudge.md +4 -2
  31. package/src/prompts/tools/todo.md +1 -1
  32. package/src/session/agent-session.ts +4 -0
  33. package/src/tools/todo.ts +20 -10
  34. 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 waiting `job` poll (every watched job still
111
- * running) that has not been sealed. Such a block never finalized, so none
112
- * of its rows entered native scrollback (the ticking spinner keeps the
113
- * stable-prefix ratchet at zero) and the whole block can be removed when a
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.20",
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.20",
52
- "@oh-my-pi/omp-stats": "16.1.20",
53
- "@oh-my-pi/pi-agent-core": "16.1.20",
54
- "@oh-my-pi/pi-ai": "16.1.20",
55
- "@oh-my-pi/pi-catalog": "16.1.20",
56
- "@oh-my-pi/pi-mnemopi": "16.1.20",
57
- "@oh-my-pi/pi-natives": "16.1.20",
58
- "@oh-my-pi/pi-tui": "16.1.20",
59
- "@oh-my-pi/pi-utils": "16.1.20",
60
- "@oh-my-pi/pi-wire": "16.1.20",
61
- "@oh-my-pi/snapcompact": "16.1.20",
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 AUTOLEARN_NUDGE = autolearnNudge.trim();
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: AUTOLEARN_NUDGE,
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
  >,