@dungle-scrubs/tallow 0.8.26 → 0.8.27

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 (48) hide show
  1. package/dist/config.d.ts +1 -1
  2. package/dist/config.js +1 -1
  3. package/dist/interactive-mode-patch.d.ts +1 -0
  4. package/dist/interactive-mode-patch.d.ts.map +1 -1
  5. package/dist/interactive-mode-patch.js +40 -1
  6. package/dist/interactive-mode-patch.js.map +1 -1
  7. package/dist/pid-manager.d.ts +2 -9
  8. package/dist/pid-manager.d.ts.map +1 -1
  9. package/dist/pid-manager.js +1 -58
  10. package/dist/pid-manager.js.map +1 -1
  11. package/dist/pid-schema.d.ts +51 -0
  12. package/dist/pid-schema.d.ts.map +1 -0
  13. package/dist/pid-schema.js +70 -0
  14. package/dist/pid-schema.js.map +1 -0
  15. package/dist/sdk.js +4 -8
  16. package/dist/sdk.js.map +1 -1
  17. package/extensions/__integration__/audit-findings.test.ts +309 -0
  18. package/extensions/__integration__/tasks-runtime.test.ts +63 -12
  19. package/extensions/_shared/lazy-init.ts +88 -3
  20. package/extensions/_shared/pid-registry.ts +8 -82
  21. package/extensions/cheatsheet/__tests__/cheatsheet.test.ts +47 -0
  22. package/extensions/clear/__tests__/clear.test.ts +38 -0
  23. package/extensions/git-status/__tests__/git-status.test.ts +32 -0
  24. package/extensions/mcp-adapter-tool/index.ts +1 -1
  25. package/extensions/minimal-skill-display/__tests__/minimal-skill-display.test.ts +20 -0
  26. package/extensions/permissions/__tests__/permissions.test.ts +213 -0
  27. package/extensions/progress-indicator/__tests__/progress-indicator.test.ts +104 -0
  28. package/extensions/random-spinner/__tests__/random-spinner.test.ts +35 -0
  29. package/extensions/show-system-prompt/__tests__/show-system-prompt.test.ts +51 -0
  30. package/extensions/subagent-tool/__tests__/presentation-rendering.test.ts +5 -4
  31. package/extensions/subagent-tool/__tests__/process-liveness.test.ts +51 -0
  32. package/extensions/subagent-tool/__tests__/subprocess-args.test.ts +120 -0
  33. package/extensions/subagent-tool/formatting.ts +2 -0
  34. package/extensions/subagent-tool/index.ts +156 -95
  35. package/extensions/subagent-tool/process.ts +126 -32
  36. package/extensions/tasks/commands/register-tasks-extension.ts +64 -20
  37. package/extensions/tasks/extension.json +1 -0
  38. package/extensions/tasks/index.ts +2 -12
  39. package/extensions/tasks/state/index.ts +26 -0
  40. package/extensions/teams-tool/dashboard.ts +13 -1
  41. package/extensions/teams-tool/tools/register-extension.ts +10 -2
  42. package/extensions/upstream-check/__tests__/upstream-check.test.ts +49 -0
  43. package/extensions/wezterm-notify/__tests__/index.test.ts +49 -11
  44. package/extensions/wezterm-notify/index.ts +5 -3
  45. package/extensions/write-tool-enhanced/__tests__/write-tool-enhanced.test.ts +296 -0
  46. package/package.json +3 -2
  47. package/runtime/pid-schema.ts +13 -0
  48. package/skills/tallow-expert/SKILL.md +1 -1
@@ -0,0 +1,296 @@
1
+ /**
2
+ * Tests for the write-tool-enhanced extension.
3
+ *
4
+ * Verifies tool registration, execute output shape, summary formatting,
5
+ * error propagation, and renderResult presentation variants.
6
+ *
7
+ * `createWriteTool` is mocked via `mock.module` so no real filesystem
8
+ * writes occur. The extension is dynamically imported after the mock
9
+ * is registered so that its module-level `createWriteTool(process.cwd())`
10
+ * call also receives the mock.
11
+ */
12
+
13
+ import { describe, expect, mock, test } from "bun:test";
14
+ import type { ExtensionContext, Theme } from "@mariozechner/pi-coding-agent";
15
+ import { ExtensionHarness } from "../../../test-utils/extension-harness.js";
16
+
17
+ // ── Mock setup ────────────────────────────────────────────────────────────────
18
+
19
+ /**
20
+ * Controllable execute mock — default returns a "wrote file" success result.
21
+ * Use `.mockImplementationOnce()` in individual tests to simulate errors.
22
+ */
23
+ const mockExecute = mock(async () => ({
24
+ content: [{ type: "text", text: "wrote file" }],
25
+ }));
26
+
27
+ /**
28
+ * Mock the entire `@mariozechner/pi-coding-agent` module so that
29
+ * `createWriteTool` never touches the real filesystem.
30
+ * The factory is evaluated once (on first import) to produce the module exports.
31
+ */
32
+ mock.module("@mariozechner/pi-coding-agent", () => ({
33
+ createWriteTool: (_cwd: string) => ({
34
+ label: "Write file",
35
+ description: "Write a new file at path, or overwrite an existing file.",
36
+ parameters: {},
37
+ execute: mockExecute,
38
+ }),
39
+ }));
40
+
41
+ // Dynamically import the extension AFTER the mock is registered.
42
+ const { default: writePreview } = await import("../index.js");
43
+
44
+ // ── Helpers ───────────────────────────────────────────────────────────────────
45
+
46
+ /**
47
+ * Build a minimal Theme stub with deterministic wrappers for assertions.
48
+ *
49
+ * @returns Fake Theme where fg/bold produce `<tag>text</tag>` strings
50
+ */
51
+ function createMockTheme(): Theme {
52
+ return {
53
+ bold: (text: string) => `<b>${text}</b>`,
54
+ fg: (color: string, text: string) => `<${color}>${text}</${color}>`,
55
+ } as unknown as Theme;
56
+ }
57
+
58
+ /**
59
+ * Build a minimal ExtensionContext stub for execute calls.
60
+ *
61
+ * @returns Partial context cast to ExtensionContext
62
+ */
63
+ function stubContext(): ExtensionContext {
64
+ return {
65
+ hasUI: false,
66
+ ui: { setWorkingMessage() {} },
67
+ cwd: "/tmp/test-write",
68
+ } as unknown as ExtensionContext;
69
+ }
70
+
71
+ // ── Tests ─────────────────────────────────────────────────────────────────────
72
+
73
+ describe("write-tool-enhanced", () => {
74
+ // ── Registration ──────────────────────────────────────────────────────────
75
+
76
+ describe("registration", () => {
77
+ test("registers a tool with name 'write'", async () => {
78
+ const harness = ExtensionHarness.create();
79
+ await harness.loadExtension(writePreview);
80
+
81
+ expect(harness.tools.has("write")).toBe(true);
82
+ });
83
+ });
84
+
85
+ // ── Execute ───────────────────────────────────────────────────────────────
86
+
87
+ describe("execute", () => {
88
+ test("attaches __write_preview__ marker to details", async () => {
89
+ const harness = ExtensionHarness.create();
90
+ await harness.loadExtension(writePreview);
91
+
92
+ const tool = harness.tools.get("write");
93
+ expect(tool).toBeDefined();
94
+ if (!tool) return;
95
+
96
+ const result = await tool.execute(
97
+ "test-id",
98
+ { path: "src/foo.ts", content: "hello\nworld" },
99
+ new AbortController().signal,
100
+ () => {},
101
+ stubContext()
102
+ );
103
+
104
+ const details = result.details as Record<string, unknown>;
105
+ expect(details.__write_preview__).toBe(true);
106
+ });
107
+
108
+ test("stores written content in details._content", async () => {
109
+ const harness = ExtensionHarness.create();
110
+ await harness.loadExtension(writePreview);
111
+
112
+ const tool = harness.tools.get("write")!;
113
+ const content = "const x = 1;\nconst y = 2;";
114
+
115
+ const result = await tool.execute(
116
+ "test-id",
117
+ { path: "src/bar.ts", content },
118
+ new AbortController().signal,
119
+ () => {},
120
+ stubContext()
121
+ );
122
+
123
+ const details = result.details as Record<string, unknown>;
124
+ expect(details._content).toBe(content);
125
+ });
126
+
127
+ test("summary reports correct line count", async () => {
128
+ const harness = ExtensionHarness.create();
129
+ await harness.loadExtension(writePreview);
130
+
131
+ const tool = harness.tools.get("write")!;
132
+ const content = "line1\nline2\nline3"; // 3 lines
133
+
134
+ const result = await tool.execute(
135
+ "test-id",
136
+ { path: "src/three.ts", content },
137
+ new AbortController().signal,
138
+ () => {},
139
+ stubContext()
140
+ );
141
+
142
+ const details = result.details as Record<string, unknown>;
143
+ const summary = details._summary as string;
144
+ expect(summary).toContain("3 lines");
145
+ });
146
+
147
+ test("summary reports correct KB size", async () => {
148
+ const harness = ExtensionHarness.create();
149
+ await harness.loadExtension(writePreview);
150
+
151
+ const tool = harness.tools.get("write")!;
152
+ // 1200 chars → 1200/1024 ≈ 1.171875 → toFixed(1) = "1.2"
153
+ const content = "a".repeat(1200);
154
+ const expectedKb = (content.length / 1024).toFixed(1);
155
+
156
+ const result = await tool.execute(
157
+ "test-id",
158
+ { path: "big.txt", content },
159
+ new AbortController().signal,
160
+ () => {},
161
+ stubContext()
162
+ );
163
+
164
+ const details = result.details as Record<string, unknown>;
165
+ const summary = details._summary as string;
166
+ expect(summary).toContain(`${expectedKb}KB`);
167
+ });
168
+
169
+ test("summary format is 'path (N lines, X.XKB)'", async () => {
170
+ const harness = ExtensionHarness.create();
171
+ await harness.loadExtension(writePreview);
172
+
173
+ const tool = harness.tools.get("write")!;
174
+ const content = "hello\nworld"; // 2 lines, 11 chars
175
+
176
+ const result = await tool.execute(
177
+ "test-id",
178
+ { path: "out/hello.ts", content },
179
+ new AbortController().signal,
180
+ () => {},
181
+ stubContext()
182
+ );
183
+
184
+ const details = result.details as Record<string, unknown>;
185
+ const expectedSizeKb = (content.length / 1024).toFixed(1);
186
+ expect(details._summary).toBe(`out/hello.ts (2 lines, ${expectedSizeKb}KB)`);
187
+ });
188
+
189
+ test("passes through content from base tool result", async () => {
190
+ const harness = ExtensionHarness.create();
191
+ await harness.loadExtension(writePreview);
192
+
193
+ const tool = harness.tools.get("write")!;
194
+
195
+ const result = await tool.execute(
196
+ "test-id",
197
+ { path: "out.txt", content: "data" },
198
+ new AbortController().signal,
199
+ () => {},
200
+ stubContext()
201
+ );
202
+
203
+ expect(result.content).toEqual([{ type: "text", text: "wrote file" }]);
204
+ });
205
+
206
+ test("propagates error thrown by base tool", async () => {
207
+ mockExecute.mockImplementationOnce(async () => {
208
+ throw new Error("disk full");
209
+ });
210
+
211
+ const harness = ExtensionHarness.create();
212
+ await harness.loadExtension(writePreview);
213
+
214
+ const tool = harness.tools.get("write")!;
215
+
216
+ await expect(
217
+ tool.execute(
218
+ "test-id",
219
+ { path: "out.txt", content: "data" },
220
+ new AbortController().signal,
221
+ () => {},
222
+ stubContext()
223
+ )
224
+ ).rejects.toThrow("disk full");
225
+ });
226
+ });
227
+
228
+ // ── renderResult ──────────────────────────────────────────────────────────
229
+
230
+ describe("renderResult", () => {
231
+ test("returns '...' placeholder for partial state", async () => {
232
+ const harness = ExtensionHarness.create();
233
+ await harness.loadExtension(writePreview);
234
+
235
+ const tool = harness.tools.get("write");
236
+ expect(tool?.renderResult).toBeDefined();
237
+ if (!tool?.renderResult) return;
238
+
239
+ const component = tool.renderResult(
240
+ { content: [], details: {} } as never,
241
+ { isPartial: true, expanded: false },
242
+ createMockTheme()
243
+ );
244
+
245
+ const rendered = component.render(200).join("\n");
246
+ expect(rendered).toContain("...");
247
+ });
248
+
249
+ test("falls back to text content when __write_preview__ marker is absent", async () => {
250
+ const harness = ExtensionHarness.create();
251
+ await harness.loadExtension(writePreview);
252
+
253
+ const tool = harness.tools.get("write");
254
+ if (!tool?.renderResult) return;
255
+
256
+ const component = tool.renderResult(
257
+ {
258
+ content: [{ type: "text", text: "fallback output line" }],
259
+ details: {}, // no __write_preview__
260
+ } as never,
261
+ { isPartial: false, expanded: false },
262
+ createMockTheme()
263
+ );
264
+
265
+ const rendered = component.render(200).join("\n");
266
+ expect(rendered).toContain("fallback output line");
267
+ });
268
+
269
+ test("renders content body and success footer when preview marker is present", async () => {
270
+ const harness = ExtensionHarness.create();
271
+ await harness.loadExtension(writePreview);
272
+
273
+ const tool = harness.tools.get("write");
274
+ if (!tool?.renderResult) return;
275
+
276
+ const component = tool.renderResult(
277
+ {
278
+ content: [{ type: "text", text: "wrote file" }],
279
+ details: {
280
+ __write_preview__: true,
281
+ _content: "const answer = 42;",
282
+ _summary: "src/answer.ts (1 lines, 0.0KB)",
283
+ },
284
+ } as never,
285
+ { isPartial: false, expanded: false },
286
+ createMockTheme()
287
+ );
288
+
289
+ const rendered = component.render(200).join("\n");
290
+ // Footer uses success semantic color
291
+ expect(rendered).toContain("<success>");
292
+ // Written content body appears in output
293
+ expect(rendered).toContain("const answer = 42;");
294
+ });
295
+ });
296
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dungle-scrubs/tallow",
3
- "version": "0.8.26",
3
+ "version": "0.8.27",
4
4
  "description": "An opinionated coding agent. Built on pi.",
5
5
  "piConfig": {
6
6
  "name": "tallow",
@@ -53,7 +53,8 @@
53
53
  "format:check": "biome format .",
54
54
  "bench:startup": "bun scripts/benchmark-startup-fast-path.ts",
55
55
  "pre-pr": "bash scripts/pre-pr.sh",
56
- "prepare": "test -d .git && husky || true"
56
+ "prepare": "test -d .git && husky || true",
57
+ "postinstall": "node scripts/patch-upstream-debug.mjs"
57
58
  },
58
59
  "lint-staged": {
59
60
  "*.{ts,tsx,js,jsx,json,jsonc,css}": [
@@ -0,0 +1,13 @@
1
+ import { resolveRuntimeModuleUrl } from "./resolve-module.js";
2
+
3
+ const pidSchemaModule = (await import(
4
+ resolveRuntimeModuleUrl("pid-schema.js")
5
+ )) as typeof import("../src/pid-schema.js");
6
+
7
+ export type PidEntry = import("../src/pid-schema.js").PidEntry;
8
+ export type SessionOwner = import("../src/pid-schema.js").SessionOwner;
9
+ export type SessionPidFile = import("../src/pid-schema.js").SessionPidFile;
10
+
11
+ export const isPidEntry = pidSchemaModule.isPidEntry;
12
+ export const isSessionOwner = pidSchemaModule.isSessionOwner;
13
+ export const toOwnerKey = pidSchemaModule.toOwnerKey;
@@ -33,7 +33,7 @@ Relay that answer to the user.
33
33
 
34
34
  | Component | Location |
35
35
  |-----------|----------|
36
- | Core source | `src/` (agent-runner.ts, atomic-write.ts, auth-hardening.ts, cli-auto-rebuild.ts, cli.ts, compaction-cancel-patch.ts, config.ts, extensions-global.d.ts, fatal-errors.ts, index.ts, install.ts, interactive-mode-patch.ts, model-metadata-overrides.ts, otel.ts, pid-manager.ts, plugins.ts, process-cleanup.ts, project-trust-banner.ts, project-trust-interop.ts, project-trust.ts, runtime-path-provider.ts, runtime-provenance.ts, sdk.ts, session-migration.ts, session-utils.ts, startup-profile.ts, startup-timing.ts, streaming-yield-patch.ts, workspace-transition-interactive.ts, workspace-transition-relay.ts, workspace-transition.ts, yield-to-io.ts) |
36
+ | Core source | `src/` (agent-runner.ts, atomic-write.ts, auth-hardening.ts, cli-auto-rebuild.ts, cli.ts, compaction-cancel-patch.ts, config.ts, extensions-global.d.ts, fatal-errors.ts, index.ts, install.ts, interactive-mode-patch.ts, model-metadata-overrides.ts, otel.ts, pid-manager.ts, pid-schema.ts, plugins.ts, process-cleanup.ts, project-trust-banner.ts, project-trust-interop.ts, project-trust.ts, runtime-path-provider.ts, runtime-provenance.ts, sdk.ts, session-migration.ts, session-utils.ts, startup-profile.ts, startup-timing.ts, streaming-yield-patch.ts, workspace-transition-interactive.ts, workspace-transition-relay.ts, workspace-transition.ts, yield-to-io.ts) |
37
37
  | Extensions | `extensions/` — extension.json + index.ts each (52 bundled) |
38
38
  | Skills | `skills/` — subdirs with SKILL.md |
39
39
  | Agents | `agents/` — markdown with YAML frontmatter |