BuildPhaseContext and JoinVerdicts iterated the verdicts map directly,
producing non-deterministic output across runs. Sort keys before
rendering so prompt injection and status logs are reproducible.
Closes council finding W-7 (context-orchestration-leverage batch). The
related buildHandoffContext path was already fixed to delegate to
FormatVerdicts (which sorts); this completes the remaining helpers.
- cli/internal/rpi/status.go: sort keys in JoinVerdicts
- cli/internal/rpi/phased_context.go: sort keys in BuildPhaseContext
- tests: replace "ordering not deterministic" waivers with exact-order
asserts and add a 50-run stability check
https://claude.ai/code/session_01Qw4bZvXUNuLs8Rp1j1homB
Co-authored-by: Claude <noreply@anthropic.com>
Commit ed35c47f added `docs` and `chore` to the script's VALID_TYPES,
but tests/scripts/check-next-work-schema-rows.bats:42 still used
`type:"docs"` as its supposed-to-be-rejected example, so the test
now passes through the validator — `bats-tests` has been red on
main since.
Switch the rejected-type example to `finding`, which is still
outside the enum and is also the canonical example cited in the
script's own header comment ("type=finding ... caught at push time").
Local: 11/11 in tests/scripts/check-next-work-schema-rows.bats pass.
Add f-2026-04-26-001 (propagation-gap) and f-2026-04-26-002
(normalize-rewrite-trap) to the registry. Both surfaced during pre-mortem
review of RFC 0001 Proposal 2 and were absorbed into Wave 1's
implementation (commit ac3ebbb2). Persisted now so future plans /
pre-mortems can match against them.
f-2026-04-26-001: pipeline field added to a candidate struct may be
silently dropped between intermediate emit struct and final serialized
output when the passthrough copier and intermediate JSON struct aren't
updated in lockstep with the source struct. Detection question: did you
trace the field all the way to the final serialized artifact and assert
its presence in an L2 test?
f-2026-04-26-002: normalize/canonicalize functions that hard-code a single
prefix list and force-prepend the legacy prefix to any unprefixed input
will silently rewrite a NEW legitimate prefix into a malformed double-
prefixed key when a second namespace is introduced. Detection question:
when introducing a new namespace prefix, are there normalize functions
that hard-code the OLD prefix list and need widening?
Both have ttl_days=60 (above the default 30) since they're architectural
patterns expected to apply across multiple future slices. Confidence:
high (each has a live regression-guard test in the same commit history
that produced them).
Surface lane-specific throughput on IngestResult and ingestSummary so the
morning digest can show external-watchlist yield without parsing every
sidecar. Update RFC 0001 Decision Record (Proposal 2 first slice landed)
and dream-run-contract.md (name both registered generators).
Changes:
- cli/internal/overnight/ingest.go: add IngestResult.ExternalWatchlistEmitted
field; recordFindingGeneratorResult increments it from sidecar.NewCandidateCount
when the sidecar's Generator name matches externalWatchlistGeneratorName.
Mirrors the pattern used for mine-findings (MineFindingsNew).
- cli/internal/overnight/loop.go: ingestSummary surfaces the counter under
"external_watchlist_emitted".
- cli/internal/overnight/loop_test.go: extend TestIngestSummary to cover the
new field — populates IngestResult with ExternalWatchlistEmitted=3 and
asserts the summary key is present, value matches, and total key count
rises 11 -> 12.
- docs/rfcs/0001-finding-generator-parallelism.md: bump status to "Proposal 1
+ Proposal 2 first slice implemented"; rewrite Decision Ask #3 from
deferred to landed with file:line citations for the generator,
registration, dedup_key fix, PROGRAM.md scope, and fitness counter.
Network fetch / dynamic dedup / goal_weight remain explicitly deferred.
- docs/contracts/dream-run-contract.md: name both registered generators
(mine-findings + external-watchlist) in the INGEST stage description.
Verification:
- cd cli && go test ./internal/overnight/... ./internal/rpi/... -> all PASS
- bash scripts/check-contract-compatibility.sh -> 0 failures
Closes the implementation of RFC 0001 Proposal 2 Wave 4. Subsequent
expansion (HTTP watchlist, dynamic dedup ranking) is a separate slice.
Wire the Wave 2 generator into the INGEST stage's parallel finding-generator
fanout. The existing runFindingGenerator wrapper already enforces the
2-minute per-source budget (RFC §253) via context.WithTimeout; no new
machinery needed.
Changes:
- cli/internal/overnight/ingest.go: append the external-watchlist generator
to findingGenerators(). Reuses defaultFindingGeneratorTimeout (2 min) and
the existing goroutine-per-generator fanout.
- PROGRAM.md: add .agents/dream/external-watchlist.yaml to Mutable Scope
when Dream is the active command, making the operator-managed source's
ownership explicit alongside the broader .agents/** rule.
- cli/internal/overnight/stages_test.go: update GeneratorSidecarCount
assertion in TestRunIngest_FindingGeneratorEmitsRealSidecarCandidates
from 1 to 2 (mine-findings + external-watchlist soft-success when no
watchlist.yaml exists).
Verification:
- cd cli && go test ./internal/overnight/... ./internal/rpi/... -> all PASS
- bash scripts/check-contract-compatibility.sh -> 0 failures
Add the external watchlist finding generator. It reads an operator-managed
YAML watchlist at .agents/dream/external-watchlist.yaml and emits one
held-for-review candidate per entry that has gone past its stale_after
window (default 168h / 1 week). Every candidate carries Status="proposed"
and Requires=["human-review"] so the queue selector from 0dc6b7f3 holds it
until an operator promotes it.
Files:
- cli/internal/overnight/external_watchlist_generator.go (new) — generator
implementation with a testable seam (runExternalWatchlistGeneratorAt) that
accepts an explicit path and "now" time so tests can pin staleness.
Missing watchlist file is soft-success with zero candidates (operators
may not have curated yet); read errors and YAML parse errors return
soft-fail sidecars. Entries with zero last_seen_at never emit — operators
must seed the cursor before the entry is active.
- cli/internal/overnight/external_watchlist_generator_test.go (new) — seven
tests covering: missing file, malformed YAML, staleness gate, default
168h threshold, never-seeded entries, fixture-driven L2 run, and ctx
cancellation soft-fail.
- cli/internal/overnight/testdata/external-watchlist-fixture.yaml (new) —
canonical fixture with four entries exercising all four gating paths
(week-stale, fresh, custom-window stale, never-seeded). Doubles as
documentation operators copy to .agents/dream/.
Generator is not yet registered in findingGenerators(); Wave 3 wires it.
The bootstrap-recommends-bd commit ran hooks/finding-compiler.sh during the
post-mortem step, which regenerated f-2026-04-14-002.md from registry.jsonl
and silently undid the deliberate confidence/utility/decay rotation from
0a24cb24 (chore(flywheel): rotate confidence decay metadata).
Reverting just this file to its HEAD~1 state. The compiler should preserve
existing decay fields when regenerating from registry — that's a separate
follow-up tracked elsewhere.
Adds a Step 0.5 "Recommend bd" section to skills/bootstrap/SKILL.md (and
its codex variant). When `bd` is missing, /bootstrap reports a one-line
recommendation pointing at `scripts/install-bd.sh` and continues — it
never executes the installer on the user's behalf. Mirrors the existing
HAS_AO graceful-degrade pattern.
- HAS_BD probe added to Step 0
- External Tools subsection documents bd as optional/recommended
- Step 6 report row + Troubleshooting row for the new state
- skills/bootstrap/scripts/validate.sh: structural check that SKILL.md
mentions install-bd.sh and probes bd
- tests/scripts/install-bd.bats: wiring assertion case (5/5 OK)
- skills-codex/bootstrap/ mirrored; codex hashes regenerated
The selector contract from 0dc6b7f3 holds queue items with status=proposed or
requires=[human-review], but the sidecar/aggregator path silently stripped
those fields between the generator and next-work.jsonl. Three coordinated
edits land the propagation, plus a pre-mortem-found Fix#1 for the dedup_key
prefix.
Changes:
- FindingGeneratorCandidate: add Status/Requires fields with omitempty.
- generatorNextWorkItem: same two fields, same json tags. The aggregator now
emits items that the selector can actually gate on.
- generatorCandidateNextWorkItem: copy Status/Requires through to the queue
line.
- normalizeGeneratorCandidate: accept BOTH "finding-generator|" and
"external-watchlist|" as legitimate dedup_key prefixes (Fix#1). Without
this, a Proposal 2 watchlist candidate with DedupKey="external-watchlist|..."
would be silently rewritten to "finding-generator|external-watchlist|..."
on its way through the aggregator, breaking the schema contract and
cross-run dedup correctness.
Tests added to stages_test.go:
- TestFindingGeneratorCandidate_StatusRequiresRoundTrip — L1 marshal/unmarshal
+ omitempty.
- TestNormalizeGeneratorCandidate_PreservesExternalWatchlistPrefix — L1
regression guard for Fix#1; also asserts finding-generator| prefix and
legacy unknown-prefix behavior.
- TestAggregateFindingGeneratorSidecars_PropagatesProposed — L2; asserts ALL
three of "status":"proposed", "requires":["human-review"], and
"dedup_key":"external-watchlist|... survive the full sidecar -> aggregator
-> next-work.jsonl chain (Fix#2 — without the third assertion, Fix#1
regressions stay invisible).
- TestAggregateFindingGeneratorSidecars_HeldByRpiSelector — L2; parses the
emitted queue line as rpi.NextWorkEntry and asserts IsQueueItemSelectable
returns false and IsQueueItemHeldForReview returns true.
Verification: cd cli && go test ./internal/overnight/... ./internal/rpi/...
-> all PASS.
Proposal 2 (external watchlist generator lane) needs the queue selector to
hold candidates that should never auto-execute. RFC 0001 Decision Ask #2
asked whether status / requires / dedup_key should be first-class schema
fields before external generators land. Approved: yes for these three;
goal_weight stays deferred until a selector ranking consumer exists.
Changes:
- cli/internal/rpi/types.go: add Status, Requires, DedupKey to NextWorkItem
with omitempty so existing producers keep round-tripping unchanged.
- cli/internal/rpi/helpers.go: add IsQueueItemHeldForReview; route
IsQueueItemSelectable through it. Items with status=proposed, any
non-empty requires entry, or an unrecognized status are held. status=ready
and status="" remain selectable for backward compatibility. Unknown
statuses fail safe (held), so a future producer can't accidentally bypass
the gate by writing a typo.
- cli/internal/rpi/types_test.go: new round-trip + omitempty tests for the
three fields, plus selector contract coverage (status=proposed alone,
requires=human-review alone, both together, unknown gates).
- docs/contracts/next-work.schema.md: bump schema_version 1.3 -> 1.4,
document the three fields and their lifecycle invariant (selectable iff
claim_status=available AND status in {ready, ""} AND requires empty).
- docs/rfcs/0001-finding-generator-parallelism.md: update Decision Record —
Decision Ask #2 resolved (status/requires/dedup_key first-class,
goal_weight deferred); Decision Ask #3 path is now unblocked for the
external watchlist generator.
Verification: cd cli && go test ./internal/rpi/... -> all PASS.
bash scripts/check-contract-compatibility.sh -> 0 failures.
docs/INDEX.md (contract catalog) and docs/index.md (MkDocs landing) shared the
same case-folded name on case-insensitive filesystems, leaving the working tree
in a phantom-modified state and breaking contract-compatibility-gate when the
script read whichever file the OS resolved at run time.
Resolves TASK-1 + TASK-2 of .agents/plans/2026-04-26-resolve-docs-index-case-collision.md:
- Move catalog content from HEAD:docs/INDEX.md into docs/documentation-index.md
- Repoint contract validators (scripts/check-contract-compatibility.sh,
scripts/contract-orphans-allowlist.txt) and their tests at the new path
- Update navigation references across README.md, AGENTS.md, CLAUDE.md, and the
docs/ tree (newcomer-guide, CONTRIBUTING, CI-CD, plans/, positioning/,
templates/, contract examples)
- Replace stale docs/README.md mentions in CLAUDE.md / AGENTS.md / repo-execution
profile example with docs/index.md (the actual MkDocs landing)
Verification:
- bash scripts/check-contract-compatibility.sh -> 0 failures, 24 contracts catalogued
- bash tests/scripts/test-contract-compatibility-gate.sh -> 7/7 PASS (verified pre-rebase)
- bash tests/skills/test-repo-native-orchestration.sh -> 28/28 PASS (verified pre-rebase)
- git ls-files | awk casefold-dup scan -> empty
TASK-3 (casefold-collision guard script) and TASK-4 (worktree disposition) from
the plan remain to be done in a follow-up.
* fix(goals): preserve diagnostic tail in truncateOutput
Head-only truncation at 500 runes cut the operator hint off the end of
gate output. The 2026-04-26 nightly retrospective showed flywheel-
compounding's "sessions must use 'ao lookup --cite ...'" tail being lost
mid-word for verbose runs.
New shape: when input exceeds 500 runes, keep first 200 + truncation
marker + last 200 runes. Short inputs untouched. Verified against live
ao goals measure --json — six long-output gates (flywheel-proof,
hook-preflight, go-cli-tests, contract-compatibility, install-smoke,
flywheel-lifecycle) now expose both their failure label and their
trailing fix-it.
Source: 2026-04-26 nightly retro task 1.
* feat(ci): add standards-injector reference completeness gate
hooks/standards-injector.sh fails open when a mapped <lang> is missing
its reference file. That's how `.js` lost standards inject for weeks
until the 2026-04-26 nightly caught it. New gate parses the case
statement, asserts every mapped lang has skills/standards/references/
<lang>.md, and runs in pre-push and CI.
Wires:
- scripts/check-standards-injector-completeness.sh (new)
- scripts/pre-push-gate.sh (slot 27b under hook category)
- .github/workflows/validate.yml (parallel job + summary deps)
- tests/scripts/check-standards-injector-completeness.bats (6 cases:
happy path, missing-named, |-alternation, parser-empty, real-repo)
Removing skills/standards/references/javascript.md temporarily makes
the gate FAIL with a clear message; restoring makes it PASS.
Source: 2026-04-26 nightly retro task 2.
* feat(rpi): add probed_stale_at + probed_by + ao rpi mark-probed
When a nightly probes a queue item and finds it stale, the knowledge
dies in the digest — tomorrow's run re-probes the same item. Adds
optional probed_stale_at (RFC3339) and probed_by fields to the v1.3
item schema, the Go round-trip type, the schema-rows acceptance test,
and a new `ao rpi mark-probed --id=... --by=...` subcommand for future
nightlies to write these without hand-editing JSON.
- docs/contracts/next-work.schema.md (Item table)
- cli/internal/rpi/types.go (NextWorkItem fields, omitempty)
- cli/internal/rpi/types_test.go (round-trip + omitempty)
- cli/cmd/ao/rpi_mark_probed.go (new subcommand)
- cli/cmd/ao/rpi_mark_probed_test.go (4 cases)
- cli/docs/COMMANDS.md (regenerated)
- tests/scripts/check-next-work-schema-rows.bats (acceptance case)
validate-next-work-contract-parity.sh stays green.
Source: 2026-04-26 nightly retro task 3.
* fix(overnight): probe Dream packets for staleness before emit
Three nightlies in a row emitted the same two stale packets ("philosophy
doc", "next-work schema v1.3") because no gate verified the cited
surface wasn't already shipped. Curator now runs a 5-second tractability
probe against each candidate's TargetFiles + scripts/ refs in the
morning command before writing it. Conclusively-stale packets are
suppressed; the suppression is recorded as a dream-curator-suppressed
entry on the run summary so operators see what was skipped instead of a
silent gap.
- cli/cmd/ao/overnight.go (overnightSummary.CuratorSuppressed)
- cli/cmd/ao/overnight_packets.go (probeDreamPacketStaleness,
extractScriptsRef, suppression plumbed through
buildDreamMorningPacketPlans return)
- cli/cmd/ao/overnight_packets_test.go (suppression-on-existing-target +
emits-when-inconclusive integration tests)
Source: 2026-04-26 nightly retro task 4.
* chore(scripts): add install-bd.sh installer
bd has been "unavailable" in three consecutive nightly runs. Upstream
(steveyegge/beads) publishes signed cross-platform binaries for
darwin/linux on amd64/arm64. New installer detects the platform,
downloads the matching tarball, installs to ~/.local/bin/bd, and
verifies via `bd version`. Idempotent: short-circuits when the
requested version is already present (use --force to override).
- scripts/install-bd.sh (new, executable)
- tests/scripts/install-bd.bats (4 offline-safe cases)
Verified end-to-end on darwin/arm64: download + extract + verify all
green.
Source: 2026-04-26 nightly retro task 5.
* fix(ci): align bats stub and AGENTS table with new standards-injector gate
PR #156 introduced scripts/check-standards-injector-completeness.sh
without:
- adding a make_stub for it in tests/scripts/pre-push-gate.bats setup;
three pre-push-gate bats tests (404, 414, 415) used a FAKE_REPO that
lacked the new script and the gate fired "missing executable", so
status came back non-zero
- listing it in the AGENTS.md CI table; validate-ci-policy-parity.sh
diffs the table against validate.yml summary.needs and flagged 2
drift groups
Both gates pass locally now.
RFC 0001 finding-generator parallelism: Proposal 1 landed in 1bd2f082 (sidecar
writer) + 08e3a38e (single-writer aggregator wired as REDUCE stage 7). Update
the RFC frontmatter from draft -> accepted and add a Decision Record citing
the implementation files, contract updates, and shipped fitness signals.
Decision Ask #2 partially resolved: dedup_key is now first-class in the
generator-aggregator path, so document it as an optional item field in
next-work.schema.md. status / requires / goal_weight remain deferred until
Proposal 2 (external watchlist) has a concrete consumer. Decision Ask #3
deferred entirely until Proposal 2.
* gate(flywheel-compounding): split σ=0/ρ=0 dormant hint from ρ=0-only
The flywheel-compounding gate had one branched hint (ρ=0 → "use --cite
applied|reference"), but ρ=0 covers two distinct corpus states:
- σ=0 AND ρ=0 — no citations of ANY kind in the measurement window;
the corpus is dormant. The fix is "run any ao lookup", not "switch
--cite kind". The high-confidence hint is misleading here.
- σ>0 AND ρ=0 — citation activity exists but only as retrieved-only
hits; the existing hint applies.
Add the σ=0 ρ=0 → dormant branch and a 6-case bats fixture pinning the
three hint branches (PASS, σ=0 ρ=0 dormant, ρ=0-only, generic) plus the
ao-failure path. Operators now see the right remediation per failure
mode without inferring it from the σρδ numbers.
This is a heavy-goal observability improvement, not a metric flip — the
goal stays fail until corpus citations land over multiple sessions.
https://claude.ai/code/session_01TVzMVJ8FXdctstCrzTcM7T
* refactor(codex_runtime): split detectLifecycleRuntimeProfile (CC 20→<14)
detectLifecycleRuntimeProfileWithOptions sat at the cli/ CC ceiling (20).
Any future case-arm tweak (e.g., a new runtime kind, or a new sub-state in
the existing four) would have pushed it past the gate's threshold.
Refactor: bundle the per-runtime config paths into a small struct
(lifecycleManifestPaths) shared by four per-runtime helpers
(populateCodexProfile / populateClaudeProfile / populateOpenCodeProfile /
populateUnknownProfile). The detector body shrinks to a switch over the
four helpers; each helper is straight-line and testable in isolation.
Behavior unchanged — verified via:
- go test -race ./cmd/ao -run "Lifecycle|Codex|Runtime"
- ./bin/ao codex status --json (live invocation, same JSON shape and
same "Detected Codex runtime without native hook support" reason)
- go-complexity-ceiling gate: cli/ <20, cli/internal/ <18
https://claude.ai/code/session_01TVzMVJ8FXdctstCrzTcM7T
* test(hooks): pin research-loop-detector behavior across 14 cases
The PostToolUse research-spiral detector at hooks/research-loop-detector.sh
had zero test coverage. A bad edit to the threshold ladder, the
read-only-bash classification, the kill-switch short-circuits, or the JSON
nudge formatting would ship silently.
Add a bats fixture covering:
- counter increment on Read/Grep/Glob/WebSearch/WebFetch
- WARN/STRONG/STOP threshold transitions at 8/12/15 with the exact
nudge text for each band
- reset on Edit/Write/NotebookEdit
- read-only Bash (grep/rg/cat/...) increments; execution Bash resets
- AGENTOPS_HOOKS_DISABLED and AGENTOPS_RESEARCH_LOOP_DISABLED kill
switches both short-circuit before any state mutation
- threshold env-var overrides (AGENTOPS_RESEARCH_WARN_THRESHOLD)
- STOP precedence over STRONG/WARN when all three are tied at 1
- emitted JSON parses round-trip via jq -e
Run against the live hook in a tmpdir mock-repo to keep tests
hermetic. All 14 scenarios PASS. Pure-test addition: no production code
touched, no fitness regression.
https://claude.ai/code/session_01TVzMVJ8FXdctstCrzTcM7T
* refactor(notebook): split runNotebookUpdate (CC 19→11) for headroom
runNotebookUpdate sat at CC=19 — close to the cli/ ceiling of 20 — and
mixed three concerns: memory-file resolution, entry resolution, and the
update pipeline itself. A single new branch (e.g., a third entry source)
would have failed the gate.
Extract two helpers:
- resolveNotebookMemoryFile(cwd) (string, bool)
- resolveNotebookEntry(cwd) *pendingEntry
Each is straight-line and individually testable; the main function now
reads as a four-step pipeline (memory-file → entry → cursor-skip →
parse/render/write).
Behavior preserved — `ao notebook update --quiet` exit 0, no output, no
state mutation when no MEMORY.md / no session entry. All cmd/ao tests
pass; CC drops to 11 (well clear of the 20 ceiling).
https://claude.ai/code/session_01TVzMVJ8FXdctstCrzTcM7T
* test(beads): pin five 0%-coverage helpers behind 19 cases
Five small pure helpers in cli/cmd/ao/beads.go and beads_audit_cluster.go
had 0% line coverage:
- beadMinInt — drives matches[:min(3, len)] citation clipping
- beadTruncate — wraps the bd parse-error message
- representativeIsEpic — picks epic vs leaf rendering for cluster output
- firstNNonEmptyLines — derives the cluster summary excerpt
- sortedMapKeys — supplies deterministic JSON ordering
A regression in any of them would corrupt user-visible output silently
(wrong message text, garbled cluster summary, non-deterministic JSON
ordering breaking diffs) rather than panicking. None had a test pinning
behavior.
Add 19 cases covering: smaller-of-two and equal-args boundaries (incl.
negatives and zeros), under/at/over the truncation limit (incl. n=0
on non-empty), epic-found / leaf-found / representative-missing /
empty-cluster branches of representativeIsEpic, whitespace-handling
and trim semantics of firstNNonEmptyLines, deterministic key order of
sortedMapKeys regardless of bool values.
All cases assert exact expected values (per .claude/rules/go.md). No
production code touched; fitness unchanged at 92.66.
https://claude.ai/code/session_01TVzMVJ8FXdctstCrzTcM7T
* refactor(contradict): split runContradict (CC 19→5) into 5 helpers
runContradict bundled four concerns at CC=19 — close to the cli/ ceiling
of 20: directory existence checks, file collection, entry parsing,
pair-comparison loop, and dual-format output. A new file source or a
new output format would have failed the gate.
Extract:
- collectContradictFiles: globs *.jsonl + *.md from learnings/patterns
- parseContradictEntries: reads + tokenizes, drops empty/zero-word files
- compareContradictPairs: O(n²) jaccard ≥ 0.4 + detectContradiction
- relPathOrAbs: Rel-with-fallback path helper (lifted from inline blocks)
- emitContradictResult: JSON-or-human writer
Behavior preserved — verified via:
- go test ./cmd/ao -run Contradict
- ./bin/ao contradict (human output identical: 20 files, 190 pairs)
- ./bin/ao contradict --output json (same {"total_files":20,...} shape)
CC drops: runContradict 19→5; new helpers all ≤6. Headroom for future
file-source additions.
https://claude.ai/code/session_01TVzMVJ8FXdctstCrzTcM7T
* refactor(rpi_serve): split serveRPIState (CC 19→5) into 4 helpers
serveRPIState mixed five HTTP-handler concerns at CC=19 — close to the
cli/ ceiling: query-param parsing/validation, run-id resolution against
the registry, fallback phased-state.json read, per-phase result
gathering, and the active-runs listing. A new state source or response
key would have failed the gate.
Extract:
- parseServeStateRunID: Validate run-id, write 400 on path traversal
- resolveStateForRunID: Look up the run via resolveServeRun, write to
resp on success, return the resolved root
- loadFallbackPhasedState: Read .agents/rpi/phased-state.json directly
only if the resolver did not already populate phased_state
- loadPhaseResults: Gather phase-{1,2,3}-result.json into a phase_N map
Behavior preserved — verified via:
- go test ./cmd/ao -run TestServeRPIState (existing handler test)
- go test ./cmd/ao (full package, 30s, all pass)
- go vet clean
CC drops: serveRPIState 19→below-5 (not in --threshold 5 listing); each
new helper ≤6.
https://claude.ai/code/session_01TVzMVJ8FXdctstCrzTcM7T
* test(hooks): pin write-time-quality across 16 per-language scenarios
hooks/write-time-quality.sh ran every Edit/Write but had zero test
coverage. A regression in any branch — Go fmt.Println in non-main, Python
bare-except / eval / missing-return-type-hint, shell missing
set -euo pipefail, the IS_TEST exemptions, the kill switch, the JSON
envelope shape — would silently degrade quality signal.
Add a 16-case bats fixture covering:
- tool-name filter (only Edit/Write trigger)
- missing/non-existent file are silent
- unsupported extension is silent
- AGENTOPS_HOOKS_DISABLED kill switch short-circuits
- Go: fmt.Println warns in non-main packages, silent in main and *_test.go
- Python: bare except warns; eval warns outside tests, silent in test_*.py;
missing return-type-hint on def-without-arrow warns
- Shell: missing 'set -euo pipefail' warns; presence suppresses warning
- JSON envelope (stdout-only) parses and includes hookEventName, file,
language, warning_count, warnings array
Each scenario uses a per-test temp file so cases don't bleed state. Pure
test addition; no production code changed.
NOTE: post-commit fitness measurement showed flywheel-proof transiently
fail due to a 503 on sum.golang.org (DNS cache overflow downloading the
go1.26.0 toolchain) — same network-flake mode PR #147 and #150 documented
on the same gate. Re-measure passes (score 92.66). Not caused by this
cycle (only test files touched).
https://claude.ai/code/session_01TVzMVJ8FXdctstCrzTcM7T
* fix(standards): add javascript.md so .js Edit/Write injects standards
hooks/standards-injector.sh maps .js → "javascript" and reads
skills/standards/references/javascript.md, but the file did not exist —
so every .js Edit/Write silently dropped the standards-context inject.
The hook's "fail-open on missing file" guard hid the gap.
Add references/javascript.md (Tier 1 baseline: ESM, prettier+eslint,
const/let, async/await, eqeqeq, common pitfalls, security defaults)
and link it in skills/standards/SKILL.md (table row + linked-references
list — required by skills/heal-skill --strict and the cmd/ao
TestSkillContract_ReferencesLinkedInSKILLMD test).
Sync the embedded copy via `cd cli && make sync-hooks` so the runtime
manifest matches the source. Add a 12-case bats fixture for
standards-injector.sh covering all six languages (go, ts, tsx, sh, js,
yaml/yml), the extensionless / missing / unsupported / kill-switch
silent paths, and exact-body-match assertions against the on-disk
references files.
Verified:
- hooks/standards-injector.sh on /x.js now returns 2111-byte body
matching the new file
- cd cli && go test -race ./cmd/ao -run TestSkillContract — pass
- bash skills/heal-skill/scripts/heal.sh --strict — All clean
- cd cli && make sync-hooks idempotent
NOTE: post-commit measurement shows flywheel-proof failing — same
network-environmental issue as cycle 8 (sum.golang.org 503 / DNS cache
overflow when the proof-run script downloads the go1.26.0 toolchain
into a fresh HOME). System Go is 1.24.7 but go.mod requires 1.26.0,
so GOTOOLCHAIN=local fallback also fails. Not caused by this cycle —
the proof-run path does not touch standards or hooks. Same pattern
PR #147 and #150 documented and shipped through.
https://claude.ai/code/session_01TVzMVJ8FXdctstCrzTcM7T
* fix(proof-run): reuse cli/bin/ao when present so 503s on sum.golang.org
don't fail flywheel-proof
tests/e2e/proof-run.sh always rebuilt ao in a fresh \$HOME, so each
gate invocation re-downloaded the go1.26.0 toolchain via
sum.golang.org. When the sum DB returns 503 ("DNS cache overflow")
the entire flywheel-proof gate (w=7) fails — even though the local
cli/bin/ao is fresh and behavior is testable.
Three changes:
- PROOF_AO_BIN=/path env override: caller can pin a pre-built binary
- Auto-detect \$REPO_ROOT/cli/bin/ao when present (and the override
is unset) — covers the common case where `make build` ran first
- PROOF_FORCE_BUILD=1 escape hatch: opt back into build-from-source
when the goal IS to verify the toolchain path
`require_cmd go` now only fires on the build path, so machines without
go installed can still run the proof against a shipped binary.
Verified:
- bash tests/e2e/proof-run.sh — auto-detects cli/bin/ao, all 20
flywheel checks PASS in ~6s (was failing in 90s before)
- PROOF_FORCE_BUILD=1 — still attempts go build (so the toolchain-
path regression test still exists)
- PROOF_AO_BIN=/path/to/ao — copies binary, skips build
flywheel-proof flips fail→pass after this cycle. This is a code-driven
flip (the script is the gate's only build path), not a runtime artifact.
https://claude.ai/code/session_01TVzMVJ8FXdctstCrzTcM7T
---------
Co-authored-by: Claude <noreply@anthropic.com>
CI failures from PR #150's first push:
1. codex-runtime-sections (validate-codex-generated-artifacts step)
"shared: manifest generated_hash drift detected"
"shared: marker generated_hash drift detected"
Cycle 7 edited skills-codex/shared/references/strict-delegation-contract.md
without regenerating the manifest hash. Ran scripts/regen-codex-hashes.sh
which updated hashes for 1 skill (shared).
2. bats-tests (tests/scripts/evolve-coverage-delta.bats)
The "parser uses the same regex anchors as the script" assertion grepped
the script for a literal regex string, but the test's escaping of the
literal didn't survive bats's preprocessor — the assertion failed even
though the script's regex was correct.
Replaced with an awk-based assertion that just confirms the script's
coverage extraction line still includes "grep -oE" + "coverage:" + the
"?%" anchor fragment. Robust against unrelated whitespace/quote shape
changes; still catches a regression that drops the % suffix.
Verified locally: full bats tests/scripts/*.bats and tests/hooks/*.bats
suites green; validate-codex-generated-artifacts.sh passes.
Cycle 1 added Tags to GOALS.md (notably long-cycle, corpus-state on
flywheel-compounding) but operators still had no way to ask "what's
the project's score if we set aside the corpus-state goals I cannot
move in this commit?".
Add --exclude-tag <tag> to `ao goals measure`. The flag drops any
goal whose Tags include the value before the snapshot runs. Combines
with --goal (goal-id filter applies first, then tag filter).
Demonstration:
$ ao goals measure --json | jq .summary.score
92.66
$ ao goals measure --exclude-tag long-cycle --json | jq .summary.score
94.06
Implementation:
- MeasureOptions.ExcludeTag (new field)
- applyMeasureFilters helper extracted from RunMeasure body so both
filter steps live together and RunMeasure stays under the cli/
CC=20 ceiling (was 17, would have grown to 21 inline → extracted)
- goalHasTag predicate on Goal.Tags
- Wired via --exclude-tag string flag (with help text) on goals_measure
- cli/docs/COMMANDS.md regenerated
Tests:
- TestGoalsMeasure_ExcludeTagFilter: long-cycle goal excluded; total=1,
failing=0, score=100
- TestGoalsMeasure_ExcludeTagPlusGoalIDInteraction: --goal narrows to a
tagged goal first, then --exclude-tag drops it → empty snapshot, no
error (named goal was found and then filtered)
newAdhocContextRunID's 1-second timestamp granularity has been a
recurring concern (sweep 2026-04-15, post-mortem next-work item).
The actual collision rate is ~1/65 536 within a second because we
suffix with 4 random hex chars, but the contract had no test pinning
that promise.
Add two tests:
- TestEnsureContextDir_IdempotentOnAdhocCollision — calling
ensureContextDir twice with the same adhoc-1234-abcd MUST reuse
the same directory (proven by writing a marker between calls and
reading it back from the second-call path)
- TestNewAdhocContextRunID_DistinctSuffixesInSameSecond — two reads
with distinct entropy in the same second produce distinct IDs
(adhoc-2000-0001 vs adhoc-2000-fffe)
Also expand godoc on newAdhocContextRunID and ensureContextDir to
document the collision rate and the MkdirAll-idempotency contract,
so future callers don't try to use the path itself as a session-
uniqueness signal.
Closes the post-mortem next-work item:
"Document adhoc ID 1-second collision behavior"
Codex DAG steps in skills-codex/ should use Codex's \$skill invocation
syntax, not Claude's Skill() tool form. The shared
strict-delegation-contract.md mirror still carried 12 Skill() references,
flagged by the vibe judge on 2026-04-19.
Rewrites in skills-codex/shared/references/strict-delegation-contract.md:
- Skill(skill="<name>", ...) → \$<skill> <args> (matches converter.sh's
rule: s/Skill\(skill="([^"]+)"...)/$1 \$2/)
- Three concrete examples in the Positive Pattern section now show
\$discovery, \$crank, \$validation invocations (not Skill(skill=...))
- Anti-Pattern section talks about Codex sub-agent spawns (not Agent())
since that's the closest Codex equivalent
- Anti-Pattern table renames "Skill() vs Agent()" → "Codex sub-agents
vs \$<skill> invocations" with corresponding cell updates
Verification:
- skills/shared/references/strict-delegation-contract.md (Claude variant)
is unchanged — still uses Skill() syntax which is correct for Claude
- bash scripts/audit-codex-parity.sh → passed
- bash skills/heal-skill/scripts/heal.sh --strict → clean
Closes the council-finding next-work item:
"Sweep skills-codex/ DAG bodies for Skill() to \$skill notation"
The script's default `find -name '*coverage*_test.go'` audited an empty
set on a clean checkout — CLAUDE.md bans `cov*_test.go` naming, so the
report had nothing to report. Post-mortem #5 flagged this.
Changes:
- New default scope: `*_test.go` (audit all test files in TARGET_DIR).
- New `--scope <pattern>` flag accepting `coverage` (legacy alias),
`all` (explicit alias for the new default), or any glob.
- Recognize `goleak.VerifyNone` as a valid assertion form (e.g.
measure_signal_test.go's goroutine-leak sentinel).
- Drop the `|| echo 0` fallback on `grep -c`. GNU grep prints `0` AND
exits 1 when there are no matches, so the fallback double-printed
("0\n0\n") and broke the awk ratio math. Use `: ${var:=0}` instead.
Closes the post-mortem next-work item:
"Expand audit-assertion-density.sh to cover all test files"
Tests (tests/scripts/audit-assertion-density.bats):
- default scope sees all *_test.go files
- default scope flags hollow files
- --scope coverage restricts to *coverage*_test.go
- --scope all is an explicit alias for the new default
- --check exits 1 when hollow tests are present
- --check exits 0 when no hollow tests in scope
- custom glob via --scope is honored
Rows 18/22/36 were inherited from a prior triage pass that marked whole
batches as row-level consumed=true while leaving some items open inside.
This triggered two validator failures:
validate-next-work-contract-parity.sh: aggregate/item lifecycle drift
smoke-test: missing required consumed boolean field
Fix: strip the batch-level consumed=true/claim_status=consumed that
incorrectly implied full consumption, restore consumed=false and
claim_status=available to match the actual item state. Individual
item-level marks (consumed=true on the confirmed-stale items) are
unchanged.
Validators now: parity=pass, schema-rows=pass, smoke next-work=pass.
Cursor smoke failure is pre-existing on main (unrelated).
https://claude.ai/code/session_01W8frsGLeX2MDeJUFNEsYe6
Cycle 1's Tags branch pushed parseGateRow to CC 18, exactly at the
cli/internal/ go-complexity-ceiling threshold. Restore head-room by
splitting the per-column read pattern into helpers:
- cellAt(cells, colMap, key) → trimmed value if mapped & in-range
- parseCheckCell(s) → strips wrapping backticks
- parseWeightCell(s) → defaults to 5 on parse error / out-of-range
parseGateRow now reads as a flat sequence of `if v, ok := cellAt(...)`
applications, dropping CC to 7. The old behavior (defaults, fallbacks,
empty-description → ID) is preserved by the helpers.
Tests:
- TestCellAt — mapped/missing/out-of-range/whitespace-trim cases
- TestParseCheckCell — backtick strip, no-backticks, empty, single backtick
- TestParseWeightCell — valid 1/5/10, out-of-range, non-numeric, empty
- All existing TestParseGateRow_* and TestParseGatesTable_* still pass
Merged batch-level consumed marks (rows 18, 22, 36) from the earlier
stale-audit-2026-04-26 pass into the Nightly 2026-04-26 base.
Combined with item-level marks from this session: 9 items retired total.
Schema: PASS (66 rows).
https://claude.ai/code/session_01W8frsGLeX2MDeJUFNEsYe6
evolve-coverage-delta.sh's grep accepted any "coverage: N" prefix and
silently consumed Go's event-coverage informational line — emitted by
packages with no _test.go files when the run uses -coverpkg, e.g. cmd/ao
prints `coverage: 1/12 events (informational)`. The "1" was extracted as
1.0% and folded into the project average, dragging it down by tens of
points.
Tighten the regex to `coverage: [0-9]+(\.[0-9]+)?%` so only true
percent-format lines participate in the average.
Verified by isolated parser test (./tests/scripts/evolve-coverage-delta.bats):
Input combining 1/12 events + 76.5% + 88.2% lines now averages to 82.3,
matching what an operator expects (the event line is excluded entirely).
Closes the post-mortem next-work finding "Fix coverage-ratchet.sh cmd/ao
event format handling" (the report named the wrong script — the bug was
in evolve-coverage-delta.sh's averaging step).
The vibe complexity step previously emitted both Python and Go analyzer
blocks unconditionally. For docs/shell/BATS-only epics this caused gocyclo
to walk the entire cli/ tree and hang (real harm reported in post-mortem).
Add a language-detection preflight that sets HAS_GO and HAS_PY from the
diff (or from <path> when no diff base is available), then guard the
radon and gocyclo blocks behind those flags. When a language is absent,
emit `ℹ️ COMPLEXITY SKIPPED: no .<ext> files in diff` instead of running
the analyzer.
heal --strict and codex-parity audit both still pass.
Closes the council-finding next-work item:
"Add language filter to vibe complexity analysis"
evidence: gocyclo hung during vibe for a docs/shell/BATS-only epic
The fuzz target proves no-panic but never asserts the parser populates
chain.ID, chain.EpicID, or chain.Entries correctly for its seed corpus. A
regression that silently dropped entries would still pass.
Add TestFuzzParseChainLines_SeedCorrectness covering all 7 seeds:
- metadata-plus-one-entry → ID + 1 entry, first step "research"
- metadata-only → ID set, 0 entries
- empty input → no error, no entries
- non-JSON first line → returns error
- malformed entry between valid ones → entry skipped, valid one survives
- empty metadata object → no error, empty fields
- epic_id with two entries → ID + EpicID + 2 entries
Closes the post-mortem #6 finding "Add fuzz seed correctness assertions"
for the last fuzz target that lacked one (cli/cmd/ao already had companion
SeedCorrectness tests for fuzz_jsonl_test.go and fuzz_context_test.go).
Adds an optional Tags column to the GOALS.md gates table, parses it through
to Goal.Tags, and propagates the tags into the Measurement JSON output of
`ao goals measure`. Tags are comma- or semicolon-separated, lowercased, and
empty entries are dropped.
Tags `flywheel-compounding` (w=8) with `long-cycle, corpus-state` so operator
tooling (e.g. /evolve selection) can recognize that this gate's green path
depends on multi-session corpus growth, not the current commit. Marks the
two compile-* gates with `runtime-artifact` for the same reason — their
green path requires `ao defrag` (or Dream's overnight preview) to have
written `.agents/.../defrag/latest.json` locally, and that file is gitignored.
Backwards-compatible: gates without a Tags column continue to parse as
before and emit `tags: null` in JSON output.
Tests:
- TestParseTagsCell (8 cases — empty, single, comma/semicolon, backticks,
case-folding, dropped-empties)
- TestParseGateRow_TagsHeaderColumn — full row with tags
- TestParseGateRow_TagsAbsentByDefault — no Tags header → nil Tags
- TestParseGatesTable_TagsRoundTrip — two-row table with mixed tags
* chore(agents): refresh next-work findings from dream 20260426T021434Z
Dream 6-iteration run added 2 new finding IDs to the queue and re-ranked
existing ones. Morning packets propose:
- Write AgentOps philosophy doc from validated flywheel thesis
- Audit context injection latency — are we lazy-loading everywhere we should
- Backfill next-work queue rows to schema v1.3 and add drift validation
https://claude.ai/code/session_01MkdcbdMtkrHRVNUJdMQiRi
* refactor(harvest): split extractFromSubdir to satisfy cc≤20 ceiling
Move the WalkDir closure body into helpers (handleWalkErr,
classifyWalkDir, isArtifactFile, readArtifactFile) so the per-file
processing pipeline reads top-down without nested branches.
Closes the 25→14 complexity gap that go-complexity-ceiling has been
flagging on extract.go since the OpenRoot/TOCTOU close.
* gate(flywheel-compounding): surface σρδ + root cause on fail
Move the inline `bash -c '...jq -e .escape_velocity_compounding'` gate
into scripts/check-flywheel-compounding.sh so failing runs print the
σ/ρ/σρ/δ structure plus the dominant root cause (typically ρ=0 because
sessions only record `retrieved` citations) instead of a bare `false`
from `jq -e`.
Behavior:
PASS prints σ=… ρ=… σρ=… δ=… (compounding) and exits 0
FAIL prints σ=… ρ=… σρ=… δ=… threshold=… — <hint> and exits 1
When ρ=0 the hint specifically calls out applied/reference citations as
the missing input, which is the operator-actionable fix.
* feat(gate): runtime queue lint for next-work.jsonl schema enums
Add scripts/check-next-work-schema-rows.sh + bats tests that validate
each row in .agents/rpi/next-work.jsonl against the v1.3 enum sets
(type, severity, source, claim_status). Complements
validate-next-work-contract-parity.sh, which only checks doc/runtime
parity, by catching legacy or hand-edited rows that drift from the
schema.
Closes the dream-2463d728 packet ("Backfill next-work queue rows to
schema v1.3 and add drift validation"). Current queue is clean — gate
is preventive rather than corrective.
10 bats cases cover: missing-file pass, clean-row pass, drift on each
enum field (severity/type/source/claim_status), legacy flat rows,
malformed JSON, empty lines.
* refactor(inject): split applyConfidenceDecayMarkdown to drop CC 17→5
Extract three helpers from applyConfidenceDecayMarkdown:
parseConfidenceField — confidence parse with default
mostRecentInteraction — pick latest of last_decay_at/last_reward_at
writeDecayedFrontmatter — read-modify-write of frontmatter (with
findFrontmatterEnd helper)
Pulls the function under the gocyclo warning threshold (15) so the
go-complexity-ceiling gate keeps its head room as new fields land.
* test(cli): cover flywheel compare/close-loop and allowlist function-level surface
Two slices to bring the cmdao-surface-parity gate green:
1. Add executeCommand-driven cobra tests for `flywheel compare` and
`flywheel close-loop`, and extend the parent-expectations map in
TestCobraCommandTreeRegistration so future drift is caught.
2. Allowlist five overnight-curator subcommands and two beads
subcommands that already have function-level _test.go coverage but
whose tests don't include the literal "ao <cmd>" string pattern the
parity script greps for. Each entry cites the test file and a
specific covered function/test name.
Result: parity gate goes from 8 uncovered commands → all 159 covered
or allowlisted.
* test(inject): pin extracted decay helpers (parse/recent/end)
Add focused L1 tests for the helpers introduced in the
applyConfidenceDecayMarkdown CC 17→5 refactor:
- parseConfidenceField: defaults on missing/zero/negative/unparseable
- mostRecentInteraction: picks latest, ignores bad timestamps, returns
zero on absence
- findFrontmatterEnd: returns first closing `---`, -1 when missing
Locks the helpers' semantics so future edits don't silently change
behavior the integration test (TestApplyConfidenceDecay_*) doesn't
notice.
---------
Co-authored-by: Claude <noreply@anthropic.com>