From 297036ca8cbcd4a9f946171d4e284fc196cd2bd4 Mon Sep 17 00:00:00 2001 From: Jaehoon You <158752+teslamint@users.noreply.github.com> Date: Mon, 28 Sep 2026 10:46:24 +0900 Subject: [PATCH 1/2] fix(skills): pin question-tools citations to the plugin root references/question-tools.md exists only at the repo root, but six skills cited it as a bare path that reads as skill-local. Add it to the check 5a root_qualified list and name the plugin root at each flagged citation. No conformance repin: no source-manifest clause section changed. Tested: bash scripts/validate.sh (ALL CHECKS PASSED, run outside the sandbox) Assisted-By: Claude Code --- scripts/validate.sh | 2 +- skills/compound-refresh/SKILL.md | 2 +- skills/compound/SKILL.md | 2 +- skills/designing/SKILL.md | 2 +- skills/planning/SKILL.md | 2 +- skills/release/SKILL.md | 2 +- skills/shipping/SKILL.md | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/scripts/validate.sh b/scripts/validate.sh index 25d907f..d0806dc 100755 --- a/scripts/validate.sh +++ b/scripts/validate.sh @@ -105,7 +105,7 @@ import sys, re, pathlib root = pathlib.Path(sys.argv[1]) shared = sorted(p.name for p in (root / "references").glob("*.md")) # A bare `references/` inside a skill reads as skill-local; these must say where it lives. -root_qualified = ["dispatch-degradation.md"] +root_qualified = ["dispatch-degradation.md", "question-tools.md"] markers = ("plugin root", "repo root", "repo-root") bad = [] for name in root_qualified: diff --git a/skills/compound-refresh/SKILL.md b/skills/compound-refresh/SKILL.md index 3635d6b..f0f797c 100644 --- a/skills/compound-refresh/SKILL.md +++ b/skills/compound-refresh/SKILL.md @@ -19,7 +19,7 @@ Strip a leading `mode:headless` token; the remainder is a scope hint. | Mode | Behavior | |---|---| -| **Interactive** (default) | Classify, then apply Update/Consolidate/Delete-with-clear-evidence directly; ask via the blocking-question pattern (`references/question-tools.md`) only when the action is genuinely ambiguous, or before a Replace/Delete whose evidence isn't unambiguous | +| **Interactive** (default) | Classify, then apply Update/Consolidate/Delete-with-clear-evidence directly; ask via the blocking-question pattern (`references/question-tools.md` at the plugin root) only when the action is genuinely ambiguous, or before a Replace/Delete whose evidence isn't unambiguous | | **Headless** | No questions. Classify every candidate per Phase 2, but **apply zero writes this version** — every action, including unambiguous ones, is reported under Recommended with full rationale. This is a deliberate narrowing from a fully-autonomous headless sweep; see Out of Scope | ## Scope Selection diff --git a/skills/compound/SKILL.md b/skills/compound/SKILL.md index 932c18f..d47e500 100644 --- a/skills/compound/SKILL.md +++ b/skills/compound/SKILL.md @@ -19,7 +19,7 @@ Strip a leading `mode:headless` token from arguments before treating the remaind | Mode | Behavior | |---|---| -| **Interactive** (default) | Ask Full vs. Lightweight via the blocking-question pattern in `references/question-tools.md`; Full mode may also offer session-history search | +| **Interactive** (default) | Ask Full vs. Lightweight via the blocking-question pattern in `references/question-tools.md` at the plugin root; Full mode may also offer session-history search | | **Headless** | No questions. Run **Full mode** without session history. Apply Discoverability edits silently if a gap exists. Skip the optional-review phase. End with the exact terminal signal from `schemas/headless-contract.md` | ## Full Mode diff --git a/skills/designing/SKILL.md b/skills/designing/SKILL.md index e529f5d..b81ba83 100644 --- a/skills/designing/SKILL.md +++ b/skills/designing/SKILL.md @@ -63,7 +63,7 @@ Before proposing approaches, scan the opening for gaps: evidence, specificity, c ## Step 6: Collaborative Dialogue -Follow `references/question-tools.md` for the blocking-tool table and the open-ended-vs-menu test. Ask what the user is already thinking before offering your own framing. Start broad (problem, users, value), then narrow (constraints, exclusions, edge cases). All rigor-gap probes from Step 5 must fire before Step 7. +Follow `references/question-tools.md` (plugin root) for the blocking-tool table and the open-ended-vs-menu test. Ask what the user is already thinking before offering your own framing. Start broad (problem, users, value), then narrow (constraints, exclusions, edge cases). All rigor-gap probes from Step 5 must fire before Step 7. **Integration check before exiting dialogue**: mentally combine what's been said so far (user-stated X + user-stated Y + your default Z) and probe any non-obvious downstream consequence the one-question-at-a-time flow hasn't surfaced yet — one probe per genuine combination effect. diff --git a/skills/planning/SKILL.md b/skills/planning/SKILL.md index 6f31d0b..90c2155 100644 --- a/skills/planning/SKILL.md +++ b/skills/planning/SKILL.md @@ -31,7 +31,7 @@ When skipping, attest that all four conditions hold, citing the work's scope, th ## 2. Scope confirmation -One compressed confirmation before spending research or authoring budget: state the scope read from the spec (stated intent plus any material forks) and ask the user to confirm or redirect, using the blocking-question pattern in `references/question-tools.md`. Skip the confirmation only for a trivial, single-unit plan with zero forks — proceed and say so in one line. When invoked as a dispatched phase worker under an AUTO gate (release-loop pipeline), this question is the orchestrator's to ask or waive — state the read scope in the ledger/log and proceed rather than blocking. +One compressed confirmation before spending research or authoring budget: state the scope read from the spec (stated intent plus any material forks) and ask the user to confirm or redirect, using the blocking-question pattern in `references/question-tools.md` at the plugin root. Skip the confirmation only for a trivial, single-unit plan with zero forks — proceed and say so in one line. When invoked as a dispatched phase worker under an AUTO gate (release-loop pipeline), this question is the orchestrator's to ask or waive — state the read scope in the ledger/log and proceed rather than blocking. ## 3. Context research diff --git a/skills/release/SKILL.md b/skills/release/SKILL.md index 6bacf05..1b98f43 100644 --- a/skills/release/SKILL.md +++ b/skills/release/SKILL.md @@ -384,7 +384,7 @@ Present one review packet before asking anything: `Release v`. Use the harness's blocking question tool per -`references/question-tools.md`. Ask one single-select question with these +`references/question-tools.md` at the plugin root. Ask one single-select question with these distinct outcomes: **Approve this exact release** (recommended), **Revise the draft or version**, and **Cancel the release**. A revision returns to Draft or Version and presents a new complete packet; rewrite `.release/draft.md` before diff --git a/skills/shipping/SKILL.md b/skills/shipping/SKILL.md index 3984e4a..4452f4d 100644 --- a/skills/shipping/SKILL.md +++ b/skills/shipping/SKILL.md @@ -224,7 +224,7 @@ gh pr merge --squash --delete-branch [--auto] Runs only for the **merge** and **discard** outcomes -- "keep as-is" and "PR open for iteration" always preserve the worktree, since the user needs it alive. -Merge ordering invariant, never reordered: **merge -> verify tests on the merged result -> run every eligible approved-plan pre-removal transition -> remove worktree -> delete branch.** This transition path applies only to a chosen merge outcome. A transition is eligible only when `shipping` is invoked by `release-loop` and the approved, body-sealed plan contains a `## Release-loop Ship-cleanup transition R:` section with an explicit owner, executable acceptance, and matching mutation/failure-state matrix row. Revalidate the plan `body_seal` immediately before running it. Execute eligible transitions in heading order after merged-result verification and before worktree removal; any failure blocks cleanup. A local transition may run headlessly only when its matrix explicitly permits that outcome and its boundary proof makes every outward target unreachable; an outward transition still requires first-hand confirmation at the point of risk. Only remove a worktree this tooling created (path under `.worktrees/` or `worktrees/`); anything else is harness- or user-owned -- leave it. The typed `discard` path executes no approved-plan transition: after `references/question-tools.md` confirmation its separate order is **typed discard confirmation -> remove worktree -> delete branch**, with force-delete (`git branch -D`) only after worktree removal succeeds. +Merge ordering invariant, never reordered: **merge -> verify tests on the merged result -> run every eligible approved-plan pre-removal transition -> remove worktree -> delete branch.** This transition path applies only to a chosen merge outcome. A transition is eligible only when `shipping` is invoked by `release-loop` and the approved, body-sealed plan contains a `## Release-loop Ship-cleanup transition R:` section with an explicit owner, executable acceptance, and matching mutation/failure-state matrix row. Revalidate the plan `body_seal` immediately before running it. Execute eligible transitions in heading order after merged-result verification and before worktree removal; any failure blocks cleanup. A local transition may run headlessly only when its matrix explicitly permits that outcome and its boundary proof makes every outward target unreachable; an outward transition still requires first-hand confirmation at the point of risk. Only remove a worktree this tooling created (path under `.worktrees/` or `worktrees/`); anything else is harness- or user-owned -- leave it. The typed `discard` path executes no approved-plan transition: after `references/question-tools.md` (plugin root) confirmation its separate order is **typed discard confirmation -> remove worktree -> delete branch**, with force-delete (`git branch -D`) only after worktree removal succeeds. For **every merge outcome**, merged-result verification is an executable prerequisite for cleanup: From 2a31e339ed0b5d7c4906191e275599f1dbba528e Mon Sep 17 00:00:00 2001 From: Jaehoon You <158752+teslamint@users.noreply.github.com> Date: Mon, 28 Sep 2026 15:12:48 +0900 Subject: [PATCH 2/2] fix(validate): require the root qualifier on each shared citation Check 5a passed a bare `references/` citation whenever "plugin root" appeared anywhere on the same line, and it missed the `./references/` form. Each citation now needs its own qualifier directly after it ("at the plugin root" or "(plugin root" / "(repo root" / "(repo-root"), and the `./` form is detected. Add scripts/test-root-citations.sh: 12 fixture cases that run the check body extracted from validate.sh. Addresses CodeRabbit review on #41. Assisted-By: Claude Code --- scripts/test-root-citations.sh | 66 ++++++++++++++++++++++++++++++++++ scripts/validate.sh | 9 ++--- 2 files changed, 71 insertions(+), 4 deletions(-) create mode 100755 scripts/test-root-citations.sh diff --git a/scripts/test-root-citations.sh b/scripts/test-root-citations.sh new file mode 100755 index 0000000..7477ddb --- /dev/null +++ b/scripts/test-root-citations.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +# Fixture harness for the shared-reference citation check (scripts/validate.sh +# check 5a.). Extracts that check's Python body from validate.sh, runs it +# against a disposable mktemp -d tree holding one skill line per case, and +# asserts pass or fail. Never mutates the real skills/ or references/ files. +# +# Manual invocation only: not wired into scripts/validate.sh or any CI. +set -uo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +FAIL_COUNT=0 + +CHECK="$(awk '/^# 5a\./{found=1; next} found && /<<'\''PY'\''/{body=1; next} body && /^PY$/{exit} body' "$ROOT/scripts/validate.sh")" +if [[ -z "$CHECK" ]]; then + echo "harness error: check 5a body not found in scripts/validate.sh" >&2 + exit 1 +fi + +run_line() { + local line="$1" dir out code + dir="$(mktemp -d 2>&1)" || { echo " harness error: mktemp -d failed: $dir" >&2; return 2; } + mkdir -p "$dir/references" "$dir/skills/demo" + : > "$dir/references/dispatch-degradation.md" + : > "$dir/references/question-tools.md" + printf '%s\n' "$line" > "$dir/skills/demo/SKILL.md" + out="$(python3 - "$dir" <<<"$CHECK" 2>&1)"; code=$? + rm -rf "$dir" + printf '%s\n' "$out" + return $code +} + +expect() { + local want="$1" label="$2" line="$3" out code + out="$(run_line "$line")"; code=$? + if [[ $code -eq 2 ]]; then + echo "Case $label: FAIL (harness)"; FAIL_COUNT=$((FAIL_COUNT + 1)); return + fi + if [[ "$want" == pass && $code -eq 0 ]] || [[ "$want" == fail && $code -ne 0 && "$out" == *"without naming the plugin root"* ]]; then + echo "Case $label: pass" + else + echo "Case $label: FAIL (expected $want, exit $code)" + printf ' %s\n' "$out" + FAIL_COUNT=$((FAIL_COUNT + 1)) + fi +} + +expect pass "at the plugin root" 'Follow `references/question-tools.md` at the plugin root.' +expect pass "(plugin root)" 'Follow `references/question-tools.md` (plugin root) for the table.' +expect pass "(plugin root; ...)" 'Per `references/dispatch-degradation.md` (plugin root; native parallel first).' +expect pass "(repo root, shared)" 'Per `references/dispatch-degradation.md` (repo root, shared): dispatch.' +expect pass "(repo-root shared ...)" 'Follow `references/dispatch-degradation.md` (repo-root shared reference) unmodified.' +expect pass "./ form qualified" 'Follow `./references/question-tools.md` at the plugin root.' +expect pass "both qualified" 'See `references/question-tools.md` (plugin root) and `references/dispatch-degradation.md` (plugin root).' +expect pass "longer path not a cite" 'The copy at `skills/demo/references/question-tools.md` is removed.' +expect fail "bare" 'Follow `references/question-tools.md` for the table.' +expect fail "./ form bare" 'Follow `./references/question-tools.md` for the table.' +expect fail "marker elsewhere on line" 'Follow `references/question-tools.md`; the plugin root holds other files.' +expect fail "one of two qualified" 'See `references/question-tools.md` and `references/dispatch-degradation.md` (plugin root).' + +echo +if [[ $FAIL_COUNT -eq 0 ]]; then + echo "ALL CASES PASSED" +else + echo "$FAIL_COUNT CASE(S) FAILED" + exit 1 +fi diff --git a/scripts/validate.sh b/scripts/validate.sh index d0806dc..f8f0b26 100755 --- a/scripts/validate.sh +++ b/scripts/validate.sh @@ -106,7 +106,8 @@ root = pathlib.Path(sys.argv[1]) shared = sorted(p.name for p in (root / "references").glob("*.md")) # A bare `references/` inside a skill reads as skill-local; these must say where it lives. root_qualified = ["dispatch-degradation.md", "question-tools.md"] -markers = ("plugin root", "repo root", "repo-root") +# The marker must qualify the citation itself: "`references/x.md` at the plugin root" or "(plugin root". +qualifier = re.compile(r"`?\s*(?:\(\s*|at the )(?:plugin root|repo root|repo-root)") bad = [] for name in root_qualified: if name not in shared: @@ -122,9 +123,9 @@ for f in sorted(root.glob("skills/**/*.md")): continue for n, line in enumerate(lines, 1): for name in root_qualified: - cited = re.search(r"(?