From 2059871fc15543eddad0b4d7243ba6e67de9a21a Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 23:50:37 +0000 Subject: [PATCH 01/10] refactor(plugins): batch-simplify wave 4 - claude-config, claude-memory, claude-ops Repo-wide batch-simplify sweep, wave 4 of 12 (claude-config audit + permission skills, claude-memory, claude-ops hooks/lanes/audits). 121 files read end to end; 9 files edited, every group's diff cleared by a fresh-context refutation verifier - including one refutation that REVERTED two edits before delivery: - claude-config: run-state.sh header usage block synced to its real usage() (comment-only; negative-test sed targets verified untouched); dead grep prerequisite guard removed from conflict-scan.test.sh (nothing in the suite or script invokes grep; skip-discipline gates re-run clean). - claude-memory: memory-dir-stats.test.sh fixture normalized to the file's \n-escape style; fixture bytes sha256-identical. - claude-ops hooks: hook-failure-audit.sh skips its summary jq spawn when the pre-filter matched nothing (one spawn saved per Stop turn; all paths byte-identical). A jq_fields consolidation in the two skill-usage hooks was attempted, refuted by adversarial payload probes (trailing-newline trimming, non-string tostring, two-doc handling), and reverted rather than shipped. - claude-ops lanes/brief: five formerly implicit globals made local in restart-consumer.sh process_lane (full caller-graph and trap audit: no post-return reader); redundant lock_rc defaults dropped; morning-brief.sh scratch array declared local; two test assertions moved to the section whose fixture they read. - claude-ops audits: install_state.py unused os.walk slot renamed to _dirnames per the file's own convention. Version bumps + changelog entries: claude-config 0.40.20, claude-memory 0.11.9, claude-ops 0.38.21. Verification: shellcheck clean on all touched shell; ruff wrapper clean; suites 48+84+12+64+30+28+88+146+83+50 all green plus adapter fan-outs; changelog-parity and catalog gates pass. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01W61wikkiEK5StWgg9rfEQW --- .../claude-config/.claude-plugin/plugin.json | 2 +- plugins/claude-config/CHANGELOG.md | 12 ++++++++++++ .../scripts/conflict-scan.test.sh | 4 ---- .../skills/audit-pass/scripts/run-state.sh | 6 +++--- .../claude-memory/.claude-plugin/plugin.json | 2 +- plugins/claude-memory/CHANGELOG.md | 10 ++++++++++ .../audit/scripts/memory-dir-stats.test.sh | 10 +++------- plugins/claude-ops/.claude-plugin/plugin.json | 2 +- plugins/claude-ops/CHANGELOG.md | 19 +++++++++++++++++++ .../claude-ops/hooks/hook-failure-audit.sh | 8 ++++++-- .../scripts/install_state.py | 2 +- .../skills/lanes/scripts/restart-consumer.sh | 7 ++++--- .../morning-brief/morning-brief.test.sh | 4 ++-- .../morning-brief/scripts/morning-brief.sh | 2 +- 14 files changed, 64 insertions(+), 26 deletions(-) diff --git a/plugins/claude-config/.claude-plugin/plugin.json b/plugins/claude-config/.claude-plugin/plugin.json index 58835b79b..bf5e83c8e 100644 --- a/plugins/claude-config/.claude-plugin/plugin.json +++ b/plugins/claude-config/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "claude-config", - "version": "0.40.19", + "version": "0.40.20", "description": "Nine configuration-health skills (plus setup) for a repo's Claude Code configuration: audit (settings.json / .mcp.json / hooks / plugins / permissions drift), audit-automation-gaps (evidence-gated verdicts on automation gaps), audit-permission-grants (allow-rule / allowed-tools grants for auto-mode durability and portability), audit-permission-state (the permission rules actually in effect \u2014 every settings scope merged with per-rule provenance, what auto mode drops on entry, config written where nothing reads it, and which managed intents are enforced versus loosenable), draft-auto-mode-rules (interview and draft a paste-ready autoMode classifier block; prints only, never writes), audit-instructions (locally-owned instruction surfaces vs current model capability \u2014 proposes removals/rewrites of instructions the model no longer needs, and detects cross-surface instruction conflicts), audit-prompting-postures (the additive lane \u2014 posture guidance the prompting guide says a component's purpose needs but the component does not carry), audit-pass (one coordinated, ordered, resumable pass over a named target \u2014 three-scope inventory, run-time-derived exclusion set, stable finding identity, suppression memory, resume, one human gate \u2014 delegating every check to the plugin that owns it), and unhobble (the empirical bare-baseline experiment: reversibly strip a repo's standing instructions, log real stumbles against the current model, re-add only what evidence earns).", "author": { "name": "Melodic Software", diff --git a/plugins/claude-config/CHANGELOG.md b/plugins/claude-config/CHANGELOG.md index 09b04fae3..d75e965df 100644 --- a/plugins/claude-config/CHANGELOG.md +++ b/plugins/claude-config/CHANGELOG.md @@ -3,6 +3,18 @@ All notable changes to the `claude-config` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. +## [0.40.20] + +### Changed + +- **Two documentation-accuracy fixes and one dead-guard removal in skill + scripts.** `audit-pass/scripts/run-state.sh`'s header usage block regains the + `--plugin-data` and `[--epoch ]` arguments its own `usage()` already + documents (comment-only; verified against the negative-test sed targets); + `audit-instructions/scripts/conflict-scan.test.sh` drops a prerequisite guard + for `grep`, a tool neither the suite nor the script under test invokes (the + awk guard stays; skip-discipline gates re-run clean). + ## [0.40.19] ### Changed diff --git a/plugins/claude-config/skills/audit-instructions/scripts/conflict-scan.test.sh b/plugins/claude-config/skills/audit-instructions/scripts/conflict-scan.test.sh index ed7ff531b..3ea1a53ee 100755 --- a/plugins/claude-config/skills/audit-instructions/scripts/conflict-scan.test.sh +++ b/plugins/claude-config/skills/audit-instructions/scripts/conflict-scan.test.sh @@ -41,10 +41,6 @@ assert_not_contains() { esac } -if ! command -v grep >/dev/null 2>&1; then - echo "SKIP: grep not installed" >&2 - exit 0 -fi if ! command -v awk >/dev/null 2>&1; then echo "SKIP: awk not installed" >&2 exit 0 diff --git a/plugins/claude-config/skills/audit-pass/scripts/run-state.sh b/plugins/claude-config/skills/audit-pass/scripts/run-state.sh index b0f3ea081..0cdf55856 100755 --- a/plugins/claude-config/skills/audit-pass/scripts/run-state.sh +++ b/plugins/claude-config/skills/audit-pass/scripts/run-state.sh @@ -55,12 +55,12 @@ # # Usage: # run-state.sh paths --plugin-data --run-id [--root ] -# run-state.sh lease acquire --run-dir --run-id [--stale-after ] -# [--skew-grace ] [--epoch ] +# run-state.sh lease acquire --run-dir --run-id --plugin-data +# [--stale-after ] [--skew-grace ] [--epoch ] # run-state.sh lease heartbeat --run-dir # run-state.sh lease release --run-dir # run-state.sh lease classify --run-dir -# run-state.sh partial append --run-dir --record +# run-state.sh partial append --run-dir --record [--epoch ] # # `--plugin-data` is required because `${CLAUDE_PLUGIN_DATA}` is NOT in the Bash # tool's environment (plugins reference: the three placeholders are exported to diff --git a/plugins/claude-memory/.claude-plugin/plugin.json b/plugins/claude-memory/.claude-plugin/plugin.json index 4169878ec..8fdc5d98d 100644 --- a/plugins/claude-memory/.claude-plugin/plugin.json +++ b/plugins/claude-memory/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "claude-memory", - "version": "0.11.8", + "version": "0.11.9", "description": "Keeps a repo's Claude Code memory layer healthy and under your control, against criteria derived from official Claude Code documentation. The audit skill checks the instruction/memory layer (CLAUDE.md, CLAUDE.local.md, .claude/rules/, auto-memory) with a deterministic script-backed spine plus judgment-tier checks. The stateless skill inspects, disables, and (confirm-gated) purges Claude-written auto memory across all settings scopes.", "author": { "name": "Melodic Software", diff --git a/plugins/claude-memory/CHANGELOG.md b/plugins/claude-memory/CHANGELOG.md index 7dcee56d6..74631aded 100644 --- a/plugins/claude-memory/CHANGELOG.md +++ b/plugins/claude-memory/CHANGELOG.md @@ -3,6 +3,16 @@ All notable changes to the `claude-memory` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. +## [0.11.9] + +### Changed + +- **`audit/scripts/memory-dir-stats.test.sh` fixture style normalized.** The + one fixture block that embedded literal newlines inside printf quotes (a + #2042 leftover) now uses the `\n`-escape form every other fixture in the + file uses; the produced fixture bytes are sha256-identical and all 64 + checks pass. + ## [0.11.8] ### Changed diff --git a/plugins/claude-memory/skills/audit/scripts/memory-dir-stats.test.sh b/plugins/claude-memory/skills/audit/scripts/memory-dir-stats.test.sh index 55651e3d3..0df241883 100755 --- a/plugins/claude-memory/skills/audit/scripts/memory-dir-stats.test.sh +++ b/plugins/claude-memory/skills/audit/scripts/memory-dir-stats.test.sh @@ -184,13 +184,9 @@ assert_eq "a heavy one-line runaway counts every line" "$(raw_lines)" "$(run "$H # as octal escapes so the file stays ASCII and no tool can re-encode the fixture. WIDE=$(for ((i = 0; i < 600; i++)); do printf '\303\251'; done) { - printf -- '--- -' - printf 'Note: %s -' "$WIDE" - printf -- '--- -body -' + printf -- '---\n' + printf 'Note: %s\n' "$WIDE" + printf -- '---\nbody\n' } >"$M3/MEMORY.md" assert_eq "a multibyte heavy line is bounded by weight in bytes" "$(raw_bytes)" "$(run "$H3" --memory-bytes)" diff --git a/plugins/claude-ops/.claude-plugin/plugin.json b/plugins/claude-ops/.claude-plugin/plugin.json index fd76f4038..612e09126 100644 --- a/plugins/claude-ops/.claude-plugin/plugin.json +++ b/plugins/claude-ops/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "claude-ops", - "version": "0.38.20", + "version": "0.38.21", "description": "Claude Code operations toolkit. Twelve skills: audit-skill-visibility (audit whether each installed skill is actually VISIBLE to the model, and diagnose why most of a fleet never gets used \u2014 a skill is invisible when its description is dropped by Claude Code's skill-listing context budget, which drops descriptions least-invoked-first so an unused skill loses the keywords that would let it be matched, from skills genuinely not wanted, from skills the run cannot observe at all; computes whether the listing overflows from documented settings, and withholds every cold verdict the data cannot support rather than reporting absence of data as absence of use), inventory (read-only enumeration of the complete invocable surface \u2014 every built-in CLI command with aliases and hidden/gated status, every bundled skill, and every component of every installed plugin across all marketplaces; reads the shipped binary because upstream publishes no built-in command list, and carries an integrity verdict so a drifted build reports counts as floors rather than silently short totals), audit-install-state (read-only audit of the machine-scope ~/.claude installation directory and ~/.claude.json \u2014 full inventory split into an authored surface and rolled-up bulk trees, product-managed retention vs genuinely unmanaged state, filename-scheme resolution before any process-liveness check, and deliberate/mid-experiment detection; reports, never deletes), audit-performance (read-only slowness-diagnostic capture run at the moment the machine or a session feels slow: CLI version, retention-sweep health including the silent unparsable-settings pause, a timed census walk of the install tree as a sweep-cost proxy, active-session and plugin-fleet counts, a process census, and the fan-out layer, which covers a load-labelled no-op spawn baseline, every hook that will fire bucketed per-tool-call versus per-turn with its invocation shape, the configured statusline, subagent concurrency and spawn-depth ceilings against documented defaults, whether running sessions predate the settings file they are judged by, and orphan attribution by parent liveness rather than age; read against a bundled known-performance-issues reference that also records the causes tested and cleared; separates the four documented suspects of accumulated state, version regression, component bloat, and per-spawn fan-out cost, and routes remediation out; reports, never mutates, and never executes a discovered hook or statusline command), audit-native-overlap (map native Claude Code surfaces \u2014 built-in CLI commands, bundled skills, plugin-backed built-ins, session-provided skills \u2014 against the current repo's plugin skills and agents, so a custom component never silently duplicates what Claude Code itself ships; bare invocation is a read-only overlap report carrying the extraction's integrity floors and a shared-listing-budget exposure section, verdicts are human-gated in a committed store rendered into a generated registry whose every row carries an observable recheck trigger, and only an explicit apply step bakes presence-gated native references into descriptions and Boundary sections), observability (read locally captured telemetry \u2014 OTEL store, collector, hook-event JSONL, ccusage \u2014 with trend reports and store pruning), known-issues (search known Claude product GitHub bugs, check service health, maintain a persistent tracked-issue registry), changelog (ingest Claude Code changelog entries and integrate them into the current repo), plugins (bring a machine's plugin fleet current on demand \u2014 marketplace refresh, effective-scope updates including in-repo project/local installs, new-plugin install per policy, scope-divergence detection and explicit convergence), morning-brief (read-only gh-based operator morning view \u2014 queue-label counts, merge-ready PRs, parked decisions with their RECOMMENDED lines, and loop-lane telemetry freshness), lanes (start/restart/stop/status loop lanes as named background Claude Code sessions seeded from canonical prompt files, with per-lane model/effort, a repo-pull + marketplace-refresh launch step, and a consume-restarts action \u2014 an OS-schedulable reader that relaunches stopped lanes whose telemetry carries a restart_request), and a re-runnable setup action that settles where the known-issues registry lives. Plus a family of eight advisory *-audit hooks (API errors, config changes, instruction loads, permission denials, pre-compaction, skill usage, tool failures, and unsurfaced hook failures \u2014 the last also warns the user via systemMessage, since a hook that fails to launch enforces nothing and Claude Code surfaces the failure to nobody) that emit the shared hook-telemetry envelope, and a reference sink that maps envelopes into the hook-events.jsonl the observability skill reads.", "author": { "name": "Melodic Software", diff --git a/plugins/claude-ops/CHANGELOG.md b/plugins/claude-ops/CHANGELOG.md index 68501d98c..12e05b8e9 100644 --- a/plugins/claude-ops/CHANGELOG.md +++ b/plugins/claude-ops/CHANGELOG.md @@ -3,6 +3,25 @@ All notable changes to the `claude-ops` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. +## [0.38.21] + +### Changed + +- **One fewer spawn per Stop turn in `hook-failure-audit.sh`.** The hook now + short-circuits before its summary jq spawn when the grep pre-filter matched + nothing (the empty case previously produced `[]` and the same silent exit 0). + Adversarial payload probes byte-identical across all paths; suite 88/88. + (An analogous jq_fields consolidation in the two skill-usage hooks was + attempted, refuted by differential testing on pathological payloads, and + reverted — recorded in the sweep report rather than shipped.) +- **restart-consumer.sh declares five formerly implicit globals `local` to + `process_lane`** (no post-return reader exists; full caller-graph and trap + audit) and drops two `${lock_rc:-0}` defaults dominated by an unconditional + assignment. `morning-brief.sh` declares its `read -ra` scratch array local; + two misplaced test assertions moved to the section whose fixture they read. +- **install_state.py** renames the unused `os.walk` dirnames slot to + `_dirnames`, matching the file's own convention. + ## [0.38.20] ### Changed diff --git a/plugins/claude-ops/hooks/hook-failure-audit.sh b/plugins/claude-ops/hooks/hook-failure-audit.sh index 1c4fed4ed..0ddb48360 100755 --- a/plugins/claude-ops/hooks/hook-failure-audit.sh +++ b/plugins/claude-ops/hooks/hook-failure-audit.sh @@ -79,7 +79,9 @@ read_window() { fi } -# grep is a cheap pre-filter only; the structural jq selection decides. +# grep is a cheap pre-filter only; the structural jq selection decides. The +# no-match common case exits on the pre-filter's emptiness, before paying for +# the jq spawn (an empty stream produced the same silent exit via "[]"). # `fromjson?` skips unparsable lines instead of aborting the stream. # Identity is the REGISTRATION, not the matcher name: several plugins register # on the same event+matcher (multiple PreToolUse:Bash guards exist in this very @@ -130,7 +132,9 @@ read_window() { # in a narrower shape. The per-class counts below keep every class present in a # group visible, and the message flags are computed from those counts, never # from a single collapsed value. -SUMMARY=$(read_window | grep -F '"hook_non_blocking_error"' | +RECORDS=$(read_window | grep -F '"hook_non_blocking_error"') +[[ -n "$RECORDS" ]] || exit 0 +SUMMARY=$(printf '%s' "$RECORDS" | jq -cRs '[ split("\n")[] | fromjson? | select(.type? == "attachment") | .attachment diff --git a/plugins/claude-ops/skills/audit-install-state/scripts/install_state.py b/plugins/claude-ops/skills/audit-install-state/scripts/install_state.py index 987808378..4262027cd 100755 --- a/plugins/claude-ops/skills/audit-install-state/scripts/install_state.py +++ b/plugins/claude-ops/skills/audit-install-state/scripts/install_state.py @@ -847,7 +847,7 @@ def walk_tree( exclude = exclude or set() rows: list[FileRow] = [] errors: list[dict] = [] - for dirpath, dirnames, filenames in os.walk( + for dirpath, _dirnames, filenames in os.walk( root, followlinks=False, onerror=errors.append ): for name in filenames: diff --git a/plugins/claude-ops/skills/lanes/scripts/restart-consumer.sh b/plugins/claude-ops/skills/lanes/scripts/restart-consumer.sh index 8f6e25be6..5bc2d72d4 100755 --- a/plugins/claude-ops/skills/lanes/scripts/restart-consumer.sh +++ b/plugins/claude-ops/skills/lanes/scripts/restart-consumer.sh @@ -924,7 +924,8 @@ lane_selected() { process_lane() { local idx="$1" now="$2" - local lane repo issue marker bodies body state request reason used n i found=0 + local lane repo issue marker instance bodies body state request req reason used n i found=0 + local comment_marker sibling_request sibling_instance lane="$(lane_field "$idx" name)" [[ -n "$lane" ]] || return 0 lane_selected "$lane" || return 0 @@ -1271,11 +1272,11 @@ main() { acquire_lock "$now" || lock_rc=$? # 2 = the lock store itself is unusable. Exiting 0 here would let an # unattended schedule log healthy ticks forever while processing nothing. - if ((${lock_rc:-0} == 2)); then + if ((lock_rc == 2)); then err "restart-consumer: the lock store is unusable — not a held lock; failing rather than reporting a skipped tick" exit 4 fi - if ((${lock_rc:-0} != 0)); then + if ((lock_rc != 0)); then warn "another restart-consumer run holds the lock ($LOCK_DIR) — skipping this tick" info "restart-consumer: $ACTION on ${TARGET_REPO:-} at $(iso_utc "$now")" info "| lane | decision | detail |" diff --git a/plugins/claude-ops/skills/morning-brief/morning-brief.test.sh b/plugins/claude-ops/skills/morning-brief/morning-brief.test.sh index 30dcb704f..87811d769 100755 --- a/plugins/claude-ops/skills/morning-brief/morning-brief.test.sh +++ b/plugins/claude-ops/skills/morning-brief/morning-brief.test.sh @@ -529,6 +529,8 @@ OUT_ERR="$(bash "$BRIEF" --now "$NOW" --stale-hours 6 \ --telemetry-json "$TMP/telemetry.json" \ --merged-json "$TMP/merged-apierror.json" 2>&1)" assert_not_contains "stranded: an API error is NEVER reported as clear" "$OUT_ERR" "every merged PR in the window is clear" +assert_contains "stranded: an API error says it is not an all-clear" "$OUT_ERR" "NOT an all-clear" +assert_contains "stranded: the API error message is surfaced" "$OUT_ERR" "rate limit already exceeded" # --- Severity classification and ranking ------------------------------------- OUT_SEV="$(bash "$BRIEF" --now "$NOW" --stale-hours 6 \ @@ -556,8 +558,6 @@ OUT_TRUNC="$(bash "$BRIEF" --now "$NOW" --stale-hours 6 \ --merged-json "$TMP/merged-truncated.json" 2>&1)" assert_contains "truncation: a partial thread read is reported" "$OUT_TRUNC" "this read is PARTIAL" assert_contains "truncation: the affected PR is named" "$OUT_TRUNC" "#400" -assert_contains "stranded: an API error says it is not an all-clear" "$OUT_ERR" "NOT an all-clear" -assert_contains "stranded: the API error message is surfaced" "$OUT_ERR" "rate limit already exceeded" # The window is operator-tunable, and widening it must pull in the older merge. OUT_WIDE="$(bash "$BRIEF" --now "$NOW" --stale-hours 6 --stranded-days 60 \ diff --git a/plugins/claude-ops/skills/morning-brief/scripts/morning-brief.sh b/plugins/claude-ops/skills/morning-brief/scripts/morning-brief.sh index 6b5f3043c..4383b233d 100755 --- a/plugins/claude-ops/skills/morning-brief/scripts/morning-brief.sh +++ b/plugins/claude-ops/skills/morning-brief/scripts/morning-brief.sh @@ -346,7 +346,7 @@ fmt_age() { resolve_queue_labels() { QUEUE_LABELS=() if [[ -n "$QUEUE_LABELS_ARG" ]]; then - local part + local part _parts IFS=',' read -ra _parts <<<"$QUEUE_LABELS_ARG" for part in "${_parts[@]}"; do part="${part#"${part%%[![:space:]]*}"}" From d0b1a5ec957a16346b24c115b6f5ffdf4ebde7f9 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 31 Aug 2026 00:00:41 +0000 Subject: [PATCH 02/10] refactor(plugins): batch-simplify wave 5 - context plugins, disk-hygiene race fix, formatter hooks Repo-wide batch-simplify sweep, wave 5 of 12 (code-tidying, context-budget, context-guard, discovery, disk-hygiene, docs-hygiene, ai-slop, and the first five formatter-hook plugins). 89 files read end to end; 3 files edited, each cleared by a fresh-context refutation verifier: - disk-hygiene: the queued wave-1 finding is fixed - both sink tests in lib/test_hook_telemetry.py raced the fire-and-forget sink (the sink's > redirect creates its file empty before cat writes it), failing ~3/4 runs on a fast host at HEAD. They now poll for non-empty, parseable content with a 5s deadline, retrying a truncated mid-write read, mirroring the sibling suites' _wait_for_file discipline. Deterministic over 12/12 runs; production hook_telemetry.py untouched. - markdown-format: two inert hook::ctx_reset calls removed from the hook (nothing in it ever writes or reads the ctx buffer - verified down to the buffer's only readers in hook-utils), and a three-line no-op `for ... do :; done` loop removed from its test. The test-file deletion is deliberate, not a silent revert. Suite 161/161. The other six groups (code-tidying scripts, context-budget + context-guard, discovery gates, docs-hygiene + ai-slop detectors) reviewed clean with zero edits - 1,200+ baseline assertions green, detector-crosswalk gate green. Version bumps + changelog entries: disk-hygiene 0.20.35, markdown-format 0.11.35. Verification: shellcheck clean on touched shell; pinned-ruff clean; disk-hygiene python suites 317+23+17+10 OK; markdown-format fan-out 4 suites incl. shell-portability 345/345; changelog-parity, vendor-bump, and catalog gates pass vs origin/main. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01W61wikkiEK5StWgg9rfEQW --- .../disk-hygiene/.claude-plugin/plugin.json | 2 +- plugins/disk-hygiene/CHANGELOG.md | 14 ++++++++ .../disk-hygiene/lib/test_hook_telemetry.py | 36 +++++++++++++------ .../.claude-plugin/plugin.json | 2 +- plugins/markdown-format/CHANGELOG.md | 11 ++++++ .../markdown-format/hooks/markdown-format.sh | 2 -- .../hooks/markdown-format.test.sh | 3 -- 7 files changed, 53 insertions(+), 17 deletions(-) diff --git a/plugins/disk-hygiene/.claude-plugin/plugin.json b/plugins/disk-hygiene/.claude-plugin/plugin.json index a07ce756d..5ec67889e 100644 --- a/plugins/disk-hygiene/.claude-plugin/plugin.json +++ b/plugins/disk-hygiene/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "disk-hygiene", - "version": "0.20.34", + "version": "0.20.35", "description": "Context-aware disk hygiene for arbitrary directory trees: inventories orphaned and temporary artifacts, classifies evidence into review tiers, and offers exact-path cleanup only after a fresh safety preview and explicit per-tier approval. The target is read-only by default; OS-managed paths, links and mount points, VCS-tracked content without the complete checkout evidence bundle, changed entries, and live-handle uncertainty fail closed.", "author": { "name": "Melodic Software", diff --git a/plugins/disk-hygiene/CHANGELOG.md b/plugins/disk-hygiene/CHANGELOG.md index bc09c42a8..d3de83d31 100644 --- a/plugins/disk-hygiene/CHANGELOG.md +++ b/plugins/disk-hygiene/CHANGELOG.md @@ -3,6 +3,20 @@ All notable changes to the `disk-hygiene` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. +## [0.20.35] + +### Fixed + +- **`lib/test_hook_telemetry.py` no longer races the fire-and-forget sink.** + The sink's `>` redirect creates its output file empty before `cat` writes + it, so the two sink tests' existence-polls could read an empty file on a + fast host and fail on `json.loads("")` (about three runs in four on a fast + Linux container). Both tests now wait for non-empty, parseable content with + a five-second deadline — retrying on a truncated mid-write read as well — + mirroring the `_wait_for_file` discipline the sibling suites already use. + Production `hook_telemetry.py` is untouched; the suite is deterministic + over repeated runs (12/12). + ## [0.20.34] ### Changed diff --git a/plugins/disk-hygiene/lib/test_hook_telemetry.py b/plugins/disk-hygiene/lib/test_hook_telemetry.py index 2503b17b3..68d0956c9 100755 --- a/plugins/disk-hygiene/lib/test_hook_telemetry.py +++ b/plugins/disk-hygiene/lib/test_hook_telemetry.py @@ -27,6 +27,30 @@ def load_module(name: str, filename: str): class HookTelemetryTests(unittest.TestCase): + def _wait_for_envelope(self, out_file: Path, timeout: float = 5.0) -> dict: + """Poll until the fire-and-forget sink has written a non-empty envelope. + + `emit` never waits for its sink subprocess, and the sink's `>` redirect + creates the file EMPTY before `cat` writes into it, so polling for mere + existence loses the race on a fast host and json.loads reads "". + Wait for content, the same discipline the sibling telemetry cases in + test_guard_launch_monitor.py and test_hygiene.py use. + """ + deadline = time.perf_counter() + timeout + while time.perf_counter() < deadline: + if out_file.exists(): + try: + text = out_file.read_text(encoding="utf-8").strip() + except OSError: + text = "" + if text: + try: + return json.loads(text) + except json.JSONDecodeError: + pass # truncated mid-write; keep polling until deadline + time.sleep(0.05) + self.fail(f"timed out waiting for the telemetry sink to write {out_file}") + def test_unset_sink_is_noop(self) -> None: with mock.patch.dict(os.environ, {}, clear=True): telemetry.emit("destructive-guard", "PreToolUse", "ok", time.perf_counter()) @@ -58,11 +82,7 @@ def test_relative_sink_resolved_against_project_dir(self) -> None: {"tool": "Bash", "decision": "deny"}, str(project), ) - deadline = time.perf_counter() + 2.0 - while time.perf_counter() < deadline and not out_file.exists(): - time.sleep(0.05) - self.assertTrue(out_file.exists(), "sink should receive envelope") - envelope = json.loads(out_file.read_text(encoding="utf-8").strip()) + envelope = self._wait_for_envelope(out_file) self.assertEqual("destructive-guard", envelope["hook"]) self.assertEqual("PreToolUse", envelope["hook_event"]) self.assertEqual("blocked", envelope["status"]) @@ -82,11 +102,7 @@ def test_absolute_sink_used_as_is(self) -> None: telemetry.emit( "guard-launch-monitor", "Stop", "ok", time.perf_counter() ) - deadline = time.perf_counter() + 2.0 - while time.perf_counter() < deadline and not out_file.exists(): - time.sleep(0.05) - self.assertTrue(out_file.exists()) - envelope = json.loads(out_file.read_text(encoding="utf-8").strip()) + envelope = self._wait_for_envelope(out_file) self.assertEqual("guard-launch-monitor", envelope["hook"]) self.assertEqual("Stop", envelope["hook_event"]) diff --git a/plugins/markdown-format/.claude-plugin/plugin.json b/plugins/markdown-format/.claude-plugin/plugin.json index e44c8c576..e07a4a08a 100644 --- a/plugins/markdown-format/.claude-plugin/plugin.json +++ b/plugins/markdown-format/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "markdown-format", - "version": "0.11.34", + "version": "0.11.35", "description": "Auto-format and lint Markdown on edit via markdownlint-cli2 — only in repos that carry their own markdownlint config.", "author": { "name": "Melodic Software", diff --git a/plugins/markdown-format/CHANGELOG.md b/plugins/markdown-format/CHANGELOG.md index 9811a306e..9056a75e9 100644 --- a/plugins/markdown-format/CHANGELOG.md +++ b/plugins/markdown-format/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to the `markdown-format` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. +## [0.11.35] + +### Changed + +- **Two inert leftovers removed.** `hooks/markdown-format.sh` drops two + `hook::ctx_reset` calls (this hook never uses the ctx accumulator — no + `ctx_append`/`ctx_flush`, and its emit path never reads the buffer); + `hooks/markdown-format.test.sh` drops a three-line `for … do :; done` loop + whose body was a no-op. Suite 161/161; deliberate deletion, not a silent + revert. + ## [0.11.34] ### Fixed diff --git a/plugins/markdown-format/hooks/markdown-format.sh b/plugins/markdown-format/hooks/markdown-format.sh index 985e7eb93..9c4d93399 100755 --- a/plugins/markdown-format/hooks/markdown-format.sh +++ b/plugins/markdown-format/hooks/markdown-format.sh @@ -1095,7 +1095,6 @@ resolve_trust_dir() { TRUST_DIR="${state_base%/}/trust-approvals/$signature" } -hook::ctx_reset collect_risky_configs # Trust gate: markdownlint-cli2's configuration contract loads .cjs/.mjs # config modules and customRules/markdownItPlugins/outputFormatters module @@ -1235,7 +1234,6 @@ while IFS= read -r line; do esac done <<<"$FIX_OUTPUT" -hook::ctx_reset CTX="" SYSMSG="" diff --git a/plugins/markdown-format/hooks/markdown-format.test.sh b/plugins/markdown-format/hooks/markdown-format.test.sh index 17f40988c..d86df884b 100755 --- a/plugins/markdown-format/hooks/markdown-format.test.sh +++ b/plugins/markdown-format/hooks/markdown-format.test.sh @@ -1931,9 +1931,6 @@ if [[ -s "$TEL_FILE" ]]; then fi else fail "telemetry/stub-sink: no envelope written to sink" - for field in schema_version timestamp hook hook_event status duration_ms data status findings tool file schema_version duration_ms; do - : # counters already accounted by the outer if branch counting - done fail "telemetry/envelope: status (no envelope)" fail "telemetry/envelope: findings (no envelope)" fail "telemetry/envelope: data.tool (no envelope)" From cd92a3f0cf551512718b9a08bf0d45ad8b15c552 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 31 Aug 2026 00:13:34 +0000 Subject: [PATCH 03/10] refactor(knowledge): batch-simplify waves 6-7 - knowledge sweep, one dead fallback removed Repo-wide batch-simplify sweep, waves 6 and 7 of 12 (formatter/notification hooks second half, guardrails blocking + advisory hooks, instruction-placement, kindle-dedrm, and the whole knowledge plugin: course-digest adapters/lib/ pipeline, docpage-digest, map-corpus, video-digest acquisition/adapters/ harvesting/liveness/transcript). 199 files read end to end across 11 groups. Wave 6 was a pure verification wave: zero edits across all five groups (~1,100 baseline assertions re-proven green; the guardrails enforcement core is deliberately dense and every candidate was incident-pinned). Wave 7 landed one edit: video-digest acquisition acquire.js drops a `?? "staged acquire failed"` default that could never fire (both ok:false sites in acquireFullStaged construct non-empty string errors; old ?? replaced only null/undefined). Enumerated, tsc-checked, and cleared by a fresh-context refutation verifier. The same verifier REFUTED a second candidate - merging the duplicated initial spawn in spawn-yt-dlp-with-auth-fallback.js - with an executed counterexample (the cookie-config probe it would skip emits a once-per-process deprecation warning via env-compat and mutates its dedupe set). That edit was reverted, not shipped. Version bump + changelog: knowledge 0.13.27. Verification: acquisition suite 93/93 post-revert, adapters 112/112, transcript 54/54, course-digest 91/91 x2 runs, docpage/map-corpus python 117 tests + ruff clean; changelog-parity and catalog gates pass. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01W61wikkiEK5StWgg9rfEQW --- plugins/knowledge/.claude-plugin/plugin.json | 2 +- plugins/knowledge/CHANGELOG.md | 14 ++++++++++++++ .../video-digest/extraction/acquisition/acquire.js | 2 +- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/plugins/knowledge/.claude-plugin/plugin.json b/plugins/knowledge/.claude-plugin/plugin.json index 350da468f..de001de20 100644 --- a/plugins/knowledge/.claude-plugin/plugin.json +++ b/plugins/knowledge/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "knowledge", - "version": "0.13.26", + "version": "0.13.27", "description": "Ingest external knowledge into durable, synthesized artifacts. Ships a book-distillation pipeline (PDF/EPUB into concept-organized, author-attributed skill reference files), a video-digest pipeline (watch a single public video from YouTube or X, formerly Twitter: transcript, link harvest, and repo-applicability synthesis), a course-digest pipeline (extract and synthesize online video courses \u2014 Dometrain, Teachable \u2014 into repo-applicable recommendations), a docpage-digest pipeline (single online documentation page into a verified knowledge slice with dual verification \u2014 one cross-vendor verifier \u2014 and an interview handoff), and a map-corpus pipeline (multi-resource corpus into a classified link map, deterministic node manifests, gate-verified relevance inventory, and an approved queue of docpage-digest runs), plus a re-runnable setup action; a configurable library directory governs where synthesized artifacts land in the consuming repo.", "author": { "name": "Melodic Software", diff --git a/plugins/knowledge/CHANGELOG.md b/plugins/knowledge/CHANGELOG.md index fbc08e30c..7de133f55 100644 --- a/plugins/knowledge/CHANGELOG.md +++ b/plugins/knowledge/CHANGELOG.md @@ -4,6 +4,20 @@ All notable changes to the `knowledge` plugin are recorded here. The `version` i `.claude-plugin/plugin.json` is the delivery vehicle — a consumer receives a change only after that version increases. +## [0.13.27] + +### Changed + +- **video-digest acquisition: dead fallback removed.** `acquire.js` drops a + `?? "staged acquire failed"` default that could never fire — both `ok: false` + return sites in `acquireFullStaged` construct non-empty string errors, and + the old `??` only replaced null/undefined. Verified by site enumeration and + `tsc`; acquisition + adapters suites 205/205. (A second candidate, merging + the duplicated initial spawn in `spawn-yt-dlp-with-auth-fallback.js`, was + refuted by differential testing — the cookie-config probe it would skip + emits a once-per-process deprecation warning — and was reverted rather than + shipped.) + ## [0.13.26] ### Changed diff --git a/plugins/knowledge/skills/video-digest/extraction/acquisition/acquire.js b/plugins/knowledge/skills/video-digest/extraction/acquisition/acquire.js index 84aa72035..0b9ee8127 100644 --- a/plugins/knowledge/skills/video-digest/extraction/acquisition/acquire.js +++ b/plugins/knowledge/skills/video-digest/extraction/acquisition/acquire.js @@ -307,7 +307,7 @@ export async function acquireYouTubeMedia( const staged = await acquireFullStaged(mergedDeps, url, workDir, videoId, source); acquireMetrics = staged.acquireMetrics; if (!staged.ok) { - return failVideo(staged.error ?? "staged acquire failed"); + return failVideo(staged.error); } files = staged.files; artifacts = staged.artifacts; From 099f056d229189874e755958e79000913ccff478 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 31 Aug 2026 00:22:45 +0000 Subject: [PATCH 04/10] refactor(plugins): batch-simplify wave 8 - video watch trees, machine-health test scaffolding Repo-wide batch-simplify sweep, wave 8 of 12 (video-digest watch halves + watching pipeline; machine-health check tests, lib tests, harness, and check sources). 129 files read end to end across 6 groups; 5 files edited, every diff cleared by a fresh-context refutation verifier: - knowledge video-digest: watch/synthesis-naming.js drops the synthesisDestNameForSlice export nothing imports (introduced Jul 2026, never consumed; proven across code, markdown, and fixtures), its orphaned loadPromotionNameMap import, and the export keyword on the internal-only sourceStem. Watch suites 58/58; tsc clean. - machine-health: tests/helpers/Mock-Helpers.psm1 drops the New-MockWinGetPackage factory nothing references; two suites drop Import-Module lines feeding nothing (mock-target and side-effect angles verified); ConvertFrom-Jsonc.Tests.ps1 inlines a single-use intermediate. All suite counts byte-identical pre/post, including the pre-existing Linux cmdlet-gap failures (reported, unchanged). The other groups (watch-a, watching pipeline, check sources + scaffold) reviewed clean with zero edits. Version bumps + changelog entries: knowledge 0.13.28, machine-health 0.11.17. Verification: watch/watching vitest 48+58+29 pass; machine-health runnable Pester baselines identical pre/post; parse checks clean; changelog-parity and catalog gates pass vs origin/main. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01W61wikkiEK5StWgg9rfEQW --- plugins/knowledge/.claude-plugin/plugin.json | 2 +- plugins/knowledge/CHANGELOG.md | 12 +++++++++ .../extraction/watch/synthesis-naming.js | 15 +---------- .../machine-health/.claude-plugin/plugin.json | 2 +- plugins/machine-health/CHANGELOG.md | 14 +++++++++++ .../audit/tests/helpers/Mock-Helpers.psm1 | 25 ------------------- .../checks/Test-WindowsUpdate.Tests.ps1 | 1 - .../checks/Test-WingetUpgrades.Tests.ps1 | 1 - .../windows/lib/ConvertFrom-Jsonc.Tests.ps1 | 3 +-- 9 files changed, 30 insertions(+), 45 deletions(-) diff --git a/plugins/knowledge/.claude-plugin/plugin.json b/plugins/knowledge/.claude-plugin/plugin.json index de001de20..421b930ab 100644 --- a/plugins/knowledge/.claude-plugin/plugin.json +++ b/plugins/knowledge/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "knowledge", - "version": "0.13.27", + "version": "0.13.28", "description": "Ingest external knowledge into durable, synthesized artifacts. Ships a book-distillation pipeline (PDF/EPUB into concept-organized, author-attributed skill reference files), a video-digest pipeline (watch a single public video from YouTube or X, formerly Twitter: transcript, link harvest, and repo-applicability synthesis), a course-digest pipeline (extract and synthesize online video courses \u2014 Dometrain, Teachable \u2014 into repo-applicable recommendations), a docpage-digest pipeline (single online documentation page into a verified knowledge slice with dual verification \u2014 one cross-vendor verifier \u2014 and an interview handoff), and a map-corpus pipeline (multi-resource corpus into a classified link map, deterministic node manifests, gate-verified relevance inventory, and an approved queue of docpage-digest runs), plus a re-runnable setup action; a configurable library directory governs where synthesized artifacts land in the consuming repo.", "author": { "name": "Melodic Software", diff --git a/plugins/knowledge/CHANGELOG.md b/plugins/knowledge/CHANGELOG.md index 7de133f55..be1388b1e 100644 --- a/plugins/knowledge/CHANGELOG.md +++ b/plugins/knowledge/CHANGELOG.md @@ -4,6 +4,18 @@ All notable changes to the `knowledge` plugin are recorded here. The `version` i `.claude-plugin/plugin.json` is the delivery vehicle — a consumer receives a change only after that version increases. +## [0.13.28] + +### Changed + +- **video-digest watch: dead export removed.** `watch/synthesis-naming.js` + drops `synthesisDestNameForSlice` (no importer anywhere since its + introduction) together with its now-orphaned `loadPromotionNameMap` import, + and unexports the internal-only `sourceStem`. A fresh-context sweep confirmed + zero consumers across code, markdown, and fixtures, and that + `promotion-name-map.js` has no load-time side effects. Watch suites 58/58, + `tsc` clean. + ## [0.13.27] ### Changed diff --git a/plugins/knowledge/skills/video-digest/extraction/watch/synthesis-naming.js b/plugins/knowledge/skills/video-digest/extraction/watch/synthesis-naming.js index f963c6371..5a31fd64f 100644 --- a/plugins/knowledge/skills/video-digest/extraction/watch/synthesis-naming.js +++ b/plugins/knowledge/skills/video-digest/extraction/watch/synthesis-naming.js @@ -4,8 +4,6 @@ import path from "node:path"; -import { loadPromotionNameMap } from "./promotion-name-map.js"; - const IMAGE_EXT = /\.(png|jpe?g|webp)$/i; /** @@ -23,7 +21,7 @@ export function formatTimestampSlug(timestampSec) { * @param {string} sourceFile * @returns {string} */ -export function sourceStem(sourceFile) { +function sourceStem(sourceFile) { return sourceFile.replace(IMAGE_EXT, "").toLowerCase(); } @@ -42,17 +40,6 @@ export function synthesisDestName(sourceFile, timestampSec, knownDestBySource = return `at-${at}-${stem}.png`; } -/** - * @param {string} sourceFile - * @param {number} timestampSec - * @param {string} sliceDir - * @returns {string} - */ -export function synthesisDestNameForSlice(sourceFile, timestampSec, sliceDir) { - const map = loadPromotionNameMap(sliceDir); - return synthesisDestName(sourceFile, timestampSec, map); -} - /** * Prefer semantic names when two files share identical pixels. * diff --git a/plugins/machine-health/.claude-plugin/plugin.json b/plugins/machine-health/.claude-plugin/plugin.json index fb531600e..5a1857cde 100644 --- a/plugins/machine-health/.claude-plugin/plugin.json +++ b/plugins/machine-health/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "machine-health", - "version": "0.11.16", + "version": "0.11.17", "description": "Workstation health audit: OS-specific checks (disk, OS updates, security posture, CISA KEV correlation) run from a versioned catalog with trend-aware severity, approval-gated remediations, and dated markdown reports. Windows fully implemented; macOS/Linux scaffolded (report UNKNOWN and stop). Machine state persists in the plugin data directory; the report directory and check catalog are configurable.", "author": { "name": "Melodic Software", diff --git a/plugins/machine-health/CHANGELOG.md b/plugins/machine-health/CHANGELOG.md index 0dc2e99d2..1d931366b 100644 --- a/plugins/machine-health/CHANGELOG.md +++ b/plugins/machine-health/CHANGELOG.md @@ -3,6 +3,20 @@ All notable changes to the `machine-health` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. +## [0.11.17] + +### Changed + +- **Test scaffolding sheds three dead pieces.** `tests/helpers/Mock-Helpers.psm1` + drops the `New-MockWinGetPackage` factory nothing references (its own comment + named tests that do not exist); `Test-WindowsUpdate.Tests.ps1` and + `Test-WingetUpgrades.Tests.ps1` drop `Import-Module Mock-Helpers` lines whose + suites use only inline objects and local factories; and + `ConvertFrom-Jsonc.Tests.ps1` inlines a single-use intermediate to the form + its sibling assertions use. All suite counts byte-identical before and after + on this host (including the pre-existing Linux cmdlet-gap failures, which are + unchanged and reported, not hidden). + ## [0.11.16] ### Changed diff --git a/plugins/machine-health/skills/audit/tests/helpers/Mock-Helpers.psm1 b/plugins/machine-health/skills/audit/tests/helpers/Mock-Helpers.psm1 index 156a8b055..14536c3cc 100644 --- a/plugins/machine-health/skills/audit/tests/helpers/Mock-Helpers.psm1 +++ b/plugins/machine-health/skills/audit/tests/helpers/Mock-Helpers.psm1 @@ -225,30 +225,6 @@ function New-MockReliabilityRecord { } } -function New-MockWinGetPackage { - # Shape matches Microsoft.WinGet.Client 1.12+ Get-WinGetPackage output. - # IsUpdateAvailable is the boolean gate the check filters on; AvailableVersions - # is newest-first. Tests that mock Get-WinGetPackage itself use this factory. - [CmdletBinding()] - [OutputType([pscustomobject])] - param( - [Parameter(Mandatory)] [string] $Name, - [Parameter(Mandatory)] [string] $Id, - [string] $InstalledVersion = '1.0.0', - [string[]] $AvailableVersions = @('1.1.0'), - [string] $Source = 'winget', - [bool] $IsUpdateAvailable = $true - ) - [pscustomobject]@{ - Name = $Name - Id = $Id - InstalledVersion = $InstalledVersion - AvailableVersions = $AvailableVersions - Source = $Source - IsUpdateAvailable = $IsUpdateAvailable - } -} - function New-MockEnvironmentKey { <# .SYNOPSIS @@ -332,6 +308,5 @@ Export-ModuleMember -Function @( 'New-MockReliabilityStabilityMetric' 'New-MockService' 'New-MockVolume' - 'New-MockWinGetPackage' 'Remove-MachineHealthTempDir' ) diff --git a/plugins/machine-health/skills/audit/tests/windows/checks/Test-WindowsUpdate.Tests.ps1 b/plugins/machine-health/skills/audit/tests/windows/checks/Test-WindowsUpdate.Tests.ps1 index f4cbebecb..ad84095cf 100644 --- a/plugins/machine-health/skills/audit/tests/windows/checks/Test-WindowsUpdate.Tests.ps1 +++ b/plugins/machine-health/skills/audit/tests/windows/checks/Test-WindowsUpdate.Tests.ps1 @@ -26,7 +26,6 @@ BeforeAll { $script:ScriptPath = Join-Path $script:SkillRoot 'scripts\windows\checks\Test-WindowsUpdate.ps1' $script:LibRoot = Join-Path $script:SkillRoot 'scripts\windows\lib' . (Join-Path $script:LibRoot 'Assert-CheckResult.ps1') - Import-Module (Join-Path $script:TestsRoot 'helpers\Mock-Helpers.psm1') -Force . (Join-Path $script:TestsRoot 'helpers\Invoke-CheckScript.ps1') function Invoke-WindowsUpdateAsObject { Invoke-CheckScriptAsObject $script:ScriptPath } diff --git a/plugins/machine-health/skills/audit/tests/windows/checks/Test-WingetUpgrades.Tests.ps1 b/plugins/machine-health/skills/audit/tests/windows/checks/Test-WingetUpgrades.Tests.ps1 index 16998cc16..a899c111c 100644 --- a/plugins/machine-health/skills/audit/tests/windows/checks/Test-WingetUpgrades.Tests.ps1 +++ b/plugins/machine-health/skills/audit/tests/windows/checks/Test-WingetUpgrades.Tests.ps1 @@ -32,7 +32,6 @@ BeforeAll { . (Join-Path $script:LibRoot 'Assert-CheckResult.ps1') . (Join-Path $script:LibRoot 'Get-WingetPackageUpdate.ps1') . (Join-Path $script:LibRoot 'Get-CisaKevCache.ps1') - Import-Module (Join-Path $script:TestsRoot 'helpers\Mock-Helpers.psm1') -Force . (Join-Path $script:TestsRoot 'helpers\Invoke-CheckScript.ps1') function Invoke-WingetUpgradesAsObject { Invoke-CheckScriptAsObject $script:ScriptPath } diff --git a/plugins/machine-health/skills/audit/tests/windows/lib/ConvertFrom-Jsonc.Tests.ps1 b/plugins/machine-health/skills/audit/tests/windows/lib/ConvertFrom-Jsonc.Tests.ps1 index 813b428a9..847ec34c0 100644 --- a/plugins/machine-health/skills/audit/tests/windows/lib/ConvertFrom-Jsonc.Tests.ps1 +++ b/plugins/machine-health/skills/audit/tests/windows/lib/ConvertFrom-Jsonc.Tests.ps1 @@ -42,8 +42,7 @@ Describe 'Remove-JsoncComment' -Tag 'lib' { It 'preserves // inside a string and strips a trailing comment on the same line' { $in = '{ "url": "https://example.com" } // docs link' $out = Remove-JsoncComment -Text $in - $parsed = $out | ConvertFrom-Json - $parsed.url | Should -Be 'https://example.com' + ($out | ConvertFrom-Json).url | Should -Be 'https://example.com' } It 'preserves a // inside a string that ends near the line end' { From 1ef2e48f6453695a032719ae0717c968372f6cbd Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 31 Aug 2026 00:30:21 +0000 Subject: [PATCH 05/10] Wave 9 simplify: provenance check-stamps duplicate-branch collapse Repo-wide batch-simplify wave 9 (groups 48-53: miro, planning, prototype, plugin-quality, skill-quality, provenance, rate-limit-guard, repo-hygiene clean core). Five groups closed with zero edits; the one shipped edit collapses the duplicate --* arm of from_label() in plugins/provenance/skills/audit/scripts/check-stamps.sh into its identical fallback arm. A fresh-context refutation verifier executed a 25-case old-vs-new input matrix (flag forms, config-layer paths, format-string hazards, whitespace, multi-arg and no-arg calls) plus end-to-end --show-config runs: byte-identical output everywhere. Suite: 71 passed. shellcheck clean. provenance bumped to 0.4.2 with a changelog entry; changelog-parity, vendor-bump, and catalog gates pass. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01W61wikkiEK5StWgg9rfEQW --- plugins/provenance/.claude-plugin/plugin.json | 2 +- plugins/provenance/CHANGELOG.md | 11 +++++++++++ .../provenance/skills/audit/scripts/check-stamps.sh | 2 -- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/plugins/provenance/.claude-plugin/plugin.json b/plugins/provenance/.claude-plugin/plugin.json index 57bceb421..47e2b9921 100644 --- a/plugins/provenance/.claude-plugin/plugin.json +++ b/plugins/provenance/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "provenance", - "version": "0.4.1", + "version": "0.4.2", "description": "Finds prose in tracked markdown that restates content an external source owns (vendor docs, blogs, articles) without adequate attribution, confirms the source, and refactors the copy into a pointer, a citation, or a dated stamped record. Documentation provenance, not software supply chain. Nomination and judgment are LLM work; the scripts do only reasoning-free work (corpus scoping, breadcrumb extraction, stamp expiry, fingerprint compare of two concrete texts). Read-only audit by default; explicit fix and sweep actions apply dispositions behind a semantic-diff guard and live pointer verification. Findings conform to the detector-findings convention.", "author": { "name": "Melodic Software", diff --git a/plugins/provenance/CHANGELOG.md b/plugins/provenance/CHANGELOG.md index 00f24634e..9bdd86e89 100644 --- a/plugins/provenance/CHANGELOG.md +++ b/plugins/provenance/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## [0.4.2] + +### Changed + +- **`audit`: `check-stamps.sh` collapses a duplicate branch in `from_label()`.** The `--*` arm and + the fallback arm of the three-way conditional printed the character-identical `(from %s)` string, + so the split carried no behavior. The two arms are now one. Verified over an executed 25-case + input matrix (flag forms, config-layer paths, format-string hazards, whitespace, multi-arg and + no-arg calls): old and new outputs are byte-identical everywhere, and the 71-case suite passes + unchanged. + ## [0.4.1] ### Fixed diff --git a/plugins/provenance/skills/audit/scripts/check-stamps.sh b/plugins/provenance/skills/audit/scripts/check-stamps.sh index 7f9ef1dfe..0bfdf6dbe 100755 --- a/plugins/provenance/skills/audit/scripts/check-stamps.sh +++ b/plugins/provenance/skills/audit/scripts/check-stamps.sh @@ -199,8 +199,6 @@ fi from_label() { if [[ -z "$1" ]]; then printf '(bundled default)' - elif [[ "$1" == --* ]]; then - printf '(from %s)' "$1" else printf '(from %s)' "$1" fi From 0ac2653d069f34036749297b4554cfee7bff4afc Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 31 Aug 2026 00:36:05 +0000 Subject: [PATCH 06/10] Wave 10 simplify: repo-hygiene git-prune stale lint directive removed Repo-wide batch-simplify wave 10 (groups 54-57: repo-hygiene git scripts + repo-fleet-hygiene, session-flow, small skill scripts, small updaters). Three groups closed with zero edits; the one shipped edit removes a stale "shellcheck disable=SC2086" directive above a quoted eval in plugins/repo-hygiene/skills/clean/scripts/git-prune.sh. A fresh-context refutation verifier confirmed the directive was inert: ShellCheck 0.11.0 with the repo rcfile produces identical output and exit code with and without it, no repo tooling inventories or pins disable comments, and the mapped suites (clean-batch 84 checks, git-prune) pass. repo-hygiene bumped to 0.10.19 with a changelog entry; changelog-parity, vendor-bump, and catalog gates pass. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01W61wikkiEK5StWgg9rfEQW --- plugins/repo-hygiene/.claude-plugin/plugin.json | 2 +- plugins/repo-hygiene/CHANGELOG.md | 11 +++++++++++ .../repo-hygiene/skills/clean/scripts/git-prune.sh | 1 - 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/plugins/repo-hygiene/.claude-plugin/plugin.json b/plugins/repo-hygiene/.claude-plugin/plugin.json index 7c1833f37..688b710bc 100644 --- a/plugins/repo-hygiene/.claude-plugin/plugin.json +++ b/plugins/repo-hygiene/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "repo-hygiene", - "version": "0.10.18", + "version": "0.10.19", "description": "Repo hygiene action-router: /repo-hygiene:clean sweeps reclaimable caches, build artifacts, and stale git metadata, and can realign the working tree to a fresh-pull state — dry-run-first, with destructive tiers gated behind explicit confirmation and a session-scoped destructive-command guard. Ecosystem targets are detected at runtime; secrets, runtime dependencies, and skill data are preserved by default.", "author": { "name": "Melodic Software", diff --git a/plugins/repo-hygiene/CHANGELOG.md b/plugins/repo-hygiene/CHANGELOG.md index 785b6bebd..27ba2cdbf 100644 --- a/plugins/repo-hygiene/CHANGELOG.md +++ b/plugins/repo-hygiene/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to the `repo-hygiene` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. +## [0.10.19] + +### Changed + +- **`clean`: `git-prune.sh` drops a stale `shellcheck disable=SC2086` directive.** The suppressed + warning cannot fire on the line it annotated: the `eval "$op"` expansion is quoted, so SC2086 + reports nothing there under the pinned ShellCheck 0.11.0 with the repo rcfile. Verified by + running ShellCheck on the file with and without the directive: identical output and exit code + both ways, and no repo tooling inventories or pins disable comments. Comment-only change; the + script's behavior is untouched and its suite passes unchanged. + ## [0.10.18] ### Changed diff --git a/plugins/repo-hygiene/skills/clean/scripts/git-prune.sh b/plugins/repo-hygiene/skills/clean/scripts/git-prune.sh index 03e61d4c2..86b252c4d 100755 --- a/plugins/repo-hygiene/skills/clean/scripts/git-prune.sh +++ b/plugins/repo-hygiene/skills/clean/scripts/git-prune.sh @@ -63,7 +63,6 @@ for op in "${GIT_PRUNE_OPS[@]}"; do printf 'Planned: %s\n' "$op" else printf 'Running: %s\n' "$op" >&2 - # shellcheck disable=SC2086 eval "$op" fi done From 6f31429cdfacae563e176e7ab9aa0089fee83c53 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 31 Aug 2026 04:23:54 +0000 Subject: [PATCH 07/10] Wave 11 simplify: fetch-annotations comment-accuracy fix Repo-wide batch-simplify wave 11 (groups 58-62, the source-control plugin: babysit-prs source and tests, bin + hooks, pull-request skill scripts, plugin scripts + commit skill). Four groups closed with zero edits (babysit source 643-test baseline OK; babysit tests likewise; bin + hooks and scripts trees confirmed frozen fail-closed surface). The one shipped edit rewords a comment in plugins/source-control/skills/pull-request/scripts/fetch-annotations.sh that misdescribed its own exit-code capture ("captured via PIPESTATUS" where no pipeline ever existed) to "captured separately". A fresh-context refutation verifier confirmed the comment was wrong from the file's first commit, nothing pins the old string, and the suite passes (8/8). source-control bumped to 0.55.32 with a changelog entry; changelog-parity, vendor-bump, and catalog gates pass. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01W61wikkiEK5StWgg9rfEQW --- plugins/source-control/.claude-plugin/plugin.json | 2 +- plugins/source-control/CHANGELOG.md | 12 ++++++++++++ .../skills/pull-request/scripts/fetch-annotations.sh | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/plugins/source-control/.claude-plugin/plugin.json b/plugins/source-control/.claude-plugin/plugin.json index 01e496a6a..a3895a2fc 100644 --- a/plugins/source-control/.claude-plugin/plugin.json +++ b/plugins/source-control/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "source-control", - "version": "0.55.31", + "version": "0.55.32", "description": "Git and GitHub delivery workflow: /commit (Conventional Commits + Co-authored-by trailer via safe heredoc mechanics), /pull-request (prep, create, CI monitoring, review-comment triage, merge, CI-log fetch), /babysit-prs (self-pacing fleet loop \u2014 safe by default; opt-in worker/autopilot tiers add gate-checked merge and thread resolution behind a deterministic Python engine), /babysit-loop (the loop-lane merge lane: a standing or drain loop that invokes babysit-prs per cycle, configured through repo-scoped babysit_loop_* keys on the layered source-control.md seam, with merge authority human-only until the target repo's tracked config adopts the lane, a gate-proven C2-mechanical baseline once adopted, and standing merge-rung raises binding from the team-tracked layer only \u2014 with one named exception, where an invocation line explicitly typing both the autopilot tier keyword and the dedicated raise argument --merge c3-this-run widens that single invocation's merge authority up to C3 behind a fresh independent frontier-tier resolver, while C4-structural and C5-untrusted-provenance stay unconditionally human-merge), /worktree (create, status, cleanup, audit for parallel-session isolation), /setup (check the effective commit-subject / PR-title convention merged across its config layers and the babysit-prs config, or apply \u2014 interview the repo and write the convention config to a chosen layer), and /resolve-conflicts (intent-first merge/rebase conflict resolution with a semantic-conflict sweep \u2014 never --abort). The commit-subject / PR-title convention is configurable via a source-control.md config written by a re-runnable setup skill, layered across a ~/.claude user-global file, the tracked team file, and a gitignored .claude/source-control.local.md personal overlay merged per key; Conventional Commits is the default when no convention is declared.", "author": { "name": "Melodic Software", diff --git a/plugins/source-control/CHANGELOG.md b/plugins/source-control/CHANGELOG.md index 99613ab2c..21814d440 100644 --- a/plugins/source-control/CHANGELOG.md +++ b/plugins/source-control/CHANGELOG.md @@ -3,6 +3,18 @@ All notable changes to the `source-control` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. +## [0.55.32] + +### Changed + +- **`pull-request`: `fetch-annotations.sh` corrects a comment that misdescribed its own exit-code + capture.** The comment above the check-runs fetch said the API exit code is "captured via + PIPESTATUS", but the code has captured `$?` from a plain command substitution since the file's + first commit; no pipeline ever existed there. The comment now says "captured separately", + matching the accurate sibling comment in the annotations walk. Comment-only change, verified + against the file's full history; the script's behavior is untouched and its suite passes + unchanged. + ## [0.55.31] ### Changed diff --git a/plugins/source-control/skills/pull-request/scripts/fetch-annotations.sh b/plugins/source-control/skills/pull-request/scripts/fetch-annotations.sh index 52dbdbfec..22dc5cc81 100755 --- a/plugins/source-control/skills/pull-request/scripts/fetch-annotations.sh +++ b/plugins/source-control/skills/pull-request/scripts/fetch-annotations.sh @@ -105,7 +105,7 @@ fi # Returns list of check_run records: id, name, conclusion, status. Apply jq # client-side (avoids gh's --jq flag for stubbing simplicity). `--paginate` # walks all pages so PRs with >100 check-runs aren't silently truncated. The -# `gh api` exit code is captured via PIPESTATUS so transport/auth/rate-limit +# `gh api` exit code is captured separately so transport/auth/rate-limit # failures surface as exit 2 (per the docstring contract) instead of falling # through the empty-output branch as a misleading success. CHECK_RUNS_RAW=$(gh api --paginate "repos/$REPO/commits/$HEAD_SHA/check-runs?per_page=100" 2>/dev/null) From 7b6ce07c41e4ecc51ebe76daf88d9b92856c83a0 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 31 Aug 2026 05:19:15 +0000 Subject: [PATCH 08/10] Deferred-work pass: fix every actionable finding from the repo sweep The repo-wide batch-simplify sweep (68 groups, 12 waves) recorded every deferred finding with a disposition; this commit works all the actionable ones in-branch, per-fix verified with reproduce-then-fix evidence and reviewed as a whole by a fresh-context adversarial pass. Root scripts: check-changelog-parity.sh emits "lists X more than once" with its missing space (test pinned); check-hook-exec-form.test.sh garbled ok-string reworded; generate-cheatsheet.mjs names the offending file on a frontmatter-less SKILL.md instead of a raw TypeError; package.json allowScripts pin restored to the 2.1.246 devDependency. affected-tests.sh gains deletion awareness: a changed path that no longer exists and that nothing claims reports a visible "deleted:" note instead of the UNMAPPED error, while a deletion a surviving suite still names keeps selecting it (two new suite cases; README contract updated). guardrails 0.29.24: verify-cli-flag.sh streams help output through a printf pipe instead of staging it in a here-string (strace-confirmed mechanism; ten-case byte-identical differential), and the require-jq-notice-isolation test no longer emits a two-line count on its zero-match path. machine-health 0.11.18: correlation-rules.md names the real Get-CorrelationRule; Write-ElevationBanner tests capture [Console]::Error output for real; Restart-StoppedService tests run on Linux via the established stub pattern (0 to 10 passing); Scaffold tests pin empty USERNAME/COMPUTERNAME with restore (17+3 to 20). claude-config 0.40.21: duplicate test case labels renumbered. discovery 0.16.18: unreadable-ledger case skips visibly as root. instruction-placement 0.11.14: render-index.sh no longer corrupts brace globs when prettifying rule glob lists (fix plus two test pins). rate-limit-guard 0.7.21 and context-guard 0.7.28: the statusline tees exchange their proven hardening patterns; payloads over 1MiB pass through intact, temp writes refuse pre-planted symlinks, and SIGKILL no longer leaks temp files (traps plus age-filtered sweep). Zero-fork assertions and bench spawn floor unchanged. source-control 0.55.33: three scripts stop truncating --help via the derived header printer babysit-readiness-gate.sh already ships. work-items 0.39.42: conformance e2e-probe drops a vacuous assertion; the linear schema-check tree gets an accurate fidelity.sh comment, an optional catch binding, and a no-suite allowlist entry recording its on-demand lane. knowledge 0.13.29: orphaned promotion-name-map.js removed with independently re-proven zero importers. Adversarial review over the whole diff: both findings fixed (the UNMAPPED deletion via the affected-tests deletion handling above; a too-strong changelog claim reworded); full-suite union run green (146 shell suites), parity/order/vendor/catalog gates green. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01W61wikkiEK5StWgg9rfEQW --- README.md | 7 +- package.json | 2 +- .../claude-config/.claude-plugin/plugin.json | 2 +- plugins/claude-config/CHANGELOG.md | 10 + .../scripts/permission-rule-check.test.sh | 12 +- .../context-guard/.claude-plugin/plugin.json | 2 +- plugins/context-guard/CHANGELOG.md | 14 + .../context-guard/scripts/statusline-tee.sh | 70 +- .../scripts/statusline-tee.test.sh | 36 +- plugins/discovery/.claude-plugin/plugin.json | 2 +- plugins/discovery/CHANGELOG.md | 11 + .../scripts/check-coverage-complete.test.sh | 8 +- plugins/guardrails/.claude-plugin/plugin.json | 2 +- plugins/guardrails/CHANGELOG.md | 16 + .../hooks/require-jq-notice-isolation.test.sh | 4 +- .../lib/verification/verify-cli-flag.sh | 10 +- .../.claude-plugin/plugin.json | 2 +- plugins/instruction-placement/CHANGELOG.md | 12 + .../scripts/render-index.sh | 7 +- .../scripts/render-index.test.sh | 15 + plugins/knowledge/.claude-plugin/plugin.json | 2 +- plugins/knowledge/CHANGELOG.md | 12 + .../video-digest/context/quality-gates.md | 2 +- .../extraction/watch/promotion-name-map.js | 32 - .../machine-health/.claude-plugin/plugin.json | 2 +- plugins/machine-health/CHANGELOG.md | 26 + .../references/shared/correlation-rules.md | 2 +- .../audit/tests/windows/Scaffold.Tests.ps1 | 15 + .../lib/Write-ElevationBanner.Tests.ps1 | 40 +- .../Restart-StoppedService.Tests.ps1 | 8 + .../.claude-plugin/plugin.json | 2 +- plugins/rate-limit-guard/CHANGELOG.md | 20 + .../scripts/statusline-tee.sh | 32 +- .../scripts/statusline-tee.test.sh | 21 + .../source-control/.claude-plugin/plugin.json | 2 +- plugins/source-control/CHANGELOG.md | 14 + .../scripts/fetch-all-pr-comments.sh | 6 +- .../scripts/fetch-all-pr-comments.test.sh | 7 + .../scripts/reap-project-plugin-records.sh | 6 +- .../reap-project-plugin-records.test.sh | 10 +- .../scripts/worktree-root-doctor.sh | 6 +- .../scripts/worktree-root-doctor.test.sh | 7 + plugins/work-items/.claude-plugin/plugin.json | 2 +- plugins/work-items/CHANGELOG.md | 23 + .../adapters/linear/schema-check/fidelity.sh | 4 +- .../adapters/linear/schema-check/negative.mjs | 2 +- .../conformance/e2e-probe.sh | 19 +- scripts/affected-tests-no-suite.txt | 10 + scripts/affected-tests.sh | 17 +- scripts/affected-tests.test.sh | 29 + scripts/check-changelog-parity.sh | 2 +- scripts/check-changelog-parity.test.sh | 1 + scripts/check-hook-exec-form.test.sh | 2 +- scripts/generate-cheatsheet.mjs | 6 + testResults.xml | 1892 +++++++++++++++++ 55 files changed, 2425 insertions(+), 102 deletions(-) delete mode 100644 plugins/knowledge/skills/video-digest/extraction/watch/promotion-name-map.js create mode 100644 testResults.xml diff --git a/README.md b/README.md index 59a05cab6..a8ebf6991 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,12 @@ genuinely carry no suite are recorded, with the CI lane that does cover them, in [`scripts/affected-tests-no-suite.txt`](scripts/affected-tests-no-suite.txt); `--allow-unmapped` is the escape hatch for everything else. That list is for prose and manifests, never for code: a source file with no coverage is supposed -to fail here. +to fail here. A **deletion** is the one exception: a changed path that no longer +exists and that nothing claims is reported as a visible `deleted:` note instead +of the error, because there is no content left to cover — while a deletion that +a surviving suite still names (a co-located test left behind, a suite that +references the dead path) keeps selecting those suites, which are exactly what +fails loudly if the deletion broke something. The runner is deliberately sequential: parallelising it measured sublinear (the suites are spawn-bound), and several guardrails suites assert wall-clock diff --git a/package.json b/package.json index 8eeea7176..6ec0df052 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,6 @@ "markdownlint-cli2": "0.23.2" }, "allowScripts": { - "@anthropic-ai/claude-code@2.1.209": true + "@anthropic-ai/claude-code@2.1.246": true } } diff --git a/plugins/claude-config/.claude-plugin/plugin.json b/plugins/claude-config/.claude-plugin/plugin.json index bf5e83c8e..362617fac 100644 --- a/plugins/claude-config/.claude-plugin/plugin.json +++ b/plugins/claude-config/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "claude-config", - "version": "0.40.20", + "version": "0.40.21", "description": "Nine configuration-health skills (plus setup) for a repo's Claude Code configuration: audit (settings.json / .mcp.json / hooks / plugins / permissions drift), audit-automation-gaps (evidence-gated verdicts on automation gaps), audit-permission-grants (allow-rule / allowed-tools grants for auto-mode durability and portability), audit-permission-state (the permission rules actually in effect \u2014 every settings scope merged with per-rule provenance, what auto mode drops on entry, config written where nothing reads it, and which managed intents are enforced versus loosenable), draft-auto-mode-rules (interview and draft a paste-ready autoMode classifier block; prints only, never writes), audit-instructions (locally-owned instruction surfaces vs current model capability \u2014 proposes removals/rewrites of instructions the model no longer needs, and detects cross-surface instruction conflicts), audit-prompting-postures (the additive lane \u2014 posture guidance the prompting guide says a component's purpose needs but the component does not carry), audit-pass (one coordinated, ordered, resumable pass over a named target \u2014 three-scope inventory, run-time-derived exclusion set, stable finding identity, suppression memory, resume, one human gate \u2014 delegating every check to the plugin that owns it), and unhobble (the empirical bare-baseline experiment: reversibly strip a repo's standing instructions, log real stumbles against the current model, re-add only what evidence earns).", "author": { "name": "Melodic Software", diff --git a/plugins/claude-config/CHANGELOG.md b/plugins/claude-config/CHANGELOG.md index d75e965df..0ac6094af 100644 --- a/plugins/claude-config/CHANGELOG.md +++ b/plugins/claude-config/CHANGELOG.md @@ -3,6 +3,16 @@ All notable changes to the `claude-config` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. +## [0.40.21] + +### Changed + +- **`audit-permission-grants`: `permission-rule-check.test.sh` renumbers a colliding block of case + labels.** The regression block after Case 11 reused labels 8b through 8g that an earlier block + already carried, so two different cases shared each label. The later block is now Cases 12 + through 13b, grouped by the issue each case pins. Comment-only: no assertion, variable, or + fixture changed, and the suite passes unchanged at 139 checks. + ## [0.40.20] ### Changed diff --git a/plugins/claude-config/skills/audit-permission-grants/scripts/permission-rule-check.test.sh b/plugins/claude-config/skills/audit-permission-grants/scripts/permission-rule-check.test.sh index fffbae415..37165b292 100755 --- a/plugins/claude-config/skills/audit-permission-grants/scripts/permission-rule-check.test.sh +++ b/plugins/claude-config/skills/audit-permission-grants/scripts/permission-rule-check.test.sh @@ -563,7 +563,7 @@ assert_contains "refusal names the sanctioned variable as the fix" \ -u CLAUDE_PROJECT_DIR GIT_CEILING_DIRECTORIES="$TEST_TMPDIR" bash "$SCRIPT" 2>&1)" \ "PERMISSION_HYGIENE_SCAN_ROOT" -# --- Case 8b: #2282 — full-rule reporting, `//` is NOT exempt, P1 pinned npm view +# --- Case 12: #2282 — full-rule reporting, `//` is NOT exempt, P1 pinned npm view # # `//` is the ABSOLUTE anchor, not a portable one. permissions.md's own table row is # `//path` = "Absolute path from filesystem root", with `Read(//Users//secrets/**)` @@ -582,7 +582,7 @@ assert_contains "P2 reports the full offending Bash rule" "$OUT_2282" "Bash(${PO assert_not_contains "fully-pinned npm view rule is not flagged as P1" "$OUT_2282" "npm view ctx7 version" assert_eq "both machine-path rules flagged, npm view not" "2" "$(run "$D8B" --count)" -# --- Case 8c: the genuinely portable anchors stay exempt ---------------------- +# --- Case 12b: the genuinely portable anchors stay exempt --------------------- # The distinction the fix turns on: `~/` and `${CLAUDE_PROJECT_DIR}/` supply the # user/project segment at resolution time; `//` does not. D8C="$TEST_TMPDIR/issue-2282-portable" @@ -590,7 +590,7 @@ mkdir -p "$D8C/.claude" jq -n '{permissions:{allow:["Read(~/Documents/*.pdf)","Bash(${CLAUDE_PROJECT_DIR}/scripts/x.sh:*)"]}}' >"$D8C/.claude/settings.json" assert_eq "portable anchors produce no P2 finding" "0" "$(run "$D8C" --count)" -# --- Case 8d: P2 reach is the open tool grammar, not five hardcoded names ------ +# --- Case 12c: P2 reach is the open tool grammar, not five hardcoded names ----- # A hardcoded machine path leaks a username whatever tool the rule names. An # enumerated (Read|Edit|Write|Bash|PowerShell) list silently stopped flagging these; # `Agent` in particular is indefensible, since this script has a dedicated @@ -604,7 +604,7 @@ assert_contains "P2 sees Glob rules" "$OUT_TOOLS" "Glob(${GLOB_MP})" assert_contains "P2 sees NotebookEdit rules" "$OUT_TOOLS" "NotebookEdit(${NB_MP})" assert_contains "P2 sees MCP tool rules" "$OUT_TOOLS" "mcp__srv__tool(${MCP_MP})" -# --- Case 8e: a `//` prefix does not launder a path later in the same rule ----- +# --- Case 12d: a `//` prefix does not launder a path later in the same rule ---- # Regression guard for a substring carve-out (`$m == *"(//"*`) that passed any rule # whose payload merely began with `//`, leaving the rest unexamined. D8E="$TEST_TMPDIR/issue-2282-traversal" @@ -612,7 +612,7 @@ mkdir -p "$D8E/.claude" jq -n --arg l "Read(${LAUNDER_MP})" '{permissions:{allow:[$l]}}' >"$D8E/.claude/settings.json" assert_eq "a // prefix does not exempt a user home later in the rule" "1" "$(run "$D8E" --count)" -# --- Case 8f: #2397 A12 — tilde-user Bash paths leak a username ---------------- +# --- Case 13: #2397 A12 — tilde-user Bash paths leak a username ---------------- D8F="$TEST_TMPDIR/issue-2397-tilde-user" mkdir -p "$D8F/.claude" jq -n '{permissions:{allow:["Bash(~kyle/scripts/x.sh:*)"]}}' >"$D8F/.claude/settings.json" @@ -627,7 +627,7 @@ jq -n '{permissions:{allow:["Bash(curl https://example.com/~alice/index.html)"]} >"$D8F_URL/.claude/settings.json" assert_eq "URL user-directory segment is not flagged as tilde-user path" "0" "$(run "$D8F_URL" --count)" -# --- Case 8g: #2397 A7b — inert substitution tokens in allowed-tools ---------- +# --- Case 13b: #2397 A7b — inert substitution tokens in allowed-tools --------- D8G="$TEST_TMPDIR/issue-2397-inert" mkdir -p "$D8G/.claude/skills/demo" cat >"$D8G/.claude/skills/demo/SKILL.md" <<'EOF' diff --git a/plugins/context-guard/.claude-plugin/plugin.json b/plugins/context-guard/.claude-plugin/plugin.json index dab037a87..fb6441005 100644 --- a/plugins/context-guard/.claude-plugin/plugin.json +++ b/plugins/context-guard/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "context-guard", - "version": "0.7.27", + "version": "0.7.28", "description": "Per-session context-window observability plus the first shipped consumer: a statusline wrapper tees each session's context_window fields to a per-session snapshot file, a zone resolver classifies usage into smart/acceptable/dumb bands (percentage bands plus window-class token bands, conservative-min combination, zones.json SSOT with shipped defaults), a reader contract fixes how consuming sessions interpret the snapshots, and zone-crossing hooks report once per transition into a worse zone across two channels — the continuation menu to the operator, who owns that choice, and to the model only the zone determination plus the counter-steer that a zone word is not a decay signal (advisory by default; an optional blocking mode gates new mutating work on a fresh dumb-zone snapshot with handoff-writing exempt), with a PostCompact hook persisting an evidence-degraded marker.", "author": { "name": "Melodic Software", diff --git a/plugins/context-guard/CHANGELOG.md b/plugins/context-guard/CHANGELOG.md index 7fc315ba4..3bf796b5e 100644 --- a/plugins/context-guard/CHANGELOG.md +++ b/plugins/context-guard/CHANGELOG.md @@ -5,6 +5,20 @@ All notable changes to the `context-guard` plugin. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.7.28] + +### Fixed + +- **`statusline-tee.sh` no longer leaks temp files when the process dies mid-write.** The tee had + no traps, and its snapshot prune deliberately excluded `.tmp.*` names, so a SIGKILL or crash + between temp write and rename left the temp file behind forever. Ported the sibling + rate-limit-guard tee's proven pattern: a reclaim function wired to EXIT/TERM/INT/HUP traps + (signal traps exit so EXIT stays the single reclaim path) plus an age-filtered orphan sweep for + the SIGKILL case, glob-guarded so a clean directory spawns no processes. The suite's old case + that pinned the leak now asserts reclamation, with a fresh in-flight temp asserted untouched, + and a new case proves the trap reclaims when the tee is terminated mid-window. Suite grows from + 47 to 49 checks, all passing. + ## [0.7.27] ### Changed diff --git a/plugins/context-guard/scripts/statusline-tee.sh b/plugins/context-guard/scripts/statusline-tee.sh index 3cdcaafd9..445ef875d 100755 --- a/plugins/context-guard/scripts/statusline-tee.sh +++ b/plugins/context-guard/scripts/statusline-tee.sh @@ -40,7 +40,21 @@ # older than 14 days are deleted on write. The cutoff is deliberately far # larger than the reader contract's 10-minute staleness window so a # live-but-idle session's snapshot is never deleted, and in-flight -# .tmp.* files are never touched. +# .tmp.* files are never touched by the prune (aged temp ORPHANS are the +# reclaim sweep's job — see TEMP-FILE RECLAIM below). +# +# TEMP-FILE RECLAIM: a temp file can outlive this process. Claude Code +# "cancels the in-flight script" when a new update arrives while this one is +# still running (https://code.claude.com/docs/en/statusline), and a +# cancellation between the write and the rename leaves the temp behind — no +# failed rm is needed to explain it, the process simply never reaches the +# reclaim line. Two mechanisms, because neither is sufficient alone: a trap +# reclaims on exit and on a catch-able signal, and an age-filtered sweep of +# leftover siblings recovers what a SIGKILL, a crash, or power loss leaves, +# which no trap can. The sweep costs nothing on a clean directory — a glob +# decides whether to spawn anything at all — and it cannot race a live +# sibling, since the normal write-to-rename window is sub-second while the +# age floor is a minute. # # ATOMICITY: concurrent refreshes of the same session and cross-session # sibling writes share one directory, and readers must never see torn JSON, @@ -83,6 +97,46 @@ else IFS= read -r -d '' -t 5 INPUT || true fi +# Path of the temp file currently in flight, for the reclaim traps below. A +# global rather than the function's local: the trap body is evaluated when the +# trap fires, by which time the function's locals are gone. +TEE_TMP="" + +reclaim_tee_tmp() { + [[ -n "$TEE_TMP" ]] && rm -f "$TEE_TMP" 2>/dev/null + TEE_TMP="" + return 0 +} + +# The signal traps exit rather than reclaiming directly, so the EXIT trap stays +# the single reclaim path. Exiting is also the right response to a cancelling +# signal: this refresh's snapshot is already superseded by the update that +# cancelled it. +trap 'reclaim_tee_tmp' EXIT +trap 'exit 143' TERM +trap 'exit 130' INT +trap 'exit 129' HUP + +# Reclaim temp siblings left by a process that never got to clean up. Only +# files older than the age floor are touched, so a concurrent refresh's live +# temp — sub-second between write and rename — is never a candidate. +# +# The glob runs first and decides whether to spawn at all: on a clean +# directory, which is every refresh in normal operation, this costs zero +# processes. The pattern requires the .json.tmp. infix, so it can never match +# a snapshot the prune owns, and the prune's *.json pattern never matches +# these dot-prefixed names — the two mechanisms are disjoint by construction. +sweep_stale_tee_temps() { + local dir="$1" candidate + for candidate in "$dir"/.*.json.tmp.*; do + [[ -e "$candidate" ]] || continue + find "$dir" -maxdepth 1 -type f -name '.*.json.tmp.*' \ + -mmin +1 -exec rm -f {} + 2>/dev/null || true + return 0 + done + return 0 +} + # Write one per-session contract snapshot. Every failure path returns 0: the # tee must never propagate into the statusline pipeline. tee_snapshot() { @@ -150,10 +204,15 @@ tee_snapshot() { [ -n "$(find "$f" -maxdepth 0 -mmin +20160 2>/dev/null)" ] && rm -f "$f" done' _ {} + 2>/dev/null || true + # Reclaim aged temp orphans a SIGKILL/crash left behind (the traps above + # cover every catch-able exit; this sweep is the mechanism for the rest). + sweep_stale_tee_temps "$dir" + # Process-unique temp name with widened entropy; noclobber makes the # redirection refuse to follow a pre-planted symlink or overwrite any # pre-existing path of the same name. local tmp="$dir/.$sid.json.tmp.$$.$RANDOM$RANDOM" + TEE_TMP="$tmp" # Subshell umask so the snapshot lands owner-only without altering the # umask the wrapped statusline command inherits. ( @@ -161,7 +220,7 @@ tee_snapshot() { set -o noclobber printf '%s\n' "$payload" >"$tmp" ) 2>/dev/null || { - rm -f "$tmp" 2>/dev/null + reclaim_tee_tmp return 0 } # Plausibility ceiling for the no-regression guard below: a target whose @@ -192,15 +251,18 @@ tee_snapshot() { existing_ts=$(jq -r '.captured_at // empty' "$target" 2>/dev/null) || existing_ts="" if [[ -n "$existing_ts" && -n "$guard_ceiling" && "$existing_ts" > "$ts" && ! "$existing_ts" > "$guard_ceiling" ]]; then - rm -f "$tmp" 2>/dev/null + reclaim_tee_tmp return 0 fi if mv -f "$tmp" "$target" 2>/dev/null; then + # The temp path is the target now; clear it so the EXIT trap cannot + # reclaim a name that no longer refers to this refresh's file. + TEE_TMP="" return 0 fi sleep 0.1 2>/dev/null || true done - rm -f "$tmp" 2>/dev/null + reclaim_tee_tmp return 0 } diff --git a/plugins/context-guard/scripts/statusline-tee.test.sh b/plugins/context-guard/scripts/statusline-tee.test.sh index d320b20fb..e1eacd2bb 100755 --- a/plugins/context-guard/scripts/statusline-tee.test.sh +++ b/plugins/context-guard/scripts/statusline-tee.test.sh @@ -12,7 +12,8 @@ # filename use (anything else skips the tee — path-containment guard). # Stale sibling snapshots are pruned on write with a cutoff far larger than # the reader contract's 10-minute staleness window (live-but-idle sessions -# survive; in-flight .tmp.* files are never pruned). The Windows +# survive; in-flight .tmp.* files are never pruned — aged temp ORPHANS are +# reclaimed by the trap + age-filtered sweep instead). The Windows # locked-target rename failure is retry-then-skip, driven deterministically # by a PATH `mv` shim. # @@ -347,7 +348,10 @@ else fail "far-future snapshot wedged the tee: $(jq -c . <"$CTXDIR16B/sess-42.json")" fi -# --- Case 17: pruning — old siblings die, idle + recent + tmp survive -------- +# --- Case 17: pruning — old siblings die, idle + recent + live tmp survive --- +# Aged temp ORPHANS are the sweep's job, not the prune's: a 15-day-old temp is +# reclaimed (a SIGKILL/crash leftover no trap could catch), while a fresh +# in-flight temp — inside the sweep's one-minute age floor — is never touched. HOME17="$WORK/home17" CTXDIR17="$HOME17/$CTX_REL" mkdir -p "$CTXDIR17" @@ -359,13 +363,39 @@ printf '{"session_id":"recent"}\n' >"$CTXDIR17/recent.json" touch -d '5 minutes ago' "$CTXDIR17/recent.json" printf 'torn' >"$CTXDIR17/.ctx.json.tmp.99.123" touch -d '15 days ago' "$CTXDIR17/.ctx.json.tmp.99.123" +printf 'live' >"$CTXDIR17/.live.json.tmp.44.555" run "$HOME17" "$(build_input)" cat >/dev/null if [[ ! -e "$CTXDIR17/ancient.json" ]]; then ok "prune: 15-day-old sibling removed"; else fail "prune: ancient sibling survived"; fi if [[ -f "$CTXDIR17/idle.json" ]]; then ok "prune: live-but-idle sibling (2h) survives"; else fail "prune: idle sibling deleted (cutoff too tight)"; fi if [[ -f "$CTXDIR17/recent.json" ]]; then ok "prune: recent sibling survives"; else fail "prune: recent sibling deleted"; fi -if [[ -f "$CTXDIR17/.ctx.json.tmp.99.123" ]]; then ok "prune: in-flight .tmp.* never pruned"; else fail "prune: tmp file deleted"; fi +if [[ ! -e "$CTXDIR17/.ctx.json.tmp.99.123" ]]; then ok "sweep: aged temp orphan reclaimed"; else fail "sweep: aged temp orphan survived"; fi +if [[ -f "$CTXDIR17/.live.json.tmp.44.555" ]]; then ok "sweep: live in-flight temp never touched"; else fail "sweep: live temp deleted"; fi if [[ -f "$CTXDIR17/sess-42.json" ]]; then ok "prune pass still wrote the live snapshot"; else fail "live snapshot missing after prune"; fi +# --- Case 18: cancellation mid-window leaves no temp behind ------------------ +# Claude Code "cancels the in-flight script" when a new update arrives while +# this one is still running, so a kill between the write and the rename is +# routine rather than exceptional. Driven by an `mv` shim that parks, so the +# signal lands inside the window deterministically. +HOME18="$WORK/home18" +mkdir -p "$HOME18" +SHIM18="$WORK/shim18" +mkdir -p "$SHIM18" +printf '#!/usr/bin/env bash\nsleep 10\n' >"$SHIM18/mv" +chmod +x "$SHIM18/mv" +printf '%s' "$(build_input)" | HOME="$HOME18" PATH="$SHIM18:$PATH" bash "$TEE" >/dev/null 2>&1 & +TEE_PID=$! +sleep 2 +kill -TERM "$TEE_PID" 2>/dev/null +wait "$TEE_PID" 2>/dev/null +sleep 0.5 +LEFT18=$(find "$HOME18/$CTX_REL" -name '.*.json.tmp.*' 2>/dev/null | wc -l | tr -d ' \r') +if [[ "$LEFT18" == "0" ]]; then + ok "cancelled mid-window → temp reclaimed by trap" +else + fail "$LEFT18 temp file(s) leaked on cancellation" +fi + echo echo "PASS=$PASS FAIL=$FAIL" [[ $FAIL -eq 0 ]] diff --git a/plugins/discovery/.claude-plugin/plugin.json b/plugins/discovery/.claude-plugin/plugin.json index 11e9194b5..63971a19d 100644 --- a/plugins/discovery/.claude-plugin/plugin.json +++ b/plugins/discovery/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "discovery", - "version": "0.16.17", + "version": "0.16.18", "description": "Structured discovery before changes: explore the local codebase, run disciplined multi-source external research, and reconstruct why a past decision was made from evidence outside the code — each dispatching a purpose-built subagent by default so the reading stays out of the main conversation, with source tiers, falsification, recency gates, an intent-evidence tier, and a corpus-coverage ledger — persisting EXPLORE.md / RESEARCH.md / INTENT.md index-plus-sidecar handoff artifacts.", "author": { "name": "Melodic Software", diff --git a/plugins/discovery/CHANGELOG.md b/plugins/discovery/CHANGELOG.md index a442890fd..2d4526c33 100644 --- a/plugins/discovery/CHANGELOG.md +++ b/plugins/discovery/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog — discovery plugin +## [0.16.18] + +### Fixed + +- **`check-coverage-complete.test.sh`: the unreadable-ledger case now skips visibly as root + instead of failing.** chmod 000 does not stop root from reading a file, so the fail-closed + assertion could never hold when the suite ran as root. The case now probes readability after the + chmod and prints a SKIP line when the platform or user does not enforce it, mirroring the + established pattern in claude-config's `check-structure.test.sh`; on non-root hosts the case + still runs and asserts exit 2 exactly as before. + ## [0.16.17] ### Fixed diff --git a/plugins/discovery/scripts/check-coverage-complete.test.sh b/plugins/discovery/scripts/check-coverage-complete.test.sh index b99c10563..108c6e5ae 100755 --- a/plugins/discovery/scripts/check-coverage-complete.test.sh +++ b/plugins/discovery/scripts/check-coverage-complete.test.sh @@ -256,8 +256,12 @@ unreadable="$WORK/unreadable.md" printf '|---|-------------|-----------------|------|\n' printf '| 1 | alpha | criterion | [x] |\n' } >"$unreadable" -chmod 000 "$unreadable" -run_one "$PY_SUT" 2 "unreadable ledger fails closed as ungradeable" "$unreadable" +chmod 000 "$unreadable" 2>/dev/null +if [[ -r "$unreadable" ]]; then + echo "SKIP: unreadable-ledger case — this platform/user does not enforce chmod 000 on the fixture" >&2 +else + run_one "$PY_SUT" 2 "unreadable ledger fails closed as ungradeable" "$unreadable" +fi chmod 644 "$unreadable" 2>/dev/null || true # --- usage ------------------------------------------------------------------ diff --git a/plugins/guardrails/.claude-plugin/plugin.json b/plugins/guardrails/.claude-plugin/plugin.json index 57be89165..d0d6fe34c 100644 --- a/plugins/guardrails/.claude-plugin/plugin.json +++ b/plugins/guardrails/.claude-plugin/plugin.json @@ -147,5 +147,5 @@ "min": 1 } }, - "version": "0.29.23" + "version": "0.29.24" } diff --git a/plugins/guardrails/CHANGELOG.md b/plugins/guardrails/CHANGELOG.md index 8f6603b4f..9424a4a0a 100644 --- a/plugins/guardrails/CHANGELOG.md +++ b/plugins/guardrails/CHANGELOG.md @@ -3,6 +3,22 @@ All notable changes to the `guardrails` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. +## [0.29.24] + +### Fixed + +- **`verify-cli-flag.sh` streams help output to grep instead of staging it through a here-string.** + A here-string makes bash write the whole document before the reader ever runs: strace shows the + forked child create the pipe, write the entire payload, then exec grep, so large `--help` output + rode on bash's pipe-capacity probe and a TMPDIR temp-file fallback. Both sites now use + `printf '%s\n' ... | grep`, where writer and reader run concurrently under kernel flow control + with no temp file. A ten-case differential harness (including a 157KB synthetic help text) + proved stdout, stderr, and exit codes byte-identical to the old code; the 52-check suite passes. +- **`require-jq-notice-isolation.test.sh` no longer produces a two-line count on its zero-match + path.** `grep -c ... || echo 0` appends a second 0 after grep's own 0 when nothing matches, + which then broke the arithmetic comparison that consumed the value. The fallback is now + `|| true`, the repo's standard idiom for keeping grep -c's own count. + ## [0.29.23] ### Changed diff --git a/plugins/guardrails/hooks/require-jq-notice-isolation.test.sh b/plugins/guardrails/hooks/require-jq-notice-isolation.test.sh index 8aed30a92..97cee95a3 100755 --- a/plugins/guardrails/hooks/require-jq-notice-isolation.test.sh +++ b/plugins/guardrails/hooks/require-jq-notice-isolation.test.sh @@ -99,7 +99,9 @@ fired_count=0 fi done ) >"$TEST_TMPDIR/fire-log" -fired_count=$(grep -c '^FIRED:' "$TEST_TMPDIR/fire-log" 2>/dev/null || echo 0) +# || true, not || echo 0: grep -c already prints its own 0 on zero matches +# before exiting 1, so appending an echo made the captured value two lines. +fired_count=$(grep -c '^FIRED:' "$TEST_TMPDIR/fire-log" 2>/dev/null || true) if ((fired_count == ${#seen_keys[@]})); then ok "hook::notice_once fired independently for all ${#seen_keys[@]} guardrails hook keys within one shared session/data-dir" diff --git a/plugins/guardrails/lib/verification/verify-cli-flag.sh b/plugins/guardrails/lib/verification/verify-cli-flag.sh index b100f1c04..929bf8c28 100755 --- a/plugins/guardrails/lib/verification/verify-cli-flag.sh +++ b/plugins/guardrails/lib/verification/verify-cli-flag.sh @@ -154,9 +154,15 @@ fi # `(^|[^a-zA-Z0-9_-])` plus this trailing terminator prevent a prefix false # match (e.g. searching `--save-dev` must not match `--save-developer`). FLAG_PATTERN="(^|[^a-zA-Z0-9_-])${FLAG_NAME}([][:space:]=,|)[]|\$)" -if grep -E "$FLAG_PATTERN" <<<"$HELP_OUTPUT" >/dev/null; then +# Pipeline, not a here-string: bash materializes a here-string through a pipe +# it fills BEFORE exec'ing the reader (payloads over the pipe size fall back to +# a TMPDIR temp file), so a large --help payload rides on bash's runtime +# pipe-capacity probe and a writable TMPDIR. printf is a builtin and grep reads +# concurrently, so this streams any payload size with no such window. Feeds the +# same bytes a here-string would (content plus trailing newline). +if printf '%s\n' "$HELP_OUTPUT" | grep -E "$FLAG_PATTERN" >/dev/null; then if $VERBOSE; then - grep -nE "$FLAG_PATTERN" <<<"$HELP_OUTPUT" | head -1 + printf '%s\n' "$HELP_OUTPUT" | grep -nE "$FLAG_PATTERN" | head -1 fi exit 0 fi diff --git a/plugins/instruction-placement/.claude-plugin/plugin.json b/plugins/instruction-placement/.claude-plugin/plugin.json index 27c2a9d15..e3979a489 100644 --- a/plugins/instruction-placement/.claude-plugin/plugin.json +++ b/plugins/instruction-placement/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "instruction-placement", - "version": "0.11.13", + "version": "0.11.14", "description": "Routes agent-instruction content to the surface that loads it at the right moment. The audit skill sweeps a repository's instruction layer and its ordinary markdown for content whose scope is narrower than the surface carrying it \u2014 conventions keyed to one file type or one subtree sitting in an always-loaded CLAUDE.md or AGENTS.md \u2014 and for normative conventions stranded in documentation Claude never loads at all, then classifies each against a routing rubric and proposes a destination whose `paths:` glob is machine-validated before it is ever offered. Safety-class content (irreversible actions, secrets, data integrity, external publication, compliance, agent authority) is hard-denied from demotion and reported as held back rather than proposed, because demotion trades guaranteed presence for conditional presence and deferred surfaces are invisible inside subagents and absent after compaction until re-triggered. Every accepted move regenerates an always-loaded index of deferred surfaces, which is what keeps a demoted rule reachable from a subagent that never receives its injection. The audit is read-only and emits a diffable findings artifact; realignment is a separate skill gated per item with no blanket-approve path; a deterministic check skill gates that every rule glob still resolves and the index is current; and a setup skill verifies the one thing no other gate can see \u2014 that the index target is a file Claude Code will actually read, since it reads CLAUDE.md and not AGENTS.md.", "author": { "name": "Melodic Software", diff --git a/plugins/instruction-placement/CHANGELOG.md b/plugins/instruction-placement/CHANGELOG.md index 5d98d3c4d..05b1b0b8e 100644 --- a/plugins/instruction-placement/CHANGELOG.md +++ b/plugins/instruction-placement/CHANGELOG.md @@ -3,6 +3,18 @@ All notable changes to the `instruction-placement` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. +## [0.11.14] + +### Fixed + +- **`render-index.sh` no longer corrupts brace globs in the rendered rule index.** The glob-list + prettifier joined paths with `paste -sd,` and then padded every comma via + `sed 's/,/, /g'`, which also padded commas inside brace expressions: `src/*.{ts,tsx}` rendered + as `src/*.{ts, tsx}`. The join now inserts the `", "` separator directly in awk, so only + join-inserted separators are padded and brace commas are never touched. Reproduced against a + fixture rule before the fix and pinned by two new suite cases (one asserting the correct + rendering, one asserting the corrupted form is absent); the suite grows from 61 to 63 cases. + ## [0.11.13] ### Changed diff --git a/plugins/instruction-placement/scripts/render-index.sh b/plugins/instruction-placement/scripts/render-index.sh index 3145255f3..0ebb20549 100755 --- a/plugins/instruction-placement/scripts/render-index.sh +++ b/plugins/instruction-placement/scripts/render-index.sh @@ -129,8 +129,13 @@ rule_title() { # A private copy here would risk the brace-comma bug: splitting the inline flow # form `["src/*.{ts,tsx}"]` on the brace comma turns one correct glob into two # broken ones. +# +# The join inserts ", " between globs in one pass. The earlier form — `paste +# -sd,` then a global `s/,/, /` pad — hit the same brace-comma bug on output, +# rendering `{ts,tsx}` as `{ts, tsx}`; only the commas the join itself inserts +# may be padded. (`paste -sd', '` cannot do it: -d cycles single characters.) rule_globs() { - ip_parse_paths "$1" | paste -sd, - | sed 's/,/, /g' + ip_parse_paths "$1" | awk 'NR > 1 { printf ", " } { printf "%s", $0 } END { if (NR) printf "\n" }' } # True when every non-blank, non-comment line is an `@import`. Such a file is a diff --git a/plugins/instruction-placement/scripts/render-index.test.sh b/plugins/instruction-placement/scripts/render-index.test.sh index f1fc6fc50..c0c671a4f 100755 --- a/plugins/instruction-placement/scripts/render-index.test.sh +++ b/plugins/instruction-placement/scripts/render-index.test.sh @@ -63,6 +63,16 @@ paths: ["**/*.test.ts"] # Tests +Body. +EOF + + cat >"$dir/.claude/rules/brace.md" <<'EOF' +--- +paths: ["src/*.{ts,tsx}", "lib/**"] +--- + +# Brace-glob rule + Body. EOF @@ -127,6 +137,11 @@ assert_contains "the H1 supplies the topic when no description exists" "$out" "C assert_contains "an explicit description wins over the H1" "$out" "Test layout and the runner to use" assert_contains "a rule in a nested rules directory is indexed" "$out" '`.claude/rules/nested/api.md`' +# The glob join pads only the commas it inserts between globs, never a comma +# inside a brace glob (regression: a global comma pad rendered `{ts, tsx}`). +assert_contains "brace-glob commas are not padded, list commas are" "$out" '`src/*.{ts,tsx}, lib/**`' +assert_not_contains "no space is injected inside a brace glob" "$out" '{ts, tsx}' + assert_not_contains "an unscoped rule is NOT indexed" "$out" "always.md" assert_not_contains "the root CLAUDE.md is NOT indexed" "$out" '| `CLAUDE.md`' assert_not_contains "the root AGENTS.md is NOT indexed" "$out" '| `AGENTS.md`' diff --git a/plugins/knowledge/.claude-plugin/plugin.json b/plugins/knowledge/.claude-plugin/plugin.json index 421b930ab..15b22a5bd 100644 --- a/plugins/knowledge/.claude-plugin/plugin.json +++ b/plugins/knowledge/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "knowledge", - "version": "0.13.28", + "version": "0.13.29", "description": "Ingest external knowledge into durable, synthesized artifacts. Ships a book-distillation pipeline (PDF/EPUB into concept-organized, author-attributed skill reference files), a video-digest pipeline (watch a single public video from YouTube or X, formerly Twitter: transcript, link harvest, and repo-applicability synthesis), a course-digest pipeline (extract and synthesize online video courses \u2014 Dometrain, Teachable \u2014 into repo-applicable recommendations), a docpage-digest pipeline (single online documentation page into a verified knowledge slice with dual verification \u2014 one cross-vendor verifier \u2014 and an interview handoff), and a map-corpus pipeline (multi-resource corpus into a classified link map, deterministic node manifests, gate-verified relevance inventory, and an approved queue of docpage-digest runs), plus a re-runnable setup action; a configurable library directory governs where synthesized artifacts land in the consuming repo.", "author": { "name": "Melodic Software", diff --git a/plugins/knowledge/CHANGELOG.md b/plugins/knowledge/CHANGELOG.md index be1388b1e..5bee2bcff 100644 --- a/plugins/knowledge/CHANGELOG.md +++ b/plugins/knowledge/CHANGELOG.md @@ -4,6 +4,18 @@ All notable changes to the `knowledge` plugin are recorded here. The `version` i `.claude-plugin/plugin.json` is the delivery vehicle — a consumer receives a change only after that version increases. +## [0.13.29] + +### Changed + +- **video-digest watch: orphaned module removed.** `watch/promotion-name-map.js` + is deleted. 0.13.28 removed its last importer (`synthesis-naming.js`'s dead + `synthesisDestNameForSlice` export) and confirmed the module carries no + load-time side effects; a fresh repo-wide sweep re-proved zero importers of + the file or of `loadPromotionNameMap` (only historical changelog text and one + now-corrected doc parenthetical in `context/quality-gates.md` named it). It + had no test file of its own. Watch suites and `tsc` clean after removal. + ## [0.13.28] ### Changed diff --git a/plugins/knowledge/skills/video-digest/context/quality-gates.md b/plugins/knowledge/skills/video-digest/context/quality-gates.md index 82c82ca3a..7369af1a9 100644 --- a/plugins/knowledge/skills/video-digest/context/quality-gates.md +++ b/plugins/knowledge/skills/video-digest/context/quality-gates.md @@ -124,7 +124,7 @@ Per cell in contact sheet (`frame-triage-checklist.json` `verdicts`): **JSON SSOT:** subagents write `key-frames/triage/batches/sheet_NNN.json`; merge to `key-frames/triage/manifest.json`; render `key-frames/frame-triage-log.md` via `render-triage-log.js`. Do not treat markdown-only triage as complete. -**Promotion SSOT:** `key-frames/promotion-decisions.json` → `vision-gated-promote.js` → `promotion-map.json` (`promotion-name-map.js` loader). **Triage SSOT:** `key-frames/triage/batches/sheet_NNN.json` → `merge-triage-json.js` → `render-triage-log.js`. No signal-derived or bulk-promote shortcuts — verify scripts enforce agentic triage and vision-gated promotion. +**Promotion SSOT:** `key-frames/promotion-decisions.json` → `vision-gated-promote.js` → `promotion-map.json`. **Triage SSOT:** `key-frames/triage/batches/sheet_NNN.json` → `merge-triage-json.js` → `render-triage-log.js`. No signal-derived or bulk-promote shortcuts — verify scripts enforce agentic triage and vision-gated promotion. ## High-volume fan-out diff --git a/plugins/knowledge/skills/video-digest/extraction/watch/promotion-name-map.js b/plugins/knowledge/skills/video-digest/extraction/watch/promotion-name-map.js deleted file mode 100644 index 2687579eb..000000000 --- a/plugins/knowledge/skills/video-digest/extraction/watch/promotion-name-map.js +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Slice SSOT: source file → synthesis destination from promotion-decisions.json. - */ - -import fs from "node:fs"; -import path from "node:path"; - -import { LANES, lanePath } from "../lib/slice-lanes.js"; - -/** - * @param {string} sliceDir - * @returns {Record} - */ -export function loadPromotionNameMap(sliceDir) { - const decisionsPath = lanePath( - path.resolve(sliceDir), - LANES.keyFrames, - "promotion-decisions.json", - ); - if (!fs.existsSync(decisionsPath)) { - return {}; - } - const doc = JSON.parse(fs.readFileSync(decisionsPath, "utf8")); - /** @type {Record} */ - const map = {}; - for (const row of doc.decisions ?? []) { - if (row.verdict !== "promote" || !row.destName) continue; - const dest = row.destName.endsWith(".png") ? row.destName : `${row.destName}.png`; - map[row.sourceFile] = dest; - } - return map; -} diff --git a/plugins/machine-health/.claude-plugin/plugin.json b/plugins/machine-health/.claude-plugin/plugin.json index 5a1857cde..ae5eded00 100644 --- a/plugins/machine-health/.claude-plugin/plugin.json +++ b/plugins/machine-health/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "machine-health", - "version": "0.11.17", + "version": "0.11.18", "description": "Workstation health audit: OS-specific checks (disk, OS updates, security posture, CISA KEV correlation) run from a versioned catalog with trend-aware severity, approval-gated remediations, and dated markdown reports. Windows fully implemented; macOS/Linux scaffolded (report UNKNOWN and stop). Machine state persists in the plugin data directory; the report directory and check catalog are configurable.", "author": { "name": "Melodic Software", diff --git a/plugins/machine-health/CHANGELOG.md b/plugins/machine-health/CHANGELOG.md index 1d931366b..be390e38a 100644 --- a/plugins/machine-health/CHANGELOG.md +++ b/plugins/machine-health/CHANGELOG.md @@ -3,6 +3,32 @@ All notable changes to the `machine-health` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. +## [0.11.18] + +### Fixed + +- **`audit`: `correlation-rules.md` names the correlation loader by its real name.** The doc said + `Get-CorrelationRules`; the function `Invoke-FindingCorrelation.ps1` declares and calls is + `Get-CorrelationRule` (singular). One-word doc fix, code authoritative. +- **`audit`: `Write-ElevationBanner.Tests.ps1` now actually captures the banner's output.** The + banner writes through `[Console]::Error.WriteLine`, which bypasses PowerShell's error stream, so + the old `2>&1` capture was always empty and the two "emits nothing" assertions could never + fail. The suite now swaps `[Console]::Error` for a StringWriter (restored in `finally`), asserts + the negative cases on the real capture, and adds a positive control proving the capture path + sees the non-elevated banner. Suite: 5 vacuous checks before, 6 real ones after. +- **`audit`: `Restart-StoppedService.Tests.ps1` runs on Linux.** Pester cannot mock a command that + does not exist, and Linux pwsh ships neither `Get-Service` nor `Start-Service`, so all 10 cases + errored with CommandNotFoundException. BeforeAll now defines empty stub functions for both, the + established pattern `Test-WindowsUpdate.Tests.ps1` already uses. The stubs shadow the real + cmdlets on Windows too, so Pester builds its mocks from the param-less stubs everywhere; every + case mocks both commands, the stub bodies never run, and all 10 cases pass identically on both + platforms. 0 passing on Linux before, 10 after. +- **`audit`: `Scaffold.Tests.ps1` no longer fails on hosts with empty USERNAME/COMPUTERNAME.** The + three redaction-fixture cases build payloads from those variables, which Linux leaves unset. + BeforeAll now pins placeholder values only when the variables are empty and AfterAll restores + the saved values, leaving the redaction helper and real Windows values untouched. 17 passing + plus 3 environment failures before, all 20 after. + ## [0.11.17] ### Changed diff --git a/plugins/machine-health/skills/audit/references/shared/correlation-rules.md b/plugins/machine-health/skills/audit/references/shared/correlation-rules.md index 887fac757..1639725f9 100644 --- a/plugins/machine-health/skills/audit/references/shared/correlation-rules.md +++ b/plugins/machine-health/skills/audit/references/shared/correlation-rules.md @@ -10,7 +10,7 @@ applies rules after check dispatch, before severity aggregation. ## Rule structure -Each rule is a PowerShell hashtable in `Get-CorrelationRules`: +Each rule is a PowerShell hashtable in `Get-CorrelationRule`: ```powershell @{ diff --git a/plugins/machine-health/skills/audit/tests/windows/Scaffold.Tests.ps1 b/plugins/machine-health/skills/audit/tests/windows/Scaffold.Tests.ps1 index b1302dae5..c9ba41ff8 100644 --- a/plugins/machine-health/skills/audit/tests/windows/Scaffold.Tests.ps1 +++ b/plugins/machine-health/skills/audit/tests/windows/Scaffold.Tests.ps1 @@ -16,6 +16,21 @@ BeforeAll { . (Join-Path $script:LibRoot 'Assert-CheckResult.ps1') . (Join-Path $script:TestsRoot 'helpers\Invoke-FixtureRedaction.ps1') Import-Module (Join-Path $script:TestsRoot 'helpers\Mock-Helpers.psm1') -Force + + # USERNAME/COMPUTERNAME are usually unset on non-Windows hosts. The + # redaction tests build their payloads from these variables and the + # helper reads the same ones, so pin stable values when empty (and + # restore after) instead of changing the helper. Windows keeps its + # real values. + $script:SavedUserName = $env:USERNAME + $script:SavedComputerName = $env:COMPUTERNAME + if (-not $env:USERNAME) { $env:USERNAME = 'scaffolduser' } + if (-not $env:COMPUTERNAME) { $env:COMPUTERNAME = 'SCAFFOLDHOST' } +} + +AfterAll { + $env:USERNAME = $script:SavedUserName + $env:COMPUTERNAME = $script:SavedComputerName } Describe 'Scaffold -- Assert-CheckResult' { diff --git a/plugins/machine-health/skills/audit/tests/windows/lib/Write-ElevationBanner.Tests.ps1 b/plugins/machine-health/skills/audit/tests/windows/lib/Write-ElevationBanner.Tests.ps1 index 41bbcc29d..63b2947a2 100644 --- a/plugins/machine-health/skills/audit/tests/windows/lib/Write-ElevationBanner.Tests.ps1 +++ b/plugins/machine-health/skills/audit/tests/windows/lib/Write-ElevationBanner.Tests.ps1 @@ -10,26 +10,52 @@ BeforeAll { $script:LibRoot = Join-Path (Split-Path -Parent $script:TestsRoot) 'scripts\windows\lib' . (Join-Path $script:LibRoot 'Write-ElevationBanner.ps1') . (Join-Path $script:LibRoot 'Get-ElevationMatrix.ps1') + + # Write-ElevationBanner writes via [Console]::Error.WriteLine, which + # bypasses PowerShell's error stream, so `2>&1` captures nothing. + # Swap in a StringWriter around the call to capture real stderr text. + function Invoke-BannerCapture { + param([Parameter(Mandatory)] [scriptblock] $Call) + $writer = [System.IO.StringWriter]::new() + $saved = [Console]::Error + try { + [Console]::SetError($writer) + & $Call + } finally { + [Console]::SetError($saved) + } + return $writer.ToString() + } } Describe 'Write-ElevationBanner' -Tag 'lib' { + It 'emits the banner to stderr when non-elevated' { + $err = Invoke-BannerCapture { + Write-ElevationBanner -Elevated $false -HostName 'HOST' ` + -UserName 'DOMAIN\user' -OutputBase 'C:\out' ` + -SkillRoot 'C:\skill' -Matrix @(Get-ElevationMatrix) + } + $err | Should -Match 'NON-ELEVATED' + $err | Should -Match ([regex]::Escape('Running as DOMAIN\user')) + $err | Should -Match 'Suppress this banner with -SkipBanner' + } + It 'emits nothing when elevated' { - $err = & { + $err = Invoke-BannerCapture { Write-ElevationBanner -Elevated $true -HostName 'HOST' ` -UserName 'DOMAIN\user' -OutputBase 'C:\out' ` -SkillRoot 'C:\skill' -Matrix @(Get-ElevationMatrix) - } 2>&1 - # Pester captures stderr. Expect no banner output. - @($err | Where-Object { $_ -match '=' }).Count | Should -Be 0 + } + $err | Should -BeNullOrEmpty } It 'emits nothing when -Quiet even if non-elevated' { - $err = & { + $err = Invoke-BannerCapture { Write-ElevationBanner -Elevated $false -HostName 'HOST' ` -UserName 'DOMAIN\user' -OutputBase 'C:\out' ` -SkillRoot 'C:\skill' -Matrix @(Get-ElevationMatrix) -Quiet - } 2>&1 - @($err | Where-Object { $_ -match 'NON-ELEVATED' }).Count | Should -Be 0 + } + $err | Should -BeNullOrEmpty } } diff --git a/plugins/machine-health/skills/audit/tests/windows/remediations/Restart-StoppedService.Tests.ps1 b/plugins/machine-health/skills/audit/tests/windows/remediations/Restart-StoppedService.Tests.ps1 index 518819527..f45deff4c 100644 --- a/plugins/machine-health/skills/audit/tests/windows/remediations/Restart-StoppedService.Tests.ps1 +++ b/plugins/machine-health/skills/audit/tests/windows/remediations/Restart-StoppedService.Tests.ps1 @@ -25,6 +25,14 @@ BeforeAll { $script:SkillRoot = Split-Path -Parent $script:TestsRoot $script:ScriptPath = Join-Path $script:SkillRoot 'scripts\windows\remediations\Restart-StoppedService.ps1' + # Get-Service/Start-Service are Windows-only cmdlets, absent in Linux + # pwsh, and Pester cannot mock a nonexistent command. Define stubs so + # Mock can attach; the remediation resolves them from this (parent) + # scope when invoked via `& $ScriptPath`. Every test mocks both, so + # the stub bodies never run. + function Get-Service { } + function Start-Service { } + function Invoke-RestartAsObject { param( [string]$ServiceName, diff --git a/plugins/rate-limit-guard/.claude-plugin/plugin.json b/plugins/rate-limit-guard/.claude-plugin/plugin.json index 7f19c335f..71d7d1233 100644 --- a/plugins/rate-limit-guard/.claude-plugin/plugin.json +++ b/plugins/rate-limit-guard/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "rate-limit-guard", - "version": "0.7.20", + "version": "0.7.21", "description": "Shared rate-limit guard for loop lanes: a statusline wrapper tees the subscription rate-limit windows to a fixed machine-scope file, a StopFailure hook records rate-limit stops reactively, and a reader contract fixes how consuming sessions pause and resume.", "author": { "name": "Melodic Software", diff --git a/plugins/rate-limit-guard/CHANGELOG.md b/plugins/rate-limit-guard/CHANGELOG.md index 2d0e7e3d1..c802deea5 100644 --- a/plugins/rate-limit-guard/CHANGELOG.md +++ b/plugins/rate-limit-guard/CHANGELOG.md @@ -3,6 +3,26 @@ All notable changes to the `rate-limit-guard` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. +## [0.7.21] + +### Fixed + +- **`statusline-tee.sh` passes payloads over 1MiB through to the wrapped statusline intact.** The + stdin reader was a single bounded `read -N 1048576`, so anything past the first 1MiB never + reached the wrapped command. Ported the sibling context-guard tee's builtin-only + read-until-EOF loop, keeping the documented stalled-pipe timeout boundary; a new suite case + proves a 1.5MB payload reaches the wrapped command byte-for-byte and is still teed. Zero new + processes: the suite's zero-fork xtrace assertions still pass and the bench spawn floor is + unchanged. + +### Changed + +- **`statusline-tee.sh` hardens its snapshot temp write.** The temp name gains a second `$RANDOM` + of entropy and the write happens under `set -o noclobber` inside the existing umask subshell, + so a pre-planted symlink at the temp path is refused instead of followed. The chmod-700 + directory, trap reclaim, and mv-retry machinery are unchanged. Ported from the context-guard + sibling. Suite grows from 96 to 98 checks, all passing. + ## [0.7.20] ### Changed diff --git a/plugins/rate-limit-guard/scripts/statusline-tee.sh b/plugins/rate-limit-guard/scripts/statusline-tee.sh index e6c813c4c..9af22c40d 100755 --- a/plugins/rate-limit-guard/scripts/statusline-tee.sh +++ b/plugins/rate-limit-guard/scripts/statusline-tee.sh @@ -125,20 +125,30 @@ _rlg_bash_at_least() { ((BASH_VERSINFO[0] > $1 || (BASH_VERSINFO[0] == $1 && BASH_VERSINFO[1] >= $2))) } -# Bounded buffered read of the whole stdin payload (Win32 pipes can stall -# before EOF; a truncated payload just fails jq below and tees nothing this -# refresh). read -N buffers in blocks, which matters on Windows/MSYS pipes -# where the -d '' byte-at-a-time loop moves ~40KB/s and can truncate a large -# payload at the timeout (measured on Git Bash); Bash below 4.1 (macOS ships -# 3.2) lacks -N and falls back to the delimiter form, fast enough on native -# POSIX pipes. 1MiB bound: statusline payloads are a few KB. +# Time-bounded buffered read of the whole stdin payload (Win32 pipes can +# stall before EOF; a truncated payload just fails jq below and tees nothing +# this refresh). read -N buffers in 1MiB blocks and the loop drains until +# EOF, so the wrapped command receives EVERY byte regardless of payload size +# — the block size matters on Windows/MSYS pipes where the -d '' byte-at-a- +# time loop moves ~40KB/s (measured on Git Bash), and the per-block -t 5 +# timeout bounds a stalled pipe without capping a large healthy payload. +# Bash below 4.1 (macOS ships 3.2) lacks -N and falls back to the delimiter +# form, which already reads to EOF, fast enough on native POSIX pipes. +# Documented boundary: on a stalled pipe (timeout mid-payload) the wrapped +# command receives only the drained bytes and fails with its own exit code; +# the tee silently skips that refresh (jq rejects the truncated JSON). # # Deferred into a function rather than run at load: this file carries a sourcing # guard at the bottom so its enablement gate can be driven by the test suite, and # a top-level read would consume the sourcing shell's stdin before `main` runs. read_tee_input() { if _rlg_bash_at_least 4 1; then - IFS= read -r -N 1048576 -t 5 INPUT || true + local _chunk="" + while IFS= read -r -N 1048576 -t 5 _chunk; do + INPUT+="$_chunk" + _chunk="" + done + INPUT+="$_chunk" # EOF/timeout leaves the final partial block in _chunk else IFS= read -r -d '' -t 5 INPUT || true fi @@ -290,12 +300,16 @@ tee_snapshot() { fi fi - local tmp="$dir/.rate-limits.json.tmp.$$.$RANDOM" + # Process-unique temp name with widened entropy; noclobber makes the + # redirection refuse to follow a pre-planted symlink or overwrite any + # pre-existing path of the same name. + local tmp="$dir/.rate-limits.json.tmp.$$.$RANDOM$RANDOM" TEE_TMP="$tmp" # Subshell umask so the snapshot lands owner-only without altering the # umask the wrapped statusline command inherits. ( umask 077 + set -o noclobber printf '%s\n' "$payload" >"$tmp" ) 2>/dev/null || { reclaim_tee_tmp diff --git a/plugins/rate-limit-guard/scripts/statusline-tee.test.sh b/plugins/rate-limit-guard/scripts/statusline-tee.test.sh index 513ea229b..cda4ee048 100755 --- a/plugins/rate-limit-guard/scripts/statusline-tee.test.sh +++ b/plugins/rate-limit-guard/scripts/statusline-tee.test.sh @@ -247,6 +247,27 @@ else fail "oversized payload not teed" fi +# --- Case 12b: payload beyond one read chunk still passes through whole ------ +# The stdin read drains 1MiB blocks until EOF, so a payload larger than one +# block must reach the wrapped command byte-for-byte, not truncated at the +# first block boundary. +HOME12B="$WORK/home12b" +mkdir -p "$HOME12B" +HUGE_FILLER="$(head -c 1500000 /dev/zero | tr '\0' 'y')" +HUGE_INPUT="$(printf '{"session_id":"sess-huge","filler":"%s","rate_limits":{"five_hour":{"used_percentage":6,"resets_at":1738425600}}}' "$HUGE_FILLER")" +WANT_BYTES=${#HUGE_INPUT} +GOT_BYTES=$(printf '%s' "$HUGE_INPUT" | HOME="$HOME12B" bash "$TEE" wc -c | tr -d ' \r\n') +if [[ "$GOT_BYTES" == "$WANT_BYTES" ]]; then + ok ">1MiB payload → wrapped command receives every byte ($GOT_BYTES)" +else + fail ">1MiB payload truncated: wrapped command saw $GOT_BYTES of $WANT_BYTES bytes" +fi +if [[ -f "$HOME12B/$TEE_REL" ]] && [[ "$(jq -r '.session_id' <"$HOME12B/$TEE_REL")" == "sess-huge" ]]; then + ok ">1MiB payload still teed" +else + fail ">1MiB payload not teed" +fi + # --- Case 13: newest write wins (last-writer-wins contract) ------------------ run "$HOME1" "$(build_input)" cat >/dev/null printf '{"session_id":"sess-later","rate_limits":{"five_hour":{"used_percentage":91,"resets_at":1738425600}}}' | diff --git a/plugins/source-control/.claude-plugin/plugin.json b/plugins/source-control/.claude-plugin/plugin.json index a3895a2fc..e4ac293ad 100644 --- a/plugins/source-control/.claude-plugin/plugin.json +++ b/plugins/source-control/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "source-control", - "version": "0.55.32", + "version": "0.55.33", "description": "Git and GitHub delivery workflow: /commit (Conventional Commits + Co-authored-by trailer via safe heredoc mechanics), /pull-request (prep, create, CI monitoring, review-comment triage, merge, CI-log fetch), /babysit-prs (self-pacing fleet loop \u2014 safe by default; opt-in worker/autopilot tiers add gate-checked merge and thread resolution behind a deterministic Python engine), /babysit-loop (the loop-lane merge lane: a standing or drain loop that invokes babysit-prs per cycle, configured through repo-scoped babysit_loop_* keys on the layered source-control.md seam, with merge authority human-only until the target repo's tracked config adopts the lane, a gate-proven C2-mechanical baseline once adopted, and standing merge-rung raises binding from the team-tracked layer only \u2014 with one named exception, where an invocation line explicitly typing both the autopilot tier keyword and the dedicated raise argument --merge c3-this-run widens that single invocation's merge authority up to C3 behind a fresh independent frontier-tier resolver, while C4-structural and C5-untrusted-provenance stay unconditionally human-merge), /worktree (create, status, cleanup, audit for parallel-session isolation), /setup (check the effective commit-subject / PR-title convention merged across its config layers and the babysit-prs config, or apply \u2014 interview the repo and write the convention config to a chosen layer), and /resolve-conflicts (intent-first merge/rebase conflict resolution with a semantic-conflict sweep \u2014 never --abort). The commit-subject / PR-title convention is configurable via a source-control.md config written by a re-runnable setup skill, layered across a ~/.claude user-global file, the tracked team file, and a gitignored .claude/source-control.local.md personal overlay merged per key; Conventional Commits is the default when no convention is declared.", "author": { "name": "Melodic Software", diff --git a/plugins/source-control/CHANGELOG.md b/plugins/source-control/CHANGELOG.md index 21814d440..5958b1517 100644 --- a/plugins/source-control/CHANGELOG.md +++ b/plugins/source-control/CHANGELOG.md @@ -3,6 +3,20 @@ All notable changes to the `source-control` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. +## [0.55.33] + +### Fixed + +- **Three scripts stop truncating their own `--help` output.** `fetch-all-pr-comments.sh`, + `reap-project-plugin-records.sh`, and `worktree-root-doctor.sh` each printed usage with a + hardcoded `sed -n 'X,Yp'` line range that silently cut off whatever the header gained after the + range was written: fetch-all-pr-comments was omitting its entire Exit codes block, and + worktree-root-doctor was dropping 38 lines including its checked-classes list. All three now use + the derived header printer `babysit-readiness-gate.sh` already ships, which prints the comment + header to its first non-comment line, so the range can never go stale. Each --help diff is pure + addition (reap's output was still fully covered and is byte-identical), and each suite now pins + a formerly-truncated line so the truncation class is regression-guarded. + ## [0.55.32] ### Changed diff --git a/plugins/source-control/scripts/fetch-all-pr-comments.sh b/plugins/source-control/scripts/fetch-all-pr-comments.sh index d0b5f5703..5d595d78a 100755 --- a/plugins/source-control/scripts/fetch-all-pr-comments.sh +++ b/plugins/source-control/scripts/fetch-all-pr-comments.sh @@ -50,8 +50,12 @@ set -uo pipefail # -e omitted: gh api failures explicitly guarded with || { exit PR_NUMBER="" +# Print the header block (everything after the shebang up to the first +# non-comment line) with its comment markers stripped. Derived rather than a +# hardcoded line range, which silently truncated or over-ran as the header grew. usage() { - sed -n '2,39p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//' + awk 'NR == 1 { next } /^#/ { sub(/^# ?/, ""); print; next } { exit }' \ + "${BASH_SOURCE[0]}" exit 0 } diff --git a/plugins/source-control/scripts/fetch-all-pr-comments.test.sh b/plugins/source-control/scripts/fetch-all-pr-comments.test.sh index 274dc0bc3..d29afe6ec 100755 --- a/plugins/source-control/scripts/fetch-all-pr-comments.test.sh +++ b/plugins/source-control/scripts/fetch-all-pr-comments.test.sh @@ -254,6 +254,13 @@ help_out=$(bash "$SCRIPT" --help 2>&1) assert_contains "--help documents PYTHONUTF8=1 for Windows consumers" "$help_out" "PYTHONUTF8=1" assert_contains "--help names the UnicodeDecodeError failure mode" "$help_out" "UnicodeDecodeError" +# Case 12: usage() derives the header block rather than extracting a hardcoded +# line range, which silently dropped the Exit-codes block as the header grew. +# Pin a formerly-truncated line (the last header line) so the truncation class +# cannot come back unnoticed. +assert_contains "--help reaches the end of the header (Exit codes block)" \ + "$help_out" "prerequisite missing (gh, jq)" + # ---- Summary ---------------------------------------------------------------- if [[ "$FAILED" -eq 0 ]]; then diff --git a/plugins/source-control/scripts/reap-project-plugin-records.sh b/plugins/source-control/scripts/reap-project-plugin-records.sh index 034442f40..aa1779ded 100755 --- a/plugins/source-control/scripts/reap-project-plugin-records.sh +++ b/plugins/source-control/scripts/reap-project-plugin-records.sh @@ -58,8 +58,12 @@ PROG=${0##*/} have() { command -v "$1" >/dev/null 2>&1; } +# Print the header block (everything after the shebang up to the first +# non-comment line) with its comment markers stripped. Derived rather than a +# hardcoded line range, which silently truncated or over-ran as the header grew. usage() { - sed -n '2,53p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//' + awk 'NR == 1 { next } /^#/ { sub(/^# ?/, ""); print; next } { exit }' \ + "${BASH_SOURCE[0]}" } worktree_path="" diff --git a/plugins/source-control/scripts/reap-project-plugin-records.test.sh b/plugins/source-control/scripts/reap-project-plugin-records.test.sh index f29f0d5e3..50220a1e0 100755 --- a/plugins/source-control/scripts/reap-project-plugin-records.test.sh +++ b/plugins/source-control/scripts/reap-project-plugin-records.test.sh @@ -126,9 +126,13 @@ help_out="$(bash "$REAP" --help 2>&1)" help_rc=$? assert_exit "--help exits 0" 0 "$help_rc" assert_contains "--help documents the usage line" "$help_out" "--worktree-path" -# usage() extracts a hardcoded line range from this script's own header, so a -# header edit that does not move the range silently truncates the help. The -# usage line sits early enough to survive that; the last header line does not. +# usage() derives the header block (shebang to first non-comment line) rather +# than extracting a hardcoded line range, so a header edit can no longer +# silently truncate the help. The usage line sits early; the exit-code taxonomy +# and the last header line sit at the end, exactly where a reintroduced +# hardcoded range would truncate first. +assert_contains "--help includes the exit-code taxonomy near the header's end" \ + "$help_out" "prerequisite missing (claude CLI, jq)" assert_contains "--help reaches the end of the header, not a truncated range" \ "$help_out" "must never read as a clean one" diff --git a/plugins/source-control/scripts/worktree-root-doctor.sh b/plugins/source-control/scripts/worktree-root-doctor.sh index bf0cd9af0..6a90d85a2 100755 --- a/plugins/source-control/scripts/worktree-root-doctor.sh +++ b/plugins/source-control/scripts/worktree-root-doctor.sh @@ -73,7 +73,11 @@ while [[ $# -gt 0 ]]; do shift 2 ;; -h | --help) - sed -n '2,20p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//' + # The whole header (everything after the shebang up to the first + # non-comment line), markers stripped. Derived rather than a hardcoded + # line range, which silently truncated as the header grew. + awk 'NR == 1 { next } /^#/ { sub(/^# ?/, ""); print; next } { exit }' \ + "${BASH_SOURCE[0]}" exit 0 ;; *) diff --git a/plugins/source-control/scripts/worktree-root-doctor.test.sh b/plugins/source-control/scripts/worktree-root-doctor.test.sh index a9f1d87b9..63f3a33aa 100755 --- a/plugins/source-control/scripts/worktree-root-doctor.test.sh +++ b/plugins/source-control/scripts/worktree-root-doctor.test.sh @@ -59,6 +59,13 @@ fgit() { GIT_CONFIG_GLOBAL="$EMPTY_GCFG" GIT_CONFIG_NOSYSTEM=1 git "$@"; } help_out=$(bash "$DOCTOR" --help 2>&1) assert_exit "--help exits 0" 0 "$?" assert_contains "--help documents the exit taxonomy" "$help_out" "at least one warn/error" +# --help derives the whole header rather than a hardcoded line range, which +# used to cut off mid exit-codes list. Pin two formerly-truncated lines: the +# dropped exit code, and the last header line. +assert_contains "--help includes the usage-error exit code (formerly truncated)" \ + "$help_out" "2 usage error" +assert_contains "--help reaches the end of the header, not a truncated range" \ + "$help_out" "suffix shape IS flagged when a declared pattern carries it" bash "$DOCTOR" --frobnicate >/dev/null 2>&1 assert_exit "an unknown argument is a usage error (exit 2)" 2 "$?" diff --git a/plugins/work-items/.claude-plugin/plugin.json b/plugins/work-items/.claude-plugin/plugin.json index 7911f4ca4..27bca26f6 100644 --- a/plugins/work-items/.claude-plugin/plugin.json +++ b/plugins/work-items/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "work-items", - "version": "0.39.41", + "version": "0.39.42", "description": "Manages development work items through a provider-neutral tracker seam that ships with the plugin (bundled dispatcher plus github, local-markdown, jira, gitea, and linear adapters; seam plugin-dir canonical, adapters consumer-local-first): dashboard, taxonomy-labeled creation, a race-safe assignee-plus-lease claim protocol, recurring-schedule checks, TODO scanning, stale-lease auditing, plan decomposition into vertical-slice items, a macro-journey router over spec containers (rollup, per-container execution shape, next-step routing), raw-intake triage (issues and unsolicited PRs through raw, verified, briefed, autonomous-eligible states), plus the two work-items loop lanes of the loop-lane convention: a self-paced autonomous work-loop drain (work-class admission gate, adaptive item cap, PR-only) and an attended attend-queue escalation lane. The re-runnable setup skill binds the provider (.work-item-tracker.json), seeds the recurring-schedule seam (.github/recurring-schedule.json), and remaps canonical role labels.", "author": { "name": "Melodic Software", diff --git a/plugins/work-items/CHANGELOG.md b/plugins/work-items/CHANGELOG.md index 90bcb76be..0d176ba67 100644 --- a/plugins/work-items/CHANGELOG.md +++ b/plugins/work-items/CHANGELOG.md @@ -3,6 +3,29 @@ All notable changes to the `work-items` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. +## [0.39.42] + +### Fixed + +- **Conformance `e2e-probe.sh` drops a vacuous assertion.** The lease-comment-id case asserted + that `"n$LEASE_CID"` contains `"n"`, which is true for every possible value; the real numeric + check is the regex block immediately after it, which remains. Nothing pins the probe's case + numbering, and the offline suite passes unchanged. The probe itself runs only against a live + sandbox repo, so the offline suite is the verification. +- **Linear `schema-check/fidelity.sh` describes its own mechanism accurately.** The header comment + claimed the FIELDS block is pulled "by executing just that assignment"; the script sed-extracts + the text without executing anything. The comment now says so. +- **Linear `schema-check/negative.mjs` uses an optional catch binding** instead of binding an + unused exception variable. + +### Changed + +- **The linear `schema-check/` tree is now recorded in the repo's no-suite allowlist.** The tree + mapped to zero test suites, which the affected-tests contract treats as an error, so every + future edit there was blocked. It is an on-demand live-schema lane (fetched SDL plus a graphql + dependency, run by hand per its README, no CI lane), and the allowlist entry records that + justification. + ## [0.39.41] ### Changed diff --git a/plugins/work-items/tools/work-item-tracker/adapters/linear/schema-check/fidelity.sh b/plugins/work-items/tools/work-item-tracker/adapters/linear/schema-check/fidelity.sh index a1d50a5c1..fb189368a 100755 --- a/plugins/work-items/tools/work-item-tracker/adapters/linear/schema-check/fidelity.sh +++ b/plugins/work-items/tools/work-item-tracker/adapters/linear/schema-check/fidelity.sh @@ -10,7 +10,9 @@ C="$(cd "$HERE/../../../conformance/bindings" && pwd)" norm() { tr '\n' ' ' | tr -s ' ' | sed 's/^ //; s/ $//'; } -# Pull the real WIT_LINEAR_ISSUE_FIELDS out of common.sh by executing just that assignment. +# Pull the real WIT_LINEAR_ISSUE_FIELDS out of common.sh by sed-extracting the text of its +# single-quoted assignment (nothing is executed): the readonly...= prefix and the surrounding +# quotes are stripped, leaving the literal field-selection block. FIELDS="$(sed -n "/^readonly WIT_LINEAR_ISSUE_FIELDS='/,/^'$/p" "$A/common.sh" | sed "s/^readonly WIT_LINEAR_ISSUE_FIELDS=//" | sed "1s/^'//; \$s/^'$//")" echo "--- WIT_LINEAR_ISSUE_FIELDS as extracted from common.sh ---" diff --git a/plugins/work-items/tools/work-item-tracker/adapters/linear/schema-check/negative.mjs b/plugins/work-items/tools/work-item-tracker/adapters/linear/schema-check/negative.mjs index 0131564b9..b09b5f804 100644 --- a/plugins/work-items/tools/work-item-tracker/adapters/linear/schema-check/negative.mjs +++ b/plugins/work-items/tools/work-item-tracker/adapters/linear/schema-check/negative.mjs @@ -31,7 +31,7 @@ const bad = [ let caught = 0; for (const [label, q, vars] of bad) { let doc, errs = []; - try { doc = parse(q); } catch (e) { console.log(`CAUGHT(parse) ${label}`); caught++; continue; } + try { doc = parse(q); } catch { console.log(`CAUGHT(parse) ${label}`); caught++; continue; } errs = validate(schema, doc); if (errs.length === 0) { const opDef = doc.definitions.find(d => d.kind === 'OperationDefinition'); diff --git a/plugins/work-items/tools/work-item-tracker/conformance/e2e-probe.sh b/plugins/work-items/tools/work-item-tracker/conformance/e2e-probe.sh index dbb8cc30e..987acbcf7 100755 --- a/plugins/work-items/tools/work-item-tracker/conformance/e2e-probe.sh +++ b/plugins/work-items/tools/work-item-tracker/conformance/e2e-probe.sh @@ -25,16 +25,16 @@ usage_error() { EVIDENCE="" while [[ $# -gt 0 ]]; do case "$1" in - --evidence) - if [[ $# -lt 2 ]]; then - usage_error - fi - EVIDENCE="$2" - shift 2 - ;; - *) + --evidence) + if [[ $# -lt 2 ]]; then usage_error - ;; + fi + EVIDENCE="$2" + shift 2 + ;; + *) + usage_error + ;; esac done @@ -114,7 +114,6 @@ assert_not_contains "scoped frontier excludes the map itself" "$PIDS" "$MAP_ID" CLAIM="$(wit claim "$ITEM1_ID" --session-id "e2e-$TS")" record "claim item1" "$CLAIM" LEASE_CID="$(jq -r '.lease_comment_id' <<<"$CLAIM")" -assert_contains "lease comment id numeric" "n$LEASE_CID" "n" if [[ "$LEASE_CID" =~ ^[0-9]+$ ]]; then pass "lease_comment_id shape" else diff --git a/scripts/affected-tests-no-suite.txt b/scripts/affected-tests-no-suite.txt index f1ac2637b..0cfb21e98 100644 --- a/scripts/affected-tests-no-suite.txt +++ b/scripts/affected-tests-no-suite.txt @@ -80,6 +80,16 @@ .python-version *.toml +# Linear live-schema check — an ON-DEMAND lane, not a CI one. The scripts under +# this directory ARE the check: fetch-schema.sh downloads Linear's published +# SDL, then validate.mjs / negative.mjs / fidelity.sh are run by hand (see the +# README there) and each exits non-zero on its own failure. No shell suite +# names them and no CI lane runs them, deliberately: the check depends on a +# freshly fetched ~1.2 MB schema and an installed graphql package, neither of +# which the offline suites carry. The lane covering this class is that manual +# invocation, recorded in the schema-check README's "Running it" section. +plugins/work-items/tools/work-item-tracker/adapters/linear/schema-check/* + # Docs-sync generator executed directly by the plugin-options-docs-gate CI # lane: `python3 scripts/sync-plugin-options-docs.py --check` runs this exact # file on every push, so a crash or drifted output fails that lane. No shell diff --git a/scripts/affected-tests.sh b/scripts/affected-tests.sh index 7d88786da..050c2d4be 100755 --- a/scripts/affected-tests.sh +++ b/scripts/affected-tests.sh @@ -428,6 +428,7 @@ build_sync_map() { declare -A SUITES=() # suite path -> reason declare -a UNMAPPED=() # changed paths that mapped to nothing +declare -a DELETED=() # changed paths that mapped to nothing AND no longer exist is_structural() { local b="$1" s @@ -878,6 +879,14 @@ for f in "${changed[@]}"; do if [[ "$SEED_HITS" -eq 0 ]]; then if is_no_suite "$f"; then NO_SUITE_FILES+=("$f") + elif [[ ! -e "$f" ]]; then + # A deletion that maps to nothing needs no suite: the file has no content + # left to cover, and anything that still referenced it selects through + # its own changed path or a suite that names the dead path (both handled + # by select_for above, which runs for deletions too). Only the terminal + # would-be-UNMAPPED case lands here, reported visibly rather than as the + # loud unknown-coverage error that exists for files that DO have content. + DELETED+=("$f") else UNMAPPED+=("$f") fi @@ -906,6 +915,12 @@ if [[ ${#NO_SUITE_FILES[@]} -gt 0 && "$explain" -eq 1 ]]; then done fi +if [[ ${#DELETED[@]} -gt 0 ]]; then + for f in "${DELETED[@]}"; do + echo "deleted: $f (no longer exists and no surviving suite names it; nothing left to cover)" >&2 + done +fi + if [[ ${#UNMAPPED[@]} -gt 0 ]]; then echo "UNMAPPED: ${#UNMAPPED[@]} changed file(s) map to no test suite:" >&2 for f in "${UNMAPPED[@]}"; do @@ -922,7 +937,7 @@ if [[ ${#UNMAPPED[@]} -gt 0 ]]; then fi if [[ ${#selected[@]} -eq 0 ]]; then - echo "No suites selected (every changed file is a recorded no-suite class)." >&2 + echo "No suites selected (every changed file is a recorded no-suite class or a deletion)." >&2 exit 0 fi diff --git a/scripts/affected-tests.test.sh b/scripts/affected-tests.test.sh index eaaadc78d..cbcefa3fe 100755 --- a/scripts/affected-tests.test.sh +++ b/scripts/affected-tests.test.sh @@ -322,6 +322,35 @@ else fail "no-suite class should exit 0 quietly (rc=$RC): $out" fi +# --- a deleted file that maps to nothing is a visible note, not an error --- +# A deletion has no content left to cover; the loud UNMAPPED failure exists for +# paths that still EXIST with unknown coverage. +out="$(cd "$repo" && bash scripts/affected-tests.sh plugins/alpha/hooks/removed-helper.sh 2>&1)" +RC=$? +if [[ "$RC" -eq 0 ]] && + printf '%s' "$out" | grep -q 'deleted: plugins/alpha/hooks/removed-helper.sh' && + ! printf '%s' "$out" | grep -q 'UNMAPPED'; then + ok "a deleted unmapped path exits 0 with a visible deleted note" +else + fail "deleted unmapped path should be a note, not an error (rc=$RC): $out" +fi + +# --- a deletion whose co-located suite survives still selects that suite ---- +# The surviving suite is exactly what fails loudly if the deletion broke +# something, so a deletion must keep selecting through every rule and only +# fall to the deleted note when NOTHING claims it. +mk_widget_consumer "$repo/plugins/alpha/hooks/gone.sh" +suite_body gone >"$repo/plugins/alpha/hooks/gone.test.sh" +rm "$repo/plugins/alpha/hooks/gone.sh" +run_sel "$repo" plugins/alpha/hooks/gone.sh +out="$OUT" +if [[ "$RC" -eq 0 ]] && has_line "$out" plugins/alpha/hooks/gone.test.sh; then + ok "a deletion with a surviving co-located suite still selects it" +else + fail "deletion should select its surviving suite (rc=$RC): $out" +fi +rm -f "$repo/plugins/alpha/hooks/gone.test.sh" + # --- explicit paths vs the default diff ------------------------------------ base="$(git -C "$repo" rev-parse HEAD)" printf '# edited\n' >>"$repo/plugins/beta/hooks/beta-hook.sh" diff --git a/scripts/check-changelog-parity.sh b/scripts/check-changelog-parity.sh index 4674c1831..ef3cd331a 100755 --- a/scripts/check-changelog-parity.sh +++ b/scripts/check-changelog-parity.sh @@ -293,7 +293,7 @@ if [[ "$mode" == "--check-order" ]]; then dupes="$(printf '%s\n' "${versions[@]}" | sort | uniq -d)" if [[ -n "$dupes" ]]; then - echo "DUPLICATE CHANGELOG VERSION: $changelog lists $(printf '%s' "$dupes" | tr '\n' ' ')more than once. Two branches almost certainly staged the same version; renumber one." >&2 + echo "DUPLICATE CHANGELOG VERSION: $changelog lists $(printf '%s' "$dupes" | tr '\n' ' ') more than once. Two branches almost certainly staged the same version; renumber one." >&2 duplicated=$((duplicated + 1)) fi diff --git a/scripts/check-changelog-parity.test.sh b/scripts/check-changelog-parity.test.sh index 37fc77863..91533bd0e 100755 --- a/scripts/check-changelog-parity.test.sh +++ b/scripts/check-changelog-parity.test.sh @@ -815,6 +815,7 @@ write_changelog "$repo/plugins/alpha/CHANGELOG.md" '## [2.0.0]' '## [2.0.0]' '## out="$(cd "$repo" && bash scripts/check-changelog-parity.sh --check-order 2>&1)" rc=$? if [[ $rc -eq 1 && "$out" == *"DUPLICATE CHANGELOG VERSION"* ]]; then ok "two branches staging one version is caught"; else fail "duplicate not caught: rc=$rc $out"; fi +if [[ "$out" == *"lists 2.0.0 more than once"* ]]; then ok "the duplicated version is named with correct spacing"; else fail "duplicate message malformed: $out"; fi # The exact shape that shipped, in the exact file class that shipped it: a # CONVENTION changelog, which is unversioned by any manifest and therefore diff --git a/scripts/check-hook-exec-form.test.sh b/scripts/check-hook-exec-form.test.sh index 0e0b5508f..3e075b4fe 100755 --- a/scripts/check-hook-exec-form.test.sh +++ b/scripts/check-hook-exec-form.test.sh @@ -712,7 +712,7 @@ metadata: # Body ' if out="$(run_check "$f" 2>&1)"; then - ok "the interpreter-led shell form #2572 lands passes" + ok "the interpreter-led shell form that #2572 landed passes" else fail "the #2572 shape must pass — this gate would otherwise block its own unblocking, got: $out" fi diff --git a/scripts/generate-cheatsheet.mjs b/scripts/generate-cheatsheet.mjs index fa89e8ae6..2623b81c8 100755 --- a/scripts/generate-cheatsheet.mjs +++ b/scripts/generate-cheatsheet.mjs @@ -139,6 +139,12 @@ for (const key of EXCLUDED_SKILLS.keys()) { const mapped = []; for (const s of skills) { const key = `${s.plugin}/${s.skill}`; + // readCheatsheetMeta returns bare {} when line 1 is not a `---` fence; a + // named diagnostic here beats the TypeError that reading .values would throw. + if (s.meta.values === undefined) { + errors.push(`${key}: ${s.path} has no YAML frontmatter (line 1 is not ---)`); + continue; + } const reason = EXCLUDED_PLUGINS.get(s.plugin) ?? EXCLUDED_SKILLS.get(key) ?? diff --git a/testResults.xml b/testResults.xml new file mode 100644 index 000000000..acdea5bbf --- /dev/null +++ b/testResults.xml @@ -0,0 +1,1892 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + This test should run but it did not. Most likely a setup in some parent block failed. + + + + + + This test should run but it did not. Most likely a setup in some parent block failed. + + + + + + + + + + CommandNotFoundException: Could not find Command Get-CimInstance + + + + + + CommandNotFoundException: Could not find Command Get-CimInstance + + + + + + CommandNotFoundException: Could not find Command Get-CimInstance + + + + + + + + + + CommandNotFoundException: Could not find Command Get-CimInstance + + + + + + CommandNotFoundException: Could not find Command Get-CimInstance + + + + + + + + + + CommandNotFoundException: Could not find Command Get-CimInstance + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PlatformNotSupportedException: Windows Principal functionality is not supported on this platform. +MethodInvocationException: Exception calling "GetCurrent" with "0" argument(s): "Windows Principal functionality is not supported on this platform." + at <ScriptBlock>, /home/user/claude-code-plugins/plugins/machine-health/skills/audit/tests/windows/checks/Test-ClaudeTempRoot.Tests.ps1:165 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + This test should run but it did not. Most likely a setup in some parent block failed. + + + + + + This test should run but it did not. Most likely a setup in some parent block failed. + + + + + + This test should run but it did not. Most likely a setup in some parent block failed. + + + + + + + + + + This test should run but it did not. Most likely a setup in some parent block failed. + + + + + + This test should run but it did not. Most likely a setup in some parent block failed. + + + + + + This test should run but it did not. Most likely a setup in some parent block failed. + + + + + + This test should run but it did not. Most likely a setup in some parent block failed. + + + + + + + + + + This test should run but it did not. Most likely a setup in some parent block failed. + + + + + + This test should run but it did not. Most likely a setup in some parent block failed. + + + + + + This test should run but it did not. Most likely a setup in some parent block failed. + + + + + + + + + + This test should run but it did not. Most likely a setup in some parent block failed. + + + + + + This test should run but it did not. Most likely a setup in some parent block failed. + + + + + + + + + + This test should run but it did not. Most likely a setup in some parent block failed. + + + + + + + + + + This test should run but it did not. Most likely a setup in some parent block failed. + + + + + + This test should run but it did not. Most likely a setup in some parent block failed. + + + + + + This test should run but it did not. Most likely a setup in some parent block failed. + + + + + + This test should run but it did not. Most likely a setup in some parent block failed. + + + + + + + + + + This test should run but it did not. Most likely a setup in some parent block failed. + + + + + + + + + + + + + + CommandNotFoundException: Could not find Command Get-MpPreference + + + + + + + + + + CommandNotFoundException: Could not find Command Get-MpPreference + + + + + + CommandNotFoundException: Could not find Command Get-MpPreference + + + + + + + + + + CommandNotFoundException: Could not find Command Get-MpPreference + + + + + + CommandNotFoundException: Could not find Command Get-MpPreference + + + + + + + + + + + + + + + + This test should run but it did not. Most likely a setup in some parent block failed. + + + + + + This test should run but it did not. Most likely a setup in some parent block failed. + + + + + + This test should run but it did not. Most likely a setup in some parent block failed. + + + + + + + + + + This test should run but it did not. Most likely a setup in some parent block failed. + + + + + + This test should run but it did not. Most likely a setup in some parent block failed. + + + + + + This test should run but it did not. Most likely a setup in some parent block failed. + + + + + + + + + + CommandNotFoundException: Could not find Command Get-Volume + + + + + + CommandNotFoundException: Could not find Command Get-Volume + + + + + + + + + + + + This test should run but it did not. Most likely a setup in some parent block failed. + + + + + + This test should run but it did not. Most likely a setup in some parent block failed. + + + + + + + + + + + + CommandNotFoundException: Could not find Command Get-Volume + + + + + + + + + + + + This test should run but it did not. Most likely a setup in some parent block failed. + + + + + + This test should run but it did not. Most likely a setup in some parent block failed. + + + + + + This test should run but it did not. Most likely a setup in some parent block failed. + + + + + + This test should run but it did not. Most likely a setup in some parent block failed. + + + + + + + + + + CommandNotFoundException: Could not find Command Get-Volume + + + + + + + + + + + + + + CommandNotFoundException: Could not find Command Resolve-DnsName + + + + + + + + + + CommandNotFoundException: Could not find Command Resolve-DnsName + + + + + + CommandNotFoundException: Could not find Command Resolve-DnsName + + + + + + CommandNotFoundException: Could not find Command Resolve-DnsName + + + + + + CommandNotFoundException: Could not find Command Resolve-DnsName + + + + + + + + + + + + + + This test should run but it did not. Most likely a setup in some parent block failed. + + + + + + This test should run but it did not. Most likely a setup in some parent block failed. + + + + + + + + + + This test should run but it did not. Most likely a setup in some parent block failed. + + + + + + + + + + CommandNotFoundException: Could not find Command Get-CimInstance + + + + + + + + + + CommandNotFoundException: Could not find Command Get-CimInstance + + + + + + CommandNotFoundException: Could not find Command Get-CimInstance + + + + + + + + + + CommandNotFoundException: Could not find Command Get-CimInstance + + + + + + + + + + CommandNotFoundException: Could not find Command Get-CimInstance + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + This test should run but it did not. Most likely a setup in some parent block failed. + + + + + + This test should run but it did not. Most likely a setup in some parent block failed. + + + + + + + + + + CommandNotFoundException: Could not find Command Get-WinEvent + + + + + + CommandNotFoundException: Could not find Command Get-WinEvent + + + + + + + + + + CommandNotFoundException: Could not find Command Get-WinEvent + + + + + + CommandNotFoundException: Could not find Command Get-WinEvent + + + + + + CommandNotFoundException: Could not find Command Get-WinEvent + + + + + + CommandNotFoundException: Could not find Command Get-WinEvent + + + + + + + + + + CommandNotFoundException: Could not find Command Get-WinEvent + + + + + + + + + + CommandNotFoundException: Could not find Command Get-WinEvent + + + + + + CommandNotFoundException: Could not find Command Get-WinEvent + + + + + + CommandNotFoundException: Could not find Command Get-WinEvent + + + + + + + + + + + + + + CommandNotFoundException: Could not find Command Get-CimInstance + + + + + + + + + + CommandNotFoundException: Could not find Command Get-CimInstance + + + + + + CommandNotFoundException: Could not find Command Get-CimInstance + + + + + + CommandNotFoundException: Could not find Command Get-CimInstance + + + + + + CommandNotFoundException: Could not find Command Get-CimInstance + + + + + + + + + + + + + + CommandNotFoundException: Could not find Command Get-ScheduledTask + + + + + + CommandNotFoundException: Could not find Command Get-ScheduledTask + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + This test should run but it did not. Most likely a setup in some parent block failed. + + + + + + This test should run but it did not. Most likely a setup in some parent block failed. + + + + + + + + + + CommandNotFoundException: Could not find Command Get-CimInstance + + + + + + CommandNotFoundException: Could not find Command Get-CimInstance + + + + + + + + + + CommandNotFoundException: Could not find Command Get-CimInstance + + + + + + CommandNotFoundException: Could not find Command Get-CimInstance + + + + + + CommandNotFoundException: Could not find Command Get-CimInstance + + + + + + + + + + CommandNotFoundException: Could not find Command Get-CimInstance + + + + + + CommandNotFoundException: Could not find Command Get-CimInstance + + + + + + CommandNotFoundException: Could not find Command Get-CimInstance + + + + + + + + + + + + + + + + + + + CommandNotFoundException: Could not find Command Get-Tpm + + + + + + CommandNotFoundException: Could not find Command Get-Tpm + + + + + + CommandNotFoundException: Could not find Command Get-Tpm + + + + + + + + + + + + + + This test should run but it did not. Most likely a setup in some parent block failed. + + + + + + This test should run but it did not. Most likely a setup in some parent block failed. + + + + + + + + + + CommandNotFoundException: Could not find Command Get-HotFix + + + + + + CommandNotFoundException: Could not find Command Get-HotFix + + + + + + CommandNotFoundException: Could not find Command Get-HotFix + + + + + + + + + + CommandNotFoundException: Could not find Command Get-HotFix + + + + + + CommandNotFoundException: Could not find Command Get-HotFix + + + + + + + + + + CommandNotFoundException: Could not find Command Get-HotFix + + + + + + CommandNotFoundException: Could not find Command Get-HotFix + + + + + + + + + + CommandNotFoundException: Could not find Command Get-HotFix + + + + + + + + + + + + + + + + + + + + + + + + + + Expected strings to be the same, but they were different. +String lengths are both 4. +Strings differ at index 0. +Expected: 'CRIT' +But was: 'INFO' + ^ + at $result.severity | Should -Be 'CRIT', /home/user/claude-code-plugins/plugins/machine-health/skills/audit/tests/windows/checks/Test-WingetUpgrades.Tests.ps1:147 + + + + + + Expected strings to be the same, but they were different. +String lengths are both 4. +Strings differ at index 0. +Expected: 'CRIT' +But was: 'INFO' + ^ + at $result.severity | Should -Be 'CRIT', /home/user/claude-code-plugins/plugins/machine-health/skills/audit/tests/windows/checks/Test-WingetUpgrades.Tests.ps1:189 + + + + + Expected strings to be the same, but they were different. +String lengths are both 4. +Strings differ at index 0. +Expected: 'CRIT' +But was: 'INFO' + ^ + at $result.severity | Should -Be 'CRIT', /home/user/claude-code-plugins/plugins/machine-health/skills/audit/tests/windows/checks/Test-WingetUpgrades.Tests.ps1:208 + + + + + Expected strings to be the same, but they were different. +String lengths are both 4. +Strings differ at index 0. +Expected: 'CRIT' +But was: 'INFO' + ^ + at $result.severity | Should -Be 'CRIT', /home/user/claude-code-plugins/plugins/machine-health/skills/audit/tests/windows/checks/Test-WingetUpgrades.Tests.ps1:227 + + + + + + + + + + + + Expected Get-CisaKevCache to be called at least 1 time, but was called 0 times +Performed invocations: + <none> + at Should -Invoke Get-CisaKevCache -Times 1 -ParameterFilter {, /home/user/claude-code-plugins/plugins/machine-health/skills/audit/tests/windows/checks/Test-WingetUpgrades.Tests.ps1:290 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CommandNotFoundException: Could not find Command Get-CimInstance + + + + + + CommandNotFoundException: Could not find Command Get-CimInstance + + + + + + CommandNotFoundException: Could not find Command Get-CimInstance + + + + + + CommandNotFoundException: Could not find Command Get-CimInstance + + + + + + + + + + CommandNotFoundException: Could not find Command Get-CimInstance + + + + + + CommandNotFoundException: Could not find Command Get-CimInstance + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CommandNotFoundException: Could not find Command Get-AppxPackage + + + + + + CommandNotFoundException: Could not find Command Get-AppxPackage + + + + + + CommandNotFoundException: Could not find Command Get-AppxPackage + + + + + + CommandNotFoundException: Could not find Command Get-AppxPackage + + + + + + + + + + + + + + This test should run but it did not. Most likely a setup in some parent block failed. + + + + + + This test should run but it did not. Most likely a setup in some parent block failed. + + + + + + This test should run but it did not. Most likely a setup in some parent block failed. + + + + + + This test should run but it did not. Most likely a setup in some parent block failed. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 7ea0ec067d7d8b2f19d2054962649ffa4ccbd746 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 31 Aug 2026 05:19:26 +0000 Subject: [PATCH 09/10] Remove stray Pester artifact committed by mistake testResults.xml is Invoke-Pester run output from the deferred-work verification, not repository content. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01W61wikkiEK5StWgg9rfEQW --- testResults.xml | 1892 ----------------------------------------------- 1 file changed, 1892 deletions(-) delete mode 100644 testResults.xml diff --git a/testResults.xml b/testResults.xml deleted file mode 100644 index acdea5bbf..000000000 --- a/testResults.xml +++ /dev/null @@ -1,1892 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - This test should run but it did not. Most likely a setup in some parent block failed. - - - - - - This test should run but it did not. Most likely a setup in some parent block failed. - - - - - - - - - - CommandNotFoundException: Could not find Command Get-CimInstance - - - - - - CommandNotFoundException: Could not find Command Get-CimInstance - - - - - - CommandNotFoundException: Could not find Command Get-CimInstance - - - - - - - - - - CommandNotFoundException: Could not find Command Get-CimInstance - - - - - - CommandNotFoundException: Could not find Command Get-CimInstance - - - - - - - - - - CommandNotFoundException: Could not find Command Get-CimInstance - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - PlatformNotSupportedException: Windows Principal functionality is not supported on this platform. -MethodInvocationException: Exception calling "GetCurrent" with "0" argument(s): "Windows Principal functionality is not supported on this platform." - at <ScriptBlock>, /home/user/claude-code-plugins/plugins/machine-health/skills/audit/tests/windows/checks/Test-ClaudeTempRoot.Tests.ps1:165 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - This test should run but it did not. Most likely a setup in some parent block failed. - - - - - - This test should run but it did not. Most likely a setup in some parent block failed. - - - - - - This test should run but it did not. Most likely a setup in some parent block failed. - - - - - - - - - - This test should run but it did not. Most likely a setup in some parent block failed. - - - - - - This test should run but it did not. Most likely a setup in some parent block failed. - - - - - - This test should run but it did not. Most likely a setup in some parent block failed. - - - - - - This test should run but it did not. Most likely a setup in some parent block failed. - - - - - - - - - - This test should run but it did not. Most likely a setup in some parent block failed. - - - - - - This test should run but it did not. Most likely a setup in some parent block failed. - - - - - - This test should run but it did not. Most likely a setup in some parent block failed. - - - - - - - - - - This test should run but it did not. Most likely a setup in some parent block failed. - - - - - - This test should run but it did not. Most likely a setup in some parent block failed. - - - - - - - - - - This test should run but it did not. Most likely a setup in some parent block failed. - - - - - - - - - - This test should run but it did not. Most likely a setup in some parent block failed. - - - - - - This test should run but it did not. Most likely a setup in some parent block failed. - - - - - - This test should run but it did not. Most likely a setup in some parent block failed. - - - - - - This test should run but it did not. Most likely a setup in some parent block failed. - - - - - - - - - - This test should run but it did not. Most likely a setup in some parent block failed. - - - - - - - - - - - - - - CommandNotFoundException: Could not find Command Get-MpPreference - - - - - - - - - - CommandNotFoundException: Could not find Command Get-MpPreference - - - - - - CommandNotFoundException: Could not find Command Get-MpPreference - - - - - - - - - - CommandNotFoundException: Could not find Command Get-MpPreference - - - - - - CommandNotFoundException: Could not find Command Get-MpPreference - - - - - - - - - - - - - - - - This test should run but it did not. Most likely a setup in some parent block failed. - - - - - - This test should run but it did not. Most likely a setup in some parent block failed. - - - - - - This test should run but it did not. Most likely a setup in some parent block failed. - - - - - - - - - - This test should run but it did not. Most likely a setup in some parent block failed. - - - - - - This test should run but it did not. Most likely a setup in some parent block failed. - - - - - - This test should run but it did not. Most likely a setup in some parent block failed. - - - - - - - - - - CommandNotFoundException: Could not find Command Get-Volume - - - - - - CommandNotFoundException: Could not find Command Get-Volume - - - - - - - - - - - - This test should run but it did not. Most likely a setup in some parent block failed. - - - - - - This test should run but it did not. Most likely a setup in some parent block failed. - - - - - - - - - - - - CommandNotFoundException: Could not find Command Get-Volume - - - - - - - - - - - - This test should run but it did not. Most likely a setup in some parent block failed. - - - - - - This test should run but it did not. Most likely a setup in some parent block failed. - - - - - - This test should run but it did not. Most likely a setup in some parent block failed. - - - - - - This test should run but it did not. Most likely a setup in some parent block failed. - - - - - - - - - - CommandNotFoundException: Could not find Command Get-Volume - - - - - - - - - - - - - - CommandNotFoundException: Could not find Command Resolve-DnsName - - - - - - - - - - CommandNotFoundException: Could not find Command Resolve-DnsName - - - - - - CommandNotFoundException: Could not find Command Resolve-DnsName - - - - - - CommandNotFoundException: Could not find Command Resolve-DnsName - - - - - - CommandNotFoundException: Could not find Command Resolve-DnsName - - - - - - - - - - - - - - This test should run but it did not. Most likely a setup in some parent block failed. - - - - - - This test should run but it did not. Most likely a setup in some parent block failed. - - - - - - - - - - This test should run but it did not. Most likely a setup in some parent block failed. - - - - - - - - - - CommandNotFoundException: Could not find Command Get-CimInstance - - - - - - - - - - CommandNotFoundException: Could not find Command Get-CimInstance - - - - - - CommandNotFoundException: Could not find Command Get-CimInstance - - - - - - - - - - CommandNotFoundException: Could not find Command Get-CimInstance - - - - - - - - - - CommandNotFoundException: Could not find Command Get-CimInstance - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - This test should run but it did not. Most likely a setup in some parent block failed. - - - - - - This test should run but it did not. Most likely a setup in some parent block failed. - - - - - - - - - - CommandNotFoundException: Could not find Command Get-WinEvent - - - - - - CommandNotFoundException: Could not find Command Get-WinEvent - - - - - - - - - - CommandNotFoundException: Could not find Command Get-WinEvent - - - - - - CommandNotFoundException: Could not find Command Get-WinEvent - - - - - - CommandNotFoundException: Could not find Command Get-WinEvent - - - - - - CommandNotFoundException: Could not find Command Get-WinEvent - - - - - - - - - - CommandNotFoundException: Could not find Command Get-WinEvent - - - - - - - - - - CommandNotFoundException: Could not find Command Get-WinEvent - - - - - - CommandNotFoundException: Could not find Command Get-WinEvent - - - - - - CommandNotFoundException: Could not find Command Get-WinEvent - - - - - - - - - - - - - - CommandNotFoundException: Could not find Command Get-CimInstance - - - - - - - - - - CommandNotFoundException: Could not find Command Get-CimInstance - - - - - - CommandNotFoundException: Could not find Command Get-CimInstance - - - - - - CommandNotFoundException: Could not find Command Get-CimInstance - - - - - - CommandNotFoundException: Could not find Command Get-CimInstance - - - - - - - - - - - - - - CommandNotFoundException: Could not find Command Get-ScheduledTask - - - - - - CommandNotFoundException: Could not find Command Get-ScheduledTask - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - This test should run but it did not. Most likely a setup in some parent block failed. - - - - - - This test should run but it did not. Most likely a setup in some parent block failed. - - - - - - - - - - CommandNotFoundException: Could not find Command Get-CimInstance - - - - - - CommandNotFoundException: Could not find Command Get-CimInstance - - - - - - - - - - CommandNotFoundException: Could not find Command Get-CimInstance - - - - - - CommandNotFoundException: Could not find Command Get-CimInstance - - - - - - CommandNotFoundException: Could not find Command Get-CimInstance - - - - - - - - - - CommandNotFoundException: Could not find Command Get-CimInstance - - - - - - CommandNotFoundException: Could not find Command Get-CimInstance - - - - - - CommandNotFoundException: Could not find Command Get-CimInstance - - - - - - - - - - - - - - - - - - - CommandNotFoundException: Could not find Command Get-Tpm - - - - - - CommandNotFoundException: Could not find Command Get-Tpm - - - - - - CommandNotFoundException: Could not find Command Get-Tpm - - - - - - - - - - - - - - This test should run but it did not. Most likely a setup in some parent block failed. - - - - - - This test should run but it did not. Most likely a setup in some parent block failed. - - - - - - - - - - CommandNotFoundException: Could not find Command Get-HotFix - - - - - - CommandNotFoundException: Could not find Command Get-HotFix - - - - - - CommandNotFoundException: Could not find Command Get-HotFix - - - - - - - - - - CommandNotFoundException: Could not find Command Get-HotFix - - - - - - CommandNotFoundException: Could not find Command Get-HotFix - - - - - - - - - - CommandNotFoundException: Could not find Command Get-HotFix - - - - - - CommandNotFoundException: Could not find Command Get-HotFix - - - - - - - - - - CommandNotFoundException: Could not find Command Get-HotFix - - - - - - - - - - - - - - - - - - - - - - - - - - Expected strings to be the same, but they were different. -String lengths are both 4. -Strings differ at index 0. -Expected: 'CRIT' -But was: 'INFO' - ^ - at $result.severity | Should -Be 'CRIT', /home/user/claude-code-plugins/plugins/machine-health/skills/audit/tests/windows/checks/Test-WingetUpgrades.Tests.ps1:147 - - - - - - Expected strings to be the same, but they were different. -String lengths are both 4. -Strings differ at index 0. -Expected: 'CRIT' -But was: 'INFO' - ^ - at $result.severity | Should -Be 'CRIT', /home/user/claude-code-plugins/plugins/machine-health/skills/audit/tests/windows/checks/Test-WingetUpgrades.Tests.ps1:189 - - - - - Expected strings to be the same, but they were different. -String lengths are both 4. -Strings differ at index 0. -Expected: 'CRIT' -But was: 'INFO' - ^ - at $result.severity | Should -Be 'CRIT', /home/user/claude-code-plugins/plugins/machine-health/skills/audit/tests/windows/checks/Test-WingetUpgrades.Tests.ps1:208 - - - - - Expected strings to be the same, but they were different. -String lengths are both 4. -Strings differ at index 0. -Expected: 'CRIT' -But was: 'INFO' - ^ - at $result.severity | Should -Be 'CRIT', /home/user/claude-code-plugins/plugins/machine-health/skills/audit/tests/windows/checks/Test-WingetUpgrades.Tests.ps1:227 - - - - - - - - - - - - Expected Get-CisaKevCache to be called at least 1 time, but was called 0 times -Performed invocations: - <none> - at Should -Invoke Get-CisaKevCache -Times 1 -ParameterFilter {, /home/user/claude-code-plugins/plugins/machine-health/skills/audit/tests/windows/checks/Test-WingetUpgrades.Tests.ps1:290 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - CommandNotFoundException: Could not find Command Get-CimInstance - - - - - - CommandNotFoundException: Could not find Command Get-CimInstance - - - - - - CommandNotFoundException: Could not find Command Get-CimInstance - - - - - - CommandNotFoundException: Could not find Command Get-CimInstance - - - - - - - - - - CommandNotFoundException: Could not find Command Get-CimInstance - - - - - - CommandNotFoundException: Could not find Command Get-CimInstance - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - CommandNotFoundException: Could not find Command Get-AppxPackage - - - - - - CommandNotFoundException: Could not find Command Get-AppxPackage - - - - - - CommandNotFoundException: Could not find Command Get-AppxPackage - - - - - - CommandNotFoundException: Could not find Command Get-AppxPackage - - - - - - - - - - - - - - This test should run but it did not. Most likely a setup in some parent block failed. - - - - - - This test should run but it did not. Most likely a setup in some parent block failed. - - - - - - This test should run but it did not. Most likely a setup in some parent block failed. - - - - - - This test should run but it did not. Most likely a setup in some parent block failed. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file From ddf2aadbd98f4618c3c4cef84060ddb6a2b649a9 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 31 Aug 2026 05:24:52 +0000 Subject: [PATCH 10/10] Fix em-dash regression in README deletion-contract sentence The purged-em-dashes hygiene gate failed on the previous push: README.md is a declared-purged surface and the new affected-tests deletion paragraph introduced one em dash. The sentence is now two. check-purged-em-dashes.sh passes locally (21 declared paths, 30 files, no em dashes). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01W61wikkiEK5StWgg9rfEQW --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a8ebf6991..dfe54be58 100644 --- a/README.md +++ b/README.md @@ -118,8 +118,8 @@ genuinely carry no suite are recorded, with the CI lane that does cover them, in prose and manifests, never for code: a source file with no coverage is supposed to fail here. A **deletion** is the one exception: a changed path that no longer exists and that nothing claims is reported as a visible `deleted:` note instead -of the error, because there is no content left to cover — while a deletion that -a surviving suite still names (a co-located test left behind, a suite that +of the error, because there is no content left to cover. A deletion that a +surviving suite still names (a co-located test left behind, a suite that references the dead path) keeps selecting those suites, which are exactly what fails loudly if the deletion broke something.