From da4e6b959053c136781460d3f781eaf8eaf34614 Mon Sep 17 00:00:00 2001 From: Bastien Chanot Date: Tue, 2 Jun 2026 18:31:48 +0200 Subject: [PATCH] feat(profile): add `gstack on|off` verb to lib/profile.sh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Centralize gstack toggling in the `profile` command without losing the active-profile label. - `gstack on` re-enables ALL parked gstack skills (moves skills-disabled/gstack__* back) but does NOT touch .active-profile, so the user layers full gstack on top of their current profile and the statusline label is preserved. Unlike `reset`, which clears the label to "none". - `gstack off` disables gstack skills not listed in the active profile; errors cleanly when no profile is active (needs one to know what to keep). Refactor (behavior-preserving): extract three shared helpers `enable_all_gstack`, `disable_gstack_not_in`, `parked_gstack_count` and rewire `cmd_reset` + `cmd_set` to reuse them instead of duplicating the symlink-toggle loops. Wire `gstack` into main() dispatch, usage(), and the header usage block. Docs: SKILL.md argument-hint, examples, and output-policy updated. The generic `make profile cmd="gstack on"` target already covers Make usage. Verified: shellcheck CLEAN, `bash -n` OK, 6-case test (help, bad-action, off-with-no-profile, on, off-trim, on-cycle) with final assertion that the live symlink state was restored exactly to its pre-test value. Memory: capitalize BDR-018 (decision), LRN-024 (DRY helper-extraction pattern), BLK-007 (6 gstack source skills ios-*/spec unlinked post submodule bump — open follow-up), EVAL-002 (self-eval, false "full.profile bug" flag corrected pre-edit). Backfill index drift: BDR-017, BLK-005/006. Co-Authored-By: Claude Opus 4.8 (1M context) --- .claude/memory/blockers.md | 11 ++++ .claude/memory/decisions.md | 16 +++++ .claude/memory/evals.md | 13 +++- .claude/memory/journal.md | 8 +++ .claude/memory/learnings.md | 12 ++++ .claude/tasks/TODO.md | 13 ++++ lib/profile.sh | 122 +++++++++++++++++++++++++++--------- skills/profile/SKILL.md | 14 +++-- 8 files changed, 174 insertions(+), 35 deletions(-) diff --git a/.claude/memory/blockers.md b/.claude/memory/blockers.md index bfb6921..54fa7db 100644 --- a/.claude/memory/blockers.md +++ b/.claude/memory/blockers.md @@ -24,6 +24,9 @@ rules: | BLK-002 | 2026-04-23 | `rmdir` denied in sandbox on empty directory | resolved | | BLK-003 | 2026-05-12 | `scripts/screenshot.mjs` hardcoded macOS path blocks PNG cards on Linux | upstream | | BLK-004 | 2026-05-20 | `/ship-feature` wrapper at `~/.claude/commands/` points to deleted agent files post-refactor | resolved | +| BLK-005 | 2026-05-21 | gstack submodule rename (checkpoint→context-save) breaks profile entries | resolved | +| BLK-006 | 2026-05-21 | `profile.sh current` false-negative via `~/.claude` symlink (`cd` not `cd -P`) | resolved | +| BLK-007 | 2026-06-02 | 6 gstack source skills (ios-*, spec) unlinked post-bump — invisible to profiles + `gstack on` | open | --- @@ -82,3 +85,11 @@ rules: - **Real cause**: `lib/profile.sh:43` set `REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"`. Default bash `cd` preserves symlinks (logical pathname mode, `set -P` off). When the script is invoked via the `~/.claude/lib/profile.sh` symlink (link.sh wires `~/.claude/lib -> /lib`), `$BASH_SOURCE[0]` is the symlinked path, `dirname` returns `~/.claude/lib`, `cd ..` lands at `~/.claude`, and `pwd` returns the logical path `/home/bchanot-ubuntu/.claude`. `$SKILLS_DIR="$REPO/skills"` still works because `~/.claude/skills` happens to be a symlink to the repo's `skills/`. But `$DISABLED_DIR="$REPO/skills-disabled"` resolves to `~/.claude/skills-disabled` — a real sibling directory created at some earlier point containing only 2 stale npx-skill symlinks (`darwin-skill`, `find-skills`). `cmd_current` scans this near-empty dir, finds 0 `gstack__*` entries, returns the "none" sentinel. - **Solution**: `REPO="$(cd -P "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"` (commit `a4558ee`). `-P` forces physical-path resolution so `$REPO` is always the real repo path regardless of how the script is invoked. Verify: `bash "$HOME/.claude/lib/profile.sh" current` now returns `full (100% match, 14 gstack skills disabled)`. - **Status**: resolved. Follow-up: `~/.claude/skills-disabled/` (real dir with only `darwin-skill`/`find-skills` symlinks) is orphaned — these npx skills are already symlinked into `/skills/` by link.sh, so the disabled-side copies serve no purpose. Could be deleted to remove confusion, but harmless as-is. + +## BLK-007 — 6 gstack source skills (ios-*, spec) unlinked — invisible to profile system + `gstack on` + +- **Date**: 2026-06-02 +- **Friction**: `skills-external/gstack/` has 53 source skills; 6 (`ios-clean`, `ios-design-review`, `ios-fix`, `ios-qa`, `ios-sync`, `spec`) exist ONLY as source — NOT symlinked into `skills/` (enabled) nor `skills-disabled/gstack__*` (parked). So invisible to Claude AND untouched by `reset`/`gstack on` (both operate on parked `gstack__*` only). Surfaced while adding `gstack on|off`: `comm` of gstack source vs `full.profile`. +- **Real cause**: gstack submodule bump added new skills; gstack's own `./setup` (source of truth for per-skill symlinks per link.sh) not re-run → symlinks never created. Same lifecycle gap class as [[toggle-external-source-only-state]] (LRN-007). NOT a `full.profile` bug — full curated by design (BDR-017 caveat: "full excludes rarely-used gstack skills"). Initial "full omits ios = bug" flag was WRONG, self-corrected (see EVAL-002). +- **Solution**: re-run gstack setup to link new skills, then reconcile profiles (decide if iOS skills belong in web/dev profiles — likely NOT). Per [[gstack-rename-profile-audit]] (LRN-022): diff `skills-external/gstack/` vs `lib/profiles/*.profile` after every submodule bump. NOT auto-fixed — gstack installer domain + iOS-in-web judgment call. +- **Status**: open. Low impact (skills unused today, never linked). Next: run gstack `./setup`, audit profile membership. diff --git a/.claude/memory/decisions.md b/.claude/memory/decisions.md index bc00f92..47e65f3 100644 --- a/.claude/memory/decisions.md +++ b/.claude/memory/decisions.md @@ -38,6 +38,8 @@ rules: | BDR-014 | 2026-05-11 | Personal SKILL.md descriptions: "Use when [triggers]…" pattern + 1024-char spec limit | accepted | | BDR-015 | 2026-05-12 | Exclude broken gstack symlinks from /darwin-skill scope (external ownership) | accepted | | BDR-016 | 2026-05-15 | doc-syncer: README AUTO+unconditional, DEPLOY.md prod-only + 14-section VPS template | accepted | +| BDR-017 | 2026-05-18 | `full` profile = web-full + plan + dev superset for /init-project MVP | accepted | +| BDR-018 | 2026-06-02 | `profile gstack on/off` verb — toggle gstack keeping active-profile label | accepted | --- @@ -321,3 +323,17 @@ rules: - `full` excludes a few rarely-used gstack skills (devex-review, pair-agent, gstack-upgrade, skills-perso). `set full` will disable those; user can `apply ` after to add back. - Sentinel rename "full" → "none" is breaking for any tooling that grepped `cmd_current` output for literal "full". No known consumers in this repo. - **Reference**: commit message references `lib/profiles/full.profile` (new), `lib/profile.sh:421` sentinel, `skills/profile/SKILL.md` table row. Linked to [[profile-sentinel-collision]] (LRN-020). + +--- + +## BDR-018 — `profile gstack on|off` verb keeps active-profile label + +- **Date**: 2026-06-02 +- **Status**: accepted +- **Decision**: New `cmd_gstack()` in `lib/profile.sh`. `gstack on` = re-enable all parked gstack (move `skills-disabled/gstack__*` back), DON'T touch `.active-profile`. `gstack off` = disable gstack skills not in active profile (errors if active=none). Wired into `main()` dispatch + `usage()` + header block + `skills/profile/SKILL.md` (argument-hint + examples + output-policy). +- **Why**: User wanted central command for "enable all gstack" + "disable gstack not needed by profile". Both ops existed (`reset`, `set`) but `reset` clobbers `.active-profile` to "none" — loses profile context in statusline. New verb does same skill-toggle WITHOUT clearing label, so user layers full gstack on top of current profile (e.g. `dev`) and statusline still reads `dev`. +- **Alternatives rejected**: + - 3 new profiles (current+gstack, current+gsd, current+gsd+gstack) — rejected: `gsd` = standalone CLI (not profile-toggleable, always-on, 0 passive token), so 2 of 3 meaningless. `full` already = current+gstack+gsd advisory. `apply` already additive. + - Just document `reset`/`set` — rejected: user wanted clearer centralized verb + label preservation. +- **Impl note**: extracted 3 shared helpers (`enable_all_gstack`, `disable_gstack_not_in`, `parked_gstack_count`); `cmd_reset`+`cmd_set` refactored to reuse (behavior preserved exact, verified by test). See [[dry-helper-extract-sibling-command]] (LRN-024). +- **Reference**: `lib/profile.sh` cmd_gstack + helpers, `skills/profile/SKILL.md`. Linked to [[full-profile-superset-init-project]] (BDR-017), [[gstack-source-only-skills-unlinked]] (BLK-007). diff --git a/.claude/memory/evals.md b/.claude/memory/evals.md index b57ff1c..e426a2f 100644 --- a/.claude/memory/evals.md +++ b/.claude/memory/evals.md @@ -22,6 +22,7 @@ rules: | ID | Date | Output | Action | |----|------|--------|--------| | EVAL-001 | 2026-04-23 | `.claude/` restructure plan (ship-feature STEP 2) | keep | +| EVAL-002 | 2026-06-02 | `profile gstack on/off` verb implementation | keep | --- @@ -31,4 +32,14 @@ rules: - **Output**: 21-task plan migrate `tasks/` to `.claude/tasks/` + create `.claude/memory/` + `.claude/audits/` + integrate CAPITALIZE across 5 skills + add `/close` skill. - **Method**: manual review of 5 impacted skills/agents; verified `rtk` path-agnostic; confirmed `~/.claude/CLAUDE.md` symlinks to project (single file edit). Radical-honesty check on session-close ritual: confirmed aspirational without skill integration → scope expanded to Option D. - **Anomalies**: none blocking. Note: `tasks/LESSONS.md` empty (101B, header only) — migration to `learnings.md` symbolic. -- **Action**: keep — plan validated, ready for execution. \ No newline at end of file +- **Action**: keep — plan validated, ready for execution. + +--- + +## EVAL-002 — `profile gstack on|off` verb implementation + +- **Date**: 2026-06-02 +- **Output**: `cmd_gstack()` + 3 extracted helpers in `lib/profile.sh`; `cmd_reset`/`cmd_set` refactored to reuse; `skills/profile/SKILL.md` doc updated. +- **Method**: shellcheck 0.10.0 (CLEAN) + `bash -n`; 6-case live test (help; bad-action exit 1; `off` with active=none → exit 1 zero-mutation; `on` restores 14 + label `full` preserved NOT cleared; `off` trim; `on` cycle) with saved manifest + final assertion final-state == original (PASS, live env untouched). +- **Anomalies**: (1) Initial flag "full.profile omits ios/spec = bug" WRONG — full curated by design, confirmed by BDR-017 caveat. Self-corrected BEFORE any edit, no bad change shipped. Lesson: verify profile INTENT vs source completeness before calling omission a bug. (2) Surfaced real source-only gap → BLK-007 (open). +- **Action**: keep — verb works, tested, documented; false bug-flag caught pre-edit. \ No newline at end of file diff --git a/.claude/memory/journal.md b/.claude/memory/journal.md index 1a194ce..dbab82b 100644 --- a/.claude/memory/journal.md +++ b/.claude/memory/journal.md @@ -129,3 +129,11 @@ rules: - `/hotfix` follow-up — `bash "$HOME/.claude/lib/profile.sh" current` falsely reported `none (all gstack skills enabled — no profile set)` even with profile applied + 14 gstack__* entries in repo's `skills-disabled/`. Root cause: `lib/profile.sh:43` used `cd "$(dirname $BASH_SOURCE)/.."` — default bash `cd` preserves symlinks, so `$REPO` resolved to `/home/bchanot-ubuntu/.claude` (symlink dir) instead of real repo path. `$DISABLED_DIR` then pointed at near-empty `~/.claude/skills-disabled/` (2 stale npx symlinks only). Fixed by adding `-P` to `cd` (commit `a4558ee`). `cmd_current` now correctly reports `full (100% match, 14 gstack skills disabled)`. - BLK-006 capitalized — `cmd_current` false-negative when invoked via `~/.claude/lib/profile.sh` symlink; status: resolved. - LRN-023 capitalized — `$REPO="$(cd -P "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"` mandatory pattern for any script meant to be invoked via a symlink into the install location. + +## 2026-06-02 + +- Added `profile gstack on|off` verb to `lib/profile.sh`. `on` = re-enable all parked gstack keeping `.active-profile` label intact (vs `reset` which clears to "none"); `off` = disable gstack not in active profile (errors if none). User wanted centralized toggle without losing profile context. +- Extracted 3 helpers (`enable_all_gstack`/`disable_gstack_not_in`/`parked_gstack_count`); refactored `cmd_reset`+`cmd_set` to reuse — behavior preserved, 6-case test + exact state-restore assertion PASS, shellcheck CLEAN. Doc: SKILL.md argument-hint + examples + output-policy. Makefile generic `make profile cmd="gstack on"` already covers it. +- Corrected own false flag: full.profile omitting ios-*/spec is curation by design (BDR-017 caveat), NOT a bug — caught before any edit. Surfaced real gap: 6 gstack source skills unlinked post-submodule-bump → BLK-007 (open, gstack ./setup domain, not auto-fixed). +- Backfilled index drift: decisions (BDR-017), blockers (BLK-005/006). +- BDR-018 + LRN-024 + BLK-007 + EVAL-002 capitalized. diff --git a/.claude/memory/learnings.md b/.claude/memory/learnings.md index 1ece219..879ae6a 100644 --- a/.claude/memory/learnings.md +++ b/.claude/memory/learnings.md @@ -40,6 +40,7 @@ rules: | LRN-017 | 2026-05-12 | Thin-dispatcher SKILL.md round-1 win = fallback + frontmatter triggers (+15 to +30) | any `/darwin-skill` round-1 on a dispatcher SKILL.md | | LRN-018 | 2026-05-12 | Darwin eval subagents drift on total math — recompute in main thread | any subagent-driven SKILL.md rescore | | LRN-019 | 2026-05-15 | Deployable-project doc split: README dev-quickstart + DEPLOY 14-section prod-VPS topology | any onboard/doc-syncer/scaffold producing docs for a deployable project | +| LRN-024 | 2026-06-02 | New sibling command sharing logic → extract helper + refactor existing caller, never copy-paste; assert pre/post state equality | adding a subcommand/branch reusing logic inline in a peer command | --- @@ -359,3 +360,14 @@ rules: - Lint via: `grep -n 'cd "$(dirname "${BASH_SOURCE'