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
|
@@ -72,11 +72,25 @@ export async function registerBundledExtensions(
|
|
|
72
72
|
|
|
73
73
|
if (enabledIds.has("agent-dashboard")) {
|
|
74
74
|
enabledIds.delete("agent-dashboard");
|
|
75
|
-
const { ensureDashboardWarmStarted } =
|
|
76
|
-
|
|
75
|
+
const { ensureDashboardWarmStarted, logHotmilkDashboardDoctorHint } =
|
|
76
|
+
await import("./dashboard.ts");
|
|
77
|
+
const warmStart = await ensureDashboardWarmStarted();
|
|
78
|
+
if (warmStart.status === "failed" || warmStart.status === "skipped-conflict") {
|
|
79
|
+
const detail = warmStart.message ?? `port ${warmStart.port}`;
|
|
80
|
+
console.warn(`[hotmilk] Dashboard warm-start ${warmStart.status}: ${detail}`);
|
|
81
|
+
} else {
|
|
82
|
+
logHotmilkDashboardDoctorHint(warmStart);
|
|
83
|
+
}
|
|
77
84
|
await registerOne(pi, "agent-dashboard");
|
|
78
85
|
}
|
|
79
86
|
|
|
87
|
+
if (enabledIds.has("btw")) {
|
|
88
|
+
const { setHotmilkBtwConfig } = await import("./btw.ts");
|
|
89
|
+
setHotmilkBtwConfig({ extensionToggles: enabled });
|
|
90
|
+
await registerOne(pi, "btw");
|
|
91
|
+
enabledIds.delete("btw");
|
|
92
|
+
}
|
|
93
|
+
|
|
80
94
|
await Promise.all([...enabledIds].map((id) => registerOne(pi, id)));
|
|
81
95
|
|
|
82
96
|
return { globalSkips: appliedSkips };
|
|
@@ -1,9 +1,43 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { basename, dirname, join } from "node:path";
|
|
3
3
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
4
4
|
|
|
5
|
+
const HOTMILK_MODULE_PREFIX = "hotmilk/";
|
|
6
|
+
|
|
7
|
+
function findHotmilkPackageRoot(fromModuleUrl: string): string {
|
|
8
|
+
let dir = dirname(fileURLToPath(fromModuleUrl));
|
|
9
|
+
|
|
10
|
+
while (true) {
|
|
11
|
+
const pkgJsonPath = join(dir, "package.json");
|
|
12
|
+
if (existsSync(pkgJsonPath)) {
|
|
13
|
+
try {
|
|
14
|
+
const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf8")) as { name?: string };
|
|
15
|
+
if (pkg.name === "hotmilk") {
|
|
16
|
+
return dir;
|
|
17
|
+
}
|
|
18
|
+
} catch {
|
|
19
|
+
// ignore invalid package.json while walking up
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const parent = dirname(dir);
|
|
24
|
+
if (parent === dir) {
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
dir = parent;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
throw new Error("Cannot locate hotmilk package root");
|
|
31
|
+
}
|
|
32
|
+
|
|
5
33
|
/** Split `pkg/subpath` — supports scoped packages (`@scope/name/...`). */
|
|
6
34
|
export function parseBundledModulePath(relativePath: string): { pkgName: string; subpath: string } {
|
|
35
|
+
if (relativePath.startsWith(HOTMILK_MODULE_PREFIX)) {
|
|
36
|
+
return {
|
|
37
|
+
pkgName: "hotmilk",
|
|
38
|
+
subpath: relativePath.slice(HOTMILK_MODULE_PREFIX.length),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
7
41
|
if (relativePath.startsWith("@")) {
|
|
8
42
|
const parts = relativePath.split("/");
|
|
9
43
|
return { pkgName: `${parts[0]}/${parts[1]}`, subpath: parts.slice(2).join("/") };
|
|
@@ -23,6 +57,16 @@ export function resolveBundledModule(
|
|
|
23
57
|
relativePath: string,
|
|
24
58
|
fromModuleUrl = import.meta.url,
|
|
25
59
|
): string {
|
|
60
|
+
if (relativePath.startsWith(HOTMILK_MODULE_PREFIX)) {
|
|
61
|
+
const local = join(
|
|
62
|
+
findHotmilkPackageRoot(fromModuleUrl),
|
|
63
|
+
relativePath.slice(HOTMILK_MODULE_PREFIX.length),
|
|
64
|
+
);
|
|
65
|
+
if (existsSync(local)) {
|
|
66
|
+
return local;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
26
70
|
const { pkgName, subpath } = parseBundledModulePath(relativePath);
|
|
27
71
|
let dir = dirname(fileURLToPath(fromModuleUrl));
|
|
28
72
|
|
|
@@ -39,6 +83,13 @@ export function resolveBundledModule(
|
|
|
39
83
|
}
|
|
40
84
|
}
|
|
41
85
|
|
|
86
|
+
if (pkgName === "hotmilk") {
|
|
87
|
+
const inTree = join(dir, subpath);
|
|
88
|
+
if (existsSync(inTree)) {
|
|
89
|
+
return inTree;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
42
93
|
const parent = dirname(dir);
|
|
43
94
|
if (parent === dir) {
|
|
44
95
|
break;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { bundledImportUrl } from "./resolve-bundled.ts";
|
|
6
|
+
|
|
7
|
+
function ensureAccessibleDir(dirPath: string): void {
|
|
8
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
9
|
+
fs.accessSync(dirPath, fs.constants.R_OK | fs.constants.W_OK);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** pi-subagents creates async/results on init but not chain-runs until the first /chain. */
|
|
13
|
+
async function ensureChainRunsDir(): Promise<void> {
|
|
14
|
+
const types = (await import(
|
|
15
|
+
bundledImportUrl("pi-subagents/src/shared/types.ts")
|
|
16
|
+
)) as typeof import("pi-subagents/src/shared/types.ts");
|
|
17
|
+
ensureAccessibleDir(types.CHAIN_RUNS_DIR);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type DoctorModule = typeof import("pi-subagents/src/extension/doctor.ts");
|
|
21
|
+
type ConfigModule = typeof import("pi-subagents/src/extension/config.ts");
|
|
22
|
+
type IntercomModule = typeof import("pi-subagents/src/intercom/intercom-bridge.ts");
|
|
23
|
+
type SubagentState = import("pi-subagents/src/shared/types.ts").SubagentState;
|
|
24
|
+
|
|
25
|
+
function expandTilde(value: string): string {
|
|
26
|
+
return value.startsWith("~/") ? path.join(os.homedir(), value.slice(2)) : value;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function buildDirectDoctorReport(
|
|
30
|
+
pi: ExtensionAPI,
|
|
31
|
+
ctx: ExtensionContext,
|
|
32
|
+
deps: {
|
|
33
|
+
buildDoctorReport: DoctorModule["buildDoctorReport"];
|
|
34
|
+
loadConfig: ConfigModule["loadConfig"];
|
|
35
|
+
resolveIntercomSessionTarget: IntercomModule["resolveIntercomSessionTarget"];
|
|
36
|
+
},
|
|
37
|
+
): string {
|
|
38
|
+
const config = deps.loadConfig();
|
|
39
|
+
let currentSessionFile: string | null = null;
|
|
40
|
+
let currentSessionId: string | null = null;
|
|
41
|
+
let sessionError: string | undefined;
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
currentSessionFile = ctx.sessionManager.getSessionFile() ?? null;
|
|
45
|
+
currentSessionId = ctx.sessionManager.getSessionId();
|
|
46
|
+
} catch (error) {
|
|
47
|
+
sessionError = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let orchestratorTarget: string | undefined;
|
|
51
|
+
try {
|
|
52
|
+
orchestratorTarget = deps.resolveIntercomSessionTarget(
|
|
53
|
+
pi.getSessionName(),
|
|
54
|
+
ctx.sessionManager.getSessionId(),
|
|
55
|
+
);
|
|
56
|
+
} catch {
|
|
57
|
+
// Intercom target is optional for the report.
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return deps.buildDoctorReport({
|
|
61
|
+
cwd: ctx.cwd,
|
|
62
|
+
config,
|
|
63
|
+
state: {
|
|
64
|
+
baseCwd: ctx.cwd,
|
|
65
|
+
currentSessionId,
|
|
66
|
+
} as SubagentState,
|
|
67
|
+
currentSessionFile,
|
|
68
|
+
currentSessionId,
|
|
69
|
+
orchestratorTarget,
|
|
70
|
+
sessionError,
|
|
71
|
+
expandTilde,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* pi-subagents registers `/subagents-doctor` through the slash event bridge, which
|
|
77
|
+
* reads `state.lastUiContext` instead of the command handler's `ctx`. After `/reload`
|
|
78
|
+
* that context can be unset while the command still runs — override with a direct report.
|
|
79
|
+
*/
|
|
80
|
+
export async function registerSubagentsDoctorCommand(pi: ExtensionAPI): Promise<void> {
|
|
81
|
+
await ensureChainRunsDir();
|
|
82
|
+
|
|
83
|
+
const [doctorMod, configMod, intercomMod] = await Promise.all([
|
|
84
|
+
import(bundledImportUrl("pi-subagents/src/extension/doctor.ts")) as Promise<DoctorModule>,
|
|
85
|
+
import(bundledImportUrl("pi-subagents/src/extension/config.ts")) as Promise<ConfigModule>,
|
|
86
|
+
import(
|
|
87
|
+
bundledImportUrl("pi-subagents/src/intercom/intercom-bridge.ts")
|
|
88
|
+
) as Promise<IntercomModule>,
|
|
89
|
+
]);
|
|
90
|
+
|
|
91
|
+
const deps = {
|
|
92
|
+
buildDoctorReport: doctorMod.buildDoctorReport,
|
|
93
|
+
loadConfig: configMod.loadConfig,
|
|
94
|
+
resolveIntercomSessionTarget: intercomMod.resolveIntercomSessionTarget,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
pi.registerCommand("subagents-doctor", {
|
|
98
|
+
description: "Show subagent diagnostics",
|
|
99
|
+
handler: async (_args, ctx) => {
|
|
100
|
+
await ensureChainRunsDir();
|
|
101
|
+
const report = buildDirectDoctorReport(pi, ctx, deps);
|
|
102
|
+
pi.sendMessage({
|
|
103
|
+
customType: "hotmilk-subagents-doctor",
|
|
104
|
+
content: report,
|
|
105
|
+
display: true,
|
|
106
|
+
});
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hotmilk shim for pi-btw: patches createAgentSession before upstream loads so BTW
|
|
3
|
+
* sub-sessions get lighter prompts, read-biased tools when subagents are on, and
|
|
4
|
+
* optional graphify_query — without vendoring pi-btw (~2k lines).
|
|
5
|
+
*/
|
|
6
|
+
import type { ExtensionAPI, ExtensionFactory } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import { installHotmilkBtwSessionHook } from "../bootstrap/btw.ts";
|
|
8
|
+
import { bundledImportUrl } from "../bootstrap/resolve-bundled.ts";
|
|
9
|
+
|
|
10
|
+
installHotmilkBtwSessionHook();
|
|
11
|
+
|
|
12
|
+
const upstreamModule = await import(bundledImportUrl("pi-btw/extensions/btw.ts"));
|
|
13
|
+
const upstream = upstreamModule.default as ExtensionFactory;
|
|
14
|
+
|
|
15
|
+
export default async function hotmilkBtw(pi: ExtensionAPI): Promise<void> {
|
|
16
|
+
await upstream(pi);
|
|
17
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -11,18 +11,24 @@ import { registerGraphHandlers } from "./bootstrap/graph.ts";
|
|
|
11
11
|
import { registerSessionHandlers } from "./bootstrap/session.ts";
|
|
12
12
|
import { registerInputCommands, routeInputCommand } from "./controller/input.ts";
|
|
13
13
|
import { registerHotmilkSessionLogo } from "./ui/session-logo.ts";
|
|
14
|
+
import { installHotmilkCtxSearchCapture } from "./bootstrap/btw.ts";
|
|
15
|
+
import { registerSubagentsDoctorCommand } from "./bootstrap/subagents-doctor.ts";
|
|
14
16
|
|
|
15
17
|
export default async function registerHotmilk(pi: ExtensionAPI): Promise<void> {
|
|
16
18
|
const runtime = createHotmilkRuntime();
|
|
17
19
|
|
|
18
20
|
// Register before bundled imports so session_start handlers exist when bindExtensions emits.
|
|
19
21
|
registerHotmilkSessionLogo(pi);
|
|
22
|
+
installHotmilkCtxSearchCapture(pi);
|
|
20
23
|
|
|
21
24
|
prepareContextStack(runtime.extensionToggles);
|
|
22
25
|
|
|
23
26
|
const bundled = await registerBundledExtensions(pi, runtime.extensionToggles, {
|
|
24
27
|
cwd: process.cwd(),
|
|
25
28
|
});
|
|
29
|
+
if (runtime.extensionToggles.subagents === true) {
|
|
30
|
+
await registerSubagentsDoctorCommand(pi);
|
|
31
|
+
}
|
|
26
32
|
runtime.globalExtensionSkips = bundled.globalSkips;
|
|
27
33
|
registerGraphHandlers(pi, runtime.graph);
|
|
28
34
|
registerDefaultsHandlers(pi, runtime.defaults);
|
package/src/ui/github-user.ts
CHANGED
|
@@ -13,7 +13,9 @@ function githubUserFromEnv(env: NodeJS.ProcessEnv): string | undefined {
|
|
|
13
13
|
return env.GITHUB_USER?.trim() || env.GH_USER?.trim() || undefined;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
function githubUsernameCommands(
|
|
16
|
+
function githubUsernameCommands(
|
|
17
|
+
cwd: string,
|
|
18
|
+
): Array<{ file: string; args: string[]; cwd?: string }> {
|
|
17
19
|
return [
|
|
18
20
|
{ file: "gh", args: ["api", "user", "-q", ".login"] },
|
|
19
21
|
{ file: "git", args: ["config", "--global", "github.user"] },
|