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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ jobs:
.github/workflows/claude-security-review.yml
.github/workflows/dependabot-miro-bundle.yml
.github/workflows/do-not-merge.yml
.github/workflows/hook-utils-timing.yml
.github/workflows/link-check.yml
.github/workflows/pr-issue-linkage.yml
.github/workflows/pr-title.yml
Expand Down Expand Up @@ -1417,9 +1418,19 @@ jobs:
--requirement .github/requirements-ci.txt
echo "$GITHUB_WORKSPACE/node_modules/.bin" >> "$GITHUB_PATH"
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
# Three suites at a time, not four. The runner has 4 vCPUs and the suites
# are spawn-bound, so four looked like the shape that pays, but at four
# three separate suites failed across two runs by producing EMPTY output
# from an external command on a path with no clock in it: an awk read of
# an agent file, a jq read of a local-markdown fixture, and a conformance
# run. Serialising each one in turn only moves the symptom to the next
# suite, so the job count is the lever rather than the allowlist. The
# default stays 1 for Windows dev boxes (see the runner's header); the
# suites that assert wall-clock ceilings run one at a time regardless,
# from scripts/run-plugin-tests-serial.txt.
- name: Run plugin contract tests
if: needs.scope.outputs.run_full == 'true'
run: scripts/run-plugin-tests.sh
run: scripts/run-plugin-tests.sh --jobs 3
# Explicit step: run-plugin-tests.sh discovers only plugins/**/*.test.sh,
# so the cheat-sheet generator suite under scripts/ never runs without it.
- name: Run cheat-sheet generator tests
Expand Down
52 changes: 52 additions & 0 deletions .github/workflows/hook-utils-timing.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: hook-utils-timing

# Weekly wall-clock comparisons for lib/hook-utils.sh's bounded stdin read.
# lib/hook-utils.test.sh carries two interleaved-pair clock comparisons (the
# late-EOF window count and the stall overshoot) that each wait out real read
# bounds: about 80 s per run on a hosted runner and identical on both operating
# systems, while the suite's own comments call them advisory next to the
# load-independent probes that guard the same regressions (#2105). Every
# ordinary run of the suite therefore reports them as deferred, and this lane
# is where they actually run: HOOK_UTILS_TIMING=1 enables them, once a week on
# each platform, off the required merge gate. Advisory by construction: nothing
# aggregates this workflow into ci-status, and a flake here blocks no merge.
#
# Two literal jobs rather than a matrix so the runner policy reads pinned
# labels (never `*-latest`), the same rule ci.yml's lanes follow.
on:
schedule:
- cron: "17 6 * * 1" # Mondays 06:17 UTC
workflow_dispatch:

permissions:
contents: read

jobs:
linux:
runs-on: ubuntu-24.04
timeout-minutes: 20
steps:
- name: Check out
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Run the shared lib suite with the clock comparisons enabled
env:
HOOK_UTILS_TIMING: "1"
run: bash lib/hook-utils.test.sh

windows:
runs-on: windows-2025
timeout-minutes: 25
defaults:
run:
shell: bash
steps:
- name: Check out
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Run the shared lib suite with the clock comparisons enabled on Windows
env:
HOOK_UTILS_TIMING: "1"
run: bash lib/hook-utils.test.sh
18 changes: 16 additions & 2 deletions lib/hook-utils.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1738,7 +1738,18 @@ bs_time_late_eof() { # $1 = shell prelude; prints elapsed ms (empty if untimed)
# returns 1 regardless. Same spawn, opposite verdict.
# shellcheck disable=SC2016 # $1 is the overriding function's own positional, not this shell's
bs_reads_on='hook::json_complete() { printf "%s" "$1" | jq -e . >/dev/null 2>&1; return 1; }'
if bs_samples 6 bs_time_late_eof "" "$bs_reads_on"; then
# HOOK_UTILS_TIMING gates the two interleaved-pair CLOCK comparisons in this
# suite (this late-EOF one and the stall-overshoot one in Test 18g). Each is
# six pairs of two arms waiting out real bounds, about 80 s of wall time on a
# hosted runner, and both are advisory by their own terms: the load-independent
# probes beside them (the chunk-boundary engagement check and the stall
# read-count check) are the regression guards (#2105). Unset, which is every
# ordinary run, the comparison is reported as deferred and the probes carry the
# coverage; the weekly `hook-utils-timing` workflow sets HOOK_UTILS_TIMING=1
# and runs the comparisons on both operating systems.
if [[ -z "${HOOK_UTILS_TIMING:-}" ]]; then
ok "buffer_stdin: late-EOF clock comparison deferred (HOOK_UTILS_TIMING unset; the weekly hook-utils-timing lane runs it, and the chunk-boundary engagement probe is the regression guard)"
elif bs_samples 6 bs_time_late_eof "" "$bs_reads_on"; then
bs_paired_verdict "buffer_stdin: late-EOF stops at the payload, not the bound" \
400 fast slow
elif [[ -n "${EPOCHREALTIME:-}" ]]; then
Expand Down Expand Up @@ -2390,7 +2401,10 @@ rm -f "$bs_stall_read_file"
# Stall overshoot is load-sensitive when asserted as an absolute wall-clock gap
# (#2105, #2080). The idle-slice probe above is the load-independent guard; this
# relative check is advisory — fail only when every timed pair contradicts slicing.
if bs_samples 6 bs_time_stall "" "$bs_unsliced"; then
# Gated on HOOK_UTILS_TIMING like the late-EOF comparison (see Test 18b).
if [[ -z "${HOOK_UTILS_TIMING:-}" ]]; then
ok "buffer_stdin: stall overshoot clock comparison deferred (HOOK_UTILS_TIMING unset; the weekly hook-utils-timing lane runs it, and the read-count probe above is the regression guard, #2105)"
elif bs_samples 6 bs_time_stall "" "$bs_unsliced"; then
bs_rel_ok=1
bs_rel_detail="deltas ${bs_deltas_a_first[*]} | ${bs_deltas_b_first[*]}"
bs_rel_neg=0
Expand Down
2 changes: 1 addition & 1 deletion plugins/context-guard/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "context-guard",
"version": "0.7.36",
"version": "0.7.37",
"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 \u2014 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",
Expand Down
12 changes: 12 additions & 0 deletions plugins/context-guard/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@ 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.37]

### Changed

- **The statusline-tee cancellation cases park their `mv` shim for two seconds
instead of ten.** Bash defers the TERM trap until the foreground `mv`
returns, so the shim's own sleep was the floor for both cancellation cases
and the suite spent most of its wall time waiting on a delay that proved
nothing. Two seconds exercises the same cancellation window behind the same
readiness marker. Test-side only; no hook, script or shipped behaviour
changes.

## [0.7.36]

### Changed
Expand Down
11 changes: 8 additions & 3 deletions plugins/context-guard/scripts/statusline-tee.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -376,16 +376,21 @@ if [[ -f "$CTXDIR17/sess-42.json" ]]; then ok "prune pass still wrote the live s
# 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.
# signal lands inside the window deterministically. The shim announces that it
# has parked by touching a marker, and the kill waits for that marker instead of
# a fixed sleep; the park itself is short because bash defers a trap until the
# foreground `mv` returns, so the shim's sleep is the floor on how long the
# reclaim takes to run.
HOME18="$WORK/home18"
mkdir -p "$HOME18"
SHIM18="$WORK/shim18"
mkdir -p "$SHIM18"
printf '#!/usr/bin/env bash\nsleep 10\n' >"$SHIM18/mv"
printf '#!/usr/bin/env bash\n: >"%s/parked"\nsleep 2\n' "$SHIM18" >"$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
tries=250
while ((tries-- > 0)) && [[ ! -e "$SHIM18/parked" ]]; do sleep 0.02; done
kill -TERM "$TEE_PID" 2>/dev/null
wait "$TEE_PID" 2>/dev/null
sleep 0.5
Expand Down
2 changes: 1 addition & 1 deletion plugins/rate-limit-guard/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "rate-limit-guard",
"version": "0.7.28",
"version": "0.7.29",
"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",
Expand Down
12 changes: 12 additions & 0 deletions plugins/rate-limit-guard/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,18 @@
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.29]

### Changed

- **The statusline-tee cancellation cases park their `mv` shim for two seconds
instead of ten.** Bash defers the TERM trap until the foreground `mv`
returns, so the shim's own sleep was the floor for both cancellation cases
and the suite spent most of its wall time waiting on a delay that proved
nothing. Two seconds exercises the same cancellation window behind the same
readiness marker. Test-side only; no hook, script or shipped behaviour
changes.

## [0.7.28]

### Changed
Expand Down
11 changes: 8 additions & 3 deletions plugins/rate-limit-guard/scripts/statusline-tee.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -279,16 +279,21 @@ if [[ "$(jq -r '.session_id' <"$TEEFILE")" == "sess-later" ]]; then ok "snapshot
# 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.
# signal lands inside the window deterministically. The shim announces that it
# has parked by touching a marker, and the kill waits for that marker instead of
# a fixed sleep; the park itself is short because bash defers a trap until the
# foreground `mv` returns, so the shim's sleep is the floor on how long the
# reclaim takes to run.
HOME14="$WORK/home14"
mkdir -p "$HOME14"
SHIM14="$WORK/shim14"
mkdir -p "$SHIM14"
printf '#!/usr/bin/env bash\nsleep 10\n' >"$SHIM14/mv"
printf '#!/usr/bin/env bash\n: >"%s/parked"\nsleep 2\n' "$SHIM14" >"$SHIM14/mv"
chmod +x "$SHIM14/mv"
printf '%s' "$(build_input)" | HOME="$HOME14" PATH="$SHIM14:$PATH" bash "$TEE" >/dev/null 2>&1 &
TEE_PID=$!
sleep 2
tries=250
while ((tries-- > 0)) && [[ ! -e "$SHIM14/parked" ]]; do sleep 0.02; done
kill -TERM "$TEE_PID" 2>/dev/null
wait "$TEE_PID" 2>/dev/null
sleep 0.5
Expand Down
2 changes: 1 addition & 1 deletion plugins/session-flow/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "session-flow",
"version": "0.34.21",
"version": "0.34.22",
"description": "Session-lifecycle toolkit of fourteen skills: workflow (navigate a staged dev workflow and suggest the next stage), handoff (write a save-point and resume prompt for /clear-and-resume), continue-in-background (delegate the task to a fresh background agent that continues it now \u2014 same save-point engine as handoff, delivered by launching a detached claude --bg session seeded with the resume prompt; launches only on explicit user request), keep-going (recover and continue after any interruption OR when live off-thread work looks stalled \u2014 inventory off-thread work, inspect its real output, act only on evidence, then continue; after a usage limit lifts it continues rather than summarizing-and-stalling), find-handoff (recover a lost handoff after /clear \u2014 when the resume prompt was written but never copied \u2014 via a read-only detection ladder: known-location glob of the handoffs dir, then a bounded, recency-ranked transcript scan for the handoff directive and dashed-rail markers, then a confirm-before-resume gate; surfaces only the resume prompt + metadata, never raw transcript content), clean-stop (get to a durable, linked stopping point before the machine may go away \u2014 sweep every repo/worktree for uncommitted, unpushed, or PR-less work, push it durable, put breadcrumbs in PR/issue bodies, then give a free-and-clear verdict), retro (structured end-of-session retrospective with transcript metrics and learning codification), running-retro (in-flight retrospective checkpoints that spawn a subagent to analyze the transcript so far and append classified findings to a cumulative running ledger \u2014 capture and route only, the live counterpart to retro; also owns a detached-observer substrate that can watch a session out-of-band and run the checkpoint autonomously after the session ends), orient (read-only session orientation \u2014 synthesize where we stand, what we are doing, and why, from durable + off-thread state the built-in /recap never sees: ledgers, handoffs, workflow checklists, running-retro ledgers, open PRs and work-items, and git), orchestrate (arm a session or worker with proactive-orchestration imperatives), reanchor (verify a session's working assumptions are still true against live reality \u2014 referenced PRs/issues/branches, base-branch drift, renamed/version-drifted surfaces, stale memory-tier files, and the goal a handoff records, compared across the chain so a re-derived goal reports as drift \u2014 before building on them), reconcile (retire finished off-thread work and reconcile this session's task ledger with reality \u2014 the prune-and-reconcile counterpart to keep-going's resume: inventory the work this session spawned, inspect its real state, retire the finished and close proven-done tasks, auto-settling the finished and gating any kill of still-running work; sibling sessions in the project are reported read-only), setup (check-centric verification of the observer's runtime prerequisites and configuration), and show-options (lay out which skills fit this moment as a ranked, nothing-hidden menu \u2014 a shortlist per bucket plus the complete remainder by name, resolved from the full installed catalog rather than the truncated in-context listing, so the human decides and no option is withheld for looking already-done).",
"author": {
"name": "Melodic Software",
Expand Down
11 changes: 11 additions & 0 deletions plugins/session-flow/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Changelog — session-flow plugin

## [0.34.22]

### Changed

- **The running-retro observer growth case sets its own idle confirmation.** The
case inherited the production 30-second idle confirmation from the test
harness, so it could only ever end at the lifetime deadline rather than when
the tail actually went idle. It now sets a one-second confirmation and a
ten-second deadline, which is what the case was always asserting. Test-side
only; the observer's own defaults are unchanged.

## [0.34.21]

### Changed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -807,8 +807,13 @@ def test_tail_no_duplicate_events_on_growth(self):
# grows in batches and assert exactly one observation per record.
with tempfile.TemporaryDirectory() as d:
tmp = Path(d)
# Growth ends about 1.8 s in; the 2 s idle window then opens and
# the 1 s idle confirmation closes the tail near 5 s. The idle
# confirmation is set explicitly because the harness default is
# the production 30 s, under which this case could only ever end
# at the lifetime deadline (a 30 s wait, measured in CI).
ob = make_observer(tmp, analysis=False, idle_seconds=2.0, poll_seconds=0.15,
max_seconds=30.0)
idle_confirm_seconds=1.0, max_seconds=10.0)
rec = '{"type":"user","message":{"content":"x"}}\n'
total = 9

Expand All @@ -820,7 +825,7 @@ def test_tail_no_duplicate_events_on_growth(self):
for _ in range(3):
f.write(rec)
time.sleep(0.5)
tailer.join(30)
tailer.join(10)
lines = [ln for ln in ob.obs_path.read_text(encoding="utf-8").splitlines() if ln]
self.assertEqual(len(lines), total,
f"expected {total} distilled events, got {len(lines)} (dupes/underread?)")
Expand Down
Loading