@heart-of-gold/toolkit 0.1.8 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -5
- package/extensions/pi/brainstorm.ts +9 -53
- package/extensions/pi/plan.ts +5 -76
- package/extensions/pi/work.ts +25 -66
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -36,12 +36,13 @@ pi install npm:@heart-of-gold/toolkit
|
|
|
36
36
|
Pi also discovers skills from the shared `~/.agents/skills/` location, so installs done with the OpenCode target are usable from Pi too.
|
|
37
37
|
|
|
38
38
|
When installed as a Pi package, Heart of Gold exposes Pi-native extension commands for the flagship workflows:
|
|
39
|
-
- `/deep-thought-brainstorm` —
|
|
40
|
-
- `/deep-thought-plan` — planning
|
|
41
|
-
- `/marvin-work` —
|
|
42
|
-
- `/marvin-execute` — leave planning mode and begin execution
|
|
39
|
+
- `/deep-thought-brainstorm` — start a brainstorm (collaborative discovery)
|
|
40
|
+
- `/deep-thought-plan` — start planning (research and produce a plan document)
|
|
41
|
+
- `/marvin-work` — start executing a plan (with always-on safety guardrails)
|
|
43
42
|
|
|
44
|
-
|
|
43
|
+
The skills themselves enforce their own boundaries (read-only for brainstorm/plan, safe commands for work) via `allowed-tools` and prompt constraints — no manual mode switching needed.
|
|
44
|
+
|
|
45
|
+
The work extension also provides always-on guardrails that protect `.env`, `.git/`, and `node_modules/` from edits, block `git add .` and destructive `rm`, and require confirmation for `git push` / `npm publish`.
|
|
45
46
|
|
|
46
47
|
### List available skills
|
|
47
48
|
```bash
|
|
@@ -1,67 +1,23 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
|
|
3
|
-
function sendPrompt(pi: ExtensionAPI, prompt: string, ctx: Parameters<NonNullable<Parameters<ExtensionAPI["registerCommand"]>[1]["handler"]>>[1]) {
|
|
4
|
-
if (ctx.isIdle()) {
|
|
5
|
-
pi.sendUserMessage(prompt);
|
|
6
|
-
} else {
|
|
7
|
-
pi.sendUserMessage(prompt, { deliverAs: "followUp" });
|
|
8
|
-
ctx.ui.notify("Brainstorm prompt queued as follow-up", "info");
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
|
|
12
3
|
export default function brainstormExtension(pi: ExtensionAPI) {
|
|
13
4
|
pi.registerCommand("deep-thought-brainstorm", {
|
|
14
|
-
description: "
|
|
5
|
+
description: "Start a brainstorm — collaborative discovery before planning",
|
|
15
6
|
handler: async (args, ctx) => {
|
|
16
|
-
|
|
17
|
-
ctx.ui.notify("/deep-thought-brainstorm requires interactive mode", "warning");
|
|
18
|
-
return;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const topic = args.trim() || (await ctx.ui.editor("Brainstorm topic", ""))?.trim();
|
|
7
|
+
const topic = args.trim() || (ctx.hasUI ? (await ctx.ui.editor("Brainstorm topic", ""))?.trim() : undefined);
|
|
22
8
|
if (!topic) {
|
|
23
|
-
ctx.ui.notify("
|
|
9
|
+
ctx.ui.notify("Usage: /deep-thought-brainstorm <topic>", "info");
|
|
24
10
|
return;
|
|
25
11
|
}
|
|
26
12
|
|
|
27
|
-
const
|
|
28
|
-
"Unclear — need discovery",
|
|
29
|
-
"Partly clear — explore tradeoffs",
|
|
30
|
-
"Quite clear — validate before planning",
|
|
31
|
-
]);
|
|
32
|
-
if (!clarity) {
|
|
33
|
-
ctx.ui.notify("Cancelled", "info");
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
13
|
+
const prompt = `/skill:brainstorm ${topic}`;
|
|
36
14
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if (!goal) {
|
|
43
|
-
ctx.ui.notify("Cancelled", "info");
|
|
44
|
-
return;
|
|
15
|
+
if (ctx.isIdle()) {
|
|
16
|
+
pi.sendUserMessage(prompt);
|
|
17
|
+
} else {
|
|
18
|
+
pi.sendUserMessage(prompt, { deliverAs: "followUp" });
|
|
19
|
+
ctx.ui.notify("Brainstorm queued as follow-up", "info");
|
|
45
20
|
}
|
|
46
|
-
|
|
47
|
-
const prompt = [
|
|
48
|
-
`/skill:brainstorm ${topic}`,
|
|
49
|
-
"",
|
|
50
|
-
"Use a pi-friendly interactive flow:",
|
|
51
|
-
"- Ask one question at a time.",
|
|
52
|
-
"- Use explicit option lists whenever there are natural choices.",
|
|
53
|
-
"- Keep momentum high and avoid dumping questionnaires.",
|
|
54
|
-
"",
|
|
55
|
-
`Current clarity: ${clarity}`,
|
|
56
|
-
`Primary goal: ${goal}`,
|
|
57
|
-
].join("\n");
|
|
58
|
-
|
|
59
|
-
const theme = ctx.ui.theme;
|
|
60
|
-
ctx.ui.setStatus(
|
|
61
|
-
"deep-thought-brainstorm",
|
|
62
|
-
theme.fg("accent", "◉") + theme.fg("dim", " Brainstorm flow active"),
|
|
63
|
-
);
|
|
64
|
-
sendPrompt(pi, prompt, ctx);
|
|
65
21
|
},
|
|
66
22
|
});
|
|
67
23
|
}
|
package/extensions/pi/plan.ts
CHANGED
|
@@ -1,93 +1,22 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
|
|
3
|
-
const PLAN_SAFE_TOOLS = ["read", "bash", "grep", "find", "ls"];
|
|
4
|
-
|
|
5
3
|
export default function planExtension(pi: ExtensionAPI) {
|
|
6
|
-
let previousTools: string[] | null = null;
|
|
7
|
-
|
|
8
|
-
const applyPlanTools = () => {
|
|
9
|
-
const available = new Set(pi.getAllTools().map((tool) => tool.name));
|
|
10
|
-
const nextTools = PLAN_SAFE_TOOLS.filter((tool) => available.has(tool));
|
|
11
|
-
if (nextTools.length > 0) {
|
|
12
|
-
pi.setActiveTools(nextTools);
|
|
13
|
-
}
|
|
14
|
-
};
|
|
15
|
-
|
|
16
4
|
pi.registerCommand("deep-thought-plan", {
|
|
17
|
-
description: "
|
|
5
|
+
description: "Start planning — research and produce a plan document",
|
|
18
6
|
handler: async (args, ctx) => {
|
|
19
|
-
|
|
20
|
-
ctx.ui.notify("/deep-thought-plan requires interactive mode", "warning");
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const source = args.trim() || (await ctx.ui.editor("Plan topic or brainstorm path", ""))?.trim();
|
|
7
|
+
const source = args.trim() || (ctx.hasUI ? (await ctx.ui.editor("Plan topic or brainstorm path", ""))?.trim() : undefined);
|
|
25
8
|
if (!source) {
|
|
26
|
-
ctx.ui.notify("
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const mode = await ctx.ui.select("How should planning mode behave?", [
|
|
31
|
-
"Read-only planning (recommended)",
|
|
32
|
-
"Allow normal tools, but stay in planning mode",
|
|
33
|
-
]);
|
|
34
|
-
if (!mode) {
|
|
35
|
-
ctx.ui.notify("Cancelled", "info");
|
|
9
|
+
ctx.ui.notify("Usage: /deep-thought-plan <topic or brainstorm path>", "info");
|
|
36
10
|
return;
|
|
37
11
|
}
|
|
38
12
|
|
|
39
|
-
|
|
40
|
-
previousTools = pi.getActiveTools();
|
|
41
|
-
}
|
|
42
|
-
if (mode.startsWith("Read-only")) {
|
|
43
|
-
applyPlanTools();
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const prompt = [
|
|
47
|
-
`/skill:plan ${source}`,
|
|
48
|
-
"",
|
|
49
|
-
"Use a pi-friendly planning flow:",
|
|
50
|
-
"- Keep planning read-only unless the user explicitly exits planning mode.",
|
|
51
|
-
"- Ask one question at a time when clarification is needed.",
|
|
52
|
-
"- Use concise option lists when choosing between paths or handoffs.",
|
|
53
|
-
].join("\n");
|
|
54
|
-
|
|
55
|
-
const theme = ctx.ui.theme;
|
|
56
|
-
ctx.ui.setStatus(
|
|
57
|
-
"deep-thought-plan",
|
|
58
|
-
theme.fg("accent", "◉") + theme.fg("dim", " Plan mode active"),
|
|
59
|
-
);
|
|
60
|
-
|
|
61
|
-
if (ctx.isIdle()) {
|
|
62
|
-
pi.sendUserMessage(prompt);
|
|
63
|
-
} else {
|
|
64
|
-
pi.sendUserMessage(prompt, { deliverAs: "followUp" });
|
|
65
|
-
ctx.ui.notify("Plan prompt queued as follow-up", "info");
|
|
66
|
-
}
|
|
67
|
-
},
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
pi.registerCommand("marvin-execute", {
|
|
71
|
-
description: "Exit planning mode, restore tools, and optionally start work",
|
|
72
|
-
handler: async (args, ctx) => {
|
|
73
|
-
if (previousTools) {
|
|
74
|
-
pi.setActiveTools(previousTools);
|
|
75
|
-
previousTools = null;
|
|
76
|
-
}
|
|
77
|
-
ctx.ui.setStatus("deep-thought-plan", "");
|
|
78
|
-
|
|
79
|
-
const target = args.trim();
|
|
80
|
-
if (!target) {
|
|
81
|
-
ctx.ui.notify("Planning mode cleared. Run /marvin-work <plan-path> when ready.", "info");
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
13
|
+
const prompt = `/skill:plan ${source}`;
|
|
84
14
|
|
|
85
|
-
const prompt = `/skill:work ${target}`;
|
|
86
15
|
if (ctx.isIdle()) {
|
|
87
16
|
pi.sendUserMessage(prompt);
|
|
88
17
|
} else {
|
|
89
18
|
pi.sendUserMessage(prompt, { deliverAs: "followUp" });
|
|
90
|
-
ctx.ui.notify("
|
|
19
|
+
ctx.ui.notify("Plan queued as follow-up", "info");
|
|
91
20
|
}
|
|
92
21
|
},
|
|
93
22
|
});
|
package/extensions/pi/work.ts
CHANGED
|
@@ -5,76 +5,15 @@ const CONFIRM_COMMANDS = [/\bgit\s+push\b/i, /\bnpm\s+publish\b/i, /\bbun\s+publ
|
|
|
5
5
|
const BLOCKED_COMMANDS = [/\bgit\s+add\s+\.\b/i, /\brm\s+(-rf?|--recursive)/i];
|
|
6
6
|
|
|
7
7
|
export default function workExtension(pi: ExtensionAPI) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
pi.registerCommand("marvin-work", {
|
|
11
|
-
description: "Interactive pi-first entrypoint for the shared work skill",
|
|
12
|
-
handler: async (args, ctx) => {
|
|
13
|
-
if (!ctx.hasUI) {
|
|
14
|
-
ctx.ui.notify("/marvin-work requires interactive mode", "warning");
|
|
15
|
-
return;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const planPath = args.trim() || (await ctx.ui.editor("Plan path", ""))?.trim();
|
|
19
|
-
if (!planPath) {
|
|
20
|
-
ctx.ui.notify("Cancelled", "info");
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const runMode = await ctx.ui.select("How strict should work mode be?", [
|
|
25
|
-
"Normal guardrails (recommended)",
|
|
26
|
-
"Strict guardrails for shipping work",
|
|
27
|
-
]);
|
|
28
|
-
if (!runMode) {
|
|
29
|
-
ctx.ui.notify("Cancelled", "info");
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
workMode = true;
|
|
34
|
-
const theme = ctx.ui.theme;
|
|
35
|
-
ctx.ui.setStatus(
|
|
36
|
-
"marvin-work",
|
|
37
|
-
theme.fg("accent", "◉") + theme.fg("dim", ` Work mode active — ${runMode}`),
|
|
38
|
-
);
|
|
39
|
-
|
|
40
|
-
const prompt = [
|
|
41
|
-
`/skill:work ${planPath}`,
|
|
42
|
-
"",
|
|
43
|
-
"Use pi guardrails while executing:",
|
|
44
|
-
"- keep progress visible as tasks move from in-progress to complete",
|
|
45
|
-
"- do not use `git add .`",
|
|
46
|
-
"- confirm push/publish actions deliberately",
|
|
47
|
-
"- protect .env, .git/, and node_modules/ from accidental edits",
|
|
48
|
-
].join("\n");
|
|
49
|
-
|
|
50
|
-
if (ctx.isIdle()) {
|
|
51
|
-
pi.sendUserMessage(prompt);
|
|
52
|
-
} else {
|
|
53
|
-
pi.sendUserMessage(prompt, { deliverAs: "followUp" });
|
|
54
|
-
ctx.ui.notify("Work prompt queued as follow-up", "info");
|
|
55
|
-
}
|
|
56
|
-
},
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
pi.registerCommand("marvin-work-off", {
|
|
60
|
-
description: "Disable Heart of Gold work-mode guardrails",
|
|
61
|
-
handler: async (_args, ctx) => {
|
|
62
|
-
workMode = false;
|
|
63
|
-
ctx.ui.setStatus("marvin-work", "");
|
|
64
|
-
ctx.ui.notify("Work mode disabled", "info");
|
|
65
|
-
},
|
|
66
|
-
});
|
|
67
|
-
|
|
8
|
+
// Always-on guardrails — no mode toggle needed
|
|
68
9
|
pi.on("tool_call", async (event, ctx) => {
|
|
69
|
-
if (!workMode) return undefined;
|
|
70
|
-
|
|
71
10
|
if (event.toolName === "write" || event.toolName === "edit") {
|
|
72
11
|
const path = String(event.input.path ?? "");
|
|
73
12
|
if (PROTECTED_PATHS.some((segment) => path.includes(segment))) {
|
|
74
13
|
if (ctx.hasUI) {
|
|
75
|
-
ctx.ui.notify(
|
|
14
|
+
ctx.ui.notify(`⛔ Protected path: ${path}`, "warning");
|
|
76
15
|
}
|
|
77
|
-
return { block: true, reason: `Protected path
|
|
16
|
+
return { block: true, reason: `Protected path: ${path}` };
|
|
78
17
|
}
|
|
79
18
|
}
|
|
80
19
|
|
|
@@ -82,7 +21,7 @@ export default function workExtension(pi: ExtensionAPI) {
|
|
|
82
21
|
const command = String(event.input.command ?? "");
|
|
83
22
|
|
|
84
23
|
if (BLOCKED_COMMANDS.some((pattern) => pattern.test(command))) {
|
|
85
|
-
return { block: true, reason: `Blocked unsafe command
|
|
24
|
+
return { block: true, reason: `Blocked unsafe command: ${command}` };
|
|
86
25
|
}
|
|
87
26
|
|
|
88
27
|
if (!CONFIRM_COMMANDS.some((pattern) => pattern.test(command))) {
|
|
@@ -93,11 +32,31 @@ export default function workExtension(pi: ExtensionAPI) {
|
|
|
93
32
|
return { block: true, reason: `Interactive confirmation required for: ${command}` };
|
|
94
33
|
}
|
|
95
34
|
|
|
96
|
-
const choice = await ctx.ui.select(`Confirm
|
|
35
|
+
const choice = await ctx.ui.select(`Confirm: ${command}`, ["Allow", "Block"]);
|
|
97
36
|
if (choice !== "Allow") {
|
|
98
37
|
return { block: true, reason: `Blocked by user: ${command}` };
|
|
99
38
|
}
|
|
100
39
|
|
|
101
40
|
return undefined;
|
|
102
41
|
});
|
|
42
|
+
|
|
43
|
+
pi.registerCommand("marvin-work", {
|
|
44
|
+
description: "Start executing a plan — implement with guardrails",
|
|
45
|
+
handler: async (args, ctx) => {
|
|
46
|
+
const planPath = args.trim() || (ctx.hasUI ? (await ctx.ui.editor("Plan path", ""))?.trim() : undefined);
|
|
47
|
+
if (!planPath) {
|
|
48
|
+
ctx.ui.notify("Usage: /marvin-work <plan path>", "info");
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const prompt = `/skill:work ${planPath}`;
|
|
53
|
+
|
|
54
|
+
if (ctx.isIdle()) {
|
|
55
|
+
pi.sendUserMessage(prompt);
|
|
56
|
+
} else {
|
|
57
|
+
pi.sendUserMessage(prompt, { deliverAs: "followUp" });
|
|
58
|
+
ctx.ui.notify("Work queued as follow-up", "info");
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
});
|
|
103
62
|
}
|