For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: 让 sub_agent 的 5 个子智能体参数与 pipeline runner 方法完全对齐,消除硬编码和参数丢弃。
Architecture: 在现有 SubAgentParams schema 上扁平扩展新字段(TypeBox description 标注适用 agent),修改每个 agent case 的调用代码,更新系统提示词。
Tech Stack: TypeBox (schema), pi-agent-core (tool interface), vitest (tests)
Files:
packages/core/src/agent/agent-tools.ts:32-43Test: packages/core/src/__tests__/agent-tools-params.test.ts (create)
[ ] Step 1: Write the failing test
// packages/core/src/__tests__/agent-tools-params.test.ts
import { describe, it, expect } from "vitest";
import { Type } from "@mariozechner/pi-ai";
// We test the schema shape by importing and inspecting the compiled schema
// Since SubAgentParams is not exported, we test via the tool's parameters property
import { createSubAgentTool } from "../agent/agent-tools.js";
describe("SubAgentParams schema", () => {
// Use a minimal mock pipeline
const mockPipeline = {} as any;
const tool = createSubAgentTool(mockPipeline, null);
const schema = tool.parameters;
const props = (schema as any).properties;
it("has architect params: title, genre, platform, language, targetChapters", () => {
expect(props.title).toBeDefined();
expect(props.genre).toBeDefined();
expect(props.platform).toBeDefined();
expect(props.language).toBeDefined();
expect(props.targetChapters).toBeDefined();
});
it("has writer/architect param: chapterWordCount", () => {
expect(props.chapterWordCount).toBeDefined();
});
it("has reviser param: mode", () => {
expect(props.mode).toBeDefined();
});
it("has exporter params: format, approvedOnly", () => {
expect(props.format).toBeDefined();
expect(props.approvedOnly).toBeDefined();
});
it("has existing params: agent, instruction, bookId, chapterNumber", () => {
expect(props.agent).toBeDefined();
expect(props.instruction).toBeDefined();
expect(props.bookId).toBeDefined();
expect(props.chapterNumber).toBeDefined();
});
it("all new params have description with agent scope", () => {
expect(props.title.description).toMatch(/architect/i);
expect(props.genre.description).toMatch(/architect/i);
expect(props.mode.description).toMatch(/reviser/i);
expect(props.format.description).toMatch(/exporter/i);
});
});
[ ] Step 2: Run test to verify it fails
Run: pnpm --filter @actalk/inkos-core test -- --run agent-tools-params
Expected: FAIL — props.title is undefined (field doesn't exist yet)
In packages/core/src/agent/agent-tools.ts, replace the SubAgentParams definition:
const SubAgentParams = Type.Object({
agent: Type.Union([
Type.Literal("architect"),
Type.Literal("writer"),
Type.Literal("auditor"),
Type.Literal("reviser"),
Type.Literal("exporter"),
]),
instruction: Type.String({ description: "Natural language instruction for the sub-agent" }),
bookId: Type.Optional(Type.String({ description: "Book ID — required for all agents except architect" })),
chapterNumber: Type.Optional(Type.Number({ description: "auditor/reviser: target chapter number. Omit to use the latest chapter." })),
// -- architect params --
title: Type.Optional(Type.String({ description: "architect only: book title" })),
genre: Type.Optional(Type.String({ description: "architect only: genre (xuanhuan, urban, mystery, romance, scifi, fantasy, wuxia, general, etc.)" })),
platform: Type.Optional(Type.Union([
Type.Literal("tomato"),
Type.Literal("qidian"),
Type.Literal("feilu"),
Type.Literal("other"),
], { description: "architect only: target platform. Default: other" })),
language: Type.Optional(Type.Union([
Type.Literal("zh"),
Type.Literal("en"),
], { description: "architect only: writing language. Default: zh" })),
targetChapters: Type.Optional(Type.Number({ description: "architect only: total chapter count. Default: 200" })),
chapterWordCount: Type.Optional(Type.Number({ description: "architect/writer: words per chapter. Default: 3000" })),
// -- reviser params --
mode: Type.Optional(Type.Union([
Type.Literal("spot-fix"),
Type.Literal("polish"),
Type.Literal("rewrite"),
Type.Literal("rework"),
Type.Literal("anti-detect"),
], { description: "reviser only: revision mode. Default: spot-fix" })),
// -- exporter params --
format: Type.Optional(Type.Union([
Type.Literal("txt"),
Type.Literal("md"),
Type.Literal("epub"),
], { description: "exporter only: export format. Default: txt" })),
approvedOnly: Type.Optional(Type.Boolean({ description: "exporter only: export only approved chapters. Default: false" })),
});
Update the destructuring in execute:
const { agent, instruction, bookId, chapterNumber, title, genre, platform, language, targetChapters, chapterWordCount, mode, format, approvedOnly } = params;
Run: pnpm --filter @actalk/inkos-core test -- --run agent-tools-params
Expected: PASS
[ ] Step 5: Commit
git add packages/core/src/agent/agent-tools.ts packages/core/src/__tests__/agent-tools-params.test.ts
git commit -m "feat(core): extend SubAgentParams with architect/writer/reviser/exporter fields"
Files:
packages/core/src/agent/agent-tools.ts:67-81 (architect case)Test: packages/core/src/__tests__/agent-tools-params.test.ts (extend)
[ ] Step 1: Write the failing test
Append to agent-tools-params.test.ts:
import { mkdtemp, rm, mkdir, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { vi, beforeEach, afterEach } from "vitest";
describe("architect agent — BookConfig construction", () => {
let initBookMock: ReturnType<typeof vi.fn>;
let tool: ReturnType<typeof createSubAgentTool>;
beforeEach(() => {
initBookMock = vi.fn(async () => {});
const mockPipeline = { initBook: initBookMock } as any;
tool = createSubAgentTool(mockPipeline, null);
});
it("passes complete BookConfig with schema params instead of hardcoded values", async () => {
await tool.execute("tc1", {
agent: "architect",
instruction: "Create a xuanhuan novel",
title: "天道独行",
genre: "xuanhuan",
platform: "tomato",
language: "zh",
targetChapters: 100,
chapterWordCount: 4000,
});
expect(initBookMock).toHaveBeenCalledOnce();
const [bookConfig, options] = initBookMock.mock.calls[0];
expect(bookConfig.title).toBe("天道独行");
expect(bookConfig.genre).toBe("xuanhuan");
expect(bookConfig.platform).toBe("tomato");
expect(bookConfig.language).toBe("zh");
expect(bookConfig.targetChapters).toBe(100);
expect(bookConfig.chapterWordCount).toBe(4000);
expect(bookConfig.status).toBe("outlining");
expect(bookConfig.createdAt).toBeDefined();
expect(bookConfig.updatedAt).toBeDefined();
expect(options.externalContext).toBe("Create a xuanhuan novel");
});
it("uses defaults when optional architect params are omitted", async () => {
await tool.execute("tc2", {
agent: "architect",
instruction: "Create a book",
});
const [bookConfig] = initBookMock.mock.calls[0];
expect(bookConfig.genre).toBe("general");
expect(bookConfig.platform).toBe("other");
expect(bookConfig.language).toBe("zh");
expect(bookConfig.targetChapters).toBe(200);
expect(bookConfig.chapterWordCount).toBe(3000);
});
});
Run: pnpm --filter @actalk/inkos-core test -- --run agent-tools-params
Expected: FAIL — bookConfig.title is "", bookConfig.genre is "general" (hardcoded)
Replace the architect case in agent-tools.ts:
case "architect": {
if (activeBookId) {
return textResult("当前已有书籍,不需要建书。如果你想创建新书,请先回到首页。");
}
const id = bookId || `book-${Date.now().toString(36)}`;
const now = new Date().toISOString();
progress(`Starting architect for book "${id}"...`);
await pipeline.initBook(
{
id,
title: title ?? "",
genre: genre ?? "general",
platform: (platform ?? "other") as any,
language: (language ?? "zh") as any,
status: "outlining" as any,
targetChapters: targetChapters ?? 200,
chapterWordCount: chapterWordCount ?? 3000,
createdAt: now,
updatedAt: now,
},
{ externalContext: instruction },
);
progress(`Architect finished — book "${id}" foundation created.`);
return textResult(`Book "${id}" initialised successfully. Foundation files are ready.`);
}
Run: pnpm --filter @actalk/inkos-core test -- --run agent-tools-params
Expected: PASS
[ ] Step 5: Commit
git add packages/core/src/agent/agent-tools.ts packages/core/src/__tests__/agent-tools-params.test.ts
git commit -m "fix(core): architect builds complete BookConfig from schema params"
Files:
packages/core/src/agent/agent-tools.ts:83-91 (writer case)Test: packages/core/src/__tests__/agent-tools-params.test.ts (extend)
[ ] Step 1: Write the failing test
describe("writer agent — wordCount and instruction passthrough", () => {
let writeNextChapterMock: ReturnType<typeof vi.fn>;
let tool: ReturnType<typeof createSubAgentTool>;
beforeEach(() => {
writeNextChapterMock = vi.fn(async () => ({ wordCount: 3000 }));
const mockPipeline = { writeNextChapter: writeNextChapterMock } as any;
tool = createSubAgentTool(mockPipeline, "existing-book");
});
it("passes chapterWordCount as wordCount to writeNextChapter", async () => {
await tool.execute("tc1", {
agent: "writer",
instruction: "Write an exciting chapter",
bookId: "my-book",
chapterWordCount: 5000,
});
expect(writeNextChapterMock).toHaveBeenCalledWith("my-book", 5000);
});
it("passes undefined wordCount when chapterWordCount is omitted", async () => {
await tool.execute("tc2", {
agent: "writer",
instruction: "Write next chapter",
bookId: "my-book",
});
expect(writeNextChapterMock).toHaveBeenCalledWith("my-book", undefined);
});
});
[ ] Step 2: Run test to verify it fails
Expected: FAIL — writeNextChapter called with only 1 argument
[ ] Step 3: Update writer case
case "writer": {
if (!bookId) return textResult("Error: bookId is required for the writer agent.");
progress(`Writing next chapter for "${bookId}"...`);
const result = await pipeline.writeNextChapter(bookId, chapterWordCount);
progress(`Writer finished chapter for "${bookId}".`);
return textResult(
`Chapter written for "${bookId}". ` +
`Word count: ${(result as any).wordCount ?? "unknown"}.`,
);
}
[ ] Step 4: Run test to verify it passes
Run: pnpm --filter @actalk/inkos-core test -- --run agent-tools-params
Expected: PASS
[ ] Step 5: Commit
git add packages/core/src/agent/agent-tools.ts packages/core/src/__tests__/agent-tools-params.test.ts
git commit -m "fix(core): writer passes chapterWordCount to writeNextChapter"
Files:
packages/core/src/agent/agent-tools.ts:94-103 (auditor case)Test: packages/core/src/__tests__/agent-tools-params.test.ts (extend)
[ ] Step 1: Write the failing test
describe("auditor agent — rich return value", () => {
let auditDraftMock: ReturnType<typeof vi.fn>;
let tool: ReturnType<typeof createSubAgentTool>;
beforeEach(() => {
auditDraftMock = vi.fn(async () => ({
chapterNumber: 3,
passed: false,
issues: [
{ severity: "warning", description: "Pacing too fast in mid section" },
{ severity: "critical", description: "Character name inconsistency" },
],
}));
const mockPipeline = { auditDraft: auditDraftMock } as any;
tool = createSubAgentTool(mockPipeline, "existing-book");
});
it("returns issue details with severity, not just count", async () => {
const result = await tool.execute("tc1", {
agent: "auditor",
instruction: "Audit chapter 3",
bookId: "my-book",
chapterNumber: 3,
});
const text = result.content[0].text;
expect(text).toContain("FAILED");
expect(text).toContain("2 issue(s)");
expect(text).toContain("[warning]");
expect(text).toContain("[critical]");
expect(text).toContain("Pacing too fast");
expect(text).toContain("Character name inconsistency");
});
});
[ ] Step 2: Run test to verify it fails
Expected: FAIL — current response only has "2 issue(s) found", no [warning] or [critical]
[ ] Step 3: Update auditor case
case "auditor": {
if (!bookId) return textResult("Error: bookId is required for the auditor agent.");
progress(`Auditing chapter ${chapterNumber ?? "latest"} for "${bookId}"...`);
const audit = await pipeline.auditDraft(bookId, chapterNumber);
progress(`Audit complete for "${bookId}".`);
const issueLines = (audit.issues ?? [])
.map((i: any) => `[${i.severity}] ${i.description}`)
.join("\n");
return textResult(
`Audit chapter ${audit.chapterNumber}: ${audit.passed ? "PASSED" : "FAILED"}, ${(audit.issues ?? []).length} issue(s).` +
(issueLines ? `\n${issueLines}` : ""),
);
}
[ ] Step 4: Run test to verify it passes
Expected: PASS
[ ] Step 5: Commit
git add packages/core/src/agent/agent-tools.ts packages/core/src/__tests__/agent-tools-params.test.ts
git commit -m "fix(core): auditor returns full issue details with severity"
Files:
packages/core/src/agent/agent-tools.ts:106-119 (reviser case)Test: packages/core/src/__tests__/agent-tools-params.test.ts (extend)
[ ] Step 1: Write the failing test
describe("reviser agent — mode field and instruction passthrough", () => {
let reviseDraftMock: ReturnType<typeof vi.fn>;
let tool: ReturnType<typeof createSubAgentTool>;
beforeEach(() => {
reviseDraftMock = vi.fn(async () => ({}));
const mockPipeline = { reviseDraft: reviseDraftMock } as any;
tool = createSubAgentTool(mockPipeline, "existing-book");
});
it("uses mode param directly instead of regex extraction", async () => {
await tool.execute("tc1", {
agent: "reviser",
instruction: "Fix this chapter",
bookId: "my-book",
chapterNumber: 5,
mode: "anti-detect",
});
expect(reviseDraftMock).toHaveBeenCalledWith("my-book", 5, "anti-detect");
});
it("defaults to spot-fix when mode is omitted", async () => {
await tool.execute("tc2", {
agent: "reviser",
instruction: "Fix chapter",
bookId: "my-book",
});
expect(reviseDraftMock).toHaveBeenCalledWith("my-book", undefined, "spot-fix");
});
it("anti-detect mode is reachable (was unreachable with regex)", async () => {
await tool.execute("tc3", {
agent: "reviser",
instruction: "Process chapter",
bookId: "my-book",
mode: "anti-detect",
});
expect(reviseDraftMock.mock.calls[0][2]).toBe("anti-detect");
});
});
[ ] Step 2: Run test to verify it fails
Expected: FAIL — anti-detect falls through to spot-fix via regex
[ ] Step 3: Update reviser case
case "reviser": {
if (!bookId) return textResult("Error: bookId is required for the reviser agent.");
const resolvedMode: ReviseMode = (mode as ReviseMode) ?? "spot-fix";
progress(`Revising "${bookId}" chapter ${chapterNumber ?? "latest"} in ${resolvedMode} mode...`);
await pipeline.reviseDraft(bookId, chapterNumber, resolvedMode);
progress(`Revision complete for "${bookId}".`);
return textResult(`Revision (${resolvedMode}) complete for "${bookId}" chapter ${chapterNumber ?? "latest"}.`);
}
[ ] Step 4: Run test to verify it passes
Expected: PASS
[ ] Step 5: Commit
git add packages/core/src/agent/agent-tools.ts packages/core/src/__tests__/agent-tools-params.test.ts
git commit -m "fix(core): reviser uses mode param directly, anti-detect now reachable"
Files:
packages/core/src/agent/agent-tools.ts:122-124 (exporter case)Test: packages/core/src/__tests__/agent-tools-params.test.ts (extend)
[ ] Step 1: Write the failing test
describe("exporter agent — export implementation", () => {
let loadChapterIndexMock: ReturnType<typeof vi.fn>;
let loadBookConfigMock: ReturnType<typeof vi.fn>;
let tool: ReturnType<typeof createSubAgentTool>;
let tmpRoot: string;
beforeEach(async () => {
tmpRoot = await mkdtemp(join(tmpdir(), "inkos-export-"));
const bookDir = join(tmpRoot, "books", "my-book");
const chaptersDir = join(bookDir, "chapters");
await mkdir(chaptersDir, { recursive: true });
await writeFile(join(chaptersDir, "0001_First.md"), "# 第一章\n\n内容内容内容");
await writeFile(join(chaptersDir, "0002_Second.md"), "# 第二章\n\n更多内容");
loadChapterIndexMock = vi.fn(async () => [
{ number: 1, title: "First", status: "approved", wordCount: 100 },
{ number: 2, title: "Second", status: "drafted", wordCount: 200 },
]);
loadBookConfigMock = vi.fn(async () => ({ title: "Test Book" }));
const mockPipeline = {} as any;
const mockState = {
bookDir: (id: string) => join(tmpRoot, "books", id),
loadChapterIndex: loadChapterIndexMock,
loadBookConfig: loadBookConfigMock,
};
// exporter needs access to state — passed via projectRoot
tool = createSubAgentTool(mockPipeline, "my-book", tmpRoot);
});
afterEach(async () => {
await rm(tmpRoot, { recursive: true, force: true });
});
it("exports all chapters in txt format by default", async () => {
const result = await tool.execute("tc1", {
agent: "exporter",
instruction: "Export the book",
bookId: "my-book",
});
const text = result.content[0].text;
expect(text).not.toContain("not yet implemented");
expect(text).toContain("export");
});
});
[ ] Step 2: Run test to verify it fails
Expected: FAIL — returns "Export is not yet implemented"
Replace the exporter case. Since exportBookToPath in project-tools.ts is not directly importable (it's a local function), inline the core logic:
case "exporter": {
if (!bookId) return textResult("Error: bookId is required for the exporter agent.");
const exportFormat = (format ?? "txt") as "txt" | "md" | "epub";
const onlyApproved = approvedOnly ?? false;
progress(`Exporting "${bookId}" as ${exportFormat}...`);
const booksRoot = join(projectRoot, "books");
const bookDir = join(booksRoot, bookId);
const chaptersDir = join(bookDir, "chapters");
// Load index and book config
const indexRaw = await readFile(join(chaptersDir, "index.json"), "utf-8");
const index: Array<{ number: number; title: string; status: string; wordCount: number }> = JSON.parse(indexRaw);
const bookRaw = await readFile(join(bookDir, "book.json"), "utf-8");
const bookConfig = JSON.parse(bookRaw);
const chapters = onlyApproved ? index.filter((ch) => ch.status === "approved") : index;
if (chapters.length === 0) {
return textResult(`No chapters to export${onlyApproved ? " (approved only filter)" : ""}.`);
}
// Build chapter file lookup
const files = await readdir(chaptersDir);
const chapterFiles = new Map<number, string>();
for (const f of files.filter((f) => /^\d{4}_.*\.md$/.test(f)).sort()) {
chapterFiles.set(parseInt(f.slice(0, 4), 10), f);
}
const parts: string[] = [];
parts.push(exportFormat === "md" ? `# ${bookConfig.title ?? bookId}\n\n---\n` : `${bookConfig.title ?? bookId}\n\n`);
for (const ch of chapters) {
const file = chapterFiles.get(ch.number);
if (!file) continue;
parts.push(await readFile(join(chaptersDir, file), "utf-8"));
parts.push("\n\n");
}
const outputPath = join(booksRoot, "..", `${bookId}_export.${exportFormat}`);
await writeFile(outputPath, parts.join(exportFormat === "md" ? "\n---\n\n" : "\n"), "utf-8");
const totalWords = chapters.reduce((s, ch) => s + ch.wordCount, 0);
progress(`Export complete for "${bookId}".`);
return textResult(
`Exported "${bookId}": ${chapters.length} chapters, ${totalWords} words → ${outputPath}`,
);
}
Note: projectRoot is already available in the closure from createSubAgentTool's third parameter (added in a prior commit).
Expected: PASS
[ ] Step 5: Commit
git add packages/core/src/agent/agent-tools.ts packages/core/src/__tests__/agent-tools-params.test.ts
git commit -m "feat(core): implement exporter agent with format and approvedOnly support"
Files:
Modify: packages/core/src/agent/agent-system-prompt.ts
[ ] Step 1: Write the failing test
Append to packages/core/src/__tests__/agent-system-prompt.test.ts:
it("book-mode prompt documents all sub_agent params", () => {
const prompt = buildAgentSystemPrompt("test-book", "zh");
// architect params
expect(prompt).toContain("title");
expect(prompt).toContain("genre");
expect(prompt).toContain("platform");
expect(prompt).toContain("chapterWordCount");
// reviser params
expect(prompt).toContain("mode");
expect(prompt).toContain("anti-detect");
// exporter params
expect(prompt).toContain("format");
expect(prompt).toContain("approvedOnly");
});
Run: pnpm --filter @actalk/inkos-core test -- --run agent-system-prompt
Expected: FAIL — prompt doesn't mention format, approvedOnly, anti-detect etc.
Update the Chinese book-mode prompt's tool section:
- **sub_agent** — 委托子智能体执行重操作:
- agent="architect" 从零建书(参数:title, genre, platform, language, targetChapters, chapterWordCount)
- agent="writer" 写下一章(参数:chapterWordCount 覆盖字数)
- agent="auditor" 审计章节质量(参数:chapterNumber 指定章节)
- agent="reviser" 修订章节(参数:chapterNumber, mode: spot-fix/polish/rewrite/rework/anti-detect)
- agent="exporter" 导出书籍(参数:format: txt/md/epub, approvedOnly: true/false)
Update the English book-mode prompt similarly.
Also update the Chinese book-create prompt to mention architect params:
2. **确认建书**(调用阶段)— 当信息足够时,调用 sub_agent 工具委托 architect 子智能体建书:
- instruction 中包含收集到的所有信息
- 同时传入结构化参数:title(书名)、genre(题材)、platform(平台)、language(语言)、targetChapters(章数)、chapterWordCount(每章字数)
Expected: PASS
[ ] Step 5: Commit
git add packages/core/src/agent/agent-system-prompt.ts packages/core/src/__tests__/agent-system-prompt.test.ts
git commit -m "docs(core): update system prompts with complete sub_agent param documentation"
Files: None (verification only)
[ ] Step 1: Run all core tests
pnpm --filter @actalk/inkos-core test -- --run
Expected: All pass
[ ] Step 2: Run all studio tests
pnpm --filter @actalk/inkos-studio test -- --run
Expected: All pass
[ ] Step 3: Run all CLI tests
pnpm --filter @actalk/inkos test -- --run
Expected: All pass (or only pre-existing Windows tar failures)
If any existing tests broke due to schema changes, fix them.