@oh-my-pi/pi-coding-agent 12.19.3 → 13.0.1
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 +47 -0
- package/package.json +7 -7
- package/src/commit/prompts/analysis-system.md +3 -3
- package/src/commit/prompts/analysis-user.md +14 -14
- package/src/commit/prompts/changelog-system.md +4 -4
- package/src/commit/prompts/changelog-user.md +4 -4
- package/src/commit/prompts/file-observer-system.md +2 -2
- package/src/commit/prompts/file-observer-user.md +2 -2
- package/src/commit/prompts/reduce-system.md +4 -4
- package/src/commit/prompts/reduce-user.md +6 -6
- package/src/commit/prompts/summary-system.md +4 -4
- package/src/commit/prompts/summary-user.md +6 -6
- package/src/discovery/helpers.ts +13 -1
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/internal-urls/index.ts +8 -3
- package/src/internal-urls/local-protocol.ts +223 -0
- package/src/internal-urls/{docs-protocol.ts → pi-protocol.ts} +12 -12
- package/src/internal-urls/router.ts +1 -1
- package/src/internal-urls/types.ts +1 -1
- package/src/ipy/executor.ts +4 -32
- package/src/memories/index.ts +1 -1
- package/src/modes/controllers/event-controller.ts +4 -4
- package/src/modes/interactive-mode.ts +84 -64
- package/src/modes/types.ts +11 -3
- package/src/modes/utils/ui-helpers.ts +5 -3
- package/src/patch/hashline.ts +42 -42
- package/src/patch/index.ts +106 -153
- package/src/patch/shared.ts +21 -51
- package/src/plan-mode/approved-plan.ts +55 -0
- package/src/prompts/agents/designer.md +6 -6
- package/src/prompts/agents/explore.md +4 -4
- package/src/prompts/agents/frontmatter.md +1 -0
- package/src/prompts/agents/init.md +10 -10
- package/src/prompts/agents/plan.md +6 -6
- package/src/prompts/agents/reviewer.md +4 -3
- package/src/prompts/agents/task.md +10 -10
- package/src/prompts/compaction/branch-summary.md +3 -3
- package/src/prompts/compaction/compaction-short-summary.md +7 -7
- package/src/prompts/compaction/compaction-summary-context.md +1 -1
- package/src/prompts/compaction/compaction-summary.md +5 -5
- package/src/prompts/compaction/compaction-turn-prefix.md +3 -3
- package/src/prompts/compaction/compaction-update-summary.md +11 -11
- package/src/prompts/memories/consolidation.md +5 -5
- package/src/prompts/memories/read-path.md +11 -0
- package/src/prompts/memories/stage_one_input.md +1 -1
- package/src/prompts/memories/stage_one_system.md +5 -5
- package/src/prompts/review-request.md +4 -4
- package/src/prompts/system/agent-creation-architect.md +21 -21
- package/src/prompts/system/agent-creation-user.md +2 -2
- package/src/prompts/system/custom-system-prompt.md +6 -6
- package/src/prompts/system/plan-mode-active.md +20 -20
- package/src/prompts/system/plan-mode-approved.md +9 -7
- package/src/prompts/system/plan-mode-reference.md +2 -2
- package/src/prompts/system/plan-mode-subagent.md +8 -8
- package/src/prompts/system/subagent-submit-reminder.md +5 -5
- package/src/prompts/system/subagent-system-prompt.md +9 -9
- package/src/prompts/system/subagent-user-prompt.md +3 -5
- package/src/prompts/system/summarization-system.md +1 -1
- package/src/prompts/system/system-prompt.md +109 -84
- package/src/prompts/system/title-system.md +2 -2
- package/src/prompts/system/ttsr-interrupt.md +2 -2
- package/src/prompts/system/web-search.md +16 -16
- package/src/prompts/tools/ask.md +6 -6
- package/src/prompts/tools/bash.md +9 -9
- package/src/prompts/tools/browser.md +5 -5
- package/src/prompts/tools/cancel-job.md +2 -2
- package/src/prompts/tools/exit-plan-mode.md +13 -10
- package/src/prompts/tools/find.md +2 -2
- package/src/prompts/tools/gemini-image.md +7 -7
- package/src/prompts/tools/grep.md +4 -3
- package/src/prompts/tools/hashline.md +55 -56
- package/src/prompts/tools/patch.md +6 -6
- package/src/prompts/tools/poll-jobs.md +1 -1
- package/src/prompts/tools/python.md +10 -12
- package/src/prompts/tools/read.md +2 -12
- package/src/prompts/tools/replace.md +7 -7
- package/src/prompts/tools/ssh.md +2 -7
- package/src/prompts/tools/task.md +48 -38
- package/src/prompts/tools/todo-write.md +65 -49
- package/src/prompts/tools/web-search.md +2 -2
- package/src/prompts/tools/write.md +4 -3
- package/src/sdk.ts +11 -9
- package/src/session/agent-session.ts +92 -51
- package/src/session/artifacts.ts +1 -1
- package/src/session/messages.ts +1 -0
- package/src/task/agents.ts +1 -0
- package/src/task/index.ts +2 -1
- package/src/task/render.ts +2 -2
- package/src/task/types.ts +1 -0
- package/src/tools/bash-interactive.ts +1 -1
- package/src/tools/bash-skill-urls.ts +3 -2
- package/src/tools/bash.ts +21 -12
- package/src/tools/exit-plan-mode.ts +30 -2
- package/src/tools/grep.ts +131 -75
- package/src/tools/index.ts +13 -3
- package/src/tools/path-utils.ts +2 -1
- package/src/tools/plan-mode-guard.ts +8 -8
- package/src/tools/python.ts +0 -2
- package/src/tools/read.ts +2 -2
- package/src/tools/todo-write.ts +276 -146
- package/src/internal-urls/plan-protocol.ts +0 -95
- package/src/modes/components/todo-display.ts +0 -114
- package/src/prompts/memories/read_path.md +0 -11
|
@@ -75,7 +75,7 @@ import { ExtensionToolWrapper } from "../extensibility/extensions/wrapper";
|
|
|
75
75
|
import type { HookCommandContext } from "../extensibility/hooks/types";
|
|
76
76
|
import type { Skill, SkillWarning } from "../extensibility/skills";
|
|
77
77
|
import { expandSlashCommand, type FileSlashCommand } from "../extensibility/slash-commands";
|
|
78
|
-
import {
|
|
78
|
+
import { resolveLocalUrlToPath } from "../internal-urls";
|
|
79
79
|
import { executePython as executePythonCommand, type PythonResult } from "../ipy/executor";
|
|
80
80
|
import { getCurrentThemeName, theme } from "../modes/theme/theme";
|
|
81
81
|
import { normalizeDiff, normalizeToLF, ParseError, previewPatch, stripBom } from "../patch";
|
|
@@ -88,7 +88,7 @@ import { closeAllConnections } from "../ssh/connection-manager";
|
|
|
88
88
|
import { unmountAll } from "../ssh/sshfs-mount";
|
|
89
89
|
import { outputMeta } from "../tools/output-meta";
|
|
90
90
|
import { resolveToCwd } from "../tools/path-utils";
|
|
91
|
-
import type
|
|
91
|
+
import { getLatestTodoPhasesFromEntries, type TodoItem, type TodoPhase } from "../tools/todo-write";
|
|
92
92
|
import { parseCommandArgs } from "../utils/command-args";
|
|
93
93
|
import { resolveFileDisplayMode } from "../utils/file-display-mode";
|
|
94
94
|
import { extractFileMentions, generateFileMentionMessages } from "../utils/file-mentions";
|
|
@@ -135,7 +135,6 @@ export type AgentSessionEvent =
|
|
|
135
135
|
|
|
136
136
|
/** Listener function for agent session events */
|
|
137
137
|
export type AgentSessionEventListener = (event: AgentSessionEvent) => void;
|
|
138
|
-
|
|
139
138
|
export type AsyncJobSnapshotItem = Pick<AsyncJob, "id" | "type" | "status" | "label" | "startTime">;
|
|
140
139
|
|
|
141
140
|
export interface AsyncJobSnapshot {
|
|
@@ -192,7 +191,7 @@ export interface PromptOptions {
|
|
|
192
191
|
streamingBehavior?: "steer" | "followUp";
|
|
193
192
|
/** Optional tool choice override for the next LLM call. */
|
|
194
193
|
toolChoice?: ToolChoice;
|
|
195
|
-
/**
|
|
194
|
+
/** Send as developer/system message instead of user. Providers that support it use the developer role; others fall back to user. */
|
|
196
195
|
synthetic?: boolean;
|
|
197
196
|
}
|
|
198
197
|
|
|
@@ -310,6 +309,7 @@ export class AgentSession {
|
|
|
310
309
|
#pendingNextTurnMessages: CustomMessage[] = [];
|
|
311
310
|
#planModeState: PlanModeState | undefined;
|
|
312
311
|
#planReferenceSent = false;
|
|
312
|
+
#planReferencePath = "local://PLAN.md";
|
|
313
313
|
|
|
314
314
|
// Compaction state
|
|
315
315
|
#compactionAbortController: AbortController | undefined = undefined;
|
|
@@ -330,6 +330,7 @@ export class AgentSession {
|
|
|
330
330
|
|
|
331
331
|
// Todo completion reminder state
|
|
332
332
|
#todoReminderCount = 0;
|
|
333
|
+
#todoPhases: TodoPhase[] = [];
|
|
333
334
|
|
|
334
335
|
// Bash execution state
|
|
335
336
|
#bashAbortController: AbortController | undefined = undefined;
|
|
@@ -395,6 +396,7 @@ export class AgentSession {
|
|
|
395
396
|
this.#forceCopilotAgentInitiator = config.forceCopilotAgentInitiator ?? false;
|
|
396
397
|
this.#obfuscator = config.obfuscator;
|
|
397
398
|
this.agent.providerSessionState = this.#providerSessionState;
|
|
399
|
+
this.#syncTodoPhasesFromBranch();
|
|
398
400
|
|
|
399
401
|
// Always subscribe to agent events for internal handling
|
|
400
402
|
// (session persistence, hooks, auto-compaction, retry logic)
|
|
@@ -602,6 +604,7 @@ export class AgentSession {
|
|
|
602
604
|
}
|
|
603
605
|
} else if (
|
|
604
606
|
event.message.role === "user" ||
|
|
607
|
+
event.message.role === "developer" ||
|
|
605
608
|
event.message.role === "assistant" ||
|
|
606
609
|
event.message.role === "toolResult" ||
|
|
607
610
|
event.message.role === "fileMention"
|
|
@@ -638,7 +641,7 @@ export class AgentSession {
|
|
|
638
641
|
const { toolName, $normative, toolCallId, details, isError, content } = event.message as {
|
|
639
642
|
toolName?: string;
|
|
640
643
|
toolCallId?: string;
|
|
641
|
-
details?: { path?: string };
|
|
644
|
+
details?: { path?: string; phases?: TodoPhase[] };
|
|
642
645
|
$normative?: Record<string, unknown>;
|
|
643
646
|
isError?: boolean;
|
|
644
647
|
content?: Array<TextContent | ImageContent>;
|
|
@@ -650,14 +653,17 @@ export class AgentSession {
|
|
|
650
653
|
if (toolName === "edit" && details?.path) {
|
|
651
654
|
this.#invalidateFileCacheForPath(details.path);
|
|
652
655
|
}
|
|
656
|
+
if (toolName === "todo_write" && !isError && Array.isArray(details?.phases)) {
|
|
657
|
+
this.setTodoPhases(details.phases);
|
|
658
|
+
}
|
|
653
659
|
if (toolName === "todo_write" && isError) {
|
|
654
660
|
const errorText = content?.find(part => part.type === "text")?.text;
|
|
655
661
|
const reminderText = [
|
|
656
|
-
"<
|
|
662
|
+
"<system-reminder>",
|
|
657
663
|
"todo_write failed, so todo progress is not visible to the user.",
|
|
658
664
|
errorText ? `Failure: ${errorText}` : "Failure: todo_write returned an error.",
|
|
659
665
|
"Fix the todo payload and call todo_write again before continuing.",
|
|
660
|
-
"</
|
|
666
|
+
"</system-reminder>",
|
|
661
667
|
].join("\n");
|
|
662
668
|
await this.sendCustomMessage(
|
|
663
669
|
{
|
|
@@ -1491,6 +1497,7 @@ export class AgentSession {
|
|
|
1491
1497
|
this.#planModeState = state;
|
|
1492
1498
|
if (state?.enabled) {
|
|
1493
1499
|
this.#planReferenceSent = false;
|
|
1500
|
+
this.#planReferencePath = state.planFilePath;
|
|
1494
1501
|
}
|
|
1495
1502
|
}
|
|
1496
1503
|
|
|
@@ -1498,6 +1505,10 @@ export class AgentSession {
|
|
|
1498
1505
|
this.#planReferenceSent = true;
|
|
1499
1506
|
}
|
|
1500
1507
|
|
|
1508
|
+
setPlanReferencePath(path: string): void {
|
|
1509
|
+
this.#planReferencePath = path;
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1501
1512
|
/**
|
|
1502
1513
|
* Inject the plan mode context message into the conversation history.
|
|
1503
1514
|
*/
|
|
@@ -1546,10 +1557,10 @@ export class AgentSession {
|
|
|
1546
1557
|
if (this.#planModeState?.enabled) return null;
|
|
1547
1558
|
if (this.#planReferenceSent) return null;
|
|
1548
1559
|
|
|
1549
|
-
const planFilePath =
|
|
1550
|
-
const resolvedPlanPath =
|
|
1551
|
-
|
|
1552
|
-
|
|
1560
|
+
const planFilePath = this.#planReferencePath;
|
|
1561
|
+
const resolvedPlanPath = resolveLocalUrlToPath(planFilePath, {
|
|
1562
|
+
getArtifactsDir: () => this.sessionManager.getArtifactsDir(),
|
|
1563
|
+
getSessionId: () => this.sessionManager.getSessionId(),
|
|
1553
1564
|
});
|
|
1554
1565
|
let planContent: string;
|
|
1555
1566
|
try {
|
|
@@ -1580,19 +1591,19 @@ export class AgentSession {
|
|
|
1580
1591
|
async #buildPlanModeMessage(): Promise<CustomMessage | null> {
|
|
1581
1592
|
const state = this.#planModeState;
|
|
1582
1593
|
if (!state?.enabled) return null;
|
|
1583
|
-
const sessionPlanUrl =
|
|
1584
|
-
const resolvedPlanPath = state.planFilePath.startsWith("
|
|
1585
|
-
?
|
|
1586
|
-
|
|
1587
|
-
|
|
1594
|
+
const sessionPlanUrl = "local://PLAN.md";
|
|
1595
|
+
const resolvedPlanPath = state.planFilePath.startsWith("local://")
|
|
1596
|
+
? resolveLocalUrlToPath(state.planFilePath, {
|
|
1597
|
+
getArtifactsDir: () => this.sessionManager.getArtifactsDir(),
|
|
1598
|
+
getSessionId: () => this.sessionManager.getSessionId(),
|
|
1588
1599
|
})
|
|
1589
1600
|
: resolveToCwd(state.planFilePath, this.sessionManager.getCwd());
|
|
1590
|
-
const resolvedSessionPlan =
|
|
1591
|
-
|
|
1592
|
-
|
|
1601
|
+
const resolvedSessionPlan = resolveLocalUrlToPath(sessionPlanUrl, {
|
|
1602
|
+
getArtifactsDir: () => this.sessionManager.getArtifactsDir(),
|
|
1603
|
+
getSessionId: () => this.sessionManager.getSessionId(),
|
|
1593
1604
|
});
|
|
1594
1605
|
const displayPlanPath =
|
|
1595
|
-
state.planFilePath.startsWith("
|
|
1606
|
+
state.planFilePath.startsWith("local://") || resolvedPlanPath !== resolvedSessionPlan
|
|
1596
1607
|
? state.planFilePath
|
|
1597
1608
|
: sessionPlanUrl;
|
|
1598
1609
|
|
|
@@ -1673,16 +1684,11 @@ export class AgentSession {
|
|
|
1673
1684
|
userContent.push(...options.images);
|
|
1674
1685
|
}
|
|
1675
1686
|
|
|
1676
|
-
|
|
1677
|
-
{
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
timestamp: Date.now(),
|
|
1682
|
-
},
|
|
1683
|
-
expandedText,
|
|
1684
|
-
options,
|
|
1685
|
-
);
|
|
1687
|
+
const message = options?.synthetic
|
|
1688
|
+
? { role: "developer" as const, content: userContent, timestamp: Date.now() }
|
|
1689
|
+
: { role: "user" as const, content: userContent, timestamp: Date.now() };
|
|
1690
|
+
|
|
1691
|
+
await this.#promptWithMessage(message, expandedText, options);
|
|
1686
1692
|
}
|
|
1687
1693
|
|
|
1688
1694
|
async promptCustomMessage<T = unknown>(
|
|
@@ -2185,6 +2191,31 @@ export class AgentSession {
|
|
|
2185
2191
|
return this.#skillWarnings;
|
|
2186
2192
|
}
|
|
2187
2193
|
|
|
2194
|
+
getTodoPhases(): TodoPhase[] {
|
|
2195
|
+
return this.#cloneTodoPhases(this.#todoPhases);
|
|
2196
|
+
}
|
|
2197
|
+
|
|
2198
|
+
setTodoPhases(phases: TodoPhase[]): void {
|
|
2199
|
+
this.#todoPhases = this.#cloneTodoPhases(phases);
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
#syncTodoPhasesFromBranch(): void {
|
|
2203
|
+
this.setTodoPhases(getLatestTodoPhasesFromEntries(this.sessionManager.getBranch()));
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
#cloneTodoPhases(phases: TodoPhase[]): TodoPhase[] {
|
|
2207
|
+
return phases.map(phase => ({
|
|
2208
|
+
id: phase.id,
|
|
2209
|
+
name: phase.name,
|
|
2210
|
+
tasks: phase.tasks.map(task => ({
|
|
2211
|
+
id: task.id,
|
|
2212
|
+
content: task.content,
|
|
2213
|
+
status: task.status,
|
|
2214
|
+
notes: task.notes,
|
|
2215
|
+
})),
|
|
2216
|
+
}));
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2188
2219
|
/**
|
|
2189
2220
|
* Abort current operation and wait for agent to become idle.
|
|
2190
2221
|
*/
|
|
@@ -2228,6 +2259,7 @@ export class AgentSession {
|
|
|
2228
2259
|
this.agent.reset();
|
|
2229
2260
|
await this.sessionManager.flush();
|
|
2230
2261
|
await this.sessionManager.newSession(options);
|
|
2262
|
+
this.setTodoPhases([]);
|
|
2231
2263
|
this.agent.sessionId = this.sessionManager.getSessionId();
|
|
2232
2264
|
this.#steeringMessages = [];
|
|
2233
2265
|
this.#followUpMessages = [];
|
|
@@ -2237,6 +2269,7 @@ export class AgentSession {
|
|
|
2237
2269
|
|
|
2238
2270
|
this.#todoReminderCount = 0;
|
|
2239
2271
|
this.#planReferenceSent = false;
|
|
2272
|
+
this.#planReferencePath = "local://PLAN.md";
|
|
2240
2273
|
this.#reconnectToAgent();
|
|
2241
2274
|
|
|
2242
2275
|
// Emit session_switch event with reason "new" to hooks
|
|
@@ -2645,6 +2678,7 @@ export class AgentSession {
|
|
|
2645
2678
|
await this.sessionManager.rewriteEntries();
|
|
2646
2679
|
const sessionContext = this.sessionManager.buildSessionContext();
|
|
2647
2680
|
this.agent.replaceMessages(sessionContext.messages);
|
|
2681
|
+
this.#syncTodoPhasesFromBranch();
|
|
2648
2682
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
2649
2683
|
return result;
|
|
2650
2684
|
}
|
|
@@ -2769,6 +2803,7 @@ export class AgentSession {
|
|
|
2769
2803
|
const newEntries = this.sessionManager.getEntries();
|
|
2770
2804
|
const sessionContext = this.sessionManager.buildSessionContext();
|
|
2771
2805
|
this.agent.replaceMessages(sessionContext.messages);
|
|
2806
|
+
this.#syncTodoPhasesFromBranch();
|
|
2772
2807
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
2773
2808
|
|
|
2774
2809
|
// Get the saved compaction entry for the hook
|
|
@@ -2925,7 +2960,7 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
2925
2960
|
|
|
2926
2961
|
try {
|
|
2927
2962
|
// Send the prompt and wait for completion
|
|
2928
|
-
await this.prompt(handoffPrompt, { expandPromptTemplates: false });
|
|
2963
|
+
await this.prompt(handoffPrompt, { expandPromptTemplates: false, synthetic: true });
|
|
2929
2964
|
await completionPromise;
|
|
2930
2965
|
|
|
2931
2966
|
if (!handoffText || this.#handoffAbortController.signal.aborted) {
|
|
@@ -2950,6 +2985,7 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
2950
2985
|
// Rebuild agent messages from session
|
|
2951
2986
|
const sessionContext = this.sessionManager.buildSessionContext();
|
|
2952
2987
|
this.agent.replaceMessages(sessionContext.messages);
|
|
2988
|
+
this.#syncTodoPhasesFromBranch();
|
|
2953
2989
|
|
|
2954
2990
|
return { document: handoffText };
|
|
2955
2991
|
} finally {
|
|
@@ -3047,25 +3083,24 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
3047
3083
|
return;
|
|
3048
3084
|
}
|
|
3049
3085
|
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
const todoPath = `${sessionFile.slice(0, -6)}/todos.json`;
|
|
3055
|
-
|
|
3056
|
-
let todos: TodoItem[];
|
|
3057
|
-
try {
|
|
3058
|
-
const data = await Bun.file(todoPath).json();
|
|
3059
|
-
todos = data?.todos ?? [];
|
|
3060
|
-
} catch (err) {
|
|
3061
|
-
if (isEnoent(err)) {
|
|
3062
|
-
this.#todoReminderCount = 0;
|
|
3063
|
-
}
|
|
3086
|
+
const phases = this.getTodoPhases();
|
|
3087
|
+
if (phases.length === 0) {
|
|
3088
|
+
this.#todoReminderCount = 0;
|
|
3064
3089
|
return;
|
|
3065
3090
|
}
|
|
3066
3091
|
|
|
3067
|
-
|
|
3068
|
-
|
|
3092
|
+
const incompleteByPhase = phases
|
|
3093
|
+
.map(phase => ({
|
|
3094
|
+
name: phase.name,
|
|
3095
|
+
tasks: phase.tasks
|
|
3096
|
+
.filter(
|
|
3097
|
+
(task): task is TodoItem & { status: "pending" | "in_progress" } =>
|
|
3098
|
+
task.status === "pending" || task.status === "in_progress",
|
|
3099
|
+
)
|
|
3100
|
+
.map(task => ({ id: task.id, content: task.content, status: task.status })),
|
|
3101
|
+
}))
|
|
3102
|
+
.filter(phase => phase.tasks.length > 0);
|
|
3103
|
+
const incomplete = incompleteByPhase.flatMap(phase => phase.tasks);
|
|
3069
3104
|
if (incomplete.length === 0) {
|
|
3070
3105
|
this.#todoReminderCount = 0;
|
|
3071
3106
|
return;
|
|
@@ -3073,13 +3108,15 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
3073
3108
|
|
|
3074
3109
|
// Build reminder message
|
|
3075
3110
|
this.#todoReminderCount++;
|
|
3076
|
-
const todoList =
|
|
3111
|
+
const todoList = incompleteByPhase
|
|
3112
|
+
.map(phase => `- ${phase.name}\n${phase.tasks.map(task => ` - ${task.content}`).join("\n")}`)
|
|
3113
|
+
.join("\n");
|
|
3077
3114
|
const reminder =
|
|
3078
|
-
`<
|
|
3115
|
+
`<system-reminder>\n` +
|
|
3079
3116
|
`You stopped with ${incomplete.length} incomplete todo item(s):\n${todoList}\n\n` +
|
|
3080
3117
|
`Please continue working on these tasks or mark them complete if finished.\n` +
|
|
3081
3118
|
`(Reminder ${this.#todoReminderCount}/${remindersMax})\n` +
|
|
3082
|
-
`</
|
|
3119
|
+
`</system-reminder>`;
|
|
3083
3120
|
|
|
3084
3121
|
logger.debug("Todo completion: sending reminder", {
|
|
3085
3122
|
incomplete: incomplete.length,
|
|
@@ -3096,7 +3133,7 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
3096
3133
|
|
|
3097
3134
|
// Inject reminder and continue the conversation
|
|
3098
3135
|
this.agent.appendMessage({
|
|
3099
|
-
role: "
|
|
3136
|
+
role: "developer",
|
|
3100
3137
|
content: [{ type: "text", text: reminder }],
|
|
3101
3138
|
timestamp: Date.now(),
|
|
3102
3139
|
});
|
|
@@ -3482,6 +3519,7 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
3482
3519
|
const newEntries = this.sessionManager.getEntries();
|
|
3483
3520
|
const sessionContext = this.sessionManager.buildSessionContext();
|
|
3484
3521
|
this.agent.replaceMessages(sessionContext.messages);
|
|
3522
|
+
this.#syncTodoPhasesFromBranch();
|
|
3485
3523
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
3486
3524
|
|
|
3487
3525
|
// Get the saved compaction entry for the hook
|
|
@@ -4054,6 +4092,7 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
4054
4092
|
}
|
|
4055
4093
|
|
|
4056
4094
|
this.agent.replaceMessages(sessionContext.messages);
|
|
4095
|
+
this.#syncTodoPhasesFromBranch();
|
|
4057
4096
|
|
|
4058
4097
|
// Restore model if saved
|
|
4059
4098
|
const defaultModelStr = sessionContext.models.default;
|
|
@@ -4135,6 +4174,7 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
4135
4174
|
} else {
|
|
4136
4175
|
this.sessionManager.createBranchedSession(selectedEntry.parentId);
|
|
4137
4176
|
}
|
|
4177
|
+
this.#syncTodoPhasesFromBranch();
|
|
4138
4178
|
this.agent.sessionId = this.sessionManager.getSessionId();
|
|
4139
4179
|
|
|
4140
4180
|
// Reload messages from entries (works for both file and in-memory mode)
|
|
@@ -4303,6 +4343,7 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
4303
4343
|
// Update agent state
|
|
4304
4344
|
const sessionContext = this.sessionManager.buildSessionContext();
|
|
4305
4345
|
this.agent.replaceMessages(sessionContext.messages);
|
|
4346
|
+
this.#syncTodoPhasesFromBranch();
|
|
4306
4347
|
|
|
4307
4348
|
// Emit session_tree event
|
|
4308
4349
|
if (this.#extensionRunner) {
|
package/src/session/artifacts.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Session-scoped artifact storage for truncated tool outputs.
|
|
3
3
|
*
|
|
4
4
|
* Artifacts are stored in a directory alongside the session file,
|
|
5
|
-
* accessible via artifact:// URLs
|
|
5
|
+
* accessible via artifact:// URLs.
|
|
6
6
|
*/
|
|
7
7
|
import * as fs from "node:fs/promises";
|
|
8
8
|
import * as path from "node:path";
|
package/src/session/messages.ts
CHANGED
package/src/task/agents.ts
CHANGED
package/src/task/index.ts
CHANGED
|
@@ -179,7 +179,8 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
|
|
|
179
179
|
onUpdate?: AgentToolUpdateCallback<TaskToolDetails>,
|
|
180
180
|
): Promise<AgentToolResult<TaskToolDetails>> {
|
|
181
181
|
const asyncEnabled = this.session.settings.get("async.enabled");
|
|
182
|
-
|
|
182
|
+
const selectedAgent = this.#discoveredAgents.find(agent => agent.name === params.agent);
|
|
183
|
+
if (!asyncEnabled || selectedAgent?.blocking === true) {
|
|
183
184
|
return this.#executeSync(_toolCallId, params, signal, onUpdate);
|
|
184
185
|
}
|
|
185
186
|
|
package/src/task/render.ts
CHANGED
|
@@ -366,9 +366,9 @@ function renderTaskSection(
|
|
|
366
366
|
const trimmed = task.trimEnd();
|
|
367
367
|
if (!expanded || !trimmed) return lines;
|
|
368
368
|
|
|
369
|
-
// Strip the shared <
|
|
369
|
+
// Strip the shared <context>...</context> block — it's the same
|
|
370
370
|
// across all tasks and just adds noise when expanded.
|
|
371
|
-
const stripped = trimmed.replace(/<
|
|
371
|
+
const stripped = trimmed.replace(/<context>[\s\S]*?<\/context>\s*/, "").trimStart();
|
|
372
372
|
if (!stripped) return lines;
|
|
373
373
|
|
|
374
374
|
lines.push(`${continuePrefix}${theme.fg("dim", "Task")}`);
|
package/src/task/types.ts
CHANGED
|
@@ -22,6 +22,7 @@ interface InternalUrlResolver {
|
|
|
22
22
|
|
|
23
23
|
export interface InternalUrlExpansionOptions {
|
|
24
24
|
skills: readonly Skill[];
|
|
25
|
+
noEscape?: boolean;
|
|
25
26
|
internalRouter?: InternalUrlResolver;
|
|
26
27
|
}
|
|
27
28
|
|
|
@@ -152,7 +153,7 @@ export function expandSkillUrls(command: string, skills: readonly Skill[]): stri
|
|
|
152
153
|
|
|
153
154
|
/**
|
|
154
155
|
* Expand supported internal URLs in a bash command string to shell-escaped absolute paths.
|
|
155
|
-
* Supported schemes: skill://, agent://, artifact://,
|
|
156
|
+
* Supported schemes: skill://, agent://, artifact://, memory://, rule://, local://
|
|
156
157
|
*/
|
|
157
158
|
export async function expandInternalUrls(command: string, options: InternalUrlExpansionOptions): Promise<string> {
|
|
158
159
|
if (!command.includes("://")) return command;
|
|
@@ -169,7 +170,7 @@ export async function expandInternalUrls(command: string, options: InternalUrlEx
|
|
|
169
170
|
|
|
170
171
|
const url = unquoteToken(token);
|
|
171
172
|
const resolvedPath = await resolveInternalUrlToPath(url, options.skills, options.internalRouter);
|
|
172
|
-
const replacement = shellEscape(resolvedPath);
|
|
173
|
+
const replacement = options.noEscape ? resolvedPath : shellEscape(resolvedPath);
|
|
173
174
|
expanded = `${expanded.slice(0, index)}${replacement}${expanded.slice(index + token.length)}`;
|
|
174
175
|
}
|
|
175
176
|
|
package/src/tools/bash.ts
CHANGED
|
@@ -16,10 +16,10 @@ import { DEFAULT_MAX_BYTES, TailBuffer } from "../session/streaming-output";
|
|
|
16
16
|
import { renderStatusLine } from "../tui";
|
|
17
17
|
import { CachedOutputBlock } from "../tui/output-block";
|
|
18
18
|
import type { ToolSession } from ".";
|
|
19
|
-
import { type BashInteractiveResult, runInteractiveBashPty } from "./bash-interactive";
|
|
19
|
+
import { type BashInteractiveResult, NO_PAGER_ENV, runInteractiveBashPty } from "./bash-interactive";
|
|
20
20
|
import { checkBashInterception } from "./bash-interceptor";
|
|
21
21
|
import { applyHeadTail } from "./bash-normalize";
|
|
22
|
-
import { expandInternalUrls } from "./bash-skill-urls";
|
|
22
|
+
import { expandInternalUrls, type InternalUrlExpansionOptions } from "./bash-skill-urls";
|
|
23
23
|
import { formatStyledTruncationWarning, type OutputMeta } from "./output-meta";
|
|
24
24
|
import { resolveToCwd } from "./path-utils";
|
|
25
25
|
import { replaceTabs } from "./render-utils";
|
|
@@ -144,6 +144,14 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
144
144
|
): Promise<AgentToolResult<BashToolDetails>> {
|
|
145
145
|
let command = rawCommand;
|
|
146
146
|
|
|
147
|
+
// Extract leading `cd <path> && ...` into cwd when the model ignores the cwd parameter.
|
|
148
|
+
if (!cwd) {
|
|
149
|
+
const cdMatch = command.match(/^cd\s+((?:[^&\\]|\\.)+?)\s*&&\s*/);
|
|
150
|
+
if (cdMatch) {
|
|
151
|
+
cwd = cdMatch[1].trim().replace(/^["']|["']$/g, "");
|
|
152
|
+
command = command.slice(cdMatch[0].length);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
147
155
|
if (asyncRequested && !this.#asyncEnabled) {
|
|
148
156
|
throw new ToolError("Async bash execution is disabled. Enable async.enabled to use async mode.");
|
|
149
157
|
}
|
|
@@ -161,10 +169,16 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
161
169
|
}
|
|
162
170
|
}
|
|
163
171
|
|
|
164
|
-
|
|
172
|
+
const internalUrlOptions: InternalUrlExpansionOptions = {
|
|
165
173
|
skills: this.session.skills ?? [],
|
|
166
174
|
internalRouter: this.session.internalRouter,
|
|
167
|
-
}
|
|
175
|
+
};
|
|
176
|
+
command = await expandInternalUrls(command, internalUrlOptions);
|
|
177
|
+
|
|
178
|
+
// Resolve protocol URLs (skill://, agent://, etc.) in extracted cwd.
|
|
179
|
+
if (cwd?.includes("://")) {
|
|
180
|
+
cwd = await expandInternalUrls(cwd, { ...internalUrlOptions, noEscape: true });
|
|
181
|
+
}
|
|
168
182
|
|
|
169
183
|
const commandCwd = cwd ? resolveToCwd(cwd, this.session.cwd) : this.session.cwd;
|
|
170
184
|
let cwdStat: fs.Stats;
|
|
@@ -195,8 +209,6 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
195
209
|
"bash",
|
|
196
210
|
label,
|
|
197
211
|
async ({ jobId, signal: runSignal, reportProgress }) => {
|
|
198
|
-
const artifactsDir = this.session.getArtifactsDir?.();
|
|
199
|
-
const extraEnv = artifactsDir ? { ARTIFACTS: artifactsDir } : undefined;
|
|
200
212
|
const { path: artifactPath, id: artifactId } =
|
|
201
213
|
(await this.session.allocateOutputArtifact?.("bash")) ?? {};
|
|
202
214
|
try {
|
|
@@ -205,7 +217,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
205
217
|
sessionKey: `${this.session.getSessionId?.() ?? ""}:async:${jobId}`,
|
|
206
218
|
timeout: timeoutMs,
|
|
207
219
|
signal: runSignal,
|
|
208
|
-
env:
|
|
220
|
+
env: NO_PAGER_ENV,
|
|
209
221
|
artifactPath,
|
|
210
222
|
artifactId,
|
|
211
223
|
onChunk: chunk => {
|
|
@@ -238,9 +250,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
238
250
|
// Track output for streaming updates (tail only)
|
|
239
251
|
const tailBuffer = new TailBuffer(DEFAULT_MAX_BYTES);
|
|
240
252
|
|
|
241
|
-
//
|
|
242
|
-
const artifactsDir = this.session.getArtifactsDir?.();
|
|
243
|
-
const extraEnv = artifactsDir ? { ARTIFACTS: artifactsDir } : undefined;
|
|
253
|
+
// Allocate artifact for truncated output storage
|
|
244
254
|
const { path: artifactPath, id: artifactId } = (await this.session.allocateOutputArtifact?.("bash")) ?? {};
|
|
245
255
|
|
|
246
256
|
const usePty = pty && $env.PI_NO_PTY !== "1" && ctx?.hasUI === true && ctx.ui !== undefined;
|
|
@@ -250,7 +260,6 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
250
260
|
cwd: commandCwd,
|
|
251
261
|
timeoutMs,
|
|
252
262
|
signal,
|
|
253
|
-
env: extraEnv,
|
|
254
263
|
artifactPath,
|
|
255
264
|
artifactId,
|
|
256
265
|
})
|
|
@@ -259,7 +268,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
259
268
|
sessionKey: this.session.getSessionId?.() ?? undefined,
|
|
260
269
|
timeout: timeoutMs,
|
|
261
270
|
signal,
|
|
262
|
-
env:
|
|
271
|
+
env: NO_PAGER_ENV,
|
|
263
272
|
artifactPath,
|
|
264
273
|
artifactId,
|
|
265
274
|
onChunk: chunk => {
|
|
@@ -8,13 +8,36 @@ import type { ToolSession } from ".";
|
|
|
8
8
|
import { resolvePlanPath } from "./plan-mode-guard";
|
|
9
9
|
import { ToolError } from "./tool-errors";
|
|
10
10
|
|
|
11
|
-
const exitPlanModeSchema = Type.Object({
|
|
11
|
+
const exitPlanModeSchema = Type.Object({
|
|
12
|
+
title: Type.String({ description: "Final plan title, e.g. WP_MIGRATION_PLAN" }),
|
|
13
|
+
});
|
|
12
14
|
|
|
13
15
|
type ExitPlanModeParams = Static<typeof exitPlanModeSchema>;
|
|
14
16
|
|
|
17
|
+
function normalizePlanTitle(title: string): { title: string; fileName: string } {
|
|
18
|
+
const trimmed = title.trim();
|
|
19
|
+
if (!trimmed) {
|
|
20
|
+
throw new ToolError("Title is required and must not be empty.");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (trimmed.includes("/") || trimmed.includes("\\") || trimmed.includes("..")) {
|
|
24
|
+
throw new ToolError("Title must not contain path separators or '..'.");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const withExtension = trimmed.toLowerCase().endsWith(".md") ? trimmed : `${trimmed}.md`;
|
|
28
|
+
if (!/^[A-Za-z0-9_-]+\.md$/.test(withExtension)) {
|
|
29
|
+
throw new ToolError("Title may only contain letters, numbers, underscores, or hyphens.");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const normalizedTitle = withExtension.slice(0, -3);
|
|
33
|
+
return { title: normalizedTitle, fileName: withExtension };
|
|
34
|
+
}
|
|
35
|
+
|
|
15
36
|
export interface ExitPlanModeDetails {
|
|
16
37
|
planFilePath: string;
|
|
17
38
|
planExists: boolean;
|
|
39
|
+
title: string;
|
|
40
|
+
finalPlanFilePath: string;
|
|
18
41
|
}
|
|
19
42
|
|
|
20
43
|
export class ExitPlanModeTool implements AgentTool<typeof exitPlanModeSchema, ExitPlanModeDetails> {
|
|
@@ -29,7 +52,7 @@ export class ExitPlanModeTool implements AgentTool<typeof exitPlanModeSchema, Ex
|
|
|
29
52
|
|
|
30
53
|
async execute(
|
|
31
54
|
_toolCallId: string,
|
|
32
|
-
|
|
55
|
+
params: ExitPlanModeParams,
|
|
33
56
|
_signal?: AbortSignal,
|
|
34
57
|
_onUpdate?: AgentToolUpdateCallback<ExitPlanModeDetails>,
|
|
35
58
|
_context?: AgentToolContext,
|
|
@@ -39,7 +62,10 @@ export class ExitPlanModeTool implements AgentTool<typeof exitPlanModeSchema, Ex
|
|
|
39
62
|
throw new ToolError("Plan mode is not active.");
|
|
40
63
|
}
|
|
41
64
|
|
|
65
|
+
const normalized = normalizePlanTitle(params.title);
|
|
66
|
+
const finalPlanFilePath = `local://${normalized.fileName}`;
|
|
42
67
|
const resolvedPlanPath = resolvePlanPath(this.session, state.planFilePath);
|
|
68
|
+
resolvePlanPath(this.session, finalPlanFilePath);
|
|
43
69
|
let planExists = false;
|
|
44
70
|
try {
|
|
45
71
|
const stat = await fs.stat(resolvedPlanPath);
|
|
@@ -55,6 +81,8 @@ export class ExitPlanModeTool implements AgentTool<typeof exitPlanModeSchema, Ex
|
|
|
55
81
|
details: {
|
|
56
82
|
planFilePath: state.planFilePath,
|
|
57
83
|
planExists,
|
|
84
|
+
title: normalized.title,
|
|
85
|
+
finalPlanFilePath,
|
|
58
86
|
},
|
|
59
87
|
};
|
|
60
88
|
}
|