@dungle-scrubs/tallow 0.8.3 → 0.8.4
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 +29 -13
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/extensions/__integration__/claude-hooks-compat.test.ts +118 -0
- package/extensions/context-fork/index.ts +1 -1
- package/extensions/context-fork/model-resolver.ts +12 -2
- package/extensions/hooks/__tests__/claude-compat.test.ts +215 -0
- package/extensions/hooks/hooks.schema.json +61 -5
- package/extensions/hooks/index.ts +299 -20
- package/extensions/slash-command-bridge/__tests__/slash-command-bridge.test.ts +32 -0
- package/extensions/slash-command-bridge/index.ts +9 -0
- package/extensions/subagent-tool/__tests__/auto-cheap-model.test.ts +50 -0
- package/extensions/subagent-tool/__tests__/model-router.test.ts +124 -6
- package/extensions/subagent-tool/model-router.ts +135 -23
- package/extensions/subagent-tool/process.ts +11 -6
- package/extensions/teams-tool/sessions/spawn.ts +1 -1
- package/extensions/wezterm-pane-control/__tests__/index.test.ts +13 -0
- package/extensions/wezterm-pane-control/index.ts +30 -6
- package/package.json +2 -2
- package/schemas/settings.schema.json +24 -0
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<h1 align="center">Tallow</h1>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
|
-
An
|
|
8
|
+
An opt-in, fully featured coding agent for your terminal. Built on <a href="https://github.com/nicobrinkkemper/pi-coding-agent">pi</a>.
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
@@ -26,22 +26,37 @@
|
|
|
26
26
|
<img src="assets/screenshot.jpg" alt="Tallow multi-agent team coordinating a docs audit" />
|
|
27
27
|
</p>
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
Tallow is opt-in by default: start minimal, then enable only what your project needs.
|
|
30
|
+
It is drop-in compatible with Claude Code projects via `.claude/` bridging.
|
|
31
|
+
Install extensions, themes, and agents in any combination.
|
|
32
|
+
This is a personal project I build in my spare time, so please be patient with
|
|
33
|
+
issue and PR response times.
|
|
31
34
|
|
|
32
35
|
## Features
|
|
33
36
|
|
|
34
|
-
- **
|
|
35
|
-
- **
|
|
36
|
-
-
|
|
37
|
-
- **
|
|
38
|
-
|
|
39
|
-
- **
|
|
40
|
-
|
|
37
|
+
- **Most valuable capabilities (non-exhaustive):**
|
|
38
|
+
- **Multi-model routing** — route work by intent/cost (`auto-cheap`, `auto-balanced`,
|
|
39
|
+
`auto-premium`) across available providers
|
|
40
|
+
- **Multi-agent teams** — coordinate specialized agents with shared task boards,
|
|
41
|
+
dependencies, messaging, and archive/resume
|
|
42
|
+
- **Context fork** — run isolated subprocess workflows with separate tools/models and
|
|
43
|
+
merge results back cleanly
|
|
44
|
+
- **Workspace rewind snapshots** — roll file changes back to earlier conversation turns
|
|
45
|
+
- **Task primitives + background execution** — explicit task lifecycle tracking and
|
|
46
|
+
non-blocking long-running work
|
|
47
|
+
- **Built-in LSP navigation** — definitions, references, hover, and workspace symbol
|
|
48
|
+
lookup
|
|
49
|
+
- **Opt-in and modular** — install only the pieces you need, skip the rest
|
|
50
|
+
- **Claude Code compatible** — `.claude/` + `.tallow/` directories are bridged so existing
|
|
51
|
+
project workflows keep working
|
|
52
|
+
- **Fully featured when you want it** — 49 bundled extensions, 34 themes, 8 slash commands,
|
|
53
|
+
and 10 specialized agents
|
|
54
|
+
- **Session naming** — auto-generated descriptive names for each session, shown in footer
|
|
55
|
+
and `--list`
|
|
41
56
|
- **Debug mode** — structured JSONL diagnostic logging with `/diag` command
|
|
42
|
-
- **Claude Code compatible** — `.claude/` directory bridging for seamless use of Tallow skills, agents, and commands in Claude Code
|
|
43
57
|
- **SDK** — embed Tallow in your own scripts and orchestrators
|
|
44
|
-
- **User-owned config** — agents and commands are installed to `~/.tallow/` where you can
|
|
58
|
+
- **User-owned config** — agents and commands are installed to `~/.tallow/` where you can
|
|
59
|
+
edit, remove, or add your own
|
|
45
60
|
|
|
46
61
|
Read the full [documentation](https://tallow.dungle-scrubs.com).
|
|
47
62
|
|
|
@@ -298,7 +313,8 @@ If it shares a name with a bundled extension, yours takes precedence.
|
|
|
298
313
|
|
|
299
314
|
- Requires Node.js 22+ (uses modern ESM features)
|
|
300
315
|
- Session persistence is local — no cloud sync
|
|
301
|
-
- The `web_fetch` extension works best with a [Firecrawl](https://firecrawl.dev) API key
|
|
316
|
+
- The `web_fetch` extension works best with a [Firecrawl](https://firecrawl.dev) API key
|
|
317
|
+
for JS-heavy pages
|
|
302
318
|
|
|
303
319
|
## Contributing
|
|
304
320
|
|
package/dist/config.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export declare const APP_NAME = "tallow";
|
|
2
|
-
export declare const TALLOW_VERSION = "0.8.
|
|
2
|
+
export declare const TALLOW_VERSION = "0.8.4";
|
|
3
3
|
export declare const CONFIG_DIR = ".tallow";
|
|
4
4
|
/** ~/.tallow (or override from ~/.config/tallow-work-dirs) — all user config, sessions, auth, extensions */
|
|
5
5
|
export declare const TALLOW_HOME: string;
|
package/dist/config.js
CHANGED
|
@@ -5,7 +5,7 @@ import { dirname, join, resolve } from "node:path";
|
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
// ─── Identity ────────────────────────────────────────────────────────────────
|
|
7
7
|
export const APP_NAME = "tallow";
|
|
8
|
-
export const TALLOW_VERSION = "0.8.
|
|
8
|
+
export const TALLOW_VERSION = "0.8.4"; // x-release-please-version
|
|
9
9
|
export const CONFIG_DIR = ".tallow";
|
|
10
10
|
// ─── Paths ───────────────────────────────────────────────────────────────────
|
|
11
11
|
/** ~/.tallow (or override from ~/.config/tallow-work-dirs) — all user config, sessions, auth, extensions */
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
2
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { loadHooksConfig } from "../hooks/index.js";
|
|
6
|
+
|
|
7
|
+
let cwd: string;
|
|
8
|
+
let homeDir: string;
|
|
9
|
+
let originalHome: string | undefined;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Writes JSON with pretty formatting, creating parent directories as needed.
|
|
13
|
+
*
|
|
14
|
+
* @param filePath - Destination file path
|
|
15
|
+
* @param value - JSON-serializable payload
|
|
16
|
+
* @returns void
|
|
17
|
+
*/
|
|
18
|
+
function writeJson(filePath: string, value: unknown): void {
|
|
19
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
20
|
+
writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
cwd = mkdtempSync(join(tmpdir(), "tallow-claude-hooks-cwd-"));
|
|
25
|
+
homeDir = mkdtempSync(join(tmpdir(), "tallow-claude-hooks-home-"));
|
|
26
|
+
originalHome = process.env.HOME;
|
|
27
|
+
process.env.HOME = homeDir;
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
if (originalHome !== undefined) {
|
|
32
|
+
process.env.HOME = originalHome;
|
|
33
|
+
} else {
|
|
34
|
+
delete process.env.HOME;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
rmSync(cwd, { recursive: true, force: true });
|
|
38
|
+
rmSync(homeDir, { recursive: true, force: true });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("Claude hooks compatibility integration", () => {
|
|
42
|
+
it("loads and translates .claude/settings.json hook events", () => {
|
|
43
|
+
writeJson(join(cwd, ".claude", "settings.json"), {
|
|
44
|
+
hooks: {
|
|
45
|
+
PreToolUse: [
|
|
46
|
+
{
|
|
47
|
+
matcher: "Bash",
|
|
48
|
+
hooks: [{ type: "command", command: "echo pre" }],
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
PostToolUseFailure: [
|
|
52
|
+
{
|
|
53
|
+
matcher: "Write",
|
|
54
|
+
hooks: [{ type: "command", command: "echo fail" }],
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const config = loadHooksConfig(cwd);
|
|
61
|
+
expect(config.tool_call).toHaveLength(1);
|
|
62
|
+
expect(config.tool_result).toHaveLength(1);
|
|
63
|
+
expect(config.tool_call[0]?.matcher).toBe("bash");
|
|
64
|
+
expect(config.tool_result[0]?.matcher).toBe("write");
|
|
65
|
+
expect(config.tool_call[0]?.hooks[0]?._claudeSource).toBe(true);
|
|
66
|
+
expect(config.tool_call[0]?.hooks[0]?._claudeEventName).toBe("PreToolUse");
|
|
67
|
+
expect(config.tool_result[0]?.hooks[0]?._claudeEventName).toBe("PostToolUseFailure");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("keeps .tallow hooks ahead of .claude hooks for matching order", () => {
|
|
71
|
+
writeJson(join(cwd, ".tallow", "hooks.json"), {
|
|
72
|
+
tool_call: [
|
|
73
|
+
{
|
|
74
|
+
matcher: "bash",
|
|
75
|
+
hooks: [{ type: "command", command: "echo tallow" }],
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
});
|
|
79
|
+
writeJson(join(cwd, ".claude", "settings.json"), {
|
|
80
|
+
hooks: {
|
|
81
|
+
PreToolUse: [
|
|
82
|
+
{
|
|
83
|
+
matcher: "Bash",
|
|
84
|
+
hooks: [{ type: "command", command: "echo claude" }],
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const config = loadHooksConfig(cwd);
|
|
91
|
+
const handlers = config.tool_call?.map((entry) => entry.hooks[0]?.command);
|
|
92
|
+
expect(handlers).toEqual(["echo tallow", "echo claude"]);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("allows native and Claude event names in the same .claude config", () => {
|
|
96
|
+
writeJson(join(cwd, ".claude", "settings.json"), {
|
|
97
|
+
hooks: {
|
|
98
|
+
PreToolUse: [
|
|
99
|
+
{
|
|
100
|
+
matcher: "Edit|Write",
|
|
101
|
+
hooks: [{ type: "command", command: "echo claude" }],
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
tool_call: [
|
|
105
|
+
{
|
|
106
|
+
matcher: "bash",
|
|
107
|
+
hooks: [{ type: "command", command: "echo native" }],
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const config = loadHooksConfig(cwd);
|
|
114
|
+
expect(config.tool_call).toHaveLength(2);
|
|
115
|
+
expect(config.tool_call?.[0]?.matcher).toBe("edit|write");
|
|
116
|
+
expect(config.tool_call?.[1]?.matcher).toBe("bash");
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -407,7 +407,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
407
407
|
const explicitModel = fm.model ?? agentConfig?.model;
|
|
408
408
|
const resolvedModel = explicitModel
|
|
409
409
|
? resolveModel(explicitModel)
|
|
410
|
-
: await routeForkedModel(content, undefined, ctx.model?.id);
|
|
410
|
+
: await routeForkedModel(content, undefined, ctx.model?.id, undefined, ctx.cwd);
|
|
411
411
|
|
|
412
412
|
// Show working indicator
|
|
413
413
|
const workingParts = [`🔀 forking: /${commandName}`];
|
|
@@ -34,15 +34,25 @@ export function resolveModel(input: string | undefined): string | undefined {
|
|
|
34
34
|
* @param modelOverride - Explicit model (fuzzy matched), skips auto-routing
|
|
35
35
|
* @param parentModelId - Parent model ID for fallback inheritance
|
|
36
36
|
* @param hints - Optional routing hints (modelScope, costPreference, etc.)
|
|
37
|
+
* @param cwd - Working directory used for project-local routing settings
|
|
37
38
|
* @returns Resolved model ID, or undefined on failure
|
|
38
39
|
*/
|
|
39
40
|
export async function routeForkedModel(
|
|
40
41
|
task: string,
|
|
41
42
|
modelOverride?: string,
|
|
42
43
|
parentModelId?: string,
|
|
43
|
-
hints?: RoutingHints
|
|
44
|
+
hints?: RoutingHints,
|
|
45
|
+
cwd?: string
|
|
44
46
|
): Promise<string | undefined> {
|
|
45
|
-
const routing = await routeModel(
|
|
47
|
+
const routing = await routeModel(
|
|
48
|
+
task,
|
|
49
|
+
modelOverride,
|
|
50
|
+
undefined,
|
|
51
|
+
parentModelId,
|
|
52
|
+
undefined,
|
|
53
|
+
hints,
|
|
54
|
+
cwd
|
|
55
|
+
);
|
|
46
56
|
if (!routing.ok) return modelOverride; // fallback to raw string
|
|
47
57
|
return routing.model.id;
|
|
48
58
|
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
adaptEventDataForHook,
|
|
4
|
+
CLAUDE_EVENT_MAP,
|
|
5
|
+
shouldSkipClaudeToolResultHandler,
|
|
6
|
+
translateClaudeHooks,
|
|
7
|
+
translateClaudeOutput,
|
|
8
|
+
translateClaudeToolMatcher,
|
|
9
|
+
} from "../index.js";
|
|
10
|
+
|
|
11
|
+
describe("translateClaudeToolMatcher", () => {
|
|
12
|
+
it("maps Claude built-in tool names to tallow tool names", () => {
|
|
13
|
+
expect(translateClaudeToolMatcher("Bash")).toBe("bash");
|
|
14
|
+
expect(translateClaudeToolMatcher("Edit|Write")).toBe("edit|write");
|
|
15
|
+
expect(translateClaudeToolMatcher("Read|Glob|Grep")).toBe("read|find|grep");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("passes through unknown and MCP matchers unchanged", () => {
|
|
19
|
+
expect(translateClaudeToolMatcher("mcp__github__.*")).toBe("mcp__github__.*");
|
|
20
|
+
expect(translateClaudeToolMatcher("CustomTool")).toBe("CustomTool");
|
|
21
|
+
expect(translateClaudeToolMatcher("*")).toBe("*");
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe("translateClaudeHooks", () => {
|
|
26
|
+
it("translates Claude event names to tallow event names", () => {
|
|
27
|
+
const config = {
|
|
28
|
+
PreToolUse: [{ matcher: "Bash", hooks: [{ type: "command", command: "echo pre" }] }],
|
|
29
|
+
PostToolUse: [{ matcher: "Write", hooks: [{ type: "command", command: "echo post" }] }],
|
|
30
|
+
Stop: [{ hooks: [{ type: "command", command: "echo stop" }] }],
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const translated = translateClaudeHooks(config, "test-source");
|
|
34
|
+
expect(translated.tool_call).toHaveLength(1);
|
|
35
|
+
expect(translated.tool_result).toHaveLength(1);
|
|
36
|
+
expect(translated.agent_end).toHaveLength(1);
|
|
37
|
+
expect(translated.tool_call[0]?.matcher).toBe("bash");
|
|
38
|
+
expect(translated.tool_result[0]?.matcher).toBe("write");
|
|
39
|
+
|
|
40
|
+
const handler = translated.tool_call[0]?.hooks[0];
|
|
41
|
+
expect(handler?._claudeSource).toBe(true);
|
|
42
|
+
expect(handler?._claudeEventName).toBe("PreToolUse");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("skips PermissionRequest with a warning", () => {
|
|
46
|
+
const warnings: string[] = [];
|
|
47
|
+
const originalWarn = console.warn;
|
|
48
|
+
console.warn = (...args: unknown[]) => {
|
|
49
|
+
warnings.push(args.map((arg) => String(arg)).join(" "));
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const translated = translateClaudeHooks(
|
|
54
|
+
{
|
|
55
|
+
PermissionRequest: [
|
|
56
|
+
{ matcher: "Bash", hooks: [{ type: "command", command: "echo deny" }] },
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
"/tmp/.claude/settings.json"
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
expect(Object.keys(translated)).toHaveLength(0);
|
|
63
|
+
expect(warnings.some((line) => line.includes("PermissionRequest"))).toBe(true);
|
|
64
|
+
} finally {
|
|
65
|
+
console.warn = originalWarn;
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("supports mixed native + Claude events in the same config", () => {
|
|
70
|
+
const translated = translateClaudeHooks({
|
|
71
|
+
PreToolUse: [{ matcher: "Edit|Write", hooks: [{ type: "command", command: "echo a" }] }],
|
|
72
|
+
tool_call: [{ matcher: "bash", hooks: [{ type: "command", command: "echo b" }] }],
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
expect(translated.tool_call).toHaveLength(2);
|
|
76
|
+
expect(translated.tool_call[0]?.matcher).toBe("edit|write");
|
|
77
|
+
expect(translated.tool_call[1]?.matcher).toBe("bash");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("passes unknown events through for forward compatibility", () => {
|
|
81
|
+
const translated = translateClaudeHooks({
|
|
82
|
+
FutureEvent: [{ hooks: [{ type: "command", command: "echo future" }] }],
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
expect(translated.FutureEvent).toHaveLength(1);
|
|
86
|
+
expect(translated.FutureEvent[0]?.hooks[0]?._claudeSource).toBe(true);
|
|
87
|
+
expect(translated.FutureEvent[0]?.hooks[0]?._claudeEventName).toBe("FutureEvent");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("maps all documented Claude events", () => {
|
|
91
|
+
const expectedMappings = {
|
|
92
|
+
SessionStart: "session_start",
|
|
93
|
+
UserPromptSubmit: "input",
|
|
94
|
+
PreToolUse: "tool_call",
|
|
95
|
+
PermissionRequest: "tool_call",
|
|
96
|
+
PostToolUse: "tool_result",
|
|
97
|
+
PostToolUseFailure: "tool_result",
|
|
98
|
+
Notification: "notification",
|
|
99
|
+
SubagentStart: "subagent_start",
|
|
100
|
+
SubagentStop: "subagent_stop",
|
|
101
|
+
Stop: "agent_end",
|
|
102
|
+
TeammateIdle: "teammate_idle",
|
|
103
|
+
TaskCompleted: "task_completed",
|
|
104
|
+
PreCompact: "session_before_compact",
|
|
105
|
+
SessionEnd: "session_shutdown",
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
expect(CLAUDE_EVENT_MAP).toEqual(expectedMappings);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("adaptEventDataForHook", () => {
|
|
113
|
+
it("adapts tool_call payload for PreToolUse hooks", () => {
|
|
114
|
+
const adapted = adaptEventDataForHook(
|
|
115
|
+
"tool_call",
|
|
116
|
+
{ input: { command: "echo hi" }, toolName: "bash" },
|
|
117
|
+
{ _claudeEventName: "PreToolUse", _claudeSource: true, type: "command" },
|
|
118
|
+
"/repo"
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
expect(adapted.tool_name).toBe("bash");
|
|
122
|
+
expect(adapted.tool_input).toEqual({ command: "echo hi" });
|
|
123
|
+
expect(adapted.hook_event_name).toBe("PreToolUse");
|
|
124
|
+
expect(adapted.cwd).toBe("/repo");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("adapts tool_result payload for PostToolUse hooks", () => {
|
|
128
|
+
const adapted = adaptEventDataForHook(
|
|
129
|
+
"tool_result",
|
|
130
|
+
{ content: [{ text: "ok", type: "text" }], input: { path: "a.ts" }, toolName: "write" },
|
|
131
|
+
{ _claudeEventName: "PostToolUse", _claudeSource: true, type: "command" },
|
|
132
|
+
"/repo"
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
expect(adapted.tool_name).toBe("write");
|
|
136
|
+
expect(adapted.tool_input).toEqual({ path: "a.ts" });
|
|
137
|
+
expect(adapted.tool_response).toEqual([{ text: "ok", type: "text" }]);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("adapts input payload for UserPromptSubmit hooks", () => {
|
|
141
|
+
const adapted = adaptEventDataForHook(
|
|
142
|
+
"input",
|
|
143
|
+
{ source: "terminal", text: "hello" },
|
|
144
|
+
{ _claudeEventName: "UserPromptSubmit", _claudeSource: true, type: "command" },
|
|
145
|
+
"/repo"
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
expect(adapted.prompt).toBe("hello");
|
|
149
|
+
expect(adapted.hook_event_name).toBe("UserPromptSubmit");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("keeps native hook payload unchanged", () => {
|
|
153
|
+
const event = { input: { command: "ls" }, toolName: "bash" };
|
|
154
|
+
const adapted = adaptEventDataForHook("tool_call", event, { type: "command" }, "/repo");
|
|
155
|
+
expect(adapted).toEqual(event);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe("translateClaudeOutput", () => {
|
|
160
|
+
it("maps permissionDecision deny to a blocking result", () => {
|
|
161
|
+
const result = translateClaudeOutput({
|
|
162
|
+
hookSpecificOutput: {
|
|
163
|
+
permissionDecision: "deny",
|
|
164
|
+
permissionDecisionReason: "no",
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
expect(result).toEqual({ decision: "block", ok: false, reason: "no" });
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("maps top-level decision block to a blocking result", () => {
|
|
172
|
+
const result = translateClaudeOutput({ decision: "block", reason: "blocked" });
|
|
173
|
+
expect(result).toEqual({ decision: "block", ok: false, reason: "blocked" });
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("maps continue=false to a stop result", () => {
|
|
177
|
+
const result = translateClaudeOutput({ continue: false, stopReason: "stop" });
|
|
178
|
+
expect(result).toEqual({ ok: false, reason: "stop" });
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("passes additional context through", () => {
|
|
182
|
+
const result = translateClaudeOutput({
|
|
183
|
+
hookSpecificOutput: { additionalContext: "remember this" },
|
|
184
|
+
});
|
|
185
|
+
expect(result).toEqual({ additionalContext: "remember this", ok: true });
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe("shouldSkipClaudeToolResultHandler", () => {
|
|
190
|
+
it("only runs PostToolUseFailure when isError=true", () => {
|
|
191
|
+
const handler = {
|
|
192
|
+
_claudeEventName: "PostToolUseFailure",
|
|
193
|
+
_claudeSource: true,
|
|
194
|
+
type: "command",
|
|
195
|
+
};
|
|
196
|
+
expect(shouldSkipClaudeToolResultHandler("tool_result", { isError: false }, handler)).toBe(
|
|
197
|
+
true
|
|
198
|
+
);
|
|
199
|
+
expect(shouldSkipClaudeToolResultHandler("tool_result", { isError: true }, handler)).toBe(
|
|
200
|
+
false
|
|
201
|
+
);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("only runs PostToolUse when isError=false", () => {
|
|
205
|
+
const handler = {
|
|
206
|
+
_claudeEventName: "PostToolUse",
|
|
207
|
+
_claudeSource: true,
|
|
208
|
+
type: "command",
|
|
209
|
+
};
|
|
210
|
+
expect(shouldSkipClaudeToolResultHandler("tool_result", { isError: true }, handler)).toBe(true);
|
|
211
|
+
expect(shouldSkipClaudeToolResultHandler("tool_result", { isError: false }, handler)).toBe(
|
|
212
|
+
false
|
|
213
|
+
);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
3
|
"title": "Pi Hooks Configuration",
|
|
4
|
-
"description": "Configuration for Pi agent hooks
|
|
4
|
+
"description": "Configuration for Pi agent hooks - shell commands, LLM prompts, or subagents triggered by lifecycle events.",
|
|
5
5
|
"type": "object",
|
|
6
6
|
"properties": {
|
|
7
7
|
"$schema": {
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
},
|
|
42
42
|
"before_agent_start": {
|
|
43
43
|
"$ref": "#/$defs/eventHooks",
|
|
44
|
-
"description": "Before the agent starts
|
|
44
|
+
"description": "Before the agent starts - can inject context."
|
|
45
45
|
},
|
|
46
46
|
"setup": {
|
|
47
47
|
"$ref": "#/$defs/eventHooks",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
},
|
|
62
62
|
"session_before_compact": {
|
|
63
63
|
"$ref": "#/$defs/eventHooks",
|
|
64
|
-
"description": "Before context compaction
|
|
64
|
+
"description": "Before context compaction - can preserve data."
|
|
65
65
|
},
|
|
66
66
|
"session_fork": {
|
|
67
67
|
"$ref": "#/$defs/eventHooks",
|
|
@@ -89,7 +89,7 @@
|
|
|
89
89
|
},
|
|
90
90
|
"context": {
|
|
91
91
|
"$ref": "#/$defs/eventHooks",
|
|
92
|
-
"description": "Context filtering
|
|
92
|
+
"description": "Context filtering - can modify messages sent to the model."
|
|
93
93
|
},
|
|
94
94
|
"model_select": {
|
|
95
95
|
"$ref": "#/$defs/eventHooks",
|
|
@@ -118,6 +118,62 @@
|
|
|
118
118
|
"task_completed": {
|
|
119
119
|
"$ref": "#/$defs/eventHooks",
|
|
120
120
|
"description": "When a team task completes. Matcher filters on assignee."
|
|
121
|
+
},
|
|
122
|
+
"SessionStart": {
|
|
123
|
+
"$ref": "#/$defs/eventHooks",
|
|
124
|
+
"description": "Claude Code alias for session_start."
|
|
125
|
+
},
|
|
126
|
+
"UserPromptSubmit": {
|
|
127
|
+
"$ref": "#/$defs/eventHooks",
|
|
128
|
+
"description": "Claude Code alias for input."
|
|
129
|
+
},
|
|
130
|
+
"PreToolUse": {
|
|
131
|
+
"$ref": "#/$defs/eventHooks",
|
|
132
|
+
"description": "Claude Code alias for tool_call. Matcher uses Claude names like Bash/Edit/Write and is translated automatically."
|
|
133
|
+
},
|
|
134
|
+
"PermissionRequest": {
|
|
135
|
+
"$ref": "#/$defs/eventHooks",
|
|
136
|
+
"description": "Claude Code event with no tallow equivalent. Parsed for compatibility and skipped with a warning."
|
|
137
|
+
},
|
|
138
|
+
"PostToolUse": {
|
|
139
|
+
"$ref": "#/$defs/eventHooks",
|
|
140
|
+
"description": "Claude Code alias for successful tool_result (isError=false)."
|
|
141
|
+
},
|
|
142
|
+
"PostToolUseFailure": {
|
|
143
|
+
"$ref": "#/$defs/eventHooks",
|
|
144
|
+
"description": "Claude Code alias for failed tool_result (isError=true)."
|
|
145
|
+
},
|
|
146
|
+
"Notification": {
|
|
147
|
+
"$ref": "#/$defs/eventHooks",
|
|
148
|
+
"description": "Claude Code alias for notification."
|
|
149
|
+
},
|
|
150
|
+
"SubagentStart": {
|
|
151
|
+
"$ref": "#/$defs/eventHooks",
|
|
152
|
+
"description": "Claude Code alias for subagent_start."
|
|
153
|
+
},
|
|
154
|
+
"SubagentStop": {
|
|
155
|
+
"$ref": "#/$defs/eventHooks",
|
|
156
|
+
"description": "Claude Code alias for subagent_stop."
|
|
157
|
+
},
|
|
158
|
+
"Stop": {
|
|
159
|
+
"$ref": "#/$defs/eventHooks",
|
|
160
|
+
"description": "Claude Code alias for agent_end."
|
|
161
|
+
},
|
|
162
|
+
"TeammateIdle": {
|
|
163
|
+
"$ref": "#/$defs/eventHooks",
|
|
164
|
+
"description": "Claude Code alias for teammate_idle."
|
|
165
|
+
},
|
|
166
|
+
"TaskCompleted": {
|
|
167
|
+
"$ref": "#/$defs/eventHooks",
|
|
168
|
+
"description": "Claude Code alias for task_completed."
|
|
169
|
+
},
|
|
170
|
+
"PreCompact": {
|
|
171
|
+
"$ref": "#/$defs/eventHooks",
|
|
172
|
+
"description": "Claude Code alias for session_before_compact."
|
|
173
|
+
},
|
|
174
|
+
"SessionEnd": {
|
|
175
|
+
"$ref": "#/$defs/eventHooks",
|
|
176
|
+
"description": "Claude Code alias for session_shutdown."
|
|
121
177
|
}
|
|
122
178
|
},
|
|
123
179
|
"additionalProperties": {
|
|
@@ -155,7 +211,7 @@
|
|
|
155
211
|
},
|
|
156
212
|
"hookHandler": {
|
|
157
213
|
"type": "object",
|
|
158
|
-
"description": "A single hook action
|
|
214
|
+
"description": "A single hook action - shell command, LLM prompt, or subagent.",
|
|
159
215
|
"properties": {
|
|
160
216
|
"type": {
|
|
161
217
|
"type": "string",
|