@koda-sl/baker-cli 0.440.0-dev.dab0aaba9 → 0.443.0-dev.edb8d8767
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/cli.js +175 -119
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5832,7 +5832,7 @@ offered, or, for a viewer who does not know the brand, when the words say they a
|
|
|
5832
5832
|
cart, a trial, a donation, a registration); who it is for is reported but never fails an ad alone. The viewer is the `viewer` recorded in
|
|
5833
5833
|
`_definition.md`, else the one its `targetAdSet` reaches (as `baker ads meta adsets --audience` reads it), else
|
|
5834
5834
|
the persona and `awareness` from the Knowledge, else a cold stranger. Not understood → exit 2 with
|
|
5835
|
-
`viewer_check_failed` and a `fix`
|
|
5835
|
+
`viewer_check_failed` and a `fix` that starts with the one rewrite answering a missed offer (`Do this: make the biggest words say "<message>"`) and names what the viewer missed. After two failures of one ad in a chat turn the check no longer holds it back: the command answers `ok: true` with `understood: false` and what was missed, and `baker canvas run` renders it, printing the miss. The casting check is asked alongside the viewer. Answers are cached per copy, scene and viewer.
|
|
5836
5836
|
`baker canvas run` makes the same check on a static ad before it spends (skipped for `--fix`, an autofix round, `--use-variant`, a re-layout and resumed runs, never for an edit of the words;
|
|
5837
5837
|
`--skip-viewer-check` renders without it; a model outage lets the render through with a warning). `--full` adds
|
|
5838
5838
|
the viewer's whole answer and how the viewer was chosen.
|
|
@@ -5879,7 +5879,7 @@ A completed render the review calls ready (`verdict` `ready` or `ready_with_note
|
|
|
5879
5879
|
|
|
5880
5880
|
**Resuming an interrupted run.** A long `baker canvas run` (multi-clip video) that is killed mid-render — session end, sandbox pause — leaves a marker under the outputs dir. The next `baker canvas run` of the same canvas automatically **resumes** that run: it reuses the run id so still-running billed jobs re-attach instead of being abandoned and re-billed, and completed nodes come from the cache. A live render keeps its marker's heartbeat fresh, so a second `run` of the same canvas is refused while it is alive (checked with `kill(pid, 0)` plus the heartbeat, on Linux and macOS alike). Resume also works from a **different workspace or a fresh sandbox**: when no local marker survives, the run history is consulted for a resumable run of the same source (canvas, `prompt.json`, scenes, compositions and formats), and it is adopted when the backend says it may be: interrupted, stopped at a checkpoint, or a `running` row whose keepalive is older than the backend's staleness threshold. Ctrl-C / SIGTERM aborts gracefully — no new nodes dispatch, a resumable snapshot is flushed, and the marker survives. A clean completion (or a handled failure) clears the marker, so a normal re-run starts a fresh generation. Force a new run with `--fresh`, or pin a specific run with `--run-id <id>` (also the escape hatch to adopt a run that is reported as concurrently live). Independent same-layer nodes (e.g. video clips) fan out in parallel up to `--parallel`/`--concurrency` (default 8; env `BAKER_CANVAS_CONCURRENCY`).
|
|
5881
5881
|
|
|
5882
|
-
**Static ads: the run corrects a blocking defect itself.** When the review of a static ad finds a blocking defect, `baker canvas run` makes up to 2 more renders (`AUTO_FIX_ROUNDS` in `static-autofix.ts`) before it reports, correcting each defect at the layer that causes it: words or a logo where an interface covers them (Reels, Stories) or the frame cuts them are **placed and extended** (`withExtendedSizes`): a `static_place` node reads where everything sits and shrinks the ad (to 60% at most) into the size's safe area, and a `static_extend_merge` node has the ad's own model paint only the margins that leaves and lays the ad's own pixels back over the painting, so the model is never asked to move anything (a size already inside its area is passed on with nothing painted or billed, and one whose words sit too far out for a shrink to clear is laid out again instead); a defect the spec asked for (a second subject, stars with no named source, a concept that does not read) and any defect of a native screen corrects the spec first (`static-spec-repair.ts`: a model rewrites `prompt.json` for those findings alone, every line of the copy kept word for word), then draws the ad again from it (a native screen redraws free); a picture wrong as a whole or in three places or more is made again from its brief, told what the last one got wrong, but only in a round with another after it unless the brief caused the defect; anything else on the picture is a `--fix` of the chosen picture for its own findings (`correctionOf`: the words to erase, the line to re-letter or add), naming what the last edit was already asked for, while each re-laid size follows as an edit of its own last layout (`withSizesFollowingFix`), keeping what an earlier round fixed in it; anything else on a re-laid size alone edits just that size from its own last layout (`withRelayEdit`). A size a round did not touch (its file the same byte for byte) keeps the round before's verdict (`carryUnchanged`)
|
|
5882
|
+
**Static ads: the run corrects a blocking defect itself.** When the review of a static ad finds a blocking defect, `baker canvas run` makes up to 2 more renders (`AUTO_FIX_ROUNDS` in `static-autofix.ts`) before it reports, correcting each defect at the layer that causes it: words or a logo where an interface covers them (Reels, Stories) or the frame cuts them are **placed and extended** (`withExtendedSizes`): a `static_place` node reads where everything sits and shrinks the ad (to 60% at most) into the size's safe area, and a `static_extend_merge` node has the ad's own model paint only the margins that leaves and lays the ad's own pixels back over the painting, so the model is never asked to move anything (a size already inside its area is passed on with nothing painted or billed, and one whose words sit too far out for a shrink to clear is laid out again instead); a defect the spec asked for (a second subject, stars with no named source, a concept that does not read) and any defect of a native screen corrects the spec first (`static-spec-repair.ts`: a model rewrites `prompt.json` for those findings alone, every line of the copy kept word for word), then draws the ad again from it (a native screen redraws free); a picture wrong as a whole or in three places or more is made again from its brief, told what the last one got wrong, but only in a round with another after it unless the brief caused the defect; anything else on the picture is a `--fix` of the chosen picture for its own findings (`correctionOf`: the words to erase, the line to re-letter or add), naming what the last edit was already asked for, while each re-laid size the review flagged follows as an edit of its own last layout (`withSizesFollowingFix`), keeping what an earlier round fixed in it, and a size it found nothing blocking on is delivered as it was (`keep`, nothing in it made again or placed); anything else on a re-laid size alone edits just that size from its own last layout (`withRelayEdit`). A size a round did not touch (its file the same byte for byte) keeps the round before's verdict (`carryUnchanged`) and is not read again: the review reads only the sizes the round changed. Each round is one render (a fix, or one request per re-laid size) and prints `[autofix] round N of 2: …` on stdout; the final JSON's hints start with "Corrected automatically before this result, …" and its review is of the corrected version. Blocking findings left after the rounds keep the hints below. A round that would only repeat the correction that just left its defect in place (a chosen option, `--use-variant`, is only ever corrected) is not run: that version is reported as it is. `--no-autofix` reports after the first render.
|
|
5883
5883
|
|
|
5884
5884
|
**Static ads: correct a version, or use another of its variants.** Correct a finished version with `--fix "<what to change>"`: one render takes that version (the last one, or `--from <run_id>`) as its first reference and changes only what you said, keeping the picture, the layout and every other word; it replaces the variants and the judge, and every size is made again from the corrected picture. **A `prompt.json` changed only in its words, their positions or its colours** since the version shown is made the same way, as an edit of that version (`[edit]` in the output, the instruction read off the change); three new options are asked for only with `--regenerate static_variants`. A `--fix` whose defect is still blocking afterwards is recorded, and asking for it again with the same words exits 2 with `no_progress`. When every blocking finding is on re-laid-out sizes, the first hint says to lay out those sizes again (`--regenerate adapt_<size>`) before any `--fix`. Every static run's hints say what it cost in credits and what for. There is no render limit per creative. Use another variant with `--use-variant <sha256>` (`--from <run_id>`, default the last render): when the person chose another of a version's variants over the judge's pick, every size is made again from that picture, and the only requests re-lay it out for each other size; `--regenerate adapt_9x16` lays out one size again from the same chosen picture when only that size came out wrong (after a `--fix` or `--use-variant`, from that corrected or chosen picture, so the size never brings back the uncorrected one; `--from <run_id>` names another version); `baker creatives feedback --slug <slug>` prints the choice (`variantChoice`) and the exact command.
|
|
5885
5885
|
|
package/dist/cli.js
CHANGED
|
@@ -73631,11 +73631,18 @@ function autoFixStep(canvas, review, opts = {}) {
|
|
|
73631
73631
|
pictureFindings.filter((f) => !BRIEF.test(withoutSize(f.what))).flatMap((f) => correctionOf(f.what) ?? [])
|
|
73632
73632
|
);
|
|
73633
73633
|
if (changes.length === 0) return sizesOnly();
|
|
73634
|
+
const flagged = new Set(blocking.flatMap((f) => sizesOf(f.what)));
|
|
73635
|
+
const keep = relaid.map((r) => r.format).filter((format) => !flagged.has(format));
|
|
73636
|
+
const remade = every.filter((size) => !keep.includes(size));
|
|
73637
|
+
const kept = {
|
|
73638
|
+
...keep.length > 0 ? { keep } : {},
|
|
73639
|
+
...remade.length > 0 ? { extend: remade } : {}
|
|
73640
|
+
};
|
|
73634
73641
|
const asked = `Change only this: ${changes.join(" ")}`;
|
|
73635
73642
|
const again = onPicture.filter((w) => persisting.has(w)).map((w) => w.replace(CATEGORY, "").replace(/\.$/, ""));
|
|
73636
73643
|
const instruction = again.length > 0 ? `${asked} The last edit was already asked for this and Image 1 still shows it, so make it clearly: ${again.join("; ")}.` : asked;
|
|
73637
73644
|
if (repeatsFailedFix(opts.lastFix, asked) || repeatsFailedFix(opts.lastFix, instruction)) return null;
|
|
73638
|
-
return { kind: "fix", instruction, ...Object.keys(edits).length > 0 ? { relay: edits } : {}, ...
|
|
73645
|
+
return { kind: "fix", instruction, ...Object.keys(edits).length > 0 ? { relay: edits } : {}, ...kept };
|
|
73639
73646
|
}
|
|
73640
73647
|
var unique = (lines) => [...new Set(lines)];
|
|
73641
73648
|
function deliveredSizes(canvas) {
|
|
@@ -73936,19 +73943,19 @@ async function variantImage(canvasRoot, runId, sha2562, fetchRecord = fetchRunRe
|
|
|
73936
73943
|
}
|
|
73937
73944
|
|
|
73938
73945
|
// src/commands/canvas/static-run-review.ts
|
|
73939
|
-
import { readFile as
|
|
73946
|
+
import { readFile as readFile24 } from "fs/promises";
|
|
73940
73947
|
|
|
73941
73948
|
// src/commands/canvas/glance-check.ts
|
|
73942
73949
|
import sharp2 from "sharp";
|
|
73943
|
-
import { z as
|
|
73950
|
+
import { z as z66 } from "zod";
|
|
73944
73951
|
|
|
73945
73952
|
// src/commands/canvas/viewer-check.ts
|
|
73946
73953
|
import { createHash as createHash4 } from "crypto";
|
|
73947
|
-
import { mkdir as
|
|
73948
|
-
import
|
|
73954
|
+
import { mkdir as mkdir9, readdir as readdir12, readFile as readFile23, stat as stat4, writeFile as writeFile14 } from "fs/promises";
|
|
73955
|
+
import path29 from "path";
|
|
73949
73956
|
import { defineCommand as defineCommand138 } from "citty";
|
|
73950
73957
|
import { parse as parseYaml3 } from "yaml";
|
|
73951
|
-
import { z as
|
|
73958
|
+
import { z as z65 } from "zod";
|
|
73952
73959
|
|
|
73953
73960
|
// src/commands/canvas/unbilled-spend.ts
|
|
73954
73961
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
@@ -74082,17 +74089,76 @@ function castingCheckFailure(answer, slug) {
|
|
|
74082
74089
|
};
|
|
74083
74090
|
}
|
|
74084
74091
|
|
|
74092
|
+
// src/commands/canvas/turn-renders.ts
|
|
74093
|
+
import { mkdir as mkdir8, readFile as readFile22, writeFile as writeFile13 } from "fs/promises";
|
|
74094
|
+
import path28 from "path";
|
|
74095
|
+
import { z as z64 } from "zod";
|
|
74096
|
+
var RUNS_PER_TURN = 2;
|
|
74097
|
+
var VIEWER_FAILS_PER_TURN = 2;
|
|
74098
|
+
var LedgerSchema = z64.object({
|
|
74099
|
+
turnId: z64.string(),
|
|
74100
|
+
creatives: z64.record(
|
|
74101
|
+
z64.string(),
|
|
74102
|
+
z64.object({ runs: z64.number().default(0), verdict: z64.string().optional(), viewerFails: z64.number().optional() })
|
|
74103
|
+
)
|
|
74104
|
+
});
|
|
74105
|
+
function turnRenderRefusal(ledger, turnId, slug) {
|
|
74106
|
+
const entry = turnId !== void 0 && ledger?.turnId === turnId ? ledger.creatives[slug] : void 0;
|
|
74107
|
+
if (!entry) return null;
|
|
74108
|
+
if (entry.verdict === "ready" || entry.verdict === "ready_with_notes") {
|
|
74109
|
+
return {
|
|
74110
|
+
code: "CONFLICT",
|
|
74111
|
+
message: `This ad is already ready in this turn (${entry.verdict}): its notes are not defects.`,
|
|
74112
|
+
fix: "Show it now. A change the person asks for comes with their next message."
|
|
74113
|
+
};
|
|
74114
|
+
}
|
|
74115
|
+
if (entry.runs >= RUNS_PER_TURN) {
|
|
74116
|
+
return {
|
|
74117
|
+
code: "CONFLICT",
|
|
74118
|
+
message: `This ad was rendered twice in this turn, its automatic corrections included, and is still not ready.`,
|
|
74119
|
+
fix: "Show the best version and say in one plain sentence what is left, in the person's words; they decide what changes next."
|
|
74120
|
+
};
|
|
74121
|
+
}
|
|
74122
|
+
return null;
|
|
74123
|
+
}
|
|
74124
|
+
function recordTurnRender(ledger, turnId, slug, verdict2) {
|
|
74125
|
+
const creatives = ledger?.turnId === turnId ? ledger.creatives : {};
|
|
74126
|
+
return { turnId, creatives: { ...creatives, [slug]: { ...creatives[slug], runs: (creatives[slug]?.runs ?? 0) + 1, verdict: verdict2 } } };
|
|
74127
|
+
}
|
|
74128
|
+
function recordViewerFail(ledger, turnId, slug) {
|
|
74129
|
+
const creatives = ledger?.turnId === turnId ? ledger.creatives : {};
|
|
74130
|
+
const entry = creatives[slug] ?? { runs: 0 };
|
|
74131
|
+
return { turnId, creatives: { ...creatives, [slug]: { ...entry, viewerFails: (entry.viewerFails ?? 0) + 1 } } };
|
|
74132
|
+
}
|
|
74133
|
+
function viewerChecksSpent(ledger, turnId, slug) {
|
|
74134
|
+
const entry = turnId !== void 0 && ledger?.turnId === turnId ? ledger.creatives[slug] : void 0;
|
|
74135
|
+
return (entry?.viewerFails ?? 0) >= VIEWER_FAILS_PER_TURN;
|
|
74136
|
+
}
|
|
74137
|
+
var ledgerPath = (cacheDir) => path28.join(cacheDir, "turn-renders.json");
|
|
74138
|
+
async function readTurnRenders(cacheDir) {
|
|
74139
|
+
try {
|
|
74140
|
+
const parsed = LedgerSchema.safeParse(JSON.parse(await readFile22(ledgerPath(cacheDir), "utf8")));
|
|
74141
|
+
return parsed.success ? parsed.data : void 0;
|
|
74142
|
+
} catch {
|
|
74143
|
+
return void 0;
|
|
74144
|
+
}
|
|
74145
|
+
}
|
|
74146
|
+
async function writeTurnRenders(cacheDir, ledger) {
|
|
74147
|
+
await mkdir8(cacheDir, { recursive: true });
|
|
74148
|
+
await writeFile13(ledgerPath(cacheDir), JSON.stringify(ledger), "utf8");
|
|
74149
|
+
}
|
|
74150
|
+
|
|
74085
74151
|
// src/commands/canvas/viewer-check.ts
|
|
74086
74152
|
var VIEWER_QUESTIONS = ["offer", "for_whom", "next_step"];
|
|
74087
74153
|
var VIEWER_CLARITY = ["clear", "guess", "unclear"];
|
|
74088
|
-
var ViewerReply =
|
|
74089
|
-
var ViewerAnswerSchema =
|
|
74154
|
+
var ViewerReply = z65.object({ answer: z65.string().default(""), clarity: z65.enum(VIEWER_CLARITY) });
|
|
74155
|
+
var ViewerAnswerSchema = z65.object({
|
|
74090
74156
|
offer: ViewerReply,
|
|
74091
74157
|
for_whom: ViewerReply,
|
|
74092
74158
|
next_step: ViewerReply,
|
|
74093
74159
|
/** The words that talk to the viewer as if they had already done something with the brand; "" when none. */
|
|
74094
|
-
presumes:
|
|
74095
|
-
missing:
|
|
74160
|
+
presumes: z65.string().default(""),
|
|
74161
|
+
missing: z65.string().default("")
|
|
74096
74162
|
});
|
|
74097
74163
|
var VIEWER_REQUIRED_CLARITY = {
|
|
74098
74164
|
offer: "clear",
|
|
@@ -74199,14 +74265,14 @@ function viewerCheckCacheKey(input, model) {
|
|
|
74199
74265
|
}
|
|
74200
74266
|
async function cachedAnswer(file) {
|
|
74201
74267
|
try {
|
|
74202
|
-
const parsed = ViewerAnswerSchema.safeParse(JSON.parse(await
|
|
74268
|
+
const parsed = ViewerAnswerSchema.safeParse(JSON.parse(await readFile23(file, "utf8")));
|
|
74203
74269
|
return parsed.success ? parsed.data : null;
|
|
74204
74270
|
} catch {
|
|
74205
74271
|
return null;
|
|
74206
74272
|
}
|
|
74207
74273
|
}
|
|
74208
74274
|
async function checkViewer(input, opts) {
|
|
74209
|
-
const file =
|
|
74275
|
+
const file = path29.join(opts.cacheDir, "viewer-check", `${viewerCheckCacheKey(input, opts.model)}.json`);
|
|
74210
74276
|
let answer = await cachedAnswer(file);
|
|
74211
74277
|
const cached = answer !== null;
|
|
74212
74278
|
const prompt = buildViewerPrompt(input);
|
|
@@ -74216,8 +74282,8 @@ async function checkViewer(input, opts) {
|
|
|
74216
74282
|
}
|
|
74217
74283
|
if (answer === null) return { status: "unavailable", input };
|
|
74218
74284
|
if (!cached) {
|
|
74219
|
-
await
|
|
74220
|
-
await
|
|
74285
|
+
await mkdir9(path29.dirname(file), { recursive: true });
|
|
74286
|
+
await writeFile14(file, `${JSON.stringify(answer, null, 2)}
|
|
74221
74287
|
`, "utf8");
|
|
74222
74288
|
}
|
|
74223
74289
|
const verdict2 = judgeViewerAnswer(answer, input.intended);
|
|
@@ -74248,7 +74314,10 @@ function viewerCheckFailure(outcome2, slug) {
|
|
|
74248
74314
|
const who = describeViewer(viewer);
|
|
74249
74315
|
const missing = outcome2.answer.missing.trim();
|
|
74250
74316
|
const unsaid = outcome2.verdict.missed.flatMap((m) => m.question === "presumes" ? [] : [QUESTION_WORDS[m.question]]);
|
|
74317
|
+
const offer = outcome2.input.intended.offer?.trim();
|
|
74318
|
+
const rewrite = offer && outcome2.verdict.missed.some((m) => m.question === "offer") ? [`Do this: make the biggest words say "${offer}", plainly, in the ad's language; keep the rest.`] : [];
|
|
74251
74319
|
const fix = [
|
|
74320
|
+
...rewrite,
|
|
74252
74321
|
...unsaid.length > 0 ? [`Seen by ${who}, the words and picture alone do not say ${unsaid.join(" or ")}.`] : [`Seen by ${who}:`],
|
|
74253
74322
|
...outcome2.verdict.missed.map(missLine),
|
|
74254
74323
|
...missing ? [`They needed: ${missing}`] : [],
|
|
@@ -74267,17 +74336,17 @@ function viewerCheckFailure(outcome2, slug) {
|
|
|
74267
74336
|
}
|
|
74268
74337
|
};
|
|
74269
74338
|
}
|
|
74270
|
-
var RecordedViewer =
|
|
74271
|
-
temperature:
|
|
74272
|
-
knows:
|
|
74273
|
-
placements:
|
|
74274
|
-
source:
|
|
74275
|
-
reasons:
|
|
74276
|
-
market:
|
|
74277
|
-
country:
|
|
74278
|
-
language:
|
|
74279
|
-
currency:
|
|
74280
|
-
address:
|
|
74339
|
+
var RecordedViewer = z65.object({
|
|
74340
|
+
temperature: z65.enum(VIEWER_TEMPERATURE_VALUES),
|
|
74341
|
+
knows: z65.array(z65.enum(VIEWER_KNOWS_VALUES)).default([]),
|
|
74342
|
+
placements: z65.array(z65.string().min(1)).default(["automatic"]),
|
|
74343
|
+
source: z65.enum(VIEWER_SOURCE_VALUES),
|
|
74344
|
+
reasons: z65.array(z65.string()).default([]),
|
|
74345
|
+
market: z65.object({
|
|
74346
|
+
country: z65.string().min(2),
|
|
74347
|
+
language: z65.string().optional(),
|
|
74348
|
+
currency: z65.string().optional(),
|
|
74349
|
+
address: z65.string().optional()
|
|
74281
74350
|
}).optional()
|
|
74282
74351
|
});
|
|
74283
74352
|
var nameKey = (text3) => wordsOf(text3).join(" ");
|
|
@@ -74336,12 +74405,12 @@ async function resolveViewerAlone(sources, lookups) {
|
|
|
74336
74405
|
notes
|
|
74337
74406
|
};
|
|
74338
74407
|
}
|
|
74339
|
-
var Frontmatter =
|
|
74340
|
-
viewer:
|
|
74341
|
-
targetAdSet:
|
|
74342
|
-
persona:
|
|
74343
|
-
awareness:
|
|
74344
|
-
angle:
|
|
74408
|
+
var Frontmatter = z65.object({
|
|
74409
|
+
viewer: z65.unknown().optional(),
|
|
74410
|
+
targetAdSet: z65.union([z65.string(), z65.number()]).transform(String).optional(),
|
|
74411
|
+
persona: z65.string().optional(),
|
|
74412
|
+
awareness: z65.enum(AWARENESS_VALUES).optional().catch(void 0),
|
|
74413
|
+
angle: z65.string().optional()
|
|
74345
74414
|
});
|
|
74346
74415
|
function frontmatterOf2(raw) {
|
|
74347
74416
|
const block2 = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/)?.[1];
|
|
@@ -74354,8 +74423,8 @@ function frontmatterOf2(raw) {
|
|
|
74354
74423
|
}
|
|
74355
74424
|
}
|
|
74356
74425
|
async function readCreativeForViewerCheck(creativeDir) {
|
|
74357
|
-
const spec = readAdSpec(JSON.parse(await
|
|
74358
|
-
const definition = frontmatterOf2(await
|
|
74426
|
+
const spec = readAdSpec(JSON.parse(await readFile23(path29.join(creativeDir, "prompt.json"), "utf8")));
|
|
74427
|
+
const definition = frontmatterOf2(await readFile23(path29.join(creativeDir, "_definition.md"), "utf8").catch(() => ""));
|
|
74359
74428
|
const offer = spec.message?.trim() || definition.angle?.trim();
|
|
74360
74429
|
const persona = spec.viewer?.who.trim() || definition.persona?.trim();
|
|
74361
74430
|
const brand = spec.brand?.name?.trim();
|
|
@@ -74384,7 +74453,7 @@ function castPeople(spec) {
|
|
|
74384
74453
|
return people.length > 0 ? { people } : {};
|
|
74385
74454
|
}
|
|
74386
74455
|
async function readPersonas(workspaceRoot) {
|
|
74387
|
-
const dir =
|
|
74456
|
+
const dir = path29.join(workspaceRoot, "src", "content", "personas");
|
|
74388
74457
|
let files;
|
|
74389
74458
|
try {
|
|
74390
74459
|
files = (await readdir12(dir, { recursive: true })).filter((f) => f.endsWith(".md"));
|
|
@@ -74393,7 +74462,7 @@ async function readPersonas(workspaceRoot) {
|
|
|
74393
74462
|
}
|
|
74394
74463
|
return await Promise.all(
|
|
74395
74464
|
files.map(async (file) => {
|
|
74396
|
-
const raw = await
|
|
74465
|
+
const raw = await readFile23(path29.join(dir, file), "utf8");
|
|
74397
74466
|
const block2 = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/)?.[1];
|
|
74398
74467
|
let segment;
|
|
74399
74468
|
try {
|
|
@@ -74403,7 +74472,7 @@ async function readPersonas(workspaceRoot) {
|
|
|
74403
74472
|
}
|
|
74404
74473
|
const title = raw.match(/^#\s+(.+)$/m)?.[1]?.trim();
|
|
74405
74474
|
return {
|
|
74406
|
-
slug:
|
|
74475
|
+
slug: path29.basename(file, ".md"),
|
|
74407
74476
|
...title ? { title } : {},
|
|
74408
74477
|
...typeof segment === "string" && segment.trim() ? { segment: segment.trim() } : {}
|
|
74409
74478
|
};
|
|
@@ -74442,8 +74511,8 @@ function liveViewerLookups(workspaceRoot) {
|
|
|
74442
74511
|
return {
|
|
74443
74512
|
adSetViewer: metaAdSetViewer,
|
|
74444
74513
|
personas: () => readPersonas(workspaceRoot),
|
|
74445
|
-
companyMarket: async () => marketFromCompanyDoc(await
|
|
74446
|
-
company: async () => (await
|
|
74514
|
+
companyMarket: async () => marketFromCompanyDoc(await readFile23(path29.join(workspaceRoot, "COMPANY.md"), "utf8").catch(() => null)),
|
|
74515
|
+
company: async () => (await readFile23(path29.join(workspaceRoot, "COMPANY.md"), "utf8").catch(() => "")).slice(0, 800)
|
|
74447
74516
|
};
|
|
74448
74517
|
}
|
|
74449
74518
|
function gatewayViewerModel(model, onSpend) {
|
|
@@ -74473,16 +74542,7 @@ async function viewerCheckBeforeRender(opts) {
|
|
|
74473
74542
|
const who = describeViewer(resolved.viewer);
|
|
74474
74543
|
const lines = resolved.notes.map((note) => `[viewer] ${note}`);
|
|
74475
74544
|
if (opts.skip) return { viewer: resolved.viewer, lines: [...lines, `[viewer] not asked: ${SKIP_WORDS[opts.skip]}.`] };
|
|
74476
|
-
const
|
|
74477
|
-
if (outcome2.status === "unavailable") {
|
|
74478
|
-
lines.push(
|
|
74479
|
-
`[warn] viewer_check_unavailable: the viewer model did not answer, so this ad renders without the viewer check. Read the copy as ${who} before showing it, or run \`baker canvas viewer-check ${opts.slug}\` again.`
|
|
74480
|
-
);
|
|
74481
|
-
return { viewer: resolved.viewer, lines };
|
|
74482
|
-
}
|
|
74483
|
-
if (outcome2.status === "failed") return { viewer: resolved.viewer, lines, failure: viewerCheckFailure(outcome2, opts.slug) };
|
|
74484
|
-
lines.push(`[viewer] ${who} understood it: ${outcome2.answer.offer.answer}${outcome2.cached ? " (cached)" : ""}.`);
|
|
74485
|
-
const casting = await castingCheck(
|
|
74545
|
+
const casting = (async () => castingCheck(
|
|
74486
74546
|
{
|
|
74487
74547
|
people: creative.ad.people ?? [],
|
|
74488
74548
|
company: await opts.lookups.company?.().catch(() => "") ?? "",
|
|
@@ -74490,10 +74550,26 @@ async function viewerCheckBeforeRender(opts) {
|
|
|
74490
74550
|
...creative.ad.intended.persona ? { persona: creative.ad.intended.persona } : {}
|
|
74491
74551
|
},
|
|
74492
74552
|
{ cacheDir: opts.cacheDir, model: opts.model, ask: opts.ask }
|
|
74493
|
-
);
|
|
74494
|
-
|
|
74495
|
-
if (
|
|
74496
|
-
|
|
74553
|
+
))().catch(() => ({ status: "unavailable" }));
|
|
74554
|
+
const outcome2 = await checkViewer({ ...creative.ad, viewer: resolved.viewer }, opts);
|
|
74555
|
+
if (outcome2.status === "unavailable") {
|
|
74556
|
+
lines.push(
|
|
74557
|
+
`[warn] viewer_check_unavailable: the viewer model did not answer, so this ad renders without the viewer check. Read the copy as ${who} before showing it, or run \`baker canvas viewer-check ${opts.slug}\` again.`
|
|
74558
|
+
);
|
|
74559
|
+
} else if (outcome2.status === "failed" && !opts.spent) {
|
|
74560
|
+
return { viewer: resolved.viewer, lines, failure: viewerCheckFailure(outcome2, opts.slug) };
|
|
74561
|
+
} else if (outcome2.status === "failed") {
|
|
74562
|
+
lines.push(
|
|
74563
|
+
`[viewer] this ad failed the viewer check twice in this turn, so it renders as written. Tell the person what ${who} missed: ${outcome2.verdict.missed.map(missLine).join(" ")}`
|
|
74564
|
+
);
|
|
74565
|
+
} else {
|
|
74566
|
+
lines.push(`[viewer] ${who} understood it: ${outcome2.answer.offer.answer}${outcome2.cached ? " (cached)" : ""}.`);
|
|
74567
|
+
}
|
|
74568
|
+
if (outcome2.status === "unavailable") return { viewer: resolved.viewer, lines };
|
|
74569
|
+
const cast = await casting;
|
|
74570
|
+
if (cast.status === "failed") return { viewer: resolved.viewer, lines, failure: castingCheckFailure(cast.answer, opts.slug) };
|
|
74571
|
+
if (cast.status === "passed") lines.push(`[casting] the people cast pass as real customers${cast.cached ? " (cached)" : ""}.`);
|
|
74572
|
+
if (cast.status === "unavailable") {
|
|
74497
74573
|
lines.push("[warn] casting_check_unavailable: the model did not answer; check yourself that the people cast look like this company's real customers.");
|
|
74498
74574
|
}
|
|
74499
74575
|
return { viewer: resolved.viewer, lines };
|
|
@@ -74514,11 +74590,11 @@ var ARGS5 = {
|
|
|
74514
74590
|
};
|
|
74515
74591
|
registerSchema({ command: "canvas.viewer-check", description: DESCRIPTION2, args: ARGS5 });
|
|
74516
74592
|
async function creativeDirOf(arg, workspaceRoot) {
|
|
74517
|
-
const direct =
|
|
74593
|
+
const direct = path29.resolve(arg);
|
|
74518
74594
|
try {
|
|
74519
|
-
return (await stat4(direct)).isDirectory() ? direct :
|
|
74595
|
+
return (await stat4(direct)).isDirectory() ? direct : path29.dirname(direct);
|
|
74520
74596
|
} catch {
|
|
74521
|
-
return
|
|
74597
|
+
return path29.join(workspaceRoot, "src", "creatives", arg);
|
|
74522
74598
|
}
|
|
74523
74599
|
}
|
|
74524
74600
|
var viewerCheckCommand = defineCommand138({
|
|
@@ -74528,7 +74604,7 @@ var viewerCheckCommand = defineCommand138({
|
|
|
74528
74604
|
const cwd = process.cwd();
|
|
74529
74605
|
const workspaceRoot = findWorkspaceRoot(cwd) ?? cwd;
|
|
74530
74606
|
const creativeDir = await creativeDirOf(String(args8.slug), workspaceRoot);
|
|
74531
|
-
const slug =
|
|
74607
|
+
const slug = path29.basename(creativeDir);
|
|
74532
74608
|
let creative;
|
|
74533
74609
|
try {
|
|
74534
74610
|
creative = await readCreativeForViewerCheck(creativeDir);
|
|
@@ -74545,7 +74621,7 @@ var viewerCheckCommand = defineCommand138({
|
|
|
74545
74621
|
}
|
|
74546
74622
|
const resolved = await resolveViewer(creative.sources, liveViewerLookups(workspaceRoot));
|
|
74547
74623
|
const input = { ...creative.ad, viewer: resolved.viewer };
|
|
74548
|
-
const cacheDir =
|
|
74624
|
+
const cacheDir = path29.join(workspaceRoot, "canvas", ".cache");
|
|
74549
74625
|
const spent = [];
|
|
74550
74626
|
const outcome2 = await checkViewer(input, {
|
|
74551
74627
|
cacheDir,
|
|
@@ -74562,6 +74638,19 @@ var viewerCheckCommand = defineCommand138({
|
|
|
74562
74638
|
...resolved.notes.length > 0 ? { notes: resolved.notes } : {}
|
|
74563
74639
|
} : {};
|
|
74564
74640
|
if (outcome2.status === "failed") {
|
|
74641
|
+
const turnId = optionalTurnId();
|
|
74642
|
+
const ledger = await readTurnRenders(cacheDir);
|
|
74643
|
+
if (viewerChecksSpent(ledger, turnId, slug)) {
|
|
74644
|
+
writeJson({
|
|
74645
|
+
ok: true,
|
|
74646
|
+
data: { checked: true, understood: false, viewer: who, missed: outcome2.verdict.missed, ...detail },
|
|
74647
|
+
hints: [
|
|
74648
|
+
`This ad failed the viewer check twice in this turn: render it as written (baker canvas run --slug ${slug}) and tell the person what ${who} missed: ${outcome2.verdict.missed.map(missLine).join(" ")}`
|
|
74649
|
+
]
|
|
74650
|
+
});
|
|
74651
|
+
return;
|
|
74652
|
+
}
|
|
74653
|
+
if (turnId) await writeTurnRenders(cacheDir, recordViewerFail(ledger, turnId, slug));
|
|
74565
74654
|
const failure = viewerCheckFailure(outcome2, slug);
|
|
74566
74655
|
writeJson({ ...failure, error: { ...failure.error, ...detail } });
|
|
74567
74656
|
process.exit(2);
|
|
@@ -74604,9 +74693,9 @@ var GLANCE_REQUIRED = {
|
|
|
74604
74693
|
var GLANCE_WIDTH = 400;
|
|
74605
74694
|
var GlanceAnswerSchema = ViewerAnswerSchema.extend({
|
|
74606
74695
|
/** What kind of product or service they take it for (D4). */
|
|
74607
|
-
category:
|
|
74608
|
-
stopped:
|
|
74609
|
-
me:
|
|
74696
|
+
category: z66.object({ answer: z66.string().default(""), clarity: z66.enum(["clear", "guess", "unclear"]).default("unclear") }).default({ answer: "", clarity: "unclear" }),
|
|
74697
|
+
stopped: z66.string().default(""),
|
|
74698
|
+
me: z66.enum(["yes", "no", "unsure"]).default("unsure")
|
|
74610
74699
|
});
|
|
74611
74700
|
function wherePhrase2(placements) {
|
|
74612
74701
|
const fullScreen = placements.length === 0 || placements.some((p) => /automatic|stor|reel/i.test(p));
|
|
@@ -74776,7 +74865,7 @@ function runCostHint(stats, kind) {
|
|
|
74776
74865
|
async function textOf(ref2) {
|
|
74777
74866
|
const r = ref2;
|
|
74778
74867
|
try {
|
|
74779
|
-
if (typeof r?.path === "string") return await
|
|
74868
|
+
if (typeof r?.path === "string") return await readFile24(r.path, "utf8");
|
|
74780
74869
|
if (typeof r?.url === "string") {
|
|
74781
74870
|
const res = await fetch(r.url);
|
|
74782
74871
|
return res.ok ? await res.text() : null;
|
|
@@ -74843,7 +74932,7 @@ async function reviewStaticRun(opts) {
|
|
|
74843
74932
|
let native;
|
|
74844
74933
|
let palette = [];
|
|
74845
74934
|
try {
|
|
74846
|
-
const spec = readAdSpec(JSON.parse(await
|
|
74935
|
+
const spec = readAdSpec(JSON.parse(await readFile24(targets.promptPath, "utf8")));
|
|
74847
74936
|
copy = spec.copy.map((l) => l.text);
|
|
74848
74937
|
lines = spec.copy.map((l) => ({ text: l.text, lettering: l.lettering, ...l.role ? { role: l.role } : {} }));
|
|
74849
74938
|
if (spec.native) {
|
|
@@ -74877,8 +74966,10 @@ async function reviewStaticRun(opts) {
|
|
|
74877
74966
|
() => null
|
|
74878
74967
|
);
|
|
74879
74968
|
const comparing = opts.previous ? nearIdenticalFindings(targets.images, opts.previous).catch(() => []) : Promise.resolve([]);
|
|
74969
|
+
const untouched = opts.settled ? await samePictureSizes(targets.images, opts.settled.images) : [];
|
|
74970
|
+
const changed = targets.images.filter((i) => !untouched.includes(i.format));
|
|
74880
74971
|
const review = await reviewRenderedStatic({
|
|
74881
|
-
images: targets.images,
|
|
74972
|
+
images: changed.length > 0 ? changed : targets.images,
|
|
74882
74973
|
copy,
|
|
74883
74974
|
lines,
|
|
74884
74975
|
palette,
|
|
@@ -74901,7 +74992,7 @@ async function reviewStaticRun(opts) {
|
|
|
74901
74992
|
const glance = review === null ? null : glanced;
|
|
74902
74993
|
const unchanged = review === null ? [] : compared;
|
|
74903
74994
|
const fresh = review === null ? null : [...review, ...glance ?? [], ...unchanged];
|
|
74904
|
-
const reviewed = fresh === null || !opts.settled ? fresh : carryUnchanged(fresh, opts.settled.review,
|
|
74995
|
+
const reviewed = fresh === null || !opts.settled ? fresh : carryUnchanged(fresh, opts.settled.review, untouched);
|
|
74905
74996
|
const verdict2 = staticVerdict(reviewed);
|
|
74906
74997
|
const hints2 = verdictHints(verdict2);
|
|
74907
74998
|
const sizeHints = verdict2 === "needs_fixes" ? sizeFixHints(opts.parsed, reviewed) : [];
|
|
@@ -75024,12 +75115,12 @@ function variantHints(judge, prefilter) {
|
|
|
75024
75115
|
}
|
|
75025
75116
|
|
|
75026
75117
|
// src/commands/canvas/static-spec-repair.ts
|
|
75027
|
-
import { readFile as
|
|
75118
|
+
import { readFile as readFile25, writeFile as writeFile15 } from "fs/promises";
|
|
75028
75119
|
import sharp3 from "sharp";
|
|
75029
|
-
import { z as
|
|
75030
|
-
var Answer =
|
|
75031
|
-
changed:
|
|
75032
|
-
set:
|
|
75120
|
+
import { z as z67 } from "zod";
|
|
75121
|
+
var Answer = z67.object({
|
|
75122
|
+
changed: z67.string().min(1),
|
|
75123
|
+
set: z67.array(z67.object({ path: z67.string().min(1), value: z67.unknown() }))
|
|
75033
75124
|
});
|
|
75034
75125
|
function withValueAt(root, path68, value2) {
|
|
75035
75126
|
const [key, ...rest] = path68;
|
|
@@ -75083,7 +75174,7 @@ function buildSpecRepairPrompt(spec, findings, schema) {
|
|
|
75083
75174
|
var REPAIR_TIMEOUT_MS = 15e4;
|
|
75084
75175
|
var SHOWN_WIDTH = 768;
|
|
75085
75176
|
async function repairSpec(opts) {
|
|
75086
|
-
const text3 = await
|
|
75177
|
+
const text3 = await readFile25(opts.promptPath, "utf8").catch(() => null);
|
|
75087
75178
|
if (text3 === null) return null;
|
|
75088
75179
|
let before;
|
|
75089
75180
|
try {
|
|
@@ -75095,57 +75186,11 @@ async function repairSpec(opts) {
|
|
|
75095
75186
|
const prompt = buildSpecRepairPrompt(text3, opts.findings, adSpecJsonSchema());
|
|
75096
75187
|
const repair = acceptRepair(before, await askAboutImages(RENDER_REVIEW_MODEL, shown ? [shown] : [], prompt, opts.onSpend, REPAIR_TIMEOUT_MS));
|
|
75097
75188
|
if (!repair) return null;
|
|
75098
|
-
await
|
|
75189
|
+
await writeFile15(opts.promptPath, `${JSON.stringify(repair.spec, null, 2)}
|
|
75099
75190
|
`, "utf8");
|
|
75100
75191
|
return repair.changed;
|
|
75101
75192
|
}
|
|
75102
75193
|
|
|
75103
|
-
// src/commands/canvas/turn-renders.ts
|
|
75104
|
-
import { mkdir as mkdir9, readFile as readFile25, writeFile as writeFile15 } from "fs/promises";
|
|
75105
|
-
import path29 from "path";
|
|
75106
|
-
import { z as z67 } from "zod";
|
|
75107
|
-
var RUNS_PER_TURN = 2;
|
|
75108
|
-
var LedgerSchema = z67.object({
|
|
75109
|
-
turnId: z67.string(),
|
|
75110
|
-
creatives: z67.record(z67.string(), z67.object({ runs: z67.number(), verdict: z67.string().optional() }))
|
|
75111
|
-
});
|
|
75112
|
-
function turnRenderRefusal(ledger, turnId, slug) {
|
|
75113
|
-
const entry = turnId !== void 0 && ledger?.turnId === turnId ? ledger.creatives[slug] : void 0;
|
|
75114
|
-
if (!entry) return null;
|
|
75115
|
-
if (entry.verdict === "ready" || entry.verdict === "ready_with_notes") {
|
|
75116
|
-
return {
|
|
75117
|
-
code: "CONFLICT",
|
|
75118
|
-
message: `This ad is already ready in this turn (${entry.verdict}): its notes are not defects.`,
|
|
75119
|
-
fix: "Show it now. A change the person asks for comes with their next message."
|
|
75120
|
-
};
|
|
75121
|
-
}
|
|
75122
|
-
if (entry.runs >= RUNS_PER_TURN) {
|
|
75123
|
-
return {
|
|
75124
|
-
code: "CONFLICT",
|
|
75125
|
-
message: `This ad was rendered twice in this turn, its automatic corrections included, and is still not ready.`,
|
|
75126
|
-
fix: "Show the best version and say in one plain sentence what is left, in the person's words; they decide what changes next."
|
|
75127
|
-
};
|
|
75128
|
-
}
|
|
75129
|
-
return null;
|
|
75130
|
-
}
|
|
75131
|
-
function recordTurnRender(ledger, turnId, slug, verdict2) {
|
|
75132
|
-
const creatives = ledger?.turnId === turnId ? ledger.creatives : {};
|
|
75133
|
-
return { turnId, creatives: { ...creatives, [slug]: { runs: (creatives[slug]?.runs ?? 0) + 1, verdict: verdict2 } } };
|
|
75134
|
-
}
|
|
75135
|
-
var ledgerPath = (cacheDir) => path29.join(cacheDir, "turn-renders.json");
|
|
75136
|
-
async function readTurnRenders(cacheDir) {
|
|
75137
|
-
try {
|
|
75138
|
-
const parsed = LedgerSchema.safeParse(JSON.parse(await readFile25(ledgerPath(cacheDir), "utf8")));
|
|
75139
|
-
return parsed.success ? parsed.data : void 0;
|
|
75140
|
-
} catch {
|
|
75141
|
-
return void 0;
|
|
75142
|
-
}
|
|
75143
|
-
}
|
|
75144
|
-
async function writeTurnRenders(cacheDir, ledger) {
|
|
75145
|
-
await mkdir9(cacheDir, { recursive: true });
|
|
75146
|
-
await writeFile15(ledgerPath(cacheDir), JSON.stringify(ledger), "utf8");
|
|
75147
|
-
}
|
|
75148
|
-
|
|
75149
75194
|
// src/commands/canvas/run.ts
|
|
75150
75195
|
var OPERATOR = "Operator only, agents never need it.";
|
|
75151
75196
|
var runCommand = defineCommand139({
|
|
@@ -75412,6 +75457,12 @@ ${describeRewrites(prepared.rewrites)}
|
|
|
75412
75457
|
change: instruction.replace(/^Change only this: /, ""),
|
|
75413
75458
|
...opts.relayAlso ? { also: opts.relayAlso } : {}
|
|
75414
75459
|
});
|
|
75460
|
+
const kept = delivered.filter((d) => opts.keepSizes?.includes(d.format));
|
|
75461
|
+
if (kept.length > 0) {
|
|
75462
|
+
parsed = withDeliveredSizes(parsed, Object.fromEntries(kept.map((d) => [d.format, d.path])));
|
|
75463
|
+
process.stdout.write(`[fix] keeping ${kept.map((d) => d.format).join(", ")} as it was: nothing blocking on it
|
|
75464
|
+
`);
|
|
75465
|
+
}
|
|
75415
75466
|
if (!editInstruction) process.stdout.write(`[fix] correcting the version of run ${from}: ${instruction}
|
|
75416
75467
|
`);
|
|
75417
75468
|
}
|
|
@@ -75565,6 +75616,7 @@ ${describeRewrites(prepared.rewrites)}
|
|
|
75565
75616
|
resumed: resumed && sameSource === true,
|
|
75566
75617
|
asked: opts.skipViewerCheck === true
|
|
75567
75618
|
}),
|
|
75619
|
+
spent: viewerChecksSpent(await readTurnRenders(turnCache), turnId, creativeSlug),
|
|
75568
75620
|
lookups: liveViewerLookups(path30.dirname(canvasRoot)),
|
|
75569
75621
|
model: VIEWER_CHECK_MODEL,
|
|
75570
75622
|
ask: gatewayViewerModel(VIEWER_CHECK_MODEL, (usage) => viewerUsage.push(usage))
|
|
@@ -75573,6 +75625,9 @@ ${describeRewrites(prepared.rewrites)}
|
|
|
75573
75625
|
`);
|
|
75574
75626
|
await holdUnbilledSpend(cacheDir, viewerUsage);
|
|
75575
75627
|
if (viewerCheck?.failure) {
|
|
75628
|
+
if (creativeSlug && turnId && viewerCheck.failure.error.code === "viewer_check_failed") {
|
|
75629
|
+
await writeTurnRenders(turnCache, recordViewerFail(await readTurnRenders(turnCache), turnId, creativeSlug));
|
|
75630
|
+
}
|
|
75576
75631
|
process.stderr.write(`${JSON.stringify(viewerCheck.failure, null, 2)}
|
|
75577
75632
|
`);
|
|
75578
75633
|
process.exit(2);
|
|
@@ -75830,6 +75885,7 @@ ${describeRewrites(prepared.rewrites)}
|
|
|
75830
75885
|
...step.kind === "fix" ? {
|
|
75831
75886
|
fix: step.instruction,
|
|
75832
75887
|
...step.relay ? { relayAlso: step.relay } : {},
|
|
75888
|
+
...step.keep ? { keepSizes: step.keep } : {},
|
|
75833
75889
|
...step.extend ? { extendSizes: step.extend } : {}
|
|
75834
75890
|
} : step.kind === "extend" ? { extendSizes: step.sizes, extendOnly: true } : step.kind === "relayout" ? {
|
|
75835
75891
|
...step.extend ? { extendSizes: step.extend } : {},
|
|
@@ -80998,7 +81054,7 @@ function ideasHints(report) {
|
|
|
80998
81054
|
if (next.length === 1) hints2.push(`Build: \`baker creatives make ${next[0]}\`.`);
|
|
80999
81055
|
if (next.length > 1)
|
|
81000
81056
|
hints2.push(
|
|
81001
|
-
`Build ${next.join(", ")} at once: one \`creative-producer\` per idea, all spawned in one message, each given its idea id. Never build them one after another: each render takes
|
|
81057
|
+
`Build ${next.join(", ")} at once: one \`creative-producer\` per idea, all spawned in one message in the background, each given its idea id, and handle each as it returns. Never build them one after another: each render takes minutes.`
|
|
81002
81058
|
);
|
|
81003
81059
|
return hints2;
|
|
81004
81060
|
}
|
|
@@ -81335,7 +81391,7 @@ function briefHints(parts) {
|
|
|
81335
81391
|
}
|
|
81336
81392
|
if (parts.customers.thin) {
|
|
81337
81393
|
hints2.push(
|
|
81338
|
-
`Only ${parts.customers.observed} customer quotes on file: run \`baker research customers\` before writing ideas, so they stand on what customers said.`
|
|
81394
|
+
`Only ${parts.customers.observed} customer quotes on file: run \`baker research customers\` now, before writing ideas, so they stand on what customers said. It gathers reviews, Reddit and the web and writes the quotes with their sources in about a minute; never a subagent or web searches of your own for this.`
|
|
81339
81395
|
);
|
|
81340
81396
|
}
|
|
81341
81397
|
if (parts.bottleneck && isAvailable(parts.bottleneck) && parts.bottleneck.verdict === "after_click") {
|