fix: wire flywheel automation into skill paths

This commit is contained in:
Boden Fuller
2026-04-03 10:47:44 -04:00
parent 5db2d4b29b
commit 2177edb30f
10 changed files with 296 additions and 24 deletions
+3
View File
@@ -470,6 +470,9 @@ func performCodexStop(cwd string) (codexStopResult, error) {
}
closeLoop = &result
}
if err := performHooklessSessionEndMaintenance(cwd); err != nil {
VerbosePrintf("Warning: codex session-end maintenance: %v\n", err)
}
memoryPath, err := syncCodexMemory(cwd)
if err != nil {
+73
View File
@@ -700,6 +700,79 @@ func TestCodexEnsureStopJSONSkipsDuplicateCloseoutForSameSession(t *testing.T) {
}
}
func TestCodexEnsureStopRunsSessionEndMaintenanceParity(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)
t.Setenv("CODEX_THREAD_ID", "019d1bf7-58ea-79e1-9f5d-02109d930082")
t.Setenv("CODEX_INTERNAL_ORIGINATOR_OVERRIDE", "Codex Desktop")
sessionID := "019d1bf7-58ea-79e1-9f5d-02109d930082"
historyPath := filepath.Join(home, ".codex", "history.jsonl")
indexPath := filepath.Join(home, ".codex", "session_index.jsonl")
if err := os.MkdirAll(filepath.Dir(historyPath), 0o755); err != nil {
t.Fatal(err)
}
history := []string{
`{"session_id":"` + sessionID + `","ts":1766945655,"text":"Close out Codex session with maintenance parity"}`,
`{"session_id":"` + sessionID + `","ts":1766945658,"text":"Archive stale uncited low-signal learnings automatically"}`,
}
if err := os.WriteFile(historyPath, []byte(strings.Join(history, "\n")+"\n"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(indexPath, []byte(`{"id":"`+sessionID+`","thread_name":"Lifecycle maintenance parity","updated_at":"2026-03-23T12:00:00Z"}`+"\n"), 0o644); err != nil {
t.Fatal(err)
}
repo := t.TempDir()
learningsDir := filepath.Join(repo, ".agents", "learnings")
if err := os.MkdirAll(learningsDir, 0o755); err != nil {
t.Fatal(err)
}
stalePath := filepath.Join(learningsDir, "stale-fragment.md")
staleLearning := `---
id: stale-fragment
type: learning
date: 2026-01-01
source: codex-test
maturity: provisional
utility: 0.5000
confidence: 0.0000
reward_count: 0
helpful_count: 0
harmful_count: 0
---
# Stale Fragment
let me force the revert because the goals agent is still running and re-wrote it.
`
if err := os.WriteFile(stalePath, []byte(staleLearning), 0o644); err != nil {
t.Fatal(err)
}
oldTime := time.Now().AddDate(0, 0, -91)
if err := os.Chtimes(stalePath, oldTime, oldTime); err != nil {
t.Fatal(err)
}
origDir, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
if err := os.Chdir(repo); err != nil {
t.Fatal(err)
}
defer func() { _ = os.Chdir(origDir) }()
out, err := executeCommand("codex", "ensure-stop", "--json")
if err != nil {
t.Fatalf("codex ensure-stop --json: %v\noutput: %s", err, out)
}
if _, err := os.Stat(filepath.Join(repo, ".agents", "archive", "learnings", "stale-fragment.md")); err != nil {
t.Fatalf("expected stale learning archived by hookless closeout maintenance: %v", err)
}
}
func TestCodexStatusJSONReflectsHooklessHealthAndSearchCitations(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)
@@ -0,0 +1,159 @@
package main
import (
"fmt"
"os"
"os/exec"
"path/filepath"
)
type aoMaintenanceGlobals struct {
dryRun bool
output string
dedupMerge bool
maturityApply bool
maturityScan bool
maturityCurate bool
maturityExpire bool
maturityArchive bool
maturityEvict bool
maturityGlobal bool
maturityMigrateMd bool
maturityRecalc bool
uncitedDays int
}
func snapshotAOMaintenanceGlobals() aoMaintenanceGlobals {
return aoMaintenanceGlobals{
dryRun: dryRun,
output: output,
dedupMerge: dedupMerge,
maturityApply: maturityApply,
maturityScan: maturityScan,
maturityCurate: maturityCurate,
maturityExpire: maturityExpire,
maturityArchive: maturityArchive,
maturityEvict: maturityEvict,
maturityGlobal: maturityGlobal,
maturityMigrateMd: maturityMigrateMd,
maturityRecalc: maturityRecalibrate,
uncitedDays: maturityUncitedDays,
}
}
func restoreAOMaintenanceGlobals(state aoMaintenanceGlobals) {
dryRun = state.dryRun
output = state.output
dedupMerge = state.dedupMerge
maturityApply = state.maturityApply
maturityScan = state.maturityScan
maturityCurate = state.maturityCurate
maturityExpire = state.maturityExpire
maturityArchive = state.maturityArchive
maturityEvict = state.maturityEvict
maturityGlobal = state.maturityGlobal
maturityMigrateMd = state.maturityMigrateMd
maturityRecalibrate = state.maturityRecalc
maturityUncitedDays = state.uncitedDays
}
func withWorkingDir(dir string, fn func() error) error {
origDir, err := os.Getwd()
if err != nil {
return fmt.Errorf("get working directory: %w", err)
}
if err := os.Chdir(dir); err != nil {
return fmt.Errorf("chdir %s: %w", dir, err)
}
defer func() { _ = os.Chdir(origDir) }()
return fn()
}
func withSuppressedOutput(fn func() error) error {
devNull, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0)
if err != nil {
return fn()
}
defer devNull.Close()
oldStdout := os.Stdout
oldStderr := os.Stderr
os.Stdout = devNull
os.Stderr = devNull
defer func() {
os.Stdout = oldStdout
os.Stderr = oldStderr
}()
return fn()
}
func bestEffortPruneAgents(cwd string) {
if os.Getenv("AGENTOPS_AUTO_PRUNE") == "0" {
return
}
script := filepath.Join(cwd, "scripts", "prune-agents.sh")
if _, err := os.Stat(script); err != nil {
return
}
cmd := exec.Command("bash", script, "--execute", "--quiet")
cmd.Dir = cwd
cmd.Stdout = nil
cmd.Stderr = nil
_ = cmd.Run()
}
func performHooklessSessionEndMaintenance(cwd string) error {
previous := snapshotAOMaintenanceGlobals()
defer restoreAOMaintenanceGlobals(previous)
return withWorkingDir(cwd, func() error {
return withSuppressedOutput(func() error {
dryRun = false
output = "table"
dedupMerge = true
maturityApply = false
maturityScan = false
maturityGlobal = false
maturityMigrateMd = false
maturityRecalibrate = false
maturityUncitedDays = 60
if err := runDedup(nil, nil); err != nil {
return fmt.Errorf("dedup maintenance: %w", err)
}
if err := runContradict(nil, nil); err != nil {
return fmt.Errorf("contradiction maintenance: %w", err)
}
if os.Getenv("AGENTOPS_EVICTION_DISABLED") != "1" {
maturityArchive = true
maturityExpire = true
maturityEvict = false
maturityCurate = false
if err := runMaturityExpire(nil); err != nil {
return fmt.Errorf("expiry maintenance: %w", err)
}
maturityExpire = false
maturityEvict = true
maturityCurate = false
if err := runMaturityEvict(nil); err != nil {
return fmt.Errorf("eviction maintenance: %w", err)
}
maturityExpire = false
maturityEvict = false
maturityCurate = true
if err := runMaturityCurate(nil); err != nil {
return fmt.Errorf("curation maintenance: %w", err)
}
}
bestEffortRefreshFindingCompiler(cwd)
bestEffortPruneAgents(cwd)
return nil
})
})
}
@@ -13,7 +13,7 @@ AgentOps originally assumed a hook-capable runtime lifecycle such as Claude/Open
| Mode | Detection | Start path | Closeout path | Guarantees |
|------|-----------|------------|---------------|------------|
| `hook-capable` | Claude/OpenCode runtime plus installed hook surfaces | SessionStart hook or explicit `ao inject` | SessionEnd/Stop hooks or explicit `ao forge transcript` + `ao flywheel close-loop` | Startup injection and close-loop maintenance can be automatic when hooks are installed |
| `codex-hookless-fallback` | Codex env/session metadata and no hook surface under `~/.codex` | `ao codex start` or skill-driven `ao codex ensure-start` | `ao codex stop` or skill-driven `ao codex ensure-stop` | Explicit startup context, transcript discovery fallback, citation capture, close-loop maintenance, and persisted lifecycle state |
| `codex-hookless-fallback` | Codex env/session metadata and no hook surface under `~/.codex` | `ao codex start` or skill-driven `ao codex ensure-start` | `ao codex stop` or skill-driven `ao codex ensure-stop` | Explicit startup context, transcript discovery fallback, citation capture, session-end-equivalent maintenance, and persisted lifecycle state |
| `manual` | No hooks and no Codex-specific runtime detection | `ao inject` / `ao lookup` | `ao forge transcript` + `ao flywheel close-loop` | Portable low-level workflow with no hidden lifecycle assumptions |
## Command Responsibilities
@@ -41,6 +41,7 @@ AgentOps originally assumed a hook-capable runtime lifecycle such as Claude/Open
- Fall back to a synthesized transcript from `~/.codex/history.jsonl` when no archive exists.
- Forge/extract from the resolved transcript and queue or persist learnings safely.
- Run close-loop maintenance unless `--no-close-loop` is set.
- Run the same post-close hygiene that SessionEnd owns in hook-capable runtimes: deduplication, contradiction scan, expiry/eviction/curation maintenance, finding-compiler refresh, and auto-prune.
- Sync `MEMORY.md` and persist stop state to `.agents/ao/codex/state.json`.
### `ao codex ensure-stop`
+18 -13
View File
@@ -2,7 +2,7 @@
> Which `ao` commands are called by which skills and hooks — and vice versa.
Auto-audited 2026-03-24. 53 CLI commands, 52 source skills, 7 runtime hook event sections.
Auto-audited 2026-04-03. 53 CLI commands, 52 source skills, 7 runtime hook event sections.
Source-of-truth note: `hooks/hooks.json` currently declares 7 runtime hook event sections. Repository hook scripts such as `worktree-setup.sh` are support/setup scripts and are listed separately when relevant.
@@ -12,8 +12,8 @@ Registry-first note: `/plan`, `/pre-mortem`, `/vibe`, and `/post-mortem` now als
| Category | Count |
|----------|-------|
| CLI commands with skill/hook callers | 30 |
| Orphan commands (user utilities, hidden, CI-only) | 20 |
| CLI commands with skill/hook callers | 31 |
| Orphan commands (user utilities, hidden, CI-only) | 19 |
| Phantom subcommands (bugs) | 2 |
---
@@ -30,24 +30,25 @@ Every `ao` command that is actively called by at least one skill or hook.
| `ao goals` | goals, evolve | — |
| `ao search` | crank, inject, plan, pre-mortem, provenance, research, using-agentops, vibe | session-start.sh |
| `ao rpi` | council, crank, plan, quickstart, research, rpi, shared, swarm | — |
| `ao flywheel` | crank, evolve, flywheel, post-mortem, quickstart, retro, status | ao-flywheel-close.sh |
| `ao flywheel` | crank, evolve, flywheel, post-mortem, quickstart, retro, status | ao-flywheel-close.sh, session-end-maintenance.sh |
| `ao pool` | crank, status | session-end-maintenance.sh |
| `ao lookup` | crank, implement, inject, plan, research, using-agentops | session-start.sh |
| `ao lookup` | crank, implement, inject, plan, pre-mortem, research, using-agentops | session-start.sh |
| `ao context` | crank, implement, swarm | context-guard.sh |
| `ao codex` | brainstorm, crank, discovery, handoff, implement, post-mortem, quickstart, recover, research, rpi, status, using-agentops, validation | — |
| `ao maturity` | flywheel | session-end-maintenance.sh |
| `ao constraint` | flywheel, post-mortem, retro | — |
| `ao badge` | flywheel, status | — |
| `ao retrieval-bench` | flywheel | — |
| `ao seed` | quickstart | — |
| `ao notebook` | retro | session-start.sh |
| `ao memory` | — | session-end-maintenance.sh |
| `ao dedup` | flywheel | session-end-maintenance.sh |
| `ao contradict` | flywheel | session-end-maintenance.sh |
| `ao metrics` | flywheel | — |
| `ao metrics` | flywheel | session-end-maintenance.sh |
| `ao extract` | — | session-start.sh |
| `ao hooks` | quickstart | — |
| `ao init` | quickstart | — |
| `ao session` | post-mortem, retro | — |
| `ao session` | post-mortem, retro | session-end-maintenance.sh |
| `ao temper` | post-mortem | — |
| `ao curate` | flywheel | — |
| `ao status` | flywheel, quickstart | — |
@@ -67,15 +68,15 @@ Which `ao` commands each skill invokes.
| **crank** | `codex ensure-start`, `context assemble`, `flywheel close-loop`, `flywheel status`, `forge transcript`, `inject`, `lookup`, `pool list`, `ratchet record`, `ratchet status`, `rpi phased`, `search` |
| **discovery** | `codex ensure-start` |
| **evolve** | `forge`, `goals measure`, `inject` |
| **flywheel** | `badge`, `constraint review`, `contradict`, `curate status`, `dedup`, `maturity`, `metrics cite-report`, `metrics health`, `anti-patterns`, `status` |
| **flywheel** | `badge`, `constraint review`, `contradict`, `curate status`, `dedup`, `maturity`, `metrics cite-report`, `metrics health`, `anti-patterns`, `retrieval-bench`, `status` |
| **forge** | `forge markdown`, `forge transcript` |
| **goals** | `goals add`, `goals drift`, `goals export`, `goals history`, `goals init`, `goals measure`, `goals meta`, `goals migrate`, `goals prune`, `goals steer`, `goals validate` |
| **handoff** | `codex ensure-stop`, `ratchet status` |
| **implement** | `codex ensure-start`, `context assemble`, `lookup`, `ratchet record`, `ratchet skip`, `ratchet spec`, `ratchet status` |
| **inject** | `inject`, `lookup`, `search` |
| **plan** | `ratchet record`, `rpi cleanup`, `rpi status`, `search` |
| **plan** | `lookup`, `ratchet record`, `rpi cleanup`, `rpi status`, `search` |
| **post-mortem** | `codex ensure-stop`, `constraint activate`, `flywheel close-loop`, `forge`, `forge markdown`, `session close`, `temper validate` |
| **pre-mortem** | `ratchet record`, `search` |
| **pre-mortem** | `lookup`, `ratchet record`, `search` |
| **provenance** | `search` |
| **quickstart** | `codex ensure-start`, `codex ensure-stop`, `codex status`, `flywheel status`, `hooks install`, `hooks test`, `init`, `rpi phased`, `seed`, `status` |
| **ratchet** | `ratchet check`, `ratchet record`, `ratchet skip`, `ratchet status` |
@@ -189,11 +190,12 @@ During Session
Session End
→ session-end-maintenance.sh
→ ao session close --auto-extract
→ ao forge transcript
→ ao notebook update
→ ao flywheel close-loop
→ ao metrics baseline
→ ao memory sync
→ ao pool ingest
→ ao maturity --expire --evict
→ ao maturity --expire --evict --curate
→ ao dedup
→ ao contradict
@@ -232,6 +234,9 @@ Codex Thread Closeout
→ later calls no-op for the same thread
→ ao forge transcript (archived transcript or history fallback)
→ ao flywheel close-loop
→ ao dedup
→ ao contradict
→ ao maturity --expire --evict --curate
Codex Health
→ ao codex status
+3 -2
View File
@@ -60,7 +60,8 @@ ao codex stop
For normal Codex skill usage, entry skills drive the same startup path with
`ao codex ensure-start`, and closeout-owner skills drive the same closeout path
with `ao codex ensure-stop`.
with `ao codex ensure-stop`, including the post-close maintenance that hook-capable
SessionEnd would normally run.
### Option 4: Slash Commands (Hook-Capable Power Users)
@@ -84,7 +85,7 @@ with `ao codex ensure-stop`.
| Mode | Start | Closeout | Notes |
|------|-------|----------|-------|
| Hook-capable | Natural language, `/session-start`, or startup hooks | Natural language, `/session-end`, or session-end hooks | Best fit for Claude/OpenCode when hooks are installed; `CLAUDE.md` is the startup surface and hooks stage state silently |
| Codex hookless fallback | `ao factory start --goal "<goal>"`, `ao codex start`, or skill-driven `ao codex ensure-start` | `ao codex stop` or skill-driven `ao codex ensure-stop` | No startup/session-end hook surface under `~/.codex`; lifecycle is explicit |
| Codex hookless fallback | `ao factory start --goal "<goal>"`, `ao codex start`, or skill-driven `ao codex ensure-start` | `ao codex stop` or skill-driven `ao codex ensure-stop` | No startup/session-end hook surface under `~/.codex`; lifecycle is explicit, and closeout owns the same curation hygiene as SessionEnd |
| Manual fallback | `ao inject`, `ao lookup` | `ao forge transcript`, `ao flywheel close-loop` | Lowest-level portable path |
---
+12 -2
View File
@@ -121,9 +121,15 @@ if command -v ao &>/dev/null; then
ao metrics health 2>/dev/null || true
ao metrics cite-report --days 30 2>/dev/null || true
# Active pruning: archive stale, evict low-utility
# Active pruning: archive stale, evict low-utility, and curate noisy uncited learnings
ao maturity --expire --archive 2>/dev/null || true
ao maturity --evict --archive 2>/dev/null || true
ao maturity --curate --archive 2>/dev/null || true
# Retrieval quality: use the representative live corpus when it exists
if [ -d cli/cmd/ao/testdata/retrieval-bench-live ]; then
ao retrieval-bench --live --corpus cli/cmd/ao/testdata/retrieval-bench-live --json 2>/dev/null || true
fi
else
echo "ao CLI not available — using file-based metrics"
@@ -189,6 +195,11 @@ Health indicator: >90% = Healthy, 70-90% = Warning, <70% = Critical.
- Stale (90d uncited): <count>
- Status: <Healthy/Warning/Critical>
## Retrieval Quality
- Live corpus coverage: <percentage or unavailable>
- Live corpus learnings: <count or unavailable>
- Status: <Healthy/Warning/Critical>
## Health Status
<Healthy/Warning/Critical>
@@ -263,4 +274,3 @@ Read `references/cache-eviction.md` for the full eviction pipeline (passive trac
- `scripts/artifact-consistency.sh`
- `scripts/validate.sh`
+2 -2
View File
@@ -40,9 +40,10 @@ Use Grep to search `.agents/` for related content. If research exists, read it t
```bash
if command -v ao &>/dev/null; then
ao search "<topic> plan decomposition patterns" 2>/dev/null | head -10
ao lookup --query "<goal>" --limit 5 2>/dev/null | head -30
fi
```
If ao returns relevant learnings or patterns, incorporate them into the plan. Skip silently if ao is unavailable or returns no results.
If ao returns relevant learnings or patterns, incorporate them into the plan. Cite any retrieved learning by filename when it influences a planning decision. Skip silently if ao is unavailable or returns no results.
### Step 2.1: Load Compiled Prevention First (Mandatory)
@@ -743,4 +744,3 @@ See [references/examples.md](references/examples.md) for more troubleshooting sc
- `scripts/validate.sh`
+12 -3
View File
@@ -42,7 +42,17 @@ ls -lt .agents/specs/ 2>/dev/null | head -3
Use the most recent file. If nothing found, ask user.
### Step 1.4: Load Compiled Prevention First (Mandatory)
### Step 1.4: Retrieve Prior Learnings (Mandatory)
Before review, retrieve learnings relevant to this plan's domain:
```bash
if command -v ao &>/dev/null; then
ao lookup --query "<plan goal or title>" --limit 5 2>/dev/null | head -30
fi
```
If learnings are returned, include them as `known_context` in the review packet. Cite any learning by filename when it influences a prediction. Skip silently if ao is unavailable or returns no results.
### Step 1.4b: Load Compiled Prevention First (Mandatory)
Before quick or deep review, load compiled checks from `.agents/pre-mortem-checks/*.md` when they exist. This is separate from flywheel search and does NOT get skipped by `--quick`.
@@ -71,7 +81,7 @@ Use the same ranked packet contract as `$plan`: compiled checks first, then acti
**By default, pre-mortem runs inline (`--quick`)** — single-agent structured review, no spawning. This catches real implementation issues at ~10% of full council cost (proven in ag-nsx: 3 actionable bugs found inline that would have caused runtime failures).
In `--quick` mode, **skip Steps 1a and 1b** (knowledge search, product context) unless `--deep`, `--mixed`, `--debate`, or `--explorers` is set. These pre-processing steps are for multi-judge council packets only.
In `--quick` mode, **skip Steps 1a and 1b** (knowledge search, product context) unless `--deep`, `--mixed`, `--debate`, or `--explorers` is set. The mandatory `ao lookup` retrieval in Step 1.4 and compiled prevention load in Step 1.4b still run in quick mode.
To escalate to full multi-judge council, use `--deep` (4 judges) or `--mixed` (cross-vendor).
@@ -487,4 +497,3 @@ $pre-mortem ← You are here
- `scripts/validate.sh`
+12 -1
View File
@@ -134,9 +134,15 @@ if command -v ao &>/dev/null; then
ao metrics health 2>/dev/null || true
ao metrics cite-report --days 30 2>/dev/null || true
# Active pruning: archive stale, evict low-utility
# Active pruning: archive stale, evict low-utility, and curate noisy uncited learnings
ao maturity --expire --archive 2>/dev/null || true
ao maturity --evict --archive 2>/dev/null || true
ao maturity --curate --archive 2>/dev/null || true
# Retrieval quality: use the representative live corpus when it exists
if [ -d cli/cmd/ao/testdata/retrieval-bench-live ]; then
ao retrieval-bench --live --corpus cli/cmd/ao/testdata/retrieval-bench-live --json 2>/dev/null || true
fi
else
echo "ao CLI not available — using file-based metrics"
@@ -202,6 +208,11 @@ Health indicator: >90% = Healthy, 70-90% = Warning, <70% = Critical.
- Stale (90d uncited): <count>
- Status: <Healthy/Warning/Critical>
## Retrieval Quality
- Live corpus coverage: <percentage or unavailable>
- Live corpus learnings: <count or unavailable>
- Status: <Healthy/Warning/Critical>
## Health Status
<Healthy/Warning/Critical>