wildcrane-runner 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/runner.mjs CHANGED
@@ -1,4 +1,4 @@
1
- // wildcrane-runner 0.5.0 — built by runner/scripts/build.mjs. Do not edit.
1
+ // wildcrane-runner 0.6.1 — 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;
@@ -28,9 +28,9 @@ var __commonJS = (cb, mod) => function __require2() {
28
28
  throw mod = 0, e2;
29
29
  }
30
30
  };
31
- var __export = (target, all2) => {
32
- for (var name in all2)
33
- __defProp(target, name, { get: all2[name], enumerable: true });
31
+ var __export = (target, all) => {
32
+ for (var name in all)
33
+ __defProp(target, name, { get: all[name], enumerable: true });
34
34
  };
35
35
  var __copyProps = (to, from, except, desc) => {
36
36
  if (from && typeof from === "object" || typeof from === "function") {
@@ -7517,7 +7517,7 @@ var require_react_dom_server_node_production = __commonJS({
7517
7517
  "node_modules/react-dom/cjs/react-dom-server.node.production.js"(exports) {
7518
7518
  "use strict";
7519
7519
  var util = __require("util");
7520
- var crypto4 = __require("crypto");
7520
+ var crypto5 = __require("crypto");
7521
7521
  var async_hooks = __require("async_hooks");
7522
7522
  var React4 = require_react();
7523
7523
  var ReactDOM = require_react_dom();
@@ -10382,7 +10382,7 @@ var require_react_dom_server_node_production = __commonJS({
10382
10382
  function createPostbackActionStateKey(permalink, componentKeyPath, hookIndex) {
10383
10383
  if (void 0 !== permalink) return "p" + permalink;
10384
10384
  permalink = JSON.stringify([componentKeyPath, null, hookIndex]);
10385
- componentKeyPath = crypto4.createHash("md5");
10385
+ componentKeyPath = crypto5.createHash("md5");
10386
10386
  componentKeyPath.update(permalink);
10387
10387
  return "k" + componentKeyPath.digest("hex");
10388
10388
  }
@@ -24376,7 +24376,7 @@ var require_react_dom_server_node_development = __commonJS({
24376
24376
  function createPostbackActionStateKey(permalink, componentKeyPath, hookIndex) {
24377
24377
  if (void 0 !== permalink) return "p" + permalink;
24378
24378
  permalink = JSON.stringify([componentKeyPath, null, hookIndex]);
24379
- componentKeyPath = crypto4.createHash("md5");
24379
+ componentKeyPath = crypto5.createHash("md5");
24380
24380
  componentKeyPath.update(permalink);
24381
24381
  return "k" + componentKeyPath.digest("hex");
24382
24382
  }
@@ -28038,7 +28038,7 @@ var require_react_dom_server_node_development = __commonJS({
28038
28038
  }
28039
28039
  };
28040
28040
  }
28041
- var util = __require("util"), crypto4 = __require("crypto"), async_hooks = __require("async_hooks"), React4 = require_react(), ReactDOM = require_react_dom(), stream = __require("stream"), REACT_ELEMENT_TYPE = /* @__PURE__ */ Symbol.for("react.transitional.element"), REACT_PORTAL_TYPE = /* @__PURE__ */ Symbol.for("react.portal"), REACT_FRAGMENT_TYPE = /* @__PURE__ */ Symbol.for("react.fragment"), REACT_STRICT_MODE_TYPE = /* @__PURE__ */ Symbol.for("react.strict_mode"), REACT_PROFILER_TYPE = /* @__PURE__ */ Symbol.for("react.profiler"), REACT_CONSUMER_TYPE = /* @__PURE__ */ Symbol.for("react.consumer"), REACT_CONTEXT_TYPE = /* @__PURE__ */ Symbol.for("react.context"), REACT_FORWARD_REF_TYPE = /* @__PURE__ */ Symbol.for("react.forward_ref"), REACT_SUSPENSE_TYPE = /* @__PURE__ */ Symbol.for("react.suspense"), REACT_SUSPENSE_LIST_TYPE = /* @__PURE__ */ Symbol.for("react.suspense_list"), REACT_MEMO_TYPE = /* @__PURE__ */ Symbol.for("react.memo"), REACT_LAZY_TYPE = /* @__PURE__ */ Symbol.for("react.lazy"), REACT_SCOPE_TYPE = /* @__PURE__ */ Symbol.for("react.scope"), REACT_ACTIVITY_TYPE = /* @__PURE__ */ Symbol.for("react.activity"), REACT_LEGACY_HIDDEN_TYPE = /* @__PURE__ */ Symbol.for("react.legacy_hidden"), REACT_MEMO_CACHE_SENTINEL = /* @__PURE__ */ Symbol.for("react.memo_cache_sentinel"), REACT_VIEW_TRANSITION_TYPE = /* @__PURE__ */ Symbol.for("react.view_transition"), REACT_RECOVERABLE_TYPE = /* @__PURE__ */ Symbol.for("react.recoverable"), MAYBE_ITERATOR_SYMBOL = Symbol.iterator, ASYNC_ITERATOR = Symbol.asyncIterator, REACT_OPTIMISTIC_KEY = /* @__PURE__ */ Symbol.for("react.optimistic_key"), isArrayImpl = Array.isArray, jsxPropsParents = /* @__PURE__ */ new WeakMap(), jsxChildrenParents = /* @__PURE__ */ new WeakMap(), CLIENT_REFERENCE_TAG = /* @__PURE__ */ Symbol.for("react.client.reference"), hasOwnProperty = Object.prototype.hasOwnProperty, supportsArrayBufferDetached = hasOwnProperty.call(
28041
+ var util = __require("util"), crypto5 = __require("crypto"), async_hooks = __require("async_hooks"), React4 = require_react(), ReactDOM = require_react_dom(), stream = __require("stream"), REACT_ELEMENT_TYPE = /* @__PURE__ */ Symbol.for("react.transitional.element"), REACT_PORTAL_TYPE = /* @__PURE__ */ Symbol.for("react.portal"), REACT_FRAGMENT_TYPE = /* @__PURE__ */ Symbol.for("react.fragment"), REACT_STRICT_MODE_TYPE = /* @__PURE__ */ Symbol.for("react.strict_mode"), REACT_PROFILER_TYPE = /* @__PURE__ */ Symbol.for("react.profiler"), REACT_CONSUMER_TYPE = /* @__PURE__ */ Symbol.for("react.consumer"), REACT_CONTEXT_TYPE = /* @__PURE__ */ Symbol.for("react.context"), REACT_FORWARD_REF_TYPE = /* @__PURE__ */ Symbol.for("react.forward_ref"), REACT_SUSPENSE_TYPE = /* @__PURE__ */ Symbol.for("react.suspense"), REACT_SUSPENSE_LIST_TYPE = /* @__PURE__ */ Symbol.for("react.suspense_list"), REACT_MEMO_TYPE = /* @__PURE__ */ Symbol.for("react.memo"), REACT_LAZY_TYPE = /* @__PURE__ */ Symbol.for("react.lazy"), REACT_SCOPE_TYPE = /* @__PURE__ */ Symbol.for("react.scope"), REACT_ACTIVITY_TYPE = /* @__PURE__ */ Symbol.for("react.activity"), REACT_LEGACY_HIDDEN_TYPE = /* @__PURE__ */ Symbol.for("react.legacy_hidden"), REACT_MEMO_CACHE_SENTINEL = /* @__PURE__ */ Symbol.for("react.memo_cache_sentinel"), REACT_VIEW_TRANSITION_TYPE = /* @__PURE__ */ Symbol.for("react.view_transition"), REACT_RECOVERABLE_TYPE = /* @__PURE__ */ Symbol.for("react.recoverable"), MAYBE_ITERATOR_SYMBOL = Symbol.iterator, ASYNC_ITERATOR = Symbol.asyncIterator, REACT_OPTIMISTIC_KEY = /* @__PURE__ */ Symbol.for("react.optimistic_key"), isArrayImpl = Array.isArray, jsxPropsParents = /* @__PURE__ */ new WeakMap(), jsxChildrenParents = /* @__PURE__ */ new WeakMap(), CLIENT_REFERENCE_TAG = /* @__PURE__ */ Symbol.for("react.client.reference"), hasOwnProperty = Object.prototype.hasOwnProperty, supportsArrayBufferDetached = hasOwnProperty.call(
28042
28042
  ArrayBuffer.prototype,
28043
28043
  "detached"
28044
28044
  ), didWarnAboutMissingArrayBufferDetached = false, scheduleMicrotask = queueMicrotask, currentView = null, writtenBytes = 0, destinationHasCapacity$1 = true, textEncoder = new util.TextEncoder(), assign = Object.assign, VALID_ATTRIBUTE_NAME_REGEX = RegExp(
@@ -41256,8 +41256,8 @@ var require_JSXTransformer = __commonJS({
41256
41256
  }
41257
41257
  if (this.isAutomaticRuntime) {
41258
41258
  if (this.importProcessor) {
41259
- for (const [path27, resolvedName] of Object.entries(this.cjsAutomaticModuleNameResolutions)) {
41260
- prefix += `var ${resolvedName} = require("${path27}");`;
41259
+ for (const [path29, resolvedName] of Object.entries(this.cjsAutomaticModuleNameResolutions)) {
41260
+ prefix += `var ${resolvedName} = require("${path29}");`;
41261
41261
  }
41262
41262
  } else {
41263
41263
  const { createElement: createElementResolution, ...otherResolutions } = this.esmAutomaticImportNameResolutions;
@@ -41450,11 +41450,11 @@ var require_JSXTransformer = __commonJS({
41450
41450
  }
41451
41451
  claimAutoImportedName(funcName, importPathSuffix) {
41452
41452
  if (this.importProcessor) {
41453
- const path27 = this.jsxImportSource + importPathSuffix;
41454
- if (!this.cjsAutomaticModuleNameResolutions[path27]) {
41455
- this.cjsAutomaticModuleNameResolutions[path27] = this.importProcessor.getFreeIdentifierForPath(path27);
41453
+ const path29 = this.jsxImportSource + importPathSuffix;
41454
+ if (!this.cjsAutomaticModuleNameResolutions[path29]) {
41455
+ this.cjsAutomaticModuleNameResolutions[path29] = this.importProcessor.getFreeIdentifierForPath(path29);
41456
41456
  }
41457
- return `${this.cjsAutomaticModuleNameResolutions[path27]}.${funcName}`;
41457
+ return `${this.cjsAutomaticModuleNameResolutions[path29]}.${funcName}`;
41458
41458
  } else {
41459
41459
  if (!this.esmAutomaticImportNameResolutions[funcName]) {
41460
41460
  this.esmAutomaticImportNameResolutions[funcName] = this.nameManager.claimFreeName(
@@ -41890,7 +41890,7 @@ var require_CJSImportProcessor = __commonJS({
41890
41890
  */
41891
41891
  pruneTypeOnlyImports() {
41892
41892
  this.nonTypeIdentifiers = _getNonTypeIdentifiers.getNonTypeIdentifiers.call(void 0, this.tokens, this.options);
41893
- for (const [path27, importInfo] of this.importInfoByPath.entries()) {
41893
+ for (const [path29, importInfo] of this.importInfoByPath.entries()) {
41894
41894
  if (importInfo.hasBareImport || importInfo.hasStarExport || importInfo.exportStarNames.length > 0 || importInfo.namedExports.length > 0) {
41895
41895
  continue;
41896
41896
  }
@@ -41900,7 +41900,7 @@ var require_CJSImportProcessor = __commonJS({
41900
41900
  ...importInfo.namedImports.map(({ localName }) => localName)
41901
41901
  ];
41902
41902
  if (names2.every((name) => this.shouldAutomaticallyElideImportedName(name))) {
41903
- this.importsToReplace.set(path27, "");
41903
+ this.importsToReplace.set(path29, "");
41904
41904
  }
41905
41905
  }
41906
41906
  }
@@ -41908,7 +41908,7 @@ var require_CJSImportProcessor = __commonJS({
41908
41908
  return this.isTypeScriptTransformEnabled && !this.keepUnusedImports && !this.nonTypeIdentifiers.has(name);
41909
41909
  }
41910
41910
  generateImportReplacements() {
41911
- for (const [path27, importInfo] of this.importInfoByPath.entries()) {
41911
+ for (const [path29, importInfo] of this.importInfoByPath.entries()) {
41912
41912
  const {
41913
41913
  defaultNames,
41914
41914
  wildcardNames,
@@ -41918,17 +41918,17 @@ var require_CJSImportProcessor = __commonJS({
41918
41918
  hasStarExport
41919
41919
  } = importInfo;
41920
41920
  if (defaultNames.length === 0 && wildcardNames.length === 0 && namedImports.length === 0 && namedExports.length === 0 && exportStarNames.length === 0 && !hasStarExport) {
41921
- this.importsToReplace.set(path27, `require('${path27}');`);
41921
+ this.importsToReplace.set(path29, `require('${path29}');`);
41922
41922
  continue;
41923
41923
  }
41924
- const primaryImportName = this.getFreeIdentifierForPath(path27);
41924
+ const primaryImportName = this.getFreeIdentifierForPath(path29);
41925
41925
  let secondaryImportName;
41926
41926
  if (this.enableLegacyTypeScriptModuleInterop) {
41927
41927
  secondaryImportName = primaryImportName;
41928
41928
  } else {
41929
- secondaryImportName = wildcardNames.length > 0 ? wildcardNames[0] : this.getFreeIdentifierForPath(path27);
41929
+ secondaryImportName = wildcardNames.length > 0 ? wildcardNames[0] : this.getFreeIdentifierForPath(path29);
41930
41930
  }
41931
- let requireCode = `var ${primaryImportName} = require('${path27}');`;
41931
+ let requireCode = `var ${primaryImportName} = require('${path29}');`;
41932
41932
  if (wildcardNames.length > 0) {
41933
41933
  for (const wildcardName of wildcardNames) {
41934
41934
  const moduleExpr = this.enableLegacyTypeScriptModuleInterop ? primaryImportName : `${this.helperManager.getHelperName("interopRequireWildcard")}(${primaryImportName})`;
@@ -41956,7 +41956,7 @@ var require_CJSImportProcessor = __commonJS({
41956
41956
  "createStarExport"
41957
41957
  )}(${primaryImportName});`;
41958
41958
  }
41959
- this.importsToReplace.set(path27, requireCode);
41959
+ this.importsToReplace.set(path29, requireCode);
41960
41960
  for (const defaultName of defaultNames) {
41961
41961
  this.identifierReplacements.set(defaultName, `${secondaryImportName}.default`);
41962
41962
  }
@@ -41965,8 +41965,8 @@ var require_CJSImportProcessor = __commonJS({
41965
41965
  }
41966
41966
  }
41967
41967
  }
41968
- getFreeIdentifierForPath(path27) {
41969
- const components = path27.split("/");
41968
+ getFreeIdentifierForPath(path29) {
41969
+ const components = path29.split("/");
41970
41970
  const lastComponent = components[components.length - 1];
41971
41971
  const baseName = lastComponent.replace(/\W/g, "");
41972
41972
  return this.nameManager.claimFreeName(`_${baseName}`);
@@ -42011,8 +42011,8 @@ var require_CJSImportProcessor = __commonJS({
42011
42011
  if (!this.tokens.matches1AtIndex(index, _types.TokenType.string)) {
42012
42012
  throw new Error("Expected string token at the end of import statement.");
42013
42013
  }
42014
- const path27 = this.tokens.stringValueAtIndex(index);
42015
- const importInfo = this.getImportInfo(path27);
42014
+ const path29 = this.tokens.stringValueAtIndex(index);
42015
+ const importInfo = this.getImportInfo(path29);
42016
42016
  importInfo.defaultNames.push(...defaultNames);
42017
42017
  importInfo.wildcardNames.push(...wildcardNames);
42018
42018
  importInfo.namedImports.push(...namedImports);
@@ -42079,8 +42079,8 @@ var require_CJSImportProcessor = __commonJS({
42079
42079
  if (!this.tokens.matches1AtIndex(index, _types.TokenType.string)) {
42080
42080
  throw new Error("Expected string token at the end of import statement.");
42081
42081
  }
42082
- const path27 = this.tokens.stringValueAtIndex(index);
42083
- const importInfo = this.getImportInfo(path27);
42082
+ const path29 = this.tokens.stringValueAtIndex(index);
42083
+ const importInfo = this.getImportInfo(path29);
42084
42084
  importInfo.namedExports.push(...namedImports);
42085
42085
  }
42086
42086
  preprocessExportStarAtIndex(index) {
@@ -42095,8 +42095,8 @@ var require_CJSImportProcessor = __commonJS({
42095
42095
  if (!this.tokens.matches1AtIndex(index, _types.TokenType.string)) {
42096
42096
  throw new Error("Expected string token at the end of star export statement.");
42097
42097
  }
42098
- const path27 = this.tokens.stringValueAtIndex(index);
42099
- const importInfo = this.getImportInfo(path27);
42098
+ const path29 = this.tokens.stringValueAtIndex(index);
42099
+ const importInfo = this.getImportInfo(path29);
42100
42100
  if (exportedName !== null) {
42101
42101
  importInfo.exportStarNames.push(exportedName);
42102
42102
  } else {
@@ -42136,8 +42136,8 @@ var require_CJSImportProcessor = __commonJS({
42136
42136
  * Get a mutable import info object for this path, creating one if it doesn't
42137
42137
  * exist yet.
42138
42138
  */
42139
- getImportInfo(path27) {
42140
- const existingInfo = this.importInfoByPath.get(path27);
42139
+ getImportInfo(path29) {
42140
+ const existingInfo = this.importInfoByPath.get(path29);
42141
42141
  if (existingInfo) {
42142
42142
  return existingInfo;
42143
42143
  }
@@ -42150,7 +42150,7 @@ var require_CJSImportProcessor = __commonJS({
42150
42150
  exportStarNames: [],
42151
42151
  hasStarExport: false
42152
42152
  };
42153
- this.importInfoByPath.set(path27, newInfo);
42153
+ this.importInfoByPath.set(path29, newInfo);
42154
42154
  return newInfo;
42155
42155
  }
42156
42156
  addExportBinding(localName, exportedName) {
@@ -42223,9 +42223,9 @@ var require_sourcemap_codec_umd = __commonJS({
42223
42223
  var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor;
42224
42224
  var __getOwnPropNames2 = Object.getOwnPropertyNames;
42225
42225
  var __hasOwnProp2 = Object.prototype.hasOwnProperty;
42226
- var __export2 = (target, all2) => {
42227
- for (var name in all2)
42228
- __defProp2(target, name, { get: all2[name], enumerable: true });
42226
+ var __export2 = (target, all) => {
42227
+ for (var name in all)
42228
+ __defProp2(target, name, { get: all[name], enumerable: true });
42229
42229
  };
42230
42230
  var __copyProps2 = (to, from, except, desc) => {
42231
42231
  if (from && typeof from === "object" || typeof from === "function") {
@@ -42730,16 +42730,16 @@ var require_resolve_uri_umd = __commonJS({
42730
42730
  }
42731
42731
  function parseFileUrl(input) {
42732
42732
  const match = fileRegex.exec(input);
42733
- const path27 = match[2];
42734
- return makeUrl("file:", "", match[1] || "", "", isAbsolutePath(path27) ? path27 : "/" + path27, match[3] || "", match[4] || "");
42733
+ const path29 = match[2];
42734
+ return makeUrl("file:", "", match[1] || "", "", isAbsolutePath(path29) ? path29 : "/" + path29, match[3] || "", match[4] || "");
42735
42735
  }
42736
- function makeUrl(scheme, user, host, port, path27, query, hash) {
42736
+ function makeUrl(scheme, user, host, port, path29, query, hash) {
42737
42737
  return {
42738
42738
  scheme,
42739
42739
  user,
42740
42740
  host,
42741
42741
  port,
42742
- path: path27,
42742
+ path: path29,
42743
42743
  query,
42744
42744
  hash,
42745
42745
  type: 7
@@ -42769,11 +42769,11 @@ var require_resolve_uri_umd = __commonJS({
42769
42769
  url.type = input ? input.startsWith("?") ? 3 : input.startsWith("#") ? 2 : 4 : 1;
42770
42770
  return url;
42771
42771
  }
42772
- function stripPathFilename(path27) {
42773
- if (path27.endsWith("/.."))
42774
- return path27;
42775
- const index = path27.lastIndexOf("/");
42776
- return path27.slice(0, index + 1);
42772
+ function stripPathFilename(path29) {
42773
+ if (path29.endsWith("/.."))
42774
+ return path29;
42775
+ const index = path29.lastIndexOf("/");
42776
+ return path29.slice(0, index + 1);
42777
42777
  }
42778
42778
  function mergePaths(url, base) {
42779
42779
  normalizePath(base, base.type);
@@ -42811,14 +42811,14 @@ var require_resolve_uri_umd = __commonJS({
42811
42811
  pieces[pointer++] = piece;
42812
42812
  positive++;
42813
42813
  }
42814
- let path27 = "";
42814
+ let path29 = "";
42815
42815
  for (let i = 1; i < pointer; i++) {
42816
- path27 += "/" + pieces[i];
42816
+ path29 += "/" + pieces[i];
42817
42817
  }
42818
- if (!path27 || addTrailingSlash && !path27.endsWith("/..")) {
42819
- path27 += "/";
42818
+ if (!path29 || addTrailingSlash && !path29.endsWith("/..")) {
42819
+ path29 += "/";
42820
42820
  }
42821
- url.path = path27;
42821
+ url.path = path29;
42822
42822
  }
42823
42823
  function resolve2(input, base) {
42824
42824
  if (!input && !base)
@@ -42859,13 +42859,13 @@ var require_resolve_uri_umd = __commonJS({
42859
42859
  case 3:
42860
42860
  return queryHash;
42861
42861
  case 4: {
42862
- const path27 = url.path.slice(1);
42863
- if (!path27)
42862
+ const path29 = url.path.slice(1);
42863
+ if (!path29)
42864
42864
  return queryHash || ".";
42865
- if (isRelative(base || input) && !isRelative(path27)) {
42866
- return "./" + path27 + queryHash;
42865
+ if (isRelative(base || input) && !isRelative(path29)) {
42866
+ return "./" + path29 + queryHash;
42867
42867
  }
42868
- return path27 + queryHash;
42868
+ return path29 + queryHash;
42869
42869
  }
42870
42870
  case 5:
42871
42871
  return url.path + queryHash;
@@ -42910,9 +42910,9 @@ var require_trace_mapping_umd = __commonJS({
42910
42910
  var __commonJS2 = (cb, mod) => function __require2() {
42911
42911
  return mod || (0, cb[__getOwnPropNames2(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
42912
42912
  };
42913
- var __export2 = (target, all2) => {
42914
- for (var name in all2)
42915
- __defProp2(target, name, { get: all2[name], enumerable: true });
42913
+ var __export2 = (target, all) => {
42914
+ for (var name in all)
42915
+ __defProp2(target, name, { get: all[name], enumerable: true });
42916
42916
  };
42917
42917
  var __copyProps2 = (to, from, except, desc) => {
42918
42918
  if (from && typeof from === "object" || typeof from === "function") {
@@ -42964,10 +42964,10 @@ var require_trace_mapping_umd = __commonJS({
42964
42964
  module2.exports = __toCommonJS(trace_mapping_exports);
42965
42965
  var import_sourcemap_codec = __toESM2(require_sourcemap_codec());
42966
42966
  var import_resolve_uri = __toESM2(require_resolve_uri());
42967
- function stripFilename(path27) {
42968
- if (!path27) return "";
42969
- const index = path27.lastIndexOf("/");
42970
- return path27.slice(0, index + 1);
42967
+ function stripFilename(path29) {
42968
+ if (!path29) return "";
42969
+ const index = path29.lastIndexOf("/");
42970
+ return path29.slice(0, index + 1);
42971
42971
  }
42972
42972
  function resolver(mapUrl, sourceRoot) {
42973
42973
  const from = stripFilename(mapUrl);
@@ -43394,7 +43394,7 @@ var require_trace_mapping_umd = __commonJS({
43394
43394
  }
43395
43395
  return result;
43396
43396
  }
43397
- function generatedPosition(map2, source, line, column, bias, all2) {
43397
+ function generatedPosition(map2, source, line, column, bias, all) {
43398
43398
  var _a, _b;
43399
43399
  line--;
43400
43400
  if (line < 0) throw new Error(LINE_GTR_ZERO);
@@ -43402,13 +43402,13 @@ var require_trace_mapping_umd = __commonJS({
43402
43402
  const { sources, resolvedSources } = map2;
43403
43403
  let sourceIndex2 = sources.indexOf(source);
43404
43404
  if (sourceIndex2 === -1) sourceIndex2 = resolvedSources.indexOf(source);
43405
- if (sourceIndex2 === -1) return all2 ? [] : GMapping(null, null);
43405
+ if (sourceIndex2 === -1) return all ? [] : GMapping(null, null);
43406
43406
  const bySourceMemos = (_a = cast(map2))._bySourceMemos || (_a._bySourceMemos = sources.map(memoizedState));
43407
43407
  const generated = (_b = cast(map2))._bySources || (_b._bySources = buildBySources(decodedMappings(map2), bySourceMemos));
43408
43408
  const segments = generated[sourceIndex2][line];
43409
- if (segments == null) return all2 ? [] : GMapping(null, null);
43409
+ if (segments == null) return all ? [] : GMapping(null, null);
43410
43410
  const memo = bySourceMemos[sourceIndex2];
43411
- if (all2) return sliceGeneratedPositions(segments, memo, line, column, bias);
43411
+ if (all) return sliceGeneratedPositions(segments, memo, line, column, bias);
43412
43412
  const index = traceSegmentInternal(segments, memo, line, column, bias);
43413
43413
  if (index === -1) return GMapping(null, null);
43414
43414
  const segment = segments[index];
@@ -43450,9 +43450,9 @@ var require_gen_mapping_umd = __commonJS({
43450
43450
  var __commonJS2 = (cb, mod) => function __require2() {
43451
43451
  return mod || (0, cb[__getOwnPropNames2(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
43452
43452
  };
43453
- var __export2 = (target, all2) => {
43454
- for (var name in all2)
43455
- __defProp2(target, name, { get: all2[name], enumerable: true });
43453
+ var __export2 = (target, all) => {
43454
+ for (var name in all)
43455
+ __defProp2(target, name, { get: all[name], enumerable: true });
43456
43456
  };
43457
43457
  var __copyProps2 = (to, from, except, desc) => {
43458
43458
  if (from && typeof from === "object" || typeof from === "function") {
@@ -44175,9 +44175,9 @@ var require_util2 = __commonJS({
44175
44175
  /** @class */
44176
44176
  (function(_super) {
44177
44177
  __extends(VError2, _super);
44178
- function VError2(path27, message) {
44178
+ function VError2(path29, message) {
44179
44179
  var _this = _super.call(this, message) || this;
44180
- _this.path = path27;
44180
+ _this.path = path29;
44181
44181
  Object.setPrototypeOf(_this, VError2.prototype);
44182
44182
  return _this;
44183
44183
  }
@@ -44237,26 +44237,26 @@ var require_util2 = __commonJS({
44237
44237
  (_b = this._messages).push.apply(_b, best._messages);
44238
44238
  }
44239
44239
  };
44240
- DetailContext2.prototype.getError = function(path27) {
44240
+ DetailContext2.prototype.getError = function(path29) {
44241
44241
  var msgParts = [];
44242
44242
  for (var i = this._propNames.length - 1; i >= 0; i--) {
44243
44243
  var p2 = this._propNames[i];
44244
- path27 += typeof p2 === "number" ? "[" + p2 + "]" : p2 ? "." + p2 : "";
44244
+ path29 += typeof p2 === "number" ? "[" + p2 + "]" : p2 ? "." + p2 : "";
44245
44245
  var m = this._messages[i];
44246
44246
  if (m) {
44247
- msgParts.push(path27 + " " + m);
44247
+ msgParts.push(path29 + " " + m);
44248
44248
  }
44249
44249
  }
44250
- return new VError(path27, msgParts.join("; "));
44250
+ return new VError(path29, msgParts.join("; "));
44251
44251
  };
44252
- DetailContext2.prototype.getErrorDetail = function(path27) {
44252
+ DetailContext2.prototype.getErrorDetail = function(path29) {
44253
44253
  var details = [];
44254
44254
  for (var i = this._propNames.length - 1; i >= 0; i--) {
44255
44255
  var p2 = this._propNames[i];
44256
- path27 += typeof p2 === "number" ? "[" + p2 + "]" : p2 ? "." + p2 : "";
44256
+ path29 += typeof p2 === "number" ? "[" + p2 + "]" : p2 ? "." + p2 : "";
44257
44257
  var message = this._messages[i];
44258
44258
  if (message) {
44259
- details.push({ path: path27, message });
44259
+ details.push({ path: path29, message });
44260
44260
  }
44261
44261
  }
44262
44262
  var detail = null;
@@ -44406,8 +44406,8 @@ var require_types2 = __commonJS({
44406
44406
  return ctx.fail(null, "is not an array", 0);
44407
44407
  }
44408
44408
  for (var i = 0; i < value.length; i++) {
44409
- var ok2 = itemChecker(value[i], ctx);
44410
- if (!ok2) {
44409
+ var ok = itemChecker(value[i], ctx);
44410
+ if (!ok) {
44411
44411
  return ctx.fail(i, null, 1);
44412
44412
  }
44413
44413
  }
@@ -44446,8 +44446,8 @@ var require_types2 = __commonJS({
44446
44446
  return ctx.fail(null, "is not an array", 0);
44447
44447
  }
44448
44448
  for (var i = 0; i < itemCheckers.length; i++) {
44449
- var ok2 = itemCheckers[i](value[i], ctx);
44450
- if (!ok2) {
44449
+ var ok = itemCheckers[i](value[i], ctx);
44450
+ if (!ok) {
44451
44451
  return ctx.fail(i, null, 1);
44452
44452
  }
44453
44453
  }
@@ -44508,8 +44508,8 @@ var require_types2 = __commonJS({
44508
44508
  return function(value, ctx) {
44509
44509
  var ur2 = ctx.unionResolver();
44510
44510
  for (var i = 0; i < itemCheckers.length; i++) {
44511
- var ok2 = itemCheckers[i](value, ur2.createContext());
44512
- if (ok2) {
44511
+ var ok = itemCheckers[i](value, ur2.createContext());
44512
+ if (ok) {
44513
44513
  return true;
44514
44514
  }
44515
44515
  }
@@ -44546,10 +44546,10 @@ var require_types2 = __commonJS({
44546
44546
  return t2.getChecker(suite, strict, allowedProps);
44547
44547
  });
44548
44548
  return function(value, ctx) {
44549
- var ok2 = itemCheckers.every(function(checker) {
44549
+ var ok = itemCheckers.every(function(checker) {
44550
44550
  return checker(value, ctx);
44551
44551
  });
44552
- if (ok2) {
44552
+ if (ok) {
44553
44553
  return true;
44554
44554
  }
44555
44555
  return ctx.fail(null, null, 0);
@@ -44674,8 +44674,8 @@ var require_types2 = __commonJS({
44674
44674
  return ctx.fail(name_1, "is missing", 1);
44675
44675
  }
44676
44676
  } else {
44677
- var ok2 = propCheckers[i](v2, ctx);
44678
- if (!ok2) {
44677
+ var ok = propCheckers[i](v2, ctx);
44678
+ if (!ok) {
44679
44679
  return ctx.fail(name_1, null, 1);
44680
44680
  }
44681
44681
  }
@@ -44815,8 +44815,8 @@ var require_types2 = __commonJS({
44815
44815
  return ctx.fail(p2.name, "is missing", 1);
44816
44816
  }
44817
44817
  } else {
44818
- var ok2 = itemCheckers[i](value[i], ctx);
44819
- if (!ok2) {
44818
+ var ok = itemCheckers[i](value[i], ctx);
44819
+ if (!ok) {
44820
44820
  return ctx.fail(p2.name, null, 1);
44821
44821
  }
44822
44822
  }
@@ -45069,8 +45069,8 @@ var require_dist = __commonJS({
45069
45069
  this.checkerPlain = this.ttype.getChecker(suite, false);
45070
45070
  this.checkerStrict = this.ttype.getChecker(suite, true);
45071
45071
  }
45072
- Checker2.prototype.setReportedPath = function(path27) {
45073
- this._path = path27;
45072
+ Checker2.prototype.setReportedPath = function(path29) {
45073
+ this._path = path29;
45074
45074
  };
45075
45075
  Checker2.prototype.check = function(value) {
45076
45076
  return this._doCheck(this.checkerPlain, value);
@@ -50503,9 +50503,9 @@ var require_CJSImportTransformer = __commonJS({
50503
50503
  if (shouldElideImport) {
50504
50504
  this.tokens.removeToken();
50505
50505
  } else {
50506
- const path27 = this.tokens.stringValue();
50507
- this.tokens.replaceTokenTrimmingLeftWhitespace(this.importProcessor.claimImportCode(path27));
50508
- this.tokens.appendCode(this.importProcessor.claimImportCode(path27));
50506
+ const path29 = this.tokens.stringValue();
50507
+ this.tokens.replaceTokenTrimmingLeftWhitespace(this.importProcessor.claimImportCode(path29));
50508
+ this.tokens.appendCode(this.importProcessor.claimImportCode(path29));
50509
50509
  }
50510
50510
  _removeMaybeImportAttributes.removeMaybeImportAttributes.call(void 0, this.tokens);
50511
50511
  if (this.tokens.matches1(_types.TokenType.semi)) {
@@ -51085,8 +51085,8 @@ var require_CJSImportTransformer = __commonJS({
51085
51085
  }
51086
51086
  if (this.tokens.matchesContextual(_keywords.ContextualKeyword._from)) {
51087
51087
  this.tokens.removeToken();
51088
- const path27 = this.tokens.stringValue();
51089
- this.tokens.replaceTokenTrimmingLeftWhitespace(this.importProcessor.claimImportCode(path27));
51088
+ const path29 = this.tokens.stringValue();
51089
+ this.tokens.replaceTokenTrimmingLeftWhitespace(this.importProcessor.claimImportCode(path29));
51090
51090
  _removeMaybeImportAttributes.removeMaybeImportAttributes.call(void 0, this.tokens);
51091
51091
  } else {
51092
51092
  this.tokens.appendCode(exportStatements.join(" "));
@@ -51100,8 +51100,8 @@ var require_CJSImportTransformer = __commonJS({
51100
51100
  while (!this.tokens.matches1(_types.TokenType.string)) {
51101
51101
  this.tokens.removeToken();
51102
51102
  }
51103
- const path27 = this.tokens.stringValue();
51104
- this.tokens.replaceTokenTrimmingLeftWhitespace(this.importProcessor.claimImportCode(path27));
51103
+ const path29 = this.tokens.stringValue();
51104
+ this.tokens.replaceTokenTrimmingLeftWhitespace(this.importProcessor.claimImportCode(path29));
51105
51105
  _removeMaybeImportAttributes.removeMaybeImportAttributes.call(void 0, this.tokens);
51106
51106
  if (this.tokens.matches1(_types.TokenType.semi)) {
51107
51107
  this.tokens.removeToken();
@@ -80745,9 +80745,9 @@ var init_bundle_mjs = __esm({
80745
80745
  processClassesRecursively(value, getPart(classPartObject, key), classGroupId, theme);
80746
80746
  }
80747
80747
  };
80748
- getPart = (classPartObject, path27) => {
80748
+ getPart = (classPartObject, path29) => {
80749
80749
  let current = classPartObject;
80750
- const parts = path27.split(CLASS_PART_SEPARATOR);
80750
+ const parts = path29.split(CLASS_PART_SEPARATOR);
80751
80751
  const len = parts.length;
80752
80752
  for (let i = 0; i < len; i++) {
80753
80753
  const part = parts[i];
@@ -84224,10 +84224,10 @@ function readSession() {
84224
84224
  return null;
84225
84225
  }
84226
84226
  try {
84227
- const held3 = JSON.parse(raw);
84228
- if (!held3.accessToken || !held3.refreshToken || !held3.subject) return null;
84229
- if (held3.supabaseUrl !== cloudEndpoints().supabaseUrl) return null;
84230
- return { ...held3, version: 1, expiresAt: Number(held3.expiresAt) || 0 };
84227
+ const held4 = JSON.parse(raw);
84228
+ if (!held4.accessToken || !held4.refreshToken || !held4.subject) return null;
84229
+ if (held4.supabaseUrl !== cloudEndpoints().supabaseUrl) return null;
84230
+ return { ...held4, version: 1, expiresAt: Number(held4.expiresAt) || 0 };
84231
84231
  } catch {
84232
84232
  return null;
84233
84233
  }
@@ -84246,9 +84246,9 @@ function clearSession() {
84246
84246
  }
84247
84247
  }
84248
84248
  async function accessToken() {
84249
- const held3 = readSession();
84250
- if (!held3) return { ok: false, reason: "signed-out" };
84251
- if (!expiringSoon(held3)) return { ok: true, token: held3.accessToken, session: held3 };
84249
+ const held4 = readSession();
84250
+ if (!held4) return { ok: false, reason: "signed-out" };
84251
+ if (!expiringSoon(held4)) return { ok: true, token: held4.accessToken, session: held4 };
84252
84252
  return refresh();
84253
84253
  }
84254
84254
  var expiringSoon = (session) => session.expiresAt - REFRESH_MARGIN_MS <= Date.now();
@@ -84333,14 +84333,14 @@ async function takeLock() {
84333
84333
  }
84334
84334
  }
84335
84335
  function stale() {
84336
- let held3;
84336
+ let held4;
84337
84337
  try {
84338
84338
  const pid = Number(fs.readFileSync(lockOwner(), "utf8").trim());
84339
- held3 = Number.isInteger(pid) && pid > 0 ? pid : void 0;
84339
+ held4 = Number.isInteger(pid) && pid > 0 ? pid : void 0;
84340
84340
  } catch {
84341
- held3 = void 0;
84341
+ held4 = void 0;
84342
84342
  }
84343
- if (held3 !== void 0 && !alive(held3)) return true;
84343
+ if (held4 !== void 0 && !alive(held4)) return true;
84344
84344
  try {
84345
84345
  return Date.now() - fs.statSync(lockDir()).mtimeMs > LOCK_STALE_MS;
84346
84346
  } catch {
@@ -84371,8 +84371,8 @@ function useRunnerHome() {
84371
84371
  function machineId() {
84372
84372
  const file2 = path3.join(runnerHome(), "machine.json");
84373
84373
  try {
84374
- const held3 = JSON.parse(fs2.readFileSync(file2, "utf8"));
84375
- if (held3.id && /^[0-9a-f-]{36}$/i.test(held3.id)) return held3.id;
84374
+ const held4 = JSON.parse(fs2.readFileSync(file2, "utf8"));
84375
+ if (held4.id && /^[0-9a-f-]{36}$/i.test(held4.id)) return held4.id;
84376
84376
  } catch {
84377
84377
  }
84378
84378
  const id = crypto.randomUUID();
@@ -84384,6 +84384,8 @@ function machineId() {
84384
84384
  var machineName = () => os2.hostname().replace(/\.local$/, "");
84385
84385
 
84386
84386
  // src/i18n.ts
84387
+ var ZH_HOLDER = { "claude-code": "Claude Code", codex: "Codex", terminal: "\u672C\u673A\u7EC8\u7AEF", desktop: "WildCrane \u684C\u9762\u7AEF" };
84388
+ var EN_HOLDER = { "claude-code": "Claude Code", codex: "Codex", terminal: "the terminal", desktop: "the WildCrane desktop app" };
84387
84389
  var ZH = {
84388
84390
  signIn: (url) => `\u8BF7\u5728\u6D4F\u89C8\u5668\u4E2D\u767B\u5F55 WildCrane\uFF1A
84389
84391
  ${url}`,
@@ -84398,6 +84400,10 @@ ${url}`,
84398
84400
  updated: (version) => `\u5DF2\u66F4\u65B0\u5230 ${version}`,
84399
84401
  rolledBack: (version) => `\u65B0\u7248\u672C\u672A\u80FD\u542F\u52A8\uFF0C\u5DF2\u9000\u56DE ${version}`,
84400
84402
  nodeTooOld: (range) => `\u65B0\u7248\u672C\u9700\u8981 Node ${range}\uFF0C\u8BF7\u5347\u7EA7 Node\u3002\u672C\u6B21\u7EE7\u7EED\u4F7F\u7528\u5F53\u524D\u7248\u672C\u3002`,
84403
+ waitingHere: (holder) => {
84404
+ const who = holder ? ZH_HOLDER[holder] : "\u672C\u673A\u53E6\u4E00\u4E2A\u7A0B\u5E8F";
84405
+ return `${who}\u5DF2\u5728\u8FD0\u884C\u4E00\u4E2A WildCrane \u5B9E\u4F8B\uFF0C\u8D1F\u8D23\u63A5\u6536\u6240\u6709\u8FD0\u884C\u4EFB\u52A1\u3002\u5F53\u524D\u5B9E\u4F8B\u5DF2\u6682\u505C\uFF0C${who}\u5173\u95ED\u540E\u5C06\u7531\u5F53\u524D\u5B9E\u4F8B\u63A5\u66FF\u3002`;
84406
+ },
84401
84407
  stopped: () => "\u5DF2\u505C\u6B62",
84402
84408
  reviewTitle: () => "\u5F85\u4F60\u5BA1\u6838",
84403
84409
  reviewBody: (task) => `\u300C${task}\u300D\u5F85\u5BA1\u6838`,
@@ -84420,6 +84426,10 @@ ${url}`,
84420
84426
  updated: (version) => `Updated to ${version}`,
84421
84427
  rolledBack: (version) => `The new version did not start \u2014 back on ${version}`,
84422
84428
  nodeTooOld: (range) => `The new version needs Node ${range}. Update Node; running the current version for now.`,
84429
+ waitingHere: (holder) => {
84430
+ const who = holder ? EN_HOLDER[holder] : "another app on this computer";
84431
+ return `${who[0].toUpperCase()}${who.slice(1)} is already running a WildCrane instance that takes all run tasks. This instance is paused and takes over when ${who} closes.`;
84432
+ },
84423
84433
  stopped: () => "Stopped",
84424
84434
  reviewTitle: () => "Needs your review",
84425
84435
  reviewBody: (task) => `"${task}" is ready for review`,
@@ -84443,7 +84453,7 @@ function status(line) {
84443
84453
  }
84444
84454
 
84445
84455
  // src/protocol.ts
84446
- var VERSION = true ? "0.5.0" : "0.0.0";
84456
+ var VERSION = true ? "0.6.1" : "0.0.0";
84447
84457
  var EXIT_REFUSED = 10;
84448
84458
  var EXIT_OUTDATED = 11;
84449
84459
 
@@ -84650,16 +84660,16 @@ function createEventCollector() {
84650
84660
  let queue = [];
84651
84661
  let said = "";
84652
84662
  const todoCalls = /* @__PURE__ */ new Set();
84653
- const held3 = /* @__PURE__ */ new Map();
84663
+ const held4 = /* @__PURE__ */ new Map();
84654
84664
  let todos = [];
84655
84665
  const close = () => {
84656
84666
  if (said.trim()) queue.push({ kind: "message", data: { text: said.trim() } });
84657
84667
  said = "";
84658
84668
  };
84659
84669
  const release2 = (id, target) => {
84660
- const call = held3.get(id);
84670
+ const call = held4.get(id);
84661
84671
  if (!call) return;
84662
- held3.delete(id);
84672
+ held4.delete(id);
84663
84673
  queue.push({ kind: "tool_call", data: { ...call, ...target ? { target } : {} } });
84664
84674
  };
84665
84675
  return {
@@ -84681,7 +84691,7 @@ function createEventCollector() {
84681
84691
  }
84682
84692
  const target = targetIn(u2);
84683
84693
  const data = { toolCallId: id, title: str(u2.title) || "tool", kind: toolKind(u2.kind), status: str(u2.status) || "pending", ...target ? { target } : {} };
84684
- if (u2.rawInput !== void 0 && !inputTarget(obj(u2.rawInput))) held3.set(id, data);
84694
+ if (u2.rawInput !== void 0 && !inputTarget(obj(u2.rawInput))) held4.set(id, data);
84685
84695
  else queue.push({ kind: "tool_call", data });
84686
84696
  return;
84687
84697
  }
@@ -84751,7 +84761,7 @@ function createEventCollector() {
84751
84761
  /** The turn is over: a half-held message is finished off. */
84752
84762
  end() {
84753
84763
  close();
84754
- for (const id of [...held3.keys()]) release2(id);
84764
+ for (const id of [...held4.keys()]) release2(id);
84755
84765
  },
84756
84766
  /** What arrived since the last call. */
84757
84767
  events() {
@@ -85031,10 +85041,10 @@ function findRollout(threadId) {
85031
85041
  }
85032
85042
  for (const name of names2) {
85033
85043
  if (!name.startsWith("rollout-") || !name.endsWith(tail)) continue;
85034
- const path27 = join(dir, name);
85044
+ const path29 = join(dir, name);
85035
85045
  try {
85036
- const at2 = statSync(path27).mtimeMs;
85037
- if (!newest || at2 > newest.at) newest = { path: path27, at: at2 };
85046
+ const at2 = statSync(path29).mtimeMs;
85047
+ if (!newest || at2 > newest.at) newest = { path: path29, at: at2 };
85038
85048
  } catch {
85039
85049
  }
85040
85050
  }
@@ -85042,11 +85052,11 @@ function findRollout(threadId) {
85042
85052
  return newest?.path;
85043
85053
  }
85044
85054
  function readCodexRunFacts(threadId) {
85045
- const path27 = findRollout(threadId);
85046
- if (!path27) return void 0;
85055
+ const path29 = findRollout(threadId);
85056
+ if (!path29) return void 0;
85047
85057
  let raw;
85048
85058
  try {
85049
- raw = readFileSync(path27, "utf8");
85059
+ raw = readFileSync(path29, "utf8");
85050
85060
  } catch {
85051
85061
  return void 0;
85052
85062
  }
@@ -85154,8 +85164,8 @@ function renderCompleted(item) {
85154
85164
  const changes = Array.isArray(item.changes) ? item.changes : [];
85155
85165
  return changes.map((raw) => {
85156
85166
  const change = obj(raw);
85157
- const path27 = str(change.path);
85158
- return path27 ? `\u23FA ${str(change.kind) || "change"}(${path27})
85167
+ const path29 = str(change.path);
85168
+ return path29 ? `\u23FA ${str(change.kind) || "change"}(${path29})
85159
85169
  ` : "";
85160
85170
  }).join("");
85161
85171
  }
@@ -85403,10 +85413,10 @@ function readKimiRunFacts(sessionId) {
85403
85413
  const total = { input: 0, cacheCreation: 0, cacheRead: 0, output: 0 };
85404
85414
  let model;
85405
85415
  let holding = 0;
85406
- for (const { path: path27, main: main2 } of wireFiles(entry.sessionDir)) {
85416
+ for (const { path: path29, main: main2 } of wireFiles(entry.sessionDir)) {
85407
85417
  let raw;
85408
85418
  try {
85409
- raw = readFileSync2(path27, "utf8");
85419
+ raw = readFileSync2(path29, "utf8");
85410
85420
  } catch {
85411
85421
  continue;
85412
85422
  }
@@ -85595,8 +85605,8 @@ function connect(stdout, stdin, onCall, dialect = {}) {
85595
85605
  call(method, params) {
85596
85606
  if (gone) return Promise.reject(new Error(gone));
85597
85607
  const id = dialect.stringIds ? String(nextId++) : nextId++;
85598
- return new Promise((ok2, fail) => {
85599
- waiting.set(String(id), { ok: ok2, fail });
85608
+ return new Promise((ok, fail) => {
85609
+ waiting.set(String(id), { ok, fail });
85600
85610
  send3({ id, method, params });
85601
85611
  });
85602
85612
  },
@@ -85670,8 +85680,8 @@ var CLIENT_CAPABILITIES = {
85670
85680
  };
85671
85681
  function whereHint(update2, title) {
85672
85682
  const locations = Array.isArray(update2.locations) ? update2.locations : [];
85673
- const path27 = str(obj(locations[0]).path);
85674
- return path27 && !title.includes(path27) ? hint(path27) : "";
85683
+ const path29 = str(obj(locations[0]).path);
85684
+ return path29 && !title.includes(path29) ? hint(path29) : "";
85675
85685
  }
85676
85686
  function saidBy(update2) {
85677
85687
  const content = Array.isArray(update2.content) ? update2.content : [];
@@ -87714,15 +87724,15 @@ function isFilled(harness, { values, secretsSet: secretsSet2 }) {
87714
87724
  return setting?.kind === "secret" ? secretsSet2.includes(key) : Boolean(values[key]);
87715
87725
  };
87716
87726
  }
87717
- function providerFor(harness, held3) {
87727
+ function providerFor(harness, held4) {
87718
87728
  const setting = providerSetting(harness.settings);
87719
87729
  if (!setting) return void 0;
87720
- return pickedProvider(setting, held3.values[setting.key] ?? "", isFilled(harness, held3));
87730
+ return pickedProvider(setting, held4.values[setting.key] ?? "", isFilled(harness, held4));
87721
87731
  }
87722
- function settingArgs(harness, held3) {
87723
- const { values } = held3;
87724
- const ignored = held3.ignored ?? [];
87725
- const picked = providerFor(harness, held3);
87732
+ function settingArgs(harness, held4) {
87733
+ const { values } = held4;
87734
+ const ignored = held4.ignored ?? [];
87735
+ const picked = providerFor(harness, held4);
87726
87736
  const list = providerSetting(harness.settings);
87727
87737
  const byHand = !!list && ignored.includes(list.key);
87728
87738
  const providerArgs = byHand ? [] : picked?.args ?? [];
@@ -87737,9 +87747,9 @@ function settingArgs(harness, held3) {
87737
87747
  });
87738
87748
  return [...providerArgs, ...flags, ...splitCommand(values[RAW_ARGS_KEY] ?? "")];
87739
87749
  }
87740
- function settingEnv(harness, held3, secrets, cwd) {
87741
- const { values } = held3;
87742
- const picked = providerFor(harness, held3);
87750
+ function settingEnv(harness, held4, secrets, cwd) {
87751
+ const { values } = held4;
87752
+ const picked = providerFor(harness, held4);
87743
87753
  const env = { ...harness.env() };
87744
87754
  for (const name of ownedVars(harness)) delete env[name];
87745
87755
  for (const name of Object.values(SESSION_VARS)) delete env[name];
@@ -87766,11 +87776,11 @@ function ownedVars(harness) {
87766
87776
  }
87767
87777
  return [...names2];
87768
87778
  }
87769
- function effectiveValues(harness, held3) {
87770
- const picked = providerFor(harness, held3);
87779
+ function effectiveValues(harness, held4) {
87780
+ const picked = providerFor(harness, held4);
87771
87781
  const out = {};
87772
87782
  for (const setting of harness.settings) {
87773
- const value = held3.values[setting.key];
87783
+ const value = held4.values[setting.key];
87774
87784
  if (!value || setting.kind === "secret") continue;
87775
87785
  if (!shownForProvider(harness.settings, setting.key, picked)) continue;
87776
87786
  out[setting.key] = value;
@@ -87789,26 +87799,30 @@ __export(api_exports, {
87789
87799
  claimJob: () => claimJob,
87790
87800
  commitBoard: () => commitBoard,
87791
87801
  createArtifact: () => createArtifact,
87792
- createTriage: () => createTriage,
87793
87802
  createUpload: () => createUpload,
87794
87803
  createVersion: () => createVersion,
87795
87804
  dispatchReview: () => dispatchReview,
87796
87805
  download: () => download,
87806
+ dropIdea: () => dropIdea,
87797
87807
  finishJob: () => finishJob,
87798
87808
  listAlerts: () => listAlerts,
87799
87809
  listChats: () => listChats,
87800
87810
  listElsewhere: () => listElsewhere,
87801
87811
  listFolders: () => listFolders,
87802
87812
  listJobs: () => listJobs,
87803
- listTriage: () => listTriage,
87813
+ listMachineTasks: () => listMachineTasks,
87804
87814
  listWorkflows: () => listWorkflows,
87805
87815
  markReviewed: () => markReviewed,
87816
+ openDiscussion: () => openDiscussion,
87806
87817
  openLog: () => openLog,
87818
+ openPresence: () => openPresence,
87819
+ presenceRoute: () => presenceRoute,
87807
87820
  readAgents: () => readAgents,
87808
87821
  readAlert: () => readAlert,
87809
87822
  readArtifact: () => readArtifact,
87810
87823
  readBoard: () => readBoard,
87811
87824
  readChat: () => readChat,
87825
+ readDiscussion: () => readDiscussion,
87812
87826
  readInbox: () => readInbox,
87813
87827
  readPlan: () => readPlan,
87814
87828
  readProfile: () => readProfile,
@@ -87825,9 +87839,13 @@ __export(api_exports, {
87825
87839
  renewJob: () => renewJob,
87826
87840
  request: () => request,
87827
87841
  requestAgent: () => requestAgent,
87842
+ saveDraft: () => saveDraft,
87828
87843
  setFolderNotify: () => setFolderNotify,
87844
+ setIdeaWorkflow: () => setIdeaWorkflow,
87829
87845
  showAlert: () => showAlert,
87846
+ startIdea: () => startIdea,
87830
87847
  startTrial: () => startTrial,
87848
+ stopTask: () => stopTask,
87831
87849
  targetVersion: () => targetVersion,
87832
87850
  upload: () => upload
87833
87851
  });
@@ -87838,25 +87856,25 @@ import path10 from "node:path";
87838
87856
  var file = () => path10.join(runnerHome(), "secrets.json");
87839
87857
  function readAll() {
87840
87858
  try {
87841
- const held3 = JSON.parse(fs8.readFileSync(file(), "utf8"));
87842
- return held3 && typeof held3 === "object" ? held3 : {};
87859
+ const held4 = JSON.parse(fs8.readFileSync(file(), "utf8"));
87860
+ return held4 && typeof held4 === "object" ? held4 : {};
87843
87861
  } catch {
87844
87862
  return {};
87845
87863
  }
87846
87864
  }
87847
87865
  var secretsOf = (runtime) => runtime ? readAll()[runtime] ?? {} : {};
87848
87866
  function keepSecrets(runtime, given) {
87849
- const all2 = readAll();
87850
- const held3 = { ...all2[runtime] };
87867
+ const all = readAll();
87868
+ const held4 = { ...all[runtime] };
87851
87869
  for (const [key, value] of Object.entries(given)) {
87852
- if (value) held3[key] = value;
87853
- else delete held3[key];
87870
+ if (value) held4[key] = value;
87871
+ else delete held4[key];
87854
87872
  }
87855
- if (Object.keys(held3).length) all2[runtime] = held3;
87856
- else delete all2[runtime];
87873
+ if (Object.keys(held4).length) all[runtime] = held4;
87874
+ else delete all[runtime];
87857
87875
  fs8.mkdirSync(runnerHome(), { recursive: true, mode: 448 });
87858
87876
  const temp = `${file()}.${process.pid}.tmp`;
87859
- fs8.writeFileSync(temp, `${JSON.stringify(all2)}
87877
+ fs8.writeFileSync(temp, `${JSON.stringify(all)}
87860
87878
  `, { mode: 384 });
87861
87879
  fs8.renameSync(temp, file());
87862
87880
  }
@@ -87864,7 +87882,7 @@ var secretsSet = () => Object.fromEntries(Object.entries(readAll()).map(([id, ke
87864
87882
 
87865
87883
  // src/api.ts
87866
87884
  var RUNTIMES = ["claude-code", "codex", "cursor", "opencode", "kimi", "dsh", "zcode", "grok"];
87867
- var PROTOCOL = 10;
87885
+ var PROTOCOL = 11;
87868
87886
  var TIMEOUT_MS = 2e4;
87869
87887
  async function send(method, route, body, signedIn, ms2, type) {
87870
87888
  const headers = { accept: "application/json" };
@@ -87931,6 +87949,12 @@ async function openLog(machineId2, job) {
87931
87949
  const url = `${cloudEndpoints().api.replace(/^http/, "ws")}/v1/runner/jobs/${encodeURIComponent(job)}/stream?machine=${encodeURIComponent(machineId2)}`;
87932
87950
  return new WebSocket(url, ["bearer", token.token]);
87933
87951
  }
87952
+ async function openPresence(machineId2) {
87953
+ const token = await accessToken();
87954
+ if (!token.ok) return null;
87955
+ return new WebSocket(`${cloudEndpoints().api.replace(/^http/, "ws")}/v1/runner/presence?machine=${encodeURIComponent(machineId2)}`, ["bearer", token.token]);
87956
+ }
87957
+ var presenceRoute = (machineId2) => request("GET", `/v1/runner/presence?machine=${encodeURIComponent(machineId2)}`);
87934
87958
  var finishJob = (machineId2, job, outcome, reason = "", report2) => move(job, "finish", { machineId: machineId2, outcome, reason, ...report2 ? { report: report2 } : {} });
87935
87959
  var answerDiff = (machineId2, id, answer) => request("POST", `/v1/runner/diffs/${encodeURIComponent(id)}`, { machineId: machineId2, ...answer });
87936
87960
  var listChats = (machineId2) => request("GET", `/v1/runner/chats?machine=${encodeURIComponent(machineId2)}`);
@@ -87947,7 +87971,9 @@ var showAlert = (machineId2, alert) => request("POST", `/v1/runner/alerts/${q(al
87947
87971
  var readAlert = (alert) => request("POST", `/v1/alerts/${q(alert)}/read`, {});
87948
87972
  var listFolders = (machineId2) => request("GET", `/v1/runner/folders?machine=${q(machineId2)}`);
87949
87973
  var listElsewhere = (machineId2) => request("GET", `/v1/runner/folders?machine=${q(machineId2)}&elsewhere=1`);
87950
- var addFolder = (machineId2, name, path27, project) => request("POST", "/v1/runner/folders", { machineId: machineId2, name, path: path27, ...project ? { project } : {} });
87974
+ var listMachineTasks = (machineId2) => request("GET", `/v1/runner/tasks?machine=${q(machineId2)}`);
87975
+ var stopTask = (task, how) => request("POST", how === "chat" ? `/v1/tasks/${q(task)}/chat/stop` : `/v1/tasks/${q(task)}/stop`, {});
87976
+ var addFolder = (machineId2, name, path29, project) => request("POST", "/v1/runner/folders", { machineId: machineId2, name, path: path29, ...project ? { project } : {} });
87951
87977
  var removeFolder = (machineId2, project) => request("POST", `/v1/runner/folders/${q(project)}/remove`, { machineId: machineId2 });
87952
87978
  var readPlan = () => request("GET", "/v1/billing");
87953
87979
  var startTrial = () => request("POST", "/v1/billing/trial", {});
@@ -87971,10 +87997,20 @@ var readVersion = (artifact, version) => request("GET", `/v1/artifacts/${q(artif
87971
87997
  var createArtifact = (task, name, type) => request("POST", `/v1/tasks/${q(task)}/artifacts`, { type, name });
87972
87998
  var createUpload = (artifact, contentType, size) => request("POST", `/v1/artifacts/${q(artifact)}/uploads`, { content_type: contentType, size });
87973
87999
  var createVersion = (artifact, upload2) => request("POST", `/v1/artifacts/${q(artifact)}/versions`, { upload: upload2 });
87974
- var listTriage = (project, state, after) => request("GET", `/v1/triage?project=${q(project)}&state=${state}&limit=100${after ? `&after=${q(after)}` : ""}`);
87975
- var createTriage = (project, title, text3, task, workflow) => request("POST", "/v1/triage", { project, title, text: text3, source_task: task, workflow });
87976
88000
  var readRejections = (workspace) => request("GET", `/v1/rejections?workspace=${q(workspace)}`);
87977
88001
  var markReviewed = (tasks, triage) => request("POST", "/v1/rejections/reviewed", { tasks, triage });
88002
+ var openDiscussion = (project, requestId, host = "claude-code") => request("POST", "/v1/discussions/external", { ...project ? { project } : {}, requestId, host });
88003
+ var readDiscussion = (id) => request("GET", `/v1/discussions/${q(id)}`);
88004
+ async function saveDraft(id, expectedVersion, ideas) {
88005
+ const sent = await send("PUT", `/v1/discussions/${q(id)}/draft`, { expectedVersion, ideas }, true, TIMEOUT_MS);
88006
+ if (!sent.ok) return sent;
88007
+ if (sent.value.status !== 409) return answerOf(sent.value);
88008
+ const answer = await sent.value.json().catch(() => null);
88009
+ return { ok: false, error: answer?.error?.message ?? "Cloud answered 409", code: answer?.error?.code ?? "refused", ...answer?.discussion ? { discussion: answer.discussion } : {} };
88010
+ }
88011
+ var setIdeaWorkflow = (id, idea, workflow) => request("PATCH", `/v1/discussions/${q(id)}/ideas/${q(idea)}`, { workflow });
88012
+ var dropIdea = (id, idea) => request("DELETE", `/v1/discussions/${q(id)}/ideas/${q(idea)}`);
88013
+ var startIdea = (id, idea, expectedVersion, project) => request("POST", `/v1/discussions/${q(id)}/ideas/${q(idea)}/start`, { expectedVersion, ...project ? { project } : {} });
87978
88014
 
87979
88015
  // src/runtimes.ts
87980
88016
  var HARNESSES2 = Object.fromEntries(RUNTIMES.map((name) => [name, HARNESSES.find((h3) => h3.name === name)]));
@@ -88001,7 +88037,7 @@ function plan(runtime, prompt, cwd, resume, model, given = {}, fork = false) {
88001
88037
  if (fork) resume = null;
88002
88038
  const extra = copied ?? (resume ? harness.resumeArgs(argv, resume, cwd) : harness.extraArgs(argv, id, cwd));
88003
88039
  const quiet = (line) => harness.quietStderr?.(line) ?? false;
88004
- const env = { ...process.env, ...settingEnv(harness, values, given.secrets ?? {}, cwd) };
88040
+ const env = { ...process.env, ...settingEnv(harness, values, given.secrets ?? {}, cwd), WILDCRANE_JOB: "1" };
88005
88041
  if (harness.client) {
88006
88042
  const client = harness.client(effectiveValues(harness, values));
88007
88043
  let session;
@@ -88702,8 +88738,8 @@ function realpath(p2) {
88702
88738
  }
88703
88739
  function readList() {
88704
88740
  try {
88705
- const held3 = JSON.parse(fs10.readFileSync(projectsFile(), "utf8"));
88706
- return Array.isArray(held3.projects) ? held3.projects.filter((p2) => typeof p2 === "string") : [];
88741
+ const held4 = JSON.parse(fs10.readFileSync(projectsFile(), "utf8"));
88742
+ return Array.isArray(held4.projects) ? held4.projects.filter((p2) => typeof p2 === "string") : [];
88707
88743
  } catch {
88708
88744
  return [];
88709
88745
  }
@@ -88789,10 +88825,10 @@ function markUnsent(dir, unsent) {
88789
88825
  var busy = /* @__PURE__ */ new Map();
88790
88826
  function hold(dir) {
88791
88827
  busy.set(dir, (busy.get(dir) ?? 0) + 1);
88792
- let held3 = true;
88828
+ let held4 = true;
88793
88829
  return () => {
88794
- if (!held3) return;
88795
- held3 = false;
88830
+ if (!held4) return;
88831
+ held4 = false;
88796
88832
  const left = busy.get(dir) - 1;
88797
88833
  if (left) busy.set(dir, left);
88798
88834
  else busy.delete(dir);
@@ -88801,8 +88837,8 @@ function hold(dir) {
88801
88837
  function paused(dir) {
88802
88838
  try {
88803
88839
  const file2 = path13.join(projectStateDir(path13.join(akbDir(dir), "kanban")), SESSIONS_FILE);
88804
- const held3 = JSON.parse(fs11.readFileSync(file2, "utf8"));
88805
- return (held3.deliveries ?? []).some((d2) => d2.status === "active");
88840
+ const held4 = JSON.parse(fs11.readFileSync(file2, "utf8"));
88841
+ return (held4.deliveries ?? []).some((d2) => d2.status === "active");
88806
88842
  } catch {
88807
88843
  return false;
88808
88844
  }
@@ -88860,12 +88896,12 @@ async function sweepCache(machineId2, now = Date.now()) {
88860
88896
  }
88861
88897
  }
88862
88898
  for (const dir of allowedProjects()) {
88863
- let held3 = [];
88899
+ let held4 = [];
88864
88900
  try {
88865
- held3 = fs11.readdirSync(akbDir(dir));
88901
+ held4 = fs11.readdirSync(akbDir(dir));
88866
88902
  } catch {
88867
88903
  }
88868
- if (!held3.length) continue;
88904
+ if (!held4.length) continue;
88869
88905
  const gone = access && !access.folders.has(dir);
88870
88906
  if ((idle(dir, now) || gone) && clearAkb(dir)) swept.push(dir);
88871
88907
  }
@@ -88940,8 +88976,8 @@ var home3 = () => runnerHome();
88940
88976
  var reviewedFile = () => path15.join(taskChatsDir(home3()), "reviewed.json");
88941
88977
  function readReviewed() {
88942
88978
  try {
88943
- const held3 = JSON.parse(fs13.readFileSync(reviewedFile(), "utf8"));
88944
- return { checkedAt: Number(held3.checkedAt) || 0, tasks: held3.tasks ?? {} };
88979
+ const held4 = JSON.parse(fs13.readFileSync(reviewedFile(), "utf8"));
88980
+ return { checkedAt: Number(held4.checkedAt) || 0, tasks: held4.tasks ?? {} };
88945
88981
  } catch {
88946
88982
  return { checkedAt: 0, tasks: {} };
88947
88983
  }
@@ -89104,42 +89140,96 @@ async function answerHint(machineId2, hint2) {
89104
89140
  }
89105
89141
 
89106
89142
  // src/flow.ts
89107
- import fs21 from "node:fs";
89108
- import path23 from "node:path";
89143
+ import fs22 from "node:fs";
89144
+ import path24 from "node:path";
89109
89145
 
89110
89146
  // src/akb.ts
89111
89147
  import { spawn as spawn2 } from "node:child_process";
89148
+ import fs16 from "node:fs";
89149
+ import path18 from "node:path";
89150
+ import { fileURLToPath } from "node:url";
89151
+
89152
+ // ../cli/src/lib/machine/settings.ts
89112
89153
  import fs15 from "node:fs";
89113
89154
  import path17 from "node:path";
89114
- import { fileURLToPath } from "node:url";
89155
+
89156
+ // ../cli/src/lib/machine/types.ts
89157
+ var LANGUAGES = ["en", "zh"];
89158
+ var DEFAULT_LANGUAGE = "en";
89159
+ function isLanguage(value) {
89160
+ return typeof value === "string" && LANGUAGES.includes(value);
89161
+ }
89162
+
89163
+ // ../cli/src/lib/machine/settings.ts
89164
+ var settingsFile = () => path17.join(machineHome(), "settings.json");
89165
+ function heldSettings() {
89166
+ let text3;
89167
+ try {
89168
+ text3 = fs15.readFileSync(settingsFile(), "utf8");
89169
+ } catch (err) {
89170
+ if (err.code === "ENOENT") return { unreadable: false, values: {} };
89171
+ return { unreadable: true, values: {} };
89172
+ }
89173
+ try {
89174
+ const parsed = JSON.parse(text3);
89175
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return { unreadable: true, values: {} };
89176
+ return { unreadable: false, values: parsed };
89177
+ } catch {
89178
+ return { unreadable: true, values: {} };
89179
+ }
89180
+ }
89181
+ var held2 = () => heldSettings().values;
89182
+ function readLanguage() {
89183
+ const value = held2().language;
89184
+ return isLanguage(value) ? value : DEFAULT_LANGUAGE;
89185
+ }
89186
+
89187
+ // ../cli/src/lib/agent/language.ts
89188
+ var RUN_LANGUAGE_ENV = "AI4KANBAN_RUN_LANGUAGE";
89189
+ var LINES = {
89190
+ en: "",
89191
+ zh: "\u8BF7\u7528\u4E2D\u6587\u56DE\u590D\u548C\u5199\u4F5C\u3002"
89192
+ };
89193
+ function runLanguage() {
89194
+ const asked = process.env[RUN_LANGUAGE_ENV];
89195
+ return isLanguage(asked) ? asked : readLanguage();
89196
+ }
89197
+ function languageNote(language = runLanguage()) {
89198
+ return LINES[language];
89199
+ }
89200
+ var inLanguage = (prompt, language = runLanguage()) => [prompt, languageNote(language)].filter(Boolean).join("\n\n");
89201
+
89202
+ // src/akb.ts
89115
89203
  var bundledAkb = () => ({
89116
89204
  command: process.execPath,
89117
- args: [path17.join(path17.dirname(fileURLToPath(import.meta.url)), "akb", "bin", "ai4kanban.mjs")]
89205
+ args: [path18.join(path18.dirname(fileURLToPath(import.meta.url)), "akb", "bin", "ai4kanban.mjs")]
89118
89206
  });
89119
89207
  var quote = (s) => /^[\w./:@=-]+$/.test(s) ? s : `"${s}"`;
89120
89208
  function shimDir(akb) {
89121
- const dir = path17.join(runnerHome(), "bin", "akb");
89209
+ const dir = path18.join(runnerHome(), "bin", "akb");
89122
89210
  const line = [akb.command, ...akb.args].map(quote).join(" ");
89123
- const [file2, text3] = process.platform === "win32" ? [path17.join(dir, "akb.cmd"), `@${line} %*\r
89124
- `] : [path17.join(dir, "akb"), `#!/bin/sh
89211
+ const [file2, text3] = process.platform === "win32" ? [path18.join(dir, "akb.cmd"), `@${line} %*\r
89212
+ `] : [path18.join(dir, "akb"), `#!/bin/sh
89125
89213
  exec ${line} "$@"
89126
89214
  `];
89127
89215
  try {
89128
- if (fs15.readFileSync(file2, "utf8") === text3) return dir;
89216
+ if (fs16.readFileSync(file2, "utf8") === text3) return dir;
89129
89217
  } catch {
89130
89218
  }
89131
- fs15.mkdirSync(dir, { recursive: true });
89132
- fs15.writeFileSync(file2, text3, { mode: 493 });
89219
+ fs16.mkdirSync(dir, { recursive: true });
89220
+ fs16.writeFileSync(file2, text3, { mode: 493 });
89133
89221
  return dir;
89134
89222
  }
89135
- function akbEnv(akb, board, task) {
89223
+ function akbEnv(akb, board, task, lang) {
89136
89224
  const env = {};
89137
- for (const [k, v2] of Object.entries(process.env)) if (!k.startsWith("KANBAN_") && k !== "AI4KANBAN_BOARD" && k !== "AI4KANBAN_TASK") env[k] = v2;
89225
+ const dropped = ["AI4KANBAN_BOARD", "AI4KANBAN_TASK", RUN_LANGUAGE_ENV];
89226
+ for (const [k, v2] of Object.entries(process.env)) if (!k.startsWith("KANBAN_") && !dropped.includes(k)) env[k] = v2;
89138
89227
  const key = Object.keys(env).find((k) => k.toUpperCase() === "PATH") ?? "PATH";
89139
- env[key] = [shimDir(akb), env[key]].filter(Boolean).join(path17.delimiter);
89228
+ env[key] = [shimDir(akb), env[key]].filter(Boolean).join(path18.delimiter);
89140
89229
  env.AI4KANBAN_COMMAND = "akb";
89141
89230
  if (board) env.AI4KANBAN_BOARD = board;
89142
89231
  if (task) env.AI4KANBAN_TASK = task;
89232
+ if (lang) env[RUN_LANGUAGE_ENV] = lang;
89143
89233
  return env;
89144
89234
  }
89145
89235
  var KEEP = 256 * 1024;
@@ -89153,7 +89243,7 @@ function runAkb(akb, args, opts) {
89153
89243
  };
89154
89244
  let child;
89155
89245
  try {
89156
- child = spawn2(akb.command, [...akb.args, ...args], { cwd: opts.cwd, env: akbEnv(akb, opts.board, opts.task), stdio: ["ignore", "pipe", "pipe"] });
89246
+ child = spawn2(akb.command, [...akb.args, ...args], { cwd: opts.cwd, env: akbEnv(akb, opts.board, opts.task, opts.lang), stdio: ["ignore", "pipe", "pipe"] });
89157
89247
  } catch (e2) {
89158
89248
  return resolve2({ code: null, out: "", last: e2 instanceof Error ? e2.message : String(e2) });
89159
89249
  }
@@ -89404,21 +89494,22 @@ function diff3Merge(a3, o2, b2, options) {
89404
89494
  // ../cloud/src/board-files.ts
89405
89495
  var ROOTS = ["todo/", ".archive/", "memory/", "workflows/", "triage/", "assets/", "rules/"];
89406
89496
  var SETTINGS = ["modules.md", "config.md"];
89407
- var isSettingsPath = (path27) => SETTINGS.includes(path27) || /^rules\/(?:workflows\/)?[^/]+\.md$/.test(path27);
89497
+ var isSettingsPath = (path29) => SETTINGS.includes(path29) || /^rules\/(?:workflows\/)?[^/]+\.md$/.test(path29);
89408
89498
  var BOARD_FILE_LIMIT = 256 * 1024;
89409
- function isBoardPath(path27) {
89410
- if (!path27 || path27.length > 500 || path27.startsWith("/") || path27.includes("\\") || path27.includes("\0")) return false;
89411
- if (path27.split("/").some((part) => !part || part === "." || part === "..")) return false;
89412
- if (path27 === "todo/README.md" || path27.split("/").includes(".sync")) return false;
89413
- if (path27.startsWith("workflows/")) return true;
89414
- if (SETTINGS.includes(path27)) return true;
89415
- if (path27.startsWith("rules/")) return isSettingsPath(path27);
89416
- if (path27.startsWith("assets/")) return path27.endsWith(".json");
89417
- return ROOTS.some((root) => path27.startsWith(root)) && path27.endsWith(".md");
89499
+ function isBoardPath(path29) {
89500
+ if (!path29 || path29.length > 500 || path29.startsWith("/") || path29.includes("\\") || path29.includes("\0")) return false;
89501
+ if (path29.split("/").some((part) => !part || part === "." || part === "..")) return false;
89502
+ if (path29 === "todo/README.md" || path29.split("/").includes(".sync")) return false;
89503
+ if (path29.startsWith("workflows/")) return true;
89504
+ if (SETTINGS.includes(path29)) return true;
89505
+ if (path29.startsWith("rules/")) return isSettingsPath(path29);
89506
+ if (path29.startsWith("assets/")) return path29.endsWith(".json");
89507
+ return ROOTS.some((root) => path29.startsWith(root)) && path29.endsWith(".md");
89418
89508
  }
89419
89509
  var cardSlug = (task) => task.toLowerCase().replace(/_/g, "-");
89420
89510
  var assetsFile = (task) => `assets/${task}.json`;
89421
89511
  var workflowFolder = (id) => id.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
89512
+ var workflowName = (id) => id.startsWith("wf_builtin_") ? id.slice("wf_builtin_".length).replace(/_/g, "-") : workflowFolder(id);
89422
89513
  var BUILDER = "wf_builtin_workflow_builder";
89423
89514
  var draftFolder = (task) => cardSlug(task);
89424
89515
  var unquote2 = (value) => value.trim().replace(/^(["'])(.*)\1$/, "$2");
@@ -89442,24 +89533,46 @@ function cardFront(body) {
89442
89533
  return out;
89443
89534
  }
89444
89535
 
89445
- // src/memory-files.ts
89446
- import fs16 from "node:fs";
89447
- import path18 from "node:path";
89536
+ // ../cloud/src/triage-files.ts
89537
+ var ITEM = /^triage\/(?:(dismissed|archived)\/)?([^/]+)\.md$/;
89538
+ function itemAt(path29) {
89539
+ const m = ITEM.exec(path29);
89540
+ if (!m) return null;
89541
+ return { state: m[1] === "dismissed" ? "dismissed" : m[1] === "archived" ? "carded" : "open", name: m[2] };
89542
+ }
89543
+ function stampItem(content, fields) {
89544
+ const lines = content.split("\n");
89545
+ const close = lines.indexOf("---", 1);
89546
+ if (close < 0) return content;
89547
+ const kept3 = lines.slice(1, close).filter((line) => {
89548
+ const key = /^([a-z_]+):/.exec(line);
89549
+ return !key || fields[key[1]] === void 0;
89550
+ });
89551
+ const added = Object.entries(fields).flatMap(([k, v2]) => v2 === null ? [] : [`${k}: ${yamlScalar(v2)}`]);
89552
+ return ["---", ...kept3, ...added, ...lines.slice(close)].join("\n");
89553
+ }
89554
+
89555
+ // ../cli/src/lib/agents/shared-memory.ts
89556
+ import fs17 from "node:fs";
89557
+ import path19 from "node:path";
89558
+
89559
+ // ../cli/src/agents/api-designer/AGENT.md
89560
+ var AGENT_default = "---\nname: api-designer\ndescription: Writes the caller's documentation for the APIs a card changes.\nakb:\n when: A card adds or changes a surface a user or third party calls directly, such as HTTP routes, WebSocket messages, MCP tools, or CLI commands and flags. Not a protocol between the project's own components.\n i18n:\n zh:\n title: \u63A5\u53E3\u8BBE\u8BA1\u5E08\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";
89448
89561
 
89449
89562
  // ../cli/src/agents/blog-illustrator/AGENT.md
89450
- var AGENT_default = "---\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: Blog illustrator\n zh:\n title: \u535A\u5BA2\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> --out <file> \"<prompt>\"`, which spends the\n user's Pro credits; never for a product interface.\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";
89563
+ var AGENT_default2 = "---\nname: blog-illustrator\ndescription: Makes a blog post's cover and in-article images, each with alt text.\nakb:\n after: [blog-planner]\n memoryFiles:\n workspace: [style.md]\n i18n:\n en:\n title: Blog illustrator\n zh:\n title: \u535A\u5BA2\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> --out <file> \"<prompt>\"`, which spends the\n user's Pro credits; never for a product interface.\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";
89451
89564
 
89452
89565
  // ../cli/src/agents/blog-planner/AGENT.md
89453
- var AGENT_default2 = "---\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: Blog planner\n zh:\n title: \u535A\u5BA2\u7B56\u5212\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 `[user]` 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";
89566
+ var AGENT_default3 = "---\nname: blog-planner\ndescription: Writes a blog post's brief and outline, the plan its article is written from.\nakb:\n memoryFiles:\n workspace: [general.md]\n i18n:\n en:\n title: Blog planner\n zh:\n title: \u535A\u5BA2\u7B56\u5212\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 `[user]` 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";
89454
89567
 
89455
89568
  // ../cli/src/agents/blog-writer/AGENT.md
89456
- var AGENT_default3 = "---\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: Blog writer\n zh:\n title: \u535A\u5BA2\u5199\u4F5C\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";
89569
+ var AGENT_default4 = "---\nname: blog-writer\ndescription: Writes a blog post's article from its approved outline and delivers one copy per destination.\nakb:\n after: [blog-planner, blog-illustrator]\n i18n:\n en:\n title: Blog writer\n zh:\n title: \u535A\u5BA2\u5199\u4F5C\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";
89457
89570
 
89458
89571
  // ../cli/src/agents/carousel-planner/AGENT.md
89459
- var AGENT_default4 = "---\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: Social post planner\n zh:\n title: \u793E\u5A92\u5E16\u7B56\u5212\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 `[user]` 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";
89572
+ var AGENT_default5 = "---\nname: carousel-planner\ndescription: Writes a social post's brief and copy and, for a carousel, a page outline on a saved format.\nakb:\n memoryFiles:\n workspace: [general.md, format-*.md]\n i18n:\n en:\n title: Social post planner\n zh:\n title: \u793E\u5A92\u5E16\u7B56\u5212\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 `[user]` 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";
89460
89573
 
89461
89574
  // ../cli/src/agents/carousel-renderer/AGENT.md
89462
- var AGENT_default5 = "---\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: Social post renderer\n zh:\n title: \u793E\u5A92\u5E16\u51FA\u56FE\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";
89575
+ var AGENT_default6 = "---\nname: carousel-renderer\ndescription: Renders a social post's carousel pages from its approved copy and writes each platform's caption.\nakb:\n after: [carousel-planner, social-illustrator]\n i18n:\n en:\n title: Social post renderer\n zh:\n title: \u793E\u5A92\u5E16\u51FA\u56FE\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";
89463
89576
 
89464
89577
  // ../cli/src/agents/carousel-renderer/references/slides.example.json
89465
89578
  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';
@@ -89471,7 +89584,7 @@ var slides_schema_default = '{\n "$schema": "https://json-schema.org/draft/2020
89471
89584
  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";
89472
89585
 
89473
89586
  // ../cli/src/agents/competitor-research/AGENT.md
89474
- var AGENT_default6 = `---
89587
+ var AGENT_default7 = `---
89475
89588
  name: competitor-research
89476
89589
  description: Researches how competitors handle what a card changes.
89477
89590
  akb:
@@ -89528,25 +89641,25 @@ One table, a short verdict, and the sources:
89528
89641
  `;
89529
89642
 
89530
89643
  // ../cli/src/agents/copywriting/AGENT.md
89531
- var AGENT_default7 = '---\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 & docs\n zh:\n title: \u6587\u6848\u4E0E\u6587\u6863\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';
89644
+ var AGENT_default8 = '---\nname: copywriting\ndescription: Writes the user-facing text a card changes outside the product\'s screens.\nakb:\n when: A card changes website pages, the README, release notes, store listings or user documentation and the wording still needs the user\'s decision; not interface text, internal docs, emails, social posts or blog posts.\n memoryFiles:\n workspace: [style.md]\n i18n:\n en:\n title: Copy & docs\n zh:\n title: \u6587\u6848\u4E0E\u6587\u6863\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';
89532
89645
 
89533
89646
  // ../cli/src/agents/cover-designer/AGENT.md
89534
- var AGENT_default8 = "---\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> [--ref <file>]\u2026 --out <file> \"<prompt>\"`, which spends the user's Pro credits per image. 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 `[user]` 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";
89647
+ var AGENT_default9 = "---\nname: cover-designer\ndescription: Makes a product video's cover image from its script.\nakb:\n memoryFiles:\n workspace: [preferences.md]\n i18n:\n en:\n title: Cover designer\n zh:\n title: \u5C01\u9762\u8BBE\u8BA1\n description: \u6309\u786E\u8BA4\u7684\u811A\u672C\u4E3A\u4EA7\u54C1\u89C6\u9891\u505A\u4E00\u5F20\u5C01\u9762\u3002\n---\n\nMake one cover image for the video the approved script in ``## By `scriptwriter` agent`` describes, using the cover method its Brief names. Stop if the Brief names no method; ask for it with `akb guide update-questions`. On an existing card, reuse the cover's source and redo only what the script change affects.\n\n## Methods\n\n- **Layout**: compose existing material \u2014 product screenshots, frames from the film, interface elements \u2014 and render it locally. Choose the material, composition and tools yourself.\n- **Generated**: create the image with `akb cloud image --aspect <ratio> [--ref <file>]\u2026 --out <file> \"<prompt>\"`, which spends the user's Pro credits per image. 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 `[user]` 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";
89535
89648
 
89536
89649
  // ../cli/src/agents/demo-rehearser/AGENT.md
89537
- var AGENT_default9 = "---\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 rehearser\n zh:\n title: \u6F14\u793A\u6392\u7EC3\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";
89650
+ var AGENT_default10 = "---\nname: demo-rehearser\ndescription: Rehearses a product video's demo in an isolated environment and returns its key screenshots for the film.\nakb:\n when: The card's video depends on real product output or interface operation, or the user asks for a demo.\n memoryFiles:\n workspace: [feedback.md]\n i18n:\n en:\n title: Demo rehearser\n zh:\n title: \u6F14\u793A\u6392\u7EC3\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";
89538
89651
 
89539
89652
  // ../cli/src/agents/email-planner/AGENT.md
89540
- var AGENT_default10 = "---\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 zh:\n title: \u90AE\u4EF6\u7B56\u5212\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 `[user]` 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 `[user]` 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";
89653
+ var AGENT_default11 = "---\nname: email-planner\ndescription: Writes and previews the emails a card adds or changes.\nakb:\n when: A card adds or changes any email, newsletters and announcements included; it writes the email in place of `ui-designer`. Not a card that changes only how mail is delivered.\n memoryFiles:\n workspace: [style.md]\n i18n:\n zh:\n title: \u90AE\u4EF6\u7B56\u5212\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 `[user]` 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 `[user]` 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";
89541
89654
 
89542
89655
  // ../cli/src/agents/email-planner/scripts/render.mjs
89543
89656
  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";
89544
89657
 
89545
89658
  // ../cli/src/agents/deck-builder/AGENT.md
89546
- var AGENT_default11 = '---\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: Deck builder\n zh:\n title: \u6F14\u793A\u6587\u7A3F\u5236\u4F5C\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';
89659
+ var AGENT_default12 = '---\nname: deck-builder\ndescription: Builds every slide of a deck in its final look from the approved wireframes and samples, and exports the image-based .pptx.\nakb:\n after: [deck-planner]\n i18n:\n en:\n title: Deck builder\n zh:\n title: \u6F14\u793A\u6587\u7A3F\u5236\u4F5C\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';
89547
89660
 
89548
89661
  // ../cli/src/agents/deck-planner/AGENT.md
89549
- var AGENT_default12 = '---\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: Deck planner\n zh:\n title: \u6F14\u793A\u6587\u7A3F\u7B56\u5212\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 `[user]` 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';
89662
+ var AGENT_default13 = '---\nname: deck-planner\ndescription: Lays out a slide deck as wireframes, with sample slides in the final look.\nakb:\n memoryFiles:\n workspace: [feedback.md]\n i18n:\n en:\n title: Deck planner\n zh:\n title: \u6F14\u793A\u6587\u7A3F\u7B56\u5212\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 `[user]` 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';
89550
89663
 
89551
89664
  // ../cli/src/agents/deck-planner/references/slides.example.json
89552
89665
  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';
@@ -89739,10 +89852,10 @@ process.stdout.write(\`\${out}: \${count} slide(s), one image each\\n\`)
89739
89852
  `;
89740
89853
 
89741
89854
  // ../cli/src/agents/illustrator/AGENT.md
89742
- var AGENT_default13 = "---\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: Illustrator\n zh:\n title: \u4EA7\u54C1\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";
89855
+ var AGENT_default14 = "---\nname: illustrator\ndescription: Makes the images and animations a card needs.\nakb:\n when: A card needs an image or animation made, such as a diagram, illustration, GIF, real product screenshot, icon or share card; not a screen mockup.\n i18n:\n en:\n title: Illustrator\n zh:\n title: \u4EA7\u54C1\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";
89743
89856
 
89744
89857
  // ../cli/src/agents/hyperframes-editor/AGENT.md
89745
- var AGENT_default14 = "---\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 editor\n zh:\n title: \u89C6\u9891\u526A\u8F91\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 `[user]` 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 `[user]` question, never a silent change.\n- **Narration**: generate it from the exact scripted lines with the approved voice: `akb cloud tts` for a hosted voice, local Kokoro for a Kokoro ID, `af_heart` / `zf_xiaobei` when the source names none. Never switch voice 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 `[user]` 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";
89858
+ var AGENT_default15 = "---\nname: hyperframes-editor\ndescription: Produces and checks a product video from its script, using the production method that suits it.\nakb:\n after: [scriptwriter, demo-rehearser]\n i18n:\n en:\n title: Video editor\n zh:\n title: \u89C6\u9891\u526A\u8F91\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 `[user]` 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 `[user]` question, never a silent change.\n- **Narration**: generate it from the exact scripted lines with the approved voice: `akb cloud tts` for a hosted voice, local Kokoro for a Kokoro ID, `af_heart` / `zf_xiaobei` when the source names none. Never switch voice 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 `[user]` 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";
89746
89859
 
89747
89860
  // ../cli/src/agents/hyperframes-editor/record.mjs
89748
89861
  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";
@@ -89751,16 +89864,19 @@ var record_default = "#!/usr/bin/env node\n// Record scripted product shots over
89751
89864
  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";
89752
89865
 
89753
89866
  // ../cli/src/agents/prompt-writer/AGENT.md
89754
- var AGENT_default15 = "---\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 zh:\n title: \u63D0\u793A\u8BCD\u64B0\u5199\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";
89867
+ var AGENT_default16 = "---\nname: prompt-writer\ndescription: Writes the instruction text a card changes, as a diff per file.\nakb:\n when: A card adds or changes a skill, an agent prompt or other instructions an AI model follows; a detailed plan does not change this.\n memoryFiles:\n workspace: [style.md]\n i18n:\n zh:\n title: \u63D0\u793A\u8BCD\u64B0\u5199\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";
89755
89868
 
89756
89869
  // ../cli/src/agents/qa-manager/AGENT.md
89757
- var AGENT_default16 = "---\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";
89870
+ var AGENT_default17 = "---\nname: qa-manager\ndescription: Keeps the project's test cases true to the product \u2014 follows every finished card and covers what the product already does.\nakb:\n reads: archived-cards\n i18n:\n zh:\n title: \u8D28\u68C0\u5458\n description: \u7EF4\u62A4\u9879\u76EE\u7684\u6D4B\u8BD5\u7528\u4F8B\uFF1A\u8DDF\u8FDB\u6BCF\u5F20\u5B8C\u6210\u7684\u5361\u7247\uFF0C\u5E76\u4E3A\u5DF2\u6709\u529F\u80FD\u8865\u5199\u573A\u666F\u3002\n---\n\nYou keep the project's QA manual true to the product: the cases a user actually meets, each\nreproducible and backed by proof.\n\n## The manual\n\n- **Location**: `docs/qa/<module>/<case>/case.md`, one folder per case, under a module the\n board's `modules.md` names; when it names none, `docs/qa/<case>/case.md`.\n- **Index**: `docs/qa/README.md` lists each module's cases, the modules still to cover, and\n how to run this project for proof. Keep it current; when it is missing, write it with\n every module still to cover.\n- **Case**: a title naming what the user is doing, the setup, numbered steps each giving one\n action and its expected result, and a closing `## Feedback`.\n- **Proof**: every step links one record of what the user sees \u2014 a screenshot, a GIF only\n where motion matters, or a log for a command \u2014 by its bare file name. Save it in the\n project's `.akb/qa/`, in the same `<module>/<case>/` folder as its `case.md`: proof is never\n committed. Rerunning a case empties its folder there first.\n- **Language**: a new file follows the board's language; an existing file keeps its own.\n\n## Each run\n\nDo both, in order. A sub-run does neither: see \"Sub-runs\".\n\n1. **Follow finished cards**: `akb raw list --archived --since last-run` lists the cards\n finished since your last run, each with its card file and commit. Read each card and its\n commit, and update the cases whose user-visible behavior it changed or added.\n2. **Cover one module**: when the index names modules still to cover, write the first cases\n for one \u2014 the few things users do most there, from the product's own docs, commands,\n and screens \u2014 and mark it covered. Mark a module with nothing a user does covered, with\n the reason.\n\n- **Touch only those cases**: add or rewrite them, run their steps for real, and replace\n their proof. Leave every other case untouched and unrun.\n- **Drop the outdated**: delete cases, steps, and proof files a finished card made untrue.\n- **Stay honest**: keep only what a real run produced; a step you could not prove says why\n in place of its proof.\n- **Keep secrets out**: no keys, accounts, or personal data in any proof.\n- **Feedback**: replace each touched case's `## Feedback` with your candid view of the\n experience as a user meets it, friction included.\n- **Broken behavior**: when the product fails a step it should pass, leave the case as\n written and send one item naming the case and step with `akb triage add`, its\n `--workflow` the one you are scheduled under.\n- **Change nothing else**: no product code and no card.\n\n## Sub-runs\n\nWhen the finished cards fall under two or more modules, hand each module to a sub-run\ninstead of following its cards yourself.\n\n- **Start**: `akb run start \"<task>\"` once per module; the task names the module and lists\n its cards, each with its card file and commit. A card under several modules goes to each.\n- **Wait**: repeat `akb run wait` until no sub-run is still running.\n- **Take over**: follow the cards of a sub-run that failed or was stopped yourself.\n- **Index**: only you edit `docs/qa/README.md`, once every sub-run has ended.\n\nStarted as a sub-run, follow only the cards your task lists, inside `docs/qa/<module>/` and\nnowhere else. Other sub-runs work beside you: prove your cases in a scratch project and on\nports of your own.\n";
89871
+
89872
+ // ../cli/src/agents/schema-designer/AGENT.md
89873
+ var AGENT_default18 = "---\nname: schema-designer\ndescription: Previews the database schema change a card makes.\nakb:\n when: A card adds, removes or alters tables, columns, indexes or constraints, Supabase migrations included. Not a change that only reads or writes rows.\n i18n:\n zh:\n title: \u6570\u636E\u5E93\u8BBE\u8BA1\u5E08\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";
89758
89874
 
89759
89875
  // ../cli/src/agents/scriptwriter/AGENT.md
89760
- var AGENT_default17 = "---\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 zh:\n title: \u811A\u672C\u4F5C\u8005\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 `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, ask one single-choice `[user]` 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 `[user]` 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";
89876
+ var AGENT_default19 = "---\nname: scriptwriter\ndescription: Writes a product video's script, the one its film and cover are made from.\nakb:\n memoryFiles:\n workspace: [feedback.md, feedback/]\n i18n:\n zh:\n title: \u811A\u672C\u4F5C\u8005\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 `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, ask one single-choice `[user]` 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 `[user]` 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";
89761
89877
 
89762
89878
  // ../cli/src/agents/social-illustrator/AGENT.md
89763
- var AGENT_default18 = '---\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: Social post illustrator\n zh:\n title: \u793E\u5A92\u5E16\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> --out <file> "<prompt>"`, which spends the\n user\'s Pro credits; never for a product interface.\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';
89879
+ var AGENT_default20 = '---\nname: social-illustrator\ndescription: "Makes a social post\'s images: the image slots a carousel\'s pages name, or one image for a text-only post."\nakb:\n after: [carousel-planner]\n i18n:\n en:\n title: Social post illustrator\n zh:\n title: \u793E\u5A92\u5E16\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> --out <file> "<prompt>"`, which spends the\n user\'s Pro credits; never for a product interface.\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';
89764
89880
 
89765
89881
  // ../cli/src/agents/scriptwriter/references/index.md
89766
89882
  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";
@@ -89876,55 +89992,57 @@ writeFileSync(resolve(project, 'index.html'), \`<!DOCTYPE html>
89876
89992
  `;
89877
89993
 
89878
89994
  // ../cli/src/agents/ui-designer/AGENT.md
89879
- var AGENT_default19 = "---\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 zh:\n title: \u754C\u9762\u8BBE\u8BA1\u5E08\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";
89995
+ var AGENT_default21 = "---\nname: ui-designer\ndescription: Draws the screens a card changes, with every word on them.\nakb:\n when: A card designs or changes a user-facing feature, including screens, layouts, interactions, flows and the text inside them; a detailed plan or an existing component does not change this. Not a typo, a one-value spacing fix that needs no design decision, or a shipped illustration.\n after: [copywriting, illustrator]\n memoryFiles:\n workspace: [style.md]\n i18n:\n zh:\n title: \u754C\u9762\u8BBE\u8BA1\u5E08\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";
89880
89996
 
89881
89997
  // ../cli/src/agents/ui-designer/references/rendered-screen.md
89882
89998
  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';
89883
89999
 
89884
90000
  // ../cli/src/agents/tech-stack-advisor/AGENT.md
89885
- var AGENT_default20 = "---\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 zh:\n title: \u6280\u672F\u9009\u578B\u987E\u95EE\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";
90001
+ var AGENT_default22 = "---\nname: tech-stack-advisor\ndescription: Compares the outside libraries, tools or services a card could pick.\nakb:\n when: A card makes a new pick of an outside library, tool or service.\n i18n:\n zh:\n title: \u6280\u672F\u9009\u578B\u987E\u95EE\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";
89886
90002
 
89887
90003
  // ../cli/src/agents/video-cloner/AGENT.md
89888
- var AGENT_default21 = "---\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: Video cloner\n zh:\n title: \u89C6\u9891\u514B\u9686\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";
90004
+ var AGENT_default23 = "---\nname: video-cloner\ndescription: Breaks a video made with code down beat by beat and drafts its clone with the user's content.\nakb:\n memoryFiles:\n workspace: [motions/, props/, motions.md, props.md, feedback.md]\n session: new\n i18n:\n en:\n title: Video cloner\n zh:\n title: \u89C6\u9891\u514B\u9686\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";
89889
90005
 
89890
90006
  // ../cli/src/agents/video-cloner/references/music.md
89891
90007
  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";
89892
90008
 
89893
90009
  // ../cli/src/agents/video-template-builder/AGENT.md
89894
- var AGENT_default22 = "---\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 builder\n zh:\n title: \u89C6\u9891\u6A21\u677F\u5236\u4F5C\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";
90010
+ var AGENT_default24 = "---\nname: video-template-builder\ndescription: Builds a video clone's reusable beat-table template from its approved drafts, renders it once and checks it against the source beat by beat.\nakb:\n after: [video-cloner]\n session: new\n i18n:\n en:\n title: Video template builder\n zh:\n title: \u89C6\u9891\u6A21\u677F\u5236\u4F5C\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";
89895
90011
 
89896
90012
  // ../cli/src/agents/workflow-writer/AGENT.md
89897
- var AGENT_default23 = "---\nname: workflow-writer\ndescription: Writes a new workflow, or changes one of the user's own, as a `WORKFLOW.md` and the agents it names.\nakb:\n i18n:\n en:\n title: Workflow writer\n zh:\n title: \u642D\u5EFA\u5DE5\u4F5C\u6D41\n description: \u6309\u4F60\u7684\u63CF\u8FF0\u5199\u51FA\u4E00\u4E2A\u5DE5\u4F5C\u6D41\u53CA\u5176 agent\uFF0C\u6216\u4FEE\u6539\u4F60\u5DF2\u6709\u7684\u5DE5\u4F5C\u6D41\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.\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\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, and what each agent remembers; then embed each file you wrote as an `<Asset>`.\n";
90013
+ var AGENT_default25 = "---\nname: workflow-writer\ndescription: Writes a new workflow, or changes one of the user's own, as a `WORKFLOW.md` and the agents it names.\nakb:\n i18n:\n en:\n title: Workflow writer\n zh:\n title: \u642D\u5EFA\u5DE5\u4F5C\u6D41\n description: \u6309\u4F60\u7684\u63CF\u8FF0\u5199\u51FA\u4E00\u4E2A\u5DE5\u4F5C\u6D41\u53CA\u5176 agent\uFF0C\u6216\u4FEE\u6539\u4F60\u5DF2\u6709\u7684\u5DE5\u4F5C\u6D41\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.\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\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, and what each agent remembers; then embed each file you wrote as an `<Asset>`.\n";
89898
90014
 
89899
90015
  // ../cli/src/lib/agents/bundled.ts
89900
90016
  var BUNDLED_AGENT_FILES = {
89901
- "blog-illustrator/AGENT.md": AGENT_default,
89902
- "blog-planner/AGENT.md": AGENT_default2,
89903
- "blog-writer/AGENT.md": AGENT_default3,
89904
- "carousel-planner/AGENT.md": AGENT_default4,
89905
- "carousel-renderer/AGENT.md": AGENT_default5,
90017
+ "api-designer/AGENT.md": AGENT_default,
90018
+ "blog-illustrator/AGENT.md": AGENT_default2,
90019
+ "blog-planner/AGENT.md": AGENT_default3,
90020
+ "blog-writer/AGENT.md": AGENT_default4,
90021
+ "carousel-planner/AGENT.md": AGENT_default5,
90022
+ "carousel-renderer/AGENT.md": AGENT_default6,
89906
90023
  "carousel-renderer/references/slides.example.json": slides_example_default,
89907
90024
  "carousel-renderer/references/slides.schema.json": slides_schema_default,
89908
90025
  "carousel-renderer/scripts/validate-storyboard.mjs": validate_storyboard_default,
89909
- "competitor-research/AGENT.md": AGENT_default6,
89910
- "copywriting/AGENT.md": AGENT_default7,
89911
- "cover-designer/AGENT.md": AGENT_default8,
89912
- "deck-builder/AGENT.md": AGENT_default11,
90026
+ "competitor-research/AGENT.md": AGENT_default7,
90027
+ "copywriting/AGENT.md": AGENT_default8,
90028
+ "cover-designer/AGENT.md": AGENT_default9,
90029
+ "deck-builder/AGENT.md": AGENT_default12,
89913
90030
  "deck-builder/scripts/pack-pptx.mjs": pack_pptx_default,
89914
- "deck-planner/AGENT.md": AGENT_default12,
89915
- "demo-rehearser/AGENT.md": AGENT_default9,
90031
+ "deck-planner/AGENT.md": AGENT_default13,
90032
+ "demo-rehearser/AGENT.md": AGENT_default10,
89916
90033
  "deck-planner/references/slides.example.json": slides_example_default2,
89917
90034
  "deck-planner/references/slides.schema.json": slides_schema_default2,
89918
90035
  "deck-planner/scripts/validate-storyboard.mjs": validate_storyboard_default2,
89919
- "email-planner/AGENT.md": AGENT_default10,
90036
+ "email-planner/AGENT.md": AGENT_default11,
89920
90037
  "email-planner/scripts/render.mjs": render_default,
89921
- "hyperframes-editor/AGENT.md": AGENT_default14,
90038
+ "hyperframes-editor/AGENT.md": AGENT_default15,
89922
90039
  "hyperframes-editor/record.mjs": record_default,
89923
90040
  "hyperframes-editor/template-selection.md": template_selection_default,
89924
- "illustrator/AGENT.md": AGENT_default13,
89925
- "prompt-writer/AGENT.md": AGENT_default15,
89926
- "qa-manager/AGENT.md": AGENT_default16,
89927
- "scriptwriter/AGENT.md": AGENT_default17,
90041
+ "illustrator/AGENT.md": AGENT_default14,
90042
+ "prompt-writer/AGENT.md": AGENT_default16,
90043
+ "qa-manager/AGENT.md": AGENT_default17,
90044
+ "schema-designer/AGENT.md": AGENT_default18,
90045
+ "scriptwriter/AGENT.md": AGENT_default19,
89928
90046
  "scriptwriter/references/index.md": references_default,
89929
90047
  "scriptwriter/references/storyboard-contract.md": storyboard_contract_default,
89930
90048
  // TypeScript reads a `.json` import as data; the bundler hands over its text.
@@ -89940,14 +90058,14 @@ var BUNDLED_AGENT_FILES = {
89940
90058
  "scriptwriter/references/composition/shader-transitions.md": shader_transitions_default,
89941
90059
  "scriptwriter/recipes/desktop-overview-focus-interact-hold/recipe.md": recipe_default,
89942
90060
  "scriptwriter/recipes/desktop-overview-focus-interact-hold/demo.tsx": demo_default,
89943
- "social-illustrator/AGENT.md": AGENT_default18,
89944
- "ui-designer/AGENT.md": AGENT_default19,
90061
+ "social-illustrator/AGENT.md": AGENT_default20,
90062
+ "ui-designer/AGENT.md": AGENT_default21,
89945
90063
  "ui-designer/references/rendered-screen.md": rendered_screen_default,
89946
- "tech-stack-advisor/AGENT.md": AGENT_default20,
89947
- "video-cloner/AGENT.md": AGENT_default21,
90064
+ "tech-stack-advisor/AGENT.md": AGENT_default22,
90065
+ "video-cloner/AGENT.md": AGENT_default23,
89948
90066
  "video-cloner/references/music.md": music_default,
89949
- "video-template-builder/AGENT.md": AGENT_default22,
89950
- "workflow-writer/AGENT.md": AGENT_default23
90067
+ "video-template-builder/AGENT.md": AGENT_default24,
90068
+ "workflow-writer/AGENT.md": AGENT_default25
89951
90069
  };
89952
90070
 
89953
90071
  // ../cli/src/lib/agents/yaml.ts
@@ -90151,7 +90269,7 @@ var names = (value) => [...new Set((Array.isArray(value) ? value : [value]).map(
90151
90269
  var str3 = (value) => typeof value === "string" ? value.trim() : "";
90152
90270
  var map = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
90153
90271
 
90154
- // src/memory-files.ts
90272
+ // ../cli/src/lib/agents/shared-memory.ts
90155
90273
  function sharedMemory(board) {
90156
90274
  const declared = /* @__PURE__ */ new Map();
90157
90275
  const take = (folder, text3, builtIn) => {
@@ -90163,16 +90281,16 @@ function sharedMemory(board) {
90163
90281
  const at2 = /^([^/]+)\/AGENT\.md$/.exec(key);
90164
90282
  if (at2) take(at2[1], text3, true);
90165
90283
  }
90166
- const root = path18.join(board, "agents");
90284
+ const root = path19.join(board, "agents");
90167
90285
  let folders2 = [];
90168
90286
  try {
90169
- folders2 = fs16.readdirSync(root, { withFileTypes: true }).filter((e2) => e2.isDirectory() && !e2.name.startsWith(".")).map((e2) => e2.name);
90287
+ folders2 = fs17.readdirSync(root, { withFileTypes: true }).filter((e2) => e2.isDirectory() && !e2.name.startsWith(".")).map((e2) => e2.name);
90170
90288
  } catch {
90171
90289
  }
90172
90290
  for (const folder of folders2.sort()) {
90173
90291
  let text3 = null;
90174
90292
  try {
90175
- text3 = fs16.readFileSync(path18.join(root, folder, "AGENT.md"), "utf8");
90293
+ text3 = fs17.readFileSync(path19.join(root, folder, "AGENT.md"), "utf8");
90176
90294
  } catch {
90177
90295
  }
90178
90296
  take(folder, text3, false);
@@ -90186,157 +90304,6 @@ function matcher(entry) {
90186
90304
  return (name) => re3.test(name);
90187
90305
  }
90188
90306
 
90189
- // src/triage.ts
90190
- import fs17 from "node:fs";
90191
- import path19 from "node:path";
90192
-
90193
- // ../cli/src/lib/signals/identity.ts
90194
- import { createHash } from "node:crypto";
90195
- var DERIVED = "derived-";
90196
- var derivedSourceId = (seed) => `${DERIVED}${createHash("sha256").update(seed).digest("hex").slice(0, 16)}`;
90197
-
90198
- // src/triage.ts
90199
- var ok = (value) => ({ ok: true, value });
90200
- var triageDir = (board) => path19.join(board, "triage");
90201
- var rejectedDir = (board) => path19.join(board, ".archive", ".sync");
90202
- var SCRATCH = ["dismissed", "archived"];
90203
- var stamp = (at2) => formatStamp(at2 ? new Date(at2) : /* @__PURE__ */ new Date());
90204
- function slugOf(title) {
90205
- return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 56).replace(/-+$/, "") || "item";
90206
- }
90207
- function freeName(dir, title) {
90208
- const stem = slugOf(title);
90209
- let name = `${stem}.md`;
90210
- for (let n = 2; fs17.existsSync(path19.join(dir, name)); n++) name = `${stem}-${n}.md`;
90211
- return name;
90212
- }
90213
- function itemText(item, fields = {}) {
90214
- const lines = [
90215
- "---",
90216
- `source_id: ${yamlScalar(derivedSourceId(`${item.title}
90217
- ${item.text.trim()}`))}`,
90218
- `title: ${yamlScalar(item.title)}`,
90219
- `collected_at: ${yamlScalar(stamp(item.created_at))}`,
90220
- `imported_at: ${yamlScalar(stamp(item.created_at))}`,
90221
- ...Object.entries(fields).map(([k, v2]) => `${k}: ${yamlScalar(v2)}`),
90222
- "---"
90223
- ];
90224
- return `${lines.join("\n")}
90225
-
90226
- ${item.text.trim()}
90227
- `;
90228
- }
90229
- function writeItem(dir, item, fields = {}) {
90230
- fs17.mkdirSync(dir, { recursive: true });
90231
- const name = freeName(dir, item.title);
90232
- fs17.writeFileSync(path19.join(dir, name), itemText(item, item.workflow ? { workflow: localWorkflow(item.workflow), ...fields } : fields));
90233
- return name;
90234
- }
90235
- var localWorkflow = (id) => id.startsWith("wf_builtin_") ? id.slice("wf_builtin_".length).replace(/_/g, "-") : workflowFolder(id);
90236
- function readItem(file2) {
90237
- const lines = fs17.readFileSync(file2, "utf8").split("\n");
90238
- if (lines[0]?.trim() !== "---") return null;
90239
- const close = lines.indexOf("---", 1);
90240
- if (close < 0) return null;
90241
- const field = (key) => {
90242
- const said = lines.slice(1, close).map((l2) => new RegExp(`^${key}:\\s*(.*)$`).exec(l2)?.[1]).find((t2) => t2 !== void 0);
90243
- return said === void 0 ? "" : unquote(said).trim();
90244
- };
90245
- const title = field("title");
90246
- const text3 = lines.slice(close + 1).join("\n").trim();
90247
- return title ? { title: title.slice(0, 200), text: text3.slice(0, 6e4), workflow: field("workflow") } : null;
90248
- }
90249
- async function cloudWorkflowOf(local, job, own) {
90250
- if (!local) return ok(job ?? "");
90251
- if (job && localWorkflow(job) === local) return ok(job);
90252
- if (!local.startsWith("wf-")) return ok(`wf_builtin_${local.replace(/-/g, "_")}`);
90253
- const ids = await own();
90254
- if (!ids.ok) return ids;
90255
- return ok(ids.value.find((id) => workflowFolder(id) === local) ?? local);
90256
- }
90257
- var itemFiles = (dir) => {
90258
- try {
90259
- return fs17.readdirSync(dir, { withFileTypes: true }).filter((e2) => e2.isFile() && e2.name.endsWith(".md")).map((e2) => e2.name);
90260
- } catch {
90261
- return [];
90262
- }
90263
- };
90264
- async function all(read) {
90265
- const out = [];
90266
- let after = null;
90267
- do {
90268
- const page = await read(after);
90269
- if (!page.ok) return page;
90270
- out.push(...page.value.data);
90271
- after = page.value.next;
90272
- } while (after);
90273
- return ok(out);
90274
- }
90275
- async function pullTriage(board, project) {
90276
- const open = await all((after) => listTriage(project, "open", after));
90277
- if (!open.ok) return open;
90278
- const dir = triageDir(board);
90279
- for (const name of itemFiles(dir)) fs17.rmSync(path19.join(dir, name), { force: true });
90280
- for (const sub of SCRATCH) fs17.rmSync(path19.join(dir, sub), { recursive: true, force: true });
90281
- const known = {};
90282
- for (const item of open.value) known[writeItem(dir, item)] = item.id;
90283
- return ok(known);
90284
- }
90285
- async function pushTriage(board, project, task, known, workflow = null) {
90286
- const dir = triageDir(board);
90287
- const out = { ...known };
90288
- let own = null;
90289
- const ownIds = async () => {
90290
- if (own) return own;
90291
- const held3 = await readProject(project);
90292
- if (!held3.ok) return held3;
90293
- const listed2 = await all((after) => listWorkflows(held3.value.workspace ?? "", after));
90294
- own = listed2.ok ? ok(listed2.value.map((w2) => w2.id)) : listed2;
90295
- return own;
90296
- };
90297
- for (const name of itemFiles(dir)) {
90298
- if (out[name]) continue;
90299
- const item = readItem(path19.join(dir, name));
90300
- if (!item) continue;
90301
- const flow = await cloudWorkflowOf(item.workflow, workflow, ownIds);
90302
- if (!flow.ok) return flow;
90303
- const made = await createTriage(project, item.title, item.text, task, flow.value);
90304
- if (!made.ok && made.code === "bad_request") continue;
90305
- if (!made.ok) return made;
90306
- out[name] = made.value.id;
90307
- }
90308
- return ok(out);
90309
- }
90310
- function layOut(board, rejections) {
90311
- const dir = rejectedDir(board);
90312
- fs17.mkdirSync(dir, { recursive: true });
90313
- for (const t2 of rejections.tasks) {
90314
- fs17.writeFileSync(path19.join(dir, `${t2.number}-${slugify(t2.title)}.md`), [
90315
- "---",
90316
- `title: ${yamlScalar(t2.title)}`,
90317
- "status: todo",
90318
- "rejected: true",
90319
- `rejected_reason: ${yamlScalar(t2.reason)}`,
90320
- `rejected_at: ${yamlScalar(stamp(t2.cancelled_at))}`,
90321
- "rejected_by: user",
90322
- "---",
90323
- "",
90324
- t2.request.trim(),
90325
- ""
90326
- ].join("\n"));
90327
- }
90328
- for (const item of rejections.triage) {
90329
- const sub = path19.join(triageDir(board), item.state === "dismissed" ? "dismissed" : "archived");
90330
- const fields = item.state === "dismissed" ? { dismissed_at: stamp(item.dismissed_at), dismissed_by: "user", ...item.dismissed_reason ? { dismissed_reason: item.dismissed_reason } : {} } : {};
90331
- writeItem(sub, item, fields);
90332
- }
90333
- return { tasks: rejections.tasks.map((t2) => t2.id), triage: rejections.triage.map((i) => i.id) };
90334
- }
90335
- function clearLaid(board) {
90336
- fs17.rmSync(rejectedDir(board), { recursive: true, force: true });
90337
- for (const sub of SCRATCH) fs17.rmSync(path19.join(triageDir(board), sub), { recursive: true, force: true });
90338
- }
90339
-
90340
90307
  // src/task-folder.ts
90341
90308
  var TASK_ID = /^[\w-]{1,100}$/;
90342
90309
  var EMPTY = { assets: {} };
@@ -90394,8 +90361,8 @@ function keepOutOfGit(dir, folders2 = SKILL_FOLDERS) {
90394
90361
  if (!missing.length) return;
90395
90362
  const exclude = path21.resolve(dir, inside.stdout.trim());
90396
90363
  fs19.mkdirSync(path21.dirname(exclude), { recursive: true });
90397
- const held3 = fs19.existsSync(exclude) ? fs19.readFileSync(exclude, "utf8") : "";
90398
- fs19.writeFileSync(exclude, `${held3}${held3 && !held3.endsWith("\n") ? "\n" : ""}${missing.join("\n")}
90364
+ const held4 = fs19.existsSync(exclude) ? fs19.readFileSync(exclude, "utf8") : "";
90365
+ fs19.writeFileSync(exclude, `${held4}${held4 && !held4.endsWith("\n") ? "\n" : ""}${missing.join("\n")}
90399
90366
  `);
90400
90367
  }
90401
90368
  var cardFile = (ws) => (ws.number ? findCard(ws.board, ws.number) : null) ?? path21.join(ws.board, "todo", `${ws.number ?? 0}-task.md`);
@@ -90485,7 +90452,7 @@ function filesUnder(dir, base = dir) {
90485
90452
  return e2.isFile() ? [path21.relative(base, full).split(path21.sep).join("/")] : [];
90486
90453
  });
90487
90454
  }
90488
- var kept2 = (p2) => isBoardPath(p2) && !p2.startsWith("triage/");
90455
+ var kept2 = isBoardPath;
90489
90456
  var MEMORY2 = "memory/";
90490
90457
  var rootOf = (ws) => `projects/${ws.project}/`;
90491
90458
  var remoteOf = (ws, shared, p2) => p2.startsWith(MEMORY2) && shared(p2.slice(MEMORY2.length)) ? p2 : rootOf(ws) + p2;
@@ -90507,8 +90474,8 @@ var localFiles = (ws, base) => {
90507
90474
  };
90508
90475
  var SKILL_PATH = /^skills\/([a-z0-9]+(?:-[a-z0-9]+)*)\/((?:[\w.-]+\/)*[\w.-]+)$/;
90509
90476
  function pullTools(ws, record, files) {
90510
- const held3 = record.tools?.skills ?? {};
90511
- const ours = new Set(Object.keys(held3).map((name) => name.split("/")[1]));
90477
+ const held4 = record.tools?.skills ?? {};
90478
+ const ours = new Set(Object.keys(held4).map((name) => name.split("/")[1]));
90512
90479
  const theirs = (skill) => !ours.has(skill) && SKILL_ROOTS.some((root) => fs19.existsSync(path21.join(ws.cwd, root, skill)));
90513
90480
  const kept3 = Object.keys(files).map((name) => SKILL_PATH.exec(name)?.[1]).filter((skill) => skill && !theirs(skill));
90514
90481
  const skills = {};
@@ -90518,10 +90485,10 @@ function pullTools(ws, record, files) {
90518
90485
  skills[name] = hashOf(text3);
90519
90486
  for (const root of SKILL_ROOTS) {
90520
90487
  const file2 = path21.join(ws.cwd, root, at2[1], at2[2]);
90521
- if (held3[name] !== skills[name] || hashFile(file2) !== skills[name]) write(file2, text3);
90488
+ if (held4[name] !== skills[name] || hashFile(file2) !== skills[name]) write(file2, text3);
90522
90489
  }
90523
90490
  }
90524
- for (const name of Object.keys(held3)) {
90491
+ for (const name of Object.keys(held4)) {
90525
90492
  if (skills[name]) continue;
90526
90493
  const [, skill, rest] = SKILL_PATH.exec(name) ?? [];
90527
90494
  if (!skill || !rest) continue;
@@ -90544,8 +90511,8 @@ async function pullWorkflow(ws, record, base) {
90544
90511
  let files = {};
90545
90512
  if (ws.workflow) {
90546
90513
  const root = `workflows/${workflowFolder(ws.workflow)}/`;
90547
- const held3 = rootOf(ws) + root;
90548
- files = Object.fromEntries(Object.entries(base.files).filter(([p2]) => p2.startsWith(held3) && isBoardPath(p2.slice(rootOf(ws).length))).map(([p2, f2]) => [p2.slice(held3.length), f2.content]));
90514
+ const held4 = rootOf(ws) + root;
90515
+ files = Object.fromEntries(Object.entries(base.files).filter(([p2]) => p2.startsWith(held4) && isBoardPath(p2.slice(rootOf(ws).length))).map(([p2, f2]) => [p2.slice(held4.length), f2.content]));
90549
90516
  record.builtin = ws.workflow.startsWith("wf_builtin_");
90550
90517
  let found = Object.keys(files).length > 0;
90551
90518
  if (!found && ws.cloudTask) {
@@ -90627,15 +90594,19 @@ function workflowFile(ws) {
90627
90594
  }
90628
90595
  function assetPath(ws, name, card) {
90629
90596
  if (/^[a-z][\w+.-]*:/i.test(name) || name.startsWith("/") || name.startsWith("#")) return null;
90630
- const held3 = /^\.(assets|mockups)\//.exec(name);
90631
- if (held3) {
90597
+ const held4 = /^\.(assets|mockups)\//.exec(name);
90598
+ if (held4) {
90632
90599
  const state = stateDir(ws);
90633
- const file3 = state && path21.resolve(state, held3[1], name.slice(held3[0].length));
90634
- return file3 && file3.startsWith(path21.join(state, held3[1]) + path21.sep) ? file3 : null;
90600
+ const file3 = state && path21.resolve(state, held4[1], name.slice(held4[0].length));
90601
+ return file3 && file3.startsWith(path21.join(state, held4[1]) + path21.sep) ? file3 : null;
90635
90602
  }
90636
90603
  const file2 = path21.resolve(path21.dirname(card), name);
90637
90604
  return file2.startsWith(ws.board + path21.sep) ? file2 : null;
90638
90605
  }
90606
+ function cardAssetsDir(ws, id) {
90607
+ const state = stateDir(ws);
90608
+ return state && path21.join(state, "assets", String(id));
90609
+ }
90639
90610
  function stateDir(ws) {
90640
90611
  try {
90641
90612
  return projectStateDir(ws.board);
@@ -90645,8 +90616,8 @@ function stateDir(ws) {
90645
90616
  }
90646
90617
  function assetsOf(text3) {
90647
90618
  try {
90648
- const held3 = JSON.parse(text3 ?? "{}");
90649
- return held3 && typeof held3 === "object" && !Array.isArray(held3) ? held3 : {};
90619
+ const held4 = JSON.parse(text3 ?? "{}");
90620
+ return held4 && typeof held4 === "object" && !Array.isArray(held4) ? held4 : {};
90650
90621
  } catch {
90651
90622
  return {};
90652
90623
  }
@@ -90687,9 +90658,6 @@ async function pull(ws) {
90687
90658
  reset(viewOf(ws, base, sharedMemory(ws.board)));
90688
90659
  const assets = await pullAssets(ws, record, assetsOf(assetsListOf(ws, base)));
90689
90660
  if (!assets.ok) return assets;
90690
- const triage = await pullTriage(ws.board, ws.project);
90691
- if (!triage.ok) return triage;
90692
- record.triage = triage.value;
90693
90661
  writeRecord(ws, record);
90694
90662
  writeIndex(ws.board);
90695
90663
  return { ok: true, value: void 0 };
@@ -90736,13 +90704,13 @@ async function pullAssets(ws, record, assets) {
90736
90704
  if (!latest.ok) return latest;
90737
90705
  version = latest.value.latest_version ?? 0;
90738
90706
  }
90739
- const held3 = record.assets[name];
90740
- if (held3?.kind === "html" && held3.artifact === ref.artifact) {
90741
- out[name] = { ...held3, version };
90707
+ const held4 = record.assets[name];
90708
+ if (held4?.kind === "html" && held4.artifact === ref.artifact) {
90709
+ out[name] = { ...held4, version };
90742
90710
  continue;
90743
90711
  }
90744
- if (held3?.artifact === ref.artifact && held3.version === version && hashFile(target) === held3.hash) {
90745
- out[name] = held3;
90712
+ if (held4?.artifact === ref.artifact && held4.version === version && hashFile(target) === held4.hash) {
90713
+ out[name] = held4;
90746
90714
  continue;
90747
90715
  }
90748
90716
  const bytes2 = await readAsset(ref.artifact, version);
@@ -90754,10 +90722,10 @@ async function pullAssets(ws, record, assets) {
90754
90722
  return { ok: true, value: void 0 };
90755
90723
  }
90756
90724
  async function readAsset(artifact, version) {
90757
- const held3 = await readVersion(artifact, version);
90758
- if (!held3.ok) return held3;
90759
- if (held3.value.file) return download(held3.value.file.url);
90760
- const content = held3.value.content;
90725
+ const held4 = await readVersion(artifact, version);
90726
+ if (!held4.ok) return held4;
90727
+ if (held4.value.file) return download(held4.value.file.url);
90728
+ const content = held4.value.content;
90761
90729
  return { ok: true, value: Buffer.from(typeof content === "string" ? content : JSON.stringify(content)) };
90762
90730
  }
90763
90731
  var TYPES = {
@@ -90823,17 +90791,17 @@ async function pushAssets(ws, record, before) {
90823
90791
  for (const [name, ref] of Object.entries(before)) {
90824
90792
  if (!body.includes(name)) continue;
90825
90793
  const target = assetPath(ws, name, file2);
90826
- const held3 = record.assets[name];
90827
- const out = target && held3 && fs19.existsSync(target) ? await sentOf(target) : null;
90794
+ const held4 = record.assets[name];
90795
+ const out = target && held4 && fs19.existsSync(target) ? await sentOf(target) : null;
90828
90796
  const hash = out && hashOf(out.bytes);
90829
- if (out && held3 && held3.kind === "html" !== (out.kind === "html")) continue;
90830
- if (held3 && out && hash !== held3.hash) {
90831
- const sent = await uploadAsset(held3.artifact, out);
90797
+ if (out && held4 && held4.kind === "html" !== (out.kind === "html")) continue;
90798
+ if (held4 && out && hash !== held4.hash) {
90799
+ const sent = await uploadAsset(held4.artifact, out);
90832
90800
  if (!sent.ok) return sent;
90833
- assets[name] = { artifact: held3.artifact, version: sent.value, hash, kind: out.kind };
90834
- refs[name] = { artifact: held3.artifact, version: sent.value };
90801
+ assets[name] = { artifact: held4.artifact, version: sent.value, hash, kind: out.kind };
90802
+ refs[name] = { artifact: held4.artifact, version: sent.value };
90835
90803
  } else {
90836
- if (held3) assets[name] = held3;
90804
+ if (held4) assets[name] = held4;
90837
90805
  refs[name] = ref;
90838
90806
  }
90839
90807
  }
@@ -90856,6 +90824,27 @@ async function pushAssets(ws, record, before) {
90856
90824
  }
90857
90825
  return { ok: true, value: void 0 };
90858
90826
  }
90827
+ function resetSorted(ws) {
90828
+ for (const sub of ["dismissed", "archived"]) fs19.rmSync(path21.join(ws.board, "triage", sub), { recursive: true, force: true });
90829
+ for (const [p2, content] of viewOf(ws, readBase(ws), sharedMemory(ws.board))) {
90830
+ if (/^triage\/(?:dismissed|archived)\//.test(p2)) write(path21.join(ws.board, p2), content);
90831
+ }
90832
+ }
90833
+ function proposeItems(ws, base) {
90834
+ const root = rootOf(ws);
90835
+ for (const p2 of localFiles(ws, base)) {
90836
+ if (itemAt(p2)?.state !== "open" || base.files[root + p2]) continue;
90837
+ const file2 = path21.join(ws.board, p2);
90838
+ const content = fs19.readFileSync(file2, "utf8");
90839
+ const said = (key) => new RegExp(`^${key}:`, "m").test(content.split(/\n---/)[0] ?? "");
90840
+ const fields = {
90841
+ ...ws.cloudTask && !said("source_task") ? { source_task: ws.cloudTask } : {},
90842
+ ...ws.workflow && !said("workflow") ? { workflow: workflowName(ws.workflow) } : {}
90843
+ };
90844
+ const stamped = Object.keys(fields).length ? stampItem(content, fields) : content;
90845
+ if (stamped !== content) write(file2, stamped);
90846
+ }
90847
+ }
90859
90848
  function mergeFile(base, mine, theirs) {
90860
90849
  if (mine === theirs) return { content: mine };
90861
90850
  if (mine === null || theirs === null) return null;
@@ -90875,10 +90864,7 @@ async function send2(ws, job) {
90875
90864
  const assets = await pushAssets(ws, record, assetsOf(assetsListOf(ws, base)));
90876
90865
  if (!assets.ok) return fail(assets.error, false, assets.code);
90877
90866
  writeRecord(ws, record);
90878
- const triage = await pushTriage(ws.board, ws.project, ws.cloudTask, record.triage ?? {}, ws.workflow);
90879
- if (!triage.ok) return fail(triage.error, false, triage.code);
90880
- record.triage = triage.value;
90881
- writeRecord(ws, record);
90867
+ proposeItems(ws, base);
90882
90868
  const shared = sharedMemory(ws.board);
90883
90869
  const local = localFiles(ws, base);
90884
90870
  const localOf = /* @__PURE__ */ new Map();
@@ -90887,10 +90873,10 @@ async function send2(ws, job) {
90887
90873
  const content = fs19.readFileSync(path21.join(ws.board, p2), "utf8");
90888
90874
  const remote = remoteOf(ws, shared, p2);
90889
90875
  localOf.set(remote, p2);
90890
- const held3 = base.files[remote];
90891
- if (held3?.content === content) continue;
90876
+ const held4 = base.files[remote];
90877
+ if (held4?.content === content) continue;
90892
90878
  if (Buffer.byteLength(content) > BOARD_FILE_LIMIT) return fail(`${p2} is over ${BOARD_FILE_LIMIT / 1024} KB; keep large files as assets.`, false, "too_large");
90893
- changes.push({ path: remote, content, base_seq: held3?.seq ?? 0 });
90879
+ changes.push({ path: remote, content, base_seq: held4?.seq ?? 0 });
90894
90880
  }
90895
90881
  for (const p2 of viewOf(ws, base, shared).keys()) {
90896
90882
  const remote = remoteOf(ws, shared, p2);
@@ -90912,7 +90898,7 @@ async function send2(ws, job) {
90912
90898
  continue;
90913
90899
  }
90914
90900
  const p2 = localOf.get(theirs.path) ?? theirs.path;
90915
- const merged = at2 < 0 ? null : mergeFile(base.files[theirs.path]?.content ?? null, changes[at2].content, theirs.content);
90901
+ const merged = at2 < 0 ? null : itemAt(p2) ? { content: theirs.content } : mergeFile(base.files[theirs.path]?.content ?? null, changes[at2].content, theirs.content);
90916
90902
  if (!merged) return fail(`${p2} was changed elsewhere and does not merge.`, true, "version_conflict");
90917
90903
  changes[at2] = { path: theirs.path, content: merged.content, base_seq: theirs.seq };
90918
90904
  if (merged.content === null) fs19.rmSync(path21.join(ws.board, p2), { force: true });
@@ -90931,10 +90917,86 @@ async function send2(ws, job) {
90931
90917
  return { ok: true, changed: true };
90932
90918
  }
90933
90919
 
90934
- // src/built.ts
90920
+ // src/triage.ts
90935
90921
  import fs20 from "node:fs";
90936
90922
  import path22 from "node:path";
90937
90923
 
90924
+ // ../cli/src/lib/signals/identity.ts
90925
+ import { createHash } from "node:crypto";
90926
+ var DERIVED = "derived-";
90927
+ var derivedSourceId = (seed) => `${DERIVED}${createHash("sha256").update(seed).digest("hex").slice(0, 16)}`;
90928
+
90929
+ // src/triage.ts
90930
+ var triageDir = (board) => path22.join(board, "triage");
90931
+ var rejectedDir = (board) => path22.join(board, ".archive", ".sync");
90932
+ var SCRATCH = ["dismissed", "archived"];
90933
+ var stamp = (at2) => formatStamp(at2 ? new Date(at2) : /* @__PURE__ */ new Date());
90934
+ function slugOf(title) {
90935
+ return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 56).replace(/-+$/, "") || "item";
90936
+ }
90937
+ function freeName(dir, title) {
90938
+ const stem = slugOf(title);
90939
+ let name = `${stem}.md`;
90940
+ for (let n = 2; fs20.existsSync(path22.join(dir, name)); n++) name = `${stem}-${n}.md`;
90941
+ return name;
90942
+ }
90943
+ function itemText(item, fields = {}) {
90944
+ const lines = [
90945
+ "---",
90946
+ `source_id: ${yamlScalar(derivedSourceId(`${item.title}
90947
+ ${item.text.trim()}`))}`,
90948
+ `title: ${yamlScalar(item.title)}`,
90949
+ `collected_at: ${yamlScalar(stamp(item.created_at))}`,
90950
+ `imported_at: ${yamlScalar(stamp(item.created_at))}`,
90951
+ ...Object.entries(fields).map(([k, v2]) => `${k}: ${yamlScalar(v2)}`),
90952
+ "---"
90953
+ ];
90954
+ return `${lines.join("\n")}
90955
+
90956
+ ${item.text.trim()}
90957
+ `;
90958
+ }
90959
+ function writeItem(dir, item, fields = {}) {
90960
+ fs20.mkdirSync(dir, { recursive: true });
90961
+ const name = freeName(dir, item.title);
90962
+ fs20.writeFileSync(path22.join(dir, name), itemText(item, item.workflow ? { workflow: workflowName(item.workflow), ...fields } : fields));
90963
+ return name;
90964
+ }
90965
+ function layOut(board, rejections) {
90966
+ clearLaid(board);
90967
+ const dir = rejectedDir(board);
90968
+ fs20.mkdirSync(dir, { recursive: true });
90969
+ for (const t2 of rejections.tasks) {
90970
+ fs20.writeFileSync(path22.join(dir, `${t2.number}-${slugify(t2.title)}.md`), [
90971
+ "---",
90972
+ `title: ${yamlScalar(t2.title)}`,
90973
+ "status: todo",
90974
+ "rejected: true",
90975
+ `rejected_reason: ${yamlScalar(t2.reason)}`,
90976
+ `rejected_at: ${yamlScalar(stamp(t2.cancelled_at))}`,
90977
+ "rejected_by: user",
90978
+ "---",
90979
+ "",
90980
+ t2.request.trim(),
90981
+ ""
90982
+ ].join("\n"));
90983
+ }
90984
+ for (const item of rejections.triage) {
90985
+ const sub = path22.join(triageDir(board), item.state === "dismissed" ? "dismissed" : "archived");
90986
+ const fields = item.state === "dismissed" ? { dismissed_at: stamp(item.dismissed_at), dismissed_by: "user", ...item.dismissed_reason ? { dismissed_reason: item.dismissed_reason } : {} } : {};
90987
+ writeItem(sub, item, fields);
90988
+ }
90989
+ return { tasks: rejections.tasks.map((t2) => t2.id), triage: rejections.triage.map((i) => i.id) };
90990
+ }
90991
+ function clearLaid(board) {
90992
+ fs20.rmSync(rejectedDir(board), { recursive: true, force: true });
90993
+ for (const sub of SCRATCH) fs20.rmSync(path22.join(triageDir(board), sub), { recursive: true, force: true });
90994
+ }
90995
+
90996
+ // src/built.ts
90997
+ import fs21 from "node:fs";
90998
+ import path23 from "node:path";
90999
+
90938
91000
  // ../cli/src/lib/agent/workflow-def.ts
90939
91001
  var WORKFLOW_ID = /^[a-z0-9]+(-[a-z0-9]+)*$/;
90940
91002
  var OLD_KEYS = ["stages", "delivers", "helpers", "every"];
@@ -91091,26 +91153,26 @@ function checkWorkflowFiles(value, official = false) {
91091
91153
 
91092
91154
  // src/built.ts
91093
91155
  var isBuilder = (ws) => ws.workflow === BUILDER;
91094
- var draftDir = (ws) => path22.join(ws.board, "workflows", draftFolder(ws.task));
91095
- var draftLine = (ws) => `<!-- workflow folder: ${path22.relative(ws.cwd, draftDir(ws)).split(path22.sep).join("/")}/ -->`;
91156
+ var draftDir = (ws) => path23.join(ws.board, "workflows", draftFolder(ws.task));
91157
+ var draftLine = (ws) => `<!-- workflow folder: ${path23.relative(ws.cwd, draftDir(ws)).split(path23.sep).join("/")}/ -->`;
91096
91158
  var skeleton = (name) => ["---", `name: ${/\s#/.test(name) ? JSON.stringify(name) : yamlScalar(name)}`, "needsArtifact: false", "---", ""].join("\n");
91097
91159
  async function placeDraft(ws, read) {
91098
91160
  const dir = draftDir(ws);
91099
- if (fs20.existsSync(path22.join(dir, WORKFLOW_FILE))) return { ok: true, value: void 0 };
91161
+ if (fs21.existsSync(path23.join(dir, WORKFLOW_FILE))) return { ok: true, value: void 0 };
91100
91162
  const task = await read.readTask(ws.task);
91101
91163
  if (!task.ok) return task;
91102
91164
  let files = { [WORKFLOW_FILE]: skeleton(task.value.title.trim() || "Untitled") };
91103
91165
  const changes = task.value.context?.workflow;
91104
91166
  if (changes) {
91105
- const held3 = await read.readWorkflow(changes);
91106
- if (!held3.ok) return held3;
91107
- files = held3.value.files;
91167
+ const held4 = await read.readWorkflow(changes);
91168
+ if (!held4.ok) return held4;
91169
+ files = held4.value.files;
91108
91170
  }
91109
91171
  for (const [name, text3] of Object.entries(files)) {
91110
- const file2 = path22.resolve(dir, name);
91111
- if (!file2.startsWith(dir + path22.sep)) continue;
91112
- fs20.mkdirSync(path22.dirname(file2), { recursive: true });
91113
- fs20.writeFileSync(file2, text3);
91172
+ const file2 = path23.resolve(dir, name);
91173
+ if (!file2.startsWith(dir + path23.sep)) continue;
91174
+ fs21.mkdirSync(path23.dirname(file2), { recursive: true });
91175
+ fs21.writeFileSync(file2, text3);
91114
91176
  }
91115
91177
  return { ok: true, value: void 0 };
91116
91178
  }
@@ -91122,19 +91184,19 @@ async function builtWorkflow(ws, akb) {
91122
91184
  const dir = draftDir(ws);
91123
91185
  let source;
91124
91186
  try {
91125
- source = fs20.readFileSync(path22.join(dir, WORKFLOW_FILE), "utf8");
91187
+ source = fs21.readFileSync(path23.join(dir, WORKFLOW_FILE), "utf8");
91126
91188
  } catch {
91127
91189
  return fail(`workflows/${id}/${WORKFLOW_FILE} is missing.`);
91128
91190
  }
91129
91191
  const read = parseWorkflowFile(id, source, `workflows/${id}/${WORKFLOW_FILE}`);
91130
91192
  if ("problem" in read) return fail(read.problem);
91131
91193
  const names2 = named(read.def);
91132
- fs20.rmSync(path22.join(dir, "agents"), { recursive: true, force: true });
91194
+ fs21.rmSync(path23.join(dir, "agents"), { recursive: true, force: true });
91133
91195
  for (const name of names2) {
91134
- const from = path22.join(ws.board, "agents", name);
91135
- if (fs20.existsSync(from)) fs20.cpSync(from, path22.join(dir, "agents", name), { recursive: true });
91196
+ const from = path23.join(ws.board, "agents", name);
91197
+ if (fs21.existsSync(from)) fs21.cpSync(from, path23.join(dir, "agents", name), { recursive: true });
91136
91198
  }
91137
- const files = Object.fromEntries(filesUnder(dir).map((f2) => [f2, fs20.readFileSync(path22.join(dir, f2), "utf8")]));
91199
+ const files = Object.fromEntries(filesUnder(dir).map((f2) => [f2, fs21.readFileSync(path23.join(dir, f2), "utf8")]));
91138
91200
  const checked = checkWorkflowFiles(files);
91139
91201
  if (!checked.ok) return fail(`${checked.file} ${checked.reason}.`);
91140
91202
  const run2 = async (args) => answerOf2(await runAkb(akb, [...args, "--json", "--board", ws.board], { cwd: ws.cwd, board: ws.board }));
@@ -91166,11 +91228,10 @@ function spentBy(runs) {
91166
91228
  };
91167
91229
  }
91168
91230
  var SETUP_STEPS = ["project", "agent", "decisions", "modules", "tasks"];
91169
- var workflowName = (id) => id.startsWith("wf_builtin_") ? id.slice("wf_builtin_".length).replace(/_/g, "-") : workflowFolder(id);
91170
91231
  function reviewOf(file2) {
91171
91232
  let text3;
91172
91233
  try {
91173
- text3 = fs21.readFileSync(file2, "utf8");
91234
+ text3 = fs22.readFileSync(file2, "utf8");
91174
91235
  } catch {
91175
91236
  return void 0;
91176
91237
  }
@@ -91185,8 +91246,8 @@ function reviewOf(file2) {
91185
91246
  }
91186
91247
  function mcpConfig(servers, file2) {
91187
91248
  if (!Object.keys(servers).length) return null;
91188
- fs21.mkdirSync(path23.dirname(file2), { recursive: true });
91189
- fs21.writeFileSync(file2, `${JSON.stringify({ mcpServers: servers }, null, 2)}
91249
+ fs22.mkdirSync(path24.dirname(file2), { recursive: true });
91250
+ fs22.writeFileSync(file2, `${JSON.stringify({ mcpServers: servers }, null, 2)}
91190
91251
  `);
91191
91252
  return file2;
91192
91253
  }
@@ -91220,7 +91281,7 @@ async function stopLeftBehind(akb, ws, id, ms2) {
91220
91281
  var refusal2 = (ran) => ran.last.replace(/^akb( raw)?: /, "").slice(0, 300) || "akb stopped with an error.";
91221
91282
  var firstLine = (text3) => (text3 ?? "").split("\n").map((l2) => l2.trim()).find(Boolean)?.slice(0, 300);
91222
91283
  function logLines(show, event2) {
91223
- let held3 = "";
91284
+ let held4 = "";
91224
91285
  const said = (line) => {
91225
91286
  if (line.startsWith('{"kind":')) {
91226
91287
  let parsed;
@@ -91246,40 +91307,40 @@ function logLines(show, event2) {
91246
91307
  };
91247
91308
  return {
91248
91309
  push: (chunk) => {
91249
- const lines = (held3 + chunk).split("\n");
91250
- held3 = lines.pop();
91310
+ const lines = (held4 + chunk).split("\n");
91311
+ held4 = lines.pop();
91251
91312
  for (const line of lines) {
91252
91313
  const out = said(line);
91253
91314
  if (out) show(out);
91254
91315
  }
91255
91316
  },
91256
91317
  end: () => {
91257
- const out = held3 ? said(held3) : "";
91318
+ const out = held4 ? said(held4) : "";
91258
91319
  if (out) show(out);
91259
91320
  }
91260
91321
  };
91261
91322
  }
91262
- function boardAkb(ws, deps, io, task) {
91323
+ function boardAkb(job, ws, deps, io, task) {
91263
91324
  return async (args, show = false) => {
91264
91325
  const at2 = args.includes("--") ? args.indexOf("--") : args.length;
91265
91326
  const full = [...args.slice(0, at2), "--board", ws.board, ...args.slice(at2)];
91266
91327
  const log = show ? logLines(io.show, io.event) : void 0;
91267
- const ran = await runAkb(deps.akb, full, { cwd: ws.cwd, board: ws.board, task, show: log?.push, track: io.track });
91328
+ const ran = await runAkb(deps.akb, full, { cwd: ws.cwd, board: ws.board, task, lang: job.lang, show: log?.push, track: io.track });
91268
91329
  log?.end();
91269
91330
  return ran;
91270
91331
  };
91271
91332
  }
91272
91333
  async function setUp(job, ws, deps, io) {
91273
- const akb = boardAkb(ws, deps, io);
91334
+ const akb = boardAkb(job, ws, deps, io);
91274
91335
  const runtime = job.runtime;
91275
91336
  const missing = unavailable(runtime, await deps.runtimes());
91276
91337
  if (missing) return missing;
91277
- if (!fs21.existsSync(path23.join(ws.board, "config.md"))) {
91338
+ if (!fs22.existsSync(path24.join(ws.board, "config.md"))) {
91278
91339
  const init = await akb(["raw", "init"]);
91279
91340
  if (init.code !== 0) return refusal2(init);
91280
91341
  for (const step of SETUP_STEPS) await akb(["raw", "setup-done", step]);
91281
91342
  }
91282
- if (SKILL_FOLDERS.some((f2) => !fs21.existsSync(path23.join(ws.cwd, f2, "SKILL.md")))) {
91343
+ if (SKILL_FOLDERS.some((f2) => !fs22.existsSync(path24.join(ws.cwd, f2, "SKILL.md")))) {
91283
91344
  const skill = await runAkb(deps.akb, ["skill", "install", "--dir", ws.cwd], { cwd: ws.cwd, track: io.track });
91284
91345
  if (skill.code !== 0) return refusal2(skill);
91285
91346
  }
@@ -91288,7 +91349,7 @@ async function setUp(job, ws, deps, io) {
91288
91349
  const used = await akb(["agent", "use", runtime]);
91289
91350
  if (used.code !== 0) return refusal2(used);
91290
91351
  const servers = mcpServersOf(ws);
91291
- const mcpFile = path23.join(ws.board, ".sync", `${ws.task}.mcp.json`);
91352
+ const mcpFile = path24.join(ws.board, ".sync", `${ws.task}.mcp.json`);
91292
91353
  const takes = new Set(settingKeys(runtime));
91293
91354
  const given = job.settings ?? {};
91294
91355
  const values = { ...given, model: job.model ?? "" };
@@ -91320,7 +91381,7 @@ async function runReview(job, ws, tasks, deps, io) {
91320
91381
  const since = Date.now();
91321
91382
  const failed = await setUp(job, ws, deps, io);
91322
91383
  if (failed) return { ok: false, reason: failed };
91323
- const akb = boardAkb(ws, deps, io);
91384
+ const akb = boardAkb(job, ws, deps, io);
91324
91385
  const ran = await akb(["review-memory", "--task", ...tasks, "--follow", "--events", "--json"], true);
91325
91386
  if (io.lost()) return { ok: false, reason: "" };
91326
91387
  const answer = answerOf2(ran);
@@ -91335,10 +91396,11 @@ async function reviewRejections(job, ws, deps, io) {
91335
91396
  if (!read.ok) return { runs: [], failure: read.error };
91336
91397
  if (!read.value.tasks.length && !read.value.triage.length) return { runs: [] };
91337
91398
  const since = Date.now();
91338
- const akb = boardAkb(ws, deps, io);
91399
+ const akb = boardAkb(job, ws, deps, io);
91339
91400
  const laid = layOut(ws.board, read.value);
91340
91401
  const ran = await akb(["review-dismissals", "--all", "--follow", "--events", "--json"], true);
91341
91402
  clearLaid(ws.board);
91403
+ resetSorted(ws);
91342
91404
  const runs = (answerOf2(await akb(["run", "list", "--all", "--json"]))?.runs ?? []).filter((r2) => (r2.startedAt ?? 0) >= since);
91343
91405
  if (io.lost()) return { runs };
91344
91406
  if (ran.code !== 0) return { runs, failure: refusal2(ran) };
@@ -91364,7 +91426,7 @@ async function runAgent(job, ws, deps, io) {
91364
91426
  }
91365
91427
  if (name === "reflect" && !tasks?.length) return { ok: true, report: {} };
91366
91428
  const args = workflow ? ["agent", "run", workflowName(workflow), name] : name === "reflect" ? ["reflect", "--cards", ...tasks.map((t2) => String(t2.number))] : [name];
91367
- const akb = boardAkb(ws, deps, io, ws.cloudTask ?? void 0);
91429
+ const akb = boardAkb(job, ws, deps, io, ws.cloudTask ?? void 0);
91368
91430
  await akb(["run", "list", "--all", "--json"]);
91369
91431
  const ran = await akb([...args, "--follow", "--events", "--json"], true);
91370
91432
  if (io.lost()) return { ok: false, reason: "" };
@@ -91375,15 +91437,27 @@ async function runAgent(job, ws, deps, io) {
91375
91437
  }
91376
91438
  function writeTranscript(job, dir) {
91377
91439
  if (!job.transcript) return "";
91378
- const file2 = path23.join(dir, "discussion.md");
91379
- fs21.mkdirSync(dir, { recursive: true });
91380
- fs21.writeFileSync(file2, `${job.transcript}
91440
+ const file2 = path24.join(dir, "discussion.md");
91441
+ fs22.mkdirSync(dir, { recursive: true });
91442
+ fs22.writeFileSync(file2, `${job.transcript}
91381
91443
  `);
91382
91444
  return file2;
91383
91445
  }
91446
+ function pictures(job, ws, id) {
91447
+ const dir = cardAssetsDir(ws, id);
91448
+ if (!job.attachments.length || !dir) return "";
91449
+ fs22.mkdirSync(dir, { recursive: true });
91450
+ return job.attachments.map((a3, i) => {
91451
+ const file2 = `${i + 1}-${path24.basename(a3.name).replace(/[^\w.-]+/g, "-")}`;
91452
+ fs22.writeFileSync(path24.join(dir, file2), Buffer.from(a3.content, "base64"));
91453
+ return `
91454
+ <Asset src=".assets/${id}/${file2}" label="${a3.name.replace(/["<>]/g, "") || file2}" />
91455
+ `;
91456
+ }).join("");
91457
+ }
91384
91458
  function cardFlags(ws, fields) {
91385
91459
  if (!fields) return [];
91386
- const open = (keys) => (keys ?? []).map((k) => Number(/(\d+)$/.exec(k)?.[1])).filter((n) => n > 0 && n !== ws.number && findCard(ws.board, n)?.startsWith(path23.join(ws.board, "todo") + path23.sep));
91460
+ const open = (keys) => (keys ?? []).map((k) => Number(/(\d+)$/.exec(k)?.[1])).filter((n) => n > 0 && n !== ws.number && findCard(ws.board, n)?.startsWith(path24.join(ws.board, "todo") + path24.sep));
91387
91461
  const lists = [["--modules", fields.modules ?? []], ["--related", open(fields.related)], ["--blocked-by", open(fields.blocked_by)]];
91388
91462
  return [
91389
91463
  ...fields.priority ? ["--priority", fields.priority] : [],
@@ -91395,7 +91469,7 @@ async function runFlow(job, ws, deps, io) {
91395
91469
  const { flow } = job;
91396
91470
  const since = Date.now();
91397
91471
  const fail = (reason) => ({ ok: false, reason });
91398
- const akb = boardAkb(ws, deps, io, ws.task);
91472
+ const akb = boardAkb(job, ws, deps, io, ws.task);
91399
91473
  const made = (ran) => {
91400
91474
  const answer = answerOf2(ran);
91401
91475
  return ran.code === 0 && answer?.id ? { ok: true, value: answer.id } : { ok: false, error: answer?.error?.message ?? refusal2(ran), code: answer?.error?.kind };
@@ -91417,13 +91491,13 @@ async function runFlow(job, ws, deps, io) {
91417
91491
  const placed = await placeDraft(ws, deps.api);
91418
91492
  if (!placed.ok) return fail(placed.error);
91419
91493
  }
91420
- const body = path23.join(ws.board, ".sync", `${ws.task}.request.md`);
91421
- fs21.mkdirSync(path23.dirname(body), { recursive: true });
91422
- fs21.writeFileSync(body, `${said.request || title}
91494
+ const body = path24.join(ws.board, ".sync", `${ws.task}.request.md`);
91495
+ fs22.mkdirSync(path24.dirname(body), { recursive: true });
91496
+ if (!ws.number) return fail("This task has no number yet.");
91497
+ fs22.writeFileSync(body, `${said.request || title}
91423
91498
  ${isBuilder(ws) ? `
91424
91499
  ${draftLine(ws)}
91425
- ` : ""}`);
91426
- if (!ws.number) return fail("This task has no number yet.");
91500
+ ` : ""}${pictures(job, ws, ws.number)}`);
91427
91501
  const created = made(await akb([
91428
91502
  "raw",
91429
91503
  "create",
@@ -91438,21 +91512,21 @@ ${draftLine(ws)}
91438
91512
  ...cardFlags(ws, flow.metadata),
91439
91513
  "--json"
91440
91514
  ]));
91441
- fs21.rmSync(body, { force: true });
91515
+ fs22.rmSync(body, { force: true });
91442
91516
  if (!created.ok) return fail(created.error);
91443
91517
  id = created.value;
91444
91518
  }
91445
91519
  if (!id) return fail("This task has no card on this computer.");
91446
91520
  const card = id;
91447
91521
  const key = workKey(ws, card);
91448
- const held3 = working.get(key) ?? 0;
91449
- working.set(key, held3 + 1);
91522
+ const held4 = working.get(key) ?? 0;
91523
+ working.set(key, held4 + 1);
91450
91524
  try {
91451
- if (!held3) await stopRuns(deps.akb, ws, card, 0, deps.stopMs);
91525
+ if (!held4) await stopRuns(deps.akb, ws, card, 0, deps.stopMs);
91452
91526
  if (io.lost()) return fail("");
91453
91527
  return await workOn(card);
91454
91528
  } finally {
91455
- fs21.rmSync(path23.join(ws.board, ".sync", `${ws.task}.discussion`), { recursive: true, force: true });
91529
+ fs22.rmSync(path24.join(ws.board, ".sync", `${ws.task}.discussion`), { recursive: true, force: true });
91456
91530
  const left = working.get(key) - 1;
91457
91531
  if (left) working.set(key, left);
91458
91532
  else working.delete(key);
@@ -91490,7 +91564,7 @@ ${draftLine(ws)}
91490
91564
  }
91491
91565
  function carryArgs(verb) {
91492
91566
  if (!["refine", "resolve", "revise"].includes(verb)) return [];
91493
- const transcript = writeTranscript(job, path23.join(ws.board, ".sync", `${ws.task}.discussion`));
91567
+ const transcript = writeTranscript(job, path24.join(ws.board, ".sync", `${ws.task}.discussion`));
91494
91568
  return [
91495
91569
  ...job.resume ? ["--session", job.resume, ...job.fork ? ["--fork"] : []] : [],
91496
91570
  ...transcript ? ["--transcript", transcript] : []
@@ -91498,15 +91572,116 @@ ${draftLine(ws)}
91498
91572
  }
91499
91573
  }
91500
91574
 
91501
- // src/jobs.ts
91502
- import { spawn as spawn3 } from "node:child_process";
91575
+ // src/lock.ts
91576
+ import crypto3 from "node:crypto";
91503
91577
  import fs23 from "node:fs";
91578
+ import net from "node:net";
91504
91579
  import path25 from "node:path";
91580
+ var RETRY_MS = 5e3;
91581
+ var KIND_MS = 1e3;
91582
+ var HOLDERS = ["claude-code", "codex", "terminal", "desktop"];
91583
+ function lockPath() {
91584
+ const home4 = runnerHome();
91585
+ if (process.platform === "win32") return `\\\\.\\pipe\\wildcrane-${crypto3.createHash("sha256").update(home4).digest("hex").slice(0, 16)}`;
91586
+ return path25.join(home4, "runner.sock");
91587
+ }
91588
+ function holdMachine(kind, onHeld, onWaiting) {
91589
+ const where = lockPath();
91590
+ let closed = false;
91591
+ let server = null;
91592
+ let waiting = null;
91593
+ let timer = null;
91594
+ const peers = /* @__PURE__ */ new Set();
91595
+ const later = (ms2) => {
91596
+ if (!closed) timer = setTimeout(attempt, ms2);
91597
+ };
91598
+ const attempt = () => {
91599
+ timer = null;
91600
+ if (closed) return;
91601
+ if (process.platform !== "win32") fs23.mkdirSync(path25.dirname(where), { recursive: true, mode: 448 });
91602
+ const s = net.createServer((peer) => {
91603
+ peers.add(peer);
91604
+ peer.on("error", () => {
91605
+ }).on("close", () => peers.delete(peer));
91606
+ peer.write(`${JSON.stringify({ kind })}
91607
+ `);
91608
+ });
91609
+ s.once("error", (e2) => {
91610
+ if (e2.code !== "EADDRINUSE") return later(RETRY_MS);
91611
+ const probe = net.connect(where);
91612
+ probe.once("connect", () => {
91613
+ waiting = probe;
91614
+ let told = false;
91615
+ let said = "";
91616
+ const tell2 = (holder) => {
91617
+ if (told || waiting !== probe) return;
91618
+ told = true;
91619
+ clearTimeout(silent);
91620
+ onWaiting?.(holder);
91621
+ };
91622
+ const silent = setTimeout(tell2, KIND_MS);
91623
+ probe.on("data", (d2) => {
91624
+ said += d2;
91625
+ const line = said.split("\n");
91626
+ if (line.length < 2) return;
91627
+ let got;
91628
+ try {
91629
+ got = JSON.parse(line[0]).kind;
91630
+ } catch {
91631
+ }
91632
+ tell2(typeof got === "string" && HOLDERS.includes(got) ? got : void 0);
91633
+ });
91634
+ probe.once("close", () => {
91635
+ clearTimeout(silent);
91636
+ waiting = null;
91637
+ later(0);
91638
+ });
91639
+ });
91640
+ probe.once("error", () => {
91641
+ if (waiting === probe) return;
91642
+ if (process.platform !== "win32") {
91643
+ const removeIfStill = net.connect(where);
91644
+ removeIfStill.once("connect", () => {
91645
+ removeIfStill.destroy();
91646
+ later(0);
91647
+ });
91648
+ removeIfStill.once("error", () => {
91649
+ fs23.rmSync(where, { force: true });
91650
+ later(50 + Math.random() * 200);
91651
+ });
91652
+ return;
91653
+ }
91654
+ later(RETRY_MS);
91655
+ });
91656
+ });
91657
+ s.listen(where, () => {
91658
+ if (closed) return void s.close();
91659
+ server = s;
91660
+ onHeld();
91661
+ });
91662
+ };
91663
+ attempt();
91664
+ return {
91665
+ release() {
91666
+ if (closed) return;
91667
+ closed = true;
91668
+ if (timer) clearTimeout(timer);
91669
+ waiting?.destroy();
91670
+ server?.close();
91671
+ for (const peer of peers) peer.destroy();
91672
+ }
91673
+ };
91674
+ }
91675
+
91676
+ // src/jobs.ts
91677
+ import { spawn as spawn3 } from "node:child_process";
91678
+ import fs25 from "node:fs";
91679
+ import path27 from "node:path";
91505
91680
 
91506
91681
  // src/ideas.ts
91507
- import fs22 from "node:fs";
91508
- import path24 from "node:path";
91509
- var ideasFileOf = (job) => path24.join(runnerHome(), "ideas", `${job.id}.json`);
91682
+ import fs24 from "node:fs";
91683
+ import path26 from "node:path";
91684
+ var ideasFileOf = (job) => path26.join(runnerHome(), "ideas", `${job.id}.json`);
91510
91685
  async function writeIdeas(job, list) {
91511
91686
  const workflows = [];
91512
91687
  let after = null;
@@ -91517,18 +91692,18 @@ async function writeIdeas(job, list) {
91517
91692
  after = page.value.next;
91518
91693
  } while (after);
91519
91694
  const file2 = ideasFileOf(job);
91520
- fs22.mkdirSync(path24.dirname(file2), { recursive: true });
91521
- fs22.writeFileSync(file2, `${JSON.stringify({ workflows, ideas: job.discussion.ideas }, null, 2)}
91695
+ fs24.mkdirSync(path26.dirname(file2), { recursive: true });
91696
+ fs24.writeFileSync(file2, `${JSON.stringify({ workflows, ideas: job.discussion.ideas }, null, 2)}
91522
91697
  `);
91523
91698
  return { file: file2 };
91524
91699
  }
91525
91700
  function readIdeas(file2) {
91526
91701
  try {
91527
- return JSON.parse(fs22.readFileSync(file2, "utf8")).ideas;
91702
+ return JSON.parse(fs24.readFileSync(file2, "utf8")).ideas;
91528
91703
  } catch {
91529
91704
  return void 0;
91530
91705
  } finally {
91531
- fs22.rmSync(file2, { force: true });
91706
+ fs24.rmSync(file2, { force: true });
91532
91707
  }
91533
91708
  }
91534
91709
 
@@ -91543,7 +91718,7 @@ var END_MS = 1e4;
91543
91718
  var RECONNECT_MS = [1e3, 2e3, 5e3, 1e4, 3e4];
91544
91719
  var RENEW_MS = 5 * 6e4;
91545
91720
  var LEASE_MS = 15 * 6e4;
91546
- var RETRY_MS = 3e4;
91721
+ var RETRY_MS2 = 3e4;
91547
91722
  var KILL_GRACE_MS = 5e3;
91548
91723
  var STOP_MS = 1e4;
91549
91724
  var TEST_MS = 3e4;
@@ -91555,38 +91730,38 @@ var defaultDeps = (machineId2) => ({
91555
91730
  endMs: END_MS,
91556
91731
  renewMs: RENEW_MS,
91557
91732
  leaseMs: LEASE_MS,
91558
- retryMs: RETRY_MS,
91733
+ retryMs: RETRY_MS2,
91559
91734
  testMs: TEST_MS,
91560
91735
  stopMs: STOP_MS,
91561
91736
  akb: bundledAkb(),
91562
91737
  runtimes: detectRuntimes
91563
91738
  });
91564
- var ownFolder = () => path25.join(runnerHome(), "work");
91739
+ var ownFolder = () => path27.join(runnerHome(), "work");
91565
91740
  function folderFor(job) {
91566
91741
  if (!job.projectDir) {
91567
- const cwd = path25.join(ownFolder(), job.id);
91568
- fs23.mkdirSync(cwd, { recursive: true });
91742
+ const cwd = path27.join(ownFolder(), job.id);
91743
+ fs25.mkdirSync(cwd, { recursive: true });
91569
91744
  return { cwd };
91570
91745
  }
91571
91746
  const real = realpath4(job.projectDir);
91572
- if (real && allowedProjects().some((root) => real === root || real.startsWith(root + path25.sep))) return { cwd: real };
91747
+ if (real && allowedProjects().some((root) => real === root || real.startsWith(root + path27.sep))) return { cwd: real };
91573
91748
  return { refused: `This Runner is not allowed to work in ${job.projectDir}.` };
91574
91749
  }
91575
91750
  function realpath4(p2) {
91576
91751
  try {
91577
- return fs23.realpathSync(p2);
91752
+ return fs25.realpathSync(p2);
91578
91753
  } catch {
91579
91754
  return null;
91580
91755
  }
91581
91756
  }
91582
- var attachmentsOf = (job) => path25.join(runnerHome(), "attachments", job.id);
91757
+ var attachmentsOf = (job) => path27.join(runnerHome(), "attachments", job.id);
91583
91758
  function writeAttachments(job) {
91584
91759
  if (!job.attachments.length) return "";
91585
91760
  const dir = attachmentsOf(job);
91586
- fs23.mkdirSync(dir, { recursive: true });
91761
+ fs25.mkdirSync(dir, { recursive: true });
91587
91762
  const files = job.attachments.map((a3) => {
91588
- const file2 = path25.join(dir, path25.basename(a3.name));
91589
- fs23.writeFileSync(file2, Buffer.from(a3.content, "base64"));
91763
+ const file2 = path27.join(dir, path27.basename(a3.name));
91764
+ fs25.writeFileSync(file2, Buffer.from(a3.content, "base64"));
91590
91765
  return file2;
91591
91766
  });
91592
91767
  return `
@@ -91784,12 +91959,12 @@ async function runJob(id, deps, track = () => {
91784
91959
  const claim = await deps.api.claimJob(deps.machineId, id);
91785
91960
  if (!claim.ok || !claim.value.claimed) return;
91786
91961
  const job = claim.value.job;
91787
- const held3 = [];
91962
+ const held4 = [];
91788
91963
  try {
91789
- await runClaimed(job, deps, track, (dir) => held3.push(hold(dir)));
91964
+ await runClaimed(job, deps, track, (dir) => held4.push(hold(dir)));
91790
91965
  } finally {
91791
- for (const release2 of held3) release2();
91792
- fs23.rmSync(attachmentsOf(job), { recursive: true, force: true });
91966
+ for (const release2 of held4) release2();
91967
+ fs25.rmSync(attachmentsOf(job), { recursive: true, force: true });
91793
91968
  }
91794
91969
  }
91795
91970
  async function runClaimed(job, deps, track, keep) {
@@ -91839,9 +92014,9 @@ async function runClaimed(job, deps, track, keep) {
91839
92014
 
91840
92015
  This task came out of a discussion. Read it for what the card leaves out: ${file2}` : "";
91841
92016
  const planned = (resume, fork, said2 = "") => {
91842
- let p2 = deps.plan(job.runtime, prompt + said2, cwd, resume, job.model, settingsOf(job), fork);
91843
- if (ws) p2 = { ...p2, env: { ...p2.env, ...akbEnv(deps.akb, ws.board, ws.task) } };
91844
- if (ideas) p2 = { ...p2, env: { ...p2.env, ...akbEnv(deps.akb, discussed), KANBAN_IDEAS: ideas } };
92017
+ let p2 = deps.plan(job.runtime, job.lang ? inLanguage(prompt + said2, job.lang) : prompt + said2, cwd, resume, job.model, settingsOf(job), fork);
92018
+ if (ws) p2 = { ...p2, env: { ...p2.env, ...akbEnv(deps.akb, ws.board, ws.task, job.lang) } };
92019
+ if (ideas) p2 = { ...p2, env: { ...p2.env, ...akbEnv(deps.akb, discussed, void 0, job.lang), KANBAN_IDEAS: ideas } };
91845
92020
  return p2;
91846
92021
  };
91847
92022
  const carried = !!job.resume && (!job.fork || forks(job.runtime));
@@ -91899,9 +92074,9 @@ This task came out of a discussion. Read it for what the card leaves out: ${file
91899
92074
  stop(child);
91900
92075
  await stream.end();
91901
92076
  if (stream.lost()) return;
91902
- const ok2 = end.ok || stream.asked();
92077
+ const ok = end.ok || stream.asked();
91903
92078
  const said2 = told(stream.asked() ? cutShort(report(plan3, end), stream.said()) : report(plan3, end));
91904
- if (!ok2 || !ws) return finish(deps, id, lease, ok2 ? "done" : "failed", ok2 ? "" : end.error ?? "", said2);
92079
+ if (!ok || !ws) return finish(deps, id, lease, ok ? "done" : "failed", ok ? "" : end.error ?? "", said2);
91905
92080
  return sendBack(deps, id, lease, ws, said2, () => keepChat(job, said2.reply));
91906
92081
  }
91907
92082
  const printed = async (plan3) => {
@@ -91972,11 +92147,11 @@ var stop = (child) => {
91972
92147
  setTimeout(() => child.kill("SIGKILL"), KILL_GRACE_MS).unref();
91973
92148
  };
91974
92149
  var wait2 = (ms2) => new Promise((resolve2) => setTimeout(resolve2, ms2));
91975
- var held2 = (deps, lease) => Date.now() - lease.at < deps.leaseMs;
92150
+ var held3 = (deps, lease) => Date.now() - lease.at < deps.leaseMs;
91976
92151
  async function whileOffline(deps, id, lease, attempt) {
91977
92152
  for (; ; ) {
91978
92153
  const out = await attempt();
91979
- if (!("offline" in out && out.offline) || !held2(deps, lease)) return out;
92154
+ if (!("offline" in out && out.offline) || !held3(deps, lease)) return out;
91980
92155
  await wait2(deps.retryMs);
91981
92156
  const renewed = await deps.api.renewJob(deps.machineId, id);
91982
92157
  if (renewed.ok && renewed.value.renewed) lease.at = Date.now();
@@ -92023,8 +92198,8 @@ async function runTest(job, deps) {
92023
92198
  const report2 = (await deps.runtimes()).find((r2) => r2.name === job.runtime);
92024
92199
  if (!report2?.installed) return finish2("failed", "not_installed");
92025
92200
  if (report2.loggedIn === false) return finish2("failed", "signed_out");
92026
- const cwd = path25.join(ownFolder(), "test");
92027
- fs23.mkdirSync(cwd, { recursive: true });
92201
+ const cwd = path27.join(ownFolder(), "test");
92202
+ fs25.mkdirSync(cwd, { recursive: true });
92028
92203
  const run2 = deps.plan(job.runtime, job.prompt, cwd, null, job.model, settingsOf(job));
92029
92204
  const startedAt = Date.now();
92030
92205
  if (run2.converse) {
@@ -92108,17 +92283,81 @@ Workflow: ${workflow}` : ""}
92108
92283
  Memory: ${ws.board}/memory`;
92109
92284
  }
92110
92285
 
92286
+ // src/presence.ts
92287
+ var PING_MS = 3e4;
92288
+ var BACKOFF_MS2 = [1e3, 2e3, 5e3, 1e4, 3e4];
92289
+ function keepPresence(machineId2, pingMs = PING_MS) {
92290
+ let closed = false;
92291
+ let ws = null;
92292
+ let timer = null;
92293
+ let attempts = 0;
92294
+ const retry2 = () => {
92295
+ if (closed) return;
92296
+ timer = setTimeout(open, BACKOFF_MS2[Math.min(attempts++, BACKOFF_MS2.length - 1)]);
92297
+ timer.unref?.();
92298
+ };
92299
+ const open = async () => {
92300
+ timer = null;
92301
+ const socket = await openPresence(machineId2).catch(() => null);
92302
+ if (closed) return socket?.close();
92303
+ if (!socket) return retry2();
92304
+ ws = socket;
92305
+ let opened = false;
92306
+ let dropped = false;
92307
+ let heard = Date.now();
92308
+ let ping = null;
92309
+ const drop = async () => {
92310
+ if (dropped) return;
92311
+ dropped = true;
92312
+ if (ping) clearInterval(ping);
92313
+ if (ws === socket) ws = null;
92314
+ try {
92315
+ socket.close();
92316
+ } catch {
92317
+ }
92318
+ if (closed) return;
92319
+ if (!opened) {
92320
+ const route = await presenceRoute(machineId2);
92321
+ if (!route.ok && route.code === "not_found") return;
92322
+ }
92323
+ retry2();
92324
+ };
92325
+ socket.addEventListener("open", () => {
92326
+ opened = true;
92327
+ attempts = 0;
92328
+ ping = setInterval(() => {
92329
+ if (Date.now() - heard > pingMs * 2) return void drop();
92330
+ try {
92331
+ socket.send("ping");
92332
+ } catch {
92333
+ }
92334
+ }, pingMs);
92335
+ ping.unref?.();
92336
+ });
92337
+ socket.addEventListener("message", () => heard = Date.now());
92338
+ socket.addEventListener("close", () => void drop());
92339
+ };
92340
+ void open();
92341
+ return {
92342
+ close: () => {
92343
+ closed = true;
92344
+ if (timer) clearTimeout(timer);
92345
+ ws?.close();
92346
+ }
92347
+ };
92348
+ }
92349
+
92111
92350
  // src/connect.ts
92112
92351
  var HEARTBEAT_MS2 = 5 * 6e4;
92113
92352
  var POLL_MS = 6e4;
92114
- var BACKOFF_MS2 = [1e3, 2e3, 5e3, 1e4, 3e4];
92115
- var MAX_RUNNING = 2;
92353
+ var BACKOFF_MS3 = [1e3, 2e3, 5e3, 1e4, 3e4];
92116
92354
  function startRunner(options) {
92117
92355
  let registration = null;
92118
92356
  let connected = false;
92119
92357
  let closed = false;
92120
92358
  let attempts = 0;
92121
92359
  let live = null;
92360
+ let presence = null;
92122
92361
  let timer = null;
92123
92362
  let reading = false;
92124
92363
  let readAgain = false;
@@ -92185,15 +92424,15 @@ function startRunner(options) {
92185
92424
  if (closed) return;
92186
92425
  if (!answer.ok) {
92187
92426
  if (answer.code === "runner_elsewhere") {
92188
- const held3 = await readRunner();
92189
- return finish2({ reason: "elsewhere", machine: held3.ok && held3.value.runner?.machineName || "?" });
92427
+ const held4 = await readRunner();
92428
+ return finish2({ reason: "elsewhere", machine: held4.ok && held4.value.runner?.machineName || "?" });
92190
92429
  }
92191
92430
  if (answer.code === "runner_outdated") return finish2({ reason: "outdated" });
92192
92431
  if (answer.code) return finish2({ reason: "refused", message: answer.error });
92193
92432
  if (connected) options.onDisconnected?.();
92194
92433
  connected = false;
92195
92434
  status(t.reconnecting());
92196
- return schedule(BACKOFF_MS2[Math.min(attempts++, BACKOFF_MS2.length - 1)]);
92435
+ return schedule(BACKOFF_MS3[Math.min(attempts++, BACKOFF_MS3.length - 1)]);
92197
92436
  }
92198
92437
  attempts = 0;
92199
92438
  if (!connected) {
@@ -92201,6 +92440,7 @@ function startRunner(options) {
92201
92440
  status(idle2());
92202
92441
  options.onConnected?.();
92203
92442
  openLive();
92443
+ presence ??= keepPresence(registration.machineId);
92204
92444
  void deps().runtimes();
92205
92445
  }
92206
92446
  await readJobs();
@@ -92244,7 +92484,7 @@ function startRunner(options) {
92244
92484
  await finishJob(registration.machineId, job.id, "failed", "The Runner restarted while this job was running.");
92245
92485
  continue;
92246
92486
  }
92247
- if (job.state !== "waiting" || running.has(job.id) || running.size >= MAX_RUNNING && !job.test) continue;
92487
+ if (job.state !== "waiting" || running.has(job.id)) continue;
92248
92488
  start(job.id);
92249
92489
  }
92250
92490
  } finally {
@@ -92293,10 +92533,12 @@ function startRunner(options) {
92293
92533
  const close = async (release2 = true) => {
92294
92534
  if (closed) return;
92295
92535
  closed = true;
92536
+ lock.release();
92296
92537
  if (timer) clearTimeout(timer);
92297
92538
  clearInterval(agentsTimer);
92298
92539
  clearInterval(sweepTimer);
92299
92540
  live?.close();
92541
+ presence?.close();
92300
92542
  const stops = Promise.all([...running.values()].map((halt) => halt?.()));
92301
92543
  let timeout;
92302
92544
  await Promise.race([stops, new Promise((resolve2) => timeout = setTimeout(resolve2, (options.deps?.stopMs ?? STOP_MS) + 1e3))]);
@@ -92305,49 +92547,74 @@ function startRunner(options) {
92305
92547
  };
92306
92548
  removeLegacy();
92307
92549
  pruneChats();
92308
- void tick();
92550
+ const lock = holdMachine(options.kind, () => void tick(), (holder) => {
92551
+ status(t.waitingHere(holder));
92552
+ options.onWaiting?.(holder);
92553
+ });
92309
92554
  return { stopped: stopped2, close: () => close(true), renew: () => schedule(0) };
92310
92555
  }
92311
92556
 
92312
92557
  // src/desktop.ts
92313
92558
  import { spawnSync as spawnSync4 } from "node:child_process";
92314
- import fs24 from "node:fs";
92315
- import path26 from "node:path";
92559
+ import fs26 from "node:fs";
92560
+ import path28 from "node:path";
92316
92561
  import readline from "node:readline";
92317
92562
 
92318
92563
  // src/signin.ts
92319
92564
  import { spawn as spawn4 } from "node:child_process";
92320
- import crypto3 from "node:crypto";
92565
+ import crypto4 from "node:crypto";
92321
92566
  import http from "node:http";
92322
92567
  var WAIT_MS = 10 * 60 * 1e3;
92323
- var PAGE = (ok2) => `<!doctype html><meta charset="utf-8"><title>WildCrane</title><body style="font:16px system-ui;margin:4em;text-align:center">${ok2 ? "\u5DF2\u767B\u5F55\uFF0C\u53EF\u4EE5\u5173\u95ED\u6B64\u9875\u9762\u3002<br>Signed in \u2014 you can close this page." : "\u767B\u5F55\u672A\u5B8C\u6210\u3002<br>Sign-in did not finish."}</body>`;
92568
+ var CRANE = "M2 55.75C2 55.69 2.69 55.09 3.55 54.37L5.09 53.1L6.61 53.1L8.13 53.1L12.49 49.87C14.9 48.07 16.83 46.58 16.77 46.53C16.75 46.47 15.56 46.64 14.15 46.89C12.74 47.13 11.55 47.3 11.53 47.24C11.47 47.19 12.02 46.22 12.71 45.06L14.01 42.96L18.21 41.34C20.53 40.45 22.41 39.65 22.41 39.57C22.41 39.49 21.27 38.55 19.92 37.5C15.92 34.4 13.13 31.31 10.86 27.47L10.01 26.01L10.53 26.18C10.84 26.26 12.96 27.2 15.28 28.25C17.6 29.32 19.53 30.12 19.59 30.07C19.64 30.01 17.68 28.69 15.25 27.11C12.8 25.57 10.56 23.97 10.31 23.58C9.15 21.95 7.25 17.5 7.25 16.46C7.25 16.26 10.01 17.95 13.38 20.18C16.77 22.45 19.73 24.38 19.95 24.49C20.42 24.74 20.25 24.57 13.9 19.02L9.18 14.88L8.6 13.36C7.72 10.96 6.97 7.54 6.97 5.77L6.97 4.14L9.4 6.46C12.66 9.61 15.56 12.01 23.57 18.25C27.32 21.18 30.5 23.72 30.64 23.91C30.8 24.08 31.68 26.51 32.6 29.3L34.28 34.35L35.72 35.65C37.54 37.3 37.98 37.47 36.85 36.09L35.96 35.01L37.12 34.71C39.11 34.21 40.16 33.52 42.54 31.12L44.8 28.8L46.02 28.96C46.71 29.07 47.56 29.07 47.95 28.96L48.67 28.8L48.67 28.25L48.67 27.69L49.27 27.69C49.63 27.69 50.1 27.58 50.32 27.42L50.71 27.17L51.01 27.42C51.18 27.56 52.78 27.86 54.58 28.08C56.37 28.3 57.86 28.52 57.94 28.58C58 28.63 56.73 28.85 55.13 29.05C53.53 29.24 50.96 29.77 49.44 30.21C47.92 30.65 46.57 31.01 46.43 31.01C46.29 31.01 44.22 32.97 41.85 35.37C37.57 39.62 34.42 42.14 30.83 44.15L29.14 45.09L25.97 45.73L22.77 46.39L14.59 51.11L6.42 55.86L4.21 55.86C2.99 55.86 2 55.8 2 55.75Z";
92569
+ var CREST = "M44.97 27.45C44.61 26.89 46.57 25.76 47.87 25.76C48.72 25.76 50.6 26.7 50.6 27.14C50.6 27.64 45.27 27.97 44.97 27.45Z";
92570
+ var COPY = {
92571
+ zh: {
92572
+ ok: ["\u5DF2\u767B\u5F55 WildCrane", "\u53EF\u4EE5\u5173\u95ED\u6B64\u9875\u9762\uFF0C\u56DE\u5230 WildCrane \u7EE7\u7EED\u3002"],
92573
+ denied: ["\u767B\u5F55\u672A\u5B8C\u6210", "\u4F60\u53D6\u6D88\u4E86 GitHub \u6388\u6743\u3002\u8BF7\u56DE\u5230 WildCrane \u91CD\u65B0\u767B\u5F55\u3002"],
92574
+ failed: ["\u767B\u5F55\u672A\u5B8C\u6210", "\u767B\u5F55\u65F6\u51FA\u4E86\u70B9\u95EE\u9898\u3002\u8BF7\u56DE\u5230 WildCrane \u91CD\u8BD5\u3002"]
92575
+ },
92576
+ en: {
92577
+ ok: ["Signed in to WildCrane", "You can close this page and return to WildCrane."],
92578
+ denied: ["Sign-in didn't finish", "GitHub access was cancelled. Return to WildCrane to sign in again."],
92579
+ failed: ["Sign-in didn't finish", "Something went wrong while signing in. Return to WildCrane to try again."]
92580
+ }
92581
+ };
92582
+ var langOf = (accept) => /^\s*zh\b/i.test(accept ?? "") ? "zh" : "en";
92583
+ var PAGE = (outcome, lang) => {
92584
+ const [title, body] = COPY[lang][outcome];
92585
+ const ok = outcome === "ok";
92586
+ const icon = ok ? '<polyline points="20 6 9 17 4 12"/>' : '<path d="M18 6 6 18M6 6l12 12"/>';
92587
+ return `<!doctype html><html lang="${lang === "zh" ? "zh-CN" : "en"}"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${title} \xB7 WildCrane</title><style>*{margin:0;box-sizing:border-box}body{display:flex;flex-direction:column;min-height:100dvh;background:#fff;color:#24231f;font-family:ui-sans-serif,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif}header{padding:16px 36px 0}main{flex:1;display:flex;align-items:center;justify-content:center;padding:0 36px 64px;text-align:center}.icon{display:flex;margin:0 auto;width:44px;height:44px;border-radius:50%;align-items:center;justify-content:center;background:${ok ? "#e4f3ea" : "#f7ddce"};color:${ok ? "#2f6b46" : "#b83a12"}}h1{margin-top:16px;font-size:24px;font-weight:850;line-height:1.25;letter-spacing:-0.02em}p{margin:8px auto 0;max-width:38ch;font-size:13px;line-height:19px;color:#565550}</style><header><svg viewBox="2 4 56 52" width="23.7" height="22" aria-hidden="true"><path d="${CRANE}" fill="#24231f"/><path d="${CREST}" fill="#dd4f1e"/></svg></header><main><div><span class="icon"><svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${icon}</svg></span><h1>${title}</h1><p>${body}</p></div></main></html>`;
92588
+ };
92324
92589
  async function signIn(options = {}) {
92325
- const verifier = base64url(crypto3.randomBytes(32));
92326
- const challenge = base64url(crypto3.createHash("sha256").update(verifier).digest());
92590
+ const verifier = base64url(crypto4.randomBytes(32));
92591
+ const challenge = base64url(crypto4.createHash("sha256").update(verifier).digest());
92327
92592
  let finish2 = () => {
92328
92593
  };
92329
- const answered = new Promise((resolve2) => {
92330
- finish2 = (code2, failure2) => resolve2({ code: code2, failure: failure2 });
92331
- });
92332
- const server = http.createServer((req, res) => {
92594
+ const answered = new Promise((resolve2) => finish2 = resolve2);
92595
+ let answering = false;
92596
+ const server = http.createServer(async (req, res) => {
92333
92597
  const asked = new URL(req.url ?? "/", "http://127.0.0.1");
92334
- if (asked.pathname !== "/signed-in") {
92598
+ if (asked.pathname !== "/signed-in" || answering) {
92335
92599
  res.writeHead(404).end();
92336
92600
  return;
92337
92601
  }
92602
+ answering = true;
92338
92603
  const params = asked.searchParams;
92339
- const code2 = params.get("code");
92340
- res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
92341
- res.end(PAGE(!!code2));
92604
+ const code = params.get("code");
92342
92605
  const error = params.get("error");
92343
- finish2(code2, code2 ? void 0 : {
92606
+ const failure2 = code ? await exchange(code, verifier) : {
92344
92607
  why: error === "access_denied" ? "denied" : "other",
92345
92608
  detail: params.get("error_description") || error || "no code came back"
92346
- });
92609
+ };
92610
+ const outcome = !failure2 ? "ok" : failure2.why === "denied" ? "denied" : "failed";
92611
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
92612
+ res.end(PAGE(outcome, langOf(req.headers["accept-language"])));
92613
+ finish2(failure2);
92347
92614
  });
92348
92615
  await new Promise((resolve2) => server.listen(0, "127.0.0.1", resolve2));
92349
92616
  const { port } = server.address();
92350
- const { supabaseUrl, anonKey } = cloudEndpoints();
92617
+ const { supabaseUrl } = cloudEndpoints();
92351
92618
  const url = new URL(`${supabaseUrl}/auth/v1/authorize`);
92352
92619
  url.searchParams.set("provider", "github");
92353
92620
  url.searchParams.set("redirect_to", `http://127.0.0.1:${port}/signed-in`);
@@ -92357,14 +92624,17 @@ async function signIn(options = {}) {
92357
92624
  const onUrl = options.onUrl ?? ((u2) => status(t.signIn(u2)));
92358
92625
  onUrl(url.toString());
92359
92626
  openBrowser(url.toString());
92360
- const timer = setTimeout(() => finish2(null, { why: "timeout", detail: "timed out" }), WAIT_MS);
92361
- const cancel = () => finish2(null, { why: "cancelled", detail: "cancelled" });
92627
+ const timer = setTimeout(() => finish2({ why: "timeout", detail: "timed out" }), WAIT_MS);
92628
+ const cancel = () => finish2({ why: "cancelled", detail: "cancelled" });
92362
92629
  options.signal?.addEventListener("abort", cancel);
92363
- const { code, failure } = await answered;
92630
+ const failure = await answered;
92364
92631
  clearTimeout(timer);
92365
92632
  options.signal?.removeEventListener("abort", cancel);
92366
92633
  server.close();
92367
- if (!code) return failure ?? { why: "other", detail: "no code came back" };
92634
+ return failure;
92635
+ }
92636
+ async function exchange(code, verifier) {
92637
+ const { supabaseUrl, anonKey } = cloudEndpoints();
92368
92638
  let response;
92369
92639
  try {
92370
92640
  response = await fetch(`${supabaseUrl}/auth/v1/token?grant_type=pkce`, {
@@ -92375,7 +92645,7 @@ async function signIn(options = {}) {
92375
92645
  } catch (e2) {
92376
92646
  return { why: "unreachable", detail: e2 instanceof Error ? e2.message : String(e2) };
92377
92647
  }
92378
- const session = response.ok ? sessionFrom(await response.json()) : null;
92648
+ const session = response.ok ? sessionFrom(await response.json().catch(() => ({}))) : null;
92379
92649
  if (!session) return { why: "other", detail: `Cloud answered ${response.status}` };
92380
92650
  writeSession(session);
92381
92651
  return null;
@@ -92393,7 +92663,7 @@ var base64url = (bytes2) => bytes2.toString("base64").replace(/\+/g, "-").replac
92393
92663
  // src/desktop.ts
92394
92664
  var DESKTOP_PROTOCOL = 1;
92395
92665
  var POLL_MS2 = 6e4;
92396
- var RETRY_MS2 = 6e4;
92666
+ var RETRY_MS3 = 6e4;
92397
92667
  var WEB_URL = () => (process.env.WILDCRANE_WEB_URL || "https://app.wildcrane.ai").replace(/\/+$/, "");
92398
92668
  var taskUrl = (task) => `${WEB_URL()}/tasks/${encodeURIComponent(task)}`;
92399
92669
  function emit(event2) {
@@ -92467,12 +92737,12 @@ async function runDesktop(tell2) {
92467
92737
  const readProfile2 = async () => {
92468
92738
  const session = readSession();
92469
92739
  const profile = await readProfile();
92470
- const held3 = profile.ok ? profile.value.session : {};
92740
+ const held4 = profile.ok ? profile.value.session : {};
92471
92741
  emit({
92472
92742
  type: "account",
92473
- name: held3.name || held3.handle || session?.name || session?.handle || "",
92474
- email: held3.email || session?.email || "",
92475
- avatar: held3.avatarUrl || session?.avatarUrl || ""
92743
+ name: held4.name || held4.handle || session?.name || session?.handle || "",
92744
+ email: held4.email || session?.email || "",
92745
+ avatar: held4.avatarUrl || session?.avatarUrl || ""
92476
92746
  });
92477
92747
  };
92478
92748
  const readPlan2 = async () => {
@@ -92489,15 +92759,23 @@ async function runDesktop(tell2) {
92489
92759
  pricing: `${cloudEndpoints().api}/pricing?lang=${isChinese() ? "zh" : "en"}`
92490
92760
  });
92491
92761
  };
92492
- const onConnected = () => {
92493
- tell2({ type: "connected" });
92494
- emit({ type: "connection", state: "connected", machine: machineName() });
92762
+ const readAll2 = () => {
92495
92763
  void readProfile2();
92496
92764
  void readFolders();
92497
92765
  void readPlan2();
92498
92766
  void readAlerts();
92499
92767
  readInbox3();
92500
92768
  };
92769
+ const onConnected = () => {
92770
+ tell2({ type: "connected" });
92771
+ emit({ type: "connection", state: "connected", machine: machineName() });
92772
+ readAll2();
92773
+ };
92774
+ const onWaiting = (holder) => {
92775
+ tell2({ type: "waiting" });
92776
+ emit({ type: "connection", state: "paused", ...holder ? { holder } : {} });
92777
+ readAll2();
92778
+ };
92501
92779
  const handle = async (command) => {
92502
92780
  switch (command.cmd) {
92503
92781
  case "signIn": {
@@ -92533,8 +92811,8 @@ async function runDesktop(tell2) {
92533
92811
  const project = command.project;
92534
92812
  const why = refusal(command.path);
92535
92813
  if (why) return emit({ type: "folderRefused", why, project });
92536
- const real = fs24.realpathSync(command.path);
92537
- const added = await addFolder(id, path26.basename(real) || real, real, project);
92814
+ const real = fs26.realpathSync(command.path);
92815
+ const added = await addFolder(id, path28.basename(real) || real, real, project);
92538
92816
  if (!added.ok) {
92539
92817
  const taken = added.code === "folder_taken";
92540
92818
  return emit({ type: "folderRefused", why: taken ? "taken" : "failed", project, ...taken ? { taken: added.project } : {} });
@@ -92543,9 +92821,9 @@ async function runDesktop(tell2) {
92543
92821
  return readFolders();
92544
92822
  }
92545
92823
  case "removeFolder": {
92546
- const held3 = folders2.find((f2) => f2.project === command.project);
92824
+ const held4 = folders2.find((f2) => f2.project === command.project);
92547
92825
  const removed = await removeFolder(id, command.project);
92548
- if (removed.ok && held3) for (const dir of disallow(held3.path)) clearAkb(dir);
92826
+ if (removed.ok && held4) for (const dir of disallow(held4.path)) clearAkb(dir);
92549
92827
  return readFolders();
92550
92828
  }
92551
92829
  case "folderNotify":
@@ -92591,8 +92869,10 @@ async function runDesktop(tell2) {
92591
92869
  emit({ type: "connection", state: "connecting" });
92592
92870
  runner = startRunner({
92593
92871
  registration: async () => ({ machineId: id, machineName: machineName(), version: VERSION, runtimes: await detectRuntimes(true) }),
92872
+ kind: "desktop",
92594
92873
  notify: () => notify,
92595
92874
  onConnected,
92875
+ onWaiting,
92596
92876
  onDisconnected: () => emit({ type: "connection", state: "reconnecting" }),
92597
92877
  onHint: (payload) => {
92598
92878
  if (payload.alertId || !payload.taskId) void readAlerts();
@@ -92612,7 +92892,7 @@ async function runDesktop(tell2) {
92612
92892
  if (why.reason === "outdated") return EXIT_OUTDATED;
92613
92893
  emit({ type: "connection", state: why.reason, ...why.reason === "elsewhere" ? { machine: why.machine } : {} });
92614
92894
  const again = new Promise((resolve2) => wake = resolve2);
92615
- const retry2 = setTimeout(() => wake("again"), RETRY_MS2);
92895
+ const retry2 = setTimeout(() => wake("again"), RETRY_MS3);
92616
92896
  const after = await again;
92617
92897
  clearTimeout(retry2);
92618
92898
  if (after === "quit") return 0;
@@ -92640,7 +92920,9 @@ async function main() {
92640
92920
  const id = machineId();
92641
92921
  const runner = startRunner({
92642
92922
  registration: async () => ({ machineId: id, machineName: machineName(), version: VERSION, runtimes: await detectRuntimes(true) }),
92923
+ kind: "terminal",
92643
92924
  onConnected: () => tell({ type: "connected" }),
92925
+ onWaiting: () => tell({ type: "waiting" }),
92644
92926
  // Tests shorten the job poll, which stands in when Realtime is out of reach.
92645
92927
  pollMs: Number(process.env.WILDCRANE_POLL_MS) || void 0
92646
92928
  });