create-cadmo 0.3.0 → 0.4.1
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
|
|
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
package/templates/AGENTS.md
CHANGED
|
@@ -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
|
|
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
|
|
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):
|
|
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
|
-
|
|
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"
|
package/templates/spec-drift.mjs
CHANGED
|
@@ -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
|
|
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 —
|
|
38
|
-
*
|
|
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',
|
|
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
|
-
|
|
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,15 @@ const specs = [];
|
|
|
160
191
|
for (const f of mdFiles(ROOT)) {
|
|
161
192
|
const text = readSpec(f);
|
|
162
193
|
const watches = watchesOf(text);
|
|
163
|
-
|
|
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.replace(/^<!--[^]*?-->/, ''))) {
|
|
198
|
+
// front matter placed AFTER a leading HTML comment is invisible to the
|
|
199
|
+
// grammar — a spec that LOOKS guarded but isn't. (A watches: example still
|
|
200
|
+
// INSIDE the comment — the untouched template — stays silent.)
|
|
201
|
+
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.`);
|
|
202
|
+
}
|
|
164
203
|
}
|
|
165
204
|
|
|
166
205
|
if (!specs.length) {
|