hotmilk 0.1.13 → 0.1.15
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 +12 -10
- package/README.md +237 -53
- package/agents/README.md +3 -3
- package/agents/assistant.md +1 -1
- package/agents/coach.md +26 -4
- package/agents/designer.md +1 -1
- package/agents/planner.md +2 -2
- package/hotmilk.json +11 -0
- package/package.json +43 -24
- package/prompts/prompt-eval.md +241 -0
- package/prompts/tidy.md +3 -1
- package/prompts/translate.md +3 -1
- package/skills/make-docs/SKILL.md +43 -0
- package/skills/pioneer/SKILL.md +135 -131
- package/skills/pioneer/references/autoresearch-routing.md +114 -0
- package/skills/pioneer/references/chat-plan.md +50 -0
- package/skills/pioneer/references/goal-gate.md +54 -0
- package/skills/pioneer/references/graph-recon-gate.md +50 -0
- package/skills/pioneer/references/observational-memory-routing.md +90 -0
- package/skills/pioneer/references/openspec-routing.md +50 -0
- package/skills/pioneer/references/plannotator-routing.md +83 -0
- package/skills/pioneer/references/prompt-eval-gate.md +38 -0
- package/skills/pioneer/references/shazam-routing.md +58 -0
- package/skills/recommend-research/SKILL.md +94 -6
- package/skills/recommend-research/references/example-narrow.md +53 -0
- package/skills/recommend-research/references/scope-gate.md +46 -0
- package/skills/recommend-research/references/shortlist-gate.md +69 -0
- package/skills/recommend-research/references/verify-gate.md +73 -0
- package/skills/update-docs/SKILL.md +86 -0
- package/skills/update-docs/references/doc-inventory.md +69 -0
- package/skills/update-docs/references/drift-verification.md +59 -0
- package/src/bootstrap/btw.ts +49 -0
- package/src/bootstrap/context-stack.ts +71 -14
- package/src/bootstrap/dashboard-settings.ts +16 -2
- package/src/bootstrap/dashboard.ts +85 -10
- package/src/bootstrap/defaults.ts +26 -0
- package/src/bootstrap/extensions.ts +10 -1
- package/src/bootstrap/global-extension-sources.ts +43 -4
- package/src/bootstrap/graph.ts +12 -0
- package/src/bootstrap/project-trust.ts +55 -0
- package/src/bootstrap/resolve-bundled.ts +28 -4
- package/src/bootstrap/session.ts +61 -1
- package/src/bootstrap/subagents-doctor.ts +12 -0
- package/src/config/bundled-extensions.ts +42 -0
- package/src/config/bundled-package-registry.ts +3 -0
- package/src/config/hotmilk.ts +76 -1
- package/src/config/mcp.ts +45 -12
- package/src/config/resolve.ts +21 -0
- package/src/config/runtime.ts +6 -0
- package/src/controller/input.ts +15 -0
- package/src/controller/mode.ts +32 -0
- package/src/index.ts +5 -0
- package/src/ui/footer.ts +63 -1
- package/src/ui/github-user.ts +52 -4
- package/src/ui/session-logo.ts +31 -3
- package/themes/monokai.json +1 -1
- package/skills/empirical-prompt-tuning/SKILL.md +0 -232
|
@@ -73,12 +73,30 @@ export const BUNDLED_EXTENSION_DEFINITIONS = [
|
|
|
73
73
|
module: "graphify-pi/extensions/graphify.ts",
|
|
74
74
|
group: "Agent tools",
|
|
75
75
|
},
|
|
76
|
+
{
|
|
77
|
+
id: "shazam",
|
|
78
|
+
package: { packageName: "pi-shazam" },
|
|
79
|
+
module: "pi-shazam/dist/index.js",
|
|
80
|
+
group: "Agent tools",
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
id: "prompt-template-model",
|
|
84
|
+
package: { packageName: "pi-prompt-template-model" },
|
|
85
|
+
module: "pi-prompt-template-model/index.ts",
|
|
86
|
+
group: "Agent tools",
|
|
87
|
+
},
|
|
76
88
|
{
|
|
77
89
|
id: "subagents",
|
|
78
90
|
package: { packageName: "pi-subagents" },
|
|
79
91
|
module: "pi-subagents/src/extension/index.ts",
|
|
80
92
|
group: "Agent tools",
|
|
81
93
|
},
|
|
94
|
+
{
|
|
95
|
+
id: "pi-actors",
|
|
96
|
+
package: { packageName: "@llblab/pi-actors" },
|
|
97
|
+
module: "@llblab/pi-actors/index.ts",
|
|
98
|
+
group: "Agent tools",
|
|
99
|
+
},
|
|
82
100
|
{
|
|
83
101
|
id: "goal",
|
|
84
102
|
package: { packageName: "pi-goal" },
|
|
@@ -116,6 +134,18 @@ export const BUNDLED_EXTENSION_DEFINITIONS = [
|
|
|
116
134
|
group: "Context & performance",
|
|
117
135
|
loadPhase: "context-stack",
|
|
118
136
|
},
|
|
137
|
+
{
|
|
138
|
+
id: "observational-memory",
|
|
139
|
+
package: { packageName: "pi-observational-memory" },
|
|
140
|
+
module: "pi-observational-memory/src/index.ts",
|
|
141
|
+
group: "Context & performance",
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
id: "supi-context",
|
|
145
|
+
package: { packageName: "@mrclrchtr/supi-context" },
|
|
146
|
+
module: "@mrclrchtr/supi-context/src/extension.ts",
|
|
147
|
+
group: "Context & performance",
|
|
148
|
+
},
|
|
119
149
|
{
|
|
120
150
|
id: "mcp-adapter",
|
|
121
151
|
package: { packageName: "pi-mcp-adapter" },
|
|
@@ -128,6 +158,12 @@ export const BUNDLED_EXTENSION_DEFINITIONS = [
|
|
|
128
158
|
module: "@tomxprime/planning-with-files/extensions/planning-with-files/index.ts",
|
|
129
159
|
group: "Workflow",
|
|
130
160
|
},
|
|
161
|
+
{
|
|
162
|
+
id: "plannotator",
|
|
163
|
+
package: { packageName: "@plannotator/pi-extension" },
|
|
164
|
+
module: "@plannotator/pi-extension/index.ts",
|
|
165
|
+
group: "Workflow",
|
|
166
|
+
},
|
|
131
167
|
{
|
|
132
168
|
id: "caveman",
|
|
133
169
|
package: { packageName: "pi-caveman" },
|
|
@@ -140,6 +176,12 @@ export const BUNDLED_EXTENSION_DEFINITIONS = [
|
|
|
140
176
|
module: "pi-red-green/dist/index.js",
|
|
141
177
|
group: "Workflow",
|
|
142
178
|
},
|
|
179
|
+
{
|
|
180
|
+
id: "autoresearch",
|
|
181
|
+
package: { packageName: "pi-autoresearch" },
|
|
182
|
+
module: "pi-autoresearch/extensions/pi-autoresearch/index.ts",
|
|
183
|
+
group: "Experiments",
|
|
184
|
+
},
|
|
143
185
|
{
|
|
144
186
|
id: "agent-dashboard",
|
|
145
187
|
package: {
|
|
@@ -8,13 +8,16 @@ export type { BundledPackageSpec };
|
|
|
8
8
|
|
|
9
9
|
export { BUNDLED_EXTENSION_PACKAGES };
|
|
10
10
|
|
|
11
|
+
/** Canonical npm package name for hotmilk itself. */
|
|
11
12
|
export const HOTMILK_PACKAGE_NAME = "hotmilk";
|
|
12
13
|
|
|
14
|
+
/** Return all package names (primary + aliases) that satisfy a bundled extension id. */
|
|
13
15
|
export function packageNamesForBundledExtension(id: BundledExtensionId): readonly string[] {
|
|
14
16
|
const spec = BUNDLED_EXTENSION_PACKAGES[id];
|
|
15
17
|
return spec.aliases ? [spec.packageName, ...spec.aliases] : [spec.packageName];
|
|
16
18
|
}
|
|
17
19
|
|
|
20
|
+
/** Return the installed package name that provides a bundled id globally, if any. */
|
|
18
21
|
export function detectGlobalProviderForBundledExtension(
|
|
19
22
|
id: BundledExtensionId,
|
|
20
23
|
installedPackageNames: ReadonlySet<string>,
|
package/src/config/hotmilk.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hotmilk user configuration loading and defaults.
|
|
3
|
+
*
|
|
4
|
+
* Reads/writes `~/.pi/agent/hotmilk.json` and derives the in-memory default
|
|
5
|
+
* config from the bundled `hotmilk.json` template.
|
|
6
|
+
*/
|
|
7
|
+
|
|
1
8
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
9
|
import { homedir } from "node:os";
|
|
3
10
|
import { dirname, join } from "node:path";
|
|
@@ -16,11 +23,16 @@ function readBundledHotmilkTemplate(): HotmilkConfig {
|
|
|
16
23
|
return JSON.parse(readFileSync(BUNDLED_TEMPLATE_PATH, "utf8")) as HotmilkConfig;
|
|
17
24
|
}
|
|
18
25
|
|
|
26
|
+
function isProjectTrustMode(value: unknown): value is ProjectTrustMode {
|
|
27
|
+
return value === "delegate" || value === "prompt" || value === "always" || value === "never";
|
|
28
|
+
}
|
|
29
|
+
|
|
19
30
|
function buildDefaultConfigFromTemplate(template: HotmilkConfig): {
|
|
20
31
|
extensions: Record<BundledExtensionId, boolean>;
|
|
21
32
|
graph: ResolvedGraphSettings;
|
|
22
33
|
defaults: ResolvedDefaults;
|
|
23
34
|
mcp: ResolvedMcpSettings;
|
|
35
|
+
projectTrust: ResolvedProjectTrust;
|
|
24
36
|
} {
|
|
25
37
|
const extensions = {} as Record<BundledExtensionId, boolean>;
|
|
26
38
|
for (const id of BUNDLED_EXTENSION_IDS) {
|
|
@@ -33,6 +45,7 @@ function buildDefaultConfigFromTemplate(template: HotmilkConfig): {
|
|
|
33
45
|
|
|
34
46
|
const language = template.defaults?.language?.trim();
|
|
35
47
|
const persona = template.defaults?.persona;
|
|
48
|
+
const projectTrustMode = template.projectTrust?.mode;
|
|
36
49
|
|
|
37
50
|
return {
|
|
38
51
|
extensions,
|
|
@@ -47,16 +60,33 @@ function buildDefaultConfigFromTemplate(template: HotmilkConfig): {
|
|
|
47
60
|
mcp: {
|
|
48
61
|
seedOnStart: template.mcp?.seedOnStart ?? false,
|
|
49
62
|
},
|
|
63
|
+
projectTrust: {
|
|
64
|
+
mode: isProjectTrustMode(projectTrustMode) ? projectTrustMode : "delegate",
|
|
65
|
+
remember: template.projectTrust?.remember ?? false,
|
|
66
|
+
},
|
|
50
67
|
};
|
|
51
68
|
}
|
|
52
69
|
|
|
70
|
+
/** Filename used for hotmilk config under the config root. */
|
|
53
71
|
export const CONFIG_FILENAME = "hotmilk.json";
|
|
72
|
+
/** Human-readable label for the agent-level config path. */
|
|
54
73
|
export const AGENT_HOTMILK_CONFIG_LABEL = "~/.pi/agent/hotmilk.json";
|
|
55
74
|
|
|
56
75
|
export { BUNDLED_EXTENSION_IDS, type BundledExtensionId } from "./bundled-extensions.ts";
|
|
57
76
|
|
|
77
|
+
/** Supported persona modes. */
|
|
58
78
|
export type PersonaMode = "gentleman" | "neutral";
|
|
59
79
|
|
|
80
|
+
/** Supported project-trust decision modes. */
|
|
81
|
+
export type ProjectTrustMode = "delegate" | "prompt" | "always" | "never";
|
|
82
|
+
|
|
83
|
+
/** Resolved project-trust settings. */
|
|
84
|
+
export type ResolvedProjectTrust = {
|
|
85
|
+
mode: ProjectTrustMode;
|
|
86
|
+
remember: boolean;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/** User-editable hotmilk config schema. */
|
|
60
90
|
export type HotmilkConfig = {
|
|
61
91
|
extensions?: Partial<Record<BundledExtensionId, boolean>>;
|
|
62
92
|
graph?: {
|
|
@@ -70,6 +100,10 @@ export type HotmilkConfig = {
|
|
|
70
100
|
mcp?: {
|
|
71
101
|
seedOnStart?: boolean;
|
|
72
102
|
};
|
|
103
|
+
projectTrust?: {
|
|
104
|
+
mode?: ProjectTrustMode;
|
|
105
|
+
remember?: boolean;
|
|
106
|
+
};
|
|
73
107
|
};
|
|
74
108
|
|
|
75
109
|
type ConfigPathResult = {
|
|
@@ -85,23 +119,36 @@ type SeedHotmilkConfigResult = ConfigPathResult & {
|
|
|
85
119
|
seeded: boolean;
|
|
86
120
|
};
|
|
87
121
|
|
|
122
|
+
/** Resolved graphify-related settings. */
|
|
88
123
|
export type ResolvedGraphSettings = {
|
|
89
124
|
warnOnStale: boolean;
|
|
90
125
|
autoSuggestUpdate: boolean;
|
|
91
126
|
};
|
|
92
127
|
|
|
128
|
+
/** Resolved default persona/language settings. */
|
|
93
129
|
export type ResolvedDefaults = {
|
|
94
130
|
language?: string;
|
|
95
131
|
persona: PersonaMode;
|
|
96
132
|
};
|
|
97
133
|
|
|
134
|
+
/** Resolved MCP bootstrap settings. */
|
|
98
135
|
export type ResolvedMcpSettings = {
|
|
99
136
|
seedOnStart: boolean;
|
|
100
137
|
};
|
|
101
138
|
|
|
102
|
-
/**
|
|
139
|
+
/**
|
|
140
|
+
* In-memory default config derived from the bundled `hotmilk.json` template.
|
|
141
|
+
*
|
|
142
|
+
* This is the single source of truth for bundled defaults.
|
|
143
|
+
*/
|
|
103
144
|
export const DEFAULT_HOTMILK_CONFIG = buildDefaultConfigFromTemplate(readBundledHotmilkTemplate());
|
|
104
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Resolve the directory that holds `hotmilk.json`.
|
|
148
|
+
*
|
|
149
|
+
* @param configRoot - explicit override
|
|
150
|
+
* @returns config root directory
|
|
151
|
+
*/
|
|
105
152
|
export function resolveHotmilkConfigRoot(configRoot?: string): string {
|
|
106
153
|
if (configRoot) {
|
|
107
154
|
return configRoot;
|
|
@@ -112,6 +159,11 @@ export function resolveHotmilkConfigRoot(configRoot?: string): string {
|
|
|
112
159
|
return join(homedir(), ".pi", "agent");
|
|
113
160
|
}
|
|
114
161
|
|
|
162
|
+
/**
|
|
163
|
+
* Resolve the full path to `hotmilk.json`.
|
|
164
|
+
*
|
|
165
|
+
* @param configRoot - explicit config root override
|
|
166
|
+
*/
|
|
115
167
|
export function getHotmilkConfigPath(configRoot?: string): string {
|
|
116
168
|
return join(resolveHotmilkConfigRoot(configRoot), CONFIG_FILENAME);
|
|
117
169
|
}
|
|
@@ -129,6 +181,12 @@ function writeHotmilkConfigFile(path: string, config: HotmilkConfig): void {
|
|
|
129
181
|
writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
130
182
|
}
|
|
131
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Copy the bundled `hotmilk.json` template to the agent config directory.
|
|
186
|
+
*
|
|
187
|
+
* @param configRoot - explicit config root override
|
|
188
|
+
* @returns seed result, with `error` set on filesystem failure
|
|
189
|
+
*/
|
|
132
190
|
export function seedHotmilkConfigIfMissing(configRoot?: string): SeedHotmilkConfigResult {
|
|
133
191
|
const configPath = getHotmilkConfigPath(configRoot);
|
|
134
192
|
if (existsSync(configPath)) {
|
|
@@ -139,6 +197,11 @@ export function seedHotmilkConfigIfMissing(configRoot?: string): SeedHotmilkConf
|
|
|
139
197
|
return { seeded: true, path: configPath };
|
|
140
198
|
}
|
|
141
199
|
|
|
200
|
+
/**
|
|
201
|
+
* Load hotmilk config from disk, falling back to {@link DEFAULT_HOTMILK_CONFIG}.
|
|
202
|
+
*
|
|
203
|
+
* @param configRoot - explicit config root override
|
|
204
|
+
*/
|
|
142
205
|
export function loadHotmilkConfig(configRoot?: string): LoadedHotmilkConfig {
|
|
143
206
|
const configPath = getHotmilkConfigPath(configRoot);
|
|
144
207
|
if (!existsSync(configPath)) {
|
|
@@ -153,6 +216,12 @@ export function loadHotmilkConfig(configRoot?: string): LoadedHotmilkConfig {
|
|
|
153
216
|
}
|
|
154
217
|
}
|
|
155
218
|
|
|
219
|
+
/**
|
|
220
|
+
* Persist hotmilk config to disk.
|
|
221
|
+
*
|
|
222
|
+
* @param config - config object to write
|
|
223
|
+
* @param configRoot - explicit config root override
|
|
224
|
+
*/
|
|
156
225
|
export function saveHotmilkConfig(config: HotmilkConfig, configRoot?: string): ConfigPathResult {
|
|
157
226
|
const configPath = getHotmilkConfigPath(configRoot);
|
|
158
227
|
try {
|
|
@@ -163,6 +232,11 @@ export function saveHotmilkConfig(config: HotmilkConfig, configRoot?: string): C
|
|
|
163
232
|
}
|
|
164
233
|
}
|
|
165
234
|
|
|
235
|
+
/**
|
|
236
|
+
* Load and resolve bundled-extension toggle state.
|
|
237
|
+
*
|
|
238
|
+
* @param configRoot - explicit config root override
|
|
239
|
+
*/
|
|
166
240
|
export function loadBundledExtensionToggles(
|
|
167
241
|
configRoot?: string,
|
|
168
242
|
): Record<BundledExtensionId, boolean> {
|
|
@@ -174,4 +248,5 @@ export {
|
|
|
174
248
|
resolveDefaults,
|
|
175
249
|
resolveGraphSettings,
|
|
176
250
|
resolveMcpSettings,
|
|
251
|
+
resolveProjectTrust,
|
|
177
252
|
} from "./resolve.ts";
|
package/src/config/mcp.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP config seeding and legacy context-mode cleanup.
|
|
3
|
+
*
|
|
4
|
+
* Seeds the agent `mcp.json` from the bundled template and prunes duplicate
|
|
5
|
+
* `context-mode` MCP server entries because context-mode exposes `ctx_*`
|
|
6
|
+
* through its own extension bridge.
|
|
7
|
+
*/
|
|
8
|
+
|
|
1
9
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
2
10
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
11
|
import { dirname, join } from "node:path";
|
|
@@ -11,6 +19,11 @@ type AgentMcpFile = {
|
|
|
11
19
|
mcpServers?: Record<string, unknown>;
|
|
12
20
|
};
|
|
13
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Copy the bundled `mcp.json` template into the agent directory when absent.
|
|
24
|
+
*
|
|
25
|
+
* @returns seed result
|
|
26
|
+
*/
|
|
14
27
|
export function seedAgentMcpJsonIfMissing(): { seeded: boolean; path: string } {
|
|
15
28
|
const agentMcpPath = join(getAgentDir(), "mcp.json");
|
|
16
29
|
if (existsSync(agentMcpPath)) {
|
|
@@ -25,29 +38,49 @@ export function seedAgentMcpJsonIfMissing(): { seeded: boolean; path: string } {
|
|
|
25
38
|
}
|
|
26
39
|
|
|
27
40
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
41
|
+
* Remove the legacy `context-mode` MCP server entry from a given `mcp.json`.
|
|
42
|
+
*
|
|
43
|
+
* Context-mode exposes `ctx_*` through its own extension bridge, so a seeded
|
|
44
|
+
* MCP server for it would duplicate functionality.
|
|
45
|
+
*
|
|
46
|
+
* @param mcpJsonPath - path to the MCP config file
|
|
47
|
+
* @returns prune result, with `error` set on filesystem failure
|
|
30
48
|
*/
|
|
31
|
-
export function pruneContextModeFromMcpJsonAt(mcpJsonPath: string):
|
|
49
|
+
export function pruneContextModeFromMcpJsonAt(mcpJsonPath: string): {
|
|
50
|
+
pruned: boolean;
|
|
51
|
+
path: string;
|
|
52
|
+
error?: string;
|
|
53
|
+
} {
|
|
32
54
|
if (!existsSync(mcpJsonPath)) {
|
|
33
|
-
return false;
|
|
55
|
+
return { pruned: false, path: mcpJsonPath };
|
|
34
56
|
}
|
|
35
57
|
|
|
36
58
|
try {
|
|
37
59
|
const parsed = JSON.parse(readFileSync(mcpJsonPath, "utf8")) as AgentMcpFile;
|
|
38
60
|
if (!parsed.mcpServers?.[CONTEXT_MODE_MCP_SERVER_ID]) {
|
|
39
|
-
return false;
|
|
61
|
+
return { pruned: false, path: mcpJsonPath };
|
|
40
62
|
}
|
|
41
63
|
delete parsed.mcpServers[CONTEXT_MODE_MCP_SERVER_ID];
|
|
42
64
|
writeFileSync(mcpJsonPath, `${JSON.stringify(parsed, null, 2)}\n`, "utf8");
|
|
43
|
-
return true;
|
|
44
|
-
} catch {
|
|
45
|
-
return
|
|
65
|
+
return { pruned: true, path: mcpJsonPath };
|
|
66
|
+
} catch (error) {
|
|
67
|
+
return {
|
|
68
|
+
pruned: false,
|
|
69
|
+
path: mcpJsonPath,
|
|
70
|
+
error: error instanceof Error ? error.message : String(error),
|
|
71
|
+
};
|
|
46
72
|
}
|
|
47
73
|
}
|
|
48
74
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
75
|
+
/**
|
|
76
|
+
* Prune legacy `context-mode` MCP server entry from the agent `mcp.json`.
|
|
77
|
+
*
|
|
78
|
+
* @returns prune result
|
|
79
|
+
*/
|
|
80
|
+
export function pruneContextModeMcpServerFromAgentConfig(): {
|
|
81
|
+
pruned: boolean;
|
|
82
|
+
path: string;
|
|
83
|
+
error?: string;
|
|
84
|
+
} {
|
|
85
|
+
return pruneContextModeFromMcpJsonAt(join(getAgentDir(), "mcp.json"));
|
|
53
86
|
}
|
package/src/config/resolve.ts
CHANGED
|
@@ -4,15 +4,18 @@ import {
|
|
|
4
4
|
type BundledExtensionId,
|
|
5
5
|
type HotmilkConfig,
|
|
6
6
|
type PersonaMode,
|
|
7
|
+
type ProjectTrustMode,
|
|
7
8
|
type ResolvedDefaults,
|
|
8
9
|
type ResolvedGraphSettings,
|
|
9
10
|
type ResolvedMcpSettings,
|
|
11
|
+
type ResolvedProjectTrust,
|
|
10
12
|
} from "./hotmilk.ts";
|
|
11
13
|
|
|
12
14
|
function isPersonaMode(value: unknown): value is PersonaMode {
|
|
13
15
|
return value === "neutral" || value === "gentleman";
|
|
14
16
|
}
|
|
15
17
|
|
|
18
|
+
/** Resolve final bundled-extension toggles by overlaying user config on bundled defaults. */
|
|
16
19
|
export function resolveBundledExtensionToggles(
|
|
17
20
|
config: HotmilkConfig,
|
|
18
21
|
): Record<BundledExtensionId, boolean> {
|
|
@@ -24,6 +27,7 @@ export function resolveBundledExtensionToggles(
|
|
|
24
27
|
) as Record<BundledExtensionId, boolean>;
|
|
25
28
|
}
|
|
26
29
|
|
|
30
|
+
/** Resolve graphify settings from user config with bundled defaults fallback. */
|
|
27
31
|
export function resolveGraphSettings(config: HotmilkConfig): ResolvedGraphSettings {
|
|
28
32
|
return {
|
|
29
33
|
warnOnStale: config.graph?.warnOnStale ?? DEFAULT_HOTMILK_CONFIG.graph.warnOnStale,
|
|
@@ -32,6 +36,7 @@ export function resolveGraphSettings(config: HotmilkConfig): ResolvedGraphSettin
|
|
|
32
36
|
};
|
|
33
37
|
}
|
|
34
38
|
|
|
39
|
+
/** Resolve default runtime settings (language, persona) from user config. */
|
|
35
40
|
export function resolveDefaults(config: HotmilkConfig): ResolvedDefaults {
|
|
36
41
|
const language = config.defaults?.language?.trim();
|
|
37
42
|
const persona = config.defaults?.persona;
|
|
@@ -41,8 +46,24 @@ export function resolveDefaults(config: HotmilkConfig): ResolvedDefaults {
|
|
|
41
46
|
};
|
|
42
47
|
}
|
|
43
48
|
|
|
49
|
+
/** Resolve MCP settings from user config with bundled defaults fallback. */
|
|
44
50
|
export function resolveMcpSettings(config: HotmilkConfig): ResolvedMcpSettings {
|
|
45
51
|
return {
|
|
46
52
|
seedOnStart: config.mcp?.seedOnStart ?? DEFAULT_HOTMILK_CONFIG.mcp.seedOnStart,
|
|
47
53
|
};
|
|
48
54
|
}
|
|
55
|
+
|
|
56
|
+
function isProjectTrustMode(value: unknown): value is ProjectTrustMode {
|
|
57
|
+
return value === "delegate" || value === "prompt" || value === "always" || value === "never";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Resolve project-trust settings from user config with bundled defaults fallback. */
|
|
61
|
+
export function resolveProjectTrust(config: HotmilkConfig): ResolvedProjectTrust {
|
|
62
|
+
const mode = config.projectTrust?.mode;
|
|
63
|
+
const remember = config.projectTrust?.remember;
|
|
64
|
+
return {
|
|
65
|
+
mode: isProjectTrustMode(mode) ? mode : DEFAULT_HOTMILK_CONFIG.projectTrust.mode,
|
|
66
|
+
remember:
|
|
67
|
+
typeof remember === "boolean" ? remember : DEFAULT_HOTMILK_CONFIG.projectTrust.remember,
|
|
68
|
+
};
|
|
69
|
+
}
|
package/src/config/runtime.ts
CHANGED
|
@@ -6,14 +6,17 @@ import {
|
|
|
6
6
|
type ResolvedDefaults,
|
|
7
7
|
type ResolvedGraphSettings,
|
|
8
8
|
type ResolvedMcpSettings,
|
|
9
|
+
type ResolvedProjectTrust,
|
|
9
10
|
} from "./hotmilk.ts";
|
|
10
11
|
import {
|
|
11
12
|
resolveBundledExtensionToggles,
|
|
12
13
|
resolveDefaults,
|
|
13
14
|
resolveGraphSettings,
|
|
14
15
|
resolveMcpSettings,
|
|
16
|
+
resolveProjectTrust,
|
|
15
17
|
} from "./resolve.ts";
|
|
16
18
|
|
|
19
|
+
/** Aggregated hotmilk configuration and resolved runtime settings. */
|
|
17
20
|
export type HotmilkRuntime = {
|
|
18
21
|
config: HotmilkConfig;
|
|
19
22
|
configPath: string;
|
|
@@ -24,8 +27,10 @@ export type HotmilkRuntime = {
|
|
|
24
27
|
defaults: ResolvedDefaults;
|
|
25
28
|
graph: ResolvedGraphSettings;
|
|
26
29
|
mcp: ResolvedMcpSettings;
|
|
30
|
+
projectTrust: ResolvedProjectTrust;
|
|
27
31
|
};
|
|
28
32
|
|
|
33
|
+
/** Load hotmilk config and resolve all runtime settings into a single object. */
|
|
29
34
|
export function createHotmilkRuntime(configRoot?: string): HotmilkRuntime {
|
|
30
35
|
const loaded = loadHotmilkConfig(configRoot);
|
|
31
36
|
return {
|
|
@@ -37,5 +42,6 @@ export function createHotmilkRuntime(configRoot?: string): HotmilkRuntime {
|
|
|
37
42
|
defaults: resolveDefaults(loaded.config),
|
|
38
43
|
graph: resolveGraphSettings(loaded.config),
|
|
39
44
|
mcp: resolveMcpSettings(loaded.config),
|
|
45
|
+
projectTrust: resolveProjectTrust(loaded.config),
|
|
40
46
|
};
|
|
41
47
|
}
|
package/src/controller/input.ts
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { openModeSettingsModal } from "./mode.ts";
|
|
3
3
|
|
|
4
|
+
/** Prefix for interrupt commands. */
|
|
4
5
|
const INTERRUPT_PREFIX = "/interrupt ";
|
|
5
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Handle /stop command input.
|
|
9
|
+
*
|
|
10
|
+
* @param ctx - extension context
|
|
11
|
+
*/
|
|
6
12
|
function handleStopInput(ctx: ExtensionContext): void {
|
|
7
13
|
if (!ctx.isIdle()) {
|
|
8
14
|
ctx.abort();
|
|
@@ -12,6 +18,13 @@ function handleStopInput(ctx: ExtensionContext): void {
|
|
|
12
18
|
ctx.ui.notify("No running work to stop.", "info");
|
|
13
19
|
}
|
|
14
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Handle /interrupt command input.
|
|
23
|
+
*
|
|
24
|
+
* @param interruptPrompt - interrupt prompt text
|
|
25
|
+
* @param pi - Pi extension API
|
|
26
|
+
* @param ctx - extension context
|
|
27
|
+
*/
|
|
15
28
|
function handleInterruptInput(
|
|
16
29
|
interruptPrompt: string,
|
|
17
30
|
pi: ExtensionAPI,
|
|
@@ -28,6 +41,7 @@ function handleInterruptInput(
|
|
|
28
41
|
ctx.ui.notify("Interrupt prompt sent.", "info");
|
|
29
42
|
}
|
|
30
43
|
|
|
44
|
+
/** Route typed slash commands (`/stop`, `/interrupt`, `/mode`) to their handlers. */
|
|
31
45
|
export function routeInputCommand(text: string, pi: ExtensionAPI, ctx: ExtensionContext): void {
|
|
32
46
|
const normalizedText = text.trim();
|
|
33
47
|
if (normalizedText === "/stop") {
|
|
@@ -41,6 +55,7 @@ export function routeInputCommand(text: string, pi: ExtensionAPI, ctx: Extension
|
|
|
41
55
|
}
|
|
42
56
|
}
|
|
43
57
|
|
|
58
|
+
/** Register `/stop`, `/interrupt`, and `/mode` as Pi commands. */
|
|
44
59
|
export function registerInputCommands(pi: ExtensionAPI): void {
|
|
45
60
|
pi.registerCommand("stop", {
|
|
46
61
|
description: "Stop current running work.",
|
package/src/controller/mode.ts
CHANGED
|
@@ -9,10 +9,22 @@ import {
|
|
|
9
9
|
saveHotmilkConfig,
|
|
10
10
|
} from "../config/hotmilk.ts";
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Format toggle state as "on" or "off".
|
|
14
|
+
*
|
|
15
|
+
* @param enabled - toggle state
|
|
16
|
+
* @returns formatted state
|
|
17
|
+
*/
|
|
12
18
|
function formatToggleState(enabled: boolean): "on" | "off" {
|
|
13
19
|
return enabled ? "on" : "off";
|
|
14
20
|
}
|
|
15
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Format toggle rows for notification display.
|
|
24
|
+
*
|
|
25
|
+
* @param toggles - extension toggle states
|
|
26
|
+
* @returns formatted toggle rows
|
|
27
|
+
*/
|
|
16
28
|
function formatToggleRows(toggles: Record<BundledExtensionId, boolean>): string {
|
|
17
29
|
return BUNDLED_EXTENSION_GROUPS.map((group) => {
|
|
18
30
|
const rows = group.ids.map((id) => ` ${id}: ${formatToggleState(toggles[id])}`);
|
|
@@ -20,6 +32,12 @@ function formatToggleRows(toggles: Record<BundledExtensionId, boolean>): string
|
|
|
20
32
|
}).join("\n\n");
|
|
21
33
|
}
|
|
22
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Create setting items for the mode settings modal.
|
|
37
|
+
*
|
|
38
|
+
* @param toggles - extension toggle states
|
|
39
|
+
* @returns setting items
|
|
40
|
+
*/
|
|
23
41
|
function createModeSettingItems(toggles: Record<BundledExtensionId, boolean>): SettingItem[] {
|
|
24
42
|
return BUNDLED_EXTENSION_GROUPS.flatMap((group) => [
|
|
25
43
|
{
|
|
@@ -36,6 +54,12 @@ function createModeSettingItems(toggles: Record<BundledExtensionId, boolean>): S
|
|
|
36
54
|
]);
|
|
37
55
|
}
|
|
38
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Notify current config state.
|
|
59
|
+
*
|
|
60
|
+
* @param ctx - extension context
|
|
61
|
+
* @param toggles - extension toggle states
|
|
62
|
+
*/
|
|
39
63
|
function notifyCurrentConfig(
|
|
40
64
|
ctx: ExtensionContext,
|
|
41
65
|
toggles: Record<BundledExtensionId, boolean>,
|
|
@@ -43,6 +67,13 @@ function notifyCurrentConfig(
|
|
|
43
67
|
ctx.ui.notify(`${AGENT_HOTMILK_CONFIG_LABEL}\n${formatToggleRows(toggles)}`, "info");
|
|
44
68
|
}
|
|
45
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Save extension toggle state to config.
|
|
72
|
+
*
|
|
73
|
+
* @param ctx - extension context
|
|
74
|
+
* @param extensionId - extension ID
|
|
75
|
+
* @param enabled - toggle state
|
|
76
|
+
*/
|
|
46
77
|
function saveExtensionToggle(
|
|
47
78
|
ctx: ExtensionContext,
|
|
48
79
|
extensionId: BundledExtensionId,
|
|
@@ -58,6 +89,7 @@ function saveExtensionToggle(
|
|
|
58
89
|
}
|
|
59
90
|
}
|
|
60
91
|
|
|
92
|
+
/** Open the interactive TUI modal for toggling bundled extensions. */
|
|
61
93
|
export async function openModeSettingsModal(ctx: ExtensionContext): Promise<void> {
|
|
62
94
|
const toggles = loadBundledExtensionToggles();
|
|
63
95
|
const items = createModeSettingItems(toggles);
|
package/src/index.ts
CHANGED
|
@@ -12,11 +12,15 @@ 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
14
|
import { installHotmilkCtxSearchCapture } from "./bootstrap/btw.ts";
|
|
15
|
+
import { registerProjectTrustHandlers } from "./bootstrap/project-trust.ts";
|
|
15
16
|
import { registerSubagentsDoctorCommand } from "./bootstrap/subagents-doctor.ts";
|
|
16
17
|
|
|
18
|
+
/** Main Pi extension entry point: register all hotmilk handlers and bundled extensions. */
|
|
17
19
|
export default async function registerHotmilk(pi: ExtensionAPI): Promise<void> {
|
|
18
20
|
const runtime = createHotmilkRuntime();
|
|
19
21
|
|
|
22
|
+
registerProjectTrustHandlers(pi, runtime.projectTrust);
|
|
23
|
+
|
|
20
24
|
// Register before bundled imports so session_start handlers exist when bindExtensions emits.
|
|
21
25
|
registerHotmilkSessionLogo(pi);
|
|
22
26
|
installHotmilkCtxSearchCapture(pi);
|
|
@@ -25,6 +29,7 @@ export default async function registerHotmilk(pi: ExtensionAPI): Promise<void> {
|
|
|
25
29
|
|
|
26
30
|
const bundled = await registerBundledExtensions(pi, runtime.extensionToggles, {
|
|
27
31
|
cwd: process.cwd(),
|
|
32
|
+
includeProjectSettings: false,
|
|
28
33
|
});
|
|
29
34
|
if (runtime.extensionToggles.subagents === true) {
|
|
30
35
|
await registerSubagentsDoctorCommand(pi);
|