create-cadmo 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/index.js CHANGED
@@ -85,7 +85,7 @@ for (const [src, dest] of plan) {
85
85
 
86
86
  console.log(`\n ${placed} file(s) ${DRY ? 'would be created' : 'created'}, ${skipped} kept.\n`);
87
87
  if (!hasClaude && !NO_CLAUDE) {
88
- console.log(' (No CLAUDE.md/.claude detected — run with --claude to also install /cadmo-* slash commands.)');
88
+ console.log(' (No CLAUDE.md/.claude folder detected — run: npm create cadmo -- --claude (the -- matters: npm swallows flags without it) to also install the /cadmo-* slash commands.)');
89
89
  }
90
90
  console.log(' Next steps:');
91
91
  console.log(' 1. Fill in AGENTS.md — the local map your AI pair reads first');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-cadmo",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Scaffold the Cadmo method into your project \u2014 a right-sized method for building software with AI as your pair.",
5
5
  "keywords": [
6
6
  "cadmo",
@@ -15,17 +15,18 @@
15
15
  ## Where things live
16
16
  - Specs / decisions: <path>
17
17
  - Plans: <path>
18
- - Demands / task board: <where>
18
+ - Demands / task board: <where — GitHub issues works; "none yet" is honest>
19
19
 
20
20
  ## How to verify (the harness — run before saying "done")
21
21
  - Build/type-check: `<command>`
22
22
  - Tests: `<command>`
23
23
  - End-to-end / smoke: `<command>`
24
- - Staging URL: <url> · Production: <url>
24
+ - Staging URL: <url — or "none: ships as a package / binary / store build"> · Production: <url or delivery channel>
25
25
 
26
26
  ## Gates
27
27
  - Plan approved before building; production ships only with explicit human approval.
28
28
  - <any project-specific rule or gotcha>
29
29
 
30
30
  ## The method
31
- Opening choreography, definition of done, the layers — see the Cadmo method (link).
31
+ Opening choreography, definition of done, the layers — see the Cadmo method:
32
+ https://github.com/tiagotorres91/cadmo/blob/main/docs/method.md
@@ -11,7 +11,7 @@ Check each item **against evidence, not memory** — run the commands, don't ass
11
11
  - [ ] **Tested functionally** — the real flow (browser, curl, CLI run), not just linters
12
12
  - [ ] Validated in staging before production (when there is one)
13
13
  - [ ] Acceptance criteria from the spec/plan — checked one by one, quoted
14
- - [ ] Spec updated if a rule changed (same commit) — run `node cadmo/spec-drift.mjs --base origin/main` if present
14
+ - [ ] Spec updated if a rule changed (same commit) — run `node cadmo/spec-drift.mjs` if present (no --base needed: it auto-resolves, remote or not)
15
15
  - [ ] Anything untested? Say it explicitly: "I didn't test X"
16
16
  - [ ] Touched auth/input/data/secrets? → flag that an adversarial security pass is due before shipping
17
17
 
@@ -1,8 +1,8 @@
1
1
  # Example workflow — copy to .github/workflows/spec-drift.yml in your project.
2
2
  # Fails when a file watched by a spec changes and the spec doesn't change with it.
3
3
  # Works on pull_request AND on push (so it fires even if you ship straight to main).
4
- # Audited escape hatch (never silent): write "spec-drift: skip -- <reason>" in the
5
- # latest commit message.
4
+ # Audited escape hatch (never silent): "spec-drift: skip -- <reason>" in the PR body
5
+ # (pull_request runs) or in the latest commit message (push runs).
6
6
  name: spec-drift
7
7
  on:
8
8
  pull_request:
@@ -19,15 +19,24 @@ jobs:
19
19
  with:
20
20
  node-version: "20"
21
21
  - name: specs must change with the code they govern
22
+ env:
23
+ # env var, never inline interpolation — a PR body is attacker-controlled text
24
+ PR_BODY: ${{ github.event.pull_request.body }}
22
25
  run: |
23
- if git log -1 --format=%B | grep -qi 'spec-drift: skip'; then
24
- echo "spec-drift: skip found in commit message — skipping."
25
- exit 0
26
- fi
27
26
  if [ "${{ github.event_name }}" = "pull_request" ]; then
27
+ if printf '%s' "$PR_BODY" | grep -qi 'spec-drift: skip'; then
28
+ echo "spec-drift: skip found in the PR body — skipping."
29
+ exit 0
30
+ fi
28
31
  BASE="origin/${{ github.base_ref }}"
29
32
  else
33
+ if git log -1 --format=%B | grep -qi 'spec-drift: skip'; then
34
+ echo "spec-drift: skip found in commit message — skipping."
35
+ exit 0
36
+ fi
30
37
  BASE="${{ github.event.before }}"
31
- git cat-file -e "${BASE}^{commit}" 2>/dev/null || BASE="$(git rev-parse HEAD~1)"
38
+ # first push of a branch/repo: event.before is all-zeros -> last commit -> empty tree
39
+ git cat-file -e "${BASE}^{commit}" 2>/dev/null \
40
+ || BASE="$(git rev-parse HEAD~1 2>/dev/null || echo 4b825dc642cb6eb9a060e54bf8d69288fbee4904)"
32
41
  fi
33
42
  node cadmo/spec-drift.mjs --base "$BASE"
@@ -30,12 +30,17 @@
30
30
  * the SINGLE grammar shared by spec-drift, cadmo-score and the plugin hook.
31
31
  *
32
32
  * Usage:
33
- * node spec-drift.mjs [--base origin/main] [--dir .] [--suspect-all]
33
+ * node spec-drift.mjs [--base <ref>] [--dir .] [--suspect-all]
34
34
  * node spec-drift.mjs --stamp <spec.md> [--dir .]
35
35
  *
36
+ * With no --base, the first existing ref wins: origin/main → main (when you're
37
+ * on a different branch) → HEAD~1 → the empty tree (single-commit repo: the
38
+ * whole initial commit is the diff). A solo repo with no remote just works.
39
+ *
36
40
  * Escape hatch (audited, not silent): if a change genuinely doesn't alter any
37
- * documented rule, say so where reviewers can see it — e.g. run the CI step
38
- * only when the commit message does NOT contain "spec-drift: skip — <reason>".
41
+ * documented rule, say so where reviewers can see it — the shipped workflow
42
+ * honors "spec-drift: skip — <reason>" in the PR body (pull_request runs) or
43
+ * in the latest commit message (push runs).
39
44
  */
40
45
  import { execFileSync } from 'node:child_process';
41
46
  import crypto from 'node:crypto';
@@ -49,8 +54,27 @@ function opt(name, fallback) {
49
54
  const i = args.indexOf(name);
50
55
  return i >= 0 && args[i + 1] ? args[i + 1] : fallback;
51
56
  }
52
- const BASE = opt('--base', 'origin/main');
53
57
  const ROOT = path.resolve(opt('--dir', '.'));
58
+ // sha of git's empty tree — diffing against it means "everything is new"
59
+ const EMPTY_TREE = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
60
+ function refExists(ref) {
61
+ try {
62
+ execFileSync('git', ['rev-parse', '--verify', '--quiet', `${ref}^{commit}`], { cwd: ROOT, stdio: ['ignore', 'ignore', 'ignore'] });
63
+ return true;
64
+ } catch { return false; }
65
+ }
66
+ function resolveBase() {
67
+ if (refExists('origin/main')) return 'origin/main';
68
+ let branch = '';
69
+ try {
70
+ branch = execFileSync('git', ['symbolic-ref', '--short', 'HEAD'], { cwd: ROOT, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
71
+ } catch { /* detached HEAD */ }
72
+ if (branch !== 'main' && refExists('main')) return 'main';
73
+ if (refExists('HEAD~1')) return 'HEAD~1';
74
+ return EMPTY_TREE;
75
+ }
76
+ const BASE_GIVEN = opt('--base', null);
77
+ const BASE = BASE_GIVEN || resolveBase();
54
78
  const STAMP = opt('--stamp', null);
55
79
  const SUSPECT_ALL = args.includes('--suspect-all');
56
80
  if (args.includes('--stamp') && !STAMP) {
@@ -147,12 +171,19 @@ if (STAMP) {
147
171
  }
148
172
 
149
173
  // --- changed files vs base ---
174
+ // The empty tree has no merge base, so it gets a two-dot diff; refs get three-dot.
175
+ const RANGE = BASE === EMPTY_TREE ? [BASE, 'HEAD'] : [`${BASE}...HEAD`];
150
176
  let changed;
151
177
  try {
152
- changed = execFileSync('git', ['diff', '--name-only', `${BASE}...HEAD`], { cwd: ROOT, encoding: 'utf8' })
178
+ changed = execFileSync('git', ['diff', '--name-only', ...RANGE], { cwd: ROOT, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] })
153
179
  .split('\n').filter(Boolean);
180
+ if (!BASE_GIVEN) {
181
+ console.log(`spec-drift: no --base given — diffing against ${BASE === EMPTY_TREE ? 'the empty tree (single-commit repo: the whole initial commit)' : BASE}.`);
182
+ }
154
183
  } catch (e) {
155
- console.error(`spec-drift: cannot diff against ${BASE} ${e.message}`);
184
+ const detail = (e.stderr ? e.stderr.toString() : e.message).split(/\r?\n/).find(Boolean) || '';
185
+ console.error(`spec-drift: cannot diff against ${BASE} — ${detail}`);
186
+ console.error('spec-drift: is this a git repository with at least one commit? Pass --base <ref> to pick the comparison point explicitly.');
156
187
  process.exit(2);
157
188
  }
158
189
 
@@ -160,7 +191,14 @@ const specs = [];
160
191
  for (const f of mdFiles(ROOT)) {
161
192
  const text = readSpec(f);
162
193
  const watches = watchesOf(text);
163
- if (watches) specs.push({ spec: path.relative(ROOT, f).split(BS).join('/'), watches, reviewed: reviewedOf(text) });
194
+ const rel = path.relative(ROOT, f).split(BS).join('/');
195
+ if (watches) {
196
+ specs.push({ spec: rel, watches, reviewed: reviewedOf(text) });
197
+ } else if (text.startsWith('<!--') && /^watches:/m.test(text)) {
198
+ // the template ships with a leading HTML comment; front matter placed after
199
+ // it is invisible to the grammar — a spec that LOOKS guarded but isn't.
200
+ console.error(`WARNING: ${rel} has a watches: line but does not start with --- (front matter must be the very first bytes — delete the leading comment). This spec is NOT being checked.`);
201
+ }
164
202
  }
165
203
 
166
204
  if (!specs.length) {