diff --git a/docs/canvases/agent-bundle-walkthrough.canvas.tsx b/docs/canvases/agent-bundle-walkthrough.canvas.tsx
index 5bc27be66..557649336 100644
--- a/docs/canvases/agent-bundle-walkthrough.canvas.tsx
+++ b/docs/canvases/agent-bundle-walkthrough.canvas.tsx
@@ -403,8 +403,9 @@ export default function AgentBundleWalkthrough() {
inputSchema, and
resultSchema exports โ there is no
execute/render split (exporting either is the AB4811 error). Other
- route kinds (events, CLI commands, skills) carry their own export
- contracts.
+ route kinds (events, CLI commands) carry their own export
+ contracts; skills are Markdown documents parsed from frontmatter
+ unless the rendered SKILL.tsx form is used.
@@ -643,7 +644,7 @@ export default function AgentBundleWalkthrough() {
n={5}
title="Thin client prints the host-native response and exits 0"
channel="wrapper โ Claude ยท stdout"
- note="Claude blocks the Write and surfaces the reason to the model. If the route had decided to allow it, the wrapper prints an explicit hookSpecificOutput.permissionDecision: 'allow' (optionally with updatedInput / additionalContext); a route that renders no decision prints nothing."
+ note="Claude blocks the Write and surfaces the reason to the model. On tool/before the wrapper always answers: an explicit hookSpecificOutput.permissionDecision ('allow' unless the route denied, optionally with updatedInput / additionalContext) โ even when the route renders no decision. Silence is reserved for observation-only families such as session/end."
payload={WIRE_STDOUT}
last
/>
diff --git a/examples/audiobook-curator/src/evidence.ts b/examples/audiobook-curator/src/evidence.ts
index 1476da35c..4dab858ef 100644
--- a/examples/audiobook-curator/src/evidence.ts
+++ b/examples/audiobook-curator/src/evidence.ts
@@ -260,11 +260,13 @@ export const identifyAudibleSample = async (
for (const candidate of selected) {
const asin = String(candidate.asin ?? '');
const region = String(candidate.region ?? 'us') as AudibleRegion;
+ const score = asRecord(candidate.evidence).score as JsonValue | undefined;
+ // Receipts are strict JSON; absent fields are omitted, never undefined.
const base = {
- asin: asin || undefined,
+ ...(asin === '' ? {} : { asin }),
region,
- score: asRecord(candidate.evidence).score as JsonValue | undefined,
- title: candidate.title,
+ ...(score === undefined ? {} : { score }),
+ ...(candidate.title === undefined ? {} : { title: candidate.title }),
};
if (asin === '') {
attempts.push({ ...base, reason: 'candidate has no ASIN', status: 'skipped' });
@@ -276,20 +278,27 @@ export const identifyAudibleSample = async (
attempts: input.attempts,
chunkSeconds: input.chunkSeconds,
file: input.file,
+ // Staging follows the receipt's disk; without this, identification
+ // would stage beside source media the receipt was chosen to avoid.
+ ...(input.receipt === undefined ? {} : { receipt: input.receipt }),
region,
...(typeof candidate.sample_url === 'string' ? { sampleUrl: candidate.sample_url } : {}),
verbose: input.verbose,
}, dependencies);
const found = outcome.fingerprint.found === true;
+ const attemptTitle = candidate.title ?? outcome.audible.title;
attempts.push({
...base,
fingerprint: outcome.fingerprint,
- sampleUrl: outcome.audible.sampleUrl,
+ ...(outcome.audible.sampleUrl === undefined ? {} : { sampleUrl: outcome.audible.sampleUrl }),
status: found ? 'matched' : 'no-match',
- title: candidate.title ?? outcome.audible.title,
+ ...(attemptTitle === undefined ? {} : { title: attemptTitle }),
});
if (found) {
- identified ??= { asin, region, title: candidate.title ?? outcome.audible.title };
+ const title = candidate.title ?? outcome.audible.title;
+ // A titleless identification must not place an undefined (non-JSON)
+ // value into the durable receipt.
+ identified ??= { asin, region, ...(title === undefined ? {} : { title }) };
if (input.all !== true) break;
}
} catch (error) {
diff --git a/examples/audiobook-curator/tests/evidence-parity.test.ts b/examples/audiobook-curator/tests/evidence-parity.test.ts
index f701963d8..ae9d252bf 100644
--- a/examples/audiobook-curator/tests/evidence-parity.test.ts
+++ b/examples/audiobook-curator/tests/evidence-parity.test.ts
@@ -168,4 +168,32 @@ describe('optional identity evidence parity', () => {
expect(receipt).toMatchObject({ exitCode: 0, operation: 'whisper-identity', status: 'transcript-ready', usableWindows: 5 });
expect(whisperCalls).toBe(5);
});
+
+
+ it('stages acoustic-identify candidate work beside the receipt when one is requested', async () => {
+ const root = await mkdtemp(join(tmpdir(), 'curator-identify-receipt-'));
+ roots.push(root);
+ const file = join(root, 'library', 'book.m4b');
+ const receiptPath = join(root, 'receipts', 'identify.json');
+ await mkdir(dirname(file), { recursive: true });
+ await writeFile(file, 'book');
+ const sampleDirs: string[] = [];
+ const matcher: AcousticMatcher = async (_source, sample) => {
+ sampleDirs.push(dirname(sample));
+ return { found: true };
+ };
+ const http: CuratorHttpClient = async (url, options) => {
+ if (options?.binary === true) return Buffer.from(url);
+ throw new Error('unexpected product request');
+ };
+ await identifyAudibleSample({
+ candidates: [{ asin: 'MATCH', evidence: { score: 90 }, region: 'us', sample_url: 'https://samples/match.mp3' }],
+ candidatesReport: join(root, 'candidates.json'),
+ file,
+ receipt: receiptPath,
+ top: 1,
+ }, { http, matcher });
+ expect(sampleDirs).toHaveLength(1);
+ expect(sampleDirs[0]!.startsWith(join(dirname(receiptPath), '.audiobook-curator-acoustic-'))).toBe(true);
+ });
});