@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.
- package/README.md +6 -2
- package/dist/cli.cjs +89 -7
- package/dist/cli.js +89 -7
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lite/.claude/hooks/git-stash-u-guard.sh +4 -1
- package/dist/lite/.claude/hooks/post-skill-friction-check.sh +6 -1
- package/dist/lite/.claude/hooks/pre-commit-gate.sh +18 -6
- package/dist/lite/.claude/hooks/pre-commit-identifier-leak-scrub.sh +10 -6
- package/dist/lite/.claude/hooks/pre-commit-manifest-autoregen.sh +4 -2
- package/dist/lite/.claude/hooks/save-state.sh +24 -0
- package/dist/lite/.claude/hooks/trace-helper.sh +29 -0
- package/dist/lite/.claude/rules/lite-manifest-schema-change-discipline.md +30 -0
- package/dist/lite/.claude/settings.json +1 -1
- package/dist/lite/.claude/skills/build/SKILL.md +19 -7
- package/dist/lite/.claude/skills/onboard-repo/SKILL.md +41 -0
- package/dist/lite/.claude/skills/release-notes/SKILL.md +24 -0
- package/dist/lite/bassclef-version.json +5 -5
- package/dist/lite/gitignore +8 -0
- package/dist/lite/lib/deferred-actions.sh +4 -4
- package/dist/lite/lib/fixture-builder.sh +9 -9
- package/dist/lite/lib/identifier-leak-check.sh +85 -0
- package/dist/lite/lib/preview-state.sh +20 -20
- package/dist/lite/lib/skip-env-inline-check.sh +61 -0
- package/dist/lite/lib/state.sh +31 -28
- package/dist/lite/lib/tier-check.sh +9 -9
- package/dist/lite/lib/tier-dependency-audit.sh +23 -23
- package/dist/lite/presence/install/schedule-auto-save.taskscheduler.md +1 -1
- package/dist/lite/standards/lite-manifest-schema-changes.md +38 -0
- package/dist/lite/standards/lite-manifest.json +38 -38
- package/dist/lite/templates/spec-template.md +10 -6
- package/package.json +1 -1
- package/dist/lite/.claude/hooks/longrun-prep-compounding-axis-check.sh +0 -492
- 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
|
|
334
|
-
#
|
|
335
|
-
#
|
|
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 '
|
|
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
|
|
340
|
-
WU_COUNT=$(echo "$WU_LIST" | grep -
|
|
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
|
-
|
|
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.
|
|
3
|
-
"marker_tag": "release-2026-
|
|
4
|
-
"release_date": "2026-
|
|
5
|
-
"release_sha": "
|
|
6
|
-
"release_tag": "release-2026-
|
|
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
|
}
|
package/dist/lite/gitignore
CHANGED
|
@@ -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
|
|
76
|
-
|
|
77
|
-
[[ -f "$
|
|
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
|
-
' "$
|
|
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
|
|
86
|
-
if [ ! -e "$
|
|
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 "$
|
|
91
|
-
echo "fixture_cleanup: refusing to remove path without sentinel: $
|
|
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 "$
|
|
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
|
|
109
|
-
for
|
|
110
|
-
[ -n "$
|
|
111
|
-
fixture_cleanup "$
|
|
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
|
|
53
|
-
|
|
54
|
-
mkdir -p "$(dirname "$
|
|
55
|
-
if [[ -f "$
|
|
56
|
-
echo "ps_create: already exists: $
|
|
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 > "$
|
|
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
|
|
77
|
-
|
|
78
|
-
[[ -f "$
|
|
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
|
-
' "$
|
|
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
|
|
94
|
-
|
|
95
|
-
[[ -f "$
|
|
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
|
-
' "$
|
|
112
|
-
mv "$tmp" "$
|
|
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
|
|
150
|
-
|
|
151
|
-
[[ -f "$
|
|
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
|
-
' "$
|
|
192
|
-
mv "$tmp" "$
|
|
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
|
+
}
|
package/dist/lite/lib/state.sh
CHANGED
|
@@ -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
|
|
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 "$
|
|
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 "$
|
|
199
|
+
dir="$(dirname "$p")"
|
|
198
200
|
[ -d "$dir" ] || mkdir -p "$dir" || return 4
|
|
199
|
-
local 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" "$
|
|
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
|
|
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 "$
|
|
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
|
-
' "$
|
|
256
|
-
__state_atomic_write "$
|
|
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
|
|
268
|
-
if [ ! -f "$
|
|
270
|
+
local p="docs/iteration-bets/${slug}.md"
|
|
271
|
+
if [ ! -f "$p" ]; then
|
|
269
272
|
return 1
|
|
270
273
|
fi
|
|
271
|
-
__state_yq2json "$
|
|
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
|
|
337
|
-
if [ ! -f "$
|
|
339
|
+
local p="docs/roadmaps/${slug}.md"
|
|
340
|
+
if [ ! -f "$p" ]; then
|
|
338
341
|
return 1
|
|
339
342
|
fi
|
|
340
|
-
__state_yq2json "$
|
|
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
|
|
348
|
-
if [ ! -f "$
|
|
349
|
-
echo "__state_roadmap_write_frontmatter: roadmap $
|
|
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 = '$
|
|
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
|
|
732
|
+
local p=""
|
|
730
733
|
if [ -f "chronicle/${id}.md" ]; then
|
|
731
|
-
|
|
734
|
+
p="chronicle/${id}.md"
|
|
732
735
|
elif [ -f "docs/chronicle/${id}.md" ]; then
|
|
733
|
-
|
|
736
|
+
p="docs/chronicle/${id}.md"
|
|
734
737
|
else
|
|
735
738
|
return 1
|
|
736
739
|
fi
|
|
737
|
-
__state_yq2json "$
|
|
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
|
|
1085
|
-
|
|
1086
|
-
if [ ! -f "$
|
|
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('$
|
|
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
|
|
135
|
-
if [ ! -f "$
|
|
134
|
+
local p="$1"
|
|
135
|
+
if [ ! -f "$p" ]; then
|
|
136
136
|
return 0
|
|
137
137
|
fi
|
|
138
138
|
|
|
139
139
|
local tier=""
|
|
140
|
-
case "$
|
|
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
|
-
' "$
|
|
147
|
+
' "$p" 2>/dev/null)
|
|
148
148
|
;;
|
|
149
149
|
*.sh|*.py)
|
|
150
|
-
tier=$(head -10 "$
|
|
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 "$
|
|
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 "$
|
|
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 "$
|
|
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 "$
|
|
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
|
|