From f57853c3f3b56be88dbee29d314861fa4382fc87 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Wed, 3 Jun 2026 14:00:17 -0400 Subject: [PATCH 01/20] design: ADK path canonicalization + write-location transparency + artifact hygiene (v6.5.0) --- ...-06-03-adk-path-canonicalization-design.md | 192 ++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 docs/plans/2026-06-03-adk-path-canonicalization-design.md diff --git a/docs/plans/2026-06-03-adk-path-canonicalization-design.md b/docs/plans/2026-06-03-adk-path-canonicalization-design.md new file mode 100644 index 0000000..365c8b7 --- /dev/null +++ b/docs/plans/2026-06-03-adk-path-canonicalization-design.md @@ -0,0 +1,192 @@ +# ADK Path Canonicalization + Write-Location Transparency — Design + +**Date:** 2026-06-03 +**Target release:** v6.5.0 +**Origin:** #70 residual (autodev v6.4.0 retro) — the activation log fragmented under worktree pipelines because every hook computes its state dir from the payload `.cwd`. + +## Problem + +Three related directory-confusion failure modes: + +1. **Inconsistent ADK state location.** Every hook independently computes + `STATE_DIR="${cwd_dir}/.claude/autodev-state"` from the hook payload `.cwd` (fallback `$PWD`). + There is **no shared resolver**. Under a git worktree the `.cwd` is the worktree, so state + (activation log, session-locks, pr-reminder marker, lock snapshots) is written *into the + worktree*. When the worktree is removed (normal cleanup after a merged PR), that state is + discarded — exactly what made the v6.4.0 retro read a fragmented activation log. Concurrent + worktrees also each get their own divergent copy. +2. **Opaque subagent writes.** A dispatched subagent works in some directory (a worktree, a + sibling checkout) and writes files, but its final report doesn't say *where*. When the + orchestrator later finds an error in a path — or the subagent wrote to the wrong worktree — + there's no ledger to relocate or reconcile the state from. +3. **Machine paths leaking into committed artifacts.** Designs, plans, retros, review reports, + and ADRs are committed to public history. An absolute operator path + (`/Users/jon/Documents/GitHub/...`) baked into one of them leaks the operator's machine layout + forever. This already happened: `docs/testing.md:152` contains a `/Users/jon/...` example path. + +## Goals / Non-goals + +**G1 (consistent writes):** all ADK state writes/reads resolve to **one canonical location per +repository**, stable across worktrees and surviving worktree removal. +**G2 (transparency):** dispatched subagents report a **write-location ledger** so the orchestrator +can verify, and relocate/reconcile state if a path is wrong. +**G3 (path hygiene):** committed artifacts (design/plan/retro/review/ADR) carry **repo-relative +paths only** — never absolute machine paths — enforced by CI. Local state logs are exempt. + +**Non-goals (YAGNI):** +- No change to *what* state is recorded, only *where* it lands. +- No new state file, no new hook, no schema change to existing state rows. +- No rewrite of `.cwd` semantics — the hook payload is unchanged; we resolve a canonical root *from* it. +- Not gitignoring `.autodev/state/phase-progress.jsonl` (it is intentionally tracked); we keep it + clean (it already uses repo-relative paths) and merely anchor its location. +- No absolute-path ban on *all* files — only on the committed-artifact set. Code examples, + `/healthz`, `/tmp/...` in scripts, etc. are untouched (the check targets operator-home paths only). + +## Design + +### C1 — Canonical state-path resolver (shared lib) + +New `hooks/lib-autodev-paths.sh`, sourced by every hook/helper that touches `.claude/autodev-state` +or `.autodev/state`. It exports one function: + +```sh +# autodev_repo_root -> echoes the canonical repo root for ADK state. +# Resolution order: +# 1. $AUTODEV_STATE_ROOT if set and non-empty (explicit override; used by tests). +# 2. The git COMMON dir's parent, resolved from : `git rev-parse --git-common-dir` +# returns the shared main `.git` from any linked worktree, so its parent is the ONE +# root all worktrees of a repo agree on (survives worktree removal). +# 3. Fallback: itself (non-git dir, or git unavailable) — i.e. today's behavior. +autodev_repo_root() { + cwd="${1:-$PWD}" + if [ -n "${AUTODEV_STATE_ROOT:-}" ]; then printf '%s\n' "$AUTODEV_STATE_ROOT"; return 0; fi + root="$(cd "$cwd" 2>/dev/null && cd "$(git rev-parse --git-common-dir 2>/dev/null)/.." 2>/dev/null && pwd)" + if [ -n "$root" ]; then printf '%s\n' "$root"; else printf '%s\n' "$cwd"; fi +} +``` + +Each consumer replaces `STATE_DIR="${cwd_dir}/.claude/autodev-state"` with: +```sh +. "$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)/lib-autodev-paths.sh" 2>/dev/null || autodev_repo_root() { printf '%s\n' "${1:-$PWD}"; } +ADK_ROOT="$(autodev_repo_root "$cwd_dir")" +STATE_DIR="${ADK_ROOT}/.claude/autodev-state" +``` + +**Robustness invariant (the #66 lesson — hooks must never break):** if the lib can't be sourced, +the inline fallback defines `autodev_repo_root` as the identity-on-cwd function, so the hook +degrades to **exactly today's cwd-scoped behavior** rather than erroring. A missing/again-broken +lib is a no-op regression, never a hook failure. + +**Consumers to retrofit** (every file referencing the state dirs): +`session-start`, `pre-compact-snapshot`, `subagent-scope-guard`, `prompt-strict-interpretation`, +`record-activity`, `completion-claim-guard`, `pretool-pr-review-reminder`, `pre-tool-scope-guard`, +`posttool-pr-created`, and the helpers `scope-lock-apply`, `scope-lock-claim`, +`scope-lock-complete`, `scope-lock-abandon`, `scope-lock-publish`. (Exact list confirmed by +`grep -rl 'autodev-state\|\.autodev/state' hooks/` at plan time.) + +`post-merge-retrospective` (the #70 consumer) reads `.claude/autodev-state/in-progress.jsonl` from +the same canonical root, closing the original residual. + +### C2 — Subagent write-location ledger + +Convention added to `agents/team-conventions.md` and the three subagent prompt templates +(`implementer-prompt.md`, `spec-reviewer-prompt.md`, `code-quality-reviewer-prompt.md`) + +`subagent-driven-development` SKILL: **every** subagent's final message ends with a `Writes:` +ledger — one line per file it created/modified, as a **repo-relative path**, plus an explicit flag +if any write landed outside the expected repo/worktree (`OUT-OF-TREE: `). The +orchestrator reads the ledger to (a) confirm work landed where expected and (b) relocate/reconcile +state if a path is wrong before committing. Repo-relative in the ledger keeps absolute paths out of +anything that might be quoted into an artifact; the one allowed absolute path is the explicit +`OUT-OF-TREE:` escape, which exists precisely to surface a mistake to the orchestrator (transcript +only, never committed). + +### C3 — Artifact path-hygiene gate + +New `tests/no-machine-paths.sh`: greps the committed-artifact set (`docs/`, `decisions/`) for +operator-home absolute paths — `/Users//`, `/home//`, and literal `$HOME`/`~/` expanded +forms — and fails with the offending `file:line`. Narrow by construction: it targets home-rooted +machine paths, not all absolute paths (so `/healthz`, `/tmp/x`, `/etc/...` in legitimate examples +pass). Wired into `.github/workflows/skill-content-check.yml` (which already gates docs/skills) + +its `paths:` filter. The existing leak at `docs/testing.md:152` is fixed to a placeholder +(`/path/to/autodev` or `/...`) in the same PR so the gate is green on landing. + +Plus a one-line rule in the artifact-writing skills (`brainstorming`, `writing-plans`, +`post-merge-retrospective`, `adversarial-design-review`, `recording-decisions`): committed +artifacts use repo-relative paths; never absolute machine paths. Local state logs +(`.claude/autodev-state/*`, gitignored) may hold absolute paths; the tracked +`.autodev/state/phase-progress.jsonl` stays repo-relative (already is). + +## Global Design Guidance + +No `docs/design-guidance.md` (recurring gap, noted again in the v6.4.0 retro — not bootstrapped +here to avoid scope creep). Inherited principles honored: skills/hooks stay tight; hooks must never +break (#66); evidence/state must live where its consumer reads it +(v6.4.0 `feedback_evidence_artifact_must_live_where_consumer_reads`); no absolute machine paths in +committed history. + +## Security Review + +Net **reduction** in exposure: C3 stops operator machine-layout (usernames, home structure) leaking +into public git history. C1 reads only local git metadata + an env var; no network, no secrets, no +new file contents. C2's ledger is transcript-only and repo-relative. The `AUTODEV_STATE_ROOT` +override is operator-controlled (an env var) — no untrusted input path. No auth/authz surface. + +## Infrastructure Impact + +No runtime/cloud/deploy change. CI gains one test step in an existing workflow. v6.5.0 bumps the 3 +plugin manifests → `release-tag.yml` auto-tags (standard kit path). The canonical-root change moves +where *local* state files are written; it does not move any committed file except fixing the one +`docs/testing.md` leak. + +## Multi-Component Validation + +The cross-component boundary is **worktree-cwd → resolver → canonical state dir → retro/consumer**. +Proof obligations for the plan: +- A test that, from a simulated linked worktree, `autodev_repo_root` returns the **main** root + (not the worktree) — and from a non-git dir returns cwd (fallback). Use a throwaway `git init` + + `git worktree add` in a temp dir, assert the resolver output. This is the load-bearing claim. +- A test that `record-activity` (run with a worktree-style `.cwd`) appends to the **canonical** + `in-progress.jsonl`, and that the retro reads it from the same place. +- The `no-machine-paths.sh` gate run against the current tree (must pass after the testing.md fix; + must FAIL on a deliberately seeded `/Users/...` line — revert-restore proof). +- Lib-missing degradation: temporarily hide the lib, confirm a retrofitted hook still emits valid + output (degrades to cwd-scoping, no error) — the #66 robustness invariant. + +## Assumptions + +- **A1:** `git rev-parse --git-common-dir` from a linked worktree's cwd returns the shared main + `.git`, whose parent is the canonical root. *Load-bearing for C1; verified by the worktree test.* +- **A2:** Hooks are always invoked with a cwd inside (or under) the target repo, so the resolver + can find the git common dir. If not (cwd outside any repo), fallback-to-cwd preserves today's + behavior — acceptable, no regression. +- **A3:** Sourcing a sibling `lib-autodev-paths.sh` via `dirname "$0"` works under `run-hook.cmd` + (which invokes `bash ${SCRIPT_DIR}/`, so `$0` is the hook's own path). *Verified by the + install-layout + a content test.* +- **A4:** Anchoring `.autodev/state/phase-progress.jsonl` to the canonical (main) root, while it is + a tracked file, will not create problematic dirty-tree noise beyond today's — worktree runs + already could touch it; consolidating to one location is strictly more predictable. +- **A5:** Concurrent worktree pipelines interleaving into one append-only JSONL log is acceptable + (timestamped rows; a unified cross-worktree view is desirable, not a hazard). + +## Rollback + +Change classes: hook/startup-config (state path) + plugin version pin. Rollback = revert the merge +commit + re-tag v6.4.0. No migration: the canonical dir is computed at runtime; reverting restores +cwd-scoping. Any state already written to a canonical dir is harmless if later read cwd-scoped +(worst case: same fragmentation as before). Per-task rollback notes in the plan for the +version-bump + the hook-retrofit tasks. + +## Self-challenge (top doubts surfaced) + +1. **~14 retrofitted hooks is a big mechanical diff — bloat risk?** Each change is ~3 lines + (source + resolve + one substitution) and *removes* divergence rather than adding behavior. The + net is a single new lib + uniform call sites; a half-retrofit would re-create the exact + inconsistency we're fixing (user chose full retrofit for this reason). +2. **Could the resolver subtly relocate state mid-pipeline and orphan an in-flight lock?** During + the transition PR, a session that wrote a cwd-scoped lock before upgrade could look for it at the + canonical path after. Mitigation: the scope-lock helpers resolve the same way for write+read, so + within a version they're consistent; cross-version, the worst case is a stale local marker (the + nag hooks already tolerate absent markers). Called out for the adversarial reviewer. +3. **`git-common-dir` edge cases (submodules, bare repos, `$GIT_DIR` set).** Fallback-to-cwd covers + the unknowns; the resolver never hard-fails. The reviewer should probe submodule + detached + scenarios. From 582ea91c9d72637af95207d1c00ba78645f34234 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Wed, 3 Jun 2026 14:12:47 -0400 Subject: [PATCH 02/20] design: revise per adversarial review (2C+3I+4m): resolver null-guard, placeholder-aware gate, dedicated CI workflow, authoritative 12-hook list --- ...adk-path-canonicalization-design-review.md | 42 ++++++++ ...-06-03-adk-path-canonicalization-design.md | 102 +++++++++++++----- 2 files changed, 116 insertions(+), 28 deletions(-) create mode 100644 docs/plans/2026-06-03-adk-path-canonicalization-design-review.md diff --git a/docs/plans/2026-06-03-adk-path-canonicalization-design-review.md b/docs/plans/2026-06-03-adk-path-canonicalization-design-review.md new file mode 100644 index 0000000..3bade00 --- /dev/null +++ b/docs/plans/2026-06-03-adk-path-canonicalization-design-review.md @@ -0,0 +1,42 @@ +# ADK Path Canonicalization — Adversarial Review + +**Phase:** design +**Artifact:** `docs/plans/2026-06-03-adk-path-canonicalization-design.md` +**Status:** FAIL → revised (all findings resolved in design text); re-run pending + +## Findings (cycle 1) + +| id | sev | class | issue | resolution | +|---|---|---|---|---| +| C-1 | Critical | Correctness | Resolver `cd "$(git rev-parse --git-common-dir)/.."` returns `/` (not `$cwd`) when git absent / non-git dir (empty substitution → `cd "/.."`). Reproduced: from `/tmp` → `/`. Violates the "degrade to today's behavior" invariant. | Resolver rewritten: capture `_gcd` first, guard `[ -n "$_gcd" ]` before the `cd`, else fall back to `$cwd`. | +| C-2 | Critical | Self-referential trap | The gate scans `docs/` for `/Users//`; the design doc + plan + retro for THIS feature must *document* that pattern, so a blanket grep fails on its own artifacts (design line had `/Users/jon/...`). | Gate regex made **placeholder-aware**: `(/Users/\|/home/)[A-Za-z0-9][A-Za-z0-9._-]*` matches a real username segment but ignores `` and ellipsis; author convention = illustrate with `/Users//`; plus a `path-hygiene-allow` line sentinel. All this feature's artifacts sanitized to placeholders (verified gate-clean). | +| I-1 | Important | Robustness | Inline fallback `source \|\| define-fn` only fires on source *failure*; a lib that sources OK but lacks the function → exit 127 under `set -euo pipefail` kills the hook. | Replaced with post-source `declare -f autodev_repo_root >/dev/null 2>&1 \|\| autodev_repo_root(){...}` — covers missing-lib AND missing-function. | +| I-2 | Important | CI wiring gap | `skill-content-check.yml` `paths:` filter is `skills/**`/`agents/**` — a docs-only/decisions-only leak PR would never trigger the gate (false guarantee). | Gate moved to a **dedicated `path-hygiene.yml`** with NO `paths:` filter → always runs on push + PR. | +| I-3 | Important | Multi-component / accuracy | Retrofit list wrong by 3: `pretool-demo-fidelity-guard` MISSING (has state ref @ line 92); `posttool-pr-created` + `scope-lock-apply` listed but have NO state ref. | Replaced with the authoritative 12 from `grep -rlE 'autodev-state\|\.autodev/state' hooks/`; excluded-list documented; plan re-runs the grep as a guard. | +| m-1 | Minor | Edge case | Bare repo: `--git-common-dir` returns `.` → resolver returns the bare dir's *parent* (not a fallback). | Documented in A2 as nil-impact (hooks never run in a bare repo), never hard-fails. | +| m-2 | Minor | Repo precedent | Source line used `$0`; repo convention is `${BASH_SOURCE[0]:-$0}`. | Adopted `${BASH_SOURCE[0]:-$0}`. | +| m-3 | Minor | Behavioral disclosure | `scope-lock-complete`/`-abandon` currently derive `repo_root` from the plan path; switching to git-canonical changes worktree behavior (intentional). | Called out explicitly in C1 as an intentional behavioral change + noted they pass `$PWD`. | +| m-4 | Minor | YAGNI / honor-system | C2 ledger has no validator → degrades over time. | Acknowledged as a deliberate soft convention; orchestrator falls back to diff inspection. | + +## Bug-class scan transcript (cycle 1) +| Class | Result | Note | +|---|---|---| +| Assumptions | Finding (C-1, m-1) | non-git → `/` bug; bare-repo edge | +| Repo-precedent | Finding (m-2) | `$0` vs `${BASH_SOURCE[0]:-$0}` | +| Artifact-class precedent | Clean | adopts existing `-design-review.md` convention (v6.4.0) | +| YAGNI | Finding (m-4) | C2 honor-system acknowledged | +| Missing failure modes | Finding (C-1, I-1) | resolver `/` return; function-absent exit 127 | +| Security | Clean | net reduction (stops machine-path leaks); env override operator-only | +| Infrastructure | Finding (I-2) | CI paths-filter gap → dedicated workflow | +| Multi-component | Finding (I-3) | retrofit list wrong by 3 | +| Rollback | Clean | revert-merge + re-tag; no migration | +| Simpler alternative | Clean | `--show-toplevel` rejected (gives worktree root); `--git-common-dir` correct | +| User-intent drift | Clean | C1/C2/C3 map directly to the 3 stated pains | +| Existence/runtime-validity | Finding (C-2) | self-referential gate failure on own docs | + +## Options taken +1. Single-pass git-common-dir with null guard — **taken** (C-1). +2. Dedicated `path-hygiene.yml` with no `paths:` filter — **taken** (I-2). +3. Placeholder-aware regex + `path-hygiene-allow` sentinel — **taken** (C-2). + +**Verdict reasoning:** Two Criticals (resolver `/`-return + self-referential gate trap) + three Importants (incomplete fallback guard, CI paths-filter false guarantee, retrofit list wrong by 3) all had concrete low-effort fixes, now in the design text. The git primitive (`--git-common-dir`) is correct; the main repo + linked-worktree path is verified. Re-run to confirm convergence. diff --git a/docs/plans/2026-06-03-adk-path-canonicalization-design.md b/docs/plans/2026-06-03-adk-path-canonicalization-design.md index 365c8b7..35c2b23 100644 --- a/docs/plans/2026-06-03-adk-path-canonicalization-design.md +++ b/docs/plans/2026-06-03-adk-path-canonicalization-design.md @@ -21,8 +21,8 @@ Three related directory-confusion failure modes: there's no ledger to relocate or reconcile the state from. 3. **Machine paths leaking into committed artifacts.** Designs, plans, retros, review reports, and ADRs are committed to public history. An absolute operator path - (`/Users/jon/Documents/GitHub/...`) baked into one of them leaks the operator's machine layout - forever. This already happened: `docs/testing.md:152` contains a `/Users/jon/...` example path. + (`/Users//Documents/GitHub/...`) baked into one of them leaks the operator's machine + layout forever. This already happened: `docs/testing.md` contains an operator-home example path. ## Goals / Non-goals @@ -60,29 +60,49 @@ or `.autodev/state`. It exports one function: autodev_repo_root() { cwd="${1:-$PWD}" if [ -n "${AUTODEV_STATE_ROOT:-}" ]; then printf '%s\n' "$AUTODEV_STATE_ROOT"; return 0; fi - root="$(cd "$cwd" 2>/dev/null && cd "$(git rev-parse --git-common-dir 2>/dev/null)/.." 2>/dev/null && pwd)" - if [ -n "$root" ]; then printf '%s\n' "$root"; else printf '%s\n' "$cwd"; fi + # C-1 fix: capture --git-common-dir FIRST and guard non-empty before cd, so an absent + # git / non-git dir yields an empty $_gcd → fallback to $cwd (NOT `/` from `cd ""/..`). + _gcd="$(cd "$cwd" 2>/dev/null && git rev-parse --git-common-dir 2>/dev/null || true)" + _root="" + [ -n "$_gcd" ] && _root="$(cd "$cwd" 2>/dev/null && cd "$_gcd/.." 2>/dev/null && pwd || true)" + if [ -n "$_root" ]; then printf '%s\n' "$_root"; else printf '%s\n' "$cwd"; fi } ``` -Each consumer replaces `STATE_DIR="${cwd_dir}/.claude/autodev-state"` with: +Each consumer sources the lib and then **guards on the function actually existing** (I-1 fix — +covers both "lib missing" AND "lib present but function absent", which under `set -euo pipefail` +would otherwise exit 127 and kill the hook): ```sh -. "$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)/lib-autodev-paths.sh" 2>/dev/null || autodev_repo_root() { printf '%s\n' "${1:-$PWD}"; } +. "$(cd -- "$(dirname -- "${BASH_SOURCE[0]:-$0}")" && pwd)/lib-autodev-paths.sh" 2>/dev/null || true +declare -f autodev_repo_root >/dev/null 2>&1 || autodev_repo_root() { printf '%s\n' "${1:-$PWD}"; } ADK_ROOT="$(autodev_repo_root "$cwd_dir")" STATE_DIR="${ADK_ROOT}/.claude/autodev-state" ``` -**Robustness invariant (the #66 lesson — hooks must never break):** if the lib can't be sourced, -the inline fallback defines `autodev_repo_root` as the identity-on-cwd function, so the hook -degrades to **exactly today's cwd-scoped behavior** rather than erroring. A missing/again-broken -lib is a no-op regression, never a hook failure. - -**Consumers to retrofit** (every file referencing the state dirs): -`session-start`, `pre-compact-snapshot`, `subagent-scope-guard`, `prompt-strict-interpretation`, -`record-activity`, `completion-claim-guard`, `pretool-pr-review-reminder`, `pre-tool-scope-guard`, -`posttool-pr-created`, and the helpers `scope-lock-apply`, `scope-lock-claim`, -`scope-lock-complete`, `scope-lock-abandon`, `scope-lock-publish`. (Exact list confirmed by -`grep -rl 'autodev-state\|\.autodev/state' hooks/` at plan time.) +**Robustness invariant (the #66 lesson — hooks must never break):** the `declare -f` guard +defines an identity-on-cwd fallback whenever `autodev_repo_root` is not in scope after the source +attempt, so a missing OR broken-interface lib degrades to **exactly today's cwd-scoped behavior** +rather than erroring. Verified by the lib-missing + empty-lib tests (Multi-Component Validation). +Uses `${BASH_SOURCE[0]:-$0}` to match the repo's existing `SCRIPT_DIR` convention (m-2). + +**Consumers to retrofit — authoritative list (I-3 fix), the exact 12 files from +`grep -rlE 'autodev-state|\.autodev/state' hooks/`:** +`completion-claim-guard`, `pre-compact-snapshot`, `pre-tool-scope-guard`, +`pretool-demo-fidelity-guard`, `pretool-pr-review-reminder`, `prompt-strict-interpretation`, +`record-activity`, `scope-lock-abandon`, `scope-lock-claim`, `scope-lock-complete`, +`session-start`, `subagent-scope-guard`. + +Explicitly **excluded** (verified no state reference): `scope-lock-apply` and `scope-lock-publish` +(write only the `.scope-lock` sidecar next to the plan — not state dirs) and `posttool-pr-created` +(a PR-creation reminder, no state). The plan re-runs the grep as its first step and fails if the +list drifts. + +**Behavioral-change note (m-3):** `scope-lock-complete` and `scope-lock-abandon` currently derive +`repo_root` from the **plan path** (`cd "${plan_dir}/../.." && pwd`, assuming `docs/plans/` depth); +they will switch to `autodev_repo_root "$PWD"` (git-canonical). In a worktree this **intentionally** +changes the result from the worktree root to the main root — that is the whole point of the change, +but the plan must call it out so the implementer doesn't treat it as a bug. These two helpers take +the shell `$PWD` (not a hook payload `.cwd`) as the cwd argument. `post-merge-retrospective` (the #70 consumer) reads `.claude/autodev-state/in-progress.jsonl` from the same canonical root, closing the original residual. @@ -100,21 +120,44 @@ anything that might be quoted into an artifact; the one allowed absolute path is `OUT-OF-TREE:` escape, which exists precisely to surface a mistake to the orchestrator (transcript only, never committed). +**Honor-system caveat (m-4):** C2 is a convention, not a mechanical gate — there is no CI validator +that a subagent emitted a ledger (the orchestrator just reads it when present). This is deliberate +(a ledger-presence linter on free-text subagent output would be brittle), but it means the ledger's +value depends on the prompt templates keeping it salient; if subagents stop emitting it the +orchestrator simply falls back to inspecting the diff, as today. Accepted as a soft convention. + ### C3 — Artifact path-hygiene gate New `tests/no-machine-paths.sh`: greps the committed-artifact set (`docs/`, `decisions/`) for -operator-home absolute paths — `/Users//`, `/home//`, and literal `$HOME`/`~/` expanded -forms — and fails with the offending `file:line`. Narrow by construction: it targets home-rooted -machine paths, not all absolute paths (so `/healthz`, `/tmp/x`, `/etc/...` in legitimate examples -pass). Wired into `.github/workflows/skill-content-check.yml` (which already gates docs/skills) + -its `paths:` filter. The existing leak at `docs/testing.md:152` is fixed to a placeholder -(`/path/to/autodev` or `/...`) in the same PR so the gate is green on landing. +operator-home absolute paths and fails with the offending `file:line`. + +**Placeholder-aware regex (C-2 fix — the self-referential trap).** A feature *about* machine paths +must be able to *document* the forbidden pattern. The gate matches a home root followed by a **real +segment that begins with an alphanumeric**: `(/Users/|/home/)[A-Za-z0-9][A-Za-z0-9._-]*`. This +catches a real leak (home root + a literal username segment) but **ignores an angle-bracket placeholder** +(`/Users//...` — `<` is not alphanumeric, so no match) and ellipsis (`/Users/...`). The +**author convention** therefore is: to illustrate a machine path in any artifact, write it with an +angle-bracket placeholder segment (`/Users//...`, `/home//...`). Belt-and-suspenders: +any line containing the literal sentinel `path-hygiene-allow` (e.g. in an HTML comment) is skipped, +for the rare case a literal real-looking path must appear. Narrow by construction — targets +home-rooted paths only, so `/healthz`, `/tmp/x`, `/etc/...` pass untouched. + +**This design doc, its plan, and the retro all use `/Users//` placeholders** so the gate is +green on landing. The pre-existing real leak in `docs/testing.md` is rewritten to a placeholder in +the same PR. + +**CI wiring (I-2 fix).** A **dedicated** `.github/workflows/path-hygiene.yml` with **no `paths:` +filter** runs `tests/no-machine-paths.sh` on every push + PR. The existing +`skill-content-check.yml` filters on `skills/**`/`agents/**`, so a docs-only or decisions-only PR +that adds a leak would never trigger it — a false guarantee. A standalone always-on workflow closes +that hole permanently. Plus a one-line rule in the artifact-writing skills (`brainstorming`, `writing-plans`, `post-merge-retrospective`, `adversarial-design-review`, `recording-decisions`): committed -artifacts use repo-relative paths; never absolute machine paths. Local state logs -(`.claude/autodev-state/*`, gitignored) may hold absolute paths; the tracked -`.autodev/state/phase-progress.jsonl` stays repo-relative (already is). +artifacts use repo-relative paths; illustrate machine paths only with `` segments; +never a literal operator-home path. Local state logs (`.claude/autodev-state/*`, gitignored) may +hold absolute paths; the tracked `.autodev/state/phase-progress.jsonl` stays repo-relative +(already is). ## Global Design Guidance @@ -157,8 +200,11 @@ Proof obligations for the plan: - **A1:** `git rev-parse --git-common-dir` from a linked worktree's cwd returns the shared main `.git`, whose parent is the canonical root. *Load-bearing for C1; verified by the worktree test.* - **A2:** Hooks are always invoked with a cwd inside (or under) the target repo, so the resolver - can find the git common dir. If not (cwd outside any repo), fallback-to-cwd preserves today's - behavior — acceptable, no regression. + can find the git common dir. If not (cwd outside any repo / git absent), the C-1-fixed resolver + returns `$cwd` (empty `--git-common-dir` → fallback), preserving today's behavior — no regression. + *Edge (m-1):* inside a **bare** repo `--git-common-dir` returns `.`, so the resolver returns the + bare dir's parent rather than falling back; practical impact is nil (ADK hooks never run in a bare + repo) and it never hard-fails, so this is documented, not guarded. - **A3:** Sourcing a sibling `lib-autodev-paths.sh` via `dirname "$0"` works under `run-hook.cmd` (which invokes `bash ${SCRIPT_DIR}/`, so `$0` is the hook's own path). *Verified by the install-layout + a content test.* From a3269fbc8392574d3a2162f2816ef22fac2095cb Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Wed, 3 Jun 2026 14:16:52 -0400 Subject: [PATCH 03/20] =?UTF-8?q?design:=20PASS=20@=20adversarial=20cycle?= =?UTF-8?q?=202=20(converged);=202=20minors=20=E2=86=92=20plan=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...26-06-03-adk-path-canonicalization-design-review.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/plans/2026-06-03-adk-path-canonicalization-design-review.md b/docs/plans/2026-06-03-adk-path-canonicalization-design-review.md index 3bade00..0549086 100644 --- a/docs/plans/2026-06-03-adk-path-canonicalization-design-review.md +++ b/docs/plans/2026-06-03-adk-path-canonicalization-design-review.md @@ -40,3 +40,13 @@ 3. Placeholder-aware regex + `path-hygiene-allow` sentinel — **taken** (C-2). **Verdict reasoning:** Two Criticals (resolver `/`-return + self-referential gate trap) + three Importants (incomplete fallback guard, CI paths-filter false guarantee, retrofit list wrong by 3) all had concrete low-effort fixes, now in the design text. The git primitive (`--git-common-dir`) is correct; the main repo + linked-worktree path is verified. Re-run to confirm convergence. + +## Cycle 2 (convergence) — PASS + +All cycle-1 findings (C-1, C-2, I-1, I-2, I-3, m-1..m-4) verified genuinely reflected in the revised design text; resolver traced correct for all 4 cwd cases with no `set -u` hazard; gate regex empirically placeholder-aware; live grep confirms exactly 12 hooks; `declare -f` safe (all 12 hooks are `#!/usr/bin/env bash`). **Converged.** + +Two new Minors → plan-time implementation notes (not design blockers): +- **scope-lock-claim dead-code:** it references `session-locks.jsonl` in a read path; retrofit should anchor the read it actually performs and not add an unused `STATE_DIR`. Plan: apply the resolver only where the file path is genuinely used. +- **`local` in the lib:** `cwd`/`_gcd`/`_root` are function-global (no `local`). All consumers are bash so `local` is available; the lib will either use `local` or carry a comment that the names are intentionally global + assigned-before-read (so a future reorder can't break `set -u`). Plan decides; either is safe. + +**Final design verdict: PASS @ cycle 2.** Proceed to writing-plans. From 7a810559d9f74f2ef5aa14d151541e44e8d081a0 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Wed, 3 Jun 2026 14:19:00 -0400 Subject: [PATCH 04/20] =?UTF-8?q?plan:=20ADK=20path=20canonicalization=20(?= =?UTF-8?q?10=20tasks,=201=20PR=20=E2=86=92=20v6.5.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2026-06-03-adk-path-canonicalization.md | 338 ++++++++++++++++++ 1 file changed, 338 insertions(+) create mode 100644 docs/plans/2026-06-03-adk-path-canonicalization.md diff --git a/docs/plans/2026-06-03-adk-path-canonicalization.md b/docs/plans/2026-06-03-adk-path-canonicalization.md new file mode 100644 index 0000000..bc1b6fd --- /dev/null +++ b/docs/plans/2026-06-03-adk-path-canonicalization.md @@ -0,0 +1,338 @@ +# ADK Path Canonicalization Implementation Plan + +> **For the implementing agent:** REQUIRED SUB-SKILL: Use autodev:executing-plans to implement this plan task-by-task. + +**Goal:** Make all ADK state writes resolve to one canonical per-repo location (worktree-safe), have subagents report where they wrote, and forbid operator-home paths in committed artifacts — fixing the #70 worktree-fragmentation residual. + +**Architecture:** One shared bash resolver (`hooks/lib-autodev-paths.sh`, git-common-dir-anchored with cwd fallback + lib-missing degradation) sourced by the 12 state-writing hooks; a subagent `Writes:` ledger convention; a placeholder-aware path-hygiene CI gate in its own always-run workflow. TDD via a real temp `git worktree` fixture proving the resolver. v6.5.0 version bump. + +**Tech Stack:** Bash (hooks + tests), Markdown (skills/agents), GitHub Actions, `scripts/bump-version.sh` + `tests/version-check.sh`. + +**Base branch:** main + +--- + +## Scope Manifest + +**PR Count:** 1 +**Tasks:** 10 +**Estimated Lines of Change:** ~420 (1 lib + 12 hook retrofits + 2 tests + 1 workflow + 6 skill/agent edits + version bump) + +**Out of scope:** +- Heuristic/ML path detection — the gate is a narrow home-rooted-path grep (placeholder-aware), nothing more. +- A CI validator that subagents actually emit the `Writes:` ledger (C2 is a soft convention by design; m-4). +- Gitignoring `.autodev/state/phase-progress.jsonl` (intentionally tracked; kept repo-relative). +- Bootstrapping `docs/design-guidance.md` (recurring follow-up; not this PR). +- Retrofitting `scope-lock-apply` / `scope-lock-publish` / `posttool-pr-created` (verified: no state-dir reference). +- Migrating the 2026-05-31 / 2026-06-03 pre-existing review reports. + +**PR Grouping:** + +| PR # | Title | Tasks | Branch | +|------|-------|-------|--------| +| 1 | ADK path canonicalization + write-location transparency + artifact hygiene (v6.5.0) | Task 1–10 | feat/adk-path-canonicalization | + +**Status:** Draft + +--- + +### Task 1: Failing tests — resolver behavior + hook-wiring + degradation + +**Change class:** Hook/test. Verification: the test (RED now; GREEN after Tasks 2–3). + +**Files:** +- Create: `tests/adk-path-canonicalization.sh` + +**Step 1: Write the failing test.** Mirror `tests/hook-contracts.sh` style (`pass()/fail()`, `failures` counter, non-zero exit). Three groups: + +```bash +#!/usr/bin/env bash +# tests/adk-path-canonicalization.sh — proves the canonical ADK state-path resolver +# and that all 12 state-writing hooks adopt it. (#70 residual; v6.5.0) +set -uo pipefail +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/.." && pwd)" +LIB="$ROOT/hooks/lib-autodev-paths.sh" +failures=0 +pass(){ printf 'PASS: %s\n' "$1"; } +fail(){ printf 'FAIL: %s\n' "$1" >&2; failures=$((failures+1)); } + +# --- Group A: resolver behavior against a REAL temp git + linked worktree --- +if [ -f "$LIB" ]; then + . "$LIB" + tmp="$(mktemp -d)" + ( cd "$tmp" && git init -q main && cd main && git -c user.email=a@b -c user.name=x commit -q --allow-empty -m init \ + && git worktree add -q ../wt >/dev/null 2>&1 ) + main_root="$(cd "$tmp/main" && pwd)" + # (a) from main checkout -> main root + [ "$(autodev_repo_root "$tmp/main")" = "$main_root" ] \ + && pass "resolver: main checkout -> main root" || fail "resolver main: got $(autodev_repo_root "$tmp/main")" + # (b) from linked worktree -> SAME main root (the load-bearing claim) + [ "$(autodev_repo_root "$tmp/wt")" = "$main_root" ] \ + && pass "resolver: linked worktree -> main root (shared)" || fail "resolver worktree: got $(autodev_repo_root "$tmp/wt")" + # (c) non-git dir -> cwd fallback (C-1: NOT '/') + ngt="$(mktemp -d)"; [ "$(autodev_repo_root "$ngt")" = "$ngt" ] \ + && pass "resolver: non-git dir -> cwd fallback" || fail "resolver non-git: got $(autodev_repo_root "$ngt") (want $ngt)" + # (d) env override wins + [ "$(AUTODEV_STATE_ROOT=/tmp/override autodev_repo_root "$tmp/main")" = "/tmp/override" ] \ + && pass "resolver: AUTODEV_STATE_ROOT override" || fail "resolver override broken" + rm -rf "$tmp" "$ngt" +else + fail "lib missing: $LIB" +fi + +# --- Group B: all 12 state-writing hooks source the lib + guard the function --- +HOOKS="completion-claim-guard pre-compact-snapshot pre-tool-scope-guard pretool-demo-fidelity-guard pretool-pr-review-reminder prompt-strict-interpretation record-activity scope-lock-abandon scope-lock-claim scope-lock-complete session-start subagent-scope-guard" +for h in $HOOKS; do + f="$ROOT/hooks/$h" + if grep -q "lib-autodev-paths.sh" "$f" && grep -q "declare -f autodev_repo_root" "$f"; then + pass "hook wired: $h" + else + fail "hook NOT wired (no lib source + declare -f guard): $h" + fi +done + +# --- Group C: lib-missing degradation — a hook with the lib hidden still emits valid output --- +# record-activity is the simplest writer: feed it a Skill payload with a bogus cwd, lib hidden, +# and assert it does NOT crash (exit !=2/127) and writes under the cwd fallback. +tmpd="$(mktemp -d)"; mkdir -p "$tmpd/.git" # make it look git-less enough to fallback +payload='{"tool_name":"Skill","tool_input":{"skill":"autodev:x"},"cwd":"'"$tmpd"'"}' +LIBBAK=""; if [ -f "$LIB" ]; then LIBBAK="$(mktemp)"; cp "$LIB" "$LIBBAK"; fi +# Don't actually delete the real lib; instead run the hook with a PATH/source that can't find it: +# simulate by copying record-activity to a temp dir WITHOUT the sibling lib. +sandbox="$(mktemp -d)"; cp "$ROOT/hooks/record-activity" "$sandbox/record-activity" +out_rc=0; printf '%s' "$payload" | bash "$sandbox/record-activity" >/dev/null 2>&1 || out_rc=$? +# 127/2 would indicate the missing-function crash; 0 or 1 (benign) is acceptable degradation. +if [ "$out_rc" != "127" ] && [ "$out_rc" != "2" ]; then + pass "degradation: record-activity survives missing lib (rc=$out_rc)" +else + fail "degradation: record-activity crashed without lib (rc=$out_rc)" +fi +rm -rf "$tmpd" "$sandbox"; [ -n "$LIBBAK" ] && rm -f "$LIBBAK" + +echo ""; echo "Results: $failures failure(s)"; [ "$failures" -eq 0 ] +``` + +**Step 2: Run, verify RED.** `bash tests/adk-path-canonicalization.sh` → FAILs (lib missing, hooks unwired), exit 1. + +**Step 3: Commit (red).** `chmod +x` + `git add` + commit `test: ADK path canonicalization resolver+wiring guard [red]`. + +--- + +### Task 2: Canonical resolver lib + +**Change class:** Hook/library. Verification: Task-1 Group A (resolver) passes. + +**Files:** +- Create: `hooks/lib-autodev-paths.sh` + +**Step 1:** Write the resolver exactly as the design's C1 block (with the C-1 null-guard). Use `local` for `cwd`/`_gcd`/`_root` (all consumers are bash) to avoid scope leakage, with a header comment that the function must stay `set -u`-safe (assign before read): + +```sh +#!/usr/bin/env bash +# lib-autodev-paths.sh — canonical ADK state-root resolver, sourced by state-writing hooks. +# autodev_repo_root -> canonical repo root (shared across worktrees, survives worktree removal). +# set -u safe: every var is assigned before any read. Sourced; uses `local` (all callers are bash). +autodev_repo_root() { + local cwd="${1:-$PWD}" _gcd="" _root="" + if [ -n "${AUTODEV_STATE_ROOT:-}" ]; then printf '%s\n' "$AUTODEV_STATE_ROOT"; return 0; fi + _gcd="$(cd "$cwd" 2>/dev/null && git rev-parse --git-common-dir 2>/dev/null || true)" + [ -n "$_gcd" ] && _root="$(cd "$cwd" 2>/dev/null && cd "$_gcd/.." 2>/dev/null && pwd || true)" + if [ -n "$_root" ]; then printf '%s\n' "$_root"; else printf '%s\n' "$cwd"; fi +} +``` + +**Step 2: Run** `bash tests/adk-path-canonicalization.sh` → Group A PASS (main, worktree, non-git, override). + +**Step 3: Commit.** `git add hooks/lib-autodev-paths.sh tests/adk-path-canonicalization.sh && git commit -m "feat(hooks): canonical ADK state-root resolver (git-common-dir anchored)"` + +--- + +### Task 3: Retrofit the 12 state-writing hooks + +**Change class:** Hook. Verification: Task-1 Group B (all 12 wired) + Group C (degradation) pass; `tests/hook-contracts.sh` still green (no behavior regression). + +**Files (Modify, all in `hooks/`):** `completion-claim-guard`, `pre-compact-snapshot`, `pre-tool-scope-guard`, `pretool-demo-fidelity-guard`, `pretool-pr-review-reminder`, `prompt-strict-interpretation`, `record-activity`, `scope-lock-abandon`, `scope-lock-claim`, `scope-lock-complete`, `session-start`, `subagent-scope-guard`. + +**Step 0 (guard against list drift):** run `grep -rlE 'autodev-state|\.autodev/state' hooks/ | sort` — confirm it returns exactly these 12. If it differs, STOP and reconcile before editing. + +**Step 1:** In each hook, immediately after its `cwd_dir` is determined (the line `[ -z "$cwd_dir" ] && cwd_dir="${PWD}"` or equivalent), insert: +```sh +. "$(cd -- "$(dirname -- "${BASH_SOURCE[0]:-$0}")" && pwd)/lib-autodev-paths.sh" 2>/dev/null || true +declare -f autodev_repo_root >/dev/null 2>&1 || autodev_repo_root() { printf '%s\n' "${1:-$PWD}"; } +ADK_ROOT="$(autodev_repo_root "$cwd_dir")" +``` +Then replace every `${cwd_dir}/.claude/autodev-state` → `${ADK_ROOT}/.claude/autodev-state` and every `${cwd_dir}/.autodev/state` → `${ADK_ROOT}/.autodev/state` **in that hook**. Leave non-state uses of `cwd_dir` (e.g. `${cwd_dir}/docs/plans`, repo-content reads) UNCHANGED — those legitimately want the working dir, not the canonical root. + +**Special cases (per design m-3 / cycle-2 minors):** +- `scope-lock-complete`, `scope-lock-abandon`: they compute `repo_root` from the plan path and take `$PWD`. Replace that `repo_root=$(cd "${plan_dir}/../.." && pwd)` derivation with `ADK_ROOT="$(autodev_repo_root "$PWD")"` (source the lib first). This intentionally switches worktree→main root for state pruning. +- `scope-lock-claim`: it only **reads** `session-locks.jsonl` for verification (write is delegated to `pre-tool-scope-guard`). Anchor the read path to `ADK_ROOT` too (so it reads the same canonical file), but do not add an unused `STATE_DIR` (cycle-2 minor). + +**Step 2: Verify.** `bash tests/adk-path-canonicalization.sh` → Group B + C PASS. `bash tests/hook-contracts.sh` → exit 0 (no contract regression). `bash tests/hook-stdout-discipline.sh` → exit 0. + +**Step 3: Commit.** `git add hooks/ && git commit -m "refactor(hooks): all 12 state writers resolve canonical root via shared lib (#70 residual)"` + +**Rollback:** revert this commit → hooks fall back to per-hook cwd-scoping (prior behavior); no state migration needed. + +--- + +### Task 4: Retro reads the canonical activation log + +**Change class:** Documentation/skill-content. Verification: content assertion + `skill-content-grep.sh`. + +**Files:** Modify `skills/post-merge-retrospective/SKILL.md` (Step 5 + Reads bullet). + +**Step 1:** Add a sentence to Step 5 + the `**Reads:**` bullet: the activation log lives at the **canonical repo root** (`git rev-parse --git-common-dir`'s parent — shared across worktrees, survives worktree cleanup), not the cwd; a worktree-executed pipeline writes there. If reading from a worktree checkout, resolve the same root. This closes the v6.4.0 retro's #70 residual (the retro that *surfaced* this). + +**Step 2: Verify.** `bash tests/skill-content-grep.sh` → exit 0. `grep -q "git-common-dir\|canonical repo root" skills/post-merge-retrospective/SKILL.md`. + +**Step 3: Commit.** `git commit -m "docs(retro): read activation log from canonical repo root (#70 residual)"` + +--- + +### Task 5: Subagent write-location ledger (C2) + +**Change class:** Documentation/skill-content. Verification: content assertion + `skill-content-grep.sh`. + +**Files:** Modify `agents/team-conventions.md`, `skills/subagent-driven-development/implementer-prompt.md`, `spec-reviewer-prompt.md`, `code-quality-reviewer-prompt.md`, and `skills/subagent-driven-development/SKILL.md`. + +**Step 1:** Add a `Writes:` ledger convention: every subagent ends its final message with a `Writes:` section — one line per file created/modified as a **repo-relative path**, plus `OUT-OF-TREE: ` for any write outside the expected repo/worktree, so the orchestrator can verify and relocate. Add the matching instruction line to each of the 3 prompt templates and a short subsection in team-conventions.md + a pointer in the SKILL. + +**Step 2: Verify.** `bash tests/skill-content-grep.sh` → exit 0. `grep -lq "Writes:" agents/team-conventions.md skills/subagent-driven-development/implementer-prompt.md`. + +**Step 3: Commit.** `git commit -m "docs(subagents): require a repo-relative Writes: ledger from every subagent (C2)"` + +--- + +### Task 6: Path-hygiene gate + fix existing leak (C3) + +**Change class:** Test/Documentation. Verification: the gate is RED on a seeded leak, GREEN on placeholders + the real tree (after the testing.md fix). + +**Files:** +- Create: `tests/no-machine-paths.sh` +- Modify: `docs/testing.md` (fix the existing operator-home leak → placeholder) + +**Step 1: Write the gate** (placeholder-aware, per design C3): +```bash +#!/usr/bin/env bash +# tests/no-machine-paths.sh — forbid operator-home absolute paths in committed artifacts. +# Catches a real leak (/Users//x) but IGNORES segments and ellipsis, +# so artifacts that DOCUMENT the pattern (this feature's own docs) pass. Lines containing the +# sentinel `path-hygiene-allow` are skipped. Scans docs/ and decisions/. +set -uo pipefail +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/.." && pwd)" +pattern='(/Users/|/home/)[A-Za-z0-9][A-Za-z0-9._-]*' +hits=0 +while IFS= read -r f; do + while IFS=: read -r line content; do + case "$content" in (*path-hygiene-allow*) continue ;; esac + printf 'LEAK: %s:%s: %s\n' "${f#$ROOT/}" "$line" "$content" >&2 + hits=$((hits+1)) + done < <(grep -nE "$pattern" "$f" 2>/dev/null || true) +done < <(find "$ROOT/docs" "$ROOT/decisions" -type f \( -name '*.md' -o -name '*.txt' \) 2>/dev/null) +if [ "$hits" -eq 0 ]; then echo "PASS: no operator-home machine paths in committed artifacts."; else + echo "FAIL: $hits machine-path leak(s) in committed artifacts." >&2; fi +[ "$hits" -eq 0 ] +``` + +**Step 2: Prove RED on a seeded leak + GREEN on placeholder (revert-restore):** +```bash +echo '/Users/realname/secret' > docs/_leak_probe.md +bash tests/no-machine-paths.sh; test $? -ne 0 && echo "OK: catches real leak" +echo '/Users//x' > docs/_leak_probe.md # placeholder +bash tests/no-machine-paths.sh; # this probe line alone is fine, but other real leaks may remain +rm -f docs/_leak_probe.md +``` +Expected: real-path probe → FAIL (exit 1); placeholder probe → not the cause of failure. + +**Step 3: Fix the existing leak.** Edit `docs/testing.md` (the `/Users//...` example line) → replace the operator-home segment with `/Users//...` placeholder (angle-bracket) or `/...`. + +**Step 4: Verify GREEN on the real tree.** `bash tests/no-machine-paths.sh` → `PASS` (exit 0). (The design + plan + review docs already use placeholders.) + +**Step 5: Commit.** `git add tests/no-machine-paths.sh docs/testing.md && git commit -m "test(hygiene): forbid operator-home paths in artifacts; fix docs/testing.md leak (C3)"` + +--- + +### Task 7: Dedicated path-hygiene CI workflow (C3 / I-2) + +**Change class:** Hook/trigger (CI). Verification: YAML valid; runs the gate with no `paths:` filter. + +**Files:** Create `.github/workflows/path-hygiene.yml`. + +**Step 1:** +```yaml +name: Path Hygiene +on: + push: + pull_request: +permissions: + contents: read +jobs: + path-hygiene: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: No operator-home paths in committed artifacts + run: bash tests/no-machine-paths.sh +``` +(No `paths:` filter — always runs, so a docs-only leak PR can't bypass it; I-2.) + +**Step 2: Verify.** `python3 -c "import yaml,sys; yaml.safe_load(open('.github/workflows/path-hygiene.yml'))"` → no error. `bash tests/no-machine-paths.sh` → exit 0. + +**Step 3: Commit.** `git commit -m "ci: dedicated always-on path-hygiene workflow (C3)"` + +--- + +### Task 8: Artifact path-hygiene skill rule (C3) + +**Change class:** Documentation/skill-content. Verification: content assertion + `skill-content-grep.sh`. + +**Files:** Modify `skills/brainstorming/SKILL.md`, `skills/writing-plans/SKILL.md`, `skills/post-merge-retrospective/SKILL.md`, `skills/adversarial-design-review/SKILL.md`, `skills/recording-decisions/SKILL.md`. + +**Step 1:** Add ONE concise rule line to each (in the artifact-writing/documentation section): "Committed artifacts use repo-relative paths; illustrate machine paths only with `` segments (e.g. `/Users//…`); never a literal operator-home path. Enforced by `tests/no-machine-paths.sh`." Keep it to one line per skill (anti-bloat). + +**Step 2: Verify.** `bash tests/skill-content-grep.sh` → exit 0. `bash tests/skill-cross-refs.sh` → exit 0. Confirm each of the 5 skills contains "repo-relative". + +**Step 3: Commit.** `git commit -m "docs(skills): repo-relative-paths rule for committed artifacts (C3)"` + +--- + +### Task 9: Full verification + wire resolver test into CI + +**Change class:** Hook/trigger (CI) + verification. + +**Files:** Modify `.github/workflows/skill-content-check.yml` (add `tests/adk-path-canonicalization.sh` step + `hooks/**` path so hook changes trigger it). + +**Step 1:** Add `hooks/**` and `tests/adk-path-canonicalization.sh` to the workflow `paths` (push + PR), and a step `run: bash tests/adk-path-canonicalization.sh`. + +**Step 2: Run the FULL local gate — all exit 0:** +```bash +bash tests/adk-path-canonicalization.sh # Results: 0 failure(s) +bash tests/no-machine-paths.sh # PASS +bash tests/hook-contracts.sh # exit 0 (no hook regression) +bash tests/hook-stdout-discipline.sh # exit 0 +bash tests/skill-content-grep.sh # exit 0 +bash tests/skill-cross-refs.sh # exit 0 +``` +Expected: every command exits 0. If any hook emits a host-token leak, fix it (`` block). + +**Step 3: Commit.** `git commit -m "ci: run ADK path canonicalization test on hook changes"` + +--- + +### Task 10: Version bump → v6.5.0 + +**Change class:** Version pin (release). Verification: `tests/version-check.sh`. **Rollback: revert merge + re-tag v6.4.0; resolver computed at runtime, no migration.** + +**Files (via script):** `.claude-plugin/plugin.json`, `.claude-plugin/marketplace.json`, `.cursor-plugin/plugin.json`. + +**Step 1:** `bash scripts/bump-version.sh 6.5.0` +**Step 2:** `bash tests/version-check.sh` → exit 0 (all three = 6.5.0). +**Step 3:** `git add .claude-plugin/plugin.json .claude-plugin/marketplace.json .cursor-plugin/plugin.json && git commit -m "chore(release): bump to v6.5.0"` + +--- + +## Verification Summary (whole-PR) +All green before PR: `adk-path-canonicalization.sh`, `no-machine-paths.sh`, `hook-contracts.sh`, `hook-stdout-discipline.sh`, `skill-content-grep.sh`, `skill-cross-refs.sh`, `version-check.sh`, `plan-scope-check --verify-lock`. Step 1e dogfood: this PR commits design/plan/review/skill docs → emit `Doc-reconciliation:` line in the PR body (and confirm no real machine paths — `no-machine-paths.sh` is the mechanical proof). + +## Rollback (whole-PR) +Revert the squash-merge commit + re-tag v6.4.0. The canonical root is computed at runtime; reverting restores cwd-scoping. No data/state migration; any state already at a canonical path is harmless. Per-task rollback notes on Task 3 (hooks) + Task 10 (version). From d21f05e4d820c32496e438c510f9b886da5ca0d8 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Wed, 3 Jun 2026 14:19:42 -0400 Subject: [PATCH 05/20] plan: enumerate PR-grouping tasks + keep Task-6 probe gate-clean (printf) --- docs/plans/2026-06-03-adk-path-canonicalization.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/plans/2026-06-03-adk-path-canonicalization.md b/docs/plans/2026-06-03-adk-path-canonicalization.md index bc1b6fd..9297300 100644 --- a/docs/plans/2026-06-03-adk-path-canonicalization.md +++ b/docs/plans/2026-06-03-adk-path-canonicalization.md @@ -30,7 +30,7 @@ | PR # | Title | Tasks | Branch | |------|-------|-------|--------| -| 1 | ADK path canonicalization + write-location transparency + artifact hygiene (v6.5.0) | Task 1–10 | feat/adk-path-canonicalization | +| 1 | ADK path canonicalization + write-location transparency + artifact hygiene (v6.5.0) | Task 1, Task 2, Task 3, Task 4, Task 5, Task 6, Task 7, Task 8, Task 9, Task 10 | feat/adk-path-canonicalization | **Status:** Draft @@ -234,15 +234,16 @@ if [ "$hits" -eq 0 ]; then echo "PASS: no operator-home machine paths in committ [ "$hits" -eq 0 ] ``` -**Step 2: Prove RED on a seeded leak + GREEN on placeholder (revert-restore):** +**Step 2: Prove RED on a seeded leak + GREEN on placeholder (revert-restore).** Build the probe +path with `printf` so this plan file itself stays gate-clean (the literal never appears here): ```bash -echo '/Users/realname/secret' > docs/_leak_probe.md +printf '/Users/%s/secret\n' realuser > docs/_leak_probe.md # real-looking at runtime bash tests/no-machine-paths.sh; test $? -ne 0 && echo "OK: catches real leak" -echo '/Users//x' > docs/_leak_probe.md # placeholder -bash tests/no-machine-paths.sh; # this probe line alone is fine, but other real leaks may remain +printf '/Users//x\n' > docs/_leak_probe.md # angle-bracket placeholder +bash tests/no-machine-paths.sh; echo "placeholder rc=$?" # probe line itself is ignored rm -f docs/_leak_probe.md ``` -Expected: real-path probe → FAIL (exit 1); placeholder probe → not the cause of failure. +Expected: real-path probe → FAIL (exit 1); placeholder probe → ignored (not a leak). **Step 3: Fix the existing leak.** Edit `docs/testing.md` (the `/Users//...` example line) → replace the operator-home segment with `/Users//...` placeholder (angle-bracket) or `/...`. From 16234fffe444f0e9ce03f170844916360bbd1bf7 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Wed, 3 Jun 2026 14:32:30 -0400 Subject: [PATCH 06/20] plan: revise per plan-phase adversarial (1C+2I+4m): C-1 test fix, drop scope-lock-claim (11 hooks), session-start/pre-compact anchors, behavioral degradation test --- ...adk-path-canonicalization-design-review.md | 2 +- ...-06-03-adk-path-canonicalization-design.md | 10 +-- ...3-adk-path-canonicalization-plan-review.md | 32 +++++++++ .../2026-06-03-adk-path-canonicalization.md | 72 +++++++++++-------- 4 files changed, 81 insertions(+), 35 deletions(-) create mode 100644 docs/plans/2026-06-03-adk-path-canonicalization-plan-review.md diff --git a/docs/plans/2026-06-03-adk-path-canonicalization-design-review.md b/docs/plans/2026-06-03-adk-path-canonicalization-design-review.md index 0549086..5c650c9 100644 --- a/docs/plans/2026-06-03-adk-path-canonicalization-design-review.md +++ b/docs/plans/2026-06-03-adk-path-canonicalization-design-review.md @@ -9,7 +9,7 @@ | id | sev | class | issue | resolution | |---|---|---|---|---| | C-1 | Critical | Correctness | Resolver `cd "$(git rev-parse --git-common-dir)/.."` returns `/` (not `$cwd`) when git absent / non-git dir (empty substitution → `cd "/.."`). Reproduced: from `/tmp` → `/`. Violates the "degrade to today's behavior" invariant. | Resolver rewritten: capture `_gcd` first, guard `[ -n "$_gcd" ]` before the `cd`, else fall back to `$cwd`. | -| C-2 | Critical | Self-referential trap | The gate scans `docs/` for `/Users//`; the design doc + plan + retro for THIS feature must *document* that pattern, so a blanket grep fails on its own artifacts (design line had `/Users/jon/...`). | Gate regex made **placeholder-aware**: `(/Users/\|/home/)[A-Za-z0-9][A-Za-z0-9._-]*` matches a real username segment but ignores `` and ellipsis; author convention = illustrate with `/Users//`; plus a `path-hygiene-allow` line sentinel. All this feature's artifacts sanitized to placeholders (verified gate-clean). | +| C-2 | Critical | Self-referential trap | The gate scans `docs/` for a home-rooted path; the design doc + plan + retro for THIS feature must *document* that pattern, so a blanket grep fails on its own artifacts (the design originally had a literal operator-home example). | Gate regex made **placeholder-aware**: `(/Users/\|/home/)[A-Za-z0-9][A-Za-z0-9._-]*` matches a real username segment but ignores `` and ellipsis; author convention = illustrate with `/Users//`; plus a `path-hygiene-allow` line sentinel. All this feature's artifacts sanitized to placeholders (verified gate-clean). | | I-1 | Important | Robustness | Inline fallback `source \|\| define-fn` only fires on source *failure*; a lib that sources OK but lacks the function → exit 127 under `set -euo pipefail` kills the hook. | Replaced with post-source `declare -f autodev_repo_root >/dev/null 2>&1 \|\| autodev_repo_root(){...}` — covers missing-lib AND missing-function. | | I-2 | Important | CI wiring gap | `skill-content-check.yml` `paths:` filter is `skills/**`/`agents/**` — a docs-only/decisions-only leak PR would never trigger the gate (false guarantee). | Gate moved to a **dedicated `path-hygiene.yml`** with NO `paths:` filter → always runs on push + PR. | | I-3 | Important | Multi-component / accuracy | Retrofit list wrong by 3: `pretool-demo-fidelity-guard` MISSING (has state ref @ line 92); `posttool-pr-created` + `scope-lock-apply` listed but have NO state ref. | Replaced with the authoritative 12 from `grep -rlE 'autodev-state\|\.autodev/state' hooks/`; excluded-list documented; plan re-runs the grep as a guard. | diff --git a/docs/plans/2026-06-03-adk-path-canonicalization-design.md b/docs/plans/2026-06-03-adk-path-canonicalization-design.md index 35c2b23..ceaef15 100644 --- a/docs/plans/2026-06-03-adk-path-canonicalization-design.md +++ b/docs/plans/2026-06-03-adk-path-canonicalization-design.md @@ -92,10 +92,12 @@ Uses `${BASH_SOURCE[0]:-$0}` to match the repo's existing `SCRIPT_DIR` conventio `record-activity`, `scope-lock-abandon`, `scope-lock-claim`, `scope-lock-complete`, `session-start`, `subagent-scope-guard`. -Explicitly **excluded** (verified no state reference): `scope-lock-apply` and `scope-lock-publish` -(write only the `.scope-lock` sidecar next to the plan — not state dirs) and `posttool-pr-created` -(a PR-creation reminder, no state). The plan re-runs the grep as its first step and fails if the -list drifts. +Explicitly **excluded**: `scope-lock-apply` and `scope-lock-publish` (write only the `.scope-lock` +sidecar next to the plan — not state dirs), `posttool-pr-created` (a PR-creation reminder, no +state), and `scope-lock-claim` (its single `autodev-state` mention is a **doc comment** — no +runtime state I/O; the session-lock write is delegated to `pre-tool-scope-guard`). So the grep +returns 12 paths but only **11** are real state writers to retrofit. The plan re-runs the grep as +its first step and treats `scope-lock-claim` as the known comment-only exclusion. **Behavioral-change note (m-3):** `scope-lock-complete` and `scope-lock-abandon` currently derive `repo_root` from the **plan path** (`cd "${plan_dir}/../.." && pwd`, assuming `docs/plans/` depth); diff --git a/docs/plans/2026-06-03-adk-path-canonicalization-plan-review.md b/docs/plans/2026-06-03-adk-path-canonicalization-plan-review.md new file mode 100644 index 0000000..7bced10 --- /dev/null +++ b/docs/plans/2026-06-03-adk-path-canonicalization-plan-review.md @@ -0,0 +1,32 @@ +# ADK Path Canonicalization — Plan-Phase Adversarial Review + +**Phase:** plan +**Artifact:** `docs/plans/2026-06-03-adk-path-canonicalization.md` +**Status:** FAIL → revised (all findings resolved); re-run pending + +## Findings (cycle 1) + +| id | sev | class | issue | resolution | +|---|---|---|---|---| +| C-1 | Critical | Verification-class / regression | Retrofitting `scope-lock-complete`/`-abandon` to `autodev_repo_root "$PWD"` breaks `hook-contracts.sh`: its **bare** invocations at lines 641 + 707 run from `$REPO_ROOT` (a real git checkout) with a `$tmp` plan, so post-retrofit state ops land in the real checkout → every pruning assertion fails. Plan claimed `hook-contracts.sh` stays green but gave no test-fix instruction. | Task 3 **Step 4** added: rewrite lines 641 + 707 to `( cd "$tmp" && "$REPO_ROOT/hooks/scope-lock-complete" docs/plans/example.md … )`, matching the already-correct cd-wrapped siblings at 746/790/833. `$tmp` is non-git → resolver falls back to `$tmp`, restoring expected behavior. Verified: 641/707 are the only two bare invocations; all abandon tests + 746/790/833 already cd. | +| I-1 | Important | Repo-precedent / wrong instruction | (a) `scope-lock-claim` has NO `cwd_dir` and no runtime state I/O — its grep hit is a comment (line 11). Retrofitting it is wrong. (b) `session-start` uses a two-step init (`cwd_dir="${PWD}"` @38, payload override @45); the generic "after `[ -z "$cwd_dir" ]`" anchor doesn't exist and inserting before L45 derives the root from `$PWD` not the payload `.cwd`. | (a) `scope-lock-claim` **dropped** from the retrofit → **11 hooks**; Task 1 Group B list, Task 3 file list, Step 0 grep note, design retrofit list, and Out-of-scope all updated. (b) Task 3 gives an explicit per-hook anchor table; `session-start` = "after line 45 (`[ -n "$cwd_from_hook" ] && cwd_dir="$cwd_from_hook"`)". | +| I-2 | Important | Missing failure mode | `pre-compact-snapshot` reads+writes `reminder_marker="${cwd_dir}/.claude/autodev-state/pr-reminder-seen"` at L43–55 **before** its common-case `noop_json` early-exit; the special-cases note omitted it, risking a split state (marker cwd-scoped, lock snapshot canonical) that breaks pr-reminder dedup across worktrees. | Task 3 special-case for `pre-compact-snapshot` added: insert (after L33) precedes L43 so `ADK_ROOT` is in scope; the `reminder_marker` substitution must be included. | +| m-1 | Minor | Existence/validity | Group C degradation test was vacuous — `record-activity` exits 0 regardless, so rc≠127 proves nothing. | Group C strengthened to a **behavioral** proof: run lib-hidden record-activity with a non-git cwd, assert it wrote `degrade-probe` to `$cwd/.claude/autodev-state/in-progress.jsonl` (fallback fired) AND didn't crash. | +| m-2 | Minor | Accuracy | Plan said "all artifacts use placeholders" but the design-review doc passed only via a coincidental `path-hygiene-allow` substring. | Design-review doc rephrased to remove the literal operator-home example → genuinely gate-clean (verified, no sentinel reliance). | +| m-3 | Minor | CI wiring ambiguity | Task 9 "add to the workflow paths" was ambiguous (path-trigger vs run-step). | Task 9 split into (a) add to both `push.paths`+`pull_request.paths` AND (b) a `run:` step. | +| m-4 | Minor | Portability | `git init -q main` needs git ≥2.28. | Task 1 fixture uses `mkdir main && (cd main && git init -q …)` — portable. | + +## Bug-class scan transcript +| Class | Result | Note | +|---|---|---| +| Assumptions / Verification-class / Multi-component | Finding (C-1) | $PWD-resolution vs test invoking from $REPO_ROOT — fixed via test cd. | +| Repo-precedent | Finding (I-1) | scope-lock-claim comment-only; session-start non-standard init. | +| Missing failure modes | Finding (I-2) | pre-compact reminder_marker pre-early-exit. | +| Existence/runtime-validity | Finding (m-1) | vacuous degradation test → behavioral proof. | +| YAGNI / Security / Infra / Rollback / Simpler-alt / User-intent / Over-decomp / Hidden-serial / Missing-rollback-wiring | Clean | No new scope; dedicated workflow sound; rollback notes present; 10 tasks appropriate grain; single PR so no mid-PR red ships. | + +## Options taken +1. Test-cd fix for the two bare invocations (vs a `--repo-root` flag) — **taken** (minimal, matches existing sibling pattern). +2. Behavioral degradation assertion — **taken** (m-1). + +**Verdict reasoning:** One Critical (a concrete `hook-contracts.sh` regression the plan didn't instruct to fix) + two Importants (wrong retrofit target `scope-lock-claim`; non-standard `session-start` anchor + missing `pre-compact-snapshot` special case) + four Minors — all verified against the real hook code, all with narrow fixes now in the plan text. The resolver design and the worktree fixture proof are sound. Re-run to confirm convergence. diff --git a/docs/plans/2026-06-03-adk-path-canonicalization.md b/docs/plans/2026-06-03-adk-path-canonicalization.md index 9297300..d3cf126 100644 --- a/docs/plans/2026-06-03-adk-path-canonicalization.md +++ b/docs/plans/2026-06-03-adk-path-canonicalization.md @@ -23,7 +23,7 @@ - A CI validator that subagents actually emit the `Writes:` ledger (C2 is a soft convention by design; m-4). - Gitignoring `.autodev/state/phase-progress.jsonl` (intentionally tracked; kept repo-relative). - Bootstrapping `docs/design-guidance.md` (recurring follow-up; not this PR). -- Retrofitting `scope-lock-apply` / `scope-lock-publish` / `posttool-pr-created` (verified: no state-dir reference). +- Retrofitting `scope-lock-apply` / `scope-lock-publish` / `posttool-pr-created` (verified: no state-dir reference) and `scope-lock-claim` (its only `autodev-state` mention is a doc comment — no runtime state I/O). - Migrating the 2026-05-31 / 2026-06-03 pre-existing review reports. **PR Grouping:** @@ -60,7 +60,9 @@ fail(){ printf 'FAIL: %s\n' "$1" >&2; failures=$((failures+1)); } if [ -f "$LIB" ]; then . "$LIB" tmp="$(mktemp -d)" - ( cd "$tmp" && git init -q main && cd main && git -c user.email=a@b -c user.name=x commit -q --allow-empty -m init \ + # portable git init (avoid `git init ` which needs git>=2.28): mkdir + `git -C` + mkdir -p "$tmp/main" + ( cd "$tmp/main" && git init -q && git -c user.email=a@b -c user.name=x commit -q --allow-empty -m init \ && git worktree add -q ../wt >/dev/null 2>&1 ) main_root="$(cd "$tmp/main" && pwd)" # (a) from main checkout -> main root @@ -80,8 +82,9 @@ else fail "lib missing: $LIB" fi -# --- Group B: all 12 state-writing hooks source the lib + guard the function --- -HOOKS="completion-claim-guard pre-compact-snapshot pre-tool-scope-guard pretool-demo-fidelity-guard pretool-pr-review-reminder prompt-strict-interpretation record-activity scope-lock-abandon scope-lock-claim scope-lock-complete session-start subagent-scope-guard" +# --- Group B: all 11 state-WRITING hooks source the lib + guard the function --- +# (scope-lock-claim is EXCLUDED — its only autodev-state mention is a comment, no runtime state I/O.) +HOOKS="completion-claim-guard pre-compact-snapshot pre-tool-scope-guard pretool-demo-fidelity-guard pretool-pr-review-reminder prompt-strict-interpretation record-activity scope-lock-abandon scope-lock-complete session-start subagent-scope-guard" for h in $HOOKS; do f="$ROOT/hooks/$h" if grep -q "lib-autodev-paths.sh" "$f" && grep -q "declare -f autodev_repo_root" "$f"; then @@ -91,23 +94,21 @@ for h in $HOOKS; do fi done -# --- Group C: lib-missing degradation — a hook with the lib hidden still emits valid output --- -# record-activity is the simplest writer: feed it a Skill payload with a bogus cwd, lib hidden, -# and assert it does NOT crash (exit !=2/127) and writes under the cwd fallback. -tmpd="$(mktemp -d)"; mkdir -p "$tmpd/.git" # make it look git-less enough to fallback -payload='{"tool_name":"Skill","tool_input":{"skill":"autodev:x"},"cwd":"'"$tmpd"'"}' -LIBBAK=""; if [ -f "$LIB" ]; then LIBBAK="$(mktemp)"; cp "$LIB" "$LIBBAK"; fi -# Don't actually delete the real lib; instead run the hook with a PATH/source that can't find it: -# simulate by copying record-activity to a temp dir WITHOUT the sibling lib. -sandbox="$(mktemp -d)"; cp "$ROOT/hooks/record-activity" "$sandbox/record-activity" +# --- Group C: lib-missing degradation — BEHAVIORAL proof (m-1): copy record-activity to a +# sandbox WITHOUT the sibling lib + a NON-git cwd, and assert it (a) doesn't crash AND +# (b) actually writes to the cwd-fallback location ($cwd/.claude/autodev-state/in-progress.jsonl), +# proving the `declare -f` identity-on-cwd fallback fired (not a vacuous exit-0). +cwdfb="$(mktemp -d)" # non-git dir => resolver (if it existed) AND the fallback both yield $cwdfb +sandbox="$(mktemp -d)"; cp "$ROOT/hooks/record-activity" "$sandbox/record-activity" # NO lib sibling +payload='{"tool_name":"Skill","tool_input":{"skill":"autodev:degrade-probe"},"cwd":"'"$cwdfb"'"}' out_rc=0; printf '%s' "$payload" | bash "$sandbox/record-activity" >/dev/null 2>&1 || out_rc=$? -# 127/2 would indicate the missing-function crash; 0 or 1 (benign) is acceptable degradation. -if [ "$out_rc" != "127" ] && [ "$out_rc" != "2" ]; then - pass "degradation: record-activity survives missing lib (rc=$out_rc)" +if [ "$out_rc" != "127" ] && [ "$out_rc" != "2" ] \ + && grep -q "degrade-probe" "$cwdfb/.claude/autodev-state/in-progress.jsonl" 2>/dev/null; then + pass "degradation: record-activity (lib hidden) wrote to cwd fallback, no crash (rc=$out_rc)" else - fail "degradation: record-activity crashed without lib (rc=$out_rc)" + fail "degradation: record-activity did not degrade to cwd fallback (rc=$out_rc; file=$cwdfb/.claude/autodev-state/in-progress.jsonl)" fi -rm -rf "$tmpd" "$sandbox"; [ -n "$LIBBAK" ] && rm -f "$LIBBAK" +rm -rf "$cwdfb" "$sandbox" echo ""; echo "Results: $failures failure(s)"; [ "$failures" -eq 0 ] ``` @@ -147,29 +148,40 @@ autodev_repo_root() { --- -### Task 3: Retrofit the 12 state-writing hooks +### Task 3: Retrofit the 11 state-writing hooks (+ keep hook-contracts green) -**Change class:** Hook. Verification: Task-1 Group B (all 12 wired) + Group C (degradation) pass; `tests/hook-contracts.sh` still green (no behavior regression). +**Change class:** Hook. Verification: Task-1 Group B (all 11 wired) + Group C (degradation) pass; `tests/hook-contracts.sh` still green **after the C-1 test fix in Step 4** (no behavior regression). -**Files (Modify, all in `hooks/`):** `completion-claim-guard`, `pre-compact-snapshot`, `pre-tool-scope-guard`, `pretool-demo-fidelity-guard`, `pretool-pr-review-reminder`, `prompt-strict-interpretation`, `record-activity`, `scope-lock-abandon`, `scope-lock-claim`, `scope-lock-complete`, `session-start`, `subagent-scope-guard`. +**Files (Modify, all in `hooks/`):** `completion-claim-guard`, `pre-compact-snapshot`, `pre-tool-scope-guard`, `pretool-demo-fidelity-guard`, `pretool-pr-review-reminder`, `prompt-strict-interpretation`, `record-activity`, `scope-lock-abandon`, `scope-lock-complete`, `session-start`, `subagent-scope-guard`. Plus `tests/hook-contracts.sh` (Step 4). -**Step 0 (guard against list drift):** run `grep -rlE 'autodev-state|\.autodev/state' hooks/ | sort` — confirm it returns exactly these 12. If it differs, STOP and reconcile before editing. +**Step 0 (guard against list drift):** run `grep -rlE 'autodev-state|\.autodev/state' hooks/ | sort` — it returns **12** files, but `scope-lock-claim` is a **comment-only** match (line 11 is a doc comment; it performs no runtime state I/O — the session-lock write is delegated to `pre-tool-scope-guard`). Retrofit the **11** with real state I/O; do NOT retrofit `scope-lock-claim`. If the grep returns any *other* set, STOP and reconcile. -**Step 1:** In each hook, immediately after its `cwd_dir` is determined (the line `[ -z "$cwd_dir" ] && cwd_dir="${PWD}"` or equivalent), insert: +**Step 1:** In each of the 11 hooks, immediately after its `cwd_dir` is finalized, insert: ```sh . "$(cd -- "$(dirname -- "${BASH_SOURCE[0]:-$0}")" && pwd)/lib-autodev-paths.sh" 2>/dev/null || true declare -f autodev_repo_root >/dev/null 2>&1 || autodev_repo_root() { printf '%s\n' "${1:-$PWD}"; } ADK_ROOT="$(autodev_repo_root "$cwd_dir")" ``` -Then replace every `${cwd_dir}/.claude/autodev-state` → `${ADK_ROOT}/.claude/autodev-state` and every `${cwd_dir}/.autodev/state` → `${ADK_ROOT}/.autodev/state` **in that hook**. Leave non-state uses of `cwd_dir` (e.g. `${cwd_dir}/docs/plans`, repo-content reads) UNCHANGED — those legitimately want the working dir, not the canonical root. +**Anchor point per hook is NOT uniform — use these explicit anchors (I-1):** +- Most hooks (`record-activity`, `pre-compact-snapshot`, `subagent-scope-guard`, `prompt-strict-interpretation`, `pre-tool-scope-guard`, `completion-claim-guard`, `pretool-pr-review-reminder`, `pretool-demo-fidelity-guard`): after their `[ -z "$cwd_dir" ] && cwd_dir="${PWD}"` line. +- **`session-start` (I-1):** insert **after line 45** (`[ -n "$cwd_from_hook" ] && cwd_dir="$cwd_from_hook"`) — its init is two-step (`cwd_dir="${PWD}"` at L38, payload override at L45). Inserting before L45 would derive `ADK_ROOT` from `$PWD` instead of the payload `.cwd` and silently break the feature for this hook. -**Special cases (per design m-3 / cycle-2 minors):** -- `scope-lock-complete`, `scope-lock-abandon`: they compute `repo_root` from the plan path and take `$PWD`. Replace that `repo_root=$(cd "${plan_dir}/../.." && pwd)` derivation with `ADK_ROOT="$(autodev_repo_root "$PWD")"` (source the lib first). This intentionally switches worktree→main root for state pruning. -- `scope-lock-claim`: it only **reads** `session-locks.jsonl` for verification (write is delegated to `pre-tool-scope-guard`). Anchor the read path to `ADK_ROOT` too (so it reads the same canonical file), but do not add an unused `STATE_DIR` (cycle-2 minor). +Then replace every `${cwd_dir}/.claude/autodev-state` → `${ADK_ROOT}/.claude/autodev-state` and every `${cwd_dir}/.autodev/state` → `${ADK_ROOT}/.autodev/state` **in that hook**. Leave non-state uses of `cwd_dir` (`${cwd_dir}/docs/plans`, repo-content reads) UNCHANGED. -**Step 2: Verify.** `bash tests/adk-path-canonicalization.sh` → Group B + C PASS. `bash tests/hook-contracts.sh` → exit 0 (no contract regression). `bash tests/hook-stdout-discipline.sh` → exit 0. +**Special cases:** +- **`pre-compact-snapshot` (I-2):** besides the lock-snapshot append at its end, it reads+writes `reminder_marker="${cwd_dir}/.claude/autodev-state/pr-reminder-seen"` at lines ~43–55 **before** its `noop_json` early-exit (the common path). The Step-1 insert (after the L33 `cwd_dir` line) precedes line 43, so `ADK_ROOT` is in scope — make sure the `reminder_marker` substitution is included, or the pr-reminder dedup splits across worktrees. +- **`scope-lock-complete`, `scope-lock-abandon`:** they currently derive `repo_root=$(cd "${plan_dir}/../.." && pwd)` from the plan path and take the shell `$PWD`. Replace that derivation with `ADK_ROOT="$(autodev_repo_root "$PWD")"` (source the lib first). This **intentionally** switches worktree→main root for state pruning (design m-3). -**Step 3: Commit.** `git add hooks/ && git commit -m "refactor(hooks): all 12 state writers resolve canonical root via shared lib (#70 residual)"` +**Step 4 (C-1 — keep hook-contracts.sh green):** because `scope-lock-complete`/`-abandon` now resolve their root from `$PWD`, the two **bare** invocations in `tests/hook-contracts.sh` that run from `$REPO_ROOT` (a real git checkout) must run from inside the tmp fixture instead — otherwise state ops land in the real checkout and every pruning assertion fails. Rewrite **line 641** and **line 707**: +```sh +# before: hooks/scope-lock-complete "$tmp/docs/plans/example.md" --evidence "tests pass" ... +# after : ( cd "$tmp" && "$REPO_ROOT/hooks/scope-lock-complete" docs/plans/example.md --evidence "tests pass" ... ) +``` +matching the already-correct cd-wrapped invocations at lines 746/790/833 (and all `scope-lock-abandon` tests). `$tmp` is a non-git `mktemp -d`, so inside it the resolver falls back to `$tmp` — restoring the prior behavior the assertions expect. + +**Step 5: Verify.** `bash tests/adk-path-canonicalization.sh` → Group B (11 wired) + C PASS. `bash tests/hook-contracts.sh` → exit 0 (Step-4 test fix applied → no contract regression). `bash tests/hook-stdout-discipline.sh` → exit 0. + +**Step 6: Commit.** `git add hooks/ tests/hook-contracts.sh && git commit -m "refactor(hooks): 11 state writers resolve canonical root via shared lib (#70 residual)"` **Rollback:** revert this commit → hooks fall back to per-hook cwd-scoping (prior behavior); no state migration needed. @@ -303,7 +315,7 @@ jobs: **Files:** Modify `.github/workflows/skill-content-check.yml` (add `tests/adk-path-canonicalization.sh` step + `hooks/**` path so hook changes trigger it). -**Step 1:** Add `hooks/**` and `tests/adk-path-canonicalization.sh` to the workflow `paths` (push + PR), and a step `run: bash tests/adk-path-canonicalization.sh`. +**Step 1:** Two distinct edits to `skill-content-check.yml` (m-3): (a) add `hooks/**` and `tests/adk-path-canonicalization.sh` to **both** the `push.paths` and `pull_request.paths` filters (so hook/test edits re-trigger), AND (b) add a `run:` step `run: bash tests/adk-path-canonicalization.sh` after the existing content-grep step. **Step 2: Run the FULL local gate — all exit 0:** ```bash From f882aa137738adb6d83fda2dac112f5d025bbacf Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Wed, 3 Jun 2026 14:36:26 -0400 Subject: [PATCH 07/20] =?UTF-8?q?plan:=20cycle-2=20fix=20=E2=80=94=20pwd?= =?UTF-8?q?=20-P=20resolver=20(C-2=20macOS=20symlink);=20verified=20all=20?= =?UTF-8?q?4=20resolver=20cases?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/plans/2026-06-03-adk-path-canonicalization-design.md | 4 +++- .../2026-06-03-adk-path-canonicalization-plan-review.md | 8 ++++++++ docs/plans/2026-06-03-adk-path-canonicalization.md | 8 ++++++-- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/docs/plans/2026-06-03-adk-path-canonicalization-design.md b/docs/plans/2026-06-03-adk-path-canonicalization-design.md index ceaef15..bb93ec0 100644 --- a/docs/plans/2026-06-03-adk-path-canonicalization-design.md +++ b/docs/plans/2026-06-03-adk-path-canonicalization-design.md @@ -64,7 +64,9 @@ autodev_repo_root() { # git / non-git dir yields an empty $_gcd → fallback to $cwd (NOT `/` from `cd ""/..`). _gcd="$(cd "$cwd" 2>/dev/null && git rev-parse --git-common-dir 2>/dev/null || true)" _root="" - [ -n "$_gcd" ] && _root="$(cd "$cwd" 2>/dev/null && cd "$_gcd/.." 2>/dev/null && pwd || true)" + # pwd -P (physical): normalizes the macOS /var->/private symlink + git's absolute-vs-relative + # common-dir so a main-checkout and a worktree invocation return the IDENTICAL root (C-2). + [ -n "$_gcd" ] && _root="$(cd "$cwd" 2>/dev/null && cd "$_gcd/.." 2>/dev/null && pwd -P || true)" if [ -n "$_root" ]; then printf '%s\n' "$_root"; else printf '%s\n' "$cwd"; fi } ``` diff --git a/docs/plans/2026-06-03-adk-path-canonicalization-plan-review.md b/docs/plans/2026-06-03-adk-path-canonicalization-plan-review.md index 7bced10..f961b91 100644 --- a/docs/plans/2026-06-03-adk-path-canonicalization-plan-review.md +++ b/docs/plans/2026-06-03-adk-path-canonicalization-plan-review.md @@ -30,3 +30,11 @@ 2. Behavioral degradation assertion — **taken** (m-1). **Verdict reasoning:** One Critical (a concrete `hook-contracts.sh` regression the plan didn't instruct to fix) + two Importants (wrong retrofit target `scope-lock-claim`; non-standard `session-start` anchor + missing `pre-compact-snapshot` special case) + four Minors — all verified against the real hook code, all with narrow fixes now in the plan text. The resolver design and the worktree fixture proof are sound. Re-run to confirm convergence. + +## Cycle 2 (re-run) — all cycle-1 resolved; revision introduced 1 Critical, now fixed + +| id | sev | class | issue | resolution | +|---|---|---|---|---| +| C-2 | Critical | Portability / test correctness | macOS `/var`→`/private` symlink asymmetry: the resolver returned the **logical** path (`/var/...`) for a main checkout (relative `.git` + `pwd`) but the **physical** path (`/private/var/...`) for a linked worktree (git's absolute common-dir), so Group-A assertion (b) failed the mandatory **local** green gate (passed in CI/Linux — worst TDD failure mode). The reviewer's `--show-toplevel` fix would have broken case (a). | Resolver git-branch uses **`pwd -P`** (physical) so main-checkout and worktree invocations return the IDENTICAL physical root; Task-1 `main_root` uses `pwd -P` too. **Empirically verified** on macOS: (a) main, (b) worktree both → `/private/var/.../main`; (c) non-git → raw `$cwd`; (d) override — all 4 OK. Also strictly more robust in production (symlink-stable). Design resolver updated to match. | + +**Cycle-2 verdict:** 7/7 cycle-1 findings verified resolved in plan text against the real hooks; the 1 new Critical (resolver symlink normalization) is fixed via `pwd -P` and proven on the author's macOS. Re-run cycle 3 to confirm convergence. diff --git a/docs/plans/2026-06-03-adk-path-canonicalization.md b/docs/plans/2026-06-03-adk-path-canonicalization.md index d3cf126..3805a8f 100644 --- a/docs/plans/2026-06-03-adk-path-canonicalization.md +++ b/docs/plans/2026-06-03-adk-path-canonicalization.md @@ -64,7 +64,7 @@ if [ -f "$LIB" ]; then mkdir -p "$tmp/main" ( cd "$tmp/main" && git init -q && git -c user.email=a@b -c user.name=x commit -q --allow-empty -m init \ && git worktree add -q ../wt >/dev/null 2>&1 ) - main_root="$(cd "$tmp/main" && pwd)" + main_root="$(cd "$tmp/main" && pwd -P)" # pwd -P to match the resolver's physical-path output (C-2) # (a) from main checkout -> main root [ "$(autodev_repo_root "$tmp/main")" = "$main_root" ] \ && pass "resolver: main checkout -> main root" || fail "resolver main: got $(autodev_repo_root "$tmp/main")" @@ -137,7 +137,11 @@ autodev_repo_root() { local cwd="${1:-$PWD}" _gcd="" _root="" if [ -n "${AUTODEV_STATE_ROOT:-}" ]; then printf '%s\n' "$AUTODEV_STATE_ROOT"; return 0; fi _gcd="$(cd "$cwd" 2>/dev/null && git rev-parse --git-common-dir 2>/dev/null || true)" - [ -n "$_gcd" ] && _root="$(cd "$cwd" 2>/dev/null && cd "$_gcd/.." 2>/dev/null && pwd || true)" + # pwd -P (physical): git returns an ABSOLUTE common-dir for a linked worktree but a RELATIVE + # `.git` for a main checkout; on macOS the tmp/cwd is a /var->/private symlink. `pwd -P` + # normalizes both to the same physical path so a main-checkout and a worktree invocation + # resolve to the IDENTICAL root string (and the same inode) — C-2 fix. + [ -n "$_gcd" ] && _root="$(cd "$cwd" 2>/dev/null && cd "$_gcd/.." 2>/dev/null && pwd -P || true)" if [ -n "$_root" ]; then printf '%s\n' "$_root"; else printf '%s\n' "$cwd"; fi } ``` From 6c809c581284a33443ded66a0df0bb51644d1002 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Wed, 3 Jun 2026 14:37:44 -0400 Subject: [PATCH 08/20] plan: PASS @ plan-adversarial cycle 3 (converged) --- .../plans/2026-06-03-adk-path-canonicalization-plan-review.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/plans/2026-06-03-adk-path-canonicalization-plan-review.md b/docs/plans/2026-06-03-adk-path-canonicalization-plan-review.md index f961b91..1e5961c 100644 --- a/docs/plans/2026-06-03-adk-path-canonicalization-plan-review.md +++ b/docs/plans/2026-06-03-adk-path-canonicalization-plan-review.md @@ -38,3 +38,7 @@ | C-2 | Critical | Portability / test correctness | macOS `/var`→`/private` symlink asymmetry: the resolver returned the **logical** path (`/var/...`) for a main checkout (relative `.git` + `pwd`) but the **physical** path (`/private/var/...`) for a linked worktree (git's absolute common-dir), so Group-A assertion (b) failed the mandatory **local** green gate (passed in CI/Linux — worst TDD failure mode). The reviewer's `--show-toplevel` fix would have broken case (a). | Resolver git-branch uses **`pwd -P`** (physical) so main-checkout and worktree invocations return the IDENTICAL physical root; Task-1 `main_root` uses `pwd -P` too. **Empirically verified** on macOS: (a) main, (b) worktree both → `/private/var/.../main`; (c) non-git → raw `$cwd`; (d) override — all 4 OK. Also strictly more robust in production (symlink-stable). Design resolver updated to match. | **Cycle-2 verdict:** 7/7 cycle-1 findings verified resolved in plan text against the real hooks; the 1 new Critical (resolver symlink normalization) is fixed via `pwd -P` and proven on the author's macOS. Re-run cycle 3 to confirm convergence. + +## Cycle 3 (convergence) — PASS + +Zero Critical, zero Important, zero Minor. The `pwd -P` fix traced correct on all 4 cases: (a) main + (b) worktree both → identical physical root; (c) non-git fallback raw on both sides; (d) override short-circuits. No production physical/logical fragmentation (an invocation is exclusively in-git→physical or non-git→raw, never mixed for one repo path; all callers converge via `pwd -P`). Group C fallback never calls `pwd -P`. All 7 cycle-1 findings remain resolved. **Converged. Ready for execution.** From 80170ef4b6f98f0cdd0ba2d483ce1ca0a77413d9 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Wed, 3 Jun 2026 14:38:42 -0400 Subject: [PATCH 09/20] chore: lock scope for adk-path-canonicalization (alignment passed) --- docs/plans/2026-06-03-adk-path-canonicalization.md | 2 +- docs/plans/2026-06-03-adk-path-canonicalization.md.scope-lock | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 docs/plans/2026-06-03-adk-path-canonicalization.md.scope-lock diff --git a/docs/plans/2026-06-03-adk-path-canonicalization.md b/docs/plans/2026-06-03-adk-path-canonicalization.md index 3805a8f..7bd44f4 100644 --- a/docs/plans/2026-06-03-adk-path-canonicalization.md +++ b/docs/plans/2026-06-03-adk-path-canonicalization.md @@ -32,7 +32,7 @@ |------|-------|-------|--------| | 1 | ADK path canonicalization + write-location transparency + artifact hygiene (v6.5.0) | Task 1, Task 2, Task 3, Task 4, Task 5, Task 6, Task 7, Task 8, Task 9, Task 10 | feat/adk-path-canonicalization | -**Status:** Draft +**Status:** Locked 2026-06-03T18:38:42Z --- diff --git a/docs/plans/2026-06-03-adk-path-canonicalization.md.scope-lock b/docs/plans/2026-06-03-adk-path-canonicalization.md.scope-lock new file mode 100644 index 0000000..e65dac2 --- /dev/null +++ b/docs/plans/2026-06-03-adk-path-canonicalization.md.scope-lock @@ -0,0 +1 @@ +600c67aa49ad89d51cee506cf4d61204ef327bba7036df363f9239620fd6cec5 From 6e5e7a71234c20ccb642f1a0ad283e0e9716da7d Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Wed, 3 Jun 2026 14:41:28 -0400 Subject: [PATCH 10/20] test: ADK path canonicalization resolver+wiring guard [red] --- tests/adk-path-canonicalization.sh | 65 ++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100755 tests/adk-path-canonicalization.sh diff --git a/tests/adk-path-canonicalization.sh b/tests/adk-path-canonicalization.sh new file mode 100755 index 0000000..4ac3b14 --- /dev/null +++ b/tests/adk-path-canonicalization.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +# tests/adk-path-canonicalization.sh — proves the canonical ADK state-path resolver +# and that all 12 state-writing hooks adopt it. (#70 residual; v6.5.0) +set -uo pipefail +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/.." && pwd)" +LIB="$ROOT/hooks/lib-autodev-paths.sh" +failures=0 +pass(){ printf 'PASS: %s\n' "$1"; } +fail(){ printf 'FAIL: %s\n' "$1" >&2; failures=$((failures+1)); } + +# --- Group A: resolver behavior against a REAL temp git + linked worktree --- +if [ -f "$LIB" ]; then + . "$LIB" + tmp="$(mktemp -d)" + # portable git init (avoid `git init ` which needs git>=2.28): mkdir + `git -C` + mkdir -p "$tmp/main" + ( cd "$tmp/main" && git init -q && git -c user.email=a@b -c user.name=x commit -q --allow-empty -m init \ + && git worktree add -q ../wt >/dev/null 2>&1 ) + main_root="$(cd "$tmp/main" && pwd -P)" # pwd -P to match the resolver's physical-path output (C-2) + # (a) from main checkout -> main root + [ "$(autodev_repo_root "$tmp/main")" = "$main_root" ] \ + && pass "resolver: main checkout -> main root" || fail "resolver main: got $(autodev_repo_root "$tmp/main")" + # (b) from linked worktree -> SAME main root (the load-bearing claim) + [ "$(autodev_repo_root "$tmp/wt")" = "$main_root" ] \ + && pass "resolver: linked worktree -> main root (shared)" || fail "resolver worktree: got $(autodev_repo_root "$tmp/wt")" + # (c) non-git dir -> cwd fallback (C-1: NOT '/') + ngt="$(mktemp -d)"; [ "$(autodev_repo_root "$ngt")" = "$ngt" ] \ + && pass "resolver: non-git dir -> cwd fallback" || fail "resolver non-git: got $(autodev_repo_root "$ngt") (want $ngt)" + # (d) env override wins + [ "$(AUTODEV_STATE_ROOT=/tmp/override autodev_repo_root "$tmp/main")" = "/tmp/override" ] \ + && pass "resolver: AUTODEV_STATE_ROOT override" || fail "resolver override broken" + rm -rf "$tmp" "$ngt" +else + fail "lib missing: $LIB" +fi + +# --- Group B: all 11 state-WRITING hooks source the lib + guard the function --- +# (scope-lock-claim is EXCLUDED — its only autodev-state mention is a comment, no runtime state I/O.) +HOOKS="completion-claim-guard pre-compact-snapshot pre-tool-scope-guard pretool-demo-fidelity-guard pretool-pr-review-reminder prompt-strict-interpretation record-activity scope-lock-abandon scope-lock-complete session-start subagent-scope-guard" +for h in $HOOKS; do + f="$ROOT/hooks/$h" + if grep -q "lib-autodev-paths.sh" "$f" && grep -q "declare -f autodev_repo_root" "$f"; then + pass "hook wired: $h" + else + fail "hook NOT wired (no lib source + declare -f guard): $h" + fi +done + +# --- Group C: lib-missing degradation — BEHAVIORAL proof (m-1): copy record-activity to a +# sandbox WITHOUT the sibling lib + a NON-git cwd, and assert it (a) doesn't crash AND +# (b) actually writes to the cwd-fallback location ($cwd/.claude/autodev-state/in-progress.jsonl), +# proving the `declare -f` identity-on-cwd fallback fired (not a vacuous exit-0). +cwdfb="$(mktemp -d)" # non-git dir => resolver (if it existed) AND the fallback both yield $cwdfb +sandbox="$(mktemp -d)"; cp "$ROOT/hooks/record-activity" "$sandbox/record-activity" # NO lib sibling +payload='{"tool_name":"Skill","tool_input":{"skill":"autodev:degrade-probe"},"cwd":"'"$cwdfb"'"}' +out_rc=0; printf '%s' "$payload" | bash "$sandbox/record-activity" >/dev/null 2>&1 || out_rc=$? +if [ "$out_rc" != "127" ] && [ "$out_rc" != "2" ] \ + && grep -q "degrade-probe" "$cwdfb/.claude/autodev-state/in-progress.jsonl" 2>/dev/null; then + pass "degradation: record-activity (lib hidden) wrote to cwd fallback, no crash (rc=$out_rc)" +else + fail "degradation: record-activity did not degrade to cwd fallback (rc=$out_rc; file=$cwdfb/.claude/autodev-state/in-progress.jsonl)" +fi +rm -rf "$cwdfb" "$sandbox" + +echo ""; echo "Results: $failures failure(s)"; [ "$failures" -eq 0 ] From fd48e1d2a73dd5ecd500f1b6c9322804f63143bd Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Wed, 3 Jun 2026 14:41:49 -0400 Subject: [PATCH 11/20] feat(hooks): canonical ADK state-root resolver (git-common-dir anchored) --- hooks/lib-autodev-paths.sh | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 hooks/lib-autodev-paths.sh diff --git a/hooks/lib-autodev-paths.sh b/hooks/lib-autodev-paths.sh new file mode 100644 index 0000000..08249ea --- /dev/null +++ b/hooks/lib-autodev-paths.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +# lib-autodev-paths.sh — canonical ADK state-root resolver, sourced by state-writing hooks. +# autodev_repo_root -> canonical repo root (shared across worktrees, survives worktree removal). +# set -u safe: every var is assigned before any read. Sourced; uses `local` (all callers are bash). +autodev_repo_root() { + local cwd="${1:-$PWD}" _gcd="" _root="" + if [ -n "${AUTODEV_STATE_ROOT:-}" ]; then printf '%s\n' "$AUTODEV_STATE_ROOT"; return 0; fi + _gcd="$(cd "$cwd" 2>/dev/null && git rev-parse --git-common-dir 2>/dev/null || true)" + # pwd -P (physical): git returns an ABSOLUTE common-dir for a linked worktree but a RELATIVE + # `.git` for a main checkout; on macOS the tmp/cwd is a /var->/private symlink. `pwd -P` + # normalizes both to the same physical path so a main-checkout and a worktree invocation + # resolve to the IDENTICAL root string (and the same inode) — C-2 fix. + [ -n "$_gcd" ] && _root="$(cd "$cwd" 2>/dev/null && cd "$_gcd/.." 2>/dev/null && pwd -P || true)" + if [ -n "$_root" ]; then printf '%s\n' "$_root"; else printf '%s\n' "$cwd"; fi +} From fdee456d11b171407e85c990cd0c0767e1546735 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Wed, 3 Jun 2026 14:44:41 -0400 Subject: [PATCH 12/20] refactor(hooks): 11 state writers resolve canonical root via shared lib (#70 residual) --- hooks/completion-claim-guard | 7 +++++-- hooks/pre-compact-snapshot | 9 ++++++--- hooks/pre-tool-scope-guard | 7 +++++-- hooks/pretool-demo-fidelity-guard | 5 ++++- hooks/pretool-pr-review-reminder | 5 ++++- hooks/prompt-strict-interpretation | 5 ++++- hooks/record-activity | 5 ++++- hooks/scope-lock-abandon | 12 +++++++----- hooks/scope-lock-complete | 12 +++++++----- hooks/session-start | 5 ++++- hooks/subagent-scope-guard | 5 ++++- tests/hook-contracts.sh | 4 ++-- 12 files changed, 56 insertions(+), 25 deletions(-) diff --git a/hooks/completion-claim-guard b/hooks/completion-claim-guard index ae605e2..d6bc5a2 100755 --- a/hooks/completion-claim-guard +++ b/hooks/completion-claim-guard @@ -26,6 +26,9 @@ hook_input=$(cat || true) cwd_dir=$(printf '%s' "$hook_input" | jq -r '.cwd // empty' 2>/dev/null || true) [ -z "$cwd_dir" ] && cwd_dir="${PWD}" +. "$(cd -- "$(dirname -- "${BASH_SOURCE[0]:-$0}")" && pwd)/lib-autodev-paths.sh" 2>/dev/null || true +declare -f autodev_repo_root >/dev/null 2>&1 || autodev_repo_root() { printf '%s\n' "${1:-$PWD}"; } +ADK_ROOT="$(autodev_repo_root "$cwd_dir")" # Honour stop_hook_active to avoid infinite loops. # When true the agent was already told to continue once; let the stop proceed. @@ -74,7 +77,7 @@ find_locked_plans() { } session_locked_plans() { - local state_file="${cwd_dir}/.claude/autodev-state/session-locks.jsonl" + local state_file="${ADK_ROOT}/.claude/autodev-state/session-locks.jsonl" [ -f "$state_file" ] || return 0 jq -r --arg session "$session_key" \ 'select(.ev == "session-lock" and .session == $session) | .pl // empty' \ @@ -141,7 +144,7 @@ if [ -z "$failures" ]; then if [ "$completionish" = "true" ] && [ "$waiting_for_user" != "true" ]; then first_plan=$(printf '%s\n' "$locked_plans" | head -1) first_plan_name=$(basename "$first_plan") - progress_file="${cwd_dir}/.autodev/state/phase-progress.jsonl" + progress_file="${ADK_ROOT}/.autodev/state/phase-progress.jsonl" checkpoint_prefix="Completion checkpoint — locked plan still in effect: ${first_plan_name} " block "${checkpoint_prefix} diff --git a/hooks/pre-compact-snapshot b/hooks/pre-compact-snapshot index 24178b8..3784a7f 100755 --- a/hooks/pre-compact-snapshot +++ b/hooks/pre-compact-snapshot @@ -31,6 +31,9 @@ hook_input=$(cat || true) cwd_dir=$(printf '%s' "$hook_input" | jq -r '.cwd // empty' 2>/dev/null || true) [ -z "$cwd_dir" ] && cwd_dir="${PWD}" +. "$(cd -- "$(dirname -- "${BASH_SOURCE[0]:-$0}")" && pwd)/lib-autodev-paths.sh" 2>/dev/null || true +declare -f autodev_repo_root >/dev/null 2>&1 || autodev_repo_root() { printf '%s\n' "${1:-$PWD}"; } +ADK_ROOT="$(autodev_repo_root "$cwd_dir")" transcript_path=$(printf '%s' "$hook_input" | jq -r '.transcript_path // empty' 2>/dev/null || true) session_key="" [ -n "$transcript_path" ] && session_key=$(basename "$transcript_path") @@ -40,7 +43,7 @@ session_key="" # earlier reminder). MUST run before the no-locked-plans early-exit below. Guard on a # non-empty session key (no blanket file wipe on identity-less hosts). if [ -n "${session_key:-}" ]; then - reminder_marker="${cwd_dir}/.claude/autodev-state/pr-reminder-seen" + reminder_marker="${ADK_ROOT}/.claude/autodev-state/pr-reminder-seen" if [ -f "$reminder_marker" ]; then # grep -v exits 1 when no lines remain (only our key) — tolerate it under set -e. remaining="$(grep -vxF "$session_key" "$reminder_marker" 2>/dev/null || true)" @@ -73,7 +76,7 @@ if [ -d "$plans_dir" ]; then } session_locked_plans() { - local state_file="${cwd_dir}/.claude/autodev-state/session-locks.jsonl" + local state_file="${ADK_ROOT}/.claude/autodev-state/session-locks.jsonl" [ -f "$state_file" ] || return 0 jq -r --arg session "$session_key" \ 'select(.ev == "session-lock" and .session == $session) | .pl // empty' \ @@ -140,7 +143,7 @@ if [ -z "$state_section" ]; then fi # ── Append to autodev-state file ───────────────────────────────────────── -STATE_DIR="${cwd_dir}/.claude/autodev-state" +STATE_DIR="${ADK_ROOT}/.claude/autodev-state" mkdir -p "$STATE_DIR" 2>/dev/null || true STATE_FILE="${STATE_DIR}/in-progress.jsonl" diff --git a/hooks/pre-tool-scope-guard b/hooks/pre-tool-scope-guard index 0d24789..95bee39 100755 --- a/hooks/pre-tool-scope-guard +++ b/hooks/pre-tool-scope-guard @@ -36,6 +36,9 @@ hook_input=$(cat || true) tool_name=$(printf '%s' "$hook_input" | jq -r '.tool_name // empty' 2>/dev/null || true) cwd_dir=$(printf '%s' "$hook_input" | jq -r '.cwd // empty' 2>/dev/null || true) [ -z "$cwd_dir" ] && cwd_dir="${PWD}" +. "$(cd -- "$(dirname -- "${BASH_SOURCE[0]:-$0}")" && pwd)/lib-autodev-paths.sh" 2>/dev/null || true +declare -f autodev_repo_root >/dev/null 2>&1 || autodev_repo_root() { printf '%s\n' "${1:-$PWD}"; } +ADK_ROOT="$(autodev_repo_root "$cwd_dir")" transcript_path=$(printf '%s' "$hook_input" | jq -r '.transcript_path // empty' 2>/dev/null || true) session_key="" [ -n "$transcript_path" ] && session_key=$(basename "$transcript_path") @@ -132,7 +135,7 @@ record_session_lock() { fi [ -n "$plan_arg" ] || return 0 - local state_dir="${cwd_dir}/.claude/autodev-state" + local state_dir="${ADK_ROOT}/.claude/autodev-state" mkdir -p "$state_dir" 2>/dev/null || return 0 local state_file="${state_dir}/session-locks.jsonl" local objective_excerpt objective_hash repo_name branch_name confirmed @@ -209,7 +212,7 @@ find_locked_plans() { [ -d "$plans_dir" ] || return 0 if [ -n "$session_key" ]; then - local state_file="${cwd_dir}/.claude/autodev-state/session-locks.jsonl" + local state_file="${ADK_ROOT}/.claude/autodev-state/session-locks.jsonl" [ -f "$state_file" ] || return 0 jq -r --arg session "$session_key" \ 'select(.ev == "session-lock" and .session == $session) | .pl' \ diff --git a/hooks/pretool-demo-fidelity-guard b/hooks/pretool-demo-fidelity-guard index 53c6126..e28f701 100755 --- a/hooks/pretool-demo-fidelity-guard +++ b/hooks/pretool-demo-fidelity-guard @@ -81,6 +81,9 @@ session_key="" cwd_dir=$(printf '%s' "$hook_input" | jq -r '.cwd // empty' 2>/dev/null || true) [ -z "$cwd_dir" ] && cwd_dir="${PWD}" +. "$(cd -- "$(dirname -- "${BASH_SOURCE[0]:-$0}")" && pwd)/lib-autodev-paths.sh" 2>/dev/null || true +declare -f autodev_repo_root >/dev/null 2>&1 || autodev_repo_root() { printf '%s\n' "${1:-$PWD}"; } +ADK_ROOT="$(autodev_repo_root "$cwd_dir")" dedup_key="" if command -v sha256sum >/dev/null 2>&1; then @@ -89,7 +92,7 @@ elif command -v shasum >/dev/null 2>&1; then dedup_key=$(printf '%s' "${session_key}:${file_path}" | shasum -a 256 2>/dev/null | cut -d' ' -f1 || true) fi -state_dir="${cwd_dir}/.claude/autodev-state" +state_dir="${ADK_ROOT}/.claude/autodev-state" state_file="${state_dir}/demo-fidelity-seen" if [ -n "$dedup_key" ]; then diff --git a/hooks/pretool-pr-review-reminder b/hooks/pretool-pr-review-reminder index 500e8c8..a9da0cc 100755 --- a/hooks/pretool-pr-review-reminder +++ b/hooks/pretool-pr-review-reminder @@ -36,10 +36,13 @@ printf '%s' "$cmd_unquoted" | grep -q 'gh pr create' || exit 0 # compaction. Keyed by transcript basename; degrade to emit-every-time when absent. cwd_dir=$(printf '%s' "$hook_input" | jq -r '.cwd // empty' 2>/dev/null || true) [ -z "$cwd_dir" ] && cwd_dir="${PWD}" +. "$(cd -- "$(dirname -- "${BASH_SOURCE[0]:-$0}")" && pwd)/lib-autodev-paths.sh" 2>/dev/null || true +declare -f autodev_repo_root >/dev/null 2>&1 || autodev_repo_root() { printf '%s\n' "${1:-$PWD}"; } +ADK_ROOT="$(autodev_repo_root "$cwd_dir")" transcript_path=$(printf '%s' "$hook_input" | jq -r '.transcript_path // empty' 2>/dev/null || true) session_key="" [ -n "$transcript_path" ] && session_key=$(basename "$transcript_path") -marker="${cwd_dir}/.claude/autodev-state/pr-reminder-seen" +marker="${ADK_ROOT}/.claude/autodev-state/pr-reminder-seen" if [ -n "$session_key" ] && grep -qxF "$session_key" "$marker" 2>/dev/null; then exit 0 # already reminded this session fi diff --git a/hooks/prompt-strict-interpretation b/hooks/prompt-strict-interpretation index 9a4c916..dbc144e 100755 --- a/hooks/prompt-strict-interpretation +++ b/hooks/prompt-strict-interpretation @@ -26,6 +26,9 @@ prompt=$(printf '%s' "$hook_input" | jq -r '.prompt // empty' 2>/dev/null || tru cwd_dir=$(printf '%s' "$hook_input" | jq -r '.cwd // empty' 2>/dev/null || true) [ -z "$cwd_dir" ] && cwd_dir="${PWD}" +. "$(cd -- "$(dirname -- "${BASH_SOURCE[0]:-$0}")" && pwd)/lib-autodev-paths.sh" 2>/dev/null || true +declare -f autodev_repo_root >/dev/null 2>&1 || autodev_repo_root() { printf '%s\n' "${1:-$PWD}"; } +ADK_ROOT="$(autodev_repo_root "$cwd_dir")" transcript_path=$(printf '%s' "$hook_input" | jq -r '.transcript_path // empty' 2>/dev/null || true) session_key="" [ -n "$transcript_path" ] && session_key=$(basename "$transcript_path") @@ -108,7 +111,7 @@ if [ -d "$plans_dir" ]; then } session_locked_plans() { - local state_file="${cwd_dir}/.claude/autodev-state/session-locks.jsonl" + local state_file="${ADK_ROOT}/.claude/autodev-state/session-locks.jsonl" [ -f "$state_file" ] || return 0 jq -r --arg session "$session_key" \ 'select(.ev == "session-lock" and .session == $session) | .pl // empty' \ diff --git a/hooks/record-activity b/hooks/record-activity index b1486e7..ef886c8 100755 --- a/hooks/record-activity +++ b/hooks/record-activity @@ -19,6 +19,9 @@ hook_input=$(cat || true) tool_name=$(printf '%s' "$hook_input" | jq -r '.tool_name // empty' 2>/dev/null || true) cwd_dir=$(printf '%s' "$hook_input" | jq -r '.cwd // empty' 2>/dev/null || true) [ -z "$cwd_dir" ] && cwd_dir="${PWD}" +. "$(cd -- "$(dirname -- "${BASH_SOURCE[0]:-$0}")" && pwd)/lib-autodev-paths.sh" 2>/dev/null || true +declare -f autodev_repo_root >/dev/null 2>&1 || autodev_repo_root() { printf '%s\n' "${1:-$PWD}"; } +ADK_ROOT="$(autodev_repo_root "$cwd_dir")" case "$tool_name" in Skill) @@ -46,7 +49,7 @@ case "$tool_name" in ;; esac -STATE_DIR="${cwd_dir}/.claude/autodev-state" +STATE_DIR="${ADK_ROOT}/.claude/autodev-state" mkdir -p "$STATE_DIR" 2>/dev/null || exit 0 STATE_FILE="${STATE_DIR}/in-progress.jsonl" LOCK_FILE="${STATE_DIR}/.in-progress.lock" diff --git a/hooks/scope-lock-abandon b/hooks/scope-lock-abandon index f0e09b1..768202b 100755 --- a/hooks/scope-lock-abandon +++ b/hooks/scope-lock-abandon @@ -73,14 +73,16 @@ grep -qE '^\*\*Status:\*\*[[:space:]]+Locked' "$plan_abs" || { printf 'scope-lock-abandon: plan is not in Locked status: %s\n' "$plan_abs" >&2; exit 2; } plan_dir=$(cd "$(dirname "$plan_abs")" && pwd) -repo_root=$(cd "${plan_dir}/../.." && pwd) +. "$(cd -- "$(dirname -- "${BASH_SOURCE[0]:-$0}")" && pwd)/lib-autodev-paths.sh" 2>/dev/null || true +declare -f autodev_repo_root >/dev/null 2>&1 || autodev_repo_root() { printf '%s\n' "${1:-$PWD}"; } +ADK_ROOT="$(autodev_repo_root "$PWD")" plan_name=$(basename "$plan_abs") plan_rel="docs/plans/${plan_name}" lock_file="${plan_abs}.scope-lock" -session_locks_file="${repo_root}/.claude/autodev-state/session-locks.jsonl" -in_progress_file="${repo_root}/.claude/autodev-state/in-progress.jsonl" -progress_dir="${repo_root}/.autodev/state" +session_locks_file="${ADK_ROOT}/.claude/autodev-state/session-locks.jsonl" +in_progress_file="${ADK_ROOT}/.claude/autodev-state/in-progress.jsonl" +progress_dir="${ADK_ROOT}/.autodev/state" progress_file="${progress_dir}/phase-progress.jsonl" ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ") @@ -104,7 +106,7 @@ prune_jsonl() { [ -n "$line" ] || continue pl=$(printf '%s' "$line" | jq -r '.pl // empty' 2>/dev/null || true) || { rm -f "$tmp"; return 1; } if [ -n "$pl" ]; then - resolved=$(canonical_path_from_base "$repo_root" "$pl" 2>/dev/null || true) + resolved=$(canonical_path_from_base "$ADK_ROOT" "$pl" 2>/dev/null || true) [ "$resolved" = "$plan_abs" ] && continue fi printf '%s\n' "$line" >> "$tmp" diff --git a/hooks/scope-lock-complete b/hooks/scope-lock-complete index 6dcf213..fb3c952 100755 --- a/hooks/scope-lock-complete +++ b/hooks/scope-lock-complete @@ -71,7 +71,9 @@ if ! grep -q '\*\*Status:\*\* Locked' "$plan_abs" 2>/dev/null; then fi plan_dir=$(cd "$(dirname "$plan_abs")" && pwd) -repo_root=$(cd "${plan_dir}/../.." && pwd) +. "$(cd -- "$(dirname -- "${BASH_SOURCE[0]:-$0}")" && pwd)/lib-autodev-paths.sh" 2>/dev/null || true +declare -f autodev_repo_root >/dev/null 2>&1 || autodev_repo_root() { printf '%s\n' "${1:-$PWD}"; } +ADK_ROOT="$(autodev_repo_root "$PWD")" plan_name=$(basename "$plan_abs") plan_rel="docs/plans/${plan_name}" lock_file="${plan_abs}.scope-lock" @@ -134,9 +136,9 @@ validate_jsonl() { done < "$file" } -session_locks_file="${repo_root}/.claude/autodev-state/session-locks.jsonl" -in_progress_file="${repo_root}/.claude/autodev-state/in-progress.jsonl" -progress_dir="${repo_root}/.autodev/state" +session_locks_file="${ADK_ROOT}/.claude/autodev-state/session-locks.jsonl" +in_progress_file="${ADK_ROOT}/.claude/autodev-state/in-progress.jsonl" +progress_dir="${ADK_ROOT}/.autodev/state" progress_file="${progress_dir}/phase-progress.jsonl" ensure_regular_or_absent() { @@ -186,7 +188,7 @@ prune_jsonl() { return 1 } if [ -n "$pl" ]; then - resolved=$(canonical_path_from_base "$repo_root" "$pl" 2>/dev/null || true) + resolved=$(canonical_path_from_base "$ADK_ROOT" "$pl" 2>/dev/null || true) [ "$resolved" = "$plan_abs" ] && continue fi printf '%s\n' "$line" >> "$tmp" diff --git a/hooks/session-start b/hooks/session-start index 98369e3..a4864a2 100755 --- a/hooks/session-start +++ b/hooks/session-start @@ -44,6 +44,9 @@ if command -v jq >/dev/null 2>&1; then cwd_from_hook=$(printf '%s' "$hook_input" | jq -r '.cwd // empty' 2>/dev/null || true) [ -n "$cwd_from_hook" ] && cwd_dir="$cwd_from_hook" fi +. "$(cd -- "$(dirname -- "${BASH_SOURCE[0]:-$0}")" && pwd)/lib-autodev-paths.sh" 2>/dev/null || true +declare -f autodev_repo_root >/dev/null 2>&1 || autodev_repo_root() { printf '%s\n' "${1:-$PWD}"; } +ADK_ROOT="$(autodev_repo_root "$cwd_dir")" [ -n "$transcript_path" ] && session_key=$(basename "$transcript_path") # Skip injection for subagent contexts entirely. Codex's @@ -57,7 +60,7 @@ if [ -n "$agent_id" ]; then exit 0 fi -STATE_DIR="${cwd_dir}/.claude/autodev-state" +STATE_DIR="${ADK_ROOT}/.claude/autodev-state" STATE_FILE="${STATE_DIR}/in-progress.jsonl" SESSION_LOCKS_FILE="${STATE_DIR}/session-locks.jsonl" SEEN_FILE="${STATE_DIR}/session-start-seen" diff --git a/hooks/subagent-scope-guard b/hooks/subagent-scope-guard index 2cc736c..0f04704 100755 --- a/hooks/subagent-scope-guard +++ b/hooks/subagent-scope-guard @@ -25,6 +25,9 @@ hook_input=$(cat || true) cwd_dir=$(printf '%s' "$hook_input" | jq -r '.cwd // empty' 2>/dev/null || true) [ -z "$cwd_dir" ] && cwd_dir="${PWD}" +. "$(cd -- "$(dirname -- "${BASH_SOURCE[0]:-$0}")" && pwd)/lib-autodev-paths.sh" 2>/dev/null || true +declare -f autodev_repo_root >/dev/null 2>&1 || autodev_repo_root() { printf '%s\n' "${1:-$PWD}"; } +ADK_ROOT="$(autodev_repo_root "$cwd_dir")" transcript_path=$(printf '%s' "$hook_input" | jq -r '.transcript_path // empty' 2>/dev/null || true) session_key="" [ -n "$transcript_path" ] && session_key=$(basename "$transcript_path") @@ -79,7 +82,7 @@ if command -v git >/dev/null 2>&1; then # exposes transcript_path; otherwise (host has no session identity) # we fall back to workspace-wide scan. find_session_locked_plans() { - local state_file="${cwd_dir}/.claude/autodev-state/session-locks.jsonl" + local state_file="${ADK_ROOT}/.claude/autodev-state/session-locks.jsonl" [ -f "$state_file" ] || return 0 jq -r --arg session "$session_key" \ 'select(.ev == "session-lock" and .session == $session) | .pl // empty' \ diff --git a/tests/hook-contracts.sh b/tests/hook-contracts.sh index 38c3e89..51f6a2e 100755 --- a/tests/hook-contracts.sh +++ b/tests/hook-contracts.sh @@ -638,7 +638,7 @@ PLAN jq -nc '{ev:"lock",pl:"example.md",st:"Locked 2026-05-25T00:00:00Z",h:"abc"}' \ > "$tmp/.claude/autodev-state/in-progress.jsonl" - hooks/scope-lock-complete "$tmp/docs/plans/example.md" --evidence "tests pass" >/dev/null + ( cd "$tmp" && "$REPO_ROOT/hooks/scope-lock-complete" docs/plans/example.md --evidence "tests pass" >/dev/null ) if ! grep -q '\*\*Status:\*\* Complete ' "$tmp/docs/plans/example.md"; then fail "scope-lock-complete: expected plan status to be Complete" @@ -704,7 +704,7 @@ test_scope_lock_complete_requires_lock_file() { PLAN set +e - output="$(hooks/scope-lock-complete "$tmp/docs/plans/example.md" --evidence "tests pass" 2>&1)" + output="$( cd "$tmp" && "$REPO_ROOT/hooks/scope-lock-complete" docs/plans/example.md --evidence "tests pass" 2>&1)" status=$? set -e From 8d5f6fa359870a4f77b4a0df2cb679f70ef41ced Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Wed, 3 Jun 2026 14:45:12 -0400 Subject: [PATCH 13/20] docs(retro): read activation log from canonical repo root (#70 residual) --- skills/post-merge-retrospective/SKILL.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skills/post-merge-retrospective/SKILL.md b/skills/post-merge-retrospective/SKILL.md index 39964c8..2695701 100644 --- a/skills/post-merge-retrospective/SKILL.md +++ b/skills/post-merge-retrospective/SKILL.md @@ -50,7 +50,7 @@ If the PR was opened ad-hoc (no design / plan in `docs/plans/`), this skill exit For each unique CI failure on the branch, ask: was this caught by `verification-before-completion` / `runtime-launch-validation` / something else, or did it slip past every local gate? Slips are gate misses too. 5. **Score skill activations.** - **Primary source: `.claude/autodev-state/in-progress.jsonl`** (written by the `record-activity` PostToolUse hook in any repo — not kit-dev-only). Read phase from the `args` field of `ev:"skill"` entries (the lead's `Skill` invocation carries `args:"--phase=design|plan …"`); the Agent-dispatched reviewer subagent is a separate `ev:"agent"` record without a phase and is ignored for phase attribution. If the jsonl is absent → emit "activation log unavailable" rows, never "script does not exist". `tests/skill-activation-audit.sh` (kit-dev convenience; absent in consumer repos) may be used to cross-check in the kit repo itself — it reports each skill once, so cross-check phase counts against the jsonl's `args=--phase=` entries when both phases are required. + **Primary source: `.claude/autodev-state/in-progress.jsonl`** (written by the `record-activity` PostToolUse hook in any repo — not kit-dev-only). The activation log lives at the **canonical repo root** (`git rev-parse --git-common-dir`'s parent — shared across worktrees, survives worktree cleanup); if the pipeline ran from a worktree checkout the log is written there, not in the worktree directory. When reading from a worktree checkout, resolve the canonical root (`cd && git rev-parse --git-common-dir` → `../`) before reading the log. This closes the v6.4.0 retro's #70 residual. Read phase from the `args` field of `ev:"skill"` entries (the lead's `Skill` invocation carries `args:"--phase=design|plan …"`); the Agent-dispatched reviewer subagent is a separate `ev:"agent"` record without a phase and is ignored for phase attribution. If the jsonl is absent → emit "activation log unavailable" rows, never "script does not exist". `tests/skill-activation-audit.sh` (kit-dev convenience; absent in consumer repos) may be used to cross-check in the kit repo itself — it reports each skill once, so cross-check phase counts against the jsonl's `args=--phase=` entries when both phases are required. Verify the expected pipeline ran. The canonical chain documented in `skills/using-autodev/SKILL.md` is: `brainstorming → adversarial-design-review (design) → writing-plans → adversarial-design-review (plan) → alignment-check → subagent-driven-development → finishing-a-development-branch → pr-monitoring → post-merge-retrospective`. For each gate that was *expected* to fire and didn't, that's a missed-activation. @@ -160,7 +160,7 @@ The retro is intentionally short. Long retros don't get read. The format above f - `docs/plans/` (design, plan, adversarial-review reports — reports now committed by `adversarial-design-review` per the deterministic path rule) - `decisions/` (ADRs cited from the design / plan) - `gh pr view`, `gh pr review-comments`, `gh run list` -- `.claude/autodev-state/in-progress.jsonl` (if present) +- `.claude/autodev-state/in-progress.jsonl` (if present — at the **canonical repo root**, i.e. `git-common-dir`'s parent; resolve from the worktree if reading from one) - `tests/skill-activation-audit.sh` (kit-dev convenience; absent in consumer repos) - `docs/design-guidance.md` or equivalent project guidance, if present From 4046f55dd04b08257fbeae3695ec0a65870f8298 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Wed, 3 Jun 2026 14:45:54 -0400 Subject: [PATCH 14/20] docs(subagents): require a repo-relative Writes: ledger from every subagent (C2) --- agents/team-conventions.md | 8 ++++++++ skills/subagent-driven-development/SKILL.md | 8 ++++++++ .../code-quality-reviewer-prompt.md | 4 ++++ skills/subagent-driven-development/implementer-prompt.md | 5 +++++ .../subagent-driven-development/spec-reviewer-prompt.md | 4 ++++ 5 files changed, 29 insertions(+) diff --git a/agents/team-conventions.md b/agents/team-conventions.md index dac0a5e..ca1cbb5 100644 --- a/agents/team-conventions.md +++ b/agents/team-conventions.md @@ -78,3 +78,11 @@ Team conventions apply identically in both execution modes: company / product-version / incident references. Dependency, runtime, and tooling version numbers are allowed when needed for accurate technical guidance. + +## Writes: ledger + +Every subagent's **final message** must end with a `Writes:` section — one line per file +created or modified, as a **repo-relative path** (e.g. `hooks/record-activity — modified`). +If any write landed outside the expected repo or worktree, flag it explicitly: +`OUT-OF-TREE: `. The orchestrator reads this ledger to confirm +work landed where expected and to relocate or reconcile state before committing. diff --git a/skills/subagent-driven-development/SKILL.md b/skills/subagent-driven-development/SKILL.md index dbe97d5..a9c5c33 100644 --- a/skills/subagent-driven-development/SKILL.md +++ b/skills/subagent-driven-development/SKILL.md @@ -316,6 +316,14 @@ task from any other or identify the caller. See --- +## Write-location transparency + +Every subagent's final message must include a `Writes:` ledger — one line per file +created or modified as a **repo-relative path**, plus `OUT-OF-TREE: ` +for any write outside the expected repo or worktree. The orchestrator reads the ledger +to verify work landed where expected and to reconcile state before committing. +See `agents/team-conventions.md` for the full convention. + ## Red Flags **Never:** diff --git a/skills/subagent-driven-development/code-quality-reviewer-prompt.md b/skills/subagent-driven-development/code-quality-reviewer-prompt.md index f123ab5..401f88f 100644 --- a/skills/subagent-driven-development/code-quality-reviewer-prompt.md +++ b/skills/subagent-driven-development/code-quality-reviewer-prompt.md @@ -91,6 +91,10 @@ Task tool (autodev:code-reviewer): When notified that a task is spec-approved and ready for quality review: - Notify the implementer when quality issues are found - Notify the orchestrator (team-lead on Claude Code) when the task is fully approved + + End your final message with a **Writes:** section listing every file + you created or modified as a repo-relative path. Flag out-of-tree + writes with `OUT-OF-TREE: `. See `agents/team-conventions.md`. ``` **Code reviewer returns:** Strengths, Issues (Critical/Important/Minor), Assessment diff --git a/skills/subagent-driven-development/implementer-prompt.md b/skills/subagent-driven-development/implementer-prompt.md index d4b6ded..ca83015 100644 --- a/skills/subagent-driven-development/implementer-prompt.md +++ b/skills/subagent-driven-development/implementer-prompt.md @@ -27,4 +27,9 @@ Task tool (general-purpose): spec-compliance gate. DM team-lead that the task is ready for merge, including the branch name and latest commit, when CI is green and reviewer is at SHIP-IT. + + End your final message with a **Writes:** section listing every file + you created or modified as a repo-relative path (e.g. + `hooks/record-activity — modified`). Flag out-of-tree writes with + `OUT-OF-TREE: `. See `agents/team-conventions.md`. ``` diff --git a/skills/subagent-driven-development/spec-reviewer-prompt.md b/skills/subagent-driven-development/spec-reviewer-prompt.md index 1bca3c5..cc19d73 100644 --- a/skills/subagent-driven-development/spec-reviewer-prompt.md +++ b/skills/subagent-driven-development/spec-reviewer-prompt.md @@ -82,6 +82,10 @@ Task tool (general-purpose): When notified that a task is ready for review: - Notify the code-reviewer when spec compliance passes - Notify the implementer when issues are found + + End your final message with a **Writes:** section listing every file + you created or modified as a repo-relative path. Flag out-of-tree + writes with `OUT-OF-TREE: `. See `agents/team-conventions.md`. ``` ## Why these steps are inlined From d63974610c7e8d49adfd061e033be53344de3cd1 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Wed, 3 Jun 2026 14:46:34 -0400 Subject: [PATCH 15/20] test(hygiene): forbid operator-home paths in artifacts; fix docs/testing.md leak (C3) --- docs/testing.md | 4 ++-- tests/no-machine-paths.sh | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) create mode 100755 tests/no-machine-paths.sh diff --git a/docs/testing.md b/docs/testing.md index 1a2823e..df0d655 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -149,8 +149,8 @@ python3 tests/claude-code/analyze-token-usage.py ~/.claude/projects//Documents/GitHub/autodev/autodev +SESSION_DIR="$HOME/.claude/projects/-Users--Documents-GitHub-autodev-autodev" # Find recent sessions ls -lt "$SESSION_DIR"/*.jsonl | head -5 diff --git a/tests/no-machine-paths.sh b/tests/no-machine-paths.sh new file mode 100755 index 0000000..65882ec --- /dev/null +++ b/tests/no-machine-paths.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# tests/no-machine-paths.sh — forbid operator-home absolute paths in committed artifacts. +# Catches a real leak (/Users//x) but IGNORES segments and ellipsis, +# so artifacts that DOCUMENT the pattern (this feature's own docs) pass. Lines containing the +# sentinel `path-hygiene-allow` are skipped. Scans docs/ and decisions/. +set -uo pipefail +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/.." && pwd)" +pattern='(/Users/|/home/)[A-Za-z0-9][A-Za-z0-9._-]*' +hits=0 +while IFS= read -r f; do + while IFS=: read -r line content; do + case "$content" in (*path-hygiene-allow*) continue ;; esac + printf 'LEAK: %s:%s: %s\n' "${f#$ROOT/}" "$line" "$content" >&2 + hits=$((hits+1)) + done < <(grep -nE "$pattern" "$f" 2>/dev/null || true) +done < <(find "$ROOT/docs" "$ROOT/decisions" -type f \( -name '*.md' -o -name '*.txt' \) 2>/dev/null) +if [ "$hits" -eq 0 ]; then echo "PASS: no operator-home machine paths in committed artifacts."; else + echo "FAIL: $hits machine-path leak(s) in committed artifacts." >&2; fi +[ "$hits" -eq 0 ] From 3ce6372bcc102bab744cd6d71d5aea19c265dac3 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Wed, 3 Jun 2026 14:46:46 -0400 Subject: [PATCH 16/20] ci: dedicated always-on path-hygiene workflow (C3) --- .github/workflows/path-hygiene.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .github/workflows/path-hygiene.yml diff --git a/.github/workflows/path-hygiene.yml b/.github/workflows/path-hygiene.yml new file mode 100644 index 0000000..69eea70 --- /dev/null +++ b/.github/workflows/path-hygiene.yml @@ -0,0 +1,13 @@ +name: Path Hygiene +on: + push: + pull_request: +permissions: + contents: read +jobs: + path-hygiene: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: No operator-home paths in committed artifacts + run: bash tests/no-machine-paths.sh From 49fc063bea303eed53676836ca44a3575f20ec8f Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Wed, 3 Jun 2026 14:48:16 -0400 Subject: [PATCH 17/20] docs(skills): repo-relative-paths rule for committed artifacts (C3) --- skills/adversarial-design-review/SKILL.md | 1 + skills/brainstorming/SKILL.md | 1 + skills/post-merge-retrospective/SKILL.md | 1 + skills/recording-decisions/SKILL.md | 1 + skills/writing-plans/SKILL.md | 1 + 5 files changed, 5 insertions(+) diff --git a/skills/adversarial-design-review/SKILL.md b/skills/adversarial-design-review/SKILL.md index 98c196e..7c3a1be 100644 --- a/skills/adversarial-design-review/SKILL.md +++ b/skills/adversarial-design-review/SKILL.md @@ -139,6 +139,7 @@ inherits the design's blast radius) and adds: concrete fix or alternative. "This design assumes X" → "Alternative: state X explicitly, and add a fallback if X is false at runtime." 7. **Write AND commit the report.** Derive the path from the artifact filename: drop `.md`, then for `--phase=design` append `-review.md` (e.g. `…-doc-sync-design.md` → `…-doc-sync-design-review.md`); for `--phase=plan` append `-plan-review.md` (e.g. `2026-06-03-…-doc-sync.md` → `2026-06-03-…-doc-sync-plan-review.md`). This matches the existing `docs/plans/2026-05-31-session-owned-lock-claims-design-review.md` convention. The **lead** writes the report text the reviewer produced to that path and commits it alongside the artifact (the subagent has no git authority). Re-runs update the same single per-phase file (append a `## Cycle N` section across cycles); safe under sequential execution. Commit verdict: PASS / FAIL. Use `autodev:condensed-pipeline-writing` for report density unless the user asked for prose. + Committed artifacts use repo-relative paths; illustrate machine paths only with `` segments (e.g. `/Users//…`); never a literal operator-home path. Enforced by `tests/no-machine-paths.sh`. ## Report format diff --git a/skills/brainstorming/SKILL.md b/skills/brainstorming/SKILL.md index 0614eab..a615338 100644 --- a/skills/brainstorming/SKILL.md +++ b/skills/brainstorming/SKILL.md @@ -155,6 +155,7 @@ When the user wants design exploration without execution, they pass `--design-on ## After the Design **Documentation:** +- Committed artifacts use repo-relative paths; illustrate machine paths only with `` segments (e.g. `/Users//…`); never a literal operator-home path. Enforced by `tests/no-machine-paths.sh`. - Write the validated design to `docs/plans/YYYY-MM-DD--design.md` - Include explicit `## Global Design Guidance`, `## Security Review`, `## Infrastructure Impact`, `## Multi-Component Validation`, `## Assumptions`, and `## Rollback` sections (rollback only required for change classes that affect runtime — see the trigger list in `runtime-launch-validation` / `finishing-a-development-branch` Step 1b) - Use elements-of-style:writing-clearly-and-concisely skill if available diff --git a/skills/post-merge-retrospective/SKILL.md b/skills/post-merge-retrospective/SKILL.md index 2695701..1a88ebd 100644 --- a/skills/post-merge-retrospective/SKILL.md +++ b/skills/post-merge-retrospective/SKILL.md @@ -67,6 +67,7 @@ If the PR was opened ad-hoc (no design / plan in `docs/plans/`), this skill exit 7. **Write the retro.** Save to `docs/retros/YYYY-MM-DD--retro.md` using the format below. Commit it. + Committed artifacts use repo-relative paths; illustrate machine paths only with `` segments (e.g. `/Users//…`); never a literal operator-home path. Enforced by `tests/no-machine-paths.sh`. ## Retro format diff --git a/skills/recording-decisions/SKILL.md b/skills/recording-decisions/SKILL.md index 60009ba..f618a88 100644 --- a/skills/recording-decisions/SKILL.md +++ b/skills/recording-decisions/SKILL.md @@ -64,6 +64,7 @@ Skip if no future maintainer would ask "why?" - Proposal/future tense → design doc until accepted. - Editing accepted ADR body → write superseding ADR. - No alternatives → not an ADR. +- Committed artifacts use repo-relative paths; illustrate machine paths only with `` segments (e.g. `/Users//…`); never a literal operator-home path. Enforced by `tests/no-machine-paths.sh`. ## Integration diff --git a/skills/writing-plans/SKILL.md b/skills/writing-plans/SKILL.md index 250f814..d67d886 100644 --- a/skills/writing-plans/SKILL.md +++ b/skills/writing-plans/SKILL.md @@ -257,6 +257,7 @@ git commit -m "feat: add specific feature" - Exact commands with expected output - Reference relevant skills with @ syntax - DRY, YAGNI, TDD, frequent commits +- Committed artifacts use repo-relative paths; illustrate machine paths only with `` segments (e.g. `/Users//…`); never a literal operator-home path. Enforced by `tests/no-machine-paths.sh`. ## Execution Handoff From 687de70a3cdba9663a3e21bf009232a0fd17bca5 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Wed, 3 Jun 2026 14:48:56 -0400 Subject: [PATCH 18/20] ci: run ADK path canonicalization test on hook changes --- .github/workflows/skill-content-check.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/skill-content-check.yml b/.github/workflows/skill-content-check.yml index e7fb0d9..e4a94d3 100644 --- a/.github/workflows/skill-content-check.yml +++ b/.github/workflows/skill-content-check.yml @@ -5,17 +5,21 @@ on: paths: - 'skills/**' - 'agents/**' + - 'hooks/**' - 'tests/skill-content-grep.sh' - 'tests/pipeline-evidence-doc-sync.sh' - 'tests/skill-cross-refs.sh' + - 'tests/adk-path-canonicalization.sh' - '.github/workflows/skill-content-check.yml' pull_request: paths: - 'skills/**' - 'agents/**' + - 'hooks/**' - 'tests/skill-content-grep.sh' - 'tests/pipeline-evidence-doc-sync.sh' - 'tests/skill-cross-refs.sh' + - 'tests/adk-path-canonicalization.sh' - '.github/workflows/skill-content-check.yml' workflow_dispatch: @@ -29,6 +33,8 @@ jobs: - uses: actions/checkout@v4 - name: Check skill content for host-specific tokens run: bash tests/skill-content-grep.sh + - name: ADK path canonicalization resolver and hook wiring + run: bash tests/adk-path-canonicalization.sh - name: Pipeline evidence + doc-sync contracts run: bash tests/pipeline-evidence-doc-sync.sh - name: Skill cross-references resolve From 5fbb30bb67328c564e0712e38289493f09e5580b Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Wed, 3 Jun 2026 14:49:05 -0400 Subject: [PATCH 19/20] chore(release): bump to v6.5.0 --- .claude-plugin/marketplace.json | 2 +- .claude-plugin/plugin.json | 2 +- .cursor-plugin/plugin.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index ac6f347..190f47d 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -9,7 +9,7 @@ { "name": "autodev", "description": "Autonomous development workflow skills for coding agents", - "version": "6.4.0", + "version": "6.5.0", "source": "./", "author": { "name": "Jon Langevin", diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index cb699c9..e54bdd0 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "autodev", "description": "Autonomous development workflow skills for coding agents: design, review, planning, execution, monitoring, and retrospectives", - "version": "6.4.0", + "version": "6.5.0", "author": { "name": "Jon Langevin", "email": "jon@gocodealone.com" diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index 9a0d664..add9424 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -2,7 +2,7 @@ "name": "autodev", "displayName": "Autonomous Dev Kit", "description": "Autonomous development workflow skills for coding agents", - "version": "6.4.0", + "version": "6.5.0", "author": { "name": "Jon Langevin", "email": "jon@gocodealone.com" From 37efa5eeb281a2f87fd308b863fe1b892be5ba34 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Wed, 3 Jun 2026 15:05:12 -0400 Subject: [PATCH 20/20] review: fix worktree prune compare-base (Critical) + extend hygiene gate to skills/ + minors - Critical: scope-lock-complete/abandon prune_jsonl compared against $ADK_ROOT but plan_abs uses $PWD -> stale lock never pruned in a worktree. Both now use $PWD (state files stay at $ADK_ROOT). Group D worktree regression test added (RED w/ bug, GREEN w/ fix). - Important: no-machine-paths.sh now scans skills/ + agents/ too (the skill rule claims enforcement); sanitized 3 pre-existing /Users/jon leaks to placeholders. - Minor: test header 12->11; lib shebang -> shellcheck shell=bash (sourced file). --- hooks/lib-autodev-paths.sh | 5 ++- hooks/scope-lock-abandon | 4 +- hooks/scope-lock-complete | 5 ++- skills/systematic-debugging/CREATION-LOG.md | 2 +- .../root-cause-tracing.md | 2 +- skills/using-git-worktrees/SKILL.md | 2 +- tests/adk-path-canonicalization.sh | 45 ++++++++++++++++++- tests/no-machine-paths.sh | 5 ++- 8 files changed, 60 insertions(+), 10 deletions(-) diff --git a/hooks/lib-autodev-paths.sh b/hooks/lib-autodev-paths.sh index 08249ea..fc1f2cd 100644 --- a/hooks/lib-autodev-paths.sh +++ b/hooks/lib-autodev-paths.sh @@ -1,5 +1,6 @@ -#!/usr/bin/env bash -# lib-autodev-paths.sh — canonical ADK state-root resolver, sourced by state-writing hooks. +# shellcheck shell=bash +# lib-autodev-paths.sh — canonical ADK state-root resolver, SOURCED by state-writing hooks +# (no shebang: this file is sourced, never executed directly — avoids shellcheck SC2148). # autodev_repo_root -> canonical repo root (shared across worktrees, survives worktree removal). # set -u safe: every var is assigned before any read. Sourced; uses `local` (all callers are bash). autodev_repo_root() { diff --git a/hooks/scope-lock-abandon b/hooks/scope-lock-abandon index 768202b..78a9671 100755 --- a/hooks/scope-lock-abandon +++ b/hooks/scope-lock-abandon @@ -106,7 +106,9 @@ prune_jsonl() { [ -n "$line" ] || continue pl=$(printf '%s' "$line" | jq -r '.pl // empty' 2>/dev/null || true) || { rm -f "$tmp"; return 1; } if [ -n "$pl" ]; then - resolved=$(canonical_path_from_base "$ADK_ROOT" "$pl" 2>/dev/null || true) + # Compare against $PWD (same base as plan_abs), NOT $ADK_ROOT — state FILES are at + # $ADK_ROOT (shared) but entry matching must use the invoking base (code-review Critical). + resolved=$(canonical_path_from_base "$PWD" "$pl" 2>/dev/null || true) [ "$resolved" = "$plan_abs" ] && continue fi printf '%s\n' "$line" >> "$tmp" diff --git a/hooks/scope-lock-complete b/hooks/scope-lock-complete index fb3c952..887ade3 100755 --- a/hooks/scope-lock-complete +++ b/hooks/scope-lock-complete @@ -188,7 +188,10 @@ prune_jsonl() { return 1 } if [ -n "$pl" ]; then - resolved=$(canonical_path_from_base "$ADK_ROOT" "$pl" 2>/dev/null || true) + # Compare against $PWD (the same base plan_abs was resolved with), NOT $ADK_ROOT. + # State FILES live at $ADK_ROOT (shared), but entry matching must use the invoking + # base so a worktree completion still prunes its own entry (code-review Critical). + resolved=$(canonical_path_from_base "$PWD" "$pl" 2>/dev/null || true) [ "$resolved" = "$plan_abs" ] && continue fi printf '%s\n' "$line" >> "$tmp" diff --git a/skills/systematic-debugging/CREATION-LOG.md b/skills/systematic-debugging/CREATION-LOG.md index 0d9fa85..9476c7d 100644 --- a/skills/systematic-debugging/CREATION-LOG.md +++ b/skills/systematic-debugging/CREATION-LOG.md @@ -4,7 +4,7 @@ Reference example of extracting, structuring, and bulletproofing a critical skil ## Source Material -Extracted debugging framework from `/Users/jon/.claude/CLAUDE.md`: +Extracted debugging framework from `/Users//.claude/CLAUDE.md`: - 4-phase systematic process (Investigation → Pattern Analysis → Hypothesis → Implementation) - Core mandate: ALWAYS find root cause, NEVER fix symptoms - Rules designed to resist time pressure and rationalization diff --git a/skills/systematic-debugging/root-cause-tracing.md b/skills/systematic-debugging/root-cause-tracing.md index 662111d..487a7d4 100644 --- a/skills/systematic-debugging/root-cause-tracing.md +++ b/skills/systematic-debugging/root-cause-tracing.md @@ -33,7 +33,7 @@ digraph when_to_use { ### 1. Observe the Symptom ``` -Error: git init failed in /Users/jon/project/packages/core +Error: git init failed in /Users//project/packages/core ``` ### 2. Find Immediate Cause diff --git a/skills/using-git-worktrees/SKILL.md b/skills/using-git-worktrees/SKILL.md index 159077c..6331ba8 100644 --- a/skills/using-git-worktrees/SKILL.md +++ b/skills/using-git-worktrees/SKILL.md @@ -188,7 +188,7 @@ You: I'm using the using-git-worktrees skill to set up an isolated workspace. [Run npm install] [Run npm test - 47 passing] -Worktree ready at /Users/jon/myproject/.worktrees/auth +Worktree ready at /Users//myproject/.worktrees/auth Tests passing (47 tests, 0 failures) Ready to implement auth feature ``` diff --git a/tests/adk-path-canonicalization.sh b/tests/adk-path-canonicalization.sh index 4ac3b14..b4982a2 100755 --- a/tests/adk-path-canonicalization.sh +++ b/tests/adk-path-canonicalization.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # tests/adk-path-canonicalization.sh — proves the canonical ADK state-path resolver -# and that all 12 state-writing hooks adopt it. (#70 residual; v6.5.0) +# and that all 11 state-writing hooks adopt it. (#70 residual; v6.5.0) set -uo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/.." && pwd)" LIB="$ROOT/hooks/lib-autodev-paths.sh" @@ -62,4 +62,47 @@ else fi rm -rf "$cwdfb" "$sandbox" +# --- Group D: worktree prune regression (code-review Critical). scope-lock-complete run from a +# linked WORKTREE must prune the session-lock entry at the CANONICAL (main) root. With the bug +# (prune compares against $ADK_ROOT instead of $PWD), the worktree-relative entry never matches +# the main-rooted resolution, so the stale lock is never pruned. +if command -v jq >/dev/null 2>&1; then + d="$(mktemp -d)"; mkdir -p "$d/main" + ( cd "$d/main" && git init -q && git -c user.email=a@b -c user.name=x commit -q --allow-empty -m init \ + && git worktree add -q ../wt >/dev/null 2>&1 ) + wt="$d/wt"; mainroot="$(cd "$d/main" && pwd -P)" + mkdir -p "$wt/docs/plans" "$wt/tests" "$mainroot/.claude/autodev-state" + cp "$ROOT/tests/plan-scope-check.sh" "$wt/tests/plan-scope-check.sh" 2>/dev/null; chmod +x "$wt/tests/plan-scope-check.sh" 2>/dev/null + cat > "$wt/docs/plans/p.md" <<'PLAN' +# P + +## Scope Manifest + +**PR Count:** 1 +**Tasks:** 1 +**Out of scope:** +- (none) + +**PR Grouping:** + +| PR # | Title | Tasks | Branch | +|------|-------|-------|--------| +| 1 | P | Task 1 | feat/p | + +**Status:** Locked 2026-05-25T00:00:00Z + +### Task 1: P +PLAN + bash "$ROOT/hooks/scope-lock-apply" "$wt/docs/plans/p.md" >/dev/null 2>&1 + jq -nc --arg pl "docs/plans/p.md" '{ev:"session-lock",session:"s.jsonl",pl:$pl}' \ + > "$mainroot/.claude/autodev-state/session-locks.jsonl" + ( cd "$wt" && "$ROOT/hooks/scope-lock-complete" docs/plans/p.md --evidence "x" >/dev/null 2>&1 ) + if grep -q 'docs/plans/p.md' "$mainroot/.claude/autodev-state/session-locks.jsonl" 2>/dev/null; then + fail "worktree prune: session-lock NOT pruned at canonical root (compare-base bug)" + else + pass "worktree prune: scope-lock-complete from worktree pruned the canonical session-lock" + fi + rm -rf "$d" +fi + echo ""; echo "Results: $failures failure(s)"; [ "$failures" -eq 0 ] diff --git a/tests/no-machine-paths.sh b/tests/no-machine-paths.sh index 65882ec..b268c9f 100755 --- a/tests/no-machine-paths.sh +++ b/tests/no-machine-paths.sh @@ -2,7 +2,8 @@ # tests/no-machine-paths.sh — forbid operator-home absolute paths in committed artifacts. # Catches a real leak (/Users//x) but IGNORES segments and ellipsis, # so artifacts that DOCUMENT the pattern (this feature's own docs) pass. Lines containing the -# sentinel `path-hygiene-allow` are skipped. Scans docs/ and decisions/. +# sentinel `path-hygiene-allow` are skipped. Scans docs/, decisions/, and skills/ (all committed +# artifact dirs — the skill-rule's "enforced by this script" claim must hold for skills/ too). set -uo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/.." && pwd)" pattern='(/Users/|/home/)[A-Za-z0-9][A-Za-z0-9._-]*' @@ -13,7 +14,7 @@ while IFS= read -r f; do printf 'LEAK: %s:%s: %s\n' "${f#$ROOT/}" "$line" "$content" >&2 hits=$((hits+1)) done < <(grep -nE "$pattern" "$f" 2>/dev/null || true) -done < <(find "$ROOT/docs" "$ROOT/decisions" -type f \( -name '*.md' -o -name '*.txt' \) 2>/dev/null) +done < <(find "$ROOT/docs" "$ROOT/decisions" "$ROOT/skills" "$ROOT/agents" -type f \( -name '*.md' -o -name '*.txt' \) 2>/dev/null) if [ "$hits" -eq 0 ]; then echo "PASS: no operator-home machine paths in committed artifacts."; else echo "FAIL: $hits machine-path leak(s) in committed artifacts." >&2; fi [ "$hits" -eq 0 ]