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/README.md +6 -0
- package/dist/akb/dist/kanban.mjs +190 -170
- package/dist/akb/package.json +1 -1
- package/dist/launcher.mjs +2 -2
- package/dist/runner.mjs +812 -527
- package/package.json +2 -2
package/dist/runner.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// wildcrane-runner 0.6.
|
|
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
|
|
84005
|
-
import
|
|
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 =
|
|
84019
|
-
if (at2 !== folder && !at2.startsWith(folder +
|
|
84020
|
-
const tries =
|
|
84021
|
-
const file2 = tries.find((f2) =>
|
|
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 =
|
|
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)(
|
|
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:
|
|
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 ?
|
|
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 =
|
|
84062
|
-
return { path: at2, base:
|
|
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 =
|
|
84070
|
-
const html =
|
|
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 =
|
|
84074
|
-
if (!at3.startsWith(folder +
|
|
84075
|
-
return `<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.
|
|
84439
|
-
var AKB_VERSION = true ? "0.11.
|
|
84440
|
-
var AKB_CONTRACT = true ?
|
|
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
|
|
84448
|
+
import fs11 from "node:fs";
|
|
84449
84449
|
import os4 from "node:os";
|
|
84450
|
-
import
|
|
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.
|
|
85927
|
-
akbContract:
|
|
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
|
-
|
|
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
|
-
|
|
88193
|
-
|
|
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/
|
|
88202
|
+
// src/goals.ts
|
|
88200
88203
|
import fs9 from "node:fs";
|
|
88201
88204
|
import path12 from "node:path";
|
|
88202
|
-
|
|
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(
|
|
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
|
-
|
|
88265
|
+
fs10.mkdirSync(runnerHome(), { recursive: true, mode: 448 });
|
|
88222
88266
|
const temp = `${file()}.${process.pid}.tmp`;
|
|
88223
|
-
|
|
88267
|
+
fs10.writeFileSync(temp, `${JSON.stringify(all)}
|
|
88224
88268
|
`, { mode: 384 });
|
|
88225
|
-
|
|
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
|
|
88341
|
-
var
|
|
88342
|
-
|
|
88343
|
-
|
|
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
|
|
88350
|
-
var
|
|
88351
|
-
var
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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(
|
|
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
|
|
88853
|
-
import
|
|
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
|
|
88858
|
-
import
|
|
88920
|
+
import fs12 from "node:fs";
|
|
88921
|
+
import path15 from "node:path";
|
|
88859
88922
|
import { fileURLToPath } from "node:url";
|
|
88860
|
-
var akbHome = () =>
|
|
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: [
|
|
88927
|
+
args: [path15.join(path15.dirname(fileURLToPath(import.meta.url)), "akb", "bin", "ai4kanban.mjs")],
|
|
88865
88928
|
version: AKB_VERSION,
|
|
88866
|
-
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" ? [
|
|
88873
|
-
`] : [
|
|
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 (
|
|
88940
|
+
if (fs12.readFileSync(file2, "utf8") === text3) return dir;
|
|
88878
88941
|
} catch {
|
|
88879
88942
|
}
|
|
88880
|
-
|
|
88881
|
-
|
|
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(
|
|
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
|
|
88939
|
-
import
|
|
89001
|
+
import fs14 from "node:fs";
|
|
89002
|
+
import path17 from "node:path";
|
|
88940
89003
|
|
|
88941
89004
|
// src/folders.ts
|
|
88942
|
-
import
|
|
88943
|
-
import
|
|
88944
|
-
var projectsFile = () =>
|
|
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
|
|
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(
|
|
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 = () =>
|
|
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
|
|
89034
|
+
return path17.join(cacheRoot(), project);
|
|
88972
89035
|
}
|
|
88973
|
-
var akbDir = (dir) =>
|
|
88974
|
-
var usedFile = (dir) =>
|
|
88975
|
-
var unsentFile = (dir) =>
|
|
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
|
-
|
|
88978
|
-
|
|
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(
|
|
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) =>
|
|
89052
|
+
var unsent = (dir) => fs14.existsSync(unsentFile(dir));
|
|
88990
89053
|
function markUnsent(dir, unsent2) {
|
|
88991
|
-
if (!unsent2) return void
|
|
88992
|
-
|
|
88993
|
-
|
|
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 =
|
|
89010
|
-
const held4 = JSON.parse(
|
|
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 =
|
|
89086
|
+
entries = fs14.readdirSync(akbDir(dir));
|
|
89024
89087
|
} catch {
|
|
89025
89088
|
return true;
|
|
89026
89089
|
}
|
|
89027
|
-
for (const name of entries)
|
|
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
|
|
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 =
|
|
89120
|
+
cached2 = fs14.readdirSync(cacheRoot(), { withFileTypes: true });
|
|
89058
89121
|
} catch {
|
|
89059
89122
|
}
|
|
89060
89123
|
for (const e2 of cached2) {
|
|
89061
|
-
const dir =
|
|
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
|
-
|
|
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 =
|
|
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"])
|
|
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 =
|
|
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 =
|
|
89092
|
-
if (!e2.isDirectory() || !
|
|
89093
|
-
|
|
89094
|
-
if (!
|
|
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
|
|
89101
|
-
import
|
|
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 (
|
|
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
|
-
|
|
89191
|
+
fs15.rmSync(staging, { recursive: true, force: true });
|
|
89129
89192
|
extract(zlib.gunzipSync(tarball), staging);
|
|
89130
|
-
if (!
|
|
89131
|
-
|
|
89132
|
-
|
|
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
|
-
|
|
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 =
|
|
89165
|
-
if (!target.startsWith(
|
|
89166
|
-
if (type === "5")
|
|
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
|
-
|
|
89169
|
-
|
|
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 = () =>
|
|
89218
|
-
var akbVersionDir = (version) =>
|
|
89219
|
-
var entryOf = (dir) =>
|
|
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(
|
|
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
|
-
|
|
89293
|
+
fs16.mkdirSync(akbHome(), { recursive: true });
|
|
89231
89294
|
const staging = `${stateFile()}.${process.pid}.tmp`;
|
|
89232
|
-
|
|
89295
|
+
fs16.writeFileSync(staging, `${JSON.stringify(state, null, 2)}
|
|
89233
89296
|
`);
|
|
89234
|
-
|
|
89297
|
+
fs16.renameSync(staging, stateFile());
|
|
89235
89298
|
}
|
|
89236
89299
|
function contractOf(dir) {
|
|
89237
89300
|
try {
|
|
89238
|
-
return JSON.parse(
|
|
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) =>
|
|
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
|
-
|
|
89314
|
+
fs16.utimesSync(dir, now, now);
|
|
89252
89315
|
} catch {
|
|
89253
89316
|
}
|
|
89254
|
-
return { command: process.execPath, args: [entryOf(dir)], version: current, 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 =
|
|
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 =
|
|
89301
|
-
if (now -
|
|
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
|
|
89307
|
-
import
|
|
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
|
|
89324
|
-
import
|
|
89386
|
+
import fs17 from "node:fs";
|
|
89387
|
+
import path20 from "node:path";
|
|
89325
89388
|
var TASK = /^[\w-]{1,100}$/;
|
|
89326
|
-
var taskChatsDir = (home4 = machineHome()) =>
|
|
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
|
|
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 =
|
|
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 = () =>
|
|
89418
|
+
var reviewedFile = () => path21.join(taskChatsDir(home3()), "reviewed.json");
|
|
89356
89419
|
function readReviewed() {
|
|
89357
89420
|
try {
|
|
89358
|
-
const held4 = JSON.parse(
|
|
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
|
-
|
|
89366
|
-
|
|
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
|
-
|
|
89377
|
-
|
|
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
|
|
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
|
-
|
|
89385
|
-
|
|
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
|
-
|
|
89404
|
-
|
|
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
|
|
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)
|
|
89483
|
+
if (kept4.length) fs18.writeFileSync(file2, kept4.map((l2) => `${JSON.stringify(l2)}
|
|
89421
89484
|
`).join(""));
|
|
89422
|
-
else
|
|
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
|
|
89461
|
-
import
|
|
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
|
|
89516
|
-
import
|
|
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
|
|
89774
|
-
import
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
"
|
|
90237
|
-
"
|
|
90238
|
-
"blog-
|
|
90239
|
-
"blog-
|
|
90240
|
-
"
|
|
90241
|
-
"carousel-
|
|
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":
|
|
90246
|
-
"copywriting/AGENT.md":
|
|
90247
|
-
"cover-designer/AGENT.md":
|
|
90248
|
-
"deck-builder/AGENT.md":
|
|
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":
|
|
90251
|
-
"demo-rehearser/AGENT.md":
|
|
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":
|
|
90497
|
+
"email-planner/AGENT.md": AGENT_default12,
|
|
90256
90498
|
"email-planner/scripts/render.mjs": render_default,
|
|
90257
|
-
"hyperframes-editor/AGENT.md":
|
|
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":
|
|
90261
|
-
"prompt-writer/AGENT.md":
|
|
90262
|
-
"qa-manager/AGENT.md":
|
|
90263
|
-
"schema-designer/AGENT.md":
|
|
90264
|
-
"scriptwriter/AGENT.md":
|
|
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":
|
|
90281
|
-
"ui-designer/AGENT.md":
|
|
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":
|
|
90284
|
-
"video-cloner/AGENT.md":
|
|
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":
|
|
90287
|
-
"workflow-writer/AGENT.md":
|
|
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 =
|
|
90745
|
+
const root = path22.join(board, "agents");
|
|
90504
90746
|
let folders2 = [];
|
|
90505
90747
|
try {
|
|
90506
|
-
folders2 =
|
|
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 =
|
|
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 =
|
|
90537
|
-
|
|
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 =
|
|
90545
|
-
|
|
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 =
|
|
90561
|
-
|
|
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 =
|
|
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 (
|
|
90584
|
-
|
|
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 =
|
|
90598
|
-
|
|
90599
|
-
const held4 =
|
|
90600
|
-
|
|
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) ??
|
|
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 =
|
|
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
|
|
90616
|
-
const root =
|
|
90617
|
-
if (e2.isDirectory() &&
|
|
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(
|
|
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(
|
|
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) =>
|
|
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(
|
|
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
|
-
|
|
90640
|
-
|
|
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) =>
|
|
90885
|
+
var baseFile = (ws) => path24.join(ws.cwd, ".akb", "base.json");
|
|
90644
90886
|
function readBase(ws) {
|
|
90645
90887
|
try {
|
|
90646
|
-
const base = JSON.parse(
|
|
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
|
-
|
|
90655
|
-
|
|
90656
|
-
|
|
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(
|
|
90903
|
+
return hashOf(fs21.readFileSync(file2));
|
|
90662
90904
|
} catch {
|
|
90663
90905
|
return null;
|
|
90664
90906
|
}
|
|
90665
90907
|
}
|
|
90666
90908
|
function write(file2, bytes2) {
|
|
90667
|
-
|
|
90668
|
-
|
|
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
|
|
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 =
|
|
90922
|
+
entries = fs21.readdirSync(dir, { withFileTypes: true });
|
|
90681
90923
|
} catch {
|
|
90682
90924
|
return [];
|
|
90683
90925
|
}
|
|
90684
90926
|
return entries.flatMap((e2) => {
|
|
90685
|
-
const full =
|
|
90927
|
+
const full = path24.join(dir, e2.name);
|
|
90686
90928
|
if (e2.isDirectory()) return filesUnder(full, base);
|
|
90687
|
-
return e2.isFile() ? [
|
|
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) =>
|
|
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) =>
|
|
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 =
|
|
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
|
-
|
|
90732
|
-
if (!Object.keys(skills).some((n) => n.startsWith(`skills/${skill}/`)))
|
|
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(
|
|
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])
|
|
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) =>
|
|
91014
|
+
var agentsRecord = (ws) => path24.join(ws.board, ".sync", "agents.json");
|
|
90773
91015
|
function readPlaced(ws) {
|
|
90774
91016
|
try {
|
|
90775
|
-
return JSON.parse(
|
|
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 =
|
|
90797
|
-
if (!agent || taken.has(agent) || !file2.startsWith(
|
|
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(
|
|
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])
|
|
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 =
|
|
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 =
|
|
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(
|
|
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 =
|
|
90827
|
-
return
|
|
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,
|
|
90830
|
-
if (/^[a-z][\w+.-]*:/i.test(name) || name.startsWith("
|
|
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
|
-
|
|
90833
|
-
|
|
90834
|
-
|
|
90835
|
-
|
|
90836
|
-
|
|
90837
|
-
|
|
90838
|
-
|
|
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 &&
|
|
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 =
|
|
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)
|
|
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 =
|
|
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))
|
|
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 =
|
|
91189
|
+
const todo = path24.join(board, "todo");
|
|
90939
91190
|
const cards = [];
|
|
90940
91191
|
let entries = [];
|
|
90941
91192
|
try {
|
|
90942
|
-
entries =
|
|
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(
|
|
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(
|
|
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
|
-
|
|
90991
|
-
|
|
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 =
|
|
91280
|
+
const ext = path24.extname(file2).toLowerCase();
|
|
91032
91281
|
const kind = kindOf(ext);
|
|
91033
|
-
if (kind !== "html") return { bytes:
|
|
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: ${
|
|
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
|
|
91063
|
-
|
|
91064
|
-
|
|
91065
|
-
const
|
|
91066
|
-
const
|
|
91067
|
-
|
|
91068
|
-
|
|
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
|
-
|
|
91081
|
-
|
|
91082
|
-
|
|
91083
|
-
|
|
91084
|
-
|
|
91085
|
-
|
|
91086
|
-
|
|
91087
|
-
|
|
91088
|
-
|
|
91089
|
-
|
|
91090
|
-
|
|
91091
|
-
|
|
91092
|
-
|
|
91093
|
-
|
|
91094
|
-
|
|
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"])
|
|
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(
|
|
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 =
|
|
91110
|
-
const content =
|
|
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
|
|
91151
|
-
|
|
91152
|
-
|
|
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 =
|
|
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
|
-
|
|
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)
|
|
91201
|
-
else write(
|
|
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 =
|
|
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 =
|
|
91260
|
-
const p2 = name && String(name).split(
|
|
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
|
|
91324
|
-
import
|
|
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 +
|
|
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
|
|
91655
|
+
return fs23.existsSync(path26.join(cached2, ".git")) ? cached2 : null;
|
|
91350
91656
|
}
|
|
91351
91657
|
function realpath3(p2) {
|
|
91352
91658
|
try {
|
|
91353
|
-
return
|
|
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
|
|
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
|
|
91710
|
+
for (const name of fs24.readdirSync("/proc")) {
|
|
91405
91711
|
if (!/^\d+$/.test(name)) continue;
|
|
91406
91712
|
try {
|
|
91407
|
-
if (
|
|
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 =
|
|
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
|
|
91504
|
-
import
|
|
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
|
|
91663
|
-
import
|
|
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) =>
|
|
91678
|
-
var rejectedDir = (board) =>
|
|
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;
|
|
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
|
-
|
|
92013
|
+
fs25.mkdirSync(dir, { recursive: true });
|
|
91708
92014
|
const name = freeName(dir, item.title);
|
|
91709
|
-
|
|
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
|
-
|
|
92021
|
+
fs25.mkdirSync(dir, { recursive: true });
|
|
91716
92022
|
for (const t2 of rejections.tasks) {
|
|
91717
|
-
|
|
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 =
|
|
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
|
-
|
|
91740
|
-
for (const sub of SCRATCH)
|
|
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
|
|
91745
|
-
import
|
|
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 =
|
|
91759
|
-
const file2 =
|
|
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 =
|
|
91765
|
-
if (
|
|
92070
|
+
at2 = path28.join(at2, name);
|
|
92071
|
+
if (fs26.lstatSync(at2, { throwIfNoEntry: false })?.isSymbolicLink()) linked = true;
|
|
91766
92072
|
}
|
|
91767
|
-
const held4 =
|
|
91768
|
-
const exists =
|
|
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
|
-
|
|
91775
|
-
|
|
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
|
|
91786
|
-
import
|
|
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
|
|
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
|
|
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) =>
|
|
91843
|
-
function readCommitHook(root, board =
|
|
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,
|
|
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 =
|
|
91850
|
-
if (
|
|
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) => (
|
|
91856
|
-
function installCommitHook(root, board =
|
|
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 =
|
|
92179
|
+
const file2 = path29.join(root, hook.path);
|
|
91874
92180
|
try {
|
|
91875
|
-
|
|
92181
|
+
fs27.mkdirSync(path29.dirname(file2), { recursive: true });
|
|
91876
92182
|
const text3 = hookScript(sessionsPath(root, board));
|
|
91877
|
-
if (read(file2) !== text3)
|
|
91878
|
-
|
|
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
|
|
91887
|
-
import
|
|
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) =>
|
|
92064
|
-
var draftLine = (ws) => `<!-- workflow folder: ${
|
|
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 (
|
|
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 =
|
|
92080
|
-
if (!file2.startsWith(dir +
|
|
92081
|
-
|
|
92082
|
-
|
|
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 =
|
|
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
|
-
|
|
92407
|
+
fs28.rmSync(path30.join(dir, "agents"), { recursive: true, force: true });
|
|
92102
92408
|
for (const name of names2) {
|
|
92103
|
-
const from =
|
|
92104
|
-
if (
|
|
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,
|
|
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(
|
|
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
|
-
|
|
92150
|
-
|
|
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(
|
|
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 =
|
|
92507
|
+
const dir = path31.join(ws.board, "deliveries");
|
|
92202
92508
|
let names2;
|
|
92203
92509
|
try {
|
|
92204
|
-
names2 =
|
|
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(
|
|
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 (!
|
|
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 =
|
|
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 =
|
|
92392
|
-
|
|
92393
|
-
|
|
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
|
-
|
|
92706
|
+
fs29.mkdirSync(dir, { recursive: true });
|
|
92401
92707
|
return job.attachments.map((a3, i) => {
|
|
92402
|
-
const file2 = `${i + 1}-${
|
|
92403
|
-
|
|
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(
|
|
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 =
|
|
92446
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
|
92562
|
-
import
|
|
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) =>
|
|
92880
|
+
var fileOf = (id) => path32.join(runnerHome(), "checkpoints", `${encodeURIComponent(id)}.json`);
|
|
92567
92881
|
function readCheckpoint(id) {
|
|
92568
92882
|
try {
|
|
92569
|
-
const held4 = JSON.parse(
|
|
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
|
-
|
|
92580
|
-
|
|
92581
|
-
|
|
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
|
-
|
|
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
|
|
92913
|
+
import fs31 from "node:fs";
|
|
92600
92914
|
import net from "node:net";
|
|
92601
|
-
import
|
|
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
|
|
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")
|
|
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
|
-
|
|
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
|
|
92702
|
-
import
|
|
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 =
|
|
92710
|
-
return allowedProjects().some((root) => real === root || real.startsWith(root +
|
|
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 =
|
|
92721
|
-
if (!dir.startsWith(
|
|
92722
|
-
const marker = JSON.parse(
|
|
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 =
|
|
92731
|
-
const deliveries =
|
|
92732
|
-
const rows =
|
|
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 =
|
|
92736
|
-
if (!tree.startsWith(
|
|
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 =
|
|
93056
|
+
const file2 = path34.join(tree, name);
|
|
92743
93057
|
try {
|
|
92744
|
-
const stat =
|
|
92745
|
-
if (stat.isSymbolicLink()) return [name, "link", digest(
|
|
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(
|
|
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 =
|
|
92773
|
-
const marker =
|
|
92774
|
-
for (const parent of [
|
|
92775
|
-
if (
|
|
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 (
|
|
93091
|
+
if (fs32.existsSync(dir)) {
|
|
92778
93092
|
if (job.handoff?.recovering) {
|
|
92779
93093
|
try {
|
|
92780
|
-
const saved = JSON.parse(
|
|
92781
|
-
if (saved.task === job.task && saved.isolation === token && git3(dir, "rev-parse", "--show-toplevel").trim() ===
|
|
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
|
-
|
|
93101
|
+
fs32.mkdirSync(path34.dirname(dir), { recursive: true });
|
|
92788
93102
|
git3(folder, "worktree", "add", "--detach", dir, "HEAD");
|
|
92789
|
-
if (
|
|
92790
|
-
|
|
92791
|
-
|
|
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
|
|
92802
|
-
import
|
|
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
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
92861
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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 (
|
|
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
|
|
93685
|
+
let savedGoals;
|
|
93401
93686
|
const told = (said2) => {
|
|
93402
|
-
if (
|
|
93403
|
-
return
|
|
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 };
|