commit cd0ffcddd55c037370572899081ef6f407a99249
parent d7a2445848574eb70106bbbe3465bfa94b035a71
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Sat, 26 Sep 2026 14:27:09 -0400
mcp: the ask and sweep plans send clip media through fetch_clip, never a hand-run yt-dlp
One step, shared by both builders: after the citation step in ask, before
Finish in the sweep. It passes the plan's corpus as source (the Rumble
mapping reads it), offers full: true for a whole recording, and says the
README's yt-dlp command is the operator's fallback, not the agent's.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Diffstat:
2 files changed, 49 insertions(+), 0 deletions(-)
diff --git a/mcp/src/instructions.test.ts b/mcp/src/instructions.test.ts
@@ -188,3 +188,29 @@ test("the sweep reads operator corrections from a sibling manifest before the fi
assert.ok(at > 0 && batch > at, `corrections step at ${at}, batch step at ${batch}`);
assert.match(text, /never re-asserted/);
});
+
+// ─── Media for a citation ───
+
+test("both builders send clip media through fetch_clip, never a hand-run yt-dlp", () => {
+ for (const text of [sweep("coffee channels=c"), ask("coffee")]) {
+ assert.match(text, /`fetch_clip`/);
+ assert.match(text, /NEVER run yt-dlp yourself/);
+ // The corpus rides along, so a Rumble embed id maps to the editor's dir.
+ assert.match(text, /pass source: "remote:https:\/\/site\.example" so a Rumble id/);
+ // The whole recording is on offer, but the window is the default.
+ assert.match(text, /full: true when the ask genuinely needs the whole recording/);
+ // Only the operator falls back to yt-dlp.
+ assert.match(text, /the operator's fallback, not yours/);
+ }
+ // In the sweep, before Finish: a report that needs a clip asks while the
+ // citation is in hand, not after the summary is written.
+ const s = sweep("coffee channels=c");
+ const clip = s.indexOf("Media for a cited moment");
+ const finish = s.indexOf("**Finish.**");
+ assert.ok(clip > 0 && finish > clip, `clip step at ${clip}, Finish at ${finish}`);
+ // In the ask, after the answer's citation rule.
+ const a = ask("coffee");
+ const cite = a.indexOf("Answer with citations");
+ const askClip = a.indexOf("Media for a cited moment");
+ assert.ok(cite > 0 && askClip > cite, `citations at ${cite}, clip step at ${askClip}`);
+});
diff --git a/mcp/src/instructions.ts b/mcp/src/instructions.ts
@@ -140,6 +140,25 @@ function batchStep(req: PromptRequest, ctx: PlanContext, reportPath: string): st
);
}
+// Media for a citation goes through the editor's fetch job, never a yt-dlp the
+// agent runs itself (the operator's rule: every fetch through Archilyzer). One
+// step, shared by both builders so the two cannot drift. `wait_seconds` and
+// `full` stay un-backticked: they are arguments, not tools.
+function clipStep(ctx: PlanContext): string {
+ return (
+ `**Media for a cited moment — through the editor only.** When I ask for ` +
+ `the clip behind a citation (or a report needs one), call \`fetch_clip\` ` +
+ `with that citation's channel slug, video id, start/end in seconds (or ` +
+ `mm:ss) and a one-line reason (or full: true when the ask genuinely needs ` +
+ `the whole recording — a window is the default), and pass ` +
+ `source: "${ctx.corpus}" so a Rumble id resolves to the right directory. ` +
+ `NEVER run yt-dlp yourself, in any form. If the tool reports no editor is ` +
+ `configured, say so and stop — the README's yt-dlp command is the ` +
+ `operator's fallback, not yours. If it returns queued, call it again with ` +
+ `the job it names.`
+ );
+}
+
// The full sweep instructions.
export function buildSweepInstructions(
req: PromptRequest,
@@ -236,6 +255,8 @@ export function buildSweepInstructions(
steps.push(batchStep(req, ctx, reportPath));
+ steps.push(clipStep(ctx));
+
steps.push(
`**Finish.** Work to the end of the worklist, then write a summary ` +
`section: the scope swept, **how many of the N you actually read**, ` +
@@ -329,6 +350,8 @@ export function buildAskInstructions(
`corpus does NOT show — an absence of matches is a finding, not a gap ` +
`to paper over.`,
);
+
+ steps.push(clipStep(ctx));
}
const numbered = steps.map((s, i) => `${i + 1}. ${s}`).join("\n\n");