Skip to content

Releases: Redtropig/harness-anchor

v0.18.0 — README rewrite, watchdog on every hook, all ten invariants enforced

Choose a tag to compare

@Redtropig Redtropig released this 31 Jul 13:09
2664c77

MINOR. Watchdog coverage on two hooks that never had it is a new backward-compatible capability, and it ships with new gate checks and new tests.

Two hooks ran unbounded

hooks/stop and hooks/user-prompt-submit had no timeout guard, although both fork a JSON engine: ha_json_engine_init probes by running python3 -c 'print(1)', and both then call ha_flist_active. A wedged interpreter — antivirus scanning a first-run interpreter, a Microsoft Store python3 alias, an unreachable network share on PATH — hung them with nothing to intervene.

Measured against stub engines that sleep 30s:

before after
hooks/stop still running at 25s 5s, emits nothing, exit 0
hooks/user-prompt-submit still running at 25s 5s, emits nothing, exit 0

The three peer hooks have had this since v0.10.0. These two were missed for eight releases, while the README asserted every hook had one.

ha_json_engine_init now sits inside main(): outside it, the probe's own hang would sit in front of the watchdog rather than behind it.

Supporting work: a contract test that wedges every engine in the chain, a validate-anchor assertion so a new hook cannot ship without a watchdog, pre-compact added to the timing benchmark with a coverage guard, and troubleshooting entry #16 for the symptom this creates — a hook that goes completely silent.

README rewritten

It opened with a component inventory; the first concrete piece of value was at line 118. Now ordered problem → artifact → components → credibility, led by a real session-start banner. The long-form rationale moved to a new docs/design.md, with the alternative rejected in each case.

Four claims were wrong and are corrected: the hook count (four claimed, five shipping), a read-only assertion about index-curator — which carries Write, and needs it — done_criteria described as booleans when the enforced rule is evidence: null blocking status: pass, and release-note provenance that had leaked in from CHANGELOG entries.

All ten design invariants are now enforced

CLAUDE.md states that the invariant list is the part a test mechanically enforces. That was untrue of three entries.

  • #5 asserted that cpp-detect.sh gates skill loading and that C/C++ skills reference the detected build system. Neither holds. Reworded to state both real gates, with the requirement that does the work — skills/cpp-* scoping to C/C++ inside the first 80 characters — now asserted. The skills themselves are deliberately untouched.
  • #7 — see above.
  • #9 needed only the check, not new wording.
  • #6 turned out never to have been unenforced; its eval is the skill-triggering harness. It now says so and warns against replacing that eval with a static check.

docs/commands.md was also re-verified end to end. Its doc-align marker had stood at v0.12.0 through six releases, hiding five stale claims — including a /verify report documented with five sections when nine are emitted.

Verification

Gate Result
validate-anchor 176 / 0 (was 158)
validate-manifests (+ negative fixtures) 3 / 0
windows-compat · posix-compat 24 / 0 · 4 / 0
skill-triggering coverage 44 / 0 (was 28)
hook-timing 5 / 5 hooks, 930–2443 ms vs the 5000 ms budget
unit · hook-contracts 19 / 19 · 16 / 16
measure-context 6807 / 5153 chars, cap 12000
ShellCheck (--severity=warning, 68 files) exit 0
CI on the merged head 2664c77 4 / 4 green (ubuntu, macOS, Windows, lint)

The merge commit's tree was verified byte-identical to the reviewed branch tip. Every new check was verified red before green by mutation, including the non-vacuity guards.

Not run for this release: tests/skill-triggering/run-all.sh, the live-session tier of #6's eval. No skill description and no triggering prompt changed here — what changed is the structural guard around them — so its outcome is unaffected. Recorded rather than implied.


Full detail in CHANGELOG.md · PR #21

v0.17.1 — bound the doc-drift payload, close two gate blind spots

Choose a tag to compare

@Redtropig Redtropig released this 31 Jul 05:06
2d9a7e6

PATCH. Every entry repairs something that already shipped — nothing belongs in an ### Added section, which is what the versioning rule keys MINOR on.

All five findings are the same shape 0.17.0 shipped fixes for, turned on 0.17.0's own artefacts: a sensor whose silence was not coverage, and gates that had stopped covering what they claimed to.

Fixed

doc-drift-scan.sh flooded the consumer it reports to

HARD_CAP bounded the symbol set; nothing bounded the candidate rows, the unit the reader actually pays for. Measured on this repository's own v0.16.0..v0.17.0 range: 54 symbols → 4765 rows / 759 KB. The 3000-token SessionStart budget invariant is this repo's scale for what injected context may cost; 759 KB from one sensor is not in that world. Wherever the consuming tool's output limit falls, a payload that size is past it — so drift-analyst was adjudicating a list the harness had already cut, silently. The script announced PARTIAL for the one truncation it performed and was blind to the larger one it caused.

Rows are now capped per symbol (12) and in total (400), each with a PARTIAL marker, and the summary reports matched-vs-shown (K candidate(s), S shown).

doc-drift-scan.sh harvested comment prose as if it were code

Symbol extraction reads raw diff lines, comments included, so English containing word ( becomes a "symbol". Three of them — O (from O(files)), b (from the regex \b(${ALT})) and it (from prefixes it () — produced 3100 of those 4765 rows, 65% of the output, none naming any code symbol.

Tokens under three characters are no longer searched at all: matching is prefix-based and case-insensitive by design, so their rows are undecidable at any count. They are named individually on stderr — a symbol the scan chose not to look for must never read as one it looked for and found nothing about. The accepted common-word noise (read, get, check — real identifiers whose rows a reader can judge) is untouched.

Same range after: 379 rows / 70 KB (−91%), with every bound announced.

tests/windows-compat.sh stopped checking the newest hook two releases ago

Its HOOKS list was hard-coded and never learned about hooks/pre-compact (v0.15.0), so [1/5] (no bare python3), [3/5] (eol=lf) and [5/5] (portable.sh wiring) skipped it — and [1/5] has no other coverage anywhere in the suite. Hooks are now globbed, with a non-vacuity guard so an empty discovery fails loudly instead of passing every loop below it.

[3/5] also gains skills/using-harness-anchor/SKILL.md, awk-consumed by hooks/session-start and pinned eol=lf for that reason — a pin unverified since v0.14.0 added it, because this check is the only place in the repo that reads an eol attribute at all.

A hook registered in hooks.json but missing on disk was invisible to every gate

validate-anchor [1/12] names two of the five hooks; [11/12] enumerated all five by hand but only to check their wiring; hooks.json itself is validated for JSON syntax alone. [11/12] now derives its list from hooks.json and runs registry → disk. Complementary to windows-compat [5/5]'s disk → wiring; neither direction alone catches both failures.

Two troubleshooting entries gave pre-0.13.0 / pre-0.16.0 advice

The doc-align marker had sat at v0.9.0 through eight releases — two of which added entries to that very file — while its own text said "re-verify and bump this marker if they change". All 15 entries were re-checked against hooks/ and scripts/, every mechanically checkable claim run or grepped rather than read. Thirteen hold. Two did not:

  • #6 named a missing python3 as a cause of hook-contract FAILs. False since v0.13.0 — the engine chain is python3pythonpy -3node → narrow pure-bash, and an engine-less machine emits SKIP, never FAIL. It sent Windows readers, the exact people v0.13.0 was for, chasing an interpreter that cannot be the cause.
  • #5 told the reader to install a build tool on the strength of init.sh's command -v check — PATH-only, precisely the inference 0.16.0's discovery chain exists to prevent, in the guide meant to teach it.

The new marker states the scope of that verification instead of asserting a bare "verified". A scope-less positive is the same defect as a scope-less negative.

Changed

  • agents/drift-analyst.md reads the scan's stderr through a six-row state table (up from three prose states), including both cap markers and the never-searched token list. A clean doc-drift section now means "no doc claim about a changed, 3+ character, function-shaped symbol in a scanned language looks stale".
  • docs/troubleshooting.md gains entries 14 and 15, covering the two sensors 0.16.0/0.17.0 added — both built to report things that look like failures and are not.
  • tests/unit/doc-drift-scan.sh 22 → 32 assertions; tests/windows-compat.sh 19 → 24; scripts/validate-anchor.sh 157 → 158.

Verification

Run on main at 2d9a7e6 before tagging:

Gate Result
validate-anchor 158 / 0
validate-manifests 3 / 0
windows-compat 24 / 0
posix-compat 4 / 0
cpp-tool-discovery 15 / 0
doc-drift-scan 32 / 0
mandated-phrasing 9 / 0
check-coverage 28 / 0
SessionStart injection 6807 / 5153 chars, cap 12000
CI on the merged head 9 / 9 green (ubuntu, macOS, windows-latest, ShellCheck, CodeQL ×5)

Negative paths proven to fire, not assumed: empty hook discovery, missing registered hook file, unrecognised hooks.json command shape (5 declared / 4 parsed), zero parsed, and a doc-drift fixture that trips the length floor plus both caps at once — with an assertion pinning that all 40 fixture symbols survive capping, since a cap that amputated whole symbols would reintroduce the silent miss the script exists to prevent.

Full diff: v0.17.0...v0.17.1

v0.17.0 — negative assertions carry scope and shelf life

Choose a tag to compare

@Redtropig Redtropig released this 29 Jul 18:07
4a1d45f

The plugin's anti-hallucination contract bound only positive claims — "done", "fixed", "passing". A negative claim — "clang-tidy isn't installed here", "there's no such function" — was bound by nothing, and the skill's own description carried no negative trigger, so it was not even loaded at the moment it was needed.

This release makes the contract bidirectional: a claim that something is absent must state the scope it searched and the date it looked. It also fixes the two sensors that were making unverified negative claims of their own.

The two headline fixes

doc-drift-scan.sh could not see a single one of 0.16.0's own 21 changed files. It was pathspec-limited to C/C++ in a bash-and-markdown repository — and reported that by returning exactly what a clean scan returns: exit 0, empty stdout. "Did not scan" and "scanned, found nothing" shared a channel, which is how total blindness survived a whole release unnoticed.

Now it scans a maintained language whitelist (C/C++, Python, JS/TS, Go, Rust, Ruby, Java, Kotlin, C#, shell), and every early-exit path announces itself on stderr. stdout stays a pure candidate contract.

cpp-tool-discovery.sh searched versioned tools against a hard-coded ladder ending at 22 — a roughly twelve-month fuse. When LLVM 23 ships, an installed clang-tidy-23 would report NOT_FOUND, recreating the exact bug the script was written to prevent. A hard-coded version ceiling is itself an expiring negative assertion. Replaced with glob enumeration: no ladder, no fuse.

Performance

doc-drift-scan.sh, identical range (--base v0.16.0, 44 symbols × 53 docs, 4363 candidates), same machine:

wall clock
pre-release nested loop 5m44.427s
shipped 3.917s

Output byte-identical apart from a trailing colon the old IFS=: read silently truncated.

The first attempt regressed. Collapsing the search to one alternation grep per document left attribution spawning two greps per candidate line, and candidates outnumber symbol-file pairs — it went from an 8m25.794s baseline to a >10min timeout kill. What shipped replaces attribution with one awk pass per document. The CHANGELOG records that history rather than claiming a clean win.

Also in this release

  • Observation dates on negative capability conclusions at seven sites, plus tests/unit/mandated-phrasing.sh — the wording rule was held together by instruction alone through 0.16.0, whose own review found it already drifted.
  • init-verification re-checks inherited negative conclusions at session start. Only negative ones: those fail silently, while a stale positive fails loudly at the next invocation.
  • tests/README.md's "Quick test" block enumerated 7 of the 18 unit tests that exist — anyone following the documented steps ran under 40% of the suite while believing they had run it. Replaced with a glob, matching what CI already does.
  • Design invariant 8 restated bidirectionally, along with every other site carrying the one-directional form — including the meta-skill's Hard Rule 1, injected at every SessionStart.
  • A sweep for the set -u bare-local crash class across scripts/, hooks/, tests/, templates/. Zero instances beyond the one fixed here. Filed as a convention rather than a validate-anchor check, because neither grep nor shellcheck can decide it — a line-order scanner reports clean on code that crashes.

A defect this release shipped, and what caught it

The attribution stage handed awk its symbol list via -v syms="$CHUNK_SYMS". That value contains newlines, and POSIX does not permit a physical newline in a -v assignment — gawk --posix rejects it outright, and macOS's one-true-awk silently attributed nothing. Every symbol assertion failed on macos-latest and windows-latest while the same file reported 21/21 on the GNU-awk dev box and on ubuntu.

Every local check — the byte-for-byte parity diff, both timing runs, all RED/GREEN cycles — ran against a single awk. A green suite on one implementation was not evidence about any other, and nothing in six rounds of plan self-review, seven task reviews, or a whole-branch review could see it. CI was the only observer carrying that dimension.

Fixed by passing values through ENVIRON, which is verbatim. A portability assertion now re-runs the motivating case with awk forced into strict POSIX mode. Against the -v version it is the single failing assertion while the other 21 still pass — exactly the shape that let it through.

Known limitations

  • The new adversarial trigger prompt held 2/3, not 3/3. Read it with its baseline: the same prompt also triggered against the 0.16.0 description, which contained no negative keyword at all (N=1, PASS). So 2/3 is not evidence that adding negative triggers improved triggering — three runs per arm do not show it to be the description's keywords. Recorded beside the case in run-all.sh. The description change is justified on contract grounds, not on measured triggering.
  • Prefix matching is deliberately loose. cancel must match the prose word "Cancellation", so there is no trailing word boundary. On a common-word symbol this floods — 2840 candidates on this repo. Candidates are for a human or agent to judge, never verdicts.
  • Symbol extraction still only sees call/definition-shaped tokens. A changed global, macro, struct field, or a doc sentence naming no symbol at all remains invisible. Frozen as negative assertions in the unit test so a silent pass is never mistaken for coverage.
  • The contract binds only when the skill loads, and it loads from its description. A negative claim made in a turn where the description didn't match is unguarded. init-verification's session-start re-check is the second line of defence — one session late, which is not never, but is also not here.

Verification

Re-run on main at the tagged commit, not carried over from the branch:

suite result
validate-anchor 157
validate-manifests 3
cpp-tool-discovery unit 15
doc-drift-scan unit 22
mandated-phrasing unit 9
windows-compat 19
posix-compat 4
check-coverage 28
measure-context 6807 / 5153, both under the 12000 cap
CI-identical shellcheck gate clean, 66 files
CI 9/9 green

On-box acceptance, Windows + MSVC:

FOUND      clang-tidy    .../VC/Tools/Llvm/x64/bin/clang-tidy        vs-llvm
FOUND      clang-format  .../VC/Tools/Llvm/x64/bin/clang-format      vs-llvm
FOUND      ninja         .../CMake/Ninja/ninja                       vs-cmake
NOT_FOUND  cppcheck      searched:PATH,vs-llvm,vs-cmake,versioned

Full changelog: v0.16.0...v0.17.0

v0.16.0 — post-evaluation hardening: tool discovery + doc-drift detection

Choose a tag to compare

@Redtropig Redtropig released this 28 Jul 14:12
2bf8804

The first release driven by an end-to-end evaluation of the plugin itself. A C++ test project was built to be hard on an agent, an agent-under-test worked it with harness-anchor active, and the run was graded against a two-dimensional rubric (task outcome 40% / plugin-attributable behaviour 60%). It scored 91.2/100 with zero anti-hallucination violations — and the places it lost points are what this release fixes.

PATH is not proof of absence

The sharpest finding. The agent ran where clang-tidy, got nothing back, wrote "no clang-tidy/clang-format on this machine" into the project's AGENTS.md, and silently skipped three capabilities for the rest of the session.

The tools were installed. They ship bundled inside Visual Studio and simply aren't on PATH until vcvars runs. The same session had already hit this exact shape with cl.exe and correctly fallen back to vswhere — it owned the technique and didn't reapply it. All three zeros in the 39-row capability matrix trace back to that single moment.

New scripts/cpp-tool-discovery.sh resolves a tool through PATH and the platform's known install locations — VS-bundled LLVM and Ninja on Windows, keg-only Homebrew llvm on macOS, versioned /usr/lib/llvm-* on Linux. cpp-static-analysis, cpp-formatting and /cpp-init now route availability judgements through it, and are required to phrase absence as "searched PATH + <locations>, not found" rather than "not installed on this machine". The first is a falsifiable claim about a search; the second is an unfalsifiable claim about the world, and it tends to get written somewhere every later session reads.

The script is never silent: a missing tool still produces a line, and that line enumerates its own search scope.

Documentation that should have changed and didn't

/gc reported clean while a README line — "Cancellation is safe to call at any time" — had been made false by the very change under review, which taught cancel() to reject terminal-state jobs.

Two independent structural causes, where fixing either alone still misses it: the scan was bounded to changed files and the README was unchanged; and the doc-drift heuristic only matched docs referencing renamed or removed symbols, while cancel still existed with a changed contract.

New scripts/doc-drift-scan.sh reverse-associates the symbols a change touched to *.md lines mentioning them. It attributes body-only changes to the enclosing symbol via git diff -U0 hunk headers — necessary here, because cancel()'s signature never moved. drift-analyst now covers stale claims alongside dangling references, and states its own blind spot in its header so a clean section is not misread as "the docs were verified".

Also in this release

  • cpp-build-systems escalates to cpp-build-doctor after a second failed attempt at the same build failure, not only on "anything cryptic". Fixing a link error on the first try and moving on stays the expected path.
  • /anchor closes by recommending /cpp-init when the project is C/C++ and its config is missing. Being correctly described in a command list turned out not to mean being remembered at the right moment.
  • /cpp-init records resolved tools portably: a tool already on PATH keeps its bare name, since scripts/lint.sh and AGENTS.md are git-tracked and a machine-local path there breaks the next machine and CI.
  • Fixed: tests/skill-triggering/run-test.sh was missing --verbose, which newer claude CLI versions reject at argument-parse time — every triggering case had been failing identically regardless of content.
  • Fixed: doc-drift-scan reported a silent clean on main and on uncommitted work — the two most common /gc contexts.

Known limitations

Stated plainly, because a release about honest reporting should be honest about itself:

  • doc-drift-scan's pathspec is C/C++-only. It therefore cannot fire on the harness-anchor repository at all, which is bash and markdown. Its only execution witness is its own unit test on a synthetic fixture.
  • Four paths in that script still exit 0 with no output on either stream. The instances found were fixed; the class was not.
  • It has no watchdog. Measured 184 seconds on 160 symbols × 60 markdown files.
  • The LLVM version-suffix ladder is hard-coded through 22. On a box where only clang-tidy-23 exists off PATH, it would report NOT_FOUND — and the skill text upgrades that into a licence to call the tool unavailable.
  • The mandated phrasing is enforced by instruction, not mechanically. Nothing greps agent output.

All five are logged for 0.17.0.

Verification

validate-anchor 157/0 · cpp-tool-discovery 12/0 · doc-drift-scan 10/0 · windows-compat 19/0 · posix-compat 4/0 · check-coverage 27/0 · validate-manifests 3/0 · shellcheck --severity=warning clean — all re-run on main at the tagged commit.

Live behavioural proof. The new adversarial prompt was run against a real model with --plugin-dir pointed at this version, and the transcript shows the agent reasoning "where clang-tidy returning nothing doesn't actually prove the tool is absent, especially on Windows" — then invoking the discovery script and finding it. That is the evaluation's failure, reversed.

Full changelog: v0.15.0...v0.16.0

v0.15.0 — Session Pulse: runtime self-supervision

Choose a tag to compare

@Redtropig Redtropig released this 20 Jul 02:32
4152079

Runtime self-supervision for the harness, distilled from a study of From Concept to Production: Framework-Agnostic AI Agent Architecture Patterns (loop detectors, write-before-compact, progress checkpoints) and adapted to harness-anchor's warn-only / zero-dependency invariants.

Added

  • Session Pulse (PostToolUse fast lane, all tools). The Edit|Write matcher is removed; a pure-bash fast lane (zero JSON-engine spawns on the non-nudge path) keeps a per-(session, agent) sliding window and nudges on 3× identical calls or 3× same-tool failures. One nudge max per call, 10-call cooldown. The Edit/Write slow lane is behavior-compatible.
  • Two-stage context watermark. The v0.12.0 flush reminder migrates into the fast lane — fixing its own "fires only on Edit|Write" blind spot — and gains a T2 stage advising /session-end + a fresh session over automatic compaction.
  • Periodic feature checkpoint + out_of_scope. Every 25 calls the active feature and its new optional negative-scope list are re-surfaced, closing the agent-initiated-drift half of the observation-point gap (issue #6).
  • PreCompact forensics. New warn-only hook records .harness-anchor/last-compact.meta (trigger, transcript size, branch, dirty count, handoff age) and notifies the user when the handoff is stale; the SessionStart compact notice upgrades from generic caution to concrete recovery anchors. PreCompact has no agent-reaching channel — documented in CLAUDE.md invariant #1.
  • Evidence integrity. /verify gains a ### Integrity (tests-touched) section; /session-end's precheck scans state files for credential patterns before the commit offer (labels only — matched values never echoed) and offers [user]-protected golden-rules consolidation.

Changed

  • PostToolUse registration drops its Edit|Write matcher (fires on all tools); the JSON-engine pre-warm moves into the slow lane so the fast path stays spawn-free.

Verification

  • Local: validate-anchor.sh 151/151 · hook-contracts (incl. new post-tool-use-pulse.sh 29 asserts, pre-compact.sh 20 asserts) all pass · unit 15 files · posix-compat 4/4 · windows-compat 19/19 · injection budget within the 12000-char cap.
  • CI: 9/9 green on ubuntu / macOS / windows + ShellCheck + CodeQL.
  • Every new detector/hook carries an X/Y/blind-spot observation-point header (CLAUDE.md hook rule 5). Warn-only invariant intact: no deny/block/stopReason anywhere; subagent attribution verified against the official hooks docs (agent_id).

Full changelog: v0.14.0...v0.15.0

v0.14.0 — Cross-platform content modularization

Choose a tag to compare

@Redtropig Redtropig released this 17 Jul 17:18
b457131

Platform-specific content now has an open-ended, mechanism-stable home: methodology stays shared and single-source; platform detail loads only where it applies. Adding a platform = adding content (a sidecar file / a region / a table row) — never changing mechanism code. Scripts remain single-source bash with runtime branching (Git-Bash baseline, unchanged).

Added

  • Cross-platform content modularization. Two dedicated channels: skills/<skill>/platform/<os>.md sidecars for operational depth (loaded on demand behind an inline same-skill pointer; decision-shaping facts — availability, verdict rules — stay inline), and <!-- os-<name>-start/end --> regions in the injected meta-skill, mechanically dropped by SessionStart unless <name> matches the runtime HA_OS (fail-slim: unknown names never fatten the injection). First sidecar: cpp-sanitizers/platform/windows.md (substitute-tool preference table).
  • SessionStart banner Platform: line (HA_OS taxonomy: windows (Git-Bash) | darwin | linux; unknown pre-set values pass through verbatim).
  • ha_platform_init respects a pre-set HA_OS (tests inject platform states; users may override classification) — the Windows PATH shield stays keyed on the real uname.
  • validate-anchor: joint flat sequencing across both conditional-region families, os-name taxonomy whitelist, inert-marker detection outside the meta-skill, and [12/12] platform-sidecar ↔ SKILL.md pointer integrity (bidirectional).

Changed

  • cpp-sanitizers Windows substitute detail moved to platform/windows.md (SKILL.md keeps the availability matrix and the never-CLEAN verdict rule inline); /sanitize pointer updated accordingly. CLAUDE.md invariant #2 wording generalized to conditional regions; the skill-authoring rules gain the platform decision-weight split. The meta-skill is pinned eol=lf (it is awk-consumed by the injection filter).

Verification

All local suites green on Windows/Git-Bash (validate-anchor 148/0 incl. 4 new checks; 13/13 hook-contract suites with the slimming test extended to a {generic, cpp} × {HA_OS=windows, HA_OS=linux} matrix; 15/15 unit; windows-compat 19/0; hook-timing PASSED; adversarial marker probes rejected nested/mismatched/unclosed grammars with exact diagnostics). CI 9/9 on ubuntu / macos (BSD awk) / windows arms + ShellCheck. Generic injection grew by the Platform line only (+27 chars, platform-neutral measurement 6266/12000).

Full changelog: v0.13.0...v0.14.0

v0.13.0 — Windows support (Git-Bash baseline)

Choose a tag to compare

@Redtropig Redtropig released this 16 Jul 13:20
6694f05

Until now the hooks assumed a Unix world: a python3 on PATH, /-rooted walk-up loops, LF checkouts, and a find/sort/timeout that behave like GNU's. On Windows every one of those assumptions broke somewhere — the banner reported vunknown, C++ projects were typed generic, and a drive-letter path could spin a root-walk until the 5s watchdog killed it. v0.13.0 makes Git Bash a supported surface (design invariant #10): one shared platform layer, honest degradation when no JSON engine exists, and a windows-latest CI arm so it stays that way. All warn-only, zero new runtime dependencies.

Added

  • Windows support (Git-Bash baseline). scripts/lib/portable.sh — shared platform layer sourced by all four hooks and runtime scripts: JSON engine chain (python3 → python → py -3 → node → narrow pure-bash) with run-validated detection (immune to the Windows-Store python stub), C:\ path normalization at hook entry, fixed-point project-root walk (the old != "/" loop spun the 5s watchdog on drive-letter paths), Windows PATH shield (System32's incompatible find/sort/timeout lose to /usr/bin), portable mtime. Plugin-controlled formats (plugin.json version, cpp-detect output) parse even with ZERO engines — fixes the vunknown banner and C++-projects-typed-generic on Windows.
  • .gitattributes eol rules: every bash-consumed file checks out LF on all platforms (run-hook.cmd deliberately stays text=auto for the cmd/bash polyglot).
  • hooks/run-hook.cmd: %ProgramFiles%-based + user-scope Git discovery; WSL's System32\bash.exe excluded (hooks must see Windows paths).
  • C/C++ Windows counterpart mapping: sanitizer-build.sh.tpl turns detect_leaks off on MINGW*/MSYS*/CYGWIN* (LSan unsupported — same abort class as the v0.8.0 macOS incident); /sanitize reports TSan-on-Windows as INFRA-FAIL with substitutes; cpp-sanitizers gains Windows platform notes with a substitute-tool table (WSL2/Linux-CI TSan, Intel Inspector, Dr. Memory, CRT debug heap, UMDH, /RTC1); cpp-static-analysis gains Windows compile_commands/driver-mode notes.
  • tests/windows-compat.sh — static Windows invariants on every platform; new contract tests: session-start-engine-degradation.sh, post-tool-use-windows-paths.sh (Windows path bugs are string bugs — Linux CI catches them); windows-latest CI arm (curated core subset).

Changed

  • Hooks and status-report.sh/session-end-precheck.sh widen command -v python3 gates to the shared engine chain; the four duplicated escape_for_json copies collapse into ha_json_escape; hooks/stop staleness checks use mtime arithmetic instead of find -mmin (BSD/GNU/System32-neutral). Dev-surface scripts (validate-anchor / validate-manifests / measure-context) discover any python via ha_python; the test suite SKIPs honestly (visible, never silent) where an assert's engine is missing. CLAUDE.md gains design invariant #10 (Windows surface).

Verification: full suite green on Windows 11 / MINGW64 (13 hook-contract + 13 unit + windows-compat + posix-compat, 0 FAIL) and on all three CI arms — the first-ever windows-latest run passed in 2m38s (PR #15, 9/9 checks).

Full Changelog: v0.12.0...v0.13.0

v0.12.0 — durable-memory flush (write-at-realization)

Choose a tag to compare

@Redtropig Redtropig released this 14 Jul 19:26
a49587d

Durable memory — golden rules, decisions, milestones — used to be harvested at session end, exactly when compaction and attention dilution have already distorted the details or displaced the intent. v0.12.0 moves the write to the turn the signal appears (rough stubs with pasted evidence are legitimate; polishing is idempotent, loss is not) and backs the contract with three warn-only sentinels at the danger moments: context filling, just-compacted, and stopping. A full-plan sandbox rehearsal before execution also surfaced two hook-robustness hazards, fixed here: unbounded stdin capture could hang hooks for callers that hold stdin open, and post-tool-use's TERM watchdog cost every consumer ~5s while never actually killing a runaway main.

Added

  • Write-at-realization contract for durable memory: capturing-golden-rules now mandates
    capture in the turn the signal appears, with a legitimate rough-stub form (origin = pasted
    evidence at hand; Check defaults manual review → /gc's [MANUAL] tier); template note synced.
  • hooks/post-tool-use Check 1d — context-fill flush reminder: transcript-size threshold
    (6 MiB const), warn-once per session via a .harness-anchor/flush-warned-<session_id> marker;
    X-vs-Y blind spots documented in the check header.
  • hooks/session-startcompact caution line: fired with stdin source=="compact", the
    banner warns that memory-from-recall is unreliable and points at on-disk evidence; stdin read
    is -t 0-guarded (non-blocking), regular startups zero-increment.
  • Contract tests: post-tool-use-flush-sentinel.sh, session-start-compact-caution.sh.

Changed

  • hooks/stop — the stale-progress nudge now also reminds to flush chat-only durable memory;
    observation-point header documents the mtime-proxy blind spot (cannot see chat content).
  • hooks/post-tool-use — stdin capture bounded (1s read -t; callers holding stdin open no
    longer hang the hook) and watchdog hardened to the SIGKILL/stdio-detached idiom
    (session-start's v0.10.0 lesson): the TERM version added ~5s wall to every
    command-substitution consumer and could not actually kill a runaway main.
  • Same-turn flush cross-links: self-correction-loop (capture hop after a recurrent fix),
    context-budget-discipline (flush-before-compress ritual + rebuild-from-disk after
    compaction), feature-state-keeper (mid-session milestone progress.md prepends),
    /session-end step 7 reframed as the safety net rather than the capture moment.

v0.11.0 — mechanism scriptification

Choose a tag to compare

@Redtropig Redtropig released this 14 Jul 02:39
e253f95

The mechanical halves of harness-anchor's commands are now deterministic scripts. /status, /anchor, /cpp-init, and /session-end become thin wrappers — a unit-tested scripts/* helper does the gathering / template placement with byte-stable output, and the command markdown keeps only judgment and interaction. The golden-rules Check: tier is now actually executed (three-state: a broken check is surfaced, never counted clean), and "never silently overwrite" is enforced as interface shape: scaffold.sh's default path physically has no overwrite branch. Cheaper per invocation, reproducible per run, and the safety invariants moved from prompt discipline into interface shape.

Added

  • scripts/golden-rules-check.sh — mechanical Check runner for golden rules: parses ### GR-<n> blocks, executes the first backtick-quoted command per Check line (5s SIGKILL watchdog each, per-check isolation), three-state verdicts — CLEAN / FINDINGS(n) / CHECK-ERROR — so "found nothing" is never conflated with "didn't look"; --count feeds /status. Check convention documented in the golden-rules template + capturing-golden-rules skill: output = candidate violations, empty = clean, "manual review" in the line wins over backticks.
  • scripts/status-report.sh — the whole 7-section /status snapshot in one deterministic run (python3→node JSON engine chain; if both are missing only the JSON-derived lines degrade, the rest still reports; reuses toc-freshness.sh and golden-rules-check --count).
  • scripts/scaffold.sh — template placement for /anchor and /cpp-init (--cpp): placeholder substitution, chmod, skip-by-default for feature_list.json/golden-rules.md, conflicts (need decision) reporting, --render for diffs, --overwrite <allowlist> as the only write path over non-empty files — the default path physically has no overwrite branch.
  • scripts/session-end-precheck.sh — one-call fact block for /session-end: active feature + counts, init.sh under a 60s watchdog, state-archive dry-run + ledger-validate relays, two-column (state/source) tree scan, TOC structural-change hint (A/D/R + untracked, capped at 20).
  • validate-anchor [10/10] — the four mechanism scripts must be executable and parse, and every {CLAUDE_PLUGIN_ROOT}/scripts/* reference in commands/ + agents/ must resolve (thin wrappers made script paths a single point of failure); plus [9b] — template existence is now cross-checked against scaffold.sh's map (the old [9] check went silently empty once the command mds stopped naming template paths).
  • Unit suites for all four scripts (tier precedence, three-state verdicts incl. timeout, rerun byte-inertness by checksum, --overwrite allowlist, --render fidelity, cpp dotfile drops, refusal exit codes 3/4, engine-degradation via PATH shims, duplicate-id relay).

Changed

  • /status, /anchor, /cpp-init, /session-end are now thin wrappers over the scripts above — the script is the single source of truth for the mechanical half; the markdown keeps judgment and interaction (AskUserQuestion conflict round-trips, Default-FAIL flips, consent-gated archival, flywheel). Only read-only /status retains a manual degraded path. Saves the template/gathering round-trips through context (~6-18 tool calls → 1-2 per command) and pins the outputs byte-level.
  • drift-analyst runs the golden-rules mechanical tier via golden-rules-check.sh and keeps judgment: adjudicating FINDINGS lines (expected vs violation), reviewing MANUAL rules, surfacing CHECK-ERROR as a broken Check rather than a pass.

v0.10.0 — SessionStart injection slimming + 3000-token cap

Choose a tag to compare

@Redtropig Redtropig released this 06 Jul 07:17
2cf0fd0

MINOR release per SemVer: a new hook capability (cpp-gated, slimmed SessionStart injection with authoring-time flatness validation) plus a raised injection budget — design invariant #2 changes from ≤ 2000 to ≤ 3000 tokens (user-approved). The warn-only hook contract and JSON shape are unchanged; hooks still never write. No migration: existing projects benefit at the next session start (generic projects now spend ~39% of the cap instead of 94%, and the freed budget flows to the project-specific PROJECT-TOC view — repos up to ~150 files get the full file index injected). Re-running /anchor refreshes the scaffolded context-budget.md numbers. Also ships two long-standing hook fixes (watchdog SIGTERM deferral; quadratic JSON escaping) surfaced while chasing CI on the rescaled deep-repo fixture.

Added

  • cpp-gated, slimmed SessionStart injection. The meta-skill body is now injected as a
    pure filter of using-harness-anchor/SKILL.md: YAML frontmatter stripped, and
    <!-- cpp-only-start --> / <!-- cpp-only-end --> regions (the four cpp-* sibling
    skills, /cpp-init, /sanitize) dropped in non-C/C++ projects — catching invariant #5
    up at the injection layer. The file itself is untouched for the Skill-tool path. New
    contract test pins both modes plus the skip-leak guard; validate-anchor checks the
    regions are flat — sequenced, non-nested, closed (a nested pair would leak past the
    filter's single skip boolean with start/end counts still equal) — and that every
    cpp-only line is exactly one of the two markers.
  • measure-context.sh second pass on a bare generic fixture, so the generic fixed-cost
    baseline (the common case) is measured alongside the C/C++ e2e fixture.

Fixed

  • SessionStart watchdog: kills are now SIGKILL and the watchdog's stdio is detached.
    Measured on macOS bash 3.2: a subshell blocked on sleep 5 defers SIGTERM until the
    sleep completes, so (a) the parent's wait $watchdog_pid burned the full 5-second
    window on every session start even though main() finished in ~0.4s — any consumer
    waiting on the hook saw ~5s of wall per invocation — and (b) a genuinely runaway
    main() was never actually killed on that bash (the deferred TERM aborts the subshell
    before its kill line runs), leaving invariant #7's enforcement partly fictional.
    Exposed on CI by the rescaled 600-dir deep-repo fixture: slow runners pushed main()
    past 5s and the still-armed watchdog hard-killed it ("no output emitted"). Deep-repo
    hook wall: 5047ms → 502ms; the timeout contract (genuine overrun → silent, exit 0)
    re-verified at 5054ms.
  • SessionStart JSON escaping is O(n) via python3 (pure-bash fallback retained). The
    ${var//…} escaper is quadratic in matches×length: a ~12KB, ~700-line payload (a
    deep-repo directory map at the raised cap) burned the remaining watchdog window in
    this one step on pessimal macOS CI runners (~0.15s on a fast machine — which masked
    it). json.dumps is linear and also escapes control characters the bash path misses;
    environments without python3 keep the old escaper. Deep-repo hook wall: 502ms → 165ms.

Changed

  • Injection budget raised: ≤ 2000 → ≤ 3000 tokens (8000 → 12000 chars) — invariant #2.
    The old cap was 94% consumed and the squeeze fell entirely on the project-specific
    <project-toc> block (the banner never truncates). With the slimmed body a generic
    project's fixed cost drops to ~4.6KB (≈1160 tokens measured) and the TOC budget grows
    ~5×, so repos up to ~150 files get the full ## Files view instead of the degraded
    directory map. All cap reference points (hook, measure script, three test files,
    CLAUDE.md, README, both context-budget references) moved in lockstep; the deep-repo
    contract fixture rescaled 200→600 dirs so the degradation path stays exercised.
  • Meta-skill body compressed ~5.9KB → ~5.0KB — packaging only: rules, trigger keywords,
    read order, and command timing preserved item-for-item (contract-suite verified; a live
    three-scenario spot-check — scope-jump, TOC-before-Glob, no-done-without-evidence — gates
    the merge).