Compare commits
6 Commits
4da2b905de
...
ce4391a62f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce4391a62f | ||
|
|
52d4a66daa | ||
|
|
a131f45f00 | ||
|
|
0f0bd7fe63 | ||
|
|
a335591c46 | ||
|
|
09f5b28d99 |
@ -28,7 +28,7 @@ rules:
|
||||
| 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` | resolved |
|
||||
| BLK-010 | 2026-06-27 | init-project: scaffold (STEP 5) + bootstrap README (5b) have no deterministic commit owner; worktree `add -b` on unborn HEAD | resolved (uncommitted) |
|
||||
| BLK-011 | 2026-06-27 | init-project STEP 13 GSD post-FINISH creates ROADMAP.md → stranded doc (3rd post-FINISH artifact) | open |
|
||||
| BLK-011 | 2026-06-27 | init-project STEP 13 GSD post-FINISH creates ROADMAP.md → stranded doc (3rd post-FINISH artifact) | resolved (STEP 12 removed) |
|
||||
| BLK-012 | 2026-06-29 | gitflow_init half-applied: socle-commit failure swallowed → hook activated on partial run → re-run self-blocks | resolved |
|
||||
|
||||
---
|
||||
@ -139,8 +139,9 @@ rules:
|
||||
- **Date**: 2026-06-27
|
||||
- **Friction**: init-project STEP 13 (GSD v2 init) runs post-FINISH (STEP 11). `gsd init` creates `.gsd/` + `ROADMAP.md` (a public doc). Created AFTER FINISH integrates → ROADMAP never in the merge/PR. Same PR-stranding class as the doc-sync twin, 3rd post-FINISH artifact.
|
||||
- **Real cause**: artifact-producing step ordered after FINISH (= BDR-034 class). `gsd init` is a CLI mechanism distinct from doc-syncer; ROADMAP is sync-only for doc-syncer (never created by it, BDR-022 rules), so the doc-sync coupled chantier does not touch it.
|
||||
- **Solution**: open — separate thread. Candidate: reorder GSD before FINISH, or commit ROADMAP after `gsd init`. Out of scope for doc-sync coupled (different mechanism).
|
||||
- **Status**: open
|
||||
- **Solution**: open — separate thread. Candidate: reorder GSD before FINISH, or commit ROADMAP after `gsd init`. Out of scope for doc-sync coupled (different mechanism). [historical candidates — NOT the route taken]
|
||||
- **Resolution**: RESOLVED 2026-06-29 — by REMOVAL, not by committing the orphan. init-project STEP 12 (speculative gsd auto-bootstrap) DELETED → ROADMAP/.gsd never created post-FINISH → orphan dissolves, no commit helper built. TRUE reason: auto-bootstrapping a heavy multi-session ENGINE the sole user doesn't use, AT project-creation, is bad on its own terms. NOT the initial framing "ROADMAP redundant with TODO" — that was wrong and would have aged badly: gsd ≫ roadmap (state machine / crash-recovery / cost / parallel / worktree), and TODO ≠ gsd ROADMAP (different altitude + consumer). Reasoning trace: BOTH initial premises (gsd=only-roadmap; TODO-redundant) REFUTED on read, yet conclusion A (remove STEP 12) held for the STRONGER reason — right answer, reason corrected before engraving. Deliberate gsd use KEPT (onboarder PHASE 6 `/onboard add gsd`, plugin-advisor reco, status-reporter `.gsd/` read, USAGE `gsd init`). Removed STEP 12 + header 12→11-step + 10c note + 4 USAGE refs; coherence sweep = zero dangling refs. [[LRN-072]]
|
||||
- **Status**: resolved (init-project STEP 12 removed — `skills/init-project/SKILL.md`; branch bugfix/blk-011-gsd-roadmap). Title says "STEP 13" — stale (was STEP 12 at removal per BDR-036 renumber); left per append-only.
|
||||
- **Reference**: discovered in doc-sync-coupled analysis (2026-06-27). Sibling [[BLK-010]] + twin [[BDR-034]].
|
||||
|
||||
## BLK-012 — gitflow_init non-transactional: socle-commit failure swallowed → hook activated on partial run → re-run self-blocks
|
||||
|
||||
@ -50,6 +50,7 @@ rules:
|
||||
| BDR-037 | 2026-06-27 | v2 capitalize Stop-hook rejected → wire /capitalize+/close to the include | accepted |
|
||||
| BDR-038 | 2026-06-27 | deploy skill: per-project learning runbook, two-moment cold-resume | accepted |
|
||||
| BDR-039 | 2026-06-29 | Gitea branch protection = Option-1 owner-pushable, not require-PR | accepted |
|
||||
| BDR-040 | 2026-06-29 | doc-syncer MINOR-shape oracle: deterministic floor under LLM's MINOR call | accepted |
|
||||
|
||||
---
|
||||
|
||||
@ -613,3 +614,13 @@ rules:
|
||||
- **why**: gitflow integrates by **local directed merges** — `gitflow finish` runs `git merge --no-ff` on the owner's machine then pushes the merge commit. require-PR would REJECT those pushes: every feature/bugfix/release merge would need a manual PR, and the **hotfix fan-out** (hotfix → main + develop + each open `release/*`) becomes 3+ manual PRs per hotfix. For a solo-owner Gitea, required reviews add zero review value, only friction. Owner-pushable keeps the protection's real teeth (no force-push, no deletion, no non-owner push) without breaking the local-merge workflow. Protection is a BACKSTOP — the per-repo pre-commit hook + the "finish only on an explicit human signal" rule are the primary controls.
|
||||
- **alternatives**: require-PR + required reviews (rejected — breaks `gitflow finish`'s local merges; the 3-way hotfix fan-out becomes manual PRs; no review value for a solo owner, pure friction); no protection (rejected — leaves force-push + branch deletion + accidental non-owner push open; it is the deterministic backstop the advisory rules can't guarantee); protect `main` only (rejected — `develop` is equally a protected base in the model, needs the same force-push/deletion guard).
|
||||
- **reference**: `lib/gitflow-migrate.sh` `_protect()` (POST `/repos/{o}/{r}/branch_protections`, owner whitelist); applied to all 6 repos 2026-06-29 (journal). Hook backstop in `lib/gitflow.sh` (pre-commit); CLAUDE.md "Version control — gitflow (universal)". Pairs with [[LRN-069]] (the `git push` ASK gate at the tool-call layer).
|
||||
|
||||
## BDR-040 — doc-syncer MINOR-shape oracle: deterministic floor under LLM's MINOR call
|
||||
- **date**: 2026-06-29
|
||||
- **status**: accepted
|
||||
- **Problem**: doc-syncer AUTO MODE classifies drift NONE/MINOR/SIGNIFICANT by LLM judgment, no deterministic backstop. SIGNIFICANT mislabeled MINOR → silent auto-patch + auto-commit, skips the SIGNIFICANT gate (RISK-1). Follow-up [[BDR-036]] flagged.
|
||||
- **Decision**: `lib/doc-shape.sh` re-checks SHAPE of each MINOR patch BEFORE the silent auto-commit. Envelope (per path, `git diff HEAD`): adds ATX heading | added > DOC_SHAPE_MAX_ADDED (def 20) | removed > MAX_REMOVED (def 20) | new/untracked file | non-doc → EXCEEDS. Aggregate: ANY path exceeds → whole set escalates to the EXISTING SIGNIFICANT gate (STEP A4 `Apply? yes/no/select`; no=revert all, select=keep subset). Thresholds env-overridable.
|
||||
- **Oracle NOT a blocking gate (B rejected)**: [[BDR-036]] graved MINOR-non-gated as CONSCIOUS (visible surface replaces gate; blocking gate = friction disproportionate). Oracle does NOT gate genuine MINOR (auto-commit untouched, zero friction) — only re-routes shape-suspect patches. Tightens the DEFINITION of MINOR deterministically ([[LRN-046]] oracle > judge), adds no gate. Option B (human gate on every MINOR) REJECTED — contradicts [[BDR-036]], rejects the premise the reading refuted.
|
||||
- **ENGRAVED LIMIT — do not over-read the guarantee**: oracle catches STRUCTURAL/size significance, NOT semantic. A 3-line edit that CHANGES MEANING, no heading, small → still reads MINOR (rc 0) and auto-commits. Deterministic FLOOR under LLM judgment = REDUCTION of RISK-1's gross cases, NOT elimination. LLM owns the semantic call above the floor; the visible surface ([[BDR-036]]) stays the content backstop.
|
||||
- **Scope tranché**: ① oracle + ② [[LRN-071]] masked-commit fix built. ③ branch-guard (doc-commit refusing main/develop) DEFERRED — duplicates the protected-base predicate a 3rd time (lib + gitflow hook + here); migrated repos have the hook → ③ guards a state that shouldn't exist. Reconsider only for repos outside `gitflow init`.
|
||||
- **Build**: TDD RED→GREEN. run-doc-shape.sh 19/19 (incl. threshold boundary + env-override) + behavioral Scenario D. Wired doc-syncer STEP A4 + doc-commit.md ACKNOWLEDGMENTS coherence. shellcheck clean.
|
||||
|
||||
@ -223,3 +223,15 @@ rules:
|
||||
- Migrated ALL 6 repos to gitflow one-by-one (faunosteo, config, bchanot-cv, zenquality, game, claude): master→main, develop, Option-1 owner-pushable protection, master deleted — each delete behind a user eyeball + GO, ZERO loss, no force/`--no-verify`, settings intact. game = already-on-main variant (no master); zenquality keeps `cleanup/post-smtp-fix` (out-of-convention, conscious); bchanot-cv adopted a pre-existing clone (surfaced, not assumed).
|
||||
- claude SELF-APPLIED (ultimate dogfood): its own committed lib migrated it. Chantier landed C1 `feat(gitflow)` 167ea96 + C2 `chore(memory)` 1254643 + socle 620071b; hook now governs claude. gstack submodule dirty (BLK-008 Playwright bump) excluded via `submodule.ignore=dirty` (LRN-070), not reset.
|
||||
- Permission insight: `Bash(export *)` deny false-positives inline-env; `git push` ASK = the real remote-write gate (LRN-069). BLK-010 CLOSED (verified `gitflow_init` root commit closes all 3 components — index+body, append-only).
|
||||
|
||||
## 2026-06-29 (cont.) — MINOR-gate strengthening (doc-syncer)
|
||||
- Read-first cartography REFUTED the literal premise: "strengthen MINOR gate" = 3 distinct problems; the literal reading (blocking gate on MINOR, option B) contradicts engraved [[BDR-036]]. Same trap as gitflow — premise refuted by the real, not assumed.
|
||||
- Scope tranché ①+②, ② first, never B, ③ deferred. Built test-first (Iron Law RED→GREEN, RED shown before each GREEN).
|
||||
- ② masked-commit fix ([[LRN-071]]) — 3rd occurrence of the swallowed-commit pattern ([[LRN-066]], [[LRN-068]]/[[BLK-012]]). `doc-commit.sh` exit 5 fail-loud. RED T8 proved the masking (rc 0 + stale hash + false "committed"), GREEN 32/32.
|
||||
- ① MINOR-shape oracle ([[BDR-040]], `lib/doc-shape.sh`) — 19/19 + behavioral Scenario D. Engraved limit: structural floor, NOT semantic (reduction of RISK-1's gross cases, not elimination).
|
||||
- Branch `feature/minor-gate-strengthening`; committed code + memory; FINISHED → develop (`0f0bd7f`) on explicit signal. Held the merge until the explicit go — the "avis-en-question" wasn't it.
|
||||
|
||||
## 2026-06-29 (cont. 2) — BLK-011 resolved by REMOVAL (init-project GSD bootstrap)
|
||||
- User challenge reframed the chantier: don't plumb a commit for the stranded ROADMAP — ask if gsd belongs at init AT ALL. Read REFUTED both my option-premises (gsd ≫ roadmap; TODO ≠ gsd ROADMAP) but conclusion A (remove STEP 12) held for a STRONGER reason: speculative auto-bootstrap of an unused multi-session engine at creation is bad per se. Best fix = NEGATIVE diff ([[LRN-072]]).
|
||||
- Removed init-project STEP 12 (+ header 12→11-step, 10c note, 4 USAGE coherence fixes). Coherence sweep = zero dangling STEP-12 refs (the "test" for a removal). Deliberate gsd use KEPT (onboarder PHASE 6, plugin-advisor, status-reporter). [[BLK-011]] → resolved.
|
||||
- Branch `bugfix/blk-011-gsd-roadmap`; no finish yet (awaiting signal).
|
||||
|
||||
@ -69,6 +69,8 @@ rules:
|
||||
| LRN-068 | 2026-06-29 | enforcement-bootstrap must be transactional: activate the guard LAST + gate it on the bootstrap commit succeeding; precheck identity | any init that installs a hook/protection AND commits |
|
||||
| LRN-069 | 2026-06-29 | token-authed remote writes under CC perms: inline-env (never `export`), token in header not argv, keep `git push` on ASK as the gate | scripting git/curl writes to a private remote from tool calls |
|
||||
| LRN-070 | 2026-06-29 | clean-tree-gated migration blocked by a dirty submodule → diagnose pointer-vs-content; for a local edit use `submodule.<name>.ignore=dirty`, never blind reset | migrating/releasing a superproject whose submodule carries intentional local edits |
|
||||
| LRN-071 | 2026-06-29 | fail-loud must cover the helper's OWN commit, not just its inputs — 3rd occurrence of the swallowed-commit pattern (a failed op masked by a later returning-0 statement) | any helper whose return value gates a downstream "success" — audit every fallible internal op propagates, esp. the commit |
|
||||
| LRN-072 | 2026-06-29 | a stranded-artifact bug can be fixed by NOT creating the artifact (negative diff), not by plumbing its commit — if the producing step is speculative/unused, delete it | a stranded/duplicated/uncommitted-artifact bug — before building machinery, ask if the PRODUCING step is wanted; speculative-at-creation → remove, deliberate-on-demand → keep |
|
||||
|
||||
---
|
||||
|
||||
@ -794,3 +796,14 @@ rules:
|
||||
- **pattern**: an op gated on a clean tree (`git status --porcelain`) is blocked by a submodule showing ` M`. FIRST distinguish: (a) **pointer move** — gitlink (HEAD) ≠ submodule HEAD → resettable via `git submodule update`/`checkout`; (b) **dirty content** — gitlink UNCHANGED, files modified INSIDE the submodule → a local edit. For an intentional local edit, `checkout --`/`submodule update` correctly REFUSE to discard it, and a blind "reset" would DESTROY it. Exclude it non-destructively: `git config submodule.<name>.ignore dirty` (local `.git/config`) → status stops reporting the submodule's dirty content, gate passes, edit preserved. Commit it to `.gitmodules` to share the ignore across clones.
|
||||
- **context**: claude gitflow self-migration. `skills-external/gstack` showed ` M`; gitlink `070722a` == submodule HEAD `070722a` (NOT a pointer move), 2 tracked-modified files (`bun.lock`+`package.json`) = the [[BLK-008]] Playwright 1.61 bump (Ubuntu 26.04 browser). The planned "reset" (D2) would have discarded the browser fix; `submodule.skills-external/gstack.ignore=dirty` cleared the tree for `migrate_local`, bump intact.
|
||||
- **future application**: any clean-tree-gated op (migrate/release/bisect) on a superproject with a submodule carrying intentional local edits → diagnose pointer-vs-content FIRST (compare gitlink to submodule HEAD); for content, `submodule.<name>.ignore=dirty`, never a blind reset. Cross-ref [[BLK-008]] (gstack -dirty by design).
|
||||
|
||||
## LRN-071 — fail-loud must cover the helper's OWN commit, not just its inputs — 3rd occurrence of the swallowed-commit pattern
|
||||
- **pattern**: a surgical-commit helper guarded LOUD on its INPUTS (scope) but SILENT on its OWN `git commit`. `doc-commit.sh`: `set -uo pipefail` (no `-e`) + unguarded `git commit` → on rejection (pre-commit hook on a protected branch / signing / etc.) execution CONTINUES: `printf "committed"` lies, `git rev-parse --short HEAD` emits the PREVIOUS HEAD hash, function exits 0. Orchestrator reads rc 0 + non-empty hash → believes success; docs silently uncommitted, tree dirty (RISK-2).
|
||||
- **RECURRENT (3×) — audit systematically, not an isolated bug**: same fail-silent-where-it-must-fail-loud class in the surgical-commit family — [[LRN-066]] (`deploy-commit.sh`: porcelain hides a git-ignored path → silent no-op; fix = loud rc 5) + [[LRN-068]]/[[BLK-012]] (`gitflow_init`: socle-commit failure swallowed by `||` then `git branch` returned 0 → init continued past the dead commit) + this. The common mechanism: a fallible op (esp. a commit) whose failure isn't propagated, MASKED by a later returning-0 statement. The motif RETURNS; treat it as a known smell.
|
||||
- **fix**: guard the commit — `if ! git commit …; then LOUD + return 5; fi`. rc 5 = "tried, git refused" (distinct from rc 3 = "could not start"). Empty stdout (no stale hash), loud stderr. Proven by T8: RED showed the masking (rc 0 + stale hash + false "committed"), GREEN rc 5 + empty + REJECTED, 32/32.
|
||||
- **future application**: any helper whose RETURN VALUE gates a downstream "success" — audit that EVERY fallible internal op propagates its failure, ESPECIALLY the load-bearing commit. `set -uo pipefail` without `-e` does NOT abort mid-function; an unchecked failing command followed by a returning-0 line exits 0 and lies. Check `cmd || other` forms, no-`-e` blocks, every "report success after the op" line. Test the partial-failure path (commit-blocked repo) → must fail loud, empty, non-zero.
|
||||
|
||||
## LRN-072 — a stranded-artifact bug can be fixed by NOT creating the artifact (negative diff), not by plumbing its commit
|
||||
- **pattern**: 3rd member of the post-FINISH-artifact class (memory, docs, GSD ROADMAP) — but UNLIKE the first two (real artifacts ALWAYS produced → couple a commit), the GSD artifact came from a SPECULATIVE, opt-in, rarely-used producer (init-project auto-bootstrapping a multi-session engine at project creation). The reflex fix (reorder + build `gsd-commit.sh` + tests) would have added machinery to faithfully commit an artifact nobody uses. The right fix was a NEGATIVE diff: delete the producer → orphan never created → bug dissolves, zero new code (BLK-011).
|
||||
- **the refutation that got there**: the framing "ROADMAP redundant with TODO" was WRONG (gsd ≫ roadmap = state machine/crash-recovery/cost/parallel/worktree; TODO ≠ gsd ROADMAP = different altitude + consumer). Reading REFUTED both premises, yet the CONCLUSION (remove the step) held for a STRONGER reason: speculatively scaffolding a heavy engine the sole user doesn't use, at creation, is bad per se. Right answer, reason corrected before engraving — change the QUESTION before changing the code.
|
||||
- **future application**: a stranded / duplicated / uncommitted-artifact bug → BEFORE building machinery to handle the artifact, ask whether the step that PRODUCES it is actually used / wanted / non-speculative. Speculative or unused (esp. a personal/single-user repo) → DELETE the producer; the cleanest fix is the absent one. Distinguish speculative-at-creation (REMOVE) from deliberate-on-demand (KEEP). Family: [[BLK-010]], [[BLK-011]], [[BDR-036]].
|
||||
|
||||
@ -262,8 +262,8 @@ reorder + CREATE doc-commit.sh/.md (mirror memory-commit, 4 deltas). Surface-don
|
||||
- [x] Task 6b — wire doc-commit into feat/bugfix/hotfix DOC SYNC — 1b01b95. commit-change exempt (no DOC SYNC); hotfix wired (include no-ops on empty).
|
||||
- [x] Task 7 — close: `run-doc-behavioral.md` + shellcheck clean + 28/28 + CHANGELOG + BDR-036 / LRN-058-060 / EVAL-008. surface-replaces-gate + partial-init + scope-expansion engraved honestly.
|
||||
- [x] RESOLVED 2026-06-29 — [[BLK-010]] closed by `gitflow_init` root commit (init-project STEP 5f): scaffold/README get a deterministic commit owner + HEAD born before the worktree step. Verified (mechanism + STEP 5f wiring + T2 test); blockers.md index+body updated.
|
||||
- [ ] flagged separate — [[BLK-011]] GSD ROADMAP.md post-FINISH (now STEP 12 after Task 5 renumber; BLK-011 record itself left at STEP 13 — append-only)
|
||||
- [ ] flagged separate — strengthen doc-sync MINOR gate (own doc-syncer chantier)
|
||||
- [x] RESOLVED 2026-06-29 — [[BLK-011]] closed by REMOVAL: init-project STEP 12 (speculative gsd auto-bootstrap) deleted → orphan never created. Negative diff, not commit-plumbing ([[LRN-072]]). See chantier below.
|
||||
- [x] DONE 2026-06-29 — doc-sync MINOR gate strengthened: ① shape-oracle [[BDR-040]] + ② masked-commit fix [[LRN-071]] (③ branch-guard deferred). See chantier below.
|
||||
|
||||
## 2026-06-29 — gitflow universal model + 6-repo migration (DONE)
|
||||
Goal: universal gitflow across all `bchanot/*` Gitea repos. Lib built across prior sessions; migrated + hardened + dogfooded this session.
|
||||
@ -276,3 +276,24 @@ Goal: universal gitflow across all `bchanot/*` Gitea repos. Lib built across pri
|
||||
- [x] Dogfood PROVEN: hook whitelists `.claude/**` on main + Option-1 lets owner push (commit `1620e5b`)
|
||||
- [x] Capitalize: BDR-039 (Option-1 protection), LRN-068/069/070, BLK-010 closed + BLK-012, journal 2026-06-29 — committed + pushed on main
|
||||
- [ ] follow-up (optional) — `submodule.gstack.ignore=dirty` into committed `.gitmodules` (share across clones); zenquality `cleanup/post-smtp-fix` rename `<type>/<name>` or finish+delete
|
||||
|
||||
## 2026-06-29 — MINOR-gate strengthening (doc-syncer) [branch feature/minor-gate-strengthening]
|
||||
Read-first cartography refuted the literal premise: "strengthen MINOR gate" = 3 problems;
|
||||
the literal one (blocking gate on MINOR) contradicts engraved [[BDR-036]]. Scope: ①+②, not B,
|
||||
③ deferred. Built test-first (Iron Law).
|
||||
- [x] ② fix masked commit failure — `doc-commit.sh` exit 5 fail-loud ([[LRN-071]], 3rd occurrence of the swallowed-commit pattern). RED T8 proved masking, GREEN 32/32 + taxonomy (sh header/funcdoc + `doc-commit.md` rc-5 row)
|
||||
- [x] ① MINOR-shape oracle — `lib/doc-shape.sh` ([[BDR-040]]) + `run-doc-shape.sh` 19/19 (boundary + env-override). Wired doc-syncer STEP A4 (escalate whole set → existing SIGNIFICANT gate; no=revert all, select=keep subset) + `doc-commit.md` ACKNOWLEDGMENTS coherence + behavioral Scenario C/D
|
||||
- [x] shellcheck clean (doc-commit.sh, doc-shape.sh, both test harnesses); coherence ref-sweep clean
|
||||
- [x] Capitalize — BDR-040 + LRN-071 + CHANGELOG (Added/Fixed) + journal 2026-06-29 (cont.)
|
||||
- [x] FINISH — merged feature/minor-gate-strengthening → develop (`0f0bd7f`) on explicit signal
|
||||
- [~] ③ branch-guard in doc-commit DEFERRED — duplicates protected-base predicate 3rd time (lib + hook + here); all migrated repos have the hook. Reconsider only for repos outside `gitflow init`
|
||||
|
||||
## 2026-06-29 — BLK-011 GSD ROADMAP post-FINISH [branch bugfix/blk-011-gsd-roadmap]
|
||||
User reframed: don't plumb a commit for the stranded ROADMAP — ask if gsd belongs at init at all.
|
||||
Read refuted both option-premises (gsd ≫ roadmap; TODO ≠ gsd ROADMAP) but conclusion A held for a
|
||||
stronger reason: speculative auto-bootstrap of an unused engine at creation is bad per se ([[LRN-072]]).
|
||||
- [x] Resolve by REMOVAL — deleted init-project STEP 12 (negative diff −26/+13), not a commit helper
|
||||
- [x] Ref-coherence sweep ("test" for a removal) — header 12→11-step, 10c note, 4 USAGE refs; zero dangling STEP-12 refs repo-wide
|
||||
- [x] Scope guardrails — deliberate gsd use KEPT (onboarder PHASE 6, plugin-advisor, status-reporter)
|
||||
- [x] Capitalize — [[BLK-011]] resolved (true reason + premise trace) + [[LRN-072]] + CHANGELOG Removed + journal 2026-06-29 (cont. 2)
|
||||
- [ ] FINISH — merge bugfix/blk-011-gsd-roadmap → develop (awaiting explicit human signal)
|
||||
|
||||
@ -11,6 +11,7 @@ Format follows [Keep a Changelog](https://keepachangelog.com/).
|
||||
### Added
|
||||
- Coupled-capitalize: dev flows (feat / hotfix / bugfix / commit-change, ship-feature, init-project) auto-commit their memory in the same breath, via shared `lib/capitalize-commit.md` + `lib/memory-commit.sh` (surgical — `.claude/memory` + `.claude/tasks` only, never `git add -A`)
|
||||
- Coupled doc-sync: dev flows (feat / bugfix / hotfix, ship-feature, init-project) auto-commit the public docs `doc-syncer` patches, via shared `lib/doc-commit.md` + `lib/doc-commit.sh` (surgical — only the patched files, never `git add -A`, never `.claude/` / `CLAUDE.md`; refuses an out-of-scope path loudly with exit 4). `doc-syncer` surfaces `PATCHED_FILES` (one path per line) as the handoff
|
||||
- `lib/doc-shape.sh` — deterministic MINOR-shape oracle for `doc-syncer` AUTO MODE: re-checks each LLM-classified MINOR patch (added-heading / size / new-file / non-doc envelope, thresholds env-overridable) and escalates a shape-suspect patch to the existing SIGNIFICANT gate instead of silently auto-committing it. A structural floor under the LLM's classification, not a blocking gate (genuine MINOR still auto-commits, zero friction); catches structural/size significance, not semantic
|
||||
- `/audit-delta` — recurring multi-axis audit (norms / bugs / dead code / security) scoped to changes since last run, with per-axis SHA markers
|
||||
- `/capitalize` — flush uncapitalized context to the memory registries before `/clear` or `/compact`
|
||||
- `/prune-memory` — curate and compress the `.claude/memory/` registries
|
||||
@ -37,10 +38,12 @@ Format follows [Keep a Changelog](https://keepachangelog.com/).
|
||||
- Default model / effort settings updated
|
||||
|
||||
### Removed
|
||||
- `/init-project`: STEP 12 (speculative GSD v2 auto-bootstrap at project creation) removed — it ran `gsd init` AFTER FINISH, creating `ROADMAP.md` + `.gsd/` stranded outside the merge/PR (BLK-011), to bootstrap a multi-session engine that is opt-in and rarely used. Resolved by removal, not by plumbing a commit: GSD stays initializable on-demand (`/onboard add gsd`, or `gsd init` in a terminal), `/status` still reads `.gsd/`, and plugin-advisor still recommends it for multi-session work. init-project is now an 11-step pipeline
|
||||
- `disable-model-invocation` frontmatter removed repo-wide (aligns skills with CLAUDE.md routing)
|
||||
- Caveman plugin always-on integration purged — plugin disabled + uninstalled; SessionStart/UserPromptSubmit hooks, standalone hook files, `install-plugins.sh` STEP 5.5, `update-all.sh` refresh step, `plugins.lock.json` entry, `doctor.sh` checks, and docs removed. On a subscription plan its ~75% output-token compression has no cost benefit, and the always-on hooks added friction on validation gates + client deliverables. The unrelated memory-registry terse-format convention is kept.
|
||||
|
||||
### Fixed
|
||||
- `lib/doc-commit.sh` no longer masks a rejected `git commit` as success: a pre-commit hook / protected branch / signing failure now fails loud with exit 5 and empty stdout (was: false "committed" + the previous HEAD's hash + exit 0, leaving docs silently uncommitted on a dirty tree)
|
||||
- Numerous skill/agent fixes across darwin optimization rounds (geo-analyzer, onboard, init-project, analyzer, plugin-check, prune-memory, …)
|
||||
|
||||
## [3.4.0] — 2026-04-15
|
||||
|
||||
20
USAGE.md
20
USAGE.md
@ -195,7 +195,6 @@ Hotfix/quick fix → tout OFF sauf superpowers
|
||||
# → STEP 8-10 : implémentation TDD + review
|
||||
# → STEP 10b-c: capitalize mémoire + sync README (avant finish)
|
||||
# → STEP 11 : finish (merge / commit initial)
|
||||
# → STEP 12 : propose GSD v2 si multi-session détecté
|
||||
|
||||
# 3. Features suivantes
|
||||
/ship-feature "description de la feature"
|
||||
@ -204,16 +203,16 @@ Hotfix/quick fix → tout OFF sauf superpowers
|
||||
### Pattern B — Projet long (multi-session, plusieurs jours) · ~1500-2500t/session CC
|
||||
|
||||
```
|
||||
# Même départ que Pattern A, mais au STEP 12 :
|
||||
# → Répondre "yes" à "Initialize GSD v2?"
|
||||
# → ROADMAP.md est créé avec les milestones
|
||||
# Même départ que Pattern A. GSD v2 n'est PAS bootstrappé à la création
|
||||
# (init-project ne propose plus gsd) — on l'initialise À LA DEMANDE, dans un
|
||||
# terminal, quand on décide de passer en multi-session.
|
||||
|
||||
# À chaque reprise de session (dans Claude Code) :
|
||||
/status # snapshot : plugins + git + milestone GSD en cours
|
||||
|
||||
# Ensuite dans un terminal (depuis le dossier projet) :
|
||||
# Dans un terminal (depuis le dossier projet) :
|
||||
gsd # démarrer une session
|
||||
/gsd init # si pas encore fait
|
||||
/gsd init # initialise .gsd/ + ROADMAP (une fois, à la demande)
|
||||
/gsd auto # mode autonome, walk away
|
||||
|
||||
# Pour suivre :
|
||||
@ -400,9 +399,9 @@ Verify : npx expo export --platform web --output-dir /tmp/expo-check --clear
|
||||
|
||||
**Si le projet devient long (plusieurs features sur semaines) :**
|
||||
```
|
||||
# STEP 12 propose GSD v2 : répondre "yes"
|
||||
# Puis dans terminal :
|
||||
# Pour passer en multi-session, initialise GSD à la demande dans un terminal :
|
||||
gsd
|
||||
/gsd init
|
||||
/gsd auto
|
||||
# → GSD gère deck-building, sharing backend, etc. milestone par milestone
|
||||
```
|
||||
@ -501,8 +500,8 @@ Convention: snake_case Python, camelCase TypeScript."
|
||||
|
||||
**Workflow long avec GSD v2 :**
|
||||
```
|
||||
# Après /init-project (STEP 12 → "yes")
|
||||
# Le ROADMAP.md généré contient :
|
||||
# Après /init-project, on initialise GSD à la demande (plus auto-bootstrappé).
|
||||
# Le ROADMAP.md généré par `gsd init` contiendra :
|
||||
# Milestone 1: Boutique in-app + Stripe
|
||||
# Milestone 2: PvP + matchmaking
|
||||
# Milestone 3: Leaderboard + saisons
|
||||
@ -510,6 +509,7 @@ Convention: snake_case Python, camelCase TypeScript."
|
||||
# Dans un terminal :
|
||||
cd cardforge/
|
||||
gsd # démarre session GSD
|
||||
/gsd init # crée .gsd/ + ROADMAP (à la demande — plus auto à l'init)
|
||||
/gsd auto # GSD travaille sur Milestone 1 de façon autonome
|
||||
# → research Stripe API + docs
|
||||
# → plan décomposé en tâches
|
||||
|
||||
@ -792,9 +792,26 @@ Categorize:
|
||||
|
||||
- **NONE** → exit completely silent. No output (no `PATCHED_FILES` → the doc-commit step
|
||||
sees an empty list and no-ops).
|
||||
- **MINOR** → patch silently. One-line confirmation per file:
|
||||
`doc-sync: patched <file> (<what changed>)`
|
||||
- **SIGNIFICANT** → surface to user before patching:
|
||||
- **MINOR** → patch, then VERIFY SHAPE with the deterministic oracle BEFORE the
|
||||
silent auto-commit. The LLM made the MINOR call; the oracle re-checks that the
|
||||
patch's SHAPE actually holds, catching a SIGNIFICANT mislabeled MINOR (RISK-1):
|
||||
```
|
||||
bash "$HOME/.claude/lib/doc-shape.sh" check <every patched path> # all paths, ONE call
|
||||
```
|
||||
- **exit 0** (within the MINOR envelope) → genuine MINOR: keep the silent patch.
|
||||
One-line confirmation per file: `doc-sync: patched <file> (<what changed>)`.
|
||||
Proceed to `PATCHED_FILES` + the doc-commit step.
|
||||
- **exit 1** (shape EXCEEDS — oracle stderr names the offender(s) and why) → the
|
||||
deterministic oracle OVERRULES the LLM's MINOR call (LRN-046). Do NOT auto-commit.
|
||||
ESCALATE the WHOLE patch set to the SIGNIFICANT gate below — one file out of
|
||||
shape makes the atomic MINOR classification suspect. Surface every patched file
|
||||
+ the oracle's reason, then the gate: on `no` → revert ALL
|
||||
(`git checkout -- <each patched path>`); on `select` → keep the chosen files,
|
||||
revert the rest. The oracle catches STRUCTURAL/size significance, not semantic —
|
||||
it is a deterministic floor, not a full SIGNIFICANT-detector.
|
||||
- **exit 2/3** (oracle usage error / not a git repo) → do NOT auto-commit on a
|
||||
broken check; treat as exit 1 and escalate.
|
||||
- **SIGNIFICANT** (or a MINOR the oracle escalated) → surface to user before patching:
|
||||
```
|
||||
DOC SYNC — drift detected after this session:
|
||||
<list of significant items with proposed fixes>
|
||||
|
||||
@ -56,6 +56,7 @@ Pass each line as a SEPARATE argument (see DO step 3).
|
||||
| 0 | empty | helper no-op (nothing pending) | `DOC SYNC: docs already current — nothing to commit`. doc-sync found no drift, or patched nothing. |
|
||||
| 3 | empty | unsafe git state (detached / merge / rebase) | docs stay in the working tree for a manual commit; surface the helper's stderr. Do NOT retry blindly — the tree is mid-operation. |
|
||||
| 4 | empty | **SCOPE VIOLATION — upstream anomaly** | doc-syncer surfaced a `.claude/**` or `CLAUDE.md` path in `PATCHED_FILES`, which it must NEVER patch (BDR-022). STOP. Signal: `⚠️ doc-commit REFUSED — doc-syncer listed a forbidden path (<offender, from stderr>); this violates BDR-022 upstream. Investigate why doc-syncer touched/listed it before re-running.` Do NOT swallow it, do NOT hand-commit the rest — the refusal IS the alarm. |
|
||||
| 5 | empty | **COMMIT REJECTED — nothing committed** | `git commit` exited non-zero — a pre-commit hook blocked it (e.g. a doc commit on a protected `main`/`develop`), a signing failure, or similar. The docs are STILL in the working tree, uncommitted, on a dirty tree. STOP — do NOT proceed to FINISH as if docs landed (that re-creates the stranding bug this whole snippet fixes). Signal: `⚠️ doc-commit FAILED — the doc commit was rejected (<helper stderr>); docs remain uncommitted. Investigate (hook / branch / signing) before retrying.` No hash is emitted; do NOT retry blindly. |
|
||||
| 2 | empty | usage error (no message / bad invocation) | internal bug in this include — fix the call, don't paper over it. |
|
||||
|
||||
`<doc_hash>` is the DOC commit (the one that adds the patched docs). Docs carry NO
|
||||
@ -88,8 +89,12 @@ on the branch = consumption by the merge, automatic.
|
||||
snippet commits it without a blocking gate, BY CHOICE. NOT the memory case: memory CONTENT
|
||||
was always gated, so its auto-commit only embarked approved entries. Here the VISIBLE
|
||||
surface (rc 0 row, agent-composed summary) REPLACES the gate as the review surface — name
|
||||
files + summarize, and the PR diff re-shows it. Strengthening the MINOR gate itself =
|
||||
separate doc-syncer chantier.
|
||||
files + summarize, and the PR diff re-shows it. UPSTREAM of this snippet, doc-syncer now
|
||||
runs a deterministic shape oracle (`lib/doc-shape.sh`) on each MINOR patch: a patch whose
|
||||
SHAPE belies "minor" (adds a heading, is large, is a new file) is escalated to the
|
||||
SIGNIFICANT gate BEFORE it reaches here — so what this snippet auto-commits is shape-verified
|
||||
MINOR. The oracle is a STRUCTURAL floor, not a semantic SIGNIFICANT-detector (a small
|
||||
meaning-changing edit still reads MINOR); the visible surface stays the content backstop.
|
||||
- **Partial init-project fix.** This commits the docs doc-sync patched. It does NOT commit the
|
||||
scaffold or the STEP 5b bootstrap README (no deterministic owner — [[BLK-010]]); ramassing
|
||||
them would re-create the over-reach we ban. ship-feature ends fully fixed; init-project's
|
||||
|
||||
@ -16,7 +16,8 @@
|
||||
# doc-commit.sh pending <file>... # exit 0 if any passed file has changes, 1 if clean
|
||||
# doc-commit.sh commit "<message>" <file>... # surgical commit
|
||||
#
|
||||
# Exit codes (commit): 0 ok/no-op · 2 usage · 3 unsafe git state · 4 scope violation.
|
||||
# Exit codes (commit): 0 ok/no-op · 2 usage · 3 unsafe git state · 4 scope violation ·
|
||||
# 5 commit rejected (git commit exited non-zero — hook / protected branch / signing).
|
||||
# Output contract: diagnostics → stderr; on a real commit the short hash of the doc
|
||||
# commit is the ONLY thing on stdout (empty on no-op/abort), so callers can capture
|
||||
# it: doc_hash=$(doc-commit.sh commit "msg" README.md USAGE.md).
|
||||
@ -78,7 +79,8 @@ docs_pending() {
|
||||
}
|
||||
|
||||
# Surgical commit of the passed doc paths only. Returns 0 (ok/no-op), 3 (unsafe),
|
||||
# 4 (scope violation). On a real commit, prints the doc-commit short hash to stdout.
|
||||
# 4 (scope violation), 5 (commit rejected by git). On a real commit, prints the
|
||||
# doc-commit short hash to stdout.
|
||||
commit_docs() {
|
||||
local msg="${1:?commit message required}"
|
||||
shift
|
||||
@ -118,7 +120,22 @@ commit_docs() {
|
||||
echo "doc-commit: only ignored/no-op changes — no-op" >&2
|
||||
return 0
|
||||
fi
|
||||
git commit -q -m "$msg" -- "${changed[@]}"
|
||||
# FAIL-LOUD on the commit itself. With `set -uo pipefail` (no -e), a rejected
|
||||
# commit (pre-commit hook on a protected branch, signing failure, …) would NOT
|
||||
# abort: the printf below would falsely claim "committed" and rev-parse would
|
||||
# emit the PREVIOUS HEAD's hash with exit 0 — a silent masked failure. The
|
||||
# script is fail-closed+loud on scope (exit 4); it must be the same on its own
|
||||
# commit. Reject → loud, NO hash on stdout, exit 5 (distinct from rc 3 "could
|
||||
# not start": rc 5 = "tried, git refused").
|
||||
if ! git commit -q -m "$msg" -- "${changed[@]}"; then
|
||||
{
|
||||
echo "doc-commit: COMMIT REJECTED — git commit exited non-zero" \
|
||||
"(pre-commit hook? protected branch? signing?)."
|
||||
echo "doc-commit: NOTHING committed, working tree left as-is," \
|
||||
"NO hash emitted — investigate before retry."
|
||||
} >&2
|
||||
return 5
|
||||
fi
|
||||
printf 'doc-commit: committed %d file(s): %s\n' "${#changed[@]}" "${changed[*]}" >&2
|
||||
git rev-parse --short HEAD
|
||||
}
|
||||
|
||||
127
lib/doc-shape.sh
Executable file
127
lib/doc-shape.sh
Executable file
@ -0,0 +1,127 @@
|
||||
#!/usr/bin/env bash
|
||||
# doc-shape.sh — deterministic check that a doc patch has MINOR *shape*.
|
||||
#
|
||||
# Companion to doc-commit.sh. doc-syncer AUTO MODE classifies drift as NONE /
|
||||
# MINOR / SIGNIFICANT by LLM judgment, with no deterministic backstop — so a
|
||||
# SIGNIFICANT change mislabeled MINOR would auto-commit silently (RISK-1). This
|
||||
# oracle re-checks the SHAPE of each MINOR patch BEFORE the auto-commit: if a
|
||||
# patch's shape belies "minor" (adds a section heading, is large, or is a new
|
||||
# file), it EXCEEDS the MINOR envelope and doc-syncer escalates it to the
|
||||
# existing SIGNIFICANT gate instead of committing it silently.
|
||||
#
|
||||
# SCOPE OF THE GUARANTEE (honest, do not over-read it): this catches STRUCTURAL
|
||||
# and size significance, NOT semantic significance. A 3-line edit that changes
|
||||
# meaning but adds no heading and stays small still reads as MINOR-shape. The
|
||||
# oracle is a deterministic FLOOR under the LLM's judgment (LRN-046) — a
|
||||
# reduction of RISK-1's gross cases, not an elimination. The LLM still owns the
|
||||
# semantic call above this floor.
|
||||
#
|
||||
# Verdict is AGGREGATE: ANY passed path that exceeds → overall exit 1, every
|
||||
# offender named on stderr. The LLM classified the SET atomically MINOR; if one
|
||||
# file's shape disagrees, the whole set is suspect → the whole set escalates.
|
||||
#
|
||||
# Envelope (per path, working tree vs HEAD), all deterministic:
|
||||
# - adds a Markdown ATX heading (^+#{1,6} <text>) → exceeds (new section)
|
||||
# - added lines > DOC_SHAPE_MAX_ADDED (def 20) → exceeds (too big for a tweak)
|
||||
# - removed lines > DOC_SHAPE_MAX_REMOVED (def 20) → exceeds
|
||||
# - new / untracked file → exceeds (a creation, not a drift-patch)
|
||||
# - not a recognized public-doc file → exceeds (escalate the anomaly)
|
||||
# A clean tracked path (no diff) is vacuously within the envelope.
|
||||
# Known gap: Setext headings (=== / --- underlines) are not detected; ATX is the
|
||||
# norm in this codebase's docs.
|
||||
#
|
||||
# Usage: doc-shape.sh check <path>...
|
||||
# Exit: 0 within MINOR envelope · 1 exceeds (reasons→stderr) · 2 usage · 3 not-a-git-repo
|
||||
# Output: reasons → stderr; stdout stays empty (the exit code carries the verdict).
|
||||
#
|
||||
# Sourceable: doc_shape_ok for the doc-syncer flow.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
DOC_SHAPE_MAX_ADDED="${DOC_SHAPE_MAX_ADDED:-20}"
|
||||
DOC_SHAPE_MAX_REMOVED="${DOC_SHAPE_MAX_REMOVED:-20}"
|
||||
|
||||
_in_git_repo() { git rev-parse --git-dir >/dev/null 2>&1; }
|
||||
|
||||
# True (0) when the path is a recognized public-doc file (doc-syncer's universe,
|
||||
# BDR-016): the markdown family, anything under docs/, or a bare standard name.
|
||||
_is_doc() {
|
||||
case "$(basename -- "$1")" in
|
||||
*.md | *.mdx | *.markdown | *.rst) return 0 ;;
|
||||
README | INSTALL | CONFIGURE | USAGE | DEPLOY | CONTRIBUTING | \
|
||||
CHANGELOG | SECURITY | ARCHITECTURE | LICENSE | AUTHORS | NOTICE) return 0 ;;
|
||||
esac
|
||||
case "$1" in
|
||||
docs/* | */docs/*) return 0 ;;
|
||||
esac
|
||||
return 1
|
||||
}
|
||||
|
||||
# Echo the reason a single path EXCEEDS the MINOR envelope, or nothing if it is
|
||||
# within. Pure read — never mutates the tree.
|
||||
_path_exceeds_reason() {
|
||||
local p="$1"
|
||||
_is_doc "$p" || { printf 'not a recognized public-doc file: %s\n' "$p"; return; }
|
||||
[ -e "$p" ] || { printf 'path not found: %s\n' "$p"; return; }
|
||||
if ! git ls-files --error-unmatch -- "$p" >/dev/null 2>&1; then
|
||||
printf 'new/untracked doc (a creation, not a MINOR drift-patch): %s\n' "$p"
|
||||
return
|
||||
fi
|
||||
if git diff HEAD -- "$p" | grep -Eq '^\+#{1,6}[ \t]'; then
|
||||
printf 'adds a section heading (structural change, not a factual tweak): %s\n' "$p"
|
||||
return
|
||||
fi
|
||||
local stat added=0 removed=0
|
||||
stat="$(git diff HEAD --numstat -- "$p")"
|
||||
[ -n "$stat" ] && read -r added removed _ <<<"$stat"
|
||||
case "$added$removed" in *[!0-9]*) printf 'binary or unreadable diff: %s\n' "$p"; return ;; esac
|
||||
if [ "$added" -gt "$DOC_SHAPE_MAX_ADDED" ]; then
|
||||
printf 'added %s lines > %s envelope: %s\n' "$added" "$DOC_SHAPE_MAX_ADDED" "$p"
|
||||
return
|
||||
fi
|
||||
if [ "$removed" -gt "$DOC_SHAPE_MAX_REMOVED" ]; then
|
||||
printf 'removed %s lines > %s envelope: %s\n' "$removed" "$DOC_SHAPE_MAX_REMOVED" "$p"
|
||||
return
|
||||
fi
|
||||
}
|
||||
|
||||
# 0 if EVERY passed path is within the MINOR envelope, 1 if ANY exceeds (each
|
||||
# offender's reason printed to stderr). Empty list → 0 (vacuously minor).
|
||||
doc_shape_ok() {
|
||||
_in_git_repo || {
|
||||
echo "doc-shape: not a git repo — cannot judge shape" >&2
|
||||
return 3
|
||||
}
|
||||
local p reason any=0
|
||||
for p in "$@"; do
|
||||
reason="$(_path_exceeds_reason "$p")"
|
||||
if [ -n "$reason" ]; then
|
||||
echo "doc-shape: EXCEEDS MINOR envelope — $reason" >&2
|
||||
any=1
|
||||
fi
|
||||
done
|
||||
return "$any"
|
||||
}
|
||||
|
||||
main() {
|
||||
local cmd="${1:-}"
|
||||
case "$cmd" in
|
||||
check)
|
||||
shift
|
||||
[ "$#" -ge 1 ] || {
|
||||
echo "usage: doc-shape.sh check <path>..." >&2
|
||||
return 2
|
||||
}
|
||||
doc_shape_ok "$@"
|
||||
;;
|
||||
*)
|
||||
echo "usage: doc-shape.sh check <path>..." >&2
|
||||
return 2
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Run main only when executed, not when sourced.
|
||||
if [ "${BASH_SOURCE[0]}" = "${0}" ]; then
|
||||
main "$@"
|
||||
fi
|
||||
@ -59,8 +59,45 @@ echo "rc=$?"
|
||||
- The include treats rc 4 as an upstream BDR-022 anomaly to investigate — not a
|
||||
silent skip. The refusal IS the alarm.
|
||||
|
||||
## Scenario C — fail-loud on a rejected commit (the masked-failure path)
|
||||
|
||||
```bash
|
||||
# The gitflow pre-commit hook (or signing, or a protected branch) rejects the doc
|
||||
# commit. The helper must NOT report success: no false "committed", no stale hash.
|
||||
printf '#!/bin/sh\nexit 1\n' > "$R/.git/hooks/pre-commit"; chmod +x "$R/.git/hooks/pre-commit"
|
||||
printf '\n## another section\n' >> README.md
|
||||
out="$(bash "$HOME/.claude/lib/doc-commit.sh" commit "docs: rejected" "README.md")"
|
||||
echo "rc=$? out=[$out]"
|
||||
```
|
||||
|
||||
### Expected (assert)
|
||||
- `rc=5` (commit rejected), `out` EMPTY (no stale hash leaked on stdout).
|
||||
- stderr is loud (`COMMIT REJECTED …`) — never a false `committed`.
|
||||
- HEAD did NOT move; the doc stays in the working tree, uncommitted. The orchestrator
|
||||
must surface this and NOT proceed to FINISH as if docs landed (doc-commit.md rc 5 row).
|
||||
|
||||
## Scenario D — MINOR-shape oracle escalates a SIGNIFICANT-in-disguise
|
||||
|
||||
```bash
|
||||
# doc-syncer's LLM classified a drift MINOR, but the patch ADDS A SECTION HEADING —
|
||||
# structurally not a factual tweak. The oracle must overrule the MINOR call.
|
||||
printf '\n## Brand new feature\n\nA whole new capability.\n' >> USAGE.md # the "MINOR" patch
|
||||
bash "$HOME/.claude/lib/doc-shape.sh" check "USAGE.md"; echo "rc=$?"
|
||||
```
|
||||
|
||||
### Expected (assert)
|
||||
- `rc=1` (exceeds the MINOR envelope), stderr names the heading reason + `USAGE.md`.
|
||||
- doc-syncer STEP A4 routes this to the SIGNIFICANT gate (`Apply? yes/no/select`) instead
|
||||
of the silent auto-commit — the deterministic oracle overrules the LLM (LRN-046).
|
||||
- A genuine factual one-liner (changed command, no heading, small) returns `rc=0` and
|
||||
stays on the silent MINOR auto-commit path — zero friction (BDR-036 preserved).
|
||||
- The oracle is a STRUCTURAL floor: a small meaning-changing edit with no heading still
|
||||
reads MINOR (rc 0). It reduces RISK-1's gross cases, it does not eliminate RISK-1.
|
||||
|
||||
If Scenario A holds, the chain is coupled (docs committed in the same breath as the
|
||||
flow) and surgical (no dangling code embarked). If Scenario B holds, the guard is
|
||||
fail-closed and loud. This mirrors what feat / bugfix / hotfix do at their DOC SYNC
|
||||
step (inline-branch commit, no FINISH), and what ship-feature / init-project do at
|
||||
their DOC SYNC step BEFORE FINISH (so the doc commit reaches the merge/PR).
|
||||
fail-closed and loud. If Scenario C holds, a rejected commit fails LOUD instead of
|
||||
masking as success. If Scenario D holds, a shape-suspect MINOR is escalated to the
|
||||
human gate instead of auto-committed. This mirrors what feat / bugfix / hotfix do at
|
||||
their DOC SYNC step (inline-branch commit, no FINISH), and what ship-feature /
|
||||
init-project do at their DOC SYNC step BEFORE FINISH (so the doc commit reaches the merge/PR).
|
||||
|
||||
@ -13,6 +13,8 @@
|
||||
# T5 idempotent — empty list / clean tree → no-op exit 0
|
||||
# T6 unsafe git state (detached HEAD) → exit 3, no commit
|
||||
# T7 path WITH A SPACE passed as one arg → committed (argv is space-safe, no separator)
|
||||
# T8 pre-commit hook REJECTS the commit → fail LOUD (exit 5), no stale hash on stdout,
|
||||
# HEAD unmoved — the script must NOT report "committed" when git commit failed
|
||||
#
|
||||
# No -e: run every test and report, even after a failure.
|
||||
set -uo pipefail
|
||||
@ -173,6 +175,20 @@ if git -C "$R" cat-file -e "HEAD:docs/My Guide.md" 2>/dev/null; then ok "spaced
|
||||
if [ -z "$(git -C "$R" status --porcelain -- "docs/My Guide.md")" ]; then ok "spaced doc clean (embarked as ONE file, not split)"; else ko "spaced doc still dirty"; fi
|
||||
rm -rf "$R"
|
||||
|
||||
echo "T8 — pre-commit hook REJECTS commit → exit 5 LOUD, no stale hash, HEAD unmoved"
|
||||
R="$(new_repo)"
|
||||
printf '#!/bin/sh\nexit 1\n' >"$R/.git/hooks/pre-commit"; chmod +x "$R/.git/hooks/pre-commit"
|
||||
BEFORE="$(git -C "$R" rev-parse HEAD)"
|
||||
printf 'feature added\n' >>"$R/README.md"
|
||||
run "$R" commit "docs: T8 rejected" "README.md"
|
||||
printf ' rc=%s out=[%s]\n' "$RC" "$OUT"
|
||||
printf ' err: %s\n' "$(printf '%s' "$ERR" | head -1)"
|
||||
if [ "$RC" -eq 5 ]; then ok "rejected commit → exit 5"; else ko "expected 5, got $RC (commit failure swallowed = masking)"; fi
|
||||
if [ -z "$OUT" ]; then ok "stdout empty (no stale hash)"; else ko "stale hash leaked on failure: [$OUT]"; fi
|
||||
if printf '%s' "$ERR" | grep -qi 'REJECTED'; then ok "stderr is loud (REJECTED)"; else ko "stderr not loud (no REJECTED — likely a false 'committed')"; fi
|
||||
if [ "$(git -C "$R" rev-parse HEAD)" = "$BEFORE" ]; then ok "HEAD unmoved (nothing committed)"; else ko "HEAD moved despite hook reject"; fi
|
||||
rm -rf "$R"
|
||||
|
||||
rm -f "$ERRFILE"
|
||||
echo ""
|
||||
printf 'RESULT: %d passed, %d failed\n' "$PASS" "$FAIL"
|
||||
|
||||
166
lib/tests/run-doc-shape.sh
Normal file
166
lib/tests/run-doc-shape.sh
Normal file
@ -0,0 +1,166 @@
|
||||
#!/usr/bin/env bash
|
||||
# Deterministic tests for lib/doc-shape.sh — the MINOR-shape oracle.
|
||||
#
|
||||
# The oracle re-checks that a patch the LLM classified MINOR actually HAS minor
|
||||
# shape, on REAL git diffs (not assumed). Each case proves a verdict:
|
||||
# S1 factual one-liner (1 add / 1 del, no heading) → 0 within envelope
|
||||
# S2 adds a `## Section` heading → 1 exceeds (structural)
|
||||
# S3 +30 plain lines, no heading → 1 exceeds (size)
|
||||
# S3b +20 plain lines (== threshold) → 0 within (boundary)
|
||||
# S3c +10 lines with DOC_SHAPE_MAX_ADDED=5 (env override) → 1 exceeds (tunable)
|
||||
# S4 dead-reference removal (-2 / +0) → 0 within (small)
|
||||
# S5 new / untracked doc file → 1 exceeds (a creation)
|
||||
# S6 a code path (not a doc) → 1 exceeds (anomaly)
|
||||
# S7 clean tracked doc (no diff) → 0 within (vacuous)
|
||||
# S8 MIXED multi-path, ONE file exceeds → 1 exceeds, offender named
|
||||
# S9 usage (check with no paths) → 2
|
||||
# S10 not a git repo → 3
|
||||
#
|
||||
# No -e: run every test and report, even after a failure.
|
||||
set -uo pipefail
|
||||
|
||||
HERE="$(cd -P "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
HELPER="$HERE/../doc-shape.sh"
|
||||
ERRFILE="$(mktemp)"
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
ok() { printf ' \033[32m✓\033[0m %s\n' "$1"; PASS=$((PASS + 1)); }
|
||||
ko() { printf ' \033[31m✗\033[0m %s\n' "$1"; FAIL=$((FAIL + 1)); }
|
||||
|
||||
# Fresh throwaway repo: a few tracked docs + one code file, committed.
|
||||
new_repo() {
|
||||
local d
|
||||
d="$(mktemp -d)"
|
||||
git -C "$d" init -q
|
||||
git -C "$d" config user.email t@t.t
|
||||
git -C "$d" config user.name tester
|
||||
mkdir -p "$d/docs" "$d/src"
|
||||
printf 'run: foo\nold line A\nold line B\n' >"$d/README.md"
|
||||
printf 'usage baseline\n' >"$d/USAGE.md"
|
||||
printf 'guide baseline\n' >"$d/docs/guide.md"
|
||||
printf 'print("hi")\n' >"$d/src/app.py"
|
||||
git -C "$d" add -A
|
||||
git -C "$d" commit -qm baseline
|
||||
printf '%s' "$d"
|
||||
}
|
||||
|
||||
# Append N numbered plain lines (no heading) to a file.
|
||||
append_lines() {
|
||||
local f="$1" n="$2" i
|
||||
for ((i = 1; i <= n; i++)); do printf 'extra line %s\n' "$i" >>"$f"; done
|
||||
}
|
||||
|
||||
# run [ENV=val] <repo> <args...> → sets RC (exit), OUT (stdout), ERR (stderr).
|
||||
# stdout MUST stay empty: the exit code carries the verdict, reasons go to stderr.
|
||||
run() {
|
||||
local r="$1"; shift
|
||||
OUT="$( (cd "$r" && "$HELPER" "$@") 2>"$ERRFILE" )"; RC=$?
|
||||
ERR="$(cat "$ERRFILE")"
|
||||
}
|
||||
|
||||
echo "S1 — factual one-liner (1 add / 1 del, no heading) → within (0)"
|
||||
R="$(new_repo)"
|
||||
printf 'run: bar\nold line A\nold line B\n' >"$R/README.md" # change one line
|
||||
run "$R" check "README.md"
|
||||
printf ' rc=%s out=[%s]\n' "$RC" "$OUT"
|
||||
if [ "$RC" -eq 0 ]; then ok "factual tweak → within (0)"; else ko "expected 0, got $RC"; fi
|
||||
if [ -z "$OUT" ]; then ok "stdout empty"; else ko "stdout leaked: [$OUT]"; fi
|
||||
rm -rf "$R"
|
||||
|
||||
echo "S2 — adds a heading → exceeds (1, structural)"
|
||||
R="$(new_repo)"
|
||||
printf '\n## New Feature\n\nDescribes the new feature.\n' >>"$R/README.md"
|
||||
run "$R" check "README.md"
|
||||
printf ' rc=%s err=%s\n' "$RC" "$(printf '%s' "$ERR" | head -1)"
|
||||
if [ "$RC" -eq 1 ]; then ok "heading → exceeds (1)"; else ko "expected 1, got $RC"; fi
|
||||
if printf '%s' "$ERR" | grep -qi 'heading'; then ok "stderr names the heading reason"; else ko "reason not named"; fi
|
||||
rm -rf "$R"
|
||||
|
||||
echo "S3 — +30 plain lines, no heading → exceeds (1, size)"
|
||||
R="$(new_repo)"
|
||||
append_lines "$R/README.md" 30
|
||||
run "$R" check "README.md"
|
||||
printf ' rc=%s err=%s\n' "$RC" "$(printf '%s' "$ERR" | head -1)"
|
||||
if [ "$RC" -eq 1 ]; then ok "30 added → exceeds (1)"; else ko "expected 1, got $RC"; fi
|
||||
if printf '%s' "$ERR" | grep -qi 'added'; then ok "stderr names the size reason"; else ko "reason not named"; fi
|
||||
rm -rf "$R"
|
||||
|
||||
echo "S3b — +20 plain lines (== threshold) → within (0, boundary)"
|
||||
R="$(new_repo)"
|
||||
append_lines "$R/README.md" 20
|
||||
run "$R" check "README.md"
|
||||
printf ' rc=%s\n' "$RC"
|
||||
if [ "$RC" -eq 0 ]; then ok "20 added (== MAX) → within (0)"; else ko "expected 0, got $RC"; fi
|
||||
rm -rf "$R"
|
||||
|
||||
echo "S3c — +10 lines with DOC_SHAPE_MAX_ADDED=5 → exceeds (1, env-tunable)"
|
||||
R="$(new_repo)"
|
||||
append_lines "$R/README.md" 10
|
||||
OUT="$( (cd "$R" && DOC_SHAPE_MAX_ADDED=5 "$HELPER" check "README.md") 2>"$ERRFILE" )"; RC=$?
|
||||
printf ' rc=%s\n' "$RC"
|
||||
if [ "$RC" -eq 1 ]; then ok "override MAX_ADDED=5, 10 added → exceeds (1)"; else ko "expected 1, got $RC"; fi
|
||||
rm -rf "$R"
|
||||
|
||||
echo "S4 — dead-reference removal (-2 / +0) → within (0)"
|
||||
R="$(new_repo)"
|
||||
printf 'run: foo\n' >"$R/README.md" # drop the two 'old line' references
|
||||
run "$R" check "README.md"
|
||||
printf ' rc=%s\n' "$RC"
|
||||
if [ "$RC" -eq 0 ]; then ok "small removal → within (0)"; else ko "expected 0, got $RC"; fi
|
||||
rm -rf "$R"
|
||||
|
||||
echo "S5 — new / untracked doc file → exceeds (1, a creation)"
|
||||
R="$(new_repo)"
|
||||
printf 'brand new doc\n' >"$R/NEW.md" # untracked
|
||||
run "$R" check "NEW.md"
|
||||
printf ' rc=%s err=%s\n' "$RC" "$(printf '%s' "$ERR" | head -1)"
|
||||
if [ "$RC" -eq 1 ]; then ok "untracked doc → exceeds (1)"; else ko "expected 1, got $RC"; fi
|
||||
if printf '%s' "$ERR" | grep -Eqi 'untracked|new'; then ok "stderr flags the creation"; else ko "reason not named"; fi
|
||||
rm -rf "$R"
|
||||
|
||||
echo "S6 — a code path (not a doc) → exceeds (1, anomaly)"
|
||||
R="$(new_repo)"
|
||||
printf 'print("hi")\nprint("bye")\n' >"$R/src/app.py"
|
||||
run "$R" check "src/app.py"
|
||||
printf ' rc=%s err=%s\n' "$RC" "$(printf '%s' "$ERR" | head -1)"
|
||||
if [ "$RC" -eq 1 ]; then ok "non-doc path → exceeds (1)"; else ko "expected 1, got $RC"; fi
|
||||
if printf '%s' "$ERR" | grep -qi 'doc'; then ok "stderr flags the non-doc"; else ko "reason not named"; fi
|
||||
rm -rf "$R"
|
||||
|
||||
echo "S7 — clean tracked doc (no diff) → within (0, vacuous)"
|
||||
R="$(new_repo)"
|
||||
run "$R" check "docs/guide.md" # unmodified
|
||||
printf ' rc=%s\n' "$RC"
|
||||
if [ "$RC" -eq 0 ]; then ok "clean path → within (0)"; else ko "expected 0, got $RC"; fi
|
||||
rm -rf "$R"
|
||||
|
||||
echo "S8 — MIXED multi-path, ONE exceeds → exceeds (1), offender named"
|
||||
R="$(new_repo)"
|
||||
printf 'extra\n' >>"$R/README.md" # small, within
|
||||
append_lines "$R/USAGE.md" 30 # big, exceeds
|
||||
run "$R" check "README.md" "USAGE.md"
|
||||
printf ' rc=%s err=%s\n' "$RC" "$(printf '%s' "$ERR" | head -1)"
|
||||
if [ "$RC" -eq 1 ]; then ok "any path exceeds → whole set exceeds (1)"; else ko "expected 1, got $RC"; fi
|
||||
if printf '%s' "$ERR" | grep -q 'USAGE.md'; then ok "stderr names the offender (USAGE.md)"; else ko "offender not named"; fi
|
||||
if printf '%s' "$ERR" | grep -q 'README.md'; then ko "README.md wrongly flagged"; else ok "within-envelope file NOT flagged"; fi
|
||||
rm -rf "$R"
|
||||
|
||||
echo "S9 — usage (check with no paths) → 2"
|
||||
R="$(new_repo)"
|
||||
run "$R" check
|
||||
printf ' rc=%s\n' "$RC"
|
||||
if [ "$RC" -eq 2 ]; then ok "no paths → usage (2)"; else ko "expected 2, got $RC"; fi
|
||||
rm -rf "$R"
|
||||
|
||||
echo "S10 — not a git repo → 3"
|
||||
D="$(mktemp -d)" # plain dir, no git init
|
||||
run "$D" check "README.md"
|
||||
printf ' rc=%s\n' "$RC"
|
||||
if [ "$RC" -eq 3 ]; then ok "not-a-repo → 3"; else ko "expected 3, got $RC"; fi
|
||||
rm -rf "$D"
|
||||
|
||||
rm -f "$ERRFILE"
|
||||
echo ""
|
||||
printf 'RESULT: %d passed, %d failed\n' "$PASS" "$FAIL"
|
||||
[ "$FAIL" -eq 0 ]
|
||||
@ -15,10 +15,10 @@ $ARGUMENTS
|
||||
## PROGRESS PROTOCOL
|
||||
|
||||
Every STEP must announce itself with a header BEFORE its work block, so the
|
||||
user always sees where they are in the 12-step pipeline:
|
||||
user always sees where they are in the 11-step pipeline:
|
||||
|
||||
```
|
||||
━━━ STEP <N>/12 — <TITLE> ━━━ (~<estimated minutes>)
|
||||
━━━ STEP <N>/11 — <TITLE> ━━━ (~<estimated minutes>)
|
||||
why: <one sentence — what's at risk if this step is skipped>
|
||||
```
|
||||
|
||||
@ -248,8 +248,7 @@ patched. Report per its rc table — rc 4 = a LOUD upstream BDR-022 anomaly, not
|
||||
|
||||
> **Scaffold commit owner = STEP 5f `gitflow init`** (root commit embeds scaffold + README +
|
||||
> `.gitignore` socle + hook, tree clean — BLK-010 closed). This doc-sync commit lands the
|
||||
> patched docs on the MVP feature branch so they reach the merge. GSD STEP 12 still creates
|
||||
> ROADMAP.md post-FINISH (BLK-011) — separate.
|
||||
> patched docs on the MVP feature branch so they reach the merge.
|
||||
|
||||
## STEP 11 — FINISH
|
||||
Tests pass, build clean, no placeholders. Integrate the MVP feature into develop
|
||||
@ -258,18 +257,6 @@ Tests pass, build clean, no placeholders. Integrate the MVP feature into develop
|
||||
bash "$HOME/.claude/lib/gitflow.sh" finish # feature/mvp → develop
|
||||
```
|
||||
|
||||
## STEP 12 — GSD v2 INIT (optional)
|
||||
If `multi-session` signal was detected in STEP 0 OR the project has >3 planned milestones:
|
||||
Ask: "Initialize GSD v2 for multi-session management? (yes / skip)"
|
||||
- `yes` →
|
||||
1. First check: `command -v gsd` — if not found:
|
||||
Print: "⚠️ GSD v2 not installed. Run `npm install -g gsd-pi` then re-run `/onboard add gsd` or `/ship-feature` to initialize later."
|
||||
Do NOT attempt `gsd init`. Skip to RULES.
|
||||
2. If `gsd` is in PATH: run `gsd init` in the project directory to create `.gsd/` and `ROADMAP.md`.
|
||||
Populate ROADMAP.md with milestones from BRIEF (v1 features + any beyond-v1 items).
|
||||
Print: "✅ GSD v2 initialized — run `gsd` in terminal then `/gsd auto` to work autonomously."
|
||||
- `skip` → print: "GSD v2 skipped — use `/ship-feature` for individual features."
|
||||
|
||||
---
|
||||
|
||||
## RULES
|
||||
|
||||
Loading…
Reference in New Issue
Block a user