docs: add sync-readme rule for consistent README updates
小橘 🍊(NEKO Team)
This commit is contained in:
@@ -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
|
||||||
@@ -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 () => ({
|
const mockChatCompletionWithTools = mock(async () => ({
|
||||||
content: "---\nstatus: done\n---",
|
content: "---\nstatus: done\n---",
|
||||||
@@ -19,7 +19,7 @@ mock.module("../src/tools/index.js", () => ({
|
|||||||
getBuiltinTools: () => [],
|
getBuiltinTools: () => [],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import { shouldNudge, executeTurnTools, runBuiltinLoop } from "../src/loop.js";
|
import { executeTurnTools, runBuiltinLoop, shouldNudge } from "../src/loop.js";
|
||||||
|
|
||||||
const fakeProvider = {} as any;
|
const fakeProvider = {} as any;
|
||||||
const fakeToolCtx = {} 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);
|
expect(shouldNudge({ noTools: true, text: "some text", turn: 0, maxTurns: 5 })).toBe(false);
|
||||||
});
|
});
|
||||||
test("2.3 returns false when text starts with ---", () => {
|
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", () => {
|
test("2.4 returns false on last turn", () => {
|
||||||
expect(shouldNudge({ noTools: false, text: "some text", turn: 4, maxTurns: 5 })).toBe(false);
|
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);
|
expect(shouldNudge({ noTools: false, text: "some text", turn: 3, maxTurns: 5 })).toBe(true);
|
||||||
});
|
});
|
||||||
test("2.6 leading whitespace before --- suppresses nudge", () => {
|
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 () => {
|
test("4.2 tool result content matches executeBuiltinTool return value", async () => {
|
||||||
mockExecuteBuiltinTool.mockResolvedValue("result-A");
|
mockExecuteBuiltinTool.mockResolvedValue("result-A");
|
||||||
const messages: any[] = [];
|
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");
|
expect(messages[0].content).toBe("result-A");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("runBuiltinLoop integration", () => {
|
describe("runBuiltinLoop integration", () => {
|
||||||
test("3.1 single text-only response returns finalText immediately", async () => {
|
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());
|
const result = await runBuiltinLoop(makeOptions());
|
||||||
expect(result.finalText).toBe("---\nstatus: done\n---");
|
expect(result.finalText).toBe("---\nstatus: done\n---");
|
||||||
expect(result.turnCount).toBe(1);
|
expect(result.turnCount).toBe(1);
|
||||||
});
|
});
|
||||||
test("3.2 noTools=true suppresses tool calls", async () => {
|
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 }));
|
const result = await runBuiltinLoop(makeOptions({ noTools: true }));
|
||||||
expect(result.finalText).toBe("ok");
|
expect(result.finalText).toBe("ok");
|
||||||
expect(result.turnCount).toBe(1);
|
expect(result.turnCount).toBe(1);
|
||||||
});
|
});
|
||||||
test("3.3 tool call followed by text response", async () => {
|
test("3.3 tool call followed by text response", async () => {
|
||||||
mockChatCompletionWithTools
|
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: [] });
|
.mockResolvedValueOnce({ content: "---\nstatus: done\n---", toolCalls: [] });
|
||||||
mockExecuteBuiltinTool.mockResolvedValue("file contents");
|
mockExecuteBuiltinTool.mockResolvedValue("file contents");
|
||||||
const result = await runBuiltinLoop(makeOptions());
|
const result = await runBuiltinLoop(makeOptions());
|
||||||
@@ -115,7 +134,8 @@ describe("runBuiltinLoop integration", () => {
|
|||||||
const result = await runBuiltinLoop(makeOptions());
|
const result = await runBuiltinLoop(makeOptions());
|
||||||
expect(result.finalText).toBe("---\nstatus: done\n---");
|
expect(result.finalText).toBe("---\nstatus: done\n---");
|
||||||
const nudgeMsg = result.messages.find(
|
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();
|
expect(nudgeMsg).toBeDefined();
|
||||||
});
|
});
|
||||||
@@ -125,7 +145,10 @@ describe("runBuiltinLoop integration", () => {
|
|||||||
expect(result.finalText).toBe("still thinking");
|
expect(result.finalText).toBe("still thinking");
|
||||||
});
|
});
|
||||||
test("3.6 original messages array is not mutated", async () => {
|
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" }];
|
const original = [{ role: "system" as const, content: "sys" }];
|
||||||
await runBuiltinLoop(makeOptions({ messages: original }));
|
await runBuiltinLoop(makeOptions({ messages: original }));
|
||||||
expect(original.length).toBe(1);
|
expect(original.length).toBe(1);
|
||||||
|
|||||||
Reference in New Issue
Block a user