Files
pi-skills/.agents/skills/evolve/references/work-selection-ladder.md
T

10 KiB
Raw Blame History

/evolve — Work-Selection Ladder (Step 3, full procedure)

Extracted from skills/evolve/SKILL.md Step 3 to keep the skill under the 10000-token ceiling (soc-opq5). SKILL.md keeps the summary + this pointer; this file owns the full per-rung procedure, code blocks, quality cascade, and dormancy hard-gate.

Selection is a ladder, not a one-shot check. After every productive cycle, return to the TOP of this step and re-read the queue before considering dormancy.

Programmatic recommendation (soc-g2qd wire): when present, consult the ladder primitive first and prefer its .recommended_bead; the rungs below are the cross-check + fallback.

ao loop next-work --help >/dev/null 2>&1 && RECO_BEAD=$(ao loop next-work --json 2>/dev/null | jq -r '.recommended_bead // empty')

When a repo-local program contract exists, apply a scope filter before Step 4:

  • candidate work that clearly requires immutable-scope edits is not eligible for direct execution
  • prefer harvested, beads, goals, and generated work that can plausibly land within mutable scope
  • if the selected item is inherently out of scope, escalate it or convert it into durable follow-up work instead of invoking /rpi and hoping discovery widens scope

Step 3.0: Scope filter — split-or-defer, never bail (soc-5qit)

Before claiming a candidate, gate scope vs session budget. If the work touches > 5 non-uniform files, introduces a new shape (schema field, validator, contract surface), is operator-level epic work, OR PRODUCTIVE_THIS_SESSION > 5 and would extend an arc rather than close one — route to scout-mode, which MUST produce one of:

  1. Splitbd create 2-N child beads (each ≤5 files, single-shape) with --deps discovered-from:<parent-id>, annotate parent, then re-enter Step 3 so the smallest child (or another ready bead) gets claimed THIS cycle.
  2. Defer — annotate the candidate with defer:<reason> and re-enter Step 3 so the next-priority ready bead gets claimed.
  3. Park (rare) — bd update <id> --status blocked --notes "scope-too-big" and re-enter Step 3.

Scout NEVER returns "no work done." If bd ready ≥1, the loop MUST claim one this cycle. See references/scout-mode.md and references/mechanical-batches.md.

Metronome gate: read mode_repeat_streak from session-state.json. If >= 3 AND the candidate would repeat the trailing run's mode, BLOCK this rung and jump to the next. If >= 5, record the gap as bead/provenance evidence and require operator override. See references/metronome-gate.md.

Step 3.1: Harvested work first

Read .agents/rpi/next-work.jsonl and pick the highest-value unconsumed item. Prefer exact repo match, then concrete implementation work, then higher severity. Read references/knowledge-loop-integration.md for the claim/release protocol.

Step 3.2: Open ready beads

If no harvested item is ready, check bd ready. Pick the highest-priority unblocked issue.

Step 3.3: Failing goals and directive gaps (skip if --beads-only)

First assess directives, then goals:

  • top-priority directive gap from ao goals measure --directives
  • highest-weight failing goals (skip quarantined oscillators)
  • lower-weight failing goals

This step exists even when all queued work is empty. Goals are the third source, not the stop condition.

DIRECTIVES=$(ao goals measure --directives 2>/dev/null)
FAILING=$(jq -r '.goals[] | select(.result=="fail") | .id' .agents/evolve/fitness-latest.json | head -1)

Oscillation check: Before working a failing goal, check if it has oscillated (improved-to-fail transitions >= 3 times). If so, quarantine it and try the next goal. See references/oscillation.md and references/fitness-scoring.md for the detection procedure.

Duplicate-work guard (mandatory before every generator bd create). The generators below (3.43.7) and the Step-4 Split rung all create beads. A stale phase-1 handoff repeatedly re-seeded beads for work already covered by an existing bead or merged PR (ag-b8m≈ag-jov, ag-6kw≈ag-c2i — ag-6jt). Before bd create, run the guard; skip creation when it reports a duplicate:

skills/evolve/scripts/duplicate-work-guard.sh "<candidate title>" || {
  echo "skip: existing work already covers this surface"; }
# exit 1 + "DUPLICATE: <id> [<status>] <title>" → an open/closed bead already
# covers it (exact title OR significant-token overlap). exit 0 → safe to create.

This complements the loop's origin/main fast-forward: the cron runs skills/evolve/scripts/sync-main-to-origin.sh before discovery (wired into scripts/overnight-evolve.sh), which fetches origin and fast-forwards local main to origin/main so discovery diffs candidate slices against the true merge base — already-merged work reports as done, not re-seen (ag-6jt). Run it manually in any rpi worktree whose local main may be stale:

skills/evolve/scripts/sync-main-to-origin.sh
# → "DIFF_BASE: origin/main <sha>" — diff slices against THIS, not local main.

Step 3.4: Testing improvements

When queues and goals are empty, generate concrete testing work via /test:

if --no-lifecycle is NOT set:
  Skill(skill="test", args="coverage")
  Only files with < 40% coverage become queue items (severity threshold).

If /test is unavailable or --no-lifecycle is set, fall back to manual scanning:

  • find packages/files with thin or missing tests
  • look for missing regression tests around recent bug-fix paths
  • identify flaky or absent headless/runtime smokes

Convert any real finding into durable work:

  • add a bead when the work needs tracked backlog ownership, or
  • append a queue item under the shared next-work contract when it should flow directly back into /rpi

Step 3.5: Validation tightening and bug-hunt passes

If testing improvement generation returns nothing, run lifecycle generators then bug-hunt sweeps:

if --no-lifecycle is NOT set:
  a) Skill(skill="deps", args="audit")
     Only deps with CVSS >= 7.0 or 2+ major versions behind become queue items.

  b) if perf-sensitive code detected (benchmarks exist, hot path patterns):
       Skill(skill="perf", args="profile --quick")
       Convert significant perf findings to queue items.

If lifecycle generators return nothing or are skipped, fall back to manual sweeps:

  • missing validation gates
  • weak lint/contract coverage
  • bug-hunt style audits for risky areas
  • stale assumptions between docs, contracts, and runtime truth

Again: convert findings into beads or queue items, then immediately select the highest-priority result and continue.

Step 3.6: Drift / hotspot / dead-code mining

If the prior generators are empty, mine for complexity debt via /refactor:

if --no-lifecycle is NOT set:
  Skill(skill="refactor", args="--sweep all --dry-run")
  Only functions with CC > 20 become queue items (severity threshold).

If /refactor is unavailable or --no-lifecycle is set, fall back to manual mining:

  • complexity hotspots
  • stale TODO/FIXME markers
  • dead code
  • stale docs
  • stale research
  • drift between generated artifacts and source-of-truth files

Do not stop here. Normalize findings into tracked work and continue.

Step 3.7: Feature suggestions

If all concrete remediation layers are empty, propose one or more specific feature ideas grounded in the repo purpose, write them as durable work, and continue:

  • create a bead when the feature needs review/backlog treatment
  • or append a queue item with source: "feature-suggestion" when it is ready for the next /rpi cycle

Quality mode (--quality) — inverted cascade (findings before directives):

Step 3.0q: Unconsumed high-severity post-mortem findings:

HIGH=$(jq -r 'select(.consumed==false) | .items[] | select(.severity=="high") | .title' \
  .agents/rpi/next-work.jsonl 2>/dev/null | head -1)

Step 3.1q: Unconsumed medium-severity findings.

Step 3.2q: Open ready beads.

Step 3.3q: Emergency gates (weight >= 5) and top directive gaps.

Step 3.4q: Testing improvements.

Step 3.5q: Validation tightening / bug-hunt / drift mining.

Step 3.6q: Feature suggestions.

This inverts the standard cascade only at the top of the ladder: findings BEFORE goals and directives. It does NOT skip the generator layers.

When evolve picks a finding, claim it first in next-work.jsonl:

  • Set claim_status: "in_progress", claimed_by: "evolve-quality:cycle-N", claimed_at: "<timestamp>"
  • Set consumed: true only after the /rpi cycle and regression gate succeed
  • If the /rpi cycle fails (regression), clear the claim and leave consumed: false

See references/quality-mode.md for scoring and full details.

Nothing found? HARD GATE — dormancy only when ALL sources empty (soc-5qit):

READY_BEADS=$(bd ready --json 2>/dev/null | jq -r 'length // 0' 2>/dev/null || echo 0)
HARVESTED=$(jq -r 'select(.consumed==false) | .severity' .agents/rpi/next-work.jsonl 2>/dev/null | wc -l | tr -d ' ')
FAILING_GOALS=$(jq -r '.goals[] | select(.result=="fail") | .id' .agents/evolve/fitness-latest.json 2>/dev/null | wc -l | tr -d ' ')
IDLE_STREAK=$(jq -r '.idle_streak // 0' .agents/evolve/session-state.json 2>/dev/null)

if [ "$READY_BEADS" -gt 0 ] || [ "$HARVESTED" -gt 0 ] || [ "$FAILING_GOALS" -gt 0 ]; then
  continue  # work exists — loop back to Step 3 (agile invariant)
fi
if [ "${GENERATOR_EMPTY_STREAK:-0}" -ge 2 ] && [ "${IDLE_STREAK:-0}" -ge 2 ]; then
  REASON="stagnation: all sources empty x3"
  # soc-g2qd wire: under loop, write-stop-marker refuses → log blocked + operator-wait, never self-halt (ADR-0007).
  if ao loop write-stop-marker --help >/dev/null 2>&1; then
    ao loop write-stop-marker --marker dormant --reason "$REASON" --mode loop 2>/dev/null \
      || ao loop blocked --reason "$REASON" --needed-context "queue empty; operator adds work or marker" 2>/dev/null || true
  else
    printf '%s\n%s\n%s\n' "cycle $CYCLE" "$(date -u +%FT%TZ)" "$REASON" > .agents/evolve/DORMANT  # fallback
  fi
fi

Agile invariant (soc-5qit): bd ready ≥ 1 ⇒ loop NEVER writes DORMANT, NEVER exits. The only path to DORMANT is fully empty backlog + dry generators. Context exhaustion → HANDOFF, not DORMANT.

If work layers were empty but generators haven't exhausted 3 passes yet, persist GENERATOR_EMPTY_STREAK and loop back to Step 1.

A cycle is idle only if NO work source returned actionable work and every generator layer also came up empty. A cycle that targeted an oscillating goal and skipped it counts as idle only after the remaining ladder was exhausted.

If --dry-run: report what would be worked on and go to Teardown.