@thebassclef/lite 1.9.7 → 1.9.9

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 (35) hide show
  1. package/README.md +6 -2
  2. package/dist/cli.cjs +89 -7
  3. package/dist/cli.js +89 -7
  4. package/dist/index.cjs +1 -1
  5. package/dist/index.d.ts +1 -1
  6. package/dist/index.js +1 -1
  7. package/dist/lite/.claude/hooks/git-stash-u-guard.sh +4 -1
  8. package/dist/lite/.claude/hooks/post-skill-friction-check.sh +6 -1
  9. package/dist/lite/.claude/hooks/pre-commit-gate.sh +18 -6
  10. package/dist/lite/.claude/hooks/pre-commit-identifier-leak-scrub.sh +10 -6
  11. package/dist/lite/.claude/hooks/pre-commit-manifest-autoregen.sh +4 -2
  12. package/dist/lite/.claude/hooks/save-state.sh +24 -0
  13. package/dist/lite/.claude/hooks/trace-helper.sh +29 -0
  14. package/dist/lite/.claude/rules/lite-manifest-schema-change-discipline.md +30 -0
  15. package/dist/lite/.claude/settings.json +1 -1
  16. package/dist/lite/.claude/skills/build/SKILL.md +19 -7
  17. package/dist/lite/.claude/skills/onboard-repo/SKILL.md +41 -0
  18. package/dist/lite/.claude/skills/release-notes/SKILL.md +24 -0
  19. package/dist/lite/bassclef-version.json +5 -5
  20. package/dist/lite/gitignore +8 -0
  21. package/dist/lite/lib/deferred-actions.sh +4 -4
  22. package/dist/lite/lib/fixture-builder.sh +9 -9
  23. package/dist/lite/lib/identifier-leak-check.sh +85 -0
  24. package/dist/lite/lib/preview-state.sh +20 -20
  25. package/dist/lite/lib/skip-env-inline-check.sh +61 -0
  26. package/dist/lite/lib/state.sh +31 -28
  27. package/dist/lite/lib/tier-check.sh +9 -9
  28. package/dist/lite/lib/tier-dependency-audit.sh +23 -23
  29. package/dist/lite/presence/install/schedule-auto-save.taskscheduler.md +1 -1
  30. package/dist/lite/standards/lite-manifest-schema-changes.md +38 -0
  31. package/dist/lite/standards/lite-manifest.json +38 -38
  32. package/dist/lite/templates/spec-template.md +10 -6
  33. package/package.json +1 -1
  34. package/dist/lite/.claude/hooks/longrun-prep-compounding-axis-check.sh +0 -492
  35. package/dist/lite/.claude/rules/compounding-axis-fresh-analysis.md +0 -173
@@ -330,14 +330,25 @@ Parse the validated goal's structure:
330
330
  ```bash
331
331
  # Already have BET_JSON, SPEC_PATH, DECOMP_PATH, UX_MIG_PATH from Phase 0
332
332
  # Read spec body for step enumeration. Per bassclef#367, spec MUST have
333
- # the exact heading `## Steps enumerated` (pinned in /spec SKILL.md
334
- # rules + templates/spec-template.md). /launch's Phase 9
335
- # emits this heading by contract. Specs that lack the section → exit 4.
333
+ # the heading `## Steps enumerated` (pinned in /spec SKILL.md rules +
334
+ # templates/spec-template.md). /launch's Phase 9 emits this heading by
335
+ # contract. Specs that lack the section → exit 4.
336
+ #
337
+ # Finding #2 + #3 cure (#2036): the previous awk range
338
+ # `/^## Steps enumerated/,/^## /` closed on its own start line because
339
+ # `^## ` matches `^## Steps enumerated`. Flag-based extraction fixes it.
340
+ # Grace window per ADR-040 D1 (through 2026-10-31): accept both the new
341
+ # "Steps enumerated" / "### Step-N" and the historical "Workunits
342
+ # enumerated" / "### WU-N" headings so legacy specs still build.
336
343
  SPEC_FILE="docs/specs/${SPEC_PATH}.md"
337
- WU_LIST=$(awk '/^## Steps enumerated/,/^## /' "$SPEC_FILE" | grep -E '^### WU-')
344
+ WU_LIST=$(awk '
345
+ /^## (Steps|Workunits) enumerated/ { in_section=1; next }
346
+ /^## / { in_section=0 }
347
+ in_section
348
+ ' "$SPEC_FILE" | grep -E '^### (Step|WU)-')
338
349
 
339
- # Each WU should reference at least one story (US-NNN)
340
- WU_COUNT=$(echo "$WU_LIST" | grep -c '^### WU-' || echo 0)
350
+ # Each step should reference at least one story (US-NNN)
351
+ WU_COUNT=$(echo "$WU_LIST" | grep -cE '^### (Step|WU)-' || echo 0)
341
352
  ```
342
353
 
343
354
  Build structured object: `{ bet, spec_path, decomp_path, ux_migration_path, wus[] }`.
@@ -348,7 +359,8 @@ Verify each step traces to at least one story. Refuse if any step lacks
348
359
  story traceability.
349
360
 
350
361
  ```bash
351
- ORPHAN_WUS=$(echo "$WU_LIST" | grep -v 'US-' || true)
362
+ # Keep only step headings; drop body lines before the US-NNN check.
363
+ ORPHAN_WUS=$(echo "$WU_LIST" | grep -E '^### (Step|WU)-' | grep -v 'US-' || true)
352
364
  if [ -n "$ORPHAN_WUS" ]; then
353
365
  echo "/build: step enumeration lacks story traceability (bassclef#358 violation)" >&2
354
366
  echo "Orphan steps (no US-NNN ref):" >&2
@@ -462,9 +462,28 @@ immediately without waiting for a sync cycle.
462
462
 
463
463
  ##### 1.2 Protect main from force-push and deletion
464
464
 
465
+ **Heads up — GitHub free-tier adopters on PRIVATE repos:** the API call
466
+ below returns HTTP 403 ("Upgrade to GitHub Pro or make this repository
467
+ public") on free-tier accounts, because branch protection is a paid
468
+ feature for private repos. Public repos get branch protection on every
469
+ plan, free included. Pick a path below BEFORE running the call.
470
+
471
+ Three ways to clear this step:
472
+
473
+ 1. **Make the repo public** — simplest; free plan includes branch
474
+ protection on public repos. Good fit when the code is open source
475
+ or ships to adopters anyway.
476
+ 2. **Upgrade the GitHub plan** — Pro / Team / Enterprise includes
477
+ branch protection on private repos.
478
+ 3. **Skip the API call** — rely on local safety (no `git push --force`
479
+ habits, PR review, pre-commit hooks). Mark the repo config so later
480
+ phases know branch protection is operator-managed, not substrate-
481
+ managed. See the fallback block below.
482
+
465
483
  ```bash
466
484
  DEFAULT_BRANCH=$(gh repo view "$REPO" --json defaultBranchRef -q '.defaultBranchRef.name')
467
485
 
486
+ # Pro / public-repo path — run the API call.
468
487
  gh api "repos/$REPO/branches/$DEFAULT_BRANCH/protection" \
469
488
  --method PUT \
470
489
  --input - <<EOF
@@ -484,6 +503,28 @@ required here — agent-created PRs (bassclef-sync) need to merge
484
503
  without human intervention for the mobile-sync story. Add stricter
485
504
  rules per-repo in `.github/branch-protection.yml` if the repo warrants.
486
505
 
506
+ **Free-tier private-repo fallback (path 3 above).** If the API
507
+ returned 403 and the operator picked "skip and manage locally":
508
+
509
+ ```bash
510
+ # Record the choice so later audits know main is not API-protected.
511
+ mkdir -p .claude
512
+ cat >> .claude/onboard-notes.md <<'NOTE'
513
+ ## Branch protection — operator-managed
514
+
515
+ GitHub free-tier does not include branch protection on private repos.
516
+ This repo relies on:
517
+ - Local discipline: no `git push --force` to main.
518
+ - PR review before merge.
519
+ - Pre-commit hooks shipped by bassclef.
520
+
521
+ Revisit if the repo moves to public, or the plan upgrades to Pro+.
522
+ NOTE
523
+ ```
524
+
525
+ The adopter can revisit this step after switching to public OR
526
+ upgrading the plan.
527
+
487
528
  ##### 1.3 Add the production approval gate (if this repo deploys to prod)
488
529
 
489
530
  If the repo will deploy to production, per `manual-prod-approval.md`:
@@ -108,6 +108,30 @@ Rules for external notes:
108
108
  - No feature names that aren't visible to users
109
109
  - Test: could someone who's never seen the codebase understand this?
110
110
 
111
+ ### 3.5. Run `/ogilvy-writing-audit` on each body
112
+
113
+ Before saving the drafted bodies to disk, run `/ogilvy-writing-audit`
114
+ on each one. The skill scans against Ogilvy + Roman-Raphaelson's
115
+ rules (see `standards/ogilvy-writing-rules.md`) and returns
116
+ severity-tagged findings.
117
+
118
+ ```bash
119
+ /ogilvy-writing-audit docs/release-notes/YYYY-MM-DD-internal.md
120
+ /ogilvy-writing-audit docs/release-notes/YYYY-MM-DD-external.md
121
+ ```
122
+
123
+ Fix every 🔴 and 🟡 finding before step 4. When a 🟡 ships without
124
+ a fix, cite the audit in the commit body and name why.
125
+
126
+ Why this step. Release notes land in reader surfaces that must read
127
+ clean — the GitHub Release artifact plus the Google Doc the team
128
+ reviews. Jargon and grade-13 prose creep in without a mechanical
129
+ check. PR #408 ran this discipline by hand across 8 entries, and
130
+ PR #412 formalizes it so every future entry inherits the floor.
131
+
132
+ Cross-ref: `.claude/rules/session-artifacts.md` §Release notes names
133
+ this step as the discipline anchor.
134
+
111
135
  ### 4. Save locally
112
136
 
113
137
  ```
@@ -1,7 +1,7 @@
1
1
  {
2
- "version": "v1.6.4",
3
- "marker_tag": "release-2026-09-28-21212e17",
4
- "release_date": "2026-09-28T01:33:41Z",
5
- "release_sha": "21212e17",
6
- "release_tag": "release-2026-09-28-21212e17"
2
+ "version": "v1.6.6",
3
+ "marker_tag": "release-2026-10-03-89ceaa15",
4
+ "release_date": "2026-10-03T10:51:10Z",
5
+ "release_sha": "89ceaa15",
6
+ "release_tag": "release-2026-10-03-89ceaa15"
7
7
  }
@@ -31,6 +31,14 @@ state/luminary-implementations/
31
31
  .claude/hooks/logs/
32
32
  .claude/hooks/*.trace
33
33
 
34
+ # SDLC traces (operator-private; runtime artifacts carry operator paths)
35
+ # Per #2036 Finding #1 (adopter report 2026-10-02) — auto-save was
36
+ # committing /Users/<name>/ paths to adopter git every fire. Shipping this
37
+ # line stops future adopter installs from leaking. Existing installs run
38
+ # `git rm --cached -r docs/sdlc-traces/` one time to untrack files
39
+ # already in history.
40
+ docs/sdlc-traces/
41
+
34
42
  # Build output
35
43
  dist/
36
44
 
@@ -72,9 +72,9 @@ _da_resolve_path() {
72
72
  da_get_field() {
73
73
  local id="$1"
74
74
  local field="$2"
75
- local path
76
- path="$(_da_resolve_path "$id")"
77
- [[ -f "$path" ]] || { echo "not-found: $path" >&2; return 1; }
75
+ local p
76
+ p="$(_da_resolve_path "$id")"
77
+ [[ -f "$p" ]] || { echo "not-found: $p" >&2; return 1; }
78
78
  # Read frontmatter only (between two `---` lines)
79
79
  awk -v f="$field" '
80
80
  BEGIN { in_fm = 0; seen_open = 0 }
@@ -89,7 +89,7 @@ da_get_field() {
89
89
  print
90
90
  exit
91
91
  }
92
- ' "$path"
92
+ ' "$p"
93
93
  }
94
94
 
95
95
  da_resolve() {
@@ -82,16 +82,16 @@ fixture_cleanup() {
82
82
  if [ "${SKIP_FIXTURE_BUILDER:-0}" = "1" ]; then
83
83
  return 0
84
84
  fi
85
- local path="${1:?fixture_cleanup requires path}"
86
- if [ ! -e "$path" ]; then
85
+ local p="${1:?fixture_cleanup requires path}"
86
+ if [ ! -e "$p" ]; then
87
87
  # Idempotent — nothing to do
88
88
  return 0
89
89
  fi
90
- if [ ! -f "$path/$_FIXTURE_SENTINEL" ]; then
91
- echo "fixture_cleanup: refusing to remove path without sentinel: $path" >&2
90
+ if [ ! -f "$p/$_FIXTURE_SENTINEL" ]; then
91
+ echo "fixture_cleanup: refusing to remove path without sentinel: $p" >&2
92
92
  return 1
93
93
  fi
94
- rm -rf "$path"
94
+ rm -rf "$p"
95
95
  return 0
96
96
  }
97
97
 
@@ -105,10 +105,10 @@ fixture_cleanup_all() {
105
105
  return 0
106
106
  fi
107
107
  local IFS=$'\n'
108
- local path
109
- for path in $_FIXTURE_REGISTRY; do
110
- [ -n "$path" ] || continue
111
- fixture_cleanup "$path" 2>/dev/null || true
108
+ local p
109
+ for p in $_FIXTURE_REGISTRY; do
110
+ [ -n "$p" ] || continue
111
+ fixture_cleanup "$p" 2>/dev/null || true
112
112
  done
113
113
  _FIXTURE_REGISTRY=""
114
114
  }
@@ -0,0 +1,85 @@
1
+ #!/bin/bash
2
+ # tier: lite
3
+ # Operator-identifier leak check for staged git content.
4
+ #
5
+ # Shared across:
6
+ # - .claude/hooks/save-state.sh (inline before `git commit`;
7
+ # bypasses pre-commit-gate.sh because save-state runs git
8
+ # directly, not through the agent's PreToolUse path)
9
+ # - future: .claude/hooks/pre-commit-gate.sh CCF-3 (unification
10
+ # deferred to a follow-on; shape intentionally matches CCF-3
11
+ # today so the two can merge without semantic drift)
12
+ #
13
+ # Rejects staged content carrying absolute operator paths matching
14
+ # /Users/<name>/ or /home/<name>/. Mirrors pre-commit-gate.sh:50-53
15
+ # exemptions so policy docs that quote the pattern don't trip.
16
+ #
17
+ # Per #2036 Finding #1 (adopter report 2026-10-02).
18
+ # Per @luminary nygard — stability pattern (circuit breaker on commit).
19
+ # Per @luminary hoare — precondition contract before `git commit`.
20
+
21
+ # check_staged_for_leaks — scan git-staged content for operator paths.
22
+ #
23
+ # Returns 0 when staged content is clean OR when every leaking file
24
+ # sits in an exempt path (policy docs, rules, luminaries).
25
+ # Returns 1 when staged content outside exempt paths carries a leak.
26
+ # On exit 1, every leaking file is named on stderr with line number
27
+ # and the matched pattern.
28
+ check_staged_for_leaks() {
29
+ # Staged non-exempt files. Match pre-commit-gate.sh:46-53 exemption shape
30
+ # so the two gates agree on which paths carry policy docs legitimately
31
+ # quoting operator paths.
32
+ local staged
33
+ staged=$(git diff --cached --name-only --diff-filter=AM 2>/dev/null | \
34
+ grep -vE '^\.claude/(rules|hooks|skills|luminaries|agents)/' | \
35
+ grep -vE '^standards/' | \
36
+ grep -vE '^architecture/' | \
37
+ grep -vE '^design/' | \
38
+ grep -vE '^docs/standards/' | \
39
+ grep -vE '^docs/rules/' | \
40
+ grep -vE '^chronicle/' | \
41
+ grep -vE '^strategy/' | \
42
+ grep -vE '^scripts/tests/' || true)
43
+
44
+ [ -z "$staged" ] && return 0
45
+
46
+ local leaked=""
47
+ while IFS= read -r f; do
48
+ [ -z "$f" ] && continue
49
+ [ ! -f "$f" ] && continue
50
+ local matches
51
+ matches=$(grep -nE '/Users/[A-Za-z0-9_.-]+|/home/[A-Za-z0-9_.-]+' "$f" 2>/dev/null || true)
52
+ if [ -n "$matches" ]; then
53
+ leaked="${leaked}
54
+ ${f}:
55
+ ${matches}
56
+ "
57
+ fi
58
+ done <<< "$staged"
59
+
60
+ if [ -n "$leaked" ]; then
61
+ {
62
+ echo ""
63
+ echo "============================================"
64
+ echo "BLOCKED: operator path in staged content (#2036 Finding #1)"
65
+ echo "============================================"
66
+ echo "$leaked"
67
+ echo ""
68
+ echo "Operator-specific paths (/Users/<name>, /home/<name>) must NOT"
69
+ echo "ship in adopter git. Common cause: docs/sdlc-traces/*.log"
70
+ echo "carrying absolute paths from PreToolUse hook callers."
71
+ echo ""
72
+ echo "Fixes:"
73
+ echo " 1. Delete the staged file if it's a trace log auto-generated"
74
+ echo " (gitignored going forward per #2036 Finding #1 cure)"
75
+ echo " 2. Rewrite the path as repo-relative"
76
+ echo " 3. Move policy docs under docs/standards/ or docs/rules/ (exempt)"
77
+ echo ""
78
+ echo "Policy: standards/two-layer-config.md (bassclef#740)"
79
+ echo "============================================"
80
+ } >&2
81
+ return 1
82
+ fi
83
+
84
+ return 0
85
+ }
@@ -49,14 +49,14 @@ ps_exists() {
49
49
 
50
50
  ps_create() {
51
51
  local slug="$1"
52
- local path
53
- path="$(ps_path "$slug")"
54
- mkdir -p "$(dirname "$path")"
55
- if [[ -f "$path" ]]; then
56
- echo "ps_create: already exists: $path" >&2
52
+ local p
53
+ p="$(ps_path "$slug")"
54
+ mkdir -p "$(dirname "$p")"
55
+ if [[ -f "$p" ]]; then
56
+ echo "ps_create: already exists: $p" >&2
57
57
  return 1
58
58
  fi
59
- cat > "$path" <<EOF
59
+ cat > "$p" <<EOF
60
60
  slug: ${slug}
61
61
  created: $(date -u +'%Y-%m-%dT%H:%M:%S+0000')
62
62
  current_tier: pending
@@ -73,16 +73,16 @@ EOF
73
73
  ps_get() {
74
74
  local slug="$1"
75
75
  local field="$2"
76
- local path
77
- path="$(ps_path "$slug")"
78
- [[ -f "$path" ]] || { echo "ps_get: not-found: $path" >&2; return 1; }
76
+ local p
77
+ p="$(ps_path "$slug")"
78
+ [[ -f "$p" ]] || { echo "ps_get: not-found: $p" >&2; return 1; }
79
79
  awk -v f="$field" '
80
80
  $0 ~ "^" f ":" {
81
81
  sub("^" f ":[[:space:]]*", "")
82
82
  print
83
83
  exit
84
84
  }
85
- ' "$path"
85
+ ' "$p"
86
86
  }
87
87
 
88
88
  ps_set() {
@@ -90,9 +90,9 @@ ps_set() {
90
90
  local field="$2"
91
91
  shift 2
92
92
  local value="$*"
93
- local path
94
- path="$(ps_path "$slug")"
95
- [[ -f "$path" ]] || { echo "ps_set: not-found: $path" >&2; return 1; }
93
+ local p
94
+ p="$(ps_path "$slug")"
95
+ [[ -f "$p" ]] || { echo "ps_set: not-found: $p" >&2; return 1; }
96
96
  local tmp
97
97
  tmp="$(mktemp)"
98
98
  awk -v f="$field" -v v="$value" '
@@ -108,8 +108,8 @@ ps_set() {
108
108
  print f ": " v
109
109
  }
110
110
  }
111
- ' "$path" > "$tmp"
112
- mv "$tmp" "$path"
111
+ ' "$p" > "$tmp"
112
+ mv "$tmp" "$p"
113
113
  }
114
114
 
115
115
  ps_status() {
@@ -146,9 +146,9 @@ ps_archive() {
146
146
  ps_reset() {
147
147
  local slug="$1"
148
148
  local reason="${2:-no reason given}"
149
- local path
150
- path="$(ps_path "$slug")"
151
- [[ -f "$path" ]] || { echo "ps_reset: not-found: $path" >&2; return 1; }
149
+ local p
150
+ p="$(ps_path "$slug")"
151
+ [[ -f "$p" ]] || { echo "ps_reset: not-found: $p" >&2; return 1; }
152
152
 
153
153
  local now
154
154
  now="$(date -u +'%Y-%m-%dT%H:%M:%S+0000')"
@@ -188,8 +188,8 @@ EOF
188
188
  next
189
189
  }
190
190
  { print }
191
- ' "$path" > "$tmp"
192
- mv "$tmp" "$path"
191
+ ' "$p" > "$tmp"
192
+ mv "$tmp" "$p"
193
193
  rm -f "$entry_file"
194
194
 
195
195
  # Reset cycle-state fields
@@ -0,0 +1,61 @@
1
+ #!/bin/bash
2
+ # tier: lite
3
+ # SKIP_* env-reach check for PreToolUse Bash hooks.
4
+ #
5
+ # Per #2036 Finding #4 — adopter report 2026-10-02.
6
+ #
7
+ # PreToolUse Bash hooks run in Claude Code's process env. The agent's
8
+ # `SKIP_FOO=1 git commit ...` recipe sets FOO in the git subshell's env,
9
+ # not the hook's env. Hooks that only read `${SKIP_FOO:-0}` miss the
10
+ # inline override and the recipe lies to the adopter.
11
+ #
12
+ # This helper parses tool_input.command for inline `SKIP_FOO=1` and
13
+ # falls back to process env. The adopter's natural recipe
14
+ # `SKIP_FOO=1 cmd` now works regardless of harness env propagation.
15
+ #
16
+ # Retirement path: once Claude Code harness guarantees inline-env
17
+ # propagation to hook subprocesses, this helper becomes redundant.
18
+ # Comment stays as the audit trail.
19
+ #
20
+ # Usage:
21
+ # source "$CLAUDE_PROJECT_DIR/lib/skip-env-inline-check.sh"
22
+ # COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
23
+ # if skip_var_set "SKIP_FOO" "$COMMAND"; then
24
+ # # bypass
25
+ # exit 0
26
+ # fi
27
+ #
28
+ # Anchors:
29
+ # @luminary john-ousterhout — deep module; narrow interface
30
+ # @luminary david-parnas — information hiding; consumer-free of mechanism
31
+ # @luminary alan-cooper — adopter-goal reliable override recipe
32
+
33
+ # skip_var_set — return 0 if the named SKIP_* var is set to "1".
34
+ # Checks two sources:
35
+ # 1. Inline prefix in command string: `SKIP_FOO=1 cmd...`
36
+ # 2. Fallback: process env `$SKIP_FOO`
37
+ # Returns 1 otherwise.
38
+ #
39
+ # Args:
40
+ # $1 — var name (e.g., "SKIP_OPERATOR_PATHS")
41
+ # $2 — command string (from tool_input.command)
42
+ skip_var_set() {
43
+ local var_name="$1"
44
+ local cmd="$2"
45
+
46
+ # Source 1 — inline prefix. The pattern anchors on word boundary (start
47
+ # of string or whitespace) and requires =1 followed by whitespace or
48
+ # end-of-string. This matches bash's own parsing of inline env prefix.
49
+ if [ -n "$cmd" ]; then
50
+ if echo "$cmd" | grep -qE "(^|[[:space:]])${var_name}=1([[:space:]]|$)"; then
51
+ return 0
52
+ fi
53
+ fi
54
+
55
+ # Source 2 — process env fallback (works where harness propagates env).
56
+ if [ "$(eval "echo \"\${${var_name}:-0}\"")" = "1" ]; then
57
+ return 0
58
+ fi
59
+
60
+ return 1
61
+ }
@@ -186,17 +186,19 @@ __state_atomic_write() {
186
186
  # crash caller).
187
187
  __state_yaml_write() {
188
188
  __state_check_binary_deps || return 4
189
- local path="$1"
189
+ # Finding #6 (#2036): `local path` is a zsh-magic variable name — ties to
190
+ # $PATH when sourced from zsh. Renamed to `local p` throughout this file.
191
+ local p="$1"
190
192
  local field="$2"
191
193
  local value="$3"
192
- if [ -z "$path" ] || [ -z "$field" ]; then
194
+ if [ -z "$p" ] || [ -z "$field" ]; then
193
195
  echo "__state_yaml_write: requires <path> <field> <value>" >&2
194
196
  return 2
195
197
  fi
196
198
  local dir
197
- dir="$(dirname "$path")"
199
+ dir="$(dirname "$p")"
198
200
  [ -d "$dir" ] || mkdir -p "$dir" || return 4
199
- local tmp="${path}.tmp.$$"
201
+ local tmp="${p}.tmp.$$"
200
202
  BASSCLEF_YAML_FIELD="$field" \
201
203
  BASSCLEF_YAML_VALUE="$value" \
202
204
  BASSCLEF_YAML_TMP="$tmp" \
@@ -213,7 +215,7 @@ except Exception as e:
213
215
  print(f"__state_yaml_write: safe_dump failed: {e}", file=sys.stderr)
214
216
  sys.exit(3)
215
217
  ' 2>&1 || { rm -f "$tmp"; return 3; }
216
- mv "$tmp" "$path" || { rm -f "$tmp"; return 4; }
218
+ mv "$tmp" "$p" || { rm -f "$tmp"; return 4; }
217
219
  }
218
220
 
219
221
  # === Whereami (Shape A — JSON authoritative + markdown rendered) ===
@@ -239,21 +241,22 @@ state_whereami_get() {
239
241
  state_whereami_set() {
240
242
  local field="$1"
241
243
  local value="$2"
242
- local path="docs/whereami.json"
244
+ local p="docs/whereami.json"
243
245
  if [ -z "$field" ] || [ -z "$value" ]; then
244
246
  echo "state_whereami_set: requires <field> <value>" >&2
245
247
  return 2
246
248
  fi
247
- if [ ! -f "$path" ]; then
249
+ if [ ! -f "$p" ]; then
248
250
  echo "state_whereami_set: docs/whereami.json does not exist" >&2
249
251
  return 4
250
252
  fi
251
253
  local updated
252
- # Use jq's setpath for nested field paths (e.g., 'phase.value')
254
+ # Use jq's setpath for nested field paths (e.g., 'phase.value').
255
+ # The jq `--arg path` names a jq variable (not shell) — safe to keep.
253
256
  updated="$(jq --arg path "$field" --arg val "$value" '
254
257
  setpath($path | split("."); $val)
255
- ' "$path")" || return 3
256
- __state_atomic_write "$path" "$updated"
258
+ ' "$p")" || return 3
259
+ __state_atomic_write "$p" "$updated"
257
260
  }
258
261
 
259
262
  # === IterationBet (Shape B — markdown + YAML frontmatter + body) ===
@@ -264,11 +267,11 @@ state_iteration_bet_get() {
264
267
  echo "state_iteration_bet_get: requires <slug>" >&2
265
268
  return 2
266
269
  fi
267
- local path="docs/iteration-bets/${slug}.md"
268
- if [ ! -f "$path" ]; then
270
+ local p="docs/iteration-bets/${slug}.md"
271
+ if [ ! -f "$p" ]; then
269
272
  return 1
270
273
  fi
271
- __state_yq2json "$path"
274
+ __state_yq2json "$p"
272
275
  }
273
276
 
274
277
  # Special-purpose: returns the single active iteration bet (status=active),
@@ -333,25 +336,25 @@ state_roadmap_get() {
333
336
  echo "state_roadmap_get: requires <slug>" >&2
334
337
  return 2
335
338
  fi
336
- local path="docs/roadmaps/${slug}.md"
337
- if [ ! -f "$path" ]; then
339
+ local p="docs/roadmaps/${slug}.md"
340
+ if [ ! -f "$p" ]; then
338
341
  return 1
339
342
  fi
340
- __state_yq2json "$path"
343
+ __state_yq2json "$p"
341
344
  }
342
345
 
343
346
  # Internal: rewrite a roadmap's frontmatter from JSON, preserving the body.
344
347
  __state_roadmap_write_frontmatter() {
345
348
  local slug="$1"
346
349
  local frontmatter_json="$2"
347
- local path="docs/roadmaps/${slug}.md"
348
- if [ ! -f "$path" ]; then
349
- echo "__state_roadmap_write_frontmatter: roadmap $path not found" >&2
350
+ local p="docs/roadmaps/${slug}.md"
351
+ if [ ! -f "$p" ]; then
352
+ echo "__state_roadmap_write_frontmatter: roadmap $p not found" >&2
350
353
  return 1
351
354
  fi
352
355
  python3 -c "
353
356
  import sys, yaml, json
354
- path = '$path'
357
+ path = '$p'
355
358
  fm = json.loads('''$frontmatter_json''')
356
359
  with open(path, 'r') as f:
357
360
  content = f.read()
@@ -726,15 +729,15 @@ state_chronicle_gate_evidence() {
726
729
  return 2
727
730
  fi
728
731
  # Try bassclef (chronicle/) and consumer-repo (docs/chronicle/) paths
729
- local path=""
732
+ local p=""
730
733
  if [ -f "chronicle/${id}.md" ]; then
731
- path="chronicle/${id}.md"
734
+ p="chronicle/${id}.md"
732
735
  elif [ -f "docs/chronicle/${id}.md" ]; then
733
- path="docs/chronicle/${id}.md"
736
+ p="docs/chronicle/${id}.md"
734
737
  else
735
738
  return 1
736
739
  fi
737
- __state_yq2json "$path" | jq -c '.gates_fired // []'
740
+ __state_yq2json "$p" | jq -c '.gates_fired // []'
738
741
  }
739
742
 
740
743
  # List chronicle IDs (filename without .md extension), optionally limited
@@ -1081,16 +1084,16 @@ __state_platform_resolve_path() {
1081
1084
 
1082
1085
  state_platform_get() {
1083
1086
  local key="${1:-}"
1084
- local path
1085
- path="$(__state_platform_resolve_path)"
1086
- if [ ! -f "$path" ]; then
1087
+ local p
1088
+ p="$(__state_platform_resolve_path)"
1089
+ if [ ! -f "$p" ]; then
1087
1090
  return 1
1088
1091
  fi
1089
1092
  local json
1090
1093
  json="$(python3 -c "
1091
1094
  import sys, yaml, json
1092
1095
  try:
1093
- with open('$path') as f:
1096
+ with open('$p') as f:
1094
1097
  data = yaml.safe_load(f) or {}
1095
1098
  print(json.dumps(data, default=str))
1096
1099
  except Exception as e:
@@ -131,35 +131,35 @@ PYEOF
131
131
  # Mirrors detection shape from .claude/hooks/tier-tag-required-at-write.sh so
132
132
  # the reader half and the writer half stay consistent.
133
133
  resolve_file_tier() {
134
- local path="$1"
135
- if [ ! -f "$path" ]; then
134
+ local p="$1"
135
+ if [ ! -f "$p" ]; then
136
136
  return 0
137
137
  fi
138
138
 
139
139
  local tier=""
140
- case "$path" in
140
+ case "$p" in
141
141
  *.md)
142
142
  # Frontmatter between --- markers, first 30 lines
143
143
  tier=$(awk '
144
144
  /^---/ { in_fm = !in_fm; if (!in_fm) exit; next }
145
145
  in_fm && /^tier:[[:space:]]/ { sub(/^tier:[[:space:]]*/, ""); sub(/[[:space:]]*$/, ""); print; exit }
146
146
  NR > 30 { exit }
147
- ' "$path" 2>/dev/null)
147
+ ' "$p" 2>/dev/null)
148
148
  ;;
149
149
  *.sh|*.py)
150
- tier=$(head -10 "$path" 2>/dev/null | grep -E '^#[[:space:]]*tier:[[:space:]]' | head -1 | sed -E 's/^#[[:space:]]*tier:[[:space:]]*//; s/[[:space:]]*$//')
150
+ tier=$(head -10 "$p" 2>/dev/null | grep -E '^#[[:space:]]*tier:[[:space:]]' | head -1 | sed -E 's/^#[[:space:]]*tier:[[:space:]]*//; s/[[:space:]]*$//')
151
151
  ;;
152
152
  *.yml|*.yaml)
153
- tier=$(head -30 "$path" 2>/dev/null | grep -E '^#[[:space:]]*tier:[[:space:]]|^tier:[[:space:]]' | head -1 | sed -E 's/^#?[[:space:]]*tier:[[:space:]]*//; s/[[:space:]]*$//')
153
+ tier=$(head -30 "$p" 2>/dev/null | grep -E '^#[[:space:]]*tier:[[:space:]]|^tier:[[:space:]]' | head -1 | sed -E 's/^#?[[:space:]]*tier:[[:space:]]*//; s/[[:space:]]*$//')
154
154
  ;;
155
155
  *.json|*.jsonc)
156
- tier=$(head -30 "$path" 2>/dev/null | grep -oE '"(_?tier)"[[:space:]]*:[[:space:]]*"[a-z]+"' | head -1 | sed -E 's/.*:[[:space:]]*"([a-z]+)"/\1/')
156
+ tier=$(head -30 "$p" 2>/dev/null | grep -oE '"(_?tier)"[[:space:]]*:[[:space:]]*"[a-z]+"' | head -1 | sed -E 's/.*:[[:space:]]*"([a-z]+)"/\1/')
157
157
  if [ -z "$tier" ]; then
158
- tier=$(head -30 "$path" 2>/dev/null | grep -oE '//[[:space:]]*tier:[[:space:]]*[a-z]+' | head -1 | sed -E 's/.*tier:[[:space:]]*//')
158
+ tier=$(head -30 "$p" 2>/dev/null | grep -oE '//[[:space:]]*tier:[[:space:]]*[a-z]+' | head -1 | sed -E 's/.*tier:[[:space:]]*//')
159
159
  fi
160
160
  ;;
161
161
  *.txt)
162
- tier=$(head -1 "$path" 2>/dev/null | grep -E '^#[[:space:]]*tier:[[:space:]]' | sed -E 's/^#[[:space:]]*tier:[[:space:]]*//; s/[[:space:]]*$//')
162
+ tier=$(head -1 "$p" 2>/dev/null | grep -E '^#[[:space:]]*tier:[[:space:]]' | sed -E 's/^#[[:space:]]*tier:[[:space:]]*//; s/[[:space:]]*$//')
163
163
  ;;
164
164
  esac
165
165