@koda-sl/baker-cli 0.447.0-dev.397af7869 → 0.452.0-dev.63867ce0e

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/cli.js CHANGED
@@ -201,7 +201,7 @@ import {
201
201
  withoutCastName,
202
202
  wordCount,
203
203
  ytDlpBlockSignal
204
- } from "./chunk-WMAHVZMY.js";
204
+ } from "./chunk-Q2ZO6I47.js";
205
205
  import {
206
206
  NAME_CONTAINS_ARG,
207
207
  csvOrJson,
@@ -67690,11 +67690,15 @@ function unsourcedFigureLines(spec, knowledge2) {
67690
67690
  const records = withDigits(knowledge2);
67691
67691
  const lower = knowledge2.toLowerCase();
67692
67692
  return spec.copy.flatMap((line) => {
67693
- const figure = [...withDigits(line.text).matchAll(CLAIMED_FIGURE)].some((m) => !figureSourced(m, records));
67694
- const superlative = [...line.text.matchAll(SUPERLATIVE)].some((m) => !lower.includes(m[0].toLowerCase()));
67695
- return figure || superlative ? [line.text] : [];
67693
+ const figures = [...withDigits(line.text).matchAll(CLAIMED_FIGURE)].filter((m) => !figureSourced(m, records));
67694
+ const superlatives = [...line.text.matchAll(SUPERLATIVE)].filter((m) => !lower.includes(m[0].toLowerCase()));
67695
+ const claims = [...new Set([...superlatives, ...figures].map((m) => m[0].trim()))];
67696
+ return claims.length > 0 ? [{ text: line.text, claims }] : [];
67696
67697
  });
67697
67698
  }
67699
+ function claimsWithoutSource(spec, knowledge2) {
67700
+ return [...new Set(unsourcedFigureLines(spec, knowledge2).flatMap((l) => l.claims))];
67701
+ }
67698
67702
  function proofProblems(spec, knowledge2) {
67699
67703
  const problems = [];
67700
67704
  const named = /* @__PURE__ */ new Set();
@@ -67713,9 +67717,10 @@ function proofProblems(spec, knowledge2) {
67713
67717
  );
67714
67718
  }
67715
67719
  }
67716
- for (const text3 of unsourcedFigureLines(spec, knowledge2)) {
67720
+ for (const { text: text3, claims } of unsourcedFigureLines(spec, knowledge2)) {
67721
+ const named2 = claims.map((c) => `"${c}"`).join(", ");
67717
67722
  problems.push(
67718
- `"${text3}" promises a figure or a superlative ("the only", "the fastest") the company's records (src/content, COMPANY.md) do not hold: say only what they record, or drop it.`
67723
+ `${claims.length === 1 && claims[0] === text3.trim() ? named2 : `${named2} in "${text3}"`}: the company's records (src/content, COMPANY.md) do not hold it, so the line promises what nobody wrote. Say only what they record, or drop ${claims.length === 1 ? "it" : "them"}.`
67719
67724
  );
67720
67725
  }
67721
67726
  return problems;
@@ -71148,7 +71153,10 @@ function fieldProblems(f) {
71148
71153
  "message is missing: the one promise a stranger should get, what is offered and what changes for them"
71149
71154
  );
71150
71155
  if (!f.why) problems.push("why is missing: the brief finding it answers");
71151
- if (!f.awareness) problems.push(`awareness must be one of ${AWARENESS_STAGES.join(", ")}`);
71156
+ if (!f.awareness) {
71157
+ const written = f.awarenessWritten ? `"${f.awarenessWritten}" ` : "";
71158
+ problems.push(`awareness ${written}must be one of ${AWARENESS_STAGES.join(", ")}`);
71159
+ }
71152
71160
  if (!(f.angle && IDEA_ANGLES.includes(f.angle)))
71153
71161
  problems.push(`angle ${f.angle ? `"${f.angle}" ` : ""}must be one of ${IDEA_ANGLES.join(", ")}`);
71154
71162
  if (!(f.format && IDEA_FORMATS.includes(f.format)))
@@ -71165,7 +71173,8 @@ function parseIdea(id, markdown) {
71165
71173
  why: text2(fm.why),
71166
71174
  angle: text2(fm.angle),
71167
71175
  format: text2(fm.format),
71168
- awareness: oneOf(AWARENESS_STAGES, fm.awareness)
71176
+ awareness: oneOf(AWARENESS_STAGES, fm.awareness),
71177
+ awarenessWritten: text2(fm.awareness)
71169
71178
  };
71170
71179
  const problems = fieldProblems(f);
71171
71180
  const { who, message, why, angle, format, awareness } = f;
@@ -71249,11 +71258,28 @@ function repeatedFailure(idea, failed) {
71249
71258
  );
71250
71259
  return lost ? [`says what "${lost.id}" said, and that idea failed in the account: change the message, not just the picture`] : [];
71251
71260
  }
71261
+ function likelyMeant(id, insights) {
71262
+ const words2 = (text3) => new Set(
71263
+ text3.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter((w) => w.length >= 3)
71264
+ );
71265
+ const wanted = words2(id);
71266
+ let best;
71267
+ for (const insight of insights.values()) {
71268
+ const said = words2(`${insight.id} ${insight.quote}`);
71269
+ const shared = [...wanted].filter((w) => said.has(w)).length;
71270
+ if (shared > 0 && shared > (best?.shared ?? 0)) best = { id: insight.id, shared };
71271
+ }
71272
+ return best?.id;
71273
+ }
71252
71274
  function backingOf(idea, insights, problems) {
71253
71275
  const backing = idea.evidence.flatMap((id) => {
71254
71276
  const insight = insights.get(id);
71255
- if (!insight)
71256
- problems.push(`evidence "${id}" is not in src/content/insights/: name an insight on file or leave it out`);
71277
+ if (!insight) {
71278
+ const meant = likelyMeant(id, insights);
71279
+ problems.push(
71280
+ `evidence "${id}" is not in src/content/insights/: ${meant ? `did you mean "${meant}"? ` : ""}name an insight on file by its id, or leave it out`
71281
+ );
71282
+ }
71257
71283
  return insight ? [insight] : [];
71258
71284
  });
71259
71285
  return [...backing].sort((a, b) => EVIDENCE_WEIGHT[b.status] - EVIDENCE_WEIGHT[a.status])[0];
@@ -71731,33 +71757,49 @@ async function staticSpecGate(canvas, canvasDir) {
71731
71757
  fix: "Write the ad in prompt.json: the scene the viewer sees and each line of copy as it prints. Then re-run."
71732
71758
  };
71733
71759
  }
71734
- const sceneWords = spec.native ? [] : sceneWordProblems(spec);
71735
- if (sceneWords.length > 0) {
71736
- return {
71737
- code: "scene_words_not_in_copy",
71738
- problems: sceneWords,
71739
- fix: "Edit prompt.json: every word or figure the picture shows is a `copy` line (`in_scene`, with `on`); describe any other surface without words. Then re-run."
71740
- };
71741
- }
71742
- const concept = conceptProblems(spec);
71743
- if (concept.length > 0) {
71744
- return {
71745
- code: "concept_does_not_fit",
71746
- problems: concept,
71747
- fix: "Edit prompt.json's `concept`: name what of ours carries each missing element (`ours`), or pick another reference with `baker winning-ads fit`. Never render a copy that has lost what the reference wins on."
71748
- };
71749
- }
71760
+ const concept = {
71761
+ code: "concept_does_not_fit",
71762
+ problems: conceptProblems(spec),
71763
+ fix: "Edit prompt.json's `concept`: name what of ours carries each missing element (`ours`), or pick another reference with `baker winning-ads fit`. Never render a copy that has lost what the reference wins on."
71764
+ };
71750
71765
  const root = await findBrandRoot(canvasDir) ?? findWorkspaceRoot(canvasDir) ?? canvasDir;
71751
71766
  const records = await readRecords(root);
71752
- if (spec.native) {
71753
- const native = nativeGateProblems(spec.native, records, spec.brand?.name);
71754
- return native.length > 0 ? { code: "native_screen_invalid", problems: native, fix: "Fix prompt.json's `native` block, then re-run." } : null;
71755
- }
71756
- const proof = proofProblems(spec, records);
71757
- if (proof.length > 0) {
71758
- return { code: "proof_without_source", problems: proof, fix: "Wire the badge or drop the claim in prompt.json, then re-run." };
71759
- }
71760
- return null;
71767
+ const gates = spec.native ? [
71768
+ concept,
71769
+ {
71770
+ // A native screen's people are photos it describes, briefed as ordinary people of the market.
71771
+ code: "native_screen_invalid",
71772
+ problems: nativeGateProblems(spec.native, records, spec.brand?.name),
71773
+ fix: "Fix prompt.json's `native` block."
71774
+ }
71775
+ ] : [
71776
+ {
71777
+ // Words the scene asks for that the copy does not hold: refused here, free, rather than erased by a paid round.
71778
+ code: "scene_words_not_in_copy",
71779
+ problems: sceneWordProblems(spec),
71780
+ fix: "Edit prompt.json: every word or figure the picture shows is a `copy` line (`in_scene`, with `on`); describe any other surface without words."
71781
+ },
71782
+ concept,
71783
+ {
71784
+ code: "proof_without_source",
71785
+ problems: proofProblems(spec, records),
71786
+ fix: proofFix(claimsWithoutSource(spec, records))
71787
+ }
71788
+ ];
71789
+ return oneRefusal(gates.filter((g) => g.problems.length > 0));
71790
+ }
71791
+ function proofFix(claims) {
71792
+ const named = claims.length > 0 ? `drop ${claims.map((c) => `"${c}"`).join(", ")} or source it in the Knowledge, and ` : "";
71793
+ return `In prompt.json, ${named}wire the badge or drop any other claim named above, everywhere it appears (copy and scene).`;
71794
+ }
71795
+ function oneRefusal(gates) {
71796
+ const first = gates[0];
71797
+ if (!first) return null;
71798
+ return {
71799
+ code: first.code,
71800
+ problems: [...new Set(gates.flatMap((g) => g.problems))],
71801
+ fix: `${gates.map((g) => g.fix).join(" ")} Then re-run.`
71802
+ };
71761
71803
  }
71762
71804
  async function readRecords(root) {
71763
71805
  const parts = [await readFile12(path14.join(root, "COMPANY.md"), "utf8").catch(() => "")];
@@ -72735,6 +72777,7 @@ async function pruneOldRuns(outputsDir, keep, currentRunId, log) {
72735
72777
 
72736
72778
  // src/commands/canvas/run-resume.ts
72737
72779
  import { randomUUID } from "crypto";
72780
+ import { rmSync } from "fs";
72738
72781
  import { mkdir as mkdir4, readFile as readFile17, rm as rm4, writeFile as writeFile9 } from "fs/promises";
72739
72782
  import path22 from "path";
72740
72783
  function markerKey(canvasPath) {
@@ -72838,6 +72881,40 @@ async function markRunInFlight(outputsDir, canvasPath, runId, sourceSha) {
72838
72881
  timer.unref();
72839
72882
  return { token, stop: () => clearInterval(timer) };
72840
72883
  }
72884
+ function busyFile(outputsDir, canvasPath) {
72885
+ return path22.join(outputsDir, ".inflight", `${markerKey(canvasPath)}.busy.json`);
72886
+ }
72887
+ async function markCanvasBusy(outputsDir, canvasPath, runId, pid = process.pid) {
72888
+ const file = busyFile(outputsDir, canvasPath);
72889
+ const write = () => writeFile9(file, JSON.stringify({ runId, pid, heartbeatAt: Date.now() }));
72890
+ try {
72891
+ await mkdir4(path22.dirname(file), { recursive: true });
72892
+ await write();
72893
+ } catch {
72894
+ }
72895
+ const timer = setInterval(() => void write().catch(() => void 0), HEARTBEAT_MS);
72896
+ timer.unref();
72897
+ let released = false;
72898
+ const release = () => {
72899
+ if (released) return;
72900
+ released = true;
72901
+ clearInterval(timer);
72902
+ try {
72903
+ rmSync(file, { force: true });
72904
+ } catch {
72905
+ }
72906
+ };
72907
+ return { release };
72908
+ }
72909
+ async function canvasBusyElsewhere(outputsDir, canvasPath, now = Date.now()) {
72910
+ try {
72911
+ const held = JSON.parse(await readFile17(busyFile(outputsDir, canvasPath), "utf8"));
72912
+ if (typeof held.runId !== "string" || held.pid === void 0 || held.pid === process.pid) return null;
72913
+ return isLiveRender(held, now) ? { runId: held.runId, pid: held.pid } : null;
72914
+ } catch {
72915
+ return null;
72916
+ }
72917
+ }
72841
72918
  async function clearRunMarker(outputsDir, canvasPath, token) {
72842
72919
  try {
72843
72920
  if (token !== void 0) {
@@ -73557,7 +73634,7 @@ var sizesOf = (what) => what.match(/^(\d+(?:\.\d+)?:\d+(?:\.\d+)?(?:,\s*\d+(?:\.
73557
73634
  function carryUnchanged(now, prior, unchanged) {
73558
73635
  const settled = (f) => {
73559
73636
  const sizes = sizesOf(f.what);
73560
- return sizes.length > 0 && sizes.every((s) => unchanged.includes(s));
73637
+ return sizes.length > 0 && sizes.every((s) => unchanged.includes(s)) && !SIZES_FINDING.test(withoutSize(f.what));
73561
73638
  };
73562
73639
  return [...now.filter((f) => !settled(f)), ...prior.filter(settled)];
73563
73640
  }
@@ -73566,6 +73643,7 @@ var PAST_BAND = /^(.+?) sits? (?:close to )?where (.+?) covers? (?:it|them) \((.
73566
73643
  var OFF_EDGE = /^(.+?) runs? off the edge of the picture \((.+)\)\.?$/;
73567
73644
  var OUTSIDE_SPACE = /^(.+?) sits? outside the space every size keeps clear \((.+)\)\.?$/;
73568
73645
  var EXTRA_WORDS = /^the picture adds words that aren't in the copy: (.+?)\. Remove them, or add them to the copy\.?$/;
73646
+ var SCENE_PRICES = /^the picture shows prices the copy does not hold: (.+?)\. Make them unreadable\.?$/;
73569
73647
  var PLACED = [PAST_BAND, OFF_EDGE, OUTSIDE_SPACE];
73570
73648
  var MISREAD = /^Text: (".+") reads (".*")\.?$/;
73571
73649
  var MISSING = /^Text: (".+") is missing\.?$/;
@@ -73601,6 +73679,10 @@ function correctionOf(finding, band) {
73601
73679
  if (extra) {
73602
73680
  return `Erase these words wherever they appear, leaving the surface they are on blank or with plain lines: ${extra[1]}.`;
73603
73681
  }
73682
+ const prices = what.match(SCENE_PRICES);
73683
+ if (prices) {
73684
+ return `Blur these figures until no digit can be read, keeping the document or screen they are on as it is: ${prices[1]}.`;
73685
+ }
73604
73686
  const misread2 = what.match(MISREAD);
73605
73687
  if (misread2) return `Re-letter ${misread2[1]} exactly as written, letter for letter (it now reads ${misread2[2]}).`;
73606
73688
  const missing = what.match(MISSING);
@@ -73636,7 +73718,12 @@ function autoFixStep(canvas, review, opts = {}) {
73636
73718
  if (ids2.length > 0) return { kind: "relayout", ids: ids2, edits, ...placing };
73637
73719
  return extend.length > 0 ? { kind: "extend", sizes: extend } : null;
73638
73720
  };
73639
- const pictureFindings = opts.previous === "relayout" || opts.previous === "extend" ? [] : rest.filter((f) => !at(f));
73721
+ const onSeveralSizes = (f) => {
73722
+ const what = withoutSize(f.what);
73723
+ return new Set(rest.filter((g) => withoutSize(g.what) === what).flatMap((g) => sizesOf(g.what))).size >= 2;
73724
+ };
73725
+ const settledPicture = opts.previous === "relayout" || opts.previous === "extend";
73726
+ const pictureFindings = rest.filter((f) => !at(f) && (!settledPicture || onSeveralSizes(f)));
73640
73727
  const onPicture = unique(pictureFindings.map((f) => withoutSize(f.what)));
73641
73728
  if (onPicture.length === 0) return sizesOnly();
73642
73729
  const every = unique([...extend, ...deliveredSizes(canvas)]);
@@ -73911,7 +73998,7 @@ var CHOSEN = [
73911
73998
  (id, out) => id === "ad" && out.role === void 0
73912
73999
  ];
73913
74000
  function urlOf(out) {
73914
- const url = out.images?.find((o) => typeof o?.url === "string")?.url ?? out.image?.url;
74001
+ const url = out.images?.find((o) => typeof o?.url === "string")?.url ?? out.image?.url ?? out.asset?.url;
73915
74002
  return typeof url === "string" ? url : null;
73916
74003
  }
73917
74004
  function chosenUrl(byNode) {
@@ -74155,7 +74242,7 @@ function turnRenderRefusal(ledger, turnId, slug) {
74155
74242
  return {
74156
74243
  code: "CONFLICT",
74157
74244
  message: `This ad was rendered twice in this turn, its automatic corrections included, and is still not ready.`,
74158
- fix: "Show the best version and say in one plain sentence what is left, in the person's words; they decide what changes next."
74245
+ fix: `Show the best version as a version ("Version 3"), never a file path or a run id, and say in one plain sentence what is left, in the person's words. End there: no question and no options; their next message says what changes.`
74159
74246
  };
74160
74247
  }
74161
74248
  return null;
@@ -75369,6 +75456,7 @@ async function canvasFileFor(opts) {
75369
75456
  throw e;
75370
75457
  }
75371
75458
  }
75459
+ var canvasHeld = false;
75372
75460
  async function executeCanvasRun(opts) {
75373
75461
  const { canvasPath: filePath, adoptRunId } = await canvasFileFor(opts);
75374
75462
  const canvasRoot = canvasOutputsRoot(filePath);
@@ -75608,7 +75696,8 @@ ${describeRewrites(prepared.rewrites)}
75608
75696
  );
75609
75697
  process.exit(2);
75610
75698
  }
75611
- for (const w of [...validation.warnings ?? [], ...await outputRatioWarningsFor(parsed, filePath)]) {
75699
+ const advisories = (validation.warnings ?? []).filter((w) => w.code !== "ORPHAN_NODE");
75700
+ for (const w of [...advisories, ...await outputRatioWarningsFor(parsed, filePath)]) {
75612
75701
  process.stdout.write(`[warn] ${w.code}: ${w.message}
75613
75702
  `);
75614
75703
  }
@@ -75625,14 +75714,15 @@ ${describeRewrites(prepared.rewrites)}
75625
75714
  sourceSha,
75626
75715
  remote: client && creativeSlug && sourceSha ? { creativeSlug, sourceSha, getActiveRun: (slug, sha) => client.getActiveRun(slug, sha) } : void 0
75627
75716
  });
75628
- if (rendering) {
75717
+ const holder = rendering ?? await canvasBusyElsewhere(outputsDir, filePath);
75718
+ if (holder) {
75629
75719
  process.stderr.write(
75630
75720
  `${JSON.stringify(
75631
75721
  {
75632
75722
  ok: false,
75633
75723
  error: {
75634
75724
  code: "CONFLICT",
75635
- message: `This canvas is already rendering in another process (run ${rendering.runId}, pid ${rendering.pid}).`,
75725
+ message: `This canvas is already rendering in another process (run ${holder.runId}, pid ${holder.pid}).`,
75636
75726
  fix: "Wait for that render to finish and use its result instead of starting a second one: two renders of one canvas share a run and overwrite each other. If you started it in the background, read its output when it completes."
75637
75727
  }
75638
75728
  },
@@ -75695,6 +75785,11 @@ ${describeRewrites(prepared.rewrites)}
75695
75785
  });
75696
75786
  }
75697
75787
  const inFlight = await markRunInFlight(outputsDir, filePath, runId, sourceSha);
75788
+ if (!canvasHeld) {
75789
+ const held = await markCanvasBusy(outputsDir, filePath, runId);
75790
+ canvasHeld = true;
75791
+ process.once("exit", held.release);
75792
+ }
75698
75793
  const clearMarker = () => {
75699
75794
  inFlight.stop();
75700
75795
  return clearRunMarker(outputsDir, filePath, inFlight.token);
@@ -81359,22 +81454,32 @@ async function readViewer(root, adSet) {
81359
81454
  }
81360
81455
  var MARKET_ADS = 40;
81361
81456
  var MARKET_FALLBACK_ADS = 15;
81457
+ var MARKET_SEARCH_MS = 12e3;
81458
+ var MARKET_QUERY_WORDS = 6;
81362
81459
  function marketQuery(companyDoc) {
81363
- const prose = companyDoc.split("\n").filter((line) => line.trim() && !line.startsWith("#")).join(" ").replace(/\s+/g, " ");
81364
- return prose.slice(0, 240);
81460
+ const first = companyDoc.split("\n").find((line) => line.trim() && !line.trim().startsWith("#")) ?? "";
81461
+ const what = first.replace(/^\s*[-*]\s+/, "").replace(/^\*\*[^*]+\*\*\s*[—–:-]?\s*/, "").split(/\s+for\s+|[,.;:(]|\s[—–-]\s/)[0];
81462
+ return (what ?? "").trim().split(/\s+/).filter(Boolean).slice(0, MARKET_QUERY_WORDS).join(" ");
81365
81463
  }
81366
81464
  async function readMarket(root) {
81367
81465
  const query = marketQuery(await readFile36(path44.join(root, "COMPANY.md"), "utf8").catch(() => ""));
81368
81466
  if (!query) return null;
81369
- const search = (body) => apiPost("/api/ad-library/winners/search", body);
81370
- const followed = await search(
81371
- buildSearchBody({ scope: "followed", platform: "meta", limit: MARKET_ADS, maxPerAdvertiser: 8 })
81372
- );
81373
- if (followed.results.length > 0) return marketMap(followed.results, "followed");
81374
- const all = await search(
81375
- buildSearchBody({ query, scope: "all", platform: "meta", limit: MARKET_FALLBACK_ADS, maxPerAdvertiser: 3 })
81376
- );
81377
- return all.results.length > 0 ? marketMap(all.results, "library") : null;
81467
+ const search = (body) => apiPost("/api/ad-library/winners/search", body, {
81468
+ timeoutMs: MARKET_SEARCH_MS
81469
+ }).catch((err) => {
81470
+ if (err instanceof ApiError && err.code === "TIMEOUT") {
81471
+ throw new Error("the ad library did not answer in time, so competitors' claims are left out");
81472
+ }
81473
+ throw err;
81474
+ });
81475
+ const [followed, all] = await Promise.allSettled([
81476
+ search(buildSearchBody({ scope: "followed", platform: "meta", limit: MARKET_ADS, maxPerAdvertiser: 8 })),
81477
+ search(buildSearchBody({ query, scope: "all", platform: "meta", limit: MARKET_FALLBACK_ADS, maxPerAdvertiser: 3 }))
81478
+ ]);
81479
+ if (followed.status === "fulfilled" && followed.value.results.length > 0)
81480
+ return marketMap(followed.value.results, "followed");
81481
+ if (all.status === "rejected") throw all.reason;
81482
+ return all.value.results.length > 0 ? marketMap(all.value.results, "library") : null;
81378
81483
  }
81379
81484
  async function metaAccountId(given) {
81380
81485
  if (given) return given.startsWith("act_") ? given : `act_${given}`;
@@ -81464,7 +81569,7 @@ function briefHints(parts) {
81464
81569
  hints2.push(`Ideas ready to build: ${parts.buildNext.join(", ")} (\`baker creatives make <idea>\`).`);
81465
81570
  } else {
81466
81571
  hints2.push(
81467
- "Write 4-8 ideas in src/creatives/_ideas/<idea>.md (who, message, evidence, awareness, angle, format, job, why), then `baker creatives ideas`."
81572
+ `Write 4-8 ideas in src/creatives/_ideas/<idea>.md (who, message, evidence, awareness, angle, format, job, why), then \`baker creatives ideas\`. awareness: ${AWARENESS_STAGES.join(" | ")}. angle: ${IDEA_ANGLES.join(", ")}. format: ${IDEA_FORMATS.join(", ")}.`
81468
81573
  );
81469
81574
  }
81470
81575
  return hints2;
@@ -82027,11 +82132,11 @@ function lastJsonObject(stdout) {
82027
82132
  }
82028
82133
  return null;
82029
82134
  }
82030
- async function baker(args8) {
82135
+ async function baker(args8, signal) {
82031
82136
  const entry = process.argv[1];
82032
82137
  const [file, argv] = entry ? [process.execPath, [...process.execArgv, entry, ...args8]] : ["baker", args8];
82033
82138
  try {
82034
- const { stdout } = await run2(file, argv, { maxBuffer: 32 * 1024 * 1024 });
82139
+ const { stdout } = await run2(file, argv, { maxBuffer: 32 * 1024 * 1024, ...signal ? { signal } : {} });
82035
82140
  return lastJsonObject(stdout) ?? { ok: false, error: { code: "INTERNAL_ERROR", message: "no answer" } };
82036
82141
  } catch (err) {
82037
82142
  const { stdout = "", stderr = "" } = err;
@@ -82073,7 +82178,8 @@ var candidateOf = (c) => ({
82073
82178
  async function pickReference(root, searches, deps = liveReferenceDeps(root), prepare) {
82074
82179
  const candidates = (await findCandidates(searches, deps)).slice(0, FITS_TRIED);
82075
82180
  const first = candidates[0];
82076
- const building = first && prepare ? prepare(candidateOf(first)) : void 0;
82181
+ const stop = new AbortController();
82182
+ const building = first && prepare ? prepare(candidateOf(first), stop.signal) : void 0;
82077
82183
  void building?.catch(() => void 0);
82078
82184
  const fits = await Promise.all(candidates.map((c) => deps.fit(asPlatform(c.platform), String(c.ad_id))));
82079
82185
  let unchecked = null;
@@ -82085,6 +82191,7 @@ async function pickReference(root, searches, deps = liveReferenceDeps(root), pre
82085
82191
  else checked += 1;
82086
82192
  if (!(fit.ok && fitsIdea(fit.fit))) continue;
82087
82193
  const layers = fit.fit.why_it_wins;
82194
+ if (index > 0) stop.abort();
82088
82195
  return {
82089
82196
  ...candidateOf(candidate),
82090
82197
  why: [layers.disguise, layers.trigger, layers.logic].filter(Boolean).join("; "),
@@ -82092,6 +82199,7 @@ async function pickReference(root, searches, deps = liveReferenceDeps(root), pre
82092
82199
  ...building ? index === 0 ? { prepared: building } : { discarded: building } : {}
82093
82200
  };
82094
82201
  }
82202
+ stop.abort();
82095
82203
  return { ...noReference(candidates.length, checked, unchecked), ...building ? { discarded: building } : {} };
82096
82204
  }
82097
82205
  function noReference(found, checked, unchecked) {
@@ -82108,6 +82216,18 @@ function noReference(found, checked, unchecked) {
82108
82216
  fix: "Change the idea's format to another container (`baker creatives ideas --full` lists them) and run make again, or pass --reference <adId> for one you chose. Do not stop the batch: build the other ideas."
82109
82217
  };
82110
82218
  }
82219
+ function alreadyWritten(promptJson, slug) {
82220
+ if (promptJson === null || promptJson === void 0) return null;
82221
+ try {
82222
+ if (unwrittenProblems(readAdSpec(promptJson)).length > 0) return null;
82223
+ } catch {
82224
+ }
82225
+ return {
82226
+ code: "ALREADY_MADE",
82227
+ message: `"${slug}" is already made and its ad is written: building it again would replace that ad with placeholders.`,
82228
+ fix: `To change this ad, edit src/creatives/${slug}/prompt.json and run \`baker canvas run\` on it. For another ad of the same idea, run make again with --slug <new-slug>.`
82229
+ };
82230
+ }
82111
82231
  var flag = (value2) => typeof value2 === "string" && value2 ? value2 : void 0;
82112
82232
  async function loadIdea(root, ideaId) {
82113
82233
  const { ideas, unreadable } = await readIdeas(root);
@@ -82160,20 +82280,23 @@ async function scaffoldIdea(root, idea, args8, slug, temperature) {
82160
82280
  reference: null
82161
82281
  };
82162
82282
  }
82163
- const onReference = (c) => baker([
82164
- "canvas",
82165
- "scaffold-static-ad",
82166
- "--source-ad-id",
82167
- c.adId,
82168
- "--source-platform",
82169
- c.platform,
82170
- ...c.advertiserId ? ["--source-advertiser-id", c.advertiserId] : [],
82171
- "--slug",
82172
- slug,
82173
- "--title",
82174
- idea.message.slice(0, 120),
82175
- ...viewerFlags
82176
- ]);
82283
+ const onReference = (c, signal) => baker(
82284
+ [
82285
+ "canvas",
82286
+ "scaffold-static-ad",
82287
+ "--source-ad-id",
82288
+ c.adId,
82289
+ "--source-platform",
82290
+ c.platform,
82291
+ ...c.advertiserId ? ["--source-advertiser-id", c.advertiserId] : [],
82292
+ "--slug",
82293
+ slug,
82294
+ "--title",
82295
+ idea.message.slice(0, 120),
82296
+ ...viewerFlags
82297
+ ],
82298
+ signal
82299
+ );
82177
82300
  const dir = path46.join(root, "src", "creatives", slug);
82178
82301
  const fresh = !await stat7(dir).catch(() => null);
82179
82302
  const reference = args8.reference ? { adId: args8.reference, platform: asPlatform(args8.platform), why: "chosen by hand", tried: 0 } : args8.fromIdea ? { code: "FROM_IDEA", message: "Asked to start from the idea." } : await pickReference(
@@ -82302,6 +82425,18 @@ var makeCommand = defineCommand159({
82302
82425
  const opts = makeArgsOf(args8);
82303
82426
  const { idea, check } = await loadIdea(root, String(args8.idea));
82304
82427
  const slug = opts.slug ?? idea.id;
82428
+ const written = await readFile38(path46.join(root, "src", "creatives", slug, "prompt.json"), "utf8").then(
82429
+ (text3) => {
82430
+ try {
82431
+ return JSON.parse(text3);
82432
+ } catch {
82433
+ return {};
82434
+ }
82435
+ },
82436
+ (err) => err.code === "ENOENT" ? null : {}
82437
+ );
82438
+ const refused = alreadyWritten(written, slug);
82439
+ if (refused) fail6(refused);
82305
82440
  const temperature = await viewerTemperature(root, opts.adset, idea.awareness);
82306
82441
  const { scaffold, reference, fromIdea } = await scaffoldIdea(root, idea, opts, slug, temperature);
82307
82442
  if (scaffold.ok !== true) fail6(scaffoldFailure(scaffold));