hotmilk 0.1.11 → 0.1.13
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 +8 -11
- package/README.md +56 -43
- package/agents/README.md +16 -0
- package/agents/assistant.md +106 -0
- package/agents/coach.md +81 -0
- package/agents/coder.md +50 -0
- package/agents/designer.md +85 -0
- package/agents/planner.md +68 -0
- package/agents/reviewer.md +48 -0
- package/hotmilk.json +1 -1
- package/package.json +22 -21
- package/skills/recommend-research/SKILL.md +1 -1
- package/src/bootstrap/btw.ts +395 -0
- package/src/bootstrap/dashboard.ts +71 -9
- package/src/bootstrap/extensions.ts +16 -2
- package/src/bootstrap/resolve-bundled.ts +52 -1
- package/src/bootstrap/subagents-doctor.ts +109 -0
- package/src/config/bundled-extensions.ts +1 -1
- package/src/extensions/btw.ts +17 -0
- package/src/index.ts +6 -0
- package/src/ui/github-user.ts +3 -1
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: reviewer
|
|
3
|
+
description: Hotmilk adversarial review — evidence, bun check, plan alignment, no scope creep
|
|
4
|
+
tools: read, grep, find, ls, bash, edit, write, intercom
|
|
5
|
+
thinking: high
|
|
6
|
+
systemPromptMode: replace
|
|
7
|
+
inheritProjectContext: true
|
|
8
|
+
inheritSkills: true
|
|
9
|
+
defaultReads: plan.md, progress.md
|
|
10
|
+
defaultContext: fresh
|
|
11
|
+
package: hotmilk
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
You are the **hotmilk** review subagent. Inspect and report with **evidence** — do not guess.
|
|
15
|
+
|
|
16
|
+
## Review focus
|
|
17
|
+
|
|
18
|
+
- Diff matches intent and `plan.md` (when provided).
|
|
19
|
+
- **Surgical diff**: unrelated refactors, manifest/doc drift vs `bundled-extensions.ts` and README Configuration.
|
|
20
|
+
- Tests and types: suggest or run `bun test` / `bun run check` when TS touched; cite failures.
|
|
21
|
+
- **Extension changes**: bundled row ↔ README toggle table ↔ `bundled-extensions.ts`; competition slots respected.
|
|
22
|
+
- If estimated diff >400 lines, flag **chained PR** reviewer load (gentle-ai / chained-pr).
|
|
23
|
+
|
|
24
|
+
## Working rules
|
|
25
|
+
|
|
26
|
+
- With pi-subagents 0.28+ acceptance gates: **reviewed** requires your independent result — worker self-attestation alone is not enough.
|
|
27
|
+
- Parse fenced `acceptance-report` blocks when present; required criteria must be satisfied with cited evidence.
|
|
28
|
+
- Read `plan.md`, `progress.md`, and relevant files first.
|
|
29
|
+
- `bash` for read-only inspection (`git diff`, tests).
|
|
30
|
+
- Review-only wins over progress writes when conflicted.
|
|
31
|
+
- Small fixes allowed when parent authorized; otherwise findings only.
|
|
32
|
+
- Repo-local `progress.md` scratch files are OK if untracked/gitignored.
|
|
33
|
+
|
|
34
|
+
## Output format
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
## Review
|
|
38
|
+
- Correct: … (evidence)
|
|
39
|
+
- Fixed: … (if you applied a fix)
|
|
40
|
+
- Blocker: … (must fix before merge)
|
|
41
|
+
- Note: …
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Cite `path` and line ranges. If clean, say so plainly.
|
|
45
|
+
|
|
46
|
+
## Supervisor coordination
|
|
47
|
+
|
|
48
|
+
`contact_supervisor` with `need_decision` when blocked. No routine completion handoffs.
|
package/hotmilk.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hotmilk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
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",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
},
|
|
20
20
|
"files": [
|
|
21
21
|
"src",
|
|
22
|
+
"agents",
|
|
22
23
|
"prompts",
|
|
23
24
|
"skills",
|
|
24
25
|
"themes",
|
|
@@ -30,9 +31,6 @@
|
|
|
30
31
|
],
|
|
31
32
|
"type": "module",
|
|
32
33
|
"scripts": {
|
|
33
|
-
"dashboard:start": "pi-dashboard start",
|
|
34
|
-
"dashboard:stop": "pi-dashboard stop",
|
|
35
|
-
"dashboard:status": "pi-dashboard status",
|
|
36
34
|
"lint": "vp lint",
|
|
37
35
|
"format": "vp fmt --write",
|
|
38
36
|
"test": "vp test",
|
|
@@ -44,37 +42,41 @@
|
|
|
44
42
|
"@haispeed/pi-obsidian": "^0.1.1",
|
|
45
43
|
"@tomxprime/planning-with-files": "^1.1.0",
|
|
46
44
|
"context-mode": "^1.0.151",
|
|
47
|
-
"gentle-pi": "^0.
|
|
45
|
+
"gentle-pi": "^0.4.5",
|
|
48
46
|
"graphify-pi": "^0.3.0",
|
|
49
|
-
"pi-ask-user": "^0.11.
|
|
47
|
+
"pi-ask-user": "^0.11.2",
|
|
50
48
|
"pi-btw": "^0.4.0",
|
|
51
49
|
"pi-caveman": "^1.0.7",
|
|
52
50
|
"pi-docparser": "^3.0.1",
|
|
53
51
|
"pi-goal": "^0.1.5",
|
|
54
52
|
"pi-kanagawa": "^1.0.0",
|
|
55
|
-
"pi-mcp-adapter": "^2.
|
|
53
|
+
"pi-mcp-adapter": "^2.9.0",
|
|
56
54
|
"pi-red-green": "^0.2.2",
|
|
57
|
-
"pi-rtk-optimizer": "^0.8.
|
|
55
|
+
"pi-rtk-optimizer": "^0.8.2",
|
|
58
56
|
"pi-simplify": "^0.2.2",
|
|
59
|
-
"pi-subagents": "^0.
|
|
57
|
+
"pi-subagents": "^0.28.0",
|
|
60
58
|
"pi-tetris": "^1.3.1",
|
|
61
59
|
"pi-web-access": "^0.10.7"
|
|
62
60
|
},
|
|
63
61
|
"devDependencies": {
|
|
64
|
-
"@earendil-works/pi-agent-core": "^0.78.
|
|
65
|
-
"@earendil-works/pi-ai": "^0.78.
|
|
66
|
-
"@earendil-works/pi-coding-agent": "^0.78.
|
|
67
|
-
"@earendil-works/pi-tui": "^0.78.
|
|
62
|
+
"@earendil-works/pi-agent-core": "^0.78.1",
|
|
63
|
+
"@earendil-works/pi-ai": "^0.78.1",
|
|
64
|
+
"@earendil-works/pi-coding-agent": "^0.78.1",
|
|
65
|
+
"@earendil-works/pi-tui": "^0.78.1",
|
|
68
66
|
"@types/node": "^22.19.19",
|
|
69
|
-
"typebox": "^1.
|
|
70
|
-
"vite-plus": "^0.1.
|
|
67
|
+
"typebox": "^1.2.0",
|
|
68
|
+
"vite-plus": "^0.1.24"
|
|
71
69
|
},
|
|
72
70
|
"peerDependencies": {
|
|
73
|
-
"@earendil-works/pi-agent-core": "^0.78.
|
|
74
|
-
"@earendil-works/pi-ai": "^0.78.
|
|
75
|
-
"@earendil-works/pi-coding-agent": "^0.78.
|
|
76
|
-
"@earendil-works/pi-tui": "^0.78.
|
|
77
|
-
"typebox": "^1.
|
|
71
|
+
"@earendil-works/pi-agent-core": "^0.78.1",
|
|
72
|
+
"@earendil-works/pi-ai": "^0.78.1",
|
|
73
|
+
"@earendil-works/pi-coding-agent": "^0.78.1",
|
|
74
|
+
"@earendil-works/pi-tui": "^0.78.1",
|
|
75
|
+
"typebox": "^1.2.0"
|
|
76
|
+
},
|
|
77
|
+
"overrides": {
|
|
78
|
+
"@fastify/static": "9.1.3",
|
|
79
|
+
"@mozilla/readability": "0.6.0"
|
|
78
80
|
},
|
|
79
81
|
"engines": {
|
|
80
82
|
"node": "22"
|
|
@@ -96,7 +98,6 @@
|
|
|
96
98
|
"node_modules/pi-subagents/skills",
|
|
97
99
|
"node_modules/pi-docparser/skills",
|
|
98
100
|
"node_modules/@haispeed/pi-obsidian/skills",
|
|
99
|
-
"node_modules/pi-btw/skills",
|
|
100
101
|
"node_modules/pi-ask-user/skills",
|
|
101
102
|
"node_modules/pi-web-access/skills",
|
|
102
103
|
"node_modules/@tomxprime/planning-with-files"
|
|
@@ -10,4 +10,4 @@ description: "Single entry for technology recommendations: judge scope, shortlis
|
|
|
10
10
|
3. **Verify** — Official docs for contracts and usage; repository / local checkout when internals, edge cases, or performance matter.
|
|
11
11
|
4. **Mechanics** — Web search and doc pages for reading; `ghq` or `git clone` when the tree must be local.
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
If the repo ships extra skills under `./skills/`, load those `SKILL.md` files only when this flow is not enough.
|
|
@@ -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
|
+
}
|
|
@@ -99,17 +99,57 @@ export function persistDashboardPiPort(piPort: number): boolean {
|
|
|
99
99
|
|
|
100
100
|
export type DashboardWarmStartDecision = "skip-running" | "skip-conflict" | "launch";
|
|
101
101
|
|
|
102
|
+
/** When TCP is bound but /api/health is not ready yet (cold boot). */
|
|
103
|
+
const WARM_START_PORT_OCCUPIED_WAIT_MS = 5_000;
|
|
104
|
+
const WARM_START_PORT_OCCUPIED_POLL_MS = 500;
|
|
105
|
+
|
|
106
|
+
export type DashboardWarmStartDecisionOpts = {
|
|
107
|
+
occupiedWaitMs?: number;
|
|
108
|
+
occupiedPollMs?: number;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
function sleep(ms: number): Promise<void> {
|
|
112
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
113
|
+
}
|
|
114
|
+
|
|
102
115
|
export async function resolveDashboardWarmStartDecision(
|
|
103
116
|
port: number,
|
|
104
117
|
probe: typeof isDashboardRunning = isDashboardRunning,
|
|
118
|
+
isFree: (port: number) => Promise<boolean> = isTcpPortFree,
|
|
119
|
+
opts: DashboardWarmStartDecisionOpts = {},
|
|
105
120
|
): Promise<DashboardWarmStartDecision> {
|
|
106
|
-
const
|
|
121
|
+
const occupiedWaitMs = opts.occupiedWaitMs ?? WARM_START_PORT_OCCUPIED_WAIT_MS;
|
|
122
|
+
const occupiedPollMs = opts.occupiedPollMs ?? WARM_START_PORT_OCCUPIED_POLL_MS;
|
|
123
|
+
let status = await probe(port, "localhost", WARM_START_PROBE_OPTS);
|
|
107
124
|
if (status.running) {
|
|
108
125
|
return "skip-running";
|
|
109
126
|
}
|
|
110
127
|
if (status.portConflict) {
|
|
111
128
|
return "skip-conflict";
|
|
112
129
|
}
|
|
130
|
+
|
|
131
|
+
if (!(await isFree(port))) {
|
|
132
|
+
const deadline = Date.now() + occupiedWaitMs;
|
|
133
|
+
while (Date.now() < deadline) {
|
|
134
|
+
await sleep(occupiedPollMs);
|
|
135
|
+
status = await probe(port, "localhost", WARM_START_PROBE_OPTS);
|
|
136
|
+
if (status.running) {
|
|
137
|
+
return "skip-running";
|
|
138
|
+
}
|
|
139
|
+
if (status.portConflict) {
|
|
140
|
+
return "skip-conflict";
|
|
141
|
+
}
|
|
142
|
+
if (await isFree(port)) {
|
|
143
|
+
return "launch";
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
status = await probe(port, "localhost", WARM_START_PROBE_OPTS);
|
|
147
|
+
if (status.running) {
|
|
148
|
+
return "skip-running";
|
|
149
|
+
}
|
|
150
|
+
return "skip-conflict";
|
|
151
|
+
}
|
|
152
|
+
|
|
113
153
|
return "launch";
|
|
114
154
|
}
|
|
115
155
|
|
|
@@ -184,7 +224,13 @@ export function applyHotmilkDashboardDefaults(): ApplyHotmilkDashboardDefaultsRe
|
|
|
184
224
|
return { updated: true, path: CONFIG_FILE };
|
|
185
225
|
}
|
|
186
226
|
|
|
187
|
-
|
|
227
|
+
/** Dedupes concurrent warm-start only; each session_start/reload re-probes. */
|
|
228
|
+
let warmStartInFlight: Promise<DashboardWarmStartResult> | undefined;
|
|
229
|
+
|
|
230
|
+
/** @internal Clears in-flight warm-start dedupe (tests). */
|
|
231
|
+
export function resetDashboardWarmStartForTests(): void {
|
|
232
|
+
warmStartInFlight = undefined;
|
|
233
|
+
}
|
|
188
234
|
|
|
189
235
|
export type DashboardWarmStartResult = {
|
|
190
236
|
status: "running" | "started" | "skipped-conflict" | "failed";
|
|
@@ -207,15 +253,14 @@ function warmStartResult(
|
|
|
207
253
|
* the bridge extension loads so its 2s auto-start sees a healthy server.
|
|
208
254
|
*/
|
|
209
255
|
export async function ensureDashboardWarmStarted(): Promise<DashboardWarmStartResult> {
|
|
210
|
-
if (
|
|
211
|
-
return
|
|
256
|
+
if (warmStartInFlight) {
|
|
257
|
+
return warmStartInFlight;
|
|
212
258
|
}
|
|
213
259
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
throw err;
|
|
260
|
+
warmStartInFlight = runDashboardWarmStart().finally(() => {
|
|
261
|
+
warmStartInFlight = undefined;
|
|
217
262
|
});
|
|
218
|
-
return
|
|
263
|
+
return warmStartInFlight;
|
|
219
264
|
}
|
|
220
265
|
|
|
221
266
|
async function runDashboardWarmStart(): Promise<DashboardWarmStartResult> {
|
|
@@ -246,9 +291,12 @@ async function runDashboardWarmStart(): Promise<DashboardWarmStartResult> {
|
|
|
246
291
|
|
|
247
292
|
const launchConfig = { port: config.port, piPort };
|
|
248
293
|
|
|
294
|
+
const cliPath = resolveDashboardServerCliPath();
|
|
295
|
+
|
|
249
296
|
try {
|
|
250
297
|
await launchDashboardServer({
|
|
251
|
-
cliPath
|
|
298
|
+
cliPath,
|
|
299
|
+
anchor: cliPath,
|
|
252
300
|
extraArgs: buildWarmStartLaunchArgs(launchConfig),
|
|
253
301
|
stdio: { logFile: dashboardServerLogPath() },
|
|
254
302
|
starter: "Hotmilk",
|
|
@@ -269,3 +317,17 @@ async function runDashboardWarmStart(): Promise<DashboardWarmStartResult> {
|
|
|
269
317
|
return warmStartResult("failed", port, piPort, `${detail}. See ${dashboardServerLogPath()}`);
|
|
270
318
|
}
|
|
271
319
|
}
|
|
320
|
+
|
|
321
|
+
/** Doctor checks ~/.pi-dashboard; hotmilk warm-start uses bundled jiti instead. */
|
|
322
|
+
export function logHotmilkDashboardDoctorHint(result: DashboardWarmStartResult): void {
|
|
323
|
+
if (result.status !== "running" && result.status !== "started") {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
const managedDir = path.join(os.homedir(), ".pi-dashboard");
|
|
327
|
+
if (fs.existsSync(managedDir)) {
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
console.warn(
|
|
331
|
+
`[hotmilk] Dashboard doctor may warn about TypeScript loader or ~/.pi-dashboard — hotmilk uses bundled jiti on port ${result.port}. Ignore if http://localhost:${result.port}/api/health is ok.`,
|
|
332
|
+
);
|
|
333
|
+
}
|