@veleads/gtm 0.2.0 → 0.3.0

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.
Files changed (43) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +72 -0
  3. package/dist/cli.js +268 -89
  4. package/kit/AGENTS.md +63 -63
  5. package/kit/providers/_template.md +1 -1
  6. package/kit/schemas/account.schema.json +2 -2
  7. package/kit/schemas/campaign-config.schema.json +3 -3
  8. package/kit/schemas/contact.schema.json +2 -2
  9. package/kit/schemas/icp.schema.json +3 -3
  10. package/kit/schemas/lead.schema.json +2 -2
  11. package/kit/schemas/{client.schema.json → project.schema.json} +7 -7
  12. package/kit/schemas/signal.schema.json +2 -2
  13. package/kit/schemas/signals.schema.json +3 -3
  14. package/kit/skills/00-intake/SKILL.md +12 -12
  15. package/kit/skills/01-icp/SKILL.md +9 -9
  16. package/kit/skills/02-tam/SKILL.md +5 -5
  17. package/kit/skills/03-enrich/SKILL.md +1 -1
  18. package/kit/skills/04-qualify/SKILL.md +4 -4
  19. package/kit/skills/05-copy/SKILL.md +6 -6
  20. package/kit/skills/06-qa/SKILL.md +2 -2
  21. package/kit/skills/07-launch/SKILL.md +3 -3
  22. package/kit/skills/08-results/SKILL.md +4 -4
  23. package/kit/templates/campaign/brief.md +1 -1
  24. package/kit/templates/campaign/config.yaml +3 -3
  25. package/kit/templates/outputs/qa-report.md +1 -1
  26. package/kit/templates/outputs/run-log.md +1 -1
  27. package/kit/templates/outputs/weekly-report.md +1 -1
  28. package/kit/templates/{client → project}/icp.md +1 -1
  29. package/kit/templates/{client → project}/icp.yaml +2 -2
  30. package/kit/templates/{client → project}/intake.md +11 -11
  31. package/kit/templates/{client → project}/offer.md +1 -1
  32. package/kit/templates/{client → project}/personas.md +1 -1
  33. package/kit/templates/project/project.yaml +12 -0
  34. package/kit/templates/{client → project}/proof.md +1 -1
  35. package/kit/templates/{client → project}/signals.yaml +2 -2
  36. package/kit/templates/{client → project}/voice.md +1 -1
  37. package/kit/templates/repo/{CLIENT.md → PROJECT.md} +5 -5
  38. package/kit/templates/repo/README.md +9 -9
  39. package/kit/templates/repo/github/workflows/gtm-upgrade.yml +10 -12
  40. package/kit/templates/repo/github/workflows/gtm.yml +14 -8
  41. package/kit/templates/repo/package.json +1 -2
  42. package/package.json +1 -1
  43. package/kit/templates/client/client.yaml +0 -12
package/dist/cli.js CHANGED
@@ -40,28 +40,32 @@ var Approval = z.object({
40
40
  );
41
41
  var FieldSources = z.record(z.string(), z.string().min(1));
42
42
 
43
- // ../schemas/src/client.ts
43
+ // ../schemas/src/project.ts
44
44
  import { z as z2 } from "zod";
45
45
 
46
46
  // ../schemas/src/versions.ts
47
47
  var FILE_VERSIONS = {
48
- client: 1,
49
- icp: 1,
50
- signals: 1,
51
- "campaign-config": 1
48
+ project: 2,
49
+ icp: 2,
50
+ signals: 2,
51
+ "campaign-config": 2
52
52
  };
53
+ var FILE_RENAMES = [
54
+ { from: "client.yaml", to: "project.yaml", since: "0.3.0" },
55
+ { from: "CLIENT.md", to: "PROJECT.md", since: "0.3.0" }
56
+ ];
53
57
 
54
- // ../schemas/src/client.ts
55
- var ClientFile = z2.object({
56
- version: z2.literal(FILE_VERSIONS.client),
57
- client_id: Slug,
58
+ // ../schemas/src/project.ts
59
+ var ProjectFile = z2.object({
60
+ version: z2.literal(FILE_VERSIONS.project),
61
+ project_id: Slug,
58
62
  name: z2.string().min(1),
59
- status: z2.enum(["onboarding", "active", "paused", "churned"]),
63
+ status: z2.enum(["onboarding", "active", "paused", "closed"]),
60
64
  started_at: IsoDate,
61
65
  owners: z2.object({
62
66
  gtm_engineer: z2.string().min(1),
63
67
  sdrs: z2.array(z2.string().min(1)),
64
- client_approver: z2.string().min(1).describe("Client-side person who signs off ICP and copy")
68
+ approver: z2.string().min(1).describe("Person who signs off ICP and copy for this project")
65
69
  }),
66
70
  regions: z2.array(z2.string().min(2)).min(1).describe("Target regions, e.g. EU, UK, US"),
67
71
  features: z2.record(z2.string(), z2.boolean()).default({}).describe("Opt-in core features, see FEATURES")
@@ -94,7 +98,7 @@ var IcpPersona = z3.object({
94
98
  });
95
99
  var IcpFile = z3.object({
96
100
  version: z3.literal(FILE_VERSIONS.icp),
97
- client_id: Slug,
101
+ project_id: Slug,
98
102
  approval: Approval,
99
103
  segments: z3.array(IcpSegment).min(1),
100
104
  personas: z3.array(IcpPersona).min(1),
@@ -124,7 +128,7 @@ var SignalDefinition = z4.object({
124
128
  });
125
129
  var SignalsFile = z4.object({
126
130
  version: z4.literal(FILE_VERSIONS.signals),
127
- client_id: Slug,
131
+ project_id: Slug,
128
132
  signals: z4.array(SignalDefinition).min(1)
129
133
  }).superRefine((f, ctx) => {
130
134
  const keys = f.signals.map((s) => s.key);
@@ -185,7 +189,7 @@ var Compliance = z5.object({
185
189
  });
186
190
  var CampaignConfig = z5.object({
187
191
  version: z5.literal(FILE_VERSIONS["campaign-config"]),
188
- client_id: Slug,
192
+ project_id: Slug,
189
193
  campaign_id: Slug,
190
194
  owner: z5.string().min(1),
191
195
  stage: Stage.describe("The stage currently being worked on"),
@@ -208,7 +212,7 @@ var CampaignConfig = z5.object({
208
212
  // ../schemas/src/account.ts
209
213
  import { z as z6 } from "zod";
210
214
  var Account = z6.object({
211
- client_id: Slug,
215
+ project_id: Slug,
212
216
  tam_id: Slug,
213
217
  domain: Domain,
214
218
  name: z6.string().min(1),
@@ -225,7 +229,7 @@ var Account = z6.object({
225
229
  import { z as z7 } from "zod";
226
230
  var EmailStatus = z7.enum(["valid", "catch_all", "invalid", "unknown", "not_found"]);
227
231
  var Contact = z7.object({
228
- client_id: Slug,
232
+ project_id: Slug,
229
233
  account_domain: Domain,
230
234
  first_name: z7.string().min(1),
231
235
  last_name: z7.string().min(1),
@@ -242,7 +246,7 @@ var Contact = z7.object({
242
246
  // ../schemas/src/signal.ts
243
247
  import { z as z8 } from "zod";
244
248
  var SignalEvent = z8.object({
245
- client_id: Slug,
249
+ project_id: Slug,
246
250
  signal_key: Slug,
247
251
  account_domain: Domain,
248
252
  contact_linkedin_url: z8.string().url().nullable(),
@@ -255,7 +259,7 @@ var SignalEvent = z8.object({
255
259
  // ../schemas/src/lead.ts
256
260
  import { z as z9 } from "zod";
257
261
  var Lead = z9.object({
258
- client_id: Slug,
262
+ project_id: Slug,
259
263
  campaign_id: Slug,
260
264
  run_id: z9.uuid(),
261
265
  email: z9.email(),
@@ -282,7 +286,27 @@ var FEATURES = {
282
286
  };
283
287
 
284
288
  // ../schemas/src/migrations.ts
285
- var MIGRATIONS = [];
289
+ import { isMap, isScalar } from "yaml";
290
+ function renameKey(doc, path, from, to) {
291
+ const node = path.length ? doc.getIn(path, true) : doc.contents;
292
+ if (!isMap(node)) return;
293
+ const pair = node.items.find((p) => (isScalar(p.key) ? p.key.value : p.key) === from);
294
+ if (!pair) return;
295
+ if (isScalar(pair.key)) pair.key.value = to;
296
+ else pair.key = to;
297
+ }
298
+ var renameClientId = (doc) => renameKey(doc, [], "client_id", "project_id");
299
+ var MIGRATIONS = [
300
+ // 0.3.0: "clients" became "projects" (one private repo per project).
301
+ { kind: "project", from: 1, description: "client_id \u2192 project_id, owners.client_approver \u2192 owners.approver, status churned \u2192 closed", up(doc) {
302
+ renameClientId(doc);
303
+ renameKey(doc, ["owners"], "client_approver", "approver");
304
+ if (doc.get("status") === "churned") doc.set("status", "closed");
305
+ } },
306
+ { kind: "icp", from: 1, description: "client_id \u2192 project_id", up: renameClientId },
307
+ { kind: "signals", from: 1, description: "client_id \u2192 project_id", up: renameClientId },
308
+ { kind: "campaign-config", from: 1, description: "client_id \u2192 project_id", up: renameClientId }
309
+ ];
286
310
  function migrateDocument(kind, doc, migrations = MIGRATIONS, target = FILE_VERSIONS[kind]) {
287
311
  const raw = doc.get("version");
288
312
  const from = typeof raw === "number" ? raw : Number(raw);
@@ -321,11 +345,11 @@ function copyContentHash(markdown) {
321
345
 
322
346
  // src/commands/approve.ts
323
347
  var USAGE = `Usage:
324
- gtm approve icp --by "<Name (side)>" [--notes "<what was approved>"] [--date YYYY-MM-DD]
325
- gtm approve copy <campaign> --by "<Name (side)>" [--notes "<what was approved>"] [--date YYYY-MM-DD]`;
348
+ gtm approve icp --by "<Name (role)>" [--notes "<what was approved>"] [--date YYYY-MM-DD]
349
+ gtm approve copy <campaign> --by "<Name (role)>" [--notes "<what was approved>"] [--date YYYY-MM-DD]`;
326
350
  function load(file) {
327
351
  if (!existsSync(file)) {
328
- console.error(`${file} not found: run this from the root of a client repo`);
352
+ console.error(`${file} not found: run this from the root of a project repo`);
329
353
  return null;
330
354
  }
331
355
  const doc = parseDocument(readFileSync(file, "utf8"));
@@ -361,7 +385,7 @@ function runApprove(root2, args2, opts) {
361
385
  }
362
386
  const by = opts.by?.trim();
363
387
  if (!by) {
364
- console.error('--by is required: the human who approved, e.g. --by "Maria Chen (client)"');
388
+ console.error('--by is required: the human who approved, e.g. --by "Maria Chen (VP Marketing)"');
365
389
  return false;
366
390
  }
367
391
  const date = opts.date ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
@@ -502,7 +526,7 @@ import { dirname as dirname3, join as join5 } from "path";
502
526
  var SHIPPED_DIRS = ["skills", "templates", "providers", "schemas"];
503
527
  function agentsMd() {
504
528
  const header = `<!-- GENERATED by \`gtm sync\` from ${PACKAGE_NAME} v${GTM_VERSION}. Do not edit: it is overwritten on upgrade.
505
- Client-specific rules go in CLIENT.md. Process changes go to the Veleads/gtm-core repo. -->
529
+ Project-specific rules go in PROJECT.md. Process changes go to the Veleads/gtm-core repo. -->
506
530
 
507
531
  `;
508
532
  return header + readFileSync4(join5(KIT_DIR, "AGENTS.md"), "utf8").replaceAll("{{GTM_VERSION}}", GTM_VERSION);
@@ -545,7 +569,7 @@ function runSync(root2, opts = {}) {
545
569
  console.log(`in sync with ${PACKAGE_NAME} v${GTM_VERSION}`);
546
570
  return true;
547
571
  }
548
- console.log(`out of sync with ${PACKAGE_NAME} v${GTM_VERSION} (run \`pnpm gtm sync\`; never edit .gtm/ or AGENTS.md by hand):`);
572
+ console.log(`out of sync with ${PACKAGE_NAME} v${GTM_VERSION} (run \`npx gtm sync\`; never edit .gtm/ or AGENTS.md by hand):`);
549
573
  for (const d of diffs) console.log(` ${d}`);
550
574
  return false;
551
575
  }
@@ -562,18 +586,18 @@ function runSync(root2, opts = {}) {
562
586
  // src/commands/init.ts
563
587
  function runInit(slug, opts) {
564
588
  if (!slug || !Slug.safeParse(slug).success || !opts.name) {
565
- console.error('Usage: gtm init <client-slug> --name "<Client Name>" [--dir <path>] (slug: lowercase, digits, hyphens)');
589
+ console.error('Usage: gtm init <project-slug> --name "<Project Name>" [--dir <path>] (slug: lowercase, digits, hyphens)');
566
590
  return false;
567
591
  }
568
- const root2 = resolve(opts.cwd, opts.dir ?? `gtm-client-${slug}`);
592
+ const root2 = resolve(opts.cwd, opts.dir ?? `gtm-project-${slug}`);
569
593
  if (existsSync4(root2) && readdirSync2(root2).length > 0) {
570
594
  console.error(`${root2} exists and is not empty`);
571
595
  return false;
572
596
  }
573
- const vars = { CLIENT_ID: slug, CLIENT_NAME: opts.name, TODAY: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), GTM_VERSION };
597
+ const vars = { PROJECT_ID: slug, PROJECT_NAME: opts.name, TODAY: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), GTM_VERSION };
574
598
  const tpl = join6(KIT_DIR, "templates");
575
599
  copyTemplate(join6(tpl, "repo"), root2, vars);
576
- copyTemplate(join6(tpl, "client"), root2, vars, { rename: (rel) => rel === "client.yaml" ? rel : join6("context", rel) });
600
+ copyTemplate(join6(tpl, "project"), root2, vars, { rename: (rel) => rel === "project.yaml" ? rel : join6("context", rel) });
577
601
  for (const d of ["campaigns", "tam"]) {
578
602
  mkdirSync3(join6(root2, d), { recursive: true });
579
603
  writeFileSync4(join6(root2, d, ".gitkeep"), "");
@@ -584,9 +608,9 @@ function runInit(slug, opts) {
584
608
  Created ${root2} (core v${GTM_VERSION})${git.status === 0 ? " with git initialised" : ""}.
585
609
 
586
610
  Next:
587
- 1. Create a PRIVATE GitHub repo for it (e.g. <your-org>/gtm-client-${slug}) and push this folder.
588
- Client repos hold client context: never make them public.
589
- 2. pnpm install && pnpm gtm validate (TODO warnings remain until intake is done)
611
+ 1. Create a PRIVATE GitHub repo for it (e.g. <your-org>/gtm-project-${slug}) and push this folder.
612
+ Project repos hold project context: never make them public.
613
+ 2. npm install && npx gtm validate (or pnpm/yarn; TODO warnings remain until intake is done)
590
614
  3. Open the repo in Claude Code or Codex and start with stage 00-intake.`);
591
615
  return true;
592
616
  }
@@ -602,22 +626,39 @@ Per AGENTS.md this is a mandatory stop: do not improvise the job with ad-hoc scr
602
626
  }
603
627
 
604
628
  // src/commands/migrate.ts
605
- import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "fs";
629
+ import { existsSync as existsSync5, readFileSync as readFileSync5, renameSync, writeFileSync as writeFileSync5 } from "fs";
606
630
  import { join as join7, relative as relative2 } from "path";
607
631
  import { parseDocument as parseDocument2 } from "yaml";
608
- function clientYamlFiles(root2) {
632
+ function projectYamlFiles(root2) {
609
633
  const files = [
610
- { kind: "client", file: join7(root2, "client.yaml") },
634
+ { kind: "project", file: join7(root2, "project.yaml") },
611
635
  { kind: "icp", file: join7(root2, "context", "icp.yaml") },
612
636
  { kind: "signals", file: join7(root2, "context", "signals.yaml") },
613
637
  ...subdirs(join7(root2, "campaigns")).map((c) => ({ kind: "campaign-config", file: join7(root2, "campaigns", c, "config.yaml") }))
614
638
  ];
615
639
  return files.filter((f) => existsSync5(f.file));
616
640
  }
641
+ function pendingRenames(root2) {
642
+ return FILE_RENAMES.filter((r) => existsSync5(join7(root2, r.from)) && !existsSync5(join7(root2, r.to)));
643
+ }
617
644
  function runMigrate(root2, opts = {}) {
618
645
  let ok = true;
619
646
  let changed = 0;
620
- for (const { kind, file } of clientYamlFiles(root2)) {
647
+ for (const { from, to } of pendingRenames(root2)) {
648
+ changed++;
649
+ console.log(` ${opts.dryRun ? "would rename" : "renamed"} ${from} \u2192 ${to}`);
650
+ if (opts.dryRun) continue;
651
+ renameSync(join7(root2, from), join7(root2, to));
652
+ if (to.endsWith(".md")) {
653
+ const p = join7(root2, to);
654
+ writeFileSync5(p, readFileSync5(p, "utf8").replace(new RegExp(`^# ${from.replace(".", "\\.")}`, "m"), `# ${to}`));
655
+ }
656
+ }
657
+ if (opts.dryRun && changed) {
658
+ console.log("(dry run: YAML migrations are listed after the renames are applied)");
659
+ return ok;
660
+ }
661
+ for (const { kind, file } of projectYamlFiles(root2)) {
621
662
  const rel = relative2(root2, file);
622
663
  const doc = parseDocument2(readFileSync5(file, "utf8"));
623
664
  if (doc.errors.length) {
@@ -634,7 +675,7 @@ function runMigrate(root2, opts = {}) {
634
675
  for (const a of res.applied) console.log(` ${opts.dryRun ? "would apply" : "applied"} ${rel}: ${a}`);
635
676
  if (pinned) {
636
677
  doc.setIn(["approval", "content_hash"], icpContentHash(doc.toJS()));
637
- console.log(` re-pinned ICP approval hash in ${rel} (core migration, content approved by the client)`);
678
+ console.log(` re-pinned ICP approval hash in ${rel} (core migration, content approved by the project approver)`);
638
679
  }
639
680
  if (!opts.dryRun) writeFileSync5(file, doc.toString({ flowCollectionPadding: false, lineWidth: 0 }));
640
681
  } catch (e) {
@@ -655,9 +696,9 @@ function runNewCampaign(root2, campaign) {
655
696
  console.error("Usage: gtm new-campaign <campaign-slug>");
656
697
  return false;
657
698
  }
658
- const clientFile = join8(root2, "client.yaml");
659
- if (!existsSync6(clientFile)) {
660
- console.error("client.yaml not found: run this from the root of a client repo");
699
+ const projectFile = join8(root2, "project.yaml");
700
+ if (!existsSync6(projectFile)) {
701
+ console.error("project.yaml not found: run this from the root of a project repo");
661
702
  return false;
662
703
  }
663
704
  const dest = join8(root2, "campaigns", campaign);
@@ -665,8 +706,8 @@ function runNewCampaign(root2, campaign) {
665
706
  console.error(`campaigns/${campaign} already exists`);
666
707
  return false;
667
708
  }
668
- const clientId = parse(readFileSync6(clientFile, "utf8"))?.client_id ?? "TODO";
669
- copyTemplate(join8(KIT_DIR, "templates", "campaign"), dest, { CLIENT_ID: clientId, CAMPAIGN_ID: campaign, OWNER: process.env.GTM_USER ?? "TODO" });
709
+ const projectId = parse(readFileSync6(projectFile, "utf8"))?.project_id ?? "TODO";
710
+ copyTemplate(join8(KIT_DIR, "templates", "campaign"), dest, { PROJECT_ID: projectId, CAMPAIGN_ID: campaign, OWNER: process.env.GTM_USER ?? "TODO" });
670
711
  mkdirSync4(join8(dest, "runs"), { recursive: true });
671
712
  writeFileSync6(join8(dest, "runs", ".gitkeep"), "");
672
713
  console.log(`Created campaigns/${campaign}. Branch: git switch -c campaign/${campaign}`);
@@ -688,9 +729,9 @@ function campaignStages(root2) {
688
729
  return subdirs(join9(root2, "campaigns")).map((id) => ({ id, stage: readYaml(join9(root2, "campaigns", id, "config.yaml"))?.stage ?? "?" }));
689
730
  }
690
731
  function runStatus(root2) {
691
- const client = readYaml(join9(root2, "client.yaml"));
732
+ const project = readYaml(join9(root2, "project.yaml"));
692
733
  const icp = readYaml(join9(root2, "context", "icp.yaml"));
693
- console.log(`${client?.name ?? "?"} (${client?.client_id ?? "?"}) \xB7 status ${client?.status ?? "?"} \xB7 core v${GTM_VERSION}`);
734
+ console.log(`${project?.name ?? "?"} (${project?.project_id ?? "?"}) \xB7 status ${project?.status ?? "?"} \xB7 core v${GTM_VERSION}`);
694
735
  console.log(`ICP gate: ${icp?.approval?.status ?? "?"}${icp?.approval?.approved_by ? ` by ${icp.approval.approved_by} on ${icp.approval.approved_at}` : ""}
695
736
  `);
696
737
  const rows = subdirs(join9(root2, "campaigns")).map((id) => {
@@ -700,7 +741,7 @@ function runStatus(root2) {
700
741
  return [id, cfg.stage ?? "?", cfg.gates?.copy?.status ?? "?", cfg.gates?.launch?.status ?? "?", cfg.owner ?? "?", last];
701
742
  });
702
743
  if (!rows.length) {
703
- console.log("no campaigns yet: pnpm gtm new-campaign <campaign-slug>");
744
+ console.log("no campaigns yet: npx gtm new-campaign <campaign-slug>");
704
745
  return true;
705
746
  }
706
747
  const header = ["campaign", "stage", "copy gate", "launch gate", "owner", "latest run log"];
@@ -766,8 +807,8 @@ function loadVersioned(r, file, kind, schema) {
766
807
  }
767
808
  const expected = FILE_VERSIONS[kind];
768
809
  if (raw?.version !== expected) {
769
- if (typeof raw?.version === "number" && raw.version < expected) r.err(file, `is version ${raw.version}, core v${GTM_VERSION} expects ${expected}: run \`pnpm gtm migrate\``);
770
- else if (typeof raw?.version === "number") r.err(file, `is version ${raw.version}, newer than core v${GTM_VERSION} (${expected}): run \`pnpm gtm upgrade\``);
810
+ if (typeof raw?.version === "number" && raw.version < expected) r.err(file, `is version ${raw.version}, core v${GTM_VERSION} expects ${expected}: run \`npx gtm migrate\``);
811
+ else if (typeof raw?.version === "number") r.err(file, `is version ${raw.version}, newer than core v${GTM_VERSION} (${expected}): run \`npx gtm upgrade\``);
771
812
  else r.err(file, `missing "version: ${expected}"`);
772
813
  return null;
773
814
  }
@@ -784,14 +825,14 @@ function checkCoreSync(r, root2) {
784
825
  if (!/^\d+\.\d+\.\d+/.test(pinned)) r.err(join10(root2, "package.json"), `${PACKAGE_NAME} must be pinned exactly (got "${pinned}")`);
785
826
  const versionFile = join10(root2, ".gtm", "VERSION");
786
827
  const synced = existsSync8(versionFile) ? readFileSync8(versionFile, "utf8").trim() : null;
787
- if (synced !== GTM_VERSION) r.err(versionFile, `is ${synced ?? "missing"}, installed core is ${GTM_VERSION}: run \`pnpm gtm sync\``);
788
- if (!existsSync8(join10(root2, "AGENTS.md"))) r.err(join10(root2, "AGENTS.md"), "missing: run `pnpm gtm sync`");
828
+ if (synced !== GTM_VERSION) r.err(versionFile, `is ${synced ?? "missing"}, installed core is ${GTM_VERSION}: run \`npx gtm sync\``);
829
+ if (!existsSync8(join10(root2, "AGENTS.md"))) r.err(join10(root2, "AGENTS.md"), "missing: run `npx gtm sync`");
789
830
  }
790
831
  function checkNoLeadData(r, root2) {
791
832
  for (const rel of walk(root2, SKIP_DIRS)) {
792
833
  const file = join10(root2, rel);
793
834
  if (DATA_FILES.test(rel)) {
794
- r.err(file, "data files are not allowed in a client repo (lead data belongs in Postgres)");
835
+ r.err(file, "data files are not allowed in a project repo (lead data belongs in Postgres)");
795
836
  continue;
796
837
  }
797
838
  if (!/\.(md|ya?ml|txt)$/i.test(rel) || rel === "pnpm-lock.yaml") continue;
@@ -811,14 +852,14 @@ function checkPinnedApproval(r, contentFile, approval, current, what, approveCmd
811
852
  r.err(contentFile, `${what} changed after approval (approved ${pinned}, now ${current}). Re-present it to the approver and record it with \`${approveCmd} --by "<name>"\`, or revert the change`);
812
853
  return false;
813
854
  }
814
- function validateCampaign(r, root2, clientId, campaign, icp, signals, icpApproved) {
855
+ function validateCampaign(r, root2, projectId, campaign, icp, signals, icpApproved) {
815
856
  const dir = join10(root2, "campaigns", campaign);
816
857
  console.log(`campaign ${campaign}`);
817
858
  for (const f of REQUIRED_CAMPAIGN) if (!existsSync8(join10(dir, f))) r.err(join10(dir, f), "missing");
818
859
  const cfgFile = join10(dir, "config.yaml");
819
860
  const cfg = loadVersioned(r, cfgFile, "campaign-config", CampaignConfig);
820
861
  if (cfg) {
821
- if (cfg.client_id !== clientId) r.err(cfgFile, `client_id "${cfg.client_id}" does not match client.yaml "${clientId}"`);
862
+ if (cfg.project_id !== projectId) r.err(cfgFile, `project_id "${cfg.project_id}" does not match project.yaml "${projectId}"`);
822
863
  if (cfg.campaign_id !== campaign) r.err(cfgFile, `campaign_id "${cfg.campaign_id}" does not match folder "${campaign}"`);
823
864
  if (icp) {
824
865
  const segs = new Set(icp.segments.map((s) => s.id));
@@ -841,7 +882,7 @@ function validateCampaign(r, root2, clientId, campaign, icp, signals, icpApprove
841
882
  r.err(cfgFile, `stage is "${cfg.stage}" but the ICP gate (context/icp.yaml approval) is not approved, or the ICP changed since approval`);
842
883
  }
843
884
  const copyFile = join10(dir, "copy.md");
844
- const copyApproved = existsSync8(copyFile) ? checkPinnedApproval(r, copyFile, cfg.gates.copy, copyContentHash(readFileSync8(copyFile, "utf8")), "copy", `pnpm gtm approve copy ${campaign}`) : false;
885
+ const copyApproved = existsSync8(copyFile) ? checkPinnedApproval(r, copyFile, cfg.gates.copy, copyContentHash(readFileSync8(copyFile, "utf8")), "copy", `npx gtm approve copy ${campaign}`) : false;
845
886
  if (stageIndex(cfg.stage) > stageIndex("copy") && !copyApproved) {
846
887
  r.err(cfgFile, `stage is "${cfg.stage}" but gates.copy is not approved, or copy.md changed since approval`);
847
888
  }
@@ -866,22 +907,23 @@ function runValidate(root2, onlyCampaign) {
866
907
  const r = new Report(root2);
867
908
  console.log(`validating ${root2} against core v${GTM_VERSION}`);
868
909
  checkCoreSync(r, root2);
869
- const meta = loadVersioned(r, join10(root2, "client.yaml"), "client", ClientFile);
870
- const clientId = meta?.client_id ?? "";
910
+ for (const { from, to } of pendingRenames(root2)) r.err(join10(root2, from), `renamed to ${to} in core 0.3.0: run \`npx gtm migrate\``);
911
+ const meta = loadVersioned(r, join10(root2, "project.yaml"), "project", ProjectFile);
912
+ const projectId = meta?.project_id ?? "";
871
913
  if (meta) {
872
914
  for (const key of Object.keys(meta.features)) {
873
- if (!FEATURES[key]) r.err(join10(root2, "client.yaml"), `unknown feature "${key}" (known: ${Object.keys(FEATURES).join(", ") || "none"})`);
915
+ if (!FEATURES[key]) r.err(join10(root2, "project.yaml"), `unknown feature "${key}" (known: ${Object.keys(FEATURES).join(", ") || "none"})`);
874
916
  }
875
917
  }
876
- if (!existsSync8(join10(root2, "CLIENT.md"))) r.warn(join10(root2, "CLIENT.md"), "missing (client-specific rules for agents)");
918
+ if (!existsSync8(join10(root2, "PROJECT.md"))) r.warn(join10(root2, "PROJECT.md"), "missing (project-specific rules for agents)");
877
919
  for (const f of REQUIRED_CONTEXT) if (!existsSync8(join10(root2, "context", f))) r.err(join10(root2, "context", f), "missing");
878
920
  const icpFile = join10(root2, "context", "icp.yaml");
879
921
  const sigFile = join10(root2, "context", "signals.yaml");
880
922
  const icp = existsSync8(icpFile) ? loadVersioned(r, icpFile, "icp", IcpFile) : null;
881
923
  const signals = existsSync8(sigFile) ? loadVersioned(r, sigFile, "signals", SignalsFile) : null;
882
- if (meta && icp && icp.client_id !== clientId) r.err(icpFile, `client_id "${icp.client_id}" does not match client.yaml "${clientId}"`);
883
- if (meta && signals && signals.client_id !== clientId) r.err(sigFile, `client_id "${signals.client_id}" does not match client.yaml "${clientId}"`);
884
- const icpApproved = icp ? checkPinnedApproval(r, icpFile, icp.approval, icpContentHash(parse3(readFileSync8(icpFile, "utf8"))), "ICP", "pnpm gtm approve icp") : false;
924
+ if (meta && icp && icp.project_id !== projectId) r.err(icpFile, `project_id "${icp.project_id}" does not match project.yaml "${projectId}"`);
925
+ if (meta && signals && signals.project_id !== projectId) r.err(sigFile, `project_id "${signals.project_id}" does not match project.yaml "${projectId}"`);
926
+ const icpApproved = icp ? checkPinnedApproval(r, icpFile, icp.approval, icpContentHash(parse3(readFileSync8(icpFile, "utf8"))), "ICP", "npx gtm approve icp") : false;
885
927
  checkNoLeadData(r, root2);
886
928
  const campaigns = onlyCampaign ? [onlyCampaign] : subdirs(join10(root2, "campaigns"));
887
929
  for (const c of campaigns) {
@@ -889,24 +931,31 @@ function runValidate(root2, onlyCampaign) {
889
931
  r.err(join10(root2, "campaigns", c), "campaign folder not found");
890
932
  continue;
891
933
  }
892
- validateCampaign(r, root2, clientId, c, icp, signals, icpApproved);
934
+ validateCampaign(r, root2, projectId, c, icp, signals, icpApproved);
893
935
  }
894
936
  r.summary();
895
937
  return r.errors === 0;
896
938
  }
897
939
 
898
940
  // src/commands/upgrade.ts
899
- function pm(root2, args2) {
900
- const onPath = spawnSync2("pnpm", ["--version"], { stdio: "ignore" }).status === 0;
901
- const pinned = JSON.parse(readFileSync9(join11(root2, "package.json"), "utf8")).packageManager ?? "pnpm@10";
902
- const [cmd, pre] = onPath ? ["pnpm", []] : ["npx", ["-y", pinned]];
903
- return spawnSync2(cmd, [...pre, ...args2], { cwd: root2, stdio: "inherit" }).status ?? 1;
941
+ function detectPackageManager(root2) {
942
+ if (existsSync9(join11(root2, "pnpm-lock.yaml"))) return "pnpm";
943
+ if (existsSync9(join11(root2, "yarn.lock"))) return "yarn";
944
+ return "npm";
945
+ }
946
+ function installExact(root2, pm, spec) {
947
+ const onPath = (bin) => spawnSync2(bin, ["--version"], { stdio: "ignore" }).status === 0;
948
+ const [cmd, args2] = pm === "npm" ? ["npm", ["install", "--save-dev", "--save-exact", spec]] : pm === "pnpm" ? onPath("pnpm") ? ["pnpm", ["add", "-D", "--save-exact", spec]] : ["npx", ["-y", "pnpm@10", "add", "-D", "--save-exact", spec]] : onPath("yarn") ? ["yarn", ["add", "-D", "--exact", spec]] : ["npx", ["-y", "yarn@1", "add", "-D", "--exact", spec]];
949
+ return spawnSync2(cmd, args2, { cwd: root2, stdio: "inherit" }).status ?? 1;
950
+ }
951
+ function gtm(root2, args2) {
952
+ return spawnSync2("npx", ["gtm", ...args2], { cwd: root2, stdio: "inherit" }).status ?? 1;
904
953
  }
905
954
  var FROZEN_STAGES = ["qa", "launch"];
906
955
  function runUpgrade(root2, opts) {
907
956
  const before = pinnedVersion(root2);
908
957
  if (!before) {
909
- console.error(`package.json does not depend on ${PACKAGE_NAME}: is this a client repo?`);
958
+ console.error(`package.json does not depend on ${PACKAGE_NAME}: is this a project repo?`);
910
959
  return false;
911
960
  }
912
961
  const frozen = campaignStages(root2).filter((c) => FROZEN_STAGES.includes(c.stage));
@@ -921,11 +970,12 @@ function runUpgrade(root2, opts) {
921
970
  return false;
922
971
  }
923
972
  const target = opts.version ?? "latest";
924
- console.log(`Upgrading ${PACKAGE_NAME} ${before} \u2192 ${target}`);
925
- if (pm(root2, ["add", "-D", "--save-exact", `${PACKAGE_NAME}@${target}`]) !== 0) return false;
926
- if (pm(root2, ["exec", "gtm", "sync"]) !== 0) return false;
927
- if (pm(root2, ["exec", "gtm", "migrate"]) !== 0) return false;
928
- const valid = pm(root2, ["exec", "gtm", "validate"]) === 0;
973
+ const pm = detectPackageManager(root2);
974
+ console.log(`Upgrading ${PACKAGE_NAME} ${before} \u2192 ${target} (with ${pm})`);
975
+ if (installExact(root2, pm, `${PACKAGE_NAME}@${target}`) !== 0) return false;
976
+ if (gtm(root2, ["sync"]) !== 0) return false;
977
+ if (gtm(root2, ["migrate"]) !== 0) return false;
978
+ const valid = gtm(root2, ["validate"]) === 0;
929
979
  const after = pinnedVersion(root2) ?? target;
930
980
  const changelog = join11(root2, "node_modules", ...PACKAGE_NAME.split("/"), "CHANGELOG.md");
931
981
  if (existsSync9(changelog)) {
@@ -939,21 +989,134 @@ ${notes || "(no changelog entries)"}
939
989
  return valid;
940
990
  }
941
991
 
992
+ // src/help.ts
993
+ var COMMANDS = {
994
+ init: {
995
+ usage: 'gtm init <project-slug> --name "<Project Name>" [--dir <path>]',
996
+ summary: "scaffold a new project repo",
997
+ details: [
998
+ "Creates a complete project repo: project.yaml, context/ templates, campaigns/, tam/, PROJECT.md,",
999
+ "CI workflows, .gtm/ (core kit) and AGENTS.md, pinned to this CLI's version, then runs git init.",
1000
+ "Push the result to a PRIVATE repo: it will hold project context."
1001
+ ],
1002
+ writes: "A new folder (default ./gtm-project-<slug>). Refuses if it exists and is not empty.",
1003
+ examples: ['npx @veleads/gtm init acme --name "Acme Corp"', 'npx @veleads/gtm init acme --name "Acme Corp" --dir ./projects/acme']
1004
+ },
1005
+ "new-campaign": {
1006
+ usage: "gtm new-campaign <campaign-slug>",
1007
+ summary: "scaffold campaigns/<slug>/",
1008
+ details: ["Copies the campaign template (brief.md, config.yaml, copy.md, runs/). Owner comes from GTM_USER."],
1009
+ writes: "campaigns/<slug>/ (refuses if it exists).",
1010
+ examples: ["git switch -c campaign/q1-uk-cfo && npx gtm new-campaign q1-uk-cfo"]
1011
+ },
1012
+ validate: {
1013
+ usage: "gtm validate [campaign]",
1014
+ summary: "check repo against schemas, gates, data rules",
1015
+ details: [
1016
+ "Checks project.yaml, context/ and campaigns against the schemas; segment/persona/signal references;",
1017
+ "gate order (no stage past an unapproved or changed ICP/copy approval); required files; run-log names;",
1018
+ "no lead data (blocks data files, warns on email addresses); .gtm/ matches the installed core.",
1019
+ "Exit code 1 if there is any error; warnings never fail."
1020
+ ],
1021
+ writes: "Nothing (read-only).",
1022
+ examples: ["npx gtm validate", "npx gtm validate q1-uk-cfo"]
1023
+ },
1024
+ status: {
1025
+ usage: "gtm status",
1026
+ summary: "campaigns, stages, gates",
1027
+ details: ["One screen: project, core version, ICP gate, and per campaign the stage, copy/launch gates, owner and latest run log."],
1028
+ writes: "Nothing (read-only).",
1029
+ examples: ["npx gtm status"]
1030
+ },
1031
+ approve: {
1032
+ usage: 'gtm approve icp --by "<Name (role)>" [--notes ".."] [--date YYYY-MM-DD]\ngtm approve copy <campaign> --by "<Name (role)>" [--notes ".."] [--date YYYY-MM-DD]',
1033
+ summary: "record a sign-off and pin a hash of the approved content",
1034
+ details: [
1035
+ "Run ONLY after the named human explicitly approved in this session (AGENTS.md section 4).",
1036
+ "Pins a content hash: if context/icp.yaml or the campaign's copy.md changes afterwards,",
1037
+ "`gtm validate` fails until it is re-approved. Refuses if the file would not validate.",
1038
+ "The launch gate is recorded by hand in config.yaml (its content is the lead set)."
1039
+ ],
1040
+ writes: "context/icp.yaml \u2192 approval (icp), or campaigns/<campaign>/config.yaml \u2192 gates.copy (copy).",
1041
+ examples: ['npx gtm approve icp --by "Maria Chen (VP Marketing)" --notes "Segments A+B, personas CFO+AP"', 'npx gtm approve copy q1-uk-cfo --by "Maria Chen (VP Marketing)"']
1042
+ },
1043
+ sync: {
1044
+ usage: "gtm sync [--check]",
1045
+ summary: "copy core kit into .gtm/ + AGENTS.md",
1046
+ details: [
1047
+ "Makes .gtm/ (skills, templates, schemas, provider docs) and AGENTS.md match the installed core.",
1048
+ "--check only compares and exits 1 if they differ (CI uses this; never edit .gtm/ by hand)."
1049
+ ],
1050
+ writes: ".gtm/ (replaced entirely) and AGENTS.md; creates CLAUDE.md if missing. With --check: nothing.",
1051
+ examples: ["npx gtm sync", "npx gtm sync --check"]
1052
+ },
1053
+ migrate: {
1054
+ usage: "gtm migrate [--dry-run]",
1055
+ summary: "upgrade YAML files to current schema versions",
1056
+ details: ["Applies core schema migrations to project.yaml, context/*.yaml and campaigns/*/config.yaml, keeping comments."],
1057
+ writes: "Those YAML files, only if a migration applies. With --dry-run: nothing.",
1058
+ examples: ["npx gtm migrate --dry-run", "npx gtm migrate"]
1059
+ },
1060
+ upgrade: {
1061
+ usage: "gtm upgrade [version] [--force]",
1062
+ summary: "install newer core, sync, migrate, validate",
1063
+ details: [
1064
+ "Installs @veleads/gtm@<version|latest> pinned exactly, then sync, migrate and validate, and prints the changelog.",
1065
+ "Refuses while a campaign is in qa or launch, or with uncommitted changes (--force overrides; ask a GTM engineer)."
1066
+ ],
1067
+ writes: "package.json, lockfile, node_modules, .gtm/, AGENTS.md and migrated YAML files.",
1068
+ examples: ["npx gtm upgrade", "npx gtm upgrade 0.3.0"]
1069
+ },
1070
+ job: {
1071
+ usage: "gtm job <name> <campaign>",
1072
+ summary: "run a pipeline job (Phase 2)",
1073
+ details: [
1074
+ "Planned: tam, enrich, suppression:import, signals, qualify, qa, push, activate, results, status.",
1075
+ "Not available yet: it exits 1. Per AGENTS.md this is a stop; do not improvise jobs with ad-hoc scripts."
1076
+ ],
1077
+ writes: "Nothing yet.",
1078
+ examples: ["npx gtm job tam q1-uk-cfo"]
1079
+ }
1080
+ };
1081
+ function generalHelp(version) {
1082
+ const width = Math.max(...Object.keys(COMMANDS).map((c) => c.length));
1083
+ const lines = Object.entries(COMMANDS).map(([name, c]) => ` ${name.padEnd(width)} ${c.summary}`);
1084
+ return `gtm v${version}: CLI for gtm-brain project repos (@veleads/gtm)
1085
+
1086
+ Usage: gtm <command> [options] (inside a project repo: npx gtm <command>)
1087
+
1088
+ Commands:
1089
+ ${lines.join("\n")}
1090
+
1091
+ Run \`gtm <command> --help\` for usage, what it writes, and examples.
1092
+
1093
+ Global options:
1094
+ --root <dir> repo root (default: current directory)
1095
+ -h, --help show help (with a command: that command's help; nothing is run)
1096
+ -v, --version print the version
1097
+
1098
+ Exit codes: 0 success, 1 failure or validation errors.
1099
+
1100
+ For coding agents: inside a project repo, read AGENTS.md first and follow it. Read-only commands
1101
+ (validate, status, sync --check, migrate --dry-run) are always safe to run.`;
1102
+ }
1103
+ function commandHelp(name) {
1104
+ const c = COMMANDS[name];
1105
+ if (!c) return null;
1106
+ return `gtm ${name}: ${c.summary}
1107
+
1108
+ Usage:
1109
+ ${c.usage.split("\n").map((u) => ` ${u}`).join("\n")}
1110
+
1111
+ ${c.details.map((d) => ` ${d}`).join("\n")}
1112
+
1113
+ Writes: ${c.writes}
1114
+
1115
+ Examples:
1116
+ ${c.examples.map((e) => ` ${e}`).join("\n")}`;
1117
+ }
1118
+
942
1119
  // src/cli.ts
943
- var HELP = `gtm v${GTM_VERSION}: gtm-brain core CLI (run inside a client repo)
944
-
945
- gtm init <client-slug> --name "<Name>" [--dir <path>] scaffold a new client repo
946
- gtm new-campaign <campaign-slug> scaffold campaigns/<slug>/
947
- gtm validate [campaign] check repo against schemas, gates, data rules
948
- gtm status campaigns, stages, gates
949
- gtm approve icp --by "<Name (side)>" [--notes ..] [--date YYYY-MM-DD]
950
- gtm approve copy <campaign> --by "<Name (side)>" [..] record a sign-off and pin a hash of the approved content
951
- gtm sync [--check] copy core kit into .gtm/ + AGENTS.md
952
- gtm migrate [--dry-run] upgrade YAML files to current schema versions
953
- gtm upgrade [version] [--force] install newer core, sync, migrate, validate
954
- gtm job <name> <campaign> run a pipeline job (Phase 2)
955
-
956
- Global: --root <dir> repo root (default: current directory)`;
957
1120
  var { values, positionals } = parseArgs({
958
1121
  allowPositionals: true,
959
1122
  options: {
@@ -973,11 +1136,27 @@ var { values, positionals } = parseArgs({
973
1136
  var [command, ...args] = positionals;
974
1137
  var cwd = process.cwd();
975
1138
  var root = resolve2(cwd, values.root ?? ".");
1139
+ function help(name) {
1140
+ if (!name) {
1141
+ console.log(generalHelp(GTM_VERSION));
1142
+ return true;
1143
+ }
1144
+ const text = commandHelp(name);
1145
+ if (!text) {
1146
+ console.error(`Unknown command "${name}"
1147
+
1148
+ ${generalHelp(GTM_VERSION)}`);
1149
+ return false;
1150
+ }
1151
+ console.log(text);
1152
+ return true;
1153
+ }
976
1154
  function run() {
977
1155
  if (values.version) {
978
1156
  console.log(GTM_VERSION);
979
1157
  return true;
980
1158
  }
1159
+ if (values.help) return help(command === "help" ? args[0] : command);
981
1160
  switch (command) {
982
1161
  case "init":
983
1162
  return runInit(args[0], { name: values.name, dir: values.dir, cwd });
@@ -998,13 +1177,13 @@ function run() {
998
1177
  case "job":
999
1178
  return runJob(args[0]);
1000
1179
  case void 0:
1180
+ return help(void 0);
1001
1181
  case "help":
1002
- console.log(HELP);
1003
- return true;
1182
+ return help(args[0]);
1004
1183
  default:
1005
1184
  console.error(`Unknown command "${command}"
1006
1185
 
1007
- ${HELP}`);
1186
+ ${generalHelp(GTM_VERSION)}`);
1008
1187
  return false;
1009
1188
  }
1010
1189
  }