From 102f4a548ea890b713e81691abbf8a9e4d1414a6 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Tue, 21 Jul 2026 06:01:27 -0400 Subject: [PATCH 1/2] fix(docs-hygiene,session-flow): wire remaining memory_dir consumers to shared parse-concern-value helper audit-noise's convention-roots scan hand-rolled the same naive ${val%%#*}-first strip #482 fixed at its two hot sites (interior-# truncation in a quoted value) plus a worse defect unique to this copy: collapsing ALL interior whitespace via ${val//[[:space:]]/}. noise-shapes.sh now resolves memory_dir/contract_dir through the shared parse-concern-value.sh helper (materialized from lib/parse-concern-value.sh into scripts/lib/parse-concern-value.sh, registered in scripts/sync-parse-concern-value.sh's copies array), matching the ratified upstream-SSOT + sync-materialization pattern from #482/#756. The session-flow handoff skill's structure.md placeholder note ("resolve it first", no mechanism named) now points at the shared helper via the retro skill's Phase 1.1 as the worked call form -- a doc pointer only, since handoff has no script of its own to rewire. The broader tracker-seam-resolution class (#470/#469/#432/#365/#369) is explicitly out of scope per the operator's ratified bounded-widen ruling. Closes #474 --- .../docs-hygiene/.claude-plugin/plugin.json | 2 +- plugins/docs-hygiene/CHANGELOG.md | 15 +++ .../skills/audit-noise/scripts/detect.test.sh | 20 +++ .../audit-noise/scripts/lib/noise-shapes.sh | 7 +- .../scripts/lib/parse-concern-value.sh | 116 ++++++++++++++++++ .../session-flow/.claude-plugin/plugin.json | 2 +- plugins/session-flow/CHANGELOG.md | 10 ++ .../skills/handoff/context/structure.md | 5 +- scripts/sync-parse-concern-value.sh | 1 + 9 files changed, 170 insertions(+), 8 deletions(-) create mode 100644 plugins/docs-hygiene/skills/audit-noise/scripts/lib/parse-concern-value.sh diff --git a/plugins/docs-hygiene/.claude-plugin/plugin.json b/plugins/docs-hygiene/.claude-plugin/plugin.json index 9a26334bbe..1fec29347b 100644 --- a/plugins/docs-hygiene/.claude-plugin/plugin.json +++ b/plugins/docs-hygiene/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "docs-hygiene", - "version": "0.8.0", + "version": "0.8.1", "description": "Documentation-hygiene toolkit of six skills: compress (flavor-trim markdown with a semantic-diff safety net), audit-noise (classify markdown noise), extract-ssot (deduplicate repeated content into a single source of truth), audit-encapsulation (detect citations into skill-private surfaces), rename-references (sweep stale references after renames), and audit-derivability (classify whether a whole document earns its existence — could a fresh agent re-derive it from the code?).", "author": { "name": "Melodic Software", diff --git a/plugins/docs-hygiene/CHANGELOG.md b/plugins/docs-hygiene/CHANGELOG.md index cf924dcf50..eb7c810502 100644 --- a/plugins/docs-hygiene/CHANGELOG.md +++ b/plugins/docs-hygiene/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog — docs-hygiene plugin +## [0.8.1] — 2026-07-21 + +### Fixed + +- **`audit-noise`'s convention-roots scan no longer truncates a quoted + `memory_dir`/`contract_dir` at an interior `#`, collapses interior + whitespace, or leaves quotes unstripped.** The hand-rolled + `${val%%#*}` + `${val//[[:space:]]/}` + ad hoc quote-peel in + `scripts/lib/noise-shapes.sh`'s `audit_noise_convention_roots_pattern` is + gone; resolution now routes through the shared `parse-concern-value.sh` + helper (materialized from `lib/parse-concern-value.sh`), which resolves + surrounding quotes and a comment-aware strip in the correct order and + never mangles interior whitespace. Held behavior: trailing-slash + normalization, and the `.`/`.work`/`docs/topics` default-root exclusions. + ## [0.8.0] — 2026-07-20 ### Added diff --git a/plugins/docs-hygiene/skills/audit-noise/scripts/detect.test.sh b/plugins/docs-hygiene/skills/audit-noise/scripts/detect.test.sh index babd57f138..9b89d13db2 100755 --- a/plugins/docs-hygiene/skills/audit-noise/scripts/detect.test.sh +++ b/plugins/docs-hygiene/skills/audit-noise/scripts/detect.test.sh @@ -269,6 +269,26 @@ assert_contains "configured contract root flags concrete slices" "$conf_out" "pr assert_contains "configured memory root flags concrete slices" "$conf_out" ".scratch/foo/" assert_not_contains "configured bare concern root stays exempt" "$conf_out" ".scratch/reviews/" +# A quoted memory_dir with an interior '#' and a trailing comment: the old +# hand-rolled `${val%%#*}`-first strip truncated this to `.scratch` (dropping +# everything from the interior '#' on, including the closing quote), so the +# configured root never matched. The shared parse-concern-value.sh helper +# resolves quotes before stripping comments, keeping the interior '#'. +CONF_ROOT_QUOTED="$TEST_TMPDIR/configured-repo-quoted" +mkdir -p "$CONF_ROOT_QUOTED/.claude" +cat >"$CONF_ROOT_QUOTED/.claude/topic-docs.yaml" <<'EOF' +memory_dir: ".scratch#dir/" # trailing comment must not eat the quoted value +EOF +CONFIGURED_QUOTED="$TEST_TMPDIR/configured-quoted.md" +cat >"$CONFIGURED_QUOTED" <<'EOF' +# Configured-roots quoted fixture + +Notes at .scratch#dir/foo/EXPLORE.md. +EOF +conf_quoted_out="$(AUDIT_NOISE_REPO_ROOT="$CONF_ROOT_QUOTED" bash "$DETECT" "$CONFIGURED_QUOTED")" +assert_contains "quoted memory_dir with interior # and trailing comment flags concrete slice" \ + "$conf_quoted_out" ".scratch#dir/foo/" + # --- Final report -------------------------------------------------------------------- if [[ "$FAILED" -eq 0 ]]; then diff --git a/plugins/docs-hygiene/skills/audit-noise/scripts/lib/noise-shapes.sh b/plugins/docs-hygiene/skills/audit-noise/scripts/lib/noise-shapes.sh index f5df1a12a0..5fa58a97b3 100644 --- a/plugins/docs-hygiene/skills/audit-noise/scripts/lib/noise-shapes.sh +++ b/plugins/docs-hygiene/skills/audit-noise/scripts/lib/noise-shapes.sh @@ -18,13 +18,12 @@ audit_noise_trim_excerpt() { # scan (a doc may cite the defaults regardless of local config). audit_noise_convention_roots_pattern() { if [[ -z "${AUDIT_NOISE_ROOTS_PATTERN:-}" ]]; then - local pattern='\.work|docs/topics' key val yaml + local pattern='\.work|docs/topics' key val yaml lib_dir yaml="${AUDIT_NOISE_REPO_ROOT:-.}/.claude/topic-docs.yaml" + lib_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" if [[ -f "$yaml" ]]; then for key in memory_dir contract_dir; do - val=$(sed -n "s/^${key}:[[:space:]]*//p" "$yaml" | head -1) - val="${val%%#*}"; val="${val//[[:space:]]/}"; val="${val%/}" - val="${val#\"}"; val="${val%\"}"; val="${val#\'}"; val="${val%\'}" + val=$("$lib_dir/parse-concern-value.sh" "$yaml" "$key") if [[ "$key" == 'contract_dir' && -n "$val" ]]; then AUDIT_NOISE_CONTRACT_ROOT="$val" fi diff --git a/plugins/docs-hygiene/skills/audit-noise/scripts/lib/parse-concern-value.sh b/plugins/docs-hygiene/skills/audit-noise/scripts/lib/parse-concern-value.sh new file mode 100644 index 0000000000..21110a2bd9 --- /dev/null +++ b/plugins/docs-hygiene/skills/audit-noise/scripts/lib/parse-concern-value.sh @@ -0,0 +1,116 @@ +#!/usr/bin/env bash +# Resolve a single scalar value from a topic-docs concern file the way every +# consuming plugin must: quote-aware, comment-safe, whitespace-trimmed, +# trailing-slash-normalized — with a caller-supplied fallback for the case the +# key is absent. +# +# Why this exists: the topic-docs seam (`.claude/topic-docs.yaml` `memory_dir` +# and siblings) was parsed inline at each consumer with a naive +# `val="${val%%#*}"` FIRST — which truncates a legitimately-quoted value that +# contains `#` (`"a#b"` -> `"a`) and strips comments before quotes are resolved. +# Centralizing the parse fixes it once for every consumer instead of re-forking +# the same bug per site. +# +# SINGLE SOURCE OF TRUTH: lib/parse-concern-value.sh at the marketplace repo +# root. The copies materialized into consuming plugins exist because installed +# plugins are cache-isolated and must be self-contained — never edit a copy. +# Edit the source and run scripts/sync-parse-concern-value.sh; CI rejects +# drifted copies. +# +# Usage: +# parse-concern-value.sh [fallback] +# +# path to the concern file (e.g. .claude/topic-docs.yaml) +# scalar key to read (e.g. memory_dir) +# [fallback] value to emit when the key is absent/empty — the caller's +# already-resolved rung-2 location (a save-point convention +# declared in CLAUDE.md / .claude/rules). Prose is an +# inference source, not a runtime authority, so the caller +# infers it and passes it in; this script never reads prose. +# +# Output: the resolved value on stdout, or empty when nothing resolves (the +# caller applies the documented default, e.g. `.work`). Always exits 0 for a +# well-formed invocation. +# +# Resolution order (mirrors the topic-docs contract's non-interactive degrade): +# 1. key present and non-empty in the concern file -> its parsed value +# 2. else the caller-supplied fallback (if non-empty) +# 3. else empty (interactive/inferred-layout rungs are the caller's job) +set -uo pipefail + +if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then + cat <<'EOF' +parse-concern-value.sh — resolve a scalar value from a topic-docs concern file. + +Usage: + parse-concern-value.sh [fallback] + +Reads from , quote-aware and comment-safe: a `#` inside a +quoted value is preserved; a trailing ` # comment` on an unquoted value is +stripped; surrounding quotes are peeled, surrounding whitespace trimmed, and a +trailing slash normalized. Emits [fallback] when the key is absent/empty, or +nothing when neither resolves. Always exits 0 for a well-formed invocation. +EOF + exit 0 +fi + +concern_file="${1:-}" +key="${2:-}" +fallback="${3:-}" + +if [[ -z "$concern_file" || -z "$key" ]]; then + echo "parse-concern-value: usage: parse-concern-value.sh [fallback]" >&2 + exit 2 +fi + +# Peel a matching surrounding quote pair, strip a comment (quote-aware), trim +# surrounding whitespace, normalize a trailing slash — in THAT order. Quote +# resolution comes first so a `#` inside quotes is never mistaken for a comment. +strip_value() { + local raw="$1" val + raw="${raw//$'\r'/}" # CRLF guard: Git Bash leaves a trailing \r on read lines + # Trim leading whitespace (sed's [[:space:]]* already ate the post-colon run, + # but a re-used helper must not assume its caller's extraction). + raw="${raw#"${raw%%[![:space:]]*}"}" + + case "$raw" in + '"'*) + # Double-quoted: value is the span up to the closing quote; anything after + # it (e.g. a trailing comment) is discarded. `#` inside is literal. + val="${raw#\"}" + val="${val%%\"*}" + ;; + "'"*) + val="${raw#\'}" + val="${val%%\'*}" + ;; + *) + # Unquoted: strip a comment at a `#` that starts the value (a comment-only + # value like `# use default` — YAML-null, must resolve to empty so the + # fallback fires) or is preceded by whitespace (` #…`); a `#` adjacent to a + # non-space char (`a#b`, `.work/#t`) is part of the scalar. Then trim + # trailing whitespace. + val="$raw" + val="$(printf '%s' "$val" | sed -e 's/^#.*$//' -e 's/[[:space:]]#.*$//')" + val="${val%"${val##*[![:space:]]}"}" + ;; + esac + + val="${val%/}" # normalize a single trailing slash + printf '%s' "$val" +} + +resolved="" +if [[ -f "$concern_file" ]]; then + # sed anchors on the literal key; keys are `[a-z_]+` identifiers, no metachars. + raw_line=$(sed -n "s/^${key}:[[:space:]]*//p" "$concern_file" | head -1) + if [[ -n "$raw_line" ]]; then + resolved=$(strip_value "$raw_line") + fi +fi + +if [[ -z "$resolved" ]]; then + resolved="$fallback" +fi + +printf '%s\n' "$resolved" diff --git a/plugins/session-flow/.claude-plugin/plugin.json b/plugins/session-flow/.claude-plugin/plugin.json index b6e0b6c249..2c6d1d5822 100644 --- a/plugins/session-flow/.claude-plugin/plugin.json +++ b/plugins/session-flow/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "session-flow", - "version": "0.11.0", + "version": "0.11.1", "description": "Session-lifecycle toolkit of eight skills: workflow (navigate a staged dev workflow and suggest the next stage), handoff (write a save-point and resume prompt for /clear, with optional --bg background-agent launch), keep-going (recover and continue after any interruption — inventory off-thread work, inspect its real state, resume or restart it, then continue the main task), clean-stop (get to a durable, linked stopping point before the machine may go away — sweep every repo/worktree for uncommitted, unpushed, or PR-less work, push it durable, put breadcrumbs in PR/issue bodies, then give a free-and-clear verdict), retro (structured end-of-session retrospective with transcript metrics and learning codification), running-retro (in-flight retrospective checkpoints that spawn a subagent to analyze the transcript so far and append classified findings to a cumulative running ledger — capture and route only, the live counterpart to retro), orchestrate (arm a session or worker with proactive-orchestration imperatives), and reanchor (verify a session's working assumptions are still true against live reality — referenced PRs/issues/branches, base-branch drift, renamed/version-drifted surfaces, stale memory-tier files — before building on them).", "author": { "name": "Melodic Software", diff --git a/plugins/session-flow/CHANGELOG.md b/plugins/session-flow/CHANGELOG.md index 8c1f94b48d..ad7d1abce9 100644 --- a/plugins/session-flow/CHANGELOG.md +++ b/plugins/session-flow/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog — session-flow plugin +## [0.11.1] — 2026-07-21 + +Changed: + +- handoff: the "Full-path write procedure" doc's `MEMORY_ROOT` placeholder + note now points at the shared `parse-concern-value.sh` helper (the + retro skill's Phase 1.1 snippet is the worked call form) instead of a + bare "resolve it first" reminder with no mechanism named. Doc pointer + only — the handoff skill has no script of its own to rewire. + ## [0.11.0] — 2026-07-20 Added: diff --git a/plugins/session-flow/skills/handoff/context/structure.md b/plugins/session-flow/skills/handoff/context/structure.md index b3e91abfae..0f795674db 100644 --- a/plugins/session-flow/skills/handoff/context/structure.md +++ b/plugins/session-flow/skills/handoff/context/structure.md @@ -113,8 +113,9 @@ Write the file into the handoff location (SKILL.md "Where handoffs live"): ```bash TS=$(date -u +%Y%m%dT%H%M%SZ) # ISO basic — Windows-safe, no colons TOPIC= # e.g. plan-rev2, retry-loop, post-merge -MEMORY_ROOT=.work # the concern file's memory_dir when set — resolve it - # first, never assume the literal .work +MEMORY_ROOT=.work # resolve via the shared parse-concern-value.sh helper + # (retro skill's Phase 1.1 is the worked call form) — + # never assume the literal .work SESSION_ID="${CLAUDE_CODE_SESSION_ID:-unknown}" # Refuse a memory root at/above the repo root before the self-ignore guard can diff --git a/scripts/sync-parse-concern-value.sh b/scripts/sync-parse-concern-value.sh index 20373c0120..ca1d5fc3bc 100755 --- a/scripts/sync-parse-concern-value.sh +++ b/scripts/sync-parse-concern-value.sh @@ -19,6 +19,7 @@ src="lib/parse-concern-value.sh" copies=( plugins/claude-memory/skills/audit/scripts/parse-concern-value.sh plugins/session-flow/skills/retro/scripts/parse-concern-value.sh + plugins/docs-hygiene/skills/audit-noise/scripts/lib/parse-concern-value.sh ) mode="${1:-sync}" From 53af576a5e00e508b46ba2dfac2c55698fdbf5ac Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Tue, 21 Jul 2026 06:01:54 -0400 Subject: [PATCH 2/2] fix(docs-hygiene): set exec bit on the materialized parse-concern-value.sh copy Windows git did not carry the executable bit through the prior commit for the new plugins/docs-hygiene/skills/audit-noise/scripts/lib/parse-concern-value.sh copy, unlike its two siblings (100755). The exec-bit gate in CI flags a 100644 shebang file, and the copy must be directly invocable to match the sync-parse-concern-value.sh precedent. --- .../skills/audit-noise/scripts/lib/parse-concern-value.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 plugins/docs-hygiene/skills/audit-noise/scripts/lib/parse-concern-value.sh diff --git a/plugins/docs-hygiene/skills/audit-noise/scripts/lib/parse-concern-value.sh b/plugins/docs-hygiene/skills/audit-noise/scripts/lib/parse-concern-value.sh old mode 100644 new mode 100755