@dungle-scrubs/tallow 0.8.7 → 0.8.8

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.
Files changed (76) hide show
  1. package/README.md +1 -1
  2. package/dist/cli.js +244 -54
  3. package/dist/cli.js.map +1 -1
  4. package/dist/config.d.ts +1 -1
  5. package/dist/config.js +1 -1
  6. package/dist/sdk.d.ts +39 -0
  7. package/dist/sdk.d.ts.map +1 -1
  8. package/dist/sdk.js +322 -13
  9. package/dist/sdk.js.map +1 -1
  10. package/dist/session-utils.d.ts +8 -0
  11. package/dist/session-utils.d.ts.map +1 -1
  12. package/dist/session-utils.js +38 -1
  13. package/dist/session-utils.js.map +1 -1
  14. package/extensions/__integration__/plan-rejection-feedback.test.ts +272 -0
  15. package/extensions/__integration__/worktree.test.ts +88 -0
  16. package/extensions/_icons/index.ts +2 -3
  17. package/extensions/_shared/inline-preview.ts +2 -4
  18. package/extensions/_shared/interop-events.ts +48 -0
  19. package/extensions/_shared/shell-policy.ts +2 -1
  20. package/extensions/_shared/tallow-paths.ts +48 -0
  21. package/extensions/agent-commands-tool/__tests__/parsing.test.ts +40 -1
  22. package/extensions/agent-commands-tool/index.ts +38 -6
  23. package/extensions/background-task-tool/index.ts +2 -2
  24. package/extensions/bash-tool-enhanced/index.ts +3 -4
  25. package/extensions/cd-tool/index.ts +3 -3
  26. package/extensions/claude-bridge/index.ts +2 -1
  27. package/extensions/command-prompt/__tests__/plugin-sources.test.ts +49 -0
  28. package/extensions/command-prompt/index.ts +36 -0
  29. package/extensions/context-files/index.ts +2 -1
  30. package/extensions/context-fork/index.ts +2 -1
  31. package/extensions/context-usage/__tests__/tool-result-memory.test.ts +62 -0
  32. package/extensions/context-usage/index.ts +169 -8
  33. package/extensions/debug/__tests__/analysis.test.ts +35 -2
  34. package/extensions/debug/__tests__/diagnostics-commands.test.ts +11 -2
  35. package/extensions/debug/analysis.ts +53 -16
  36. package/extensions/debug/index.ts +84 -10
  37. package/extensions/debug/logger.ts +2 -2
  38. package/extensions/health/index.ts +2 -2
  39. package/extensions/hooks/__tests__/claude-compat.test.ts +31 -0
  40. package/extensions/hooks/extension.json +3 -1
  41. package/extensions/hooks/hooks.schema.json +17 -1
  42. package/extensions/hooks/index.ts +52 -7
  43. package/extensions/lsp/index.ts +2 -7
  44. package/extensions/output-styles-tool/index.ts +2 -4
  45. package/extensions/plan-mode-tool/extension.json +1 -0
  46. package/extensions/plan-mode-tool/index.ts +119 -4
  47. package/extensions/prompt-suggestions/index.ts +2 -3
  48. package/extensions/random-spinner/index.ts +2 -3
  49. package/extensions/read-tool-enhanced/__tests__/notebook-read.test.ts +100 -0
  50. package/extensions/read-tool-enhanced/__tests__/notebook.test.ts +136 -0
  51. package/extensions/read-tool-enhanced/extension.json +4 -4
  52. package/extensions/read-tool-enhanced/index.ts +112 -10
  53. package/extensions/read-tool-enhanced/notebook.ts +526 -0
  54. package/extensions/rewind/__tests__/snapshots.test.ts +43 -1
  55. package/extensions/rewind/snapshots.ts +18 -6
  56. package/extensions/session-memory/index.ts +3 -2
  57. package/extensions/stats/stats-log.ts +2 -3
  58. package/extensions/subagent-tool/__tests__/discovery-defaults.test.ts +23 -0
  59. package/extensions/subagent-tool/__tests__/isolation-frontmatter.test.ts +100 -0
  60. package/extensions/subagent-tool/__tests__/model-router.test.ts +8 -0
  61. package/extensions/subagent-tool/agents.ts +77 -16
  62. package/extensions/subagent-tool/index.ts +213 -48
  63. package/extensions/subagent-tool/model-router.ts +3 -3
  64. package/extensions/subagent-tool/process.ts +233 -22
  65. package/extensions/subagent-tool/schema.ts +11 -0
  66. package/extensions/subagent-tool/widget.ts +15 -2
  67. package/extensions/tasks/__tests__/widget-subagents.test.ts +28 -7
  68. package/extensions/tasks/commands/register-tasks-extension.ts +15 -33
  69. package/extensions/tasks/state/index.ts +2 -2
  70. package/extensions/theme-selector/index.ts +3 -7
  71. package/extensions/worktree/__tests__/lifecycle.test.ts +115 -0
  72. package/extensions/worktree/extension.json +31 -0
  73. package/extensions/worktree/index.ts +149 -0
  74. package/extensions/worktree/lifecycle.ts +372 -0
  75. package/package.json +1 -1
  76. package/skills/tallow-expert/SKILL.md +1 -1
@@ -20,6 +20,9 @@ interface ToolTimingStats {
20
20
  avgMs: number;
21
21
  p50Ms: number;
22
22
  p95Ms: number;
23
+ totalPayloadBytes: number;
24
+ avgPayloadBytes: number;
25
+ summarizedCount: number;
23
26
  }
24
27
 
25
28
  /**
@@ -35,6 +38,18 @@ function percentile(sorted: number[], p: number): number {
35
38
  return sorted[Math.max(0, idx)];
36
39
  }
37
40
 
41
+ /**
42
+ * Format bytes with compact units for table output.
43
+ *
44
+ * @param count - Byte count
45
+ * @returns Human-readable byte string
46
+ */
47
+ function formatBytes(count: number): string {
48
+ if (count < 1024) return `${count}B`;
49
+ if (count < 1024 * 1024) return `${(count / 1024).toFixed(1)}KB`;
50
+ return `${(count / (1024 * 1024)).toFixed(1)}MB`;
51
+ }
52
+
38
53
  /**
39
54
  * Computes per-tool timing statistics from tool result log entries.
40
55
  *
@@ -45,35 +60,53 @@ function percentile(sorted: number[], p: number): number {
45
60
  * @returns Array of timing stats sorted by total time descending
46
61
  */
47
62
  export function summarizeToolTimings(entries: LogEntry[]): ToolTimingStats[] {
48
- const timingsByTool = new Map<string, number[]>();
63
+ const timingsByTool = new Map<
64
+ string,
65
+ { durations: number[]; payloadBytes: number; summarizedCount: number }
66
+ >();
49
67
 
50
68
  for (const entry of entries) {
51
69
  if (entry.cat !== "tool" || entry.evt !== "result") continue;
52
- const { name, durationMs } = entry.data as { name?: string; durationMs?: number };
70
+ const { durationMs, name, payloadBytes, summarizedByRetention } = entry.data as {
71
+ durationMs?: number;
72
+ name?: string;
73
+ payloadBytes?: number;
74
+ summarizedByRetention?: boolean;
75
+ };
53
76
  if (!name || durationMs == null) continue;
54
77
 
55
78
  const existing = timingsByTool.get(name);
56
79
  if (existing) {
57
- existing.push(durationMs);
80
+ existing.durations.push(durationMs);
81
+ existing.payloadBytes += typeof payloadBytes === "number" ? Math.max(0, payloadBytes) : 0;
82
+ existing.summarizedCount += summarizedByRetention ? 1 : 0;
58
83
  } else {
59
- timingsByTool.set(name, [durationMs]);
84
+ timingsByTool.set(name, {
85
+ durations: [durationMs],
86
+ payloadBytes: typeof payloadBytes === "number" ? Math.max(0, payloadBytes) : 0,
87
+ summarizedCount: summarizedByRetention ? 1 : 0,
88
+ });
60
89
  }
61
90
  }
62
91
 
63
92
  const stats: ToolTimingStats[] = [];
64
- for (const [name, durations] of timingsByTool) {
65
- durations.sort((a, b) => a - b);
66
- const totalMs = durations.reduce((sum, d) => sum + d, 0);
93
+ for (const [name, values] of timingsByTool) {
94
+ values.durations.sort((a, b) => a - b);
95
+ const totalMs = values.durations.reduce((sum, d) => sum + d, 0);
67
96
 
68
97
  stats.push({
69
98
  name,
70
- callCount: durations.length,
99
+ callCount: values.durations.length,
71
100
  totalMs,
72
- minMs: durations[0],
73
- maxMs: durations[durations.length - 1],
74
- avgMs: Math.round(totalMs / durations.length),
75
- p50Ms: percentile(durations, 50),
76
- p95Ms: percentile(durations, 95),
101
+ minMs: values.durations[0],
102
+ maxMs: values.durations[values.durations.length - 1],
103
+ avgMs: Math.round(totalMs / values.durations.length),
104
+ p50Ms: percentile(values.durations, 50),
105
+ p95Ms: percentile(values.durations, 95),
106
+ totalPayloadBytes: values.payloadBytes,
107
+ avgPayloadBytes:
108
+ values.durations.length > 0 ? Math.round(values.payloadBytes / values.durations.length) : 0,
109
+ summarizedCount: values.summarizedCount,
77
110
  });
78
111
  }
79
112
 
@@ -92,13 +125,13 @@ export function formatToolTimings(stats: ToolTimingStats[]): string {
92
125
  if (stats.length === 0) return "No tool timing data found.";
93
126
 
94
127
  const lines = [
95
- "| Tool | Calls | Total (ms) | Avg (ms) | p50 (ms) | p95 (ms) | Max (ms) |",
96
- "|------|-------|------------|----------|----------|----------|----------|",
128
+ "| Tool | Calls | Total (ms) | Avg (ms) | p50 (ms) | p95 (ms) | Max (ms) | Avg payload | Summarized |",
129
+ "|------|-------|------------|----------|----------|----------|----------|-------------|------------|",
97
130
  ];
98
131
 
99
132
  for (const s of stats) {
100
133
  lines.push(
101
- `| ${s.name} | ${s.callCount} | ${s.totalMs} | ${s.avgMs} | ${s.p50Ms} | ${s.p95Ms} | ${s.maxMs} |`
134
+ `| ${s.name} | ${s.callCount} | ${s.totalMs} | ${s.avgMs} | ${s.p50Ms} | ${s.p95Ms} | ${s.maxMs} | ${formatBytes(s.avgPayloadBytes)} | ${s.summarizedCount} |`
102
135
  );
103
136
  }
104
137
 
@@ -281,7 +314,11 @@ export function formatEntries(entries: LogEntry[]): string {
281
314
  const highlights: string[] = [];
282
315
  if (data.name) highlights.push(`name=${data.name}`);
283
316
  if (data.durationMs != null) highlights.push(`${data.durationMs}ms`);
317
+ if (typeof data.payloadBytes === "number") {
318
+ highlights.push(`payload=${formatBytes(Math.max(0, data.payloadBytes))}`);
319
+ }
284
320
  if (data.ok !== undefined) highlights.push(data.ok ? "ok" : "FAILED");
321
+ if (data.summarizedByRetention === true) highlights.push("summarized");
285
322
  if (data.message) highlights.push(`"${String(data.message).slice(0, 80)}"`);
286
323
  if (data.agentId) highlights.push(`agent=${data.agentId}`);
287
324
  if (data.exitCode !== undefined) highlights.push(`exit=${data.exitCode}`);
@@ -16,6 +16,7 @@
16
16
  import { existsSync, readFileSync } from "node:fs";
17
17
  import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
18
18
  import { Type } from "@sinclair/typebox";
19
+ import { getTallowPath } from "../_shared/tallow-paths.js";
19
20
  import {
20
21
  calculateTurnMetrics,
21
22
  formatEntries,
@@ -35,19 +36,81 @@ let totalToolCalls = 0;
35
36
  let totalTurns = 0;
36
37
  let sessionStartTime = 0;
37
38
 
39
+ /** Marker key set by sdk retention when historical results are summarized. */
40
+ const TOOL_RESULT_RETENTION_MARKER = "__tallow_summarized_tool_result__";
41
+
42
+ /** Payload metrics captured for each tool_result event. */
43
+ interface ToolResultPayloadMetrics {
44
+ readonly contentLength: number;
45
+ readonly contentBytes: number;
46
+ readonly detailsBytes: number;
47
+ readonly imageBlocks: number;
48
+ readonly payloadBytes: number;
49
+ readonly summarizedByRetention: boolean;
50
+ readonly textBlocks: number;
51
+ }
52
+
38
53
  /**
39
- * Safely extracts text content length from a tool result's content array.
40
- * @param content - Array of text/image content blocks
41
- * @returns Total character count of text blocks
54
+ * Estimate UTF-8 bytes for JSON-serializable data.
55
+ *
56
+ * @param value - Arbitrary data payload
57
+ * @returns Byte length of serialized data, or 0 on serialization failure
42
58
  */
43
- function contentLength(content: Array<{ type: string; text?: string }>): number {
44
- let len = 0;
59
+ function safeJsonBytes(value: unknown): number {
60
+ if (value == null) return 0;
61
+ try {
62
+ return Buffer.byteLength(JSON.stringify(value), "utf-8");
63
+ } catch {
64
+ return 0;
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Measure text/image/details payload characteristics for a tool result.
70
+ *
71
+ * @param content - Tool result content blocks
72
+ * @param details - Tool result details payload
73
+ * @returns Structured payload metrics for debug logging
74
+ */
75
+ function measureToolResultPayload(
76
+ content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>,
77
+ details: unknown
78
+ ): ToolResultPayloadMetrics {
79
+ let contentLength = 0;
80
+ let contentBytes = 0;
81
+ let imageBlocks = 0;
82
+ let textBlocks = 0;
83
+
45
84
  for (const block of content) {
46
- if (block.type === "text" && block.text) {
47
- len += block.text.length;
85
+ if (block.type === "text") {
86
+ const text = block.text ?? "";
87
+ contentLength += text.length;
88
+ contentBytes += Buffer.byteLength(text, "utf-8");
89
+ textBlocks += 1;
90
+ continue;
91
+ }
92
+ if (block.type === "image") {
93
+ contentBytes += Buffer.byteLength(block.data ?? "", "utf-8");
94
+ contentBytes += Buffer.byteLength(block.mimeType ?? "", "utf-8");
95
+ imageBlocks += 1;
48
96
  }
49
97
  }
50
- return len;
98
+
99
+ const detailsBytes = safeJsonBytes(details);
100
+ const summarizedByRetention =
101
+ typeof details === "object" &&
102
+ details !== null &&
103
+ (details as Record<string, unknown>)[TOOL_RESULT_RETENTION_MARKER] === true;
104
+
105
+ return {
106
+ contentLength,
107
+ contentBytes,
108
+ detailsBytes,
109
+ imageBlocks,
110
+ payloadBytes: contentBytes + detailsBytes,
111
+ summarizedByRetention,
112
+ textBlocks,
113
+ };
51
114
  }
52
115
 
53
116
  /**
@@ -256,12 +319,23 @@ export default function (pi: ExtensionAPI) {
256
319
  const durationMs = startTime !== undefined ? Math.round(performance.now() - startTime) : null;
257
320
  toolTimings.delete(event.toolCallId);
258
321
 
322
+ const payload = measureToolResultPayload(
323
+ event.content as Array<{ type: string; text?: string; data?: string; mimeType?: string }>,
324
+ event.details
325
+ );
326
+
259
327
  logger.log("tool", "result", {
260
328
  toolCallId: event.toolCallId,
261
329
  name: event.toolName,
262
330
  durationMs,
263
331
  ok: !event.isError,
264
- contentLength: contentLength(event.content as Array<{ type: string; text?: string }>),
332
+ contentLength: payload.contentLength,
333
+ contentBytes: payload.contentBytes,
334
+ detailsBytes: payload.detailsBytes,
335
+ payloadBytes: payload.payloadBytes,
336
+ textBlocks: payload.textBlocks,
337
+ imageBlocks: payload.imageBlocks,
338
+ summarizedByRetention: payload.summarizedByRetention,
265
339
  });
266
340
  });
267
341
 
@@ -501,7 +575,7 @@ export default function (pi: ExtensionAPI) {
501
575
  * @returns Absolute path to the debug log
502
576
  */
503
577
  function getLogPath(): string {
504
- return logger?.logPath ?? `${process.env.HOME}/.tallow/debug.log`;
578
+ return logger?.logPath ?? getTallowPath("debug.log");
505
579
  }
506
580
 
507
581
  /**
@@ -18,8 +18,8 @@ import {
18
18
  truncateSync,
19
19
  writeFileSync,
20
20
  } from "node:fs";
21
- import { homedir } from "node:os";
22
21
  import { join } from "node:path";
22
+ import { getTallowHomeDir } from "../_shared/tallow-paths.js";
23
23
 
24
24
  /** Log entry categories that partition diagnostic output. */
25
25
  export type LogCategory =
@@ -238,7 +238,7 @@ export class DebugLogger {
238
238
  logDir?: string
239
239
  ) {
240
240
  this.useStderr = process.env.TALLOW_DEBUG === "stderr";
241
- const dir = logDir ?? join(homedir(), ".tallow");
241
+ const dir = logDir ?? getTallowHomeDir();
242
242
  this.logPath = join(dir, "debug.log");
243
243
 
244
244
  if (!this.useStderr) {
@@ -7,7 +7,6 @@
7
7
  */
8
8
 
9
9
  import { existsSync, readFileSync } from "node:fs";
10
- import { homedir } from "node:os";
11
10
  import { join } from "node:path";
12
11
  import type {
13
12
  ContextUsage,
@@ -16,6 +15,7 @@ import type {
16
15
  Theme,
17
16
  } from "@mariozechner/pi-coding-agent";
18
17
  import { BorderedBox, ROUNDED, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
18
+ import { getTallowHomeDir } from "../_shared/tallow-paths.js";
19
19
 
20
20
  // ── Types ────────────────────────────────────────────────────────────────────
21
21
 
@@ -580,7 +580,7 @@ export default function healthExtension(pi: ExtensionAPI): void {
580
580
  const piVersion = readPackageVersion(
581
581
  join(packageDir, "node_modules", "@mariozechner", "pi-coding-agent", "package.json")
582
582
  );
583
- const tallowHome = process.env.TALLOW_CODING_AGENT_DIR ?? join(homedir(), ".tallow");
583
+ const tallowHome = getTallowHomeDir();
584
584
 
585
585
  // ── Message renderer ─────────────────────────────────────────────────
586
586
 
@@ -42,6 +42,35 @@ describe("translateClaudeHooks", () => {
42
42
  expect(handler?._claudeEventName).toBe("PreToolUse");
43
43
  });
44
44
 
45
+ it("translates worktree aliases and preserves scope matchers", () => {
46
+ const translated = translateClaudeHooks({
47
+ WorktreeCreate: [
48
+ { matcher: "project|feature", hooks: [{ type: "command", command: "echo create" }] },
49
+ ],
50
+ WorktreeRemove: [
51
+ { matcher: "project", hooks: [{ type: "command", command: "echo remove" }] },
52
+ ],
53
+ });
54
+
55
+ expect(translated.worktree_create).toHaveLength(1);
56
+ expect(translated.worktree_remove).toHaveLength(1);
57
+ expect(translated.worktree_create[0]?.matcher).toBe("project|feature");
58
+ expect(translated.worktree_remove[0]?.matcher).toBe("project");
59
+ expect(translated.worktree_remove[0]?.hooks[0]?._claudeEventName).toBe("WorktreeRemove");
60
+ });
61
+
62
+ it("translates worktree lifecycle aliases", () => {
63
+ const translated = translateClaudeHooks({
64
+ WorktreeCreate: [{ hooks: [{ type: "command", command: "echo create" }] }],
65
+ WorktreeRemove: [{ hooks: [{ type: "command", command: "echo remove" }] }],
66
+ });
67
+
68
+ expect(translated.worktree_create).toHaveLength(1);
69
+ expect(translated.worktree_remove).toHaveLength(1);
70
+ expect(translated.worktree_create[0]?.hooks[0]?._claudeEventName).toBe("WorktreeCreate");
71
+ expect(translated.worktree_remove[0]?.hooks[0]?._claudeEventName).toBe("WorktreeRemove");
72
+ });
73
+
45
74
  it("skips PermissionRequest with a warning", () => {
46
75
  const warnings: string[] = [];
47
76
  const originalWarn = console.warn;
@@ -98,6 +127,8 @@ describe("translateClaudeHooks", () => {
98
127
  Notification: "notification",
99
128
  SubagentStart: "subagent_start",
100
129
  SubagentStop: "subagent_stop",
130
+ WorktreeCreate: "worktree_create",
131
+ WorktreeRemove: "worktree_remove",
101
132
  Stop: "agent_end",
102
133
  TeammateIdle: "teammate_idle",
103
134
  TaskCompleted: "task_completed",
@@ -31,7 +31,9 @@
31
31
  "tool_result",
32
32
  "turn_end",
33
33
  "turn_start",
34
- "user_bash"
34
+ "user_bash",
35
+ "worktree_create",
36
+ "worktree_remove"
35
37
  ]
36
38
  },
37
39
  "permissionSurface": {
@@ -107,6 +107,14 @@
107
107
  "$ref": "#/$defs/eventHooks",
108
108
  "description": "When a subagent completes. Matcher filters on agent_type."
109
109
  },
110
+ "worktree_create": {
111
+ "$ref": "#/$defs/eventHooks",
112
+ "description": "When a managed worktree is created. Matcher filters on scope (session/subagent)."
113
+ },
114
+ "worktree_remove": {
115
+ "$ref": "#/$defs/eventHooks",
116
+ "description": "When a managed worktree is removed. Matcher filters on scope (session/subagent)."
117
+ },
110
118
  "notification": {
111
119
  "$ref": "#/$defs/eventHooks",
112
120
  "description": "When a notification is emitted via the event bus. Matcher filters on type."
@@ -155,6 +163,14 @@
155
163
  "$ref": "#/$defs/eventHooks",
156
164
  "description": "Claude Code alias for subagent_stop."
157
165
  },
166
+ "WorktreeCreate": {
167
+ "$ref": "#/$defs/eventHooks",
168
+ "description": "Claude Code alias for worktree_create."
169
+ },
170
+ "WorktreeRemove": {
171
+ "$ref": "#/$defs/eventHooks",
172
+ "description": "Claude Code alias for worktree_remove."
173
+ },
158
174
  "Stop": {
159
175
  "$ref": "#/$defs/eventHooks",
160
176
  "description": "Claude Code alias for agent_end."
@@ -196,7 +212,7 @@
196
212
  "properties": {
197
213
  "matcher": {
198
214
  "type": "string",
199
- "description": "Regex pattern to filter events. Empty or '*' matches all. For tool_call/tool_result, matches toolName."
215
+ "description": "Regex pattern to filter events. Empty or '*' matches all. Uses event-specific fields (for example: tool_call/tool_result=toolName, task_completed=assignee, teammate_idle=teammate, worktree_create/worktree_remove=scope)."
200
216
  },
201
217
  "hooks": {
202
218
  "type": "array",
@@ -37,6 +37,7 @@ import {
37
37
  } from "../../src/agent-runner.js";
38
38
  import { isProjectTrusted } from "../_shared/project-trust.js";
39
39
  import { evaluateCommand } from "../_shared/shell-policy.js";
40
+ import { getTallowHomeDir, getTallowPath } from "../_shared/tallow-paths.js";
40
41
  import { createHookStateManager, type HookStateManager } from "./state-manager.js";
41
42
 
42
43
  /** Hook execution strategy: shell command, LLM prompt, or agent subprocess. */
@@ -304,6 +305,8 @@ const MATCHER_FIELDS: Record<string, string> = {
304
305
  tool_result: "toolName",
305
306
  teammate_idle: "teammate",
306
307
  task_completed: "assignee",
308
+ worktree_create: "scope",
309
+ worktree_remove: "scope",
307
310
  setup: "trigger",
308
311
  subagent_start: "agent_type",
309
312
  subagent_stop: "agent_type",
@@ -323,6 +326,8 @@ export const CLAUDE_EVENT_MAP: Readonly<Record<string, string>> = {
323
326
  Notification: "notification",
324
327
  SubagentStart: "subagent_start",
325
328
  SubagentStop: "subagent_stop",
329
+ WorktreeCreate: "worktree_create",
330
+ WorktreeRemove: "worktree_remove",
326
331
  Stop: "agent_end",
327
332
  TeammateIdle: "teammate_idle",
328
333
  TaskCompleted: "task_completed",
@@ -1129,10 +1134,24 @@ export default function (pi: ExtensionAPI) {
1129
1134
  }
1130
1135
  };
1131
1136
 
1137
+ /**
1138
+ * Dispatches EventBus hook execution without blocking the publisher.
1139
+ *
1140
+ * @param eventName - Hook event name to execute
1141
+ * @param eventData - Event payload forwarded to hook matchers/handlers
1142
+ * @returns Nothing
1143
+ */
1144
+ const dispatchEventBusHooks = (eventName: string, eventData: Record<string, unknown>): void => {
1145
+ void runHooks(eventName, eventData).catch((error: unknown) => {
1146
+ const message = error instanceof Error ? error.message : String(error);
1147
+ console.error(`[hooks] EventBus dispatch failed for ${eventName}: ${message}`);
1148
+ });
1149
+ };
1150
+
1132
1151
  /** Forward teammate_idle events to hook handlers. */
1133
1152
  const onTeammateIdle = (data: unknown) => {
1134
1153
  const event = data as { team: string; teammate: string; role: string };
1135
- runHooks("teammate_idle", event);
1154
+ dispatchEventBusHooks("teammate_idle", event);
1136
1155
  };
1137
1156
 
1138
1157
  /** Forward task_completed events to hook handlers. */
@@ -1144,7 +1163,7 @@ export default function (pi: ExtensionAPI) {
1144
1163
  assignee: string;
1145
1164
  result: string;
1146
1165
  };
1147
- runHooks("task_completed", event);
1166
+ dispatchEventBusHooks("task_completed", event);
1148
1167
  };
1149
1168
 
1150
1169
  /** Forward subagent_start events from EventBus to hook handlers. */
@@ -1156,7 +1175,7 @@ export default function (pi: ExtensionAPI) {
1156
1175
  cwd: string;
1157
1176
  background: boolean;
1158
1177
  };
1159
- runHooks("subagent_start", event);
1178
+ dispatchEventBusHooks("subagent_start", event);
1160
1179
  };
1161
1180
 
1162
1181
  /** Forward subagent_stop events from EventBus to hook handlers. */
@@ -1169,7 +1188,7 @@ export default function (pi: ExtensionAPI) {
1169
1188
  result: string;
1170
1189
  background: boolean;
1171
1190
  };
1172
- runHooks("subagent_stop", event);
1191
+ dispatchEventBusHooks("subagent_stop", event);
1173
1192
  };
1174
1193
 
1175
1194
  /** Forward notification events from EventBus to hook handlers. */
@@ -1179,7 +1198,29 @@ export default function (pi: ExtensionAPI) {
1179
1198
  type: string;
1180
1199
  source?: string;
1181
1200
  };
1182
- runHooks("notification", event);
1201
+ dispatchEventBusHooks("notification", event);
1202
+ };
1203
+
1204
+ /**
1205
+ * Forward worktree_create events from EventBus to hook handlers.
1206
+ *
1207
+ * @param data - Event payload from EventBus
1208
+ * @returns Nothing
1209
+ */
1210
+ const onWorktreeCreate = (data: unknown) => {
1211
+ const event = data as Record<string, unknown>;
1212
+ dispatchEventBusHooks("worktree_create", event);
1213
+ };
1214
+
1215
+ /**
1216
+ * Forward worktree_remove events from EventBus to hook handlers without blocking.
1217
+ *
1218
+ * @param data - Event payload from EventBus
1219
+ * @returns Nothing
1220
+ */
1221
+ const onWorktreeRemove = (data: unknown) => {
1222
+ const event = data as Record<string, unknown>;
1223
+ dispatchEventBusHooks("worktree_remove", event);
1183
1224
  };
1184
1225
 
1185
1226
  // ── Session lifecycle ────────────────────────────────────────
@@ -1188,10 +1229,10 @@ export default function (pi: ExtensionAPI) {
1188
1229
  ctx = context;
1189
1230
  currentCwd = context.cwd;
1190
1231
  hooksConfig = loadHooksConfig(currentCwd);
1191
- agentsDir = path.join(process.env.HOME || "", ".tallow", "agents");
1232
+ agentsDir = getTallowPath("agents");
1192
1233
 
1193
1234
  // Initialize once-hook state manager
1194
- const tallowHome = path.join(process.env.HOME || "", ".tallow");
1235
+ const tallowHome = getTallowHomeDir();
1195
1236
  stateManager = createHookStateManager(tallowHome);
1196
1237
 
1197
1238
  // Check for project-local agents dir
@@ -1215,6 +1256,8 @@ export default function (pi: ExtensionAPI) {
1215
1256
  const unsub4 = pi.events.on("subagent_start", onSubagentStart);
1216
1257
  const unsub5 = pi.events.on("subagent_stop", onSubagentStop);
1217
1258
  const unsub6 = pi.events.on("notification", onNotification);
1259
+ const unsub7 = pi.events.on("worktree_create", onWorktreeCreate);
1260
+ const unsub8 = pi.events.on("worktree_remove", onWorktreeRemove);
1218
1261
  G.__hooksEventCleanup = () => {
1219
1262
  unsub1();
1220
1263
  unsub2();
@@ -1222,6 +1265,8 @@ export default function (pi: ExtensionAPI) {
1222
1265
  unsub4();
1223
1266
  unsub5();
1224
1267
  unsub6();
1268
+ unsub7();
1269
+ unsub8();
1225
1270
  };
1226
1271
 
1227
1272
  // Run setup hooks if triggered by --init, --init-only, or --maintenance CLI flags.
@@ -13,7 +13,6 @@
13
13
 
14
14
  import { type ChildProcess, spawn } from "node:child_process";
15
15
  import * as fs from "node:fs";
16
- import { homedir } from "node:os";
17
16
  import * as path from "node:path";
18
17
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
19
18
  import { Type } from "@sinclair/typebox";
@@ -41,6 +40,7 @@ import {
41
40
  WorkspaceSymbolRequest,
42
41
  } from "vscode-languageserver-protocol";
43
42
  import { getIcon } from "../_icons/index.js";
43
+ import { getTallowSettingsPath } from "../_shared/tallow-paths.js";
44
44
 
45
45
  /** Language server binary configuration and project detection markers. */
46
46
  interface ServerConfig {
@@ -280,12 +280,7 @@ function readStartupTimeoutFromSettings(settings: Record<string, unknown>): numb
280
280
  * @returns Absolute path to the user-level settings file
281
281
  */
282
282
  function getGlobalSettingsPath(): string {
283
- const configuredAgentDir = process.env.TALLOW_CODING_AGENT_DIR ?? process.env.PI_CODING_AGENT_DIR;
284
- if (typeof configuredAgentDir === "string" && configuredAgentDir.trim().length > 0) {
285
- return path.join(configuredAgentDir, "settings.json");
286
- }
287
-
288
- return path.join(homedir(), ".tallow", "settings.json");
283
+ return getTallowSettingsPath();
289
284
  }
290
285
 
291
286
  /**
@@ -18,6 +18,7 @@ import * as fs from "node:fs";
18
18
  import * as path from "node:path";
19
19
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
20
20
  import { atomicWriteFileSync } from "../_shared/atomic-write.js";
21
+ import { getTallowPath } from "../_shared/tallow-paths.js";
21
22
  import {
22
23
  buildReminderContent,
23
24
  buildStyledPrompt,
@@ -29,10 +30,7 @@ import {
29
30
  // ── Discovery ───────────────────────────────────────
30
31
 
31
32
  /** Standard locations for output style files */
32
- const USER_STYLES_DIR = path.join(
33
- process.env.PI_CODING_AGENT_DIR || path.join(process.env.HOME || "", ".tallow"),
34
- "output-styles"
35
- );
33
+ const USER_STYLES_DIR = getTallowPath("output-styles");
36
34
  const PROJECT_STYLES_DIR = path.join(process.cwd(), ".tallow", "output-styles");
37
35
 
38
36
  /**
@@ -12,6 +12,7 @@
12
12
  "context",
13
13
  "session_start",
14
14
  "tool_call",
15
+ "tool_result",
15
16
  "turn_end"
16
17
  ]
17
18
  },