Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/725-receipt-paths-optional.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"agent-bundle": patch

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Remove the unrelated agent-bundle changeset

This commit changes only the private examples/audiobook-curator package, its tests, and documentation, so naming agent-bundle here will cause the release workflow to issue an unnecessary package version bump and changelog entry for behavior that the published package did not change. Private examples and the website are explicitly ignored and must not be named in a changeset, so this changeset should be deleted.

AGENTS.md reference: AGENTS.md:L159-L164

Useful? React with 👍 / 👎.

---

Document the `audiobook-curator` example's CLI migration accurately: since the `<tool>.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)
7 changes: 6 additions & 1 deletion examples/audiobook-curator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,12 @@ 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
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.

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
Expand Down
82 changes: 81 additions & 1 deletion examples/audiobook-curator/tests/route-unit/cli-dispatch.test.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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[] = [];
Expand Down Expand Up @@ -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 { 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 } });

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));
});

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');
Expand Down Expand Up @@ -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).toBe(planned.stderr);
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);
Expand All @@ -370,6 +396,60 @@ 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);
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 } });

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();
Expand Down
5 changes: 4 additions & 1 deletion website/docs/en/examples/audiobook-curator.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 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.

## Working in the workspace

Expand Down
4 changes: 3 additions & 1 deletion website/docs/zh/examples/audiobook-curator.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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` 把关与错误输出在两种情况下都不变。

## 在工作区中开发

Expand Down
Loading