Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions scripts/test-root-citations.sh
Original file line number Diff line number Diff line change
@@ -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
11 changes: 6 additions & 5 deletions scripts/validate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,9 @@ import sys, re, pathlib
root = pathlib.Path(sys.argv[1])
shared = sorted(p.name for p in (root / "references").glob("*.md"))
# A bare `references/<name>` inside a skill reads as skill-local; these must say where it lives.
root_qualified = ["dispatch-degradation.md"]
markers = ("plugin root", "repo root", "repo-root")
root_qualified = ["dispatch-degradation.md", "question-tools.md"]
# 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:
Expand All @@ -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"(?<![\w./-])references/" + re.escape(name), line)
if cited and not any(m in line for m in markers):
bad.append(f"{f.relative_to(root)}:{n} cites references/{name} without naming the plugin root")
for cited in re.finditer(r"(?<![\w./-])(?:\./)?references/" + re.escape(name), line):
if not qualifier.match(line, cited.end()):
bad.append(f"{f.relative_to(root)}:{n} cites references/{name} without naming the plugin root")
if bad:
print("FAIL: " + "; ".join(bad)); sys.exit(1)
print(f"ok: shared references ({', '.join(shared)}) have one copy; root-qualified citations name the plugin root")
Expand Down
2 changes: 1 addition & 1 deletion skills/compound-refresh/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion skills/compound/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion skills/designing/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
2 changes: 1 addition & 1 deletion skills/planning/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion skills/release/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,7 @@ Present one review packet before asking anything:
`Release v<resolved-version>`.

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
Expand Down
2 changes: 1 addition & 1 deletion skills/shipping/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ gh pr merge <number> --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<N>:` 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<N>:` 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:

Expand Down
Loading