@thebassclef/lite 1.9.16 → 1.9.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +1 -1
  2. package/dist/index.cjs +1 -1
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.js +1 -1
  5. package/dist/lite/.claude/agents/builder.md +7 -4
  6. package/dist/lite/.claude/bassclef-orientation.md +22 -0
  7. package/dist/lite/.claude/hooks/artifact-ingestion-gate.sh +11 -3
  8. package/dist/lite/.claude/hooks/assert-verify-steering.sh +1 -19
  9. package/dist/lite/.claude/hooks/plain-english-steering.sh +86 -41
  10. package/dist/lite/.claude/hooks/pre-commit-gate.sh +10 -2
  11. package/dist/lite/.claude/hooks/pre-push-shellcheck.sh +196 -0
  12. package/dist/lite/.claude/hooks/reviewer-dispatch-check.sh +153 -0
  13. package/dist/lite/.claude/hooks/session-reflection.d/83-install-written-paths-rebuild.sh +15 -32
  14. package/dist/lite/.claude/hooks/turn-prose-density-measure.sh +109 -0
  15. package/dist/lite/.claude/hooks/turn-prose-kiss-check.sh +28 -0
  16. package/dist/lite/.claude/rules/branching.md +2 -0
  17. package/dist/lite/.claude/rules/identifier-leak-prevention.md +2 -1
  18. package/dist/lite/.claude/rules/install-written-paths-mediation.md +7 -0
  19. package/dist/lite/.claude/rules/lite-manifest-schema-change-discipline.md +23 -0
  20. package/dist/lite/.claude/rules/reviewer-dispatch.md +95 -0
  21. package/dist/lite/.claude/settings.json +10 -0
  22. package/dist/lite/.claude/skills/build/SKILL.md +81 -6
  23. package/dist/lite/.claude/skills/eisenhower/SKILL.md +26 -0
  24. package/dist/lite/.claude/skills/launch/SKILL.md +45 -2
  25. package/dist/lite/.claude/skills/onboard-repo/SKILL.md +208 -40
  26. package/dist/lite/.claude/skills/onboard-repo/claude-oauth-probe.sh +60 -0
  27. package/dist/lite/.claude/skills/onboard-repo/concise-prompt.sh +73 -0
  28. package/dist/lite/.claude/skills/onboard-repo/write-state-marker.sh +81 -0
  29. package/dist/lite/.claude/skills/pre-mortem/SKILL.md +56 -0
  30. package/dist/lite/.claude/skills/sprint/SKILL.md +24 -3
  31. package/dist/lite/.claude/skills/temperance/SKILL.md +15 -4
  32. package/dist/lite/README.md +2 -0
  33. package/dist/lite/bassclef-version.json +5 -5
  34. package/dist/lite/lib/install-written-paths.sh +250 -3
  35. package/dist/lite/lib/prose-marker.sh +203 -0
  36. package/dist/lite/lib/tier-dependency-audit.sh +52 -25
  37. package/dist/lite/presence/install/bassclef-sync.template.sh +86 -8
  38. package/dist/lite/presence/install/schedule-auto-save.cron.sh +30 -2
  39. package/dist/lite/scripts/generate-lite-manifest.sh +15 -0
  40. package/dist/lite/scripts/launch/local-serve.sh +149 -8
  41. package/dist/lite/standards/bassclef-wiring-manifest.json +12 -0
  42. package/dist/lite/standards/lite-manifest-schema-changes.md +84 -0
  43. package/dist/lite/standards/lite-manifest.json +100 -29
  44. package/dist/lite/standards/prose-marker-format.md +105 -0
  45. package/package.json +1 -1
@@ -0,0 +1,73 @@
1
+ #!/bin/bash
2
+ # tier: lite
3
+ # install-class: project
4
+ # concise-prompt.sh — handle Y/n/skip for Anthropic Concise output style.
5
+ # Called by /onboard-repo SKILL Phase 2.7. Writes operator ~/.claude/settings.json
6
+ # atomically via jq read-modify-write so every pre-existing field survives.
7
+ #
8
+ # Per Longrun 2 Step 2g (bassclef-upstream#2197).
9
+ # ADR-049 opt-in default off — adopter picks Y explicitly.
10
+ #
11
+ # Env: HOME_OVERRIDE points at a fixture home for testing.
12
+ # Returns:
13
+ # 0 — path applied (Y wrote field, n wrote declined sentinel, skip wrote skip sentinel)
14
+ # 1 — jq missing OR write failed (fail-soft; operator sees warning)
15
+ # 2 — invalid choice
16
+
17
+ set +e
18
+
19
+ concise_prompt_apply() {
20
+ local choice="${1:-}"
21
+ local home="${HOME_OVERRIDE:-$HOME}"
22
+ local settings="$home/.claude/settings.json"
23
+ local state_dir="$home/.claude/state"
24
+ mkdir -p "$home/.claude" "$state_dir" 2>/dev/null
25
+
26
+ case "$choice" in
27
+ Y|y)
28
+ if ! command -v jq >/dev/null 2>&1; then
29
+ echo "concise-prompt: jq not installed; skipping outputStyle write" >&2
30
+ return 1
31
+ fi
32
+ [ -f "$settings" ] || echo '{}' > "$settings"
33
+ local tmp
34
+ tmp=$(mktemp 2>/dev/null) || return 1
35
+ if jq '. + {"outputStyle": "Concise"}' "$settings" > "$tmp" 2>/dev/null; then
36
+ mv "$tmp" "$settings"
37
+ else
38
+ rm -f "$tmp" 2>/dev/null
39
+ return 1
40
+ fi
41
+ rm -f "$state_dir/concise-prompt-skipped" "$state_dir/concise-prompt-declined" 2>/dev/null
42
+ return 0
43
+ ;;
44
+ n|N)
45
+ touch "$state_dir/concise-prompt-declined" 2>/dev/null || return 1
46
+ return 0
47
+ ;;
48
+ skip|SKIP|s|S)
49
+ touch "$state_dir/concise-prompt-skipped" 2>/dev/null || return 1
50
+ return 0
51
+ ;;
52
+ *)
53
+ return 2
54
+ ;;
55
+ esac
56
+ }
57
+
58
+ concise_prompt_should_fire() {
59
+ local home="${HOME_OVERRIDE:-$HOME}"
60
+ local state_dir="$home/.claude/state"
61
+ local settings="$home/.claude/settings.json"
62
+ [ -f "$state_dir/concise-prompt-declined" ] && return 1
63
+ if [ -f "$settings" ] && command -v jq >/dev/null 2>&1; then
64
+ if jq -e '.outputStyle == "Concise"' "$settings" >/dev/null 2>&1; then
65
+ return 1
66
+ fi
67
+ fi
68
+ return 0
69
+ }
70
+
71
+ if [ "${BASH_SOURCE[0]}" = "${0}" ]; then
72
+ concise_prompt_apply "${1:-}"
73
+ fi
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env bash
2
+ # tier: lite
3
+ # write-state-marker.sh — Phase 2.3.9 helper for /onboard-repo (bassclef-upstream#664).
4
+ # Retagged standard→lite per bassclef-upstream#2140 (Cure 1) so lite adopters
5
+ # running /onboard-repo at Phase 2.3.9 capture the state marker cleanly.
6
+ # Script shape is small (≤80 lines, no bash gotchas); belongs in the lite bundle.
7
+ #
8
+ # Writes .claude/state/onboard-repo.state.json capturing which mode ran, when,
9
+ # and which files landed. Re-runs append to history[] instead of clobbering.
10
+ #
11
+ # Usage:
12
+ # bash .claude/skills/onboard-repo/write-state-marker.sh <mode>
13
+ #
14
+ # Env:
15
+ # ONBOARD_TARGET_DIR root of the adopter repo (defaults to $PWD)
16
+ #
17
+ # Composes with:
18
+ # - Configs UI first-run overlay (bassclef/configs/, bet 2026-07-08a WU-7)
19
+ # - @luminary john-ousterhout — deep module — one script, one job
20
+
21
+ set -eu
22
+
23
+ MODE="${1:-default}"
24
+ TARGET="${ONBOARD_TARGET_DIR:-$PWD}"
25
+
26
+ mkdir -p "$TARGET/.claude/state"
27
+ MARKER="$TARGET/.claude/state/onboard-repo.state.json"
28
+ NOW=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
29
+
30
+ # Collect paths written to the adopter repo — check for existence.
31
+ PATHS_JSON='['
32
+ sep=""
33
+ for p in \
34
+ ".claude/bassclef-configs.jsonc" \
35
+ ".bassclef-source.json" \
36
+ ".claude/CLAUDE.md" \
37
+ "docs/whereami.md"; do
38
+ if [ -f "$TARGET/$p" ]; then
39
+ PATHS_JSON="$PATHS_JSON$sep\"$p\""
40
+ sep=","
41
+ fi
42
+ done
43
+ PATHS_JSON="$PATHS_JSON]"
44
+
45
+ if [ -f "$MARKER" ]; then
46
+ # Existing marker — append prior run to history[]; keep top-level as this run.
47
+ python3 - "$MARKER" "$MODE" "$NOW" "$PATHS_JSON" <<'PY'
48
+ import json, sys
49
+ marker_path = sys.argv[1]
50
+ mode = sys.argv[2]
51
+ now = sys.argv[3]
52
+ paths_written = json.loads(sys.argv[4])
53
+ with open(marker_path) as f:
54
+ data = json.load(f)
55
+ history = data.get("history", [])
56
+ history.append({
57
+ "mode": data.get("mode", "unknown"),
58
+ "timestamp_iso": data.get("timestamp_iso", "unknown"),
59
+ "paths_written": data.get("paths_written", []),
60
+ })
61
+ data["mode"] = mode
62
+ data["timestamp_iso"] = now
63
+ data["paths_written"] = paths_written
64
+ data["history"] = history
65
+ data["skill_version"] = data.get("skill_version", "1.0")
66
+ with open(marker_path, "w") as f:
67
+ json.dump(data, f, indent=2)
68
+ PY
69
+ else
70
+ cat > "$MARKER" <<JSON
71
+ {
72
+ "mode": "$MODE",
73
+ "timestamp_iso": "$NOW",
74
+ "paths_written": $PATHS_JSON,
75
+ "history": [],
76
+ "skill_version": "1.0"
77
+ }
78
+ JSON
79
+ fi
80
+
81
+ echo "wrote onboard-repo state marker at $MARKER (mode: $MODE)"
@@ -111,13 +111,69 @@ For non-always-fire scope, `/pre-mortem light` picks 3 lenses appropriate to the
111
111
  - Per lens: 5-8 imagined failures written by operator; Claude collates without ranking during the pass
112
112
  - Follow-up pass: pick top 5 risks total (across all lenses); name owner + mitigation per risk
113
113
  - Output at `docs/risk-ledgers/<slug>.md` — short-form ledger
114
+ - **Write marker at `state/markers/pre-mortem/<branch-slug>.marker`** — downstream skills (`/build` Phase 3, Reviewer) read it. See § Marker contract below.
114
115
 
115
116
  ### Full mode (90 min)
116
117
 
118
+ <!-- FKGL-EXEMPT: full-mode bullets inherit light-mode shape with denser per-risk fields (severity + trigger + mitigation); dense bullet reference list, not prose. -->
117
119
  - 5-8 rotating lenses per domain map
118
120
  - Per lens: 10-15 imagined failures; per-risk severity (high/medium/low); per-risk trigger signal (what would tell operator the risk materialized); per-risk mitigation
119
121
  - Follow-up pass: rank all risks by severity × probability; name owner + mitigation trigger + monitoring signal per top-10 risks
120
122
  - Output at `docs/risk-ledgers/<slug>.md` — research-grade ledger
123
+ - **Write marker at `state/markers/pre-mortem/<branch-slug>.marker`** — downstream skills read it. See § Marker contract below.
124
+ <!-- /FKGL-EXEMPT -->
125
+
126
+ <!-- FKGL-EXEMPT: § Marker contract — technical reference with bash fenced blocks and marker body template; not prose. Per bassclef-upstream#2175 Batch B — F4. -->
127
+
128
+ ## Marker contract (per bassclef-upstream#2175 Batch B — F4)
129
+
130
+ After the risk ledger lands, write a marker at `state/markers/pre-mortem/<branch-slug>.marker`. Downstream skills read it. `/build` Phase 3 surfaces the top risks. Reviewer reads it during review.
131
+
132
+ Marker body shape:
133
+
134
+ ```
135
+ # Pre-mortem marker — <scope>
136
+ - date: YYYY-MM-DD
137
+ - branch: <branch name>
138
+ - ledger: docs/risk-ledgers/<slug>.md
139
+ - mode: light | full
140
+ - lenses: lead-slug + supporting slugs (comma list)
141
+ - top-5 risks: one line per risk — <severity glyph> <one-sentence summary> (<lens slug>)
142
+ - disposition: fold | monitor | scope-cut per risk
143
+ ```
144
+
145
+ The ledger path is the canonical reference. The top-5 risks give a scan summary. Lenses pin the authoring luminaries.
146
+
147
+ ```bash
148
+ BRANCH=$(git branch --show-current)
149
+ BRANCH_SLUG=$(echo "$BRANCH" | tr '/' '-')
150
+ mkdir -p state/markers/pre-mortem
151
+ cat > "state/markers/pre-mortem/${BRANCH_SLUG}.marker" <<EOF
152
+ # Pre-mortem marker — <scope>
153
+ - date: $(date -u +%Y-%m-%d)
154
+ - branch: $BRANCH
155
+ - ledger: docs/risk-ledgers/<slug>.md
156
+ - mode: light
157
+ - lenses: <lead-slug>, <supporting-slug-1>, <supporting-slug-2>
158
+ - top-5 risks:
159
+ - 🔴 <risk one summary> (<lens-slug>)
160
+ - 🟡 <risk two summary> (<lens-slug>)
161
+ - 🟢 <risk three summary> (<lens-slug>)
162
+ - 🟡 <risk four summary> (<lens-slug>)
163
+ - 🔴 <risk five summary> (<lens-slug>)
164
+ - disposition: fold per risk (name mitigation inline)
165
+ EOF
166
+ ```
167
+
168
+ Guard the git-stage with a check-ignore test — adopter repos vary on whether `state/markers/` is tracked:
169
+
170
+ ```bash
171
+ if ! git check-ignore -q "state/markers/pre-mortem/${BRANCH_SLUG}.marker" 2>/dev/null; then
172
+ git add "state/markers/pre-mortem/${BRANCH_SLUG}.marker"
173
+ fi
174
+ ```
175
+
176
+ <!-- /FKGL-EXEMPT -->
121
177
 
122
178
  ## Output
123
179
 
@@ -100,6 +100,21 @@ Option labels are typeable letters per memory `feedback_keyboard_typeable_option
100
100
 
101
101
  **Step 0** — Lead with a `/value-prop tweet` claim (≤180 chars) plus a `/kiss words` grade-10 summary (2-3 sentences). This is what the operator reads first on mobile. Tables come after.
102
102
 
103
+ **Step 0.5 — First-run detection (per bassclef-upstream#2009; Sam-A).** Before any thread walk fires, check whether the repo is cold. Cold means:
104
+
105
+ - `docs/whereami.md` is missing OR has no `iteration_bet:` + no `next_bet:` field, AND
106
+ - `docs/canvases/` is empty or missing, AND
107
+ - `docs/roadmaps/` is empty or missing, AND
108
+ - `state/markers/temperance/` is empty or missing
109
+
110
+ When cold, jump straight to Step 7d. Skip Steps 1-6. The cold-repo output uses plain adopter vocabulary — no "parent-goal thread", no "orientation gates", no "roadmap reconcile", no "tier taxonomies". A cold adopter has no model for those words yet.
111
+
112
+ Routing hint for cold adopters points at the build path:
113
+
114
+ > New here? Try `/stage` for a clickable mock of your idea, `/launch` for a full variant gallery, or `/build` to ship against an existing spec. These let you see something real on your screen before learning the deeper surface area.
115
+
116
+ When warm (any of the four signals present), continue to Step 1.
117
+
103
118
  **Step 1** — Orientation gates. Check `state/markers/orientation-gate/<branch-slug>.marker`. Check `state/markers/roadmap-reconcile-gate/<branch-slug>.marker`. When either is missing on a feature branch, refuse Step 7. Point at the BLOCKED block. On main this step is silent.
104
119
 
105
120
  **Step 2** — Walk the parent chain before reading leaf state. Read the active goal's parent_drivers via `state_iteration_bet_active`. Walk up to parent_bet when present. Walk further until parent_goal is null. Read parent_roadmap when any goal in the chain has one. Fetch its `last_event` and `next_pending` via `state_roadmap_get`. Surface active goal plus parent goal plus parent roadmap plus combined parent_drivers.
@@ -177,11 +192,17 @@ Instead of refusing to render, `/sprint` reads three fallback sources:
177
192
 
178
193
  From these sources, propose 3 candidate options in the same card + scan-table shape (Step 7c). Cite the source per option — for example, "From your open issues #NNN + recent commits on `<area>`". The value-prop per option comes from what the source itself says, not from a strategic doc.
179
194
 
180
- End with a plain-English hint pointing the adopter at ways to sharpen future runs:
195
+ End with two plain-English hints. The first points the adopter at the build path — the fastest route from idea to something on screen:
196
+
197
+ > New here? Try `/stage` for a clickable mock, `/launch` for a full variant gallery, or `/build` to ship against an existing spec. These let you see something real on your screen before learning the deeper surface area.
198
+
199
+ The second hint points at ways to sharpen future runs:
200
+
201
+ > Want sharper option proposals from `/sprint` later? Author strategic docs first — `/lean-canvas` (business model), `/spec` (buildable spec), or `/user-stories` (backlog shape). These feed `/sprint` on future calls.
181
202
 
182
- > Want sharper option proposals next time? Author strategic docs first — `/lean-canvas` (business model), `/spec` (buildable spec), or `/user-stories` (INVEST backlog). These feed `/sprint` on future calls.
203
+ Both hints are opt-in. The adopter does NOT need to author strategic docs to get value from this call. This mirrors `/onboard-repo` Path B "greenfield-from-intent" — the skill produces useful output on its first fire, and hints at deeper flows without gating on them.
183
204
 
184
- The fallback is opt-in on the hint side. The adopter does NOT need to author strategic docs to get value from this call. This mirrors `/onboard-repo` Path B "greenfield-from-intent" — the skill produces useful output on its first fire, and hints at deeper flows without gating on them.
205
+ Note per Sam PDF cure (bassclef-upstream#2009): the first hint takes precedence over the second. Sam's bounce rule is that unfamiliar vocabulary at first mention causes exit. The build path uses adopter-vocab (`/stage`, `/launch`, `/build`); the strategic-docs path uses deeper vocab (`/lean-canvas`, `/user-stories`). First hint fires first.
185
206
 
186
207
  Note per risk ledger R2: production fixture harness for this fallback path uses a brownfield fixture (no canvas, no whereami) so the fallback runs under test. Adding that fixture ships as a follow-on when the fallback surface gets its first Tier 0 test.
187
208
 
@@ -82,7 +82,9 @@ State these five lines after the checklist:
82
82
  - **What you are NOT doing** — the temptations you resist.
83
83
  - **Verification plan** — how you will know it works.
84
84
 
85
- Then write the gate marker. BOTH the ephemeral flag AND the durable git-tracked marker. The pre-build-gate hook accepts either. The durable marker lets the 5-session compliance counter see mobile firings that would evaporate at sandbox teardown.
85
+ Then write the gate marker. BOTH the ephemeral flag AND the durable marker. The pre-build-gate hook accepts either. The durable marker lets the 5-session compliance counter see mobile firings that would evaporate at sandbox teardown.
86
+
87
+ <!-- FKGL-EXEMPT: bash command block; shell commands are not graded prose. F6 cure per bassclef-upstream#2175 Batch B wraps git add in check-ignore guard. -->
86
88
 
87
89
  ```bash
88
90
  BRANCH=$(git branch --show-current)
@@ -93,13 +95,22 @@ SESSION_ID=${CLAUDE_SESSION_ID:-$$}
93
95
  # Ephemeral flag (existing pre-build-gate contract)
94
96
  touch "/tmp/claude-temperance-done-${SESSION_ID}-${BRANCH_HASH}"
95
97
 
96
- # Durable git-tracked marker
98
+ # Durable marker (gitignore-aware per F6 cure)
97
99
  mkdir -p state/markers/temperance
98
100
  date -u +%Y-%m-%dT%H:%M:%SZ > "state/markers/temperance/${BRANCH_SLUG}.marker"
99
- git add "state/markers/temperance/${BRANCH_SLUG}.marker"
101
+
102
+ # F6 cure (bassclef-upstream#2175 Batch B) — skip git add when the path is
103
+ # ignored. Adopter repos vary on whether state/markers/ is tracked. The marker
104
+ # write stays unconditional (local file always). The stage runs only when the
105
+ # path is tracked.
106
+ if ! git check-ignore -q "state/markers/temperance/${BRANCH_SLUG}.marker" 2>/dev/null; then
107
+ git add "state/markers/temperance/${BRANCH_SLUG}.marker"
108
+ fi
100
109
  ```
101
110
 
102
- The durable marker commits with the branch's normal flow. `state/markers/` is git-tracked per `.claude/rules/mobile-ephemeral-session.md` section 2.
111
+ <!-- /FKGL-EXEMPT -->
112
+
113
+ The durable marker always lands as a local file. The git stage runs only when the adopter repo tracks `state/markers/`. Per `.claude/rules/mobile-ephemeral-session.md` section 2 and `.claude/rules/session-artifacts.md` posture note.
103
114
 
104
115
  Then proceed with the work.
105
116
 
@@ -13,6 +13,8 @@ description: A shared substrate for working with AI coding agents.
13
13
 
14
14
  **A shared substrate for working with AI coding agents.** Install it once into your repo, and every Claude Code session starts loaded: skills you invoke, rules the agent reads, specialist agents you dispatch, practitioner profiles it designs against, hooks that catch mistakes, plus the standards and decision records that wire them together.
15
15
 
16
+ **What lands on disk.** Bassclef installs its working files under `.claude/`, plus one project-state document at `docs/whereami.md`, a config at `substrate.config.md`, and a `CLAUDE.md` that loads them. Most of these are bassclef's own files — you normally don't edit them. Your own code is untouched.
17
+
16
18
  Run `/skills` or `/luminary` in any session to see exactly what your install carries. The counts differ by tier, so this README does not print them.
17
19
 
18
20
  The agent reads your rules, knows the catalog, handles failure cleanly, and ships with discipline. You stop re-explaining your conventions every session — the difference between an assistant you brief and a band that already knows the set.
@@ -1,7 +1,7 @@
1
1
  {
2
- "version": "v1.8.0",
3
- "marker_tag": "release-2026-10-06-a874c748",
4
- "release_date": "2026-10-06T20:56:57Z",
5
- "release_sha": "a874c748",
6
- "release_tag": "v1.8.0"
2
+ "version": "v1.11.1",
3
+ "marker_tag": "release-2026-10-18-3b0203d5",
4
+ "release_date": "2026-10-18T14:59:01Z",
5
+ "release_sha": "3b0203d5",
6
+ "release_tag": "v1.11.1"
7
7
  }
@@ -112,6 +112,100 @@ _iwp_manifest_valid() {
112
112
  return 0
113
113
  }
114
114
 
115
+ # ---- Ajv-runtime schema validator (bassclef-upstream#2157 step 6) ----
116
+ #
117
+ # Wraps the install-written-paths manifest in ajv-based schema validation.
118
+ # Called from install_written_paths_verify (read-side gate) + from
119
+ # install_written_paths_register (post-write gate).
120
+ #
121
+ # Reads via is_install_written_path stay cheap (no ajv — hot path).
122
+ # Writes are rare, so post-write ajv cost is acceptable.
123
+ #
124
+ # Fail-soft on ajv missing (adopter machine without ajv-cli): returns 0.
125
+ # Mirrors the step 2 validator pattern at scripts/validate-cross-repo-contracts.sh:139-144.
126
+ # This keeps lite-tier adopters working when ajv is not installed; the step 3
127
+ # CI gate catches drift at every PR regardless of adopter ajv presence.
128
+ #
129
+ # @pattern patterns/code/ousterhout/deep-module.md — one wrapper, many callers
130
+ # @pattern patterns/code/vernon/anticorruption-layer.md — ajv at the write boundary
131
+
132
+ _iwp_schema_path() {
133
+ local root="${CLAUDE_PROJECT_DIR:-$(pwd)}"
134
+ echo "$root/standards/state-spine/schemas/install-written-paths.schema.json"
135
+ }
136
+
137
+ # _iwp_ajv_validate <manifest-path>
138
+ # Returns 0 when ajv absent (fail-soft) OR when ajv confirms the manifest
139
+ # conforms to standards/state-spine/schemas/install-written-paths.schema.json.
140
+ # Returns 1 when ajv is present AND the manifest fails schema validation.
141
+ _iwp_ajv_validate() {
142
+ local manifest_file="${1:-}"
143
+ [ -n "$manifest_file" ] || return 1
144
+ [ -f "$manifest_file" ] || return 1
145
+
146
+ if ! command -v ajv >/dev/null 2>&1; then
147
+ _iwp_log "ajv-validate skip reason=ajv-missing manifest=$manifest_file"
148
+ return 0
149
+ fi
150
+
151
+ local schema
152
+ schema=$(_iwp_schema_path)
153
+ if [ ! -f "$schema" ]; then
154
+ _iwp_log "ajv-validate skip reason=schema-missing path=$schema"
155
+ return 0
156
+ fi
157
+
158
+ if ajv validate -s "$schema" -d "$manifest_file" --spec=draft2020 -c ajv-formats >/dev/null 2>&1; then
159
+ _iwp_log "ajv-validate pass manifest=$manifest_file"
160
+ return 0
161
+ fi
162
+
163
+ _iwp_log "ajv-validate fail manifest=$manifest_file"
164
+ return 1
165
+ }
166
+
167
+ # ---- CLI init.manifest.json fallback (bassclef-upstream#2138) ----
168
+ #
169
+ # bassclef-cli's `bassclef init` writes `.bassclef/init.manifest.json`
170
+ # with its own schema (per bassclef-cli/src/lib/manifest-types.ts):
171
+ # { schema_version: 2|3, entries: [{path, template, outcome, ...}] }
172
+ #
173
+ # When upstream's state/install-written-paths.json doesn't have an
174
+ # entry for a path, fall back to this CLI manifest. Treat outcome in
175
+ # {created, updated, unchanged} as install-written. Treat refused |
176
+ # error as NOT install-written (the write didn't actually land, so
177
+ # discipline should still fire).
178
+ #
179
+ # No hash verification against the CLI manifest. The CLI schema uses
180
+ # `content_hash_sha256` without the `sha256:` prefix; shape-mismatch
181
+ # is tolerated because the fallback is a presence + outcome check,
182
+ # not an integrity check. The upstream manifest keeps strict hash
183
+ # verification per Hoare.
184
+ _iwp_init_manifest_path() {
185
+ local root="${CLAUDE_PROJECT_DIR:-$(pwd)}"
186
+ echo "$root/.bassclef/init.manifest.json"
187
+ }
188
+
189
+ # Returns 0 when the path is present in the CLI init manifest with
190
+ # outcome in {created, updated, unchanged}. Returns 1 otherwise
191
+ # (missing file, malformed JSON, path absent, or outcome refused/error).
192
+ _iwp_init_manifest_has_path() {
193
+ local tgt="${1:-}"
194
+ [ -n "$tgt" ] || return 1
195
+ local init_manifest
196
+ init_manifest=$(_iwp_init_manifest_path)
197
+ [ -f "$init_manifest" ] || return 1
198
+ command -v jq >/dev/null 2>&1 || return 1
199
+ # Guard against malformed JSON: jq -e . exits non-zero on parse failure.
200
+ jq -e . "$init_manifest" >/dev/null 2>&1 || return 1
201
+ local outcome
202
+ outcome=$(jq -r --arg path "$tgt" '.entries[]? | select(.path == $path) | .outcome' "$init_manifest" 2>/dev/null)
203
+ case "$outcome" in
204
+ created|updated|unchanged) return 0 ;;
205
+ *) return 1 ;;
206
+ esac
207
+ }
208
+
115
209
  # ---- Public API ----
116
210
 
117
211
  # install_written_paths_register <path> <writer>
@@ -182,7 +276,19 @@ install_written_paths_register() {
182
276
  fi
183
277
 
184
278
  rm -f "$tmp" 2>/dev/null || true
185
- _iwp_log "register path=$tgt writer=$writer"
279
+
280
+ # Post-write ajv check (step 6 — bassclef-upstream#2157). On fail, surface
281
+ # a warning but keep the write — fail-soft so adopters never lose data
282
+ # over schema drift. The step 2 validator + step 3 CI gate + step 5 release
283
+ # gate catch drift at later layers; this layer is advisory at write-time.
284
+ if ! _iwp_ajv_validate "$mpath"; then
285
+ echo "install-written-paths: WARNING — manifest at $mpath fails ajv schema validation after register" >&2
286
+ echo "install-written-paths: entry kept; run 'install_written_paths_verify' for details" >&2
287
+ _iwp_log "register path=$tgt writer=$writer post-write-ajv=fail"
288
+ else
289
+ _iwp_log "register path=$tgt writer=$writer post-write-ajv=pass"
290
+ fi
291
+
186
292
  return 0
187
293
  }
188
294
 
@@ -204,10 +310,17 @@ is_install_written_path() {
204
310
  local mpath
205
311
  mpath=$(_iwp_manifest_path)
206
312
 
207
- # Fail-soft: manifest missing or malformed → not install-written
313
+ # Fail-soft: upstream manifest missing or malformed. Before giving
314
+ # up, consult the CLI init manifest (bassclef-upstream#2138). Cold
315
+ # adopters that ran only `bassclef init` have no upstream manifest
316
+ # yet; the CLI manifest is their only install-written source.
208
317
  if ! _iwp_manifest_valid; then
209
- _iwp_log "check path=$tgt result=false reason=no-manifest-or-malformed"
210
318
  [ -f "$mpath" ] && echo "install-written-paths: manifest malformed; treating as not-install-written" >&2
319
+ if _iwp_init_manifest_has_path "$tgt"; then
320
+ _iwp_log "check path=$tgt result=true reason=cli-init-manifest-fallback-no-upstream-manifest"
321
+ return 0
322
+ fi
323
+ _iwp_log "check path=$tgt result=false reason=no-manifest-or-malformed"
211
324
  return 1
212
325
  fi
213
326
 
@@ -220,6 +333,14 @@ is_install_written_path() {
220
333
  local recorded_hash
221
334
  recorded_hash=$(jq -r --arg path "$tgt" '.entries[] | select(.path == $path) | .content_hash' "$mpath" 2>/dev/null)
222
335
  if [ -z "$recorded_hash" ] || [ "$recorded_hash" = "null" ]; then
336
+ # Fallback per bassclef-upstream#2138 — the CLI's `bassclef init`
337
+ # writes `.bassclef/init.manifest.json` with its own shape. Lite
338
+ # adopters hit the lead-lens 3-marker gate because the two manifests
339
+ # never agreed. Consult the CLI manifest before giving up.
340
+ if _iwp_init_manifest_has_path "$tgt"; then
341
+ _iwp_log "check path=$tgt result=true reason=cli-init-manifest-fallback"
342
+ return 0
343
+ fi
223
344
  _iwp_log "check path=$tgt result=false reason=not-in-manifest"
224
345
  return 1
225
346
  fi
@@ -335,6 +456,125 @@ install_written_paths_rebuild() {
335
456
  return 0
336
457
  }
337
458
 
459
+ # install_written_paths_rebuild_from_lite_manifest <lite-manifest-path>
460
+ #
461
+ # Bulk-rebuild cure per bassclef-upstream#2200.
462
+ #
463
+ # Replaces the fragment's 451 serial `install_written_paths_register` loop
464
+ # with one atomic rebuild. Reads lite-manifest.json, computes SHA256 per
465
+ # path that exists on disk, writes the full entries array in one jq pass
466
+ # to a tmp file, runs ajv once, then atomically moves tmp over manifest.
467
+ #
468
+ # Deep module per @luminary john-ousterhout — one verb hides the whole
469
+ # rebuild path. Caller stays dumb. Register's per-path semantics stay
470
+ # untouched for ad-hoc callers (hook-connect, migrations) that need
471
+ # per-call ajv (PR #2174 intent preserved).
472
+ #
473
+ # Postcondition (per @luminary tony-hoare):
474
+ # On return 0: manifest at _iwp_manifest_path() carries one entry per
475
+ # lite-manifest.json path that passes _iwp_validate_path AND exists on
476
+ # disk; writer=bassclef-sync-template; content_hash=sha256:<hex>;
477
+ # written_at=current UTC timestamp. Schema-valid per ajv when ajv present.
478
+ # On return 1: lite-manifest missing OR malformed; existing manifest
479
+ # untouched (atomic mv never ran).
480
+ #
481
+ # Returns:
482
+ # 0 success (manifest written)
483
+ # 1 lite-manifest missing | malformed | jq missing
484
+ install_written_paths_rebuild_from_lite_manifest() {
485
+ local lite_manifest="${1:-}"
486
+
487
+ if [ -z "$lite_manifest" ] || [ ! -f "$lite_manifest" ]; then
488
+ _iwp_log "bulk-rebuild fail reason=lite-manifest-missing path=$lite_manifest"
489
+ return 1
490
+ fi
491
+
492
+ if ! command -v jq >/dev/null 2>&1; then
493
+ echo "install-written-paths: jq not available; cannot bulk-rebuild" >&2
494
+ return 1
495
+ fi
496
+
497
+ # Precondition: lite-manifest.json is valid JSON
498
+ if ! jq -e . "$lite_manifest" >/dev/null 2>&1; then
499
+ _iwp_log "bulk-rebuild fail reason=lite-manifest-malformed path=$lite_manifest"
500
+ return 1
501
+ fi
502
+
503
+ local mpath
504
+ mpath=$(_iwp_manifest_path)
505
+ mkdir -p "$(dirname "$mpath")"
506
+
507
+ local root="${CLAUDE_PROJECT_DIR:-$(pwd)}"
508
+ local now
509
+ now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
510
+
511
+ # Build entries array in a tmp jq-input file: one JSON object per line.
512
+ # Each line is a {path, writer, written_at, content_hash} record.
513
+ # Skip entries that fail path validation OR have no file on disk.
514
+ local jq_input
515
+ jq_input=$(mktemp)
516
+ # shellcheck disable=SC2064
517
+ trap "rm -f \"$jq_input\"" RETURN
518
+
519
+ local registered=0
520
+ local skipped=0
521
+ while IFS= read -r rel_path; do
522
+ [ -z "$rel_path" ] && continue
523
+ if ! _iwp_validate_path "$rel_path"; then
524
+ skipped=$((skipped + 1))
525
+ continue
526
+ fi
527
+ local full_path
528
+ case "$rel_path" in
529
+ /*) full_path="$rel_path" ;;
530
+ *) full_path="$root/$rel_path" ;;
531
+ esac
532
+ if [ ! -f "$full_path" ]; then
533
+ skipped=$((skipped + 1))
534
+ continue
535
+ fi
536
+ local hash
537
+ hash=$(_iwp_sha256 "$full_path")
538
+ if [ -z "$hash" ]; then
539
+ skipped=$((skipped + 1))
540
+ continue
541
+ fi
542
+ jq -cn --arg p "$rel_path" --arg w "bassclef-sync-template" --arg ts "$now" --arg h "$hash" \
543
+ '{path:$p, writer:$w, written_at:$ts, content_hash:$h}' >> "$jq_input"
544
+ registered=$((registered + 1))
545
+ done < <(jq -r '.entries[]?.path // empty' "$lite_manifest")
546
+
547
+ # Build the manifest from the per-line records using jq --slurp.
548
+ # Tmp filename MUST end in .json so ajv's auto-detect reads it as JSON
549
+ # instead of trying to require() it as a module (macOS mktemp without
550
+ # --suffix produces extensionless files that fail ajv's parse).
551
+ local tmp
552
+ tmp="$mpath.rebuild.$$.json"
553
+ if ! jq -s --arg sv "$_IWP_SCHEMA_VERSION" \
554
+ '{schema_version:$sv, entries:.}' "$jq_input" > "$tmp"; then
555
+ rm -f "$tmp" 2>/dev/null || true
556
+ _iwp_log "bulk-rebuild fail reason=jq-slurp-failed registered=$registered"
557
+ return 1
558
+ fi
559
+
560
+ # Post-write ajv validate; on fail, keep old manifest (atomic safety per Nygard)
561
+ if ! _iwp_ajv_validate "$tmp"; then
562
+ echo "install-written-paths: WARNING — bulk-rebuild output fails ajv; keeping prior manifest" >&2
563
+ rm -f "$tmp" 2>/dev/null || true
564
+ _iwp_log "bulk-rebuild fail reason=ajv-validate registered=$registered"
565
+ return 1
566
+ fi
567
+
568
+ if ! mv "$tmp" "$mpath"; then
569
+ rm -f "$tmp" 2>/dev/null || true
570
+ _iwp_log "bulk-rebuild fail reason=mv-failed registered=$registered"
571
+ return 1
572
+ fi
573
+
574
+ _iwp_log "bulk-rebuild ok registered=$registered skipped=$skipped mpath=$mpath"
575
+ return 0
576
+ }
577
+
338
578
  # install_written_paths_verify
339
579
  # Returns 0 on well-formed manifest, 1 on schema violation.
340
580
  install_written_paths_verify() {
@@ -365,5 +605,12 @@ install_written_paths_verify() {
365
605
  return 1
366
606
  fi
367
607
 
608
+ # Ajv-runtime schema check (step 6 — bassclef-upstream#2157).
609
+ # Fail-soft on ajv missing (returns 0); strict reject on schema failure.
610
+ if ! _iwp_ajv_validate "$mpath"; then
611
+ echo "install-written-paths: manifest fails ajv schema validation" >&2
612
+ return 1
613
+ fi
614
+
368
615
  return 0
369
616
  }