@piercebarney/whs-eleventy 2026.9.10 → 2026.9.22

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/lib/_project.js CHANGED
@@ -40,12 +40,16 @@ function openRequests(root = ROOT) {
40
40
  // 1. WHS_STANDARD env var (the authoring repo points this at itself)
41
41
  // 2. the copy bundled into this package at publish time (standard/)
42
42
  // 3. the legacy ~/.claude/standards/web-house-style symlink
43
+ // With { preferLocal: true } the ~/.claude checkout is tried before the bundled
44
+ // copy (still behind WHS_STANDARD) — for `whs status`, which asks whether a
45
+ // newer standard exists on this machine rather than measuring against the pin.
43
46
  // Returns an absolute dir path, or null if none of them has a core.md.
44
- function resolveStandard() {
47
+ function resolveStandard({ preferLocal = false } = {}) {
48
+ const bundled = path.join(__dirname, "..", "standard");
49
+ const local = path.join(os.homedir(), ".claude", "standards", "web-house-style");
45
50
  const candidates = [
46
51
  process.env.WHS_STANDARD,
47
- path.join(__dirname, "..", "standard"),
48
- path.join(os.homedir(), ".claude", "standards", "web-house-style"),
52
+ ...(preferLocal ? [local, bundled] : [bundled, local]),
49
53
  ].filter(Boolean);
50
54
  for (const dir of candidates) {
51
55
  if (fs.existsSync(path.join(dir, "core.md"))) return dir;
package/lib/compliance.js CHANGED
@@ -717,9 +717,9 @@ function changelogSlugs(changelog, pin) {
717
717
  return [...slugs];
718
718
  }
719
719
 
720
- function versionDrift() {
720
+ function versionDrift(standardDir = STANDARD) {
721
721
  const pin = (claudeMd.match(/standard-version:\s*([0-9-]+)/) || [])[1];
722
- if (!STANDARD) {
722
+ if (!standardDir) {
723
723
  return {
724
724
  pin,
725
725
  current: null,
@@ -728,12 +728,12 @@ function versionDrift() {
728
728
  ],
729
729
  };
730
730
  }
731
- const coreMd = read("core.md", STANDARD) || "";
731
+ const coreMd = read("core.md", standardDir) || "";
732
732
  const current = (coreMd.match(/\*\*Version:\*\*\s*([0-9-]+)/) || [])[1];
733
733
  if (!pin || !current) return { pin, current, rows: [] };
734
734
  if (pin >= current) return { pin, current, rows: [] };
735
735
 
736
- const changelog = read("CHANGELOG.md", STANDARD) || "";
736
+ const changelog = read("CHANGELOG.md", standardDir) || "";
737
737
  // Worst-case severity per slug: one breaking touch marks it breaking for
738
738
  // good, even if a later (or earlier) entry touching the same slug since
739
739
  // the pin was non-breaking.
package/lib/doctor.js CHANGED
@@ -21,7 +21,7 @@
21
21
  const fs = require("node:fs");
22
22
  const path = require("node:path");
23
23
  const { execSync } = require("node:child_process");
24
- const { ROOT, projectRequire, openRequests } = require("./_project.js");
24
+ const { ROOT, projectRequire, tryProjectRequire, openRequests } = require("./_project.js");
25
25
  const { asRegexMap } = require("./header-expect.js");
26
26
 
27
27
  const CACHE = path.join(ROOT, ".cache", "doctor.json");
@@ -29,7 +29,6 @@ const CACHE = path.join(ROOT, ".cache", "doctor.json");
29
29
  // Loaded lazily (inside the functions) so requiring this module for its API
30
30
  // doesn't fail in a dir without a project tree.
31
31
  const loadSite = () => projectRequire("src/_data/site.js");
32
- const loadThirdparties = () => projectRequire("src/_data/thirdparties.js");
33
32
 
34
33
  // Expected header value patterns — the single source (header-expect.js) also
35
34
  // feeds the /audit/ page's live-header check via _data/headerExpect.js.
@@ -51,8 +50,8 @@ function runRepoChecks({ preflight = false } = {}) {
51
50
  const checks = [];
52
51
  const add = (id, label, ok, severity, detail) =>
53
52
  checks.push({ id, label, ok, severity: ok ? "ok" : severity, detail });
54
- const site = loadSite();
55
- const thirdparties = loadThirdparties();
53
+ const site = tryProjectRequire("src/_data/site.js", null);
54
+ const thirdparties = tryProjectRequire("src/_data/thirdparties.js", null);
56
55
 
57
56
  // 1. Security headers declared in netlify.toml
58
57
  const toml = readText("netlify.toml");
@@ -74,21 +73,31 @@ function runRepoChecks({ preflight = false } = {}) {
74
73
  );
75
74
 
76
75
  // 2. Every origin the CSP grants is a declared third party
77
- const cspMatch = toml.match(/Content-Security-Policy\s*=\s*"([^"]+)"/);
78
- const cspOrigins = cspMatch
79
- ? [...new Set((cspMatch[1].match(/https?:\/\/[^\s;'"]+/g) || []).map(originOf))]
80
- : [];
81
- const declaredOrigins = new Set(
82
- thirdparties.flatMap((t) => [t.origin, ...(t.cspOrigins || [])]),
83
- );
84
- const undeclared = cspOrigins.filter((o) => !declaredOrigins.has(o));
85
- add(
86
- "csp-thirdparties",
87
- "CSP origins all appear in src/_data/thirdparties.js",
88
- undeclared.length === 0,
89
- "warn",
90
- undeclared.length ? `undeclared: ${undeclared.join(", ")}` : "in sync",
91
- );
76
+ if (!thirdparties) {
77
+ add(
78
+ "csp-thirdparties",
79
+ "CSP origins all appear in src/_data/thirdparties.js",
80
+ false,
81
+ "blocker",
82
+ "src/_data/thirdparties.js missing",
83
+ );
84
+ } else {
85
+ const cspMatch = toml.match(/Content-Security-Policy\s*=\s*"([^"]+)"/);
86
+ const cspOrigins = cspMatch
87
+ ? [...new Set((cspMatch[1].match(/https?:\/\/[^\s;'"]+/g) || []).map(originOf))]
88
+ : [];
89
+ const declaredOrigins = new Set(
90
+ thirdparties.flatMap((t) => [t.origin, ...(t.cspOrigins || [])]),
91
+ );
92
+ const undeclared = cspOrigins.filter((o) => !declaredOrigins.has(o));
93
+ add(
94
+ "csp-thirdparties",
95
+ "CSP origins all appear in src/_data/thirdparties.js",
96
+ undeclared.length === 0,
97
+ "warn",
98
+ undeclared.length ? `undeclared: ${undeclared.join(", ")}` : "in sync",
99
+ );
100
+ }
92
101
  }
93
102
 
94
103
  // 3. .nvmrc matches the running Node major
@@ -190,8 +199,18 @@ function runRepoChecks({ preflight = false } = {}) {
190
199
  }
191
200
 
192
201
  // 8. site.url is a clean absolute origin
193
- const cleanUrl = /^https:\/\/[^/]+\.[a-z]{2,}$/i.test(site.url);
194
- add("site-url", "src/_data/site.js url is a clean https origin", cleanUrl, "warn", site.url);
202
+ if (!site) {
203
+ add(
204
+ "site-url",
205
+ "src/_data/site.js url is a clean https origin",
206
+ false,
207
+ "blocker",
208
+ "src/_data/site.js missing",
209
+ );
210
+ } else {
211
+ const cleanUrl = /^https:\/\/[^/]+\.[a-z]{2,}$/i.test(site.url);
212
+ add("site-url", "src/_data/site.js url is a clean https origin", cleanUrl, "warn", site.url);
213
+ }
195
214
 
196
215
  return checks;
197
216
  }
package/lib/status.js CHANGED
@@ -4,37 +4,56 @@
4
4
  // full codebase sweep. Cheap enough to run on every Claude Code SessionStart.
5
5
  //
6
6
  // whs status print the version delta + the re-check rows
7
- // whs status --hook silent when current; one line when behind (for the
8
- // SessionStart hook — never non-zero, never noisy)
7
+ // whs status --hook silent when current; one line when behind, plus one
8
+ // more if the project's src/_data/dashboard.js
9
+ // (core.md#adopting's one-time setup checklist) has
10
+ // unconfirmed items (for the SessionStart hook —
11
+ // never non-zero, never noisy)
9
12
  //
10
13
  // Exit code is always 0: like `compliance` without --strict, drift is a
11
14
  // backlog signal, not a build break.
12
15
 
13
16
  const fs = require("node:fs");
14
- const os = require("node:os");
15
17
  const path = require("node:path");
16
18
 
17
- // `status` answers "is there a newer standard available on this machine?" — so
18
- // before the compliance module resolves its standard text, prefer the local
19
- // canonical checkout (the ~/.claude symlink the README keeps) when one exists
20
- // and nothing more specific was set. `whs compliance` still measures the
21
- // codebase against the tooling's own bundled snapshot; `status` measures the
22
- // pin against the freshest tree available, which is what a "you're behind"
23
- // nudge should reflect. Machines without the symlink (CI, a fresh clone) fall
24
- // back to the bundled snapshot unchanged.
25
- if (!process.env.WHS_STANDARD) {
26
- const local = path.join(os.homedir(), ".claude", "standards", "web-house-style");
27
- if (fs.existsSync(path.join(local, "core.md"))) process.env.WHS_STANDARD = local;
19
+ const { versionDrift } = require("./compliance.js");
20
+ const { resolveStandard, tryProjectRequire } = require("./_project.js");
21
+
22
+ // Best-effort: count src/_data/dashboard.js items still missing a verifiedOn
23
+ // date (core.md#adopting's one-time setup checklist, rendered on /audit/'s
24
+ // Infra tab). Never throws — a missing or malformed dashboard.js just means
25
+ // nothing to report, same as doctor.js's live tier treats "unavailable".
26
+ function pendingDashboardCount() {
27
+ const dashboard = tryProjectRequire("src/_data/dashboard.js", null);
28
+ if (!dashboard || !Array.isArray(dashboard.items)) return 0;
29
+ return dashboard.items.filter((i) => !(i && String(i.verifiedOn || "").trim())).length;
28
30
  }
29
31
 
30
- const { versionDrift } = require("./compliance.js");
31
- const { resolveStandard } = require("./_project.js");
32
+ function printDashboardPending() {
33
+ let pending;
34
+ try {
35
+ pending = pendingDashboardCount();
36
+ } catch {
37
+ return;
38
+ }
39
+ if (pending > 0) {
40
+ console.log(`${pending} manual checklist item(s) pending — see /audit/`);
41
+ }
42
+ }
43
+
44
+ // `status` answers "is there a newer standard available on this machine?" — so
45
+ // it resolves the standard text with { preferLocal: true }: the local canonical
46
+ // checkout (the ~/.claude symlink the README keeps) wins over the tooling's
47
+ // bundled snapshot when it exists. `whs compliance` deliberately measures the
48
+ // codebase against the bundled snapshot; `status` measures the pin against the
49
+ // freshest tree available, which is what a "you're behind" nudge should
50
+ // reflect. Machines without the symlink (CI, a fresh clone) fall back to the
51
+ // bundled snapshot unchanged. WHS_STANDARD still wins over both.
32
52
 
33
53
  // Distinct CHANGELOG dates after the pin — the "N versions behind" count. The
34
54
  // standard versions by date, and several entries can share one date (one
35
55
  // release); this counts releases, not entries.
36
- function versionsBehind(pin) {
37
- const std = resolveStandard();
56
+ function versionsBehind(pin, std) {
38
57
  if (!std || !pin) return 0;
39
58
  try {
40
59
  const changelog = fs.readFileSync(path.join(std, "CHANGELOG.md"), "utf8");
@@ -51,12 +70,14 @@ function versionsBehind(pin) {
51
70
 
52
71
  function main() {
53
72
  const hook = process.argv.includes("--hook");
54
- const { pin, current, rows } = versionDrift();
73
+ const std = resolveStandard({ preferLocal: true });
74
+ const { pin, current, rows } = versionDrift(std);
55
75
 
56
76
  const unresolved =
57
77
  rows.length === 1 && rows[0].startsWith("MANUAL: standard text not resolvable");
58
78
  if (unresolved) {
59
79
  if (!hook) console.log(rows[0]);
80
+ else printDashboardPending();
60
81
  return;
61
82
  }
62
83
 
@@ -67,12 +88,14 @@ function main() {
67
88
  ? `web house style: up to date (pinned ${pin}, standard at ${current})`
68
89
  : "web house style: standard-version not pinned in CLAUDE.md",
69
90
  );
91
+ } else {
92
+ printDashboardPending();
70
93
  }
71
94
  return;
72
95
  }
73
96
 
74
97
  const breaking = rows.filter((r) => r.startsWith("BREAKING:")).length;
75
- const versions = versionsBehind(pin);
98
+ const versions = versionsBehind(pin, std);
76
99
  const behind = `${versions} version${versions === 1 ? "" : "s"} behind`;
77
100
  const recheck = `${rows.length} re-check${rows.length === 1 ? "" : "s"}${
78
101
  breaking ? ` (${breaking} breaking)` : ""
@@ -83,6 +106,7 @@ function main() {
83
106
  `⚠ web house style: ${behind}, ${recheck} — pinned ${pin}, standard at ${current}. ` +
84
107
  `Run /whs:upgrade to catch up.`,
85
108
  );
109
+ printDashboardPending();
86
110
  return;
87
111
  }
88
112
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@piercebarney/whs-eleventy",
3
- "version": "2026.9.10",
3
+ "version": "2026.9.22",
4
4
  "description": "The web house style's Eleventy + Netlify tooling — the compliance sweep, the infra doctor, the link/CSP integrity check, and the a11y scan, shared by every project on the stack.",
5
5
  "bin": {
6
6
  "whs": "cli.js"
@@ -4,7 +4,10 @@ Slug-keyed. A change to a shared concept (a `core.md` chapter) lists the slug so
4
4
  "when did the deploy philosophy last change, and which docs moved" is answerable
5
5
  at a glance. Stack-only changes list the file.
6
6
 
7
- Format: `## <date>` → `### core: <slug>` / `### <stack>` entries.
7
+ Format: `## <date>` → `### core: <slug>` / `### <stack>` entries. Template
8
+ `.claude/` tooling (commands, agents, hooks) and repo-root routing files (e.g.
9
+ `COWORK.md`) are recorded here too, under `### templates/<stack>` / `### <file>`
10
+ — the same as any mechanism an adopting project consumes.
8
11
 
9
12
  **Severity.** Every `## <date>` heading also carries `[breaking]` or
10
13
  `[non-breaking]`. An entry is **breaking** if a project that was fully
@@ -18,6 +21,372 @@ enforced, new *opt-in* chapters/capabilities, tooling-only changes — is
18
21
 
19
22
  ---
20
23
 
24
+ ## 2026-09-22 [non-breaking]
25
+
26
+ ### packages/whs-eleventy
27
+
28
+ `doctor.js`'s `runRepoChecks()` crashed with an unhandled `MODULE_NOT_FOUND`
29
+ when a project had no `src/` tree at all, instead of reporting the missing
30
+ layout as a finding — found while adopting a real project via `/whs:adopt`
31
+ and filed as `feedback/2026-09-22-doctor-crashes-on-no-src-layout.md`. Fixed
32
+ in `7b43511`; this release publishes the fix. No `core.md` chapter,
33
+ `stacks/*.md`, or `index.json` content changed.
34
+
35
+ ---
36
+
37
+ ## 2026-09-18 — Republish 2026-09-17-2 as a clean bare-date version [non-breaking]
38
+
39
+ No new content. This bump republishes everything already documented under
40
+ the 2026-09-17 entries below as a clean bare-date version, specifically to
41
+ trigger the npm publish of `@piercebarney/whs-eleventy` that the
42
+ 2026-09-17-2 same-day-suffix mechanism deliberately does not trigger on its
43
+ own (per that suffix's own documented exception in `CLAUDE.md`'s Versions
44
+ section: a suffixed pin gets no git tag and no npm publish). `core.md`,
45
+ `stacks/*.md`, `index.json`, and every `standard-version:` pin move from
46
+ `2026-09-17-2` to `2026-09-18`; `packages/whs-eleventy/package.json` moves to
47
+ the corresponding npm version. Nothing else changes.
48
+
49
+ ---
50
+
51
+ ## 2026-09-17 — /whs:new, and both commands go global [non-breaking]
52
+
53
+ ### core: adopting
54
+
55
+ A new paragraph, "Mechanized new-project start," names `/whs:new` —
56
+ `templates/commands/whs/new.md` — as the pre-scaffold sibling to
57
+ `/whs:build`/`/whs:upgrade`/`/whs:adopt`: it mechanizes this chapter's
58
+ "Starting point" paragraph plus the six-question flow for a project that
59
+ doesn't exist yet. Before the user commits to a framework it reports each
60
+ stack's real readiness — derived live from `index.json`'s `status` field
61
+ plus an actual filesystem check for that stack's `bin/init` and stage
62
+ agents, not a static description that can drift — then applies this
63
+ paragraph's own fit test: an idea that doesn't honestly fit any offered
64
+ stack routes to the same `feedback/` mechanism `/whs:adopt` already uses,
65
+ filed before scaffolding rather than worked around. Knowingly choosing a
66
+ stack still marked `draft` is explicitly **not** that case — it's an
67
+ informed choice against an already-tracked, deliberately-deferred
68
+ limitation, not a new gap. Once a stack is chosen it scaffolds via the
69
+ existing, unmodified `templates/new-project.sh` and hands off to
70
+ `/whs:build` where a guided build actually exists (Eleventy today);
71
+ for a draft-stack scaffold it reports the partial result honestly and
72
+ points at that stack's own `TEMPLATE.md`.
73
+
74
+ ### stacks/eleventy-netlify.md
75
+
76
+ A short cross-reference in its own `#adopting` section: for a brand-new
77
+ project, `/whs:new` will report Eleventy fully ready — scaffold, the full
78
+ `/whs:build` pipeline, and compliance tooling all already in place, the
79
+ one stack where the readiness table's top row is real today.
80
+
81
+ ### stacks/phoenix.md, stacks/sveltekit-netlify.md
82
+
83
+ Each gets the matching honest counterpart: `/whs:new` reports them as
84
+ `draft` — scaffold-only via `templates/new-project.sh`, no `bin/init`
85
+ (manual placeholder fill per each stack's own `TEMPLATE.md`), no guided
86
+ `/whs:build` equivalent, no compliance tooling — the same gap list their
87
+ own `#adopting` sections already carry, not a new one.
88
+
89
+ ### templates/commands
90
+
91
+ New file, `templates/commands/whs/new.md` — the fourth command in the
92
+ `/whs:build` / `/whs:upgrade` / `/whs:adopt` family, this one for a
93
+ destination that doesn't exist yet, not even a directory. It walks
94
+ `templates/concept-brief.md` conversationally (the same ideation
95
+ `COWORK.md`'s "New project" flow already does, run here instead), stops
96
+ before answering the framework question to show the live readiness table,
97
+ applies the fit test with its three outcomes (fits cleanly, including a
98
+ knowing `draft` choice / no offered stack fits, routing to `feedback/` /
99
+ abandon), then scaffolds via `templates/new-project.sh` unmodified and
100
+ reports one of three real outcomes matching the script's actual behavior:
101
+ `bin/init` ran (Eleventy), a partial scaffold with no `bin/init` (Phoenix,
102
+ SvelteKit today), or a genuine unexpected failure.
103
+
104
+ ### README.md
105
+
106
+ Both `/whs:new` and `/whs:adopt` switch distribution model: instead of a
107
+ per-project `cp` of the command file into `.claude/commands/whs/`, the
108
+ `## Home` section now documents a single global symlink,
109
+ `~/.claude/commands/whs/` → this repo's `templates/commands/whs/`, set up
110
+ once per machine. This is a real behavior change for `/whs:adopt`, not
111
+ just new documentation for `/whs:new`: a machine that already did the old
112
+ per-project copy-in keeps working for that project, but every new project
113
+ now picks up both commands automatically, and editing a command's source
114
+ here updates what's "installed" everywhere with no copy-and-drift step.
115
+ The "Onboarding an existing project" subsection drops its old copy
116
+ instructions and now just points at `## Home`.
117
+
118
+ This bump moves the doc pins to `2026-09-17-2` (a same-day suffix — see
119
+ this repo's `CLAUDE.md`'s Versions section): `2026-09-17` was already used
120
+ today for the `/whs:adopt` entry below. `packages/*/package.json` is left
121
+ untouched on purpose; this release gets no git tag and no npm publish on
122
+ its own.
123
+
124
+ ---
125
+
126
+ ## 2026-09-17 — /whs:adopt mechanizes onboarding for a preexisting project [non-breaking]
127
+
128
+ ### core: adopting
129
+
130
+ A new paragraph, "Mechanized onboarding," names `/whs:adopt` — copied from
131
+ `templates/commands/whs/adopt.md` into a project's own
132
+ `.claude/commands/whs/adopt.md` — as the command that runs this entire
133
+ chapter end-to-end inside a project that has never adopted the standard,
134
+ including one on a stack this repo has no binding for. It detects the
135
+ stack, runs the six-question flow already documented here, and, before
136
+ writing anything, judges whether the project is even compatible with the
137
+ standard: a genuine out-of-scope project (native mobile, an embedded CLI,
138
+ anything the standard's domain never covered) gets a clean decline with
139
+ nothing written or filed; a real gap in the standard routes to the existing
140
+ `feedback/` mechanism, the same "Starting point" reasoning this chapter
141
+ already applies to a new-project brief that doesn't honestly fit an offered
142
+ stack. Once adoption proceeds it writes the `## House style` block, wires in
143
+ real tooling where it exists, and produces — but deliberately does not
144
+ work — an initial migration backlog, which stays incremental by hand or via
145
+ `/whs:upgrade` afterward. Nothing here changes what a compliant project
146
+ already had to do; this is a new, opt-in way to reach the same state
147
+ `core.md#adopting` already describes.
148
+
149
+ ### stacks/eleventy-netlify.md
150
+
151
+ Its own `#adopting` section gets a short cross-reference: for a project
152
+ `/whs:adopt` onboards, it proposes `@piercebarney/whs-eleventy` as a
153
+ devDependency plus `check`/`compliance`/`status` npm scripts (asking first
154
+ if a conflicting script already exists or the build setup looks
155
+ non-trivial), then runs `npm run compliance` for the initial backlog — the
156
+ one stack where the mechanized path can actually run a real check.
157
+
158
+ ### stacks/phoenix.md, stacks/sveltekit-netlify.md
159
+
160
+ Each gets the same kind of cross-reference, honestly scoped down: since
161
+ neither draft binding has compliance-sweep tooling yet (the same
162
+ `mix a11y`/`links.check`/`whs.check` gap their own `#adopting` sections
163
+ already note), `/whs:adopt` can still detect the stack and record the
164
+ `## House style` block with the gate command filled in from what the
165
+ project already runs (`mix check` / `npm run check`), but the migration
166
+ backlog for these two comes from walking `core.md`'s chapters by hand
167
+ rather than from an automated sweep.
168
+
169
+ ### templates/commands
170
+
171
+ New file, `templates/commands/whs/adopt.md` — the command itself, the
172
+ third in the `/whs:build` / `/whs:upgrade` / `/whs:adopt` family, this one
173
+ for a project that's never touched the standard at all. Unlike the other
174
+ two it assumes nothing scaffolded yet (no stage agents, no `## House style`
175
+ block, possibly no stack binding), so it runs mostly inline: two guards
176
+ before starting (an already-adopted project gets pointed at `/whs:upgrade`
177
+ instead; a directory with no manifest at all gets pointed at
178
+ `templates/new-project.sh` instead), then stack detection, the
179
+ six-question flow, the compatibility judgment with its three outcomes
180
+ (compatible / out-of-scope-not-a-defect / genuine-gap-file-a-feedback-note),
181
+ recording the `## House style` block, tiered tooling wiring, and a
182
+ committed `.claude/whs-migration-backlog.md` as the durable artifact —
183
+ never a chat transcript — of what the project still owes.
184
+
185
+ ### README.md
186
+
187
+ New "Onboarding an existing project" subsection under "How to use it,"
188
+ documenting the one-time manual copy-in (`cp
189
+ ~/.claude/standards/web-house-style/templates/commands/whs/adopt.md
190
+ .claude/commands/whs/adopt.md`) and pointing at `/whs:adopt` — the same
191
+ "standard checked out at `~/.claude/standards/web-house-style/`"
192
+ assumption every adopted project's own `CLAUDE.md` directive already
193
+ makes, no new install mechanism.
194
+
195
+ ### CLAUDE.md, .claude/agents/infra.md
196
+
197
+ Both the root `CLAUDE.md` agent-roster listing and `infra`'s own Scope
198
+ section are extended to name stack-agnostic content living directly at the
199
+ `templates/` root — not nested inside any single `templates/<stack>/`,
200
+ e.g. `templates/concept-brief.md`, `templates/new-project.sh`, and now
201
+ `templates/commands/` — as `infra`'s territory. `/whs:adopt` is the first
202
+ file to actually land there; without this the delegation rule this repo's
203
+ own `CLAUDE.md` requires had no answer for who owns it.
204
+
205
+ ---
206
+
207
+ ## 2026-09-13 — One-time adopting setup becomes a tracked, dated confirmation [non-breaking]
208
+
209
+ ### core: adopting
210
+
211
+ The one-time human setup a project owes after adopting/converting/upgrading
212
+ (secrets, host config, DNS) is now tracked the same way `#compliance` already
213
+ tracks its `MANUAL` rows: a committed, dated `verifiedOn` confirmation per
214
+ item, plus a best-effort, non-blocking session-start check that flags
215
+ unconfirmed items. This is not a gate — same reasoning `#compliance` gives
216
+ for keeping `MANUAL` rows out of `#the-gate` — so nothing that was compliant
217
+ before this change becomes non-compliant after it; the mechanism is purely
218
+ additive.
219
+
220
+ ### templates/eleventy-netlify
221
+
222
+ Implements the contract for real: `bin/init` now seeds
223
+ `src/_data/dashboard.js` with the one-time setup items, committed as part of
224
+ the initial commit (a reviewer-caught ordering bug in the original build,
225
+ now fixed). `packages/whs-eleventy`'s `whs status --hook` (already
226
+ SessionStart-hooked) counts unconfirmed dashboard items and prints a
227
+ best-effort pending-count line. The dangling `BUILD-STATUS.md` reference —
228
+ never actually created by anything — is retired from `bin/init` and
229
+ `TEMPLATE.md`.
230
+
231
+ ### stacks/sveltekit-netlify, stacks/phoenix
232
+
233
+ Each gets a short honest note in its `#adopting` mirror that this tracking
234
+ isn't built for that stack yet. No functional or template changes.
235
+
236
+ This bump moves the doc pins to `2026-09-13-2` (a same-day suffix — see this
237
+ repo's `CLAUDE.md`'s Versions section): `2026-09-13` was already used today
238
+ for the republish entry below. `packages/whs-eleventy/package.json` is left
239
+ untouched on purpose; this release gets no git tag and no npm publish on its
240
+ own.
241
+
242
+ ---
243
+
244
+ ## 2026-09-13 — Republish 2026-09-12-2 as a clean bare-date version [non-breaking]
245
+
246
+ No new content. This bump republishes everything already documented under
247
+ the 2026-09-11 and 2026-09-12 entries below as a clean bare-date version,
248
+ specifically to trigger the npm publish of `@piercebarney/whs-eleventy` that
249
+ the 2026-09-12-2 same-day-suffix mechanism deliberately does not trigger on
250
+ its own (per that suffix's own documented exception in `CLAUDE.md`'s
251
+ Versions section: a suffixed pin gets no git tag and no npm publish). `core.md`,
252
+ `stacks/*.md`, `index.json`, and every `standard-version:` pin move from
253
+ `2026-09-12-2` to `2026-09-13`; `packages/whs-eleventy/package.json` moves to
254
+ the corresponding npm version. Nothing else changes.
255
+
256
+ ---
257
+
258
+ ## 2026-09-12 — A same-day-release suffix, two feedback fixes, and an /whs:upgrade false-negative [non-breaking]
259
+
260
+ ### templates/eleventy-netlify
261
+
262
+ `/whs:upgrade`'s "Before starting" guard stopped immediately whenever
263
+ `npm run status` reported the pin current — trusting a signal that itself
264
+ trusts whatever `@piercebarney/whs-eleventy` version happens to be
265
+ installed. Found today: that installed version's `resolveStandard()`
266
+ predates the `preferLocal` live-checkout preference entirely, so it always
267
+ reads its own frozen bundled snapshot — meaning `npm run status` can report
268
+ "up to date" while the real standard has moved well past it, and the guard
269
+ would stop before ever reaching the CHANGELOG read that would have caught
270
+ it. The guard now also checks `~/.claude/standards/web-house-style/
271
+ CHANGELOG.md` directly for any `## <date>` heading after the pin, and only
272
+ stops if that agrees with `npm run status`.
273
+
274
+ ### bin/check
275
+
276
+ `feedback/` is now excluded from check 4's real-adopter-name denylist.
277
+ Reported same-day: `feedback/README.md`'s own filing template asks for the
278
+ real adopter's directory name in `project:`, which the denylist rejected —
279
+ the documented feedback process couldn't pass its own gate as written. The
280
+ sterility grep (check 3) still applies to everything else in a report; only
281
+ the project-name denylist is exempted, and only for `feedback/`.
282
+
283
+ ### bin/set-version, CLAUDE.md
284
+
285
+ Same problem, a real fix instead of a workaround: `bin/set-version` now
286
+ accepts `<YYYY-MM-DD>-N` for a same-day re-release, and refuses a bare
287
+ re-run of a date already in use (telling you to add a suffix) instead of
288
+ silently no-opping. The doc pins (`core.md`, `stacks/*.md`, `index.json`,
289
+ every `standard-version:`) take the full suffixed string; `bin/check`'s
290
+ version-agreement check now compares base dates (suffix stripped) so a
291
+ suffixed pin still agrees with `packages/*/package.json`, which is left
292
+ untouched on purpose — no same-day-aware npm version scheme exists, so a
293
+ suffixed pin gets no git tag and no npm publish, a narrow, documented
294
+ exception to "a pin corresponds to both a git tag and the npm version."
295
+ This version bump (to 2026-09-12-2) is the suffix mechanism's own first
296
+ real use, closing out both feedback reports filed today.
297
+
298
+ ---
299
+
300
+ ## 2026-09-11 — Feature flags, with ads as the reference implementation [non-breaking]
301
+
302
+ ### core: config-idiom
303
+
304
+ Feature flags formalized as a named pattern: boolean env vars, one name per
305
+ concern (`FEATURE_<NAME>`), read through the same centralised env-access
306
+ module as build context. Additive — `#config-idiom` already required
307
+ centralised environment access; this names the flag shape so future features
308
+ follow one convention instead of inventing their own.
309
+
310
+ ### core: ads
311
+
312
+ Ad serving now also respects a kill-switch flag (`#config-idiom`), independent
313
+ of the `ads:` provider declaration — the declaration controls whether the
314
+ plumbing exists, the flag controls whether it's currently allowed to render.
315
+ Defaults to on, so an unset flag preserves prior behavior.
316
+
317
+ ### templates/eleventy-netlify
318
+
319
+ `_data/build.js` gains `flags.ads` (`FEATURE_ADS`, default on); `ad-slot.njk`
320
+ and `add-ons/ads/layout-head.njk` gate on it. `bin/deploy` and `deploy.yml`
321
+ read the flag's current value from Netlify's own stored environment variables
322
+ (`netlify env:get FEATURE_ADS --context production`) immediately before the
323
+ production build, so the toggle lives in Netlify's dashboard and takes effect
324
+ on the next deploy — consistent with `#ci-cd`'s "deploys are deliberate";
325
+ there is no live no-redeploy toggle for any switch in this standard, ads
326
+ included.
327
+
328
+ ## 2026-09-11 — Implementation work always runs through an agent [non-breaking]
329
+
330
+ ### templates/eleventy-netlify
331
+
332
+ `CLAUDE.md`'s House-style prose gains a standing convention: any request that
333
+ becomes an actual change is run through the same plan → delegate → verify →
334
+ record loop `/whs:build` already uses per stage — a named stage agent
335
+ (`brand`/`content`/`layout`/`pack`) when the work is its territory, the new
336
+ `infra` agent for config/CI-CD/deploy/security-header/secrets/feature-flag
337
+ work, a general-purpose agent otherwise — never edited inline in the main
338
+ session, except a single `Edit`/`Write` touching 5 lines or fewer in one file.
339
+ `/whs:upgrade`'s step 3 ("otherwise edit directly") is updated to match: it
340
+ now delegates (to `infra` or general-purpose) instead. The exception is
341
+ mechanically enforced, not just documented: `.claude/hooks/agent-only.js`, a
342
+ `PreToolUse` hook wired into `.claude/settings.json`, denies a main-session
343
+ `Edit`/`Write` over the 5-line threshold and any main-session `MultiEdit`,
344
+ telling the assistant to delegate instead — a subagent's own edits (identified
345
+ by the `agent_id` field the hook payload carries only inside a subagent call)
346
+ are never blocked. The "changed lines" check is a positional line compare, not
347
+ a real diff — a full-file rewrite that happens to keep every line in the same
348
+ position reads as small; documented as a known limitation in the hook's own
349
+ comments and `infra.md`, not silently assumed exact.
350
+
351
+ ## 2026-09-10 — COWORK.md backfilled, a sterility fix, and a tooling cleanup [non-breaking]
352
+
353
+ ### COWORK.md
354
+
355
+ Backfilled — landed the same day as the entry below but as its own commit with
356
+ no CHANGELOG entry. The router file a Cowork (Claude Desktop) Project's
357
+ instructions point at: routes between new-project setup (ideates, live-checks
358
+ whether the chosen stack actually has a working pipeline before committing to
359
+ it, stages the brief, scaffolds via `new-project.sh`, hands off to
360
+ `/whs:build`), content work on an existing project (already solved by
361
+ `CONTENT.md` — just recognizes and points at it, plus a standing
362
+ `standard-version` drift check), and converting an existing non-WHS project
363
+ (deliberately parked, same stance as the deferred stacks). Depended on two
364
+ small fixes, also unrecorded until now: `bin/init` seeds a given `--brief`
365
+ file into the new project's `.claude/brief.md`; `/whs:build` defaults to
366
+ reading it from there.
367
+
368
+ ### PROJECT-OVERVIEW.md, CHANGELOG.md
369
+
370
+ Closed a sterility leak: both named the one real project that has adopted the
371
+ standard in plain text, which `CLAUDE.md`'s "Keep it sterile" bans. Replaced
372
+ with an unnamed reference, substance preserved.
373
+
374
+ ### bin/check
375
+
376
+ Added a sub-check (4b): a maintained denylist of known real adopting-project
377
+ names/slugs, `git grep`'d over tracked files. The existing sterile grep (check
378
+ 4) only catches a leaked domain or AdSense publisher id by shape; a bare
379
+ project name has no such shape and was passing silently.
380
+
381
+ ### packages/whs-eleventy
382
+
383
+ Cleanup, no behavior change: `whs status` resolved the standard by mutating
384
+ `process.env.WHS_STANDARD` before requiring `compliance.js` — a side-channel.
385
+ `resolveStandard()` now takes a `{ preferLocal }` option instead;
386
+ `versionDrift()` takes the resolved standard dir as an optional argument.
387
+ Publishes to npm at the next dated release (no `package.json` version bump
388
+ here — nothing changes what a compliant project must do).
389
+
21
390
  ## 2026-09-10 — a one-command standard-version catch-up, and drift that surfaces itself [non-breaking]
22
391
 
23
392
  Catching an adopted project up to a moved-on standard was a hand-written prompt
@@ -94,6 +463,39 @@ true, contradicting the standard's own stated default.
94
463
  keeps `CONTENT.md` + `requests/` and the reserved paragraph; the default
95
464
  keeps neither and shows the in-repo paragraph instead.
96
465
 
466
+ ## 2026-09-08 — /whs:build, the four stage agents, and opt-in verify.sh [non-breaking]
467
+
468
+ Backfilled: this landed the same day as the content-ops entry above but had no
469
+ CHANGELOG entry of its own — a whole build-automation capability was visible
470
+ only in git log. Recorded now so the log is a complete account of what a
471
+ compliant project can rely on, not just what happened to get a dated entry.
472
+
473
+ ### templates/eleventy-netlify
474
+
475
+ - `/whs:build` (`.claude/commands/whs/build.md`) — a Claude Code command that
476
+ runs a scaffolded project's post-`bin/init` work as four fixed stages, always
477
+ in order: Brand → Content → Pages & Layout → Add-on packs. Each stage plans
478
+ first (asking only when the brief/standard doesn't already answer something
479
+ and getting it wrong would force rework), delegates execution to a dedicated
480
+ agent, verifies against `npm run check`, and commits before moving to the
481
+ next stage. Never deploys. Piloted on `eleventy-netlify` only — same "don't
482
+ build a path nothing needs yet" stance as Phoenix/SvelteKit's deferred
483
+ tooling (`TODO.md`).
484
+ - Four stage agents (`.claude/agents/{brand,content,layout,pack}.md`), each
485
+ anchored to a named authority: Brad Frost for Brand (tokens before
486
+ components), Karen McGrane for Content (structure before prose — checks
487
+ `CONTENT.md`'s reservation status before writing anything), Nielsen/Krug for
488
+ Pages & Layout (identify the primary friction point before designing), Kent
489
+ Beck for Add-on packs (one pack per invocation, its own commit, tests first).
490
+
491
+ ### templates/*
492
+
493
+ - Opt-in `.claude/verify.sh` in all three stack templates, wired to a global
494
+ Claude Code Stop hook that lives outside this repo (the operator's own
495
+ `~/.claude/hooks/verify-stop.sh`): a session can't report a build done while
496
+ the project's own gate is red. No-ops for a project that never creates the
497
+ script.
498
+
97
499
  ## 2026-09-04 — sveltekit-netlify's brand token layer, and a Pico gap that predates it [non-breaking]
98
500
 
99
501
  Third of the four functional gaps being closed ahead of the real project
@@ -455,7 +857,7 @@ tag, so `whs compliance --strict` actually gates on being behind on a
455
857
  breaking change rather than treating every changed slug — a typo fix and a
456
858
  broken-mechanism correction alike — as the same easy-to-miss `MANUAL` note.
457
859
  This is the concrete fix for the gap `TODO.md` #1's evidence section
458
- documented: `everydaymoneycalc` being 8 entries behind (several of them
860
+ documented: the one real adopting project being 8 entries behind (several of them
459
861
  breaking, per the backfill above) produced no signal beyond a note buried in
460
862
  a `MANUAL` list.
461
863
 
package/standard/core.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Web project house style — CORE (stack-agnostic)
2
2
 
3
- **Version:** 2026-09-10 · **Status:** active
3
+ **Version:** 2026-09-22 · **Status:** active
4
4
 
5
5
  This is the stack-agnostic contract every web project follows, regardless of
6
6
  framework, host, or CSS system. It says **what** must be true, with concrete
@@ -79,6 +79,12 @@ contents, and uses one mechanism per concern.
79
79
  sidecar files).
80
80
  - Environment access is centralised (see `#seo-urls` for the build-context
81
81
  module and `#secrets` for tokens).
82
+ - **Feature flags** are boolean environment variables, one named flag per
83
+ concern (`FEATURE_<NAME>`), read through the same centralised env-access
84
+ module as build context — never a scattered `process.env` read. Each flag
85
+ defaults to preserving current behaviour when unset, and is documented in
86
+ `.env.example` with what it gates (`#ads`'s visibility flag is the worked
87
+ example).
82
88
 
83
89
  **Verify.** Review.
84
90
 
@@ -704,6 +710,12 @@ ad directives, and documented.
704
710
  gated on the same `isProduction` flag as `noindex`. Serving ads on a preview /
705
711
  staging URL wastes impressions, risks an invalid-traffic policy strike, and
706
712
  pollutes analytics.
713
+ - **Kill switch.** Ad serving (loader, CMP, and units) also respects a feature
714
+ flag (`#config-idiom`) independent of the `ads:` provider declaration — the
715
+ declaration controls whether the plumbing (CSP, CMP, `ads.txt`) exists at
716
+ all; the flag controls whether it's currently allowed to render. Defaults to
717
+ on, so an unset flag preserves existing behaviour; flipping it off is the
718
+ fast path to pausing ads without a code change.
707
719
  - **Root files.** `ads.txt` (and `app-ads.txt` if the account has apps) at the
708
720
  site root, listing authorized sellers, is generated from data or is a reviewed
709
721
  file — not hand-edited over time. Excluded from the sitemap. Treated like
@@ -725,7 +737,8 @@ standard honest and makes the tradeoff explicit and reviewable.
725
737
  **Verify.** The audit page has an **Ads** panel (only when `ads:` ≠ `none`):
726
738
  `ads.txt` present and parseable; the CMP script present; the CSP grants exactly
727
739
  the declared ad origins; any `'unsafe-*'` exception noted; the loader gated on
728
- `isProduction`. The link/reference check (`#internal-links`) fails on any
740
+ `isProduction`; the kill switch's current state (active vs. paused). The
741
+ link/reference check (`#internal-links`) fails on any
729
742
  `adsbygoogle` / ad-provider reference in a non-production build and on a missing
730
743
  `ads.txt` in the production output. Manually, in a fresh browser on production:
731
744
  decline consent → no ad requests in the network panel; accept → ads load; check
@@ -1381,6 +1394,38 @@ brief cannot honestly fit an offered stack, that is a `feedback/` note (below)
1381
1394
  filed *before* the build — resolved as a new binding, or the nearest stack with
1382
1395
  the compromise recorded.
1383
1396
 
1397
+ **Mechanized new-project start.** `/whs:new` — `templates/commands/whs/new.md`,
1398
+ available the same way as `/whs:adopt` via the `~/.claude/commands/whs/`
1399
+ symlink (`README.md`'s `## Home` section) — mechanizes this "Starting point"
1400
+ paragraph plus the six-question flow above,
1401
+ for a project that doesn't exist yet. Before asking the user to commit to a
1402
+ framework, it reports each stack's actual readiness live — derived from
1403
+ `index.json`'s status plus a real filesystem check (does that stack's
1404
+ `bin/init` exist? do its stage agents exist?), not a static description that
1405
+ can drift out of date — naming explicitly which stacks carry a full pipeline
1406
+ (scaffold plus a guided build) versus a draft scaffold with known,
1407
+ already-tracked gaps. It then applies this paragraph's own test: if the
1408
+ described idea doesn't honestly fit any offered stack, that's the same
1409
+ `feedback/` path above, filed before scaffolding rather than worked around —
1410
+ knowingly choosing a stack still marked `draft` is *not* that case; it's an
1411
+ informed choice against an already-tracked, deliberately-deferred limitation,
1412
+ not a new gap. Once a stack is chosen, `/whs:new` hands off to `/whs:build`
1413
+ where a guided build actually exists (Eleventy today); for a draft-stack
1414
+ scaffold it reports the partial result honestly and points at that stack's
1415
+ own `TEMPLATE.md`.
1416
+
1417
+ **One-time setup.** Adopting, converting, or upgrading against the standard
1418
+ often leaves a project owing **one-time human setup** no script can finish for
1419
+ it — hosting/repo config, DNS, minting a secret (`#secrets`). That debt is
1420
+ tracked the same way `#compliance` already tracks a `MANUAL` row: a committed
1421
+ item carrying a dated `verifiedOn` confirmation, not a message printed once
1422
+ to a terminal and forgotten. A session-start check flags any item still
1423
+ unconfirmed so it can't go silently stale — **not a gate**; per
1424
+ `#compliance`'s own reasoning for staying out of `#the-gate`, visibility beats
1425
+ blocking so an unrelated pending item never freezes incremental adoption. The
1426
+ concrete shape — which items, how they're seeded, how the check runs — lives
1427
+ in each impl doc's own new-project checklist.
1428
+
1384
1429
  **Record** the result near the top of the project's `.claude/CLAUDE.md` /
1385
1430
  `AGENTS.md` as a `## House style` section — a directive paragraph plus a data
1386
1431
  block. The recommended text:
@@ -1422,7 +1467,7 @@ governs it and follow that chapter — the slugs are the index (a `<script>` →
1422
1467
  - production-url: https://example.com
1423
1468
  - content-type: tool # tool | article (article => feeds)
1424
1469
  - publishing-rate: ~5 pages/week
1425
- - standard-version: 2026-09-10 # recommended — enables standard-version drift tracking (#compliance)
1470
+ - standard-version: 2026-09-22 # recommended — enables standard-version drift tracking (#compliance)
1426
1471
  ```
1427
1472
 
1428
1473
  If the section is absent, the assistant's first action is to run the flow above
@@ -1451,6 +1496,25 @@ worked incrementally (the impl doc's migration path is the ordered version of
1451
1496
  that backlog). The project is not expected to be all-`PASS` on day one; it is
1452
1497
  expected to know exactly where it isn't.
1453
1498
 
1499
+ **Mechanized onboarding.** `/whs:adopt` — copied from
1500
+ `templates/commands/whs/adopt.md` into a project's own
1501
+ `.claude/commands/whs/adopt.md` — runs this entire section end-to-end inside a
1502
+ preexisting, never-adopted project: it detects the stack, runs the six-question
1503
+ flow above, then, before writing anything, judges whether the project is
1504
+ actually compatible with the standard. Two failure modes are distinct there:
1505
+ the project may simply be out of the standard's domain (not a defect — nothing
1506
+ gets filed or written), or the standard may have a genuine gap for a real need
1507
+ the project has, which routes to the same `feedback/` mechanism below
1508
+ (**Feedback to the standard**) before or alongside finalizing adoption, exactly
1509
+ as this section's own "Starting point" paragraph already does for a new-project
1510
+ brief that doesn't honestly fit an offered stack. Once adoption proceeds, it
1511
+ writes the `## House style` block above, then, where stack tooling exists to
1512
+ wire in, runs an initial compliance pass. It **produces** the initial
1513
+ migration backlog; it does not work it — working the backlog stays
1514
+ incremental, by hand or later via `/whs:upgrade` once the project is
1515
+ version-pinned and catching up to a newer standard release. `/whs:adopt` and
1516
+ `/whs:upgrade` are deliberately different commands with different scopes.
1517
+
1454
1518
  **Feedback to the standard.** Adopting the house style is what surfaces its
1455
1519
  bugs — a check that misfires, a `core.md` ↔ impl-doc contradiction, a Contract
1456
1520
  that doesn't survive a real codebase, a need it never anticipated. That signal