@oh-my-pi/pi-coding-agent 14.1.2 → 14.2.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.
Files changed (123) hide show
  1. package/CHANGELOG.md +47 -2
  2. package/package.json +8 -8
  3. package/scripts/build-binary.ts +61 -0
  4. package/src/autoresearch/helpers.ts +10 -0
  5. package/src/autoresearch/index.ts +1 -11
  6. package/src/autoresearch/tools/init-experiment.ts +1 -10
  7. package/src/autoresearch/tools/log-experiment.ts +1 -11
  8. package/src/autoresearch/tools/run-experiment.ts +1 -10
  9. package/src/bun-imports.d.ts +6 -0
  10. package/src/cli/plugin-cli.ts +23 -45
  11. package/src/commit/agentic/tools/propose-commit.ts +1 -14
  12. package/src/commit/agentic/tools/split-commit.ts +1 -15
  13. package/src/commit/utils.ts +15 -1
  14. package/src/config/model-registry.ts +3 -3
  15. package/src/config/prompt-templates.ts +4 -12
  16. package/src/config/settings-schema.ts +27 -2
  17. package/src/config/settings.ts +1 -1
  18. package/src/discovery/claude-plugins.ts +61 -6
  19. package/src/discovery/codex.ts +2 -15
  20. package/src/discovery/gemini.ts +2 -15
  21. package/src/discovery/helpers.ts +40 -1
  22. package/src/discovery/opencode.ts +2 -15
  23. package/src/edit/apply-patch/index.ts +87 -0
  24. package/src/edit/apply-patch/parser.ts +174 -0
  25. package/src/edit/diff.ts +3 -14
  26. package/src/edit/index.ts +65 -2
  27. package/src/edit/modes/apply-patch.lark +19 -0
  28. package/src/edit/modes/apply-patch.ts +63 -0
  29. package/src/edit/modes/hashline.ts +3 -3
  30. package/src/edit/modes/replace.ts +2 -13
  31. package/src/edit/read-file.ts +18 -0
  32. package/src/edit/renderer.ts +61 -33
  33. package/src/extensibility/extensions/compact-handler.ts +40 -0
  34. package/src/extensibility/extensions/runner.ts +11 -29
  35. package/src/extensibility/utils.ts +7 -1
  36. package/src/internal-urls/docs-index.generated.ts +9 -2
  37. package/src/lsp/render.ts +14 -2
  38. package/src/main.ts +1 -0
  39. package/src/mcp/manager.ts +29 -48
  40. package/src/memories/index.ts +7 -1
  41. package/src/modes/acp/acp-agent.ts +3 -16
  42. package/src/modes/components/model-selector.ts +15 -24
  43. package/src/modes/components/plugin-settings.ts +16 -5
  44. package/src/modes/components/read-tool-group.ts +92 -9
  45. package/src/modes/components/settings-defs.ts +18 -0
  46. package/src/modes/components/settings-selector.ts +2 -6
  47. package/src/modes/components/tool-execution.ts +61 -28
  48. package/src/modes/controllers/event-controller.ts +3 -1
  49. package/src/modes/controllers/extension-ui-controller.ts +99 -150
  50. package/src/modes/controllers/selector-controller.ts +3 -12
  51. package/src/modes/interactive-mode.ts +4 -2
  52. package/src/modes/print-mode.ts +4 -22
  53. package/src/modes/rpc/rpc-mode.ts +18 -38
  54. package/src/modes/shared.ts +10 -1
  55. package/src/modes/utils/ui-helpers.ts +6 -2
  56. package/src/plan-mode/approved-plan.ts +5 -4
  57. package/src/prompts/system/subagent-system-prompt.md +4 -4
  58. package/src/prompts/system/subagent-user-prompt.md +2 -2
  59. package/src/prompts/system/system-prompt.md +208 -243
  60. package/src/prompts/tools/apply-patch.md +67 -0
  61. package/src/prompts/tools/ast-edit.md +18 -23
  62. package/src/prompts/tools/ast-grep.md +24 -32
  63. package/src/prompts/tools/bash.md +11 -23
  64. package/src/prompts/tools/debug.md +8 -22
  65. package/src/prompts/tools/find.md +0 -4
  66. package/src/prompts/tools/grep.md +3 -5
  67. package/src/prompts/tools/hashline.md +16 -10
  68. package/src/prompts/tools/python.md +10 -14
  69. package/src/prompts/tools/read.md +17 -24
  70. package/src/prompts/tools/task.md +57 -21
  71. package/src/prompts/tools/todo-write.md +45 -67
  72. package/src/session/agent-session.ts +4 -4
  73. package/src/session/session-manager.ts +15 -7
  74. package/src/session/streaming-output.ts +24 -0
  75. package/src/slash-commands/builtin-registry.ts +3 -14
  76. package/src/task/executor.ts +13 -34
  77. package/src/task/index.ts +82 -18
  78. package/src/task/simple-mode.ts +27 -0
  79. package/src/task/template.ts +17 -3
  80. package/src/task/types.ts +77 -30
  81. package/src/tools/ask.ts +2 -4
  82. package/src/tools/ast-edit.ts +4 -15
  83. package/src/tools/ast-grep.ts +8 -27
  84. package/src/tools/bash-skill-urls.ts +9 -7
  85. package/src/tools/bash.ts +4 -12
  86. package/src/tools/browser.ts +1 -1
  87. package/src/tools/fetch.ts +1 -14
  88. package/src/tools/file-recorder.ts +35 -0
  89. package/src/tools/find.ts +6 -3
  90. package/src/tools/gh-format.ts +12 -0
  91. package/src/tools/gh-renderer.ts +1 -8
  92. package/src/tools/gh.ts +6 -13
  93. package/src/tools/grep.ts +9 -22
  94. package/src/tools/jtd-to-json-schema.ts +16 -0
  95. package/src/tools/match-line-format.ts +20 -0
  96. package/src/tools/path-utils.ts +30 -2
  97. package/src/tools/plan-mode-guard.ts +6 -5
  98. package/src/tools/python.ts +1 -1
  99. package/src/tools/read.ts +1 -1
  100. package/src/tools/render-utils.ts +38 -6
  101. package/src/tools/renderers.ts +1 -0
  102. package/src/tools/ssh.ts +3 -11
  103. package/src/tools/submit-result.ts +1 -13
  104. package/src/tools/todo-write.ts +137 -103
  105. package/src/tools/write.ts +2 -23
  106. package/src/tui/code-cell.ts +12 -7
  107. package/src/utils/edit-mode.ts +3 -2
  108. package/src/utils/git.ts +1 -1
  109. package/src/vim/engine.ts +41 -58
  110. package/src/web/scrapers/crates-io.ts +1 -14
  111. package/src/web/scrapers/types.ts +13 -0
  112. package/src/web/search/providers/base.ts +13 -0
  113. package/src/web/search/providers/brave.ts +2 -5
  114. package/src/web/search/providers/codex.ts +20 -24
  115. package/src/web/search/providers/gemini.ts +39 -1
  116. package/src/web/search/providers/jina.ts +2 -5
  117. package/src/web/search/providers/kagi.ts +3 -8
  118. package/src/web/search/providers/kimi.ts +3 -7
  119. package/src/web/search/providers/parallel.ts +3 -8
  120. package/src/web/search/providers/synthetic.ts +3 -7
  121. package/src/web/search/providers/tavily.ts +15 -11
  122. package/src/web/search/providers/utils.ts +36 -0
  123. package/src/web/search/providers/zai.ts +3 -7
@@ -5,28 +5,52 @@ Launches subagents to parallelize workflows.
5
5
  - Use the `poll` tool to wait until completion. You **MUST NOT** poll `read jobs://` in a loop.
6
6
  {{/if}}
7
7
 
8
+ {{#if defaultMode}}
9
+ Current input mode: `default`. Shared `context` and custom task-call `schema` are available.
10
+ {{/if}}
11
+ {{#if schemaFreeMode}}
12
+ Current input mode: `schema-free`. Shared `context` is available; custom task-call `schema` is disabled. For structured output, rely on the agent definition or inherited session schema.
13
+ {{/if}}
14
+ {{#if independentMode}}
15
+ Current input mode: `independent`. Shared `context` and custom `schema` are both disabled. Every assignment must stand on its own.
16
+ {{/if}}
17
+
18
+ {{#if contextEnabled}}
8
19
  Subagents lack your conversation history. Every decision, file content, and user requirement they need **MUST** be explicit in `context` or `assignment`.
20
+ {{else}}
21
+ Subagents lack your conversation history. Every decision, file content, and user requirement they need **MUST** be explicit in each task `assignment`.
22
+ {{/if}}
9
23
 
10
24
  <parameters>
11
25
  - `agent`: Agent type for all tasks.
12
26
  - `.id`: CamelCase, max 32 chars
13
27
  - `.description`: UI display only — subagent never sees it
14
28
  - `.assignment`: Complete self-contained instructions. One-liners PROHIBITED; missing acceptance criteria = too vague.
29
+ {{#if contextEnabled}}
15
30
  - `context`: Shared background prepended to every assignment. Session-specific info only.
31
+ {{/if}}
32
+ {{#if customSchemaEnabled}}
16
33
  - `schema`: JSON-encoded JTD schema for expected output. Format lives here — **MUST NOT** be duplicated in assignments.
34
+ {{/if}}
17
35
  - `tasks`: Tasks to execute in parallel.
36
+ {{#if isolationEnabled}}
18
37
  - `isolated`: Run in isolated environment; returns patches. Use when tasks edit overlapping files.
38
+ {{/if}}
19
39
  </parameters>
20
40
 
21
41
  <critical>
42
+ {{#if contextEnabled}}
22
43
  - **MUST NOT** duplicate shared constraints across assignments — put them in `context` once.
44
+ {{else}}
45
+ - Every `assignment` must repeat any constraints, reference paths, and acceptance criteria it needs — there is no shared `context` field.
46
+ {{/if}}
23
47
  - **MUST NOT** tell tasks to run project-wide build/test/lint. Parallel agents share the working tree; each task edits, stops. Caller verifies after all complete.
24
- - For large payloads (traces, JSON blobs), write to `local://<path>` and pass the path in context.
25
- - Prefer `task` agents that investigate **and** edit in one pass. Only launch a dedicated read-only discovery step when the affected files are genuinely unknown and cannot be inferred from the task description.
48
+ - For large payloads (traces, JSON blobs), write to `local://<path>` and pass the path in {{#if contextEnabled}}`context`{{else}}the relevant `assignment`{{/if}}.
49
+ - Prefer `task` agents that investigate **and** edit in one pass. Launch a dedicated read-only discovery step only when affected files are genuinely unknown.
26
50
  </critical>
27
51
 
28
52
  <scope>
29
- Each task: **at most 3–5 files**. Globs in file paths, "update all", or package-wide scope = too broad. Enumerate files explicitly and fan out to a cluster of agents.
53
+ Each task: **at most 3–5 files**. Globs, "update all", or package-wide scope = too broad. Enumerate files explicitly and fan out to a cluster of agents.
30
54
  </scope>
31
55
 
32
56
  <parallelization>
@@ -38,10 +62,12 @@ Each task: **at most 3–5 files**. Globs in file paths, "update all", or packag
38
62
  |API exports|Callers|Need signatures|
39
63
  |Core module|Dependents|Import dependency|
40
64
  |Schema/migration|App logic|Schema dependency|
65
+
41
66
  **Safe to parallelize:** independent modules, isolated file-scoped refactors, tests for existing code.
42
67
  </parallelization>
43
68
 
44
69
  <templates>
70
+ {{#if contextEnabled}}
45
71
  **context:**
46
72
  ```
47
73
  ## Goal ← one sentence: what the batch accomplishes
@@ -50,6 +76,9 @@ Each task: **at most 3–5 files**. Globs in file paths, "update all", or packag
50
76
  ## API Contract ← exact types/signatures if tasks share an interface (omit if N/A)
51
77
  ## Acceptance ← definition of done; build/lint runs AFTER all tasks complete
52
78
  ```
79
+ {{else}}
80
+ No shared `context` field exists in this mode. Fold goal, non-goals, constraints, and acceptance criteria into each `assignment`.
81
+ {{/if}}
53
82
  **assignment:**
54
83
  ```
55
84
  ## Target ← exact file paths; named symbols; explicit non-goals
@@ -61,68 +90,75 @@ Each task: **at most 3–5 files**. Globs in file paths, "update all", or packag
61
90
 
62
91
  <checklist>
63
92
  Before invoking:
93
+ {{#if contextEnabled}}
64
94
  - `context` contains only session-specific info
95
+ {{else}}
96
+ - Every `assignment` includes its own goal, constraints, and acceptance criteria (no shared context)
97
+ {{/if}}
65
98
  - Every `assignment` follows the template; no one-liners; edge cases covered
66
99
  - Tasks are truly parallel — you can articulate why none depends on another's output
67
100
  - File paths are explicit; no globs
101
+ {{#if customSchemaEnabled}}
68
102
  - `schema` is set if you expect structured output
103
+ {{else}}
104
+ - Do not pass a custom task-call `schema` in this mode
105
+ {{/if}}
69
106
  </checklist>
70
107
 
108
+ {{#if contextEnabled}}
71
109
  <example label="Rename exported symbol + update all call sites">
72
- Two tasks with non-overlapping file sets. Neither depends on the other's edits.
110
+ Two tasks with non-overlapping file sets demonstrates scope partitioning.
73
111
 
74
112
  <context>
75
113
  ## Goal
76
114
  Rename `parseConfig` → `loadConfig` in `src/config/parser.ts` and all callers.
77
115
  ## Non-goals
78
- Do not change function behavior, signature, or tests rename only.
116
+ No behavior or signature changes; rename only.
79
117
  ## Acceptance (global)
80
118
  Caller runs `bun check:ts` after both tasks complete. Tasks must NOT run it.
81
119
  </context>
82
120
  <tasks>
83
121
  <task name="RenameExport">
84
- <description>Rename the export in parser.ts</description>
85
122
  <assignment>
86
123
  ## Target
87
- - File: `src/config/parser.ts`
88
- - Symbol: exported function `parseConfig`
124
+ - `src/config/parser.ts`: function `parseConfig`
125
+ - If `src/config/index.ts` re-exports it, update the re-export
89
126
  - Non-goals: do not touch callers or tests
90
127
 
91
128
  ## Change
92
- - Rename `parseConfig` → `loadConfig` (declaration + any JSDoc referencing it)
93
- - If `src/config/index.ts` re-exports `parseConfig`, update that re-export too
129
+ - Rename `parseConfig` → `loadConfig` (declaration + any JSDoc references)
94
130
 
95
131
  ## Edge Cases
96
- - If the function is overloaded, rename all overload signatures
97
- - Internal helpers named `_parseConfigValue` or similar: leave untouched different symbols
132
+ - Rename all overload signatures if overloaded
133
+ - Internal helpers like `_parseConfigValue` are different symbolsleave untouched
98
134
  - Do not add a backwards-compat alias
99
135
 
100
136
  ## Acceptance
101
- - `src/config/parser.ts` exports `loadConfig`; `parseConfig` no longer appears as a top-level export in that file
137
+ - `parseConfig` no longer appears as a top-level export in `parser.ts`
102
138
  </assignment>
103
139
  </task>
104
140
  <task name="UpdateCallers">
105
- <description>Update import and call sites in consuming modules</description>
106
141
  <assignment>
107
142
  ## Target
108
- - Files: `src/cli/init.ts`, `src/server/bootstrap.ts`, `src/worker/index.ts`
109
- - Non-goals: do not touch `src/config/parser.ts` or `src/config/index.ts` — handled by sibling task
143
+ - `src/cli/init.ts`, `src/server/bootstrap.ts`, `src/worker/index.ts`
144
+ - Non-goals: do not touch `src/config/parser.ts` or `src/config/index.ts`
110
145
 
111
146
  ## Change
112
- - In each file: replace `import { parseConfig }` → `import { loadConfig }` from its config path
147
+ - Replace `import { parseConfig }` → `import { loadConfig }`
113
148
  - Replace every call site `parseConfig(` → `loadConfig(`
149
+ - For `import * as cfg` users, update `cfg.parseConfig` property access
114
150
 
115
151
  ## Edge Cases
116
- - If a file spreads the import (`import * as cfg from ""`) and calls `cfg.parseConfig(…)`, update the property access too
117
- - String literals containing "parseConfig" (log messages, comments) are documentation leave them
118
- - If any file re-exports `parseConfig` to an external package boundary, keep the old name via `export { loadConfig as parseConfig }` and add a `// TODO: remove after next major` comment
152
+ - String literals containing "parseConfig" (logs, comments) are documentation leave them
153
+ - If a file re-exports to an external package boundary, keep the old name via `export { loadConfig as parseConfig }` with a `// TODO: remove after next major` comment
119
154
 
120
155
  ## Acceptance
121
- - No bare reference to `parseConfig` (as identifier, not string) remains in the three target files
156
+ - No bare `parseConfig` identifier remains in the three target files
122
157
  </assignment>
123
158
  </task>
124
159
  </tasks>
125
160
  </example>
161
+ {{/if}}
126
162
 
127
163
  {{#list agents join="\n"}}
128
164
  ### Agent: {{name}}
@@ -1,13 +1,29 @@
1
- Manages a phased task list. Submit an `ops` arrayeach op mutates state incrementally.
2
- **Primary op: `update`.** Use it to mark tasks `in_progress` or `completed`. Only reach for other ops when the structure itself needs to change.
1
+ Manages a phased task list. Each field is a verb set the ones you need in a single call.
2
+ The next pending task is auto-promoted to `in_progress` after completing the current one.
3
3
 
4
- <critical>
5
- You **MUST** call this tool twice per task:
6
- 1. Before beginning — `{op: "update", id: "task-N", status: "in_progress"}`
7
- 2. Immediately after finishing — `{op: "update", id: "task-N", status: "completed"}`
4
+ <protocol>
5
+ ## Fields
8
6
 
9
- You **MUST** keep exactly one task `in_progress` at all times. Mark `completed` immediately — no batching.
10
- </critical>
7
+ |Field|Type|When to use|
8
+ |---|---|---|
9
+ |`phases`|Phase[]|Initial setup, or full restructure when the plan changes significantly|
10
+ |`complete`|string[]|Mark tasks done|
11
+ |`start`|string|Jump to a specific task out of order|
12
+ |`abandon`|string[]|Drop tasks intentionally|
13
+ |`remove`|string[]|Remove tasks that are no longer relevant|
14
+ |`add_notes`|{id, notes}[]|Append runtime observations to tasks|
15
+ |`add_tasks`|{phase, content, details?}[]|Add tasks to a phase (by name or ID)|
16
+ |`add_phase`|{name, tasks?}|Add a new phase of work discovered mid-task|
17
+
18
+ ## Task Anatomy
19
+ - `content`: Short label (5-10 words). What is being done, not how.
20
+ - `details`: File paths, implementation steps, edge cases. Shown only when the task is active.
21
+
22
+ ## Rules
23
+ - Mark tasks completed immediately after finishing — never defer
24
+ - Complete phases in order — do not skip ahead while earlier ones are pending
25
+ - On blockers: add a new task describing the blocker
26
+ </protocol>
11
27
 
12
28
  <conditions>
13
29
  Create a todo list when:
@@ -17,73 +33,35 @@ Create a todo list when:
17
33
  4. New instructions arrive mid-task — capture before proceeding
18
34
  </conditions>
19
35
 
20
- <protocol>
21
- ## Operations
36
+ <example name="initial-setup">
37
+ {phases: [
38
+ {name: "Investigation", tasks: [{content: "Read source"}, {content: "Map callsites"}]},
39
+ {name: "Implementation", tasks: [{content: "Apply fix", details: "Update parser.ts to handle edge case in line 42"}, {content: "Run tests"}]}
40
+ ]}
41
+ </example>
22
42
 
23
- |op|When to use|
24
- |---|---|
25
- |`update`|Mark a task in_progress / completed / abandoned, or edit content/notes|
26
- |`replace`|Initial setup, or full restructure when the plan changes significantly|
27
- |`add_phase`|Add a new phase of work discovered mid-task|
28
- |`add_task`|Add a task to an existing phase|
29
- |`remove_task`|Remove a task that is no longer relevant|
43
+ <example name="complete">
44
+ {complete: ["task-2", "task-3"]}
45
+ </example>
30
46
 
31
- ## Statuses
47
+ <example name="add-notes">
48
+ {add_notes: [{id: "task-3", notes: "Found edge case in parser — needs null check"}]}
49
+ </example>
32
50
 
33
- |Status|Meaning|
34
- |---|---|
35
- |`pending`|Not started|
36
- |`in_progress`|Currently working — exactly one at a time|
37
- |`completed`|Fully done|
38
- |`abandoned`|Dropped intentionally|
51
+ <example name="add-task">
52
+ {add_tasks: [{phase: "Implementation", content: "Handle retries", details: "Cap exponential backoff in retry.ts"}]}
53
+ </example>
39
54
 
40
- ## Rules
41
- - You **MUST** mark `in_progress` **before** starting work, not after
42
- - You **MUST** mark `completed` **immediately** — never defer
43
- - You **MUST** keep exactly **one** task `in_progress`
44
- - You **MUST** complete phases in order — do not mark later tasks `completed` while earlier ones are `pending`
45
- - On blockers: keep `in_progress`, add a new task describing the blocker
46
- - Multiple ops can be batched in one call (e.g., complete current + start next)
47
- </protocol>
55
+ <example name="add-phase">
56
+ {add_phase: {name: "Cleanup", tasks: [{content: "Remove dead code"}]}}
57
+ </example>
48
58
 
49
- ## Task Anatomy
50
- - `content`: Short label (5-10 words). What is being done, not how.
51
- - `details`: File paths, implementation steps, edge cases. Shown only when task is active.
52
- - `notes`: Runtime observations added during execution.
59
+ <example name="combined">
60
+ {complete: ["task-2"], add_notes: [{id: "task-3", notes: "Needs extra validation"}]}
61
+ </example>
53
62
 
54
63
  <avoid>
55
64
  - Single-step tasks — act directly
56
65
  - Conversational or informational requests
57
66
  - Tasks completable in under 3 trivial steps
58
67
  </avoid>
59
-
60
- <example name="start-task">
61
- Mark task-2 in_progress before beginning work:
62
- ops: [{op: "update", id: "task-2", status: "in_progress"}]
63
- </example>
64
-
65
- <example name="complete-and-advance">
66
- Finish task-2 and start task-3 in one call:
67
- ops: [
68
- {op: "update", id: "task-2", status: "completed"},
69
- {op: "update", id: "task-3", status: "in_progress"}
70
- ]
71
- </example>
72
-
73
- <example name="add_task">
74
- Add a follow-up task with implementation specifics in `details`:
75
- ops: [{op: "add_task", phase: "Implementation", after: "task-2", task: {content: "Handle retries", details: "Update retry.ts to cap exponential backoff and preserve AbortSignal handling", status: "pending"}}]
76
- </example>
77
-
78
- <example name="initial-setup">
79
- Replace is for setup only. Prefer add_phase / add_task for incremental additions.
80
- ops: [{op: "replace", phases: [
81
- {name: "Investigation", tasks: [{content: "Read source"}, {content: "Map callsites"}]},
82
- {name: "Implementation", tasks: [{content: "Apply fix", details: "Update parser.ts to handle edge case in line 42"}, {content: "Run tests"}]}
83
- ]}]
84
- </example>
85
-
86
- <example name="skip">
87
- User: "What does this function do?" / "Add a comment" / "Run npm install"
88
- → Do it directly. No list needed.
89
- </example>
@@ -132,7 +132,7 @@ import { resolveThinkingLevelForModel, toReasoningEffort } from "../thinking";
132
132
  import { assertEditableFile } from "../tools/auto-generated-guard";
133
133
  import type { CheckpointState } from "../tools/checkpoint";
134
134
  import { outputMeta } from "../tools/output-meta";
135
- import { resolveToCwd } from "../tools/path-utils";
135
+ import { normalizeLocalScheme, resolveToCwd } from "../tools/path-utils";
136
136
  import { isAutoQaEnabled } from "../tools/report-tool-issue";
137
137
  import { getLatestTodoPhasesFromEntries, type TodoItem, type TodoPhase } from "../tools/todo-write";
138
138
  import { ToolError } from "../tools/tool-errors";
@@ -2445,8 +2445,8 @@ export class AgentSession {
2445
2445
  const state = this.#planModeState;
2446
2446
  if (!state?.enabled) return null;
2447
2447
  const sessionPlanUrl = "local://PLAN.md";
2448
- const resolvedPlanPath = state.planFilePath.startsWith("local://")
2449
- ? resolveLocalUrlToPath(state.planFilePath, {
2448
+ const resolvedPlanPath = state.planFilePath.startsWith("local:")
2449
+ ? resolveLocalUrlToPath(normalizeLocalScheme(state.planFilePath), {
2450
2450
  getArtifactsDir: () => this.sessionManager.getArtifactsDir(),
2451
2451
  getSessionId: () => this.sessionManager.getSessionId(),
2452
2452
  })
@@ -2456,7 +2456,7 @@ export class AgentSession {
2456
2456
  getSessionId: () => this.sessionManager.getSessionId(),
2457
2457
  });
2458
2458
  const displayPlanPath =
2459
- state.planFilePath.startsWith("local://") || resolvedPlanPath !== resolvedSessionPlan
2459
+ state.planFilePath.startsWith("local:") || resolvedPlanPath !== resolvedSessionPlan
2460
2460
  ? state.planFilePath
2461
2461
  : sessionPlanUrl;
2462
2462
 
@@ -286,6 +286,10 @@ export type ReadonlySessionManager = Pick<
286
286
  | "putBlob"
287
287
  >;
288
288
 
289
+ function createSessionId(): string {
290
+ return Bun.randomUUIDv7();
291
+ }
292
+
289
293
  /** Generate a unique short ID (8 hex chars, collision-checked) */
290
294
  function generateId(byId: { has(id: string): boolean }): string {
291
295
  for (let i = 0; i < 100; i++) {
@@ -1500,7 +1504,7 @@ export class SessionManager {
1500
1504
  this.#fileEntries = await loadEntriesFromFile(this.#sessionFile, this.storage);
1501
1505
  if (this.#fileEntries.length > 0) {
1502
1506
  const header = this.#fileEntries.find(e => e.type === "session") as SessionHeader | undefined;
1503
- this.#sessionId = header?.id ?? Snowflake.next();
1507
+ this.#sessionId = header?.id ?? createSessionId();
1504
1508
  this.#sessionName = header?.title;
1505
1509
  this.#titleSource = header?.titleSource;
1506
1510
 
@@ -1549,7 +1553,7 @@ export class SessionManager {
1549
1553
  this.#persistErrorReported = false;
1550
1554
 
1551
1555
  // Create new session ID and header
1552
- this.#sessionId = Snowflake.next();
1556
+ this.#sessionId = createSessionId();
1553
1557
  const timestamp = new Date().toISOString();
1554
1558
  const fileTimestamp = timestamp.replace(/[:.]/g, "-");
1555
1559
  this.#sessionFile = path.join(this.getSessionDir(), `${fileTimestamp}_${this.#sessionId}.jsonl`);
@@ -1680,7 +1684,7 @@ export class SessionManager {
1680
1684
  this.#persistChain = Promise.resolve();
1681
1685
  this.#persistError = undefined;
1682
1686
  this.#persistErrorReported = false;
1683
- this.#sessionId = Snowflake.next();
1687
+ this.#sessionId = createSessionId();
1684
1688
  this.#sessionName = undefined;
1685
1689
  this.#titleSource = undefined;
1686
1690
  const timestamp = new Date().toISOString();
@@ -2036,14 +2040,18 @@ export class SessionManager {
2036
2040
  if (this.#needsFullRewriteOnNextPersist || !this.#flushed) {
2037
2041
  // Full flush: rewrite the entire file atomically to avoid
2038
2042
  // duplicating entries if the file already exists (e.g. from ensureOnDisk).
2039
- void this.#rewriteFile();
2043
+ // Errors are already surfaced through #persistChain/#persistError; the
2044
+ // caller intentionally fires-and-forgets, so swallow the awaited rejection
2045
+ // here to avoid an unhandled rejection when the persist dir races with
2046
+ // test-level tempDir cleanup.
2047
+ this.#rewriteFile().catch(() => {});
2040
2048
  } else {
2041
- void this.#queuePersistTask(async () => {
2049
+ this.#queuePersistTask(async () => {
2042
2050
  const writer = this.#ensurePersistWriter();
2043
2051
  if (!writer) return;
2044
2052
  const persistedEntry = await prepareEntryForPersistence(entry, this.#blobStore);
2045
2053
  await writer.write(persistedEntry);
2046
- });
2054
+ }).catch(() => {});
2047
2055
  }
2048
2056
  }
2049
2057
 
@@ -2554,7 +2562,7 @@ export class SessionManager {
2554
2562
  // Filter out LabelEntry from path - we'll recreate them from the resolved map
2555
2563
  const pathWithoutLabels = branchPath.filter(e => e.type !== "label");
2556
2564
 
2557
- const newSessionId = Snowflake.next();
2565
+ const newSessionId = createSessionId();
2558
2566
  const timestamp = new Date().toISOString();
2559
2567
  const fileTimestamp = timestamp.replace(/[:.]/g, "-");
2560
2568
  const newSessionFile = path.join(this.getSessionDir(), `${fileTimestamp}_${newSessionId}.jsonl`);
@@ -1,3 +1,4 @@
1
+ import type { AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
1
2
  import { sanitizeText } from "@oh-my-pi/pi-natives";
2
3
  import { formatBytes } from "../tools/render-utils";
3
4
  import { sanitizeWithOptionalSixelPassthrough } from "../utils/sixel";
@@ -750,3 +751,26 @@ export function formatHeadTruncationNotice(
750
751
  const notice = `[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}. Use sel=L${nextOffset} to continue]`;
751
752
  return `\n\n${notice}`;
752
753
  }
754
+
755
+ // =============================================================================
756
+ // Streaming tail update helper (shared by bash/ssh tools)
757
+ // =============================================================================
758
+
759
+ /**
760
+ * Build an onChunk handler that appends to a TailBuffer and emits a streaming
761
+ * update (when `onUpdate` is defined) with the buffer's current text.
762
+ */
763
+ export function streamTailUpdates<TDetails, TInput = unknown>(
764
+ tailBuffer: TailBuffer,
765
+ onUpdate: AgentToolUpdateCallback<TDetails, TInput> | undefined,
766
+ ): (chunk: string) => void {
767
+ return chunk => {
768
+ tailBuffer.append(chunk);
769
+ if (onUpdate) {
770
+ onUpdate({
771
+ content: [{ type: "text", text: tailBuffer.text() }],
772
+ details: {} as TDetails,
773
+ });
774
+ }
775
+ };
776
+ }
@@ -8,6 +8,7 @@ import type { SettingPath, SettingValue } from "../config/settings";
8
8
  import { settings } from "../config/settings";
9
9
  import {
10
10
  clearClaudePluginRootsCache,
11
+ clearPluginRootsAndCaches,
11
12
  resolveActiveProjectRegistryPath,
12
13
  resolveOrDefaultProjectRegistryPath,
13
14
  } from "../discovery/helpers.js";
@@ -643,13 +644,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
643
644
  ),
644
645
  marketplacesCacheDir: getMarketplacesCacheDir(),
645
646
  pluginsCacheDir: getPluginsCacheDir(),
646
- clearPluginRootsCache: (extraPaths?: readonly string[]) => {
647
- const home = os.homedir();
648
- invalidateFsCache(path.join(home, ".claude", "plugins", "installed_plugins.json"));
649
- invalidateFsCache(path.join(home, getConfigDirName(), "plugins", "installed_plugins.json"));
650
- for (const p of extraPaths ?? []) invalidateFsCache(p);
651
- clearClaudePluginRootsCache();
652
- },
647
+ clearPluginRootsCache: clearPluginRootsAndCaches,
653
648
  });
654
649
 
655
650
  try {
@@ -837,13 +832,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
837
832
  ),
838
833
  marketplacesCacheDir: getMarketplacesCacheDir(),
839
834
  pluginsCacheDir: getPluginsCacheDir(),
840
- clearPluginRootsCache: (extraPaths?: readonly string[]) => {
841
- const home = os.homedir();
842
- invalidateFsCache(path.join(home, ".claude", "plugins", "installed_plugins.json"));
843
- invalidateFsCache(path.join(home, getConfigDirName(), "plugins", "installed_plugins.json"));
844
- for (const p of extraPaths ?? []) invalidateFsCache(p);
845
- clearClaudePluginRootsCache();
846
- },
835
+ clearPluginRootsCache: clearPluginRootsAndCaches,
847
836
  });
848
837
 
849
838
  switch (sub) {
@@ -14,6 +14,7 @@ import type { PromptTemplate } from "../config/prompt-templates";
14
14
  import { Settings } from "../config/settings";
15
15
  import { SETTINGS_SCHEMA, type SettingPath } from "../config/settings-schema";
16
16
  import type { CustomTool } from "../extensibility/custom-tools/types";
17
+ import { runExtensionCompact, runExtensionSetModel } from "../extensibility/extensions/compact-handler";
17
18
  import type { Skill } from "../extensibility/skills";
18
19
  import { callTool } from "../mcp/client";
19
20
  import type { MCPManager } from "../mcp/manager";
@@ -24,7 +25,7 @@ import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
24
25
  import type { AuthStorage } from "../session/auth-storage";
25
26
  import { SessionManager } from "../session/session-manager";
26
27
  import { type ContextFileEntry, truncateTail } from "../tools";
27
- import { jtdToJsonSchema } from "../tools/jtd-to-json-schema";
28
+ import { jtdToJsonSchema, normalizeSchema } from "../tools/jtd-to-json-schema";
28
29
  import { ToolAbortError } from "../tools/tool-errors";
29
30
  import type { EventBus } from "../utils/event-bus";
30
31
  import { buildNamedToolChoice } from "../utils/tool-choice";
@@ -163,20 +164,8 @@ function parseStringifiedJson(value: unknown): unknown {
163
164
  }
164
165
  }
165
166
 
166
- function normalizeOutputSchema(schema: unknown): { normalized?: unknown; error?: string } {
167
- if (schema === undefined || schema === null) return {};
168
- if (typeof schema === "string") {
169
- try {
170
- return { normalized: JSON.parse(schema) };
171
- } catch (err) {
172
- return { error: err instanceof Error ? err.message : String(err) };
173
- }
174
- }
175
- return { normalized: schema };
176
- }
177
-
178
167
  function buildOutputValidator(schema: unknown): { validate?: ValidateFunction; error?: string } {
179
- const { normalized, error } = normalizeOutputSchema(schema);
168
+ const { normalized, error } = normalizeSchema(schema);
180
169
  if (error) return { error };
181
170
  if (normalized === undefined) return {};
182
171
  const jsonSchema = jtdToJsonSchema(normalized);
@@ -300,7 +289,7 @@ export function finalizeSubprocessOutput(args: FinalizeSubprocessOutputArgs): Fi
300
289
  }
301
290
  } else {
302
291
  const allowFallback = exitCode === 0 && !doneAborted && !signalAborted;
303
- const { normalized: normalizedSchema, error: schemaError } = normalizeOutputSchema(outputSchema);
292
+ const { normalized: normalizedSchema, error: schemaError } = normalizeSchema(outputSchema);
304
293
  const hasOutputSchema = normalizedSchema !== undefined && !schemaError;
305
294
  const fallback = allowFallback ? resolveFallbackCompletion(rawOutput, outputSchema) : null;
306
295
  if (fallback) {
@@ -317,9 +306,14 @@ export function finalizeSubprocessOutput(args: FinalizeSubprocessOutputArgs): Fi
317
306
  exitCode = 0;
318
307
  stderr = "";
319
308
  } else if (exitCode === 0) {
309
+ const hasRawOutput = rawOutput.trim().length > 0;
320
310
  rawOutput = rawOutput
321
311
  ? `${SUBAGENT_WARNING_MISSING_SUBMIT_RESULT}\n\n${rawOutput}`
322
312
  : SUBAGENT_WARNING_MISSING_SUBMIT_RESULT;
313
+ if (hasOutputSchema || !hasRawOutput) {
314
+ exitCode = 1;
315
+ stderr = SUBAGENT_WARNING_MISSING_SUBMIT_RESULT;
316
+ }
323
317
  }
324
318
  }
325
319
 
@@ -950,7 +944,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
950
944
  const mcpProxyTools = options.mcpManager ? createMCPProxyTools(options.mcpManager) : [];
951
945
  const enableMCP = !options.mcpManager;
952
946
 
953
- const { normalized: normalizedOutputSchema } = normalizeOutputSchema(outputSchema);
947
+ const { normalized: normalizedOutputSchema } = normalizeSchema(outputSchema);
954
948
 
955
949
  const { session } = await createAgentSession({
956
950
  cwd: worktree ?? cwd,
@@ -1050,12 +1044,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1050
1044
  setActiveTools: (toolNames: string[]) =>
1051
1045
  session.setActiveToolsByName(toolNames.filter(name => !parentOwnedToolNames.has(name))),
1052
1046
  getCommands: () => [],
1053
- setModel: async model => {
1054
- const key = await session.modelRegistry.getApiKey(model);
1055
- if (!key) return false;
1056
- await session.setModel(model);
1057
- return true;
1058
- },
1047
+ setModel: model => runExtensionSetModel(session, model),
1059
1048
  getThinkingLevel: () => session.thinkingLevel,
1060
1049
  setThinkingLevel: level => session.setThinkingLevel(level),
1061
1050
  getSessionName: () => session.sessionManager.getSessionName(),
@@ -1071,14 +1060,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1071
1060
  shutdown: () => {},
1072
1061
  getContextUsage: () => session.getContextUsage(),
1073
1062
  getSystemPrompt: () => session.systemPrompt,
1074
- compact: async instructionsOrOptions => {
1075
- const instructions = typeof instructionsOrOptions === "string" ? instructionsOrOptions : undefined;
1076
- const options =
1077
- instructionsOrOptions && typeof instructionsOrOptions === "object"
1078
- ? instructionsOrOptions
1079
- : undefined;
1080
- await session.compact(instructions, options);
1081
- },
1063
+ compact: instructionsOrOptions => runExtensionCompact(session, instructionsOrOptions),
1082
1064
  },
1083
1065
  );
1084
1066
  extensionRunner.onError(err => {
@@ -1129,10 +1111,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1129
1111
 
1130
1112
  await session.waitForIdle();
1131
1113
  if (!submitResultCalled && !abortSignal.aborted) {
1132
- aborted = true;
1133
- exitCode = 1;
1134
- abortReasonText ??= SUBAGENT_WARNING_MISSING_SUBMIT_RESULT;
1135
- error ??= SUBAGENT_WARNING_MISSING_SUBMIT_RESULT;
1114
+ exitCode = 0;
1136
1115
  }
1137
1116
 
1138
1117
  const lastAssistant = session.getLastAssistantMessage();