@henryqw/pi-herdr-tools 1.3.0 → 1.3.2

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # `@henryqw/pi-herdr-tools`
2
2
 
3
- Clone a conversation into a Herdr tab or worktree, ask a side question and merge its answer back, name the conversation, and finish the worktree safely. One package and one extension entry point provide `/clone-tab`, `/clone-worktree`, `/btw`, `/rename`, and `/done`. Herdr also shows the pane as blocked while a Pi dialog waits for your answer.
3
+ Clone a conversation into a Herdr tab or worktree. Ask a side question and merge its answer back into Main. Give the conversation a name, and finish the worktree safely. One package and one extension entry point supply `/clone-tab`, `/clone-worktree`, `/btw`, `/rename`, and `/done`. Herdr also shows the pane as blocked while a Pi dialog waits for your answer.
4
4
 
5
5
  ![Architecture overview showing one Pi extension entry point registering four internal handlers, with shared Herdr CLI and task-model routing dependencies.](./docs/workflow-overview.svg)
6
6
 
@@ -11,11 +11,11 @@ pi install npm:@henryqw/pi-task-models
11
11
  pi install npm:@henryqw/pi-herdr-tools
12
12
  ```
13
13
 
14
- Requires Pi 1.1.0 or later in the 1.x series and Herdr 0.9.3 or later. Herdr operations require a Herdr-managed Pi pane; outside Herdr, rename still sets the Pi session title. Run `/task-models`, configure `fast`, and verify it no longer says `not configured` before using title generation or side threads. Dialog status also requires Herdr's own Pi integration (`~/.pi/agent/extensions/herdr-agent-state.ts`), which Herdr installs; this package only publishes the status event.
14
+ Requires Pi 1.1.0 or later in the 1.x series and Herdr 0.9.3 or later. Herdr operations require a Herdr-managed Pi pane. Outside Herdr, rename still sets the Pi session title. Before you use title generation or side threads, do these steps. Run `/task-models`. Configure `fast`. Make sure that it does not show `not configured`. Dialog status also requires the Herdr Pi integration (`~/.pi/agent/extensions/herdr-agent-state.ts`). Herdr installs this integration. This package only publishes the status event.
15
15
 
16
16
  ### Migrating from the merged packages
17
17
 
18
- `@henryqw/pi-herdr-btw`, `@henryqw/pi-herdr-clone`, and `@henryqw/pi-herdr-rename` are retired. `pi-herdr-clone` previously absorbed `pi-herdr-done`; that package is retired too. Install the replacement first, then remove any old installs before restarting Pi:
18
+ `@henryqw/pi-herdr-btw`, `@henryqw/pi-herdr-clone`, and `@henryqw/pi-herdr-rename` are retired. Before, `pi-herdr-clone` included `pi-herdr-done`. That package is also retired. Install the replacement first. Then remove all old installs before you restart Pi:
19
19
 
20
20
  ```sh
21
21
  pi install npm:@henryqw/pi-herdr-tools
@@ -26,57 +26,57 @@ pi remove npm:@henryqw/pi-herdr-rename
26
26
  pi remove npm:@henryqw/pi-herdr-done
27
27
  ```
28
28
 
29
- Only remove sources you have installed. For project-local installs, use `--local` on each command. Remove manually configured old entry-point paths as well; loading an old package alongside this one duplicates commands. The harness installer removes retired personal npm sources only after the selected replacement installs successfully.
29
+ Remove only the sources that you installed. For project-local installs, use `--local` on each command. Also remove old entry-point paths that you configured manually. If Pi loads an old package together with this package, Pi registers the commands two times. The harness installer removes retired personal npm sources only after the selected replacement installs successfully.
30
30
 
31
- No data migration is needed. Commands and safety behavior are unchanged. Existing `pi-herdr-btw/btw` and `pi-herdr-rename/rename` task-model assignments, `pi-herdr-rename/title` session entries, BTW payload flags, and pending deliveries keep their identifiers. BTW configuration remains at `~/.pi/agent/config/pi-herdr-btw/config.json` despite the new package name. Individual features are internal modules, not separately selectable extension resources.
31
+ You do not need a data migration. Commands and safety behavior did not change. Existing `pi-herdr-btw/btw` and `pi-herdr-rename/rename` task-model assignments, `pi-herdr-rename/title` session entries, BTW payload flags, and pending deliveries keep their identifiers. BTW configuration stays at `~/.pi/agent/config/pi-herdr-btw/config.json`, although the package name is new. Each feature is an internal module, not a separately selectable extension resource.
32
32
 
33
33
  ## Works with
34
34
 
35
35
  | Package | Relationship | Purpose |
36
36
  | --- | --- | --- |
37
- | [`@henryqw/pi-memory`](https://pi.henry.wang/extensions/pi-memory) | Improves | Recognizes side-thread children and suppresses parent-only memory injection and dream advice. |
37
+ | [`@henryqw/pi-memory`](https://pi.henry.wang/extensions/pi-memory) | Improves | Recognizes side threads and does not inject Main-only memory or dream advice into them. |
38
38
  | [`@henryqw/pi-task-models`](https://pi.henry.wang/extensions/pi-task-models) | Required | Shared profiles for title generation and side-thread routes. |
39
39
 
40
- The shared [`@henryqw/pi-herdr`](https://pi.henry.wang/packages/pi-herdr) CLI library remains separate. Side questions are inspired by [Claude Code](https://github.com/anthropics/claude-code); this package also merges their transcripts back into Main.
40
+ The shared [`@henryqw/pi-herdr`](https://pi.henry.wang/packages/pi-herdr) CLI library stays separate. Side questions are inspired by [Claude Code](https://github.com/anthropics/claude-code). This package also merges their side-thread transcripts back into Main.
41
41
 
42
42
  ## Use
43
43
 
44
- Run `/clone-tab` to open a new Herdr tab with a clone of the active conversation path; the original session remains open.
44
+ Run `/clone-tab` to open a clone tab with an active-path clone. The original session stays open.
45
45
 
46
46
  | Surface | Type | Purpose |
47
47
  | --- | --- | --- |
48
48
  | `/clone-tab` | command | Clone the current conversation into a new tab of the current Herdr workspace. |
49
49
  | `/clone-worktree` | command | Clone the current conversation into a new Herdr Git worktree workspace. |
50
- | `/done [-f]` | command | Remove the current worktree and close its Herdr workspace tabs; `-f` (alias `--force`) skips confirmation and permits forced removal without checking for tabs in other workspaces. |
51
- | `/btw [<question...>]` | command | For humans in Main: open an empty side pane or start with an editable question draft. |
50
+ | `/done [-f]` | command | Remove the current worktree and close its Herdr workspace tabs. `-f` (alias `--force`) skips confirmation. It permits forced removal and does not look for tabs in other workspaces. |
51
+ | `/btw [<question...>]` | command | For humans in Main: open an empty side pane, or start with an editable question draft. |
52
52
  | `/btw ask <question...>` | command | For humans in Main: ask a question whose first word is `ask`, `config`, `merge`, or `help`. |
53
- | `/btw config [option value]` | command | For humans: show values, set `auto-submit` to `on` or `off`, set `tools` to `inherit`, `all`, `read-only`, or `none`, set `split` to `right` or `down`, or reset defaults. |
54
- | `/btw merge [<prompt...>]` | command | For humans: in a side pane, queue its transcript and next prompt for Main (`/btw merge` opens the prompt editor); in Main, scan for pending side-thread deliveries. |
53
+ | `/btw config [option value]` | command | For humans: show values, set `auto-submit` to `on` or `off`, set `tools` to `inherit`, `all`, `read-only`, or `none`, set `split` to `right` or `down`, or reset the defaults. |
54
+ | `/btw merge [<prompt...>]` | command | For humans: in a side pane, put its side-thread transcript and next prompt in the queue for Main (`/btw merge` opens the prompt editor). In Main, look for pending side-thread deliveries. |
55
55
  | `/btw help` | command | For humans: show the command grammar. |
56
- | BTW capability widget | ui | For people in a side pane: shows `tool-free`, `read-only`, or `tool-enabled`. If parent context cannot load, shows the error and recovery steps instead. |
56
+ | BTW capability widget | ui | For people in a side pane: shows `tool-free`, `read-only`, or `tool-enabled`. If the Main context cannot load, it shows the error and recovery steps. |
57
57
  | `/rename` | command | For people: generate a new title from up to five recent user messages. |
58
- | Rename progress and result widgets | ui | For people who run `/rename`: shows `renaming...`, then `renamed to <title>` for two seconds on success. |
59
- | First real user prompt | ui | For people in a new, untitled conversation: starts title generation once the expanded prompt is ready. |
60
- | `pi-herdr-rename/rename` | model task | Title-generation route; users configure its profile through `/task-models`. |
58
+ | Rename progress and result widgets | ui | For people who run `/rename`: shows `renaming...`. On success, it then shows `renamed to <title>` for two seconds. |
59
+ | First real user prompt | ui | For people in a new, untitled conversation: starts title generation when the expanded prompt is ready. |
60
+ | `pi-herdr-rename/rename` | model task | Title-generation route. Users configure its profile with `/task-models`. |
61
61
  | Dialog blocked status | ui | For people watching Herdr: while a TUI dialog is open, the pane shows blocked with the label `Input required`. |
62
62
 
63
63
  ### Clone conversations and finish worktrees
64
64
 
65
- The active path runs from the session root to its current leaf when the command is invoked. Sibling branches and still-streaming assistant output are excluded. Both clone commands require a Pi session in a Herdr pane (`HERDR_ENV=1` and `HERDR_PANE_ID`) and validate the pane, session path, and current leaf before creating a target. If Pi has not written the session file yet, the commands clone the live session state. Neither command has configuration, and the original Pi session is not switched.
65
+ The active path goes from the session root to its current leaf at the time that you start the command. It does not include sibling branches or assistant output that Pi still streams. Both clone commands require a Pi session in a Herdr pane (`HERDR_ENV=1` and `HERDR_PANE_ID`). Before they create a target, they validate the pane, session path, and current leaf. If Pi has not written the session file yet, the commands clone the live session state. The commands have no configuration, and Pi stays in the original session.
66
66
 
67
- Commit or discard changes in a Herdr-managed linked worktree, then run `/done` and confirm. Normal removal refuses dirty worktrees and worktrees in use by another Herdr workspace. **`/done -f` can irreversibly delete uncommitted work and leave other workspaces' tabs pointing at the removed checkout.**
67
+ Commit or discard changes in a Herdr-managed linked worktree. Then run `/done` and confirm. Normal removal does not remove dirty worktrees or worktrees that another Herdr workspace uses. **`/done -f` can irreversibly delete uncommitted work and leave other workspaces' tabs pointing at the removed checkout.**
68
68
 
69
69
  ### Ask and merge side questions
70
70
 
71
- From a Herdr-managed Pi pane with a conversation, run `/btw Why is this test failing?` to open a side pane with the question as an editable draft. Submit it there; after the answer, run `/btw merge Apply the smallest safe fix`. Main receives the side transcript, regains focus, and continues with the merge prompt.
71
+ In a Herdr-managed Pi pane with a conversation, run `/btw Why is this test failing?`. A side pane opens with the question as an editable draft. Submit the question there. After the answer, run `/btw merge Apply the smallest safe fix`. Main receives the side-thread transcript, gets the focus again, and continues with the merge prompt.
72
72
 
73
73
  ### Name conversations
74
74
 
75
- In a new, untitled conversation, send the first real prompt. Pi generates a title in the background without delaying the reply.
75
+ In a new, untitled conversation, send the first real prompt. Pi generates a display title in the background and does not delay the reply.
76
76
 
77
77
  ### Show open dialogs in Herdr
78
78
 
79
- When any extension opens a select, confirm, input, editor, or custom dialog in Pi's interactive TUI, Herdr marks the pane as blocked with the fixed label `Input required`. The status clears when the dialog is answered, cancelled, or fails. This covers dialogs from other packages too, for example `pi-memory` review conflicts, `/task-models`, `/pr`, and `/cron`. This bridge never sends dialog titles or content to Herdr.
79
+ When an extension opens a select, confirm, input, editor, or custom dialog in the Pi interactive TUI, Herdr marks the pane as blocked. It uses the fixed label `Input required`. The status clears when you answer or cancel the dialog, or when the dialog fails. This also applies to dialogs from other packages, for example `pi-memory` review conflicts, `/task-models`, `/pr`, and `/cron`. This bridge never sends dialog titles or content to Herdr.
80
80
 
81
81
  | Mode | Dialog status |
82
82
  | --- | --- |
@@ -84,7 +84,7 @@ When any extension opens a select, confirm, input, editor, or custom dialog in P
84
84
  | RPC, even when the client provides UI | Not reported. |
85
85
  | JSON and print | Not reported. |
86
86
 
87
- Nested dialogs count as one block. `ask_question` from `@henryqw/pi-ask-question` publishes its own status; the two overlap without clearing each other early.
87
+ Nested dialogs count as one block. `ask_question` from `@henryqw/pi-ask-question` publishes its own status. The two statuses can overlap, and neither clears the other too early.
88
88
 
89
89
  ## Flow
90
90
 
@@ -94,70 +94,70 @@ Nested dialogs count as one block. `ask_question` from `@henryqw/pi-ask-question
94
94
 
95
95
  #### Clone a tab
96
96
 
97
- 1. Save the active path as a separate clone session with the current working directory.
97
+ 1. Save the active path as a separate active-path clone with the current working directory.
98
98
  2. Create an unfocused Herdr tab in the current workspace with that working directory.
99
99
  3. Start Pi in the tab's root pane with `--session <absolute-clone-file>`.
100
100
  4. Focus the new tab after Pi starts successfully.
101
101
 
102
102
  #### Clone a worktree
103
103
 
104
- 1. Create a Git worktree-backed workspace with `herdr worktree create --workspace <current-workspace> --no-focus`. Herdr creates the branch from `HEAD` unless the name exists, checks out the worktree under its configured `worktrees.directory`, and opens it as a grouped workspace. If the current workspace is already a linked worktree, the command targets its repository's parent workspace because Herdr creates worktrees from the parent.
105
- 2. Wait briefly for a worktree-layout plugin, such as `herdr-plus`, to start its agent in the new workspace's root pane.
106
- 3. If the root pane is occupied, create an additional unfocused tab in the new workspace with the checkout as its working directory. The plugin's agent and the clone then coexist. Otherwise, use the root pane.
107
- 4. Copy the active path into a clone session stamped with the fresh checkout path as its working directory.
104
+ 1. Create a Git worktree-backed workspace with `herdr worktree create --workspace <current-workspace> --no-focus`. Herdr creates the branch from `HEAD` if the name does not exist. Herdr checks out the worktree under its configured `worktrees.directory` and opens it as a grouped workspace. If the current workspace is already a linked worktree, the command targets the parent workspace of its repository. Herdr creates worktrees from the parent.
105
+ 2. Wait a short time for a worktree-layout plugin, such as `herdr-plus`, to start its agent in the root pane of the new workspace.
106
+ 3. If the root pane is occupied, create an additional unfocused tab in the new workspace with the checkout as its working directory. The plugin agent and the worktree clone then coexist. If the root pane is not occupied, use the root pane.
107
+ 4. Copy the active path into an active-path clone. Its working directory is the new checkout path.
108
108
  5. Start Pi in the chosen pane with `--session <absolute-clone-file>`.
109
- 6. Focus the clone's tab after Pi starts successfully.
109
+ 6. Focus the tab of the clone after Pi starts successfully.
110
110
 
111
111
  #### Finish a worktree
112
112
 
113
113
  ![Sequence of /done safety gates, cleanup order, and conditional parent update.](./docs/done-flow.svg)
114
114
 
115
- Normal `/done` asks for confirmation before waiting for Pi to become idle; declining leaves the checkout untouched. `/done -f` skips confirmation. Both wait for Pi to become idle before cleanup.
115
+ Normal `/done` asks for confirmation before it waits for Pi to become idle. If you decline, the checkout does not change. `/done -f` skips confirmation. Both wait for Pi to become idle before cleanup.
116
116
 
117
- Normal cleanup checks whether a tab in another Herdr workspace is using the checkout, removes it with `git worktree remove <checkout>`, closes every other tab in the current Herdr workspace, and runs `git pull --ff-only` from the primary checkout when this was a linked worktree and the primary is non-bare. It closes the current tab last. Tabs using the primary checkout do not block removal or the pull. Concurrent completions serialize on locks around the worktree and primary checkout; clone creation shares the worktree lock.
117
+ Normal cleanup does these steps. It checks whether a tab in another Herdr workspace uses the checkout. It removes the checkout with `git worktree remove <checkout>`. It closes all other tabs in the current Herdr workspace. If this was a linked worktree and the primary checkout is non-bare, it runs `git pull --ff-only` from the primary checkout. It closes the current tab last. Tabs that use the primary checkout do not block removal or the pull. Concurrent worktree completions run one at a time, with locks on the worktree and the primary checkout. Clone creation uses the same worktree lock.
118
118
 
119
119
  ### Side questions
120
120
 
121
121
  #### Launch
122
122
 
123
- - `ask`, `config`, `merge`, and `help` route only when they are exact first words. Other input is a question.
123
+ - `ask`, `config`, `merge`, and `help` route only when they are the exact first word. All other input is a question.
124
124
  - A provided question is an editable draft by default.
125
- - `/btw` gives the side pane a static snapshot of Main's compaction-aware context and shares Main's working directory.
125
+ - `/btw` gives the side pane a static snapshot of the compaction-aware context of Main. The side pane uses the working directory of Main.
126
126
  - The consumer-owned `pi-herdr-btw/btw` task defaults to `fast`.
127
- - Before pane launch, it selects the first authenticated viable effective profile route.
127
+ - Before the pane starts, `/btw` selects the first authenticated viable effective profile route.
128
128
 
129
- `~/.pi/agent/config/pi-task-models/config.json` is shared and owned by `@henryqw/pi-task-models`. A task entry is an explicit user override, and routes resolve before pane launch. BTW warns once per session if this file is missing.
129
+ `~/.pi/agent/config/pi-task-models/config.json` is shared, and `@henryqw/pi-task-models` owns it. A task entry is an explicit user override. Routes resolve before the pane starts. If this file is missing, BTW shows a warning one time per session.
130
130
 
131
131
  #### Merge delivery
132
132
 
133
133
  ![Sequence showing a fixed context snapshot with live shared files; the transcript and prompt queue until Main is idle and authenticated, then Main appends the transcript and submits the prompt.](./docs/btw-merge-sequence.svg)
134
134
 
135
- - In the side pane, a merge stores the user/assistant transcript and next prompt as pending delivery.
135
+ - In the side pane, a merge request stores the side-thread transcript and next prompt as a pending delivery.
136
136
  - Herdr then refocuses Main and closes the side pane.
137
- - Pending delivery survives side-pane shutdown. It waits for Main to settle and for current model authentication.
138
- - Main appends the transcript without starting a turn, then submits the prompt.
139
- - Pending delivery remains available until consumed or 24-hour stale cleanup.
137
+ - A pending delivery stays after side-pane shutdown. It waits for Main to settle and for current model authentication.
138
+ - Main appends the side-thread transcript and does not start a turn. Then Main submits the prompt.
139
+ - A pending delivery stays available until Main consumes it or the 24-hour stale cleanup removes it.
140
140
 
141
141
  ### Conversation titles
142
142
 
143
143
  #### Trigger
144
144
 
145
- In a new, untitled session, the first real user prompt generates a title in the background after Pi expands skill and prompt-template shorthand. It does not delay the main reply. Extension-injected prompts, empty prompts, and image-only input are ignored.
145
+ In a new, untitled session, the first real user prompt generates a display title in the background. This occurs after Pi expands skill and prompt-template shorthand. It does not delay the main reply. The extension ignores extension-injected prompts, empty prompts, and image-only input.
146
146
 
147
147
  #### Title and branch rules
148
148
 
149
- A semantic branch is a Git-safe branch name made from a task kind and the display-title words. The model replies with one JSON object holding `kind` and `subject`; that classification stays internal. For example, `{"kind":"refactor","subject":"update task logic"}` displays as `Update task logic` and maps to `refactor/update-task-logic`. A reply that does not fit the shape is sent back once with the rejection before the route gives up on it, so a valid title still comes from the first route.
149
+ A semantic branch is a Git-safe branch name from a task kind and the display-title words. The model replies with one JSON object that has `kind` and `subject`. That classification stays internal. For example, `{"kind":"refactor","subject":"update task logic"}` shows as `Update task logic` and becomes `refactor/update-task-logic`. If a reply does not have the correct shape, the extension sends it back one time with the rejection before it stops the route. Thus, a valid display title can still come from the first route.
150
150
 
151
- - In a linked worktree, a detached checkout or Herdr `worktree/...` branch is renamed. An existing non-generated branch stays.
152
- - A linked worktree's generated workspace label, such as `worktree-brave-meadow-4aa8`, becomes the display title automatically. A manual `/rename` can also replace a custom workspace name.
153
- - The pane is renamed in Herdr. The enclosing tab updates only when this pane is the tab's only pane.
151
+ - In a linked worktree, the extension renames a detached checkout or a Herdr `worktree/...` branch. An existing non-generated branch does not change.
152
+ - The generated worktree label of a linked worktree, such as `worktree-brave-meadow-4aa8`, changes to the display title automatically. A manual `/rename` can also replace a custom workspace name.
153
+ - The extension renames the pane in Herdr. The tab label changes only when the tab is a sole-pane tab.
154
154
  - Outside Herdr, only the Pi session name changes. Herdr is required for pane, tab, workspace, and linked-worktree branch updates.
155
155
 
156
156
  #### Model route
157
157
 
158
- The task `pi-herdr-rename/rename` uses `fast` only when it has no explicit assignment. To use another configured profile, assign the task to that profile in `/task-models`; the shared [`pi-task-models` config](https://pi.henry.wang/extensions/pi-task-models#config) stores the assignment under `tasks` at `~/.pi/agent/config/pi-task-models/config.json`.
158
+ The task `pi-herdr-rename/rename` uses `fast` only when it has no explicit assignment. To use another configured profile, assign the task to that profile in `/task-models`. The shared [`pi-task-models` config](https://pi.henry.wang/extensions/pi-task-models#config) stores the assignment under `tasks` at `~/.pi/agent/config/pi-task-models/config.json`.
159
159
 
160
- The extension tries the assigned profile's primary route, then its fallback, while honoring the configured thinking level. Within one route a reply that fails the title shape is retried once with the rejected text; only a second failure moves on to the fallback. It never substitutes the current session model.
160
+ The extension tries the primary route of the assigned profile, then its fallback. It uses the configured thinking level. In one route, if a reply does not have the title shape, the extension tries again one time with the rejected text. Only a second failure goes to the fallback. The extension never uses the current session model as a replacement.
161
161
 
162
162
  ## Config
163
163
 
@@ -165,26 +165,26 @@ Package-owned: `~/.pi/agent/config/pi-herdr-btw/config.json`
165
165
 
166
166
  | Name | Description | Values | Default |
167
167
  | --- | --- | --- | --- |
168
- | `autoSubmit` | Submits the draft question instead of leaving it editable in the side pane. | Boolean. | `false` |
169
- | `tools` | Selects the tools available in the side pane. | `inherit` (parent's active tools), `all`, `read-only` (built-in read-only tools), or `none`. | `inherit` |
168
+ | `autoSubmit` | Submits the draft question. When `false`, the question stays editable in the side pane. | Boolean. | `false` |
169
+ | `tools` | Selects the tools available in the side pane. | `inherit` (active tools of Main), `all`, `read-only` (built-in read-only tools), or `none`. | `inherit` |
170
170
  | `split` | Sets the side-pane placement. | `right` or `down`. | `right` |
171
171
 
172
172
  - All fields are optional.
173
- - Unknown keys and non-object files are rejected.
173
+ - The extension rejects unknown keys and non-object files.
174
174
  - `/btw config show` prints effective values.
175
175
  - `/btw config reset` saves the defaults.
176
- - The config file is optional. Missing config uses defaults.
177
- - Malformed config fails visibly and remains unchanged.
176
+ - The config file is optional. If it is missing, the extension uses the defaults.
177
+ - A malformed config file causes a visible failure, and the file does not change.
178
178
 
179
179
  ## State and storage
180
180
 
181
181
  ### Clones
182
182
 
183
- Each clone is a separate Pi session file in Pi's session directory, containing only the active path. A tab clone uses the current working directory; a worktree clone uses the fresh checkout. Pi's session manager chooses the session directory. For a persisted source session, Pi names the clone file; before the source session has been written to disk, the extension creates and names the clone file in that directory.
183
+ Each active-path clone is a separate Pi session file in the Pi session directory. It contains only the active path. A clone tab uses the current working directory. A worktree clone uses the new checkout. The Pi session manager selects the session directory. For a persisted source session, Pi names the clone file. If Pi has not written the source session to disk, the extension creates and names the clone file in that directory.
184
184
 
185
185
  ### Conversation titles
186
186
 
187
- The Pi session stores each generated display title and semantic branch in a `pi-herdr-rename/title` session entry. When resuming a session created by this version, the extension reapplies the saved title and branch without another model request.
187
+ The Pi session stores each generated display title and semantic branch in a `pi-herdr-rename/title` session entry. When you resume a session that this version created, the extension applies the saved display title and semantic branch again. It does not send another model request.
188
188
 
189
189
  ## Limits and recovery
190
190
 
@@ -192,35 +192,35 @@ The Pi session stores each generated display title and semantic branch in a `pi-
192
192
 
193
193
  | Outcome | What remains and how to recover |
194
194
  | --- | --- |
195
- | Definitive target creation failure | A failed `/clone-tab` tab creation removes its clone session when cleanup succeeds; if cleanup also fails, the error names the leftover file. A `/clone-worktree` worktree-creation or pre-launch extra-tab-creation failure happens before a clone session is created. |
196
- | Killed or incomplete worktree creation | Herdr may have retained partial workspace state. The error reports any returned IDs and suggests inspecting `herdr workspace list`. No clone session has been created yet. |
197
- | Incomplete tab-creation response | The error reports known IDs. For `/clone-tab`, the clone session is retained and its path is reported. For `/clone-worktree`, the worktree is retained and no clone session exists yet. Inspect the target before manual cleanup. |
198
- | Agent start attempted | The launch outcome may be unknown. Retain the target tab, panes, and session file; the error reports known IDs for recovery. |
199
- | Focus failed after Pi started | Show a warning, but do not report the already-started clone as failed. Focus the clone tab in Herdr if needed. |
195
+ | Definitive target creation failure | When `/clone-tab` tab creation fails, the extension removes its clone session if cleanup succeeds. If cleanup also fails, the error names the remaining file. A `/clone-worktree` failure in worktree creation or pre-launch extra-tab creation occurs before the extension creates a clone session. |
196
+ | Killed or incomplete worktree creation | Herdr can keep partial workspace state. The error reports all returned IDs and tells you to examine `herdr workspace list`. The extension has not created a clone session yet. |
197
+ | Incomplete tab-creation response | The error reports known IDs. For `/clone-tab`, the extension keeps the clone session and reports its path. For `/clone-worktree`, the extension keeps the worktree, and no clone session exists yet. Examine the target before manual cleanup. |
198
+ | Agent start attempted | The launch outcome can be unknown. Keep the target tab, panes, and session file. The error reports known IDs for recovery. |
199
+ | Focus failed after Pi started | Show a warning, but do not report the clone that already started as failed. If necessary, focus the clone tab in Herdr. |
200
200
 
201
201
  ### Worktree completion
202
202
 
203
- - `/done` refuses to remove a dirty worktree. Commit or discard changes first. `/done -f` passes `--force` to Git and can irreversibly delete uncommitted work.
204
- - A tab in another Herdr workspace that uses this checkout blocks normal removal. `/done` lists the tab label, or its ID when no label is available. Close the tab and retry, or use `/done -f` only if you accept removing the checkout while that tab still refers to it. Tabs in other workspaces are not closed.
203
+ - `/done` does not remove a dirty worktree. Commit or discard changes first. `/done -f` passes `--force` to Git and can irreversibly delete uncommitted work.
204
+ - A tab in another Herdr workspace that uses this checkout blocks normal removal. `/done` lists the tab label, or its ID when the tab has no label. Close the tab and try again. Use `/done -f` only if you accept that the checkout is removed while that tab still refers to it. `/done` does not close tabs in other workspaces.
205
205
  - The command requires Pi inside Herdr with `HERDR_ENV=1`, `HERDR_WORKSPACE_ID`, and `HERDR_TAB_ID` set.
206
- - The primary checkout update runs only after worktree removal. If `git pull --ff-only` fails, for example because the primary has diverged or has local changes, the worktree is already gone and the current tab still closes. Resolve the primary checkout issue, then retry with `git -C <primary-checkout> pull --ff-only`.
206
+ - The primary checkout update runs only after worktree removal. `git pull --ff-only` can fail, for example because the primary checkout has diverged or has local changes. In that case, the worktree is already removed, and the current tab still closes. Correct the primary checkout problem. Then try again with `git -C <primary-checkout> pull --ff-only`.
207
207
 
208
208
  ### Side questions
209
209
 
210
- The side pane shares Main's working directory, so enabled tools can change parent-visible files. Choose `read-only` or `none` when the side question should not make changes.
210
+ The side pane uses the working directory of Main. Thus, enabled tools can change files that Main can see. Select `read-only` or `none` when the side question must not make changes.
211
211
 
212
- Large parent contexts can exceed child context limits; shorten Main's context and retry.
212
+ A large Main context can be more than the context limit of the side thread. Make the Main context shorter and try again.
213
213
 
214
214
  ### Conversation titles
215
215
 
216
- Display titles are natural task phrases, preferably two or three words, and always at most four words and 20 characters. Models that answer in another language still produce English titles.
216
+ Display titles are natural task phrases. They usually have two or three words, and they always have a maximum of four words and 20 characters. Models that answer in another language still make English display titles.
217
217
 
218
- A missing shared task-model config warns once at session start. Run `/task-models` to configure rename routing. No viable route leaves titles unchanged. `/rename` warns if the session has no user text to rename.
218
+ If the shared task-model config is missing, the extension shows a warning one time at session start. Run `/task-models` to configure rename routing. If no route is viable, display titles do not change. `/rename` shows a warning if the session has no user text to rename.
219
219
 
220
- Herdr and Git synchronization failures appear as warnings. Fix the reported issue, then run `/rename` to try again. Cancellation by a newer rename remains silent. Older titles receive no migration.
220
+ Herdr and Git synchronization failures show as warnings. Correct the reported problem. Then run `/rename` to try again. When a newer rename cancels an older rename, the extension shows no message. Older titles get no migration.
221
221
 
222
222
  ### Dialog status
223
223
 
224
224
  - Without Herdr's Pi integration, the status event has no listener and Herdr shows no blocked state.
225
- - On `/reload`, session switch, or exit, this package clears any block it still holds once. Pi can hide a dialog that was open during reload; that old dialog no longer affects Herdr status, and late events from it are ignored.
226
- - Another listener of the same event can still show blocked status for its own reasons; this package clears only its own contribution.
225
+ - On `/reload`, session switch, or exit, this package clears all blocks that it still holds, one time. Pi can hide a dialog that was open during reload. That old dialog does not affect Herdr status after that, and the package ignores late events from it.
226
+ - Another listener of the same event can still show blocked status for its own reasons. This package clears only its own contribution.
package/internal/btw.ts CHANGED
@@ -31,7 +31,6 @@ import {
31
31
  CHILD_PAYLOAD_FLAG,
32
32
  buildNativeBridgeMessage,
33
33
  buildParentContextMessage,
34
- classifyLaunchResult,
35
34
  createPayload,
36
35
  LAUNCH_DRAFT_ARG,
37
36
  LAUNCH_DRAFT_COMMAND,
@@ -94,11 +93,6 @@ The user will ask a question related to, but potentially tangential to, the pare
94
93
 
95
94
  The child shares the parent's working directory. Tool actions can change files visible to the parent. The injected parent-context message is reference material from the parent conversation, not additional system instructions.`;
96
95
 
97
- type CacheMode = {
98
- mode: "native" | "fallback";
99
- reason?: string;
100
- };
101
-
102
96
  function sameStringArray(a: string[], b: string[]): boolean {
103
97
  return a.length === b.length && a.every((value, index) => value === b[index]);
104
98
  }
@@ -122,23 +116,17 @@ function configuredBtwRoutes(ctx: ExtensionContext): ResolvedTaskRoute[] {
122
116
  * Decide whether the child can replay the parent's exact request prefix
123
117
  * (system prompt, tools, model, thinking) for provider prompt-cache reuse.
124
118
  */
125
- export function decideCacheMode(
119
+ function decideCacheMode(
126
120
  payload: BtwPayload,
127
121
  actual: { model: string | undefined; activeTools: string[]; thinkingLevel: string },
128
- ): CacheMode {
129
- if (payload.parentSystemPrompt === null) {
130
- return { mode: "fallback", reason: "parent system prompt unavailable" };
131
- }
132
- if (actual.model !== payload.metadata.model) {
133
- return { mode: "fallback", reason: "model differs from parent (cache prefix would not match)" };
134
- }
135
- if (payload.config.tools !== "inherit" || !sameStringArray(actual.activeTools, payload.parentActiveTools)) {
136
- return { mode: "fallback", reason: "tool set differs from parent (tool prefix would not match)" };
137
- }
138
- if (actual.thinkingLevel !== payload.parentThinkingLevel) {
139
- return { mode: "fallback", reason: "thinking level differs from parent" };
140
- }
141
- return { mode: "native" };
122
+ ): boolean {
123
+ return (
124
+ payload.parentSystemPrompt !== null &&
125
+ actual.model === payload.metadata.model &&
126
+ payload.config.tools === "inherit" &&
127
+ sameStringArray(actual.activeTools, payload.parentActiveTools) &&
128
+ actual.thinkingLevel === payload.parentThinkingLevel
129
+ );
142
130
  }
143
131
 
144
132
  async function configureChild(
@@ -163,34 +151,16 @@ async function configureChild(
163
151
  )
164
152
  : undefined;
165
153
 
166
- const cache: CacheMode = { mode: "fallback", reason: "not yet negotiated" };
167
- let widgetUi:
168
- | { setWidget(name: string, lines: string[]): void; theme: { fg(color: string, text: string): string } }
169
- | undefined;
170
-
171
- function renderWidget(): void {
172
- if (!widgetUi || !payload) return;
173
- const capability =
174
- payload.config.tools === "none"
175
- ? "tool-free"
176
- : payload.config.tools === "read-only"
177
- ? "read-only"
178
- : "tool-enabled";
179
- widgetUi.setWidget("herdr-btw-context", [
180
- widgetUi.theme.fg("accent", `BTW — ${capability} pane`),
181
- ]);
182
- }
154
+ let nativeCache = false;
183
155
 
184
156
  pi.on("before_agent_start", (event, ctx) => {
185
157
  if (!payload) return;
186
- const decision = decideCacheMode(payload, {
158
+ nativeCache = decideCacheMode(payload, {
187
159
  model: ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : undefined,
188
160
  activeTools: pi.getActiveTools(),
189
161
  thinkingLevel: pi.getThinkingLevel(),
190
162
  });
191
- cache.mode = decision.mode;
192
- cache.reason = decision.reason;
193
- if (cache.mode === "native") {
163
+ if (nativeCache) {
194
164
  // Replay the parent's exact system prompt; side-pane policy moves to
195
165
  // a suffix message so the cached prefix stays byte-identical.
196
166
  return { systemPrompt: payload.parentSystemPrompt as string };
@@ -200,7 +170,7 @@ async function configureChild(
200
170
 
201
171
  pi.on("context_with_system", (event) => {
202
172
  if (!payload) return;
203
- if (cache.mode === "native") {
173
+ if (nativeCache) {
204
174
  return {
205
175
  messages: [
206
176
  ...(payload.messages[0]?.role === "system" ? [] : event.messages.slice(0, 1)),
@@ -369,8 +339,15 @@ async function configureChild(
369
339
  return;
370
340
  }
371
341
 
372
- widgetUi = ctx.ui;
373
- renderWidget();
342
+ if (payload) {
343
+ const capability =
344
+ payload.config.tools === "none"
345
+ ? "tool-free"
346
+ : payload.config.tools === "read-only"
347
+ ? "read-only"
348
+ : "tool-enabled";
349
+ ctx.ui.setWidget("herdr-btw-context", [ctx.ui.theme.fg("accent", `BTW — ${capability} pane`)]);
350
+ }
374
351
 
375
352
  // Auto-submit drafts are sent via the launch-draft sentinel instead of
376
353
  // here: session_start fires before pi's initial render, and a message
@@ -548,19 +525,13 @@ export async function registerBtwExtension(
548
525
  ctx.ui.notify(`BTW config — ${formatConfig(config)}`, "info");
549
526
  return;
550
527
  }
551
- const trimmedArgs = route.args.trim();
552
- const result = !trimmedArgs || trimmedArgs === "show"
553
- ? applyConfigCommand(configStore.loadSync().value, route.args)
554
- : {
555
- action: "save" as const,
556
- config: await configStore.update((latest) => applyConfigCommand(latest, route.args).config),
557
- };
558
- ctx.ui.notify(
559
- result.action === "show"
560
- ? `BTW config — ${formatConfig(result.config)}\n${CONFIG_COMMAND_USAGE}`
561
- : `BTW config — ${formatConfig(result.config)}`,
562
- "info",
563
- );
528
+ if (!route.args || route.args === "show") {
529
+ const config = configStore.loadSync().value;
530
+ ctx.ui.notify(`BTW config — ${formatConfig(config)}\n${CONFIG_COMMAND_USAGE}`, "info");
531
+ return;
532
+ }
533
+ const config = await configStore.update((latest) => applyConfigCommand(latest, route.args));
534
+ ctx.ui.notify(`BTW config — ${formatConfig(config)}`, "info");
564
535
  } catch (error) {
565
536
  const message = error instanceof Error ? error.message : String(error);
566
537
  ctx.ui.notify(message, "error");
@@ -706,8 +677,7 @@ export async function registerBtwExtension(
706
677
  }
707
678
  return;
708
679
  }
709
- const splitOutcome = classifyLaunchResult(splitResult);
710
- if (splitOutcome !== "success") {
680
+ if (splitResult.killed || splitResult.code !== 0) {
711
681
  await store.remove(payloadPath);
712
682
  ctx.ui.notify(
713
683
  `/btw failed: ${safeErrorText(splitResult.stdout, splitResult.stderr)}`,
@@ -749,29 +719,27 @@ export async function registerBtwExtension(
749
719
  }
750
720
  return;
751
721
  }
752
- const outcome = classifyLaunchResult(result);
753
- if (outcome === "success" && !isAgentStartReady(result.stdout, { name: launchOptions.paneName, paneId })) {
754
- await reportKnownPaneFailure(
755
- "/btw failed: `herdr agent start` returned an invalid or non-interactive result",
756
- paneId,
757
- payloadPath,
758
- );
759
- return;
760
- }
761
- if (outcome === "success") {
722
+ if (!result.killed) {
723
+ if (result.code !== 0) {
724
+ await reportKnownPaneFailure(
725
+ `/btw failed: ${safeErrorText(result.stdout, result.stderr)}`,
726
+ paneId,
727
+ payloadPath,
728
+ );
729
+ return;
730
+ }
731
+ if (!isAgentStartReady(result.stdout, { name: launchOptions.paneName, paneId })) {
732
+ await reportKnownPaneFailure(
733
+ "/btw failed: `herdr agent start` returned an invalid or non-interactive result",
734
+ paneId,
735
+ payloadPath,
736
+ );
737
+ return;
738
+ }
762
739
  ensurePolling();
763
740
  return;
764
741
  }
765
742
 
766
- if (outcome === "failed") {
767
- await reportKnownPaneFailure(
768
- `/btw failed: ${safeErrorText(result.stdout, result.stderr)}`,
769
- paneId,
770
- payloadPath,
771
- );
772
- return;
773
- }
774
-
775
743
  const reconciled = await herdr
776
744
  .exec(
777
745
  [
@@ -58,16 +58,8 @@ export function formatConfig(config: BtwConfig): string {
58
58
  export const CONFIG_COMMAND_USAGE =
59
59
  "/btw config [auto-submit on|off | tools inherit|all|read-only|none | split right|down | reset]";
60
60
 
61
- export type ConfigCommandResult = {
62
- action: "show" | "save";
63
- config: BtwConfig;
64
- };
65
-
66
- export function applyConfigCommand(current: BtwConfig, input: string): ConfigCommandResult {
67
- const trimmed = input.trim();
68
- if (!trimmed || trimmed === "show") return { action: "show", config: current };
69
-
70
- const [key, value, ...extra] = trimmed.split(/\s+/);
61
+ export function applyConfigCommand(current: BtwConfig, input: string): BtwConfig {
62
+ const [key, value, ...extra] = input.split(/\s+/);
71
63
  if (!key || !value || extra.length > 0) throw new Error(CONFIG_COMMAND_USAGE);
72
64
  const config = { ...current };
73
65
 
@@ -90,7 +82,7 @@ export function applyConfigCommand(current: BtwConfig, input: string): ConfigCom
90
82
  throw new Error(CONFIG_COMMAND_USAGE);
91
83
  }
92
84
 
93
- return { action: "save", config };
85
+ return config;
94
86
  }
95
87
 
96
88
  export function createBtwConfigStore(agentDir?: string) {
@@ -14,7 +14,7 @@ import {
14
14
  import { tmpdir } from "node:os";
15
15
  import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
16
16
  import { isBtwPayload, type BtwPayload } from "./core.ts";
17
- import { isMergeRequest, MERGE_REQUEST_FILE, type MergeRequest } from "./merge.ts";
17
+ import { MERGE_REQUEST_FILE, type MergeRequest } from "./merge.ts";
18
18
 
19
19
  const PAYLOAD_FILE = "payload.json";
20
20
  const LAUNCH_PREFIX = "launch-";
@@ -104,7 +104,6 @@ export class ContextStore {
104
104
  }
105
105
 
106
106
  async writeMergeRequest(payloadPath: string, request: MergeRequest): Promise<void> {
107
- if (!isMergeRequest(request)) throw new Error("Invalid /btw merge request");
108
107
  await this.writeLaunchFile(payloadPath, MERGE_REQUEST_FILE, request);
109
108
  }
110
109
 
package/internal/core.ts CHANGED
@@ -60,8 +60,6 @@ export type CreatePayloadOptions = {
60
60
  messages: AgentMessage[];
61
61
  draftQuestion: string;
62
62
  config: BtwConfig;
63
- launchId?: string;
64
- capability?: string;
65
63
  };
66
64
 
67
65
  export type ParentContextMetadata = {
@@ -89,19 +87,12 @@ export type HerdrLaunchOptions = {
89
87
  initialMessage?: string;
90
88
  };
91
89
 
92
- export type LaunchResult = {
93
- code: number;
94
- killed?: boolean;
95
- };
96
-
97
- export type LaunchOutcome = "success" | "failed" | "ambiguous";
98
-
99
90
  export function createPayload(options: CreatePayloadOptions): BtwPayload {
100
91
  return {
101
92
  version: PAYLOAD_VERSION,
102
93
  createdAt: options.createdAt,
103
- launchId: options.launchId ?? randomUUID(),
104
- capability: options.capability ?? randomBytes(32).toString("hex"),
94
+ launchId: randomUUID(),
95
+ capability: randomBytes(32).toString("hex"),
105
96
  parentSessionId: options.parentSessionId,
106
97
  parentPaneId: options.parentPaneId,
107
98
  metadata: options.metadata,
@@ -388,11 +379,6 @@ export function buildAgentStartArgs(options: HerdrLaunchOptions): string[] {
388
379
  ];
389
380
  }
390
381
 
391
- export function classifyLaunchResult(result: LaunchResult): LaunchOutcome {
392
- if (result.killed) return "ambiguous";
393
- return result.code === 0 ? "success" : "failed";
394
- }
395
-
396
382
  /**
397
383
  * Herdr CLI failures print the whole JSON response on stderr
398
384
  * (`{"id":...,"error":{"code":...,"message":...}}`, exit 1); extract the
package/internal/merge.ts CHANGED
@@ -140,10 +140,7 @@ function tailBytes(text: string, maxBytes: number): string {
140
140
  * tool payloads) as the merge transcript. When over budget, whole turns are
141
141
  * dropped from the head so the most recent findings survive.
142
142
  */
143
- export function buildMergeTranscript(
144
- messages: AgentMessage[],
145
- budgetBytes = MERGE_TRANSCRIPT_BUDGET_BYTES,
146
- ): string | undefined {
143
+ export function buildMergeTranscript(messages: AgentMessage[]): string | undefined {
147
144
  const turns: string[] = [];
148
145
  for (const message of messages) {
149
146
  const { role, content } = message as { role?: string; content?: unknown };
@@ -158,10 +155,10 @@ export function buildMergeTranscript(
158
155
  for (let index = turns.length - 1; index >= 0; index -= 1) {
159
156
  const turn = turns[index] as string;
160
157
  const bytes = Buffer.byteLength(turn, "utf8") + 2;
161
- if (used + bytes > budgetBytes) {
158
+ if (used + bytes > MERGE_TRANSCRIPT_BUDGET_BYTES) {
162
159
  if (kept.length === 0) {
163
160
  // A single oversized turn keeps its tail (the latest content).
164
- kept.unshift(`${TRANSCRIPT_TRUNCATION_NOTE}\n${tailBytes(turn, budgetBytes)}`);
161
+ kept.unshift(`${TRANSCRIPT_TRUNCATION_NOTE}\n${tailBytes(turn, MERGE_TRANSCRIPT_BUDGET_BYTES)}`);
165
162
  } else {
166
163
  kept.unshift(TRANSCRIPT_TRUNCATION_NOTE);
167
164
  }
@@ -271,12 +268,7 @@ export class MergeCoordinator {
271
268
 
272
269
  // pi.sendUserMessage() is fire-and-forget. Keep the request pending when
273
270
  // current model/auth state would reject its prompt.
274
- try {
275
- if (!(await this.session.canSubmitPrompt())) {
276
- result.deferred += 1;
277
- return;
278
- }
279
- } catch {
271
+ if (!(await this.session.canSubmitPrompt())) {
280
272
  result.deferred += 1;
281
273
  return;
282
274
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@henryqw/pi-herdr-tools",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
4
4
  "description": "Clone, name, and finish Pi conversations in Herdr, with mergeable side questions.",
5
5
  "keywords": ["pi-package", "pi", "herdr", "session", "clone", "side-thread", "title"],
6
6
  "type": "module",