hotmilk 0.1.10 → 0.1.12
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/AGENTS.md +1 -1
- package/README.md +78 -38
- package/hotmilk.json +4 -3
- package/package.json +5 -4
- package/src/bootstrap/btw.ts +395 -0
- package/src/bootstrap/context-stack.ts +3 -11
- package/src/bootstrap/dashboard.ts +285 -44
- package/src/bootstrap/extensions.ts +65 -34
- package/src/bootstrap/global-extension-sources.ts +154 -0
- package/src/bootstrap/resolve-bundled.ts +52 -1
- package/src/bootstrap/session.ts +15 -0
- package/src/config/bundled-extensions.ts +235 -0
- package/src/config/bundled-package-registry.ts +28 -0
- package/src/config/hotmilk.ts +2 -25
- package/src/config/runtime.ts +4 -0
- package/src/controller/input.ts +1 -2
- package/src/controller/mode.ts +2 -3
- package/src/extensions/btw.ts +17 -0
- package/src/index.ts +6 -6
- package/src/ui/footer.ts +83 -12
- package/src/ui/github-user.ts +155 -0
- package/src/ui/session-logo.ts +9 -3
- package/src/bootstrap/rtk-coexist.ts +0 -6
- package/src/config/extension-groups.ts +0 -32
- package/src/controller/context.ts +0 -5
- package/src/ui/footer-time.ts +0 -9
- package/src/ui/logo.ts +0 -7
package/AGENTS.md
CHANGED
|
@@ -20,7 +20,7 @@ Requires **Node.js 22+**. Targets **Pi 0.78** (`@earendil-works/pi-coding-agent`
|
|
|
20
20
|
- **Runtime**: [Pi](https://github.com/badlogic/pi-mono) with `@earendil-works/pi-coding-agent` — install hotmilk via `pi install npm:hotmilk`.
|
|
21
21
|
- **Single extension entry**: only `./src/index.ts` is listed under `package.json` → `pi.extensions`. All other bundled packages load via **dynamic `import()`** when their toggle is `true`.
|
|
22
22
|
- **Config**: `~/.pi/agent/hotmilk.json` — change with `/mode`, apply with `/reload`.
|
|
23
|
-
- **Do not** add packages to `pi.extensions` for toggled deps; add
|
|
23
|
+
- **Do not** add packages to `pi.extensions` for toggled deps; add one row to `BUNDLED_EXTENSION_DEFINITIONS` in `src/config/bundled-extensions.ts`.
|
|
24
24
|
|
|
25
25
|
Details: [docs/pi.md](docs/pi.md).
|
|
26
26
|
|
package/README.md
CHANGED
|
@@ -6,19 +6,19 @@ Use it when you want a practical engineering workstation without hand-picking a
|
|
|
6
6
|
|
|
7
7
|
## What you get
|
|
8
8
|
|
|
9
|
-
| Layer | Packages / assets
|
|
10
|
-
| ----------------------- |
|
|
11
|
-
| **Orchestration** | [gentle-pi](https://www.npmjs.com/package/gentle-pi) (SDD, skill registry, `sdd-init`)
|
|
12
|
-
| **Context** | [context-mode](https://www.npmjs.com/package/context-mode)
|
|
13
|
-
| **Codebase graph** | [graphify-pi](https://www.npmjs.com/package/graphify-pi)
|
|
14
|
-
| **Subagents** | [pi-subagents](https://www.npmjs.com/package/pi-subagents), [pi-ask-user](https://www.npmjs.com/package/pi-ask-user)
|
|
15
|
-
| **Goals & docs** | [pi-goal](https://www.npmjs.com/package/pi-goal), [pi-docparser](https://www.npmjs.com/package/pi-docparser)
|
|
16
|
-
| **File-based planning** | [@tomxprime/planning-with-files](https://www.npmjs.com/package/@tomxprime/planning-with-files)
|
|
17
|
-
| **Integrations** | [pi-mcp-adapter](https://www.npmjs.com/package/pi-mcp-adapter), [pi-btw](https://www.npmjs.com/package/pi-btw), [@haispeed/pi-obsidian](https://www.npmjs.com/package/@haispeed/pi-obsidian)
|
|
18
|
-
| **Dashboard** | [@blackbelt-technology/pi-agent-dashboard](https://www.npmjs.com/package/@blackbelt-technology/pi-agent-dashboard)
|
|
19
|
-
| **Web tools** | [pi-web-access](https://www.npmjs.com/package/pi-web-access)
|
|
20
|
-
| **Flows** | [@blackbelt-technology/pi-flows](https://www.npmjs.com/package/@blackbelt-technology/pi-flows)
|
|
21
|
-
| **Local assets** | `./prompts`, `./skills`, `./themes`, `mcp.json` template
|
|
9
|
+
| Layer | Packages / assets |
|
|
10
|
+
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
11
|
+
| **Orchestration** | [gentle-pi](https://www.npmjs.com/package/gentle-pi) (SDD, skill registry, `sdd-init`) |
|
|
12
|
+
| **Context** | [context-mode](https://www.npmjs.com/package/context-mode) |
|
|
13
|
+
| **Codebase graph** | [graphify-pi](https://www.npmjs.com/package/graphify-pi) |
|
|
14
|
+
| **Subagents** | [pi-subagents](https://www.npmjs.com/package/pi-subagents), [pi-ask-user](https://www.npmjs.com/package/pi-ask-user) |
|
|
15
|
+
| **Goals & docs** | [pi-goal](https://www.npmjs.com/package/pi-goal), [pi-docparser](https://www.npmjs.com/package/pi-docparser) |
|
|
16
|
+
| **File-based planning** | [@tomxprime/planning-with-files](https://www.npmjs.com/package/@tomxprime/planning-with-files) |
|
|
17
|
+
| **Integrations** | [pi-mcp-adapter](https://www.npmjs.com/package/pi-mcp-adapter), [pi-btw](https://www.npmjs.com/package/pi-btw) (side channel — see below), [@haispeed/pi-obsidian](https://www.npmjs.com/package/@haispeed/pi-obsidian) |
|
|
18
|
+
| **Dashboard** | [@blackbelt-technology/pi-agent-dashboard](https://www.npmjs.com/package/@blackbelt-technology/pi-agent-dashboard) |
|
|
19
|
+
| **Web tools** | [pi-web-access](https://www.npmjs.com/package/pi-web-access) |
|
|
20
|
+
| **Flows** | [@blackbelt-technology/pi-flows](https://www.npmjs.com/package/@blackbelt-technology/pi-flows) |
|
|
21
|
+
| **Local assets** | `./prompts`, `./skills`, `./themes`, `mcp.json` template |
|
|
22
22
|
|
|
23
23
|
Bundled extension **on/off** is controlled in `hotmilk.json` (via `/mode`), then `/reload`. Only `src/index.ts` is listed in `package.json` → `pi.extensions`; every other bundled package is loaded dynamically when its toggle is `true`.
|
|
24
24
|
|
|
@@ -91,11 +91,10 @@ Or ask Pi to use the planning-with-files skill. The extension maintains `task_pl
|
|
|
91
91
|
"context-mode": true,
|
|
92
92
|
"ask-user": true,
|
|
93
93
|
"graphify": true,
|
|
94
|
-
"subagents":
|
|
94
|
+
"subagents": true,
|
|
95
95
|
"goal": true,
|
|
96
96
|
"docparser": true,
|
|
97
97
|
"obsidian": true,
|
|
98
|
-
"cursor-provider": true,
|
|
99
98
|
"btw": true,
|
|
100
99
|
"simplify": true,
|
|
101
100
|
"rtk-optimizer": false,
|
|
@@ -105,7 +104,9 @@ Or ask Pi to use the planning-with-files skill. The extension maintains `task_pl
|
|
|
105
104
|
"red-green": false,
|
|
106
105
|
"agent-dashboard": false,
|
|
107
106
|
"web-access": false,
|
|
108
|
-
"pi-flows": false
|
|
107
|
+
"pi-flows": false,
|
|
108
|
+
"kanagawa": false,
|
|
109
|
+
"tetris": false
|
|
109
110
|
},
|
|
110
111
|
"graph": {
|
|
111
112
|
"warnOnStale": true,
|
|
@@ -121,41 +122,80 @@ Or ask Pi to use the planning-with-files skill. The extension maintains `task_pl
|
|
|
121
122
|
}
|
|
122
123
|
```
|
|
123
124
|
|
|
124
|
-
| Key / area | Behavior
|
|
125
|
-
| --------------------------------- |
|
|
126
|
-
| `extensions.*` | Set to `false` to skip registering that bundled extension
|
|
127
|
-
| `extensions.subagents` | Default `
|
|
128
|
-
| `extensions.
|
|
129
|
-
| `extensions.
|
|
130
|
-
| `extensions.
|
|
131
|
-
|
|
|
132
|
-
|
|
|
133
|
-
| `graph.
|
|
134
|
-
| `
|
|
135
|
-
| `defaults.
|
|
136
|
-
| `
|
|
137
|
-
| `
|
|
125
|
+
| Key / area | Behavior |
|
|
126
|
+
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
127
|
+
| `extensions.*` | Set to `false` to skip registering that bundled extension |
|
|
128
|
+
| `extensions.subagents` | Default `true`. Imports pi-subagents (~10s). Use with `gentle-ai` for delegation; set `false` for faster startup without Task tools |
|
|
129
|
+
| `extensions.btw` | Default `true`. Side conversation via `/btw` while main runs. **Delegate implementation to subagents**; use BTW for quick human questions. See [pi-btw coexistence](#pi-btw-with-subagents-default-on) |
|
|
130
|
+
| `extensions.context-mode` | Default `true`. Prefer `ctx_*` for large outputs (see project context-window rules) |
|
|
131
|
+
| `extensions.rtk-optimizer` | Default `false`. Bash/read/grep output compaction; enable with `context-mode` for leftover shell output. Install [`rtk` CLI](https://github.com/rtk-ai/rtk) for command rewrite (`/rtk verify`) |
|
|
132
|
+
| `extensions.goal` … `mcp-adapter` | Integration / perf extensions (formerly always loaded via `pi.extensions`; now toggled like other bundled deps) |
|
|
133
|
+
| Enabled extensions | Loaded **in parallel** on session start (faster than sequential import when many toggles are on) |
|
|
134
|
+
| `graph.warnOnStale` | Notify when `graphify-out/needs_update` exists |
|
|
135
|
+
| `graph.autoSuggestUpdate` | Append `graphify update .` to that notification |
|
|
136
|
+
| `defaults.persona` | Seeds `.pi/gentle-ai/persona.json` when missing (`gentleman` \| `neutral`) |
|
|
137
|
+
| `defaults.language` | Appends a project language hint to the system prompt each turn |
|
|
138
|
+
| `mcp.seedOnStart` | Copy `mcp.json` template into `~/.pi/agent/mcp.json` when missing (empty template; for pi-mcp-adapter) |
|
|
139
|
+
| `extensions.mcp-adapter` | Default `false`. Enable only when you want MCP servers from `~/.pi/agent/mcp.json` (do not duplicate context-mode) |
|
|
138
140
|
|
|
139
141
|
**MCP (default):** `context-mode` extension registers `ctx_*` via its built-in bridge (same module as [upstream `.pi/extensions/context-mode`](https://github.com/mksglu/context-mode/tree/main/.pi/extensions/context-mode), loaded from `build/adapters/pi/extension.js`). Hotmilk removes any `context-mode` server from `~/.pi/agent/mcp.json` when the extension is on. Enable `mcp-adapter` only for **other** MCP servers—not a second context-mode entry.
|
|
140
142
|
|
|
143
|
+
### pi-btw with subagents (default on)
|
|
144
|
+
|
|
145
|
+
Both **`subagents`** and **`btw`** default to **on**. They do not share commands or extension IDs; hotmilk loads them in parallel.
|
|
146
|
+
|
|
147
|
+
| Do this | Tool |
|
|
148
|
+
| ----------------------------------------------- | -------------------------------------------------------------------------------------------- |
|
|
149
|
+
| Exploration, implementation, review, SDD phases | **subagents** (`Task`, `/run`, `/chain`; use `worktree: true` when running parallel writers) |
|
|
150
|
+
| Ask a quick question while main is working | **`/btw`** or **`/btw:tangent`** (`Alt+/` toggles BTW ↔ main) |
|
|
151
|
+
| Bring BTW results back to the main thread | **`/btw:inject`** |
|
|
152
|
+
|
|
153
|
+
BTW runs a **separate** Pi session. hotmilk wraps upstream pi-btw (`src/extensions/btw.ts`):
|
|
154
|
+
|
|
155
|
+
- **`subagents: true` (default):** read-biased tools only (`read`, `grep`, `find`, `ls`, `bash`) — no main-cwd `edit`/`write`.
|
|
156
|
+
- **`graphify: true` + `graphify-out/graph.json`:** adds **`graphify_query`** (CLI-backed) for architecture questions.
|
|
157
|
+
- **`context-mode: true`:** adds **`ctx_search`** proxy to the main session knowledge base (read-only).
|
|
158
|
+
- Inherited prompts drop main-session harness noise (gentle-ai orchestrator, graphify rules, caveman); project AGENTS.md stays.
|
|
159
|
+
- Still **no** `ctx_execute`, Task, or MCP inside BTW — use main/subagents for heavy ctx work.
|
|
160
|
+
|
|
161
|
+
Global `npm:pi-btw` in Pi settings skips the hotmilk shim (standard dedupe). Prefer bundled hotmilk so BTW gets prompt/tool patches via `createAgentSession` hook.
|
|
162
|
+
|
|
163
|
+
During subagent chains, avoid BTW file edits on the main cwd; use read-only or `:tangent` until workers finish.
|
|
164
|
+
|
|
165
|
+
Set `"btw": false` in `/mode` if you want delegation only with no side channel.
|
|
166
|
+
|
|
141
167
|
### Optional extensions (off by default)
|
|
142
168
|
|
|
143
169
|
Enable in `/mode` or set the key to `true` in `hotmilk.json`, then `/reload`.
|
|
144
170
|
|
|
145
|
-
| Toggle | Package | Notes
|
|
146
|
-
| ----------------- | ------------------------------------------------------------------------------------------------------------------ |
|
|
147
|
-
| `caveman` | [pi-caveman](https://www.npmjs.com/package/pi-caveman) | Terse English (`/caveman`). Conflicts with `defaults.language: ja`
|
|
148
|
-
| `red-green` | [pi-red-green](https://www.npmjs.com/package/pi-red-green) | TDD via `/tdd`, `/tdd-status`. Config: `~/.pi/red-green/config.json`
|
|
149
|
-
| `agent-dashboard` | [@blackbelt-technology/pi-agent-dashboard](https://www.npmjs.com/package/@blackbelt-technology/pi-agent-dashboard) | Warm-starts
|
|
150
|
-
| `web-access` | [pi-web-access](https://www.npmjs.com/package/pi-web-access) | `web_search`, fetch, GitHub clone, PDF/video. Optional keys: `~/.pi/web-search.json`
|
|
151
|
-
| `pi-flows` | [@blackbelt-technology/pi-flows](https://www.npmjs.com/package/@blackbelt-technology/pi-flows) | YAML DAG workflows (`/flows`). Peers **0.74** + `@sinclair/typebox`; off by default on 0.78 stacks
|
|
171
|
+
| Toggle | Package | Notes |
|
|
172
|
+
| ----------------- | ------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ |
|
|
173
|
+
| `caveman` | [pi-caveman](https://www.npmjs.com/package/pi-caveman) | Terse English (`/caveman`). Conflicts with `defaults.language: ja` |
|
|
174
|
+
| `red-green` | [pi-red-green](https://www.npmjs.com/package/pi-red-green) | TDD via `/tdd`, `/tdd-status`. Config: `~/.pi/red-green/config.json` |
|
|
175
|
+
| `agent-dashboard` | [@blackbelt-technology/pi-agent-dashboard](https://www.npmjs.com/package/@blackbelt-technology/pi-agent-dashboard) | Warm-starts before the bridge loads (cold boot up to ~60s). Peers **0.74**; test on 0.78 before relying on it. Node.js ≥ 22.18 |
|
|
176
|
+
| `web-access` | [pi-web-access](https://www.npmjs.com/package/pi-web-access) | `web_search`, fetch, GitHub clone, PDF/video. Optional keys: `~/.pi/web-search.json` |
|
|
177
|
+
| `pi-flows` | [@blackbelt-technology/pi-flows](https://www.npmjs.com/package/@blackbelt-technology/pi-flows) | YAML DAG workflows (`/flows`). Peers **0.74** + `@sinclair/typebox`; off by default on 0.78 stacks |
|
|
178
|
+
| `kanagawa` | [pi-kanagawa](https://www.npmjs.com/package/pi-kanagawa) | Kanagawa theme (also in `/theme` via shipped assets), wave animation, `/thinking`. **Replaces hotmilk footer** when on |
|
|
179
|
+
| `tetris` | [pi-tetris](https://www.npmjs.com/package/pi-tetris) | Play Tetris with `/tetris`. Lightweight; default off |
|
|
152
180
|
|
|
153
181
|
**Agent dashboard troubleshooting**
|
|
154
182
|
|
|
155
183
|
- Run **one** dashboard process: either hotmilk warm-start (`agent-dashboard: true`) **or** manual `npm run dashboard:start`, not both.
|
|
156
184
|
- Keep only `"hotmilk"` in `~/.pi/agent/settings.json` `packages[]` (not a standalone dashboard extension path). Hotmilk prunes duplicate dashboard paths on session start when `agent-dashboard` is enabled.
|
|
157
|
-
-
|
|
185
|
+
- Hotmilk seeds dashboard HTTP port **8102** (when config still has upstream default `8000`) to avoid common port conflicts. Custom ports are preserved.
|
|
186
|
+
- `EADDRINUSE` on `8102` (or `9999` for pi bridge): stop the stale dashboard (`pi-dashboard stop` or `lsof -i :8102` / `:9999`), then restart Pi.
|
|
158
187
|
- Without `zrok` on PATH, hotmilk sets `tunnel.enabled` to `false`.
|
|
188
|
+
- **`ERR TypeScript loader` / jiti or tsx not found** (paths under `~/.pi-dashboard/node_modules/`): the dashboard doctor checks the legacy managed install dir and hardcodes port **8000** in its messages. Hotmilk runs on **8102** with bundled jiti from `pi-dashboard-server`. If `http://localhost:8102/api/health` returns `{"ok":true}`, the doctor error is a false positive — hotmilk logs a hint on warm-start. To silence it entirely: run **Help → Setup** once, or `npm install jiti` under `~/.pi-dashboard` after setup creates that tree.
|
|
189
|
+
|
|
190
|
+
### Cursor models (optional, not bundled)
|
|
191
|
+
|
|
192
|
+
hotmilk does not ship [@netandreus/pi-cursor-provider](https://www.npmjs.com/package/@netandreus/pi-cursor-provider). Install when you route Pi through the Cursor Agent CLI:
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
pi install npm:@netandreus/pi-cursor-provider
|
|
196
|
+
agent login
|
|
197
|
+
# then in Pi: /model cursor/auto
|
|
198
|
+
```
|
|
159
199
|
|
|
160
200
|
### Migrating from `pi-ninja`
|
|
161
201
|
|
package/hotmilk.json
CHANGED
|
@@ -6,11 +6,10 @@
|
|
|
6
6
|
"context-mode": true,
|
|
7
7
|
"ask-user": true,
|
|
8
8
|
"graphify": true,
|
|
9
|
-
"subagents":
|
|
9
|
+
"subagents": true,
|
|
10
10
|
"goal": true,
|
|
11
11
|
"docparser": true,
|
|
12
12
|
"obsidian": true,
|
|
13
|
-
"cursor-provider": true,
|
|
14
13
|
"btw": true,
|
|
15
14
|
"simplify": true,
|
|
16
15
|
"rtk-optimizer": false,
|
|
@@ -20,7 +19,9 @@
|
|
|
20
19
|
"red-green": false,
|
|
21
20
|
"agent-dashboard": false,
|
|
22
21
|
"web-access": false,
|
|
23
|
-
"pi-flows": false
|
|
22
|
+
"pi-flows": false,
|
|
23
|
+
"kanagawa": false,
|
|
24
|
+
"tetris": false
|
|
24
25
|
},
|
|
25
26
|
"graph": {
|
|
26
27
|
"warnOnStale": true,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hotmilk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
4
4
|
"description": "hotmilk — Pi package bundling gentle-pi, context-mode, graphify, subagents, and bundled extension toggles via hotmilk.json",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"hotmilk",
|
|
@@ -42,7 +42,6 @@
|
|
|
42
42
|
"@blackbelt-technology/pi-agent-dashboard": "^0.5.4",
|
|
43
43
|
"@blackbelt-technology/pi-flows": "^0.2.1",
|
|
44
44
|
"@haispeed/pi-obsidian": "^0.1.1",
|
|
45
|
-
"@netandreus/pi-cursor-provider": "^0.1.4",
|
|
46
45
|
"@tomxprime/planning-with-files": "^1.1.0",
|
|
47
46
|
"context-mode": "^1.0.151",
|
|
48
47
|
"gentle-pi": "^0.3.10",
|
|
@@ -52,11 +51,13 @@
|
|
|
52
51
|
"pi-caveman": "^1.0.7",
|
|
53
52
|
"pi-docparser": "^3.0.1",
|
|
54
53
|
"pi-goal": "^0.1.5",
|
|
54
|
+
"pi-kanagawa": "^1.0.0",
|
|
55
55
|
"pi-mcp-adapter": "^2.8.0",
|
|
56
56
|
"pi-red-green": "^0.2.2",
|
|
57
57
|
"pi-rtk-optimizer": "^0.8.1",
|
|
58
58
|
"pi-simplify": "^0.2.2",
|
|
59
59
|
"pi-subagents": "^0.25.0",
|
|
60
|
+
"pi-tetris": "^1.3.1",
|
|
60
61
|
"pi-web-access": "^0.10.7"
|
|
61
62
|
},
|
|
62
63
|
"devDependencies": {
|
|
@@ -95,13 +96,13 @@
|
|
|
95
96
|
"node_modules/pi-subagents/skills",
|
|
96
97
|
"node_modules/pi-docparser/skills",
|
|
97
98
|
"node_modules/@haispeed/pi-obsidian/skills",
|
|
98
|
-
"node_modules/pi-btw/skills",
|
|
99
99
|
"node_modules/pi-ask-user/skills",
|
|
100
100
|
"node_modules/pi-web-access/skills",
|
|
101
101
|
"node_modules/@tomxprime/planning-with-files"
|
|
102
102
|
],
|
|
103
103
|
"themes": [
|
|
104
|
-
"./themes"
|
|
104
|
+
"./themes",
|
|
105
|
+
"node_modules/pi-kanagawa/themes"
|
|
105
106
|
]
|
|
106
107
|
}
|
|
107
108
|
}
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { Type } from "@sinclair/typebox";
|
|
5
|
+
import * as piSdk from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import {
|
|
7
|
+
createExtensionRuntime,
|
|
8
|
+
defineTool,
|
|
9
|
+
type AgentToolResult,
|
|
10
|
+
type CreateAgentSessionOptions,
|
|
11
|
+
type ExtensionAPI,
|
|
12
|
+
type ResourceLoader,
|
|
13
|
+
type ToolDefinition,
|
|
14
|
+
} from "@earendil-works/pi-coding-agent";
|
|
15
|
+
import type { BundledExtensionId } from "../config/bundled-extensions.ts";
|
|
16
|
+
import { bundledImportUrl } from "./resolve-bundled.ts";
|
|
17
|
+
|
|
18
|
+
// --- Session config (injected before btw extension loads) ---
|
|
19
|
+
|
|
20
|
+
export type HotmilkBtwConfig = {
|
|
21
|
+
extensionToggles: Readonly<Record<BundledExtensionId, boolean>>;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
let activeConfig: HotmilkBtwConfig | null = null;
|
|
25
|
+
|
|
26
|
+
export function setHotmilkBtwConfig(config: HotmilkBtwConfig): void {
|
|
27
|
+
activeConfig = config;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function getHotmilkBtwConfig(): HotmilkBtwConfig {
|
|
31
|
+
if (!activeConfig) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
"hotmilk BTW config not initialized — registerBundledExtensions must call setHotmilkBtwConfig first",
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
return activeConfig;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function resetHotmilkBtwConfigForTests(): void {
|
|
40
|
+
activeConfig = null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// --- Prompt shaping ---
|
|
44
|
+
|
|
45
|
+
export function stripDynamicSystemPromptFooter(systemPrompt: string): string {
|
|
46
|
+
return systemPrompt
|
|
47
|
+
.replace(/\nCurrent date and time:[^\n]*(?:\nCurrent working directory:[^\n]*)?$/u, "")
|
|
48
|
+
.replace(/\nCurrent working directory:[^\n]*$/u, "")
|
|
49
|
+
.trim();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const MAIN_SESSION_SECTION_MARKERS = [
|
|
53
|
+
/^## graphify\b/im,
|
|
54
|
+
/^## el Gentleman Orchestrator\b/im,
|
|
55
|
+
/^# el Gentleman Identity and Harness\b/im,
|
|
56
|
+
/^<context_window_protection>/im,
|
|
57
|
+
/^IMPORTANT: You are in CAVEMAN MODE\./im,
|
|
58
|
+
/^## SDD Session Preflight\b/im,
|
|
59
|
+
/^<behavioral_directive>/im,
|
|
60
|
+
] as const;
|
|
61
|
+
|
|
62
|
+
export function stripHotmilkMainSessionHarness(systemPrompt: string): string {
|
|
63
|
+
let next = systemPrompt;
|
|
64
|
+
for (const marker of MAIN_SESSION_SECTION_MARKERS) {
|
|
65
|
+
const match = marker.exec(next);
|
|
66
|
+
if (!match || match.index === undefined) continue;
|
|
67
|
+
next = next.slice(0, match.index).trimEnd();
|
|
68
|
+
}
|
|
69
|
+
return next.trim();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const HOTMILK_BTW_SYSTEM_PROMPT = [
|
|
73
|
+
"You are having an aside conversation with the user, separate from their main working session.",
|
|
74
|
+
"If main session messages are provided, they are for context only — that work is being handled by another agent.",
|
|
75
|
+
"If no main session messages are provided, treat this as a fully contextless tangent thread and rely only on the user's words plus your general instructions.",
|
|
76
|
+
"Focus on answering the user's side questions, helping them think through ideas, or planning next steps.",
|
|
77
|
+
"Do not act as if you need to continue unfinished work from the main session unless the user explicitly asks you to prepare something for injection back to it.",
|
|
78
|
+
"hotmilk routing: multi-file implementation, scout/worker/review, and SDD phases belong on the main line via subagents (Task, /run, /chain) — not in BTW.",
|
|
79
|
+
"While subagents may be writing in worktrees, avoid edit/write on the main cwd unless the user explicitly asks; prefer read-only checks and :tangent for brainstorming.",
|
|
80
|
+
"ctx_search is available as a read-only proxy to the main session knowledge base — use it for timeline recall, prior decisions, and indexed content. Do not use ctx_execute, ctx_batch_execute, or ctx_purge from BTW; loading context-mode here would spawn a second MCP child.",
|
|
81
|
+
].join(" ");
|
|
82
|
+
|
|
83
|
+
export function buildHotmilkBtwAppendPrompt(options: {
|
|
84
|
+
graphifyEnabled: boolean;
|
|
85
|
+
subagentsEnabled: boolean;
|
|
86
|
+
contextModeEnabled: boolean;
|
|
87
|
+
}): string[] {
|
|
88
|
+
const lines = [HOTMILK_BTW_SYSTEM_PROMPT];
|
|
89
|
+
if (options.contextModeEnabled) {
|
|
90
|
+
lines.push(
|
|
91
|
+
"Prefer ctx_search(queries: [...]) for session memory and indexed corpus recall instead of large bash output or re-reading raw files.",
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
if (options.graphifyEnabled) {
|
|
95
|
+
lines.push(
|
|
96
|
+
'When graphify-out/graph.json exists, prefer the graphify_query tool (or `graphify query "..."` via bash) over broad grep for architecture and cross-module questions.',
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
if (options.subagentsEnabled) {
|
|
100
|
+
lines.push(
|
|
101
|
+
"When the user asks for implementation or multi-file exploration, answer briefly in BTW if possible, then suggest /btw:inject plus a main-line Task — do not start parallel file edits here.",
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
return lines;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const PI_BTW_APPEND_MARKERS = [
|
|
108
|
+
"aside conversation with the user, separate from their main working session",
|
|
109
|
+
"Summarize the side conversation concisely",
|
|
110
|
+
] as const;
|
|
111
|
+
|
|
112
|
+
/** BTW must never load bundled extensions (context-mode → MCP fork-bomb #516). */
|
|
113
|
+
function createBtwEmptyExtensionsResult() {
|
|
114
|
+
return { extensions: [], errors: [], runtime: createExtensionRuntime() };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function isPiBtwResourceLoader(loader: ResourceLoader): boolean {
|
|
118
|
+
const append = loader.getAppendSystemPrompt();
|
|
119
|
+
const text = (Array.isArray(append) ? append : []).join("\n");
|
|
120
|
+
return PI_BTW_APPEND_MARKERS.some((marker) => text.includes(marker));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function isBtwSummarizeSession(loader: ResourceLoader): boolean {
|
|
124
|
+
const append = loader.getAppendSystemPrompt();
|
|
125
|
+
const text = (Array.isArray(append) ? append : []).join("\n");
|
|
126
|
+
return text.includes("Summarize the side conversation");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function adaptBtwResourceLoaderForHotmilk(
|
|
130
|
+
loader: ResourceLoader,
|
|
131
|
+
config: HotmilkBtwConfig,
|
|
132
|
+
): ResourceLoader {
|
|
133
|
+
const emptyExtensions = createBtwEmptyExtensionsResult();
|
|
134
|
+
const sharedLoader = {
|
|
135
|
+
getExtensions: () => emptyExtensions,
|
|
136
|
+
getSkills: () => ({ skills: [], diagnostics: [] }),
|
|
137
|
+
getPrompts: () => ({ prompts: [], diagnostics: [] }),
|
|
138
|
+
getThemes: () => ({ themes: [], diagnostics: [] }),
|
|
139
|
+
getAgentsFiles: () => ({ agentsFiles: [] }),
|
|
140
|
+
extendResources: () => {},
|
|
141
|
+
reload: async () => {},
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
if (isBtwSummarizeSession(loader)) {
|
|
145
|
+
return {
|
|
146
|
+
...sharedLoader,
|
|
147
|
+
getSystemPrompt: () => stripHotmilkMainSessionHarness(loader.getSystemPrompt() ?? ""),
|
|
148
|
+
getAppendSystemPrompt: () => loader.getAppendSystemPrompt(),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
...sharedLoader,
|
|
154
|
+
getSystemPrompt: () => stripHotmilkMainSessionHarness(loader.getSystemPrompt() ?? ""),
|
|
155
|
+
getAppendSystemPrompt: () =>
|
|
156
|
+
buildHotmilkBtwAppendPrompt({
|
|
157
|
+
graphifyEnabled: config.extensionToggles.graphify === true,
|
|
158
|
+
subagentsEnabled: config.extensionToggles.subagents === true,
|
|
159
|
+
contextModeEnabled: config.extensionToggles["context-mode"] === true,
|
|
160
|
+
}),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// --- Main-session ctx_search capture (proxy target for BTW) ---
|
|
165
|
+
|
|
166
|
+
type MainCtxSearchTool = {
|
|
167
|
+
description: string;
|
|
168
|
+
parameters: ToolDefinition["parameters"];
|
|
169
|
+
execute: (
|
|
170
|
+
toolCallId: string,
|
|
171
|
+
params: Record<string, unknown>,
|
|
172
|
+
) => Promise<AgentToolResult<unknown>>;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
let mainCtxSearchTool: MainCtxSearchTool | null = null;
|
|
176
|
+
let ctxSearchCaptureInstalled = false;
|
|
177
|
+
|
|
178
|
+
export function captureMainCtxSearchTool(tool: {
|
|
179
|
+
name: string;
|
|
180
|
+
description: string;
|
|
181
|
+
parameters: ToolDefinition["parameters"];
|
|
182
|
+
execute: ToolDefinition["execute"];
|
|
183
|
+
}): void {
|
|
184
|
+
if (tool.name !== "ctx_search") return;
|
|
185
|
+
mainCtxSearchTool = {
|
|
186
|
+
description: tool.description,
|
|
187
|
+
parameters: tool.parameters,
|
|
188
|
+
execute: (toolCallId, params) =>
|
|
189
|
+
tool.execute(toolCallId, params as never, undefined, undefined, undefined as never),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function getMainCtxSearchToolForTests(): MainCtxSearchTool | null {
|
|
194
|
+
return mainCtxSearchTool;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function resetMainCtxSearchCaptureForTests(): void {
|
|
198
|
+
mainCtxSearchTool = null;
|
|
199
|
+
ctxSearchCaptureInstalled = false;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function resolveMainCtxSearchTool(): Promise<MainCtxSearchTool | null> {
|
|
203
|
+
if (mainCtxSearchTool) return mainCtxSearchTool;
|
|
204
|
+
try {
|
|
205
|
+
const mod = await import(bundledImportUrl("context-mode/build/adapters/pi/extension.js"));
|
|
206
|
+
await mod._mcpBridgeReady;
|
|
207
|
+
} catch {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
return mainCtxSearchTool;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Capture context-mode's main-session ctx_search registration so BTW can proxy
|
|
215
|
+
* without loading context-mode inside the BTW sub-session (fork-bomb guard #516).
|
|
216
|
+
*/
|
|
217
|
+
export function installHotmilkCtxSearchCapture(pi: ExtensionAPI): void {
|
|
218
|
+
if (ctxSearchCaptureInstalled) return;
|
|
219
|
+
ctxSearchCaptureInstalled = true;
|
|
220
|
+
|
|
221
|
+
const original = pi.registerTool.bind(pi);
|
|
222
|
+
pi.registerTool = (tool) => {
|
|
223
|
+
captureMainCtxSearchTool(tool);
|
|
224
|
+
return original(tool);
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// --- Tools ---
|
|
229
|
+
|
|
230
|
+
const GRAPH_JSON = join("graphify-out", "graph.json");
|
|
231
|
+
|
|
232
|
+
const CTX_SEARCH_FALLBACK_PARAMS = Type.Object({
|
|
233
|
+
queries: Type.Optional(
|
|
234
|
+
Type.Array(
|
|
235
|
+
Type.String({ description: "Search queries — batch all related questions in one call." }),
|
|
236
|
+
),
|
|
237
|
+
),
|
|
238
|
+
query: Type.Optional(
|
|
239
|
+
Type.String({ description: "Single search query (legacy alias for queries[0])." }),
|
|
240
|
+
),
|
|
241
|
+
limit: Type.Optional(Type.Number({ description: "Results per query (default 3)." })),
|
|
242
|
+
source: Type.Optional(
|
|
243
|
+
Type.String({ description: "Filter to an indexed source label (partial match)." }),
|
|
244
|
+
),
|
|
245
|
+
contentType: Type.Optional(
|
|
246
|
+
Type.Union([Type.Literal("code"), Type.Literal("prose")], {
|
|
247
|
+
description: "Filter by content shape.",
|
|
248
|
+
}),
|
|
249
|
+
),
|
|
250
|
+
sort: Type.Optional(
|
|
251
|
+
Type.Union([Type.Literal("relevance"), Type.Literal("timeline")], {
|
|
252
|
+
description: "relevance (default) or timeline for session memory.",
|
|
253
|
+
}),
|
|
254
|
+
),
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
export function graphifyGraphExists(cwd = process.cwd()): boolean {
|
|
258
|
+
return existsSync(join(cwd, GRAPH_JSON));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function resolveHotmilkBtwTools(config: HotmilkBtwConfig): string[] {
|
|
262
|
+
if (config.extensionToggles.subagents === true) {
|
|
263
|
+
return ["read", "grep", "find", "ls", "bash"];
|
|
264
|
+
}
|
|
265
|
+
return ["read", "bash", "edit", "write"];
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function createCtxSearchProxyTool(mainTool: MainCtxSearchTool | null): ToolDefinition {
|
|
269
|
+
return defineTool({
|
|
270
|
+
name: "ctx_search",
|
|
271
|
+
label: "Search indexed content",
|
|
272
|
+
description:
|
|
273
|
+
mainTool?.description ??
|
|
274
|
+
"Search the main session knowledge base (read-only proxy). Batch related questions in queries: [...].",
|
|
275
|
+
parameters: mainTool?.parameters ?? CTX_SEARCH_FALLBACK_PARAMS,
|
|
276
|
+
execute: async (toolCallId, params) => {
|
|
277
|
+
const main = await resolveMainCtxSearchTool();
|
|
278
|
+
if (!main) {
|
|
279
|
+
return {
|
|
280
|
+
content: [
|
|
281
|
+
{
|
|
282
|
+
type: "text",
|
|
283
|
+
text:
|
|
284
|
+
"ctx_search unavailable — context-mode bridge is not ready on the main session. " +
|
|
285
|
+
"Ensure context-mode is enabled, wait for startup, or use small bounded bash output.",
|
|
286
|
+
},
|
|
287
|
+
],
|
|
288
|
+
details: { ok: false as const },
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
return main.execute(toolCallId, params as Record<string, unknown>);
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function createHotmilkBtwCustomTools(config: HotmilkBtwConfig): ToolDefinition[] {
|
|
297
|
+
const tools: ToolDefinition[] = [];
|
|
298
|
+
|
|
299
|
+
if (config.extensionToggles["context-mode"] === true) {
|
|
300
|
+
tools.push(createCtxSearchProxyTool(mainCtxSearchTool));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (config.extensionToggles.graphify !== true || !graphifyGraphExists()) {
|
|
304
|
+
return tools;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const graphifyQuery = defineTool({
|
|
308
|
+
name: "graphify_query",
|
|
309
|
+
label: "Graphify query",
|
|
310
|
+
description:
|
|
311
|
+
"Traverse graphify-out/graph.json for architecture and relationship questions. Prefer this over grep for cross-module queries.",
|
|
312
|
+
parameters: Type.Object({
|
|
313
|
+
question: Type.String({ description: "Natural-language question about the codebase graph" }),
|
|
314
|
+
dfs: Type.Optional(
|
|
315
|
+
Type.Boolean({ description: "Use depth-first traversal for dependency chains" }),
|
|
316
|
+
),
|
|
317
|
+
budget: Type.Optional(
|
|
318
|
+
Type.Number({ description: "Optional token budget cap for the graphify answer" }),
|
|
319
|
+
),
|
|
320
|
+
}),
|
|
321
|
+
execute: async (_toolCallId, params, _signal, _onUpdate, ctx) => {
|
|
322
|
+
const args = ["query", params.question, "--graph", GRAPH_JSON];
|
|
323
|
+
if (params.dfs) args.push("--dfs");
|
|
324
|
+
if (params.budget !== undefined) args.push("--budget", String(params.budget));
|
|
325
|
+
|
|
326
|
+
try {
|
|
327
|
+
const output = execFileSync("graphify", args, {
|
|
328
|
+
cwd: ctx.cwd,
|
|
329
|
+
encoding: "utf8",
|
|
330
|
+
maxBuffer: 512 * 1024,
|
|
331
|
+
timeout: 120_000,
|
|
332
|
+
});
|
|
333
|
+
return {
|
|
334
|
+
content: [{ type: "text", text: output.trim() || "(graphify returned no output)" }],
|
|
335
|
+
details: { question: params.question, ok: true as const },
|
|
336
|
+
};
|
|
337
|
+
} catch (error) {
|
|
338
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
339
|
+
return {
|
|
340
|
+
content: [
|
|
341
|
+
{
|
|
342
|
+
type: "text",
|
|
343
|
+
text: `graphify query failed: ${message}. Ensure graphify-out/graph.json exists or run graphify on the main session.`,
|
|
344
|
+
},
|
|
345
|
+
],
|
|
346
|
+
details: { question: params.question, ok: false as const, error: message },
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
tools.push(graphifyQuery);
|
|
353
|
+
return tools;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function patchHotmilkBtwSessionOptions(
|
|
357
|
+
options: CreateAgentSessionOptions,
|
|
358
|
+
): CreateAgentSessionOptions {
|
|
359
|
+
const loader = options.resourceLoader;
|
|
360
|
+
if (!loader || !isPiBtwResourceLoader(loader)) {
|
|
361
|
+
return options;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const config = getHotmilkBtwConfig();
|
|
365
|
+
const summarize = isBtwSummarizeSession(loader);
|
|
366
|
+
const next: CreateAgentSessionOptions = {
|
|
367
|
+
...options,
|
|
368
|
+
resourceLoader: adaptBtwResourceLoaderForHotmilk(loader, config),
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
if (!summarize) {
|
|
372
|
+
next.tools = resolveHotmilkBtwTools(config);
|
|
373
|
+
next.customTools = createHotmilkBtwCustomTools(config);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return next;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
let btwHookInstalled = false;
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Wrap Pi {@link createAgentSession} so pi-btw sub-sessions get hotmilk prompts/tools
|
|
383
|
+
* without vendoring the upstream extension (~2k lines).
|
|
384
|
+
*/
|
|
385
|
+
export function installHotmilkBtwSessionHook(): void {
|
|
386
|
+
if (btwHookInstalled) return;
|
|
387
|
+
btwHookInstalled = true;
|
|
388
|
+
|
|
389
|
+
const original = piSdk.createAgentSession.bind(piSdk);
|
|
390
|
+
const patched = async (options?: CreateAgentSessionOptions) => {
|
|
391
|
+
const resolved = options ? patchHotmilkBtwSessionOptions(options) : options;
|
|
392
|
+
return original(resolved);
|
|
393
|
+
};
|
|
394
|
+
(piSdk as { createAgentSession: typeof patched }).createAgentSession = patched;
|
|
395
|
+
}
|