344 Commits

Author SHA1 Message Date
Claude 5240d0dcda test: expand coverage for context, quality, search, overnight, python
Add tests for previously untested surface area:
- internal/context: pure helpers for readiness classification, action
  mapping, display formatting, timestamp parsing, tmux target parsing,
  file-tail reading (23.9% -> 41.2%)
- internal/quality: golden signals math (linear regression, Gini coefficient,
  top-10/bottom ratio, citation counting, verdict computation) (15.3% -> 27.9%)
- internal/search: findings parsing (frontmatter, title extraction,
  freshness scoring), bead context resolution (env + cache + keywords),
  constraint index (load, save, lock, filter stale) (8.7% -> 22.4%)
- internal/overnight: loop helpers (validateRunLoopOptions, summary
  round-trips, snapshot partitioning, test fault injector hook) (77.4% -> 78%)
- internal/goals: platform-specific process-group tests for
  measure_unix.go (track/untrack/killAllChildren)
- tests/python: unittest-based tests for generate-context-shards.py
  covering is_binary, count_lines, units_for_file, pack_shards,
  verify_manifest, build_manifest (22 tests; runs under vanilla python3)
- tests/scripts: behavioral tests for ci-local-release.sh flag parsing
  and validation (17 bats cases)

https://claude.ai/code/session_01EMusoGMSUSbtPqkotJy3U7
2026-04-22 11:53:01 +00:00
Claude 0ed30f83fe test: expand coverage for lifecycle, goals, and rpi packages
Add tests targeting previously untested pure helpers:
- lifecycle: seed, temper, curate, dedup, defrag, maturity, feedback,
  task_sync (4.1% -> 71.3% statement coverage)
- goals: RunValidate, RunMigrate, RunPrune, RunInit, RunSteerAdd/Remove/
  Prioritize, DirectivesFromPillars, FindMissingPath, SplitCommaSeparated,
  DetectGates, AutoDetectTemplate, LoadTemplate, BuildInteractiveGoalFile
  (58.8% -> 84.6%)
- rpi: artifacts (classification, path safety, content type), complexity
  (scoring, classification), handoff (markdown parsing, dedup, truncation),
  evaluator (verdict logic, finding generation), cancel (signal parsing,
  PID filtering, process tree), cleanup (stale detection, state patching,
  worktree validation) (18.1% -> 40.5%)

https://claude.ai/code/session_01EMusoGMSUSbtPqkotJy3U7
2026-04-22 11:43:49 +00:00
Boden Fuller 1e073d8363 chore: purge archived AO↔Olympus bridge integration + prefer local Go in precommit hook
Olympus (ol) was the power-user daemon predecessor to AgentOps, archived
in strategic-direction.md. The bridge contracts (SPEC_ONLY, never
enforced), MemRL policy surface, OL quarantine constraints collector, and
supporting scripts/tests all referenced a now-dead integration. Purging
them removes ~2400 lines of stale surface area.

## Deleted files
- docs/ol-bridge-contracts.md
- docs/architecture/ao-olympus-ownership-matrix.md
- docs/plans/2026-02-13-ao-olympus-bridge-next-steps.md
- docs/contracts/memrl-policy-integration.md
- docs/contracts/memrl-policy.schema.json
- docs/contracts/memrl-policy.profile.example.json
- cli/cmd/ao/inject_ol_test.go
- skills/vibe/scripts/ol-validate.sh + codex mirror
- skills-codex/swarm/scripts/ol-ratchet.sh, ol-wave-loader.sh
- tests/ol-integration/ (entire directory)

## CLI code stripped
- internal/search/types.go: OLConstraint struct + InjectedKnowledge.OLConstraints field
- internal/search/inject_run.go: CollectOLConstraints, WriteConstraintsSection, quarantineRelPath const, all related trim/render logic
- cmd/ao/inject.go: gatherOLConstraints, collectOLConstraints wrapper, olConstraint type alias, init-time call site
- Package comments in provenance/storage/types/formatter updated "olympus" → "agentops"
- formatter/markdown.go: session tag "olympus" → "agentops" (+ test update)
- Tests: deleted TestCollectOLConstraints + TestInject_collectOLConstraints, stripped OL subtests from TestFormatKnowledgeMarkdown and TestInject_*, renamed "olympus" literal test data to "other-repo"
- rpi_phased_context.go: stripped "matches Olympus DefaultTimeout" and "Modeled on Olympus internal/git/worktree.go" comments

## Docs + skills stripped
- mkdocs.yml nav: 3 entries removed (AO-Olympus Matrix, MemRL Policy, OL-AO Bridge)
- docs/INDEX.md, contracts/index.md, architecture/index.md, index.md: cross-refs removed
- strategic-direction.md: retirement rationale compressed to one paragraph
- docs/context-packet.md, positioning/devops-for-vibe-coding.md, plans/README.md: OL-specific content removed
- skills/vibe/SKILL.md + codex mirror, swarm/SKILL.md: OL script refs removed
- skills/vibe/references/deep-checks.md, plan/references/planning-rules.md (+ codex), pre-mortem/references/council-fail-patterns.md (+ codex): OL rule/pattern entries removed
- skills-codex hashes regenerated (plan, pre-mortem, swarm, vibe)

## Precommit hook fix
- hooks/go-test-precommit.sh: prefer ~/.local/go/bin/go when present. Hooks
  don't inherit interactive-shell PATH, so apt-provided go-1.22 would
  otherwise override a user-local 1.26 install. Mirrors the zshrc pattern.

## Retained (historical / useful)
- strategic-direction.md: 1-paragraph archival note
- Historical release notes (docs/releases/*.md) — immutable past-release record
- cli/testdata/transcripts/real-2.4mb.jsonl — captured test fixture
- docs/brownian-ratchet.md ol-rg3p bead-ID refs — internal bead prefix, not Olympus

## Verification
- go build ./... && go vet ./... clean
- go test ./... clean except pre-existing TestGCBridgeVersion_Live panic
- scripts/docs-build.sh --check (mkdocs strict) clean
- scripts/check-contract-compatibility.sh passes
- scripts/regen-codex-hashes.sh run — 4 skills updated
2026-04-19 14:33:00 -04:00
Claude fc42b0aa63 fix(overnight): findings-router emits claim_status=available, not pending
tests/smoke-test.sh validates every next-work.jsonl entry against
docs/contracts/next-work.schema.md; the entry-level claim_status
enum is {available, in_progress, consumed}. Dream's
findings_router.go was opening brand-new entries with "pending",
which the schema doc explicitly says writers must emit as
"available" (§Lifecycle, §Enums). Today's Dream run appended such
a line, tripping smoke-test on PR #116.

- Flip the writer literal to "available".
- Patch the one offending line already on disk
  (.agents/rpi/next-work.jsonl line 56).
- Extend TestRouteFindings_EmitsSchemaCompliantEnums to assert the
  entry-level claim_status against a validNextWorkClaimStatuses
  set so this exact regression is caught at test time, not push
  time.

https://claude.ai/code/session_01FDsQrm2EbQ8ubcJcJren9N
2026-04-19 07:32:58 +00:00
Claude df095d89c9 test(quality): add direct unit tests for stale-refs helpers
Bumped direct coverage of CheckStaleReferences and CountUniqueFiles
from 0% to 100% by testing them against the quality-package API
rather than only through cmd/ao/doctor_test.go. Keeps regression
signal close to the function under test.

https://claude.ai/code/session_01FDsQrm2EbQ8ubcJcJren9N
2026-04-19 06:58:28 +00:00
Claude f6f03f1bf8 fix(quality): skip rename-doc lines in stale-refs scan
Lines documenting command renames (`ao old` → `ao new`) intentionally
mention the deprecated form; treat them as doc, not stale usage. Fixes
the recurring warn from ao doctor on docs/CHANGELOG.md.

https://claude.ai/code/session_01FDsQrm2EbQ8ubcJcJren9N
2026-04-19 06:41:34 +00:00
Bo e2e353e4f3 Merge pull request #114 from boshu2/nightly/2026-04-17
Nightly 2026-04-17 — 6 cycles, ASCII fast-path sweep + bootstrap-maturity JSONL fix
2026-04-17 08:12:42 -04:00
Claude 2da07bb0f2 fix(overnight): collapse critical/blocker severity to high for next-work v1.3
mapSeverity could return "critical" but the next-work v1.3 severity
enum is strictly {low, medium, high} (see
docs/contracts/next-work.schema.md §Enums → Severity). A finding
frontmatter with severity: critical or severity: blocker would have
produced a next-work.jsonl line that the pre-push contract parity
gate rejects.

Collapse "critical" and "blocker" to "high" — they represent the
strongest signal the next-work contract supports, so the mapping is
lossless at that ceiling.

Also extend the cycle-2 schema guard test to check severity, and add
TestMapSeverity_CollapsesCriticalToHigh to lock in the full mapping
table (low/minor, medium/significant, high/major/critical/blocker,
default, case-insensitive, whitespace-tolerant).
2026-04-17 06:48:23 +00:00
Claude bcd6c336ca test(overnight): build-time guard that findings router emits schema-compliant enums
Cycle 1 fixed the router's schema-invalid enum values; this adds a
guard test so the same drift cannot recur without a deliberate test
update.

TestRouteFindings_EmitsSchemaCompliantEnums hard-codes the Type and
Source enum sets from docs/contracts/next-work.schema.md and asserts
that RouteFindings emits values from them. If the schema doc genuinely
grows a new enum value, the test comment directs the reader to update
both the doc and this test together — same shape as other contract
parity checks in this repo.

Previously the only drift surface was validate-next-work-contract-parity.sh,
which runs at push time against the concrete .agents/rpi/next-work.jsonl
file. This test catches the drift at 'go test' time.
2026-04-17 06:44:16 +00:00
Claude 271d4de41d fix(overnight): emit valid next-work v1.3 enums in findings router
RouteFindings was emitting type="finding" and source="finding-router",
neither of which is in the next-work v1.3 enums (see
docs/contracts/next-work.schema.md). Every Dream run was tripping the
pre-push gate's next-work contract parity check, forcing an auto-revert.

Map the router output to the closest valid enums:
- type: "finding"         -> "tech-debt"
- source: "finding-router" -> "council-finding"

Original finding provenance is still carried in SourcePath and the
finding file's own frontmatter; no information is lost for downstream
consumers (triage, swarm, flywheel).

Test updated to assert the new enum values explicitly.
2026-04-17 06:36:42 +00:00
Claude cbeb606831 perf(cli): finish ASCII fast-path sweep across remaining truncate sites
Completes the sweep started in cycles 1-4. Applies the same byte-length
upper-bound fast-path to the four remaining rune-safe truncation sites:

- internal/search/search_cass.go TruncateContext
- internal/parser/parser.go Parser.truncate
- cmd/ao/contradict.go truncateSnippet
- cmd/ao/extract.go truncateForPrompt

All call sites now follow the same pattern: if len(s) <= max, return s
without allocating a []rune slice. Behavior is unchanged for inputs
whose byte length exceeds max (the rune-count branch still fires).
2026-04-17 02:58:14 +00:00
Claude 2d766145b6 perf(goals,pool): ASCII fast-path sweep for byte-length rune truncation
Applies the same fast-path introduced in cycles 1-3 to the remaining
call sites in goals/measure.go (truncateOutput) and pool/pool.go
(truncateAtWordBoundary). The existing measure.go comment explicitly
flagged the opportunity ("ASCII-only strings could use a len(s)
fast-path") — honoring that note.

Adds a multibyte regression case to truncateAtWordBoundary so the
rune-count fallback is exercised under the fast-path gate.
2026-04-17 02:51:46 +00:00
Claude 74dfab3a27 perf(search): ASCII fast-path for TruncateText
Third application of the byte-length-upper-bound fast path for
rune-safe truncation (after truncateForError and TruncateRunes).
When len(s) <= maxLen, the rune count cannot exceed maxLen either,
so the []rune allocation is skipped. Adds multibyte tests covering
all three branches: fast-path, rune-count fallback, truncation.
2026-04-17 02:50:33 +00:00
Claude 9a1e5c9f19 perf(rpi): ASCII fast-path for TruncateRunes
Same pattern as cycle 1 (cli/internal/parser/parser.go truncateForError):
byte length is an upper bound on rune count, so when len(s) <= cap we can
skip the []rune allocation. Adds a multibyte test that proves the
rune-count fallback still fires when byte length exceeds cap but rune
count fits, ensuring the fast-path does not change behavior.
2026-04-17 02:49:17 +00:00
Claude 8dca64ffb1 perf(parser): ASCII fast-path for truncateForError
When the input byte length already fits within maxLen, the rune count
cannot exceed it either (bytes >= runes). Short-circuit before the
[]rune allocation. Adds multibyte test cases proving both the fast-path
(bytes==runes), the rune-count fit case (bytes>maxLen, runes<=maxLen),
and the truncation case.

Closes finding: ag-yht / Add ASCII fast-path comment to truncate()
2026-04-17 02:45:14 +00:00
Boden Fuller 729eaf2783 feat(shellutil): sanitize bash invocations to bypass user shell aliases
Adds cli/internal/shellutil package with:
- SanitizedBashCommand(ctx, script) — bash --noprofile --norc -c with
  BASH_ENV and ENV stripped from the environment.
- SanitizedEnv(env) — exported for composition.

Wires two goals-package callsites through the helper:
- MeasureOne (cli/internal/goals/measure.go) — user-authored goal
  check strings run via bash -c and could be silently rewritten by
  aliases defined in BASH_ENV + shopt -s expand_aliases.
- AddGoal dry-run check (cli/internal/goals/commands.go).

New tests cover env filter correctness, flag shape, BASH_ENV stripping
from cmd.Env, context cancellation, and a regression guard that writes
a real BASH_ENV rcfile aliasing echo -> 'printf HIJACKED' and asserts
the alias does NOT leak into the sanitized command.

Consumes swarm-post-mortem-findings 'Sanitize agent shell environment
to bypass aliases'.
2026-04-15 15:53:15 -04:00
Boden Fuller 56c4462d77 fix(rpi-serve): relax --run-id flag pattern to accept legacy 8-hex
ao rpi serve --run-id <id> rejected legacy bare 8-hex run IDs (e.g.
0aa420a9, 3e298842) that still exist on disk at .agents/rpi/runs/<id>/,
forcing users to either pass them positionally (where they misroute
as goal strings and trigger a new orchestration instead of watching
the existing run) or not at all. Commit e8572ee8 (2026-03-05)
tightened ServeRunIDPattern to avoid false positives with short git
SHAs typed as positional goals, which was correct for the positional
branch but wrong for the explicit --run-id flag where user intent is
unambiguous.

Splits the regex:
- ServeRunIDPattern (positional, strict) — unchanged
- ExplicitServeRunIDPattern (flag path, permissive) — adds the
  8-hex form: ^(rpi-[a-f0-9]{8,12}|[a-f0-9]{8}|[a-f0-9]{12})$

ClassifyServeArg flag branch and ValidateExplicitServeRunID now use
the permissive pattern. Error message updated to mention <8 hex>.

New cli/internal/rpi/serve_test.go covers flag-vs-positional
asymmetry, numeric/slug/UUID/invalid shapes, and a proper-superset
invariant. Existing positional classifier test unchanged.

Consumes 'Fix rpi serve --run-id classification for run IDs'
from v2.9.3..HEAD epic.
2026-04-15 15:04:31 -04:00
Boden Fuller 0b03ce3907 fix(overnight): raise Dream Council runner cap to 180s + make configurable
Real overnight runs repeatedly logged 'claude council timed out after
1m30s' while Codex completed successfully. The Claude lane's cold-start
plus structured-output envelope routinely lands in the 90-150s band, so
the hard-coded 90s runner cap was structurally too tight.

- dreamCouncilRunnerTimeout default: 90s -> 180s (evidence-backed).
- withDreamCouncilRunnerTimeout now takes a per-run configured
  duration (zero = default); still clamps to parent deadline.
- overnightSettings.CouncilRunnerTimeout + new
  resolveDreamCouncilRunnerTimeout helper plumb the duration through.
- config.go: new DreamConfig.CouncilRunnerTimeout field
  (yaml: dream.council_runner_timeout) + merge wiring.

Adds 3 tests: config parse paths, default=180s invariant +
configured-wins + parent-deadline-wins, and a regression guard
asserting the runner lane deadline is >90s.

Consumes na-jox1 from .agents/rpi/next-work.jsonl (source_epic=na-22xi).
2026-04-15 14:50:26 -04:00
Boden Fuller 34647fc6fa fix(harvest): slim ExcludedCandidates JSON output (schema v2)
BuildCatalog previously serialized full Artifact structs (including
heavy Frontmatter map) for every excluded candidate. With >100
exclusions the JSON ballooned. Projects to ExcludedArtifactMeta
{id, title, confidence} on serialization; keeps excludedFull
in-memory for TopExcludedNearMiss(). Bumps CatalogSchemaVersion to 2.

BREAKING: catalog JSON consumers reading excluded_candidates.*
fields beyond id/title/confidence must update. Verified no
in-repo consumers affected.
2026-04-15 13:46:18 -04:00
Boden Fuller dbd5ed3ad5 fix(config): surface YAML parse errors to stderr
Invalid YAML in ~/.agentops/config.yaml previously fell through silently
to auto-detect. Now warns 'Warning: invalid YAML in <path>: <err>' and
still falls back (backward compat). Missing files remain silent.
Adds TestLoadConfig_SurfacesYAMLErrorToStderr.
2026-04-15 13:46:14 -04:00
Boden Fuller fd919b715c feat(compile): runtime preference in ~/.agentops/config.yaml (na-pmx1.9)
Users who have 'claude' on PATH but prefer Ollama (for privacy, cost,
or latency) were forced to export AGENTOPS_COMPILE_RUNTIME=ollama in
every shell. Add a persistent preference:

    ~/.agentops/config.yaml
    compile:
      preferred_runtime: ollama

Precedence (high → low):
  1. --runtime flag
  2. AGENTOPS_COMPILE_RUNTIME env var
  3. compile.preferred_runtime config
  4. auto-detect (claude binary → claude-cli)
  5. empty (preflight error)

Tests cover all 5 precedence levels including a graceful fallback
when the config load itself errors.

Closes na-pmx1.9.
2026-04-15 12:52:31 -04:00
Boden Fuller 59104040a1 feat(harvest): surface min-confidence exclusion count + top-5 near-miss
ao harvest previously reported only "Promoted N artifacts" and silently
dropped every deduped winner that fell below --min-confidence. Users
could not see how much signal the threshold was cutting or whether a
slight tune (0.5 → 0.45) would materially change the promotion set.

Catalog now carries ExcludedCandidates (sorted by confidence desc).
outputHarvestCatalog prints:

    Excluded N candidate(s) under confidence threshold 0.50
      Top near-miss candidates (closest to threshold):
        0.48  promotable-learning-one  (learning)
        ...

Tests:
- TestBuildCatalog_TracksExcludedCandidates: sort order, n-bounds, nil on n<=0

Closes na-pmx1.10.
2026-04-15 12:44:14 -04:00
Boden Fuller c9134d1aaf test(harvest): update malformed fixtures after salvage pass
The salvage pass added in the previous commit rescues frontmatter where
a scalar value contains ": " mid-string. Two tests that used that exact
shape as their "malformed" fixture now correctly succeed at extraction.

Switch those fixtures to tab-indented YAML, which is genuinely
unparseable and salvage-proof, so the "continues after malformed
frontmatter" and "malformed file becomes warning" assertions still
exercise the failure path.

Follow-up to na-pmx1.2.
2026-04-15 12:36:10 -04:00
Boden Fuller 8061c5965a fix(harvest): salvage frontmatter with mid-value colons
Adds a retry pass to parseFrontmatter that quotes top-level scalar values
when the initial yaml.Unmarshal fails. Root cause on 2026-04-15:
unquoted `description:` or `name:` scalars containing a literal ": "
(like "Solution: layer the CA bundle") are read by yaml as nested
mappings and break the parse. Users had to hand-fix the file before
harvest would re-scan without warnings.

The salvage pass only rewrites lines at column 0 whose value is a plain
scalar (not already quoted, not a sequence/map marker, not a block
header), so valid documents are untouched.

Tests:
- TestParseFrontmatter_SalvagesMidValueColons (3 real-world fixtures)
- TestParseFrontmatter_ValidDocIsUnchanged (false-positive guard)

Closes na-pmx1.2.
2026-04-15 12:32:30 -04:00
Boden Fuller a01235a3a8 feat(dream): add adaptive long-haul corroboration 2026-04-14 23:12:36 -04:00
Boden Fuller 8df916ed7f feat(dream): add yield telemetry to reports 2026-04-14 22:37:00 -04:00
Boden Fuller 4d9f0a110b feat(dream): emit actionable morning packets 2026-04-14 20:53:15 -04:00
Boden Fuller de19895c4f fix(dream): harden overnight council execution 2026-04-14 19:54:59 -04:00
Boden Fuller d837104c06 test: refactor project config resolve 2026-04-13 23:39:52 -04:00
Boden Fuller 559b00559e test: refactor ratchet promotion validation 2026-04-13 22:18:46 -04:00
Boden Fuller 3ccc2a3494 test: refactor codex parser edge cases 2026-04-13 21:01:45 -04:00
Boden Fuller eaa7d4a80a test: refactor failed summary contract test 2026-04-13 20:24:14 -04:00
Boden Fuller c9d7c03b0b test: refactor overnight corpus quality loop 2026-04-13 20:13:19 -04:00
Boden Fuller 3cf1525811 test: refactor live tree hash invariant checks 2026-04-13 20:03:52 -04:00
Boden Fuller 11dcff33c4 test: refactor pool chain retrieval checks 2026-04-13 19:52:44 -04:00
Boden Fuller 0f829a2acc test: refactor home config resolution checks 2026-04-13 19:42:58 -04:00
Boden Fuller 2a7042a45d test: refactor memrl policy errors 2026-04-13 18:35:51 -04:00
Boden Fuller 0ee3802f73 test(goals): split adversarial markdown checks 2026-04-13 17:13:50 -04:00
Boden Fuller 05128a00ab test(overnight): reduce state-machine fixture size 2026-04-13 16:30:11 -04:00
Boden Fuller cbd5b60a59 test(cli): reduce cmd ao race runtime 2026-04-13 16:14:29 -04:00
Boden Fuller 66088ba7f1 perf(overnight): add darwin checkpoint clone fast path 2026-04-13 14:50:08 -04:00
Boden Fuller fd52245b61 fix(gc): validate bridge json contracts 2026-04-13 12:04:08 -04:00
Boden Fuller f603b589f8 fix(rpi): record mixed-mode provenance 2026-04-12 23:11:11 -04:00
Boden Fuller c87eed8215 fix(inject): dedupe global learnings by content 2026-04-12 21:46:42 -04:00
Boden Fuller 38a7cc52ee feat(wiki): formalize .agents/ as Karpathy-style knowledge wiki
Adds the three missing pieces that turn .agents/ into a proper
LLM wiki following the Karpathy pattern:

1. .agents/INDEX.md — canonical discovery surface cataloging all 202
   learnings, plans, research, council reports, and wiki pages with
   wikilinks. Auto-generated by scripts/generate-index.sh.

2. .agents/LOG.md — append-only chronological operation log. Entries
   formatted as: YYYY-MM-DD HH:MM | actor | VERB | subject | [[wikilink]]

3. .agents/wiki/ directory structure:
   - wiki/sources/ — tier 1 LLM session summaries (forge output)
   - wiki/synthesis/ — cross-session insights
   - wiki/concepts/ — promoted concept pages
   - wiki/entities/ — promoted entity pages

Integration:
  - ao search now scans wiki/sources/, wiki/synthesis/, wiki/concepts/
    (3 new appendKnowledgeMarkdownSearch calls in search.go)
  - ao forge --tier=1 writes to .agents/wiki/sources/ (was ao/sessions/)
  - Dream post-loop hook + evolve dream sub-cycle write to wiki/sources/
  - forge_tier1.go appends to LOG.md and INDEX.md after each session page
  - wiki_log.go: AppendToLog + AppendToIndex helpers for all ingest paths

Obsidian-ready: open .agents/ as vault, INDEX.md is the home page,
wikilinks resolve to page navigation, graph view shows connections.
2026-04-12 20:16:41 -04:00
Boden Fuller 1835d77057 feat(search): include Dream vault sources 2026-04-12 11:12:36 -04:00
Boden Fuller 683fce3450 test(vibecheck): cover polluted git discovery env 2026-04-12 10:41:41 -04:00
Boden Fuller 4405fddccb feat(forge): use JSON Ollama tier1 output 2026-04-12 10:33:03 -04:00
Boden Fuller 7ff69d9c37 fix(forge): render prompts without fmt verbs 2026-04-12 10:30:20 -04:00
Boden Fuller be45214dc6 fix(dream): add hard iteration cap + batch stale marker recovery
Two fixes for the 20260411T031037Z runaway Dream run that created
22K+ commit markers:

1. Hard safety cap of 100 iterations in RunLoop.shouldHalt(), even
   when --max-iterations=0 (budget-bounded). This prevents iteration
   count from going unbounded if the budget/timeout check fails to
   fire. The cap is intentionally generous (typical runs use 1-5
   iterations) but prevents the 10K+ explosion observed.

2. Batch recovery log: when recoverOvernightStart finds >20 stale
   items, it summarizes as a single degraded entry ("cleaned up N
   stale items") instead of appending one entry per action. This
   prevents the 22K-entry degraded array that made the summary.json
   44KB and obscured real issues.
2026-04-12 09:57:17 -04:00