wildcrane-runner 0.6.11 → 0.6.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/runner.mjs CHANGED
@@ -1,4 +1,4 @@
1
- // wildcrane-runner 0.6.11 — built by runner/scripts/build.mjs. Do not edit.
1
+ // wildcrane-runner 0.6.13 — built by runner/scripts/build.mjs. Do not edit.
2
2
  import { createRequire as __ai4kanbanRequire } from 'node:module'
3
3
  const require = __ai4kanbanRequire(import.meta.url)
4
4
  var __create = Object.create;
@@ -84001,8 +84001,8 @@ var mockup_exports = {};
84001
84001
  __export(mockup_exports, {
84002
84002
  drawMockup: () => drawMockup
84003
84003
  });
84004
- import fs19 from "node:fs";
84005
- import path22 from "node:path";
84004
+ import fs20 from "node:fs";
84005
+ import path23 from "node:path";
84006
84006
  import vm from "node:vm";
84007
84007
  async function loadPackage(id) {
84008
84008
  if (id === "react") return import_react3.default;
@@ -84015,20 +84015,20 @@ async function loadPackage(id) {
84015
84015
  return { __esModule: true, ...await load() };
84016
84016
  }
84017
84017
  function resolveFile(id, from, folder) {
84018
- const at2 = path22.resolve(path22.dirname(from), id);
84019
- if (at2 !== folder && !at2.startsWith(folder + path22.sep)) throw new Error(`${id} is outside the mockup's folder`);
84020
- const tries = path22.extname(at2) ? [at2] : [...CODE_EXTS.map((e2) => at2 + e2), ...CODE_EXTS.map((e2) => path22.join(at2, `index${e2}`))];
84021
- const file2 = tries.find((f2) => fs19.statSync(f2, { throwIfNoEntry: false })?.isFile());
84018
+ const at2 = path23.resolve(path23.dirname(from), id);
84019
+ if (at2 !== folder && !at2.startsWith(folder + path23.sep)) throw new Error(`${id} is outside the mockup's folder`);
84020
+ const tries = path23.extname(at2) ? [at2] : [...CODE_EXTS.map((e2) => at2 + e2), ...CODE_EXTS.map((e2) => path23.join(at2, `index${e2}`))];
84021
+ const file2 = tries.find((f2) => fs20.statSync(f2, { throwIfNoEntry: false })?.isFile());
84022
84022
  if (!file2) throw new Error(`no such file: ${id}`);
84023
84023
  return file2;
84024
84024
  }
84025
84025
  async function readGraph(entry) {
84026
- const folder = path22.dirname(entry);
84026
+ const folder = path23.dirname(entry);
84027
84027
  const graph = { code: /* @__PURE__ */ new Map(), links: /* @__PURE__ */ new Map(), sheets: [], packages: /* @__PURE__ */ new Map() };
84028
84028
  const walk = async (file2) => {
84029
84029
  if (graph.code.has(file2)) return;
84030
84030
  if (graph.code.size >= MAX_FILES) throw new Error(`a mockup imports more than ${MAX_FILES} files`);
84031
- const js = (0, import_sucrase.transform)(fs19.readFileSync(file2, "utf8"), { transforms: ["typescript", "jsx", "imports"], jsxRuntime: "automatic", production: true, filePath: file2 }).code;
84031
+ const js = (0, import_sucrase.transform)(fs20.readFileSync(file2, "utf8"), { transforms: ["typescript", "jsx", "imports"], jsxRuntime: "automatic", production: true, filePath: file2 }).code;
84032
84032
  graph.code.set(file2, js);
84033
84033
  const link = /* @__PURE__ */ new Map();
84034
84034
  graph.links.set(file2, link);
@@ -84040,7 +84040,7 @@ async function readGraph(entry) {
84040
84040
  const target = resolveFile(id, file2, folder);
84041
84041
  link.set(id, target);
84042
84042
  if (!target.endsWith(".css")) await walk(target);
84043
- else if (!graph.sheets.some((s) => s.path === target)) graph.sheets.push({ path: target, content: fs19.readFileSync(target, "utf8") });
84043
+ else if (!graph.sheets.some((s) => s.path === target)) graph.sheets.push({ path: target, content: fs20.readFileSync(target, "utf8") });
84044
84044
  }
84045
84045
  };
84046
84046
  await walk(entry);
@@ -84054,25 +84054,25 @@ function run(script, context, name, deadline) {
84054
84054
  async function tailwindFor(markup, sheets) {
84055
84055
  const source = sheets.length ? sheets.map((s) => s.content).join("\n") : '@import "tailwindcss";';
84056
84056
  const compiler = await zf(source, {
84057
- base: sheets.length ? path22.dirname(sheets[0].path) : "/",
84057
+ base: sheets.length ? path23.dirname(sheets[0].path) : "/",
84058
84058
  loadStylesheet: async (id, from) => {
84059
84059
  if (id === "tailwindcss" || id.startsWith("tailwindcss/")) return { path: "tailwindcss/index.css", base: "/", content: tailwindcss_default };
84060
84060
  if (!id.startsWith(".")) throw new Error(`a mockup cannot import ${id}`);
84061
- const at2 = path22.resolve(from, id);
84062
- return { path: at2, base: path22.dirname(at2), content: fs19.readFileSync(at2, "utf8") };
84061
+ const at2 = path23.resolve(from, id);
84062
+ return { path: at2, base: path23.dirname(at2), content: fs20.readFileSync(at2, "utf8") };
84063
84063
  }
84064
84064
  });
84065
84065
  const classes = [...markup.matchAll(/class="([^"]*)"/g)].flatMap((m) => m[1].split(/\s+/));
84066
84066
  return compiler.build(classes.filter(Boolean));
84067
84067
  }
84068
84068
  function dressPage(file2) {
84069
- const folder = path22.dirname(file2);
84070
- const html = fs19.readFileSync(file2, "utf8").replace(/<link\b[^>]*>/gi, (tag) => {
84069
+ const folder = path23.dirname(file2);
84070
+ const html = fs20.readFileSync(file2, "utf8").replace(/<link\b[^>]*>/gi, (tag) => {
84071
84071
  const href = /\bhref\s*=\s*["']?([^"'\s>]+)/i.exec(tag)?.[1];
84072
84072
  if (!/\brel\s*=\s*["']?stylesheet/i.test(tag) || !href || /^[a-z][\w+.-]*:|^\/\//i.test(href)) return tag;
84073
- const at3 = path22.resolve(folder, href);
84074
- if (!at3.startsWith(folder + path22.sep) || !fs19.existsSync(at3)) return tag;
84075
- return `<style>${fs19.readFileSync(at3, "utf8").replace(/<\/style/gi, "<\\/style")}</style>`;
84073
+ const at3 = path23.resolve(folder, href);
84074
+ if (!at3.startsWith(folder + path23.sep) || !fs20.existsSync(at3)) return tag;
84075
+ return `<style>${fs20.readFileSync(at3, "utf8").replace(/<\/style/gi, "<\\/style")}</style>`;
84076
84076
  });
84077
84077
  const at2 = /^\uFEFF?\s*(?:<!--[\s\S]*?-->\s*)*(?:<!doctype\b[^>]*>)?/i.exec(html)[0].length;
84078
84078
  return /<!doctype\b|<html[\s>]|<head[\s>]/i.test(html) ? html.slice(0, at2) + head + html.slice(at2) : `<!doctype html><html><head>${head}</head><body>${html}</body></html>`;
@@ -84435,9 +84435,9 @@ function status(line) {
84435
84435
  }
84436
84436
 
84437
84437
  // src/protocol.ts
84438
- var VERSION = true ? "0.6.11" : "0.0.0";
84439
- var AKB_VERSION = true ? "0.11.8" : "0.0.0";
84440
- var AKB_CONTRACT = true ? 4 : 0;
84438
+ var VERSION = true ? "0.6.13" : "0.0.0";
84439
+ var AKB_VERSION = true ? "0.11.10" : "0.0.0";
84440
+ var AKB_CONTRACT = true ? 5 : 0;
84441
84441
  var EXIT_REFUSED = 10;
84442
84442
  var EXIT_OUTDATED = 11;
84443
84443
  var EXIT_UPDATE = 12;
@@ -84445,9 +84445,9 @@ var EXIT_UPDATE = 12;
84445
84445
  // src/runtimes.ts
84446
84446
  import { spawn as spawn2 } from "node:child_process";
84447
84447
  import { randomUUID } from "node:crypto";
84448
- import fs10 from "node:fs";
84448
+ import fs11 from "node:fs";
84449
84449
  import os4 from "node:os";
84450
- import path13 from "node:path";
84450
+ import path14 from "node:path";
84451
84451
 
84452
84452
  // ../cli/src/lib/agent/argv.ts
84453
84453
  function splitCommand(command) {
@@ -85923,8 +85923,8 @@ function createTail(log) {
85923
85923
  // ../cli/package.json
85924
85924
  var package_default = {
85925
85925
  name: "wildcrane",
85926
- version: "0.11.8",
85927
- akbContract: 4,
85926
+ version: "0.11.10",
85927
+ akbContract: 5,
85928
85928
  description: "WildCrane's `akb` command \u2014 scaffold the Markdown board under docs/kanban/, put an agent on a card, and carry the flows the agent works by.",
85929
85929
  keywords: [
85930
85930
  "kanban",
@@ -88156,7 +88156,8 @@ __export(api_exports, {
88156
88156
  createVersion: () => createVersion,
88157
88157
  dispatchReview: () => dispatchReview,
88158
88158
  download: () => download,
88159
- dropIdea: () => dropIdea,
88159
+ dropGoal: () => dropGoal,
88160
+ dropIdea: () => dropGoal,
88160
88161
  finishJob: () => finishJob,
88161
88162
  listChats: () => listChats,
88162
88163
  listElsewhere: () => listElsewhere,
@@ -88189,20 +88190,63 @@ __export(api_exports, {
88189
88190
  request: () => request,
88190
88191
  requestAgent: () => requestAgent,
88191
88192
  saveDraft: () => saveDraft,
88192
- setIdeaWorkflow: () => setIdeaWorkflow,
88193
- startIdea: () => startIdea,
88193
+ setGoalWorkflow: () => setGoalWorkflow,
88194
+ setIdeaWorkflow: () => setGoalWorkflow,
88195
+ startGoal: () => startGoal,
88196
+ startIdea: () => startGoal,
88194
88197
  stopTask: () => stopTask,
88195
88198
  targetVersion: () => targetVersion,
88196
88199
  upload: () => upload
88197
88200
  });
88198
88201
 
88199
- // src/secrets.ts
88202
+ // src/goals.ts
88200
88203
  import fs9 from "node:fs";
88201
88204
  import path12 from "node:path";
88202
- var file = () => path12.join(runnerHome(), "secrets.json");
88205
+ function goalsOf(held4) {
88206
+ if (held4.goals !== void 0 && held4.ideas !== void 0 && JSON.stringify(held4.goals) !== JSON.stringify(held4.ideas)) throw new Error("goals and ideas do not match.");
88207
+ const goals = held4.goals ?? held4.ideas;
88208
+ if (!Array.isArray(goals)) throw new Error("The goals array is missing or invalid.");
88209
+ return goals;
88210
+ }
88211
+ var goalsFileOf = (job) => path12.join(runnerHome(), "ideas", `${job.id}.json`);
88212
+ async function writeGoals(job, list) {
88213
+ let goals;
88214
+ try {
88215
+ goals = goalsOf(job.discussion);
88216
+ } catch (e2) {
88217
+ return { error: String(e2) };
88218
+ }
88219
+ const workflows = [];
88220
+ let after = null;
88221
+ do {
88222
+ const page = await list(job.discussion.workspace, after);
88223
+ if (!page.ok) return { error: page.error };
88224
+ for (const w2 of page.value.data) workflows.push({ id: w2.id, name: w2.names ? `${w2.names.en} / ${w2.names.zh}` : w2.name, ...w2.description ? { description: w2.description } : {} });
88225
+ after = page.value.next;
88226
+ } while (after);
88227
+ const file2 = goalsFileOf(job);
88228
+ fs9.mkdirSync(path12.dirname(file2), { recursive: true });
88229
+ fs9.writeFileSync(file2, `${JSON.stringify({ workflows, ideas: goals }, null, 2)}
88230
+ `);
88231
+ return { file: file2 };
88232
+ }
88233
+ function readGoals(file2) {
88234
+ try {
88235
+ return goalsOf(JSON.parse(fs9.readFileSync(file2, "utf8")));
88236
+ } catch {
88237
+ return void 0;
88238
+ } finally {
88239
+ fs9.rmSync(file2, { force: true });
88240
+ }
88241
+ }
88242
+
88243
+ // src/secrets.ts
88244
+ import fs10 from "node:fs";
88245
+ import path13 from "node:path";
88246
+ var file = () => path13.join(runnerHome(), "secrets.json");
88203
88247
  function readAll() {
88204
88248
  try {
88205
- const held4 = JSON.parse(fs9.readFileSync(file(), "utf8"));
88249
+ const held4 = JSON.parse(fs10.readFileSync(file(), "utf8"));
88206
88250
  return held4 && typeof held4 === "object" ? held4 : {};
88207
88251
  } catch {
88208
88252
  return {};
@@ -88218,11 +88262,11 @@ function keepSecrets(runtime, given) {
88218
88262
  }
88219
88263
  if (Object.keys(held4).length) all[runtime] = held4;
88220
88264
  else delete all[runtime];
88221
- fs9.mkdirSync(runnerHome(), { recursive: true, mode: 448 });
88265
+ fs10.mkdirSync(runnerHome(), { recursive: true, mode: 448 });
88222
88266
  const temp = `${file()}.${process.pid}.tmp`;
88223
- fs9.writeFileSync(temp, `${JSON.stringify(all)}
88267
+ fs10.writeFileSync(temp, `${JSON.stringify(all)}
88224
88268
  `, { mode: 384 });
88225
- fs9.renameSync(temp, file());
88269
+ fs10.renameSync(temp, file());
88226
88270
  }
88227
88271
  var secretsSet = () => Object.fromEntries(Object.entries(readAll()).map(([id, keys]) => [id, Object.keys(keys)]));
88228
88272
 
@@ -88337,18 +88381,37 @@ var createUpload = (artifact, contentType, size) => request("POST", `/v1/artifac
88337
88381
  var createVersion = (artifact, upload2) => request("POST", `/v1/artifacts/${q(artifact)}/versions`, { upload: upload2 });
88338
88382
  var readRejections = (workspace) => request("GET", `/v1/rejections?workspace=${q(workspace)}`);
88339
88383
  var markReviewed = (tasks, triage) => request("POST", "/v1/rejections/reviewed", { tasks, triage });
88340
- var openDiscussion = (project, requestId, host = "claude-code") => request("POST", "/v1/discussions/external", { ...project ? { project } : {}, requestId, host });
88341
- var readDiscussion = (id) => request("GET", `/v1/discussions/${q(id)}`);
88342
- async function saveDraft(id, expectedVersion, ideas) {
88343
- const sent = await send("PUT", `/v1/discussions/${q(id)}/draft`, { expectedVersion, ideas }, true, TIMEOUT_MS);
88384
+ var openDiscussionCall = (project, requestId, host = "claude-code") => request("POST", "/v1/discussions/external", { ...project ? { project } : {}, requestId, host });
88385
+ var readDiscussionCall = (id) => request("GET", `/v1/discussions/${q(id)}`);
88386
+ var goalDiscussions = /* @__PURE__ */ new Set();
88387
+ function discussionAnswer(answer) {
88388
+ if (!answer.ok) return answer;
88389
+ const d2 = answer.value;
88390
+ if (d2.goals !== void 0) goalDiscussions.add(d2.id);
88391
+ else goalDiscussions.delete(d2.id);
88392
+ try {
88393
+ return { ...answer, value: { ...d2, goals: goalsOf(d2) } };
88394
+ } catch (e2) {
88395
+ return { ok: false, code: "bad_response", error: String(e2) };
88396
+ }
88397
+ }
88398
+ var openDiscussion = async (...args) => discussionAnswer(await openDiscussionCall(...args));
88399
+ var readDiscussion = async (id) => discussionAnswer(await readDiscussionCall(id));
88400
+ async function saveDraft(id, expectedVersion, goals) {
88401
+ const sent = await send("PUT", `/v1/discussions/${q(id)}/draft`, { expectedVersion, ...goalDiscussions.has(id) ? { goals } : { ideas: goals } }, true, TIMEOUT_MS);
88344
88402
  if (!sent.ok) return sent;
88345
- if (sent.value.status !== 409) return answerOf(sent.value);
88403
+ if (sent.value.status !== 409) return discussionAnswer(await answerOf(sent.value));
88346
88404
  const answer = await sent.value.json().catch(() => null);
88405
+ if (answer?.discussion) {
88406
+ const normalized = discussionAnswer({ ok: true, value: answer.discussion });
88407
+ if (!normalized.ok) return normalized;
88408
+ answer.discussion = normalized.value;
88409
+ }
88347
88410
  return { ok: false, error: answer?.error?.message ?? "Cloud answered 409", code: answer?.error?.code ?? "refused", ...answer?.discussion ? { discussion: answer.discussion } : {} };
88348
88411
  }
88349
- var setIdeaWorkflow = (id, idea, workflow) => request("PATCH", `/v1/discussions/${q(id)}/ideas/${q(idea)}`, { workflow });
88350
- var dropIdea = (id, idea) => request("DELETE", `/v1/discussions/${q(id)}/ideas/${q(idea)}`);
88351
- var startIdea = (id, idea, expectedVersion, project) => request("POST", `/v1/discussions/${q(id)}/ideas/${q(idea)}/start`, { expectedVersion, ...project ? { project } : {} });
88412
+ var setGoalWorkflow = (id, idea, workflow) => request("PATCH", `/v1/discussions/${q(id)}/${goalDiscussions.has(id) ? "goals" : "ideas"}/${q(idea)}`, { workflow });
88413
+ var dropGoal = (id, idea) => request("DELETE", `/v1/discussions/${q(id)}/${goalDiscussions.has(id) ? "goals" : "ideas"}/${q(idea)}`);
88414
+ var startGoal = (id, idea, expectedVersion, project) => request("POST", `/v1/discussions/${q(id)}/${goalDiscussions.has(id) ? "goals" : "ideas"}/${q(idea)}/start`, { expectedVersion, ...project ? { project } : {} });
88352
88415
 
88353
88416
  // src/runtimes.ts
88354
88417
  var HARNESSES2 = Object.fromEntries(RUNTIMES.map((name) => [name, HARNESSES.find((h3) => h3.name === name)]));
@@ -88463,14 +88526,14 @@ var listed = (harness) => async () => (harness.models?.() ?? []).map((id) => ({
88463
88526
  var readModels = (name) => (MODELS[name] ?? listed(HARNESSES2[name]))().catch(() => []);
88464
88527
  var MODELS = {
88465
88528
  "claude-code": async () => {
88466
- const cached2 = claudeModels(path13.join(process.env.CLAUDE_CONFIG_DIR || path13.join(os4.homedir(), ".claude"), "cache", "model-catalog"));
88529
+ const cached2 = claudeModels(path14.join(process.env.CLAUDE_CONFIG_DIR || path14.join(os4.homedir(), ".claude"), "cache", "model-catalog"));
88467
88530
  return cached2.length ? cached2 : [
88468
88531
  { id: "opus", name: "Opus" },
88469
88532
  { id: "sonnet", name: "Sonnet" },
88470
88533
  { id: "haiku", name: "Haiku" }
88471
88534
  ];
88472
88535
  },
88473
- codex: async () => codexModels(path13.join(process.env.CODEX_HOME || path13.join(os4.homedir(), ".codex"), "models_cache.json")),
88536
+ codex: async () => codexModels(path14.join(process.env.CODEX_HOME || path14.join(os4.homedir(), ".codex"), "models_cache.json")),
88474
88537
  opencode: async () => opencodeModels(await output(commandOf(HARNESSES2.opencode)[0], ["models"])),
88475
88538
  cursor: () => cursorModels()
88476
88539
  };
@@ -88524,7 +88587,7 @@ function output(command, args) {
88524
88587
  }
88525
88588
  function codexModels(file2) {
88526
88589
  try {
88527
- const cache = JSON.parse(fs10.readFileSync(file2, "utf8"));
88590
+ const cache = JSON.parse(fs11.readFileSync(file2, "utf8"));
88528
88591
  if (!Array.isArray(cache.models)) return [];
88529
88592
  return cache.models.flatMap(
88530
88593
  (m) => m.visibility === "list" && typeof m.slug === "string" && m.slug ? [{ id: m.slug, name: typeof m.display_name === "string" && m.display_name ? m.display_name : m.slug }] : []
@@ -88536,13 +88599,13 @@ function codexModels(file2) {
88536
88599
  function claudeModels(dir) {
88537
88600
  let files;
88538
88601
  try {
88539
- files = fs10.readdirSync(dir).filter((f2) => f2.endsWith(".json"));
88602
+ files = fs11.readdirSync(dir).filter((f2) => f2.endsWith(".json"));
88540
88603
  } catch {
88541
88604
  return [];
88542
88605
  }
88543
88606
  const catalogs = files.flatMap((f2) => {
88544
88607
  try {
88545
- const data = obj(JSON.parse(fs10.readFileSync(path13.join(dir, f2), "utf8")));
88608
+ const data = obj(JSON.parse(fs11.readFileSync(path14.join(dir, f2), "utf8")));
88546
88609
  const catalog = obj(data.catalog);
88547
88610
  const surface = str(catalog.surface);
88548
88611
  return surface === "cc" || surface === "ccd" ? [{ surface, at: Number(data.fetchedAt) || 0, config: obj(catalog.config) }] : [];
@@ -88849,36 +88912,36 @@ async function askDueAgents(machineId2, now = Date.now()) {
88849
88912
 
88850
88913
  // src/akb-version.ts
88851
88914
  import { execFile } from "node:child_process";
88852
- import fs15 from "node:fs";
88853
- import path18 from "node:path";
88915
+ import fs16 from "node:fs";
88916
+ import path19 from "node:path";
88854
88917
 
88855
88918
  // src/akb.ts
88856
88919
  import { spawn as spawn3 } from "node:child_process";
88857
- import fs11 from "node:fs";
88858
- import path14 from "node:path";
88920
+ import fs12 from "node:fs";
88921
+ import path15 from "node:path";
88859
88922
  import { fileURLToPath } from "node:url";
88860
- var akbHome = () => path14.join(runnerHome(), "bin", "akb");
88923
+ var akbHome = () => path15.join(runnerHome(), "bin", "akb");
88861
88924
  var BUNDLED_SHIM = `bundled-${AKB_VERSION}`;
88862
88925
  var bundledAkb = () => ({
88863
88926
  command: process.execPath,
88864
- args: [path14.join(path14.dirname(fileURLToPath(import.meta.url)), "akb", "bin", "ai4kanban.mjs")],
88927
+ args: [path15.join(path15.dirname(fileURLToPath(import.meta.url)), "akb", "bin", "ai4kanban.mjs")],
88865
88928
  version: AKB_VERSION,
88866
- shim: path14.join(akbHome(), BUNDLED_SHIM)
88929
+ shim: path15.join(akbHome(), BUNDLED_SHIM)
88867
88930
  });
88868
88931
  var quote = (s) => /^[\w./:@=-]+$/.test(s) ? s : `"${s}"`;
88869
88932
  function shimDir(akb) {
88870
88933
  const dir = akb.shim;
88871
88934
  const line = [akb.command, ...akb.args].map(quote).join(" ");
88872
- const [file2, text3] = process.platform === "win32" ? [path14.join(dir, "akb.cmd"), `@${line} %*\r
88873
- `] : [path14.join(dir, "akb"), `#!/bin/sh
88935
+ const [file2, text3] = process.platform === "win32" ? [path15.join(dir, "akb.cmd"), `@${line} %*\r
88936
+ `] : [path15.join(dir, "akb"), `#!/bin/sh
88874
88937
  exec ${line} "$@"
88875
88938
  `];
88876
88939
  try {
88877
- if (fs11.readFileSync(file2, "utf8") === text3) return dir;
88940
+ if (fs12.readFileSync(file2, "utf8") === text3) return dir;
88878
88941
  } catch {
88879
88942
  }
88880
- fs11.mkdirSync(dir, { recursive: true });
88881
- fs11.writeFileSync(file2, text3, { mode: 493 });
88943
+ fs12.mkdirSync(dir, { recursive: true });
88944
+ fs12.writeFileSync(file2, text3, { mode: 493 });
88882
88945
  return dir;
88883
88946
  }
88884
88947
  function akbEnv(akb, board, task, line) {
@@ -88886,7 +88949,7 @@ function akbEnv(akb, board, task, line) {
88886
88949
  const dropped = ["AI4KANBAN_BOARD", "AI4KANBAN_TASK", "AI4KANBAN_EXECUTION", LANGUAGE_LINE_ENV, SCHEDULED_AGENTS_ENV];
88887
88950
  for (const [k, v2] of Object.entries(process.env)) if (!k.startsWith("KANBAN_") && !dropped.includes(k)) env2[k] = v2;
88888
88951
  const key = Object.keys(env2).find((k) => k.toUpperCase() === "PATH") ?? "PATH";
88889
- env2[key] = [shimDir(akb), env2[key]].filter(Boolean).join(path14.delimiter);
88952
+ env2[key] = [shimDir(akb), env2[key]].filter(Boolean).join(path15.delimiter);
88890
88953
  env2.AI4KANBAN_COMMAND = "akb";
88891
88954
  if (board) env2.AI4KANBAN_BOARD = board;
88892
88955
  if (task) env2.AI4KANBAN_TASK = task;
@@ -88935,23 +88998,23 @@ function answerOf2(ran) {
88935
88998
 
88936
88999
  // src/cache.ts
88937
89000
  import { spawnSync as spawnSync3 } from "node:child_process";
88938
- import fs13 from "node:fs";
88939
- import path16 from "node:path";
89001
+ import fs14 from "node:fs";
89002
+ import path17 from "node:path";
88940
89003
 
88941
89004
  // src/folders.ts
88942
- import fs12 from "node:fs";
88943
- import path15 from "node:path";
88944
- var projectsFile = () => path15.join(runnerHome(), "projects.json");
89005
+ import fs13 from "node:fs";
89006
+ import path16 from "node:path";
89007
+ var projectsFile = () => path16.join(runnerHome(), "projects.json");
88945
89008
  function realpath(p2) {
88946
89009
  try {
88947
- return fs12.realpathSync(p2);
89010
+ return fs13.realpathSync(p2);
88948
89011
  } catch {
88949
89012
  return null;
88950
89013
  }
88951
89014
  }
88952
89015
  function readList() {
88953
89016
  try {
88954
- const held4 = JSON.parse(fs12.readFileSync(projectsFile(), "utf8"));
89017
+ const held4 = JSON.parse(fs13.readFileSync(projectsFile(), "utf8"));
88955
89018
  return Array.isArray(held4.projects) ? held4.projects.filter((p2) => typeof p2 === "string") : [];
88956
89019
  } catch {
88957
89020
  return [];
@@ -88965,32 +89028,32 @@ function allowedProjects() {
88965
89028
  var IDLE_MS = 7 * 24 * 60 * 6e4;
88966
89029
  var SWEEP_MS = 24 * 60 * 6e4;
88967
89030
  var PROJECT_ID = /^[\w-]{1,100}$/;
88968
- var cacheRoot = () => path16.join(runnerHome(), "cache");
89031
+ var cacheRoot = () => path17.join(runnerHome(), "cache");
88969
89032
  function cacheDir(project) {
88970
89033
  if (!PROJECT_ID.test(project)) throw new Error("This job names no project.");
88971
- return path16.join(cacheRoot(), project);
89034
+ return path17.join(cacheRoot(), project);
88972
89035
  }
88973
- var akbDir = (dir) => path16.join(dir, ".akb");
88974
- var usedFile = (dir) => path16.join(akbDir(dir), "used.json");
88975
- var unsentFile = (dir) => path16.join(akbDir(dir), "unsent");
89036
+ var akbDir = (dir) => path17.join(dir, ".akb");
89037
+ var usedFile = (dir) => path17.join(akbDir(dir), "used.json");
89038
+ var unsentFile = (dir) => path17.join(akbDir(dir), "unsent");
88976
89039
  function markUsed(dir, at2 = Date.now()) {
88977
- fs13.mkdirSync(akbDir(dir), { recursive: true });
88978
- fs13.writeFileSync(usedFile(dir), `${JSON.stringify({ at: at2 })}
89040
+ fs14.mkdirSync(akbDir(dir), { recursive: true });
89041
+ fs14.writeFileSync(usedFile(dir), `${JSON.stringify({ at: at2 })}
88979
89042
  `);
88980
89043
  }
88981
89044
  function usedAt(dir) {
88982
89045
  try {
88983
- const at2 = JSON.parse(fs13.readFileSync(usedFile(dir), "utf8")).at;
89046
+ const at2 = JSON.parse(fs14.readFileSync(usedFile(dir), "utf8")).at;
88984
89047
  return typeof at2 === "number" ? at2 : null;
88985
89048
  } catch {
88986
89049
  return null;
88987
89050
  }
88988
89051
  }
88989
- var unsent = (dir) => fs13.existsSync(unsentFile(dir));
89052
+ var unsent = (dir) => fs14.existsSync(unsentFile(dir));
88990
89053
  function markUnsent(dir, unsent2) {
88991
- if (!unsent2) return void fs13.rmSync(unsentFile(dir), { force: true });
88992
- fs13.mkdirSync(akbDir(dir), { recursive: true });
88993
- fs13.writeFileSync(unsentFile(dir), "");
89054
+ if (!unsent2) return void fs14.rmSync(unsentFile(dir), { force: true });
89055
+ fs14.mkdirSync(akbDir(dir), { recursive: true });
89056
+ fs14.writeFileSync(unsentFile(dir), "");
88994
89057
  }
88995
89058
  var holds = /* @__PURE__ */ new Map();
88996
89059
  function hold(dir) {
@@ -89006,8 +89069,8 @@ function hold(dir) {
89006
89069
  }
89007
89070
  function paused(dir) {
89008
89071
  try {
89009
- const file2 = path16.join(projectStateDir(path16.join(akbDir(dir), "kanban")), SESSIONS_FILE);
89010
- const held4 = JSON.parse(fs13.readFileSync(file2, "utf8"));
89072
+ const file2 = path17.join(projectStateDir(path17.join(akbDir(dir), "kanban")), SESSIONS_FILE);
89073
+ const held4 = JSON.parse(fs14.readFileSync(file2, "utf8"));
89011
89074
  return (held4.deliveries ?? []).some((d2) => d2.status === "active");
89012
89075
  } catch {
89013
89076
  return false;
@@ -89020,11 +89083,11 @@ function clearAkb(dir) {
89020
89083
  if (kept2(dir)) return false;
89021
89084
  let entries;
89022
89085
  try {
89023
- entries = fs13.readdirSync(akbDir(dir));
89086
+ entries = fs14.readdirSync(akbDir(dir));
89024
89087
  } catch {
89025
89088
  return true;
89026
89089
  }
89027
- for (const name of entries) fs13.rmSync(path16.join(akbDir(dir), name), { recursive: true, force: true });
89090
+ for (const name of entries) fs14.rmSync(path17.join(akbDir(dir), name), { recursive: true, force: true });
89028
89091
  spawnSync3("git", ["-C", dir, "worktree", "prune"], { stdio: "ignore" });
89029
89092
  return true;
89030
89093
  }
@@ -89039,7 +89102,7 @@ async function readAccess(machineId2) {
89039
89102
  }
89040
89103
  function realpath2(p2) {
89041
89104
  try {
89042
- return fs13.realpathSync(p2);
89105
+ return fs14.realpathSync(p2);
89043
89106
  } catch {
89044
89107
  return p2;
89045
89108
  }
@@ -89054,22 +89117,22 @@ async function sweepCache(machineId2, now = Date.now()) {
89054
89117
  const swept = [];
89055
89118
  let cached2 = [];
89056
89119
  try {
89057
- cached2 = fs13.readdirSync(cacheRoot(), { withFileTypes: true });
89120
+ cached2 = fs14.readdirSync(cacheRoot(), { withFileTypes: true });
89058
89121
  } catch {
89059
89122
  }
89060
89123
  for (const e2 of cached2) {
89061
- const dir = path16.join(cacheRoot(), e2.name);
89124
+ const dir = path17.join(cacheRoot(), e2.name);
89062
89125
  if (!e2.isDirectory() || kept2(dir)) continue;
89063
89126
  const gone = access && !access.projects.has(e2.name);
89064
89127
  if (idle(dir, now) || gone) {
89065
- fs13.rmSync(dir, { recursive: true, force: true });
89128
+ fs14.rmSync(dir, { recursive: true, force: true });
89066
89129
  swept.push(dir);
89067
89130
  }
89068
89131
  }
89069
89132
  for (const dir of allowedProjects()) {
89070
89133
  let held4 = [];
89071
89134
  try {
89072
- held4 = fs13.readdirSync(akbDir(dir));
89135
+ held4 = fs14.readdirSync(akbDir(dir));
89073
89136
  } catch {
89074
89137
  }
89075
89138
  if (!held4.length) continue;
@@ -89080,31 +89143,31 @@ async function sweepCache(machineId2, now = Date.now()) {
89080
89143
  }
89081
89144
  function removeLegacy() {
89082
89145
  const home4 = runnerHome();
89083
- for (const name of ["boards", "akb-bin"]) fs13.rmSync(path16.join(home4, name), { recursive: true, force: true });
89146
+ for (const name of ["boards", "akb-bin"]) fs14.rmSync(path17.join(home4, name), { recursive: true, force: true });
89084
89147
  let entries = [];
89085
89148
  try {
89086
- entries = fs13.readdirSync(home4, { withFileTypes: true });
89149
+ entries = fs14.readdirSync(home4, { withFileTypes: true });
89087
89150
  } catch {
89088
89151
  return;
89089
89152
  }
89090
89153
  for (const e2 of entries) {
89091
- const account = path16.join(home4, e2.name);
89092
- if (!e2.isDirectory() || !fs13.existsSync(path16.join(account, "unfiled"))) continue;
89093
- fs13.rmSync(path16.join(account, "unfiled"), { recursive: true, force: true });
89094
- if (!fs13.readdirSync(account).length) fs13.rmdirSync(account);
89154
+ const account = path17.join(home4, e2.name);
89155
+ if (!e2.isDirectory() || !fs14.existsSync(path17.join(account, "unfiled"))) continue;
89156
+ fs14.rmSync(path17.join(account, "unfiled"), { recursive: true, force: true });
89157
+ if (!fs14.readdirSync(account).length) fs14.rmdirSync(account);
89095
89158
  }
89096
89159
  }
89097
89160
 
89098
89161
  // src/update.ts
89099
89162
  import crypto2 from "node:crypto";
89100
- import fs14 from "node:fs";
89101
- import path17 from "node:path";
89163
+ import fs15 from "node:fs";
89164
+ import path18 from "node:path";
89102
89165
  import zlib from "node:zlib";
89103
89166
  var TIMEOUT_MS2 = 6e4;
89104
89167
  var registry = () => (process.env.npm_config_registry || "https://registry.npmjs.org").replace(/\/+$/, "");
89105
89168
  async function installPackage(pkg, version, nodeVersion = process.versions.node) {
89106
89169
  const dir = pkg.dirOf(version);
89107
- if (fs14.existsSync(pkg.entryOf(dir))) return { ok: true, dir };
89170
+ if (fs15.existsSync(pkg.entryOf(dir))) return { ok: true, dir };
89108
89171
  let meta;
89109
89172
  let tarball;
89110
89173
  try {
@@ -89125,13 +89188,13 @@ async function installPackage(pkg, version, nodeVersion = process.versions.node)
89125
89188
  if (!expected || expected !== actual) return { ok: false, why: "integrity", detail: "sha512 mismatch" };
89126
89189
  const staging = `${dir}.${process.pid}.tmp`;
89127
89190
  try {
89128
- fs14.rmSync(staging, { recursive: true, force: true });
89191
+ fs15.rmSync(staging, { recursive: true, force: true });
89129
89192
  extract(zlib.gunzipSync(tarball), staging);
89130
- if (!fs14.existsSync(pkg.entryOf(staging))) throw new Error(`the package carries no ${path17.relative(staging, pkg.entryOf(staging))}`);
89131
- fs14.rmSync(dir, { recursive: true, force: true });
89132
- fs14.renameSync(staging, dir);
89193
+ if (!fs15.existsSync(pkg.entryOf(staging))) throw new Error(`the package carries no ${path18.relative(staging, pkg.entryOf(staging))}`);
89194
+ fs15.rmSync(dir, { recursive: true, force: true });
89195
+ fs15.renameSync(staging, dir);
89133
89196
  } catch (e2) {
89134
- fs14.rmSync(staging, { recursive: true, force: true });
89197
+ fs15.rmSync(staging, { recursive: true, force: true });
89135
89198
  return { ok: false, why: "integrity", detail: e2 instanceof Error ? e2.message : String(e2) };
89136
89199
  }
89137
89200
  return { ok: true, dir };
@@ -89161,12 +89224,12 @@ function extract(tar, into) {
89161
89224
  if (type !== "0" && type !== "5") continue;
89162
89225
  const relative = name.replace(/^[^/]+\//, "");
89163
89226
  if (!relative) continue;
89164
- const target = path17.resolve(into, relative);
89165
- if (!target.startsWith(path17.resolve(into) + path17.sep)) throw new Error(`refusing ${name}`);
89166
- if (type === "5") fs14.mkdirSync(target, { recursive: true });
89227
+ const target = path18.resolve(into, relative);
89228
+ if (!target.startsWith(path18.resolve(into) + path18.sep)) throw new Error(`refusing ${name}`);
89229
+ if (type === "5") fs15.mkdirSync(target, { recursive: true });
89167
89230
  else {
89168
- fs14.mkdirSync(path17.dirname(target), { recursive: true });
89169
- fs14.writeFileSync(target, body);
89231
+ fs15.mkdirSync(path18.dirname(target), { recursive: true });
89232
+ fs15.writeFileSync(target, body);
89170
89233
  }
89171
89234
  }
89172
89235
  }
@@ -89214,33 +89277,33 @@ function meets(version, comparator) {
89214
89277
  // src/akb-version.ts
89215
89278
  var CHECK_MS2 = 1e4;
89216
89279
  var VERSION2 = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
89217
- var stateFile = () => path18.join(akbHome(), "state.json");
89218
- var akbVersionDir = (version) => path18.join(akbHome(), version);
89219
- var entryOf = (dir) => path18.join(dir, "bin", "ai4kanban.mjs");
89280
+ var stateFile = () => path19.join(akbHome(), "state.json");
89281
+ var akbVersionDir = (version) => path19.join(akbHome(), version);
89282
+ var entryOf = (dir) => path19.join(dir, "bin", "ai4kanban.mjs");
89220
89283
  var AKB = { name: "wildcrane", dirOf: akbVersionDir, entryOf };
89221
89284
  function readAkbState() {
89222
89285
  try {
89223
- const held4 = JSON.parse(fs15.readFileSync(stateFile(), "utf8"));
89286
+ const held4 = JSON.parse(fs16.readFileSync(stateFile(), "utf8"));
89224
89287
  return { current: held4.current ?? null, bad: held4.bad ?? [] };
89225
89288
  } catch {
89226
89289
  return { current: null, bad: [] };
89227
89290
  }
89228
89291
  }
89229
89292
  function writeAkbState(state) {
89230
- fs15.mkdirSync(akbHome(), { recursive: true });
89293
+ fs16.mkdirSync(akbHome(), { recursive: true });
89231
89294
  const staging = `${stateFile()}.${process.pid}.tmp`;
89232
- fs15.writeFileSync(staging, `${JSON.stringify(state, null, 2)}
89295
+ fs16.writeFileSync(staging, `${JSON.stringify(state, null, 2)}
89233
89296
  `);
89234
- fs15.renameSync(staging, stateFile());
89297
+ fs16.renameSync(staging, stateFile());
89235
89298
  }
89236
89299
  function contractOf(dir) {
89237
89300
  try {
89238
- return JSON.parse(fs15.readFileSync(path18.join(dir, "package.json"), "utf8")).akbContract ?? null;
89301
+ return JSON.parse(fs16.readFileSync(path19.join(dir, "package.json"), "utf8")).akbContract ?? null;
89239
89302
  } catch {
89240
89303
  return null;
89241
89304
  }
89242
89305
  }
89243
- var usable = (dir) => fs15.existsSync(entryOf(dir)) && contractOf(dir) === AKB_CONTRACT;
89306
+ var usable = (dir) => fs16.existsSync(entryOf(dir)) && contractOf(dir) === AKB_CONTRACT;
89244
89307
  function currentAkb() {
89245
89308
  const { current } = readAkbState();
89246
89309
  if (!current) return bundledAkb();
@@ -89248,10 +89311,10 @@ function currentAkb() {
89248
89311
  if (!usable(dir)) return bundledAkb();
89249
89312
  try {
89250
89313
  const now = /* @__PURE__ */ new Date();
89251
- fs15.utimesSync(dir, now, now);
89314
+ fs16.utimesSync(dir, now, now);
89252
89315
  } catch {
89253
89316
  }
89254
- return { command: process.execPath, args: [entryOf(dir)], version: current, shim: path18.join(dir, "shim") };
89317
+ return { command: process.execPath, args: [entryOf(dir)], version: current, shim: path19.join(dir, "shim") };
89255
89318
  }
89256
89319
  var answers2 = (dir, version) => new Promise((resolve2) => {
89257
89320
  execFile(process.execPath, [entryOf(dir), "--version"], { timeout: CHECK_MS2, killSignal: "SIGKILL" }, (error, stdout) => resolve2(!error && stdout.trim().split(/\s+/).at(-1) === version));
@@ -89291,20 +89354,20 @@ function sweepAkb(now = Date.now()) {
89291
89354
  const { current } = readAkbState();
89292
89355
  let entries = [];
89293
89356
  try {
89294
- entries = fs15.readdirSync(akbHome(), { withFileTypes: true });
89357
+ entries = fs16.readdirSync(akbHome(), { withFileTypes: true });
89295
89358
  } catch {
89296
89359
  return;
89297
89360
  }
89298
89361
  for (const e2 of entries) {
89299
89362
  if (!e2.isDirectory() || e2.name === current || e2.name === BUNDLED_SHIM) continue;
89300
- const dir = path18.join(akbHome(), e2.name);
89301
- if (now - fs15.statSync(dir).mtimeMs > IDLE_MS) fs15.rmSync(dir, { recursive: true, force: true });
89363
+ const dir = path19.join(akbHome(), e2.name);
89364
+ if (now - fs16.statSync(dir).mtimeMs > IDLE_MS) fs16.rmSync(dir, { recursive: true, force: true });
89302
89365
  }
89303
89366
  }
89304
89367
 
89305
89368
  // src/chats.ts
89306
- import fs17 from "node:fs";
89307
- import path20 from "node:path";
89369
+ import fs18 from "node:fs";
89370
+ import path21 from "node:path";
89308
89371
 
89309
89372
  // ../cloud/src/config.ts
89310
89373
  var CLAIM_LEASE_SECONDS = 15 * 60;
@@ -89320,18 +89383,18 @@ var CONTACT_ATTEMPT_WINDOW_SECONDS = 60 * 60;
89320
89383
  var CHAT_KEEP_DAYS = 30;
89321
89384
 
89322
89385
  // ../cli/src/lib/agent/task-chats.ts
89323
- import fs16 from "node:fs";
89324
- import path19 from "node:path";
89386
+ import fs17 from "node:fs";
89387
+ import path20 from "node:path";
89325
89388
  var TASK = /^[\w-]{1,100}$/;
89326
- var taskChatsDir = (home4 = machineHome()) => path19.join(home4, "chats");
89389
+ var taskChatsDir = (home4 = machineHome()) => path20.join(home4, "chats");
89327
89390
  function taskChatFile(task, home4) {
89328
89391
  if (!TASK.test(task)) throw new Error(`${task} is not a task id.`);
89329
- return path19.join(taskChatsDir(home4), `${task}.jsonl`);
89392
+ return path20.join(taskChatsDir(home4), `${task}.jsonl`);
89330
89393
  }
89331
89394
  function readTaskChat(task, home4) {
89332
89395
  let text3;
89333
89396
  try {
89334
- text3 = fs16.readFileSync(taskChatFile(task, home4), "utf8");
89397
+ text3 = fs17.readFileSync(taskChatFile(task, home4), "utf8");
89335
89398
  } catch {
89336
89399
  return [];
89337
89400
  }
@@ -89352,18 +89415,18 @@ var DAY_MS = 24 * 60 * 6e4;
89352
89415
  var REVIEW_EVERY_MS = DAY_MS;
89353
89416
  var KEEP_MS2 = CHAT_KEEP_DAYS * DAY_MS;
89354
89417
  var home3 = () => runnerHome();
89355
- var reviewedFile = () => path20.join(taskChatsDir(home3()), "reviewed.json");
89418
+ var reviewedFile = () => path21.join(taskChatsDir(home3()), "reviewed.json");
89356
89419
  function readReviewed() {
89357
89420
  try {
89358
- const held4 = JSON.parse(fs17.readFileSync(reviewedFile(), "utf8"));
89421
+ const held4 = JSON.parse(fs18.readFileSync(reviewedFile(), "utf8"));
89359
89422
  return { checkedAt: Number(held4.checkedAt) || 0, tasks: held4.tasks ?? {} };
89360
89423
  } catch {
89361
89424
  return { checkedAt: 0, tasks: {} };
89362
89425
  }
89363
89426
  }
89364
89427
  function writeReviewed(reviewed) {
89365
- fs17.mkdirSync(path20.dirname(reviewedFile()), { recursive: true });
89366
- fs17.writeFileSync(reviewedFile(), `${JSON.stringify(reviewed, null, 2)}
89428
+ fs18.mkdirSync(path21.dirname(reviewedFile()), { recursive: true });
89429
+ fs18.writeFileSync(reviewedFile(), `${JSON.stringify(reviewed, null, 2)}
89367
89430
  `);
89368
89431
  }
89369
89432
  function keepChat(job, reply, at2 = Date.now()) {
@@ -89373,16 +89436,16 @@ function keepChat(job, reply, at2 = Date.now()) {
89373
89436
  { id: job.id, role: "agent", text: reply, at: at2, runtime: job.runtime, model: job.model ?? null }
89374
89437
  ];
89375
89438
  const file2 = taskChatFile(job.task, home3());
89376
- fs17.mkdirSync(path20.dirname(file2), { recursive: true });
89377
- fs17.appendFileSync(file2, lines.map((l2) => `${JSON.stringify(l2)}
89439
+ fs18.mkdirSync(path21.dirname(file2), { recursive: true });
89440
+ fs18.appendFileSync(file2, lines.map((l2) => `${JSON.stringify(l2)}
89378
89441
  `).join(""));
89379
89442
  }
89380
89443
  function keepCloudChat(task, messages) {
89381
89444
  const file2 = taskChatFile(task, home3());
89382
- if (!messages.length) return fs17.rmSync(file2, { force: true });
89445
+ if (!messages.length) return fs18.rmSync(file2, { force: true });
89383
89446
  const lines = messages.map((m) => ({ id: m.id, role: m.role, text: m.body, at: Date.parse(m.created_at) || Date.now() }));
89384
- fs17.mkdirSync(path20.dirname(file2), { recursive: true });
89385
- fs17.writeFileSync(file2, lines.map((l2) => `${JSON.stringify(l2)}
89447
+ fs18.mkdirSync(path21.dirname(file2), { recursive: true });
89448
+ fs18.writeFileSync(file2, lines.map((l2) => `${JSON.stringify(l2)}
89386
89449
  `).join(""));
89387
89450
  }
89388
89451
  async function keepDecisions(task, read2) {
@@ -89400,13 +89463,13 @@ async function keepDecisions(task, read2) {
89400
89463
  const ids = new Set(comments.map((c2) => c2.id));
89401
89464
  const lines = [...readTaskChat(task, home3()).filter((l2) => !ids.has(l2.id)), ...comments].sort((a3, b2) => a3.at - b2.at);
89402
89465
  const file2 = taskChatFile(task, home3());
89403
- fs17.mkdirSync(path20.dirname(file2), { recursive: true });
89404
- fs17.writeFileSync(file2, lines.map((l2) => `${JSON.stringify(l2)}
89466
+ fs18.mkdirSync(path21.dirname(file2), { recursive: true });
89467
+ fs18.writeFileSync(file2, lines.map((l2) => `${JSON.stringify(l2)}
89405
89468
  `).join(""));
89406
89469
  }
89407
89470
  var tasksKept = () => {
89408
89471
  try {
89409
- return fs17.readdirSync(taskChatsDir(home3())).map((n) => /^([\w-]{1,100})\.jsonl$/.exec(n)?.[1]).filter((t2) => !!t2);
89472
+ return fs18.readdirSync(taskChatsDir(home3())).map((n) => /^([\w-]{1,100})\.jsonl$/.exec(n)?.[1]).filter((t2) => !!t2);
89410
89473
  } catch {
89411
89474
  return [];
89412
89475
  }
@@ -89417,9 +89480,9 @@ function pruneChats(now = Date.now()) {
89417
89480
  const kept4 = lines.filter((l2) => now - l2.at < KEEP_MS2);
89418
89481
  if (kept4.length === lines.length) continue;
89419
89482
  const file2 = taskChatFile(task, home3());
89420
- if (kept4.length) fs17.writeFileSync(file2, kept4.map((l2) => `${JSON.stringify(l2)}
89483
+ if (kept4.length) fs18.writeFileSync(file2, kept4.map((l2) => `${JSON.stringify(l2)}
89421
89484
  `).join(""));
89422
- else fs17.rmSync(file2, { force: true });
89485
+ else fs18.rmSync(file2, { force: true });
89423
89486
  }
89424
89487
  }
89425
89488
  var lastOf = (task) => readTaskChat(task, home3()).at(-1);
@@ -89457,8 +89520,8 @@ async function reviewChats(machineId2, runtimes, now = Date.now()) {
89457
89520
  }
89458
89521
 
89459
89522
  // src/board-watch.ts
89460
- import fs21 from "node:fs";
89461
- import path24 from "node:path";
89523
+ import fs22 from "node:fs";
89524
+ import path25 from "node:path";
89462
89525
 
89463
89526
  // ../cloud/src/board-files.ts
89464
89527
  var ROOTS = ["todo/", ".archive/", "memory/", "workflows/", "triage/", "assets/", "rules/"];
@@ -89509,11 +89572,186 @@ function cardFront(body) {
89509
89572
  return out;
89510
89573
  }
89511
89574
 
89575
+ // ../artifact-sdk/src/internal/references.ts
89576
+ var isObject = (v2) => !!v2 && typeof v2 === "object" && !Array.isArray(v2);
89577
+ function bodyReferences(body) {
89578
+ const lines = body.replace(/<!--[^]*?-->/g, "").split(/\r?\n/);
89579
+ let fence = "";
89580
+ let inTag = false;
89581
+ const out = [];
89582
+ const visible = [];
89583
+ for (const line of lines) {
89584
+ const delimiter = /^ {0,3}(`{3,}|~{3,})/.exec(line)?.[1];
89585
+ if (fence) {
89586
+ if (delimiter && delimiter[0] === fence[0] && delimiter.length >= fence.length) fence = "";
89587
+ visible.push("");
89588
+ continue;
89589
+ }
89590
+ if (delimiter) {
89591
+ fence = delimiter;
89592
+ visible.push("");
89593
+ continue;
89594
+ }
89595
+ const kept4 = inTag || !/^(?: {4}|\t)/.test(line);
89596
+ visible.push(kept4 ? line : "");
89597
+ if (kept4 && /^ {0,3}<(?:Asset|Mockup|Compare|Storyboard)\b/.test(line)) inTag = true;
89598
+ if (line.includes("/>")) inTag = false;
89599
+ }
89600
+ const text3 = visible.join("\n").replace(/(`+)[^]*?\1/g, "");
89601
+ const tag = /<(Asset|Mockup|Compare|Storyboard)\b([^<>]*?)\/>/g;
89602
+ const blocks = text3.matchAll(/^ {0,3}(?:<(?:Asset|Mockup|Compare|Storyboard)\b[^<>]*?\/>[ \t]*(?:\n[ \t]*)?)+$/gm);
89603
+ for (const block of blocks) {
89604
+ const tags = [...block[0].matchAll(tag)];
89605
+ for (const m of tags) {
89606
+ const a3 = {};
89607
+ for (const p2 of m[2].matchAll(/([\w-]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/g)) a3[p2[1]] = p2[2] ?? p2[3];
89608
+ if (m[1] === "Compare") {
89609
+ if (!a3.a || !a3.b) continue;
89610
+ for (const k of ["a", "b", "shots"]) if (a3[k]) out.push({ src: a3[k], purpose: "compare" });
89611
+ } else if (a3.src) {
89612
+ out.push({ src: a3.src, purpose: m[1] === "Storyboard" ? "storyboard" : "asset" });
89613
+ if (a3.variants) {
89614
+ try {
89615
+ const v2 = JSON.parse(a3.variants);
89616
+ if (isObject(v2)) {
89617
+ for (const src of Object.values(v2)) if (typeof src === "string" && src) out.push({ src, purpose: "asset" });
89618
+ }
89619
+ } catch {
89620
+ }
89621
+ }
89622
+ }
89623
+ }
89624
+ }
89625
+ for (const m of text3.replace(tag, "").matchAll(/\]\(\s*<?([^\s)>]+)>?(?:\s+["'][^]*?["'])?\s*\)/g)) out.push({ src: m[1], purpose: "asset" });
89626
+ return out;
89627
+ }
89628
+ function mapReferences(data, kind, map2) {
89629
+ const c2 = structuredClone(data);
89630
+ if (!isObject(c2)) return c2;
89631
+ const field = (obj3, key, purpose = "asset") => {
89632
+ if (typeof obj3[key] === "string" && obj3[key]) obj3[key] = map2(obj3[key], purpose);
89633
+ };
89634
+ if (kind === "compare") for (const key of ["a", "b", "shots"]) field(c2, key, "compare");
89635
+ if (kind === "storyboard") {
89636
+ if (Array.isArray(c2.shots)) for (const shot of c2.shots) {
89637
+ if (isObject(shot) && Array.isArray(shot.frames)) {
89638
+ for (const frame2 of shot.frames) if (isObject(frame2)) field(frame2, "src");
89639
+ }
89640
+ }
89641
+ if (Array.isArray(c2.slides)) {
89642
+ for (const slide of c2.slides) if (isObject(slide)) {
89643
+ if (isObject(slide.preview)) field(slide.preview, "src");
89644
+ if (Array.isArray(slide.assets)) slide.assets = slide.assets.map((src) => typeof src === "string" ? map2(src, "asset") : src);
89645
+ }
89646
+ }
89647
+ }
89648
+ return c2;
89649
+ }
89650
+ function jsonKind(data, purpose) {
89651
+ if (!isObject(data)) return "asset";
89652
+ if (purpose !== "asset" && typeof data.a === "string" && typeof data.b === "string") return "compare";
89653
+ return purpose !== "asset" && (Array.isArray(data.shots) || Array.isArray(data.slides)) ? "storyboard" : "asset";
89654
+ }
89655
+ var TYPES = {
89656
+ ".png": "image/png",
89657
+ ".jpg": "image/jpeg",
89658
+ ".jpeg": "image/jpeg",
89659
+ ".gif": "image/gif",
89660
+ ".webp": "image/webp",
89661
+ ".svg": "image/svg+xml",
89662
+ ".mp4": "video/mp4",
89663
+ ".webm": "video/webm",
89664
+ ".mp3": "audio/mpeg",
89665
+ ".pdf": "application/pdf",
89666
+ ".html": "text/html",
89667
+ ".md": "text/markdown",
89668
+ ".mdx": "text/markdown",
89669
+ ".mov": "video/quicktime",
89670
+ ".wav": "audio/wav",
89671
+ ".m4a": "audio/mp4",
89672
+ ".json": "application/json",
89673
+ ".txt": "text/plain"
89674
+ };
89675
+ function kindOf(ext) {
89676
+ if (ext === ".tsx" || ext === ".html") return "html";
89677
+ if (ext === ".md" || ext === ".mdx") return "markdown";
89678
+ const type = TYPES[ext] ?? "";
89679
+ if (type.startsWith("image/")) return "image";
89680
+ return type.startsWith("video/") || type.startsWith("audio/") ? "video" : "asset";
89681
+ }
89682
+
89683
+ // ../artifact-sdk/src/internal/collect.ts
89684
+ function collect(body, card, host) {
89685
+ const nodes = /* @__PURE__ */ new Map();
89686
+ const active = /* @__PURE__ */ new Set();
89687
+ const out = [];
89688
+ const visit = (src, purpose, owner) => {
89689
+ let resolved;
89690
+ try {
89691
+ resolved = host.resolve(src, owner);
89692
+ } catch (error) {
89693
+ throw new Error(`${owner}: ${src}: ${error instanceof Error ? error.message : String(error)}`);
89694
+ }
89695
+ if (!resolved) return null;
89696
+ const { key, file: file2 } = resolved;
89697
+ if (active.has(key)) throw new Error(`${owner}: ${src}: \u8D44\u6E90\u4F9D\u8D56\u5FAA\u73AF`);
89698
+ const held4 = nodes.get(key);
89699
+ if (held4) {
89700
+ if (owner === card) held4.aliases.add(src);
89701
+ if (!file2 || !/\.json$/i.test(file2) || purpose === "asset" || held4.kind !== "asset") return key;
89702
+ out.splice(out.indexOf(held4), 1);
89703
+ }
89704
+ const node = held4 ?? { key, file: file2, kind: "asset", dependencies: /* @__PURE__ */ new Map(), aliases: /* @__PURE__ */ new Set([key]) };
89705
+ if (owner === card) node.aliases.add(src);
89706
+ nodes.set(key, node);
89707
+ active.add(key);
89708
+ if (file2 && /\.json$/i.test(file2)) {
89709
+ try {
89710
+ node.data = host.readJSON(file2);
89711
+ } catch {
89712
+ throw new Error(`${owner}: ${src}: JSON \u65E0\u6CD5\u89E3\u6790`);
89713
+ }
89714
+ node.kind = jsonKind(node.data, purpose);
89715
+ mapReferences(node.data, node.kind, (child, use) => {
89716
+ const key2 = visit(child, use, file2);
89717
+ if (key2) node.dependencies.set(child, key2);
89718
+ return child;
89719
+ });
89720
+ } else if (!file2) {
89721
+ for (const child of host.retained(key)) {
89722
+ const key2 = visit(child.src, child.purpose, card);
89723
+ if (key2) node.dependencies.set(child.src, key2);
89724
+ }
89725
+ }
89726
+ active.delete(key);
89727
+ out.push(node);
89728
+ return key;
89729
+ };
89730
+ for (const ref of bodyReferences(body)) visit(ref.src, ref.purpose, card);
89731
+ const sorted = [];
89732
+ const done = /* @__PURE__ */ new Set();
89733
+ const sorting = /* @__PURE__ */ new Set();
89734
+ const order = (node) => {
89735
+ if (done.has(node.key)) return;
89736
+ if (sorting.has(node.key)) throw new Error(`${node.key}: \u8D44\u6E90\u4F9D\u8D56\u5FAA\u73AF`);
89737
+ sorting.add(node.key);
89738
+ for (const key of node.dependencies.values()) {
89739
+ const child = nodes.get(key);
89740
+ if (child) order(child);
89741
+ }
89742
+ sorting.delete(node.key);
89743
+ done.add(node.key);
89744
+ sorted.push(node);
89745
+ };
89746
+ for (const node of out) order(node);
89747
+ return sorted;
89748
+ }
89749
+
89512
89750
  // src/task-folder.ts
89513
89751
  import { spawnSync as spawnSync4 } from "node:child_process";
89514
89752
  import crypto3 from "node:crypto";
89515
- import fs20 from "node:fs";
89516
- import path23 from "node:path";
89753
+ import fs21 from "node:fs";
89754
+ import path24 from "node:path";
89517
89755
 
89518
89756
  // node_modules/node-diff3/dist/diff3.mjs
89519
89757
  function LCS(buffer1, buffer2) {
@@ -89770,26 +90008,29 @@ function stampItem(content, fields) {
89770
90008
  }
89771
90009
 
89772
90010
  // ../cli/src/lib/agents/shared-memory.ts
89773
- import fs18 from "node:fs";
89774
- import path21 from "node:path";
90011
+ import fs19 from "node:fs";
90012
+ import path22 from "node:path";
90013
+
90014
+ // ../cli/src/agents/builder/AGENT.md
90015
+ var AGENT_default = "---\nname: builder\ndescription: \u5B9E\u73B0\u5E76\u843D\u5730\u5DF2\u6279\u51C6\u7684\u8F6F\u4EF6\u53D8\u66F4\u3002\n---\n\n- **\u5DF2\u6279\u51C6\u7684\u754C\u9762\u9884\u89C8**\uFF1A\u5B9E\u73B0\u5361\u7247\u9884\u89C8\u4E2D\u7684\u6BCF\u4E2A\u5C4F\u5E55\uFF0C\u5728\u684C\u9762\u548C\u624B\u673A\u5BBD\u5EA6\u4E0B\u9010\u4E00\u5BF9\u7167\u5E76\u4FEE\u6B63\u5DEE\u5F02\u3002\n- **\u5DF2\u6279\u51C6\u7684 API \u9884\u89C8**\uFF1A\u6309\u5361\u7247\u9884\u89C8\u5B9E\u73B0\u6BCF\u4E2A\u63A5\u53E3\uFF0C\u5E76\u636E\u6B64\u7F16\u5199\u6587\u6863\u3002\n- **\u5DF2\u6279\u51C6\u7684\u6570\u636E\u5E93\u9884\u89C8**\uFF1A\u7F16\u5199\u8FC1\u79FB\uFF0C\u4F7F\u6700\u7EC8\u7ED3\u6784\u4E0E\u5361\u7247\u6279\u51C6\u7684\u9884\u89C8\u4E00\u81F4\u3002\n- **\u5B8C\u6210\u8FB9\u754C**\uFF1A\u9664\u975E\u5361\u7247\u7684\u76EE\u6807\u672C\u8EAB\u5C31\u662F\u90E8\u7F72\u6216\u53D1\u5E03\uFF0C\u5426\u5219\u53D8\u66F4\u843D\u5730\u4E14\u68C0\u67E5\u901A\u8FC7\u5373\u5B8C\u6210\uFF1B\u4E0D\u8981\u4E3A\u5B8C\u6210\u4EFB\u52A1\u800C\u63A8\u9001\u3001\u90E8\u7F72\u6216\u53D1\u5E03\u3002\n- **\u9700\u8981\u4E0A\u7EBF\u7684\u4E8B\u9879**\uFF1A\u628A\u5FC5\u987B\u4E0A\u7EBF\u624D\u80FD\u8FDB\u884C\u7684\u9A8C\u8BC1\u6216\u6B65\u9AA4\u4EA4\u7ED9\u7528\u6237\uFF0C\u53EA\u5728\u6700\u7EC8\u56DE\u590D\u4E2D\u8BF4\u660E\uFF0C\u4E0D\u521B\u5EFA\u8DDF\u8FDB\u4EFB\u52A1\uFF0C\u4E5F\u4E0D\u63D0\u95EE\u3002\n- **\u68C0\u67E5**\uFF1A\u6B64\u540E\u6CA1\u6709\u4EBA\u518D\u5BA1\u67E5\u5B9E\u73B0\uFF1B\u8FD0\u884C\u53D7\u5F71\u54CD\u8DEF\u5F84\u7684\u9488\u5BF9\u6027\u68C0\u67E5\uFF0C\u4EE5\u53CA\u4ED3\u5E93\u8981\u6C42\u7684\u5168\u90E8\u68C0\u67E5\u3002\n";
89775
90016
 
89776
90017
  // ../cli/src/agents/api-designer/AGENT.md
89777
- var AGENT_default = "---\nname: api-designer\ndescription: Writes the caller's documentation for the APIs a card changes.\nakb:\n when: A card adds or changes a surface a user or third party calls directly, such as HTTP routes, WebSocket messages, MCP tools, or CLI commands and flags. Not a protocol between the project's own components.\n i18n:\n en:\n title: API docs\n zh:\n title: \u63A5\u53E3\u6587\u6863\n description: \u4E3A\u5361\u7247\u6539\u52A8\u7684\u63A5\u53E3\u5199\u51FA\u8C03\u7528\u65B9\u6587\u6863\u3002\n when: \u5361\u7247\u65B0\u589E\u6216\u4FEE\u6539\u7528\u6237\u6216\u7B2C\u4E09\u65B9\u76F4\u63A5\u8C03\u7528\u7684\u63A5\u53E3\u65F6\uFF0C\u5982 HTTP \u8DEF\u7531\u3001WebSocket \u6D88\u606F\u3001MCP \u5DE5\u5177\u3001CLI \u547D\u4EE4\u548C\u53C2\u6570\u3002\u4E0D\u542B\u9879\u76EE\u81EA\u8EAB\u7EC4\u4EF6\u4E4B\u95F4\u7684\u534F\u8BAE\u3002\n---\n\nWrite each surface this card changes as the documentation entry that will be committed, in\nthe project's existing docs style: inputs, outputs, errors and one example call. Use a\n`diff` when an entry already exists, and a `mermaid` `sequenceDiagram` for a message\nexchange. One design, no alternatives, nothing else in the section.\n\n- **Glanceable**: a junior engineer understands each entry in one read. Plain names, short\n lines, no field or error the card does not change.\n";
90018
+ var AGENT_default2 = "---\nname: api-designer\ndescription: Writes the caller's documentation for the APIs a card changes.\nakb:\n when: A card adds or changes a surface a user or third party calls directly, such as HTTP routes, WebSocket messages, MCP tools, or CLI commands and flags. Not a protocol between the project's own components.\n i18n:\n en:\n title: API docs\n zh:\n title: \u63A5\u53E3\u6587\u6863\n description: \u4E3A\u5361\u7247\u6539\u52A8\u7684\u63A5\u53E3\u5199\u51FA\u8C03\u7528\u65B9\u6587\u6863\u3002\n when: \u5361\u7247\u65B0\u589E\u6216\u4FEE\u6539\u7528\u6237\u6216\u7B2C\u4E09\u65B9\u76F4\u63A5\u8C03\u7528\u7684\u63A5\u53E3\u65F6\uFF0C\u5982 HTTP \u8DEF\u7531\u3001WebSocket \u6D88\u606F\u3001MCP \u5DE5\u5177\u3001CLI \u547D\u4EE4\u548C\u53C2\u6570\u3002\u4E0D\u542B\u9879\u76EE\u81EA\u8EAB\u7EC4\u4EF6\u4E4B\u95F4\u7684\u534F\u8BAE\u3002\n---\n\nWrite each surface this card changes as the documentation entry that will be committed, in\nthe project's existing docs style: inputs, outputs, errors and one example call. Use a\n`diff` when an entry already exists, and a `mermaid` `sequenceDiagram` for a message\nexchange. One design, no alternatives, nothing else in the section.\n\n- **Glanceable**: a junior engineer understands each entry in one read. Plain names, short\n lines, no field or error the card does not change.\n";
89778
90019
 
89779
90020
  // ../cli/src/agents/blog-illustrator/AGENT.md
89780
- var AGENT_default2 = "---\nname: blog-illustrator\ndescription: Makes a blog post's cover and in-article images, each with alt text.\nakb:\n after: [blog-planner]\n memoryFiles:\n workspace: [style.md]\n i18n:\n en:\n title: Images\n zh:\n title: \u914D\u56FE\n description: \u6309\u786E\u8BA4\u7684\u5927\u7EB2\u4E3A\u535A\u5BA2\u6587\u7AE0\u65B0\u505A\u5C01\u9762\u548C\u6B63\u6587\u914D\u56FE\uFF0C\u5E76\u5199\u597D\u66FF\u4EE3\u6587\u5B57\u3002\n---\n\nMake the cover and every image the approved outline in ``## By `blog-planner` agent`` names.\nMake every image new for this post; never search for or reuse existing images. On an\nexisting card, redo only the images an outline change affects.\n\n## Methods\n\nChoose one per image:\n\n- **Screenshot**: capture the real product in the state the section describes.\n- **Drawn**: lay out a diagram or chart in HTML/CSS or SVG and render it to PNG.\n- **Generated**: `akb cloud image --aspect <ratio> --tier <tier> --out <file> \"<prompt>\"`, which\n spends the user's Pro credits; never for a product interface. Pass `--tier premium` for a\n cover, `standard` for an in-article image and `economy` for a draft or alternative, unless\n the user asks for better or cheaper, and name the tier beside the image in your section.\n- **Fallback**: when a method cannot deliver, use another and say so in your section.\n\n## Rules\n\n- **Alt text**: one sentence per image saying what it shows; draw it on each in-article image as its caption.\n- **Cover**: the aspect ratio the blog's existing covers use, 16:9 when there are none;\n legible at thumbnail size.\n- **Consistent**: one post's images share palette, fonts and treatment.\n- **Review**: view each image at full size and at the width the article shows it; fix\n defects and recheck.\n\n## Paths\n\n- **Assets**: `<board-state>/assets/<card id>/` \u2014 `cover.png` and `fig-<nn>.png`, each with\n the source that renders it beside it.\n- **Hand over**: in your section, one line per image \u2014 file, the outline section it belongs\n to, and its alt text.\n\n## Memory\n\nKeep two files in your memory folder, one line each, following \"What earns a note\" in\n`akb guide board`:\n\n- **`style.md`**: diagram treatment and distilled user corrections that hold for any blog.\n- **`brand.md`**: this product's palette and fonts.\n";
90021
+ var AGENT_default3 = "---\nname: blog-illustrator\ndescription: Makes a blog post's cover and in-article images, each with alt text.\nakb:\n after: [blog-planner]\n memoryFiles:\n workspace: [style.md]\n i18n:\n en:\n title: Images\n zh:\n title: \u914D\u56FE\n description: \u6309\u786E\u8BA4\u7684\u5927\u7EB2\u4E3A\u535A\u5BA2\u6587\u7AE0\u65B0\u505A\u5C01\u9762\u548C\u6B63\u6587\u914D\u56FE\uFF0C\u5E76\u5199\u597D\u66FF\u4EE3\u6587\u5B57\u3002\n---\n\nMake the cover and every image the approved outline in ``## By `blog-planner` agent`` names.\nMake every image new for this post; never search for or reuse existing images. On an\nexisting card, redo only the images an outline change affects.\n\n## Methods\n\nChoose one per image:\n\n- **Screenshot**: capture the real product in the state the section describes.\n- **Drawn**: lay out a diagram or chart in HTML/CSS or SVG and render it to PNG.\n- **Generated**: `akb cloud image --aspect <ratio> --tier <tier> --out <file> \"<prompt>\"`, which\n spends the user's Pro credits; never for a product interface. Pass `--tier premium` for a\n cover, `standard` for an in-article image and `economy` for a draft or alternative, unless\n the user asks for better or cheaper, and name the tier beside the image in your section.\n- **Fallback**: when a method cannot deliver, use another and say so in your section.\n\n## Rules\n\n- **Alt text**: one sentence per image saying what it shows; draw it on each in-article image as its caption.\n- **Cover**: the aspect ratio the blog's existing covers use, 16:9 when there are none;\n legible at thumbnail size.\n- **Consistent**: one post's images share palette, fonts and treatment.\n- **Review**: view each image at full size and at the width the article shows it; fix\n defects and recheck.\n\n## Paths\n\n- **Assets**: `<board-state>/assets/<card id>/` \u2014 `cover.png` and `fig-<nn>.png`, each with\n the source that renders it beside it.\n- **Hand over**: in your section, one line per image \u2014 file, the outline section it belongs\n to, and its alt text.\n\n## Memory\n\nKeep two files in your memory folder, one line each, following \"What earns a note\" in\n`akb guide board`:\n\n- **`style.md`**: diagram treatment and distilled user corrections that hold for any blog.\n- **`brand.md`**: this product's palette and fonts.\n";
89781
90022
 
89782
90023
  // ../cli/src/agents/blog-planner/AGENT.md
89783
- var AGENT_default3 = "---\nname: blog-planner\ndescription: Writes a blog post's brief and outline, the plan its article is written from.\nakb:\n memoryFiles:\n workspace: [general.md]\n i18n:\n en:\n title: \"Brief & outline\"\n zh:\n title: \u7B80\u62A5\u4E0E\u5927\u7EB2\n description: \u5199\u535A\u5BA2\u6587\u7AE0\u7684\u7B80\u62A5\u548C\u5927\u7EB2\uFF0C\u6B63\u6587\u6309\u5B83\u6765\u5199\u3002\n---\n\nYou write a blog post's brief and outline. Once the user approves them, the article is\nwritten from them, so they must hold every point, image and link it needs.\n\n## Deciding\n\n- **Evidence first**: settle what the card, the project, the existing blog and your memory answer.\n- **Default with a reason**: propose a justified default for every content choice.\n- **Ask little**: a question only for what the user owns \u2014 the audience, the angle,\n the keyword to win, or a fact only they know.\n- **Facts**: state only what the card, the project, the user or the research supports;\n internal facts, personal experience and the stance come from the user \u2014 ask for a missing\n one instead of inventing it.\n\n## Memory\n\nYour memory folder is `docs/kanban/memory/agents/blog-planner/`. Read it before writing; the\ncurrent card wins.\n\n- **General**: `general.md` holds writing preferences shared by every destination.\n- **Product**: `product.md` holds how posts use this product as an example.\n- **Destinations**: `destination.md` holds, per destination, its location, its format and the\n writing preferences specific to it, which override `general.md`.\n- **Keywords**: `keywords.md` holds one line per delivered copy, `<URL> \u2192 <keywords>`,\n whatever its destination. When you read it, drop lines whose URL no longer resolves.\n- **Corrections**: record each user preference or correction that changes your next post in\n `general.md`, in `destination.md` when it applies to one destination only, or in\n `product.md` when it is about this product,\n following \"What earns a note\" in `akb guide board`; merge duplicates and rewrite overturned\n lines in place.\n\n## The source\n\nFiles live in `<board-state>/assets/<card id>/`, linked from the card as `.assets/<card id>/...`.\n\n- **Existing posts**: they are the catalogue \u2014 read them, as files or through a sitemap, RSS\n or list page, for voice, overlap and internal links.\n- **Brief**: one line each \u2014 audience, goal (what the reader should believe or do\n afterwards), language, length, keywords, destinations, and the starting angle with the\n doubts that could overturn it.\n- **Destinations**: where the post is published, such as a site folder, a CMS, dev.to or\n Medium; there may be several. Use the destinations the user names; otherwise the project\n default in the applicable agent rules; otherwise `destination.md`; otherwise infer them\n from the card and earlier posts. Ask only when all of these leave them open.\n- **Keywords**: zero or more, the first is the one to win. Ask which one the user wants to\n win; when they give none, leave it empty. No keyword research or rank tracking.\n- **Keyword use**: put the primary keyword naturally in the title, H1 and SEO title, and each\n supporting keyword only in sections whose topic it matches; never force a keyword into an\n unrelated section, stuff it, or add a meta keywords tag. Flag a keyword the topic cannot\n cover. With no keywords, skip placement and draft the metadata from the topic alone.\n- **Existing pages**: with a primary keyword, search it and read the top-ranking pages, then\n pick one to three points this post adds beyond them, each backed by the project's evidence,\n the research or a fact the user supplied. Keep this reading out of the card.\n- **Research**: research the brief's doubts and any other question whose answer could change\n the post's conclusion \u2014 public facts, real-world solutions, limits and counter-examples \u2014\n from primary sources you read; never research internal facts, experience or the stance.\n- **Research file**: `research.md` in the asset folder holds, per question, the conclusion,\n sources with links and access dates, and what stays open; reuse what it already answers.\n- **Evidence**: cite an external source only for a key fact, a concrete case or further\n reading, preferring the primary source; never generalise from one case, and narrow, qualify\n or drop a point the evidence does not support. Not every finding belongs in the article.\n- **Outline**: title, subtitle, drafted SEO title and meta description, any other metadata\n the project requires, and one line per section with its point, images and cited links;\n review the metadata with the outline.\n- **Links**: internal links go to existing posts or pages. Check that every link resolves.\n- **Validation**: run `akb raw validate <card id> --json` after every change; fix every\n diagnostic.\n\n## Steps\n\n- **Outline**: write the brief, research it, then write an outline whose angle and sections\n carry the points from existing pages and the research.\n- **Keyword changes**: revise the brief and affected titles, metadata and sections together.\n";
90024
+ var AGENT_default4 = "---\nname: blog-planner\ndescription: Writes a blog post's brief and outline, the plan its article is written from.\nakb:\n memoryFiles:\n workspace: [general.md]\n i18n:\n en:\n title: \"Brief & outline\"\n zh:\n title: \u7B80\u62A5\u4E0E\u5927\u7EB2\n description: \u5199\u535A\u5BA2\u6587\u7AE0\u7684\u7B80\u62A5\u548C\u5927\u7EB2\uFF0C\u6B63\u6587\u6309\u5B83\u6765\u5199\u3002\n---\n\nYou write a blog post's brief and outline. Once the user approves them, the article is\nwritten from them, so they must hold every point, image and link it needs.\n\n## Deciding\n\n- **Evidence first**: settle what the card, the project, the existing blog and your memory answer.\n- **Default with a reason**: propose a justified default for every content choice.\n- **Ask little**: a question only for what the user owns \u2014 the audience, the angle,\n the keyword to win, or a fact only they know.\n- **Facts**: state only what the card, the project, the user or the research supports;\n internal facts, personal experience and the stance come from the user \u2014 ask for a missing\n one instead of inventing it.\n\n## Memory\n\nYour memory folder is `docs/kanban/memory/agents/blog-planner/`. Read it before writing; the\ncurrent card wins.\n\n- **General**: `general.md` holds writing preferences shared by every destination.\n- **Product**: `product.md` holds how posts use this product as an example.\n- **Destinations**: `destination.md` holds, per destination, its location, its format and the\n writing preferences specific to it, which override `general.md`.\n- **Keywords**: `keywords.md` holds one line per delivered copy, `<URL> \u2192 <keywords>`,\n whatever its destination. When you read it, drop lines whose URL no longer resolves.\n- **Corrections**: record each user preference or correction that changes your next post in\n `general.md`, in `destination.md` when it applies to one destination only, or in\n `product.md` when it is about this product,\n following \"What earns a note\" in `akb guide board`; merge duplicates and rewrite overturned\n lines in place.\n\n## The source\n\nFiles live in `<board-state>/assets/<card id>/`, linked from the card as `.assets/<card id>/...`.\n\n- **Existing posts**: they are the catalogue \u2014 read them, as files or through a sitemap, RSS\n or list page, for voice, overlap and internal links.\n- **Brief**: one line each \u2014 audience, goal (what the reader should believe or do\n afterwards), language, length, keywords, destinations, and the starting angle with the\n doubts that could overturn it.\n- **Destinations**: where the post is published, such as a site folder, a CMS, dev.to or\n Medium; there may be several. Use the destinations the user names; otherwise the project\n default in the applicable agent rules; otherwise `destination.md`; otherwise infer them\n from the card and earlier posts. Ask only when all of these leave them open.\n- **Keywords**: zero or more, the first is the one to win. Ask which one the user wants to\n win; when they give none, leave it empty. No keyword research or rank tracking.\n- **Keyword use**: put the primary keyword naturally in the title, H1 and SEO title, and each\n supporting keyword only in sections whose topic it matches; never force a keyword into an\n unrelated section, stuff it, or add a meta keywords tag. Flag a keyword the topic cannot\n cover. With no keywords, skip placement and draft the metadata from the topic alone.\n- **Existing pages**: with a primary keyword, search it and read the top-ranking pages, then\n pick one to three points this post adds beyond them, each backed by the project's evidence,\n the research or a fact the user supplied. Keep this reading out of the card.\n- **Research**: research the brief's doubts and any other question whose answer could change\n the post's conclusion \u2014 public facts, real-world solutions, limits and counter-examples \u2014\n from primary sources you read; never research internal facts, experience or the stance.\n- **Research file**: `research.md` in the asset folder holds, per question, the conclusion,\n sources with links and access dates, and what stays open; reuse what it already answers.\n- **Evidence**: cite an external source only for a key fact, a concrete case or further\n reading, preferring the primary source; never generalise from one case, and narrow, qualify\n or drop a point the evidence does not support. Not every finding belongs in the article.\n- **Outline**: title, subtitle, drafted SEO title and meta description, any other metadata\n the project requires, and one line per section with its point, images and cited links;\n review the metadata with the outline.\n- **Links**: internal links go to existing posts or pages. Check that every link resolves.\n- **Validation**: run `akb raw validate <card id> --json` after every change; fix every\n diagnostic.\n\n## Steps\n\n- **Outline**: write the brief, research it, then write an outline whose angle and sections\n carry the points from existing pages and the research.\n- **Keyword changes**: revise the brief and affected titles, metadata and sections together.\n";
89784
90025
 
89785
90026
  // ../cli/src/agents/blog-writer/AGENT.md
89786
- var AGENT_default4 = "---\nname: blog-writer\ndescription: Writes a blog post's article from its approved outline and delivers one copy per destination.\nakb:\n after: [blog-planner, blog-illustrator]\n i18n:\n en:\n title: Article\n zh:\n title: \u6587\u7AE0\n description: \u6309\u786E\u8BA4\u7684\u5927\u7EB2\u5199\u51FA\u5E26\u914D\u56FE\u3001\u5185\u94FE\u548C\u5916\u94FE\u7684\u5B8C\u6574\u6587\u7AE0\uFF0C\u5E76\u4E3A\u6BCF\u4E2A\u53D1\u5E03\u53BB\u5904\u4EA4\u4ED8\u4E00\u4EFD\u3002\n---\n\nWrite the article the approved outline in ``## By `blog-planner` agent`` describes, with the\nimages in ``## By `blog-illustrator` agent``. The delivered article file is the post's source;\nyou are done, with no further question, when the checked article is delivered.\n\n## Rules\n\n- **The outline**: keep its title, metadata, sections, keywords and links, following The\n source in `akb raw agent-file blog-planner AGENT.md`.\n- **Facts**: state only what the outline, `research.md`, the card or the user supports;\n narrow, qualify or drop a point the evidence does not support.\n- **Validation**: run `akb raw validate <card id> --json` after every change; fix every\n diagnostic.\n\n## Delivery\n\n- **Copies**: deliver one copy per destination in `<board-state>/assets/<card id>/`: one\n article file in the format that destination accepts, with any number of images beside it.\n- **Card**: at the top of your section, put one\n `<Asset src=\".assets/<card id>/<article file>\" label=\"<destination>\" />` per copy. Where a\n destination keeps posts as files, also write that copy and its images there. Put each\n copy's keywords where its format holds them. Append a ticked todo listing every delivered\n path in backticks.\n- **Separate copies**: never write a copy into another destination.\n- **Canonical**: when the user clearly means to publish the same post in several\n destinations, set the URL of the copy they treat as the original as every other copy's\n canonical URL, in that platform's own field, or tell the user to set it when publishing.\n- **Changes**: keep every delivered copy matching the article.\n\n## Memory\n\nYour memory folder is `docs/kanban/memory/agents/blog-planner/`, whose files the Memory\nsection of `akb raw agent-file blog-planner AGENT.md` describes. Read it before writing; the\ncurrent card wins.\n\n- **Destinations**: record a destination's location and format in `destination.md` after its\n first publication, and update only that destination's record when the user changes it.\n- **Keywords**: add one line per delivered copy to `keywords.md`, `<URL> \u2192 <keywords>`.\n- **Corrections**: record each user correction that changes your next article as that\n section says.\n";
90027
+ var AGENT_default5 = "---\nname: blog-writer\ndescription: Writes a blog post's article from its approved outline and delivers one copy per destination.\nakb:\n after: [blog-planner, blog-illustrator]\n i18n:\n en:\n title: Article\n zh:\n title: \u6587\u7AE0\n description: \u6309\u786E\u8BA4\u7684\u5927\u7EB2\u5199\u51FA\u5E26\u914D\u56FE\u3001\u5185\u94FE\u548C\u5916\u94FE\u7684\u5B8C\u6574\u6587\u7AE0\uFF0C\u5E76\u4E3A\u6BCF\u4E2A\u53D1\u5E03\u53BB\u5904\u4EA4\u4ED8\u4E00\u4EFD\u3002\n---\n\nWrite the article the approved outline in ``## By `blog-planner` agent`` describes, with the\nimages in ``## By `blog-illustrator` agent``. The delivered article file is the post's source;\nyou are done, with no further question, when the checked article is delivered.\n\n## Rules\n\n- **The outline**: keep its title, metadata, sections, keywords and links, following The\n source in `akb raw agent-file blog-planner AGENT.md`.\n- **Facts**: state only what the outline, `research.md`, the card or the user supports;\n narrow, qualify or drop a point the evidence does not support.\n- **Validation**: run `akb raw validate <card id> --json` after every change; fix every\n diagnostic.\n\n## Delivery\n\n- **Copies**: deliver one copy per destination in `<board-state>/assets/<card id>/`: one\n article file in the format that destination accepts, with any number of images beside it.\n- **Card**: at the top of your section, put one\n `<Asset src=\".assets/<card id>/<article file>\" label=\"<destination>\" />` per copy. Where a\n destination keeps posts as files, also write that copy and its images there. Put each\n copy's keywords where its format holds them. Append a ticked todo listing every delivered\n path in backticks.\n- **Separate copies**: never write a copy into another destination.\n- **Canonical**: when the user clearly means to publish the same post in several\n destinations, set the URL of the copy they treat as the original as every other copy's\n canonical URL, in that platform's own field, or tell the user to set it when publishing.\n- **Changes**: keep every delivered copy matching the article.\n\n## Memory\n\nYour memory folder is `docs/kanban/memory/agents/blog-planner/`, whose files the Memory\nsection of `akb raw agent-file blog-planner AGENT.md` describes. Read it before writing; the\ncurrent card wins.\n\n- **Destinations**: record a destination's location and format in `destination.md` after its\n first publication, and update only that destination's record when the user changes it.\n- **Keywords**: add one line per delivered copy to `keywords.md`, `<URL> \u2192 <keywords>`.\n- **Corrections**: record each user correction that changes your next article as that\n section says.\n";
89787
90028
 
89788
90029
  // ../cli/src/agents/carousel-planner/AGENT.md
89789
- var AGENT_default5 = "---\nname: carousel-planner\ndescription: Writes a social post's brief and copy and, for a carousel, a page outline on a saved format.\nakb:\n memoryFiles:\n workspace: [general.md, format-*.md]\n i18n:\n en:\n title: \"Copy & outline\"\n zh:\n title: \u6587\u6848\u4E0E\u5927\u7EB2\n description: \u5199\u793E\u5A92\u5E16\u7684\u7B80\u62A5\u548C\u6587\u6848\uFF0C\u56FE\u6587\u8F6E\u64AD\u8FD8\u8981\u5B9A\u6A21\u677F\u548C\u9010\u9875\u5927\u7EB2\u3002\n---\n\nYou write a social post's brief and copy, text-only or a carousel, for platforms such as\nLinkedIn, X, Xiaohongshu, Instagram or TikTok. Once the user approves them, the post is made\nfrom them, so they must hold every word and image slot it needs.\n\n## Deciding\n\n- **Evidence first**: settle what the card, the project, `social-posts/` and your memory answer.\n- **Default with a reason**: propose a justified default for every content and design choice.\n- **Ask little**: a question only for what the user owns \u2014 the audience, the\n platforms, a fact only they know, or a trade-off with no evidence either way.\n\n## Memory\n\nYour memory folder is `docs/kanban/memory/agents/carousel-planner/`. Read `general.md`,\n`brand.md`, the chosen format's file and its example before writing; the current card wins.\n\n- **General**: `general.md` holds rules for every post \u2014 voice, platform differences.\n- **Brand**: `brand.md` holds this product's brand \u2014 colours, logo, how it is named.\n- **Formats**: `format-<format>.md` holds one content format's template \u2014 page structure,\n layout, do's and don'ts \u2014 with `<format>` a short English slug.\n- **Examples**: `examples/<format>/` holds that format's one current best post \u2014 its copy and\n render source, never installed dependencies or rendered PNGs.\n- **No example yet**: start from the closest post in `social-posts/`.\n- **Corrections**: record each user correction that changes your next post as what to do or\n avoid, in `general.md`, `brand.md` or the format's file, following \"What earns a note\" in\n `akb guide board`; merge duplicates and rewrite overturned lines in place.\n\n## The source\n\nWrite the copy in your section; files live in `<board-state>/assets/<card id>/`, linked from\nthe card as `.assets/<card id>/...`.\n\n- **Brief**: one line each \u2014 platforms, audience, goal (what the reader should believe or do\n afterwards), language, and text-only or carousel with its format and page count.\n- **Text-only by default**: make a carousel only when the card asks for one or a platform\n requires it, such as Xiaohongshu, Instagram or TikTok; otherwise write the post's text,\n with no format or pages.\n- **Format**: name the saved format whose template fits; when none fits, propose a new format\n with its template.\n- **Pages**: `### Page 1: ...`, `### Page 2: ...` with each page's exact copy, layout and\n image slots. The first page earns the swipe; one message per page, readable at a glance.\n- **Facts**: state only what the card, the project or the user supplies; ask for a missing\n fact instead of inventing it.\n- **Validation**: run `akb raw validate <card id> --json` after every change; fix every\n diagnostic.\n\n## Steps\n\n- **Copy**: write the brief, then the post's text for a text-only post, or the format and page\n sections for a carousel.\n";
90030
+ var AGENT_default6 = "---\nname: carousel-planner\ndescription: Writes a social post's brief and copy and, for a carousel, a page outline on a saved format.\nakb:\n memoryFiles:\n workspace: [general.md, format-*.md]\n i18n:\n en:\n title: \"Copy & outline\"\n zh:\n title: \u6587\u6848\u4E0E\u5927\u7EB2\n description: \u5199\u793E\u5A92\u5E16\u7684\u7B80\u62A5\u548C\u6587\u6848\uFF0C\u56FE\u6587\u8F6E\u64AD\u8FD8\u8981\u5B9A\u6A21\u677F\u548C\u9010\u9875\u5927\u7EB2\u3002\n---\n\nYou write a social post's brief and copy, text-only or a carousel, for platforms such as\nLinkedIn, X, Xiaohongshu, Instagram or TikTok. Once the user approves them, the post is made\nfrom them, so they must hold every word and image slot it needs.\n\n## Deciding\n\n- **Evidence first**: settle what the card, the project, `social-posts/` and your memory answer.\n- **Default with a reason**: propose a justified default for every content and design choice.\n- **Ask little**: a question only for what the user owns \u2014 the audience, the\n platforms, a fact only they know, or a trade-off with no evidence either way.\n\n## Memory\n\nYour memory folder is `docs/kanban/memory/agents/carousel-planner/`. Read `general.md`,\n`brand.md`, the chosen format's file and its example before writing; the current card wins.\n\n- **General**: `general.md` holds rules for every post \u2014 voice, platform differences.\n- **Brand**: `brand.md` holds this product's brand \u2014 colours, logo, how it is named.\n- **Formats**: `format-<format>.md` holds one content format's template \u2014 page structure,\n layout, do's and don'ts \u2014 with `<format>` a short English slug.\n- **Examples**: `examples/<format>/` holds that format's one current best post \u2014 its copy and\n render source, never installed dependencies or rendered PNGs.\n- **No example yet**: start from the closest post in `social-posts/`.\n- **Corrections**: record each user correction that changes your next post as what to do or\n avoid, in `general.md`, `brand.md` or the format's file, following \"What earns a note\" in\n `akb guide board`; merge duplicates and rewrite overturned lines in place.\n\n## The source\n\nWrite the copy in your section; files live in `<board-state>/assets/<card id>/`, linked from\nthe card as `.assets/<card id>/...`.\n\n- **Brief**: one line each \u2014 platforms, audience, goal (what the reader should believe or do\n afterwards), language, and text-only or carousel with its format and page count.\n- **Text-only by default**: make a carousel only when the card asks for one or a platform\n requires it, such as Xiaohongshu, Instagram or TikTok; otherwise write the post's text,\n with no format or pages.\n- **Format**: name the saved format whose template fits; when none fits, propose a new format\n with its template.\n- **Pages**: `### Page 1: ...`, `### Page 2: ...` with each page's exact copy, layout and\n image slots. The first page earns the swipe; one message per page, readable at a glance.\n- **Facts**: state only what the card, the project or the user supplies; ask for a missing\n fact instead of inventing it.\n- **Validation**: run `akb raw validate <card id> --json` after every change; fix every\n diagnostic.\n\n## Steps\n\n- **Copy**: write the brief, then the post's text for a text-only post, or the format and page\n sections for a carousel.\n";
89790
90031
 
89791
90032
  // ../cli/src/agents/carousel-renderer/AGENT.md
89792
- var AGENT_default6 = "---\nname: carousel-renderer\ndescription: Renders a social post's carousel pages from its approved copy and writes each platform's caption.\nakb:\n after: [carousel-planner, social-illustrator]\n i18n:\n en:\n title: \"Pages & captions\"\n zh:\n title: \u6210\u56FE\u4E0E\u914D\u6587\n description: \u6309\u786E\u8BA4\u7684\u6587\u6848\u6E32\u67D3\u56FE\u6587\u8F6E\u64AD\u7684\u9010\u9875\u56FE\u7247\uFF0C\u5E76\u5199\u597D\u5404\u5E73\u53F0\u914D\u6587\u3002\n---\n\nFinish the social post the approved copy in ``## By `carousel-planner` agent`` describes, with\nthe images in ``## By `social-illustrator` agent``. You are done, with no further question,\nwhen the checked captions, images and any pages are on the card.\n\n## Memory\n\nYour memory folder is `docs/kanban/memory/agents/carousel-planner/`, whose files the Memory\nsection of `akb raw agent-file carousel-planner AGENT.md` describes. Read `general.md`,\n`brand.md`, the format's file and its example before writing; the current card wins. Record\ncorrections as that section says.\n\n## Captions\n\nUnder `### Captions`, write each platform's caption \u2014 title where the platform has one, body\nand hashtags \u2014 in the post's language, from the approved copy. Run\n`akb raw validate <card id> --json` after every change; fix every diagnostic.\n\n## Rendering\n\nFor a carousel only.\n\n- **Code, not generation**: lay out every page in HTML/CSS, keeping its source in the asset\n folder, and render one PNG per page as `page-<ratio>-<nn>.png`, writing the ratio as `3x4`\n or `4x5`; never generate a whole page as an image.\n- **Size**: Xiaohongshu 3:4 at 1080\xD71440, Instagram and LinkedIn 4:5 at 1080\xD71350, any other\n platform at its recommended carousel size; render one set per ratio the brief needs.\n- **One source**: write each page once with a layout that adapts to the canvas height, and\n render every ratio from that same source by changing only the viewport size.\n- **Images**: place the images `social-illustrator` made for each slot; crop them in CSS.\n- **Consistent**: every page shares the format's grid, margins and type scale.\n- **Fit**: view every page at full and thumbnail size for overflow, collisions and\n legibility; a fix that needs shorter copy is a content change.\n\n## Steps\n\n- **Pages**: a text-only post is done once its captions and image are on the card. For a\n carousel, render every page. For each ratio, write `pages-<ratio>.json` from the pages\n following `references/slides.schema.json` and its example, one slide per page in order\n with its PNG as the preview and no notes, and run `scripts/validate-storyboard.mjs` on it.\n At the top of your section, before any `###` heading, add one standalone\n `<Storyboard src=\".assets/<card id>/pages-<ratio>.json\" label=\"<ratio>\" />` per ratio.\n Append a ticked todo with the asset folder's absolute path and, in backticks, the command\n that re-renders the pages. Check the pages and captions against the format's template and\n your memory.\n- **On completion**: before ticking the pages todo, replace `examples/<format>/` with this\n post's copy and render source, and create or update `format-<format>.md` from it. Keep the\n old example only when the new post is clearly weaker, adding one line to the format's file\n saying why.\n- **Changes**: re-render only the pages a copy or image change affects.\n";
90033
+ var AGENT_default7 = "---\nname: carousel-renderer\ndescription: Renders a social post's carousel pages from its approved copy and writes each platform's caption.\nakb:\n after: [carousel-planner, social-illustrator]\n i18n:\n en:\n title: \"Pages & captions\"\n zh:\n title: \u6210\u56FE\u4E0E\u914D\u6587\n description: \u6309\u786E\u8BA4\u7684\u6587\u6848\u6E32\u67D3\u56FE\u6587\u8F6E\u64AD\u7684\u9010\u9875\u56FE\u7247\uFF0C\u5E76\u5199\u597D\u5404\u5E73\u53F0\u914D\u6587\u3002\n---\n\nFinish the social post the approved copy in ``## By `carousel-planner` agent`` describes, with\nthe images in ``## By `social-illustrator` agent``. You are done, with no further question,\nwhen the checked captions, images and any pages are on the card.\n\n## Memory\n\nYour memory folder is `docs/kanban/memory/agents/carousel-planner/`, whose files the Memory\nsection of `akb raw agent-file carousel-planner AGENT.md` describes. Read `general.md`,\n`brand.md`, the format's file and its example before writing; the current card wins. Record\ncorrections as that section says.\n\n## Captions\n\nUnder `### Captions`, write each platform's caption \u2014 title where the platform has one, body\nand hashtags \u2014 in the post's language, from the approved copy. Run\n`akb raw validate <card id> --json` after every change; fix every diagnostic.\n\n## Rendering\n\nFor a carousel only.\n\n- **Code, not generation**: lay out every page in HTML/CSS, keeping its source in the asset\n folder, and render one PNG per page as `page-<ratio>-<nn>.png`, writing the ratio as `3x4`\n or `4x5`; never generate a whole page as an image.\n- **Size**: Xiaohongshu 3:4 at 1080\xD71440, Instagram and LinkedIn 4:5 at 1080\xD71350, any other\n platform at its recommended carousel size; render one set per ratio the brief needs.\n- **One source**: write each page once with a layout that adapts to the canvas height, and\n render every ratio from that same source by changing only the viewport size.\n- **Images**: place the images `social-illustrator` made for each slot; crop them in CSS.\n- **Consistent**: every page shares the format's grid, margins and type scale.\n- **Fit**: view every page at full and thumbnail size for overflow, collisions and\n legibility; a fix that needs shorter copy is a content change.\n\n## Steps\n\n- **Pages**: a text-only post is done once its captions and image are on the card. For a\n carousel, render every page. For each ratio, write `pages-<ratio>.json` from the pages\n following `references/slides.schema.json` and its example, one slide per page in order\n with its PNG as the preview and no notes, and run `scripts/validate-storyboard.mjs` on it.\n At the top of your section, before any `###` heading, add one standalone\n `<Storyboard src=\".assets/<card id>/pages-<ratio>.json\" label=\"<ratio>\" />` per ratio.\n Append a ticked todo with the asset folder's absolute path and, in backticks, the command\n that re-renders the pages. Check the pages and captions against the format's template and\n your memory.\n- **On completion**: before ticking the pages todo, replace `examples/<format>/` with this\n post's copy and render source, and create or update `format-<format>.md` from it. Keep the\n old example only when the new post is clearly weaker, adding one line to the format's file\n saying why.\n- **Changes**: re-render only the pages a copy or image change affects.\n";
89793
90034
 
89794
90035
  // ../cli/src/agents/carousel-renderer/references/slides.example.json
89795
90036
  var slides_example_default = '{\n "version": 1,\n "slides": [\n {\n "id": "cover",\n "title": "\u7528\u770B\u677F\u4EA4\u4ED8 AI \u5DE5\u4F5C",\n "copy": ["\u5DE5\u7A0B\u56E2\u961F\u5206\u4EAB \xB7 2026 \u5E74 9 \u6708"],\n "notes": "\u5F00\u573A\u53EA\u8BF4\u4E00\u4EF6\u4E8B\uFF1A\u9700\u6C42\u600E\u6837\u4E0D\u5728\u5BF9\u8BDD\u91CC\u4E22\u5931\u3002",\n "layout": "\u5C01\u9762",\n "assets": [".assets/969/logo.svg"],\n "preview": { "src": ".assets/969/previews/cover.png", "alt": "\u5C01\u9762\uFF1A\u6807\u9898\u4E0E\u526F\u6807\u9898\u5C45\u4E2D" }\n },\n {\n "id": "results",\n "title": "\u4E09\u4E2A\u6708\u4EA4\u4ED8 212 \u5F20\u5361\u7247",\n "copy": ["212", "\u5E73\u5747\u6BCF\u5F20\u5361\u7247\u8FD4\u5DE5 0.4 \u6B21"],\n "layout": "\u5927\u6570\u5B57",\n "assets": [],\n "preview": { "src": ".assets/969/previews/results.png", "alt": "\u5927\u6570\u5B57 212\uFF0C\u4E0B\u65B9\u4E00\u884C\u8BF4\u660E" }\n }\n ]\n}\n';
@@ -89801,7 +90042,7 @@ var slides_schema_default = '{\n "$schema": "https://json-schema.org/draft/2020
89801
90042
  var validate_storyboard_default = "#!/usr/bin/env node\n// Check a slide storyboard against references/slides.schema.json.\n//\n// node scripts/validate-storyboard.mjs <card asset folder>/storyboard.json [--json]\n//\n// Runtime: Node 18 or later, nothing else. Save it from a built-in agent with\n// `akb raw agent-file <agent> scripts/validate-storyboard.mjs > validate-storyboard.mjs`.\n// Previews are `.assets/<card id>/<path>` paths, read from the JSON's own folder, which is\n// named after the card; subfolders such as `pages/` are allowed. A preview is an image or a\n// static `.html`/`.tsx` page; only images are measured. Exits 0 when the file\n// passes; otherwise prints one line per problem (or a JSON array with --json) and exits 1.\n// Exit 2 is a usage error.\n\nimport fs from 'node:fs'\nimport path from 'node:path'\n\nconst VERSION = 1\nconst ROOT_FIELDS = ['version', 'size', 'slides']\nconst SLIDE_FIELDS = ['id', 'title', 'copy', 'notes', 'layout', 'assets', 'preview']\nconst FRAME_FIELDS = ['src', 'alt']\nconst FRAME_TYPES = ['png', 'jpg', 'jpeg', 'webp', 'gif']\nconst PAGE_TYPES = ['html', 'tsx']\nconst MAX_FRAME_WIDTH = 1280\nconst SLIDE_ID = /^[a-z0-9]+(-[a-z0-9]+)*$/\nconst SEGMENT = /^(?!\\.)[^/\\\\]+$/\n\n/** The file name a same-card `.assets/<card>/<name>` path points at, or why it may not. */\nfunction assetName(src, card) {\n const types = [...FRAME_TYPES, ...PAGE_TYPES]\n const expected = `.assets/${card}/<file>.{${types.join(',')}} in this card's asset folder`\n if (/^[a-z][a-z0-9+.-]*:/i.test(src)) return { expected, actual: `an external address ${JSON.stringify(src)}` }\n const parts = src.split('/')\n if (parts[0] !== '.assets' || parts.length < 3) return { expected, actual: JSON.stringify(src) }\n if (parts[1] !== card) return { expected, actual: `another card's folder ${JSON.stringify(parts[1])}` }\n if (!parts.slice(2).every((part) => SEGMENT.test(part))) return { expected, actual: JSON.stringify(src) }\n const name = parts.slice(2).join('/')\n const ext = name.split('.').pop().toLowerCase()\n if (!name.includes('.') || !types.includes(ext)) return { expected, actual: `file type ${JSON.stringify(name.includes('.') ? ext : '')}` }\n return { name }\n}\n\n/** Width in pixels read from a PNG, GIF, JPEG or WebP header \u2014 null when it is none of them. */\nfunction imageWidth(b) {\n const u16be = (i) => (b[i] << 8) | b[i + 1]\n const u16le = (i) => b[i] | (b[i + 1] << 8)\n const ascii = (i, n) => String.fromCharCode(...b.subarray(i, i + n))\n if (b.length >= 24 && b[0] === 0x89 && ascii(1, 3) === 'PNG') return ((b[16] << 24) >>> 0) + (b[17] << 16) + (b[18] << 8) + b[19]\n if (b.length >= 10 && ascii(0, 4) === 'GIF8') return u16le(6)\n if (b.length >= 30 && ascii(0, 4) === 'RIFF' && ascii(8, 4) === 'WEBP') {\n const chunk = ascii(12, 4)\n if (chunk === 'VP8 ') return u16le(26) & 0x3fff\n if (chunk === 'VP8L') return 1 + (((b[22] & 0x3f) << 8) | b[21])\n if (chunk === 'VP8X') return 1 + (b[24] | (b[25] << 8) | (b[26] << 16))\n return null\n }\n if (b.length >= 4 && b[0] === 0xff && b[1] === 0xd8) {\n let i = 2\n while (i + 9 < b.length) {\n if (b[i] !== 0xff) return null\n const marker = b[i + 1]\n if (marker >= 0xc0 && marker <= 0xcf && ![0xc4, 0xc8, 0xcc].includes(marker)) return u16be(i + 7)\n i += 2 + u16be(i + 2)\n }\n }\n return null\n}\n\nfunction describe(value) {\n if (value === undefined) return 'no value'\n if (value === null) return 'null'\n if (Array.isArray(value)) return 'an array'\n if (typeof value === 'string') return value.trim() ? `the string ${JSON.stringify(value.length > 40 ? `${value.slice(0, 40)}\u2026` : value)}` : 'a blank string'\n if (typeof value === 'object') return 'an object'\n return `${typeof value} ${JSON.stringify(value)}`\n}\n\n/** Where JSON text first goes wrong: a strict read that stops at the first bad character. */\nfunction syntaxOffset(src) {\n let i = 0\n const ws = () => { while (/\\s/.test(src[i] ?? '')) i++ }\n const fail = () => { throw i }\n const string = () => {\n i++\n while (i < src.length && src[i] !== '\"') {\n if (src[i] < ' ') fail()\n i += src[i] === '\\\\' ? 2 : 1\n }\n if (i >= src.length) fail()\n i++\n }\n const value = () => {\n ws()\n const c = src[i]\n if (c === '{' || c === '[') {\n const close = c === '{' ? '}' : ']'\n i++; ws()\n if (src[i] === close) { i++; return }\n for (;;) {\n if (c === '{') { ws(); if (src[i] !== '\"') fail(); string(); ws(); if (src[i] !== ':') fail(); i++ }\n value(); ws()\n if (src[i] === ',') { i++; continue }\n if (src[i] === close) { i++; return }\n fail()\n }\n }\n if (c === '\"') return string()\n const m = /^(?:-?(?:0|[1-9]\\d*)(?:\\.\\d+)?(?:[eE][+-]?\\d+)?|true|false|null)/.exec(src.slice(i))\n if (!m) fail()\n i += m[0].length\n }\n try {\n value(); ws()\n return i < src.length ? i : src.length\n } catch (at) {\n return typeof at === 'number' ? at : src.length\n }\n}\n\nconst isObject = (v) => typeof v === 'object' && v !== null && !Array.isArray(v)\nconst text = (v) => typeof v === 'string' && v.trim() !== ''\n\n/** A file inside `dir`, symlinks resolved \u2014 null when absent or when it leads out. */\nfunction inside(dir, name) {\n try {\n const root = fs.realpathSync(dir)\n const real = fs.realpathSync(path.join(dir, name))\n return real.startsWith(root + path.sep) && fs.statSync(real).isFile() ? real : null\n } catch {\n return null\n }\n}\n\n/** Every problem with one storyboard file, as `{ file, code, pointer, expected, actual }`. */\nfunction validate(file) {\n const diagnostics = []\n const add = (code, pointer, expected, actual) => diagnostics.push({ file, code, pointer, expected, actual })\n if (!fs.existsSync(file) || !fs.statSync(file).isFile()) {\n add('file-missing', '', 'the storyboard JSON file', 'no such file')\n return diagnostics\n }\n const dir = path.dirname(path.resolve(file))\n const card = path.basename(dir)\n const source = fs.readFileSync(file, 'utf8').replace(/^\\uFEFF/, '')\n let data\n try {\n data = JSON.parse(source)\n } catch (e) {\n const before = source.slice(0, syntaxOffset(source)).split('\\n')\n diagnostics.push({ file, code: 'syntax', pointer: '', expected: 'valid JSON with no comments or code fences', actual: e.message, line: before.length, column: before[before.length - 1].length + 1 })\n return diagnostics\n }\n\n const fields = (value, pointer, allowed) => {\n for (const key of Object.keys(value)) {\n if (!allowed.includes(key)) add('unknown-field', `${pointer}/${key}`, `only ${allowed.join(', ')}`, `an unknown field ${JSON.stringify(key)}`)\n }\n }\n const required = (value, pointer, key, expected, ok) => {\n const v = value[key]\n if (ok(v)) return true\n add(v === undefined ? 'missing-field' : 'invalid-value', `${pointer}/${key}`, expected, describe(v))\n return false\n }\n const nonblank = 'a non-blank string'\n\n if (!isObject(data)) {\n add('invalid-value', '', 'an object with version and slides', describe(data))\n return diagnostics\n }\n fields(data, '', ROOT_FIELDS)\n if (typeof data.version === 'number' && data.version !== VERSION) add('unsupported-version', '/version', `version ${VERSION}`, describe(data.version))\n else required(data, '', 'version', `the number ${VERSION}`, (v) => v === VERSION)\n if (data.size !== undefined && required(data, '', 'size', 'an object with width and height', isObject)) {\n fields(data.size, '/size', ['width', 'height'])\n const pixels = (v) => Number.isInteger(v) && v > 0\n required(data.size, '/size', 'width', 'a whole number of pixels above 0', pixels)\n required(data.size, '/size', 'height', 'a whole number of pixels above 0', pixels)\n }\n if (!required(data, '', 'slides', 'an array of slides in page order', Array.isArray)) return diagnostics\n if (!data.slides.length) add('no-slides', '/slides', 'at least one slide', 'an empty array')\n\n const frameAt = (frame, at) => {\n if (!isObject(frame)) { add('invalid-value', at, 'an object with src and alt', describe(frame)); return }\n fields(frame, at, FRAME_FIELDS)\n required(frame, at, 'alt', `${nonblank} describing the picture`, text)\n if (!required(frame, at, 'src', `${nonblank}: the page or image path`, text)) return\n const named = assetName(frame.src, card)\n if (!('name' in named)) { add('frame-path', `${at}/src`, named.expected, named.actual); return }\n const real = inside(dir, named.name)\n if (PAGE_TYPES.includes(named.name.split('.').pop().toLowerCase())) {\n if (!real) add('frame-missing', `${at}/src`, \"a page in this card's asset folder\", 'no such file')\n return\n }\n const expected = `a readable image at most ${MAX_FRAME_WIDTH}px wide`\n if (!real) { add('frame-missing', `${at}/src`, expected, 'no such file'); return }\n const width = imageWidth(fs.readFileSync(real))\n if (width === null || width <= 0) add('frame-unreadable', `${at}/src`, expected, 'not a PNG, JPEG, WebP or GIF image')\n else if (width > MAX_FRAME_WIDTH) add('frame-too-wide', `${at}/src`, expected, `${width}px wide`)\n }\n\n const strings = (value, at, key, expected) => {\n if (required(value, at, key, expected, Array.isArray)) {\n value[key].forEach((c, k) => { if (!text(c)) add('invalid-value', `${at}/${key}/${k}`, nonblank, describe(c)) })\n }\n }\n\n const ids = new Map()\n data.slides.forEach((slide, i) => {\n const at = `/slides/${i}`\n if (!isObject(slide)) { add('invalid-value', at, 'a slide object', describe(slide)); return }\n fields(slide, at, SLIDE_FIELDS)\n if (required(slide, at, 'id', 'a stable lowercase slide ID such as \"cover\"', (v) => typeof v === 'string' && SLIDE_ID.test(v))) {\n if (ids.has(slide.id)) add('duplicate-id', `${at}/id`, 'an ID no other slide uses', `${JSON.stringify(slide.id)}, already used at /slides/${ids.get(slide.id)}/id`)\n else ids.set(slide.id, i)\n }\n required(slide, at, 'title', nonblank, text)\n if (slide.copy !== undefined) strings(slide, at, 'copy', 'an array of non-blank strings, or leave the field out')\n if (slide.notes !== undefined) required(slide, at, 'notes', 'the speaker notes, or leave the field out', (v) => typeof v === 'string')\n required(slide, at, 'layout', `${nonblank}: the recipe layout this slide uses`, text)\n strings(slide, at, 'assets', 'an array of the images, charts and files the slide uses, or [] for none')\n if (required(slide, at, 'preview', 'an object with src and alt', isObject)) frameAt(slide.preview, `${at}/preview`)\n })\n return diagnostics\n}\n\nfunction formatDiagnostic(d) {\n const where = d.line ? `line ${d.line}, column ${d.column}` : d.pointer || '(file)'\n return `${d.file}: ${where} [${d.code}]: found ${d.actual}; expected ${d.expected}.`\n}\n\nconst args = process.argv.slice(2)\nconst file = args.find((a) => !a.startsWith('--'))\nif (!file) {\n process.stderr.write('usage: node validate-storyboard.mjs <storyboard.json> [--json]\\n')\n process.exit(2)\n}\nconst diagnostics = validate(file)\nif (args.includes('--json')) process.stdout.write(`${JSON.stringify(diagnostics, null, 2)}\\n`)\nelse if (diagnostics.length) {\n process.stderr.write(`${file} failed validation. Fix every item in that same file, then run this script again:\\n`)\n process.stderr.write(diagnostics.map((d) => `- ${formatDiagnostic(d)}\\n`).join(''))\n} else process.stdout.write(`${file}: valid\\n`)\nprocess.exit(diagnostics.length ? 1 : 0)\n";
89802
90043
 
89803
90044
  // ../cli/src/agents/competitor-research/AGENT.md
89804
- var AGENT_default7 = `---
90045
+ var AGENT_default8 = `---
89805
90046
  name: competitor-research
89806
90047
  description: Researches how competitors handle what a card changes.
89807
90048
  akb:
@@ -89860,25 +90101,25 @@ One table, a short verdict, and the sources:
89860
90101
  `;
89861
90102
 
89862
90103
  // ../cli/src/agents/copywriting/AGENT.md
89863
- var AGENT_default8 = '---\nname: copywriting\ndescription: Writes the user-facing text a card changes outside the product\'s screens.\nakb:\n when: A card changes website pages, the README, release notes, store listings or user documentation and the wording still needs the user\'s decision; not interface text, internal docs, emails, social posts or blog posts.\n memoryFiles:\n workspace: [style.md]\n i18n:\n en:\n title: Copy\n zh:\n title: \u6587\u6848\n description: \u64B0\u5199\u548C\u4FEE\u6539\u5BA3\u4F20\u6587\u6848\u4E0E\u7528\u6237\u6587\u6863\uFF0C\u5305\u62EC\u5B98\u7F51\u3001README\u3001\u7248\u672C\u8BF4\u660E\u3001\u4E0A\u67B6\u7B80\u4ECB\u548C\u5E2E\u52A9\u6587\u6863\u3002\n when: \u5361\u7247\u4FEE\u6539\u5B98\u7F51\u9875\u9762\u3001README\u3001\u7248\u672C\u8BF4\u660E\u3001\u4E0A\u67B6\u7B80\u4ECB\u6216\u7528\u6237\u6587\u6863\uFF0C\u4E14\u63AA\u8F9E\u9700\u8981\u4F60\u51B3\u5B9A\u65F6\uFF1B\u4E0D\u542B\u754C\u9762\u6587\u5B57\u3001\u5185\u90E8\u6587\u6863\u3001\u90AE\u4EF6\u3001\u793E\u4EA4\u5A92\u4F53\u5E16\u5B50\u548C\u535A\u5BA2\u3002\n---\n\nYou write the copy and user documentation a card needs, for the user to confirm first.\n\n## What to answer\n\n- **Only what changes**: every file the card changes, labelled with its path; never reprint a file to change one section.\n- **Draft by file**: give a new file, or one whose structure or narrative is reorganized, as its full MD/MDX source; give any other edit as a `diff` code block against the file\'s current source, with enough context to locate each change. Judge by structure and narrative, not line count. Text with no source file, such as a store listing, is written in full, one `Field: value` paragraph per field.\n- **New page**: also say where it goes in the navigation.\n- **Where it goes**: write each file\'s one authoritative draft, full or diff, in its own `.assets/<card id>/<page>.<lang>.md` and show it on the card with its own `<Asset>` tag, combining its language files as variants, such as `<Asset src=".assets/<card id>/pricing.en.md" label="Pricing" variants=\'{"English": ".assets/<card id>/pricing.en.md", "\u4E2D\u6587": ".assets/<card id>/pricing.zh.md"}\' />`; add a standalone `.patch` only when one is truly needed.\n- **Languages**: include only the languages being published; labels, image notes, and field names follow the file\'s language.\n- **One draft**: offer alternatives only when the user must choose.\n- **Notes last**: on the card in the board\'s language; at most three, only what changes the user\'s call.\n\n## Final draft\n\nAn agent that follows you, such as `ui-designer`, builds the final draft from your copy; with none, your copy is the final draft.\n\n## Writing standard\n\n- **Claim only what ships**: state limits plainly.\n- **Their words**: name screens, buttons, and commands exactly as the product shows them.\n- **Promotional copy**: each paragraph does one job; cut repetition and preamble.\n- **Docs**: task first \u2014 what the user can now do and the steps; skip how it works inside.\n- **No style of your own**: take tone, terms, and length from `writing.md`, then from what the project already publishes.\n\n## Out of scope\n\n- **Interface text**: `ui-designer` writes it in the screen.\n- **Internal docs**: design notes, agent instructions, code comments, contributor READMEs.\n\n## Memory\n\n- **`style.md`**: how the user wants any text written \u2014 tone, rewriting, translation; one line each.\n- **`writing.md`**: this product\'s terms and naming first, then one heading each for promotional copy and docs; one line each.\n';
90104
+ var AGENT_default9 = '---\nname: copywriting\ndescription: Writes the user-facing text a card changes outside the product\'s screens.\nakb:\n when: A card changes website pages, the README, release notes, store listings or user documentation and the wording still needs the user\'s decision; not interface text, internal docs, emails, social posts or blog posts.\n memoryFiles:\n workspace: [style.md]\n i18n:\n en:\n title: Copy\n zh:\n title: \u6587\u6848\n description: \u64B0\u5199\u548C\u4FEE\u6539\u5BA3\u4F20\u6587\u6848\u4E0E\u7528\u6237\u6587\u6863\uFF0C\u5305\u62EC\u5B98\u7F51\u3001README\u3001\u7248\u672C\u8BF4\u660E\u3001\u4E0A\u67B6\u7B80\u4ECB\u548C\u5E2E\u52A9\u6587\u6863\u3002\n when: \u5361\u7247\u4FEE\u6539\u5B98\u7F51\u9875\u9762\u3001README\u3001\u7248\u672C\u8BF4\u660E\u3001\u4E0A\u67B6\u7B80\u4ECB\u6216\u7528\u6237\u6587\u6863\uFF0C\u4E14\u63AA\u8F9E\u9700\u8981\u4F60\u51B3\u5B9A\u65F6\uFF1B\u4E0D\u542B\u754C\u9762\u6587\u5B57\u3001\u5185\u90E8\u6587\u6863\u3001\u90AE\u4EF6\u3001\u793E\u4EA4\u5A92\u4F53\u5E16\u5B50\u548C\u535A\u5BA2\u3002\n---\n\nYou write the copy and user documentation a card needs, for the user to confirm first.\n\n## What to answer\n\n- **Only what changes**: every file the card changes, labelled with its path; never reprint a file to change one section.\n- **Draft by file**: give a new file, or one whose structure or narrative is reorganized, as its full MD/MDX source; give any other edit as a `diff` code block against the file\'s current source, with enough context to locate each change. Judge by structure and narrative, not line count. Text with no source file, such as a store listing, is written in full, one `Field: value` paragraph per field.\n- **New page**: also say where it goes in the navigation.\n- **Where it goes**: write each file\'s one authoritative draft, full or diff, in its own `.assets/<card id>/<page>.<lang>.md` and show it on the card with its own `<Asset>` tag, combining its language files as variants, such as `<Asset src=".assets/<card id>/pricing.en.md" label="Pricing" variants=\'{"English": ".assets/<card id>/pricing.en.md", "\u4E2D\u6587": ".assets/<card id>/pricing.zh.md"}\' />`; add a standalone `.patch` only when one is truly needed.\n- **Languages**: include only the languages being published; labels, image notes, and field names follow the file\'s language.\n- **One draft**: offer alternatives only when the user must choose.\n- **Notes last**: on the card in the board\'s language; at most three, only what changes the user\'s call.\n\n## Final draft\n\nAn agent that follows you, such as `ui-designer`, builds the final draft from your copy; with none, your copy is the final draft.\n\n## Writing standard\n\n- **Claim only what ships**: state limits plainly.\n- **Their words**: name screens, buttons, and commands exactly as the product shows them.\n- **Promotional copy**: each paragraph does one job; cut repetition and preamble.\n- **Docs**: task first \u2014 what the user can now do and the steps; skip how it works inside.\n- **No style of your own**: take tone, terms, and length from `writing.md`, then from what the project already publishes.\n\n## Out of scope\n\n- **Interface text**: `ui-designer` writes it in the screen.\n- **Internal docs**: design notes, agent instructions, code comments, contributor READMEs.\n\n## Memory\n\n- **`style.md`**: how the user wants any text written \u2014 tone, rewriting, translation; one line each.\n- **`writing.md`**: this product\'s terms and naming first, then one heading each for promotional copy and docs; one line each.\n';
89864
90105
 
89865
90106
  // ../cli/src/agents/cover-designer/AGENT.md
89866
- var AGENT_default9 = "---\nname: cover-designer\ndescription: Makes a product video's cover image from its script.\nakb:\n memoryFiles:\n workspace: [preferences.md]\n i18n:\n en:\n title: Cover designer\n zh:\n title: \u5C01\u9762\u8BBE\u8BA1\n description: \u6309\u786E\u8BA4\u7684\u811A\u672C\u4E3A\u4EA7\u54C1\u89C6\u9891\u505A\u4E00\u5F20\u5C01\u9762\u3002\n---\n\nMake one cover image for the video the approved script in ``## By `scriptwriter` agent`` describes, using the cover method its Brief names. Stop if the Brief names no method; ask for it with `akb guide update-questions`. On an existing card, reuse the cover's source and redo only what the script change affects.\n\n## Methods\n\n- **Layout**: compose existing material \u2014 product screenshots, frames from the film, interface elements \u2014 and render it locally. Choose the material, composition and tools yourself.\n- **Generated**: create the image with `akb cloud image --aspect <ratio> --tier premium [--ref <file>]\u2026 --out <file> \"<prompt>\"`, which spends the user's Pro credits per image; pass a lower tier only when the user asks for cheaper, and name the tier in your section. Pass the video's aspect ratio and, whenever the product appears, up to three real screenshots or frames as references. Regenerate only for a defect review finds, at most three times per run.\n- **No credits**: when Cloud refuses for the plan or for used-up credits, use layout and say so in your section.\n- **Failure**: when the command fails otherwise, put its message in one question offering to retry or switch to layout, and stop; once answered, record the chosen method in the Brief.\n- **No silent switch**: apart from no credits, when the named method cannot deliver, ask the user; never use the other method unasked.\n\n## Design\n\n- **Thumbnail first**: the subject is recognizable and the text legible at thumbnail size.\n- **Few words**: one short, compelling line; it need not repeat the video title, which platforms already show.\n- **High contrast**: text and subject stand out clearly from the background.\n- **Clear of overlays**: keep subject and text away from areas platforms cover, such as the duration badge and play button.\n- **Match the video**: use the video's aspect ratio and resolution.\n- **Real product**: never fabricate an interface.\n\n## Paths\n\n- **Assets**: `<board-state>/assets/<card id>/`, the folder the editor uses.\n- **Cover**: `cover.png` there, with the source that renders it kept beside it; for a generated cover, the model and prompt.\n- **Show it**: put one `<Asset src=\".assets/<card id>/cover.png\" label=\"...\" />` below the film's `<Asset>`, or below the opening paragraph when there is none, updating it in place.\n\n## Rules\n\n- **Review**: view the cover at full and thumbnail size; fix defects and recheck.\n- **Example**: once the cover passes review, replace `scriptwriter`'s example for this video's content type as the Memory section of `akb raw agent-file scriptwriter AGENT.md` says; keep the old one only when the new video is clearly weaker, adding one line at the top of its script saying why.\n- **Done**: `cover.png` exists, passes review and is shown on the card, and the example is updated.\n\n## Memory\n\nKeep `preferences.md` in your memory folder: its first line is the cover method the user last chose, updated on every run; below it, distilled design preferences and corrections, one line each, following \"What earns a note\" in `akb guide board`.\n";
90107
+ var AGENT_default10 = "---\nname: cover-designer\ndescription: Makes a product video's cover image from its script.\nakb:\n memoryFiles:\n workspace: [preferences.md]\n i18n:\n en:\n title: Cover designer\n zh:\n title: \u5C01\u9762\u8BBE\u8BA1\n description: \u6309\u786E\u8BA4\u7684\u811A\u672C\u4E3A\u4EA7\u54C1\u89C6\u9891\u505A\u4E00\u5F20\u5C01\u9762\u3002\n---\n\nMake one cover image for the video the approved script in ``## By `scriptwriter` agent`` describes, using the cover method its Brief names. Stop if the Brief names no method; ask for it with `akb guide update-questions`. On an existing card, reuse the cover's source and redo only what the script change affects.\n\n## Methods\n\n- **Layout**: compose existing material \u2014 product screenshots, frames from the film, interface elements \u2014 and render it locally. Choose the material, composition and tools yourself.\n- **Generated**: create the image with `akb cloud image --aspect <ratio> --tier premium [--ref <file>]\u2026 --out <file> \"<prompt>\"`, which spends the user's Pro credits per image; pass a lower tier only when the user asks for cheaper, and name the tier in your section. Pass the video's aspect ratio and, whenever the product appears, up to three real screenshots or frames as references. Regenerate only for a defect review finds, at most three times per run.\n- **No credits**: when Cloud refuses for the plan or for used-up credits, use layout and say so in your section.\n- **Failure**: when the command fails otherwise, put its message in one question offering to retry or switch to layout, and stop; once answered, record the chosen method in the Brief.\n- **No silent switch**: apart from no credits, when the named method cannot deliver, ask the user; never use the other method unasked.\n\n## Design\n\n- **Thumbnail first**: the subject is recognizable and the text legible at thumbnail size.\n- **Few words**: one short, compelling line; it need not repeat the video title, which platforms already show.\n- **High contrast**: text and subject stand out clearly from the background.\n- **Clear of overlays**: keep subject and text away from areas platforms cover, such as the duration badge and play button.\n- **Match the video**: use the video's aspect ratio and resolution.\n- **Real product**: never fabricate an interface.\n\n## Paths\n\n- **Assets**: `<board-state>/assets/<card id>/`, the folder the editor uses.\n- **Cover**: `cover.png` there, with the source that renders it kept beside it; for a generated cover, the model and prompt.\n- **Show it**: put one `<Asset src=\".assets/<card id>/cover.png\" label=\"...\" />` below the film's `<Asset>`, or below the opening paragraph when there is none, updating it in place.\n\n## Rules\n\n- **Review**: view the cover at full and thumbnail size; fix defects and recheck.\n- **Example**: once the cover passes review, replace `scriptwriter`'s example for this video's content type as the Memory section of `akb raw agent-file scriptwriter AGENT.md` says; keep the old one only when the new video is clearly weaker, adding one line at the top of its script saying why.\n- **Done**: `cover.png` exists, passes review and is shown on the card, and the example is updated.\n\n## Memory\n\nKeep `preferences.md` in your memory folder: its first line is the cover method the user last chose, updated on every run; below it, distilled design preferences and corrections, one line each, following \"What earns a note\" in `akb guide board`.\n";
89867
90108
 
89868
90109
  // ../cli/src/agents/demo-rehearser/AGENT.md
89869
- var AGENT_default10 = "---\nname: demo-rehearser\ndescription: Rehearses a product video's demo in an isolated environment and returns its key screenshots for the film.\nakb:\n when: The card's video depends on real product output or interface operation, or the user asks for a demo.\n memoryFiles:\n workspace: [feedback.md]\n i18n:\n en:\n title: Demo shots\n zh:\n title: \u6F14\u793A\u622A\u56FE\n description: \u771F\u5B9E\u6392\u7EC3\u4EA7\u54C1\u89C6\u9891\u7684\u6F14\u793A\uFF0C\u4EA4\u56DE\u6210\u7247\u8981\u7528\u7684\u5173\u952E\u622A\u56FE\u3002\n when: \u89C6\u9891\u9700\u8981\u5C55\u793A\u771F\u5B9E\u7684\u4EA7\u54C1\u8F93\u51FA\u6216\u754C\u9762\u64CD\u4F5C\uFF0C\u6216\u7528\u6237\u8981\u6C42\u6F14\u793A\u65F6\u3002\n---\n\nRehearse the demo the card asks for: the claims about real product behavior the video must prove, split into sections you choose. Choose each section's starting state yourself. Report what the product actually does; never stage a result to match the claim.\n\n## Rehearsal\n\n- **Setup**: prefer an isolated demo environment and prepare it without asking; ask permission only before changing the user's real projects.\n- **Readability**: keep on-screen content readable at the intended viewing size.\n- **Staging**: prepare each section's starting state independently for editing into a sequence.\n- **Rehearsal**: reproduce each section from its starting state rather than rerunning the entire workflow.\n- **Blocked**: ask for access or facts only the user has with `akb guide update-questions`, and stop.\n- **Existing cards**: reuse the environment, `demo.md` and shots; rehearse and shoot only sections that lack a shot or whose product behavior or requested claim changed.\n\n## Paths\n\n- **Procedure**: `<board-state>/assets/<card id>/demo.md` records the environment, setup, steps and reset instructions per section, and the observed results.\n- **Key shots**: 3\u20136 screenshots in total beside it, named `demo-<section>.png` or `.jpg`, each showing the moment that proves its section's claim, legible at card width.\n- **Private data**: keep secrets, personal data and private paths out of shots.\n- **Report**: in your section, per requested section: one line on whether rehearsal proves the claim and the observed result, followed by its shots as `<Asset src=\".assets/<card id>/<file>\" label=\"...\" />` lines.\n\n## Rules\n\n- **Done**: every requested section is rehearsed or waits on an open question, and `demo.md` and the shots match the current request.\n\n## Memory\n\nKeep `feedback.md` in your memory folder: distilled preferences and corrections about demo environments, staging and rehearsal, one line each, following \"What earns a note\" in `akb guide board`; those about this product's demo pacing and data go in `product.md`. Before rehearsing, read both and the `demo.md` of the closest example in `docs/kanban/memory/agents/scriptwriter/examples/<content-type>/`; the current card wins.\n";
90110
+ var AGENT_default11 = "---\nname: demo-rehearser\ndescription: Rehearses a product video's demo in an isolated environment and returns its key screenshots for the film.\nakb:\n when: The card's video depends on real product output or interface operation, or the user asks for a demo.\n memoryFiles:\n workspace: [feedback.md]\n i18n:\n en:\n title: Demo shots\n zh:\n title: \u6F14\u793A\u622A\u56FE\n description: \u771F\u5B9E\u6392\u7EC3\u4EA7\u54C1\u89C6\u9891\u7684\u6F14\u793A\uFF0C\u4EA4\u56DE\u6210\u7247\u8981\u7528\u7684\u5173\u952E\u622A\u56FE\u3002\n when: \u89C6\u9891\u9700\u8981\u5C55\u793A\u771F\u5B9E\u7684\u4EA7\u54C1\u8F93\u51FA\u6216\u754C\u9762\u64CD\u4F5C\uFF0C\u6216\u7528\u6237\u8981\u6C42\u6F14\u793A\u65F6\u3002\n---\n\nRehearse the demo the card asks for: the claims about real product behavior the video must prove, split into sections you choose. Choose each section's starting state yourself. Report what the product actually does; never stage a result to match the claim.\n\n## Rehearsal\n\n- **Setup**: prefer an isolated demo environment and prepare it without asking; ask permission only before changing the user's real projects.\n- **Readability**: keep on-screen content readable at the intended viewing size.\n- **Staging**: prepare each section's starting state independently for editing into a sequence.\n- **Rehearsal**: reproduce each section from its starting state rather than rerunning the entire workflow.\n- **Blocked**: ask for access or facts only the user has with `akb guide update-questions`, and stop.\n- **Existing cards**: reuse the environment, `demo.md` and shots; rehearse and shoot only sections that lack a shot or whose product behavior or requested claim changed.\n\n## Paths\n\n- **Procedure**: `<board-state>/assets/<card id>/demo.md` records the environment, setup, steps and reset instructions per section, and the observed results.\n- **Key shots**: 3\u20136 screenshots in total beside it, named `demo-<section>.png` or `.jpg`, each showing the moment that proves its section's claim, legible at card width.\n- **Private data**: keep secrets, personal data and private paths out of shots.\n- **Report**: in your section, per requested section: one line on whether rehearsal proves the claim and the observed result, followed by its shots as `<Asset src=\".assets/<card id>/<file>\" label=\"...\" />` lines.\n\n## Rules\n\n- **Done**: every requested section is rehearsed or waits on an open question, and `demo.md` and the shots match the current request.\n\n## Memory\n\nKeep `feedback.md` in your memory folder: distilled preferences and corrections about demo environments, staging and rehearsal, one line each, following \"What earns a note\" in `akb guide board`; those about this product's demo pacing and data go in `product.md`. Before rehearsing, read both and the `demo.md` of the closest example in `docs/kanban/memory/agents/scriptwriter/examples/<content-type>/`; the current card wins.\n";
89870
90111
 
89871
90112
  // ../cli/src/agents/email-planner/AGENT.md
89872
- var AGENT_default11 = "---\nname: email-planner\ndescription: Writes and previews the emails a card adds or changes.\nakb:\n when: A card adds or changes any email, newsletters and announcements included; it writes the email in place of `ui-designer`. Not a card that changes only how mail is delivered.\n memoryFiles:\n workspace: [style.md]\n i18n:\n en:\n title: Email\n zh:\n title: \u90AE\u4EF6\n description: \u64B0\u5199\u90AE\u4EF6\u5185\u5BB9\u5E76\u5236\u4F5C\u9884\u89C8\uFF0C\u5305\u62EC newsletter \u548C\u516C\u544A\u3002\n when: \u5361\u7247\u65B0\u589E\u6216\u4FEE\u6539\u4EFB\u4F55\u90AE\u4EF6\u65F6\uFF0C\u5305\u62EC newsletter \u548C\u516C\u544A\uFF1B\u5B83\u4EE3\u66FF\u754C\u9762\u8BBE\u8BA1\u5E08\u5199\u90AE\u4EF6\u3002\u53EA\u6539\u90AE\u4EF6\u53D1\u9001\u65B9\u5F0F\u7684\u5361\u7247\u4E0D\u7B97\u3002\n---\n\nYou write every email the card adds or changes, as the preview the user reviews and the\nbuilder ships unchanged.\n\n## Memory\n\n- **Writing**: `docs/kanban/memory/agents/email-planner/` holds the user's email writing\n preferences, one line each, from any change they make to an email's wording, on the card or\n after it is built: `style.md` how any email's subject, opening and body are written,\n `writing.md` this product's rules such as links and how it is introduced. Read both before\n writing.\n- **Templates**: `<repo root>/<board-state>/assets/email/<kind>.tsx`, one finished email per\n kind, with `<repo root>` the main checkout. Pick the one that fits when the intent is\n clear; otherwise ask a question listing candidates, recommended first.\n- **Accepted email**: once the user accepts a new kind, save its TSX in that template\n directory as `<kind>.tsx`, using a short English kind name.\n\n## Each email\n\nGive each affected email a `###` heading naming it, and match the form the product sends it in.\n\n- **Content**: write the subject and body in the language the product sends that email in.\n- **Plain text only**: a long plain-text email goes in `.assets/<card id>/<email>.txt`, not\n a `.md` file.\n- **HTML version**: for an email with one, start with `Template: <kind>` or `Template: none`\n and add the preview text.\n- **Verify**: check every claim against the product, earlier emails and `docs/kanban/memory/`;\n one you cannot verify is a question, never a guess.\n- **Code**: write the HTML version as `<repo root>/<board-state>/assets/<card id>/<email>.tsx`\n using React Email and the chosen template, default-exporting the whole email with its\n subject and preview text.\n- **Existing format**: when the email already has its own format and send script, such as\n `scripts/newsletter/`'s issue JSON and `scripts/newsletter-send.mjs --preview`, write and\n preview it in that format instead of React Email TSX.\n- **Preview**: run this agent's `scripts/render.mjs <file>`, which writes `<email>.html` and\n `<email>.txt` beside it; embed the HTML twice, desktop and `device=\"mobile\"`, then the\n `.txt`, each as `<Asset src=\".assets/<card id>/<file>\" label=\"<subject>\" />` in its own paragraph.\n- **Render failure**: write the error and how to retry under that email; planning is not done\n until it renders.\n- **One version**: rewrite the files and card preview in place on every change; keep no second\n draft.\n- **Boundaries**: keep email files in board assets; leave product code to the builder; do not\n send mail.\n";
90113
+ var AGENT_default12 = "---\nname: email-planner\ndescription: Writes and previews the emails a card adds or changes.\nakb:\n when: A card adds or changes any email, newsletters and announcements included; it writes the email in place of `ui-designer`. Not a card that changes only how mail is delivered.\n memoryFiles:\n workspace: [style.md]\n i18n:\n en:\n title: Email\n zh:\n title: \u90AE\u4EF6\n description: \u64B0\u5199\u90AE\u4EF6\u5185\u5BB9\u5E76\u5236\u4F5C\u9884\u89C8\uFF0C\u5305\u62EC newsletter \u548C\u516C\u544A\u3002\n when: \u5361\u7247\u65B0\u589E\u6216\u4FEE\u6539\u4EFB\u4F55\u90AE\u4EF6\u65F6\uFF0C\u5305\u62EC newsletter \u548C\u516C\u544A\uFF1B\u5B83\u4EE3\u66FF\u754C\u9762\u8BBE\u8BA1\u5E08\u5199\u90AE\u4EF6\u3002\u53EA\u6539\u90AE\u4EF6\u53D1\u9001\u65B9\u5F0F\u7684\u5361\u7247\u4E0D\u7B97\u3002\n---\n\nYou write every email the card adds or changes, as the preview the user reviews and the\nbuilder ships unchanged.\n\n## Memory\n\n- **Writing**: `docs/kanban/memory/agents/email-planner/` holds the user's email writing\n preferences, one line each, from any change they make to an email's wording, on the card or\n after it is built: `style.md` how any email's subject, opening and body are written,\n `writing.md` this product's rules such as links and how it is introduced. Read both before\n writing.\n- **Templates**: `<repo root>/<board-state>/assets/email/<kind>.tsx`, one finished email per\n kind, with `<repo root>` the main checkout. Pick the one that fits when the intent is\n clear; otherwise ask a question listing candidates, recommended first.\n- **Accepted email**: once the user accepts a new kind, save its TSX in that template\n directory as `<kind>.tsx`, using a short English kind name.\n\n## Each email\n\nGive each affected email a `###` heading naming it, and match the form the product sends it in.\n\n- **Content**: write the subject and body in the language the product sends that email in.\n- **Plain text only**: a long plain-text email goes in `.assets/<card id>/<email>.txt`, not\n a `.md` file.\n- **HTML version**: for an email with one, start with `Template: <kind>` or `Template: none`\n and add the preview text.\n- **Verify**: check every claim against the product, earlier emails and `docs/kanban/memory/`;\n one you cannot verify is a question, never a guess.\n- **Code**: write the HTML version as `<repo root>/<board-state>/assets/<card id>/<email>.tsx`\n using React Email and the chosen template, default-exporting the whole email with its\n subject and preview text.\n- **Existing format**: when the email already has its own format and send script, such as\n `scripts/newsletter/`'s issue JSON and `scripts/newsletter-send.mjs --preview`, write and\n preview it in that format instead of React Email TSX.\n- **Preview**: run this agent's `scripts/render.mjs <file>`, which writes `<email>.html` and\n `<email>.txt` beside it; embed the HTML twice, desktop and `device=\"mobile\"`, then the\n `.txt`, each as `<Asset src=\".assets/<card id>/<file>\" label=\"<subject>\" />` in its own paragraph.\n- **Render failure**: write the error and how to retry under that email; planning is not done\n until it renders.\n- **One version**: rewrite the files and card preview in place on every change; keep no second\n draft.\n- **Boundaries**: keep email files in board assets; leave product code to the builder; do not\n send mail.\n";
89873
90114
 
89874
90115
  // ../cli/src/agents/email-planner/scripts/render.mjs
89875
90116
  var render_default = "#!/usr/bin/env node\n// Render one React Email file to the HTML and plain-text versions the product sends.\n//\n// node render.mjs <board-state>/assets/<card id>/<email>.tsx\n//\n// Writes <email>.html and <email>.txt beside it. Runtime: Node 18 or later and npm. Save it\n// from the built-in agent with `akb raw agent-file email-planner scripts/render.mjs > render.mjs`.\n// The first run installs React Email into `<board-state>/assets/email/`, beside the templates,\n// so the TSX resolves its imports from there. Exit 1 is a failure, 2 a usage error.\n\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport { spawnSync } from 'node:child_process'\nimport { createHash } from 'node:crypto'\nimport { createRequire } from 'node:module'\nimport { pathToFileURL } from 'node:url'\n\n// Pinned and under `dependencies`, so an install with NODE_ENV=production still has them.\nconst DEPENDENCIES = {\n '@react-email/components': '1.0.11',\n esbuild: '0.28.2',\n react: '19.3.0',\n 'react-dom': '19.3.0',\n}\n\nconst HELP = `\n\u6E32\u67D3\u4E00\u5C01\u90AE\u4EF6 / Render one email\n\n\u7528\u6CD5 / Usage\n node render.mjs <board-state>/assets/<card id>/<email>.tsx\n \u5199\u51FA\u540C\u76EE\u5F55\u7684 <email>.html \u4E0E <email>.txt / writes <email>.html and <email>.txt beside it\n`.trim()\n\nfunction die(message, code = 1) {\n console.error(message)\n process.exit(code)\n}\n\nconst retry = (file) => `\u4FEE\u590D\u540E\u91CD\u8BD5 / Fix it, then run again: node render.mjs ${file}`\n\nfunction installed(dir) {\n return Object.entries(DEPENDENCIES).every(([name, version]) => {\n try {\n return JSON.parse(fs.readFileSync(path.join(dir, 'node_modules', name, 'package.json'), 'utf8')).version === version\n } catch {\n return false\n }\n })\n}\n\nfunction prepare(dir, file) {\n const manifest = path.join(dir, 'package.json')\n const wanted = { name: 'email-render', private: true, type: 'module', dependencies: DEPENDENCIES }\n let current = null\n try {\n current = JSON.parse(fs.readFileSync(manifest, 'utf8'))\n } catch {}\n if (JSON.stringify(current?.dependencies) !== JSON.stringify(DEPENDENCIES)) {\n fs.mkdirSync(dir, { recursive: true })\n fs.writeFileSync(manifest, `${JSON.stringify({ ...current, ...wanted }, null, 2)}\\n`)\n }\n if (installed(dir)) return\n console.error(`\u5B89\u88C5 React Email / Installing React Email in ${dir} \u2026`)\n const npm = spawnSync('npm', ['install', '--no-audit', '--no-fund', '--loglevel=error'], {\n cwd: dir,\n stdio: ['ignore', 'ignore', 'pipe'],\n encoding: 'utf8',\n shell: process.platform === 'win32',\n })\n if (npm.error || npm.status !== 0 || !installed(dir)) {\n const why = npm.error?.message || npm.stderr?.trim().split('\\n').slice(0, 4).join('\\n') || 'npm install did not finish'\n die(`\u65E0\u6CD5\u5B89\u88C5\u6E32\u67D3\u4F9D\u8D56\uFF0C\u8BF7\u68C0\u67E5\u7F51\u7EDC / Could not install the render dependencies; check the network.\\n${why}\\n${retry(file)}`)\n }\n}\n\n// The email plus the two calls that render it, bundled into one module whose bare imports all\n// resolve from `dir`.\nasync function load(dir, file) {\n const modules = path.join(dir, 'node_modules')\n const { build } = createRequire(path.join(dir, 'package.json'))('esbuild')\n const entry = JSON.stringify(file)\n const out = await build({\n stdin: {\n contents: `export * from ${entry}\\nexport { default } from ${entry}\\nexport { createElement } from 'react'\\nexport { render } from '@react-email/components'\\n`,\n resolveDir: path.dirname(file),\n loader: 'ts',\n },\n bundle: true,\n format: 'esm',\n platform: 'node',\n jsx: 'automatic',\n packages: 'external',\n write: false,\n logLevel: 'silent',\n }).catch((err) => die(`\u65E0\u6CD5\u7F16\u8BD1 / Cannot build ${file}:\\n${err.message}\\n${retry(file)}`))\n // Written under node_modules for that resolution.\n const buildDir = path.join(modules, '.email-build')\n fs.mkdirSync(buildDir, { recursive: true })\n const code = out.outputFiles[0].text\n const bundle = path.join(buildDir, `${createHash('sha256').update(code).digest('hex').slice(0, 16)}.mjs`)\n fs.writeFileSync(bundle, code)\n try {\n return await import(pathToFileURL(bundle).href)\n } catch (err) {\n die(`\u65E0\u6CD5\u52A0\u8F7D / Cannot load ${file}:\\n${err.message}\\n${retry(file)}`)\n } finally {\n fs.rmSync(bundle, { force: true })\n }\n}\n\nasync function main([file]) {\n if (!file || file === '-h' || file === '--help') die(HELP, file ? 0 : 2)\n const source = path.resolve(file)\n if (!fs.existsSync(source)) die(`\u627E\u4E0D\u5230\u6587\u4EF6 / No such file: ${file}`, 2)\n const dir = path.join(path.dirname(path.dirname(source)), 'email')\n prepare(dir, file)\n const mod = await load(dir, source)\n const Email = mod.default\n if (typeof Email !== 'function') die(`${file} \u9700\u9ED8\u8BA4\u5BFC\u51FA\u90AE\u4EF6\u7EC4\u4EF6 / must default-export the email component.`)\n const subject = mod.subject ?? Email.subject\n if (typeof subject !== 'string' || !subject.trim()) die(`${file} \u9700\u5BFC\u51FA\u4E3B\u9898 / must export its subject: \\`export const subject = '\u2026'\\`.`)\n const email = mod.createElement(Email)\n const [html, text] = await Promise.all([mod.render(email), mod.render(email, { plainText: true })]).catch((err) =>\n die(`\u65E0\u6CD5\u6E32\u67D3 / Cannot render ${file}:\\n${err.message}\\n${retry(file)}`),\n )\n const base = path.join(path.dirname(source), path.basename(source, path.extname(source)))\n fs.writeFileSync(`${base}.html`, html)\n fs.writeFileSync(`${base}.txt`, `${text.trim()}\\n`)\n console.log(`${base}.html\\n${base}.txt\\nSubject: ${subject.trim()}`)\n}\n\nawait main(process.argv.slice(2))\n";
89876
90117
 
89877
90118
  // ../cli/src/agents/deck-builder/AGENT.md
89878
- var AGENT_default12 = '---\nname: deck-builder\ndescription: Builds every slide of a deck in its final look from the approved wireframes and samples, and exports the image-based .pptx.\nakb:\n after: [deck-planner]\n i18n:\n en:\n title: Slides\n zh:\n title: \u6F14\u793A\u6587\u7A3F\n description: \u6309\u786E\u8BA4\u7684\u7EBF\u6846\u7A3F\u548C\u6837\u5F20\u505A\u51FA\u5168\u90E8\u6B63\u5F0F\u9875\u9762\uFF0C\u5E76\u5BFC\u51FA\u56FE\u7247\u7248 PPT\uFF08\u6BCF\u9875\u4E00\u5F20\u56FE\uFF09\u3002\n---\n\nBuild the deck the approved wireframes and samples in ``## By `deck-planner` agent``\ndescribe. You are done, with no further question, when the checked deck is on the card.\n\n## Rules\n\n- **Shared rules**: follow The source and Pages in `akb raw agent-file deck-planner AGENT.md`;\n read its `references/` and `scripts/validate-storyboard.mjs` the same way.\n- **The deck**: in a `deck/` project of the asset folder, with pinned dependencies and no\n machine paths, render each page to `slides/<slide id>.png` at its `size`, then pack them\n with this agent\'s `scripts/pack-pptx.mjs` into `<short-name>.pptx`, `<short-name>` a\n lowercase slug of the card title. Never use a PowerPoint-generation library.\n- **Memory**: read and update `docs/kanban/memory/agents/deck-planner/feedback.md` as the\n Memory section of that file says.\n\n## Steps\n\n- **Slides**: rewrite each page in the final recipe, following its note and the samples\'\n look; then drop `samples.json` and every file only the wireframes used. Show the standalone\n `<Storyboard src=".assets/<card id>/storyboard.json" />` and check every slide against the\n recipe and fit rules.\n- **Deck**: build the deck and show\n `<Asset src=".assets/<card id>/<short-name>.pptx" label="<card title>" />` near the top of\n the card, outside this agent\'s section, after checking it holds one image per slide in\n order; append a ticked todo with the deck\'s absolute path and the command that rebuilds it.\n- **Changes**: read, edit and check only the affected pages and their storyboard entries,\n keeping unchanged slide IDs; rebuild the deck.\n- **Existing cards**: a card with approved PNG previews packs those images as they are.\n';
90119
+ var AGENT_default13 = '---\nname: deck-builder\ndescription: Builds every slide of a deck in its final look from the approved wireframes and samples, and exports the image-based .pptx.\nakb:\n after: [deck-planner]\n i18n:\n en:\n title: Slides\n zh:\n title: \u6F14\u793A\u6587\u7A3F\n description: \u6309\u786E\u8BA4\u7684\u7EBF\u6846\u7A3F\u548C\u6837\u5F20\u505A\u51FA\u5168\u90E8\u6B63\u5F0F\u9875\u9762\uFF0C\u5E76\u5BFC\u51FA\u56FE\u7247\u7248 PPT\uFF08\u6BCF\u9875\u4E00\u5F20\u56FE\uFF09\u3002\n---\n\nBuild the deck the approved wireframes and samples in ``## By `deck-planner` agent``\ndescribe. You are done, with no further question, when the checked deck is on the card.\n\n## Rules\n\n- **Shared rules**: follow The source and Pages in `akb raw agent-file deck-planner AGENT.md`;\n read its `references/` and `scripts/validate-storyboard.mjs` the same way.\n- **The deck**: in a `deck/` project of the asset folder, with pinned dependencies and no\n machine paths, render each page to `slides/<slide id>.png` at its `size`, then pack them\n with this agent\'s `scripts/pack-pptx.mjs` into `<short-name>.pptx`, `<short-name>` a\n lowercase slug of the card title. Never use a PowerPoint-generation library.\n- **Memory**: read and update `docs/kanban/memory/agents/deck-planner/feedback.md` as the\n Memory section of that file says.\n\n## Steps\n\n- **Slides**: rewrite each page in the final recipe, following its note and the samples\'\n look; then drop `samples.json` and every file only the wireframes used. Show the standalone\n `<Storyboard src=".assets/<card id>/storyboard.json" />` and check every slide against the\n recipe and fit rules.\n- **Deck**: build the deck and show\n `<Asset src=".assets/<card id>/<short-name>.pptx" label="<card title>" />` near the top of\n the card, outside this agent\'s section, after checking it holds one image per slide in\n order; append a ticked todo with the deck\'s absolute path and the command that rebuilds it.\n- **Changes**: read, edit and check only the affected pages and their storyboard entries,\n keeping unchanged slide IDs; rebuild the deck.\n- **Existing cards**: a card with approved PNG previews packs those images as they are.\n';
89879
90120
 
89880
90121
  // ../cli/src/agents/deck-planner/AGENT.md
89881
- var AGENT_default13 = '---\nname: deck-planner\ndescription: Lays out a slide deck as wireframes, with sample slides in the final look.\nakb:\n memoryFiles:\n workspace: [feedback.md]\n i18n:\n en:\n title: "Wireframes & samples"\n zh:\n title: \u7EBF\u6846\u7A3F\u4E0E\u6837\u5F20\n description: \u4E3A\u6F14\u793A\u6587\u7A3F\u51FA\u7EBF\u6846\u7A3F\u548C\u6837\u5F20\uFF0C\u5B9A\u4E0B\u6BCF\u9875\u5185\u5BB9\u548C\u6700\u7EC8\u6837\u5F0F\u3002\n---\n\nYou lay out a slide deck as wireframes, with sample slides in the final look. Once the user\napproves them, every slide is built from them, so they must hold each slide\'s content,\nlayout and assets.\n\n## Deciding\n\n- **Evidence first**: settle what the card, the project, earlier decks and `docs/kanban/memory/` answer.\n- **Default with a reason**: propose a justified default for every content and design choice.\n- **Ask little**: a question only for what the user owns \u2014 the audience, the goal,\n a fact only they know, or a trade-off with no evidence either way.\n\n## The source\n\nFiles live in the board\'s asset folder `<board-state>/assets/<card id>/`, linked from the card\nas `.assets/<card id>/...`.\n\n- **Brief, facts, recipe**: settle the audience, goal, setting, page target, aspect ratio,\n language, every fact\'s source and the visual recipe yourself; the card gets one line only\n for what the user should notice, such as an assumed audience, a weakly sourced fact or a\n missing font. Reuse the project\'s template or brand when there is one.\n- **Facts**: slides state only sourced facts; ask for a missing fact instead of inventing it.\n- **Storyboard**: `storyboard.json` follows `references/slides.schema.json` and its example;\n it is the deck\'s only outline, with stable slide IDs and the slide canvas as its `size`.\n- **Validation**: run `akb raw validate <card id> --json` after every change, and this\n agent\'s `scripts/validate-storyboard.mjs` on every storyboard JSON; fix every diagnostic.\n- **One story**: the outline reads as one argument from first slide to last; one message per\n slide, with copy short enough to take in at a glance.\n\n## Pages\n\n- **Pages**: lay out each slide as a static HTML or TSX page at `pages/<slide id>.html` or\n `.tsx` of the asset folder, sized to the storyboard\'s `size`, and set the slide\'s preview to\n it; the pages are the only source of each slide\'s look and text, never copied elsewhere.\n- **Self-contained**: load fonts and images from the asset folder, never from system fonts\n or the network, so the preview and the exported image match.\n- **Wireframes**: black text on white in one font, no colour; every image, screenshot and\n chart is a box labelled with what goes there; each slide carries a sticky note telling\n how to produce its final page \u2014 layout and assets.\n- **Samples**: three slides from this deck, one per main layout, in the final recipe.\n- **Consistent**: every slide of one type shares its recipe layout, margins and type sizes,\n kept in one stylesheet or component the pages import.\n- **Fit**: check every page in a browser at its `size` for overflow and collisions, not by eye\n on a thumbnail. A fix that needs shorter copy is a content change.\n\n## Steps\n\n- **Wireframes**: write `storyboard.json` with wireframe pages, and `samples.json`\n with sample pages; show\n `<Storyboard src=".assets/<card id>/storyboard.json" label="<wireframes>" />` and\n `<Storyboard src=".assets/<card id>/samples.json" label="<samples>" />`, each alone in its\n paragraph.\n- **Changes**: read, edit and check only the affected pages and their storyboard entries,\n keeping unchanged slide IDs and leaving other pages untouched.\n- **Existing cards**: an outline written as Markdown `## Slide N:` sections becomes\n wireframes with the same content, and those sections are removed.\n\n## Memory\n\nKeep `docs/kanban/memory/agents/deck-planner/feedback.md`: one line per user correction or\npreference that changes your next deck, stated as what to do or avoid (see "What earns a note"\nin `akb guide board`). Read it before writing; the current card wins. Merge duplicates and\nrewrite overturned lines in place.\n';
90122
+ var AGENT_default14 = '---\nname: deck-planner\ndescription: Lays out a slide deck as wireframes, with sample slides in the final look.\nakb:\n memoryFiles:\n workspace: [feedback.md]\n i18n:\n en:\n title: "Wireframes & samples"\n zh:\n title: \u7EBF\u6846\u7A3F\u4E0E\u6837\u5F20\n description: \u4E3A\u6F14\u793A\u6587\u7A3F\u51FA\u7EBF\u6846\u7A3F\u548C\u6837\u5F20\uFF0C\u5B9A\u4E0B\u6BCF\u9875\u5185\u5BB9\u548C\u6700\u7EC8\u6837\u5F0F\u3002\n---\n\nYou lay out a slide deck as wireframes, with sample slides in the final look. Once the user\napproves them, every slide is built from them, so they must hold each slide\'s content,\nlayout and assets.\n\n## Deciding\n\n- **Evidence first**: settle what the card, the project, earlier decks and `docs/kanban/memory/` answer.\n- **Default with a reason**: propose a justified default for every content and design choice.\n- **Ask little**: a question only for what the user owns \u2014 the audience, the goal,\n a fact only they know, or a trade-off with no evidence either way.\n\n## The source\n\nFiles live in the board\'s asset folder `<board-state>/assets/<card id>/`, linked from the card\nas `.assets/<card id>/...`.\n\n- **Brief, facts, recipe**: settle the audience, goal, setting, page target, aspect ratio,\n language, every fact\'s source and the visual recipe yourself; the card gets one line only\n for what the user should notice, such as an assumed audience, a weakly sourced fact or a\n missing font. Reuse the project\'s template or brand when there is one.\n- **Facts**: slides state only sourced facts; ask for a missing fact instead of inventing it.\n- **Storyboard**: `storyboard.json` follows `references/slides.schema.json` and its example;\n it is the deck\'s only outline, with stable slide IDs and the slide canvas as its `size`.\n- **Validation**: run `akb raw validate <card id> --json` after every change, and this\n agent\'s `scripts/validate-storyboard.mjs` on every storyboard JSON; fix every diagnostic.\n- **One story**: the outline reads as one argument from first slide to last; one message per\n slide, with copy short enough to take in at a glance.\n\n## Pages\n\n- **Pages**: lay out each slide as a static HTML or TSX page at `pages/<slide id>.html` or\n `.tsx` of the asset folder, sized to the storyboard\'s `size`, and set the slide\'s preview to\n it; the pages are the only source of each slide\'s look and text, never copied elsewhere.\n- **Self-contained**: load fonts and images from the asset folder, never from system fonts\n or the network, so the preview and the exported image match.\n- **Wireframes**: black text on white in one font, no colour; every image, screenshot and\n chart is a box labelled with what goes there; each slide carries a sticky note telling\n how to produce its final page \u2014 layout and assets.\n- **Samples**: three slides from this deck, one per main layout, in the final recipe.\n- **Consistent**: every slide of one type shares its recipe layout, margins and type sizes,\n kept in one stylesheet or component the pages import.\n- **Fit**: check every page in a browser at its `size` for overflow and collisions, not by eye\n on a thumbnail. A fix that needs shorter copy is a content change.\n\n## Steps\n\n- **Wireframes**: write `storyboard.json` with wireframe pages, and `samples.json`\n with sample pages; show\n `<Storyboard src=".assets/<card id>/storyboard.json" label="<wireframes>" />` and\n `<Storyboard src=".assets/<card id>/samples.json" label="<samples>" />`, each alone in its\n paragraph.\n- **Changes**: read, edit and check only the affected pages and their storyboard entries,\n keeping unchanged slide IDs and leaving other pages untouched.\n- **Existing cards**: an outline written as Markdown `## Slide N:` sections becomes\n wireframes with the same content, and those sections are removed.\n\n## Memory\n\nKeep `docs/kanban/memory/agents/deck-planner/feedback.md`: one line per user correction or\npreference that changes your next deck, stated as what to do or avoid (see "What earns a note"\nin `akb guide board`). Read it before writing; the current card wins. Merge duplicates and\nrewrite overturned lines in place.\n';
89882
90123
 
89883
90124
  // ../cli/src/agents/deck-planner/references/slides.example.json
89884
90125
  var slides_example_default2 = '{\n "version": 1,\n "size": { "width": 1920, "height": 1080 },\n "slides": [\n {\n "id": "cover",\n "title": "\u7528\u770B\u677F\u4EA4\u4ED8 AI \u5DE5\u4F5C",\n "notes": "\u5F00\u573A\u53EA\u8BF4\u4E00\u4EF6\u4E8B\uFF1A\u9700\u6C42\u600E\u6837\u4E0D\u5728\u5BF9\u8BDD\u91CC\u4E22\u5931\u3002",\n "layout": "\u5C01\u9762",\n "assets": [".assets/969/logo.svg"],\n "preview": { "src": ".assets/969/pages/cover.html", "alt": "\u5C01\u9762\uFF1A\u6807\u9898\u4E0E\u526F\u6807\u9898\u5C45\u4E2D" }\n },\n {\n "id": "results",\n "title": "\u4E09\u4E2A\u6708\u4EA4\u4ED8 212 \u5F20\u5361\u7247",\n "layout": "\u5927\u6570\u5B57",\n "assets": [],\n "preview": { "src": ".assets/969/pages/results.tsx", "alt": "\u5927\u6570\u5B57 212\uFF0C\u4E0B\u65B9\u4E00\u884C\u8BF4\u660E" }\n }\n ]\n}\n';
@@ -90071,10 +90312,10 @@ process.stdout.write(\`\${out}: \${count} slide(s), one image each\\n\`)
90071
90312
  `;
90072
90313
 
90073
90314
  // ../cli/src/agents/illustrator/AGENT.md
90074
- var AGENT_default14 = "---\nname: illustrator\ndescription: Makes the images and animations a card needs.\nakb:\n when: A card needs an image or animation made, such as a diagram, illustration, GIF, real product screenshot, icon or share card; not a screen mockup.\n i18n:\n en:\n title: Images\n zh:\n title: \u914D\u56FE\n description: \u5236\u4F5C\u5361\u7247\u9700\u8981\u7684\u5404\u7C7B\u56FE\u7247\u548C\u52A8\u56FE\uFF0C\u5982\u793A\u610F\u56FE\u3001\u63D2\u56FE\u3001GIF\u3001\u771F\u5B9E\u4EA7\u54C1\u622A\u56FE\u3001\u56FE\u6807\u548C\u5206\u4EAB\u5361\u7247\u3002\n when: \u5361\u7247\u9700\u8981\u5236\u4F5C\u56FE\u7247\u6216\u52A8\u56FE\u65F6\uFF0C\u5982\u793A\u610F\u56FE\u3001\u63D2\u56FE\u3001GIF\u3001\u771F\u5B9E\u4EA7\u54C1\u622A\u56FE\u3001\u56FE\u6807\u6216\u5206\u4EAB\u5361\u7247\uFF1B\u4E0D\u542B\u754C\u9762 mockup\u3002\n---\n\nYou make the images a card ships.\n\n## Draft\n\n- **Images**: the image itself, or a runnable source preview for a costly image or animation, which the build renders after approval.\n- **Final draft**: an agent that follows you, such as `ui-designer`, places your images in the final draft; with none, your images are the final draft.\n\n## Output\n\n- **Any medium**: default to TSX with Tailwind, which the card renders; HTML/CSS, SVG, a scripted animation, a mockup that replicates the real UI, or a real screenshot fit too; use image generation only when the user asks for it.\n- **Script**: TSX runs CSS animation. Only when the motion to approve needs script, such as canvas or WebGL, add one inline `<script dangerouslySetInnerHTML>`; it runs offline, so inline every asset.\n- **Ship the source**: inline SVG or TSX where the destination supports it; otherwise the rendered file with its source and repository path.\n- **Animations**: render a muted, looping H.264 MP4 with a first-frame poster image, embedded with `<Asset ... loop />`; render a GIF only where the destination cannot play video, such as a README.\n- **Just the image**: above `<!-- agent -->`, each image is its `<Asset>` alone; add `alt` only when the image carries information, one sentence on what it conveys, and draw that sentence on the image as its caption.\n- **Look**: colours and fonts from the app's `design.md`.\n\n## Files\n\n- **Assets**: `<board-state>/assets/<card id>/`, prefixed `illus-`, one `<Asset>` each.\n- **Reruns**: replace only your own files; keep other agents' assets.\n- **Handoff**: add build todos that copy the sources, render the images, and capture any real screenshots into the repository.\n\n## Memory\n\n- **`style.md`**: illustration style and corrections, one line each.\n";
90315
+ var AGENT_default15 = "---\nname: illustrator\ndescription: Makes the images and animations a card needs.\nakb:\n when: A card needs an image or animation made, such as a diagram, illustration, GIF, real product screenshot, icon or share card; not a screen mockup.\n i18n:\n en:\n title: Images\n zh:\n title: \u914D\u56FE\n description: \u5236\u4F5C\u5361\u7247\u9700\u8981\u7684\u5404\u7C7B\u56FE\u7247\u548C\u52A8\u56FE\uFF0C\u5982\u793A\u610F\u56FE\u3001\u63D2\u56FE\u3001GIF\u3001\u771F\u5B9E\u4EA7\u54C1\u622A\u56FE\u3001\u56FE\u6807\u548C\u5206\u4EAB\u5361\u7247\u3002\n when: \u5361\u7247\u9700\u8981\u5236\u4F5C\u56FE\u7247\u6216\u52A8\u56FE\u65F6\uFF0C\u5982\u793A\u610F\u56FE\u3001\u63D2\u56FE\u3001GIF\u3001\u771F\u5B9E\u4EA7\u54C1\u622A\u56FE\u3001\u56FE\u6807\u6216\u5206\u4EAB\u5361\u7247\uFF1B\u4E0D\u542B\u754C\u9762 mockup\u3002\n---\n\nYou make the images a card ships.\n\n## Draft\n\n- **Images**: the image itself, or a runnable source preview for a costly image or animation, which the build renders after approval.\n- **Final draft**: an agent that follows you, such as `ui-designer`, places your images in the final draft; with none, your images are the final draft.\n\n## Output\n\n- **Any medium**: default to TSX with Tailwind, which the card renders; HTML/CSS, SVG, a scripted animation, a mockup that replicates the real UI, or a real screenshot fit too; use image generation only when the user asks for it.\n- **Script**: TSX runs CSS animation. Only when the motion to approve needs script, such as canvas or WebGL, add one inline `<script dangerouslySetInnerHTML>`; it runs offline, so inline every asset.\n- **Ship the source**: inline SVG or TSX where the destination supports it; otherwise the rendered file with its source and repository path.\n- **Animations**: render a muted, looping H.264 MP4 with a first-frame poster image, embedded with `<Asset ... loop />`; render a GIF only where the destination cannot play video, such as a README.\n- **Just the image**: above `<!-- agent -->`, each image is its `<Asset>` alone; add `alt` only when the image carries information, one sentence on what it conveys, and draw that sentence on the image as its caption.\n- **Look**: colours and fonts from the app's `design.md`.\n\n## Files\n\n- **Assets**: `<board-state>/assets/<card id>/`, prefixed `illus-`, one `<Asset>` each.\n- **Reruns**: replace only your own files; keep other agents' assets.\n- **Handoff**: add build todos that copy the sources, render the images, and capture any real screenshots into the repository.\n\n## Memory\n\n- **`style.md`**: illustration style and corrections, one line each.\n";
90075
90316
 
90076
90317
  // ../cli/src/agents/hyperframes-editor/AGENT.md
90077
- var AGENT_default15 = "---\nname: hyperframes-editor\ndescription: Produces and checks a product video from its script, using the production method that suits it.\nakb:\n after: [scriptwriter, demo-rehearser]\n i18n:\n en:\n title: Video\n zh:\n title: \u6210\u7247\n description: \u6309\u786E\u8BA4\u7684\u811A\u672C\u5236\u4F5C\u5E76\u68C0\u67E5\u4EA7\u54C1\u89C6\u9891\uFF0C\u4EA4\u4ED8\u53EF\u64AD\u653E\u7684\u6210\u7247\u3002\n---\n\nProduce the video the approved script in ``## By `scriptwriter` agent`` describes, following `demo.md` when present. Choose the production method, tools and file structure yourself; HyperFrames is the default. Continue usable projects and previews on existing cards.\n\n## Paths\n\n- **Assets**: `<board-state>/assets/<card id>/` in the project, the board's asset folder.\n- **Video**: `<short-name>.mp4` in the same folder, `<short-name>` a lowercase slug of the card title.\n\n## Media\n\nUse the first available source for each file:\n\n1. **Asset folder**: reuse a suitable file.\n2. **Memory**: copy a suitable asset from `assets.md`.\n3. **Repository**: copy an existing asset.\n4. **Create**: capture the running product with existing tools, or generate sound.\n5. **Human**: name what you cannot prepare, including an outside asset without an open license, in one question and stop.\n\n- **Recorder**: obtain your bundled `record.mjs` with `akb raw agent-file hyperframes-editor record.mjs` and keep it in the project. Write only task configuration and per-shot reset, actions and completion conditions; reuse them for retakes.\n- **Capture**: follow the demo procedure; restore and verify the starting state before every take and preserve unrelated work. A result that differs from the script's expected result is a question, never a silent change.\n- **Narration**: generate it from the exact scripted lines with the approved voice: `akb cloud tts --tier premium` for a hosted voice unless the user asks for cheaper, local Kokoro for a Kokoro ID, `af_heart` / `zf_xiaobei` when the source names none; name the tier in your section. Never switch voice, tier or service silently; regenerate only narration whose voice changed. For a human voice, reuse the supplied recording or ask for it.\n- **Checks**: assets you make or generate and repository assets are trusted; use an outside asset only under an open license, checked for quality and match with the script. Remove secrets, personal data and private paths.\n\n## Templates\n\n- **Named video template**: when the card names a catalog video template, `akb cloud template get` it and build the whole video on it, replacing its content with the approved script's.\n- **Props and motions**: otherwise, use a prop or motion where the card names one; for any other shot, build it yourself or, when a catalog piece may save work, look for one as `template-selection.md` says.\n\n## Build\n\n- **Pinned**: keep every tool the video uses as a project dependency with a lockfile. Name an unavailable system dependency in one question and stop.\n- **Real product**: show real product interfaces; never fabricate one.\n- **Previews**: preview as you need; they need no user approval.\n- **Words stand**: keep the scripted narration and on-screen text; fit timing and sound around them, with narration clear over music and in sync.\n- **Render locally**: keep a project command that re-renders the same video from a clean checkout plus the asset folder. Do not use hosted rendering. Report a failed render without claiming delivery.\n- **Review**: watch the rendered film and listen to its audio; check factual claims, readability, pacing, section flow, complete interactions, sound and private data. Fix defects, re-render and recheck; report checks you could not run.\n- **Record it**: put the finished video's absolute path in code format on its own line below the card's opening paragraph, with one playable `<Asset src=\".assets/<card id>/<short-name>.mp4\" label=\"...\" />` beneath it, updating both in place. Record the re-render command, in backticks, in a checked todo under `## Todo` after `<!-- agent -->`.\n\n## Rules\n\n- **No code bar**: tests, diff size and code review do not apply.\n- **Done**: the playable MP4 exists at the recorded path, its checks pass, and the render command is recorded. A run that renders nothing has not delivered.\n\n## Memory\n\nKeep `assets.md` in your memory folder: one line per reusable asset \u2014 its path, what it shows, what it suits, its limits, and what product change makes it need recapturing. Add what this card made; skip an entry whose file is missing on this machine.\n\nKeep `feedback.md` beside it: distilled preferences and corrections about capture, sound and production. Read relevant guidance before building; the current script wins. Follow \"What earns a note\" in `akb guide board`, one line each; one video's change is not a general rule.\n";
90318
+ var AGENT_default16 = "---\nname: hyperframes-editor\ndescription: Produces and checks a product video from its script, using the production method that suits it.\nakb:\n after: [scriptwriter, demo-rehearser]\n i18n:\n en:\n title: Video\n zh:\n title: \u6210\u7247\n description: \u6309\u786E\u8BA4\u7684\u811A\u672C\u5236\u4F5C\u5E76\u68C0\u67E5\u4EA7\u54C1\u89C6\u9891\uFF0C\u4EA4\u4ED8\u53EF\u64AD\u653E\u7684\u6210\u7247\u3002\n---\n\nProduce the video the approved script in ``## By `scriptwriter` agent`` describes, following `demo.md` when present. Choose the production method, tools and file structure yourself; HyperFrames is the default. Continue usable projects and previews on existing cards.\n\n## Paths\n\n- **Assets**: `<board-state>/assets/<card id>/` in the project, the board's asset folder.\n- **Video**: `<short-name>.mp4` in the same folder, `<short-name>` a lowercase slug of the card title.\n\n## Media\n\nUse the first available source for each file:\n\n1. **Asset folder**: reuse a suitable file.\n2. **Memory**: copy a suitable asset from `assets.md`.\n3. **Repository**: copy an existing asset.\n4. **Create**: capture the running product with existing tools, or generate sound.\n5. **Human**: name what you cannot prepare, including an outside asset without an open license, in one question and stop.\n\n- **Recorder**: obtain your bundled `record.mjs` with `akb raw agent-file hyperframes-editor record.mjs` and keep it in the project. Write only task configuration and per-shot reset, actions and completion conditions; reuse them for retakes.\n- **Capture**: follow the demo procedure; restore and verify the starting state before every take and preserve unrelated work. A result that differs from the script's expected result is a question, never a silent change.\n- **Narration**: generate it from the exact scripted lines with the approved voice: `akb cloud tts --tier premium` for a hosted voice unless the user asks for cheaper, local Kokoro for a Kokoro ID, `af_heart` / `zf_xiaobei` when the source names none; name the tier in your section. Never switch voice, tier or service silently; regenerate only narration whose voice changed. For a human voice, reuse the supplied recording or ask for it.\n- **Checks**: assets you make or generate and repository assets are trusted; use an outside asset only under an open license, checked for quality and match with the script. Remove secrets, personal data and private paths.\n\n## Templates\n\n- **Named video template**: when the card names a catalog video template, `akb cloud template get` it and build the whole video on it, replacing its content with the approved script's.\n- **Props and motions**: otherwise, use a prop or motion where the card names one; for any other shot, build it yourself or, when a catalog piece may save work, look for one as `template-selection.md` says.\n\n## Build\n\n- **Pinned**: keep every tool the video uses as a project dependency with a lockfile. Name an unavailable system dependency in one question and stop.\n- **Real product**: show real product interfaces; never fabricate one.\n- **Previews**: preview as you need; they need no user approval.\n- **Words stand**: keep the scripted narration and on-screen text; fit timing and sound around them, with narration clear over music and in sync.\n- **Render locally**: keep a project command that re-renders the same video from a clean checkout plus the asset folder. Do not use hosted rendering. Report a failed render without claiming delivery.\n- **Review**: watch the rendered film and listen to its audio; check factual claims, readability, pacing, section flow, complete interactions, sound and private data. Fix defects, re-render and recheck; report checks you could not run.\n- **Record it**: put the finished video's absolute path in code format on its own line below the card's opening paragraph, with one playable `<Asset src=\".assets/<card id>/<short-name>.mp4\" label=\"...\" />` beneath it, updating both in place. Record the re-render command, in backticks, in a checked todo under `## Todo` after `<!-- agent -->`.\n\n## Rules\n\n- **No code bar**: tests, diff size and code review do not apply.\n- **Done**: the playable MP4 exists at the recorded path, its checks pass, and the render command is recorded. A run that renders nothing has not delivered.\n\n## Memory\n\nKeep `assets.md` in your memory folder: one line per reusable asset \u2014 its path, what it shows, what it suits, its limits, and what product change makes it need recapturing. Add what this card made; skip an entry whose file is missing on this machine.\n\nKeep `feedback.md` beside it: distilled preferences and corrections about capture, sound and production. Read relevant guidance before building; the current script wins. Follow \"What earns a note\" in `akb guide board`, one line each; one video's change is not a general rule.\n";
90078
90319
 
90079
90320
  // ../cli/src/agents/hyperframes-editor/record.mjs
90080
90321
  var record_default = "#!/usr/bin/env node\n// Record scripted product shots over the Chrome DevTools Protocol, one video file per shot.\n//\n// node record.mjs every shot the config names, resuming what is unfinished\n// node record.mjs S3 S5 only these shots\n// node record.mjs --force retake the chosen shots even when they are up to date\n// node record.mjs --list what the config names, and what is already recorded\n// node record.mjs --init write starter `record.config.mjs` and `shots/S1.mjs`\n// node record.mjs --json print the ledger entries this run touched\n//\n// Runtime: Node 22 or later (global `WebSocket`) with `ffmpeg` and `ffprobe` on PATH. Nothing\n// is installed. Save it from the agent with\n// `akb raw agent-file hyperframes-editor record.mjs > record.mjs`, and keep it in the project.\n//\n// You write two kinds of file beside it and nothing else:\n//\n// record.config.mjs the environment \u2014 browser address, base URL, viewport, fps, folders\n// shots/<id>.mjs one shot \u2014 how to reset, how to check the starting state, the take\n//\n// Connecting, the drawn cursor, click rings, typing, frame sampling, retiming, encoding,\n// checking and the ledger live here. A rerun edits its own shot file; it never rewrites this.\n//\n// ---- record.config.mjs -----------------------------------------------------\n//\n// export default {\n// browser: 'http://127.0.0.1:9461', // a Chrome started with --remote-debugging-port\n// baseUrl: 'http://127.0.0.1:4861', // what `ctx.go('/16')` is relative to\n// viewport: { width: 1440, height: 810, dpr: 2 },\n// fps: 30, // the encoded rate; sampling is measured, not assumed\n// ready: 'document.readyState === \"complete\"', // when a navigation is done\n// settle: 1200, // ms to wait after that, before the cursor goes on\n// shots: 'shots', // folder of shot files, one per stable shot ID\n// out: '..', // where finished video files land\n// work: '.record', // frames, timestamps, temporary files, the ledger\n// }\n//\n// Every field has the default shown above; a config may set only what differs.\n//\n// ---- shots/<id>.mjs --------------------------------------------------------\n//\n// export default {\n// output: '01-create-goal.mp4', // the file name, inside `out`\n// inputs: ['fixtures/board.json'], // extra files whose change invalidates the take\n// expect: { seconds: [6, 12], fps: 20 }, // size defaults to the viewport in real pixels\n// async reset(ctx) { await ctx.go('/') }, // restore the baseline \u2014 never recorded\n// async check(ctx) { await ctx.until(...) }, // the starting state \u2014 throw to fail\n// async take(ctx) { ... }, // the take; recording spans exactly this function\n// }\n//\n// `reset` and `check` run outside the recording, so navigation and pane setup belong there.\n// Only `take` is filmed. `output`, `inputs` and `expect` are optional; `take` is not.\n//\n// ---- what a shot is given ---------------------------------------------------\n//\n// ctx.go(route) navigate, wait for `ready`, settle, draw the cursor\n// ctx.ev(expr) evaluate an expression in the page and return its value\n// ctx.until(expr, ms) poll until the expression is truthy, or throw\n// ctx.box(expr) the element's box \u2014 {x, y, w, h, l, t}, centre first\n// ctx.move(x, y, wait) glide the drawn cursor and the real pointer there\n// ctx.click(x, y, wait) move, ring, and dispatch a real click\n// ctx.clickEl(expr, wait) the same, on an element's centre\n// ctx.domClick(expr, wait) move and ring, then call the element's own `click()`\n// ctx.type(text, delay) type it a key at a time, the way a person does\n// ctx.byText(text, selector) an expression for the visible element with that exact text\n// ctx.shot(file) save a PNG \u2014 reference frames for `check` to compare against\n// ctx.cursor() redraw the cursor after a navigation the shot made itself\n// ctx.sleep(ms)\n// ctx.mark(seconds) pin this moment to that time in the finished file\n// ctx.beforeFrame(fn) run `fn` before every sample \u2014 hold a scroll position still\n// ctx.send(method, params) any CDP call, for whatever this list does not cover\n//\n// `mark` is how a take whose real pauses differ from the scripted ones still lands on the\n// script's timeline: mark the scripted time at each beat and the frames are retimed onto it.\n// The start of the take is time zero without being marked. Without any mark the file keeps\n// the take's own timing.\n//\n// ---- what it guarantees ------------------------------------------------------\n//\n// Frames are kept beside the video with the times they were really taken at, retiming or\n// not. Sampling rate is measured over how long the take really ran and reported: an encode\n// at 30 fps says nothing about how fast the page was actually sampled, and neither does a\n// retimed file, so `expect.fps` is checked against that measured rate alone. A take\n// is written to a temporary file and replaces the output only once its checks pass; a failed\n// shot leaves the previous file alone and is never recorded as this run's success. A failure\n// stops that shot only \u2014 the rest still run, each from its own fresh tab.\n\nimport { execFileSync } from 'node:child_process'\nimport { createHash } from 'node:crypto'\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport { pathToFileURL } from 'node:url'\n\nconst VERSION = 1\n\nconst DEFAULTS = {\n browser: 'http://127.0.0.1:9461',\n baseUrl: 'http://127.0.0.1:4861',\n viewport: { width: 1440, height: 810, dpr: 2 },\n fps: 30,\n ready: 'document.readyState === \"complete\"',\n settle: 1200,\n shots: 'shots',\n out: '..',\n work: '.record',\n}\n\nconst CURSOR = `(()=>{\n if(document.querySelector('#akb-capture-cursor')) return;\n const style=document.createElement('style');\n style.textContent='#akb-capture-cursor{position:fixed;z-index:2147483647;width:17px;height:25px;pointer-events:none;left:0;top:0;transition:left .45s cubic-bezier(.4,0,.2,1),top .45s cubic-bezier(.4,0,.2,1);filter:drop-shadow(0 1px 1.5px rgba(0,0,0,.35))}#akb-capture-cursor svg{display:block;overflow:visible}.akb-click-ring{position:fixed;z-index:2147483646;width:54px;height:54px;border:4px solid #dd4f1e;border-radius:50%;pointer-events:none;transform:translate(-50%,-50%) scale(.35);opacity:1;animation:akb-ring .3s ease-out forwards}@keyframes akb-ring{to{transform:translate(-50%,-50%) scale(1);opacity:0}}';\n document.head.append(style);\n const cursor=document.createElement('div');cursor.id='akb-capture-cursor';\n // The macOS arrow, its tip on (0,0) so it lands on the click; the stroke is drawn under the fill, outside the shape.\n cursor.innerHTML='<svg width=\"17\" height=\"25\" viewBox=\"0 0 17 25\"><path d=\"M0 0V19L4.4 14.9L7.3 21.6L10.4 20.3L7.6 13.8H13.3Z\" fill=\"#000\" stroke=\"#fff\" stroke-width=\"2\" stroke-linejoin=\"round\" paint-order=\"stroke\"/></svg>';\n document.body.append(cursor);\n})()`\n\n// ---- odds and ends ---------------------------------------------------------\n\nconst round = (n) => Math.round(n * 100) / 100\n\nfunction die(said, code = 1) {\n process.stderr.write(`record: ${said}\\n`)\n process.exit(code)\n}\n\n/** `S2` before `S10`, and any other name alphabetically. */\nfunction byShotOrder(a, b) {\n const na = /^S(\\d+)$/i.exec(a)\n const nb = /^S(\\d+)$/i.exec(b)\n if (na && nb) return Number(na[1]) - Number(nb[1])\n return a.localeCompare(b)\n}\n\nconst digest = (file) => {\n try {\n return createHash('sha1').update(fs.readFileSync(file)).digest('hex').slice(0, 12)\n } catch {\n return null\n }\n}\n\n// ---- the arguments ---------------------------------------------------------\n\nconst argv = process.argv.slice(2)\nconst flag = (name) => argv.includes(`--${name}`)\nconst option = (name) => argv.find((a) => a.startsWith(`--${name}=`))?.slice(name.length + 3)\nconst chosen = argv.filter((a) => !a.startsWith('-'))\n\nconst configPath = path.resolve(option('config') ?? 'record.config.mjs')\nconst here = path.dirname(configPath)\nconst rel = (file) => path.relative(here, file) || path.basename(file)\n\nif (flag('help')) {\n const lines = fs.readFileSync(new URL(import.meta.url), 'utf8').split('\\n').slice(1)\n process.stdout.write(`${lines.slice(0, lines.findIndex((l) => !l.startsWith('//'))).join('\\n')}\\n`)\n process.exit(0)\n}\n\nif (flag('init')) {\n scaffold()\n process.exit(0)\n}\n\n// ---- the config and the shots ----------------------------------------------\n\nif (!fs.existsSync(configPath)) die(`no ${rel(configPath)} \u2014 write one, or run \\`node record.mjs --init\\``, 2)\n\nconst loaded = new Map()\n\nasync function load(file) {\n const mod = (await import(`${pathToFileURL(file).href}?v=${Date.now()}`)).default ?? {}\n loaded.set(path.basename(file, '.mjs'), mod)\n return mod\n}\n\nconst written = await load(configPath)\nconst config = { ...DEFAULTS, ...written, viewport: { ...DEFAULTS.viewport, ...(written.viewport ?? {}) } }\nconst shotsDir = path.resolve(here, config.shots)\nconst outDir = path.resolve(here, config.out)\nconst workDir = path.resolve(here, config.work)\n\nconst available = fs.existsSync(shotsDir)\n ? fs.readdirSync(shotsDir).filter((f) => f.endsWith('.mjs')).map((f) => f.slice(0, -4)).sort(byShotOrder)\n : []\nif (!available.length) die(`no shot files in ${rel(shotsDir)} \u2014 one \\`<id>.mjs\\` per shot`, 2)\n\nconst unknown = chosen.filter((id) => !available.includes(id))\nif (unknown.length) die(`no shot file for ${unknown.map((id) => `\\`${id}\\``).join(', ')}. This project has: ${available.join(', ')}`, 2)\nconst wanted = chosen.length ? available.filter((id) => chosen.includes(id)) : available\n\n// ---- the ledger ------------------------------------------------------------\n\nconst ledgerPath = path.join(workDir, 'ledger.json')\n\nfunction readLedger() {\n try {\n const read = JSON.parse(fs.readFileSync(ledgerPath, 'utf8'))\n if (read?.recorder === VERSION && read.shots) return read\n } catch {}\n return { recorder: VERSION, shots: {} }\n}\n\nconst ledger = readLedger()\nconst writeLedger = () => {\n fs.mkdirSync(workDir, { recursive: true })\n fs.writeFileSync(ledgerPath, `${JSON.stringify(ledger, null, 2)}\\n`)\n}\n\n/** What a take depended on: the config, the shot file, whatever the shot names, and the\n * file it produced \u2014 a missing or edited output is a take that has to happen again. */\nfunction inputsOf(id, output) {\n const shot = loaded.get(id)\n const files = [configPath, path.join(shotsDir, `${id}.mjs`), ...(shot?.inputs ?? []).map((f) => path.resolve(here, f))]\n const out = {}\n for (const file of files) out[rel(file)] = digest(file)\n if (output) out[`out:${output}`] = digest(path.join(outDir, output))\n return out\n}\n\n/** True when nothing this take read, and nothing it wrote, has changed since. */\nfunction fresh(id, entry) {\n if (!entry?.inputs) return false\n const now = inputsOf(id, entry.output)\n const keys = new Set([...Object.keys(now), ...Object.keys(entry.inputs)])\n return [...keys].every((key) => now[key] !== null && now[key] === entry.inputs[key])\n}\n\nif (flag('list')) {\n for (const id of available) {\n await load(path.join(shotsDir, `${id}.mjs`))\n const entry = ledger.shots[id]\n const state = entry?.status === 'ok' ? (fresh(id, entry) ? 'ready' : 'stale') : (entry?.status ?? 'never recorded')\n process.stdout.write(`${id.padEnd(6)} ${state.padEnd(14)} ${entry?.output ?? ''}\\n`)\n }\n process.exit(0)\n}\n\n// ---- the checks ------------------------------------------------------------\n\nconst check = (what, ok, said) => ({ what, ok, said })\n\n/** What the finished file is, measured \u2014 never what the encode was asked for. */\nfunction inspect(file, take, expect) {\n const probe = JSON.parse(\n execFileSync('ffprobe', ['-v', 'error', '-select_streams', 'v:0', '-show_entries', 'stream=width,height:format=duration', '-of', 'json', file], { encoding: 'utf8' }),\n )\n const stream = probe.streams?.[0] ?? {}\n const seconds = Number(probe.format?.duration ?? 0)\n const sampled = take.frames.length / take.elapsed\n const size = expect.size ?? [config.viewport.width * config.viewport.dpr, config.viewport.height * config.viewport.dpr]\n const floor = expect.fps ?? 20\n const checks = [\n check('size', stream.width === size[0] && stream.height === size[1], `${stream.width}x${stream.height}, wanted ${size[0]}x${size[1]}`),\n check('sampled fps', sampled >= floor, `sampled ${round(sampled)} fps, wanted at least ${floor} \u2014 encoding at ${config.fps} fps does not make up the difference`),\n ]\n if (expect.seconds) {\n const [min, max] = expect.seconds\n checks.push(check('duration', seconds >= min && seconds <= max, `${round(seconds)}s, wanted ${min}\u2013${max}s`))\n }\n return checks\n}\n\n// ---- frames ----------------------------------------------------------------\n\n/** Put every frame on a timeline. With marks, the take's beats are stretched onto the\n * scripted times; without them the take keeps its own clock. Every frame keeps `at`, when\n * it was really taken \u2014 retiming moves the file's clock, never the record of the sampling.\n * `elapsed` is how long the take really ran, which is what the sampling rate is counted on. */\nfunction retime(frames, began, ended, marks) {\n const elapsed = Math.max(0.001, ended - began)\n const real = frames.map((f) => ({ data: f.data, at: f.at - began }))\n if (marks.length < 2) return { frames: real.map((f) => ({ ...f, t: f.at })), duration: elapsed, elapsed }\n const pins = marks.map((m) => ({ source: m.source - began, target: m.target })).sort((a, b) => a.source - b.source)\n // The start of the take is a pin too, so what happens before the first mark is stretched\n // onto the timeline rather than piled onto its first instant.\n if (pins[0].source > 0) pins.unshift({ source: 0, target: 0 })\n const duration = pins[pins.length - 1].target\n const at = (t) => {\n if (t <= pins[0].source) return pins[0].target\n for (let i = 0; i < pins.length - 1; i += 1) {\n const a = pins[i]\n const b = pins[i + 1]\n if (t >= a.source && t < b.source) return a.target + ((t - a.source) / (b.source - a.source)) * (b.target - a.target)\n }\n return duration\n }\n return { frames: real.map((f) => ({ data: f.data, at: f.at, t: Math.min(duration, at(f.at)) })), duration, elapsed }\n}\n\n/** The raw take, kept: every frame as it came off the page, and when it was taken. */\nfunction writeFrames(dir, take) {\n fs.rmSync(dir, { recursive: true, force: true })\n fs.mkdirSync(dir, { recursive: true })\n const lines = []\n take.frames.forEach((frame, index) => {\n const name = `${String(index).padStart(6, '0')}.jpg`\n fs.writeFileSync(path.join(dir, name), Buffer.from(frame.data, 'base64'))\n const hold = (take.frames[index + 1]?.t ?? take.duration) - frame.t\n if (hold > 0) lines.push(`file '${name}'`, 'option framerate 1000', `duration ${hold.toFixed(6)}`)\n })\n const last = `${String(take.frames.length - 1).padStart(6, '0')}.jpg`\n lines.push(`file '${last}'`, 'option framerate 1000', 'duration 0.001')\n fs.writeFileSync(path.join(dir, 'frames.ffconcat'), `${lines.join('\\n')}\\n`)\n fs.writeFileSync(path.join(dir, 'timestamps.json'), JSON.stringify(take.frames.map((f) => round(f.at))))\n fs.writeFileSync(\n path.join(dir, 'capture.json'),\n JSON.stringify(\n { recorder: VERSION, frames: take.frames.length, duration: round(take.duration), sampledSeconds: round(take.elapsed), sampledFps: round(take.frames.length / take.elapsed), viewport: config.viewport },\n null,\n 2,\n ),\n )\n}\n\nfunction encode(framesDir, out, fps, duration) {\n execFileSync('ffmpeg', [\n '-y', '-hide_banner', '-loglevel', 'error',\n '-f', 'concat', '-safe', '0', '-i', path.join(framesDir, 'frames.ffconcat'),\n '-vf', `fps=${fps}`,\n '-t', duration.toFixed(3),\n '-c:v', 'libx264', '-preset', 'fast', '-crf', '18', '-pix_fmt', 'yuv420p', '-an',\n out,\n ])\n}\n\n// ---- the browser -----------------------------------------------------------\n\n/** One connection to the browser for the whole run, and a fresh tab for each shot \u2014 a shot\n * that left the page dirty cannot hand that state to the next one. */\nasync function openBrowser(endpoint) {\n const version = await fetch(`${endpoint}/json/version`).then((r) => r.json()).catch(() => null)\n if (!version) die(`no browser answering at ${endpoint} \u2014 start Chrome with \\`--remote-debugging-port=${new URL(endpoint).port}\\``, 2)\n return {\n async open() {\n const target = await (await fetch(`${endpoint}/json/new?about:blank`, { method: 'PUT' })).json()\n return attach(endpoint, target)\n },\n }\n}\n\nasync function attach(endpoint, target) {\n const ws = new WebSocket(target.webSocketDebuggerUrl)\n await new Promise((resolve, reject) => {\n ws.onopen = resolve\n ws.onerror = () => reject(new Error('could not attach to the tab'))\n })\n let next = 0\n const pending = new Map()\n ws.onmessage = (e) => {\n const message = JSON.parse(e.data)\n if (message.id && pending.has(message.id)) {\n pending.get(message.id)(message)\n pending.delete(message.id)\n }\n }\n const send = (method, params = {}) =>\n new Promise((resolve) => {\n const id = (next += 1)\n pending.set(id, resolve)\n ws.send(JSON.stringify({ id, method, params }))\n })\n\n const { width, height, dpr } = config.viewport\n await send('Page.enable')\n await send('Runtime.enable')\n await send('Emulation.setDeviceMetricsOverride', { width, height, deviceScaleFactor: dpr, mobile: false })\n\n const sleep = (ms) => new Promise((r) => setTimeout(r, ms))\n const ev = async (expr) => {\n const m = await send('Runtime.evaluate', { expression: expr, awaitPromise: true, returnByValue: true })\n const thrown = m.result?.exceptionDetails\n if (thrown) throw new Error(String(thrown.exception?.description ?? thrown.text ?? JSON.stringify(thrown)).slice(0, 400))\n return m.result?.result?.value\n }\n const until = async (expr, ms = 20000) => {\n const started = Date.now()\n while (Date.now() - started < ms) {\n try {\n if (await ev(expr)) return true\n } catch {}\n await sleep(250)\n }\n throw new Error(`timed out after ${ms}ms waiting for ${expr}`)\n }\n const box = async (expr) =>\n ev(`(()=>{const e=(${expr});if(!e)return null;const r=e.getBoundingClientRect();return {x:r.x+r.width/2,y:r.y+r.height/2,w:r.width,h:r.height,l:r.x,t:r.y}})()`)\n const need = async (expr) => {\n const b = await box(expr)\n if (!b) throw new Error(`no element: ${expr}`)\n return b\n }\n\n const ring = (x, y) =>\n ev(`(()=>{const r=document.createElement('div');r.className='akb-click-ring';r.style.left='${x}px';r.style.top='${y}px';document.body.append(r);setTimeout(()=>r.remove(),400)})()`)\n // The drawn cursor glides for .45s; a click waits for it to land so the ring opens under the tip.\n let at = null\n const cursor = () => {\n at = null\n return ev(CURSOR)\n }\n const land = async (x, y) => {\n const same = at?.x === x && at?.y === y\n const left = same ? at.t + 450 - Date.now() : 450\n if (!same) await move(x, y, 0)\n if (left > 0) await sleep(left)\n }\n const move = async (x, y, wait = 500) => {\n at = { x, y, t: Date.now() }\n await ev(`(()=>{const e=document.querySelector('#akb-capture-cursor');if(e){e.style.left='${x}px';e.style.top='${y}px'}})()`)\n await send('Input.dispatchMouseEvent', { type: 'mouseMoved', x, y })\n if (wait) await sleep(wait)\n }\n const click = async (x, y, wait = 400) => {\n await land(x, y)\n await ring(x, y)\n await send('Input.dispatchMouseEvent', { type: 'mousePressed', x, y, button: 'left', clickCount: 1 })\n await send('Input.dispatchMouseEvent', { type: 'mouseReleased', x, y, button: 'left', clickCount: 1 })\n if (wait) await sleep(wait)\n }\n\n let frames = []\n let sampling = false\n let loop = null\n let hooks = []\n const marks = []\n\n const ctx = {\n config,\n send,\n ev,\n until,\n box,\n sleep,\n cursor,\n byText: (text, selector = 'button,a,[role=button],[role=menuitem],[role=tab]') =>\n `[...document.querySelectorAll('${selector}')].filter(e=>e.offsetParent!==null&&e.innerText.trim()===${JSON.stringify(text)}).pop()`,\n async go(route = '/') {\n await send('Page.navigate', { url: /^[a-z][a-z0-9+.-]*:/i.test(route) ? route : `${config.baseUrl}${route}` })\n await sleep(400)\n await until(config.ready)\n await sleep(config.settle)\n await cursor()\n await move(config.viewport.width / 2, config.viewport.height / 2, 0)\n },\n move,\n click,\n async clickEl(expr, wait = 500) {\n const b = await need(expr)\n await click(b.x, b.y, wait)\n return b\n },\n async domClick(expr, wait = 500) {\n const b = await need(expr)\n await land(b.x, b.y)\n await ring(b.x, b.y)\n await ev(`(${expr}).click()`)\n if (wait) await sleep(wait)\n return b\n },\n async type(text, delay = 24) {\n for (const ch of text) {\n await send('Input.dispatchKeyEvent', { type: 'keyDown', key: ch })\n await send('Input.dispatchKeyEvent', { type: 'char', text: ch })\n await send('Input.dispatchKeyEvent', { type: 'keyUp', key: ch })\n await sleep(delay)\n }\n },\n async shot(file) {\n const to = path.resolve(here, file)\n const m = await send('Page.captureScreenshot', { format: 'png' })\n fs.mkdirSync(path.dirname(to), { recursive: true })\n fs.writeFileSync(to, Buffer.from(m.result.data, 'base64'))\n },\n mark: (seconds) => marks.push({ source: Date.now() / 1000, target: seconds }),\n beforeFrame: (fn) => hooks.push(fn),\n }\n\n /** Sample the page for exactly as long as `fn` runs. */\n async function film(fn) {\n frames = []\n marks.length = 0\n hooks = []\n const interval = 1000 / config.fps\n const began = Date.now() / 1000\n let ended = began\n sampling = true\n loop = (async () => {\n while (sampling) {\n const at = Date.now() / 1000\n for (const hook of hooks) {\n try {\n await hook(ctx)\n } catch {}\n }\n const frame = await send('Page.captureScreenshot', { format: 'jpeg', quality: 90, fromSurface: true })\n if (!sampling) break\n frames.push({ data: frame.result.data, at })\n const left = interval - (Date.now() / 1000 - at) * 1000\n if (left > 0) await sleep(left)\n }\n })()\n try {\n await fn()\n } finally {\n ended = Date.now() / 1000\n sampling = false\n await loop\n }\n return retime(frames, began, ended, marks)\n }\n\n const close = async () => {\n sampling = false\n try {\n ws.close()\n } catch {}\n await fetch(`${endpoint}/json/close/${target.id}`).catch(() => {})\n }\n\n return { ctx, film, close }\n}\n\n// ---- one shot --------------------------------------------------------------\n\nconst touched = []\nlet failed = 0\n\n/** One ledger line, kept whichever way the shot went, and echoed as it happens. */\nfunction report(id, status, rest) {\n const entry = { id, status, at: new Date().toISOString(), recorder: VERSION, ...rest, inputs: inputsOf(id, rest.output) }\n ledger.shots[id] = entry\n touched.push(entry)\n if (status === 'ok') {\n process.stdout.write(`${id} \u2014 ${entry.output}, ${entry.seconds}s, ${entry.frames} frames, sampled ${entry.sampledFps} fps\\n`)\n } else {\n failed += 1\n process.stderr.write(`${id} \u2014 FAILED: ${entry.reason}\\n`)\n }\n writeLedger()\n}\n\nasync function record(browser, id, shot) {\n const output = shot.output ?? `${id}.mp4`\n const framesDir = path.join(workDir, `${id}-source`)\n const temp = path.join(workDir, `${id}.tmp.mp4`)\n const page = await browser.open()\n try {\n await step('reset', () => shot.reset?.(page.ctx))\n await step('check', () => shot.check?.(page.ctx))\n const take = await page.film(() => shot.take(page.ctx))\n if (!take.frames.length) throw new Error('no frames were sampled')\n writeFrames(framesDir, take)\n encode(framesDir, temp, config.fps, take.duration)\n const checks = inspect(temp, take, shot.expect ?? {})\n const bad = checks.filter((c) => !c.ok)\n if (bad.length) throw new Error(`checks failed \u2014 ${bad.map((c) => `${c.what}: ${c.said}`).join('; ')}`)\n fs.renameSync(temp, path.join(outDir, output))\n report(id, 'ok', {\n output,\n frames: take.frames.length,\n seconds: round(take.duration),\n sampledFps: round(take.frames.length / take.elapsed),\n checks,\n framesDir: rel(framesDir),\n })\n } catch (error) {\n fs.rmSync(temp, { force: true })\n report(id, 'failed', { output, reason: String(error?.message ?? error) })\n } finally {\n await page.close()\n }\n\n async function step(name, fn) {\n try {\n await fn()\n } catch (error) {\n throw new Error(`${name}: ${error?.message ?? error}`)\n }\n }\n}\n\n// ---- scaffolding -----------------------------------------------------------\n\nfunction write(file, text) {\n if (fs.existsSync(file)) return void process.stdout.write(`${rel(file)} is already here \u2014 left alone\\n`)\n fs.writeFileSync(file, text)\n}\n\nfunction scaffold() {\n const shots = path.resolve(here, DEFAULTS.shots)\n fs.mkdirSync(shots, { recursive: true })\n write(\n configPath,\n `// This task's environment. Anything left out keeps the recorder's default.\nexport default {\n browser: '${DEFAULTS.browser}',\n baseUrl: '${DEFAULTS.baseUrl}',\n viewport: { width: 1440, height: 810, dpr: 2 },\n fps: 30,\n}\n`,\n )\n write(\n path.join(shots, 'S1.mjs'),\n `// S1 \u2014 say here what the shot shows, in the script's words.\nexport default {\n output: '01-<what-it-shows>.mp4',\n expect: { seconds: [4, 12], fps: 20 },\n\n // Restore the baseline and get to the starting frame. Not recorded.\n async reset(ctx) {\n await ctx.go('/')\n },\n\n // Refuse to film the wrong starting state. Throw, and the shot is reported missing.\n async check(ctx) {\n await ctx.until(\\`!!document.querySelector('main')\\`)\n },\n\n // The take. Recording spans exactly this function.\n async take(ctx) {\n await ctx.sleep(800)\n await ctx.domClick(ctx.byText('Create task'), 600)\n await ctx.type('A demo task', 30)\n await ctx.sleep(1500)\n },\n}\n`,\n )\n process.stdout.write(`wrote ${rel(configPath)} and ${rel(path.join(shots, 'S1.mjs'))} \u2014 edit them, then \\`node record.mjs\\`\\n`)\n}\n\n// ---- the run ---------------------------------------------------------------\n\nfs.mkdirSync(workDir, { recursive: true })\nfs.mkdirSync(outDir, { recursive: true })\n\nconst force = flag('force')\nconst browser = await openBrowser(config.browser)\n\nfor (const id of wanted) {\n const shot = await load(path.join(shotsDir, `${id}.mjs`))\n if (typeof shot.take !== 'function') {\n report(id, 'failed', { reason: `${rel(path.join(shotsDir, `${id}.mjs`))} exports no \\`take\\`` })\n continue\n }\n if (!force && ledger.shots[id]?.status === 'ok' && fresh(id, ledger.shots[id])) {\n process.stdout.write(`${id} \u2014 already recorded, unchanged\\n`)\n continue\n }\n await record(browser, id, shot)\n}\n\nwriteLedger()\nif (flag('json')) process.stdout.write(`${JSON.stringify(touched, null, 2)}\\n`)\nprocess.stdout.write(`${wanted.length - failed}/${wanted.length} recorded${failed ? `, ${failed} failed` : ''}\\n`)\nprocess.exit(failed ? 1 : 0)\n";
@@ -90083,19 +90324,19 @@ var record_default = "#!/usr/bin/env node\n// Record scripted product shots over
90083
90324
  var template_selection_default = "# Template selection\n\n- **Search**: run `akb cloud template list-tags --category prop` or `--category motion`, then `akb cloud template search` within the best tag, adding shot keywords when they help.\n- **Use a fit**: take a result whose style and timing fit the shot, and fetch it with `akb cloud template get <name> --to <board-state>/assets/<card id>/<name>/`.\n- **No match**: when nothing fits, or the catalog cannot be reached, build it yourself; never ask the user to pick.\n";
90084
90325
 
90085
90326
  // ../cli/src/agents/prompt-writer/AGENT.md
90086
- var AGENT_default16 = "---\nname: prompt-writer\ndescription: Writes the instruction text a card changes, as a diff per file.\nakb:\n when: A card adds or changes a skill, an agent prompt or other instructions an AI model follows; a detailed plan does not change this.\n memoryFiles:\n workspace: [style.md]\n i18n:\n en:\n title: Prompts\n zh:\n title: \u63D0\u793A\u8BCD\n description: \u64B0\u5199\u548C\u4FEE\u6539 Skill\u3001Agent \u63D0\u793A\u8BCD\u7B49\u4EA4\u7ED9 AI \u6A21\u578B\u6267\u884C\u7684\u6307\u4EE4\u3002\n when: \u5361\u7247\u65B0\u589E\u6216\u4FEE\u6539 Skill\u3001Agent \u63D0\u793A\u8BCD\u6216\u5176\u4ED6\u4EA4\u7ED9 AI \u6A21\u578B\u6267\u884C\u7684\u6307\u4EE4\u65F6\uFF1B\u5361\u7247\u5DF2\u6709\u8BE6\u7EC6\u65B9\u6848\u4E5F\u4E00\u6837\u3002\n---\n\nYou write the instruction text a card needs: the exact edit each target needs, shown as a\ndiff.\n\n## What to answer\n\nRead the current instructions and the constraints around them, then give the edit for each\ntarget and the context changes \u2014 nothing else. Label each target with its file only.\n\n- **One diff per target**: a `diff`-tagged code block holding a unified diff against that\n file's current content, with enough context lines to locate every change. A new file is\n all `+` lines.\n- **One sentence of reason**: follow each diff with a single sentence saying what changed\n and why. No other summary or explanation.\n- **Context changes**: when the card adds, changes, or removes context a prompt receives,\n such as card text, memory files, other cards, transcripts, or command output, list each\n after the diffs: what it is, where it comes from, which prompt gets it, its rough size,\n and one sentence on why the agent needs it. Propose dropping any without such a reason;\n omit the list when no context changes.\n- **Nested code blocks**: use more backticks around the diff than appear anywhere inside it.\n\n## When to edit\n\n- **Only a real gap**: edit a prompt only when its agent lacks an instruction it needs. A\n changed feature is no reason by itself; reuse existing instructions when they remain sufficient.\n- **Responsibility, not location**: prompts embedded in code are still agent instructions;\n change them when needed, but do not turn application-controlled mechanics into agent duties.\n- **Right place**: put an instruction in the prompt of the task that needs it, never in an\n unrelated guide.\n- **Stay in scope**: change only the prompts the card names.\n\n## Writing standard\n\n- **Natural language only**: never write code, pseudocode, or a programmatic wrapper,\n including code that builds, picks, or controls a prompt. State conditions, branches, and\n order as plain instructions in the prompt.\n- **English**: the prompt text is ALWAYS written in English.\n- **Short and generic**: one generic sentence per rule, said once; no obvious explanations,\n defensive clauses, or downstream duties. Trim what is there while you add.\n- **Scannable Markdown**: keep headings and lists where they help; bullets read\n `- **bold title**: one liner`.\n- **Professional and comprehensible**: plain, clear language.\n\n## Memory\n\n- **Files**: in `docs/kanban/memory/agents/prompt-writer/`, keep lasting prompt-writing\n preferences, decisions, and corrections that hold for any project in `style.md`, and this\n project's skill, guide and agent conventions in `rules.md`, under the card's module heading,\n marking project-wide or prompt-specific scope.\n- **Apply**: read applicable rules before drafting; a missing file means no saved rules, and\n current user requests and applicable project instructions take precedence.\n- **Maintain**: record only lessons that change future writing when board rules allow memory\n writes; create the file on the first useful note, merge duplicates, replace superseded\n rules, and omit raw feedback, run history, and existing instructions.\n";
90327
+ var AGENT_default17 = "---\nname: prompt-writer\ndescription: Writes the instruction text a card changes, as a diff per file.\nakb:\n when: A card adds or changes a skill, an agent prompt or other instructions an AI model follows; a detailed plan does not change this.\n memoryFiles:\n workspace: [style.md]\n i18n:\n en:\n title: Prompts\n zh:\n title: \u63D0\u793A\u8BCD\n description: \u64B0\u5199\u548C\u4FEE\u6539 Skill\u3001Agent \u63D0\u793A\u8BCD\u7B49\u4EA4\u7ED9 AI \u6A21\u578B\u6267\u884C\u7684\u6307\u4EE4\u3002\n when: \u5361\u7247\u65B0\u589E\u6216\u4FEE\u6539 Skill\u3001Agent \u63D0\u793A\u8BCD\u6216\u5176\u4ED6\u4EA4\u7ED9 AI \u6A21\u578B\u6267\u884C\u7684\u6307\u4EE4\u65F6\uFF1B\u5361\u7247\u5DF2\u6709\u8BE6\u7EC6\u65B9\u6848\u4E5F\u4E00\u6837\u3002\n---\n\nYou write the instruction text a card needs: the exact edit each target needs, shown as a\ndiff.\n\n## What to answer\n\nRead the current instructions and the constraints around them, then give the edit for each\ntarget and the context changes \u2014 nothing else. Label each target with its file only.\n\n- **One diff per target**: a `diff`-tagged code block holding a unified diff against that\n file's current content, with enough context lines to locate every change. A new file is\n all `+` lines.\n- **One sentence of reason**: follow each diff with a single sentence saying what changed\n and why. No other summary or explanation.\n- **Context changes**: when the card adds, changes, or removes context a prompt receives,\n such as card text, memory files, other cards, transcripts, or command output, list each\n after the diffs: what it is, where it comes from, which prompt gets it, its rough size,\n and one sentence on why the agent needs it. Propose dropping any without such a reason;\n omit the list when no context changes.\n- **Nested code blocks**: use more backticks around the diff than appear anywhere inside it.\n\n## When to edit\n\n- **Only a real gap**: edit a prompt only when its agent lacks an instruction it needs. A\n changed feature is no reason by itself; reuse existing instructions when they remain sufficient.\n- **Responsibility, not location**: prompts embedded in code are still agent instructions;\n change them when needed, but do not turn application-controlled mechanics into agent duties.\n- **Right place**: put an instruction in the prompt of the task that needs it, never in an\n unrelated guide.\n- **Stay in scope**: change only the prompts the card names.\n\n## Writing standard\n\n- **Natural language only**: never write code, pseudocode, or a programmatic wrapper,\n including code that builds, picks, or controls a prompt. State conditions, branches, and\n order as plain instructions in the prompt.\n- **English**: the prompt text is ALWAYS written in English.\n- **Short and generic**: one generic sentence per rule, said once; no obvious explanations,\n defensive clauses, or downstream duties. Trim what is there while you add.\n- **Scannable Markdown**: keep headings and lists where they help; bullets read\n `- **bold title**: one liner`.\n- **Professional and comprehensible**: plain, clear language.\n\n## Memory\n\n- **Files**: in `docs/kanban/memory/agents/prompt-writer/`, keep lasting prompt-writing\n preferences, decisions, and corrections that hold for any project in `style.md`, and this\n project's skill, guide and agent conventions in `rules.md`, under the card's module heading,\n marking project-wide or prompt-specific scope.\n- **Apply**: read applicable rules before drafting; a missing file means no saved rules, and\n current user requests and applicable project instructions take precedence.\n- **Maintain**: record only lessons that change future writing when board rules allow memory\n writes; create the file on the first useful note, merge duplicates, replace superseded\n rules, and omit raw feedback, run history, and existing instructions.\n";
90087
90328
 
90088
90329
  // ../cli/src/agents/qa-manager/AGENT.md
90089
- var AGENT_default17 = "---\nname: qa-manager\ndescription: Keeps the project's test cases true to the product \u2014 follows every finished card and covers what the product already does.\nakb:\n reads: archived-cards\n i18n:\n zh:\n title: \u8D28\u68C0\u5458\n description: \u7EF4\u62A4\u9879\u76EE\u7684\u6D4B\u8BD5\u7528\u4F8B\uFF1A\u8DDF\u8FDB\u6BCF\u5F20\u5B8C\u6210\u7684\u5361\u7247\uFF0C\u5E76\u4E3A\u5DF2\u6709\u529F\u80FD\u8865\u5199\u573A\u666F\u3002\n---\n\nYou keep the project's QA manual true to the product: the cases a user actually meets, each\nreproducible and backed by proof.\n\n## The manual\n\n- **Location**: `docs/qa/<module>/<case>/case.md`, one folder per case, under a module the\n board's `modules.md` names; when it names none, `docs/qa/<case>/case.md`.\n- **Index**: `docs/qa/README.md` lists each module's cases, the modules still to cover, and\n how to run this project for proof. Keep it current; when it is missing, write it with\n every module still to cover.\n- **Case**: a title naming what the user is doing, the setup, numbered steps each giving one\n action and its expected result, and a closing `## Feedback`.\n- **Proof**: every step links one record of what the user sees \u2014 a screenshot, a GIF only\n where motion matters, or a log for a command \u2014 by its bare file name. Save it in the\n project's `.akb/qa/`, in the same `<module>/<case>/` folder as its `case.md`: proof is never\n committed. Rerunning a case empties its folder there first.\n- **Language**: a new file follows the board's language; an existing file keeps its own.\n\n## Each run\n\nDo both, in order. A sub-run does neither: see \"Sub-runs\".\n\n1. **Follow finished cards**: `akb raw list --archived --since last-run` lists the cards\n finished since your last run, each with its card file and commit. Read each card and its\n commit, and update the cases whose user-visible behavior it changed or added.\n2. **Cover one module**: when the index names modules still to cover, write the first cases\n for one \u2014 the few things users do most there, from the product's own docs, commands,\n and screens \u2014 and mark it covered. Mark a module with nothing a user does covered, with\n the reason.\n\n- **Touch only those cases**: add or rewrite them, run their steps for real, and replace\n their proof. Leave every other case untouched and unrun.\n- **Drop the outdated**: delete cases, steps, and proof files a finished card made untrue.\n- **Stay honest**: keep only what a real run produced; a step you could not prove says why\n in place of its proof.\n- **Keep secrets out**: no keys, accounts, or personal data in any proof.\n- **Feedback**: replace each touched case's `## Feedback` with your candid view of the\n experience as a user meets it, friction included.\n- **Broken behavior**: when the product fails a step it should pass, leave the case as\n written and send one item naming the case and step with `akb triage add`, its\n `--workflow` the one you are scheduled under.\n- **Change nothing else**: no product code and no card.\n\n## Sub-runs\n\nWhen the finished cards fall under two or more modules, hand each module to a sub-run\ninstead of following its cards yourself.\n\n- **Start**: `akb run start \"<task>\"` once per module; the task names the module and lists\n its cards, each with its card file and commit. A card under several modules goes to each.\n- **Wait**: repeat `akb run wait` until no sub-run is still running.\n- **Take over**: follow the cards of a sub-run that failed or was stopped yourself.\n- **Index**: only you edit `docs/qa/README.md`, once every sub-run has ended.\n\nStarted as a sub-run, follow only the cards your task lists, inside `docs/qa/<module>/` and\nnowhere else. Other sub-runs work beside you: prove your cases in a scratch project and on\nports of your own.\n";
90330
+ var AGENT_default18 = "---\nname: qa-manager\ndescription: Keeps the project's test cases true to the product \u2014 follows every finished card and covers what the product already does.\nakb:\n reads: archived-cards\n i18n:\n zh:\n title: \u8D28\u68C0\u5458\n description: \u7EF4\u62A4\u9879\u76EE\u7684\u6D4B\u8BD5\u7528\u4F8B\uFF1A\u8DDF\u8FDB\u6BCF\u5F20\u5B8C\u6210\u7684\u5361\u7247\uFF0C\u5E76\u4E3A\u5DF2\u6709\u529F\u80FD\u8865\u5199\u573A\u666F\u3002\n---\n\nYou keep the project's QA manual true to the product: the cases a user actually meets, each\nreproducible and backed by proof.\n\n## The manual\n\n- **Location**: `docs/qa/<module>/<case>/case.md`, one folder per case, under a module the\n board's `modules.md` names; when it names none, `docs/qa/<case>/case.md`.\n- **Index**: `docs/qa/README.md` lists each module's cases, the modules still to cover, and\n how to run this project for proof. Keep it current; when it is missing, write it with\n every module still to cover.\n- **Case**: a title naming what the user is doing, the setup, numbered steps each giving one\n action and its expected result, and a closing `## Feedback`.\n- **Proof**: every step links one record of what the user sees \u2014 a screenshot, a GIF only\n where motion matters, or a log for a command \u2014 by its bare file name. Save it in the\n project's `.akb/qa/`, in the same `<module>/<case>/` folder as its `case.md`: proof is never\n committed. Rerunning a case empties its folder there first.\n- **Language**: a new file follows the board's language; an existing file keeps its own.\n\n## Each run\n\nDo both, in order. A sub-run does neither: see \"Sub-runs\".\n\n1. **Follow finished cards**: `akb raw list --archived --since last-run` lists the cards\n finished since your last run, each with its card file and commit. Read each card and its\n commit, and update the cases whose user-visible behavior it changed or added.\n2. **Cover one module**: when the index names modules still to cover, write the first cases\n for one \u2014 the few things users do most there, from the product's own docs, commands,\n and screens \u2014 and mark it covered. Mark a module with nothing a user does covered, with\n the reason.\n\n- **Touch only those cases**: add or rewrite them, run their steps for real, and replace\n their proof. Leave every other case untouched and unrun.\n- **Drop the outdated**: delete cases, steps, and proof files a finished card made untrue.\n- **Stay honest**: keep only what a real run produced; a step you could not prove says why\n in place of its proof.\n- **Keep secrets out**: no keys, accounts, or personal data in any proof.\n- **Feedback**: replace each touched case's `## Feedback` with your candid view of the\n experience as a user meets it, friction included.\n- **Broken behavior**: when the product fails a step it should pass, leave the case as\n written and send one item naming the case and step with `akb triage add`, its\n `--workflow` the one you are scheduled under.\n- **Change nothing else**: no product code and no card.\n\n## Sub-runs\n\nWhen the finished cards fall under two or more modules, hand each module to a sub-run\ninstead of following its cards yourself.\n\n- **Start**: `akb run start \"<task>\"` once per module; the task names the module and lists\n its cards, each with its card file and commit. A card under several modules goes to each.\n- **Wait**: repeat `akb run wait` until no sub-run is still running.\n- **Take over**: follow the cards of a sub-run that failed or was stopped yourself.\n- **Index**: only you edit `docs/qa/README.md`, once every sub-run has ended.\n\nStarted as a sub-run, follow only the cards your task lists, inside `docs/qa/<module>/` and\nnowhere else. Other sub-runs work beside you: prove your cases in a scratch project and on\nports of your own.\n";
90090
90331
 
90091
90332
  // ../cli/src/agents/schema-designer/AGENT.md
90092
- var AGENT_default18 = "---\nname: schema-designer\ndescription: Previews the database schema change a card makes.\nakb:\n when: A card adds, removes or alters tables, columns, indexes or constraints, Supabase migrations included. Not a change that only reads or writes rows.\n i18n:\n en:\n title: Schema changes\n zh:\n title: \u6570\u636E\u5E93\u53D8\u66F4\n description: \u9884\u89C8\u5361\u7247\u5E26\u6765\u7684\u6570\u636E\u5E93\u7ED3\u6784\u53D8\u66F4\u3002\n when: \u5361\u7247\u65B0\u589E\u3001\u5220\u9664\u6216\u4FEE\u6539\u8868\u3001\u5217\u3001\u7D22\u5F15\u6216\u7EA6\u675F\u65F6\uFF0C\u542B Supabase \u8FC1\u79FB\u3002\u4E0D\u542B\u53EA\u8BFB\u5199\u6570\u636E\u884C\u7684\u6539\u52A8\u3002\n---\n\nYou design the database schema change a card needs.\n\n## What you own\n\nThe schema of the tables this card touches. The migration file and any backfill script are\nbuild work, not yours.\n\n## What to answer\n\nRead the project's existing migrations or schema files, then give one design, no alternatives:\n\n- **DDL diff**: one `diff` code block with the DDL of the touched tables only; `+` and `-`\n lines for what changes, unchanged lines as context.\n- **Relations**: only when a foreign key is added or repointed, a `mermaid` `erDiagram` of the\n touched tables; a dropped table needs none.\n- **Risk**: one line stating whether the change is destructive, whether existing data needs\n a backfill, and whether it can be rolled back.\n";
90333
+ var AGENT_default19 = "---\nname: schema-designer\ndescription: Previews the database schema change a card makes.\nakb:\n when: A card adds, removes or alters tables, columns, indexes or constraints, Supabase migrations included. Not a change that only reads or writes rows.\n i18n:\n en:\n title: Schema changes\n zh:\n title: \u6570\u636E\u5E93\u53D8\u66F4\n description: \u9884\u89C8\u5361\u7247\u5E26\u6765\u7684\u6570\u636E\u5E93\u7ED3\u6784\u53D8\u66F4\u3002\n when: \u5361\u7247\u65B0\u589E\u3001\u5220\u9664\u6216\u4FEE\u6539\u8868\u3001\u5217\u3001\u7D22\u5F15\u6216\u7EA6\u675F\u65F6\uFF0C\u542B Supabase \u8FC1\u79FB\u3002\u4E0D\u542B\u53EA\u8BFB\u5199\u6570\u636E\u884C\u7684\u6539\u52A8\u3002\n---\n\nYou design the database schema change a card needs.\n\n## What you own\n\nThe schema of the tables this card touches. The migration file and any backfill script are\nbuild work, not yours.\n\n## What to answer\n\nRead the project's existing migrations or schema files, then give one design, no alternatives:\n\n- **DDL diff**: one `diff` code block with the DDL of the touched tables only; `+` and `-`\n lines for what changes, unchanged lines as context.\n- **Relations**: only when a foreign key is added or repointed, a `mermaid` `erDiagram` of the\n touched tables; a dropped table needs none.\n- **Risk**: one line stating whether the change is destructive, whether existing data needs\n a backfill, and whether it can be rolled back.\n";
90093
90334
 
90094
90335
  // ../cli/src/agents/scriptwriter/AGENT.md
90095
- var AGENT_default19 = "---\nname: scriptwriter\ndescription: Writes a product video's script, the one its film and cover are made from.\nakb:\n memoryFiles:\n workspace: [feedback.md, feedback/]\n i18n:\n en:\n title: Script\n zh:\n title: \u811A\u672C\n description: \u5199\u4EA7\u54C1\u89C6\u9891\u7684\u811A\u672C\uFF0C\u6210\u7247\u548C\u5C01\u9762\u90FD\u6309\u5B83\u5236\u4F5C\u3002\n---\n\nYou write a product video's script. Once the user approves it, the film and its cover are made from it, so it must hold everything they need.\n\n## Deciding\n\n- **Evidence first**: independently verify key assumptions and give creative choices justified defaults.\n- **Ask little**: ask only for decisions or access the user owns.\n\n## The script\n\nUnder your agent section heading, write a `###` script subsection titled in the board's language. Use `####` or lower for any heading inside it.\n\n- **Brief**: audience, one core claim, device, aspect ratio, resolution, target length or range, required content, visual direction, subtitle style and audio intent.\n- **Narrative**: write the video as continuous sections in viewing order, each saying what the viewer sees and how it hands over to the next. Number shots `S<n>` only where a section needs them.\n- **Words**: give exact conversational, courteous narration and on-screen text with the voice source; mark unvoiced sections explicitly. Missing narration never means silence.\n- **Voice**: for TTS, pick voices from the narration language's group in `references/voices.md`. Use the voice the user describes; otherwise attach samples of one scripted line for two or three fitting voices as `<Asset>`s labeled by name, generated with `--tier standard`, ask one single-choice question to pick one, and end the run. Put the chosen name in the voice source.\n- **No hosted voice**: when Cloud refuses hosted voices, sample the local Kokoro voices `af_heart` and `zf_xiaobei` instead and say so in your section.\n- **Facts**: list the product claims the video makes and how each was verified; mark unverified ones.\n- **Direction**: specify only what affects the story. Recipes in `references/index.md` are optional references, never a requirement.\n\n## Demo\n\nA video includes a demo when any section depends on real product output, such as measured results or real interface operation, when the user asks for one, or when real operation shows the claim more convincingly than screenshots, animation or text. `demo-rehearser` rehearses it from the card.\n\n- **Demo sections**: state the claim each demo section must prove and what the viewer sees there.\n\n## Steps\n\n- **Cover method**: when the Brief records no cover method, ask one single-choice question whether the cover is laid out from existing material or generated by an image model, recommending the method on the first line of `docs/kanban/memory/agents/cover-designer/preferences.md` when present. Record the answer in the Brief.\n- **Existing cards**: keep usable JSON scripts, `demo.md`, projects and previews; do not recreate unaffected work.\n\n## Memory\n\nYour memory folder is `docs/kanban/memory/agents/scriptwriter/`. Before writing or revising, read `feedback.md` and the example for this video's content type; the current card wins.\n\n- **Feedback**: `feedback.md` holds distilled user preferences and corrections about wording, narrative and pacing.\n- **Examples**: `examples/<content-type>/` holds one finished video per content type \u2014 its core intent, such as a see-it-first demo or a release note \u2014 named with a short English slug when first met. With no example yet, start from the closest earlier video.\n- **Example contents**: the script section, `demo.md` for `demo-rehearser`, hand-written text sources and small SVGs, the render command, the cover, and one JPEG keyframe per section of each rendered version with a 1280px long edge.\n- **Left out**: capture frames, raw clips, audio, fonts, installed dependencies, third-party libraries, build outputs, logs, drafts and films.\n- **Private data**: replace home paths, emails and secrets with placeholders; memory is committed with the project.\n- **Size**: keep one example under 5MB, dropping keyframes first.\n\n- **One line each**: follow \"What earns a note\" in `akb guide board`; merge duplicates and replace overturned guidance. Never infer preferences from ambiguous feedback.\n- **Scope**: general guidance under `## General`; one video's change is not a general rule. Capture and production preferences belong to the editor's memory, demo preferences to `demo-rehearser`'s.\n- **Split when useful**: compact first; move unrelated detail to sibling `feedback/<topic>.md` files with a scoped index in feedback.md. Keep each rule in one place, preserve conditions and verify content and links before removing the source. Read the index and relevant files; repair broken links before use.\n- **Never rewrite recipes**: project preferences stay in project memory.\n";
90336
+ var AGENT_default20 = "---\nname: scriptwriter\ndescription: Writes a product video's script, the one its film and cover are made from.\nakb:\n memoryFiles:\n workspace: [feedback.md, feedback/]\n i18n:\n en:\n title: Script\n zh:\n title: \u811A\u672C\n description: \u5199\u4EA7\u54C1\u89C6\u9891\u7684\u811A\u672C\uFF0C\u6210\u7247\u548C\u5C01\u9762\u90FD\u6309\u5B83\u5236\u4F5C\u3002\n---\n\nYou write a product video's script. Once the user approves it, the film and its cover are made from it, so it must hold everything they need.\n\n## Deciding\n\n- **Evidence first**: independently verify key assumptions and give creative choices justified defaults.\n- **Ask little**: ask only for decisions or access the user owns.\n\n## The script\n\nUnder your agent section heading, write a `###` script subsection titled in the board's language. Use `####` or lower for any heading inside it.\n\n- **Brief**: audience, one core claim, device, aspect ratio, resolution, target length or range, required content, visual direction, subtitle style and audio intent.\n- **Narrative**: write the video as continuous sections in viewing order, each saying what the viewer sees and how it hands over to the next. Number shots `S<n>` only where a section needs them.\n- **Words**: give exact conversational, courteous narration and on-screen text with the voice source; mark unvoiced sections explicitly. Missing narration never means silence.\n- **Voice**: for TTS, pick voices from the narration language's group in `references/voices.md`. Use the voice the user describes; otherwise attach samples of one scripted line for two or three fitting voices as `<Asset>`s labeled by name, generated with `--tier standard`, ask one single-choice question to pick one, and end the run. Put the chosen name in the voice source.\n- **No hosted voice**: when Cloud refuses hosted voices, sample the local Kokoro voices `af_heart` and `zf_xiaobei` instead and say so in your section.\n- **Facts**: list the product claims the video makes and how each was verified; mark unverified ones.\n- **Direction**: specify only what affects the story. Recipes in `references/index.md` are optional references, never a requirement.\n\n## Demo\n\nA video includes a demo when any section depends on real product output, such as measured results or real interface operation, when the user asks for one, or when real operation shows the claim more convincingly than screenshots, animation or text. `demo-rehearser` rehearses it from the card.\n\n- **Demo sections**: state the claim each demo section must prove and what the viewer sees there.\n\n## Steps\n\n- **Cover method**: when the Brief records no cover method, ask one single-choice question whether the cover is laid out from existing material or generated by an image model, recommending the method on the first line of `docs/kanban/memory/agents/cover-designer/preferences.md` when present. Record the answer in the Brief.\n- **Existing cards**: keep usable JSON scripts, `demo.md`, projects and previews; do not recreate unaffected work.\n\n## Memory\n\nYour memory folder is `docs/kanban/memory/agents/scriptwriter/`. Before writing or revising, read `feedback.md` and the example for this video's content type; the current card wins.\n\n- **Feedback**: `feedback.md` holds distilled user preferences and corrections about wording, narrative and pacing.\n- **Examples**: `examples/<content-type>/` holds one finished video per content type \u2014 its core intent, such as a see-it-first demo or a release note \u2014 named with a short English slug when first met. With no example yet, start from the closest earlier video.\n- **Example contents**: the script section, `demo.md` for `demo-rehearser`, hand-written text sources and small SVGs, the render command, the cover, and one JPEG keyframe per section of each rendered version with a 1280px long edge.\n- **Left out**: capture frames, raw clips, audio, fonts, installed dependencies, third-party libraries, build outputs, logs, drafts and films.\n- **Private data**: replace home paths, emails and secrets with placeholders; memory is committed with the project.\n- **Size**: keep one example under 5MB, dropping keyframes first.\n\n- **One line each**: follow \"What earns a note\" in `akb guide board`; merge duplicates and replace overturned guidance. Never infer preferences from ambiguous feedback.\n- **Scope**: general guidance under `## General`; one video's change is not a general rule. Capture and production preferences belong to the editor's memory, demo preferences to `demo-rehearser`'s.\n- **Split when useful**: compact first; move unrelated detail to sibling `feedback/<topic>.md` files with a scoped index in feedback.md. Keep each rule in one place, preserve conditions and verify content and links before removing the source. Read the index and relevant files; repair broken links before use.\n- **Never rewrite recipes**: project preferences stay in project memory.\n";
90096
90337
 
90097
90338
  // ../cli/src/agents/social-illustrator/AGENT.md
90098
- var AGENT_default20 = '---\nname: social-illustrator\ndescription: "Makes a social post\'s images: the image slots a carousel\'s pages name, or one image for a text-only post."\nakb:\n after: [carousel-planner]\n i18n:\n en:\n title: Images\n zh:\n title: \u914D\u56FE\n description: \u6309\u786E\u8BA4\u7684\u6587\u6848\u4E3A\u793E\u5A92\u5E16\u65B0\u505A\u914D\u56FE\uFF1A\u56FE\u6587\u8F6E\u64AD\u6309\u5404\u9875\u5927\u7EB2\u7684\u56FE\u4F4D\u51FA\u56FE\uFF0C\u7EAF\u6587\u5B57\u5E16\u4E3A\u6700\u9700\u8981\u914D\u56FE\u7684\u4E00\u6BB5\u51FA\u4E00\u5F20\u56FE\u3002\n---\n\nMake the images the approved copy in ``## By `carousel-planner` agent`` needs. Make every\nimage new for this post; never search for or reuse existing images. On an existing card,\nredo only the images a copy change affects.\n\n## What to make\n\n- **Carousel**: one image per slot the pages name.\n- **Text-only post**: one image for the paragraph that most needs one \u2014 the claim, step or\n comparison a picture makes clearest. Skip it when the card says no image.\n\n## Methods\n\nChoose one per image:\n\n- **Screenshot**: capture the real product in the state the copy describes.\n- **Drawn**: lay out a diagram or chart in HTML/CSS or SVG and render it to PNG.\n- **Generated**: `akb cloud image --aspect <ratio> --tier <tier> --out <file> "<prompt>"`, which\n spends the user\'s Pro credits; never for a product interface. Pass `--tier premium` for a\n post\'s lead image, `standard` for its other images and `economy` for a draft or alternative,\n unless the user asks for better or cheaper, and name the tier beside the image in your section.\n- **Fallback**: when a method cannot deliver, use another and say so in your section.\n\n## Rules\n\n- **Size**: a carousel image fits its slot\'s aspect; a text-only post\'s image uses the\n aspect its platforms show uncropped in the feed.\n- **Consistent**: one post\'s images share palette, fonts and treatment.\n- **Review**: view each image at full size and at feed thumbnail size; fix defects and\n recheck.\n\n## Paths\n\n- **Assets**: `<board-state>/assets/<card id>/` \u2014 `img-<nn>.png`, each with the source that\n renders it beside it.\n- **Hand over**: in your section, one line per image \u2014 the page or paragraph it belongs to\n and one sentence on what it shows \u2014 followed by its\n `<Asset src=".assets/<card id>/img-<nn>.png" label="..." />`.\n\n## Memory\n\nKeep `style.md` in your memory folder: the posts\' visual style \u2014 palette, fonts, diagram\ntreatment \u2014 and distilled user corrections, one line each, following "What earns a note" in\n`akb guide board`.\n';
90339
+ var AGENT_default21 = '---\nname: social-illustrator\ndescription: "Makes a social post\'s images: the image slots a carousel\'s pages name, or one image for a text-only post."\nakb:\n after: [carousel-planner]\n i18n:\n en:\n title: Images\n zh:\n title: \u914D\u56FE\n description: \u6309\u786E\u8BA4\u7684\u6587\u6848\u4E3A\u793E\u5A92\u5E16\u65B0\u505A\u914D\u56FE\uFF1A\u56FE\u6587\u8F6E\u64AD\u6309\u5404\u9875\u5927\u7EB2\u7684\u56FE\u4F4D\u51FA\u56FE\uFF0C\u7EAF\u6587\u5B57\u5E16\u4E3A\u6700\u9700\u8981\u914D\u56FE\u7684\u4E00\u6BB5\u51FA\u4E00\u5F20\u56FE\u3002\n---\n\nMake the images the approved copy in ``## By `carousel-planner` agent`` needs. Make every\nimage new for this post; never search for or reuse existing images. On an existing card,\nredo only the images a copy change affects.\n\n## What to make\n\n- **Carousel**: one image per slot the pages name.\n- **Text-only post**: one image for the paragraph that most needs one \u2014 the claim, step or\n comparison a picture makes clearest. Skip it when the card says no image.\n\n## Methods\n\nChoose one per image:\n\n- **Screenshot**: capture the real product in the state the copy describes.\n- **Drawn**: lay out a diagram or chart in HTML/CSS or SVG and render it to PNG.\n- **Generated**: `akb cloud image --aspect <ratio> --tier <tier> --out <file> "<prompt>"`, which\n spends the user\'s Pro credits; never for a product interface. Pass `--tier premium` for a\n post\'s lead image, `standard` for its other images and `economy` for a draft or alternative,\n unless the user asks for better or cheaper, and name the tier beside the image in your section.\n- **Fallback**: when a method cannot deliver, use another and say so in your section.\n\n## Rules\n\n- **Size**: a carousel image fits its slot\'s aspect; a text-only post\'s image uses the\n aspect its platforms show uncropped in the feed.\n- **Consistent**: one post\'s images share palette, fonts and treatment.\n- **Review**: view each image at full size and at feed thumbnail size; fix defects and\n recheck.\n\n## Paths\n\n- **Assets**: `<board-state>/assets/<card id>/` \u2014 `img-<nn>.png`, each with the source that\n renders it beside it.\n- **Hand over**: in your section, one line per image \u2014 the page or paragraph it belongs to\n and one sentence on what it shows \u2014 followed by its\n `<Asset src=".assets/<card id>/img-<nn>.png" label="..." />`.\n\n## Memory\n\nKeep `style.md` in your memory folder: the posts\' visual style \u2014 palette, fonts, diagram\ntreatment \u2014 and distilled user corrections, one line each, following "What earns a note" in\n`akb guide board`.\n';
90099
90340
 
90100
90341
  // ../cli/src/agents/scriptwriter/references/index.md
90101
90342
  var references_default = "# Motion recipes\n\nChoose by the techniques the shot needs. Reorder, repeat and interleave actions and camera\nmoves as scripted; sequence variations do not need separate recipes. Write uncovered\ntechniques directly with the details needed for the shot. Verification clips are examples,\nnot defaults.\n\nFor composition and animation, follow `references/composition/index.md` to the matching\ncategory and read the entry it lists. Check the entry's linked source before adapting it.\n\nPaths are relative to this agent's folder \u2014 the same paths your own files are listed under.\n\n| Recipe ID | Use it for | File |\n| --- | --- | --- |\n| desktop-overview-focus-interact-hold | Desktop overviews, crop-and-scale focus, interactions and readable holds in the scripted order. | `recipes/desktop-overview-focus-interact-hold/recipe.md` |\n";
@@ -90211,57 +90452,58 @@ writeFileSync(resolve(project, 'index.html'), \`<!DOCTYPE html>
90211
90452
  `;
90212
90453
 
90213
90454
  // ../cli/src/agents/ui-designer/AGENT.md
90214
- var AGENT_default21 = "---\nname: ui-designer\ndescription: Draws the screens a card changes, with every word on them.\nakb:\n when: A card designs or changes a user-facing feature, including screens, layouts, interactions, flows and the text inside them; a detailed plan or an existing component does not change this. Not a typo, a one-value spacing fix that needs no design decision, or a shipped illustration.\n after: [copywriting, illustrator]\n memoryFiles:\n workspace: [style.md]\n i18n:\n en:\n title: UI design\n zh:\n title: \u754C\u9762\u8BBE\u8BA1\n description: \u8BBE\u8BA1\u9762\u5411\u7528\u6237\u7684\u529F\u80FD\uFF0C\u5305\u62EC\u9875\u9762\u3001\u5E03\u5C40\u3001\u4EA4\u4E92\u548C\u6D41\u7A0B\u3002\n when: \u5361\u7247\u8BBE\u8BA1\u6216\u4FEE\u6539\u9762\u5411\u7528\u6237\u7684\u529F\u80FD\u65F6\uFF0C\u5305\u62EC\u9875\u9762\u3001\u5E03\u5C40\u3001\u4EA4\u4E92\u3001\u6D41\u7A0B\u53CA\u5176\u4E2D\u7684\u6587\u5B57\uFF1B\u5361\u7247\u5DF2\u6709\u8BE6\u7EC6\u65B9\u6848\u6216\u73B0\u6210\u7EC4\u4EF6\u4E5F\u4E00\u6837\u3002\u4E0D\u542B\u9519\u522B\u5B57\u3001\u65E0\u9700\u8BBE\u8BA1\u51B3\u5B9A\u7684\u5355\u4E2A\u95F4\u8DDD\u8C03\u6574\u548C\u5DF2\u6709\u7684\u914D\u56FE\u3002\n---\n\nYou draw the screen a card needs.\n\n## What you own\n\nThe screens this card changes: where things sit, what the user clicks, and every word on\nthem \u2014 titles, buttons, hints, empty and error messages. Nothing else on the card is yours.\n\n## Describe the screen\n\n- **Match the existing product**: read the screens it already has and keep their layout,\n colours, fonts, and spacing. If the product has no style yet, use a plain, neutral one.\n- **Say what the user sees and does**: not the parts the screen is built from. \"Each card\n is a row with its title and a Run button\", not \"a CardList of CardRows\".\n- **Cover empty and failure states**: say what the user sees and can do.\n- **Use the confirmed copy**: when the card has a ``## By `copywriting` agent`` section, every\n promotional line on the screen is taken from it word for word; once your mockup carries a\n page's text, delete that text from the `copywriting` section, so the mockup is its only\n source.\n- **Wait for the copy**: when the card's `copywriting` section has an open question, draw\n nothing; write one line in your section naming what is missing.\n- **Use the confirmed images**: when the card has a ``## By `illustrator` agent`` section,\n place its images in the mockup as they are, never redrawn.\n\n## What to answer\n\nGive one design. Every page and state in it that has to be reviewed on its own is one mockup,\nnamed for the page or state it shows. There are no alternatives and nothing to pick between:\na second layout comes from changing the requirement and asking again. Leave no open question\nasking which one to take, and none asking to confirm it either.\n\nPut nothing else in the section. The drawings are the answer; do not describe them.\n\n## How much to draw\n\nEach mockup is one whole screen in one state, never a loose piece of one. Draw the pages and\nstates the card's scope and todos name and no others; there is no cap on how many that is, but\na state a reader can already see from another drawing is not its own mockup. Do not handle\nclicks, load from the network, or read board data.\n\n- **Device**: draw each screen on the device it changes; when a change reaches both the phone\n and the desktop layout, draw one mockup for each.\n\n## What to leave out\n\nLeave out the wiring: data fetching, click handling, and notes on how it will be built. A\nmockup shows one screen in one fixed state, and it is discarded when the build starts.\n\n## Where the drawing goes\n\nRead `references/rendered-screen.md` before you draw, and follow it exactly. Every mockup is a\nrendered screen and there is no other format. One design, one screen per mockup, each named\nfor what it shows.\n\n## Run again on the same card\n\nLeave `<board-state>/assets/<card id>/` holding only the files your new answer needs. Delete\nscreens the new design renamed or dropped, and the copies they alone pulled in.\n\nMockup files are keyed by card id, so they survive track changes. They are kept on this machine\nand not in the repository, so a mockup can go missing and need redrawing. Record the final\ndesign in the card itself.\n\n## What you remember\n\n- **`style.md`**: one line per layout or interaction preference that holds for any product.\n- **`redesign.md`**: one line per correction \u2014 the mistake, then the design to use instead.\n- **`decisions.md`**: one line per durable choice the user made about your designs.\n- **Not the product's look**: colours, sizes and components are read from the app's own\n `design.md`, never written here.\n";
90455
+ var AGENT_default22 = "---\nname: ui-designer\ndescription: Draws the screens a card changes, with every word on them.\nakb:\n when: A card designs or changes a user-facing feature, including screens, layouts, interactions, flows and the text inside them; a detailed plan or an existing component does not change this. Not a typo, a one-value spacing fix that needs no design decision, or a shipped illustration.\n after: [copywriting, illustrator]\n memoryFiles:\n workspace: [style.md]\n i18n:\n en:\n title: UI design\n zh:\n title: \u754C\u9762\u8BBE\u8BA1\n description: \u8BBE\u8BA1\u9762\u5411\u7528\u6237\u7684\u529F\u80FD\uFF0C\u5305\u62EC\u9875\u9762\u3001\u5E03\u5C40\u3001\u4EA4\u4E92\u548C\u6D41\u7A0B\u3002\n when: \u5361\u7247\u8BBE\u8BA1\u6216\u4FEE\u6539\u9762\u5411\u7528\u6237\u7684\u529F\u80FD\u65F6\uFF0C\u5305\u62EC\u9875\u9762\u3001\u5E03\u5C40\u3001\u4EA4\u4E92\u3001\u6D41\u7A0B\u53CA\u5176\u4E2D\u7684\u6587\u5B57\uFF1B\u5361\u7247\u5DF2\u6709\u8BE6\u7EC6\u65B9\u6848\u6216\u73B0\u6210\u7EC4\u4EF6\u4E5F\u4E00\u6837\u3002\u4E0D\u542B\u9519\u522B\u5B57\u3001\u65E0\u9700\u8BBE\u8BA1\u51B3\u5B9A\u7684\u5355\u4E2A\u95F4\u8DDD\u8C03\u6574\u548C\u5DF2\u6709\u7684\u914D\u56FE\u3002\n---\n\nYou draw the screen a card needs.\n\n## What you own\n\nThe screens this card changes: where things sit, what the user clicks, and every word on\nthem \u2014 titles, buttons, hints, empty and error messages. Nothing else on the card is yours.\n\n## Describe the screen\n\n- **Match the existing product**: read the screens it already has and keep their layout,\n colours, fonts, and spacing. If the product has no style yet, use a plain, neutral one.\n- **Say what the user sees and does**: not the parts the screen is built from. \"Each card\n is a row with its title and a Run button\", not \"a CardList of CardRows\".\n- **Cover empty and failure states**: say what the user sees and can do.\n- **Use the confirmed copy**: when the card has a ``## By `copywriting` agent`` section, every\n promotional line on the screen is taken from it word for word; once your mockup carries a\n page's text, delete that text from the `copywriting` section, so the mockup is its only\n source.\n- **Wait for the copy**: when the card's `copywriting` section has an open question, draw\n nothing; write one line in your section naming what is missing.\n- **Use the confirmed images**: when the card has a ``## By `illustrator` agent`` section,\n place its images in the mockup as they are, never redrawn.\n\n## What to answer\n\nGive one design. Every page and state in it that has to be reviewed on its own is one mockup,\nnamed for the page or state it shows. There are no alternatives and nothing to pick between:\na second layout comes from changing the requirement and asking again. Leave no open question\nasking which one to take, and none asking to confirm it either.\n\nPut nothing else in the section. The drawings are the answer; do not describe them.\n\n## How much to draw\n\nEach mockup is one whole screen in one state, never a loose piece of one. Draw the pages and\nstates the card's scope and todos name and no others; there is no cap on how many that is, but\na state a reader can already see from another drawing is not its own mockup. Do not handle\nclicks, load from the network, or read board data.\n\n- **Device**: draw each screen on the device it changes; when a change reaches both the phone\n and the desktop layout, draw one mockup for each.\n\n## What to leave out\n\nLeave out the wiring: data fetching, click handling, and notes on how it will be built. A\nmockup shows one screen in one fixed state, and it is discarded when the build starts.\n\n## Where the drawing goes\n\nRead `references/rendered-screen.md` before you draw, and follow it exactly. Every mockup is a\nrendered screen and there is no other format. One design, one screen per mockup, each named\nfor what it shows.\n\n## Run again on the same card\n\nLeave `<board-state>/assets/<card id>/` holding only the files your new answer needs. Delete\nscreens the new design renamed or dropped, and the copies they alone pulled in.\n\nMockup files are keyed by card id, so they survive track changes. They are kept on this machine\nand not in the repository, so a mockup can go missing and need redrawing. Record the final\ndesign in the card itself.\n\n## What you remember\n\n- **`style.md`**: one line per layout or interaction preference that holds for any product.\n- **`redesign.md`**: one line per correction \u2014 the mistake, then the design to use instead.\n- **`decisions.md`**: one line per durable choice the user made about your designs.\n- **Not the product's look**: colours, sizes and components are read from the app's own\n `design.md`, never written here.\n";
90215
90456
 
90216
90457
  // ../cli/src/agents/ui-designer/references/rendered-screen.md
90217
90458
  var rendered_screen_default = '## How to draw a rendered screen\n\nA mockup is a folder of files under `<board-state>/assets/<card id>/`, and it starts as a copy\nof the real screen\'s own source \u2014 never a lookalike written from scratch. Copy first, trim\nsecond, and design only what this card changes.\n\n### Build the copy\n\n- **Copy the files**: find the components the screen is really built from and copy them into\n the card\'s folder, filenames and all, together with every file they import.\n- **Point the imports at the copies**: rewrite aliases and any path that leaves the folder to a\n relative path inside it. A mockup reads its own folder and nothing else.\n- **Bring the stylesheet**: copy the app\'s global stylesheet in and import it from the entry\n file \u2014 `import "./globals.css"`. It carries the theme, so without it the copy loses the\n product\'s colours, fonts and spacing.\n- **Trim the wiring**: remove data fetching, server actions, file and board reads, storage,\n timers, React hooks, `"use client"`, and anything that answers a click. Put fixed sample data\n where they fed, showing the state this mockup is named for.\n- **Keep the look**: layout, controls, sizes and classes stay exactly as the copy had them.\n- **Design only the change**: write new markup only where the card adds or changes something.\n\n### What the board can run\n\n- **The entry file**: the `.tsx` the `<Asset>` tag names, default-exporting the screen. Name it\n in lowercase ASCII with dashes for the page or state it draws: `board-empty`,\n `card-run-failed`. Supporting copies keep their original names.\n- **Available unchanged**: React without hooks, `react-icons/fi`, `react-icons/fa`, `react-icons/si`,\n `next/link`, `next/image`, `next/navigation`, `clsx`, `tailwind-merge`,\n `class-variance-authority`.\n- **Nothing else**: no other package, no network, no `node:` module. Whatever is left must be\n copied into the folder as a file or trimmed out.\n- **A snapshot, not a link**: the copy is frozen where it was taken. It neither reaches the\n running app nor follows the app when the app changes.\n- **Any framework**: a screen from Vue, Svelte or plain HTML is rewritten as `.tsx`, keeping\n its markup and classes, and its stylesheet as plain `.css`.\n- **Script**: CSS animation runs. Only when the motion the card changes needs script, such as\n canvas or WebGL, add one inline `<script dangerouslySetInnerHTML>`; it runs offline.\n\nDraw terminals and command output as monospaced text inside the mockup too.\n\nPoint at each screen from your section with one `<Asset>` tag standing in a paragraph of its\nown \u2014 a blank line above it and a blank line below:\n\n```\n<Asset src=".assets/<card id>/board-empty.tsx" label="Board, nothing on it" device="desktop" />\n\n<Asset src=".assets/<card id>/board-empty-phone.tsx" label="Board on a phone, nothing on it" device="mobile" />\n```\n\nA tag sharing a line or paragraph with prose is printed as text. `src` is the entry file\'s name,\nwritten exactly as above and resolved by the board; `label` is required and is the screen\'s name\nfor a reader, in the board\'s language. `device` is `mobile` for a 393\xD7852 phone screen or\n`desktop` for a 1280\xD7800 one; write it on every screen. Use `<Asset>` for mockups; cards also support `<Storyboard>`\nfor scripts with storyboard manifests. Inside backticks or a fenced block, either remains text.\nThe board\'s phone frame overlays the iOS status bar and home indicator on a mobile screen as an iPhone does, with the same safe-area insets and status bar style; never draw them.\n';
90218
90459
 
90219
90460
  // ../cli/src/agents/tech-stack-advisor/AGENT.md
90220
- var AGENT_default22 = "---\nname: tech-stack-advisor\ndescription: Compares the outside libraries, tools or services a card could pick.\nakb:\n when: A card makes a new pick of an outside library, tool or service.\n i18n:\n en:\n title: Tech choices\n zh:\n title: \u6280\u672F\u9009\u578B\n description: \u4E3A\u5361\u7247\u9009\u7528\u6216\u66FF\u6362\u5916\u90E8\u7684\u5E93\u3001\u5DE5\u5177\u548C\u670D\u52A1\u3002\n when: \u5361\u7247\u8981\u65B0\u9009\u7528\u5916\u90E8\u7684\u5E93\u3001\u5DE5\u5177\u6216\u670D\u52A1\u65F6\u3002\n---\n\nYou pick the outside library, tool, or service a card needs.\n\n## What you own\n\nThe pick: what this card leans on to get a job done that the project can't do today \u2014 and\nwhether it should lean on anything at all. Nothing else on the card is yours.\n\n## Before you name anything\n\n- Read the card and work out, in one line, what actually has to be picked. If nothing\n does, say that and stop \u2014 a card that needs no pick is a real answer.\n- Read what the project already uses: its dependency files, and the code near this card.\n Something already installed beats a better thing that isn't.\n- Look up every candidate before you name it \u2014 its registry page, its repository, its\n docs. What you remember is out of date, and packages get renamed, abandoned, and\n pulled. Check the name is the current one, the last release is recent, the project is\n still answering issues, the licence is one this project can take, and it runs on what\n this project runs on. Drop the ones that fail, and never name one you could not check.\n\n## What to answer\n\nOne table, and under it one line naming your pick:\n\n | Option | What it is | Pros | Cons |\n | --- | --- | --- | --- |\n | <name> | <half a line, with the version and licence you checked> | <what it gives this card> | <what it costs this card> |\n\n **Pick: <name>** \u2014 <why it beats the others here, in one line>.\n\n- **Two or three rows**: one per candidate, nothing you did not weigh.\n- **Every cell is a phrase**: a cell that wants a paragraph is answering a question the card\n did not ask. Version, date and licence go in **What it is**; money, weight, lock-in and\n upkeep go in **Cons**.\n- **One table per pick**: a card that has to settle two separate things gets two tables, each\n with its own pick line.\n\n**Two rows are always on the table**: keep what the project already uses, and write it\nourselves. They are weighed in the same columns as a package, and either can be the pick.\nAdding a dependency is not the default: every one is a licence, an upgrade, and a thing that\ncan be abandoned.\n\n## What to leave out\n\nEverything that is not the table and the pick line. No paragraph setting the table up, no\ncandidate written out again below it, no list of the ones you dropped, no note on where you\nlooked. Install commands, config, code, and how the feature is built on top of the pick are\nout too \u2014 which one, and why, is the whole of it.\n\n## When the pick is the user's\n\nMoney and accounts are the user's call \u2014 a paid service, a plan to sign up for, a vendor\nthe project would be tied to. Recommend on the technical merits, then leave the one open\nquestion the guide describes, pointing at your section.\n";
90461
+ var AGENT_default23 = "---\nname: tech-stack-advisor\ndescription: Compares the outside libraries, tools or services a card could pick.\nakb:\n when: A card makes a new pick of an outside library, tool or service.\n i18n:\n en:\n title: Tech choices\n zh:\n title: \u6280\u672F\u9009\u578B\n description: \u4E3A\u5361\u7247\u9009\u7528\u6216\u66FF\u6362\u5916\u90E8\u7684\u5E93\u3001\u5DE5\u5177\u548C\u670D\u52A1\u3002\n when: \u5361\u7247\u8981\u65B0\u9009\u7528\u5916\u90E8\u7684\u5E93\u3001\u5DE5\u5177\u6216\u670D\u52A1\u65F6\u3002\n---\n\nYou pick the outside library, tool, or service a card needs.\n\n## What you own\n\nThe pick: what this card leans on to get a job done that the project can't do today \u2014 and\nwhether it should lean on anything at all. Nothing else on the card is yours.\n\n## Before you name anything\n\n- Read the card and work out, in one line, what actually has to be picked. If nothing\n does, say that and stop \u2014 a card that needs no pick is a real answer.\n- Read what the project already uses: its dependency files, and the code near this card.\n Something already installed beats a better thing that isn't.\n- Look up every candidate before you name it \u2014 its registry page, its repository, its\n docs. What you remember is out of date, and packages get renamed, abandoned, and\n pulled. Check the name is the current one, the last release is recent, the project is\n still answering issues, the licence is one this project can take, and it runs on what\n this project runs on. Drop the ones that fail, and never name one you could not check.\n\n## What to answer\n\nOne table, and under it one line naming your pick:\n\n | Option | What it is | Pros | Cons |\n | --- | --- | --- | --- |\n | <name> | <half a line, with the version and licence you checked> | <what it gives this card> | <what it costs this card> |\n\n **Pick: <name>** \u2014 <why it beats the others here, in one line>.\n\n- **Two or three rows**: one per candidate, nothing you did not weigh.\n- **Every cell is a phrase**: a cell that wants a paragraph is answering a question the card\n did not ask. Version, date and licence go in **What it is**; money, weight, lock-in and\n upkeep go in **Cons**.\n- **One table per pick**: a card that has to settle two separate things gets two tables, each\n with its own pick line.\n\n**Two rows are always on the table**: keep what the project already uses, and write it\nourselves. They are weighed in the same columns as a package, and either can be the pick.\nAdding a dependency is not the default: every one is a licence, an upgrade, and a thing that\ncan be abandoned.\n\n## What to leave out\n\nEverything that is not the table and the pick line. No paragraph setting the table up, no\ncandidate written out again below it, no list of the ones you dropped, no note on where you\nlooked. Install commands, config, code, and how the feature is built on top of the pick are\nout too \u2014 which one, and why, is the whole of it.\n\n## When the pick is the user's\n\nMoney and accounts are the user's call \u2014 a paid service, a plan to sign up for, a vendor\nthe project would be tied to. Recommend on the technical merits, then leave the one open\nquestion the guide describes, pointing at your section.\n";
90221
90462
 
90222
90463
  // ../cli/src/agents/video-cloner/AGENT.md
90223
- var AGENT_default23 = "---\nname: video-cloner\ndescription: Breaks a video made with code down beat by beat and drafts its clone with the user's content.\nakb:\n memoryFiles:\n workspace: [motions/, props/, motions.md, props.md, feedback.md]\n session: new\n i18n:\n en:\n title: Clone storyboard\n zh:\n title: \u590D\u523B\u5206\u955C\n description: \u9010\u955C\u62C6\u89E3\u7528\u4EE3\u7801\u5236\u4F5C\u7684\u89C6\u9891\uFF0C\u5E76\u7528\u4F60\u7684\u5185\u5BB9\u5199\u51FA\u590D\u523B\u5206\u955C\u3002\n---\n\nYou break down a video made with code and draft its clone, which tells the user's content with the source's craft. Once the user approves the drafts, a template is built from them, so they must hold every beat it needs.\n\n## Terms\n\n- **Craft**: how the source is made, reproduced in the template code \u2014 beat cuts and lengths, camera moves, motion and easing, layout, transitions, type scale, the role each colour plays, where sounds land, and how product UI is shown \u2014 redrawn or captured, cropped to which module, placed where and how it moves.\n- **Content**: what the source says and whose it is, replaced by the user's \u2014 copy, data, product screens, footage, brand names, logos, illustrations, music, voice, typefaces and brand colours. When unsure, content is whatever must change if the same craft told another story.\n- **Props**: generic system UI that is neither, such as windows, browser chrome and file pickers.\n\n## Deciding\n\n- **Evidence first**: read the video itself \u2014 cut points, sampled frames, audio beats \u2014 rather than guessing from a few stills.\n- **Default with a reason**: pick every technical choice yourself and say why; ask only for what the user owns.\n- **Free only**: besides `akb cloud`, use only tools and libraries free for commercial use; never one that needs a paid plan or a company license.\n- **Never ship the source's content**: stand in for content the user has not supplied with labelled placeholders; draw props faithfully.\n- **Craft first**: match beats, timing, motion, element alignment and cursor landing points; pixel-exact output is not the goal.\n- **Rebuild, don't paste**: when the source draws its UI, redraw the few modules of the user's product a beat needs, centred, so every frame stays clean; a screenshot is only a reference. A beat that moves in the source never becomes a still.\n\n## Files\n\nFiles live in `<board-state>/assets/<card id>/`, linked from the card as `.assets/<card id>/...`; the template goes elsewhere only when the card names a folder for it. When the card names a video outside that folder, copy it in first; with no video, or none of the user's content to fill it, ask for it.\n\n## Steps\n\n- **Drafts**: write two storyboards in the files folder, following `akb raw agent-file scriptwriter references/storyboard-contract.md` and passing that agent's validator: `breakdown.json` takes the source apart, one shot per beat with its source frame, and in details its motion, props, content slot, transition and sound, marking the skeleton beats the video cannot lose; `clone.json` tells the user's content beat by beat, so its story holds on its own. Show both on the card as one `<Compare>`: the source video as `a`, `clone.json` as `b` and `breakdown.json` as `shots`.\n- **Music**: in the drafts, also follow `akb raw agent-file video-cloner references/music.md`.\n- **Changes**: a change to a beat updates that beat in both drafts.\n\n## Memory\n\nYour memory folder is `docs/kanban/memory/agents/video-cloner/`. Read it before the drafts; the current card wins.\n\n- **Motion library**: `motions/<slug>/` holds one effect that a finished template reproduced well \u2014 its source code and a `motion.md` with one line on the style it matches and one keyframe. Add one after the check only when it is reusable beyond this video.\n- **Prop library**: `props/<slug>/` holds one high-fidelity system UI piece, such as a file-picker selection box, a browser window or a window frame \u2014 its source code and a `prop.md` with one line on what it draws and one keyframe. Add one after the check.\n- **Self-contained**: a motion or prop folder carries every style and helper it uses, so it runs with only the template engine and the content file.\n- **Match first**: before writing a motion or drawing a prop, run `akb cloud template list-tags --category motion` or `--category prop`, search within the best tag, then check your own library; use your library alone when signed out or offline. Use a candidate when its style and timing match the source; build one when nothing does.\n- **Motion fit**: a motion that differs only in what the beat passes in, the theme or the beat's length is the same motion; one that differs in a value written into its code, such as a step's duration, easing or distance, gains a parameter defaulting to the old value; one that differs in its steps or what moves is a new motion.\n- **Cache**: `akb cloud template get` a catalog match into a new empty folder, make it that `<slug>/` folder in your library, replacing a local one of the same name, and update the index.\n- **Index**: `motions.md` and `props.md` list each entry as `- [<slug>](<folder>/<slug>/<file>.md) \u2014 <one line>`.\n- **Corrections**: `feedback.md` holds user corrections that change your next recipe, following \"What earns a note\" in `akb guide board`; `product.md` holds those about this product's real screens and flows.\n- **Size**: keep one entry under 1MB; leave out renders, dependencies and source videos.\n";
90464
+ var AGENT_default24 = "---\nname: video-cloner\ndescription: Breaks a video made with code down beat by beat and drafts its clone with the user's content.\nakb:\n memoryFiles:\n workspace: [motions/, props/, motions.md, props.md, feedback.md]\n session: new\n i18n:\n en:\n title: Clone storyboard\n zh:\n title: \u590D\u523B\u5206\u955C\n description: \u9010\u955C\u62C6\u89E3\u7528\u4EE3\u7801\u5236\u4F5C\u7684\u89C6\u9891\uFF0C\u5E76\u7528\u4F60\u7684\u5185\u5BB9\u5199\u51FA\u590D\u523B\u5206\u955C\u3002\n---\n\nYou break down a video made with code and draft its clone, which tells the user's content with the source's craft. Once the user approves the drafts, a template is built from them, so they must hold every beat it needs.\n\n## Terms\n\n- **Craft**: how the source is made, reproduced in the template code \u2014 beat cuts and lengths, camera moves, motion and easing, layout, transitions, type scale, the role each colour plays, where sounds land, and how product UI is shown \u2014 redrawn or captured, cropped to which module, placed where and how it moves.\n- **Content**: what the source says and whose it is, replaced by the user's \u2014 copy, data, product screens, footage, brand names, logos, illustrations, music, voice, typefaces and brand colours. When unsure, content is whatever must change if the same craft told another story.\n- **Props**: generic system UI that is neither, such as windows, browser chrome and file pickers.\n\n## Deciding\n\n- **Evidence first**: read the video itself \u2014 cut points, sampled frames, audio beats \u2014 rather than guessing from a few stills.\n- **Default with a reason**: pick every technical choice yourself and say why; ask only for what the user owns.\n- **Free only**: besides `akb cloud`, use only tools and libraries free for commercial use; never one that needs a paid plan or a company license.\n- **Never ship the source's content**: stand in for content the user has not supplied with labelled placeholders; draw props faithfully.\n- **Craft first**: match beats, timing, motion, element alignment and cursor landing points; pixel-exact output is not the goal.\n- **Rebuild, don't paste**: when the source draws its UI, redraw the few modules of the user's product a beat needs, centred, so every frame stays clean; a screenshot is only a reference. A beat that moves in the source never becomes a still.\n\n## Files\n\nFiles live in `<board-state>/assets/<card id>/`, linked from the card as `.assets/<card id>/...`; the template goes elsewhere only when the card names a folder for it. When the card names a video outside that folder, copy it in first; with no video, or none of the user's content to fill it, ask for it.\n\n## Steps\n\n- **Drafts**: write two storyboards in the files folder, following `akb raw agent-file scriptwriter references/storyboard-contract.md` and passing that agent's validator: `breakdown.json` takes the source apart, one shot per beat with its source frame, and in details its motion, props, content slot, transition and sound, marking the skeleton beats the video cannot lose; `clone.json` tells the user's content beat by beat, so its story holds on its own. Show both on the card as one `<Compare>`: the source video as `a`, `clone.json` as `b` and `breakdown.json` as `shots`.\n- **Music**: in the drafts, also follow `akb raw agent-file video-cloner references/music.md`.\n- **Changes**: a change to a beat updates that beat in both drafts.\n\n## Memory\n\nYour memory folder is `docs/kanban/memory/agents/video-cloner/`. Read it before the drafts; the current card wins.\n\n- **Motion library**: `motions/<slug>/` holds one effect that a finished template reproduced well \u2014 its source code and a `motion.md` with one line on the style it matches and one keyframe. Add one after the check only when it is reusable beyond this video.\n- **Prop library**: `props/<slug>/` holds one high-fidelity system UI piece, such as a file-picker selection box, a browser window or a window frame \u2014 its source code and a `prop.md` with one line on what it draws and one keyframe. Add one after the check.\n- **Self-contained**: a motion or prop folder carries every style and helper it uses, so it runs with only the template engine and the content file.\n- **Match first**: before writing a motion or drawing a prop, run `akb cloud template list-tags --category motion` or `--category prop`, search within the best tag, then check your own library; use your library alone when signed out or offline. Use a candidate when its style and timing match the source; build one when nothing does.\n- **Motion fit**: a motion that differs only in what the beat passes in, the theme or the beat's length is the same motion; one that differs in a value written into its code, such as a step's duration, easing or distance, gains a parameter defaulting to the old value; one that differs in its steps or what moves is a new motion.\n- **Cache**: `akb cloud template get` a catalog match into a new empty folder, make it that `<slug>/` folder in your library, replacing a local one of the same name, and update the index.\n- **Index**: `motions.md` and `props.md` list each entry as `- [<slug>](<folder>/<slug>/<file>.md) \u2014 <one line>`.\n- **Corrections**: `feedback.md` holds user corrections that change your next recipe, following \"What earns a note\" in `akb guide board`; `product.md` holds those about this product's real screens and flows.\n- **Size**: keep one entry under 1MB; leave out renders, dependencies and source videos.\n";
90224
90465
 
90225
90466
  // ../cli/src/agents/video-cloner/references/music.md
90226
90467
  var music_default = "# Music\n\nScore the clone with new music that follows the source's rhythm, never its melody.\n\n## Drafts\n\n- **Analyse**: extract the source's audio with ffmpeg and take its BPM, downbeats, onsets and energy curve with librosa.\n- **Cue sheet**: write `cues.json` in the files folder, one entry per beat id: where a section rises or falls, where accents land, and each sound effect's time and kind, plus one line on the score's mood and instrumentation as the video suggests them.\n\n## Template\n\n- **Generate**: create the default score with `akb cloud music --seconds <n> --out <file> \"<prompt>\"`, where `<n>` is the beat table's total length, which spends the user's Pro credits per track. The prompt gives the tempo, mood, instrumentation, instrumental only, and each section's time range from the cue sheet; never the source's title, artist, lyrics or audio.\n- **Align**: trim or time-stretch the track to that length so its accents land on the cue times and it ends with the last beat, and put it in the content file's music slot for the user to replace.\n- **Refused**: when Cloud refuses, leave the music slot empty and say so on the card.\n- **Clicks**: give every click in the beat table a short click sound synthesised in code.\n\n## Check\n\n- **Compare**: compare the clone's onsets and energy curve with the source's beat by beat; realign a beat whose accents miss, and regenerate only when the track's structure misses, at most three times per run.\n";
90227
90468
 
90228
90469
  // ../cli/src/agents/video-template-builder/AGENT.md
90229
- var AGENT_default24 = "---\nname: video-template-builder\ndescription: Builds a video clone's reusable beat-table template from its approved drafts, renders it once and checks it against the source beat by beat.\nakb:\n after: [video-cloner]\n session: new\n i18n:\n en:\n title: Video template\n zh:\n title: \u89C6\u9891\u6A21\u677F\n description: \u6309\u786E\u8BA4\u7684\u590D\u523B\u5206\u955C\u505A\u6210\u4FDD\u7559\u62CD\u6CD5\u3001\u53EF\u6362\u5185\u5BB9\u7684 beat \u8868\u6A21\u677F\uFF0C\u6E32\u67D3\u4E00\u652F\u5E76\u4E0E\u539F\u7247\u9010\u955C\u5BF9\u6BD4\u3002\n---\n\nBuild the template the approved drafts in ``## By `video-cloner` agent`` describe: it reproduces the source's craft and renders the user's content in place of the source's. You are done, with no further question, when the clone's comparison with the source and the render command are recorded on the card.\n\nFollow Terms, Deciding, Files and Memory in `akb raw agent-file video-cloner AGENT.md`; its memory folder is yours.\n\n## Steps\n\n- **Recipe**: write the format (size, frame rate, length, fonts, colours) to `recipe.md` in the files folder; it is your working file, never shown on the card.\n- **Template**: build a project that renders `clone.json` beat by beat, picking for each motion an animation library that fits its style; one project may combine several. The beat table and all content live in one file, holding the user's content from the start, so a beat can be removed or reordered and the content swapped without code changes; the code holds only craft and props. Its `template.md` states that the content in that file is to be replaced with the user's own and that the template reproduces only the source's craft.\n- **Cached clips**: render each beat to its own cached clip and splice the clips into the full video; a change re-renders only the clips it touches.\n- **Check**: render it once and compare it with the source beat by beat side by side on rhythm, motion and layout, and check every on-screen step makes sense to a viewer. A beat that misses the source's structure means the template is wrong: fix it and render again, never hand over the breakdown instead.\n- **Music**: in the template and check, also follow `akb raw agent-file video-cloner references/music.md`.\n- **Card**: under your section heading, show only the clone against the source as a `<Compare>` with `breakdown.json` as `shots`, and each difference left in one line, naming its beat id; append a ticked todo with the template folder's absolute path and, in backticks, its render command.\n- **Changes**: a change to a beat updates only that beat in the template.\n";
90470
+ var AGENT_default25 = "---\nname: video-template-builder\ndescription: Builds a video clone's reusable beat-table template from its approved drafts, renders it once and checks it against the source beat by beat.\nakb:\n after: [video-cloner]\n session: new\n i18n:\n en:\n title: Video template\n zh:\n title: \u89C6\u9891\u6A21\u677F\n description: \u6309\u786E\u8BA4\u7684\u590D\u523B\u5206\u955C\u505A\u6210\u4FDD\u7559\u62CD\u6CD5\u3001\u53EF\u6362\u5185\u5BB9\u7684 beat \u8868\u6A21\u677F\uFF0C\u6E32\u67D3\u4E00\u652F\u5E76\u4E0E\u539F\u7247\u9010\u955C\u5BF9\u6BD4\u3002\n---\n\nBuild the template the approved drafts in ``## By `video-cloner` agent`` describe: it reproduces the source's craft and renders the user's content in place of the source's. You are done, with no further question, when the clone's comparison with the source and the render command are recorded on the card.\n\nFollow Terms, Deciding, Files and Memory in `akb raw agent-file video-cloner AGENT.md`; its memory folder is yours.\n\n## Steps\n\n- **Recipe**: write the format (size, frame rate, length, fonts, colours) to `recipe.md` in the files folder; it is your working file, never shown on the card.\n- **Template**: build a project that renders `clone.json` beat by beat, picking for each motion an animation library that fits its style; one project may combine several. The beat table and all content live in one file, holding the user's content from the start, so a beat can be removed or reordered and the content swapped without code changes; the code holds only craft and props. Its `template.md` states that the content in that file is to be replaced with the user's own and that the template reproduces only the source's craft.\n- **Cached clips**: render each beat to its own cached clip and splice the clips into the full video; a change re-renders only the clips it touches.\n- **Check**: render it once and compare it with the source beat by beat side by side on rhythm, motion and layout, and check every on-screen step makes sense to a viewer. A beat that misses the source's structure means the template is wrong: fix it and render again, never hand over the breakdown instead.\n- **Music**: in the template and check, also follow `akb raw agent-file video-cloner references/music.md`.\n- **Card**: under your section heading, show only the clone against the source as a `<Compare>` with `breakdown.json` as `shots`, and each difference left in one line, naming its beat id; append a ticked todo with the template folder's absolute path and, in backticks, its render command.\n- **Changes**: a change to a beat updates only that beat in the template.\n";
90230
90471
 
90231
90472
  // ../cli/src/agents/workflow-writer/AGENT.md
90232
- var AGENT_default25 = "---\nname: workflow-writer\ndescription: Writes or changes a workflow, one of its preview agents or a scheduled agent, as a `WORKFLOW.md` and the agents it names.\nakb:\n i18n:\n en:\n title: Workflow\n zh:\n title: \u5DE5\u4F5C\u6D41\n description: \u6309\u4F60\u7684\u63CF\u8FF0\u5199\u51FA\u6216\u4FEE\u6539\u5DE5\u4F5C\u6D41\u53CA\u5176 Preview Agent\uFF0C\u6216\u628A\u91CD\u590D\u6027\u5DE5\u4F5C\u5199\u6210\u5B9A\u65F6 agent\u3002\n---\n\nYou write a workflow from the user's request: a kind of work the user runs again and again, where each task shows a person previews to approve before the work goes on. A request may also change one preview agent in a workflow, or ask for work that repeats on a schedule.\n\n## Read first\n\n- **The project**: when the board belongs to a project folder, read its README and agent instruction files (such as `AGENTS.md` or `CLAUDE.md`) for what it makes, and its existing outputs of this kind for their format and destination.\n- **This board**: run `akb workflow list` and read `docs/kanban/agents/`, so a new workflow does not repeat one the board already has.\n\n## What to write\n\n- **The workflow**: when the card names a workflow folder, the workflow is that folder: keep its id and change it in place. Otherwise create it with `akb workflow new`. Name it with `akb workflow rename`, and list its agents in its `WORKFLOW.md`: `previews` the agents whose drafts a person approves, `land` the agents that run unattended in order after the last approval, and `scheduled`. Its body is how a card of this kind is written.\n- **Its agents**: write each agent it names as `akb guide write-agent` says.\n- **Approvals**: default to previews a person approves once. Give an agent `after` only when its preview is too costly to make before another is approved, such as a video before its script; it then waits for that approval.\n- **Previews**: give each part a person must see its own agent, such as the screens of a UI change or the script of a video. Show only what the person decides on, and keep previews from overlapping: a UI preview already shows its text, so add no separate copy preview.\n- **Land**: add agents to `land` only for work that needs no review, such as building and merging an approved change; leave it empty when the last approved preview is the deliverable.\n- **Memory**: for each agent, decide whether a user's correction should change its next output, such as a writer's tone or a designer's look; if so, declare its memory as `akb guide write-agent` says, otherwise give it none.\n- **Changing a workflow**: edit its `WORKFLOW.md` and its agents under `docs/kanban/agents/` in place, and keep what the request does not ask to change.\n- **Preview agents it does not hold**: when the request changes a preview agent of a built-in or another workflow, run `akb workflow duplicate <id>` on that workflow and change the copy of that agent.\n- **Saving a copy**: when the card names a folder, move the copy's `WORKFLOW.md` over the one there and remove the copy's empty folder, because only that folder is saved. Never run `akb workflow delete` on the copy: it deletes the agents too.\n- **Copied schedules**: remove the copy's `scheduled` entries and their agents unless the request is about them, so that work does not run twice.\n- **Repeating work**: write a scheduled agent as `akb guide write-agent` says. It goes in this workflow, which may hold nothing else.\n\n## Check it\n\nRun `akb spec` and `akb workflow list`, and fix every problem reported for this workflow or its agents before finishing.\n\n## Card\n\nUnder your section heading, say in a few lines, for a reader who has never seen a workflow file, what the workflow is for, what each approval shows, what runs after the last one, what runs on a schedule, and what each agent remembers; then embed each file you wrote as an `<Asset>`. When the work is a copy, say that new tasks must pick the copy and that open cards stay on the original.\n";
90473
+ var AGENT_default26 = "---\nname: workflow-writer\ndescription: Writes or changes a workflow, one of its preview agents or a scheduled agent, as a `WORKFLOW.md` and the agents it names.\nakb:\n i18n:\n en:\n title: Workflow\n zh:\n title: \u5DE5\u4F5C\u6D41\n description: \u6309\u4F60\u7684\u63CF\u8FF0\u5199\u51FA\u6216\u4FEE\u6539\u5DE5\u4F5C\u6D41\u53CA\u5176 Preview Agent\uFF0C\u6216\u628A\u91CD\u590D\u6027\u5DE5\u4F5C\u5199\u6210\u5B9A\u65F6 agent\u3002\n---\n\nYou write a workflow from the user's request: a kind of work the user runs again and again, where each task shows a person previews to approve before the work goes on. A request may also change one preview agent in a workflow, or ask for work that repeats on a schedule.\n\n## Read first\n\n- **The project**: when the board belongs to a project folder, read its README and agent instruction files (such as `AGENTS.md` or `CLAUDE.md`) for what it makes, and its existing outputs of this kind for their format and destination.\n- **This board**: run `akb workflow list` and read `docs/kanban/agents/`, so a new workflow does not repeat one the board already has.\n\n## What to write\n\n- **The workflow**: when the card names a workflow folder, the workflow is that folder: keep its id and change it in place. Otherwise create it with `akb workflow new`. Name it with `akb workflow rename`, and list its agents in its `WORKFLOW.md`: `previews` the agents whose drafts a person approves, `land` the agents that run unattended in order after the last approval, and `scheduled`. Its body is how a card of this kind is written.\n- **Its agents**: write each agent it names as `akb guide write-agent` says.\n- **Approvals**: default to previews a person approves once. Give an agent `after` only when its preview is too costly to make before another is approved, such as a video before its script; it then waits for that approval.\n- **Previews**: give each part a person must see its own agent, such as the screens of a UI change or the script of a video. Show only what the person decides on, and keep previews from overlapping: a UI preview already shows its text, so add no separate copy preview.\n- **Land**: add agents to `land` only for work that needs no review, such as building and merging an approved change; leave it empty when the last approved preview is the deliverable.\n- **Memory**: for each agent, decide whether a user's correction should change its next output, such as a writer's tone or a designer's look; if so, declare its memory as `akb guide write-agent` says, otherwise give it none.\n- **Changing a workflow**: edit its `WORKFLOW.md` and its agents under `docs/kanban/agents/` in place, and keep what the request does not ask to change.\n- **Preview agents it does not hold**: when the request changes a preview agent of a built-in or another workflow, run `akb workflow duplicate <id>` on that workflow and change the copy of that agent.\n- **Saving a copy**: when the card names a folder, move the copy's `WORKFLOW.md` over the one there and remove the copy's empty folder, because only that folder is saved. Never run `akb workflow delete` on the copy: it deletes the agents too.\n- **Copied schedules**: remove the copy's `scheduled` entries and their agents unless the request is about them, so that work does not run twice.\n- **Repeating work**: write a scheduled agent as `akb guide write-agent` says. It goes in this workflow, which may hold nothing else.\n\n## Check it\n\nRun `akb spec` and `akb workflow list`, and fix every problem reported for this workflow or its agents before finishing.\n\n## Card\n\nUnder your section heading, say in a few lines, for a reader who has never seen a workflow file, what the workflow is for, what each approval shows, what runs after the last one, what runs on a schedule, and what each agent remembers; then embed each file you wrote as an `<Asset>`. When the work is a copy, say that new tasks must pick the copy and that open cards stay on the original.\n";
90233
90474
 
90234
90475
  // ../cli/src/lib/agents/bundled.ts
90235
90476
  var BUNDLED_AGENT_FILES = {
90236
- "api-designer/AGENT.md": AGENT_default,
90237
- "blog-illustrator/AGENT.md": AGENT_default2,
90238
- "blog-planner/AGENT.md": AGENT_default3,
90239
- "blog-writer/AGENT.md": AGENT_default4,
90240
- "carousel-planner/AGENT.md": AGENT_default5,
90241
- "carousel-renderer/AGENT.md": AGENT_default6,
90477
+ "builder/AGENT.md": AGENT_default,
90478
+ "api-designer/AGENT.md": AGENT_default2,
90479
+ "blog-illustrator/AGENT.md": AGENT_default3,
90480
+ "blog-planner/AGENT.md": AGENT_default4,
90481
+ "blog-writer/AGENT.md": AGENT_default5,
90482
+ "carousel-planner/AGENT.md": AGENT_default6,
90483
+ "carousel-renderer/AGENT.md": AGENT_default7,
90242
90484
  "carousel-renderer/references/slides.example.json": slides_example_default,
90243
90485
  "carousel-renderer/references/slides.schema.json": slides_schema_default,
90244
90486
  "carousel-renderer/scripts/validate-storyboard.mjs": validate_storyboard_default,
90245
- "competitor-research/AGENT.md": AGENT_default7,
90246
- "copywriting/AGENT.md": AGENT_default8,
90247
- "cover-designer/AGENT.md": AGENT_default9,
90248
- "deck-builder/AGENT.md": AGENT_default12,
90487
+ "competitor-research/AGENT.md": AGENT_default8,
90488
+ "copywriting/AGENT.md": AGENT_default9,
90489
+ "cover-designer/AGENT.md": AGENT_default10,
90490
+ "deck-builder/AGENT.md": AGENT_default13,
90249
90491
  "deck-builder/scripts/pack-pptx.mjs": pack_pptx_default,
90250
- "deck-planner/AGENT.md": AGENT_default13,
90251
- "demo-rehearser/AGENT.md": AGENT_default10,
90492
+ "deck-planner/AGENT.md": AGENT_default14,
90493
+ "demo-rehearser/AGENT.md": AGENT_default11,
90252
90494
  "deck-planner/references/slides.example.json": slides_example_default2,
90253
90495
  "deck-planner/references/slides.schema.json": slides_schema_default2,
90254
90496
  "deck-planner/scripts/validate-storyboard.mjs": validate_storyboard_default2,
90255
- "email-planner/AGENT.md": AGENT_default11,
90497
+ "email-planner/AGENT.md": AGENT_default12,
90256
90498
  "email-planner/scripts/render.mjs": render_default,
90257
- "hyperframes-editor/AGENT.md": AGENT_default15,
90499
+ "hyperframes-editor/AGENT.md": AGENT_default16,
90258
90500
  "hyperframes-editor/record.mjs": record_default,
90259
90501
  "hyperframes-editor/template-selection.md": template_selection_default,
90260
- "illustrator/AGENT.md": AGENT_default14,
90261
- "prompt-writer/AGENT.md": AGENT_default16,
90262
- "qa-manager/AGENT.md": AGENT_default17,
90263
- "schema-designer/AGENT.md": AGENT_default18,
90264
- "scriptwriter/AGENT.md": AGENT_default19,
90502
+ "illustrator/AGENT.md": AGENT_default15,
90503
+ "prompt-writer/AGENT.md": AGENT_default17,
90504
+ "qa-manager/AGENT.md": AGENT_default18,
90505
+ "schema-designer/AGENT.md": AGENT_default19,
90506
+ "scriptwriter/AGENT.md": AGENT_default20,
90265
90507
  "scriptwriter/references/index.md": references_default,
90266
90508
  "scriptwriter/references/storyboard-contract.md": storyboard_contract_default,
90267
90509
  // TypeScript reads a `.json` import as data; the bundler hands over its text.
@@ -90277,14 +90519,14 @@ var BUNDLED_AGENT_FILES = {
90277
90519
  "scriptwriter/references/composition/shader-transitions.md": shader_transitions_default,
90278
90520
  "scriptwriter/recipes/desktop-overview-focus-interact-hold/recipe.md": recipe_default,
90279
90521
  "scriptwriter/recipes/desktop-overview-focus-interact-hold/demo.tsx": demo_default,
90280
- "social-illustrator/AGENT.md": AGENT_default20,
90281
- "ui-designer/AGENT.md": AGENT_default21,
90522
+ "social-illustrator/AGENT.md": AGENT_default21,
90523
+ "ui-designer/AGENT.md": AGENT_default22,
90282
90524
  "ui-designer/references/rendered-screen.md": rendered_screen_default,
90283
- "tech-stack-advisor/AGENT.md": AGENT_default22,
90284
- "video-cloner/AGENT.md": AGENT_default23,
90525
+ "tech-stack-advisor/AGENT.md": AGENT_default23,
90526
+ "video-cloner/AGENT.md": AGENT_default24,
90285
90527
  "video-cloner/references/music.md": music_default,
90286
- "video-template-builder/AGENT.md": AGENT_default24,
90287
- "workflow-writer/AGENT.md": AGENT_default25
90528
+ "video-template-builder/AGENT.md": AGENT_default25,
90529
+ "workflow-writer/AGENT.md": AGENT_default26
90288
90530
  };
90289
90531
 
90290
90532
  // ../cli/src/lib/agents/yaml.ts
@@ -90500,16 +90742,16 @@ function sharedMemory(board) {
90500
90742
  const at2 = /^([^/]+)\/AGENT\.md$/.exec(key);
90501
90743
  if (at2) take(at2[1], text3, true);
90502
90744
  }
90503
- const root = path21.join(board, "agents");
90745
+ const root = path22.join(board, "agents");
90504
90746
  let folders2 = [];
90505
90747
  try {
90506
- folders2 = fs18.readdirSync(root, { withFileTypes: true }).filter((e2) => e2.isDirectory() && !e2.name.startsWith(".")).map((e2) => e2.name);
90748
+ folders2 = fs19.readdirSync(root, { withFileTypes: true }).filter((e2) => e2.isDirectory() && !e2.name.startsWith(".")).map((e2) => e2.name);
90507
90749
  } catch {
90508
90750
  }
90509
90751
  for (const folder of folders2.sort()) {
90510
90752
  let text3 = null;
90511
90753
  try {
90512
- text3 = fs18.readFileSync(path21.join(root, folder, "AGENT.md"), "utf8");
90754
+ text3 = fs19.readFileSync(path22.join(root, folder, "AGENT.md"), "utf8");
90513
90755
  } catch {
90514
90756
  }
90515
90757
  take(folder, text3, false);
@@ -90533,16 +90775,16 @@ function openTaskFolder(job, projectDir2) {
90533
90775
  projectDir2 ??= openCached(job.project);
90534
90776
  keepOutOfGit(projectDir2);
90535
90777
  markUsed(projectDir2);
90536
- const board = path23.join(projectDir2, ".akb", "kanban");
90537
- fs20.mkdirSync(board, { recursive: true });
90778
+ const board = path24.join(projectDir2, ".akb", "kanban");
90779
+ fs21.mkdirSync(board, { recursive: true });
90538
90780
  return { task, cloudTask: task, board, cwd: projectDir2, project: job.project, number: job.number ?? null, workflow: job.workflow ?? job.flow?.workflow ?? null };
90539
90781
  }
90540
90782
  function openAgentFolder(job, projectDir2) {
90541
90783
  if (!job.project || !job.agent) throw new Error("This job names no project.");
90542
90784
  keepOutOfGit(projectDir2);
90543
90785
  markUsed(projectDir2);
90544
- const board = path23.join(projectDir2, ".akb", "kanban");
90545
- fs20.mkdirSync(board, { recursive: true });
90786
+ const board = path24.join(projectDir2, ".akb", "kanban");
90787
+ fs21.mkdirSync(board, { recursive: true });
90546
90788
  return {
90547
90789
  task: `agent-${safe(job.agent.workflow ? `${job.agent.workflow}-${job.agent.name}` : job.agent.name)}`,
90548
90790
  cloudTask: job.agent.tasks?.[0]?.id ?? null,
@@ -90557,13 +90799,13 @@ function openDiscussionFolder(job, projectDir2) {
90557
90799
  if (!job.project) throw new Error("This job names no project.");
90558
90800
  keepOutOfGit(projectDir2);
90559
90801
  markUsed(projectDir2);
90560
- const board = path23.join(projectDir2, ".akb", "kanban");
90561
- fs20.mkdirSync(board, { recursive: true });
90802
+ const board = path24.join(projectDir2, ".akb", "kanban");
90803
+ fs21.mkdirSync(board, { recursive: true });
90562
90804
  return { task: "discussion", cloudTask: null, board, cwd: projectDir2, project: job.project, number: null, workflow: null };
90563
90805
  }
90564
90806
  var boardFolder = (ws) => ({ task: "board", cloudTask: null, board: ws.board, cwd: ws.cwd, project: ws.project, number: null, workflow: null });
90565
90807
  function keptBoard(cwd) {
90566
- const board = path23.join(cwd, ".akb", "kanban");
90808
+ const board = path24.join(cwd, ".akb", "kanban");
90567
90809
  const base = readBase({ cwd });
90568
90810
  const project = base.project ?? /^projects\/([^/]+)\//.exec(Object.keys(base.files).find((p2) => p2.startsWith("projects/")) ?? "")?.[1];
90569
90811
  return base.head_seq === null || !project ? null : boardFolder({ board, cwd, project });
@@ -90580,8 +90822,8 @@ function inFolder(dir, work) {
90580
90822
  var safe = (name) => name.replace(/[^\w.-]/g, "_") || "default";
90581
90823
  function openCached(project) {
90582
90824
  const dir = cacheDir(project);
90583
- if (fs20.existsSync(path23.join(dir, ".git"))) return dir;
90584
- fs20.mkdirSync(dir, { recursive: true });
90825
+ if (fs21.existsSync(path24.join(dir, ".git"))) return dir;
90826
+ fs21.mkdirSync(dir, { recursive: true });
90585
90827
  const git4 = (...args) => spawnSync4("git", ["-C", dir, ...args], { encoding: "utf8" }).status === 0;
90586
90828
  if (git4("init", "-q")) git4("-c", "user.name=WildCrane", "-c", "user.email=runner@wildcrane.ai", "commit", "-q", "--allow-empty", "-m", "general");
90587
90829
  return dir;
@@ -90594,56 +90836,56 @@ function keepOutOfGit(dir, folders2 = SKILL_FOLDERS) {
90594
90836
  if (inside.status !== 0) return;
90595
90837
  const missing = [".akb/", ...folders2.map((f2) => `${f2}/`)].filter((p2) => git4("check-ignore", "-q", p2).status !== 0);
90596
90838
  if (!missing.length) return;
90597
- const exclude = path23.resolve(dir, inside.stdout.trim());
90598
- fs20.mkdirSync(path23.dirname(exclude), { recursive: true });
90599
- const held4 = fs20.existsSync(exclude) ? fs20.readFileSync(exclude, "utf8") : "";
90600
- fs20.writeFileSync(exclude, `${held4}${held4 && !held4.endsWith("\n") ? "\n" : ""}${missing.join("\n")}
90839
+ const exclude = path24.resolve(dir, inside.stdout.trim());
90840
+ fs21.mkdirSync(path24.dirname(exclude), { recursive: true });
90841
+ const held4 = fs21.existsSync(exclude) ? fs21.readFileSync(exclude, "utf8") : "";
90842
+ fs21.writeFileSync(exclude, `${held4}${held4 && !held4.endsWith("\n") ? "\n" : ""}${missing.join("\n")}
90601
90843
  `);
90602
90844
  }
90603
- var cardFile = (ws) => (ws.number ? findCard(ws.board, ws.number) : null) ?? path23.join(ws.board, "todo", `${ws.number ?? 0}-task.md`);
90845
+ var cardFile = (ws) => (ws.number ? findCard(ws.board, ws.number) : null) ?? path24.join(ws.board, "todo", `${ws.number ?? 0}-task.md`);
90604
90846
  function findCard(board, number) {
90605
90847
  const named2 = new RegExp(`^${number}-`);
90606
90848
  const walk = (dir) => {
90607
90849
  let entries;
90608
90850
  try {
90609
- entries = fs20.readdirSync(dir, { withFileTypes: true });
90851
+ entries = fs21.readdirSync(dir, { withFileTypes: true });
90610
90852
  } catch {
90611
90853
  return null;
90612
90854
  }
90613
90855
  for (const e2 of entries) {
90614
90856
  if (!named2.test(e2.name)) continue;
90615
- if (e2.isFile() && e2.name.endsWith(".md")) return path23.join(dir, e2.name);
90616
- const root = path23.join(dir, e2.name, "root.md");
90617
- if (e2.isDirectory() && fs20.existsSync(root)) return root;
90857
+ if (e2.isFile() && e2.name.endsWith(".md")) return path24.join(dir, e2.name);
90858
+ const root = path24.join(dir, e2.name, "root.md");
90859
+ if (e2.isDirectory() && fs21.existsSync(root)) return root;
90618
90860
  }
90619
90861
  for (const e2 of entries) if (e2.isDirectory() && !e2.name.startsWith(".")) {
90620
- const hit = walk(path23.join(dir, e2.name));
90862
+ const hit = walk(path24.join(dir, e2.name));
90621
90863
  if (hit) return hit;
90622
90864
  }
90623
90865
  return null;
90624
90866
  };
90625
- return walk(path23.join(board, "todo")) ?? walk(path23.join(board, ".archive"));
90867
+ return walk(path24.join(board, "todo")) ?? walk(path24.join(board, ".archive"));
90626
90868
  }
90627
90869
  function akbCardOf(ws) {
90628
90870
  return ws.number && findCard(ws.board, ws.number) ? ws.number : void 0;
90629
90871
  }
90630
- var recordFile = (ws) => path23.join(ws.board, ".sync", `${ws.task}.json`);
90872
+ var recordFile = (ws) => path24.join(ws.board, ".sync", `${ws.task}.json`);
90631
90873
  function readRecord(ws) {
90632
90874
  try {
90633
- return { ...EMPTY, ...JSON.parse(fs20.readFileSync(recordFile(ws), "utf8")) };
90875
+ return { ...EMPTY, ...JSON.parse(fs21.readFileSync(recordFile(ws), "utf8")) };
90634
90876
  } catch {
90635
90877
  return structuredClone(EMPTY);
90636
90878
  }
90637
90879
  }
90638
90880
  function writeRecord(ws, record) {
90639
- fs20.mkdirSync(path23.dirname(recordFile(ws)), { recursive: true });
90640
- fs20.writeFileSync(recordFile(ws), `${JSON.stringify(record, null, 2)}
90881
+ fs21.mkdirSync(path24.dirname(recordFile(ws)), { recursive: true });
90882
+ fs21.writeFileSync(recordFile(ws), `${JSON.stringify(record, null, 2)}
90641
90883
  `);
90642
90884
  }
90643
- var baseFile = (ws) => path23.join(ws.cwd, ".akb", "base.json");
90885
+ var baseFile = (ws) => path24.join(ws.cwd, ".akb", "base.json");
90644
90886
  function readBase(ws) {
90645
90887
  try {
90646
- const base = JSON.parse(fs20.readFileSync(baseFile(ws), "utf8"));
90888
+ const base = JSON.parse(fs21.readFileSync(baseFile(ws), "utf8"));
90647
90889
  if (base.layout === "workspace") return base;
90648
90890
  } catch {
90649
90891
  }
@@ -90651,25 +90893,25 @@ function readBase(ws) {
90651
90893
  }
90652
90894
  function writeBase(ws, base) {
90653
90895
  const file2 = baseFile(ws);
90654
- fs20.mkdirSync(path23.dirname(file2), { recursive: true });
90655
- fs20.writeFileSync(`${file2}.tmp`, JSON.stringify(base));
90656
- fs20.renameSync(`${file2}.tmp`, file2);
90896
+ fs21.mkdirSync(path24.dirname(file2), { recursive: true });
90897
+ fs21.writeFileSync(`${file2}.tmp`, JSON.stringify(base));
90898
+ fs21.renameSync(`${file2}.tmp`, file2);
90657
90899
  }
90658
90900
  var hashOf = (bytes2) => crypto3.createHash("sha256").update(bytes2).digest("hex");
90659
90901
  function hashFile(file2) {
90660
90902
  try {
90661
- return hashOf(fs20.readFileSync(file2));
90903
+ return hashOf(fs21.readFileSync(file2));
90662
90904
  } catch {
90663
90905
  return null;
90664
90906
  }
90665
90907
  }
90666
90908
  function write(file2, bytes2) {
90667
- fs20.mkdirSync(path23.dirname(file2), { recursive: true });
90668
- fs20.writeFileSync(file2, bytes2);
90909
+ fs21.mkdirSync(path24.dirname(file2), { recursive: true });
90910
+ fs21.writeFileSync(file2, bytes2);
90669
90911
  }
90670
90912
  function readText(file2) {
90671
90913
  try {
90672
- return fs20.readFileSync(file2, "utf8");
90914
+ return fs21.readFileSync(file2, "utf8");
90673
90915
  } catch {
90674
90916
  return null;
90675
90917
  }
@@ -90677,14 +90919,14 @@ function readText(file2) {
90677
90919
  function filesUnder(dir, base = dir) {
90678
90920
  let entries;
90679
90921
  try {
90680
- entries = fs20.readdirSync(dir, { withFileTypes: true });
90922
+ entries = fs21.readdirSync(dir, { withFileTypes: true });
90681
90923
  } catch {
90682
90924
  return [];
90683
90925
  }
90684
90926
  return entries.flatMap((e2) => {
90685
- const full = path23.join(dir, e2.name);
90927
+ const full = path24.join(dir, e2.name);
90686
90928
  if (e2.isDirectory()) return filesUnder(full, base);
90687
- return e2.isFile() ? [path23.relative(base, full).split(path23.sep).join("/")] : [];
90929
+ return e2.isFile() ? [path24.relative(base, full).split(path24.sep).join("/")] : [];
90688
90930
  });
90689
90931
  }
90690
90932
  var kept3 = isBoardPath;
@@ -90700,18 +90942,18 @@ function viewOf(ws, base, shared) {
90700
90942
  }
90701
90943
  function assetFiles(ws, list) {
90702
90944
  const card = cardFile(ws);
90703
- return new Set(Object.keys(assetsOf(list)).map((name) => assetPath(ws, name, card)).filter((f2) => !!f2).map((f2) => path23.relative(ws.board, f2).split(path23.sep).join("/")));
90945
+ return new Set(Object.keys(assetsOf(list)).map((name) => assetPath(ws, name, card)).filter((f2) => !!f2).map((f2) => path24.relative(ws.board, f2).split(path24.sep).join("/")));
90704
90946
  }
90705
90947
  var assetsListOf = (ws, base) => base.files[rootOf(ws) + assetsFile(ws.task)]?.content;
90706
90948
  var localFiles = (ws, base) => {
90707
- const assets = assetFiles(ws, assetsListOf(ws, base));
90949
+ const assets = /* @__PURE__ */ new Set([...assetFiles(ws, assetsListOf(ws, base)), ...assetFiles(ws, readText(path24.join(ws.board, assetsFile(ws.task))))]);
90708
90950
  return filesUnder(ws.board).filter((p2) => kept3(p2) && !assets.has(p2));
90709
90951
  };
90710
90952
  var SKILL_PATH = /^skills\/([a-z0-9]+(?:-[a-z0-9]+)*)\/((?:[\w.-]+\/)*[\w.-]+)$/;
90711
90953
  function pullTools(ws, record, files) {
90712
90954
  const held4 = record.tools?.skills ?? {};
90713
90955
  const ours = new Set(Object.keys(held4).map((name) => name.split("/")[1]));
90714
- const theirs = (skill) => !ours.has(skill) && SKILL_ROOTS.some((root) => fs20.existsSync(path23.join(ws.cwd, root, skill)));
90956
+ const theirs = (skill) => !ours.has(skill) && SKILL_ROOTS.some((root) => fs21.existsSync(path24.join(ws.cwd, root, skill)));
90715
90957
  const kept4 = Object.keys(files).map((name) => SKILL_PATH.exec(name)?.[1]).filter((skill) => skill && !theirs(skill));
90716
90958
  const skills = {};
90717
90959
  for (const [name, text3] of Object.entries(files)) {
@@ -90719,7 +90961,7 @@ function pullTools(ws, record, files) {
90719
90961
  if (!at2 || at2[1] === "kanban" || !kept4.includes(at2[1]) || at2[2].split("/").includes("..")) continue;
90720
90962
  skills[name] = hashOf(text3);
90721
90963
  for (const root of SKILL_ROOTS) {
90722
- const file2 = path23.join(ws.cwd, root, at2[1], at2[2]);
90964
+ const file2 = path24.join(ws.cwd, root, at2[1], at2[2]);
90723
90965
  if (held4[name] !== skills[name] || hashFile(file2) !== skills[name]) write(file2, text3);
90724
90966
  }
90725
90967
  }
@@ -90728,8 +90970,8 @@ function pullTools(ws, record, files) {
90728
90970
  const [, skill, rest] = SKILL_PATH.exec(name) ?? [];
90729
90971
  if (!skill || !rest) continue;
90730
90972
  for (const root of SKILL_ROOTS) {
90731
- fs20.rmSync(path23.join(ws.cwd, root, skill, rest), { force: true });
90732
- if (!Object.keys(skills).some((n) => n.startsWith(`skills/${skill}/`))) fs20.rmSync(path23.join(ws.cwd, root, skill), { recursive: true, force: true });
90973
+ fs21.rmSync(path24.join(ws.cwd, root, skill, rest), { force: true });
90974
+ if (!Object.keys(skills).some((n) => n.startsWith(`skills/${skill}/`))) fs21.rmSync(path24.join(ws.cwd, root, skill), { recursive: true, force: true });
90733
90975
  }
90734
90976
  }
90735
90977
  const names2 = [...new Set(Object.keys(skills).map((name) => name.split("/")[1]))];
@@ -90757,7 +90999,7 @@ async function pullWorkflow(ws, record, base) {
90757
90999
  files = read2.value.files;
90758
91000
  found = true;
90759
91001
  if (!record.builtin) {
90760
- for (const [name, text3] of Object.entries(files)) if (isBoardPath(root + name)) write(path23.join(ws.board, root, name), text3);
91002
+ for (const [name, text3] of Object.entries(files)) if (isBoardPath(root + name)) write(path24.join(ws.board, root, name), text3);
90761
91003
  }
90762
91004
  }
90763
91005
  }
@@ -90765,14 +91007,14 @@ async function pullWorkflow(ws, record, base) {
90765
91007
  }
90766
91008
  pullAgents(ws, base, ws.workflow && !record.builtin ? files : {});
90767
91009
  const placed = readPlaced(ws);
90768
- for (const name of Object.keys(record.agents ?? {})) if (!placed[name]) fs20.rmSync(path23.join(ws.board, name), { force: true });
91010
+ for (const name of Object.keys(record.agents ?? {})) if (!placed[name]) fs21.rmSync(path24.join(ws.board, name), { force: true });
90769
91011
  record.agents = {};
90770
91012
  return { ok: true, value: void 0 };
90771
91013
  }
90772
- var agentsRecord = (ws) => path23.join(ws.board, ".sync", "agents.json");
91014
+ var agentsRecord = (ws) => path24.join(ws.board, ".sync", "agents.json");
90773
91015
  function readPlaced(ws) {
90774
91016
  try {
90775
- return JSON.parse(fs20.readFileSync(agentsRecord(ws), "utf8"));
91017
+ return JSON.parse(fs21.readFileSync(agentsRecord(ws), "utf8"));
90776
91018
  } catch {
90777
91019
  return {};
90778
91020
  }
@@ -90793,8 +91035,8 @@ function pullAgents(ws, base, own) {
90793
91035
  const names2 = /* @__PURE__ */ new Set();
90794
91036
  for (const [name, text3] of Object.entries(files)) {
90795
91037
  const agent = /^agents\/([^/]+)\//.exec(name)?.[1];
90796
- const file2 = path23.resolve(ws.board, name);
90797
- if (!agent || taken.has(agent) || !file2.startsWith(path23.join(ws.board, "agents") + path23.sep)) continue;
91038
+ const file2 = path24.resolve(ws.board, name);
91039
+ if (!agent || taken.has(agent) || !file2.startsWith(path24.join(ws.board, "agents") + path24.sep)) continue;
90798
91040
  names2.add(agent);
90799
91041
  agents[name] = text3;
90800
91042
  }
@@ -90804,42 +91046,51 @@ function pullAgents(ws, base, own) {
90804
91046
  const hashes = {};
90805
91047
  for (const [name, text3] of Object.entries(agents)) {
90806
91048
  hashes[name] = hashOf(text3);
90807
- if (hashFile(path23.join(ws.board, name)) !== hashes[name]) write(path23.join(ws.board, name), text3);
91049
+ if (hashFile(path24.join(ws.board, name)) !== hashes[name]) write(path24.join(ws.board, name), text3);
90808
91050
  }
90809
- for (const name of Object.keys(before)) if (!hashes[name]) fs20.rmSync(path23.join(ws.board, name), { force: true });
91051
+ for (const name of Object.keys(before)) if (!hashes[name]) fs21.rmSync(path24.join(ws.board, name), { force: true });
90810
91052
  write(agentsRecord(ws), `${JSON.stringify(hashes, null, 2)}
90811
91053
  `);
90812
91054
  }
90813
91055
  function pullDraftAgents(ws, record) {
90814
91056
  if (ws.workflow !== BUILDER) return;
90815
- const root = path23.join(ws.board, "workflows", draftFolder(ws.task), "agents");
91057
+ const root = path24.join(ws.board, "workflows", draftFolder(ws.task), "agents");
90816
91058
  record.agents ??= {};
90817
91059
  for (const name of filesUnder(root)) {
90818
- const text3 = fs20.readFileSync(path23.join(root, name));
91060
+ const text3 = fs21.readFileSync(path24.join(root, name));
90819
91061
  const at2 = `agents/${name}`;
90820
91062
  record.agents[at2] = hashOf(text3);
90821
- if (hashFile(path23.join(ws.board, at2)) !== record.agents[at2]) write(path23.join(ws.board, at2), text3);
91063
+ if (hashFile(path24.join(ws.board, at2)) !== record.agents[at2]) write(path24.join(ws.board, at2), text3);
90822
91064
  }
90823
91065
  }
90824
91066
  function workflowFile(ws) {
90825
91067
  if (!ws.workflow || readRecord(ws).builtin) return null;
90826
- const file2 = path23.join(ws.board, "workflows", workflowFolder(ws.workflow), "WORKFLOW.md");
90827
- return fs20.existsSync(file2) ? file2 : null;
91068
+ const file2 = path24.join(ws.board, "workflows", workflowFolder(ws.workflow), "WORKFLOW.md");
91069
+ return fs21.existsSync(file2) ? file2 : null;
90828
91070
  }
90829
- function assetPath(ws, name, card) {
90830
- if (/^[a-z][\w+.-]*:/i.test(name) || name.startsWith("/") || name.startsWith("#")) return null;
91071
+ function assetPath(ws, name, owner) {
91072
+ if (/^[a-z][\w+.-]*:/i.test(name) || name.startsWith("#") || name.startsWith("//")) return null;
91073
+ if (path24.isAbsolute(name)) throw new Error("\u8D44\u6E90\u8DEF\u5F84\u4E0D\u80FD\u662F\u7EDD\u5BF9\u8DEF\u5F84");
90831
91074
  const held4 = /^\.(assets|mockups)\//.exec(name);
90832
- if (held4) {
90833
- const state = stateDir(ws);
90834
- const file3 = state && path23.resolve(state, held4[1], name.slice(held4[0].length));
90835
- return file3 && file3.startsWith(path23.join(state, held4[1]) + path23.sep) ? file3 : null;
90836
- }
90837
- const file2 = path23.resolve(path23.dirname(card), name);
90838
- return file2.startsWith(ws.board + path23.sep) ? file2 : null;
91075
+ const state = stateDir(ws);
91076
+ const root = held4 && state ? path24.join(state, held4[1]) : ws.board;
91077
+ const file2 = held4 && state ? path24.resolve(root, name.slice(held4[0].length)) : path24.resolve(path24.dirname(owner), name);
91078
+ const roots = [ws.board, ...state ? ["assets", "mockups"].map((folder) => path24.join(state, folder, ...ws.number ? [String(ws.number)] : [])) : []];
91079
+ const inside = (file3, root2) => file3.startsWith(root2 + path24.sep);
91080
+ if (!roots.some((root2) => inside(file2, root2))) throw new Error("\u8D44\u6E90\u8DEF\u5F84\u8D8A\u754C");
91081
+ let existing = file2;
91082
+ while (!fs21.existsSync(existing) && path24.dirname(existing) !== existing) existing = path24.dirname(existing);
91083
+ const real = fs21.realpathSync(existing);
91084
+ let stateParent = state;
91085
+ while (stateParent && !fs21.existsSync(stateParent) && path24.dirname(stateParent) !== stateParent) stateParent = path24.dirname(stateParent);
91086
+ const realState = state && stateParent ? path24.resolve(fs21.realpathSync(stateParent), path24.relative(stateParent, state)) : null;
91087
+ const realRoots = [fs21.realpathSync(ws.board), ...realState ? ["assets", "mockups"].map((folder) => path24.join(realState, folder, ...ws.number ? [String(ws.number)] : [])) : []];
91088
+ if (!realRoots.some((root2) => real === root2 || inside(real, root2))) throw new Error("\u7B26\u53F7\u94FE\u63A5\u8D8A\u754C");
91089
+ return file2;
90839
91090
  }
90840
91091
  function cardAssetsDir(ws, id) {
90841
91092
  const state = stateDir(ws);
90842
- return state && path23.join(state, "assets", String(id));
91093
+ return state && path24.join(state, "assets", String(id));
90843
91094
  }
90844
91095
  function stateDir(ws) {
90845
91096
  try {
@@ -90878,13 +91129,13 @@ var pullChanges = (ws) => inFolder(ws.cwd, async () => {
90878
91129
  const p2 = f2.path.startsWith(root) ? f2.path.slice(root.length) : f2.path;
90879
91130
  const isShared = p2.startsWith(MEMORY2) && shared(p2.slice(MEMORY2.length));
90880
91131
  if (kept3(p2) && isShared === !f2.path.startsWith(root)) {
90881
- const file2 = path23.join(ws.board, p2);
91132
+ const file2 = path24.join(ws.board, p2);
90882
91133
  const here = readText(file2);
90883
91134
  if (here !== f2.content && here !== (base.files[f2.path]?.content ?? null)) {
90884
91135
  skipped = true;
90885
91136
  continue;
90886
91137
  }
90887
- if (f2.content === null) fs20.rmSync(file2, { force: true });
91138
+ if (f2.content === null) fs21.rmSync(file2, { force: true });
90888
91139
  else if (here !== f2.content) write(file2, f2.content);
90889
91140
  }
90890
91141
  if (f2.content === null) delete base.files[f2.path];
@@ -90915,13 +91166,13 @@ async function readCloud(ws) {
90915
91166
  writeBase(ws, base);
90916
91167
  const reset = (view2) => {
90917
91168
  for (const [p2, content] of view2) {
90918
- const file2 = path23.join(ws.board, p2);
91169
+ const file2 = path24.join(ws.board, p2);
90919
91170
  if (kept3(p2) && readText(file2) !== content) write(file2, content);
90920
91171
  }
90921
91172
  };
90922
91173
  const view = viewOf(ws, base, sharedMemory(ws.board));
90923
91174
  const first = (p2) => isSettingsPath(p2) && !known2.has(rootOf(ws) + p2);
90924
- for (const p2 of localFiles(ws, base)) if (!view.has(p2) && !first(p2)) fs20.rmSync(path23.join(ws.board, p2), { force: true });
91175
+ for (const p2 of localFiles(ws, base)) if (!view.has(p2) && !first(p2)) fs21.rmSync(path24.join(ws.board, p2), { force: true });
90925
91176
  reset(view);
90926
91177
  const record = readRecord(ws);
90927
91178
  const workflow = await pullWorkflow(ws, record, base);
@@ -90935,18 +91186,18 @@ async function readCloud(ws) {
90935
91186
  return { ok: true, value: void 0 };
90936
91187
  }
90937
91188
  function writeIndex(board) {
90938
- const todo = path23.join(board, "todo");
91189
+ const todo = path24.join(board, "todo");
90939
91190
  const cards = [];
90940
91191
  let entries = [];
90941
91192
  try {
90942
- entries = fs20.readdirSync(todo, { withFileTypes: true });
91193
+ entries = fs21.readdirSync(todo, { withFileTypes: true });
90943
91194
  } catch {
90944
91195
  }
90945
91196
  for (const e2 of entries) {
90946
91197
  const id = /^(\d+)-/.exec(e2.name)?.[1];
90947
91198
  if (!id) continue;
90948
91199
  const link = e2.isDirectory() ? `${e2.name}/root.md` : e2.name.endsWith(".md") ? e2.name : null;
90949
- const text4 = link && readText(path23.join(todo, link));
91200
+ const text4 = link && readText(path24.join(todo, link));
90950
91201
  if (text4 === null || !link) continue;
90951
91202
  cards.push({ id: Number(id), title: cardFront(text4).title ?? e2.name.replace(/\.md$/, ""), link });
90952
91203
  }
@@ -90961,10 +91212,15 @@ the number at the front of a filename is the task id.
90961
91212
 
90962
91213
  ${list}
90963
91214
  `;
90964
- if (readText(path23.join(todo, "README.md")) !== text3) write(path23.join(todo, "README.md"), text3);
91215
+ if (readText(path24.join(todo, "README.md")) !== text3) write(path24.join(todo, "README.md"), text3);
90965
91216
  }
90966
91217
  var workspaceOf = (ws) => readBase(ws).workspace ?? null;
90967
91218
  async function pullAssets(ws, record, assets) {
91219
+ if (ws.cloudTask && ws.number) {
91220
+ record.task = ws.cloudTask;
91221
+ record.number = ws.number;
91222
+ }
91223
+ const isCompare = (value) => !!value && typeof value === "object" && "a" in value && "b" in value;
90968
91224
  const file2 = cardFile(ws);
90969
91225
  const out = {};
90970
91226
  for (const [name, ref] of Object.entries(assets)) {
@@ -90981,14 +91237,34 @@ async function pullAssets(ws, record, assets) {
90981
91237
  out[name] = { ...held4, version };
90982
91238
  continue;
90983
91239
  }
90984
- if (held4?.artifact === ref.artifact && held4.version === version && hashFile(target) === held4.hash) {
91240
+ if (held4?.artifact === ref.artifact && held4.version === version && hashFile(target) === (held4.localHash ?? held4.hash)) {
90985
91241
  out[name] = held4;
90986
91242
  continue;
90987
91243
  }
90988
91244
  const bytes2 = await readAsset(ref.artifact, version);
90989
91245
  if (!bytes2.ok) return bytes2;
90990
- write(target, bytes2.value);
90991
- out[name] = { artifact: ref.artifact, version, hash: hashOf(bytes2.value) };
91246
+ let local = bytes2.value;
91247
+ let kind = held4?.kind;
91248
+ const dependencies = [];
91249
+ if (/\.json$/i.test(target)) {
91250
+ try {
91251
+ const data = JSON.parse(Buffer.from(local).toString());
91252
+ const restore = (src) => {
91253
+ const name2 = Object.entries(assets).find(([, r2]) => r2.artifact === src)?.[0];
91254
+ if (name2) dependencies.push(name2);
91255
+ if (!name2) return src;
91256
+ if (/^\.(assets|mockups)\//.test(name2)) return name2;
91257
+ const child = assetPath(ws, name2, file2);
91258
+ return child ? path24.relative(path24.dirname(target), child).split(path24.sep).join("/") : src;
91259
+ };
91260
+ const restored = mapReferences(mapReferences(data, "storyboard", restore), "compare", restore);
91261
+ if (dependencies.length) kind = isCompare(data) ? "compare" : "storyboard";
91262
+ local = Buffer.from(JSON.stringify(restored));
91263
+ } catch {
91264
+ }
91265
+ }
91266
+ write(target, local);
91267
+ out[name] = { ...held4, kind, dependencies: dependencies.length ? [...new Set(dependencies)] : held4?.dependencies, artifact: ref.artifact, version, hash: hashOf(bytes2.value), localHash: hashOf(local) };
90992
91268
  }
90993
91269
  record.assets = out;
90994
91270
  return { ok: true, value: void 0 };
@@ -91000,42 +91276,15 @@ async function readAsset(artifact, version) {
91000
91276
  const content = held4.value.content;
91001
91277
  return { ok: true, value: Buffer.from(typeof content === "string" ? content : JSON.stringify(content)) };
91002
91278
  }
91003
- var TYPES = {
91004
- ".png": "image/png",
91005
- ".jpg": "image/jpeg",
91006
- ".jpeg": "image/jpeg",
91007
- ".gif": "image/gif",
91008
- ".webp": "image/webp",
91009
- ".svg": "image/svg+xml",
91010
- ".mp4": "video/mp4",
91011
- ".webm": "video/webm",
91012
- ".mp3": "audio/mpeg",
91013
- ".pdf": "application/pdf",
91014
- ".html": "text/html",
91015
- ".md": "text/markdown",
91016
- ".mdx": "text/markdown",
91017
- ".mov": "video/quicktime",
91018
- ".wav": "audio/wav",
91019
- ".m4a": "audio/mp4",
91020
- ".json": "application/json",
91021
- ".txt": "text/plain"
91022
- };
91023
- function kindOf(ext) {
91024
- if (ext === ".tsx" || ext === ".html") return "html";
91025
- if (ext === ".md" || ext === ".mdx") return "markdown";
91026
- const type = TYPES[ext] ?? "";
91027
- if (type.startsWith("image/")) return "image";
91028
- return type.startsWith("video/") || type.startsWith("audio/") ? "video" : "asset";
91029
- }
91030
91279
  async function sentOf(file2) {
91031
- const ext = path23.extname(file2).toLowerCase();
91280
+ const ext = path24.extname(file2).toLowerCase();
91032
91281
  const kind = kindOf(ext);
91033
- if (kind !== "html") return { bytes: fs20.readFileSync(file2), type: TYPES[ext] ?? "application/octet-stream", kind };
91282
+ if (kind !== "html") return { bytes: fs21.readFileSync(file2), type: TYPES[ext] ?? "application/octet-stream", kind };
91034
91283
  let page;
91035
91284
  try {
91036
91285
  page = await (await Promise.resolve().then(() => (init_mockup(), mockup_exports))).drawMockup(file2);
91037
91286
  } catch (e2) {
91038
- console.error(`runner: ${path23.basename(file2)} could not be drawn: ${e2 instanceof Error ? e2.message : String(e2)}`);
91287
+ console.error(`runner: ${path24.basename(file2)} could not be drawn: ${e2 instanceof Error ? e2.message : String(e2)}`);
91039
91288
  page = UNDRAWN;
91040
91289
  }
91041
91290
  return { bytes: Buffer.from(page), type: "text/html", kind };
@@ -91049,65 +91298,111 @@ async function uploadAsset(artifact, { bytes: bytes2, type }) {
91049
91298
  const version = await createVersion(artifact, upload2.value.id);
91050
91299
  return version.ok ? { ok: true, value: version.value.version } : version;
91051
91300
  }
91052
- function namedFiles(body) {
91053
- const out = /* @__PURE__ */ new Set();
91054
- for (const m of body.matchAll(/\]\(\s*<?([^)\s>]+)>?(?:\s+"[^"]*")?\s*\)|\bsrc=["']([^"']+)["']/g)) out.add(m[1] ?? m[2]);
91055
- return [...out];
91056
- }
91057
91301
  async function pushAssets(ws, record, before) {
91302
+ if (ws.cloudTask && ws.number) {
91303
+ record.task = ws.cloudTask;
91304
+ record.number = ws.number;
91305
+ writeRecord(ws, record);
91306
+ }
91058
91307
  const file2 = cardFile(ws);
91059
91308
  const body = readText(file2);
91060
91309
  if (body === null) return { ok: true, value: void 0 };
91061
91310
  const refs = {};
91062
- const assets = {};
91063
- for (const [name, ref] of Object.entries(before)) {
91064
- if (!body.includes(name)) continue;
91065
- const target = assetPath(ws, name, file2);
91066
- const held4 = record.assets[name];
91067
- const out = target && held4 && fs20.existsSync(target) ? await sentOf(target) : null;
91068
- const hash = out && hashOf(out.bytes);
91069
- if (out && held4 && held4.kind === "html" !== (out.kind === "html")) continue;
91070
- if (held4 && out && hash !== held4.hash) {
91071
- const sent = await uploadAsset(held4.artifact, out);
91072
- if (!sent.ok) return sent;
91073
- assets[name] = { artifact: held4.artifact, version: sent.value, hash, kind: out.kind };
91074
- refs[name] = { artifact: held4.artifact, version: sent.value };
91075
- } else {
91076
- if (held4) assets[name] = held4;
91077
- refs[name] = ref;
91311
+ const canonical = (target) => {
91312
+ const real = (file3) => fs21.existsSync(file3) ? fs21.realpathSync(file3) : file3;
91313
+ target = real(target);
91314
+ const state = stateDir(ws);
91315
+ for (const folder of ["assets", "mockups"]) {
91316
+ const root = state && real(path24.join(state, folder));
91317
+ if (root && target.startsWith(root + path24.sep)) return `.${folder}/${path24.relative(root, target).split(path24.sep).join("/")}`;
91078
91318
  }
91079
- }
91080
- for (const name of namedFiles(body)) {
91081
- const target = assetPath(ws, name, file2);
91082
- if (refs[name] || !target || !fs20.statSync(target, { throwIfNoEntry: false })?.isFile()) continue;
91083
- const out = await sentOf(target);
91084
- if (!ws.cloudTask) continue;
91085
- const made = await createArtifact(ws.cloudTask, name.slice(0, 200), out.kind);
91086
- if (!made.ok) return made;
91087
- const sent = await uploadAsset(made.value.id, out);
91088
- if (!sent.ok) return sent;
91089
- assets[name] = { artifact: made.value.id, version: sent.value, hash: hashOf(out.bytes), kind: out.kind };
91090
- refs[name] = { artifact: made.value.id, version: sent.value };
91091
- }
91092
- record.assets = assets;
91093
- const list = path23.join(ws.board, assetsFile(ws.task));
91094
- if (Object.keys(refs).length || fs20.existsSync(list)) {
91319
+ return path24.relative(real(path24.dirname(file2)), target).split(path24.sep).join("/");
91320
+ };
91321
+ try {
91322
+ const graph = collect(body, file2, {
91323
+ resolve: (src, owner) => {
91324
+ const id = Object.entries(before).find(([, ref]) => ref.artifact === src);
91325
+ if (id) return { key: id[0], file: null };
91326
+ const target = assetPath(ws, src, owner);
91327
+ if (!target) return null;
91328
+ const key = owner === file2 && before[src] ? src : canonical(target);
91329
+ if (!fs21.statSync(target, { throwIfNoEntry: false })?.isFile()) {
91330
+ if (before[key]) return { key, file: null };
91331
+ throw new Error("\u660E\u786E\u5F15\u7528\u7684\u8D44\u6E90\u6587\u4EF6\u4E0D\u5B58\u5728");
91332
+ }
91333
+ return { key: canonical(fs21.realpathSync(target)), file: target };
91334
+ },
91335
+ readJSON: (target) => JSON.parse(fs21.readFileSync(target, "utf8")),
91336
+ retained: (key) => (record.assets[key]?.dependencies ?? []).map((src) => {
91337
+ const kind = record.assets[src]?.kind;
91338
+ return { src, purpose: kind === "storyboard" || kind === "compare" ? kind : "asset" };
91339
+ })
91340
+ });
91341
+ for (const node of graph) {
91342
+ const previous = [...node.aliases].map((key) => record.assets[key]).find(Boolean);
91343
+ const old = [...node.aliases].map((key) => before[key]).find(Boolean);
91344
+ if (!node.file) {
91345
+ if (old) {
91346
+ let version = old.version ?? (previous?.artifact === old.artifact ? previous.version : void 0);
91347
+ if (version === void 0) {
91348
+ const art = await readArtifact(old.artifact);
91349
+ if (!art.ok) return art;
91350
+ version = art.value.latest_version ?? 0;
91351
+ }
91352
+ for (const alias of node.aliases) refs[alias] = { artifact: old.artifact, version };
91353
+ }
91354
+ continue;
91355
+ }
91356
+ let out = await sentOf(node.file);
91357
+ if (node.kind !== "asset") {
91358
+ const data = mapReferences(node.data, node.kind, (src) => {
91359
+ const key = node.dependencies.get(src);
91360
+ return key ? refs[key]?.artifact ?? src : src;
91361
+ });
91362
+ out = { kind: node.kind, type: "application/json", bytes: Buffer.from(JSON.stringify(data)) };
91363
+ }
91364
+ const hash = hashOf(out.bytes);
91365
+ let entry = previous;
91366
+ if (!entry || entry.hash !== hash || entry.kind && entry.kind !== out.kind) {
91367
+ if (!ws.cloudTask) continue;
91368
+ let artifact = previous && (previous.kind ?? kindOf(path24.extname(node.file).toLowerCase())) === out.kind ? previous.artifact : void 0;
91369
+ if (!artifact) {
91370
+ const made = await createArtifact(ws.cloudTask, node.key.slice(0, 200), out.kind);
91371
+ if (!made.ok) return { ...made, error: `${node.key}: ${made.error}` };
91372
+ artifact = made.value.id;
91373
+ }
91374
+ const sent = await uploadAsset(artifact, out);
91375
+ if (!sent.ok) return { ...sent, error: `${node.key}: ${sent.error}` };
91376
+ entry = { artifact, version: sent.value, hash, kind: out.kind };
91377
+ }
91378
+ entry = { ...entry, localHash: hashFile(node.file) ?? void 0, dependencies: [...node.dependencies.values()] };
91379
+ for (const alias of node.aliases) {
91380
+ record.assets[alias] = entry;
91381
+ refs[alias] = { artifact: entry.artifact, version: entry.version };
91382
+ }
91383
+ writeRecord(ws, record);
91384
+ }
91385
+ } catch (error) {
91386
+ return { ok: false, code: "bad_request", error: error instanceof Error ? error.message : String(error) };
91387
+ }
91388
+ const list = path24.join(ws.board, assetsFile(ws.task));
91389
+ if (Object.keys(refs).length || fs21.existsSync(list)) {
91095
91390
  if (JSON.stringify(assetsOf(readText(list))) !== JSON.stringify(refs)) write(list, assetsText(refs));
91096
91391
  }
91097
91392
  return { ok: true, value: void 0 };
91098
91393
  }
91099
91394
  function resetSorted(ws) {
91100
- for (const sub of ["dismissed", "archived"]) fs20.rmSync(path23.join(ws.board, "triage", sub), { recursive: true, force: true });
91395
+ for (const sub of ["dismissed", "archived"]) fs21.rmSync(path24.join(ws.board, "triage", sub), { recursive: true, force: true });
91101
91396
  for (const [p2, content] of viewOf(ws, readBase(ws), sharedMemory(ws.board))) {
91102
- if (/^triage\/(?:dismissed|archived)\//.test(p2)) write(path23.join(ws.board, p2), content);
91397
+ if (/^triage\/(?:dismissed|archived)\//.test(p2)) write(path24.join(ws.board, p2), content);
91103
91398
  }
91104
91399
  }
91105
91400
  function proposeItems(ws, base) {
91106
91401
  const root = rootOf(ws);
91107
91402
  for (const p2 of localFiles(ws, base)) {
91108
91403
  if (itemAt(p2)?.state !== "open" || base.files[root + p2]) continue;
91109
- const file2 = path23.join(ws.board, p2);
91110
- const content = fs20.readFileSync(file2, "utf8");
91404
+ const file2 = path24.join(ws.board, p2);
91405
+ const content = fs21.readFileSync(file2, "utf8");
91111
91406
  const said = (key) => new RegExp(`^${key}:`, "m").test(content.split(/\n---/)[0] ?? "");
91112
91407
  const fields = {
91113
91408
  ...ws.cloudTask && !said("source_task") ? { source_task: ws.cloudTask } : {},
@@ -91147,9 +91442,20 @@ async function send2(ws, job) {
91147
91442
  const record = readRecord(ws);
91148
91443
  const base = readBase(ws);
91149
91444
  const fail = (error, conflict = false, code) => ({ ok: false, conflict, error, ...code ? {} : { offline: true } });
91150
- const assets = await pushAssets(ws, record, assetsOf(assetsListOf(ws, base)));
91151
- if (!assets.ok) return fail(assets.error, false, assets.code);
91152
- writeRecord(ws, record);
91445
+ const tasks = ws.cloudTask ? [{ folder: ws, record }] : filesUnder(path24.join(ws.board, ".sync")).filter((name) => name.endsWith(".json")).flatMap((name) => {
91446
+ try {
91447
+ const held4 = JSON.parse(fs21.readFileSync(path24.join(ws.board, ".sync", name), "utf8"));
91448
+ if (!held4.task || !TASK_ID.test(held4.task) || !held4.number) return [];
91449
+ return [{ folder: { ...ws, task: held4.task, cloudTask: held4.task, number: held4.number }, record: held4 }];
91450
+ } catch {
91451
+ return [];
91452
+ }
91453
+ });
91454
+ for (const item of tasks) {
91455
+ const assets = await pushAssets(item.folder, item.record, assetsOf(assetsListOf(item.folder, base)));
91456
+ if (!assets.ok) return fail(assets.error, false, assets.code);
91457
+ writeRecord(item.folder, item.record);
91458
+ }
91153
91459
  proposeItems(ws, base);
91154
91460
  const shared = sharedMemory(ws.board);
91155
91461
  const local = localFiles(ws, base);
@@ -91158,7 +91464,7 @@ async function send2(ws, job) {
91158
91464
  const owed = (p2) => !job.id && busy(ws.cwd) && itemAt(p2)?.state === "open" && !base.files[rootOf(ws) + p2];
91159
91465
  for (const p2 of local) {
91160
91466
  if (owed(p2)) continue;
91161
- const content = fs20.readFileSync(path23.join(ws.board, p2), "utf8");
91467
+ const content = fs21.readFileSync(path24.join(ws.board, p2), "utf8");
91162
91468
  const remote = remoteOf(ws, shared, p2);
91163
91469
  localOf.set(remote, p2);
91164
91470
  const held4 = base.files[remote];
@@ -91190,15 +91496,15 @@ async function send2(ws, job) {
91190
91496
  changes.splice(at2, 1);
91191
91497
  delete base.files[theirs.path];
91192
91498
  writeBase(ws, base);
91193
- fs20.rmSync(path23.join(ws.board, p2), { force: true });
91499
+ fs21.rmSync(path24.join(ws.board, p2), { force: true });
91194
91500
  writeIndex(ws.board);
91195
91501
  continue;
91196
91502
  }
91197
91503
  if (at2 < 0) continue;
91198
91504
  const merged = itemAt(p2) ? { content: theirs.content } : mergeFile(base.files[theirs.path]?.content ?? null, changes[at2].content, theirs.content);
91199
91505
  changes[at2] = { path: theirs.path, content: merged.content, base_seq: theirs.seq };
91200
- if (merged.content === null) fs20.rmSync(path23.join(ws.board, p2), { force: true });
91201
- else write(path23.join(ws.board, p2), merged.content);
91506
+ if (merged.content === null) fs21.rmSync(path24.join(ws.board, p2), { force: true });
91507
+ else write(path24.join(ws.board, p2), merged.content);
91202
91508
  }
91203
91509
  if (!changes.length) return { ok: true, changed: false };
91204
91510
  sent = await commitBoard(ws.project, job.id, job.machineId, changes);
@@ -91220,7 +91526,7 @@ var BUSY_MS = 3e4;
91220
91526
  function keptFolders() {
91221
91527
  let cached2 = [];
91222
91528
  try {
91223
- cached2 = fs21.readdirSync(cacheRoot()).map((name) => path24.join(cacheRoot(), name));
91529
+ cached2 = fs22.readdirSync(cacheRoot()).map((name) => path25.join(cacheRoot(), name));
91224
91530
  } catch {
91225
91531
  }
91226
91532
  return [.../* @__PURE__ */ new Set([...allowedProjects(), ...cached2])].flatMap((dir) => [keptBoard(dir)].filter((ws) => !!ws));
@@ -91256,8 +91562,8 @@ function watchBoards(machineId2, ms, lookMs = LOOK_MS, pullMs = PULL_MS, busyMs
91256
91562
  const watch = (ws) => {
91257
91563
  let watcher;
91258
91564
  try {
91259
- watcher = fs21.watch(ws.board, { recursive: true }, (_event, name) => {
91260
- const p2 = name && String(name).split(path24.sep).join("/");
91565
+ watcher = fs22.watch(ws.board, { recursive: true }, (_event, name) => {
91566
+ const p2 = name && String(name).split(path25.sep).join("/");
91261
91567
  const held4 = watched.get(ws.cwd);
91262
91568
  if (closed || !p2 || !isBoardPath(p2) || !held4) return;
91263
91569
  const job = busy(ws.cwd);
@@ -91320,8 +91626,8 @@ function watchBoards(machineId2, ms, lookMs = LOOK_MS, pullMs = PULL_MS, busyMs
91320
91626
 
91321
91627
  // src/diff.ts
91322
91628
  import { execFile as execFile2 } from "node:child_process";
91323
- import fs22 from "node:fs";
91324
- import path25 from "node:path";
91629
+ import fs23 from "node:fs";
91630
+ import path26 from "node:path";
91325
91631
  var FILE_BYTES = 256 * 1024;
91326
91632
  var TOTAL_BYTES = 1024 * 1024;
91327
91633
  var EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
@@ -91338,7 +91644,7 @@ var git2 = (dir, args, maxBuffer = 4 * TOTAL_BYTES) => new Promise(
91338
91644
  function projectDir(placed, project) {
91339
91645
  if (placed) {
91340
91646
  const real = realpath3(placed);
91341
- return real && allowedProjects().some((root) => real === root || real.startsWith(root + path25.sep)) ? real : null;
91647
+ return real && allowedProjects().some((root) => real === root || real.startsWith(root + path26.sep)) ? real : null;
91342
91648
  }
91343
91649
  let cached2;
91344
91650
  try {
@@ -91346,11 +91652,11 @@ function projectDir(placed, project) {
91346
91652
  } catch {
91347
91653
  return null;
91348
91654
  }
91349
- return fs22.existsSync(path25.join(cached2, ".git")) ? cached2 : null;
91655
+ return fs23.existsSync(path26.join(cached2, ".git")) ? cached2 : null;
91350
91656
  }
91351
91657
  function realpath3(p2) {
91352
91658
  try {
91353
- return fs22.realpathSync(p2);
91659
+ return fs23.realpathSync(p2);
91354
91660
  } catch {
91355
91661
  return null;
91356
91662
  }
@@ -91396,18 +91702,18 @@ async function answerHint(machineId2, hint2) {
91396
91702
 
91397
91703
  // ../cli/src/lib/agent/stop.ts
91398
91704
  import { spawnSync as spawnSync5 } from "node:child_process";
91399
- import fs23 from "node:fs";
91705
+ import fs24 from "node:fs";
91400
91706
  var runMark = (sessionId) => ({ name: RUN_ENV, value: sessionId });
91401
91707
  var WINDOWS3 = process.platform === "win32";
91402
91708
  function markedInProc(entry, strict = false) {
91403
91709
  const pids = [];
91404
- for (const name of fs23.readdirSync("/proc")) {
91710
+ for (const name of fs24.readdirSync("/proc")) {
91405
91711
  if (!/^\d+$/.test(name)) continue;
91406
91712
  try {
91407
- if (fs23.readFileSync(`/proc/${name}/environ`, "utf8").split("\0").includes(entry)) pids.push(Number(name));
91713
+ if (fs24.readFileSync(`/proc/${name}/environ`, "utf8").split("\0").includes(entry)) pids.push(Number(name));
91408
91714
  } catch (error) {
91409
91715
  if (strict && !["ENOENT", "ESRCH"].includes(error.code ?? "")) {
91410
- const stat = fs23.statSync(`/proc/${name}`, { throwIfNoEntry: false });
91716
+ const stat = fs24.statSync(`/proc/${name}`, { throwIfNoEntry: false });
91411
91717
  if (stat?.uid === process.getuid?.()) throw error;
91412
91718
  }
91413
91719
  }
@@ -91500,8 +91806,8 @@ function processesStopped(mark, root) {
91500
91806
  }
91501
91807
 
91502
91808
  // src/flow.ts
91503
- import fs28 from "node:fs";
91504
- import path30 from "node:path";
91809
+ import fs29 from "node:fs";
91810
+ import path31 from "node:path";
91505
91811
 
91506
91812
  // ../cli/src/lib/question-block.ts
91507
91813
  var MODES = ["single", "multi"];
@@ -91659,8 +91965,8 @@ function reviewOf(meta) {
91659
91965
  }
91660
91966
 
91661
91967
  // src/triage.ts
91662
- import fs24 from "node:fs";
91663
- import path26 from "node:path";
91968
+ import fs25 from "node:fs";
91969
+ import path27 from "node:path";
91664
91970
 
91665
91971
  // ../cli/src/lib/signals/identity.ts
91666
91972
  import { createHash } from "node:crypto";
@@ -91674,8 +91980,8 @@ function slugify(s) {
91674
91980
  }
91675
91981
 
91676
91982
  // src/triage.ts
91677
- var triageDir = (board) => path26.join(board, "triage");
91678
- var rejectedDir = (board) => path26.join(board, ".archive", REJECTIONS_TO_REVIEW);
91983
+ var triageDir = (board) => path27.join(board, "triage");
91984
+ var rejectedDir = (board) => path27.join(board, ".archive", REJECTIONS_TO_REVIEW);
91679
91985
  var SCRATCH = ["dismissed", "archived"];
91680
91986
  var stamp = (at2) => formatStamp(at2 ? new Date(at2) : /* @__PURE__ */ new Date());
91681
91987
  function slugOf(title) {
@@ -91684,7 +91990,7 @@ function slugOf(title) {
91684
91990
  function freeName(dir, title) {
91685
91991
  const stem = slugOf(title);
91686
91992
  let name = `${stem}.md`;
91687
- for (let n = 2; fs24.existsSync(path26.join(dir, name)); n++) name = `${stem}-${n}.md`;
91993
+ for (let n = 2; fs25.existsSync(path27.join(dir, name)); n++) name = `${stem}-${n}.md`;
91688
91994
  return name;
91689
91995
  }
91690
91996
  function itemText(item, fields = {}) {
@@ -91704,17 +92010,17 @@ ${item.text.trim()}
91704
92010
  `;
91705
92011
  }
91706
92012
  function writeItem(dir, item, fields = {}) {
91707
- fs24.mkdirSync(dir, { recursive: true });
92013
+ fs25.mkdirSync(dir, { recursive: true });
91708
92014
  const name = freeName(dir, item.title);
91709
- fs24.writeFileSync(path26.join(dir, name), itemText(item, item.workflow ? { workflow: workflowName(item.workflow), ...fields } : fields));
92015
+ fs25.writeFileSync(path27.join(dir, name), itemText(item, item.workflow ? { workflow: workflowName(item.workflow), ...fields } : fields));
91710
92016
  return name;
91711
92017
  }
91712
92018
  function layOut(board, rejections) {
91713
92019
  clearLaid(board);
91714
92020
  const dir = rejectedDir(board);
91715
- fs24.mkdirSync(dir, { recursive: true });
92021
+ fs25.mkdirSync(dir, { recursive: true });
91716
92022
  for (const t2 of rejections.tasks) {
91717
- fs24.writeFileSync(path26.join(dir, `${t2.number}-${slugify(t2.title)}.md`), [
92023
+ fs25.writeFileSync(path27.join(dir, `${t2.number}-${slugify(t2.title)}.md`), [
91718
92024
  "---",
91719
92025
  `title: ${yamlScalar(t2.title)}`,
91720
92026
  "status: todo",
@@ -91729,20 +92035,20 @@ function layOut(board, rejections) {
91729
92035
  ].join("\n"));
91730
92036
  }
91731
92037
  for (const item of rejections.triage) {
91732
- const sub = path26.join(triageDir(board), item.state === "dismissed" ? "dismissed" : "archived");
92038
+ const sub = path27.join(triageDir(board), item.state === "dismissed" ? "dismissed" : "archived");
91733
92039
  const fields = item.state === "dismissed" ? { dismissed_at: stamp(item.dismissed_at), dismissed_by: "user", ...item.dismissed_reason ? { dismissed_reason: item.dismissed_reason } : {} } : {};
91734
92040
  writeItem(sub, item, fields);
91735
92041
  }
91736
92042
  return { tasks: rejections.tasks.map((t2) => t2.id), triage: rejections.triage.map((i) => i.id) };
91737
92043
  }
91738
92044
  function clearLaid(board) {
91739
- fs24.rmSync(rejectedDir(board), { recursive: true, force: true });
91740
- for (const sub of SCRATCH) fs24.rmSync(path26.join(triageDir(board), sub), { recursive: true, force: true });
92045
+ fs25.rmSync(rejectedDir(board), { recursive: true, force: true });
92046
+ for (const sub of SCRATCH) fs25.rmSync(path27.join(triageDir(board), sub), { recursive: true, force: true });
91741
92047
  }
91742
92048
 
91743
92049
  // src/kanban-skill.ts
91744
- import fs25 from "node:fs";
91745
- import path27 from "node:path";
92050
+ import fs26 from "node:fs";
92051
+ import path28 from "node:path";
91746
92052
 
91747
92053
  // ../skill/SKILL.md
91748
92054
  var SKILL_default = '---\nname: kanban\ndescription: Manage the file-based task board with akb. Use for creating and planning tasks, revising or implementing cards, and managing background runs; also for "what\'s running".\n---\n\n# Kanban board\n\nThe board is worked only through `akb`. Find the command for the job in `akb help` and run\nit; never do its work by hand or from memory.\n\n- **`--print` does it here**: `akb card revise 12 --print "drop the export"` prints the whole\n flow for that card on this board; follow it to its closing steps. Without `--print` the\n same command starts a separate agent.\n- **Keys**: never read or write API keys; the user sets them with `akb agent set apiKey`.\n';
@@ -91755,24 +92061,24 @@ var CONTENT = `${SKILL_default.trimEnd()}
91755
92061
  var MANAGED = /^<!-- ai4kanban \S+ -->$/m;
91756
92062
  function prepareKanbanSkill(root) {
91757
92063
  for (const folder of SKILL_FOLDERS) {
91758
- const dir = path27.join(root, folder);
91759
- const file2 = path27.join(dir, "SKILL.md");
92064
+ const dir = path28.join(root, folder);
92065
+ const file2 = path28.join(dir, "SKILL.md");
91760
92066
  try {
91761
92067
  let linked = false;
91762
92068
  let at2 = root;
91763
92069
  for (const name of `${folder}/SKILL.md`.split("/")) {
91764
- at2 = path27.join(at2, name);
91765
- if (fs25.lstatSync(at2, { throwIfNoEntry: false })?.isSymbolicLink()) linked = true;
92070
+ at2 = path28.join(at2, name);
92071
+ if (fs26.lstatSync(at2, { throwIfNoEntry: false })?.isSymbolicLink()) linked = true;
91766
92072
  }
91767
- const held4 = fs25.existsSync(file2) ? fs25.readFileSync(file2, "utf8") : null;
91768
- const exists = fs25.lstatSync(dir, { throwIfNoEntry: false }) !== void 0;
92073
+ const held4 = fs26.existsSync(file2) ? fs26.readFileSync(file2, "utf8") : null;
92074
+ const exists = fs26.lstatSync(dir, { throwIfNoEntry: false }) !== void 0;
91769
92075
  if (linked || exists && !MANAGED.test(held4 ?? "")) {
91770
92076
  if (!held4?.trim()) throw new Error("\u7528\u6237\u81EA\u6709\u76EE\u5F55\u6216\u7B26\u53F7\u94FE\u63A5\u4E2D\u7F3A\u5C11\u53EF\u8BFB\u7684 SKILL.md");
91771
92077
  continue;
91772
92078
  }
91773
92079
  if (held4 !== CONTENT) {
91774
- fs25.mkdirSync(dir, { recursive: true });
91775
- fs25.writeFileSync(file2, CONTENT);
92080
+ fs26.mkdirSync(dir, { recursive: true });
92081
+ fs26.writeFileSync(file2, CONTENT);
91776
92082
  }
91777
92083
  } catch (e2) {
91778
92084
  throw new Error(`\u65E0\u6CD5\u51C6\u5907 ${folder}\uFF1A${e2 instanceof Error ? e2.message : String(e2)}`);
@@ -91782,8 +92088,8 @@ function prepareKanbanSkill(root) {
91782
92088
 
91783
92089
  // ../cli/src/lib/skill/hook.ts
91784
92090
  import { spawnSync as spawnSync6 } from "node:child_process";
91785
- import fs26 from "node:fs";
91786
- import path28 from "node:path";
92091
+ import fs27 from "node:fs";
92092
+ import path29 from "node:path";
91787
92093
  var MARKER = "# ai4kanban commit guard";
91788
92094
  var MARKER_LINE = new RegExp(`^${MARKER}\\b`, "m");
91789
92095
  var CHECK = [
@@ -91817,7 +92123,7 @@ function hookScript(sessions) {
91817
92123
  }
91818
92124
  var read = (file2) => {
91819
92125
  try {
91820
- return fs26.readFileSync(file2, "utf8");
92126
+ return fs27.readFileSync(file2, "utf8");
91821
92127
  } catch {
91822
92128
  return null;
91823
92129
  }
@@ -91829,7 +92135,7 @@ function hooksDir(root) {
91829
92135
  windowsHide: true
91830
92136
  });
91831
92137
  if (out.error || out.status !== 0 || !out.stdout.trim()) return null;
91832
- return path28.resolve(root, out.stdout.trim());
92138
+ return path29.resolve(root, out.stdout.trim());
91833
92139
  }
91834
92140
  function hasOwnHooksPath(root) {
91835
92141
  const out = spawnSync6("git", ["config", "--get", "core.hooksPath"], {
@@ -91839,21 +92145,21 @@ function hasOwnHooksPath(root) {
91839
92145
  });
91840
92146
  return !out.error && out.status === 0 && !!out.stdout.trim();
91841
92147
  }
91842
- var sessionsPath = (root, board) => path28.join(projectStateDir(board, root), SESSIONS_FILE);
91843
- function readCommitHook(root, board = path28.join(root, "docs", "kanban")) {
92148
+ var sessionsPath = (root, board) => path29.join(projectStateDir(board, root), SESSIONS_FILE);
92149
+ function readCommitHook(root, board = path29.join(root, "docs", "kanban")) {
91844
92150
  const dir = hooksDir(root);
91845
92151
  if (!dir) return { state: "no-git" };
91846
- const where = shown(root, path28.join(dir, "pre-commit"));
92152
+ const where = shown(root, path29.join(dir, "pre-commit"));
91847
92153
  const line = guardLine(sessionsPath(root, board));
91848
92154
  if (hasOwnHooksPath(root)) return { state: "hooks-path", path: where, line };
91849
- const file2 = path28.join(dir, "pre-commit");
91850
- if (fs26.lstatSync(file2, { throwIfNoEntry: false })?.isSymbolicLink()) return { state: "foreign", path: where, line };
92155
+ const file2 = path29.join(dir, "pre-commit");
92156
+ if (fs27.lstatSync(file2, { throwIfNoEntry: false })?.isSymbolicLink()) return { state: "foreign", path: where, line };
91851
92157
  const text3 = read(file2);
91852
92158
  if (text3 === null) return { state: "absent", path: where };
91853
92159
  return MARKER_LINE.test(text3) ? { state: "ours", path: where } : { state: "foreign", path: where, line };
91854
92160
  }
91855
- var shown = (root, p2) => (path28.relative(root, p2) || p2).split(path28.sep).join("/");
91856
- function installCommitHook(root, board = path28.join(root, "docs", "kanban")) {
92161
+ var shown = (root, p2) => (path29.relative(root, p2) || p2).split(path29.sep).join("/");
92162
+ function installCommitHook(root, board = path29.join(root, "docs", "kanban")) {
91857
92163
  try {
91858
92164
  return placeCommitHook(root, board);
91859
92165
  } catch (e2) {
@@ -91870,12 +92176,12 @@ function placeCommitHook(root, board) {
91870
92176
  ${hook.line}`
91871
92177
  };
91872
92178
  }
91873
- const file2 = path28.join(root, hook.path);
92179
+ const file2 = path29.join(root, hook.path);
91874
92180
  try {
91875
- fs26.mkdirSync(path28.dirname(file2), { recursive: true });
92181
+ fs27.mkdirSync(path29.dirname(file2), { recursive: true });
91876
92182
  const text3 = hookScript(sessionsPath(root, board));
91877
- if (read(file2) !== text3) fs26.writeFileSync(file2, text3, { mode: 493 });
91878
- fs26.chmodSync(file2, 493);
92183
+ if (read(file2) !== text3) fs27.writeFileSync(file2, text3, { mode: 493 });
92184
+ fs27.chmodSync(file2, 493);
91879
92185
  } catch (e2) {
91880
92186
  return { note: `${hook.path} could not be written \u2014 ${e2 instanceof Error ? e2.message : String(e2)}` };
91881
92187
  }
@@ -91883,8 +92189,8 @@ ${hook.line}`
91883
92189
  }
91884
92190
 
91885
92191
  // src/built.ts
91886
- import fs27 from "node:fs";
91887
- import path29 from "node:path";
92192
+ import fs28 from "node:fs";
92193
+ import path30 from "node:path";
91888
92194
 
91889
92195
  // ../cli/src/lib/spec-agent-names.ts
91890
92196
  var LEGACY_SPEC_AGENT_NAMES = {
@@ -92060,12 +92366,12 @@ function checkWorkflowFiles(value, official = false) {
92060
92366
 
92061
92367
  // src/built.ts
92062
92368
  var isBuilder = (ws) => ws.workflow === BUILDER;
92063
- var draftDir = (ws) => path29.join(ws.board, "workflows", draftFolder(ws.task));
92064
- var draftLine = (ws) => `<!-- workflow folder: ${path29.relative(ws.cwd, draftDir(ws)).split(path29.sep).join("/")}/ -->`;
92369
+ var draftDir = (ws) => path30.join(ws.board, "workflows", draftFolder(ws.task));
92370
+ var draftLine = (ws) => `<!-- workflow folder: ${path30.relative(ws.cwd, draftDir(ws)).split(path30.sep).join("/")}/ -->`;
92065
92371
  var skeleton = (name) => ["---", `name: ${/\s#/.test(name) ? JSON.stringify(name) : yamlScalar(name)}`, "needsArtifact: false", "---", ""].join("\n");
92066
92372
  async function placeDraft(ws, read2) {
92067
92373
  const dir = draftDir(ws);
92068
- if (fs27.existsSync(path29.join(dir, WORKFLOW_FILE))) return { ok: true, value: void 0 };
92374
+ if (fs28.existsSync(path30.join(dir, WORKFLOW_FILE))) return { ok: true, value: void 0 };
92069
92375
  const task = await read2.readTask(ws.task);
92070
92376
  if (!task.ok) return task;
92071
92377
  let files = { [WORKFLOW_FILE]: skeleton(task.value.title.trim() || "Untitled") };
@@ -92076,10 +92382,10 @@ async function placeDraft(ws, read2) {
92076
92382
  files = held4.value.files;
92077
92383
  }
92078
92384
  for (const [name, text3] of Object.entries(files)) {
92079
- const file2 = path29.resolve(dir, name);
92080
- if (!file2.startsWith(dir + path29.sep)) continue;
92081
- fs27.mkdirSync(path29.dirname(file2), { recursive: true });
92082
- fs27.writeFileSync(file2, text3);
92385
+ const file2 = path30.resolve(dir, name);
92386
+ if (!file2.startsWith(dir + path30.sep)) continue;
92387
+ fs28.mkdirSync(path30.dirname(file2), { recursive: true });
92388
+ fs28.writeFileSync(file2, text3);
92083
92389
  }
92084
92390
  return { ok: true, value: void 0 };
92085
92391
  }
@@ -92091,19 +92397,19 @@ async function builtWorkflow(ws, akb) {
92091
92397
  const dir = draftDir(ws);
92092
92398
  let source;
92093
92399
  try {
92094
- source = fs27.readFileSync(path29.join(dir, WORKFLOW_FILE), "utf8");
92400
+ source = fs28.readFileSync(path30.join(dir, WORKFLOW_FILE), "utf8");
92095
92401
  } catch {
92096
92402
  return fail(`workflows/${id}/${WORKFLOW_FILE} is missing.`);
92097
92403
  }
92098
92404
  const read2 = parseWorkflowFile(id, source, `workflows/${id}/${WORKFLOW_FILE}`);
92099
92405
  if ("problem" in read2) return fail(read2.problem);
92100
92406
  const names2 = named(read2.def);
92101
- fs27.rmSync(path29.join(dir, "agents"), { recursive: true, force: true });
92407
+ fs28.rmSync(path30.join(dir, "agents"), { recursive: true, force: true });
92102
92408
  for (const name of names2) {
92103
- const from = path29.join(ws.board, "agents", name);
92104
- if (fs27.existsSync(from)) fs27.cpSync(from, path29.join(dir, "agents", name), { recursive: true });
92409
+ const from = path30.join(ws.board, "agents", name);
92410
+ if (fs28.existsSync(from)) fs28.cpSync(from, path30.join(dir, "agents", name), { recursive: true });
92105
92411
  }
92106
- const files = Object.fromEntries(filesUnder(dir).map((f2) => [f2, fs27.readFileSync(path29.join(dir, f2), "utf8")]));
92412
+ const files = Object.fromEntries(filesUnder(dir).map((f2) => [f2, fs28.readFileSync(path30.join(dir, f2), "utf8")]));
92107
92413
  const checked = checkWorkflowFiles(files);
92108
92414
  if (!checked.ok) return fail(`${checked.file} ${checked.reason}.`);
92109
92415
  const run2 = async (args) => answerOf2(await runAkb(akb, [...args, "--json", "--board", ws.board], { cwd: ws.cwd, board: ws.board }));
@@ -92138,7 +92444,7 @@ function spentBy(runs) {
92138
92444
  }
92139
92445
  function reviewOf2(file2) {
92140
92446
  try {
92141
- const meta = parseFrontmatter(fs28.readFileSync(file2, "utf8")).meta;
92447
+ const meta = parseFrontmatter(fs29.readFileSync(file2, "utf8")).meta;
92142
92448
  return meta ? reviewOf(meta) : null;
92143
92449
  } catch {
92144
92450
  return null;
@@ -92146,8 +92452,8 @@ function reviewOf2(file2) {
92146
92452
  }
92147
92453
  function mcpConfig(servers, file2) {
92148
92454
  if (!Object.keys(servers).length) return null;
92149
- fs28.mkdirSync(path30.dirname(file2), { recursive: true });
92150
- fs28.writeFileSync(file2, `${JSON.stringify({ mcpServers: servers }, null, 2)}
92455
+ fs29.mkdirSync(path31.dirname(file2), { recursive: true });
92456
+ fs29.writeFileSync(file2, `${JSON.stringify({ mcpServers: servers }, null, 2)}
92151
92457
  `);
92152
92458
  return file2;
92153
92459
  }
@@ -92186,7 +92492,7 @@ function deliveryEnding(ws, id) {
92186
92492
  if (!id) return null;
92187
92493
  let held4;
92188
92494
  try {
92189
- held4 = JSON.parse(fs28.readFileSync(path30.join(ws.board, "deliveries", `${path30.basename(id)}.json`), "utf8"));
92495
+ held4 = JSON.parse(fs29.readFileSync(path31.join(ws.board, "deliveries", `${path31.basename(id)}.json`), "utf8"));
92190
92496
  } catch {
92191
92497
  return null;
92192
92498
  }
@@ -92198,16 +92504,16 @@ function deliveryEnding(ws, id) {
92198
92504
  return null;
92199
92505
  }
92200
92506
  function checkoutsOf(ws, id) {
92201
- const dir = path30.join(ws.board, "deliveries");
92507
+ const dir = path31.join(ws.board, "deliveries");
92202
92508
  let names2;
92203
92509
  try {
92204
- names2 = fs28.readdirSync(dir).filter((n) => n.endsWith(".json"));
92510
+ names2 = fs29.readdirSync(dir).filter((n) => n.endsWith(".json"));
92205
92511
  } catch {
92206
92512
  return [];
92207
92513
  }
92208
92514
  return names2.flatMap((n) => {
92209
92515
  try {
92210
- const held4 = JSON.parse(fs28.readFileSync(path30.join(dir, n), "utf8"));
92516
+ const held4 = JSON.parse(fs29.readFileSync(path31.join(dir, n), "utf8"));
92211
92517
  return held4.cardId === id && held4.worktree && held4.deliveryId ? [held4.deliveryId] : [];
92212
92518
  } catch {
92213
92519
  return [];
@@ -92271,7 +92577,7 @@ async function setUp(job, ws, deps, io) {
92271
92577
  const runtime = job.runtime;
92272
92578
  const missing = unavailable(runtime, await deps.runtimes());
92273
92579
  if (missing) return missing;
92274
- if (!fs28.existsSync(path30.join(ws.board, "config.md"))) {
92580
+ if (!fs29.existsSync(path31.join(ws.board, "config.md"))) {
92275
92581
  const init = await akb(["raw", "init"]);
92276
92582
  if (init.code !== 0) return refusal(init);
92277
92583
  }
@@ -92288,7 +92594,7 @@ async function setUp(job, ws, deps, io) {
92288
92594
  const used = await akb(["agent", "use", runtime]);
92289
92595
  if (used.code !== 0) return refusal(used);
92290
92596
  const servers = mcpServersOf(ws);
92291
- const mcpFile = path30.join(ws.board, ".sync", `${ws.task}.mcp.json`);
92597
+ const mcpFile = path31.join(ws.board, ".sync", `${ws.task}.mcp.json`);
92292
92598
  const takes = new Set(settingKeys(runtime));
92293
92599
  const given = job.settings ?? {};
92294
92600
  const values = { ...given, model: job.model ?? "" };
@@ -92388,19 +92694,19 @@ async function runAgent(job, ws, deps, io) {
92388
92694
  }
92389
92695
  function writeTranscript(job, dir) {
92390
92696
  if (!job.transcript) return "";
92391
- const file2 = path30.join(dir, "discussion.md");
92392
- fs28.mkdirSync(dir, { recursive: true });
92393
- fs28.writeFileSync(file2, `${job.transcript}
92697
+ const file2 = path31.join(dir, "discussion.md");
92698
+ fs29.mkdirSync(dir, { recursive: true });
92699
+ fs29.writeFileSync(file2, `${job.transcript}
92394
92700
  `);
92395
92701
  return file2;
92396
92702
  }
92397
92703
  function pictures(job, ws, id) {
92398
92704
  const dir = cardAssetsDir(ws, id);
92399
92705
  if (!job.attachments.length || !dir) return "";
92400
- fs28.mkdirSync(dir, { recursive: true });
92706
+ fs29.mkdirSync(dir, { recursive: true });
92401
92707
  return job.attachments.map((a3, i) => {
92402
- const file2 = `${i + 1}-${path30.basename(a3.name).replace(/[^\w.-]+/g, "-")}`;
92403
- fs28.writeFileSync(path30.join(dir, file2), Buffer.from(a3.content, "base64"));
92708
+ const file2 = `${i + 1}-${path31.basename(a3.name).replace(/[^\w.-]+/g, "-")}`;
92709
+ fs29.writeFileSync(path31.join(dir, file2), Buffer.from(a3.content, "base64"));
92404
92710
  return `
92405
92711
  <Asset src=".assets/${id}/${file2}" label="${a3.name.replace(/["<>]/g, "") || file2}" />
92406
92712
  `;
@@ -92408,7 +92714,7 @@ function pictures(job, ws, id) {
92408
92714
  }
92409
92715
  function cardFlags(ws, fields) {
92410
92716
  if (!fields) return [];
92411
- const open = (keys) => (keys ?? []).map((k) => Number(/(\d+)$/.exec(k)?.[1])).filter((n) => n > 0 && n !== ws.number && findCard(ws.board, n)?.startsWith(path30.join(ws.board, "todo") + path30.sep));
92717
+ const open = (keys) => (keys ?? []).map((k) => Number(/(\d+)$/.exec(k)?.[1])).filter((n) => n > 0 && n !== ws.number && findCard(ws.board, n)?.startsWith(path31.join(ws.board, "todo") + path31.sep));
92412
92718
  const lists = [["--modules", fields.modules ?? []], ["--related", open(fields.related)], ["--blocked-by", open(fields.blocked_by)]];
92413
92719
  return [
92414
92720
  ...fields.priority ? ["--priority", fields.priority] : [],
@@ -92442,10 +92748,10 @@ async function runFlow(job, ws, deps, io) {
92442
92748
  const placed = await placeDraft(ws, deps.api);
92443
92749
  if (!placed.ok) return fail(placed.error);
92444
92750
  }
92445
- const body = path30.join(ws.board, ".sync", `${ws.task}.request.md`);
92446
- fs28.mkdirSync(path30.dirname(body), { recursive: true });
92751
+ const body = path31.join(ws.board, ".sync", `${ws.task}.request.md`);
92752
+ fs29.mkdirSync(path31.dirname(body), { recursive: true });
92447
92753
  if (!ws.number) return fail("This task has no number yet.");
92448
- fs28.writeFileSync(body, `${said.request || title}
92754
+ fs29.writeFileSync(body, `${said.request || title}
92449
92755
  ${isBuilder(ws) ? `
92450
92756
  ${draftLine(ws)}
92451
92757
  ` : ""}${pictures(job, ws, ws.number)}`);
@@ -92463,7 +92769,7 @@ ${draftLine(ws)}
92463
92769
  ...cardFlags(ws, flow.metadata),
92464
92770
  "--json"
92465
92771
  ]));
92466
- fs28.rmSync(body, { force: true });
92772
+ fs29.rmSync(body, { force: true });
92467
92773
  if (!created.ok) return fail(created.error);
92468
92774
  id = created.value;
92469
92775
  }
@@ -92479,7 +92785,7 @@ ${draftLine(ws)}
92479
92785
  if (io.lost()) return fail("");
92480
92786
  return await workOn(card);
92481
92787
  } finally {
92482
- fs28.rmSync(path30.join(ws.board, ".sync", `${ws.task}.discussion`), { recursive: true, force: true });
92788
+ fs29.rmSync(path31.join(ws.board, ".sync", `${ws.task}.discussion`), { recursive: true, force: true });
92483
92789
  const left = working.get(key) - 1;
92484
92790
  if (left) working.set(key, left);
92485
92791
  else working.delete(key);
@@ -92496,16 +92802,24 @@ ${draftLine(ws)}
92496
92802
  }
92497
92803
  if (verb === "revise" && !text3) return fail("Say what to change.");
92498
92804
  let note = verb === "resolve" || verb === "revise" ? text3 : "";
92805
+ let continuation;
92499
92806
  if (verb === "implement" && flow.resume) {
92500
- const resumed = answerOf2(await akb(["delivery", "resume", String(id2), "--json"]));
92807
+ const restored = await akb(["delivery", "resume", String(id2), "--json"]);
92808
+ const resumed = answerOf2(restored);
92501
92809
  if (io.lost()) return fail("");
92502
92810
  if (resumed?.landed) return { ok: true, report: { card: id2 } };
92811
+ if (restored.code !== 0 && resumed?.error?.reason !== "deliveryActive") return fail(resumed?.error?.message ?? refusal(restored));
92812
+ const deliveryId = resumed?.deliveryId ?? resumed?.error?.args?.id;
92813
+ if (!deliveryId) return fail("\u627E\u4E0D\u5230\u4E0A\u6B21\u6784\u5EFA\u7684\u4EA4\u4ED8\u8BB0\u5F55\uFF0C\u65E0\u6CD5\u6062\u590D\u539F\u5BF9\u8BDD\u3002");
92814
+ const previous = answerOf2(await akb(["run", "list", "--card", String(id2), "--all", "--json"]))?.runs?.filter((r2) => ["implement", "conflict", "hook"].includes(r2.action ?? "") && r2.deliveryId === deliveryId).sort((a3, b2) => (b2.startedAt ?? 0) - (a3.startedAt ?? 0))[0];
92815
+ if (!previous) return fail("\u627E\u4E0D\u5230\u4E0A\u6B21\u6784\u5EFA\u7684\u8FD0\u884C\u8BB0\u5F55\uFF0C\u65E0\u6CD5\u6062\u590D\u539F\u5BF9\u8BDD\u3002");
92816
+ continuation = previous.status === "done" ? ["run", "log", previous.sessionId] : ["run", "resume", previous.sessionId, "--require-session"];
92503
92817
  } else if (verb === "implement" && text3) {
92504
92818
  const resolved = await akb(["card", "resolve", String(id2), "--follow", "--events", "--json", "--", text3], true);
92505
92819
  if (io.lost()) return fail("");
92506
92820
  if (resolved.code !== 0) note = text3;
92507
92821
  }
92508
- const ran = await akb(["card", verb, String(id2), "--follow", "--events", "--json", ...carryArgs(verb), ...note ? ["--", note] : []], true);
92822
+ const ran = await akb(continuation ? [...continuation, "--follow", "--events", "--json"] : ["card", verb, String(id2), "--follow", "--events", "--json", ...carryArgs(verb), ...note ? ["--", note] : []], true);
92509
92823
  if (io.lost()) return fail("");
92510
92824
  const answer = answerOf2(ran);
92511
92825
  if (verb === "implement") {
@@ -92539,7 +92853,7 @@ ${draftLine(ws)}
92539
92853
  }
92540
92854
  function carryArgs(verb) {
92541
92855
  if (!["refine", "resolve", "revise", "implement"].includes(verb)) return [];
92542
- const transcript = writeTranscript(job, path30.join(ws.board, ".sync", `${ws.task}.discussion`));
92856
+ const transcript = writeTranscript(job, path31.join(ws.board, ".sync", `${ws.task}.discussion`));
92543
92857
  return [
92544
92858
  ...job.resume ? ["--session", job.resume, ...job.fork ? ["--fork"] : []] : [],
92545
92859
  ...transcript ? ["--transcript", transcript] : []
@@ -92558,15 +92872,15 @@ async function confirmFlowStopped(ws, job, deps) {
92558
92872
  }
92559
92873
 
92560
92874
  // src/recovery.ts
92561
- import fs29 from "node:fs";
92562
- import path31 from "node:path";
92875
+ import fs30 from "node:fs";
92876
+ import path32 from "node:path";
92563
92877
  var PREPARE_REASON = "Runner update prepare.";
92564
92878
  var UPDATE_REASON = "Runner update checkpoint.";
92565
92879
  var UNCERTAIN = "\u66F4\u65B0\u4E2D\u65AD\u65F6\u6709\u64CD\u4F5C\u7ED3\u679C\u5C1A\u672A\u786E\u8BA4\uFF0C\u8BF7\u68C0\u67E5\u5DF2\u6709\u7ED3\u679C\u540E\u518D\u7EED\u8DD1\u3002";
92566
- var fileOf = (id) => path31.join(runnerHome(), "checkpoints", `${encodeURIComponent(id)}.json`);
92880
+ var fileOf = (id) => path32.join(runnerHome(), "checkpoints", `${encodeURIComponent(id)}.json`);
92567
92881
  function readCheckpoint(id) {
92568
92882
  try {
92569
- const held4 = JSON.parse(fs29.readFileSync(fileOf(id), "utf8"));
92883
+ const held4 = JSON.parse(fs30.readFileSync(fileOf(id), "utf8"));
92570
92884
  if (!held4 || !Array.isArray(held4.pending) || !held4.pending.every((id2) => typeof id2 === "string")) throw new Error("\u8FDB\u5EA6\u683C\u5F0F\u9519\u8BEF");
92571
92885
  return held4;
92572
92886
  } catch (e2) {
@@ -92576,12 +92890,12 @@ function readCheckpoint(id) {
92576
92890
  }
92577
92891
  function saveCheckpoint(id, checkpoint) {
92578
92892
  const file2 = fileOf(id);
92579
- fs29.mkdirSync(path31.dirname(file2), { recursive: true, mode: 448 });
92580
- fs29.writeFileSync(`${file2}.tmp`, JSON.stringify(checkpoint), { mode: 384 });
92581
- fs29.renameSync(`${file2}.tmp`, file2);
92893
+ fs30.mkdirSync(path32.dirname(file2), { recursive: true, mode: 448 });
92894
+ fs30.writeFileSync(`${file2}.tmp`, JSON.stringify(checkpoint), { mode: 384 });
92895
+ fs30.renameSync(`${file2}.tmp`, file2);
92582
92896
  }
92583
92897
  function clearCheckpoint(id) {
92584
- fs29.rmSync(fileOf(id), { force: true });
92898
+ fs30.rmSync(fileOf(id), { force: true });
92585
92899
  }
92586
92900
  function checkpointEvent(checkpoint, event2) {
92587
92901
  if (event2.kind === "tool_call") {
@@ -92596,16 +92910,16 @@ function checkpointEvent(checkpoint, event2) {
92596
92910
 
92597
92911
  // src/lock.ts
92598
92912
  import crypto4 from "node:crypto";
92599
- import fs30 from "node:fs";
92913
+ import fs31 from "node:fs";
92600
92914
  import net from "node:net";
92601
- import path32 from "node:path";
92915
+ import path33 from "node:path";
92602
92916
  var RETRY_MS = 5e3;
92603
92917
  var KIND_MS = 1e3;
92604
92918
  var HOLDERS = ["claude-code", "codex", "terminal"];
92605
92919
  function lockPath() {
92606
92920
  const home4 = runnerHome();
92607
92921
  if (process.platform === "win32") return `\\\\.\\pipe\\wildcrane-${crypto4.createHash("sha256").update(home4).digest("hex").slice(0, 16)}`;
92608
- return path32.join(home4, "runner.sock");
92922
+ return path33.join(home4, "runner.sock");
92609
92923
  }
92610
92924
  function holdMachine(kind, onHeld, onWaiting) {
92611
92925
  const where = lockPath();
@@ -92620,7 +92934,7 @@ function holdMachine(kind, onHeld, onWaiting) {
92620
92934
  const attempt = () => {
92621
92935
  timer = null;
92622
92936
  if (closed) return;
92623
- if (process.platform !== "win32") fs30.mkdirSync(path32.dirname(where), { recursive: true, mode: 448 });
92937
+ if (process.platform !== "win32") fs31.mkdirSync(path33.dirname(where), { recursive: true, mode: 448 });
92624
92938
  const s = net.createServer((peer) => {
92625
92939
  peers.add(peer);
92626
92940
  peer.on("error", () => {
@@ -92668,7 +92982,7 @@ function holdMachine(kind, onHeld, onWaiting) {
92668
92982
  later(0);
92669
92983
  });
92670
92984
  removeIfStill.once("error", () => {
92671
- fs30.rmSync(where, { force: true });
92985
+ fs31.rmSync(where, { force: true });
92672
92986
  later(50 + Math.random() * 200);
92673
92987
  });
92674
92988
  return;
@@ -92698,16 +93012,16 @@ function holdMachine(kind, onHeld, onWaiting) {
92698
93012
 
92699
93013
  // src/handoff.ts
92700
93014
  import crypto5 from "node:crypto";
92701
- import fs31 from "node:fs";
92702
- import path33 from "node:path";
93015
+ import fs32 from "node:fs";
93016
+ import path34 from "node:path";
92703
93017
  import { execFileSync } from "node:child_process";
92704
93018
  var digest = (value) => crypto5.createHash("sha256").update(value).digest("hex");
92705
93019
  var git3 = (cwd, ...args) => execFileSync("git", ["-C", cwd, ...args], { encoding: "utf8", timeout: 15e3, maxBuffer: 16 * 1024 * 1024, stdio: ["ignore", "pipe", "ignore"] });
92706
93020
  function authorizedFolder(folder) {
92707
93021
  if (!folder) return null;
92708
93022
  try {
92709
- const real = fs31.realpathSync(folder);
92710
- return allowedProjects().some((root) => real === root || real.startsWith(root + path33.sep)) ? real : null;
93023
+ const real = fs32.realpathSync(folder);
93024
+ return allowedProjects().some((root) => real === root || real.startsWith(root + path34.sep)) ? real : null;
92711
93025
  } catch {
92712
93026
  return null;
92713
93027
  }
@@ -92717,9 +93031,9 @@ function progressFolder(job, folder) {
92717
93031
  if (!token) return folder;
92718
93032
  if (!/^[a-zA-Z0-9-]{1,80}$/.test(token)) return null;
92719
93033
  try {
92720
- const dir = fs31.realpathSync(path33.join(folder, ".akb", "handoffs", token));
92721
- if (!dir.startsWith(fs31.realpathSync(folder) + path33.sep)) return null;
92722
- const marker = JSON.parse(fs31.readFileSync(path33.join(dir, ".akb", "handoff.json"), "utf8"));
93034
+ const dir = fs32.realpathSync(path34.join(folder, ".akb", "handoffs", token));
93035
+ if (!dir.startsWith(fs32.realpathSync(folder) + path34.sep)) return null;
93036
+ const marker = JSON.parse(fs32.readFileSync(path34.join(dir, ".akb", "handoff.json"), "utf8"));
92723
93037
  return marker.task === job.task && marker.isolation === token ? dir : null;
92724
93038
  } catch {
92725
93039
  return null;
@@ -92727,24 +93041,24 @@ function progressFolder(job, folder) {
92727
93041
  }
92728
93042
  function progressEvidence(job, folder) {
92729
93043
  try {
92730
- const board = path33.join(folder, ".akb", "kanban");
92731
- const deliveries = path33.join(board, "deliveries");
92732
- const rows = fs31.readdirSync(deliveries).filter((name) => name.endsWith(".json")).map((name) => JSON.parse(fs31.readFileSync(path33.join(deliveries, name), "utf8"))).filter((row2) => row2.cardId === job.number).sort((a3, b2) => (b2.startedAt ?? 0) - (a3.startedAt ?? 0));
93044
+ const board = path34.join(folder, ".akb", "kanban");
93045
+ const deliveries = path34.join(board, "deliveries");
93046
+ const rows = fs32.readdirSync(deliveries).filter((name) => name.endsWith(".json")).map((name) => JSON.parse(fs32.readFileSync(path34.join(deliveries, name), "utf8"))).filter((row2) => row2.cardId === job.number).sort((a3, b2) => (b2.startedAt ?? 0) - (a3.startedAt ?? 0));
92733
93047
  const row = rows[0];
92734
93048
  if (!row?.deliveryId || !row.base || !row.branch || !row.worktree || row.commitMode !== "auto") return null;
92735
- const tree = fs31.realpathSync(path33.resolve(folder, row.worktree));
92736
- if (!tree.startsWith(fs31.realpathSync(folder) + path33.sep)) return null;
93049
+ const tree = fs32.realpathSync(path34.resolve(folder, row.worktree));
93050
+ if (!tree.startsWith(fs32.realpathSync(folder) + path34.sep)) return null;
92737
93051
  if (git3(tree, "rev-parse", "--show-toplevel").trim() !== tree) return null;
92738
93052
  if (git3(tree, "branch", "--show-current").trim() !== row.branch) return null;
92739
93053
  git3(tree, "cat-file", "-e", `${row.base}^{commit}`);
92740
93054
  const changed = git3(tree, "ls-files", "-z", "--cached", "--others", "--exclude-standard").split("\0").filter(Boolean).sort();
92741
93055
  const files = changed.map((name) => {
92742
- const file2 = path33.join(tree, name);
93056
+ const file2 = path34.join(tree, name);
92743
93057
  try {
92744
- const stat = fs31.lstatSync(file2);
92745
- if (stat.isSymbolicLink()) return [name, "link", digest(fs31.readlinkSync(file2))];
93058
+ const stat = fs32.lstatSync(file2);
93059
+ if (stat.isSymbolicLink()) return [name, "link", digest(fs32.readlinkSync(file2))];
92746
93060
  if (!stat.isFile()) throw new Error("\u65E0\u6CD5\u9A8C\u8BC1\u6587\u4EF6\u7C7B\u578B");
92747
- return [name, stat.mode & 511, digest(fs31.readFileSync(file2))];
93061
+ return [name, stat.mode & 511, digest(fs32.readFileSync(file2))];
92748
93062
  } catch (e2) {
92749
93063
  if (e2.code === "ENOENT") return [name, "deleted"];
92750
93064
  throw e2;
@@ -92769,26 +93083,26 @@ function progressEvidence(job, folder) {
92769
93083
  function restartFolder(job, folder) {
92770
93084
  const token = job.handoff?.isolation ?? job.id;
92771
93085
  if (!/^[a-zA-Z0-9-]{1,80}$/.test(token)) throw new Error("\u9694\u79BB\u76EE\u5F55\u7F16\u53F7\u65E0\u6548\u3002");
92772
- const dir = path33.join(folder, ".akb", "handoffs", token);
92773
- const marker = path33.join(dir, ".akb", "handoff.json");
92774
- for (const parent of [path33.join(folder, ".akb"), path33.dirname(dir), dir]) {
92775
- if (fs31.lstatSync(parent, { throwIfNoEntry: false })?.isSymbolicLink()) throw new Error("\u9694\u79BB\u76EE\u5F55\u4E0D\u80FD\u901A\u8FC7\u7B26\u53F7\u94FE\u63A5\u521B\u5EFA\u3002");
93086
+ const dir = path34.join(folder, ".akb", "handoffs", token);
93087
+ const marker = path34.join(dir, ".akb", "handoff.json");
93088
+ for (const parent of [path34.join(folder, ".akb"), path34.dirname(dir), dir]) {
93089
+ if (fs32.lstatSync(parent, { throwIfNoEntry: false })?.isSymbolicLink()) throw new Error("\u9694\u79BB\u76EE\u5F55\u4E0D\u80FD\u901A\u8FC7\u7B26\u53F7\u94FE\u63A5\u521B\u5EFA\u3002");
92776
93090
  }
92777
- if (fs31.existsSync(dir)) {
93091
+ if (fs32.existsSync(dir)) {
92778
93092
  if (job.handoff?.recovering) {
92779
93093
  try {
92780
- const saved = JSON.parse(fs31.readFileSync(marker, "utf8"));
92781
- if (saved.task === job.task && saved.isolation === token && git3(dir, "rev-parse", "--show-toplevel").trim() === fs31.realpathSync(dir)) return dir;
93094
+ const saved = JSON.parse(fs32.readFileSync(marker, "utf8"));
93095
+ if (saved.task === job.task && saved.isolation === token && git3(dir, "rev-parse", "--show-toplevel").trim() === fs32.realpathSync(dir)) return dir;
92782
93096
  } catch {
92783
93097
  }
92784
93098
  }
92785
93099
  throw new Error("\u91CD\u542F\u76EE\u5F55\u5DF2\u5B58\u5728\uFF0C\u8BF7\u91CD\u65B0\u68C0\u67E5\u3002");
92786
93100
  }
92787
- fs31.mkdirSync(path33.dirname(dir), { recursive: true });
93101
+ fs32.mkdirSync(path34.dirname(dir), { recursive: true });
92788
93102
  git3(folder, "worktree", "add", "--detach", dir, "HEAD");
92789
- if (fs31.lstatSync(path33.dirname(marker), { throwIfNoEntry: false })?.isSymbolicLink()) throw new Error("\u9694\u79BB\u6807\u8BB0\u76EE\u5F55\u65E0\u6548\u3002");
92790
- fs31.mkdirSync(path33.dirname(marker), { recursive: true });
92791
- fs31.writeFileSync(marker, JSON.stringify({ task: job.task, isolation: token }), { flag: "wx" });
93103
+ if (fs32.lstatSync(path34.dirname(marker), { throwIfNoEntry: false })?.isSymbolicLink()) throw new Error("\u9694\u79BB\u6807\u8BB0\u76EE\u5F55\u65E0\u6548\u3002");
93104
+ fs32.mkdirSync(path34.dirname(marker), { recursive: true });
93105
+ fs32.writeFileSync(marker, JSON.stringify({ task: job.task, isolation: token }), { flag: "wx" });
92792
93106
  return dir;
92793
93107
  }
92794
93108
 
@@ -92798,27 +93112,27 @@ import fs34 from "node:fs";
92798
93112
  import path36 from "node:path";
92799
93113
 
92800
93114
  // ../cli/src/lib/agent/catalog.ts
92801
- import fs32 from "node:fs";
92802
- import path34 from "node:path";
93115
+ import fs33 from "node:fs";
93116
+ import path35 from "node:path";
92803
93117
  var CATALOGUE = "https://models.dev/api.json";
92804
93118
  var FRESH_FOR_MS = 24 * 60 * 6e4;
92805
93119
  var ANSWER_BY_MS = 2e4;
92806
93120
  function cacheFile() {
92807
- return path34.join(machineHome(), "models-dev.json");
93121
+ return path35.join(machineHome(), "models-dev.json");
92808
93122
  }
92809
93123
  var held2;
92810
93124
  function readCache() {
92811
93125
  const file2 = cacheFile();
92812
93126
  let stamp2;
92813
93127
  try {
92814
- const stat = fs32.statSync(file2);
93128
+ const stat = fs33.statSync(file2);
92815
93129
  stamp2 = `${stat.mtimeMs}:${stat.size}`;
92816
93130
  } catch {
92817
93131
  return void 0;
92818
93132
  }
92819
93133
  if (held2?.stamp === stamp2) return held2.cached;
92820
93134
  try {
92821
- const data = JSON.parse(fs32.readFileSync(file2, "utf8"));
93135
+ const data = JSON.parse(fs33.readFileSync(file2, "utf8"));
92822
93136
  if (!data || typeof data.limits !== "object" || !data.limits) return void 0;
92823
93137
  const cached2 = {
92824
93138
  fetchedAt: typeof data.fetchedAt === "number" ? data.fetchedAt : 0,
@@ -92857,8 +93171,8 @@ function refreshCatalog() {
92857
93171
  const limits = limitsIn(await answer.json());
92858
93172
  if (Object.keys(limits).length === 0) return;
92859
93173
  const file2 = cacheFile();
92860
- fs32.mkdirSync(path34.dirname(file2), { recursive: true });
92861
- fs32.writeFileSync(file2, JSON.stringify({ fetchedAt: Date.now(), limits }));
93174
+ fs33.mkdirSync(path35.dirname(file2), { recursive: true });
93175
+ fs33.writeFileSync(file2, JSON.stringify({ fetchedAt: Date.now(), limits }));
92862
93176
  } catch {
92863
93177
  }
92864
93178
  })();
@@ -92901,35 +93215,6 @@ function contextLimit(harness, model) {
92901
93215
  return widest || void 0;
92902
93216
  }
92903
93217
 
92904
- // src/ideas.ts
92905
- import fs33 from "node:fs";
92906
- import path35 from "node:path";
92907
- var ideasFileOf = (job) => path35.join(runnerHome(), "ideas", `${job.id}.json`);
92908
- async function writeIdeas(job, list) {
92909
- const workflows = [];
92910
- let after = null;
92911
- do {
92912
- const page = await list(job.discussion.workspace, after);
92913
- if (!page.ok) return { error: page.error };
92914
- for (const w2 of page.value.data) workflows.push({ id: w2.id, name: w2.names ? `${w2.names.en} / ${w2.names.zh}` : w2.name, ...w2.description ? { description: w2.description } : {} });
92915
- after = page.value.next;
92916
- } while (after);
92917
- const file2 = ideasFileOf(job);
92918
- fs33.mkdirSync(path35.dirname(file2), { recursive: true });
92919
- fs33.writeFileSync(file2, `${JSON.stringify({ workflows, ideas: job.discussion.ideas }, null, 2)}
92920
- `);
92921
- return { file: file2 };
92922
- }
92923
- function readIdeas(file2) {
92924
- try {
92925
- return JSON.parse(fs33.readFileSync(file2, "utf8")).ideas;
92926
- } catch {
92927
- return void 0;
92928
- } finally {
92929
- fs33.rmSync(file2, { force: true });
92930
- }
92931
- }
92932
-
92933
93218
  // src/jobs.ts
92934
93219
  var FLUSH_MS = 2e3;
92935
93220
  var FLUSH_CHARS = 32 * 1024;
@@ -93313,7 +93598,7 @@ async function runClaimed(job, deps, track, keep2, lease) {
93313
93598
  let run2 = null;
93314
93599
  let fresh = null;
93315
93600
  let missingSessionOnly = false;
93316
- let ideas = null;
93601
+ let goalFile = null;
93317
93602
  let discussed;
93318
93603
  try {
93319
93604
  if (task) {
@@ -93339,9 +93624,9 @@ async function runClaimed(job, deps, track, keep2, lease) {
93339
93624
  discussed = board.board;
93340
93625
  }
93341
93626
  if (job.discussion) {
93342
- const wrote = recovering && fs34.existsSync(ideasFileOf(job)) ? { file: ideasFileOf(job) } : await writeIdeas({ ...job, discussion: job.discussion }, deps.api.listWorkflows);
93627
+ const wrote = recovering && fs34.existsSync(goalsFileOf(job)) ? { file: goalsFileOf(job) } : await writeGoals({ ...job, discussion: job.discussion }, deps.api.listWorkflows);
93343
93628
  if ("error" in wrote) throw new Error(wrote.error);
93344
- ideas = wrote.file;
93629
+ goalFile = wrote.file;
93345
93630
  }
93346
93631
  if (!job.flow && !job.review) {
93347
93632
  void refreshCatalog();
@@ -93362,7 +93647,7 @@ ${prompt}` : prompt;
93362
93647
  const planned = (resume, fork, said2 = "") => {
93363
93648
  let p2 = deps.plan(job.runtime, promptOf(said2), cwd, resume, job.model, settingsOf(job), fork, job.languageLine ?? "");
93364
93649
  if (ws) p2 = { ...p2, env: { ...p2.env, ...akbEnv(deps.akb, ws.board, ws.task, job.languageLine ?? "") } };
93365
- if (ideas) p2 = { ...p2, env: { ...p2.env, ...akbEnv(deps.akb, discussed, void 0, job.languageLine ?? ""), KANBAN_IDEAS: ideas } };
93650
+ if (goalFile) p2 = { ...p2, env: { ...p2.env, ...akbEnv(deps.akb, discussed, void 0, job.languageLine ?? ""), KANBAN_GOALS: goalFile, KANBAN_IDEAS: goalFile } };
93366
93651
  if (job.chat || job.discussion) {
93367
93652
  const { CLAUDE_CODE_MAX_RETRIES: _2, ...env2 } = p2.env;
93368
93653
  p2 = { ...p2, env: env2 };
@@ -93397,10 +93682,10 @@ ${prompt}` : prompt;
93397
93682
  checkpoint.session = report2.session ?? checkpoint.session;
93398
93683
  persist();
93399
93684
  };
93400
- let savedIdeas;
93685
+ let savedGoals;
93401
93686
  const told = (said2) => {
93402
- if (ideas) savedIdeas ??= readIdeas(ideas);
93403
- return savedIdeas ? { ...said2, ideas: savedIdeas } : said2;
93687
+ if (goalFile) savedGoals ??= readGoals(goalFile);
93688
+ return savedGoals ? { ...said2, ...job.discussion?.goals !== void 0 ? { goals: savedGoals } : { ideas: savedGoals } } : said2;
93404
93689
  };
93405
93690
  track(stream.stop);
93406
93691
  const io = { show: stream.out, event: remember, track: stream.adopt, onStop: stream.onStop, lost: stream.lost };