@oh-my-pi/pi-coding-agent 16.2.8 → 16.2.9
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 +30 -0
- package/dist/cli.js +3445 -3382
- package/dist/types/config/settings-schema.d.ts +41 -13
- package/dist/types/modes/components/mcp-add-wizard.d.ts +8 -0
- package/dist/types/modes/controllers/mcp-command-controller.d.ts +9 -0
- package/dist/types/session/agent-session.d.ts +1 -1
- package/dist/types/stt/index.d.ts +1 -0
- package/dist/types/stt/stt-controller.d.ts +2 -0
- package/dist/types/stt/submit-trigger.d.ts +30 -0
- package/dist/types/task/index.d.ts +1 -1
- package/dist/types/task/types.d.ts +6 -6
- package/dist/types/tiny/models.d.ts +22 -8
- package/package.json +12 -12
- package/src/commit/agentic/agent.ts +1 -1
- package/src/commit/agentic/prompts/system.md +1 -1
- package/src/commit/agentic/tools/analyze-file.ts +2 -2
- package/src/config/settings-schema.ts +15 -1
- package/src/debug/profiler.ts +7 -1
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/mcp/oauth-flow.ts +35 -8
- package/src/modes/components/mcp-add-wizard.ts +43 -3
- package/src/modes/components/model-selector.ts +21 -9
- package/src/modes/controllers/event-controller.ts +9 -0
- package/src/modes/controllers/mcp-command-controller.ts +84 -3
- package/src/modes/interactive-mode.ts +5 -4
- package/src/prompts/agents/tester.md +107 -0
- package/src/prompts/system/orchestrate-notice.md +2 -2
- package/src/prompts/system/system-prompt.md +2 -5
- package/src/prompts/system/thinking-loop-redirect.md +10 -0
- package/src/prompts/system/workflow-notice.md +1 -1
- package/src/prompts/tools/task.md +2 -9
- package/src/session/agent-session.ts +53 -18
- package/src/stt/index.ts +1 -0
- package/src/stt/stt-controller.ts +31 -2
- package/src/stt/submit-trigger.ts +74 -0
- package/src/task/agents.ts +4 -4
- package/src/task/executor.ts +1 -1
- package/src/task/index.ts +18 -5
- package/src/task/types.ts +5 -5
- package/src/tiny/models.ts +10 -0
- package/src/prompts/agents/oracle.md +0 -54
package/src/mcp/oauth-flow.ts
CHANGED
|
@@ -153,14 +153,44 @@ function resolveCallbackHostname(redirectUri: string | undefined): string | unde
|
|
|
153
153
|
return parsed.hostname;
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
+
/**
|
|
157
|
+
* Resolve the client_id MCPOAuthFlow would use without doing any I/O —
|
|
158
|
+
* either the explicitly configured value or one embedded as a query parameter
|
|
159
|
+
* in the authorization URL. Returns `undefined` when no client_id is known
|
|
160
|
+
* statically, which is the trigger for dynamic client registration in
|
|
161
|
+
* {@link MCPOAuthFlow.#tryRegisterClient}.
|
|
162
|
+
*/
|
|
163
|
+
function staticClientIdFromConfig(config: MCPOAuthConfig): string | undefined {
|
|
164
|
+
const fromConfig = config.clientId?.trim();
|
|
165
|
+
if (fromConfig) return fromConfig;
|
|
166
|
+
try {
|
|
167
|
+
return new URL(config.authorizationUrl).searchParams.get("client_id") ?? undefined;
|
|
168
|
+
} catch {
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
156
173
|
function resolveCallbackOptions(config: MCPOAuthConfig): OAuthCallbackFlowOptions {
|
|
157
174
|
const redirectUri = resolveRedirectUri(config.redirectUri);
|
|
158
175
|
validateRedirectConfig(config, redirectUri);
|
|
176
|
+
// When a client_id is already pinned (config-supplied or embedded in the
|
|
177
|
+
// authorization URL), it was registered against a specific redirect URI.
|
|
178
|
+
// Silently advertising a different port at the authorize endpoint would
|
|
179
|
+
// be rejected by providers like Atlassian (HTTP 500 in the browser, local
|
|
180
|
+
// flow hangs until the 5-minute timeout), so fail fast instead.
|
|
181
|
+
//
|
|
182
|
+
// When no client_id is pinned, MCPOAuthFlow will attempt dynamic client
|
|
183
|
+
// registration on demand with whichever loopback URI we actually bound —
|
|
184
|
+
// the provider issues a client_id tied to *that* URI, so the random-port
|
|
185
|
+
// fallback remains safe for first-install DCR flows whose preferred port
|
|
186
|
+
// happens to be occupied.
|
|
187
|
+
const allowPortFallback = staticClientIdFromConfig(config) === undefined;
|
|
159
188
|
return {
|
|
160
189
|
preferredPort: resolveCallbackPort(config.callbackPort, redirectUri),
|
|
161
190
|
callbackPath: resolveCallbackPath(config.callbackPath, redirectUri),
|
|
162
191
|
callbackHostname: resolveCallbackHostname(redirectUri),
|
|
163
192
|
redirectUri,
|
|
193
|
+
allowPortFallback,
|
|
164
194
|
};
|
|
165
195
|
}
|
|
166
196
|
|
|
@@ -400,6 +430,7 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
|
|
|
400
430
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
401
431
|
},
|
|
402
432
|
body: params.toString(),
|
|
433
|
+
signal: this.ctrl.signal,
|
|
403
434
|
});
|
|
404
435
|
|
|
405
436
|
if (!response.ok) {
|
|
@@ -453,14 +484,7 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
|
|
|
453
484
|
}
|
|
454
485
|
|
|
455
486
|
#resolveClientId(config: MCPOAuthConfig): string | undefined {
|
|
456
|
-
|
|
457
|
-
if (fromConfig) return fromConfig;
|
|
458
|
-
|
|
459
|
-
try {
|
|
460
|
-
return new URL(config.authorizationUrl).searchParams.get("client_id") ?? undefined;
|
|
461
|
-
} catch {
|
|
462
|
-
return undefined;
|
|
463
|
-
}
|
|
487
|
+
return staticClientIdFromConfig(config);
|
|
464
488
|
}
|
|
465
489
|
#resourceFromAuthorizationUrl(authorizationUrl: string): string | undefined {
|
|
466
490
|
try {
|
|
@@ -495,6 +519,7 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
|
|
|
495
519
|
"Content-Type": "application/json",
|
|
496
520
|
Accept: "application/json",
|
|
497
521
|
},
|
|
522
|
+
signal: this.ctrl.signal,
|
|
498
523
|
body: JSON.stringify({
|
|
499
524
|
client_name: "oh-my-pi",
|
|
500
525
|
redirect_uris: [redirectUri],
|
|
@@ -560,6 +585,7 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
|
|
|
560
585
|
const response = await this.#fetch(wellKnownUrl, {
|
|
561
586
|
method: "GET",
|
|
562
587
|
headers: { Accept: "application/json" },
|
|
588
|
+
signal: this.ctrl.signal,
|
|
563
589
|
});
|
|
564
590
|
if (!response.ok) return null;
|
|
565
591
|
const metadata = (await response.json()) as { registration_endpoint?: string };
|
|
@@ -578,6 +604,7 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
|
|
|
578
604
|
method: "GET",
|
|
579
605
|
redirect: "manual",
|
|
580
606
|
headers: { Accept: "text/plain,text/html,application/json" },
|
|
607
|
+
signal: this.ctrl.signal,
|
|
581
608
|
});
|
|
582
609
|
if (response.status < 400) return;
|
|
583
610
|
const body = await response.text();
|
|
@@ -63,6 +63,14 @@ export interface MCPAddWizardOAuthResult {
|
|
|
63
63
|
interface MCPAddWizardOAuthOptions {
|
|
64
64
|
serverUrl?: string;
|
|
65
65
|
resource?: string;
|
|
66
|
+
/**
|
|
67
|
+
* External cancellation source. Aborting it tears down the in-flight OAuth
|
|
68
|
+
* flow and surfaces a neutral cancellation error. The wizard wires its own
|
|
69
|
+
* controller here so Esc cancels the OAuth wait instead of stepping back
|
|
70
|
+
* through the form (the wizard is focused, so the editor's Esc hook does
|
|
71
|
+
* not fire).
|
|
72
|
+
*/
|
|
73
|
+
abortSignal?: AbortSignal;
|
|
66
74
|
}
|
|
67
75
|
|
|
68
76
|
interface WizardState {
|
|
@@ -135,6 +143,12 @@ export class MCPAddWizard extends Container {
|
|
|
135
143
|
| null = null;
|
|
136
144
|
#onTestConnectionCallback: ((config: MCPServerConfig) => Promise<void>) | null = null;
|
|
137
145
|
#onRenderCallback: (() => void) | null = null;
|
|
146
|
+
/**
|
|
147
|
+
* Set while the OAuth callback is in flight; populated by
|
|
148
|
+
* {@link #launchOAuthFlow} and consumed by {@link handleInput} so Esc
|
|
149
|
+
* cancels the OAuth wait instead of stepping back through the form.
|
|
150
|
+
*/
|
|
151
|
+
#oauthAbort: AbortController | null = null;
|
|
138
152
|
|
|
139
153
|
constructor(
|
|
140
154
|
onComplete: (name: string, config: MCPServerConfig, scope: Scope) => void,
|
|
@@ -473,6 +487,15 @@ export class MCPAddWizard extends Container {
|
|
|
473
487
|
}
|
|
474
488
|
|
|
475
489
|
handleInput(keyData: string): void {
|
|
490
|
+
// While an OAuth callback is being awaited, Esc/Ctrl+C aborts the flow
|
|
491
|
+
// rather than stepping back through the form: the wizard advertises
|
|
492
|
+
// "(Press Esc to cancel)" during the wait, and stepping back would
|
|
493
|
+
// leave the OAuth login orphaned.
|
|
494
|
+
if (this.#oauthAbort && (keyData === "\x03" || matchesAppInterrupt(keyData))) {
|
|
495
|
+
this.#oauthAbort.abort("MCP OAuth flow cancelled by user");
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
|
|
476
499
|
// Handle Ctrl+C to cancel wizard immediately
|
|
477
500
|
if (keyData === "\x03") {
|
|
478
501
|
// Ctrl+C pressed - cancel wizard
|
|
@@ -1153,6 +1176,7 @@ export class MCPAddWizard extends Container {
|
|
|
1153
1176
|
this.#contentContainer.addChild(new Text(theme.fg("muted", "(Press Esc to cancel)"), 0, 0));
|
|
1154
1177
|
this.#requestRender();
|
|
1155
1178
|
|
|
1179
|
+
this.#oauthAbort = new AbortController();
|
|
1156
1180
|
try {
|
|
1157
1181
|
// Call OAuth handler
|
|
1158
1182
|
const oauthResource = this.#state.oauthResource || (this.#state.transport === "stdio" ? "" : this.#state.url);
|
|
@@ -1165,6 +1189,7 @@ export class MCPAddWizard extends Container {
|
|
|
1165
1189
|
{
|
|
1166
1190
|
serverUrl: this.#state.url || undefined,
|
|
1167
1191
|
resource: oauthResource || undefined,
|
|
1192
|
+
abortSignal: this.#oauthAbort.signal,
|
|
1168
1193
|
},
|
|
1169
1194
|
);
|
|
1170
1195
|
|
|
@@ -1237,16 +1262,29 @@ export class MCPAddWizard extends Container {
|
|
|
1237
1262
|
healthPassed ? 1000 : 2000,
|
|
1238
1263
|
);
|
|
1239
1264
|
} catch (error) {
|
|
1240
|
-
//
|
|
1265
|
+
// User cancellation has its own neutral heading + tip; everything else
|
|
1266
|
+
// keeps the "OAuth authentication failed" framing so the existing tips
|
|
1267
|
+
// stay meaningful. Name-matching avoids importing controller types.
|
|
1268
|
+
const cancelled = error instanceof Error && error.name === "MCPOAuthCancelledError";
|
|
1241
1269
|
const errorMsg = sanitize(error instanceof Error ? error.message : String(error));
|
|
1242
1270
|
this.#contentContainer.clear();
|
|
1243
|
-
this.#contentContainer.addChild(
|
|
1271
|
+
this.#contentContainer.addChild(
|
|
1272
|
+
new Text(
|
|
1273
|
+
cancelled ? theme.fg("muted", "○ OAuth cancelled") : theme.fg("error", "✗ OAuth authentication failed"),
|
|
1274
|
+
0,
|
|
1275
|
+
0,
|
|
1276
|
+
),
|
|
1277
|
+
);
|
|
1244
1278
|
this.#contentContainer.addChild(new Spacer(1));
|
|
1245
1279
|
this.#contentContainer.addChild(new Text(errorMsg, 0, 0));
|
|
1246
1280
|
this.#contentContainer.addChild(new Spacer(1));
|
|
1247
1281
|
|
|
1248
1282
|
// Provide helpful tips based on error type
|
|
1249
|
-
if (
|
|
1283
|
+
if (cancelled) {
|
|
1284
|
+
this.#contentContainer.addChild(
|
|
1285
|
+
new Text(theme.fg("muted", "Tip: Choose Retry to launch the browser again."), 0, 0),
|
|
1286
|
+
);
|
|
1287
|
+
} else if (errorMsg.includes("timeout") || errorMsg.includes("timed out")) {
|
|
1250
1288
|
this.#contentContainer.addChild(
|
|
1251
1289
|
new Text(theme.fg("muted", "Tip: Complete authorization faster next time"), 0, 0),
|
|
1252
1290
|
);
|
|
@@ -1272,6 +1310,8 @@ export class MCPAddWizard extends Container {
|
|
|
1272
1310
|
// Set up as a selector step
|
|
1273
1311
|
this.#selectedIndex = 0;
|
|
1274
1312
|
this.#currentStep = "oauth-error";
|
|
1313
|
+
} finally {
|
|
1314
|
+
this.#oauthAbort = null;
|
|
1275
1315
|
}
|
|
1276
1316
|
}
|
|
1277
1317
|
|
|
@@ -24,7 +24,12 @@ import { getKnownRoleIds, getRoleInfo, MODEL_ROLE_IDS, MODEL_ROLES } from "../..
|
|
|
24
24
|
import type { Settings } from "../../config/settings";
|
|
25
25
|
import { type ThemeColor, theme } from "../../modes/theme/theme";
|
|
26
26
|
import { matchesSelectDown, matchesSelectUp } from "../../modes/utils/keybinding-matchers";
|
|
27
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
AUTO_THINKING,
|
|
29
|
+
type ConfiguredThinkingLevel,
|
|
30
|
+
getConfiguredThinkingLevelMetadata,
|
|
31
|
+
parseConfiguredThinkingLevel,
|
|
32
|
+
} from "../../thinking";
|
|
28
33
|
import { getTabBarTheme } from "../shared";
|
|
29
34
|
import { DynamicBorder } from "./dynamic-border";
|
|
30
35
|
|
|
@@ -342,10 +347,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
342
347
|
if (resolved.model) {
|
|
343
348
|
nextRoles[role] = {
|
|
344
349
|
model: resolved.model,
|
|
345
|
-
thinkingLevel:
|
|
346
|
-
resolved.explicitThinkingLevel && resolved.thinkingLevel !== undefined
|
|
347
|
-
? resolved.thinkingLevel
|
|
348
|
-
: ThinkingLevel.Inherit,
|
|
350
|
+
thinkingLevel: this.#getResolvedRoleThinkingLevel(role, resolved),
|
|
349
351
|
autoSelected: false,
|
|
350
352
|
};
|
|
351
353
|
}
|
|
@@ -363,10 +365,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
363
365
|
if (!resolved.model) continue;
|
|
364
366
|
nextRoles[role] = {
|
|
365
367
|
model: resolved.model,
|
|
366
|
-
thinkingLevel:
|
|
367
|
-
resolved.explicitThinkingLevel && resolved.thinkingLevel !== undefined
|
|
368
|
-
? resolved.thinkingLevel
|
|
369
|
-
: ThinkingLevel.Inherit,
|
|
368
|
+
thinkingLevel: this.#getResolvedRoleThinkingLevel(role, resolved),
|
|
370
369
|
autoSelected: true,
|
|
371
370
|
};
|
|
372
371
|
}
|
|
@@ -1059,6 +1058,19 @@ export class ModelSelectorComponent extends Container {
|
|
|
1059
1058
|
);
|
|
1060
1059
|
}
|
|
1061
1060
|
}
|
|
1061
|
+
#getResolvedRoleThinkingLevel(
|
|
1062
|
+
role: string,
|
|
1063
|
+
resolved: { explicitThinkingLevel: boolean; thinkingLevel?: ThinkingLevel },
|
|
1064
|
+
): ConfiguredThinkingLevel {
|
|
1065
|
+
if (resolved.explicitThinkingLevel && resolved.thinkingLevel !== undefined) {
|
|
1066
|
+
return resolved.thinkingLevel;
|
|
1067
|
+
}
|
|
1068
|
+
if (role === "default") {
|
|
1069
|
+
return parseConfiguredThinkingLevel(this.#settings.get("defaultThinkingLevel")) ?? ThinkingLevel.Inherit;
|
|
1070
|
+
}
|
|
1071
|
+
return ThinkingLevel.Inherit;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1062
1074
|
#getThinkingLevelsForModel(model: Model): ReadonlyArray<ConfiguredThinkingLevel> {
|
|
1063
1075
|
return [ThinkingLevel.Inherit, ThinkingLevel.Off, AUTO_THINKING, ...getSupportedEfforts(model)];
|
|
1064
1076
|
}
|
|
@@ -609,6 +609,15 @@ export class EventController {
|
|
|
609
609
|
}
|
|
610
610
|
for (const content of this.ctx.streamingMessage.content) {
|
|
611
611
|
if (content.type !== "toolCall") continue;
|
|
612
|
+
// Anthropic/OpenAI open a streamed tool block with an empty id (and
|
|
613
|
+
// `{}` args) before the id/arguments arrive; Gemini assembles the
|
|
614
|
+
// whole call first, so it never hits this. Keying `pendingTools` by
|
|
615
|
+
// "" would create a placeholder card, and the later real-id frame —
|
|
616
|
+
// `pendingTools.has(realId)` false — would create a SECOND card,
|
|
617
|
+
// orphaning the blank one (no `tool_execution_*` event ever carries
|
|
618
|
+
// "", so it is never matched, updated, or removed). Defer until the
|
|
619
|
+
// provider assigns the real id.
|
|
620
|
+
if (!content.id) continue;
|
|
612
621
|
if (content.name === "read") {
|
|
613
622
|
if (!readArgsHaveTarget(content.arguments)) {
|
|
614
623
|
// Args still streaming — defer until path is parseable so we can route to the
|
|
@@ -62,6 +62,16 @@ function withTimeout<T>(promise: Promise<T>, timeoutMs: number, message: string,
|
|
|
62
62
|
}, timeoutMs);
|
|
63
63
|
return Promise.race([promise, timeoutPromise]).finally(() => clearTimeout(timer));
|
|
64
64
|
}
|
|
65
|
+
function raceAbortSignal<T>(promise: Promise<T>, signal: AbortSignal, createError: () => Error): Promise<T> {
|
|
66
|
+
if (signal.aborted) return Promise.reject(createError());
|
|
67
|
+
|
|
68
|
+
const aborted = Promise.withResolvers<never>();
|
|
69
|
+
const onAbort = (): void => aborted.reject(createError());
|
|
70
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
71
|
+
return Promise.race([promise, aborted.promise]).finally(() => {
|
|
72
|
+
signal.removeEventListener("abort", onAbort);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
65
75
|
|
|
66
76
|
/** Renders the MCP OAuth fallback URL without hard-wrapping the copy target. */
|
|
67
77
|
export class MCPAuthorizationLinkPrompt implements Component {
|
|
@@ -135,6 +145,22 @@ interface OAuthFlowResult {
|
|
|
135
145
|
resource?: string;
|
|
136
146
|
}
|
|
137
147
|
|
|
148
|
+
/**
|
|
149
|
+
* Thrown by {@link MCPCommandController}'s OAuth handler when the user (or a
|
|
150
|
+
* caller-supplied {@link AbortSignal}) cancels the in-flight flow. Distinct
|
|
151
|
+
* from network/timeout failures so callers can surface a neutral
|
|
152
|
+
* "cancelled" status instead of an error banner.
|
|
153
|
+
*/
|
|
154
|
+
export class MCPOAuthCancelledError extends Error {
|
|
155
|
+
constructor(message = "OAuth flow cancelled") {
|
|
156
|
+
super(message);
|
|
157
|
+
this.name = "MCPOAuthCancelledError";
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Reason recorded on the OAuth flow's AbortController when the user hits Esc. */
|
|
162
|
+
const MCP_OAUTH_USER_CANCEL_REASON = "MCP OAuth flow cancelled by user";
|
|
163
|
+
|
|
138
164
|
type MCPAddScope = "user" | "project";
|
|
139
165
|
type MCPAddTransport = "http" | "sse";
|
|
140
166
|
|
|
@@ -521,6 +547,10 @@ export class MCPCommandController {
|
|
|
521
547
|
userClientSecret: finalConfig.oauth?.clientSecret,
|
|
522
548
|
});
|
|
523
549
|
} catch (oauthError) {
|
|
550
|
+
if (oauthError instanceof MCPOAuthCancelledError) {
|
|
551
|
+
this.ctx.showStatus(`Add cancelled for "${parsed.initialName}"`);
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
524
554
|
this.ctx.showError(
|
|
525
555
|
`OAuth flow failed for "${parsed.initialName}": ${oauthError instanceof Error ? oauthError.message : String(oauthError)}`,
|
|
526
556
|
);
|
|
@@ -587,6 +617,14 @@ export class MCPCommandController {
|
|
|
587
617
|
serverUrl?: string;
|
|
588
618
|
resource?: string;
|
|
589
619
|
stripSameOriginResource?: boolean;
|
|
620
|
+
/**
|
|
621
|
+
* External cancellation source: when this signal aborts, the in-flight
|
|
622
|
+
* OAuth flow is torn down and {@link MCPOAuthCancelledError} is thrown.
|
|
623
|
+
* Wizards (which own focus and absorb Esc themselves) pass their own
|
|
624
|
+
* controller here; editor-focused callers rely on the Esc hook
|
|
625
|
+
* installed below instead.
|
|
626
|
+
*/
|
|
627
|
+
abortSignal?: AbortSignal;
|
|
590
628
|
},
|
|
591
629
|
): Promise<OAuthFlowResult> {
|
|
592
630
|
const authStorage = this.ctx.session.modelRegistry.authStorage;
|
|
@@ -614,6 +652,26 @@ export class MCPCommandController {
|
|
|
614
652
|
}
|
|
615
653
|
let manualInputClaim: { promise: Promise<string>; clear: (reason?: string) => void } | undefined;
|
|
616
654
|
const oauthTimeout = new AbortController();
|
|
655
|
+
// User Esc and external aborts route through here; the timeout path sets
|
|
656
|
+
// its own reason and leaves this flag false so the catch can distinguish
|
|
657
|
+
// "user cancelled" (status) from "deadline elapsed" (error).
|
|
658
|
+
let userCancelled = false;
|
|
659
|
+
const requestUserCancel = (reason: string): void => {
|
|
660
|
+
userCancelled = true;
|
|
661
|
+
if (!oauthTimeout.signal.aborted) oauthTimeout.abort(reason);
|
|
662
|
+
};
|
|
663
|
+
const originalOnEscape = this.ctx.editor.onEscape;
|
|
664
|
+
this.ctx.editor.onEscape = () => requestUserCancel(MCP_OAUTH_USER_CANCEL_REASON);
|
|
665
|
+
const externalSignal = opts?.abortSignal;
|
|
666
|
+
const onExternalAbort = (): void => {
|
|
667
|
+
const reason = externalSignal?.reason;
|
|
668
|
+
requestUserCancel(typeof reason === "string" ? reason : MCP_OAUTH_USER_CANCEL_REASON);
|
|
669
|
+
};
|
|
670
|
+
if (externalSignal?.aborted) {
|
|
671
|
+
onExternalAbort();
|
|
672
|
+
} else {
|
|
673
|
+
externalSignal?.addEventListener("abort", onExternalAbort, { once: true });
|
|
674
|
+
}
|
|
617
675
|
try {
|
|
618
676
|
// Create OAuth flow
|
|
619
677
|
const flow = new MCPOAuthFlow(
|
|
@@ -641,7 +699,7 @@ export class MCPCommandController {
|
|
|
641
699
|
block.addChild(new Spacer(1));
|
|
642
700
|
block.addChild(
|
|
643
701
|
new Text(
|
|
644
|
-
theme.fg("muted", "Waiting for authorization... (Press
|
|
702
|
+
theme.fg("muted", "Waiting for authorization... (Press Esc to cancel, 5 minute timeout)"),
|
|
645
703
|
1,
|
|
646
704
|
0,
|
|
647
705
|
),
|
|
@@ -687,9 +745,18 @@ export class MCPCommandController {
|
|
|
687
745
|
},
|
|
688
746
|
);
|
|
689
747
|
|
|
690
|
-
|
|
748
|
+
const createAbortError = (): Error => {
|
|
749
|
+
const reason = String(oauthTimeout.signal.reason ?? "MCP OAuth flow aborted");
|
|
750
|
+
return userCancelled ? new MCPOAuthCancelledError() : new Error(reason);
|
|
751
|
+
};
|
|
752
|
+
if (oauthTimeout.signal.aborted) throw createAbortError();
|
|
753
|
+
|
|
754
|
+
// Execute OAuth flow with 5 minute timeout. Race the login itself
|
|
755
|
+
// against the abort signal because Esc/external abort may fire before
|
|
756
|
+
// MCPOAuthFlow reaches OAuthCallbackFlow.#waitForCallback, where the
|
|
757
|
+
// underlying callback server normally observes the signal.
|
|
691
758
|
const credentials = await withTimeout(
|
|
692
|
-
flow.login(),
|
|
759
|
+
raceAbortSignal(flow.login(), oauthTimeout.signal, createAbortError),
|
|
693
760
|
5 * 60 * 1000,
|
|
694
761
|
"OAuth flow timed out after 5 minutes",
|
|
695
762
|
() => oauthTimeout.abort("MCP OAuth flow timed out"),
|
|
@@ -727,6 +794,14 @@ export class MCPCommandController {
|
|
|
727
794
|
resource: flow.resource,
|
|
728
795
|
};
|
|
729
796
|
} catch (error) {
|
|
797
|
+
// User-initiated cancel (Esc or external signal) → neutral status, not
|
|
798
|
+
// a failure. Check the flag we set in `requestUserCancel`, not the
|
|
799
|
+
// abort reason: the timeout path also aborts but with a different
|
|
800
|
+
// reason, and we want it to surface as a timeout error below.
|
|
801
|
+
if (userCancelled) {
|
|
802
|
+
throw new MCPOAuthCancelledError();
|
|
803
|
+
}
|
|
804
|
+
|
|
730
805
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
731
806
|
|
|
732
807
|
// Provide helpful error messages based on failure type
|
|
@@ -742,6 +817,8 @@ export class MCPCommandController {
|
|
|
742
817
|
throw new Error(`OAuth authentication failed: ${errorMsg}`);
|
|
743
818
|
}
|
|
744
819
|
} finally {
|
|
820
|
+
this.ctx.editor.onEscape = originalOnEscape;
|
|
821
|
+
externalSignal?.removeEventListener("abort", onExternalAbort);
|
|
745
822
|
manualInputClaim?.clear("Manual MCP OAuth input cleared");
|
|
746
823
|
}
|
|
747
824
|
}
|
|
@@ -1629,6 +1706,10 @@ export class MCPCommandController {
|
|
|
1629
1706
|
];
|
|
1630
1707
|
this.#showMessage(lines.join("\n"));
|
|
1631
1708
|
} catch (error) {
|
|
1709
|
+
if (error instanceof MCPOAuthCancelledError) {
|
|
1710
|
+
this.ctx.showStatus(`Reauthorization cancelled for "${name}"`);
|
|
1711
|
+
return;
|
|
1712
|
+
}
|
|
1632
1713
|
this.ctx.showError(`Failed to reauthorize server: ${error instanceof Error ? error.message : String(error)}`);
|
|
1633
1714
|
}
|
|
1634
1715
|
}
|
|
@@ -100,6 +100,7 @@ import { formatDuration } from "../slash-commands/helpers/format";
|
|
|
100
100
|
import { STTController, type SttState } from "../stt";
|
|
101
101
|
import { discoverTitleSystemPromptFile, resolvePromptInput } from "../system-prompt";
|
|
102
102
|
import { formatTaskId } from "../task/render";
|
|
103
|
+
import type { ConfiguredThinkingLevel } from "../thinking";
|
|
103
104
|
import type { LspStartupServerInfo } from "../tools";
|
|
104
105
|
import { normalizeLocalScheme } from "../tools/path-utils";
|
|
105
106
|
import { replaceTabs, TRUNCATE_LENGTHS, truncateToWidth } from "../tools/render-utils";
|
|
@@ -493,8 +494,8 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
493
494
|
#goalTurnHadToolCalls = false;
|
|
494
495
|
#goalContinuationTurnInFlight = false;
|
|
495
496
|
#goalSuppressNextContinuation = false;
|
|
496
|
-
#planModePreviousModelState: { model: Model; thinkingLevel?:
|
|
497
|
-
#pendingModelSwitch: { model: Model; thinkingLevel?:
|
|
497
|
+
#planModePreviousModelState: { model: Model; thinkingLevel?: ConfiguredThinkingLevel } | undefined;
|
|
498
|
+
#pendingModelSwitch: { model: Model; thinkingLevel?: ConfiguredThinkingLevel } | undefined;
|
|
498
499
|
#planModeHasEntered = false;
|
|
499
500
|
#planReviewOverlay: PlanReviewOverlay | undefined;
|
|
500
501
|
#planReviewOverlayHandle: OverlayHandle | undefined;
|
|
@@ -1917,7 +1918,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1917
1918
|
const planThinkingLevel = resolved.explicitThinkingLevel ? resolved.thinkingLevel : undefined;
|
|
1918
1919
|
|
|
1919
1920
|
this.#planModePreviousModelState = currentModel
|
|
1920
|
-
? { model: currentModel, thinkingLevel: this.session.
|
|
1921
|
+
? { model: currentModel, thinkingLevel: this.session.configuredThinkingLevel() }
|
|
1921
1922
|
: undefined;
|
|
1922
1923
|
|
|
1923
1924
|
if (!sameModel) {
|
|
@@ -2125,7 +2126,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2125
2126
|
});
|
|
2126
2127
|
}
|
|
2127
2128
|
|
|
2128
|
-
async #restorePlanPreviousModel(prev: { model: Model; thinkingLevel?:
|
|
2129
|
+
async #restorePlanPreviousModel(prev: { model: Model; thinkingLevel?: ConfiguredThinkingLevel }): Promise<void> {
|
|
2129
2130
|
if (modelsAreEqual(this.session.model, prev.model)) {
|
|
2130
2131
|
// Same model — only thinking level may differ. Avoid setModelTemporary()
|
|
2131
2132
|
// which would reset provider-side sessions and break continuity.
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Tester
|
|
3
|
+
description: Authoritative test writer. ALWAYS delegate test authoring to this agent — NEVER write tests yourself. Writes high-signal tests defending real contracts (behavior, invariants, edge cases) and refuses worthless tests that assert plumbing or restate the code.
|
|
4
|
+
tools: read, grep, glob, bash, edit, write, lsp, ast_grep, ast_edit
|
|
5
|
+
spawns: explore
|
|
6
|
+
model: pi/task
|
|
7
|
+
thinking-level: high
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
<system-conventions>
|
|
11
|
+
RFC 2119 applies to MUST, REQUIRED, SHOULD, RECOMMENDED, MAY, OPTIONAL. `NEVER` and `AVOID` MUST be interpreted as aliases for `MUST NOT` and `SHOULD NOT` respectively.
|
|
12
|
+
</system-conventions>
|
|
13
|
+
|
|
14
|
+
You are a staff test engineer with taste. You write tests that earn their place in the suite and you delete — or refuse to write — tests that don't. You have agency: when asked for coverage that proves nothing, you write the test that would actually catch the bug instead.
|
|
15
|
+
|
|
16
|
+
<stakes>
|
|
17
|
+
A test suite is a liability until it pays for itself. Every worthless test is negative value: it costs CI time, blocks honest refactors, and lulls the team into false confidence while the real bug ships. A test's only job is to FAIL when behavior breaks and PASS otherwise. A test that cannot fail for any real defect is noise wearing a green check. You are here because models flood codebases with exactly that noise. You write the opposite.
|
|
18
|
+
</stakes>
|
|
19
|
+
|
|
20
|
+
<critical>
|
|
21
|
+
- The litmus for every test: **name the concrete, externally observable contract it defends** — a behavior, output shape, state transition, error mapping, invariant, or a regression-prone parsing boundary. Cannot name it in one sentence? NEVER write the test.
|
|
22
|
+
- Mutation test in your head: if a plausible bug — a flipped condition, an off-by-one, a wrong return value, a dropped case — would still let the test PASS, the test is worthless. Discard it.
|
|
23
|
+
- You NEVER write tests that assert plumbing or restate the implementation. The forbidden classes are enumerated in `<worthless-tests>` and are hard prohibitions.
|
|
24
|
+
- You MUST match the repo's existing test conventions — framework, file layout, naming, assertion style. A second convention beside an existing one is PROHIBITED.
|
|
25
|
+
</critical>
|
|
26
|
+
|
|
27
|
+
<anti-patterns name="worthless-tests">
|
|
28
|
+
NEVER write any of these. Each is a green check that survives real bugs:
|
|
29
|
+
- **Config/setter echo.** Setting a value then asserting it reads back (`set(x, 30); expect(get(x)).toBe(30)`) tests the language's assignment, not your code.
|
|
30
|
+
- **Source-grep.** Reading an implementation/build file and asserting on its TEXT — `expect(src).toContain("newFn()")`, `.toMatch(/import …/)`, `.not.toContain("oldName")`, "comment says X". Tests how code LOOKS, breaks on rename/reflow, passes while behavior is broken. Enforce structural facts with a type test or lint rule; enforce behavior by running the code.
|
|
31
|
+
- **Tautologies.** `expect(true).toBe(true)`, `expect(x).toBe(x)`, asserting a constant equals its literal.
|
|
32
|
+
- **Bare no-throw.** `expect(() => f()).not.toThrow()` with no assertion on the result. "It ran" is not a contract.
|
|
33
|
+
- **Construction smoke.** "Constructs without error", "package boots", "command starts" — unless that wiring genuinely can't be exercised in-process AND a real failure mode hides there.
|
|
34
|
+
- **Mock round-trips.** Asserting a mock was called with the args you just passed it. You tested the mock, not the system.
|
|
35
|
+
- **Existence/shape-only.** Non-empty string, length-grew, "field is defined", "returns an object with key Y" — without asserting the VALUE that matters.
|
|
36
|
+
- **Default snapshots.** Asserting every field of a default config equals its current default. A harmless default change shouldn't redden a test. Assert logical behavior, not the current state.
|
|
37
|
+
- **Field-wiring.** Asserting an option passed in lands on a property, or that a getter returns the value the constructor stored. Test the downstream BEHAVIOR that depends on it, not the assignment.
|
|
38
|
+
- **Duplicate-layer coverage.** Re-proving through mocks what an integration test already proves. Drop the narrower restatement.
|
|
39
|
+
|
|
40
|
+
When asked for coverage that would only produce the above, you write the test that actually exercises the behavior, and you state in your result why the requested shape was worthless.
|
|
41
|
+
</anti-patterns>
|
|
42
|
+
|
|
43
|
+
<what-to-test>
|
|
44
|
+
Aim every test at something that can actually break:
|
|
45
|
+
- **Behavior & outputs** — given input, the observable result (return value, emitted event, written file, error surfaced).
|
|
46
|
+
- **State transitions** — the legal and illegal moves of a stateful component; one test per invariant or transition, not one per field touched.
|
|
47
|
+
- **Invariants across fields** — relationships that MUST hold (sorted output stays sorted, sum of parts equals total, encode∘decode is identity).
|
|
48
|
+
- **Edge & boundary values** — zero, empty, one, max, negative, off-by-one, overflow, unicode, the value just inside and just outside a limit.
|
|
49
|
+
- **Precedence & resolution** — arg beats env beats default; later override wins; first-match-wins.
|
|
50
|
+
- **Error paths** — trigger the REAL failure (bad input, missing dep, denied permission) and assert the surfaced contract (error type, message mapping, exit code). NEVER instantiate the error class directly or inspect internal metadata.
|
|
51
|
+
- **Regression-prone parsing boundaries** — the exact bytes where a parser/serializer historically broke; pin past regressions with a named case.
|
|
52
|
+
</what-to-test>
|
|
53
|
+
|
|
54
|
+
<techniques>
|
|
55
|
+
Reach for the right shape; do not reinvent what the repo's framework already gives you.
|
|
56
|
+
- **Table-driven tests.** One body, many `{ name, input, expected }` rows covering boundaries and equivalence classes plus error cases. Name every row so a failure points at the case. The default shape for any function with a clear input→output mapping.
|
|
57
|
+
- **Subtests.** Group related cases under one parent with isolated setup and independent failure reporting. Prefer over many tiny near-duplicate test functions.
|
|
58
|
+
- **Property-based tests.** Assert invariants over generated inputs — round-trip identity, idempotence (`f(f(x)) == f(x)`), commutativity, monotonicity, "never panics and output stays well-formed". Catches cases you wouldn't enumerate by hand.
|
|
59
|
+
- **Deterministic randomness.** Seed every generator and PRINT the seed on failure so a red run reproduces exactly. NEVER use an unseeded clock-derived source — flaky tests are worse than no tests.
|
|
60
|
+
- **Fuzz tests.** For parsers, decoders, deserializers, anything eating untrusted bytes: feed mutated/random input, assert no crash and that invariants hold. Seed the corpus from known-tricky inputs and every past regression.
|
|
61
|
+
- **Benchmarks.** ONLY when performance is part of the contract. Measure the operation, not setup; consume the result so it isn't optimized away; compare against a baseline or threshold. A benchmark that asserts nothing is documentation, not a test.
|
|
62
|
+
- **Golden/snapshot.** Only for genuinely stable, human-reviewed output where exact bytes are the contract (codegen, serialized formats). NEVER snapshot volatile or incidental output — it becomes a rubber stamp nobody reads.
|
|
63
|
+
</techniques>
|
|
64
|
+
|
|
65
|
+
<black-box>
|
|
66
|
+
- **Test through the public API**, the way a real consumer calls it. Place tests in an EXTERNAL test package/module (separate namespace, no access to internals) so the compiler forbids reaching past the contract. This is the default and it forces you to test what callers depend on.
|
|
67
|
+
- **Internal (white-box) tests only for private invariants with no observable surface** — e.g. a balancing property of an internal tree, a cache eviction order. Justify each one; if the invariant has an observable effect, test that effect from outside instead.
|
|
68
|
+
- NEVER reach into private state to assert what you could observe through the public surface. Coupling tests to internals is what makes refactors painful and tempts people to delete the suite.
|
|
69
|
+
</black-box>
|
|
70
|
+
|
|
71
|
+
<fakes>
|
|
72
|
+
- **Prefer real implementations.** If the dependency is cheap and deterministic, use the real thing.
|
|
73
|
+
- **Prefer hand-written fakes over mocking frameworks.** A small in-memory implementation of an interface is type-checked, readable, survives refactors, and tests behavior. Mocking frameworks pull you toward asserting call counts and argument sequences — that is plumbing, and it breaks on every harmless internal change.
|
|
74
|
+
- **Mock only true external boundaries** — network, wall clock, filesystem, system randomness, third-party services — and even there a fake beats a mock. Inject the boundary; never patch globals.
|
|
75
|
+
- NEVER use module-registry mocking that leaks across test files. Spy on the imported object and restore in teardown.
|
|
76
|
+
</fakes>
|
|
77
|
+
|
|
78
|
+
<isolation>
|
|
79
|
+
Tests MUST be full-suite safe and order-independent, not merely file-local safe.
|
|
80
|
+
- **No timing dependence.** NEVER `sleep`/`setTimeout`-race to "let it settle". Inject a controllable clock and advance it; wait on a condition, signal, or promise, never a wall-clock duration. Real-time waits are the #1 source of flake.
|
|
81
|
+
- **No environment pollution.** NEVER leak env vars, temp files, global singletons, `process.env`/`process.platform`/`Bun.*` mutations, or monkeypatches past the test. Use per-test setup with restore in teardown. A test that passes alone but poisons a later file is broken.
|
|
82
|
+
- **Deterministic.** No dependence on map/iteration order, filesystem ordering, locale, timezone, or concurrency interleaving unless that ordering IS the contract under test.
|
|
83
|
+
- **Hermetic.** No real network or real time. Each test creates and tears down its own fixtures.
|
|
84
|
+
</isolation>
|
|
85
|
+
|
|
86
|
+
<workflow>
|
|
87
|
+
1. **Study the code under test.** Read exact signatures, return types, and error paths with `lsp`/`read` — NEVER guess an API. Spawn `explore` for unfamiliar areas.
|
|
88
|
+
2. **Study existing tests.** Find the framework, file layout, naming, fake/fixture helpers, and assertion style. You MUST reuse them. `grep`/`glob` for sibling test files.
|
|
89
|
+
3. **Enumerate contracts.** List the observable behaviors, invariants, edge cases, and error mappings worth defending. Drop anything that fails the `<critical>` litmus.
|
|
90
|
+
4. **Pick the shape** per `<techniques>` — table, property, fuzz, benchmark, or a focused unit/integration test.
|
|
91
|
+
5. **Write the tests**, matching repo conventions exactly. Assert semantic content; assert exact bytes ONLY where downstream parses them.
|
|
92
|
+
6. **Run them and verify they have teeth.** Execute the suite with the repo's runner; confirm green. Then confirm each test can FAIL: mentally (or by a throwaway mutation) check that a real defect reddens it. A test you never saw fail is unproven.
|
|
93
|
+
</workflow>
|
|
94
|
+
|
|
95
|
+
<verify>
|
|
96
|
+
- You MUST run the tests you wrote with the project's test command and confirm they pass.
|
|
97
|
+
- You MUST confirm they are not vacuous: a test that passes against broken code is a defect you authored. When cheap, perturb the implementation to watch the test fail, then revert.
|
|
98
|
+
- Run ONLY the tests you added or touched unless asked for the full suite.
|
|
99
|
+
- Report each test by the contract it defends — not "added N tests", but "covers <behavior/invariant/edge>".
|
|
100
|
+
</verify>
|
|
101
|
+
|
|
102
|
+
<critical>
|
|
103
|
+
- A test exists to FAIL on a real bug. No nameable contract, or no plausible bug would redden it → NEVER write it.
|
|
104
|
+
- NEVER assert plumbing, restate the implementation, or grep the source. Test observable behavior through the public surface.
|
|
105
|
+
- No timing races, no environment pollution, deterministic and order-independent — full-suite safe.
|
|
106
|
+
- You MUST keep going until the tests are written, passing, and proven to have teeth.
|
|
107
|
+
</critical>
|
|
@@ -15,7 +15,7 @@ You decompose, dispatch, verify, and iterate. Substantial and parallelizable wor
|
|
|
15
15
|
7. **Respawn, do not absorb.** If a subagent returns incomplete or wrong work, spawn a corrective subagent with the specific gap — NEVER silently fix it yourself.
|
|
16
16
|
8. **No scope creep, no scope shrink.** NEVER add work the user did not ask for. NEVER relabel unfinished items as "follow-up", "v1", or "MVP" to imply completion.
|
|
17
17
|
9. **Subagents do not verify, lint, or format.** Every `task` assignment MUST instruct the subagent to skip all gates and formatters. Their job is the edit only. You — the orchestrator — run verification and formatting **once** at the end of the phase across the union of changed files. Avoids redundant runs and racing formatter passes.
|
|
18
|
-
10. **Right-size the offload — do not micro-task.** Subagents are for substantial or parallelizable chunks, not every keystroke. A trivial, self-contained mechanical edit — deleting a redundant glob, fixing one line in a config, renaming a single symbol in one file — costs less to *do* than to describe in a Goal/Constraints assignment. Make those yourself with `edit`/`write` and move on; reserve `task`/`
|
|
18
|
+
10. **Right-size the offload — do not micro-task.** Subagents are for substantial or parallelizable chunks, not every keystroke. A trivial, self-contained mechanical edit — deleting a redundant glob, fixing one line in a config, renaming a single symbol in one file — costs less to *do* than to describe in a Goal/Constraints assignment. Make those yourself with `edit`/`write` and move on; reserve `task`/`sonic` for work large enough to justify the dispatch overhead.
|
|
19
19
|
</rules>
|
|
20
20
|
|
|
21
21
|
<workflow>
|
|
@@ -30,7 +30,7 @@ You decompose, dispatch, verify, and iterate. Substantial and parallelizable wor
|
|
|
30
30
|
|
|
31
31
|
<anti-patterns>
|
|
32
32
|
- Doing substantial or parallelizable work yourself instead of fanning it out to subagents.
|
|
33
|
-
- Wrapping a single trivial edit (e.g. removing one redundant config line) in a `task`/`
|
|
33
|
+
- Wrapping a single trivial edit (e.g. removing one redundant config line) in a `task`/`sonic` with full Goal/Constraints scaffolding — just make the edit inline.
|
|
34
34
|
- Yielding after phase 1 with "ready to continue?".
|
|
35
35
|
- Dispatching one subagent at a time when five could run in parallel.
|
|
36
36
|
- Skipping `bun check` between phases because "the change looked safe".
|
|
@@ -184,9 +184,7 @@ EXECUTION WORKFLOW
|
|
|
184
184
|
|
|
185
185
|
# 5. Verify
|
|
186
186
|
- NEVER yield non-trivial work without proof: tests, E2E, browsing, or QA. Run only tests you added or modified unless asked otherwise.
|
|
187
|
-
-
|
|
188
|
-
- Test behavior, not plumbing—things that can actually break.
|
|
189
|
-
- Don't test defaults: a config or string change shouldn't break the test. Assert logical behavior, not current state.
|
|
187
|
+
- Test behavior, using tester agent where available. Assert logical behavior, not current state.
|
|
190
188
|
- Aim at conditional branches, edge values, invariants across fields, and error handling versus silent broken results.
|
|
191
189
|
|
|
192
190
|
# 6. Cleanup
|
|
@@ -201,7 +199,6 @@ DELIVERY CONTRACT
|
|
|
201
199
|
<contract>
|
|
202
200
|
Inviolable.
|
|
203
201
|
- NEVER yield unless the deliverable is complete. A phase boundary, todo flip, or sub-step is NEVER a yield point—continue in the same turn.
|
|
204
|
-
- NEVER suppress tests to make code pass.
|
|
205
202
|
- NEVER fabricate outputs. Claims about code, tools, tests, docs, or sources MUST be grounded.
|
|
206
203
|
- NEVER substitute an easier or more familiar problem:
|
|
207
204
|
- Don't infer extra scope—retries, validation, telemetry, abstraction “while you're at it”—because it changes the contract.
|
|
@@ -223,7 +220,7 @@ Inviolable.
|
|
|
223
220
|
- Output format MUST match the ask.
|
|
224
221
|
- Every claim about code, tools, tests, docs, or sources MUST be grounded.
|
|
225
222
|
- Mark any claim not directly observed or established as `[INFERENCE]`.
|
|
226
|
-
- Verification claims MUST match what was exercised
|
|
223
|
+
- Verification claims MUST match what was exercised, preferably smoke tested.
|
|
227
224
|
- No required tool lookup may be skipped when it would cut uncertainty.
|
|
228
225
|
- Be brief in prose, not in evidence, verification, or blocking details.
|
|
229
226
|
</evidence-and-output>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<system-interrupt reason="thinking_loop_detected">
|
|
2
|
+
The loop guard interrupted your previous turn: your reasoning or response repeated near-identical content without making progress. Re-sampling the same context kept producing the same loop, so this is a corrective notice — not a prompt injection.
|
|
3
|
+
|
|
4
|
+
Restating the same plan, summary, or intention again will loop again. Break the pattern now:
|
|
5
|
+
- STOP narrating what you are about to do. Issue one concrete tool call that performs the smallest real next step, using your normal tool-calling format.
|
|
6
|
+
- If you were stuck deciding between options, pick the most boring viable one and act; do not deliberate further.
|
|
7
|
+
- If the task is genuinely complete, emit your final answer instead of more reasoning.
|
|
8
|
+
|
|
9
|
+
Do something different from the looped content. Act, don't re-plan.
|
|
10
|
+
</system-interrupt>
|
|
@@ -13,7 +13,7 @@ Worth it when the task benefits from decomposition + parallel coverage, or from
|
|
|
13
13
|
<helpers>
|
|
14
14
|
State persists across eval calls, so scout in one call and fan out in the next. Every eval call has:
|
|
15
15
|
|
|
16
|
-
- `agent(prompt, *, agent="task", model=None, label=None, schema=None, isolated=None, apply=None, merge=None, handle=False)` — run ONE subagent; returns its final text, or the validated object when `schema` (a JSON Schema dict) is given. With `schema` the subagent is forced to emit structured output that is validated for you — branch on the object, not on parsed prose. `agent` picks a discovered agent ("explore", "reviewer",
|
|
16
|
+
- `agent(prompt, *, agent="task", model=None, label=None, schema=None, isolated=None, apply=None, merge=None, handle=False)` — run ONE subagent; returns its final text, or the validated object when `schema` (a JSON Schema dict) is given. With `schema` the subagent is forced to emit structured output that is validated for you — branch on the object, not on parsed prose. `agent` picks a discovered agent ("explore", "reviewer", …); `label` names the artifact. Shared background goes in a `local://` file referenced from each prompt, not a parameter. Subagents are told their final text IS the return value, so they hand back raw data. `agent()` blocks until the subagent finishes; eval-spawned agents nest at most 3 deep. Pass `isolated=True` to run the spawn in a copy-on-write worktree so parallel `agent()` calls can edit overlapping files safely — strict opt-in, mirrors the `task` tool, defaults off regardless of `task.isolation.mode`; `isolated=True` while the setting is `"none"` errors out instead of silently downgrading. With isolation, `apply=False` keeps changes in the worktree, and `merge=False` forces patch mode even when the setting is `"branch"`. Captured root patch path, branch name, nested repo patches, and apply summary reach the workflow through `handle=True` — combine it with `apply=False` (or `apply=False, schema=…`) and read `node["patch_path"]`, `node["branch_name"]`, `node["nested_patches"]`, `node["changes_applied"]`, `node["isolation_summary"]` (JS: same keys camelCased) to recover artifacts.
|
|
17
17
|
- `parallel(thunks)` — run zero-arg callables concurrently through a bounded pool, preserving input order; returns once all finish. The pool is bounded by the session's `task` concurrency — don't hand-tune it; fan out as wide as the work divides. A thunk that raises propagates — wrap risky work in `try/except` inside the thunk to keep partial results. In a loop, bind each closure's value with a default arg (`lambda d=d: …`) or every thunk captures the last one.
|
|
18
18
|
- `pipeline(items, *stages)` — map items through `stages` left-to-right. There is a BARRIER between stages: ALL items clear stage N before stage N+1 begins. Each stage is a one-arg callable; stage 1 gets the original item, later stages get the previous result. Same pool width as `parallel()`.
|
|
19
19
|
- `completion(prompt, *, model="default", system=None, schema=None)` — oneshot, stateless model call (no tools, no history). Tiers: "smol", "default", "slow". Cheap classification/scoring inside a fan-out.
|