@koda-sl/baker-cli 0.232.0 → 0.233.0-dev.bfa5a91e4

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
@@ -58,7 +58,7 @@ import {
58
58
  ulid,
59
59
  validateCanvasDeep,
60
60
  ytDlpBlockSignal
61
- } from "./chunk-EKLAHWSF.js";
61
+ } from "./chunk-CMPAHYLB.js";
62
62
  import {
63
63
  csvOrJson,
64
64
  daysAgoIso,
@@ -565,498 +565,6 @@ function listSchemas() {
565
565
  return [...registry.keys()];
566
566
  }
567
567
 
568
- // src/commands/actions/tagError.ts
569
- var UNKNOWN_TAG_RE = /Unknown action tag\(s\): ([^.]+)\. Valid tags: ([^.]+)\./;
570
- function splitSlugs(list) {
571
- return list.split(",").map((slug) => slug.trim()).filter((slug) => slug.length > 0);
572
- }
573
- function actionTagFix(message) {
574
- const match = UNKNOWN_TAG_RE.exec(message);
575
- if (!match) {
576
- return null;
577
- }
578
- const [, unknownList, validList] = match;
579
- const unknownTags = splitSlugs(unknownList ?? "");
580
- const validTags = splitSlugs(validList ?? "");
581
- const first = unknownTags[0];
582
- if (first === void 0 || validTags.length === 0) {
583
- return null;
584
- }
585
- return {
586
- action: "retry_with_a_valid_tag",
587
- explanation: `The tag(s) ${unknownTags.join(", ")} are not in this company's taxonomy. \`validTags\` below is the complete accepted set \u2014 pick from it and re-run the same command. Only mint a new tag when none of them names this work; a near-duplicate of an existing tag splits the backlog's filter instead of extending it.`,
588
- unknownTags,
589
- validTags,
590
- mintCommand: `baker actions tags create --slug ${first} --description "<what ${first} groups>"`,
591
- listCommand: "baker actions tags list"
592
- };
593
- }
594
-
595
- // src/commands/actions/shared.ts
596
- function writeOk(data) {
597
- writeJson({ ok: true, data: data ?? null });
598
- }
599
- function failValidation(message) {
600
- writeJson({ ok: false, error: { code: "VALIDATION_ERROR", message } });
601
- process.exit(1);
602
- }
603
- function failApi(err) {
604
- if (err instanceof ApiError) {
605
- const fix = actionTagFix(err.message);
606
- writeJson({ ok: false, error: { code: err.code, message: err.message, ...fix ? { fix } : {} } });
607
- process.exit(1);
608
- }
609
- if (err instanceof Error) {
610
- writeJson({ ok: false, error: { code: "INTERNAL_ERROR", message: err.message } });
611
- process.exit(1);
612
- }
613
- writeJson({ ok: false, error: { code: "INTERNAL_ERROR", message: "Unexpected error" } });
614
- process.exit(1);
615
- }
616
- function generateTempId() {
617
- return `temp_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
618
- }
619
- function isTempId(id) {
620
- return id.startsWith("temp_");
621
- }
622
- function parseTagList(value) {
623
- if (typeof value !== "string") {
624
- return void 0;
625
- }
626
- const tags = value.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
627
- return [...new Set(tags)];
628
- }
629
- function fold(text2) {
630
- return text2.normalize("NFD").replace(/[̀-ͯ]/g, "");
631
- }
632
- var SCHEDULE_SIGNALS = [
633
- /\brecurr(?:ing|ence)\b/i,
634
- /\bcadence\b/i,
635
- /\bcron\b/i,
636
- /\bschedule[ds]?\b/i,
637
- /\b(?:daily|weekly|monthly|quarterly|annual(?:ly)?|biweekly|nightly)\b/i,
638
- /\bevery\s+(?:day|week|month|quarter|year|morning|monday|tuesday|wednesday|thursday|friday|saturday|sunday|\d)/i,
639
- /\beach\s+(?:day|week|month|monday|tuesday|wednesday|thursday|friday|saturday|sunday)\b/i,
640
- /\bremind(?:er|s)?\b/i,
641
- /\brun\s+at\b/i
642
- ];
643
- var ACTION_PRIORITIES = ["urgent", "high", "medium", "low"];
644
- function parsePriority(value, { allowClear }) {
645
- if (value === void 0) {
646
- return void 0;
647
- }
648
- if (typeof value !== "string") {
649
- failValidation(
650
- `--priority must be one of: ${ACTION_PRIORITIES.join(", ")}${allowClear ? ", or 'none' to clear" : ""}.`
651
- );
652
- }
653
- const trimmed = value.trim().toLowerCase();
654
- if (trimmed === "") {
655
- if (allowClear) {
656
- return null;
657
- }
658
- return void 0;
659
- }
660
- if (allowClear && (trimmed === "none" || trimmed === "clear")) {
661
- return null;
662
- }
663
- if (ACTION_PRIORITIES.includes(trimmed)) {
664
- return trimmed;
665
- }
666
- failValidation(
667
- `Unknown --priority "${value}". Expected one of: ${ACTION_PRIORITIES.join(", ")}${allowClear ? ", or 'none' to clear" : ""}.`
668
- );
669
- }
670
- function looksScheduled(name, description) {
671
- const haystack = fold(`${name}
672
- ${description}`);
673
- return SCHEDULE_SIGNALS.some((re2) => re2.test(haystack));
674
- }
675
- var CAMPAIGN_ENTITY = String.raw`(?:campaigns?|campanas?|ad\s*sets?|adsets?|ad\s*groups?|adgroups?|ads?|anuncios?|conjuntos?\s+de\s+anuncios|grupos?\s+de\s+anuncios)`;
676
- var LIFECYCLE_VERB = "(?:pause|unpause|enable|disable|launch|restructure|duplicate|clone|rebuild|prune|reweight|split|consolidate|pausar|pausa|activar|podar|poda|reponderar|duplicar|reestructurar|consolidar)";
677
- var BUDGET_VERB = "(?:cap|raise|lower|increase|decrease|adjust|change|set|update|reallocate|shift|limitar|subir|bajar|ajustar|aplicar|reasignar)";
678
- var re = (...parts) => new RegExp(parts.join(""), "i");
679
- var SURFACE_RULES = [
680
- {
681
- surface: "ask-user",
682
- signals: [
683
- /\bproduction\s+(?:domain|url|site|hostname)\b/i,
684
- /\b(?:confirm|check|verify|confirmar|verificar)\s+(?:with|w\/|con)\s+(?:the\s+|el\s+|la\s+)?(?:client|user|customer|team|cliente|usuario|equipo)\b/i,
685
- /\bmissing\s+(?:the\s+)?(?:value|values|id|ids|url|budget|domain|credentials?|account\s+id|measurement\s+id)\b/i,
686
- /\b(?:ask|get)\s+(?:the\s+)?(?:client|user)\s+for\b/i,
687
- /\b(?:pedir|solicitar)\s+(?:al\s+)?cliente\b/i
688
- ],
689
- hint: "This is a missing input, not blocked work. Ask for it with AskUserQuestion (one question), then do the work in the same turn \u2014 filing the Task AND asking the question is the same request twice."
690
- },
691
- {
692
- surface: "analysis",
693
- nameOnly: true,
694
- signals: [
695
- /^\s*(?:re-?)?(?:investigat|analy[sz]|audit|review|document|assess|evaluat|research|map|inventor|examin|diagnos|verif|validat|benchmark)\w*\b/i,
696
- /^\s*(?:auditar|auditoria|investigar|investigacion|revisar|revision|analizar|analisis|documentar|evaluar|evaluacion|diagnosticar|diagnostico|mapear|inventariar|verificar|verificacion|comprobar|estudiar|estudio)\b/i
697
- ],
698
- hint: "This names the analysis itself \u2014 investigating, auditing and documenting are work you do now, not work you file. Run it in this chat and deliver the finding; a Task is for a fix something genuinely blocks (bar: `__tooling__/docs/tools/baker/actions.md`)."
699
- },
700
- {
701
- surface: "ads-write",
702
- // "Approve X" is a human decision, not a staged write — and the budget
703
- // signals are broad enough to swallow it otherwise. Name-anchored so a
704
- // description recalling an earlier approval doesn't veto real work.
705
- exceptName: [/^\s*(?:approve|approval|sign[-\s]?off|aprobar|aprobacion)\b/i],
706
- // Bare `keywords` is the work when it's the subject of the Task, but plain
707
- // context when a description happens to discuss search terms.
708
- nameOnlySignals: [/\b(?:keywords?|palabras\s+clave)\b/i],
709
- signals: [
710
- // Targeting.
711
- /\b(?:geo|location|geographic|geografic\w*)[\s-]*(?:targeting|target|segmentacion)\b/i,
712
- /\bsegmentacion\s+(?:geografica|por\s+ubicacion)\b/i,
713
- /\bpresence[-\s_]?(?:only|or[-\s_]?interest)\b/i,
714
- /\b(?:ad\s*schedul\w*|dayparting|calendario\s+de\s+anuncios)\b/i,
715
- // Extensions / assets.
716
- /\b(?:callouts?|sitelinks?|site\s*links?|structured\s+snippets?|price\s+extensions?|lead\s+form\s+extensions?|extensiones|asset\s+groups?)\b/i,
717
- // Ad copy. Compound forms only — bare `headline` and `copy` belong to
718
- // `landing`, and an RSA headline has to say so.
719
- /\b(?:rsas?|responsive\s+search\s+ads?|ad\s+copy|copy\s+de\s+(?:los\s+)?anuncios)\b/i,
720
- /\b(?:headline|titular|description)\s+(?:pool|set|slots?)\b/i,
721
- /\bdisplay\s+paths?\b|\brutas?\s+visibles?\b/i,
722
- // Keywords. `negatives` is bare in English because in this product's
723
- // vocabulary the noun is always negative keywords; the Spanish side is
724
- // feminine-only, because "resultados negativos" is a real phrase.
725
- /\bmatch\s+types?\b|\bconcordancias?\b/i,
726
- /\bnegatives?\b/i,
727
- /\b(?:palabras\s+clave\s+)?negativas\b/i,
728
- /\bkeyword\s+lists?\b|\blistas?\s+de\s+palabras\s+clave\b/i,
729
- // Audiences. `LAL` is the operators' own shorthand for a lookalike and
730
- // has no other reading in this corpus.
731
- /\b(?:in-?market|affinity|audience\s+signals?|senales?\s+de\s+audiencia)\b/i,
732
- /\b(?:audiencias?|lal)\b/i,
733
- /\blistas?\s+de\s+(?:exclusion|remarketing)\b|\bexclusion\s+lists?\b/i,
734
- // URLs.
735
- /\btracking\s+(?:url\s+)?templates?\b|\bfinal\s+urls?\b|\bfinal\s+url\s+suffix\b/i,
736
- // Bidding.
737
- /\bbid\s+(?:adjustments?|modifiers?|strateg\w*)\b|\bajustes?\s+de\s+puja\b/i,
738
- /\b(?:tcpa|troas|target\s+cpa|target\s+roas)\b/i,
739
- // Lifecycle. Bare `campaign` is deliberately NOT a signal — it appears
740
- // everywhere and would strip tag-manager and landing of correct
741
- // routings, so every campaign-shaped rule needs a verb or a specific
742
- // setting noun next to it.
743
- // Bounded, and never across the name/description boundary. `ads` is an
744
- // ordinary word in this domain, so an unbounded gap lets a verb in the
745
- // name pair with a passing mention in the description — and since
746
- // ads-write is evaluated first, that spurious match outranks the surface
747
- // which should have answered.
748
- re(String.raw`\b`, LIFECYCLE_VERB, String.raw`\b[^\n]{0,40}\b`, CAMPAIGN_ENTITY, String.raw`\b`),
749
- // Budget. Two complementary shapes — verb before, quantity after — so a
750
- // change stated either way lands, guarded by the `exceptName` veto above.
751
- /\b(?:daily|campaign|lifetime|ad\s*set|minimum|min|competitor)\s+budgets?\b/i,
752
- /\bpresupuestos?\b/i,
753
- re(String.raw`\b`, BUDGET_VERB, String.raw`\b[\s\S]{0,24}\bbudgets?\b`),
754
- /\bbudgets?\b[\s\S]{0,24}\b(?:at|to|cap|down|up|\d|%|€|\$)/i,
755
- /\bnegative\s+keywords?\b/i,
756
- /\b(?:lookalike|custom)\s+audience\b/i,
757
- /\bswap\s+(?:the\s+)?creative\b/i
758
- ],
759
- hint: "This is an ad-platform change the write surface covers \u2014 stage it now with `baker ads google|meta|linkedin` and it applies at publish (guides: `__tooling__/docs/tools/baker/ads-google.md`, `__tooling__/docs/tools/baker/ads-meta.md`, `__tooling__/docs/tools/baker/ads-linkedin.md`). Staging is not going live."
760
- },
761
- {
762
- surface: "tag-manager",
763
- signals: [
764
- /\bgtm\b/i,
765
- /\btag\s*manager\b/i,
766
- /\bdata\s*layer\b/i,
767
- /\bfiring\s+trigger\b/i,
768
- /\bbuilt-?in\s+variables?\b/i,
769
- /\bconsent\s+(?:mode|settings?|state)\b/i,
770
- /\bga4\s+(?:config|configuration|event)\b/i,
771
- /\bcontainer\s+(?:tag|trigger|variable)s?\b/i,
772
- /\b(?:conversion|form|submit|submission|click|purchase|lead|custom)\s+event\b/i,
773
- /\btrack\w*\b[\s\S]*\bevent\b/i,
774
- /\beventos?\s+de\s+(?:formulario|conversion|clic|compra)\b/i,
775
- /\bconsentimiento\b/i,
776
- /\bmedicion\b/i
777
- ],
778
- except: [/\bserver-?\s?side\b/i, /\bsgtm\b/i, /\bhosting\b/i],
779
- hint: "This is a change inside the GTM container \u2014 `baker tag-manager` stages it now and it applies at publish (guide: `__tooling__/docs/tools/baker/tag-manager.md`). Stage it instead of filing it, and if several container fixes really are blocked, they belong in ONE Task, not one per finding."
780
- },
781
- {
782
- surface: "site-tags",
783
- signals: [
784
- /\bpixel\b/i,
785
- /\binsight\s+tag\b/i,
786
- /\bclarity\b/i,
787
- /\bhotjar\b/i,
788
- /\bgtm\s+snippet\b/i,
789
- /\bcapi\b/i,
790
- /\b(?:install|add|remove|swap|instalar|colocar)\b[\s\S]*\b(?:snippet|script|tag)\b/i
791
- ],
792
- // Two vetoes. A structured snippet is a Google Ads extension, not a script
793
- // on the page — `ads-write` claims it above, and this stops both rules
794
- // being able to answer. Server-side delivery is infrastructure nobody here
795
- // can provision, the same call `tag-manager` already makes for sGTM.
796
- except: [/\bstructured\s+snippets?\b/i, /\bserver-?\s?side\b/i, /\bhosting\b/i],
797
- hint: "This is a tag or script on the site \u2014 it goes through the `request_tag_input` approval form in this chat, which also collects any secret values (guide: `__tooling__/docs/tools/baker/tags.md`). Show the form instead of filing a Task."
798
- },
799
- {
800
- surface: "landing",
801
- nameOnlySignals: [/\b(?:landing\s+pages?|landings?|hero|headline|above\s+the\s+fold|pagina)\b/i],
802
- signals: [/\b(?:rewrite|tighten|restyle)\b[\s\S]*\b(?:copy|page|section)\b/i],
803
- hint: "This is a page change \u2014 build it in this chat with the `/landing` skill. Only file it if something outside the page blocks the work."
804
- },
805
- {
806
- surface: "flow",
807
- signals: [/\b(?:form|flow)\s+(?:step|steps|branching|logic|redirect|behaviou?r)\b/i, /\bthank\s*you\s+redirect\b/i],
808
- hint: "This is form behaviour \u2014 change it in this chat with the `/flow-builder` skill. Only file it if something outside the form blocks the work."
809
- }
810
- ];
811
- var FAN_OUT_NUDGE_THRESHOLD = 2;
812
- var FAN_OUT_THRESHOLD = 3;
813
- var SCHEDULED_HINT = 'This reads as recurring/scheduled work. If it should run on a cadence or a future date, create a Scheduled Action instead \u2014 baker scheduled-actions create --cron "0 9 * * MON" (or --run-at; guide: `__tooling__/docs/tools/baker/scheduled-actions.md`). Do NOT capture "set up a scheduled action" as a Work Action.';
814
- function looksExecutable(name, description) {
815
- const foldedName = fold(name);
816
- const full = `${foldedName}
817
- ${fold(description)}`;
818
- for (const rule of SURFACE_RULES) {
819
- const haystack = rule.nameOnly ? foldedName : full;
820
- if (rule.except?.some((re2) => re2.test(full))) {
821
- continue;
822
- }
823
- if (rule.exceptName?.some((re2) => re2.test(foldedName))) {
824
- continue;
825
- }
826
- if (rule.signals.some((re2) => re2.test(haystack))) {
827
- return { surface: rule.surface, hint: rule.hint };
828
- }
829
- if (rule.nameOnlySignals?.some((re2) => re2.test(foldedName))) {
830
- return { surface: rule.surface, hint: rule.hint };
831
- }
832
- }
833
- return null;
834
- }
835
- function buildCreateHints({
836
- name,
837
- description,
838
- tempId,
839
- tags,
840
- prioritySet,
841
- draftCreateCount
842
- }) {
843
- const hints = [];
844
- const advisory = advisoryHint(name, description);
845
- if (advisory) {
846
- hints.push(
847
- `${advisory.hint} If nothing actually blocks it: baker actions draft remove ${tempId}, then do the work.`
848
- );
849
- }
850
- if (draftCreateCount >= FAN_OUT_THRESHOLD) {
851
- hints.push(
852
- `${draftCreateCount} Tasks are staged in this chat. Re-read them with \`baker actions draft\`: can any be done now with a surface you already have, and do any two touch the same system (same container, same campaign, same page)? Those belong in ONE Task \u2014 fold them together with \`baker actions update\` and drop the extras with \`baker actions draft remove <tempId>\`.`
853
- );
854
- } else if (draftCreateCount === FAN_OUT_NUDGE_THRESHOLD) {
855
- hints.push(
856
- `Second Task staged in this chat. If these two land on the same system (same container, same campaign, same page) they are ONE Task \u2014 fold them with \`baker actions update\` and drop the extra with \`baker actions draft remove ${tempId}\`.`
857
- );
858
- }
859
- hints.push(`Link dependencies: baker actions link --blocker <id> --blocked ${tempId}`);
860
- if (!description) {
861
- hints.push("Add description: baker actions update <tempId> --description '...' (what/why/where/done-when)");
862
- }
863
- if (!tags || tags.length === 0) {
864
- hints.push(
865
- "MISSING --tags. This action is invisible to the backlog's tag filter. Re-run with --tags <slug,...> (`baker actions tags list` for the taxonomy, `baker actions tags create --slug <slug>` to mint) \u2014 or `baker actions update <tempId> --tags <slug,...>`."
866
- );
867
- }
868
- if (!prioritySet) {
869
- hints.push(
870
- `MISSING --priority. Without it this action ranks as 'normal' (medium) in the do-first ordering, so urgent/high client work won't surface first. Re-run with --priority ${ACTION_PRIORITIES.join("|")} \u2014 or \`baker actions update <tempId> --priority <level>\`.`
871
- );
872
- }
873
- return hints;
874
- }
875
- function advisoryHint(name, description) {
876
- if (looksScheduled(name, description)) {
877
- return { kind: "scheduled", hint: SCHEDULED_HINT };
878
- }
879
- const executable = looksExecutable(name, description);
880
- return executable ? { kind: "executable", ...executable } : null;
881
- }
882
- var ACTIONS_LIST_DEFAULT_LIMIT = 500;
883
- function buildListHints({ returned, limit }) {
884
- if (returned < limit) {
885
- return [];
886
- }
887
- return [
888
- `This list is capped at ${limit} Tasks and came back full, so older Tasks exist that are NOT shown. Do not treat it as the whole backlog \u2014 re-run with --limit ${limit * 2} (or narrow with --status / --q).`
889
- ];
890
- }
891
-
892
- // src/commands/actions/skillCatalog.ts
893
- import { existsSync, readdirSync, readFileSync } from "fs";
894
- import { dirname, join } from "path";
895
- var DESCRIPTION_MAX = 600;
896
- var SKILLS_SUBPATH = join(".claude", "skills");
897
- var EXCLUDED_SKILLS = /* @__PURE__ */ new Set(["actions"]);
898
- function stripQuotes(value) {
899
- const trimmed = value.trim();
900
- if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
901
- return trimmed.slice(1, -1).replace(/\\"/g, '"').replace(/\\n/g, "\n");
902
- }
903
- if (trimmed.startsWith("'") && trimmed.endsWith("'")) {
904
- return trimmed.slice(1, -1).replace(/''/g, "'");
905
- }
906
- return trimmed;
907
- }
908
- function extractFrontmatterLines(md) {
909
- if (!md.startsWith("---")) return null;
910
- const end = md.indexOf("\n---", 3);
911
- if (end === -1) return null;
912
- return md.slice(md.indexOf("\n", 3) + 1, end).split("\n");
913
- }
914
- var BLOCK_SCALAR = /^([|>])[+-]?$/;
915
- function collectBlockLines(lines, start) {
916
- const collected = [];
917
- for (let i = start; i < lines.length; i++) {
918
- const line = lines[i] ?? "";
919
- if (line.trim() === "") {
920
- collected.push("");
921
- } else if (/^\s/.test(line)) {
922
- collected.push(line.trim());
923
- } else {
924
- break;
925
- }
926
- }
927
- while (collected.length > 0 && collected.at(-1) === "") collected.pop();
928
- return collected;
929
- }
930
- function foldLines(collected) {
931
- const paragraphs = [];
932
- let buffer = [];
933
- for (const line of collected) {
934
- if (line === "") {
935
- if (buffer.length > 0) paragraphs.push(buffer.join(" "));
936
- buffer = [];
937
- } else {
938
- buffer.push(line);
939
- }
940
- }
941
- if (buffer.length > 0) paragraphs.push(buffer.join(" "));
942
- return paragraphs.join("\n");
943
- }
944
- function readBlockScalar(lines, start, style) {
945
- const collected = collectBlockLines(lines, start);
946
- return style === "|" ? collected.join("\n") : foldLines(collected);
947
- }
948
- function readField(lines, field) {
949
- for (let i = 0; i < lines.length; i++) {
950
- const match = (lines[i] ?? "").match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
951
- if (!match || match[1] !== field) continue;
952
- const inline = (match[2] ?? "").trim();
953
- const scalar = inline.match(BLOCK_SCALAR);
954
- if (scalar) {
955
- return readBlockScalar(lines, i + 1, scalar[1]);
956
- }
957
- return stripQuotes(inline);
958
- }
959
- return null;
960
- }
961
- function parseSkillFrontmatter(md) {
962
- const lines = extractFrontmatterLines(md);
963
- if (!lines) return null;
964
- const name = readField(lines, "name");
965
- const description = readField(lines, "description");
966
- if (!name || !description) return null;
967
- const trimmed = description.length > DESCRIPTION_MAX ? `${description.slice(0, DESCRIPTION_MAX)}\u2026` : description;
968
- return { name, description: trimmed };
969
- }
970
- function findSkillsDir(startDir) {
971
- let dir = startDir;
972
- for (; ; ) {
973
- const candidate = join(dir, SKILLS_SUBPATH);
974
- if (existsSync(candidate)) return candidate;
975
- const parent = dirname(dir);
976
- if (parent === dir) return null;
977
- dir = parent;
978
- }
979
- }
980
- function readSkillCatalog(startDir) {
981
- const skillsDir = findSkillsDir(startDir);
982
- if (!skillsDir) return [];
983
- const entries = [];
984
- for (const dirent of readdirSync(skillsDir, { withFileTypes: true })) {
985
- if (!dirent.isDirectory() || EXCLUDED_SKILLS.has(dirent.name)) continue;
986
- const skillFile = join(skillsDir, dirent.name, "SKILL.md");
987
- if (!existsSync(skillFile)) continue;
988
- try {
989
- const parsed = parseSkillFrontmatter(readFileSync(skillFile, "utf8"));
990
- if (parsed) entries.push(parsed);
991
- } catch {
992
- }
993
- }
994
- return entries.sort((a, b) => a.name.localeCompare(b.name));
995
- }
996
-
997
- // src/commands/actions/claim.ts
998
- registerSchema({
999
- command: "actions.claim",
1000
- description: "Claim an action for the current chat (live \u2014 visible to other chats immediately). Claim only what you're actively working on now: it's required before `complete`, but NOT for `update` or `discard` (those stage without a claim). Returns action details plus a fast-model recommendation of which skills to load for the work (`recommendedSkills`) and a routing hint: load every skill that owns part of the work and the tool doc for each baker CLI family you'll use.",
1001
- args: {
1002
- id: { type: "string", description: "Action ID", required: true }
1003
- }
1004
- });
1005
- async function recommendSkills(actionId) {
1006
- const skills = readSkillCatalog(process.cwd());
1007
- if (skills.length === 0) return [];
1008
- const response = await apiPost("/api/actions/recommend-skills", {
1009
- actionId,
1010
- skills
1011
- });
1012
- return response.data.recommendations;
1013
- }
1014
- var ROUTING_HINT = "Actions often span several surfaces (a landing page plus a Google Ads change, a sheet pull plus an audience upload). Before starting, load EVERY skill that owns part of the work and read the tool doc (__tooling__/docs/tools/baker/<family>.md) for each baker CLI family you'll use. If the action references source material living in a connected tool (an email attachment, a call, a CRM record, a spreadsheet), reach for that tool too. If it describes recurring or future-dated work, it belongs on a schedule \u2014 see __tooling__/docs/tools/baker/scheduled-actions.md.";
1015
- function buildHints(recommendations) {
1016
- if (recommendations.length === 0) {
1017
- return ["Review the action name, description, and tags above.", ROUTING_HINT];
1018
- }
1019
- return [
1020
- "Recommended skills for this action (load the ones you'll use):",
1021
- ...recommendations.map((r) => ` /${r.name} \u2014 ${r.reason}`),
1022
- "Suggestions come from the action's name/description/tags only \u2014 the work may need more.",
1023
- ROUTING_HINT
1024
- ];
1025
- }
1026
- var claimCommand = defineCommand({
1027
- meta: {
1028
- name: "claim",
1029
- description: "Claim an action so other chats see you're working on it. Required before `complete` (update and discard don't need a claim). Example: baker actions claim <action-id>"
1030
- },
1031
- args: {
1032
- id: { type: "positional", description: "Action ID", required: false },
1033
- "action-id": { type: "string", description: "Action ID", required: false }
1034
- },
1035
- run: async ({ args }) => {
1036
- try {
1037
- const id = args.id || args["action-id"];
1038
- if (!id) {
1039
- failValidation("Action ID is required.");
1040
- }
1041
- validateConvexId(id);
1042
- const chatId = requireChatId();
1043
- const response = await apiPost("/api/actions/claim", {
1044
- actionId: id,
1045
- chatId
1046
- });
1047
- let recommendedSkills = [];
1048
- try {
1049
- recommendedSkills = await recommendSkills(id);
1050
- } catch {
1051
- }
1052
- const data = response.data ? { ...response.data, recommendedSkills } : response.data;
1053
- writeJson({ ok: response.ok, data, hints: buildHints(recommendedSkills) });
1054
- } catch (err) {
1055
- failApi(err);
1056
- }
1057
- }
1058
- });
1059
-
1060
568
  // ../api/src/actions.ts
1061
569
  import { z } from "zod";
1062
570
  var actionStatusSchema = z.enum(["pending", "in_progress", "completed", "discarded"]);
@@ -1199,9 +707,13 @@ function okResponse(data) {
1199
707
  function okOnlySchema() {
1200
708
  return z.object({ ok: z.literal(true) });
1201
709
  }
710
+ var ACTION_TEMP_ID_PREFIX = "temp_";
711
+ var actionTempIdSchema = z.string().refine((value) => value.startsWith(ACTION_TEMP_ID_PREFIX) && value.length > ACTION_TEMP_ID_PREFIX.length, {
712
+ message: "tempId must start with `temp_` and have something after it (e.g. temp_hero_copy). Omit --temp-id to get a generated one."
713
+ });
1202
714
  var actionsCreateRequestSchema = z.object({
1203
715
  chatId: z.string(),
1204
- tempId: z.string(),
716
+ tempId: actionTempIdSchema,
1205
717
  name: z.string().min(1),
1206
718
  description: z.string(),
1207
719
  tags: z.array(z.string()).optional(),
@@ -6665,6 +6177,498 @@ var videosIngestResponseSchema = z21.object({
6665
6177
  var videosDeleteRequestSchema = z21.object({ id: z21.string().min(1, "Missing video ID") });
6666
6178
  var videosDeleteResponseSchema = z21.object({ ok: z21.literal(true) });
6667
6179
 
6180
+ // src/commands/actions/tagError.ts
6181
+ var UNKNOWN_TAG_RE = /Unknown action tag\(s\): ([^.]+)\. Valid tags: ([^.]+)\./;
6182
+ function splitSlugs(list) {
6183
+ return list.split(",").map((slug) => slug.trim()).filter((slug) => slug.length > 0);
6184
+ }
6185
+ function actionTagFix(message) {
6186
+ const match = UNKNOWN_TAG_RE.exec(message);
6187
+ if (!match) {
6188
+ return null;
6189
+ }
6190
+ const [, unknownList, validList] = match;
6191
+ const unknownTags = splitSlugs(unknownList ?? "");
6192
+ const validTags = splitSlugs(validList ?? "");
6193
+ const first = unknownTags[0];
6194
+ if (first === void 0 || validTags.length === 0) {
6195
+ return null;
6196
+ }
6197
+ return {
6198
+ action: "retry_with_a_valid_tag",
6199
+ explanation: `The tag(s) ${unknownTags.join(", ")} are not in this company's taxonomy. \`validTags\` below is the complete accepted set \u2014 pick from it and re-run the same command. Only mint a new tag when none of them names this work; a near-duplicate of an existing tag splits the backlog's filter instead of extending it.`,
6200
+ unknownTags,
6201
+ validTags,
6202
+ mintCommand: `baker actions tags create --slug ${first} --description "<what ${first} groups>"`,
6203
+ listCommand: "baker actions tags list"
6204
+ };
6205
+ }
6206
+
6207
+ // src/commands/actions/shared.ts
6208
+ function writeOk(data) {
6209
+ writeJson({ ok: true, data: data ?? null });
6210
+ }
6211
+ function failValidation(message) {
6212
+ writeJson({ ok: false, error: { code: "VALIDATION_ERROR", message } });
6213
+ process.exit(1);
6214
+ }
6215
+ function failApi(err) {
6216
+ if (err instanceof ApiError) {
6217
+ const fix = actionTagFix(err.message);
6218
+ writeJson({ ok: false, error: { code: err.code, message: err.message, ...fix ? { fix } : {} } });
6219
+ process.exit(1);
6220
+ }
6221
+ if (err instanceof Error) {
6222
+ writeJson({ ok: false, error: { code: "INTERNAL_ERROR", message: err.message } });
6223
+ process.exit(1);
6224
+ }
6225
+ writeJson({ ok: false, error: { code: "INTERNAL_ERROR", message: "Unexpected error" } });
6226
+ process.exit(1);
6227
+ }
6228
+ function generateTempId() {
6229
+ return `${ACTION_TEMP_ID_PREFIX}${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
6230
+ }
6231
+ function isTempId(id) {
6232
+ return id.startsWith(ACTION_TEMP_ID_PREFIX);
6233
+ }
6234
+ function parseTagList(value) {
6235
+ if (typeof value !== "string") {
6236
+ return void 0;
6237
+ }
6238
+ const tags = value.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
6239
+ return [...new Set(tags)];
6240
+ }
6241
+ function fold(text2) {
6242
+ return text2.normalize("NFD").replace(/[̀-ͯ]/g, "");
6243
+ }
6244
+ var SCHEDULE_SIGNALS = [
6245
+ /\brecurr(?:ing|ence)\b/i,
6246
+ /\bcadence\b/i,
6247
+ /\bcron\b/i,
6248
+ /\bschedule[ds]?\b/i,
6249
+ /\b(?:daily|weekly|monthly|quarterly|annual(?:ly)?|biweekly|nightly)\b/i,
6250
+ /\bevery\s+(?:day|week|month|quarter|year|morning|monday|tuesday|wednesday|thursday|friday|saturday|sunday|\d)/i,
6251
+ /\beach\s+(?:day|week|month|monday|tuesday|wednesday|thursday|friday|saturday|sunday)\b/i,
6252
+ /\bremind(?:er|s)?\b/i,
6253
+ /\brun\s+at\b/i
6254
+ ];
6255
+ var ACTION_PRIORITIES = ["urgent", "high", "medium", "low"];
6256
+ function parsePriority(value, { allowClear }) {
6257
+ if (value === void 0) {
6258
+ return void 0;
6259
+ }
6260
+ if (typeof value !== "string") {
6261
+ failValidation(
6262
+ `--priority must be one of: ${ACTION_PRIORITIES.join(", ")}${allowClear ? ", or 'none' to clear" : ""}.`
6263
+ );
6264
+ }
6265
+ const trimmed = value.trim().toLowerCase();
6266
+ if (trimmed === "") {
6267
+ if (allowClear) {
6268
+ return null;
6269
+ }
6270
+ return void 0;
6271
+ }
6272
+ if (allowClear && (trimmed === "none" || trimmed === "clear")) {
6273
+ return null;
6274
+ }
6275
+ if (ACTION_PRIORITIES.includes(trimmed)) {
6276
+ return trimmed;
6277
+ }
6278
+ failValidation(
6279
+ `Unknown --priority "${value}". Expected one of: ${ACTION_PRIORITIES.join(", ")}${allowClear ? ", or 'none' to clear" : ""}.`
6280
+ );
6281
+ }
6282
+ function looksScheduled(name, description) {
6283
+ const haystack = fold(`${name}
6284
+ ${description}`);
6285
+ return SCHEDULE_SIGNALS.some((re2) => re2.test(haystack));
6286
+ }
6287
+ var CAMPAIGN_ENTITY = String.raw`(?:campaigns?|campanas?|ad\s*sets?|adsets?|ad\s*groups?|adgroups?|ads?|anuncios?|conjuntos?\s+de\s+anuncios|grupos?\s+de\s+anuncios)`;
6288
+ var LIFECYCLE_VERB = "(?:pause|unpause|enable|disable|launch|restructure|duplicate|clone|rebuild|prune|reweight|split|consolidate|pausar|pausa|activar|podar|poda|reponderar|duplicar|reestructurar|consolidar)";
6289
+ var BUDGET_VERB = "(?:cap|raise|lower|increase|decrease|adjust|change|set|update|reallocate|shift|limitar|subir|bajar|ajustar|aplicar|reasignar)";
6290
+ var re = (...parts) => new RegExp(parts.join(""), "i");
6291
+ var SURFACE_RULES = [
6292
+ {
6293
+ surface: "ask-user",
6294
+ signals: [
6295
+ /\bproduction\s+(?:domain|url|site|hostname)\b/i,
6296
+ /\b(?:confirm|check|verify|confirmar|verificar)\s+(?:with|w\/|con)\s+(?:the\s+|el\s+|la\s+)?(?:client|user|customer|team|cliente|usuario|equipo)\b/i,
6297
+ /\bmissing\s+(?:the\s+)?(?:value|values|id|ids|url|budget|domain|credentials?|account\s+id|measurement\s+id)\b/i,
6298
+ /\b(?:ask|get)\s+(?:the\s+)?(?:client|user)\s+for\b/i,
6299
+ /\b(?:pedir|solicitar)\s+(?:al\s+)?cliente\b/i
6300
+ ],
6301
+ hint: "This is a missing input, not blocked work. Ask for it with AskUserQuestion (one question), then do the work in the same turn \u2014 filing the Task AND asking the question is the same request twice."
6302
+ },
6303
+ {
6304
+ surface: "analysis",
6305
+ nameOnly: true,
6306
+ signals: [
6307
+ /^\s*(?:re-?)?(?:investigat|analy[sz]|audit|review|document|assess|evaluat|research|map|inventor|examin|diagnos|verif|validat|benchmark)\w*\b/i,
6308
+ /^\s*(?:auditar|auditoria|investigar|investigacion|revisar|revision|analizar|analisis|documentar|evaluar|evaluacion|diagnosticar|diagnostico|mapear|inventariar|verificar|verificacion|comprobar|estudiar|estudio)\b/i
6309
+ ],
6310
+ hint: "This names the analysis itself \u2014 investigating, auditing and documenting are work you do now, not work you file. Run it in this chat and deliver the finding; a Task is for a fix something genuinely blocks (bar: `__tooling__/docs/tools/baker/actions.md`)."
6311
+ },
6312
+ {
6313
+ surface: "ads-write",
6314
+ // "Approve X" is a human decision, not a staged write — and the budget
6315
+ // signals are broad enough to swallow it otherwise. Name-anchored so a
6316
+ // description recalling an earlier approval doesn't veto real work.
6317
+ exceptName: [/^\s*(?:approve|approval|sign[-\s]?off|aprobar|aprobacion)\b/i],
6318
+ // Bare `keywords` is the work when it's the subject of the Task, but plain
6319
+ // context when a description happens to discuss search terms.
6320
+ nameOnlySignals: [/\b(?:keywords?|palabras\s+clave)\b/i],
6321
+ signals: [
6322
+ // Targeting.
6323
+ /\b(?:geo|location|geographic|geografic\w*)[\s-]*(?:targeting|target|segmentacion)\b/i,
6324
+ /\bsegmentacion\s+(?:geografica|por\s+ubicacion)\b/i,
6325
+ /\bpresence[-\s_]?(?:only|or[-\s_]?interest)\b/i,
6326
+ /\b(?:ad\s*schedul\w*|dayparting|calendario\s+de\s+anuncios)\b/i,
6327
+ // Extensions / assets.
6328
+ /\b(?:callouts?|sitelinks?|site\s*links?|structured\s+snippets?|price\s+extensions?|lead\s+form\s+extensions?|extensiones|asset\s+groups?)\b/i,
6329
+ // Ad copy. Compound forms only — bare `headline` and `copy` belong to
6330
+ // `landing`, and an RSA headline has to say so.
6331
+ /\b(?:rsas?|responsive\s+search\s+ads?|ad\s+copy|copy\s+de\s+(?:los\s+)?anuncios)\b/i,
6332
+ /\b(?:headline|titular|description)\s+(?:pool|set|slots?)\b/i,
6333
+ /\bdisplay\s+paths?\b|\brutas?\s+visibles?\b/i,
6334
+ // Keywords. `negatives` is bare in English because in this product's
6335
+ // vocabulary the noun is always negative keywords; the Spanish side is
6336
+ // feminine-only, because "resultados negativos" is a real phrase.
6337
+ /\bmatch\s+types?\b|\bconcordancias?\b/i,
6338
+ /\bnegatives?\b/i,
6339
+ /\b(?:palabras\s+clave\s+)?negativas\b/i,
6340
+ /\bkeyword\s+lists?\b|\blistas?\s+de\s+palabras\s+clave\b/i,
6341
+ // Audiences. `LAL` is the operators' own shorthand for a lookalike and
6342
+ // has no other reading in this corpus.
6343
+ /\b(?:in-?market|affinity|audience\s+signals?|senales?\s+de\s+audiencia)\b/i,
6344
+ /\b(?:audiencias?|lal)\b/i,
6345
+ /\blistas?\s+de\s+(?:exclusion|remarketing)\b|\bexclusion\s+lists?\b/i,
6346
+ // URLs.
6347
+ /\btracking\s+(?:url\s+)?templates?\b|\bfinal\s+urls?\b|\bfinal\s+url\s+suffix\b/i,
6348
+ // Bidding.
6349
+ /\bbid\s+(?:adjustments?|modifiers?|strateg\w*)\b|\bajustes?\s+de\s+puja\b/i,
6350
+ /\b(?:tcpa|troas|target\s+cpa|target\s+roas)\b/i,
6351
+ // Lifecycle. Bare `campaign` is deliberately NOT a signal — it appears
6352
+ // everywhere and would strip tag-manager and landing of correct
6353
+ // routings, so every campaign-shaped rule needs a verb or a specific
6354
+ // setting noun next to it.
6355
+ // Bounded, and never across the name/description boundary. `ads` is an
6356
+ // ordinary word in this domain, so an unbounded gap lets a verb in the
6357
+ // name pair with a passing mention in the description — and since
6358
+ // ads-write is evaluated first, that spurious match outranks the surface
6359
+ // which should have answered.
6360
+ re(String.raw`\b`, LIFECYCLE_VERB, String.raw`\b[^\n]{0,40}\b`, CAMPAIGN_ENTITY, String.raw`\b`),
6361
+ // Budget. Two complementary shapes — verb before, quantity after — so a
6362
+ // change stated either way lands, guarded by the `exceptName` veto above.
6363
+ /\b(?:daily|campaign|lifetime|ad\s*set|minimum|min|competitor)\s+budgets?\b/i,
6364
+ /\bpresupuestos?\b/i,
6365
+ re(String.raw`\b`, BUDGET_VERB, String.raw`\b[\s\S]{0,24}\bbudgets?\b`),
6366
+ /\bbudgets?\b[\s\S]{0,24}\b(?:at|to|cap|down|up|\d|%|€|\$)/i,
6367
+ /\bnegative\s+keywords?\b/i,
6368
+ /\b(?:lookalike|custom)\s+audience\b/i,
6369
+ /\bswap\s+(?:the\s+)?creative\b/i
6370
+ ],
6371
+ hint: "This is an ad-platform change the write surface covers \u2014 stage it now with `baker ads google|meta|linkedin` and it applies at publish (guides: `__tooling__/docs/tools/baker/ads-google.md`, `__tooling__/docs/tools/baker/ads-meta.md`, `__tooling__/docs/tools/baker/ads-linkedin.md`). Staging is not going live."
6372
+ },
6373
+ {
6374
+ surface: "tag-manager",
6375
+ signals: [
6376
+ /\bgtm\b/i,
6377
+ /\btag\s*manager\b/i,
6378
+ /\bdata\s*layer\b/i,
6379
+ /\bfiring\s+trigger\b/i,
6380
+ /\bbuilt-?in\s+variables?\b/i,
6381
+ /\bconsent\s+(?:mode|settings?|state)\b/i,
6382
+ /\bga4\s+(?:config|configuration|event)\b/i,
6383
+ /\bcontainer\s+(?:tag|trigger|variable)s?\b/i,
6384
+ /\b(?:conversion|form|submit|submission|click|purchase|lead|custom)\s+event\b/i,
6385
+ /\btrack\w*\b[\s\S]*\bevent\b/i,
6386
+ /\beventos?\s+de\s+(?:formulario|conversion|clic|compra)\b/i,
6387
+ /\bconsentimiento\b/i,
6388
+ /\bmedicion\b/i
6389
+ ],
6390
+ except: [/\bserver-?\s?side\b/i, /\bsgtm\b/i, /\bhosting\b/i],
6391
+ hint: "This is a change inside the GTM container \u2014 `baker tag-manager` stages it now and it applies at publish (guide: `__tooling__/docs/tools/baker/tag-manager.md`). Stage it instead of filing it, and if several container fixes really are blocked, they belong in ONE Task, not one per finding."
6392
+ },
6393
+ {
6394
+ surface: "site-tags",
6395
+ signals: [
6396
+ /\bpixel\b/i,
6397
+ /\binsight\s+tag\b/i,
6398
+ /\bclarity\b/i,
6399
+ /\bhotjar\b/i,
6400
+ /\bgtm\s+snippet\b/i,
6401
+ /\bcapi\b/i,
6402
+ /\b(?:install|add|remove|swap|instalar|colocar)\b[\s\S]*\b(?:snippet|script|tag)\b/i
6403
+ ],
6404
+ // Two vetoes. A structured snippet is a Google Ads extension, not a script
6405
+ // on the page — `ads-write` claims it above, and this stops both rules
6406
+ // being able to answer. Server-side delivery is infrastructure nobody here
6407
+ // can provision, the same call `tag-manager` already makes for sGTM.
6408
+ except: [/\bstructured\s+snippets?\b/i, /\bserver-?\s?side\b/i, /\bhosting\b/i],
6409
+ hint: "This is a tag or script on the site \u2014 it goes through the `request_tag_input` approval form in this chat, which also collects any secret values (guide: `__tooling__/docs/tools/baker/tags.md`). Show the form instead of filing a Task."
6410
+ },
6411
+ {
6412
+ surface: "landing",
6413
+ nameOnlySignals: [/\b(?:landing\s+pages?|landings?|hero|headline|above\s+the\s+fold|pagina)\b/i],
6414
+ signals: [/\b(?:rewrite|tighten|restyle)\b[\s\S]*\b(?:copy|page|section)\b/i],
6415
+ hint: "This is a page change \u2014 build it in this chat with the `/landing` skill. Only file it if something outside the page blocks the work."
6416
+ },
6417
+ {
6418
+ surface: "flow",
6419
+ signals: [/\b(?:form|flow)\s+(?:step|steps|branching|logic|redirect|behaviou?r)\b/i, /\bthank\s*you\s+redirect\b/i],
6420
+ hint: "This is form behaviour \u2014 change it in this chat with the `/flow-builder` skill. Only file it if something outside the form blocks the work."
6421
+ }
6422
+ ];
6423
+ var FAN_OUT_NUDGE_THRESHOLD = 2;
6424
+ var FAN_OUT_THRESHOLD = 3;
6425
+ var SCHEDULED_HINT = 'This reads as recurring/scheduled work. If it should run on a cadence or a future date, create a Scheduled Action instead \u2014 baker scheduled-actions create --cron "0 9 * * MON" (or --run-at; guide: `__tooling__/docs/tools/baker/scheduled-actions.md`). Do NOT capture "set up a scheduled action" as a Work Action.';
6426
+ function looksExecutable(name, description) {
6427
+ const foldedName = fold(name);
6428
+ const full = `${foldedName}
6429
+ ${fold(description)}`;
6430
+ for (const rule of SURFACE_RULES) {
6431
+ const haystack = rule.nameOnly ? foldedName : full;
6432
+ if (rule.except?.some((re2) => re2.test(full))) {
6433
+ continue;
6434
+ }
6435
+ if (rule.exceptName?.some((re2) => re2.test(foldedName))) {
6436
+ continue;
6437
+ }
6438
+ if (rule.signals.some((re2) => re2.test(haystack))) {
6439
+ return { surface: rule.surface, hint: rule.hint };
6440
+ }
6441
+ if (rule.nameOnlySignals?.some((re2) => re2.test(foldedName))) {
6442
+ return { surface: rule.surface, hint: rule.hint };
6443
+ }
6444
+ }
6445
+ return null;
6446
+ }
6447
+ function buildCreateHints({
6448
+ name,
6449
+ description,
6450
+ tempId,
6451
+ tags,
6452
+ prioritySet,
6453
+ draftCreateCount
6454
+ }) {
6455
+ const hints = [];
6456
+ const advisory = advisoryHint(name, description);
6457
+ if (advisory) {
6458
+ hints.push(
6459
+ `${advisory.hint} If nothing actually blocks it: baker actions draft remove ${tempId}, then do the work.`
6460
+ );
6461
+ }
6462
+ if (draftCreateCount >= FAN_OUT_THRESHOLD) {
6463
+ hints.push(
6464
+ `${draftCreateCount} Tasks are staged in this chat. Re-read them with \`baker actions draft\`: can any be done now with a surface you already have, and do any two touch the same system (same container, same campaign, same page)? Those belong in ONE Task \u2014 fold them together with \`baker actions update\` and drop the extras with \`baker actions draft remove <tempId>\`.`
6465
+ );
6466
+ } else if (draftCreateCount === FAN_OUT_NUDGE_THRESHOLD) {
6467
+ hints.push(
6468
+ `Second Task staged in this chat. If these two land on the same system (same container, same campaign, same page) they are ONE Task \u2014 fold them with \`baker actions update\` and drop the extra with \`baker actions draft remove ${tempId}\`.`
6469
+ );
6470
+ }
6471
+ hints.push(`Link dependencies: baker actions link --blocker <id> --blocked ${tempId}`);
6472
+ if (!description) {
6473
+ hints.push("Add description: baker actions update <tempId> --description '...' (what/why/where/done-when)");
6474
+ }
6475
+ if (!tags || tags.length === 0) {
6476
+ hints.push(
6477
+ "MISSING --tags. This action is invisible to the backlog's tag filter. Re-run with --tags <slug,...> (`baker actions tags list` for the taxonomy, `baker actions tags create --slug <slug>` to mint) \u2014 or `baker actions update <tempId> --tags <slug,...>`."
6478
+ );
6479
+ }
6480
+ if (!prioritySet) {
6481
+ hints.push(
6482
+ `MISSING --priority. Without it this action ranks as 'normal' (medium) in the do-first ordering, so urgent/high client work won't surface first. Re-run with --priority ${ACTION_PRIORITIES.join("|")} \u2014 or \`baker actions update <tempId> --priority <level>\`.`
6483
+ );
6484
+ }
6485
+ return hints;
6486
+ }
6487
+ function advisoryHint(name, description) {
6488
+ if (looksScheduled(name, description)) {
6489
+ return { kind: "scheduled", hint: SCHEDULED_HINT };
6490
+ }
6491
+ const executable = looksExecutable(name, description);
6492
+ return executable ? { kind: "executable", ...executable } : null;
6493
+ }
6494
+ var ACTIONS_LIST_DEFAULT_LIMIT = 500;
6495
+ function buildListHints({ returned, limit }) {
6496
+ if (returned < limit) {
6497
+ return [];
6498
+ }
6499
+ return [
6500
+ `This list is capped at ${limit} Tasks and came back full, so older Tasks exist that are NOT shown. Do not treat it as the whole backlog \u2014 re-run with --limit ${limit * 2} (or narrow with --status / --q).`
6501
+ ];
6502
+ }
6503
+
6504
+ // src/commands/actions/skillCatalog.ts
6505
+ import { existsSync, readdirSync, readFileSync } from "fs";
6506
+ import { dirname, join } from "path";
6507
+ var DESCRIPTION_MAX = 600;
6508
+ var SKILLS_SUBPATH = join(".claude", "skills");
6509
+ var EXCLUDED_SKILLS = /* @__PURE__ */ new Set(["actions"]);
6510
+ function stripQuotes(value) {
6511
+ const trimmed = value.trim();
6512
+ if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
6513
+ return trimmed.slice(1, -1).replace(/\\"/g, '"').replace(/\\n/g, "\n");
6514
+ }
6515
+ if (trimmed.startsWith("'") && trimmed.endsWith("'")) {
6516
+ return trimmed.slice(1, -1).replace(/''/g, "'");
6517
+ }
6518
+ return trimmed;
6519
+ }
6520
+ function extractFrontmatterLines(md) {
6521
+ if (!md.startsWith("---")) return null;
6522
+ const end = md.indexOf("\n---", 3);
6523
+ if (end === -1) return null;
6524
+ return md.slice(md.indexOf("\n", 3) + 1, end).split("\n");
6525
+ }
6526
+ var BLOCK_SCALAR = /^([|>])[+-]?$/;
6527
+ function collectBlockLines(lines, start) {
6528
+ const collected = [];
6529
+ for (let i = start; i < lines.length; i++) {
6530
+ const line = lines[i] ?? "";
6531
+ if (line.trim() === "") {
6532
+ collected.push("");
6533
+ } else if (/^\s/.test(line)) {
6534
+ collected.push(line.trim());
6535
+ } else {
6536
+ break;
6537
+ }
6538
+ }
6539
+ while (collected.length > 0 && collected.at(-1) === "") collected.pop();
6540
+ return collected;
6541
+ }
6542
+ function foldLines(collected) {
6543
+ const paragraphs = [];
6544
+ let buffer = [];
6545
+ for (const line of collected) {
6546
+ if (line === "") {
6547
+ if (buffer.length > 0) paragraphs.push(buffer.join(" "));
6548
+ buffer = [];
6549
+ } else {
6550
+ buffer.push(line);
6551
+ }
6552
+ }
6553
+ if (buffer.length > 0) paragraphs.push(buffer.join(" "));
6554
+ return paragraphs.join("\n");
6555
+ }
6556
+ function readBlockScalar(lines, start, style) {
6557
+ const collected = collectBlockLines(lines, start);
6558
+ return style === "|" ? collected.join("\n") : foldLines(collected);
6559
+ }
6560
+ function readField(lines, field) {
6561
+ for (let i = 0; i < lines.length; i++) {
6562
+ const match = (lines[i] ?? "").match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
6563
+ if (!match || match[1] !== field) continue;
6564
+ const inline = (match[2] ?? "").trim();
6565
+ const scalar = inline.match(BLOCK_SCALAR);
6566
+ if (scalar) {
6567
+ return readBlockScalar(lines, i + 1, scalar[1]);
6568
+ }
6569
+ return stripQuotes(inline);
6570
+ }
6571
+ return null;
6572
+ }
6573
+ function parseSkillFrontmatter(md) {
6574
+ const lines = extractFrontmatterLines(md);
6575
+ if (!lines) return null;
6576
+ const name = readField(lines, "name");
6577
+ const description = readField(lines, "description");
6578
+ if (!name || !description) return null;
6579
+ const trimmed = description.length > DESCRIPTION_MAX ? `${description.slice(0, DESCRIPTION_MAX)}\u2026` : description;
6580
+ return { name, description: trimmed };
6581
+ }
6582
+ function findSkillsDir(startDir) {
6583
+ let dir = startDir;
6584
+ for (; ; ) {
6585
+ const candidate = join(dir, SKILLS_SUBPATH);
6586
+ if (existsSync(candidate)) return candidate;
6587
+ const parent = dirname(dir);
6588
+ if (parent === dir) return null;
6589
+ dir = parent;
6590
+ }
6591
+ }
6592
+ function readSkillCatalog(startDir) {
6593
+ const skillsDir = findSkillsDir(startDir);
6594
+ if (!skillsDir) return [];
6595
+ const entries = [];
6596
+ for (const dirent of readdirSync(skillsDir, { withFileTypes: true })) {
6597
+ if (!dirent.isDirectory() || EXCLUDED_SKILLS.has(dirent.name)) continue;
6598
+ const skillFile = join(skillsDir, dirent.name, "SKILL.md");
6599
+ if (!existsSync(skillFile)) continue;
6600
+ try {
6601
+ const parsed = parseSkillFrontmatter(readFileSync(skillFile, "utf8"));
6602
+ if (parsed) entries.push(parsed);
6603
+ } catch {
6604
+ }
6605
+ }
6606
+ return entries.sort((a, b) => a.name.localeCompare(b.name));
6607
+ }
6608
+
6609
+ // src/commands/actions/claim.ts
6610
+ registerSchema({
6611
+ command: "actions.claim",
6612
+ description: "Claim an action for the current chat (live \u2014 visible to other chats immediately). Claim only what you're actively working on now: it's required before `complete`, but NOT for `update` or `discard` (those stage without a claim). Returns action details plus a fast-model recommendation of which skills to load for the work (`recommendedSkills`) and a routing hint: load every skill that owns part of the work and the tool doc for each baker CLI family you'll use.",
6613
+ args: {
6614
+ id: { type: "string", description: "Action ID", required: true }
6615
+ }
6616
+ });
6617
+ async function recommendSkills(actionId) {
6618
+ const skills = readSkillCatalog(process.cwd());
6619
+ if (skills.length === 0) return [];
6620
+ const response = await apiPost("/api/actions/recommend-skills", {
6621
+ actionId,
6622
+ skills
6623
+ });
6624
+ return response.data.recommendations;
6625
+ }
6626
+ var ROUTING_HINT = "Actions often span several surfaces (a landing page plus a Google Ads change, a sheet pull plus an audience upload). Before starting, load EVERY skill that owns part of the work and read the tool doc (__tooling__/docs/tools/baker/<family>.md) for each baker CLI family you'll use. If the action references source material living in a connected tool (an email attachment, a call, a CRM record, a spreadsheet), reach for that tool too. If it describes recurring or future-dated work, it belongs on a schedule \u2014 see __tooling__/docs/tools/baker/scheduled-actions.md.";
6627
+ function buildHints(recommendations) {
6628
+ if (recommendations.length === 0) {
6629
+ return ["Review the action name, description, and tags above.", ROUTING_HINT];
6630
+ }
6631
+ return [
6632
+ "Recommended skills for this action (load the ones you'll use):",
6633
+ ...recommendations.map((r) => ` /${r.name} \u2014 ${r.reason}`),
6634
+ "Suggestions come from the action's name/description/tags only \u2014 the work may need more.",
6635
+ ROUTING_HINT
6636
+ ];
6637
+ }
6638
+ var claimCommand = defineCommand({
6639
+ meta: {
6640
+ name: "claim",
6641
+ description: "Claim an action so other chats see you're working on it. Required before `complete` (update and discard don't need a claim). Example: baker actions claim <action-id>"
6642
+ },
6643
+ args: {
6644
+ id: { type: "positional", description: "Action ID", required: false },
6645
+ "action-id": { type: "string", description: "Action ID", required: false }
6646
+ },
6647
+ run: async ({ args }) => {
6648
+ try {
6649
+ const id = args.id || args["action-id"];
6650
+ if (!id) {
6651
+ failValidation("Action ID is required.");
6652
+ }
6653
+ validateConvexId(id);
6654
+ const chatId = requireChatId();
6655
+ const response = await apiPost("/api/actions/claim", {
6656
+ actionId: id,
6657
+ chatId
6658
+ });
6659
+ let recommendedSkills = [];
6660
+ try {
6661
+ recommendedSkills = await recommendSkills(id);
6662
+ } catch {
6663
+ }
6664
+ const data = response.data ? { ...response.data, recommendedSkills } : response.data;
6665
+ writeJson({ ok: response.ok, data, hints: buildHints(recommendedSkills) });
6666
+ } catch (err) {
6667
+ failApi(err);
6668
+ }
6669
+ }
6670
+ });
6671
+
6668
6672
  // src/commands/actions/complete.ts
6669
6673
  import { defineCommand as defineCommand2 } from "citty";
6670
6674
  registerSchema({
@@ -6739,7 +6743,11 @@ registerSchema({
6739
6743
  description: `User priority for the do-first ordering. One of: ${ACTION_PRIORITIES.join(", ")}.`,
6740
6744
  required: false
6741
6745
  },
6742
- "temp-id": { type: "string", description: "Custom tempId (auto-generated if omitted)", required: false }
6746
+ "temp-id": {
6747
+ type: "string",
6748
+ description: "Custom tempId \u2014 must start with `temp_` (e.g. temp_hero_copy). Auto-generated if omitted; omitting it is fine.",
6749
+ required: false
6750
+ }
6743
6751
  }
6744
6752
  });
6745
6753
  var createCommand = defineCommand3({
@@ -6752,7 +6760,7 @@ var createCommand = defineCommand3({
6752
6760
  description: { type: "string", description: "Description", required: false, default: "" },
6753
6761
  tags: { type: "string", description: "Comma-separated tag slugs (see `baker actions tags list`)", required: false },
6754
6762
  priority: { type: "string", description: `User priority: ${ACTION_PRIORITIES.join("|")}`, required: false },
6755
- "temp-id": { type: "string", description: "Optional custom tempId", required: false }
6763
+ "temp-id": { type: "string", description: "Optional custom tempId (must start with `temp_`)", required: false }
6756
6764
  },
6757
6765
  run: async ({ args }) => {
6758
6766
  try {
@@ -6760,8 +6768,15 @@ var createCommand = defineCommand3({
6760
6768
  if (!name || name.trim().length === 0) {
6761
6769
  failValidation("--name is required.");
6762
6770
  }
6771
+ const customTempId = args["temp-id"];
6772
+ if (customTempId) {
6773
+ const parsed = actionTempIdSchema.safeParse(customTempId);
6774
+ if (!parsed.success) {
6775
+ failValidation(parsed.error.issues[0]?.message ?? "Invalid --temp-id.");
6776
+ }
6777
+ }
6763
6778
  const chatId = requireChatId();
6764
- const tempId = args["temp-id"] || generateTempId();
6779
+ const tempId = customTempId || generateTempId();
6765
6780
  const tags = parseTagList(args.tags);
6766
6781
  const priority = parsePriority(args.priority, { allowClear: false });
6767
6782
  const response = await apiPost("/api/actions/create", {
@@ -43301,8 +43316,8 @@ import { defineCommand as defineCommand178 } from "citty";
43301
43316
  // src/commands/scheduled-actions/shared.ts
43302
43317
  var TEMP_SCHEDULED_ACTION_PREFIX = "temp_sched_";
43303
43318
  var TEMP_ID_PREFIX = "temp_";
43304
- function writeOk2(data) {
43305
- writeJson({ ok: true, data: data ?? null });
43319
+ function writeOk2(data, hints) {
43320
+ writeJson({ ok: true, data: data ?? null, ...hints && hints.length > 0 ? { hints } : {} });
43306
43321
  }
43307
43322
  function failValidation4(message) {
43308
43323
  writeJson({ ok: false, error: { code: "VALIDATION_ERROR", message } });
@@ -43343,10 +43358,41 @@ function isPromptWithoutAgent(args, agentDisabled) {
43343
43358
  function failIfPromptWithoutAgent(args, agentDisabled) {
43344
43359
  if (isPromptWithoutAgent(args, agentDisabled)) {
43345
43360
  failValidation4(
43346
- "--prompt only applies when an agent is spawned; it has no effect with --no-spawn-agent or --spawn-agent false."
43361
+ "--prompt only applies when a run does the work; it has no effect with --mode remind, --no-spawn-agent or --spawn-agent false."
43347
43362
  );
43348
43363
  }
43349
43364
  }
43365
+ function isModeAndSpawnAgentBothSet(args, mode) {
43366
+ const spawnAgentGiven = args["spawn-agent"] !== void 0 || args["no-spawn-agent"] === true || args.noSpawnAgent === true;
43367
+ return mode !== void 0 && spawnAgentGiven;
43368
+ }
43369
+ function failIfModeAndSpawnAgent(args, mode) {
43370
+ if (isModeAndSpawnAgentBothSet(args, mode)) {
43371
+ failValidation4("--mode and --spawn-agent/--no-spawn-agent set the same thing. Use --mode on its own.");
43372
+ }
43373
+ }
43374
+ var TASK_MODES = [
43375
+ { id: "remind", does: "Opens the Task on schedule and leaves it. Nothing runs." },
43376
+ { id: "recommend", does: "Opens a Task with what it would change and why. Touches nothing itself." },
43377
+ { id: "propose", does: "Does the work and leaves it for the user to review, then publish." },
43378
+ { id: "publish", does: "Does the work and publishes it as soon as it finishes \u2014 nobody reviews it first." }
43379
+ ];
43380
+ var TASK_MODE_IDS = TASK_MODES.map((mode) => mode.id);
43381
+ var TASK_MODE_ARG_DESCRIPTION = `How far a run of this task goes on its own. ${TASK_MODES.map(
43382
+ (mode) => `${mode.id}: ${mode.does}`
43383
+ ).join(" ")} Only set publish when the user asked for it outright \u2014 those runs go live unreviewed and can spend money.`;
43384
+ function isKnownTaskMode(raw) {
43385
+ return typeof raw === "string" && TASK_MODE_IDS.some((id) => id === raw);
43386
+ }
43387
+ function parseModeFlag(raw) {
43388
+ if (raw === void 0) {
43389
+ return void 0;
43390
+ }
43391
+ if (!isKnownTaskMode(raw)) {
43392
+ failValidation4(`--mode must be one of: ${TASK_MODE_IDS.join(", ")}.`);
43393
+ }
43394
+ return raw;
43395
+ }
43350
43396
  function parseBooleanFlag(raw, flagName) {
43351
43397
  if (raw === void 0) {
43352
43398
  return void 0;
@@ -43402,6 +43448,12 @@ registerSchema({
43402
43448
  },
43403
43449
  timezone: { type: "string", description: "IANA timezone override; defaults to Company Timezone", required: false },
43404
43450
  disabled: { type: "boolean", description: "Create disabled", required: false, default: false },
43451
+ mode: {
43452
+ type: "string",
43453
+ description: `${TASK_MODE_ARG_DESCRIPTION} Defaults to propose, or to the template's own mode with --template.`,
43454
+ required: false,
43455
+ enum: TASK_MODE_IDS
43456
+ },
43405
43457
  "no-spawn-agent": {
43406
43458
  type: "boolean",
43407
43459
  description: "Do not spawn an agent when this scheduled action fires",
@@ -43428,6 +43480,7 @@ var createCommand3 = defineCommand178({
43428
43480
  "run-at": { type: "string", description: "ISO UTC timestamp ending in Z", required: false },
43429
43481
  timezone: { type: "string", description: "IANA timezone override", required: false },
43430
43482
  disabled: { type: "boolean", description: "Create disabled", required: false, default: false },
43483
+ mode: { type: "string", description: `One of: ${TASK_MODE_IDS.join(", ")}`, required: false },
43431
43484
  "no-spawn-agent": { type: "boolean", description: "Disable agent spawning", required: false, default: false },
43432
43485
  prompt: { type: "string", description: "Additional spawned-agent instructions", required: false },
43433
43486
  template: { type: "string", description: "Template id to run on this schedule", required: false }
@@ -43445,7 +43498,9 @@ var createCommand3 = defineCommand178({
43445
43498
  const schedule = buildScheduleBody(args, { required: true });
43446
43499
  const chatId = requireChatId();
43447
43500
  const noSpawnAgent = isNoSpawnAgentFlagSet(args);
43448
- failIfPromptWithoutAgent(args, noSpawnAgent);
43501
+ const mode = parseModeFlag(args.mode);
43502
+ failIfModeAndSpawnAgent(args, mode);
43503
+ failIfPromptWithoutAgent(args, noSpawnAgent || mode === "remind");
43449
43504
  const body = {
43450
43505
  chatId,
43451
43506
  name,
@@ -43454,6 +43509,9 @@ var createCommand3 = defineCommand178({
43454
43509
  spawnAgent: !noSpawnAgent,
43455
43510
  ...schedule
43456
43511
  };
43512
+ if (mode !== void 0) {
43513
+ body.mode = mode;
43514
+ }
43457
43515
  if (typeof args.prompt === "string") {
43458
43516
  body.agentPrompt = args.prompt;
43459
43517
  }
@@ -43795,6 +43853,12 @@ registerSchema({
43795
43853
  },
43796
43854
  timezone: { type: "string", description: "IANA timezone override", required: false },
43797
43855
  enabled: { type: "string", description: "Replacement enabled state", required: false, enum: ["true", "false"] },
43856
+ mode: {
43857
+ type: "string",
43858
+ description: TASK_MODE_ARG_DESCRIPTION,
43859
+ required: false,
43860
+ enum: TASK_MODE_IDS
43861
+ },
43798
43862
  "spawn-agent": {
43799
43863
  type: "string",
43800
43864
  description: "Replacement spawn-agent state",
@@ -43807,7 +43871,7 @@ registerSchema({
43807
43871
  var updateCommand3 = defineCommand184({
43808
43872
  meta: {
43809
43873
  name: "update",
43810
- description: "Stage a scheduled action update. Example: baker scheduled-actions update <id> --enabled false"
43874
+ description: "Stage a scheduled action update. Examples: baker scheduled-actions update <id> --enabled false | baker scheduled-actions update <id> --mode publish"
43811
43875
  },
43812
43876
  args: {
43813
43877
  id: { type: "positional", description: "Scheduled action ID or temp_sched_* draft ID", required: false },
@@ -43822,6 +43886,7 @@ var updateCommand3 = defineCommand184({
43822
43886
  "run-at": { type: "string", description: "ISO UTC timestamp ending in Z", required: false },
43823
43887
  timezone: { type: "string", description: "IANA timezone override", required: false },
43824
43888
  enabled: { type: "string", description: "true|false", required: false },
43889
+ mode: { type: "string", description: `One of: ${TASK_MODE_IDS.join(", ")}`, required: false },
43825
43890
  "spawn-agent": { type: "string", description: "true|false", required: false },
43826
43891
  prompt: { type: "string", description: "Replacement additional spawned-agent instructions", required: false }
43827
43892
  },
@@ -43850,24 +43915,30 @@ var updateCommand3 = defineCommand184({
43850
43915
  body.enabled = enabled;
43851
43916
  hasPatch = true;
43852
43917
  }
43918
+ const mode = parseModeFlag(args.mode);
43919
+ failIfModeAndSpawnAgent(args, mode);
43920
+ if (mode !== void 0) {
43921
+ body.mode = mode;
43922
+ hasPatch = true;
43923
+ }
43853
43924
  const spawnAgent = parseBooleanFlag(args["spawn-agent"], "--spawn-agent");
43854
43925
  if (spawnAgent !== void 0) {
43855
43926
  body.spawnAgent = spawnAgent;
43856
43927
  hasPatch = true;
43857
43928
  }
43858
- failIfPromptWithoutAgent(args, spawnAgent === false);
43929
+ failIfPromptWithoutAgent(args, spawnAgent === false || mode === "remind");
43859
43930
  if (typeof args.prompt === "string") {
43860
43931
  body.agentPrompt = args.prompt;
43861
43932
  hasPatch = true;
43862
43933
  }
43863
43934
  if (!hasPatch) {
43864
43935
  failValidation4(
43865
- "Provide at least one of --name, --description, --cron, --run-at, --timezone, --enabled, --spawn-agent, --prompt."
43936
+ "Provide at least one of --name, --description, --cron, --run-at, --timezone, --enabled, --mode, --spawn-agent, --prompt."
43866
43937
  );
43867
43938
  }
43868
43939
  body.chatId = requireChatId();
43869
- await apiPost("/api/scheduled-actions/update", body);
43870
- writeOk2();
43940
+ const response = await apiPost("/api/scheduled-actions/update", body);
43941
+ writeOk2(null, response.hints);
43871
43942
  } catch (err) {
43872
43943
  failApi2(err);
43873
43944
  }
@@ -43887,6 +43958,7 @@ Examples:
43887
43958
  baker scheduled-actions get <id-or-temp_sched_id>
43888
43959
  baker scheduled-actions create --name "Weekly report" --description "Prepare weekly report" --cron "0 9 * * MON"
43889
43960
  baker scheduled-actions update <id-or-temp_sched_id> --enabled false
43961
+ baker scheduled-actions update <id-or-temp_sched_id> --mode publish # what a run does on its own: remind|recommend|propose|publish
43890
43962
  baker scheduled-actions delete <id-or-temp_sched_id>
43891
43963
  baker scheduled-actions trigger <id>
43892
43964
  Full guide: __tooling__/docs/tools/baker/scheduled-actions.md`