@veleads/gtm 0.2.1 → 0.4.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 +19 -0
  2. package/README.md +19 -18
  3. package/dist/cli.js +251 -140
  4. package/kit/AGENTS.md +64 -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 +13 -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);
@@ -406,8 +430,8 @@ function runApprove(root2, args2, opts) {
406
430
 
407
431
  // src/commands/init.ts
408
432
  import { spawnSync } from "child_process";
409
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync2, writeFileSync as writeFileSync4 } from "fs";
410
- import { join as join6, resolve } from "path";
433
+ import { appendFileSync, existsSync as existsSync4, readFileSync as readFileSync5 } from "fs";
434
+ import { basename, join as join6, relative as relative2, resolve } from "path";
411
435
 
412
436
  // src/paths.ts
413
437
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
@@ -425,7 +449,7 @@ var KIT_DIR = kit;
425
449
  var CHANGELOG_FILE = firstExisting([join2(pkgRoot, "CHANGELOG.md"), join2(pkgRoot, "..", "..", "CHANGELOG.md")]);
426
450
 
427
451
  // src/template.ts
428
- import { cpSync, mkdirSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
452
+ import { mkdirSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
429
453
  import { dirname as dirname2, join as join4 } from "path";
430
454
 
431
455
  // src/fsutil.ts
@@ -481,19 +505,22 @@ function destName(rel) {
481
505
  const [first, ...rest] = rel.split("/");
482
506
  return [DOTFILES.has(first) ? `.${first}` : first, ...rest].join("/");
483
507
  }
508
+ function planTemplate(src, vars, rename = destName) {
509
+ return walk(src).map((rel) => {
510
+ const raw = readFileSync3(join4(src, rel));
511
+ if (!/\.(md|ya?ml|json|txt|example)$|gitignore$/.test(rel)) return { rel: rename(rel), content: raw };
512
+ let text = raw.toString("utf8");
513
+ for (const [k, v] of Object.entries(vars)) text = text.replaceAll(`__${k}__`, v);
514
+ return { rel: rename(rel), content: text };
515
+ });
516
+ }
517
+ function writePlanned(root2, file) {
518
+ const out = join4(root2, file.rel);
519
+ mkdirSync(dirname2(out), { recursive: true });
520
+ writeFileSync2(out, file.content);
521
+ }
484
522
  function copyTemplate(src, dest, vars, opts = {}) {
485
- for (const rel of walk(src)) {
486
- if (opts.only && !opts.only.includes(rel)) continue;
487
- const out = join4(dest, (opts.rename ?? destName)(rel));
488
- mkdirSync(dirname2(out), { recursive: true });
489
- if (/\.(md|ya?ml|json|txt|example)$|gitignore$/.test(rel)) {
490
- let text = readFileSync3(join4(src, rel), "utf8");
491
- for (const [k, v] of Object.entries(vars)) text = text.replaceAll(`__${k}__`, v);
492
- writeFileSync2(out, text);
493
- } else {
494
- cpSync(join4(src, rel), out);
495
- }
496
- }
523
+ for (const file of planTemplate(src, vars, opts.rename)) writePlanned(dest, file);
497
524
  }
498
525
 
499
526
  // src/commands/sync.ts
@@ -502,7 +529,7 @@ import { dirname as dirname3, join as join5 } from "path";
502
529
  var SHIPPED_DIRS = ["skills", "templates", "providers", "schemas"];
503
530
  function agentsMd() {
504
531
  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. -->
532
+ Project-specific rules go in PROJECT.md. Process changes go to the Veleads/gtm-core repo. -->
506
533
 
507
534
  `;
508
535
  return header + readFileSync4(join5(KIT_DIR, "AGENTS.md"), "utf8").replaceAll("{{GTM_VERSION}}", GTM_VERSION);
@@ -545,7 +572,7 @@ function runSync(root2, opts = {}) {
545
572
  console.log(`in sync with ${PACKAGE_NAME} v${GTM_VERSION}`);
546
573
  return true;
547
574
  }
548
- console.log(`out of sync with ${PACKAGE_NAME} v${GTM_VERSION} (run \`pnpm gtm sync\`; never edit .gtm/ or AGENTS.md by hand):`);
575
+ console.log(`out of sync with ${PACKAGE_NAME} v${GTM_VERSION} (run \`npx gtm sync\`; never edit .gtm/ or AGENTS.md by hand):`);
549
576
  for (const d of diffs) console.log(` ${d}`);
550
577
  return false;
551
578
  }
@@ -560,33 +587,86 @@ function runSync(root2, opts = {}) {
560
587
  }
561
588
 
562
589
  // src/commands/init.ts
563
- function runInit(slug, opts) {
564
- 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)');
590
+ var USAGE2 = `Usage:
591
+ gtm init <project-slug> --name "<Project Name>" [--dir <path>] create ./gtm-project-<slug> (or <path>)
592
+ gtm init . --name "<Project Name>" [--slug <project-slug>] set up the current folder
593
+ Slugs: lowercase letters, digits and hyphens.`;
594
+ var MERGEABLE = /* @__PURE__ */ new Set([".gitignore", "README.md", "CLAUDE.md", "campaigns/.gitkeep", "tam/.gitkeep"]);
595
+ function slugFromFolder(path) {
596
+ return basename(path).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/^gtm-project-/, "");
597
+ }
598
+ function missingIgnoreLines(existing, ours) {
599
+ const have = new Set(existing.split("\n").map((l) => l.trim()));
600
+ return ours.split("\n").filter((l) => l.trim() && !l.trim().startsWith("#") && !have.has(l.trim()));
601
+ }
602
+ function runInit(target, opts) {
603
+ const here = target === "." || opts.dir === ".";
604
+ const root2 = resolve(opts.cwd, here ? opts.dir ?? "." : opts.dir ?? `gtm-project-${target ?? ""}`);
605
+ const slug = target && target !== "." ? target : opts.slug ?? slugFromFolder(root2);
606
+ if (!opts.name || !slug || !Slug.safeParse(slug).success) {
607
+ console.error(USAGE2);
608
+ if (!slug && here) console.error(`Couldn't derive a slug from the folder name "${basename(root2)}": pass one with --slug`);
609
+ else if (slug && !Slug.safeParse(slug).success) console.error(`"${slug}" is not a valid slug: pass one with --slug`);
566
610
  return false;
567
611
  }
568
- const root2 = resolve(opts.cwd, opts.dir ?? `gtm-client-${slug}`);
569
- if (existsSync4(root2) && readdirSync2(root2).length > 0) {
570
- console.error(`${root2} exists and is not empty`);
612
+ if (existsSync4(join6(root2, "project.yaml")) || existsSync4(join6(root2, "client.yaml"))) {
613
+ console.error(`${root2} is already a gtm project. Use \`npx gtm sync\` or \`npx gtm upgrade\` instead.`);
571
614
  return false;
572
615
  }
573
- const vars = { CLIENT_ID: slug, CLIENT_NAME: opts.name, TODAY: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), GTM_VERSION };
616
+ const vars = { PROJECT_ID: slug, PROJECT_NAME: opts.name, TODAY: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), GTM_VERSION };
574
617
  const tpl = join6(KIT_DIR, "templates");
575
- copyTemplate(join6(tpl, "repo"), root2, vars);
576
- copyTemplate(join6(tpl, "client"), root2, vars, { rename: (rel) => rel === "client.yaml" ? rel : join6("context", rel) });
577
- for (const d of ["campaigns", "tam"]) {
578
- mkdirSync3(join6(root2, d), { recursive: true });
579
- writeFileSync4(join6(root2, d, ".gitkeep"), "");
618
+ const planned = [
619
+ ...planTemplate(join6(tpl, "repo"), vars),
620
+ ...planTemplate(join6(tpl, "project"), vars, (rel) => rel === "project.yaml" ? rel : join6("context", rel)),
621
+ { rel: "campaigns/.gitkeep", content: "" },
622
+ { rel: "tam/.gitkeep", content: "" }
623
+ ];
624
+ const coreOwned = [...expectedFiles().keys()];
625
+ const conflicts = [...planned.map((f) => f.rel), ...coreOwned].filter((rel) => existsSync4(join6(root2, rel)) && !MERGEABLE.has(rel));
626
+ if (existsSync4(join6(root2, ".gtm"))) conflicts.unshift(".gtm/");
627
+ if (conflicts.length) {
628
+ console.error(`Cannot set up ${root2}: these files already exist and belong to gtm. Nothing was written.`);
629
+ for (const c of [...new Set(conflicts)].slice(0, 15)) console.error(` \u2717 ${c}`);
630
+ console.error("Move them away (or use an empty folder) and run init again.");
631
+ return false;
580
632
  }
633
+ const notes = [];
634
+ for (const file of planned) {
635
+ const path = join6(root2, file.rel);
636
+ if (!existsSync4(path)) {
637
+ writePlanned(root2, file);
638
+ continue;
639
+ }
640
+ if (file.rel === ".gitignore") {
641
+ const add = missingIgnoreLines(readFileSync5(path, "utf8"), file.content.toString());
642
+ if (add.length) {
643
+ appendFileSync(path, `
644
+ # gtm-brain
645
+ ${add.join("\n")}
646
+ `);
647
+ notes.push(`.gitignore: added ${add.length} gtm rule(s)`);
648
+ }
649
+ } else if (file.rel === "README.md") {
650
+ notes.push("README.md: kept yours (see .gtm/templates/repo/README.md for the gtm version)");
651
+ }
652
+ }
653
+ const claude = join6(root2, "CLAUDE.md");
654
+ const hadClaude = existsSync4(claude);
581
655
  runSync(root2);
582
- const git = spawnSync("git", ["init", "-q", "-b", "main"], { cwd: root2 });
656
+ if (hadClaude && !readFileSync5(claude, "utf8").includes("@AGENTS.md")) {
657
+ appendFileSync(claude, "\n@AGENTS.md\n");
658
+ notes.push("CLAUDE.md: added @AGENTS.md so Claude Code loads the operating manual");
659
+ }
660
+ const inRepo = spawnSync("git", ["rev-parse", "--is-inside-work-tree"], { cwd: root2, encoding: "utf8" }).stdout?.trim() === "true";
661
+ const gitNote = inRepo ? "existing git repository kept" : spawnSync("git", ["init", "-q", "-b", "main"], { cwd: root2 }).status === 0 ? "git initialised" : "git not initialised";
662
+ const where = relative2(opts.cwd, root2) || ".";
663
+ console.log(`
664
+ Set up project "${opts.name}" (${slug}) in ${root2} (core v${GTM_VERSION}; ${gitNote}).`);
665
+ for (const n of notes) console.log(` \xB7 ${n}`);
583
666
  console.log(`
584
- Created ${root2} (core v${GTM_VERSION})${git.status === 0 ? " with git initialised" : ""}.
585
-
586
667
  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)
668
+ 1. ${where === "." ? "" : `cd ${where} && `}npm install && npx gtm validate (or pnpm/yarn; TODO warnings remain until intake is done)
669
+ 2. ${inRepo ? "Commit and push to your PRIVATE repo." : `Create a PRIVATE GitHub repo (e.g. <your-org>/gtm-project-${slug}) and push.`} Project repos hold project context: never make them public.
590
670
  3. Open the repo in Claude Code or Codex and start with stage 00-intake.`);
591
671
  return true;
592
672
  }
@@ -602,24 +682,41 @@ Per AGENTS.md this is a mandatory stop: do not improvise the job with ad-hoc scr
602
682
  }
603
683
 
604
684
  // src/commands/migrate.ts
605
- import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "fs";
606
- import { join as join7, relative as relative2 } from "path";
685
+ import { existsSync as existsSync5, readFileSync as readFileSync6, renameSync, writeFileSync as writeFileSync4 } from "fs";
686
+ import { join as join7, relative as relative3 } from "path";
607
687
  import { parseDocument as parseDocument2 } from "yaml";
608
- function clientYamlFiles(root2) {
688
+ function projectYamlFiles(root2) {
609
689
  const files = [
610
- { kind: "client", file: join7(root2, "client.yaml") },
690
+ { kind: "project", file: join7(root2, "project.yaml") },
611
691
  { kind: "icp", file: join7(root2, "context", "icp.yaml") },
612
692
  { kind: "signals", file: join7(root2, "context", "signals.yaml") },
613
693
  ...subdirs(join7(root2, "campaigns")).map((c) => ({ kind: "campaign-config", file: join7(root2, "campaigns", c, "config.yaml") }))
614
694
  ];
615
695
  return files.filter((f) => existsSync5(f.file));
616
696
  }
697
+ function pendingRenames(root2) {
698
+ return FILE_RENAMES.filter((r) => existsSync5(join7(root2, r.from)) && !existsSync5(join7(root2, r.to)));
699
+ }
617
700
  function runMigrate(root2, opts = {}) {
618
701
  let ok = true;
619
702
  let changed = 0;
620
- for (const { kind, file } of clientYamlFiles(root2)) {
621
- const rel = relative2(root2, file);
622
- const doc = parseDocument2(readFileSync5(file, "utf8"));
703
+ for (const { from, to } of pendingRenames(root2)) {
704
+ changed++;
705
+ console.log(` ${opts.dryRun ? "would rename" : "renamed"} ${from} \u2192 ${to}`);
706
+ if (opts.dryRun) continue;
707
+ renameSync(join7(root2, from), join7(root2, to));
708
+ if (to.endsWith(".md")) {
709
+ const p = join7(root2, to);
710
+ writeFileSync4(p, readFileSync6(p, "utf8").replace(new RegExp(`^# ${from.replace(".", "\\.")}`, "m"), `# ${to}`));
711
+ }
712
+ }
713
+ if (opts.dryRun && changed) {
714
+ console.log("(dry run: YAML migrations are listed after the renames are applied)");
715
+ return ok;
716
+ }
717
+ for (const { kind, file } of projectYamlFiles(root2)) {
718
+ const rel = relative3(root2, file);
719
+ const doc = parseDocument2(readFileSync6(file, "utf8"));
623
720
  if (doc.errors.length) {
624
721
  console.log(` \u2717 ${rel}: invalid YAML: ${doc.errors[0].message}`);
625
722
  ok = false;
@@ -634,9 +731,9 @@ function runMigrate(root2, opts = {}) {
634
731
  for (const a of res.applied) console.log(` ${opts.dryRun ? "would apply" : "applied"} ${rel}: ${a}`);
635
732
  if (pinned) {
636
733
  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)`);
734
+ console.log(` re-pinned ICP approval hash in ${rel} (core migration, content approved by the project approver)`);
638
735
  }
639
- if (!opts.dryRun) writeFileSync5(file, doc.toString({ flowCollectionPadding: false, lineWidth: 0 }));
736
+ if (!opts.dryRun) writeFileSync4(file, doc.toString({ flowCollectionPadding: false, lineWidth: 0 }));
640
737
  } catch (e) {
641
738
  console.log(` \u2717 ${rel}: ${e.message}`);
642
739
  ok = false;
@@ -647,7 +744,7 @@ function runMigrate(root2, opts = {}) {
647
744
  }
648
745
 
649
746
  // src/commands/new-campaign.ts
650
- import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
747
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
651
748
  import { join as join8 } from "path";
652
749
  import { parse } from "yaml";
653
750
  function runNewCampaign(root2, campaign) {
@@ -655,9 +752,9 @@ function runNewCampaign(root2, campaign) {
655
752
  console.error("Usage: gtm new-campaign <campaign-slug>");
656
753
  return false;
657
754
  }
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");
755
+ const projectFile = join8(root2, "project.yaml");
756
+ if (!existsSync6(projectFile)) {
757
+ console.error("project.yaml not found: run this from the root of a project repo");
661
758
  return false;
662
759
  }
663
760
  const dest = join8(root2, "campaigns", campaign);
@@ -665,21 +762,21 @@ function runNewCampaign(root2, campaign) {
665
762
  console.error(`campaigns/${campaign} already exists`);
666
763
  return false;
667
764
  }
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" });
670
- mkdirSync4(join8(dest, "runs"), { recursive: true });
671
- writeFileSync6(join8(dest, "runs", ".gitkeep"), "");
765
+ const projectId = parse(readFileSync7(projectFile, "utf8"))?.project_id ?? "TODO";
766
+ copyTemplate(join8(KIT_DIR, "templates", "campaign"), dest, { PROJECT_ID: projectId, CAMPAIGN_ID: campaign, OWNER: process.env.GTM_USER ?? "TODO" });
767
+ mkdirSync3(join8(dest, "runs"), { recursive: true });
768
+ writeFileSync5(join8(dest, "runs", ".gitkeep"), "");
672
769
  console.log(`Created campaigns/${campaign}. Branch: git switch -c campaign/${campaign}`);
673
770
  return true;
674
771
  }
675
772
 
676
773
  // src/commands/status.ts
677
- import { existsSync as existsSync7, readdirSync as readdirSync3, readFileSync as readFileSync7 } from "fs";
774
+ import { existsSync as existsSync7, readdirSync as readdirSync2, readFileSync as readFileSync8 } from "fs";
678
775
  import { join as join9 } from "path";
679
776
  import { parse as parse2 } from "yaml";
680
777
  function readYaml(file) {
681
778
  try {
682
- return parse2(readFileSync7(file, "utf8"));
779
+ return parse2(readFileSync8(file, "utf8"));
683
780
  } catch {
684
781
  return null;
685
782
  }
@@ -688,19 +785,19 @@ function campaignStages(root2) {
688
785
  return subdirs(join9(root2, "campaigns")).map((id) => ({ id, stage: readYaml(join9(root2, "campaigns", id, "config.yaml"))?.stage ?? "?" }));
689
786
  }
690
787
  function runStatus(root2) {
691
- const client = readYaml(join9(root2, "client.yaml"));
788
+ const project = readYaml(join9(root2, "project.yaml"));
692
789
  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}`);
790
+ console.log(`${project?.name ?? "?"} (${project?.project_id ?? "?"}) \xB7 status ${project?.status ?? "?"} \xB7 core v${GTM_VERSION}`);
694
791
  console.log(`ICP gate: ${icp?.approval?.status ?? "?"}${icp?.approval?.approved_by ? ` by ${icp.approval.approved_by} on ${icp.approval.approved_at}` : ""}
695
792
  `);
696
793
  const rows = subdirs(join9(root2, "campaigns")).map((id) => {
697
794
  const cfg = readYaml(join9(root2, "campaigns", id, "config.yaml")) ?? {};
698
795
  const runsDir = join9(root2, "campaigns", id, "runs");
699
- const last = existsSync7(runsDir) ? readdirSync3(runsDir).filter((f) => f.endsWith(".md")).sort().at(-1) ?? "-" : "-";
796
+ const last = existsSync7(runsDir) ? readdirSync2(runsDir).filter((f) => f.endsWith(".md")).sort().at(-1) ?? "-" : "-";
700
797
  return [id, cfg.stage ?? "?", cfg.gates?.copy?.status ?? "?", cfg.gates?.launch?.status ?? "?", cfg.owner ?? "?", last];
701
798
  });
702
799
  if (!rows.length) {
703
- console.log("no campaigns yet: pnpm gtm new-campaign <campaign-slug>");
800
+ console.log("no campaigns yet: npx gtm new-campaign <campaign-slug>");
704
801
  return true;
705
802
  }
706
803
  const header = ["campaign", "stage", "copy gate", "launch gate", "owner", "latest run log"];
@@ -714,7 +811,7 @@ function runStatus(root2) {
714
811
 
715
812
  // src/commands/upgrade.ts
716
813
  import { spawnSync as spawnSync2 } from "child_process";
717
- import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
814
+ import { existsSync as existsSync9, readFileSync as readFileSync10 } from "fs";
718
815
  import { join as join11 } from "path";
719
816
 
720
817
  // src/changelog.ts
@@ -733,7 +830,7 @@ function changelogBetween(text, from, to) {
733
830
  }
734
831
 
735
832
  // src/commands/validate.ts
736
- import { existsSync as existsSync8, readdirSync as readdirSync4, readFileSync as readFileSync8 } from "fs";
833
+ import { existsSync as existsSync8, readdirSync as readdirSync3, readFileSync as readFileSync9 } from "fs";
737
834
  import { join as join10 } from "path";
738
835
  import { parse as parse3 } from "yaml";
739
836
  var REQUIRED_CONTEXT = ["intake.md", "icp.md", "icp.yaml", "signals.yaml", "personas.md", "offer.md", "proof.md", "voice.md"];
@@ -746,7 +843,7 @@ var SKIP_DIRS = ["node_modules", ".git", ".gtm"];
746
843
  var stageIndex = (s) => Stage.options.indexOf(s);
747
844
  function readPackageJson(root2) {
748
845
  const p = join10(root2, "package.json");
749
- return existsSync8(p) ? JSON.parse(readFileSync8(p, "utf8")) : null;
846
+ return existsSync8(p) ? JSON.parse(readFileSync9(p, "utf8")) : null;
750
847
  }
751
848
  function pinnedVersion(root2) {
752
849
  const pkg = readPackageJson(root2);
@@ -759,15 +856,15 @@ function loadVersioned(r, file, kind, schema) {
759
856
  }
760
857
  let raw;
761
858
  try {
762
- raw = parse3(readFileSync8(file, "utf8"));
859
+ raw = parse3(readFileSync9(file, "utf8"));
763
860
  } catch (e) {
764
861
  r.err(file, `invalid YAML: ${e.message}`);
765
862
  return null;
766
863
  }
767
864
  const expected = FILE_VERSIONS[kind];
768
865
  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\``);
866
+ 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\``);
867
+ 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
868
  else r.err(file, `missing "version: ${expected}"`);
772
869
  return null;
773
870
  }
@@ -783,20 +880,20 @@ function checkCoreSync(r, root2) {
783
880
  if (!pinned) return;
784
881
  if (!/^\d+\.\d+\.\d+/.test(pinned)) r.err(join10(root2, "package.json"), `${PACKAGE_NAME} must be pinned exactly (got "${pinned}")`);
785
882
  const versionFile = join10(root2, ".gtm", "VERSION");
786
- 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`");
883
+ const synced = existsSync8(versionFile) ? readFileSync9(versionFile, "utf8").trim() : null;
884
+ if (synced !== GTM_VERSION) r.err(versionFile, `is ${synced ?? "missing"}, installed core is ${GTM_VERSION}: run \`npx gtm sync\``);
885
+ if (!existsSync8(join10(root2, "AGENTS.md"))) r.err(join10(root2, "AGENTS.md"), "missing: run `npx gtm sync`");
789
886
  }
790
887
  function checkNoLeadData(r, root2) {
791
888
  for (const rel of walk(root2, SKIP_DIRS)) {
792
889
  const file = join10(root2, rel);
793
890
  if (DATA_FILES.test(rel)) {
794
- r.err(file, "data files are not allowed in a client repo (lead data belongs in Postgres)");
891
+ r.err(file, "data files are not allowed in a project repo (lead data belongs in Postgres)");
795
892
  continue;
796
893
  }
797
894
  if (!/\.(md|ya?ml|txt)$/i.test(rel) || rel === "pnpm-lock.yaml") continue;
798
- if (/\.ya?ml$/i.test(rel) && /\bTODO\b/.test(readFileSync8(file, "utf8"))) r.warn(file, "still contains TODO placeholders");
799
- const found = (readFileSync8(file, "utf8").match(EMAIL) ?? []).filter((e) => !ALLOWED_EMAIL.test(e));
895
+ if (/\.ya?ml$/i.test(rel) && /\bTODO\b/.test(readFileSync9(file, "utf8"))) r.warn(file, "still contains TODO placeholders");
896
+ const found = (readFileSync9(file, "utf8").match(EMAIL) ?? []).filter((e) => !ALLOWED_EMAIL.test(e));
800
897
  if (found.length) r.warn(file, `contains email address(es) ${[...new Set(found)].slice(0, 3).join(", ")}: confirm none are prospects`);
801
898
  }
802
899
  }
@@ -811,14 +908,14 @@ function checkPinnedApproval(r, contentFile, approval, current, what, approveCmd
811
908
  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
909
  return false;
813
910
  }
814
- function validateCampaign(r, root2, clientId, campaign, icp, signals, icpApproved) {
911
+ function validateCampaign(r, root2, projectId, campaign, icp, signals, icpApproved) {
815
912
  const dir = join10(root2, "campaigns", campaign);
816
913
  console.log(`campaign ${campaign}`);
817
914
  for (const f of REQUIRED_CAMPAIGN) if (!existsSync8(join10(dir, f))) r.err(join10(dir, f), "missing");
818
915
  const cfgFile = join10(dir, "config.yaml");
819
916
  const cfg = loadVersioned(r, cfgFile, "campaign-config", CampaignConfig);
820
917
  if (cfg) {
821
- if (cfg.client_id !== clientId) r.err(cfgFile, `client_id "${cfg.client_id}" does not match client.yaml "${clientId}"`);
918
+ if (cfg.project_id !== projectId) r.err(cfgFile, `project_id "${cfg.project_id}" does not match project.yaml "${projectId}"`);
822
919
  if (cfg.campaign_id !== campaign) r.err(cfgFile, `campaign_id "${cfg.campaign_id}" does not match folder "${campaign}"`);
823
920
  if (icp) {
824
921
  const segs = new Set(icp.segments.map((s) => s.id));
@@ -841,7 +938,7 @@ function validateCampaign(r, root2, clientId, campaign, icp, signals, icpApprove
841
938
  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
939
  }
843
940
  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;
941
+ const copyApproved = existsSync8(copyFile) ? checkPinnedApproval(r, copyFile, cfg.gates.copy, copyContentHash(readFileSync9(copyFile, "utf8")), "copy", `npx gtm approve copy ${campaign}`) : false;
845
942
  if (stageIndex(cfg.stage) > stageIndex("copy") && !copyApproved) {
846
943
  r.err(cfgFile, `stage is "${cfg.stage}" but gates.copy is not approved, or copy.md changed since approval`);
847
944
  }
@@ -857,7 +954,7 @@ function validateCampaign(r, root2, clientId, campaign, icp, signals, icpApprove
857
954
  }
858
955
  const runs = join10(dir, "runs");
859
956
  if (existsSync8(runs)) {
860
- for (const f of readdirSync4(runs).filter((f2) => !f2.startsWith("."))) {
957
+ for (const f of readdirSync3(runs).filter((f2) => !f2.startsWith("."))) {
861
958
  if (!RUN_LOG_NAME.test(f)) r.err(join10(runs, f), "run logs must be named YYYY-MM-DD-<user>[-n].md");
862
959
  }
863
960
  }
@@ -866,22 +963,23 @@ function runValidate(root2, onlyCampaign) {
866
963
  const r = new Report(root2);
867
964
  console.log(`validating ${root2} against core v${GTM_VERSION}`);
868
965
  checkCoreSync(r, root2);
869
- const meta = loadVersioned(r, join10(root2, "client.yaml"), "client", ClientFile);
870
- const clientId = meta?.client_id ?? "";
966
+ for (const { from, to } of pendingRenames(root2)) r.err(join10(root2, from), `renamed to ${to} in core 0.3.0: run \`npx gtm migrate\``);
967
+ const meta = loadVersioned(r, join10(root2, "project.yaml"), "project", ProjectFile);
968
+ const projectId = meta?.project_id ?? "";
871
969
  if (meta) {
872
970
  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"})`);
971
+ if (!FEATURES[key]) r.err(join10(root2, "project.yaml"), `unknown feature "${key}" (known: ${Object.keys(FEATURES).join(", ") || "none"})`);
874
972
  }
875
973
  }
876
- if (!existsSync8(join10(root2, "CLIENT.md"))) r.warn(join10(root2, "CLIENT.md"), "missing (client-specific rules for agents)");
974
+ if (!existsSync8(join10(root2, "PROJECT.md"))) r.warn(join10(root2, "PROJECT.md"), "missing (project-specific rules for agents)");
877
975
  for (const f of REQUIRED_CONTEXT) if (!existsSync8(join10(root2, "context", f))) r.err(join10(root2, "context", f), "missing");
878
976
  const icpFile = join10(root2, "context", "icp.yaml");
879
977
  const sigFile = join10(root2, "context", "signals.yaml");
880
978
  const icp = existsSync8(icpFile) ? loadVersioned(r, icpFile, "icp", IcpFile) : null;
881
979
  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;
980
+ if (meta && icp && icp.project_id !== projectId) r.err(icpFile, `project_id "${icp.project_id}" does not match project.yaml "${projectId}"`);
981
+ if (meta && signals && signals.project_id !== projectId) r.err(sigFile, `project_id "${signals.project_id}" does not match project.yaml "${projectId}"`);
982
+ const icpApproved = icp ? checkPinnedApproval(r, icpFile, icp.approval, icpContentHash(parse3(readFileSync9(icpFile, "utf8"))), "ICP", "npx gtm approve icp") : false;
885
983
  checkNoLeadData(r, root2);
886
984
  const campaigns = onlyCampaign ? [onlyCampaign] : subdirs(join10(root2, "campaigns"));
887
985
  for (const c of campaigns) {
@@ -889,24 +987,31 @@ function runValidate(root2, onlyCampaign) {
889
987
  r.err(join10(root2, "campaigns", c), "campaign folder not found");
890
988
  continue;
891
989
  }
892
- validateCampaign(r, root2, clientId, c, icp, signals, icpApproved);
990
+ validateCampaign(r, root2, projectId, c, icp, signals, icpApproved);
893
991
  }
894
992
  r.summary();
895
993
  return r.errors === 0;
896
994
  }
897
995
 
898
996
  // 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;
997
+ function detectPackageManager(root2) {
998
+ if (existsSync9(join11(root2, "pnpm-lock.yaml"))) return "pnpm";
999
+ if (existsSync9(join11(root2, "yarn.lock"))) return "yarn";
1000
+ return "npm";
1001
+ }
1002
+ function installExact(root2, pm, spec) {
1003
+ const onPath = (bin) => spawnSync2(bin, ["--version"], { stdio: "ignore" }).status === 0;
1004
+ 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]];
1005
+ return spawnSync2(cmd, args2, { cwd: root2, stdio: "inherit" }).status ?? 1;
1006
+ }
1007
+ function gtm(root2, args2) {
1008
+ return spawnSync2("npx", ["gtm", ...args2], { cwd: root2, stdio: "inherit" }).status ?? 1;
904
1009
  }
905
1010
  var FROZEN_STAGES = ["qa", "launch"];
906
1011
  function runUpgrade(root2, opts) {
907
1012
  const before = pinnedVersion(root2);
908
1013
  if (!before) {
909
- console.error(`package.json does not depend on ${PACKAGE_NAME}: is this a client repo?`);
1014
+ console.error(`package.json does not depend on ${PACKAGE_NAME}: is this a project repo?`);
910
1015
  return false;
911
1016
  }
912
1017
  const frozen = campaignStages(root2).filter((c) => FROZEN_STAGES.includes(c.stage));
@@ -921,15 +1026,16 @@ function runUpgrade(root2, opts) {
921
1026
  return false;
922
1027
  }
923
1028
  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;
1029
+ const pm = detectPackageManager(root2);
1030
+ console.log(`Upgrading ${PACKAGE_NAME} ${before} \u2192 ${target} (with ${pm})`);
1031
+ if (installExact(root2, pm, `${PACKAGE_NAME}@${target}`) !== 0) return false;
1032
+ if (gtm(root2, ["sync"]) !== 0) return false;
1033
+ if (gtm(root2, ["migrate"]) !== 0) return false;
1034
+ const valid = gtm(root2, ["validate"]) === 0;
929
1035
  const after = pinnedVersion(root2) ?? target;
930
1036
  const changelog = join11(root2, "node_modules", ...PACKAGE_NAME.split("/"), "CHANGELOG.md");
931
1037
  if (existsSync9(changelog)) {
932
- const notes = changelogBetween(readFileSync9(changelog, "utf8"), before, after);
1038
+ const notes = changelogBetween(readFileSync10(changelog, "utf8"), before, after);
933
1039
  console.log(`
934
1040
  === Changes ${before} \u2192 ${after} ===
935
1041
  ${notes || "(no changelog entries)"}
@@ -942,44 +1048,48 @@ ${notes || "(no changelog entries)"}
942
1048
  // src/help.ts
943
1049
  var COMMANDS = {
944
1050
  init: {
945
- usage: 'gtm init <client-slug> --name "<Client Name>" [--dir <path>]',
946
- summary: "scaffold a new client repo",
1051
+ usage: 'gtm init <project-slug> --name "<Project Name>" [--dir <path>]\ngtm init . --name "<Project Name>" [--slug <project-slug>]',
1052
+ summary: 'scaffold a project repo (new folder, or the current one with ".")',
947
1053
  details: [
948
- "Creates a complete client repo: client.yaml, context/ templates, campaigns/, tam/, CLIENT.md,",
949
- "CI workflows, .gtm/ (core kit) and AGENTS.md, pinned to this CLI's version, then runs git init.",
950
- "Push the result to a PRIVATE repo: it will hold client context."
1054
+ "Creates a complete project repo: project.yaml, context/ templates, campaigns/, tam/, PROJECT.md,",
1055
+ "CI workflows, .gtm/ (core kit) and AGENTS.md, pinned to this CLI's version.",
1056
+ '"." sets up the current folder; the slug comes from the folder name (gtm-project-acme \u2192 acme) or --slug.',
1057
+ "Works in a fresh clone: .git is kept (no git init inside an existing repo), README.md is kept,",
1058
+ ".gitignore gets gtm rules appended, CLAUDE.md gets @AGENTS.md. Any other existing gtm file",
1059
+ "(e.g. AGENTS.md, .gtm/, package.json) stops init before anything is written.",
1060
+ "Push the result to a PRIVATE repo: it will hold project context."
951
1061
  ],
952
- writes: "A new folder (default ./gtm-client-<slug>). Refuses if it exists and is not empty.",
953
- examples: ['npx @veleads/gtm init acme --name "Acme Corp"', 'npx @veleads/gtm init acme --name "Acme Corp" --dir ./clients/acme']
1062
+ writes: 'The target folder (new ./gtm-project-<slug>, --dir, or the current folder with "."). Never overwrites existing files.',
1063
+ examples: ['npx @veleads/gtm init . --name "Acme Corp"', 'npx @veleads/gtm init acme --name "Acme Corp"', 'npx @veleads/gtm init . --name "Acme Corp" --slug acme']
954
1064
  },
955
1065
  "new-campaign": {
956
1066
  usage: "gtm new-campaign <campaign-slug>",
957
1067
  summary: "scaffold campaigns/<slug>/",
958
1068
  details: ["Copies the campaign template (brief.md, config.yaml, copy.md, runs/). Owner comes from GTM_USER."],
959
1069
  writes: "campaigns/<slug>/ (refuses if it exists).",
960
- examples: ["git switch -c campaign/q1-uk-cfo && pnpm gtm new-campaign q1-uk-cfo"]
1070
+ examples: ["git switch -c campaign/q1-uk-cfo && npx gtm new-campaign q1-uk-cfo"]
961
1071
  },
962
1072
  validate: {
963
1073
  usage: "gtm validate [campaign]",
964
1074
  summary: "check repo against schemas, gates, data rules",
965
1075
  details: [
966
- "Checks client.yaml, context/ and campaigns against the schemas; segment/persona/signal references;",
1076
+ "Checks project.yaml, context/ and campaigns against the schemas; segment/persona/signal references;",
967
1077
  "gate order (no stage past an unapproved or changed ICP/copy approval); required files; run-log names;",
968
1078
  "no lead data (blocks data files, warns on email addresses); .gtm/ matches the installed core.",
969
1079
  "Exit code 1 if there is any error; warnings never fail."
970
1080
  ],
971
1081
  writes: "Nothing (read-only).",
972
- examples: ["pnpm gtm validate", "pnpm gtm validate q1-uk-cfo"]
1082
+ examples: ["npx gtm validate", "npx gtm validate q1-uk-cfo"]
973
1083
  },
974
1084
  status: {
975
1085
  usage: "gtm status",
976
1086
  summary: "campaigns, stages, gates",
977
- details: ["One screen: client, core version, ICP gate, and per campaign the stage, copy/launch gates, owner and latest run log."],
1087
+ details: ["One screen: project, core version, ICP gate, and per campaign the stage, copy/launch gates, owner and latest run log."],
978
1088
  writes: "Nothing (read-only).",
979
- examples: ["pnpm gtm status"]
1089
+ examples: ["npx gtm status"]
980
1090
  },
981
1091
  approve: {
982
- usage: 'gtm approve icp --by "<Name (side)>" [--notes ".."] [--date YYYY-MM-DD]\ngtm approve copy <campaign> --by "<Name (side)>" [--notes ".."] [--date YYYY-MM-DD]',
1092
+ usage: 'gtm approve icp --by "<Name (role)>" [--notes ".."] [--date YYYY-MM-DD]\ngtm approve copy <campaign> --by "<Name (role)>" [--notes ".."] [--date YYYY-MM-DD]',
983
1093
  summary: "record a sign-off and pin a hash of the approved content",
984
1094
  details: [
985
1095
  "Run ONLY after the named human explicitly approved in this session (AGENTS.md section 4).",
@@ -988,7 +1098,7 @@ var COMMANDS = {
988
1098
  "The launch gate is recorded by hand in config.yaml (its content is the lead set)."
989
1099
  ],
990
1100
  writes: "context/icp.yaml \u2192 approval (icp), or campaigns/<campaign>/config.yaml \u2192 gates.copy (copy).",
991
- examples: ['pnpm gtm approve icp --by "Maria Chen (client)" --notes "Segments A+B, personas CFO+AP"', 'pnpm gtm approve copy q1-uk-cfo --by "Maria Chen (client)"']
1101
+ 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)"']
992
1102
  },
993
1103
  sync: {
994
1104
  usage: "gtm sync [--check]",
@@ -998,14 +1108,14 @@ var COMMANDS = {
998
1108
  "--check only compares and exits 1 if they differ (CI uses this; never edit .gtm/ by hand)."
999
1109
  ],
1000
1110
  writes: ".gtm/ (replaced entirely) and AGENTS.md; creates CLAUDE.md if missing. With --check: nothing.",
1001
- examples: ["pnpm gtm sync", "pnpm gtm sync --check"]
1111
+ examples: ["npx gtm sync", "npx gtm sync --check"]
1002
1112
  },
1003
1113
  migrate: {
1004
1114
  usage: "gtm migrate [--dry-run]",
1005
1115
  summary: "upgrade YAML files to current schema versions",
1006
- details: ["Applies core schema migrations to client.yaml, context/*.yaml and campaigns/*/config.yaml, keeping comments."],
1116
+ details: ["Applies core schema migrations to project.yaml, context/*.yaml and campaigns/*/config.yaml, keeping comments."],
1007
1117
  writes: "Those YAML files, only if a migration applies. With --dry-run: nothing.",
1008
- examples: ["pnpm gtm migrate --dry-run", "pnpm gtm migrate"]
1118
+ examples: ["npx gtm migrate --dry-run", "npx gtm migrate"]
1009
1119
  },
1010
1120
  upgrade: {
1011
1121
  usage: "gtm upgrade [version] [--force]",
@@ -1015,7 +1125,7 @@ var COMMANDS = {
1015
1125
  "Refuses while a campaign is in qa or launch, or with uncommitted changes (--force overrides; ask a GTM engineer)."
1016
1126
  ],
1017
1127
  writes: "package.json, lockfile, node_modules, .gtm/, AGENTS.md and migrated YAML files.",
1018
- examples: ["pnpm gtm upgrade", "pnpm gtm upgrade 0.3.0"]
1128
+ examples: ["npx gtm upgrade", "npx gtm upgrade 0.3.0"]
1019
1129
  },
1020
1130
  job: {
1021
1131
  usage: "gtm job <name> <campaign>",
@@ -1025,15 +1135,15 @@ var COMMANDS = {
1025
1135
  "Not available yet: it exits 1. Per AGENTS.md this is a stop; do not improvise jobs with ad-hoc scripts."
1026
1136
  ],
1027
1137
  writes: "Nothing yet.",
1028
- examples: ["pnpm gtm job tam q1-uk-cfo"]
1138
+ examples: ["npx gtm job tam q1-uk-cfo"]
1029
1139
  }
1030
1140
  };
1031
1141
  function generalHelp(version) {
1032
1142
  const width = Math.max(...Object.keys(COMMANDS).map((c) => c.length));
1033
1143
  const lines = Object.entries(COMMANDS).map(([name, c]) => ` ${name.padEnd(width)} ${c.summary}`);
1034
- return `gtm v${version}: CLI for gtm-brain client repos (@veleads/gtm)
1144
+ return `gtm v${version}: CLI for gtm-brain project repos (@veleads/gtm)
1035
1145
 
1036
- Usage: gtm <command> [options] (inside a client repo: pnpm gtm <command>)
1146
+ Usage: gtm <command> [options] (inside a project repo: npx gtm <command>)
1037
1147
 
1038
1148
  Commands:
1039
1149
  ${lines.join("\n")}
@@ -1047,7 +1157,7 @@ Global options:
1047
1157
 
1048
1158
  Exit codes: 0 success, 1 failure or validation errors.
1049
1159
 
1050
- For coding agents: inside a client repo, read AGENTS.md first and follow it. Read-only commands
1160
+ For coding agents: inside a project repo, read AGENTS.md first and follow it. Read-only commands
1051
1161
  (validate, status, sync --check, migrate --dry-run) are always safe to run.`;
1052
1162
  }
1053
1163
  function commandHelp(name) {
@@ -1073,6 +1183,7 @@ var { values, positionals } = parseArgs({
1073
1183
  root: { type: "string" },
1074
1184
  name: { type: "string" },
1075
1185
  dir: { type: "string" },
1186
+ slug: { type: "string" },
1076
1187
  by: { type: "string" },
1077
1188
  notes: { type: "string" },
1078
1189
  date: { type: "string" },
@@ -1109,7 +1220,7 @@ function run() {
1109
1220
  if (values.help) return help(command === "help" ? args[0] : command);
1110
1221
  switch (command) {
1111
1222
  case "init":
1112
- return runInit(args[0], { name: values.name, dir: values.dir, cwd });
1223
+ return runInit(args[0], { name: values.name, dir: values.dir, slug: values.slug, cwd });
1113
1224
  case "new-campaign":
1114
1225
  return runNewCampaign(root, args[0]);
1115
1226
  case "validate":