@veleads/gtm 0.3.0 → 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.
- package/CHANGELOG.md +7 -0
- package/README.md +5 -4
- package/dist/cli.js +123 -62
- package/kit/AGENTS.md +3 -2
- package/kit/skills/00-intake/SKILL.md +2 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,13 @@ All notable changes to `@veleads/gtm`. Each entry is tagged: **no action**, **op
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.4.0] - 2026-10-24
|
|
9
|
+
- **no action** `gtm init . --name "<Name>"` sets up the current folder instead of creating a
|
|
10
|
+
subfolder (slug from the folder name, e.g. `gtm-project-acme` → `acme`, or `--slug`). Built for a
|
|
11
|
+
freshly cloned repo: `.git` is kept (no `git init` inside an existing repository), `README.md` is
|
|
12
|
+
kept, `.gitignore` gets gtm rules appended, `CLAUDE.md` gets `@AGENTS.md`. Any other existing gtm
|
|
13
|
+
file stops init before anything is written; init never overwrites files.
|
|
14
|
+
|
|
8
15
|
## [0.3.0] - 2026-10-24
|
|
9
16
|
- **migration** "Clients" are now **projects**: one private repo = one project.
|
|
10
17
|
`client.yaml` → `project.yaml`, `CLIENT.md` → `PROJECT.md`, `client_id` → `project_id` in every YAML
|
package/README.md
CHANGED
|
@@ -12,14 +12,15 @@ Source: [Veleads/gtm-core](https://github.com/Veleads/gtm-core) · MIT
|
|
|
12
12
|
Requires Node.js 22+. npm is enough; pnpm or yarn work too (the repo follows whichever lockfile you commit).
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
# in an empty or freshly cloned (private) repo folder:
|
|
16
|
+
npx @veleads/gtm init . --name "Acme Corp"
|
|
17
17
|
npm install
|
|
18
18
|
npx gtm status
|
|
19
19
|
npx gtm validate
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
`init .` keeps an existing `.git`, README and `.gitignore` (gtm rules are appended) and never overwrites
|
|
23
|
+
files; `init acme --name "Acme Corp"` creates `./gtm-project-acme` instead. Push to a **private** repository, then open it in your coding agent. The generated
|
|
23
24
|
`AGENTS.md` tells the agent how to work in the repo, starting at stage `00-intake`.
|
|
24
25
|
|
|
25
26
|
## What you get in a project repo
|
|
@@ -43,7 +44,7 @@ command writes**, and examples, without running it.
|
|
|
43
44
|
|
|
44
45
|
| Command | What it does | Writes |
|
|
45
46
|
|---|---|---|
|
|
46
|
-
| `init <slug> --name "<Name>"` |
|
|
47
|
+
| `init . --name "<Name>"` / `init <slug> --name "<Name>"` | set up the current folder / a new one | target folder, never overwrites |
|
|
47
48
|
| `new-campaign <slug>` | scaffold `campaigns/<slug>/` | that folder |
|
|
48
49
|
| `validate [campaign]` | schemas, gates, cross-references, no lead data, core sync | nothing |
|
|
49
50
|
| `status` | campaigns, stages, gates | nothing |
|
package/dist/cli.js
CHANGED
|
@@ -430,8 +430,8 @@ function runApprove(root2, args2, opts) {
|
|
|
430
430
|
|
|
431
431
|
// src/commands/init.ts
|
|
432
432
|
import { spawnSync } from "child_process";
|
|
433
|
-
import { existsSync as existsSync4,
|
|
434
|
-
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";
|
|
435
435
|
|
|
436
436
|
// src/paths.ts
|
|
437
437
|
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
@@ -449,7 +449,7 @@ var KIT_DIR = kit;
|
|
|
449
449
|
var CHANGELOG_FILE = firstExisting([join2(pkgRoot, "CHANGELOG.md"), join2(pkgRoot, "..", "..", "CHANGELOG.md")]);
|
|
450
450
|
|
|
451
451
|
// src/template.ts
|
|
452
|
-
import {
|
|
452
|
+
import { mkdirSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
453
453
|
import { dirname as dirname2, join as join4 } from "path";
|
|
454
454
|
|
|
455
455
|
// src/fsutil.ts
|
|
@@ -505,19 +505,22 @@ function destName(rel) {
|
|
|
505
505
|
const [first, ...rest] = rel.split("/");
|
|
506
506
|
return [DOTFILES.has(first) ? `.${first}` : first, ...rest].join("/");
|
|
507
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
|
+
}
|
|
508
522
|
function copyTemplate(src, dest, vars, opts = {}) {
|
|
509
|
-
for (const
|
|
510
|
-
if (opts.only && !opts.only.includes(rel)) continue;
|
|
511
|
-
const out = join4(dest, (opts.rename ?? destName)(rel));
|
|
512
|
-
mkdirSync(dirname2(out), { recursive: true });
|
|
513
|
-
if (/\.(md|ya?ml|json|txt|example)$|gitignore$/.test(rel)) {
|
|
514
|
-
let text = readFileSync3(join4(src, rel), "utf8");
|
|
515
|
-
for (const [k, v] of Object.entries(vars)) text = text.replaceAll(`__${k}__`, v);
|
|
516
|
-
writeFileSync2(out, text);
|
|
517
|
-
} else {
|
|
518
|
-
cpSync(join4(src, rel), out);
|
|
519
|
-
}
|
|
520
|
-
}
|
|
523
|
+
for (const file of planTemplate(src, vars, opts.rename)) writePlanned(dest, file);
|
|
521
524
|
}
|
|
522
525
|
|
|
523
526
|
// src/commands/sync.ts
|
|
@@ -584,33 +587,86 @@ function runSync(root2, opts = {}) {
|
|
|
584
587
|
}
|
|
585
588
|
|
|
586
589
|
// src/commands/init.ts
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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`);
|
|
590
610
|
return false;
|
|
591
611
|
}
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
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.`);
|
|
595
614
|
return false;
|
|
596
615
|
}
|
|
597
616
|
const vars = { PROJECT_ID: slug, PROJECT_NAME: opts.name, TODAY: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), GTM_VERSION };
|
|
598
617
|
const tpl = join6(KIT_DIR, "templates");
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
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;
|
|
604
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);
|
|
605
655
|
runSync(root2);
|
|
606
|
-
|
|
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}`);
|
|
607
666
|
console.log(`
|
|
608
|
-
Created ${root2} (core v${GTM_VERSION})${git.status === 0 ? " with git initialised" : ""}.
|
|
609
|
-
|
|
610
667
|
Next:
|
|
611
|
-
1.
|
|
612
|
-
|
|
613
|
-
2. npm install && npx gtm validate (or pnpm/yarn; 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.
|
|
614
670
|
3. Open the repo in Claude Code or Codex and start with stage 00-intake.`);
|
|
615
671
|
return true;
|
|
616
672
|
}
|
|
@@ -626,8 +682,8 @@ Per AGENTS.md this is a mandatory stop: do not improvise the job with ad-hoc scr
|
|
|
626
682
|
}
|
|
627
683
|
|
|
628
684
|
// src/commands/migrate.ts
|
|
629
|
-
import { existsSync as existsSync5, readFileSync as
|
|
630
|
-
import { join as join7, relative as
|
|
685
|
+
import { existsSync as existsSync5, readFileSync as readFileSync6, renameSync, writeFileSync as writeFileSync4 } from "fs";
|
|
686
|
+
import { join as join7, relative as relative3 } from "path";
|
|
631
687
|
import { parseDocument as parseDocument2 } from "yaml";
|
|
632
688
|
function projectYamlFiles(root2) {
|
|
633
689
|
const files = [
|
|
@@ -651,7 +707,7 @@ function runMigrate(root2, opts = {}) {
|
|
|
651
707
|
renameSync(join7(root2, from), join7(root2, to));
|
|
652
708
|
if (to.endsWith(".md")) {
|
|
653
709
|
const p = join7(root2, to);
|
|
654
|
-
|
|
710
|
+
writeFileSync4(p, readFileSync6(p, "utf8").replace(new RegExp(`^# ${from.replace(".", "\\.")}`, "m"), `# ${to}`));
|
|
655
711
|
}
|
|
656
712
|
}
|
|
657
713
|
if (opts.dryRun && changed) {
|
|
@@ -659,8 +715,8 @@ function runMigrate(root2, opts = {}) {
|
|
|
659
715
|
return ok;
|
|
660
716
|
}
|
|
661
717
|
for (const { kind, file } of projectYamlFiles(root2)) {
|
|
662
|
-
const rel =
|
|
663
|
-
const doc = parseDocument2(
|
|
718
|
+
const rel = relative3(root2, file);
|
|
719
|
+
const doc = parseDocument2(readFileSync6(file, "utf8"));
|
|
664
720
|
if (doc.errors.length) {
|
|
665
721
|
console.log(` \u2717 ${rel}: invalid YAML: ${doc.errors[0].message}`);
|
|
666
722
|
ok = false;
|
|
@@ -677,7 +733,7 @@ function runMigrate(root2, opts = {}) {
|
|
|
677
733
|
doc.setIn(["approval", "content_hash"], icpContentHash(doc.toJS()));
|
|
678
734
|
console.log(` re-pinned ICP approval hash in ${rel} (core migration, content approved by the project approver)`);
|
|
679
735
|
}
|
|
680
|
-
if (!opts.dryRun)
|
|
736
|
+
if (!opts.dryRun) writeFileSync4(file, doc.toString({ flowCollectionPadding: false, lineWidth: 0 }));
|
|
681
737
|
} catch (e) {
|
|
682
738
|
console.log(` \u2717 ${rel}: ${e.message}`);
|
|
683
739
|
ok = false;
|
|
@@ -688,7 +744,7 @@ function runMigrate(root2, opts = {}) {
|
|
|
688
744
|
}
|
|
689
745
|
|
|
690
746
|
// src/commands/new-campaign.ts
|
|
691
|
-
import { existsSync as existsSync6, mkdirSync as
|
|
747
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
|
|
692
748
|
import { join as join8 } from "path";
|
|
693
749
|
import { parse } from "yaml";
|
|
694
750
|
function runNewCampaign(root2, campaign) {
|
|
@@ -706,21 +762,21 @@ function runNewCampaign(root2, campaign) {
|
|
|
706
762
|
console.error(`campaigns/${campaign} already exists`);
|
|
707
763
|
return false;
|
|
708
764
|
}
|
|
709
|
-
const projectId = parse(
|
|
765
|
+
const projectId = parse(readFileSync7(projectFile, "utf8"))?.project_id ?? "TODO";
|
|
710
766
|
copyTemplate(join8(KIT_DIR, "templates", "campaign"), dest, { PROJECT_ID: projectId, CAMPAIGN_ID: campaign, OWNER: process.env.GTM_USER ?? "TODO" });
|
|
711
|
-
|
|
712
|
-
|
|
767
|
+
mkdirSync3(join8(dest, "runs"), { recursive: true });
|
|
768
|
+
writeFileSync5(join8(dest, "runs", ".gitkeep"), "");
|
|
713
769
|
console.log(`Created campaigns/${campaign}. Branch: git switch -c campaign/${campaign}`);
|
|
714
770
|
return true;
|
|
715
771
|
}
|
|
716
772
|
|
|
717
773
|
// src/commands/status.ts
|
|
718
|
-
import { existsSync as existsSync7, readdirSync as
|
|
774
|
+
import { existsSync as existsSync7, readdirSync as readdirSync2, readFileSync as readFileSync8 } from "fs";
|
|
719
775
|
import { join as join9 } from "path";
|
|
720
776
|
import { parse as parse2 } from "yaml";
|
|
721
777
|
function readYaml(file) {
|
|
722
778
|
try {
|
|
723
|
-
return parse2(
|
|
779
|
+
return parse2(readFileSync8(file, "utf8"));
|
|
724
780
|
} catch {
|
|
725
781
|
return null;
|
|
726
782
|
}
|
|
@@ -737,7 +793,7 @@ function runStatus(root2) {
|
|
|
737
793
|
const rows = subdirs(join9(root2, "campaigns")).map((id) => {
|
|
738
794
|
const cfg = readYaml(join9(root2, "campaigns", id, "config.yaml")) ?? {};
|
|
739
795
|
const runsDir = join9(root2, "campaigns", id, "runs");
|
|
740
|
-
const last = existsSync7(runsDir) ?
|
|
796
|
+
const last = existsSync7(runsDir) ? readdirSync2(runsDir).filter((f) => f.endsWith(".md")).sort().at(-1) ?? "-" : "-";
|
|
741
797
|
return [id, cfg.stage ?? "?", cfg.gates?.copy?.status ?? "?", cfg.gates?.launch?.status ?? "?", cfg.owner ?? "?", last];
|
|
742
798
|
});
|
|
743
799
|
if (!rows.length) {
|
|
@@ -755,7 +811,7 @@ function runStatus(root2) {
|
|
|
755
811
|
|
|
756
812
|
// src/commands/upgrade.ts
|
|
757
813
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
758
|
-
import { existsSync as existsSync9, readFileSync as
|
|
814
|
+
import { existsSync as existsSync9, readFileSync as readFileSync10 } from "fs";
|
|
759
815
|
import { join as join11 } from "path";
|
|
760
816
|
|
|
761
817
|
// src/changelog.ts
|
|
@@ -774,7 +830,7 @@ function changelogBetween(text, from, to) {
|
|
|
774
830
|
}
|
|
775
831
|
|
|
776
832
|
// src/commands/validate.ts
|
|
777
|
-
import { existsSync as existsSync8, readdirSync as
|
|
833
|
+
import { existsSync as existsSync8, readdirSync as readdirSync3, readFileSync as readFileSync9 } from "fs";
|
|
778
834
|
import { join as join10 } from "path";
|
|
779
835
|
import { parse as parse3 } from "yaml";
|
|
780
836
|
var REQUIRED_CONTEXT = ["intake.md", "icp.md", "icp.yaml", "signals.yaml", "personas.md", "offer.md", "proof.md", "voice.md"];
|
|
@@ -787,7 +843,7 @@ var SKIP_DIRS = ["node_modules", ".git", ".gtm"];
|
|
|
787
843
|
var stageIndex = (s) => Stage.options.indexOf(s);
|
|
788
844
|
function readPackageJson(root2) {
|
|
789
845
|
const p = join10(root2, "package.json");
|
|
790
|
-
return existsSync8(p) ? JSON.parse(
|
|
846
|
+
return existsSync8(p) ? JSON.parse(readFileSync9(p, "utf8")) : null;
|
|
791
847
|
}
|
|
792
848
|
function pinnedVersion(root2) {
|
|
793
849
|
const pkg = readPackageJson(root2);
|
|
@@ -800,7 +856,7 @@ function loadVersioned(r, file, kind, schema) {
|
|
|
800
856
|
}
|
|
801
857
|
let raw;
|
|
802
858
|
try {
|
|
803
|
-
raw = parse3(
|
|
859
|
+
raw = parse3(readFileSync9(file, "utf8"));
|
|
804
860
|
} catch (e) {
|
|
805
861
|
r.err(file, `invalid YAML: ${e.message}`);
|
|
806
862
|
return null;
|
|
@@ -824,7 +880,7 @@ function checkCoreSync(r, root2) {
|
|
|
824
880
|
if (!pinned) return;
|
|
825
881
|
if (!/^\d+\.\d+\.\d+/.test(pinned)) r.err(join10(root2, "package.json"), `${PACKAGE_NAME} must be pinned exactly (got "${pinned}")`);
|
|
826
882
|
const versionFile = join10(root2, ".gtm", "VERSION");
|
|
827
|
-
const synced = existsSync8(versionFile) ?
|
|
883
|
+
const synced = existsSync8(versionFile) ? readFileSync9(versionFile, "utf8").trim() : null;
|
|
828
884
|
if (synced !== GTM_VERSION) r.err(versionFile, `is ${synced ?? "missing"}, installed core is ${GTM_VERSION}: run \`npx gtm sync\``);
|
|
829
885
|
if (!existsSync8(join10(root2, "AGENTS.md"))) r.err(join10(root2, "AGENTS.md"), "missing: run `npx gtm sync`");
|
|
830
886
|
}
|
|
@@ -836,8 +892,8 @@ function checkNoLeadData(r, root2) {
|
|
|
836
892
|
continue;
|
|
837
893
|
}
|
|
838
894
|
if (!/\.(md|ya?ml|txt)$/i.test(rel) || rel === "pnpm-lock.yaml") continue;
|
|
839
|
-
if (/\.ya?ml$/i.test(rel) && /\bTODO\b/.test(
|
|
840
|
-
const found = (
|
|
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));
|
|
841
897
|
if (found.length) r.warn(file, `contains email address(es) ${[...new Set(found)].slice(0, 3).join(", ")}: confirm none are prospects`);
|
|
842
898
|
}
|
|
843
899
|
}
|
|
@@ -882,7 +938,7 @@ function validateCampaign(r, root2, projectId, campaign, icp, signals, icpApprov
|
|
|
882
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`);
|
|
883
939
|
}
|
|
884
940
|
const copyFile = join10(dir, "copy.md");
|
|
885
|
-
const copyApproved = existsSync8(copyFile) ? checkPinnedApproval(r, copyFile, cfg.gates.copy, copyContentHash(
|
|
941
|
+
const copyApproved = existsSync8(copyFile) ? checkPinnedApproval(r, copyFile, cfg.gates.copy, copyContentHash(readFileSync9(copyFile, "utf8")), "copy", `npx gtm approve copy ${campaign}`) : false;
|
|
886
942
|
if (stageIndex(cfg.stage) > stageIndex("copy") && !copyApproved) {
|
|
887
943
|
r.err(cfgFile, `stage is "${cfg.stage}" but gates.copy is not approved, or copy.md changed since approval`);
|
|
888
944
|
}
|
|
@@ -898,7 +954,7 @@ function validateCampaign(r, root2, projectId, campaign, icp, signals, icpApprov
|
|
|
898
954
|
}
|
|
899
955
|
const runs = join10(dir, "runs");
|
|
900
956
|
if (existsSync8(runs)) {
|
|
901
|
-
for (const f of
|
|
957
|
+
for (const f of readdirSync3(runs).filter((f2) => !f2.startsWith("."))) {
|
|
902
958
|
if (!RUN_LOG_NAME.test(f)) r.err(join10(runs, f), "run logs must be named YYYY-MM-DD-<user>[-n].md");
|
|
903
959
|
}
|
|
904
960
|
}
|
|
@@ -923,7 +979,7 @@ function runValidate(root2, onlyCampaign) {
|
|
|
923
979
|
const signals = existsSync8(sigFile) ? loadVersioned(r, sigFile, "signals", SignalsFile) : null;
|
|
924
980
|
if (meta && icp && icp.project_id !== projectId) r.err(icpFile, `project_id "${icp.project_id}" does not match project.yaml "${projectId}"`);
|
|
925
981
|
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(
|
|
982
|
+
const icpApproved = icp ? checkPinnedApproval(r, icpFile, icp.approval, icpContentHash(parse3(readFileSync9(icpFile, "utf8"))), "ICP", "npx gtm approve icp") : false;
|
|
927
983
|
checkNoLeadData(r, root2);
|
|
928
984
|
const campaigns = onlyCampaign ? [onlyCampaign] : subdirs(join10(root2, "campaigns"));
|
|
929
985
|
for (const c of campaigns) {
|
|
@@ -979,7 +1035,7 @@ function runUpgrade(root2, opts) {
|
|
|
979
1035
|
const after = pinnedVersion(root2) ?? target;
|
|
980
1036
|
const changelog = join11(root2, "node_modules", ...PACKAGE_NAME.split("/"), "CHANGELOG.md");
|
|
981
1037
|
if (existsSync9(changelog)) {
|
|
982
|
-
const notes = changelogBetween(
|
|
1038
|
+
const notes = changelogBetween(readFileSync10(changelog, "utf8"), before, after);
|
|
983
1039
|
console.log(`
|
|
984
1040
|
=== Changes ${before} \u2192 ${after} ===
|
|
985
1041
|
${notes || "(no changelog entries)"}
|
|
@@ -992,15 +1048,19 @@ ${notes || "(no changelog entries)"}
|
|
|
992
1048
|
// src/help.ts
|
|
993
1049
|
var COMMANDS = {
|
|
994
1050
|
init: {
|
|
995
|
-
usage: 'gtm init <project-slug> --name "<Project Name>" [--dir <path>]',
|
|
996
|
-
summary:
|
|
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 ".")',
|
|
997
1053
|
details: [
|
|
998
1054
|
"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
|
|
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.",
|
|
1000
1060
|
"Push the result to a PRIVATE repo: it will hold project context."
|
|
1001
1061
|
],
|
|
1002
|
-
writes:
|
|
1003
|
-
examples: ['npx @veleads/gtm init
|
|
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']
|
|
1004
1064
|
},
|
|
1005
1065
|
"new-campaign": {
|
|
1006
1066
|
usage: "gtm new-campaign <campaign-slug>",
|
|
@@ -1123,6 +1183,7 @@ var { values, positionals } = parseArgs({
|
|
|
1123
1183
|
root: { type: "string" },
|
|
1124
1184
|
name: { type: "string" },
|
|
1125
1185
|
dir: { type: "string" },
|
|
1186
|
+
slug: { type: "string" },
|
|
1126
1187
|
by: { type: "string" },
|
|
1127
1188
|
notes: { type: "string" },
|
|
1128
1189
|
date: { type: "string" },
|
|
@@ -1159,7 +1220,7 @@ function run() {
|
|
|
1159
1220
|
if (values.help) return help(command === "help" ? args[0] : command);
|
|
1160
1221
|
switch (command) {
|
|
1161
1222
|
case "init":
|
|
1162
|
-
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 });
|
|
1163
1224
|
case "new-campaign":
|
|
1164
1225
|
return runNewCampaign(root, args[0]);
|
|
1165
1226
|
case "validate":
|
package/kit/AGENTS.md
CHANGED
|
@@ -182,8 +182,9 @@ Every job prints a `run_id`, counts and cost. Copy those into the run log.
|
|
|
182
182
|
|
|
183
183
|
## 9. Onboarding
|
|
184
184
|
|
|
185
|
-
- **New project:**
|
|
186
|
-
|
|
185
|
+
- **New project:** create an empty **private** repo (e.g. `<org>/gtm-project-<slug>`), clone it, and in
|
|
186
|
+
that folder run `npx @veleads/gtm init . --name "<Name>"` (or `init <slug>` to create a new folder).
|
|
187
|
+
Commit, push, then stage `00-intake` begins.
|
|
187
188
|
- **New campaign:** from up-to-date `main`: `git switch -c campaign/<slug>`,
|
|
188
189
|
`npx gtm new-campaign <slug>`, fill `brief.md`, set `stage`, follow the routing table.
|
|
189
190
|
- **New SDR:** read this file and `PROJECT.md`, run `npx gtm status`, then section 2 with a GTM engineer watching.
|
|
@@ -13,7 +13,8 @@ Collect everything later stages need, once, in files, so no one has to re-ask th
|
|
|
13
13
|
- `context/intake.md` (scaffolded from the core template by `gtm init`).
|
|
14
14
|
|
|
15
15
|
## Steps
|
|
16
|
-
1. If the project repo does not exist yet, a GTM engineer creates it:
|
|
16
|
+
1. If the project repo does not exist yet, a GTM engineer creates it: an empty **private** repo, cloned,
|
|
17
|
+
then `npx @veleads/gtm init . --name "<Name>"` inside it (keeps `.git` and README).
|
|
17
18
|
2. Fill `project.yaml`: owners (GTM engineer, SDRs, **project approver**), regions, status `onboarding`.
|
|
18
19
|
3. Walk the questionnaire in `context/intake.md` with the human. For each answer, write what the
|
|
19
20
|
stakeholders said, not your interpretation. Unknown → write `unknown` and add it to open questions.
|