diff --git a/package-lock.json b/package-lock.json index b016ff6..bda3fcf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18682,6 +18682,26 @@ "peerDependencies": { "zod": "^3.25.28 || ^4" } + }, + "node_modules/@iurysouza/pi-wtf": { + "resolved": "packages/pi-wtf", + "link": true + }, + "packages/pi-wtf": { + "name": "@iurysza/pi-wtf", + "version": "0.1.0", + "license": "MIT", + "devDependencies": { + "@earendil-works/pi-coding-agent": "^0.80.10", + "@types/node": "^24.12.0", + "typescript": "^5.9.3" + }, + "engines": { + "node": ">=22.19.0" + }, + "peerDependencies": { + "@earendil-works/pi-coding-agent": "*" + } } } } diff --git a/package.json b/package.json index e5b9824..f2765cf 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "./packages/pi-agent-explorer/extensions/agent-explorer.ts", "./packages/pi-token-tank/src/index.ts", "./packages/pi-context-audit/src/index.ts", + "./packages/pi-wtf/src/index.ts", "./packages/pi-secret-env/src/index.ts", "./packages/pi-ghost-in-the-machine/src/index.ts", "./packages/artifact-explorer/index.ts" diff --git a/packages/pi-wtf/LICENSE b/packages/pi-wtf/LICENSE new file mode 100644 index 0000000..76f826a --- /dev/null +++ b/packages/pi-wtf/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Iury Souza + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/pi-wtf/README.md b/packages/pi-wtf/README.md new file mode 100644 index 0000000..e7fad73 --- /dev/null +++ b/packages/pi-wtf/README.md @@ -0,0 +1,21 @@ +# @iurysza/pi-wtf + +A small Pi extension for preserving repeated or systemic workflow friction in the current workspace. + +## Tool + +### `wtf` + +Accepts one required `note` and appends it under an ISO timestamp in `WTF.md`. The first write creates the file with a `# WTF` heading. + +Use it for recurring process or tooling problems worth fixing. Do not use it for one-off failures, status updates, or general notes. + +## Install + +From a clone of the `pi-extensions` repository: + +```bash +pi install /absolute/path/to/pi-extensions/packages/pi-wtf +``` + +Reload Pi after installation with `/reload`. diff --git a/packages/pi-wtf/package.json b/packages/pi-wtf/package.json new file mode 100644 index 0000000..b43d3f4 --- /dev/null +++ b/packages/pi-wtf/package.json @@ -0,0 +1,33 @@ +{ + "name": "@iurysza/pi-wtf", + "version": "0.1.0", + "description": "Record repeated workflow friction in a workspace WTF.md file.", + "author": "Iury Souza", + "license": "MIT", + "type": "module", + "keywords": ["pi-package", "pi-extension", "workflow", "friction"], + "files": ["src", "README.md", "LICENSE"], + "publishConfig": { "access": "public" }, + "repository": { + "type": "git", + "url": "git+https://github.com/iurysza/pi-extensions.git", + "directory": "packages/pi-wtf" + }, + "bugs": { "url": "https://github.com/iurysza/pi-extensions/issues" }, + "homepage": "https://github.com/iurysza/pi-extensions/tree/main/packages/pi-wtf#readme", + "engines": { "node": ">=22.19.0" }, + "peerDependencies": { + "@earendil-works/pi-coding-agent": "*" + }, + "devDependencies": { + "@earendil-works/pi-coding-agent": "^0.80.10", + "@types/node": "^24.12.0", + "typescript": "^5.9.3" + }, + "scripts": { + "test": "node --test tests/*.test.mjs", + "typecheck": "tsc --noEmit -p tsconfig.json", + "check": "npm run typecheck && npm test" + }, + "pi": { "extensions": ["./src/index.ts"] } +} diff --git a/packages/pi-wtf/src/index.ts b/packages/pi-wtf/src/index.ts new file mode 100644 index 0000000..bcc46d8 --- /dev/null +++ b/packages/pi-wtf/src/index.ts @@ -0,0 +1,65 @@ +import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; +import { appendFile, readFile } from "node:fs/promises"; +import path from "node:path"; + +const FILE_NAME = "WTF.md"; + +export async function recordWtf(cwd: string, note: string, now: Date = new Date()): Promise { + const normalized = note.trim(); + if (!normalized) throw new Error("note must not be empty"); + + const filePath = path.join(cwd, FILE_NAME); + let prefix = ""; + try { + const existing = await readFile(filePath, "utf8"); + if (existing.length > 0 && !existing.endsWith("\n")) prefix = "\n"; + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; + prefix = "# WTF\n\n"; + } + + const entry = `## ${now.toISOString()}\n\n${normalized}\n\n`; + await appendFile(filePath, `${prefix}${entry}`, "utf8"); + return filePath; +} + +export default function piWtf(pi: ExtensionAPI): void { + pi.registerTool({ + name: "wtf", + label: "WTF", + description: "Record repeated or systemic workflow friction in the current workspace's WTF.md. Use only for recurring process/tooling problems worth fixing, not one-off errors or general notes.", + promptSnippet: "wtf — record repeated/systemic workflow friction in WTF.md", + promptGuidelines: [ + "Use wtf when repeated or systemic workflow friction should be preserved for later improvement.", + "Do not use wtf for one-off errors, task status, or general notes.", + ], + parameters: { + type: "object", + properties: { + note: { + type: "string", + minLength: 1, + description: "Concise friction report: what repeats, its impact, and useful context.", + }, + }, + required: ["note"], + additionalProperties: false, + } as any, + async execute(_toolCallId, params, _signal, _onUpdate, ctx) { + try { + const { note } = params as { note: string }; + const filePath = await recordWtf(ctx.cwd, note); + return { + content: [{ type: "text" as const, text: `Recorded workflow friction in ${filePath}` }], + details: { path: filePath }, + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + return { + content: [{ type: "text" as const, text: `Failed to record workflow friction: ${message}` }], + details: { error: true }, + }; + } + }, + }); +} diff --git a/packages/pi-wtf/tests/wtf.test.mjs b/packages/pi-wtf/tests/wtf.test.mjs new file mode 100644 index 0000000..e1c7e9e --- /dev/null +++ b/packages/pi-wtf/tests/wtf.test.mjs @@ -0,0 +1,38 @@ +import assert from "node:assert/strict"; +import { mkdtemp, readFile, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import test from "node:test"; + +import piWtf, { recordWtf } from "../src/index.ts"; + +async function withWorkspace(run) { + const cwd = await mkdtemp(path.join(tmpdir(), "pi-wtf-")); + try { await run(cwd); } finally { await rm(cwd, { recursive: true, force: true }); } +} + +test("registers the wtf tool", () => { + let tool; + piWtf({ registerTool(value) { tool = value; } }); + assert.equal(tool.name, "wtf"); + assert.match(tool.description, /WTF\.md/); +}); + +test("creates WTF.md and appends timestamped friction", async () => { + await withWorkspace(async (cwd) => { + await recordWtf(cwd, " Search repeatedly misses generated files. ", new Date("2026-04-01T10:00:00.000Z")); + await recordWtf(cwd, "Need to retry the same setup after every reload.", new Date("2026-04-01T11:00:00.000Z")); + const content = await readFile(path.join(cwd, "WTF.md"), "utf8"); + assert.equal(content.match(/^# WTF$/gm)?.length, 1); + assert.match(content, /## 2026-04-01T10:00:00\.000Z/); + assert.match(content, /Search repeatedly misses generated files\./); + assert.match(content, /## 2026-04-01T11:00:00\.000Z/); + }); +}); + +test("rejects empty notes without creating WTF.md", async () => { + await withWorkspace(async (cwd) => { + await assert.rejects(recordWtf(cwd, " "), /must not be empty/); + await assert.rejects(readFile(path.join(cwd, "WTF.md")), { code: "ENOENT" }); + }); +}); diff --git a/packages/pi-wtf/tsconfig.json b/packages/pi-wtf/tsconfig.json new file mode 100644 index 0000000..0639c89 --- /dev/null +++ b/packages/pi-wtf/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "noEmit": true, + "skipLibCheck": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"] +}