Skip to content
Merged
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
2 changes: 1 addition & 1 deletion plugins/docs-hygiene/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "docs-hygiene",
"version": "0.8.1",
"version": "0.8.2",
"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",
Expand Down
15 changes: 15 additions & 0 deletions plugins/docs-hygiene/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
# Changelog — docs-hygiene plugin

## [0.8.2] — 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.1] — 2026-07-21

### Added
Expand Down
20 changes: 20 additions & 0 deletions plugins/docs-hygiene/skills/audit-noise/scripts/detect.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <concern-file> <key> [fallback]
#
# <concern-file> path to the concern file (e.g. .claude/topic-docs.yaml)
# <key> 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 <concern-file> <key> [fallback]

Reads <key> from <concern-file>, 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 <concern-file> <key> [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"
2 changes: 1 addition & 1 deletion plugins/session-flow/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "session-flow",
"version": "0.12.0",
"version": "0.12.1",
"description": "Session-lifecycle toolkit of nine 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 OR when live off-thread work looks stalled — inventory off-thread work, inspect its real output, act only on evidence, then continue; after a usage limit lifts it continues rather than summarizing-and-stalling), 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), orient (read-only session orientation — synthesize where we stand, what we are doing, and why, from durable + off-thread state the built-in /recap never sees: ledgers, handoffs, workflow checklists, running-retro ledgers, open PRs and work-items, and git), 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",
Expand Down
10 changes: 10 additions & 0 deletions plugins/session-flow/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog — session-flow plugin

## [0.12.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.12.0] — 2026-07-21

Added:
Expand Down
5 changes: 3 additions & 2 deletions plugins/session-flow/skills/handoff/context/structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=<short-kebab-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
Expand Down
1 change: 1 addition & 0 deletions scripts/sync-parse-concern-value.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand Down
Loading