Skip to content

v5.0.0

Choose a tag to compare

@Kanevry Kanevry released this 13 Sep 12:46
· 615 commits to main since this release

Migration from 4.x: v5 migration guide. The major version reflects the changed readCurrentStatus() return contract and the now-enabled default close-time discovery scan.

Added

  • orchestrator.issue_budget.refunded event (#1353) — hooks/post-bash-issue-budget-refund.mjs emitted only a stderr line, which under exit 0 reaches the debug log alone, so refunds were uncountable after the fact (HR-105). The event fires once per refund DECISION including the no-op branches, so a census has a denominator; reason is a closed enum, and the payload carries no command text, issue title or path. Lazily imported, awaited and caught — a failing emit can never change the hook's exit code.

  • scripts/lib/git-porcelain.mjs (#1355) — one git status --porcelain -z parser for the three consumers that each had their own (quality-gate.mjs's private copy, project-hygiene.mjs's inline loop, pre-dispatch-check.mjs's hand-written unquoting). Malformed-entry drop and the R/C rename double-field rule now live in one place.

  • sizingSubject() + session_profile on the session record (#1247) — see below.

  • Time-bounded operations route in session-start: explicit deadline, scoped accounts and products, rolling queue, a single publisher, and provider read-back evidence. Uses the current harness; installs no scheduler or new receipt engine.

  • withFileLock(lockPath, fn, { onRelease }) in scripts/lib/file-lock.mjs (#1336): an optional, throw-safe callback receives the releaseFileLock result; the return shape of withFileLock is unchanged. scripts/lib/vault-status/board-lock.mjs forwards it as onReleaseOutcome, and orchestrator.vault.board_written now carries lock.release: 'not-owner' | 'busy' when the lease expired mid-write or the release gave up.

  • checkCiStatus({ sha }) in scripts/lib/ci-status-banner.mjs (#1332): an optional full 40/64-hex commit id (validated before any CLI spawns) replaces the internal getHeadSha for GitLab and GitHub; without it behaviour is unchanged. The ci-status session-start probe uses it through a new optional registry followUp step: on no-pipeline-for-head-sha / no-check-runs-for-head, and only behind SO_PROBES_INCLUDE_NETWORK, it renders the pushed commit's verdict (last pushed: <sha8> — CI <status> (#<pipeline>)). The follow-up runs inside the probe's own budget race; if it throws or overruns, the HEAD reading already delivered stands (follow_up: 'threw' | 'budget-exceeded' in orchestrator.probes.completed). The GitLab re-query is skipped when the pushed SHA equals HEAD.

  • isCoordinatorDirectHousekeeping(record) in scripts/lib/session-schema/filters.mjs (#1321) — the one definition of the coordinator-direct housekeeping record shape (every wave role: 'Housekeeping' with coordinator_direct: true), used by the eval engine and cited by the session-end writer rule and /evolve analyze.

  • Budget pre-flight in the reconcile writer (scripts/lib/reconcile/writer.mjs, #1316): before writing, writeApprovedRules projects the instruction budget with the guard's own computeInstructionBudget on a temporary copy of .claude/rules/ plus the pending writes. If an axis would end over its ceiling and the batch grows it, nothing is written, no candidate is stamped, and the result carries ok: false, reason: 'instruction-budget-exceeded' (BUDGET_REFUSAL_REASON) with a hint to consolidate into a thematic file. tests/rules/receiving-review.test.mjs stays the second safety net.

  • Campaign film embedded on site/index.html and site/de/index.html, inside the #loop section (#1306): <figure class="fig film"> with poster, preload="none", visible controls (the film carries sound, unlike the silent hero loop), no autoplay, the webm source before the mp4 source, and an AI-disclosure figcaption. site/assets/ui.js's hero-loop <video> now reads its filename from that element's data-video-base attribute instead of a hardcoded agent-production-loop literal — needed once a second <video> exists on the same page.

  • claude-md-drift-check Check 9 (rule-scoping) gained a sixth probe, fleet-intent-glob, and a third JSON output category, notes[]: a rule can declare a globs: pattern as intentionally zero-match here via its own fleet-intent-globs: frontmatter key, and the checker reports it in notes[] instead of warning on it. .claude/rules/testing.md now declares this for **/*Tests* (#445) — on 2026-09-09 the unqualified zero-match warning had led an agent to delete that pattern (reverted only because a pinning test caught it). Measured against the live rule corpus: 37 warnings → 36 warnings + 1 note (total findings unchanged).

  • scripts/lib/scope-echo.mjs + CLI + orchestrator.wave_dispatch.scope_echo_checked event (#1092) — soft receive-side signal that a dispatched agent echoed its declared file-scope back in its report; a named ceiling, never blocking. Additive applicable: false + reason: 'scope-empty' payload keys fire for an EMPTY declared scope, so consumers filter that never-instructed population out of the echo rate; the CLI gained --help and now exits 1 with a stderr WARN on an unreadable scope file instead of failing silently.

  • native_source + resume_linkage fields on orchestrator.session.started (#1091) — measurement only, answers whether Claude Code preserves the raw session_id across a native resume instead of guessing.

  • references/ splits of the bootstrap/discovery/evolve/session-plan/wave-executor SKILL.md bodies (#1246) — byte-identical extraction; line counts 641→444, 571→404, 720→264, 620→478, 541→414.

  • The optional tmux agent-status pane (Pane 5, #565) renders through that reader instead of jq-ing the cache file (#1342): buildStatusPaneRenderCommand() in scripts/lib/tmux-layout/layouts.mjs is exported so a test can run the render once instead of the 2 s poll loop, and its header line carries source=…, at=… plus TEXT markers (⚠, STALE, DEGRADED: <reasons>), never colour alone. Only source=absent (no ledger, no cache yet — the normal fresh-repo state) is deliberately unmarked (host-resources.md HR-101); every degraded reason on any other source renders DEGRADED:.

  • hooks/post-bash-issue-budget-refund.mjs (PostToolUseFailure, wired in hooks/hooks.json and hooks/hooks-cursor.json) plus refundBooking() in scripts/lib/issue-budget.mjs (#1347): a gh/glab issue create that fails gives its budget slot back, so the retry does not spend a second one of issue-budget.max-per-session. A refund requires PROOF OF CHARGE: chargeIssueBudget (called by hooks/pre-bash-issue-budget.mjs with the payload's tool_use_id + statement index) records every counter-incrementing charge in the persisted charged[] ledger ({id, key, unit, at}, bookingId() = <tool_use_id>#<index>, bookingKey() = hash of session|command|index, CHARGED_LEDGER_MAX = 64, eviction fail-closed) and refundBooking refunds only a matched record, removes it, and decrements the unit it names (count or exempt). A create the pre-hook PARKED (#1314) or blocked was never charged and is never refunded, however often it is retried; overflow[] is never touched; count/exempt never go below 0. The hook refunds only when the failure is attributable to the create itself — statementsCoverWholeCommand() (hooks/_lib/vcs-create-matcher.mjs) must hold, so glab issue create … && false is chain-not-attributable and refunds nothing (the review's cap-drain exploit). isFailure() reads fields only (exit_code !== 0, non-empty error, is_error at top level or in tool_response), never the bare event name; on the Cursor bridge (scripts/lib/cursor-hook-bridge.mjs) the only reachable failure field is top-level is_error, and a failure event without any field is a loud no-op (stderr WARN).

  • hooks/enforce-scope.mjs Gate 5c (#1295): this repo's Claude Code auto-memory directory (~/.claude/projects/<encoded-repo-path>/memory/, encoded via the one repo encoder encodeProjectDir()) is always writable — for EVERY agent of the session, coordinator and dispatched subagents alike, because the hook cannot distinguish them (tracked as #1352, priority::high) — evaluated inside Gate 6's out-of-root branch. A sibling repo's memory directory and every other out-of-repo path stay denied, and each allow emits one orchestrator.scope.memory_dir_allowed event so the carveout is countable. Before this, every wave manifest blocked the coordinator's own memory writes until it was torn down (six sessions in a consumer repo re-documented the same workaround, bewerbungs-assistent#307).

Changed

  • hooks/enforce-scope.mjs classifyCaller distinguishes a PRESENT-but-unusable agent_id (number, object, array, blank string) as 'malformed' rather than folding it into 'absent' (#1352, review follow-up). The fail-open posture is unchanged — fail-closed would deny the coordinator's own /close memory writes — but the ledger can now tell a marker that arrived broken from a genuine main-thread call, which is what the HR-105 countability claim rested on.

  • The orchestrator.issue_budget.refunded emits now all sit behind the mode: off check (#1353, review follow-up). Two of the four fired before it and so contradicted the contract written into docs/events-schema.md in the same change: a repo with the feature off contributed records to a census it can never supply a numerator for. Three enum values (no-signal, not-charged, unit: 'exempt') were emitted, reached by the suite and asserted by nothing — each is now pinned by a test proven red under mutation.

  • scripts/emit-session.mjs adopts session_profile only when the STATE.md frontmatter session matches the record's session_id (#1247, review follow-up), and omits the field with a stderr WARN otherwise. Without the check, two sessions sharing one working copy cross-contaminate: a plain deep close reads a concurrent ultradeep session's STATE.md and files its wave numbers under the ultradeep sizing row — the contamination #1247 exists to remove, inverted. The same block is now fail-safe: an unreadable STATE.md (EISDIR/EACCES) previously escaped to the top-level handler and exited 2 having appended NO record at all, letting an optional enrichment read decide whether the ledger got a line.

  • claude-md-drift-check Check 6's unknown-local-key warning states only what it measured (#1356, review follow-up): the key's absence from the template's two ## Session Config blocks, that population named inline. It previously also claimed the key was documented nowhere and that nothing read it — both refuted by measurement for all three keys it fires on, each of which is documented in the template's own sections and read at runtime.

  • hooks/enforce-scope.mjs Gate 5c now grants the auto-memory carve-out to the COORDINATOR only (#1352). The discriminator is the PreToolUse payload's agent_id, measured against the shipped Claude Code binary rather than assumed — its own schema states the field is present only inside a subagent and absent on the main thread. A dispatched wave agent (including Discovery under allowedPaths: []) now falls through to the normal gates, which denies it. Ambiguity stays fail-OPEN — fail-closed would deny the coordinator's own /close memory writes — and rides on the existing orchestrator.scope.memory_dir_allowed event as discriminator, so the fail-open case is countable.

  • scripts/lib/pre-dispatch-check.mjs reads git status with -z (#1354). Its hand-written unquoting handled only \" and \\, so an untracked path carrying a TAB or a non-ASCII byte (git's octal C-quoting) resolved to a non-existent path and the overlap guard silently passed. Both call sites in the file were on the same defect and both are fixed.

  • claude-md-drift-check Check 6 routes opt-in session-config gaps to notes[] instead of warnings[] (#1356). All 36 live parity warnings in this repo were unactionable "not required" entries; after the split the live run reports 3 warnings — genuine template lag for auto-skill-dispatch, wave-reviewers and issue-budget — and 37 notes. Mandatory-key gaps stay errors[]. Category separation, not a threshold change.

  • scripts/lib/locks/state-md-lock.mjs and staging-fence-lock.mjs release through releaseFileLock (#1349) instead of each carrying its own read-check-unlink sequence (net −60 production lines). Both gain the .acquire sibling guard from #1285: a release that races a concurrent takeover now answers busy and leaves the lock intact, where the old code unlinked it and reported success. Not a security repair — both use staleCheck: 'pid', under which the takeover race is unreachable; the revisit trigger is the first of them moving to a lease mode.

  • readStdin in scripts/lib/io.mjs accepts an injectable timeout (#1358), replacing a permanently skipped test with an empty body that asserted nothing. The 5 s default and the public signature are unchanged.

  • Probe work-time attribution is tested with synchronized invocation windows and a controlled clock (#1360). This removes scheduler-dependent duration assertions while retaining the regression check and the original 50 ms budget. Vitest worker fanout is bounded to reduce contention from integration child processes.

  • BREAKING (deep-import surface, scripts/lib/agent-status.mjs): provenance on the agent-status reader (#1342) — readCurrentStatus() no longer returns the bare map. agent-status.jsonl is documented as the source of truth and agent-status-current.json as its rebuildable cache. rebuildCurrentFromLedger({ repoRoot, maxBytes }) folds a bounded ledger tail (DEFAULT_REBUILD_MAX_BYTES = 256 KiB — named ceiling with a revisit trigger) into the same map shape, one entry per agentId won by the greatest parsable ts, marking each binding: 'bound' | 'legacy' | 'unknown' so an older session's record can never overwrite a newer one. readCurrentStatus() now returns { entries, source: 'live-map' | 'rebuilt-log' | 'stale-cache' | 'absent', at, degraded? } instead of the bare map, folding PER agentId (cache vs ledger, newer ts wins; equal-ms ties retain the cache only when its ledger payload agrees) — a global newest-timestamp comparison would have let one sibling's later successful push mask another agent's lost map write, which is the #1342 case itself (review finding, fake-regression-proven). absent means neither ledger nor cache exists; live-map means every entry was verified against the ledger. Entries live in null-prototype maps and __proto__/constructor/prototype agent ids are rejected as unbound records. readCurrentStatusEntries() is the backwards-compatible bare-map accessor.

  • discovery-on-close now defaults to true when the key is absent and accepts auto (= true) (#1340). Consumer repos without the key now run the close-time discovery scan — one extra Explore agent per /close, and critical/high findings count against the issue budget; set discovery-on-close: false to keep the previous behaviour. Before this, the parser hard-defaulted to false (the 2026-07-29 doc switch to true never reached code) and the template's recommended auto made the whole Session Config parse throw.

  • project-hygiene's ignored-ballast check runs one du per 500 paths instead of one per path (#1317): checkProjectHygiene ~773 ms → ~142 ms on this repo (298 ignored entries), findings unchanged; a partial du failure still parses the surviving output.

  • The path-scoped instruction-budget axis now counts toward the aggregate verdict (#1316): overBudget ORs in overPathScopedBudget, and a path-scoped breach alone raises the session-start banner. The 124,000 B ceiling is unchanged — a replay over 90 recorded states of .claude/rules/ showed it fires on 5/90 (5.6 %), while the peak + 2.5 % method used for the generated axis would have been an unfalsifiable raise (the peak was HEAD). Before folding, the 8 generated rule files were condensed prose-only (every provenance pair, heading, evidence line and frontmatter unchanged; alreadyMaterialized identical): bySurface.generated 99,774 → 75,130 B, bySurface.pathScoped 147,407 → 122,763 B (headroom 1,237 B). A review-panel pass restored five qualifiers the condensation had weakened.