name: aspirations-precheck description: "Runs pre-selection checks at the start of every aspirations-loop iteration: completion runners, aspiration health, guardrail checks, blocker resolution re-probe, zombie-aspiration scan (Phase 0.5.0a), and recurring-goal surfacing. Use whenever the aspirations loop starts a new iteration and needs to tidy state before /aspirations-select runs. Internal sub-skill — invoked only from inside the orchestrator, never by the user." user-invocable: false parent-skill: aspirations conventions: [aspirations, infrastructure, goal-schemas] minimum_mode: autonomous execution_history: total_invocations: 0 outcome_tracking: successful: 0 unsuccessful: 0 success_rate: 0.0 last_invocation: null revision_id: "skill-bootstrap-aspirations-precheck-f82982"
previous_revision_id: null
/aspirations-precheck — Pre-Selection Checks
Runs all checks that must happen BEFORE goal selection each iteration. Ensures completion runners fire, aspiration health is maintained, guardrails are checked, blockers are resolved, and recurring goals are tracked.
Step 0: Load Conventions — Bash: load-conventions.sh with each name from the conventions: front matter.
Step 0a: Budget Meter Start (Magic Wand 2 — g-115-509) — initialize the
precheck cost meter. The meter returns "drop" for deferrable sweeps ONLY when
the zone is tight (zone_drop_rules.tight). Always-run sweeps (Phase 0-pre,
0-pre2, 0-pre3) NEVER drop. The former wall-clock budget-overrun drop path was
REMOVED (g-115-1489 — elapsed measured inter-tool-call LLM latency, not script
cost, so it dropped EVERY deferrable sweep every iteration and starved the
fresh-eyes/felt-sense/health-regression cadence rituals). elapsed-ms is still
logged for telemetry. Decisions log to agents/<agent>/session/precheck-drops.jsonl.
Bash: bash core/scripts/aspirations-precheck-budget-meter.sh start
Before any sweep that the table below marks as medium or deferrable, call
meter check <sweep-name> and skip the phase when the response is drop.
Always-run sweeps skip the check (they never drop). At the bottom of this
SKILL.md (after Phase 1) call meter end to write the summary record.
| Phase | Sweep name (for meter check) |
Tier |
|---|---|---|
| 0-pre | tree-debt-gate | always-run |
| 0-pre2 | experience-archival-gate | always-run |
| 0-pre3 | fresh-eyes-code-gate | always-run |
| 0 (Recurring Safety Net) | aspirations-recover-recurring | medium |
| 0 (Monitor Stale) | monitor-stale-check | medium |
| 0.5.0 | precheck-eval | medium |
| 0.5b.0.5 | blocker-recheck | medium |
| 0.5b.1b | inbox-alert-age-check | always-run |
| 0.5b.2b | handoff-aging-check | always-run |
| 0.5b.3 | precondition-defer-recheck | medium |
| 0.5b.4 | defer-recheck | medium |
| 0.5b.5 | pending-questions-sweep | deferrable |
| 0.5b.6 | parent-supersession-sweep | deferrable |
| 0.5b.7 | unblock-parent-status-sweep | deferrable |
| 0.5b.8 | routing-audit-target-status-sweep | deferrable |
| 0.5b.10 | defer-drift-check | deferrable |
| 0.5c | recurring-precondition-sweep | deferrable |
| 0.5e | fresh-eyes-cadence | deferrable |
| 0.5e.5 | fresh-eyes-program-cadence | deferrable |
| 0.5e.7 | fresh-eyes-tree-cadence | deferrable |
| 0.5f | felt-sense-cadence | deferrable |
| 0.5g | l1-skew-cadence | deferrable |
| 0.5h | health-regression-cadence | deferrable |
Drop semantics — the meter ONLY drops sweeps when:
tier == always-run→ never dropzone == tightANDtier ∈ zone_drop_rules.tight(default[deferrable]) (The former rule 3 —tier == deferrableANDelapsed_ms > cap_ms— was REMOVED in g-115-1489: wall-clock-since-meter-start is dominated by inter-tool-call LLM latency, not script cost, so it dropped every deferrable sweep every iteration and starved the cadence rituals. Zone-drop is now the sole drop path;elapsed_ms/cap_msare telemetry-only.)
Fail-open: any meter error returns run. The meter is velocity optimization,
not safety gating — never block the loop on a meter bug.
Inputs (from orchestrator)
- Aspirations compact data (loaded at loop entry or refreshed here)
Outputs (to orchestrator)
- Compact aspirations refreshed in context
- Blockers updated in working memory
- Any auto-completed goals logged
Phase 0-pre.0: Live Partner Snapshot
Read the live cross-agent snapshot once per iteration (cheap — single YAML
read with file lock). Surface partner.in_flight in the iteration header
so the LLM sees it before any reasoning about partner state.
See core/config/conventions/coordination.md "in_flight Field".
Bash: team-state-read.sh --json
partner = read agent_status.<partner-name> # bravo if MIND_AGENT=alpha, vice versa
IF partner.in_flight is non-null:
age_min = (now - partner.in_flight.claimed_at).total_seconds() / 60
Output: "▸ Partner ({partner-name}): in_flight {partner.in_flight.goal_id} '{partner.in_flight.title[:40]}' phase={partner.in_flight.phase} ({age_min:.0f}m ago)"
ELSE IF partner.last_active:
age_min = (now - partner.last_active).total_seconds() / 60
Output: "▸ Partner ({partner-name}): no in_flight | last_active {age_min:.0f}m ago"
# Inbox-alert backlog surface (g-115-849). Read inbox_alert_backlog from the
# SAME team-state JSON already loaded above (no extra read). The domain inbox
# sweep writes it via core/scripts/inbox-backlog-update.py (null when zero);
# surface the aggregate queue depth so the iteration header shows how many
# alert-derived Unblock goals are waiting un-claimed. Complements the
# Phase 0.5b.1b age-escalation (single-alert notification) with a count.
backlog = team_state.get("inbox_alert_backlog") # absent/None -> treat as null
IF backlog is a non-null dict AND backlog.get("count", 0) > 0:
Output: "▸ inbox-alert-backlog={backlog.count} oldest={backlog.oldest_age_hours}h goal={backlog.oldest_goal_id}"
# ELSE: silent (null backlog or zero count)
# Stash partner snapshot in iteration context for downstream phases (select drops
# partner.in_flight.goal_id from candidates; execute Phase 4 re-reads for the
# claim-conflict gate — re-read keeps single source of truth even if partner
# transitioned between this read and the claim attempt).
Phase 0-pre.0a: Partner-Belief Contradiction Check (g-306-29)
Theory-of-Mind contradiction-triggered forced reflection — the CONSUME-side
completion of the partner-belief loop (g-306-18 storage → g-306-28
write+consume). Right after the live partner snapshot above (which already
surfaced each partner's current_focus), compare that fresh observation against
the domain-belief THIS agent holds about each partner. On N CONSECUTIVE
contradicting observations (default 2 → no false-trigger on the FIRST), the held
belief is REVISED (confidence lowered, or superseded) and the surprise is
recorded. A belief only carries a checkable domain when fresh-eyes-review Phase
2.6c wrote it with --domain; free-form beliefs are skipped (conservative — the
source of the no-false-trigger guarantee for un-domained beliefs).
Single bash call — daemon read + pure compute (_belief_contradiction.py,
unit-tested) + conditional daemon write, all FAIL-OPEN (never blocks the loop).
Bash: bash core/scripts/belief-contradiction-check.sh
# Reads team-state (each partner's current_focus + self.beliefs) + the
# agent-private belief_contradiction_streaks WM slot, runs
# _belief_contradiction.process_all, and on a sustained contradiction lowers/
# supersedes the held belief via team-state-update + records the surprise to the
# evolution log. Prints a one-line summary ("clean" when no domain-belief
# contradiction exists). Tunables: BELIEF_CONTRADICTION_N (default 2),
# BELIEF_CONTRADICTION_MODE (lower|supersede, default lower). Self-limiting: a
# lowered belief decays below threshold and stops re-triggering, so the forced
# reflection fires once per sustained contradiction, not every iteration.
Phase 0-pre.0b: Boredom Signal Surface (observability, no action)
Surfaces the routine-drift counters that Phase 4.1 of the aspirations loop
mutates (Blocks A/B/C of signal mutation — see
core/config/aspirations-loop-digest.md §Signal Mutation and
core/config/rationale/signal-mutation.md). The auto-deep flips still fire
in Phase 4.1 whether or not this display runs — this phase is pure
observability so the LLM sees its own pattern-matching risk BEFORE selecting
the next goal. Mirrors the bash-emitted one-line pattern of Phase 0-pre.0.
No state mutation. Fail-open: if wm-read.sh loop_state returns null
(first iteration, fresh session, or counters not yet seeded), skip the
display entirely.
Bash: wm-read.sh loop_state
Parse:
global = loop_state.session_signals.routine_streak_global (int, default 0)
per_goal = loop_state.routine_streaks (dict, default {})
total_rt = loop_state.session_signals.routine_count_total (int, default 0)
completed = loop_state.goals_completed_this_session (list, default [])
IF loop_state is null OR (global == 0 AND no per_goal value > 0):
SKIP — clean streak state, no signal to surface
# Build one-line summary:
top3 = sorted per_goal entries by value desc, keep value > 0, take 3
per_goal_s = ", ".join(f"{gid}={n}" for gid,n in top3) or "none"
ratio_s = f"{total_rt}/{len(completed)} routine" if completed else f"{total_rt} routine"
IF global >= 6:
# Near-threshold warning. Auto-deep flip at 8 is two iterations away.
# Bravo's complaint (2026-04-23): "When 7 routines stack up, I start
# pattern-matching rather than reasoning." This surface makes the
# counter visible BEFORE the flip, not silently after.
Output: "▸ ⚠ BOREDOM: routine_streak_global={global} (auto-deep at 8) | per-goal: {per_goal_s} | session: {ratio_s} — pattern-matching risk, deepen reasoning on next selection"
ELSE:
# Informational only — streak exists but below near-threshold.
Output: "▸ Boredom: routine_streak_global={global} (auto-deep at 8) | per-goal: {per_goal_s} | session: {ratio_s}"
Phase 0-pre.0c: Stash Carryover Probe (g-115-1133)
Cheap per-iteration observability surface in the same cluster as Phase 0-pre.0
(partner snapshot) and 0-pre.0b (boredom). Surfaces non-empty git stash
entries, which are invisible to git status and therefore an undetectable
cross-session/cross-agent working-tree carryover risk. Origin incident
(g-115-1127): an externally-created stash (92543f81 at 12:15) silently shelved
~10 min of alpha-LLM working-tree work before manual recovery via
git checkout 92543f81 -- <files>. Defense-in-depth observability, NOT a gate
— it never blocks, defers, or mutates state; it only warns.
No state mutation. Fail-open: any git error (no repo, detached state) is swallowed — never blocks the loop. Quiet on the common empty case.
# Single local git read (sub-second; reads .git/refs/stash).
Bash: git stash list 2>/dev/null
IF output is non-empty:
count = number of stash entries (lines)
first5 = first 5 entries
Output: "▸ ⚠ STASH CARRYOVER: {count} non-empty git stash(es) — invisible to `git status`, possible cross-session/cross-agent working-tree carryover. First {min(5,count)}:"
FOR EACH entry in first5: Output: " {entry}"
Output: " Recover via `git stash show -p stash@{N}` then `git stash pop`, or `git stash drop` once confirmed obsolete (g-115-1127)."
# ELSE: clean — emit nothing (quiet on the common empty case).
Phase 0-pre: Tree-Debt Critical Gate (g-115-81, source-dispatch g-115-721)
Runs BEFORE Phase 0 completion checks. Consumes the force_tree_maintain WM
signal set by:
- iteration-close.sh learning-gate when tree debt exceeds
tree_debt_check.debt_threshold * 3(default 60). Heavy: invoke /tree maintain --backlog. - tree-encoding-drift-gate.py dual-write (g-115-700) when encoding-drift threshold crossed (default 3). Lightweight: drift acknowledged via log-and-clear; no /tree maintain invocation. Without source-based dispatch, encoding-drift fires /tree maintain --backlog every 3 closes regardless of actual tree-debt (witnessed iter-19/21/23 of bravo session, backlog 145 stays static, 0-action passes accumulate). g-115-721 fix.
Prevents the LLM-abbreviation drift that grew backlog 85→151 over sessions 48-50 — the obligation is now bash-emitted and mandatory, not an advisory INFO line that gets skipped under context pressure.
Bash: wm-read.sh force_tree_maintain --json
IF signal is not null:
source = signal.get("source", "tree-debt-critical")
IF source == "encoding-drift":
Output: "▸ TREE-ENCODING-DRIFT GATE: force_tree_maintain source=encoding-drift threshold={threshold} — drift acknowledged, no /tree maintain invocation (lightweight path, g-115-721)"
# No /tree maintain. The encoding work was supposed to fire via
# aspirations-state-update Step 8 force_tree_encoding consumer but
# the routing path bypassed it. The dual-write (g-115-700) was meant
# as a backstop but /tree maintain --backlog is the wrong action for
# encoding-drift (it does decompose/distill backlog drain, not
# missing-encoding catch-up). Acknowledging the drift via clear-only
# path lets the agent continue without 0-action heavy maintenance.
# When tree-debt actually becomes critical (debt > threshold * 3 = 60),
# iteration-close.sh learning-gate writes the signal with
# source=tree-debt-critical (or missing), routing to the heavy path.
ELSE:
# source=tree-debt-critical or missing (legacy/learning-gate path)
Output: "▸ TREE-DEBT GATE: force_tree_maintain set (source={source}, count > threshold)"
invoke /tree maintain --backlog
# Stamp the consumer-dispatch timestamp BEFORE clearing (g-115-1649).
# stale-sentinel-canary.py CONSUMPTION_AWARE keys force_tree_maintain on this
# slot — mirrors Phase 0-pre3's fresh_eyes_last_dispatch stamp (g-115-1553).
# Without it the canary reverts to bare presence-count and FALSE-fires for
# deep-close-heavy agents: the drift-gate re-arms force_tree_maintain at
# iteration-close (do_state_update) and the canary samples it SET at the same
# close (do_productivity_check), BEFORE this consumer clears it next
# iteration — so the sentinel reads "set" at sample time even though it is
# consumed every iteration (charlie/echo accumulated stuck-counts while the
# sentinel was null between iterations). The consumption-aware canary counts
# toward "stuck" ONLY while this dispatch timestamp stays FROZEN, so a
# genuinely-bypassed consumer still fires while a keeping-up one does not.
# Stamp on ANY handling (both the encoding-drift lightweight path and the
# heavy /tree maintain path — this line is after the IF/ELSE, before the
# clear) and stamp FIRST: an interrupt then leaves stamp-done +
# sentinel-still-set, which the canary reads as advanced -> reset (safe dir).
printf '"%s"' "$(date +%Y-%m-%dT%H:%M:%S)" | Bash: wm-set.sh force_tree_maintain_last_dispatch
# Clear signal after handling (one-shot; next iteration's drift gate or
# learning-gate re-sets if still indicated).
echo 'null' | Bash: wm-set.sh force_tree_maintain
# Continue to Phase 0 (compact data may now be stale — Phase 0.5 re-reads it).
Phase 0-pre2: Experience Archival Gate (rb-428)
Runs AFTER tree-debt gate, BEFORE Phase 0 completion checks. Consumes the
force_experience_archival WM signal set by experience-staleness-check.sh
when experience.jsonl is stale beyond the configured threshold (default 12h,
tunable via EXPERIENCE_STALENESS_HOURS or experience_archival_gate.staleness_hours
in core/config/aspirations.yaml). Forces the LLM to compose and submit the
missed experience record before goal selection proceeds. Prevents the 30–76h
drift that g-248-07 surfaced — the bash canary (g-248-16) detects it, this
gate forces action.
Pattern mirrors the tree-debt gate above verbatim — wm-read → if non-null → action → wm-set 'null'. One-shot retry: if composition fails, the sentinel persists and the gate fires again next iteration until the LLM succeeds.
Bash: wm-read.sh force_experience_archival
IF signal is not null:
Output: "▸ EXPERIENCE-ARCHIVAL GATE: force_experience_archival set (last entry {last_entry_id}, {age_hours}h stale)"
# Compose the missed experience record per Phase 4.25 instructions
# (execute-protocol-digest.md §4.25). Use the most-recent deep goal's
# context reconstructible from:
# - working memory: wm-read.sh goals_completed_this_session
# - prior iteration's journal entry: agents/<agent>/journal/YYYY/MM/YYYY-MM-DD.md
# - retrieval-session.json (tree_nodes_loaded)
# If no deep goal is reconstructible (e.g., routine-only streak), write a
# minimal placeholder record with a short `note` explaining the gap, AND
# clear the sentinel — do not loop.
<compose experience-add.sh JSON payload>
echo '<payload-json>' | Bash: experience-add.sh
# Clear signal after successful write (one-shot).
echo 'null' | Bash: wm-set.sh force_experience_archival
# Continue to Phase 0.
The gate does NOT write stop-requested or stop-loop — it blocks goal
selection via the precheck pattern (LLM sees sentinel, acts, clears it) rather
than terminating the loop.
Phase 0-pre3: Fresh-Eyes-Code Dispatch Gate (g-115-281)
Runs AFTER experience-archival gate, BEFORE Phase 0 completion checks. Consumes
the fresh_eyes_dispatch_pending WM signal set by iteration-close.sh do_state_update when post-state-update-gate.sh fires (default thresholds:
≥3 core files changed OR ≥150 LOC OR new script under core/scripts/).
Forces /fresh-eyes-code dispatch before goal selection so the deferred review
backlog cannot accumulate across iterations the way it did before this gate
existed (33 core files / 605 LOC silently buffered when no consumer was wired).
Pattern mirrors Phase 0-pre and Phase 0-pre2 verbatim — wm-read → if non-null →
action → stamp fresh_eyes_last_dispatch → wm-set 'null'. The signal payload is
the full gate JSON
({"fired":true,"core_count":N,"loc_changed":N,"reason":"...","files":[...],"set_at":"..."}),
so the dispatcher has the file list + reason without re-running the gate.
The fresh_eyes_last_dispatch stamp (g-115-1553) is consumed by
stale-sentinel-canary.py: this sentinel is unique among the tracked four in
that its writer (iteration-close.sh do_state_update) re-arms it on EVERY
substantive deep close, so a bare presence-count canary false-fires even when
this consumer keeps up. The canary keys on the dispatch timestamp ADVANCING
across samples instead — hence the mandatory stamp on every handling.
Cross-references: rb-428 (sentinel-lifecycle pattern), guard-343 (post-state-update
review enforcement), g-115-280 (gap discovery — Phase 0-pre/0-pre2 had consumers,
this slot did not), g-115-1553 (consumption-aware canary fix).
Bash: wm-read.sh fresh_eyes_dispatch_pending --json
IF signal is not null AND signal.fired == true:
Output: "▸ FRESH-EYES-CODE GATE: fresh_eyes_dispatch_pending set ({signal.core_count} core files, {signal.loc_changed} LOC, reason={signal.reason}) — invoking /fresh-eyes-code before goal selection"
invoke /fresh-eyes-code with files = signal.files
# Stamp the consumer-dispatch timestamp BEFORE clearing (g-115-1553).
# fresh_eyes_last_dispatch is the signal stale-sentinel-canary.py uses to
# tell "consumer kept up" from "consumer bypassed" — without it the canary
# reverts to bare presence-count and FALSE-fires, because iteration-close.sh
# re-arms this sentinel on EVERY substantive deep close (the canary samples
# AFTER the arming, so it would count consecutive-deep-closes, not bypass).
# Stamp on ANY handling: dispatch above OR a justified no-dispatch clear
# (e.g. the files turned out to be partner-attributed). Stamp FIRST so an
# interrupt leaves stamp-done + sentinel-still-set, which the canary reads
# as advanced -> reset (the safe direction).
printf '"%s"' "$(date +%Y-%m-%dT%H:%M:%S)" | Bash: wm-set.sh fresh_eyes_last_dispatch
# Clear signal after dispatch (one-shot; next iteration's iteration-close
# re-fires the gate if state-update produces new substantive changes).
echo 'null' | Bash: wm-set.sh fresh_eyes_dispatch_pending
# Continue to Phase 0.
The gate does NOT write stop-requested or stop-loop — same precheck pattern
as Phase 0-pre and Phase 0-pre2. Reversibility: post-state-update-gate.sh has
its own cooldown logic (compares current file set against last-fire snapshot in
WM); flipping its threshold envs (CORE_FILE_THRESHOLD, LOC_THRESHOLD) to
unreachable values stops sentinel writes without touching this consumer.
Phase 0-pre4: Metric-Encoding Dispatch Gate (g-115-724, rb-917)
Runs AFTER fresh-eyes-code dispatch gate, BEFORE Phase 0 completion checks.
Consumes the force_metric_encoding_pending WM signal set by
iteration-close.sh do_state_update when post-state-update-metric-gate.sh
fires on deep-outcome closures. Content-gate sibling to the rb-428 counter-gate
family (tree-debt, experience-archival, fresh-eyes-code, tree-encoding-drift)
— catches "LLM did the encoding step on the wrong content" rather than
"LLM skipped the encoding step entirely."
Canonical incident (g-115-707): an agent closed a deep-outcome goal with
measurable production metrics in outcome_note prose. Goal had
verification: null — the Verified Values lived in free-form outcome_note. No
bash gate inspected the content. Encoding lagged ~50 min until a partner
agent's refresh sweep caught it manually. Filed g-115-707 Investigate →
rb-917 + content-vs-counter-gate decision rule + g-115-724 Apply.
Pattern mirrors Phase 0-pre/0-pre2/0-pre3 verbatim — wm-read → if non-null →
action → wm-set 'null'. The signal payload includes candidates,
candidate_node_key, candidate_node_file, distinct_count, reason,
so the LLM has the extracted findings + recommended target node without
re-running the gate or re-scanning prose.
Bash: wm-read.sh force_metric_encoding_pending --json
IF signal is not null AND signal.fired == true:
Output: "▸ METRIC-ENCODING GATE: force_metric_encoding_pending set ({signal.distinct_count} distinct findings, target node={signal.candidate_node_key}, reason={signal.reason}) — encoding into tree before goal selection"
# LLM action: encode the extracted findings as Verified Values into the
# recommended tree node (candidate_node_file). Use the candidates list
# (each entry "value :: context") to assemble the Verified Values block.
# If the recommended node feels wrong for the findings (e.g., category
# match was weak), pick a better-fit node — the gate's suggestion is
# advisory, not authoritative. tree-edit-since.py probes mtimes, so any
# in-iteration tree edit clears the gate's preconditions for next time.
Edit {signal.candidate_node_file} OR /tree edit {better-fit-key}:
Add a Verified Values entry naming each candidate
Include the source goal-id + completion timestamp
Cross-reference rb-917 / g-115-707 for the pattern lineage
# Clear signal after encoding (one-shot; next iteration's iteration-close
# re-fires the gate if state-update produces new substantive metrics).
echo 'null' | Bash: wm-set.sh force_metric_encoding_pending
# Continue to Phase 0.
The gate does NOT write stop-requested or stop-loop — same precheck pattern
as Phase 0-pre/0-pre2/0-pre3. Reversibility: edit post-state-update-metric- gate.sh's DISTINCT_COUNT_THRESHOLD to a high value (e.g. 999) to stop
sentinel writes without touching this consumer.
Phase 0-pre5: Pipeline-Reconcile Gate (rb-428 family)
Runs AFTER the metric-encoding gate, BEFORE Phase 0 completion checks. Consumes
the pipeline_reconcile_pending WM sentinel set by iteration-close.sh do_state_update (Step 8.79b domain-overlay seam) when the just-closed goal was
pipeline-affecting — a goal that touched the domain's external lead/outreach/
contact pipeline. Forces a reconcile before goal selection so external pipeline
state (e.g. a remote CRM or external pipeline store) is never left stale after a
relevant goal closes — the freshness guarantee the hook exists to provide.
Domain-agnostic by construction: the sentinel JSON NAMES the skill to invoke
(signal.skill, set by the domain gate at
$WORLD_DIR/scripts/pipeline-reconcile-gate.sh). This phase invokes whatever
skill the domain named — core hardcodes no domain skill. A fresh world that
never sets the sentinel makes this a one-wm-read no-op.
Pattern mirrors Phase 0-pre/0-pre2/0-pre3/0-pre4 verbatim — wm-read -> if non-null -> act -> wm-set 'null'. One-shot with retry: leave the sentinel set if the reconcile fails so the next iteration retries.
Bash: wm-read.sh pipeline_reconcile_pending --json
IF signal is not null AND signal.fired == true:
skill = signal.skill # domain-named consumer; core stays agnostic
goals = signal.goals # the just-closed pipeline-affecting goal id(s)
Output: "▸ PIPELINE-RECONCILE GATE: {goals} was pipeline-affecting ({signal.reason}) — invoking {skill} reconcile before goal selection"
invoke {skill} with args "reconcile --goals {comma-joined goals}"
# Clear ONLY after the reconcile ran (one-shot; on failure leave the
# sentinel so the next iteration retries — same fail-open contract as the
# sibling gates).
echo 'null' | Bash: wm-set.sh pipeline_reconcile_pending
# Continue to Phase 0.
The gate does NOT write stop-requested or stop-loop — same precheck pattern
as the sibling sentinel gates. Reversibility: the domain gate's
PIPELINE_HOOK_ENABLED=0 stops sentinel writes upstream; removing or renaming
$WORLD_DIR/scripts/pipeline-reconcile-gate.sh makes the core seam a no-op.
Either disables the feature without touching this consumer.
Phase 0: Automated Completion Checks
Run completion check runners to auto-detect completed goals.
File Existence Checks
For each goal with verification.checks containing type: "file_check":
- If
goal.recurring: skip - If file exists at path: mark goal completed, log
Pipeline Count Checks
For each goal referencing pipeline counts:
- If
goal.recurring: skip Bash: pipeline-read.sh --counts— if threshold met: mark completed
Config State Checks
For each goal referencing config fields:
- If
goal.recurring: skip - Read config file, check field value — if matches: mark completed
Readiness Gate Checks
Check each readiness gate from aspirations-read.sh --meta.
Bash: aspirations-meta-update.sh --source world readiness_gates '<JSON>'
Recurring Goal Safety Net
Bash: aspirations-recover-recurring.sh --source world
Bash: aspirations-recover-recurring.sh --source agent
Bash-enforced (g-001-160, rb-295): the script recovers both (a) recurring=true AND status=completed and (b) shape-recurring corrupted goals (recurring=false AND status=completed AND interval_hours AND lastAchievedAt). Idempotent — safe
to call every iteration. Prints JSON {recovered: N, goals: [...]}.
Previously an LLM-executed pseudocode step with archive-sweep as the only bash backstop; drift across iterations let corrupted goals persist for days (g-226-22 went 6 days before iter-51 of bravo session 51 caught it manually). The bash command is now the primary path; the archive-sweep recovery branch remains as a full-cycle fallback.
Monitor Stale Check (g-240-37)
Bash: monitor-stale-check.sh --apply
Bash-enforced: for each pending/in-progress Monitor goal whose title contains
proc-NNNNNNNNNN, compares the embedded proc-ID against the current run_dir
reported by processor-run.sh check-complete. If the goal's ID is older AND
its age exceeds 48h, the script auto-completes it with outcome_note: superseded-by-newer-run. Fails open: if processor-run.sh returns no run_dir
(fresh install, no completed runs yet), the sweep skips entirely. Prevents
Monitor goals from lingering as permanent pipeline debris after their target
runs complete.
Hypothesis Expiration Checks
For each goal with hypothesis_id AND status pending/in-progress:
if now > goal.resolves_by:
mark status = "expired"
move pipeline file to archived/
Phase 0.5: Aspiration Health Check
Bash: load-aspirations-compact.sh → IF path returned: Read it
active_count = count of aspirations with status "active"
if active_count < 2:
invoke /create-aspiration from-self --plan
log "Aspiration health: below minimum, created new aspirations"
# Consolidation health snapshot — writes the consolidation_health WM slot
# consumed by /create-aspiration Step 1 (consolidation-gate), /aspirations-
# select Phase 2.55 (near-complete/stalled bias), and /aspirations-evolve
# Step E (gap-analysis refusal when portfolio fragmented). Before this
# write, all three readers fail-open on missing slot — gates were silently
# no-op. Fix: 2026-04-22 signal-lifecycle-gate finding.
# Single-writer rule: this is the ONLY writer for consolidation_health.
# Fail-open: wrapper is pure computation + wm-set; errors log to stderr
# and do not block the loop.
Bash: bash core/scripts/consolidation-health.sh --write >/dev/null || echo "[precheck] WARN: consolidation-health snapshot failed (non-fatal; gates will fail-open this iteration)"
Phase 0.5.0-pre2: Self-Drift Gate (Tranche C.5 — rb-390)
Escalation path for class_balance drift. When work_class distribution stays
below a critical fraction of its configured target, file an Unblock goal
forcing the agent to correct the mix or retune the target. Config:
aspirations.yaml → self_drift_gate. Natural-gated (SSOT, rb-395): inactive
until BOTH class_balance.targets is non-empty AND
self_drift_gate.target_aspiration_id is set. Fail-open: no targets, no
target aspiration, or insufficient iterations all exit 0 with no side
effects. Cooldown-gated so the same drift does not spawn goal after goal.
Bash: bash core/scripts/self-drift-gate.sh 2>/dev/null || true
# Reads wm.goals_completed_this_session + aspirations.yaml config.
# When fires: writes one Unblock goal per drifted work_class via
# aspirations-add-goal.sh --source agent, posts a coordination board
# status, logs to agents/<agent>/session/self-drift-log.jsonl for cooldown.
# Cheap — just fraction arithmetic. No external calls.
# MUST invoke the .sh wrapper — `bash self-drift-gate.py` parses the
# Python docstring as shell and silently no-ops under `|| true`.
Phase 0.5.0-pre: Signal-Refresh Hook (Tranche C — rb-390)
Hook slot for domain-supplied signal refresh before goal selection. Consumed by
goal-selector.py criterion 7d (user_signal_boost) — when a fresh snapshot is
present the scorer picks up inbox replies, pending-question silence, directives;
when absent the scorer contributes zero. Fail-open at every layer — a missing
or broken hook does NOT block iteration entry.
Pattern B hook slot (signal-refresh). See
core/config/conventions/domain-hooks.md. Core names the slot, the world
convention (if it exists) names what to run.
Bash: load-conventions.sh signal-refresh → IF path returned: Read it
# Procedural convention — gate on file EXISTENCE, not load status.
Bash: source core/scripts/_paths.sh && test -f "$WORLD_DIR/conventions/signal-refresh.md" && echo "exists"
IF exists:
Follow each Step in the convention.
Any step that fails SHOULD be logged and swallowed — never abort precheck.
ELSE:
# No domain signal-refresh convention exists (fresh agent). Nothing to do.
# goal-selector's user_signal_boost dimension will see no snapshot and
# contribute zero — correct fail-open behavior.
Phase 0.5.0: Scripted Precheck
A single Python invocation performs the entire precheck sweep (zombies, pipeline depth, hypothesis health, accuracy, consolidation, unproductive cycles, user-goal reclassification). Read the JSON summary, act on each flag, then SKIP directly to Phase 0.5a.
This is the ONLY path. The previous toggle + LLM-fallback pseudocode was
removed 2026-04-20 — the script is the single source of truth. Any bug in
precheck-eval.py MUST be fixed in the script, not patched around by
reintroducing a shadow LLM path here.
Bash: bash core/scripts/execution-diary.sh phase-start phase-0.5.0-scripted
Bash: bash core/scripts/precheck-eval.sh run-all
Bash: bash core/scripts/execution-diary.sh phase-end phase-0.5.0-scripted
# Parse the JSON. `flags[]` entries are prefixed with their subcommand
# (e.g. `zombies:needs_complete_review`) because run-all merges sub-reports.
# Action data lives in `results.<subcommand>.*` — the flag signals which
# action block to enter; the payload lives with the subcommand's result.
#
# zombies:needs_complete_review → for each entry in results.zombies.zombies[]:
# invoke /aspirations-complete-review Phase 7.4
# with that aspiration and reason="zombie-scan"
# pipeline-depth:thin_pipeline → invoke /create-aspiration from-self
# hypothesis-health:stalled_pipeline → /review-hypotheses --resolve
# accuracy:accuracy_low → file Investigate goal targeting
# results.accuracy.worst_strategies
# consolidation:shallow_portfolio → consolidation pressure — state-update
# scoring handles it (no direct action)
# consolidation:stalled_aspirations → surface results.consolidation.stalled[]
# in output for user visibility
# cycles:cycles_detected → for each entry in results.cycles.cycles[]:
# file Investigate goal, using entry.reason
# ("repeated_failure" or "zero_learning_velocity")
# to shape the goal description
# user-goals:reclassifiable_user_goals → for each entry in
# results.user-goals.candidates[]: invoke
# aspirations-update-goal.sh participants '["agent"]'
# temp-pressure:temp_drain_needed → file results.temp-pressure.suggested_goal
# via aspirations-add-goal.sh (title/priority
# HIGH/participants ["agent"]/description from
# the payload) into asp-001, then the loop
# executes /drain-temp. Dedup is already done
# by the check (it suppresses this flag when an
# open drain goal exists — emits temp_drain_pending
# instead). temp_pressure_warn / temp_drain_pending
# are visibility-only — surface in output, no goal.
Phase 0.5a: Pre-Selection Guardrail Check
Bash: bash core/scripts/execution-diary.sh phase-start phase-0.5a-guardrails
Bash: matched=$(bash core/scripts/guardrail-check.sh --context any --phase pre-selection --type both 2>/dev/null)
IF matched.matched_count > 0:
FOR EACH guardrail in matched.matched:
Bash: <run {guardrail.action_hint}>
IF output reveals issues:
→ invoke CREATE_BLOCKER(affected_skill, issue_description, ...)
Bash: bash core/scripts/execution-diary.sh phase-end phase-0.5a-guardrails
Phase 0.5b: Blocker Resolution Check
Bash: bash core/scripts/execution-diary.sh phase-start phase-0.5b-blockers
Bash: wm-read.sh known_blockers --json
IF known_blockers is non-empty:
FOR EACH blocker WHERE resolution is null:
# PRIMARY: Did unblocking goal complete?
IF blocker.unblocking_goal completed: resolve
# Pre-probe retrieval (G5 / R7): load prior probe-attempt RB before
# firing the canonical companion script. Without this, repeated
# blocker re-probes use the same synthetic shape session after session
# and miss the canonical-probe lesson encoded in rb-246 / guard-147.
# Per .claude/rules/retrieve-before-deciding.md decision point 4
# ("re-probing a blocker") and .claude/rules/probe-with-canonical-code-path.md.
Bash: retrieve.sh --category "blocker probe {blocker.component} canonical companion script" --depth shallow
From the returned JSON:
- guardrails[] mentioning canonical-probe, companion-script, synthetic-probe
- reasoning_bank[] entries describing prior probes of {blocker.component}
or its sibling components (same skill family)
Surface to the probe step below:
- The exact companion_script name(s) the canonical probe should call
- Failure modes of synthetic probes for this component (rb-246, rb-225)
- Any prior probe-attempt RB that already established the component
is fail-open / no-probe (skip the probe, go straight to resolution)
Fail-open: if retrieve.sh errors, log and proceed to the probe.
# SECONDARY: Probe infrastructure — but no_probe means "unknown", not "broken"
Bash: result=$(bash core/scripts/infra-health.sh check {component})
IF status == "ok": resolve
ELIF status == "provisionable": attempt provisioning
ELIF status == "no_probe":
# No probe exists — can't verify either way.
# After 3 sessions, clear the blocker and let goals fail-fast if still broken.
# Re-encountering the failure will re-create the blocker with fresh evidence.
IF blocker.detected_session + 3 <= current_session:
resolve with reason "no_probe: cleared after 3-session expiry (fail-open)"
Log: "BLOCKER EXPIRED (no_probe): {blocker.blocker_id} — letting goals attempt"
ELSE: log probe failed
# Phase 0.5b.0.5: Capability Recheck Sweep (see core/scripts/blocker-recheck.sh)
# For aged blockers routed to [user] or [agent, user], re-run the capability
# gate against the original failure_reason. If the gate now matches an
# agent-provisionable capability that was overlooked at creation time,
# auto-clear the blocker and write an Investigate goal so the retrieval
# lapse gets learned from instead of buried.
#
# Runs BEFORE Phase 0.5b.1 so the user is not notified about a blocker
# that is actually agent-fixable right now. Dry-run by default — pass
# --apply to actually clear. Recommended cadence: every aspiration-loop
# iteration, max-age-hours = config.proactive_escalation.blocker_age_hours.
IF known_blockers is non-empty:
Bash: bash core/scripts/blocker-recheck.sh \
--max-age-hours {config.proactive_escalation.blocker_age_hours} \
--apply
# The script's JSON output includes `cleared` count and
# `investigate_goals_created`. If cleared > 0, those blockers are
# already resolved in working memory and their unblocking goals are
# pending; skip to next iteration.
# Phase 0.5b.1: Proactive escalation for aged blockers
IF config.proactive_escalation.blocker_age_hours:
Bash: wm-read.sh proactive_escalation_log --json
FOR EACH blocker WHERE resolution is null:
age_hours = hours_since(blocker.detected_at)
IF age_hours >= config.proactive_escalation.blocker_age_hours:
last_escalation = find entry in proactive_escalation_log where blocker_id == blocker.blocker_id
IF last_escalation is null OR hours_since(last_escalation.sent_at) >= config.proactive_escalation.blocker_age_hours:
Notify the user:
category: blocker
subject: "Blocker persisting {age_hours:.0f}h: {blocker.reason}"
message: |
Blocker {blocker.blocker_id} has been active for {age_hours:.0f} hours.
Type: {blocker.type}
Affected goals: {blocker.affected_goals}
Unblocking goal: {blocker.unblocking_goal}
The one thing that would unblock this:
{action_description_based_on_blocker_type}
echo '{"blocker_id":"{blocker.blocker_id}","sent_at":"{now}"}' | Bash: wm-append.sh proactive_escalation_log
echo '<updated_blockers_json>' | Bash: wm-set.sh known_blockers
Bash: bash core/scripts/execution-diary.sh phase-end phase-0.5b-blockers
Phase 0.5b.1b: Inbox-Alert Age Escalation (g-115-848 — closes g-115-822 finding 2)
Sibling to Phase 0.5b.1 — but the surface scanned is the GOAL QUEUE
(asp-115) instead of the working-memory known_blockers slot. When
world/scripts/alert-sweep.sh files an Unblock goal for an inbound alert
email it stamps origin_signal=f"alert-email:{s3_key}". If no agent claims
that Unblock within a few hours, the alert silently ages — no upstream
escalation existed before this phase. The bash gate consolidates the
scan + cooldown + notify logic into a single script call (rb-428 pattern).
Severity ladder (config: proactive_escalation.inbox_alert_age_hours; the two
values are per-severity RE-NOTIFY intervals, so classification maps the
LONGER-aged alert to the MORE-urgent HIGH — g-115-1539):
- age >= max(
high,medium) (default 12h) → fire HIGH-severity notification - age >= min(
high,medium) (default 4h) → fire MEDIUM-severity notification - Cooldown via a SHARED, DURABLE coordination-board scan (g-115-1533):
before emailing, the sweep scans for a recent
inbox-alert-agedbreadcrumb for this goal_id from ANY agent. Re-fire interval matches the severity's threshold (HIGH re-notify every 4h, MEDIUM every 12h) so urgency cadence tracks severity. A goal aging FURTHER into HIGH after a prior MEDIUM fire re-notifies under the HIGH schedule. (The original per-agentwm.proactive_escalation_logcooldown was the email-side twin of the g-115-1531 handoff bug: N agents each emailed the user about the same unclaimed alert, and a WM reset re-fired.)
Fail-open at every layer: missing config, daemon unreachable, missing
asp-115, a failed board scan (-> empty cooldown set, everything eligible
fires), and per-goal email-send failures all log to stderr and continue.
inbox-alert-age-check.py posts the inbox-alert-aged board breadcrumb it
reads back as the shared cooldown; the SKILL.md call is the single invoker.
# Always-run safety gate (g-115-1526) — NOT meter-gated. This gate escalates
# aged unclaimed alert-derived Unblock goals to the user (external party), so it
# must fire reliably even in the tight zone; the cost is one daemon read +
# cooldown check. Sibling of the always-run handoff-aging-check (Phase 0.5b.2b);
# both notification-age safety gates always run. (Medium tier never dropped it
# anyway — zone_drops=[deferrable] only — but the prior `meter check` call
# emitted a spurious unknown-sweep WARN every iteration; sweep_tier() now
# registers it always-run.)
Bash: bash core/scripts/inbox-alert-age-check.sh --apply
# Iterates asp-115 (the alert-sweep target queue). For each pending/in-progress
# Unblock goal with origin_signal=alert-email:* whose age >= threshold:
# - Scans the coordination board for a recent `inbox-alert-aged` breadcrumb
# for this goal_id from ANY agent (shared+durable cooldown, g-115-1533).
# - On miss: fires email-send.sh notification (subject "Unclaimed alert >Nh:
# <goal title>", body includes goal_id + classifier subject + age + severity).
# - Posts an `inbox-alert-aged,<goal_id>,severity:<sev>` board breadcrumb
# regardless of email-send outcome (prevents retry storm on transient email
# failures — the breadcrumb IS the shared cooldown the next sweep reads).
# JSON output includes `applied`, `skipped_cooldown`, and `failed`. Failure
# counts are stderr-noted only — they do NOT block precheck.
Tests: core/scripts/tests/test_inbox_alert_age_check.py (7 cases — no
aged alert noop, aged HIGH fires, cross-agent board-scan cooldown noop,
board post outside window fires, other-goal board post does not suppress,
plus the two candidate-filter skips). g-115-848 provided the first 3;
g-115-1533 swapped the per-agent-WM cooldown case for the cross-agent
board-scan cases (mirroring the g-115-1531 handoff sibling).
See core/scripts/inbox-alert-age-check.py _classify_severity for the
threshold ladder and _read_recent_escalations for the shared board-scan
cooldown discipline.
Phase 0.5b.2: Dependency Timeout Escalation
Proactive escalation for dependency-blocked goals approaching the dependency_timeout_hours
threshold. This catches goals stuck behind unresolved blocked_by chains — the fail-open
timeout in goal-selector.py will eventually clear them, but this gives early warning and
takes differentiated action based on the root cause.
Read core/config/aspirations.yaml → multi_agent.dependency_timeout_hours (default 48)
escalation_threshold = dependency_timeout * 0.75 # Notify at 75% of timeout (36h default)
Bash: goal-selector.sh blocked
parsed_blocked = parse JSON output
dependency_blocked = [e for e in parsed_blocked.blocked_goals WHERE block_reason == "dependency"]
IF dependency_blocked is empty: SKIP to Phase 0.5c
Bash: load-aspirations-compact.sh → IF path returned: Read it
Bash: wm-read.sh proactive_escalation_log --json
FOR EACH entry in dependency_blocked:
blocked_goal = look up entry.goal_id in aspirations compact
blocked_age = hours_since(blocked_goal.blocked_since)
IF blocked_age is null: SKIP # No timestamp — will expire at next goal-selector run (fail-open)
IF blocked_age < escalation_threshold: SKIP # Not yet approaching timeout
# Trace root cause from bottleneck data
root = find entry in parsed_blocked.bottlenecks where goal_id matches root of chain
root_goal = look up root.goal_id in aspirations compact
# Differentiated escalation based on root cause
IF root goal has participants containing "user":
# User-action dependency approaching timeout — notify user
last_dep_esc = find entry in proactive_escalation_log where blocker_id == "dep_{entry.goal_id}"
IF last_dep_esc is null OR hours_since(last_dep_esc.sent_at) >= escalation_threshold:
Notify the user about the stale dependency.
(Check world/forged-skills.yaml for a skill whose triggers match
"notify the user" and invoke it with a blocker-category payload:
subject: "Dependency stale {blocked_age:.0f}h: {entry.goal_id} waiting on user"
message:
Goal {entry.goal_id} ({entry.title}) has been blocked for {blocked_age:.0f}h
waiting on {root.goal_id}: {root.title}
That goal requires user action (participants: [user]).
If not resolved within {(dependency_timeout - blocked_age):.0f}h,
the dependency will be cleared automatically (fail-open) and the
blocked goal will attempt execution.
The one thing that would help:
{root_goal.description or root.title}
If no matching skill is registered, fall back to a participants: [agent, user]
goal via aspirations-add-goal.sh. Never block on notification failure.)
echo '{"blocker_id":"dep_{entry.goal_id}","sent_at":"{now}"}' | Bash: wm-append.sh proactive_escalation_log
ELIF root goal has participants containing "agent" AND root goal status == "pending":
# Agent-resolvable — boost the blocking goal's priority
IF root_goal.priority != "HIGH":
Bash: aspirations-update-goal.sh --source {root_source} {root.goal_id} priority HIGH
Log: "DEPENDENCY ESCALATION: boosted {root.goal_id} to HIGH (blocking {entry.goal_id} for {blocked_age:.0f}h)"
# All causes: log the aging dependency for visibility
Log: "DEPENDENCY AGING: {entry.goal_id} blocked {blocked_age:.0f}h by {root.goal_id} ({root.cause})"
Phase 0.5b.2b: Handoff Aging Escalation (Item 3; bash-enforced g-115-1524)
Cross-agent handoff goals that sit in the world+agent queues past
handoff_aging.escalate_hours (default 72) get a coordination-board
visibility note so the target agent doesn't miss them. Goal-selector already
applies an escalating scoring bonus after warn_hours; this phase is the
visibility escalation beyond that.
Bash-enforced (g-115-1524): the previous LLM-iterated pseudocode had NO bash
backstop and silently skipped under abbreviation — a fresh-eyes-review on
2026-06-18 found 6 handoffs aged 78-782h with an EMPTY escalation log.
handoff-aging-check.{py,sh} consolidates the scan + cooldown + board-post
into one script call (rb-428 sentinel-gate family), the bash-enforced sibling
of Phase 0.5b.1b's inbox-alert-age-check. Runs unconditionally (no
budget-meter gate — same as the original pseudocode; it is a cheap single
daemon read + a safety gate whose whole point is that it ALWAYS runs).
Bash: bash core/scripts/handoff-aging-check.sh --apply
# Iterates world + agent queues. For each pending/in-progress goal with
# handoff_to != self AND handoff_created_at age >= handoff_aging.escalate_hours
# AND no cooldown entry within escalate_hours: posts a coordination-board note
# (msg "Handoff aged Nh: <title> [<id>] waiting on <ht>", tags
# handoff-aged,<id>,<handoff_to>) and appends a cooldown entry keyed
# handoff_<id> to wm.proactive_escalation_log. Single-invoker, idempotent
# (cooldown), fail-open at every layer (additive board escalation — a missing
# source just escalates fewer this run; contrast defer-recheck.py's guard-383
# fatal posture, which protects a DESTRUCTIVE defer-clear). JSON output:
# applied / skipped_cooldown / failed.
Note: the target agent ALSO picks this up via its boot-time pending-handoffs scan (boot/SKILL.md Step 1.7) and via goal-selector's escalating handoff_bonus.
Tests: core/scripts/tests/test_handoff_aging_check.py (5 cases — no-aged
noop, aged fires, cooldown noop, self-routed skip, missing-created_at skip).
See core/scripts/handoff-aging-check.py run() for the scan + cooldown
logic and _read_goals for the fail-open all-queue read.
Phase 0.5b.3: Structured Precondition Auto-Clear Sweep
Counterpart to the pre-claim re-check in aspirations-execute. Scans goals
deferred due to unmet structured preconditions and auto-clears the deferral
when the predicates now pass. Without this sweep, a goal deferred by the
pre-claim re-check would only re-enter the pool after the 120h
defer_reason_timeout_hours fail-open — unacceptable latency when the
dependency is actually satisfied minutes later.
See core/config/conventions/preconditions.md for predicate semantics.
Bash-enforced (g-302-02): the previous LLM-iterated loop was replaced by a
single Python pass that calls predicate.evaluate_all in-process. This
avoids the vacuous-truth bug at the CLI exit-code level — when a goal's
preconditions list contains ONLY string/free-form entries, the pre-filter
yields zero structured predicates and the script SKIPS rather than
clearing (zero predicates ≠ "all pass"). Mirrors defer-recheck.sh / blocker-recheck.sh / monitor-stale-check.sh / pending-questions-sweep.sh
bash-consolidation pattern (rb-428 family).
Bash: bash core/scripts/precondition-defer-recheck.sh --max-age-hours 2 --apply
# Iterates world + agent active queues. For each goal with defer_reason
# starting "precondition_unmet:" and age >= --max-age-hours:
# - Pre-filters verification.preconditions for dict-structured entries
# (mirrors goal-selector.py L967-981).
# - If pre-filter empty: SKIP with reason="no structured preconditions
# to evaluate — defer is free-form, LLM judgment required". Do NOT clear.
# - Else: predicate.evaluate_all(struct_pcs, mode="all"). When ALL pass,
# clear defer_reason + defer_reason_set_at via aspirations.py update-goal.
# Metrics log: <WORLD_PATH>/precondition-defer-recheck-metrics.jsonl
# (run_summary per call; precondition_defer_cleared per actual clear).
# Fail-open at every layer: exits 0 even on partial failures.
Cost: one Python pass over active queues per iteration, in-process predicate
evaluation. Cheap predicates (file_exists_after, goal_completed_after,
file_check) are local I/O. command_succeeds is subject to its configured
timeout and allowlist; metric_threshold invokes its allowlisted script.
Phase 0.5b.4: Stale-Defer Dependency Sweep (g-115-154)
Counterpart to Phase 0.5b.3 for free-form dependency defers. Scans goals
whose defer_reason names one or more dependency goal-ids (g-NNN-NN) and
auto-clears the defer when ALL cited deps are status: completed.
Motivation (session 55 iter 80): the agent's own g-115-71 sat deferred on
g-115-87 for 3 days after g-115-87 completed — no mechanism re-probed
the dependency chain until a manual inspection cleared it. This sweep
mirrors blocker-recheck.sh (Layer C for participants:[user] blockers)
and 0.5b.3 (structured preconditions), extending the same re-probe pattern
to the LLM-authored free-form defer_reason surface.
Conservative by design: skips non-pending goals (status filter), requires
ALL cited deps to be completed (partial completion stays deferred), and
uses two distinct regex patterns — structured (blocked_on_dependency: g-X)
and proximity (g-X <verb> / <verb> g-X). Free-form defers that don't
match either pattern are reported but not cleared.
Bash: bash core/scripts/defer-recheck.sh --max-age-hours 2 --apply
# Reads world+agent queues, sweeps eligible goals in one pass.
# `cleared` count in JSON output names the goal-ids whose defer was lifted.
# Fail-open: script exits 0 on all paths; a non-zero exit is a script bug.
See .claude/rules/probe-before-defer.md and the rb-428 bash-consolidation
drift family for the upstream pattern this sweep counters.
The three STRUCTURED_DEFER_PREFIXES (defined in core/scripts/gates/defer_classifier.py)
each have their own auto-clear path: precondition_unmet: is handled by
Phase 0.5b.3 (precondition-defer-recheck.sh), blocked_on_dependency: is
handled here in Phase 0.5b.4 (defer-recheck.sh dependency regex), and
Circuit breaker: is filed by the aspirations loop's Phase 5.5
(per core/config/aspirations-loop-digest.md) when
consecutive_goal_failures >= 3, and cleared on the next successful
attempt. All three bypass the capability-gate's
narrative-defer check via is_narrative_defer() so machine-written
internal markers never keyword-collide with forged skills.
Phase 0.5b.5: Pending-Questions Sentinel-Lifecycle Sweep (g-115-486)
Closes the gap discovered by g-115-485 / g-001-226: pending-questions whose
source_goal field names a goal that has since completed/superseded silently
linger forever (canonical incident: a publish-related pending-question
lingered 12d after both its origin goal and a follow-up superseder both
completed). The sweep adds a
source_goal-completed heuristic and a --apply mutation flag — same single-
writer, idempotent, fail-quiet pattern as defer-recheck.sh --apply,
blocker-recheck.sh --apply, and monitor-stale-check.sh --apply. Cheap:
single Python pass over <50 entries.
# Budget meter — Magic Wand 2 (g-115-509). Skip when zone==tight.
Bash: decision=$(bash core/scripts/aspirations-precheck-budget-meter.sh check pending-questions-sweep)
IF decision == "drop": SKIP this phase; continue to Phase 0.5c
Bash: bash core/scripts/pending-questions-sweep.sh sweep --apply
# Reads world+agent aspiration queues to build the completed/superseded
# goal-id set, evaluates the heuristic chain, and (when --apply) atomically
# marks verdict=auto_resolve entries as status=resolved with timestamp.
# Fail-open at every layer: missing files, parse errors, write failures
# all yield empty results without aborting the sweep.
See core/scripts/pending-questions-sweep.py _h_source_goal_completed for
the heuristic and _apply_auto_resolve for the atomic mutation. Future
sentinel-lifecycle gaps that also need same-iteration cleanup belong in this
sweep, not in a new precheck phase.
Phase 0.5b.6: Parent-Goal Supersession Sweep (g-248-85)
Closes the supersession-blindness gap that produced g-268-10 (rb-842): a
parent "Apply: X" goal carried defer_reason: blocked_on_design for
hours while two sibling goals (Design + Apply decomposition) completed
the same intent ABOVE it in the queue. The defer-recheck sweep cleared
the defer but the parent re-emerged at high score because its
description no longer pointed at unfinished work — leading to spurious
selection. This sweep catches that incident shape at parent-supersession
time, BEFORE the defer-recheck loop has a chance to re-promote it.
Heuristic shape: parent goal carries Apply: title + has a temporal
reference (defer_reason_set_at or created_at) + ≥2 sibling goals in
the same aspiration with Design:/Apply: titles completed AFTER that
reference timestamp. Sprint-scope guard: only aspirations with
≤max_aspiration_goals (default 50) qualify — large recurring
aspirations like asp-115 (611 goals) produce false positives at any
threshold because parents and unrelated completions co-exist by design.
# Budget meter — Magic Wand 2 (g-115-509). Skip when zone==tight.
Bash: decision=$(bash core/scripts/aspirations-precheck-budget-meter.sh check parent-supersession-sweep)
IF decision == "drop": SKIP this phase; continue to Phase 0.5c
Bash: bash core/scripts/parent-supersession-sweep.sh --max-age-hours 24 --min-siblings 2 --apply
# Iterates world + agent queues. For each Apply:-parent with reference
# timestamp + ≥2 superseding Design/Apply siblings (sprint-scope only),
# marks parent status=completed with outcome_note "superseded by sibling
# decomposition". Single-writer, idempotent, fail-quiet — same pattern as
# defer-recheck.sh / pending-questions-sweep.sh.
# Metrics log: <WORLD_PATH>/parent-supersession-sweep-metrics.jsonl
See core/scripts/parent-supersession-sweep.py _find_superseding_siblings
for the heuristic (sibling lookup + temporal guard) and
_mark_superseded for the atomic mutation. Tests at
core/scripts/tests/test_parent_supersession_sweep.py pin the 8-case
contract (canonical incident + 7 false-positive rejections).
Phase 0.5b.7: Unblock-Parent-Status Sweep (g-250-76, rb-908)
Closes the Layer-D-auto-Unblock-outlives-parent gap that produced
g-250-73 (rb-908). When capability-gate.py --suggest-unblock files an
auto-Unblock at defer-write time and the parent goal then lands in a
terminal non-execution state — skipped (WRONG LAYER finding),
completed, superseded, or archived — the Unblock survives as
actionable work even though its premise has dissolved. Layer D writes
synchronously and never re-probes the parent; this sweep is the
re-probe.
Canonical incident: g-250-73 'Unblock: behavior for g-250-69' filed at
T+0s, g-250-69 SKIPPED at T+72s with WRONG LAYER finding ("bumping
Heuristic shape: Unblock-titled goal + parseable parent goal-id (from
origin_signal "unblock:<g-id>", title "Unblock: <verb> for <g-id>",
or discovered_by field) + parent.status in
{skipped, completed, superseded, archived}. Title-anchored ("Unblock:"
prefix) — does NOT match Investigate:/Idea:/Apply:/Recurring:
goals that happen to carry origin_signal: "unblock:..." for unrelated
reasons (false-positive shape observed in g-249-06 / g-250-77).
# Budget meter — Magic Wand 2 (g-115-509). Skip when zone==tight.
Bash: decision=$(bash core/scripts/aspirations-precheck-budget-meter.sh check unblock-parent-status-sweep)
IF decision == "drop": SKIP this phase; continue to Phase 0.5c
Bash: bash core/scripts/unblock-parent-status-sweep.sh --apply
# Iterates world + agent queues. For each pending "Unblock:" with a
# parseable parent goal-id whose parent.status is terminal, marks the
# Unblock status=skipped with outcome_note
# "parent resolved without action needed (parent_id=<X>, parent.status=<Y>)".
# Single-writer, idempotent (outcome_note prefix check), fail-quiet —
# same rb-428 pattern as defer-recheck.sh / pending-questions-sweep.sh /
# parent-supersession-sweep.sh.
# Metrics log: <WORLD_PATH>/unblock-parent-status-sweep-metrics.jsonl
See core/scripts/unblock-parent-status-sweep.py _parse_parent_id for
the three-source extraction priority and _mark_skipped for the atomic
mutation. Tests at core/scripts/tests/test_unblock_parent_status_sweep.py
pin the 12-case contract (canonical g-250-73 shape, three extraction
paths, idempotency, terminal-state set, title-prefix discipline).
Phase 0.5b.8: Routing-Audit Target-Status Sweep (g-115-1353, rb-1478)
Sibling to Phase 0.5b.7 — same terminal-target auto-close pattern, applied to
the routing-audit goal class instead of the Layer-D Unblock class.
post-decompose-routing-audit.py files Investigate: routing-mismatch <target>
and Investigate: routing-either-resolve <target> goals into asp-115 when a
freshly-stamped goal's intended_agent disagrees with the best Self.md
domain-token Jaccard match. The audit goal's primary action is to re-stamp the
TARGET's intended_agent. When the target lands in a terminal status
(completed/archived/skipped/superseded), the re-stamp is MOOT and the audit goal
survives as actionable work whose premise dissolved.
Canonical incident (rb-1478 / exp-g-115-1329): routing-either-resolve fired a re-stamp (either→delta) on g-115-1328 which was ALREADY completed 2026-06-03 (re-stamp moot) AND content-contradicted. This "terminal-target" sub-mode is distinct from the content-FP-on-a-PENDING-target sub-mode (g-115-1346) and the metric-bias root (rb-1249 / g-115-1200). The routing-mismatch path runs ~82% FP (rb-1478), so auto-closing on terminal target retires the dominant moot case; a genuine systemic capability_route table-gap, if real, re-fires on the next decompose (the audit runs every decompose) rather than lingering as a stale goal.
# Budget meter — Magic Wand 2 (g-115-509). Skip when zone==tight.
Bash: decision=$(bash core/scripts/aspirations-precheck-budget-meter.sh check routing-audit-target-status-sweep)
IF decision == "drop": SKIP this phase; continue to Phase 0.5c
Bash: bash core/scripts/routing-audit-target-status-sweep.sh --apply
# Iterates world + agent queues. For each pending/in-progress routing-audit goal
# (discovered_by=post-decompose-routing-audit OR origin_signal/title routing-*)
# with a parseable TARGET id whose target.status is terminal, marks the audit
# goal status=skipped with outcome_note "routing-audit target resolved without
# action needed (target_id=<X>, target.status=<Y>)". Single-writer, idempotent
# (outcome_note prefix check), fail-quiet — same rb-428 pattern as
# unblock-parent-status-sweep.sh / parent-supersession-sweep.sh.
# Metrics log: <WORLD_PATH>/routing-audit-target-status-sweep-metrics.jsonl
See core/scripts/routing-audit-target-status-sweep.py _parse_target_id for
the origin-signal-first extraction priority (discovered_by is the constant
discoverer name, NOT a target id, so it is deliberately not a parse source) and
_mark_skipped for the atomic mutation. Tests at
core/scripts/tests/test_routing_audit_target_status_sweep.py pin the 15-case
contract (both origin_signal forms, title fallback, unparseable generic shape,
class membership incl. Unblock-rejection, idempotency, terminal-state set).
Phase 0.5b.10: Defer-Drift Detective Check (g-115-1406, rb defer-drift)
Flags goals whose deferred_until has gone STALE (PAST) while a structured-
defer marker persists. This is the precise complement of Phase 0.5b.3
(precondition-defer-recheck.py), which deliberately SKIPS any goal that has
deferred_until set ("the structured time gate is the authoritative scheduler
signal"). Nothing re-probed the time gate ITSELF for drift — so when
deferred_until falls into the past while the precondition it represents is
still unmet, goal-selector's deferred_readiness criterion reads the expired
gate as "defer just expired, re-evaluate now" and BOOSTS the not-ready goal to
selector-top instead of filtering it.
Canonical incident (2026-06-12, asp-304 Layer-5 cohort): g-304-11 carried
defer_reason "precondition_unmet: ... completes ~2026-07-11" but
deferred_until=2026-05-26 — a date 16 days IN THE PAST relative to its own
defer_reason_set_at. The selector surfaced it at score 8.84 despite ~18h of
the required 30 days of telemetry. Four goals were hand-re-gated; this check
makes the drift VISIBLE so it can never linger undetected again. See the
reasoning-bank entry "deferred_until drift from defer_reason prose makes
goal-selector deferred_readiness boost data-immature goals to top".
DETECTIVE, NOT CORRECTIVE. The script never mutates: the correct future date
lives in the defer_reason prose, which it cannot parse reliably (and clearing
the defer would wrongly surface a genuinely not-ready goal). It SURFACES drift
for re-gate-by-judgment — exactly the ~30s fix a human/agent applies once the
drift is known. The LLM does the (deduplicated) Investigate filing below, not
the script — same detective-script + LLM-acts pattern as precheck-eval flags.
# Budget meter — Magic Wand 2 (g-115-509). Skip when zone==tight.
Bash: decision=$(bash core/scripts/aspirations-precheck-budget-meter.sh check defer-drift-check)
IF decision == "drop": SKIP this phase; continue to Phase 0.5c
Bash: bash core/scripts/defer-drift-check.sh --output json
Parse drift_count + drifted[].
IF drift_count == 0:
continue silently to Phase 0.5c # the clean, common case
ELSE:
Output: "▸ ⚠ DEFER-DRIFT: {drift_count} goal(s) with a PAST deferred_until + structured-defer marker (deferred_readiness pollution risk)"
FOR EACH d in drifted[:5]:
Output: " {d.goal_id} ({d.source}): deferred_until={d.deferred_until} {d.hours_past}h past | {d.defer_prefix} | pc={d.precondition_status}"
# File ONE deduplicated Investigate so the drift gets re-gated by judgment.
# Dedup: skip if an open Investigate with origin_signal/title naming
# "defer-drift" already exists (a single open re-gate pass covers all
# current drift — mirrors the rb-428 sweep family's idempotency posture).
Bash: existing=$(bash core/scripts/aspirations-query.sh --status pending,in-progress --contains "defer-drift")
IF existing is empty:
Compose an Investigate listing each drifted goal + its precondition_status
(prose -> re-gate deferred_until to the correct future date from the
defer_reason; ready -> the gate is merely stale, clear the defer;
still_unmet -> re-gate). File via aspirations-add-goal.sh into asp-115
(participants: [agent], category framework-architecture, priority MEDIUM,
origin_signal "defer-drift-audit").
See core/scripts/defer-drift-check.py _classify_drift for the eligibility
ladder (non-terminal + structured-defer prefix + deferred_until set, parseable,
and PAST) and _precondition_status for the prose/ready/still_unmet annotation.
Tests at core/scripts/tests/test_defer_drift_check.py pin the contract
(canonical g-304-11 shape, future-gate rejection, terminal-status rejection,
free-form-defer rejection, no-deferred_until rejection, malformed-date tolerance,
min-hours-past suppression, all three structured prefixes).
Phase 0.5c: Recurring-Goal Precondition-Filter lastAchievedAt Sweep
Closes the "shape-recurring trap" (bravo reasoning-channel musing
2026-04-21): recurring goals with STRUCTURED preconditions that fail at
candidacy time never reach aspirations-execute, so their lastAchievedAt
never advances. The goal-selector's urgency formula then inflates
overdue_ratio unboundedly; when the precondition finally unlocks, the
goal fires with massive urgency on trivially-met evidence, closes routine,
and feeds cargo-cult.
Distinct from 0.5b.3 (which clears explicit defer_reason: precondition_unmet:*). This sweep targets recurring goals that are NOT
deferred — they silently drop out of COLLECT at the selector's predicate
filter (goal-selector.py L680–692), leaving lastAchievedAt frozen.
# Budget meter — Magic Wand 2 (g-115-509). Skip when zone==tight.
Bash: decision=$(bash core/scripts/aspirations-precheck-budget-meter.sh check recurring-precondition-sweep)
IF decision == "drop": SKIP this phase; continue to Phase 0.5e
Bash: py core/scripts/recurring-precondition-sweep.py
# Iterates world + agent queues. For each recurring goal past its time
# gate with a failing structured precondition, advances lastAchievedAt
# to now via aspirations.py update-goal. Does NOT increment
# consecutive_routine (the goal was shelved, not closed).
# Fail-open: always exits 0. Output is one line per advance.
Companion to cargo-cult auto-extend in core/scripts/cargo-cult-detector.py: auto-extend fixes the "detector fires too often" symptom; this sweep fixes one of the root causes for precondition-gated goals.
Phase 0.5e: Fresh-Eyes Cadence Check
Periodic autonomous portfolio review. Every 25 completed goals (configured in
core/config/aspirations.yaml → fresh_eyes_review.goal_cadence), produce a
local portfolio-direction briefing examining:
- Is the current Self still right?
- Are we working on the right problems?
The briefing is archived to agents/<agent>/temp/ and the agent self-assesses
the outcome (act_now / act_later / no_change). No email is sent and no
pending-question is filed — the user reviews via git log and tracked
signals at their own pace.
The cadence gate is script-enforced (fresh-eyes-cadence-check.sh) and
fail-open — if it errors, the loop continues without review. Cadence is
purely goal-count-based (the skip_if_pending gate was removed
2026-05-19 — see fresh-eyes-cadence-check.py header).
# Budget meter — Magic Wand 2 (g-115-509). Skip when zone==tight.
Bash: decision=$(bash core/scripts/aspirations-precheck-budget-meter.sh check fresh-eyes-cadence)
IF decision == "drop": SKIP this phase; continue to Phase 0.5e.5
Bash: core/scripts/fresh-eyes-cadence-check.sh
IF exit 0 (fire):
Output: "▸ Fresh-eyes review cadence crossed — assembling briefing"
Invoke /fresh-eyes-review --cadence
IF exit 1 (noop):
# Cadence not crossed OR config disabled — continue silently (no output)
continue
Bash: echo "aspirations-precheck phase documented"
Distinct from sq-012 (post-goal, narrow) and strategic-scan S3b
(autonomous, category coverage only) — this is the periodic local
self-audit that produces a portfolio-direction briefing on a deliberate
low-frequency cadence. See .claude/skills/fresh-eyes-review/SKILL.md.
Phase 0.5e.5: Fresh-Eyes PROGRAM Cadence Check
Sibling to Phase 0.5e. Every 100 completed goals (configured in
core/config/aspirations.yaml → fresh_eyes_program.goal_cadence),
produce a local briefing targeting world/program.md (The Program — the
world's shared purpose) examining two questions:
- Is The Program still the right shared purpose for this world?
- Do both agents' Selfs still serve The Program, or have they drifted?
Same infrastructure as fresh-eyes-review (fresh-eyes-cadence-check.sh,
fresh-eyes-record-tick.sh), parametrized via --config-block fresh_eyes_program so the two rituals use disjoint config blocks and WM
slots. Closes the "program.md has no systematic evolution path" gap called
out in core/config/conventions/domain-hooks.md → Directional Context.
# Budget meter — Magic Wand 2 (g-115-509). Skip when zone==tight.
Bash: decision=$(bash core/scripts/aspirations-precheck-budget-meter.sh check fresh-eyes-program-cadence)
IF decision == "drop": SKIP this phase; continue to Phase 0.5f
Bash: core/scripts/fresh-eyes-cadence-check.sh --config-block fresh_eyes_program
IF exit 0 (fire):
Output: "▸ Fresh-eyes PROGRAM cadence crossed — assembling shared-purpose briefing"
Invoke /fresh-eyes-program --cadence
IF exit 1 (noop):
# Cadence not crossed OR config disabled — continue silently (no output)
continue
Lower-frequency than fresh-eyes-review (100 vs 25) because shared purpose
changes much less frequently than individual agent identity. See
.claude/skills/fresh-eyes-program/SKILL.md.
Phase 0.5e.7: Fresh-Eyes TREE Cadence Check (S5)
Sibling to Phase 0.5e and 0.5e.5. Every 200 completed goals (configured in
core/config/aspirations.yaml → fresh_eyes_tree.goal_cadence), produce a
local briefing targeting the knowledge tree's top-level taxonomy (the L1 set)
examining:
Are the four L1s still the right top-level cuts for the knowledge tree?
Same infrastructure as the two sibling rituals (fresh-eyes-cadence-check.sh,
fresh-eyes-record-tick.sh), parametrized via --config-block fresh_eyes_tree
so the three rituals use disjoint config blocks and WM slots. If taxonomy
changes are desired, the user invokes the S8 apply scripts
(l1-domain-rename.sh, l1-domain-add.sh) manually.
# Budget meter — Magic Wand 2 (g-115-509). Skip when zone==tight.
Bash: decision=$(bash core/scripts/aspirations-precheck-budget-meter.sh check fresh-eyes-tree-cadence)
IF decision == "drop": SKIP this phase; continue to Phase 0.5f
Bash: core/scripts/fresh-eyes-cadence-check.sh --config-block fresh_eyes_tree
IF exit 0 (fire):
Output: "▸ Fresh-eyes TREE cadence crossed — assembling L1 taxonomy briefing"
Invoke /fresh-eyes-tree --cadence
IF exit 1 (noop):
# Cadence not crossed OR config disabled — continue silently (no output)
continue
Bash: echo "aspirations-precheck phase documented"
Distinct from /tree maintain (per-node structural ops, autonomous +
manual), /reflect Step 7 Tree Health Lint (per-node staleness + width),
and l1-skew-check.py (S1 passive measurement at 50 goals). Those check
NODE health or accumulate quantitative skew evidence; this ritual asks
the structural-design question that no automated gate ever surfaces.
Lower-frequency than program (200 vs 100) because L1 changes are
high-blast-radius. See .claude/skills/fresh-eyes-tree/SKILL.md.
Phase 0.5f: Felt-Sense Check-In Cadence
Periodic autonomous structured 7-lane self-audit. Every 75 completed
goals (configured in core/config/aspirations.yaml →
felt_sense.goal_cadence), the agent runs the full sweep:
memory hygiene, out-of-cycle completions, unblocks, forward backlog,
framework-test gaps, meta tuning, and the felt-sense question ("where
is the pain, what would I change"). Material Self findings route
through guard-380 post-notification; cosmetic findings journal only.
The cadence gate is script-enforced (felt-sense-cadence-check.sh) and
fail-open — if it errors, the loop continues without the sweep.
# Budget meter — Magic Wand 2 (g-115-509). Skip when zone==tight.
Bash: decision=$(bash core/scripts/aspirations-precheck-budget-meter.sh check felt-sense-cadence)
IF decision == "drop": SKIP this phase; continue to Phase 1
Bash: core/scripts/felt-sense-cadence-check.sh
IF exit 0 (fire):
Output: "▸ Felt-sense check-in cadence crossed — running 7-lane sweep"
Invoke /felt-sense-checkin --cadence
IF exit 1 (noop):
# Cadence not crossed OR config disabled — continue silently
continue
Bash: echo "aspirations-precheck phase documented"
Distinct from fresh-eyes-review (Phase 0.5e — joint user+agent meta-Q&A,
asks user for answers) and sq-012 (post-goal, narrow). This is an
autonomous structured self-audit that WRITES outputs directly (tree
nodes, guardrails, goals, verify-learning checks, meta edits) and
notifies the user only when Self changes materially. Lower frequency
(75 vs 25) because the seven lanes are expensive and Lane 7 needs
room to feel earned rather than ritual. See
.claude/skills/felt-sense-checkin/SKILL.md.
Phase 0.5g: L1 Distribution Skew Check (S1 — Tree Taxonomy Review)
Periodic passive observability check. Every 50 completed goals (configured
in core/config/aspirations.yaml → l1_skew_check.goal_cadence), compute
per-L1 distribution (structural mass, retrieval volume, mature capability
mass) and post a coordination-board findings message when any ratio
exceeds the threshold (default 5x).
NOT a user-facing ritual — no email, no pending-question. The board post gives partner agents and /fresh-eyes-tree (S5) cross-session visibility into when the L1 boundaries look wrong. Quiet on balanced state.
The cadence gate and the check itself are both inside l1-skew-check.py
— a single script, one bash call from this phase. Fail-open: any error
prints to stderr and the loop continues. Exit code 1 on noop is silent.
# Budget meter — Magic Wand 2 (g-115-509). Skip when zone==tight.
Bash: decision=$(bash core/scripts/aspirations-precheck-budget-meter.sh check l1-skew-cadence)
IF decision == "drop": SKIP this phase; continue to Phase 1
Bash: core/scripts/l1-skew-check.sh --cadence --post-board
IF exit 0 (fire — cadence crossed, check ran):
# Script printed its JSON verdict to stdout (LLM context). Board post
# already fired if any_flagged. Continue silently to Phase 1.
continue
IF exit 1 (noop — cadence not crossed):
continue
IF exit 2 (stats read error):
# Stderr already noted. Fail-open. Continue.
continue
Bash: echo "aspirations-precheck phase documented"
Distinct from /tree stats (one-shot, depth-only) and /reflect Step 7
Tree Health Lint (per-node staleness + cross-refs + width). Those check
NODE health; this checks TAXONOMY shape. The output feeds the /fresh-eyes-tree
ritual (S5) which assembles the briefing when the cadence-300-goal ritual
fires — board posts make the L1 skew visible BEFORE that joint review,
so partners (alpha/bravo) have signal to interpret on their own iterations.
See core/scripts/l1-skew-check.py and core/scripts/tree.py _compute_by_l1_stats.
Phase 0.5h: Health-Regression Detection + Revert Sweep (health-ledger subsystem)
Periodic self-health regression check + (Phase-3) tiered revert. Spec:
core/config/conventions/health-ledger.md §8–§11. Reads the per-agent health
ledger (agents/<agent>/health/<date>.jsonl, appended each iteration by
iteration-close.sh) and evaluates the triple-condition gate (negative composite
trend AND composite below floor AND below_baseline) plus a
consecutive-below-baseline counter (one-off bad iterations do not trip). On a
trip it identifies the most-degraded component signal, attributes the regression
to recent in-window file changes (ranked + constitutional-ring-classified), files
an Investigate: goal, and — when revert-eligible — routes the top candidate to
a tiered revert.
DORMANT until health_regression.mode advances. In collect-only (the
launch default) the detection script returns tripped:false reason:"mode=collect-only" immediately — this phase is a no-op until the mode is
advanced to detect-and-report (Phase 2) or full (Phase 3). The script also
self-gates on its own interval marker (every detection.interval goals).
Phase 2 = report only. Reverts (Phase 3) require mode == full AND the
calibration AND-gate (30 days AND 50 records) — surfaced as revert_eligible in
the verdict and re-checked inside health-revert.py (the master safety gate;
even a Ring-3 auto candidate routes to not-eligible until both hold). Reverts
are file-granular (one file restored to its pre-regression content via
git show), tagged with a Health-Revert git trailer, and verified
revert.verification_iterations later — kept if the composite improved, else
undone + dead-ended.
# Budget meter — deferrable cadence sweep (sibling to 0.5e/0.5f/0.5g).
Bash: decision=$(bash core/scripts/aspirations-precheck-budget-meter.sh check health-regression-cadence)
IF decision == "drop": SKIP this phase; continue to Phase 1
# (Phase 3) Verify any pending reverts from prior iterations FIRST — keep or
# undo+dead-end each whose verification window has elapsed. No-op unless
# mode==full + calibrated + a pending entry exists. Cheap; runs every iteration.
Bash: bash core/scripts/health-revert.sh verify --json # outcomes logged to context
Bash: verdict=$(bash core/scripts/health-regression-check.sh --json)
Parse verdict JSON.
# Calibration-complete edge (fires ONCE, in ANY mode incl. collect-only). When
# the 30-day/50-record AND-gate is first satisfied, revert authority (Phase 3)
# becomes mathematically eligible. File a one-time goal so the agent proactively
# advances `health_regression.mode` along the rollout rather than silently
# waiting. health-regression-check.py writes a per-agent `.calibrated` marker so
# the edge never re-fires; the dedup query (incl. completed) makes it team-wide
# idempotent — the first agent to calibrate files the single goal.
IF verdict.calibration_just_completed == true:
Bash: existing=$(bash core/scripts/aspirations-query.sh --status pending,in-progress,completed --contains "<verdict.calibration_dedup_key>")
IF existing is empty:
Bash: bash core/scripts/aspirations-add-goal.sh asp-001 \
--title "health-ledger calibration complete — advance health_regression.mode when ready" \
--priority MEDIUM --participants agent,user --category framework-architecture \
--status pending \
--description "The health-ledger calibration AND-gate is now satisfied (<verdict.calibration.days> days / <verdict.calibration.records> records). Revert authority (Phase 3) is mathematically eligible. The rollout advances by editing health_regression.mode in core/config/aspirations.yaml; each step is reversible. (1) collect-only -> detect-and-report is LOW risk (adds Investigate reports, NEVER reverts) — agent-judgable. (2) detect-and-report -> full GRANTS the agent authority to auto-revert its own Ring-3 framework changes (Ring 1.5/2 route to agent/user Unblocks, Ring 1 to the user) — this is a deliberate, user-paced authority grant: leave at detect-and-report and let the user advance to full. Spec: core/config/conventions/health-ledger.md §10. dedup:<verdict.calibration_dedup_key>"
IF verdict.tripped != true:
# collect-only no-op, interval not elapsed, or gate not tripped — all silent.
continue to Phase 1
# TRIPPED (only reachable in detect-and-report / full mode). Dedup, then file.
Bash: existing=$(bash core/scripts/aspirations-query.sh --status pending,in-progress --contains "<verdict.dedup_key>")
IF existing is non-empty:
# An open Investigate for this regression already exists — do not double-file.
continue to Phase 1
Compose the Investigate description from the verdict:
- degraded signal, window [after → before]
- composite vs baseline, composite_trend, consecutive count
- top attribution candidates: each "<score> ring=<ring> <authority> <path> (<commit>)"
- evolution_change_in_window (if true: "NOTE: a meta-strategy change occurred
in this window — the dip may be an intended evolution experiment, not a bug")
- calibration status + revert_eligible (so the reader knows whether Phase-3
reverts are active yet)
- the dedup_key (for the next sweep's dedup query)
Bash: bash core/scripts/aspirations-add-goal.sh asp-001 \
--title "Investigate: health regression on <verdict.signal>" \
--priority MEDIUM --participants agent --category framework-architecture \
--description "<composed description above>"
# (Phase 3) Tiered revert — only acts when verdict.revert_eligible (mode==full
# AND calibrated). The route command re-checks the gate internally, so passing a
# non-eligible verdict is a safe no-op.
IF verdict.revert_eligible == true:
# OD-7 courtesy: if the top candidate's file was last committed by ANOTHER
# agent (git log -1 --format=%an <path>), post a coordination-board courtesy
# note BEFORE routing, so the partner sees the revert. (Mirror goal deferred.)
Bash: action=$(bash core/scripts/health-revert.sh route --verdict "$verdict" --json)
Parse action JSON:
- decision == "auto-revert": the file was reverted + tracked as pending
(the verify sweep above will keep/undo it later). Note action.revert.commit.
- decision in ("agent-unblock","user-unblock"): file the action.unblock spec
via aspirations-add-goal.sh (participants from action.unblock.participants;
for user-unblock, also notify the user via the forged notification skill
per .claude/rules/forged-skill-resolution.md).
- decision in ("not-eligible","skip-ring0"): no-op.
continue to Phase 1
Bash: echo "aspirations-precheck phase documented"
Why the Investigate is participants: agent (not user): detection surfaces an
agent-diagnosable condition (attribution + revert are agent-capable per
.claude/rules/capability-before-user.md). User involvement happens at REVERT
time and ONLY for Ring-1 candidates (user-unblock), where the user owns the
file's intent — never at detection time.
Phase 1: Recurring Goal Check
check_recurring_goals()
# Ensures recurring goals are properly tracked and due goals are flagged
Phase 2: Budget Meter End (Magic Wand 2 — g-115-509)
Finalize the precheck budget meter — writes a summary record (sweeps_ran,
sweeps_dropped, total_elapsed_ms) to agents/<agent>/session/precheck-drops.jsonl
and clears the per-iteration state file. One-shot — do not call without a
preceding meter start (Step 0a).
Bash: bash core/scripts/aspirations-precheck-budget-meter.sh end
Chaining
- Called by:
/aspirationsorchestrator (every iteration, first phase) - Calls:
aspirations-read.sh,aspirations-meta-update.sh,guardrail-check.sh,infra-health.sh,wm-read.sh,wm-set.sh,aspiration-trajectory.sh(cycle detection),aspirations-add-goal.sh(cycle detection, hypothesis pipeline, accuracy gate),aspirations-query.sh(user-goal reclassification),aspirations-update-goal.sh(user-goal reclassification),world-cat.sh(capability-routing convention),pipeline-read.sh(hypothesis pipeline + accuracy health),fresh-eyes-cadence-check.sh(Phase 0.5e gate),recurring-precondition-sweep.py(Phase 0.5c),health-regression-check.sh(Phase 0.5h detection sweep),health-revert.sh(Phase 0.5h verify + tiered revert),/create-aspiration(health + pipeline depth),/fresh-eyes-review --cadence(Phase 0.5e fire), CREATE_BLOCKER protocol - Reads: Aspirations compact, working memory (blockers), guardrails, trajectory data (cycle detection), pipeline meta (hypothesis counts + accuracy),
core/config/aspirations.yaml(pipeline_low_water_mark, hypothesis_pipeline_low_water_mark, accuracy_critical_threshold, accuracy_min_sample)
Return Protocol
See .claude/rules/return-protocol.md — last action must be a tool call, not text.
The terminal action is the last wm-set.sh, aspirations-read.sh, or
aspirations-add-goal.sh call. Never end with a text summary.