@oh-my-pi/pi-coding-agent 12.19.2 → 13.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +53 -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/config/settings-schema.ts +0 -11
- 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/main.ts +0 -1
- package/src/memories/index.ts +1 -1
- package/src/modes/components/settings-defs.ts +0 -5
- 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 +24 -21
- package/src/patch/shared.ts +21 -43
- 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 +17 -17
- 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 +37 -39
- package/src/prompts/tools/patch.md +5 -5
- package/src/prompts/tools/poll-jobs.md +1 -1
- package/src/prompts/tools/python.md +8 -10
- package/src/prompts/tools/read.md +2 -12
- package/src/prompts/tools/replace.md +6 -6
- package/src/prompts/tools/ssh.md +2 -7
- package/src/prompts/tools/task.md +34 -23
- 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 +38 -19
- 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
package/src/sdk.ts
CHANGED
|
@@ -46,11 +46,11 @@ import { type FileSlashCommand, loadSlashCommands as loadSlashCommandsInternal }
|
|
|
46
46
|
import {
|
|
47
47
|
AgentProtocolHandler,
|
|
48
48
|
ArtifactProtocolHandler,
|
|
49
|
-
DocsProtocolHandler,
|
|
50
49
|
InternalUrlRouter,
|
|
51
50
|
JobsProtocolHandler,
|
|
51
|
+
LocalProtocolHandler,
|
|
52
52
|
MemoryProtocolHandler,
|
|
53
|
-
|
|
53
|
+
PiProtocolHandler,
|
|
54
54
|
RuleProtocolHandler,
|
|
55
55
|
SkillProtocolHandler,
|
|
56
56
|
} from "./internal-urls";
|
|
@@ -799,6 +799,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
799
799
|
},
|
|
800
800
|
getPlanModeState: () => session.getPlanModeState(),
|
|
801
801
|
getCompactContext: () => session.formatCompactContext(),
|
|
802
|
+
getTodoPhases: () => session.getTodoPhases(),
|
|
803
|
+
setTodoPhases: phases => session.setTodoPhases(phases),
|
|
802
804
|
allocateOutputArtifact: async toolType => {
|
|
803
805
|
try {
|
|
804
806
|
return await sessionManager.allocateArtifactPath(toolType);
|
|
@@ -812,20 +814,20 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
812
814
|
asyncJobManager,
|
|
813
815
|
};
|
|
814
816
|
|
|
815
|
-
// Initialize internal URL router for internal protocols (agent://, artifact://,
|
|
817
|
+
// Initialize internal URL router for internal protocols (agent://, artifact://, memory://, skill://, rule://, local://)
|
|
816
818
|
const internalRouter = new InternalUrlRouter();
|
|
817
819
|
const getArtifactsDir = () => sessionManager.getArtifactsDir();
|
|
818
820
|
internalRouter.register(new AgentProtocolHandler({ getArtifactsDir }));
|
|
819
821
|
internalRouter.register(new ArtifactProtocolHandler({ getArtifactsDir }));
|
|
820
822
|
internalRouter.register(
|
|
821
|
-
new
|
|
822
|
-
|
|
823
|
-
cwd,
|
|
823
|
+
new MemoryProtocolHandler({
|
|
824
|
+
getMemoryRoot: () => getMemoryRoot(agentDir, settings.getCwd()),
|
|
824
825
|
}),
|
|
825
826
|
);
|
|
826
827
|
internalRouter.register(
|
|
827
|
-
new
|
|
828
|
-
|
|
828
|
+
new LocalProtocolHandler({
|
|
829
|
+
getArtifactsDir,
|
|
830
|
+
getSessionId: () => sessionManager.getSessionId(),
|
|
829
831
|
}),
|
|
830
832
|
);
|
|
831
833
|
internalRouter.register(
|
|
@@ -838,7 +840,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
838
840
|
getRules: () => rulebookRules,
|
|
839
841
|
}),
|
|
840
842
|
);
|
|
841
|
-
internalRouter.register(new
|
|
843
|
+
internalRouter.register(new PiProtocolHandler());
|
|
842
844
|
internalRouter.register(new JobsProtocolHandler({ getAsyncJobManager: () => asyncJobManager }));
|
|
843
845
|
toolSession.internalRouter = internalRouter;
|
|
844
846
|
toolSession.getArtifactsDir = getArtifactsDir;
|
|
@@ -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";
|
|
@@ -34,6 +34,11 @@ const bashSchemaBase = Type.Object({
|
|
|
34
34
|
cwd: Type.Optional(Type.String({ description: "Working directory (default: cwd)" })),
|
|
35
35
|
head: Type.Optional(Type.Number({ description: "Return only first N lines of output" })),
|
|
36
36
|
tail: Type.Optional(Type.Number({ description: "Return only last N lines of output" })),
|
|
37
|
+
pty: Type.Optional(
|
|
38
|
+
Type.Boolean({
|
|
39
|
+
description: "Run in PTY mode when command needs a real terminal (e.g. sudo/ssh/top/less); default: false",
|
|
40
|
+
}),
|
|
41
|
+
),
|
|
37
42
|
});
|
|
38
43
|
|
|
39
44
|
const bashSchemaWithAsync = Type.Object({
|
|
@@ -54,6 +59,7 @@ export interface BashToolInput {
|
|
|
54
59
|
head?: number;
|
|
55
60
|
tail?: number;
|
|
56
61
|
async?: boolean;
|
|
62
|
+
pty?: boolean;
|
|
57
63
|
}
|
|
58
64
|
|
|
59
65
|
export interface BashToolDetails {
|
|
@@ -123,13 +129,29 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
123
129
|
|
|
124
130
|
async execute(
|
|
125
131
|
_toolCallId: string,
|
|
126
|
-
{
|
|
132
|
+
{
|
|
133
|
+
command: rawCommand,
|
|
134
|
+
timeout: rawTimeout = 300,
|
|
135
|
+
cwd,
|
|
136
|
+
head,
|
|
137
|
+
tail,
|
|
138
|
+
async: asyncRequested = false,
|
|
139
|
+
pty = false,
|
|
140
|
+
}: BashToolInput,
|
|
127
141
|
signal?: AbortSignal,
|
|
128
142
|
onUpdate?: AgentToolUpdateCallback<BashToolDetails>,
|
|
129
143
|
ctx?: AgentToolContext,
|
|
130
144
|
): Promise<AgentToolResult<BashToolDetails>> {
|
|
131
145
|
let command = rawCommand;
|
|
132
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
|
+
}
|
|
133
155
|
if (asyncRequested && !this.#asyncEnabled) {
|
|
134
156
|
throw new ToolError("Async bash execution is disabled. Enable async.enabled to use async mode.");
|
|
135
157
|
}
|
|
@@ -147,10 +169,16 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
147
169
|
}
|
|
148
170
|
}
|
|
149
171
|
|
|
150
|
-
|
|
172
|
+
const internalUrlOptions: InternalUrlExpansionOptions = {
|
|
151
173
|
skills: this.session.skills ?? [],
|
|
152
174
|
internalRouter: this.session.internalRouter,
|
|
153
|
-
}
|
|
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
|
+
}
|
|
154
182
|
|
|
155
183
|
const commandCwd = cwd ? resolveToCwd(cwd, this.session.cwd) : this.session.cwd;
|
|
156
184
|
let cwdStat: fs.Stats;
|
|
@@ -181,17 +209,15 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
181
209
|
"bash",
|
|
182
210
|
label,
|
|
183
211
|
async ({ jobId, signal: runSignal, reportProgress }) => {
|
|
184
|
-
const artifactsDir = this.session.getArtifactsDir?.();
|
|
185
|
-
const extraEnv = artifactsDir ? { ARTIFACTS: artifactsDir } : undefined;
|
|
186
212
|
const { path: artifactPath, id: artifactId } =
|
|
187
213
|
(await this.session.allocateOutputArtifact?.("bash")) ?? {};
|
|
188
214
|
try {
|
|
189
215
|
const result = await executeBash(command, {
|
|
190
216
|
cwd: commandCwd,
|
|
191
|
-
sessionKey: this.session.getSessionId?.() ??
|
|
217
|
+
sessionKey: `${this.session.getSessionId?.() ?? ""}:async:${jobId}`,
|
|
192
218
|
timeout: timeoutMs,
|
|
193
219
|
signal: runSignal,
|
|
194
|
-
env:
|
|
220
|
+
env: NO_PAGER_ENV,
|
|
195
221
|
artifactPath,
|
|
196
222
|
artifactId,
|
|
197
223
|
onChunk: chunk => {
|
|
@@ -224,23 +250,16 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
224
250
|
// Track output for streaming updates (tail only)
|
|
225
251
|
const tailBuffer = new TailBuffer(DEFAULT_MAX_BYTES);
|
|
226
252
|
|
|
227
|
-
//
|
|
228
|
-
const artifactsDir = this.session.getArtifactsDir?.();
|
|
229
|
-
const extraEnv = artifactsDir ? { ARTIFACTS: artifactsDir } : undefined;
|
|
253
|
+
// Allocate artifact for truncated output storage
|
|
230
254
|
const { path: artifactPath, id: artifactId } = (await this.session.allocateOutputArtifact?.("bash")) ?? {};
|
|
231
255
|
|
|
232
|
-
const usePty =
|
|
233
|
-
this.session.settings.get("bash.virtualTerminal") === "on" &&
|
|
234
|
-
$env.PI_NO_PTY !== "1" &&
|
|
235
|
-
ctx?.hasUI === true &&
|
|
236
|
-
ctx.ui !== undefined;
|
|
256
|
+
const usePty = pty && $env.PI_NO_PTY !== "1" && ctx?.hasUI === true && ctx.ui !== undefined;
|
|
237
257
|
const result: BashResult | BashInteractiveResult = usePty
|
|
238
258
|
? await runInteractiveBashPty(ctx.ui!, {
|
|
239
259
|
command,
|
|
240
260
|
cwd: commandCwd,
|
|
241
261
|
timeoutMs,
|
|
242
262
|
signal,
|
|
243
|
-
env: extraEnv,
|
|
244
263
|
artifactPath,
|
|
245
264
|
artifactId,
|
|
246
265
|
})
|
|
@@ -249,7 +268,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
249
268
|
sessionKey: this.session.getSessionId?.() ?? undefined,
|
|
250
269
|
timeout: timeoutMs,
|
|
251
270
|
signal,
|
|
252
|
-
env:
|
|
271
|
+
env: NO_PAGER_ENV,
|
|
253
272
|
artifactPath,
|
|
254
273
|
artifactId,
|
|
255
274
|
onChunk: chunk => {
|