From b8a9e0f67446ccc252708bcb8a8a51b088cbccf8 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Mon, 7 Sep 2026 07:09:18 +0000 Subject: [PATCH 1/4] docs(audiobook-curator): document optional --report/--receipt migration; test both paths (#725 follow-up) --- .changeset/725-receipt-paths-optional.md | 5 ++ examples/audiobook-curator/README.md | 6 +- .../tests/route-unit/cli-dispatch.test.ts | 81 ++++++++++++++++++- .../docs/en/examples/audiobook-curator.mdx | 5 +- .../docs/zh/examples/audiobook-curator.mdx | 4 +- 5 files changed, 97 insertions(+), 4 deletions(-) create mode 100644 .changeset/725-receipt-paths-optional.md diff --git a/.changeset/725-receipt-paths-optional.md b/.changeset/725-receipt-paths-optional.md new file mode 100644 index 000000000..290dfc088 --- /dev/null +++ b/.changeset/725-receipt-paths-optional.md @@ -0,0 +1,5 @@ +--- +"agent-bundle": patch +--- + +Document the `audiobook-curator` example's CLI migration accurately: since the `.cli.ts` projections replaced the `src/cli/` tree (#734), `inventory --report` and `convert --receipt` are optional, as they are on the tools, instead of required; a command run without one writes no receipt file, and exit codes, `--apply` gating, and error output are unchanged. (#PR) diff --git a/examples/audiobook-curator/README.md b/examples/audiobook-curator/README.md index 9dbf472c1..4702d36e6 100644 --- a/examples/audiobook-curator/README.md +++ b/examples/audiobook-curator/README.md @@ -166,7 +166,11 @@ result-schema-validated JSON value followed by a newline: the canonical final `Agent.Result` value, never the Markdown presentation or an intermediate Suspense fallback, and byte for byte the `structuredContent` of the tool call. `--report` and `--receipt` are optional on the command line exactly as they are -on the tool; a command that gets one still writes the receipt file. +on the tool; a command that gets one still writes the receipt file. This is a +behavior change from the retired `src/cli/` tree, where `inventory --report` +and `convert --receipt` were required: both commands now run without a receipt +path and write no file, and their exit codes, `--apply` gating, and error +output are unchanged either way. Each tool module declares its `inputSchema` as an inline zod literal, because the argv projection is compiled statically from that literal; it is the only diff --git a/examples/audiobook-curator/tests/route-unit/cli-dispatch.test.ts b/examples/audiobook-curator/tests/route-unit/cli-dispatch.test.ts index 24a9a267d..f4e7527ae 100644 --- a/examples/audiobook-curator/tests/route-unit/cli-dispatch.test.ts +++ b/examples/audiobook-curator/tests/route-unit/cli-dispatch.test.ts @@ -1,4 +1,4 @@ -import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, readdir, readFile, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -10,6 +10,7 @@ import { inputSchema as convertAudiobookInputSchema } from '../../src/mcp/curato import { inputSchema as inspectInputSchema, resultSchema as inspectResultSchema } from '../../src/mcp/curator/tools/inspect_sources.tsx'; import { resultSchema as inventoryResultSchema } from '../../src/mcp/curator/tools/inventory_sources.tsx'; import { resultSchema as audibleSearchResultSchema } from '../../src/mcp/curator/tools/search_audible.tsx'; +import { resultSchema as audibleSelectResultSchema } from '../../src/mcp/curator/tools/select_audible_edition.tsx'; import { discoveryOperations } from '../../src/operations/discovery.ts'; const directories: string[] = []; @@ -148,6 +149,22 @@ describe('audiobook-curator at the CLI dispatch proof level', () => { expect(inventoryResultSchema.parse(JSON.parse(await readFile(report, 'utf8')))).toEqual(receipt); }); + it('runs inventory without --report, as the tool allows, and writes no report file', async () => { + // Migration note (#734): the retired `src/cli/inventory.tsx` required + // `--report`; the projected command shares the tool's optional field. + const { library, report } = await temporaryLibrary(); + const run = await invokeCli(['inventory', library, '--strict', '--json']); + const receipt = inventoryResultSchema.parse(cliJson(run)); + const tool = await invokeMcpTool('inventory_sources', { input: { source: library, strict: true } }); + + expect(run.exitCode).toBe(0); + expect(receipt).toMatchObject({ exitCode: 0, operation: 'inventory', summary: { errors: 0, files: 0 } }); + expect(run.value).toEqual(receipt); + expect(tool.isError).toBe(false); + expect(withoutGeneratedAt(tool.structuredContent)).toEqual(withoutGeneratedAt(receipt)); + await expect(readFile(report, 'utf8')).rejects.toMatchObject({ code: 'ENOENT' }); + }); + it('uses a failing inventory receipt exit code as the process exit code without ffprobe', async () => { const { directory, library, report } = await temporaryLibrary(); await writeFile(join(library, 'broken.mp3'), 'not audio'); @@ -358,6 +375,15 @@ describe('audiobook-curator at the CLI dispatch proof level', () => { expect(planned.stderr).not.toContain('--yes'); expect(planned.value).toBeUndefined(); + // Migration note (#734): the retired `src/cli/convert.tsx` required + // `--receipt`; the projected command shares the tool's optional field. + // With or without it, the failure is the same and no receipt is written. + const receipt = join(directory, 'convert-receipt.json'); + const withReceipt = await invokeCli([...argv, '--receipt', receipt, '--json']); + expect(withReceipt.exitCode).toBe(1); + expect(withReceipt.stderr).toContain('Selection contains no audio files.'); + await expect(readFile(receipt, 'utf8')).rejects.toMatchObject({ code: 'ENOENT' }); + // The projection declares confirm: false, so --yes is not an option here. const confirmed = await invokeCli([...argv, '--yes']); expect(confirmed.exitCode).toBe(2); @@ -370,6 +396,59 @@ describe('audiobook-curator at the CLI dispatch proof level', () => { }); }); + describe('receipt paths are optional on the projected commands', () => { + const candidateReport = async (): Promise<{ readonly candidates: string; readonly directory: string }> => { + const { directory } = await temporaryLibrary(); + const candidates = join(directory, 'candidates.json'); + await writeFile(candidates, JSON.stringify({ + candidates: [{ + asin: 'B0CURATOR01', + authors: [{ name: 'Ada Author' }], + evidence: { authorMatch: true, languageMatch: true, narratorMatch: true, score: 100, strictIdentityMatch: true, titleMatch: true, unabridged: true }, + narrators: [{ name: 'Nora Narrator' }], + region: 'us', + title: 'The Selected Edition', + }], + errors: [], + exitCode: 0, + generatedAt: '2026-09-02T18:00:00.000Z', + humanReviewRequired: true, + mutation: false, + operation: 'audible-search', + query: { title: 'The Selected Edition' }, + reviewNote: 'Choose the matching edition.', + })); + return { candidates, directory }; + }; + + it('records an Audible selection with and without --receipt and writes the file only when asked', async () => { + const { candidates, directory } = await candidateReport(); + const receiptPath = join(directory, 'selection.json'); + const argv = ['audible-select', '--candidate', '1', '--candidates', candidates, '--json']; + + const without = await invokeCli(argv); + const withReceipt = await invokeCli([...argv, '--receipt', receiptPath]); + const tool = await invokeMcpTool('select_audible_edition', { input: { candidate: 1, candidates } }); + + for (const run of [without, withReceipt]) { + expect(run.exitCode).toBe(0); + expect(run.stderr).toBe(''); + expect(run.routeId).toBe('tool:curator/select_audible_edition'); + expect(audibleSelectResultSchema.parse(cliJson(run))).toMatchObject({ + candidateNumber: 1, + humanReviewed: true, + mutation: false, + operation: 'audible-select', + selected: { asin: 'B0CURATOR01' }, + }); + } + expect(withoutGeneratedAt(cliJson(withReceipt))).toEqual(withoutGeneratedAt(cliJson(without))); + expect(withoutGeneratedAt(tool.structuredContent)).toEqual(withoutGeneratedAt(cliJson(without))); + expect(audibleSelectResultSchema.parse(JSON.parse(await readFile(receiptPath, 'utf8')))).toEqual(withReceipt.value); + expect((await readdir(directory)).filter((name) => name.endsWith('.json')).sort()).toEqual(['candidates.json', 'selection.json']); + }); + }); + describe('the rendered library-audit command', () => { it('emits exactly one final Markdown document when stdout is piped', async () => { const { report, run } = await invokeLibraryAudit(); diff --git a/website/docs/en/examples/audiobook-curator.mdx b/website/docs/en/examples/audiobook-curator.mdx index 9ab4ac66f..c8b6cc832 100644 --- a/website/docs/en/examples/audiobook-curator.mdx +++ b/website/docs/en/examples/audiobook-curator.mdx @@ -76,7 +76,10 @@ emits one result-schema-validated JSON value followed by a newline: the canonica `Agent.Result` value, never the Markdown presentation and never an intermediate Suspense fallback, and byte for byte the `structuredContent` of the tool call. `--report` and `--receipt` are optional on the command line exactly as they are on the tool; a command that gets one still -writes the receipt file. +writes the receipt file. This is a behavior change from the retired `src/cli/` tree, where +`inventory --report` and `convert --receipt` were required: both commands now run without a +receipt path and write no file, and their exit codes, `--apply` gating, and error output are +unchanged either way. ## Working in the workspace diff --git a/website/docs/zh/examples/audiobook-curator.mdx b/website/docs/zh/examples/audiobook-curator.mdx index a9db9d63b..66bbcc248 100644 --- a/website/docs/zh/examples/audiobook-curator.mdx +++ b/website/docs/zh/examples/audiobook-curator.mdx @@ -61,7 +61,9 @@ description: '有声书策展器示例:一个由路由模块、请求上下文 最终的 Markdown 文档——与 MCP 客户端作为文本内容收到的是同一份标题与报告。`--json` 选择机器输出, 并输出一个经结果 schema 校验的 JSON 值加一个换行:这是最终的规范 `Agent.Result` 值,绝不是 Markdown 表现,也绝不是中间的 Suspense 回退,并且与工具调用的 `structuredContent` 逐字节一致。`--report` 与 -`--receipt` 在命令行上和在工具上一样是可选的;传入了的命令仍会写出收据文件。 +`--receipt` 在命令行上和在工具上一样是可选的;传入了的命令仍会写出收据文件。这相对于已移除的 +`src/cli/` 树是一处行为变化:那里的 `inventory --report` 与 `convert --receipt` 是必填的;现在这两条命令 +不带收据路径也能运行且不写文件,而退出码、`--apply` 把关与错误输出在两种情况下都不变。 ## 在工作区中开发 From 4c40a89b8846c68072fa2d2607d1ea92e8a55c50 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Mon, 7 Sep 2026 07:09:33 +0000 Subject: [PATCH 2/4] changeset: point at #738 --- .changeset/725-receipt-paths-optional.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/725-receipt-paths-optional.md b/.changeset/725-receipt-paths-optional.md index 290dfc088..36f2e9a10 100644 --- a/.changeset/725-receipt-paths-optional.md +++ b/.changeset/725-receipt-paths-optional.md @@ -2,4 +2,4 @@ "agent-bundle": patch --- -Document the `audiobook-curator` example's CLI migration accurately: since the `.cli.ts` projections replaced the `src/cli/` tree (#734), `inventory --report` and `convert --receipt` are optional, as they are on the tools, instead of required; a command run without one writes no receipt file, and exit codes, `--apply` gating, and error output are unchanged. (#PR) +Document the `audiobook-curator` example's CLI migration accurately: since the `.cli.ts` projections replaced the `src/cli/` tree (#734), `inventory --report` and `convert --receipt` are optional, as they are on the tools, instead of required; a command run without one writes no receipt file, and exit codes, `--apply` gating, and error output are unchanged. (#738) From 97178d3972b6c0f27c2c9695a9c38d288420a250 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Mon, 7 Sep 2026 07:18:28 +0000 Subject: [PATCH 3/4] review: report/receipt wording; assert directory contents and identical stderr --- .changeset/725-receipt-paths-optional.md | 2 +- examples/audiobook-curator/README.md | 2 +- .../tests/route-unit/cli-dispatch.test.ts | 7 ++++--- website/docs/en/examples/audiobook-curator.mdx | 2 +- website/docs/zh/examples/audiobook-curator.mdx | 2 +- 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.changeset/725-receipt-paths-optional.md b/.changeset/725-receipt-paths-optional.md index 36f2e9a10..22deaefad 100644 --- a/.changeset/725-receipt-paths-optional.md +++ b/.changeset/725-receipt-paths-optional.md @@ -2,4 +2,4 @@ "agent-bundle": patch --- -Document the `audiobook-curator` example's CLI migration accurately: since the `.cli.ts` projections replaced the `src/cli/` tree (#734), `inventory --report` and `convert --receipt` are optional, as they are on the tools, instead of required; a command run without one writes no receipt file, and exit codes, `--apply` gating, and error output are unchanged. (#738) +Document the `audiobook-curator` example's CLI migration accurately: since the `.cli.ts` projections replaced the `src/cli/` tree (#734), `inventory --report` and `convert --receipt` are optional, as they are on the tools, instead of required; a command run without one writes no report or receipt file, and exit codes, `--apply` gating, and error output are unchanged. (#738) diff --git a/examples/audiobook-curator/README.md b/examples/audiobook-curator/README.md index 4702d36e6..7cce9b767 100644 --- a/examples/audiobook-curator/README.md +++ b/examples/audiobook-curator/README.md @@ -169,7 +169,7 @@ Suspense fallback, and byte for byte the `structuredContent` of the tool call. on the tool; a command that gets one still writes the receipt file. This is a behavior change from the retired `src/cli/` tree, where `inventory --report` and `convert --receipt` were required: both commands now run without a receipt -path and write no file, and their exit codes, `--apply` gating, and error +path and write no report or receipt file, and their exit codes, `--apply` gating, and error output are unchanged either way. Each tool module declares its `inputSchema` as an inline zod literal, because diff --git a/examples/audiobook-curator/tests/route-unit/cli-dispatch.test.ts b/examples/audiobook-curator/tests/route-unit/cli-dispatch.test.ts index f4e7527ae..a598ef9f2 100644 --- a/examples/audiobook-curator/tests/route-unit/cli-dispatch.test.ts +++ b/examples/audiobook-curator/tests/route-unit/cli-dispatch.test.ts @@ -152,8 +152,9 @@ describe('audiobook-curator at the CLI dispatch proof level', () => { it('runs inventory without --report, as the tool allows, and writes no report file', async () => { // Migration note (#734): the retired `src/cli/inventory.tsx` required // `--report`; the projected command shares the tool's optional field. - const { library, report } = await temporaryLibrary(); + const { directory, library } = await temporaryLibrary(); const run = await invokeCli(['inventory', library, '--strict', '--json']); + expect(await readdir(directory)).toEqual(['library']); const receipt = inventoryResultSchema.parse(cliJson(run)); const tool = await invokeMcpTool('inventory_sources', { input: { source: library, strict: true } }); @@ -162,7 +163,6 @@ describe('audiobook-curator at the CLI dispatch proof level', () => { expect(run.value).toEqual(receipt); expect(tool.isError).toBe(false); expect(withoutGeneratedAt(tool.structuredContent)).toEqual(withoutGeneratedAt(receipt)); - await expect(readFile(report, 'utf8')).rejects.toMatchObject({ code: 'ENOENT' }); }); it('uses a failing inventory receipt exit code as the process exit code without ffprobe', async () => { @@ -381,7 +381,7 @@ describe('audiobook-curator at the CLI dispatch proof level', () => { const receipt = join(directory, 'convert-receipt.json'); const withReceipt = await invokeCli([...argv, '--receipt', receipt, '--json']); expect(withReceipt.exitCode).toBe(1); - expect(withReceipt.stderr).toContain('Selection contains no audio files.'); + expect(withReceipt.stderr).toBe(planned.stderr); await expect(readFile(receipt, 'utf8')).rejects.toMatchObject({ code: 'ENOENT' }); // The projection declares confirm: false, so --yes is not an option here. @@ -427,6 +427,7 @@ describe('audiobook-curator at the CLI dispatch proof level', () => { const argv = ['audible-select', '--candidate', '1', '--candidates', candidates, '--json']; const without = await invokeCli(argv); + expect(await readdir(directory)).toEqual(['candidates.json', 'library']); const withReceipt = await invokeCli([...argv, '--receipt', receiptPath]); const tool = await invokeMcpTool('select_audible_edition', { input: { candidate: 1, candidates } }); diff --git a/website/docs/en/examples/audiobook-curator.mdx b/website/docs/en/examples/audiobook-curator.mdx index c8b6cc832..287423c77 100644 --- a/website/docs/en/examples/audiobook-curator.mdx +++ b/website/docs/en/examples/audiobook-curator.mdx @@ -78,7 +78,7 @@ and byte for byte the `structuredContent` of the tool call. `--report` and `--re optional on the command line exactly as they are on the tool; a command that gets one still writes the receipt file. This is a behavior change from the retired `src/cli/` tree, where `inventory --report` and `convert --receipt` were required: both commands now run without a -receipt path and write no file, and their exit codes, `--apply` gating, and error output are +receipt path and write no report or receipt file, and their exit codes, `--apply` gating, and error output are unchanged either way. ## Working in the workspace diff --git a/website/docs/zh/examples/audiobook-curator.mdx b/website/docs/zh/examples/audiobook-curator.mdx index 66bbcc248..0f3531b8b 100644 --- a/website/docs/zh/examples/audiobook-curator.mdx +++ b/website/docs/zh/examples/audiobook-curator.mdx @@ -63,7 +63,7 @@ description: '有声书策展器示例:一个由路由模块、请求上下文 表现,也绝不是中间的 Suspense 回退,并且与工具调用的 `structuredContent` 逐字节一致。`--report` 与 `--receipt` 在命令行上和在工具上一样是可选的;传入了的命令仍会写出收据文件。这相对于已移除的 `src/cli/` 树是一处行为变化:那里的 `inventory --report` 与 `convert --receipt` 是必填的;现在这两条命令 -不带收据路径也能运行且不写文件,而退出码、`--apply` 把关与错误输出在两种情况下都不变。 +不带收据路径也能运行且不写报告或收据文件,而退出码、`--apply` 把关与错误输出在两种情况下都不变。 ## 在工作区中开发 From b3e8441b89a731f6bd6168809901737e164ebb6f Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Mon, 7 Sep 2026 07:21:34 +0000 Subject: [PATCH 4/4] docs: receipt is written when the command succeeds --- examples/audiobook-curator/README.md | 3 ++- website/docs/en/examples/audiobook-curator.mdx | 2 +- website/docs/zh/examples/audiobook-curator.mdx | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/examples/audiobook-curator/README.md b/examples/audiobook-curator/README.md index 7cce9b767..03f2f405f 100644 --- a/examples/audiobook-curator/README.md +++ b/examples/audiobook-curator/README.md @@ -166,7 +166,8 @@ result-schema-validated JSON value followed by a newline: the canonical final `Agent.Result` value, never the Markdown presentation or an intermediate Suspense fallback, and byte for byte the `structuredContent` of the tool call. `--report` and `--receipt` are optional on the command line exactly as they are -on the tool; a command that gets one still writes the receipt file. This is a +on the tool; a command that gets one still writes the receipt file +when it succeeds. This is a behavior change from the retired `src/cli/` tree, where `inventory --report` and `convert --receipt` were required: both commands now run without a receipt path and write no report or receipt file, and their exit codes, `--apply` gating, and error diff --git a/website/docs/en/examples/audiobook-curator.mdx b/website/docs/en/examples/audiobook-curator.mdx index 287423c77..6c8c449f7 100644 --- a/website/docs/en/examples/audiobook-curator.mdx +++ b/website/docs/en/examples/audiobook-curator.mdx @@ -76,7 +76,7 @@ emits one result-schema-validated JSON value followed by a newline: the canonica `Agent.Result` value, never the Markdown presentation and never an intermediate Suspense fallback, and byte for byte the `structuredContent` of the tool call. `--report` and `--receipt` are optional on the command line exactly as they are on the tool; a command that gets one still -writes the receipt file. This is a behavior change from the retired `src/cli/` tree, where +writes the receipt file when it succeeds. This is a behavior change from the retired `src/cli/` tree, where `inventory --report` and `convert --receipt` were required: both commands now run without a receipt path and write no report or receipt file, and their exit codes, `--apply` gating, and error output are unchanged either way. diff --git a/website/docs/zh/examples/audiobook-curator.mdx b/website/docs/zh/examples/audiobook-curator.mdx index 0f3531b8b..3f3737f68 100644 --- a/website/docs/zh/examples/audiobook-curator.mdx +++ b/website/docs/zh/examples/audiobook-curator.mdx @@ -61,7 +61,7 @@ description: '有声书策展器示例:一个由路由模块、请求上下文 最终的 Markdown 文档——与 MCP 客户端作为文本内容收到的是同一份标题与报告。`--json` 选择机器输出, 并输出一个经结果 schema 校验的 JSON 值加一个换行:这是最终的规范 `Agent.Result` 值,绝不是 Markdown 表现,也绝不是中间的 Suspense 回退,并且与工具调用的 `structuredContent` 逐字节一致。`--report` 与 -`--receipt` 在命令行上和在工具上一样是可选的;传入了的命令仍会写出收据文件。这相对于已移除的 +`--receipt` 在命令行上和在工具上一样是可选的;传入了的命令在成功时仍会写出收据文件。这相对于已移除的 `src/cli/` 树是一处行为变化:那里的 `inventory --report` 与 `convert --receipt` 是必填的;现在这两条命令 不带收据路径也能运行且不写报告或收据文件,而退出码、`--apply` 把关与错误输出在两种情况下都不变。