diff --git a/.cursor/rules/sync-readme.mdc b/.cursor/rules/sync-readme.mdc new file mode 100644 index 0000000..e807006 --- /dev/null +++ b/.cursor/rules/sync-readme.mdc @@ -0,0 +1,67 @@ +# Sync README + +When updating README.md files in this monorepo, follow these conventions. + +## Scope + +- Root `README.md` — project overview and navigation hub +- Per-package `packages/*/README.md` — each package self-contained + +## Root README Structure + +The root README should have these sections in order: + +1. **Title and one-liner** — stateless workflow engine driven by single-step CLI +2. **Overview** — 2-3 paragraphs explaining what it does and key concepts +3. **Architecture** — dependency layer diagram (text-based) +4. **Packages** — table with ALL packages from packages/ directory, columns: Package, Description, Type (cli/lib/agent/app) +5. **Quick Start** — install, build, register workflow, start thread, run step +6. **CLI Reference** — brief command list, detailed usage in cli-workflow README +7. **Development** — bun install / build / check / test + +## Per-Package README Structure + +Each package README should have: + +1. **Title** — package name +2. **One-line description** — matching package.json +3. **Overview** — what it does, where it sits in the architecture, dependencies +4. **Installation** — bun add (for libs) or "included as binary" (for cli/agents) +5. **API** (lib packages) — all exports from src/index.ts with type signatures, grouped by category, minimal usage examples +6. **CLI Usage** (cli/agent packages) — command reference with examples +7. **Internal Structure** — brief src/ file organization +8. **Configuration** (if applicable) + +## Execution Steps + +### Step 1: Gather current state +For each package read: +- package.json (name, version, description, dependencies, bin) +- src/index.ts (public API exports) +- Existing README.md (preserve hand-written content worth keeping) + +### Step 2: Update root README +- Ensure ALL packages in packages/ directory are listed in the table +- Update CLI command reference from uwf --help output +- Keep Quick Start examples valid + +### Step 3: Write/update each package README +- Follow the per-package structure +- API section MUST match actual src/index.ts exports — never invent +- For agent packages: document CLI binary name, how it is invoked +- For lib packages: document exported types and functions +- Internal structure: list actual files in src/ + +### Step 4: Verify +- All relative links work +- Package names match package.json +- No references to removed/renamed packages +- bun run build still passes + +## Guidelines + +- Only document what src/index.ts actually exports +- Root README summarizes, package READMEs go into detail +- Verify CLI examples against actual commands +- Preserve existing good prose when updating +- English for all README content diff --git a/packages/workflow-agent-builtin/__tests__/loop.test.ts b/packages/workflow-agent-builtin/__tests__/loop.test.ts index f2c58dd..c1ea654 100644 --- a/packages/workflow-agent-builtin/__tests__/loop.test.ts +++ b/packages/workflow-agent-builtin/__tests__/loop.test.ts @@ -1,4 +1,4 @@ -import { describe, test, expect, mock, beforeEach } from "bun:test"; +import { beforeEach, describe, expect, mock, test } from "bun:test"; const mockChatCompletionWithTools = mock(async () => ({ content: "---\nstatus: done\n---", @@ -19,7 +19,7 @@ mock.module("../src/tools/index.js", () => ({ getBuiltinTools: () => [], })); -import { shouldNudge, executeTurnTools, runBuiltinLoop } from "../src/loop.js"; +import { executeTurnTools, runBuiltinLoop, shouldNudge } from "../src/loop.js"; const fakeProvider = {} as any; const fakeToolCtx = {} as any; @@ -51,7 +51,9 @@ describe("shouldNudge", () => { expect(shouldNudge({ noTools: true, text: "some text", turn: 0, maxTurns: 5 })).toBe(false); }); test("2.3 returns false when text starts with ---", () => { - expect(shouldNudge({ noTools: false, text: "---\nstatus: done", turn: 0, maxTurns: 5 })).toBe(false); + expect(shouldNudge({ noTools: false, text: "---\nstatus: done", turn: 0, maxTurns: 5 })).toBe( + false, + ); }); test("2.4 returns false on last turn", () => { expect(shouldNudge({ noTools: false, text: "some text", turn: 4, maxTurns: 5 })).toBe(false); @@ -60,7 +62,9 @@ describe("shouldNudge", () => { expect(shouldNudge({ noTools: false, text: "some text", turn: 3, maxTurns: 5 })).toBe(true); }); test("2.6 leading whitespace before --- suppresses nudge", () => { - expect(shouldNudge({ noTools: false, text: " ---\nstatus: done", turn: 0, maxTurns: 5 })).toBe(false); + expect(shouldNudge({ noTools: false, text: " ---\nstatus: done", turn: 0, maxTurns: 5 })).toBe( + false, + ); }); }); @@ -81,27 +85,42 @@ describe("executeTurnTools", () => { test("4.2 tool result content matches executeBuiltinTool return value", async () => { mockExecuteBuiltinTool.mockResolvedValue("result-A"); const messages: any[] = []; - await executeTurnTools([{ id: "c1", name: "read_file", arguments: "{}" }], fakeToolCtx, messages, "/tmp", "sess"); + await executeTurnTools( + [{ id: "c1", name: "read_file", arguments: "{}" }], + fakeToolCtx, + messages, + "/tmp", + "sess", + ); expect(messages[0].content).toBe("result-A"); }); }); describe("runBuiltinLoop integration", () => { test("3.1 single text-only response returns finalText immediately", async () => { - mockChatCompletionWithTools.mockResolvedValue({ content: "---\nstatus: done\n---", toolCalls: [] }); + mockChatCompletionWithTools.mockResolvedValue({ + content: "---\nstatus: done\n---", + toolCalls: [], + }); const result = await runBuiltinLoop(makeOptions()); expect(result.finalText).toBe("---\nstatus: done\n---"); expect(result.turnCount).toBe(1); }); test("3.2 noTools=true suppresses tool calls", async () => { - mockChatCompletionWithTools.mockResolvedValue({ content: "ok", toolCalls: [{ id: "c1", name: "read_file", arguments: "{}" }] }); + mockChatCompletionWithTools.mockResolvedValue({ + content: "ok", + toolCalls: [{ id: "c1", name: "read_file", arguments: "{}" }], + }); const result = await runBuiltinLoop(makeOptions({ noTools: true })); expect(result.finalText).toBe("ok"); expect(result.turnCount).toBe(1); }); test("3.3 tool call followed by text response", async () => { mockChatCompletionWithTools - .mockResolvedValueOnce({ content: null, toolCalls: [{ id: "c1", name: "read_file", arguments: "{}" }] }) + .mockResolvedValueOnce({ + content: null, + toolCalls: [{ id: "c1", name: "read_file", arguments: "{}" }], + }) .mockResolvedValueOnce({ content: "---\nstatus: done\n---", toolCalls: [] }); mockExecuteBuiltinTool.mockResolvedValue("file contents"); const result = await runBuiltinLoop(makeOptions()); @@ -115,7 +134,8 @@ describe("runBuiltinLoop integration", () => { const result = await runBuiltinLoop(makeOptions()); expect(result.finalText).toBe("---\nstatus: done\n---"); const nudgeMsg = result.messages.find( - (m) => m.role === "user" && typeof m.content === "string" && m.content.includes("frontmatter"), + (m) => + m.role === "user" && typeof m.content === "string" && m.content.includes("frontmatter"), ); expect(nudgeMsg).toBeDefined(); }); @@ -125,7 +145,10 @@ describe("runBuiltinLoop integration", () => { expect(result.finalText).toBe("still thinking"); }); test("3.6 original messages array is not mutated", async () => { - mockChatCompletionWithTools.mockResolvedValue({ content: "---\nstatus: done\n---", toolCalls: [] }); + mockChatCompletionWithTools.mockResolvedValue({ + content: "---\nstatus: done\n---", + toolCalls: [], + }); const original = [{ role: "system" as const, content: "sys" }]; await runBuiltinLoop(makeOptions({ messages: original })); expect(original.length).toBe(1);