Skip to content

Commit 9188e1d

Browse files
author
lpb-docs
committed
feat(validate): worktree-state checks + hook unstaged guard
- validate: new 'Worktree state' section — config worktree must be clean (hard check; runtime state is gitignored there, so drift is always an anomaly); extension-repo WIP is reported, not blocking (it previously sat invisible until a workspace sync skipped the repo) - workspace: _dirty_files() helper (porcelain minus lockfile tool-noise that sync auto-discards) - pre-commit: fail fast when tracked devstack changes are unstaged (they would silently be missing from the commit being made) - skill doc updated
1 parent e9a0216 commit 9188e1d

4 files changed

Lines changed: 73 additions & 6 deletions

File tree

.githooks/pre-commit

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,19 @@
1313
# SKIP_TESTS=1 git commit ... — skip unit tests (~4s)
1414
# git commit --no-verify ... — skip everything
1515

16+
# Devstack's own tree: tracked changes must be STAGED — unstaged edits
17+
# would silently be missing from the commit being made right now.
18+
# (Untracked files are fine: new work-in-progress.)
19+
echo "=== devstack staged ==="
20+
unstaged=$(git diff --name-only)
21+
if [ -n "$unstaged" ]; then
22+
echo "❌ Unstaged changes in devstack (NOT included in this commit):"
23+
echo "$unstaged" | sed 's/^/ /'
24+
echo " git add the files above (or commit them separately), or --no-verify"
25+
exit 1
26+
fi
27+
echo ""
28+
1629
echo "=== lpb-devstack validate (workspace copy) ==="
1730
if ! python3 scripts/lpb-devstack validate; then
1831
echo ""

.pi/skills/localpibox-repo-workflow/SKILL.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -277,12 +277,15 @@ Same pattern — template in config repo, user config on host volume:
277277
## Hooks (devstack only, `core.hooksPath=.githooks`)
278278

279279
**pre-commit** — validates BEFORE commit (exit non-zero aborts):
280-
1. `python3 scripts/lpb-devstack validate` — the full stack alignment
280+
1. devstack's tracked changes are fully staged (unstaged edits would
281+
silently miss the commit)
282+
2. `python3 scripts/lpb-devstack validate` — the full stack alignment
281283
check (VERSION, `LPB_PI_VERSION`, extension pins, branch alignment,
282-
pipeline consistency). Runs the WORKSPACE copy on purpose: the PATH
283-
`lpb-devstack` is the image-baked one and stays stale until the next
284-
rebuild, so the hook always validates with the current logic.
285-
2. `scripts/test_lpb.py` passes (skip with `SKIP_TESTS=1`)
284+
pipeline consistency, config worktree clean + extension-repo WIP
285+
report). Runs the WORKSPACE copy on purpose: the PATH `lpb-devstack`
286+
is the image-baked one and stays stale until the next rebuild, so the
287+
hook always validates with the current logic.
288+
3. `scripts/test_lpb.py` passes (skip with `SKIP_TESTS=1`)
286289

287290
The full test suite in pre-commit is intentional — it guards against
288291
low-quality changes reaching the repo. `lpb-devstack validate-hooks` runs

scripts/localpibox/stack/validate.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
33
Covers: VERSION file, config repo branch, workspace repo branches/symlinks,
44
pipeline consistency (no mixed dev/main state), extension alignment,
5-
stack env refs, and settings.json pins.
5+
worktree state (config clean, extension-repo WIP report), stack env refs,
6+
and settings.json pins.
67
"""
78

89
from __future__ import annotations
@@ -23,6 +24,7 @@
2324
from .version import _find_version_file, expected_branch, expected_pin_version, get_stack_env, get_stack_env_base, get_version
2425
from .workspace import (
2526
_detached_ref,
27+
_dirty_files,
2628
_get_pinned_versions,
2729
_read_settings,
2830
_repo_branch,
@@ -146,6 +148,44 @@ def check(label: str, condition: bool, detail: str = "", fix: str = "") -> None:
146148
f"lpb-devstack --tag {pipeline} workspace sync",
147149
)
148150

151+
# ── 3b. Worktree state — leftover uncommitted changes ─────────────
152+
cons.info("")
153+
cons.info(" Worktree state:")
154+
155+
# config repo: everything runtime is gitignored there, so a dirty
156+
# worktree means uncommitted template/skill/doc content — always an
157+
# anomaly, and a hard check.
158+
if (config_path / ".git").exists():
159+
cfg_dirty = _dirty_files(config_path)
160+
check(
161+
" config worktree clean",
162+
not cfg_dirty,
163+
"uncommitted changes:" if cfg_dirty else "clean",
164+
"git -C ~/.pi/agent status — commit or discard first",
165+
)
166+
if cfg_dirty:
167+
for line in cfg_dirty[:5]:
168+
cons.warn(f" {line.strip()}")
169+
170+
# Extension code repos: uncommitted WIP is normal during dev — report
171+
# it (it previously sat invisible until a sync skipped the repo), but
172+
# don't block on it. devstack's own tree is judged by the pre-commit
173+
# hook (unstaged check), not here.
174+
dirty_ext: list[str] = []
175+
for name, is_sym, is_ext, dev_branch, main_branch in WORKSPACE_REPOS:
176+
if name == "devstack":
177+
continue
178+
path = _resolve_repo_path(name)
179+
if path is None:
180+
continue
181+
files = _dirty_files(path)
182+
if files:
183+
dirty_ext.append(f"{name} ({len(files)} file(s))")
184+
if dirty_ext:
185+
cons.warn(f" WIP in extension repos (not blocking): {', '.join(dirty_ext)}")
186+
else:
187+
cons.info(" extension repos: no uncommitted changes")
188+
149189
# ── 4. Extension repos match workspace ─────────────────────────────
150190
cons.info("")
151191
cons.info(" Extension alignment:")

scripts/localpibox/stack/workspace.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,17 @@ def _is_dirty(path: Path) -> bool:
106106
return bool(out.strip())
107107

108108

109+
def _dirty_files(path: Path) -> list[str]:
110+
"""Uncommitted change lines (porcelain), minus lockfile tool-noise.
111+
112+
Lockfile rewrites are discarded automatically by workspace sync, so
113+
they are not reported as drift.
114+
"""
115+
st, _, _ = git(path, "status", "--porcelain")
116+
return [line for line in st.splitlines()
117+
if line.strip() and line[3:] not in LOCKFILE_NAMES]
118+
119+
109120
# Dependency lockfiles a package manager rewrites during `npm install`.
110121
# pi's extension manager runs npm install inside the extension clones, and a
111122
# newer npm than the one that generated the committed lockfile rewrites it

0 commit comments

Comments
 (0)