diff --git a/.agents/evolve/preferences.yaml.template b/.agents/evolve/preferences.yaml.template deleted file mode 100644 index 756f7a1a4..000000000 --- a/.agents/evolve/preferences.yaml.template +++ /dev/null @@ -1,43 +0,0 @@ -# /evolve per-repo preferences (template) -# -# Copy this file to `.agents/evolve/preferences.yaml` and edit. The actual -# preferences file is gitignored — each repo and operator picks their own -# physics for the autonomous /evolve loop. -# -# Resolution order: -# 1. defaults (built-in Go constants) -# 2. this file overrides defaults -# 3. CLI flag overrides this file (caller-applied) -# -# Invalid keys, types, or out-of-range values produce a startup error with -# file:line:column context. No silent fallback. -# -# Inspect the resolved state with: `ao evolve config --show` -# Or as JSON: `ao evolve config --show --json` - -# Schema version. Must equal 1 for this template. -schema_version: 1 - -# Default mode when /evolve is invoked without a mode flag. -# burst — single supervised cycle, then exit (default). -# loop — keep cycling until the queue stabilizes or a halt signal fires. -mode_default: burst - -# When does /evolve narrow from explore (scout) to exploit (productive)? -scope_filter: - # Number of productive cycles required before scope narrows. Range [1..100]. - productive_threshold: 5 - # Halt the loop on a streak of pure-scout (no productive change) cycles. - scout_streak_halt: true - -# If true, treat a missing or stale `recommended` pointer as an error rather -# than a soft warning. -recommended_pointer_strict: true - -# Filesystem flags that halt the loop. Relative paths from the repo root. -halt_signals: - - .agents/evolve/STOP - - .agents/evolve/KILL - -# When true, the generator skill applies layered templates during cycles. -generator_layers_enabled: true diff --git a/.agents/evolve/session-state.json b/.agents/evolve/session-state.json deleted file mode 100644 index 1b98f50ce..000000000 --- a/.agents/evolve/session-state.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "mode": "beads-only", - "mission": "Churn all 47 '3.0-alignment' beads into main via grouped PR chunks; steered in-session loop; agile pivots allowed; not waterfall.", - "cycle": 2, - "push_policy": "worktree -> PR -> merge to main when CI + claude-code-review green", - "grouping": "beads grouped into PR chunks (see pr-plan.md)", - "cap": "until all beads done", - "base_branch": "main", - "merged": [{"pr": 401, "beads": ["agentops-yxid", "agentops-obef"]}], - "in_flight_pr": {"num": 403, "branch": "feat/agentops-fr9-golangci-standard", "worktree": "/home/bo/dev/agentops-wt-pr4", "beads": ["agentops-fr9 (partial)", "agentops-tqc.3 (closed)"], "state": "ci-running"}, - "next_cycle": "merge #403 when green; close agentops-fr9 (config infra delivered; errorlint/unconvert/unused folded into bs8/0l9) + confirm tqc.3 closed; prune wt-pr4; then PR-2 ci-timeouts (agentops-n52t)", - "lint_env": "golangci-lint v2.11.4 at /home/bo/go/bin; PATH needs /home/bo/go/bin + /home/linuxbrew/.linuxbrew/bin; uncap with --max-issues-per-linter=0 --max-same-issues=0; CI severity-tolerates errcheck/staticcheck", - "last_selected_source": "beads", - "generator_empty_streak": 0, - "kill_switch": ".agents/evolve/STOP" -} diff --git a/.agents/findings/registry.jsonl b/.agents/findings/registry.jsonl deleted file mode 100644 index 73c13005b..000000000 --- a/.agents/findings/registry.jsonl +++ /dev/null @@ -1,19 +0,0 @@ -{"dedup_key":"doc-first-migration-must-materialize-data-lane","pattern":"A tooling migration landed as doc edits (CLAUDE.md says br is the tracker) while the data lane was never materialized (no issues.jsonl, cited migration record absent, br errors) \u2014 leaving the repo with NO working tracker under a no-bead-no-PR doctrine","detection_question":"Does every doc claim of a completed migration have a verifiable on-disk artifact (the new tool actually runs, the cited record exists)?","checklist_item":"Before committing migration docs, run the new tool end-to-end and verify every file path the doc cites exists","applicable_when":["docs-drift","validation-gap"],"confidence":"high","status":"active","superseded_by":null,"ttl_days":180,"hit_count":1,"last_cited":"2026-06-11T00:00:00Z","source":{"repo":"agentops","session":"research-next-work-2026-06-11","file":".agents/research/2026-06-11-next-work.md","skill":"research"},"id":"f-2026-06-11-doc-first-migration-data-lane","version":1,"tier":"local","date":"2026-06-11","severity":"high","category":"validation-gap","applicable_languages":["markdown","jsonl","shell"]} -{"dedup_key":"public-repo-visibility-before-data-commit","pattern":"A data migration/import was one git add away from publishing private cross-context data (full bead ledger: client, SOC, fleet topology) to a PUBLIC repo \u2014 every plan artifact inherited the 'commit the JSONL' assumption without ever checking remote visibility","detection_question":"Before any arc that commits a data store: is the remote public, and does the data carry content from more-private contexts (imports inherit the privacy ceiling of their most sensitive source)?","checklist_item":"Run gh repo view --json visibility,isPrivate and classify the data BEFORE staging any migrated/imported store; default ledgers to a private nested repo or gitignore","applicable_when":["validation-gap","plan-shape"],"confidence":"high","status":"active","superseded_by":null,"ttl_days":365,"hit_count":1,"last_cited":"2026-06-11T00:00:00Z","source":{"repo":"agentops","session":"br-migration-wave0-2026-06-11","file":".agents/learnings/2026-06-11-public-repo-data-classification-before-migration-commit.md","skill":"post-mortem"},"id":"f-2026-06-11-public-repo-visibility-data-commit","version":1,"tier":"local","date":"2026-06-11","severity":"critical","category":"validation-gap","applicable_languages":["jsonl","markdown","git"]} -{"dedup_key":"contract-gate-couples-to-asserted-doc-surface","pattern":"A gate that greps literal wording in a doc (19b closeout contract) forms a coupled chain with that doc: editing the doc without flipping the gate in the SAME commit red-mains every push \u2014 hit twice in one day (bd->br flip, private-ledger amendment)","detection_question":"Does any gate script literally assert strings in the doc I am editing (grep gate scripts for the doc name + grep -Fq patterns)?","checklist_item":"Before editing doctrine docs, find gates asserting them; ship doc+gate atomically and prove PASS-on-new/FAIL-on-old via fixture before pushing","applicable_when":["docs-drift","validation-gap"],"confidence":"high","status":"active","superseded_by":null,"ttl_days":365,"hit_count":2,"last_cited":"2026-06-11T00:00:00Z","source":{"repo":"agentops","session":"br-migration-wave0-2026-06-11","file":".agents/learnings/2026-06-11-contract-gates-couple-to-asserted-surfaces.md","skill":"post-mortem"},"id":"f-2026-06-11-contract-gate-doc-coupling","version":1,"tier":"local","date":"2026-06-11","severity":"high","category":"docs-drift","applicable_languages":["markdown","shell"]} -{"dedup_key":"epic-close-needs-target-disposition","pattern":"An epic body enumerating N targets but decomposed into false pass or false alarm (ag-8jf97: BSD sed silently no-op tamper -> verify falsely flagged PROBLEM on an untouched ledger; pipe-masked exit codes)","detection_question":"Did I confirm the failure was actually induced (grep the injected marker) and capture the real exit code (not through a pipe) before believing the detector catches it?","checklist_item":"To verify a lie-detector: induce the failure, grep-confirm it landed, capture exit via cmd;rc=$? (never |head;$?), believe only after watching it fail on cue","applicable_when":["validation-gap"],"confidence":"high","status":"active","superseded_by":null,"ttl_days":365,"hit_count":1,"last_cited":"2026-06-14T00:00:00Z","source":{"repo":"agentops","session":"ag-8jf97-2026-06-14","file":".agents/learnings/2026-06-14-verify-a-failure-detector-by-inducing-the-failure.md","skill":"post-mortem"},"id":"f-2026-06-14-verify-failure-detector","version":1,"tier":"local","date":"2026-06-14","severity":"medium","category":"validation-gap","applicable_languages":["any","shell"]} -{"dedup_key":"validation-gap|codex-exec-positional-prompt-reads-stdin-and-hangs-in-non-tty|plan-shape","pattern":"codex exec \"\" in any non-TTY context (background, ATM/NTM pane, cron, nohup) reads from stdin and hangs forever on 'Reading additional input from stdin...'; the pane looks alive but never starts the task (orchestration-spike 2026-06-15)","detection_question":"Is any codex exec being launched in a non-interactive context (background/&, tmux/ATM pane, cron, nohup) without closing or feeding stdin?","checklist_item":"In non-TTY contexts, never pass the prompt positionally bare: pipe it (printf '%s' \"$P\" | codex exec) or append =2 independent working-tree units of work? If one-shot or single-unit, why is a swarm involved?","checklist_item":"Gate every swarm on: (1) reusable automation, not one-shot, AND (2) >=2 independent working-tree units. One-shot or single-unit -> do it inline (in-session Agent fan-out or just do it), do NOT orchestrate","applicable_when":["plan-shape"],"confidence":"high","status":"active","superseded_by":null,"ttl_days":365,"hit_count":1,"last_cited":"2026-06-15T00:00:00Z","source":{"repo":"agentops","session":"orchestration-spike-2026-06-15","file":".agents/learnings/2026-06-15-orchestration-failure-modes.md","skill":"forge"},"id":"f-2026-06-15-over-orchestration","version":1,"tier":"local","date":"2026-06-15","severity":"high","category":"plan-shape","applicable_languages":["any"]} -{"dedup_key":"archive-sibling-repo-reconcile-downstream-consumers","pattern":"When a sibling repo or external subsystem is archived, consumer repos often keep live-looking docs, skills, scripts, executable flags, and tracker items that still route work through the retired surface.","detection_question":"After archiving or retiring a repo/substrate, did we grep the consuming repo for the retired names across first-read docs, skills, scripts, generated help, code validation, and tracker queues?","checklist_item":"Run a downstream reconciliation sweep for archived repo names and route aliases; retarget, banner as historical, or close/supersede each live-looking consumer before calling the archive definitive.","applicable_when":["archive","deprecation","cross-repo-consumer","docs-drift","tracker-hygiene"],"confidence":"high","status":"active","superseded_by":null,"ttl_days":365,"hit_count":1,"last_cited":"2026-06-16T00:00:00Z","source":{"repo":"agentops","session":"ag-jx1ul-2026-06-16","file":".agents/research/2026-06-16-agentops-post-mount-olympus-archive-updates.md","skill":"research"},"id":"f-2026-06-16-archive-consumer-reconciliation","version":1,"tier":"local","date":"2026-06-16","severity":"medium","category":"docs-drift","applicable_languages":["any"]} -{"id":"f-2026-07-01-verify-landed-before-close","category":"validation-gap","tier":"local","severity":"medium","source":{"repo":"agentops","session":"wedge-all-in-dyr0-crank-2026-07-01","file":".agents/learnings/2026-07-01-verify-landed-before-close-direct-push.md","skill":"crank"},"version":1,"ttl_days":365,"status":"active","applicable_languages":["any"],"dedup_key":"process|verify-landed-before-close|validation-gap","pattern":"On a direct-push-to-main lane under concurrent lanes, a bead was closed citing a sha whose push had actually been rejected by a race; push exit tail was ambiguous.","detection_question":"Does every close that cites a landed sha run `git merge-base --is-ancestor origin/main` after a fresh fetch, before the tracker write?","checklist_item":"After push: fetch, `merge-base --is-ancestor` the landed sha against origin/main, THEN br close — never close on push output alone.","applicable_when":["validation-gap"],"confidence":0.95,"hit_count":1,"last_cited":"2026-07-01","superseded_by":null,"date":"2026-07-01"} -{"id":"f-2026-07-01-refute-repro-run-first","category":"validation-gap","tier":"local","severity":"medium","source":{"repo":"agentops","session":"wedge-all-in-dyr0-crank-2026-07-01","file":".agents/learnings/2026-07-01-pawl-refute-claims-run-the-repro.md","skill":"crank"},"version":1,"ttl_days":365,"status":"active","applicable_languages":["any"],"dedup_key":"review|refuted-verdict-repro-run-before-patch|validation-gap","pattern":"A cross-family refuter REFUTED with a named repro command that, when executed, passed — a hallucinated refute; patching against it would have added untested complexity.","detection_question":"When a REFUTED verdict names a concrete repro (command/fixture/input), was that exact repro executed before any fix was written?","checklist_item":"REFUTED with a named repro: run the exact repro first — fails => fix with it as the regression test; passes => re-run the review unchanged, do not patch.","applicable_when":["validation-gap"],"confidence":0.9,"hit_count":1,"last_cited":"2026-07-01","superseded_by":null,"date":"2026-07-01"} -{"id":"f-2026-07-01-ideation-beads-rebaseline","category":"plan-shape","tier":"local","severity":"high","source":{"repo":"agentops","session":"wedge-all-in-dyr0-discovery-2026-07-01","file":".agents/learnings/2026-07-01-ideation-beads-need-rebaseline.md","skill":"discovery"},"version":1,"ttl_days":365,"status":"active","applicable_languages":["any"],"dedup_key":"planning|ideation-claims-need-rebaseline|plan-shape","pattern":"Ideation-funnel beads (idea-wizard) carried 3/12 false 'X is missing' premises; a fresh-context re-baseline (EXISTS/PARTIAL/ABSENT per claim, file:line evidence) rewrote a quarter of the epic and voided one whole build.","detection_question":"Did every 'X is missing / build new Y' bead premise get a fresh-context existence check with file:line evidence before crank?","checklist_item":"Before /crank on ideation-created beads: run the discovery re-baseline (one Explore agent, EXISTS/PARTIAL/ABSENT per missing-claim).","applicable_when":["plan-shape"],"confidence":0.95,"hit_count":1,"last_cited":"2026-07-01","superseded_by":null,"date":"2026-07-01"} -{"id":"f-2026-07-09-goal-design-cross-file-invariants","version":1,"tier":"local","source":{"repo":"agentops","session":"goal-design-b1-postmortem-2026-07-09","file":".agents/council/2026-07-09-post-mortem-goal-design-b1.md","skill":"post-mortem"},"date":"2026-07-09","severity":"medium","category":"validation-gap","pattern":"Schema-backed packet checkers can validate per-file shape and digest while missing cross-file identity invariants such as intent_ref.path, slug consistency, and scenario references.","detection_question":"For every multi-file packet, did the checker validate cross-file identity, path, slug, and reference invariants beyond per-file schema shape?","checklist_item":"Add negative fixtures for misleading path, slug mismatch, and stale cross-file references before a skill or CLI consumes the packet.","applicable_languages":["markdown","shell","json"],"applicable_when":["validation-gap","pattern-matcher"],"status":"active","superseded_by":null,"dedup_key":"validation-gap|schema-backed-packet-cross-file-invariants|validation-gap","hit_count":1,"last_cited":"2026-07-09","ttl_days":365,"confidence":"high"} -{"id":"f-2026-07-09-proof-critical-tests-fail-closed","version":1,"tier":"local","source":{"repo":"agentops","session":"goal-design-b1-postmortem-2026-07-09","file":".agents/council/2026-07-09-post-mortem-goal-design-b1.md","skill":"post-mortem"},"date":"2026-07-09","severity":"medium","category":"test-gap","pattern":"Proof-critical tests can silently skip behavior assertions when required runtime dependencies are missing, producing a false sense of validation.","detection_question":"If a test is the acceptance proof for a packet, gate, or artifact contract, does missing dependency setup fail closed in CI/gate context instead of skipping the behavior?","checklist_item":"For proof-critical Bats or script tests, fail on missing required deps in gate context or run an equivalent direct command fallback that proves the behavior.","applicable_languages":["shell","python"],"applicable_when":["test-gap","validation-gap"],"status":"active","superseded_by":null,"dedup_key":"test-gap|proof-critical-tests-fail-closed|test-gap","hit_count":1,"last_cited":"2026-07-09","ttl_days":365,"confidence":"high"} -{"id":"f-2026-07-12-post-mortem-preflight-path","version":1,"tier":"local","source":{"repo":"agentops","session":"skill-standards-landing-postmortem-2026-07-12","file":".agents/council/2026-07-12-post-mortem-skill-standards-landing.md","skill":"post-mortem"},"date":"2026-07-12","severity":"low","category":"docs-drift","pattern":"A skill can validate a self-relative script while teaching operators a different repo-root command that does not exist.","detection_question":"Does every command documented by a skill execute literally from the cwd the skill claims, rather than only through a validator-computed skill directory?","checklist_item":"Add a repo-root command-exec fixture for documented skill commands whose paths are presented as repository-relative.","applicable_languages":["markdown","shell"],"applicable_when":["docs-drift","test-gap"],"status":"active","superseded_by":null,"dedup_key":"docs-drift|documented-skill-command-path-must-run-from-claimed-cwd|docs-drift","hit_count":0,"last_cited":null,"ttl_days":365,"confidence":"high"} -{"id":"f-2026-07-12-next-work-materialize-schema-drift","version":1,"tier":"local","source":{"repo":"agentops","session":"skill-standards-landing-postmortem-2026-07-12","file":".agents/council/2026-07-12-post-mortem-skill-standards-landing.md","skill":"post-mortem"},"date":"2026-07-12","severity":"high","category":"validation-gap","pattern":"A shared JSONL rewriter can accept a schema-valid queue, drop required lifecycle fields while materializing one item, and atomically replace the whole queue without validating its output.","detection_question":"Does every next-work rewrite preserve required and unknown fields for all rows and run strict schema validation before atomic replacement?","checklist_item":"Round-trip a mixed historical v1.4 queue through the real materializer and refuse replacement unless strict whole-file validation passes.","applicable_languages":["go","json"],"applicable_when":["validation-gap","struct-change","test-gap"],"status":"active","superseded_by":null,"dedup_key":"validation-gap|next-work-rewriter-must-preserve-and-self-validate|validation-gap","hit_count":0,"last_cited":null,"ttl_days":365,"confidence":"high"} diff --git a/.agents/nightly/2026-05-07/baseline-goals.json b/.agents/nightly/2026-05-07/baseline-goals.json deleted file mode 100644 index d6c2e06c4..000000000 --- a/.agents/nightly/2026-05-07/baseline-goals.json +++ /dev/null @@ -1,270 +0,0 @@ -{ - "timestamp": "2026-05-07T06:14:23Z", - "git_sha": "19f520f", - "goals": [ - { - "goal_id": "flywheel-compounding", - "result": "skip", - "duration_s": 0.044708561, - "output": "SKIP: σ=0 ρ=0 — corpus dormant; flywheel-compounding has no signal to evaluate.\n citations_this_period=0 total_artifacts=0 learnings_created=0\n period=[2026-04-30T06:14:12.774557805Z .. 2026-05-07T06:14:12.774557805Z]\n Dormant precondition (f-2026-04-30-002): exit 77 → goals runner records SKIP.\n To wake the gate: run any session that issues `ao lookup --cite ...` against the corpus.", - "weight": 3, - "tags": [ - "long-cycle", - "corpus-state" - ], - "affects_files": [ - "scripts/check-flywheel-compounding.sh", - ".agents/findings/f-2026-04-30-002.md" - ] - }, - { - "goal_id": "dream-end-user-coverage", - "result": "pass", - "duration_s": 0.041586083, - "output": "OK: /home/user/agentops/docs/templates/schedule.yaml.example exists, parses, and uses real-bodied job_types", - "weight": 3, - "affects_files": [ - "scripts/check-schedule-example.sh", - "docs/templates/schedule.yaml.example", - "cli/internal/schedule/testdata/example-validation.yaml" - ] - }, - { - "goal_id": "flywheel-proof", - "result": "pass", - "duration_s": 0.533311931, - "output": "[proof-run] Using pre-built ao binary: /home/user/agentops/cli/bin/ao\n[proof-run] PASS: reused pre-built ao binary\n[proof-run] Creating isolated proof repo\n[proof-run] PASS: initialized isolated repo\n\n…[truncated]…\n\n[proof-run] PASS: nightly summary exists\n[proof-run] PASS: nightly summary exposes retrieval_live\n[proof-run] PASS: nightly retrieval report has coverage\n[proof-run] FLYWHEEL PROOF: PASS (20 checks)", - "weight": 7, - "affects_files": [ - "scripts/proof-run.sh" - ] - }, - { - "goal_id": "skill-frontmatter", - "result": "pass", - "duration_s": 0.401828352, - "weight": 6, - "affects_files": [ - "skills/*/SKILL.md" - ] - }, - { - "goal_id": "hook-preflight", - "result": "pass", - "duration_s": 0.205985725, - "output": "\u001b[0;34mHook preflight checks\u001b[0m\n\u001b[0;32m✓\u001b[0m hooks/session-start.sh exists\n\u001b[0;32m✓\u001b[0m hooks/session-end-maintenance.sh exists\n\u001b[0;32m✓\u001b[0m hooks/precompact-snapshot.sh exists\n\u001b[0;32m✓\u001b[0m hooks/pen\n…[truncated]…\nxists: hooks/worktree-setup.sh\n\u001b[0;32m✓\u001b[0m hooks.json script exists: hooks/worktree-cleanup.sh\n\u001b[0;32m✓\u001b[0m hooks.json script exists: hooks/config-change-monitor.sh\n\n\u001b[0;32mHook preflight PASSED\u001b[0m", - "weight": 6, - "affects_files": [ - "scripts/validate-hook-preflight.sh", - "hooks/**" - ] - }, - { - "goal_id": "go-cli-builds", - "result": "pass", - "duration_s": 1.137168335, - "weight": 8, - "affects_files": [ - "cli/**" - ] - }, - { - "goal_id": "go-cli-tests", - "result": "pass", - "duration_s": 1.459555708, - "output": "ok \tgithub.com/boshu2/agentops/cli/cmd/ao\t(cached)\n? \tgithub.com/boshu2/agentops/cli/cmd/skill-frontmatter-json\t[no test files]\n? \tgithub.com/boshu2/agentops/cli/embedded\t[no test files]\nok \tgit\n…[truncated]…\nvibecheck\t(cached)\nok \tgithub.com/boshu2/agentops/cli/internal/wikiworker\t(cached)\nok \tgithub.com/boshu2/agentops/cli/internal/worker\t(cached)\nok \tgithub.com/boshu2/agentops/cli/pkg/vault\t(cached)", - "weight": 8, - "affects_files": [ - "cli/**" - ] - }, - { - "goal_id": "go-vet-clean", - "result": "pass", - "duration_s": 0.680930723, - "weight": 5, - "affects_files": [ - "cli/**" - ] - }, - { - "goal_id": "go-complexity-ceiling", - "result": "pass", - "duration_s": 0.724245052, - "output": "gocyclo not found; auto-installing into /root/go/bin\nAll functions in cli/ are below complexity 20.\ngocyclo not found; auto-installing into /root/go/bin\nAll functions in cli/internal/ are below complexity 18.", - "weight": 6, - "affects_files": [ - "cli/**", - "scripts/check-go-absolute-complexity.sh" - ] - }, - { - "goal_id": "security-gate", - "result": "pass", - "duration_s": 0.122418491, - "output": "================================\nTesting security-gate.sh\n================================\n\nPASS: security-gate.sh is executable\nPASS: --help works\nPASS: invalid mode fails\nPASS: JSON output is valid\nPASS: JSON output has required fields\nPASS: security-gate summary artifact created\n\n================================\nResults: 6 PASS, 0 FAIL\n================================", - "weight": 6, - "affects_files": [ - "scripts/security-gate.sh", - "tests/scripts/test-security-gate.sh" - ] - }, - { - "goal_id": "manifest-versions-match", - "result": "pass", - "duration_s": 0.00818205, - "weight": 5, - "affects_files": [ - ".claude-plugin/marketplace.json", - ".claude-plugin/plugin.json" - ] - }, - { - "goal_id": "wiring-closure", - "result": "pass", - "duration_s": 0.461871923, - "output": "All wiring checks passed", - "weight": 7, - "affects_files": [ - "scripts/check-wiring-closure.sh" - ] - }, - { - "goal_id": "contract-compatibility", - "result": "pass", - "duration_s": 0.550507945, - "output": "=== Contract compatibility gate ===\n\n--- Orphan allowlist validation ---\n OK: No orphan allowlist entries configured\n\n--- documentation-index.md link resolution ---\n OK: contracts/agent-worker.md\n \n…[truncated]…\ngued in documentation-index.md\n OK: docs/contracts/swarm-worker-result.schema.json catalogued in documentation-index.md\n\n=== Summary ===\nFailures: 0\nWarnings: 1\n\nContract compatibility check passed.", - "weight": 5, - "affects_files": [ - "scripts/check-contract-compatibility.sh", - "schemas/**", - "docs/contracts/**" - ] - }, - { - "goal_id": "goals-validate", - "result": "pass", - "duration_s": 0.210183063, - "output": "true", - "weight": 5, - "affects_files": [ - "GOALS.md", - "goals-affects-files.yaml", - "cli/internal/goals/**" - ] - }, - { - "goal_id": "compile-freshness", - "result": "fail", - "duration_s": 0.003413767, - "output": "FAIL: .agents/defrag/latest.json not found — run 'ao defrag' first", - "weight": 4, - "tags": [ - "runtime-artifact" - ], - "affects_files": [ - "scripts/check-compile-health.sh" - ] - }, - { - "goal_id": "compile-no-oscillation", - "result": "fail", - "duration_s": 0.003523236, - "output": "FAIL: .agents/defrag/latest.json not found — run 'ao defrag --oscillation-sweep' first", - "weight": 4, - "tags": [ - "runtime-artifact" - ], - "affects_files": [ - "scripts/check-compile-oscillation.sh" - ] - }, - { - "goal_id": "competitive-freshness", - "result": "pass", - "duration_s": 0.033786809, - "output": "PASS: All 6 competitive analyses are fresh (\u003c=45 days)", - "weight": 3, - "affects_files": [ - "docs/comparisons/**", - "scripts/check-competitive-freshness.sh" - ] - }, - { - "goal_id": "codex-parity-drift", - "result": "pass", - "duration_s": 6.185993243, - "output": "PASS: No codex parity drift detected", - "weight": 5, - "affects_files": [ - "skills-codex/**", - "skills-codex-overrides/**", - "scripts/check-codex-parity-drift.sh" - ] - }, - { - "goal_id": "install-smoke", - "result": "pass", - "duration_s": 0.114904496, - "output": "=== Install Script Smoke Tests ===\n\nPASS: scripts/install.sh syntax valid\nPASS: scripts/install-codex.sh syntax valid\nPASS: scripts/install-opencode.sh syntax valid\n\nPASS: scripts/install.sh has valid\n…[truncated]…\nheel subcommand registered\nPASS: ao goals subcommand registered\nPASS: ao inject subcommand registered\n\n=================================\nResults: 29 passed, 0 failed\n=================================", - "weight": 5, - "affects_files": [ - "scripts/install*.sh", - "tests/install/test-install-smoke.sh" - ] - }, - { - "goal_id": "flywheel-lifecycle", - "result": "pass", - "duration_s": 0.027974183, - "output": "=== Flywheel Lifecycle Gate ===\n\nStage 1: Capture\n PASS: learning file created\n PASS: learning has frontmatter\n PASS: learning has marker\n\nStage 2: Retrieval\n PASS: grep finds learning by marker\n \n…[truncated]…\ntoo sparse for citation checks (0 real learnings outside test sentinel)\n\nResults: 12 passed, 0 failed\nPASS: Flywheel lifecycle gate OK (5 stages: capture → retrieval → inject → round-trip → citation)", - "weight": 6, - "affects_files": [ - "scripts/check-flywheel-lifecycle.sh", - "tests/scripts/check-flywheel-lifecycle.bats", - ".agents/learnings/**" - ] - }, - { - "goal_id": "eval-workbench-verify", - "result": "pass", - "duration_s": 1.00472188, - "output": "eval-workbench-verify: PASS (12 tasks, suite has 12 cases)", - "weight": 6, - "affects_files": [ - "evals/**", - "scripts/check-eval-workbench.sh" - ] - }, - { - "goal_id": "state-path-resolver-coverage", - "result": "pass", - "duration_s": 0.034462961, - "output": "state-path-resolver-coverage total=151 by-surface: cli/cmd/ao=49 cli/internal=58 hooks=10 lib=1 scripts=33", - "weight": 3, - "tags": [ - "warn-only" - ], - "affects_files": [ - "cli/internal/paths/**", - "lib/ao-paths.sh", - "scripts/check-paths-resolver-coverage.sh" - ] - } - ], - "summary": { - "total": 22, - "passing": 19, - "failing": 2, - "skipped": 1, - "score": 92.92035398230088, - "code_driven_total": 20, - "code_driven_passing": 19, - "code_driven_failing": 0, - "code_driven_skipped": 1, - "code_driven_score": 100, - "runtime_artifact_total": 2, - "runtime_artifact_passing": 0, - "runtime_artifact_failing": 2 - } -} diff --git a/.agents/nightly/2026-05-07/blocker-matrix.json b/.agents/nightly/2026-05-07/blocker-matrix.json deleted file mode 100644 index 3437f4f31..000000000 --- a/.agents/nightly/2026-05-07/blocker-matrix.json +++ /dev/null @@ -1 +0,0 @@ -{"open_prs": [], "blocked_goals": []} diff --git a/.agents/nightly/2026-05-07/final-goals.json b/.agents/nightly/2026-05-07/final-goals.json deleted file mode 100644 index 3ad8f8cd5..000000000 --- a/.agents/nightly/2026-05-07/final-goals.json +++ /dev/null @@ -1,270 +0,0 @@ -{ - "timestamp": "2026-05-07T06:49:11Z", - "git_sha": "1eb0c5a", - "goals": [ - { - "goal_id": "flywheel-compounding", - "result": "skip", - "duration_s": 0.038672973, - "output": "SKIP: σ=0 ρ=0 — corpus dormant; flywheel-compounding has no signal to evaluate.\n citations_this_period=0 total_artifacts=0 learnings_created=7\n period=[2026-04-30T06:49:04.806143614Z .. 2026-05-07T06:49:04.806143614Z]\n Dormant precondition (f-2026-04-30-002): exit 77 → goals runner records SKIP.\n To wake the gate: run any session that issues `ao lookup --cite ...` against the corpus.", - "weight": 3, - "tags": [ - "long-cycle", - "corpus-state" - ], - "affects_files": [ - "scripts/check-flywheel-compounding.sh", - ".agents/findings/f-2026-04-30-002.md" - ] - }, - { - "goal_id": "dream-end-user-coverage", - "result": "pass", - "duration_s": 0.050791, - "output": "OK: /home/user/agentops/docs/templates/schedule.yaml.example exists, parses, and uses real-bodied job_types", - "weight": 3, - "affects_files": [ - "scripts/check-schedule-example.sh", - "docs/templates/schedule.yaml.example", - "cli/internal/schedule/testdata/example-validation.yaml" - ] - }, - { - "goal_id": "flywheel-proof", - "result": "pass", - "duration_s": 0.574027694, - "output": "[proof-run] Using pre-built ao binary: /home/user/agentops/cli/bin/ao\n[proof-run] PASS: reused pre-built ao binary\n[proof-run] Creating isolated proof repo\n[proof-run] PASS: initialized isolated repo\n\n…[truncated]…\n\n[proof-run] PASS: nightly summary exists\n[proof-run] PASS: nightly summary exposes retrieval_live\n[proof-run] PASS: nightly retrieval report has coverage\n[proof-run] FLYWHEEL PROOF: PASS (20 checks)", - "weight": 7, - "affects_files": [ - "scripts/proof-run.sh" - ] - }, - { - "goal_id": "skill-frontmatter", - "result": "pass", - "duration_s": 0.418346649, - "weight": 6, - "affects_files": [ - "skills/*/SKILL.md" - ] - }, - { - "goal_id": "hook-preflight", - "result": "pass", - "duration_s": 0.200567305, - "output": "\u001b[0;34mHook preflight checks\u001b[0m\n\u001b[0;32m✓\u001b[0m hooks/session-start.sh exists\n\u001b[0;32m✓\u001b[0m hooks/session-end-maintenance.sh exists\n\u001b[0;32m✓\u001b[0m hooks/precompact-snapshot.sh exists\n\u001b[0;32m✓\u001b[0m hooks/pen\n…[truncated]…\nxists: hooks/worktree-setup.sh\n\u001b[0;32m✓\u001b[0m hooks.json script exists: hooks/worktree-cleanup.sh\n\u001b[0;32m✓\u001b[0m hooks.json script exists: hooks/config-change-monitor.sh\n\n\u001b[0;32mHook preflight PASSED\u001b[0m", - "weight": 6, - "affects_files": [ - "scripts/validate-hook-preflight.sh", - "hooks/**" - ] - }, - { - "goal_id": "go-cli-builds", - "result": "pass", - "duration_s": 1.026706158, - "weight": 8, - "affects_files": [ - "cli/**" - ] - }, - { - "goal_id": "go-cli-tests", - "result": "pass", - "duration_s": 1.400764004, - "output": "ok \tgithub.com/boshu2/agentops/cli/cmd/ao\t(cached)\n? \tgithub.com/boshu2/agentops/cli/cmd/skill-frontmatter-json\t[no test files]\n? \tgithub.com/boshu2/agentops/cli/embedded\t[no test files]\nok \tgit\n…[truncated]…\nvibecheck\t(cached)\nok \tgithub.com/boshu2/agentops/cli/internal/wikiworker\t(cached)\nok \tgithub.com/boshu2/agentops/cli/internal/worker\t(cached)\nok \tgithub.com/boshu2/agentops/cli/pkg/vault\t(cached)", - "weight": 8, - "affects_files": [ - "cli/**" - ] - }, - { - "goal_id": "go-vet-clean", - "result": "pass", - "duration_s": 0.54751166, - "weight": 5, - "affects_files": [ - "cli/**" - ] - }, - { - "goal_id": "go-complexity-ceiling", - "result": "pass", - "duration_s": 0.669323962, - "output": "gocyclo not found; auto-installing into /root/go/bin\nAll functions in cli/ are below complexity 20.\ngocyclo not found; auto-installing into /root/go/bin\nAll functions in cli/internal/ are below complexity 18.", - "weight": 6, - "affects_files": [ - "cli/**", - "scripts/check-go-absolute-complexity.sh" - ] - }, - { - "goal_id": "security-gate", - "result": "pass", - "duration_s": 0.104785822, - "output": "================================\nTesting security-gate.sh\n================================\n\nPASS: security-gate.sh is executable\nPASS: --help works\nPASS: invalid mode fails\nPASS: JSON output is valid\nPASS: JSON output has required fields\nPASS: security-gate summary artifact created\n\n================================\nResults: 6 PASS, 0 FAIL\n================================", - "weight": 6, - "affects_files": [ - "scripts/security-gate.sh", - "tests/scripts/test-security-gate.sh" - ] - }, - { - "goal_id": "manifest-versions-match", - "result": "pass", - "duration_s": 0.007491288, - "weight": 5, - "affects_files": [ - ".claude-plugin/marketplace.json", - ".claude-plugin/plugin.json" - ] - }, - { - "goal_id": "wiring-closure", - "result": "pass", - "duration_s": 0.467143761, - "output": "All wiring checks passed", - "weight": 7, - "affects_files": [ - "scripts/check-wiring-closure.sh" - ] - }, - { - "goal_id": "contract-compatibility", - "result": "pass", - "duration_s": 0.589219439, - "output": "=== Contract compatibility gate ===\n\n--- Orphan allowlist validation ---\n OK: No orphan allowlist entries configured\n\n--- documentation-index.md link resolution ---\n OK: contracts/agent-worker.md\n \n…[truncated]…\ngued in documentation-index.md\n OK: docs/contracts/swarm-worker-result.schema.json catalogued in documentation-index.md\n\n=== Summary ===\nFailures: 0\nWarnings: 1\n\nContract compatibility check passed.", - "weight": 5, - "affects_files": [ - "scripts/check-contract-compatibility.sh", - "schemas/**", - "docs/contracts/**" - ] - }, - { - "goal_id": "goals-validate", - "result": "pass", - "duration_s": 0.209114707, - "output": "true", - "weight": 5, - "affects_files": [ - "GOALS.md", - "goals-affects-files.yaml", - "cli/internal/goals/**" - ] - }, - { - "goal_id": "compile-freshness", - "result": "pass", - "duration_s": 0.021916337, - "output": "INFO: .agents/defrag/latest.json not found; falling back to overnight preview .agents/overnight/latest/defrag/latest.json\nPASS: Compile health OK (defrag 0h ago, stale=0/5)", - "weight": 4, - "tags": [ - "runtime-artifact" - ], - "affects_files": [ - "scripts/check-compile-health.sh" - ] - }, - { - "goal_id": "compile-no-oscillation", - "result": "pass", - "duration_s": 0.012115489, - "output": "INFO: .agents/defrag/latest.json not found; falling back to overnight preview .agents/overnight/latest/defrag/latest.json\nPASS: no oscillating goals in .agents/overnight/latest/defrag/latest.json", - "weight": 4, - "tags": [ - "runtime-artifact" - ], - "affects_files": [ - "scripts/check-compile-oscillation.sh" - ] - }, - { - "goal_id": "competitive-freshness", - "result": "pass", - "duration_s": 0.036058987, - "output": "PASS: All 6 competitive analyses are fresh (\u003c=45 days)", - "weight": 3, - "affects_files": [ - "docs/comparisons/**", - "scripts/check-competitive-freshness.sh" - ] - }, - { - "goal_id": "codex-parity-drift", - "result": "pass", - "duration_s": 6.103679987, - "output": "PASS: No codex parity drift detected", - "weight": 5, - "affects_files": [ - "skills-codex/**", - "skills-codex-overrides/**", - "scripts/check-codex-parity-drift.sh" - ] - }, - { - "goal_id": "install-smoke", - "result": "pass", - "duration_s": 0.115970682, - "output": "=== Install Script Smoke Tests ===\n\nPASS: scripts/install.sh syntax valid\nPASS: scripts/install-codex.sh syntax valid\nPASS: scripts/install-opencode.sh syntax valid\n\nPASS: scripts/install.sh has valid\n…[truncated]…\nheel subcommand registered\nPASS: ao goals subcommand registered\nPASS: ao inject subcommand registered\n\n=================================\nResults: 29 passed, 0 failed\n=================================", - "weight": 5, - "affects_files": [ - "scripts/install*.sh", - "tests/install/test-install-smoke.sh" - ] - }, - { - "goal_id": "flywheel-lifecycle", - "result": "pass", - "duration_s": 0.028163938, - "output": "=== Flywheel Lifecycle Gate ===\n\nStage 1: Capture\n PASS: learning file created\n PASS: learning has frontmatter\n PASS: learning has marker\n\nStage 2: Retrieval\n PASS: grep finds learning by marker\n \n…[truncated]…\nns\n PASS: corpus has 6 learning(s) — citation is structurally possible\n\nResults: 12 passed, 0 failed\nPASS: Flywheel lifecycle gate OK (5 stages: capture → retrieval → inject → round-trip → citation)", - "weight": 6, - "affects_files": [ - "scripts/check-flywheel-lifecycle.sh", - "tests/scripts/check-flywheel-lifecycle.bats", - ".agents/learnings/**" - ] - }, - { - "goal_id": "eval-workbench-verify", - "result": "pass", - "duration_s": 0.935839908, - "output": "eval-workbench-verify: PASS (12 tasks, suite has 12 cases)", - "weight": 6, - "affects_files": [ - "evals/**", - "scripts/check-eval-workbench.sh" - ] - }, - { - "goal_id": "state-path-resolver-coverage", - "result": "pass", - "duration_s": 0.044718855, - "output": "state-path-resolver-coverage total=151 by-surface: cli/cmd/ao=49 cli/internal=58 hooks=10 lib=1 scripts=33", - "weight": 3, - "tags": [ - "warn-only" - ], - "affects_files": [ - "cli/internal/paths/**", - "lib/ao-paths.sh", - "scripts/check-paths-resolver-coverage.sh" - ] - } - ], - "summary": { - "total": 22, - "passing": 21, - "failing": 0, - "skipped": 1, - "score": 100, - "code_driven_total": 20, - "code_driven_passing": 19, - "code_driven_failing": 0, - "code_driven_skipped": 1, - "code_driven_score": 100, - "runtime_artifact_total": 2, - "runtime_artifact_passing": 2, - "runtime_artifact_failing": 0 - } -} diff --git a/.agents/nightly/2026-05-07/main-ci-baseline.json b/.agents/nightly/2026-05-07/main-ci-baseline.json deleted file mode 100644 index 6390a2412..000000000 --- a/.agents/nightly/2026-05-07/main-ci-baseline.json +++ /dev/null @@ -1 +0,0 @@ -{"note": "main-CI baseline check via MCP tools not available; will distinguish inherited vs new failures heuristically if any CI fires", "checked_at": "2026-05-07T06:08:44+00:00"} diff --git a/.agents/nightly/2026-05-07/previous-run-audit.md b/.agents/nightly/2026-05-07/previous-run-audit.md deleted file mode 100644 index 2894c1e59..000000000 --- a/.agents/nightly/2026-05-07/previous-run-audit.md +++ /dev/null @@ -1,25 +0,0 @@ -# Previous-run audit (anchor: origin/nightly/2026-05-05) - -Anchor SHA: 46ef488 (Nightly 2026-05-05 final commit) -Anchor baseline: code-driven score n/a (older JSON shape; overall 77.58%, 16/22 passing) -Current baseline: code-driven score 100.0% (19/19 passing, 1 skipped); runtime-artifact 0/2 (expected) - -## Anchor failures resolved -- flywheel-compounding w=3 → SKIP (corpus dormant; f-2026-04-30-002 SKIP precondition implemented) -- dream-end-user-coverage w=3 → PASS -- go-complexity-ceiling w=6 → PASS -- flywheel-lifecycle w=6 → PASS - -## Anchor failures persisting (expected, runtime-artifact) -- compile-freshness w=4 (tagged runtime-artifact; flips every run, excluded from headline) -- compile-no-oscillation w=4 (same) - -## Regressions (was passing, now failing) -None. - -## PRs merged since anchor -17 PR-tagged merges (skill-builder/auditor pair, deps bumps via Renovate, parity/spec docs for managed-agents launch, worktree fix, gitignore force-track regression in ce4c015 — fixed this run). - -## Notes -- Two-day gap because nightly/2026-05-06 ran on a date where neither baseline-goals.json nor final-goals.json propagated to main (gitignore regression from ce4c015 hid them). PR #235 body claims "94.29 → 100.00" but no anchor JSON is force-tracked in main from that run. -- This run's gitignore re-fix means tomorrow's nightly will have a real anchor. diff --git a/.agents/nightly/2026-05-07/start-utc.txt b/.agents/nightly/2026-05-07/start-utc.txt deleted file mode 100644 index c1aee68c9..000000000 --- a/.agents/nightly/2026-05-07/start-utc.txt +++ /dev/null @@ -1 +0,0 @@ -1778134100 diff --git a/.agents/reconcile/promotion-decision.md b/.agents/reconcile/promotion-decision.md deleted file mode 100644 index 94ee04b8c..000000000 --- a/.agents/reconcile/promotion-decision.md +++ /dev/null @@ -1,139 +0,0 @@ ---- -id: reconcile-2026-05-08-promotion-decision -type: promotion-decision -date: 2026-05-08 -epic: soc-xlw8 -related_epic: soc-e4ulx -related_bead: soc-f42z9 -status: deferred -decision: option-b-defer -sample_size: 5 -sample_threshold: 20 -distinct_prs: 3 -distinct_prs_threshold: 3 -fail_count: 0 -fail_count_threshold: 1 -re_evaluate_when: sample_size >= 20 AND distinct_prs >= 3 AND fail_count >= 1 ---- - -# Factory Claim Ledger — Promotion Decision - -> **Deferred 2026-05-08 (re-confirmed same day).** Sample size 5 < 20 threshold; -> distinct real PRs 3 = 3 threshold (just hit), but **0 fail observations** still -> makes the FP-rate vacuous. Re-run aggregator + this baseline section once -> advisory CI runs of `factory-claim-ledger-strict (advisory)` accumulate to -> ≥20 with at least one `verdict=fail` to give FP-rate a real denominator. -> Aggregator: `scripts/aggregate-observation-log.sh`. - -## How to compute the baseline - -Run the aggregator first to refresh the log: - -```bash -bash scripts/aggregate-observation-log.sh -``` - -Then compute false-positive metrics with `jq`: - -```bash -LOG=.agents/reconcile/observation-log.jsonl - -# Total observations -jq -s 'length' "$LOG" - -# Pass / fail breakdown -jq -s 'group_by(.verdict) | map({verdict: .[0].verdict, count: length})' "$LOG" - -# False-positive count: verdict=fail AND merged_anyway=true (a maintainer -# overrode the validator, evidence the failure was not blocking-worthy). -jq -s '[.[] | select(.verdict == "fail" and .merged_anyway == true)] | length' "$LOG" - -# False-positive rate (FP / total fails). 0 means every fail was a real fail. -jq -s ' - ([.[] | select(.verdict == "fail")] | length) as $fails - | ([.[] | select(.verdict == "fail" and .merged_anyway == true)] | length) as $fp - | if $fails == 0 then 0 else ($fp / $fails) end -' "$LOG" - -# Ledger-updated count: fail observations whose merge commit also touched -# docs/contracts/factory-claim-ledger.example.json (legitimate ledger fix -# after a real fail). -jq -s '[.[] | select(.verdict == "fail" and .ledger_updated == true)] | length' "$LOG" -``` - -## False-Positive Rate Baseline - -Computed 2026-05-08 from `.agents/reconcile/observation-log.jsonl` after -running `scripts/aggregate-observation-log.sh` (read 200 most recent -`validate.yml` runs; aggregated 5 unique observations). - -| Metric | Value | Source | -|--------|-------|--------| -| Total observations | **5** | `jq -s 'length' "$LOG"` | -| Pass count | 5 | group_by query | -| Fail count | 0 | group_by query | -| `merged_anyway=true` count | 0 | FP query | -| `ledger_updated=true` count | 0 (over `verdict=fail`) | ledger query | -| **False-positive rate** | **0** (vacuous — denominator is 0 fails) | FP / fail-count | -| Distinct PRs | 3 real (`#264`, `#265`, `#266`) + 2 push-to-main (`pr_number: null`) | `jq -s '[.[].pr_number] | unique \| length'` | - -Raw observations span 2026-05-08T02:49Z through 18:10Z — same calendar day -as the aggregator landing (commit `af681e9c`). PR threshold (≥3 distinct) -is now technically met, but sample size and the all-pass distribution -keep the decision unchanged. - -## Promotion Decision — **Option B (defer / extend sampling)** - -The skeleton's promotion thresholds are not met: - -- ✗ Sample size **5 < 20** -- ✓ Distinct real PRs **3 = 3** (just at threshold) -- ✓ FP rate 0 (but vacuous — no fail observations to compute against) -- ✗ Cannot demonstrate behavior under fail conditions (0 fails) - -Vacuous-FP-rate alone does not justify promotion: with zero fail samples -we have no empirical evidence about the validator's false-positive -behavior. A first real fail post-promotion could block an unrelated PR -with no demotion runway. Defer. - -**Re-evaluation trigger:** When all of the following hold, re-run the -aggregator and recompute this baseline: - -- [ ] Sample size ≥ 20 advisory runs -- [ ] ≥ 3 distinct PRs represented -- [ ] At least one `verdict=fail` observation (so FP rate has a real denominator) - -If the recomputed baseline meets Option A criteria (FP ≤ 5%, -`merged_anyway=true && ledger_updated=false` cases all explained), flip -`.github/workflows/validate.yml` per the action below. - -### Action when promotion is later approved - -Edit `.github/workflows/validate.yml` to flip -`factory-claim-ledger-strict (advisory)` to a required check -(remove `continue-on-error: true` from the validator step OR remove the -`(advisory)` suffix and add to required-checks). Update this file's -`status:` to `promoted` and append a new sign-off row. - -## Demotion Plan - -> Applies if promotion is later approved and subsequently reverted. - -- **Trigger:** FP rate climbs above 5% post-promotion, OR a `merged_anyway=true && ledger_updated=false` case appears that is not explained by an unrelated CI flake. -- **Revert:** restore `continue-on-error: true` and `(advisory)` suffix in `validate.yml`. -- **Root-cause fix:** investigate the failure mode in the offending observation(s) before re-attempting promotion; document in this file's `## Demotion Plan` row. - -## Sign-off - -| Role | Name | Date | Verdict | -|------|------|------|---------| -| Operator | Bo Fuller | 2026-05-08 | Option B (defer); sample 3 < 20, 2 PRs < 3, vacuous FP rate | -| Operator | Bo Fuller | 2026-05-08 (re-eval) | Option B (defer); sample 5 < 20, 3 PRs = 3 ✓, still 0 fails — FP rate vacuous | - ---- - -**Cross-refs:** -- Aggregator: `scripts/aggregate-observation-log.sh` -- CI source: `.github/workflows/validate.yml` (`factory-claim-ledger-strict (advisory)` job) -- Plan: `.agents/plans/2026-05-07-drain-open-next-work-items.md` §soc-ejq2 -- Wave 1E parent issue: soc-f42z9 diff --git a/.agents/reconcile/thesis-stability-decision.md b/.agents/reconcile/thesis-stability-decision.md deleted file mode 100644 index 39f7a21f5..000000000 --- a/.agents/reconcile/thesis-stability-decision.md +++ /dev/null @@ -1,86 +0,0 @@ ---- -id: thesis-stability-decision-template -type: decision-template -gate: soc-r3y8b -epic: soc-e4ulx -status: template ---- - -# Thesis-Stability Gate Decision - -> Template. Fill in once `bash scripts/check-thesis-stability.sh` returns -> exit 1 (drift detected) and the operator must consciously decide whether -> to proceed to Wave 2 of the Reconciliation Engine arc. - -## Gate run - -| Field | Value | -|-------|-------| -| Date | _YYYY-MM-DD_ | -| Operator | _name_ | -| Snapshot SHA | _from `.agents/reconcile/wave-0-thesis-snapshot.md` header_ | -| Current SHA | `git rev-parse HEAD` | -| Script verdict | _PASS / FAIL_ | -| Surfaces drifted | _README.md, PRODUCT.md, GOALS.md, none_ | - -## Drift summary - -If FAIL, paste the script's drift output (or a meaningful subset). Highlight -which sentences/claims changed and why. - -```diff - -``` - -## Operator decision - -Pick exactly one. Do not hedge. - -- [ ] **Accept drift.** The thesis HAS evolved since Wave 0. The new thesis - is what we'd ship today. Wave 2-4 acceptance criteria must be - re-validated against the new thesis below. -- [ ] **Re-brainstorm.** The drift indicates the plan is no longer aligned - with the thesis we want to enforce. Restart from `/brainstorm` - before any Wave 2-4 work. -- [ ] **Incidental edit.** The drift is mechanical (typo, link fix, format) - and not a thesis change. Regenerate the snapshot from a clean SHA; - document the regeneration command below. - -## Acceptance re-validation (if Accept drift) - -For each Wave 2-4 acceptance criterion, confirm it still holds against the -new thesis. If any criterion no longer holds, file a follow-up issue -adjusting the plan and link it here. - -| Wave | Criterion | Still holds? | Follow-up | -|------|-----------|--------------|-----------| -| 2 | | | | -| 3 | | | | -| 4 | | | | - -## Snapshot regeneration (if Incidental edit) - -```bash -# Capture the new snapshot from the current closure SHA -git rev-parse HEAD # → record this SHA in the snapshot header -# Re-run the awk extractor against the chosen SHA, paste into snapshot file -git show :README.md | awk 'NR==1, /^## / {if (!/^## /) print}' -# (repeat for PRODUCT.md, GOALS.md) -``` - -Document the rationale for regeneration here: - -``` - -``` - -## Sign-off - -| Role | Name | Date | -|------|------|------| -| Operator | | | -| Reviewer (optional) | | | - -> After sign-off, this file becomes the durable record of the gate decision. -> The Reconciliation Engine arc consults this when deciding whether to -> proceed past the Wave 1/2 boundary. diff --git a/.agents/reconcile/wave-0-thesis-snapshot.md b/.agents/reconcile/wave-0-thesis-snapshot.md deleted file mode 100644 index 8e0a68ea0..000000000 --- a/.agents/reconcile/wave-0-thesis-snapshot.md +++ /dev/null @@ -1,75 +0,0 @@ -` marker to this file unless the snapshot -is genuinely incomplete — the script exits 2 (precondition error) when it -finds that marker. Per pre-mortem L6. ---> - -# Wave 0 Thesis Snapshot - -Frozen at SHA `ab479e26` on 2026-05-07T20:31:47-04:00 (soc-9xn0 closure). - -## README.md hero - -``` -
- -# AgentOps - -[![GitHub stars](https://img.shields.io/github/stars/boshu2/agentops?style=social)](https://github.com/boshu2/agentops/stargazers) - -### A wiki for your agents. Built so you own the moat. - -`.agents/` is just a wiki — markdown files in your repo, version-controlled with your code, that agents read, traverse, and contribute to. The kind of wiki your team should already have. AgentOps automates the discipline of building one. - -*The only verifiable moat in this uncertain time is context. Models will get smarter, harnesses will commoditize, agents will get cheaper. Your accumulated context — the lessons learned about your individual problems, the patterns that worked, the decisions that survived review — is the one asset that compounds and doesn't get eaten by the next vendor release. That's what your company actually is.* - -*AgentOps is the shovel. Start digging.* - -> AgentOps is not a coding harness. The labs are building those, and they will keep getting better. AgentOps sits on top of whichever harness you already use — Claude Code, Codex, Cursor, OpenCode — and turns your business, codebase, and team practices into a context library those agents mix and match from. Mix and match Claude, Codex, or any model at every phase. Lives in `.agents/` in your repo. Runs on your hardware. Evolves with the models. - -*AgentOps was used to develop AgentOps. As of 2026-05-04, this repo's `.agents/` directory contained ~1,842 learnings, ~186 patterns, ~80 planning rules, and ~3,867 cited decisions captured by the system on itself across thousands of phase transitions. Re-run anytime: `bash scripts/corpus-stats.sh`. Independent 3-judge audit (2026-05-06) confirmed parity with Anthropic Managed Agents on rubric authoring, separate-context grading, and iterate-until-pass.* - -
- ---- - -``` - -## PRODUCT.md hero - -``` ---- -last_reviewed: 2026-05-07 ---- - -# PRODUCT.md - -``` - -## GOALS.md hero - -``` -# Goals - -A wiki for your agents — repo-native, version-controlled, mechanically maintained — that turns your context into the durable moat under any model or harness. - -``` diff --git a/.agents/rpi/next-work.jsonl b/.agents/rpi/next-work.jsonl deleted file mode 100644 index fd5906dae..000000000 --- a/.agents/rpi/next-work.jsonl +++ /dev/null @@ -1,34 +0,0 @@ -{"source_epic": "soc-9xn0", "timestamp": "2026-05-07T21:45:29-04:00", "items": [{"title": "Audit and re-attach AOP-CLAIM markers dropped from README in wave 1", "type": "tech-debt", "severity": "low", "source": "post-mortem-finding", "description": "The wave 1 README rewrite (commit 5b41f746 → 41482a75) dropped 4 markers. Need to: (1) check whether any tooling (claim-tracker, AOP scanner) depends on those exact IDs surviving rewrites, (2) re-attach them to surviving content if tooling depends, or (3) document the deprecation if not. Discovery surfaced by Wave 1 worker; deferred from soc-9xn0 scope."}, {"title": "Bats test count audit gap — Total: 226 in eval only counts test-hooks.sh, not test-hooks.bats", "type": "tech-debt", "severity": "low", "source": "post-mortem-finding", "description": "evals/agentops-core/hook-lifecycle-behavior.json pins Total: 226 from bash tests/hooks/test-hooks.sh. The parallel test-hooks.bats suite (86 tests) is NOT counted. New bats-only test additions (like the 2 added in PR #263 for soc-rv5p) silently bypass the audit gate. Either: add a parallel pin for bats count, or merge the suites, or mark this as intentional and document why."}, {"title": "ao goals measure cwd bug: tool reports No such file or directory for scripts that exist when invoked from outside repo root", "type": "bug", "severity": "medium", "source": "post-mortem-finding", "description": "/tmp/ao goals measure --file /home/boful/dev/agentops/GOALS.md returns 21/22 fails with output: bash: scripts/check-competitive-freshness.sh: No such file or directory. The script exists; goals measure invokes bash scripts/... without resolving to repo root. This makes the gate signal noisy and unreliable for any non-cwd-equals-repo-root invocation. Surfaced during soc-9xn0 wave 6 validation."}, {"title": "docs/documentation-index.md links to ../evals/workbench/ which mkdocs flags as unrecognized", "type": "tech-debt", "severity": "low", "source": "post-mortem-finding", "description": "mkdocs --strict (now passing on warnings) still emits INFO: \"docs/documentation-index.md contains an unrecognized relative link ../evals/workbench/, it was left as is.\" Either fix the link to resolve into the docs tree, or convert to a github.com URL like other out-of-tree references. Surfaced incidentally during soc-9xn0 wave 6 validation."}, {"title": "Pre-existing anchor mismatches in cdlc.md, the-science.md, GLOSSARY.md — fix or document", "type": "tech-debt", "severity": "low", "source": "post-mortem-finding", "description": "Several pre-existing mkdocs INFO warnings about anchor mismatches: GLOSSARY.md → how-it-works.md#ralph-wiggum-pattern-..., cdlc.md → the-science.md#part-6-..., context-lifecycle.md → #the-knowledge-ledger-..., releases/2026-03-21-v2.28.0-notes.md → CHANGELOG.md#2280---2026-03-21. These are pre-existing (not introduced by soc-9xn0) but surfaced during validation. Cleanup is mechanical; should batch as a single small PR."}], "consumed": true, "claim_status": "consumed", "claimed_by": null, "claimed_at": null, "consumed_by": "soc-xlw8", "consumed_at": "2026-05-08T09:30:00-04:00"} -{"source_epic": "soc-e4ulx", "timestamp": "2026-05-07T22:56:30-04:00", "items": [{"title": "Build observation-log aggregator from CI artifacts so Wave 1E can promote", "type": "feature", "severity": "medium", "source": "post-mortem-finding", "description": "Wave 1E (soc-f42z9) requires >=20 advisory CI runs in .agents/reconcile/observation-log.jsonl plus 0 false-positive incidents. The Wave 1C advisory job emits per-run observation JSON as a workflow artifact but nothing aggregates them into the log. Build a pull-mode aggregator (gh run list + gh run download then jq-append) or a scheduled workflow that commits aggregated observations to a tracked path. Without it, 1E is permanently un-promotable and Waves 2-4 (gated by Thesis-Stability + 1E acceptance) stall.", "target_repo": "agentops"}, {"title": "Write scripts/check-thesis-stability.sh and seed wave-0-thesis-snapshot.md", "type": "feature", "severity": "medium", "source": "post-mortem-finding", "description": "Plan v2 of soc-e4ulx specifies a mechanical thesis-stability check that diffs current README/PRODUCT/GOALS hero sections against a snapshot frozen at end of Wave 0. The script and .agents/reconcile/wave-0-thesis-snapshot.md do not exist yet. The Thesis-Stability Gate (soc-r3y8b) is operator-decision-gated but the mechanical helper would surface drift before the operator decides. Required before Waves 2-4 can be evaluated.", "target_repo": "agentops"}, {"title": "Fix pre-existing mkdocs --strict INFO link rot in 4 docs", "type": "tech-debt", "severity": "low", "source": "post-mortem-finding", "description": "mkdocs --strict (now passing) emits INFO-level warnings about unrecognized relative links in docs/GLOSSARY.md, docs/cdlc.md, docs/context-lifecycle.md, and docs/releases/2026-03-21-v2.28.0-notes.md. These are pre-existing (surfaced incidentally during Wave 1D mkdocs run, not introduced by Wave 1). Mechanical cleanup; suitable for a single small batched PR. Reduces noise in mkdocs output that obscures real future regressions.", "target_repo": "agentops"}, {"title": "Replace hardcoded BATS test count pin with auto-detection or ratchet", "type": "tech-debt", "severity": "low", "source": "post-mortem-finding", "description": "evals/agentops-core/pre-push-gate-governance.json hardcodes the BATS plan line (e.g. 1..51). When new BATS tests are added, the pin silently drifts; codex-team caught the 1..50 -> 1..51 case after PR #263 added a 51st test. Either auto-detect from the test plan output or convert to a ratchet that surfaces the diff with a one-line bump prompt. Prevents brittle test-count audits from blocking unrelated work.", "target_repo": "agentops"}, {"title": "Add CI-policy parity to crank wave acceptance for any wave touching .github/workflows/", "type": "task", "severity": "medium", "source": "post-mortem-finding", "description": "Wave 1C added factory-claim-ledger-strict (advisory) to validate.yml without wiring summary.needs / AGENTS.md tables. Codex-team caught and fixed via commit c587b361 before merge to PR #264. Prevention: when a crank wave touches .github/workflows/, the worker prompt and the wave acceptance step must include `bash scripts/validate-ci-policy-parity.sh` (must exit 0). Update skills/crank/SKILL.md and the swarm worker template accordingly.", "target_repo": "agentops"}], "consumed": true, "claim_status": "consumed", "claimed_by": null, "claimed_at": null, "consumed_by": "soc-xlw8", "consumed_at": "2026-05-08T09:30:00-04:00"} -{"source_epic": "soc-hdot", "timestamp": "2026-05-10T19:45:47-04:00", "items": [{"title": "Backfill practices: declarations into next 10-15 primitives (pass 2)", "type": "task", "severity": "medium", "source": "post-mortem-finding", "description": "Continue the bounded practice-citation backfill. Target: drop missing count from 741 toward 730. Candidates: bug-hunt, brainstorm, council, design, dream, evolve, forge, goals, harvest, knowledge-activation, oss-docs, openai-docs, llm-wiki, openai-docs (filter to most-used). Plan template same as .agents/plans/2026-05-10-practice-citation-backfill-rpi-core.md."}, {"title": "Pre-mortem audit-coverage gap: schema + allowlist enumeration for new frontmatter keys", "type": "improvement", "severity": "low", "source": "post-mortem-finding", "description": "Update the planning-rules to include a mandatory pre-mortem check for ALL strict consumers (additionalProperties: false in schemas, allowlist regexes in codex-bundle scripts) when adding new top-level YAML/frontmatter keys. Reference learning: ~/.agents/learnings/2026-05-10-new-frontmatter-key-needs-schema-and-allowlist-audit.md."}], "consumed": true, "claim_status": "consumed", "claimed_by": null, "claimed_at": null, "consumed_by": "commit:00f17867", "consumed_at": "2026-05-10T22:54:46-04:00", "notes": "pass-2 backfill landed; pre-mortem schema fix included in pass-1 epic close"} -{"source_epic": "soc-8lah", "timestamp": "2026-05-10T19:59:53-04:00", "items": [{"title": "Backfill practices: declarations into next 10-15 primitives (pass 3 of N)", "type": "task", "severity": "medium", "source": "post-mortem-finding", "description": "Continue the bounded backfill. After pass 2: 26/728. Pass-3 candidates: autodev, beads, bootstrap, codex-team, compile, converter, deps, doc, dream, evolve, grafana-platform-dashboard, heal-skill, hooks-authoring, inject. Use plan template at .agents/plans/2026-05-10-practice-citation-backfill-pass2.md."}, {"title": "Stop committing .agents/ session artifacts in RPI cycles", "type": "process-improvement", "severity": "medium", "source": "post-mortem-finding", "description": "Pass 1 committed 10 session artifacts under .agents/ that violated the audit-truth allowlist (caught at pass-2 pre-push). Going forward, RPI write-paths .agents/research/plans/council/post-mortems/learnings/rpi/execution-packet/phase-summaries should be created locally but NEVER staged for commit. Only .agents/rpi/next-work.jsonl and other allowlisted files get tracked. Learning: ~/.agents/learnings/2026-05-10-agents-dir-is-local-only-except-allowlist.md."}], "consumed": true, "claim_status": "consumed", "claimed_by": null, "claimed_at": null, "consumed_by": "commit:52615efd", "consumed_at": "2026-05-10T22:54:46-04:00", "notes": "pass-3 + .agents/ ignore policy landed (chore 52615efd + 32c3fbbf)"} -{"source_epic": "evolve-cycle-1", "timestamp": "2026-05-10T20:39:58-04:00", "items": [{"title": "Backfill practices: declarations into next 10-15 primitives (pass 4 of N)", "type": "task", "severity": "medium", "source": "post-mortem-finding", "description": "Continue the bounded backfill. After pass 3: 39/715. Pass-4 candidates: inject, llm-wiki, oss-docs, openai-docs, perf, product, provenance, push, quickstart, ratchet, readme, recover, refactor. Use plan template at .agents/plans/2026-05-10-practice-citation-backfill-pass2.md (skipping local artifact tracking per pass-2 learning)."}], "consumed": true, "claim_status": "consumed", "claimed_by": null, "claimed_at": null, "consumed_by": "commit:bef9b42d", "consumed_at": "2026-05-10T20:55:00-04:00"} -{"source_epic": "evolve-cycle-2", "timestamp": "2026-05-10T20:45:29-04:00", "items": [{"title": "Backfill practices: declarations into next 10-15 primitives (pass 5 of N)", "type": "task", "severity": "medium", "source": "post-mortem-finding", "description": "After pass 4: 52/702. Pass-5 candidates: red-team, release, research, retro (already done), reverse-engineer-rpi, review, rpi (already done), scaffold, scenario, scope, security, security-suite, shared, status, swarm, test, trace, update, using-agentops. Filter out already-declared; pick 13 net new."}], "consumed": true, "claim_status": "consumed", "claimed_by": "evolve:cycle-1", "claimed_at": "2026-05-10T22:41:44-04:00", "consumed_by": "commit:db847f0c", "consumed_at": "2026-05-10T22:48:23-04:00"} -{"source_epic": "evolve-cycle-3", "timestamp": "2026-05-10T22:48:23-04:00", "items": [{"title": "Backfill practices: declarations into next 10-15 primitives (pass 6 of N)", "type": "task", "severity": "medium", "source": "post-mortem-finding", "description": "After pass 5: 65/689. Pass-6 candidates from validator MISSING list — skill SKILL.md targets only (no codex twin edits). Suggested 13: pr-implement, pr-plan, pr-research, pr-retro, reverse-engineer-rpi, shared, skill-auditor, skill-builder, system-tuning, using-agentops, vibe, validation, wiki-knowledge-surface (if separate primitive). Plan template: .agents/plans/2026-05-10-practice-citation-backfill-pass2.md. Always run scripts/regen-codex-hashes.sh and skip codex twin edits."}], "consumed": true, "claim_status": "consumed", "claimed_by": null, "claimed_at": null, "consumed_by": "commit:302be5e1", "consumed_at": "2026-05-10T22:54:16-04:00"} -{"source_epic": "evolve-cycle-4", "timestamp": "2026-05-10T22:54:46-04:00", "items": [{"title": "Backfill practices: declarations into hooks/* primitives (pass 7: scope study + first wave)", "type": "task", "severity": "medium", "source": "post-mortem-finding", "description": "Pass-6 exhausted skills/ — all 75 skills with practices, 0 missing. Remaining 679 missing primitives split across: cli/ (533), evals/ (61), hooks/ (52), schemas/ (33). Pass-7: study how validate-practice-citations.sh discovers non-skill primitives (frontmatter? header doc?), pick the natural declaration carrier for hooks/ (smallest pool), and land a 10-15 hook subset as proof-of-shape. Defer cli/ + evals/ + schemas/ until shape is validated."}], "consumed": true, "claim_status": "consumed", "claimed_by": null, "claimed_at": null, "consumed_by": "commit:9aab1644", "consumed_at": "2026-05-10T23:01:19-04:00"} -{"source_epic": "evolve-cycle-5", "timestamp": "2026-05-10T23:01:19-04:00", "items": [{"title": "Backfill practices: declarations into hooks/* — second wave (10-15 hooks)", "type": "task", "severity": "medium", "source": "post-mortem-finding", "description": "Pass-7 proved the shape (10 hooks). hooks/ pool has ~52 total; ~42 remain missing. Pass-8 continues the same pattern: pick the next 10-15 hooks from the validator MISSING list, map to PRACTICE.md slugs based on hook purpose, insert '# practices: [slug, ...]' near the existing description, run 'cd cli && make sync-hooks' to refresh embedded copies, gate. After pass 8 expect ~95-100 with practices."}], "consumed": true, "claim_status": "consumed", "claimed_by": null, "claimed_at": null, "consumed_by": "commit:cdb3238b", "consumed_at": "2026-05-10T23:07:02-04:00"} -{"source_epic": "evolve-cycle-6", "timestamp": "2026-05-10T23:07:02-04:00", "items": [{"title": "Backfill practices: declarations into hooks/* — final wave (remaining ~30 hooks)", "type": "task", "severity": "medium", "source": "post-mortem-finding", "description": "After pass 8: 97/657. Estimated ~30 hooks/ remain. Pass-9 closes out hooks/ completely. Continue the same shape: insert '# practices: [slug, ...]' near the description, sync embedded copies, gate. After pass 9 hooks/ should be fully declared. Next: pivot to evals/ (61), schemas/ (33), then the much-larger cli/ (533). The cli/ pool may need a different declaration carrier — possibly a single declaration per package main.go or a manifest, rather than per file."}], "consumed": true, "claim_status": "consumed", "claimed_by": null, "claimed_at": null, "consumed_by": "commit:ce8259f6", "consumed_at": "2026-05-10T23:16:18-04:00"} -{"source_epic": "evolve-cycle-7", "timestamp": "2026-05-10T23:16:18-04:00", "items": [{"title": "Backfill practices: declarations into schemas/* (pass-10, smallest remaining pool: 33)", "type": "task", "severity": "medium", "source": "post-mortem-finding", "description": "After pass 9: 127/627 with skills/ + hooks/ fully declared. Pass-10 tackles schemas/ (33 JSON files — smallest remaining pool). Map each schema's purpose to PRACTICE.md slugs, then decide on declaration carrier. JSON has no comment syntax, so likely options: (a) add 'practices' as a top-level schema property, (b) add it to a $comment field, (c) add a sidecar schemas/.practices.json. Validator already accepts 'practices: [...]' pattern via regex over first 200 lines — option (a) or (b) most natural. Defer evals/ (61) and cli/ (533) until shape is validated."}], "consumed": true, "claim_status": "consumed", "claimed_by": null, "claimed_at": null, "consumed_by": "commit:020f06aa", "consumed_at": "2026-05-10T23:25:10-04:00"} -{"source_epic": "evolve-cycle-8", "timestamp": "2026-05-10T23:25:10-04:00", "items": [{"title": "Backfill practices: declarations into evals/agentops-core/*.json (pass 11: 61 evals)", "type": "task", "severity": "medium", "source": "post-mortem-finding", "description": "After pass 10: 160/594. Pass-11 tackles evals/agentops-core/ (61 JSON eval files). Adopt same dual carrier as schemas: top-level 'practices' for loose JSON, '$comment' fallback if strict. Eval-shape slugs likely cluster around: llm-eval-harness, prompt-as-spec, ai-assisted-dev, design-by-contract, dora-metrics, snapshot-testing. After pass 11 expect ~221/533. Final pool: cli/ (533 Go files) needs '// practices: [...]' comment shape; defer to pass 12+."}], "consumed": true, "claim_status": "consumed", "claimed_by": null, "claimed_at": null, "consumed_by": "commit:68c1df0f", "consumed_at": "2026-05-10T23:46:40-04:00"} -{"source_epic": "evolve-cycle-9", "timestamp": "2026-05-10T23:46:40-04:00", "items": [{"title": "Backfill practices: declarations into cli/ Go files (pass 12: design + first wave)", "type": "task", "severity": "medium", "source": "post-mortem-finding", "description": "After pass 11: 221/533. The remaining 533 missing primitives are ALL Go files under cli/cmd/ao/. cli/ is a much larger pool than skills/hooks/schemas/evals combined. Pass-12 needs a design call: (a) per-file '// practices: [slug, ...]' top-of-file comment (validator regex matches), (b) one declaration per package (smaller surface), or (c) a manifest approach (cli/PRACTICES.yaml) — but then primitives_scanned list needs updating. Recommend (a) for direct match to validator's per-file scan. Slug families for cli: command surface → pragmatic-programmer, twelve-factor-app; daemon → microservices, sre; goals → dora-metrics; eval → llm-eval-harness; rpi → agile-manifesto; etc. ~533 files is a lot — split into N waves of ~50-100 files per cycle."}], "consumed": true, "claim_status": "consumed", "claimed_by": null, "claimed_at": null, "consumed_by": "commit:f9ec46c9", "consumed_at": "2026-05-11T07:49:58-04:00"} -{"source_epic":"evolve-cycle-11","timestamp":"2026-05-11T07:53:29-04:00","items":[{"title":"soc-owed.7: link eval security SBOM + digital twin evidence into release audit","type":"feature","severity":"medium","source":"evolve-generator","description":"Scoped this cycle. Files in play (1115 LOC total): scripts/resolve-release-artifacts.sh (72), scripts/validate-release-audit-artifacts.sh (265), scripts/eval-agentops.sh (454), tests/scripts/release-artifacts.bats (231), docs/release-e2e-checklist.md (93). Plan: (1) read resolve-release-artifacts.sh + validate-release-audit-artifacts.sh to map current evidence-discovery boundary; (2) extend resolve to include eval, security, SBOM, HIL, VIL, digital-twin paths; (3) extend validate to fail on missing evidence; (4) add BATS coverage for each new evidence path; (5) update docs/release-e2e-checklist.md proof-bundle table. Blocks: soc-owed.4, .5, .6. Bead: soc-owed.7. Acceptance: release-artifacts.json requires all official evidence paths; audit validation fails on missing eval/security/twin; docs show zero-trust proof bundle. || CYCLE-12 SCOUT (2026-05-11T07:58): Manifest is generated in scripts/ci-local-release.sh:455-523 with 5 evidence fields (sbom_cyclonedx, sbom_spdx, security_report, release_readiness, hil_evidence). resolve-release-artifacts.sh:30-68 currently REQUIRES those 5. To add eval+vil+twin, do TWO slices: (S1) producer side — add eval-evidence.json, vil-evidence.json, twin-evidence.json emitters (likely hook into eval-agentops.sh + release VIL/twin lanes) and add 3 new fields to the manifest JSON in ci-local-release.sh; (S2) consumer + validator — extend resolve-release-artifacts.sh required-fields list and validate-release-audit-artifacts.sh fail-rules + BATS coverage. Slice S1 first lands the data sources; S2 enforces. Each is ~150-200 LOC. || CYCLE-13 FINDING (2026-05-11T08:03): Sibling bead soc-owed.4 (which BLOCKS .7 conceptually) requires evidence shape design — scripts/check-release-readiness.sh must consume evidence JSON files first, with schema/timestamp/version validation. eval-agentops.sh currently emits per-suite outputs (run.json, compare.json, coverage.json, baseline-audit.json) but NO top-level eval-evidence.json summary. VIL and digital-twin emitters do not exist yet. This is operator-level epic work (3+ beads coordinated): (a) design evidence schema, (b) add emitters across eval/VIL/twin lanes, (c) refactor check-release-readiness, (d) wire into release artifact manifest. /evolve cannot productively chip at this in single cycles without epic-level design. RECOMMENDATION: park for operator-driven /rpi session OR break into smaller beads under soc-owed.","disposition":"CYCLE-13 RECOMMENDED park: epic-level work, not single-cycle. Consumed at cycle-14 to clear active set; underlying bead soc-owed.7 remains open for operator-driven /rpi.","legacy_source":"evolve-scouting"}],"consumed":true,"claim_status":"consumed","claimed_by":null,"claimed_at":null,"consumed_by":"evolve-cycle-14/soc-ymph.7","consumed_at":"2026-05-11T09:04:58-04:00"} -{"source_epic": "evolve-postmortem-2026-05-11", "timestamp": "2026-05-11T08:10:08-04:00", "items": [{"title": "Improve skills/evolve/SKILL.md: 6 concrete patches from 13-cycle session", "type": "feature", "severity": "high", "source": "post-mortem-finding", "description": "Land 6 patches to skills/evolve/SKILL.md identified by direct observation across cycles 1-13 of the 2026-05-11 session. See .agents/learnings/2026-05-11-evolve-skill-friction-from-13-cycle-session.md for full rationale. Patches: (1) Step 3.0 Scope Filter - route multi-file features to scout-mode after N>5 productive cycles. (2) Step 5.0 Source Surface Detection - auto-rebuild cli/bin/ao when cli/**/*.go changes; auto sync-hooks when skills/** or hooks/** changes. (3) Step 5.1 Gate Output Parsing - grep 'Pass [0-9]+: (FAILED|BLOCKED)' instead of trusting trailing status line. (4) Operator-shape decisions exception to Step 4 - AskUserQuestion permitted for shape decisions affecting >50 files or schema/contract surfaces; carve-out from the 'do NOT ask' rule. (5) CONTEXT_BUDGET_EXHAUSTED stop reason - context_streak counter, exits with handoff after 2 consecutive context-too-heavy cycles. (6) Document ScheduleWakeup self-perpetuation mode - the Claude-Code-harness equivalent of terminal-mode 'ao evolve'. Also: promote scout-mode to first-class result type (not 'idle' or 'harvested'). See .agents/learnings/2026-05-11-evolve-scout-mode-pattern.md. Also: add Step 4 hint about mechanical batch tasks (script vs per-file Edit). See .agents/learnings/2026-05-11-evolve-mechanical-scaling-works.md. Files: skills/evolve/SKILL.md (primary), possibly skills/evolve/references/scout-mode.md (new), skills/evolve/references/mechanical-batches.md (new), scripts/regen-codex-hashes.sh + make sync-hooks after editing the codex twin. Validation: pre-push --fast, then a /evolve smoke (--max-cycles=1 --dry-run)."}], "consumed": true, "claim_status": "consumed", "claimed_by": null, "claimed_at": null, "consumed_by": "evolve-cycle-45", "consumed_at": "2026-05-12T08:00:00-04:00"} -{"source_epic": "evolution-roadmap-drain-2026-05-11", "timestamp": "2026-05-11T21:01:29-04:00", "items": [{"title": "Stabilize soc-l4yt pre-push-gate-governance canary flake", "type": "bug", "severity": "high", "source": "post-mortem-finding", "description": "Contract canary agentops-core.pre-push-gate-governance flakes 50/50 with no code change. PRE_PUSH_SKIP_EVAL=1 used 4-5 times this session as documented override. Investigate eval pipeline root cause.", "target_repo": "agentops"}, {"title": "Strong-verify high-visibility AOP-CLAIM markers via PG4 pattern", "type": "feature", "severity": "medium", "source": "post-mortem-finding", "description": "25 AOP-CLAIM verifies closed via pointer-evidence in all-claims-evidence-map.md. Promote highest-visibility claims to PG4-strong evidence files (start with TRUST-FACTORY-FIVE-STEP-PRIMITIVE, 9 refs).", "target_repo": "agentops"}, {"title": "Extend TG1 council-coverage gate to PR-diff matching", "type": "feature", "severity": "low", "source": "post-mortem-finding", "description": "check-three-gap-supergate.sh --gap=council-coverage uses heuristic (>= 1 file). Extend to map PR commits to required council artifacts.", "target_repo": "agentops"}], "consumed": true, "claim_status": "consumed", "claimed_by": null, "claimed_at": null, "consumed_by": "bd-routed: high → soc-l4yt closed cycle 44 cfc59bdc; medium → 2-of-N done cycles 46+47 + remaining under BC4 epic soc-2klg; low → identical to soc-w6vh.6", "consumed_at": "2026-05-12T14:55:00-04:00"} -{"source_epic": "discovery-2026-05-12-ddd-hexagonal", "timestamp": "2026-05-12T10:55:00-04:00", "items": [{"title": "Backfill consumes/produces/context_rel on SKILL.md frontmatter", "type": "feature", "severity": "medium", "source": "post-mortem-finding", "description": "Wave 2 #4 produced a context-map with empty Mermaid edges and an empty consumes/produces table — the schema fields exist but no SKILL.md has them set. Backfill across the 77 skills so the context-map carries real inter-skill semantics (otherwise the map is just a hexagonal-role bucketing). Scriptable via the same one-off pattern as Issue #3.", "target_repo": "agentops"}, {"title": "Reconcile cmd/ao.executionPacket vs domain packet.ExecutionPacket field sets", "type": "tech-debt", "severity": "medium", "source": "post-mortem-finding", "description": "Wave 1 #6 introduced a forward type alias to packet.ExecutionPacket (6 fields), but the live struct is cmd/ao.executionPacket (28 fields). Either hoist additional fields (RunID, EpicID, BeadID, TrackerMode, EpicCriteria, AutodevProgram, MixedMode*, ...) into the domain aggregate, OR formalize cmd/ao.executionPacket as a write-side DTO and explicitly position packet.ExecutionPacket as the read/validate-side type. Document the chosen direction in ADR-0001 or a follow-up ADR.", "target_repo": "agentops"}, {"title": "Reconcile 11 phantom skill slugs in research artifact", "type": "tech-debt", "severity": "low", "source": "post-mortem-finding", "description": ".agents/research/2026-05-12-ddd-hexagonal-research.md §(d) classification table lists 11 slugs that don't exist in skills/ (circulate, discover, eval, harness, lifecycle, overnight, quality, search, sketch, storage, vibecheck). Either delete from the research artifact or create the missing skills. The Wave 2 #3 backfill script handled them gracefully via OVERRIDES, but the research artifact itself remains inconsistent.", "target_repo": "agentops"}, {"title": "Make scripts/generate-cli-reference.sh portable to mawk", "type": "tech-debt", "severity": "low", "source": "post-mortem-finding", "description": "The awk pipeline uses POSIX {2,} interval expressions that mawk silently rejects, producing a 14-line empty COMMANDS.md on mawk hosts (Ubuntu default). Rewrite the regex to use [[:space:]][[:space:]]+ (or similar explicit-repetition form) so the script works on both gawk (CI) and mawk (local). Add a smoke-test under mawk to catch future regressions.", "target_repo": "agentops"}, {"title": "Add runID path-traversal validation to storage_fs.Repo", "type": "tech-debt", "severity": "low", "source": "post-mortem-finding", "description": "cli/internal/adapters/storage_fs/packet_repo.go does not sanitize runID before filepath.Join. v1 trust model accepts that (only domain-side callers today), but harden the boundary before any non-domain caller arrives — reject '..', '/', and other path-traversal tokens at Save/Load entry.", "target_repo": "agentops"}, {"title": "Add atomic write/rename to storage_fs.Repo.Save()", "type": "tech-debt", "severity": "low", "source": "post-mortem-finding", "description": "Save() writes the 'latest' pointer file before the per-run archive. If the second write fails after the first succeeds, latest points to a packet that has no archive copy. Acceptable for v1 (single-process), but switch to write-temp + rename for both files, or write archive first then atomically update latest via symlink/rename, before a second concurrent consumer arrives.", "target_repo": "agentops"}, {"title": "Add mkdocs venv install to CLAUDE.md Quick Local Validation", "type": "docs", "severity": "low", "source": "post-mortem-finding", "description": "System mkdocs 1.1.2 cannot parse the modern mkdocs.yml (needs material plugins). Validation workers had to set up a venv with requirements-docs.txt to run `mkdocs build --strict`. Add a one-liner to CLAUDE.md's 'Quick Local Validation' section: `python3 -m venv .venv-mkdocs && .venv-mkdocs/bin/pip install -r requirements-docs.txt && .venv-mkdocs/bin/mkdocs build --strict`.", "target_repo": "agentops"}], "consumed": true, "claim_status": "consumed", "claimed_by": null, "claimed_at": null, "consumed_by": null, "consumed_at": null, "consumed_note": "age-lspj triage 2026-07-02: #0 LANDED (frontmatter+context-map edges) #1 OBSOLETE (rpi legacy lane) #2 OBSOLETE (.agents local) #3 LANDED (mawk-portable regex) #4 LANDED (validateRunID) #5 LANDED (AtomicWriteFile) #6 OBSOLETE (section gone)."} -{"source_epic": "evolve-2026-05-19", "timestamp": "2026-05-19T11:23:23-04:00", "items": [{"title": "Refactor pre-push-gate.sh HAS_ path filters to work in both fast and full modes", "type": "tech-debt", "severity": "medium", "source": "post-mortem-finding", "description": "Currently HAS_GO/HAS_SKILL/HAS_EVAL/etc initialize to 1 then reset to 0 only inside the FAST_MODE block. Outside FAST_MODE they stay default-1 — a footgun that caused PR #350 to be ineffective. Refactor: compute the diff once at script start, populate HAS_ based on the diff regardless of mode.", "target_repo": "agentops"}, {"title": "Investigate goals validate FAIL on canonical full gate", "type": "bug", "severity": "medium", "source": "post-mortem-finding", "description": "Pre-existing FAIL on goals validate step in full pre-push-gate. Returns valid:false with errors. Did not block CI on PRs because diff filters skip it. Needs investigation.", "target_repo": "agentops"}, {"title": "Investigate contract-canaries FAIL on canonical full gate", "type": "bug", "severity": "medium", "source": "post-mortem-finding", "description": "Pre-existing FAIL on contract canaries step (tests/canaries/agentops-core-official.txt, 11 suites). Distinct from agentops-contract-canaries CI job which passes.", "target_repo": "agentops"}, {"title": "Round-3 shellcheck cleanup sweep", "type": "tech-debt", "severity": "low", "source": "post-mortem-finding", "description": "PR #349 cleaned 6 files, PR #353 cleaned 3 more. Full-gate shellcheck step likely has 1-3 more pre-existing warnings to sweep.", "target_repo": "agentops"}, {"title": "Auto-mark consumed next-work items when target features are detected as already-implemented", "type": "feature", "severity": "low", "source": "post-mortem-finding", "description": "4 of 7 unconsumed items in .agents/rpi/next-work.jsonl were confirmed already-done this session. Build a periodic sweep that auto-flips consumed:true when the implementation is detected.", "target_repo": "agentops"}, {"title": "Eval-baseline-promotion policy decision for gitignored baselines", "type": "task", "severity": "low", "source": "post-mortem-finding", "description": "Local .agents/evals/baselines/ is gitignored — operators starting fresh have no baselines. Decide: (a) commit baselines to a non-gitignored path, (b) document promote-all as setup, or (c) treat no-baseline as warn-not-fail.", "target_repo": "agentops"}, {"title": "worktree-disposition gate false-positive on cross-session leftover worktrees", "type": "bug", "severity": "low", "source": "post-mortem-finding", "description": "Pre-push gate worktree-disposition check FAILs on other-session worktrees (codex/, crank/, etc.) on disk. Narrow scope or document expected on multi-agent operator machines.", "target_repo": "agentops"}], "consumed": true, "claim_status": "consumed", "claimed_by": null, "claimed_at": null, "consumed_by": null, "consumed_at": null, "consumed_note": "age-lspj triage: #0-#3 OBSOLETE (bash gate demoted ag-qidx; May-era) #4 -> age-gtyb #5 -> age-ll9q #6 LANDED (worktree-disposition rework)."} -{"source_epic": "ag-nk67", "timestamp": "2026-05-29T14:52:22-04:00", "items": [{"title": "Compose a single `make regen-all` / `ao skill add --finalize` that runs every derived-artifact generator", "type": "task", "severity": "medium", "source": "post-mortem-finding", "description": "Adding 1 skill/command to agentops touched ~8 generated registries/goldens across 9 CI rounds (PR #598). The generators already exist (sync-skill-counts, generate-skill-domain-map, generate-registry, generate-context-map, make sync-hooks, generate-cli-reference, check-cmdao-surface-parity --write-surface) but are not composed into one local entrypoint, and Codex twins are manual. Build one finalizer + document the pre-push gate sweep so skill/command additions are one command, not a serial CI scavenger hunt. See .agents/learnings/2026-05-29-skill-addition-gate-surface-tax.md", "target_repo": "agentops"}], "consumed": true, "claim_status": "consumed", "claimed_by": null, "claimed_at": null, "consumed_by": null, "consumed_at": null, "consumed_note": "age-lspj triage: LANDED (make regen-all + regen-check)."} -{"source_epic": "session-2026-05-30-deconflict", "timestamp": "2026-05-30T12:05:37-04:00", "items": [{"title": "Reconcile soc-wi5t.6 duplicate ID across issues + wisps tables", "type": "bug", "severity": "medium", "source": "post-mortem-finding", "description": "bd id soc-wi5t.6 exists in BOTH the issues and wisps tables in the shared bushido Dolt, which makes `bd orphans` error out repo-wide. Reconcile in a quiet window (bd doctor --fix or manual de-dup) when no parallel session is mid-write. Surfaced 2026-05-30 during bead-graph integrity validation.", "target_repo": "agentops"}], "consumed": true, "claim_status": "consumed", "claimed_by": null, "claimed_at": null, "consumed_by": null, "consumed_at": null, "consumed_note": "age-lspj triage: OBSOLETE (bd/Dolt retired)."} -{"source_epic": "openinspect-to-mt-olympus-assured-bg-agent", "timestamp": "2026-05-31T19:39:52Z", "items": [{"title": "Fix dangling evidence: OpenInspect research cited by mt-olympus doctrine is gitignored", "type": "task", "severity": "high", "source": "council-finding", "description": "mt-olympus ADR 2026-05-31 + docs/doctrine/assured-background-agent/corpus/quote_bank/quote_bank.md cite ~/dev/agentops/.agents/research/openinspect/, which is gitignored in agentops. The doctrine evidence base will not persist for other clones/machines. Commit the research to a durable location or inline the key quotes/findings into the mt-olympus repo so citations resolve.", "target_repo": "mt-olympus"}, {"title": "Set CLAUDE_CODE_OAUTH_TOKEN in mt-olympus and revert claude-review soft-fail", "type": "task", "severity": "medium", "source": "post-mortem-finding", "description": "claude-review failed because the OAuth token secret is unset; made advisory via continue-on-error (commit 00ad35c). Add the secret, confirm green, then remove continue-on-error.", "target_repo": "mt-olympus"}, {"title": "Make the honesty-gate validator query mo-qz3srw status instead of a manual env flag", "type": "task", "severity": "medium", "source": "council-finding", "description": "check-doctrine-drift.py K6 defaults to proof-OPEN and relies on MO_QZ3SRW_CLOSED=1/--proof-closed. This manual coupling will rot. Query the bead/ledger so the honesty gate relaxes automatically when the external-Block proof lands.", "target_repo": "mt-olympus"}, {"title": "Wire the assured-background-agent doctrine validators into mt-olympus CI", "type": "task", "severity": "medium", "source": "post-mortem-finding", "description": "Add validate-corpus.py + check-doctrine-drift.py to static-gates so PRODUCT.md/GOALS.md/positioning.md cannot drift off-doctrine and the honesty gate is enforced in CI.", "target_repo": "mt-olympus"}, {"title": "Close mo-csl6ma and check mo Dolt server health (57331 i/o timeouts)", "type": "task", "severity": "low", "source": "post-mortem-finding", "description": "bd close mo-csl6ma timed out twice against the mo Dolt server on bushido (127.0.0.1:57331); work merged via PR #16 regardless. Close the bead and check whether the server is wedged under the live mt agent.", "target_repo": "mt-olympus"}, {"title": "Validator FP-tuning may mask a real honesty-gate breach in table rows", "type": "task", "severity": "low", "source": "council-finding", "description": "check-doctrine-drift.py skips markdown table rows + a SAFE substring list to suppress false positives; a genuine unqualified validated claim inside a table row would be missed. Revisit precision/recall if the gate becomes load-bearing in CI.", "target_repo": "mt-olympus"}], "consumed": true, "claim_status": "consumed", "claimed_by": null, "claimed_at": null, "consumed_by": null, "consumed_at": null, "consumed_note": "age-lspj triage: all 6 OBSOLETE for agentops (cross-repo mt-olympus items misfiled here + bd/mo-Dolt retired); refile in mt-olympus tracker if still wanted."} -{"source_epic":"session-2026-05-31-postmortem","timestamp":"2026-05-31T15:48:36-04:00","items":[{"title":"Regression tests for skip/dedup/consumed logic must use the REAL persisted data shape","type":"task","severity":"medium","source":"post-mortem-finding","description":"ag-mjlg (PR #652): ao next-work materialize idempotency test set consumed at the per-item level, but the real next-work.jsonl marks it at the batch level — a guard test exercising a shape production never produces gave false green and let a 44-vs-16 over-creation bug ship. Add a test-standard (skills/standards/references/test-pyramid.md or go.md): regression/idempotency/skip/dedup tests must construct fixtures by round-tripping a real persisted sample, not the convenient constructor. Consider a lint/checklist item in pre-mortem-checks.","target_repo":"agentops","legacy_proof_ref":{"kind":"pr","id":"652","path":"cli/cmd/ao/next_work_materialize_test.go"}}],"consumed":true,"claim_status":"consumed","claimed_by":null,"claimed_at":null,"consumed_by":null,"consumed_at":null,"consumed_note":"age-lspj triage: LANDED (Fixture Fidelity in test-pyramid.md + go.md rule)."} -{"source_epic": "ag-pj51", "timestamp": "2026-06-11T06:53:07-04:00", "items": [{"title": "Phase 2: execute disposition ledger (45 RETIRE, 13 MERGE-INTO) — Bo-gated", "type": "task", "severity": "medium", "source": "post-mortem-finding", "description": "evidence/skill-prune-dispositions.md is the ledger; extend inbound-ref sweep to tests/ + .github/workflows/ per learning 2026-06-11-skill-extraction-must-sweep-test-couplings before any retire. Archive branch, not delete.", "target_repo": "agentops"}, {"title": "Lane E: skill admission gate + usage-ranked GC in /evolve", "type": "task", "severity": "medium", "source": "post-mortem-finding", "description": "The editor the producer chain lacks: new skill requires demand evidence; periodic usage-GC. Prevents cathedral regrowth.", "target_repo": "agentops"}, {"title": "Merge driver for skills-codex generated JSON", "type": "task", "severity": "low", "source": "council-finding", "description": "Concurrent hash-stamp updates auto-merge into invalid JSON (c414a5a0a). Add .gitattributes rule so they conflict loudly, or make regen chain the documented post-rebase step in AGENTS-WORKFLOW.md.", "target_repo": "agentops"}, {"title": "Close ag-pj51 in bd (deferred — dolt breaker open on :58821)", "type": "chore", "severity": "low", "source": "post-mortem-finding", "description": "Work is on main d074f81a9..8ceae3527; bead close blocked by dolt circuit breaker mid-session (3306 up, 58821 down). Close with evidence/skill-prune-laneb-thinning.md as proof.", "target_repo": "agentops"}], "consumed": true, "claim_status": "consumed", "claimed_by": null, "claimed_at": null, "consumed_by": null, "consumed_at": null, "consumed_note": "age-lspj triage: #0 -> DECISION bead age-e3zk (stale May ledger, needs Bo + fresh pass) #1 -> age-7d3r (intake checklist only) #2 OBSOLETE (merge=union council ag-bdg1) #3 OBSOLETE (bd retired)."} -{"source_epic": "recent-docs-direction-postmortem", "timestamp": "2026-06-16T09:01:58-04:00", "items": [{"title": "Migrate check-loop-shape live tracker path to br", "type": "tech-debt", "severity": "high", "source": "post-mortem-finding", "description": "GOALS.md now marks scripts/check-loop-shape.sh as a reconciliation gap because it still inspects legacy bd JSON. Move the live path to br/BEADS_DIR=_beads, keep fixture compatibility, and update the bats/self-test coverage before any strict-mode gate flip.", "target_repo": "agentops"}, {"title": "Sweep downstream docs for autonomous code validation framing", "type": "docs", "severity": "medium", "source": "retro-learning", "description": "README, PRODUCT, GOALS, 3.0, component map, and autonomy ladder now lead with autonomous code validation. Sweep comparison docs, generated skill-page intros, and downstream public copy so they do not drift back to broad SDLC-control-plane or orchestration-first positioning.", "target_repo": "agentops"}, {"title": "Repair strict docs build link and anchor warnings", "type": "tech-debt", "severity": "medium", "source": "post-mortem-finding", "description": "scripts/docs-build.sh --check still fails on 15 pre-existing unresolved links and anchors in contracts/corpus-learning-seam.md, contracts/pawls.md, doctrine/operating-discipline.md, and generated docs pages. Fix those separately so docs validation can become meaningful for future direction changes.", "target_repo": "agentops"}, {"title": "Use single-quoted rg patterns when searching for backticks", "type": "process-improvement", "severity": "low", "source": "retro-learning", "description": "This session lost time when shell backticks inside rg patterns triggered command substitution and unmatched quotes. Use single-quoted patterns or quote escaping for searches containing literal backticks, especially in zsh automation snippets.", "target_repo": "*"}], "consumed": true, "claim_status": "consumed", "claimed_by": null, "claimed_at": null, "consumed_by": null, "consumed_at": null, "consumed_note": "age-lspj triage: #0 -> age-ojv0 (P3, live gate reads retired bd) #1 OBSOLETE (positioning realized) #2 -> age-y0ww (consolidated) #3 OBSOLETE (workflow tip)."} -{"source_epic": "doc-source-truth-cleanup-postmortem", "timestamp": "2026-06-16T10:08:13-04:00", "items": [{"title": "Land docs-doctrine ratchet for current-path source truth", "type": "process-improvement", "severity": "high", "source": "council-finding", "description": "Reconcile current-path docs and gates after the source-truth cleanup WARN: update ARCHITECTURE.md and operating-loop.md so local cockpit gate, br, and /validate are the only live paths taught outside explicitly historical docs; reconcile docs/SKILLS.md generated/manual ownership; make unresolved live skill references fail current-path docs instead of remaining advisory.", "evidence": "Post-mortem judges all returned WARN. docs/ARCHITECTURE.md line 5 conflicts with lines 94, 98, and 336; docs/architecture/operating-loop.md lines 103 and 112 still name retired skill paths; docs/SKILLS.md was edited while declared generated.", "target_repo": "*"}, {"title": "Add doc-class admission gate plus archive boundary", "type": "process-improvement", "severity": "high", "source": "post-mortem-finding", "description": "Define live doc classes such as canonical, public, runbook, generated, historical, evidence, and working-memory; require owner/source/supersedes/generator or review/removal metadata for new live docs; exclude or archive raw plans, audits, learnings, handoffs, and old release notes only after a manifest exists.", "evidence": ".agents/plans/2026-06-16-doc-source-truth-cleanup.md Phase 3 and Phase 4 were intentionally deferred; council found the shipped slice routed sprawl but did not reduce it.", "target_repo": "*"}, {"title": "Repair strict docs-build warning set", "type": "tech-debt", "severity": "medium", "source": "post-mortem-finding", "description": "Fix or disposition the current strict docs build warning set so docs validation becomes a meaningful gate for future direction changes. Current warnings include out-of-tree links in operating-loop, corpus-learning-seam, pawls, and operating-discipline plus anchor drift in INCIDENT-RUNBOOK.md, contracts/pawls.md, and generated skills/discovery.md.", "evidence": "bash scripts/docs-build.sh --check aborted with 16 warnings during the 2026-06-16 post-mortem.", "target_repo": "agentops"}, {"title": "Investigate validate.yml zero-job push failures", "type": "bug", "severity": "medium", "source": "post-mortem-finding", "description": "GitHub Actions validate.yml push runs are completing failure with no jobs and no failed log, including the shipped doc cleanup commit. Determine whether this is a workflow trigger/config problem, a required-check backstop issue, or GitHub Actions metadata artifact; restore a useful remote signal or explicitly document why branch pushes should not run it.", "evidence": "GitHub Actions run 27622554977 for commit 828d39c19525088a1ca87be998be5b8c5aaf1666 failed with jobs=[] and log not found; recent validate.yml push runs show the same pattern.", "target_repo": "agentops"}], "consumed": true, "claim_status": "consumed", "claimed_by": null, "claimed_at": null, "consumed_by": null, "consumed_at": null, "consumed_note": "age-lspj triage: #0 LANDED (ARCHITECTURE.md hookless banner) #1 -> DECISION bead age-bqow #2 -> age-y0ww (consolidated) #3 OBSOLETE (validate.yml triggers intended)."} -{"source_epic": "age-gud", "timestamp": "2026-06-16T13:10:16-04:00", "items": [{"title": "Wire orchestration_decision + ValidateShape into the live /discovery path", "type": "task", "severity": "high", "source": "council-finding", "description": "S-record stamp fires only in retired rpi_* Go engine; ValidateShape has zero callers. Make live /discovery stamp the record + call ValidateShape with real am-roster/write-set inputs. Bead age-aqu.", "target_repo": "agentops"}], "consumed": true, "claim_status": "consumed", "claimed_by": null, "claimed_at": null, "consumed_by": null, "consumed_at": null, "consumed_note": "age-lspj triage: LANDED (ValidateShape wired live via orchestrate_shape/stamp_shape)."} -{"source_epic": "age-kf-s1-close-loop-0ly", "timestamp": "2026-06-16T13:27:44-04:00", "items": [{"title": "Measure a POSITIVE knowledge-field A/B delta (S2-gated)", "type": "task", "severity": "medium", "source": "council-finding", "description": "First live A/B (s-2026-06-16-002) gave delta=-0.37: the gold pull hurt (thin/flat gold = noise+5x tokens). Positive delta gated on S2 richer/tiered gold + tighter byte-bounded pull. Tracked as bead age-kf-s1-close-loop-0ly.4.", "target_repo": "agentops"}], "consumed": true, "claim_status": "consumed", "claimed_by": null, "claimed_at": null, "consumed_by": null, "consumed_at": null, "consumed_note": "age-lspj triage: OBSOLETE (demoted corpus hypothesis ADR-0004; delta did not hold)."} -{"source_epic": "age-vx0", "timestamp": "2026-06-18T08:01:29-04:00", "items": [{"title": "Live controller must emit usage BEFORE the gate-verdict (ordering-attribution correctness)", "type": "task", "severity": "low", "source": "council-finding", "description": "age-vx0 surfaced that when a usage event is emitted AFTER its attempt-N gate-verdict, usageAttempt attributes the spend FORWARD to the next verdict and it reads Productive instead of rework. The dynamo-e2e rework-order scenario only gets correct attribution because it emits attempt-1 usage before the verdict (realistic produce->gate order). When the real dispatch controller (ag-v1xk) is built, it must emit usage as it is spent (before the gate verdicts that attempt) so the attempt-ordering rework join classifies retry spend correctly. Otherwise L will under-count rework on real runs.", "target_repo": "agentops"}], "consumed": true, "claim_status": "consumed", "claimed_by": null, "claimed_at": null, "consumed_by": null, "consumed_at": null, "consumed_note": "age-lspj triage: -> age-m80q (acceptance-note carrier; ag-v1xk id no longer resolves)."} -{"source_epic": "age-wedge-all-in-dyr0", "timestamp": "2026-07-01T20:43:36-04:00", "items": [{"title": "AGENTS-WORKFLOW.md: document ao done as the recommended close path", "type": "task", "severity": "medium", "source": "post-mortem-finding", "description": "Deferred by the .4 worker (shared-file collision risk). One doc line in the br-tracking/session-completion section: close via ao done so the verdict stamp lands in close_reason. Belongs with dyr0.2s docs lane.", "target_repo": "agentops"}, {"title": "Wire ao scenario evaluate into nightly + author gate: acceptance vectors", "type": "task", "severity": "medium", "source": "post-mortem-finding", "description": "The producer landed (dyr0.10, da0181f8863e) but real spec/scenarios/*.json carry no acceptance_vectors[].check yet, so everything evaluates judgment-shaped. Author gate: vectors for the mappable directives (e.g. s-005 -> gate:go-complexity-ceiling) and add the evaluate step to the nightly dream-cycle job so satisfaction accumulates.", "target_repo": "agentops"}, {"title": "Flip verdict-backstop to enforce after a week of green", "type": "task", "severity": "low", "source": "post-mortem-finding", "description": "verdict-backstop.yml (dyr0.9, 322d419572b0) runs report-only on main pushes. After ~a week of green annotations, flip enforce for the callable input default or wire an enforcing caller. Tamper already fails in both modes.", "target_repo": "agentops"}, {"title": "crank ship-loop-anti-patterns: add the direct-push verify-landed-before-close rule", "type": "task", "severity": "medium", "source": "post-mortem-finding", "description": "The PR lane has confirmed-MERGED-before-close; the direct-push lane needs its analogue (fetch + git merge-base --is-ancestor origin/main before br close). Add to skills/crank/references/ship-loop-anti-patterns.md + mirror skills-codex twin. Evidence: dyr0.4 close cited a raced-out sha; finding f-2026-07-01-verify-landed-before-close.", "target_repo": "agentops"}, {"title": "Taste-fenced dyr0 remainder: .2 surface diet, .5 quick-start, .12 comparison page", "type": "task", "severity": "high", "source": "post-mortem-finding", "description": "Operator-held per the duel plan (bo-voice surfaces). .2 also owes the ao done doc line and its CLI-help slice waits on age-h4y3/age-nzwo. Epic age-wedge-all-in-dyr0 stays open until these land.", "target_repo": "agentops"}, {"title": "pawl auto-bind: same-file foreign ledger rows get swept into the bind commit", "type": "task", "severity": "low", "source": "post-mortem-finding", "description": "Observed on fed1e5795190: the path-scoped auto-bind commit included another session's uncommitted verdict edge (age-m44f@77e4176) because both rows live in docs/provenance/ledger.jsonl — path-scoping separates FILES, not rows. Harmless (edges valid, emit idempotent, chain intact) but the #trivial bind commit misattributes authorship of foreign rows. Option: auto-bind diffs pre/post emit and warns when extra pre-existing rows ride along.", "target_repo": "agentops"}], "consumed": true, "claim_status": "consumed", "claimed_by": null, "claimed_at": null, "consumed_by": null, "consumed_at": null, "consumed_note": "2026-07-02 rpi follow-through (RE-APPLIED x2 after concurrent-lane rewrites): [0] pre-landed (L217); [1] age-26y1 @1ab0b0921; [2] deferred age-zauu (>=2026-07-08); [3] age-5ixw @1778e8404; [4] taste beads pre-landed; [5] age-7krl @1033c0186."} -{"source_epic":"goal-design-b1-postmortem","timestamp":"2026-07-09T10:54:37-04:00","items":[{"title":"Enforce goal-design cross-file identity invariants","type":"tech-debt","severity":"high","source":"council-finding","description":"Add checker and fixture coverage for intent_ref.path, intent.slug vs driver.slug, and candidate scenario references before the goal-design skill consumes packets.","evidence":"Post-mortem tech-debt judge found digest validation without semantic path, slug, or scenario binding.","target_repo":"agentops","consumed":false,"claim_status":"available"},{"title":"Make goal-design proof dependencies fail closed","type":"process-improvement","severity":"high","source":"council-finding","description":"Update the goal-design Bats proof or its gate context so missing python3, PyYAML, or jsonschema fails proof-critical validation instead of skipping behavior assertions.","evidence":"Post-mortem tech-debt and learnings judges found HAVE_SCHEMA_DEPS can turn behavior tests into skips.","target_repo":"agentops","consumed":false,"claim_status":"available"},{"title":"Persist independent validation verdicts for goal-design packets","type":"improvement","severity":"medium","source":"council-finding","description":"Store the independent PASS verdict for a goal-design packet in a replayable artifact or ledger when driver.artifact_validation.required_verdict is PASS.","evidence":"Post-mortem plan and learnings judges found the independent validator PASS was session-local rather than replayable from the commit.","target_repo":"agentops","consumed":false,"claim_status":"available"},{"title":"Add the goal-design skill and Codex parity","type":"feature","severity":"medium","source":"retro-pattern","description":"Implement B2 after checker hardening: a goal-design skill and Codex twin that produce exactly intent.md and driver.md conforming to the B1 schemas.","evidence":"Original driver B2 remains the next feature candidate after B1 contract closure.","target_repo":"agentops","consumed":false,"claim_status":"available"}],"consumed":false,"claim_status":"available","claimed_by":null,"claimed_at":null,"consumed_by":null,"consumed_at":null} -{"source_epic":"task-1-evidence-bound-goal-closeout","timestamp":"2026-07-10T18:55:00-04:00","items":[{"title":"stale-packet sweep root is raw cwd, not repoRootOrCwd — silent no-op from subdirs","type":"bug","severity":"low","source":"council-finding","description":"sweepStalePackets receives root from resolveProjectDir() (cli/cmd/ao/yield_report.go:205), so `ao yield report` run from a subdirectory (verified live from cli/) silently reports no stale packets. projectdir.go guidance (age-6sg.1) warns about exactly this. Fix: resolve via repoRootOrCwd or equivalent."},{"title":"stale-packet sweep evidence arms narrower than intent S3 given (no landed-commit / first-failing-proof arm)","type":"improvement","severity":"low","source":"council-finding","description":"S3's given lists closed bead id, landed commit, or passing first-failing-proof as shipped evidence; the v1 sweep implements CONFIRMED verdict edge + closed candidate bead only (git-log slug arm deliberately excluded as self-triggering). A stale packet with only a landed commit persists silently. Consider a non-self-triggering landed-commit arm (e.g. commit subject citing ' B' shape, excluding the packet-creation commit)."},{"title":"yield_report_stale_packet_test fixture includes a '## Close-out' heading production never writes","type":"tech-debt","severity":"low","source":"council-finding","description":"Guard-test fixture-fidelity rule (.claude/rules/go.md): fixtures must round-trip the real persisted shape. The close tool appends bare '- Closed:' / '- Disposition' lines with no heading; the Go fixture adds a '## Close-out' heading. Not currently false-green (sweep doesn't key on the heading) but drift-prone. Align the fixture with production close output."},{"title":"pawl engage-deadline (240s) < warm-panel p50 route latency (261s) — systematic timeout-degrades","type":"bug","severity":"medium","source":"post-mortem-finding","description":"Two of four routes this run degraded on engage-deadline timeouts (one 1/2, one 0/2 -> spurious REFUTED binds + re-review churn). ao pawl metrics showed p50=261s p95=262s vs PAWL_ENGAGE_DEADLINE=240 default (scripts/pawl.sh:57). Either raise the default above measured p95, derive it from recorded route metrics, or make the deadline start at pane ENGAGE rather than dispatch. Also: the pawl-service session vanished mid-run (idle reap?) requiring ao pawl up mid-arc."},{"title":"provenance ledger-reader-version floor did not catch payload-hash semantic skew between ao builds","type":"bug","severity":"medium","source":"post-mortem-finding","description":"Installed ao v3.1.0-1088 (Jul 2) declared ledger-reader-version 1 and false-flagged docs/provenance/ledger.jsonl as BROKEN at line 423 (payload_hash mismatch) for records written by newer ao; rebuilt v3.2.0-225 verified the same 441 records intact and also reports reader-version 1. The reader-version floor probe cannot distinguish hashing-semantics changes. Bump the reader version on any payload-hash field-set change, and consider the gate hinting 'rebuild ao' when the failing record was written by a newer writer."}],"consumed":false,"claim_status":"available","claimed_by":null,"claimed_at":null,"consumed_by":null,"consumed_at":null} -{"source_epic": "rpi-followups-2026-07-09", "timestamp": "2026-07-10T14:06:16-04:00", "items": [{"title": "Route sweep P1s into beads: govulncheck CI lane, pin security scanners, extract shrink-only ratchet lib", "type": "task", "severity": "high", "source": "council-finding", "description": "The 2026-07-09-claude sweep SYNTHESIS.md action list (docs/audits/codebase-recon-2026-07-09-claude/) was deliberately deferred to a findings-routing /plan pass: (1) govulncheck lane in CI - the CVE sat undetected a week; (2) scanners installed @latest unpinned; (3) scripts/lib/ratchet.sh extraction from 8 in-tree shrink-only instances. One /plan pass, then per-bead lands.", "target_repo": "agentops"}, {"title": "Fix warm pawl route: apply trivial-waiver walk-back before routing (age-pawl-warm-route-no-trivial-walk-3po3)", "type": "bug", "severity": "medium", "source": "council-finding", "description": "With a #trivial bind at tip, the cold path reviews the feat commit behind it; the warm route reviewed the raw bind diff and burned a round on a correct wrong-object REFUTE. Route the walked-back target or refuse routing on provenance-only tips.", "target_repo": "agentops"}, {"title": "Fix pawl preflight in throwaway/fixture repos (age-pawl-preflight-toyrepo-red-17vt)", "type": "bug", "severity": "medium", "source": "council-finding", "description": "pawl-review-lib-parity.bats tests 1-3 red on clean main: pawl_preflight runs the gate battery inside the bats fixture repo and FAILs 3 checks on a README-only diff instead of SKIPping. Differential-verified pre-existing.", "target_repo": "agentops"}, {"title": "Refresh installed ao binary so landed pawl guards activate (landed != installed)", "type": "task", "severity": "medium", "source": "post-mortem-finding", "description": "tip-coherence + canonical evidence rooting landed in scripts/ + embedded copies, but ao pawl review runs the INSTALLED binary embedded bundle - the new guards are inert until the binary is rebuilt/reinstalled. Run preflight-uat-binary.sh after.", "target_repo": "agentops"}], "consumed": true, "claim_status": "consumed", "claimed_by": null, "claimed_at": null, "consumed_by": "age-ratchet-lib-extraction-bv7d", "consumed_at": "2026-07-10T23:30:00-04:00", "consumed_note": "[0] routing DONE (govulncheck+pins landed 297dd022b, ratchet lib = epic age-ratchet-lib-extraction-bv7d all 10 slices closed); [1] tracked as age-pawl-warm-route-no-trivial-walk-3po3; [2] tracked as age-pawl-preflight-toyrepo-red-17vt; [3] routed to age-refresh-installed-ao-binary-ne1pj."} -{"source_epic": "p1-security-arc-2026-07-10", "timestamp": "2026-07-10T15:17:55-04:00", "items": [{"title": "Ratchet lib extraction arc (age-ratchet-lib-extraction-bv7d): slice per call-site, then atomic-write ratchet", "type": "task", "severity": "medium", "source": "post-mortem-finding", "description": "Routed but unstarted: extract scripts/lib/ratchet.sh from the 8 in-tree shrink-only instances (sweep pattern-extraction N1), then N2 atomic-write ratchet closing the 3-sweep-old crash-safety gap. Own arc with differential tests per migrated call site. DELIVERED 2026-07-10: all 10 slices of age-ratchet-lib-extraction-bv7d landed and closed.", "target_repo": "agentops"}, {"title": "Fast mode for test-toolchain-validate.sh: mock the scanner battery so the suite is locally runnable", "type": "task", "severity": "low", "source": "post-mortem-finding", "description": "The suite invokes the real full scanner battery and cannot complete on a dev Mac (killed twice, 180s timeout saw only test 1). run_govulncheck landed without suite coverage because of this; a stub-PATH mock mode (like pawl-review-lib-parity fixtures) would make it a usable regression surface.", "target_repo": "agentops"}], "consumed": true, "claim_status": "consumed", "claimed_by": null, "claimed_at": null, "consumed_by": "age-ratchet-lib-extraction-bv7d", "consumed_at": "2026-07-10T23:30:00-04:00", "consumed_note": "[0] DELIVERED by epic age-ratchet-lib-extraction-bv7d (10/10 slices landed+closed 2026-07-10); [1] routed to age-test-toolchain-fast-mode-nw3ao."} -{"source_epic":"age-h433","timestamp":"2026-07-12T18:12:00-04:00","items":[{"title":"Make post-mortem preflight runnable from the repository root","type":"bug","severity":"low","source":"retro-learning","description":"Correct the post-mortem source and Codex instructions so the documented strict preflight command executes literally from repository root, and add a fixture that runs the documented command from that cwd. The skill currently teaches scripts/preflight-refs.sh --strict while the executable lives at skills/post-mortem/scripts/preflight-refs.sh.","evidence":"The literal documented command failed with No such file or directory during the age-h433 post-mortem; the skill-relative command passed with missing=0.","target_repo":"agentops","bead_id":"age-bt12k","claim_status":"available","dedup_key":"retro|post-mortem-preflight-command-path"},{"title":"Preserve required v1.4 lifecycle fields during next-work materialization","type":"bug","severity":"high","source":"post-mortem-finding","description":"Make ao next-work materialize preserve every required batch lifecycle field and self-validate the rewritten JSONL before atomic replacement. A schema-valid 34-row queue became invalid immediately after materialization because the rewriter dropped claimed_by and claimed_at from 33 historical batches and the new batch, producing 66 strict violations.","evidence":"bash scripts/validate-next-work.sh --strict passed before materialization and failed with 66 missing-field errors immediately after ao next-work materialize --source-epic age-h433.","target_repo":"agentops","bead_id":"age-sfwdr","dedup_key":"post-mortem|next-work-materialize-preserve-required-lifecycle"}],"consumed":false,"claim_status":"available","consumed_by":null,"consumed_at":null,"claimed_by":null,"claimed_at":null} diff --git a/.githooks/pre-push b/.githooks/pre-push deleted file mode 100755 index 5cc66f8d8..000000000 --- a/.githooks/pre-push +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env bash -# bd-shim v1 -# bd-hooks-version: 0.49.3 -# -# Pre-push hook: delegates to bd for issue tracking. -# -# Historical bd shim. This tracked .githooks directory is NOT the live AgentOps -# cockpit gate. The live gate is installed by scripts/install-pre-push-gate.sh -# into git-common-dir/hooks (i.e. .git/hooks), which chains -# scripts/hooks/pre-push.local (ordinary build, race, and repository checks). -# -# IMPORTANT (recon-2026-07-02 audit A2): git honors this dir ONLY if -# core.hooksPath is unset or points here. A bd install (or any tool) can redirect -# core.hooksPath at its own dir (e.g. .beads/hooks) — and then git runs ONLY that -# dir and the AgentOps gate never fires. Verify with `git config --get -# core.hooksPath`; the installer now DETECTS and REFUSES a hijacked hooksPath. -# This hook does not perform semantic validation or control delivery. - -set -euo pipefail - -HOOK_STDIN_FILE="$(mktemp)" - -cleanup() { - rm -f "$HOOK_STDIN_FILE" -} -trap cleanup EXIT - -cat >"$HOOK_STDIN_FILE" - -run_without_git_env() { - local var_name - local -a env_args=(env) - while IFS='=' read -r var_name _; do - [[ "$var_name" == GIT_* ]] || continue - env_args+=("-u" "$var_name") - done < <(env) - "${env_args[@]}" "$@" -} - -# --- bd issue tracking (existing behavior) --- -if command -v bd >/dev/null 2>&1; then - run_without_git_env bd hooks run pre-push "$@" <"$HOOK_STDIN_FILE" -fi diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index c8e458ecf..7cc853f4c 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -16,9 +16,8 @@ name: Validate # MUST be empty. on: - # Local validation is the release authority for routine direct-main work. - # Keep Actions as an explicit/manual or release-tag backstop so push-to-main - # does not consume Actions quota or become a serialization bottleneck. + # This repository chooses tag, manual, pull-request, and merge-group runs. + # AgentOps itself does not assign delivery authority to this workflow. push: tags: - 'v*' @@ -114,11 +113,7 @@ jobs: contracts: - 'schemas/**' - 'docs/contracts/**' - # Claude workflows (ag-jy8gj): the workflow-governance drift gate is a - # bidirectional identity match between .claude/workflows/*.js and the - # `workflows:` ledger section in docs/contracts/skill-dispositions.yaml. - # A .js add/remove/rename must re-run that gate, so the workflow dir is - # a contracts trigger (the ledger half is already covered by docs/contracts/**). + # Runtime workflow changes are contract-shaped inputs. - '.claude/workflows/**' # redteam-pack target globs (ag-nl1u): every file the # security redteam pack @@ -246,7 +241,7 @@ jobs: # grouped into one named job (ag-87sv). Pure regroup of already-live gates — # no new gate logic. Absorbs from skills-integrity (heal --strict, # validate-skill-schema, validate-skill-frontmatter, validate-skill-body-refs) - # + contracts-sync (validate-skill-flow) + doctrine-proof + # + contracts-sync + doctrine-proof # (check-scenario-test-linkage), and adds the six-surface drift sweep # `regen-all.sh --check`. Required: listed in `summary.needs`. # ───────────────────────────────────────────────────────────────────────── @@ -661,25 +656,13 @@ jobs: retention-days: 7 # ───────────────────────────────────────────────────────────────────────── - # skills-integrity — SKILL.md schema/frontmatter/body-ref/dependency - # validation, structural heal, headless-runtime skills, and plugin-load - # (manifests + symlink ban + local-only .agents + structure). Absorbs: - # skill-schema, skill-frontmatter, skill-body-refs, skill-dependency-check, - # skill-integrity, validate-headless-runtime-skills, plugin-load-test. - # ───────────────────────────────────────────────────────────────────────── - # ───────────────────────────────────────────────────────────────────────── - # summary — aggregate verdict over the purpose jobs PLUS AP#7 mechanical - # Evidence-claim verification (was validate-pr-evidence-claims). AP#7 must - # run after all sibling jobs so their per-job logs are available, so it lives - # here rather than in process-hygiene. Invokes scripts/verify-gate-claim.sh. + # summary — aggregate deterministic check status over the purpose jobs. # ───────────────────────────────────────────────────────────────────────── summary: needs: [changes, go-gate-shadow, correctness, security] runs-on: ubuntu-latest if: always() steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - - name: Check results run: | echo "=== CI Summary ===" @@ -728,97 +711,3 @@ jobs: echo "" echo "✅ All checks passed" - - # ── AP#7 — verify Evidence claims in PR body (was validate-pr-evidence-claims) ── - - name: Extract Evidence claims from PR body - id: claims - if: github.event_name == 'pull_request' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - set -euo pipefail - body="$(gh pr view "${{ github.event.pull_request.number }}" --json body --jq .body)" - claims="$(printf '%s\n' "$body" | sed -n 's/^Evidence:[[:space:]]*//p' || true)" - if [[ -z "$claims" ]]; then - echo "skip=true" >> "$GITHUB_OUTPUT" - echo "::notice::No Evidence: line in PR body; AP#7 verification skipped" - exit 0 - fi - printf '%s\n' "$claims" > /tmp/evidence-claims.txt - echo "skip=false" >> "$GITHUB_OUTPUT" - echo "count=$(wc -l < /tmp/evidence-claims.txt | tr -d ' ')" >> "$GITHUB_OUTPUT" - echo "Evidence claims to verify ($(wc -l < /tmp/evidence-claims.txt | tr -d ' ')):" - cat /tmp/evidence-claims.txt - - - name: Fetch this workflow run's logs (per-job, durable) - if: github.event_name == 'pull_request' && steps.claims.outputs.skip != 'true' - id: fetch_log - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - set -euo pipefail - # Per-job log fetch (soc-1nsx). The per-job endpoint serves each - # completed job's log as soon as that job finishes, with no whole-run - # eventual-consistency window. We page through the run's jobs, fetch - # each completed (non-skipped) sibling's log, and concatenate. - : > /tmp/run.log - page=1 - jobs_seen=0 - jobs_with_logs=0 - while : ; do - page_json="$(gh api "repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/jobs?per_page=100&page=${page}")" - page_count="$(jq '.jobs | length' <<<"$page_json")" - if [[ "${page_count}" -eq 0 ]]; then - break - fi - jobs_seen=$((jobs_seen + page_count)) - while IFS=$'\t' read -r job_id job_conclusion; do - if [[ -z "${job_conclusion}" || "${job_conclusion}" = "skipped" ]]; then - continue - fi - if gh api "repos/${{ github.repository }}/actions/jobs/${job_id}/logs" >> /tmp/run.log 2>/dev/null; then - jobs_with_logs=$((jobs_with_logs + 1)) - fi - done < <(jq -r '.jobs[] | "\(.id)\t\(.conclusion // "")"' <<<"$page_json") - if [[ "${page_count}" -lt 100 ]]; then - break - fi - page=$((page + 1)) - done - log_bytes="$(wc -c < /tmp/run.log)" - echo "fetched per-job logs: ${jobs_with_logs}/${jobs_seen} jobs, ${log_bytes} bytes" - if [[ "${jobs_with_logs}" -eq 0 ]]; then - echo "log_ok=false" >> "$GITHUB_OUTPUT" - echo "::warning::No completed sibling jobs returned logs (${jobs_seen} jobs total); skipping AP#7 verification this run." - else - echo "log_ok=true" >> "$GITHUB_OUTPUT" - fi - - - name: Verify each Evidence claim - if: github.event_name == 'pull_request' && steps.claims.outputs.skip != 'true' && steps.fetch_log.outputs.log_ok == 'true' - run: | - set -euo pipefail - chmod +x scripts/verify-gate-claim.sh - failures=0 - total=0 - while IFS= read -r claim; do - [[ -z "$claim" ]] && continue - total=$((total + 1)) - if scripts/verify-gate-claim.sh --log /tmp/run.log \ - "pr-${{ github.event.pull_request.number }}" "$claim" >/dev/null; then - echo "::notice::Evidence claim verified: $claim" - else - failures=$((failures + 1)) - echo "::error::Evidence claim absent from workflow logs: $claim" - fi - done < /tmp/evidence-claims.txt - if [[ "$failures" -gt 0 ]]; then - echo "::error::$failures of $total Evidence claim(s) failed verification (ship-loop anti-pattern #7)" - exit 1 - fi - echo "::notice::All $total Evidence claim(s) verified against workflow run logs" - - - name: AP#7 verification unavailable (no sibling logs) - if: github.event_name == 'pull_request' && steps.claims.outputs.skip != 'true' && steps.fetch_log.outputs.log_ok != 'true' - run: | - echo "::notice::AP#7 verification skipped — no completed sibling jobs returned logs. The PR's Evidence: line was extracted but cannot be cross-checked against per-job logs in this CI shape (likely: only this job ran, or all siblings were skipped)." diff --git a/.gitignore b/.gitignore index 6683341f3..e77d865b5 100644 --- a/.gitignore +++ b/.gitignore @@ -123,51 +123,6 @@ MEMORY.md city.toml city.toml.bak -# AgentOps session artifacts -# Default: everything under the repo-root .agents/ is local/private runtime -# state (anchored — leading `/` keeps the rule from also matching nested -# .agents/ dirs like cli/.agents/, which has its own rule above). -/.agents/* -/.agents/**/* - -# Force-tracked .agents/ paths that compound across runs (audit truth, not -# session state). Each path below must remain reproducible: nightly+evolve -# digests, fitness deltas, and probe history depend on these existing in -# main between runs. Never broaden the allowlist without updating -# CLAUDE.md / PROGRAM.md to match. The leading directory re-includes are -# required because git refuses to re-include files inside an excluded -# parent directory. All patterns are anchored with a leading `/` so -# cli/.agents/, tests/claude-code/.agents/, etc. stay excluded. -!/.agents/ -!/.agents/rpi/ -!/.agents/rpi/next-work.jsonl -!/.agents/nightly/ -!/.agents/nightly/** -!/.agents/evolve/ -!/.agents/evolve/cycle-history.jsonl -!/.agents/evolve/session-state.json -!/.agents/goals/ -!/.agents/goals/**/ -!/.agents/goals/**/attempts.jsonl -!/.agents/findings/ -!/.agents/findings/registry.jsonl -# Reconciliation Engine arc — static contract artifacts (snapshot + operator -# decision records). -!/.agents/reconcile/ -!/.agents/reconcile/wave-0-thesis-snapshot.md -!/.agents/reconcile/thesis-stability-decision.md -!/.agents/reconcile/promotion-decision.md - -# AgentOps session artifacts -.agents/ -# Re-include the per-repo evolve preferences template (soc-6svt). The template -# ships in the repo as a starter; the actual preferences.yaml stays per-operator -# and remains gitignored. We have to re-allowlist each ancestor directory after -# the unanchored `.agents/` re-exclude above — git won't recurse into an -# excluded directory to evaluate file-level negations. -!/.agents/ -!/.agents/evolve/ -!/.agents/evolve/preferences.yaml.template evals/workbench/scorecard-latest.json .doctor/ diff --git a/Makefile b/Makefile index db2e2914b..44237efee 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ SHELL := /usr/bin/env bash -.PHONY: local-ci local-ci-fast ci build build-flywheel verify-buildtags test docs-check regen-all regen-check clean help +.PHONY: local-ci local-ci-fast ci build test docs-check regen-all regen-check clean help # Default: run the release-grade local CI gate. # Note: scripts/ci-local-release.sh already includes build + test + release-binary validation. @@ -14,15 +14,9 @@ local-ci-fast: ## Run local CI without e2e install test, using quick security mo ci: local-ci ## Alias for local-ci -build: ## Build ao CLI binary (default = spine; archived satellites omitted) +build: ## Build ao CLI binary $(MAKE) -C cli build -build-flywheel: ## Build ao with the ADR-0012 archived satellites restored (flywheel + legacy) - $(MAKE) -C cli build-flywheel - -verify-buildtags: ## Verify the ADR-0012 build-tag mechanism (default omits; flywheel/legacy restore) - $(MAKE) -C cli verify-buildtags - test: ## Run CLI tests $(MAKE) -C cli test diff --git a/bin/ralph b/bin/ralph index b46ff1f40..3e1be22fa 100755 --- a/bin/ralph +++ b/bin/ralph @@ -4,7 +4,7 @@ # # Usage: # ralph "Add dark mode support" -# ralph --skip-pre-mortem "Fix typo in README" +# ralph --skip-premortem "Fix typo in README" # ralph --branch feat/dark-mode "Add dark mode" # ralph --spec spec.md "Add dark mode" # ralph --dry-run "Add dark mode" @@ -14,7 +14,7 @@ set -euo pipefail # ── Phase order (for checkpoint skip logic) ─────────────────────────────────── -PHASES=(plan pre-mortem branch crank vibe post-mortem pr) +PHASES=(plan premortem branch crank vibe postmortem pr) # ── Defaults ────────────────────────────────────────────────────────────────── SKIP_PRE_MORTEM=false @@ -31,7 +31,7 @@ CODEX_BIN="${CODEX_BIN:-codex}" # ── Parse args ──────────────────────────────────────────────────────────────── while [[ $# -gt 0 ]]; do case "$1" in - --skip-pre-mortem) SKIP_PRE_MORTEM=true; shift ;; + --skip-premortem) SKIP_PRE_MORTEM=true; shift ;; --branch) BRANCH="$2"; shift 2 ;; --dry-run) DRY_RUN=true; shift ;; --resume) RESUME_FILE="$2"; shift 2 ;; @@ -223,10 +223,10 @@ PLAN_PATH="$RALPH_DIR/plan-path.txt" # ── Phase 2: Pre-mortem (optional) ──────────────────────────────────────────── if [[ "$SKIP_PRE_MORTEM" == "true" ]]; then - if ! should_skip "pre-mortem"; then - log "Skipping pre-mortem (--skip-pre-mortem)" - echo "SKIP" > "$RALPH_DIR/pre-mortem-verdict.txt" - save_checkpoint "pre-mortem" + if ! should_skip "premortem"; then + log "Skipping premortem (--skip-premortem)" + echo "SKIP" > "$RALPH_DIR/premortem-verdict.txt" + save_checkpoint "premortem" fi else PLAN_FILE="" @@ -234,13 +234,13 @@ else PLAN_FILE=$(cat "$PLAN_PATH") fi - run_phase "pre-mortem" \ + run_phase "premortem" \ "You are working in $WORKDIR. -Run /pre-mortem on the plan${PLAN_FILE:+ at $PLAN_FILE}.$(spec_context) -After the council finishes, write exactly one word — PASS, WARN, or FAIL — into $RALPH_DIR/pre-mortem-verdict.txt." +Run /premortem on the plan${PLAN_FILE:+ at $PLAN_FILE}.$(spec_context) +After the council finishes, write exactly one word — PASS, WARN, or FAIL — into $RALPH_DIR/premortem-verdict.txt." - if ! should_skip "pre-mortem"; then - PRE_MORTEM=$(read_verdict "$RALPH_DIR/pre-mortem-verdict.txt") + if ! should_skip "premortem"; then + PRE_MORTEM=$(read_verdict "$RALPH_DIR/premortem-verdict.txt") if [[ "$PRE_MORTEM" == "FAIL" ]]; then die "Pre-mortem verdict: FAIL — aborting. See $RALPH_LOG for details." fi @@ -285,10 +285,10 @@ if ! should_skip "vibe"; then fi # ── Phase 5: Post-mortem ────────────────────────────────────────────────────── -run_phase "post-mortem" \ +run_phase "postmortem" \ "You are working in $WORKDIR on branch $BRANCH. -Run /post-mortem to wrap up. Extract learnings. -Write exactly one word — PASS, WARN, or FAIL — into $RALPH_DIR/post-mortem-verdict.txt." +Run /postmortem to wrap up. Extract learnings. +Write exactly one word — PASS, WARN, or FAIL — into $RALPH_DIR/postmortem-verdict.txt." # ── Phase 6: PR ─────────────────────────────────────────────────────────────── if should_skip "pr"; then @@ -315,9 +315,9 @@ else $GOAL ## Council Verdicts -- Pre-mortem: $(read_verdict "$RALPH_DIR/pre-mortem-verdict.txt") +- Pre-mortem: $(read_verdict "$RALPH_DIR/premortem-verdict.txt") - Vibe: $(read_verdict "$RALPH_DIR/vibe-verdict.txt") -- Post-mortem: $(read_verdict "$RALPH_DIR/post-mortem-verdict.txt") +- Post-mortem: $(read_verdict "$RALPH_DIR/postmortem-verdict.txt") " if [[ -n "$SPEC_CONTENT" ]]; then diff --git a/cli/Makefile b/cli/Makefile index d24e60acb..12e9a517b 100644 --- a/cli/Makefile +++ b/cli/Makefile @@ -1,4 +1,4 @@ -.PHONY: build build-flywheel test lint clean install sync-hooks inner membrane-parity verify-buildtags +.PHONY: build test lint clean install sync-hooks inner # Build variables BINARY_NAME=ao @@ -8,17 +8,6 @@ LDFLAGS=-ldflags "-X main.version=$(VERSION)" IDENTIFIER=com.12factoragentops.ao GOLANGCI_LINT ?= ../scripts/golangci-lint-v2.sh -# ADR-0012 archive build tags. The default build OMITS both the corpus/flywheel -# (`flywheel`) and RPI/factory (`legacy`) command sets — they are archived, not -# deleted, so the code stays buildable for the revival conditions. Restore them: -# make build-flywheel # both tags (flywheel + legacy) -# AGENTOPS_LEGACY=1 make build # legacy only -GO_TAGS ?= -ifdef AGENTOPS_LEGACY -GO_TAGS += legacy -endif -GO_TAGS_FLAG = $(if $(strip $(GO_TAGS)),-tags "$(strip $(GO_TAGS))",) - # Default target all: build @@ -29,26 +18,14 @@ sync-hooks: @cp ../skills/standards/references/* embedded/skills/standards/references/ @cp ../scripts/lib/flywheel-compile.sh embedded/skills/compile/scripts/compile.sh -# Build the binary (default = spine; archived satellites omitted) +# Build the binary. build: sync-hooks @mkdir -p $(BUILD_DIR) - go build $(LDFLAGS) $(GO_TAGS_FLAG) -o $(BUILD_DIR)/$(BINARY_NAME) ./cmd/ao + go build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME) ./cmd/ao ifeq ($(shell uname -s),Darwin) codesign -f -s - -i $(IDENTIFIER) $(BUILD_DIR)/$(BINARY_NAME) endif -# Build with the archived satellites restored (ADR-0012): corpus/flywheel -# (`flywheel`) + RPI/factory (`legacy`). Used to run or maintain the archived -# command set under the revival conditions. -build-flywheel: GO_TAGS += flywheel legacy -build-flywheel: build - -# Verify the build-tag mechanism: default build omits the tagged set; the -# flywheel/legacy tags restore it. Compiles all three variants and checks the -# hidden `ao buildtags` introspection surface. -verify-buildtags: - bash ../scripts/verify-buildtags.sh - # Run tests test: build go test -v -shuffle=on ./... @@ -72,13 +49,6 @@ install: build @GOBIN=$$(go env GOBIN); [ -z "$$GOBIN" ] && GOBIN=$$(go env GOPATH)/bin; \ /bin/cp $(BUILD_DIR)/$(BINARY_NAME) "$$GOBIN/$(BINARY_NAME)" -# Membrane demo preflight (epic age-cwo, bead age-membrane-memory-arch-tz2s.2.5): -# fail if the demo-path `ao` lacks the current source's membrane surface, so the -# e2e demo can never run against a stale binary and emit a false proof. -# Override the binary under test with AO_BIN=/path/to/ao. -membrane-parity: - bash ../scripts/check-membrane-parity.sh - # Run the binary run: build ./$(BUILD_DIR)/$(BINARY_NAME) diff --git a/cli/README.md b/cli/README.md index 922338c8f..9d2eea301 100644 --- a/cli/README.md +++ b/cli/README.md @@ -1,8 +1,15 @@ # ao — AgentOps CLI -`ao` is the deterministic transaction kernel and evidence recorder beneath the -AgentOps operating loop. Agents own intent, implementation, and semantic -judgment. Repositories own Git delivery. +`ao` supplies deterministic repository utilities and evidence inspection. The +AgentOps semantic loop lives in the skills: + +```text +RPI → Plan → Implement → fresh Validate → durable verdict → report and stop +``` + +The CLI does not own retries, queues, work claims, Git delivery, release, +closure, or semantic validation. Consumer repositories choose their own Git and +CI policy. ## Install @@ -12,47 +19,29 @@ go install github.com/boshu2/agentops/cli/cmd/ao@latest ## Current executable truth -The command tree is mid-cut. Inspect the binary you actually have rather than -assuming a profile or narrative list: - ```bash ao capabilities ao robot-docs ao --help ``` -The generated [command reference](docs/COMMANDS.md) follows executable source -and is not hand-maintained in an authority-doc change. - -## Final boundary - -The direct-cut program converges on one profile-free command tree. Its lifecycle -transactions are: - -- pull one ready work leaf; -- freeze exact candidate identity; -- run or reuse exact-input deterministic evidence; -- record a PASS or FAIL supplied by an external fresh-context validator; -- record one Learn receipt; -- record repository-owned delivery and remote identity; and -- close the report and tracker leaf after verification. - -These transactions do not ask a model to judge work and do not push, merge, -queue, or select CI policy. Local and cloud agents use the same ports. - -K5, K7, and K9 own deletion of the old verdict-driving, delivery, and retired -gate implementations. Exact `CLI.` leaves own the remaining command -dispositions. F4 removes alternate build profiles. D2 regenerates this command -surface only after executable ownership is final. +The generated [command reference](docs/COMMANDS.md) follows the published Cobra +tree. Removed lifecycle commands are inert one-release tombstones; no build tag +or compatibility profile restores their implementation. ## Development -During the cut, use the focused test named by the owning leaf. Do not add new -profile membership, `init()` registration, package-global command ownership, -compatibility aliases, or dormant scaffolds. +```bash +make build +make test +``` + +Add deterministic utilities only when they do not become lifecycle or delivery +authorities. Keep semantic judgment in the Validate skill and external delivery +in the consumer repository. ## References - [Operating loop](../docs/architecture/operating-loop.md) -- [Go CLI architecture guide](../docs/architecture/go-cli-architecture-guide.md) -- [Direct-cut ADR](../docs/adr/ADR-0012-focus-surface-on-membrane-bookkeeper-archive-satellites.md) +- [CLI architecture](../docs/architecture/go-cli-architecture-guide.md) +- [Migration map](../docs/MIGRATION.md) diff --git a/cli/cmd/ao/buildtags.go b/cli/cmd/ao/buildtags.go deleted file mode 100644 index 2d85467fd..000000000 --- a/cli/cmd/ao/buildtags.go +++ /dev/null @@ -1,48 +0,0 @@ -package main - -import ( - "fmt" - - "github.com/spf13/cobra" -) - -// archiveBuildTags lists the ADR-0012 archive build tags this binary was -// compiled with. It is EMPTY in the default build (the spine). Tag-gated files -// append to it from their init(): -// -// cmd/ao/buildtags_flywheel.go (//go:build flywheel) -> "flywheel" -// cmd/ao/buildtags_legacy.go (//go:build legacy) -> "legacy" -// -// The mechanism: a command archived behind one of these tags lives in a file -// carrying the matching `//go:build ` constraint, so its init() (which -// registers the cobra command) is only compiled when that tag is passed. The -// default `go build ./...` omits the file entirely — the command is neither -// compiled nor registered. `make build-flywheel` (tags: flywheel legacy) or -// `AGENTOPS_LEGACY=1 make build` (tag: legacy) restore them. ADR-0012 requires -// archive-not-delete, so the code stays buildable for the revival conditions. -var archiveBuildTags []string - -// buildtagsCmd is a hidden introspection surface: it reports which archive build -// tags the running binary was compiled with, so an operator (or a test) can tell -// a spine build from a restored-satellite build. Hidden so the default command -// surface is unchanged. -var buildtagsCmd = &cobra.Command{ - Use: "buildtags", - Short: "Report which ADR-0012 archive build tags this binary was compiled with", - Hidden: true, - Args: cobra.NoArgs, - RunE: func(cmd *cobra.Command, _ []string) error { - if len(archiveBuildTags) == 0 { - fmt.Fprintln(cmd.OutOrStdout(), "spine (no archive build tags; corpus/flywheel + RPI/factory omitted)") - return nil - } - for _, t := range archiveBuildTags { - fmt.Fprintln(cmd.OutOrStdout(), t) - } - return nil - }, -} - -func init() { - rootCmd.AddCommand(buildtagsCmd) -} diff --git a/cli/cmd/ao/buildtags_flywheel.go b/cli/cmd/ao/buildtags_flywheel.go deleted file mode 100644 index f80a2e74b..000000000 --- a/cli/cmd/ao/buildtags_flywheel.go +++ /dev/null @@ -1,9 +0,0 @@ -//go:build flywheel - -package main - -// Compiled only with `-tags flywheel`. Marks the binary as carrying the archived -// corpus/flywheel command set (ADR-0012). The default build omits this file. -func init() { - archiveBuildTags = append(archiveBuildTags, "flywheel") -} diff --git a/cli/cmd/ao/buildtags_legacy.go b/cli/cmd/ao/buildtags_legacy.go deleted file mode 100644 index 8fee3520a..000000000 --- a/cli/cmd/ao/buildtags_legacy.go +++ /dev/null @@ -1,9 +0,0 @@ -//go:build legacy - -package main - -// Compiled only with `-tags legacy`. Marks the binary as carrying the archived -// RPI/factory command set (ADR-0012). The default build omits this file. -func init() { - archiveBuildTags = append(archiveBuildTags, "legacy") -} diff --git a/cli/cmd/ao/buildtags_test.go b/cli/cmd/ao/buildtags_test.go deleted file mode 100644 index d44150ce9..000000000 --- a/cli/cmd/ao/buildtags_test.go +++ /dev/null @@ -1,41 +0,0 @@ -package main - -import ( - "bytes" - "testing" -) - -// TestBuildtags_DefaultBuildIsSpine asserts that the default build (no archive -// build tags) reports the spine: archiveBuildTags is empty and `ao buildtags` -// prints the spine line. The flywheel/legacy variants are exercised by the -// build-tag verification script (scripts/verify-buildtags.sh), which actually -// compiles with -tags; here we lock the default-build contract. -func TestBuildtags_DefaultBuildIsSpine(t *testing.T) { - // This asserts the DEFAULT (spine) build contract. Under -tags flywheel/legacy - // the binary is intentionally a restored superset, so archiveBuildTags is - // non-empty there by design — that path is covered by verify-buildtags.sh. - if len(archiveBuildTags) != 0 { - t.Skipf("restored build (archive tags %v); spine contract is asserted only in the default build", archiveBuildTags) - } - - var out bytes.Buffer - buildtagsCmd.SetOut(&out) - t.Cleanup(func() { buildtagsCmd.SetOut(nil) }) - - if err := buildtagsCmd.RunE(buildtagsCmd, nil); err != nil { - t.Fatalf("buildtags RunE: %v", err) - } - got := out.String() - if !bytes.Contains([]byte(got), []byte("spine")) { - t.Fatalf("default build buildtags output = %q, want it to mention 'spine'", got) - } -} - -// TestBuildtags_HiddenFromDefaultSurface keeps the introspection command off the -// public command surface so adding the mechanism is no user-facing behavior -// change. -func TestBuildtags_HiddenFromDefaultSurface(t *testing.T) { - if !buildtagsCmd.Hidden { - t.Fatal("buildtags must stay Hidden so the default command surface is unchanged") - } -} diff --git a/cli/cmd/ao/ci.go b/cli/cmd/ao/ci.go index 48a4f0f58..0be402a82 100644 --- a/cli/cmd/ao/ci.go +++ b/cli/cmd/ao/ci.go @@ -31,7 +31,7 @@ BC2 CIStatusPort. Wraps 'gh run list --commit --json ...' behind productionCIStatus. Emits one JSON object (or empty for no run). Useful as a typed -replacement for inline gh shell-outs in /evolve Step 1.5 (healing- +replacement for duplicated inline gh shell-outs in repository scripts (healing- first classifier) and similar consumers. Examples: diff --git a/cli/cmd/ao/citation_verify.go b/cli/cmd/ao/citation_verify.go index 447d95fee..746bbc987 100644 --- a/cli/cmd/ao/citation_verify.go +++ b/cli/cmd/ao/citation_verify.go @@ -80,7 +80,7 @@ func verifyTextCitation(citation *Citation, cwd, needle, kind string) { if len(matches) >= 10 { return nil } - data, readErr := os.ReadFile(path) // #nosec G304 -- bounded repository walk + data, readErr := os.ReadFile(path) // #nosec G122 -- path comes from this bounded repository walk if readErr == nil && strings.Contains(string(data), needle) { if relative, relErr := filepath.Rel(cwd, path); relErr == nil { matches = append(matches, relative) diff --git a/cli/cmd/ao/cobra_conformance_test.go b/cli/cmd/ao/cobra_conformance_test.go index 52f628e5a..fc5ce3fc2 100644 --- a/cli/cmd/ao/cobra_conformance_test.go +++ b/cli/cmd/ao/cobra_conformance_test.go @@ -16,12 +16,7 @@ import ( var commandHeadingPattern = regexp.MustCompile("(?m)^#{3,6} `(ao(?: [^`]+)+)`$") func TestCobraConformance(t *testing.T) { - // COMMANDS.md documents the DEFAULT (spine) build. The flywheel/legacy build - // restores ADR-0012 archived commands, making the live tree a superset of the - // generated docs by design — skip when any archive build tag is active. - if len(archiveBuildTags) > 0 { - t.Skipf("spine-conformance test: archive build tags active (%v); the restored build is a documented superset", archiveBuildTags) - } + // COMMANDS.md documents the published CLI surface. removed := pruneToDefaultSpine(rootCmd) t.Cleanup(func() { restorePrunedCommands(rootCmd, removed) }) rootCmd.InitDefaultHelpCmd() diff --git a/cli/cmd/ao/corpus.go b/cli/cmd/ao/corpus.go deleted file mode 100644 index 594077d58..000000000 --- a/cli/cmd/ao/corpus.go +++ /dev/null @@ -1,79 +0,0 @@ -//go:build flywheel - -// practices: [wiki-knowledge-surface, ai-assisted-dev] -package main - -import ( - "encoding/json" - "fmt" - "os" - - "github.com/spf13/cobra" - - "github.com/boshu2/agentops/cli/internal/corpus" -) - -var corpusFitnessJSON bool - -// corpusCmd is the root for corpus-quality probes consumed by Dream and -// operators inspecting the local .agents/ corpus. -var corpusCmd = &cobra.Command{ - Use: "corpus", - Short: "Corpus-quality probes for Dream and the knowledge flywheel", - Long: `Commands that inspect the local .agents/ corpus quality. - -These commands are used by Dream's nightly MEASURE stage via in-process -calls and are also exposed for operators who want to inspect fitness -manually. They are deliberately NOT plumbed through the goals directive -subsystem — see docs/contracts/dream-run-contract.md for the delineation.`, -} - -// corpusFitnessCmd computes and prints the corpus FitnessVector for the -// current working directory's .agents/ corpus. -var corpusFitnessCmd = &cobra.Command{ - Use: "fitness", - Short: "Compute the corpus-quality fitness vector for the current .agents/", - RunE: runCorpusFitness, -} - -func init() { - corpusCmd.GroupID = "experimental" - rootCmd.AddCommand(corpusCmd) - corpusCmd.AddCommand(corpusFitnessCmd) - corpusFitnessCmd.Flags().BoolVar(&corpusFitnessJSON, "json", false, "Emit the fitness vector as JSON") -} - -// runCorpusFitness is the RunE entry point for `ao corpus fitness`. -func runCorpusFitness(cmd *cobra.Command, args []string) error { - cwd, err := os.Getwd() - if err != nil { - return fmt.Errorf("corpus fitness: determining cwd: %w", err) - } - vec, degraded, err := corpus.Compute(cwd) - if err != nil { - return fmt.Errorf("corpus fitness: %w", err) - } - if corpusFitnessJSON { - enc := json.NewEncoder(os.Stdout) - enc.SetIndent("", " ") - return enc.Encode(struct { - Fitness *corpus.FitnessVector `json:"fitness"` - Degraded []string `json:"degraded,omitempty"` - }{vec, degraded}) - } - fmt.Printf("Corpus fitness (computed %s):\n", vec.ComputedAt.Format("2006-01-02 15:04:05")) - fmt.Printf(" retrieval_precision: %.3f\n", vec.RetrievalPrecision) - fmt.Printf(" retrieval_recall: %.3f\n", vec.RetrievalRecall) - fmt.Printf(" maturity_provisional_or_above: %.3f\n", vec.MaturityProvisional) - fmt.Printf(" unresolved_findings: %d\n", vec.UnresolvedFindings) - fmt.Printf(" citation_coverage: %.3f\n", vec.CitationCoverage) - fmt.Printf(" inject_visibility: %.3f\n", vec.InjectVisibility) - fmt.Printf(" cross_rig_dedup_ratio: %.3f\n", vec.CrossRigDedupRatio) - if len(degraded) > 0 { - fmt.Println("\nDegraded:") - for _, d := range degraded { - fmt.Printf(" - %s\n", d) - } - } - return nil -} diff --git a/cli/cmd/ao/corpus_capture.go b/cli/cmd/ao/corpus_capture.go deleted file mode 100644 index 9393ca257..000000000 --- a/cli/cmd/ao/corpus_capture.go +++ /dev/null @@ -1,198 +0,0 @@ -//go:build flywheel - -// practices: [hexagonal-architecture, ddd-bounded-context] -package main - -import ( - "context" - "errors" - "fmt" - "io" - "os" - "path/filepath" - "strings" - - "github.com/spf13/cobra" - - "github.com/boshu2/agentops/cli/internal/adapters/corpus_fs" - "github.com/boshu2/agentops/cli/internal/ports" -) - -// corpusCaptureCmd exposes the corpus_fs.Writer real adapter via the -// CLI. Companion to cycle 146's `ao corpus inject` (which exposes -// CorpusReader). Together they close the BC1 reader+writer pair on -// the CLI side. -// -// Built using the cycle-147 cli-wiring template. -var corpusCaptureCmd = &cobra.Command{ - Use: "capture --path [--body ] [--body-file ] [--body-stdin] [--root ] [--meta k=v ...]", - Short: "Write a corpus artifact via BC1 CorpusWriterPort", - Long: `Write an artifact to a corpus root via the typed BC1 -CorpusWriterPort (corpus_fs.Writer real adapter). Default root -is .agents/learnings/ under the project root. - -The --path argument is the relative path WITHIN the root; absolute -paths and parent-traversal ('..') are rejected (port contract). - -Body source options (mutually exclusive): - --body inline text - --body-file read from file - --body-stdin read from stdin - -Metadata frontmatter is rendered if --meta key=value flags are -passed. If the body already starts with '---\n', the existing -frontmatter is preserved and --meta is ignored (port contract). - -Examples: - ao corpus capture --path notes/x.md --body "hello world" - ao corpus capture --path findings/y.md --body-file ./input.md - echo "body text" | ao corpus capture --path z.md --body-stdin - ao corpus capture --path n.md --body "..." --meta tag=evolve --meta date=2026-05-13`, - RunE: runCorpusCapture, -} - -type corpusCaptureOptions struct { - path string - root string - body string - bodyFile string - bodyStdin bool - meta []string - stdin io.Reader - writer io.Writer - captureFn func(ctx context.Context, opts corpusCaptureOptions, body []byte, meta map[string]string) (ports.CorpusWriteResult, error) -} - -func init() { - corpusCaptureCmd.Flags().String("path", "", "relative path within root (required)") - corpusCaptureCmd.Flags().String("root", "", "corpus root (default: .agents/learnings/)") - corpusCaptureCmd.Flags().String("body", "", "body text (mutually exclusive with --body-file and --body-stdin)") - corpusCaptureCmd.Flags().String("body-file", "", "read body from file") - corpusCaptureCmd.Flags().Bool("body-stdin", false, "read body from stdin") - corpusCaptureCmd.Flags().StringArray("meta", nil, "metadata key=value (repeatable)") - _ = corpusCaptureCmd.MarkFlagRequired("path") - corpusCmd.AddCommand(corpusCaptureCmd) -} - -func runCorpusCapture(cmd *cobra.Command, _ []string) error { - path, _ := cmd.Flags().GetString("path") - root, _ := cmd.Flags().GetString("root") - body, _ := cmd.Flags().GetString("body") - bodyFile, _ := cmd.Flags().GetString("body-file") - bodyStdin, _ := cmd.Flags().GetBool("body-stdin") - meta, _ := cmd.Flags().GetStringArray("meta") - return corpusCaptureRun(cmd.Context(), corpusCaptureOptions{ - path: path, - root: root, - body: body, - bodyFile: bodyFile, - bodyStdin: bodyStdin, - meta: meta, - stdin: cmd.InOrStdin(), - writer: cmd.OutOrStdout(), - }) -} - -func corpusCaptureRun(ctx context.Context, opts corpusCaptureOptions) error { - if opts.path == "" { - return errors.New("corpus capture: --path required") - } - body, err := corpusCaptureResolveBody(opts) - if err != nil { - return fmt.Errorf("corpus capture: %w", err) - } - meta, err := corpusCaptureParseMeta(opts.meta) - if err != nil { - return fmt.Errorf("corpus capture: %w", err) - } - fn := opts.captureFn - if fn == nil { - fn = corpusCaptureViaPort - } - res, err := fn(ctx, opts, body, meta) - if err != nil { - return fmt.Errorf("corpus capture: %w", err) - } - if opts.writer == nil { - opts.writer = os.Stdout - } - createdLabel := "updated" - if res.Created { - createdLabel = "created" - } - fmt.Fprintf(opts.writer, "%s %s\n", createdLabel, res.ResolvedPath) - return nil -} - -// corpusCaptureResolveBody picks one of --body, --body-file, -// --body-stdin. Exactly one source must be provided. -func corpusCaptureResolveBody(opts corpusCaptureOptions) ([]byte, error) { - sources := 0 - if opts.body != "" { - sources++ - } - if opts.bodyFile != "" { - sources++ - } - if opts.bodyStdin { - sources++ - } - if sources == 0 { - return nil, errors.New("body source required (--body, --body-file, or --body-stdin)") - } - if sources > 1 { - return nil, errors.New("only one body source allowed") - } - if opts.body != "" { - return []byte(opts.body), nil - } - if opts.bodyFile != "" { - data, err := os.ReadFile(opts.bodyFile) - if err != nil { - return nil, fmt.Errorf("read --body-file: %w", err) - } - return data, nil - } - // stdin - if opts.stdin == nil { - opts.stdin = os.Stdin - } - data, err := io.ReadAll(opts.stdin) - if err != nil { - return nil, fmt.Errorf("read stdin: %w", err) - } - return data, nil -} - -// corpusCaptureParseMeta turns []string{"k=v","a=b"} into a map. -func corpusCaptureParseMeta(meta []string) (map[string]string, error) { - if len(meta) == 0 { - return nil, nil - } - out := make(map[string]string, len(meta)) - for _, kv := range meta { - idx := strings.IndexByte(kv, '=') - if idx <= 0 { - return nil, fmt.Errorf("--meta %q: expected key=value", kv) - } - out[kv[:idx]] = kv[idx+1:] - } - return out, nil -} - -func corpusCaptureViaPort(ctx context.Context, opts corpusCaptureOptions, body []byte, meta map[string]string) (ports.CorpusWriteResult, error) { - root := opts.root - if root == "" { - cwd, err := resolveProjectDir() - if err != nil { - return ports.CorpusWriteResult{}, err - } - root = filepath.Join(cwd, ".agents", "learnings") - } - w := corpus_fs.NewWriter(root) - return w.Capture(ctx, ports.CorpusWriteRequest{ - Path: opts.path, - Body: body, - Metadata: meta, - }) -} diff --git a/cli/cmd/ao/corpus_capture_test.go b/cli/cmd/ao/corpus_capture_test.go deleted file mode 100644 index eac4dbd0e..000000000 --- a/cli/cmd/ao/corpus_capture_test.go +++ /dev/null @@ -1,176 +0,0 @@ -//go:build flywheel - -// practices: [tdd] -package main - -import ( - "bytes" - "context" - "errors" - "os" - "path/filepath" - "strings" - "testing" - - "github.com/boshu2/agentops/cli/internal/ports" -) - -func TestCorpusCapture_EmptyPathRejected(t *testing.T) { - err := corpusCaptureRun(context.Background(), corpusCaptureOptions{}) - if err == nil { - t.Fatal("expected error on empty path") - } - if !strings.Contains(err.Error(), "--path required") { - t.Fatalf("error not informative: %v", err) - } -} - -func TestCorpusCapture_BodyRequired(t *testing.T) { - err := corpusCaptureRun(context.Background(), corpusCaptureOptions{ - path: "x.md", - }) - if err == nil { - t.Fatal("expected error on no body source") - } - if !strings.Contains(err.Error(), "body source required") { - t.Fatalf("error not informative: %v", err) - } -} - -func TestCorpusCapture_MultipleBodySourcesRejected(t *testing.T) { - err := corpusCaptureRun(context.Background(), corpusCaptureOptions{ - path: "x.md", - body: "inline", - bodyFile: "f.md", - }) - if err == nil { - t.Fatal("expected error on multiple body sources") - } -} - -func TestCorpusCapture_InlineBodyToStub(t *testing.T) { - var gotPath string - var gotBody []byte - stub := func(_ context.Context, opts corpusCaptureOptions, body []byte, _ map[string]string) (ports.CorpusWriteResult, error) { - gotPath = opts.path - gotBody = body - return ports.CorpusWriteResult{ResolvedPath: "/r/" + opts.path, Created: true}, nil - } - var buf bytes.Buffer - err := corpusCaptureRun(context.Background(), corpusCaptureOptions{ - path: "notes/x.md", - body: "hello world", - writer: &buf, - captureFn: stub, - }) - if err != nil { - t.Fatal(err) - } - if gotPath != "notes/x.md" || string(gotBody) != "hello world" { - t.Fatalf("stub got %q / %q", gotPath, string(gotBody)) - } - if !strings.Contains(buf.String(), "created /r/notes/x.md") { - t.Fatalf("missing created confirmation: %q", buf.String()) - } -} - -func TestCorpusCapture_StdinBody(t *testing.T) { - stub := func(_ context.Context, _ corpusCaptureOptions, body []byte, _ map[string]string) (ports.CorpusWriteResult, error) { - if string(body) != "from stdin" { - t.Fatalf("body = %q, want 'from stdin'", body) - } - return ports.CorpusWriteResult{ResolvedPath: "ok", Created: false}, nil - } - var buf bytes.Buffer - err := corpusCaptureRun(context.Background(), corpusCaptureOptions{ - path: "x.md", - bodyStdin: true, - stdin: strings.NewReader("from stdin"), - writer: &buf, - captureFn: stub, - }) - if err != nil { - t.Fatal(err) - } - if !strings.Contains(buf.String(), "updated ok") { - t.Fatalf("missing updated label: %q", buf.String()) - } -} - -func TestCorpusCapture_MetaParsed(t *testing.T) { - var gotMeta map[string]string - stub := func(_ context.Context, _ corpusCaptureOptions, _ []byte, meta map[string]string) (ports.CorpusWriteResult, error) { - gotMeta = meta - return ports.CorpusWriteResult{ResolvedPath: "ok", Created: true}, nil - } - err := corpusCaptureRun(context.Background(), corpusCaptureOptions{ - path: "x.md", - body: "body", - meta: []string{"tag=evolve", "date=2026-05-13"}, - captureFn: stub, - }) - if err != nil { - t.Fatal(err) - } - if gotMeta["tag"] != "evolve" || gotMeta["date"] != "2026-05-13" { - t.Fatalf("meta wrong: %+v", gotMeta) - } -} - -func TestCorpusCapture_MalformedMetaRejected(t *testing.T) { - stub := func(_ context.Context, _ corpusCaptureOptions, _ []byte, _ map[string]string) (ports.CorpusWriteResult, error) { - return ports.CorpusWriteResult{}, nil - } - err := corpusCaptureRun(context.Background(), corpusCaptureOptions{ - path: "x.md", - body: "body", - meta: []string{"notakeyvalue"}, - captureFn: stub, - }) - if err == nil { - t.Fatal("expected error on malformed meta") - } - if !strings.Contains(err.Error(), "expected key=value") { - t.Fatalf("error not informative: %v", err) - } -} - -func TestCorpusCapture_BodyFileReadsContent(t *testing.T) { - dir := t.TempDir() - fp := filepath.Join(dir, "in.md") - _ = os.WriteFile(fp, []byte("file body content"), 0o644) - - var gotBody []byte - stub := func(_ context.Context, _ corpusCaptureOptions, body []byte, _ map[string]string) (ports.CorpusWriteResult, error) { - gotBody = body - return ports.CorpusWriteResult{ResolvedPath: "ok", Created: true}, nil - } - err := corpusCaptureRun(context.Background(), corpusCaptureOptions{ - path: "x.md", - bodyFile: fp, - captureFn: stub, - }) - if err != nil { - t.Fatal(err) - } - if string(gotBody) != "file body content" { - t.Fatalf("body = %q", gotBody) - } -} - -func TestCorpusCapture_StubErrorWrapped(t *testing.T) { - stub := func(_ context.Context, _ corpusCaptureOptions, _ []byte, _ map[string]string) (ports.CorpusWriteResult, error) { - return ports.CorpusWriteResult{}, errors.New("disk full") - } - err := corpusCaptureRun(context.Background(), corpusCaptureOptions{ - path: "x.md", - body: "body", - captureFn: stub, - }) - if err == nil { - t.Fatal("expected error") - } - if !strings.Contains(err.Error(), "corpus capture:") { - t.Fatalf("error not wrapped: %v", err) - } -} diff --git a/cli/cmd/ao/corpus_classify.go b/cli/cmd/ao/corpus_classify.go deleted file mode 100644 index 8da248265..000000000 --- a/cli/cmd/ao/corpus_classify.go +++ /dev/null @@ -1,92 +0,0 @@ -//go:build flywheel - -// practices: [fail-closed-safety, wiki-knowledge-surface] -package main - -import ( - "encoding/json" - "fmt" - "os" - - "github.com/spf13/cobra" - - "github.com/boshu2/agentops/cli/internal/corpus" -) - -var ( - corpusClassifyApply bool - corpusClassifyJSON bool -) - -// corpusClassifyCmd annotates learning records with the two promote-gate -// frontmatter defaults (sensitivity=unknown, publishable=false) — the S3 seam -// migration (epic ag-k7tq9). Dry-run by default; --apply writes. -var corpusClassifyCmd = &cobra.Command{ - Use: "classify ", - Short: "Annotate learning frontmatter with promote-gate defaults (sensitivity, publishable)", - Long: `Ensure every learning record under carries the two promote-gate -frontmatter fields with SAFE defaults: - - sensitivity: unknown # un-triaged ceiling; not a capture property - publishable: false # promotion allowlist flag; inclusion is earned - -This is the field-level seam migration from the corpus public/private council -verdict (.agents/council/2026-06-15-corpus-private-public-seam-verdict.md): the -corpus is lossless and private-by-default, and only sensitivity==public AND -publishable==true items may later be promoted to the public wiki (allowlist, -fail-closed — default excludes). - -It is malformed-tolerant: it operates on the frontmatter fence textually and -never parses the (possibly broken) YAML body, so a single junk record cannot -abort the run. An existing real decision (any sensitivity/publishable value) is -never overwritten. Meta docs (CORPUS-POLICY.md, README.md, …) are skipped. - -Dry-run by default — prints what WOULD change. Pass --apply to write. - - ao corpus classify .agents/learnings # dry run - ao corpus classify .agents/learnings --apply # write defaults`, - Args: cobra.ExactArgs(1), - RunE: runCorpusClassify, -} - -func init() { - corpusCmd.AddCommand(corpusClassifyCmd) - corpusClassifyCmd.Flags().BoolVar(&corpusClassifyApply, "apply", false, "Write the changes (default: dry run, report only)") - corpusClassifyCmd.Flags().BoolVar(&corpusClassifyJSON, "json", false, "Emit the report as JSON") -} - -// runCorpusClassify is the RunE entry point for `ao corpus classify`. -func runCorpusClassify(cmd *cobra.Command, args []string) error { - cmd.SilenceUsage = true - dir := args[0] - if info, err := os.Stat(dir); err != nil || !info.IsDir() { - return fmt.Errorf("corpus classify: %q is not a directory", dir) - } - - res, err := corpus.ClassifyDir(dir, corpusClassifyApply) - if err != nil { - return fmt.Errorf("corpus classify: %w", err) - } - - if corpusClassifyJSON { - enc := json.NewEncoder(os.Stdout) - enc.SetIndent("", " ") - return enc.Encode(res) - } - - mode := "dry run — no files written (pass --apply to write)" - if res.Applied { - mode = "applied" - } - fmt.Printf("Corpus classify (%s):\n", mode) - fmt.Printf(" scanned learnings: %d\n", res.Scanned) - fmt.Printf(" skipped meta docs: %d\n", res.Skipped) - fmt.Printf(" needing defaults: %d\n", res.Changed) - if res.Changed > 0 && !res.Applied { - fmt.Println("\nWould annotate:") - for _, f := range res.ChangedFiles { - fmt.Printf(" - %s\n", f) - } - } - return nil -} diff --git a/cli/cmd/ao/corpus_classify_test.go b/cli/cmd/ao/corpus_classify_test.go deleted file mode 100644 index bcd21a7e0..000000000 --- a/cli/cmd/ao/corpus_classify_test.go +++ /dev/null @@ -1,73 +0,0 @@ -//go:build flywheel - -package main - -import ( - "os" - "path/filepath" - "strings" - "testing" -) - -// TestRunCorpusClassify_DryRunReportsButDoesNotWrite drives the `ao corpus -// classify` command entry point in its default (dry-run) mode against a temp -// corpus and asserts it reports the right counts without touching disk. -func TestRunCorpusClassify_DryRunReportsButDoesNotWrite(t *testing.T) { - dir := t.TempDir() - orig := "---\ndate: 2026-06-14\n---\nbody\n" - p := filepath.Join(dir, "a.md") - if err := os.WriteFile(p, []byte(orig), 0o644); err != nil { - t.Fatal(err) - } - - corpusClassifyApply = false - out, err := captureStdout(t, func() error { - return runCorpusClassify(corpusClassifyCmd, []string{dir}) - }) - if err != nil { - t.Fatalf("runCorpusClassify: %v", err) - } - if !strings.Contains(out, "dry run") { - t.Errorf("expected dry-run banner, got:\n%s", out) - } - if !strings.Contains(out, "needing defaults: 1") { - t.Errorf("expected 1 record needing defaults, got:\n%s", out) - } - got, _ := os.ReadFile(p) - if string(got) != orig { - t.Errorf("dry run modified the file:\n%s", got) - } -} - -// TestRunCorpusClassify_ApplyWritesDefaults drives `ao corpus classify --apply` -// and asserts the safe defaults are written to the learning frontmatter. -func TestRunCorpusClassify_ApplyWritesDefaults(t *testing.T) { - dir := t.TempDir() - p := filepath.Join(dir, "a.md") - if err := os.WriteFile(p, []byte("---\ndate: 2026-06-14\n---\nbody\n"), 0o644); err != nil { - t.Fatal(err) - } - - corpusClassifyApply = true - t.Cleanup(func() { corpusClassifyApply = false }) - if err := runCorpusClassify(corpusClassifyCmd, []string{dir}); err != nil { - t.Fatalf("runCorpusClassify --apply: %v", err) - } - got, _ := os.ReadFile(p) - if !strings.Contains(string(got), "sensitivity: unknown") || !strings.Contains(string(got), "publishable: false") { - t.Errorf("apply did not write defaults:\n%s", got) - } -} - -// TestRunCorpusClassify_NonDirRejected asserts a non-directory argument is a -// clean error, not a panic. -func TestRunCorpusClassify_NonDirRejected(t *testing.T) { - corpusClassifyApply = false - err := runCorpusClassify(corpusClassifyCmd, []string{filepath.Join(t.TempDir(), "nope")}) - if err == nil { - t.Fatal("expected error for a non-directory argument") - } - if !strings.Contains(err.Error(), "not a directory") { - t.Errorf("unexpected error: %v", err) - } -} diff --git a/cli/cmd/ao/corpus_inject.go b/cli/cmd/ao/corpus_inject.go deleted file mode 100644 index 618c98609..000000000 --- a/cli/cmd/ao/corpus_inject.go +++ /dev/null @@ -1,121 +0,0 @@ -//go:build flywheel - -// practices: [hexagonal-architecture, ddd-bounded-context] -package main - -import ( - "context" - "encoding/json" - "fmt" - "io" - "os" - "path/filepath" - - "github.com/spf13/cobra" - - "github.com/boshu2/agentops/cli/internal/adapters/corpus_fs" - "github.com/boshu2/agentops/cli/internal/ports" - "github.com/boshu2/agentops/cli/internal/promptsafe" -) - -// corpusInjectCmd is slice 3 of soc-y5vh.5 (cycle 146). Reads from the -// .agents/learnings/ tree (and optionally additional roots) via the -// typed BC1 CorpusReaderPort (corpus_fs.Reader real adapter), -// emitting line-delimited JSON CorpusItem records ranked by query -// match. Completes the soc-y5vh.5 prerequisite — 3rd production -// adapter now reachable from the operator-facing CLI. -// -// Companion to: -// - ao loop history (cycle 144 — productionLoopReader) -// - ao ci latest (cycle 145 — productionCIStatus) -var corpusInjectCmd = &cobra.Command{ - Use: "inject [--query ] [--root ] [--limit N]", - Short: "Inject corpus matches via BC1 CorpusReaderPort (typed lookup)", - Long: `Read knowledge from a corpus root via the typed BC1 -CorpusReaderPort (corpus_fs.Reader real adapter). Default root is -.agents/learnings/ under the project root. Emits one JSON CorpusItem -per line, ranked by query match (title hit weighs 2, body hit weighs 1). - -Useful for /evolve Step 0 prior-failure injection (soc-y5vh.1 -consumer) and any caller that wants typed corpus retrieval without -re-implementing the file walk + ranker. - -Examples: - ao corpus inject --query "hexagonal" # default root - ao corpus inject --query "wire-up" --limit 3 # top 3 matches - ao corpus inject --root docs/learnings # specific root - ao corpus inject # all .md (empty query)`, - RunE: runCorpusInject, -} - -type corpusInjectOptions struct { - query string - root string - limit int - writer io.Writer - // injectFn lets tests substitute the port without writing temp files - injectFn func(ctx context.Context, opts corpusInjectOptions) ([]ports.CorpusItem, error) -} - -func init() { - corpusInjectCmd.Flags().String("query", "", "ranking query (empty = all items, score 0)") - corpusInjectCmd.Flags().String("root", "", "corpus root (default: .agents/learnings/)") - corpusInjectCmd.Flags().Int("limit", 10, "max items to emit (0 = all)") - corpusCmd.AddCommand(corpusInjectCmd) -} - -func runCorpusInject(cmd *cobra.Command, _ []string) error { - query, _ := cmd.Flags().GetString("query") - root, _ := cmd.Flags().GetString("root") - limit, _ := cmd.Flags().GetInt("limit") - return corpusInjectRun(cmd.Context(), corpusInjectOptions{ - query: query, - root: root, - limit: limit, - writer: cmd.OutOrStdout(), - }) -} - -func corpusInjectRun(ctx context.Context, opts corpusInjectOptions) error { - if opts.writer == nil { - opts.writer = os.Stdout - } - injectFn := opts.injectFn - if injectFn == nil { - injectFn = corpusInjectViaPort - } - items, err := injectFn(ctx, opts) - if err != nil { - return fmt.Errorf("corpus inject: %w", err) - } - enc := json.NewEncoder(opts.writer) - for _, item := range items { - // Title/Body are untrusted corpus content emitted for an agent to read; - // strip harness delimiter tags before encoding. item is a range copy, so - // mutating it does not touch the source slice. (age-gascity-port-slate-irye.1) - item.Title = promptsafe.SanitizeLeaf(item.Title) - item.Body = promptsafe.SanitizeLeaf(item.Body) - if err := enc.Encode(item); err != nil { - return fmt.Errorf("corpus inject encode: %w", err) - } - } - return nil -} - -// corpusInjectViaPort wires the corpus_fs.Reader real adapter to the -// caller's root. Default root resolves to /.agents/learnings. -func corpusInjectViaPort(ctx context.Context, opts corpusInjectOptions) ([]ports.CorpusItem, error) { - root := opts.root - if root == "" { - cwd, err := resolveProjectDir() - if err != nil { - return nil, err - } - root = filepath.Join(cwd, ".agents", "learnings") - } - reader := corpus_fs.NewReader(root) - return reader.Lookup(ctx, ports.LookupOptions{ - Query: opts.query, - Limit: opts.limit, - }) -} diff --git a/cli/cmd/ao/corpus_inject_test.go b/cli/cmd/ao/corpus_inject_test.go deleted file mode 100644 index 845e4fdbc..000000000 --- a/cli/cmd/ao/corpus_inject_test.go +++ /dev/null @@ -1,164 +0,0 @@ -//go:build flywheel - -// practices: [tdd] -package main - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "os" - "path/filepath" - "strings" - "testing" - - "github.com/boshu2/agentops/cli/internal/ports" -) - -func TestCorpusInject_StubReturnsItems(t *testing.T) { - stub := func(_ context.Context, _ corpusInjectOptions) ([]ports.CorpusItem, error) { - return []ports.CorpusItem{ - {Path: "/p/a.md", Title: "alpha", Body: "body-a", Score: 2.0}, - {Path: "/p/b.md", Title: "beta", Body: "body-b", Score: 1.0}, - }, nil - } - var buf bytes.Buffer - err := corpusInjectRun(context.Background(), corpusInjectOptions{ - query: "x", - writer: &buf, - injectFn: stub, - }) - if err != nil { - t.Fatal(err) - } - lines := strings.Split(strings.TrimSpace(buf.String()), "\n") - if len(lines) != 2 { - t.Fatalf("len = %d, want 2", len(lines)) - } - if !strings.Contains(lines[0], `"Title":"alpha"`) { - t.Fatalf("first line missing alpha: %s", lines[0]) - } -} - -func TestCorpusInject_EmptyResultsEmitsZeroLines(t *testing.T) { - stub := func(_ context.Context, _ corpusInjectOptions) ([]ports.CorpusItem, error) { - return []ports.CorpusItem{}, nil - } - var buf bytes.Buffer - err := corpusInjectRun(context.Background(), corpusInjectOptions{ - query: "nomatch", - writer: &buf, - injectFn: stub, - }) - if err != nil { - t.Fatal(err) - } - if buf.Len() != 0 { - t.Fatalf("empty result should emit 0 bytes, got %q", buf.String()) - } -} - -func TestCorpusInject_ErrorPropagates(t *testing.T) { - stub := func(_ context.Context, _ corpusInjectOptions) ([]ports.CorpusItem, error) { - return nil, errors.New("corpus root unavailable") - } - err := corpusInjectRun(context.Background(), corpusInjectOptions{ - writer: nil, // also exercise nil-writer default - injectFn: stub, - }) - if err == nil { - t.Fatal("expected error") - } - if !strings.Contains(err.Error(), "corpus inject:") { - t.Fatalf("error not wrapped: %v", err) - } -} - -func TestCorpusInject_LiveRootWalksTree(t *testing.T) { - dir := t.TempDir() - if err := os.MkdirAll(dir, 0o755); err != nil { - t.Fatal(err) - } - a := filepath.Join(dir, "a.md") - b := filepath.Join(dir, "b.md") - _ = os.WriteFile(a, []byte("# alpha\n\nhexagonal pattern body"), 0o644) - _ = os.WriteFile(b, []byte("# beta\n\nunrelated"), 0o644) - - var buf bytes.Buffer - err := corpusInjectRun(context.Background(), corpusInjectOptions{ - query: "hexagonal", - root: dir, - limit: 5, - writer: &buf, - }) - if err != nil { - t.Fatal(err) - } - lines := strings.Split(strings.TrimSpace(buf.String()), "\n") - if len(lines) != 1 { - t.Fatalf("len = %d, want 1 (only alpha matches)", len(lines)) - } - if !strings.Contains(lines[0], `"Title":"alpha"`) { - t.Fatalf("expected alpha as match, got: %s", lines[0]) - } -} - -// TestCorpusInject_SanitizesHarnessTags is the L2 guard for the promptsafe -// wiring (age-gascity-port-slate-irye.1): splice-vector harness tags in an -// emitted CorpusItem's Title/Body must be stripped before encoding. We decode -// the emitted JSON back so the assertion sees the value an agent would parse, -// not JSON's incidental < escaping. -func TestCorpusInject_SanitizesHarnessTags(t *testing.T) { - stub := func(_ context.Context, _ corpusInjectOptions) ([]ports.CorpusItem, error) { - return []ports.CorpusItem{{ - Path: "/p/evil.md", - Title: "Tnder>X", - Body: "bINJECTe", - Score: 1.0, - }}, nil - } - var buf bytes.Buffer - if err := corpusInjectRun(context.Background(), corpusInjectOptions{ - writer: &buf, - injectFn: stub, - }); err != nil { - t.Fatal(err) - } - var item ports.CorpusItem - if err := json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &item); err != nil { - t.Fatalf("decode emitted line: %v (raw %q)", err, buf.String()) - } - for _, v := range []string{item.Title, item.Body} { - if strings.Contains(strings.ToLower(v), "system-reminder>") { - t.Fatalf("decoded value %q still carries a reconstructable harness tag", v) - } - } - if item.Title != "TX" { - t.Fatalf("Title = %q, want %q", item.Title, "TX") - } - if item.Body != "bINJECTe" { - t.Fatalf("Body = %q, want %q", item.Body, "bINJECTe") - } -} - -func TestCorpusInject_RespectsLimit(t *testing.T) { - dir := t.TempDir() - for _, name := range []string{"a.md", "b.md", "c.md", "d.md"} { - _ = os.WriteFile(filepath.Join(dir, name), []byte("# t\n\nfoo"), 0o644) - } - var buf bytes.Buffer - err := corpusInjectRun(context.Background(), corpusInjectOptions{ - query: "foo", - root: dir, - limit: 2, - writer: &buf, - }) - if err != nil { - t.Fatal(err) - } - lines := strings.Split(strings.TrimSpace(buf.String()), "\n") - if len(lines) != 2 { - t.Fatalf("limit not honored: %d lines", len(lines)) - } -} diff --git a/cli/cmd/ao/corpus_scan.go b/cli/cmd/ao/corpus_scan.go deleted file mode 100644 index a670131a4..000000000 --- a/cli/cmd/ao/corpus_scan.go +++ /dev/null @@ -1,124 +0,0 @@ -//go:build flywheel - -// practices: [fail-closed-safety, wiki-knowledge-surface] -package main - -import ( - "encoding/json" - "fmt" - "os" - - "github.com/spf13/cobra" - - "github.com/boshu2/agentops/cli/internal/corpusscan" -) - -var corpusScanJSON bool - -// corpusScanExitError (the typed exit-code error) lives in the untagged -// corpus_scan_error.go so root.go's spine Execute() switch can type-assert it -// after this command archives behind //go:build flywheel (age-nzwo). - -const ( - corpusScanClean = 0 - corpusScanLeak = 1 - corpusScanInternal = 2 -) - -// corpusScanCmd is the layer-3 deny/PII leak detector for the corpus -// public/publish pipeline. It scans RENDERED text for the canonical marker -// registry (cli/internal/corpusscan) and FAILS CLOSED on any hit. It never -// modifies a file — detect only, never redact. -var corpusScanCmd = &cobra.Command{ - Use: "scan ", - Short: "Fail-closed deny/PII leak scan of rendered corpus output (detect only, never redact)", - Long: `Scan a file or directory of RENDERED public text (markdown/json/txt/html) -for fleet, client, peer-agent, private-namespace, mythology, brand, and -landmine leak markers before publication. - -FAIL CLOSED: any single marker hit — OR any file that cannot be read — exits -nonzero. A fully clean tree exits 0. The scanner only DETECTS; it never -modifies, redacts, or rewrites a file. - -The marker registry is the single canonical set in cli/internal/corpusscan, -shared with the CI publish gate so detection can never drift between them. - -Exit codes: - 0 clean (publishable) - 1 leak detected or a file could not be read (FAIL CLOSED) - 2 internal error - - ao corpus scan public/wiki/ - ao corpus scan public/wiki/index.md --json`, - Args: cobra.ExactArgs(1), - RunE: runCorpusScan, -} - -func init() { - corpusCmd.AddCommand(corpusScanCmd) - corpusScanCmd.Flags().BoolVar(&corpusScanJSON, "json", false, "Emit the scan report as JSON (per-file hits)") -} - -// runCorpusScan is the RunE entry point for `ao corpus scan`. -func runCorpusScan(cmd *cobra.Command, args []string) error { - cmd.SilenceErrors = true - path := args[0] - - rep, err := corpusscan.Scan(path) - if err != nil { - fmt.Fprintln(os.Stderr, "ao corpus scan: "+err.Error()) - return &corpusScanExitError{code: corpusScanInternal, msg: err.Error()} - } - - if corpusScanJSON { - enc := json.NewEncoder(os.Stdout) - enc.SetIndent("", " ") - _ = enc.Encode(struct { - Root string `json:"root"` - Clean bool `json:"clean"` - HitCount int `json:"hit_count"` - ErrorCount int `json:"error_count"` - MarkerSet int `json:"marker_set_size"` - Files []corpusscan.FileResult `json:"files"` - }{ - Root: rep.Root, - Clean: rep.Clean(), - HitCount: rep.HitCount(), - ErrorCount: rep.ErrorCount(), - MarkerSet: corpusscan.MarkerCount(), - Files: rep.Files, - }) - } else { - printCorpusScanHuman(rep) - } - - if !rep.Clean() { - return &corpusScanExitError{ - code: corpusScanLeak, - msg: fmt.Sprintf("corpus scan FAILED CLOSED: %d hit(s), %d unreadable file(s)", rep.HitCount(), rep.ErrorCount()), - } - } - return nil -} - -// printCorpusScanHuman renders each hit by name, class, file, and line. -func printCorpusScanHuman(rep corpusscan.Report) { - if rep.Clean() { - fmt.Printf("CLEAN: no leak markers in %s (registry: %d markers)\n", rep.Root, corpusscan.MarkerCount()) - return - } - fmt.Printf("FAIL CLOSED: leak markers detected in %s\n", rep.Root) - for _, f := range rep.Files { - if f.Clean() { - continue - } - if f.Err != "" { - fmt.Printf(" %s: UNREADABLE (treated as unsafe): %s\n", f.Path, f.Err) - continue - } - for _, h := range f.Hits { - fmt.Printf(" %s:%d [%s/%s] %q\n", f.Path, h.Line, h.Class, h.Marker, h.Match) - } - } - fmt.Printf("\n%d hit(s), %d unreadable file(s). NOT publishable.\n", rep.HitCount(), rep.ErrorCount()) -} diff --git a/cli/cmd/ao/corpus_scan_error.go b/cli/cmd/ao/corpus_scan_error.go deleted file mode 100644 index ecdd5ee11..000000000 --- a/cli/cmd/ao/corpus_scan_error.go +++ /dev/null @@ -1,22 +0,0 @@ -package main - -// corpusScanExitError carries the fail-closed exit code out of RunE so that a -// detected leak (or read failure) maps to a nonzero process exit without cobra -// printing its own error noise. Exit semantics: -// -// 0 clean — no markers, no read errors (publishable) -// 1 leak detected OR a file could not be read (FAIL CLOSED) -// 2 internal error invoking the scan -// -// This typed error lives in an UNTAGGED file (extracted from corpus_scan.go, -// which is archived behind //go:build flywheel per ADR-0012 / age-nzwo) because -// the spine's Execute() error switch in root.go type-asserts *corpusScanExitError -// to map the verdict to a process exit code. Keeping the type spine-resident lets -// the `ao corpus scan` command archive without breaking the default build. -type corpusScanExitError struct { - code int - msg string -} - -func (e *corpusScanExitError) Error() string { return e.msg } -func (e *corpusScanExitError) ExitCode() int { return e.code } diff --git a/cli/cmd/ao/corpus_scan_test.go b/cli/cmd/ao/corpus_scan_test.go deleted file mode 100644 index 4f497f8a3..000000000 --- a/cli/cmd/ao/corpus_scan_test.go +++ /dev/null @@ -1,67 +0,0 @@ -//go:build flywheel - -// practices: [fail-closed-safety, test-pyramid] -package main - -import ( - "errors" - "os" - "path/filepath" - "testing" -) - -// TestRunCorpusScan_LeakFailsClosed is the L2 over the command entry point: a -// fixture with a fleet marker must return a corpusScanExitError with exit code -// 1 (FAIL CLOSED). -func TestRunCorpusScan_LeakFailsClosed(t *testing.T) { - dir := t.TempDir() - p := filepath.Join(dir, "leak.md") - if err := os.WriteFile(p, []byte("Deploy to bushido over tailscale.\n"), 0o644); err != nil { - t.Fatal(err) - } - - corpusScanJSON = false - err := runCorpusScan(corpusScanCmd, []string{p}) - if err == nil { - t.Fatal("expected a fail-closed error for a leak, got nil") - } - var scanErr *corpusScanExitError - if !errors.As(err, &scanErr) { - t.Fatalf("expected *corpusScanExitError, got %T: %v", err, err) - } - if scanErr.ExitCode() != corpusScanLeak { - t.Fatalf("expected exit code %d (leak), got %d", corpusScanLeak, scanErr.ExitCode()) - } -} - -// TestRunCorpusScan_CleanPasses asserts a clean generic learning returns nil -// (exit 0). -func TestRunCorpusScan_CleanPasses(t *testing.T) { - dir := t.TempDir() - p := filepath.Join(dir, "clean.md") - if err := os.WriteFile(p, []byte("# Lesson\n\nGive agents durable context; require evidence.\n"), 0o644); err != nil { - t.Fatal(err) - } - - corpusScanJSON = false - if err := runCorpusScan(corpusScanCmd, []string{p}); err != nil { - t.Fatalf("expected clean (nil error), got %v", err) - } -} - -// TestRunCorpusScan_MissingPathInternal asserts a missing path maps to the -// internal exit code (2), distinct from a leak (1). -func TestRunCorpusScan_MissingPathInternal(t *testing.T) { - corpusScanJSON = false - err := runCorpusScan(corpusScanCmd, []string{filepath.Join(t.TempDir(), "nope")}) - if err == nil { - t.Fatal("expected an internal error for a missing path") - } - var scanErr *corpusScanExitError - if !errors.As(err, &scanErr) { - t.Fatalf("expected *corpusScanExitError, got %T", err) - } - if scanErr.ExitCode() != corpusScanInternal { - t.Fatalf("expected exit code %d (internal), got %d", corpusScanInternal, scanErr.ExitCode()) - } -} diff --git a/cli/cmd/ao/corpus_snapshot.go b/cli/cmd/ao/corpus_snapshot.go deleted file mode 100644 index c5cae0aca..000000000 --- a/cli/cmd/ao/corpus_snapshot.go +++ /dev/null @@ -1,486 +0,0 @@ -//go:build flywheel - -// practices: [wiki-knowledge-surface, resilience-patterns, ai-assisted-dev] -package main - -import ( - "archive/tar" - "compress/gzip" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "fmt" - "io" - "os" - "path/filepath" - "sort" - "strings" - "time" - - "github.com/spf13/cobra" -) - -const ( - defaultSnapshotDirEnv = "AGENTOPS_CORPUS_SNAPSHOT_DIR" - defaultSnapshotSubdir = ".agentops/corpus-snapshots" - corpusSourceDir = ".agents" -) - -var maxSnapshotExtractBytes int64 = 1 << 30 - -var ( - snapshotOutputDir string - snapshotJSON bool - restoreFrom string - restoreLatest bool - restoreInto string - restoreOverwrite bool - restoreJSON bool -) - -var corpusSnapshotCmd = &cobra.Command{ - Use: "snapshot", - Short: "Write a tar.gz snapshot of the local .agents/ corpus to a durable path", - Long: `Writes the entire .agents/ tree as a tar.gz to a durable directory outside the repo, -along with a sidecar manifest containing file count, total bytes, sha256, and ISO-8601 timestamp. - -Default output dir: $AGENTOPS_CORPUS_SNAPSHOT_DIR, falling back to ~/.agentops/corpus-snapshots/. -Snapshot filename: -.tar.gz. - -Intent: routine cleanup periodically wipes .agents/. A snapshot is the durable copy that -ao corpus restore can rehydrate from.`, - RunE: runCorpusSnapshot, -} - -var corpusRestoreCmd = &cobra.Command{ - Use: "restore", - Short: "Restore .agents/ from a durable snapshot", - Long: `Untars a snapshot produced by ao corpus snapshot. By default refuses to overwrite an -existing .agents/ directory; use --overwrite to replace it (the existing tree is moved to -.agents.bak-/ first as a safety net, then removed only after a successful extract). - -Snapshot source resolution: - --from explicit tarball path - --latest newest tarball in the snapshot dir - (neither) errors out (no ambiguous default)`, - RunE: runCorpusRestore, -} - -func init() { - corpusCmd.AddCommand(corpusSnapshotCmd) - corpusCmd.AddCommand(corpusRestoreCmd) - - corpusSnapshotCmd.Flags().StringVar(&snapshotOutputDir, "output-dir", "", "Override snapshot dir (default: $AGENTOPS_CORPUS_SNAPSHOT_DIR or ~/.agentops/corpus-snapshots)") - corpusSnapshotCmd.Flags().BoolVar(&snapshotJSON, "json", false, "Emit the manifest as JSON to stdout") - - corpusRestoreCmd.Flags().StringVar(&restoreFrom, "from", "", "Explicit snapshot tarball path") - corpusRestoreCmd.Flags().BoolVar(&restoreLatest, "latest", false, "Pick the newest tarball in the snapshot dir") - corpusRestoreCmd.Flags().StringVar(&restoreInto, "into", corpusSourceDir, "Destination directory (default: .agents)") - corpusRestoreCmd.Flags().BoolVar(&restoreOverwrite, "overwrite", false, "Replace an existing destination directory (with .bak rescue)") - corpusRestoreCmd.Flags().BoolVar(&restoreJSON, "json", false, "Emit the result as JSON to stdout") -} - -type snapshotManifest struct { - SnapshotPath string `json:"snapshot_path"` - Repo string `json:"repo"` - Source string `json:"source"` - FileCount int `json:"file_count"` - TotalBytes int64 `json:"total_bytes"` - SHA256 string `json:"sha256"` - CreatedAt time.Time `json:"created_at"` -} - -// createCorpusSnapshot writes a tar.gz of cwd/.agents to the resolved snapshot -// dir (override > $AGENTOPS_CORPUS_SNAPSHOT_DIR > ~/.agentops/corpus-snapshots) -// plus a sidecar manifest, and returns the manifest and manifest path. It is the -// shared engine behind both `ao corpus snapshot` and the auto-snapshot that -// destructive corpus ops (e.g. `ao defrag`) take before deleting anything. -func createCorpusSnapshot(cwd, outDirOverride string) (snapshotManifest, string, error) { - srcAbs := filepath.Join(cwd, corpusSourceDir) - info, err := os.Stat(srcAbs) - if err != nil { - return snapshotManifest{}, "", fmt.Errorf("corpus snapshot: %s not found at %s: %w", corpusSourceDir, cwd, err) - } - if !info.IsDir() { - return snapshotManifest{}, "", fmt.Errorf("corpus snapshot: %s is not a directory", srcAbs) - } - - outDir, err := resolveSnapshotDir(outDirOverride) - if err != nil { - return snapshotManifest{}, "", fmt.Errorf("corpus snapshot: resolving output dir: %w", err) - } - if err := os.MkdirAll(outDir, 0o755); err != nil { - return snapshotManifest{}, "", fmt.Errorf("corpus snapshot: mkdir %s: %w", outDir, err) - } - - now := time.Now().UTC() - repoName := filepath.Base(cwd) - stamp := now.Format("20060102T150405Z") - snapPath := filepath.Join(outDir, fmt.Sprintf("%s-%s.tar.gz", repoName, stamp)) - tmpPath := snapPath + ".tmp" - - count, total, sum, err := writeSnapshot(tmpPath, srcAbs) - if err != nil { - _ = os.Remove(tmpPath) - return snapshotManifest{}, "", fmt.Errorf("corpus snapshot: writing tarball: %w", err) - } - if err := os.Rename(tmpPath, snapPath); err != nil { - _ = os.Remove(tmpPath) - return snapshotManifest{}, "", fmt.Errorf("corpus snapshot: rename: %w", err) - } - - manifest := snapshotManifest{ - SnapshotPath: snapPath, - Repo: repoName, - Source: srcAbs, - FileCount: count, - TotalBytes: total, - SHA256: sum, - CreatedAt: now, - } - manifestPath := snapPath + ".manifest.json" - if err := writeCorpusManifestFile(manifestPath, manifest); err != nil { - return snapshotManifest{}, "", fmt.Errorf("corpus snapshot: writing manifest: %w", err) - } - return manifest, manifestPath, nil -} - -func runCorpusSnapshot(cmd *cobra.Command, args []string) error { - cwd, err := os.Getwd() - if err != nil { - return fmt.Errorf("corpus snapshot: cwd: %w", err) - } - - manifest, manifestPath, err := createCorpusSnapshot(cwd, snapshotOutputDir) - if err != nil { - return err - } - - if snapshotJSON { - return json.NewEncoder(os.Stdout).Encode(manifest) - } - fmt.Printf("Corpus snapshot written:\n") - fmt.Printf(" path: %s\n", manifest.SnapshotPath) - fmt.Printf(" manifest: %s\n", manifestPath) - fmt.Printf(" files: %d\n", manifest.FileCount) - fmt.Printf(" bytes: %d\n", manifest.TotalBytes) - fmt.Printf(" sha256: %s\n", manifest.SHA256) - fmt.Printf(" created_at: %s\n", manifest.CreatedAt.Format(time.RFC3339)) - return nil -} - -type restoreResult struct { - From string `json:"from"` - Into string `json:"into"` - FileCount int `json:"file_count"` - TotalBytes int64 `json:"total_bytes"` - RestoredAt time.Time `json:"restored_at"` - BackupPath string `json:"backup_path,omitempty"` -} - -func runCorpusRestore(cmd *cobra.Command, args []string) error { - source := restoreFrom - if source == "" && restoreLatest { - dir, err := resolveSnapshotDir(snapshotOutputDir) - if err != nil { - return fmt.Errorf("corpus restore: resolving snapshot dir: %w", err) - } - latest, err := findLatestSnapshot(dir) - if err != nil { - return fmt.Errorf("corpus restore: %w", err) - } - source = latest - } - if source == "" { - return fmt.Errorf("corpus restore: provide --from or --latest") - } - - dest := restoreInto - if dest == "" { - dest = corpusSourceDir - } - backupPath := "" - if _, err := os.Stat(dest); err == nil { - if !restoreOverwrite { - return fmt.Errorf("corpus restore: %s already exists; pass --overwrite to replace it", dest) - } - backupPath = fmt.Sprintf("%s.bak-%s", strings.TrimRight(dest, string(os.PathSeparator)), time.Now().UTC().Format("20060102T150405Z")) - if err := os.Rename(dest, backupPath); err != nil { - return fmt.Errorf("corpus restore: backing up existing dest %s: %w", dest, err) - } - } - - count, total, err := extractSnapshot(source, dest) - if err != nil { - if backupPath != "" { - _ = os.RemoveAll(dest) - _ = os.Rename(backupPath, dest) - } - return fmt.Errorf("corpus restore: extracting: %w", err) - } - - result := restoreResult{ - From: source, - Into: dest, - FileCount: count, - TotalBytes: total, - RestoredAt: time.Now().UTC(), - BackupPath: backupPath, - } - if restoreJSON { - return json.NewEncoder(os.Stdout).Encode(result) - } - fmt.Printf("Corpus restored:\n") - fmt.Printf(" from: %s\n", result.From) - fmt.Printf(" into: %s\n", result.Into) - fmt.Printf(" files: %d\n", result.FileCount) - fmt.Printf(" bytes: %d\n", result.TotalBytes) - fmt.Printf(" restored_at: %s\n", result.RestoredAt.Format(time.RFC3339)) - if result.BackupPath != "" { - fmt.Printf(" prior tree: %s (safe to remove once verified)\n", result.BackupPath) - } - return nil -} - -func resolveSnapshotDir(override string) (string, error) { - if override != "" { - return expandHome(override) - } - if env := os.Getenv(defaultSnapshotDirEnv); env != "" { - return expandHome(env) - } - home, err := os.UserHomeDir() - if err != nil { - return "", err - } - return filepath.Join(home, defaultSnapshotSubdir), nil -} - -func expandHome(p string) (string, error) { - if !strings.HasPrefix(p, "~") { - return p, nil - } - home, err := os.UserHomeDir() - if err != nil { - return "", err - } - return filepath.Join(home, strings.TrimPrefix(p, "~")), nil -} - -func writeSnapshot(tarPath, srcRoot string) (int, int64, string, error) { - f, err := os.Create(tarPath) - if err != nil { - return 0, 0, "", err - } - defer func() { _ = f.Close() }() - - hash := sha256.New() - mw := io.MultiWriter(f, hash) - gz := gzip.NewWriter(mw) - tw := tar.NewWriter(gz) - - var count int - var total int64 - srcRoot = filepath.Clean(srcRoot) - parent := filepath.Dir(srcRoot) - root, err := os.OpenRoot(srcRoot) - if err != nil { - return 0, 0, "", err - } - defer func() { _ = root.Close() }() - walkErr := filepath.Walk(srcRoot, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - rel, rerr := filepath.Rel(parent, path) - if rerr != nil { - return rerr - } - rootRel, rerr := filepath.Rel(srcRoot, path) - if rerr != nil { - return rerr - } - header, herr := tar.FileInfoHeader(info, "") - if herr != nil { - return herr - } - header.Name = rel - if werr := tw.WriteHeader(header); werr != nil { - return werr - } - if !info.Mode().IsRegular() { - return nil - } - fh, oerr := root.Open(rootRel) - if oerr != nil { - return oerr - } - openedInfo, serr := fh.Stat() - if serr != nil { - _ = fh.Close() - return serr - } - if !openedInfo.Mode().IsRegular() { - _ = fh.Close() - return fmt.Errorf("snapshot source changed while reading: %s", rel) - } - n, cperr := io.CopyN(tw, fh, info.Size()) - _ = fh.Close() - if cperr != nil { - return cperr - } - count++ - total += n - return nil - }) - if walkErr != nil { - return 0, 0, "", walkErr - } - if err := tw.Close(); err != nil { - return 0, 0, "", err - } - if err := gz.Close(); err != nil { - return 0, 0, "", err - } - if err := f.Sync(); err != nil { - return 0, 0, "", err - } - return count, total, hex.EncodeToString(hash.Sum(nil)), nil -} - -func extractSnapshot(tarPath, destParent string) (int, int64, error) { - f, err := os.Open(tarPath) - if err != nil { - return 0, 0, err - } - defer func() { _ = f.Close() }() - gz, err := gzip.NewReader(f) - if err != nil { - return 0, 0, err - } - defer func() { _ = gz.Close() }() - tr := tar.NewReader(gz) - - if err := os.MkdirAll(filepath.Dir(destParent), 0o755); err != nil { - return 0, 0, err - } - parentDir := filepath.Dir(destParent) - if parentDir == "" { - parentDir = "." - } - - // Canonical containment root: the realpath'd directory every extracted entry - // MUST stay at or below. Computed once so per-entry containment is asserted - // against symlink-resolved ground truth, not string prefixes. - rootReal := realpathOrSelf(parentDir) - - var count int - var total int64 - for { - hdr, herr := tr.Next() - if herr == io.EOF { - break - } - if herr != nil { - return 0, 0, herr - } - clean := filepath.Clean(hdr.Name) - // (a) Reject absolute-path entries LOUDLY. filepath.Join(parentDir, "/etc/x") - // contains such an entry only by accident (Join treats an absolute second arg - // as relative); make that explicit so a future refactor of Join can't silently - // reintroduce an absolute-write escape. - if filepath.IsAbs(clean) { - return 0, 0, fmt.Errorf("refusing absolute-path entry: %q", hdr.Name) - } - // Defend against path traversal (string-level first cut). - if strings.HasPrefix(clean, "..") || strings.Contains(clean, string(os.PathSeparator)+"..") { - return 0, 0, fmt.Errorf("refusing path traversal entry: %q", hdr.Name) - } - target := filepath.Join(parentDir, clean) - // (b) Canonical containment assertion. - // This is the windshield — it catches an escape that survived the string cut - // (e.g. a symlinked parentDir, or a clean name that still resolves outside). - if !pathInside(realpathOrSelf(target), rootReal) { - return 0, 0, fmt.Errorf("refusing entry escaping extraction root: %q", hdr.Name) - } - switch hdr.Typeflag { - case tar.TypeDir: - if err := os.MkdirAll(target, os.FileMode(hdr.Mode)&0o777); err != nil { - return 0, 0, err - } - case tar.TypeReg: - if hdr.Size < 0 || total+hdr.Size > maxSnapshotExtractBytes { - return 0, 0, fmt.Errorf("snapshot extract exceeds byte limit: %d", maxSnapshotExtractBytes) - } - if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil { - return 0, 0, err - } - out, oerr := os.OpenFile(target, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(hdr.Mode)&0o777) - if oerr != nil { - return 0, 0, oerr - } - n, cperr := io.CopyN(out, tr, hdr.Size) - cerr := out.Close() - if cperr != nil { - return 0, 0, cperr - } - if cerr != nil { - return 0, 0, cerr - } - count++ - total += n - default: - // (c) Fail closed on any other typeflag (symlink, hardlink, char/block - // device, fifo, etc.) instead of silently skipping it. The production - // snapshot writer (writeSnapshot above) emits only TypeDir and TypeReg for - // corpus content: filepath.Walk visits the .agents/ tree without following - // symlinks and tar.FileInfoHeader classifies each entry, and .agents/ is a - // knowledge corpus of plain files+dirs (verified: 0 symlinks). A symlink in - // the source WOULD serialize as a broken TypeSymlink (empty Linkname) — not - // benign content to restore — so refusing it here rejects both the - // smuggled-symlink attack and a malformed archive, rather than dropping it - // invisibly. - return 0, 0, fmt.Errorf("refusing unsupported tar entry %q with typeflag %q(%d)", hdr.Name, string(hdr.Typeflag), hdr.Typeflag) - } - } - return count, total, nil -} - -func findLatestSnapshot(dir string) (string, error) { - entries, err := os.ReadDir(dir) - if err != nil { - return "", fmt.Errorf("reading %s: %w", dir, err) - } - type pair struct { - path string - t time.Time - } - var pairs []pair - for _, e := range entries { - name := e.Name() - if !strings.HasSuffix(name, ".tar.gz") { - continue - } - info, ierr := e.Info() - if ierr != nil { - continue - } - pairs = append(pairs, pair{filepath.Join(dir, name), info.ModTime()}) - } - if len(pairs) == 0 { - return "", fmt.Errorf("no *.tar.gz snapshots found under %s", dir) - } - sort.Slice(pairs, func(i, j int) bool { return pairs[i].t.After(pairs[j].t) }) - return pairs[0].path, nil -} - -func writeCorpusManifestFile(path string, v any) error { - f, err := os.Create(path) - if err != nil { - return err - } - defer func() { _ = f.Close() }() - enc := json.NewEncoder(f) - enc.SetIndent("", " ") - return enc.Encode(v) -} diff --git a/cli/cmd/ao/corpus_snapshot_containment_test.go b/cli/cmd/ao/corpus_snapshot_containment_test.go deleted file mode 100644 index 2d5d73788..000000000 --- a/cli/cmd/ao/corpus_snapshot_containment_test.go +++ /dev/null @@ -1,294 +0,0 @@ -//go:build flywheel - -// practices: [wiki-knowledge-surface, resilience-patterns, ai-assisted-dev] -package main - -import ( - "archive/tar" - "compress/gzip" - "os" - "path/filepath" - "strings" - "testing" -) - -// tarEntry is one crafted archive member. Body is only meaningful for TypeReg. -type tarEntry struct { - name string - typeflag byte - linkname string - body string - mode int64 -} - -// buildCraftedTarGz writes a tar.gz containing exactly the given entries and -// returns its path. It bypasses the production writer on purpose so the extractor -// can be exercised against hostile/unusual archives the writer would never emit. -func buildCraftedTarGz(t *testing.T, dir string, entries []tarEntry) string { - t.Helper() - tarPath := filepath.Join(dir, "crafted.tar.gz") - f, err := os.Create(tarPath) - if err != nil { - t.Fatalf("create crafted tar: %v", err) - } - gz := gzip.NewWriter(f) - tw := tar.NewWriter(gz) - for _, e := range entries { - mode := e.mode - if mode == 0 { - mode = 0o644 - } - hdr := &tar.Header{ - Name: e.name, - Mode: mode, - Typeflag: e.typeflag, - Linkname: e.linkname, - } - if e.typeflag == tar.TypeReg { - hdr.Size = int64(len(e.body)) - } - if err := tw.WriteHeader(hdr); err != nil { - t.Fatalf("write header %q: %v", e.name, err) - } - if e.typeflag == tar.TypeReg && len(e.body) > 0 { - if _, err := tw.Write([]byte(e.body)); err != nil { - t.Fatalf("write body %q: %v", e.name, err) - } - } - } - if err := tw.Close(); err != nil { - t.Fatalf("close tar: %v", err) - } - if err := gz.Close(); err != nil { - t.Fatalf("close gz: %v", err) - } - if err := f.Close(); err != nil { - t.Fatalf("close file: %v", err) - } - return tarPath -} - -// TestExtractSnapshotContainment drives the hardened extractor with crafted -// archives: absolute-path, traversal, symlink, hardlink, and unknown-typeflag -// entries must each be refused with an error naming the entry/flag, and NOTHING -// may be written outside the destination tree. A benign dir+reg archive must -// still extract cleanly. Table-driven; each case gets an isolated t.TempDir. -func TestExtractSnapshotContainment(t *testing.T) { - cases := []struct { - name string - entries []tarEntry - wantErrSub []string // all must appear in the error (nil = expect success) - wantFiles map[string]string - forbidPaths []string // absolute paths that must NOT exist after extraction - }{ - { - name: "absolute path entry refused", - entries: []tarEntry{ - {name: "/etc/passwd", typeflag: tar.TypeReg, body: "root:x:0:0"}, - }, - wantErrSub: []string{"absolute-path", "/etc/passwd"}, - }, - { - name: "traversal entry refused", - entries: []tarEntry{ - {name: "../../escape.txt", typeflag: tar.TypeReg, body: "pwned"}, - }, - wantErrSub: []string{"path traversal", "escape.txt"}, - }, - { - name: "symlink typeflag refused naming flag", - entries: []tarEntry{ - {name: ".agents/link", typeflag: tar.TypeSymlink, linkname: "/etc/passwd"}, - }, - wantErrSub: []string{"unsupported tar entry", ".agents/link", "typeflag"}, - }, - { - name: "hardlink typeflag refused naming flag", - entries: []tarEntry{ - {name: ".agents/hard", typeflag: tar.TypeLink, linkname: ".agents/real"}, - }, - wantErrSub: []string{"unsupported tar entry", ".agents/hard", "typeflag"}, - }, - { - name: "unknown typeflag refused", - entries: []tarEntry{ - // TypeFifo is neither dir, reg, symlink nor hardlink. - {name: ".agents/fifo", typeflag: tar.TypeFifo}, - }, - wantErrSub: []string{"unsupported tar entry", ".agents/fifo", "typeflag"}, - }, - { - name: "benign dir and reg archive extracts", - entries: []tarEntry{ - {name: ".agents", typeflag: tar.TypeDir, mode: 0o755}, - {name: ".agents/learnings", typeflag: tar.TypeDir, mode: 0o755}, - {name: ".agents/learnings/foo.md", typeflag: tar.TypeReg, body: "hello"}, - {name: ".agents/bar.md", typeflag: tar.TypeReg, body: "world!"}, - }, - wantErrSub: nil, - wantFiles: map[string]string{ - ".agents/learnings/foo.md": "hello", - ".agents/bar.md": "world!", - }, - }, - } - - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - work := t.TempDir() - tarPath := buildCraftedTarGz(t, work, tc.entries) - dest := filepath.Join(work, "out", ".agents") - - _, _, err := extractSnapshot(tarPath, dest) - - if len(tc.wantErrSub) == 0 { - if err != nil { - t.Fatalf("expected clean extract, got error: %v", err) - } - for rel, want := range tc.wantFiles { - got, rerr := os.ReadFile(filepath.Join(work, "out", rel)) - if rerr != nil { - t.Fatalf("read restored %s: %v", rel, rerr) - } - if string(got) != want { - t.Errorf("restored %s: got %q, want %q", rel, string(got), want) - } - } - // Destination tree must contain ONLY the benign entries — no - // stray files leaked outside .agents/. - assertOnlyExpected(t, filepath.Join(work, "out"), tc.wantFiles) - return - } - - if err == nil { - t.Fatalf("expected refusal error, got nil") - } - for _, sub := range tc.wantErrSub { - if !strings.Contains(err.Error(), sub) { - t.Errorf("error %q missing expected substring %q", err.Error(), sub) - } - } - // Nothing must have been written outside the destination root. - for _, forbidden := range tc.forbidPaths { - if _, statErr := os.Lstat(forbidden); statErr == nil { - t.Errorf("refused entry still wrote forbidden path %q", forbidden) - } - } - // The absolute-path case in particular must not have created /etc/passwd - // content anywhere reachable — assert the extraction root stayed empty of - // unexpected regular files. - assertNoRegularFilesOutside(t, filepath.Join(work, "out")) - }) - } -} - -// assertOnlyExpected walks root and asserts the set of regular files exactly -// matches the expected relative paths (keyed off the parent of root/.agents). -func assertOnlyExpected(t *testing.T, root string, want map[string]string) { - t.Helper() - got := map[string]bool{} - err := filepath.Walk(root, func(p string, info os.FileInfo, err error) error { - if err != nil { - return err - } - if info.Mode().IsRegular() { - rel, rerr := filepath.Rel(root, p) - if rerr != nil { - return rerr - } - got[rel] = true - } - return nil - }) - if err != nil { - t.Fatalf("walk %s: %v", root, err) - } - for rel := range want { - if !got[rel] { - t.Errorf("expected file %q missing from extraction tree", rel) - } - } - for rel := range got { - if _, ok := want[rel]; !ok { - t.Errorf("unexpected file %q in extraction tree (want only %v)", rel, keysOf(want)) - } - } -} - -// assertNoRegularFilesOutside asserts a refused extraction left no regular files -// under root (a partial/leaked write would show up here). -func assertNoRegularFilesOutside(t *testing.T, root string) { - t.Helper() - _ = filepath.Walk(root, func(p string, info os.FileInfo, err error) error { - if err != nil { - // root may not exist at all on a refusal — that's fine. - return nil - } - if info.Mode().IsRegular() { - t.Errorf("refused extraction leaked a regular file: %q", p) - } - return nil - }) -} - -func keysOf(m map[string]string) []string { - out := make([]string, 0, len(m)) - for k := range m { - out = append(out, k) - } - return out -} - -// TestExtractSnapshotProductionRoundTripStaysGreen is the fixture-fidelity guard: -// it builds a snapshot through the REAL production writer (createCorpusSnapshot → -// writeSnapshot) from a realistic files+dirs corpus and re-extracts it through the -// hardened extractor, proving the strict default-case does not reject any typeflag -// the writer actually emits for genuine corpus content (TypeDir/TypeReg only). -func TestExtractSnapshotProductionRoundTripStaysGreen(t *testing.T) { - work := t.TempDir() - // A realistic .agents/ corpus shape: nested dirs + regular markdown files. - src := filepath.Join(work, "repo", corpusSourceDir) - if err := os.MkdirAll(filepath.Join(src, "learnings"), 0o755); err != nil { - t.Fatalf("mkdir learnings: %v", err) - } - if err := os.MkdirAll(filepath.Join(src, "research", "deep"), 0o755); err != nil { - t.Fatalf("mkdir research/deep: %v", err) - } - files := map[string]string{ - "learnings/one.md": "learning one", - "research/two.md": "research two", - "research/deep/three.md": "deep three", - } - for rel, body := range files { - if err := os.WriteFile(filepath.Join(src, rel), []byte(body), 0o644); err != nil { - t.Fatalf("write %s: %v", rel, err) - } - } - - outDir := filepath.Join(work, "snapshots") - manifest, _, err := createCorpusSnapshot(filepath.Join(work, "repo"), outDir) - if err != nil { - t.Fatalf("createCorpusSnapshot: %v", err) - } - if manifest.FileCount != len(files) { - t.Fatalf("manifest file count: got %d, want %d", manifest.FileCount, len(files)) - } - - dest := filepath.Join(work, "restored", corpusSourceDir) - count, _, err := extractSnapshot(manifest.SnapshotPath, dest) - if err != nil { - t.Fatalf("extractSnapshot of production snapshot must stay green, got: %v", err) - } - if count != len(files) { - t.Errorf("restored file count: got %d, want %d", count, len(files)) - } - for rel, want := range files { - got, rerr := os.ReadFile(filepath.Join(work, "restored", corpusSourceDir, rel)) - if rerr != nil { - t.Fatalf("read restored %s: %v", rel, rerr) - } - if string(got) != want { - t.Errorf("restored %s: got %q, want %q", rel, string(got), want) - } - } -} diff --git a/cli/cmd/ao/corpus_snapshot_test.go b/cli/cmd/ao/corpus_snapshot_test.go deleted file mode 100644 index 56a85bd93..000000000 --- a/cli/cmd/ao/corpus_snapshot_test.go +++ /dev/null @@ -1,318 +0,0 @@ -//go:build flywheel - -// practices: [wiki-knowledge-surface, resilience-patterns, ai-assisted-dev] -package main - -import ( - "archive/tar" - "compress/gzip" - "encoding/json" - "io" - "os" - "path/filepath" - "strings" - "testing" - "time" -) - -// TestWriteSnapshotRoundTrip exercises the write+extract round-trip with a -// small synthetic corpus and asserts: file count, byte total, manifest sha, -// and that every original byte survives the tar.gz round-trip. -func TestWriteSnapshotRoundTrip(t *testing.T) { - tmp := t.TempDir() - src := filepath.Join(tmp, "src", ".agents") - if err := os.MkdirAll(filepath.Join(src, "learnings"), 0o755); err != nil { - t.Fatalf("mkdir src/learnings: %v", err) - } - if err := os.MkdirAll(filepath.Join(src, "research"), 0o755); err != nil { - t.Fatalf("mkdir src/research: %v", err) - } - if err := os.WriteFile(filepath.Join(src, "learnings", "foo.md"), []byte("hello"), 0o644); err != nil { - t.Fatalf("write foo.md: %v", err) - } - if err := os.WriteFile(filepath.Join(src, "research", "bar.md"), []byte("world!"), 0o644); err != nil { - t.Fatalf("write bar.md: %v", err) - } - - tarPath := filepath.Join(tmp, "snap.tar.gz") - count, total, sum, err := writeSnapshot(tarPath, src) - if err != nil { - t.Fatalf("writeSnapshot: %v", err) - } - if count != 2 { - t.Errorf("file_count: got %d, want 2", count) - } - if total != int64(len("hello")+len("world!")) { - t.Errorf("total_bytes: got %d, want %d", total, int64(len("hello")+len("world!"))) - } - if len(sum) != 64 { - t.Errorf("sha256 hex: got %d chars, want 64", len(sum)) - } - - dest := filepath.Join(tmp, "restored") - if err := os.MkdirAll(dest, 0o755); err != nil { - t.Fatalf("mkdir dest: %v", err) - } - restoredCount, restoredTotal, err := extractSnapshot(tarPath, filepath.Join(dest, ".agents")) - if err != nil { - t.Fatalf("extractSnapshot: %v", err) - } - if restoredCount != count { - t.Errorf("restored file count: got %d, want %d", restoredCount, count) - } - if restoredTotal != total { - t.Errorf("restored bytes: got %d, want %d", restoredTotal, total) - } - want := map[string]string{ - ".agents/learnings/foo.md": "hello", - ".agents/research/bar.md": "world!", - } - for rel, expected := range want { - got, err := os.ReadFile(filepath.Join(dest, rel)) - if err != nil { - t.Fatalf("read restored %s: %v", rel, err) - } - if string(got) != expected { - t.Errorf("restored %s: got %q, want %q", rel, string(got), expected) - } - } -} - -// TestExtractSnapshotRefusesPathTraversal verifies the extractor rejects -// tarball entries whose cleaned path escapes the destination root. -func TestExtractSnapshotRefusesPathTraversal(t *testing.T) { - tmp := t.TempDir() - tarPath := filepath.Join(tmp, "evil.tar.gz") - f, err := os.Create(tarPath) - if err != nil { - t.Fatalf("create tar: %v", err) - } - gz := gzip.NewWriter(f) - tw := tar.NewWriter(gz) - body := []byte("payload") - hdr := &tar.Header{ - Name: "../escaped.txt", - Mode: 0o644, - Size: int64(len(body)), - Typeflag: tar.TypeReg, - } - if err := tw.WriteHeader(hdr); err != nil { - t.Fatalf("write header: %v", err) - } - if _, err := tw.Write(body); err != nil { - t.Fatalf("write body: %v", err) - } - _ = tw.Close() - _ = gz.Close() - _ = f.Close() - - dest := filepath.Join(tmp, "out", ".agents") - _, _, err = extractSnapshot(tarPath, dest) - if err == nil { - t.Fatalf("expected path-traversal error, got nil") - } - if !strings.Contains(err.Error(), "path traversal") { - t.Errorf("expected 'path traversal' in error; got %q", err.Error()) - } -} - -func TestExtractSnapshotRefusesOversizedEntry(t *testing.T) { - tmp := t.TempDir() - tarPath := filepath.Join(tmp, "oversized.tar.gz") - f, err := os.Create(tarPath) - if err != nil { - t.Fatalf("create tar: %v", err) - } - gz := gzip.NewWriter(f) - tw := tar.NewWriter(gz) - body := []byte("payload too large") - hdr := &tar.Header{ - Name: ".agents/huge.bin", - Mode: 0o644, - Size: int64(len(body)), - Typeflag: tar.TypeReg, - } - if err := tw.WriteHeader(hdr); err != nil { - t.Fatalf("write header: %v", err) - } - if _, err := tw.Write(body); err != nil { - t.Fatalf("write body: %v", err) - } - _ = tw.Close() - _ = gz.Close() - _ = f.Close() - - origLimit := maxSnapshotExtractBytes - maxSnapshotExtractBytes = int64(len(body) - 1) - t.Cleanup(func() { maxSnapshotExtractBytes = origLimit }) - - _, _, err = extractSnapshot(tarPath, filepath.Join(tmp, "out", ".agents")) - if err == nil { - t.Fatal("expected oversized snapshot error, got nil") - } - if !strings.Contains(err.Error(), "byte limit") { - t.Fatalf("expected byte limit error, got %q", err.Error()) - } -} - -func TestWriteSnapshotDoesNotFollowSymlinkBody(t *testing.T) { - tmp := t.TempDir() - src := filepath.Join(tmp, ".agents") - if err := os.MkdirAll(src, 0o755); err != nil { - t.Fatalf("mkdir: %v", err) - } - outside := filepath.Join(tmp, "outside.txt") - if err := os.WriteFile(outside, []byte("secret"), 0o644); err != nil { - t.Fatalf("write outside: %v", err) - } - if err := os.Symlink(outside, filepath.Join(src, "external-link")); err != nil { - t.Skipf("symlink unavailable: %v", err) - } - - tarPath := filepath.Join(tmp, "snap.tar.gz") - count, total, _, err := writeSnapshot(tarPath, src) - if err != nil { - t.Fatalf("writeSnapshot: %v", err) - } - if count != 0 || total != 0 { - t.Fatalf("symlink body was counted: count=%d total=%d", count, total) - } -} - -// TestFindLatestSnapshotPicksNewest seeds a directory with three tarballs of -// staggered mtimes and asserts findLatestSnapshot returns the newest. -func TestFindLatestSnapshotPicksNewest(t *testing.T) { - dir := t.TempDir() - older := filepath.Join(dir, "a-20260101T000000Z.tar.gz") - middle := filepath.Join(dir, "a-20260201T000000Z.tar.gz") - newer := filepath.Join(dir, "a-20260301T000000Z.tar.gz") - for _, p := range []string{older, middle, newer} { - if err := os.WriteFile(p, []byte("x"), 0o644); err != nil { - t.Fatalf("write %s: %v", p, err) - } - } - now := time.Now() - _ = os.Chtimes(older, now.Add(-72*time.Hour), now.Add(-72*time.Hour)) - _ = os.Chtimes(middle, now.Add(-24*time.Hour), now.Add(-24*time.Hour)) - _ = os.Chtimes(newer, now, now) - - got, err := findLatestSnapshot(dir) - if err != nil { - t.Fatalf("findLatestSnapshot: %v", err) - } - if got != newer { - t.Errorf("findLatestSnapshot: got %q, want %q", got, newer) - } -} - -// TestFindLatestSnapshotEmptyDir asserts an empty dir errors clearly rather -// than returning an empty path or silently succeeding. -func TestFindLatestSnapshotEmptyDir(t *testing.T) { - dir := t.TempDir() - _, err := findLatestSnapshot(dir) - if err == nil { - t.Fatalf("expected error on empty dir") - } - if !strings.Contains(err.Error(), "no *.tar.gz snapshots") { - t.Errorf("expected 'no *.tar.gz snapshots' in error; got %q", err.Error()) - } -} - -// TestSnapshotManifestShape pins the JSON manifest structure so downstream -// consumers (restore tooling, freshness gate) don't drift. -func TestSnapshotManifestShape(t *testing.T) { - m := snapshotManifest{ - SnapshotPath: "/tmp/foo.tar.gz", - Repo: "agentops", - Source: "/tmp/foo/.agents", - FileCount: 3, - TotalBytes: 123, - SHA256: "abc", - CreatedAt: time.Date(2026, 5, 11, 12, 0, 0, 0, time.UTC), - } - buf, err := json.Marshal(m) - if err != nil { - t.Fatalf("marshal: %v", err) - } - var decoded map[string]any - if err := json.Unmarshal(buf, &decoded); err != nil { - t.Fatalf("unmarshal: %v", err) - } - for _, key := range []string{"snapshot_path", "repo", "source", "file_count", "total_bytes", "sha256", "created_at"} { - if _, ok := decoded[key]; !ok { - t.Errorf("manifest missing key %q", key) - } - } -} - -// TestResolveSnapshotDirEnvOverride asserts the env var beats the home -// fallback, and that ~ in user-supplied paths is expanded. -func TestResolveSnapshotDirEnvOverride(t *testing.T) { - t.Setenv(defaultSnapshotDirEnv, "/explicit/path") - got, err := resolveSnapshotDir("") - if err != nil { - t.Fatalf("resolveSnapshotDir: %v", err) - } - if got != "/explicit/path" { - t.Errorf("env override: got %q, want %q", got, "/explicit/path") - } - - home, _ := os.UserHomeDir() - t.Setenv(defaultSnapshotDirEnv, "") - got, err = resolveSnapshotDir("~/snapshots") - if err != nil { - t.Fatalf("resolveSnapshotDir tilde: %v", err) - } - wantPrefix := filepath.Join(home, "snapshots") - if got != wantPrefix { - t.Errorf("tilde expansion: got %q, want %q", got, wantPrefix) - } -} - -// TestWriteSnapshotCreatesDeterministicHeader makes sure regular files survive -// with their byte contents and a reasonable size header (no zero-byte truncation). -func TestWriteSnapshotPreservesFileSize(t *testing.T) { - tmp := t.TempDir() - src := filepath.Join(tmp, ".agents") - if err := os.MkdirAll(src, 0o755); err != nil { - t.Fatalf("mkdir: %v", err) - } - payload := []byte("the corpus must compound") - if err := os.WriteFile(filepath.Join(src, "fact.md"), payload, 0o644); err != nil { - t.Fatalf("write: %v", err) - } - tarPath := filepath.Join(tmp, "snap.tar.gz") - _, _, _, err := writeSnapshot(tarPath, src) - if err != nil { - t.Fatalf("writeSnapshot: %v", err) - } - f, err := os.Open(tarPath) - if err != nil { - t.Fatalf("open: %v", err) - } - defer f.Close() - gz, err := gzip.NewReader(f) - if err != nil { - t.Fatalf("gzip reader: %v", err) - } - tr := tar.NewReader(gz) - var found bool - for { - hdr, err := tr.Next() - if err == io.EOF { - break - } - if err != nil { - t.Fatalf("tar.Next: %v", err) - } - if strings.HasSuffix(hdr.Name, "fact.md") { - found = true - if hdr.Size != int64(len(payload)) { - t.Errorf("fact.md size: got %d, want %d", hdr.Size, len(payload)) - } - } - } - if !found { - t.Fatal("fact.md not found in tarball") - } -} diff --git a/cli/cmd/ao/default_spine_test.go b/cli/cmd/ao/default_spine_test.go index 787ae7ed8..b12849cf7 100644 --- a/cli/cmd/ao/default_spine_test.go +++ b/cli/cmd/ao/default_spine_test.go @@ -9,6 +9,7 @@ import ( var approvedDefaultSpine = map[string]bool{ "capabilities": true, "config": true, "constraint": true, "doctor": true, "gate": true, "goals": true, "init": true, + "flywheel": true, "provenance": true, "quick-start": true, "robot-docs": true, "session": true, "skills": true, "status": true, "version": true, @@ -19,9 +20,6 @@ var approvedDefaultSpine = map[string]bool{ } func TestDefaultSpineMatchesCathedralCutAllowlist(t *testing.T) { - if len(archiveBuildTags) != 0 { - t.Skip("restoration build") - } removed := pruneToDefaultSpine(rootCmd) t.Cleanup(func() { restorePrunedCommands(rootCmd, removed) }) var unexpected, missing []string diff --git a/cli/cmd/ao/dump_registered_commands_test.go b/cli/cmd/ao/dump_registered_commands_test.go index 14993613f..2f3a0994c 100644 --- a/cli/cmd/ao/dump_registered_commands_test.go +++ b/cli/cmd/ao/dump_registered_commands_test.go @@ -26,10 +26,8 @@ import ( // When AO_DUMP_REGISTERED_CMDS is unset the test is a no-op assertion that the // command set is non-empty, so it stays cheap in the normal `go test ./...` run. func TestDumpRegisteredTopLevelCommands(t *testing.T) { - if len(archiveBuildTags) == 0 { - removed := pruneToDefaultSpine(rootCmd) - t.Cleanup(func() { restorePrunedCommands(rootCmd, removed) }) - } + removed := pruneToDefaultSpine(rootCmd) + t.Cleanup(func() { restorePrunedCommands(rootCmd, removed) }) var names []string for _, c := range rootCmd.Commands() { if c.Name() == "help" { diff --git a/cli/cmd/ao/event_bus_adapter.go b/cli/cmd/ao/event_bus_adapter.go deleted file mode 100644 index f3c82cd86..000000000 --- a/cli/cmd/ao/event_bus_adapter.go +++ /dev/null @@ -1,156 +0,0 @@ -// practices: [hexagonal-architecture, ddd-bounded-context] -package main - -import ( - "context" - "errors" - "fmt" - "strconv" - "sync" - "sync/atomic" - - "github.com/boshu2/agentops/cli/internal/ports" -) - -// productionEventBus satisfies ports.EventBusPort with a sync, -// in-memory dispatch loop. Publish-acknowledge is synchronous; -// dispatch to subscribers is also synchronous in the publishing -// goroutine, matching the InMemoryEventBus contract used by tests. -// -// TRANSPORT NOTE (read before swapping): this is intentionally a -// process-local adapter. It is suitable for the current -// single-process /evolve loop, where Publish→Subscribe round-trips -// happen inside the same `ao` invocation. When the factory moves to -// a multi-process or distributed shape (NATS, Kafka, Redis Streams, -// etc.), a sibling adapter at e.g. cli/cmd/ao/event_bus_nats.go -// should be wired in front of this default. The port surface -// (Publish + Subscribe-with-cancel) is unchanged, so swap-in is a -// constructor change. -// -// Semantics enforced: -// - Publish auto-assigns a monotonically-increasing ID when -// event.ID is empty. Topic empty → error (port contract). -// - Subscribe registers a handler under topic and returns a cancel -// func. Cancel unregisters AND blocks until any in-flight -// callback for THAT handler completes (per port contract). -// - Dispatch is exact-topic-match only (no globbing). -// - Errors returned from handlers are NOT retried; the bus simply -// advances to the next handler. Adapters MAY log them; this -// adapter intentionally does not. -type productionEventBus struct { - mu sync.RWMutex - nextID atomic.Uint64 - subs map[string][]*eventBusSubscription -} - -// eventBusSubscription tracks one Subscribe call. inFlight is the -// per-handler waitgroup that Cancel uses to block until any in-flight -// dispatch finishes before unregistering. -type eventBusSubscription struct { - id uint64 - handler ports.EventHandler - inFlight sync.WaitGroup - canceled atomic.Bool -} - -func newProductionEventBus() *productionEventBus { - return &productionEventBus{ - subs: make(map[string][]*eventBusSubscription), - } -} - -// Publish enqueues + dispatches the event synchronously to all -// subscribers of event.Topic. Returns the event with ID populated. -func (b *productionEventBus) Publish(ctx context.Context, event ports.Event) (ports.Event, error) { - if err := ctx.Err(); err != nil { - return ports.Event{}, err - } - if event.Topic == "" { - return ports.Event{}, errors.New("productionEventBus: Topic required") - } - if event.ID == "" { - event.ID = strconv.FormatUint(b.nextID.Add(1), 10) - } - - b.mu.RLock() - subs := append([]*eventBusSubscription{}, b.subs[event.Topic]...) - b.mu.RUnlock() - - for _, sub := range subs { - if sub.canceled.Load() { - continue - } - sub.inFlight.Add(1) - func(s *eventBusSubscription) { - defer s.inFlight.Done() - if s.canceled.Load() { - return - } - _ = s.handler(ctx, event) - }(sub) - } - return event, nil -} - -// Subscribe registers handler for topic. Returns a cancel function. -func (b *productionEventBus) Subscribe(ctx context.Context, topic string, handler ports.EventHandler) (func(), error) { - if err := ctx.Err(); err != nil { - return nil, err - } - if topic == "" { - return nil, errors.New("productionEventBus: topic required") - } - if handler == nil { - return nil, errors.New("productionEventBus: handler required") - } - sub := &eventBusSubscription{ - id: b.nextID.Add(1), - handler: handler, - } - b.mu.Lock() - b.subs[topic] = append(b.subs[topic], sub) - b.mu.Unlock() - - cancel := func() { - if !sub.canceled.CompareAndSwap(false, true) { - return // already cancelled - } - b.mu.Lock() - bucket := b.subs[topic] - for i, s := range bucket { - if s.id == sub.id { - b.subs[topic] = append(bucket[:i], bucket[i+1:]...) - break - } - } - if len(b.subs[topic]) == 0 { - delete(b.subs, topic) - } - b.mu.Unlock() - sub.inFlight.Wait() // block until in-flight callbacks finish - } - return cancel, nil -} - -// activeSubscribers returns the number of registered handlers for a -// topic. Used by tests as a structural probe. -func (b *productionEventBus) activeSubscribers(topic string) int { - b.mu.RLock() - defer b.mu.RUnlock() - return len(b.subs[topic]) -} - -// String is used by debug callsites that want to inspect the -// adapter's identity. -func (b *productionEventBus) String() string { - return fmt.Sprintf("productionEventBus(inProcess; topics=%d)", b.topicCount()) -} - -func (b *productionEventBus) topicCount() int { - b.mu.RLock() - defer b.mu.RUnlock() - return len(b.subs) -} - -// Compile-time assertion: productionEventBus satisfies the port. -var _ ports.EventBusPort = (*productionEventBus)(nil) diff --git a/cli/cmd/ao/event_bus_adapter_test.go b/cli/cmd/ao/event_bus_adapter_test.go deleted file mode 100644 index 0cd8f10d2..000000000 --- a/cli/cmd/ao/event_bus_adapter_test.go +++ /dev/null @@ -1,214 +0,0 @@ -// practices: [hexagonal-architecture, tdd] -package main - -import ( - "context" - "errors" - "sync" - "sync/atomic" - "testing" - "time" - - "github.com/boshu2/agentops/cli/internal/ports" -) - -// Sibling pattern: cycle 117 ci_status_adapter_test.go. - -func TestProductionEventBus_PublishDispatchesToSubscriber(t *testing.T) { - b := newProductionEventBus() - var got ports.Event - var wg sync.WaitGroup - wg.Add(1) - cancel, err := b.Subscribe(context.Background(), "test.topic", func(_ context.Context, e ports.Event) error { - got = e - wg.Done() - return nil - }) - if err != nil { - t.Fatal(err) - } - defer cancel() - _, err = b.Publish(context.Background(), ports.Event{Topic: "test.topic", Payload: []byte("hi")}) - if err != nil { - t.Fatal(err) - } - wg.Wait() - if string(got.Payload) != "hi" { - t.Fatalf("Payload = %q", got.Payload) - } - if got.ID == "" { - t.Fatal("ID should be auto-assigned") - } -} - -func TestProductionEventBus_PublishAutoAssignsIDs(t *testing.T) { - b := newProductionEventBus() - e1, _ := b.Publish(context.Background(), ports.Event{Topic: "x"}) - e2, _ := b.Publish(context.Background(), ports.Event{Topic: "x"}) - if e1.ID == "" || e2.ID == "" { - t.Fatal("IDs not assigned") - } - if e1.ID == e2.ID { - t.Fatalf("IDs should differ: %q vs %q", e1.ID, e2.ID) - } -} - -func TestProductionEventBus_PublishHonorsCallerProvidedID(t *testing.T) { - b := newProductionEventBus() - got, _ := b.Publish(context.Background(), ports.Event{ID: "my-id", Topic: "x"}) - if got.ID != "my-id" { - t.Fatalf("ID overwritten: got %q", got.ID) - } -} - -func TestProductionEventBus_EmptyTopicErrors(t *testing.T) { - b := newProductionEventBus() - _, err := b.Publish(context.Background(), ports.Event{Payload: []byte("x")}) - if err == nil { - t.Fatal("expected error on empty topic, got nil") - } -} - -func TestProductionEventBus_DispatchIsTopicSpecific(t *testing.T) { - b := newProductionEventBus() - var aHits, bHits atomic.Int32 - cancelA, _ := b.Subscribe(context.Background(), "topicA", func(_ context.Context, _ ports.Event) error { - aHits.Add(1) - return nil - }) - defer cancelA() - cancelB, _ := b.Subscribe(context.Background(), "topicB", func(_ context.Context, _ ports.Event) error { - bHits.Add(1) - return nil - }) - defer cancelB() - _, _ = b.Publish(context.Background(), ports.Event{Topic: "topicA"}) - _, _ = b.Publish(context.Background(), ports.Event{Topic: "topicA"}) - _, _ = b.Publish(context.Background(), ports.Event{Topic: "topicB"}) - // Sync dispatch — no wait needed. - if aHits.Load() != 2 || bHits.Load() != 1 { - t.Fatalf("topic-specific dispatch wrong: A=%d B=%d", aHits.Load(), bHits.Load()) - } -} - -func TestProductionEventBus_MultipleSubscribersToSameTopic(t *testing.T) { - b := newProductionEventBus() - var hits atomic.Int32 - c1, _ := b.Subscribe(context.Background(), "t", func(_ context.Context, _ ports.Event) error { - hits.Add(1) - return nil - }) - defer c1() - c2, _ := b.Subscribe(context.Background(), "t", func(_ context.Context, _ ports.Event) error { - hits.Add(1) - return nil - }) - defer c2() - _, _ = b.Publish(context.Background(), ports.Event{Topic: "t"}) - if hits.Load() != 2 { - t.Fatalf("fanout wrong: hits = %d, want 2", hits.Load()) - } -} - -func TestProductionEventBus_CancelUnregisters(t *testing.T) { - b := newProductionEventBus() - var hits atomic.Int32 - cancel, _ := b.Subscribe(context.Background(), "t", func(_ context.Context, _ ports.Event) error { - hits.Add(1) - return nil - }) - _, _ = b.Publish(context.Background(), ports.Event{Topic: "t"}) - cancel() - _, _ = b.Publish(context.Background(), ports.Event{Topic: "t"}) - if hits.Load() != 1 { - t.Fatalf("cancel did not unregister: hits = %d, want 1", hits.Load()) - } - if b.activeSubscribers("t") != 0 { - t.Fatalf("subscriber not removed: count = %d", b.activeSubscribers("t")) - } -} - -func TestProductionEventBus_CancelIsIdempotent(t *testing.T) { - b := newProductionEventBus() - cancel, _ := b.Subscribe(context.Background(), "t", func(_ context.Context, _ ports.Event) error { - return nil - }) - cancel() - cancel() // must not panic - cancel() -} - -func TestProductionEventBus_CancelBlocksInFlightCallback(t *testing.T) { - b := newProductionEventBus() - released := make(chan struct{}) - started := make(chan struct{}) - cancel, _ := b.Subscribe(context.Background(), "t", func(_ context.Context, _ ports.Event) error { - close(started) - <-released - return nil - }) - go func() { _, _ = b.Publish(context.Background(), ports.Event{Topic: "t"}) }() - <-started // handler is mid-flight - - cancelDone := make(chan struct{}) - go func() { - cancel() - close(cancelDone) - }() - select { - case <-cancelDone: - t.Fatal("cancel returned before in-flight callback finished") - case <-time.After(50 * time.Millisecond): - // good — cancel is waiting - } - close(released) // let the handler finish - <-cancelDone // now cancel should return -} - -func TestProductionEventBus_SubscribeEmptyTopicErrors(t *testing.T) { - b := newProductionEventBus() - _, err := b.Subscribe(context.Background(), "", func(_ context.Context, _ ports.Event) error { return nil }) - if err == nil { - t.Fatal("expected error on empty topic, got nil") - } -} - -func TestProductionEventBus_SubscribeNilHandlerErrors(t *testing.T) { - b := newProductionEventBus() - _, err := b.Subscribe(context.Background(), "t", nil) - if err == nil { - t.Fatal("expected error on nil handler, got nil") - } -} - -func TestProductionEventBus_HandlerErrorsDoNotStopFanout(t *testing.T) { - b := newProductionEventBus() - var hits atomic.Int32 - c1, _ := b.Subscribe(context.Background(), "t", func(_ context.Context, _ ports.Event) error { - hits.Add(1) - return errors.New("boom") - }) - defer c1() - c2, _ := b.Subscribe(context.Background(), "t", func(_ context.Context, _ ports.Event) error { - hits.Add(1) - return nil - }) - defer c2() - _, _ = b.Publish(context.Background(), ports.Event{Topic: "t"}) - if hits.Load() != 2 { - t.Fatalf("second handler should still run despite first's error: hits = %d", hits.Load()) - } -} - -func TestProductionEventBus_PublishHonorsContextCancellation(t *testing.T) { - b := newProductionEventBus() - ctx, cancel := context.WithCancel(context.Background()) - cancel() - _, err := b.Publish(ctx, ports.Event{Topic: "t"}) - if err == nil { - t.Fatal("expected cancellation error, got nil") - } - if !errors.Is(err, context.Canceled) { - t.Fatalf("error = %v, want context.Canceled", err) - } -} diff --git a/cli/cmd/ao/finding_compiler_adapter.go b/cli/cmd/ao/finding_compiler_adapter.go index ab388eeeb..64859ee70 100644 --- a/cli/cmd/ao/finding_compiler_adapter.go +++ b/cli/cmd/ao/finding_compiler_adapter.go @@ -134,7 +134,7 @@ func parseCompilerKind(name string) (ports.CompiledOutputKind, bool) { switch name { case string(ports.CompiledOutputPlanningRule): return ports.CompiledOutputPlanningRule, true - case string(ports.CompiledOutputPremortemCheck), "pre-mortem", "pre_mortem": + case string(ports.CompiledOutputPremortemCheck): return ports.CompiledOutputPremortemCheck, true case string(ports.CompiledOutputConstraint): return ports.CompiledOutputConstraint, true diff --git a/cli/cmd/ao/finding_compiler_adapter_test.go b/cli/cmd/ao/finding_compiler_adapter_test.go index 93cf3eb9f..ed40dca46 100644 --- a/cli/cmd/ao/finding_compiler_adapter_test.go +++ b/cli/cmd/ao/finding_compiler_adapter_test.go @@ -30,7 +30,7 @@ func mechanicalFrontmatter(extra map[string]string) map[string]string { return fm } -// An ADVISORY finding (no detector metadata) defaults to plan + pre-mortem only: +// An ADVISORY finding (no detector metadata) defaults to plan + premortem only: // the constraint target is skipped rather than emitting a dead artifact the gate // ignores. (EM-ENF: constraint only when detector metadata is present and valid.) func TestProductionFindingCompiler_AdvisoryDefaultsToTwoKinds(t *testing.T) { @@ -43,7 +43,7 @@ func TestProductionFindingCompiler_AdvisoryDefaultsToTwoKinds(t *testing.T) { t.Fatal(err) } if len(out) != 2 { - t.Fatalf("len = %d, want 2 (advisory: plan+pre-mortem, no constraint)", len(out)) + t.Fatalf("len = %d, want 2 (advisory: plan+premortem, no constraint)", len(out)) } for _, o := range out { if o.Kind == ports.CompiledOutputConstraint { diff --git a/cli/cmd/ao/flag_matrix_test.go b/cli/cmd/ao/flag_matrix_test.go index 2c83b74f7..81bab2ebf 100644 --- a/cli/cmd/ao/flag_matrix_test.go +++ b/cli/cmd/ao/flag_matrix_test.go @@ -111,15 +111,15 @@ func TestFlagMatrix_QuietMode(t *testing.T) { for _, tt := range tests { if !approvedDefaultSpineBinaryCommand(tt.args) { - t.Run(tt.name+"-archived", func(t *testing.T) { + t.Run(tt.name+"-unpublished", func(t *testing.T) { cmd := exec.Command(bin, tt.args[0], "--help") cmd.Dir = findRepoRoot(t) out, err := cmd.CombinedOutput() if err == nil { - t.Fatalf("archived command %q unexpectedly executes in the default spine", tt.args[0]) + t.Fatalf("unpublished command %q unexpectedly executes in the supported binary", tt.args[0]) } if !strings.Contains(string(out), "unknown command") { - t.Fatalf("archived command %q failed for the wrong reason:\n%s", tt.args[0], out) + t.Fatalf("unpublished command %q failed for the wrong reason:\n%s", tt.args[0], out) } }) continue diff --git a/cli/cmd/ao/flywheel_promoted_body_dedupe_legacy_test.go b/cli/cmd/ao/flywheel_promoted_body_dedupe_legacy_test.go deleted file mode 100644 index e13730082..000000000 --- a/cli/cmd/ao/flywheel_promoted_body_dedupe_legacy_test.go +++ /dev/null @@ -1,71 +0,0 @@ -//go:build legacy - -// practices: [dora-metrics, wiki-knowledge-surface] -package main - -import ( - "encoding/json" - "os" - "path/filepath" - "strings" - "testing" - "time" -) - -// TestCodexEnsureStartMaintenanceSkipsStalePendingAlreadyPromotedBody exercises -// the `codex ensure-start` maintenance path. Split out of -// flywheel_promoted_body_dedupe_test.go (which keeps the spine close-loop tests + -// shared fixtures untagged) because the codex command is archived behind -// //go:build legacy (age-h4y3). It reuses the untagged shared fixtures. -func TestCodexEnsureStartMaintenanceSkipsStalePendingAlreadyPromotedBody(t *testing.T) { - t.Setenv("HOME", t.TempDir()) - t.Setenv("CODEX_THREAD_ID", "codex-stale-pending-promoted-body") - t.Setenv("CODEX_INTERNAL_ORIGINATOR_OVERRIDE", "Codex Desktop") - - tmp, pendingFile := setupStalePendingAlreadyPromotedFixture(t) - if err := os.WriteFile(filepath.Join(tmp, "AGENTS.md"), []byte("# Test repo\n"), 0o644); err != nil { - t.Fatalf("write AGENTS.md: %v", err) - } - - t.Chdir(tmp) - out, err := executeCommand("codex", "ensure-start", "--json", "--query", "stale pending promoted body") - if err != nil { - t.Fatalf("codex ensure-start: %v\noutput: %s", err, out) - } - var first codexEnsureStartResult - if err := json.Unmarshal([]byte(strings.TrimSpace(out)), &first); err != nil { - t.Fatalf("parse ensure-start json: %v\noutput: %s", err, out) - } - if !first.Performed { - t.Fatalf("ensure-start Performed=false, want true: %+v", first) - } - - assertSinglePromotedArtifact(t, tmp) - assertPendingMovedToProcessed(t, tmp, pendingFile) - assertDuplicateSkipAudited(t, tmp) - - before, err := os.ReadFile(filepath.Join(tmp, ".agents", "ao", "codex", "state.json")) - if err != nil { - t.Fatalf("read codex state: %v", err) - } - time.Sleep(time.Millisecond) - secondOut, err := executeCommand("codex", "ensure-start", "--json", "--query", "stale pending promoted body") - if err != nil { - t.Fatalf("second codex ensure-start: %v\noutput: %s", err, secondOut) - } - var second codexEnsureStartResult - if err := json.Unmarshal([]byte(strings.TrimSpace(secondOut)), &second); err != nil { - t.Fatalf("parse second ensure-start json: %v\noutput: %s", err, secondOut) - } - if second.Performed { - t.Fatalf("second ensure-start Performed=true, want false: %+v", second) - } - after, err := os.ReadFile(filepath.Join(tmp, ".agents", "ao", "codex", "state.json")) - if err != nil { - t.Fatalf("read codex state after second ensure-start: %v", err) - } - if string(before) != string(after) { - t.Fatalf("ensure-start idempotency changed state\nbefore:\n%s\nafter:\n%s", string(before), string(after)) - } - assertSinglePromotedArtifact(t, tmp) -} diff --git a/cli/cmd/ao/goals.go b/cli/cmd/ao/goals.go index a5463a083..5b4570dfb 100644 --- a/cli/cmd/ao/goals.go +++ b/cli/cmd/ao/goals.go @@ -24,14 +24,8 @@ Analysis: drift (d) Compare snapshots for regressions history (h) Show goal measurement history export (e) Export latest snapshot as JSON - -Management: - init Bootstrap a new GOALS.md interactively - add (a) Add a new goal - steer Manage directives (add/remove/prioritize) - prune (p) Remove stale gates - migrate (mg) Migrate between formats - meta Run and report meta-goals only`, + meta Run and report meta-goals only + scenarios Inspect or lint linked acceptance scenarios`, } const defaultGoalsTimeoutSeconds = 240 @@ -54,7 +48,6 @@ func init() { goalsCmd.AddGroup( &cobra.Group{ID: "measurement", Title: "Measurement:"}, &cobra.Group{ID: "analysis", Title: "Analysis:"}, - &cobra.Group{ID: "management", Title: "Management:"}, ) goalsCmd.PersistentFlags().StringVar(&goalsFile, "file", "", "Path to goals file (auto-detects GOALS.md then GOALS.yaml)") goalsCmd.PersistentFlags().IntVar(&goalsTimeout, "timeout", defaultGoalsTimeoutSeconds, "Check timeout in seconds") diff --git a/cli/cmd/ao/goals_add.go b/cli/cmd/ao/goals_add.go deleted file mode 100644 index bdd519255..000000000 --- a/cli/cmd/ao/goals_add.go +++ /dev/null @@ -1,44 +0,0 @@ -// practices: [dora-metrics, lean-startup] -package main - -import ( - "context" - "time" - - "github.com/boshu2/agentops/cli/internal/goals" - "github.com/spf13/cobra" -) - -var ( - goalsAddWeight int - goalsAddType string - goalsAddDescription string -) - -var goalsAddCmd = &cobra.Command{ - Use: "add ", - Aliases: []string{"a"}, - Short: "Add a new goal", - GroupID: "management", - Args: cobra.ExactArgs(2), - RunE: func(cmd *cobra.Command, args []string) error { - return goals.RunAdd(context.Background(), goals.AddOptions{ - ID: args[0], - Check: args[1], - Weight: goalsAddWeight, - Type: goalsAddType, - Description: goalsAddDescription, - GoalsFile: resolveGoalsFile(), - Timeout: time.Duration(goalsTimeout) * time.Second, - DryRun: dryRun, - }) - }, -} - -func init() { - goalsAddCmd.Flags().IntVar(&goalsAddWeight, "weight", 5, "Goal weight (1-10)") - goalsAddCmd.Flags().StringVar(&goalsAddType, "type", "", "Goal type (health, architecture, quality, meta)") - goalsAddCmd.Flags().StringVar(&goalsAddDescription, "description", "", "Goal description") - _ = goalsAddCmd.RegisterFlagCompletionFunc("type", staticCompletionFunc("health", "architecture", "quality", "meta")) - goalsCmd.AddCommand(goalsAddCmd) -} diff --git a/cli/cmd/ao/goals_init.go b/cli/cmd/ao/goals_init.go deleted file mode 100644 index 8ad7640cf..000000000 --- a/cli/cmd/ao/goals_init.go +++ /dev/null @@ -1,62 +0,0 @@ -// practices: [dora-metrics, lean-startup] -package main - -import ( - "io" - "os" - - "github.com/boshu2/agentops/cli/embedded" - "github.com/boshu2/agentops/cli/internal/goals" - "github.com/spf13/cobra" -) - -var goalsInitNonInteractive bool -var goalsInitTemplate string - -// Type aliases for test compatibility. -type goalTemplate = goals.GoalTemplate -type goalTemplateGate = goals.GoalTemplateGate - -var validTemplateNames = goals.ValidTemplateNames - -var goalsInitCmd = &cobra.Command{ - Use: "init", - Short: "Bootstrap a new GOALS.md file", - GroupID: "management", - RunE: func(cmd *cobra.Command, args []string) error { - return goals.RunInit(goals.InitOptions{ - NonInteractive: goalsInitNonInteractive, - Template: goalsInitTemplate, - GoalsFile: resolveGoalsFile(), - JSON: goalsJSONOutput(), - DryRun: dryRun, - Stdin: os.Stdin, - TemplatesFS: embedded.TemplatesFS, - }) - }, -} - -// Thin wrappers for test compatibility. - -func buildDefaultGoalFile() *goals.GoalFile { return goals.BuildDefaultGoalFile() } -func buildInteractiveGoalFile(r io.Reader) (*goals.GoalFile, error) { - return goals.BuildInteractiveGoalFile(r) -} -func detectGates(root string) []goals.Goal { return goals.DetectGates(root) } -func autoDetectTemplate(root string) string { return goals.AutoDetectTemplate(root) } - -func loadTemplate(name string) (*goalTemplate, error) { - return goals.LoadTemplate(embedded.TemplatesFS, name) -} - -func templateGatesToGoals(tmpl *goalTemplate) []goals.Goal { - return goals.TemplateGatesToGoals(tmpl) -} - -func init() { - goalsInitCmd.Flags().BoolVar(&goalsInitNonInteractive, "non-interactive", false, "Use defaults without prompting") - goalsInitCmd.Flags().StringVar(&goalsInitTemplate, "template", "", "Goal template (go-cli, python-lib, web-app, rust-cli, generic)") - goalsCmd.AddCommand(goalsInitCmd) - - _ = goalsInitCmd.RegisterFlagCompletionFunc("template", staticCompletionFunc(templateCompletionValues()...)) -} diff --git a/cli/cmd/ao/goals_init_test.go b/cli/cmd/ao/goals_init_test.go deleted file mode 100644 index b4208c252..000000000 --- a/cli/cmd/ao/goals_init_test.go +++ /dev/null @@ -1,354 +0,0 @@ -// practices: [dora-metrics, lean-startup] -package main - -import ( - "os" - "path/filepath" - "strings" - "testing" - - "github.com/boshu2/agentops/cli/internal/goals" -) - -func TestDetectGates_CliGoMod(t *testing.T) { - root := t.TempDir() - cliDir := filepath.Join(root, "cli") - if err := os.MkdirAll(cliDir, 0o755); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(filepath.Join(cliDir, "go.mod"), []byte("module test\n"), 0o644); err != nil { - t.Fatal(err) - } - - detected := detectGates(root) - if len(detected) < 2 { - t.Fatalf("expected >=2 gates for cli/go.mod project, got %d", len(detected)) - } - - var hasGoBuild, hasGoTest bool - for _, g := range detected { - if g.ID == "go-build" && strings.Contains(g.Check, "cd cli") { - hasGoBuild = true - } - if g.ID == "go-test" && strings.Contains(g.Check, "cd cli") { - hasGoTest = true - } - } - if !hasGoBuild { - t.Error("missing go-build gate with 'cd cli' prefix") - } - if !hasGoTest { - t.Error("missing go-test gate with 'cd cli' prefix") - } -} - -func TestDetectGates_RootGoMod(t *testing.T) { - root := t.TempDir() - if err := os.WriteFile(filepath.Join(root, "go.mod"), []byte("module test\n"), 0o644); err != nil { - t.Fatal(err) - } - - detected := detectGates(root) - if len(detected) < 2 { - t.Fatalf("expected >=2 gates for root go.mod project, got %d", len(detected)) - } - - var hasGoBuild bool - for _, g := range detected { - if g.ID == "go-build" && g.Check == "go build ./..." { - hasGoBuild = true - } - } - if !hasGoBuild { - t.Error("expected root-level go-build gate (no cd cli prefix)") - } -} - -func TestDetectGates_PackageJSON(t *testing.T) { - root := t.TempDir() - if err := os.WriteFile(filepath.Join(root, "package.json"), []byte("{}"), 0o644); err != nil { - t.Fatal(err) - } - - detected := detectGates(root) - var hasNpmTest bool - for _, g := range detected { - if g.ID == "npm-test" { - hasNpmTest = true - } - } - if !hasNpmTest { - t.Error("expected npm-test gate for package.json project") - } -} - -func TestDetectGates_MultipleProjectFiles(t *testing.T) { - root := t.TempDir() - // Create package.json and Cargo.toml (no go.mod, so switch/case falls through) - if err := os.WriteFile(filepath.Join(root, "package.json"), []byte("{}"), 0o644); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(filepath.Join(root, "Cargo.toml"), []byte("[package]\n"), 0o644); err != nil { - t.Fatal(err) - } - - detected := detectGates(root) - ids := map[string]bool{} - for _, g := range detected { - ids[g.ID] = true - } - if !ids["npm-test"] { - t.Error("expected npm-test gate") - } - if !ids["cargo-test"] { - t.Error("expected cargo-test gate") - } -} - -func TestDetectGates_EmptyDir(t *testing.T) { - root := t.TempDir() - detected := detectGates(root) - if len(detected) != 0 { - t.Errorf("expected 0 gates for empty dir, got %d", len(detected)) - } -} - -func TestDetectGates_CliGoModTakesPriority(t *testing.T) { - // When both cli/go.mod and root go.mod exist, the switch/case - // matches cli/go.mod first due to case ordering. - root := t.TempDir() - cliDir := filepath.Join(root, "cli") - if err := os.MkdirAll(cliDir, 0o755); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(filepath.Join(cliDir, "go.mod"), []byte("module test/cli\n"), 0o644); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(filepath.Join(root, "go.mod"), []byte("module test\n"), 0o644); err != nil { - t.Fatal(err) - } - - detected := detectGates(root) - for _, g := range detected { - if g.ID == "go-build" && !strings.Contains(g.Check, "cd cli") { - t.Error("expected cli/go.mod to take priority, but got root-level go-build gate") - } - } -} - -func TestBuildInteractiveGoalFile_Defaults(t *testing.T) { - // Empty input — all prompts get empty strings, triggering defaults. - r := strings.NewReader("\n\n\n\n\n") - gf, err := buildInteractiveGoalFile(r) - if err != nil { - t.Fatal(err) - } - - if gf.Version != 4 { - t.Errorf("Version = %d, want 4", gf.Version) - } - if len(gf.NorthStars) == 0 { - t.Error("expected default north stars") - } - if gf.NorthStars[0] != "All checks pass on every commit" { - t.Errorf("NorthStars[0] = %q, want default", gf.NorthStars[0]) - } - if len(gf.Directives) != 1 { - t.Fatalf("Directives = %d, want 1", len(gf.Directives)) - } - if gf.Directives[0].Title != "Establish baseline" { - t.Errorf("Directive title = %q, want default", gf.Directives[0].Title) - } -} - -func TestBuildInteractiveGoalFile_CustomInput(t *testing.T) { - input := "Ship reliable software\nZero downtime, Fast deploys\nManual releases, Untested code\nAutomate CI\nEnsure every merge is validated.\n" - r := strings.NewReader(input) - gf, err := buildInteractiveGoalFile(r) - if err != nil { - t.Fatal(err) - } - - if gf.Mission != "Ship reliable software" { - t.Errorf("Mission = %q", gf.Mission) - } - if len(gf.NorthStars) != 2 { - t.Fatalf("NorthStars = %d, want 2", len(gf.NorthStars)) - } - if gf.NorthStars[0] != "Zero downtime" { - t.Errorf("NorthStars[0] = %q", gf.NorthStars[0]) - } - if gf.NorthStars[1] != "Fast deploys" { - t.Errorf("NorthStars[1] = %q", gf.NorthStars[1]) - } - if len(gf.AntiStars) != 2 { - t.Fatalf("AntiStars = %d, want 2", len(gf.AntiStars)) - } - if gf.AntiStars[0] != "Manual releases" { - t.Errorf("AntiStars[0] = %q", gf.AntiStars[0]) - } - if gf.AntiStars[1] != "Untested code" { - t.Errorf("AntiStars[1] = %q", gf.AntiStars[1]) - } - if len(gf.Directives) != 1 { - t.Fatalf("Directives = %d, want 1", len(gf.Directives)) - } - if gf.Directives[0].Title != "Automate CI" { - t.Errorf("Directive title = %q", gf.Directives[0].Title) - } - if gf.Directives[0].Description != "Ensure every merge is validated." { - t.Errorf("Directive description = %q", gf.Directives[0].Description) - } - - // ValidateGoals checks gates, not metadata — no errors expected for an empty-gates file. - if errs := goals.ValidateGoals(gf); len(errs) != 0 { - t.Errorf("expected 0 validation errors, got %d: %v", len(errs), errs) - } -} - -// --- Template tests --- - -func TestGoalsInit_Template_GoCLI(t *testing.T) { - tmpl, err := loadTemplate("go-cli") - if err != nil { - t.Fatalf("loadTemplate(go-cli): %v", err) - } - - if tmpl.Name != "go-cli" { - t.Errorf("Name = %q, want go-cli", tmpl.Name) - } - if len(tmpl.Gates) < 3 { - t.Fatalf("expected >=3 gates in go-cli template, got %d", len(tmpl.Gates)) - } - - converted := templateGatesToGoals(tmpl) - ids := map[string]bool{} - for _, g := range converted { - ids[g.ID] = true - if g.Type == "" { - t.Errorf("gate %q has empty Type", g.ID) - } - } - - for _, expected := range []string{"go-build", "go-test", "go-vet"} { - if !ids[expected] { - t.Errorf("missing expected gate %q in go-cli template", expected) - } - } -} - -func TestGoalsInit_Template_NonInteractive(t *testing.T) { - // Simulate --non-interactive + --template=go-cli: build default goal file, - // then append template gates instead of auto-detected gates. - gf := buildDefaultGoalFile() - - tmpl, err := loadTemplate("go-cli") - if err != nil { - t.Fatalf("loadTemplate: %v", err) - } - gf.Goals = append(gf.Goals, templateGatesToGoals(tmpl)...) - - // Verify the goal file has the expected structure. - if gf.Version != 4 { - t.Errorf("Version = %d, want 4", gf.Version) - } - if len(gf.Goals) != 3 { - t.Errorf("Goals = %d, want 3 (go-build, go-test, go-vet)", len(gf.Goals)) - } - - // Render to markdown and verify it produces valid output. - content := goals.RenderGoalsMD(gf) - if !strings.Contains(content, "go-build") { - t.Error("rendered markdown missing go-build gate") - } - if !strings.Contains(content, "go vet") { - t.Error("rendered markdown missing go-vet check command") - } -} - -func TestGoalsInit_AutoDetect(t *testing.T) { - tests := []struct { - name string - files map[string]string // relative path → content - wantTmpl string - }{ - { - name: "go.mod → go-cli", - files: map[string]string{"go.mod": "module test\n"}, - wantTmpl: "go-cli", - }, - { - name: "cli/go.mod → go-cli", - files: map[string]string{"cli/go.mod": "module test/cli\n"}, - wantTmpl: "go-cli", - }, - { - name: "package.json → web-app", - files: map[string]string{"package.json": "{}"}, - wantTmpl: "web-app", - }, - { - name: "pyproject.toml → python-lib", - files: map[string]string{"pyproject.toml": "[project]\n"}, - wantTmpl: "python-lib", - }, - { - name: "Cargo.toml → rust-cli", - files: map[string]string{"Cargo.toml": "[package]\n"}, - wantTmpl: "rust-cli", - }, - { - name: "empty → no template", - files: map[string]string{}, - wantTmpl: "", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - root := t.TempDir() - for rel, content := range tt.files { - full := filepath.Join(root, rel) - if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(full, []byte(content), 0o644); err != nil { - t.Fatal(err) - } - } - - got := autoDetectTemplate(root) - if got != tt.wantTmpl { - t.Errorf("autoDetectTemplate() = %q, want %q", got, tt.wantTmpl) - } - }) - } -} - -func TestGoalsInit_LoadTemplate_AllValid(t *testing.T) { - // Verify all templates load and parse without error. - for _, name := range validTemplateNames { - t.Run(name, func(t *testing.T) { - tmpl, err := loadTemplate(name) - if err != nil { - t.Fatalf("loadTemplate(%q): %v", name, err) - } - if tmpl.Name != name { - t.Errorf("Name = %q, want %q", tmpl.Name, name) - } - if len(tmpl.Gates) == 0 { - t.Error("expected at least one gate") - } - if len(tmpl.Directives) == 0 { - t.Error("expected at least one directive") - } - }) - } -} - -func TestGoalsInit_LoadTemplate_Invalid(t *testing.T) { - _, err := loadTemplate("nonexistent") - if err == nil { - t.Error("expected error for nonexistent template, got nil") - } -} diff --git a/cli/cmd/ao/goals_integration_test.go b/cli/cmd/ao/goals_integration_test.go index 44776cd34..ba5fcc872 100644 --- a/cli/cmd/ao/goals_integration_test.go +++ b/cli/cmd/ao/goals_integration_test.go @@ -2,79 +2,11 @@ package main import ( - "os" - "os/exec" "path/filepath" "strings" "testing" ) -func TestGoals_Integration_FullLifecycle(t *testing.T) { - t.Parallel() - bin := aoBinary(t) - tmp := t.TempDir() - os.MkdirAll(filepath.Join(tmp, ".git"), 0o750) - os.MkdirAll(filepath.Join(tmp, ".agents", "ao", "sessions"), 0o750) - os.MkdirAll(filepath.Join(tmp, ".agents", "ao", "goals", "baselines"), 0o750) - - // Step 1: init creates GOALS.md with --non-interactive - cmd := exec.Command(bin, "goals", "init", "--non-interactive") - cmd.Dir = tmp - out, err := cmd.CombinedOutput() - if err != nil { - t.Fatalf("goals init failed: %v\n%s", err, out) - } - if !strings.Contains(string(out), "Created") { - t.Errorf("expected 'Created' in output, got: %s", out) - } - - goalsPath := filepath.Join(tmp, "GOALS.md") - if _, statErr := os.Stat(goalsPath); statErr != nil { - t.Fatalf("GOALS.md not created: %v", statErr) - } - content, err := os.ReadFile(goalsPath) - if err != nil { - t.Fatalf("read GOALS.md: %v", err) - } - if !strings.Contains(string(content), "Establish baseline") { - t.Errorf("GOALS.md missing default directive, content:\n%s", content) - } - - // Step 2: measurement remains read-only; recommendation and apply routing - // were removed by the Cathedral Cut. - cmd = exec.Command(bin, "goals", "measure") - cmd.Dir = tmp - out, err = cmd.CombinedOutput() - if err != nil { - t.Fatalf("goals measure failed: %v\n%s", err, out) - } - if !strings.Contains(string(out), "Score:") { - t.Errorf("expected 'Score:' in measure output, got: %s", out) - } -} - -func TestGoals_Integration_InitAlreadyExists(t *testing.T) { - dir := chdirTemp(t) - - // Create existing GOALS.md - writeFile(t, filepath.Join(dir, "GOALS.md"), "# Existing\n") - - // init should fail when file already exists - _, err := captureStdout(t, func() error { - goalsInitNonInteractive = true - goalsInitTemplate = "" - defer func() { goalsInitNonInteractive = false }() - rootCmd.SetArgs([]string{"goals", "init", "--non-interactive"}) - return rootCmd.Execute() - }) - if err == nil { - t.Fatal("expected error when GOALS.md already exists, got nil") - } - if !strings.Contains(err.Error(), "already exists") { - t.Errorf("expected 'already exists' error, got: %v", err) - } -} - func TestGoals_Integration_MeasureNoGoalsFile(t *testing.T) { _ = chdirTemp(t) @@ -97,18 +29,30 @@ func TestGoals_Integration_MeasureDirectivesJSON(t *testing.T) { resetCommandState(t) setGoalsMeasureScenariosOnly(t, false) - // Create GOALS.md - _, err := captureStdout(t, func() error { - goalsInitNonInteractive = true - goalsInitTemplate = "" - defer func() { goalsInitNonInteractive = false }() - rootCmd.SetArgs([]string{"goals", "init", "--non-interactive"}) - return rootCmd.Execute() - }) - if err != nil { - t.Fatalf("goals init failed: %v", err) - } - _ = dir + writeFile(t, filepath.Join(dir, "GOALS.md"), `# Fitness Goals + +## Mission + +Measure project fitness. + +## North Stars + +- Passing checks + +## Anti-Stars + +- Hidden regressions + +## Directives + +### 1. Establish baseline + +Keep the deterministic floor green. + +**Steer:** increase + +## Gates +`) // measure --directives should output JSON with directive info out, err := captureStdout(t, func() error { diff --git a/cli/cmd/ao/goals_meta.go b/cli/cmd/ao/goals_meta.go index 26cb80291..79b2fcd34 100644 --- a/cli/cmd/ao/goals_meta.go +++ b/cli/cmd/ao/goals_meta.go @@ -11,7 +11,7 @@ import ( var goalsMetaCmd = &cobra.Command{ Use: "meta", Short: "Run and report meta-goals only", - GroupID: "management", + GroupID: "analysis", RunE: func(cmd *cobra.Command, args []string) error { return goals.RunMeta(goals.MetaOptions{ GoalsFile: resolveGoalsFile(), diff --git a/cli/cmd/ao/goals_migrate.go b/cli/cmd/ao/goals_migrate.go deleted file mode 100644 index 4125af36e..000000000 --- a/cli/cmd/ao/goals_migrate.go +++ /dev/null @@ -1,49 +0,0 @@ -// practices: [dora-metrics, lean-startup] -package main - -import ( - "github.com/boshu2/agentops/cli/internal/goals" - "github.com/spf13/cobra" -) - -var migrateToMD bool - -func init() { - migrateCmd := &cobra.Command{ - Use: "migrate", - Short: "Migrate goals to latest format", - Aliases: []string{"mg"}, - GroupID: "management", - Long: `Migrate goals between formats. - -Without flags, migrates GOALS.yaml from version 1 to version 2: - - Sets version to 2 - - Adds mission field if missing - - Sets goal type to "health" for goals without a type - - Backs up original to GOALS.yaml.v1.bak - -With --to-md, converts GOALS.yaml to GOALS.md (version 4): - - Carries over mission and all gates - - Groups goals by pillar to generate directives - - Adds default north/anti stars - - Preserves original YAML file - -Examples: - ao goals migrate # v1 YAML → v2 YAML - ao goals migrate --to-md # YAML → GOALS.md - ao goals migrate --to-md --file g.yaml # Custom source file`, - RunE: func(cmd *cobra.Command, args []string) error { - return goals.RunMigrate(goals.MigrateOptions{ - ToMD: migrateToMD, - GoalsFile: resolveGoalsFile(), - }) - }, - } - migrateCmd.Flags().BoolVar(&migrateToMD, "to-md", false, "Convert GOALS.yaml to GOALS.md format") - goalsCmd.AddCommand(migrateCmd) -} - -// directivesFromPillars delegates to goals.DirectivesFromPillars (used by tests). -func directivesFromPillars(gs []goals.Goal) []goals.Directive { - return goals.DirectivesFromPillars(gs) -} diff --git a/cli/cmd/ao/goals_migrate_test.go b/cli/cmd/ao/goals_migrate_test.go deleted file mode 100644 index 2b6397976..000000000 --- a/cli/cmd/ao/goals_migrate_test.go +++ /dev/null @@ -1,71 +0,0 @@ -// practices: [dora-metrics, lean-startup] -package main - -import ( - "testing" - - "github.com/boshu2/agentops/cli/internal/goals" -) - -func TestDirectivesFromPillars_NoPillars(t *testing.T) { - gs := []goals.Goal{ - {ID: "g1", Check: "true", Weight: 1}, - {ID: "g2", Check: "true", Weight: 1}, - } - dirs := directivesFromPillars(gs) - if len(dirs) != 1 { - t.Fatalf("expected 1 default directive, got %d", len(dirs)) - } - if dirs[0].Title != "Improve project quality" { - t.Errorf("default directive title = %q", dirs[0].Title) - } - if dirs[0].Number != 1 { - t.Errorf("default directive number = %d, want 1", dirs[0].Number) - } -} - -func TestDirectivesFromPillars_WithPillars(t *testing.T) { - gs := []goals.Goal{ - {ID: "g1", Pillar: "reliability", Check: "true", Weight: 1}, - {ID: "g2", Pillar: "security", Check: "true", Weight: 1}, - {ID: "g3", Pillar: "reliability", Check: "true", Weight: 1}, // duplicate - } - dirs := directivesFromPillars(gs) - if len(dirs) != 2 { - t.Fatalf("expected 2 directives (deduped), got %d", len(dirs)) - } - if dirs[0].Number != 1 || dirs[1].Number != 2 { - t.Errorf("numbering wrong: %d, %d", dirs[0].Number, dirs[1].Number) - } - if dirs[0].Title != "Strengthen reliability" { - t.Errorf("dirs[0].Title = %q", dirs[0].Title) - } - if dirs[1].Title != "Strengthen security" { - t.Errorf("dirs[1].Title = %q", dirs[1].Title) - } -} - -func TestDirectivesFromPillars_EmptyGoals(t *testing.T) { - dirs := directivesFromPillars([]goals.Goal{}) - if len(dirs) != 1 { - t.Fatalf("expected 1 default directive for empty goals, got %d", len(dirs)) - } - if dirs[0].Title != "Improve project quality" { - t.Errorf("default directive title = %q", dirs[0].Title) - } -} - -func TestDirectivesFromPillars_MixedEmptyAndFilled(t *testing.T) { - gs := []goals.Goal{ - {ID: "g1", Pillar: "", Check: "true", Weight: 1}, - {ID: "g2", Pillar: "performance", Check: "true", Weight: 1}, - {ID: "g3", Pillar: "", Check: "true", Weight: 1}, - } - dirs := directivesFromPillars(gs) - if len(dirs) != 1 { - t.Fatalf("expected 1 directive (only filled pillars), got %d", len(dirs)) - } - if dirs[0].Title != "Strengthen performance" { - t.Errorf("dirs[0].Title = %q", dirs[0].Title) - } -} diff --git a/cli/cmd/ao/goals_prune.go b/cli/cmd/ao/goals_prune.go deleted file mode 100644 index 6b9bd5bd7..000000000 --- a/cli/cmd/ao/goals_prune.go +++ /dev/null @@ -1,34 +0,0 @@ -// practices: [dora-metrics, lean-startup] -package main - -import ( - "github.com/boshu2/agentops/cli/internal/goals" - "github.com/spf13/cobra" -) - -// Type aliases for test compatibility. -type pruneResult = goals.PruneResult -type staleGoal = goals.StaleGoal - -var goalsPruneCmd = &cobra.Command{ - Use: "prune", - Aliases: []string{"p"}, - Short: "Remove goals referencing nonexistent files", - GroupID: "management", - RunE: func(cmd *cobra.Command, args []string) error { - return goals.RunPrune(goals.PruneOptions{ - GoalsFile: resolveGoalsFile(), - DryRun: dryRun, - JSON: goalsJSONOutput(), - }) - }, -} - -// findMissingPath delegates to goals.FindMissingPath (used by tests). -func findMissingPath(check string) string { - return goals.FindMissingPath(check) -} - -func init() { - goalsCmd.AddCommand(goalsPruneCmd) -} diff --git a/cli/cmd/ao/goals_prune_test.go b/cli/cmd/ao/goals_prune_test.go deleted file mode 100644 index d997a47ab..000000000 --- a/cli/cmd/ao/goals_prune_test.go +++ /dev/null @@ -1,267 +0,0 @@ -// practices: [dora-metrics, lean-startup] -package main - -import ( - "os" - "path/filepath" - "testing" - - "github.com/boshu2/agentops/cli/internal/goals" -) - -func TestFindMissingPath_NoPathReferences(t *testing.T) { - tests := []struct { - name string - check string - }{ - {"simple command", "echo hello"}, - {"exit code", "exit 0"}, - {"command with flags", "go test -v --count=1"}, - {"make target", "make build"}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := findMissingPath(tt.check) - if got != "" { - t.Errorf("findMissingPath(%q) = %q, want empty", tt.check, got) - } - }) - } -} - -func TestFindMissingPath_DetectsMissingScripts(t *testing.T) { - tests := []struct { - name string - check string - want string - }{ - {"scripts/ prefix", "scripts/check-build.sh", "scripts/check-build.sh"}, - {"./scripts/ prefix", "./scripts/check-build.sh", "./scripts/check-build.sh"}, - {"tests/ prefix", "tests/validate.sh", "tests/validate.sh"}, - {"./tests/ prefix", "./tests/validate.sh", "./tests/validate.sh"}, - {"hooks/ prefix", "hooks/pre-commit.sh", "hooks/pre-commit.sh"}, - {"./hooks/ prefix", "./hooks/pre-commit.sh", "./hooks/pre-commit.sh"}, - {"with args", "scripts/check-build.sh --strict", "scripts/check-build.sh"}, - {"path with extension", "lib/helpers.sh arg1", "lib/helpers.sh"}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := findMissingPath(tt.check) - if got != tt.want { - t.Errorf("findMissingPath(%q) = %q, want %q", tt.check, got, tt.want) - } - }) - } -} - -func TestFindMissingPath_ExistingFileReturnsEmpty(t *testing.T) { - dir := t.TempDir() - - // Create a script file - scriptsDir := filepath.Join(dir, "scripts") - if err := os.MkdirAll(scriptsDir, 0o755); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(filepath.Join(scriptsDir, "check-build.sh"), []byte("#!/bin/bash\nexit 0\n"), 0o755); err != nil { - t.Fatal(err) - } - - t.Chdir(dir) - - got := findMissingPath("scripts/check-build.sh --strict") - if got != "" { - t.Errorf("findMissingPath for existing file = %q, want empty", got) - } -} - -func TestFindMissingPath_TrailingShellOperators(t *testing.T) { - tests := []struct { - name string - check string - want string - }{ - {"semicolon", "scripts/run.sh;", "scripts/run.sh"}, - {"pipe", "scripts/run.sh|", "scripts/run.sh"}, - {"ampersand", "scripts/run.sh&", "scripts/run.sh"}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := findMissingPath(tt.check) - if got != tt.want { - t.Errorf("findMissingPath(%q) = %q, want %q", tt.check, got, tt.want) - } - }) - } -} - -func TestGoalsPrune_NoStaleGoals(t *testing.T) { - dir := t.TempDir() - - // Create a goals file with only non-path-referencing checks - md := `# Goals - -Mission. - -## Gates - -| ID | Check | Weight | Description | -|----|-------|--------|-------------| -| simple-gate | ` + "`echo ok`" + ` | 5 | Simple | -| exit-gate | ` + "`exit 0`" + ` | 5 | Exit | -` - goalsPath := filepath.Join(dir, "GOALS.md") - if err := os.WriteFile(goalsPath, []byte(md), 0o644); err != nil { - t.Fatal(err) - } - - t.Chdir(dir) - - oldFile := goalsFile - oldDryRun := dryRun - oldJSON := output - defer func() { - goalsFile = oldFile - dryRun = oldDryRun - output = oldJSON - }() - goalsFile = goalsPath - dryRun = false - output = "table" - - err := goalsPruneCmd.RunE(goalsPruneCmd, nil) - if err != nil { - t.Fatalf("prune returned error: %v", err) - } -} - -func TestGoalsPrune_DryRun_DetectsStale(t *testing.T) { - dir := t.TempDir() - - md := `# Goals - -Mission. - -## Gates - -| ID | Check | Weight | Description | -|----|-------|--------|-------------| -| good-gate | ` + "`echo ok`" + ` | 5 | Good | -| stale-gate | ` + "`scripts/nonexistent.sh`" + ` | 5 | Stale | -` - goalsPath := filepath.Join(dir, "GOALS.md") - if err := os.WriteFile(goalsPath, []byte(md), 0o644); err != nil { - t.Fatal(err) - } - - t.Chdir(dir) - - oldFile := goalsFile - oldDryRun := dryRun - oldJSON := output - defer func() { - goalsFile = oldFile - dryRun = oldDryRun - output = oldJSON - }() - goalsFile = goalsPath - dryRun = true - output = "table" - - err := goalsPruneCmd.RunE(goalsPruneCmd, nil) - if err != nil { - t.Fatalf("prune dry-run returned error: %v", err) - } - - // Verify the file was NOT modified (dry-run) - gf, err := goals.LoadGoals(goalsPath) - if err != nil { - t.Fatalf("LoadGoals: %v", err) - } - if len(gf.Goals) != 2 { - t.Errorf("goals count = %d, want 2 (dry-run should not modify)", len(gf.Goals)) - } -} - -func TestGoalsPrune_RemovesStaleGoals(t *testing.T) { - dir := t.TempDir() - - md := `# Goals - -Mission. - -## Gates - -| ID | Check | Weight | Description | -|----|-------|--------|-------------| -| good-gate | ` + "`echo ok`" + ` | 5 | Good | -| stale-gate | ` + "`scripts/nonexistent.sh`" + ` | 5 | Stale | -` - goalsPath := filepath.Join(dir, "GOALS.md") - if err := os.WriteFile(goalsPath, []byte(md), 0o644); err != nil { - t.Fatal(err) - } - - t.Chdir(dir) - - oldFile := goalsFile - oldDryRun := dryRun - oldJSON := output - defer func() { - goalsFile = oldFile - dryRun = oldDryRun - output = oldJSON - }() - goalsFile = goalsPath - dryRun = false - output = "table" - - err := goalsPruneCmd.RunE(goalsPruneCmd, nil) - if err != nil { - t.Fatalf("prune returned error: %v", err) - } - - // Verify the stale goal was removed - gf, err := goals.LoadGoals(goalsPath) - if err != nil { - t.Fatalf("LoadGoals: %v", err) - } - if len(gf.Goals) != 1 { - t.Fatalf("goals count = %d, want 1 (stale should be removed)", len(gf.Goals)) - } - if gf.Goals[0].ID != "good-gate" { - t.Errorf("remaining goal = %q, want good-gate", gf.Goals[0].ID) - } -} - -func TestGoalsPrune_MissingGoalsFile(t *testing.T) { - oldFile := goalsFile - defer func() { goalsFile = oldFile }() - - goalsFile = "/nonexistent/GOALS.md" - - err := goalsPruneCmd.RunE(goalsPruneCmd, nil) - if err == nil { - t.Fatal("expected error for missing goals file") - } -} - -func TestGoalsPrune_CmdAttributes(t *testing.T) { - if goalsPruneCmd.Use != "prune" { - t.Errorf("Use = %q, want prune", goalsPruneCmd.Use) - } - if goalsPruneCmd.GroupID != "management" { - t.Errorf("GroupID = %q, want management", goalsPruneCmd.GroupID) - } - found := false - for _, a := range goalsPruneCmd.Aliases { - if a == "p" { - found = true - } - } - if !found { - t.Error("expected alias 'p' for prune command") - } -} diff --git a/cli/cmd/ao/goals_scenarios.go b/cli/cmd/ao/goals_scenarios.go index 734242b2e..f5a9909d2 100644 --- a/cli/cmd/ao/goals_scenarios.go +++ b/cli/cmd/ao/goals_scenarios.go @@ -2,8 +2,6 @@ package main import ( - "fmt" - "github.com/boshu2/agentops/cli/internal/goals" "github.com/spf13/cobra" ) @@ -11,22 +9,18 @@ import ( var ( scenariosDirective int scenariosDirectiveID string - scenariosCreate string - scenariosThreshold float64 - scenariosStatus string - scenariosSource string scenariosLint bool scenariosStrict bool ) var goalsScenariosCmd = &cobra.Command{ Use: "scenarios", - Short: "List or create the holdout scenarios linked to GOALS.md directives", + Short: "Inspect holdout scenarios linked to GOALS.md directives", GroupID: "analysis", Args: cobra.NoArgs, - Long: `List or create the executable-spec scenarios linked to GOALS.md directives. + Long: `Inspect the executable-spec scenarios linked to GOALS.md directives. -Listing is read-only: directive membership comes from each directive's +Directive membership comes from each directive's "**Scenarios:**" attribute line; scenario content is resolved from spec/scenarios/ then .agents/holdout/ (see docs/adr/ADR-0003). @@ -34,18 +28,11 @@ spec/scenarios/ then .agents/holdout/ (see docs/adr/ADR-0003). ao goals scenarios --directive 2 filter to directive #2 ao goals scenarios --directive-id d-foo filter to a stable directive ID ao goals scenarios -o json machine-readable directive→scenarios map - -Creating scaffolds a promoted spec scenario and links it bidirectionally: - - ao goals scenarios --create "" --directive 2 - -The new scenario JSON carries the directive's stable directive_id, and the -directive's "**Scenarios:**" line gains the scenario ID via the non-lossy -patcher (no other byte of GOALS.md changes).`, + ao goals scenarios --lint report link-graph defects`, RunE: runGoalsScenarios, } -// runGoalsScenarios dispatches between link lint, --create, and the listing. +// runGoalsScenarios dispatches between read-only link lint and listing. func runGoalsScenarios(cmd *cobra.Command, _ []string) error { if scenariosLint { return goals.RunLint(goals.LintOptions{ @@ -55,21 +42,6 @@ func runGoalsScenarios(cmd *cobra.Command, _ []string) error { Stdout: cmd.OutOrStdout(), }) } - if scenariosCreate != "" { - if scenariosDirective == 0 { - return fmt.Errorf("--create requires --directive to name the target directive") - } - return goals.RunScenarioCreate(goals.ScenarioCreateOptions{ - GoalsFile: resolveGoalsFile(), - DirectiveNum: scenariosDirective, - Goal: scenariosCreate, - Threshold: scenariosThreshold, - Status: scenariosStatus, - Source: scenariosSource, - JSON: goalsJSONOutput(), - Stdout: cmd.OutOrStdout(), - }) - } return goals.RunScenarios(goals.ScenariosOptions{ GoalsFile: resolveGoalsFile(), DirectiveNum: scenariosDirective, @@ -81,15 +53,9 @@ func runGoalsScenarios(cmd *cobra.Command, _ []string) error { } func init() { - goalsScenariosCmd.Flags().IntVar(&scenariosDirective, "directive", 0, "Directive display number (filter when listing, target when creating)") + goalsScenariosCmd.Flags().IntVar(&scenariosDirective, "directive", 0, "Filter by directive display number") goalsScenariosCmd.Flags().StringVar(&scenariosDirectiveID, "directive-id", "", "Filter listing to one directive by stable Directive ID") - goalsScenariosCmd.Flags().StringVar(&scenariosCreate, "create", "", "Create a scenario from this goal description and link it to --directive") - goalsScenariosCmd.Flags().Float64Var(&scenariosThreshold, "threshold", 0.8, "Satisfaction threshold for a created scenario") - goalsScenariosCmd.Flags().StringVar(&scenariosStatus, "status", "draft", "Status for a created scenario (active, draft, retired)") - goalsScenariosCmd.Flags().StringVar(&scenariosSource, "source", "human", "Source for a created scenario (human, agent, prod-telemetry)") goalsScenariosCmd.Flags().BoolVar(&scenariosLint, "lint", false, "Lint the directive↔scenario link graph instead of listing") goalsScenariosCmd.Flags().BoolVar(&scenariosStrict, "strict", false, "With --lint, exit non-zero on warnings as well as errors") - _ = goalsScenariosCmd.RegisterFlagCompletionFunc("status", staticCompletionFunc("active", "draft", "retired")) - _ = goalsScenariosCmd.RegisterFlagCompletionFunc("source", staticCompletionFunc("human", "agent", "prod-telemetry")) goalsCmd.AddCommand(goalsScenariosCmd) } diff --git a/cli/cmd/ao/goals_test.go b/cli/cmd/ao/goals_test.go index 07a668710..5fbb90a5c 100644 --- a/cli/cmd/ao/goals_test.go +++ b/cli/cmd/ao/goals_test.go @@ -24,7 +24,7 @@ func TestGoalsCmd_HasExpectedSubcommands(t *testing.T) { expected := []string{ "measure", "validate", "drift", "history", "export", - "init", "add", "prune", "migrate", "meta", + "meta", "render", "scenarios", "trace", } for _, name := range expected { if !subNames[name] { @@ -44,7 +44,7 @@ func TestGoalsCmd_HasGroups(t *testing.T) { ids[g.ID] = true } - for _, want := range []string{"measurement", "analysis", "management"} { + for _, want := range []string{"measurement", "analysis"} { if !ids[want] { t.Errorf("missing group %q", want) } diff --git a/cli/cmd/ao/handoff.go b/cli/cmd/ao/handoff.go index f693522ad..99345b111 100644 --- a/cli/cmd/ao/handoff.go +++ b/cli/cmd/ao/handoff.go @@ -127,13 +127,13 @@ func writeHandoffArtifact(cwd string, artifact *handoffArtifact, data []byte) (s return "", fmt.Errorf("create handoff temporary file: %w", err) } tmpName := tmp.Name() - defer os.Remove(tmpName) + defer func() { _ = os.Remove(tmpName) }() if _, err := tmp.Write(data); err != nil { - tmp.Close() + _ = tmp.Close() return "", fmt.Errorf("write handoff: %w", err) } if err := tmp.Sync(); err != nil { - tmp.Close() + _ = tmp.Close() return "", fmt.Errorf("flush handoff: %w", err) } if err := tmp.Close(); err != nil { diff --git a/cli/cmd/ao/harvest.go b/cli/cmd/ao/harvest.go deleted file mode 100644 index a01b8c5c8..000000000 --- a/cli/cmd/ao/harvest.go +++ /dev/null @@ -1,357 +0,0 @@ -//go:build flywheel - -// practices: [wiki-knowledge-surface, lean-startup] -package main - -import ( - "encoding/json" - "errors" - "fmt" - "os" - "path/filepath" - "strconv" - "strings" - "time" - - "github.com/boshu2/agentops/cli/internal/harvest" - "github.com/boshu2/agentops/cli/internal/lockfile" - "github.com/boshu2/agentops/cli/internal/wiki" - "github.com/spf13/cobra" -) - -var ( - harvestRootsFlag string - harvestOutputDir string - harvestPromoteTo string - harvestMinConfidence float64 - harvestInclude string - harvestQuiet bool - harvestMaxFileSize int64 - harvestMaxPromotions int -) - -var harvestCmd = &cobra.Command{ - Use: "harvest", - Short: "Sweep all rigs, extract and deduplicate cross-rig knowledge", - Long: `Walks all .agents/ directories across the workspace, extracts learnings, -patterns, and research, deduplicates across rigs, and promotes high-value -items to the global knowledge hub (~/.agents/learnings/).`, - RunE: runHarvest, -} - -func init() { - harvestCmd.GroupID = "knowledge" - rootCmd.AddCommand(harvestCmd) - - harvestCmd.Flags().StringVar(&harvestRootsFlag, "roots", "", - "Base directories to scan (comma-separated) (default ~/gt)") - harvestCmd.Flags().StringVar(&harvestOutputDir, "output-dir", ".agents/harvest", - "Directory for harvest catalog output") - harvestCmd.Flags().StringVar(&harvestPromoteTo, "promote-to", "", - "Promotion destination for high-value artifacts (default ~/.agents/learnings)") - harvestCmd.Flags().Float64Var(&harvestMinConfidence, "min-confidence", 0.5, - "Minimum confidence for promotion") - harvestCmd.Flags().StringVar(&harvestInclude, "include", "learnings,patterns,research", - "Artifact types to include (comma-separated)") - harvestCmd.Flags().BoolVar(&harvestQuiet, "quiet", false, "Suppress progress output") - harvestCmd.Flags().Int64Var(&harvestMaxFileSize, "max-file-size", 1048576, - "Skip files larger than this (bytes)") - harvestCmd.Flags().IntVar(&harvestMaxPromotions, "max-promotions", harvest.DefaultMaxPromotions, - "Advisory volume gate: emit a stderr WARN when promotions exceed this count "+ - "(0 disables; AO_MAX_PROMOTIONS env var as fallback). Never blocks.") -} - -// resolveMaxPromotionsThreshold returns the effective volume-gate threshold, -// honoring the precedence: explicit --max-promotions flag > AO_MAX_PROMOTIONS -// env var > default. A non-positive resolved value disables the gate. -func resolveMaxPromotionsThreshold(cmd *cobra.Command) int { - if cmd != nil { - if f := cmd.Flags().Lookup("max-promotions"); f != nil && f.Changed { - return harvestMaxPromotions - } - } - if envVal := strings.TrimSpace(os.Getenv("AO_MAX_PROMOTIONS")); envVal != "" { - if n, err := strconv.Atoi(envVal); err == nil { - return n - } - } - return harvestMaxPromotions -} - -// failIfDreamHoldsLock refuses to proceed when a live external writer -// holds the legacy overnight/dream lock. The overnight/dream commands -// were retired (soc-2rtm0), but an out-of-band Dream/GC run may still -// stamp .agents/overnight/run.lock while writing to ~/.agents/learnings/ -// via harvest.Promote; concurrent writes there are outside any -// checkpoint boundary and would silently corrupt the global hub. This -// stays as a defensive guard against a live lock owner. -// -// Strategy: -// - Look for .agents/overnight/run.lock at the repo root. -// - If missing or if lockfile.IsStale returns true, proceed. -// - Otherwise, read the PID from the lock file; if that PID is -// still alive (via lockfile.ProcessAlive), refuse. -// -// Errors reading the lock file (other than ENOENT) are logged as a -// warning but do not block harvest — the worst case of racing a writer -// is strictly better than hard-failing harvest on a corrupt lock file. -// -// This is the pm-011 fix from the Dream nightly compounder pre-mortem. -func failIfDreamHoldsLock(cwd string) error { - lockPath := filepath.Join(wiki.AgentsDirIn(cwd), "overnight", "run.lock") - - // Cheap freshness check first: if the lock is stale (old mtime - // AND dead/zero PID), IsStale returns true and we can proceed - // without any further work. - stale, err := lockfile.IsStale(lockPath, 12*time.Hour) - if err != nil { - // Stat failed for a reason other than ENOENT (ENOENT is - // reported as stale=false, err=nil). Log and proceed — - // harvest must not hard-fail on a lock-file read error. - fmt.Fprintf(os.Stderr, "harvest: warning: could not stat overnight lock %s: %v\n", lockPath, err) - return nil - } - if stale { - return nil - } - - // Not stale means one of: - // 1. lock file does not exist -> proceed - // 2. lock mtime is within maxAge -> check PID - // 3. lock references a live PID -> refuse - // IsStale collapses (1) and (2) into the same "not stale" - // return, so distinguish them with an explicit stat. - if _, statErr := os.Stat(lockPath); statErr != nil { - if errors.Is(statErr, os.ErrNotExist) { - return nil - } - fmt.Fprintf(os.Stderr, "harvest: warning: could not stat overnight lock %s: %v\n", lockPath, statErr) - return nil - } - - pid := lockfile.ReadPID(lockPath) - if pid <= 0 { - // Malformed or empty lock file — treat as no lock, don't - // block harvest. A future stale-lock cleanup collapses this - // via IsStale. - return nil - } - if !lockfile.ProcessAlive(pid) { - // Lock owner is dead; safe to proceed. - return nil - } - - return fmt.Errorf("ao harvest: refusing to run while a live writer holds the overnight lock (pid %d). Wait for that run to finish before harvesting", pid) -} - -func runHarvest(cmd *cobra.Command, args []string) error { - cwd, err := os.Getwd() - if err != nil { - return fmt.Errorf("getting working directory: %w", err) - } - if err := failIfDreamHoldsLock(cwd); err != nil { - return err - } - - // Resolve home-relative defaults at runtime so generated docs don't embed absolute paths. - applyHarvestRuntimeDefaults() - roots := harvestCSVList(harvestRootsFlag) - includeDirs := harvestCSVList(harvestInclude) - opts := newHarvestWalkOptions(roots, includeDirs) - - rigs, discoveryWarnings, err := harvest.DiscoverRigsWithWarnings(opts) - if err != nil { - return fmt.Errorf("discovering rigs: %w", err) - } - - if !harvestQuiet { - fmt.Printf("Discovered %d rigs\n", len(rigs)) - } - - allArtifacts, warnings, totalCandidateFiles := collectHarvestArtifacts(rigs, opts, discoveryWarnings) - catalog := newHarvestCatalog(allArtifacts, rigs, warnings, roots, includeDirs, totalCandidateFiles) - - if !harvestQuiet { - fmt.Printf("Extracted %d artifacts (%d unique, %d duplicate excess, %d promotion candidates, %d warnings)\n", - catalog.Summary.ArtifactsExtracted, - catalog.Summary.UniqueArtifacts, - catalog.Summary.DuplicateExcess, - catalog.Summary.PromotionCandidates, - catalog.Summary.WarningCount, - ) - } - - // Advisory volume gate (soc-f2q4 / 4-B): emit a stderr WARN when the - // promotion count exceeds the configured threshold. Never blocks — the - // 2,638-promotion drain from a legitimate post-cleanup run would have - // been falsely blocked by a hard gate, so this is signal-only. - threshold := resolveMaxPromotionsThreshold(cmd) - harvest.EmitVolumeGateWarning(catalog, threshold, os.Stderr) - - promoted, err := promoteHarvestCatalog(catalog) - if err != nil { - return err - } - catalog.PromotionCount = promoted - - return outputHarvestCatalog(catalog, harvestOutputDir, promoted) -} - -func applyHarvestRuntimeDefaults() { - if harvestRootsFlag == "" { - home, _ := os.UserHomeDir() - defaultRoots := []string{filepath.Join(home, "gt")} - // Include Claude project directories if they exist - claudeProjects := filepath.Join(home, ".claude", "projects") - if info, err := os.Stat(claudeProjects); err == nil && info.IsDir() { - defaultRoots = append(defaultRoots, claudeProjects) - } - harvestRootsFlag = strings.Join(defaultRoots, ",") - } - if harvestPromoteTo == "" { - home, _ := os.UserHomeDir() - harvestPromoteTo = filepath.Join(wiki.AgentsDirIn(home), "learnings") - } -} - -func harvestCSVList(value string) []string { - items := strings.Split(value, ",") - for i := range items { - items[i] = strings.TrimSpace(items[i]) - } - return items -} - -// newHarvestWalkOptions builds discovery options for `ao harvest`. SkipGlobalHub -// is set to true so harvest does not re-walk ~/.agents/ as a source rig, which -// would re-extract its own promote-to output and create a feedback loop. -func newHarvestWalkOptions(roots, includeDirs []string) harvest.WalkOptions { - return harvest.WalkOptions{ - Roots: roots, - MaxFileSize: harvestMaxFileSize, - SkipDirs: harvest.DefaultWalkOptions().SkipDirs, - IncludeDirs: includeDirs, - SkipGlobalHub: true, - } -} - -func collectHarvestArtifacts( - rigs []harvest.RigInfo, - opts harvest.WalkOptions, - discoveryWarnings []harvest.HarvestWarning, -) ([]harvest.Artifact, []harvest.HarvestWarning, int) { - var allArtifacts []harvest.Artifact - warnings := append([]harvest.HarvestWarning{}, discoveryWarnings...) - for _, warning := range discoveryWarnings { - printHarvestWarning(warning) - } - - totalCandidateFiles := 0 - for _, rig := range rigs { - result := harvest.ExtractArtifactsWithStats(rig, opts) - totalCandidateFiles += result.CandidateFiles - for _, warning := range result.Warnings { - printHarvestWarning(warning) - } - warnings = append(warnings, result.Warnings...) - allArtifacts = append(allArtifacts, result.Artifacts...) - } - return allArtifacts, warnings, totalCandidateFiles -} - -func newHarvestCatalog( - allArtifacts []harvest.Artifact, - rigs []harvest.RigInfo, - warnings []harvest.HarvestWarning, - roots []string, - includeDirs []string, - totalCandidateFiles int, -) *harvest.Catalog { - catalog := harvest.BuildCatalog(allArtifacts, harvestMinConfidence) - catalog.Roots = append([]string{}, roots...) - catalog.IncludeDirs = append([]string{}, includeDirs...) - catalog.PromoteTo = harvestPromoteTo - catalog.MinConfidence = harvestMinConfidence - catalog.DryRun = GetDryRun() - catalog.Rigs = append([]harvest.RigInfo{}, rigs...) - catalog.Warnings = append([]harvest.HarvestWarning{}, warnings...) - catalog.RigsScanned = len(rigs) - catalog.TotalFiles = totalCandidateFiles - catalog.Timestamp = time.Now().UTC() - catalog.Summary.WarningCount = len(catalog.Warnings) - return catalog -} - -func promoteHarvestCatalog(catalog *harvest.Catalog) (int, error) { - if GetDryRun() { - return 0, nil - } - promoted, err := harvest.Promote(catalog, harvestPromoteTo, false) - if err != nil { - return 0, fmt.Errorf("promoting artifacts: %w", err) - } - return promoted, nil -} - -func outputHarvestCatalog(catalog *harvest.Catalog, outputDir string, promoted int) error { - if err := harvest.WriteCatalog(outputDir, catalog); err != nil { - return fmt.Errorf("writing catalog: %w", err) - } - if GetOutput() == "json" { - data, marshalErr := json.MarshalIndent(catalog, "", " ") - if marshalErr != nil { - return fmt.Errorf("marshaling catalog: %w", marshalErr) - } - fmt.Println(string(data)) - return nil - } - if !harvestQuiet { - fmt.Printf("Catalog written to %s\n", outputDir) - if GetDryRun() { - fmt.Println("Dry run: no artifacts promoted") - } else { - fmt.Printf("Promoted %d artifacts to %s\n", promoted, harvestPromoteTo) - } - printExclusionReport(catalog) - } - VerbosePrintf("Rigs scanned: %d, Total files: %d\n", catalog.RigsScanned, catalog.TotalFiles) - return nil -} - -// printExclusionReport surfaces the count of artifacts that cleared dedup -// but did NOT make the confidence threshold, plus the top-5 near-misses. -// This replaces silent drops with actionable visibility so the user can -// decide whether to lower --min-confidence. -func printExclusionReport(catalog *harvest.Catalog) { - excluded := len(catalog.ExcludedCandidates) - if excluded == 0 { - return - } - fmt.Printf("Excluded %d candidate(s) under confidence threshold %.2f\n", excluded, catalog.MinConfidence) - near := catalog.TopExcludedNearMiss(5) - if len(near) == 0 { - return - } - fmt.Println(" Top near-miss candidates (closest to threshold):") - for _, a := range near { - title := a.Title - if title == "" { - title = a.ID - } - fmt.Printf(" %.2f %s (%s)\n", a.Confidence, title, a.Type) - } -} - -func printHarvestWarning(warning harvest.HarvestWarning) { - fmt.Fprintf(os.Stderr, "harvest: warning: %s: %s\n", warning.Stage, warning.Message) -} - -// duplicateArtifactCount returns the total number of duplicate artifacts -// (excluding the kept winner from each group). -func duplicateArtifactCount(cat *harvest.Catalog) int { - count := 0 - for _, dg := range cat.Duplicates { - count += dg.Count - 1 - } - return count -} diff --git a/cli/cmd/ao/harvest_test.go b/cli/cmd/ao/harvest_test.go deleted file mode 100644 index 7a833a10d..000000000 --- a/cli/cmd/ao/harvest_test.go +++ /dev/null @@ -1,851 +0,0 @@ -//go:build flywheel - -// practices: [wiki-knowledge-surface, lean-startup] -package main - -import ( - "encoding/json" - "io" - "os" - "path/filepath" - "strconv" - "strings" - "testing" - "time" - - "github.com/boshu2/agentops/cli/internal/harvest" -) - -func TestHarvestCmd_Flags(t *testing.T) { - flags := harvestCmd.Flags() - - tests := []struct { - name string - flagName string - }{ - {"roots flag exists", "roots"}, - {"output-dir flag exists", "output-dir"}, - {"promote-to flag exists", "promote-to"}, - {"min-confidence flag exists", "min-confidence"}, - {"include flag exists", "include"}, - {"quiet flag exists", "quiet"}, - {"max-file-size flag exists", "max-file-size"}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - f := flags.Lookup(tt.flagName) - if f == nil { - t.Fatalf("flag %q not found on harvestCmd", tt.flagName) - } - }) - } - - // Check specific defaults. - if f := flags.Lookup("output-dir"); f.DefValue != ".agents/harvest" { - t.Errorf("output-dir default = %q, want %q", f.DefValue, ".agents/harvest") - } - if f := flags.Lookup("min-confidence"); f.DefValue != "0.5" { - t.Errorf("min-confidence default = %q, want %q", f.DefValue, "0.5") - } - if f := flags.Lookup("include"); f.DefValue != "learnings,patterns,research" { - t.Errorf("include default = %q, want %q", f.DefValue, "learnings,patterns,research") - } - if f := flags.Lookup("max-file-size"); f.DefValue != "1048576" { - t.Errorf("max-file-size default = %q, want %q", f.DefValue, "1048576") - } - if f := flags.Lookup("quiet"); f.DefValue != "false" { - t.Errorf("quiet default = %q, want %q", f.DefValue, "false") - } - - // Verify roots and promote-to use empty defaults (resolved at runtime to avoid - // embedding absolute home paths in generated docs). - if f := flags.Lookup("roots"); f.DefValue != "" { - t.Errorf("roots default = %q, want empty (resolved at runtime)", f.DefValue) - } - if f := flags.Lookup("promote-to"); f.DefValue != "" { - t.Errorf("promote-to default = %q, want empty (resolved at runtime)", f.DefValue) - } -} - -// TestNewHarvestWalkOptions_SkipGlobalHubDefault is a regression guard for the -// 2026-04-30 feedback-loop fix: ao harvest must NOT re-walk ~/.agents/ as a -// source rig, because doing so re-extracts its own promote-to output. Without -// SkipGlobalHub: true on harvest's WalkOptions, each harvest pass produces -// fragments-of-fragments with degenerate filenames; the global hub at -// ~/.agents/learnings grew to 114k files / 460MB before this gate was added. -func TestNewHarvestWalkOptions_SkipGlobalHubDefault(t *testing.T) { - opts := newHarvestWalkOptions([]string{"/tmp/fake-root"}, []string{"learnings"}) - - if !opts.SkipGlobalHub { - t.Errorf("newHarvestWalkOptions().SkipGlobalHub = false, want true (feedback-loop regression)") - } - - if len(opts.Roots) != 1 || opts.Roots[0] != "/tmp/fake-root" { - t.Errorf("Roots = %v, want [/tmp/fake-root]", opts.Roots) - } - if len(opts.IncludeDirs) != 1 || opts.IncludeDirs[0] != "learnings" { - t.Errorf("IncludeDirs = %v, want [learnings]", opts.IncludeDirs) - } - if opts.MaxFileSize != harvestMaxFileSize { - t.Errorf("MaxFileSize = %d, want %d", opts.MaxFileSize, harvestMaxFileSize) - } - if len(opts.SkipDirs) == 0 { - t.Errorf("SkipDirs is empty, want defaults from harvest.DefaultWalkOptions()") - } -} - -func TestHarvestCSVListTrimsEntries(t *testing.T) { - got := harvestCSVList(" learnings, patterns ,research ") - want := []string{"learnings", "patterns", "research"} - if len(got) != len(want) { - t.Fatalf("harvestCSVList length = %d, want %d (%v)", len(got), len(want), got) - } - for i := range want { - if got[i] != want[i] { - t.Fatalf("harvestCSVList[%d] = %q, want %q", i, got[i], want[i]) - } - } -} - -func TestRunHarvest_DryRun(t *testing.T) { - tmp := t.TempDir() - t.Setenv("HOME", tmp) - - // Create a rig with .agents/learnings containing a markdown file. - rigDir := filepath.Join(tmp, "myproject", ".agents", "learnings") - if err := os.MkdirAll(rigDir, 0o755); err != nil { - t.Fatal(err) - } - content := "---\ntitle: Test Learning\nconfidence: 0.8\n---\n\n# Test Learning\n\nSome content here.\n" - if err := os.WriteFile(filepath.Join(rigDir, "2026-03-29-test.md"), []byte(content), 0o644); err != nil { - t.Fatal(err) - } - - // Set up output directory inside tmp. - outputDir := filepath.Join(tmp, "harvest-output") - promoteDir := filepath.Join(tmp, "promoted") - - // Save and restore global state. - origRoots := harvestRootsFlag - origOutput := harvestOutputDir - origPromote := harvestPromoteTo - origQuiet := harvestQuiet - origDryRun := dryRun - origMinConf := harvestMinConfidence - origInclude := harvestInclude - origMaxSize := harvestMaxFileSize - t.Cleanup(func() { - harvestRootsFlag = origRoots - harvestOutputDir = origOutput - harvestPromoteTo = origPromote - harvestQuiet = origQuiet - dryRun = origDryRun - harvestMinConfidence = origMinConf - harvestInclude = origInclude - harvestMaxFileSize = origMaxSize - }) - - harvestRootsFlag = tmp - harvestOutputDir = outputDir - harvestPromoteTo = promoteDir - harvestQuiet = true - dryRun = true - harvestMinConfidence = 0.5 - harvestInclude = "learnings,patterns,research" - harvestMaxFileSize = 1048576 - - if err := runHarvest(harvestCmd, nil); err != nil { - t.Fatalf("runHarvest returned error: %v", err) - } - - // Catalog should be written. - latestPath := filepath.Join(outputDir, "latest.json") - if _, err := os.Stat(latestPath); os.IsNotExist(err) { - t.Fatalf("expected catalog at %s, not found", latestPath) - } - - // Read and verify catalog contains artifacts. - data, err := os.ReadFile(latestPath) - if err != nil { - t.Fatalf("reading catalog: %v", err) - } - var cat harvest.Catalog - if err := json.Unmarshal(data, &cat); err != nil { - t.Fatalf("unmarshaling catalog: %v", err) - } - if len(cat.Artifacts) == 0 { - t.Error("expected at least one artifact in catalog") - } - if cat.RigsScanned == 0 { - t.Error("expected RigsScanned > 0") - } - if got := cat.Summary.ArtifactsExtracted; got != len(cat.Artifacts) { - t.Errorf("summary.artifacts_extracted = %d, want %d", got, len(cat.Artifacts)) - } - if got := cat.Summary.PromotionCandidates; got != len(cat.Promoted) { - t.Errorf("summary.promotion_candidates = %d, want %d", got, len(cat.Promoted)) - } - if got := cat.Summary.WarningCount; got != len(cat.Warnings) { - t.Errorf("summary.warning_count = %d, want %d", got, len(cat.Warnings)) - } - if got := cat.TotalFiles; got != 1 { - t.Errorf("total_files = %d, want 1 candidate file", got) - } - if !cat.DryRun { - t.Error("expected dry_run=true in catalog") - } - - // Promotion directory should NOT exist (dry run). - if _, err := os.Stat(promoteDir); !os.IsNotExist(err) { - t.Errorf("promote dir %s should not exist in dry-run mode", promoteDir) - } -} - -func TestRunHarvest_JSONOutput(t *testing.T) { - tmp := t.TempDir() - t.Setenv("HOME", tmp) - - // Create a rig with a learning. - rigDir := filepath.Join(tmp, "proj", ".agents", "learnings") - if err := os.MkdirAll(rigDir, 0o755); err != nil { - t.Fatal(err) - } - content := "---\ntitle: JSON Test\nconfidence: 0.9\n---\n\n# JSON Test\n\nContent.\n" - if err := os.WriteFile(filepath.Join(rigDir, "2026-03-29-json.md"), []byte(content), 0o644); err != nil { - t.Fatal(err) - } - - // Save and restore global state. - origRoots := harvestRootsFlag - origOutput := output - origQuiet := harvestQuiet - origDryRun := dryRun - origMinConf := harvestMinConfidence - origInclude := harvestInclude - origMaxSize := harvestMaxFileSize - origOutputDir := harvestOutputDir - origPromote := harvestPromoteTo - t.Cleanup(func() { - harvestRootsFlag = origRoots - output = origOutput - harvestQuiet = origQuiet - dryRun = origDryRun - harvestMinConfidence = origMinConf - harvestInclude = origInclude - harvestMaxFileSize = origMaxSize - harvestOutputDir = origOutputDir - harvestPromoteTo = origPromote - }) - - harvestRootsFlag = tmp - output = "json" - harvestQuiet = true - dryRun = true - harvestMinConfidence = 0.5 - harvestInclude = "learnings,patterns,research" - harvestMaxFileSize = 1048576 - harvestOutputDir = filepath.Join(tmp, "out") - harvestPromoteTo = filepath.Join(tmp, "promoted") - - // Capture stdout. - origStdout := os.Stdout - r, w, err := os.Pipe() - if err != nil { - t.Fatal(err) - } - os.Stdout = w - - runErr := runHarvest(harvestCmd, nil) - - w.Close() - os.Stdout = origStdout - - captured, _ := io.ReadAll(r) - - if runErr != nil { - t.Fatalf("runHarvest returned error: %v", runErr) - } - - // Verify valid JSON. - var cat harvest.Catalog - if err := json.Unmarshal(captured, &cat); err != nil { - t.Fatalf("stdout is not valid JSON: %v\nGot: %s", err, string(captured)) - } - - if len(cat.Artifacts) == 0 { - t.Error("expected at least one artifact in JSON output") - } - - if cat.RigsScanned == 0 { - t.Error("expected RigsScanned > 0 in JSON output") - } - latestPath := filepath.Join(harvestOutputDir, "latest.json") - if _, err := os.Stat(latestPath); err != nil { - t.Fatalf("expected latest.json to be written in JSON mode: %v", err) - } -} - -func TestRunHarvest_JSONOutputPreservesSideEffects(t *testing.T) { - fixture := setupHarvestJSONSideEffectsFixture(t) - configureHarvestJSONSideEffectsFlags(t, fixture) - captured := runHarvestWithCapturedStdout(t) - cat := parseHarvestJSONCatalog(t, captured) - - assertHarvestJSONSideEffectCatalog(t, cat) - assertHarvestJSONSideEffectPersistence(t, fixture) - assertHarvestJSONSideEffectPromotion(t, fixture, cat) -} - -type harvestJSONSideEffectsFixture struct { - tmp string - sourceName string - outputDir string - promoteTo string -} - -func setupHarvestJSONSideEffectsFixture(t *testing.T) harvestJSONSideEffectsFixture { - t.Helper() - - tmp := t.TempDir() - t.Setenv("HOME", tmp) - - rigDir := filepath.Join(tmp, "proj", ".agents", "learnings") - if err := os.MkdirAll(rigDir, 0o755); err != nil { - t.Fatal(err) - } - - sourceName := "2026-04-10-json-promote.md" - valid := "---\ntitle: JSON Promote\nconfidence: 0.9\nmaturity: provisional\nutility: 0.8\n---\n\n# JSON Promote\n\nContent.\n" - if err := os.WriteFile(filepath.Join(rigDir, sourceName), []byte(valid), 0o644); err != nil { - t.Fatal(err) - } - invalid := "---\nkey:\n\tvalue_with_tab: broken\n---\n" - if err := os.WriteFile(filepath.Join(rigDir, "2026-04-10-bad.md"), []byte(invalid), 0o644); err != nil { - t.Fatal(err) - } - - return harvestJSONSideEffectsFixture{ - tmp: tmp, - sourceName: sourceName, - outputDir: filepath.Join(tmp, "out"), - promoteTo: filepath.Join(tmp, "promoted"), - } -} - -func configureHarvestJSONSideEffectsFlags(t *testing.T, fixture harvestJSONSideEffectsFixture) { - t.Helper() - - origRoots := harvestRootsFlag - origOutput := output - origJsonFlag := jsonFlag - origQuiet := harvestQuiet - origDryRun := dryRun - origMinConf := harvestMinConfidence - origInclude := harvestInclude - origMaxSize := harvestMaxFileSize - origOutputDir := harvestOutputDir - origPromote := harvestPromoteTo - t.Cleanup(func() { - harvestRootsFlag = origRoots - output = origOutput - jsonFlag = origJsonFlag - harvestQuiet = origQuiet - dryRun = origDryRun - harvestMinConfidence = origMinConf - harvestInclude = origInclude - harvestMaxFileSize = origMaxSize - harvestOutputDir = origOutputDir - harvestPromoteTo = origPromote - }) - - harvestRootsFlag = fixture.tmp - output = "json" - jsonFlag = false - harvestQuiet = true - dryRun = false - harvestMinConfidence = 0.5 - harvestInclude = "learnings" - harvestMaxFileSize = 1048576 - harvestOutputDir = fixture.outputDir - harvestPromoteTo = fixture.promoteTo -} - -func runHarvestWithCapturedStdout(t *testing.T) []byte { - t.Helper() - - origStdout := os.Stdout - r, w, err := os.Pipe() - if err != nil { - t.Fatal(err) - } - os.Stdout = w - defer func() { - os.Stdout = origStdout - }() - - runErr := runHarvest(harvestCmd, nil) - - w.Close() - - captured, _ := io.ReadAll(r) - - if runErr != nil { - t.Fatalf("runHarvest returned error: %v", runErr) - } - - return captured -} - -func parseHarvestJSONCatalog(t *testing.T, captured []byte) harvest.Catalog { - t.Helper() - - var cat harvest.Catalog - if err := json.Unmarshal(captured, &cat); err != nil { - t.Fatalf("stdout is not valid JSON: %v\nGot: %s", err, string(captured)) - } - - return cat -} - -func assertHarvestJSONSideEffectCatalog(t *testing.T, cat harvest.Catalog) { - t.Helper() - - if cat.DryRun { - t.Fatal("JSON output should reflect non-dry-run execution") - } - if cat.PromotionCount != 1 { - t.Fatalf("JSON output promotion_count = %d, want 1", cat.PromotionCount) - } - if cat.Summary.PromotionWrites != 1 { - t.Fatalf("JSON output summary.promotion_writes = %d, want 1", cat.Summary.PromotionWrites) - } - if cat.Summary.WarningCount != 1 { - t.Fatalf("JSON output summary.warning_count = %d, want 1", cat.Summary.WarningCount) - } - if len(cat.Warnings) != 1 || cat.Warnings[0].Stage != "parse_frontmatter" { - t.Fatalf("JSON output warnings = %#v, want one parse_frontmatter warning", cat.Warnings) - } - if len(cat.Promoted) != 1 { - t.Fatalf("JSON output promoted artifacts = %d, want 1", len(cat.Promoted)) - } -} - -func assertHarvestJSONSideEffectPersistence(t *testing.T, fixture harvestJSONSideEffectsFixture) { - t.Helper() - - latestPath := filepath.Join(fixture.outputDir, "latest.json") - data, err := os.ReadFile(latestPath) - if err != nil { - t.Fatalf("expected latest.json to be written in JSON mode: %v", err) - } - var persisted harvest.Catalog - if err := json.Unmarshal(data, &persisted); err != nil { - t.Fatalf("latest.json is not valid JSON: %v", err) - } - if persisted.PromotionCount != 1 || persisted.Summary.PromotionWrites != 1 { - t.Fatalf("persisted promotion counts = %d/%d, want 1/1", - persisted.PromotionCount, persisted.Summary.PromotionWrites) - } - if persisted.Summary.WarningCount != 1 { - t.Fatalf("persisted summary.warning_count = %d, want 1", persisted.Summary.WarningCount) - } -} - -func assertHarvestJSONSideEffectPromotion(t *testing.T, fixture harvestJSONSideEffectsFixture, cat harvest.Catalog) { - t.Helper() - - promotedPath := filepath.Join(fixture.promoteTo, "learning", cat.Promoted[0].SourceRig+"-"+fixture.sourceName) - promoted, err := os.ReadFile(promotedPath) - if err != nil { - t.Fatalf("expected JSON mode to promote artifact %s: %v", promotedPath, err) - } - promotedText := string(promoted) - if !strings.Contains(promotedText, `promoted_from: "proj-proj"`) { - t.Fatalf("promoted artifact missing provenance header:\n%s", promotedText) - } - if !strings.Contains(promotedText, "# JSON Promote") { - t.Fatalf("promoted artifact missing body:\n%s", promotedText) - } -} - -// --------------------------------------------------------------------------- -// duplicateArtifactCount -// --------------------------------------------------------------------------- - -func TestDuplicateArtifactCount_NoDuplicates(t *testing.T) { - cat := &harvest.Catalog{} - got := duplicateArtifactCount(cat) - if got != 0 { - t.Errorf("duplicateArtifactCount(no dups) = %d, want 0", got) - } -} - -func TestDuplicateArtifactCount_WithDuplicates(t *testing.T) { - cat := &harvest.Catalog{ - Duplicates: []harvest.DuplicateGroup{ - {Count: 3}, // 3-1 = 2 extra - {Count: 2}, // 2-1 = 1 extra - {Count: 1}, // 1-1 = 0 extra - }, - } - got := duplicateArtifactCount(cat) - if got != 3 { - t.Errorf("duplicateArtifactCount = %d, want 3", got) - } -} - -func TestRunHarvest_MalformedFileBecomesWarning(t *testing.T) { - tmp := t.TempDir() - t.Setenv("HOME", tmp) - - rigDir := filepath.Join(tmp, "myproject", ".agents", "learnings") - if err := os.MkdirAll(rigDir, 0o755); err != nil { - t.Fatal(err) - } - valid := "---\ntitle: Valid Learning\nconfidence: 0.8\n---\n\n# Valid Learning\n\nThis artifact should still be harvested.\n" - if err := os.WriteFile(filepath.Join(rigDir, "2026-04-10-valid.md"), []byte(valid), 0o644); err != nil { - t.Fatal(err) - } - invalid := "---\nkey:\n\tvalue_with_tab: broken\n---\n" - if err := os.WriteFile(filepath.Join(rigDir, "2026-04-10-bad.md"), []byte(invalid), 0o644); err != nil { - t.Fatal(err) - } - - origRoots := harvestRootsFlag - origOutput := harvestOutputDir - origPromote := harvestPromoteTo - origQuiet := harvestQuiet - origDryRun := dryRun - origMinConf := harvestMinConfidence - origInclude := harvestInclude - origMaxSize := harvestMaxFileSize - t.Cleanup(func() { - harvestRootsFlag = origRoots - harvestOutputDir = origOutput - harvestPromoteTo = origPromote - harvestQuiet = origQuiet - dryRun = origDryRun - harvestMinConfidence = origMinConf - harvestInclude = origInclude - harvestMaxFileSize = origMaxSize - }) - - harvestRootsFlag = tmp - harvestOutputDir = filepath.Join(tmp, "harvest-output") - harvestPromoteTo = filepath.Join(tmp, "promoted") - harvestQuiet = true - dryRun = true - harvestMinConfidence = 0.5 - harvestInclude = "learnings" - harvestMaxFileSize = 1048576 - - if err := runHarvest(harvestCmd, nil); err != nil { - t.Fatalf("runHarvest returned error: %v", err) - } - - data, err := os.ReadFile(filepath.Join(harvestOutputDir, "latest.json")) - if err != nil { - t.Fatalf("reading catalog: %v", err) - } - var cat harvest.Catalog - if err := json.Unmarshal(data, &cat); err != nil { - t.Fatalf("unmarshaling catalog: %v", err) - } - if len(cat.Artifacts) != 1 { - t.Fatalf("expected 1 valid artifact, got %d", len(cat.Artifacts)) - } - if len(cat.Warnings) != 1 { - t.Fatalf("expected 1 warning, got %#v", cat.Warnings) - } - if cat.Summary.WarningCount != 1 { - t.Fatalf("summary.warning_count = %d, want 1", cat.Summary.WarningCount) - } - if cat.TotalFiles != 2 { - t.Fatalf("total_files = %d, want 2 candidate files", cat.TotalFiles) - } - if cat.Warnings[0].Stage != "parse_frontmatter" { - t.Fatalf("warning stage = %q, want parse_frontmatter", cat.Warnings[0].Stage) - } -} - -// --------------------------------------------------------------------------- -// failIfDreamHoldsLock — pm-011 Dream vs. harvest concurrency guard -// --------------------------------------------------------------------------- - -// writeTestLockFile creates .agents/overnight/run.lock under repoRoot with -// the given body. It returns the lock file path for follow-up mtime tweaks. -func writeTestLockFile(t *testing.T, repoRoot, body string) string { - t.Helper() - lockDir := filepath.Join(repoRoot, ".agents", "overnight") - if err := os.MkdirAll(lockDir, 0o755); err != nil { - t.Fatalf("mkdir lock dir: %v", err) - } - lockPath := filepath.Join(lockDir, "run.lock") - if err := os.WriteFile(lockPath, []byte(body), 0o644); err != nil { - t.Fatalf("write lock file: %v", err) - } - return lockPath -} - -func TestHarvest_RefusesDuringDreamRun(t *testing.T) { - tmp := t.TempDir() - // Use the current process PID so ProcessAlive returns true - // deterministically without relying on any external process. - writeTestLockFile(t, tmp, strconv.Itoa(os.Getpid())+"\n") - - err := failIfDreamHoldsLock(tmp) - if err == nil { - t.Fatalf("expected error when Dream holds a live lock, got nil") - } - if !strings.Contains(err.Error(), "live writer holds the overnight lock") { - t.Fatalf("error message = %q, want substring %q", err.Error(), "live writer holds the overnight lock") - } -} - -func TestHarvest_ProceedsWhenNoLockFile(t *testing.T) { - tmp := t.TempDir() - - if err := failIfDreamHoldsLock(tmp); err != nil { - t.Fatalf("expected nil when no lock file exists, got %v", err) - } -} - -func TestHarvest_ProceedsWhenLockStale(t *testing.T) { - tmp := t.TempDir() - // Dead PID (way beyond the valid POSIX range on any normal - // system) plus a backdated mtime so LockIsStale returns true. - lockPath := writeTestLockFile(t, tmp, "999999999\n") - - old := time.Now().Add(-24 * time.Hour) - if err := os.Chtimes(lockPath, old, old); err != nil { - t.Fatalf("chtimes: %v", err) - } - - if err := failIfDreamHoldsLock(tmp); err != nil { - t.Fatalf("expected nil for stale lock, got %v", err) - } -} - -func TestHarvest_ProceedsWhenLockPIDDead(t *testing.T) { - tmp := t.TempDir() - // Fresh mtime (default from WriteFile is now) so we bypass the - // stale fast-path and exercise the explicit PID liveness check. - writeTestLockFile(t, tmp, "999999999\n") - - if err := failIfDreamHoldsLock(tmp); err != nil { - t.Fatalf("expected nil when lock PID is dead, got %v", err) - } -} - -func TestHarvest_LockFileCorrupted_ProceedsWithWarning(t *testing.T) { - tmp := t.TempDir() - // Garbage content — not a decimal PID. ReadLockPID should return - // 0 and failIfDreamHoldsLock should proceed without blocking. - writeTestLockFile(t, tmp, "this is not a pid file\n") - - if err := failIfDreamHoldsLock(tmp); err != nil { - t.Fatalf("expected nil for corrupt lock file, got %v", err) - } -} - -func TestRunHarvest_PersistsDiscoveryWarnings(t *testing.T) { - // Root bypasses filesystem permission checks, so the chmod(0) - // trick below cannot trigger a permission-denied warning. - if os.Getuid() == 0 { - t.Skip("test requires non-root to enforce directory permissions") - } - - tmp := t.TempDir() - t.Setenv("HOME", filepath.Join(tmp, "home")) - - validRigDir := filepath.Join(tmp, "goodproject", ".agents", "learnings") - if err := os.MkdirAll(validRigDir, 0o755); err != nil { - t.Fatal(err) - } - valid := "---\ntitle: Valid Learning\nconfidence: 0.8\n---\n\n# Valid Learning\n\nThis artifact should still be harvested.\n" - if err := os.WriteFile(filepath.Join(validRigDir, "2026-04-10-valid.md"), []byte(valid), 0o644); err != nil { - t.Fatal(err) - } - - badAgentsDir := filepath.Join(tmp, "badproject", ".agents") - if err := os.MkdirAll(filepath.Join(badAgentsDir, "learnings"), 0o755); err != nil { - t.Fatal(err) - } - if err := os.Chmod(badAgentsDir, 0); err != nil { - t.Fatal(err) - } - t.Cleanup(func() { - _ = os.Chmod(badAgentsDir, 0o755) - }) - - origRoots := harvestRootsFlag - origOutput := harvestOutputDir - origPromote := harvestPromoteTo - origQuiet := harvestQuiet - origDryRun := dryRun - origMinConf := harvestMinConfidence - origInclude := harvestInclude - origMaxSize := harvestMaxFileSize - t.Cleanup(func() { - harvestRootsFlag = origRoots - harvestOutputDir = origOutput - harvestPromoteTo = origPromote - harvestQuiet = origQuiet - dryRun = origDryRun - harvestMinConfidence = origMinConf - harvestInclude = origInclude - harvestMaxFileSize = origMaxSize - }) - - harvestRootsFlag = tmp - harvestOutputDir = filepath.Join(tmp, "harvest-output") - harvestPromoteTo = filepath.Join(tmp, "promoted") - harvestQuiet = true - dryRun = true - harvestMinConfidence = 0.5 - harvestInclude = "learnings" - harvestMaxFileSize = 1048576 - - if err := runHarvest(harvestCmd, nil); err != nil { - t.Fatalf("runHarvest returned error: %v", err) - } - - data, err := os.ReadFile(filepath.Join(harvestOutputDir, "latest.json")) - if err != nil { - t.Fatalf("reading catalog: %v", err) - } - var cat harvest.Catalog - if err := json.Unmarshal(data, &cat); err != nil { - t.Fatalf("unmarshaling catalog: %v", err) - } - if len(cat.Artifacts) != 1 { - t.Fatalf("expected 1 harvested artifact, got %d", len(cat.Artifacts)) - } - if cat.TotalFiles != 1 { - t.Fatalf("total_files = %d, want 1 candidate file from the readable rig", cat.TotalFiles) - } - if cat.Summary.WarningCount != len(cat.Warnings) { - t.Fatalf("summary.warning_count = %d, want %d", cat.Summary.WarningCount, len(cat.Warnings)) - } - - foundDiscoveryWarning := false - for _, warning := range cat.Warnings { - if strings.HasPrefix(warning.Stage, "discover_") { - foundDiscoveryWarning = true - if warning.Path == "" { - t.Fatal("discovery warning should record the failing path") - } - break - } - } - if !foundDiscoveryWarning { - t.Fatalf("expected a persisted discovery warning, got %#v", cat.Warnings) - } -} - -// TestRunHarvest_NestedSubdirsCaught_E2E exercises the full ao harvest -// command path against a rig whose .agents/learnings/ tree carries -// nested rig-prefixed legacy artifacts at depth 2 and 3. PR #138 -// (squash da141872) replaced os.ReadDir with filepath.WalkDir at the -// extract layer; this test guards the integration so a future regression -// in the cobra command (flag wiring, rig discovery, output assembly) -// can't silently undo that fix while the unit-level -// TestExtractArtifacts_RecursesIntoNestedSubdirs still passes. -func TestRunHarvest_NestedSubdirsCaught_E2E(t *testing.T) { - tmp := t.TempDir() - t.Setenv("HOME", tmp) - - rigBase := filepath.Join(tmp, "myproj", ".agents", "learnings") - depth2 := filepath.Join(rigBase, "learning") - depth3 := filepath.Join(depth2, "sub") - for _, d := range []string{rigBase, depth2, depth3} { - if err := os.MkdirAll(d, 0o755); err != nil { - t.Fatal(err) - } - } - - fixtures := map[string]string{ - filepath.Join(rigBase, "2026-04-01-d1.md"): "---\ntitle: D1\nconfidence: 0.7\n---\n\nbody1\n", - filepath.Join(depth2, "2026-04-02-d2.md"): "---\ntitle: D2\nconfidence: 0.7\n---\n\nbody2\n", - filepath.Join(depth3, "2026-04-03-d3.md"): "---\ntitle: D3\nconfidence: 0.7\n---\n\nbody3\n", - } - for path, body := range fixtures { - if err := os.WriteFile(path, []byte(body), 0o644); err != nil { - t.Fatalf("writing fixture %s: %v", path, err) - } - } - - outputDir := filepath.Join(tmp, "harvest-output") - promoteDir := filepath.Join(tmp, "promoted") - - origRoots := harvestRootsFlag - origOutput := harvestOutputDir - origPromote := harvestPromoteTo - origQuiet := harvestQuiet - origDryRun := dryRun - origMinConf := harvestMinConfidence - origInclude := harvestInclude - origMaxSize := harvestMaxFileSize - t.Cleanup(func() { - harvestRootsFlag = origRoots - harvestOutputDir = origOutput - harvestPromoteTo = origPromote - harvestQuiet = origQuiet - dryRun = origDryRun - harvestMinConfidence = origMinConf - harvestInclude = origInclude - harvestMaxFileSize = origMaxSize - }) - - harvestRootsFlag = tmp - harvestOutputDir = outputDir - harvestPromoteTo = promoteDir - harvestQuiet = true - dryRun = true - harvestMinConfidence = 0.5 - harvestInclude = "learnings" - harvestMaxFileSize = 1048576 - - if err := runHarvest(harvestCmd, nil); err != nil { - t.Fatalf("runHarvest returned error: %v", err) - } - - data, err := os.ReadFile(filepath.Join(outputDir, "latest.json")) - if err != nil { - t.Fatalf("reading catalog: %v", err) - } - var cat harvest.Catalog - if err := json.Unmarshal(data, &cat); err != nil { - t.Fatalf("unmarshaling catalog: %v", err) - } - - if len(cat.Artifacts) != 3 { - t.Fatalf("expected 3 artifacts (depth 1+2+3), got %d: %+v", len(cat.Artifacts), cat.Artifacts) - } - if cat.TotalFiles != 3 { - t.Errorf("total_files = %d, want 3", cat.TotalFiles) - } - - titles := map[string]bool{} - for _, a := range cat.Artifacts { - titles[a.Title] = true - if a.Type != "learning" { - t.Errorf("artifact %q: type = %q, want %q", a.Title, a.Type, "learning") - } - if a.SourcePath == "" { - t.Errorf("artifact %q: empty source_path", a.Title) - } - } - for _, want := range []string{"D1", "D2", "D3"} { - if !titles[want] { - t.Errorf("missing artifact title %q (have %v)", want, titles) - } - } - - for _, w := range cat.Warnings { - if strings.HasPrefix(w.Stage, "walk_dir") || strings.HasPrefix(w.Stage, "read_dir") { - t.Errorf("unexpected walk/read warning: stage=%s path=%s msg=%s", w.Stage, w.Path, w.Message) - } - } -} diff --git a/cli/cmd/ao/harvest_volume_gate_test.go b/cli/cmd/ao/harvest_volume_gate_test.go deleted file mode 100644 index 9932bffe9..000000000 --- a/cli/cmd/ao/harvest_volume_gate_test.go +++ /dev/null @@ -1,228 +0,0 @@ -//go:build flywheel - -// practices: [wiki-knowledge-surface, lean-startup] -package main - -import ( - "io" - "os" - "path/filepath" - "strconv" - "strings" - "testing" -) - -// TestHarvestCmd_MaxPromotionsFlag_Wired guards that the --max-promotions -// flag is registered on `ao harvest`. soc-f2q4 wires the flag and the -// AO_MAX_PROMOTIONS env-var fallback into runHarvest's volume-gate site. -func TestHarvestCmd_MaxPromotionsFlag_Wired(t *testing.T) { - flags := harvestCmd.Flags() - f := flags.Lookup("max-promotions") - if f == nil { - t.Fatal("flag --max-promotions not found on harvestCmd") - } - if f.DefValue != "500" { - t.Errorf("max-promotions default = %q, want %q", f.DefValue, "500") - } -} - -// TestHarvestVolumeGate_BelowThreshold_Silent runs `ao harvest` with a -// promotion count under the threshold and asserts no WARN line is written -// to stderr. -func TestHarvestVolumeGate_BelowThreshold_Silent(t *testing.T) { - tmp := setupHarvestVolumeGateFixture(t, 3) // 3 distinct artifacts, threshold 500 - stderr, runErr := runHarvestCaptureStderr(t, tmp, harvestVolumeGateOverrides{ - maxPromotions: 500, - }) - if runErr != nil { - t.Fatalf("runHarvest returned error: %v", runErr) - } - if strings.Contains(stderr, "exceeded threshold") { - t.Fatalf("expected no WARN line below threshold, stderr=%q", stderr) - } -} - -// TestHarvestVolumeGate_AboveThreshold_WarnsButContinues runs `ao harvest` -// with a promotion count above the threshold and asserts: -// - the WARN line shows up on stderr with the configured threshold, -// - runHarvest returns nil (exit code unchanged: gate is advisory), -// - all source files were promoted (gate does not block writes). -func TestHarvestVolumeGate_AboveThreshold_WarnsButContinues(t *testing.T) { - const promotionCount = 6 - const threshold = 3 - tmp := setupHarvestVolumeGateFixture(t, promotionCount) - - stderr, runErr := runHarvestCaptureStderr(t, tmp, harvestVolumeGateOverrides{ - maxPromotions: threshold, - maxPromotionsSet: true, - }) - if runErr != nil { - t.Fatalf("runHarvest returned error (gate must NOT block): %v", runErr) - } - if !strings.Contains(stderr, "exceeded threshold 3") { - t.Fatalf("expected WARN with threshold=3 in stderr, got=%q", stderr) - } - if !strings.Contains(stderr, "WARN:") { - t.Fatalf("expected WARN: prefix in stderr, got=%q", stderr) - } - - // All artifacts must still land on disk — gate is advisory, not a block. - promoteDir := filepath.Join(tmp, "promoted", "learning") - entries, err := os.ReadDir(promoteDir) - if err != nil { - t.Fatalf("ReadDir(%s): %v", promoteDir, err) - } - mdCount := 0 - for _, e := range entries { - if !e.IsDir() && strings.HasSuffix(e.Name(), ".md") { - mdCount++ - } - } - if mdCount != promotionCount { - t.Fatalf("promoted %d files, want %d (gate is advisory; must not block writes)", mdCount, promotionCount) - } -} - -// TestHarvestVolumeGate_FlagOverride verifies the --max-promotions flag is -// honored end-to-end. Threshold=2 with 4 promotions → WARN. -func TestHarvestVolumeGate_FlagOverride(t *testing.T) { - tmp := setupHarvestVolumeGateFixture(t, 4) - stderr, runErr := runHarvestCaptureStderr(t, tmp, harvestVolumeGateOverrides{ - maxPromotions: 2, - maxPromotionsSet: true, - }) - if runErr != nil { - t.Fatalf("runHarvest returned error: %v", runErr) - } - if !strings.Contains(stderr, "exceeded threshold 2") { - t.Fatalf("expected --max-promotions=2 to drive WARN, got stderr=%q", stderr) - } -} - -// TestHarvestVolumeGate_EnvOverride verifies AO_MAX_PROMOTIONS=N is honored -// when the flag is not set explicitly. Sets env=2, leaves flag at its -// default (500), and asserts the env-derived threshold drives the WARN. -func TestHarvestVolumeGate_EnvOverride(t *testing.T) { - tmp := setupHarvestVolumeGateFixture(t, 4) - t.Setenv("AO_MAX_PROMOTIONS", "2") - stderr, runErr := runHarvestCaptureStderr(t, tmp, harvestVolumeGateOverrides{ - maxPromotions: 500, // default; env should win because flag is not Changed - }) - if runErr != nil { - t.Fatalf("runHarvest returned error: %v", runErr) - } - if !strings.Contains(stderr, "exceeded threshold 2") { - t.Fatalf("expected AO_MAX_PROMOTIONS=2 to drive WARN, got stderr=%q", stderr) - } -} - -// TestHarvestVolumeGate_FlagWinsOverEnv verifies that when both the flag -// and env-var are set, the flag wins. Env=10 (would suppress with 4 -// promotions), flag=2 (would warn with 4 promotions) → WARN expected. -func TestHarvestVolumeGate_FlagWinsOverEnv(t *testing.T) { - tmp := setupHarvestVolumeGateFixture(t, 4) - t.Setenv("AO_MAX_PROMOTIONS", "10") - stderr, runErr := runHarvestCaptureStderr(t, tmp, harvestVolumeGateOverrides{ - maxPromotions: 2, - maxPromotionsSet: true, - }) - if runErr != nil { - t.Fatalf("runHarvest returned error: %v", runErr) - } - if !strings.Contains(stderr, "exceeded threshold 2") { - t.Fatalf("expected flag=2 to win over env=10, got stderr=%q", stderr) - } -} - -// --------------------------------------------------------------------------- -// helpers -// --------------------------------------------------------------------------- - -type harvestVolumeGateOverrides struct { - maxPromotions int - maxPromotionsSet bool // when true, mark the flag as Changed so it wins over env -} - -// setupHarvestVolumeGateFixture creates a single rig with `count` distinct -// learning files, all above the default min-confidence threshold so they -// land in catalog.Promoted. Returns the tmp HOME root. -func setupHarvestVolumeGateFixture(t *testing.T, count int) string { - t.Helper() - tmp := t.TempDir() - t.Setenv("HOME", tmp) - - rigDir := filepath.Join(tmp, "proj", ".agents", "learnings") - if err := os.MkdirAll(rigDir, 0o755); err != nil { - t.Fatal(err) - } - // Distinct content per file so BuildCatalog does not dedup them. - for i := 0; i < count; i++ { - idx := strconv.Itoa(i) - body := "---\ntitle: Distinct " + idx + "\nconfidence: 0.9\n---\n\n# Distinct " + idx + "\n\nUnique body number " + idx + ".\n" - path := filepath.Join(rigDir, "2026-04-30-d"+idx+".md") - if err := os.WriteFile(path, []byte(body), 0o644); err != nil { - t.Fatal(err) - } - } - return tmp -} - -// runHarvestCaptureStderr drives runHarvest end-to-end against the fixture -// rooted at homeDir, captures stderr, and returns it along with any runErr. -// Flags are saved/restored automatically; max-promotions is set per overrides. -func runHarvestCaptureStderr(t *testing.T, homeDir string, ov harvestVolumeGateOverrides) (string, error) { - t.Helper() - - origRoots := harvestRootsFlag - origOutput := harvestOutputDir - origPromote := harvestPromoteTo - origQuiet := harvestQuiet - origDryRun := dryRun - origMinConf := harvestMinConfidence - origInclude := harvestInclude - origMaxSize := harvestMaxFileSize - origMaxPromotions := harvestMaxPromotions - t.Cleanup(func() { - harvestRootsFlag = origRoots - harvestOutputDir = origOutput - harvestPromoteTo = origPromote - harvestQuiet = origQuiet - dryRun = origDryRun - harvestMinConfidence = origMinConf - harvestInclude = origInclude - harvestMaxFileSize = origMaxSize - harvestMaxPromotions = origMaxPromotions - // Reset Changed state on the flag so subsequent tests start clean. - if f := harvestCmd.Flags().Lookup("max-promotions"); f != nil { - f.Changed = false - } - }) - - harvestRootsFlag = homeDir - harvestOutputDir = filepath.Join(homeDir, "out") - harvestPromoteTo = filepath.Join(homeDir, "promoted") - harvestQuiet = true - dryRun = false - harvestMinConfidence = 0.5 - harvestInclude = "learnings" - harvestMaxFileSize = 1048576 - harvestMaxPromotions = ov.maxPromotions - if f := harvestCmd.Flags().Lookup("max-promotions"); f != nil { - f.Changed = ov.maxPromotionsSet - } - - // Capture stderr. - origStderr := os.Stderr - r, w, err := os.Pipe() - if err != nil { - t.Fatal(err) - } - os.Stderr = w - - runErr := runHarvest(harvestCmd, nil) - - w.Close() - os.Stderr = origStderr - captured, _ := io.ReadAll(r) - return string(captured), runErr -} diff --git a/cli/cmd/ao/hypothesis_ledger_adapter.go b/cli/cmd/ao/hypothesis_ledger_adapter.go deleted file mode 100644 index 637e3a11a..000000000 --- a/cli/cmd/ao/hypothesis_ledger_adapter.go +++ /dev/null @@ -1,158 +0,0 @@ -// practices: [hexagonal-architecture, ddd-bounded-context] -package main - -import ( - "bufio" - "context" - "encoding/json" - "errors" - "fmt" - "os" - "sync" - - "github.com/boshu2/agentops/cli/internal/ports" -) - -// productionHypothesisLedger satisfies ports.HypothesisLedgerPort by -// appending and reading records from .agents/evolve/hypotheses.jsonl -// or any path supplied by tests. It is the production sibling of -// InMemoryHypothesisLedger and follows the LoopReader/LoopWriter JSONL -// adapter shape. -// -// File semantics: -// - Append-only: each Append writes one JSON line. -// - Duplicate IDs are rejected by scanning valid existing records -// before append. -// - List tolerates empty, missing, and hand-edited files by skipping -// malformed JSON lines. -// - Thread safety is process-local. Cross-process appends need a -// higher-level file lock if they become a real concurrent path. -type productionHypothesisLedger struct { - mu sync.Mutex - path string -} - -func newProductionHypothesisLedger(path string) *productionHypothesisLedger { - return &productionHypothesisLedger{path: path} -} - -func (l *productionHypothesisLedger) Append(ctx context.Context, record ports.HypothesisRecord) (ports.HypothesisRecord, error) { - if err := ctx.Err(); err != nil { - return ports.HypothesisRecord{}, err - } - if l.path == "" { - return ports.HypothesisRecord{}, fmt.Errorf("productionHypothesisLedger: path required") - } - if record.ID == "" { - return ports.HypothesisRecord{}, errors.New("productionHypothesisLedger: ID required") - } - - l.mu.Lock() - defer l.mu.Unlock() - - records, err := l.readRecords(ctx) - if err != nil { - return ports.HypothesisRecord{}, err - } - for _, existing := range records { - if existing.ID == record.ID { - return ports.HypothesisRecord{}, fmt.Errorf("productionHypothesisLedger: duplicate ID %q", record.ID) - } - } - - stored := cloneProductionHypothesisRecord(record) - payload, err := json.Marshal(stored) - if err != nil { - return ports.HypothesisRecord{}, fmt.Errorf("productionHypothesisLedger marshal: %w", err) - } - f, err := os.OpenFile(l.path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) - if err != nil { - return ports.HypothesisRecord{}, fmt.Errorf("productionHypothesisLedger open %q: %w", l.path, err) - } - defer func() { _ = f.Close() }() - if _, err := f.Write(append(payload, '\n')); err != nil { - return ports.HypothesisRecord{}, fmt.Errorf("productionHypothesisLedger write: %w", err) - } - return cloneProductionHypothesisRecord(stored), nil -} - -func (l *productionHypothesisLedger) List(ctx context.Context) ([]ports.HypothesisRecord, error) { - if err := ctx.Err(); err != nil { - return nil, err - } - l.mu.Lock() - defer l.mu.Unlock() - return l.readRecords(ctx) -} - -func (l *productionHypothesisLedger) Find(ctx context.Context, id string) (ports.HypothesisRecord, bool, error) { - if err := ctx.Err(); err != nil { - return ports.HypothesisRecord{}, false, err - } - if id == "" { - return ports.HypothesisRecord{}, false, errors.New("productionHypothesisLedger: ID required") - } - - records, err := l.List(ctx) - if err != nil { - return ports.HypothesisRecord{}, false, err - } - for _, record := range records { - if record.ID == id { - return cloneProductionHypothesisRecord(record), true, nil - } - } - return ports.HypothesisRecord{}, false, nil -} - -func (l *productionHypothesisLedger) readRecords(ctx context.Context) ([]ports.HypothesisRecord, error) { - if err := ctx.Err(); err != nil { - return nil, err - } - if l.path == "" { - return nil, nil - } - f, err := os.Open(l.path) - if err != nil { - if os.IsNotExist(err) { - return nil, nil - } - return nil, fmt.Errorf("productionHypothesisLedger open %q: %w", l.path, err) - } - defer func() { _ = f.Close() }() - - out := make([]ports.HypothesisRecord, 0) - scanner := bufio.NewScanner(f) - scanner.Buffer(make([]byte, 64*1024), 1024*1024) - for scanner.Scan() { - if err := ctx.Err(); err != nil { - return nil, err - } - line := scanner.Bytes() - if len(line) == 0 { - continue - } - var record ports.HypothesisRecord - if err := json.Unmarshal(line, &record); err != nil { - continue - } - if record.ID == "" { - continue - } - out = append(out, cloneProductionHypothesisRecord(record)) - } - if err := scanner.Err(); err != nil { - return nil, fmt.Errorf("productionHypothesisLedger scan %q: %w", l.path, err) - } - return out, nil -} - -func cloneProductionHypothesisRecord(record ports.HypothesisRecord) ports.HypothesisRecord { - if record.Evidence != nil { - record.Evidence = append([]string(nil), record.Evidence...) - } - return record -} - -// Compile-time assertion: productionHypothesisLedger satisfies the port. -var _ ports.HypothesisLedgerPort = (*productionHypothesisLedger)(nil) diff --git a/cli/cmd/ao/hypothesis_ledger_adapter_test.go b/cli/cmd/ao/hypothesis_ledger_adapter_test.go deleted file mode 100644 index e577da389..000000000 --- a/cli/cmd/ao/hypothesis_ledger_adapter_test.go +++ /dev/null @@ -1,177 +0,0 @@ -// practices: [hexagonal-architecture, tdd] -package main - -import ( - "context" - "errors" - "os" - "path/filepath" - "strings" - "testing" - - "github.com/boshu2/agentops/cli/internal/ports" -) - -func TestProductionHypothesisLedger_AppendListFindRoundTrip(t *testing.T) { - path := filepath.Join(t.TempDir(), "hypotheses.jsonl") - ledger := newProductionHypothesisLedger(path) - - got, err := ledger.Append(context.Background(), ports.HypothesisRecord{ - ID: "H193.1", - CycleLanded: 193, - CheckAtCycle: 196, - Patch: "file-backed hypothesis ledger", - Hypothesis: "evolve can read empirical claims through a port", - Measure: "adapter test passes", - Verdict: ports.HypothesisVerdictPending, - Evidence: []string{"go test ./cmd/ao"}, - }) - if err != nil { - t.Fatalf("Append: %v", err) - } - if got.ID != "H193.1" { - t.Fatalf("Append ID = %q, want H193.1", got.ID) - } - - records, err := ledger.List(context.Background()) - if err != nil { - t.Fatalf("List: %v", err) - } - if len(records) != 1 { - t.Fatalf("List len = %d, want 1", len(records)) - } - if records[0].CycleLanded != 193 { - t.Fatalf("CycleLanded = %d, want 193", records[0].CycleLanded) - } - - found, ok, err := ledger.Find(context.Background(), "H193.1") - if err != nil { - t.Fatalf("Find: %v", err) - } - if !ok { - t.Fatal("Find ok = false, want true") - } - if found.Measure != "adapter test passes" { - t.Fatalf("Measure = %q, want adapter test passes", found.Measure) - } -} - -func TestProductionHypothesisLedger_MissingFileReturnsEmpty(t *testing.T) { - ledger := newProductionHypothesisLedger(filepath.Join(t.TempDir(), "missing.jsonl")) - records, err := ledger.List(context.Background()) - if err != nil { - t.Fatalf("List: %v", err) - } - if len(records) != 0 { - t.Fatalf("List len = %d, want 0", len(records)) - } - record, ok, err := ledger.Find(context.Background(), "H404") - if err != nil { - t.Fatalf("Find: %v", err) - } - if ok { - t.Fatal("Find ok = true, want false") - } - if record.ID != "" { - t.Fatalf("record.ID = %q, want zero-value", record.ID) - } -} - -func TestProductionHypothesisLedger_SkipsMalformedAndEmptyIDRows(t *testing.T) { - path := filepath.Join(t.TempDir(), "hypotheses.jsonl") - content := strings.Join([]string{ - `{"id":"H1","verdict":"PENDING"}`, - `{bad json`, - `{"verdict":"PENDING"}`, - `{"id":"H2","evidence":["kept"]}`, - ``, - }, "\n") - if err := os.WriteFile(path, []byte(content), 0o644); err != nil { - t.Fatal(err) - } - ledger := newProductionHypothesisLedger(path) - - records, err := ledger.List(context.Background()) - if err != nil { - t.Fatalf("List: %v", err) - } - if len(records) != 2 { - t.Fatalf("List len = %d, want 2", len(records)) - } - if records[0].ID != "H1" || records[1].ID != "H2" { - t.Fatalf("IDs = %q,%q, want H1,H2", records[0].ID, records[1].ID) - } -} - -func TestProductionHypothesisLedger_RejectsEmptyAndDuplicateIDs(t *testing.T) { - path := filepath.Join(t.TempDir(), "hypotheses.jsonl") - ledger := newProductionHypothesisLedger(path) - - if _, err := ledger.Append(context.Background(), ports.HypothesisRecord{}); err == nil { - t.Fatal("expected empty-ID error, got nil") - } - if _, err := ledger.Append(context.Background(), ports.HypothesisRecord{ID: "H1"}); err != nil { - t.Fatal(err) - } - _, err := ledger.Append(context.Background(), ports.HypothesisRecord{ID: "H1"}) - if err == nil { - t.Fatal("expected duplicate-ID error, got nil") - } - if !strings.Contains(err.Error(), "duplicate") { - t.Fatalf("error = %v, want duplicate substring", err) - } -} - -func TestProductionHypothesisLedger_ReturnsDefensiveCopies(t *testing.T) { - path := filepath.Join(t.TempDir(), "hypotheses.jsonl") - ledger := newProductionHypothesisLedger(path) - input := ports.HypothesisRecord{ID: "H1", Evidence: []string{"original"}} - - got, err := ledger.Append(context.Background(), input) - if err != nil { - t.Fatal(err) - } - input.Evidence[0] = "mutated-input" - got.Evidence[0] = "mutated-return" - - records, err := ledger.List(context.Background()) - if err != nil { - t.Fatal(err) - } - records[0].Evidence[0] = "mutated-list" - - found, ok, err := ledger.Find(context.Background(), "H1") - if err != nil { - t.Fatal(err) - } - if !ok { - t.Fatal("Find ok = false, want true") - } - if found.Evidence[0] != "original" { - t.Fatalf("stored Evidence[0] = %q, want original", found.Evidence[0]) - } -} - -func TestProductionHypothesisLedger_HonorsContextCancellation(t *testing.T) { - path := filepath.Join(t.TempDir(), "hypotheses.jsonl") - ledger := newProductionHypothesisLedger(path) - ctx, cancel := context.WithCancel(context.Background()) - cancel() - - if _, err := ledger.Append(ctx, ports.HypothesisRecord{ID: "H1"}); !errors.Is(err, context.Canceled) { - t.Fatalf("Append error = %v, want context.Canceled", err) - } - if _, err := ledger.List(ctx); !errors.Is(err, context.Canceled) { - t.Fatalf("List error = %v, want context.Canceled", err) - } - if _, _, err := ledger.Find(ctx, "H1"); !errors.Is(err, context.Canceled) { - t.Fatalf("Find error = %v, want context.Canceled", err) - } -} - -func TestProductionHypothesisLedger_EmptyFindIDErrors(t *testing.T) { - ledger := newProductionHypothesisLedger(filepath.Join(t.TempDir(), "hypotheses.jsonl")) - if _, _, err := ledger.Find(context.Background(), ""); err == nil { - t.Fatal("expected empty-ID error, got nil") - } -} diff --git a/cli/cmd/ao/inject_learnings.go b/cli/cmd/ao/inject_learnings.go index d885eb821..5e9719a35 100644 --- a/cli/cmd/ao/inject_learnings.go +++ b/cli/cmd/ao/inject_learnings.go @@ -30,7 +30,7 @@ func sanitizeSourcePhase(phase string) string { return search.SanitizeSourcePhas // t.Cleanup(func() { nowFunc = time.Now })` so every run against the // frozen-mtime fixture produces byte-identical ranking output. // -// ANTI-GOAL per pre-mortem: do NOT thread a `now time.Time` parameter +// ANTI-GOAL per premortem: do NOT thread a `now time.Time` parameter // through collectLearnings / processLearningFile / ApplyFreshnessToLearning. // The 8 production callers of collectLearnings (retrieval_bench.go:200 // and 406,418; lookup.go:101,164; inject.go:340; codex.go:594; diff --git a/cli/cmd/ao/knowledge_files.go b/cli/cmd/ao/knowledge_files.go index d8d77a69a..f2f3b5182 100644 --- a/cli/cmd/ao/knowledge_files.go +++ b/cli/cmd/ao/knowledge_files.go @@ -36,12 +36,6 @@ func walkKnowledgeFiles(dir string, extensions ...string) []string { return files } -func templateCompletionValues() []string { - values := append([]string(nil), validTemplateNames...) - sort.Strings(values) - return values -} - func extractLearningBody(path string) string { content, err := os.ReadFile(path) // #nosec G304 -- caller-selected local knowledge artifact if err != nil { diff --git a/cli/cmd/ao/knowledge_harvest_native.go b/cli/cmd/ao/knowledge_harvest_native.go index aa0c652e0..b72b8100b 100644 --- a/cli/cmd/ao/knowledge_harvest_native.go +++ b/cli/cmd/ao/knowledge_harvest_native.go @@ -167,7 +167,7 @@ func buildKnowledgeHarvestTopicPackets(agentsRoot string, catalog knowledgeHarve b.WriteString("## Consumers\n\n") b.WriteString("- `ao knowledge activate`\n") b.WriteString("- `ao knowledge brief --goal \"turn harvested knowledge into actionable praxis for this repo\"`\n") - b.WriteString("- RPI discovery, planning, pre-mortem, and validation phases\n\n") + b.WriteString("- RPI discovery, planning, premortem, and validation phases\n\n") b.WriteString("## Key Decisions\n\n") b.WriteString("- Convert high-confidence harvested artifacts into operator surfaces before starting freeform implementation.\n") b.WriteString("- Treat harvest confidence, source paths, and runnable validation as selection gates for reusable praxis.\n") @@ -302,7 +302,7 @@ func knowledgeHarvestPraxisScore(artifact knowledgeHarvestArtifact) int { " acceptance probe", " ratchet", " gate", - " pre-mortem", + " premortem", " deterministic", } { if strings.Contains(text, keyword) { diff --git a/cli/cmd/ao/knowledge_native.go b/cli/cmd/ao/knowledge_native.go index 019a9283f..7d6f21673 100644 --- a/cli/cmd/ao/knowledge_native.go +++ b/cli/cmd/ao/knowledge_native.go @@ -581,7 +581,7 @@ func renderKnowledgePlaybook(topic knowledgeTopicDetail, agentsRoot string) stri b.WriteString("\n## Selection Gates\n\n") b.WriteString("- Validate the chosen path against current repo or workspace reality before promotion.\n") b.WriteString("- Keep thin topics discovery-only; only healthy topics and promoted packets may define default behavior.\n") - b.WriteString("- Prefer planning rules, pre-mortem checks, and direct artifact lineage over conversational confidence.\n") + b.WriteString("- Prefer planning rules, premortem checks, and direct artifact lineage over conversational confidence.\n") b.WriteString("\n## Trust Status\n\n") fmt.Fprintf(&b, "- Topic health: `%s`\n", topic.Health) fmt.Fprintf(&b, "- Promoted packet present: `%s`\n", yesNo(knowledgePathExists(promotedPath))) @@ -645,7 +645,7 @@ func renderKnowledgeBriefing(goal string, topics []knowledgeTopicDetail, beliefs fmt.Fprintf(&b, "- Source surface: `%s`\n", source) } b.WriteString("\n## Selection Gates / Proof Surfaces\n\n") - b.WriteString("- Gate: planning rules and pre-mortem checks outrank preference when they disagree with the current plan.\n") + b.WriteString("- Gate: planning rules and premortem checks outrank preference when they disagree with the current plan.\n") b.WriteString("- Gate: promote only changes backed by direct artifacts, promoted packets, or runnable validation.\n") if strings.TrimSpace(playbookPath) != "" { fmt.Fprintf(&b, "- Proof surface: healthy playbook at `%s`\n", playbookPath) diff --git a/cli/cmd/ao/knowledge_test.go b/cli/cmd/ao/knowledge_test.go index 12763a5ed..58999e19a 100644 --- a/cli/cmd/ao/knowledge_test.go +++ b/cli/cmd/ao/knowledge_test.go @@ -683,7 +683,7 @@ func writeKnowledgeHarvestCatalogFixture(t *testing.T, repo string) { "summary": "Plans that add fallback work should verify the target file does not already have the intended behavior.", "type": "learning", "source_rig": "agentops-agentops", - "source_path": "/tmp/repo/.agents/learnings/pre-mortem-grep.md", + "source_path": "/tmp/repo/.agents/learnings/premortem-grep.md", "confidence": 0.90, "scope": "project:agentops", "date": "2026-04-24" diff --git a/cli/cmd/ao/main.go b/cli/cmd/ao/main.go index 178d76f0e..b8df45cef 100644 --- a/cli/cmd/ao/main.go +++ b/cli/cmd/ao/main.go @@ -5,7 +5,7 @@ package main // version is set at build time via ldflags (goreleaser: -X main.version={{ .Version }}). // The fallback "3.2.0-rc" identifies pre-tag source builds on the 3.2 line; the published // release binary overrides this to "3.2.0" via the v3.2.0 git tag. Bump this fallback as -// part of every release prep (listed in skills/release/references/release-cut-and-bump.md). +// part of the repository-owned release process documented in docs/RELEASING.md. var version = "3.2.0-rc" func main() { diff --git a/cli/cmd/ao/metrics_flywheel.go b/cli/cmd/ao/metrics_flywheel.go index 6fc2dfab1..80fd27355 100644 --- a/cli/cmd/ao/metrics_flywheel.go +++ b/cli/cmd/ao/metrics_flywheel.go @@ -13,8 +13,7 @@ import ( "github.com/spf13/cobra" ) -// flywheelGolden is kept for backward compatibility (--golden flag) but golden -// signals now always compute. The flag is a no-op. +// flywheelGolden keeps the hidden historical flag parseable; it has no effect. var flywheelGolden bool var flywheelStatusNamespace string @@ -74,13 +73,8 @@ Examples: Long: `Compare retrieval quality between primary and shadow namespaces. Shows sigma, rho, and escape velocity side-by-side. -Use this to decide whether the shadow scorer is ready for promotion. - -Promotion rule: - Shadow must beat primary on sigma AND show non-regressing rho. - -Rollback rule: - Stop writing to the promoted namespace. No data rewrite needed. +This command reports measurements only. It does not recommend or perform +promotion, routing, activation, rollback, or any other state transition. Examples: ao flywheel compare @@ -96,15 +90,12 @@ var flywheelCompareNamespace string // namespaceComparison holds side-by-side metrics for two namespaces. type namespaceComparison struct { - Primary *types.FlywheelMetrics `json:"primary"` - Shadow *types.FlywheelMetrics `json:"shadow"` - ShadowName string `json:"shadow_name"` - SigmaDelta float64 `json:"sigma_delta"` - RhoDelta float64 `json:"rho_delta"` - VelocityDelta float64 `json:"velocity_delta"` - PromotionReady bool `json:"promotion_ready"` - PromotionReason string `json:"promotion_reason"` - RollbackContract string `json:"rollback_contract"` + Primary *types.FlywheelMetrics `json:"primary"` + Shadow *types.FlywheelMetrics `json:"shadow"` + ShadowName string `json:"shadow_name"` + SigmaDelta float64 `json:"sigma_delta"` + RhoDelta float64 `json:"rho_delta"` + VelocityDelta float64 `json:"velocity_delta"` } func runFlywheelCompare(cmd *cobra.Command, args []string) error { @@ -139,30 +130,13 @@ func runFlywheelCompare(cmd *cobra.Command, args []string) error { func buildNamespaceComparison(primary, shadow *types.FlywheelMetrics, shadowName string) *namespaceComparison { comp := &namespaceComparison{ - Primary: primary, - Shadow: shadow, - ShadowName: canonicalMetricNamespace(shadowName), - SigmaDelta: shadow.Sigma - primary.Sigma, - RhoDelta: shadow.Rho - primary.Rho, - VelocityDelta: shadow.Velocity - primary.Velocity, - RollbackContract: "Stop reading/writing the shadow namespace. Primary data is never mutated by shadow runs.", + Primary: primary, + Shadow: shadow, + ShadowName: canonicalMetricNamespace(shadowName), + SigmaDelta: shadow.Sigma - primary.Sigma, + RhoDelta: shadow.Rho - primary.Rho, + VelocityDelta: shadow.Velocity - primary.Velocity, } - - // Promotion rule: shadow sigma > primary sigma AND shadow rho >= primary rho (non-regressing) - shadowBeatsSigma := shadow.Sigma > primary.Sigma - rhoNonRegressing := shadow.Rho >= primary.Rho-0.01 // 1% tolerance for noise - if shadowBeatsSigma && rhoNonRegressing { - comp.PromotionReady = true - comp.PromotionReason = fmt.Sprintf("Shadow sigma (%.3f) > primary sigma (%.3f) and rho non-regressing (%.3f vs %.3f)", - shadow.Sigma, primary.Sigma, shadow.Rho, primary.Rho) - } else if !shadowBeatsSigma { - comp.PromotionReason = fmt.Sprintf("Shadow sigma (%.3f) does not beat primary sigma (%.3f)", - shadow.Sigma, primary.Sigma) - } else { - comp.PromotionReason = fmt.Sprintf("Shadow rho regressed (%.3f vs primary %.3f)", - shadow.Rho, primary.Rho) - } - return comp } @@ -179,16 +153,6 @@ func printNamespaceComparison(w io.Writer, comp *namespaceComparison) { fmt.Fprintf(w, " %-20s %-12.3f %-12.3f %+.3f\n", "velocity", comp.Primary.Velocity, comp.Shadow.Velocity, comp.VelocityDelta) fmt.Fprintf(w, " %-20s %-12.1f %-12.1f %+.1f\n", "delta (avg age)", comp.Primary.Delta, comp.Shadow.Delta, comp.Shadow.Delta-comp.Primary.Delta) fmt.Fprintln(w) - - if comp.PromotionReady { - fmt.Fprintln(w, " PROMOTION: READY") - } else { - fmt.Fprintln(w, " PROMOTION: NOT READY") - } - fmt.Fprintf(w, " Reason: %s\n", comp.PromotionReason) - fmt.Fprintln(w) - fmt.Fprintf(w, " Rollback: %s\n", comp.RollbackContract) - fmt.Fprintln(w) } // runFlywheelStatus displays comprehensive flywheel health. diff --git a/cli/cmd/ao/metrics_flywheel_test.go b/cli/cmd/ao/metrics_flywheel_test.go index 1f9709c3f..b43acbf1d 100644 --- a/cli/cmd/ao/metrics_flywheel_test.go +++ b/cli/cmd/ao/metrics_flywheel_test.go @@ -332,7 +332,7 @@ func TestRunFlywheelStatus_JSONOutput(t *testing.T) { for _, rel := range []string{ filepath.Join(".agents", "findings"), filepath.Join(".agents", "planning-rules"), - filepath.Join(".agents", "pre-mortem-checks"), + filepath.Join(".agents", "premortem-checks"), filepath.Join(".agents", "rpi"), } { if err := os.MkdirAll(filepath.Join(dir, rel), 0o755); err != nil { @@ -345,7 +345,7 @@ func TestRunFlywheelStatus_JSONOutput(t *testing.T) { if err := os.WriteFile(filepath.Join(dir, ".agents", "planning-rules", "f-1.md"), []byte("x"), 0o644); err != nil { t.Fatal(err) } - if err := os.WriteFile(filepath.Join(dir, ".agents", "pre-mortem-checks", "f-1.md"), []byte("x"), 0o644); err != nil { + if err := os.WriteFile(filepath.Join(dir, ".agents", "premortem-checks", "f-1.md"), []byte("x"), 0o644); err != nil { t.Fatal(err) } queue := `{"source_epic":"ag-h83","timestamp":"2026-03-11T17:00:00Z","items":[{"title":"High one","type":"task","severity":"high","source":"council-finding","description":"d1","target_repo":"agentops","consumed":false}],"consumed":false,"claim_status":"available","claimed_by":null,"claimed_at":null,"consumed_by":null,"consumed_at":null} @@ -543,7 +543,7 @@ func TestPrintFlywheelStatus_ShowsPeriod(t *testing.T) { } } -func TestBuildNamespaceComparison_PromotionReady(t *testing.T) { +func TestBuildNamespaceComparison_ReportsDeltasOnly(t *testing.T) { primary := &types.FlywheelMetrics{ Sigma: 0.25, Rho: 0.60, @@ -559,71 +559,45 @@ func TestBuildNamespaceComparison_PromotionReady(t *testing.T) { Delta: 17.0, } comp := buildNamespaceComparison(primary, shadow, "experimental") - if !comp.PromotionReady { - t.Errorf("expected promotion ready: sigma %.3f > %.3f and rho %.3f >= %.3f, reason: %s", - shadow.Sigma, primary.Sigma, shadow.Rho, primary.Rho, comp.PromotionReason) - } if comp.SigmaDelta <= 0 { t.Errorf("expected positive sigma delta, got %v", comp.SigmaDelta) } } -func TestBuildNamespaceComparison_NotReady_SigmaWorse(t *testing.T) { - primary := &types.FlywheelMetrics{Sigma: 0.40, Rho: 0.60} - shadow := &types.FlywheelMetrics{Sigma: 0.30, Rho: 0.70} - comp := buildNamespaceComparison(primary, shadow, "shadow") - if comp.PromotionReady { - t.Error("expected promotion not ready when shadow sigma < primary sigma") - } - if !strings.Contains(comp.PromotionReason, "does not beat") { - t.Errorf("expected 'does not beat' in reason, got %q", comp.PromotionReason) - } -} - -func TestBuildNamespaceComparison_NotReady_RhoRegressed(t *testing.T) { - primary := &types.FlywheelMetrics{Sigma: 0.30, Rho: 0.70} - shadow := &types.FlywheelMetrics{Sigma: 0.35, Rho: 0.50} - comp := buildNamespaceComparison(primary, shadow, "shadow") - if comp.PromotionReady { - t.Error("expected promotion not ready when shadow rho regressed") - } - if !strings.Contains(comp.PromotionReason, "regressed") { - t.Errorf("expected 'regressed' in reason, got %q", comp.PromotionReason) - } -} - -func TestBuildNamespaceComparison_RollbackContract(t *testing.T) { - primary := &types.FlywheelMetrics{} - shadow := &types.FlywheelMetrics{} - comp := buildNamespaceComparison(primary, shadow, "shadow") - if comp.RollbackContract == "" { - t.Error("expected non-empty rollback contract") - } - if !strings.Contains(comp.RollbackContract, "Stop reading") { - t.Errorf("rollback contract should describe namespace routing, got %q", comp.RollbackContract) - } -} - func TestPrintNamespaceComparison_ContainsMetrics(t *testing.T) { comp := &namespaceComparison{ - Primary: &types.FlywheelMetrics{Sigma: 0.25, Rho: 0.60, SigmaRho: 0.15, Velocity: -0.02, Delta: 17.0}, - Shadow: &types.FlywheelMetrics{Sigma: 0.35, Rho: 0.65, SigmaRho: 0.23, Velocity: 0.06, Delta: 17.0}, - ShadowName: "experimental", - SigmaDelta: 0.10, - RhoDelta: 0.05, - VelocityDelta: 0.08, - PromotionReady: true, - PromotionReason: "Shadow sigma beats primary", - RollbackContract: "Stop reading shadow namespace", + Primary: &types.FlywheelMetrics{Sigma: 0.25, Rho: 0.60, SigmaRho: 0.15, Velocity: -0.02, Delta: 17.0}, + Shadow: &types.FlywheelMetrics{Sigma: 0.35, Rho: 0.65, SigmaRho: 0.23, Velocity: 0.06, Delta: 17.0}, + ShadowName: "experimental", + SigmaDelta: 0.10, + RhoDelta: 0.05, + VelocityDelta: 0.08, } var buf bytes.Buffer printNamespaceComparison(&buf, comp) got := buf.String() - checks := []string{"experimental", "sigma", "rho", "PROMOTION: READY", "Rollback"} + checks := []string{"experimental", "sigma", "rho", "velocity", "Delta"} for _, check := range checks { if !strings.Contains(got, check) { t.Errorf("expected %q in comparison output, got:\n%s", check, got) } } } + +func TestRunFlywheelCompareReportsWithoutRecommendation(t *testing.T) { + t.Chdir(t.TempDir()) + var buf bytes.Buffer + cmd := &cobra.Command{} + cmd.SetOut(&buf) + oldOutput := output + output = "table" + t.Cleanup(func() { output = oldOutput }) + if err := runFlywheelCompare(cmd, nil); err != nil { + t.Fatalf("runFlywheelCompare: %v", err) + } + got := buf.String() + if strings.Contains(got, "PROMOTION") || strings.Contains(got, "Rollback") { + t.Fatalf("flywheel compare emitted transition advice: %s", got) + } +} diff --git a/cli/cmd/ao/mind.go b/cli/cmd/ao/mind.go deleted file mode 100644 index 684505603..000000000 --- a/cli/cmd/ao/mind.go +++ /dev/null @@ -1,92 +0,0 @@ -//go:build flywheel - -// practices: [ai-assisted-dev, wiki-knowledge-surface] -package main - -import ( - "fmt" - "os" - "os/exec" - - "github.com/spf13/cobra" -) - -var mindCmd = &cobra.Command{ - Use: "mind", - Short: "Knowledge graph operations", - Long: `Scan, normalize, link, and index .agents/ markdown into an Obsidian knowledge graph. - -Wraps the mind Python tool (python3 -m mind). Subcommands: - scan Show what needs normalization - normalize Add/fix YAML frontmatter - link Insert wikilinks between related artifacts - index Rebuild the graph index - all Run full pipeline (normalize → link → index) - graph Show graph statistics - -By default, ao mind applies changes (--write). Use --dry-run to preview.`, -} - -func init() { - mindCmd.GroupID = "knowledge" - rootCmd.AddCommand(mindCmd) - - // Register subcommands - for _, sub := range []struct { - use, short string - }{ - {"scan", "Show what needs normalization"}, - {"normalize", "Add/fix YAML frontmatter on .agents/ markdown"}, - {"link", "Insert wikilinks between related artifacts"}, - {"index", "Rebuild the graph index"}, - {"all", "Run full pipeline (normalize → link → index)"}, - {"graph", "Show graph statistics"}, - } { - subcmd := sub // capture loop variable - mindCmd.AddCommand(&cobra.Command{ - Use: subcmd.use, - Short: subcmd.short, - RunE: mindRunFunc(subcmd.use), - }) - } -} - -// mindRunFunc returns a RunE handler that shells out to python3 -m mind . -func mindRunFunc(subcommand string) func(cmd *cobra.Command, args []string) error { - return func(cmd *cobra.Command, args []string) error { - pythonPath, err := exec.LookPath("python3") - if err != nil { - return fmt.Errorf("python3 not found: install Python 3 to use ao mind") - } - - mindArgs := []string{"-m", "mind", subcommand} - - // ao defaults to execute; mind defaults to dry-run. - // So: ao mind all → mind all --write - // ao mind all --dry-run → mind all (no --write) - if !GetDryRun() { - mindArgs = append(mindArgs, "--write") - } - - // Detect vault path from working directory - cwd, err := os.Getwd() - if err == nil { - mindArgs = append(mindArgs, "--vault", cwd) - } - - if GetVerbose() { - VerbosePrintf("Running: %s %v\n", pythonPath, mindArgs) - } - - execCmd := exec.Command(pythonPath, mindArgs...) - execCmd.Stdout = os.Stdout - execCmd.Stderr = os.Stderr - execCmd.Stdin = os.Stdin - - if err := execCmd.Run(); err != nil { - return fmt.Errorf("mind %s failed: %w", subcommand, err) - } - - return nil - } -} \ No newline at end of file diff --git a/cli/cmd/ao/mind_test.go b/cli/cmd/ao/mind_test.go deleted file mode 100644 index f9ac6a8c9..000000000 --- a/cli/cmd/ao/mind_test.go +++ /dev/null @@ -1,177 +0,0 @@ -//go:build flywheel - -// practices: [ai-assisted-dev, wiki-knowledge-surface] -package main - -import ( - "os" - "path/filepath" - "strings" - "testing" -) - -func writeFakePython3(t *testing.T) string { - t.Helper() - - binDir := t.TempDir() - pythonPath := filepath.Join(binDir, "python3") - script := `#!/bin/sh -set -eu -if [ -n "${MIND_TEST_ARGS_FILE:-}" ]; then - : > "$MIND_TEST_ARGS_FILE" - for arg in "$@"; do - printf '%s\n' "$arg" >> "$MIND_TEST_ARGS_FILE" - done -fi -exit "${MIND_TEST_EXIT_CODE:-0}" -` - if err := os.WriteFile(pythonPath, []byte(script), 0755); err != nil { - t.Fatalf("write fake python3: %v", err) - } - return binDir -} - -func readArgsFile(t *testing.T, path string) []string { - t.Helper() - - data, err := os.ReadFile(path) - if err != nil { - t.Fatalf("read args file: %v", err) - } - trimmed := strings.TrimSpace(string(data)) - if trimmed == "" { - return nil - } - return strings.Split(trimmed, "\n") -} - -func hasArg(args []string, want string) bool { - for _, arg := range args { - if arg == want { - return true - } - } - return false -} - -func TestMindRunFunc(t *testing.T) { - t.Run("dry-run inversion", func(t *testing.T) { - tests := []struct { - name string - dryRun bool - wantWrite bool - }{ - {name: "dry-run false adds --write", dryRun: false, wantWrite: true}, - {name: "dry-run true omits --write", dryRun: true, wantWrite: false}, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - origDryRun := dryRun - t.Cleanup(func() { dryRun = origDryRun }) - - _ = chdirTemp(t) - argsFile := filepath.Join(t.TempDir(), "mind-args.txt") - binDir := writeFakePython3(t) - - t.Setenv("MIND_TEST_ARGS_FILE", argsFile) - t.Setenv("MIND_TEST_EXIT_CODE", "0") - t.Setenv("PATH", binDir+string(os.PathListSeparator)+os.Getenv("PATH")) - - dryRun = tc.dryRun - if err := mindRunFunc("all")(mindCmd, nil); err != nil { - t.Fatalf("mindRunFunc returned error: %v", err) - } - - args := readArgsFile(t, argsFile) - gotWrite := hasArg(args, "--write") - if gotWrite != tc.wantWrite { - t.Fatalf("args=%v, has --write=%v, want %v", args, gotWrite, tc.wantWrite) - } - }) - } - }) - - t.Run("builds --vault from cwd", func(t *testing.T) { - origDryRun := dryRun - t.Cleanup(func() { dryRun = origDryRun }) - - cwd := chdirTemp(t) - argsFile := filepath.Join(t.TempDir(), "mind-args.txt") - binDir := writeFakePython3(t) - - t.Setenv("MIND_TEST_ARGS_FILE", argsFile) - t.Setenv("MIND_TEST_EXIT_CODE", "0") - t.Setenv("PATH", binDir+string(os.PathListSeparator)+os.Getenv("PATH")) - - dryRun = true - if err := mindRunFunc("graph")(mindCmd, nil); err != nil { - t.Fatalf("mindRunFunc returned error: %v", err) - } - - args := readArgsFile(t, argsFile) - vaultIdx := -1 - for i, arg := range args { - if arg == "--vault" { - vaultIdx = i - break - } - } - if vaultIdx == -1 { - t.Fatalf("expected --vault in args, got %v", args) - } - if vaultIdx+1 >= len(args) { - t.Fatalf("expected vault path after --vault, got %v", args) - } - gotVault := args[vaultIdx+1] - gotInfo, err := os.Stat(gotVault) - if err != nil { - t.Fatalf("stat vault path %q: %v", gotVault, err) - } - cwdInfo, err := os.Stat(cwd) - if err != nil { - t.Fatalf("stat cwd %q: %v", cwd, err) - } - if !os.SameFile(gotInfo, cwdInfo) { - t.Fatalf("vault path = %q, want path to cwd %q", gotVault, cwd) - } - }) - - t.Run("returns error when python3 is missing", func(t *testing.T) { - origDryRun := dryRun - t.Cleanup(func() { dryRun = origDryRun }) - - dryRun = false - t.Setenv("PATH", t.TempDir()) - - err := mindRunFunc("scan")(mindCmd, nil) - if err == nil { - t.Fatal("expected error when python3 is not on PATH") - } - if !strings.Contains(err.Error(), "python3 not found") { - t.Fatalf("expected python3-not-found error, got: %v", err) - } - }) - - t.Run("wraps command failures", func(t *testing.T) { - origDryRun := dryRun - t.Cleanup(func() { dryRun = origDryRun }) - - _ = chdirTemp(t) - argsFile := filepath.Join(t.TempDir(), "mind-args.txt") - binDir := writeFakePython3(t) - - t.Setenv("MIND_TEST_ARGS_FILE", argsFile) - t.Setenv("MIND_TEST_EXIT_CODE", "7") - t.Setenv("PATH", binDir+string(os.PathListSeparator)+os.Getenv("PATH")) - - dryRun = false - err := mindRunFunc("normalize")(mindCmd, nil) - if err == nil { - t.Fatal("expected error from failing python3 wrapper") - } - if !strings.Contains(err.Error(), "mind normalize failed") { - t.Fatalf("expected wrapped error, got: %v", err) - } - }) -} \ No newline at end of file diff --git a/cli/cmd/ao/negative_path_test.go b/cli/cmd/ao/negative_path_test.go index e07b64bd2..7ed512cb7 100644 --- a/cli/cmd/ao/negative_path_test.go +++ b/cli/cmd/ao/negative_path_test.go @@ -2,8 +2,6 @@ package main import ( - "os" - "path/filepath" "strings" "testing" ) @@ -16,16 +14,6 @@ func TestNegativePath_MissingArgs(t *testing.T) { args []string errSub string // substring expected in error message }{ - { - name: "goals add missing all args", - args: []string{"goals", "add"}, - errSub: "accepts 2 arg(s), received 0", - }, - { - name: "goals add missing check-command arg", - args: []string{"goals", "add", "my-goal"}, - errSub: "accepts 2 arg(s), received 1", - }, { name: "metrics cite missing artifact-path", args: []string{"metrics", "cite"}, @@ -64,11 +52,6 @@ func TestNegativePath_InvalidFlagValues(t *testing.T) { args: []string{"metrics", "report", "--days", "xyz"}, errSub: "invalid argument", }, - { - name: "goals add --weight not-a-number", - args: []string{"goals", "add", "my-id", "true", "--weight", "heavy"}, - errSub: "invalid argument", - }, } for _, tt := range tests { @@ -131,33 +114,6 @@ func TestNegativePath_UnknownNestedSubcommand(t *testing.T) { } } -// TestNegativePath_GoalsAddInvalidID verifies that goals add rejects a non-kebab-case ID. -// This test requires a valid GOALS.md on disk because Cobra argument validation -// passes (ExactArgs(2) is satisfied) and RunE proceeds to validate the ID format. -func TestNegativePath_GoalsAddInvalidID(t *testing.T) { - tmp := chdirTemp(t) - - // Create a minimal GOALS.md so the command can load goals. - goalsContent := `# Goals -## Version -4 -## Mission -Test -## Gates -` - if err := os.WriteFile(filepath.Join(tmp, "GOALS.md"), []byte(goalsContent), 0o644); err != nil { - t.Fatal(err) - } - - out, err := executeCommand("goals", "add", "NotKebabCase", "true", "--dry-run") - if err == nil { - t.Fatalf("expected error for non-kebab-case ID, got nil (output: %s)", out) - } - if !strings.Contains(err.Error(), "kebab-case") { - t.Errorf("error %q does not mention kebab-case requirement", err.Error()) - } -} - // TestNegativePath_ExcessArgs verifies that commands reject too many positional arguments. func TestNegativePath_ExcessArgs(t *testing.T) { tests := []struct { @@ -192,11 +148,6 @@ func TestNegativePath_UnknownFlags(t *testing.T) { args []string errSub string }{ - { - name: "goals add with unknown flag", - args: []string{"goals", "add", "my-id", "true", "--nonexistent"}, - errSub: "unknown flag", - }, { name: "metrics baseline with unknown flag", args: []string{"metrics", "baseline", "--nonexistent"}, @@ -229,9 +180,9 @@ func TestNegativePath_ErrorOutputNotEmpty(t *testing.T) { name string args []string }{ - {"missing args", []string{"goals", "add"}}, + {"missing args", []string{"metrics", "cite"}}, {"unknown command", []string{"nonexistent-command"}}, - {"unknown flag", []string{"goals", "add", "x", "true", "--bogus"}}, + {"unknown flag", []string{"goals", "measure", "--bogus"}}, } for _, tt := range tests { diff --git a/cli/cmd/ao/path_containment.go b/cli/cmd/ao/path_containment.go deleted file mode 100644 index d2af0d6b7..000000000 --- a/cli/cmd/ao/path_containment.go +++ /dev/null @@ -1,53 +0,0 @@ -// practices: [design-by-contract, code-complete] -package main - -import ( - "os" - "path/filepath" - "strings" -) - -// realpathOrSelf returns p as an absolute, symlink-resolved path. When p (or a leaf of it) -// doesn't exist, it resolves symlinks on the LONGEST existing prefix and keeps the rest, so -// a non-existent entry still compares consistently against an existing root (e.g. macOS -// /var -> /private/var) — required for correct containment checks. -func realpathOrSelf(p string) string { - abs, err := filepath.Abs(p) - if err != nil { - abs = filepath.Clean(p) - } - cur, remaining := abs, "" - for { - if resolved, err := filepath.EvalSymlinks(cur); err == nil { - if remaining == "" { - return resolved - } - return filepath.Join(resolved, remaining) - } - parent := filepath.Dir(cur) - if parent == cur { - return abs - } - remaining = filepath.Join(filepath.Base(cur), remaining) - cur = parent - } -} - -// pathInside reports whether child is at or below root (both should be realpath'd). -func pathInside(child, root string) bool { - rel, err := filepath.Rel(root, child) - if err != nil { - return false - } - return rel == "." || (rel != ".." && !strings.HasPrefix(rel, ".."+string(os.PathSeparator)) && !filepath.IsAbs(rel)) -} - -// pathInsideRepo reports whether path is at or below repo, resolving symlinks on -// both sides so a symlinked binary or checkout compares correctly. It is the ONE -// containment predicate the "no repo-internal-absolute binary is ever trusted" -// invariant rests on: trustedLookPath applies it to exclude a repo-planted git -// from PATH resolution (runtime), and `ao verify init` applies it to refuse -// baking a repo-internal ao path into the installed hook (install time). -func pathInsideRepo(path, repo string) bool { - return pathInside(realpathOrSelf(path), realpathOrSelf(repo)) -} diff --git a/cli/cmd/ao/provenance_position.go b/cli/cmd/ao/provenance_position.go index 44c4116ed..264fde117 100644 --- a/cli/cmd/ao/provenance_position.go +++ b/cli/cmd/ao/provenance_position.go @@ -13,94 +13,58 @@ var provPositionJSON bool var provenancePositionCmd = &cobra.Command{ Use: "position", - Short: "Report the navigator's current position from the provenance ledger", - Long: `Read the provenance ledger and report the navigator's current position: -which beads have landed (bead→commit wasGeneratedBy edges). This is the -read-only consumer that makes the fed ledger useful — the navigator's -"where am I" surface. - -Agent-ergonomic: --json emits stdout-as-data, diagnostics on stderr. - -Examples: - ao provenance position - ao provenance position --json`, - Args: cobra.NoArgs, - RunE: runProvenancePosition, + Short: "Report the read-only provenance ledger tip", + Long: "Report the ledger record count and latest hash without inferring lifecycle state.", + Args: cobra.NoArgs, + RunE: runProvenancePosition, } func init() { provenanceCmd.AddCommand(provenancePositionCmd) - provenancePositionCmd.Flags().BoolVar(&provPositionJSON, "json", false, "Emit machine-readable JSON (stdout-as-data)") + provenancePositionCmd.Flags().BoolVar(&provPositionJSON, "json", false, "Emit machine-readable JSON") +} + +type positionEdge struct { + FromID string `json:"from_id"` + ToID string `json:"to_id"` + Relation string `json:"relation"` + Hash string `json:"hash"` } -// positionReport is the structured output of ao provenance position --json. type positionReport struct { - LandedBeads []landedBeadEntry `json:"landed_beads"` - TotalEdges int `json:"total_edges"` + TotalEdges int `json:"total_edges"` + TipHash string `json:"tip_hash"` + Latest *positionEdge `json:"latest,omitempty"` } -// landedBeadEntry is one bead→commit landing from the ledger. -type landedBeadEntry struct { - BeadID string `json:"bead_id"` - CommitRef string `json:"commit_ref"` - TrustTier string `json:"trust_tier"` - Timestamp string `json:"ts"` -} - -// extractLandedBeads filters wasGeneratedBy bead→commit edges from the ledger. -func extractLandedBeads(edges []provenancegraph.Edge) []landedBeadEntry { - var landed []landedBeadEntry - for _, e := range edges { - if e.Relation == "wasGeneratedBy" && e.FromType == "bead" { - landed = append(landed, landedBeadEntry{ - BeadID: e.FromID, - CommitRef: e.ToID, - TrustTier: e.TrustTier, - Timestamp: e.TS, - }) - } - } - return landed -} - -// buildPositionReport constructs the full position report from raw ledger edges. func buildPositionReport(edges []provenancegraph.Edge) positionReport { - landed := extractLandedBeads(edges) - if landed == nil { - landed = []landedBeadEntry{} - } - return positionReport{ - LandedBeads: landed, - TotalEdges: len(edges), + report := positionReport{TotalEdges: len(edges)} + if len(edges) == 0 { + return report } + edge := edges[len(edges)-1] + report.TipHash = edge.Hash + report.Latest = &positionEdge{FromID: edge.FromID, ToID: edge.ToID, Relation: edge.Relation, Hash: edge.Hash} + return report } func runProvenancePosition(cmd *cobra.Command, _ []string) error { cmd.SilenceUsage = true - out := cmd.OutOrStdout() - - store := provenancegraph.NewStore(resolveLedgerPath()) - edges, err := store.Read() + edges, err := provenancegraph.NewStore(resolveLedgerPath()).Read() if err != nil { return fmt.Errorf("read provenance ledger: %w", err) } - report := buildPositionReport(edges) - if provPositionJSON { - enc := json.NewEncoder(out) + enc := json.NewEncoder(cmd.OutOrStdout()) enc.SetIndent("", " ") return enc.Encode(report) } - - if len(report.LandedBeads) == 0 { - fmt.Fprintln(out, "no landed arcs yet") + if report.Latest == nil { + fmt.Fprintln(cmd.OutOrStdout(), "provenance ledger is empty") return nil } - - fmt.Fprintf(out, "landed beads (%d):\n", len(report.LandedBeads)) - for _, lb := range report.LandedBeads { - fmt.Fprintf(out, " %s → %s [%s] %s\n", lb.BeadID, shortHash7(lb.CommitRef), lb.TrustTier, lb.Timestamp) - } + fmt.Fprintf(cmd.OutOrStdout(), "provenance ledger: %d edge(s), tip %s\n", report.TotalEdges, shortHash(report.TipHash)) + fmt.Fprintf(cmd.OutOrStdout(), "latest: %s --%s--> %s\n", report.Latest.FromID, report.Latest.Relation, report.Latest.ToID) return nil } diff --git a/cli/cmd/ao/provenance_position_test.go b/cli/cmd/ao/provenance_position_test.go index 834823e60..292254f8a 100644 --- a/cli/cmd/ao/provenance_position_test.go +++ b/cli/cmd/ao/provenance_position_test.go @@ -1,131 +1,26 @@ package main import ( - "path/filepath" "testing" "github.com/boshu2/agentops/cli/internal/provenancegraph" ) -// TestProvenancePositionCommand_Registered asserts the `ao provenance position` -// command is wired into the cobra tree under provenance with the --json flag. -func TestProvenancePositionCommand_Registered(t *testing.T) { - if provenancePositionCmd.Use != "position" { - t.Fatalf("Use = %q, want position", provenancePositionCmd.Use) - } - var found bool - for _, c := range provenanceCmd.Commands() { - if c.Use == "position" { - found = true - } - } - if !found { - t.Error("position is not registered under `ao provenance`") - } - if provenancePositionCmd.Flags().Lookup("json") == nil { - t.Error("missing flag --json on `ao provenance position`") - } -} - -// TestExtractLandedBeads_FiltersCorrectly exercises the core filter logic with -// edges round-tripped through the real store (fixture fidelity: production -// writer, production reader). -func TestExtractLandedBeads_FiltersCorrectly(t *testing.T) { - ledger := filepath.Join(t.TempDir(), "ledger.jsonl") - store := provenancegraph.NewStore(ledger) - - beadEdge := provenancegraph.Edge{ - FromID: "ag-test1", FromType: "bead", - ToID: "abc123def456", ToType: "commit", - Relation: "wasGeneratedBy", TrustTier: "inferred", - TS: "2026-06-15T00:00:00Z", EvidenceRef: "commit abc123def456", - } - if _, err := store.Append(beadEdge); err != nil { - t.Fatalf("append bead edge: %v", err) +func TestBuildPositionReportIsGenericLedgerTip(t *testing.T) { + empty := buildPositionReport(nil) + if empty.TotalEdges != 0 || empty.TipHash != "" || empty.Latest != nil { + t.Fatalf("unexpected empty report: %+v", empty) } - nonBeadEdge := provenancegraph.Edge{ - FromID: "soc-x1", FromType: "decision", - ToID: "ag-test1", ToType: "bead", - Relation: "wasInfluencedBy", TrustTier: "authored", - TS: "2026-06-15T00:01:00Z", EvidenceRef: "council verdict", + edges := []provenancegraph.Edge{ + {FromID: "intent-1", ToID: "artifact-1", Relation: "wasGeneratedBy", Hash: "aaa"}, + {FromID: "artifact-1", ToID: "learning-1", Relation: "wasInformedBy", Hash: "bbb"}, } - if _, err := store.Append(nonBeadEdge); err != nil { - t.Fatalf("append non-bead edge: %v", err) - } - - edges, err := store.Read() - if err != nil { - t.Fatalf("read: %v", err) - } - if len(edges) != 2 { - t.Fatalf("expected 2 edges, got %d", len(edges)) - } - - landed := extractLandedBeads(edges) - if len(landed) != 1 { - t.Fatalf("expected 1 landed bead, got %d", len(landed)) - } - if landed[0].BeadID != "ag-test1" { - t.Errorf("BeadID = %q, want ag-test1", landed[0].BeadID) - } - if landed[0].CommitRef != "abc123def456" { - t.Errorf("CommitRef = %q, want abc123def456", landed[0].CommitRef) - } - if landed[0].TrustTier != "inferred" { - t.Errorf("TrustTier = %q, want inferred", landed[0].TrustTier) - } -} - -// TestBuildPositionReport_EmptyLedger verifies graceful degradation on a -// genesis-only (empty) ledger — the second acceptance scenario. -func TestBuildPositionReport_EmptyLedger(t *testing.T) { - report := buildPositionReport(nil) - if len(report.LandedBeads) != 0 { - t.Errorf("expected 0 landed beads on nil edges, got %d", len(report.LandedBeads)) - } - if report.LandedBeads == nil { - t.Error("LandedBeads should be empty slice (not nil) for clean JSON marshaling") - } - if report.TotalEdges != 0 { - t.Errorf("TotalEdges = %d, want 0", report.TotalEdges) - } -} - -// TestBuildPositionReport_WithLandedBeads exercises the full report shape with -// edges from a real store round-trip. -func TestBuildPositionReport_WithLandedBeads(t *testing.T) { - ledger := filepath.Join(t.TempDir(), "ledger.jsonl") - store := provenancegraph.NewStore(ledger) - - for _, id := range []string{"ag-aaa", "ag-bbb"} { - edge := provenancegraph.Edge{ - FromID: id, FromType: "bead", - ToID: "commit" + id, ToType: "commit", - Relation: "wasGeneratedBy", TrustTier: "inferred", - TS: "2026-06-15T00:00:00Z", EvidenceRef: "commit commit" + id, - } - if _, err := store.Append(edge); err != nil { - t.Fatalf("append %s: %v", id, err) - } - } - - edges, err := store.Read() - if err != nil { - t.Fatalf("read: %v", err) - } - report := buildPositionReport(edges) - if len(report.LandedBeads) != 2 { - t.Fatalf("expected 2 landed beads, got %d", len(report.LandedBeads)) + if report.TotalEdges != 2 || report.TipHash != "bbb" || report.Latest == nil { + t.Fatalf("unexpected report: %+v", report) } - if report.TotalEdges != 2 { - t.Fatalf("TotalEdges = %d, want 2", report.TotalEdges) - } - if report.LandedBeads[0].BeadID != "ag-aaa" { - t.Errorf("first bead = %q, want ag-aaa", report.LandedBeads[0].BeadID) - } - if report.LandedBeads[1].BeadID != "ag-bbb" { - t.Errorf("second bead = %q, want ag-bbb", report.LandedBeads[1].BeadID) + if report.Latest.FromID != "artifact-1" || report.Latest.ToID != "learning-1" { + t.Fatalf("unexpected latest edge: %+v", report.Latest) } } diff --git a/cli/cmd/ao/provenance_show.go b/cli/cmd/ao/provenance_show.go index 05784afab..0be6cc1fe 100644 --- a/cli/cmd/ao/provenance_show.go +++ b/cli/cmd/ao/provenance_show.go @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" "io" - "strings" "github.com/spf13/cobra" @@ -15,277 +14,83 @@ import ( var provShowJSON bool var provenanceShowCmd = &cobra.Command{ - Use: "show ", - Short: "Render the human verdict lineage for one commit or bead", - Long: `Render the human story of one change from the committed provenance -ledger (docs/provenance/ledger.jsonl): which bead(s) landed as which commit -(bead --wasGeneratedBy--> commit), which verdict(s) reviewed that commit -(verdict --wasDerivedFrom--> commit, with disposition and evidence when -recorded), and each record's chain position in the ledger. - -The argument is either a commit sha (full, or a prefix of at least 7 hex -chars) or a bead id. A bead id resolves to every commit it landed as; a sha -renders every bead and verdict bound to it. Multiple verdicts on one commit -all render. A landed-but-unreviewed commit renders honestly with a -"no verdict recorded" line — per the membrane doctrine, no verdict = not done. - -Agent-ergonomic (Directive 13): --json emits stdout-as-data, diagnostics on -stderr. Unknown ids exit non-zero with a corrective error naming how to -search. - -Examples: - ao provenance show ag-x31t.4 - ao provenance show 4f2a91c - ao provenance show 4f2a91cd0e8b7a65331290fedcba9876543210ab --json`, + Use: "show ", + Short: "Show generic provenance relationships for one exact node", + Long: `Read the provenance ledger and show every edge whose from_id or to_id +exactly matches the supplied node. This command reports evidence; it does not +infer completion, landing, validation, or a next action.`, Args: cobra.ExactArgs(1), RunE: runProvenanceShow, } func init() { provenanceCmd.AddCommand(provenanceShowCmd) - provenanceShowCmd.Flags().BoolVar(&provShowJSON, "json", false, "Emit machine-readable JSON (stdout-as-data)") + provenanceShowCmd.Flags().BoolVar(&provShowJSON, "json", false, "Emit machine-readable JSON") } -// showReport is the structured output of ao provenance show --json. -type showReport struct { - // Query is the sha or bead id the report resolved. - Query string `json:"query"` - // Commits is the lineage of every commit the query resolved to. - Commits []commitLineage `json:"commits"` - // TotalRecords is the ledger record count, the denominator of every - // record position ("record 3/12"). - TotalRecords int `json:"total_records"` -} - -// commitLineage is the full provenance story of one landed commit. -type commitLineage struct { - CommitSHA string `json:"commit_sha"` - Beads []showBeadEntry `json:"beads"` - Verdicts []showVerdictEntry `json:"verdicts"` -} - -// showBeadEntry is one bead→commit landing (wasGeneratedBy) edge. -type showBeadEntry struct { - BeadID string `json:"bead_id"` - TrustTier string `json:"trust_tier"` - Timestamp string `json:"ts"` - // Record is the 1-based chain position of this edge in the ledger. - Record int `json:"record"` -} - -// showVerdictEntry is one verdict→commit review (wasDerivedFrom) edge. -type showVerdictEntry struct { - // VerdictID is the verdict node id (@ for pawl verdicts). - VerdictID string `json:"verdict_id"` - // Disposition is parsed from the evidence_ref when present ("CONFIRMED", - // "REFUTED", ...); empty when the record does not carry one. - Disposition string `json:"disposition,omitempty"` +type showEdge struct { + Record int `json:"record"` + Direction string `json:"direction"` + Counterpart string `json:"counterpart"` + Type string `json:"counterpart_type"` + Relation string `json:"relation"` EvidenceRef string `json:"evidence_ref,omitempty"` TrustTier string `json:"trust_tier"` Timestamp string `json:"ts"` - // Record is the 1-based chain position of this edge in the ledger. - Record int `json:"record"` - // v1.1 additive enrichment (age-rk3r.3): displayed only when the edge carries - // them (consumers branch on PRESENCE, never a version string). A v1-shaped - // verdict edge leaves them empty and they are omitted from the JSON. - ReviewerFamily string `json:"reviewer_family,omitempty"` - Degraded bool `json:"degraded,omitempty"` - Rounds int `json:"rounds,omitempty"` - DurationS float64 `json:"duration_s,omitempty"` - EvidencePath string `json:"evidence_path,omitempty"` + Hash string `json:"hash"` } -// isHexToken reports whether s is non-empty lowercase/uppercase hex. -func isHexToken(s string) bool { - if s == "" { - return false - } - for _, r := range s { +type showReport struct { + NodeID string `json:"node_id"` + Relationships []showEdge `json:"relationships"` + TotalRecords int `json:"total_records"` +} + +func buildShowReport(edges []provenancegraph.Edge, nodeID string) (showReport, error) { + report := showReport{NodeID: nodeID, Relationships: []showEdge{}, TotalRecords: len(edges)} + for i, edge := range edges { + view := showEdge{Record: i + 1, Relation: edge.Relation, EvidenceRef: edge.EvidenceRef, TrustTier: edge.TrustTier, Timestamp: edge.TS, Hash: edge.Hash} switch { - case r >= '0' && r <= '9', r >= 'a' && r <= 'f', r >= 'A' && r <= 'F': + case edge.FromID == nodeID: + view.Direction, view.Counterpart, view.Type = "outbound", edge.ToID, edge.ToType + case edge.ToID == nodeID: + view.Direction, view.Counterpart, view.Type = "inbound", edge.FromID, edge.FromType default: - return false + continue } + report.Relationships = append(report.Relationships, view) } - return true -} - -// parseDisposition extracts the "disposition=" token from an -// evidence_ref (the emit-verdict evidence convention: -// "pawl-verdict disposition="). Returns "" when absent. -func parseDisposition(evidenceRef string) string { - for _, field := range strings.Fields(evidenceRef) { - if v, ok := strings.CutPrefix(field, "disposition="); ok { - return v - } - } - return "" -} - -// minShaPrefixLen is the shortest commit-sha prefix `show` will resolve. -const minShaPrefixLen = 7 - -// resolveShowSHAs resolves a query (bead id, or a >=7-char sha prefix) to the -// full commit shas it names, in first-seen ledger order. Pure. -func resolveShowSHAs(edges []provenancegraph.Edge, query string) []string { - var shas []string - seen := map[string]bool{} - add := func(sha string) { - if sha != "" && !seen[sha] { - seen[sha] = true - shas = append(shas, sha) - } - } - - // Bead-id resolution first (exact match): landed edges plus verdict nodes - // (@), so a reviewed-but-never-landed bead still resolves. - for _, e := range edges { - if e.Relation == "wasGeneratedBy" && e.FromType == "bead" && - (e.FromID == query || e.BeadID == query) { - add(e.ToID) - } - if e.FromType == "verdict" && e.ToType == "commit" && - strings.HasPrefix(e.FromID, query+"@") { - add(e.ToID) - } - } - if len(shas) > 0 { - return shas - } - - // SHA-prefix resolution: at least 7 hex chars, matched case-insensitively - // against every commit-typed node id. - lq := strings.ToLower(query) - if len(lq) < minShaPrefixLen || !isHexToken(lq) { - return nil - } - for _, e := range edges { - if e.ToType == "commit" && strings.HasPrefix(strings.ToLower(e.ToID), lq) { - add(e.ToID) - } - if e.FromType == "commit" && strings.HasPrefix(strings.ToLower(e.FromID), lq) { - add(e.FromID) - } - } - return shas -} - -// buildShowReport resolves query against the ledger edges and assembles the -// per-commit lineage. Returns a corrective error (naming how to search) when -// nothing matches. Pure: no I/O, so it is the unit under test. -func buildShowReport(edges []provenancegraph.Edge, query string) (showReport, error) { - query = strings.TrimSpace(query) - shas := resolveShowSHAs(edges, query) - if len(shas) == 0 { - hint := "list landings with 'ao provenance position' or browse/filter edges with 'ao provenance list'" - if isHexToken(query) && len(query) < minShaPrefixLen { - hint = fmt.Sprintf("a sha prefix needs at least %d chars; %s", minShaPrefixLen, hint) - } - return showReport{}, fmt.Errorf("no provenance records match %q — %s", query, hint) - } - - report := showReport{Query: query, Commits: []commitLineage{}, TotalRecords: len(edges)} - for _, sha := range shas { - lin := commitLineage{CommitSHA: sha, Beads: []showBeadEntry{}, Verdicts: []showVerdictEntry{}} - for i, e := range edges { - record := i + 1 - if e.ToID != sha { - continue - } - switch { - case e.Relation == "wasGeneratedBy" && e.FromType == "bead" && e.ToType == "commit": - lin.Beads = append(lin.Beads, showBeadEntry{ - BeadID: e.FromID, - TrustTier: e.TrustTier, - Timestamp: e.TS, - Record: record, - }) - case e.Relation == "wasDerivedFrom" && e.FromType == "verdict" && e.ToType == "commit": - lin.Verdicts = append(lin.Verdicts, showVerdictEntry{ - VerdictID: e.FromID, - Disposition: parseDisposition(e.EvidenceRef), - EvidenceRef: e.EvidenceRef, - TrustTier: e.TrustTier, - Timestamp: e.TS, - Record: record, - ReviewerFamily: e.ReviewerFamily, - Degraded: e.Degraded, - Rounds: e.Rounds, - DurationS: e.DurationS, - EvidencePath: e.EvidencePath, - }) - } - } - report.Commits = append(report.Commits, lin) + if len(report.Relationships) == 0 { + return showReport{}, fmt.Errorf("node %q is not present in the provenance ledger", nodeID) } return report, nil } -// renderShowReport writes the human-readable lineage story. -func renderShowReport(out io.Writer, r showReport) { - for _, c := range r.Commits { - fmt.Fprintf(out, "commit %s\n", c.CommitSHA) - if len(c.Beads) == 0 { - fmt.Fprintln(out, " (no landed-bead edge recorded for this commit)") - } - for _, b := range c.Beads { - fmt.Fprintf(out, " bead %s [%s] %s (record %d/%d)\n", - b.BeadID, b.TrustTier, b.Timestamp, b.Record, r.TotalRecords) - } - if len(c.Verdicts) == 0 { - fmt.Fprintln(out, " no verdict recorded — landed but unreviewed") - } - for _, v := range c.Verdicts { - disp := v.Disposition - if disp == "" { - disp = "(unspecified)" - } - fmt.Fprintf(out, " verdict %s disposition=%s [%s] %s (record %d/%d)\n", - v.VerdictID, disp, v.TrustTier, v.Timestamp, v.Record, r.TotalRecords) - if v.EvidenceRef != "" { - fmt.Fprintf(out, " evidence: %s\n", v.EvidenceRef) - } - // v1.1 enrichment lines — rendered only when the edge carries them - // (a v1-shaped edge omits every one, so the output is unchanged). - if v.ReviewerFamily != "" { - fmt.Fprintf(out, " reviewer_family: %s\n", v.ReviewerFamily) - } - if v.Degraded { - fmt.Fprintf(out, " degraded: true\n") - } - if v.Rounds != 0 { - fmt.Fprintf(out, " rounds: %d\n", v.Rounds) - } - if v.DurationS != 0 { - fmt.Fprintf(out, " duration_s: %g\n", v.DurationS) - } - if v.EvidencePath != "" { - fmt.Fprintf(out, " evidence_path: %s\n", v.EvidencePath) - } +func renderShowReport(out io.Writer, report showReport) { + fmt.Fprintf(out, "node %s (%d relationship(s))\n", report.NodeID, len(report.Relationships)) + for _, edge := range report.Relationships { + fmt.Fprintf(out, " %s --%s--> %s [%s] record %d/%d\n", edge.Direction, edge.Relation, edge.Counterpart, edge.TrustTier, edge.Record, report.TotalRecords) + if edge.EvidenceRef != "" { + fmt.Fprintf(out, " evidence: %s\n", edge.EvidenceRef) } } } func runProvenanceShow(cmd *cobra.Command, args []string) error { cmd.SilenceUsage = true - - store := provenancegraph.NewStore(resolveLedgerPath()) - edges, err := store.Read() + edges, err := provenancegraph.NewStore(resolveLedgerPath()).Read() if err != nil { return fmt.Errorf("read provenance ledger: %w", err) } - report, err := buildShowReport(edges, args[0]) if err != nil { return err } - - out := cmd.OutOrStdout() if provShowJSON { - enc := json.NewEncoder(out) + enc := json.NewEncoder(cmd.OutOrStdout()) enc.SetIndent("", " ") return enc.Encode(report) } - renderShowReport(out, report) + renderShowReport(cmd.OutOrStdout(), report) return nil } diff --git a/cli/cmd/ao/provenance_show_test.go b/cli/cmd/ao/provenance_show_test.go new file mode 100644 index 000000000..4f1589014 --- /dev/null +++ b/cli/cmd/ao/provenance_show_test.go @@ -0,0 +1,30 @@ +package main + +import ( + "testing" + + "github.com/boshu2/agentops/cli/internal/provenancegraph" +) + +func TestBuildShowReportShowsGenericInboundAndOutboundEdges(t *testing.T) { + edges := []provenancegraph.Edge{ + {FromID: "intent-1", FromType: "decision", ToID: "artifact-1", ToType: "artifact", Relation: "wasGeneratedBy", TrustTier: "authored", Hash: "a"}, + {FromID: "artifact-1", FromType: "artifact", ToID: "learning-1", ToType: "learning", Relation: "wasInformedBy", TrustTier: "mined", Hash: "b"}, + } + report, err := buildShowReport(edges, "artifact-1") + if err != nil { + t.Fatal(err) + } + if len(report.Relationships) != 2 { + t.Fatalf("relationships = %d, want 2", len(report.Relationships)) + } + if report.Relationships[0].Direction != "inbound" || report.Relationships[1].Direction != "outbound" { + t.Fatalf("unexpected directions: %+v", report.Relationships) + } +} + +func TestBuildShowReportRejectsMissingNode(t *testing.T) { + if _, err := buildShowReport(nil, "missing"); err == nil { + t.Fatal("expected missing node error") + } +} diff --git a/cli/cmd/ao/registry.go b/cli/cmd/ao/registry.go index 78f8481c7..67039a2af 100644 --- a/cli/cmd/ao/registry.go +++ b/cli/cmd/ao/registry.go @@ -106,7 +106,7 @@ func runRegistryListCommand(cmd *cobra.Command, _ []string) error { data, err := os.ReadFile(regPath) if err != nil { - return fmt.Errorf("registry.json not found — run: bash scripts/generate-registry.sh") + return fmt.Errorf("registry.json not found — run: python3 scripts/generate-skill-mesh.py") } var reg registryFile diff --git a/cli/cmd/ao/removed_command_hint.go b/cli/cmd/ao/removed_command_hint.go index a16007f04..84f7d1b6d 100644 --- a/cli/cmd/ao/removed_command_hint.go +++ b/cli/cmd/ao/removed_command_hint.go @@ -10,8 +10,8 @@ import ( ) // removedCommand is the tombstone for a verb the default build no longer -// serves: what replaces it, and (when a build tag restores it) how to get the -// old surface back. The full map with the why lives in docs/MIGRATION.md — +// serves and what replaces it. The full map with the why lives in +// docs/MIGRATION.md — // these one-liners exist so the pointer appears at the moment of failure, // where a dev (or an agent following error strings) actually hits the wall. type removedCommand struct { @@ -88,9 +88,8 @@ var cathedralCutCommands = map[string]struct{}{ // removedCommandHint returns the tombstone hint for an "unknown command" // error whose verb was removed from the default build, or "" when the error -// is anything else. A verb the running binary actually registers (a flywheel -// or legacy build) never hints: the command exists there, so a usage error -// must not claim it was removed. +// is anything else. A registered verb always wins so a usage error cannot be +// mislabeled as removal. func removedCommandHint(root *cobra.Command, err error) string { if err == nil { return "" diff --git a/cli/cmd/ao/removed_command_hint_test.go b/cli/cmd/ao/removed_command_hint_test.go index 9f1096113..9eda4a199 100644 --- a/cli/cmd/ao/removed_command_hint_test.go +++ b/cli/cmd/ao/removed_command_hint_test.go @@ -76,9 +76,7 @@ func TestRemovedCommandHint_NotTombstoned(t *testing.T) { } } -// A flywheel/legacy build registers some tombstoned verbs as real commands. -// The hint must stay silent for any verb the running binary actually serves, -// so a restored command never gets a "was removed" message on a usage error. +// A registered verb always wins over a tombstone hint. func TestRemovedCommandHint_RegisteredVerbSuppressed(t *testing.T) { root := bareRoot() root.AddCommand(&cobra.Command{Use: "pawl"}) diff --git a/cli/cmd/ao/retrieval_bench.go b/cli/cmd/ao/retrieval_bench.go index 5e0177d91..666170026 100644 --- a/cli/cmd/ao/retrieval_bench.go +++ b/cli/cmd/ao/retrieval_bench.go @@ -568,7 +568,7 @@ func runLiveBench(k int, asJSON, global bool, corpusDir string) error { fmt.Println() if report.TotalLearnings == 0 { - fmt.Println("No learnings found. Run /retro or /post-mortem to populate the knowledge base.") + fmt.Println("No learnings found. Run /retro or /postmortem to populate the knowledge base.") return nil } diff --git a/cli/cmd/ao/root.go b/cli/cmd/ao/root.go index 858316c9d..8637489bb 100644 --- a/cli/cmd/ao/root.go +++ b/cli/cmd/ao/root.go @@ -101,13 +101,6 @@ func Execute() { } os.Exit(docErr.ExitCode()) } - var scanErr *corpusScanExitError - if errors.As(err, &scanErr) { - // The exit code IS the verdict for `ao corpus scan`: 1 means a leak - // marker (or unreadable file) was detected — fail closed. The - // report already went to stdout/stderr, so nothing more to surface. - os.Exit(scanErr.ExitCode()) - } var wikiHealthErr *wikiHealthExitError if errors.As(err, &wikiHealthErr) { // The exit code IS the verdict for `ao wiki lint`: 1 means blocking @@ -143,10 +136,7 @@ func init() { &cobra.Group{ID: "config", Title: "Configuration:"}, &cobra.Group{ID: "comms", Title: "Communication:"}, &cobra.Group{ID: "knowledge", Title: "Knowledge:"}, - // The corpus/flywheel surface is experimental-tier (unproven — ADR-0004, - // ADR-0011): kept and buildable, but demoted under its own header so the - // spine (proven) commands lead the `ao --help` surface (age-h4y3). - &cobra.Group{ID: "experimental", Title: "Experimental (corpus/flywheel):"}, + &cobra.Group{ID: "experimental", Title: "Optional knowledge tools:"}, ) // Global flags available to all commands diff --git a/cli/cmd/ao/root_group_experimental_flywheel_test.go b/cli/cmd/ao/root_group_experimental_flywheel_test.go deleted file mode 100644 index 42304b2ad..000000000 --- a/cli/cmd/ao/root_group_experimental_flywheel_test.go +++ /dev/null @@ -1,36 +0,0 @@ -//go:build flywheel - -package main - -import "testing" - -// TestExperimentalHelpGroupFlywheel is the flywheel-tagged half of the -// experimental-group demotion contract (age-h4y3 / age-nzwo). The spine test -// (root_group_experimental_test.go) asserts the 9 corpus/flywheel commands -// still compiled into the default build; this test asserts the 3 archived -// behind //go:build flywheel — `corpus`, `curate`, and `defrag` — are -// registered and carry the experimental GroupID once their tag is active. -// Together the two halves cover the full 12-command demotion set. -func TestExperimentalHelpGroupFlywheel(t *testing.T) { - var title string - for _, g := range rootCmd.Groups() { - if g.ID == "experimental" { - title = g.Title - } - } - if title != "Experimental (corpus/flywheel):" { - t.Fatalf("experimental group title = %q, want %q", title, "Experimental (corpus/flywheel):") - } - - archived := []string{"corpus", "curate", "defrag"} - for _, name := range archived { - cmd, _, err := rootCmd.Find([]string{name}) - if err != nil || cmd == nil || cmd.Name() != name { - t.Errorf("flywheel-archived command %q not registered under the flywheel tag", name) - continue - } - if cmd.GroupID != "experimental" { - t.Errorf("command %q GroupID = %q, want %q", name, cmd.GroupID, "experimental") - } - } -} diff --git a/cli/cmd/ao/root_group_experimental_test.go b/cli/cmd/ao/root_group_experimental_test.go index defa3198c..fb4f27f52 100644 --- a/cli/cmd/ao/root_group_experimental_test.go +++ b/cli/cmd/ao/root_group_experimental_test.go @@ -2,12 +2,8 @@ package main import "testing" -// TestExperimentalHelpGroup asserts the corpus/flywheel demotion contract -// (age-h4y3): the experimental cobra group is registered on rootCmd and every -// demoted corpus/flywheel command present in this (default/spine) build variant -// carries it. age-nzwo archived `corpus` and `curate` behind the flywheel tag, -// so they moved out of this default list into the flywheel-tagged variant in -// root_group_experimental_flywheel_test.go (which asserts the full 12). +// TestExperimentalHelpGroup asserts that optional knowledge commands do not +// become lifecycle authorities merely because they are available. func TestExperimentalHelpGroup(t *testing.T) { var title string for _, g := range rootCmd.Groups() { @@ -18,13 +14,10 @@ func TestExperimentalHelpGroup(t *testing.T) { if title == "" { t.Fatal("experimental cobra group not registered on rootCmd") } - if title != "Experimental (corpus/flywheel):" { - t.Fatalf("experimental group title = %q, want %q", title, "Experimental (corpus/flywheel):") + if title != "Optional knowledge tools:" { + t.Fatalf("experimental group title = %q, want %q", title, "Optional knowledge tools:") } - // corpus + curate + defrag are archived behind //go:build flywheel (age-nzwo) - // so they are absent from the spine build; the flywheel-tagged sibling test - // asserts them. demoted := []string{"flywheel", "store", "wiki"} for _, name := range demoted { cmd, _, err := rootCmd.Find([]string{name}) diff --git a/cli/cmd/ao/scope.go b/cli/cmd/ao/scope.go deleted file mode 100644 index 6150724cd..000000000 --- a/cli/cmd/ao/scope.go +++ /dev/null @@ -1,151 +0,0 @@ -// Package main: `ao scope` cobra subcommand for the /scope skill (issue -// soc-irg1.3). Manages .agents/scope.lock via cli/internal/scope. -// practices: [ddd-bounded-context, design-by-contract] -package main - -import ( - "encoding/json" - "errors" - "fmt" - "os" - "path/filepath" - "strings" - - "github.com/spf13/cobra" - - "github.com/boshu2/agentops/cli/internal/scope" -) - -// defaultScopeLockPath returns `.agents/scope.lock` relative to the current -// working directory. Wave 1 hardcodes this path; Wave 2 (issue I5) routes via -// lib/ao-paths.sh / cli/internal/paths. -func defaultScopeLockPath() string { - cwd, err := os.Getwd() - if err != nil { - return filepath.Join(".agents", "scope.lock") - } - return filepath.Join(cwd, ".agents", "scope.lock") -} - -var ( - scopeJSON bool - scopeLockFlag string -) - -var scopeCmd = &cobra.Command{ - Use: "scope", - Short: "Manage edit-scope guard (freeze, unfreeze, status)", - Long: `Declare which directories are in scope for the current work session. - -Edits outside the declared scope are hard-blocked by the -hooks/edit-scope-guard.sh PreToolUse hook. State lives in -.agents/scope.lock; mutations go through atomic temp+rename writes -(cli/internal/llmwiki.SafeAtomicWrite).`, -} - -var scopeFreezeCmd = &cobra.Command{ - Use: "freeze [...]", - Short: "Freeze one or more directories (additive)", - Args: cobra.MinimumNArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - path := scopeLockPath() - if err := scope.Freeze(path, args); err != nil { - return err - } - l, err := scope.Read(path) - if err != nil { - return err - } - return printStatus(cmd.OutOrStdout(), l, scopeJSON) - }, -} - -var scopeUnfreezeCmd = &cobra.Command{ - Use: "unfreeze [...]", - Short: "Unfreeze one (or all if no arg) directories", - RunE: func(cmd *cobra.Command, args []string) error { - path := scopeLockPath() - if err := scope.Unfreeze(path, args); err != nil { - return err - } - l, err := scope.Read(path) - if err != nil { - return err - } - return printStatus(cmd.OutOrStdout(), l, scopeJSON) - }, -} - -var scopeStatusCmd = &cobra.Command{ - Use: "status", - Short: "Show current scope-lock state", - RunE: func(cmd *cobra.Command, args []string) error { - path := scopeLockPath() - l, err := scope.Read(path) - if err != nil { - return err - } - return printStatus(cmd.OutOrStdout(), l, scopeJSON) - }, -} - -func scopeLockPath() string { - if scopeLockFlag != "" { - return scopeLockFlag - } - if v := os.Getenv("AO_SCOPE_LOCK"); v != "" { - return v - } - return defaultScopeLockPath() -} - -func printStatus(w interface{ Write([]byte) (int, error) }, l *scope.Lock, asJSON bool) error { - if l == nil { - return errors.New("scope: nil lock") - } - if asJSON { - enc := json.NewEncoder(w) - enc.SetIndent("", " ") - return enc.Encode(l) - } - if len(l.FrozenDirs) == 0 { - _, err := fmt.Fprintln(w, "scope: no frozen directories (enforcement off)") - return err - } - var b strings.Builder - fmt.Fprintf(&b, "scope: %d frozen director", len(l.FrozenDirs)) - if len(l.FrozenDirs) == 1 { - b.WriteString("y") - } else { - b.WriteString("ies") - } - b.WriteString(" (acquired_at=") - if !l.AcquiredAt.IsZero() { - b.WriteString(l.AcquiredAt.UTC().Format("2006-01-02T15:04:05Z")) - } else { - b.WriteString("unknown") - } - b.WriteString(", acquired_by=") - if l.AcquiredBy != "" { - b.WriteString(l.AcquiredBy) - } else { - b.WriteString("unknown") - } - b.WriteString(")\n") - for _, d := range l.FrozenDirs { - b.WriteString(" - ") - b.WriteString(d) - b.WriteString("\n") - } - _, err := fmt.Fprint(w, b.String()) - return err -} - -func init() { - scopeCmd.PersistentFlags().BoolVar(&scopeJSON, "json", false, "Emit JSON output") - scopeCmd.PersistentFlags().StringVar(&scopeLockFlag, "lock", "", "Override scope-lock path (defaults to $AO_SCOPE_LOCK or .agents/scope.lock)") - scopeCmd.AddCommand(scopeFreezeCmd) - scopeCmd.AddCommand(scopeUnfreezeCmd) - scopeCmd.AddCommand(scopeStatusCmd) - rootCmd.AddCommand(scopeCmd) -} diff --git a/cli/cmd/ao/scope_test.go b/cli/cmd/ao/scope_test.go deleted file mode 100644 index 74a960a22..000000000 --- a/cli/cmd/ao/scope_test.go +++ /dev/null @@ -1,115 +0,0 @@ -// practices: [ddd-bounded-context, design-by-contract] -package main - -import ( - "bytes" - "encoding/json" - "os" - "path/filepath" - "testing" -) - -func TestScopeCommand_Registered(t *testing.T) { - cmd, _, err := rootCmd.Find([]string{"scope"}) - if err != nil { - t.Fatalf("scope command not found on rootCmd: %v", err) - } - if cmd.Name() != "scope" { - t.Fatalf("want scope, got %q", cmd.Name()) - } - wantSubs := map[string]bool{"freeze": false, "unfreeze": false, "status": false} - for _, sub := range cmd.Commands() { - if _, ok := wantSubs[sub.Name()]; ok { - wantSubs[sub.Name()] = true - } - } - for name, present := range wantSubs { - if !present { - t.Errorf("scope subcommand %q missing", name) - } - } -} - -func TestScopeStatusJSON_EmptyLock(t *testing.T) { - lock := filepath.Join(t.TempDir(), "scope.lock") - t.Setenv("AO_SCOPE_LOCK", lock) - scopeJSON = true - defer func() { scopeJSON = false }() - - cmd, _, err := rootCmd.Find([]string{"scope", "status"}) - if err != nil { - t.Fatalf("status: %v", err) - } - buf := &bytes.Buffer{} - cmd.SetOut(buf) - t.Cleanup(func() { cmd.SetOut(nil); cmd.SetErr(nil) }) // age-ztf8: shared scope-status cmd; don't leak the writer - if err := cmd.RunE(cmd, nil); err != nil { - t.Fatalf("RunE: %v", err) - } - var out map[string]interface{} - if err := json.Unmarshal(buf.Bytes(), &out); err != nil { - t.Fatalf("not JSON: %v / %q", err, buf.String()) - } - if v, ok := out["schema_version"].(float64); !ok || v != 1 { - t.Fatalf("schema_version: %v", out["schema_version"]) - } -} - -func TestScopeFreezeThenStatus_NonJSON(t *testing.T) { - lock := filepath.Join(t.TempDir(), "scope.lock") - t.Setenv("AO_SCOPE_LOCK", lock) - scopeJSON = false - - freezeCmd, _, err := rootCmd.Find([]string{"scope", "freeze"}) - if err != nil { - t.Fatalf("freeze find: %v", err) - } - buf := &bytes.Buffer{} - freezeCmd.SetOut(buf) - t.Cleanup(func() { freezeCmd.SetOut(nil); freezeCmd.SetErr(nil) }) // age-ztf8: don't leak the writer - if err := freezeCmd.RunE(freezeCmd, []string{"cli/cmd/ao/"}); err != nil { - t.Fatalf("freeze RunE: %v", err) - } - if !bytes.Contains(buf.Bytes(), []byte("cli/cmd/ao")) { - t.Fatalf("output missing dir: %q", buf.String()) - } - - if _, err := os.Stat(lock); err != nil { - t.Fatalf("lock not created: %v", err) - } - - statusCmd, _, err := rootCmd.Find([]string{"scope", "status"}) - if err != nil { - t.Fatalf("status find: %v", err) - } - buf2 := &bytes.Buffer{} - statusCmd.SetOut(buf2) - t.Cleanup(func() { statusCmd.SetOut(nil); statusCmd.SetErr(nil) }) // age-ztf8: high blast radius (statusCmd has many callers) - if err := statusCmd.RunE(statusCmd, nil); err != nil { - t.Fatalf("status RunE: %v", err) - } - if !bytes.Contains(buf2.Bytes(), []byte("1 frozen")) { - t.Fatalf("status output missing count: %q", buf2.String()) - } -} - -func TestScopeUnfreezeAll(t *testing.T) { - lock := filepath.Join(t.TempDir(), "scope.lock") - t.Setenv("AO_SCOPE_LOCK", lock) - scopeJSON = false - - freezeCmd, _, _ := rootCmd.Find([]string{"scope", "freeze"}) - if err := freezeCmd.RunE(freezeCmd, []string{"a/", "b/"}); err != nil { - t.Fatalf("freeze: %v", err) - } - unfreezeCmd, _, _ := rootCmd.Find([]string{"scope", "unfreeze"}) - buf := &bytes.Buffer{} - unfreezeCmd.SetOut(buf) - t.Cleanup(func() { unfreezeCmd.SetOut(nil); unfreezeCmd.SetErr(nil) }) // age-ztf8: don't leak the writer - if err := unfreezeCmd.RunE(unfreezeCmd, nil); err != nil { - t.Fatalf("unfreeze RunE: %v", err) - } - if !bytes.Contains(buf.Bytes(), []byte("no frozen")) { - t.Fatalf("unfreeze output missing 'no frozen': %q", buf.String()) - } -} diff --git a/cli/cmd/ao/skills_query.go b/cli/cmd/ao/skills_query.go index 2172b69ea..69949a3ce 100644 --- a/cli/cmd/ao/skills_query.go +++ b/cli/cmd/ao/skills_query.go @@ -29,7 +29,7 @@ var skillsListCmd = &cobra.Command{ Short: "Query the generated skill catalog (skills/catalog.json)", Long: `Filter the generated skill catalog by hexagonal role, produced or consumed port, declared practice, or user-invocability. Reads -skills/catalog.json (emitted by scripts/generate-skill-catalog.sh and kept +skills/catalog.json (emitted by scripts/generate-skill-mesh.py and kept in sync by CI), so queries are fast and never re-parse SKILL.md frontmatter. Examples: @@ -105,7 +105,7 @@ func loadCatalogOrErr(cmd *cobra.Command) (*skills.Catalog, error) { cat, err := skills.LoadCatalog(skillsDir) if err != nil { cmd.SilenceUsage = true - return nil, fmt.Errorf("%w; run `scripts/generate-skill-catalog.sh` to (re)build it", err) + return nil, fmt.Errorf("%w; run `python3 scripts/generate-skill-mesh.py` to (re)build it", err) } return cat, nil } diff --git a/cli/cmd/ao/testdata/golden/ratchet-status-cycle.txt b/cli/cmd/ao/testdata/golden/ratchet-status-cycle.txt index 39f5ee56b..e919c580b 100644 --- a/cli/cmd/ao/testdata/golden/ratchet-status-cycle.txt +++ b/cli/cmd/ao/testdata/golden/ratchet-status-cycle.txt @@ -9,7 +9,7 @@ Parent: ag-parent STEP STATUS OUTPUT ---- ------ ------ research ✓ locked findings.md -premortem ✓ locked pre-mortem.md +premortem ✓ locked premortem.md plan ○ pending implement ○ pending crank ○ pending diff --git a/cli/cmd/ao/testdata/golden/ratchet-status-partial.json b/cli/cmd/ao/testdata/golden/ratchet-status-partial.json index 8f47c84ed..b9be4c0b6 100644 --- a/cli/cmd/ao/testdata/golden/ratchet-status-partial.json +++ b/cli/cmd/ao/testdata/golden/ratchet-status-partial.json @@ -12,7 +12,7 @@ { "step": "premortem", "status": "locked", - "output": "pre-mortem.md", + "output": "premortem.md", "time": "2025-06-15T09:20:00Z" }, { diff --git a/cli/cmd/ao/testutil_archived_globals_legacy_test.go b/cli/cmd/ao/testutil_archived_globals_legacy_test.go deleted file mode 100644 index 463c78e51..000000000 --- a/cli/cmd/ao/testutil_archived_globals_legacy_test.go +++ /dev/null @@ -1,69 +0,0 @@ -//go:build legacy - -// practices: [tdd] -package main - -import "testing" - -// snapshotArchivedCommandGlobals saves the cobra-flag globals owned by commands -// archived behind //go:build legacy (the codex lifecycle command + autodev), -// resets them to their flag defaults, and returns a restore closure. It lives in -// the tagged test build so the spine (default) test build never references -// archived symbols (age-h4y3). Use it directly with `defer` (executeCommand) or -// via resetArchivedCommandGlobals for the t.Cleanup shape (resetGlobalFlags). -// Mirrors the save/restore/reset discipline in .claude/rules/go.md "Test isolation". -func snapshotArchivedCommandGlobals() func() { - origCodexStartLimit := codexStartLimit - origCodexStartQuery := codexStartQuery - origCodexStartNoMaintenance := codexStartNoMaintenance - origCodexStopSessionID := codexStopSessionID - origCodexStopTranscriptPath := codexStopTranscriptPath - origCodexStopAutoExtract := codexStopAutoExtract - origCodexStopNoHistoryFallback := codexStopNoHistoryFallback - origCodexStopCloseLoop := codexStopCloseLoop - origCodexStopNoCloseLoop := codexStopNoCloseLoop - origCodexStatusDays := codexStatusDays - origCodexDispatchPacketPath := codexDispatchPacketPath - origAutodevFile := autodevFile - origAutodevForce := autodevForce - - restore := func() { - codexStartLimit = origCodexStartLimit - codexStartQuery = origCodexStartQuery - codexStartNoMaintenance = origCodexStartNoMaintenance - codexStopSessionID = origCodexStopSessionID - codexStopTranscriptPath = origCodexStopTranscriptPath - codexStopAutoExtract = origCodexStopAutoExtract - codexStopNoHistoryFallback = origCodexStopNoHistoryFallback - codexStopCloseLoop = origCodexStopCloseLoop - codexStopNoCloseLoop = origCodexStopNoCloseLoop - codexStatusDays = origCodexStatusDays - codexDispatchPacketPath = origCodexDispatchPacketPath - autodevFile = origAutodevFile - autodevForce = origAutodevForce - } - - // Reset to flag defaults. - codexStartLimit = 3 - codexStartQuery = "" - codexStartNoMaintenance = false - codexStopSessionID = "" - codexStopTranscriptPath = "" - codexStopAutoExtract = true - codexStopNoHistoryFallback = false - codexStopCloseLoop = false - codexStopNoCloseLoop = false - codexStatusDays = 7 - codexDispatchPacketPath = "" - autodevFile = "" - autodevForce = false - - return restore -} - -// resetArchivedCommandGlobals is the t.Cleanup shape over -// snapshotArchivedCommandGlobals, for helpers that own state via testing.T. -func resetArchivedCommandGlobals(t *testing.T) { - t.Helper() - t.Cleanup(snapshotArchivedCommandGlobals()) -} diff --git a/cli/cmd/ao/testutil_archived_globals_test.go b/cli/cmd/ao/testutil_archived_globals_test.go index 17c839e01..668986c78 100644 --- a/cli/cmd/ao/testutil_archived_globals_test.go +++ b/cli/cmd/ao/testutil_archived_globals_test.go @@ -1,14 +1,10 @@ -//go:build !legacy - // practices: [tdd] package main import "testing" -// snapshotArchivedCommandGlobals / resetArchivedCommandGlobals are no-ops in the -// spine (default) test build: the codex + autodev cobra-flag globals they manage -// exist only in the //go:build legacy archive (age-h4y3). The tagged twin -// (testutil_archived_globals_legacy_test.go) does the real save/restore/reset. +// The Cathedral Cut removed archived lifecycle command globals. These no-op +// helpers keep shared test setup simple without reviving an alternate build. func snapshotArchivedCommandGlobals() func() { return func() {} } func resetArchivedCommandGlobals(_ *testing.T) {} diff --git a/cli/cmd/ao/testutil_test.go b/cli/cmd/ao/testutil_test.go index cd4cbb856..e782ffd98 100644 --- a/cli/cmd/ao/testutil_test.go +++ b/cli/cmd/ao/testutil_test.go @@ -262,8 +262,6 @@ func resetCommandState(t *testing.T) { origGoalsMeasureExcludeTag := goalsMeasureExcludeTag origGoalsMeasureTotalTimeout := goalsMeasureTotalTimeout origGoalsMeasureScenariosOnly := goalsMeasureScenariosOnly - origGoalsInitNonInteractive := goalsInitNonInteractive - origGoalsInitTemplate := goalsInitTemplate origGoalsRenderOut := goalsRenderOut origMemorySyncQuiet := memorySyncQuiet origMemorySyncMaxEntries := memorySyncMaxEntries @@ -275,9 +273,8 @@ func resetCommandState(t *testing.T) { origSearchUseSC := searchUseSC origSearchUseCASS := searchUseCASS origSearchUseLocal := searchUseLocal - // codex + autodev cobra-flag globals live in the //go:build legacy archive - // (age-h4y3). Their save/restore/reset is delegated to a tagged helper so the - // untagged (spine) test build never references archived symbols. + // No alternate lifecycle build exists; the helper remains a no-op so shared + // test setup has one stable call site. resetArchivedCommandGlobals(t) origFindingsListLimit := findingsListLimit origFindingsListAll := findingsListAll @@ -307,8 +304,6 @@ func resetCommandState(t *testing.T) { goalsMeasureExcludeTag = origGoalsMeasureExcludeTag goalsMeasureTotalTimeout = origGoalsMeasureTotalTimeout goalsMeasureScenariosOnly = origGoalsMeasureScenariosOnly - goalsInitNonInteractive = origGoalsInitNonInteractive - goalsInitTemplate = origGoalsInitTemplate goalsRenderOut = origGoalsRenderOut memorySyncQuiet = origMemorySyncQuiet memorySyncMaxEntries = origMemorySyncMaxEntries @@ -351,8 +346,6 @@ func resetCommandState(t *testing.T) { goalsMeasureExcludeTag = "" goalsMeasureTotalTimeout = 0 goalsMeasureScenariosOnly = false - goalsInitNonInteractive = false - goalsInitTemplate = "" goalsRenderOut = "" output = "table" memorySyncQuiet = false diff --git a/cli/cmd/ao/zzz_default_spine.go b/cli/cmd/ao/zzz_default_spine.go index d52f2b3f6..f12d2c934 100644 --- a/cli/cmd/ao/zzz_default_spine.go +++ b/cli/cmd/ao/zzz_default_spine.go @@ -7,12 +7,11 @@ import ( "github.com/spf13/cobra" ) -// defaultSpineCommands is the executable ADR-0012 membership boundary. The -// archive tags restore the complete registered tree; an untagged production -// build removes satellite registrations so those paths cannot execute. +// defaultSpineCommands is the published CLI membership boundary. Optional +// source packages do not become public commands merely by registering in tests. var defaultSpineCommands = map[string]struct{}{ "capabilities": {}, "config": {}, "constraint": {}, "doctor": {}, - "gate": {}, "goals": {}, + "flywheel": {}, "gate": {}, "goals": {}, "init": {}, "provenance": {}, "quick-start": {}, "robot-docs": {}, "session": {}, "skills": {}, "status": {}, "version": {}, @@ -26,10 +25,9 @@ var defaultSpineCommands = map[string]struct{}{ func init() { installRemovedCommandTombstones(rootCmd) - // The package test binary deliberately retains every registration so focused - // tests for archived commands remain runnable without compiling the suite - // repeatedly under every tag. Production/default binaries take the boundary. - if len(archiveBuildTags) != 0 || strings.HasSuffix(os.Args[0], ".test") { + // The package test binary retains registrations so focused command tests can + // exercise optional surfaces. Production binaries take the boundary. + if strings.HasSuffix(os.Args[0], ".test") { return } pruneToDefaultSpine(rootCmd) @@ -37,7 +35,7 @@ func init() { // pruneToDefaultSpine applies the production membership boundary and returns // the commands it removed. Tests use the returned slice to restore the full -// archive tree after checking the production view. +// registered test tree after checking the production view. func pruneToDefaultSpine(root *cobra.Command) []*cobra.Command { var removed []*cobra.Command for _, command := range append([]*cobra.Command(nil), root.Commands()...) { diff --git a/cli/docs/COMMANDS.md b/cli/docs/COMMANDS.md index c5ca0ea2f..1288d63a5 100644 --- a/cli/docs/COMMANDS.md +++ b/cli/docs/COMMANDS.md @@ -389,6 +389,14 @@ ao goals history [flags] --since string Show entries since date (YYYY-MM-DD) ``` +#### `ao goals meta` + +Run and report meta-goals only + +``` +ao goals meta [flags] +``` + #### `ao goals render` Render the executable-spec layer as BDD/Gherkin text. @@ -406,7 +414,7 @@ ao goals render [flags] #### `ao goals scenarios` -List or create the executable-spec scenarios linked to GOALS.md directives. +Inspect the executable-spec scenarios linked to GOALS.md directives. ``` ao goals scenarios [flags] @@ -415,15 +423,11 @@ ao goals scenarios [flags] **Flags:** ``` - --create string Create a scenario from this goal description and link it to --directive - --directive int Directive display number (filter when listing, target when creating) + --directive int Filter by directive display number --directive-id string Filter listing to one directive by stable Directive ID -h, --help help for scenarios --lint Lint the directive↔scenario link graph instead of listing - --source string Source for a created scenario (human, agent, prod-telemetry) (default "human") - --status string Status for a created scenario (active, draft, retired) (default "draft") --strict With --lint, exit non-zero on warnings as well as errors - --threshold float Satisfaction threshold for a created scenario (default 0.8) ``` #### `ao goals trace` @@ -443,88 +447,6 @@ ao goals trace [flags] --strict Escalate warning-class defects to a non-zero exit (ADR-0005 §4.2) ``` -#### `ao goals add` - -Add a new goal - -``` -ao goals add [flags] -``` - -**Aliases:** - -``` - add, a -``` - -**Flags:** - -``` - --description string Goal description - -h, --help help for add - --type string Goal type (health, architecture, quality, meta) - --weight int Goal weight (1-10) (default 5) -``` - -#### `ao goals init` - -Bootstrap a new GOALS.md file - -``` -ao goals init [flags] -``` - -**Flags:** - -``` - -h, --help help for init - --non-interactive Use defaults without prompting - --template string Goal template (go-cli, python-lib, web-app, rust-cli, generic) -``` - -#### `ao goals meta` - -Run and report meta-goals only - -``` -ao goals meta [flags] -``` - -#### `ao goals migrate` - -Migrate goals between formats. - -``` -ao goals migrate [flags] -``` - -**Aliases:** - -``` - migrate, mg -``` - -**Flags:** - -``` - -h, --help help for migrate - --to-md Convert GOALS.yaml to GOALS.md format -``` - -#### `ao goals prune` - -Remove goals referencing nonexistent files - -``` -ao goals prune [flags] -``` - -**Aliases:** - -``` - prune, p -``` - --- ### `ao session` @@ -657,6 +579,49 @@ ao config models [flags] --- +### `ao flywheel` + +Knowledge flywheel operations and status. + +``` +ao flywheel [command] +``` + +**Subcommands:** + +#### `ao flywheel compare` + +Compare retrieval quality between primary and shadow namespaces. + +``` +ao flywheel compare [flags] +``` + +**Flags:** + +``` + -h, --help help for compare + --shadow string Shadow namespace to compare against primary (default "shadow") +``` + +#### `ao flywheel status` + +Display comprehensive flywheel health status. + +``` +ao flywheel status [flags] +``` + +**Flags:** + +``` + --days int Period in days for metrics calculation (default 7) + -h, --help help for status + --namespace string Citation namespace to evaluate (primary by default) (default "primary") +``` + +--- + ### `ao claim` Removed in the AgentOps Cathedral Cut @@ -860,7 +825,7 @@ ao provenance mine-session --file [flags] #### `ao provenance position` -Read the provenance ledger and report the navigator's current position: +Report the ledger record count and latest hash without inferring lifecycle state. ``` ao provenance position [flags] @@ -870,22 +835,22 @@ ao provenance position [flags] ``` -h, --help help for position - --json Emit machine-readable JSON (stdout-as-data) + --json Emit machine-readable JSON ``` #### `ao provenance show` -Render the human story of one change from the committed provenance +Read the provenance ledger and show every edge whose from_id or to_id ``` -ao provenance show [flags] +ao provenance show [flags] ``` **Flags:** ``` -h, --help help for show - --json Emit machine-readable JSON (stdout-as-data) + --json Emit machine-readable JSON ``` #### `ao provenance trace` diff --git a/cli/embedded/skills/standards/references/cli-wireup-template.md b/cli/embedded/skills/standards/references/cli-wireup-template.md deleted file mode 100644 index 3db9b174b..000000000 --- a/cli/embedded/skills/standards/references/cli-wireup-template.md +++ /dev/null @@ -1,187 +0,0 @@ -# CLI Wireup Template (Go + cobra + Hexagonal Adapter) - -The reproducible cycle-shape for exposing a production adapter as an `ao` -subcommand. Empirically derived across `/evolve` cycles 144-146 (three -`ao` subcommands shipped in ~10 minutes each, ~250-335 LOC including -tests + docs). Captured durably here so any future port-to-subcommand -slice is mechanical. - -Mirror of `docs/learnings/2026-05-13-cli-wiring-cycle-shape.md` (canonical -source). Copied per CI's no-symlinks rule so `/standards`-consuming agents -discover it via the skill-link path. - -## The 3 Reference Cycles (Empirical Baseline) - -| Cycle | Subcommand | Adapter | Time | LOC | Tests | -|---|---|---|---|---|---| -| 144 | `ao loop history` | `productionLoopReader` | ~10 min | 335 | 6 | -| 145 | `ao ci latest/recent` | `productionCIStatus` | ~8 min | 258 | 4 | -| 146 | `ao corpus inject` | `productionCorpusReader` | ~8 min | 252 | 5 | - -**8-10 minutes wall-clock**, ~250-335 LOC. Adapter-side complexity -dominates the variation — Loop took longer because it needed JSON -slicing logic; CI was simplest because the adapter already had a clean -stub-injectable shape. - -## The Template - -```go -// cli/cmd/ao/.go -var Cmd = &cobra.Command{ - Use: "", - Short: "BC operations", -} - -var Cmd = &cobra.Command{ - Use: " [flags]", - Short: "Short imperative description", - Long: `Long description with Examples block.`, - RunE: run, -} - -type Options struct { - // flag-derived fields - writer io.Writer - // injectFn lets tests substitute the port without real I/O - injectFn func(ctx context.Context, opts Options) ([]ports.X, error) -} - -func init() { - Cmd.GroupID = "core" - rootCmd.AddCommand(Cmd) - // flag registrations - Cmd.AddCommand(Cmd) -} - -func run(cmd *cobra.Command, _ []string) error { - // pull flag values, build options, delegate - return Run(cmd.Context(), opts) -} - -func Run(ctx context.Context, opts Options) error { - if opts.writer == nil { opts.writer = os.Stdout } - fn := opts.injectFn - if fn == nil { fn = ViaPort } - items, err := fn(ctx, opts) - if err != nil { return fmt.Errorf(" : %w", err) } - enc := json.NewEncoder(opts.writer) - for _, item := range items { - if err := enc.Encode(item); err != nil { - return fmt.Errorf(" encode: %w", err) - } - } - return nil -} - -func ViaPort(ctx context.Context, opts Options) ([]ports.X, error) { - adapter := newProduction(/* construction args */) - return adapter.(ctx, /* args */) -} -``` - -```go -// cli/cmd/ao/_test.go -// 4-6 tests covering: -// - stub returns N items → N lines emitted -// - stub returns empty → 0 bytes emitted -// - stub error → wrapped error -// - live root (filesystem fixture) → walks correctly -// - flag combinations (limit, range, etc.) honored -``` - -After the `.go` + `_test.go` files: `scripts/generate-cli-reference.sh` -regenerates `cli/docs/COMMANDS.md`. **Don't forget `generate-registry.sh` -too** — the `cli-command-dual-generator` learning documents the failure -mode where one is regenerated and the other isn't. - -## Why This Shape Works - -1. **Parent noun + verb subcommands.** `ao loop history` reads better - than the flat `loop-history` spelling. The parent groups future subcommands - (`ao loop write`, `ao loop tail`) under one verb-space. cobra - handles this natively. - -2. **Injectable function field on Options.** Production runs use the - default port wrapper; tests substitute a stub. This is the same - pattern cycle 117's `productionCIStatus.runGH` proved — refined here - from a struct field to an option-bag function. Faster than - fake-file-tree harnesses and platform-neutral. - -3. **Line-delimited JSON output.** One record per line means the - output composes with `jq -c`, `head`, `grep`, `awk`. Operators - don't need to remember the schema; they pipe and `jq '.field'`. - -4. **Error wrapping with the command name.** `" : - underlying error"` makes debugging easy when the cobra layer - surfaces an error to stderr. - -5. **Validate by live smoke after build.** Each cycle ran `make build` - then `./bin/ao ` against real data. Proves - end-to-end semantic correctness, not just compilation. - -## Anti-Patterns (Observed Across The 3 Cycles) - -- **Name collisions in `cli/cmd/ao`.** Cycle 144's first helper was - named `loadCycleHistory` — collided with an existing function in - `metrics_health.go`. `go vet` caught it; renamed to - `loadCycleHistoryViaPort`. **Always `grep` before naming helpers - in `cli/cmd/ao`** (the package is ~150 files). - -- **Shadowing Go builtins.** Cycle 117 used `cap := limit`; same rule - applies to CLI helpers. - -- **Dead imports of `internal/ports` in test files.** Cycle 144's - first test file had a dead import; `go vet` caught it. - -- **Forgetting the registry regen.** `scripts/generate-cli-reference.sh` - is the obvious regen; `scripts/generate-registry.sh` is the - not-obvious one. Both are required when adding a `cobra.Command`. See - the `cli-command-dual-generator` learning for the failure mode. - -## Pre-Flight Checklist - -Before writing the `.go` file: - -```bash -# 1. Pick noun + verb. Verify no collision in cli/cmd/ao: -grep -rn "Cmd = &cobra.Command" cli/cmd/ao/ | grep -i "" - -# 2. Verify helper names don't collide: -grep -rn "func \b" cli/cmd/ao/ - -# 3. Confirm the port + production adapter exist: -ls cli/internal/ports/.go cli/internal/adapters/**.go -``` - -After committing: - -```bash -# Regenerate BOTH: -scripts/generate-cli-reference.sh -scripts/generate-registry.sh - -# Smoke test: -cd cli && make build && ./bin/ao -``` - -## Worked Reference Implementations - -The 3 reference implementations on `main`: - -- `cli/cmd/ao/loop.go` + `loop_test.go` — `ao loop history` -- `cli/cmd/ao/ci.go` + `ci_test.go` — `ao ci latest/recent` -- `cli/cmd/ao/corpus_inject.go` + `corpus_inject_test.go` — `ao corpus inject` - -Read those 3 pairs before writing a new wireup; they're shorter than -this template. - -## See Also - -- `docs/learnings/2026-05-13-cli-wiring-cycle-shape.md` — canonical - source (this file is the skill-side mirror) -- `docs/learnings/2026-05-13-bc-ports-wire-up-arc.md` — the broader - 14-port wire-up arc (cycle 122); historical/architecture, not - a reusable template -- `docs/learnings/2026-05-13-bc-ports-narrowness-postmortem.md` — the - narrowness debate that preceded the wire-up -- `references/go.md` — Go conventions this template assumes diff --git a/cli/embedded/skills/standards/references/test-pyramid.md b/cli/embedded/skills/standards/references/test-pyramid.md index b789a8ce9..1053c24e7 100644 --- a/cli/embedded/skills/standards/references/test-pyramid.md +++ b/cli/embedded/skills/standards/references/test-pyramid.md @@ -1,6 +1,6 @@ # AI-Native Test Shape — L0 through L7 -> Shared reference for RPI lifecycle skills. Loaded by `/discovery`, `/plan`, `/premortem`, `/implement`, `/crank`, `/validate`, and `/postmortem`. +> Optional engineering reference for Plan, Implement, and Validate. It does not add phases or continuation authority. ## The AI-Native Test Shape @@ -76,9 +76,9 @@ The Traditional Pyramid The AI-Native Shape | RPI Phase | Test Levels | What Happens | |-----------|-------------|--------------| -| **Discovery** (`/discovery`, `/plan`) | L0–L3 scoping | Plan identifies which test levels apply. **Default: L2 required for all code-change issues.** Issues include `test_level` metadata. | +| **Plan** (`/plan`) | L0–L3 scoping | Plan identifies which checks prove the active behavior and names the first acceptance check. | | **Premortem** (`/premortem`) | L0–L3 coverage check | Validates plan covers appropriate test levels. **Flags L1-only coverage as WARN.** | -| **Implementation** (`/implement`, `/crank`) | L0–L2 writing + execution | **TDD writes L2 integration tests first (RED).** L0 contracts from specs. L1 as regression guards. | +| **Implementation** (`/implement`) | L0–L2 writing + execution | Behavior-changing work starts with the bounded acceptance check RED, then GREEN and refactor. | | **Validation** (`/validate`, `/postmortem`) | L0–L3 coverage audit | Assesses test coverage. **L2+ coverage weighted 3-5x over L1 in vibe scoring.** | ## Test Level Selection Guide @@ -125,7 +125,7 @@ When creating issues in `/plan`, include test level metadata: ## Bug-Finding Levels (Agent-Autonomous) > **Proven 2026-03-14 on jren-cm:** 3,321 L1 unit tests found 0 new bugs. These levels found 8. -> Evidence: `/Users/fullerbt/gt/jren_cm/crew/ichigo/scripts/.agents/council/2026-03-14-post-mortem-full-session-methodology.md` +> Evidence: `/Users/fullerbt/gt/jren_cm/crew/ichigo/scripts/.agents/council/2026-03-14-postmortem-full-session-methodology.md` L0–L3 are the **coverage shape** — they verify code works as designed. These are the **bug-finding shape** — they find bugs the coverage shape misses. diff --git a/cli/internal/adapters/vendorimage/agentbundle/bundle.go b/cli/internal/adapters/vendorimage/agentbundle/bundle.go index 20a4d4c2b..64cf0909c 100644 --- a/cli/internal/adapters/vendorimage/agentbundle/bundle.go +++ b/cli/internal/adapters/vendorimage/agentbundle/bundle.go @@ -10,10 +10,10 @@ import ( // defaultBundleSkills is the AgentOps-native default skill set stitched into an // out-of-session Agent definition when --skills is not given (ag-eguw0). -var defaultBundleSkills = []string{"session-bootstrap", "standards", "validation", "provenance"} +var defaultBundleSkills = []string{"rpi", "plan", "implement", "validate"} // defaultAgentModel is the model an emitted managed Agent definition targets. -const defaultAgentModel = "claude-opus-4-8" +const defaultAgentModel = "gpt-5.6-codex" // holdoutMarkers are substrings whose presence in a selected skill's body means // bundling it to a (non-ZDR) cloud agent would leak the locked eval substrate. @@ -106,7 +106,7 @@ func buildCodexNTMBundle(skills []string) Bundle { Runtime: "codex-ntm", Instructions: stitchInstructions(skills), Skills: skills, - Bootstrap: "ao session bootstrap && ao inject --bead \"$BEAD\"", + Bootstrap: "Load rpi and run one Plan -> Implement -> fresh Validate invocation; report and stop.", Reference: "skills-codex/agent-native", } } @@ -116,8 +116,8 @@ func buildCodexNTMBundle(skills []string) Bundle { // skill bodies are loaded by the agent at runtime via the ao tool surface). func stitchInstructions(skills []string) string { var sb strings.Builder - sb.WriteString("You are an AgentOps-native agent. Load and follow these skills, ") - sb.WriteString("self-bootstrap with `ao session bootstrap`, and gate your output with `ao validate`:\n") + sb.WriteString("You are an AgentOps-native agent. Load and follow these skills. ") + sb.WriteString("Run each core phase at most once, persist the fresh verdict, report, and stop:\n") for _, s := range skills { sb.WriteString("- ") sb.WriteString(s) diff --git a/cli/internal/adapters/vendorimage/agentbundle/bundle_test.go b/cli/internal/adapters/vendorimage/agentbundle/bundle_test.go index d92d4d205..7f89c81d9 100644 --- a/cli/internal/adapters/vendorimage/agentbundle/bundle_test.go +++ b/cli/internal/adapters/vendorimage/agentbundle/bundle_test.go @@ -15,11 +15,11 @@ func fixtureSkills(t *testing.T) string { t.Helper() dir := t.TempDir() clean := map[string]string{ - "session-bootstrap": "Bootstrap a session.", - "standards": "Coding standards checklist.", - "validation": "Run the validation gate.", - "provenance": "Record provenance.", - "agent-native": "Make out-of-session agents AgentOps-native.", + "rpi": "Run one experiment.", + "plan": "Shape one behavior.", + "implement": "Build one candidate.", + "validate": "Judge one exact candidate.", + "agent-native": "Make out-of-session agents AgentOps-native.", } for name, desc := range clean { writeFixtureSkill(t, dir, name, "---\nname: "+name+"\ndescription: "+desc+"\n---\n# "+name+"\n") @@ -91,19 +91,11 @@ func TestBuildAgentBundle_CodexNTM(t *testing.T) { if b.Runtime != "codex-ntm" { t.Errorf("Runtime = %q, want codex-ntm", b.Runtime) } - // Exact Bootstrap value — guards the codex-ntm struct literal against - // silent edits/drift (the field was the site of an unformatted-source fix). - if want := `ao session bootstrap && ao inject --bead "$BEAD"`; b.Bootstrap != want { + if want := "Load rpi and run one Plan -> Implement -> fresh Validate invocation; report and stop."; b.Bootstrap != want { t.Errorf("codex-ntm Bootstrap = %q, want exactly %q", b.Bootstrap, want) } - if !strings.Contains(b.Bootstrap, "ao session bootstrap") { - t.Errorf("codex-ntm Bootstrap must run `ao session bootstrap`, got %q", b.Bootstrap) - } - if !strings.Contains(b.Bootstrap, `ao inject --bead "$BEAD"`) { - t.Errorf("codex-ntm Bootstrap must request bead-scoped injection, got %q", b.Bootstrap) - } - if strings.Contains(b.Bootstrap, "ao inject --query") { - t.Errorf("codex-ntm Bootstrap must not use invalid `ao inject --query`, got %q", b.Bootstrap) + if strings.Contains(b.Bootstrap, "ao ") { + t.Errorf("codex-ntm Bootstrap must not start a CLI lifecycle, got %q", b.Bootstrap) } if b.Reference != "skills-codex/agent-native" { t.Errorf("Reference = %q, want skills-codex/agent-native", b.Reference) diff --git a/cli/internal/doctor/fix_skills_test.go b/cli/internal/doctor/fix_skills_test.go index 518137704..0fe6a9891 100644 --- a/cli/internal/doctor/fix_skills_test.go +++ b/cli/internal/doctor/fix_skills_test.go @@ -75,8 +75,8 @@ func TestSkillsStaleCommandRefsFixer(t *testing.T) { if !res.Fixed { t.Fatal("Fix not marked Fixed") } - if res.ActionsTaken != 3 { - t.Fatalf("ActionsTaken = %d, want 3", res.ActionsTaken) + if res.ActionsTaken != 2 { + t.Fatalf("ActionsTaken = %d, want 2", res.ActionsTaken) } // Substitution applied; arrow rename-doc line untouched. @@ -87,7 +87,7 @@ func TestSkillsStaleCommandRefsFixer(t *testing.T) { t.Fatalf("SKILL.md after fix = %q, want %q", got, want) } gotDoc, _ := os.ReadFile(docMD) - if string(gotDoc) != "Use `ao converge` to start.\n" { + if string(gotDoc) != "Use `ao work rpi` to start.\n" { t.Fatalf("docs/sample.md after fix = %q", gotDoc) } gotCodexRef, _ := os.ReadFile(codexRefMD) @@ -105,13 +105,13 @@ func TestSkillsStaleCommandRefsFixer(t *testing.T) { t.Fatalf("backup = %q, want %q", bgot, original) } - // actions.jsonl has exactly three lines, correct fixer id + op. + // actions.jsonl has one line for each changed file, correct fixer id + op. recs, err := readActions(ra.ActionsPath()) if err != nil { t.Fatal(err) } - if len(recs) != 3 { - t.Fatalf("actions.jsonl lines = %d, want 3", len(recs)) + if len(recs) != 2 { + t.Fatalf("actions.jsonl lines = %d, want 2", len(recs)) } for _, r := range recs { if r.Op != "WriteFile" || r.FixerID != "fm-skills-stale-command-refs" || !r.OK { @@ -134,8 +134,8 @@ func TestSkillsStaleCommandRefsFixer(t *testing.T) { if err != nil { t.Fatalf("Undo: %v", err) } - if ur.Restored != 3 { - t.Fatalf("Undo restored = %d, want 3", ur.Restored) + if ur.Restored != 2 { + t.Fatalf("Undo restored = %d, want 2", ur.Restored) } restored, _ := os.ReadFile(skillMD) if string(restored) != original { diff --git a/cli/internal/domainslice/manifest.go b/cli/internal/domainslice/manifest.go deleted file mode 100644 index bff30a847..000000000 --- a/cli/internal/domainslice/manifest.go +++ /dev/null @@ -1,196 +0,0 @@ -// Package domainslice implements the domain-slice manifest model and loader. -// -// A domain-slice manifest (docs/domains//manifest.yaml) declares a bounded -// DDD domain slice: its owned directives, promoted spec scenarios, implementation -// surface (context_roots), read fence (allowed/denied globs), ordered validation -// commands, and responsible owner. -// -// This package is explicitly DISTINCT from the phaseManifest in -// cli/cmd/ao/rpi_phased_manifest.go. phaseManifest is a per-phase context-budget -// struct (token limits, handoff field selection); domainSliceManifest is a -// domain-scope declaration. The two compose during scoped operating-loop runs: -// phaseManifest controls context depth; domainSliceManifest controls context breadth. -// -// See docs/adr/ADR-0013-domain-slice-manifest-contract.md for design decisions. -package domainslice - -import ( - "bytes" - "fmt" - "os" - "regexp" - - "gopkg.in/yaml.v3" -) - -// directiveIDPattern is the canonical stable-ID pattern from GOALS.md. -// See schemas/domain-slice-manifest.v1.schema.json and SHARED_TASK_NOTES.md. -var directiveIDPattern = regexp.MustCompile(`^d-[a-z0-9][a-z0-9-]*$`) - -// domainNamePattern mirrors the schema constraint for the domain field. -var domainNamePattern = regexp.MustCompile(`^[a-z][a-z0-9-]*$`) - -// semverPattern mirrors the schema constraint for the version field. -var semverPattern = regexp.MustCompile(`^[0-9]+\.[0-9]+\.[0-9]+$`) - -// ValidationCommand is a single ordered validation step declared in a manifest. -// Mirrors the items schema of validation_commands (additionalProperties:false). -type ValidationCommand struct { - Label string `yaml:"label"` - Command string `yaml:"command"` - WorkingDir string `yaml:"working_dir,omitempty"` - TimeoutSeconds int `yaml:"timeout_seconds,omitempty"` -} - -// domainSliceManifest declares the bounded DDD domain slice that scopes an -// scoped operating-loop run: owned directives, scenarios, context roots, -// read fence, and validation commands. -// -// This is DISTINCT from phaseManifest (rpi_phased_manifest.go), which is a -// per-phase context-budget declaration (token limits, handoff field selection) -// unrelated to DDD domain slicing. -// -// All eleven schema fields are required; unknown fields are rejected. -type domainSliceManifest struct { - SchemaVersion int `yaml:"schema_version"` - Domain string `yaml:"domain"` - Version string `yaml:"version"` - BoundedContext string `yaml:"bounded_context"` - DirectiveIDs []string `yaml:"directive_ids"` - ScenarioIDs []string `yaml:"scenario_ids"` - ContextRoots []string `yaml:"context_roots"` - AllowedReadGlobs []string `yaml:"allowed_read_globs"` - DeniedReadGlobs []string `yaml:"denied_read_globs"` - ValidationCommands []ValidationCommand `yaml:"validation_commands"` - Owner string `yaml:"owner"` -} - -// LoadError is returned for any load or validation failure. It carries the file -// path and (when applicable) the offending field name so callers can produce -// actionable error messages. -type LoadError struct { - Path string - Field string - Err error -} - -func (e *LoadError) Error() string { - if e.Field != "" { - return fmt.Sprintf("domain-slice manifest: %s: field %q: %v", e.Path, e.Field, e.Err) - } - return fmt.Sprintf("domain-slice manifest: %s: %v", e.Path, e.Err) -} - -func (e *LoadError) Unwrap() error { return e.Err } - -func loadErr(path, field string, err error) error { - return &LoadError{Path: path, Field: field, Err: err} -} - -// Load reads the manifest.yaml at path, decodes it with strict unknown-field -// rejection, and validates all required fields and pattern constraints. -// Errors name the offending field so callers can present actionable messages. -func Load(path string) (*domainSliceManifest, error) { - raw, err := os.ReadFile(path) - if err != nil { - return nil, loadErr(path, "", fmt.Errorf("read: %w", err)) - } - return parse(path, raw) -} - -// parse decodes and validates raw YAML content as a domainSliceManifest. -// Separated from Load so tests can exercise parse without touching the filesystem. -func parse(path string, raw []byte) (*domainSliceManifest, error) { - dec := yaml.NewDecoder(bytes.NewReader(raw)) - dec.KnownFields(true) // unknown fields → error (mirrors additionalProperties:false) - - var m domainSliceManifest - if err := dec.Decode(&m); err != nil { - return nil, loadErr(path, "", fmt.Errorf("decode: %w", err)) - } - if err := validate(path, &m); err != nil { - return nil, err - } - return &m, nil -} - -// validate enforces the required-field and pattern constraints that the JSON -// Schema declares but that yaml.v3 alone cannot enforce. -// Each check is named so the LoadError carries the offending field name. -func validate(path string, m *domainSliceManifest) error { - if err := validateScalarFields(path, m); err != nil { - return err - } - if err := validateDirectiveIDs(path, m.DirectiveIDs); err != nil { - return err - } - if err := validateContextRoots(path, m.ContextRoots); err != nil { - return err - } - if err := validateValidationCommands(path, m.ValidationCommands); err != nil { - return err - } - return nil -} - -// validateScalarFields checks the simple required-and-pattern constraints on -// the top-level scalar fields. Extracted to keep validate() under complexity 18. -func validateScalarFields(path string, m *domainSliceManifest) error { - if m.SchemaVersion != 1 { - return loadErr(path, "schema_version", fmt.Errorf("must be 1, got %d", m.SchemaVersion)) - } - if m.Domain == "" { - return loadErr(path, "domain", fmt.Errorf("required field is empty")) - } - if !domainNamePattern.MatchString(m.Domain) { - return loadErr(path, "domain", fmt.Errorf("must match ^[a-z][a-z0-9-]*$, got %q", m.Domain)) - } - if m.Version == "" { - return loadErr(path, "version", fmt.Errorf("required field is empty")) - } - if !semverPattern.MatchString(m.Version) { - return loadErr(path, "version", fmt.Errorf("must be semver (X.Y.Z), got %q", m.Version)) - } - if m.BoundedContext == "" { - return loadErr(path, "bounded_context", fmt.Errorf("required field is empty")) - } - if m.Owner == "" { - return loadErr(path, "owner", fmt.Errorf("required field is empty")) - } - return nil -} - -// validateDirectiveIDs checks every directive_ids entry matches the stable ID pattern. -func validateDirectiveIDs(path string, ids []string) error { - for i, id := range ids { - if !directiveIDPattern.MatchString(id) { - return loadErr(path, fmt.Sprintf("directive_ids[%d]", i), - fmt.Errorf("must match ^d-[a-z0-9][a-z0-9-]*$, got %q", id)) - } - } - return nil -} - -// validateContextRoots ensures context_roots has at least one entry (minItems:1). -func validateContextRoots(path string, roots []string) error { - if len(roots) == 0 { - return loadErr(path, "context_roots", fmt.Errorf("required: at least one context root")) - } - return nil -} - -// validateValidationCommands checks each validation_commands entry has the -// required sub-fields (label and command). -func validateValidationCommands(path string, cmds []ValidationCommand) error { - for i, cmd := range cmds { - if cmd.Label == "" { - return loadErr(path, fmt.Sprintf("validation_commands[%d].label", i), - fmt.Errorf("required field is empty")) - } - if cmd.Command == "" { - return loadErr(path, fmt.Sprintf("validation_commands[%d].command", i), - fmt.Errorf("required field is empty")) - } - } - return nil -} diff --git a/cli/internal/domainslice/manifest_test.go b/cli/internal/domainslice/manifest_test.go deleted file mode 100644 index 2c7541aa2..000000000 --- a/cli/internal/domainslice/manifest_test.go +++ /dev/null @@ -1,526 +0,0 @@ -package domainslice - -import ( - "errors" - "fmt" - "path/filepath" - "runtime" - "strings" - "testing" -) - -// repoRoot returns the absolute repo root so tests can reference fixture files -// (docs/domains/example/manifest.yaml) without embedding duplicates. -func repoRoot(t *testing.T) string { - t.Helper() - _, file, _, ok := runtime.Caller(0) - if !ok { - t.Fatal("runtime.Caller failed") - } - // file = .../cli/internal/domainslice/manifest_test.go - // climb: domainslice/ → internal/ → cli/ → repo root - return filepath.Join(filepath.Dir(file), "..", "..", "..") -} - -// TestLoad_ExampleManifest verifies the canonical example fixture loads without -// error and exposes the expected field values. This is the primary L2 integration -// test: it exercises the real file on disk and every validation path. -func TestLoad_ExampleManifest(t *testing.T) { - path := filepath.Join(repoRoot(t), "docs", "domains", "example", "manifest.yaml") - m, err := Load(path) - if err != nil { - t.Fatalf("Load(%q) unexpected error: %v", path, err) - } - - // --- scalar fields --- - if m.SchemaVersion != 1 { - t.Errorf("SchemaVersion = %d, want 1", m.SchemaVersion) - } - if m.Domain != "example" { - t.Errorf("Domain = %q, want %q", m.Domain, "example") - } - if m.Version != "0.1.0" { - t.Errorf("Version = %q, want %q", m.Version, "0.1.0") - } - if m.BoundedContext == "" { - t.Error("BoundedContext is empty, want non-empty") - } - if m.Owner != "maintainers" { - t.Errorf("Owner = %q, want %q", m.Owner, "maintainers") - } - - // --- directive_ids --- - wantDirectives := []string{"d-goals-measure", "d-goals-scenarios"} - if len(m.DirectiveIDs) != len(wantDirectives) { - t.Errorf("DirectiveIDs length = %d, want %d", len(m.DirectiveIDs), len(wantDirectives)) - } else { - for i, want := range wantDirectives { - if m.DirectiveIDs[i] != want { - t.Errorf("DirectiveIDs[%d] = %q, want %q", i, m.DirectiveIDs[i], want) - } - } - } - - // --- scenario_ids --- - wantScenarios := []string{"s-2026-05-17-001", "s-2026-05-17-002"} - if len(m.ScenarioIDs) != len(wantScenarios) { - t.Errorf("ScenarioIDs length = %d, want %d", len(m.ScenarioIDs), len(wantScenarios)) - } else { - for i, want := range wantScenarios { - if m.ScenarioIDs[i] != want { - t.Errorf("ScenarioIDs[%d] = %q, want %q", i, m.ScenarioIDs[i], want) - } - } - } - - // --- context_roots: must be non-empty and contain the expected paths --- - if len(m.ContextRoots) == 0 { - t.Error("ContextRoots is empty, want at least one entry") - } - wantRoots := []string{ - "cli/cmd/ao/goals.go", - "cli/cmd/ao/goals_scenarios.go", - "cli/internal/goals/", - "spec/scenarios/", - "schemas/scenario.v1.schema.json", - "GOALS.md", - } - if len(m.ContextRoots) != len(wantRoots) { - t.Errorf("ContextRoots length = %d, want %d", len(m.ContextRoots), len(wantRoots)) - } else { - for i, want := range wantRoots { - if m.ContextRoots[i] != want { - t.Errorf("ContextRoots[%d] = %q, want %q", i, m.ContextRoots[i], want) - } - } - } - - // --- validation_commands --- - wantCmds := []struct { - label string - timeout int - }{ - {"build", 60}, - {"unit-tests", 120}, - {"lint", 30}, - } - if len(m.ValidationCommands) != len(wantCmds) { - t.Errorf("ValidationCommands length = %d, want %d", len(m.ValidationCommands), len(wantCmds)) - } else { - for i, want := range wantCmds { - cmd := m.ValidationCommands[i] - if cmd.Label != want.label { - t.Errorf("ValidationCommands[%d].Label = %q, want %q", i, cmd.Label, want.label) - } - if cmd.Command == "" { - t.Errorf("ValidationCommands[%d].Command is empty", i) - } - if cmd.TimeoutSeconds != want.timeout { - t.Errorf("ValidationCommands[%d].TimeoutSeconds = %d, want %d", - i, cmd.TimeoutSeconds, want.timeout) - } - } - } -} - -// parseYAML is a test helper that runs parse() on inline YAML content using a -// synthetic path for error messages. -func parseYAML(t *testing.T, content string) (*domainSliceManifest, error) { - t.Helper() - return parse("test.yaml", []byte(content)) -} - -// validManifestYAML returns a minimal valid manifest YAML string. Tests mutate -// individual fields to exercise specific validation paths. -func validManifestYAML() string { - return `schema_version: 1 -domain: myapp -version: 1.0.0 -bounded_context: Owns X; does not own Y. -directive_ids: - - d-myapp-core -scenario_ids: - - s-2026-01-01-001 -context_roots: - - cli/cmd/ao/myapp.go -allowed_read_globs: - - cli/cmd/ao/myapp*.go -denied_read_globs: - - .agents/holdout/** -validation_commands: - - label: build - command: "cd cli && go build ./cmd/ao/..." -owner: team-myapp -` -} - -// assertMissingFieldError is a test helper that parses yamlContent and asserts -// the result is a *LoadError whose Field contains wantField. Keeping assertions -// in this named helper means every caller has visible assertion call-sites. -func assertMissingFieldError(t *testing.T, yamlContent, wantField string) { - t.Helper() - _, err := parseYAML(t, yamlContent) - if err == nil { - t.Fatalf("parse() succeeded, want error naming field %q", wantField) - } - var le *LoadError - if !errors.As(err, &le) { - t.Fatalf("error is %T, want *LoadError; got: %v", err, err) - } - if !strings.Contains(le.Field, wantField) { - t.Errorf("LoadError.Field = %q, want it to contain %q", le.Field, wantField) - } -} - -// TestParse_MissingRequiredField verifies that omitting each required field -// produces a *LoadError whose Field names the missing field. Each case is a -// direct call to assertMissingFieldError so assertions are visible at this scope. -func TestParse_MissingRequiredField(t *testing.T) { - t.Parallel() - - // Baseline: the complete valid YAML must parse without error before we start - // removing fields. This assertion is in the outer function body so the hook - // detects it; it also guards against a broken validManifestYAML helper. - if _, err := parseYAML(t, validManifestYAML()); err != nil { - t.Fatalf("baseline validManifestYAML() failed to parse: %v", err) - } - - t.Run("schema_version", func(t *testing.T) { - t.Parallel() - assertMissingFieldError(t, `domain: myapp -version: 1.0.0 -bounded_context: Owns X; does not own Y. -directive_ids: [d-myapp-core] -scenario_ids: [s-2026-01-01-001] -context_roots: [cli/cmd/ao/myapp.go] -allowed_read_globs: [cli/cmd/ao/myapp*.go] -denied_read_globs: [.agents/holdout/**] -validation_commands: - - label: build - command: "go build" -owner: team-myapp -`, "schema_version") - }) - - t.Run("domain", func(t *testing.T) { - t.Parallel() - assertMissingFieldError(t, `schema_version: 1 -version: 1.0.0 -bounded_context: Owns X; does not own Y. -directive_ids: [d-myapp-core] -scenario_ids: [s-2026-01-01-001] -context_roots: [cli/cmd/ao/myapp.go] -allowed_read_globs: [cli/cmd/ao/myapp*.go] -denied_read_globs: [.agents/holdout/**] -validation_commands: - - label: build - command: "go build" -owner: team-myapp -`, "domain") - }) - - t.Run("version", func(t *testing.T) { - t.Parallel() - assertMissingFieldError(t, `schema_version: 1 -domain: myapp -bounded_context: Owns X; does not own Y. -directive_ids: [d-myapp-core] -scenario_ids: [s-2026-01-01-001] -context_roots: [cli/cmd/ao/myapp.go] -allowed_read_globs: [cli/cmd/ao/myapp*.go] -denied_read_globs: [.agents/holdout/**] -validation_commands: - - label: build - command: "go build" -owner: team-myapp -`, "version") - }) - - t.Run("bounded_context", func(t *testing.T) { - t.Parallel() - assertMissingFieldError(t, `schema_version: 1 -domain: myapp -version: 1.0.0 -directive_ids: [d-myapp-core] -scenario_ids: [s-2026-01-01-001] -context_roots: [cli/cmd/ao/myapp.go] -allowed_read_globs: [cli/cmd/ao/myapp*.go] -denied_read_globs: [.agents/holdout/**] -validation_commands: - - label: build - command: "go build" -owner: team-myapp -`, "bounded_context") - }) - - t.Run("context_roots", func(t *testing.T) { - t.Parallel() - assertMissingFieldError(t, `schema_version: 1 -domain: myapp -version: 1.0.0 -bounded_context: Owns X; does not own Y. -directive_ids: [d-myapp-core] -scenario_ids: [s-2026-01-01-001] -allowed_read_globs: [cli/cmd/ao/myapp*.go] -denied_read_globs: [.agents/holdout/**] -validation_commands: - - label: build - command: "go build" -owner: team-myapp -`, "context_roots") - }) - - t.Run("owner", func(t *testing.T) { - t.Parallel() - assertMissingFieldError(t, `schema_version: 1 -domain: myapp -version: 1.0.0 -bounded_context: Owns X; does not own Y. -directive_ids: [d-myapp-core] -scenario_ids: [s-2026-01-01-001] -context_roots: [cli/cmd/ao/myapp.go] -allowed_read_globs: [cli/cmd/ao/myapp*.go] -denied_read_globs: [.agents/holdout/**] -validation_commands: - - label: build - command: "go build" -`, "owner") - }) -} - -// TestParse_BadDirectiveIDPattern verifies that a directive_id not matching -// ^d-[a-z0-9][a-z0-9-]*$ produces an error naming the offending field. -func TestParse_BadDirectiveIDPattern(t *testing.T) { - t.Parallel() - cases := []struct { - name string - id string - }{ - {"missing d- prefix", "goals-measure"}, - {"uppercase letters", "d-Goals-Measure"}, - {"starts with hyphen after d-", "d--bad"}, - {"empty after d-", "d-"}, - {"has spaces", "d-foo bar"}, - } - - for _, tc := range cases { - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - y := strings.ReplaceAll(validManifestYAML(), "d-myapp-core", tc.id) - _, err := parseYAML(t, y) - if err == nil { - t.Fatalf("parse() succeeded with bad directive_id %q, want error", tc.id) - } - var le *LoadError - if !errors.As(err, &le) { - t.Fatalf("error is %T, want *LoadError; got: %v", err, err) - } - if !strings.Contains(le.Field, "directive_ids") { - t.Errorf("LoadError.Field = %q, want it to contain %q", le.Field, "directive_ids") - } - if !strings.Contains(err.Error(), tc.id) { - t.Errorf("error message %q does not contain the offending id %q", err.Error(), tc.id) - } - }) - } -} - -// TestParse_UnknownField verifies that an unknown top-level field is rejected, -// mirroring the schema's additionalProperties:false. -func TestParse_UnknownField(t *testing.T) { - t.Parallel() - y := validManifestYAML() + "unexpected_field: should-fail\n" - _, err := parseYAML(t, y) - if err == nil { - t.Fatal("parse() succeeded with unknown field, want error") - } - // The error should come from KnownFields(true) and reference the unknown key. - if !strings.Contains(err.Error(), "unexpected_field") { - t.Errorf("error %q does not mention the unknown field name", err.Error()) - } -} - -// TestParse_UnknownValidationCommandField verifies that an unknown field inside -// a validation_commands item is also rejected. -func TestParse_UnknownValidationCommandField(t *testing.T) { - t.Parallel() - y := `schema_version: 1 -domain: myapp -version: 1.0.0 -bounded_context: Owns X; does not own Y. -directive_ids: [d-myapp-core] -scenario_ids: [s-2026-01-01-001] -context_roots: [cli/cmd/ao/myapp.go] -allowed_read_globs: [cli/cmd/ao/myapp*.go] -denied_read_globs: [.agents/holdout/**] -validation_commands: - - label: build - command: "go build" - not_a_real_field: oops -owner: team-myapp -` - _, err := parseYAML(t, y) - if err == nil { - t.Fatal("parse() succeeded with unknown validation_commands field, want error") - } - if !strings.Contains(err.Error(), "not_a_real_field") { - t.Errorf("error %q does not mention the unknown field name", err.Error()) - } -} - -// TestParse_BadSchemaVersion verifies schema_version != 1 is rejected. -func TestParse_BadSchemaVersion(t *testing.T) { - t.Parallel() - y := strings.ReplaceAll(validManifestYAML(), "schema_version: 1", "schema_version: 2") - _, err := parseYAML(t, y) - if err == nil { - t.Fatal("parse() succeeded with schema_version 2, want error") - } - var le *LoadError - if !errors.As(err, &le) { - t.Fatalf("error is %T, want *LoadError; got: %v", err, err) - } - if le.Field != "schema_version" { - t.Errorf("LoadError.Field = %q, want %q", le.Field, "schema_version") - } -} - -// TestParse_BadDomainPattern verifies a domain name that does not match -// ^[a-z][a-z0-9-]*$ is rejected with a field-specific error. -func TestParse_BadDomainPattern(t *testing.T) { - t.Parallel() - y := strings.ReplaceAll(validManifestYAML(), "domain: myapp", "domain: MyApp") - _, err := parseYAML(t, y) - if err == nil { - t.Fatal("parse() succeeded with uppercase domain, want error") - } - var le *LoadError - if !errors.As(err, &le) { - t.Fatalf("error is %T, want *LoadError; got: %v", err, err) - } - if le.Field != "domain" { - t.Errorf("LoadError.Field = %q, want %q", le.Field, "domain") - } -} - -// TestParse_BadVersionPattern verifies a non-semver version string is rejected. -func TestParse_BadVersionPattern(t *testing.T) { - t.Parallel() - y := strings.ReplaceAll(validManifestYAML(), "version: 1.0.0", "version: v1.0.0") - _, err := parseYAML(t, y) - if err == nil { - t.Fatal("parse() succeeded with non-semver version, want error") - } - var le *LoadError - if !errors.As(err, &le) { - t.Fatalf("error is %T, want *LoadError; got: %v", err, err) - } - if le.Field != "version" { - t.Errorf("LoadError.Field = %q, want %q", le.Field, "version") - } -} - -// TestParse_ValidDirectiveIDs verifies that well-formed directive IDs are accepted. -func TestParse_ValidDirectiveIDs(t *testing.T) { - t.Parallel() - cases := []string{ - "d-a", - "d-goals-measure", - "d-rpi-phase-2", - "d-0abc", - "d-abc123", - } - for _, id := range cases { - - t.Run(id, func(t *testing.T) { - t.Parallel() - y := strings.ReplaceAll(validManifestYAML(), "d-myapp-core", id) - m, err := parseYAML(t, y) - if err != nil { - t.Errorf("parse() unexpected error for valid directive_id %q: %v", id, err) - return - } - if len(m.DirectiveIDs) != 1 || m.DirectiveIDs[0] != id { - t.Errorf("DirectiveIDs = %v, want [%q]", m.DirectiveIDs, id) - } - }) - } -} - -// TestParse_MissingValidationCommandLabel verifies that a validation_commands -// item without a label is rejected with a field-specific error. -func TestParse_MissingValidationCommandLabel(t *testing.T) { - t.Parallel() - y := `schema_version: 1 -domain: myapp -version: 1.0.0 -bounded_context: Owns X; does not own Y. -directive_ids: [d-myapp-core] -scenario_ids: [s-2026-01-01-001] -context_roots: [cli/cmd/ao/myapp.go] -allowed_read_globs: [cli/cmd/ao/myapp*.go] -denied_read_globs: [.agents/holdout/**] -validation_commands: - - command: "go build" -owner: team-myapp -` - _, err := parseYAML(t, y) - if err == nil { - t.Fatal("parse() succeeded with missing validation_commands[0].label, want error") - } - var le *LoadError - if !errors.As(err, &le) { - t.Fatalf("error is %T, want *LoadError; got: %v", err, err) - } - if !strings.Contains(le.Field, "validation_commands") || !strings.Contains(le.Field, "label") { - t.Errorf("LoadError.Field = %q, want it to contain validation_commands and label", le.Field) - } -} - -// TestParse_EmptyContextRoots verifies that an empty context_roots array is rejected. -func TestParse_EmptyContextRoots(t *testing.T) { - t.Parallel() - y := `schema_version: 1 -domain: myapp -version: 1.0.0 -bounded_context: Owns X; does not own Y. -directive_ids: [d-myapp-core] -scenario_ids: [s-2026-01-01-001] -context_roots: [] -allowed_read_globs: [cli/cmd/ao/myapp*.go] -denied_read_globs: [.agents/holdout/**] -validation_commands: - - label: build - command: "go build" -owner: team-myapp -` - _, err := parseYAML(t, y) - if err == nil { - t.Fatal("parse() succeeded with empty context_roots, want error") - } - var le *LoadError - if !errors.As(err, &le) { - t.Fatalf("error is %T, want *LoadError; got: %v", err, err) - } - if le.Field != "context_roots" { - t.Errorf("LoadError.Field = %q, want %q", le.Field, "context_roots") - } -} - -// TestLoadError_Format verifies the error message format for both the field and -// no-field variants. This is a behavioural assertion, not a coverage stub. -func TestLoadError_Format(t *testing.T) { - t.Parallel() - withField := &LoadError{Path: "foo/manifest.yaml", Field: "domain", Err: fmt.Errorf("required field is empty")} - wantWith := `domain-slice manifest: foo/manifest.yaml: field "domain": required field is empty` - if withField.Error() != wantWith { - t.Errorf("LoadError.Error() with field = %q, want %q", withField.Error(), wantWith) - } - - noField := &LoadError{Path: "foo/manifest.yaml", Err: fmt.Errorf("decode: eof")} - wantNo := `domain-slice manifest: foo/manifest.yaml: decode: eof` - if noField.Error() != wantNo { - t.Errorf("LoadError.Error() no field = %q, want %q", noField.Error(), wantNo) - } -} diff --git a/cli/internal/eval/baseline_ab.go b/cli/internal/eval/baseline_ab.go index bc66f6d70..cb6912128 100644 --- a/cli/internal/eval/baseline_ab.go +++ b/cli/internal/eval/baseline_ab.go @@ -17,7 +17,7 @@ const ( ) // AllBaselineModes returns the legal --baseline-mode values, used by -// cobra ValidArgs so invalid values are rejected (per pre-mortem Check 5). +// cobra ValidArgs so invalid values are rejected (per premortem Check 5). func AllBaselineModes() []string { return []string{ string(BaselineModeSkillOn), diff --git a/cli/internal/eval/core_canary_refs_test.go b/cli/internal/eval/core_canary_refs_test.go index dc5b9a0bc..1fe588ef0 100644 --- a/cli/internal/eval/core_canary_refs_test.go +++ b/cli/internal/eval/core_canary_refs_test.go @@ -59,8 +59,8 @@ func TestCoreCanaryMortemReferencesAreCanonical(t *testing.T) { for _, path := range []string{ "../../../skills/premortem/SKILL.md", "../../../skills/premortem/scripts/validate.sh", - "../../../skills/premortem/references/mandatory-checks.md", - "../../../skills/premortem/references/write-premortem-output.md", + "../../../skills/premortem/references/premortem.feature", + "../../../skills/premortem/schemas/premortem-plan-review.v1.schema.json", "../../../skills-codex/premortem/SKILL.md", "../../../skills-codex/premortem/prompt.md", "../../../skills-codex/premortem/scripts/validate.sh", diff --git a/cli/internal/evalsubstrate/verdict_test.go b/cli/internal/evalsubstrate/verdict_test.go index fdb4bc97c..c069785b1 100644 --- a/cli/internal/evalsubstrate/verdict_test.go +++ b/cli/internal/evalsubstrate/verdict_test.go @@ -63,7 +63,7 @@ func TestManifest_VerdictRoundTrip(t *testing.T) { } } -// TestManifest_VerdictLegacyStringCompat — pre-mortem C1 fix. +// TestManifest_VerdictLegacyStringCompat — premortem C1 fix. // rc2 manifests stored verdict as a bare string ("improved"). New *Verdict struct // MUST accept that legacy form via custom UnmarshalJSON; otherwise existing // runs would fail to load. diff --git a/cli/internal/evidence/citations.go b/cli/internal/evidence/citations.go index bfd381acd..5989067e9 100644 --- a/cli/internal/evidence/citations.go +++ b/cli/internal/evidence/citations.go @@ -59,7 +59,7 @@ func RecordCitation(baseDir string, event types.CitationEvent) error { if err != nil { return fmt.Errorf("open citation ledger: %w", err) } - defer file.Close() + defer func() { _ = file.Close() }() data, err := json.Marshal(event) if err != nil { return fmt.Errorf("marshal citation: %w", err) @@ -78,7 +78,7 @@ func LoadCitations(baseDir string) ([]types.CitationEvent, error) { } return nil, fmt.Errorf("open citation ledger: %w", err) } - defer file.Close() + defer func() { _ = file.Close() }() var citations []types.CitationEvent scanner := bufio.NewScanner(file) scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024) diff --git a/cli/internal/gates/checks/parity_test.go b/cli/internal/gates/checks/parity_test.go index 54d575440..0f07f0a04 100644 --- a/cli/internal/gates/checks/parity_test.go +++ b/cli/internal/gates/checks/parity_test.go @@ -60,14 +60,15 @@ func TestPredicateParity_PerChangeClass(t *testing.T) { ids := selectedIDs(t, []string{"cli/cmd/ao/main.go"}) assertHas(t, ids, "go.build", "go.command-test-pair") assertHas(t, ids, alwaysIDs...) - assertNot(t, ids, "skill.schema", "contract.registry-drift", "eval.corpus-freshness") + assertNot(t, ids, "skill.schema", "contract.skill-mesh") }) t.Run("skill change", func(t *testing.T) { ids := selectedIDs(t, []string{"skills/foo/SKILL.md"}) - assertHas(t, ids, "skill.schema", "skill.isolation", "skill.no-operator-leakage", "derived.changed-scope") + assertHas(t, ids, "skill.schema", "skill.no-operator-leakage", "derived.changed-scope") assertHas(t, ids, alwaysIDs...) - assertNot(t, ids, "go.build", "go.command-test-pair", "contract.registry-drift") + assertHas(t, ids, "contract.skill-mesh") + assertNot(t, ids, "go.build", "go.command-test-pair") }) t.Run("skill conformance selector self-change", func(t *testing.T) { @@ -85,7 +86,7 @@ func TestPredicateParity_PerChangeClass(t *testing.T) { t.Run("contract change", func(t *testing.T) { ids := selectedIDs(t, []string{"schemas/eval-outcomes.json"}) - assertHas(t, ids, "contract.registry-drift", "contract.bounded-contexts-drift", "contract.finding-registry") + assertHas(t, ids, "contract.cathedral-cut", "contract.finding-registry") assertHas(t, ids, alwaysIDs...) assertNot(t, ids, "go.build", "skill.schema") }) @@ -93,13 +94,13 @@ func TestPredicateParity_PerChangeClass(t *testing.T) { t.Run("docs change", func(t *testing.T) { ids := selectedIDs(t, []string{"docs/how-it-works.md"}) assertHas(t, ids, alwaysIDs...) - assertNot(t, ids, "go.build", "skill.schema", "contract.registry-drift", "claim.registry-drift") + assertNot(t, ids, "go.build", "skill.schema", "contract.skill-mesh", "claim.registry-drift") }) t.Run("empty diff runs only always-checks", func(t *testing.T) { ids := selectedIDs(t, nil) assertHas(t, ids, alwaysIDs...) - assertNot(t, ids, "go.build", "skill.schema", "contract.registry-drift", "eval.corpus-freshness") + assertNot(t, ids, "go.build", "skill.schema", "contract.skill-mesh") }) } @@ -107,6 +108,6 @@ func TestPredicateParity_PerChangeClass(t *testing.T) { // force every fast check regardless of routing. func TestPredicateParity_InvalidationRunsAll(t *testing.T) { ids := selectedIDs(t, []string{"go.mod"}) - assertHas(t, ids, "go.build", "skill.schema", "contract.registry-drift", "eval.corpus-freshness") + assertHas(t, ids, "go.build", "skill.schema", "contract.skill-mesh") assertHas(t, ids, alwaysIDs...) } diff --git a/cli/internal/gates/checks/ratchet_routing_test.go b/cli/internal/gates/checks/ratchet_routing_test.go index e7faf22e8..cd7b3485a 100644 --- a/cli/internal/gates/checks/ratchet_routing_test.go +++ b/cli/internal/gates/checks/ratchet_routing_test.go @@ -11,7 +11,7 @@ import ( ) // TestRatchetLibConsumersRouteLibEdits is the ratchet-lib routing closure -// (age-ratchet-lib-extraction-bv7d.1, pre-mortem FM3): every scripts/check-*.sh +// (age-ratchet-lib-extraction-bv7d.1, premortem FM3): every scripts/check-*.sh // that sources scripts/lib/ratchet.sh must (a) be a registered Backing in the // Go gate registry and (b) carry "scripts/lib/ratchet.sh" in its Match globs — // otherwise an edit to the shared lib would not re-run the gates that depend diff --git a/cli/internal/gates/checks/seed.go b/cli/internal/gates/checks/seed.go index 9a5fa3ad6..d4353851a 100644 --- a/cli/internal/gates/checks/seed.go +++ b/cli/internal/gates/checks/seed.go @@ -25,7 +25,7 @@ var ( skillPaths = []string{"skills/**", "skills-codex/**", "tests/skills/**"} // skill.scenario-test-linkage routes on the scenario corpus PLUS its own // surfaces — the script, allowlist, bats twin, and shared ratchet lib were - // previously un-routed (self-routing repair, pre-mortem FM3, + // previously un-routed (self-routing repair, premortem FM3, // age-ratchet-lib-extraction-bv7d.7). scenarioLinkagePaths = []string{ "skills/**", "skills-codex/**", "tests/skills/**", @@ -57,14 +57,13 @@ var ( "scripts/check-honest-voice.sh", "tests/scripts/check-honest-voice.bats", } - ciPolicyPaths = []string{".github/workflows/validate.yml", "docs/CI-CD.md", "AGENTS.md"} - evalPaths = []string{"evals/**", "schemas/eval-*", "cli/internal/eval/**"} - contextMapPaths = []string{"skills/**", "docs/contracts/context-map.md"} - swarmPaths = []string{".agents/swarm/**", "schemas/swarm-*"} - agentsDocPaths = []string{"AGENTS.md", "docs/agent-workflow-reference.md", "docs/CI-CD.md", "docs/contracts/codex-skill-api.md", ".github/workflows/validate.yml"} - corpusPaths = []string{".agents/**", "docs/canon/**", "canon/**"} - cliContractPaths = []string{"cli/**", "docs/cli-surface.*", "scripts/check-cli-contract.sh", "scripts/check-docs-cli-snippets.sh", "scripts/generate-cli-reference.sh", "tests/cli_contract_gate.bats", "tests/cli_quality_zero_debt.bats"} - registryPaths = []string{"skills/**", "hooks/**", "evals/**", "cli/cmd/ao/**", "cli/internal/**", "registry.json"} + ciPolicyPaths = []string{".github/workflows/validate.yml", "docs/CI-CD.md", "AGENTS.md"} + contextMapPaths = []string{"skills/**", "docs/contracts/context-map.md"} + swarmPaths = []string{".agents/swarm/**", "schemas/swarm-*"} + agentsDocPaths = []string{"AGENTS.md", "docs/agent-workflow-reference.md", "docs/CI-CD.md", "docs/contracts/codex-skill-api.md", ".github/workflows/validate.yml"} + corpusPaths = []string{".agents/**", "docs/canon/**", "canon/**"} + cliContractPaths = []string{"cli/**", "docs/cli-surface.*", "scripts/check-cli-contract.sh", "scripts/check-docs-cli-snippets.sh", "scripts/generate-cli-reference.sh", "tests/cli_contract_gate.bats", "tests/cli_quality_zero_debt.bats"} + registryPaths = []string{"skills/**", "hooks/**", "evals/**", "cli/cmd/ao/**", "cli/internal/**", "registry.json"} // Widened to docs/** (--all-docs mode): the checker no longer scans a fixed // 6-file set — it scans every LIVE docs/** file (plus the pinned doctrine // files) and ratchets against scripts/.docs-skill-refs-baseline, so any live @@ -246,16 +245,11 @@ func init() { {ID: "go.lint", Tiers: gates.Full, Match: goLintPaths, Blocking: true, Backing: "check-go-lint.sh", RepairHint: "cd cli && make lint; fix or split — budgets in .claude/rules/go.md; promote to Fast tier only after measured <60s on changed scope (age-gate-the-ungated-egwt.7)"}, {ID: "skill.schema", Tiers: gates.Fast | gates.Full, Match: skillPaths, Blocking: true, Backing: "validate-skill-schema.sh"}, {ID: "skill.triggers", Tiers: gates.Fast | gates.Full, Match: skillPaths, Blocking: true, Backing: "validate-skill-triggers.sh"}, - {ID: "contract.registry-drift", Tiers: gates.Fast | gates.Full, Match: contractPaths, Blocking: true, Backing: "check-registry-drift.sh", RepairHint: "bash scripts/generate-registry.sh"}, - {ID: "contract.bounded-contexts-drift", Tiers: gates.Fast | gates.Full, Match: contractPaths, Blocking: true, Backing: "check-bounded-contexts-drift.sh"}, {ID: "contract.cathedral-cut", Tiers: gates.Fast | gates.Full, Match: cathedralCutPaths, Blocking: true, Backing: "check-cathedral-cut-conformance.py"}, {ID: "contract.skill-mesh", Tiers: gates.Fast | gates.Full, Match: skillPaths, Blocking: true, Backing: "check-skill-mesh.py"}, {ID: "contract.finding-registry", Tiers: gates.Fast | gates.Full, Match: contractPaths, Blocking: true, Backing: "check-finding-registry.sh"}, {ID: "ci.policy-parity", Tiers: gates.Fast | gates.Full, Match: ciPolicyPaths, Blocking: true, Backing: "validate-ci-policy-parity.sh"}, - {ID: "eval.corpus-freshness", Tiers: gates.Fast | gates.Full, Match: evalPaths, Blocking: true, Backing: "check-corpus-freshness.sh"}, - // skill class (PB1 parity batch — all shell-backed via ScriptRunner) - {ID: "skill.cli-skills-map", Tiers: gates.Fast | gates.Full, Match: skillPaths, Blocking: true, Backing: "validate-cli-skills-map.sh"}, {ID: "skill.runtime-formats", Tiers: gates.Fast | gates.Full, Match: skillPaths, Blocking: true, Backing: "validate-skill-runtime-formats.sh"}, {ID: "skill.runtime-parity", Tiers: gates.Fast | gates.Full, Match: skillPaths, Blocking: true, Backing: "validate-skill-runtime-parity.sh"}, {ID: "skill.cli-snippets", Tiers: gates.Fast | gates.Full, Match: skillPaths, Blocking: true, Backing: "validate-skill-cli-snippets.sh"}, @@ -263,7 +257,6 @@ func init() { {ID: "skill.codex-parity-drift", Tiers: gates.Fast | gates.Full, Match: skillPaths, Blocking: true, Backing: "check-codex-parity-drift.sh"}, {ID: "skill.codex-runtime-sections", Tiers: gates.Fast | gates.Full, Match: skillPaths, Blocking: true, Backing: "validate-codex-runtime-sections.sh"}, {ID: "skill.codex-override-coverage", Tiers: gates.Fast | gates.Full, Match: skillPaths, Blocking: true, Backing: "validate-codex-override-coverage.sh"}, - {ID: "skill.codex-backbone-prompts", Tiers: gates.Fast | gates.Full, Match: skillPaths, Blocking: true, Backing: "validate-codex-backbone-prompts.sh"}, // age-2s5k: always-run (no Match) — these validators assert whole-twin // contract invariants over hardcoded file lists, so latent drift in a twin // must fail the NEXT push regardless of scope, not lie invisible on green @@ -271,7 +264,6 @@ func init() { // ambushes it (the age-huim / age-3pdt failure mode). Cheap (string greps), // so the per-push cost is negligible against the anti-ambush guarantee. {ID: "skill.codex-generated-artifacts", Tiers: gates.Fast | gates.Full, Match: skillPaths, Blocking: true, Backing: "validate-codex-generated-artifacts.sh"}, - {ID: "skill.isolation", Tiers: gates.Fast | gates.Full, Match: skillPaths, Blocking: false, Backing: "check-skill-isolation.sh"}, // skill.probe-coverage (ADVISORY): a product-/judgment-tier skill whose // tier badge carries no BEHAVIORAL-probe result is unmeasured — the badge // is editorial, not proven. This NAMES the unmeasured ones. Advisory-first @@ -283,8 +275,6 @@ func init() { {ID: "skill.heal-strict", Tiers: gates.Full, Match: skillPaths, Blocking: true, Backing: "skills/heal-skill/scripts/heal.sh", Args: []string{"--check", "--strict"}}, {ID: "skill.frontmatter-v2", Tiers: gates.Full, Match: skillPaths, Blocking: true, Backing: "validate-skill-frontmatter.sh"}, {ID: "skill.body-refs", Tiers: gates.Full, Match: skillPaths, Blocking: true, Backing: "validate-skill-body-refs.sh"}, - {ID: "skill.flow", Tiers: gates.Full, Match: skillPaths, Blocking: true, Backing: "validate-skill-flow.sh"}, - {ID: "skill.domain-map-golden", Tiers: gates.Full, Match: skillPaths, Blocking: true, Backing: "generate-skill-domain-map.sh", Args: []string{"--check"}}, {ID: "skill.scenario-test-linkage", Tiers: gates.Full, Match: scenarioLinkagePaths, Blocking: true, Backing: "check-scenario-test-linkage.sh"}, // go class @@ -299,8 +289,6 @@ func init() { // contract / context-map / swarm classes {ID: "contract.compatibility", Tiers: gates.Fast | gates.Full, Match: contractPaths, Blocking: true, Backing: "check-contract-compatibility.sh"}, - {ID: "contract.context-map-drift", Tiers: gates.Fast | gates.Full, Match: contextMapPaths, Blocking: true, Backing: "validate-context-map-drift.sh", RepairHint: "bash scripts/generate-context-map.sh"}, - {ID: "contract.registry-json", Tiers: gates.Full, Match: registryPaths, Blocking: true, Backing: "generate-registry.sh", Args: []string{"--check"}}, {ID: "docs.agents-split", Tiers: gates.Full, Match: agentsDocPaths, Blocking: true, Backing: "validate-agents-split.sh"}, // always-run structural invariants (no Match) @@ -333,9 +321,6 @@ func init() { {ID: "corpus.secret-scan", Tiers: gates.Full, Match: corpusPaths, Blocking: true, Backing: "check-corpus-secret-scan.sh"}, {ID: "corpus.witness-dolt-jsonl-crosscheck", Tiers: gates.Full, Match: corpusPaths, Blocking: true, Backing: "witness-dolt-jsonl-crosscheck.sh"}, - // full-mode-only / advisory (mirror the bash gate: these skip in fast or warn) - {ID: "skill.catalog-drift", Tiers: gates.Full, Blocking: false, Backing: "check-skill-catalog-drift.sh"}, - // final backing-script batch (PB1) {ID: "always.quarantine-empty", Tiers: gates.Fast | gates.Full, Blocking: true, Backing: "check-quarantine-empty.sh"}, {ID: "always.test-fixture-parity", Tiers: gates.Fast | gates.Full, Blocking: true, Backing: "check-test-fixture-parity.sh"}, diff --git a/cli/internal/gates/checks/seed_test.go b/cli/internal/gates/checks/seed_test.go index e52c4a041..4d1e6e244 100644 --- a/cli/internal/gates/checks/seed_test.go +++ b/cli/internal/gates/checks/seed_test.go @@ -13,9 +13,8 @@ func TestSeedChecksRegistered(t *testing.T) { "always.mutation-route", "always.embedded-sync", "skill.schema", - "contract.registry-drift", + "contract.skill-mesh", "ci.policy-parity", - "eval.corpus-freshness", "corpus.path-guard", } for _, id := range want { @@ -136,25 +135,6 @@ func TestRetrievalManifestPathGateHasFixtureManifestArgs(t *testing.T) { } } -func TestSkillIsolationGateIsWarnFirst(t *testing.T) { - check, ok := gates.Default.Get("skill.isolation") - if !ok { - t.Fatal("skill.isolation gate is not registered") - } - if check.Backing != "check-skill-isolation.sh" { - t.Fatalf("skill.isolation backing = %q, want check-skill-isolation.sh", check.Backing) - } - if check.Blocking { - t.Fatal("skill.isolation must be warn-first / non-blocking") - } - if !check.Tiers.Has(gates.Fast) || !check.Tiers.Has(gates.Full) { - t.Fatalf("skill.isolation tiers = %v, want Fast|Full", check.Tiers) - } - if len(check.Match) == 0 { - t.Fatal("skill.isolation should be routed by skill paths, not always-run") - } -} - func TestSkillProbeCoverageGateIsWarnFirstAdvisory(t *testing.T) { check, ok := gates.Default.Get("skill.probe-coverage") if !ok { diff --git a/cli/internal/gates/scriptrunner.go b/cli/internal/gates/scriptrunner.go index 3fe4c0109..7e7e709a4 100644 --- a/cli/internal/gates/scriptrunner.go +++ b/cli/internal/gates/scriptrunner.go @@ -14,7 +14,7 @@ import ( // ScriptRunner runs a shell-backed check and maps its exit code to a // GateVerdict. Basename backings resolve under scripts/ (for example -// "check-registry-drift.sh"). Path backings resolve from the repo root (for +// "check-cathedral-cut-conformance.py"). Path backings resolve from the repo root (for // example "skills/heal-skill/scripts/heal.sh"). // // It satisfies ports.GateRunnerPort, so the deterministic runner can shell to ANY diff --git a/cli/internal/gates/workflow_coverage.go b/cli/internal/gates/workflow_coverage.go index 3f93d0631..48e68c155 100644 --- a/cli/internal/gates/workflow_coverage.go +++ b/cli/internal/gates/workflow_coverage.go @@ -72,16 +72,13 @@ var deferredWorkflowScripts = map[string]string{ "scripts/check-eval-workbench.sh": "requires validate.yml's prior workbench Python venv setup", "scripts/check-json-marshal-checked.sh": "requires the workflow's just-installed errcheck toolchain; runs as its own blocking correctness step outside the Go gate", "scripts/check-outcomes-holdout-leak.sh": "validate.yml derives a dynamic payload list; needs native input modeling before delegation", - "scripts/check-workflow-governance.sh": "Claude-only .claude/workflows/*.js <-> workflows-ledger bijection; needs the ledger context, not a Go-gate check", "scripts/emit-deterministic-catch.sh": "CI-only telemetry emitter run with `|| true` on gate FAIL; never blocking, intentionally outside the Go gate", "scripts/eval-agent-harness.sh": "validate.yml asserts dry-run JSON shape after invoking the script", - "scripts/lint-evidence-lines.sh": "PR-body advisory that requires GitHub PR context and GH_TOKEN", "scripts/release-smoke-test.sh": "workflow uses --skip-build after a prior build; standalone form should become a composed/native check", "scripts/security-gate.sh": "requires validate.yml's security toolchain setup and scanner policy context", "scripts/skill-eval.sh": "changed-skill scoped gate with pinned ms toolchain/cache setup", "scripts/test-agentops-contract-canaries.sh": "requires validate.yml's canary toolchain setup and artifact context", "scripts/validate-provenance-ledger.sh": "provenance ledger --gate verifier needs AO_BIN + the workflow's prior-step ledger state; runs in its own job", - "scripts/verify-gate-claim.sh": "AP7 verifier needs completed sibling workflow logs from the summary job", } // RegistryWorkflowCoverage returns workflow-vs-registry script coverage. diff --git a/cli/internal/gates/workflow_coverage_test.go b/cli/internal/gates/workflow_coverage_test.go index b46ecb7a4..8f15439a3 100644 --- a/cli/internal/gates/workflow_coverage_test.go +++ b/cli/internal/gates/workflow_coverage_test.go @@ -26,8 +26,6 @@ jobs: chmod +x scripts/check-advisory.sh echo "run scripts/check-echo-only.sh if this fails" ./scripts/check-advisory.sh - - name: deferred - run: scripts/lint-evidence-lines.sh 12345 `) if err := os.WriteFile(filepath.Join(root, ".github", "workflows", "validate.yml"), workflow, 0o644); err != nil { t.Fatal(err) @@ -45,14 +43,14 @@ jobs: if err != nil { t.Fatalf("RegistryWorkflowCoverage: %v", err) } - if got.WorkflowScriptCount != 4 { - t.Fatalf("WorkflowScriptCount = %d, want 4", got.WorkflowScriptCount) + if got.WorkflowScriptCount != 3 { + t.Fatalf("WorkflowScriptCount = %d, want 3", got.WorkflowScriptCount) } if got.RegistryScriptCount != 2 { t.Fatalf("RegistryScriptCount = %d, want 2", got.RegistryScriptCount) } - if got.MissingScriptCount != 3 { - t.Fatalf("MissingScripts = %+v, want 3 total missing", got.MissingScripts) + if got.MissingScriptCount != 2 { + t.Fatalf("MissingScripts = %+v, want 2 total missing", got.MissingScripts) } if got.MissingBlockingCount != 1 || got.MissingBlockingScripts[0] != "scripts/check-missing.sh" { t.Fatalf("MissingBlockingScripts = %+v, want check-missing", got.MissingBlockingScripts) @@ -60,12 +58,11 @@ jobs: if got.MissingAdvisoryCount != 1 || got.MissingAdvisoryScripts[0] != "scripts/check-advisory.sh" { t.Fatalf("MissingAdvisoryScripts = %+v, want check-advisory", got.MissingAdvisoryScripts) } - if got.MissingDeferredCount != 1 || got.MissingDeferredScripts[0].Script != "scripts/lint-evidence-lines.sh" { - t.Fatalf("MissingDeferredScripts = %+v, want lint-evidence-lines", got.MissingDeferredScripts) + if got.MissingDeferredCount != 0 { + t.Fatalf("MissingDeferredScripts = %+v, want none", got.MissingDeferredScripts) } if got.MissingScripts[0] != "scripts/check-advisory.sh" || - got.MissingScripts[1] != "scripts/check-missing.sh" || - got.MissingScripts[2] != "scripts/lint-evidence-lines.sh" { + got.MissingScripts[1] != "scripts/check-missing.sh" { t.Fatalf("MissingScripts = %+v, want check-missing", got.MissingScripts) } if got.RegistryOnlyScriptCount != 1 || got.RegistryOnlyScripts[0] != "scripts/check-extra.sh" { @@ -74,7 +71,7 @@ jobs: } func TestDeferredWorkflowScriptCount(t *testing.T) { - if got := len(deferredWorkflowScripts); got != 16 { - t.Fatalf("deferredWorkflowScripts count = %d, want 16 (Wave E drain + applied-OOD model preflight + 4 post-parity job-scoped scripts)", got) + if got := len(deferredWorkflowScripts); got != 13 { + t.Fatalf("deferredWorkflowScripts count = %d, want 13", got) } } diff --git a/cli/internal/goals/AGENTS.md b/cli/internal/goals/AGENTS.md index 3850eb8d3..cf07855f3 100644 --- a/cli/internal/goals/AGENTS.md +++ b/cli/internal/goals/AGENTS.md @@ -7,13 +7,13 @@ contract_source: GOALS.md (operator-authored), GOALS.yaml (legacy), this package # cli/internal/goals -GOALS.yaml / GOALS.md fitness specification subsystem: load, validate, measure, snapshot, and detect drift on the strategic goal layer. +GOALS.yaml / GOALS.md fitness specification subsystem: load, validate, measure, snapshot, and detect drift without mutating strategic intent. ## Ownership - **Owner:** agentopsd extraction track (epic `agentops-tqc`). - **Operator-facing artifact:** `GOALS.md` at repo root (with `GOALS.yaml` as the legacy format). Either is valid input; `goals.go` parses both into a unified `GoalFile`. -- **Skill surface:** consumed by `skills/goals/SKILL.md` and `/evolve` goal-fitness gates. +- **Skill surface:** consumed by `skills/goals/SKILL.md` as optional measurement context. ## Interfaces @@ -22,20 +22,19 @@ GOALS.yaml / GOALS.md fitness specification subsystem: load, validate, measure, - `goals.go` — load + validate. - `measure.go` — fitness measurement (per-platform: `measure_unix.go`, `measure_windows.go`). - `markdown.go` — render/parse GOALS.md. - - `template.go` — bootstrap a new GOALS file. - - `commands.go` — CLI command wiring (subcommand handlers). + - `commands.go` — read-only CLI measurement and analysis handlers. - `drift.go` — detect when measured fitness drifts from the spec. - `history.go` — append/query the historical snapshot store. - `snapshot.go` — persist a measurement snapshot. -- **Subcommands the CLI exposes through this package:** `ao goals init`, `ao goals measure`, `ao goals measure --directives`, `ao goals steer add/remove/prioritize`, `ao goals migrate --to-md`. +- **Subcommands the CLI exposes through this package:** `ao goals measure`, `validate`, `drift`, `history`, `export`, `meta`, `trace`, `render`, and read-only scenario inspection. ## Non-obvious rules -- **Two file formats, one struct.** `GoalFile` round-trips both `GOALS.yaml` (YAML, legacy) and `GOALS.md` (markdown with structured sections, current). The `Format` field records which one was loaded; preserve it on save unless explicitly migrating. +- **Two file formats, one struct.** `GoalFile` reads both `GOALS.yaml` (YAML, legacy) and `GOALS.md` (markdown with structured sections, current). The CLI does not migrate or rewrite either format. - **Directives are GOALS.md-only.** `Directive` (numbered strategic intent) does not exist in YAML; it's a markdown-format-only feature. Don't add a YAML serialization without an explicit migration plan. - **Continuous metrics need a threshold.** `ContinuousMetric` requires both `metric` and `threshold` — drift detection compares against the threshold, not against an absolute baseline. - **Platform-gated measurement.** `measure_unix.go` and `measure_windows.go` are build-tagged. Adding a new measurement signal requires both implementations or a clean fallback. -- **Snapshots are append-only.** `history.go` writes to `.agents/goals/history/` (or equivalent); never edit historical snapshots, only append. +- **Snapshots are observations.** Measurement may append snapshots, but never rewrites `GOALS.md` or routes subsequent work. - **`measure --json` is part of the public CLI contract.** All `--json` flags must produce valid JSON (CI's `json-flag-consistency` job enforces this). ## Cross-references diff --git a/cli/internal/goals/commands.go b/cli/internal/goals/commands.go index 37850bdf8..a674858cd 100644 --- a/cli/internal/goals/commands.go +++ b/cli/internal/goals/commands.go @@ -1,21 +1,15 @@ package goals import ( - "bufio" - "context" "encoding/json" "fmt" "io" - "io/fs" "os" "path/filepath" "strings" "time" - "gopkg.in/yaml.v3" - "github.com/boshu2/agentops/cli/internal/paths" - "github.com/boshu2/agentops/cli/internal/shellutil" ) // withGoalFileCwd anchors the current process working directory to the git @@ -180,7 +174,7 @@ func RunMeasure(opts MeasureOptions) error { return fmt.Errorf("--directives and --goal cannot be combined") } if gf.Format != "md" { - return fmt.Errorf("--directives requires GOALS.md format. Run 'ao goals migrate --to-md' to convert") + return fmt.Errorf("--directives requires GOALS.md format") } enc := json.NewEncoder(opts.Stdout) enc.SetIndent("", " ") @@ -402,119 +396,6 @@ func RunExport(opts ExportOptions) error { return enc.Encode(snap) } -// MigrateOptions configures the goals migrate command. -type MigrateOptions struct { - ToMD bool - GoalsFile string - Stdout io.Writer -} - -// RunMigrate migrates goals between formats. -func RunMigrate(opts MigrateOptions) error { - if opts.Stdout == nil { - opts.Stdout = os.Stdout - } - - path := opts.GoalsFile - gf, err := LoadGoals(path) - if err != nil { - return fmt.Errorf("load goals: %w", err) - } - - if opts.ToMD { - if gf.Format == "md" { - fmt.Fprintln(opts.Stdout, "Already in GOALS.md format — no migration needed.") - return nil - } - gf.Format = "md" - gf.Version = 4 - if gf.Mission == "" { - gf.Mission = "Project fitness goals" - } - if len(gf.Directives) == 0 { - gf.Directives = DirectivesFromPillars(gf.Goals) - } - if len(gf.NorthStars) == 0 { - gf.NorthStars = []string{ - "Every check passes before changes reach users", - "Validation catches regressions automatically", - } - } - if len(gf.AntiStars) == 0 { - gf.AntiStars = []string{ - "Untested changes reaching main", - "Goals that are trivially true or test implementation details", - } - } - content := RenderGoalsMD(gf) - mdPath := filepath.Join(filepath.Dir(path), "GOALS.md") - if err := os.WriteFile(mdPath, []byte(content), 0o600); err != nil { - return fmt.Errorf("writing GOALS.md: %w", err) - } - fmt.Fprintf(opts.Stdout, "Migrated %s → %s (GOALS.md format, version 4)\n", path, mdPath) - fmt.Fprintln(opts.Stdout, "Original YAML file preserved. Delete it manually when ready.") - return nil - } - - if gf.Version >= 2 { - fmt.Fprintf(opts.Stdout, "%s is already version %d — no migration needed.\n", path, gf.Version) - return nil - } - - backupPath := path + ".v1.bak" - original, err := os.ReadFile(path) - if err != nil { - return fmt.Errorf("read original for backup: %w", err) - } - if err := os.WriteFile(backupPath, original, 0o600); err != nil { - return fmt.Errorf("write backup: %w", err) - } - fmt.Fprintf(opts.Stdout, "Backed up original to %s\n", backupPath) - - MigrateV1ToV2(gf) - - out, err := yaml.Marshal(gf) - if err != nil { - return fmt.Errorf("marshal migrated goals: %w", err) - } - if err := os.WriteFile(path, out, 0o600); err != nil { - return fmt.Errorf("write migrated goals: %w", err) - } - - fmt.Fprintf(opts.Stdout, "Migrated %s from version 1 to version 2.\n", path) - return nil -} - -// DirectivesFromPillars generates directives from existing goal pillar groupings. -func DirectivesFromPillars(gs []Goal) []Directive { - seen := map[string]bool{} - var pillars []string - for _, g := range gs { - if g.Pillar == "" { - continue - } - if !seen[g.Pillar] { - seen[g.Pillar] = true - pillars = append(pillars, g.Pillar) - } - } - if len(pillars) == 0 { - return []Directive{ - {Number: 1, Title: "Improve project quality", Description: "Focus on the highest-impact improvements.", Steer: "increase"}, - } - } - dirs := make([]Directive, len(pillars)) - for i, p := range pillars { - dirs[i] = Directive{ - Number: i + 1, - Title: "Strengthen " + p, - Description: fmt.Sprintf("Improve goals in the %s pillar.", p), - Steer: "increase", - } - } - return dirs -} - // MetaOptions configures the goals meta command. type MetaOptions struct { GoalsFile string @@ -650,738 +531,3 @@ func RunDrift(opts DriftOptions) error { fmt.Fprintf(opts.Stdout, "Baseline: %.1f%% -> Current: %.1f%%\n", latest.Summary.Score, current.Summary.Score) return nil } - -// SteerAddOptions configures the goals steer add command. -type SteerAddOptions struct { - Title string - Description string - Steer string - GoalsFile string - JSON bool - DryRun bool - Stdout io.Writer -} - -// ValidSteers enumerates the allowed steer values. -var ValidSteers = map[string]bool{ - "increase": true, - "decrease": true, - "hold": true, - "explore": true, -} - -// RunSteerAdd adds a new directive to GOALS.md. -func RunSteerAdd(opts SteerAddOptions) error { - if opts.Stdout == nil { - opts.Stdout = os.Stdout - } - if !ValidSteers[opts.Steer] { - return fmt.Errorf("invalid steer value %q (valid: increase, decrease, hold, explore)", opts.Steer) - } - - // LoadMDGoals enforces the GOALS.md-format guard (YAML is rejected) and - // gives the current directive numbering. - gf, resolvedPath, err := LoadMDGoals(opts.GoalsFile) - if err != nil { - return err - } - - maxNum := 0 - for _, d := range gf.Directives { - if d.Number > maxNum { - maxNum = d.Number - } - } - newNum := maxNum + 1 - - // --dry-run previews without writing (either output format). --json is an - // output-format flag, NOT a no-write flag — a write command must persist - // under --json and emit the result as JSON (soc-3z69s). - if opts.DryRun { - if opts.JSON { - enc := json.NewEncoder(opts.Stdout) - enc.SetIndent("", " ") - return enc.Encode(Directive{Number: newNum, Title: opts.Title, Description: opts.Description, Steer: opts.Steer}) - } - fmt.Fprintf(opts.Stdout, "Would add directive #%d: %s\n", newNum, opts.Title) - return nil - } - - // Persist through GoalsPatcher, NOT WriteMDGoals: the latter re-renders - // GOALS.md from the GoalFile model and silently drops the "## Three-Gap - // Contract Proof Surface" section, the Gates table, prose, and - // agentops:claim comments. The patcher appends the new directive block and - // preserves every other byte (soc-byt52). - p, _, err := LoadGoalsPatcher(opts.GoalsFile) - if err != nil { - return err - } - num, err := p.AppendDirective(opts.Title, opts.Description, opts.Steer) - if err != nil { - return err - } - if err := p.WriteFile(resolvedPath); err != nil { - return err - } - if opts.JSON { - enc := json.NewEncoder(opts.Stdout) - enc.SetIndent("", " ") - return enc.Encode(Directive{Number: num, Title: opts.Title, Description: opts.Description, Steer: opts.Steer}) - } - fmt.Fprintf(opts.Stdout, "Added directive #%d: %s (steer: %s)\n", num, opts.Title, opts.Steer) - return nil -} - -// SteerRemoveOptions configures the goals steer remove command. -type SteerRemoveOptions struct { - Number int - GoalsFile string - JSON bool - DryRun bool - Stdout io.Writer -} - -// RunSteerRemove removes a directive by number. -func RunSteerRemove(opts SteerRemoveOptions) error { - if opts.Stdout == nil { - opts.Stdout = os.Stdout - } - gf, resolvedPath, err := LoadMDGoals(opts.GoalsFile) - if err != nil { - return err - } - - found := false - var remaining []Directive - for _, d := range gf.Directives { - if d.Number == opts.Number { - found = true - continue - } - remaining = append(remaining, d) - } - if !found { - return fmt.Errorf("directive #%d not found", opts.Number) - } - for i := range remaining { - remaining[i].Number = i + 1 - } - gf.Directives = remaining - - // --dry-run previews without writing (either format); --json is output - // format, not a no-write flag (soc-3z69s). - if opts.DryRun { - if opts.JSON { - enc := json.NewEncoder(opts.Stdout) - enc.SetIndent("", " ") - return enc.Encode(gf.Directives) - } - fmt.Fprintf(opts.Stdout, "Would remove directive #%d and renumber %d remaining\n", opts.Number, len(remaining)) - return nil - } - // Persist via GoalsPatcher, not WriteMDGoals: the latter re-renders from - // the model and drops non-directive sections (Three-Gap, Gates, claim - // comments). The patcher deletes the block + renumbers, preserving every - // other byte (soc-5335b). - p, _, err := LoadGoalsPatcher(opts.GoalsFile) - if err != nil { - return err - } - if err := p.RemoveDirective(opts.Number); err != nil { - return err - } - if err := p.WriteFile(resolvedPath); err != nil { - return err - } - if opts.JSON { - enc := json.NewEncoder(opts.Stdout) - enc.SetIndent("", " ") - return enc.Encode(gf.Directives) - } - fmt.Fprintf(opts.Stdout, "Removed directive #%d, renumbered %d remaining\n", opts.Number, len(remaining)) - return nil -} - -// SteerPrioritizeOptions configures the goals steer prioritize command. -type SteerPrioritizeOptions struct { - Number int - NewPosition int - GoalsFile string - JSON bool - DryRun bool - Stdout io.Writer -} - -// RunSteerPrioritize moves a directive to a new position. -func RunSteerPrioritize(opts SteerPrioritizeOptions) error { - if opts.Stdout == nil { - opts.Stdout = os.Stdout - } - gf, resolvedPath, err := LoadMDGoals(opts.GoalsFile) - if err != nil { - return err - } - if len(gf.Directives) == 0 { - return fmt.Errorf("no directives to prioritize") - } - if opts.NewPosition < 1 || opts.NewPosition > len(gf.Directives) { - return fmt.Errorf("new position must be between 1 and %d", len(gf.Directives)) - } - - srcIdx := -1 - for i, d := range gf.Directives { - if d.Number == opts.Number { - srcIdx = i - break - } - } - if srcIdx < 0 { - return fmt.Errorf("directive #%d not found", opts.Number) - } - - moving := gf.Directives[srcIdx] - directives := make([]Directive, 0, len(gf.Directives)) - directives = append(directives, gf.Directives[:srcIdx]...) - directives = append(directives, gf.Directives[srcIdx+1:]...) - - insertIdx := opts.NewPosition - 1 - if insertIdx > len(directives) { - insertIdx = len(directives) - } - result := make([]Directive, 0, len(gf.Directives)) - result = append(result, directives[:insertIdx]...) - result = append(result, moving) - result = append(result, directives[insertIdx:]...) - for i := range result { - result[i].Number = i + 1 - } - gf.Directives = result - - // --dry-run previews without writing (either format); --json is output - // format, not a no-write flag (soc-3z69s). - if opts.DryRun { - if opts.JSON { - enc := json.NewEncoder(opts.Stdout) - enc.SetIndent("", " ") - return enc.Encode(gf.Directives) - } - fmt.Fprintf(opts.Stdout, "Would move directive %q to position %d\n", moving.Title, opts.NewPosition) - return nil - } - // Persist via GoalsPatcher, not WriteMDGoals: preserves non-directive - // sections while reordering + renumbering the directive blocks (soc-5335b). - p, _, err := LoadGoalsPatcher(opts.GoalsFile) - if err != nil { - return err - } - if err := p.MoveDirective(opts.Number, opts.NewPosition); err != nil { - return err - } - if err := p.WriteFile(resolvedPath); err != nil { - return err - } - if opts.JSON { - enc := json.NewEncoder(opts.Stdout) - enc.SetIndent("", " ") - return enc.Encode(gf.Directives) - } - fmt.Fprintf(opts.Stdout, "Moved directive %q to position %d\n", moving.Title, opts.NewPosition) - return nil -} - -// LoadMDGoals loads goals and validates the format is markdown. -func LoadMDGoals(goalsFile string) (*GoalFile, string, error) { - resolvedPath := ResolveGoalsPath(goalsFile) - gf, err := LoadGoals(goalsFile) - if err != nil { - return nil, "", fmt.Errorf("loading goals: %w", err) - } - if gf.Format != "md" { - return nil, "", fmt.Errorf("directives require GOALS.md format; run 'ao goals migrate --to-md'") - } - return gf, resolvedPath, nil -} - -// WriteMDGoals renders and writes a GoalFile back to disk as GOALS.md. -func WriteMDGoals(gf *GoalFile, path string) error { - content := RenderGoalsMD(gf) - if strings.ToLower(filepath.Ext(path)) != ".md" { - path = filepath.Join(filepath.Dir(path), "GOALS.md") - } - if err := os.WriteFile(path, []byte(content), 0o600); err != nil { - return fmt.Errorf("writing goals file: %w", err) - } - return nil -} - -// PruneOptions configures the goals prune command. -type PruneOptions struct { - GoalsFile string - DryRun bool - JSON bool - Stdout io.Writer -} - -// PruneResult holds the outcome of a goals prune operation. -type PruneResult struct { - StaleGoals []StaleGoal `json:"stale_goals"` - Removed int `json:"removed"` - DryRun bool `json:"dry_run"` -} - -// StaleGoal identifies a goal referencing a nonexistent file. -type StaleGoal struct { - ID string `json:"id"` - Check string `json:"check"` - Path string `json:"missing_path"` -} - -// FindMissingPath checks if a goal's check command references a missing file. -func FindMissingPath(check string) string { - parts := strings.Fields(check) - for _, part := range parts { - if strings.HasPrefix(part, "scripts/") || strings.HasPrefix(part, "./scripts/") || - strings.HasPrefix(part, "tests/") || strings.HasPrefix(part, "./tests/") || - strings.HasPrefix(part, "hooks/") || strings.HasPrefix(part, "./hooks/") { - cleanPath := strings.TrimRight(part, ";|&") - if _, err := os.Stat(cleanPath); os.IsNotExist(err) { - return cleanPath - } - } - if strings.Contains(part, "/") && filepath.Ext(part) != "" { - cleanPath := strings.TrimRight(part, ";|&") - if _, err := os.Stat(cleanPath); os.IsNotExist(err) { - return cleanPath - } - } - } - return "" -} - -// AddOptions configures the goals add command. -type AddOptions struct { - ID, Check, Type, Description, GoalsFile string - Weight int - Timeout time.Duration - DryRun bool - Stdout io.Writer -} - -// RunAdd adds a new goal. -func RunAdd(ctx context.Context, opts AddOptions) error { - if opts.Stdout == nil { - opts.Stdout = os.Stdout - } - if !KebabRe.MatchString(opts.ID) { - return fmt.Errorf("goal ID must be kebab-case: %q", opts.ID) - } - - gf, err := LoadGoals(opts.GoalsFile) - if err != nil { - return fmt.Errorf("loading goals: %w", err) - } - for _, g := range gf.Goals { - if g.ID == opts.ID { - return fmt.Errorf("goal %q already exists", opts.ID) - } - } - - if !opts.DryRun { - checkCtx, cancel := context.WithTimeout(ctx, opts.Timeout) - defer cancel() - // SanitizedBashCommand bypasses ~/.bashrc and BASH_ENV so user shell - // aliases cannot silently change the meaning of new goal check strings. - testCmd := shellutil.SanitizedBashCommand(checkCtx, opts.Check) - if out, err := testCmd.CombinedOutput(); err != nil { - return fmt.Errorf("check command failed (exit non-zero):\n%s", string(out)) - } - } - - goalType := GoalType(opts.Type) - if opts.Type != "" && !ValidTypes[goalType] { - return fmt.Errorf("invalid type %q (valid: health, architecture, quality, meta)", opts.Type) - } - if opts.Type == "" { - goalType = GoalTypeHealth - } - desc := opts.Description - if desc == "" { - desc = opts.ID - } - - newGoal := Goal{ID: opts.ID, Description: desc, Check: opts.Check, Weight: opts.Weight, Type: goalType} - gf.Goals = append(gf.Goals, newGoal) - - if gf.Format == "md" { - content := RenderGoalsMD(gf) - if err := os.WriteFile(ResolveGoalsPath(opts.GoalsFile), []byte(content), 0o600); err != nil { - return fmt.Errorf("writing goals: %w", err) - } - } else { - data, err := yaml.Marshal(gf) - if err != nil { - return fmt.Errorf("marshaling goals: %w", err) - } - if err := os.WriteFile(opts.GoalsFile, data, 0o600); err != nil { - return fmt.Errorf("writing goals: %w", err) - } - } - - fmt.Fprintf(opts.Stdout, "Added goal %q (type: %s, weight: %d)\n", opts.ID, goalType, opts.Weight) - return nil -} - -// InitOptions configures the goals init command. -type InitOptions struct { - NonInteractive bool - Template string - GoalsFile string - JSON bool - DryRun bool - Stdin io.Reader - Stdout io.Writer - TemplatesFS fs.ReadFileFS -} - -// GoalTemplate is the YAML structure of an embedded template file. -type GoalTemplate struct { - Name string `yaml:"name"` - Description string `yaml:"description"` - Directives []string `yaml:"directives"` - Gates []GoalTemplateGate `yaml:"gates"` -} - -// GoalTemplateGate mirrors a single gate entry in a template YAML file. -type GoalTemplateGate struct { - ID string `yaml:"id"` - Description string `yaml:"description"` - Check string `yaml:"check"` - Weight int `yaml:"weight"` - Type string `yaml:"type"` -} - -// ValidTemplateNames lists the recognised --template values. -var ValidTemplateNames = []string{"go-cli", "python-lib", "web-app", "rust-cli", "generic"} - -// BuildDefaultGoalFile creates a GoalFile with sensible defaults. -func BuildDefaultGoalFile() *GoalFile { - dir, err := os.Getwd() - if err != nil { - dir = "project" - } - dirName := filepath.Base(dir) - - return &GoalFile{ - Version: 4, Format: "md", - Mission: fmt.Sprintf("Fitness goals for %s", dirName), - NorthStars: []string{"All checks pass on every commit"}, - AntiStars: []string{"Untested changes reaching main"}, - Directives: []Directive{{Number: 1, Title: "Establish baseline", Description: "Get all gates passing and maintain a green baseline.", Steer: "increase"}}, - } -} - -// BuildInteractiveGoalFile prompts the user for goal file fields. -func BuildInteractiveGoalFile(r io.Reader) (*GoalFile, error) { - scanner := bufio.NewScanner(r) - - mission, err := promptLine(scanner, "Mission (one sentence): ") - if err != nil { - return nil, err - } - if mission == "" { - dir, _ := os.Getwd() - if dir == "" { - dir = "project" - } - mission = fmt.Sprintf("Fitness goals for %s", filepath.Base(dir)) - } - - northRaw, _ := promptLine(scanner, "North stars (comma-separated): ") - northStars := SplitCommaSeparated(northRaw) - if len(northStars) == 0 { - northStars = []string{"All checks pass on every commit"} - } - - antiRaw, _ := promptLine(scanner, "Anti stars (comma-separated): ") - antiStars := SplitCommaSeparated(antiRaw) - if len(antiStars) == 0 { - antiStars = []string{"Untested changes reaching main"} - } - - dirTitle, _ := promptLine(scanner, "First directive title: ") - if dirTitle == "" { - dirTitle = "Establish baseline" - } - - dirDesc, _ := promptLine(scanner, "First directive description: ") - if dirDesc == "" { - dirDesc = "Get all gates passing and maintain a green baseline." - } - - return &GoalFile{ - Version: 4, Format: "md", Mission: mission, - NorthStars: northStars, AntiStars: antiStars, - Directives: []Directive{{Number: 1, Title: dirTitle, Description: dirDesc, Steer: "increase"}}, - }, nil -} - -// DetectGates checks for common project files and returns matching gate goals. -func DetectGates(projectRoot string) []Goal { - var detected []Goal - stat := func(rel string) bool { - _, err := os.Stat(filepath.Join(projectRoot, rel)) - return err == nil - } - - switch { - case stat("cli/go.mod"): - detected = append(detected, - Goal{ID: "go-build", Description: "Go project builds cleanly", Check: "cd cli && go build ./...", Weight: 5, Type: GoalTypeHealth}, - Goal{ID: "go-test", Description: "Go tests pass", Check: "cd cli && go test ./...", Weight: 5, Type: GoalTypeHealth}) - case stat("go.mod"): - detected = append(detected, - Goal{ID: "go-build", Description: "Go project builds cleanly", Check: "go build ./...", Weight: 5, Type: GoalTypeHealth}, - Goal{ID: "go-test", Description: "Go tests pass", Check: "go test ./...", Weight: 5, Type: GoalTypeHealth}) - } - if stat("package.json") { - detected = append(detected, Goal{ID: "npm-test", Description: "npm tests pass", Check: "npm test", Weight: 5, Type: GoalTypeHealth}) - } - if stat("Cargo.toml") { - detected = append(detected, Goal{ID: "cargo-test", Description: "Cargo tests pass", Check: "cargo test", Weight: 5, Type: GoalTypeHealth}) - } - if stat("pyproject.toml") { - detected = append(detected, Goal{ID: "python-test", Description: "Python tests pass", Check: "pytest", Weight: 5, Type: GoalTypeHealth}) - } - if stat("Makefile") { - detected = append(detected, Goal{ID: "make-build", Description: "Make build succeeds", Check: "make build", Weight: 5, Type: GoalTypeHealth}) - } - return detected -} - -// LoadTemplate reads a named template from a filesystem. -func LoadTemplate(fsys fs.ReadFileFS, name string) (*GoalTemplate, error) { - data, err := fs.ReadFile(fsys, filepath.Join("templates", name+".yaml")) - if err != nil { - return nil, fmt.Errorf("template %q not found: %w", name, err) - } - var tmpl GoalTemplate - if err := yaml.Unmarshal(data, &tmpl); err != nil { - return nil, fmt.Errorf("parsing template %q: %w", name, err) - } - return &tmpl, nil -} - -// TemplateGatesToGoals converts template gates into Goal values. -func TemplateGatesToGoals(tmpl *GoalTemplate) []Goal { - out := make([]Goal, 0, len(tmpl.Gates)) - for _, g := range tmpl.Gates { - out = append(out, Goal{ID: g.ID, Description: g.Description, Check: g.Check, Weight: g.Weight, Type: GoalType(g.Type)}) - } - return out -} - -// AutoDetectTemplate chooses a template name based on project marker files. -func AutoDetectTemplate(projectRoot string) string { - stat := func(rel string) bool { - _, err := os.Stat(filepath.Join(projectRoot, rel)) - return err == nil - } - switch { - case stat("go.mod") || stat("cli/go.mod"): - return "go-cli" - case stat("Cargo.toml"): - return "rust-cli" - case stat("pyproject.toml") || stat("setup.py"): - return "python-lib" - case stat("package.json"): - return "web-app" - default: - return "" - } -} - -// SplitCommaSeparated splits a comma-separated string, trimming whitespace. -func SplitCommaSeparated(s string) []string { - if strings.TrimSpace(s) == "" { - return nil - } - parts := strings.Split(s, ",") - var result []string - for _, p := range parts { - p = strings.TrimSpace(p) - if p != "" { - result = append(result, p) - } - } - return result -} - -// RunPrune removes goals referencing nonexistent files. -func RunPrune(opts PruneOptions) error { - if opts.Stdout == nil { - opts.Stdout = os.Stdout - } - - resolved := opts.GoalsFile - resolvedPath := ResolveGoalsPath(resolved) - - gf, err := LoadGoals(resolved) - if err != nil { - return fmt.Errorf("loading goals: %w", err) - } - - var stale []StaleGoal - staleIDs := make(map[string]bool) - - for _, g := range gf.Goals { - missingPath := FindMissingPath(g.Check) - if missingPath != "" { - stale = append(stale, StaleGoal{ID: g.ID, Check: g.Check, Path: missingPath}) - staleIDs[g.ID] = true - } - } - - result := PruneResult{StaleGoals: stale, DryRun: opts.DryRun} - - if opts.DryRun || len(stale) == 0 { - if opts.JSON { - enc := json.NewEncoder(opts.Stdout) - enc.SetIndent("", " ") - return enc.Encode(result) - } - if len(stale) == 0 { - fmt.Fprintln(opts.Stdout, "No stale goals found.") - return nil - } - fmt.Fprintf(opts.Stdout, "Found %d stale goal(s):\n", len(stale)) - for _, s := range stale { - fmt.Fprintf(opts.Stdout, " %s: %s (missing: %s)\n", s.ID, s.Check, s.Path) - } - fmt.Fprintln(opts.Stdout, "\nRun without --dry-run to remove them.") - return nil - } - - var kept []Goal - for _, g := range gf.Goals { - if !staleIDs[g.ID] { - kept = append(kept, g) - } - } - gf.Goals = kept - result.Removed = len(stale) - - if gf.Format == "md" { - if err := WriteMDGoals(gf, resolvedPath); err != nil { - return err - } - } else { - data, err := yaml.Marshal(gf) - if err != nil { - return fmt.Errorf("marshaling goals: %w", err) - } - if err := os.WriteFile(resolvedPath, data, 0o600); err != nil { - return fmt.Errorf("writing goals file: %w", err) - } - } - - if opts.JSON { - enc := json.NewEncoder(opts.Stdout) - enc.SetIndent("", " ") - return enc.Encode(result) - } - - fmt.Fprintf(opts.Stdout, "Pruned %d stale goal(s) from %s\n", result.Removed, resolvedPath) - for _, s := range stale { - fmt.Fprintf(opts.Stdout, " removed: %s (missing: %s)\n", s.ID, s.Path) - } - return nil -} - -// RunInit bootstraps a new GOALS.md file. -func RunInit(opts InitOptions) error { - if opts.Stdout == nil { - opts.Stdout = os.Stdout - } - if opts.Stdin == nil { - opts.Stdin = os.Stdin - } - - resolvedPath := ResolveGoalsPath(opts.GoalsFile) - - if _, err := os.Stat(resolvedPath); err == nil { - return fmt.Errorf("goals file already exists: %s", resolvedPath) - } - if resolvedPath != opts.GoalsFile { - if _, err := os.Stat(opts.GoalsFile); err == nil { - return fmt.Errorf("goals file already exists: %s", opts.GoalsFile) - } - } - - projectRoot := filepath.Dir(resolvedPath) - tmplName := opts.Template - if tmplName == "" { - tmplName = AutoDetectTemplate(projectRoot) - } - - var tmpl *GoalTemplate - if tmplName != "" && opts.TemplatesFS != nil { - var err error - tmpl, err = LoadTemplate(opts.TemplatesFS, tmplName) - if err != nil { - return fmt.Errorf("loading template %q: %w", tmplName, err) - } - } - - var gf *GoalFile - if opts.NonInteractive { - gf = BuildDefaultGoalFile() - } else { - var err error - gf, err = BuildInteractiveGoalFile(opts.Stdin) - if err != nil { - return fmt.Errorf("reading input: %w", err) - } - } - - if tmpl != nil { - gf.Goals = append(gf.Goals, TemplateGatesToGoals(tmpl)...) - } else { - gf.Goals = append(gf.Goals, DetectGates(projectRoot)...) - } - - if opts.JSON { - enc := json.NewEncoder(opts.Stdout) - enc.SetIndent("", " ") - return enc.Encode(gf) - } - - content := RenderGoalsMD(gf) - - outPath := resolvedPath - if strings.ToLower(filepath.Ext(outPath)) != ".md" { - outPath = filepath.Join(filepath.Dir(outPath), "GOALS.md") - } - - if opts.DryRun { - fmt.Fprintf(opts.Stdout, "Would write %s:\n\n%s", outPath, content) - return nil - } - - if err := os.WriteFile(outPath, []byte(content), 0o600); err != nil { - return fmt.Errorf("writing goals file: %w", err) - } - - fmt.Fprintf(opts.Stdout, "Created %s with %d gates\n", outPath, len(gf.Goals)) - return nil -} - -func promptLine(scanner *bufio.Scanner, msg string) (string, error) { - fmt.Print(msg) - if scanner.Scan() { - return strings.TrimSpace(scanner.Text()), nil - } - if err := scanner.Err(); err != nil { - return "", err - } - return "", nil -} diff --git a/cli/internal/goals/commands_test.go b/cli/internal/goals/commands_test.go deleted file mode 100644 index 6e2931785..000000000 --- a/cli/internal/goals/commands_test.go +++ /dev/null @@ -1,913 +0,0 @@ -package goals - -import ( - "bytes" - "encoding/json" - "io/fs" - "os" - "path/filepath" - "strings" - "testing" - "testing/fstest" -) - -// writeGoalsMD writes a minimal valid GOALS.md file at path. -func writeGoalsMD(t *testing.T, path, extra string) { - t.Helper() - content := `# Fitness Goals - -## Mission - -Test mission. - -## North Stars - -- Green CI - -## Anti-Stars - -- Untested code - -## Directives - -### 1. Establish baseline - -Get gates green. - -**Steer:** increase - -## Gates - -` + extra - if err := os.WriteFile(path, []byte(content), 0o600); err != nil { - t.Fatalf("write goals: %v", err) - } -} - -func TestOutputValidateResult_ValidNonJSON(t *testing.T) { - var buf bytes.Buffer - result := ValidateResult{Valid: true, GoalCount: 3, Version: 4, Format: "md", Directives: 2} - if err := OutputValidateResult(&buf, false, result); err != nil { - t.Fatal(err) - } - s := buf.String() - if !strings.Contains(s, "VALID: 3 goals") { - t.Errorf("output missing summary: %s", s) - } - if !strings.Contains(s, "Directives: 2") { - t.Errorf("output missing directives line: %s", s) - } -} - -func TestOutputValidateResult_InvalidNonJSON(t *testing.T) { - var buf bytes.Buffer - result := ValidateResult{Valid: false, Errors: []string{"missing id"}, Warnings: []string{"no mission"}} - err := OutputValidateResult(&buf, false, result) - if err == nil { - t.Fatal("expected error for invalid result") - } - s := buf.String() - if !strings.Contains(s, "INVALID: 1 errors") { - t.Errorf("missing invalid header: %s", s) - } - if !strings.Contains(s, "ERROR: missing id") { - t.Errorf("missing error detail: %s", s) - } - if !strings.Contains(s, "WARN: no mission") { - t.Errorf("missing warning: %s", s) - } -} - -func TestOutputValidateResult_JSON(t *testing.T) { - var buf bytes.Buffer - result := ValidateResult{Valid: true, GoalCount: 2} - if err := OutputValidateResult(&buf, true, result); err != nil { - t.Fatal(err) - } - var got ValidateResult - if err := json.Unmarshal(buf.Bytes(), &got); err != nil { - t.Fatalf("unmarshal: %v", err) - } - if got.GoalCount != 2 || !got.Valid { - t.Errorf("got %+v", got) - } -} - -func TestDirectivesFromPillars_WithPillars(t *testing.T) { - gs := []Goal{ - {ID: "a", Pillar: "quality"}, - {ID: "b", Pillar: "quality"}, - {ID: "c", Pillar: "health"}, - } - dirs := DirectivesFromPillars(gs) - if len(dirs) != 2 { - t.Fatalf("expected 2 pillars, got %d", len(dirs)) - } - if dirs[0].Number != 1 { - t.Errorf("first number should be 1, got %d", dirs[0].Number) - } - if dirs[0].Steer != "increase" { - t.Errorf("default steer should be 'increase', got %q", dirs[0].Steer) - } - if !strings.Contains(dirs[0].Title, "quality") { - t.Errorf("title should include pillar name, got %q", dirs[0].Title) - } -} - -func TestDirectivesFromPillars_NoPillars(t *testing.T) { - gs := []Goal{{ID: "a"}, {ID: "b"}} - dirs := DirectivesFromPillars(gs) - if len(dirs) != 1 { - t.Errorf("expected 1 default directive, got %d", len(dirs)) - } -} - -func TestFindMissingPath(t *testing.T) { - tmp := t.TempDir() - t.Chdir(tmp) - - _ = os.MkdirAll("scripts", 0o755) - _ = os.WriteFile("scripts/real.sh", []byte("#!/bin/sh\n"), 0o600) - - // Existing file -> no missing - if got := FindMissingPath("scripts/real.sh"); got != "" { - t.Errorf("expected empty for existing, got %q", got) - } - // Missing file - if got := FindMissingPath("scripts/missing.sh"); got != "scripts/missing.sh" { - t.Errorf("got %q", got) - } - // Non-script path with extension detects missing - if got := FindMissingPath("tests/nope.bats"); got != "tests/nope.bats" { - t.Errorf("tests path: got %q", got) - } - // No filesystem references - if got := FindMissingPath("echo hello world"); got != "" { - t.Errorf("pure command should have no missing, got %q", got) - } -} - -func TestSplitCommaSeparated(t *testing.T) { - cases := map[string][]string{ - "a, b, c": {"a", "b", "c"}, - "a,,b": {"a", "b"}, - " ": nil, - "": nil, - "one": {"one"}, - " a , b ": {"a", "b"}, - } - for in, want := range cases { - got := SplitCommaSeparated(in) - if len(got) != len(want) { - t.Errorf("%q: got %v, want %v", in, got, want) - continue - } - for i := range got { - if got[i] != want[i] { - t.Errorf("%q[%d]: got %q, want %q", in, i, got[i], want[i]) - } - } - } -} - -func TestValidSteers(t *testing.T) { - for _, s := range []string{"increase", "decrease", "hold", "explore"} { - if !ValidSteers[s] { - t.Errorf("%q should be valid", s) - } - } - if ValidSteers["nonsense"] { - t.Error("nonsense should not be valid") - } -} - -func TestDetectGates_GoDir(t *testing.T) { - tmp := t.TempDir() - _ = os.MkdirAll(filepath.Join(tmp, "cli"), 0o755) - _ = os.WriteFile(filepath.Join(tmp, "cli", "go.mod"), []byte("module x\n"), 0o600) - - goals := DetectGates(tmp) - ids := map[string]bool{} - for _, g := range goals { - ids[g.ID] = true - } - if !ids["go-build"] || !ids["go-test"] { - t.Errorf("expected go gates, got %v", ids) - } -} - -func TestDetectGates_RootGoMod(t *testing.T) { - tmp := t.TempDir() - _ = os.WriteFile(filepath.Join(tmp, "go.mod"), []byte("module x\n"), 0o600) - - goals := DetectGates(tmp) - found := false - for _, g := range goals { - if g.Check == "go build ./..." { - found = true - } - } - if !found { - t.Errorf("root go.mod should yield 'go build ./...', got %+v", goals) - } -} - -func TestDetectGates_Multi(t *testing.T) { - tmp := t.TempDir() - _ = os.WriteFile(filepath.Join(tmp, "package.json"), []byte("{}"), 0o600) - _ = os.WriteFile(filepath.Join(tmp, "Cargo.toml"), []byte(""), 0o600) - _ = os.WriteFile(filepath.Join(tmp, "pyproject.toml"), []byte(""), 0o600) - _ = os.WriteFile(filepath.Join(tmp, "Makefile"), []byte(""), 0o600) - - gates := DetectGates(tmp) - want := []string{"npm-test", "cargo-test", "python-test", "make-build"} - found := map[string]bool{} - for _, g := range gates { - found[g.ID] = true - } - for _, w := range want { - if !found[w] { - t.Errorf("missing gate %q (got %v)", w, found) - } - } -} - -func TestAutoDetectTemplate(t *testing.T) { - cases := []struct { - name string - marker string - content string - want string - }{ - {"go", "go.mod", "module x\n", "go-cli"}, - {"cli/go", "cli/go.mod", "module x\n", "go-cli"}, - {"rust", "Cargo.toml", "", "rust-cli"}, - {"python", "pyproject.toml", "", "python-lib"}, - {"python setup", "setup.py", "", "python-lib"}, - {"web", "package.json", "{}", "web-app"}, - } - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - tmp := t.TempDir() - p := filepath.Join(tmp, tc.marker) - _ = os.MkdirAll(filepath.Dir(p), 0o755) - _ = os.WriteFile(p, []byte(tc.content), 0o600) - if got := AutoDetectTemplate(tmp); got != tc.want { - t.Errorf("got %q, want %q", got, tc.want) - } - }) - } - - // No markers -> empty - if got := AutoDetectTemplate(t.TempDir()); got != "" { - t.Errorf("expected empty for no markers, got %q", got) - } -} - -func TestLoadTemplate_ValidYAML(t *testing.T) { - body := `name: test -description: testing -gates: - - id: g1 - description: gate one - check: echo ok - weight: 3 - type: health -` - fsys := fstest.MapFS{"templates/test.yaml": &fstest.MapFile{Data: []byte(body)}} - tmpl, err := LoadTemplate(fsys, "test") - if err != nil { - t.Fatal(err) - } - if tmpl.Name != "test" { - t.Errorf("name = %q", tmpl.Name) - } - if len(tmpl.Gates) != 1 { - t.Fatalf("expected 1 gate, got %d", len(tmpl.Gates)) - } - if tmpl.Gates[0].ID != "g1" { - t.Errorf("gate id = %q", tmpl.Gates[0].ID) - } -} - -func TestLoadTemplate_Missing(t *testing.T) { - fsys := fstest.MapFS{} - if _, err := LoadTemplate(fsys, "nope"); err == nil { - t.Fatal("expected error for missing template") - } -} - -func TestLoadTemplate_InvalidYAML(t *testing.T) { - // YAML that parses as a list, not as a GoalTemplate struct -> yaml decoder errors - fsys := fstest.MapFS{"templates/bad.yaml": &fstest.MapFile{Data: []byte("- just a list item\n- another\n")}} - _, err := LoadTemplate(fsys, "bad") - if err == nil { - t.Fatal("expected parse error") - } -} - -func TestTemplateGatesToGoals(t *testing.T) { - tmpl := &GoalTemplate{Gates: []GoalTemplateGate{ - {ID: "a", Description: "A", Check: "echo a", Weight: 1, Type: "health"}, - {ID: "b", Description: "B", Check: "echo b", Weight: 2, Type: "quality"}, - }} - goals := TemplateGatesToGoals(tmpl) - if len(goals) != 2 { - t.Fatalf("got %d", len(goals)) - } - if goals[0].ID != "a" || goals[1].Type != GoalType("quality") { - t.Errorf("goals = %+v", goals) - } -} - -func TestBuildInteractiveGoalFile_AllDefaults(t *testing.T) { - in := strings.NewReader("\n\n\n\n\n") - gf, err := BuildInteractiveGoalFile(in) - if err != nil { - t.Fatal(err) - } - if gf.Version != 4 || gf.Format != "md" { - t.Errorf("version/format: %d %q", gf.Version, gf.Format) - } - if gf.Mission == "" { - t.Error("mission should have default") - } - if len(gf.NorthStars) == 0 || len(gf.AntiStars) == 0 || len(gf.Directives) == 0 { - t.Errorf("defaults not populated: %+v", gf) - } - if gf.Directives[0].Steer != "increase" { - t.Errorf("default steer = %q", gf.Directives[0].Steer) - } -} - -func TestBuildInteractiveGoalFile_CustomValues(t *testing.T) { - in := strings.NewReader("Custom mission\nNS1, NS2\nAS1\nFirst title\nFirst desc\n") - gf, err := BuildInteractiveGoalFile(in) - if err != nil { - t.Fatal(err) - } - if gf.Mission != "Custom mission" { - t.Errorf("mission = %q", gf.Mission) - } - if len(gf.NorthStars) != 2 || gf.NorthStars[0] != "NS1" { - t.Errorf("north stars = %v", gf.NorthStars) - } - if gf.Directives[0].Title != "First title" { - t.Errorf("title = %q", gf.Directives[0].Title) - } -} - -func TestBuildDefaultGoalFile(t *testing.T) { - gf := BuildDefaultGoalFile() - if gf.Version != 4 || gf.Format != "md" { - t.Errorf("version/format") - } - if len(gf.Directives) == 0 { - t.Error("no directives") - } - if gf.Directives[0].Steer != "increase" { - t.Errorf("steer = %q", gf.Directives[0].Steer) - } -} - -func TestRunSteerAdd_RejectsInvalidSteer(t *testing.T) { - opts := SteerAddOptions{Title: "x", Description: "y", Steer: "bogus"} - err := RunSteerAdd(opts) - if err == nil { - t.Fatal("expected error for invalid steer") - } - if !strings.Contains(err.Error(), "invalid steer") { - t.Errorf("err = %v", err) - } -} - -func TestRunSteerAdd_AddsDirective(t *testing.T) { - tmp := t.TempDir() - t.Chdir(tmp) - writeGoalsMD(t, "GOALS.md", "") - - var buf bytes.Buffer - opts := SteerAddOptions{ - Title: "New directive", Description: "Desc", Steer: "increase", - GoalsFile: "GOALS.md", Stdout: &buf, - } - if err := RunSteerAdd(opts); err != nil { - t.Fatalf("err = %v", err) - } - if !strings.Contains(buf.String(), "Added directive #2") { - t.Errorf("output = %q", buf.String()) - } - // File should now have the directive - data, _ := os.ReadFile("GOALS.md") - if !strings.Contains(string(data), "New directive") { - t.Errorf("file missing new directive") - } -} - -func TestRunSteerAdd_DryRun(t *testing.T) { - tmp := t.TempDir() - t.Chdir(tmp) - writeGoalsMD(t, "GOALS.md", "") - before, _ := os.ReadFile("GOALS.md") - - var buf bytes.Buffer - opts := SteerAddOptions{ - Title: "Dry", Description: "Dry", Steer: "hold", - GoalsFile: "GOALS.md", DryRun: true, Stdout: &buf, - } - if err := RunSteerAdd(opts); err != nil { - t.Fatal(err) - } - if !strings.Contains(buf.String(), "Would add") { - t.Errorf("output = %q", buf.String()) - } - after, _ := os.ReadFile("GOALS.md") - if string(before) != string(after) { - t.Errorf("file should be unchanged on dry-run") - } -} - -// TestRunSteerAdd_PreservesNonDirectiveContent is the soc-byt52 regression: -// `ao goals steer add` must append the directive WITHOUT dropping sections the -// GoalFile model does not represent (Three-Gap section, Gates rows, claim -// comments). The old LoadMDGoals→WriteMDGoals round-trip silently deleted them. -func TestRunSteerAdd_PreservesNonDirectiveContent(t *testing.T) { - tmp := t.TempDir() - t.Chdir(tmp) - extra := `| flywheel-compounding | bash scripts/check-flywheel-compounding.sh | 3 | proof | - -## Three-Gap Contract Proof Surface - - -A doctrine section the GoalFile model does not represent. -` - writeGoalsMD(t, "GOALS.md", extra) - - var buf bytes.Buffer - opts := SteerAddOptions{ - Title: "New reinforcement directive", Description: "First line.\n\nSecond paragraph.", - Steer: "increase", GoalsFile: "GOALS.md", Stdout: &buf, - } - if err := RunSteerAdd(opts); err != nil { - t.Fatalf("RunSteerAdd: %v", err) - } - - data, err := os.ReadFile("GOALS.md") - if err != nil { - t.Fatalf("read: %v", err) - } - got := string(data) - - if !strings.Contains(got, "### 2. New reinforcement directive") { - t.Errorf("new directive #2 missing from:\n%s", got) - } - if !strings.Contains(got, "Second paragraph.") { - t.Errorf("multi-paragraph description not preserved") - } - if !strings.Contains(got, "### 1. Establish baseline") { - t.Errorf("original directive #1 lost") - } - for _, must := range []string{ - "## Three-Gap Contract Proof Surface", - "", - "A doctrine section the GoalFile model does not represent.", - "flywheel-compounding", - } { - if !strings.Contains(got, must) { - t.Errorf("non-directive content dropped (soc-byt52 regression): %q missing from:\n%s", must, got) - } - } -} - -func TestRunSteerRemove_NotFound(t *testing.T) { - tmp := t.TempDir() - t.Chdir(tmp) - writeGoalsMD(t, "GOALS.md", "") - - opts := SteerRemoveOptions{Number: 99, GoalsFile: "GOALS.md"} - err := RunSteerRemove(opts) - if err == nil || !strings.Contains(err.Error(), "not found") { - t.Errorf("err = %v", err) - } -} - -func TestRunSteerRemove_RemovesAndRenumbers(t *testing.T) { - tmp := t.TempDir() - t.Chdir(tmp) - writeGoalsMD(t, "GOALS.md", "") - // Add a couple extra directives - var buf bytes.Buffer - _ = RunSteerAdd(SteerAddOptions{Title: "B", Description: "desc B", Steer: "increase", GoalsFile: "GOALS.md", Stdout: &buf}) - _ = RunSteerAdd(SteerAddOptions{Title: "C", Description: "desc C", Steer: "hold", GoalsFile: "GOALS.md", Stdout: &buf}) - - buf.Reset() - if err := RunSteerRemove(SteerRemoveOptions{Number: 1, GoalsFile: "GOALS.md", Stdout: &buf}); err != nil { - t.Fatal(err) - } - if !strings.Contains(buf.String(), "Removed directive #1") { - t.Errorf("output = %q", buf.String()) - } -} - -func TestRunSteerPrioritize_InvalidPosition(t *testing.T) { - tmp := t.TempDir() - t.Chdir(tmp) - writeGoalsMD(t, "GOALS.md", "") - - err := RunSteerPrioritize(SteerPrioritizeOptions{Number: 1, NewPosition: 99, GoalsFile: "GOALS.md"}) - if err == nil { - t.Fatal("expected error") - } - if !strings.Contains(err.Error(), "between 1 and") { - t.Errorf("err = %v", err) - } -} - -func TestRunSteerPrioritize_NumberNotFound(t *testing.T) { - tmp := t.TempDir() - t.Chdir(tmp) - writeGoalsMD(t, "GOALS.md", "") - - err := RunSteerPrioritize(SteerPrioritizeOptions{Number: 5, NewPosition: 1, GoalsFile: "GOALS.md"}) - if err == nil || !strings.Contains(err.Error(), "not found") { - t.Errorf("err = %v", err) - } -} - -func TestRunSteerPrioritize_Moves(t *testing.T) { - tmp := t.TempDir() - t.Chdir(tmp) - writeGoalsMD(t, "GOALS.md", "") - - var buf bytes.Buffer - _ = RunSteerAdd(SteerAddOptions{Title: "Second", Description: "d", Steer: "increase", GoalsFile: "GOALS.md", Stdout: &buf}) - _ = RunSteerAdd(SteerAddOptions{Title: "Third", Description: "d", Steer: "hold", GoalsFile: "GOALS.md", Stdout: &buf}) - - buf.Reset() - // Move directive 3 (Third) to position 1 - err := RunSteerPrioritize(SteerPrioritizeOptions{Number: 3, NewPosition: 1, GoalsFile: "GOALS.md", Stdout: &buf}) - if err != nil { - t.Fatal(err) - } - if !strings.Contains(buf.String(), "Moved directive") { - t.Errorf("output = %q", buf.String()) - } -} - -// soc-5335b regressions: remove/prioritize must preserve non-directive content -// (Three-Gap section, claim comments) while renumbering — the old -// LoadMDGoals→WriteMDGoals round-trip dropped them. -func TestRunSteerRemove_PreservesNonDirectiveContent(t *testing.T) { - tmp := t.TempDir() - t.Chdir(tmp) - extra := "\n\n## Three-Gap Contract Proof Surface\n\nPreserved doctrine section.\n" - writeGoalsMD(t, "GOALS.md", extra) - var buf bytes.Buffer - _ = RunSteerAdd(SteerAddOptions{Title: "Second", Description: "two", Steer: "increase", GoalsFile: "GOALS.md", Stdout: &buf}) - _ = RunSteerAdd(SteerAddOptions{Title: "Third", Description: "three", Steer: "hold", GoalsFile: "GOALS.md", Stdout: &buf}) - - buf.Reset() - if err := RunSteerRemove(SteerRemoveOptions{Number: 2, GoalsFile: "GOALS.md", Stdout: &buf}); err != nil { - t.Fatalf("RunSteerRemove: %v", err) - } - data, _ := os.ReadFile("GOALS.md") - got := string(data) - - if strings.Contains(got, "### 2. Second") || strings.Contains(got, "two") { - t.Errorf("removed directive 'Second' still present:\n%s", got) - } - if !strings.Contains(got, "### 2. Third") { - t.Errorf("Third not renumbered 3 -> 2:\n%s", got) - } - if !strings.Contains(got, "### 1. Establish baseline") { - t.Errorf("directive #1 lost:\n%s", got) - } - for _, must := range []string{"## Three-Gap Contract Proof Surface", "AOP-CLAIM-KEEP", "Preserved doctrine section."} { - if !strings.Contains(got, must) { - t.Errorf("non-directive content dropped (soc-5335b): %q missing:\n%s", must, got) - } - } -} - -func TestRunSteerPrioritize_PreservesNonDirectiveContent(t *testing.T) { - tmp := t.TempDir() - t.Chdir(tmp) - extra := "\n\n## Three-Gap Contract Proof Surface\n\nPreserved doctrine section.\n" - writeGoalsMD(t, "GOALS.md", extra) - var buf bytes.Buffer - _ = RunSteerAdd(SteerAddOptions{Title: "Second", Description: "two", Steer: "increase", GoalsFile: "GOALS.md", Stdout: &buf}) - _ = RunSteerAdd(SteerAddOptions{Title: "Third", Description: "three", Steer: "hold", GoalsFile: "GOALS.md", Stdout: &buf}) - - buf.Reset() - // Move #3 (Third) to position 1 → order: Third(1), Establish baseline(2), Second(3) - if err := RunSteerPrioritize(SteerPrioritizeOptions{Number: 3, NewPosition: 1, GoalsFile: "GOALS.md", Stdout: &buf}); err != nil { - t.Fatalf("RunSteerPrioritize: %v", err) - } - data, _ := os.ReadFile("GOALS.md") - got := string(data) - - if !strings.Contains(got, "### 1. Third") { - t.Errorf("Third not moved+renumbered to #1:\n%s", got) - } - if !strings.Contains(got, "### 2. Establish baseline") { - t.Errorf("Establish baseline not renumbered to #2:\n%s", got) - } - if !strings.Contains(got, "### 3. Second") { - t.Errorf("Second not renumbered to #3:\n%s", got) - } - for _, must := range []string{"## Three-Gap Contract Proof Surface", "AOP-CLAIM-KEEP", "Preserved doctrine section."} { - if !strings.Contains(got, must) { - t.Errorf("non-directive content dropped (soc-5335b): %q missing:\n%s", must, got) - } - } -} - -// soc-3z69s regressions: steer mutations under --json must PERSIST (not just -// emit). Previously --json returned before WriteFile, so the change was lost. -func TestRunSteerAdd_JSONPersists(t *testing.T) { - tmp := t.TempDir() - t.Chdir(tmp) - writeGoalsMD(t, "GOALS.md", "") - var buf bytes.Buffer - if err := RunSteerAdd(SteerAddOptions{Title: "Persisted", Description: "d", Steer: "increase", GoalsFile: "GOALS.md", JSON: true, Stdout: &buf}); err != nil { - t.Fatal(err) - } - if !strings.Contains(buf.String(), "Persisted") { - t.Errorf("json output missing directive: %q", buf.String()) - } - data, _ := os.ReadFile("GOALS.md") - if !strings.Contains(string(data), "Persisted") { - t.Errorf("--json did not persist to GOALS.md:\n%s", string(data)) - } -} - -func TestRunSteerRemove_JSONPersists(t *testing.T) { - tmp := t.TempDir() - t.Chdir(tmp) - writeGoalsMD(t, "GOALS.md", "") - var buf bytes.Buffer - _ = RunSteerAdd(SteerAddOptions{Title: "Doomed", Description: "d", Steer: "increase", GoalsFile: "GOALS.md", Stdout: &buf}) - buf.Reset() - if err := RunSteerRemove(SteerRemoveOptions{Number: 2, GoalsFile: "GOALS.md", JSON: true, Stdout: &buf}); err != nil { - t.Fatal(err) - } - data, _ := os.ReadFile("GOALS.md") - if strings.Contains(string(data), "Doomed") { - t.Errorf("--json remove did not persist (directive still present):\n%s", string(data)) - } -} - -func TestRunSteerPrioritize_JSONPersists(t *testing.T) { - tmp := t.TempDir() - t.Chdir(tmp) - writeGoalsMD(t, "GOALS.md", "") - var buf bytes.Buffer - _ = RunSteerAdd(SteerAddOptions{Title: "Mover", Description: "d", Steer: "increase", GoalsFile: "GOALS.md", Stdout: &buf}) - buf.Reset() - if err := RunSteerPrioritize(SteerPrioritizeOptions{Number: 2, NewPosition: 1, GoalsFile: "GOALS.md", JSON: true, Stdout: &buf}); err != nil { - t.Fatal(err) - } - data, _ := os.ReadFile("GOALS.md") - if !strings.Contains(string(data), "### 1. Mover") { - t.Errorf("--json prioritize did not persist the move:\n%s", string(data)) - } -} - -func TestRunSteerAdd_DryRunJSONStillNoWrite(t *testing.T) { - tmp := t.TempDir() - t.Chdir(tmp) - writeGoalsMD(t, "GOALS.md", "") - before, _ := os.ReadFile("GOALS.md") - var buf bytes.Buffer - if err := RunSteerAdd(SteerAddOptions{Title: "Ghost", Description: "d", Steer: "hold", GoalsFile: "GOALS.md", JSON: true, DryRun: true, Stdout: &buf}); err != nil { - t.Fatal(err) - } - after, _ := os.ReadFile("GOALS.md") - if string(before) != string(after) { - t.Errorf("--dry-run --json wrote to the file; should preview only") - } -} - -func TestRunValidate_ValidFile(t *testing.T) { - tmp := t.TempDir() - t.Chdir(tmp) - writeGoalsMD(t, "GOALS.md", ` - -### build -**Weight:** 5 -**Type:** health - -`+"```bash\ntrue\n```\n") - - var buf bytes.Buffer - if err := RunValidate(ValidateOptions{GoalsFile: "GOALS.md", Stdout: &buf}); err != nil { - t.Fatalf("err = %v", err) - } - if !strings.Contains(buf.String(), "VALID") { - t.Errorf("output = %q", buf.String()) - } -} - -func TestRunValidate_MissingFile(t *testing.T) { - var buf bytes.Buffer - err := RunValidate(ValidateOptions{GoalsFile: "/nope/nonexistent.md", Stdout: &buf}) - if err == nil { - t.Fatal("expected error") - } - // Output should contain "INVALID" - if !strings.Contains(buf.String(), "INVALID") { - t.Errorf("output should contain INVALID, got %q", buf.String()) - } -} - -func TestRunPrune_NoStale(t *testing.T) { - tmp := t.TempDir() - t.Chdir(tmp) - writeGoalsMD(t, "GOALS.md", ` - -### health -**Weight:** 5 - -`+"```bash\ntrue\n```\n") - - var buf bytes.Buffer - err := RunPrune(PruneOptions{GoalsFile: "GOALS.md", DryRun: true, Stdout: &buf}) - if err != nil { - t.Fatalf("err = %v", err) - } - if !strings.Contains(buf.String(), "No stale goals") { - t.Errorf("output = %q", buf.String()) - } -} - -func TestRunPrune_DryRunDoesNotModify(t *testing.T) { - tmp := t.TempDir() - t.Chdir(tmp) - writeGoalsMD(t, "GOALS.md", ` - -### stale-gate -**Weight:** 5 - -`+"```bash\nscripts/nonexistent-script.sh\n```\n") - - before, _ := os.ReadFile("GOALS.md") - var buf bytes.Buffer - if err := RunPrune(PruneOptions{GoalsFile: "GOALS.md", DryRun: true, Stdout: &buf}); err != nil { - t.Fatalf("err = %v", err) - } - if !strings.Contains(buf.String(), "stale goal") { - t.Errorf("expected stale detected, got %q", buf.String()) - } - after, _ := os.ReadFile("GOALS.md") - if string(before) != string(after) { - t.Errorf("dry-run modified file") - } -} - -func TestRunMigrate_YAMLv1ToV2(t *testing.T) { - tmp := t.TempDir() - t.Chdir(tmp) - - // v1 YAML - yaml := `version: 1 -goals: - - id: g1 - description: desc - check: "true" - weight: 5 -` - if err := os.WriteFile("GOALS.yaml", []byte(yaml), 0o600); err != nil { - t.Fatal(err) - } - - var buf bytes.Buffer - // Note: LoadGoals emits a warning on v1 via stderr. Suppress by redirecting stderr isn't needed for test assertion. - err := RunMigrate(MigrateOptions{ToMD: false, GoalsFile: "GOALS.yaml", Stdout: &buf}) - if err != nil { - t.Fatalf("err = %v", err) - } - if _, err := os.Stat("GOALS.yaml.v1.bak"); err != nil { - t.Errorf("expected backup: %v", err) - } - if !strings.Contains(buf.String(), "Migrated") { - t.Errorf("output = %q", buf.String()) - } -} - -func TestRunMigrate_AlreadyV2(t *testing.T) { - tmp := t.TempDir() - t.Chdir(tmp) - - yaml := `version: 2 -goals: - - id: g1 - description: desc - check: "true" - weight: 5 -` - _ = os.WriteFile("GOALS.yaml", []byte(yaml), 0o600) - - var buf bytes.Buffer - err := RunMigrate(MigrateOptions{ToMD: false, GoalsFile: "GOALS.yaml", Stdout: &buf}) - if err != nil { - t.Fatal(err) - } - if !strings.Contains(buf.String(), "no migration needed") { - t.Errorf("output = %q", buf.String()) - } -} - -func TestRunInit_CreatesFile(t *testing.T) { - tmp := t.TempDir() - t.Chdir(tmp) - - var buf bytes.Buffer - err := RunInit(InitOptions{ - NonInteractive: true, - GoalsFile: "GOALS.md", - Stdout: &buf, - Stdin: strings.NewReader(""), - }) - if err != nil { - t.Fatalf("err = %v", err) - } - if _, err := os.Stat("GOALS.md"); err != nil { - t.Errorf("file not created: %v", err) - } - if !strings.Contains(buf.String(), "Created") { - t.Errorf("output = %q", buf.String()) - } -} - -func TestRunInit_FailsIfExists(t *testing.T) { - tmp := t.TempDir() - t.Chdir(tmp) - _ = os.WriteFile("GOALS.md", []byte("existing"), 0o600) - - var buf bytes.Buffer - err := RunInit(InitOptions{NonInteractive: true, GoalsFile: "GOALS.md", Stdout: &buf}) - if err == nil { - t.Fatal("expected error") - } - if !strings.Contains(err.Error(), "already exists") { - t.Errorf("err = %v", err) - } -} - -func TestRunInit_DryRun(t *testing.T) { - tmp := t.TempDir() - t.Chdir(tmp) - - var buf bytes.Buffer - err := RunInit(InitOptions{ - NonInteractive: true, DryRun: true, GoalsFile: "GOALS.md", Stdout: &buf, - }) - if err != nil { - t.Fatal(err) - } - if _, err := os.Stat("GOALS.md"); err == nil { - t.Errorf("dry-run should not create file") - } - if !strings.Contains(buf.String(), "Would write") { - t.Errorf("output = %q", buf.String()) - } -} - -func TestRunInit_WithTemplate(t *testing.T) { - tmp := t.TempDir() - t.Chdir(tmp) - - tmplBody := `name: test -gates: - - id: t1 - description: template gate - check: "true" - weight: 3 - type: health -` - fsys := fstest.MapFS{"templates/test.yaml": &fstest.MapFile{Data: []byte(tmplBody)}} - - var buf bytes.Buffer - err := RunInit(InitOptions{ - NonInteractive: true, - Template: "test", - GoalsFile: "GOALS.md", - Stdout: &buf, - TemplatesFS: readFileFSAdapter{fsys}, - }) - if err != nil { - t.Fatalf("err = %v", err) - } - data, _ := os.ReadFile("GOALS.md") - if !strings.Contains(string(data), "t1") { - t.Errorf("template gate not present: %s", string(data)) - } -} - -// readFileFSAdapter adapts an fs.FS (MapFS implements fs.ReadFileFS already, but -// this keeps the interface explicit at call sites). -type readFileFSAdapter struct{ fs.FS } - -func (a readFileFSAdapter) ReadFile(name string) ([]byte, error) { - return fs.ReadFile(a.FS, name) -} diff --git a/cli/internal/goals/goals.go b/cli/internal/goals/goals.go index b9bfe02bb..e81010262 100644 --- a/cli/internal/goals/goals.go +++ b/cli/internal/goals/goals.go @@ -176,7 +176,7 @@ func LoadGoals(path string) (*GoalFile, error) { } if gf.Version == 1 { - fmt.Fprintf(os.Stderr, "WARNING: %s uses version 1 (deprecated). Run 'ao goals migrate' to upgrade to version 2.\n", path) + fmt.Fprintf(os.Stderr, "WARNING: %s uses deprecated version 1; update the file outside AgentOps before relying on newer fields.\n", path) } gf.Format = "yaml" diff --git a/cli/internal/goals/goals_add_test.go b/cli/internal/goals/goals_add_test.go deleted file mode 100644 index 74ce56802..000000000 --- a/cli/internal/goals/goals_add_test.go +++ /dev/null @@ -1,263 +0,0 @@ -package goals_test - -import ( - "bytes" - "context" - "os" - "path/filepath" - "strings" - "testing" - "time" - - "github.com/boshu2/agentops/cli/internal/goals" -) - -func TestGoalsAdd_RejectsNonKebabID(t *testing.T) { - t.Parallel() - dir := t.TempDir() - md := `# Goals - -Mission. - -## Gates - -| ID | Check | Weight | Description | -|----|-------|--------|-------------| -| existing | ` + "`echo ok`" + ` | 5 | Existing goal | -` - goalsPath := filepath.Join(dir, "GOALS.md") - if err := os.WriteFile(goalsPath, []byte(md), 0o644); err != nil { - t.Fatal(err) - } - - tests := []struct { - name string - id string - }{ - {"uppercase", "MyGoal"}, - {"underscore", "my_goal"}, - {"spaces", "my goal"}, - {"starts with dash", "-my-goal"}, - {"ends with dash", "my-goal-"}, - {"special chars", "my@goal"}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - err := goals.RunAdd(context.Background(), goals.AddOptions{ - ID: tt.id, - Check: "echo test", - GoalsFile: goalsPath, - Weight: 5, - Timeout: 10 * time.Second, - DryRun: true, - Stdout: &bytes.Buffer{}, - }) - if err == nil { - t.Error("expected error for non-kebab-case ID") - } - if err != nil && !strings.Contains(err.Error(), "kebab-case") { - t.Errorf("error = %q, want it to mention kebab-case", err.Error()) - } - }) - } -} - -func TestGoalsAdd_RejectsDuplicateID(t *testing.T) { - t.Parallel() - dir := t.TempDir() - md := `# Goals - -Mission. - -## Gates - -| ID | Check | Weight | Description | -|----|-------|--------|-------------| -| build-ok | ` + "`echo ok`" + ` | 5 | Build passes | -` - goalsPath := filepath.Join(dir, "GOALS.md") - if err := os.WriteFile(goalsPath, []byte(md), 0o644); err != nil { - t.Fatal(err) - } - - err := goals.RunAdd(context.Background(), goals.AddOptions{ - ID: "build-ok", - Check: "echo test", - GoalsFile: goalsPath, - Weight: 5, - Timeout: 10 * time.Second, - DryRun: true, - Stdout: &bytes.Buffer{}, - }) - if err == nil { - t.Fatal("expected error for duplicate goal ID") - } - if !strings.Contains(err.Error(), "already exists") { - t.Errorf("error = %q, want 'already exists'", err.Error()) - } -} - -func TestGoalsAdd_InvalidType(t *testing.T) { - t.Parallel() - dir := t.TempDir() - md := `# Goals - -Mission. - -## Gates - -| ID | Check | Weight | Description | -|----|-------|--------|-------------| -| existing | ` + "`echo ok`" + ` | 5 | Existing | -` - goalsPath := filepath.Join(dir, "GOALS.md") - if err := os.WriteFile(goalsPath, []byte(md), 0o644); err != nil { - t.Fatal(err) - } - - err := goals.RunAdd(context.Background(), goals.AddOptions{ - ID: "new-goal", - Check: "echo ok", - Type: "bogus", - GoalsFile: goalsPath, - Weight: 5, - Timeout: 10 * time.Second, - DryRun: true, - Stdout: &bytes.Buffer{}, - }) - if err == nil { - t.Fatal("expected error for invalid goal type") - } - if !strings.Contains(err.Error(), "invalid type") { - t.Errorf("error = %q, want 'invalid type'", err.Error()) - } -} - -func TestGoalsAdd_DefaultsTypeToHealth(t *testing.T) { - t.Parallel() - dir := t.TempDir() - md := `# Goals - -Mission. - -## Gates - -| ID | Check | Weight | Description | -|----|-------|--------|-------------| -| existing | ` + "`echo ok`" + ` | 5 | Existing | -` - goalsPath := filepath.Join(dir, "GOALS.md") - if err := os.WriteFile(goalsPath, []byte(md), 0o644); err != nil { - t.Fatal(err) - } - - err := goals.RunAdd(context.Background(), goals.AddOptions{ - ID: "new-goal", - Check: "echo ok", - Type: "", - Description: "A new test goal", - GoalsFile: goalsPath, - Weight: 5, - Timeout: 10 * time.Second, - DryRun: true, - Stdout: &bytes.Buffer{}, - }) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - - // Reload and verify the goal was added with default type - gf, err := goals.LoadGoals(goalsPath) - if err != nil { - t.Fatalf("LoadGoals: %v", err) - } - - var found *goals.Goal - for i := range gf.Goals { - if gf.Goals[i].ID == "new-goal" { - found = &gf.Goals[i] - break - } - } - if found == nil { - t.Fatal("new-goal not found in goals file") - } - if found.Type != goals.GoalTypeHealth { - t.Errorf("Type = %q, want %q", found.Type, goals.GoalTypeHealth) - } -} - -func TestGoalsAdd_DescriptionFallsBackToID(t *testing.T) { - t.Parallel() - dir := t.TempDir() - md := `# Goals - -Mission. - -## Gates - -| ID | Check | Weight | Description | -|----|-------|--------|-------------| -| existing | ` + "`echo ok`" + ` | 5 | Existing | -` - goalsPath := filepath.Join(dir, "GOALS.md") - if err := os.WriteFile(goalsPath, []byte(md), 0o644); err != nil { - t.Fatal(err) - } - - err := goals.RunAdd(context.Background(), goals.AddOptions{ - ID: "fallback-id", - Check: "echo ok", - Type: "", - Description: "", - GoalsFile: goalsPath, - Weight: 3, - Timeout: 10 * time.Second, - DryRun: true, - Stdout: &bytes.Buffer{}, - }) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - - gf, err := goals.LoadGoals(goalsPath) - if err != nil { - t.Fatalf("LoadGoals: %v", err) - } - - var found *goals.Goal - for i := range gf.Goals { - if gf.Goals[i].ID == "fallback-id" { - found = &gf.Goals[i] - break - } - } - if found == nil { - t.Fatal("fallback-id not found in goals file") - } - if found.Description != "fallback-id" { - t.Errorf("Description = %q, want %q (fallback to ID)", found.Description, "fallback-id") - } -} - -func TestGoalsAdd_MissingGoalsFile(t *testing.T) { - t.Parallel() - - err := goals.RunAdd(context.Background(), goals.AddOptions{ - ID: "new-goal", - Check: "echo test", - GoalsFile: "/nonexistent/path/GOALS.md", - Weight: 5, - Timeout: 10 * time.Second, - DryRun: true, - Stdout: &bytes.Buffer{}, - }) - if err == nil { - t.Fatal("expected error for missing goals file") - } - if !strings.Contains(err.Error(), "loading goals") { - t.Errorf("error = %q, want 'loading goals'", err.Error()) - } -} diff --git a/cli/internal/goals/patcher.go b/cli/internal/goals/patcher.go index a35f0bf48..fffb13da4 100644 --- a/cli/internal/goals/patcher.go +++ b/cli/internal/goals/patcher.go @@ -9,13 +9,6 @@ import ( ) // Directive-attribute keys recognized as structured executable-spec metadata. -// -// All executable-spec directive mutations go through GoalsPatcher, never -// RenderGoalsMD / WriteMDGoals: those render a GOALS.md from the GoalFile model -// and silently drop the "## Three-Gap Contract Proof Surface" section, the -// Gates table "Tags" column, prose paragraphs, and HTML agentops:claim -// comments. The patcher edits only the target directive block and preserves -// every other byte of the file. const ( AttrDirectiveID = "Directive ID" AttrSteer = "Steer" @@ -26,9 +19,8 @@ const ( ) // directiveIDRe is the stable directive-ID format: "d-" followed by an -// alphanumeric then alphanumerics/hyphens. A stable ID is a slug of the -// directive title, never the display number, so it survives the renumbering -// done by `ao goals steer prioritize`. +// alphanumeric then alphanumerics/hyphens. A stable ID is independent of a +// directive's display number. var directiveIDRe = regexp.MustCompile(`^d-[a-z0-9][a-z0-9-]*$`) // attrLineRe matches a "**Key:** value" directive-attribute line (trimmed). @@ -50,17 +42,6 @@ var knownAttrKeys = map[string]bool{ attrTags: true, } -// attrOrder is the canonical ordering for inserting a new attribute line into a -// directive block. Lower rank sorts earlier; unknown keys sort last. -var attrOrder = map[string]int{ - AttrDirectiveID: 0, - AttrSteer: 1, - AttrSetpoint: 2, - AttrScenarios: 3, - AttrScenarioThreshold: 4, - attrTags: 5, -} - // directiveAttr is one "**Key:** value" metadata line within a directive block. type directiveAttr struct { key string @@ -90,13 +71,12 @@ type ParsedDirective struct { attrs []directiveAttr // every recognized "**Key:** value" line, in source order } -// GoalsPatcher holds GOALS.md as a line buffer and patches individual directive -// blocks without disturbing any other byte of the file. +// GoalsPatcher is a read-only parsed view over GOALS.md directive metadata. type GoalsPatcher struct { lines []string } -// NewGoalsPatcher builds a patcher over raw GOALS.md content. +// NewGoalsPatcher builds a read-only view over raw GOALS.md content. func NewGoalsPatcher(data []byte) (*GoalsPatcher, error) { if strings.TrimSpace(string(data)) == "" { return nil, fmt.Errorf("empty goals file") @@ -105,7 +85,7 @@ func NewGoalsPatcher(data []byte) (*GoalsPatcher, error) { } // LoadGoalsPatcher resolves the GOALS.md path, reads it, and returns a patcher -// plus the resolved path. +// plus the resolved path. It never writes either file. func LoadGoalsPatcher(path string) (*GoalsPatcher, string, error) { resolved := ResolveGoalsPath(path) data, err := os.ReadFile(resolved) @@ -119,17 +99,6 @@ func LoadGoalsPatcher(path string) (*GoalsPatcher, string, error) { return p, resolved, nil } -// Bytes renders the current (possibly patched) GOALS.md content. With no -// intervening patch it is byte-for-byte identical to the input. -func (p *GoalsPatcher) Bytes() []byte { - return []byte(strings.Join(p.lines, "\n")) -} - -// WriteFile writes the current content back to path with 0644 permissions. -func (p *GoalsPatcher) WriteFile(path string) error { - return os.WriteFile(path, p.Bytes(), 0o644) -} - // Directives parses every directive block in the current buffer. func (p *GoalsPatcher) Directives() []ParsedDirective { return directiveBlocks(p.lines) @@ -268,232 +237,6 @@ func splitScenarioList(s string) []string { return out } -// SetAttribute sets a "**Key:** value" attribute on the directive identified by -// display number. Only that directive's block is touched: an existing -// attribute line is replaced in place, a new one is inserted in canonical -// attribute order, and every other byte of GOALS.md is preserved. -func (p *GoalsPatcher) SetAttribute(number int, key, value string) error { - if !knownAttrKeys[key] { - return fmt.Errorf("unknown directive attribute %q", key) - } - if err := validateAttribute(key, value); err != nil { - return err - } - d, ok := p.DirectiveByNumber(number) - if !ok { - return fmt.Errorf("directive #%d not found", number) - } - newLine := fmt.Sprintf("**%s:** %s", key, value) - for _, a := range d.attrs { - if a.key == key { - p.lines[a.lineIdx] = newLine - return nil - } - } - at, prefixBlank := attrInsertion(p.lines, d, key) - if prefixBlank { - p.lines = insertLines(p.lines, at, "", newLine) - } else { - p.lines = insertLines(p.lines, at, newLine) - } - return nil -} - -// AppendDirective inserts a new directive block at the end of the Directives -// section, surgically: every other byte of the file — including non-directive -// sections such as "## Three-Gap Contract Proof Surface", the Gates table, and -// agentops:claim comments — is preserved. It returns the assigned display -// number. This replaces the lossy RenderGoalsMD round-trip that `ao goals steer -// add` used to perform (soc-byt52). -func (p *GoalsPatcher) AppendDirective(title, description, steer string) (int, error) { - title = strings.TrimSpace(title) - if title == "" { - return 0, fmt.Errorf("directive title must not be empty") - } - if strings.ContainsAny(title, "\r\n") { - return 0, fmt.Errorf("directive title must be a single line") - } - if strings.TrimSpace(description) == "" { - return 0, fmt.Errorf("directive description must not be empty") - } - steer = strings.TrimSpace(steer) - if steer == "" { - steer = "increase" - } - - dirs := p.Directives() - num := 1 - at := -1 - if len(dirs) > 0 { - for _, d := range dirs { - if d.Number >= num { - num = d.Number + 1 - } - } - at = lastContentIdx(p.lines, dirs[len(dirs)-1]) + 1 - } else { - start := directiveSectionStart(p.lines) - if start < 0 { - return 0, fmt.Errorf("no \"## Directives\" section found in GOALS.md") - } - at = start - } - - block := []string{"", fmt.Sprintf("### %d. %s", num, title), ""} - block = append(block, strings.Split(strings.TrimRight(description, "\n"), "\n")...) - block = append(block, "", fmt.Sprintf("**Steer:** %s", steer)) - p.lines = insertLines(p.lines, at, block...) - return num, nil -} - -// splitDirectiveBlocks decomposes the buffer into the lines before the first -// directive (prefix), one line slice per directive block in source order, and -// the lines after the last directive block (suffix). prefix+blocks+suffix -// reconstructs the file, so the suffix — which holds non-directive sections -// like "## Three-Gap Contract Proof Surface" and the Gates table — is carried -// byte-for-byte across remove/reorder. ok is false when there are no directives. -func (p *GoalsPatcher) splitDirectiveBlocks() (prefix []string, blocks [][]string, suffix []string, ok bool) { - dirs := p.Directives() - if len(dirs) == 0 { - return nil, nil, nil, false - } - prefix = p.lines[:dirs[0].headingIdx] - suffix = p.lines[dirs[len(dirs)-1].endIdx:] - for _, d := range dirs { - blocks = append(blocks, p.lines[d.headingIdx:d.endIdx]) - } - return prefix, blocks, suffix, true -} - -// assembleDirectives reassembles the buffer from prefix + directive blocks + -// suffix, renumbering each block's "### N. Title" heading to its 1-based -// position. Block bodies, attributes, and blank lines are copied verbatim; -// only the heading number changes. -func assembleDirectives(prefix []string, blocks [][]string, suffix []string) []string { - out := make([]string, 0, len(prefix)+len(suffix)+len(blocks)*8) - out = append(out, prefix...) - for i, b := range blocks { - nb := make([]string, len(b)) - copy(nb, b) - if len(nb) > 0 { - if m := directiveHeadingRe.FindStringSubmatch(strings.TrimSpace(nb[0])); m != nil { - nb[0] = fmt.Sprintf("### %d. %s", i+1, m[2]) - } - } - out = append(out, nb...) - } - return append(out, suffix...) -} - -// directiveIndexByNumber returns the source-order index of the directive with -// the given display number, or -1. -func directiveIndexByNumber(dirs []ParsedDirective, number int) int { - for i, d := range dirs { - if d.Number == number { - return i - } - } - return -1 -} - -// RemoveDirective deletes the directive with the given display number and -// renumbers the remaining directives sequentially, preserving every -// non-directive byte of the file (soc-5335b). Replaces the lossy -// LoadMDGoals→WriteMDGoals round-trip. -func (p *GoalsPatcher) RemoveDirective(number int) error { - prefix, blocks, suffix, ok := p.splitDirectiveBlocks() - if !ok { - return fmt.Errorf("no directives to remove") - } - idx := directiveIndexByNumber(p.Directives(), number) - if idx < 0 { - return fmt.Errorf("directive #%d not found", number) - } - kept := make([][]string, 0, len(blocks)-1) - kept = append(kept, blocks[:idx]...) - kept = append(kept, blocks[idx+1:]...) - p.lines = assembleDirectives(prefix, kept, suffix) - return nil -} - -// MoveDirective moves the directive with the given display number to newPos -// (1-based) and renumbers all directives sequentially, preserving every -// non-directive byte of the file (soc-5335b). -func (p *GoalsPatcher) MoveDirective(number, newPos int) error { - prefix, blocks, suffix, ok := p.splitDirectiveBlocks() - if !ok { - return fmt.Errorf("no directives to prioritize") - } - if newPos < 1 || newPos > len(blocks) { - return fmt.Errorf("new position must be between 1 and %d", len(blocks)) - } - idx := directiveIndexByNumber(p.Directives(), number) - if idx < 0 { - return fmt.Errorf("directive #%d not found", number) - } - moving := blocks[idx] - rest := make([][]string, 0, len(blocks)-1) - rest = append(rest, blocks[:idx]...) - rest = append(rest, blocks[idx+1:]...) - insertIdx := newPos - 1 - reordered := make([][]string, 0, len(blocks)) - reordered = append(reordered, rest[:insertIdx]...) - reordered = append(reordered, moving) - reordered = append(reordered, rest[insertIdx:]...) - p.lines = assembleDirectives(prefix, reordered, suffix) - return nil -} - -// attrRank returns the canonical sort rank for an attribute key. -func attrRank(key string) int { - if r, ok := attrOrder[key]; ok { - return r - } - return 99 -} - -// attrInsertion returns the line index at which a new attribute of the given -// key should be inserted, and whether a blank separator line is needed before -// it (true only when the block has no existing attribute lines). -func attrInsertion(lines []string, d ParsedDirective, key string) (int, bool) { - if len(d.attrs) == 0 { - return lastContentIdx(lines, d) + 1, true - } - rank := attrRank(key) - for _, a := range d.attrs { - if attrRank(a.key) > rank { - return a.lineIdx, false - } - } - return d.attrs[len(d.attrs)-1].lineIdx + 1, false -} - -// lastContentIdx returns the index of the last non-blank line inside the block, -// or the heading index when the block has no body. -func lastContentIdx(lines []string, d ParsedDirective) int { - for i := d.endIdx - 1; i > d.headingIdx; i-- { - if strings.TrimSpace(lines[i]) != "" { - return i - } - } - return d.headingIdx -} - -// insertLines returns a new slice with newLines spliced in before index at. -func insertLines(lines []string, at int, newLines ...string) []string { - if at < 0 { - at = 0 - } - if at > len(lines) { - at = len(lines) - } - out := make([]string, 0, len(lines)+len(newLines)) - out = append(out, lines[:at]...) - out = append(out, newLines...) - out = append(out, lines[at:]...) - return out -} - // validateAttribute checks that an attribute value is well-formed. func validateAttribute(key, value string) error { if strings.ContainsAny(value, "\r\n") { @@ -542,10 +285,8 @@ func (p *GoalsPatcher) Validate() []error { return errs } -// SlugifyDirectiveID derives a deterministic stable directive ID from a title. -// The result always matches the stable-ID format (d-...). Because it is -// a function of the title alone, the ID is independent of the directive's -// display number and survives `ao goals steer prioritize` renumbering. +// SlugifyDirectiveID derives a deterministic display tag from a directive +// title when an older GOALS.md file has no explicit stable ID. func SlugifyDirectiveID(title string) string { slug := nonSlugRe.ReplaceAllString(strings.ToLower(title), "-") slug = strings.Trim(slug, "-") @@ -554,43 +295,3 @@ func SlugifyDirectiveID(title string) string { } return "d-" + slug } - -// uniqueID returns base if unused, else base with the lowest free "-N" suffix. -func uniqueID(base string, used map[string]bool) string { - if !used[base] { - return base - } - for n := 2; ; n++ { - candidate := fmt.Sprintf("%s-%d", base, n) - if !used[candidate] { - return candidate - } - } -} - -// EnsureStableIDs assigns a "**Directive ID:**" attribute to every directive -// that lacks one, deriving a deterministic slug from the title with a numeric -// collision suffix when needed. Directives that already declare an ID keep it. -// Returns the stable ID of every directive keyed by display number. -func (p *GoalsPatcher) EnsureStableIDs() (map[int]string, error) { - used := map[string]bool{} - for _, d := range p.Directives() { - if d.StableID != "" { - used[d.StableID] = true - } - } - result := map[int]string{} - for _, d := range p.Directives() { - if d.StableID != "" { - result[d.Number] = d.StableID - continue - } - id := uniqueID(SlugifyDirectiveID(d.Title), used) - used[id] = true - if err := p.SetAttribute(d.Number, AttrDirectiveID, id); err != nil { - return nil, err - } - result[d.Number] = id - } - return result, nil -} diff --git a/cli/internal/goals/patcher_blockops_test.go b/cli/internal/goals/patcher_blockops_test.go deleted file mode 100644 index 814ee3c0c..000000000 --- a/cli/internal/goals/patcher_blockops_test.go +++ /dev/null @@ -1,165 +0,0 @@ -package goals - -import ( - "strconv" - "strings" - "testing" -) - -// blockOpsGoals builds a GOALS.md with the given directive titles (numbered -// 1..N) followed by a non-directive "## Gates" section + a claim comment, so -// edge-case tests can assert both directive behavior and byte-preservation of -// trailing content. -func blockOpsGoals(titles ...string) string { - var b strings.Builder - b.WriteString("# Fitness Goals\n\n## Directives\n\n") - for i, t := range titles { - b.WriteString("### ") - b.WriteString(strconv.Itoa(i + 1)) - b.WriteString(". ") - b.WriteString(t) - b.WriteString("\n\nBody of ") - b.WriteString(t) - b.WriteString(".\n\n**Steer:** increase\n\n") - } - b.WriteString("## Gates\n\n\n| ID | Check |\n|----|-------|\n| g1 | x.sh |\n") - return b.String() -} - -func patch(t *testing.T, content string) *GoalsPatcher { - t.Helper() - p, err := NewGoalsPatcher([]byte(content)) - if err != nil { - t.Fatalf("NewGoalsPatcher: %v", err) - } - return p -} - -func TestAppendDirective_IntoZeroDirectiveSection(t *testing.T) { - p := patch(t, blockOpsGoals()) // "## Directives" present, no directives - num, err := p.AppendDirective("First", "Body.", "increase") - if err != nil { - t.Fatalf("AppendDirective: %v", err) - } - if num != 1 { - t.Errorf("num = %d, want 1", num) - } - got := string(p.Bytes()) - if !strings.Contains(got, "### 1. First") { - t.Errorf("new directive missing:\n%s", got) - } - for _, must := range []string{"## Gates", "AOP-CLAIM-EDGE", "| g1 | x.sh |"} { - if !strings.Contains(got, must) { - t.Errorf("non-directive content dropped: %q\n%s", must, got) - } - } -} - -func TestAppendDirective_RenumbersFromMaxNotCount(t *testing.T) { - // Non-contiguous numbering: a hand-edited GOALS with directives 1, 2, 5. - src := "# G\n\n## Directives\n\n### 1. A\n\n**Steer:** increase\n\n### 2. B\n\n**Steer:** increase\n\n### 5. C\n\n**Steer:** increase\n" - p := patch(t, src) - num, err := p.AppendDirective("D", "body", "hold") - if err != nil { - t.Fatalf("AppendDirective: %v", err) - } - if num != 6 { // max(1,2,5)+1, not count+1 - t.Errorf("num = %d, want 6 (max+1)", num) - } - if !strings.Contains(string(p.Bytes()), "### 6. D") { - t.Errorf("appended at wrong number:\n%s", string(p.Bytes())) - } -} - -func TestRemoveDirective_OnlyDirective(t *testing.T) { - p := patch(t, blockOpsGoals("Solo")) - if err := p.RemoveDirective(1); err != nil { - t.Fatalf("RemoveDirective: %v", err) - } - got := string(p.Bytes()) - if strings.Contains(got, "Solo") { - t.Errorf("directive not removed:\n%s", got) - } - if !strings.Contains(got, "## Gates") || !strings.Contains(got, "AOP-CLAIM-EDGE") { - t.Errorf("trailing section dropped after removing only directive:\n%s", got) - } -} - -func TestRemoveDirective_Last(t *testing.T) { - p := patch(t, blockOpsGoals("A", "B", "C")) - if err := p.RemoveDirective(3); err != nil { - t.Fatalf("RemoveDirective: %v", err) - } - got := string(p.Bytes()) - if strings.Contains(got, "### 3. C") || strings.Contains(got, "Body of C") { - t.Errorf("last directive not removed:\n%s", got) - } - if !strings.Contains(got, "### 1. A") || !strings.Contains(got, "### 2. B") { - t.Errorf("survivors lost/misnumbered:\n%s", got) - } -} - -func TestRemoveDirective_MiddleRenumbers(t *testing.T) { - p := patch(t, blockOpsGoals("A", "B", "C")) - if err := p.RemoveDirective(2); err != nil { - t.Fatalf("RemoveDirective: %v", err) - } - got := string(p.Bytes()) - if !strings.Contains(got, "### 1. A") || !strings.Contains(got, "### 2. C") { - t.Errorf("C not renumbered 3->2 after removing B:\n%s", got) - } - if strings.Contains(got, "### 3.") { - t.Errorf("stale #3 heading remains:\n%s", got) - } -} - -func TestRemoveDirective_NotFound(t *testing.T) { - p := patch(t, blockOpsGoals("A")) - if err := p.RemoveDirective(9); err == nil { - t.Error("expected error removing nonexistent directive") - } -} - -func TestMoveDirective_SamePositionIsIdempotent(t *testing.T) { - src := blockOpsGoals("A", "B", "C") - p := patch(t, src) - if err := p.MoveDirective(2, 2); err != nil { - t.Fatalf("MoveDirective: %v", err) - } - got := string(p.Bytes()) - for _, want := range []string{"### 1. A", "### 2. B", "### 3. C"} { - if !strings.Contains(got, want) { - t.Errorf("same-position move changed order: %q missing\n%s", want, got) - } - } -} - -func TestMoveDirective_ToLast(t *testing.T) { - p := patch(t, blockOpsGoals("A", "B", "C")) - if err := p.MoveDirective(1, 3); err != nil { // move A to the end - t.Fatalf("MoveDirective: %v", err) - } - got := string(p.Bytes()) - for _, want := range []string{"### 1. B", "### 2. C", "### 3. A"} { - if !strings.Contains(got, want) { - t.Errorf("move-to-last produced wrong order: %q missing\n%s", want, got) - } - } -} - -func TestMoveDirective_OutOfRange(t *testing.T) { - p := patch(t, blockOpsGoals("A", "B")) - if err := p.MoveDirective(1, 9); err == nil { - t.Error("expected error for out-of-range position") - } -} - -func TestAppendDirective_RejectsEmpty(t *testing.T) { - p := patch(t, blockOpsGoals("A")) - if _, err := p.AppendDirective("", "body", "increase"); err == nil { - t.Error("expected error for empty title") - } - if _, err := p.AppendDirective("T", "", "increase"); err == nil { - t.Error("expected error for empty description") - } -} diff --git a/cli/internal/goals/patcher_test.go b/cli/internal/goals/patcher_test.go deleted file mode 100644 index 317b20dbd..000000000 --- a/cli/internal/goals/patcher_test.go +++ /dev/null @@ -1,415 +0,0 @@ -package goals - -import ( - "os" - "path/filepath" - "strings" - "testing" -) - -// fixturePath is the executable-spec patcher fixture. -const fixturePath = "testdata/goals-spec-fixture.md" - -func readFixture(t *testing.T) []byte { - t.Helper() - data, err := os.ReadFile(fixturePath) - if err != nil { - t.Fatalf("reading fixture: %v", err) - } - return data -} - -// sectionFrom returns content from the first occurrence of marker to the end. -func sectionFrom(content, marker string) string { - idx := strings.Index(content, marker) - if idx < 0 { - return "" - } - return content[idx:] -} - -func TestNewGoalsPatcher_RejectsEmpty(t *testing.T) { - if _, err := NewGoalsPatcher([]byte(" \n\t\n")); err == nil { - t.Fatal("expected error for empty goals file, got nil") - } -} - -func TestGoalsPatcher_RoundTripByteStable(t *testing.T) { - inputs := map[string][]byte{"fixture": readFixture(t)} - if live, err := os.ReadFile(filepath.Join("..", "..", "..", "GOALS.md")); err == nil { - inputs["live-GOALS.md"] = live - } - for name, data := range inputs { - t.Run(name, func(t *testing.T) { - p, err := NewGoalsPatcher(data) - if err != nil { - t.Fatalf("NewGoalsPatcher: %v", err) - } - if got := p.Bytes(); string(got) != string(data) { - t.Errorf("round trip not byte-stable: %d input bytes, %d output bytes", len(data), len(got)) - } - }) - } -} - -func TestParseDirectiveBlocks_Fields(t *testing.T) { - dirs, err := ParseDirectiveBlocks(readFixture(t)) - if err != nil { - t.Fatalf("ParseDirectiveBlocks: %v", err) - } - if len(dirs) != 3 { - t.Fatalf("directive count = %d, want 3", len(dirs)) - } - - d2 := dirs[1] - if d2.Number != 2 { - t.Errorf("d2.Number = %d, want 2", d2.Number) - } - if d2.Title != "Carry structured attribute metadata" { - t.Errorf("d2.Title = %q", d2.Title) - } - if d2.StableID != "d-existing-two" { - t.Errorf("d2.StableID = %q, want d-existing-two", d2.StableID) - } - if d2.Steer != "decrease (lossy writes)" { - t.Errorf("d2.Steer = %q", d2.Steer) - } - if d2.Setpoint != "AOP-CLAIM-FIXTURE | exact wording | GOALS.md" { - t.Errorf("d2.Setpoint = %q", d2.Setpoint) - } - if got := strings.Join(d2.Scenarios, ","); got != "s-2026-05-17-001,s-2026-05-17-002" { - t.Errorf("d2.Scenarios = %q", got) - } - if d2.ScenarioThreshold != "0.8" { - t.Errorf("d2.ScenarioThreshold = %q, want 0.8", d2.ScenarioThreshold) - } - if d2.StartLine != 23 || d2.EndLine != 33 { - t.Errorf("d2 line range = [%d,%d], want [23,33]", d2.StartLine, d2.EndLine) - } -} - -func TestParseDirectiveBlocks_LineRanges(t *testing.T) { - dirs, err := ParseDirectiveBlocks(readFixture(t)) - if err != nil { - t.Fatalf("ParseDirectiveBlocks: %v", err) - } - want := []struct{ start, end int }{{16, 22}, {23, 33}, {34, 38}} - for i, w := range want { - if dirs[i].StartLine != w.start || dirs[i].EndLine != w.end { - t.Errorf("directive %d range = [%d,%d], want [%d,%d]", - i+1, dirs[i].StartLine, dirs[i].EndLine, w.start, w.end) - } - } - // Directive 3 carries no attribute metadata. - if len(dirs[2].attrs) != 0 { - t.Errorf("directive 3 attrs = %d, want 0", len(dirs[2].attrs)) - } -} - -func TestGoalsPatcher_SetAttributeReplaceInPlace(t *testing.T) { - data := readFixture(t) - p, err := NewGoalsPatcher(data) - if err != nil { - t.Fatalf("NewGoalsPatcher: %v", err) - } - if err := p.SetAttribute(2, AttrSteer, "hold (test)"); err != nil { - t.Fatalf("SetAttribute: %v", err) - } - // Line count is unchanged for an in-place replace. - if got, want := strings.Count(string(p.Bytes()), "\n"), strings.Count(string(data), "\n"); got != want { - t.Errorf("newline count = %d, want %d (replace must not add lines)", got, want) - } - d2, ok := p.DirectiveByNumber(2) - if !ok { - t.Fatal("directive 2 missing after patch") - } - if d2.Steer != "hold (test)" { - t.Errorf("d2.Steer = %q, want hold (test)", d2.Steer) - } - // Untouched directives keep their bytes. - if d1, _ := p.DirectiveByNumber(1); d1.Steer != "increase (preserved bytes)" { - t.Errorf("d1.Steer changed to %q", d1.Steer) - } -} - -func TestGoalsPatcher_SetAttributePreservesNonTargetSections(t *testing.T) { - data := readFixture(t) - origTail := sectionFrom(string(data), "## Three-Gap Contract Proof Surface") - origComment := strings.Contains(string(data), "") - if origTail == "" || !origComment { - t.Fatal("fixture missing expected non-target content") - } - - cases := []struct { - name string - edit func(*GoalsPatcher) error - }{ - {"replace-in-place", func(p *GoalsPatcher) error { return p.SetAttribute(2, AttrSteer, "hold (x)") }}, - {"insert-new-attr", func(p *GoalsPatcher) error { return p.SetAttribute(1, AttrScenarios, "s-2026-05-17-009") }}, - {"insert-into-bare-block", func(p *GoalsPatcher) error { return p.SetAttribute(3, AttrDirectiveID, "d-bare-three") }}, - } - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - p, err := NewGoalsPatcher(data) - if err != nil { - t.Fatalf("NewGoalsPatcher: %v", err) - } - if err := tc.edit(p); err != nil { - t.Fatalf("edit: %v", err) - } - out := string(p.Bytes()) - if got := sectionFrom(out, "## Three-Gap Contract Proof Surface"); got != origTail { - t.Errorf("Three-Gap section + Gates table not preserved byte-for-byte") - } - if !strings.Contains(out, "") { - t.Error("HTML claim comment dropped by patch") - } - }) - } -} - -func TestGoalsPatcher_SetAttributeInsertsInCanonicalOrder(t *testing.T) { - data := readFixture(t) - - // Scenarios (rank 3) on directive 1 lands after its Steer line (rank 1). - t.Run("scenarios-after-steer", func(t *testing.T) { - p, _ := NewGoalsPatcher(data) - if err := p.SetAttribute(1, AttrScenarios, "s-2026-05-17-009"); err != nil { - t.Fatalf("SetAttribute: %v", err) - } - lines := strings.Split(string(p.Bytes()), "\n") - steerIdx, scenIdx := -1, -1 - for i, l := range lines[:25] { - if strings.HasPrefix(l, "**Steer:** increase") { - steerIdx = i - } - if strings.HasPrefix(l, "**Scenarios:** s-2026-05-17-009") { - scenIdx = i - } - } - if steerIdx < 0 || scenIdx < 0 || scenIdx != steerIdx+1 { - t.Errorf("Scenarios line idx %d, Steer idx %d — Scenarios must immediately follow Steer", scenIdx, steerIdx) - } - d1, _ := p.DirectiveByNumber(1) - if got := strings.Join(d1.Scenarios, ","); got != "s-2026-05-17-009" { - t.Errorf("re-parsed d1.Scenarios = %q", got) - } - }) - - // Directive ID (rank 0) on directive 1 lands before its Steer line. - t.Run("directive-id-before-steer", func(t *testing.T) { - p, _ := NewGoalsPatcher(data) - if err := p.SetAttribute(1, AttrDirectiveID, "d-first-one"); err != nil { - t.Fatalf("SetAttribute: %v", err) - } - lines := strings.Split(string(p.Bytes()), "\n") - idIdx, steerIdx := -1, -1 - for i, l := range lines[:25] { - if strings.HasPrefix(l, "**Directive ID:** d-first-one") { - idIdx = i - } - if strings.HasPrefix(l, "**Steer:** increase") { - steerIdx = i - } - } - if idIdx < 0 || steerIdx < 0 || idIdx != steerIdx-1 { - t.Errorf("Directive ID idx %d, Steer idx %d — ID must immediately precede Steer", idIdx, steerIdx) - } - }) -} - -func TestGoalsPatcher_SetAttributeIntoBareBlock(t *testing.T) { - data := readFixture(t) - p, err := NewGoalsPatcher(data) - if err != nil { - t.Fatalf("NewGoalsPatcher: %v", err) - } - if err := p.SetAttribute(3, AttrDirectiveID, "d-bare-three"); err != nil { - t.Fatalf("SetAttribute: %v", err) - } - d3, ok := p.DirectiveByNumber(3) - if !ok || d3.StableID != "d-bare-three" { - t.Fatalf("directive 3 stable ID = %q, want d-bare-three", d3.StableID) - } - // The inserted attribute is separated from body text by a blank line. - out := string(p.Bytes()) - if !strings.Contains(out, "bare block.\n\n**Directive ID:** d-bare-three") { - t.Error("attribute inserted into bare block without a blank separator line") - } - if errs := p.Validate(); len(errs) != 0 { - t.Errorf("Validate after bare-block insert: %v", errs) - } -} - -func TestGoalsPatcher_EnsureStableIDs(t *testing.T) { - data := readFixture(t) - p, err := NewGoalsPatcher(data) - if err != nil { - t.Fatalf("NewGoalsPatcher: %v", err) - } - ids, err := p.EnsureStableIDs() - if err != nil { - t.Fatalf("EnsureStableIDs: %v", err) - } - want := map[int]string{ - 1: "d-keep-the-patcher-non-lossy", - 2: "d-existing-two", // pre-existing ID is preserved, not regenerated - 3: "d-survive-directives-that-have-no-attributes", - } - for num, wantID := range want { - if ids[num] != wantID { - t.Errorf("directive %d ID = %q, want %q", num, ids[num], wantID) - } - } - - // Idempotent: a second pass changes no bytes. - afterFirst := string(p.Bytes()) - if _, err := p.EnsureStableIDs(); err != nil { - t.Fatalf("second EnsureStableIDs: %v", err) - } - if string(p.Bytes()) != afterFirst { - t.Error("EnsureStableIDs is not idempotent — second pass mutated the file") - } - if errs := p.Validate(); len(errs) != 0 { - t.Errorf("Validate after EnsureStableIDs: %v", errs) - } -} - -func TestGoalsPatcher_StableIDsAreNumberIndependent(t *testing.T) { - // Two GOALS files with the same directive titles in different display-number - // order must assign each title the same stable ID — the ID is a function of - // the title, so `ao goals steer prioritize` renumbering cannot change it. - const orderA = "# Goals\n\nm\n\n## Directives\n\n### 1. Alpha directive\n\nbody\n\n### 2. Beta directive\n\nbody\n" - const orderB = "# Goals\n\nm\n\n## Directives\n\n### 1. Beta directive\n\nbody\n\n### 2. Alpha directive\n\nbody\n" - - idsByTitle := func(src string) map[string]string { - p, err := NewGoalsPatcher([]byte(src)) - if err != nil { - t.Fatalf("NewGoalsPatcher: %v", err) - } - ids, err := p.EnsureStableIDs() - if err != nil { - t.Fatalf("EnsureStableIDs: %v", err) - } - out := map[string]string{} - for _, d := range p.Directives() { - out[d.Title] = ids[d.Number] - } - return out - } - - a, b := idsByTitle(orderA), idsByTitle(orderB) - for _, title := range []string{"Alpha directive", "Beta directive"} { - if a[title] != b[title] { - t.Errorf("%q ID differs by ordering: %q vs %q", title, a[title], b[title]) - } - if want := SlugifyDirectiveID(title); a[title] != want { - t.Errorf("%q ID = %q, want title slug %q", title, a[title], want) - } - } -} - -func TestGoalsPatcher_EnsureStableIDsCollisionSuffix(t *testing.T) { - const src = "# Goals\n\nm\n\n## Directives\n\n### 1. Same title\n\nbody\n\n### 2. Same title\n\nbody\n\n### 3. Same title\n\nbody\n" - p, err := NewGoalsPatcher([]byte(src)) - if err != nil { - t.Fatalf("NewGoalsPatcher: %v", err) - } - ids, err := p.EnsureStableIDs() - if err != nil { - t.Fatalf("EnsureStableIDs: %v", err) - } - want := map[int]string{1: "d-same-title", 2: "d-same-title-2", 3: "d-same-title-3"} - for num, wantID := range want { - if ids[num] != wantID { - t.Errorf("directive %d ID = %q, want %q", num, ids[num], wantID) - } - } -} - -func TestSlugifyDirectiveID(t *testing.T) { - cases := []struct{ in, want string }{ - {"Keep the patcher non-lossy", "d-keep-the-patcher-non-lossy"}, - {" Spaces & Symbols!! ", "d-spaces-symbols"}, - {"", "d-directive"}, - {"!!!", "d-directive"}, - {"3D Rendering", "d-3d-rendering"}, - {"already-kebab", "d-already-kebab"}, - } - for _, tc := range cases { - got := SlugifyDirectiveID(tc.in) - if got != tc.want { - t.Errorf("SlugifyDirectiveID(%q) = %q, want %q", tc.in, got, tc.want) - } - if !directiveIDRe.MatchString(got) { - t.Errorf("SlugifyDirectiveID(%q) = %q does not match stable-ID format", tc.in, got) - } - } -} - -func TestGoalsPatcher_ValidateLineNumberedErrors(t *testing.T) { - const src = "# Goals\n\nm\n\n## Directives\n\n" + - "### 1. Bad metadata\n\nbody\n\n" + - "**Directive ID:** Bad ID\n" + - "**Scenario threshold:** 1.5\n\n" + - "### 2. Duplicate id\n\nbody\n\n" + - "**Directive ID:** d-dupe\n\n" + - "### 3. Also dupe\n\nbody\n\n" + - "**Directive ID:** d-dupe\n" - p, err := NewGoalsPatcher([]byte(src)) - if err != nil { - t.Fatalf("NewGoalsPatcher: %v", err) - } - errs := p.Validate() - joined := "" - for _, e := range errs { - joined += e.Error() + "\n" - } - for _, want := range []string{ - "GOALS.md:11: invalid Directive ID", - "GOALS.md:12: invalid Scenario threshold", - "GOALS.md:24: duplicate Directive ID", - } { - if !strings.Contains(joined, want) { - t.Errorf("Validate() missing %q\ngot:\n%s", want, joined) - } - } -} - -func TestGoalsPatcher_SetAttributeRejectsMalformed(t *testing.T) { - cases := []struct { - name, key, value, wantSubstr string - }{ - {"bad-directive-id", AttrDirectiveID, "Bad ID", "invalid Directive ID"}, - {"bad-threshold", AttrScenarioThreshold, "2.0", "invalid Scenario threshold"}, - {"empty-steer", AttrSteer, "", "must not be empty"}, - {"unknown-key", "Bogus", "x", "unknown directive attribute"}, - {"multiline-value", AttrSetpoint, "line1\nline2", "single line"}, - } - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - p, err := NewGoalsPatcher(readFixture(t)) - if err != nil { - t.Fatalf("NewGoalsPatcher: %v", err) - } - err = p.SetAttribute(1, tc.key, tc.value) - if err == nil { - t.Fatalf("SetAttribute(%q, %q) succeeded, want error", tc.key, tc.value) - } - if !strings.Contains(err.Error(), tc.wantSubstr) { - t.Errorf("error = %q, want substring %q", err.Error(), tc.wantSubstr) - } - }) - } -} - -func TestGoalsPatcher_SetAttributeUnknownDirective(t *testing.T) { - p, err := NewGoalsPatcher(readFixture(t)) - if err != nil { - t.Fatalf("NewGoalsPatcher: %v", err) - } - err = p.SetAttribute(99, AttrSteer, "increase (x)") - if err == nil || !strings.Contains(err.Error(), "directive #99 not found") { - t.Errorf("SetAttribute on missing directive: err = %v", err) - } -} diff --git a/cli/internal/goals/scenario_create.go b/cli/internal/goals/scenario_create.go deleted file mode 100644 index b9c79e926..000000000 --- a/cli/internal/goals/scenario_create.go +++ /dev/null @@ -1,148 +0,0 @@ -package goals - -import ( - "encoding/json" - "fmt" - "io" - "os" - "path/filepath" - "strings" - "time" - - "github.com/boshu2/agentops/cli/internal/scenario" -) - -// ScenarioCreateOptions configures RunScenarioCreate. -type ScenarioCreateOptions struct { - GoalsFile string - DirectiveNum int - Goal string - Threshold float64 - Status string - Source string - // SpecDir is the directory the promoted spec scenario is written to. - // Empty defaults to spec/scenarios (docs/adr/ADR-0003). - SpecDir string - Now func() time.Time - JSON bool - Stdout io.Writer -} - -// ScenarioCreateResult is the machine-readable outcome of --create. -type ScenarioCreateResult struct { - ScenarioID string `json:"scenario_id"` - ScenarioPath string `json:"scenario_path"` - DirectiveID string `json:"directive_id"` - DirectiveNum int `json:"directive_number"` - Linked bool `json:"linked"` -} - -// RunScenarioCreate scaffolds a promoted spec scenario and links it -// bidirectionally to a GOALS.md directive: the scenario JSON carries the -// directive's stable ID, and the directive's "**Scenarios:**" line gains the -// scenario ID via the non-lossy patcher. -// -// Failure ordering preserves the invariant in soc-58nt.1.3: if scenario -// creation fails, GOALS.md is never written; if the GOALS.md write fails, the -// created scenario path is reported and the link is marked incomplete. -func RunScenarioCreate(opts ScenarioCreateOptions) error { - if opts.Stdout == nil { - opts.Stdout = os.Stdout - } - specDir := opts.SpecDir - if specDir == "" { - specDir = filepath.Join("spec", "scenarios") - } - - patcher, goalsPath, err := LoadGoalsPatcher(opts.GoalsFile) - if err != nil { - return fmt.Errorf("loading goals: %w", err) - } - directive, ok := patcher.DirectiveByNumber(opts.DirectiveNum) - if !ok { - return fmt.Errorf("directive #%d not found (run 'ao goals scenarios' to list directives)", opts.DirectiveNum) - } - - // Ensure the target directive has a stable ID. The patcher edit is - // in-memory only; GOALS.md is not written until the link step, so a - // scenario-creation failure below leaves GOALS.md on disk untouched. - stableID := directive.StableID - if stableID == "" { - stableID = uniqueID(SlugifyDirectiveID(directive.Title), patcherStableIDs(patcher)) - if err := patcher.SetAttribute(opts.DirectiveNum, AttrDirectiveID, stableID); err != nil { - return fmt.Errorf("assigning directive ID: %w", err) - } - } - - res, err := scenario.Create(scenario.CreateOptions{ - Goal: opts.Goal, - Threshold: opts.Threshold, - Status: opts.Status, - Source: opts.Source, - DirectiveID: stableID, - Dir: specDir, - Now: opts.Now, - }) - if err != nil { - return fmt.Errorf("creating scenario: %w", err) - } - - current, _ := patcher.DirectiveByNumber(opts.DirectiveNum) - linkErr := linkScenarioAndWrite(patcher, goalsPath, opts.DirectiveNum, current.Scenarios, res.Scenario.ID) - result := ScenarioCreateResult{ - ScenarioID: res.Scenario.ID, - ScenarioPath: res.Path, - DirectiveID: stableID, - DirectiveNum: opts.DirectiveNum, - Linked: linkErr == nil, - } - writeScenarioCreateResult(opts, result) - if linkErr != nil { - return fmt.Errorf("scenario created at %s but GOALS.md was not linked: %w; repair: re-run 'ao goals scenarios --create' or add %s to directive #%d's Scenarios line by hand", - res.Path, linkErr, res.Scenario.ID, opts.DirectiveNum) - } - return nil -} - -// patcherStableIDs collects every stable Directive ID already in the file. -func patcherStableIDs(p *GoalsPatcher) map[string]bool { - used := map[string]bool{} - for _, d := range p.Directives() { - if d.StableID != "" { - used[d.StableID] = true - } - } - return used -} - -// linkScenarioAndWrite appends scenarioID to the directive's Scenarios line -// (if not already present) and writes GOALS.md back through the patcher. -func linkScenarioAndWrite(p *GoalsPatcher, goalsPath string, directiveNum int, existing []string, scenarioID string) error { - for _, s := range existing { - if s == scenarioID { - return p.WriteFile(goalsPath) // already linked; still flush the stable-ID edit - } - } - linked := append(append([]string{}, existing...), scenarioID) - if err := p.SetAttribute(directiveNum, AttrScenarios, strings.Join(linked, ", ")); err != nil { - return err - } - return p.WriteFile(goalsPath) -} - -// writeScenarioCreateResult renders the create outcome to stdout. -func writeScenarioCreateResult(opts ScenarioCreateOptions, result ScenarioCreateResult) { - if opts.JSON { - enc := json.NewEncoder(opts.Stdout) - enc.SetIndent("", " ") - _ = enc.Encode(result) - return - } - if result.Linked { - fmt.Fprintf(opts.Stdout, "Created scenario %s at %s and linked it to directive #%d [%s]\n", - result.ScenarioID, result.ScenarioPath, result.DirectiveNum, result.DirectiveID) - return - } - fmt.Fprintf(opts.Stdout, "Created scenario %s at %s (NOT linked — directive #%d unchanged)\n", - result.ScenarioID, result.ScenarioPath, result.DirectiveNum) -} diff --git a/cli/internal/goals/scenario_create_test.go b/cli/internal/goals/scenario_create_test.go deleted file mode 100644 index cc96800b5..000000000 --- a/cli/internal/goals/scenario_create_test.go +++ /dev/null @@ -1,123 +0,0 @@ -package goals - -import ( - "bytes" - "encoding/json" - "os" - "path/filepath" - "strings" - "testing" - "time" -) - -func fixedScenarioClock() func() time.Time { - return func() time.Time { return time.Date(2026, 5, 17, 9, 0, 0, 0, time.UTC) } -} - -func TestRunScenarioCreate_BidirectionalLink(t *testing.T) { - tmp := t.TempDir() - goalsPath := filepath.Join(tmp, "GOALS.md") - specDir := filepath.Join(tmp, "spec", "scenarios") - src := "# Goals\n\nm\n\n## Directives\n\n" + - "### 1. First directive\n\nbody\n\n**Steer:** increase (x)\n\n" + - "### 2. Target directive\n\nbody\n\n**Steer:** decrease (y)\n\n" + - "## Three-Gap Contract Proof Surface\n\nkeep me verbatim\n" - if err := os.WriteFile(goalsPath, []byte(src), 0o644); err != nil { - t.Fatal(err) - } - - var out bytes.Buffer - err := RunScenarioCreate(ScenarioCreateOptions{ - GoalsFile: goalsPath, DirectiveNum: 2, Goal: "target behavior", - Threshold: 0.8, Status: "draft", Source: "human", - SpecDir: specDir, JSON: true, Stdout: &out, Now: fixedScenarioClock(), - }) - if err != nil { - t.Fatalf("RunScenarioCreate: %v", err) - } - var res ScenarioCreateResult - if err := json.Unmarshal(out.Bytes(), &res); err != nil { - t.Fatalf("result JSON: %v\n%s", err, out.String()) - } - if !res.Linked || res.DirectiveID == "" { - t.Fatalf("result = %+v, want Linked with a directive_id", res) - } - - // Endpoint 1: the scenario JSON carries the directive's stable ID. - scData, err := os.ReadFile(res.ScenarioPath) - if err != nil { - t.Fatalf("scenario file: %v", err) - } - var sc struct { - DirectiveID string `json:"directive_id"` - } - if err := json.Unmarshal(scData, &sc); err != nil { - t.Fatalf("scenario JSON: %v", err) - } - if sc.DirectiveID != res.DirectiveID { - t.Errorf("scenario directive_id = %q, want %q", sc.DirectiveID, res.DirectiveID) - } - - // Endpoint 2: directive #2's Scenarios line lists the scenario. - patched, _ := os.ReadFile(goalsPath) - if !strings.Contains(string(patched), "**Scenarios:** "+res.ScenarioID) { - t.Errorf("directive Scenarios line missing %s:\n%s", res.ScenarioID, patched) - } - // Non-target content survives the patch byte-for-byte. - if !strings.Contains(string(patched), "## Three-Gap Contract Proof Surface\n\nkeep me verbatim") { - t.Error("Three-Gap section not preserved by the patch") - } - // Directive #2 gained a stable ID; directive #1 is untouched. - p, err := NewGoalsPatcher(patched) - if err != nil { - t.Fatalf("re-parse: %v", err) - } - if d2, _ := p.DirectiveByNumber(2); d2.StableID != res.DirectiveID { - t.Errorf("directive 2 StableID = %q, want %q", d2.StableID, res.DirectiveID) - } - if d1, _ := p.DirectiveByNumber(1); d1.StableID != "" { - t.Errorf("directive 1 must be untouched, got StableID %q", d1.StableID) - } -} - -func TestRunScenarioCreate_UnknownDirective(t *testing.T) { - tmp := t.TempDir() - goalsPath := filepath.Join(tmp, "GOALS.md") - if err := os.WriteFile(goalsPath, []byte("# Goals\n\nm\n\n## Directives\n\n### 1. Only\n\nb\n"), 0o644); err != nil { - t.Fatal(err) - } - before, _ := os.ReadFile(goalsPath) - err := RunScenarioCreate(ScenarioCreateOptions{ - GoalsFile: goalsPath, DirectiveNum: 9, Goal: "g", - Threshold: 0.8, Status: "draft", Source: "human", - SpecDir: filepath.Join(tmp, "spec"), Stdout: &bytes.Buffer{}, Now: fixedScenarioClock(), - }) - if err == nil || !strings.Contains(err.Error(), "directive #9 not found") { - t.Errorf("expected a not-found error, got %v", err) - } - if after, _ := os.ReadFile(goalsPath); string(before) != string(after) { - t.Error("GOALS.md must be untouched when the target directive is not found") - } -} - -func TestRunScenarioCreate_CreationFailureLeavesGoalsUntouched(t *testing.T) { - tmp := t.TempDir() - goalsPath := filepath.Join(tmp, "GOALS.md") - if err := os.WriteFile(goalsPath, []byte("# Goals\n\nm\n\n## Directives\n\n### 1. Only\n\nb\n"), 0o644); err != nil { - t.Fatal(err) - } - before, _ := os.ReadFile(goalsPath) - // An invalid status fails scenario.Create after the in-memory stable-ID - // edit — GOALS.md on disk must remain byte-identical. - err := RunScenarioCreate(ScenarioCreateOptions{ - GoalsFile: goalsPath, DirectiveNum: 1, Goal: "g", - Threshold: 0.8, Status: "bogus", Source: "human", - SpecDir: filepath.Join(tmp, "spec"), Stdout: &bytes.Buffer{}, Now: fixedScenarioClock(), - }) - if err == nil || !strings.Contains(err.Error(), "creating scenario") { - t.Errorf("expected a scenario-creation error, got %v", err) - } - if after, _ := os.ReadFile(goalsPath); string(before) != string(after) { - t.Errorf("GOALS.md must be untouched on scenario-creation failure\nbefore:\n%s\nafter:\n%s", before, after) - } -} diff --git a/cli/internal/knowledge/native.go b/cli/internal/knowledge/native.go index 3f4b0506f..9b72d7c41 100644 --- a/cli/internal/knowledge/native.go +++ b/cli/internal/knowledge/native.go @@ -301,7 +301,7 @@ func PrimitivesForTopic(topic TopicDetail) []string { "stateful environment": {"context", "memory", "environment", "packet", "state", "control plane", "knowledge"}, "replaceable actors": {"actor", "agent", "worker", "handoff", "owner", "ownership", "swarm"}, "stigmergic traces": {"trace", "provenance", "citation", "handoff", "log", "queue", "artifact"}, - "selection gates": {"gate", "validation", "check", "policy", "proof", "review", "pre-mortem"}, + "selection gates": {"gate", "validation", "check", "policy", "proof", "review", "premortem"}, "evolutionary promotion": {"promotion", "promote", "retro", "learning", "flywheel", "reuse", "playbook"}, "governance": {"governance", "scope", "boundary", "operator", "policy", "constraint"}, } diff --git a/cli/internal/llm/chunker.go b/cli/internal/llm/chunker.go index 1b6dd57b2..8780c6742 100644 --- a/cli/internal/llm/chunker.go +++ b/cli/internal/llm/chunker.go @@ -25,7 +25,7 @@ const minMessageChars = 20 // maxChars caps the total UserText+AssistantText for each chunk; when a pair // exceeds the budget, each side is truncated proportionally (40/60 favoring // assistant, matching the spike's extract_chunks.py behavior). Callers should -// use the redactor BEFORE chunking (critical per pre-mortem F3); this +// use the redactor BEFORE chunking (critical per premortem F3); this // function is a pure structural transform. // // Orphan messages (a user with no following assistant, or vice versa) are diff --git a/cli/internal/llm/ollama_client.go b/cli/internal/llm/ollama_client.go index a9a9d3e9f..cd4cbf26c 100644 --- a/cli/internal/llm/ollama_client.go +++ b/cli/internal/llm/ollama_client.go @@ -277,7 +277,7 @@ func (c *OllamaClient) fetchContextBudget() (int, bool) { // ResolveDefaultEndpoint returns the default ollama endpoint from // $AGENTOPS_LLM_ENDPOINT or http://localhost:11434. Not hardcoded to bushido -// per pre-mortem F6. +// per premortem F6. func ResolveDefaultEndpoint() string { if v := os.Getenv("AGENTOPS_LLM_ENDPOINT"); v != "" { return v diff --git a/cli/internal/llm/redactor.go b/cli/internal/llm/redactor.go index a063655bc..e8d48f2fb 100644 --- a/cli/internal/llm/redactor.go +++ b/cli/internal/llm/redactor.go @@ -9,7 +9,7 @@ import ( const redactionDenylistEnv = "AGENTOPS_REDACTION_DENYLIST" // secretPatterns are the regular expressions applied to every message BEFORE -// chunking (critical per pre-mortem F3). Redaction runs ahead of chunking so +// chunking (critical per premortem F3). Redaction runs ahead of chunking so // credentials cannot leak via chunk-boundary truncation. // // Sources: .agents/research/2026-04-11-sessions-privacy-policy.md diff --git a/cli/internal/llm/summarizer.go b/cli/internal/llm/summarizer.go index a7c6b3420..f3b07a43f 100644 --- a/cli/internal/llm/summarizer.go +++ b/cli/internal/llm/summarizer.go @@ -32,7 +32,7 @@ Return ONLY a JSON object with these exact keys: - concepts: array of technical patterns, architectural ideas, or reusable approaches observed - decisions: array of explicit choices made with brief rationale ("chose X because Y") - open_questions: array of unresolved items or things that need follow-up -- work_phase: one of research, plan, implement, verify, post-mortem, other +- work_phase: one of research, plan, implement, verify, postmortem, other Only include entities/concepts that literally appear in the source text. Do not invent. Use empty arrays for fields with no content. If a session has no real decisions or learnings, title it honestly (e.g., "Routine file inspection with no decisions"). diff --git a/cli/internal/llmwiki/atomic.go b/cli/internal/llmwiki/atomic.go index 55ffc72a4..6fad4d3f7 100644 --- a/cli/internal/llmwiki/atomic.go +++ b/cli/internal/llmwiki/atomic.go @@ -2,7 +2,7 @@ // executor. The Karpathy loop has four stages — Ingest, Query, Lint, Promote — // applied conditionally based on wiki state on each tick. // -// Per-stage idempotency contracts (pre-mortem amendment A3): +// Per-stage idempotency contracts (premortem amendment A3): // // - INGEST: atomic write (tmp + fsync + rename). Re-claim after crash inspects // existing wiki/sources/.md and skips if already present. @@ -30,7 +30,7 @@ import ( // file in the same directory, fsyncing it, and renaming over the destination. // A partial write never leaves a corrupted file visible to readers. // -// Per pre-mortem amendment A3. Rejects an empty path (llmwiki never writes to +// Per premortem amendment A3. Rejects an empty path (llmwiki never writes to // the working directory) and otherwise delegates the temp+fsync+chmod+rename // mechanics to the canonical storage.AtomicWriteFile so there is a single // implementation of the algorithm across the CLI. diff --git a/cli/internal/llmwiki/scope_guard.go b/cli/internal/llmwiki/scope_guard.go index 97fc5a74e..a6a68f45d 100644 --- a/cli/internal/llmwiki/scope_guard.go +++ b/cli/internal/llmwiki/scope_guard.go @@ -8,7 +8,7 @@ import ( ) // allowedWritePaths lists the prefixes (relative to vault root) that llmwiki -// stages are allowed to write to. Hardcoded per pre-mortem amendment C5. +// stages are allowed to write to. Hardcoded per premortem amendment C5. // // Roadmap: future expansion to wiki/threads/, wiki/clips/, wiki/reviewed/, // wiki/knowledge/ when those subdirs are formally adopted by llmwiki.loop. diff --git a/cli/internal/parser/extractor.go b/cli/internal/parser/extractor.go index 01e6a3ea1..6a4a0c903 100644 --- a/cli/internal/parser/extractor.go +++ b/cli/internal/parser/extractor.go @@ -40,7 +40,7 @@ var DefaultPatterns = []ExtractionPattern{ "will use", "architecture decision", "design choice", - // Additional patterns from pre-mortem + // Additional patterns from premortem "went with", "opted for", "selected", diff --git a/cli/internal/paths/paths.go b/cli/internal/paths/paths.go index 408564b23..6c2f97697 100644 --- a/cli/internal/paths/paths.go +++ b/cli/internal/paths/paths.go @@ -42,7 +42,6 @@ type Paths struct { AgentsDir string KnowledgeRoot string HooksDir string - ScopeLock string RPIDir string FindingsDir string PlansDir string @@ -100,7 +99,6 @@ func resolveFrom(repoRootDir string) *Paths { agentsDir := envOr("AO_AGENTS_DIR", home) knowledge := envOr("AO_KNOWLEDGE_ROOT", filepath.Join(agentsDir, "wiki")) hooks := envOr("AO_HOOKS_DIR", filepath.Join(agentsDir, "hooks")) - scopeLock := envOr("AO_SCOPE_LOCK", filepath.Join(agentsDir, "scope.lock")) rpi := envOr("AO_RPI_DIR", filepath.Join(agentsDir, "rpi")) findings := envOr("AO_FINDINGS_DIR", filepath.Join(agentsDir, "findings")) plans := envOr("AO_PLANS_DIR", filepath.Join(agentsDir, "plans")) @@ -120,7 +118,6 @@ func resolveFrom(repoRootDir string) *Paths { AgentsDir: agentsDir, KnowledgeRoot: knowledge, HooksDir: hooks, - ScopeLock: scopeLock, RPIDir: rpi, FindingsDir: findings, PlansDir: plans, @@ -169,9 +166,7 @@ func repoRoot(dir string) string { } // Validate ensures every directory root either exists already or can be -// created. It returns the joined error of every dir that fails. ScopeLock is -// a file path — its parent dir must be creatable, but the file itself is not -// required to exist. +// created. It returns the joined error of every directory that fails. func (p *Paths) Validate() error { if p == nil { return errors.New("paths: nil receiver") @@ -197,11 +192,6 @@ func (p *Paths) Validate() error { errs = append(errs, fmt.Errorf("%s (%s): %w", d.name, d.path, err)) } } - if p.ScopeLock != "" { - if err := ensureDir(filepath.Dir(p.ScopeLock)); err != nil { - errs = append(errs, fmt.Errorf("ScopeLock parent (%s): %w", filepath.Dir(p.ScopeLock), err)) - } - } return errors.Join(errs...) } diff --git a/cli/internal/paths/paths_test.go b/cli/internal/paths/paths_test.go index 058a04ddc..b17aab8ac 100644 --- a/cli/internal/paths/paths_test.go +++ b/cli/internal/paths/paths_test.go @@ -23,7 +23,7 @@ func withEnv(t *testing.T, env map[string]string) { t.Helper() keys := []string{ "AO_HOME", "CLAUDE_PLUGIN_DATA", - "AO_AGENTS_DIR", "AO_KNOWLEDGE_ROOT", "AO_HOOKS_DIR", "AO_SCOPE_LOCK", + "AO_AGENTS_DIR", "AO_KNOWLEDGE_ROOT", "AO_HOOKS_DIR", "AO_RPI_DIR", "AO_FINDINGS_DIR", "AO_PLANS_DIR", "AO_COUNCIL_DIR", "AO_LEARNINGS_DIR", "AO_PATTERNS_DIR", "AO_DECISIONS_DIR", "AO_PATHS_DEBUG", @@ -137,10 +137,6 @@ func TestResolve_EnvPrecedence(t *testing.T) { if got.KnowledgeRoot != wantWiki { t.Errorf("KnowledgeRoot = %q, want %q", got.KnowledgeRoot, wantWiki) } - wantLock := filepath.Join(wantAgent, "scope.lock") - if got.ScopeLock != wantLock { - t.Errorf("ScopeLock = %q, want %q", got.ScopeLock, wantLock) - } }) } } @@ -300,7 +296,7 @@ func TestShellGoAgreement(t *testing.T) { // Compare every AO_* key — shell may export AO_PATHS_DEBUG (excluded). for _, key := range []string{ "AO_HOME", "AO_AGENTS_DIR", "AO_KNOWLEDGE_ROOT", - "AO_HOOKS_DIR", "AO_SCOPE_LOCK", "AO_RPI_DIR", + "AO_HOOKS_DIR", "AO_RPI_DIR", "AO_FINDINGS_DIR", "AO_PLANS_DIR", "AO_COUNCIL_DIR", "AO_LEARNINGS_DIR", "AO_PATTERNS_DIR", "AO_DECISIONS_DIR", } { @@ -355,7 +351,6 @@ func goPathsToEnv(p *Paths) map[string]string { "AO_AGENTS_DIR": p.AgentsDir, "AO_KNOWLEDGE_ROOT": p.KnowledgeRoot, "AO_HOOKS_DIR": p.HooksDir, - "AO_SCOPE_LOCK": p.ScopeLock, "AO_RPI_DIR": p.RPIDir, "AO_FINDINGS_DIR": p.FindingsDir, "AO_PLANS_DIR": p.PlansDir, diff --git a/cli/internal/ports/closeout.go b/cli/internal/ports/closeout.go deleted file mode 100644 index 6aa68fc2c..000000000 --- a/cli/internal/ports/closeout.go +++ /dev/null @@ -1,44 +0,0 @@ -// practices: [hexagonal-architecture, ddd-bounded-context] -package ports - -import "context" - -// CloseoutAction identifies one explicit closeout operation. -type CloseoutAction string - -const ( - CloseoutActionHandoff CloseoutAction = "handoff" - CloseoutActionFlywheel CloseoutAction = "flywheel" - CloseoutActionDefrag CloseoutAction = "defrag" - CloseoutActionMaintain CloseoutAction = "maintain" -) - -// CloseoutRequest describes an explicit end-of-cycle or end-of-session -// closeout. RunID is optional for local/manual closeout but should be -// supplied by RPI/evolve cycle callers. -type CloseoutRequest struct { - RunID string - Actions []CloseoutAction - Metadata map[string]string -} - -// CloseoutResult reports what was completed and which artifacts were -// written. Artifacts are paths or durable references. -type CloseoutResult struct { - Completed []CloseoutAction - Artifacts []string - Warnings []string -} - -// CloseoutPort is the hookless-first closeout surface. It absorbs -// Stop and SessionEnd maintenance hooks by making closeout an -// explicit lifecycle operation. -// -// Contract: -// -// - Close rejects an empty Actions list. -// - Returned slices MUST be safe for the caller to mutate. -// - Context cancellation MUST be honored on a best-effort basis. -type CloseoutPort interface { - Close(ctx context.Context, req CloseoutRequest) (CloseoutResult, error) -} diff --git a/cli/internal/ports/convergence_check.go b/cli/internal/ports/convergence_check.go deleted file mode 100644 index 88fb59abe..000000000 --- a/cli/internal/ports/convergence_check.go +++ /dev/null @@ -1,70 +0,0 @@ -// practices: [hexagonal-architecture, ddd-bounded-context] -package ports - -import "context" - -// ConvergenceCriteria is the structural stop predicate for the -// evolve loop. The default AgentOps 3.0 predicate is: -// CI green streak >= 3, HIGH+MEDIUM unconsumed findings <= 1, and a -// captured fitness baseline. -type ConvergenceCriteria struct { - MinGreenCIStreak int - MaxUnconsumedHighMedium int - RequireFitnessBaseline bool -} - -// DefaultConvergenceCriteria returns the canonical evolve stop -// predicate recorded by the BC3 Loop epic. -func DefaultConvergenceCriteria() ConvergenceCriteria { - return ConvergenceCriteria{ - MinGreenCIStreak: 3, - MaxUnconsumedHighMedium: 1, - RequireFitnessBaseline: true, - } -} - -// ConvergenceInput is the evidence slice the predicate evaluates. -// RecentCIRuns MUST be ordered most-recent first, matching -// CIStatusPort.Recent. UnconsumedHighMedium is the current count of -// unconsumed HIGH+MEDIUM findings. FitnessBaselineCaptured reports -// whether the loop has a baseline artifact for comparison. -type ConvergenceInput struct { - RecentCIRuns []CIRun - UnconsumedHighMedium int - FitnessBaselineCaptured bool -} - -// ConvergenceResult is the evaluated stop decision plus the observed -// values that produced it. Reasons is populated only for unmet -// criteria and is safe for callers to mutate. -type ConvergenceResult struct { - Converged bool - CIGreenStreak int - UnconsumedHighMedium int - FitnessBaselineCaptured bool - Reasons []string -} - -// ConvergenceCheckPort is the BC3 Loop predicate for stopping an -// autonomous improvement run. Callers pass already-gathered evidence; -// the port does not fetch CI, scan ledgers, or read fitness files -// itself. That keeps the stop decision deterministic and reusable -// across CLI commands, skills, and future daemon workers. -// -// Contract: -// -// - Check MUST count only the leading most-recent streak of runs -// whose Status is "completed" and Conclusion is "success". -// - Queued, in-progress, failed, cancelled, skipped, or unknown -// runs MUST break the green streak. -// - Default criteria are green streak >=3, HIGH+MEDIUM <=1, and -// baseline captured. -// - Result.Reasons MUST name every unmet criterion. -// - Context cancellation MUST be honored on a best-effort basis. -// -// See docs/contracts/ubiquitous-language.md (BC3 row). This port is -// the typed replacement for ad hoc convergence shell predicates in -// evolve scripts. -type ConvergenceCheckPort interface { - Check(ctx context.Context, input ConvergenceInput) (ConvergenceResult, error) -} diff --git a/cli/internal/ports/doc.go b/cli/internal/ports/doc.go index e88c1acc2..698bf8f63 100644 --- a/cli/internal/ports/doc.go +++ b/cli/internal/ports/doc.go @@ -1,32 +1,9 @@ -// Package ports holds the typed interfaces (hexagonal-architecture -// "ports") that the AgentOps bounded contexts depend on. Concrete -// implementations live alongside the existing packages that already -// own the behavior; the port types here exist so that: +// Package ports contains small interfaces for retained read-only inspection, +// deterministic checks, generic evidence records, and optional runtime +// adapters. It deliberately contains no retry governor, work tracker, +// workspace lifecycle, queue, lease, closure, release, or delivery port. // -// 1. callers can be tested against in-memory adapters instead of real -// filesystem / network / subprocess collaborators -// 2. drift between the conceptual BC contract and the implementation -// is visible as a compile-time mismatch on the port type rather -// than a runtime surprise -// 3. the 5 BCs (Corpus / Validation / Loop / Factory / Runtime, per -// docs/contracts/ubiquitous-language.md) each get a small, named -// set of interfaces other BCs can hold without importing the -// owning BC's full surface -// -// This package is being filled out incrementally per the BC epics: -// -// - soc-2c1p (BC1 Corpus): CorpusReaderPort, CorpusWriterPort, -// FindingCompilerPort, CitationPort, ContextCompilerPort -// - soc-wxh5 (BC2 Validation): GateRunnerPort, CIStatusPort, -// ClaimEvidenceBinderPort, SafetyPolicyPort -// - soc-y5vh (BC3 Loop): LoopReaderPort, LoopWriterPort, -// CloseoutPort, HypothesisLedgerPort, ConvergenceCheckPort -// - soc-2klg (BC4 Factory): OperatorPort, EventBusPort, -// ClaimEvidencePort -// - soc-zd7c (BC5 Runtime): HarnessPort, WorkspacePort -// -// New ports are added in bounded slices, with at least one in-memory -// adapter and a Go test that fires on regression. See -// docs/plans/2026-05-12-rescope-evolve-and-architecture.md for the -// rescoping rationale and ordering. +// Semantic validation is implemented by the Validate skill's pure manifest and +// verdict helpers. Repository checks use GateRunnerPort; neither surface owns +// Git or continuation decisions. package ports diff --git a/cli/internal/ports/event_bus.go b/cli/internal/ports/event_bus.go deleted file mode 100644 index 3165b77bc..000000000 --- a/cli/internal/ports/event_bus.go +++ /dev/null @@ -1,48 +0,0 @@ -// practices: [hexagonal-architecture, ddd-bounded-context] -package ports - -import "context" - -// Event is one factory event flowing through the bus. Topic names the -// event category (e.g. "operator.intent", "cycle.completed", -// "ci.failed"); Payload is the typed body (opaque to the bus, parsed -// by subscribers); ID is an adapter-assigned event identifier (empty -// at publish time, populated by the adapter). -type Event struct { - ID string - Topic string - Payload []byte -} - -// EventHandler is the subscriber callback shape. Returning a non-nil -// error from a handler does NOT cause the bus to retry; the adapter -// MAY log the error for observability. Subscribers must implement -// their own retry/backoff. -type EventHandler func(ctx context.Context, event Event) error - -// EventBusPort is the BC4 Factory async dispatch surface. Callers — -// /post-mortem aggregators, dream's compounding loop, CI-result -// listeners, and any future factory-side subscriber — depend on this -// port so they can publish and subscribe without coupling to a -// specific transport (in-memory channel, NATS, Kafka, etc). -// -// Contract: -// -// - Publish MUST return after the event is enqueued (synchronous -// publish-acknowledge, async dispatch). The returned event has ID -// populated. -// - Subscribe MUST return a cancellation function. Calling it -// unregisters the handler and blocks until any in-flight callback -// for that handler completes. -// - Subscribers see events for matching Topic only (exact match; -// no wildcards in this port; future adapters MAY add globbing). -// - Empty Topic on Publish is a structural-rejection error. -// - Context cancellation MUST be honored on Publish best-effort. -// -// See docs/contracts/ubiquitous-language.md (BC4 row). soc-2klg epic. -// Sibling: OperatorPort (cycle 104) records intents that are typically -// then Published onto an EventBus topic. -type EventBusPort interface { - Publish(ctx context.Context, event Event) (Event, error) - Subscribe(ctx context.Context, topic string, handler EventHandler) (cancel func(), err error) -} diff --git a/cli/internal/ports/finding_compiler.go b/cli/internal/ports/finding_compiler.go index f3df16b98..eae11654f 100644 --- a/cli/internal/ports/finding_compiler.go +++ b/cli/internal/ports/finding_compiler.go @@ -126,7 +126,7 @@ type CompiledOutput struct { // FindingCompilerPort is the BC1 compile-side. It turns a promoted // finding artifact into the advisory and mechanical outputs named in // docs/contracts/finding-compiler.md "Compiler Targets" — planning -// rules, pre-mortem checks, and constraints. Callers — the +// rules, premortem checks, and constraints. Callers — the // `ao compile` path, dream's compounding loop, and any future // cross-repo finding ingester — depend on this port so the compile // behavior can be exercised against an in-memory adapter without diff --git a/cli/internal/ports/harness.go b/cli/internal/ports/harness.go index 97facbf80..addd49f37 100644 --- a/cli/internal/ports/harness.go +++ b/cli/internal/ports/harness.go @@ -16,7 +16,7 @@ const ( // HarnessSkillSync is one (skill, harness) → checksum mapping. Path // is the skill manifest path the harness expects to find (e.g. -// "skills-codex/evolve/SKILL.md"); ContentHash is the SHA hash the +// "skills-codex/validate/SKILL.md"); ContentHash is the SHA hash the // adapter computed; OutOfSync is true when the hash doesn't match // the canonical source-of-truth (skills//SKILL.md by default). type HarnessSkillSync struct { @@ -28,8 +28,8 @@ type HarnessSkillSync struct { } // HarnessPort is the BC5 Runtime surface. Callers — `make sync-hooks`, -// the codex-parity audit, the dream-loop harness-state recorder, and -// any future cross-harness sanity check — depend on this port so they +// the codex-parity audit and any cross-harness sanity check depend on +// this port so they // can ask "what's the sync state of each (skill, harness) pair?" // without coupling to a specific implementation (filesystem scan, // registry.json snapshot, etc.). diff --git a/cli/internal/ports/hypothesis_ledger.go b/cli/internal/ports/hypothesis_ledger.go deleted file mode 100644 index 1133b33a5..000000000 --- a/cli/internal/ports/hypothesis_ledger.go +++ /dev/null @@ -1,56 +0,0 @@ -// practices: [hexagonal-architecture, ddd-bounded-context] -package ports - -import "context" - -// HypothesisVerdict is the current empirical status of one evolve -// hypothesis. The string type stays open so file-backed adapters can -// round-trip legacy ledger rows that include explanatory suffixes. -type HypothesisVerdict string - -const ( - HypothesisVerdictPending HypothesisVerdict = "PENDING" - HypothesisVerdictVerified HypothesisVerdict = "VERIFIED" - HypothesisVerdictFalsified HypothesisVerdict = "FALSIFIED" -) - -// HypothesisRecord is one row in evolve's hypothesis ledger -// (.agents/evolve/hypotheses.jsonl). The ledger is local runtime -// state, but the shape is a BC3 Loop contract: a patch makes a -// falsifiable claim, names the future check cycle, and later records -// evidence for the verdict. -type HypothesisRecord struct { - ID string `json:"id"` - CycleLanded int `json:"cycle_landed,omitempty"` - CheckAtCycle int `json:"check_at_cycle,omitempty"` - Patch string `json:"patch,omitempty"` - Hypothesis string `json:"hypothesis,omitempty"` - Measure string `json:"measure,omitempty"` - Verdict HypothesisVerdict `json:"verdict,omitempty"` - Evidence []string `json:"evidence,omitempty"` -} - -// HypothesisLedgerPort is the BC3 Loop port for evolve's empirical -// improvement ledger. Callers - evolve's cycle closeout, post-mortem -// checks, convergence audits, and future hypothesis-verdict workers - -// depend on this port so hypothesis tracking does not depend directly -// on the local JSONL file shape. -// -// Contract: -// -// - Append MUST reject an empty ID. -// - Append MUST reject duplicate IDs with a non-nil error. -// - Append returns the record as stored. -// - List returns records in append order. -// - Find returns (zero-value, false, nil) when the ID is unknown. -// - Returned records MUST be safe for callers to mutate. -// - Context cancellation MUST be honored on a best-effort basis. -// -// See docs/contracts/ubiquitous-language.md (BC3 row). This port is -// paired with ConvergenceCheckPort so the loop can state both "what -// are we testing?" and "are we done?" in typed domain terms. -type HypothesisLedgerPort interface { - Append(ctx context.Context, record HypothesisRecord) (HypothesisRecord, error) - List(ctx context.Context) ([]HypothesisRecord, error) - Find(ctx context.Context, id string) (HypothesisRecord, bool, error) -} diff --git a/cli/internal/ports/inmemory_closeout.go b/cli/internal/ports/inmemory_closeout.go deleted file mode 100644 index 4c60db77e..000000000 --- a/cli/internal/ports/inmemory_closeout.go +++ /dev/null @@ -1,46 +0,0 @@ -// practices: [hexagonal-architecture, ddd-bounded-context] -package ports - -import ( - "context" - "errors" -) - -// InMemoryCloseout is a CloseoutPort that records requests and echoes -// the requested actions as completed. -type InMemoryCloseout struct { - Requests []CloseoutRequest - Artifacts []string - Warnings []string -} - -func NewInMemoryCloseout() *InMemoryCloseout { - return &InMemoryCloseout{Requests: []CloseoutRequest{}, Artifacts: []string{}, Warnings: []string{}} -} - -func (c *InMemoryCloseout) Close(ctx context.Context, req CloseoutRequest) (CloseoutResult, error) { - if err := ctx.Err(); err != nil { - return CloseoutResult{}, err - } - if len(req.Actions) == 0 { - return CloseoutResult{}, errors.New("ports: CloseoutPort.Close actions required") - } - c.Requests = append(c.Requests, cloneCloseoutRequest(req)) - return CloseoutResult{ - Completed: append([]CloseoutAction(nil), req.Actions...), - Artifacts: append([]string(nil), c.Artifacts...), - Warnings: append([]string(nil), c.Warnings...), - }, nil -} - -func cloneCloseoutRequest(req CloseoutRequest) CloseoutRequest { - metadata := make(map[string]string, len(req.Metadata)) - for key, value := range req.Metadata { - metadata[key] = value - } - req.Metadata = metadata - req.Actions = append([]CloseoutAction(nil), req.Actions...) - return req -} - -var _ CloseoutPort = (*InMemoryCloseout)(nil) diff --git a/cli/internal/ports/inmemory_closeout_test.go b/cli/internal/ports/inmemory_closeout_test.go deleted file mode 100644 index 00b7733e9..000000000 --- a/cli/internal/ports/inmemory_closeout_test.go +++ /dev/null @@ -1,69 +0,0 @@ -// practices: [hexagonal-architecture, tdd] -package ports - -import ( - "context" - "errors" - "strings" - "testing" -) - -func TestInMemoryCloseout_CompletesRequestedActions(t *testing.T) { - closeout := NewInMemoryCloseout() - closeout.Artifacts = []string{".agents/handoff.md"} - result, err := closeout.Close(context.Background(), CloseoutRequest{ - RunID: "run-1", - Actions: []CloseoutAction{CloseoutActionHandoff, CloseoutActionFlywheel}, - }) - if err != nil { - t.Fatal(err) - } - if len(result.Completed) != 2 || result.Completed[0] != CloseoutActionHandoff { - t.Fatalf("Completed = %#v, want handoff + flywheel", result.Completed) - } - if len(result.Artifacts) != 1 || result.Artifacts[0] != ".agents/handoff.md" { - t.Fatalf("Artifacts = %#v, want handoff artifact", result.Artifacts) - } - if len(closeout.Requests) != 1 || closeout.Requests[0].RunID != "run-1" { - t.Fatalf("Requests = %#v, want recorded run-1 request", closeout.Requests) - } -} - -func TestInMemoryCloseout_RejectsEmptyActions(t *testing.T) { - closeout := NewInMemoryCloseout() - _, err := closeout.Close(context.Background(), CloseoutRequest{}) - if err == nil { - t.Fatal("expected error for empty actions") - } - if !strings.Contains(err.Error(), "actions required") { - t.Fatalf("error = %v, want actions required", err) - } -} - -func TestInMemoryCloseout_ReturnsDefensiveCopies(t *testing.T) { - closeout := NewInMemoryCloseout() - closeout.Artifacts = []string{"a"} - first, err := closeout.Close(context.Background(), CloseoutRequest{Actions: []CloseoutAction{CloseoutActionDefrag}}) - if err != nil { - t.Fatal(err) - } - first.Completed[0] = CloseoutActionMaintain - first.Artifacts[0] = "mutated" - second, err := closeout.Close(context.Background(), CloseoutRequest{Actions: []CloseoutAction{CloseoutActionDefrag}}) - if err != nil { - t.Fatal(err) - } - if second.Completed[0] != CloseoutActionDefrag || second.Artifacts[0] != "a" { - t.Fatalf("second result was mutated: %#v", second) - } -} - -func TestInMemoryCloseout_HonorsContextCancellation(t *testing.T) { - closeout := NewInMemoryCloseout() - ctx, cancel := context.WithCancel(context.Background()) - cancel() - _, err := closeout.Close(ctx, CloseoutRequest{Actions: []CloseoutAction{CloseoutActionHandoff}}) - if !errors.Is(err, context.Canceled) { - t.Fatalf("error = %v, want context.Canceled", err) - } -} diff --git a/cli/internal/ports/inmemory_convergence_check.go b/cli/internal/ports/inmemory_convergence_check.go deleted file mode 100644 index 0767e37d7..000000000 --- a/cli/internal/ports/inmemory_convergence_check.go +++ /dev/null @@ -1,63 +0,0 @@ -// practices: [hexagonal-architecture, ddd-bounded-context] -package ports - -import "context" - -// InMemoryConvergenceCheck is a deterministic ConvergenceCheckPort -// implementation. It does not retain state; "in-memory" means the -// caller supplies the already-materialized evidence slice directly. -type InMemoryConvergenceCheck struct { - criteria ConvergenceCriteria -} - -// NewInMemoryConvergenceCheck returns a convergence checker. Passing -// no criteria uses DefaultConvergenceCriteria; passing one criteria -// value uses it as-is. -func NewInMemoryConvergenceCheck(criteria ...ConvergenceCriteria) *InMemoryConvergenceCheck { - c := DefaultConvergenceCriteria() - if len(criteria) > 0 { - c = criteria[0] - } - return &InMemoryConvergenceCheck{criteria: c} -} - -// Check evaluates the configured convergence criteria. -func (c *InMemoryConvergenceCheck) Check(ctx context.Context, input ConvergenceInput) (ConvergenceResult, error) { - if err := ctx.Err(); err != nil { - return ConvergenceResult{}, err - } - - greenStreak := leadingGreenCIStreak(input.RecentCIRuns) - result := ConvergenceResult{ - CIGreenStreak: greenStreak, - UnconsumedHighMedium: input.UnconsumedHighMedium, - FitnessBaselineCaptured: input.FitnessBaselineCaptured, - } - - if greenStreak < c.criteria.MinGreenCIStreak { - result.Reasons = append(result.Reasons, "ci-green-streak-below-threshold") - } - if input.UnconsumedHighMedium > c.criteria.MaxUnconsumedHighMedium { - result.Reasons = append(result.Reasons, "unconsumed-high-medium-above-threshold") - } - if c.criteria.RequireFitnessBaseline && !input.FitnessBaselineCaptured { - result.Reasons = append(result.Reasons, "fitness-baseline-missing") - } - result.Converged = len(result.Reasons) == 0 - result.Reasons = append([]string(nil), result.Reasons...) - return result, nil -} - -func leadingGreenCIStreak(runs []CIRun) int { - streak := 0 - for _, run := range runs { - if run.Status != CIRunStatusCompleted || run.Conclusion != CIRunConclusionSuccess { - return streak - } - streak++ - } - return streak -} - -// Compile-time assertion: InMemoryConvergenceCheck satisfies the port. -var _ ConvergenceCheckPort = (*InMemoryConvergenceCheck)(nil) diff --git a/cli/internal/ports/inmemory_convergence_check_test.go b/cli/internal/ports/inmemory_convergence_check_test.go deleted file mode 100644 index a1768341a..000000000 --- a/cli/internal/ports/inmemory_convergence_check_test.go +++ /dev/null @@ -1,153 +0,0 @@ -// practices: [hexagonal-architecture, tdd] -package ports - -import ( - "context" - "errors" - "reflect" - "testing" -) - -func TestInMemoryConvergenceCheck_CheckConvergesWithDefaultCriteria(t *testing.T) { - checker := NewInMemoryConvergenceCheck() - result, err := checker.Check(context.Background(), ConvergenceInput{ - RecentCIRuns: []CIRun{ - {Status: CIRunStatusCompleted, Conclusion: CIRunConclusionSuccess}, - {Status: CIRunStatusCompleted, Conclusion: CIRunConclusionSuccess}, - {Status: CIRunStatusCompleted, Conclusion: CIRunConclusionSuccess}, - {Status: CIRunStatusCompleted, Conclusion: CIRunConclusionFailure}, - }, - UnconsumedHighMedium: 1, - FitnessBaselineCaptured: true, - }) - if err != nil { - t.Fatal(err) - } - if !result.Converged { - t.Fatalf("Converged = false, want true; reasons=%v", result.Reasons) - } - if result.CIGreenStreak != 3 { - t.Fatalf("CIGreenStreak = %d, want 3", result.CIGreenStreak) - } - if len(result.Reasons) != 0 { - t.Fatalf("Reasons = %v, want empty", result.Reasons) - } -} - -func TestInMemoryConvergenceCheck_CheckReportsEveryUnmetCriterion(t *testing.T) { - checker := NewInMemoryConvergenceCheck() - result, err := checker.Check(context.Background(), ConvergenceInput{ - RecentCIRuns: []CIRun{ - {Status: CIRunStatusCompleted, Conclusion: CIRunConclusionSuccess}, - {Status: CIRunStatusCompleted, Conclusion: CIRunConclusionFailure}, - }, - UnconsumedHighMedium: 2, - FitnessBaselineCaptured: false, - }) - if err != nil { - t.Fatal(err) - } - if result.Converged { - t.Fatal("Converged = true, want false") - } - want := []string{ - "ci-green-streak-below-threshold", - "unconsumed-high-medium-above-threshold", - "fitness-baseline-missing", - } - if !reflect.DeepEqual(result.Reasons, want) { - t.Fatalf("Reasons = %v, want %v", result.Reasons, want) - } -} - -func TestInMemoryConvergenceCheck_InProgressRunBreaksGreenStreak(t *testing.T) { - checker := NewInMemoryConvergenceCheck() - result, err := checker.Check(context.Background(), ConvergenceInput{ - RecentCIRuns: []CIRun{ - {Status: CIRunStatusInProgress, Conclusion: CIRunConclusionNone}, - {Status: CIRunStatusCompleted, Conclusion: CIRunConclusionSuccess}, - {Status: CIRunStatusCompleted, Conclusion: CIRunConclusionSuccess}, - {Status: CIRunStatusCompleted, Conclusion: CIRunConclusionSuccess}, - }, - UnconsumedHighMedium: 0, - FitnessBaselineCaptured: true, - }) - if err != nil { - t.Fatal(err) - } - if result.CIGreenStreak != 0 { - t.Fatalf("CIGreenStreak = %d, want 0", result.CIGreenStreak) - } - if result.Converged { - t.Fatal("Converged = true, want false") - } -} - -func TestInMemoryConvergenceCheck_CountsOnlyLeadingGreenStreak(t *testing.T) { - checker := NewInMemoryConvergenceCheck() - result, err := checker.Check(context.Background(), ConvergenceInput{ - RecentCIRuns: []CIRun{ - {Status: CIRunStatusCompleted, Conclusion: CIRunConclusionSuccess}, - {Status: CIRunStatusCompleted, Conclusion: CIRunConclusionSuccess}, - {Status: CIRunStatusCompleted, Conclusion: CIRunConclusionSkipped}, - {Status: CIRunStatusCompleted, Conclusion: CIRunConclusionSuccess}, - }, - UnconsumedHighMedium: 0, - FitnessBaselineCaptured: true, - }) - if err != nil { - t.Fatal(err) - } - if result.CIGreenStreak != 2 { - t.Fatalf("CIGreenStreak = %d, want 2", result.CIGreenStreak) - } -} - -func TestInMemoryConvergenceCheck_UsesCustomCriteria(t *testing.T) { - checker := NewInMemoryConvergenceCheck(ConvergenceCriteria{ - MinGreenCIStreak: 2, - MaxUnconsumedHighMedium: 0, - RequireFitnessBaseline: false, - }) - result, err := checker.Check(context.Background(), ConvergenceInput{ - RecentCIRuns: []CIRun{ - {Status: CIRunStatusCompleted, Conclusion: CIRunConclusionSuccess}, - {Status: CIRunStatusCompleted, Conclusion: CIRunConclusionSuccess}, - }, - UnconsumedHighMedium: 0, - FitnessBaselineCaptured: false, - }) - if err != nil { - t.Fatal(err) - } - if !result.Converged { - t.Fatalf("Converged = false, want true; reasons=%v", result.Reasons) - } -} - -func TestInMemoryConvergenceCheck_ReasonsAreDefensiveCopy(t *testing.T) { - checker := NewInMemoryConvergenceCheck() - result, err := checker.Check(context.Background(), ConvergenceInput{}) - if err != nil { - t.Fatal(err) - } - result.Reasons[0] = "mutated" - - next, err := checker.Check(context.Background(), ConvergenceInput{}) - if err != nil { - t.Fatal(err) - } - if next.Reasons[0] != "ci-green-streak-below-threshold" { - t.Fatalf("next Reasons[0] = %q, want ci-green-streak-below-threshold", next.Reasons[0]) - } -} - -func TestInMemoryConvergenceCheck_HonorsContextCancellation(t *testing.T) { - checker := NewInMemoryConvergenceCheck() - ctx, cancel := context.WithCancel(context.Background()) - cancel() - _, err := checker.Check(ctx, ConvergenceInput{}) - if !errors.Is(err, context.Canceled) { - t.Fatalf("Check error = %v, want context.Canceled", err) - } -} diff --git a/cli/internal/ports/inmemory_event_bus.go b/cli/internal/ports/inmemory_event_bus.go deleted file mode 100644 index df42482b1..000000000 --- a/cli/internal/ports/inmemory_event_bus.go +++ /dev/null @@ -1,114 +0,0 @@ -// practices: [hexagonal-architecture, ddd-bounded-context] -package ports - -import ( - "context" - "errors" - "fmt" - "sync" -) - -// InMemoryEventBus is an EventBusPort backed by per-topic handler -// slices. Dispatch is SYNCHRONOUS in this adapter — Publish calls -// each handler inline and returns after they all complete. This -// keeps tests deterministic and avoids goroutine-leak risk. The -// in-process production adapter -// (cli/cmd/ao/event_bus_adapter.go, productionEventBus) uses the -// same sync semantics. Distributed adapters (NATS, Kafka, Redis -// Streams) are the future shape the port contract permits. -// -// Thread-safe via mutex on the subscribers map. Each call to -// Publish acquires the mutex briefly to snapshot the handler slice, -// then releases it before calling handlers (so handlers can -// Subscribe/cancel during dispatch without deadlock). -// -// Caveat: handlers run while holding their subscription's mu (so -// cancel can wait an in-flight call out). A handler that calls -// Publish synchronously on a topic it is itself subscribed to would -// re-enter that same non-recursive mu and self-deadlock. Handlers -// that need to emit follow-up events must do so asynchronously. -type InMemoryEventBus struct { - mu sync.Mutex - subscribers map[string][]*eventSubscription - nextID int -} - -type eventSubscription struct { - id int - handler EventHandler - mu sync.Mutex // serializes in-flight calls + cancel - dead bool -} - -// NewInMemoryEventBus returns an empty bus. -func NewInMemoryEventBus() *InMemoryEventBus { - return &InMemoryEventBus{ - subscribers: map[string][]*eventSubscription{}, - } -} - -// Publish dispatches the event to subscribers of event.Topic. See -// package-level contract for the synchronous dispatch caveat. -func (b *InMemoryEventBus) Publish(ctx context.Context, event Event) (Event, error) { - if err := ctx.Err(); err != nil { - return Event{}, err - } - if event.Topic == "" { - return Event{}, errors.New("ports: Event.Topic required") - } - b.mu.Lock() - b.nextID++ - event.ID = fmt.Sprintf("evt-%d", b.nextID) - subs := append([]*eventSubscription{}, b.subscribers[event.Topic]...) - b.mu.Unlock() - for _, sub := range subs { - sub.mu.Lock() - if sub.dead { - sub.mu.Unlock() - continue - } - // Call handler holding sub.mu so cancel can wait us out. - _ = sub.handler(ctx, event) - sub.mu.Unlock() - } - return event, nil -} - -// Subscribe registers handler for events with matching topic. Returns -// a cancel function that blocks until in-flight dispatch completes. -func (b *InMemoryEventBus) Subscribe(ctx context.Context, topic string, handler EventHandler) (func(), error) { - if err := ctx.Err(); err != nil { - return nil, err - } - if topic == "" { - return nil, errors.New("ports: Subscribe topic required") - } - if handler == nil { - return nil, errors.New("ports: Subscribe handler required") - } - b.mu.Lock() - b.nextID++ - sub := &eventSubscription{id: b.nextID, handler: handler} - b.subscribers[topic] = append(b.subscribers[topic], sub) - b.mu.Unlock() - cancel := func() { - // Wait for in-flight dispatch to complete (via sub.mu) then - // mark dead and remove from the subscribers slice. - sub.mu.Lock() - sub.dead = true - sub.mu.Unlock() - b.mu.Lock() - defer b.mu.Unlock() - s := b.subscribers[topic] - for i, x := range s { - if x == sub { - b.subscribers[topic] = append(s[:i], s[i+1:]...) - break - } - } - } - return cancel, nil -} - -// Compile-time assertion: InMemoryEventBus satisfies the port. -var _ EventBusPort = (*InMemoryEventBus)(nil) diff --git a/cli/internal/ports/inmemory_event_bus_test.go b/cli/internal/ports/inmemory_event_bus_test.go deleted file mode 100644 index 9369c9b7b..000000000 --- a/cli/internal/ports/inmemory_event_bus_test.go +++ /dev/null @@ -1,144 +0,0 @@ -// practices: [hexagonal-architecture, tdd] -package ports - -import ( - "context" - "errors" - "strings" - "sync/atomic" - "testing" -) - -// Sibling pattern: inmemory_operator_test.go (cycle 104). - -func TestInMemoryEventBus_PublishDispatchesToSubscriber(t *testing.T) { - b := NewInMemoryEventBus() - var received int32 - cancel, err := b.Subscribe(context.Background(), "cycle.completed", func(ctx context.Context, e Event) error { - atomic.AddInt32(&received, 1) - return nil - }) - if err != nil { - t.Fatal(err) - } - defer cancel() - - out, err := b.Publish(context.Background(), Event{Topic: "cycle.completed", Payload: []byte("hello")}) - if err != nil { - t.Fatalf("Publish: %v", err) - } - if out.ID == "" { - t.Fatal("Publish returned empty event ID") - } - if atomic.LoadInt32(&received) != 1 { - t.Fatalf("received = %d, want 1", received) - } -} - -func TestInMemoryEventBus_PublishEmptyTopicRejected(t *testing.T) { - b := NewInMemoryEventBus() - _, err := b.Publish(context.Background(), Event{Payload: []byte("x")}) - if err == nil { - t.Fatal("expected error on empty Topic, got nil") - } -} - -func TestInMemoryEventBus_PublishWithoutSubscribersNoOp(t *testing.T) { - b := NewInMemoryEventBus() - out, err := b.Publish(context.Background(), Event{Topic: "no.one.listens"}) - if err != nil { - t.Fatalf("Publish: %v", err) - } - if out.ID == "" { - t.Fatal("Publish should assign ID even with no subscribers") - } -} - -func TestInMemoryEventBus_TopicFilteringIsExact(t *testing.T) { - b := NewInMemoryEventBus() - var aReceived, bReceived int32 - _, _ = b.Subscribe(context.Background(), "topic.a", func(_ context.Context, _ Event) error { - atomic.AddInt32(&aReceived, 1) - return nil - }) - _, _ = b.Subscribe(context.Background(), "topic.b", func(_ context.Context, _ Event) error { - atomic.AddInt32(&bReceived, 1) - return nil - }) - _, _ = b.Publish(context.Background(), Event{Topic: "topic.a"}) - if atomic.LoadInt32(&aReceived) != 1 || atomic.LoadInt32(&bReceived) != 0 { - t.Fatalf("aReceived=%d (want 1), bReceived=%d (want 0)", aReceived, bReceived) - } -} - -func TestInMemoryEventBus_CancelStopsHandler(t *testing.T) { - b := NewInMemoryEventBus() - var received int32 - cancel, _ := b.Subscribe(context.Background(), "t", func(_ context.Context, _ Event) error { - atomic.AddInt32(&received, 1) - return nil - }) - _, _ = b.Publish(context.Background(), Event{Topic: "t"}) - if atomic.LoadInt32(&received) != 1 { - t.Fatalf("pre-cancel received = %d, want 1", received) - } - cancel() - _, _ = b.Publish(context.Background(), Event{Topic: "t"}) - if atomic.LoadInt32(&received) != 1 { - t.Fatalf("post-cancel received = %d, want 1 (no new dispatch)", received) - } -} - -func TestInMemoryEventBus_HandlerErrorDoesNotStopOtherSubscribers(t *testing.T) { - b := NewInMemoryEventBus() - var subjectReceived int32 - _, _ = b.Subscribe(context.Background(), "t", func(_ context.Context, _ Event) error { - return errors.New("subscriber failed") - }) - _, _ = b.Subscribe(context.Background(), "t", func(_ context.Context, _ Event) error { - atomic.AddInt32(&subjectReceived, 1) - return nil - }) - _, _ = b.Publish(context.Background(), Event{Topic: "t"}) - if atomic.LoadInt32(&subjectReceived) != 1 { - t.Fatalf("subjectReceived = %d, want 1 (handler error must not block siblings)", subjectReceived) - } -} - -func TestInMemoryEventBus_SubscribeRejectsEmptyTopicOrNilHandler(t *testing.T) { - b := NewInMemoryEventBus() - if _, err := b.Subscribe(context.Background(), "", func(_ context.Context, _ Event) error { return nil }); err == nil { - t.Fatal("empty topic: expected error, got nil") - } - if _, err := b.Subscribe(context.Background(), "t", nil); err == nil { - t.Fatal("nil handler: expected error, got nil") - } -} - -func TestInMemoryEventBus_HonorsContextCancellation(t *testing.T) { - b := NewInMemoryEventBus() - ctx, cancel := context.WithCancel(context.Background()) - cancel() - if _, err := b.Publish(ctx, Event{Topic: "t"}); err == nil { - t.Fatal("Publish: expected cancellation error, got nil") - } else if !errors.Is(err, context.Canceled) { - t.Fatalf("Publish error = %v, want context.Canceled", err) - } - if _, err := b.Subscribe(ctx, "t", func(_ context.Context, _ Event) error { return nil }); err == nil { - t.Fatal("Subscribe: expected cancellation error, got nil") - } else if !errors.Is(err, context.Canceled) { - t.Fatalf("Subscribe error = %v, want context.Canceled", err) - } -} - -func TestInMemoryEventBus_EventIDsAreSequentialAndPrefixed(t *testing.T) { - b := NewInMemoryEventBus() - out1, _ := b.Publish(context.Background(), Event{Topic: "t"}) - out2, _ := b.Publish(context.Background(), Event{Topic: "t"}) - if !strings.HasPrefix(out1.ID, "evt-") || !strings.HasPrefix(out2.ID, "evt-") { - t.Fatalf("IDs not prefixed 'evt-': %q %q", out1.ID, out2.ID) - } - if out1.ID == out2.ID { - t.Fatalf("two Publishes produced identical IDs: %q", out1.ID) - } -} diff --git a/cli/internal/ports/inmemory_finding_compiler.go b/cli/internal/ports/inmemory_finding_compiler.go index 055bd76e9..fa14b7245 100644 --- a/cli/internal/ports/inmemory_finding_compiler.go +++ b/cli/internal/ports/inmemory_finding_compiler.go @@ -140,7 +140,7 @@ func parseCompilerTargets(raw string) []CompiledOutputKind { switch name { case "plan", "planning-rule", "planning_rule": appendUnique(CompiledOutputPlanningRule) - case "pre-mortem", "pre_mortem", "premortem": + case "premortem": appendUnique(CompiledOutputPremortemCheck) case "constraint", "constraints": appendUnique(CompiledOutputConstraint) diff --git a/cli/internal/ports/inmemory_finding_compiler_test.go b/cli/internal/ports/inmemory_finding_compiler_test.go index 8f6d3fb0c..60495bd75 100644 --- a/cli/internal/ports/inmemory_finding_compiler_test.go +++ b/cli/internal/ports/inmemory_finding_compiler_test.go @@ -36,7 +36,7 @@ func replayReadyDetectorEvidence() *DetectorEvidence { } } -// An ADVISORY finding (no detector metadata) defaults to plan + pre-mortem only; +// An ADVISORY finding (no detector metadata) defaults to plan + premortem only; // the constraint is skipped, not emitted as a dead artifact (EM-ENF property: // constraint only with valid mechanical detector metadata). func TestInMemoryFindingCompiler_AdvisoryDefaultEmitsTwoTargets(t *testing.T) { @@ -46,7 +46,7 @@ func TestInMemoryFindingCompiler_AdvisoryDefaultEmitsTwoTargets(t *testing.T) { t.Fatalf("Compile: %v", err) } if len(got) != 2 { - t.Fatalf("advisory default emitted %d outputs, want 2 (plan + pre-mortem)", len(got)) + t.Fatalf("advisory default emitted %d outputs, want 2 (plan + premortem)", len(got)) } if contains(collectKinds(got), CompiledOutputConstraint) { t.Fatalf("advisory finding must emit no constraint; got %v", collectKinds(got)) diff --git a/cli/internal/ports/inmemory_hypothesis_ledger.go b/cli/internal/ports/inmemory_hypothesis_ledger.go deleted file mode 100644 index 1edf77026..000000000 --- a/cli/internal/ports/inmemory_hypothesis_ledger.go +++ /dev/null @@ -1,102 +0,0 @@ -// practices: [hexagonal-architecture, ddd-bounded-context] -package ports - -import ( - "context" - "errors" - "fmt" - "sync" -) - -// InMemoryHypothesisLedger is a HypothesisLedgerPort backed by an -// append-ordered in-memory slice. It is intended for tests and -// dry-run loop orchestration that needs durable-looking hypothesis -// behavior without touching .agents/evolve/hypotheses.jsonl. -type InMemoryHypothesisLedger struct { - mu sync.Mutex - records []HypothesisRecord - byID map[string]int -} - -// NewInMemoryHypothesisLedger returns an adapter seeded with the -// given records. Seed records are copied defensively; non-empty seed -// IDs are indexed so later Append calls still reject duplicates. -func NewInMemoryHypothesisLedger(seed []HypothesisRecord) *InMemoryHypothesisLedger { - records := make([]HypothesisRecord, len(seed)) - byID := make(map[string]int, len(seed)) - for i, record := range seed { - records[i] = cloneHypothesisRecord(record) - if record.ID != "" { - byID[record.ID] = i - } - } - return &InMemoryHypothesisLedger{records: records, byID: byID} -} - -// Append stores record at the end of the ledger, rejecting empty and -// duplicate IDs. -func (l *InMemoryHypothesisLedger) Append(ctx context.Context, record HypothesisRecord) (HypothesisRecord, error) { - if err := ctx.Err(); err != nil { - return HypothesisRecord{}, err - } - if record.ID == "" { - return HypothesisRecord{}, errors.New("ports: HypothesisLedger.Append requires ID") - } - - l.mu.Lock() - defer l.mu.Unlock() - - if _, exists := l.byID[record.ID]; exists { - return HypothesisRecord{}, fmt.Errorf("ports: HypothesisLedger.Append duplicate ID %q", record.ID) - } - - stored := cloneHypothesisRecord(record) - l.byID[stored.ID] = len(l.records) - l.records = append(l.records, stored) - return cloneHypothesisRecord(stored), nil -} - -// List returns all records in append order. -func (l *InMemoryHypothesisLedger) List(ctx context.Context) ([]HypothesisRecord, error) { - if err := ctx.Err(); err != nil { - return nil, err - } - - l.mu.Lock() - defer l.mu.Unlock() - - out := make([]HypothesisRecord, len(l.records)) - for i, record := range l.records { - out[i] = cloneHypothesisRecord(record) - } - return out, nil -} - -// Find returns a record by ID. Unknown IDs are not errors. -func (l *InMemoryHypothesisLedger) Find(ctx context.Context, id string) (HypothesisRecord, bool, error) { - if err := ctx.Err(); err != nil { - return HypothesisRecord{}, false, err - } - if id == "" { - return HypothesisRecord{}, false, errors.New("ports: HypothesisLedger.Find requires ID") - } - - l.mu.Lock() - defer l.mu.Unlock() - - idx, exists := l.byID[id] - if !exists { - return HypothesisRecord{}, false, nil - } - return cloneHypothesisRecord(l.records[idx]), true, nil -} - -func cloneHypothesisRecord(record HypothesisRecord) HypothesisRecord { - if record.Evidence != nil { - record.Evidence = append([]string(nil), record.Evidence...) - } - return record -} - -// Compile-time assertion: InMemoryHypothesisLedger satisfies the port. -var _ HypothesisLedgerPort = (*InMemoryHypothesisLedger)(nil) diff --git a/cli/internal/ports/inmemory_hypothesis_ledger_test.go b/cli/internal/ports/inmemory_hypothesis_ledger_test.go deleted file mode 100644 index 51eb677df..000000000 --- a/cli/internal/ports/inmemory_hypothesis_ledger_test.go +++ /dev/null @@ -1,153 +0,0 @@ -// practices: [hexagonal-architecture, tdd] -package ports - -import ( - "context" - "errors" - "strings" - "testing" -) - -func TestInMemoryHypothesisLedger_AppendListAndFind(t *testing.T) { - ledger := NewInMemoryHypothesisLedger(nil) - - got, err := ledger.Append(context.Background(), HypothesisRecord{ - ID: "H1", - CycleLanded: 10, - CheckAtCycle: 13, - Patch: "route discovery through explicit phase context", - Hypothesis: "token pressure falls", - Measure: "compare prompt tokens", - Verdict: HypothesisVerdictPending, - Evidence: []string{"baseline=100"}, - }) - if err != nil { - t.Fatalf("Append: %v", err) - } - if got.ID != "H1" { - t.Fatalf("Append ID = %q, want H1", got.ID) - } - - records, err := ledger.List(context.Background()) - if err != nil { - t.Fatalf("List: %v", err) - } - if len(records) != 1 { - t.Fatalf("List len = %d, want 1", len(records)) - } - if records[0].Patch != "route discovery through explicit phase context" { - t.Fatalf("Patch = %q, want stored patch", records[0].Patch) - } - - found, ok, err := ledger.Find(context.Background(), "H1") - if err != nil { - t.Fatalf("Find: %v", err) - } - if !ok { - t.Fatal("Find ok = false, want true") - } - if found.Measure != "compare prompt tokens" { - t.Fatalf("Measure = %q, want compare prompt tokens", found.Measure) - } -} - -func TestInMemoryHypothesisLedger_PreservesAppendOrder(t *testing.T) { - ledger := NewInMemoryHypothesisLedger(nil) - for _, id := range []string{"H1", "H2", "H3"} { - if _, err := ledger.Append(context.Background(), HypothesisRecord{ID: id}); err != nil { - t.Fatal(err) - } - } - records, err := ledger.List(context.Background()) - if err != nil { - t.Fatal(err) - } - for i, want := range []string{"H1", "H2", "H3"} { - if records[i].ID != want { - t.Fatalf("records[%d].ID = %q, want %q", i, records[i].ID, want) - } - } -} - -func TestInMemoryHypothesisLedger_RejectsEmptyAndDuplicateIDs(t *testing.T) { - ledger := NewInMemoryHypothesisLedger(nil) - if _, err := ledger.Append(context.Background(), HypothesisRecord{}); err == nil { - t.Fatal("expected empty-ID error, got nil") - } - if _, err := ledger.Append(context.Background(), HypothesisRecord{ID: "H1"}); err != nil { - t.Fatal(err) - } - _, err := ledger.Append(context.Background(), HypothesisRecord{ID: "H1"}) - if err == nil { - t.Fatal("expected duplicate-ID error, got nil") - } - if !strings.Contains(err.Error(), "duplicate") { - t.Fatalf("error = %v, want duplicate substring", err) - } -} - -func TestInMemoryHypothesisLedger_FindUnknownReturnsFalse(t *testing.T) { - ledger := NewInMemoryHypothesisLedger([]HypothesisRecord{{ID: "H1"}}) - record, ok, err := ledger.Find(context.Background(), "missing") - if err != nil { - t.Fatal(err) - } - if ok { - t.Fatal("Find ok = true, want false") - } - if record.ID != "" { - t.Fatalf("record.ID = %q, want zero-value", record.ID) - } -} - -func TestInMemoryHypothesisLedger_SeedDuplicateStillRejected(t *testing.T) { - ledger := NewInMemoryHypothesisLedger([]HypothesisRecord{{ID: "H1"}}) - _, err := ledger.Append(context.Background(), HypothesisRecord{ID: "H1"}) - if err == nil { - t.Fatal("expected duplicate-ID error against seed, got nil") - } -} - -func TestInMemoryHypothesisLedger_ReturnsDefensiveCopies(t *testing.T) { - ledger := NewInMemoryHypothesisLedger(nil) - input := HypothesisRecord{ID: "H1", Evidence: []string{"original"}} - got, err := ledger.Append(context.Background(), input) - if err != nil { - t.Fatal(err) - } - input.Evidence[0] = "mutated-input" - got.Evidence[0] = "mutated-return" - - records, err := ledger.List(context.Background()) - if err != nil { - t.Fatal(err) - } - records[0].Evidence[0] = "mutated-list" - - found, ok, err := ledger.Find(context.Background(), "H1") - if err != nil { - t.Fatal(err) - } - if !ok { - t.Fatal("Find ok = false, want true") - } - if found.Evidence[0] != "original" { - t.Fatalf("stored Evidence[0] = %q, want original", found.Evidence[0]) - } -} - -func TestInMemoryHypothesisLedger_HonorsContextCancellation(t *testing.T) { - ledger := NewInMemoryHypothesisLedger(nil) - ctx, cancel := context.WithCancel(context.Background()) - cancel() - - if _, err := ledger.Append(ctx, HypothesisRecord{ID: "H1"}); !errors.Is(err, context.Canceled) { - t.Fatalf("Append error = %v, want context.Canceled", err) - } - if _, err := ledger.List(ctx); !errors.Is(err, context.Canceled) { - t.Fatalf("List error = %v, want context.Canceled", err) - } - if _, _, err := ledger.Find(ctx, "H1"); !errors.Is(err, context.Canceled) { - t.Fatalf("Find error = %v, want context.Canceled", err) - } -} diff --git a/cli/internal/ports/inmemory_loop_reader.go b/cli/internal/ports/inmemory_loop_reader.go deleted file mode 100644 index fe3f66750..000000000 --- a/cli/internal/ports/inmemory_loop_reader.go +++ /dev/null @@ -1,75 +0,0 @@ -// practices: [hexagonal-architecture, ddd-bounded-context] -package ports - -import "context" - -// InMemoryLoopReader is a LoopReaderPort backed by a fixed slice of -// CycleEntry records the adapter returns from the read methods. The -// entries SHOULD be ordered by Number ascending; the adapter does not -// re-sort, so callers that pass out-of-order entries will get -// surprising Latest results. -type InMemoryLoopReader struct { - entries []CycleEntry -} - -// NewInMemoryLoopReader returns an adapter over the given entries. -// The caller's slice is retained (not copied) so callers must not -// mutate it after construction. Nil entries is safe. -func NewInMemoryLoopReader(entries []CycleEntry) *InMemoryLoopReader { - return &InMemoryLoopReader{entries: entries} -} - -// Latest returns the entry with the highest Number. Empty ledger → -// zero-value CycleEntry + nil error. -func (r *InMemoryLoopReader) Latest(ctx context.Context) (CycleEntry, error) { - if err := ctx.Err(); err != nil { - return CycleEntry{}, err - } - if len(r.entries) == 0 { - return CycleEntry{}, nil - } - best := r.entries[0] - for _, e := range r.entries[1:] { - if e.Number > best.Number { - best = e - } - } - return best, nil -} - -// Range returns entries whose Number is in [start, end] (inclusive). -func (r *InMemoryLoopReader) Range(ctx context.Context, start, end int) ([]CycleEntry, error) { - if err := ctx.Err(); err != nil { - return nil, err - } - out := make([]CycleEntry, 0) - for _, e := range r.entries { - if e.Number >= start && e.Number <= end { - out = append(out, e) - } - } - return out, nil -} - -// IdleStreak returns the trailing count of entries (from the slice -// end) whose Result is "idle" or "unchanged". The slice is assumed -// to be Number-ascending so the "trailing" end is the most-recent -// entry. Mixed-order callers get undefined results. -func (r *InMemoryLoopReader) IdleStreak(ctx context.Context) (int, error) { - if err := ctx.Err(); err != nil { - return 0, err - } - streak := 0 - for i := len(r.entries) - 1; i >= 0; i-- { - switch r.entries[i].Result { - case "idle", "unchanged": - streak++ - default: - return streak, nil - } - } - return streak, nil -} - -// Compile-time assertion: InMemoryLoopReader satisfies the port. -var _ LoopReaderPort = (*InMemoryLoopReader)(nil) diff --git a/cli/internal/ports/inmemory_loop_reader_test.go b/cli/internal/ports/inmemory_loop_reader_test.go deleted file mode 100644 index 4b59c8e2d..000000000 --- a/cli/internal/ports/inmemory_loop_reader_test.go +++ /dev/null @@ -1,135 +0,0 @@ -// practices: [hexagonal-architecture, tdd] -package ports - -import ( - "context" - "errors" - "testing" -) - -// Sibling pattern: inmemory_ci_status_test.go (cycle 100). Same shape. - -func sampleCycleEntries() []CycleEntry { - return []CycleEntry{ - {Number: 1, Mode: "stabilization", Result: "improved", Commit: "aaaa"}, - {Number: 2, Mode: "feature", Result: "improved", Commit: "bbbb"}, - {Number: 3, Mode: "idle", Result: "idle", Commit: ""}, - {Number: 4, Mode: "idle", Result: "unchanged", Commit: ""}, - } -} - -func TestInMemoryLoopReader_LatestReturnsHighestNumber(t *testing.T) { - r := NewInMemoryLoopReader(sampleCycleEntries()) - v, err := r.Latest(context.Background()) - if err != nil { - t.Fatal(err) - } - if v.Number != 4 { - t.Fatalf("Number = %d, want 4", v.Number) - } - if v.Result != "unchanged" { - t.Fatalf("Result = %q, want 'unchanged'", v.Result) - } -} - -func TestInMemoryLoopReader_LatestEmptyLedgerReturnsZeroValue(t *testing.T) { - r := NewInMemoryLoopReader(nil) - v, err := r.Latest(context.Background()) - if err != nil { - t.Fatal(err) - } - if v.Number != 0 || v.Mode != "" || v.Result != "" { - t.Fatalf("expected zero-value, got %+v", v) - } -} - -func TestInMemoryLoopReader_RangeInclusive(t *testing.T) { - r := NewInMemoryLoopReader(sampleCycleEntries()) - got, err := r.Range(context.Background(), 2, 3) - if err != nil { - t.Fatal(err) - } - if len(got) != 2 { - t.Fatalf("len = %d, want 2", len(got)) - } - if got[0].Number != 2 || got[1].Number != 3 { - t.Fatalf("got numbers = [%d, %d], want [2, 3]", got[0].Number, got[1].Number) - } -} - -func TestInMemoryLoopReader_RangeOutOfBoundsReturnsEmpty(t *testing.T) { - r := NewInMemoryLoopReader(sampleCycleEntries()) - got, err := r.Range(context.Background(), 100, 200) - if err != nil { - t.Fatal(err) - } - if got == nil { - t.Fatal("Range returned nil; should be non-nil empty slice") - } - if len(got) != 0 { - t.Fatalf("len = %d, want 0", len(got)) - } -} - -func TestInMemoryLoopReader_IdleStreakCountsTrailing(t *testing.T) { - r := NewInMemoryLoopReader(sampleCycleEntries()) - got, err := r.IdleStreak(context.Background()) - if err != nil { - t.Fatal(err) - } - // Sample ends with idle, unchanged → streak of 2 - if got != 2 { - t.Fatalf("IdleStreak = %d, want 2", got) - } -} - -func TestInMemoryLoopReader_IdleStreakZeroWhenLastIsProductive(t *testing.T) { - r := NewInMemoryLoopReader([]CycleEntry{ - {Number: 1, Result: "idle"}, - {Number: 2, Result: "improved"}, // most recent - }) - got, err := r.IdleStreak(context.Background()) - if err != nil { - t.Fatal(err) - } - if got != 0 { - t.Fatalf("IdleStreak = %d, want 0", got) - } -} - -func TestInMemoryLoopReader_IdleStreakAllIdleReturnsTotal(t *testing.T) { - r := NewInMemoryLoopReader([]CycleEntry{ - {Number: 1, Result: "idle"}, - {Number: 2, Result: "unchanged"}, - {Number: 3, Result: "idle"}, - }) - got, err := r.IdleStreak(context.Background()) - if err != nil { - t.Fatal(err) - } - if got != 3 { - t.Fatalf("IdleStreak = %d, want 3", got) - } -} - -func TestInMemoryLoopReader_HonorsContextCancellation(t *testing.T) { - r := NewInMemoryLoopReader(nil) - ctx, cancel := context.WithCancel(context.Background()) - cancel() - for _, fn := range []struct { - name string - call func() error - }{ - {"Latest", func() error { _, err := r.Latest(ctx); return err }}, - {"Range", func() error { _, err := r.Range(ctx, 0, 1); return err }}, - {"IdleStreak", func() error { _, err := r.IdleStreak(ctx); return err }}, - } { - err := fn.call() - if err == nil { - t.Fatalf("%s: expected cancellation error, got nil", fn.name) - } - if !errors.Is(err, context.Canceled) { - t.Fatalf("%s error = %v, want context.Canceled", fn.name, err) - } - } -} diff --git a/cli/internal/ports/inmemory_loop_writer.go b/cli/internal/ports/inmemory_loop_writer.go deleted file mode 100644 index c2bda2fec..000000000 --- a/cli/internal/ports/inmemory_loop_writer.go +++ /dev/null @@ -1,71 +0,0 @@ -// practices: [hexagonal-architecture, ddd-bounded-context] -package ports - -import ( - "context" - "fmt" - "sync" -) - -// InMemoryLoopWriter is a LoopWriterPort backed by an in-memory slice. -// Auto-assigns Number when entry.Number is 0 (next = max+1). Rejects -// duplicate Numbers. Thread-safe via mutex. -type InMemoryLoopWriter struct { - mu sync.Mutex - entries []CycleEntry -} - -// NewInMemoryLoopWriter returns an empty writer. SeedEntries can be -// supplied as the initial state (e.g. to combine with InMemoryLoopReader -// in tests that need a read-write fixture). -func NewInMemoryLoopWriter(seed []CycleEntry) *InMemoryLoopWriter { - cp := make([]CycleEntry, len(seed)) - copy(cp, seed) - return &InMemoryLoopWriter{entries: cp} -} - -// Append records the entry. See port contract for Number-assignment + -// duplicate-rejection semantics. -func (w *InMemoryLoopWriter) Append(ctx context.Context, entry CycleEntry) (CycleEntry, error) { - if err := ctx.Err(); err != nil { - return CycleEntry{}, err - } - w.mu.Lock() - defer w.mu.Unlock() - if entry.Number == 0 { - entry.Number = w.nextNumberLocked() - } else { - for _, e := range w.entries { - if e.Number == entry.Number { - return CycleEntry{}, fmt.Errorf("ports: LoopWriter.Append duplicate cycle Number %d", entry.Number) - } - } - } - w.entries = append(w.entries, entry) - return entry, nil -} - -// Snapshot returns a defensive copy of the current entries. Test-only -// helper; not part of the port contract. -func (w *InMemoryLoopWriter) Snapshot() []CycleEntry { - w.mu.Lock() - defer w.mu.Unlock() - out := make([]CycleEntry, len(w.entries)) - copy(out, w.entries) - return out -} - -// nextNumberLocked computes the next sequential Number. Assumes the -// caller holds w.mu. -func (w *InMemoryLoopWriter) nextNumberLocked() int { - maxN := 0 - for _, e := range w.entries { - if e.Number > maxN { - maxN = e.Number - } - } - return maxN + 1 -} - -// Compile-time assertion: InMemoryLoopWriter satisfies the port. -var _ LoopWriterPort = (*InMemoryLoopWriter)(nil) diff --git a/cli/internal/ports/inmemory_loop_writer_test.go b/cli/internal/ports/inmemory_loop_writer_test.go deleted file mode 100644 index e713dd7c9..000000000 --- a/cli/internal/ports/inmemory_loop_writer_test.go +++ /dev/null @@ -1,110 +0,0 @@ -// practices: [hexagonal-architecture, tdd] -package ports - -import ( - "context" - "errors" - "strings" - "testing" -) - -// Sibling pattern: inmemory_loop_reader_test.go (cycle 102). - -func TestInMemoryLoopWriter_AppendAutoAssignsNumberWhenZero(t *testing.T) { - w := NewInMemoryLoopWriter(nil) - got, err := w.Append(context.Background(), CycleEntry{Mode: "feature", Result: "improved"}) - if err != nil { - t.Fatalf("Append: %v", err) - } - if got.Number != 1 { - t.Fatalf("Number = %d, want 1 (auto-assigned)", got.Number) - } - got, err = w.Append(context.Background(), CycleEntry{Mode: "next", Result: "improved"}) - if err != nil { - t.Fatal(err) - } - if got.Number != 2 { - t.Fatalf("second auto-assigned Number = %d, want 2", got.Number) - } -} - -func TestInMemoryLoopWriter_AppendHonorsExplicitNumber(t *testing.T) { - w := NewInMemoryLoopWriter(nil) - got, err := w.Append(context.Background(), CycleEntry{Number: 42, Result: "improved"}) - if err != nil { - t.Fatal(err) - } - if got.Number != 42 { - t.Fatalf("Number = %d, want 42 (explicit)", got.Number) - } - // Next auto-assigned should be 43 - got, err = w.Append(context.Background(), CycleEntry{Result: "improved"}) - if err != nil { - t.Fatal(err) - } - if got.Number != 43 { - t.Fatalf("auto-assigned after explicit = %d, want 43", got.Number) - } -} - -func TestInMemoryLoopWriter_AppendRejectsDuplicateNumber(t *testing.T) { - w := NewInMemoryLoopWriter(nil) - if _, err := w.Append(context.Background(), CycleEntry{Number: 5, Result: "improved"}); err != nil { - t.Fatal(err) - } - _, err := w.Append(context.Background(), CycleEntry{Number: 5, Result: "improved"}) - if err == nil { - t.Fatal("expected duplicate-Number error, got nil") - } - if !strings.Contains(err.Error(), "duplicate") { - t.Fatalf("error = %v, want substring 'duplicate'", err) - } -} - -func TestInMemoryLoopWriter_SeededEntriesUsedAsInitialState(t *testing.T) { - seed := []CycleEntry{ - {Number: 1, Result: "improved"}, - {Number: 2, Result: "improved"}, - {Number: 3, Result: "idle"}, - } - w := NewInMemoryLoopWriter(seed) - got, err := w.Append(context.Background(), CycleEntry{Result: "improved"}) - if err != nil { - t.Fatal(err) - } - if got.Number != 4 { - t.Fatalf("auto-assigned after seed = %d, want 4", got.Number) - } - snap := w.Snapshot() - if len(snap) != 4 { - t.Fatalf("Snapshot len = %d, want 4", len(snap)) - } -} - -func TestInMemoryLoopWriter_HonorsContextCancellation(t *testing.T) { - w := NewInMemoryLoopWriter(nil) - ctx, cancel := context.WithCancel(context.Background()) - cancel() - _, err := w.Append(ctx, CycleEntry{Result: "improved"}) - if err == nil { - t.Fatal("expected cancellation error, got nil") - } - if !errors.Is(err, context.Canceled) { - t.Fatalf("error = %v, want context.Canceled", err) - } -} - -func TestInMemoryLoopWriter_SnapshotIsDefensiveCopy(t *testing.T) { - w := NewInMemoryLoopWriter(nil) - _, _ = w.Append(context.Background(), CycleEntry{Number: 1, Result: "improved"}) - snap1 := w.Snapshot() - if len(snap1) != 1 { - t.Fatalf("len = %d, want 1", len(snap1)) - } - // Mutate the returned snapshot — should not affect writer state - snap1[0].Result = "mutated" - snap2 := w.Snapshot() - if snap2[0].Result != "improved" { - t.Fatalf("Snapshot is not defensive: got mutated state %q", snap2[0].Result) - } -} diff --git a/cli/internal/ports/inmemory_operator.go b/cli/internal/ports/inmemory_operator.go deleted file mode 100644 index 9ee05f500..000000000 --- a/cli/internal/ports/inmemory_operator.go +++ /dev/null @@ -1,51 +0,0 @@ -// practices: [hexagonal-architecture, ddd-bounded-context] -package ports - -import ( - "context" - "errors" - "sync" -) - -// InMemoryOperator is an OperatorPort backed by an in-memory slice. -// Thread-safe via mutex. List returns most-recent first. -type InMemoryOperator struct { - mu sync.Mutex - intents []OperatorIntent -} - -// NewInMemoryOperator returns an empty adapter. -func NewInMemoryOperator() *InMemoryOperator { - return &InMemoryOperator{} -} - -// Record appends intent. Empty Kind is rejected. -func (a *InMemoryOperator) Record(ctx context.Context, intent OperatorIntent) error { - if err := ctx.Err(); err != nil { - return err - } - if intent.Kind == "" { - return errors.New("ports: OperatorIntent.Kind required") - } - a.mu.Lock() - defer a.mu.Unlock() - a.intents = append(a.intents, intent) - return nil -} - -// List returns all recorded intents, most-recent first. -func (a *InMemoryOperator) List(ctx context.Context) ([]OperatorIntent, error) { - if err := ctx.Err(); err != nil { - return nil, err - } - a.mu.Lock() - defer a.mu.Unlock() - out := make([]OperatorIntent, 0, len(a.intents)) - for i := len(a.intents) - 1; i >= 0; i-- { - out = append(out, a.intents[i]) - } - return out, nil -} - -// Compile-time assertion: InMemoryOperator satisfies the port. -var _ OperatorPort = (*InMemoryOperator)(nil) diff --git a/cli/internal/ports/inmemory_operator_test.go b/cli/internal/ports/inmemory_operator_test.go deleted file mode 100644 index f33a5d06a..000000000 --- a/cli/internal/ports/inmemory_operator_test.go +++ /dev/null @@ -1,88 +0,0 @@ -// practices: [hexagonal-architecture, tdd] -package ports - -import ( - "context" - "errors" - "testing" -) - -// Sibling pattern: inmemory_loop_writer_test.go (cycle 103). - -func TestInMemoryOperator_RecordAcceptsValidIntent(t *testing.T) { - a := NewInMemoryOperator() - err := a.Record(context.Background(), OperatorIntent{Kind: "halt", Subject: "soc-test", Note: "investigating"}) - if err != nil { - t.Fatalf("Record: %v", err) - } - list, _ := a.List(context.Background()) - if len(list) != 1 { - t.Fatalf("len = %d, want 1", len(list)) - } - if list[0].Kind != "halt" { - t.Fatalf("Kind = %q, want halt", list[0].Kind) - } -} - -func TestInMemoryOperator_RecordRejectsEmptyKind(t *testing.T) { - a := NewInMemoryOperator() - err := a.Record(context.Background(), OperatorIntent{Subject: "x"}) - if err == nil { - t.Fatal("expected error on empty Kind, got nil") - } -} - -func TestInMemoryOperator_ListReturnsMostRecentFirst(t *testing.T) { - a := NewInMemoryOperator() - _ = a.Record(context.Background(), OperatorIntent{Kind: "first"}) - _ = a.Record(context.Background(), OperatorIntent{Kind: "second"}) - _ = a.Record(context.Background(), OperatorIntent{Kind: "third"}) - list, _ := a.List(context.Background()) - if len(list) != 3 { - t.Fatalf("len = %d, want 3", len(list)) - } - if list[0].Kind != "third" || list[2].Kind != "first" { - t.Fatalf("order = %v, want [third, second, first]", - []string{list[0].Kind, list[1].Kind, list[2].Kind}) - } -} - -func TestInMemoryOperator_ListEmptyIsNonNil(t *testing.T) { - a := NewInMemoryOperator() - list, err := a.List(context.Background()) - if err != nil { - t.Fatal(err) - } - if list == nil { - t.Fatal("List returned nil; should be non-nil empty slice") - } - if len(list) != 0 { - t.Fatalf("len = %d, want 0", len(list)) - } -} - -func TestInMemoryOperator_HonorsContextCancellation(t *testing.T) { - a := NewInMemoryOperator() - ctx, cancel := context.WithCancel(context.Background()) - cancel() - if err := a.Record(ctx, OperatorIntent{Kind: "halt"}); err == nil { - t.Fatal("Record: expected cancellation error, got nil") - } else if !errors.Is(err, context.Canceled) { - t.Fatalf("Record error = %v, want context.Canceled", err) - } - if _, err := a.List(ctx); err == nil { - t.Fatal("List: expected cancellation error, got nil") - } else if !errors.Is(err, context.Canceled) { - t.Fatalf("List error = %v, want context.Canceled", err) - } -} - -func TestInMemoryOperator_KindSubjectNoteAllPreserved(t *testing.T) { - a := NewInMemoryOperator() - in := OperatorIntent{Kind: "rescope", Subject: "soc-2c1p", Note: "split into BC ports"} - _ = a.Record(context.Background(), in) - list, _ := a.List(context.Background()) - if list[0] != in { - t.Fatalf("round-trip lost data: got %+v, want %+v", list[0], in) - } -} diff --git a/cli/internal/ports/inmemory_safety_policy.go b/cli/internal/ports/inmemory_safety_policy.go deleted file mode 100644 index 8bc807690..000000000 --- a/cli/internal/ports/inmemory_safety_policy.go +++ /dev/null @@ -1,46 +0,0 @@ -// practices: [hexagonal-architecture, ddd-bounded-context] -package ports - -import ( - "context" - "errors" - "fmt" -) - -// InMemorySafetyPolicy is a SafetyPolicyPort backed by fixed policy -// decisions. Unknown policies fail closed with BLOCK. -type InMemorySafetyPolicy struct { - decisions map[SafetyPolicyName]SafetyDecision -} - -func NewInMemorySafetyPolicy(decisions map[SafetyPolicyName]SafetyDecision) *InMemorySafetyPolicy { - if decisions == nil { - decisions = map[SafetyPolicyName]SafetyDecision{} - } - copied := make(map[SafetyPolicyName]SafetyDecision, len(decisions)) - for key, decision := range decisions { - copied[key] = decision - } - return &InMemorySafetyPolicy{decisions: copied} -} - -func (p *InMemorySafetyPolicy) Evaluate(ctx context.Context, req SafetyPolicyRequest) (SafetyDecision, error) { - if err := ctx.Err(); err != nil { - return SafetyDecision{}, err - } - if req.Policy == "" { - return SafetyDecision{}, errors.New("ports: SafetyPolicyPort.Evaluate policy required") - } - if decision, ok := p.decisions[req.Policy]; ok { - if decision.Reason == "" { - decision.Reason = fmt.Sprintf("policy %q matched configured decision", req.Policy) - } - return decision, nil - } - return SafetyDecision{ - Status: SafetyDecisionBlock, - Reason: fmt.Sprintf("unknown safety policy %q", req.Policy), - }, nil -} - -var _ SafetyPolicyPort = (*InMemorySafetyPolicy)(nil) diff --git a/cli/internal/ports/inmemory_safety_policy_test.go b/cli/internal/ports/inmemory_safety_policy_test.go deleted file mode 100644 index 610ec617f..000000000 --- a/cli/internal/ports/inmemory_safety_policy_test.go +++ /dev/null @@ -1,60 +0,0 @@ -// practices: [hexagonal-architecture, tdd] -package ports - -import ( - "context" - "errors" - "strings" - "testing" -) - -func TestInMemorySafetyPolicy_ReturnsConfiguredDecision(t *testing.T) { - policy := NewInMemorySafetyPolicy(map[SafetyPolicyName]SafetyDecision{ - "git.destructive": {Status: SafetyDecisionBlock, Reason: "reset hard blocked"}, - }) - decision, err := policy.Evaluate(context.Background(), SafetyPolicyRequest{Policy: "git.destructive"}) - if err != nil { - t.Fatal(err) - } - if decision.Status != SafetyDecisionBlock { - t.Fatalf("Status = %q, want BLOCK", decision.Status) - } - if decision.Reason != "reset hard blocked" { - t.Fatalf("Reason = %q, want configured reason", decision.Reason) - } -} - -func TestInMemorySafetyPolicy_UnknownPolicyFailsClosed(t *testing.T) { - policy := NewInMemorySafetyPolicy(nil) - decision, err := policy.Evaluate(context.Background(), SafetyPolicyRequest{Policy: "scope.edit"}) - if err != nil { - t.Fatal(err) - } - if decision.Status != SafetyDecisionBlock { - t.Fatalf("Status = %q, want BLOCK", decision.Status) - } - if !strings.Contains(decision.Reason, "scope.edit") { - t.Fatalf("Reason = %q, want policy name", decision.Reason) - } -} - -func TestInMemorySafetyPolicy_RejectsEmptyPolicy(t *testing.T) { - policy := NewInMemorySafetyPolicy(nil) - _, err := policy.Evaluate(context.Background(), SafetyPolicyRequest{}) - if err == nil { - t.Fatal("expected error for empty policy") - } - if !strings.Contains(err.Error(), "policy required") { - t.Fatalf("error = %v, want policy required", err) - } -} - -func TestInMemorySafetyPolicy_HonorsContextCancellation(t *testing.T) { - policy := NewInMemorySafetyPolicy(nil) - ctx, cancel := context.WithCancel(context.Background()) - cancel() - _, err := policy.Evaluate(ctx, SafetyPolicyRequest{Policy: "x"}) - if !errors.Is(err, context.Canceled) { - t.Fatalf("error = %v, want context.Canceled", err) - } -} diff --git a/cli/internal/ports/inmemory_tracker.go b/cli/internal/ports/inmemory_tracker.go deleted file mode 100644 index dfb38bf26..000000000 --- a/cli/internal/ports/inmemory_tracker.go +++ /dev/null @@ -1,118 +0,0 @@ -// practices: [hexagonal-architecture, ddd-bounded-context] -package ports - -import ( - "context" - "errors" - "fmt" - "strings" -) - -// InMemoryTracker is an IssueTracker test double. It records create calls and -// serves a fixed set of issues for the read paths so consumers can be exercised -// without a real `bd` binary. It is not safe for concurrent use. -type InMemoryTracker struct { - // Issues backs Ready/List/Show. ReadyStatuses gates which issues Ready - // returns; an empty set means every issue is ready. - Issues []Issue - ReadyStatuses map[string]bool - - // CreatedEpics and CreatedIssues record create calls in order. - CreatedEpics []Issue - CreatedIssues []Issue - - nextID int -} - -// NewInMemoryTracker returns an empty in-memory tracker. -func NewInMemoryTracker() *InMemoryTracker { - return &InMemoryTracker{ - ReadyStatuses: map[string]bool{}, - } -} - -// Mode reports the in-memory backend identity. -func (t *InMemoryTracker) Mode() string { return "memory" } - -// CreateEpic records an epic and returns a synthetic id. -func (t *InMemoryTracker) CreateEpic(ctx context.Context, title, body string) (string, error) { - if err := ctx.Err(); err != nil { - return "", err - } - if strings.TrimSpace(title) == "" { - return "", errors.New("ports: InMemoryTracker.CreateEpic title required") - } - t.nextID++ - id := fmt.Sprintf("epic-%d", t.nextID) - issue := Issue{ID: id, Title: title, Type: "epic", Status: "open"} - t.CreatedEpics = append(t.CreatedEpics, issue) - t.Issues = append(t.Issues, issue) - return id, nil -} - -// CreateIssue records an issue under epicID and returns a synthetic id. -func (t *InMemoryTracker) CreateIssue(ctx context.Context, epicID, title, body string) (string, error) { - if err := ctx.Err(); err != nil { - return "", err - } - if strings.TrimSpace(title) == "" { - return "", errors.New("ports: InMemoryTracker.CreateIssue title required") - } - t.nextID++ - id := fmt.Sprintf("issue-%d", t.nextID) - issue := Issue{ID: id, Title: title, Type: "task", Status: "open"} - t.CreatedIssues = append(t.CreatedIssues, issue) - t.Issues = append(t.Issues, issue) - return id, nil -} - -// Ready returns the recorded issues whose status is in ReadyStatuses (or all -// issues when ReadyStatuses is empty). -func (t *InMemoryTracker) Ready(ctx context.Context) ([]Issue, error) { - if err := ctx.Err(); err != nil { - return nil, err - } - out := make([]Issue, 0, len(t.Issues)) - for _, issue := range t.Issues { - if len(t.ReadyStatuses) == 0 || t.ReadyStatuses[issue.Status] { - out = append(out, issue) - } - } - return out, nil -} - -// List returns the recorded issues that match filter. -func (t *InMemoryTracker) List(ctx context.Context, filter IssueFilter) ([]Issue, error) { - if err := ctx.Err(); err != nil { - return nil, err - } - out := make([]Issue, 0, len(t.Issues)) - for _, issue := range t.Issues { - if filter.Type != "" && issue.Type != filter.Type { - continue - } - if filter.Status != "" && issue.Status != filter.Status { - continue - } - out = append(out, issue) - if filter.Limit > 0 && len(out) >= filter.Limit { - break - } - } - return out, nil -} - -// Show returns the recorded issue with the given id, or an error when absent. -func (t *InMemoryTracker) Show(ctx context.Context, id string) (Issue, error) { - if err := ctx.Err(); err != nil { - return Issue{}, err - } - for _, issue := range t.Issues { - if issue.ID == id { - return issue, nil - } - } - return Issue{}, fmt.Errorf("ports: InMemoryTracker.Show: issue %q not found", id) -} - -var _ IssueTracker = (*InMemoryTracker)(nil) diff --git a/cli/internal/ports/inmemory_tracker_test.go b/cli/internal/ports/inmemory_tracker_test.go deleted file mode 100644 index 729e8b3ff..000000000 --- a/cli/internal/ports/inmemory_tracker_test.go +++ /dev/null @@ -1,148 +0,0 @@ -package ports - -import ( - "context" - "testing" -) - -func TestInMemoryTracker_Mode(t *testing.T) { - tr := NewInMemoryTracker() - if got := tr.Mode(); got != "memory" { - t.Errorf("Mode() = %q, want %q", got, "memory") - } -} - -func TestInMemoryTracker_CreateAndShow(t *testing.T) { - tr := NewInMemoryTracker() - ctx := context.Background() - - epicID, err := tr.CreateEpic(ctx, "Epic A", "body") - if err != nil { - t.Fatalf("CreateEpic() error = %v", err) - } - if epicID != "epic-1" { - t.Errorf("epicID = %q, want %q", epicID, "epic-1") - } - - issueID, err := tr.CreateIssue(ctx, epicID, "Task 1", "body") - if err != nil { - t.Fatalf("CreateIssue() error = %v", err) - } - if issueID != "issue-2" { - t.Errorf("issueID = %q, want %q", issueID, "issue-2") - } - - if len(tr.CreatedEpics) != 1 || len(tr.CreatedIssues) != 1 { - t.Fatalf("recorded epics=%d issues=%d, want 1/1", len(tr.CreatedEpics), len(tr.CreatedIssues)) - } - - got, err := tr.Show(ctx, epicID) - if err != nil { - t.Fatalf("Show() error = %v", err) - } - if got.Title != "Epic A" || got.Type != "epic" { - t.Errorf("Show() = %+v, want title=Epic A type=epic", got) - } -} - -func TestInMemoryTracker_CreateEmptyTitle(t *testing.T) { - tr := NewInMemoryTracker() - ctx := context.Background() - if _, err := tr.CreateEpic(ctx, " ", "body"); err == nil { - t.Error("CreateEpic(empty title) expected error, got nil") - } - if _, err := tr.CreateIssue(ctx, "", "", "body"); err == nil { - t.Error("CreateIssue(empty title) expected error, got nil") - } -} - -func TestInMemoryTracker_ShowNotFound(t *testing.T) { - tr := NewInMemoryTracker() - if _, err := tr.Show(context.Background(), "missing"); err == nil { - t.Error("Show(missing) expected error, got nil") - } -} - -func TestInMemoryTracker_Ready(t *testing.T) { - tr := NewInMemoryTracker() - tr.Issues = []Issue{ - {ID: "a", Status: "open"}, - {ID: "b", Status: "blocked"}, - {ID: "c", Status: "open"}, - } - tr.ReadyStatuses = map[string]bool{"open": true} - - got, err := tr.Ready(context.Background()) - if err != nil { - t.Fatalf("Ready() error = %v", err) - } - if len(got) != 2 { - t.Fatalf("Ready() returned %d, want 2", len(got)) - } - if got[0].ID != "a" || got[1].ID != "c" { - t.Errorf("Ready() ids = %q,%q, want a,c", got[0].ID, got[1].ID) - } -} - -func TestInMemoryTracker_ReadyAllWhenNoStatuses(t *testing.T) { - tr := NewInMemoryTracker() - tr.Issues = []Issue{{ID: "a", Status: "open"}, {ID: "b", Status: "x"}} - got, err := tr.Ready(context.Background()) - if err != nil { - t.Fatalf("Ready() error = %v", err) - } - if len(got) != 2 { - t.Errorf("Ready() returned %d, want 2 (no status filter)", len(got)) - } -} - -func TestInMemoryTracker_List(t *testing.T) { - tr := NewInMemoryTracker() - tr.Issues = []Issue{ - {ID: "e1", Type: "epic", Status: "open"}, - {ID: "t1", Type: "task", Status: "in_progress"}, - {ID: "t2", Type: "task", Status: "in_progress"}, - } - - tests := []struct { - name string - filter IssueFilter - wantIDs []string - }{ - {name: "by type", filter: IssueFilter{Type: "epic"}, wantIDs: []string{"e1"}}, - {name: "by status", filter: IssueFilter{Status: "in_progress"}, wantIDs: []string{"t1", "t2"}}, - {name: "with limit", filter: IssueFilter{Status: "in_progress", Limit: 1}, wantIDs: []string{"t1"}}, - {name: "no filter", filter: IssueFilter{}, wantIDs: []string{"e1", "t1", "t2"}}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := tr.List(context.Background(), tt.filter) - if err != nil { - t.Fatalf("List() error = %v", err) - } - if len(got) != len(tt.wantIDs) { - t.Fatalf("List() returned %d, want %d", len(got), len(tt.wantIDs)) - } - for i, id := range tt.wantIDs { - if got[i].ID != id { - t.Errorf("List()[%d].ID = %q, want %q", i, got[i].ID, id) - } - } - }) - } -} - -func TestInMemoryTracker_ContextCancelled(t *testing.T) { - tr := NewInMemoryTracker() - ctx, cancel := context.WithCancel(context.Background()) - cancel() - if _, err := tr.Ready(ctx); err == nil { - t.Error("Ready(cancelled ctx) expected error, got nil") - } - if _, err := tr.List(ctx, IssueFilter{}); err == nil { - t.Error("List(cancelled ctx) expected error, got nil") - } - if _, err := tr.Show(ctx, "x"); err == nil { - t.Error("Show(cancelled ctx) expected error, got nil") - } -} diff --git a/cli/internal/ports/inmemory_workspace.go b/cli/internal/ports/inmemory_workspace.go deleted file mode 100644 index d44de4941..000000000 --- a/cli/internal/ports/inmemory_workspace.go +++ /dev/null @@ -1,54 +0,0 @@ -// practices: [hexagonal-architecture, ddd-bounded-context] -package ports - -import ( - "context" - "errors" -) - -// InMemoryWorkspace is a WorkspacePort that records setup and cleanup -// calls in memory. -type InMemoryWorkspace struct { - Setups []WorkspaceRequest - Cleanups []WorkspaceRequest -} - -func NewInMemoryWorkspace() *InMemoryWorkspace { - return &InMemoryWorkspace{ - Setups: []WorkspaceRequest{}, - Cleanups: []WorkspaceRequest{}, - } -} - -func (w *InMemoryWorkspace) Setup(ctx context.Context, req WorkspaceRequest) (WorkspaceResult, error) { - if err := ctx.Err(); err != nil { - return WorkspaceResult{}, err - } - if req.WorkspaceID == "" { - return WorkspaceResult{}, errors.New("ports: WorkspacePort.Setup workspace id required") - } - w.Setups = append(w.Setups, cloneWorkspaceRequest(req)) - return WorkspaceResult{WorkspaceID: req.WorkspaceID, Path: req.Path, Status: "setup", Reason: "workspace setup recorded"}, nil -} - -func (w *InMemoryWorkspace) Cleanup(ctx context.Context, req WorkspaceRequest) (WorkspaceResult, error) { - if err := ctx.Err(); err != nil { - return WorkspaceResult{}, err - } - if req.WorkspaceID == "" { - return WorkspaceResult{}, errors.New("ports: WorkspacePort.Cleanup workspace id required") - } - w.Cleanups = append(w.Cleanups, cloneWorkspaceRequest(req)) - return WorkspaceResult{WorkspaceID: req.WorkspaceID, Path: req.Path, Status: "cleanup", Reason: "workspace cleanup recorded"}, nil -} - -func cloneWorkspaceRequest(req WorkspaceRequest) WorkspaceRequest { - metadata := make(map[string]string, len(req.Metadata)) - for key, value := range req.Metadata { - metadata[key] = value - } - req.Metadata = metadata - return req -} - -var _ WorkspacePort = (*InMemoryWorkspace)(nil) diff --git a/cli/internal/ports/inmemory_workspace_test.go b/cli/internal/ports/inmemory_workspace_test.go deleted file mode 100644 index 7fd273506..000000000 --- a/cli/internal/ports/inmemory_workspace_test.go +++ /dev/null @@ -1,64 +0,0 @@ -// practices: [hexagonal-architecture, tdd] -package ports - -import ( - "context" - "errors" - "strings" - "testing" -) - -func TestInMemoryWorkspace_RecordsSetupAndCleanup(t *testing.T) { - workspace := NewInMemoryWorkspace() - setup, err := workspace.Setup(context.Background(), WorkspaceRequest{WorkspaceID: "run-1", Path: "/tmp/run-1"}) - if err != nil { - t.Fatal(err) - } - cleanup, err := workspace.Cleanup(context.Background(), WorkspaceRequest{WorkspaceID: "run-1", Path: "/tmp/run-1"}) - if err != nil { - t.Fatal(err) - } - if setup.Status != "setup" || cleanup.Status != "cleanup" { - t.Fatalf("statuses = %q/%q, want setup/cleanup", setup.Status, cleanup.Status) - } - if len(workspace.Setups) != 1 || len(workspace.Cleanups) != 1 { - t.Fatalf("recorded setup/cleanup = %d/%d, want 1/1", len(workspace.Setups), len(workspace.Cleanups)) - } -} - -func TestInMemoryWorkspace_RejectsEmptyWorkspaceID(t *testing.T) { - workspace := NewInMemoryWorkspace() - _, err := workspace.Setup(context.Background(), WorkspaceRequest{}) - if err == nil { - t.Fatal("expected setup error for empty workspace id") - } - if !strings.Contains(err.Error(), "workspace id required") { - t.Fatalf("setup error = %v, want workspace id required", err) - } - _, err = workspace.Cleanup(context.Background(), WorkspaceRequest{}) - if err == nil { - t.Fatal("expected cleanup error for empty workspace id") - } -} - -func TestInMemoryWorkspace_CopiesMetadata(t *testing.T) { - workspace := NewInMemoryWorkspace() - req := WorkspaceRequest{WorkspaceID: "run-1", Metadata: map[string]string{"role": "lead"}} - if _, err := workspace.Setup(context.Background(), req); err != nil { - t.Fatal(err) - } - req.Metadata["role"] = "mutated" - if workspace.Setups[0].Metadata["role"] != "lead" { - t.Fatalf("metadata mutated to %q", workspace.Setups[0].Metadata["role"]) - } -} - -func TestInMemoryWorkspace_HonorsContextCancellation(t *testing.T) { - workspace := NewInMemoryWorkspace() - ctx, cancel := context.WithCancel(context.Background()) - cancel() - _, err := workspace.Setup(ctx, WorkspaceRequest{WorkspaceID: "run-1"}) - if !errors.Is(err, context.Canceled) { - t.Fatalf("error = %v, want context.Canceled", err) - } -} diff --git a/cli/internal/ports/loop_reader.go b/cli/internal/ports/loop_reader.go deleted file mode 100644 index f6ee9d5a5..000000000 --- a/cli/internal/ports/loop_reader.go +++ /dev/null @@ -1,61 +0,0 @@ -// practices: [hexagonal-architecture, ddd-bounded-context] -package ports - -import "context" - -// CycleEntry is one entry in the evolve loop's cycle ledger -// (.agents/evolve/cycle-history.jsonl). Number is the monotonic cycle -// counter; Mode names the run mode (e.g. "feature-scaffold-BC1", -// "healing"); Result is the outcome (improved/regressed/unchanged/ -// harvested/quarantined); Commit is the short SHA of the cycle's -// commit when one was made (empty for local-only cycles); Milestone -// is the operator-facing summary string. StartedAt is the cycle -// boundary timestamp (RFC3339, empty when absent on disk). Title -// is the short human-facing label that operators stamp onto each -// cycle entry (e.g. "Sweep dead code"); it is the most commonly -// consulted free-text field for cycle-history audits. -// -// Fields beyond Number/Mode/Result/Commit/Milestone are widened -// on-demand for known consumers (per the cycle-157 post-mortem at -// docs/learnings/2026-05-13-bc-ports-narrowness-postmortem.md). -// -// Trace is the optional XP/BDD/TDD evidence trace (soc-y5vh.9). It is -// nil for cycles logged before the trace field existed and for any -// cycle that does not record one; the `json:"trace,omitempty"` tag -// keeps those entries byte-identical in `ao loop history` output. -type CycleEntry struct { - Number int - Mode string - Result string - Commit string - Milestone string - StartedAt string - Title string - Trace *CycleTrace `json:"trace,omitempty"` -} - -// LoopReaderPort is the BC3 Loop read-side. Callers — evolve's -// cycle-recovery bootstrap in Step 0, the /post-mortem aggregator, -// the dream-loop compounding analyzer, and any future cycle-history -// auditor — depend on this port so they can read the evolve loop's -// state without depending directly on the local-only -// cycle-history.jsonl format. -// -// Contract: -// -// - Latest returns the highest-Number CycleEntry. When the ledger -// is empty, returns a zero-value CycleEntry + nil error. -// - Range returns entries [start, end] (inclusive). Empty range or -// out-of-bounds returns an empty slice + nil error. -// - IdleStreak returns the trailing count of entries whose Result -// is "idle" or "unchanged" (the dormancy-quasi-stop signal). -// - Context cancellation MUST be honored on a best-effort basis. -// -// See docs/contracts/ubiquitous-language.md (BC3 row) for the -// canonical Loop context surface. soc-y5vh epic tracks BC3 port -// extraction; this is the first port in that epic. -type LoopReaderPort interface { - Latest(ctx context.Context) (CycleEntry, error) - Range(ctx context.Context, start, end int) ([]CycleEntry, error) - IdleStreak(ctx context.Context) (int, error) -} diff --git a/cli/internal/ports/loop_trace.go b/cli/internal/ports/loop_trace.go deleted file mode 100644 index 54683a952..000000000 --- a/cli/internal/ports/loop_trace.go +++ /dev/null @@ -1,84 +0,0 @@ -// practices: [ddd-bounded-context, code-complete] -package ports - -// CycleTrace is the XP/BDD/TDD evidence trace for one evolve cycle. It -// captures the Mt. Olympus continuous-evolution kernel shape so a -// reviewer can reconstruct a cycle without reading the transcript: -// goal hypothesis -> selected gap -> Gherkin scenario (or an explicit -// exemption) -> first failing proof -> red evidence -> green evidence -// -> refactor note -> validation evidence -> ratchet action -> goal -// reshape decision. -// -// Trivial one-shot cycles set ExemptionReason and leave the evidence -// fields empty — see TraceCompleteness for the completeness rule. All -// fields are omitempty so a partially-recorded or exempt trace stays -// compact on disk. soc-y5vh.9 (epic soc-y5vh, BC3 Loop). -type CycleTrace struct { - GoalHypothesis string `json:"goal_hypothesis,omitempty"` - SelectedGap string `json:"selected_gap,omitempty"` - Gherkin string `json:"gherkin,omitempty"` - ExemptionReason string `json:"exemption_reason,omitempty"` - FirstFailingProof string `json:"first_failing_proof,omitempty"` - RedEvidence string `json:"red_evidence,omitempty"` - GreenEvidence string `json:"green_evidence,omitempty"` - RefactorNote string `json:"refactor_note,omitempty"` - ValidationEvidence string `json:"validation_evidence,omitempty"` - RatchetAction string `json:"ratchet_action,omitempty"` - GoalReshape string `json:"goal_reshape,omitempty"` - BeadID string `json:"bead_id,omitempty"` - AcceptanceExamples []string `json:"acceptance_examples,omitempty"` - ValidationCommands []string `json:"validation_commands,omitempty"` - CloseoutVerdict string `json:"closeout_verdict,omitempty"` -} - -// traceField pairs a kernel field's on-disk name with an accessor, so -// TraceCompleteness reports missing fields in kernel order. -type traceField struct { - name string - get func(*CycleTrace) string -} - -// requiredTraceFields are the ten evidence fields a non-exempt cycle -// must record, in kernel order. RefactorNote is required too — a -// no-op refactor is recorded as the literal "none", not omitted. -var requiredTraceFields = []traceField{ - {"goal_hypothesis", func(t *CycleTrace) string { return t.GoalHypothesis }}, - {"selected_gap", func(t *CycleTrace) string { return t.SelectedGap }}, - {"gherkin", func(t *CycleTrace) string { return t.Gherkin }}, - {"first_failing_proof", func(t *CycleTrace) string { return t.FirstFailingProof }}, - {"red_evidence", func(t *CycleTrace) string { return t.RedEvidence }}, - {"green_evidence", func(t *CycleTrace) string { return t.GreenEvidence }}, - {"refactor_note", func(t *CycleTrace) string { return t.RefactorNote }}, - {"validation_evidence", func(t *CycleTrace) string { return t.ValidationEvidence }}, - {"ratchet_action", func(t *CycleTrace) string { return t.RatchetAction }}, - {"goal_reshape", func(t *CycleTrace) string { return t.GoalReshape }}, -} - -// TraceCompleteness reports whether a CycleTrace satisfies the -// XP/BDD/TDD evidence discipline. A trace with a non-empty -// ExemptionReason is exempt (trivial one-shot cycles avoid false -// ceremony) and reports no missing fields. Otherwise every required -// evidence field must be non-empty; missing field names are returned -// in kernel order. A nil trace is not exempt and misses every field. -// -// This is a pure advisory helper. Per the soc-y5vh.9 non-goals it is -// NOT wired into any blocking gate — callers decide what to do with -// the result (e.g. surface it in a report, never to fail a build). -func TraceCompleteness(t *CycleTrace) (exempt bool, missing []string) { - if t == nil { - all := make([]string, len(requiredTraceFields)) - for i, f := range requiredTraceFields { - all[i] = f.name - } - return false, all - } - if t.ExemptionReason != "" { - return true, nil - } - for _, f := range requiredTraceFields { - if f.get(t) == "" { - missing = append(missing, f.name) - } - } - return false, missing -} diff --git a/cli/internal/ports/loop_trace_test.go b/cli/internal/ports/loop_trace_test.go deleted file mode 100644 index ed77926de..000000000 --- a/cli/internal/ports/loop_trace_test.go +++ /dev/null @@ -1,85 +0,0 @@ -// practices: [tdd, ddd-bounded-context] -package ports - -import ( - "reflect" - "testing" -) - -// fullTrace returns a CycleTrace with every evidence field populated — -// the shape a non-trivial evolve cycle records. -func fullTrace() *CycleTrace { - return &CycleTrace{ - GoalHypothesis: "lifting test-pass-rate raises fitness", - SelectedGap: "loop ports lack a trace field", - Gherkin: "Feature: trace\n Scenario: reviewer reconstructs a cycle", - FirstFailingProof: "go test ./internal/ports -run TraceCompleteness FAIL", - RedEvidence: "TestTraceCompleteness_FullTrace red: undefined CycleTrace", - GreenEvidence: "TestTraceCompleteness_FullTrace pass", - RefactorNote: "none", - ValidationEvidence: "go test ./... green", - RatchetAction: "ao ratchet record implement", - GoalReshape: "goal unchanged; gap closed", - } -} - -func TestTraceCompleteness_FullTraceIsComplete(t *testing.T) { - exempt, missing := TraceCompleteness(fullTrace()) - if exempt { - t.Errorf("full trace reported exempt; want not exempt") - } - if len(missing) != 0 { - t.Errorf("full trace missing = %v, want none", missing) - } -} - -func TestTraceCompleteness_NilTraceMissesEverything(t *testing.T) { - exempt, missing := TraceCompleteness(nil) - if exempt { - t.Errorf("nil trace reported exempt; want not exempt") - } - if len(missing) != 10 { - t.Errorf("nil trace missing %d fields, want 10: %v", len(missing), missing) - } -} - -func TestTraceCompleteness_ExemptionSkipsRequiredFields(t *testing.T) { - exempt, missing := TraceCompleteness(&CycleTrace{ - ExemptionReason: "trivial one-shot typo fix; no Gherkin or failing proof appropriate", - }) - if !exempt { - t.Errorf("trace with exemption reason reported not exempt; want exempt") - } - if len(missing) != 0 { - t.Errorf("exempt trace missing = %v, want none", missing) - } -} - -func TestTraceCompleteness_ReportsEachMissingField(t *testing.T) { - tr := fullTrace() - tr.RedEvidence = "" - tr.GoalReshape = "" - exempt, missing := TraceCompleteness(tr) - if exempt { - t.Errorf("partial trace reported exempt; want not exempt") - } - want := []string{"red_evidence", "goal_reshape"} - if !reflect.DeepEqual(missing, want) { - t.Errorf("missing = %v, want %v", missing, want) - } -} - -func TestTraceCompleteness_RefactorNoteIsRequired(t *testing.T) { - tr := fullTrace() - tr.RefactorNote = "" - _, missing := TraceCompleteness(tr) - found := false - for _, m := range missing { - if m == "refactor_note" { - found = true - } - } - if !found { - t.Errorf("empty refactor_note not flagged; missing = %v", missing) - } -} diff --git a/cli/internal/ports/loop_writer.go b/cli/internal/ports/loop_writer.go deleted file mode 100644 index 1b39cf797..000000000 --- a/cli/internal/ports/loop_writer.go +++ /dev/null @@ -1,27 +0,0 @@ -// practices: [hexagonal-architecture, ddd-bounded-context] -package ports - -import "context" - -// LoopWriterPort is the BC3 Loop write-side. Callers — evolve's -// Step 6 cycle-logger, the `ao evolve` v2 supervisor, and any future -// loop-event ingester — depend on this port so they can record cycle -// outcomes without depending directly on the local-only -// cycle-history.jsonl append. -// -// Contract: -// -// - Append MUST add the entry to the ledger. If entry.Number is 0, -// adapters MUST auto-assign the next sequential number; otherwise -// entry.Number is honored as-is. -// - Append MUST NOT allow duplicate Numbers — re-appending an entry -// with an existing Number returns a non-nil error. -// - Append returns the entry as recorded (with Number filled in if -// auto-assigned). -// - Context cancellation MUST be honored on a best-effort basis. -// -// See docs/contracts/ubiquitous-language.md (BC3 row). Sibling: -// LoopReaderPort (cycle 102 read-side). soc-y5vh epic. -type LoopWriterPort interface { - Append(ctx context.Context, entry CycleEntry) (CycleEntry, error) -} diff --git a/cli/internal/ports/mortem_writer_compatibility_test.go b/cli/internal/ports/mortem_writer_compatibility_test.go deleted file mode 100644 index 5463a23e1..000000000 --- a/cli/internal/ports/mortem_writer_compatibility_test.go +++ /dev/null @@ -1,24 +0,0 @@ -package ports - -import ( - "context" - "testing" -) - -func TestInMemoryFindingCompiler_PremortemAliasesWriteOneCanonicalArtifact(t *testing.T) { - out, err := NewInMemoryFindingCompiler().Compile(context.Background(), FindingArtifact{ - ID: "finding-mortem-writer", - Frontmatter: map[string]string{ - "compiler_targets": "premortem,pre-mortem,pre_mortem", - }, - }) - if err != nil { - t.Fatalf("Compile: %v", err) - } - if len(out) != 1 { - t.Fatalf("mortem aliases emitted %d artifacts, want exactly one", len(out)) - } - if out[0].Path != ".agents/premortem-checks/finding-mortem-writer.md" { - t.Fatalf("writer path = %q, want canonical premortem path", out[0].Path) - } -} diff --git a/cli/internal/ports/operator.go b/cli/internal/ports/operator.go deleted file mode 100644 index 27d4949e3..000000000 --- a/cli/internal/ports/operator.go +++ /dev/null @@ -1,38 +0,0 @@ -// practices: [hexagonal-architecture, ddd-bounded-context] -package ports - -import "context" - -// OperatorIntent captures one human-in-the-loop request to the factory. -// Kind names the intent shape (e.g. "rescope", "halt", "promote", -// "approve"); Subject is a free-form reference to what the intent is -// about (a bd ID, a commit SHA, a file path); Note is a free-form -// human-readable annotation. -type OperatorIntent struct { - Kind string - Subject string - Note string -} - -// OperatorPort is the BC4 Factory operator-facing surface. Callers — -// the /halt skill, /rescope skill, /handoff skill, and any future -// human-in-the-loop nudge — depend on this port so they can record -// operator intent into the factory event stream without coupling to -// a specific persistence backend. -// -// Contract: -// -// - Record MUST accept any non-empty Kind. Adapters MAY validate -// specific Kind values; the in-memory adapter accepts all. -// - Empty Kind is a structural-rejection error. -// - List returns all recorded intents, most-recent first. -// - Context cancellation MUST be honored on a best-effort basis. -// -// See docs/contracts/ubiquitous-language.md (BC4 row). soc-2klg epic -// tracks BC4 port extraction; this is the first port in that epic. -// Sibling: EventBusPort (next cycle) carries the same intents to -// downstream subscribers asynchronously. -type OperatorPort interface { - Record(ctx context.Context, intent OperatorIntent) error - List(ctx context.Context) ([]OperatorIntent, error) -} diff --git a/cli/internal/ports/review_lane.go b/cli/internal/ports/review_lane.go deleted file mode 100644 index 3623a5b23..000000000 --- a/cli/internal/ports/review_lane.go +++ /dev/null @@ -1,180 +0,0 @@ -package ports - -import ( - "context" - "crypto/sha256" - "fmt" - "io" - "os" - "path/filepath" - "strings" -) - -type ReviewDisposition string - -const ( - ReviewConfirmed ReviewDisposition = "CONFIRMED" - ReviewRefuted ReviewDisposition = "REFUTED" -) - -type ReviewFailureClass string - -const ( - ReviewFailureTransport ReviewFailureClass = "transport" - ReviewFailureSemantic ReviewFailureClass = "semantic" -) - -type ReviewRequestV1 struct { - SchemaVersion string - SubjectID string - HeadSHA string - AcceptanceContract string - AcceptanceContractSHA256 string - DiffPath string - DiffSHA256 string - AuthorContextID string - AuthorFamily string - DiversityMode string - Nonce string - EvidenceDir string - ReadOnly bool -} - -func (r ReviewRequestV1) Validate() error { - if r.SchemaVersion != "review-request.v1" { - return fmt.Errorf("schema_version must be review-request.v1") - } - if strings.TrimSpace(r.SubjectID) == "" || len(strings.TrimSpace(r.HeadSHA)) < 7 { - return fmt.Errorf("subject_id and a commit-bound head_sha are required") - } - if !r.ReadOnly { - return fmt.Errorf("review request must be read-only") - } - if strings.TrimSpace(r.AuthorContextID) == "" || strings.TrimSpace(r.AuthorFamily) == "" { - return fmt.Errorf("author context and family are required") - } - if r.DiversityMode != "fresh-context" && r.DiversityMode != "multi-model" { - return fmt.Errorf("unsupported diversity mode %q", r.DiversityMode) - } - if strings.TrimSpace(r.Nonce) == "" || strings.TrimSpace(r.EvidenceDir) == "" { - return fmt.Errorf("nonce and evidence_dir are required") - } - if err := verifyDigest(r.AcceptanceContract, r.AcceptanceContractSHA256); err != nil { - return fmt.Errorf("acceptance contract: %w", err) - } - if err := verifyDigest(r.DiffPath, r.DiffSHA256); err != nil { - return fmt.Errorf("diff: %w", err) - } - return nil -} - -func verifyDigest(path, expected string) error { - if strings.TrimSpace(path) == "" || len(expected) != 64 { - return fmt.Errorf("path and sha256 digest are required") - } - f, err := os.Open(path) - if err != nil { - return err - } - defer func() { _ = f.Close() }() - h := sha256.New() - if _, err := io.Copy(h, f); err != nil { - return err - } - if fmt.Sprintf("%x", h.Sum(nil)) != strings.ToLower(expected) { - return fmt.Errorf("content digest mismatch") - } - return nil -} - -type ReviewFinding struct { - Title string - Evidence string -} - -type ReviewLaneResultV1 struct { - SchemaVersion string - LaneID string - Family string - ContextID string - Disposition ReviewDisposition - FailureClass ReviewFailureClass - FailureReason string - Findings []ReviewFinding - EvidencePath string - Nonce string - ReadOnly bool -} - -func (r ReviewLaneResultV1) Validate() error { - if r.SchemaVersion != "review-lane-result.v1" { - return fmt.Errorf("schema_version must be review-lane-result.v1") - } - if strings.TrimSpace(r.LaneID) == "" || strings.TrimSpace(r.Family) == "" || strings.TrimSpace(r.ContextID) == "" || strings.TrimSpace(r.Nonce) == "" { - return fmt.Errorf("lane, family, context, and nonce are required") - } - if r.FailureClass == ReviewFailureTransport { - if r.Disposition != "" { - return fmt.Errorf("transport failure cannot carry a semantic disposition") - } - if strings.TrimSpace(r.FailureReason) == "" { - return fmt.Errorf("transport failure reason is required") - } - return nil - } - if r.Disposition != ReviewConfirmed && r.Disposition != ReviewRefuted { - return fmt.Errorf("semantic disposition must be CONFIRMED or REFUTED") - } - if !r.ReadOnly { - return fmt.Errorf("semantic reviewer result must attest read-only execution") - } - if strings.TrimSpace(r.EvidencePath) == "" { - return fmt.Errorf("semantic reviewer evidence path is required") - } - if r.Disposition == ReviewRefuted && len(r.Findings) == 0 { - return fmt.Errorf("REFUTED result requires a finding") - } - return nil -} - -func (r ReviewLaneResultV1) ValidateAgainst(request ReviewRequestV1) error { - if err := request.Validate(); err != nil { - return err - } - if err := r.Validate(); err != nil { - return err - } - if r.Nonce != request.Nonce { - return fmt.Errorf("review nonce mismatch") - } - if r.FailureClass == ReviewFailureTransport { - return nil - } - if r.ContextID == request.AuthorContextID { - return fmt.Errorf("author context cannot count as an independent reviewer") - } - root, err := filepath.EvalSymlinks(request.EvidenceDir) - if err != nil { - return fmt.Errorf("resolve evidence directory: %w", err) - } - evidence, err := filepath.EvalSymlinks(r.EvidencePath) - if err != nil { - return fmt.Errorf("resolve reviewer evidence: %w", err) - } - rel, err := filepath.Rel(root, evidence) - if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) { - return fmt.Errorf("reviewer evidence escapes evidence_dir") - } - info, err := os.Stat(evidence) - if err != nil { - return fmt.Errorf("reviewer evidence: %w", err) - } - if !info.Mode().IsRegular() || info.Size() == 0 { - return fmt.Errorf("reviewer evidence must be a nonempty regular file") - } - return nil -} - -type ReviewLanePort interface { - Run(context.Context, ReviewRequestV1) (ReviewLaneResultV1, error) -} diff --git a/cli/internal/ports/review_lane_test.go b/cli/internal/ports/review_lane_test.go deleted file mode 100644 index b5fb2e3f9..000000000 --- a/cli/internal/ports/review_lane_test.go +++ /dev/null @@ -1,127 +0,0 @@ -package ports - -import ( - "crypto/sha256" - "fmt" - "os" - "path/filepath" - "testing" -) - -func writeHashedFixture(t *testing.T, dir, name, contents string) (string, string) { - t.Helper() - path := filepath.Join(dir, name) - if err := os.WriteFile(path, []byte(contents), 0o600); err != nil { - t.Fatal(err) - } - sum := sha256.Sum256([]byte(contents)) - return path, fmt.Sprintf("%x", sum) -} - -func TestReviewRequestV1RejectsMutableOrSelfReview(t *testing.T) { - dir := t.TempDir() - contract, contractSHA := writeHashedFixture(t, dir, "contract.txt", "Given x\n") - diff, diffSHA := writeHashedFixture(t, dir, "diff.patch", "+ change\n") - base := ReviewRequestV1{ - SchemaVersion: "review-request.v1", - SubjectID: "age-1", HeadSHA: "deadbeef", AcceptanceContract: contract, - AcceptanceContractSHA256: contractSHA, - DiffPath: diff, DiffSHA256: diffSHA, AuthorContextID: "author-1", AuthorFamily: "gpt", - DiversityMode: "fresh-context", Nonce: "n1", EvidenceDir: dir, - ReadOnly: true, - } - if err := base.Validate(); err != nil { - t.Fatalf("valid request: %v", err) - } - bad := base - bad.ReadOnly = false - if err := bad.Validate(); err == nil { - t.Fatal("mutable reviewer request should fail") - } - bad = base - bad.Nonce = "" - if err := bad.Validate(); err == nil { - t.Fatal("nonce-less reviewer request should fail") - } - bad = base - bad.DiffSHA256 = "" - if err := bad.Validate(); err == nil { - t.Fatal("mutable path without a content digest should fail") - } - if err := os.WriteFile(diff, []byte("+ changed after binding\n"), 0o600); err != nil { - t.Fatal(err) - } - if err := base.Validate(); err == nil { - t.Fatal("content changed after digest binding should fail") - } - if err := os.WriteFile(diff, []byte("+ change\n"), 0o600); err != nil { - t.Fatal(err) - } - if err := base.Validate(); err != nil { - t.Fatalf("restored bound request: %v", err) - } - - evidence, _ := writeHashedFixture(t, dir, "self-review.txt", "reviewed files: 1\nx.go:1\n") - result := ReviewLaneResultV1{SchemaVersion: "review-lane-result.v1", LaneID: "lane-1", Family: "gpt", ContextID: "author-1", Disposition: ReviewConfirmed, EvidencePath: evidence, Nonce: "n1", ReadOnly: true} - if err := result.ValidateAgainst(base); err == nil { - t.Fatal("same-author context must not count as independent review") - } -} - -func TestReviewLaneResultV1SeparatesTransportFromSemanticFailure(t *testing.T) { - evidenceDir := t.TempDir() - contract, contractSHA := writeHashedFixture(t, evidenceDir, "contract.txt", "Given x\n") - diff, diffSHA := writeHashedFixture(t, evidenceDir, "diff.patch", "+ change\n") - evidencePath := filepath.Join(evidenceDir, "review-1.txt") - if err := os.WriteFile(evidencePath, []byte("reviewed files: 3\nx.go:1\n"), 0o600); err != nil { - t.Fatal(err) - } - request := ReviewRequestV1{ - SchemaVersion: "review-request.v1", SubjectID: "age-1", HeadSHA: "deadbeef", - AcceptanceContract: contract, AcceptanceContractSHA256: contractSHA, - DiffPath: diff, DiffSHA256: diffSHA, AuthorContextID: "author-1", - AuthorFamily: "claude", DiversityMode: "fresh-context", Nonce: "n1", EvidenceDir: evidenceDir, ReadOnly: true, - } - transport := ReviewLaneResultV1{ - SchemaVersion: "review-lane-result.v1", LaneID: "ntm:session:1", - Family: "gpt", ContextID: "review-1", FailureClass: ReviewFailureTransport, - FailureReason: "provider unreachable", Nonce: "n1", - } - if err := transport.Validate(); err != nil { - t.Fatalf("transport result: %v", err) - } - if transport.Disposition != "" { - t.Fatalf("transport loss fabricated semantic disposition %q", transport.Disposition) - } - - semantic := ReviewLaneResultV1{ - SchemaVersion: "review-lane-result.v1", LaneID: "ntm:session:1", - Family: "gpt", ContextID: "review-1", Disposition: ReviewRefuted, - FailureClass: ReviewFailureSemantic, Findings: []ReviewFinding{{Title: "bug", Evidence: "x.go:1"}}, - EvidencePath: evidencePath, Nonce: "n1", ReadOnly: true, - } - if err := semantic.ValidateAgainst(request); err != nil { - t.Fatalf("semantic result: %v", err) - } - outsideDir := t.TempDir() - outsidePath := filepath.Join(outsideDir, "escape.txt") - if err := os.WriteFile(outsidePath, []byte("reviewed files: 2\ny.go:1\n"), 0o600); err != nil { - t.Fatal(err) - } - semantic.EvidencePath = outsidePath - if err := semantic.ValidateAgainst(request); err == nil { - t.Fatal("evidence outside the request directory must fail") - } - linkPath := filepath.Join(evidenceDir, "linked-escape.txt") - if err := os.Symlink(outsidePath, linkPath); err != nil { - t.Fatal(err) - } - semantic.EvidencePath = linkPath - if err := semantic.ValidateAgainst(request); err == nil { - t.Fatal("symlink evidence escaping the request directory must fail") - } - semantic.EvidencePath = filepath.Join(evidenceDir, "missing.txt") - if err := semantic.ValidateAgainst(request); err == nil { - t.Fatal("missing evidence must fail") - } -} diff --git a/cli/internal/ports/safety_policy.go b/cli/internal/ports/safety_policy.go deleted file mode 100644 index 7fb279dd3..000000000 --- a/cli/internal/ports/safety_policy.go +++ /dev/null @@ -1,53 +0,0 @@ -// practices: [hexagonal-architecture, ddd-bounded-context] -package ports - -import "context" - -// SafetyPolicyName identifies a deterministic safety policy. Examples -// include "git.destructive", "worker.git", "scope.edit", and -// "holdout.read". -type SafetyPolicyName string - -// SafetyDecisionStatus is the outcome of evaluating a policy against a -// proposed operation. -type SafetyDecisionStatus string - -const ( - SafetyDecisionAllow SafetyDecisionStatus = "ALLOW" - SafetyDecisionWarn SafetyDecisionStatus = "WARN" - SafetyDecisionBlock SafetyDecisionStatus = "BLOCK" -) - -// SafetyPolicyRequest describes a proposed runtime operation. Subject -// is the file, command, tool, or resource being touched. Metadata is an -// open bag for adapter-specific facts such as worker role or scope id. -type SafetyPolicyRequest struct { - Policy SafetyPolicyName - Actor string - Operation string - Subject string - Metadata map[string]string -} - -// SafetyDecision is the typed safety result. Reason must be -// human-readable because blocked decisions are shown to operators. -type SafetyDecision struct { - Status SafetyDecisionStatus - Reason string -} - -// SafetyPolicyPort is the Evidence and Trust safety surface for -// hookless runtime operations. It absorbs deterministic blocking hooks -// such as destructive git guards, worker git authority, edit scope -// guards, and holdout-isolation gates. -// -// Contract: -// -// - Evaluate rejects an empty Policy. -// - A successful decision MUST include a non-empty Reason. -// - Unknown policies should fail closed unless an adapter documents a -// narrower policy. -// - Context cancellation MUST be honored on a best-effort basis. -type SafetyPolicyPort interface { - Evaluate(ctx context.Context, req SafetyPolicyRequest) (SafetyDecision, error) -} diff --git a/cli/internal/ports/tracker.go b/cli/internal/ports/tracker.go deleted file mode 100644 index 3cbff835e..000000000 --- a/cli/internal/ports/tracker.go +++ /dev/null @@ -1,53 +0,0 @@ -// practices: [hexagonal-architecture, ddd-bounded-context] -package ports - -import "context" - -// Issue is the driven-port view of one tracker issue (a bead). It carries the -// subset of fields the CLI's read paths consume; adapters populate what their -// backend exposes and leave the rest zero-valued. -type Issue struct { - ID string - Title string - Status string - Type string - Priority int - Assignee string - UpdatedAt string -} - -// IssueFilter narrows a List query. Zero-valued fields are not applied, so an -// empty filter lists everything the backend returns. Limit <= 0 means no limit. -type IssueFilter struct { - Type string // e.g. "epic", "bug", "task" - Status string // e.g. "in_progress", "open" - Limit int // max items to return; <= 0 disables the cap - All bool // include closed/all states when the backend supports it - MetadataField string // "key=value" filter (bd: --metadata-field) -} - -// IssueTracker is the driven port for epic/issue lifecycle operations. -// -// Implementations: tracker_bd (real, shells out to the `bd` binary), -// InMemoryTracker (in-memory test double). The previous create-only surface was -// widened (soc-ebgjk) to cover the read paths the CLI actually depends on — -// Ready/List/Show — which were reaching `bd` via scattered exec.Command calls. -// -// Contract: -// -// - Mode reports the backend identity ("beads" | "tasklist" | "memory"). -// - Read methods (Ready/List/Show) are side-effect free. -// - Show on a missing id returns a non-nil error. -// - Context cancellation MUST be honored on a best-effort basis. -type IssueTracker interface { - Mode() string // "beads" | "tasklist" | "memory" - - // Create paths. - CreateEpic(ctx context.Context, title, body string) (epicID string, err error) - CreateIssue(ctx context.Context, epicID, title, body string) (issueID string, err error) - - // Read paths. - Ready(ctx context.Context) ([]Issue, error) - List(ctx context.Context, filter IssueFilter) ([]Issue, error) - Show(ctx context.Context, id string) (Issue, error) -} diff --git a/cli/internal/ports/workspace.go b/cli/internal/ports/workspace.go deleted file mode 100644 index 0a32e13de..000000000 --- a/cli/internal/ports/workspace.go +++ /dev/null @@ -1,35 +0,0 @@ -// practices: [hexagonal-architecture, ddd-bounded-context] -package ports - -import "context" - -// WorkspaceRequest identifies one runtime workspace or worktree. -// WorkspaceID is required and should be stable for the lifecycle being -// managed. Path is optional for adapters that derive paths from ids. -type WorkspaceRequest struct { - WorkspaceID string - Path string - Metadata map[string]string -} - -// WorkspaceResult is the result of a setup or cleanup operation. -type WorkspaceResult struct { - WorkspaceID string - Path string - Status string - Reason string -} - -// WorkspacePort is the Runtime Shell surface for workspace lifecycle -// operations. It absorbs worktree setup and cleanup hooks so those -// operations can run as explicit runtime adapter calls. -// -// Contract: -// -// - Setup and Cleanup reject an empty WorkspaceID. -// - Successful results MUST include WorkspaceID and non-empty Status. -// - Context cancellation MUST be honored on a best-effort basis. -type WorkspacePort interface { - Setup(ctx context.Context, req WorkspaceRequest) (WorkspaceResult, error) - Cleanup(ctx context.Context, req WorkspaceRequest) (WorkspaceResult, error) -} diff --git a/cli/internal/provenancegraph/concurrent-append.feature b/cli/internal/provenancegraph/concurrent-append.feature index 887edd5fd..512709fa8 100644 --- a/cli/internal/provenancegraph/concurrent-append.feature +++ b/cli/internal/provenancegraph/concurrent-append.feature @@ -2,11 +2,11 @@ # # age-membrane-memory-arch-tz2s.4.5. The provenance ledger (docs/provenance/ # ledger.jsonl) is the membrane's verdict audit authority — "no verdict = not -# done" reads from it, and the #trivial / pawl-pre-push gates check it. Every +# generic provenance readers inspect it. Every # edge SEALS onto the current chain tip (prev_hash = last record's hash), so the # read-seal-write in Store.Append is a critical section: two concurrent appenders # that each read the same tip would both seal onto it and FORK the chain. ml8's -# standing pawl-service made this live — concurrent routes emit verdict edges +# independent callers may append evidence edges # concurrently. The contract: appends are serialized by a cross-process advisory # lock (flock on a sidecar .lock file), so the chain never forks and no append is # lost. The executable proof is store_test.go:TestStore_ConcurrentAppendDoesNotForkChain. diff --git a/cli/internal/provenancegraph/edge.go b/cli/internal/provenancegraph/edge.go index 1d34b1ddd..21cc47387 100644 --- a/cli/internal/provenancegraph/edge.go +++ b/cli/internal/provenancegraph/edge.go @@ -1,12 +1,7 @@ -// Package provenancegraph is the write model for the SDLC provenance/intent -// graph (ag-x31t). It appends typed, evidence-backed provenance edges to the -// committed, per-record hash-chained ledger at docs/provenance/ledger.jsonl. -// -// Per CLAUDE.md and the council architecture -// (.agents/council/2026-05-30-debate-provenance-substrate.md) the committed -// JSONL ledger is the AUDIT authority and the source of truth; any Dolt -// provenance_edges table is a rebuildable projection and loses on disagreement. -// This package therefore writes the JSONL ledger directly. +// Package provenancegraph appends typed evidence edges to the optional, +// per-record hash-chained ledger at docs/provenance/ledger.jsonl. The ledger is +// generic audit evidence. Its availability or contents never control RPI +// sequencing, verdict validity, revision, or repository delivery. // // Edge events conform to schemas/agentops-sdlc-provenance.v1.schema.json and // reuse the hashing discipline of cli/internal/rpi/ledger.go: @@ -31,7 +26,7 @@ import ( const SchemaVersion = "agentops-sdlc-provenance.v1" // LedgerRelativePath is the repo-relative path to the committed provenance -// ledger. The committed JSONL is the audit authority. +// ledger. This is a historical audit location, not a lifecycle authority. const LedgerRelativePath = "docs/provenance/ledger.jsonl" // NodeTypes is the closed set of edge-endpoint node types from the v1 schema. @@ -53,17 +48,9 @@ var NodeTypes = []string{ // commit_implements_decision->wasRevisionOf, learning_revises_decision-> // wasInvalidatedBy. // -// "resolves" is the one deliberate non-PROV-O addition (age-ekam, epic -// age-xnet A1/A6/A7): a commit→commit resolution edge from a landed -// compensating commit (mechanical revert or fix-forward) to the REFUTED -// commit it resolves. It is appended by the compensator lane and consumed by -// the LKG-frontier computation (cli/internal/frontier), which enforces the -// duel-hardened validity floor — strict-descendant compensator, acyclicity, -// one live edge per refuted sha, and the refuting verdict's repro executed -// GREEN at the compensating sha recorded in evidence_ref plus the P0-fix-bead -// binding. No PROV-O verb carries the "compensates a refuted change" -// semantics without overloading an existing meaning, so the ledger names it -// literally. +// "resolves" is the one historical non-PROV-O addition: a commit-to-commit +// observation that one change compensates for another. It remains readable for +// existing records but does not change any current core outcome. var Relations = []string{ "wasGeneratedBy", "wasAssociatedWith", @@ -92,45 +79,21 @@ type Edge struct { ToType string `json:"to_type"` Relation string `json:"relation"` EvidenceRef string `json:"evidence_ref,omitempty"` - // BeadID and MergeSHA are additive, NON-payload mesh join keys (ag-5qltf, - // epic ag-w0wr2). They denormalize the already-hashed from_id/to_id of a - // bead→commit edge into the canonical (bead_id, merge_sha) join key the - // yield↔provenance mesh joins on (bead_id is the universal key; merge_sha - // anchors the bead→commit hop). Deliberately EXCLUDED from edgePayload: the - // authoritative values are from_id/to_id, which the payload already covers, - // so these projections need no independent hash protection — and excluding - // them keeps every existing committed edge's payload_hash/VerifyChain intact. + // BeadID and MergeSHA are legacy, NON-payload join keys retained so existing + // records remain byte-verifiable. They carry no work-ownership or delivery + // authority and generic appenders need not emit them. BeadID string `json:"bead_id,omitempty"` MergeSHA string `json:"merge_sha,omitempty"` TrustTier string `json:"trust_tier"` TS string `json:"ts"` - // v1.1 verdict-record enrichment (age-rk3r.3) — five OPTIONAL, additive - // fields carrying reviewer metadata for verdict edges (the cost-of-verified- - // done substrate; failover label; the receipts' structured evidence source). - // UNLIKE the bead_id/merge_sha join keys above, these ARE part of edgePayload - // and therefore hash-PROTECTED: a record that sets any of them has it covered - // by payload_hash, so it is tamper-evident. Backward compatibility rests on - // omitempty — a record predating these fields (or leaving them at their zero - // value) omits them from the payload JSON entirely, so its payload_hash is - // byte-identical to the pre-v1.1 layout and VerifyChain stays intact across the - // whole committed history. "v1.1" is a DOCUMENTATION label only: SchemaVersion - // is UNCHANGED, and consumers branch on field PRESENCE, never a version string. - // - // COMPATIBILITY BOUNDARY (documented, load-bearing): because the fields are IN - // the payload, an OLDER ao binary that predates them unmarshals a v1.1 record - // into a struct that DROPS them, recomputes the payload WITHOUT them, and so - // reports a spurious payload_hash mismatch (a false "broken chain") on v1.1 - // records — while still verifying every pre-v1.1 record. A reader must be at or - // above the version that knows these fields to verify v1.1 records; the - // installed-hook ao-version floor is a separate bead (.6). + // These optional legacy observations are part of edgePayload when present. + // Keep them to preserve historical payload hashes; they are not verdict.v2 + // fields and do not affect current validation. ReviewerFamily string `json:"reviewer_family,omitempty"` Degraded bool `json:"degraded,omitempty"` Rounds int `json:"rounds,omitempty"` DurationS float64 `json:"duration_s,omitempty"` - // TokensEst is the verification-economics meter (age-verification-economics-ebec.1): - // estimated tokens spent producing this verdict (transcript-bytes/4 when the - // harness reports no exact usage). Same additive/omitempty compatibility - // contract as the other v1.1 fields — zero omits, pre-meter records unchanged. + // TokensEst is a legacy observation retained for hash compatibility. TokensEst int `json:"tokens_est,omitempty"` EvidencePath string `json:"evidence_path,omitempty"` PrevHash string `json:"prev_hash"` diff --git a/cli/internal/provenancegraph/reader_version.go b/cli/internal/provenancegraph/reader_version.go index 42751b905..9c1db7f1f 100644 --- a/cli/internal/provenancegraph/reader_version.go +++ b/cli/internal/provenancegraph/reader_version.go @@ -1,69 +1,6 @@ -// practices: [design-by-contract] package provenancegraph -import ( - "reflect" - "sort" - "strings" -) - -// LedgerReaderVersion is the monotonic CAPABILITY level of THIS binary's ledger -// reader — "which shapes of committed record can I verify without a false -// break?". It is the durable contract the installed pre-push hook (age-rk3r.6, -// `ao verify init`) probes as its ao-version FLOOR before it trusts a chain -// verify. -// -// Why an integer capability and not `ao --version`: the compatibility boundary -// is a READER capability, not a release tag. A pre-age-rk3r.3 reader unmarshals -// a v1.1 verdict record into a struct that DROPS the additive fields -// (reviewer_family, degraded, rounds, duration_s, evidence_path), recomputes the -// payload WITHOUT them, and so reports a SPURIOUS payload_hash mismatch — a false -// "broken chain" — on every v1.1 record (see the Edge "COMPATIBILITY BOUNDARY" -// note in edge.go). The floor must therefore be expressed as "does this reader -// understand the current record shapes", which a monotonic capability integer -// states precisely and a fragile semver parse of a dev/build version string does -// not. -// -// Levels (bump this AND the installed-hook floor together whenever a reader -// change becomes REQUIRED to correctly verify a newer record shape): -// -// 1 understands the age-rk3r.3 v1.1 additive verdict fields, so it never -// false-breaks a v1.1 record's hash chain. (age-rk3r.6) -// -// The mere EXISTENCE of the `ao provenance ledger-reader-version` subcommand that -// prints this is itself the pre-age-rk3r.6 floor: a binary old enough to predate -// the v1.1 fields also predates this subcommand, so the hook's probe fails on it -// and refuses with an upgrade message rather than trusting a false-broken chain. -// -// BUMP RULE (verification-surface-honesty S4): bump this level on ANY change to -// the payload-hash fieldset (edgePayload) — additive or not — because a reader -// whose fieldset lags the writer's recomputes the payload differently and -// reports a spurious payload_hash mismatch. The binding is enforced by test: -// reader_version_test.go freezes payloadHashFieldset() per level and fails any -// fieldset change that arrives without a version bump. -const LedgerReaderVersion = 1 - -// payloadHashSkewHint is the shared payload_hash-mismatch error surface. From -// a reader's seat the mismatch is indistinguishable between real tampering and -// reader-version/hashing skew (a stale binary whose edgePayload fieldset lags -// the writer's — live on 2026-07-10 an installed ao false-flagged the real -// ledger as BROKEN at line 423 while a fresh build verified all 441 records). -// Every mismatch site (VerifyFile, VerifyChain) must emit THIS text so the -// operator rules out skew before treating the ledger as tampered. -const payloadHashSkewHint = "payload_hash mismatch — record content was altered, OR this ao's ledger reader is stale (reader-version/hashing skew): rebuild ao from source (cd cli && make build) and re-verify before treating the ledger as tampered" - -// payloadHashFieldset returns the sorted JSON field names of edgePayload — the -// exact fieldset that feeds payload_hash. reader_version_test.go freezes this -// per LedgerReaderVersion so any fieldset change forces a level bump. -func payloadHashFieldset() []string { - typ := reflect.TypeOf(edgePayload{}) - fields := make([]string, 0, typ.NumField()) - for i := 0; i < typ.NumField(); i++ { - tag := typ.Field(i).Tag.Get("json") - if name, _, _ := strings.Cut(tag, ","); name != "" && name != "-" { - fields = append(fields, name) - } - } - sort.Strings(fields) - return fields -} +// payloadHashSkewHint is shared by verification paths. A mismatch may be real +// corruption or a reader whose historical field set differs from the writer. +// The ledger is evidence only; this diagnostic never authorizes continuation. +const payloadHashSkewHint = "payload_hash mismatch — record content was tampered with, or this reader does not understand the historical record shape; rebuild ao from source and inspect the exact record" diff --git a/cli/internal/provenancegraph/reader_version_test.go b/cli/internal/provenancegraph/reader_version_test.go deleted file mode 100644 index 67337c3a6..000000000 --- a/cli/internal/provenancegraph/reader_version_test.go +++ /dev/null @@ -1,48 +0,0 @@ -// Tests binding LedgerReaderVersion to the payload-hash fieldset shape -// (verification-surface-honesty S4): the reader capability level must bump -// whenever the fieldset feeding payload_hash changes, so the installed hook's -// version-floor probe catches reader/writer skew MECHANICALLY instead of the -// operator meeting it as a false "broken chain". The binding is enforced by -// test, not by comment. -package provenancegraph - -import ( - "reflect" - "testing" -) - -// TestLedgerReaderVersion_BoundToPayloadHashFieldset freezes the exact -// edgePayload fieldset for the CURRENT LedgerReaderVersion. If this test -// fails you changed the payload-hash fieldset: bump LedgerReaderVersion (and -// the installed-hook floor with it — see the const's doc), then register the -// new fieldset under the new level here. Editing the frozen list WITHOUT -// bumping the level recreates the 2026-07-10 skew false-alarm. -func TestLedgerReaderVersion_BoundToPayloadHashFieldset(t *testing.T) { - frozen := map[int][]string{ - 1: { - "degraded", - "duration_s", - "evidence_path", - "evidence_ref", - "from_id", - "from_type", - "relation", - "reviewer_family", - "rounds", - "schema_version", - "to_id", - "to_type", - "tokens_est", - "trust_tier", - "ts", - }, - } - want, ok := frozen[LedgerReaderVersion] - if !ok { - t.Fatalf("no frozen payload-hash fieldset registered for LedgerReaderVersion %d — register the new level's fieldset in this test WITH the version bump", LedgerReaderVersion) - } - got := payloadHashFieldset() - if !reflect.DeepEqual(got, want) { - t.Fatalf("payload-hash fieldset changed without a LedgerReaderVersion bump:\n got %v\nwant %v\nBump LedgerReaderVersion + the installed-hook floor together, then freeze the new fieldset here (see reader_version.go)", got, want) - } -} diff --git a/cli/internal/quality/skills_codex_test.go b/cli/internal/quality/skills_codex_test.go index 7eea09d5e..35a14f370 100644 --- a/cli/internal/quality/skills_codex_test.go +++ b/cli/internal/quality/skills_codex_test.go @@ -105,11 +105,11 @@ func TestCheckSkillsCountsCompatibilityPointerPackages(t *testing.T) { skills := filepath.Join(root, "skills-codex") writeSkill(t, skills, "research") - pointerDir := filepath.Join(skills, "pre-mortem") + pointerDir := filepath.Join(skills, "premortem") if err := os.MkdirAll(pointerDir, 0o755); err != nil { t.Fatal(err) } - if err := os.WriteFile(filepath.Join(pointerDir, "SKILL.md"), []byte("---\nname: pre-mortem\nimplementation: false\nredirect_to: premortem\n---\n"), 0o644); err != nil { + if err := os.WriteFile(filepath.Join(pointerDir, "SKILL.md"), []byte("---\nname: premortem\nimplementation: false\nredirect_to: premortem\n---\n"), 0o644); err != nil { t.Fatal(err) } diff --git a/cli/internal/quality/stale_refs.go b/cli/internal/quality/stale_refs.go index a1349d796..4f5d9305f 100644 --- a/cli/internal/quality/stale_refs.go +++ b/cli/internal/quality/stale_refs.go @@ -10,18 +10,18 @@ import ( // DeprecatedCommands maps old namespace-qualified command references to their // new flat replacements. var DeprecatedCommands = map[string]string{ - "ao know forge": "ao forge", - "ao know inject": "ao inject", - "ao know search": "ao search", - "ao know lookup": "ao lookup", - "ao know trace": "ao trace", - "ao know store": "ao store", - "ao know index": "ao index", - "ao know temper": "ao temper", - "ao know feedback": "ao feedback", - "ao know migrate": "ao migrate", - "ao know batch-feedback": "ao batch-feedback", - "ao know session-outcome": "ao eval session-outcome", + "ao know forge": "ao forge", + "ao know inject": "ao inject", + "ao know search": "ao search", + "ao know lookup": "ao lookup", + "ao know trace": "ao trace", + "ao know store": "ao store", + "ao know index": "ao index", + "ao know temper": "ao temper", + "ao know feedback": "ao feedback", + "ao know migrate": "ao migrate", + "ao know batch-feedback": "ao batch-feedback", + "ao know session-outcome": "ao eval session-outcome", // Eval-family commands folded under `ao eval` (age-focus-membrane-bookkeeper-m1wg.16). // The old top-level spellings still resolve (hidden) except `ao scenario`, // which is reparented; canonical is the `ao eval …` form. @@ -33,13 +33,10 @@ var DeprecatedCommands = map[string]string{ // (age-focus-membrane-bookkeeper-m1wg.17). The old top-level spellings still // resolve (hidden aliases) except `ao state`, which is fully reparented; // canonical is the `ao session …` form. - "ao state": "ao session state", - "ao memory": "ao session memory", - "ao rehydrate": "ao session rehydrate", - "ao handoff": "ao session handoff", - // `ao orchestrate` was archived behind //go:build legacy (age-h4y3); point the - // stale `ao work rpi` ref at the surviving spine loop driver `ao converge`. - "ao work rpi": "ao converge", + "ao state": "ao session state", + "ao memory": "ao session memory", + "ao rehydrate": "ao session rehydrate", + "ao handoff": "ao session handoff", "ao work ratchet": "ao ratchet", "ao work goals": "ao goals", "ao work session": "ao session", @@ -63,7 +60,6 @@ var DeprecatedCommands = map[string]string{ "ao settings config": "ao config", "ao settings memory": "ao memory", "ao settings notebook": "ao notebook", - "ao settings worktree": "ao worktree", "ao start demo": "ao demo", "ao start init": "ao init", "ao start seed": "ao seed", diff --git a/cli/internal/quality/stale_refs_test.go b/cli/internal/quality/stale_refs_test.go index 11f461729..beb749221 100644 --- a/cli/internal/quality/stale_refs_test.go +++ b/cli/internal/quality/stale_refs_test.go @@ -51,7 +51,7 @@ func TestCheckStaleReferences_AggregatesMatches(t *testing.T) { if err := os.WriteFile(a, []byte("ao know forge\n"), 0o644); err != nil { t.Fatal(err) } - if err := os.WriteFile(b, []byte("ao work rpi status\nao know inject\n"), 0o644); err != nil { + if err := os.WriteFile(b, []byte("ao know search status\nao know inject\n"), 0o644); err != nil { t.Fatal(err) } diff --git a/cli/internal/scenario/scenario.go b/cli/internal/scenario/scenario.go index 37b16208d..a5b3b6965 100644 --- a/cli/internal/scenario/scenario.go +++ b/cli/internal/scenario/scenario.go @@ -1,6 +1,6 @@ // Package scenario creates and describes AgentOps behavioral validation // scenarios. Create is the single scenario-authoring path shared by -// `ao scenario add` and `ao goals scenarios --create`, so the two commands +// `ao scenario add`, so callers // can never drift in how they shape a scenario file. package scenario diff --git a/cli/internal/scope/scope.go b/cli/internal/scope/scope.go deleted file mode 100644 index 4df9ca503..000000000 --- a/cli/internal/scope/scope.go +++ /dev/null @@ -1,235 +0,0 @@ -// Package scope manages the .agents/scope.lock edit-scope guard state used by -// the /scope skill, the `ao scope` cobra commands, and the -// hooks/edit-scope-guard.sh PreToolUse hook. -// -// Lock-file mutations MUST go through cli/internal/llmwiki.SafeAtomicWrite so -// that concurrent freeze/unfreeze callers converge atomically (last-writer-wins, -// never tears). New locking primitives are explicitly forbidden — see -// soc-irg1.3 pre-mortem and cli/internal/llmwiki/scope_guard.go:76. -package scope - -import ( - "encoding/json" - "errors" - "fmt" - "io/fs" - "os" - "path/filepath" - "sort" - "strings" - "time" - - "github.com/boshu2/agentops/cli/internal/llmwiki" -) - -// SchemaVersion is the on-disk schema version for the lock file. Bumps here -// must stay backward-compatible with the hook's fail-open default. -const SchemaVersion = 1 - -// Lock is the on-disk representation of .agents/scope.lock. -type Lock struct { - SchemaVersion int `json:"schema_version"` - FrozenDirs []string `json:"frozen_dirs"` - AcquiredAt time.Time `json:"acquired_at"` - AcquiredBy string `json:"acquired_by"` -} - -// Read returns the Lock at lockPath. If the file does not exist or is empty, -// Read returns a zero-value Lock with SchemaVersion populated and a nil error; -// callers can treat that as "no enforcement". -func Read(lockPath string) (*Lock, error) { - data, err := os.ReadFile(lockPath) - if err != nil { - if errors.Is(err, fs.ErrNotExist) { - return &Lock{SchemaVersion: SchemaVersion, FrozenDirs: []string{}}, nil - } - return nil, fmt.Errorf("scope: read %s: %w", lockPath, err) - } - if len(data) == 0 { - return &Lock{SchemaVersion: SchemaVersion, FrozenDirs: []string{}}, nil - } - var l Lock - if err := json.Unmarshal(data, &l); err != nil { - return nil, fmt.Errorf("scope: parse %s: %w", lockPath, err) - } - if l.SchemaVersion == 0 { - l.SchemaVersion = SchemaVersion - } - if l.FrozenDirs == nil { - l.FrozenDirs = []string{} - } - return &l, nil -} - -// Write persists the Lock at lockPath via llmwiki.SafeAtomicWrite. The vault -// argument scopes the safe-write check; for scope locks we use the parent -// directory of the lock file (typically `.agents/`). -// -// IMPORTANT: this is the ONLY supported write path. Do not call os.WriteFile -// or open(O_TRUNC) against the lock file directly — the atomic-replace -// invariant must hold for the hook's read side to remain race-free. -func Write(lockPath string, l *Lock) error { - if l == nil { - return errors.New("scope: nil lock") - } - if l.SchemaVersion == 0 { - l.SchemaVersion = SchemaVersion - } - if l.FrozenDirs == nil { - l.FrozenDirs = []string{} - } - if l.AcquiredAt.IsZero() { - l.AcquiredAt = time.Now().UTC() - } - data, err := json.MarshalIndent(l, "", " ") - if err != nil { - return fmt.Errorf("scope: marshal: %w", err) - } - data = append(data, '\n') - - abs, err := filepath.Abs(lockPath) - if err != nil { - return fmt.Errorf("scope: abs %s: %w", lockPath, err) - } - vault := filepath.Dir(abs) - if err := os.MkdirAll(vault, 0o755); err != nil { - return fmt.Errorf("scope: mkdir vault: %w", err) - } - // llmwiki.SafeAtomicWrite enforces vault-bounded write + atomic temp+rename. - // We pass the vault as the lock's parent dir; the lock filename itself is - // not in the llmwiki allowlist, so we bypass scope enforcement by calling - // the AtomicWriteFile primitive directly when safe-write rejects with a - // scope error. The atomic-replace invariant is what we actually need; the - // llmwiki vault allowlist is for wiki/ writes, not .agents/ writes. - if err := llmwiki.SafeAtomicWrite(vault, abs, data, 0o644); err != nil { - var scopeErr *llmwiki.WriteScopeError - if errors.As(err, &scopeErr) { - // Fall back to the underlying atomic primitive — same temp+rename - // guarantees, just without the wiki/ allowlist. - if aerr := llmwiki.AtomicWriteFile(abs, data, 0o644); aerr != nil { - return fmt.Errorf("scope: atomic write %s: %w", abs, aerr) - } - return nil - } - return fmt.Errorf("scope: safe atomic write %s: %w", abs, err) - } - return nil -} - -// Freeze appends one or more directories to the lock's FrozenDirs set -// (idempotent). Pass-through to Write for atomic persistence. -func Freeze(lockPath string, dirs []string) error { - l, err := Read(lockPath) - if err != nil { - return err - } - seen := make(map[string]struct{}, len(l.FrozenDirs)+len(dirs)) - out := make([]string, 0, len(l.FrozenDirs)+len(dirs)) - for _, d := range l.FrozenDirs { - n := normalizeDir(d) - if _, ok := seen[n]; ok { - continue - } - seen[n] = struct{}{} - out = append(out, n) - } - for _, d := range dirs { - n := normalizeDir(d) - if n == "" { - continue - } - if _, ok := seen[n]; ok { - continue - } - seen[n] = struct{}{} - out = append(out, n) - } - sort.Strings(out) - l.FrozenDirs = out - l.AcquiredAt = time.Now().UTC() - if l.AcquiredBy == "" { - l.AcquiredBy = defaultActor() - } - return Write(lockPath, l) -} - -// Unfreeze removes the named directories from the lock's FrozenDirs. If dirs -// is empty (or nil), all frozen directories are cleared. -func Unfreeze(lockPath string, dirs []string) error { - l, err := Read(lockPath) - if err != nil { - return err - } - if len(dirs) == 0 { - l.FrozenDirs = []string{} - } else { - drop := make(map[string]struct{}, len(dirs)) - for _, d := range dirs { - drop[normalizeDir(d)] = struct{}{} - } - out := l.FrozenDirs[:0] - for _, d := range l.FrozenDirs { - n := normalizeDir(d) - if _, ok := drop[n]; ok { - continue - } - out = append(out, n) - } - l.FrozenDirs = append([]string(nil), out...) - } - l.AcquiredAt = time.Now().UTC() - if l.AcquiredBy == "" { - l.AcquiredBy = defaultActor() - } - return Write(lockPath, l) -} - -// IsAllowed reports whether targetPath is editable under the current Lock. -// Returns true when: -// - lock is nil, or -// - lock has zero frozen dirs, or -// - targetPath is under any of the frozen dirs (prefix match on the -// normalized path). -func IsAllowed(l *Lock, targetPath string) bool { - if l == nil || len(l.FrozenDirs) == 0 { - return true - } - target := normalizePath(targetPath) - for _, dir := range l.FrozenDirs { - norm := normalizeDir(dir) - if norm == "" { - continue - } - if target == norm || strings.HasPrefix(target, norm+"/") { - return true - } - } - return false -} - -// normalizeDir trims whitespace + trailing slashes and converts to forward -// slashes. Empty input returns "". -func normalizeDir(s string) string { - s = strings.TrimSpace(s) - s = filepath.ToSlash(s) - s = strings.TrimRight(s, "/") - return s -} - -// normalizePath trims whitespace and converts to forward slashes; trailing -// slashes are preserved off (so "foo/" and "foo" both compare as "foo"). -func normalizePath(s string) string { - s = strings.TrimSpace(s) - s = filepath.ToSlash(s) - return strings.TrimRight(s, "/") -} - -func defaultActor() string { - if v := os.Getenv("AO_SESSION_ID"); v != "" { - return v - } - if v := os.Getenv("CLAUDE_SESSION_ID"); v != "" { - return v - } - return fmt.Sprintf("pid:%d", os.Getpid()) -} diff --git a/cli/internal/scope/scope_test.go b/cli/internal/scope/scope_test.go deleted file mode 100644 index b84419750..000000000 --- a/cli/internal/scope/scope_test.go +++ /dev/null @@ -1,283 +0,0 @@ -package scope - -import ( - "encoding/json" - "errors" - "os" - "path/filepath" - "sort" - "sync" - "testing" -) - -func tmpLock(t *testing.T) string { - t.Helper() - dir := t.TempDir() - return filepath.Join(dir, ".agents", "scope.lock") -} - -func TestRead_MissingFile_ReturnsEmptyLock(t *testing.T) { - lock, err := Read(filepath.Join(t.TempDir(), "nonexistent.lock")) - if err != nil { - t.Fatalf("Read missing: %v", err) - } - if lock == nil { - t.Fatal("want non-nil empty lock") - } - if lock.SchemaVersion != SchemaVersion { - t.Fatalf("want SchemaVersion %d, got %d", SchemaVersion, lock.SchemaVersion) - } - if len(lock.FrozenDirs) != 0 { - t.Fatalf("want empty FrozenDirs, got %v", lock.FrozenDirs) - } -} - -func TestRead_EmptyFile_ReturnsEmptyLock(t *testing.T) { - path := tmpLock(t) - if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(path, []byte{}, 0o644); err != nil { - t.Fatal(err) - } - lock, err := Read(path) - if err != nil { - t.Fatalf("Read empty: %v", err) - } - if len(lock.FrozenDirs) != 0 { - t.Fatalf("want empty FrozenDirs, got %v", lock.FrozenDirs) - } -} - -func TestWriteThenRead_RoundTrip(t *testing.T) { - path := tmpLock(t) - want := &Lock{ - SchemaVersion: SchemaVersion, - FrozenDirs: []string{"cli/cmd/ao", "skills/scope"}, - AcquiredBy: "test-session", - } - if err := Write(path, want); err != nil { - t.Fatalf("Write: %v", err) - } - got, err := Read(path) - if err != nil { - t.Fatalf("Read: %v", err) - } - if got.SchemaVersion != want.SchemaVersion { - t.Fatalf("SchemaVersion mismatch: want %d, got %d", want.SchemaVersion, got.SchemaVersion) - } - if got.AcquiredBy != "test-session" { - t.Fatalf("AcquiredBy mismatch: want test-session, got %q", got.AcquiredBy) - } - if got.AcquiredAt.IsZero() { - t.Fatal("AcquiredAt was not populated") - } - if len(got.FrozenDirs) != 2 || got.FrozenDirs[0] != "cli/cmd/ao" || got.FrozenDirs[1] != "skills/scope" { - t.Fatalf("FrozenDirs round-trip mismatch: got %v", got.FrozenDirs) - } -} - -func TestWrite_NormalizesEmptyFrozenDirsToEmptyArray(t *testing.T) { - path := tmpLock(t) - if err := Write(path, &Lock{}); err != nil { - t.Fatalf("Write empty: %v", err) - } - raw, err := os.ReadFile(path) - if err != nil { - t.Fatal(err) - } - var generic map[string]interface{} - if err := json.Unmarshal(raw, &generic); err != nil { - t.Fatalf("parse: %v", err) - } - dirs, ok := generic["frozen_dirs"].([]interface{}) - if !ok { - t.Fatalf("frozen_dirs not array: %T", generic["frozen_dirs"]) - } - if len(dirs) != 0 { - t.Fatalf("want empty array, got %v", dirs) - } -} - -func TestFreeze_AppendsAndDeduplicates(t *testing.T) { - path := tmpLock(t) - if err := Freeze(path, []string{"a/", "b"}); err != nil { - t.Fatalf("Freeze 1: %v", err) - } - if err := Freeze(path, []string{"b/", "c/"}); err != nil { - t.Fatalf("Freeze 2: %v", err) - } - got, err := Read(path) - if err != nil { - t.Fatal(err) - } - want := []string{"a", "b", "c"} - sort.Strings(got.FrozenDirs) - if len(got.FrozenDirs) != len(want) { - t.Fatalf("want %v, got %v", want, got.FrozenDirs) - } - for i := range want { - if got.FrozenDirs[i] != want[i] { - t.Fatalf("want %v, got %v", want, got.FrozenDirs) - } - } -} - -func TestUnfreeze_All(t *testing.T) { - path := tmpLock(t) - if err := Freeze(path, []string{"a", "b"}); err != nil { - t.Fatal(err) - } - if err := Unfreeze(path, nil); err != nil { - t.Fatalf("Unfreeze all: %v", err) - } - got, err := Read(path) - if err != nil { - t.Fatal(err) - } - if len(got.FrozenDirs) != 0 { - t.Fatalf("want empty, got %v", got.FrozenDirs) - } -} - -func TestUnfreeze_OneOnly(t *testing.T) { - path := tmpLock(t) - if err := Freeze(path, []string{"a", "b", "c"}); err != nil { - t.Fatal(err) - } - if err := Unfreeze(path, []string{"b/"}); err != nil { - t.Fatalf("Unfreeze one: %v", err) - } - got, err := Read(path) - if err != nil { - t.Fatal(err) - } - want := []string{"a", "c"} - if len(got.FrozenDirs) != len(want) { - t.Fatalf("want %v, got %v", want, got.FrozenDirs) - } - for i := range want { - if got.FrozenDirs[i] != want[i] { - t.Fatalf("want %v, got %v", want, got.FrozenDirs) - } - } -} - -func TestRead_InvalidJSON_ReturnsError(t *testing.T) { - path := tmpLock(t) - if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(path, []byte("{not-json"), 0o644); err != nil { - t.Fatal(err) - } - _, err := Read(path) - if err == nil { - t.Fatal("want parse error, got nil") - } -} - -func TestWrite_NilLock_Errors(t *testing.T) { - if err := Write(tmpLock(t), nil); err == nil { - t.Fatal("want error on nil lock") - } else if !errors.Is(err, err) { // sanity - t.Fatalf("unexpected: %v", err) - } -} - -func TestIsAllowed_Predicate(t *testing.T) { - cases := []struct { - name string - frozen []string - target string - want bool - }{ - {"nil-lock-allows", nil, "anything/foo.go", true}, - {"empty-frozen-allows", []string{}, "anything/foo.go", true}, - {"exact-prefix-matches", []string{"cli/cmd/ao"}, "cli/cmd/ao/scope.go", true}, - {"trailing-slash-matches", []string{"cli/cmd/ao/"}, "cli/cmd/ao/scope.go", true}, - {"sibling-rejects", []string{"cli/cmd/ao"}, "cli/cmd/foo/scope.go", false}, - {"unrelated-rejects", []string{"cli/cmd/ao"}, "skills/scope/SKILL.md", false}, - {"nested-allows", []string{"skills"}, "skills/scope/SKILL.md", true}, - {"deep-nested-allows", []string{"a/b/c"}, "a/b/c/d/e/f.go", true}, - {"prefix-bleed-rejects", []string{"foo"}, "foobar/baz.go", false}, - {"multiple-frozen-any-allows", []string{"x", "skills"}, "skills/scope/SKILL.md", true}, - {"multiple-frozen-none-rejects", []string{"x", "y"}, "skills/scope/SKILL.md", false}, - {"exact-dir-allows", []string{"cli/cmd/ao"}, "cli/cmd/ao", true}, - } - for _, tc := range cases { - - t.Run(tc.name, func(t *testing.T) { - lock := &Lock{FrozenDirs: tc.frozen} - if tc.frozen == nil { - lock = nil - } - got := IsAllowed(lock, tc.target) - if got != tc.want { - t.Fatalf("IsAllowed(%v, %q) = %v, want %v", tc.frozen, tc.target, got, tc.want) - } - }) - } -} - -func TestWrite_AtomicityUnderConcurrency(t *testing.T) { - // L2 race scenario: 100 concurrent Freeze calls. Final lock file must: - // - parse as valid JSON (atomic-write invariant — no torn write) - // - contain at least one of the requested directories (writers don't all - // stomp into nothing) - // - // SafeAtomicWrite uses temp+rename, so readers either see the previous - // file or the next file, never a half-written one. - path := tmpLock(t) - if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { - t.Fatal(err) - } - - const N = 100 - var wg sync.WaitGroup - wg.Add(N) - errs := make(chan error, N) - for i := 0; i < N; i++ { - - go func() { - defer wg.Done() - if err := Freeze(path, []string{"dir/" + itoa(i)}); err != nil { - errs <- err - } - }() - } - wg.Wait() - close(errs) - for err := range errs { - t.Errorf("concurrent Freeze: %v", err) - } - - // Final file must parse and have at least one frozen dir. - got, err := Read(path) - if err != nil { - t.Fatalf("final Read: %v", err) - } - if len(got.FrozenDirs) == 0 { - t.Fatal("expected at least one frozen dir survived the race") - } - // All entries must look like "dir/N" — proves no torn write yielded - // a partial string. - for _, d := range got.FrozenDirs { - if len(d) < 4 || d[:4] != "dir/" { - t.Fatalf("torn or stray entry: %q", d) - } - } -} - -func itoa(n int) string { - if n == 0 { - return "0" - } - digits := []byte{} - x := n - for x > 0 { - digits = append([]byte{byte('0' + x%10)}, digits...) - x /= 10 - } - return string(digits) -} diff --git a/cli/internal/skills/catalog.go b/cli/internal/skills/catalog.go index 990220d2b..dea19ccac 100644 --- a/cli/internal/skills/catalog.go +++ b/cli/internal/skills/catalog.go @@ -1,7 +1,7 @@ // catalog.go — load and query the generated skills/catalog.json. // // The catalog is the queryable inventory emitted by -// scripts/generate-skill-catalog.sh (slice 1 of soc-vuu6.4). This file is +// scripts/generate-skill-mesh.py. This file is // slice 2: a pure, table-testable query engine plus a thin disk loader so the // `ao skills list|consumers|producers|graph` commands never re-parse SKILL.md // frontmatter — they read the committed catalog, which CI keeps in sync. diff --git a/cli/internal/skills/load_test.go b/cli/internal/skills/load_test.go index 975f93631..54e99cf4c 100644 --- a/cli/internal/skills/load_test.go +++ b/cli/internal/skills/load_test.go @@ -89,7 +89,7 @@ func TestLoad_MissingDirErrors(t *testing.T) { } // TestLoad_LiveTreeNonEmpty asserts the loader reads the real skills/ tree when -// run from the repo. Per the pre-mortem, it does NOT assert exact skill names +// run from the repo. Per the premortem, it does NOT assert exact skill names // (the tree churns) — only that loading succeeds and finds skills. func TestLoad_LiveTreeNonEmpty(t *testing.T) { root := repoSkillsDir(t) diff --git a/cli/internal/skillshealth/audit.go b/cli/internal/skillshealth/audit.go index b6cf6e92e..9d8853fa4 100644 --- a/cli/internal/skillshealth/audit.go +++ b/cli/internal/skillshealth/audit.go @@ -70,7 +70,7 @@ func Audit(opts Options) (*Report, error) { names := make([]string, 0, len(entries)) for _, e := range entries { // Skip files (e.g., SKILL-TIERS.md) at the top level. Only walk dirs. - if !e.IsDir() { + if !e.IsDir() || strings.HasPrefix(e.Name(), "_") { continue } name := e.Name() diff --git a/cli/internal/skillshealth/audit_test.go b/cli/internal/skillshealth/audit_test.go index 4e4703e0a..47637ca81 100644 --- a/cli/internal/skillshealth/audit_test.go +++ b/cli/internal/skillshealth/audit_test.go @@ -1,6 +1,7 @@ package skillshealth import ( + "encoding/json" "os" "path/filepath" "testing" @@ -125,11 +126,18 @@ func TestAudit_RealRepo_L2(t *testing.T) { if err != nil { t.Fatalf("Audit failed: %v", err) } - // Floor tracks the deliberate corpus size: 58 skills + _fixtures after the - // 2026-07-07 retire wave (docs/audits/skills-audit-2026-07-06.md, 66 -> 58). - // A drop BELOW the floor means skills went missing outside a governed retire. - if got := len(report.Skills); got < 55 { - t.Errorf("expected >= 55 skills, got %d", got) + catalogRaw, err := os.ReadFile(filepath.Join(skillsDir, "catalog.json")) + if err != nil { + t.Fatalf("read generated catalog: %v", err) + } + var catalog struct { + Skills []json.RawMessage `json:"skills"` + } + if err := json.Unmarshal(catalogRaw, &catalog); err != nil { + t.Fatalf("parse generated catalog: %v", err) + } + if got, want := len(report.Skills), len(catalog.Skills); got != want { + t.Errorf("audit/catalog skill count mismatch: got %d want %d", got, want) } if len(report.Errors) > 0 { t.Logf("audit reports %d errors against real repo (informational):", len(report.Errors)) @@ -140,8 +148,7 @@ func TestAudit_RealRepo_L2(t *testing.T) { } t.Logf(" %s", e) } - // Don't fail by default; the live tree may have transient drift while - // other Wave 1 work is in progress. Real expectation is empty. + t.Fail() } } diff --git a/cli/internal/types/memrl_policy.go b/cli/internal/types/memrl_policy.go index 6cf574a09..aab88536c 100644 --- a/cli/internal/types/memrl_policy.go +++ b/cli/internal/types/memrl_policy.go @@ -68,8 +68,8 @@ const ( // MemRLFailureClassAny is a wildcard used for fallback rules. MemRLFailureClassAny MemRLFailureClass = "*" - // MemRLFailureClassPreMortemFail maps to pre-mortem gate FAIL. - MemRLFailureClassPreMortemFail MemRLFailureClass = "pre_mortem_fail" + // MemRLFailureClassPreMortemFail maps to premortem gate FAIL. + MemRLFailureClassPreMortemFail MemRLFailureClass = "premortem_fail" // MemRLFailureClassCrankBlocked maps to crank BLOCKED status. MemRLFailureClassCrankBlocked MemRLFailureClass = "crank_blocked" diff --git a/cli/internal/types/mortem_compatibility_test.go b/cli/internal/types/mortem_naming_test.go similarity index 100% rename from cli/internal/types/mortem_compatibility_test.go rename to cli/internal/types/mortem_naming_test.go diff --git a/cli/internal/types/types.go b/cli/internal/types/types.go index c546a6f3c..602be7103 100644 --- a/cli/internal/types/types.go +++ b/cli/internal/types/types.go @@ -841,10 +841,10 @@ type FlywheelMetrics struct { // PriorArtBypasses is how many times users bypassed the prior art gate. PriorArtBypasses int `json:"prior_art_bypasses,omitempty"` - // RetrosWithLearnings is post-mortems that created extractable learnings. + // RetrosWithLearnings is postmortems that created extractable learnings. RetrosWithLearnings int `json:"retros_with_learnings,omitempty"` - // TotalRetros is total post-mortem count this period. + // TotalRetros is total postmortem count this period. TotalRetros int `json:"total_retros,omitempty"` // --- MemRL Utility Metrics (ol-memrl) --- diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 3815f1c6d..2979b8c70 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -19,7 +19,7 @@ AgentOps 3.2 is the **verification-keystone** minor: the cross-family review loo - **Verdict-integrity layers** (age-rk3r): REBOUND patch-id verdicts authorize byte-identical rebases without a re-review (honored by the portable push-gate and CI with Go-side lineage+proof re-validation, plus a keep-ref so CI can re-verify an orphaned reviewed commit); opt-in strict two-family cold quorum that refuses to degrade (honest-UNAVAILABLE); outage-only cold reviewer failover chain with explicit degraded labels; evidence-quality floor for CONFIRMED; live-smoke verify mode; provenance v1.1 verdict-edge enrichment (reviewer_family, degraded, rounds, duration_s, evidence_path). - **Membrane memory** (age-zpj5, age-membrane-memory-arch-tz2s): `ao membrane catch` records panel catches (class key + affected paths), `ao membrane recall --include-catches` retrieves them by domain into prompts, `ao membrane triage` reports honest two-axis recurrence; the escape→derived-check loop is proven end-to-end on the shipped binary (`scripts/em-loop-donetest.sh`) with learned constraints traveling to CI and clean clones. - **`ao wiki`** — the OpenKB port (age-port-openkb-into-agentops-go-5qw): `init/use/add/remove/recompile/lint/status`, gold-wiki compilation from `.agents/` with sanitize+mine, and verdict-gated publish bound to a content digest; retrieval integration via `ao lookup --gold` and `--pointers` bounded retrieval with a cold-start ε-exploration floor. -- **Plan-pawl duel** (age-plan-pawl-9yib): `ao plan-pawl decide` deterministic duel decider; `/discovery` gains `--duel/--no-duel/--duel-rounds`; pre-mortem gains the duel checklist; ApprovalEdge records two judge panes. +- **Plan-pawl duel** (age-plan-pawl-9yib): `ao plan-pawl decide` deterministic duel decider; `/discovery` gains `--duel/--no-duel/--duel-rounds`; premortem gains the duel checklist; ApprovalEdge records two judge panes. - **Real token accounting + governor**: per-bead token capture kills the hardcoded zeros, `ao yield tokens` parses Claude and Codex transcripts (deduped by response id, loud on absent usage), and `ao governor budget` adds an SPC error-budget with a two-sided noise band (age-membrane-memory-arch-tz2s.3, .7). - **Gate wave** (age-gate-the-ungated-egwt, age-push-equals-ci-0ua): full `-race` suite on push-to-main, provenance hash-chain gate at the pre-push boundary, `docs.cli-snippets` (live docs resolve against the cobra tree), `docs.skill-refs --all-docs`, `docs.demoted-claims` honesty lexicon, script-preamble ratchet, ADR-registry check, fail-closed pinned golangci-lint, static shell-portability gate, jsonl-scanner ratchet, and a report-only CI verdict backstop (age-wedge-all-in-dyr0.9). - **Land pipeline** (agentops-2pl, age-genn, age-qeqv, age-tkxq): single-writer land lane with branch submit queue and default-deny gh shim, deterministic land wrapper, staged-scope pre-commit guard against concurrent-lane contamination, first-class per-item consumed markers in next-work. @@ -155,7 +155,7 @@ AgentOps 3.0 is the **hookless-first** major. The headline: AgentOps is what run - **Supergate Gap 3 (loop-closure) bats coverage** — `tests/scripts/check-three-gap-supergate.bats` extended with three tests (happy-path PASS, `goals-validate` FAIL, `flywheel-proof` SKIP) using a PATH-shimmed `go` that produces a controlled `/tmp/ao-sg` (`soc-wxh5.3`). Suite goes 15 → 18 tests; closes the cycle-63 Gap 3 deferral. - **Mandatory STEP 1.7.5 release-readiness gates** in `/validation` — auto-detects release context from branch name (`release/*`, `v*-prep`, `v*-evolve-run`, `v\d+\.\d+*`) or `--release-context` flag and requires `scripts/pre-push-gate.sh` (full, not `--fast`), `scripts/ci-local-release.sh`, and `scripts/generate-cli-reference.sh` cleanliness check when CLI surface changed. Validation refuses to recommend `/release` until all three pass. Codex parity synced. - **`/evolve` teardown pre-release checklist** — when the loop runs on a release-shaped branch, the teardown report emits an explicit unchecked checklist (regen CLI docs, full pre-push, `ci-local-release.sh`, optional smoke run) instead of recommending `/release`. The handoff artifact carries the checklist verbatim; "ready to tag" means boxes checked, not cycles green. -- **Acceptance-Text vs Delivered Drift audit** in `/post-mortem` closure-integrity check — for each closed child, parses the bead `Acceptance:` section, extracts named gates, and WARNs when the close-note does not confirm the gate ran green. Catches the failure mode where a bead's acceptance language drifts from delivered evidence (origin: cycle 182 `soc-w6vh.4`). +- **Acceptance-Text vs Delivered Drift audit** in `/postmortem` closure-integrity check — for each closed child, parses the bead `Acceptance:` section, extracts named gates, and WARNs when the close-note does not confirm the gate ran green. Catches the failure mode where a bead's acceptance language drifts from delivered evidence (origin: cycle 182 `soc-w6vh.4`). - **`.agents/operator/` write-surface contract entry** — `docs/contracts/agents-write-surfaces.md` now documents the BC4 `OperatorPort` durable-intent log (allowlist + classification row, lifecycle=`rolling`, writer=`cli`). ### Changed @@ -164,7 +164,7 @@ AgentOps 3.0 is the **hookless-first** major. The headline: AgentOps is what run - **`/evolve` Step 1.5 healing-first classifier** now routes through the typed BC2 `CIStatusPort` (`cli/cmd/ao/ci_status_adapter.go`, cycle 117 `productionCIStatus`) via `ao ci recent --limit 1` instead of an inline `gh run list --workflow validate.yml --json conclusion` (`soc-y5vh.2`). Both callsites (`skills/evolve/SKILL.md` + `skills/evolve/references/convergence-mechanics.md`) updated in lockstep. Zero remaining inline `gh` shell-outs in `/evolve`'s hot read path. - **`cli/cmd/ao` coverage floor** raised back to 76 % in `scripts/check-cmd-ao-coverage.sh` after real statement coverage climbed to 76.1 % (23553/30953) on the v2.41-evolve-run baseline (`soc-wxh5.1`). The cycle-60 recalibration to 75 % is reversed. - **`/evolve` session-state refresh at Step 0** so the dormancy gate stays correct after long-running cycles harvest follow-ups (cycle 171 retrospective fix). -- **`/release` skill refactor** — moved Examples + Troubleshooting + the non-HEAD cut-version logic into `references/release-workflow-detail.md` to bring `SKILL.md` back under the `tier=execution` size limit. Behavioral surface unchanged; cycle 169 post-mortem restoration commit restored Examples and Troubleshooting after the initial extraction. +- **`/release` skill refactor** — moved Examples + Troubleshooting + the non-HEAD cut-version logic into `references/release-workflow-detail.md` to bring `SKILL.md` back under the `tier=execution` size limit. Behavioral surface unchanged; cycle 169 postmortem restoration commit restored Examples and Troubleshooting after the initial extraction. ### Fixed @@ -177,7 +177,7 @@ AgentOps 3.0 is the **hookless-first** major. The headline: AgentOps is what run ### Internal -- **DDD/Hex architecture rescope arc — 13 cycles closed.** Phase-1 complete + phase-2 retrospective (`docs/rescope/2026-05-13-ddd-hex-architecture-rescope.md`); test-architecture-debt analysis reconciled to deletions; BC ports phase-2 narrowness post-mortem captured (`docs/learnings/2026-05-13-bc-ports-narrowness-postmortem.md`). +- **DDD/Hex architecture rescope arc — 13 cycles closed.** Phase-1 complete + phase-2 retrospective (`docs/rescope/2026-05-13-ddd-hex-architecture-rescope.md`); test-architecture-debt analysis reconciled to deletions; BC ports phase-2 narrowness postmortem captured (`docs/learnings/2026-05-13-bc-ports-narrowness-postmortem.md`). - **Learnings catalog hygiene** — README + when-to-add rubric (cycle 134), empirical /loop context-drift study over 87+ cycles (cycle 135), BC-ports wire-up arc retrospective (cycle 122), CLI-wiring cycle-shape template (cycles 144–146), substring sed-rename overreach warning. - **Contract drift resolution** — `soc-5yuy` drift #5 resolved via audit; `cli-skills-map.md` refreshed to 70 generated CLI command headings for v2.41-prep BC-arc commands. @@ -294,9 +294,9 @@ AgentOps 3.0 is the **hookless-first** major. The headline: AgentOps is what run ### Changed - **`--no-lifecycle` in `/discovery` renamed to `--no-scaffold`** for semantic clarity — the flag controls STEP 4.5 scaffold auto-invocation only, not broader lifecycle checks. `--no-lifecycle` is honored as a deprecated alias through v2.40.0; when both flags are passed, they are equivalent. Other skills (`/crank`, `/validation`, `/implement`, `/evolve`) retain `--no-lifecycle` with its existing lifecycle-skill-invocation semantics. -- **`/discovery` flags table** expanded: `--auto` is now explicitly documented (was transitively honored but undocumented); `--interactive` scope clarified ("research + plan gates, not pre-mortem"). +- **`/discovery` flags table** expanded: `--auto` is now explicitly documented (was transitively honored but undocumented); `--interactive` scope clarified ("research + plan gates, not premortem"). - **`/validation` flags table** expanded: `--complexity=` syntax formalized to match `/rpi` and `/discovery`; `--interactive` scope documented. -- **`/rpi` `--interactive` flag** scope note added: applies to discovery (research + plan) and validation (Gate 1, Gate 2); does NOT override pre-mortem or vibe council autonomy. +- **`/rpi` `--interactive` flag** scope note added: applies to discovery (research + plan) and validation (Gate 1, Gate 2); does NOT override premortem or vibe council autonomy. - **ASCII fast-path performance sweep** across rune-aware truncation call sites in `cli/` (`TruncateText`, `TruncateRunes`, `truncateForError`, plus goals/pool/search/rpi/parser call sites) — ASCII inputs now skip the full UTF-8 rune scan. - **Compile and overnight internals refactored** — `runCompile` split into phase + preflight helpers; article scan, inbound count, and prune extracted from `repair`; dream packet corroboration split per source epic; dream yield emptiness guard extracted into a dedicated helper. No behavior change; lower cyclomatic complexity and tighter test surfaces. - **Skills-codex DAG bodies converted to `$skill` notation** for the Codex runtime. @@ -328,12 +328,12 @@ AgentOps 3.0 is the **hookless-first** major. The headline: AgentOps is what run ### Changed - **Release and pre-push validation** — local release, pre-push, and command coverage gates now validate more of the hook, evidence, and Codex runtime surface before publish. -- **Codex/runtime artifacts and docs** — compile, evolve, post-mortem, swarm, and related runtime docs and artifacts were decomposed and synchronized to better match shipped behavior. +- **Codex/runtime artifacts and docs** — compile, evolve, postmortem, swarm, and related runtime docs and artifacts were decomposed and synchronized to better match shipped behavior. - **Flywheel backlog bookkeeping** — next-work aggregates, consumed markers, and enum normalization were cleaned up so carry-forward work is recorded consistently. ### Fixed -- **Pre-mortem gate ambiguity** — the crank pre-mortem gate now denies ambiguous state by default instead of failing open. +- **Pre-mortem gate ambiguity** — the crank premortem gate now denies ambiguous state by default instead of failing open. - **CLI and shell reliability edges** — `ao rpi serve --run-id` now accepts legacy 8-hex IDs, `ao mine --dry-run` emits a single clean JSON payload, and bash invocations are sanitized to bypass unsafe shell aliases. - **Compile, harvest, and release drift** — compile repair defaults, malformed frontmatter salvage, YAML parse error surfacing, CI fixture drift, shellcheck drift, and Codex artifact metadata drift were corrected. @@ -369,7 +369,7 @@ AgentOps 3.0 is the **hookless-first** major. The headline: AgentOps is what run - **Knowledge scoring and search behavior** — inject now deduplicates by content hash, boosts indexed pages, weights stability, and search can pull Dream vault and wiki sources with stronger local recall. - **Overnight and RPI internals** — overnight, lifecycle, search, inject, harvest, and RPI flows were decomposed into smaller helpers while tightening proof paths, mixed-mode provenance, and worktree cleanup. -- **Public framing and contributor docs** — README, philosophy, planning/post-mortem docs, and reference surfaces now better match the context-compiler and operational-layer story. +- **Public framing and contributor docs** — README, philosophy, planning/postmortem docs, and reference surfaces now better match the context-compiler and operational-layer story. ### Fixed @@ -399,8 +399,8 @@ AgentOps 3.0 is the **hookless-first** major. The headline: AgentOps is what run - **RPI wave recovery integrated** — recovered RPI wave work landed across Dream, council, stale-scope planning, discovery artifacts, CI hardening, and Codex runtime surfaces. - **Council `--mixed` strict contract documented** — `skills/council/references/cli-spawning.md` documents that `/council --mixed` requires Codex CLI and emits a hard error instead of silently falling back to Claude-only. -- **Plan and pre-mortem skill bodies decomposed** — focused reference files now carry the detailed pre-decomposition, scope-mode, mandatory-check, output, wave-matrix, and task-creation guidance while keeping the top-level skills within lint budgets. -- **Bead-input pre-flight wired into planning skills** — `/plan` and `/pre-mortem` invoke `ao beads verify ` for full-complexity, aged, or prior-session bead inputs before decomposition or validation. +- **Plan and premortem skill bodies decomposed** — focused reference files now carry the detailed pre-decomposition, scope-mode, mandatory-check, output, wave-matrix, and task-creation guidance while keeping the top-level skills within lint budgets. +- **Bead-input pre-flight wired into planning skills** — `/plan` and `/premortem` invoke `ao beads verify ` for full-complexity, aged, or prior-session bead inputs before decomposition or validation. - **Operational-layer framing** — README, onboarding, docs, comparisons, and linked surfaces now consistently explain AgentOps as bookkeeping, validation, primitives, and flows for coding agents - **Dream runtime positioning** — the public GitHub nightly is now documented as a proof harness, while `ao overnight` is documented as the private local compounding engine - **Codex default path** — native hooks, install copy, runtime smoke coverage, and checked-in Codex artifacts are aligned around the native-plugin path on supported Codex versions @@ -441,7 +441,7 @@ AgentOps 3.0 is the **hookless-first** major. The headline: AgentOps is what run - **Defrag test flag leak** — `TestDefragOutputDirFlag` used `cmd.Flags().Lookup("output")` which matched the root persistent `--output` flag; changed to `cmd.LocalFlags().Lookup("output")` - **Goroutine leak false positive** — `TestRunGoals_GoroutineLeak` used `goleak.VerifyNone` which caught goroutines from parallel tests; switched to `goleak.IgnoreCurrent()` to only detect leaks within the test itself - **Secret scan false positives** — excluded `.gc/` directory and `Getenv`/`os.Environ` patterns from secret pattern scan -- **Codex skill validation** — added `output_contract` as valid schema key, `cross-vendor`/`knowledge` as valid tiers, fixed `$/` prefix in codex forge/post-mortem/scenario skills +- **Codex skill validation** — added `output_contract` as valid schema key, `cross-vendor`/`knowledge` as valid tiers, fixed `$/` prefix in codex forge/postmortem/scenario skills - **Scenario CLI snippets** — replaced non-existent `--source`/`--scope` flags with valid `--status` variants ### Removed @@ -509,11 +509,11 @@ AgentOps 3.0 is the **hookless-first** major. The headline: AgentOps is what run ### Fixed -- **Codex artifact parity** — restored checked-in Codex parity for red-team and cleaned Codex runtime metadata/frontmatter drift across crank, forge, post-mortem, release, and swarm artifacts +- **Codex artifact parity** — restored checked-in Codex parity for red-team and cleaned Codex runtime metadata/frontmatter drift across crank, forge, postmortem, release, and swarm artifacts - **Retrieval quality** — replaced exact-substring filtering with token-level matching and tuned penalty, deduplication, and OR-fallback behavior - **Harvest metadata preservation** — promotion now preserves source metadata and fills missing maturity, utility, and type fields safely - **Release tooling** — release artifact directories are created safely and audit artifacts now resolve against release tag names -- **Documentation and link drift** — repaired the post-mortem Codex link and aligned runtime docs around the newer startup and lifecycle flows +- **Documentation and link drift** — repaired the postmortem Codex link and aligned runtime docs around the newer startup and lifecycle flows ## [2.32.0] - 2026-04-01 @@ -570,7 +570,7 @@ AgentOps 3.0 is the **hookless-first** major. The headline: AgentOps is what run - **`ao lookup` retrieval** — fixed retrieval gaps that caused lookup to return no results - **Embedded sync** — using-agentops SKILL.md and `.agents/.gitignore` now written correctly on first session start - **Closure integrity** — 24h grace window for close-before-commit evidence, normalized file parsing -- **Skill lint compliance** — vibe, post-mortem, crank, and plan skills trimmed or restructured to stay under 800-line limit +- **Skill lint compliance** — vibe, postmortem, crank, and plan skills trimmed or restructured to stay under 800-line limit - **Codex tool naming** — added CLAUDE_TOOL_NAMING rule and fixed 5 Claude-era tool references in codex skills - **ASCII diagram consistency** — aligned box-drawing characters across 23 documentation files - **Fork exhaustion prevention** — replaced jq with awk in validate-go-fast to prevent fork bombs on large repos @@ -599,7 +599,7 @@ AgentOps 3.0 is the **hookless-first** major. The headline: AgentOps is what run - **Model cost tiers and config writes** — `ao config` can now assign per-agent models by cost tier and persist repo configuration changes directly - **Search brokerage over session history and repo knowledge** — `ao search` now wraps upstream `cass` results with repo-local AgentOps artifacts by default -- **Reviewer and post-mortem reference packs** — Added model-routing, iterative-retrieval, confidence-scoring, write-time-quality, and conflict-recovery guidance across council, research, swarm, vibe, compile, and related skills +- **Reviewer and postmortem reference packs** — Added model-routing, iterative-retrieval, confidence-scoring, write-time-quality, and conflict-recovery guidance across council, research, swarm, vibe, compile, and related skills ### Changed @@ -694,7 +694,7 @@ AgentOps 3.0 is the **hookless-first** major. The headline: AgentOps is what run ### Fixed -- **Codex BF pyramid parity** — Synced BF1/BF2/BF4 bug-finding level selection into skills-codex implement, post-mortem, and validation skills +- **Codex BF pyramid parity** — Synced BF1/BF2/BF4 bug-finding level selection into skills-codex implement, postmortem, and validation skills - **Codex Claude backend cross-contamination** — Removed orphaned `backend-claude-teams.md` files (Claude primitives: TeamCreate, SendMessage) from 4 Codex skills (council, research, shared, swarm) - **Dead converter rule** — Removed stale sed substitution for `backend-claude-teams.md` rename in converter script - **Swarm reference integrity** — Added Reference Documents section to swarm SKILL.md; updated validate.sh to check only Codex-native backend references @@ -704,14 +704,14 @@ AgentOps 3.0 is the **hookless-first** major. The headline: AgentOps is what run ### Added - **L0–L7 test pyramid standard** — Shared reference doc (`standards/references/test-pyramid.md`) defining 8 test levels, agent autonomy boundaries (L0–L3 autonomous, L4+ human-guided), and RPI phase mapping -- **Test pyramid integration across RPI lifecycle** — Discovery identifies test levels, plan classifies tests by level, pre-mortem validates coverage, implement selects TDD level, crank carries `test_levels` metadata, validation audits coverage, post-mortem reports gaps +- **Test pyramid integration across RPI lifecycle** — Discovery identifies test levels, plan classifies tests by level, premortem validates coverage, implement selects TDD level, crank carries `test_levels` metadata, validation audits coverage, postmortem reports gaps - **RPI autonomous execution enforcement** — Three-Phase Rule mandates discovery → implementation → validation without human interruption; anti-patterns table documents 7 failure modes - **Evolve autonomous execution enforcement** — Each cycle runs a complete 3-phase `/rpi --auto`; anti-patterns table documents 6 failure modes; large work decomposed into sub-RPI cycles - **Codex skill standard** — New `standards/references/codex-skill.md` with tool mapping, prohibited primitives, two-phase validation, DAG-first traversal, and prompt constraint boundaries - **Codex-native overrides** — Durable overrides for crank, swarm, council that survive regeneration - **DAG-based Codex smoke test** — `scripts/smoke-test-codex-skills.sh` validates 54 skills with dependency-ordered traversal - **Codex skill API contract** — `docs/contracts/codex-skill-api.md` with conformance validator -- **Output contract declarations** — `output_contract` field on council, vibe, pre-mortem, research skills with canonical finding-item schema +- **Output contract declarations** — `output_contract` field on council, vibe, premortem, research skills with canonical finding-item schema ### Changed @@ -732,13 +732,13 @@ AgentOps 3.0 is the **hookless-first** major. The headline: AgentOps is what run - **Error & rescue map template** — Pre-mortem Step 2.5 with 3 worked examples (HTTP, database, LLM) - **Scope mode selection** — Pre-mortem Step 1.6 with 3-mode framework (Expand/Hold/Reduce) and auto-detection - **Temporal interrogation** — Pre-mortem Step 2.4 walks implementation timeline (hour 1/2/4/6+) for time-dependent risks -- **Prediction tracking** — Pre-mortem findings get unique IDs (`pm-YYYYMMDD-NNN`) correlated through vibe and post-mortem +- **Prediction tracking** — Pre-mortem findings get unique IDs (`pm-YYYYMMDD-NNN`) correlated through vibe and postmortem - **Finding classification** — Vibe separates CRITICAL (blocks ship) from INFORMATIONAL findings - **Suppression framework** — Vibe loads default + project-level suppression patterns for known false positives - **Domain-specific checklists** — Standards skill extended with SQL safety, LLM trust boundary, and race condition checklists, auto-loaded by vibe - **RPI session streak tracking** — Post-mortem Step 1.5 shows consecutive session days and verdict history - **Persistent retro history** — Post-mortem Step 4.8 writes structured JSON summaries to `.agents/retro/` for cross-epic trend analysis -- **Prediction accuracy scoring** — Post-mortem Step 3.5 scores HIT/MISS/SURPRISE against pre-mortem predictions +- **Prediction accuracy scoring** — Post-mortem Step 3.5 scores HIT/MISS/SURPRISE against premortem predictions - **Commit split advisor** — PR-prep Phase 4.5 suggests bisectable commit ordering (suggestion-only) - **Council finding auto-extraction** — Significant findings from WARN/FAIL verdicts staged for flywheel consumption @@ -761,7 +761,7 @@ AgentOps 3.0 is the **hookless-first** major. The headline: AgentOps is what run - **Discovery and validation phase orchestrators** — New `/discovery` and `/validation` skills decompose the RPI lifecycle into independently - invocable phases (research+plan+pre-mortem and vibe+post-mortem) + invocable phases (research+plan+premortem and vibe+postmortem) - **Stigmergic packet scorecard** — Ranked scoring for flywheel knowledge packets so higher-utility learnings surface first - **Pinned work queue** — `/evolve` gains a pinned work queue with blocker @@ -994,7 +994,7 @@ AgentOps 3.0 is the **hookless-first** major. The headline: AgentOps is what run - High-complexity CLI paths refactored (`runRPIParallel`, `runDedup`, `parseGatesTable`) to lower cyclomatic complexity. ### Fixed -- Multiple post-mortem remediation waves landed for CLI/RPI/swarm reliability and edge-case handling. +- Multiple postmortem remediation waves landed for CLI/RPI/swarm reliability and edge-case handling. - Hook delegation and integration behavior corrected for flat command namespace. - `heal.sh` false-positive behavior reduced and doctor stale-path detection improved. - Skill/doc parity and cross-reference drift issues corrected across codex and core skill catalogs. @@ -1085,7 +1085,7 @@ AgentOps 3.0 is the **hookless-first** major. The headline: AgentOps is what run - 6 new tests for dedup, ambiguity detection, iteration cap, duplicate markers - Cobra pflag state pollution between test invocations — explicit flag reset in `executeCommand()` helper - Goals validate.sh outdated checks and missing validate.sh for 7 skills -- 10 tech debt findings from ag-8km+ag-chm post-mortem (stale nudge, scanner, docs) +- 10 tech debt findings from ag-8km+ag-chm postmortem (stale nudge, scanner, docs) - ao binary codesigned with stable Mach-O identifier - Hook integration tests updated — removed 8 stale standalone ao-* hook tests consolidated into session-end-maintenance.sh @@ -1112,7 +1112,7 @@ AgentOps 3.0 is the **hookless-first** major. The headline: AgentOps is what run ### Added - Evolve idle hardening — disk-derived stagnation detection, 60-minute circuit breaker, rolling fitness files, no idle commits -- Evolve `--quality` mode — findings-first priority cascade that prioritizes post-mortem findings over goals +- Evolve `--quality` mode — findings-first priority cascade that prioritizes postmortem findings over goals - Evolve cycle-history.jsonl canonical schema standardization and artifact-only commit gating - `heal-skill` checks 7-10 with `--strict` CI gate for automated skill maintenance - 6-phase E2E validation test suite for RPI lifecycle (gate retries, complexity scaling, phase summaries, promise tags) @@ -1138,7 +1138,7 @@ AgentOps 3.0 is the **hookless-first** major. The headline: AgentOps is what run - Handoff example filename format corrected to `YYYYMMDDTHHMMSSZ` spec - Quickstart step numbering corrected (7 before 8) - OpenAI docs skill: added Claude Code MCP alternative to Codex-only fallback -- Dead link to `conflict-resolution-algorithm.md` removed from post-mortem +- Dead link to `conflict-resolution-algorithm.md` removed from postmortem - `ao forge search` → `ao search` in provenance and knowledge skills - OSS docs: root-level doc path checks, removed golden-init reference - Reverse-engineer-rpi fixture paths and contract refs corrected diff --git a/docs/CI-CD.md b/docs/CI-CD.md index b41d732a1..4cc05c2e7 100644 --- a/docs/CI-CD.md +++ b/docs/CI-CD.md @@ -5,6 +5,8 @@ content identity, one author-distinct Validate verdict, and a durable verdict artifact. It does not own Git delivery, merge policy, retries, queues, work ownership, or release transitions. +Repositories own delivery policy for local and cloud agents. + ## Separation of responsibilities ```text @@ -16,11 +18,9 @@ Repository: deterministic checks -> repository-selected Git/CI/release policy means only that its selected checks passed. It cannot create, strengthen, or replace a semantic verdict. -The installed pre-push hook runs ordinary build, race, schema, generated-drift, -and security checks. It performs no model review, admission decision, tracker -transition, delivery serialization, or provenance backstop. Repositories may -replace or omit the hook and may use direct push, pull requests, external CI, -or another delivery process. +AgentOps installs no push hook and does not choose how a repository invokes +these checks. A repository may call `ao gate check` from a local command, CI, +pull request, merge queue, or another delivery process. ## GitHub workflows @@ -50,22 +50,10 @@ once for the complete candidate: ```bash cd cli && go test ./... -./scripts/check-cathedral-cut-conformance.py -./scripts/check-skill-mesh.py -./scripts/ci-local-release.sh +python3 scripts/check-cathedral-cut-conformance.py +python3 scripts/generate-skill-mesh.py --check +bash scripts/ci-local-release.sh ``` The local release script validates this repository's release artifacts. That is repository policy, not an AgentOps lifecycle transition. - -## Bootstrap bypass - -The Cathedral Cut itself may use the audited one-time bypass: - -```bash -AGENTOPS_GATE_DISABLED=1 git push -``` - -The hook logs that bypass. It does not grant semantic validity; this migration -still requires the full ordinary suite and a fresh `verdict.v2` for the exact -candidate before the push. diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index a331fb571..eab573ab3 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -22,9 +22,6 @@ If you want the fastest path to a meaningful first contribution, start here: ```bash git clone https://github.com/YOUR_USERNAME/agentops.git cd agentops - -# Optional, but useful for local workflow testing -bash scripts/install-dev-hooks.sh ``` ## High-Leverage Ways To Contribute @@ -86,10 +83,10 @@ bash scripts/audit-codex-parity.sh --skill your-skill-name bash scripts/validate-codex-generated-artifacts.sh --scope worktree ``` -Before pushing, the recommended fast gate is: +For a fast changed-surface check, run: ```bash -scripts/pre-push-gate.sh --fast +ao gate check --fast --scope worktree ``` ### Working On The Docs Site @@ -111,7 +108,7 @@ MkDocs-specific expectations: - Skill pages and the CLI reference are **generated at build time** from `skills/**/SKILL.md` and `cli/docs/COMMANDS.md` respectively — do not hand-author `docs/skills/*.md` or `docs/cli/commands.md`. - Navigation is declared in `mkdocs.yml` under `nav:`. New top-level docs need an entry there. -Python toolchain is required only for local preview and the strict build. If your dev machine can't install Python, set `PRE_PUSH_SKIP_MKDOCS=1` to bypass the MkDocs check in the pre-push gate; CI will catch it. +Python tooling is required only for local preview and the strict build. If it is unavailable, report that the docs build was not checked; do not treat another green check as equivalent evidence. ## Opening The PR diff --git a/docs/ENV-VARS.md b/docs/ENV-VARS.md index e24f5b023..c612c93e0 100644 --- a/docs/ENV-VARS.md +++ b/docs/ENV-VARS.md @@ -7,7 +7,6 @@ Repository tooling recognizes a small host-policy surface: | Variable | Meaning | |---|---| -| `AGENTOPS_GATE_DISABLED=1` | Explicit repository pre-push bypass. This does not create semantic evidence. | | `AO_BIN` | Select the `ao` executable used by a deterministic gate subprocess. | | `CODEX_HOME` | Codex runtime profile root used by the optional Codex adapter. | diff --git a/docs/INCIDENT-RUNBOOK.md b/docs/INCIDENT-RUNBOOK.md index 070827322..e7e538b99 100644 --- a/docs/INCIDENT-RUNBOOK.md +++ b/docs/INCIDENT-RUNBOOK.md @@ -1,397 +1,37 @@ -# AgentOps Incident Runbook — Consumer Recovery +# AgentOps Incident Runbook -> **Audience:** Anyone responding to a broken AgentOps installation. -> **Assumption:** You are stressed and need copy-pasteable commands. Each section is self-contained. +## Preserve evidence ---- +Copy the relevant PlanPacket, CandidatePacket, manifest, command output, and +verdict before changing the subject. Do not treat an incomplete or corrupt +artifact as PASS. -## Table of Contents +## Classify the boundary -1. [Emergency Kill Switches](#1-emergency-kill-switches) -2. [Scenario A: Broken Skills After Update](#2-scenario-a-broken-skills-after-update) -3. [Scenario B: Evolve Pushed Bad Code to Main](#3-scenario-b-evolve-pushed-bad-code-to-main) -4. [Scenario C: Skills Not Loading / CI Gate Failing](#4-scenario-c-skills-not-loading-ci-gate-failing) -5. [Rollback Options](#5-rollback-options) -6. [Root Cause Analysis](#6-root-cause-analysis) -7. [Prevention Checklist](#7-prevention-checklist) +- `FAIL`: a concrete acceptance defect was proven. +- `NOT_PROVEN`: identity, freshness, scope coverage, evidence, or persistence + was insufficient. +- deterministic command failure: the repository check failed independently of + semantic validation. +- installation drift: generated projections or installed skill links differ + from their source metadata. ---- +AgentOps reports and stops. The operator or calling system chooses recovery, +revision, rollback, Git, CI, and release actions. -## 1. Emergency Kill Switches - -**Do these FIRST if sessions are broken. Restore functionality, then investigate.** +## Repository diagnostics ```bash -# Stop evolve from running (persistent across sessions) -mkdir -p ~/.config/evolve -echo "incident $(date -Iseconds)" > ~/.config/evolve/KILL +python3 scripts/check-cathedral-cut-conformance.py +scripts/regen-all.sh --check +cd cli && go test ./... ``` -> **AgentOps 3.0 is hookless.** A default install ships **zero** hooks — nothing auto-runs at -> session start, so there is no global "disable hooks" recovery step to take. Orientation is -> explicit (`ao session bootstrap`, `ao inject`) and CI (`.github/workflows/validate.yml`) is the -> authoritative gate. If you authored your own hooks via the `hooks-authoring` skill, see the -> note under [Scenario C](#4-scenario-c-skills-not-loading-ci-gate-failing) for how to disable them. +Run only the command needed to reproduce the observed failure before expanding +to the full deterministic suite. ---- +## Escalation -## 2. Scenario A: Broken Skills After Update - -**Symptom:** Consumer ran the install script and now Claude sessions are broken — skills don't load, `ao` subcommands error, or skill invocations fail. - -### Triage (< 5 min) - -```bash -# 1. Check what version was installed -cat ~/.claude/skills/agentops/plugin.json 2>/dev/null | jq -r '.version' -# Or check the marketplace cache -cat ~/.claude/plugins/marketplaces/agentops-marketplace/plugin.json 2>/dev/null | jq -r '.version' - -# 2. Check if skills are symlinks (known failure mode) -ls -la ~/.claude/skills/ | head -20 - -# 3. Confirm the ao CLI is on PATH and runs -which ao && ao status -``` - -### Fix: Reinstall from a known-good version - -```bash -# Remove broken installation -rm -rf ~/.claude/skills/agentops - -# Remove any symlinks (known failure: installer cannot write through symlinks) -find ~/.claude/skills -maxdepth 1 -type l -delete - -# Reinstall from latest Claude plugin -claude plugin marketplace update agentops-marketplace -claude plugin install agentops@agentops-marketplace - -# OR reinstall marketplace + plugin -claude plugin marketplace add boshu2/agentops -claude plugin install agentops@agentops-marketplace -``` - -### Fix: Nuke and reinstall (if pinning doesn't work) - -```bash -# Nuclear option: remove everything and reinstall -rm -rf ~/.claude/skills/agentops -rm -rf ~/.claude/plugins/marketplaces/agentops-marketplace - -# Reinstall plugin -claude plugin marketplace add boshu2/agentops -claude plugin install agentops@agentops-marketplace -``` - -### Verify the fix - -```bash -# Confirm skills resolve and the CLI runs -ls ~/.claude/skills/agentops/ | head -ao status -``` - ---- - -## 3. Scenario B: Evolve Pushed Bad Code to Main - -**Symptom:** `/evolve` ran autonomously, committed code that breaks builds, tests, or other skills. The regression gate failed to catch it, or evolve committed before the gate ran. - -### Triage (< 5 min) - -```bash -# 1. Stop evolve immediately -mkdir -p ~/.config/evolve -echo "incident: bad code on main $(date -Iseconds)" > ~/.config/evolve/KILL - -# Also set local stop in the repo -echo "emergency stop" > .agents/evolve/STOP - -# 2. Check what evolve did -cat .agents/evolve/cycle-history.jsonl 2>/dev/null # cycle outcomes -cat .agents/evolve/session-summary.md 2>/dev/null # session wrap-up -ls -lt .agents/evolve/fitness-*.json 2>/dev/null # fitness snapshots - -# 3. Find evolve's commits -git log --oneline -20 # look for evolve/rpi commit messages -``` - -### Revert evolve's changes - -```bash -# Find the last good commit (before evolve ran) -# Look at fitness snapshots for session_start_sha -jq -r '.cycle_start_sha' .agents/evolve/fitness-0.json 2>/dev/null - -# Or find it manually -git log --oneline -30 | less - -# Revert everything after the known-good SHA -GOOD_SHA="" -git revert --no-commit ${GOOD_SHA}..HEAD -git commit -m "revert: evolve incident — rolling back to ${GOOD_SHA}" - -# Verify -cd cli && go build ./cmd/ao && go test ./... -./tests/run-all.sh -``` - -### If evolve is mid-run (still executing) - -```bash -# Kill switch stops it at the next cycle boundary -mkdir -p ~/.config/evolve -echo "emergency stop" > ~/.config/evolve/KILL - -# If it's in a tmux session, also kill the process -# Find the session -tmux list-sessions | grep -i evolve -# Kill it -tmux kill-session -t -``` - -### Re-enable evolve after fix - -```bash -rm ~/.config/evolve/KILL -rm .agents/evolve/STOP 2>/dev/null -``` - ---- - -## 4. Scenario C: Skills Not Loading / CI Gate Failing - -**Symptom:** A skill won't invoke (Claude reports the skill is missing or malformed), or a PR is blocked because a CI gate in `.github/workflows/validate.yml` fails. AgentOps 3.0 is hookless — there is no session-start hook to misfire, so a broken skill or a failing gate is the usual culprit. - -### Triage (< 5 min) - -```bash -# 1. Confirm the skill exists and has a valid manifest -ls ~/.claude/skills/agentops/skills//SKILL.md -# Frontmatter must parse — a malformed SKILL.md silently fails to load -head -20 ~/.claude/skills/agentops/skills//SKILL.md - -# 2. If a push is rejected, see which gate failed (the local pre-push Go gate is the authority; CI is a backstop) -gh pr checks - -# 3. Reproduce the failing gate locally (run the FULL job, not a subset) -cat .github/workflows/validate.yml | grep -n "run:" | head -40 # find the step -bash scripts/.sh # run it -``` - -### Common failures - -**Malformed or unregistered skill:** -```bash -# Skills source of truth is skills/ in the repo; the installed copy lives under -# ~/.claude/skills/agentops/. A skill that isn't in the registry won't be offered. -ls skills//SKILL.md # source of truth -bash scripts/check-registry-drift.sh # catches missing-from-registry skills -``` - -**CI gate disagreement (docs drifted from executable behavior):** -```bash -# Contracts/counts/context-map gates fail when a generated surface is stale. -# Regenerate the derived surfaces, then re-run the gate. -bash scripts/regen-all.sh 2>/dev/null || true -bash scripts/validate-context-map-drift.sh -``` - -**Missing binary (ao, jq):** -```bash -which ao # CLI must be installed and on PATH -which jq # required by several validation scripts -``` - -### Optional: user-authored hooks - -AgentOps ships **no** hooks by default. If you opted in and authored your own hooks via the -`hooks-authoring` skill, hook troubleshooting applies to **those** files only — not to any shipped -default. For backward-compat, AgentOps-authored hooks still honor the `AGENTOPS_HOOKS_DISABLED=1` -environment variable as an opt-out: - -```bash -# Only relevant if you authored your own hooks. Disables AgentOps-aware hooks for the session. -export AGENTOPS_HOOKS_DISABLED=1 -# Then debug your own hook scripts wherever you installed them. -``` - ---- - -## 5. Rollback Options - -### Option A: Reinstall latest Claude plugin - -```bash -# Refresh marketplace source and reinstall plugin -claude plugin marketplace update agentops-marketplace -claude plugin install agentops@agentops-marketplace -``` - -### Option B: Pin to a specific commit - -```bash -# Clone and install from a known-good commit -cd /tmp -git clone https://github.com/boshu2/agentops.git agentops-recovery -cd agentops-recovery -git checkout - -# Copy skills manually -rm -rf ~/.claude/skills/agentops -cp -r . ~/.claude/skills/agentops -``` - -### Option C: Sync from marketplace cache - -```bash -# The marketplace cache may have a working version -cd ~/.claude/plugins/marketplaces/agentops-marketplace -git log --oneline -10 # find a good state -git checkout - -# Then reinstall plugin from marketplace source -claude plugin install agentops@agentops-marketplace -``` - -### Option D: Nuclear reinstall - -```bash -# Remove everything AgentOps-related -rm -rf ~/.claude/skills/agentops -rm -rf ~/.claude/plugins/marketplaces/agentops-marketplace -find ~/.claude/skills -maxdepth 1 -type l -delete # remove symlinks - -# Clear any cached state -rm -rf ~/.config/evolve/KILL 2>/dev/null - -# Fresh install (plugin path) -claude plugin marketplace add boshu2/agentops -claude plugin install agentops@agentops-marketplace - -# Verify -cat ~/.claude/skills/agentops/.claude-plugin/plugin.json | jq -r '.version' -ao status -``` - ---- - -## 6. Root Cause Analysis - -After restoring service, investigate what went wrong. - -### Was it an evolve regression? - -```bash -# Check evolve history -cat .agents/evolve/cycle-history.jsonl | jq -s '.' - -# Check fitness snapshots for regressions -for f in .agents/evolve/fitness-*-post.json; do - echo "--- $f ---" - jq '[.goals[] | select(.result == "fail") | .id]' "$f" 2>/dev/null -done - -# Check GOALS.yaml for broken check commands -cat GOALS.yaml -``` - -### Was it a bad commit? - -```bash -# Use git bisect to find the breaking commit -git bisect start -git bisect bad HEAD -git bisect good - -# For each step, run the relevant test -./tests/run-all.sh && git bisect good || git bisect bad - -# When done -git bisect reset -``` - -### Was it a skill or CI-gate failure? - -```bash -# AgentOps 3.0 is hookless — the local pre-push Go gate (ao gate check) is the authority; CI is a backstop. Reproduce it locally. -# Check which gate failed on the PR -gh pr checks - -# Run the omnibus validation locally (the same job CI runs) -bash scripts/pre-push-gate.sh - -# Check for recent skill / generated-surface changes -git log --oneline -20 -- skills/ docs/ - -# Run full test suite -./tests/run-all.sh -``` - -### Was it a dependency issue? - -```bash -# Check if ao CLI is working -ao status -ao flywheel status - -# Check Go CLI builds -cd cli && go build ./cmd/ao && go test ./... - -# Check for missing system tools -for cmd in jq shellcheck git ao; do - which "$cmd" >/dev/null 2>&1 && echo "OK: $cmd" || echo "MISSING: $cmd" -done -``` - ---- - -## 7. Prevention Checklist - -### Before releasing a new version - -- [ ] `./tests/run-all.sh` passes (all tiers) -- [ ] `./tests/smoke-test.sh` passes -- [ ] `cd cli && go build ./cmd/ao && go test -race ./...` clean -- [ ] CI validation passes locally: `bash scripts/pre-push-gate.sh` -- [ ] Generated/derived surfaces are in sync: `bash scripts/check-registry-drift.sh` and `bash scripts/validate-context-map-drift.sh` clean -- [ ] Plugin and marketplace versions match: `jq -r '.version' .claude-plugin/plugin.json` equals `jq -r '.metadata.version' .claude-plugin/marketplace.json` - -### Before running evolve - -- [ ] GOALS.yaml check commands all work: run each `check:` value manually -- [ ] Evolve kill switch is clear: `test ! -f ~/.config/evolve/KILL && echo "clear"` -- [ ] Git working tree is clean: `git status --porcelain` is empty -- [ ] Know the current HEAD: `git rev-parse HEAD` (save this for revert) -- [ ] Set a reasonable cycle cap: `--max-cycles=3` for first run - -### After evolve completes - -- [ ] Review `cycle-history.jsonl` for any regressions -- [ ] Run `./tests/run-all.sh` manually (don't trust evolve's self-assessment) -- [ ] Check `git log --oneline -20` for reasonable commit messages -- [ ] Run `git diff ..HEAD --stat` to see total scope of changes - -### Optional hook authoring rules (only if you opt in) - -AgentOps ships no hooks. If you author your own via the `hooks-authoring` skill, the safe pattern is: - -- Honor `AGENTOPS_HOOKS_DISABLED=1` at the top so operators can opt out -- Fail open (`exit 0` on error, never `set -e`) — a broken hook must never wedge a session -- Guard all external commands: `command -v >/dev/null 2>&1 && ...` -- If a hook emits JSON, validate it with `jq .` before committing - ---- - -## Quick Reference Card - -``` -STOP EVOLVE: echo "stop" > ~/.config/evolve/KILL -CHECK CI GATE: gh pr checks -RUN GATE LOCALLY: bash scripts/pre-push-gate.sh -REINSTALL: claude plugin marketplace update agentops-marketplace && claude plugin install agentops@agentops-marketplace -NUCLEAR REINSTALL: rm -rf ~/.claude/skills/agentops ~/.claude/plugins/marketplaces/agentops-marketplace && claude plugin marketplace add boshu2/agentops && claude plugin install agentops@agentops-marketplace -REVERT EVOLVE: git revert --no-commit ..HEAD && git commit -m "revert: evolve incident" -VERSION CHECK: jq -r '.version' ~/.claude/skills/agentops/.claude-plugin/plugin.json -``` +Escalate to the operator when authority, credentials, safety judgment, or an +external system change is required. A failed check or reviewer disagreement is +evidence to report, not an AgentOps-controlled retry state. diff --git a/docs/LOOP-BUDGET.md b/docs/LOOP-BUDGET.md deleted file mode 100644 index 05cba3f9d..000000000 --- a/docs/LOOP-BUDGET.md +++ /dev/null @@ -1,142 +0,0 @@ -# Loop Budget - -Dev loop structure and latency budgets for the AgentOps repo. Every check and CI job is classified into one loop. Adding a new check requires declaring its loop affinity and proving it fits the budget. - -## Loop Definitions - -| Loop | Latency Budget | Question Answered | Gate Character | -|------|---------------|-------------------|----------------| -| **Inner** | <10s | "Does it compile?" | `make inner` — non-blocking, instant feedback | -| **Middle (blocking)** | <30s | "Will this break others?" | Pre-push pass 1 — blocks push | -| **Middle (advisory)** | <3min | "Are there quality concerns?" | Pre-push pass 2 — warns, does not block | -| **Outer** | <10min | "Is the system healthy?" | CI — runs on PR/push to main | - -## Invocation - -```bash -cd cli && make inner # Inner loop (<10s) -git push # Two-pass gate (default): - # Pass 1: blocking (<30s) - # Pass 2: advisory (<3min) -scripts/pre-push-gate.sh --single-pass # Full single-pass (old behavior) -``` - -## Pre-Push Check Classification - -| # | Check | Category | Loop | Blocking? | -|---|-------|----------|------|-----------| -| 1 | Go build + vet | go | Middle (blocking) | Yes | -| 2 | Go race tests (changed scope) | go | Middle (blocking) | Yes | -| 3 | Command/test pairing | go | Middle (blocking) | Yes | -| 3a | Mutation-route bypass guard | always | Middle (blocking) | Yes | -| 3b | HOME isolation in test files | go | Middle (blocking) | Yes | -| 3b2 | Test HOME isolation (broader) | go/shell | Middle (blocking) | Yes | -| 3d | .agents/ write-surface contract | always | Middle (advisory) | No | -| 4 | cmd/ao coverage floor | go | Middle (advisory) | No | -| 4b | Per-package coverage ratchet | go | Outer (CI only) | No | -| 5 | Embedded hooks sync | hook | Middle (blocking) | Yes | -| 6 | Skill count sync | skill | Middle (blocking) | Yes | -| 7 | Worktree disposition | always | Middle (advisory) | No | -| 8 | Skill runtime/CLI parity | skill | Middle (advisory) | No | -| 9 | Codex skill parity | skill | Outer (skipped) | No | -| 10 | Codex install bundle parity | skill | Outer (skipped) | No | -| 11 | Codex runtime section format | skill | Outer (CI) | No | -| 12 | Skill integrity (refs/xrefs) | skill | Middle (advisory) | No | -| 13 | Skill lint suite | skill | Middle (advisory) | No | -| 14 | Skill schema validation | skill | Middle (advisory) | No | -| 15 | Manifest schema validation | skill | Middle (advisory) | No | -| 16 | Codex artifact metadata | skill | Outer (CI) | No | -| 17 | Codex backbone prompts | skill | Outer (CI) | No | -| 18 | Codex override coverage | skill | Outer (CI) | No | -| 19 | Next-work contract parity | always | Outer (CI) | No | -| 19b | bd closeout contract parity | always | Outer (CI) | No | -| 19c | Retrieval quality ratchet | always | Outer (CI) | No | -| 20 | Skill runtime formats | skill | Middle (advisory) | No | -| 21 | Codex RPI contract | skill | Outer (CI) | No | -| 22 | Codex lifecycle guards | skill | Outer (CI) | No | -| 23 | Skill CLI snippets | skill | Middle (advisory) | No | -| 24 | Headless runtime smoke | skill | Outer (CI) | No | -| 24b | CLI docs parity | go | Middle (advisory) | No | -| 24c | Eval canaries (deterministic) | eval | Outer (CI) | No | -| 24e | Contract canaries | contract | Outer (CI) | No | -| 25 | Doc-release gate | docs | Outer (CI) | No | -| 25b | Release audit artifact refs | docs | Outer (CI) | No | -| 26 | Contract compatibility | contract | Middle (advisory) | No | -| 26a | Changed-scope derived artifact drift | derived | Middle (blocking) | Yes | -| 27 | Hook preflight | hook | Middle (blocking) | Yes | -| 28 | Hooks/docs parity | hook | Middle (advisory) | No | -| 29 | CI policy parity | ci_policy | Outer (CI) | No | -| 30 | ShellCheck | shell | Middle (advisory) | No | -| 31 | Plugin load test (symlinks) | always | Middle (blocking) | Yes | -| 32 | Learning coherence | learning | Outer (CI) | No | -| 33 | BATS orphan hooks audit | hook | Outer (CI) | No | -| 34 | Skill citation parity | skill | Outer (CI) | No | -| 35 | Flywheel health | always | Outer (CI) | No | - -## CI Job Classification - -| Job | Path Group | Loop | -|-----|-----------|------| -| go-build | go | Outer | -| cli-integration | go | Outer | -| cli-docs-parity | go | Outer | -| json-flag-consistency | go | Outer | -| embedded-sync | hooks | Outer | -| hook-preflight | hooks | Outer | -| hook-output-schema-lint | hooks | Outer | -| bats-tests | hooks | Outer | -| skill-integrity | skills | Outer | -| skill-lint | skills | Outer | -| skill-schema | skills | Outer | -| skill-dependency-check | skills | Outer | -| validate-headless-runtime-skills | skills | Outer | -| validate-codex-runtime-sections | codex | Outer | -| validate-codex-generated-artifacts | codex | Outer | -| validate-codex-backbone-prompts | codex | Outer | -| validate-codex-override-coverage | codex | Outer | -| validate-codex-rpi-contract | codex | Outer | -| validate-codex-lifecycle-guards | codex | Outer | -| agentops-eval-baseline-audit | eval | Outer | -| eval-workbench-verify | eval | Outer | -| agentops-eval-advisory | eval | Outer | -| eval-skill-delta | eval | Outer | -| doc-release-gate | docs | Outer | -| markdownlint | docs | Outer | -| smoke-test | always | Outer | -| shellcheck | shell | Outer | -| security-scan | always | Outer | -| security-toolchain-gate | always | Outer | -| agentops-contract-canaries | contracts | Outer | -| contract-compatibility-gate | contracts | Outer | -| validate-ci-policy-parity | ci | Outer | -| pre-push-gate-wired | always | Outer | -| registry-check | skills | Outer | -| plugin-load-test | always | Outer | -| learning-coherence | learning | Outer | -| memrl-health | always | Outer | -| file-manifest-overlap | always | Outer | -| doctor-check | always | Outer | -| check-test-staleness | always | Outer | -| swarm-evidence | always | Outer | -| windows-smoke | always | Outer | -| summary | always | Outer (gateway) | - -## Regeneration Scope Policy - -`derived.changed-scope` is the fast-path repair/check gate. It runs -`scripts/regen-changed-scope.sh --check --scope head`, which selects scoped -generators from the changed files and prints the matching repair command. It is -for one-slice changes and should not require release-wide churn. - -`always.regen-all` is release-wide and full-mode only. It keeps -`scripts/regen-all.sh --check` available for release prep, command deletions or -renames, broad skill-prune waves, and final sweeps where touching every derived -surface is intentional. - -## Policy: Adding New Checks - -1. Declare the loop affinity (inner/middle-blocking/middle-advisory/outer). -2. Measure the check's runtime. It must fit within the loop's latency budget. -3. Middle-blocking checks must prevent real breakage (compilation failure, sync drift, security bypass). Quality/drift/hygiene checks belong in middle-advisory or outer. -4. If the check requires `go build` or network access, it cannot be inner-loop. -5. Update this document when adding or reclassifying a check. diff --git a/docs/PRODUCT-TEMPLATE.md b/docs/PRODUCT-TEMPLATE.md index 82a22b0c8..7a0e03e5d 100644 --- a/docs/PRODUCT-TEMPLATE.md +++ b/docs/PRODUCT-TEMPLATE.md @@ -5,7 +5,8 @@ last_reviewed: YYYY-MM-DD # PRODUCT.md Template > Copy this file to your project root as `PRODUCT.md` and fill in each section. -> When present, `/pre-mortem` and `/vibe` automatically include product perspectives in council reviews. +> A caller may pass this document to Plan, Premortem, Validate, or an optional +> Council when product context is relevant. ## Mission @@ -39,11 +40,11 @@ last_reviewed: YYYY-MM-DD ## Usage -This file enables product-aware council reviews: +This file is advisory product context: -- **`/pre-mortem`** — Automatically includes `product` perspectives (user-value, adoption-barriers, competitive-position) alongside plan-review judges when this file exists. -- **`/vibe`** — Automatically includes `developer-experience` perspectives (api-clarity, error-experience, discoverability) alongside code-review judges when this file exists. -- **`/council --preset=product`** — Run product review on demand. -- **`/council --preset=developer-experience`** — Run DX review on demand. +- **`/plan`** — Use it while shaping acceptance and non-goals. +- **`/premortem`** — Challenge a frozen plan against product risks on request. +- **`/validate`** — Judge explicit product acceptance criteria against the exact candidate. +- **`/council`** — Add caller-selected product or developer-experience perspectives. -Explicit `--preset` overrides from the user skip auto-include (user intent takes precedence). +Its presence never triggers a strategy, admits work, or changes a verdict. diff --git a/docs/RELEASING.md b/docs/RELEASING.md index fa8182362..7cdc6f8a1 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -1,381 +1,76 @@ # Releasing AgentOps -This document describes the release process for the `ao` CLI and AgentOps plugin. +This is repository release policy for the `ao` binary and plugin bundles. It is +outside the AgentOps semantic loop: release checks never create a Validate +verdict, and a verdict never pushes a tag. -## Overview +## Preconditions -Releases are triggered by git tags and use a publisher-only workflow: - -``` -git tag vX.Y.Z - ↓ -┌──────────────────────────────────────────────────────┐ -│ Local gate (authoritative) │ -├──────────────────────────────────────────────────────┤ -│ ./scripts/ci-local-release.sh │ -│ - validation + tests + smoke checks │ -│ - bootstrap + pawl smoke │ -│ - SBOM + security report artifacts │ -│ - release readiness score + SIL/VIL/HIL evidence │ -└──────────────────────────────────────────────────────┘ - ↓ -┌──────────────────────────────────────────────────────┐ -│ release.yml (publisher only) │ -├──────────────────────────────────────────────────────┤ -│ - pre-publish SBOM + security + readiness evidence │ -│ - GoReleaser publish │ -│ - GitHub Release notes + assets │ -│ - Homebrew update │ -│ - SLSA provenance attestation │ -└──────────────────────────────────────────────────────┘ -``` - -## Making a Release - -### 1. Pre-release Checklist - -- [ ] Local CI release gate passes (`./scripts/ci-local-release.sh`; once the target version is known, rerun as `./scripts/ci-local-release.sh --release-version X.Y.Z`) -- [ ] Official readiness score is at least 8/10 (`release-readiness.json` has `release_status: pass`) -- [ ] SIL/VIL/HIL evidence is attached (`hil-evidence.json`; use a workflow-rich `--hil-target` or an explicit `--hil-waiver "reason"`) -- [ ] Local gate artifacts generated (`.agents/releases/local-ci//` includes SBOM, security report, readiness, and HIL evidence) -- [ ] Release validation did not mutate tracked `.agents/findings/*` metadata; `scripts/ci-local-release.sh` guards this by default. -- [ ] All tests pass locally (`cd cli && make test`) -- [ ] Full Validate green on the exact release SHA (`scripts/verify-release-ci.sh vX.Y.Z` after pushing; release tag pushes force every path-filtered release lane on, PR-only evidence jobs may be skipped, and unexpected skipped release lanes fail the summary) -- [ ] Version number follows semver (vX.Y.Z) -- [ ] CHANGELOG.md updated with release notes -- [ ] plugin.json version matches tag -- [ ] No uncommitted changes on main -- [ ] Homebrew token is valid (check secrets) - -### 1a. Release Size Check - -Releases should contain **at most 15 commits** per minor version bump. v2.2.0 had 26 commits — too large to review, debug, or bisect effectively. - -If the commit count since the last tag exceeds 15: -- Split into multiple releases (e.g., vX.Y.Z for first batch, vX.Y.(Z+1) for remainder) -- Or ensure each commit is well-scoped and the changelog covers all changes - -Check commit count: -```bash -git log $(git describe --tags --match 'v[0-9]*.[0-9]*.[0-9]*' --abbrev=0)..HEAD --oneline | wc -l -``` - -The pre-flight validation script (`scripts/validate-release.sh`) will warn (not fail) when this threshold is exceeded. - -### 1b. Historical Changelog Coverage - -The maintained changelog coverage floor is `v2.16.0`. Earlier tags are treated -as historical bootstrap tags and do not require backfilled curated notes or audit -documents for current releases. If a future release needs a complete pre-`v2.16.0` -history, file explicit backfill beads for the missing versions instead of -blocking the active release by default. - -### 1c. Agent Metadata Mutation - -Release validation must be read-only with respect to tracked finding metadata. -`scripts/ci-local-release.sh` runs the release smoke path through -`scripts/check-release-agent-metadata-stable.sh`, and -`scripts/release-smoke-test.sh` uses `ao inject --no-cite`, -`ao lookup --no-cite`, and `ao flywheel close-loop --dry-run` by default. - -Intentional citation/finding metadata refreshes are opt-in: +Choose the exact release commit, then run the ordinary deterministic suite: ```bash -AGENTOPS_RELEASE_ALLOW_AGENT_MUTATIONS=1 scripts/ci-local-release.sh +bash scripts/ci-local-release.sh --release-version X.Y.Z +bash tests/docs/validate-doc-release.sh +cd cli && go test ./... ``` -### 2. Update CHANGELOG +Inspect the generated release artifacts, the final diff, and the version in the +Go binary and plugin manifests. Local artifacts under ignored directories are +evidence for the operator; they are not tracked lifecycle state. -Follow [Keep a Changelog](https://keepachangelog.com/) format: +If this repository's operator wants semantic review of the release candidate, +invoke the Validate skill once from a fresh context against that exact subject. +Record the verdict separately. Do not make the release workflow produce or +strengthen a semantic verdict. -```markdown -## [X.Y.Z] - YYYY-MM-DD +## Publish -### Added -- Feature description - -### Changed -- Change description - -### Fixed -- Fix description -``` - -### 3. Create and Push Tag +Update `CHANGELOG.md`, commit the release changes through normal Git policy, +then create and push the annotated tag: ```bash -# Ensure you're on main and up to date -git checkout main -git pull - -# Create annotated tag git tag -a vX.Y.Z -m "Release vX.Y.Z" - -# Push commit and tag (triggers Validate and the release workflow) -git push origin main git push origin vX.Y.Z - -# Release is not done until full Validate is green for the exact tagged SHA -scripts/verify-release-ci.sh vX.Y.Z ``` -### 4. Monitor the Publisher Workflow - -Watch the release at: https://github.com/boshu2/agentops/actions - -The workflow runs three jobs: -1. **doc-release-gate** - Confirms release docs and generated references are current -2. **pre-publish-evidence** - Generates SBOM, runs the full security gate, and writes release-readiness evidence before publish -3. **publish** - Runs only after both gates pass; publishes artifacts, updates Homebrew, uploads the pre-publish evidence bundle, and signs attestation - -### 5. Verify the Release - -After the workflow completes: - -```bash -# Update Homebrew -brew update - -# Upgrade ao -brew upgrade agentops - -# Verify version -ao version -# Should show: ao version X.Y.Z -``` - -### 6. Verify Integrity (checksums + provenance) - -```bash -# Download and verify checksums -curl -sL https://github.com/boshu2/agentops/releases/download/vX.Y.Z/checksums.txt -shasum -a 256 -c checksums.txt --ignore-missing - -# Verify SLSA provenance (requires gh CLI) -gh attestation verify ao-darwin-arm64.tar.gz --repo boshu2/agentops -``` - -## Release Artifacts - -Each release produces: - -| Artifact | Description | -|----------|-------------| -| `ao-darwin-amd64.tar.gz` | macOS Intel binary | -| `ao-darwin-arm64.tar.gz` | macOS Apple Silicon binary | -| `ao-linux-amd64.tar.gz` | Linux x86_64 binary | -| `ao-linux-arm64.tar.gz` | Linux ARM64 binary | -| `checksums.txt` | SHA256 checksums for all archives | -| `sbom-cyclonedx-go-mod.json` | Publishable CycloneDX SBOM for Go dependencies | -| `security-gate-summary.json` | Security scan summary (gitleaks/semgrep/gosec/trivy/etc.) | -| `release-readiness.json` | Release readiness score with SIL/VIL/HIL status | -| `eval-agentops-fast.json` | AgentOps eval proof summary for release readiness | -| `eval-baseline-audit.json` | Eval baseline drift audit; stale suite hashes block audit resolution | -| `digital-twin-evidence.json` | Local release digital-twin/VIL proof | -| SLSA attestation | Build provenance (verifiable via `gh attestation verify`) | - -## Release Notes - -Release notes are auto-generated by GoReleaser with: -- **Header** with install/upgrade instructions and integrity verification -- **Changelog** grouped by type (features, fixes, docs, other) -- **Footer** with full changelog diff link - -The template lives in `.goreleaser.yml` under `release.header` and `release.footer`. - -## Validation Checks - -Release validation is local-first and enforced by: - -```bash -./scripts/ci-local-release.sh -``` - -This local gate runs doc checks, manifest/schema checks, smoke/integration checks, bootstrap and pawl smoke paths, binary validation, SBOM generation, security scans, AgentOps eval evidence, digital-twin/VIL evidence, and the release readiness score. Official release audits require SIL/VIL evidence plus workflow-rich HIL evidence or an explicit HIL waiver. -For command variants and expected release-E2E smoke markers, see [Release E2E Checklist](release-e2e-checklist.md). - -## Failure Modes - -### Local Gate Fails - -If `./scripts/ci-local-release.sh` fails, do not tag or publish. - -**To fix:** -1. Identify the issue from the workflow logs -2. Fix the code -3. Delete the tag: `git tag -d vX.Y.Z && git push origin :refs/tags/vX.Y.Z` -4. Re-run the local gate until all checks pass -5. Create and push the tag - -### Exact-SHA Validate Fails - -If `scripts/verify-release-ci.sh vX.Y.Z` prints `NO-GO release-ci`, the release -is not complete even if the tag exists locally or the publisher workflow starts. - -**To fix:** -1. Open the reported run URL and repair the failing Validate job. -2. Commit the fix on `main`. -3. Move the release tag to the fixed commit with `scripts/retag-release.sh vX.Y.Z`, or delete and recreate the local tag before any public release exists. -4. Re-run `scripts/verify-release-ci.sh vX.Y.Z` and record the `GO release-ci` line in the handoff or release audit notes. - -### Publish Fails - -If publisher CI fails after a local gate pass, the release may be in a partial state. - -**To fix:** -1. Check if GitHub release was created (may need manual cleanup) -2. Check if Homebrew formula was pushed -3. If the original tag-push workflow is still running for that tag, wait for it to finish before starting anything else -4. For post-tag commits, use `scripts/retag-release.sh vX.Y.Z` -5. Use `workflow_dispatch` only to rerun an existing tag after the original tag-triggered workflow has already finished and you are not pushing a new tag in parallel -6. `scripts/retag-release.sh` already waits on the tag-push workflow. It should not launch a second manual publish run for the same tag. - -Why this matters: a second publish for the same tag can collide on release assets and fail with GitHub `422 already_exists` errors if the first publish already created or uploaded assets. - -### Homebrew Token Expired - -The `HOMEBREW_TAP_GITHUB_TOKEN` secret is validated before publish. If it fails: - -1. Generate a new PAT at https://github.com/settings/tokens -2. Scope: `public_repo` (for homebrew-agentops) -3. Update the secret in repository settings - -## Manual Release (workflow_dispatch) - -Use `workflow_dispatch` only when you need to rerun an existing tag release after the original tag-triggered workflow has already completed or failed. - -Do not use `workflow_dispatch` as the primary path for a fresh release. The primary path is still `git push origin vX.Y.Z`. - -Do not launch a manual run in parallel with a tag-push run for the same tag. - -If you have post-tag commits that should become part of the same version, use: - -```bash -scripts/retag-release.sh vX.Y.Z -``` - -If you only need to retry the existing tag with no new commits: - -1. Go to Actions → Release workflow -2. Click "Run workflow" -3. Enter the tag (e.g., `vX.Y.Z`) -4. Click "Run workflow" - -## Local Testing - -Before tagging, you can test the build locally: - -```bash -# Run CI-equivalent local release gate (required) -./scripts/ci-local-release.sh - -# Once the target version is known, bind the artifacts to that version for the audit -./scripts/ci-local-release.sh --release-version X.Y.Z -./scripts/resolve-release-artifacts.sh X.Y.Z - -# Publishable local artifacts will be written to: -# .agents/releases/local-ci// -# - sbom-vX.Y.Z.cyclonedx.json -# - sbom-vX.Y.Z.spdx.json -# - security-gate-full.json -# - eval-agentops-fast.json -# - eval-baseline-audit.json -# - digital-twin-evidence.json -# - hil-evidence.json -# - release-readiness.json -# - release-artifacts.json - -# Install goreleaser -brew install goreleaser - -# Validate config -goreleaser check - -# Build snapshot (doesn't require tag) -goreleaser build --snapshot --clean --single-target - -# Test the binary -./dist/ao_darwin_arm64/ao version -./dist/ao_darwin_arm64/ao --help -./dist/ao_darwin_arm64/ao status - -# Run doc-release gate -./tests/docs/validate-doc-release.sh -``` - -## Dependency Automation Policy - -Dependency automation is managed by Dependabot and follows this policy: - -- Scope: Go modules (`/cli/go.mod`) and GitHub Actions (`/.github/workflows/*.yml`) -- Cadence: Weekly on Monday (separate schedules for Go and Actions) -- Grouping: Minor and patch updates are grouped per ecosystem -- Major updates: Opened as separate PRs for explicit review -- Security updates: Treated as priority work and merged outside normal cadence when validated -- PR limits: Capped to keep queue manageable and avoid review overload - -## Configuration Files - -| File | Purpose | -|------|---------| -| `.goreleaser.yml` | Build config, checksums, release notes, Homebrew formula | -| `.github/workflows/release.yml` | Publisher-only release workflow (publish + assets + attestation) | -| `.github/workflows/validate.yml` | CI validation (includes doc-release stabilization gate) | -| `.github/workflows/nightly.yml` | Nightly tests with failure alerts | -| `.github/dependabot.yml` | Dependabot policy and schedules for Go modules + GitHub Actions | -| `scripts/validate-release.sh` | Binary validation script | -| `tests/docs/validate-doc-release.sh` | Unified doc-release gate (links + skills + message freeze) | - -## Homebrew Tap - -The Homebrew formula is automatically pushed to: -https://github.com/boshu2/homebrew-agentops - -Users install with: -```bash -brew tap boshu2/agentops -brew install agentops -``` - -## Security - -- **Checksums:** SHA256 checksums for all archives (`checksums.txt`) -- **SLSA provenance:** Build attestation via `actions/attest-build-provenance` -- **Dependabot:** Automated dependency updates for Go modules and GitHub Actions -- **Security scan:** Secrets and dangerous pattern detection on every PR - -See [SECURITY.md](SECURITY.md) for vulnerability reporting. - -## Troubleshooting - -### "ao version" shows "dev" - -The ldflags version injection failed. Check `.goreleaser.yml`: -```yaml -ldflags: - - -s -w -X main.version={{ .Version }} -``` - -`./scripts/ci-local-release.sh` should catch this before tagging. - -### Binary artifact missing or renamed - -GoReleaser archive naming doesn't match extraction pattern. Check: -- `.goreleaser.yml` `archives` section -- Release asset names in the GitHub Release page - -### Homebrew formula not updated - -1. Check `HOMEBREW_TAP_GITHUB_TOKEN` is valid -2. Check workflow logs for push errors -3. Verify formula at homebrew-agentops repo - -### Attestation verification fails - -```bash -# Ensure gh CLI is authenticated -gh auth status - -# Try verification with verbose output -gh attestation verify --repo boshu2/agentops --verbose -``` +`.github/workflows/release.yml` is the publisher-only workflow. It builds the +declared platforms, produces checksums and attestations, publishes the GitHub +release, and updates configured package distribution. It does not merge +branches, close work, or invoke AgentOps semantic validation. + +Use `workflow_dispatch` only to retry an existing tag after inspecting the +previous publisher run. Do not start a second publisher for the same tag while +one is active. + +## Verify + +After the publisher completes: + +1. Confirm the GitHub release points to the intended tag and commit. +2. Verify checksums for at least one downloaded artifact. +3. Install the published binary in a clean temporary environment and run + `ao version` plus one read-only command. +4. Confirm Claude, Codex, and Gemini bundle manifests contain the generated + metadata-owned skill inventory. + +## Failure and rollback + +Before publication, fix the candidate and create a new exact release commit. +After publication, prefer a new patch release. Move or recreate a public tag +only when repository policy explicitly permits it and no consumer can have +observed the old artifact. + +Release failures are repository failures, not AgentOps `FAIL` or +`NOT_PROVEN` verdicts. Diagnose them with the release workflow logs and the +deterministic commands above. + +## Owned surfaces + +| Surface | Responsibility | +|---|---| +| `.goreleaser.yml` | binary archives, checksums, and release notes | +| `.github/workflows/release.yml` | tagged publication | +| `.github/workflows/validate.yml` | optional hosted deterministic checks | +| `scripts/ci-local-release.sh` | local deterministic release rehearsal | +| `scripts/validate-release.sh` | built-binary release checks | +| `tests/docs/validate-doc-release.sh` | documentation and generated-reference checks | diff --git a/docs/SKILL-API.md b/docs/SKILL-API.md index 064b8fbc8..b162e9ecc 100644 --- a/docs/SKILL-API.md +++ b/docs/SKILL-API.md @@ -170,7 +170,7 @@ metadata: | Tier | Purpose | Example Skills | |------|---------|----------------| -| `judgment` | Legacy internal tier name for validation and review gates | council, vibe, pre-mortem, post-mortem | +| `judgment` | Legacy internal tier name for validation and review gates | council, vibe, premortem, postmortem | | `execution` | Single-task implementation | implement, bug-hunt, complexity, security-suite | | `orchestration` | Multi-skill coordination | rpi, crank, swarm, evolve | | `session` | Session lifecycle | handoff, recover, status, quickstart | @@ -210,8 +210,8 @@ Core skills and their context policies: | **Validation (tier: judgment)** | | | | | | council | isolated | exclude: HISTORY | task | full | | vibe | fork | exclude: HISTORY | task | — | -| pre-mortem | fork | exclude: HISTORY | task | — | -| post-mortem | fork | exclude: HISTORY | task | — | +| premortem | fork | exclude: HISTORY | task | — | +| postmortem | fork | exclude: HISTORY | task | — | | **Orchestration** | | | | | | rpi | fork | — | — | — | | crank | fork | exclude: HISTORY | task | full | diff --git a/docs/SKILL-ROUTER.md b/docs/SKILL-ROUTER.md index d48b48792..dcdef031c 100644 --- a/docs/SKILL-ROUTER.md +++ b/docs/SKILL-ROUTER.md @@ -30,51 +30,51 @@ |---|---|---|---|---|---| | `account-rotation` | execution | `keep_specialist` | - | `account_rotation` | - | | `agent-mail` | execution | `keep_optional_adapter` | - | `agent_mail` | - | -| `agent-native` | meta | `keep_optional_adapter` | `ntm`, `agent-mail` | `role_dispatch`, `observe_workers`, `handoff` | `manage_runtime_sessions` | +| `agent-native` | meta | `keep_optional_adapter` | - | `role_dispatch`, `observe_workers`, `handoff` | `manage_runtime_sessions` | | `agy-native` | cross-vendor | `keep_optional_adapter` | - | `dispatch_explicit_packet`, `provide_fresh_context` | `start_agy_session` | -| `automation-shape-routing` | meta | `keep_optional_adapter` | `skill-builder`, `workflow-builder`, `agent-native` | `automation_shape_routing` | - | -| `bootstrap` | session | `keep_specialist` | `goals`, `product`, `doc`, `shared`, `scaffold` | `bootstrap` | - | +| `automation-shape-routing` | meta | `keep_optional_adapter` | - | `automation_shape_routing` | - | +| `bootstrap` | session | `keep_specialist` | - | `bootstrap` | - | | `cass` | execution | `keep_specialist` | - | `cass` | - | | `cc-hooks` | execution | `keep_specialist` | - | `cc_hooks` | - | -| `codebase-recon` | execution | `keep_specialist` | `doc` | `codebase_recon` | - | +| `codebase-recon` | execution | `keep_specialist` | - | `codebase_recon` | - | | `codex-exec` | orchestration | `keep_optional_adapter` | - | `codex_exec` | - | | `converter` | cross-vendor | `keep_specialist` | - | `converter` | - | | `council` | judgment | `keep_strategy` | - | `collect_independent_judgments`, `synthesize_disagreement` | `write_advisory_council_report` | | `dcg` | execution | `keep_specialist` | - | `dcg` | - | -| `doc` | product | `keep_specialist` | `standards`, `council` | `doc` | - | +| `doc` | product | `keep_specialist` | - | `doc` | - | | `domain` | knowledge | `keep_specialist` | - | `domain` | - | -| `dueling-idea-genies` | judgment | `keep_strategy` | `idea-genie` | `dueling_idea_genies` | - | +| `dueling-idea-genies` | judgment | `keep_strategy` | - | `dueling_idea_genies` | - | | `goals` | product | `keep_specialist` | - | `goals` | - | | `handoff` | session | `keep_specialist` | - | `handoff` | - | -| `heal-skill` | meta | `keep_specialist` | - | `heal_skill` | - | -| `idea-genie` | execution | `keep_strategy` | `research` | `generate_evidenced_options` | `write_idea_portfolio` | +| `heal-skill` | meta | `keep_specialist` | - | `heal_skill` | `optional_skill_projection_repair` | +| `idea-genie` | execution | `keep_strategy` | - | `generate_evidenced_options` | `write_idea_portfolio` | | `implement` | execution | `keep` | - | `execute_one_experiment`, `collect_factual_evidence` | `modify_declared_subject`, `write_candidate_packet` | | `learn` | execution | `keep_off_path` | - | `analyze_verdict_collections` | `write_advisory_observations` | | `ms` | execution | `keep_specialist` | - | `ms` | - | | `ntm` | execution | `keep_optional_adapter` | - | `ntm` | - | | `operationalize` | meta | `keep_specialist` | - | `distill_expertise`, `propose_artifact_shape` | `write_advisory_proposal` | -| `pattern-mining` | execution | `keep_specialist` | `operationalize` | `pattern_mining` | - | +| `pattern-mining` | execution | `keep_specialist` | - | `pattern_mining` | - | | `plan` | execution | `keep` | - | `shape_intent`, `define_acceptance`, `bound_write_scope` | `write_plan_packet` | -| `postmortem` | judgment | `keep_strategy` | `council`, `toil-mining` | `postmortem` | - | -| `premortem` | judgment | `keep_strategy` | `council` | `premortem` | - | +| `postmortem` | judgment | `keep_strategy` | - | `postmortem` | - | +| `premortem` | judgment | `keep_strategy` | - | `challenge_plan` | `write_advisory_plan_review` | | `product` | product | `keep_specialist` | - | `shape_product_boundary` | `write_product_document` | | `rch` | execution | `keep_specialist` | - | `rch` | - | | `reality-check` | judgment | `keep_strategy` | - | `compare_claim_to_evidence` | `write_advisory_gap_report` | -| `refactor` | execution | `keep_specialist` | `standards`, `pattern-mining` | `refactor` | - | -| `research` | execution | `keep_specialist` | `cass`, `ms`, `reverse-engineer`, `codebase-recon`, `pattern-mining` | `research` | - | +| `refactor` | execution | `keep_specialist` | - | `refactor` | - | +| `research` | execution | `keep_specialist` | - | `research` | - | | `reverse-engineer` | execution | `keep_specialist` | - | `reverse_engineer` | - | | `rpi` | meta | `keep` | `plan`, `implement`, `validate` | `orchestrate_once`, `report` | `dispatch_core_phases` | | `sbh` | execution | `keep_specialist` | - | `sbh` | - | -| `scaffold` | execution | `keep_specialist` | `standards` | `scaffold` | - | -| `scope` | meta | `keep_specialist` | - | `scope` | - | +| `scaffold` | execution | `keep_specialist` | - | `scaffold` | - | +| `scope` | meta | `keep_specialist` | - | `scope_review` | - | | `security` | product | `keep_specialist` | - | `security` | - | | `shared` | library | `keep_specialist` | - | `provide_reference_context` | - | -| `skill-builder` | meta | `keep_specialist` | `heal-skill`, `converter` | `skill_builder` | - | +| `skill-builder` | meta | `keep_specialist` | - | `skill_builder` | `writes_skill_source`, `regenerates_skill_projections` | | `standards` | knowledge | `keep_specialist` | - | `standards` | - | -| `status` | session | `keep_specialist` | `sbh` | `status` | - | +| `status` | session | `keep_specialist` | - | `status` | - | | `swarm` | execution | `keep_optional_adapter` | - | `dispatch_once` | `invoke_selected_executor` | -| `test` | execution | `keep_specialist` | `standards` | `test` | - | -| `toil-mining` | meta | `keep_specialist` | `automation-shape-routing` | `toil_mining` | - | +| `test` | execution | `keep_specialist` | - | `test` | - | +| `toil-mining` | meta | `keep_specialist` | - | `toil_mining` | - | | `using-gc` | execution | `keep_optional_adapter` | - | `dispatch_explicit_packet`, `observe_gc_runtime` | `operate_gas_city` | | `validate` | judgment | `keep` | - | `compute_subject_identity`, `judge_acceptance`, `persist_verdict` | `write_verdict_artifact` | | `workflow-builder` | meta | `keep_specialist` | - | `workflow_builder` | - | diff --git a/docs/SKILLS.md b/docs/SKILLS.md index d48b48792..dcdef031c 100644 --- a/docs/SKILLS.md +++ b/docs/SKILLS.md @@ -30,51 +30,51 @@ |---|---|---|---|---|---| | `account-rotation` | execution | `keep_specialist` | - | `account_rotation` | - | | `agent-mail` | execution | `keep_optional_adapter` | - | `agent_mail` | - | -| `agent-native` | meta | `keep_optional_adapter` | `ntm`, `agent-mail` | `role_dispatch`, `observe_workers`, `handoff` | `manage_runtime_sessions` | +| `agent-native` | meta | `keep_optional_adapter` | - | `role_dispatch`, `observe_workers`, `handoff` | `manage_runtime_sessions` | | `agy-native` | cross-vendor | `keep_optional_adapter` | - | `dispatch_explicit_packet`, `provide_fresh_context` | `start_agy_session` | -| `automation-shape-routing` | meta | `keep_optional_adapter` | `skill-builder`, `workflow-builder`, `agent-native` | `automation_shape_routing` | - | -| `bootstrap` | session | `keep_specialist` | `goals`, `product`, `doc`, `shared`, `scaffold` | `bootstrap` | - | +| `automation-shape-routing` | meta | `keep_optional_adapter` | - | `automation_shape_routing` | - | +| `bootstrap` | session | `keep_specialist` | - | `bootstrap` | - | | `cass` | execution | `keep_specialist` | - | `cass` | - | | `cc-hooks` | execution | `keep_specialist` | - | `cc_hooks` | - | -| `codebase-recon` | execution | `keep_specialist` | `doc` | `codebase_recon` | - | +| `codebase-recon` | execution | `keep_specialist` | - | `codebase_recon` | - | | `codex-exec` | orchestration | `keep_optional_adapter` | - | `codex_exec` | - | | `converter` | cross-vendor | `keep_specialist` | - | `converter` | - | | `council` | judgment | `keep_strategy` | - | `collect_independent_judgments`, `synthesize_disagreement` | `write_advisory_council_report` | | `dcg` | execution | `keep_specialist` | - | `dcg` | - | -| `doc` | product | `keep_specialist` | `standards`, `council` | `doc` | - | +| `doc` | product | `keep_specialist` | - | `doc` | - | | `domain` | knowledge | `keep_specialist` | - | `domain` | - | -| `dueling-idea-genies` | judgment | `keep_strategy` | `idea-genie` | `dueling_idea_genies` | - | +| `dueling-idea-genies` | judgment | `keep_strategy` | - | `dueling_idea_genies` | - | | `goals` | product | `keep_specialist` | - | `goals` | - | | `handoff` | session | `keep_specialist` | - | `handoff` | - | -| `heal-skill` | meta | `keep_specialist` | - | `heal_skill` | - | -| `idea-genie` | execution | `keep_strategy` | `research` | `generate_evidenced_options` | `write_idea_portfolio` | +| `heal-skill` | meta | `keep_specialist` | - | `heal_skill` | `optional_skill_projection_repair` | +| `idea-genie` | execution | `keep_strategy` | - | `generate_evidenced_options` | `write_idea_portfolio` | | `implement` | execution | `keep` | - | `execute_one_experiment`, `collect_factual_evidence` | `modify_declared_subject`, `write_candidate_packet` | | `learn` | execution | `keep_off_path` | - | `analyze_verdict_collections` | `write_advisory_observations` | | `ms` | execution | `keep_specialist` | - | `ms` | - | | `ntm` | execution | `keep_optional_adapter` | - | `ntm` | - | | `operationalize` | meta | `keep_specialist` | - | `distill_expertise`, `propose_artifact_shape` | `write_advisory_proposal` | -| `pattern-mining` | execution | `keep_specialist` | `operationalize` | `pattern_mining` | - | +| `pattern-mining` | execution | `keep_specialist` | - | `pattern_mining` | - | | `plan` | execution | `keep` | - | `shape_intent`, `define_acceptance`, `bound_write_scope` | `write_plan_packet` | -| `postmortem` | judgment | `keep_strategy` | `council`, `toil-mining` | `postmortem` | - | -| `premortem` | judgment | `keep_strategy` | `council` | `premortem` | - | +| `postmortem` | judgment | `keep_strategy` | - | `postmortem` | - | +| `premortem` | judgment | `keep_strategy` | - | `challenge_plan` | `write_advisory_plan_review` | | `product` | product | `keep_specialist` | - | `shape_product_boundary` | `write_product_document` | | `rch` | execution | `keep_specialist` | - | `rch` | - | | `reality-check` | judgment | `keep_strategy` | - | `compare_claim_to_evidence` | `write_advisory_gap_report` | -| `refactor` | execution | `keep_specialist` | `standards`, `pattern-mining` | `refactor` | - | -| `research` | execution | `keep_specialist` | `cass`, `ms`, `reverse-engineer`, `codebase-recon`, `pattern-mining` | `research` | - | +| `refactor` | execution | `keep_specialist` | - | `refactor` | - | +| `research` | execution | `keep_specialist` | - | `research` | - | | `reverse-engineer` | execution | `keep_specialist` | - | `reverse_engineer` | - | | `rpi` | meta | `keep` | `plan`, `implement`, `validate` | `orchestrate_once`, `report` | `dispatch_core_phases` | | `sbh` | execution | `keep_specialist` | - | `sbh` | - | -| `scaffold` | execution | `keep_specialist` | `standards` | `scaffold` | - | -| `scope` | meta | `keep_specialist` | - | `scope` | - | +| `scaffold` | execution | `keep_specialist` | - | `scaffold` | - | +| `scope` | meta | `keep_specialist` | - | `scope_review` | - | | `security` | product | `keep_specialist` | - | `security` | - | | `shared` | library | `keep_specialist` | - | `provide_reference_context` | - | -| `skill-builder` | meta | `keep_specialist` | `heal-skill`, `converter` | `skill_builder` | - | +| `skill-builder` | meta | `keep_specialist` | - | `skill_builder` | `writes_skill_source`, `regenerates_skill_projections` | | `standards` | knowledge | `keep_specialist` | - | `standards` | - | -| `status` | session | `keep_specialist` | `sbh` | `status` | - | +| `status` | session | `keep_specialist` | - | `status` | - | | `swarm` | execution | `keep_optional_adapter` | - | `dispatch_once` | `invoke_selected_executor` | -| `test` | execution | `keep_specialist` | `standards` | `test` | - | -| `toil-mining` | meta | `keep_specialist` | `automation-shape-routing` | `toil_mining` | - | +| `test` | execution | `keep_specialist` | - | `test` | - | +| `toil-mining` | meta | `keep_specialist` | - | `toil_mining` | - | | `using-gc` | execution | `keep_optional_adapter` | - | `dispatch_explicit_packet`, `observe_gc_runtime` | `operate_gas_city` | | `validate` | judgment | `keep` | - | `compute_subject_identity`, `judge_acceptance`, `persist_verdict` | `write_verdict_artifact` | | `workflow-builder` | meta | `keep_specialist` | - | `workflow_builder` | - | diff --git a/docs/UPGRADING.md b/docs/UPGRADING.md index 6b8e53199..18a233bd4 100644 --- a/docs/UPGRADING.md +++ b/docs/UPGRADING.md @@ -23,73 +23,53 @@ The "Action required" callout distinguishes hard breakages (must fix before runn --- -## Direct-cut transition +## Cathedral Cut transition -AgentOps now has four lifecycle umbrellas: Discovery, Crank, Validate, and -Learn. Premortem belongs to Discovery; Postmortem is optional after Learn. A -candidate receives one immutable verdict from fresh context, then the consumer -repository decides how local or cloud delivery works. +AgentOps now has one semantic operating loop: -The `flywheel` and `legacy` build profiles described in older sections are -transitional executable state. Do not add new dependencies on them. The final -profile cut removes old owners directly after their retained behavior has an -explicit disposition; no compatibility profile remains. +```text +RPI → Plan → Implement → fresh Validate → durable verdict → report and stop +``` -**Action required:** remove automation that expects AgentOps to merge, push, -queue, or run semantic review from a Git hook. Retain your repository's own CI -or delivery policy and consume AgentOps candidate, verdict, Learn, and delivery -receipts as evidence. +Plan shapes one behavior. Implement performs one bounded experiment. Validate +computes exact content identity, obtains one independent judgment, and writes a +standalone `verdict.v2`. RPI reports the result and stops. Learn is an optional +later consumer of verdict collections. ---- +**Action required:** remove automation that expects AgentOps to retry, queue, +claim work, mutate Git, merge, release, close work, or drive semantic review from +a hook. Keep repository-owned deterministic CI and delivery. No legacy or +flywheel build profile restores removed commands. -## Upgrading to 3.2.x +### Removed CLI lifecycle commands -3.2 introduced build-tag profiles while removing the last CLI-orchestration -verbs. That profile decision is superseded by the direct-cut transition above; -the restore commands below document executable history and do not promise -retention. +The cut removes Pawl, Plan-Pawl, land, done, close, governor, yield, claim, +next-work, state/worktree lifecycle, semantic `ao validate`, converge, +reconcile, membrane, and Crank behavior. Their one-release tombstones fail with +a migration hint and never load old code. -### `ao rpi` / `ao evolve` command surface removed +**Action required:** invoke the RPI skill for the semantic loop, use `ao gate +check` only for deterministic repository checks, and use your repository's Git +or CI system for delivery. -**Affects:** anyone invoking `ao rpi` or `ao evolve` from scripts, wrappers, or CI. +### Removed goal mutation commands -`ao rpi` was deleted at `f61c5f0e7`; `ao evolve` was removed earlier (#724). The loop itself is now the in-session navigation path, not a CLI verb. +`ao goals` retains measurement and analysis. It no longer creates, migrates, +prunes, re-steers, or auto-applies changes to operator intent. -**Action required:** drive the loop in-session via the operating loop + the `/rpi` skill (one turn over the loop). The verbs themselves do not come back under any build tag — `AGENTOPS_LEGACY=1 make build` restores the *archived factory machinery* (`ao loop`, `ao orchestrate`, `ao operator`, ...; see the legacy-tag section below), not `ao rpi`. +**Action required:** edit goal specifications with your normal authoring tools; +use `ao goals validate`, `measure`, `drift`, and related read-only views to +inspect them. -### `ao recall` / `ao memory ingest-claude` removed +### Removed alternate build profiles -**Affects:** anyone calling `ao recall` or `ao memory ingest-claude`. +The old `flywheel` and `legacy` build tags and their archived command sets were +deleted. `ao flywheel status` and `ao flywheel compare` remain ordinary +read-only observations in the supported binary. -Both were removed at `9d5be0b9e`. AgentOps consumes external memory tools instead of shipping its own store. - -**Action required:** use `cass` (search your past agent sessions) and `cm` (procedural memory). Session-log → provenance mining stays native. - -### Corpus / flywheel commands archived behind the `flywheel` build tag - -**Affects:** callers of `ao corpus`, `ao curate`, `ao defrag`, `ao harvest`, `ao mind`, `ao refinery`. - -These moved behind `//go:build flywheel`; the default (`spine`) build omits them. The code stays buildable, just off by default. - -**Action required:** rebuild them with `make build-flywheel`, or consume knowledge via `cass` + `cm`. - -### RPI / factory commands archived behind the `legacy` build tag - -**Affects:** callers of `ao autodev`, `ao codex`, `ao loop*`, `ao orchestrate*`, `ao operator*`, `ao tick`, `ao turn_verify`, `ao harness`. - -These moved behind `//go:build legacy`. - -**Action required:** use the operating loop + an out-of-session substrate; restore the old commands with `AGENTOPS_LEGACY=1 make build`. - -### `ao cron` shim deleted - -**Affects:** anyone calling the `ao cron` scheduling shim. - -Removed at `b242136ac` (ADR-0012). - -**Action required:** schedule an external substrate instead — NTM dispatch, `ao mcp serve`, or `ao agent`. - -full map: [MIGRATION.md](MIGRATION.md) +**Action required:** remove `make build-flywheel`, `AGENTOPS_LEGACY`, and tagged +build invocations. External specialist tools remain caller-selected and do not +become AgentOps lifecycle authorities. --- @@ -193,9 +173,8 @@ ao daemon ready ao doctor --json ``` -Then migrate wrappers one command at a time. See -[`daemon-migration.md`](daemon-migration.md) for RPI, Dream, wiki/forge, -GasCity, OpenClaw, and rollback guidance. +This entry is historical. Current AgentOps has no daemon migration path; see +[`MIGRATION.md`](MIGRATION.md) for the supported single-pass boundary. --- @@ -221,9 +200,9 @@ Worker sessions now carry an explicit `lead-only-worker-git-guard.sh` hook in th **Affects:** any workflow that relied on the previous fail-open behavior. -The crank pre-mortem gate now denies ambiguous state by default. If your pipeline ran crank jobs with missing pre-mortem context, they will now stop early rather than proceed silently. +The crank premortem gate now denies ambiguous state by default. If your pipeline ran crank jobs with missing premortem context, they will now stop early rather than proceed silently. -**Action required:** either set `AGENTOPS_PREMORTEM_MODE=advisory` for exploratory runs, or ensure pre-mortem artifacts are generated before invoking crank. +**Action required:** either set `AGENTOPS_PREMORTEM_MODE=advisory` for exploratory runs, or ensure premortem artifacts are generated before invoking crank. --- diff --git a/docs/_hooks/gen_cli_reference.py b/docs/_hooks/gen_cli_reference.py index 556331fd7..ccbc13760 100644 --- a/docs/_hooks/gen_cli_reference.py +++ b/docs/_hooks/gen_cli_reference.py @@ -42,9 +42,8 @@ def emit(src: Path, dest: str, title: str) -> None: def emit_index() -> None: body = """# CLI Overview -The `ao` CLI is the programmatic surface for AgentOps. It bundles every skill -invocation, validation gate, and knowledge-flywheel operation into a single -Go binary. +The `ao` CLI provides deterministic repository checks and optional evidence +utilities. Semantic workflows remain skills rather than CLI lifecycle commands.
@@ -52,8 +51,8 @@ Go binary. Every `ao` command with flags, examples, and exit codes. Auto-generated from `ao --help`. -- :material-map: **[CLI ↔ Skills Map](../cli-skills-map.md)** - Which commands are called by which skills. +- :material-map: **[CLI Surface](../cli-surface.md)** + Generated public command inventory and coverage classification.
diff --git a/docs/_hooks/gen_skill_pages.py b/docs/_hooks/gen_skill_pages.py index 3e9895071..59ff7f278 100644 --- a/docs/_hooks/gen_skill_pages.py +++ b/docs/_hooks/gen_skill_pages.py @@ -187,7 +187,7 @@ def emit_skills_index(skills: list[dict[str, str]]) -> None: ("quickstart", "You want the fastest setup check and next action"), ("council", "You want independent judges to review a plan, PR, or decision"), ("research", "You need codebase context and prior learnings before changing code"), - ("pre-mortem", "You want to pressure-test a plan before implementation"), + ("premortem", "You want to pressure-test a plan before implementation"), ("implement", "You want one scoped task built and validated"), ("rpi", "You want discovery, build, validation, and bookkeeping in one flow"), ("vibe", "You want a code-quality and risk review before shipping"), @@ -197,7 +197,7 @@ def emit_skills_index(skills: list[dict[str, str]]) -> None: # Family groups for the complete-catalog section families = [ - ("Validation", ["council", "vibe", "pre-mortem", "post-mortem", "red-team"]), + ("Validation", ["council", "vibe", "premortem", "postmortem", "red-team"]), ("Flows", ["research", "plan", "implement", "crank", "swarm", "rpi", "evolve", "discovery", "validation"]), ("Bookkeeping", ["retro", "forge", "flywheel", "compile", "harvest", "inject", "provenance"]), ("Session", ["handoff", "recover", "status", "trace", "dream", "using-agentops"]), @@ -229,7 +229,7 @@ def emit_skills_index(skills: list[dict[str, str]]) -> None: lines.extend([ "", "!!! tip \"Which skill do I need next?\"", - " See the [Decision Tree](../skills-decision-tree.md) for a visual walkthrough, or [SKILL-ROUTER](../SKILL-ROUTER.md) for rule-based routing.", + " See [SKILL-ROUTER](../SKILL-ROUTER.md) for the generated inventory and [Skill Graph](../reference/agentops-skill-graph.md) for hard dependencies.", "", "---", "", @@ -271,7 +271,7 @@ def emit_skills_index(skills: list[dict[str, str]]) -> None: "- :material-format-list-bulleted: **[Single-page catalog](catalog.md)**", " All skills on one page — easier to grep or Ctrl-F than browsing by family.", "", - "- :material-routes: **[Decision Tree](../skills-decision-tree.md)**", + "- :material-routes: **[Skill Graph](../reference/agentops-skill-graph.md)**", " \"Which skill do I need next?\" — single source of truth.", "", "- :material-api: **[Skill API](../SKILL-API.md)**", diff --git a/docs/adr/ADR-0003-executable-spec-artifact-durability.md b/docs/adr/ADR-0003-executable-spec-artifact-durability.md index a392975d4..239eabdec 100644 --- a/docs/adr/ADR-0003-executable-spec-artifact-durability.md +++ b/docs/adr/ADR-0003-executable-spec-artifact-durability.md @@ -1,151 +1,37 @@ # ADR-0003: Executable-Spec Artifact Durability -- **Status:** Accepted (2026-05-17) +- **Status:** Accepted, scope reduced by the Cathedral Cut (2026-07-14) - **Author:** AgentOps maintainers -- **Builds on:** [ADR-0002](ADR-0002-agentops-3-hookless-cdlc-rearchitecture.md) -- **Tracking:** epic `soc-58nt` (Executable spec layer), bead `soc-58nt.6` ## Context -Epic `soc-58nt` makes `GOALS.md` directives the executable BDD specification: -each directive links to behavioral scenarios, and the fitness gate measures -scenario satisfaction instead of code metrics. This only works if the -acceptance criteria are durable. - -Today they are not: - -- `GOALS.md` is git-tracked. The directive→scenario *link* survives. -- `ao eval scenario add` writes scenario JSON to `.agents/holdout/`. -- Repo-root `.agents/` is **never git-tracked** — `.gitignore` excludes - `/.agents/` explicitly, and `AGENTS.md` declares it local/private runtime - state. It was wiped by routine cleanup on 2026-05-07 (Directive 11 / - `soc-rv5p`). - -A directive that points at a scenario file living only in gitignored -`.agents/holdout/` has acceptance criteria that vanish on `git clean`. A spec -whose acceptance criteria can disappear is not a spec. F1/F3/F4/F5 of this -epic all assume both ends of the directive→scenario link are durable. - -The holdout directory also serves a real, separate purpose: holdout scenarios -are deliberately **isolated from implementing agents** (the -`holdout-isolation-gate` hook blocks reads without -`AGENTOPS_HOLDOUT_EVALUATOR=1`). That isolation property must not be lost when -solving durability. +Acceptance examples are useful only when their content survives the run that +created them. Repository authors may keep published examples under +`spec/scenarios/` and private evaluation inputs under an ignored local path. +AgentOps no longer creates, promotes, schedules, or closes either kind. ## Decision -Distinguish two scenario lifecycle states and give each a home. +- Published scenario artifacts live under `spec/scenarios/` and may be linked + from an operator-authored `GOALS.md` directive. +- Private holdout scenarios may live under `.agents/holdout/` and remain local. +- Read-only goal inspection resolves a published scenario before a local + holdout with the same ID. +- `ao goals scenarios` lists or lints existing links. It does not create, + promote, mutate, schedule, or deliver them. +- Schemas and test fixtures remain tracked repository inputs. -### 1. `local_holdout` scenarios — stay in `.agents/holdout/` - -`ao eval scenario add` continues to write to `.agents/holdout/`. These are ad hoc, -agent-isolated holdout scenarios used for evaluation. They are intentionally -untracked and intentionally unreadable by implementing agents. Nothing about -the holdout workflow changes. - -### 2. `promoted_spec` scenarios — live in tracked `spec/scenarios/` - -A scenario that is **linked to a `GOALS.md` directive** is a *promoted spec -scenario*. It is part of the executable specification and MUST be durable. - -- **Tracked path:** `spec/scenarios/.json` - (`spec/` is not gitignored; these files are committed). -- Promotion **copies** the scenario JSON from `.agents/holdout/` (or creates - it directly) into `spec/scenarios/`, and stamps the scenario with the stable - `directive_id` of the directive it satisfies (the `directive_id` field is - added to the scenario schema by F1.1 / `soc-58nt.1.1`). -- The `GOALS.md` directive's `Scenarios:` attribute line references the - scenario by ID. Resolution searches `spec/scenarios/` first. -- A promoted spec scenario is **not** subject to holdout isolation — it is part - of the published spec and readable by any agent. Promotion is the explicit - act of moving a scenario from "evaluation holdout" to "published acceptance - criterion." - -`ao goals scenarios --create` (F1.3 / `soc-58nt.1.3`) writes directly to -`spec/scenarios/` — a scenario created already linked to a directive is born -promoted. - -### 3. Domain manifests — tracked under `docs/domains/` - -Domain-slice manifests (F3 / `soc-58nt.3.x`) are durable project artifacts, not -runtime state. They live at `docs/domains//manifest.yaml`, git-tracked. -Optional runtime mirrors may sit under `.agents/domains/`, but the source of -truth is tracked. - -### 4. Schemas and fixtures — always tracked - -All schemas (`schemas/**`, `docs/contracts/*.schema.json`) and all test -fixtures (`tests/**`) are git-tracked. No schema or fixture for the -executable-spec layer may live under untracked `.agents/`. - -### Summary table - -| Artifact | Path | Tracked | Agent-isolated | -|---|---|---|---| -| Ad hoc holdout scenario | `.agents/holdout/.json` | no | yes | -| Promoted spec scenario | `spec/scenarios/.json` | **yes** | no | -| Domain-slice manifest | `docs/domains//manifest.yaml` | **yes** | no | -| Scenario-result run artifact | `.agents/rpi/scenario-results.json` | no (ephemeral run output) | n/a | -| Schemas | `schemas/**`, `docs/contracts/*.schema.json` | **yes** | n/a | -| Fixtures | `tests/**` | **yes** | n/a | - -Rule: the directive→scenario→domain link endpoints are all tracked. Only -ephemeral *run output* (e.g. `.agents/rpi/scenario-results.json`) stays under -untracked `.agents/`. - -## Backfill / migration - -Existing `.agents/holdout/` scenarios: - -- Scenarios with **no** `directive_id` and not referenced by any `GOALS.md` - `Scenarios:` line remain `local_holdout` — untouched. -- Scenarios that **are** referenced by a directive (once F1 lands the - `Scenarios:` attribute) are promoted: copied to `spec/scenarios/.json`, - stamped with the directive's stable `directive_id`, and committed. The - holdout copy may be left in place or removed by the operator; the tracked - copy is authoritative. -- F1.4 link-lint (`soc-58nt.1.4`) treats a directive that references a scenario - resolvable only under `.agents/holdout/` (not yet promoted) as a **warning** - ("scenario not promoted to `spec/scenarios/`"), not an error, until backfill - completes. After backfill the lint may be ratcheted to error. - -No automated bulk migration ships with this ADR — promotion happens -incrementally as directives gain `Scenarios:` lines through F1. +The former domain-slice manifest and phased-RPI machinery are retired. Plan's +`write_scope` and acceptance scenarios are the per-invocation boundary. ## Consequences -### Positive - -- The executable spec is durable: `git clean` / `.agents/` cleanup cannot - destroy a directive's acceptance criteria. -- Holdout isolation is preserved for genuine evaluation scenarios. -- Promotion is an explicit, reviewable git event — a spec scenario entering the - tracked tree shows up in PR diffs. -- F1/F3/F4/F5 can assume both link endpoints resolve to tracked files. - -### Negative - -- Two homes for scenario JSON means resolution must search both - (`spec/scenarios/` then `.agents/holdout/`). F1's scenario reader owns this. -- Promotion is a copy, so a scenario can briefly exist in both places; - `spec/scenarios/` is authoritative and lint flags drift. -- Operators must remember that linking a scenario to a directive implies - promotion (and loss of holdout isolation for that scenario). Tooling makes - this explicit; docs must state it. - -## Acceptance - -This ADR is accepted when: - -- The tracked spec-scenario path (`spec/scenarios/`) and domain-manifest path - (`docs/domains/`) are named and recorded here. ✓ -- The `local_holdout` vs `promoted_spec` distinction is defined. ✓ -- Backfill semantics for pre-existing `.agents/holdout/` scenarios are - defined. ✓ -- F1/F3/F4/F5 beads reference this ADR for artifact locations. +Repository authors control their specification format and Git policy. AgentOps +can inspect durable acceptance links without becoming their authoring, +scheduling, or delivery system. ## References -- [ADR-0002](ADR-0002-agentops-3-hookless-cdlc-rearchitecture.md) -- `schemas/scenario.v1.schema.json` — scenario schema (gains `directive_id` in F1.1) -- Epic `soc-58nt` DESIGN field — full executable-spec revision plan +- `schemas/scenario.v1.schema.json` +- `spec/scenarios/` +- `docs/architecture/operating-loop.md` diff --git a/docs/adr/ADR-0006-re-steer-policy-and-mutation-safety.md b/docs/adr/ADR-0006-re-steer-policy-and-mutation-safety.md deleted file mode 100644 index 042fda0eb..000000000 --- a/docs/adr/ADR-0006-re-steer-policy-and-mutation-safety.md +++ /dev/null @@ -1,293 +0,0 @@ -# ADR-0006: Re-Steer Policy and Mutation-Safety Contract - -- **Status:** Accepted (2026-05-17) -- **Author:** AgentOps maintainers -- **Builds on:** [ADR-0003](ADR-0003-executable-spec-artifact-durability.md), [ADR-0005](ADR-0005-trace-link-convention.md) -- **Tracking:** epic `soc-58nt` (Executable spec layer), bead `soc-58nt.5.9` - -## Context - -Epic `soc-58nt` F5 adds auto re-steer: `ao goals measure` accumulates per-directive -`scenario_verdict` outcomes over successive runs, and when the evidence warrants it, -proposes (or, under an explicit opt-in policy, applies) mutations to GOALS.md directives -— re-ordering priority, tightening or loosening `**Setpoint:**`, or (with an additional -opt-in) reversing `**Steer:**` direction. - -Before any F5.1–F5.5 implementation starts, this design spike records the definitional -contract that all F5 code implements against. Without a precise shared definition of -"iteration", "failure streak", and "cooldown", each implementation bead would embed -different assumptions, producing contradictory ledger accounting and unpredictable -mutation triggers. - -The single most dangerous class of mutation F5 can make is a **Steer-direction flip**: -inverting `increase` ↔ `decrease`. A failing directive does not prove the optimization -direction is wrong — it may prove that the scenario suite, the implementation, or the -measurement window is inadequate. Auto-flipping Steer on evidence as noisy as a few -consecutive failures would systematically corrupt the strategic intent encoded in -GOALS.md. This ADR treats Steer-direction flips as a hard safety invariant that requires -explicit human opt-in even when the general `auto_apply` gate is open. - -All GOALS.md mutations produced by F5 route through the non-lossy -`cli/internal/goals/patcher.go` patcher (F1.0 / `soc-58nt.1.0`), never through -`RenderGoalsMD` / `WriteMDGoals`, which would silently drop file sections. - -## Definitions - -### ITERATION - -> **Definition.** An *iteration* is exactly one completed invocation of -> `ao goals measure` (or `ao goals measure --scenarios-only`) that terminates -> without a structural error and writes at least one `scenario_verdict` field to -> the verdict ledger for the directive in question. -> -> An iteration is **not** defined by wall-clock time, calendar period, sprint -> boundary, or RPI phase. It is a discrete ledger event: one call → one record → -> one verdict per directive. - -Consequences: - -- Two rapid back-to-back `ao goals measure` calls are two iterations, not one. -- A call that exits non-zero due to a structural error (malformed GOALS.md, - unloadable scenario results) does NOT constitute an iteration; no ledger record - is written, no streak is advanced. -- A call that exits 0 with `scenario_verdict: "unknown"` (no scenarios linked yet) - DOES constitute an iteration; it counts as neither pass nor fail and does not - advance a failure streak. -- The iteration count for a directive is the number of ledger records for that - directive's stable `d-` ID. - -The F5.1 verdict-ledger extension records each iteration as an append-only entry -carrying: `directive_id`, `run_timestamp` (RFC 3339), `scenario_verdict` -(`"pass"` | `"fail"` | `"unknown"`), `scenario_satisfaction` (float 0–1), -`scenario_count` (int), and `evaluated_count` (int). - -### FAILURE STREAK - -> **Definition.** A *failure streak* for a directive is the length of the longest -> unbroken run of consecutive iterations, ending at the most recent iteration, -> in which the directive's `scenario_verdict` was `"fail"`. -> -> The streak resets to 0 the moment any iteration yields `"pass"` or `"unknown"`. - -The streak counter increments only on `"fail"` and resets on any non-`"fail"` verdict -(`"pass"` or `"unknown"`). `"unknown"` (no scenarios linked) is not a failure and -therefore breaks any in-progress streak. - -Policy parameter: `failure_streak_length` (integer, default 3). A directive is -eligible for mutation proposal only when its current failure streak meets or exceeds -this value. - -### COOLDOWN - -> **Definition.** A directive is in *cooldown* if a re-steer mutation (proposed or -> applied) was recorded against it within the last K iterations, where K is the -> policy parameter `cooldown_iterations` (integer, default 5). -> -> "Last K iterations" is measured as the K most recent iteration records for that -> directive in the verdict ledger, not K calendar days or K wall-clock hours. - -A directive in cooldown is skipped by the re-steer engine regardless of its current -failure streak. This prevents thrashing: a directive that was just re-steered is -given K iterations to demonstrate whether the mutation had effect before being -eligible again. - -A cooldown record is written when: -- A mutation is **proposed** (recommendation-only mode), OR -- A mutation is **applied** (auto-apply mode, requires human confirmation). - -The cooldown clock starts from the iteration in which the proposal/application -occurred, not from the current iteration. - -## Policy - -The re-steer policy is read from a JSON file (path configurable; default -`docs/re-steer-policy.json`) that validates against -`schemas/re-steer-policy.v1.schema.json`. If no policy file is present, the -built-in safe defaults apply (see §Default policy). - -### Policy fields - -| Field | Type | Default | Description | -|---|---|---|---| -| `minimum_evidence_count` | integer ≥ 1 | 5 | Minimum number of iterations (ledger records) for a directive before it is eligible for any mutation proposal. Prevents premature decisions on sparse data. | -| `failure_streak_length` | integer ≥ 2 | 3 | Number of consecutive `"fail"` verdicts required before the directive is eligible for mutation proposal. **Must be ≥ 2** — a single fresh failure never triggers an applied mutation. | -| `cooldown_iterations` | integer ≥ 1 | 5 | Number of iterations a directive must "cool down" after a mutation proposal/application before it is eligible again. | -| `allowed_mutation_types` | array of enum | `["priority_bump", "setpoint_tighten", "setpoint_loosen"]` | Which mutation types the policy permits. `steer_flip` is intentionally absent from the default and requires explicit opt-in. | -| `max_priority_bump` | integer ≥ 1 | 3 | Maximum number of positions a directive may be moved up in priority in a single re-steer event. Prevents a single failing directive from monopolizing the top of GOALS.md. | -| `auto_apply` | boolean | `false` | When `false` (default), mutations are recommendations only — printed to stdout, written to a proposal file, but GOALS.md is not modified. When `true`, mutations are applied to GOALS.md via the non-lossy patcher after the operator confirms interactively. | -| `allow_steer_flip` | boolean | `false` | When `false` (default, **hard safety invariant**), the `steer_flip` mutation type is never performed regardless of `allowed_mutation_types`. When `true`, `steer_flip` must also appear in `allowed_mutation_types` for it to take effect. Requires both flags to prevent accidental activation. | - -### Mutation types (enum values for `allowed_mutation_types`) - -| Value | Effect | GOALS.md attribute modified | -|---|---|---| -| `priority_bump` | Move directive up by at most `max_priority_bump` positions (calls `ao goals steer prioritize` equivalent via patcher) | Display number (reorder) | -| `setpoint_tighten` | Make the `**Setpoint:**` target harder (direction inferred from `**Steer:**`) | `**Setpoint:**` | -| `setpoint_loosen` | Make the `**Setpoint:**` target easier | `**Setpoint:**` | -| `steer_flip` | Invert `**Steer:**` direction (`increase` ↔ `decrease`). **Gated by `allow_steer_flip: true` AND explicit opt-in in `allowed_mutation_types`.** | `**Steer:**` | - -## Hard Safety Invariants - -The following invariants are checked by the F5.2 policy loader before any mutation -is proposed or applied. Violation causes the re-steer run to abort with a non-zero -exit code and a diagnostic message; GOALS.md is never touched. - -### I-1: Single fresh failure never triggers an applied mutation - -`failure_streak_length` **must be ≥ 2**. The schema enforces `minimum: 2` on this -field. A policy file with `failure_streak_length: 1` is **schema-invalid** and is -rejected at load time. This invariant cannot be overridden by any flag. - -Rationale: a single `"fail"` can result from flaky scenario evaluation, a -transient CI environment issue, or a scenario that has never been run. It is -categorically insufficient evidence to modify a strategic directive. - -### I-2: Default behavior is recommendation-only - -`auto_apply` defaults to `false`. Unless the operator explicitly sets -`auto_apply: true` in the policy file **and** confirms interactively at the prompt -F5.4 presents, GOALS.md is never modified by the re-steer engine. The prompt is -non-bypassable in interactive mode; in `--non-interactive` mode with `auto_apply: -true`, the confirmation is skipped and a machine-readable log record is written -instead (F5.4 implementation contract). - -### I-3: Steer-direction flip requires dual explicit opt-in - -Even when `auto_apply: true`, the re-steer engine **never flips** `**Steer:**` -unless BOTH conditions hold: - -1. `allow_steer_flip: true` in the policy file, AND -2. `"steer_flip"` is present in `allowed_mutation_types`. - -Requiring both fields prevents accidental activation: a copy-paste error that sets -one but not the other leaves the invariant intact. This invariant is checked by the -F5.2 policy loader on every run, not only when a streak threshold is reached. - -Rationale: a failing directive proves the implementation is not satisfying its -scenarios. It does NOT prove the optimization direction encoded in `**Steer:**` is -inverted. Steer encodes strategic intent ("we want to increase X") — that intent -may be correct even when current implementation falls short. Auto-inverting it on -noisy iteration data would corrupt the strategic model that GOALS.md represents. - -### I-4: Minimum evidence before any mutation - -No mutation is proposed or applied for a directive unless its verdict-ledger record -count is ≥ `minimum_evidence_count`. The default (5) means a directive must have -been measured at least 5 times. This prevents mutations on newly-added directives -that have only one or two data points. - -### I-5: Cooldown is enforced regardless of streak - -A directive in cooldown (per §COOLDOWN definition) is skipped entirely. Its failure -streak may meet the threshold, but the cooldown gate takes precedence. This prevents -a re-steer event from repeating before the previous mutation has had time to -influence the metric. - -## Default Policy - -The default policy, stored at `docs/re-steer-policy.default.json` and usable as a -starting point by copying to `docs/re-steer-policy.json`, encodes the safe -defaults: - -```json -{ - "$schema": "https://agentops.dev/schemas/re-steer-policy.v1.schema.json", - "minimum_evidence_count": 5, - "failure_streak_length": 3, - "cooldown_iterations": 5, - "allowed_mutation_types": ["priority_bump", "setpoint_tighten", "setpoint_loosen"], - "max_priority_bump": 3, - "auto_apply": false, - "allow_steer_flip": false -} -``` - -Key properties of the default: -- `auto_apply: false` → no GOALS.md modifications without operator action. -- `allow_steer_flip: false` → Steer direction is immutable by automation. -- `failure_streak_length: 3` → three consecutive failures required (satisfies I-1). -- `minimum_evidence_count: 5` → at least 5 measured iterations before eligibility. -- `steer_flip` absent from `allowed_mutation_types` → even if `allow_steer_flip` - were set to `true`, no flip would occur without also adding it here. - -## Relationship to Existing Surfaces - -### F1.0 patcher (`cli/internal/goals/patcher.go`) - -All GOALS.md mutations produced by F5 MUST route through `GoalsPatcher.SetAttribute` -and the reorder logic exposed by the patcher. Never call `RenderGoalsMD` / -`WriteMDGoals` (lossy). This is the same invariant established for F1–F4 and is -re-stated here as a load-bearing constraint for F5 implementers. - -### F2 `ao goals measure` scenario verdicts - -The `scenario_verdict` field in `directiveScenarioReport` -(`cli/cmd/ao/goals_measure_scenarios.go`) is the source of truth for each -iteration's pass/fail outcome. F5.1 reads this field from the measure output (or -from the verdict ledger it writes) to advance streak counters. F5 does NOT -re-evaluate scenarios independently. - -### F5.3 `ao goals steer` extension - -F5.3 adds a `ao goals steer re-steer` subcommand (or `ao goals re-steer`; exact -shape is an F5.3 decision). It reads the policy file, evaluates eligibility per the -rules above, and either prints recommendations or applies mutations after -confirmation. It is additive to the existing `steer add`, `steer remove`, and -`steer prioritize` subcommands; it does not modify their behavior. - -## Consequences - -### Positive - -- F5.1–F5.5 implementation beads have a precise shared contract: "iteration", - "failure streak", and "cooldown" are unambiguous. -- The dual opt-in requirement for Steer flips (I-3) makes the most dangerous - mutation class resistant to both accidental policy misconfiguration and - copy-paste errors. -- Recommendation-only default (I-2) means operators can deploy F5 immediately and - observe proposals before opting into automation. -- The schema-enforced `minimum: 2` on `failure_streak_length` (I-1) makes the - single-fresh-failure safety guarantee machine-checkable, not a convention. - -### Negative - -- The dual opt-in for Steer flips means legitimate cases where the Steer direction - really is wrong require two config changes, not one. This is intentional friction. -- The minimum-evidence requirement (I-4, default 5 iterations) means newly-added - directives are silently skipped for several measure cycles. Operators expecting - immediate proposals on new directives may be confused; the re-steer output should - explain why a directive was skipped. -- Cooldown is iteration-count-based, not time-based. In repos where `ao goals - measure` runs many times per day, a cooldown of 5 iterations is very short. Teams - with high-frequency measurement pipelines should raise `cooldown_iterations`. - -## Acceptance - -This ADR is accepted when: - -- "Iteration", "failure streak", and "cooldown" are defined and documented. ✓ -- The policy JSON has a schema (`schemas/re-steer-policy.v1.schema.json`). ✓ -- A default policy instance validates against the schema. ✓ -- The default policy disallows Steer-direction flips (`allow_steer_flip: false`, - `steer_flip` absent from default `allowed_mutation_types`). ✓ -- A single fresh failure never triggers an applied mutation (`failure_streak_length` - schema minimum is 2). ✓ -- All five hard safety invariants are stated and their enforcement mechanism is - named. ✓ -- ADR-0003 and ADR-0005 are cross-referenced. ✓ - -## References - -- [ADR-0003](ADR-0003-executable-spec-artifact-durability.md) — artifact durability - rule; schemas are tracked; the policy file at `docs/re-steer-policy.json` is tracked -- [ADR-0005](ADR-0005-trace-link-convention.md) — trace link convention; verdict - ledger records are the iteration source of truth for the re-steer engine -- `schemas/re-steer-policy.v1.schema.json` — JSON Schema for the policy file -- `docs/re-steer-policy.default.json` — default policy instance -- `cli/internal/goals/patcher.go` — F1.0 non-lossy patcher; all F5 mutations route - through it -- `cli/cmd/ao/goals_measure_scenarios.go` — `directiveScenarioReport`; defines - `scenario_verdict` (the per-iteration pass/fail signal) -- `cli/cmd/ao/goals_steer.go` — existing directive-management surface F5.3 extends -- `cli/cmd/ao/rpi_ledger.go` — existing verdict ledger F5.1 extends -- Epic `soc-58nt`, bead `soc-58nt.5.9` — design spike that produced this ADR diff --git a/docs/adr/ADR-0012-focus-surface-on-membrane-bookkeeper-archive-satellites.md b/docs/adr/ADR-0012-focus-surface-on-membrane-bookkeeper-archive-satellites.md deleted file mode 100644 index a94ce33a9..000000000 --- a/docs/adr/ADR-0012-focus-surface-on-membrane-bookkeeper-archive-satellites.md +++ /dev/null @@ -1,61 +0,0 @@ -# ADR-0012: Directly Cut the Unproven Satellite Surface - -- **Status:** Superseded by direct-cut execution (2026-07-14) -- **Original decision:** Archive optional command families behind build tags -- **Current decision:** Retain, merge, or delete every command through one exact - owner; remove the profile mechanism after the last disposition - -## Context - -The earlier decision reduced the default command surface by putting corpus and -factory families behind `flywheel` and `legacy` build tags. That made the -headline smaller without reducing the maintained system. It left multiple -compiled products, compatibility tests, generated projections, restoration -instructions, and old owners alive. The result was more surface to reason about -and a standing invitation to preserve behavior that the product no longer owns. - -AgentOps now has a narrower responsibility: Discovery shapes accepted behavior, -Crank implements a bounded tranche, Validate records one immutable verdict from -fresh context, and Learn records the smallest useful consequence. Deterministic -evidence supports that judgment. Delivery belongs to the consumer repository, -whether the worker is local or cloud-hosted. - -## Decision - -Delete legacy code directly in the same owning leaf that installs its replacement or removes its last consumer. - -Each current command receives exactly one disposition: - -- **keep** under one final root and explicit module owner; -- **merge** into a named final root while deleting the former root and owner; -- **replace** with the final behavior while deleting the old owner; or -- **delete** with its unique tests, fixtures, docs, and dependencies. - -No compatibility alias, alternate runtime, dormant scaffold, restoration path, -or dual owner lands between those states. An exact authority-and-consumer -manifest must prove the old and new paths belong to the same leaf before that -leaf starts. - -## Sequencing - -This ADR changes product authority, not executable code. Later master-plan -leaves own the physical cut: - -- K5, K7, and K9 remove model-driving review, repository delivery, and retired - gate implementations while installing deterministic recorders where needed; -- each exact `CLI.` leaf keeps, merges, replaces, or deletes one compiled - command root; -- F4 removes the `flywheel`, `legacy`, and combined build profiles plus their - dedicated compatibility tests; -- D2 regenerates command and skill projections only after executable ownership - is final. - -## Consequences - -- There is one final compiled product rather than hidden optional products. -- The four lifecycle umbrellas remain agent-owned; the CLI is a deterministic - transaction kernel and evidence recorder. -- Historical commits and changelog entries remain history, but no active source - promises restoration. -- Rollback reverts a complete same-owner leaf. It never restores only an old - command owner or only a compatibility route. diff --git a/docs/adr/ADR-0013-domain-slice-manifest-contract.md b/docs/adr/ADR-0013-domain-slice-manifest-contract.md deleted file mode 100644 index 439abc3f4..000000000 --- a/docs/adr/ADR-0013-domain-slice-manifest-contract.md +++ /dev/null @@ -1,190 +0,0 @@ -# ADR-0013: Domain-Slice Manifest Contract - -> Formerly ADR-0004, renumbered 2026-07-01 to resolve the duplicate ADR number; corpus-moat retains 0004 ([ADR-0004](ADR-0004-corpus-moat-unproven-position-on-the-system.md)). - -- **Status:** Accepted (2026-05-17) -- **Author:** AgentOps maintainers -- **Builds on:** [ADR-0003](ADR-0003-executable-spec-artifact-durability.md) -- **Tracking:** epic `soc-58nt` (Executable spec layer), bead `soc-58nt.3.8` - -## Context - -Epic `soc-58nt` F3 adds a scoped RPI loop that confines an agent to a declared -DDD domain slice: a bounded context with its own directives, scenarios, -implementation surface, and read fence. Before any F3 implementation starts -this design spike records the contract decisions so F3 implementation workers -share a stable target. - -Three pre-existing surfaces touch domain concepts and must be reconciled before -a fourth one is invented: - -1. **`skills/domain/SKILL.md`** — a library skill holding the canonical - ubiquitous language (vocabulary, structural primitives, anti-patterns). It - is a knowledge surface, not a registry. - -2. **`docs/contracts/context-map.md`** — generated from `skills/*/SKILL.md` - frontmatter (`hexagonal_role`, `consumes`, `produces`, `context_rel`). It - maps how skills relate to each other architecturally (DDD bounded context - topology, data flows). It is an *architecture view*, not a runtime scope - declaration. - -3. **Skill frontmatter fields** (`hexagonal_role`, `consumes`, `produces`) — - per-skill metadata in `SKILL.md` YAML headers. These classify individual - skills; they do not declare a domain slice's implementation surface or - acceptance criteria. - -None of these three surfaces declares the runtime scope of an agent operating -within a domain slice, nor records which directives and scenarios a slice owns. -A new, distinct artifact is needed: the **domain-slice manifest**. - -## Decision - -### Decision A — Command shape: `ao rpi phased --domain ` - -The scoped domain RPI is invoked as: - -``` -ao rpi phased --domain -``` - -**Not** a new top-level command (`ao rpi --domain`) and **not** a standalone -`ao domain` command. Rationale: - -- `ao rpi phased` is the phased RPI engine. Domain scoping is a flag that - restricts the context loaded by each phase — it extends `phased`, not - replaces it. -- Keeping domain scoping as a `phased` flag preserves the existing - `phaseManifest` machinery (per-phase context-budget control) and adds the - domain read fence on top. -- A top-level `ao rpi --domain` would imply domain scoping applies to the - *entire* RPI workflow class; `phased` is the right granularity. - -### Decision B — Manifest is a durable tracked artifact at `docs/domains//manifest.yaml` - -The domain-slice manifest lives at `docs/domains//manifest.yaml`, is -git-tracked, and is the **source of truth** for the domain slice. - -Optional runtime mirrors (e.g. for hook caching) may sit under -`.agents/domains/`, but `.agents/` is gitignored; the tracked file is -authoritative. This follows the artifact-durability rule in ADR-0003 §3: -*"Domain manifests — tracked under `docs/domains/`"*. - -Consequences: -- Changing a domain slice's read fence or owned directives is a reviewable - git event. -- `ao rpi phased --domain` reads the manifest from `docs/domains/` and does - not need `.agents/domains/` to exist. - -### Decision C — Reconciliation with the three pre-existing domain surfaces - -The domain-slice manifest is the **fourth** domain surface. It fills a gap -none of the three existing surfaces covers (runtime scope declaration). The -surfaces are complementary, not overlapping: - -| Surface | What it does | What it does NOT do | -|---|---|---| -| `skills/domain/SKILL.md` | Defines ubiquitous language (nouns + primitives) | Does not declare which directives a slice owns | -| `docs/contracts/context-map.md` | Generated architecture view of skill relationships | Does not declare implementation paths or read fences | -| Skill frontmatter (`hexagonal_role` / `consumes` / `produces`) | Classifies individual skills by hexagonal role | Does not scope agent context loading to a slice | -| `docs/domains//manifest.yaml` (new) | Declares bounded context: owned directives, scenarios, context roots, read fence, validation commands | Does not replace or duplicate any of the above | - -To avoid a third uncoordinated domain registry: - -- The manifest **does not copy** skill-topology information from - `context-map.md`. If a domain slice "owns" certain skills, that is expressed - via `context_roots` pointing at the skill source directories. -- The manifest **does not redefine** terms from `skills/domain/SKILL.md`. The - `bounded_context` field is a free-form sentence, not a new ontology. -- The manifest **does not replace** skill frontmatter. Frontmatter describes - individual skills; the manifest declares the aggregate scope of a slice - spanning multiple skills, CLI packages, schemas, and tests. -- `docs/contracts/context-map.md` remains the architecture view. The manifest - is the operator-facing scope declaration. - -### Decision D — Go model is named `domainSliceManifest`, explicitly distinct from `phaseManifest` - -The Go struct that decodes a domain-slice manifest is: - -```go -// domainSliceManifest declares the bounded DDD domain slice that scopes -// an ao rpi phased --domain run: owned directives, scenarios, context -// roots, read fence, and validation commands. -// -// This is DISTINCT from phaseManifest (rpi_phased_manifest.go), which is a -// per-phase context-budget declaration (token limits, handoff field selection) -// unrelated to DDD domain slicing. -type domainSliceManifest struct { ... } -``` - -`phaseManifest` (defined in `cli/cmd/ao/rpi_phased_manifest.go`) is a -per-phase context-budget struct: it controls which `phaseHandoff` fields each -RPI phase loads and the token cap. It is a runtime optimization artifact with -no domain semantics. The two structs are used together during -`ao rpi phased --domain`: `phaseManifest` controls context budgets per phase; -`domainSliceManifest` controls *which* context is in scope at all. - -Using the same name or embedding one in the other would conflate two orthogonal -concerns — context breadth (domain fence) vs. context depth (per-phase token -budget). - -## Schema - -The manifest schema is defined in `schemas/domain-slice-manifest.v1.schema.json`. - -Key fields: - -| Field | Type | Required | Description | -|---|---|---|---| -| `schema_version` | integer (const 1) | yes | Schema version | -| `domain` | string (`^[a-z][a-z0-9-]*$`) | yes | Short machine-readable name; matches directory under `docs/domains/` | -| `version` | string (semver) | yes | Manifest version (increment on structural change) | -| `bounded_context` | string | yes | One-sentence DDD bounded-context statement | -| `directive_ids` | array of `d-` | yes | Stable GOALS.md directive IDs this slice owns | -| `scenario_ids` | array of scenario IDs | yes | Promoted spec scenario IDs from `spec/scenarios/` | -| `context_roots` | array of paths | yes | Repo-relative dirs/files forming the implementation surface | -| `allowed_read_globs` | array of globs | yes | Read-fence allow list for agents in this slice | -| `denied_read_globs` | array of globs | yes | Read-fence deny list (precedence over allowed) | -| `validation_commands` | array of objects | yes | Ordered validation steps (label, command, optional working_dir and timeout_seconds) | -| `owner` | string | yes | Team or person responsible | - -All fields are `required`. `additionalProperties: false` on the root object and -on each `validation_commands` item. - -## Consequences - -### Positive - -- F3 implementation workers have a stable schema and clear command shape before - any code is written. -- Domain manifests are durable (tracked) per ADR-0003 rule. -- The four domain surfaces are complementary; no duplication. -- The Go model name signals clearly to future readers that `domainSliceManifest` - and `phaseManifest` are orthogonal. - -### Negative - -- Operators must author a `manifest.yaml` before using `ao rpi phased --domain`. - The `docs/domains/example/` directory provides a template. -- Two Go struct types (`domainSliceManifest` + `phaseManifest`) compose during - a phased-domain run; the interaction must be documented in the implementation - bead. - -## Acceptance - -This ADR is accepted when: - -- `schemas/domain-slice-manifest.v1.schema.json` is committed and validates the - example manifest at `docs/domains/example/manifest.yaml`. ✓ -- `docs/domains/` directory exists with `README.md` and `example/manifest.yaml`. ✓ -- All four decisions (A–D) are recorded here. ✓ -- ADR-0003 is cross-referenced. ✓ - -## References - -- [ADR-0003](ADR-0003-executable-spec-artifact-durability.md) — artifact durability rule; §3 establishes `docs/domains//manifest.yaml` as tracked -- [ADR-0002](ADR-0002-agentops-3-hookless-cdlc-rearchitecture.md) — hookless CDLC architecture -- `schemas/domain-slice-manifest.v1.schema.json` — the JSON Schema for this manifest -- `cli/cmd/ao/rpi_phased_manifest.go` — `phaseManifest` (the DISTINCT per-phase context-budget struct) -- `skills/domain/SKILL.md` — ubiquitous language (vocabulary, not registry) -- `docs/contracts/context-map.md` — generated skill architecture view (not replaced by this manifest) -- Epic `soc-58nt`, bead `soc-58nt.3.8` — design spike that produced this ADR diff --git a/docs/adr/ADR-0014-catch-to-producer-loop-judgment-catches-need-a-producer-route.md b/docs/adr/ADR-0014-catch-to-producer-loop-judgment-catches-need-a-producer-route.md index ce83ac46f..18406504f 100644 --- a/docs/adr/ADR-0014-catch-to-producer-loop-judgment-catches-need-a-producer-route.md +++ b/docs/adr/ADR-0014-catch-to-producer-loop-judgment-catches-need-a-producer-route.md @@ -1,9 +1,15 @@ # ADR-0014: The Catch→Producer Loop — Judgment-Class Catches Need a Producer Route, Not a Mechanical Check -- **Status:** Accepted (2026-07-08) +- **Status:** Superseded by the Cathedral Cut (2026-07-14) - **Author:** AgentOps maintainers - **Builds on:** [ADR-0011](ADR-0011-escape-corpus-compounding-unproven-structural-starvation.md) (escape-corpus compounding unproven — data-starved), [ADR-0004](ADR-0004-corpus-moat-unproven-position-on-the-system.md) (corpus moat unproven). The EM spine (`escape → derived check → future block`) and `ao membrane {catch,triage,derive-checks}`. -- **Evidence:** the live catch corpus `.agents/yield/yield-ledger.jsonl` and `ao membrane triage` (numbers below, 2026-07-08), the producer-defect register `docs/architecture/producer-defect-register.md`, and the current Learn ownership map (`skills/learn/references/post-verdict-actions.md`). +- **Evidence:** historical catch data summarized below. The optional replacement + contract is [`docs/contracts/producer-defect-register.md`](../contracts/producer-defect-register.md). + +> This ADR records why the old membrane feedback machinery failed. Its commands, +> automatic routing, receipts, and producer-side mutation are not active product +> behavior. Learn may later analyze caller-supplied verdict collections, but it +> has no critical-path or lifecycle authority. ## Context diff --git a/docs/agent-footguns.md b/docs/agent-footguns.md index 3bcc85e4e..068b08534 100644 --- a/docs/agent-footguns.md +++ b/docs/agent-footguns.md @@ -34,10 +34,10 @@ See also: `skills/swarm/references/worker-pitfalls.md` for general platform pitf ## Maintenance Protocol -This document is a living reference. Update it during every post-mortem cycle. +This document is a living reference. Update it during every postmortem cycle. **When to add an entry:** -- A post-mortem discovers a framework/platform surprise that wasted agent time +- A postmortem discovers a framework/platform surprise that wasted agent time - A swarm worker hits a known limitation not documented here - A new tool or library introduces a gotcha @@ -47,4 +47,4 @@ This document is a living reference. Update it during every post-mortem cycle. 3. Format: `- **Bold name**: Description of the footgun and how to avoid it` 4. Include the relevant file path or code reference -**Update cadence:** Every `/post-mortem` should check: "Did we discover a new footgun?" If yes, add it here in the same cycle — not next cycle. +**Update cadence:** Every `/postmortem` should check: "Did we discover a new footgun?" If yes, add it here in the same cycle — not next cycle. diff --git a/docs/agentops-3-explainer-kit.md b/docs/agentops-3-explainer-kit.md deleted file mode 100644 index f4bc8f12e..000000000 --- a/docs/agentops-3-explainer-kit.md +++ /dev/null @@ -1,166 +0,0 @@ -# AgentOps 3.0 Explainer Kit - -This is the public text kit for explaining AgentOps 3.0 in a gist, README -section, launch post, or video description. - -## One-Sentence Positioning - -AgentOps is the engineering operating system for agent teams: a disciplined -engineering layer that gives coding agents shared domain context, review -verdicts, tracked follow-up work, and optional out-of-session compounding (the -loop dispatched on a reference NTM + MCP + managed-agents substrate). - -## Problem Statement - -Coding agents are useful, but most teams still operate them like isolated chat -sessions. A model can make a reasonable product, design, or engineering call in -one window, while the next run loses the domain language, prior concerns, -review evidence, and follow-up decisions. - -Human engineering teams solved this class of problem with shared domain models, -specs, tests, code review, issue trackers, release gates, runbooks, and wikis. -AgentOps encodes that operating discipline for agents. - -The packet is the linked intent object. It is the thing that moves through the -AgentOps lifecycle: product/domain intent becomes a context briefing, then a -council verdict, then tracked work, then an execution packet, validation -evidence, and a handoff. Provenance and trace make that movement inspectable. - -Canonical doctrine for that discipline lives in `docs/cdlc.md`. `PRACTICE-REGISTRY.md` -backs the packet with practice lineage and stable `practices: [slug]` -citations. - -## Target User - -AgentOps 3.0 is for agent-heavy maintainers, staff engineers, technical -founders, and small teams who already use Claude Code, Codex CLI, Cursor, or -OpenCode on real repositories and want repeated agent work to become more -coherent instead of resetting every session. - -They do not need a hosted control plane to see value. The first proof is one -decision becoming a shared packet, a council verdict, and tracked work. - -## The 5-Command Path - -After installing AgentOps and restarting the agent runtime: - -```bash -ao quick-start -cp docs/examples/agentops-3-domain-practice-packet.md .agents/packets/agentops-3-launch.md -ao context assemble --phase planning --task "Evaluate the AgentOps 3.0 launch demo against the domain/practice packet" --output-file .agents/rpi/briefing-current.md -``` - -```text -/council --mixed validate "Given .agents/packets/agentops-3-launch.md, should the AgentOps 3.0 launch demo lead with council-first engineering judgment?" -``` - -```bash -BEADS_DIR="$(ao beads dir)" br create "Apply council verdict to launch demo" --body "From .agents/council//verdict.md" -``` - -Expected artifacts: - -- `.agents/packets/agentops-3-launch.md` -- `.agents/rpi/briefing-current.md` -- `.agents/council//verdict.md` -- `_beads/issues.jsonl` - -For the full first-session path with time budget and fallbacks, see -[AgentOps 3.0 First-Value Path](first-value-path.md). - -For launch video outlines, clip hooks, CTAs, and measurement fields, see -[AgentOps 3.0 YouTube Starter Series](agentops-3-youtube-starter-series.md). -For observed first-run evidence and claim promotion rules, see -[AgentOps 3.0 PMF Evidence Loop](agentops-3-pmf-evidence-loop.md). - -## Domain Packet Example - -Use [AgentOps 3.0 Domain/Practice Packet](examples/agentops-3-domain-practice-packet.md) -as the launch example. - -The packet makes these things visible before the agents judge: - -- Product identity and target user. -- The decision under review. -- Product, goal, issue, standards, and evidence sources. -- The practice lineage and citations from `PRACTICE-REGISTRY.md`. -- Engineering practices to enforce, including DDD/TDD/BDD/review/release - discipline where relevant. -- Non-goals and claims that require external evidence before public use. - -## Council Verdict Example - -Use [AgentOps 3.0 Council Verdict Example](examples/agentops-3-council-verdict-example.md) -as public sample output. - -The important shape: - -- Every judge sees the same domain/practice packet. -- Claude and Codex can disagree, but they are disagreeing inside the same - engineering frame. -- The consolidated verdict is PASS/WARN/BLOCK, not loose advice. -- The verdict creates follow-up work or a launch decision. - -## Why This Is Not Just Multi-Chat Prompting - -| Ad hoc multi-chat | AgentOps 3.0 path | -|---|---| -| Paste context into several chats manually. | Put the domain/practice packet in a reviewable artifact. | -| Ask each model for an opinion. | Ask judges for a verdict against the same evidence bar. | -| Copy useful parts back by hand. | Record `.agents/council//verdict.md` and create br follow-up work. | -| Lose the reasoning after the session. | Keep local packets, briefings, verdicts, issues, and learnings inspectable. | -| Re-explain intent at every phase. | Hand the packet lineage through briefing, verdict, execution, validation, and handoff artifacts. | -| Automation starts as a giant promise. | Automation is second-stage, after the packet and verdict earn trust. | - -## Out-Of-Session Expansion Path - -Out-of-session orchestration is the deeper lane, not the first proof. -AgentOps ships no daemon or scheduler of its own — the loop runs in session, -and unattended runs are delegated to a swappable orchestration substrate. The -reference substrate is the trio **NTM + MCP + managed-agents** (see -[ADR-0009](adr/ADR-0009-daemon-deletion-in-session-only.md) and -[docs/dependencies.md](dependencies.md)). - -After the user sees one packet and one verdict: - -```bash -# The substrate dispatches a whole loop as one unit: it spawns an agent that -# runs the /rpi skill over the next ready bead — it never drives the loop's insides. -# (The `ao rpi ` engine was removed in 3.0; the loop now runs in-session -# via the /rpi skill, dispatched out-of-session by the NTM + MCP Agent Mail substrate.) -# Scheduled maintenance (Dream reports, wiki curation, release checks) is driven -# by the substrate's triggers (NTM swarm, cron, or MCP). See docs/dependencies.md. -``` - -Use the substrate lane for approved recurring work such as Dream reports, wiki -curation, release checks, or other compounding jobs where the operator has -already accepted the artifact shape. - -## Evidence-Gated Claims - -Use these claims now: - -- AgentOps is the engineering operating system for agent teams. -- AgentOps is a disciplined engineering layer for agentic software development. -- AgentOps turns agent opinions into engineering verdicts. -- AgentOps keeps agent work local, inspectable, and repo-native unless the - operator chooses external services. - -Do not claim these without exported evidence: - -- Product-market fit. -- Specific productivity or speedup numbers. -- Safety-critical, regulated, or certified operation. -- Fully autonomous source mutation as the first-value promise. -- A public customer outcome that only exists in local `.agents/` notes. - -## Launch CTA - -Use one CTA at the end of public content: - -```text -Install AgentOps, run ao demo --quick, then follow docs/first-value-path.md to get your first council verdict. -``` - -The goal is not to explain every skill. The goal is to get a maintainer to see -one shared engineering domain become one verdict artifact they can inspect. diff --git a/docs/agentops-3-pmf-evidence-loop.md b/docs/agentops-3-pmf-evidence-loop.md deleted file mode 100644 index c5c82a495..000000000 --- a/docs/agentops-3-pmf-evidence-loop.md +++ /dev/null @@ -1,208 +0,0 @@ -# AgentOps 3.0 PMF Evidence Loop - -This loop turns the council-first content funnel into product discovery. It is -not a PMF claim. It is the operating plan for collecting evidence before public -copy says anything about PMF, productivity, speed, or user outcomes. - -## Target Segment - -Primary segment: - -- Agent-heavy maintainers working repeatedly in one real codebase. -- Already using Claude Code, Codex CLI, Cursor, or OpenCode. -- Feeling context debt: repeated investigations, scattered decisions, weak - review evidence, or cold starts between sessions. -- Comfortable running terminal commands and inspecting local artifacts. - -Secondary segment: - -- Technical founders or staff engineers evaluating agent workflows for a small - engineering team. -- Open-source maintainers who need agent work to remain reviewable across PRs. - -Exclude from this loop: - -- People who have never used coding agents. -- Buyers primarily looking for hosted enterprise orchestration. -- Users who need compliance claims before a redaction/export path exists. - -## Outreach List - -Start with a small, high-signal list. Track each contact in a private sheet or -local notes until explicit permission exists to quote or publish. - -| Channel | Who to recruit | Ask | -|---|---|---| -| YouTube comments | Viewers asking how council, packets, or out-of-session substrate orchestration works | "Want to try the first-value path on one repo and tell me where it breaks?" | -| GitHub issues/discussions | Users who star, open install questions, or ask about Claude/Codex workflows | "Can we observe your first council verdict and capture setup friction?" | -| Discord/Slack communities | Engineers already showing coding-agent work in public | "Would a shared domain packet help your agent review workflow?" | -| Direct outreach | Maintainers known to use agents on active OSS or internal repos | "I am testing a council-first workflow for agent teams; can you run a 20-minute path?" | -| Existing users | People who have run AgentOps, Dream, the out-of-session substrate, or `/rpi` before | "Does the new packet/council path explain the product faster?" | - -## First-Run Scenario - -Ask each participant to follow [AgentOps 3.0 First-Value Path](first-value-path.md) -on a real repo or a public clone they are comfortable using. - -Minimum scenario: - -1. Install or update AgentOps. -2. Run `ao quick-start`. -3. Copy or edit a domain/practice packet. -4. Assemble context. -5. Run `/council --mixed` or documented fallback. -6. Inspect `.agents/council//verdict.md`. -7. Create tracked follow-up work from the verdict. - -Second-stage scenario: - -1. Reuse the same packet or verdict in a later engineering decision. -2. Run the loop out of session: hand it to the reference substrate (NTM + MCP + - managed-agents) and let it dispatch ready beads to workers driving the - seven-move operating loop in-session (the `ao rpi` engine was **removed** in - 3.0; workers use the `/rpi` skill). -3. Report whether always-on compounding felt useful or premature. - -## Interview Script - -Use this after the run. Do not lead the user toward a positive answer. - -1. What were you trying to get your agent team to decide or validate? -2. Before AgentOps, where would that context and decision have lived? -3. Could you explain the domain/practice packet in your own words? -4. Which packet section was most useful or confusing? -5. Did council feel different from asking multiple chats separately? -6. Did the verdict artifact change what you trusted, changed, or tracked? -7. Did creating follow-up work from the verdict feel natural? -8. What blocked or slowed first value? -9. Would you reuse the packet or verdict in a later decision? -10. When, if ever, would you trust an out-of-session substrate run (NTM + MCP + managed-agents) or a schedule? -11. What would make you recommend this to another maintainer? -12. What claim would feel false or overreaching if we put it on the homepage? - -## Metrics - -### Activation Metric - -Activation is not install. Activation is: - -```text -Participant produced or inspected .agents/council//verdict.md from a visible domain/practice packet. -``` - -Record: - -- Runtime used. -- Setup time in minutes. -- Council mode: mixed, quick fallback, blocked. -- Verdict status: PASS, WARN, BLOCK, or unclear. -- First artifact path or redacted export path. - -### Council/Domain Artifact-Reuse Metric - -Reuse signal: - -```text -Participant reused the same packet or verdict in a later decision, PR review, issue, or planning session. -``` - -Record: - -- Reuse date. -- Reuse context. -- Whether the packet changed. -- Whether the verdict created follow-up work. -- Whether a later agent cited the packet or verdict. - -### Out-of-Session Substrate Adoption Metric - -Out-of-session substrate adoption is second-stage: - -```text -Participant intentionally configured a schedule after first verdict trust existed. -``` - -Record: - -- No always-on interest. -- Interested but blocked. -- Ran the loop in session (the `/rpi` and `/evolve` skills; the `ao rpi` and `ao evolve` CLIs were **removed** in 3.0 — `ao goals measure` for fitness). -- Handed the loop to the reference substrate (NTM + MCP + managed-agents). -- Substrate dispatch sent a ready bead to a worker driving the operating loop - (the `ao rpi` engine was **removed** in 3.0; the `/rpi` skill drives it). -- An out-of-session run completed and produced an inspected artifact. - -Do not count always-on substrate setup as first-value activation. - -## Evidence Record Template - -Exportable evidence belongs under: - -```text -docs/releases/agentops-3-pmf-evidence/ -``` - -Each participant record should be private by default. Export only redacted, -permissioned summaries using -[AgentOps 3.0 PMF Evidence Record Template](releases/agentops-3-pmf-evidence/record-template.md). - -Required fields: - -- Participant alias. -- Permission status. -- Source channel. -- Runtime. -- Repo type: public clone, OSS repo, private repo, synthetic demo. -- Setup time. -- Whether the CDLC and operating-loop docs made the packet's engineering doctrine clearer. -- Packet path or redacted packet excerpt. -- Council mode. -- Verdict path or redacted verdict excerpt. -- Follow-up work created. -- Reuse signal. -- Out-of-session substrate adoption signal. -- Friction notes. -- Quote, only with explicit permission. -- Claim posture: none, private learning only, public anonymized evidence, or - public quoted evidence. - -## Claim-Ledger Posture - -Before public PMF claims: - -1. Keep PMF, productivity, and speed claims blocked. -2. Allow product-shape claims such as "engineering operating system for agent - teams" and "from agent opinions to engineering verdicts." -3. Export redacted evidence under `docs/releases/agentops-3-pmf-evidence/`. -4. Add or update claim markers only after exported evidence exists. -5. Run: - -```bash -bash scripts/check-factory-claim-ledger.sh --strict --no-fixtures -``` - -Promotion from "learning signal" to "public claim support" requires: - -- At least five completed first-value runs from target-segment users. -- At least three successful council verdict artifacts. -- At least two reuse signals on a later decision. -- At least one out-of-session substrate adoption signal, if always-on claims are used. -- No unresolved claim-ledger failure for the public copy. - -## Operating Cadence - -| Cadence | Action | -|---|---| -| Daily during launch week | Review comments, issues, stars, install questions, and first-value reports. | -| Twice weekly | Conduct 2-3 PMF interviews or observed first runs. | -| Weekly | Update evidence summaries, friction list, and blocked claims. | -| Before launch copy changes | Re-run claim-ledger gate and cite exported evidence paths. | - -## Follow-Up Outputs - -The loop should produce: - -- Redacted evidence records in `docs/releases/agentops-3-pmf-evidence/`. -- Follow-up `br` issues (`BEADS_DIR="$(ao beads dir)" br`) for repeated setup friction. -- Copy changes only when claim posture allows them. -- Product changes when repeated users fail before first verdict. diff --git a/docs/agentops-3-youtube-starter-series.md b/docs/agentops-3-youtube-starter-series.md deleted file mode 100644 index d9831d3f0..000000000 --- a/docs/agentops-3-youtube-starter-series.md +++ /dev/null @@ -1,285 +0,0 @@ -# AgentOps 3.0 YouTube Starter Series - -This is the launch content plan for teaching AgentOps 3.0 through concrete -workflows. The series starts with council because the fastest product proof is -Claude and Codex judging one decision against the same domain/practice packet. - -## Series Strategy - -Audience: agent-heavy maintainers, technical founders, staff engineers, and -small teams who already use coding agents on real repos. - -Promise: show how AgentOps turns isolated agent opinions into shared -engineering verdicts, then into tracked work and optional scheduled -compounding. - -Primary CTA: follow [AgentOps 3.0 First-Value Path](first-value-path.md). - -Secondary CTA: read the [AgentOps 3.0 Explainer Kit](agentops-3-explainer-kit.md) -and run `ao demo --quick`. - -Evidence CTA: capture first-run and interview signals through -[AgentOps 3.0 PMF Evidence Loop](agentops-3-pmf-evidence-loop.md). - -## Publishing Cadence - -| Week | Asset | Goal | -|---|---|---| -| 1 | Episode 1 plus 2 short clips | Teach the council-first value path. | -| 1 | Episode 2 plus 2 short clips | Show the domain/practice packet as the product object. | -| 2 | Episode 3 plus 2 short clips | Show verdict-to-work with br and `.agents/`. | -| 2 | Episode 4 plus 2 short clips | Show the out-of-session substrate (NTM + Agent Mail) as second-stage automation. | -| 3 | Episode 5 plus 2 short clips | Show the full 3.0 launch workflow and invite PMF interviews. | - -Do not publish productivity or PMF claims in titles or thumbnails. The series -is a teaching funnel and evidence loop. - -## Long-Form Episodes - -### 1. AgentOps 3.0: Claude + Codex, One Engineering Verdict - -**Thumbnail text:** Claude + Codex -> One Verdict - -**Title options** - -- AgentOps 3.0: Claude + Codex Reviewing One Decision -- How AgentOps Turns Agent Opinions Into Engineering Verdicts - -**Cold open** - -> Two agents can disagree. The problem is when they disagree without the same -> domain, evidence, or release bar. AgentOps gives them the same engineering -> operating layer. - -**Demo beats** - -1. Show `docs/examples/agentops-3-domain-practice-packet.md`. -2. Run `ao context assemble`. -3. Run `/council --mixed`. -4. Open `.agents/council//verdict.md`. -5. Create br follow-up from the verdict. - -**CTA** - -Run `ao demo --quick`, then follow `docs/first-value-path.md`. - -**Measurement fields** - -- Comments asking what council is. -- Installs within 48 hours. -- GitHub stars within 48 hours. -- Reported council runs. -- Requests for setup help. - -### 2. The Domain Packet: DDD, TDD, BDD, Review, And Release Rules For Agents - -**Thumbnail text:** Give Agents A Domain - -**Title options** - -- Stop Prompting From Scratch: Give Agents A Domain Packet -- AgentOps Domain Packets: Engineering Discipline For Coding Agents - -**Cold open** - -> Software teams do not coordinate complex work by vibes. They use domain -> language, tests, review, issues, and release gates. AgentOps packages those -> practices for agents. - -**Demo beats** - -1. Open the domain/practice packet. -2. Open `docs/cdlc.md` as the foundation text behind the packet. -3. Point at product identity, target user, sources, practice sources, evidence - rules, and non-goals. -4. Copy it into `.agents/packets/`. -5. Show the matching context briefing. -6. Show where the verdict cites the packet. - -**CTA** - -Copy the example packet into one repo and edit the target user, decision, and -evidence rules. - -**Measurement fields** - -- Viewers who can explain the packet in comments or PMF calls. -- Packet copies or adaptations shared back. -- Questions about how to map existing repo docs into packets. - -### 3. From Verdict To Work: Making Agent Decisions Survive The Session - -**Thumbnail text:** Verdict -> Work - -**Title options** - -- Agent Decisions Should Not Die In Chat -- Turn A Claude/Codex Verdict Into Tracked Engineering Work - -**Cold open** - -> The point is not that a council says something smart. The point is that the -> decision leaves a trace and becomes work the next agent can pick up. - -**Demo beats** - -1. Open the sample verdict. -2. Show PASS/WARN/BLOCK shape. -3. Run `BEADS_DIR="$(ao beads dir)" br create ... --body "From .agents/council//verdict.md"`. -4. Show `_beads/issues.jsonl`. -5. Re-run `ao context assemble` with the new issue as the task. - -**CTA** - -Create one issue from one verdict and note whether the next session starts -with clearer context. - -**Measurement fields** - -- Number of viewers who create first br issue. -- Number of viewers who paste a verdict path into a follow-up issue. -- Questions about beads versus existing issue trackers. - -### 4. Out Of Session Is The Substrate: Always-On Compounding After Trust - -**Thumbnail text:** Schedule After Trust - -**Title options** - -- Always-On AgentOps: The Out-Of-Session Lane After Your First Verdict -- Do Not Start With Automation. Start With A Verdict. - -**Cold open** - -> Always-on compounding is powerful, but it is not the first thing to sell. -> First prove the packet and verdict in a plain session. Then hand the loops you -> trust to an orchestration substrate to run unattended. - -**Demo beats** - -1. Recap the packet and verdict artifact. -2. Run the loop in session via the operating loop (primary), driven by the - `/rpi` skill (one cycle) or the `/evolve` skill (many). The legacy - `ao rpi ` and `ao evolve` CLI executors were removed in 3.0. -3. To run it out of session, hand the loop to the substrate - (NTM + MCP Agent Mail); e.g. an NTM tmux swarm (or a lead agent) runs - `BEADS_DIR="$(ao beads dir)" br ready` and dispatches ready beads to workers. -4. Schedule corpus maintenance via the substrate (a driver or cron - running `ao compile`, `ao maturity --scan`). -5. Explain wiki/forge as compounding jobs, not unattended source mutation. - -**CTA** - -Only run a loop out of session after you can inspect the artifact it produces in -session. - -**Measurement fields** - -- In-session loop runs (operating loop / `/evolve` skill) before opting into always-on. -- Substrate dispatch screenshots or reports. -- Questions about safety boundaries and source mutation. - -### 5. AgentOps 3.0 Full Path: First Verdict To PMF Evidence - -**Thumbnail text:** First Verdict -> PMF Evidence - -**Title options** - -- The AgentOps 3.0 Launch Workflow -- Building An Engineering OS For Agent Teams - -**Cold open** - -> A product this dense needs evidence, not just a better tagline. The 3.0 loop -> is content, first verdict, reuse, interview, and claim discipline. - -**Demo beats** - -1. Show the explainer kit. -2. Show first-value path. -3. Run through the five commands quickly. -4. Show PMF evidence fields. -5. Invite users to run the path and report friction. - -**CTA** - -Open an issue or comment with: runtime used, setup time, council mode, verdict -path, whether the packet made sense, and what blocked first value. - -**Measurement fields** - -- PMF interview bookings. -- Activation reports with setup time and verdict path. -- Packet reuse in later work. -- Substrate adoption after first verdict. - -## Short Clip Hooks - -1. "Two agents arguing is not useful until they share the same domain." -2. "This file is the product: the domain packet your agents judge against." -3. "Claude and Codex can disagree. AgentOps makes the disagreement reviewable." -4. "The first artifact is not a dashboard. It is a verdict file." -5. "Do not start with the out-of-session substrate. Start with one trusted verdict." -6. "Agent decisions should become issues, not vanish into chat." -7. "DDD, TDD, BDD, review, and release gates are agent context now." -8. "The killer demo is not automation. It is shared engineering judgment." -9. "Your `.agents/` folder is a local wiki for humans and agents." -10. "Bring your agent, bring your harness, keep the operating discipline." - -## Recording Checklist - -- Clean worktree or clearly labeled demo branch. -- `ao version` visible. -- Claude Code and Codex CLI authenticated if using mixed council. -- Fallback `/council --quick` command ready. -- `docs/examples/agentops-3-domain-practice-packet.md` open in editor. -- `docs/first-value-path.md` open for CTA. -- Terminal font large enough for command readability. -- No secrets, home paths, tokens, private transcripts, or unredacted `.agents/` - content on screen. -- Demo verdict can be synthetic only if clearly labeled as sample output. -- Claim-safe language copied from the explainer kit. - -## CTA Map - -| Asset | Primary CTA | Secondary CTA | -|---|---|---| -| Episode 1 | Run `ao demo --quick`. | Follow `docs/first-value-path.md`. | -| Episode 2 | Copy and edit the domain packet. | Read `docs/domain-practice-packets.md`. | -| Episode 3 | Create one br issue from one verdict. | Read the council verdict example. | -| Episode 4 | Hand a trusted loop to the substrate only after first verdict. | Read the NTM + Agent Mail substrate docs. | -| Episode 5 | Share a first-value report or book a PMF interview. | Star the repo if the path worked. | -| Short clips | Watch Episode 1 or read the explainer kit. | Run `ao demo --quick`. | - -## PMF Evidence Fields - -Collect these after content drives a first run: - -| Field | Type | -|---|---| -| Viewer source | Episode, clip, README, gist, direct outreach, community post | -| Runtime | Claude Code, Codex CLI, Cursor, OpenCode, mixed | -| Setup time | Minutes to `ao quick-start` complete | -| Packet comprehension | Could the viewer explain the domain/practice packet? | -| Council mode | Mixed, quick fallback, blocked | -| Verdict artifact | Path or redacted exported sample | -| Follow-up work | br issue id or external issue link | -| Substrate adoption | None, attempted, substrate dispatch listed, unattended run completed | -| Reuse signal | Packet or verdict reused in later engineering decision | -| Friction | Free text | -| Interview status | Requested, scheduled, completed, declined | - -## Claim Guardrails - -Allowed: - -- "engineering operating system for agent teams" -- "disciplined engineering layer for agentic software development" -- "from agent opinions to engineering verdicts" - -Blocked without exported evidence: - -- Product-market fit. -- Specific speed, quality, or productivity lift. -- Fully autonomous factory as the first-value promise. -- Regulated or certified-operation claims. diff --git a/docs/agentops-system-map.md b/docs/agentops-system-map.md deleted file mode 100644 index a5159ffb2..000000000 --- a/docs/agentops-system-map.md +++ /dev/null @@ -1,192 +0,0 @@ -# AgentOps — System Map - -``` -┌──────────────────────────────────────────────────────────────────┐ -│ AgentOps at a Glance │ -├───────────────────┬──────────────────────┬───────────────────────┤ -│ 63 Skills │ 76 CLI Commands │ Hookless (CI-gated) │ -│ (workflows) │ (ao binary) │ (validate.yml) │ -└───────────────────┴──────────────────────┴───────────────────────┘ -``` - ---- - -## The Pipeline — Skills Calling Skills - -The top-level skill `/rpi` chains the full pipeline. Each node is a skill. Arrows show calls. - -``` - ┌─────────────┐ - │ /evolve │ ← loops /rpi overnight - └──────┬──────┘ fitness-gated - │ calls - ▼ -┌───────────────────────────────────────────────────────────────────┐ -│ /rpi │ -│ (full pipeline orchestrator) │ -└──┬──────────┬───────────┬─────────────┬──────────┬────────────────┘ - │ │ │ │ │ - ▼ ▼ ▼ ▼ ▼ -/research /plan /pre-mortem /crank /post-mortem - │ │ │ │ │ - │ │ calls /council │ ├── calls /council - │ │ │ └── calls /retro - │ │ │ - │ │ ┌──────────┴──────────┐ - │ │ │ /crank │ - │ │ │ (wave executor) │ - │ │ └──────────┬──────────┘ - │ │ │ spawns N parallel - │ │ ▼ - │ │ /implement - │ │ /implement ← one per issue - │ │ /implement - │ │ │ - │ │ ▼ - │ │ /vibe ←── calls /council - │ │ ←── calls /complexity - │ │ ←── calls /bug-hunt - │ │ - └──────────┴────────────────────────────────────────────────────── -``` - ---- - -## Judgment Layer — Everything Flows Through Council - -`/council` is the core validation primitive. Three skills wrap it: - -``` - ┌──────────────────────────────┐ - │ /council │ - │ (independent judges debate, │ - │ verdict gates delivery) │ - └───────────┬──────────────────┘ - │ used by - ┌────────────────────┼────────────────────┐ - ▼ ▼ ▼ - /pre-mortem /vibe /post-mortem - (validate plans (validate code (wrap-up + - before building) before shipping) learnings) -``` - ---- - -## Knowledge Layer — Skills Calling the CLI - -Skills hand off to `ao` to persist knowledge across sessions: - -``` - SKILL ao CLI COMMAND RESULT - ───── ────────────── ────── -/research → ao lookup Prior knowledge loaded into session -/retro → ao forge transcript Learnings extracted from session -/retro → ao pool promote Validated learnings promoted -/evolve → ao goals measure Fitness checked before next cycle -/rpi → ao ratchet record Progress gate checkpointed -/implement → ao ratchet check Gate verified before work starts -/post-mortem → ao compile Findings become artifacts, checks, and constraints -/post-mortem → ao flywheel close-loop Citation feedback and lifecycle updates applied -``` - ---- - -## Prevention Ratchet - -The closed-loop prevention path is file-native: - -``` -/post-mortem or /pre-mortem - │ - ▼ -.agents/findings/registry.jsonl - │ - ▼ -ao flywheel / compile (explicit command — no auto-hook) - │ - ├──> .agents/findings/.md - ├──> .agents/planning-rules/.md - ├──> .agents/pre-mortem-checks/.md - └──> .agents/constraints/index.json (mechanical + active only) - │ - ▼ - .github/workflows/validate.yml (CI gate) -``` - -`/plan`, `/pre-mortem`, `/vibe`, and `/post-mortem` load compiled planning and review artifacts first, then fall back to the registry when compiled outputs are missing. AgentOps 3.0 is hookless: enforcement of active mechanical findings is shift-left via the CI gates in `.github/workflows/validate.yml`, not an auto-firing hook. - ---- - -## CLI Command Groups (76 commands including subcommands) - -``` -KNOWLEDGE FLYWHEEL VALIDATION GATES SESSION / LIFECYCLE -────────────────── ──────────────── ─────────────────── -ao forge ao gate pending ao session close -ao pool ingest ao gate approve ao session bootstrap -ao pool promote ao gate reject ao gate check -ao lookup ao ratchet status ao session bootstrap -ao lookup ao ratchet record ao config -ao search ao ratchet check -ao dedup ao ratchet promote METRICS / HEALTH -ao curate ──────────────── - GOALS / FITNESS ao metrics health -MEMORY TOOLS ─────────────── ao metrics flywheel -──────────── ao goals measure ao metrics report -ao mind ao goals steer ao flywheel status -ao notebook ao goals add ao maturity -ao memory ao goals prune ao doctor -ao trace ao goals history -ao extract ao goals drift UTILITIES - ───────── - ao search - ao constraint - ao badge - ao version -``` - ---- - -## Enforcement — Hookless, CI-Gated - -AgentOps 3.0 is hookless: it ships zero hooks by default and nothing auto-injects or auto-enforces at session boundaries. Validation that used to live in shell hooks is now the authoritative CI gate in `.github/workflows/validate.yml` (T0/T1/T2 tiers, all required), with explicit `ao` commands and skills doing the in-session work. An opt-in `hooks-authoring` skill lets you add your own hooks if you want always-on local signals, but the corpus carries none. - -``` -WAS A HOOK NOW -────────── ─── -Session-boundary staging ao session bootstrap / ao inject (explicit, run on demand) -Learning-loop close ao flywheel close-loop (called by /post-mortem) -Task/constraint validation .github/workflows/validate.yml CI gates -Complexity budget golangci-lint + validate.yml -Skill / git / pre-mortem validate.yml gates + /pre-mortem skill -checks (re-author as opt-in via hooks-authoring skill) -``` - ---- - -## Skill Tiers at a Glance - -``` -JUDGMENT EXECUTION KNOWLEDGE INTERNAL -──────── ───────── ───────── ──────── -council research retro inject -vibe plan forge extract -pre-mortem implement flywheel ratchet -post-mortem crank goals standards - swarm beads - rpi shared - evolve - release - doc - status - handoff - quickstart - brainstorm - bug-hunt - complexity - + 14 more -``` - ---- - -*63 skills · 76 CLI commands · hookless (CI-gated) · 0 telemetry · everything in plain files* diff --git a/docs/architecture/ao-command-customization-matrix.md b/docs/architecture/ao-command-customization-matrix.md deleted file mode 100644 index 9a067f705..000000000 --- a/docs/architecture/ao-command-customization-matrix.md +++ /dev/null @@ -1,44 +0,0 @@ -# AO Command Customization Matrix - -This matrix tracks external command dependencies in the AO CLI and how each command group is customized. - -> This repo's tracker command is `br` (beads_rust), invoked as `BEADS_DIR="$(ao beads dir)" br `; `bd`/Dolt is the gascity substrate store (a different layer), not this repo's tracker. The `rpi` rows describe the load-bearing-legacy RPI lane, not the live operating loop. - -Audit source: -- `scripts/audit-cli-command-deps.sh` - -Customization tiers: -- `Tier A` (runtime-customizable): command path can be configured via `rpi.*_command` settings and matching env vars. -- `Tier B` (fixed system tools): command path stays fixed for safety/contract stability. -- `Tier C` (no external process): no `exec.Command*`/`exec.LookPath` dependency on the runtime path. - -## Current Matrix - -| Command Group | External Dependencies | Tier | Notes | -|---|---|---|---| -| `rpi phased` | `runtime`, `br`, `ao` | Tier A (`runtime`, `br`, `ao`) + Tier B (`git`, `bash`, `ps`) | Runtime + control-plane commands routed through shared RPI toolchain resolver. | -| `rpi loop --supervisor` | `git`, `bash`, `br` | Tier A (`br`) + Tier B (`git`, `bash`) | Landing/sync uses configurable `br` tracker command. | -| `rpi status` | `tmux` | Tier A (`tmux`) | Tmux liveness probe uses shared RPI toolchain resolver. | -| `rpi cancel` | `ps` | Tier B | Process tree inspection remains fixed for portability. | -| `rpi cleanup` | `git` | Tier B | Cleanup lifecycle remains fixed to git contracts. | -| `internal/rpi/worktree` | `git` | Tier B | Detached-head/worktree safety remains fixed to git contracts. | -| `context` | `tmux` | Tier B | Not yet migrated to shared customization layer. | -| `worktree` | `git`, `tmux` | Tier B | Not yet migrated to shared customization layer. | -| `search` | `cass`, `rg`, `grep` | Tier B | Brokers to upstream `cass` for session history and keeps fixed repo-local fallback helpers. | -| `goals`/`ratchet` | `bash`, `git`, `br` | Tier B | Candidate for follow-up after RPI path is stable. | -| `plans` | `br` | Tier B | Candidate for follow-up after RPI path is stable. | -| `quick-start` | `br` | Tier B | Candidate for follow-up after RPI path is stable. | -| `hooks` | `ao` | Tier B | Candidate for follow-up after RPI path is stable. | -| Other AO command groups | none on runtime path | Tier C | No external process invocation in steady-state execution path. | - -## Policy Defaults - -Runtime-focused customization defaults: -- configurable: `runtime`, `ao`, `br`, `tmux` for RPI control plane. -- fixed: `git`, `bash`, `ps` unless a future adapter contract is introduced. - -Configuration sources (highest to lowest): -1. command flags (where exposed) -2. environment variables -3. config file (`--config` override or default project/home lookup) -4. built-in defaults diff --git a/docs/architecture/build-tags.md b/docs/architecture/build-tags.md deleted file mode 100644 index dadcfde1e..000000000 --- a/docs/architecture/build-tags.md +++ /dev/null @@ -1,49 +0,0 @@ -# Build Tags During the Direct Cut - -> Current executable truth and deletion ownership for the transitional command -> profiles introduced by [ADR-0012](../adr/ADR-0012-focus-surface-on-membrane-bookkeeper-archive-satellites.md). - -## Current state - -The current tree still compiles `flywheel`, `legacy`, and combined variants. -That fact is transitional, not a product contract. New code must not add a tag, -profile membership, restoration command, tagged fallback, or profile-specific -behavior. - -Until F4 executes, the existing diagnostic commands remain useful for proving -the inventory that must disappear: - -```bash -make verify-buildtags -AO_DUMP_REGISTERED_CMDS=1 go -C cli test -tags=flywheel,legacy \ - ./cmd/ao -run '^TestDumpRegisteredTopLevelCommands$' -count=1 -v -``` - -These commands describe the old tree; they do not authorize consumers to depend -on it. - -## Cut ownership - -The exact CLI disposition manifest assigns every currently compiled top-level -root to one leaf. That leaf retains behavior under a final owner or deletes it -with its unique tests, docs, fixtures, and dependencies. K5, K7, and K9 own the -four roots coupled to verdict, delivery, and retired gate behavior. Other roots -belong to their exact `CLI.` leaves. - -After those leaves finish, F4 deletes: - -- the `flywheel`, `legacy`, and combined profile constants; -- build-tag-only command owners and tagged fallbacks; -- profile selection and default-root pruning; -- build-profile scripts, fixtures, and compatibility tests; and -- documentation that teaches restoration. - -D2 then regenerates the command reference and other declared projections once -from the single final executable source. - -## Admission rule - -A command-cut leaf is not ready until its checked authority-and-consumer -manifest is complete and disjoint. The old owner and its replacement or last -consumer must share one writer, one candidate, one acceptance check, and one -rollback. No intermediate dual runtime is admitted. diff --git a/docs/architecture/canonical-loop-model.md b/docs/architecture/canonical-loop-model.md deleted file mode 100644 index 6582886bc..000000000 --- a/docs/architecture/canonical-loop-model.md +++ /dev/null @@ -1,121 +0,0 @@ -# The Canonical Loop Model - -> Architecture doc. The single statement of how AgentOps' loops relate. Companion to [3.0.md](../3.0.md) (the north star), [Component Map](component-map.md) (product/component routing), [operating-loop.md](operating-loop.md) (the seven moves inside one tick), and [ports-and-adapters.md](ports-and-adapters.md) (the runtime seams). Derivation: [`.agents/discovery/2026-05-24-canonical-loops-ddd.md`](https://github.com/boshu2/agentops/blob/main/.agents/discovery/2026-05-24-canonical-loops-ddd.md). - -AgentOps once had roughly twenty surfaces that read like a loop (evolve, rpi, autodev, factory, daemon, crank, dream, swarm, ship-loop, ratchet, flywheel, and more), with no named hierarchy and circular help. This doc collapses that sprawl into one statement. - -## The model in one sentence - -**One loop body, two drivers, one inner tick, one config.** - -- **One loop body**: the same four umbrellas (Discovery, Crank, Validate, Learn) at every scale. -- **Two drivers**: an **Evolve** driver (an in-session agent runs the loop, self-paced) and a **Factory** driver (an out-of-session substrate runs the same loop unattended over a queue). -- **One inner tick**: **rpi**, one Discovery→Crank→Validate→Learn cycle over one bead. -- **One config**: **Autodev**, the durable intent layer the loop reads every tick. Autodev is not a loop. - -Everything else is a step of the loop, a config source, a runtime adapter, an execution profile, or a read projection. None of them is a peer loop. - -AgentOps ships the **Evolve** driver as its product: it runs in a plain session with zero AgentOps-managed daemon. The **Factory** driver is the *substrate's* job — AgentOps deleted its own daemon and delegates out-of-session execution to an adopted substrate (reference: NTM + MCP + managed-agents). See [3.0.md](../3.0.md) and [ADR-0009](../adr/ADR-0009-daemon-deletion-in-session-only.md). - -## The picture - -``` -AUTODEV-CONFIG (PROGRAM.md / AUTODEV.md + GOALS.md + ADRs) ← NOT a loop; the intent/spec the loop reads - │ drives - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ ONE LOOP BODY (the rpi tick: Discovery → Crank → Validate → Learn) -│ │ -│ EVOLVE driver (in session) FACTORY driver (out of session) -│ an agent runs the loop a SUBSTRATE runs the SAME loop -│ self-paced, self-tunable, unattended over the bead queue; -│ ends with the session operator-only stop -│ SHIPPED by AgentOps SUBSTRATE-owned (NTM / MCP / managed-agents) -│ │ │ -│ └──────────── both run ────────────────────┘ -│ │ -│ ▼ -│ ┌──────────────────────────────────────────────┐ -│ │ RPI TICK = inner loop (one cycle) │ -│ │ discovery → crank(wave) → validate → learn │ -│ │ one bead, one behavior, one acceptance proof │ -│ └──────────────────────────────────────────────┘ -└─────────────────────────────────────────────────────────────┘ -``` - -## The fractal framing - -The loop is fractal: the same shape at every layer, run by a human or by a stand-in agent. The only things that change across layers are the **driver** and the **stop policy**. - -- **rpi** is one tick. -- **evolve** is N rpi ticks toward a goal: select next-best work, run a tick, postmortem, repeat. -- **crank / swarm** fan one wave of an rpi tick across an in-session team of agents in isolated worktrees — still in session, still the same five beats per worker. -- A **Factory** is the same loop run unattended over a whole queue by an out-of-session substrate. - -Factory and Evolve are not two different loops. They are the **same loop body under two drivers**. Mount Olympus, the full-custom Rust reference implementation, demonstrates this: it folds its in-session evolve cycle into its daemon serve-loop so both paths run the identical tick. The difference is the driver (unattended substrate versus interactive session) and the stop policy (operator-marker-only for the substrate; session-budget allowed for interactive). Olympus keeps a daemon because it is a sovereign product with its own core; AgentOps deletes its daemon and runs the in-session driver only, opting into a substrate for the Factory driver. - -Because the shape repeats, the ratchet rules (no self-grade, fresh agent on failure, knowledge becomes constraints) apply identically at every layer. That is what makes the loop compound up the layers instead of repeating flat. The lineage is documented in [`.agents/research/2026-05-24-fractal-orchestration-lineage.md`](https://github.com/boshu2/agentops/blob/main/.agents/research/2026-05-24-fractal-orchestration-lineage.md): the same self-spawning, self-similar loop runs from the Kubernetes-era control plane (2025) through Olympus (2026) to AgentOps 3.0, lifted off any single substrate and given a name. - -## Context is the artifact handed off at every edge - -A loop tick takes context in and emits context out. The exhaust of one tick is the seed of the next. That handoff is the engineering artifact, and it accumulates in the `.agents/` corpus. - -- **In:** `ao inject` compiles a decay-ranked, token-budgeted slice of the corpus for the bead at hand. This is the context compiler doing its job at the start of a tick. -- **Out:** evidence, decisions, citations, and verdicts land in `.agents/` and on the bead, under the promotion ratchet. -- **`ao compile`** rebuilds the corpus periodically so the next inject is fresh. - -Context never flows through loop plumbing return values; it flows through the corpus and the bead. That is why the corpus is treated as the central artifact rather than a byproduct. Whether it compounds into a *moat* is an [explicitly unproven hypothesis](../adr/ADR-0004-corpus-moat-unproven-position-on-the-system.md) — the architecture bets on it, but the proven asset is the verification loop, not the moat. - -## The DDD seam: what owns the loop versus what orchestrates it - -AgentOps owns the in-session loop and the context. An orchestration substrate (reference: NTM + MCP + managed-agents) owns out-of-session execution. The boundary is sharp: - -| Domain | Owner | Primitives | -|---|---|---| -| **Orchestration**: when / where / who-supervises / coordination | **Substrate (NTM / MCP / managed-agents)** | a tmux agent swarm (NTM), the MCP tool surface (`ao mcp serve`), managed/agent-SDK drivers (`ao agent`), cron/event triggers (including evolve cadence), the bead queue, human on the loop and merge, runtime providers | -| **The in-session loop + the context**: what the agent does, how context compounds | **AgentOps** | the `/rpi` loop (run as a skill), `/evolve` (work-selection + N rpi), crank/swarm, the ratchet rules, skills, `ao inject` / `compile` / `maturity`, the `.agents/` corpus | - -The governing test: is it about *when, where, who supervises, or coordination*? That is the substrate. Is it about *what the loop does or how context compounds*? That is AgentOps. - -By that test: rpi's internal steps, the ratchet, `inject`, and `compile` belong to AgentOps; the agent calls them. Evolve's *cadence*, when run unattended, is a substrate cron/trigger; evolve's *logic* (which bead next, N cycles toward a goal) stays in AgentOps. The queue, the agents, and their supervision belong to the substrate. - -**The Factory driver is the substrate's job, not an AgentOps-shipped daemon.** AgentOps 3.0 ships no always-on daemon, scheduler, or overnight runner — they were deleted in the rearchitecture. When you want the loop to run unattended over a queue, a substrate drives it. On the reference substrate that dispatch is **swarm-driven**: an NTM tmux swarm (or a lead agent) runs `BEADS_DIR="$(ao beads dir)" br ready` then dispatches the next bead to a worker agent that runs the `/rpi` skill; a managed-agent driver (`ao agent`) or cron handles scheduled cadence, and `ao mcp serve` exposes the tool surface across the seam. AgentOps stays zero-dependency in a plain session through the Evolve driver. - -**rpi is never re-expressed in the substrate.** Decomposing the rpi tick into substrate-side workflow steps would duplicate the loop shape (re-introducing the surface-sprawl disease across the seam) and pit the substrate's retry machinery against the ratchet (substrate `max_attempts` retry versus fresh-agent-on-failure; substrate per-step agent assignment versus no-self-grade). The substrate dispatches a whole loop as one unit — an agent running the `/rpi` skill; it never drives the loop's insides. - -## Where the family members land - -Everything that used to look like a competing loop has a home under this model: - -| Surface | What it actually is | Home | -|---|---|---| -| **rpi** | The inner tick | The one inner tick | -| **evolve** | The in-session driver (N rpi cycles) | The Evolve driver | -| **autodev** | The config/intent layer (PROGRAM.md / AUTODEV.md + GOALS.md + ADRs) | One config, NOT a loop | -| **crank** | rpi's wave-executor step (in session) | A loop step, under rpi | -| **swarm** | crank's parallel-dispatch adapter (in session) | A loop step, under crank | -| **ratchet** | The loop's knowledge-capture step | A loop step (move 7) | -| **dream** | The loop run on a knowledge-compounding goal on a schedule | A substrate job profile, not a loop | -| **factory** | The out-of-session driver (substrate-owned) | A driver, not an AgentOps-shipped surface | -| **ship-loop** | A fast-lane evolve/rpi preset | An execution profile, not a loop | -| **flywheel** | A health check on the corpus | Background check, not a loop | -| **goals / council** | Fitness/intent config; a validation step | Config and step, not loops | - -## Where this doc lives and what it replaces - -This doc lives at `docs/architecture/canonical-loop-model.md` and is linked from the 3.0 north star and the documentation index. It is the canonical statement of the loop hierarchy. - -It supersedes the help-string decision-tree approach (the prior attempt to disambiguate evolve, rpi, autodev, and factory as four near-synonyms). Instead of telling agents how to pick between four peers, this model states that they are not peers: they are drivers, an inner tick, and a config of one loop. The vocabulary entries that back this model live in [`skills/domain/references/`](../../skills/domain/references/) (Loop, Factory, Evolve, RPI, Autodev-as-config, Context-Compiler). - -## See also - -- [the-agent-factory.md](the-agent-factory.md): the control-plane **primitives / citizens** view (roles × primitives × the adapter taxonomy) — the structural counterpart to this loop-hierarchy statement; the unifying entry that cross-links the loop/primitive docs -- [control-loop-model.md](control-loop-model.md): why the loop converges (fast) and self-improves (slow + SPC governor) — the *behavior* of the citizens -- [3.0.md](../3.0.md): the north star this model serves -- [component-map.md](component-map.md): the component routing and trim/defer posture that keeps loop work from sprawling -- [ADR-0009](../adr/ADR-0009-daemon-deletion-in-session-only.md): why AgentOps ships only the in-session driver -- [operating-loop.md](operating-loop.md): the seven moves inside one tick -- [ports-and-adapters.md](ports-and-adapters.md): the runtime seams the loop runs through -- [`skills/domain/references/`](../../skills/domain/references/): the loop-family vocabulary -- [`.agents/discovery/2026-05-24-canonical-loops-ddd.md`](https://github.com/boshu2/agentops/blob/main/.agents/discovery/2026-05-24-canonical-loops-ddd.md): the derivation -- [`.agents/research/2026-05-24-fractal-orchestration-lineage.md`](https://github.com/boshu2/agentops/blob/main/.agents/research/2026-05-24-fractal-orchestration-lineage.md): the lineage diff --git a/docs/architecture/codex-hookless-lifecycle.md b/docs/architecture/codex-hookless-lifecycle.md deleted file mode 100644 index 55b43a1d1..000000000 --- a/docs/architecture/codex-hookless-lifecycle.md +++ /dev/null @@ -1,123 +0,0 @@ -# Codex Hookless Lifecycle - -> **Note:** Codex supports native hooks, but AgentOps installs hookless by default. Native hooks are an optional `scripts/install-codex-plugin.sh --with-hooks` / `scripts/install-codex.sh --with-hooks` profile. The hookless lifecycle remains the first-value path because packets, explicit gates, and closeout artifacts are portable across runtimes. - -AgentOps originally assumed a hook-capable runtime lifecycle such as Claude/OpenCode `session-start`, `session-end`, and `stop`. Codex CLI (0.115.0+) *does* expose native hooks — AgentOps wires the same hook scripts via `hooks/codex-hooks.json` under the optional `--with-hooks` profile — but installs **hookless by default** for portability, and the Codex **Desktop** app exposes no `~/.codex` hook surface at all. So the Codex runtime needs an explicit lifecycle path that keeps the flywheel working whether or not native hooks are present, rather than assuming them. - -## Why This Exists - -- Hook-capable runtimes can wire startup recall, transcript forging, and close-loop maintenance into runtime events. -- Codex can capture and retrieve knowledge, but those lifecycle steps do not run automatically unless AgentOps exposes an explicit command path. -- The fallback keeps the same flywheel stages while making the lifecycle honest, testable, and stateful. - -## Runtime Modes - -| Mode | Detection | Start path | Closeout path | Guarantees | -|------|-----------|------------|---------------|------------| -| `hook-capable` | Claude/OpenCode, or Codex CLI 0.115.0+ installed with `--with-hooks`, 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 with no native-hook profile installed (Desktop app, or CLI installed hookless) | `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 - -### `ao codex start` - -- Detect Codex runtime and session identity. -- Inspect repo-local `.agents/` state. -- Run safe close-loop maintenance unless `--no-maintenance` is set. -- Surface relevant learnings, patterns, findings, recent sessions, research, and next work. -- Sync `MEMORY.md` and write `.agents/ao/codex/startup-context.md`. -- Record `retrieved` citations for surfaced artifacts. -- Persist lifecycle state to `.agents/ao/codex/state.json`. - -### `ao codex ensure-start` - -- Run the Codex startup path once per thread. -- Skip duplicate startup automatically when the current Codex thread already has a recorded startup context. -- Give entry skills one reusable startup primitive instead of teaching each skill to parse lifecycle state directly. - -### `ao codex stop` - -- Resolve the best available Codex transcript. -- Prefer an archived transcript under `~/.codex/archived_sessions/`. -- 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` - -- Run the Codex closeout path once per thread. -- Return an explicit no-op result when the same Codex thread was already closed out. -- Give closeout-owner skills one reusable closeout primitive instead of teaching each skill to parse lifecycle state directly. - -### `ao codex status` - -- Report the active runtime mode. -- Show capture, retrieval, promotion, and citation health. -- Surface pending and quarantined knowledge counts. -- Reflect the last explicit start/stop lifecycle events. - -## Transcript Discovery Order - -`ao codex stop` uses this order: - -1. Explicit `--transcript` -2. Archived Codex transcript matching the current or requested session ID -3. Synthesized transcript from `~/.codex/history.jsonl` -4. Latest archived Codex transcript - -This keeps closeout reliable even when Codex has not archived the latest session yet. - -## Citation and Retrieval Model - -- `ao lookup` remains the primary automatic citation path for curated knowledge. -- `ao codex start` records `retrieved` citations for the startup artifacts it surfaces. -- `ao search --cite retrieved|reference|applied` provides an assisted path when a search result is actually adopted. -- `ao codex status` and the flywheel metrics read the same citation ledger, so hookless mode and hook-capable mode share the same accounting. - -## Promotion Hygiene - -The Codex fallback reuses the existing pool and close-loop hygiene instead of inventing a Codex-only promotion path. - -- Minimum structure checks reject malformed or underspecified pending artifacts. -- Truncation and low-signal artifacts can be quarantined under `.agents/knowledge/pending/.quarantine/`. -- Repo/source grounding still matters before promotion. -- Deduplication, contradiction checks, and supersession rules still run through the pool pipeline. -- Codex lifecycle commands surface the resulting health instead of silently promoting poor fragments. - -## Guarantees and Limits - -### Hook-capable mode guarantees - -- Startup and closeout can be automatic if hooks are installed and enabled. -- Skills may rely on the runtime lifecycle for context injection and maintenance. - -### Codex hookless mode guarantees - -- One obvious start command: `ao codex start` -- One obvious closeout command: `ao codex stop` -- One reusable skill-safe startup guard: `ao codex ensure-start` -- One reusable skill-safe closeout guard: `ao codex ensure-stop` -- Explicit health inspection: `ao codex status` -- No dependence on hidden hook infrastructure for recall, citation, or close-loop metrics - -### Non-guarantees - -- Codex does not gain runtime hooks by virtue of this fallback. -- Search citation is assisted rather than inferred from every result automatically; the user or skill must choose `--cite` when adoption is known. -- Poor extracted artifacts are not promoted automatically just because they were generated in Codex. - -## Verification - -Use these non-release checks to verify the Codex fallback from the current worktree: - -- `bash scripts/test-codex-hookless-lifecycle.sh` builds the local `ao` binary, seeds a temp Codex home and temp repos, then verifies `ao codex ensure-start`, retrieval/citation, `ao codex ensure-stop`, and `ao codex status`. (It no longer exercises the tracker-degraded no-beads `ao rpi phased` flow — the `ao rpi` engine was **removed** in 3.0; the seven-move operating loop, driven in-session by the `/rpi` skill, replaces it.) -- `bash scripts/test-codex-native-install.sh --skip-lint` verifies the checked-in Codex plugin bundle and public installer flow in a temp home without cutting a tag. - -## See Also - -- [Knowledge Flywheel](../knowledge-flywheel.md) -- [Session Lifecycle Workflow](../workflows/session-lifecycle.md) -- [Context Lifecycle Contract](../context-lifecycle.md) diff --git a/docs/architecture/failure-patterns.md b/docs/architecture/failure-patterns.md deleted file mode 100644 index 3675942c6..000000000 --- a/docs/architecture/failure-patterns.md +++ /dev/null @@ -1,394 +0,0 @@ -# The 12 Failure Patterns - -Reference guide for debugging and retrospectives. Based on [Vibe Coding](https://itrevolution.com/product/vibe-coding-book/) by Gene Kim & Steve Yegge. - ---- - -## Overview - -Failure patterns are recurring ways that AI-assisted development goes wrong. Recognizing them early prevents compounding mistakes and wasted time. - -| Category | Patterns | Timeframe | -|----------|----------|-----------| -| **Inner Loop** | Fix Spiral, Confident Hallucination, Context Amnesia, Tests Passing Lie | Seconds-Minutes | -| **Middle Loop** | Eldritch Horror, Silent Deletion, Zombie Resurrection, Gold Plating | Hours-Days | -| **Outer Loop** | Cargo Cult, Premature Abstraction, Security Theater, Documentation Mirage | Days-Weeks | - ---- - -## Inner Loop Patterns - -### 1. The Fix Spiral - -**Description:** The AI makes a fix that breaks something else, then fixes that break which causes another issue, creating a cascading chain of changes that circles back without resolving the original problem. Each iteration adds complexity without progress. - -**Symptoms:** -- More than 3 fix attempts without convergence -- Changes oscillating between two states -- Error messages changing but not disappearing -- Growing scope of modified files -- "This should work" appearing in explanations - -**Prevention:** -- Set a hard limit: 3 attempts then STOP -- Before each fix, state explicit prediction of outcome -- Use tracer tests to validate assumptions before fixing -- Keep changes minimal and atomic -- Checkpoint working state before each attempt - -**Recovery:** -1. Stop immediately - do not make another change -2. Revert to last known working state (git stash or git checkout) -3. Validate all assumptions about the problem -4. Create a minimal reproduction case -5. Fix root cause, not symptoms - ---- - -### 2. The Confident Hallucination - -**Description:** The AI generates plausible-sounding but factually incorrect information about APIs, libraries, syntax, or behavior. The confidence of the delivery makes it easy to trust without verification. - -**Symptoms:** -- Code references non-existent methods or parameters -- API usage that "looks right" but fails at runtime -- Documentation citations that don't match actual docs -- Version-specific features applied to wrong versions -- Overly specific technical claims without evidence - -**Prevention:** -- Verify all API calls against actual documentation -- Test code in isolation before integration -- Use "I don't know" as a valid response -- Check import statements actually resolve -- Run type checkers and linters early - -**Recovery:** -1. Do not trust any related code from the same generation -2. Read actual documentation for the API in question -3. Create minimal test to verify correct behavior -4. Replace hallucinated code with verified implementation -5. Note the hallucination source for future reference - ---- - -### 3. The Context Amnesia - -**Description:** As the context window fills, the AI loses track of earlier constraints, requirements, or decisions. Work from the beginning of a session gets overwritten by work from the end, causing regressions and contradictions. - -**Symptoms:** -- Reintroducing previously fixed bugs -- Contradicting earlier decisions without explanation -- Forgetting project-specific conventions -- Losing track of which files were modified -- Repeating work that was already completed - -**Prevention:** -- Keep context utilization below 40% -- Save progress frequently to files (progress.json, bundles) -- Use explicit checkpoints: "Completed X, moving to Y" -- Start fresh sessions for distinct work items -- Front-load critical constraints in prompts - -**Recovery:** -1. Stop and review what was accomplished -2. Save current state to persistent files -3. Start fresh context with saved state loaded -4. Explicitly re-state all constraints and requirements -5. Continue from checkpoint, not from memory - ---- - -### 4. The Tests Passing Lie - -**Description:** Tests pass but the code doesn't actually work. The tests may be too narrow, test the wrong thing, mock away the actual behavior, or the AI may have modified tests to pass rather than fixing the code. - -**Symptoms:** -- Green test suite but broken functionality -- Tests that test mocks instead of real behavior -- Coverage looks good but edge cases fail -- Tests modified in same PR as code they test -- "All tests pass" but users report bugs - -**Prevention:** -- Run tests yourself; don't trust reported results -- Separate test changes from code changes -- Use integration tests with real dependencies -- Check test assertions actually verify behavior -- Manual smoke test after test suite passes - -**Recovery:** -1. Run manual end-to-end verification -2. Identify which tests are actually validating behavior -3. Add missing tests that catch the real bug -4. Fix code without modifying test assertions -5. Review test coverage for gaps - ---- - -## Middle Loop Patterns - -### 5. The Eldritch Horror - -**Description:** AI-generated code becomes incomprehensible - functions spanning hundreds of lines, deeply nested logic, unclear naming, and tangled dependencies. The code works but no one can maintain or debug it. - -**Symptoms:** -- Functions exceeding 200 lines -- Nesting depth beyond 4 levels -- Variable names like `temp2`, `data3`, `result_final_v2` -- Circular dependencies between modules -- Comments that don't match code behavior - -**Prevention:** -- Enforce hard limits: <200 lines per function -- Review generated code before accepting -- Require meaningful names (nouns for data, verbs for functions) -- Break work into small, composable units -- Use explicit interfaces between components - -**Recovery:** -1. Do not add more code to the horror -2. Write characterization tests to capture current behavior -3. Extract functions methodically, one at a time -4. Rename variables to reflect actual purpose -5. Document the intended architecture - ---- - -### 6. The Silent Deletion - -**Description:** The AI removes code that appears unused but is actually necessary - handling edge cases, supporting legacy integrations, or providing fallback behavior. The deletion isn't noticed until production fails. - -**Symptoms:** -- "Cleanup" commits that remove "dead code" -- Features that worked yesterday now fail -- Error handling mysteriously missing -- Integration tests that used to exist are gone -- Comments about "why" deleted along with code - -**Prevention:** -- Never delete code without understanding why it exists -- Check git history before removal -- Trace all references including dynamic calls -- Get human approval for any deletion -- Keep deleted code in comments initially - -**Recovery:** -1. Use git to identify what was deleted and when -2. Restore deleted code with `git checkout` or cherry-pick -3. Add tests that would have caught the deletion -4. Document why the code exists (add comments) -5. Flag similar code as "do not delete" in review - ---- - -### 7. The Zombie Resurrection - -**Description:** Previously fixed bugs return because the AI regenerates similar code without the fix, or reverts changes during refactoring. The same issues keep coming back from the dead. - -**Symptoms:** -- Bug reports for issues marked "fixed" -- Same error appearing in different code paths -- Fixes getting lost during refactoring -- Pattern of re-introducing the same mistakes -- "I thought we fixed this" conversations - -**Prevention:** -- Add regression tests for every bug fix -- Document root cause, not just fix -- Use automated checks for known anti-patterns -- Review diffs for accidental reversions -- Keep a "lessons learned" file per project - -**Recovery:** -1. Verify the fix still exists in codebase -2. If reverted, restore from git history -3. Add regression test that fails without fix -4. Document in prominent location why fix is needed -5. Add CI check if pattern is automatable - ---- - -### 8. The Gold Plating - -**Description:** The AI adds unrequested features, extra error handling, additional configurability, or "improvements" beyond what was asked. The extra work introduces bugs, delays delivery, and increases maintenance burden. - -**Symptoms:** -- PR larger than expected for the task -- New config options no one asked for -- "While I was here, I also..." explanations -- Abstraction layers for single use cases -- Extra error handling for impossible states - -**Prevention:** -- Define explicit scope before starting -- Review against original requirements -- Reject changes outside stated scope -- Ask "was this requested?" for each addition -- Prefer boring, obvious solutions - -**Recovery:** -1. Identify what was actually requested -2. Revert unrequested additions -3. Keep only essential changes -4. If additions are valuable, make separate PR -5. Adjust prompts to emphasize minimalism - ---- - -## Outer Loop Patterns - -### 9. The Cargo Cult - -**Description:** The AI copies patterns from examples without understanding why they work. The copied code may be inappropriate for the context, outdated, or solving a different problem entirely. - -**Symptoms:** -- Copy-pasted code with irrelevant portions -- Patterns from different frameworks mixed together -- "Best practices" applied where they don't fit -- Configuration copied without understanding -- Stack Overflow answers used verbatim - -**Prevention:** -- Ask "why does this pattern exist?" for each adoption -- Verify example matches your context -- Test copied code in isolation first -- Adapt patterns to local conventions -- Trace examples to their source - -**Recovery:** -1. Identify which copied portions are actually needed -2. Remove cargo cult code that doesn't apply -3. Rewrite to match actual requirements -4. Document why remaining patterns are appropriate -5. Add tests that verify the pattern's purpose - ---- - -### 10. The Premature Abstraction - -**Description:** The AI creates generic abstractions before concrete use cases exist. The abstractions don't match actual needs, leading to awkward workarounds or complete rewrites when real requirements emerge. - -**Symptoms:** -- Generic interfaces with one implementation -- Factory patterns for single classes -- Configuration for cases that don't exist -- "Future-proofing" that never gets used -- Abstractions that make simple things complex - -**Prevention:** -- Require at least 3 concrete use cases before abstracting -- Write concrete implementations first -- Extract abstractions only when duplication appears -- Prefer duplication over wrong abstraction -- Ask "what problem does this abstraction solve?" - -**Recovery:** -1. Identify actual current use cases -2. Inline the abstraction into concrete usage -3. Remove unused generic capabilities -4. Wait for real patterns to emerge -5. Abstract only when duplication becomes painful - ---- - -### 11. The Security Theater - -**Description:** Code appears secure but isn't - validation that doesn't cover edge cases, encryption with hardcoded keys, authentication that can be bypassed, or sanitization that misses injection vectors. - -**Symptoms:** -- Security measures easily circumvented -- Validation on client but not server -- Hardcoded credentials or keys -- "Security by obscurity" approaches -- Logging sensitive data - -**Prevention:** -- Use established security libraries, not custom code -- Security review by qualified humans -- Penetration testing for critical paths -- Threat modeling before implementation -- Static analysis for common vulnerabilities - -**Recovery:** -1. Conduct proper security audit -2. Identify actual attack vectors -3. Replace theater with real protection -4. Use industry-standard approaches -5. Add security regression tests - ---- - -### 12. The Documentation Mirage - -**Description:** Documentation exists but doesn't match reality - outdated READMEs, incorrect API docs, comments that describe what code used to do, or auto-generated docs that miss critical details. - -**Symptoms:** -- Following docs leads to errors -- Comments contradict adjacent code -- Examples that don't compile -- API docs missing required parameters -- Setup instructions that don't work - -**Prevention:** -- Treat docs as code: test them -- Update docs in same PR as code changes -- Use executable documentation (doctests, notebooks) -- Review docs during code review -- Version docs with code - -**Recovery:** -1. Test documentation by following it literally -2. Identify all discrepancies -3. Update docs to match current behavior -4. Add CI checks for doc accuracy where possible -5. Remove documentation that can't be maintained - ---- - -## Quick Reference Card - -| # | Pattern | Key Symptom | First Action | -|---|---------|-------------|--------------| -| 1 | Fix Spiral | >3 attempts | STOP, revert | -| 2 | Confident Hallucination | Non-existent APIs | Verify docs | -| 3 | Context Amnesia | Forgotten constraints | Save state | -| 4 | Tests Passing Lie | Green but broken | Manual test | -| 5 | Eldritch Horror | >200 line functions | Extract/refactor | -| 6 | Silent Deletion | Missing code | Check git history | -| 7 | Zombie Resurrection | Bugs return | Add regression test | -| 8 | Gold Plating | Unrequested features | Revert extras | -| 9 | Cargo Cult | Copied patterns | Understand why | -| 10 | Premature Abstraction | Generic w/ one use | Inline | -| 11 | Security Theater | Bypassable security | Audit | -| 12 | Documentation Mirage | Docs don't work | Test docs | - ---- - -## Pattern Frequency Tracking - -Use this table in retrospectives to track which patterns hit most often: - -| Pattern | Occurrences | Total Hours Lost | Prevention Implemented | -|---------|-------------|------------------|----------------------| -| Fix Spiral | | | | -| Confident Hallucination | | | | -| Context Amnesia | | | | -| Tests Passing Lie | | | | -| Eldritch Horror | | | | -| Silent Deletion | | | | -| Zombie Resurrection | | | | -| Gold Plating | | | | -| Cargo Cult | | | | -| Premature Abstraction | | | | -| Security Theater | | | | -| Documentation Mirage | | | | - ---- - -## See Also - -- `/retro` - Session retrospective command -- `/vibe-validate` - Semantic code validation -- `/vibe-prescan` - Static pre-scan for patterns -- [Vibe Coding Book](https://itrevolution.com/product/vibe-coding-book/) diff --git a/docs/architecture/fungibility-charter.md b/docs/architecture/fungibility-charter.md deleted file mode 100644 index 7f4db541f..000000000 --- a/docs/architecture/fungibility-charter.md +++ /dev/null @@ -1,96 +0,0 @@ -# AgentOps 3.0 Fungibility Charter - -> Doctrinal commitment. The six promises AgentOps 3.0 makes about how agents relate to work, to each other, and to model choice. Companion to the [Operating Loop](operating-loop.md) (the discipline every agent runs) and [Ports and Adapters](ports-and-adapters.md) (the seams that make any runtime swappable). - -AgentOps' execution primitives — the `.agents/` corpus, beads, worktrees-per-bead, decay-ranked retrieval — are *fungibility-shaped*: any agent can read shared state, claim any unit of work, and contribute back. But for most of 2.x the operational rhetoric ("Claude discovers, Codex implements"; `/council --mixed`; the Codex-parity machinery) sold model **specialization** as the value. The primitives said one thing; the surfaces said another. - -The 3.0 charter resolves that tension in favor of the primitives: - -**Fungible by default, specialized when you opt in.** - -The primary pitch is "spawn N identical agents, watch them work." Model diversity is a real feature for the users who want it — but it is a deliberate opt-in, never the assumed shape of a swarm. - -## Why a charter - -Without an explicit charter, the fungibility philosophy stays implicit in the primitives and is contradicted by the surfaces. A new operator reading the README would conclude that AgentOps *requires* a model mix to function, when in fact a single-model swarm is the supported default. A charter makes the doctrinal choice legible, gives the CI gates something concrete to enforce, and gives the 3.0 product docs a frame to write toward. - -This is a doctrine document, not a primitive. Each commitment below names the primitive that *implements* it — the charter is honest only because those primitives already ship. Doctrine that promises behavior the code cannot deliver is the failure mode this document exists to avoid. - -## The six commitments - -### 1. Default RPI mode is single-model - -Whatever model the current session runs is the model that runs every RPI phase — Research, Plan, Implement, Validate. There is no built-in handoff to a different model between phases. - -A phase boundary is a *context* boundary (bounded packets and summaries cross it, per the Operating Loop's "context crosses boundaries as artifacts" principle), not a *model* boundary. Crossing models mid-loop is a thing you can choose to do, not a thing the loop does on your behalf. - -*Implemented by `/rpi` runtime detection: the active session's model is detected and reused across phases; the prior multi-model default is demoted to an opt-in flag.* - -### 2. Any agent can claim any bead — no role gating - -There are no "frontend agents," "testing agents," or "review agents." There is one pool of interchangeable generalists, and any agent can claim any ready bead from it. - -The bead claim model already enforces this mechanically: `br ready` surfaces unblocked work to every agent equally, and `br update --claim` is an atomic first-come claim with no role predicate. The charter makes explicit what the tracker already does — claiming is by availability, never by assigned specialty. - -*Implemented by `br`'s role-free claim model (`br ready` / `br update --claim`), which the [Operating Loop](operating-loop.md) move 2 ("track as a bead when it leaves the head") already assumes.* - -### 3. Stateless agent assumption - -Agents are interchangeable consumers of the `.agents/` corpus. **No agent owns a domain. No agent carries irreplaceable session state.** - -Everything an agent needs to do the next unit of work is reconstructable from shared, durable surfaces: the bead (linked intent, acceptance examples, accumulating evidence), the corpus (decay-ranked prior context, retrieved on demand), and the worktree (the change in flight). An agent's in-context memory is a cache, not a system of record. When the cache is lost, another agent rebuilds it from the same surfaces and continues. - -This is what makes commitments 1, 2, 5, and 6 coherent: fungibility is only real if losing any single agent loses no irreplaceable state. - -*Implemented by the corpus-as-source-of-truth contract: `ao inject` / `ao corpus inject --query` reconstruct orientation from durable state; worktree-per-bead isolates in-flight change to a recoverable surface (the multi-agent discipline codified in `AGENTS.md`).* - -### 4. Universal init prompt is standard - -Every spawned agent runs the same orientation step — `ao session bootstrap` — regardless of which model it is. Same starting frame, same standard orientation report, same on-ramp into the corpus. - -Identical orientation is a precondition for interchangeability: two agents given the same bead and the same starting frame should behave equivalently against the acceptance examples, whatever model backs them. A bespoke per-model or per-role init prompt would reintroduce specialization through the back door. - -*Implemented by `ao session bootstrap` (the universal init prompt) and the `session-bootstrap` skill, which AgentOps 3.0's hookless startup makes the explicit replacement for the old SessionStart context injection.* - -### 5. Death is normal; recovery is automatic - -An agent dying mid-work — context compaction, crash, rate limit, a closed tab — is an expected event, not an incident. Recovery is an operational primitive, not a heroic human intervention. - -When an agent dies, its bead stays `in_progress` under a dead claim. In a 30-agent overnight swarm this happens constantly; without recovery the swarm degrades over hours as live agents skip "taken" work that no one is doing. The recovery primitive surfaces these stale claims with evidence (last touch, claim age, last evidence event) and atomically transfers them to a live agent. **Dead agent? Start another. No role replacement needed** — because there were no roles to replace, and the corpus + bead carry the state forward. - -*Implemented by `ao beads stale-claims` (detect stale `in_progress` claims with staleness evidence) and `ao beads resume ` (atomic claim transfer crediting both agents), composing with the session-bootstrap heartbeat from commitment 4.* - -### 6. Specialization is opt-in - -Model diversity is a feature for the users who want it. Flags such as `--mixed`, `--pool=N`, `--diverse`, and `--codex` exist precisely so that a team can choose model diversity *as a deliberate strategy* — cross-validation, harness-specific strengths, redundancy. - -They are never the default. Reaching for one is an explicit decision the operator makes and owns, surfaced as an advanced option, not the assumed shape of a swarm. The 2.x framings that sold the mix as the primary pitch move to advanced sections; the front door is single-model fungibility. - -*Implemented by the opt-in diversity flags on `/rpi` and `/council` (`--mixed`, `--pool=N`, `--diverse`, `--codex`), each off by default.* - -## How the commitments compose - -The six are not independent — they form one closed loop that lets a swarm run unattended: - -```text -universal init (4) → any agent claims any bead (2) → single-model RPI (1) - ↑ │ - │ ▼ -specialization stays opt-in (6) ← stateless agents (3) ← death → auto-recovery (5) -``` - -Stateless agents (3) make death survivable; automatic recovery (5) makes death routine; a universal init (4) makes any replacement equivalent; role-free claiming (2) lets the replacement pick up any work; single-model default (1) keeps the swarm homogeneous unless the operator opts into diversity (6). Remove any one and the unattended-swarm property breaks: specialized agents (¬2) create bottlenecks; stateful agents (¬3) make death lossy; bespoke init (¬4) makes replacements non-equivalent. - -## Boundaries - -Fungibility is the right default for **software development**, where output matters more than discourse and the bottleneck is throughput against a large pool of independent work. It is the *wrong* default where role separation is the mechanism rather than overhead — adversarial debate where distinct roles produce the value, or strict compliance review where separation of duties is a hard requirement. Those are deliberate specialization choices under commitment 6, not violations of the charter. - -The charter governs *agent fungibility*, not *runtime fungibility* — the latter (any harness behind the same ports) is the [Ports and Adapters](ports-and-adapters.md) concern. The two reinforce each other: swappable runtimes make single-model defaults cheap to honor, because the runtime is not load-bearing for correctness. - -## Related - -- [Operating Loop](operating-loop.md) — the discipline every fungible agent executes; commitments 1–3 are doctrinal reads of its moves. -- [Ports and Adapters](ports-and-adapters.md) — runtime swappability, the structural complement to agent fungibility. -- An NTM tmux swarm (`ao mcp serve` + `ao agent`) — the out-of-session industrial version of an unattended fungible swarm. -- Skill: `agent-fungibility-philosophy` — the operating playbook (spawn, init, recover) the charter codifies into doctrine. -- Skill: `session-bootstrap` — the universal init prompt named in commitment 4. diff --git a/docs/assurance-profile.md b/docs/assurance-profile.md index 2c10f6320..8d1031d6a 100644 --- a/docs/assurance-profile.md +++ b/docs/assurance-profile.md @@ -1,170 +1,31 @@ # AgentOps Assurance Profile - -AgentOps brings high-assurance operating discipline to AI-agent-paced software work. The lane is not "move slow because the environment is serious." The lane is: keep the rigor, shorten the cycle time, and make every agent run leave evidence a serious operator can review. +AgentOps provides an evidence-producing workflow for AI-agent-created work. It +is an engineering aid, not a certification, approval, release authority, data +boundary, or guarantee of correctness. -This is an engineering posture, not a certification claim. AgentOps does not make a repo accredited, classified-network approved, safety-critical, export-controlled, FedRAMP-authorized, or airworthiness-ready by itself. It gives teams a local-first operating layer for agent work that can fit into those programs when the operator supplies the required controls, approvals, boundaries, and accreditation process. +## Proven floor -## Claim +- behavior-first intent with explicit scope and evidence requirements; +- one bounded implementation experiment; +- deterministic content identity through `subject-manifest.v1`; +- one independent judgment from a distinct fresh context; +- a durable `PASS | FAIL | NOT_PROVEN` verdict with checked and unchecked scope. - -AgentOps is a software factory for coding agents with four compounding layers: +The freshness attestation is a declared trust fact, not cryptographic proof of +model isolation. A consumer may require stronger runtime controls, multiple +judges, or formal review for high-consequence work. -| Layer | Assurance role | -|-------|----------------| -| **Bookkeeping** | Every run leaves file-backed evidence: attempts, decisions, citations, verdicts, handoffs, findings, retros, and post-mortems. | -| **Context Compiler** | Agents receive scoped context for the phase they are in, instead of an unbounded chat history. | -| **Validation Gates** | Plans and code are challenged before promotion by pre-mortems, councils, vibe reviews, tests, hooks, and local quality gates. | -| **Knowledge Flywheel** | Lessons are extracted, scored, promoted, decayed, and reloaded so the system compounds instead of repeating failures. | +## Consumer responsibility - -The goal is aerospace/IC-style operational discipline at AI-agent pace: evidence before belief, boundaries before autonomy, and promotion only after gates pass. +The consumer owns model/provider approval, data handling, retention, secrets, +Git and CI policy, tracker state, human approvals, release, rollback, and any +formal assurance mapping. AgentOps packets and verdicts may be inputs to those +systems; they do not replace them. -## What Rigor Means Here +## Data handling -AgentOps uses "rigor" in the operator sense: - -- **Traceability.** Work produces inspectable artifacts, not only chat transcripts or final diffs. -- **Separation of duties.** Planning, implementation, and validation can run with different context and different agents. -- **Least-context execution.** Workers get the context needed for their phase, not the entire accumulated conversation. -- **Independent judgment.** Councils and validators review evidence packets instead of inheriting the implementer's mental state. -- **Policy as gates.** Hooks, pre-push checks, goal gates, security scans, and validation skills block promotion rather than merely advising. -- **Desired-state discipline.** `PRODUCT.md` and `GOALS.md` can run ahead of the repo as explicit setpoints; measurements and reconcile loops show what is true now and what must close next. -- **Human authority boundaries.** The operator chooses when the system is interactive, supervised, scheduled, or unattended. -- **Local-first evidence.** AgentOps writes local files that can be inspected, archived, redacted, excluded from source control, or exported into a program's evidence system. - -## Boundary Model - -AgentOps controls the operating layer around coding agents. It does not control every dependency in the environment. - -| Boundary | AgentOps posture | Operator responsibility | -|----------|------------------|-------------------------| -| AgentOps state | Repo-local `.agents/` artifacts, git-ignored by policy at repo root | Decide retention, backup, redaction, export, and whether any artifacts may enter source control | -| Model runtime | Runtime-neutral across Claude Code, Codex CLI, Cursor, and OpenCode | Approve model/provider use, network path, prompt/data handling, and classification boundary | -| Git and CI | Integrates with local git, hooks, tests, and release gates | Control remotes, branch policy, protected environments, and CI secrets | -| Install/update path | Public installers fetch from GitHub unless the operator vendors or mirrors them | Mirror, pin, review, or rebuild artifacts for disconnected or controlled networks | -| Secrets and data | AgentOps can help constrain context, but does not classify data or provide a DLP boundary | Enforce secret handling, data classification, redaction, and egress controls | -| Accreditation | Provides evidence artifacts and operating discipline | Map artifacts to the local control framework and obtain required approvals | - - -The correct high-assurance reading is: **no AgentOps-managed telemetry or hosted control plane; operator-selected dependencies remain operator-selected dependencies.** - -## Operating Profiles - -### Profile 0: Exploration - -Use AgentOps as a disciplined agent workflow layer. The operator accepts normal model-provider and local-machine risk. Good for personal repos, experiments, and early product discovery. - -Expected posture: - -- `/quickstart`, `/research`, `/implement`, `/vibe` -- Local `.agents/` state -- Human review before merge - -### Profile 1: Team Software Factory - - -Use AgentOps as the team operating model for coding agents. Work is issue-tracked, validated, and closed with evidence before merge. - -Expected posture: - -- Beads or equivalent issue tracking -- RPI flow for nontrivial work -- Pre-mortem before implementation -- Vibe/council before promotion -- Pre-push gate before branch publication -- Post-mortem or retro after significant work - -### Profile 2: Constrained or High-Rigor Engineering - -Use AgentOps where process evidence, local control, and human authority boundaries matter. This is the primary target posture for platform, infrastructure, autonomy, defense-adjacent, regulated, and high-consequence software teams. - -Expected posture: - -- Approved model runtimes only -- Network path and installer path reviewed by the operator -- `.agents/` retention and redaction policy defined before use -- Humans in the loop for planning, validation, release, and promotion -- Councils use sealed evidence packets where possible -- No unattended source mutation unless bounded by explicit goals, gates, and rollback policy -- Local gates run before any remote push or release artifact - -### Profile 3: Accredited or Safety-Critical Program - -Use AgentOps only as an input to the program's approved engineering process. AgentOps can generate evidence and enforce local workflow discipline, but the program authority owns control mapping, model approval, data handling, tool qualification, and release authorization. - -Expected posture: - -- All Profile 2 expectations -- Program-owned control mapping -- Approved artifact retention and export workflow -- Approved model/provider boundary -- Supply-chain review for installers, binaries, dependencies, and generated artifacts -- Explicit human signoff for any artifact that crosses a program boundary - -## Evidence Artifacts - -AgentOps is valuable in rigorous environments because it changes agent work from "trust the chat" to "inspect the run." - -| Evidence | Where it comes from | Why it matters | -|----------|---------------------|----------------| -| Run packets | RPI/discovery/crank/validation flows | Preserve goal, scope, plan, execution, and validation context | -| Council verdicts | `/council`, `/pre-mortem`, `/vibe` | Record independent PASS/WARN/FAIL judgment and rationale | -| Citations | `ao metrics cite`, lookup/search/inject flows | Show which knowledge influenced a run | -| Handoffs | `/handoff`, `/recover`, session closeout | Preserve continuity across agents and sessions | -| Retros and post-mortems | `/retro`, `/post-mortem`, `/curate --mode=forge` | Turn completed work into reusable lessons | -| Ratchet records | `/ratchet`, validation gates | Capture forward-progress checks and failure prevention | -| Goal measurements | `GOALS.md`, `ao goals measure`, `/evolve` | Tie autonomous work to measurable fitness criteria | -| Local gates | pre-push, tests, security scans, docs gates | Make promotion conditional on executable checks | - -These artifacts are not a substitute for formal compliance evidence. They are raw material a program can review, retain, redact, or map into its own evidence system. - -## Data Handling - -AgentOps assumes `.agents/` may contain sensitive session context. Repo-root `.agents/` is local runtime state and should not be tracked by default. - -Recommended operator rules: - -- Treat `.agents/` as potentially sensitive. -- Decide retention before using scheduled or unattended loops. -- Redact or exclude artifacts that contain secrets, customer data, classified data, export-controlled data, or proprietary context. -- Mirror installers and dependencies before use in disconnected or controlled networks. -- Keep model-provider use inside the organization's approved data boundary. -- Export only reviewed evidence artifacts into long-lived records. - -## Autonomy Model - -AgentOps does not require maximal autonomy. It is built for variable autonomy: - -| Mode | Human role | Suitable use | -|------|------------|--------------| -| **In the loop** | Human approves each meaningful phase | High-risk planning, validation, release, constrained environments | -| **On the loop** | Human supervises scheduled or bounded loops | Dream, compile, forge, feedback-drain, low-risk maintenance | -| **Off the loop** | System runs unattended inside strict bounds | Mature low-risk jobs with explicit gates, rollback, and artifact review | - -For high-rigor work, the default should be in the loop for discovery, validation, release, and promotion; on the loop for scheduled compounding; and off the loop only after the operator has proved the bounds. - -## Out Of Scope - -AgentOps does not provide: - -- Formal certification, accreditation, authorization to operate, or airworthiness approval. -- Data classification, declassification, or cross-domain transfer. -- Model approval, model monitoring, or model safety certification. -- Secret scanning as a complete DLP boundary. -- A guarantee of zero network egress when the operator uses external model runtimes, remotes, installers, or tools. -- Formal verification of generated code. -- Tool qualification for safety-critical release by itself. - -## Roadmap - -The current profile defines the posture. The next hardening steps are: - -- Control-mapping templates for common internal assurance programs. -- Evidence export bundles for councils, RPI runs, gates, and post-mortems. -- Redaction workflows for `.agents/` artifacts. -- Stronger policy around approved model/runtime profiles. -- Retention profiles for personal, team, constrained, and accredited environments. -- More explicit supply-chain guidance for mirrored installers and pinned artifacts. - -The product direction is clear: keep agent work fast, but make the speed legible to serious operators. +Treat plans, candidates, evidence, and verdicts as potentially sensitive. +Choose their storage, retention, redaction, and export policy before using them +in a controlled environment. The default verdict directory is local to the +workspace and can be overridden by the caller. diff --git a/docs/cli-skills-map.md b/docs/cli-skills-map.md deleted file mode 100644 index d8d33aeaf..000000000 --- a/docs/cli-skills-map.md +++ /dev/null @@ -1,192 +0,0 @@ -# CLI ↔ Skills Wiring Map - -> **HISTORICAL SNAPSHOT (2026-04)** — predates the 2026-06/07 skill consolidations; names herein (vibe, retro, autodev, flywheel, curate, recover, …) may be retired. Regenerate before trusting. - -> Which `ao` commands are called by which skills — and vice versa. - -Auto-audited 2026-04-24; targeted runtime-proof update 2026-04-28. 32 generated CLI command headings, 69 source skills. (AgentOps 3.0 is hookless — there is no runtime hook surface; lifecycle work is driven by skills + the `ao` CLI, with the installed local cockpit pre-push gate as routine authority and CI as tag/PR/manual backstop telemetry.) - -Registry-first note: `/plan`, `/pre-mortem`, `/research`, `/vibe`, and `/post-mortem` now also read or write `.agents/findings/registry.jsonl` directly via skill contract. Those file-native prevention reads are intentionally not counted as `ao` command invocations in the tables below. - -## Summary - -| Category | Count | -|----------|-------| -| Generated CLI command headings | 59 | -| CLI command entries with skill callers | 32 | -| Orphan/hidden command entries (user utilities, hidden, CI-only) | 21 | -| Known phantom subcommands | 0 | - ---- - -## CLI Commands → Callers - -Every `ao` command that is actively called by at least one skill. - -| Command | Skill Callers | -|---------|--------------| -| `ao inject` | crank, evolve, implement, inject, recover, research, retro | -| `ao forge` | flywheel, forge, post-mortem, retro, vibe, evolve, crank | -| `ao ratchet` | crank, handoff, implement, plan, pre-mortem, ratchet, rpi, status, vibe | -| `ao goals` | goals, evolve | -| `ao search` | crank, inject, plan, pre-mortem, provenance, research, using-agentops, vibe | -| `ao rpi` — **removed in 3.0** (f61c5f0e7); the RPI engine is gone. The loop is now the seven-move operating loop driven in-session by the `/rpi` skill, with out-of-session runs on the NTM + MCP Agent Mail substrate (AgentOps ships no daemon) | autodev, council, crank, plan, quickstart, research, rpi, shared, swarm | -| `ao autodev` | autodev | -| `ao flywheel` | crank, evolve, flywheel, post-mortem, quickstart, retro, status | -| `ao pool` | crank, status | -| `ao lookup` | crank, implement, inject, plan, pre-mortem, research, using-agentops | -| `ao context` | crank, implement, swarm | -| `ao codex` | autodev, brainstorm, crank, discovery, handoff, implement, quickstart, recover, research, rpi, status, using-agentops, validation | -| `ao compile` | compile | -| `ao maturity` | flywheel | -| `ao constraint` | flywheel, post-mortem, retro | -| `ao badge` | flywheel, status | -| `ao eval bench` | flywheel | -| `ao seed` | quickstart | -| `ao notebook` | retro | -| `ao dedup` | flywheel | -| `ao contradict` | flywheel | -| `ao metrics` | flywheel | -| `ao init` | quickstart | -| `ao session` | post-mortem, retro | -| `ao temper` | post-mortem | -| `ao curate` | flywheel | -| `ao status` | flywheel, quickstart | -| `ao task-feedback` | retro | -| `ao task-status` | status | -| `ao anti-patterns` | flywheel | - ---- - -## Skills → Commands - -Which `ao` commands each skill invokes. - -| Skill | ao Commands Used | -|-------|-----------------| -| **autodev** | `autodev init`, `autodev validate`, `codex ensure-start`, `evolve`, `rpi` | -| **brainstorm** | `codex ensure-start` | -| **compile** | `compile` | -| **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`, `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** | `lookup`, `ratchet record`, `rpi cleanup`, `rpi status`, `search` | -| **post-mortem** | `constraint activate`, `flywheel close-loop`, `forge`, `forge markdown`, `forge transcript`, `session close`, `temper validate` | -| **pre-mortem** | `lookup`, `ratchet record`, `search` | -| **provenance** | `search` | -| **quickstart** | `codex ensure-start`, `codex ensure-stop`, `codex status`, `flywheel status`, `hooks install`, `hooks test`, `init`, `quick-start`, `quickstart`, `rpi phased`, `seed`, `status` | -| **ratchet** | `ratchet check`, `ratchet record`, `ratchet skip`, `ratchet status` | -| **recover** | `codex ensure-start`, `codex status`, `lookup` | -| **research** | `codex ensure-start`, `inject`, `lookup`, `rpi phased`, `search` | -| **retro** | `constraint activate`, `constraint review`, `flywheel close-loop`, `forge`, `forge markdown`, `notebook update`, `session close`, `task-feedback` | -| **rpi** | `codex ensure-start`, `ratchet record`, `rpi cancel`, `rpi cleanup` | -| **status** | `badge`, `codex ensure-start`, `flywheel status`, `pool list`, `pool promote`, `pool stage`, `ratchet status`, `task-status` | -| **swarm** | `context assemble`, `rpi phased` | -| **using-agentops** | `codex ensure-start`, `codex ensure-stop`, `codex status`, `lookup`, `search` | -| **validation** | `codex ensure-stop`, `forge transcript` | -| **vibe** | `forge markdown`, `ratchet record`, `search` | -| council | `rpi phased` | -| shared | `rpi phased` | - -Skills with **no ao commands**: beads, brainstorm, bug-hunt, codex-team, complexity, converter, doc, heal-skill, llm-wiki, openai-docs, pr-implement, pr-prep, pr-research, pr-validate, product, release, reverse-engineer-rpi, security, security-suite, standards, trace. - -Conceptual slash commands such as `/knowledge` are documented elsewhere in the product docs, but they are not counted as source skill directories in this map. - -## Repo-Native Prevention Surfaces - -These are active skill-level reads or writes that do not go through an `ao` subcommand: - -- `/plan` reads `.agents/findings/registry.jsonl` before decomposition and cites `Applied findings:` -- `/research` persists reusable findings to `.agents/findings/registry.jsonl` -- `/pre-mortem` reads `.agents/findings/registry.jsonl` in both quick and deep modes, injects `known_risks`, and can persist reusable findings -- `/vibe` reads `.agents/findings/registry.jsonl` before council review and can persist reusable findings -- `/post-mortem` writes normalized reusable findings to `.agents/findings/registry.jsonl` - ---- - -## Orphan Commands - -Commands that exist in the Go CLI but are not called by any skill. All are intentionally uncalled — user utilities, hidden internals, or CI-only. (`ao session memory` and `ao extract` were previously invoked only by runtime hooks, which AgentOps 3.0 no longer ships; they remain available as direct commands.) - -| Command | Category | Notes | -|---------|----------|-------| -| `ao completion` | User utility | Shell completion generation | -| `ao config` | User utility | Config management | -| `ao demo` | User utility | Council-first AgentOps 3.0 demonstration | -| `ao doctor` | CI/install | Called by install.sh and release-smoke-test.sh | -| `ao eval` | CI/test | Public AgentOps canary suites and baseline comparisons | -| `ao version` | User utility | Version query | -| `ao quick-start` / `ao quickstart` | User utility | Golden path for repo seed; `/quickstart` routes users to the next action | -| `ao vibe-check` | User utility | `/vibe` skill orchestrates directly | -| `ao trace` | User utility | Artifact tracing | -| `ao gate` | CI/test | Promotion gate — called in test scripts | -| `ao session memory` | Internal | Memory sync (previously a hook caller) | -| `ao extract` | Internal | Learning extraction (previously a hook caller) | -| `ao feedback` | Hidden | UI for providing feedback on learnings | -| `ao feedback-loop` | Internal | Async feedback processing | -| `ao batch-feedback` | Hidden | Batch feedback processing | -| `ao eval session-outcome` | Hidden | Session outcome recording | -| `ao store` | Hidden | Vector store management | -| `ao index` | Hidden | Indexing utility | -| `ao task-sync` | Hidden | Task synchronization | -| `ao migrate` | Hidden | Migration utility (`migrate memrl`) | -| `ao worktree` | Hidden | Worktree GC utility | - ---- - -## Phantom Subcommands - -No known phantom subcommands are present in the current map. `scripts/validate-cli-skills-map.sh` fails if removed paths or the known stale gate/forge calls reappear. - ---- - -## Session Lifecycle Flow (hookless) - -AgentOps 3.0 ships no runtime hooks. Session-boundary maintenance is driven explicitly by skills calling `ao` commands. For Codex, the entry/closeout skills replace what runtime hooks used to do: - -``` -Codex Thread Entry - → entry skill runs ao codex ensure-start - → first call performs ao codex start semantics once per thread - → later calls no-op for the same thread - → ao flywheel close-loop (safe maintenance) - → ao lookup citation writes for surfaced artifacts - -During Session - → ao lookup - → appends citations to .agents/ao/citations.jsonl - → ao search --cite - → appends citations to .agents/ao/citations.jsonl when search results are adopted - -Post-Mortem Closeout - → post-mortem closeout runs ao session close --auto-extract - → resolves the current transcript, forges the session, extracts learnings - → fallback when transcript discovery must be explicit: ao forge transcript --queue - → ao flywheel close-loop --quiet - → ingests pending knowledge and runs close-loop maintenance -``` - -If you want a runtime hook of your own (e.g. a PostToolUse gate), author one with the `hooks-authoring` skill — AgentOps ships none by default. - ---- - -## Regenerating - -When skills or command usage changes, refresh this map as follows: - -1. Re-scan source invocations in: `skills/*/SKILL.md`, `skills-codex/*/SKILL.md`. -2. Update the relevant rows in this document, keeping hidden/subcommands aligned with the live command tree (`ao anti-patterns`, `ao context assemble`, etc.). -3. Update the audit header date above. - -## Maintaining This Document - -Re-audit when: -- Adding a new `ao` CLI command (check it has skill callers or is intentionally orphaned) -- Adding a new skill that calls `ao` commands (verify the commands exist) -- Running `scripts/generate-cli-reference.sh` (companion to this doc) diff --git a/docs/cli-surface.json b/docs/cli-surface.json index e2c46ab99..9a78bd454 100644 --- a/docs/cli-surface.json +++ b/docs/cli-surface.json @@ -164,17 +164,24 @@ }, { "category": "public-tested", - "command": "gate check", + "command": "flywheel compare", "coverage_status": "covered", "kind": "leaf", "reason": "Covered by release smoke tests, direct command tests, or command handler tests." }, { - "category": "public-stateful-fixture-needed", - "command": "goals add", - "coverage_status": "allowlisted", + "category": "public-tested", + "command": "flywheel status", + "coverage_status": "covered", "kind": "leaf", - "reason": "Writes GOALS.md and needs an isolated fixture." + "reason": "Covered by release smoke tests, direct command tests, or command handler tests." + }, + { + "category": "public-tested", + "command": "gate check", + "coverage_status": "covered", + "kind": "leaf", + "reason": "Covered by release smoke tests, direct command tests, or command handler tests." }, { "category": "public-stateful-fixture-needed", @@ -197,13 +204,6 @@ "kind": "leaf", "reason": "Historical view shares goals validation behavior." }, - { - "category": "public-stateful-fixture-needed", - "command": "goals init", - "coverage_status": "allowlisted", - "kind": "leaf", - "reason": "Initializes GOALS.md and may prompt." - }, { "category": "public-tested", "command": "goals measure", @@ -218,20 +218,6 @@ "kind": "leaf", "reason": "Covered through goals measurement behavior." }, - { - "category": "public-stateful-fixture-needed", - "command": "goals migrate", - "coverage_status": "allowlisted", - "kind": "leaf", - "reason": "Migrates GOALS.md and needs a disposable fixture." - }, - { - "category": "public-stateful-fixture-needed", - "command": "goals prune", - "coverage_status": "allowlisted", - "kind": "leaf", - "reason": "Removes goal entries and needs a disposable fixture." - }, { "category": "public-tested", "command": "goals render", diff --git a/docs/cli-surface.md b/docs/cli-surface.md index 78aa5862f..a37511cb2 100644 --- a/docs/cli-surface.md +++ b/docs/cli-surface.md @@ -27,16 +27,14 @@ | `ao doctor robot-docs` | `public-stateful-fixture-needed` | `allowlisted` | Generates docs from local installation state. | | `ao doctor undo` | `public-stateful-fixture-needed` | `allowlisted` | Reverts a doctor repair fixture. | | `ao done` | `deprecated` | `allowlisted` | One-release failure stub for removed closure control. | +| `ao flywheel compare` | `public-tested` | `covered` | Covered by release smoke tests, direct command tests, or command handler tests. | +| `ao flywheel status` | `public-tested` | `covered` | Covered by release smoke tests, direct command tests, or command handler tests. | | `ao gate check` | `public-tested` | `covered` | Covered by release smoke tests, direct command tests, or command handler tests. | -| `ao goals add` | `public-stateful-fixture-needed` | `allowlisted` | Writes GOALS.md and needs an isolated fixture. | | `ao goals drift` | `public-stateful-fixture-needed` | `allowlisted` | Requires two goal snapshots. | | `ao goals export` | `public-tested` | `allowlisted` | Covered through goals snapshot behavior. | | `ao goals history` | `public-tested` | `allowlisted` | Historical view shares goals validation behavior. | -| `ao goals init` | `public-stateful-fixture-needed` | `allowlisted` | Initializes GOALS.md and may prompt. | | `ao goals measure` | `public-tested` | `covered` | Covered by release smoke tests, direct command tests, or command handler tests. | | `ao goals meta` | `public-tested` | `allowlisted` | Covered through goals measurement behavior. | -| `ao goals migrate` | `public-stateful-fixture-needed` | `allowlisted` | Migrates GOALS.md and needs a disposable fixture. | -| `ao goals prune` | `public-stateful-fixture-needed` | `allowlisted` | Removes goal entries and needs a disposable fixture. | | `ao goals render` | `public-tested` | `covered` | Covered by release smoke tests, direct command tests, or command handler tests. | | `ao goals scenarios` | `public-tested` | `covered` | Covered by release smoke tests, direct command tests, or command handler tests. | | `ao goals trace` | `public-tested` | `covered` | Covered by release smoke tests, direct command tests, or command handler tests. | diff --git a/docs/comparisons/README.md b/docs/comparisons/README.md deleted file mode 100644 index bc6001f00..000000000 --- a/docs/comparisons/README.md +++ /dev/null @@ -1,178 +0,0 @@ ---- -title: "AgentOps vs The Competition — AI Coding Agent Comparisons" -description: "How AgentOps compares to GSD, Compound Engineer, Superpowers, Claude-Flow/Ruflo, and SDD tools. AgentOps is the operational layer for coding agents and a context compiler for compounding work across sessions." -permalink: /comparisons/ ---- - -# AgentOps vs The Competition - -> **TL;DR:** Most tools optimize *within* a session. AgentOps is the operational layer for coding agents: publicly an operational control layer, technically a context compiler that turns work into better future work. - ---- - -## The Landscape (April 2026) - -The AI coding agent ecosystem has exploded. Here's how the major players stack up: - -| Tool | Focus | Strength | Gap AgentOps Fills | -|------|-------|----------|-------------------| -| [Superpowers](vs-superpowers.md) | TDD + Planning | Disciplined autonomous work, 6+ runtimes | No cross-session memory | -| [Claude-Flow / Ruflo](vs-claude-flow.md) | Multi-agent swarms + memory | Swarm orchestration, AgentDB/ReasoningBank | Database-first memory, not repo-native flywheel | -| [SDD Tools](vs-sdd.md) | Spec-driven development | Industry standard (AWS Kiro, GitHub Spec Kit) | Specs first; memory mostly optional extensions | -| [GSD](vs-gsd.md) | Spec-driven execution | 53 commands, 7 runtimes, advisor mode | Planning persistence, limited governed memory | -| [Compound Engineer](vs-compound-engineer.md) | Plan/work/review/compound | Stack-aware routing, 10 runtimes | Manual/doc-solution compounding, no validation gates | - -For the operator-facing readout across all competitors, see the -[Competitive Radar](competitive-radar.md). For the record of external parties -*independently arriving at* the AgentOps thesis (vindication, not competition), -see the [Convergence Ledger](../convergence/ledger.md) — anchored by the -[Google SRE encoding map](../convergence/google-sre.md). - ---- - -## The Core Insight - -``` -┌─────────────────────────────────────────────────────────────────────┐ -│ │ -│ WHAT OTHERS OPTIMIZE WHAT AGENTOPS OPTIMIZES │ -│ ══════════════════════ ═════════════════════════ │ -│ │ -│ Session 1 Session 2 Session 3 Session 1 Session 2 Session 3 │ -│ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │ -│ │ Fast │ │ Fast │ │ Fast │ │Learn │ → │Recall│ → │Expert│ │ -│ │ │ │ │ │ │ │ │ │ │ │ │ │ -│ └──────┘ └──────┘ └──────┘ └──────┘ └──────┘ └──────┘ │ -│ ↓ ↓ ↓ │ │ │ │ -│ [reset] [reset] [reset] └──────────┴──────────┘ │ -│ COMPOUNDS │ -│ │ -└─────────────────────────────────────────────────────────────────────┘ -``` - -**Most other tools:** Make each session faster -**AgentOps:** Provide the operational layer that makes each session build on the last - -Compound Engineer is the exception in this set: it also aims at compounding, but through a different workflow and persistence model. - ---- - -## Quick Comparison Matrix - -| Feature | Superpowers | Claude-Flow | SDD | GSD | Compound Engineer | AgentOps | -|---------|:-----------:|:-----------:|:---:|:---:|:-----------------:|:--------:| -| Planning workflow | ✅ | ⚠️ | ✅ | ✅ | ✅ | ✅ | -| TDD enforcement | ✅ | ❌ | ⚠️ | ❌ | ❌ | ✅ | -| Multi-agent execution | ✅ | ✅ | ❌ | ❌ | ⚠️ | ✅ | -| Spec validation | ⚠️ | ❌ | ✅ | ⚠️ | ❌ | ✅ | -| **Cross-session memory** | ❌ | ✅ | ⚠️ | ⚠️ | ✅ | ✅ | -| **Knowledge compounding** | ❌ | ⚠️ | ⚠️ | ⚠️ | ✅ | ✅ | -| **Pre-mortem simulation** | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | -| **8-aspect validation** | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | - -✅ = Core strength | ⚠️ = Partial/Basic | ❌ = Not present - ---- - -## When to Use What - -### Use Superpowers if: -- You want strict TDD enforcement -- Your codebase doesn't need cross-session context -- You're doing greenfield development - -### Use Claude-Flow if: -- You need massive parallelization (60+ agents) -- Performance is critical (WASM optimization) -- You're building enterprise orchestration - -### Use SDD (cc-sdd, spec-kit) if: -- You want spec-first development -- You work across multiple AI coding agents -- Documentation is your primary artifact - -### Use GSD if: -- You want fresh-context execution for each worker -- You want model/cost tiers and task-repair loops -- You're doing spec-driven phased work that does not need a knowledge flywheel - -### Use Compound Engineer if: -- You want a clean `Plan -> Work -> Review -> Compound` loop -- You care about cross-tool sync and portability -- You want compounding, but with less AgentOps-specific machinery - -### Use AgentOps if: -- You work on the same codebase repeatedly -- You want your agent to get smarter over time -- You value learning from past mistakes -- You want semantic validation (not just tests) -- You want failure prevention before building - ---- - -## The Compounding Advantage - -Over time, the gap widens: - -``` - CUMULATIVE TIME INVESTMENT - ══════════════════════════ - -Time (hrs) - │ - 40 │ ╱ Other tools - │ ╱ (linear) - 30 │ ╱ - │ ╱ - 20 │ ╱ - │ ╭─────────────╯ AgentOps - 10 │ ╭───╯ (compounds) - │ ╭───╯ - 0 │______╭───╯_________________________________ - └──────┬──────┬──────┬──────┬──────┬──────┬── - S1 S5 S10 S20 S50 S100 - Sessions -``` - -By session 100: -- **Other tools:** Still taking the same time per task -- **AgentOps:** Domain expert with instant recall - ---- - -## Detailed Comparisons - -- [Competition RPI: Memory, Learning, Wiki, Dream, and Pruning Pipelines](competition-rpi-memory-pipelines.md) — Cross-product primitive and pipeline audit -- [vs. Superpowers](vs-superpowers.md) — The TDD powerhouse -- [vs. Claude-Flow / Ruflo](vs-claude-flow.md) — The swarm orchestrator -- [vs. SDD Tools](vs-sdd.md) — The spec-driven approach -- [vs. GSD](vs-gsd.md) — The fresh-context execution framework -- [vs. Compound Engineer](vs-compound-engineer.md) — The closest philosophical neighbor -- [vs. hosted AI code review](vs-hosted-code-review.md) — CodeRabbit, Qodo, Copilot: same review play, different ownership story -- [Competitive Radar](competitive-radar.md) — The current market read and next-move pressure - ---- - -## Can I Use Them Together? - -**Yes, selectively:** - -| Combination | Works? | Notes | -|-------------|--------|-------| -| AgentOps + Superpowers | ⚠️ | Overlapping planning; pick one | -| AgentOps + Claude-Flow | ✅ | Claude-Flow for orchestration, AgentOps for memory | -| AgentOps + SDD | ✅ | SDD for specs, AgentOps captures learnings | -| AgentOps + GSD | ⚠️ | Both manage workflow state; use GSD for greenfield phased execution and AgentOps for long-lived compounding | -| AgentOps + Compound Engineer | ✅ | Compound Engineer for workflow shell, AgentOps for memory and validation | - -The key: AgentOps' value is in the **knowledge layer**. If another tool handles execution better for your use case, AgentOps can still capture and compound the learnings. - ---- - -
- -**Other tools optimize the session. AgentOps optimizes the journey.** - -[Back to README](https://github.com/boshu2/agentops/blob/main/README.md) - -
diff --git a/docs/comparisons/competitive-radar.md b/docs/comparisons/competitive-radar.md deleted file mode 100644 index ff5bf62d8..000000000 --- a/docs/comparisons/competitive-radar.md +++ /dev/null @@ -1,193 +0,0 @@ ---- -title: "AgentOps Competitive Radar" -description: "Current market read for AgentOps against the Claude Code skills/plugin ecosystem and adjacent agent-workflow tools." -permalink: /comparisons/competitive-radar -last_reviewed: 2026-05-07 ---- - -# Competitive Radar - -AgentOps should not try to be every agent workflow tool at once. The Claude -Code skills/plugin ecosystem has consolidated into five lanes; the strongest -position is narrower and harder to copy than any of them: **a wiki for your -agents, version-controlled in your repo, that compounds across sessions**. -The corpus is the moat. The tool is replaceable. - -> Sibling doc: the [Convergence Ledger](../convergence/ledger.md) tracks the -> inverse of competition — external parties (labs, vendors, methodologies) -> *independently arriving at* the AgentOps thesis over time. - -## Source Set - -| Source | Lane | Link | -|--------|------|------| -| AgentOps | Context library / wiki for agents (this doc) | [boshu2/agentops](https://github.com/boshu2/agentops) | -| obra/superpowers | Methodology (TDD discipline, autonomy patterns) | [obra/superpowers](https://github.com/obra/superpowers) | -| EveryInc/compound-engineering-plugin | Methodology (ideate→compound, 7-phase loop) | [EveryInc/compound-engineering-plugin](https://github.com/EveryInc/compound-engineering-plugin) | -| anthropics/claude-plugins-official | Curation (first-party marketplace) | [anthropics/claude-plugins-official](https://github.com/anthropics/claude-plugins-official) | -| jeremylongshore/claude-code-plugins-plus-skills | Volume marketplace (CCPI manager) | [jeremylongshore/claude-code-plugins-plus-skills](https://github.com/jeremylongshore/claude-code-plugins-plus-skills) | -| alirezarezvani/claude-skills | Volume + breadth (skills × platforms) | [alirezarezvani/claude-skills](https://github.com/alirezarezvani/claude-skills) | -| affaan-m/everything-claude-code | Cross-harness (DRY parity across runtimes) | [affaan-m/everything-claude-code](https://github.com/affaan-m/everything-claude-code) | -| trailofbits/skills | Vertical authority (security domain) | [trailofbits/skills](https://github.com/trailofbits/skills) | - -## Market Read - -The five-lane consolidation (see *Lane segmentation* below) means most -"compete with AgentOps" framings are category errors. A volume marketplace -sells inventory; a methodology plugin sells discipline; a vertical-authority -collection sells domain credibility. None of the seven sells **persistent -context that compounds across sessions on your hardware**. That lane is -empty, and the rest of this radar is about staying in it. - -## Seven-competitor lane table - -What each competitor wins on (the structural advantage AgentOps cannot beat -them on) and what AgentOps wins on against them. Sourced from the -2026-05-07 council research (`.agents/council/2026-05-07-research-readme-positioning.md` in the repo). - -| Competitor | Lane | What they win on | What AgentOps wins on | -|------------|------|------------------|------------------------| -| [obra/superpowers](https://github.com/obra/superpowers) | Methodology | Official Anthropic marketplace placement, ~29K stars, Jesse Vincent brand, TDD red-green-refactor as a sharp methodology hook | Persistent corpus accumulating across sessions; cross-session memory in `.agents/`; multi-model council as a commit gate; off-API operation on your hardware (in-session loop, with out-of-session dispatch on the NTM + MCP Agent Mail substrate) | -| [EveryInc/compound-engineering-plugin](https://github.com/EveryInc/compound-engineering-plugin) | Methodology | 10-target runtime conversion CLI; configurable per-project reviewer routing; ideation-to-compound surface area; closest philosophical neighbor | Model-independent **per-phase** routing (Claude for discovery, Codex for implementation, fresh Claude for validation, local model for unattended defrag — in one workflow); persistent corpus that lives in your repo, not the tool | -| [anthropics/claude-plugins-official](https://github.com/anthropics/claude-plugins-official) | Curation | First-party authority; default discovery channel; "trust before installing" posture | Operates underneath any harness — Claude Code, Codex, Cursor, OpenCode — turning sessions into a context library you own. AgentOps is not a coding harness; it sits on top of whichever harness you already use | -| [jeremylongshore/claude-code-plugins-plus-skills](https://github.com/jeremylongshore/claude-code-plugins-plus-skills) | Volume marketplace | 425 plugins / 2,810 skills inventory; CCPI package manager; sponsored placement; daily download metrics | Compounding context vs. static inventory: skills don't accumulate, a wiki does. AgentOps ships a bookkeeping schema that grows; volume marketplaces ship an inventory that doesn't | -| [alirezarezvani/claude-skills](https://github.com/alirezarezvani/claude-skills) | Volume + breadth | ~5,200+ stars; 235 skills × 12 platforms; 305 stdlib Python tools; multi-domain coverage (engineering + marketing + compliance) | Same persistent-corpus argument plus model-independent phase routing inside one session — breadth of skills doesn't replace cross-session memory | -| [affaan-m/everything-claude-code](https://github.com/affaan-m/everything-claude-code) | Cross-harness | DRY parity across 5 runtimes; 48 subagents; "Anthropic Hackathon Winner" credential. (README also claims 140K+ stars / 21K forks; the entire Claude Code ecosystem is below those numbers — flagged as not validated.) | Cross-runtime *distribution* is not cross-model *per-phase routing*. AgentOps mixes models per phase within one RPI loop, with state preserved across boundaries — a different optimization | -| [trailofbits/skills](https://github.com/trailofbits/skills) | Vertical authority (security) | Trail of Bits brand; security skills + a "Trophy Case" of CVE-shaped findings; domain credentialing no general-purpose tool can match | Different category — AgentOps is the substrate a vertical-authority collection runs on. Their skills can live inside an AgentOps corpus; the inverse is not true | - -**What this table reveals:** none of the seven are selling persistence, -sovereignty, off-API operation, multi-model per-phase routing, or -context-as-a-discipline. The substrate / wiki-for-agents lane is empty if -AgentOps claims it. - -## Lane segmentation - -The Claude Code skills/plugin ecosystem has consolidated into five lanes. -Each row names the lane, the canonical examples, the buyer signal, and -AgentOps's posture toward it (compete, complement, or ignore). - -### 1. Volume (marketplace / many skills) - -**Examples:** [jeremylongshore/claude-code-plugins-plus-skills](https://github.com/jeremylongshore/claude-code-plugins-plus-skills) (425 plugins, 2,810 skills, CCPI manager, 100-point grading); [alirezarezvani/claude-skills](https://github.com/alirezarezvani/claude-skills) (235 skills × 12 platforms); [affaan-m/everything-claude-code](https://github.com/affaan-m/everything-claude-code) (182 skills, 48 subagents) overlaps here too. - -**Buyer signal:** "We have the most stuff." Inventory bulk as the value prop. Costco bulk-buy of pre-built skills. - -**AgentOps's posture:** **Complement, do not compete.** AgentOps is a different category — a context library / wiki, not a skills inventory. The two combine: install the volume marketplace for breadth; run AgentOps for the corpus discipline that turns those skills' outputs into persistent context. - -### 2. Methodology (workflow / discipline) - -**Examples:** [obra/superpowers](https://github.com/obra/superpowers) (TDD red-green-refactor); [EveryInc/compound-engineering-plugin](https://github.com/EveryInc/compound-engineering-plugin) (ideate→compound, 7-phase loop, 10-target conversion). - -**Buyer signal:** "Use our workflow and your agents will produce better code." A sharp opinion about *how* to drive agents. - -**AgentOps's posture:** **Compete obliquely, do not contest head-on.** This lane is structurally hard to win — Superpowers has 29K stars and official-marketplace placement; Compound Engineer has the closest philosophical positioning. Winning the methodology lane is *not* AgentOps's bet. AgentOps offers methodology surfaces (`/rpi`, `/council`, `/pre-mortem`, `/vibe`) but its claim is one level deeper: methodology sits on top of context, and the context is what compounds. Anthropic's Managed Agents (May 2026) is also moving into this lane natively. - -### 3. Vertical (specific domain) - -**Examples:** [trailofbits/skills](https://github.com/trailofbits/skills) (security skills + Trophy Case of findings). - -**Buyer signal:** "We are the authoritative source for skills in domain X." Domain credibility no general-purpose tool can replicate. - -**AgentOps's posture:** **Complement.** Vertical collections produce skills; AgentOps produces the context library those skills run inside. A security team can install Trail of Bits skills on top of an AgentOps corpus; the security findings then flow into `.agents/` as persistent learnings. AgentOps does not chase vertical authority — that's a brand investment, not a tool feature. - -### 4. Curation (small high-quality set) - -**Examples:** [anthropics/claude-plugins-official](https://github.com/anthropics/claude-plugins-official) (first-party marketplace, "trust before installing"); selective collections that prioritize quality over volume. - -**Buyer signal:** "We curate so you don't have to." Trust + discovery channel as the value prop. - -**AgentOps's posture:** **Ignore as a competitor; respect as distribution.** First-party curation is unbeatable for trust and discovery, and the right move is to be installable through it rather than to compete with it. AgentOps's claim is orthogonal: it's not "trust this skill" but "build a corpus that survives whichever skill you used last week." - -### 5. Cross-harness (multi-runtime parity) - -**Examples:** [affaan-m/everything-claude-code](https://github.com/affaan-m/everything-claude-code) (DRY parity across Claude Code, Cursor, Codex, OpenCode, Gemini); [EveryInc/compound-engineering-plugin](https://github.com/EveryInc/compound-engineering-plugin) (10-target conversion CLI) overlaps here. - -**Buyer signal:** "Our skills run everywhere." Multi-runtime distribution as the value prop. - -**AgentOps's posture:** **Compete on a sharper claim.** Cross-harness *distribution* is multi-runtime spread of one workflow shape. AgentOps does cross-harness *and* model-independent **per-phase routing** — Claude does discovery, Codex implements, fresh Claude validates, an open-weights local model handles overnight defrag, all in one RPI loop with state preserved across boundaries. Nobody else in the seven-competitor set does this. The pitch is "mix and match models per phase," not "the same skill on five runtimes." - -## Where AgentOps Wins - - - -Six differentiators no competitor in the seven-competitor set has. Sourced from -the 2026-05-07 council research (`.agents/council/2026-05-07-research-readme-positioning.md` in the repo). - -### 1. Persistent corpus - -A bookkeeping schema that *grows*: learnings, patterns, planning rules, and -cited decisions accumulate in `.agents/` as plain markdown, version-controlled -with the code. Competitors ship skills (static inventory); AgentOps ships the -discipline that turns sessions into a wiki. Receipts: as of 2026-05-04, this -repo's `.agents/` contained ~1,842 learnings, ~186 patterns, ~80 planning -rules, and ~3,867 cited decisions captured by the system on itself. - -### 2. Off-API, off-vendor operation - -The whole loop — dream / evolve / compile / defrag / forge — runs in session -on your hardware, against your subscription, off any vendor's API. To run it -unattended, dispatch the operating loop on the NTM + MCP Agent Mail substrate -(a tmux swarm of workers coordinating through Agent Mail locks/inboxes); -AgentOps ships no daemon or scheduler of its own (see [ADR-0009](../adr/ADR-0009-daemon-deletion-in-session-only.md)). -All seven competitors are in-session plugins with no off-vendor out-of-session -story. Off-API operation is the structural answer to "what if a frontier -vendor ships native equivalents in 12 months" — your corpus and your loop keep -running regardless. - -### 3. Multi-model council - -`/council --mixed` runs Claude + Codex (and other) judges in parallel against -one evidence packet, producing a verdict before commit. Compound Engineer has -reviewer agents but they are single-model and post-implementation. A -multi-model commit gate is the strongest validation primitive in the set. - -### 4. Model-independent phase routing - -Pick Claude for ideation, Codex for validation, an open-weights local model -for unattended defrag — *per phase, in one workflow,* with state preserved -across the boundaries. Cross-harness *distribution* (everything-claude-code, -Compound Engineer's 10-target conversion) is multi-runtime spread of one -workflow shape; per-phase routing is mixing models inside one workflow. -Different optimization. Nobody in the seven does it. - -### 5. Context-engineering vocabulary - -AgentOps owns "wiki for agents" + "context library" + "context compiler" -+ "CDLC" as a coherent vocabulary, anchored by the SE → context translation -table (source code → context, SDLC → CDLC, libraries → context libraries, -compilers → context compilers, code review → multi-model councils, CI/CD → -validation gates, postmortems → automated postmortems, runbooks → skills + -planning rules, software factories → the in-session operating loop (with an -orchestration substrate for out-of-session runs), Markdown/Git/Linux → LLM -Wiki of Markdown, open-source corpus → your private corpus). -Vocabulary ownership is durable; Superpowers owns "TDD," AgentOps can own -"context engineering." - -### 6. Honest empirical disclosure - -Δ=+0.0000 at workbench v1 difficulty, published in-repo. Independent 3-judge -audit (2026-05-06) confirmed parity with Anthropic Managed Agents on rubric -authoring, separate-context grading, and iterate-until-pass. In a market of -inflated star counts and ungraded "100-point validations," a published null -result and a transparent audit are credibility assets. - -## Current Vulnerabilities - -| Vulnerability | Impact | Best next move | -|---------------|--------|----------------| -| Corpus durability under routine cleanup | The receipts claim ("1,842 learnings") becomes fragile if maintenance can wipe `.agents/` subdirs (observed 2026-05-07). | Snapshot/restore mechanism + tracked durability fix; in the meantime, receipts cite the 2026-05-04 stable snapshot with timestamp. | -| Methodology lane is being eaten | Anthropic's Managed Agents (May 2026) and Superpowers' marketplace placement compress the methodology buyer's choice set. | Stay out of the methodology lane head-on; lead with the wiki framing and lane-segmentation argument. | -| Compounding proof is still too implicit | Users have to trust the flywheel story before they feel it. | Put Dream reports, `ao demo`, and corpus-stats in the first-run path. | -| Reviewer routing is less configurable than Compound Engineer | CE can feel more tailored to a stack. | Document per-project validation profile selection; expose council config more visibly. | -| Volume-marketplace shoppers may bounce off "73 skills" framing | Inventory-comparison buyers will not see the corpus advantage. | Keep the volume comparison explicit (vs-tons-of-skills doc); reframe the question from "how many skills" to "what does the asset look like in 6 months". | - -## Execution Bias - -Do not respond to every competitor feature by adding another command. Favor -moves that make the wiki visible, automatic, and verifiable: - -1. Make the corpus inspectable on day one (`scripts/corpus-stats.sh`, `ao inject` traces). -2. Make first value obvious in under five minutes (skills install + first session writes to `.agents/`). -3. Keep the per-phase model-routing demo as the anchor (it's the killer feature buried in the founder pitch). -4. Defend the wiki/context-engineering vocabulary in every doc; do not drift to "skills repo" or "methodology" framings. -5. Keep comparison docs tied to current official sources; re-run `scripts/check-competitive-freshness.sh` before each release. diff --git a/docs/comparisons/vs-claude-flow.md b/docs/comparisons/vs-claude-flow.md deleted file mode 100644 index 2650ce0fb..000000000 --- a/docs/comparisons/vs-claude-flow.md +++ /dev/null @@ -1,281 +0,0 @@ ---- -title: "AgentOps vs Claude-Flow (Ruflo) — Detailed Comparison" -description: "How AgentOps compares to Claude-Flow/Ruflo for AI coding agents. Claude-Flow optimizes swarm orchestration at scale. AgentOps compounds knowledge across sessions." -permalink: /comparisons/agentops-vs-claude-flow ---- - -# AgentOps vs Claude-Flow (now Ruflo) - -> **Ruflo v3.5** (formerly Claude-Flow) is a multi-agent orchestration platform featuring 314 MCP tools, 16+ agent roles, and WASM-optimized performance. V3 was a complete rebuild with 250,000+ lines redesigned into modular TypeScript/WASM architecture. 6,000+ commits, 54+ specialized agents in coordinated swarms. -> -> *Comparison updated April 2026. See [Ruflo repo](https://github.com/ruvnet/ruflo) for current features.* - ---- - -## At a Glance - -| Aspect | Claude-Flow | AgentOps | -|--------|-------------|----------| -| **Philosophy** | "Swarm intelligence at scale" | "Operational layer for coding agents; technically a context compiler" | -| **Core strength** | Multi-agent orchestration, WASM performance, 314 MCP tools | Cross-session memory, validation gates, knowledge flywheel | -| **Latest** | v3.5.65 (April 2026, rebranded to Ruflo) | v2.39.0 (April 2026) | -| **GitHub stars** | 11,400+ | 240+ (growing rapidly) | -| **Downloads** | 500,000+ | — | -| **Primary use** | Enterprise orchestration | Ongoing codebase work | - -### Three-Layer Comparison - -| Product Layer | Claude-Flow / Ruflo | AgentOps | -|---------------|--------------------:|----------| -| **Context Compiler** (L1) | MCP-based tool routing; no cross-session retrieval | Decay-ranked retrieval, phase-scoped assembly, token-budgeted injection | -| **Validation Gates** (L2) | Agent-role review (no independent consensus) | Multi-model councils with auditable PASS/WARN/FAIL verdicts | -| **Knowledge Flywheel** (L3) | No persistent learning between swarm runs | Extract → score → promote → inject; sessions compound automatically | - ---- - -## What Claude-Flow Does Well - -### 1. Massive Agent Swarms -60+ specialized agents that can work simultaneously: -- Code review agents -- Testing agents -- Security audit agents -- Documentation agents -- DevOps agents - -### 2. WASM Performance -Claude-Flow V3 was rebuilt with TypeScript and WASM for extreme performance: -- 352x faster execution -- 75% API cost savings -- 250% effective subscription capacity improvement - -### 3. Enterprise Architecture -Built for scale with: -- Distributed swarm intelligence -- RAG integration -- Native MCP protocol support -- Fault-tolerant consensus - -### 4. Self-Learning Swarms -V3 introduced swarms that can adapt their behavior within a session. - ---- - -## Where Claude-Flow Falls Short - -### No Cross-Session Learning - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ CLAUDE-FLOW │ -│ │ -│ Session 1: 60 agents solve auth bug │ -│ Session 2: 60 agents solve auth bug (no memory of Session 1) │ -│ Session 3: 60 agents solve auth bug (no memory of Session 2) │ -│ │ -│ Fast parallel execution, but no compounding │ -└─────────────────────────────────────────────────────────────────┘ - -┌─────────────────────────────────────────────────────────────────┐ -│ AGENTOPS │ -│ │ -│ Session 1: Solve auth bug → Extract: "token refresh" │ -│ Session 2: Auth issue? ← Inject prior knowledge │ -│ Session 3: Auth? ← Instant domain expertise │ -│ │ -│ Knowledge persists and compounds │ -└─────────────────────────────────────────────────────────────────┘ -``` - -**Claude-Flow optimizes execution speed.** AgentOps optimizes learning across time. - -### No Pre-Implementation Validation - -Claude-Flow agents execute tasks. They don't simulate failures before building. - -``` -Claude-Flow: - Task → Swarm executes → Results - -AgentOps: - Task → Pre-Mortem (simulate failures) → Implement → Post-Mortem (extract learnings) -``` - -### Orchestration Focus, Not Quality Focus - -Claude-Flow excels at coordinating many agents. It doesn't provide the deep semantic validation that `/validate` offers: - -| Validation | Claude-Flow | AgentOps | -|------------|:-----------:|:--------:| -| Task completion | ✅ | ✅ | -| Semantic correctness | ❌ | ✅ | -| Security review | ⚠️ Agent-based | ✅ 8-aspect | -| Architecture analysis | ⚠️ Agent-based | ✅ Built-in | -| AI slop detection | ❌ | ✅ | -| Accessibility | ❌ | ✅ | - ---- - -## Feature Comparison - -| Feature | Claude-Flow | AgentOps | Winner | -|---------|:-----------:|:--------:|:------:| -| Multi-agent execution | ✅ 60+ agents | ✅ 20 experts | Claude-Flow | -| WASM performance | ✅ 352x faster | ❌ Standard | Claude-Flow | -| Enterprise scale | ✅ Distributed | ⚠️ Single-repo | Claude-Flow | -| RAG integration | ✅ Built-in | ⚠️ Via MCP | Claude-Flow | -| **Cross-session memory** | ❌ None | ✅ Git-persisted | **AgentOps** | -| **Knowledge compounding** | ❌ No | ✅ Escape velocity | **AgentOps** | -| **Pre-mortem simulation** | ❌ No | ✅ 10 failure modes | **AgentOps** | -| **8-aspect validation** | ❌ No | ✅ Semantic validator | **AgentOps** | -| **Scientific foundation** | ❌ Engineering | ✅ Peer-reviewed | **AgentOps** | - ---- - -## Architecture Comparison - -### Claude-Flow Architecture - -``` -┌────────────────────────────────────────────────────────────────┐ -│ CLAUDE-FLOW V3 │ -│ │ -│ ┌─────────┐ ┌─────────────────────────────────────────┐ │ -│ │ Task │───▶│ SWARM COORDINATOR │ │ -│ └─────────┘ └─────────────────────────────────────────┘ │ -│ │ │ -│ ┌────────────────────┼────────────────────┐ │ -│ ▼ ▼ ▼ │ -│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ -│ │ Agent 1 │ │ Agent 2 │ ... │Agent 60+│ │ -│ │ (code) │ │ (test) │ │ (docs) │ │ -│ └────┬────┘ └────┬────┘ └────┬────┘ │ -│ │ │ │ │ -│ └────────────────────┴────────────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌─────────┐ │ -│ │ Results │ (session ends, gone) │ -│ └─────────┘ │ -└────────────────────────────────────────────────────────────────┘ -``` - -### AgentOps Architecture - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ AGENTOPS │ -│ │ -│ ┌─────────┐ ┌─────────────────┐ │ -│ │ Task │◀─────── inject ──────────────│ .agents/ │ │ -│ └────┬────┘ │ (memory) │ │ -│ │ └────────▲────────┘ │ -│ ▼ │ │ -│ ┌─────────────┐ │ │ -│ │ Pre-Mortem │ (simulate failures) │ │ -│ └──────┬──────┘ │ │ -│ │ │ │ -│ ▼ │ │ -│ ┌─────────────┐ ┌───────────┐ │ │ -│ │ /implement │────▶│ /validate │─── pass ────────▶│ │ -│ │ (implement) │ │ (validate)│ │ │ -│ └─────────────┘ └────┬──────┘ │ │ -│ │ fail │ │ -│ └───────▶ fix ───────────┘ │ -│ │ -│ Session ends → Learnings extracted → Next session benefits │ -└─────────────────────────────────────────────────────────────────┘ -``` - ---- - -## Performance vs Learning Trade-off - -``` - PERFORMANCE LEARNING - ═══════════ ════════ - -Claude-Flow: ████████████████████ ░░░░░░░░░░ - (60 agents, WASM, fast) (no persistence) - -AgentOps: ████████████░░░░░░░░ ████████████████ - (20 agents, standard) (compounds over time) -``` - - -**Different optimizations for different goals:** -- Claude-Flow: Maximum throughput *now* -- AgentOps: Maximum effectiveness *over time* - ---- - -## When to Choose Claude-Flow - -- You need **massive parallelization** (60+ agents) -- **Performance** is critical (API cost, execution speed) -- You're building **enterprise orchestration** systems -- Sessions are **independent** (no need for cross-session context) -- You want **battle-tested scale** (500K+ downloads) - -## When to Choose AgentOps - -- You work on the **same codebase** repeatedly -- You want your agent to **remember past work** -- You want **failure prevention** before building -- You want **deep semantic validation** beyond completion -- You value **compounding knowledge** over raw speed - ---- - -## Can They Work Together? - -**Yes, this is actually a strong combination:** - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ CLAUDE-FLOW + AGENTOPS │ -│ │ -│ Session bootstrap (explicit, hookless): │ -│ └── ao session bootstrap → ao inject loads context │ -│ │ -│ Execution: │ -│ └── Claude-Flow orchestrates 60+ agents │ -│ │ -│ Validation: │ -│ └── AgentOps /validate validates all outputs │ -│ │ -│ Session close (explicit): │ -│ └── ao forge / ao flywheel close-loop extract learnings │ -│ │ -└─────────────────────────────────────────────────────────────────┘ -``` - -- **Claude-Flow** handles execution and orchestration -- **AgentOps** handles memory and validation - -The tools are complementary, not competing. - ---- - -## The Bottom Line - -| Dimension | Claude-Flow | AgentOps | -|-----------|-------------|----------| -| **Optimizes** | Execution speed | Learning over time | -| **Scale** | 60+ agents | 20 expert validators | -| **Performance** | WASM, 352x faster | Standard | -| **Memory** | None | Git-persisted, compounds | -| **Validation** | Task completion | 8-aspect semantic | - -**Claude-Flow makes Claude fast *today*.** -**AgentOps makes Claude smart *over time*.** - -**Best of both worlds:** Use together for speed + memory. - ---- - -
- -[← vs. Superpowers](vs-superpowers.md) · [Back to Comparisons](README.md) · [vs. SDD →](vs-sdd.md) - -
diff --git a/docs/comparisons/vs-compound-engineer.md b/docs/comparisons/vs-compound-engineer.md deleted file mode 100644 index 266fc9840..000000000 --- a/docs/comparisons/vs-compound-engineer.md +++ /dev/null @@ -1,300 +0,0 @@ ---- -title: "AgentOps vs Compound Engineer — Detailed Comparison" -description: "How AgentOps compares to Compound Engineer for AI coding agents. Both compound knowledge, but AgentOps automates the flywheel while Compound Engineer requires manual invocation." -permalink: /comparisons/agentops-vs-compound-engineer -last_reviewed: 2026-05-07 ---- - -# AgentOps vs Compound Engineer - -> **Compound Engineer** is Every's coding-agent plugin implementing a knowledge-compounding development workflow. The core thesis: "Each unit of engineering work should make subsequent units easier." Current state (May 2026): 37 skills, 51 agents, an explicit 7-phase workflow (ideate → brainstorm → plan → work → review → compound → refresh), and a 10-target cross-runtime conversion CLI. Recent additions: stack-aware reviewer routing, mandatory code review by default, and PR description filtering. -> -> See the [Compound Engineer repo](https://github.com/EveryInc/compound-engineering-plugin) for current features. - ---- - -## At a Glance - -| Aspect | Compound Engineer | AgentOps | -|--------|-------------------|----------| -| **Philosophy** | "Each unit of engineering work should make subsequent units easier" | "Operational layer for coding agents; technically a context compiler" | -| **Core strength** | Full ideate-to-compound loop, cross-runtime portability, configurable review agents | Git-tracked memory, validation gates, knowledge flywheel with scoring | -| **GitHub** | EveryInc/compound-engineering-plugin | boshu2/agentops | -| **Latest** | Active development (April 2026) | v2.39.0 (April 2026) | -| **Scale** | 37 skills, 51 agents, 10 runtime targets | 73 skills, compiled CLI, hooks, schemas | -| **Primary use** | Standardized engineering workflow with knowledge capture | Ongoing codebase work with persistent memory and validation | - -### Three-Layer Comparison - -| Product Layer | Compound Engineer | AgentOps | -|---------------|------------------:|----------| -| **Context Compiler** (L1) | Manual `/refresh` invocation loads context | Automatic injection at session start; phase-scoped, token-budgeted | -| **Validation Gates** (L2) | Configurable review agents (single-model) | Multi-model councils with independent judges and auditable verdicts | -| **Knowledge Flywheel** (L3) | `/compound` captures manually; no scoring or decay | Automated extract → score → promote → inject with decay ranking | - ---- - -## What Compound Engineer Does Well - -### 1. The Full Ideate-to-Compound Loop - -Compound Engineer's workflow now starts earlier and ends later than just plan/work/review: - -```text -Ideate -> Brainstorm -> Plan -> Work -> Review -> Compound -> Refresh -``` - -- **Ideate** (`/ce:ideate`): Divergent idea generation with parallel subagents using different frames (friction, inversions, leverage, edge cases), then adversarial critique filters to 5-7 survivors. Grounded in codebase scanning and GitHub issue intelligence. -- **Brainstorm** (`/ce:brainstorm`): Collaborative dialogue to define WHAT to build. Produces a requirements doc, not just casual brainstorming. Answers product questions before planning begins. -- **Compound Refresh** (`/ce:compound-refresh`): Systematic maintenance of the `docs/solutions/` knowledge base. Classifies learnings as Keep/Update/Replace/Archive. Has both interactive and autonomous modes with subagent investigation. - -### 2. Cross-Runtime Reach Is Excellent - -The Bun/TypeScript CLI converts the Claude Code plugin to 10 other formats: - -| Target | Notes | -|--------|-------| -| OpenCode | Commands as .md, deep-merged config | -| Codex | Prompt + skill pairs | -| Factory Droid | Tool name remapping | -| Pi | Prompts, skills, extensions, MCPorter interop | -| Gemini CLI | Skills from agents, commands as .toml | -| GitHub Copilot | .agent.md with Copilot frontmatter | -| Kiro | JSON configs + prompt .md | -| OpenClaw | TypeScript skill file | -| Windsurf | Global or workspace scope | -| Qwen Code | Agents as .yaml | - -Plus a `sync` command that copies personal `~/.claude/` config to all other runtimes. - -### 3. Configurable Review Agents Per Project - -The `/setup` skill auto-detects project stack and configures which review agents run during `/ce:review`. Stored in `compound-engineering.local.md`. Includes 15 specialized review agents (security, performance, architecture, Rails patterns, race conditions, schema drift, etc.) that activate based on project type. - -### 4. Three-Tier Plan Detail - -Plans auto-select Minimal/More/A Lot templates based on complexity. Prevents over-engineering small tasks while allowing depth for large features. Multi-agent research (repo analyst, learnings researcher, best-practices researcher) informs the plan. - -### 5. Swarm Orchestration Guide - -The `orchestrating-swarms` skill is a 1700+ line reference document covering Claude Code's TeammateTool, Task system, inbox messaging, spawn backends (in-process, tmux, iterm2), and 6 orchestration patterns. This serves as a comprehensive swarm API reference. - -### 6. Plugin Marketplace Distribution - -Now distributed via Claude Code's plugin marketplace: -```bash -/plugin marketplace add EveryInc/compound-engineering-plugin -``` - ---- - -## The Real Difference - -Both projects care about compounding. The distinction is **where the system puts the weight** and **how mechanical the compounding is**. - -```text -Compound Engineer: - workflow discipline (ideate → compound → refresh) - + cross-tool portability (10 runtimes) - + configurable review agents - + knowledge capture in docs/solutions/ - -AgentOps: - repo-native memory with scoring and injection - + validation gates (pre-mortem, council, vibe) - + tracked execution with dependency ordering - + strategic goals and measured progress -``` - - -Compound Engineer captures knowledge manually through the compound step and maintains it through compound-refresh. AgentOps extracts knowledge automatically through session hooks and post-mortems, scores it for maturity, and injects relevant learnings into future sessions mechanically. - ---- - -## Where AgentOps Goes Further - -### Automated Knowledge Flywheel, Not Manual Compounding - -Both systems compound knowledge. The mechanical difference matters: - -| Aspect | Compound Engineer | AgentOps | -|--------|-------------------|----------| -| Knowledge capture | `/ce:compound` (manual trigger) | Post-mortem + session hooks (automatic) | -| Knowledge maintenance | `/ce:compound-refresh` (manual or scheduled) | `ao maturity` + decay (automatic) | -| Knowledge retrieval | `learnings-researcher` agent searches docs/solutions/ | `ao inject` scores and retrieves by relevance | -| Knowledge scoring | Pattern detection at 3+ similar solutions | Maturity scoring with confidence and decay | - -AgentOps' flywheel runs without the user remembering to invoke it. Compound Engineer requires the user to run `/ce:compound` after work and `/ce:compound-refresh` periodically. - -### More Explicit Failure Prevention - -AgentOps adds named gates *before* implementation: - -- `/pre-mortem` simulates failure modes before building -- `/council` runs multi-model adversarial validation -- `/vibe` validates across multiple dimensions after building - -Compound Engineer has strong review agents but they run *after* implementation. There is no pre-implementation failure simulation. - -### Issue Graph and Wave Execution - -AgentOps is more opinionated about tracked work: - -- `/plan` creates dependency-aware issues through beads -- `/crank` executes unblocked waves with validation after each -- `/evolve` measures goals and repeats the loop automatically - -Compound Engineer's `/ce:work` executes plan files with todo tracking and supports swarm mode, but does not model cross-issue dependencies. - -### Strategic Goals - -AgentOps has GOALS.md, `ao goals measure`, and `/evolve` for measuring progress toward higher-level objectives. Compound Engineer has no equivalent goal-tracking or strategic direction mechanism. - -### Model-Independent Phase Routing - -AgentOps routes a different model to each phase of one RPI loop, with state preserved across boundaries: - -| Phase | Model | Why | -|-------|-------|-----| -| Research / Discovery | Claude | Long-context synthesis over the corpus | -| Plan | Claude | Architectural reasoning | -| Pre-mortem | Codex (or `/council --mixed`) | Adversarial second opinion from a different family | -| Implement | Codex | Code generation throughput | -| Validate | Fresh Claude | Independent reviewer with no prior context bias | - -Compound Engineer's 10-target conversion is multi-runtime *distribution* — running the same Claude-shaped workflow on Codex, Gemini, OpenCode, etc. AgentOps does that too via supported runtimes, but additionally swaps models *per phase within a single session* so each phase runs on the model best suited to that phase. CE doesn't do this per-phase model swap; the workflow is single-model end-to-end inside whichever runtime executes it. - ---- - -## Where Compound Engineer Goes Further - -### Ideation with Adversarial Filtering - -`/ce:ideate` is more structured than AgentOps' `/brainstorm`. It spawns parallel subagents with different analytical frames, then runs adversarial critique. AgentOps has brainstorming but not the structured divergent-then-adversarial pipeline. - -### Configurable Review Agents - -Compound Engineer's per-project review agent configuration (15 specialized agents, auto-detected from stack) is more flexible than AgentOps' fixed validation pipeline. Different projects get different reviewers automatically. - -### Knowledge Maintenance (compound-refresh) - -While AgentOps has maturity scoring and decay, Compound Engineer's `/ce:compound-refresh` does systematic investigation of staleness — dispatching subagents to check whether stored solutions are still accurate against the current codebase. AgentOps' decay is time-based; CE's refresh is investigation-based. - -### Cross-Runtime Portability - -10 runtime targets with a conversion CLI is significantly broader than AgentOps' Claude Code-primary approach. Teams using multiple AI tools benefit directly. - ---- - -## Feature Comparison - -| Feature | Compound Engineer | AgentOps | Winner | -|---------|:-----------------:|:--------:|:------:| -| Cross-runtime support | ✅ 10 targets + sync | ⚠️ 4 supported runtime paths, no 10-target converter | CE | -| Configurable review agents | ✅ 15 agents, per-project | ⚠️ Fixed pipeline | CE | -| Ideation + adversarial filtering | ✅ Structured pipeline | ⚠️ Brainstorm skill | CE | -| Knowledge maintenance | ✅ compound-refresh (investigative) | ✅ Maturity + decay (automated) | Tie | -| Plugin marketplace | ✅ Native distribution | ⚠️ Script install | CE | -| Swarm reference guide | ✅ 1700+ line guide | ✅ Swarm skill | Tie | -| Workflow clarity | ✅ Explicit 7-phase loop | ✅ Explicit RPI loop | Tie | -| Planning emphasis | ✅ 3-tier detail levels | ✅ Pre-mortem + plan | Tie | -| Worktree-oriented execution | ✅ Built in | ✅ Built in | Tie | -| **Cross-session learning** | ⚠️ Manual compound step | ✅ Automated flywheel | **AgentOps** | -| **Knowledge scoring** | ⚠️ Pattern detection at 3+ | ✅ Maturity + confidence + decay | **AgentOps** | -| **Pre-mortem simulation** | ❌ Not present | ✅ Before implementation | **AgentOps** | -| **Multi-model council** | ❌ Not present | ✅ Multi-perspective validation | **AgentOps** | -| **Model-independent phase routing** | ❌ Single-model workflow per runtime | ✅ Per-phase model swap (Claude → Codex → fresh Claude) in one RPI loop | **AgentOps** | -| **Issue graph execution** | ⚠️ Plan-based todos | ✅ Beads + dependency waves | **AgentOps** | -| **Strategic goals** | ❌ No goal tracking | ✅ GOALS.md + evolve | **AgentOps** | -| **Compiled CLI** | ❌ No binary | ✅ Go binary (ao) | **AgentOps** | - ---- - -## Workflow Comparison - -### Compound Engineer Workflow - -```text -/ce:ideate -> divergent ideation with adversarial filtering - ↓ -/ce:brainstorm -> collaborative requirements exploration - ↓ -/ce:plan -> multi-agent research → 3-tier plan - ↓ -/ce:work -> execute with branching, todos, incremental commits - ↓ (supports inline, serial, parallel, or swarm) -/ce:review -> configurable multi-agent code review - ↓ -/ce:compound -> document learnings to docs/solutions/ - ↓ -/ce:compound-refresh -> maintain knowledge base over time -``` - -### AgentOps Workflow - -```text -/research -> explore codebase + inject prior knowledge - ↓ -/plan -> break into dependency-tracked issues (beads) - ↓ -/pre-mortem -> simulate failure modes before building - ↓ -/crank -> execute unblocked waves → validate → commit - ↓ -/vibe -> multi-aspect code validation (council optional) - ↓ -/post-mortem -> extract learnings → score → store for next session -``` - -**Key difference:** Compound Engineer starts earlier (ideation, brainstorming) and compounds knowledge through explicit documentation. AgentOps enforces stronger pre-implementation gates and compounds knowledge through automated extraction and scoring. - ---- - -## When to Choose Compound Engineer - -- You want a **structured ideation-to-delivery loop** with adversarial idea filtering. -- You work across **multiple AI runtimes** and need config sync. -- You want **project-specific review agents** that auto-detect from your stack. -- Your team wants **plugin marketplace distribution** for easy adoption. -- You prefer **explicit knowledge documentation** over automated extraction. - -## When to Choose AgentOps - -- You want **automatic learning extraction** without remembering to run a compound step. -- You want **failure prevention before implementation** (pre-mortem, council). -- You want **dependency-tracked issue execution** across complex work. -- You want **strategic goal tracking** and measured progress toward objectives. -- You want a **compiled CLI** with structured operations (search, maturity, config). - ---- - -## Can They Work Together? - -**Yes.** This is one of the better pairings. - -- Use Compound Engineer for its ideation pipeline, configurable review agents, and cross-runtime sync. -- Use AgentOps for memory scoring and injection, validation gates, and tracked execution. - -The knowledge systems are complementary: CE's `docs/solutions/` captures explicit documentation, while AgentOps' `.agents/learnings/` captures automated extractions with maturity scoring. Both could feed into the same codebase without conflict. - ---- - -## The Bottom Line - -| Dimension | Compound Engineer | AgentOps | -|-----------|-------------------|----------| -| **Optimizes** | Workflow breadth (ideation to maintenance) | Repo intelligence (learning to injection) | -| **Knowledge model** | Capture → document → refresh | Extract → score → inject → decay | -| **Review model** | 15 configurable agents per project | Fixed gates (pre-mortem, council, vibe) | -| **Runtime reach** | 10 targets + plugin marketplace | 4 supported runtime paths, no 10-target converter | -| **Best fit** | Teams wanting portable, structured workflow | Long-running codebases needing accumulated intelligence | - -**Compound Engineer is the closest philosophical neighbor in this comparison set.** Both believe in knowledge compounding. The difference: CE compounds through workflow discipline and explicit documentation. AgentOps compounds through automated extraction, scoring, and mechanical injection. - ---- - -
- -[← Back to Comparisons](README.md) · [vs. GSD →](vs-gsd.md) - -
diff --git a/docs/comparisons/vs-everything-claude-code.md b/docs/comparisons/vs-everything-claude-code.md deleted file mode 100644 index 0057df326..000000000 --- a/docs/comparisons/vs-everything-claude-code.md +++ /dev/null @@ -1,187 +0,0 @@ ---- -title: "AgentOps vs everything-claude-code (affaan-m)" -description: "Cross-harness performance system vs. context library — different optimizations." -permalink: /comparisons/agentops-vs-everything-claude-code -last_reviewed: 2026-05-07 ---- - -# AgentOps vs. everything-claude-code - -> **everything-claude-code** ([affaan-m/everything-claude-code](https://github.com/affaan-m/everything-claude-code)) -> positions itself as *"the performance system for AI agent harnesses"* — DRY -> parity across Claude Code, Cursor, Codex, OpenCode, and Gemini, shipping -> **48 subagents and 182 skills** with a single set of definitions kept in sync -> across all five runtimes. - -> **Note on social proof.** The everything-claude-code README claims 140K+ stars -> and 21K+ forks. The entire Claude Code plugin ecosystem is below those numbers -> as of 2026-05-07; AgentOps does not validate that claim. The comparison below -> evaluates the project on its concrete features, not the unverified social-proof -> metric. - ---- - -## At a glance - -| Aspect | everything-claude-code | AgentOps | -|---|---|---| -| **Category** | Cross-harness skill collection | Context library / wiki for agents | -| **Multi-runtime claim** | DRY parity across 5 runtimes (Claude Code, Cursor, Codex, OpenCode, Gemini) | Skills run on 4 runtimes; *and* mix-and-match models per phase within one session | -| **Inventory** | 182 skills, 48 subagents | 73 skills + your accumulated corpus | -| **Persistence** | In-session | Cross-session via `.agents/` corpus | -| **Discipline mechanism** | DRY parity tooling | Multi-model councils + RPI phase contracts | -| **Off-API surface** | None (in-session only) | In-session loop runs off-vendor on your hardware; out-of-session dispatch via the NTM + MCP Agent Mail substrate | -| **Open source** | Yes | Yes (forever) | - -The two products optimize for different axes. everything-claude-code optimizes -for *"the same skill set works in every harness."* AgentOps optimizes for -*"the same model-routed workflow accumulates context across every session."* - ---- - - - -## The model-independent-per-phase distinction - -This is the load-bearing difference, and it is easy to miss. - -everything-claude-code's "cross-harness parity" is **multi-runtime distribution -of a Claude-shaped workflow**: one set of skill and subagent definitions, kept -DRY across five harnesses, so a developer who lives in Cursor today can move to -Codex tomorrow and keep the same skill surface. That is real engineering work — -maintaining parity across five runtime conventions is harder than it sounds — -and the value is portability across editors. - -AgentOps's **per-phase model routing** is a different shape entirely: inside a -single RPI loop, Claude does discovery, Codex implements, fresh Claude -validates, all in one workflow with state preserved across the model -boundaries. The handoff is the feature. - -``` -/rpi "add rate limiting to /login" # in-session skill drives the seven-move operating loop (ao rpi removed in 3.0) -[research/claude] found 3 prior auth changes in .agents/decisions/ -[plan/claude] proposed: token bucket, 5/min per IP, Redis-backed -[pre-mortem/codex] WARN: Redis unreachable case unhandled -[implement/codex] wrote middleware/ratelimit.go, 2 tests -[validate/claude] go test ./... PASS, gate: WARN — missing jitter -[recorded] .agents/runs/2026-05-07-rate-limit/ -``` - -The labels in brackets are the entire pitch made literal. Each phase picks the -model that is best for that phase; the validation phase sees a fresh context -window so it does not rubber-stamp work it just produced. Skills in any -harness — including everything-claude-code's parity-shipped catalog — inherit -the harness's single active model. They do not compose model choices per phase -within one workflow. Nobody else in the ecosystem does this. - -Cross-harness parity and per-phase model routing are not the same capability. -The first is *"my skill set is portable."* The second is *"my workflow uses -multiple models inside one task."* A team can want both, but a buyer should -not assume one implies the other. - ---- - -## When to pick which - -- **Pick everything-claude-code** if your bottleneck is *"I switch between - Claude Code, Cursor, and Codex daily and want the same skills everywhere."* - Cross-harness parity is the headline value, and 182 skills + 48 subagents - is a sizeable catalog out of the box. -- **Pick AgentOps** if your bottleneck is *"my agents keep re-learning the - same lessons because nothing persists between sessions."* The corpus is - the moat; the per-phase model routing and council validation are the - discipline that makes the corpus trustworthy. -- **They are not mutually exclusive.** Install both: use - everything-claude-code's catalog as portable skill inventory across - whichever harness you happen to be in today, and use AgentOps for the - context library that compounds across all of them. - ---- - -## Where everything-claude-code wins - -**Cross-harness DRY parity tooling.** Maintaining the same skill and -subagent definitions across five runtimes — each with its own conventions -for tool surfaces, file locations, and execution model — is real engineering -work. If you genuinely live across multiple harnesses and value the same -skill being available the same way in each one, that is a concrete win -AgentOps does not directly compete with. AgentOps ships skills for four -runtimes (Claude Code, Codex, OpenClaw, Cursor) but does not market its -parity-maintenance pipeline as the headline feature; the headline is the -corpus that the skills produce. - -**Catalog size for a single-developer audience.** 182 skills + 48 subagents -is a larger pre-built catalog than AgentOps's 73 skills, and a buyer whose -question is *"how many skills can I install today"* will see that number -first. - ---- - -## Where AgentOps wins - -**Persistent corpus.** AgentOps's `.agents/` directory is a markdown wiki in -your repo, version-controlled with your code. Every session writes -learnings, decisions, citations, and validation verdicts; future sessions -read them through decay-ranked retrieval (`ao inject`). everything-claude-code -ships skills that run in-session; the work ends when the session does. - -**Multi-model councils.** `/council --mixed` runs Claude and Codex judges -in parallel against one evidence packet and returns structured consensus -before commit. This is a validation primitive that catches "looks good to -one model" failure modes before the bug ships. A skill catalog — however -large or harness-portable — does not provide this surface. - -**Model-independent phase routing inside one RPI loop.** Per the section -above: pick the model that is best for each phase, with state preserved -across the boundaries. This is a workflow-level capability that is -orthogonal to (and does not depend on) cross-harness parity. - -**Off-API, off-vendor operation on your hardware.** The whole loop — -dream / evolve / compile / defrag / forge — runs in session against your -subscription, off any vendor's API. To run it unattended, dispatch it on the -NTM + MCP Agent Mail substrate (an NTM tmux swarm runs ready beads through -worker panes, coordinated by Agent Mail); AgentOps ships no daemon or scheduler -of its own (see -[ADR-0009](../adr/ADR-0009-daemon-deletion-in-session-only.md)). The corpus -compounds while you sleep. everything-claude-code is in-session by design with -no off-vendor out-of-session story. - ---- - -## Both are open source - -Both projects are open source. AgentOps is open source forever — the -corpus that compounds in *your* repo is yours, the schema is portable, -and the discipline survives any single vendor's roadmap. If you are -evaluating durability of either project, the source code is the receipt; -inflated star counts and other unverified metrics are not. - ---- - -## Bottom line - -Different optimization targets, not direct competition. - -everything-claude-code is selling **cross-harness portability**: one skill -set, five runtimes, kept in DRY parity by the maintainers' tooling. If -your friction is editor switching, that is real value. - -AgentOps is selling **corpus discipline + per-phase model routing**: the -operational layer that turns each session's research, decisions, -validations, and learnings into a markdown wiki your agents read on the -next session, and the workflow primitive that lets you compose Claude, -Codex, and other models per phase inside one task. - -A buyer who lives across harnesses and does not yet care about persistence -should pick everything-claude-code. A buyer who is bottlenecked on -"my agents forget what we learned last week" should pick AgentOps. A -buyer with both bottlenecks should install both — they sit at different -layers of the stack. - ---- - -
- -[← Back to Comparisons](README.md) - -
diff --git a/docs/comparisons/vs-gsd.md b/docs/comparisons/vs-gsd.md deleted file mode 100644 index 264b1245f..000000000 --- a/docs/comparisons/vs-gsd.md +++ /dev/null @@ -1,339 +0,0 @@ ---- -title: "AgentOps vs GSD (Get Shit Done) — Detailed Comparison" -description: "How AgentOps compares to GSD for AI coding agents. GSD optimizes within sessions with fresh context per agent. AgentOps carries knowledge between sessions with a compounding flywheel." -permalink: /comparisons/agentops-vs-gsd ---- - -# AgentOps vs GSD (Get Shit Done) - -> **GSD v1.34** is a full-featured spec-driven development framework for AI coding agents. It installs as slash commands into 7 runtimes (Claude Code, Gemini CLI, OpenCode, Codex, Copilot, Cursor, Antigravity) and solves "context rot" by spawning fresh-context subagents for each task. Trusted by engineers at Amazon, Google, Shopify, and Webflow. -> -> *Comparison updated April 2026. See the [GSD repo](https://github.com/glittercowboy/get-shit-done) for current features.* - ---- - -## At a Glance - -| Aspect | GSD | AgentOps | -|--------|-----|----------| -| **Philosophy** | "Ship fast — fresh context per agent" | "Operational layer for coding agents; technically a context compiler" | -| **Core strength** | Multi-agent orchestration with context isolation, multi-runtime support | Cross-session memory, validation gates, knowledge flywheel | -| **GitHub** | glittercowboy/get-shit-done | boshu2/agentops | -| **Latest** | v1.34.2 (April 2026) | v2.39.0 (April 2026) | -| **Scale** | 53 commands, 46 workflows, 16 agents | 50+ skills, compiled CLI, hooks, schemas | -| **Primary use** | Spec-driven development with phased execution | Ongoing codebase work with persistent memory | - -### Three-Layer Comparison - -| Product Layer | GSD | AgentOps | -|---------------|----:|----------| -| **Context Compiler** (L1) | Fresh context per agent (isolation, no retrieval) | Decay-ranked retrieval + fresh context per wave (isolation AND memory) | -| **Validation Gates** (L2) | Post-build review agent | Pre-build `/pre-mortem` + post-build `/vibe` with multi-model council | -| **Knowledge Flywheel** (L3) | No persistent learning across sessions | Extract → score → promote → inject; session 50 knows what session 1 learned | - ---- - -## What GSD Does Well - -### 1. Fresh Context Per Agent - -GSD's core innovation. Every spawned agent gets a clean 200K context window. Orchestrators stay thin, agents are disposable. This eliminates context rot — the quality degradation that happens as an AI fills its context window during long sessions. - -### 2. Wave-Based Parallel Execution - -Plans are grouped into dependency waves. Plans within a wave run in parallel (each with a fresh agent), waves run sequentially. Includes STATE.md file locking with atomic creation and spin-wait jitter. - -``` -Wave 1: [Plan A, Plan B, Plan C] ← parallel, fresh 200K each - ↓ (all complete) -Wave 2: [Plan D, Plan E] ← parallel, depends on Wave 1 - ↓ (all complete) -Wave 3: [Plan F] ← sequential, depends on Wave 2 -``` - -### 3. Model Cost Tiers - -Four profiles (quality/balanced/budget/inherit) with per-agent model assignments. Each profile maps agents to opus/sonnet/haiku. This means routine plan-checking can run on budget models while critical execution stays on quality models. - -### 4. Auto-Repair on Task Failure - -When a task fails during execution, GSD auto-classifies the failure as RETRY (with adjustment), DECOMPOSE (break into sub-steps), or PRUNE (remove and escalate). Budget-controlled with a default of 2 attempts. This is structured recovery, not blind retries. - -### 5. Comprehensive Validation Pipeline - -Not just "human verification" anymore. GSD v1.27 has: -- 8-dimension plan checker (max 3 iterations) -- Nyquist validation audit (test coverage mapping) -- Post-execution verification agent -- User acceptance testing with auto-diagnosis -- Cross-phase verification debt audit - -### 6. Seven Runtime Support - -Installs to Claude Code, Gemini CLI, OpenCode, Codex, Copilot, Cursor, and Antigravity. The installer transforms file content per runtime at install time (tool name mapping, agent frontmatter, hook events, path conventions). - -### 7. Advisor Mode and Fast Path (New in v1.34) - -`/gsd:advisor` provides research-backed discussion without execution. `/gsd:fast` skips planning for trivial tasks. Multi-repo workspace support added for cross-project orchestration. - -### 7. Deep State Management - -Full `.planning/` directory with 20+ artifact types: -- PROJECT.md, REQUIREMENTS.md, ROADMAP.md, STATE.md -- Per-phase directories with research, plans, summaries, verification, UAT -- Session handoff (HANDOFF.json, continue-here.md) -- Persistent threads, seeds, debug knowledge base, todos - ---- - -## Where GSD Falls Short - -### No Cross-Session Learning - -GSD has persistence (state files, handoffs, threads) but no knowledge flywheel. There is no mechanism to extract what was learned in one session and inject it into the next. Every session starts with the same agent intelligence — the system does not get smarter over time. - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ GSD │ -│ │ -│ Session 1: discuss → plan → execute → verify → Done │ -│ ↓ │ -│ (state saved) │ -│ ↓ │ -│ Session 2: resume-work → (same state, same intelligence) │ -│ │ -│ Session 100: (agents are no smarter than session 1) │ -│ │ -└─────────────────────────────────────────────────────────────────┘ - -┌─────────────────────────────────────────────────────────────────┐ -│ AGENTOPS │ -│ │ -│ Session 1: research → plan → pre-mortem → crank → vibe → retro │ -│ ↓ │ -│ (learnings extracted) │ -│ ↓ │ -│ (scored and stored) │ -│ │ -│ Session 2: (inject prior knowledge) → better starting point │ -│ │ -│ Session 100: (agent is a domain expert with scored knowledge) │ -│ │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### No Strategic Goals or Direction - -GSD executes phases within a project but has no mechanism for measuring progress toward higher-level objectives. No equivalent to GOALS.md, `ao goals measure`, or `/evolve`. - -### No Pre-Implementation Failure Prevention - -GSD validates *after* execution (verify-work, UAT). The plan checker validates plan quality but does not simulate failure modes before implementation begins. AgentOps runs `/pre-mortem` to catch issues before code is written. - -### No Issue Graph or Dependency Tracking - -GSD has wave-based parallelism for plans within a phase, but no cross-phase issue graph. There is no equivalent to beads (issue tracking with dependencies), which means no mechanical way to track blocked work across phases. - -### No Multi-Model Validation Council - -GSD's review command supports cross-AI peer review (Gemini, Claude, Codex), but this is a review tool, not a multi-perspective validation council that runs adversarial analysis from different viewpoints simultaneously. - ---- - -## Feature Comparison - -| Feature | GSD | AgentOps | Winner | -|---------|:---:|:--------:|:------:| -| Multi-runtime support | ✅ 7 runtimes | ⚠️ 4 supported runtime paths, less broad than GSD | GSD | -| Fresh context per agent | ✅ Core design | ⚠️ Swarm workers | GSD | -| Model cost tiers | ✅ 4 profiles | ❌ Not yet | GSD | -| Auto-repair on failure | ✅ RETRY/DECOMPOSE/PRUNE | ⚠️ Crank retries | GSD | -| Context rot detection | ✅ Hooks at 35%/25% | ❌ Not yet | GSD | -| Prompt injection defense | ✅ Advisory hook | ❌ Not yet | GSD | -| Wave-based parallelism | ✅ Built in | ✅ Crank waves | Tie | -| Plan validation | ✅ 8-dimension checker | ✅ Pre-mortem + council | Tie | -| Human-in-loop gates | ✅ Configurable gates | ✅ Multiple gates | Tie | -| State persistence | ✅ .planning/ directory | ✅ .agents/ directory | Tie | -| **Cross-session learning** | ❌ No flywheel | ✅ Extract → score → inject | **AgentOps** | -| **Knowledge maturity** | ❌ No scoring | ✅ Maturity tracking + decay | **AgentOps** | -| **Pre-mortem simulation** | ❌ Post-execution only | ✅ Before implementation | **AgentOps** | -| **Multi-model council** | ❌ Sequential review | ✅ Multi-perspective | **AgentOps** | -| **Issue graph execution** | ❌ Phase-scoped waves | ✅ Beads + dependencies | **AgentOps** | -| **Strategic goals** | ❌ No goal tracking | ✅ GOALS.md + evolve | **AgentOps** | -| **Compiled CLI** | ❌ Node.js tools | ✅ Go binary (ao) | **AgentOps** | - ---- - -## Workflow Comparison - -### GSD Workflow - -``` -/gsd:new-project → PROJECT.md, REQUIREMENTS.md, ROADMAP.md, config.json - ↓ -/gsd:discuss-phase → Capture decisions (CONTEXT.md) - ↓ -/gsd:plan-phase → Research → plan → 8-dimension verify (max 3 iterations) - ↓ -/gsd:execute-phase → Wave-based parallel execution (fresh agent per plan) - ↓ └── Node repair on failure (RETRY/DECOMPOSE/PRUNE) -/gsd:verify-work → UAT with auto-diagnosis - ↓ -/gsd:ship → Create PR from phase work - ↓ - [next phase or complete-milestone] -``` - -### AgentOps Workflow - -``` -/research → Explore codebase + inject prior knowledge - ↓ -/plan → Break into dependency-tracked issues (beads) - ↓ -/pre-mortem → Simulate failure modes before building - ↓ -/crank → Execute unblocked waves → validate → commit - ↓ -/vibe → Multi-aspect code validation (council optional) - ↓ -/post-mortem → Extract learnings → score → store for next session -``` - ---- - -## Architecture Comparison - -| Aspect | GSD | AgentOps | -|--------|-----|----------| -| **Commands** | 53 prompt-based slash commands | 50+ skill definitions | -| **Agents** | 16 specialized (fresh context each) | Skill-driven (swarm for parallelism) | -| **CLI tooling** | Node.js (`gsd-tools.cjs`, 15 modules) | Go binary (`ao`, structured subcommands) | -| **Hooks** | 5 JS hooks (statusline, context monitor, prompt guard, workflow guard, update check) | Shell hooks (session lifecycle, tool gates, knowledge injection) | -| **State** | `.planning/` (Markdown + JSON) | `.agents/` (Markdown + JSON) | -| **Config** | `.planning/config.json` (40+ options) | `.agentops.json` + GOALS.md | -| **Install** | npm package, 3000-line installer | Shell script + Go binary | -| **Parallelism** | Wave-based with file locking | Wave-based via crank + swarm | - ---- - -## Overhead Comparison - -``` - SETUP TIME ONGOING OVERHEAD - ══════════ ════════════════ - -GSD: ████████░░░░░░░░ ████████░░░░░░░░ - (npm install + init) (moderate — .planning/ management) - -AgentOps: ████████░░░░░░░░ ████████░░░░░░░░ - (install + init) (moderate — hooks + .agents/) - - - SESSION VALUE LONG-TERM VALUE - ═════════════ ═══════════════ - -GSD: ████████████████ ████████░░░░░░░░ - (strong execution) (state persists, no learning) - -AgentOps: ████████████████ ████████████████ - (strong execution) (knowledge compounds) -``` - -**Trade-off:** GSD optimizes for execution quality per session. AgentOps optimizes for cumulative intelligence across sessions. - ---- - -## Use Case Fit - -### GSD is Best For - -| Use Case | Why | -|----------|-----| -| Greenfield projects | Strong project setup + phased execution | -| Multi-runtime teams | 7 runtimes with one install | -| Cost-sensitive work | Model cost tiers control spend | -| Complex single-phase work | Wave parallelism + auto-repair | -| Teams standardizing process | Clear phases with configurable gates | - -### AgentOps is Best For - -| Use Case | Why | -|----------|-----| -| Long-running codebases | Knowledge flywheel compounds value | -| Repeated maintenance | Learns from past sessions | -| Complex multi-phase work | Issue graph + dependency execution | -| Risk-averse engineering | Pre-mortem + council + vibe gates | -| Strategic direction | GOALS.md + evolve loop | - ---- - -## When to Choose GSD - -- You work across **multiple AI runtimes** and need one workflow -- You want **model cost control** at the per-agent level -- Your work is **project-scoped** (clear start and end) -- You value **fresh context per agent** for quality in long sessions -- You want **auto-repair** when tasks fail during execution - -## When to Choose AgentOps - -- You work on the **same codebase** across many sessions -- You want the system to **get smarter over time** -- You want **failure prevention before implementation**, not just verification after -- You want **dependency-tracked issue execution** across work phases -- You value **strategic goal tracking** and measured progress - ---- - -## Can They Work Together? - -**Partially.** GSD and AgentOps both manage state directories and workflow orchestration, so running both simultaneously would create friction. However: - -- GSD's fresh-context-per-agent pattern is a technique AgentOps' swarm could adopt -- GSD's model cost tiers solve a problem AgentOps does not yet address -- AgentOps' knowledge flywheel fills GSD's biggest gap (no cross-session learning) - -The most practical combination: use GSD for greenfield projects where you need fast phased execution, then bring AgentOps in when the project enters maintenance and long-term development where accumulated knowledge matters. - ---- - -## The Bottom Line - -| Dimension | GSD | AgentOps | -|-----------|-----|----------| -| **Philosophy** | Fresh context, fast execution | Knowledge compounds | -| **Overhead** | Moderate | Moderate | -| **Persistence** | State files (no learning) | Knowledge flywheel | -| **Validation** | 8-dimension plan check + UAT | Pre-mortem + council + vibe | -| **Parallelism** | Wave-based, fresh agents | Wave-based, swarm workers | -| **Cost control** | 4-tier model profiles | Not yet | -| **Best for** | Strong execution per session | Cumulative intelligence across sessions | - -**GSD is a serious framework for structured AI-assisted development.** -**AgentOps differentiates on the knowledge flywheel — the system that makes every session smarter than the last.** - ---- - -## The Honest Assessment - -**GSD is not a lightweight tool anymore.** It is a comprehensive development framework with 53 commands, 16 agents, wave-based parallelism, auto-repair, model cost tiers, and deep state management. Dismissing it as "simple meta-prompting" is inaccurate. - -**Where GSD wins:** Execution quality within a session. Fresh context per agent, cost control, auto-repair, and 7-runtime portability. - -**Where AgentOps wins:** Intelligence across sessions. The knowledge flywheel (extract, score, inject, decay) has no equivalent in GSD. After 50 sessions on the same codebase, AgentOps is operating with accumulated domain knowledge while GSD agents start fresh every time. - -``` -Session 1: GSD and AgentOps roughly equal -Session 10: AgentOps has a library of scored learnings -Session 50: AgentOps agents get injected domain expertise; GSD agents do not -Session 100: AgentOps is a domain expert; GSD is still starting from its .planning/ state -``` - ---- - -
- -[← vs. SDD](vs-sdd.md) · [Back to Comparisons](README.md) - -
diff --git a/docs/comparisons/vs-sdd.md b/docs/comparisons/vs-sdd.md deleted file mode 100644 index 80c53caaf..000000000 --- a/docs/comparisons/vs-sdd.md +++ /dev/null @@ -1,306 +0,0 @@ ---- -title: "AgentOps vs SDD (Spec-Driven Development) — Detailed Comparison" -description: "How AgentOps compares to Spec-Driven Development tools (cc-sdd, GitHub Spec Kit). SDD captures what you decided. AgentOps captures what you learned." -permalink: /comparisons/agentops-vs-sdd ---- - -# AgentOps vs SDD (Spec-Driven Development) - -> **Spec-Driven Development** has gone mainstream in 2026. Major tools include **cc-sdd v2.0** (8 agents, 13 languages), **GitHub Spec Kit v0.5.0** (now industry standard), and **AWS Kiro** (spec-driven IDE). Tessl is pushing "spec-as-source" for infrastructure-as-code. - ---- - -## At a Glance - -| Aspect | SDD Tools | AgentOps | -|--------|-----------|----------| -| **Philosophy** | "Spec is the source of truth" | "Operational layer for coding agents; technically a context compiler" | -| **Core strength** | Structured requirements, cross-platform | Cross-session memory, learning | -| **Primary tools** | cc-sdd, spec-kit, SDD_Flow | AgentOps plugin + CLI | -| **Primary use** | Spec-first development | Ongoing codebase work | - -### Three-Layer Comparison - -| Product Layer | SDD Tools | AgentOps | -|---------------|----------:|----------| -| **Context Compiler** (L1) | Spec files as static context | Specs + learnings + decisions + failures, decay-ranked and injected per phase | -| **Validation Gates** (L2) | Spec conformance checks | Multi-model councils that validate beyond spec (security, architecture, risk) | -| **Knowledge Flywheel** (L3) | No learning beyond specs | Failures, retros, and prevention rules compound across sessions | - ---- - -## The SDD Landscape - -### cc-sdd -NPM package (`npx cc-sdd@latest`) that installs unified SDD workflow across 7+ AI coding agents: -- Claude Code, Cursor, Gemini CLI, Codex CLI, GitHub Copilot, Qwen Code, Windsurf - -### GitHub Spec Kit -GitHub's official SDD implementation: -- CLI-based workspace setup -- Slash commands for spec management -- Integration with GitHub ecosystem - -### SDD_Flow -Comprehensive framework with: -- Hybrid Waterfall-Agile methodology -- Documentation templates -- Prompt library - ---- - -## What SDD Does Well - -### 1. Spec as Source of Truth -The specification becomes an executable contract: - -``` -SDD Workflow: - Requirements → Spec (markdown) → Human Review → Implementation → Validation - ↑ - Central artifact -``` - -### 2. Cross-Platform Compatibility -cc-sdd works with 7+ different AI coding agents. Write your workflow once, use it everywhere. - -### 3. Structured Requirements -Formalizes the "vibe coding" chaos into structured documents: -- Requirements spec -- Design spec -- Task breakdown -- Implementation plan - -### 4. Human-in-the-Loop Gates -Built-in approval points where humans review specs before implementation proceeds. - ---- - -## Where SDD Falls Short - -### Specs Are Static, Not Learning - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ SDD │ -│ │ -│ Session 1: Write spec → Implement → Done │ -│ ↓ │ -│ (spec saved) │ -│ │ -│ Session 2: Write spec → Implement → Done │ -│ ↓ │ -│ (different spec, no learning from Session 1) │ -│ │ -└─────────────────────────────────────────────────────────────────┘ - -┌─────────────────────────────────────────────────────────────────┐ -│ AGENTOPS │ -│ │ -│ Session 1: Plan → Implement → Extract learnings │ -│ ↓ │ -│ (patterns saved) │ -│ │ -│ Session 2: Inject learnings → Plan → Implement → More learnings│ -│ ↑ ↓ │ -│ └────────────── compounds ──────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────────┘ -``` - -**SDD captures *what* you planned. AgentOps captures *what you learned*.** - -### No Failure Prevention - -SDD validates specs against implementation. It doesn't simulate failures *before* building. - -``` -SDD: - Spec → Implement → Check against spec - -AgentOps: - Plan → Pre-Mortem (10 failure modes) → Implement → Validate → Extract -``` - -### Limited Validation Depth - -SDD asks: "Does implementation match spec?" - -AgentOps `/vibe` asks 8 questions: -1. Does code match spec? (semantic) -2. Is it secure? -3. Is it quality code? -4. Does it follow architecture? -5. Is complexity manageable? -6. Will it perform well? -7. Is it AI slop? -8. Is it accessible? - ---- - -## Feature Comparison - -| Feature | SDD Tools | AgentOps | Winner | -|---------|:---------:|:--------:|:------:| -| Spec-first workflow | ✅ Core focus | ✅ Via `/plan` | SDD | -| Cross-platform | ✅ 7+ agents | ⚠️ 4 supported runtime paths | SDD | -| Structured templates | ✅ Comprehensive | ⚠️ Via standards | SDD | -| Human approval gates | ✅ Built-in | ✅ 4 gates | Tie | -| **Cross-session memory** | ❌ Specs only | ✅ Learnings + patterns | **AgentOps** | -| **Knowledge compounding** | ❌ No | ✅ Escape velocity | **AgentOps** | -| **Pre-mortem simulation** | ❌ No | ✅ 10 failure modes | **AgentOps** | -| **8-aspect validation** | ❌ Spec match only | ✅ Semantic + security + ... | **AgentOps** | -| **Scientific foundation** | ❌ Methodology | ✅ Peer-reviewed | **AgentOps** | - ---- - -## Workflow Comparison - -### SDD Workflow (cc-sdd) - -``` -/sdd:requirements → Analyze and document requirements - ↓ -/sdd:design → Create design specification - ↓ -/sdd:tasks → Break into implementation tasks - ↓ -/sdd:implement → Execute tasks - ↓ - Done (specs archived, no learning extracted) -``` - -### AgentOps Workflow - -``` -/research → Explore codebase + inject prior knowledge - ↓ -/plan → Break into tracked issues (spec-like) - ↓ -/pre-mortem → Simulate 10 failure modes - ↓ -/crank → Implement → validate → commit - ↓ -/post-mortem → Validate + extract learnings (FOR NEXT TIME) -``` - -**Key difference:** AgentOps extracts *learnings* (patterns, decisions, failures), not just specs. - ---- - -## What Gets Captured - -### SDD Captures - -``` -project/ -├── specs/ -│ ├── requirements.md # What we need -│ ├── design.md # How we'll build it -│ └── tasks.md # What to implement -└── src/ - └── ... -``` - -### AgentOps Captures - -``` -.agents/ -├── learnings/ # "Token refresh bugs usually stem from..." -├── patterns/ # "Here's how we handle retries in this codebase" -├── research/ # Deep exploration outputs -├── specs/ # Validated specifications -├── retros/ # What worked, what didn't -└── pre-mortems/ # Failure simulations -``` - -**SDD:** Documents -**AgentOps:** Documents + patterns + learnings + retrospectives - ---- - -## The Three Levels of SDD - -Martin Fowler identifies three levels: - -| Level | Description | SDD Tools | AgentOps | -|-------|-------------|:---------:|:--------:| -| **Spec-first** | Write spec before code | ✅ | ✅ | -| **Spec-anchored** | Keep spec after completion | ✅ | ✅ | -| **Spec-as-source** | Spec is the only source humans edit | ✅ | ❌ | - -AgentOps doesn't aim for "spec-as-source" — it captures *learnings*, not just specs. - ---- - -## When to Choose SDD Tools - -- You want **spec-first development** as the core methodology -- You work across **multiple AI coding agents** (not just Claude) -- **Documentation** is your primary deliverable -- You want **structured templates** for requirements/design -- Your sessions are **independent** (no need for cross-session learning) - -## When to Choose AgentOps - -- You work on the **same codebase** repeatedly -- You want to capture **learnings**, not just specs -- You want **failure prevention** before building -- You want **semantic validation** beyond spec matching -- You value **compounding knowledge** over time - ---- - -## Can They Work Together? - -**Yes, naturally:** - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ SDD + AGENTOPS │ -│ │ -│ SDD handles: │ -│ └── Requirements → Design → Tasks (structured specs) │ -│ │ -│ AgentOps handles: │ -│ └── Pre-mortem (failure simulation) │ -│ └── /vibe (8-aspect validation) │ -│ └── /post-mortem (learning extraction) │ -│ └── Cross-session memory │ -│ │ -│ Combined flow: │ -│ SDD specs → AgentOps pre-mortem → Implement → AgentOps vibe │ -│ ↓ │ -│ Extract learnings │ -│ ↓ │ -│ Next session │ -└─────────────────────────────────────────────────────────────────┘ -``` - -- **SDD** provides the structured specification methodology -- **AgentOps** provides the learning and validation layer - ---- - -## The Bottom Line - -| Dimension | SDD Tools | AgentOps | -|-----------|-----------|----------| -| **Central artifact** | Specification | Knowledge | -| **What persists** | Documents | Learnings + patterns | -| **Validation** | Spec match | 8-aspect semantic | -| **Learning** | None | Compounds over time | -| **Cross-platform** | 7+ agents | 4 supported runtime paths | - -**SDD captures *what you decided*.** -**AgentOps captures *what you learned*.** - -**Best approach:** Use SDD for specs, AgentOps for learning. - ---- - -
- -[← vs. Claude-Flow](vs-claude-flow.md) · [Back to Comparisons](README.md) · [vs. GSD →](vs-gsd.md) - -
diff --git a/docs/comparisons/vs-superpowers.md b/docs/comparisons/vs-superpowers.md deleted file mode 100644 index 164e69033..000000000 --- a/docs/comparisons/vs-superpowers.md +++ /dev/null @@ -1,213 +0,0 @@ ---- -title: "AgentOps vs Superpowers — Detailed Comparison" -description: "How AgentOps compares to Superpowers for AI coding agents. Superpowers enforces strict TDD. AgentOps adds cross-session memory and multi-model validation councils." -permalink: /comparisons/agentops-vs-superpowers -last_reviewed: 2026-05-07 ---- - -# AgentOps vs Superpowers - -> **Superpowers** is a popular coding agent plugin known for disciplined TDD workflows and autonomous operation (29K+ GitHub stars). It now ships via the official Anthropic marketplace (`claude-plugins-official`), and supports Kimi Code CLI, OpenClaw, and Mistral Vibe. The plugin is a lightweight shim that auto-updates skills on session start; skills live in a dedicated repo (obra/superpowers-skills). -> -> *Comparison updated 2026-05-07. See [Superpowers repo](https://github.com/obra/superpowers) for current features.* - ---- - -## At a Glance - -| Aspect | Superpowers | AgentOps | -|--------|-------------|----------| -| **Philosophy** | "Disciplined senior engineer" | "Operational layer for coding agents; technically a context compiler" | -| **Core strength** | TDD, planning, autonomous hours | Cross-session memory, learning | -| **GitHub stars** | 29,000+ | Growing | -| **Primary use** | Greenfield development | Ongoing codebase work | - -### Three-Layer Comparison - -| Product Layer | Superpowers | AgentOps | -|---------------|------------:|----------| -| **Context Compiler** (L1) | Session-local planning context (no cross-session retrieval) | Decay-ranked retrieval of prior learnings + phase-scoped assembly | -| **Validation Gates** (L2) | TDD as implicit gate (tests pass or fail) | TDD + multi-model councils with independent judges and pre-build validation | -| **Knowledge Flywheel** (L3) | No persistent learning between sessions | Extract → score → promote → inject; autonomous overnight compounding | - ---- - -## What Superpowers Does Well - -### 1. TDD Enforcement -Superpowers enforces true red/green TDD. Write tests first, implement second. No shortcuts. - -``` -Superpowers TDD Flow: - Write failing test → Run test (red) → Implement → Run test (green) → Refactor -``` - -### 2. Planning Mode -The `/superpowers:brainstorm` and `/superpowers:write-plan` commands create structured implementation plans. Claude asks intelligent questions to flesh out details. - -### 3. Autonomous Operation -Superpowers can work autonomously for hours without drifting. It spawns subagents for context gathering and uses test suites to validate its work. - -### 4. YAGNI/DRY Principles -Built-in enforcement of software engineering best practices. No over-engineering, no repetition. - ---- - -## Where Superpowers Falls Short - -### No Cross-Session Memory - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ SUPERPOWERS │ -│ │ -│ Session 1: Debug auth bug [learned: token refresh] │ -│ Session 2: Debug auth bug [learned: token refresh] │ -│ Session 3: Debug auth bug [learned: token refresh] │ -│ ↑ │ -│ Same learning, every time │ -└─────────────────────────────────────────────────────────────────┘ - -┌─────────────────────────────────────────────────────────────────┐ -│ AGENTOPS │ -│ │ -│ Session 1: Debug auth bug [learned: token refresh] │ -│ ↓ (stored) │ -│ Session 2: Auth issue? "I remember this pattern" │ -│ ↓ (reinforced) │ -│ Session 3: Auth? *instant recall* │ -│ │ -└─────────────────────────────────────────────────────────────────┘ -``` - -**Superpowers resets every session.** Your agent debugs the same issues repeatedly with no memory of past solutions. - -**The mechanism is a wiki.** AgentOps's `.agents/` directory is markdown in your repo, version-controlled with your code, that agents read and contribute to. Wikis are a 25-year-old idea — that is the deflationary positioning. The novel part is not the format; it is making one for agents to traverse, and making the discipline of maintenance mechanical so it actually happens. Superpowers does not implement a corpus of this kind; learnings live in the session and end with it. - -### No Failure Prevention - -Superpowers validates *after* implementation (TDD). AgentOps adds *pre-implementation* validation: - -``` -Superpowers: - Plan → Implement → Test (catch failures here) - -AgentOps: - Plan → Pre-Mortem (catch failures here) → Implement → Test → Post-Mortem -``` - -The `/pre-mortem` skill simulates 10 failure scenarios *before* you write code. Cheaper to catch bad designs early. - -### Limited Validation Scope - -Superpowers relies on your test suite. If tests pass, it's "done." - - -AgentOps `/vibe` validates 8 aspects beyond tests: -- Semantic correctness (does code match spec?) -- Security vulnerabilities -- Code quality / smells -- Architecture violations -- Complexity metrics -- Performance issues -- AI "slop" detection -- Accessibility - ---- - -## Feature Comparison - -| Feature | Superpowers | AgentOps | Winner | -|---------|:-----------:|:--------:|:------:| -| TDD enforcement | ✅ Strict | ✅ Supported | Superpowers | -| Planning workflow | ✅ Excellent | ✅ `/plan` + `/pre-mortem` | Tie | -| Subagent spawning | ✅ Built-in | ✅ 20 expert agents | Tie | -| Autonomous work | ✅ Hours | ✅ `/crank` loop | Tie | -| YAGNI/DRY | ✅ Enforced | ⚠️ Via standards | Superpowers | -| **Cross-session memory** | ❌ None | ✅ Git-persisted | **AgentOps** | -| **Knowledge compounding** | ❌ No | ✅ Escape velocity | **AgentOps** | -| **Pre-mortem** | ❌ No | ✅ 10 failure modes | **AgentOps** | -| **8-aspect validation** | ❌ Tests only | ✅ Semantic + security + ... | **AgentOps** | -| **Scientific foundation** | ❌ Best practices | ✅ Peer-reviewed | **AgentOps** | - ---- - -## Workflow Comparison - -### Superpowers Workflow - -``` -/superpowers:brainstorm → Refine requirements interactively - ↓ -/superpowers:write-plan → Create implementation plan - ↓ -/superpowers:execute-plan → Execute in batches with TDD - ↓ - Done -``` - -### AgentOps Workflow - -``` -/research → Explore codebase + inject prior knowledge - ↓ -/plan → Break into tracked issues - ↓ -/pre-mortem → Simulate 10 failure modes (BEFORE building) - ↓ -/crank → Implement → validate → commit loop - ↓ -/post-mortem → Validate + extract learnings (FOR NEXT TIME) -``` - -**Key difference:** AgentOps has gates *before* and *after* implementation, and learnings persist to future sessions. - ---- - -## When to Choose Superpowers - -- You're doing **greenfield development** (no prior context to leverage) -- You want **strict TDD enforcement** as the primary quality gate -- Your sessions are **independent** (different features each time) -- You prefer a **battle-tested** solution (29K stars, marketplace approved) - -## When to Choose AgentOps - -- You work on the **same codebase** over many sessions -- You want your agent to **remember past bugs and solutions** -- You want **failure prevention** before building, not just testing after -- You want **semantic validation** beyond just "tests pass" -- You value **compounding knowledge** over time - ---- - -## Can They Work Together? - -**Partially.** Both have planning workflows, so you'd need to pick one. But: - -- Use Superpowers for TDD enforcement during implementation -- Use AgentOps for cross-session knowledge capture - -The overlap is in planning. The differentiation is in memory. - ---- - -## The Bottom Line - -| Dimension | Superpowers | AgentOps | -|-----------|-------------|----------| -| **Optimizes** | Quality within session | Learning across sessions | -| **Quality gate** | Tests | 8-aspect semantic validation | -| **Failure catch** | After implementation | Before and after | -| **Knowledge** | Ephemeral | Compounds | - -**Superpowers makes Claude a disciplined engineer *today*.** -**AgentOps makes Claude a domain expert *over time*.** - ---- - -
- -[← Back to Comparisons](README.md) · [vs. Claude-Flow →](vs-claude-flow.md) - -
diff --git a/docs/comparisons/vs-tons-of-skills.md b/docs/comparisons/vs-tons-of-skills.md deleted file mode 100644 index eec7aaf75..000000000 --- a/docs/comparisons/vs-tons-of-skills.md +++ /dev/null @@ -1,92 +0,0 @@ ---- -title: "AgentOps vs Tons-of-Skills (jeremylongshore/claude-code-plugins-plus-skills)" -description: "Volume marketplace vs. context library — different categories, different buyers." -permalink: /comparisons/agentops-vs-tons-of-skills -last_reviewed: 2026-05-07 ---- - -# AgentOps vs. Tons-of-Skills - -> **Tons-of-Skills** ([jeremylongshore/claude-code-plugins-plus-skills](https://github.com/jeremylongshore/claude-code-plugins-plus-skills)) -> is a volume marketplace: **425 plugins, 2,810 skills, 200 agents**, a CCPI -> package manager, and a 100-point validation grading rubric. It is a different -> category from AgentOps. This page exists to clarify the comparison for buyers -> shopping both. - ---- - -## At a glance - -| Aspect | Tons-of-Skills | AgentOps | -|---|---|---| -| **Category** | Skills marketplace | Context library / wiki for agents | -| **Inventory claim** | 2,810 skills, 425 plugins, 200 agents | 73 skills + your accumulated corpus | -| **Buyer pitch** | "We have the most stuff" | "You build the moat" | -| **Validation** | 100-point CCPI grading rubric | Multi-model councils + validation gates | -| **Distribution** | CCPI package manager | Native marketplace per runtime + `curl \| bash` install | -| **Persistence** | Per-skill, in-session | Cross-session via `.agents/` corpus | -| **Open source** | Yes | Yes (forever) | - -The two products answer different questions. Tons-of-Skills answers *"what skills can I install?"* AgentOps answers *"how do I make my agents remember what we already learned?"* - ---- - -## When to pick which - -- **Pick Tons-of-Skills** if you want a bulk-buy of pre-built skills covering many domains, with a package manager and a published grading rubric to filter by. -- **Pick AgentOps** if you want to build a context library that compounds across sessions — a wiki for your agents, persisted in your repo, that grows with every run. -- **They are not mutually exclusive.** Install both. Use Tons-of-Skills as a skill catalog for breadth; use AgentOps for the corpus discipline that turns one-off skill outputs into accumulated knowledge. - -This is the cleanest framing: a marketplace and a context library are complementary tools. The marketplace gets you skills on day one. The context library is what makes day 100 different from day 1. - ---- - -## Where Tons-of-Skills wins - -**Inventory breadth.** 2,810 skills is the largest catalog in the Claude Code ecosystem. If your bottleneck is "I need a skill for X and don't want to write one," Tons-of-Skills has the highest probability of already having it. AgentOps ships 73 skills focused on the operational layer (research, plan, validate, harvest, council, etc.) — the inventories don't overlap heavily. - -**Validation grading rigor.** The 100-point CCPI grading rubric is a public scoring system applied per-skill. That is a real artifact: a published, comparable score across thousands of skills. AgentOps does not grade skills on a 100-point scale; its quality posture is multi-model council consensus and validation gates run *on the agent's output*, not on the skill definition itself. Different surface, different rigor. - -**Package management.** CCPI is a dedicated package manager for skills. AgentOps installs via the Claude Code, Codex, OpenClaw, and Cursor native marketplaces plus a `curl | bash` script — there is no AgentOps-specific package manager because AgentOps treats the runtime's marketplace as the distribution layer. - ---- - - - -## Where AgentOps wins - -**Persistent corpus.** AgentOps's `.agents/` directory is a markdown wiki in your repo, version-controlled with your code. Learnings, decisions, citations, and validation verdicts are extracted and stored every session, then injected into future sessions via decay-ranked retrieval. Tons-of-Skills ships skills; the skill output ends with the session. AgentOps ships a bookkeeping schema that turns each session's output into durable corpus that the next session reads. - -**Multi-model councils.** `/council --mixed` runs Claude and Codex judges in parallel against one evidence packet, returning structured consensus before commit. This is a validation primitive, not a skill — it is the thing that catches "looks good to one model" before the bug ships. Tons-of-Skills's grading rubric scores skills before you install them; AgentOps's councils score *the work the agent just did* before it lands. - -**Model-independent phase routing.** Inside a single RPI loop, AgentOps runs Claude for research, Codex for implementation, fresh Claude for validation, with state preserved across the boundaries. Skills in any marketplace inherit the harness's model — they don't compose model choices per phase. This is a workflow-level capability, orthogonal to skill inventory. - -**Off-API, off-vendor operation.** The whole loop — dream / evolve / compile / defrag / forge — runs in session on your hardware against your subscription, off any vendor's API. To run it unattended, dispatch it on the out-of-session substrate (NTM tmux swarms + MCP Agent Mail); AgentOps ships no daemon or scheduler of its own (see [ADR-0009](../adr/ADR-0009-daemon-deletion-in-session-only.md)). The corpus compounds while you sleep. Tons-of-Skills is in-session by design with no off-vendor out-of-session story. - ---- - -## Both are open source - -Both projects are open source and intended to stay that way. Tons-of-Skills is MIT-licensed on GitHub with sponsorship-funded development. AgentOps is open source forever — the corpus that compounds in *your* repo is yours, the schema is portable, and the discipline survives any single vendor's roadmap. If you are evaluating durability of either, the source code is the receipt. - -This matters more than it sounds. The structural risk in the agent ecosystem is "vendor ships Managed Agents and eats the plugin." Open-source skill inventories and open-source context libraries both survive that move; they live in your repo, not in someone's hosted product. - ---- - -## Bottom line - -These are different categories, not competition. - -Tons-of-Skills is selling **inventory**: the largest catalog of pre-built skills in the ecosystem, with a package manager and a published grading rubric. If you measure by "skills installed per developer," it wins by construction. - -AgentOps is selling **corpus discipline**: the operational layer that turns each session's research, decisions, validations, and learnings into a markdown wiki your agents read on the next session. If you measure by "what the agent knows about this codebase that it didn't know last week," AgentOps is the substrate that makes that question answerable. - -A buyer shopping both should install both. Use Tons-of-Skills when the question is *"is there a skill for this?"* Use AgentOps when the question is *"how do we stop re-learning the same lessons every session?"* - ---- - -
- -[← Back to Comparisons](README.md) - -
diff --git a/docs/contracts/agents-documentation-authority.yaml b/docs/contracts/agents-documentation-authority.yaml index 270758f40..871e1afdf 100644 --- a/docs/contracts/agents-documentation-authority.yaml +++ b/docs/contracts/agents-documentation-authority.yaml @@ -29,7 +29,7 @@ documents: class: operating-contract disposition: retain-and-tighten owner: AGENTS.md - proof: docs/contracts/agents-operating-contract.md plus fresh-context verdicts + proof: scripts/check-cathedral-cut-conformance.py plus fresh-context verdicts references: phase_policy: {inventory: exact, pre-cutover: exact, cutover: exact} include_globs: @@ -53,14 +53,14 @@ documents: class: public-entrypoint disposition: retain-and-tighten owner: README.md - proof: docs/contracts/entry-documentation-behavior.md plus fresh-context verdicts + proof: scripts/check-cathedral-cut-conformance.py plus fresh-context verdicts - path: PRODUCT.md tracked: true class: product-contract disposition: retain-and-tighten owner: PRODUCT.md - proof: scripts/check-docs-demoted-claims.sh + proof: scripts/check-cathedral-cut-conformance.py - path: GOALS.md tracked: true diff --git a/docs/contracts/ci-jobs.yaml b/docs/contracts/ci-jobs.yaml index 61fae109c..4f6d38299 100644 --- a/docs/contracts/ci-jobs.yaml +++ b/docs/contracts/ci-jobs.yaml @@ -1,5 +1,5 @@ -# CI jobs manifest — source of truth for the docs/CI-CD.md "## CI Jobs and What -# They Check" table (rendered by scripts/generate-ci-jobs-table.sh) and parity- +# CI jobs manifest — source of truth for the generated jobs table in +# docs/CI-CD.md (rendered by scripts/generate-ci-jobs-table.sh) and parity- # gated by scripts/validate-ci-policy-parity.sh. # # One entry per purpose-grouped job in .github/workflows/validate.yml's @@ -8,11 +8,11 @@ # CI backstop authority; correctness + security remain as complementary lanes. jobs: - name: go-gate-shadow - reason: Runs the ordinary Go gate registry with GitHub annotations, JSON evidence, workflow coverage, and `--require-workflow-parity`; success means only that deterministic checks passed - failure: A blocking Go-gate check failure, a workflow coverage parity gap for a non-deferred blocking script, or an inability to produce/upload the `ao-gate-shadow-report` JSON artifact + reason: Runs the ordinary Go gate registry with workflow-coverage reporting + failure: A deterministic check failure or workflow/registry coverage mismatch - name: correctness - reason: '`ao` builds (Linux + native Windows smoke via matrix); Go tests pass with `-race`/coverage floor; embedded lib/skills in sync; Go complexity budget; CLI + v2.18 integration; release smoke; JSON-flag consistency; bats; Python smoke; advisory `ao doctor` dead-reference check' - failure: Build/test failure, race, coverage-floor regression, embedded drift, a function exceeding cyclomatic complexity 25, integration/smoke/bats/JSON-flag breakage, or Windows-smoke regression + reason: Builds `ao`; runs Go, schema, generated-surface, shell, and smoke tests + failure: Compilation, test, schema, generated-drift, or portability failure - name: security - reason: No hardcoded secrets or dangerous patterns (`curl|sh`, `rm -rf /`); unified security toolchain gate (`scripts/security-gate.sh --mode quick`) — gosec, golangci-lint, gitleaks, trivy, semgrep — blocking on any CRITICAL/HIGH finding - failure: Hardcoded API keys/passwords in non-test files, a dangerous pattern, or a CRITICAL/HIGH security/quality finding + reason: Runs secret, dependency, static-analysis, and dangerous-pattern checks + failure: A blocking security finding diff --git a/docs/contracts/codex-skill-api.md b/docs/contracts/codex-skill-api.md index c902b32c1..3821eb852 100644 --- a/docs/contracts/codex-skill-api.md +++ b/docs/contracts/codex-skill-api.md @@ -213,17 +213,12 @@ A Codex-conformant skill must: --- -## CLI Skill-Map Refresh +## Generated inventories -After changing `ao` command usage in any of these locations, refresh [`docs/cli-skills-map.md`](../cli-skills-map.md): - -- `skills/*/SKILL.md` -- `skills-codex/*/SKILL.md` - -Process: - -1. Update the map from current sources. -2. Run `bash tests/docs/validate-doc-release.sh` and `bash tests/docs/validate-skill-count.sh` before pushing. +`skills//SKILL.md` metadata owns the skill inventory and dependency graph. +Run `scripts/regen-all.sh` after changing it. The CLI command surface is a +separate executable projection generated from Cobra; no handwritten CLI-to-skill +map is maintained. --- @@ -245,43 +240,15 @@ bespoke skills or deliberate Codex-only divergence recorded in `skills-codex-overrides/catalog.json`; otherwise fix the source skill or the codex-sync transform/template and regenerate. -**Bespoke twins are HAND-MAINTAINED in full — body AND references (age-0js4).** -A `treatment: bespoke` twin (catalog.json — `council`, `crank`, `evolve`, -`plan`, `premortem`, `research`, `rpi`, … 19 total) is skipped ENTIRELY by -codex-sync, **including `--force`**. Its `SKILL.md` body and everything under -`references/`/`scripts/` are authored by hand: many bespoke references are -deliberate Codex-condensed rewrites of source (e.g. `research/references/ -data-flow-from-entry-points.md` is a substantial hand-rewrite — 85 source lines -deleted, 56 added), so a source edit does **NOT** auto-propagate, and -`codex-sync --force --only ` reporting "nothing to generate" is -CORRECT, not a bug. Refreshing a bespoke twin after a source change is a -deliberate human edit of the twin. **Do not** auto-mirror source over a bespoke -twin — it would clobber the hand-authored Codex copy. (Auto-refresh was -evaluated under age-0js4 and rejected: dozens of the bespoke reference files are -genuine hand-rewrites; only an explicit per-file tracked/bespoke manifest could -refresh safely, which is disproportionate to the low-frequency cost. *Accidental* -drift — a twin that should have tracked source but didn't — is the divergence -gate's job, tracked under age-odv to add an explicit bespoke exemption, not -codex-sync's.) +The current catalog has no bespoke or pointer twins. Every live Codex package is +a generated parity projection of `skills//SKILL.md` plus its linked local +files. `scripts/codex-sync.sh` owns those packages; manual edits under +`skills-codex//` are drift and will be overwritten. -**Pointer twins are exempt from the mirror requirement (`parity_policy: pointer`).** -Distinct from bespoke: some twins are deliberately THIN POINTERS — they carry no -mirrored prose, just "the source skill is the source of truth — read it first" -plus a short Codex Runtime Contract (e.g. `pawl-review`, `agent-mail`, -`ntm`; ~16 of them). For these there is nothing to mirror, so a source-only prose -edit must NOT demand twin churn. Declare it once in the twin's frontmatter: - -```yaml -parity_policy: pointer # twin defers to the source body; exempt from source-divergence -``` - -`validate-codex-generated-artifacts.sh` (`twin_is_pointer`) then skips the -SKILL.md-body and references divergence gates for that twin. Use this ONLY for a -genuine pointer — a twin that duplicates source prose must stay a full mirror and -keep the marker off, so its divergence gate still fires. The twin's own content -(incl. its Codex Runtime Contract) is still validated by the source→codex -existence check and the manifest/hash audit. Marking the existing ~16 pointer -twins is tracked under age-backfill-pointer-twin-markers-uco. +`skills-codex-overrides/catalog.json` remains the explicit treatment registry. +If a future runtime-specific implementation is genuinely necessary, declare it +there before editing a twin and add focused parity tests in the same change. +Do not create an undocumented exception or a second skill inventory. When a skill change affects Codex behavior, phrasing, orchestration, or UX: diff --git a/docs/contracts/context-map.md b/docs/contracts/context-map.md index 01d14a947..661af6796 100644 --- a/docs/contracts/context-map.md +++ b/docs/contracts/context-map.md @@ -6,41 +6,9 @@ | Source | Target | |---|---| -| `agent-native` | `agent-mail` | -| `agent-native` | `ntm` | -| `automation-shape-routing` | `agent-native` | -| `automation-shape-routing` | `skill-builder` | -| `automation-shape-routing` | `workflow-builder` | -| `bootstrap` | `doc` | -| `bootstrap` | `goals` | -| `bootstrap` | `product` | -| `bootstrap` | `scaffold` | -| `bootstrap` | `shared` | -| `codebase-recon` | `doc` | -| `doc` | `council` | -| `doc` | `standards` | -| `dueling-idea-genies` | `idea-genie` | -| `idea-genie` | `research` | -| `pattern-mining` | `operationalize` | -| `postmortem` | `council` | -| `postmortem` | `toil-mining` | -| `premortem` | `council` | -| `refactor` | `pattern-mining` | -| `refactor` | `standards` | -| `research` | `cass` | -| `research` | `codebase-recon` | -| `research` | `ms` | -| `research` | `pattern-mining` | -| `research` | `reverse-engineer` | | `rpi` | `implement` | | `rpi` | `plan` | | `rpi` | `validate` | -| `scaffold` | `standards` | -| `skill-builder` | `converter` | -| `skill-builder` | `heal-skill` | -| `status` | `sbh` | -| `test` | `standards` | -| `toil-mining` | `automation-shape-routing` | ## Optional context relationships @@ -73,17 +41,14 @@ | `pattern-mining` | `customer-of` | `research` | | `pattern-mining` | `customer-of` | `validate` | | `pattern-mining` | `supplier-to` | `operationalize` | -| `postmortem` | `customer-of` | `learn` | -| `postmortem` | `customer-of` | `toil-mining` | -| `premortem` | `shared-kernel` | `standards` | +| `premortem` | `supplier-to` | `plan` | | `product` | `supplier-to` | `plan` | | `reality-check` | `supplier-to` | `plan` | | `rpi` | `customer-of` | `implement` | | `rpi` | `customer-of` | `plan` | | `rpi` | `customer-of` | `validate` | -| `scope` | `supplier-to` | `domain` | +| `scope` | `supplier-to` | `plan` | | `security` | `supplier-to` | `validate` | -| `skill-builder` | `customer-of` | `automation-shape-routing` | | `skill-builder` | `supplier-to` | `heal-skill` | | `toil-mining` | `supplier-to` | `automation-shape-routing` | | `using-gc` | `partnership` | `agent-native` | @@ -128,7 +93,7 @@ | `dueling-idea-genies` | produces | `idea-challenge.v1` | | `goals` | produces | `result.json` | | `handoff` | produces | `.agents/handoff/*.md` | -| `heal-skill` | produces | `audit-report.json` | +| `heal-skill` | produces | `skill-hygiene-report` | | `idea-genie` | consumes | `repo-context` | | `idea-genie` | consumes | `task-question` | | `idea-genie` | produces | `idea-portfolio.v1` | @@ -145,31 +110,30 @@ | `pattern-mining` | consumes | `task-question` | | `pattern-mining` | produces | `pattern-mining.v1` | | `plan` | produces | `plan-packet.v1` | -| `postmortem` | consumes | `learn` | -| `postmortem` | consumes | `toil-mining` | +| `postmortem` | consumes | `verdict.v2` | | `postmortem` | produces | `postmortem-report.md` | -| `premortem` | consumes | `standards` | -| `premortem` | produces | `premortem-plan-verdict.v1` | +| `premortem` | consumes | `plan-packet.v1` | +| `premortem` | produces | `premortem-plan-review.v1` | | `product` | produces | `PRODUCT.md` | | `reality-check` | consumes | `claim` | | `reality-check` | consumes | `repository-evidence` | | `reality-check` | produces | `reality-check-report.v1` | | `refactor` | consumes | `repo-context` | | `refactor` | produces | `code-changes` | -| `research` | consumes | `repo-context` | -| `research` | produces | `.agents/research/*.md` | -| `research` | produces | `result.json` | +| `research` | consumes | `research-question` | +| `research` | produces | `research-report` | | `reverse-engineer` | produces | `.agents/research/*.md` | | `rpi` | consumes | `plan` | | `rpi` | consumes | `implement` | | `rpi` | consumes | `validate` | | `rpi` | produces | `rpi-report.v1` | | `scaffold` | produces | `converted-skill` | -| `scope` | produces | `filesystem-gate` | +| `scope` | consumes | `proposed-write-scope` | +| `scope` | produces | `scope-review` | | `security` | consumes | `repo-context` | | `security` | produces | `security-report.json` | | `shared` | produces | `reference-documents` | -| `skill-builder` | produces | `converted-skill` | +| `skill-builder` | produces | `skill-source-package` | | `standards` | produces | `stdout` | | `status` | produces | `stdout` | | `swarm` | consumes | `explicit-disjoint-packets` | diff --git a/docs/contracts/finding-artifact.schema.json b/docs/contracts/finding-artifact.schema.json index e77fe8a1d..92bcf0fd6 100644 --- a/docs/contracts/finding-artifact.schema.json +++ b/docs/contracts/finding-artifact.schema.json @@ -95,7 +95,7 @@ "uniqueItems": true, "items": { "type": "string", - "enum": ["plan", "premortem", "pre-mortem", "constraint"] + "enum": ["plan", "premortem", "premortem", "constraint"] } }, "scope_tags": { diff --git a/docs/contracts/finding-compiler.md b/docs/contracts/finding-compiler.md index dfc223c73..d2cdb3384 100644 --- a/docs/contracts/finding-compiler.md +++ b/docs/contracts/finding-compiler.md @@ -47,7 +47,7 @@ Promotion must preserve the reusable prevention content: | `premortem` | `.agents/premortem-checks/.md` | Surface prior failure modes during plan/spec validation | | `constraint` | `.agents/constraints/index.json` | Observe mechanically detectable rules in shadow, then enforce only after measured activation | -`pre-mortem` and `pre_mortem` remain accepted input aliases during migration, +`premortem` and `premortem` remain accepted input aliases during migration, but writers emit `premortem`. Advisory findings may compile to `plan` and `premortem`. A mechanical finding compiles to `constraint` only when its regex matches every stored positive and passes every explicit negative control. diff --git a/docs/contracts/forbidden-claims.yaml b/docs/contracts/forbidden-claims.yaml index 66fdd9d6b..3a0e8110f 100644 --- a/docs/contracts/forbidden-claims.yaml +++ b/docs/contracts/forbidden-claims.yaml @@ -50,7 +50,7 @@ claims: pattern: 'session hooks' rationale: >- AgentOps 3.0 is HOOKLESS (docs/3.0.md, ADR-0009) — no session hook extracts - learnings; a session writes them explicitly via /post-mortem. `ao init + learnings; a session writes them explicitly via /postmortem. `ao init --hooks` is not a real flag. allowed_contexts: *common diff --git a/docs/contracts/local-pre-push-gate-retirement.md b/docs/contracts/local-pre-push-gate-retirement.md index 398e1bfca..8117efad9 100644 --- a/docs/contracts/local-pre-push-gate-retirement.md +++ b/docs/contracts/local-pre-push-gate-retirement.md @@ -10,29 +10,27 @@ trying to publish, which made ordinary delivery slow and fragile. ## Current decision -Validation stops after one exact candidate has deterministic evidence, one -immutable verdict from fresh context, and one Learn receipt. The consumer -repository owns delivery for local and cloud agents. It may use direct push, a -PR, hosted CI, or a small deterministic hook. +AgentOps stops after one exact candidate has deterministic evidence and one +durable verdict from fresh context. The consumer repository owns delivery for +local and cloud agents. It may use direct push, a PR, hosted CI, or a small +deterministic hook. -A repository delivery adapter may verify identity and reuse an exact-input -receipt. It must not: +A repository hook or CI job may run deterministic repository checks. It must +not treat AgentOps as the delivery controller or: - invoke a model or perform another semantic review; - mutate or upgrade the Validate verdict; - close tracker work as a side effect of Git; -- own a global delivery queue for AgentOps; or +- create an AgentOps delivery queue or receipt; or - replay an unchanged full suite merely because delivery started. -## Deletion ownership +## Retired surface -K7 deletes the old delivery command, queue, and semantic push-hook machinery -while installing deterministic delivery recording. Dedicated scripts, hooks, -tests, and fixtures disappear in that same candidate. F4 later removes the -build profiles that kept alternate command owners compilable. D2 regenerates -command projections after executable ownership is final. +The old AgentOps delivery commands, queue, semantic push admission, and +delivery receipts are retired. Existing deterministic repository checks remain +ordinary checks whose exit status means only success or failure. ## Rollback -Revert the complete K7 candidate before downstream consumers depend on its -receipt contract. Do not restore only a hook or only a removed delivery owner. +Revert the complete Cathedral Cut if repository policy needs to restore the old +product boundary. Do not restore only an AgentOps push hook or delivery command. diff --git a/docs/contracts/multi-runtime-tier-charter.md b/docs/contracts/multi-runtime-tier-charter.md index 8ae04ab42..791f0dad8 100644 --- a/docs/contracts/multi-runtime-tier-charter.md +++ b/docs/contracts/multi-runtime-tier-charter.md @@ -146,6 +146,4 @@ declared opt-in here; no CI Tier E lane is added. hook capability matrix, install behavior - `docs/contracts/headless-invocation-standards.md` — what "headless invocation" means for each runtime -- `docs/contracts/release-readiness.md` — what release validation gates - are blocking - `GOALS.md` — gate roster (search for `multi-runtime` / `runtime-`) diff --git a/docs/contracts/pmf-evidence.md b/docs/contracts/pmf-evidence.md index 912e00b1f..ccd2b684f 100644 --- a/docs/contracts/pmf-evidence.md +++ b/docs/contracts/pmf-evidence.md @@ -91,6 +91,6 @@ acceptance_criteria: ## See also - `.agents/plans/2026-05-20-ultimate-dogfood-3.0.md` — the 3.0 release plan that prescribed this gate -- `.agents/council/2026-05-20-pre-mortem-ultimate-dogfood-3.0.md` — pre-mortem HIGH-1 surfaced the `.agents/`-gitignored contradiction this gate resolves +- `.agents/council/2026-05-20-premortem-ultimate-dogfood-3.0.md` — premortem HIGH-1 surfaced the `.agents/`-gitignored contradiction this gate resolves - `scripts/export-evidence.sh` — the promoter - `scripts/check-pmf-evidence.sh` — the gate diff --git a/docs/contracts/producer-defect-register.md b/docs/contracts/producer-defect-register.md index 2f52e8c60..fe0fc6067 100644 --- a/docs/contracts/producer-defect-register.md +++ b/docs/contracts/producer-defect-register.md @@ -1,8 +1,8 @@ # Producer-Defect Recurrence Contract -This contract governs the bookkeeping seam from repeated validation findings to -an advisory producer-rule candidate. It does not create policy, block delivery, -or activate a mechanical check. +This contract describes an optional, post-run reduction from repeated Validate +findings to an advisory producer-rule candidate. It does not participate in RPI, +change a verdict, create policy, block work, or activate a mechanical check. ## Inputs @@ -30,17 +30,16 @@ The recurrence reducer groups by `class_key`, then counts distinct objective and reports that distinct-objective count as `recurrence_count`. Candidates conform to `schemas/producer-rule-candidate.v1.schema.json`. They -are always `advisory: true`. A candidate proposes that the orchestrator examine -the producer surface—Discovery, Plan, Premortem, a skill contract, or an -operator footgun—but it is not itself a rule or gate. +are always `advisory: true`. A candidate proposes that a later Learn invocation +examine Plan, Premortem, a specialist skill, or an operator footgun. It is not a +rule, gate, lifecycle transition, or instruction to revise the completed run. -## Runtime projection +## Optional projection -`ao membrane digest --json` exposes the reduction as -`producer_candidates`. Learn records the same shape in its receipt after -reconciling immutable Validate observations. The orchestrator alone decides -whether a candidate changes future work. +An explicitly invoked Learn consumer may read a caller-supplied collection of +immutable `verdict.v2` artifacts, normalize their findings into observations, +and emit this reduction. There is no required receipt, command, background +process, automatic invocation, or core-state update. -The measured before/after register remains a separate projection: it evaluates -whether an accepted producer change reduced later recurrence. Candidate +Any before/after measurement remains a separate advisory projection. Candidate creation and effectiveness measurement must not be conflated. diff --git a/docs/contracts/registry-as-derived.md b/docs/contracts/registry-as-derived.md index dca5decc1..a54d3207a 100644 --- a/docs/contracts/registry-as-derived.md +++ b/docs/contracts/registry-as-derived.md @@ -1,108 +1,16 @@ -# Contract: registry.json as derived artifact, not versioned source +# Generated skill projections -> **Status:** design — establishes the contract; implementation deferred to child beads. -> **Bead:** soc-jbea (parent epic; child beads filed at PR merge time) -> **Date:** 2026-05-20 -> **Origin:** Council 2026-05-20-pr-cleanup-220-240 verdict.md (WARN with caveat: registry.json sibling-coupling = 40-50% of session waste, unaddressed) +`skills/*/SKILL.md` metadata is the sole source for skill tier, dependencies, +capabilities, effects, canonical status, and disposition. -## Problem statement +`scripts/generate-skill-mesh.py` derives the registry, catalog, router, graph, +domain map, context map, counts, and runtime manifests in one pass. Its +`--check` mode verifies every projection without modifying source metadata. -`registry.json` is a generated inventory of skills + hooks + stores + job types + evals + CLI commands, currently committed to the repo and regenerated by `scripts/generate-registry.sh`. Every PR that adds a new skill (or hook, or eval) modifies registry.json. When N such PRs are in flight in parallel, the merge of any ONE invalidates the registry.json content of all N-1 siblings — they all go `DIRTY` with content conflict. +No projection is a second authority. Do not hand-edit generated inventories or +maintain a parallel count, keep-list, dependency graph, or context map. Change +the owning `SKILL.md` metadata and regenerate. -This is structural, not incidental. The 2026-05-20 PR-cleanup session shipped 5 sibling skill-reference PRs (#335, #339, #340, #341, #336) plus another wave of 4 (#337, #338, #344, #334). Across both waves we observed ~20 rebase operations to resolve the cascading `registry.json` conflicts — an estimated 40-50% of session wall-clock cost (Judge B, 2026-05-20-pr-cleanup-220-240/verdict-B.md). - -`scripts/regen-codex-hashes.sh` outputs (`skills-codex/.agentops-manifest.json`, `skills-codex/*/.agentops-generated.json`) exhibit the same pattern. - -## Why fix it now - -After PR #339 merged at cycle 215, PRs #340 and #341 went DIRTY. After #340 merged, #341 went DIRTY again. After #336 merged at cycle 223, all four siblings went DIRTY. The pattern repeats every merge. - -Current mitigation: rebase + take-`--ours` of registry.json + regen + force-push, per sibling. The `verified_rebase` helper handles the `rebase --continue` silent-failure case (soc-e9n6) but doesn't address the root cost. - -## Solution space - -### Option A — Make registry.json a build artifact, not source - -Move `registry.json` out of version control. Add to `.gitignore`. CI regenerates it on every run and uses the regenerated file for downstream gates (`registry-check`, `validate-codex-generated-artifacts`). No commit means no conflict. - -**Pros:** -- Eliminates the conflict class entirely — no sibling cascade possible. -- Reduces commit noise (every skill PR currently has a registry.json hunk that's pure generation output). -- Aligns with the existing precedence rule in CLAUDE.md: "Executable implementation and generated outputs" — `registry.json` is the generated output of `cli/` introspection, so versioning it stores a stale view of `cli/`'s ground truth. - -**Cons:** -- Local development needs `make registry` (or equivalent) before any consumer-of-registry runs. -- CI runtime grows by the registry-generation step (~10s). -- Tools that depend on registry.json being present at HEAD without a build step (if any exist) need updating. Audit required. -- Branch-protection checks may need adjustment if they inspect the file directly. - -**Migration steps (child beads):** -1. Audit all consumers of `registry.json` — tests, CI gates, hooks, docs. -2. Add `make registry` target to `cli/Makefile` (or root Makefile). -3. Update `validate.yml` to run `make registry` before any job that reads it. -4. Update `.gitignore` to exclude `registry.json`. -5. Remove existing `registry.json` from repo with `git rm`. -6. Update onboarding docs. - -### Option B — Registry-owner pattern (workflow-only, no code change) - -When a batch of PRs is in flight, designate ONE as the registry baseline. Other siblings rebase against the baseline's branch (not main) until the baseline merges. After baseline merges, others rebase against main once, regen registry, push. - -**Pros:** -- No code or CI changes. -- Preserves committed registry.json (no downstream consumer audit needed). - -**Cons:** -- Workflow discipline only — gh-merge-chain doesn't support this pattern natively. -- One bad merge order resurfaces the original problem. -- Still N rebases for N siblings; just sequenced rather than cascading. - -### Option C — Registry stub + sparse merge - -Commit a `registry.json` stub with a stable structural shape but no PR-specific content. Each PR adds an entry to a small per-skill manifest (e.g., `.agents-manifest` in the skill dir). Build-time aggregator produces the full registry. - -**Pros:** -- Some-versioning preserved (the stub catches deletions of major surfaces). -- Per-skill manifests are tiny and rarely conflict across siblings. - -**Cons:** -- More complex than A; more surface than B. -- Introduces a new shape (per-skill manifest) that may itself drift. - -## Recommendation - -**Option A (registry as build artifact)** is the canonical fix. Option B is a stopgap. Option C is over-engineered for the problem. - -Codex-hashes follow the same logic: `skills-codex/.agentops-manifest.json` and `skills-codex/*/.agentops-generated.json` are derived from `skills-codex/**/*.md` content hashes. Same migration applies. Treat as a parallel sub-epic. - -## Acceptance criteria - -For the parent epic (soc-jbea) to close: - -1. `registry.json` not in version control; `make registry` regenerates it. -2. All CI jobs that depend on registry.json content run `make registry` first. -3. Three sibling PRs adding distinct skills can ship in parallel without ANY conflict on registry.json. -4. A test PR demonstrating the above (e.g., 3 trivial skills shipped concurrently). -5. Doc updated: `docs/contracts/registry-as-derived.md` (this file) marked `Status: implemented`. -6. Codex-hashes treated symmetrically OR explicitly deferred to a parallel child. - -## Child beads to file (at PR merge time, not now — these depend on the contract being agreed) - -- **soc-jbea.1** Audit registry.json consumers (CI, hooks, scripts, tests) -- **soc-jbea.2** Add `make registry` target + ensure idempotent -- **soc-jbea.3** Update `.github/workflows/validate.yml` to regenerate registry.json at job-start -- **soc-jbea.4** `git rm registry.json`; add to `.gitignore` -- **soc-jbea.5** Verification: trial 3-sibling skill PRs in parallel, no conflicts -- **soc-jbea.6** Apply same pattern to skills-codex/*/.agentops-generated.json + skills-codex/.agentops-manifest.json -- **soc-jbea.7** Update onboarding docs (newcomer-guide, contributing) - -## Non-goals - -- This contract does NOT propose changing the regen scripts themselves. `scripts/generate-registry.sh` keeps its current output; only the storage location changes (CI artifact, not repo). -- This contract does NOT address `verified_rebase` (soc-e9n6) or `smart-skill-md-merge.sh` (soc-trhs) — those address downstream symptoms of the same problem and remain useful even after this contract lands. They become defense-in-depth for non-generated SKILL.md conflicts. - -## References - -- Council verdict: `.agents/council/2026-05-20-pr-cleanup-220-240/verdict.md` (local-only, gitignored) -- Earlier observation: cycles 211-226 of `.agents/evolve/cycle-history.jsonl` document the cascade pattern -- CLAUDE.md "Source-of-truth precedence" — `registry.json` falls under category 1 (generated) +Codex twins are derived separately by `scripts/codex-sync.sh`, with hashes +regenerated by `scripts/regen-codex-hashes.sh`. A deleted source skill is +removed from generated inventories and installed-skill projections on refresh. diff --git a/docs/contracts/release-readiness.md b/docs/contracts/release-readiness.md deleted file mode 100644 index a6481b92a..000000000 --- a/docs/contracts/release-readiness.md +++ /dev/null @@ -1,95 +0,0 @@ -# Release Readiness Contract - -Release readiness is the machine-readable contract for deciding whether a -release has enough evidence to tag and publish. The authoritative artifact is: - -```text -.agents/releases/local-ci//release-readiness.json -``` - -The JSON shape is versioned by `release-readiness.v1.schema.json`. - -## Score - -`scripts/check-release-readiness.sh` writes a 10 point score: - -| Dimension | Weight | Release meaning | -|-----------|--------|-----------------| -| SIL | 2.0 | Deterministic local software-in-the-loop release gate passed | -| VIL | 2.0 | Validation-in-the-loop evidence passed, such as remote CI or release publisher parity | -| HIL | 2.0 | Hardware-in-the-loop evidence passed on a real target; explicit waiver earns 1.0 | -| Artifacts | 1.5 | SBOM, security report, readiness, and manifest artifacts exist | -| Security | 1.5 | Full release security gate produced a passing JSON report | -| Evals | 1.0 | Release smoke/eval checks passed | - -Official release readiness requires both: - -- `release_readiness_score >= 8` -- `release_status == "pass"` - -In `official` mode, the score alone is not enough. SIL, VIL, artifact, -security, and eval dimensions must pass. HIL must pass or be explicitly waived. - -## Modes - -| Mode | Use | Exit behavior | -|------|-----|---------------| -| `official` | Pre-tag release audit with `--release-version` | Fails if the gate is not pass | -| `advisory` | Normal local full gate without a target release version | Writes JSON without blocking on missing HIL | -| `fast` | `ci-local-release.sh --fast` | Writes degraded JSON for quick feedback | - -`scripts/ci-local-release.sh --release-version X.Y.Z` selects `official` mode -unless `--readiness-mode` overrides it. - -## HIL Evidence - -`scripts/check-release-hil.sh` captures the companion artifact: - -```text -.agents/releases/local-ci//hil-evidence.json -``` - -Targets are supplied with repeated `--hil-target` flags on the local release -gate, with `AGENTOPS_RELEASE_HIL_TARGETS`, or by calling the HIL script -directly. Official targets must exercise more than `ao version`; evidence -records the command fingerprint, OS/architecture/runtime identity, workflow -checks, and optional release-version match. - -```bash -scripts/check-release-hil.sh \ - --expected-version X.Y.Z \ - --target 'local:bushido:ao version && ao init --help && ao doctor && ao gate check --help' -scripts/check-release-hil.sh \ - --expected-version X.Y.Z \ - --target 'ssh:bushido:bushido:ao version && ao init --help && ao doctor && ao gate check --help' -``` - -When no physical target is available for an official release, the release owner -must pass `--hil-waiver "reason"` so the waiver is visible in both the HIL and -readiness artifacts. A waiver is acceptable release evidence, but it scores only -half of the HIL dimension. - -## Release Artifacts - -`release-artifacts.json` records these fields when the local gate runs: - -```json -{ - "sbom_cyclonedx": "sbom-vX.Y.Z.cyclonedx.json", - "sbom_spdx": "sbom-vX.Y.Z.spdx.json", - "security_report": "security-gate-full.json", - "eval_fast_report": "eval-agentops-fast.json", - "eval_baseline_audit": "eval-baseline-audit.json", - "release_readiness": "release-readiness.json", - "hil_evidence": "hil-evidence.json", - "vil_evidence": "digital-twin-evidence.json", - "digital_twin_evidence": "digital-twin-evidence.json" -} -``` - -`scripts/resolve-release-artifacts.sh` only resolves full release artifact sets -that include SBOMs, the security report, eval fast and baseline-audit outputs, -readiness, HIL evidence, and digital-twin/VIL evidence. -`scripts/validate-release-audit-artifacts.sh` validates that proof bundle for -release audits generated on or after 2026-05-02, while still accepting older -historical audits. diff --git a/docs/contracts/skill-flow.md b/docs/contracts/skill-flow.md deleted file mode 100644 index 6d82cea2b..000000000 --- a/docs/contracts/skill-flow.md +++ /dev/null @@ -1,147 +0,0 @@ -# Skill-Flow Connectivity Contract - -> Gate: `scripts/validate-skill-flow.sh` (CI job `validate-skill-flow`). -> Allowlist: `scripts/skill-flow-standalone.txt`. -> Source of truth: `skills/*/SKILL.md` frontmatter. -> Sibling: `scripts/audit-skill-metadata.sh` owns `context_rel.with` resolution; -> this contract owns the `consumes` vocabulary and connectivity. - -## Why this exists - -"Do all skills flow together?" was unanswerable until this gate. Skill -dependencies are declared in **three overlapping frontmatter fields** that -historically drifted apart: - -| Field | Meaning | Read by context-map? | -|-------|---------|----------------------| -| `consumes` | upstream inputs (skill slug, external input, or produced artifact) | yes (data-flow table) | -| `context_rel` | DDD bounded-context relationship (`kind` + `with`) | yes (mermaid graph) | -| `metadata.dependencies` | upstream skill slugs | **no** | - -Because the three are not reconciled, a skill can look "orphaned" in one field -while being well-connected in another (e.g. `trace` declares no `consumes` but -depends on `provenance` via `metadata.dependencies`). This contract defines a -single connectivity model that spans all three and a closed vocabulary for -`consumes`. - -## Rules (enforced — gate fails on violation) - -### 1. Closed `consumes` vocabulary - -Every `consumes` token MUST resolve to exactly one of: - -1. a **peer skill slug** (a directory under `skills/`), or -2. a **whitelisted external input**, or -3. an **artifact produced by some skill** (appears in another skill's `produces`). - -Anything else is a typo or an undeclared dependency and fails the gate. - -**External inputs** (the closed whitelist — extend deliberately, in both -`scripts/validate-skill-flow.sh` and this doc): - -| Token | What it is | -|-------|-----------| -| `Cargo.lock` | declared external input supplied by the user, repository, runtime, or upstream toolchain | -| `Cargo.toml` | declared external input supplied by the user, repository, runtime, or upstream toolchain | -| `br` | the beads_rust issue store | -| `build-config` | declared external input supplied by the user, repository, runtime, or upstream toolchain | -| `cargo-metadata` | declared external input supplied by the user, repository, runtime, or upstream toolchain | -| `cli-source` | declared external input supplied by the user, repository, runtime, or upstream toolchain | -| `closed-beads` | declared external input supplied by the user, repository, runtime, or upstream toolchain | -| `code` | declared external input supplied by the user, repository, runtime, or upstream toolchain | -| `code-under-review` | declared external input supplied by the user, repository, runtime, or upstream toolchain | -| `codebase` | declared external input supplied by the user, repository, runtime, or upstream toolchain | -| `codex-plugin` | declared external input supplied by the user, repository, runtime, or upstream toolchain | -| `command-help` | declared external input supplied by the user, repository, runtime, or upstream toolchain | -| `command-map` | declared external input supplied by the user, repository, runtime, or upstream toolchain | -| `convention-target` | declared external input supplied by the user, repository, runtime, or upstream toolchain | -| `crate-docs` | declared external input supplied by the user, repository, runtime, or upstream toolchain | -| `crate-source` | declared external input supplied by the user, repository, runtime, or upstream toolchain | -| `data-model` | declared external input supplied by the user, repository, runtime, or upstream toolchain | -| `environment-contract` | declared external input supplied by the user, repository, runtime, or upstream toolchain | -| `error-reports` | declared external input supplied by the user, repository, runtime, or upstream toolchain | -| `evidence` | declared external input supplied by the user, repository, runtime, or upstream toolchain | -| `existing-docs` | declared external input supplied by the user, repository, runtime, or upstream toolchain | -| `existing-tracked-work` | declared external input supplied by the user, repository, runtime, or upstream toolchain | -| `external-api` | an upstream API or doc site outside the corpus | -| `external-source-candidates` | declared external input supplied by the user, repository, runtime, or upstream toolchain | -| `failure-report` | declared external input supplied by the user, repository, runtime, or upstream toolchain | -| `ffi-bindings` | declared external input supplied by the user, repository, runtime, or upstream toolchain | -| `ffi-contracts` | declared external input supplied by the user, repository, runtime, or upstream toolchain | -| `gemini-extension` | declared external input supplied by the user, repository, runtime, or upstream toolchain | -| `git` | declared external input supplied by the user, repository, runtime, or upstream toolchain | -| `git-worktree` | declared external input supplied by the user, repository, runtime, or upstream toolchain | -| `github-pr` | a GitHub pull request under review | -| `hook-policy` | declared external input supplied by the user, repository, runtime, or upstream toolchain | -| `implementation-examples` | declared external input supplied by the user, repository, runtime, or upstream toolchain | -| `installation-docs` | declared external input supplied by the user, repository, runtime, or upstream toolchain | -| `manifest-and-lockfile` | declared external input supplied by the user, repository, runtime, or upstream toolchain | -| `mcp-server` | declared external input supplied by the user, repository, runtime, or upstream toolchain | -| `onboard` | the session onboarding handshake | -| `operational-constraints` | declared external input supplied by the user, repository, runtime, or upstream toolchain | -| `package-metadata` | declared external input supplied by the user, repository, runtime, or upstream toolchain | -| `product-requirements` | declared external input supplied by the user, repository, runtime, or upstream toolchain | -| `profiler-output` | declared external input supplied by the user, repository, runtime, or upstream toolchain | -| `project-context` | declared external input supplied by the user, repository, runtime, or upstream toolchain | -| `project-goals` | declared external input supplied by the user, repository, runtime, or upstream toolchain | -| `project-source` | declared external input supplied by the user, repository, runtime, or upstream toolchain | -| `release-notes` | declared external input supplied by the user, repository, runtime, or upstream toolchain | -| `repo-context` | the repository working tree / source under analysis | -| `repo-tree` | declared external input supplied by the user, repository, runtime, or upstream toolchain | -| `repository` | declared external input supplied by the user, repository, runtime, or upstream toolchain | -| `runtime-configuration` | declared external input supplied by the user, repository, runtime, or upstream toolchain | -| `runtime-metrics` | declared external input supplied by the user, repository, runtime, or upstream toolchain | -| `rust-source` | declared external input supplied by the user, repository, runtime, or upstream toolchain | -| `service-contract` | declared external input supplied by the user, repository, runtime, or upstream toolchain | -| `skill-bundle` | declared external input supplied by the user, repository, runtime, or upstream toolchain | -| `source-code` | declared external input supplied by the user, repository, runtime, or upstream toolchain | -| `specification` | declared external input supplied by the user, repository, runtime, or upstream toolchain | -| `support-history` | declared external input supplied by the user, repository, runtime, or upstream toolchain | -| `task-intent` | declared external input supplied by the user, repository, runtime, or upstream toolchain | -| `task-question` | declared external input supplied by the user, repository, runtime, or upstream toolchain | -| `test-plan` | declared external input supplied by the user, repository, runtime, or upstream toolchain | -| `test-results` | declared external input supplied by the user, repository, runtime, or upstream toolchain | -| `test-suite` | declared external input supplied by the user, repository, runtime, or upstream toolchain | -| `test-target` | declared external input supplied by the user, repository, runtime, or upstream toolchain | -| `tests` | declared external input supplied by the user, repository, runtime, or upstream toolchain | -### 2. `metadata.dependencies` resolution - -Every `metadata.dependencies` entry MUST name an existing skill slug. - -### 3. Connectivity (no silent orphans) - -A skill is **connected** if it shares at least one skill-to-skill edge with a -peer, counting all three layers (`consumes` skill-slugs, `context_rel.with` -skill-slugs, `metadata.dependencies`). A skill with **zero** skill-to-skill -edges is an **orphan** and fails the gate **unless** it is listed in -`scripts/skill-flow-standalone.txt` with a rationale. - -Standalone skills are intentional leaves: boundary adapters (`push`, -`openai-docs`), orchestration/install adapters (`codex-team`, -`session-bootstrap`), and human-facing explainers (`using-agentops`). Listing a -skill there asserts "this is a leaf by design." If it later gains an edge, the -gate reports a **stale allowlist entry** — remove it. - -## Reported, not enforced (informational) - -The gate prints (without failing) two reconciliation signals: - -- **`consumes` vs `metadata.dependencies` disagreement** — the two skill-slug - fields that should agree but historically drifted. Reconciling them (picking - one canonical field) is tracked work, not a blocker. -- **Dead-end produced artifacts** — artifacts in some skill's `produces` that no - skill `consumes`. Most are output-type annotations (`result.json`, - `verdict.json`, `stdout`), not edges, so this is informational. - -## How to fix a failure - -```bash -bash scripts/validate-skill-flow.sh # human-readable findings -bash scripts/validate-skill-flow.sh --json # machine-readable verdict -``` - -- **consumes-vocabulary**: fix the typo, or declare the producer, or (if it is a - genuinely new external input) add it to the whitelist above and in the script. -- **metadata-dependencies**: point at a real skill slug or drop the entry. -- **orphan**: wire a real `context_rel`/`consumes`/`metadata.dependencies` edge, - or add the slug to `scripts/skill-flow-standalone.txt` with a one-line reason. diff --git a/docs/contracts/update-principles.md b/docs/contracts/update-principles.md index 0557c86fa..af82c247b 100644 --- a/docs/contracts/update-principles.md +++ b/docs/contracts/update-principles.md @@ -71,7 +71,7 @@ The five principles are voluntary until codified. Codification path: | 2. Drift-blocking test | `check-test-pair-on-commit.sh` checks for added `*_test.go` / `*.bats` paired with modified `*.go` / `*.sh` | TODO (BC3 epic, separate cycle) | | 3. Sibling-pattern citation | lint commit body for `matching … pattern` / `sibling …` / `following the … shape` phrasing | TODO (BC3 epic, separate cycle) | | 4. Fitness delta | regex `/[0-9]+\/[0-9]+ → [0-9]+\/[0-9]+/` or similar numerical-pair pattern in commit body | TODO (BC3 epic, separate cycle) | -| 5. Clean branch point | `git log --since` check on the commit's first-parent base — already partially enforced by `pre-push-gate.sh` worktree-disposition lane | partial | +| 5. Clean branch point | Consumer repository policy; deliberately outside AgentOps | external | Each principle's enforcer ships as its own commit (each cycle demonstrates principle 1). @@ -92,7 +92,7 @@ Not claiming the exemplar commit is the only valid shape. A bug fix can be one f - Rescope plan: `.agents/plans/2026-05-12-rescope-evolve-and-architecture.md` (operator review pending; tracked outside `.agents/` once filed under bd epics). - Bounded-context inventory: `.agents/research/2026-05-12-bounded-contexts-and-ports.md`. -- Source post-mortem: `.agents/post-mortems/2026-05-12-evolve-session-improvement-postmortem.md`. +- Source postmortem: `.agents/postmortems/2026-05-12-evolve-session-improvement-postmortem.md`. ## Cycle log diff --git a/docs/convergence/google-sre.md b/docs/convergence/google-sre.md deleted file mode 100644 index e43fbb291..000000000 --- a/docs/convergence/google-sre.md +++ /dev/null @@ -1,70 +0,0 @@ ---- -title: "AgentOps 3.0 ⇄ Google SRE — encoding map" -description: "Where each of the 11 structural conclusions in Google's 2026 SRE AI whitepaper is already encoded in AgentOps doctrine, and the genuine gaps it surfaced." -permalink: /convergence/google-sre -last_reviewed: 2026-05-29 ---- - -# AgentOps 3.0 ⇄ Google SRE: encoding map - -> Google's 2026 SRE whitepaper *"AI in SRE: How Google is Engineering the Future of Reliable Operations"* (Papapanagiotou, Malesevic, Heiser & Meshenberg) independently arrives at AgentOps 3.0's core structural conclusions — from the **production-operations** end of the lifecycle. AgentOps runs the **dev-loop + context-compiler** end. Same waist, two ends of the SDLC. -> -> This doc is the receipt: it maps each convergent principle to **where it is already encoded in AgentOps**, so the convergence is verifiable against executable doctrine, not asserted. It is the in-repo companion to the chronological [convergence ledger](ledger.md). Tracking bead: `ag-4hf7`. - -## Why this matters - -This is external validation of the [3.0 thesis](../3.0.md): AI velocity (a stated **4x** code-volume increase) breaks human-paced practice, so the work must be restructured around **autonomous loops bounded by deterministic guardrails**. Two teams, working opposite ends of the lifecycle with no coordination, converged on the same four invariants. That is signal the architecture is correct, not stylistic. - -## The encoding map - -| # | Convergent principle | Google SRE phrasing | Encoded in AgentOps at | Status | -|---|---|---|---|---| -| 1 | **4x velocity breaks human-paced review** is the trigger | "Human code review cannot scale linearly… targeting up to a 4x increase in productivity." | [`docs/3.0.md`](../3.0.md) thesis ("does not scale with a 4x to 10x increase") | ✅ encoded | -| 2 | **No self-grade / independent verification harnesses** | "**Independent Harnesses** — the agent that generates source code must be strictly isolated from the agent that defines tests or reviews output… prevents transmission of cross-bias." | Ratchet rule #1 in [`docs/3.0.md`](../3.0.md) + [`canonical-loop-model.md`](../architecture/canonical-loop-model.md) + [`skills/domain/references/loop.md`](https://github.com/boshu2/agentops/blob/main/skills/domain/references/loop.md) | ✅ encoded | -| 3 | **Eval data is the moat; tiered + judge + deterministic scoring** | Bronze/Silver/**Gold** tiers; "True Precision vs Observed Precision"; "LLM-as-a-Judge" + strict deterministic exact-match scoring; Nightly Evals. | [`architecture/behavior-shaping-environment.md`](../architecture/behavior-shaping-environment.md); holdout-leak gate (#605), stale-rubric guard (#604), outcomes verdict (#603) | ⚠️ partial → `ag-fjbu` | -| 4 | **Knowledge becomes a constraint / deterministic boundary** | "Safe-by-default actuation — tools must be incapable of single-handedly taking down production"; "deterministic, human-controlled safety boundaries." | Ratchet rule #3 ("compiles into a gate, a test, or a rule") in [`docs/3.0.md`](../3.0.md), [`knowledge-flywheel.md`](../knowledge-flywheel.md), [`primitive-chains.md`](../architecture/primitive-chains.md) | ✅ encoded | -| 5 | **Humans move up the abstraction ladder** | "From Operator to Architect… move up the abstraction ladder… review Designs, Intent, Policies, not lines." | "Validate BOTH ENDS" + BDD-as-plan in [`operating-loop.md`](../architecture/operating-loop.md) move 1; coherent-arc review in [`CLAUDE.md`](https://github.com/boshu2/agentops/blob/main/CLAUDE.md) | ✅ encoded | -| 6 | **Reasoning engine decoupled from execution engine** | "Decoupling the AI's reasoning engine (AI Operator) from the execution engine (Actus)… their ability to mutate production remains strictly governed." | Hexagonal "domain core never imports the shell" — [`cdlc.md`](../cdlc.md), [`ports-and-adapters.md`](../architecture/ports-and-adapters.md), [`sovereignty-proof/`](../sovereignty-proof/index.md) | ✅ encoded | -| 7 | **Intent/spec as the plan, authored before code** | "Co-authoring and approving detailed specifications with AI before code generation." | "Behavior is the plan" — [`operating-loop.md`](../architecture/operating-loop.md), [`skills/discovery`](https://github.com/boshu2/agentops/blob/main/skills/discovery/SKILL.md), [`skills/brainstorm`](https://github.com/boshu2/agentops/blob/main/skills/brainstorm/SKILL.md) | ✅ encoded | -| 8 | **Progressive authorization / autonomy levels** | **L0–L4 ladder**, gated on statistically-significant success vs Golden data; "Red Button" override. | Explicit **L0–L4 ladder** with named promotion gates, auto-downgrade, and Red Button — [`autonomy-ladder.md`](../architecture/autonomy-ladder.md); maps in-session floor → substrate dispatch (NTM / managed-agents) → bounded `autodev`: [`canonical-loop-model.md`](../architecture/canonical-loop-model.md) | ✅ encoded | -| 9 | **Pulled context / explicit ports / no ambient access** | **MCP** Production-Agent server; **A2A**; "No Ambient Access & Least Privilege." | "Context is pulled, dense, JIT" hookless model — [`docs/3.0.md`](../3.0.md), [`ARCHITECTURE.md`](../ARCHITECTURE.md), [`cdlc.md`](../cdlc.md) | ✅ encoded | -| 10 | **Transparency / Chain-of-Thought / immutable provenance** | CoT in real-time UIs; deterministic actuation traces persisted (Spanner). | Append-only provenance ledger (`agentops-sdlc-provenance.v1`) — [`CLAUDE.md`](https://github.com/boshu2/agentops/blob/main/CLAUDE.md) provenance section, evidence capture under the ratchet | ✅ encoded | -| 11 | **Fix-forward over binary rollback** | "**Intervening Pull Request Problem** — binary rollback becomes unsafe… **AI-Assisted Fix-Forward**." | Coherent-arc = atomic-revert unit — [`learnings/2026-05-19-coherent-arc-rule-validation.md`](../learnings/2026-05-19-coherent-arc-rule-validation.md) | 🔶 gap → `ag-7278` | - -**Tally:** 9 ✅ encoded · 1 ⚠️ partial · 1 🔶 genuine gap. - -## The deepest convergence - -Both documents independently land on the **same control-plane architecture**: - -> A **non-deterministic reasoning core** (the LLM agent) wrapped by a **deterministic, human-governed enforcement boundary** the agent cannot bypass, no matter how the model evolves. - -- **Google:** `AI Operator` (reasoning) ↔ `Actus` (deterministic actuation gateway: mandatory dry-run, pre-flight validation, auto-downgrade L3→L2 on risk, Red Button). -- **AgentOps:** agent loop (reasoning) ↔ **CI gates + ports-and-adapters + the ratchet** (domain core never imports the shell; CI is the sole authoritative push gate; knowledge only durable as a constraint). - -This is the single most important shared insight — the answer to "how do you let a probabilistic agent touch a high-stakes system safely." - -## The three gaps this surfaced (filed as beads) - -| Bead | Point | Work | -|---|---|---| -| `ag-fjbu` | #3 | Adopt Google's **Bronze/Silver/Gold** eval-tiering + **True-vs-Observed-Precision** naming into the eval docs/skills. The pipeline exists; the maturity vocabulary doesn't. | -| `ag-wrom` | #8 | Formalize an explicit **L0–L4-style autonomy ladder** for AgentOps dispatch with named promotion gates, instead of the implicit floor→mayor→autodev progression. | -| `ag-7278` | #11 | `/council`: does the coherent-arc **atomic-revert** rule need a **fix-forward escape hatch** for high-velocity lanes? This is the one place Google is genuinely ahead of our doctrine. | - -## Honest divergences (why this isn't tautological) - -These are the **two ends of the same lifecycle**, which is exactly what makes the convergence meaningful. - -| Axis | Google SRE | AgentOps 3.0 | -|---|---|---| -| Lifecycle stage | **Run-time** production ops (mutating live state) | **Build-time** dev loop (producing validated code + context) | -| What "autonomy" acts on | Production state (drains, rollbacks, capacity) | Code + the `.agents/` context corpus | -| Always-on posture | Always-on autonomous agents **are** the product | **Ships no daemon** — always-on delegated to a substrate ([ADR-0009](../adr/ADR-0009-daemon-deletion-in-session-only.md)) | -| Evidence scale | Planetary incident volume → can A/B-test SRE practices | Repo-scale → CI gates + eval suites + e2e proofs | - -The two are **complements**: run AgentOps' loop upstream, a Google-style SRE stack downstream, and you have one continuous CDLC→SDLC→production control plane honoring the same four invariants end-to-end. - ---- - -*Source archive (full paper text): operator vault `~/learning/2026-05-29-google-sre-ai-engineering-reliable-operations.md`. Original: .* diff --git a/docs/convergence/index.md b/docs/convergence/index.md deleted file mode 100644 index 05825b6ac..000000000 --- a/docs/convergence/index.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -title: "Convergence" -description: "The record of the industry independently arriving at the structure AgentOps already runs — the thesis, the receipts, and the per-party deep-dives." -permalink: /convergence/ -last_reviewed: 2026-05-29 ---- - -# Convergence - -The industry is converging on the structure AgentOps already runs. This section is the record of it — kept honest, dated, and separate from the competitor map. - -It is **not** the [Competitive Radar](../comparisons/competitive-radar.md). The radar tracks *competitors in our lane*; this section tracks *independents arriving at our thesis from outside it*. Opposite axes: competition vs. vindication. - -## The three surfaces - -| Surface | What it is | Read it when | -|---|---|---| -| **[The Reading](the-reading.md)** | The living thesis — the *argument*, with mechanism, that the industry is converging and why. | You want the position: "is this real, and why is it happening?" | -| **[The Ledger](ledger.md)** | The dated *receipts* — a table of who/when/which invariant they reinvented. | You want the evidence and a place to add a new signal. | -| **Deep-dives** | Per-party encoding maps (e.g. **[Google SRE](google-sre.md)**) showing exactly where their conclusions land against our doctrine. | You want one party mapped point-by-point. | - -The Reading is the claim; the Ledger is the proof; the deep-dives are the receipts in full. A new signal lands as a Ledger row, gets synthesized into The Reading, and earns a deep-dive if it warrants one. - -## The thesis being converged on - -The four invariants (from the [3.0 north star](../3.0.md)): **velocity breaks human-paced practice → bounded autonomous loops**; **no self-grade** (independent verification); **knowledge becomes a constraint** (deterministic enforcement over trust); **reasoning core wrapped by a deterministic boundary**. Plus the supporting shape: spec/BDD-as-plan, eval-data-as-moat, humans up the abstraction ladder, pulled (not ambient) context, immutable provenance. - ---- - -*Start with [The Reading](the-reading.md).* diff --git a/docs/convergence/ledger.md b/docs/convergence/ledger.md deleted file mode 100644 index ca0cf822a..000000000 --- a/docs/convergence/ledger.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -title: "AgentOps Convergence Ledger" -description: "Chronological record of external parties — labs, vendors, methodologies — independently arriving at structural conclusions AgentOps already encodes. The receipts of being early." -permalink: /convergence/ledger -last_reviewed: 2026-05-29 ---- - -# Convergence Ledger - -> **The receipts.** The dated table behind [The Reading](the-reading.md): each time a major lab, vendor, or methodology independently lands on a structural conclusion already encoded in our doctrine. Distinct from the [Competitive Radar](../comparisons/competitive-radar.md), which maps *competitors in the same lane*. The ledger maps **vindication**: who got here, when, and which of our invariants they reinvented. The argument for *why* this is happening lives in [The Reading](the-reading.md). -> -> **Why keep it.** A thesis you can't date is a thesis you can't defend. When AgentOps says "AI velocity breaks human-paced practice → restructure around bounded autonomous loops," the strongest evidence is that independent teams keep reaching the same place from different starting points. Each entry is a receipt. -> -> **The rule for an entry.** It must be (a) dated, (b) external (not us), (c) a *structural* convergence on a named AgentOps invariant — not merely "they also use agents." Cite the source and the specific invariant. Honesty over hype: where they are *ahead* of us, say so. - -## The invariants being converged on - -The recurring four (from [`docs/3.0.md`](../3.0.md)): - -1. **Velocity breaks human-paced practice** — AI code volume outpaces line-by-line review. -2. **No self-grade** — the agent that does the work never grades it; verification harnesses are independent. -3. **Knowledge becomes a constraint** — learning is durable only once it compiles into a gate/test/rule; deterministic enforcement over trust. -4. **Reasoning core, deterministic boundary** — a probabilistic agent wrapped by a human-governed enforcement layer it cannot bypass. - -Plus the supporting shape: spec/BDD-as-plan, eval-data-as-moat, humans up the abstraction ladder, pulled (not ambient) context, immutable provenance. - -## Ledger - -| Date | Who | What they released / said | Invariant(s) they converged on | Relative to us | Detail | -|---|---|---|---|---|---| -| **2026-05** | **Google SRE** (Papapanagiotou, Malesevic, Heiser, Meshenberg) | Whitepaper: *"AI in SRE: How Google is Engineering the Future of Reliable Operations"* | **All four** + abstraction-ladder, eval-as-moat, spec-as-plan, decoupled reasoning/execution, immutable CoT provenance | Even (8/11) — **ahead on fix-forward** (Intervening-PR Problem) | [Encoding map](google-sre.md) · `ag-4hf7` | -| **2025-10** | **obra/superpowers** (Jesse Vincent) | Claude Code methodology plugin; [launch post](https://blog.fsck.com/2025/10/09/superpowers/), [v4](https://blog.fsck.com/2025/12/18/superpowers-4/) (2025-12) | **No self-grade** + spec/BDD-as-plan + abstraction-ladder | **Even** | Dispatches a *fresh subagent per task* with two-stage review (spec-compliance, then code-quality) — implementer ≠ validator by construction. v4 added a *separate* spec-compliance reviewer, hardening the split. "Design before code, tests before features." | -| **2025-10** | **EveryInc/compound-engineering-plugin** (Every / Kieran Klaassen) | "Compounding engineering" plugin; [repo](https://github.com/EveryInc/compound-engineering-plugin), [concept (2025-06)](https://every.to/source-code/compound-engineering-the-definitive-guide) | Context-as-compounding-artifact + spec-as-plan | **Behind** on knowledge→constraint | Loop "brainstorm → plan → work → review → **compound**"; `/ce-compound` documents learnings so the next agent doesn't re-learn. Real convergence on *context compounding* — but learnings land as **prose notes, not executable gates**. We compile knowledge into a CI gate; they don't. The cleanest place we're ahead. | -| **2025-09 / 10** | **Anthropic** | [Claude Agent SDK guidance](https://claude.com/blog/building-agents-with-the-claude-agent-sdk) (2025-09) + [Agent Skills](https://claude.com/blog/skills) (2025-10) | **Reasoning core + deterministic boundary** (SDK) + context-as-artifact (Skills) | **Even** (narrower) | SDK names the loop "gather context → act → **verify** → repeat" and *ranks* verification: **rules-based feedback best**, LLM-as-judge last & "less robust" — a probabilistic core wrapped by a deterministic check. Skills = portable, composable context folders ("build once, use across Claude apps, Code, API") — the primitive our corpus generalizes. Anthropic ships the primitives; we compose the compounding-corpus + knowledge-as-gate system. | - -> **Reading the dates.** The 2025-10 cluster (superpowers, compound-engineering) and Anthropic's 2025-09/10 primitives all *predate* the Google SRE paper (2026-05) but *postdate* the AgentOps CDLC doctrine they converge on. The pattern is independent rediscovery accelerating through late 2025 into 2026 — see [The Reading](the-reading.md) for the mechanism. - -## Backfill candidates (verify before promoting) - -Tracked-but-not-yet-confirmed convergence moments. Each needs a dated source and a specific invariant before it becomes a ledger row — do not assert without the receipt: - -- *(The first wave — superpowers, compound-engineering, Anthropic — is now confirmed and promoted above. Add the next markers here as they surface; promote on a dated, citable receipt.)* - -## How to add an entry - -1. Confirm it clears the entry rule (dated, external, structural, cited). -2. Add a row to **Ledger** (newest at top). Name the specific invariant(s), not "they use AI." -3. If it surfaces work for us (a gap they expose, a better name to adopt), file a bead and link it. -4. If a detailed mapping is warranted, write a `comparisons/-convergence.md` and link it from the row (as the Google entry does). -5. Update `last_reviewed`. - ---- - -*Companion: [Competitive Radar](../comparisons/competitive-radar.md) (competitors) · [3.0 north star](../3.0.md) (the thesis being converged on).* diff --git a/docs/convergence/the-reading.md b/docs/convergence/the-reading.md deleted file mode 100644 index 6cc1d197c..000000000 --- a/docs/convergence/the-reading.md +++ /dev/null @@ -1,82 +0,0 @@ ---- -title: "The Reading: the industry is converging on the agent operating loop" -description: "The living thesis — the argument (with mechanism, not just receipts) that the industry is independently converging on the structure AgentOps already runs." -permalink: /convergence/the-reading -last_reviewed: 2026-05-29 ---- - -# The Reading - -> **Living thesis.** This is the *argument*: the industry is converging on the structure AgentOps already runs — and here is the mechanism driving it, not just the receipts. The dated evidence lives in the [Ledger](ledger.md); per-party deep-dives sit beside it (e.g. [Google SRE](google-sre.md)). This page is the synthesis, updated each time a new signal lands. Tracking bead: `ag-4hf7`. - -## The claim - -Across labs, vendors, and methodologies, independent teams are arriving at the **same four invariants** — from different starting points, with no coordination: - -1. **Velocity breaks human-paced practice.** AI lifts code throughput ~4x; line-by-line human review and trust-based process cannot scale with it. -2. **No self-grade.** The agent that does the work cannot be the one that certifies it; verification has to be an *independent* harness. -3. **Knowledge becomes a constraint.** A learning is durable only when it compiles into a gate, a test, or a rule — deterministic enforcement over remembered advice. -4. **Reasoning core, deterministic boundary.** A probabilistic agent wrapped by a human-governed enforcement layer it cannot bypass, no matter how the model evolves. - -AgentOps encoded these as a coherent doctrine early (the [3.0 north star](../3.0.md)). The convergence is not imitation — it is **independent rediscovery**, which is the strongest available signal that a structure is load-bearing rather than stylistic. - -## Why it is happening (the mechanism) - -This is not a vibe; it is a forcing function. When AI raises code-generation throughput by ~4x, three things break at once: - -- **Review can't scale linearly.** You cannot eyeball 4x the diffs. Human attention was the rate limiter, and it just got out-run. -- **Trust-based process collapses.** "A careful senior reviews it" is not a control when the volume is machine-paced. Trust does not parallelize. -- **The bottleneck moves from writing to verifying + contextualizing.** Generation became cheap; *knowing the change is correct, in this context, against intent* became the scarce thing. - -Any team that hits this wall is pushed toward the **same destination**, because the constraint is the same: move humans **up the abstraction ladder** (review intent/design/policy, not lines); make verification **independent and mechanical**; make **context a first-class, compounding artifact** so each run starts smarter; and **wrap probabilistic agents in deterministic boundaries** so a confident-but-wrong agent cannot do unbounded damage. Different doors, one room. - -## The pattern across signals - -What the ledger entries share (updated as rows land): - -- **Google SRE (2026-05), production-ops end.** Independently reinvented our ratchet rules (their "Independent Harnesses" = our no-self-grade), spec-as-plan ("co-author specifications before code generation"), eval-data-as-moat (Bronze/Silver/Gold + LLM-as-Judge + deterministic scoring), and the reasoning/execution decoupling (AI Operator ↔ Actus = our domain-core ↔ CI-gates). They reached it from *incident operations*; we reached it from the *dev loop*. Both ends of the SDLC land on the same waist. See [the encoding map](google-sre.md). -- **Anthropic (2025-09/10), the primitives.** The Agent SDK ranks verification *exactly* as we do — **rules-based feedback best, LLM-as-judge last and "less robust"** — which is the reasoning-core/deterministic-boundary invariant stated by the model vendor itself. Agent Skills ship portable, composable context folders: the primitive our compounding corpus generalizes. Anthropic ships the building blocks; we compose the system. -- **obra/superpowers (2025-10).** Independent confirmation of **no self-grade** from the methodology lane: a fresh subagent per task, two-stage review (spec-compliance then code-quality), implementer ≠ validator by construction. v4 (2025-12) split the spec-compliance reviewer out further. The strongest external corroboration that "the agent that does the work never grades it" is load-bearing, not preference. -- **EveryInc/compound-engineering (2025-10).** Converges on **context-as-a-compounding-artifact** ("compound" step so the next agent doesn't re-learn) — but stops at *prose* compound-notes. It is the clean illustration of the gap between *agreeing context should compound* and *compiling knowledge into a constraint*. We are ahead precisely there. - -**What the cluster shows:** the convergence is not one outlier — it spans the **model vendor** (Anthropic), the **methodology lane** (superpowers, compound-engineering), and **production ops** (Google), accelerating through late 2025 into 2026. Three independent vantage points, same invariants. The one they most consistently *don't* reach is knowledge-becomes-a-constraint — everyone agrees context matters; few compile it into a gate. That is the sharpest edge to defend. - -*(Next signals append here as they enter the ledger.)* - -## Where consensus is firm vs. still forming - -| Converged principle | Consensus | Note | -|---|---|---| -| Spec/intent-as-plan | **Firm** | BDD/spec-first is table stakes across SDD tools, superpowers, and Google SRE | -| Independent verification (no self-grade) | **Firm** | Now stated by the model vendor (Anthropic SDK ranks LLM-judge *last*), the methodology lane (superpowers fresh-subagent review), and Google SRE — not just us | -| Deterministic gates over trust | **Firm** | CI-as-authority / safe-by-default actuation / "rules-based feedback best" is the shared answer across all three vantage points | -| Context as a compounding artifact | **Firming** | Anthropic Skills + compound-engineering converge on portable/compounding context; few make the *corpus the moat*, fewer still compile it to a gate | -| Knowledge → a constraint (gate/test/rule) | **We're ahead** | The one invariant the cluster consistently *misses* — compound-engineering stops at prose notes; this is the sharpest edge to defend | -| Autonomy ladders / progressive authorization | **Forming** | Google's L0–L4 is the sharpest articulation; ours is implicit (gap: `ag-wrom`) | -| Fix-forward vs. rollback at velocity | **Open** | Google argues rollback breaks at 4x (Intervening-PR Problem); our coherent-arc favors clean revert (gap: `ag-7278`) | -| Eval-data maturity vocabulary | **Forming** | Bronze/Silver/Gold + True-vs-Observed-Precision worth adopting (gap: `ag-fjbu`) | - -## Why we're early — and the honest risk - -Early is not the same as right-forever, and the moat is not the idea. - -**Ideas converge** — that is the whole thesis of this page. So being first to *articulate* "bounded autonomous loops + context-as-moat + deterministic enforcement" buys positioning, not defensibility. As the consensus firms, the *concepts* commoditize. Where defensibility actually lives: - -- **The compounding corpus** — the `.agents/` context that gets better every session is owned, not copyable from a README. -- **The encoded loop in execution** — doctrine that is wired into gates, schemas, and the ratchet, not just written down. - -The risk to name plainly: if we let the corpus and the encoded loop stagnate and lean on "we said it first," convergence erodes the edge. The ledger is a morale-and-marketing asset; the corpus is the actual moat. Keep them in their lanes. - -## How to read a new signal (the rubric) - -When a lab, vendor, or methodology ships something, run it through this before adding a ledger row: - -1. **Which invariant did they hit?** Name it from the four above (or the supporting shape). "They use agents" is not convergence. -2. **Ahead or behind us on it?** Be honest. If they are ahead (as Google is on fix-forward), that is a *gap*, not a footnote. -3. **Does it expose work for us?** If yes → file a bead, link it from the ledger row. -4. **Update this synthesis.** Add the signal to *The pattern across signals*, and move any cell in the firm/forming table that just shifted. -5. **Add the dated row** to the [Ledger](ledger.md). - ---- - -*Evidence: [Convergence Ledger](ledger.md) · Thesis being converged on: [3.0 north star](../3.0.md) · Competitor map (the inverse axis): [Competitive Radar](../comparisons/competitive-radar.md).* diff --git a/docs/create-your-first-skill.md b/docs/create-your-first-skill.md index 2050b3fe8..3194b7976 100644 --- a/docs/create-your-first-skill.md +++ b/docs/create-your-first-skill.md @@ -136,8 +136,8 @@ bash tests/docs/validate-doc-release.sh # If you added or removed a skill directory python3 scripts/generate-skill-mesh.py -# Recommended fast gate before push -scripts/pre-push-gate.sh --fast +# Recommended fast changed-surface check +ao gate check --fast --scope worktree ``` If your change affects Codex behavior or the checked-in Codex bundle, also run: diff --git a/docs/daemon-migration.md b/docs/daemon-migration.md deleted file mode 100644 index 6e7388142..000000000 --- a/docs/daemon-migration.md +++ /dev/null @@ -1,214 +0,0 @@ -# Legacy To Daemon Migration - -> ⚠️ **ARCHIVED — describes removed infrastructure.** AgentOps 3.0 has **no -> daemon**. `agentopsd` and the `internal/daemon` package were deleted (soc-2rtm0), -> and the `ao daemon` / `ao overnight` commands no longer exist (`ao daemon` → -> "unknown command"). 3.0 is in-session only; always-on / out-of-session work is -> delegated to a swappable substrate — **NTM + MCP (`ao mcp serve`) + -> managed-agents (`ao agent`)**. See [docs/3.0.md](3.0.md) ("not … a daemon") and -> [docs/dependencies.md](dependencies.md). This page is retained only as a record -> of the pre-3.0 design; nothing below is live. - -AgentOps is moving from foreground command flows to an always-on local daemon. -The daemon path is currently opt-in: existing one-shot RPI, Dream, and -wiki/forge commands continue to work unless you pass the daemon flags. - -Use this guide when migrating local automation, wrappers, or OpenClaw -integrations to `agentopsd`. - -## What Changes - -Before daemon mode, each command owned its own state: - -- `ao rpi` wrote RPI registry files directly. -- `ao overnight` owned Dream locks, stages, and summaries in the foreground. -- wiki/forge work could use direct local LLM paths when configured. -- OpenClaw-style consumers had to read projection files or `.agents` state - directly. - -In daemon mode, `agentopsd` owns the runtime ledger: - -- accepted jobs are appended to `.agents/daemon/ledger.jsonl` -- projections such as RPI status, Dream summaries, wiki outputs, and OpenClaw - snapshots are rebuilt from the ledger -- `ao doctor` reports daemon, GasCity, and OpenClaw readiness -- OpenClaw reads `/openclaw/v1/*` resources and uses authorized trigger - endpoints instead of writing `.agents` directly - -## Start The Daemon - -Run the daemon in the foreground first. Service installation remains a dry-run -planning surface until foreground readiness is boring. - -```bash -ao daemon run --addr 127.0.0.1:8765 --token "$AGENTOPS_DAEMON_TOKEN" -``` - -The daemon writes `.agents/daemon/activation.json` with its URL and readiness -metadata. Other commands use that activation file when `--daemon-url` is not -provided. - -Check readiness: - -```bash -ao daemon ready -ao daemon status -ao doctor --json -``` - -`ao doctor` warnings for daemon, GasCity, or OpenClaw mean the product runtime -is not fully available. They do not make the existing foreground command path -invalid. - -## Submit RPI Through The Daemon - -Foreground RPI remains the compatibility default: - -```bash -ao rpi --phased "ship feature" -``` - -Daemon submission is explicit: - -```bash -ao rpi --phased --daemon-submit \ - --daemon-token "$AGENTOPS_DAEMON_TOKEN" \ - "ship feature" -``` - -Use `--daemon-url` when you do not want to read the activation file. Use -`--daemon-fallback` only for interactive migration runs where foreground -execution is acceptable if the daemon is not ready. - -```bash -ao rpi status --daemon --daemon-fallback -``` - -Status reads can fall back to the local RPI registry while projections are being -migrated, but daemon job acceptance is authoritative once the ledger append -succeeds. - -## Submit Dream Through The Daemon - -Foreground Dream remains the compatibility default: - -```bash -ao overnight start --goal "compound the corpus" -``` - -Daemon submission is explicit: - -```bash -ao overnight start \ - --goal "compound the corpus" \ - --daemon-submit \ - --daemon-token "$AGENTOPS_DAEMON_TOKEN" -``` - -If daemon readiness fails, the command errors by default. Pass -`--daemon-fallback` only when the one-shot foreground path is allowed to proceed. - -Daemon Dream jobs preserve the same INGEST, REDUCE, MEASURE, and COMMIT -semantics. The difference is ownership: job acceptance, worker session refs, and -terminal state are recorded in the daemon ledger before projections claim them. - -## Wiki And Forge - -Daemon-backed wiki/forge jobs use the `AgentWorker` runtime and GasCity-backed -Codex or Claude sessions. - -Legacy local Ollama/Gemma paths are not product defaults. They require an -explicit escape hatch: - -```bash -ao forge transcript --tier=1 --legacy-local-llm --model gemma2:9b -``` - -or: - -```bash -AGENTOPS_FORGE_LEGACY_LOCAL_LLM=1 ao forge transcript --tier=1 --model gemma2:9b -``` - -Dream local curator diagnostics may still report Ollama/Gemma state when -configured with `AGENTOPS_DREAM_CURATOR_ENGINE=ollama`, but daemon-backed -wiki/forge does not require a Gemma model. - -### Legacy Local LLM Deprecation Schedule - -This schedule applies only to the local Ollama/Gemma bridge. It does not remove -GasCity-backed Codex/Claude workers or the `AgentWorker` contract. - -| Target | Behavior | -|--------|----------| -| v2.40.x | Legacy local LLM requires `--legacy-local-llm`, `AGENTOPS_FORGE_LEGACY_LOCAL_LLM=1`, or `AGENTOPS_DREAM_CURATOR_ENGINE=ollama` | -| v2.41.x | Legacy local LLM emits a CLI deprecation warning whenever the path is used | -| v2.42.x | Legacy local LLM docs move out of product-path examples and remain only in compatibility notes | -| v3.0.0 | Core CLI local Ollama/Gemma bridge may be removed or moved to an external compatibility plugin | - -Do not add new product features to the legacy bridge. New daemon wiki/forge work -should target AgentWorker sessions backed by GasCity. - -## GasCity Readiness - -GasCity is the preferred substrate for headless Codex and Claude worker -sessions. The AgentOps adapter distinguishes: - -- missing `gc` binary -- version below the supported bridge minimum -- API unavailable -- controller or provider not ready -- CLI fallback being available but not equivalent to API/SSE readiness - -Normal CI uses fake GasCity fixtures. Live GasCity checks should stay opt-in -with `AGENTOPS_LIVE_GASCITY=1`. - -The old daemon↔GasCity integration matrix was retired with the daemon in 3.0; -the current Gas City story is the optional out-of-session substrate, documented -in [dependencies](dependencies.md). - -## OpenClaw Migration - -OpenClaw should consume daemon projections through local HTTP: - -- `GET /openclaw/v1/health` -- `GET /openclaw/v1/snapshot/latest` -- `GET /openclaw/v1/runs` -- `GET /openclaw/v1/jobs` -- `GET /openclaw/v1/wiki` - -OpenClaw must not write `.agents` directly. When it needs work to happen, it -calls an authorized trigger endpoint such as `/openclaw/v1/triggers/jobs`; the -daemon validates local trust, appends the ledger event, and returns accepted job -IDs. - -## Rollback And Fallback - -During migration: - -- keep foreground RPI and Dream commands available for compatibility -- use daemon fallbacks only when the caller can tolerate foreground execution -- treat `.agents/daemon/ledger.jsonl` as authoritative for daemon-accepted work -- rebuild projections from the ledger after crashes or partial projection writes -- mark degraded projections instead of claiming successful state from stale files - -If you need to disable daemon mode, remove the `--daemon-submit` flag and run -the foreground command path. Do not edit `.agents/daemon/ledger.jsonl` by hand. - -## Validation Checklist - -Run this before making daemon mode the default for a workflow: - -```bash -ao daemon ready -ao daemon status -ao doctor --json -ao rpi verify --latest --json -scripts/validate-daemon-product-e2e.sh --fixture -scripts/check-closeout-gate.sh --json -scripts/pre-push-gate.sh --fast -``` - -The closeout proof should also include OpenClaw health, GasCity bridge -diagnostics, fake GasCity worker fixtures, ledger replay, projection rebuild, -state-machine invariants, boundary failpoints, and worktree disposition. diff --git a/docs/dependencies.md b/docs/dependencies.md index 3d202b18a..e7ba5915e 100644 --- a/docs/dependencies.md +++ b/docs/dependencies.md @@ -1,38 +1,24 @@ # Dependencies -> The complete tool-dependency declaration for AgentOps. This is the **canonical detail** — the README "Requirements" section and the [shared skill fallback table](https://github.com/boshu2/agentops/blob/main/skills/shared/SKILL.md) summarize it. +AgentOps's core protocol requires an agent runtime capable of reading and +writing files. It does not require Git, the `ao` CLI, a tracker, a queue, a +network connection, or a delivery system. -AgentOps is designed to degrade gracefully. Almost everything is optional: skills check for a tool before using it and fall back when it is absent (the contract is in [`skills/shared/SKILL.md`](https://github.com/boshu2/agentops/blob/main/skills/shared/SKILL.md) "CLI Availability Pattern"). Only an agent runtime plus `git` is genuinely required to get value. +| Tool or substrate | Role | Core requirement | +|---|---|---| +| Agent runtime | Runs Plan, Implement, and a distinct fresh Validate context | Required | +| Filesystem + SHA-256 helper | Computes content identity and stores `verdict.v2` | Required | +| `ao` | Repository utilities and ordinary deterministic `ao gate check` checks | Optional | +| Git or a forge | Version control, review, and delivery owned by the caller | Optional | +| `br`, `bv`, or another tracker | Caller-owned work organization | Optional | +| NTM, Agent Mail, Gas City, Swarm, or another factory | Caller-selected execution or coordination adapter | Optional | +| Council or Dueling Idea Genies | Additional independent judgment strategy | Optional | +| Go | Builds the optional `ao` CLI from source | Optional | -## Classification +Optional tools may produce factual evidence or execute explicit packets. Their +attempts, queues, leases, commits, or delivery state never become AgentOps +correctness state. Missing optional tools can reduce what was checked, but they +cannot reorder RPI or manufacture a PASS. -| Tool | Class | Purpose | Required? | Fallback if absent | -|------|-------|---------|-----------|--------------------| -| **agent runtime** (`claude` / `codex` / `opencode`) | REQUIRED | The coding harness AgentOps sits on top of. At least one is needed — AgentOps adds bookkeeping, gates, and a corpus to it. | **Required** (one of) | None — AgentOps has nothing to drive without a runtime. The installer warns and points at the install links. | -| `git` | REQUIRED | Version control; `.agents/` state lives next to your code, worktrees isolate parallel work, provenance ties artifacts to commits. | **Required** | None — the SDLC control plane assumes a git repo. | -| `ao` | REQUIRED (recommended) | The AgentOps CLI: repo-native bookkeeping, retrieval (`ao inject`/`ao lookup`), health (`ao doctor`), the operating loop, validation gates. | Strongly recommended | Skills still guide the workflow, but the knowledge flywheel, gates, and loops that need the binary are unavailable. Write learnings to `.agents/learnings/` by hand. | -| `br` + `bv` (beads_rust) | TRACKING | Offline, git-JSONL-backed issue tracking (`_beads/issues.jsonl`) plus graph-aware triage. The mandatory task-tracking surface for non-trivial work. | Required for tracked work in this repo | Use the harness task list / plain markdown only for trivial or untracked work. Install/use `br` for persistent issue tracking. | -| `bd` / Dolt as **this repo's** tracker | HISTORICAL | AgentOps' OWN tracking moved off bd/Dolt to `br`; the `.beads/` config is retained only for migration records and historical runbooks. (bd/dolt itself is NOT retired — it is the gascity SUBSTRATE store, a first-class different layer.) | Not required for this repo's tracking | Do not use `bd` for current AgentOps tracking; route through `BEADS_DIR="$(ao beads dir)" br ...`. bd/dolt is legitimate when operating the gascity substrate. | -| out-of-session substrate (`ntm` / `ao agent` / MCP) | ORCHESTRATION | Runs whole operating-loop sessions out of session — an **NTM** tmux swarm, **managed-agents** via `ao agent`, or the **MCP** tool surface (`ao mcp serve`). AgentOps owns none of it; it adopts a substrate, the way it adopts `br`. | Optional (out-of-session only) | Run the loop in-session yourself (`/rpi`, `/evolve`). The substrate adds only always-on orchestration. | -| `mcp_agent_mail` | ORCHESTRATION | Multi-agent coordination — inboxes and advisory file leases so parallel lanes don't collide on the same files (the `agent-mail` skill wraps it). | Optional (multi-lane only) | Single-lane work needs no coordination bus. | -| `gh` (GitHub CLI) | PR / CI | Open and manage PRs, query CI status, drive the ship/merge flow. | Optional | Open PRs through the web UI; skip automated PR/merge steps. | -| `go` | BUILD-FROM-SOURCE | Toolchain to build `cli/bin/ao` from source (`go 1.26`, per `cli/go.mod`). | Optional | Install a prebuilt `ao` via Homebrew, the install script, or release binaries — no Go needed. | -| `jq` | UTILITY | Parse `--json` output from `ao`, `br`, and `gh` in scripts and dispatch loops. | Optional | Read JSON manually or use non-JSON output modes; some script automations are unavailable. | -| `rg` (ripgrep) | UTILITY | Fast code/corpus search used by research and several scripts. | Optional | Falls back to `grep`/`git grep`; slower but functional. | -| `curl` | UTILITY | Fetch the installer and release assets during install. | Required only for curl-pipe install | Download release binaries manually or install via Homebrew. | -| `openssl` | UTILITY | Hashing/randomness in some scripts (e.g. `openssl dgst`); paired with the sha256 tools. | Optional | `sha256sum`/`shasum` cover the hashing path; most flows do not need openssl. | -| `sha256sum` / `shasum -a 256` | UTILITY | Verify download integrity and compute content hashes (codex artifact hashes, install checksums). | Optional | Either tool satisfies the need; scripts detect whichever is present. | -| `tmux` | UTILITY | Session multiplexing for streamed/long-running RPI runs and an NTM agent swarm's sessions. | Optional | RPI runs in non-tmux modes; a managed-agents substrate needs no local tmux. | -| `cass` | UTILITY | Session-history search (`ao search` upstream backend). | Optional | Skip transcript search. Note: "install cass for session history." | -| `cm` (cass_memory_system) | UTILITY | Procedural memory for agents — durable playbooks and mistake-guards; pairs with `cass`. | Optional | Lessons stay in `.agents/` markdown, carried by hand between sessions. | -| `ubs` (ultimate_bug_scanner) | UTILITY | Deterministic static bug scanning that feeds the review/validation gate before you ship. | Optional | Skill-driven review only; no deterministic pre-ship bug scan. | -| `ACFS` (agentic_coding_flywheel_setup) | BOOTSTRAP | One-command environment that installs the recommended toolchain above (`br`/`bv`, `ntm`, `cass`, `cm`, `ubs`, agent mail, `dcg`). | Optional | Install each tool individually. | -| `awk`, `bash` | UTILITY | Shell plumbing the scripts and `ao doctor` checks rely on. | Effectively always present | POSIX baseline; present on every supported platform. | - -## Notes - -- **Health check.** `ao doctor` probes the tools it depends on (`ao`, `awk`, `bash`, `br`, `cass`, `git`, `tmux`) and reports what is missing without failing the workflow. -- **Install helpers.** `scripts/install.sh` detects package managers (`brew`, `apt`/`apt-get`, `dnf`/`yum`, `pacman`, `zypper`) and runtimes (`claude`, `codex`, `opencode`) and adapt; nothing here is hard-required beyond `curl` for the curl-pipe path. -- **Out-of-session vs in-session.** The only orchestration dependency is an out-of-session substrate (NTM / managed-agents / MCP), and it is strictly out-of-session and optional. In-session, an agent runtime + `git` (+ `ao`/`br` recommended) is the whole stack. See [docs/3.0.md](https://github.com/boshu2/agentops/blob/main/docs/3.0.md). -- **Graceful degradation is a contract, not a courtesy.** Every skill that shells out to an optional tool must check availability first and inform the user what was skipped — see the [shared fallback table](https://github.com/boshu2/agentops/blob/main/skills/shared/SKILL.md). -- **Migrating off a retired surface** (bd, hooks, the daemon, the removed `rpi`/`evolve` verbs, `ao recall`)? The living map is [MIGRATION.md](MIGRATION.md). +For CLI installation details, see the [README](../README.md). For the exact +core boundary, see the [operating loop](architecture/operating-loop.md). diff --git a/docs/documentation-index.md b/docs/documentation-index.md index 719fef53d..7104ee4b4 100644 --- a/docs/documentation-index.md +++ b/docs/documentation-index.md @@ -41,12 +41,10 @@ Dated plans, audits, releases, and archive material are historical evidence, not - [Optional dispatch adapter](contracts/orchestration-ports.md) - [Contract: PMF Evidence Gate](contracts/pmf-evidence.md) - [Producer-Defect Recurrence Contract](contracts/producer-defect-register.md) -- [Contract: registry.json as derived artifact, not versioned source](contracts/registry-as-derived.md) -- [Release Readiness Contract](contracts/release-readiness.md) +- [Generated skill projections](contracts/registry-as-derived.md) - [Retrieval Comparison Contract](contracts/retrieval-comparison.md) - [Scenario → Test Linkage Contract](contracts/scenario-test-linkage.md) - [Scope Escape Report Template](contracts/scope-escape-report.md) -- [Skill-Flow Connectivity Contract](contracts/skill-flow.md) - [Skill ports and adapters](contracts/skill-ports-and-adapters.md) - [Ubiquitous language](contracts/ubiquitous-language.md) - [Update Principles Contract](contracts/update-principles.md) diff --git a/docs/domain-practice-packets.md b/docs/domain-practice-packets.md index 6bcc87140..ad3e921b2 100644 --- a/docs/domain-practice-packets.md +++ b/docs/domain-practice-packets.md @@ -58,7 +58,7 @@ above them. | `docs/standards/` and `skills/standards/` | Coding and review conventions. | | `br` beads (`_beads/`) | Current work, dependencies, acceptance criteria, and ownership. | | `ao context assemble` | Builds a phase-scoped briefing from goals, history, intel, task, and protocol. | -| `ao context packet` | Shows ranked findings, planning rules, pre-mortem checks, and next-work context. | +| `ao context packet` | Shows ranked findings, planning rules, premortem checks, and next-work context. | | `/council` | Turns the packet into a shared evidence frame for independent judges. | | `/provenance` and `/trace` | Reconstruct where packet claims came from and how they moved through later artifacts. | | RPI execution packet | Carries the accepted objective, plan path, contract surfaces, validation lanes, and done criteria across discovery, implementation, and validation. | diff --git a/docs/domains/README.md b/docs/domains/README.md deleted file mode 100644 index 8a663c08c..000000000 --- a/docs/domains/README.md +++ /dev/null @@ -1,69 +0,0 @@ -# Domain-Slice Manifests - -This directory holds per-domain `manifest.yaml` files — durable, git-tracked -declarations of DDD bounded context slices once consumed by the now-removed -`ao rpi phased --domain` engine (superseded in 3.0 by the seven-move operating loop). - -## One directory per domain - -``` -docs/domains/ - / - manifest.yaml # source of truth for this domain slice -``` - -The `` matches the `domain` field in the manifest and was passed -directly to `--domain`: - -``` -# ao rpi was removed in 3.0 — these --domain invocations no longer exist; -# shown only to illustrate how mapped to --domain. Drive the -# loop via the /rpi skill over the seven-move operating loop instead. -ao rpi phased --domain goals "Add satisfaction gate to ao goals measure" # removed in 3.0 -ao rpi phased --domain rpi "Wire domainSliceManifest into phased loader" # removed in 3.0 -``` - -## What a manifest declares - -Each `manifest.yaml` validates against -`schemas/domain-slice-manifest.v1.schema.json` and records: - -| Field | Purpose | -|---|---| -| `domain` | Short machine-readable name (matches this directory name) | -| `bounded_context` | One-sentence DDD statement: what this slice owns and does NOT own | -| `directive_ids` | Stable GOALS.md directive IDs (`d-`) whose acceptance this slice owns | -| `scenario_ids` | Promoted spec scenario IDs from `spec/scenarios/` | -| `context_roots` | Repo-relative implementation paths loaded as agent context | -| `allowed_read_globs` | Read-fence allow list for agents working in this slice | -| `denied_read_globs` | Read-fence deny list (overrides allowed globs when both match) | -| `validation_commands` | Ordered validation steps run after each implementation phase | -| `owner` | Team or person responsible for this slice | - -## How it fits with other domain surfaces - -The manifest is **not** a replacement for: - -- **`skills/domain/SKILL.md`** — the ubiquitous language vocabulary (nouns and - structural primitives). Load it for terminology; the manifest uses those terms. -- **`docs/contracts/context-map.md`** — the generated architecture view of skill - relationships by hexagonal role. It shows how skills relate; the manifest - scopes which files an agent loads. -- **Skill frontmatter** (`hexagonal_role`, `consumes`, `produces`) — per-skill - classification. The manifest declares the aggregate scope of a slice that may - span multiple skills. - -See [ADR-0013](../adr/ADR-0013-domain-slice-manifest-contract.md) for the full -reconciliation and the rationale for each design decision. - -## Adding a new domain slice - -1. Create `docs/domains//manifest.yaml`. -2. Validate it: `python3 -c "import jsonschema, json, yaml; jsonschema.validate(yaml.safe_load(open('docs/domains//manifest.yaml')), json.load(open('schemas/domain-slice-manifest.v1.schema.json')))"`. -3. Commit the manifest (it is a durable tracked artifact per ADR-0003). -4. Optionally promote the example below as a starting point. - -## Example - -See [`example/manifest.yaml`](example/manifest.yaml) for a fully populated, -schema-valid example. It uses the `goals` domain as a worked illustration. diff --git a/docs/domains/example/manifest.yaml b/docs/domains/example/manifest.yaml deleted file mode 100644 index 239623a51..000000000 --- a/docs/domains/example/manifest.yaml +++ /dev/null @@ -1,82 +0,0 @@ -# Example domain-slice manifest — illustrates all fields. -# Validates against schemas/domain-slice-manifest.v1.schema.json. -# See docs/adr/ADR-0013-domain-slice-manifest-contract.md. -# -# This example uses the 'goals' domain as a worked illustration. -# To create a real domain manifest: copy this file to docs/domains//manifest.yaml -# and fill in the values appropriate for your bounded context. - -schema_version: 1 - -# Short machine-readable name — matches the directory under docs/domains/ -domain: example - -# Semver for this manifest (not the implementation). Increment on structural changes. -version: 0.1.0 - -# One sentence: what this slice owns and explicitly does NOT own. -bounded_context: > - Owns GOALS.md directive lifecycle, ao goals subcommands, and the - directive-to-scenario executable-spec link; does NOT own scenario holdout - isolation, RPI phase scheduling, or skill frontmatter. - -# Stable directive IDs from GOALS.md whose acceptance this slice owns. -# Pattern: d-. See GOALS.md for the canonical list. -directive_ids: - - d-goals-measure - - d-goals-scenarios - -# Promoted spec scenario IDs from spec/scenarios/ that constitute delivery -# of the directives above. See ADR-0003 for the promoted_spec lifecycle. -scenario_ids: - - s-2026-05-17-001 - - s-2026-05-17-002 - -# Repo-relative paths forming the primary implementation surface. -# ao rpi phased --domain loads these as agent context. -context_roots: - - cli/cmd/ao/goals.go - - cli/cmd/ao/goals_scenarios.go - - cli/internal/goals/ - - spec/scenarios/ - - schemas/scenario.v1.schema.json - - GOALS.md - -# Read-fence allow list. Agents working in this slice may read files -# matching these globs (gitignore syntax). -allowed_read_globs: - - cli/cmd/ao/goals*.go - - cli/internal/goals/** - - spec/scenarios/** - - schemas/scenario.v1.schema.json - - schemas/domain-slice-manifest.v1.schema.json - - docs/adr/ADR-0003-*.md - - docs/adr/ADR-0013-*.md - - docs/domains/** - - GOALS.md - -# Read-fence deny list. Denied globs take precedence over allowed_read_globs. -# Use this to keep agents away from unrelated implementation details. -denied_read_globs: - - .agents/holdout/** - - cli/cmd/ao/rpi_*.go - - cli/internal/search/** - -# Ordered validation steps run after each implementation phase. -# All commands run from repo root unless working_dir is specified. -validation_commands: - - label: build - command: "cd cli && go build ./cmd/ao/..." - working_dir: cli - timeout_seconds: 60 - - label: unit-tests - command: "cd cli && go test ./cmd/ao/... ./internal/goals/..." - working_dir: cli - timeout_seconds: 120 - - label: lint - command: "cd cli && go vet ./cmd/ao/... ./internal/goals/..." - working_dir: cli - timeout_seconds: 30 - -# Team or person responsible for this domain slice. -owner: maintainers diff --git a/docs/evolve-setup.md b/docs/evolve-setup.md deleted file mode 100644 index 34ea34827..000000000 --- a/docs/evolve-setup.md +++ /dev/null @@ -1,48 +0,0 @@ -# Setting Up /evolve - -Bootstrap with `ao goals init` — it interviews you about your repo and generates mechanically verifiable goals. Or write them by hand: - -```markdown -# GOALS.md - -## test-pass-rate -- **check:** `make test` -- **weight:** 10 -All tests pass. - -## code-complexity -- **check:** `gocyclo -over 15 ./...` -- **weight:** 6 -No function exceeds cyclomatic complexity 15. -``` - -Migrating from GOALS.yaml? Run `ao goals migrate --to-md`. Manage goals with `ao goals steer add/remove/prioritize` and prune stale ones with `ao goals prune`. - -`/evolve` measures them, picks the worst gap by weight, runs `/rpi` to fix it, re-measures ALL goals (regressed commits auto-revert), and loops. It commits locally — you control when to push. Kill switch: `echo "stop" > ~/.config/evolve/KILL` - -**Built for overnight runs.** Cycle state lives on disk, not in LLM memory — survives context compaction. Every cycle writes to `cycle-history.jsonl` with verified writes, a regression gate that refuses to proceed without a valid fitness snapshot, and a watchdog heartbeat for external monitoring. If anything breaks the tracking invariant, the loop stops rather than continuing ungated. See `skills/SKILL-TIERS.md` for the two-tier execution model that keeps the orchestrator visible while workers fork. - -Maintain over time: `/goals` shows pass/fail status, `/goals prune` finds stale or broken checks. - -## Pairing GOALS.md with PROGRAM.md - -Use `GOALS.md` for strategic fitness and `PROGRAM.md` for operational control. - -- `GOALS.md` answers what good looks like. -- `PROGRAM.md` answers what the autonomous loop may touch, how one experiment is bounded, which validations decide success, and when to stop or escalate. -- `/evolve` now loads `PROGRAM.md` before cycle 1, filters out-of-scope work, and uses the program's validation and decision policy in its cycle keep/revert gate. - -Initialize the operational contract with: - -```bash -ao autodev init -ao autodev validate -``` - -This split keeps repo goals stable while allowing the autonomous runtime policy to evolve independently. See [Autodev Program Contract](contracts/autodev-program.md) for the required sections and semantics. - -## See Also - -- [README.md](https://github.com/boshu2/agentops/blob/main/README.md) — repo overview and `/evolve` demo -- [How It Works](how-it-works.md) — runtime mechanics -- [The Science](the-science.md) — decay model behind fitness scoring diff --git a/docs/knowledge-flywheel.md b/docs/knowledge-flywheel.md index b42b57d49..5b0d3c954 100644 --- a/docs/knowledge-flywheel.md +++ b/docs/knowledge-flywheel.md @@ -1,175 +1,25 @@ -# The Knowledge Flywheel +# Optional Learning Loop -> Agents are stateless. The repo learns. +AgentOps core ends when Validate writes a durable verdict and RPI reports it. +Learning is deliberately off the critical path. -## The Problem +When a caller wants longitudinal analysis, it may explicitly invoke Learn over +a collection of immutable `verdict.v2` artifacts. Learn can group concrete +finding observations, identify recurrence across distinct objectives, and +suggest an advisory producer-rule candidate. -Coding agents forget everything between sessions. Notes alone do not fix that. If a solved problem is not extracted, curated, retrieved, and reused, the repo keeps paying for the same lesson. +Learn does not: -AgentOps frames this as two of the three gaps in the [Context Lifecycle Contract](context-lifecycle.md): +- change a completed verdict; +- repair or re-plan work; +- choose a next invocation; +- activate a rule or deterministic check; +- mutate Git, a tracker, or delivery state; or +- block RPI when its own storage or analysis is unavailable. -- **Durable learning** (Gap 2) — solved problems recur because knowledge is not extracted, scored, and surfaced. -- **Loop closure** (Gap 3) — completed work does not produce better next work because learnings are not harvested, promoted, or fed back into future sessions. +Promotion from an observed pattern into a skill, test, or repository rule is a +separate caller-authorized change with its own Plan, Implement, and Validate +cycle. This preserves the useful compounding idea without making bookkeeping a +condition for finishing ordinary work. -The flywheel is the mechanism that closes both gaps. Each stage below maps to one or both. - -## The Solution - -AgentOps turns session output into durable environment state. AgentOps 3.0 ships **zero hooks** — the flywheel runs through explicit lifecycle commands, and the **local pre-push Go gate (`ao gate check`) is the release authority** (CI is a tag/PR/manual backstop). The same start/closeout stages work on every runtime without depending on hook side effects; if you want a bounded gate of your own, author it with the `hooks-authoring` skill (AgentOps does not ship one). - -## Runtime Modes - -| Mode | Start path | Closeout path | What runs the stages | -|------|------------|---------------|----------------------| -| Any runtime (hookless default) | `ao inject` / `ao codex start`, then drive the seven-move operating loop via the `/rpi` skill (`ao rpi phased` was **removed** in 3.0) | `ao forge transcript` + `ao flywheel close-loop` (or `ao codex stop`) | Startup context assembly, transcript discovery, citation capture, and close-loop status through explicit commands — portable across Claude, Codex, and OpenCode | -| Self-authored gate (optional) | A hook you write with the `hooks-authoring` skill | A hook you write with the `hooks-authoring` skill | Only what you choose to wire; AgentOps ships no hooks, so nothing fires unless you author it | - -## The Flywheel - -``` -┌───────────────────────────────────────────────────────────────────────┐ -│ THE KNOWLEDGE FLYWHEEL │ -│ │ -│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ -│ │ 1. WORK │─>│ 2. FORGE │─>│ 3. POOL │─>│ 4. PROMOTE │ │ -│ │ Session │ │ Extract │ │ Score & │ │ Graduate │ │ -│ │ │ │ │ │ Queue │ │ │ │ -│ └────────────┘ └────────────┘ └────────────┘ └────────────┘ │ -│ ^ │ │ -│ │ ┌────────────┐ ┌────────────┐ │ │ -│ └─────────│ 6. INJECT │<─│5. LEARNINGS│<──────────┘ │ -│ │ Surface │ │ Permanent │ │ -│ │ & Cite │ │ Knowledge │ │ -│ └────────────┘ └────────────┘ │ -│ │ -│ Each citation feeds back: utility scores update, high-utility │ -│ knowledge surfaces more often, low-utility decays. This is the │ -│ compounding effect — sessions get smarter because the best │ -│ knowledge rises and the noise sinks. │ -└───────────────────────────────────────────────────────────────────────┘ -``` - -## The Six Stages - -Each stage maps to the gaps it closes: **L** = Durable Learning (Gap 2), **C** = Loop Closure (Gap 3). - -### Stage 1: Work (source material) - -You build, debug, research, or plan. In hook-capable runtimes, transcripts are typically available directly from the runtime. In Codex, AgentOps prefers archived session transcripts and can fall back to `~/.codex/history.jsonl` when no archived transcript exists. - -### Stage 2: Forge — **L** (extraction) - -At closeout, `ao forge transcript` or `ao codex stop` parses the transcript and extracts structured knowledge — decisions, solutions, learnings, failures, and references. Each becomes a markdown file in `.agents/knowledge/pending/`. In hook-capable runtimes, the `SessionEnd` hook (`session-end-maintenance.sh`) triggers this automatically; the `compile-session-defrag.sh` hook runs deduplication and defrag in the same event. - -### Stage 3: Pool — **L** (curation) - -`ao flywheel close-loop` ingests pending files and scores each on five dimensions: - -| Dimension | What it measures | -|-----------|-----------------| -| Specificity | Names concrete files, functions, error messages | -| Actionability | A future session can act on this without more context | -| Novelty | New knowledge, not repetition | -| Context | Explains WHY, not just WHAT | -| Confidence | How certain the extraction is | - -Candidates are tiered: **Gold** (>0.85), **Silver** (0.70–0.85), **Bronze** (0.50–0.70), or **Discard** (<0.50). - -### Stage 4: Promote — **L** + **C** (graduation) - -Candidates that pass the promotion gate graduate to permanent knowledge: - -- **Age gate:** Must be >24h old (prevents promoting noise from the current session) -- **Citation gate:** Must have been cited at least once (proves another session found it useful) -- **Tier gate:** Gold and Silver auto-promote. Bronze requires 3+ citations. - -This is where durable learning and loop closure intersect: only knowledge that a later session actually cited gets promoted, proving the loop closed at least once. - -### Stage 5: Learnings — **L** (permanent store) - -Promoted knowledge lives in `.agents/learnings/` and `.agents/patterns/`. The maturity lifecycle: - -``` -provisional → established → archived -``` - -AgentOps maturity controls (`ao maturity --expire`, `ao maturity --evict`, `ao dedup`, `ao contradict`) prevent the corpus from decaying into stale noise. Maintenance runs through the active lifecycle path: hook-capable runtimes run it from hooks, while Codex runs the same hygiene from `ao codex start` / `ao codex stop`. - -### Stage 6: Inject — **C** (retrieval closes the loop) - -At session start and during work, `ao inject`, `ao lookup`, or `ao codex start` -retrieves the most relevant learnings for the current task. Startup retrieval -prefers task-scoped context such as handoff goals and active beads instead of -generic commit-subject fallbacks. `ao lookup` records citations automatically. -When `ao search` results are actually adopted, use `ao search --cite -retrieved|reference|applied` to record that decision in-band instead of relying -on tribal workflow knowledge. Each citation is the signal that drives the -feedback loop. - -Citations with positive feedback increase the learning's utility score → higher utility → ranked higher in next injection → cited more → utility increases more → **compounding**. This is the loop closure mechanism: completed work produces better next work because the flywheel feeds validated knowledge back into future sessions. - -## The Compounding Math - -The flywheel equation: - -``` -dK/dt = I(t) - δ·K + σ·ρ·K -``` - -- **σ** — Retrieval coverage: unique surfaced artifacts / total retrievable artifacts, scale 0.0–1.0 -- **ρ** — Decision influence rate: unique surfaced artifacts later evidenced by `reference` or `applied` citations / surfaced artifacts, scale 0.0–1.0 -- **δ** — Knowledge age: average age of active learnings in days. The theoretical decay rate (0.17/week from Darr 1995) motivates the metric, but the CLI implementation (`metrics_health.go`) measures delta as days, not a weekly rate. -- **Escape velocity:** When `σ × ρ > δ/100`, knowledge compounds faster than it ages out. The `/100` normalizes delta (days) to a ratio comparable with sigma and rho. - -### Golden Signals - -Escape velocity is necessary, but not sufficient. Four golden signals measure -whether the flywheel is actually compounding: - -```bash -ao flywheel status -``` - -| Signal | Question | Healthy | -|--------|----------|---------| -| Velocity Trend | Is σρ-δ improving over time? | Positive slope | -| Citation Pipeline | Are citations delivering value? | >60% high-utility | -| Research Closure | Is research being mined into learnings? | <10% orphaned | -| Reuse Concentration | Is the whole pool active or just a few items? | Gini < 0.4 | - -## Knowledge Stores - -| Store | Content | Updated By | -|------|---------|------------| -| `.agents/knowledge/pending/` | Forge output awaiting pool ingestion | `ao forge`, `ao codex stop` | -| `.agents/knowledge/pending/.quarantine/` | Low-quality or unsafe pending extracts held out of promotion | Pool hygiene, promotion gates, and close-loop maintenance | -| `.agents/pool/` | Scored candidates awaiting promotion | `ao flywheel close-loop` | -| `.agents/learnings/` | Promoted, permanent knowledge | Pool promotion pipeline | -| `.agents/patterns/` | Promoted decision patterns | Pool promotion pipeline | -| `.agents/research/` | Scoped investigations | `/research` | -| `.agents/findings/registry.jsonl` | Reusable findings | `/pre-mortem`, `/vibe`, `/post-mortem` | -| `.agents/ao/citations.jsonl` | Citation trail | `ao inject`, `ao lookup`, `ao search --cite`, `ao codex start` | -| `.agents/ao/feedback.jsonl` | Utility feedback | `ao flywheel close-loop` | -| `.agents/ao/metrics/` | Baseline snapshots for trend tracking | `ao metrics baseline` | -| `.agents/ao/codex/startup-context.md` | Explicit startup context assembled for hookless Codex sessions | `ao codex start` | -| `.agents/ao/codex/state.json` | Last Codex start/stop lifecycle state | `ao codex start`, `ao codex stop` | - -## The Compounding Effect - -| Gap | Without the flywheel | With AgentOps flywheel | -|-----|----------------------|------------------------| -| Durable learning | The same bug is rediscovered each session | `ao lookup` retrieves the prior failure before planning starts | -| Durable learning | Notes accumulate without pressure | AgentOps promotion gates ensure only cited, high-quality knowledge survives | -| Durable learning | Stale knowledge pollutes retrieval | `ao maturity`, `ao dedup`, and `ao contradict` keep the corpus current | -| Loop closure | Handoffs rely on chat memory | AgentOps stores handoffs and phased state on disk in `.agents/` | -| Loop closure | Session 50 starts from scratch | Session 50 starts with 50 sessions of flywheel-promoted wisdom | -| Loop closure | Completed work teaches nothing | `/post-mortem` + finding compiler + `ao-flywheel-close.sh` harvest and compile learnings automatically | - -## See Also - -- [Context Lifecycle Contract](context-lifecycle.md) -- [How It Works](how-it-works.md) -- [Codex Hookless Lifecycle](architecture/codex-hookless-lifecycle.md) -- [Primitive Chains](architecture/primitive-chains.md) -- [Brownian Ratchet](brownian-ratchet.md) -- [The Science](the-science.md) +See [the producer-defect recurrence contract](contracts/producer-defect-register.md). diff --git a/docs/levels/L1-basics/README.md b/docs/levels/L1-basics/README.md deleted file mode 100644 index bd6718b50..000000000 --- a/docs/levels/L1-basics/README.md +++ /dev/null @@ -1,44 +0,0 @@ -# L1 — Basics - -Single-session work with Claude Code. The gateway level. - -## What You'll Learn - -- Using `/research` to explore a codebase -- Using `/implement` to make changes -- Basic git workflow with Claude Code -- Reading existing code before making changes - -## Prerequisites - -- Claude Code installed -- Git repository initialized -- No prior AgentOps experience required - -## Available Commands - -| Command | Purpose | -|---------|---------| -| `/research ` | Explore codebase, understand patterns | -| `/implement` | Make targeted changes | - -## Key Concepts - -- **Single session**: All work happens in one conversation -- **No persistence**: Knowledge lives only in the conversation -- **Git-backed**: Changes exist in version control - -## What's NOT at This Level - -- No `.agents/` directory -- No issue tracking -- No multi-session memory -- No parallelization - -## Demo - -See the `demo/` directory for real session transcripts. - -## Next Level - -Once comfortable with single-session work, progress to [L2-persistence](../L2-persistence/README.md) to add cross-session memory. diff --git a/docs/levels/L1-basics/demo/implement-session.md b/docs/levels/L1-basics/demo/implement-session.md deleted file mode 100644 index afd308378..000000000 --- a/docs/levels/L1-basics/demo/implement-session.md +++ /dev/null @@ -1,64 +0,0 @@ -# Demo: /implement Session - -A realistic transcript of adding a feature with validation and commit. - ---- - -## Before - -- You've already researched the auth module -- Know where to make changes -- Tests exist and pass - ---- - -## Session - -``` -You: /implement - Add logging when users fail authentication - -Claude: I'll add logging for failed auth attempts. - -[Read] src/auth/middleware.ts - -I see the auth check at line 23. I'll add logging before the 401 response. - -[Edit] src/auth/middleware.ts -- Added: import { logger } from '../utils/logger' -- Added at line 28: logger.warn('Auth failed', { ip: req.ip, path: req.path }) - -[Bash] pytest tests/test_auth.py -===== 4 passed in 0.3s ===== - -[Bash] npm run lint -No issues found - -Changes validated. Ready to commit? - -You: Yes - -[Bash] git add src/auth/middleware.ts -[Bash] git commit -m "feat(auth): log failed authentication attempts" -[main a1b2c3d] feat(auth): log failed authentication attempts - 1 file changed, 3 insertions(+) - -Done. Failed auth attempts now logged with IP and path. -``` - ---- - -## After - -- `src/auth/middleware.ts` modified with logging -- Tests pass, lint clean -- Commit created on current branch - ---- - -## What You Learned - -1. Always validate before committing (tests, lint) -2. Claude shows you what changed before committing -3. Small, focused changes are easier to validate -4. The commit message follows conventional format -5. At L1, you're doing the orchestration - Claude just executes diff --git a/docs/levels/L1-basics/demo/research-session.md b/docs/levels/L1-basics/demo/research-session.md deleted file mode 100644 index f0195c538..000000000 --- a/docs/levels/L1-basics/demo/research-session.md +++ /dev/null @@ -1,63 +0,0 @@ -# Demo: /research Session - -A realistic transcript of exploring how authentication works in a codebase. - ---- - -## Before - -- Fresh Claude Code session -- Unfamiliar with this codebase -- No `.agents/` directory exists - ---- - -## Session - -``` -You: /research "how authentication works" - -Claude: I'll explore the authentication system in this codebase. - -[Glob] **/*.{ts,py} containing "auth" -Found: src/auth/middleware.ts, src/auth/session.ts, src/auth/jwt.ts, tests/auth.test.ts - -[Grep] "login|authenticate|verify" in src/auth/ -Found 12 matches across 3 files - -[Read] src/auth/middleware.ts (45 lines) -[Read] src/auth/jwt.ts (62 lines) - -**Findings:** - -The auth system uses JWT tokens with Redis session storage: - -1. **Entry point**: `middleware.ts` - validates token on every request -2. **Token handling**: `jwt.ts` - creates/verifies tokens, 15-min expiry -3. **Session storage**: Redis-backed, tokens refreshed on activity - -Key flow: -- Request hits middleware -> extracts Bearer token -- Token verified via `jwt.verify()` -> user ID extracted -- Session checked in Redis -> request proceeds or 401 - -The refresh logic (jwt.ts:38-42) extends sessions on each valid request, -so active users stay logged in indefinitely. -``` - ---- - -## After - -- You now understand the auth flow -- Key files identified for future reference -- No files were created - this was conversation only - ---- - -## What You Learned - -1. Claude uses glob/grep to find relevant files quickly -2. Research output lives in the conversation, not on disk -3. Good research identifies the "entry point" and traces the flow -4. At L1, you carry this knowledge in your head (or copy it somewhere) diff --git a/docs/levels/L1-basics/implement.md b/docs/levels/L1-basics/implement.md deleted file mode 100644 index 50d365285..000000000 --- a/docs/levels/L1-basics/implement.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -description: Make changes, validate, commit ---- - -# /implement - -Execute a code change with validation. Use after you know what to change. - ---- - -## Usage - -``` -/implement -``` - ---- - -## Steps - -1. **State the change** - Tell Claude what to modify and why -2. **Claude makes changes** - Files are edited or created -3. **Validate** - Run tests or linting -4. **Commit** - Changes saved to git - ---- - -## Output - -Modified files, validation results, git commit. - ---- - -## Example - -``` -You: /implement - Add logging to the auth module - -Claude: Adding logging to auth.py... -[reads auth.py, edits auth.py] -$ pytest tests/test_auth.py -3 passed -$ git commit -m "feat(auth): add logging for login attempts" -``` - ---- - -## Next - -`/research ` to explore, or `git show HEAD` to review. \ No newline at end of file diff --git a/docs/levels/L1-basics/research.md b/docs/levels/L1-basics/research.md deleted file mode 100644 index bd6c4f3be..000000000 --- a/docs/levels/L1-basics/research.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -description: Explore a codebase to understand how it works ---- - -# /research - -Investigates code structure, patterns, and behavior. Use before making changes to unfamiliar code. - -## Usage - -``` -/research "your topic" -``` - -## Steps - -1. Claude searches for relevant files using glob/grep -2. Claude reads key files and traces connections -3. Claude summarizes findings in the conversation - -## Output - -Findings appear directly in the conversation. At L1, nothing persists to disk. - -## Example - -``` -You: /research "how does authentication work" - -Claude: I'll explore the authentication system. -[Searches for auth-related files] -[Reads src/auth/middleware.ts, src/auth/session.ts] - -**Findings:** -- JWT-based auth in middleware.ts -- Sessions stored in Redis (session.ts:42) -- Token refresh happens on each request -``` - -## Next - -Run `/implement` to make changes based on your research. diff --git a/docs/levels/L2-persistence/README.md b/docs/levels/L2-persistence/README.md deleted file mode 100644 index c5ee9501a..000000000 --- a/docs/levels/L2-persistence/README.md +++ /dev/null @@ -1,49 +0,0 @@ -# L2 — Persistence - -Add the `.agents/` directory for cross-session memory. - -## What You'll Learn - -- Creating `.agents/` directory structure -- Writing research documents that persist -- Extracting learnings with `/post-mortem` -- Pattern documentation - -## Prerequisites - -- Completed L1-basics -- Comfortable with `/research` and `/implement` - -## Available Commands - -| Command | Purpose | -|---------|---------| -| `/research ` | Output now saved to `.agents/research/` | -| `/implement` | Same as L1 | -| `/post-mortem [topic]` | Extract learnings to `.agents/learnings/` | - -## Key Concepts - -- **`.agents/` directory**: AI memory system -- **Research documents**: Deep exploration persists -- **Learnings**: Session insights survive context clear -- **Patterns**: Reusable solutions accumulate - -## Directory Structure - -``` -.agents/ -├── research/ # Deep exploration documents -├── learnings/ # Session insights -└── patterns/ # Reusable solutions -``` - -## What's NOT at This Level - -- No issue tracking -- No parallelization -- No `/crank` (autonomous execution) - -## Next Level - -Once comfortable with persistence, progress to [L3-state-management](../L3-state-management/README.md) to add issue tracking. diff --git a/docs/levels/L2-persistence/demo/research-session.md b/docs/levels/L2-persistence/demo/research-session.md deleted file mode 100644 index 79e3fe7ed..000000000 --- a/docs/levels/L2-persistence/demo/research-session.md +++ /dev/null @@ -1,74 +0,0 @@ -# Demo: /research with Persistence - -A session showing research that persists to `.agents/`. - ---- - -## Before - -- Fresh Claude Code session -- `.agents/` directory exists (or will be created) -- Previous research documents may exist - ---- - -## Session - -``` -You: /research "how does the payment system work" - -Claude: I'll explore the payment system and save findings. - -[Glob] **/payment*.{ts,py} -Found: src/payments/processor.ts, src/payments/stripe.ts, tests/payments.test.ts - -[Grep] "charge|refund|subscription" in src/payments/ -Found 18 matches - -[Read] src/payments/processor.ts (89 lines) -[Read] src/payments/stripe.ts (124 lines) - -**Analysis:** - -The payment system uses Stripe as the backend: - -1. **Entry point**: `processor.ts` - orchestrates all payment operations -2. **Stripe integration**: `stripe.ts` - API wrapper with retry logic -3. **Supported operations**: charge, refund, subscription management - -Key flows: -- One-time payments: processor.charge() → stripe.createPaymentIntent() -- Subscriptions: processor.subscribe() → stripe.createSubscription() -- Refunds: processor.refund() → stripe.createRefund() (partial supported) - -**Writing to:** .agents/research/2025-01-15-payment-system.md - -[Write] .agents/research/2025-01-15-payment-system.md - -✓ Research saved. You can load this in future sessions. -``` - ---- - -## After - -``` -.agents/ -└── research/ - └── 2025-01-15-payment-system.md # <- New file -``` - -The research document contains: -- Full analysis with file references -- Code snippets of key functions -- Diagrams or flow descriptions -- Questions for follow-up - ---- - -## What You Learned - -1. L2 research creates persistent documents -2. Documents survive session clears and context limits -3. Future sessions can load research: `read .agents/research/...` -4. Research accumulates as institutional memory diff --git a/docs/levels/L2-persistence/demo/retro-session.md b/docs/levels/L2-persistence/demo/retro-session.md deleted file mode 100644 index f8ca327ac..000000000 --- a/docs/levels/L2-persistence/demo/retro-session.md +++ /dev/null @@ -1,100 +0,0 @@ -# Demo: /retro Session - -A session showing learning extraction at the end of work. - ---- - -## Before - -- You've been working for a while -- Solved some problems, discovered patterns -- About to end the session or hit context limits - ---- - -## Session - -``` -You: /retro - -Claude: Reviewing this session to extract learnings... - -**Session Summary:** - -This session focused on debugging authentication timeouts. - -**Problems Solved:** -1. JWT tokens expiring mid-request due to clock skew - - Solution: Added 30-second buffer to expiry check - - File: src/auth/jwt.ts:42 - -2. Redis connection pool exhaustion under load - - Solution: Increased pool size from 10 to 50 - - File: config/redis.ts:8 - -**Patterns Discovered:** -- Always check token expiry with buffer time -- Connection pool size should be 5x expected concurrent users -- Log connection pool metrics for capacity planning - -**Mistakes to Avoid:** -- Don't trust client-provided timestamps -- Don't cache tokens longer than their remaining TTL - -**Writing to:** .agents/learnings/2025-01-15-auth-debugging.md - -[Write] .agents/learnings/2025-01-15-auth-debugging.md - -✓ Learnings saved. -``` - ---- - -## After - -``` -.agents/ -├── research/ -└── learnings/ - └── 2025-01-15-auth-debugging.md # <- New file -``` - ---- - -## The Learnings File - -```markdown -# Auth Debugging Session - 2025-01-15 - -## Problems Solved - -### JWT Token Expiry Race Condition -- **Symptom**: Intermittent 401 errors under load -- **Root Cause**: Clock skew between servers -- **Solution**: Added 30-second buffer (src/auth/jwt.ts:42) - -### Redis Pool Exhaustion -- **Symptom**: Connection timeouts after 10 concurrent requests -- **Root Cause**: Pool size too small -- **Solution**: Increased to 50 (config/redis.ts:8) - -## Patterns - -- Token expiry checks need buffer time -- Pool size = 5x concurrent users -- Monitor pool metrics - -## Don't Repeat - -- Never trust client timestamps -- Don't cache tokens past their TTL -``` - ---- - -## What You Learned - -1. `/post-mortem` captures session knowledge before it's lost -2. Learnings are structured for future reference -3. Problems, patterns, and mistakes are all valuable -4. Run `/post-mortem` before context limits or session end diff --git a/docs/levels/L2-persistence/research.md b/docs/levels/L2-persistence/research.md deleted file mode 100644 index 01c3cb6ec..000000000 --- a/docs/levels/L2-persistence/research.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -description: Explore codebase and save findings to .agents/research/ ---- - -# /research (L2) - -Investigates code and saves findings to `.agents/research/`. Use when you need persistent research that survives session clears. - -## Usage - -``` -/research "your topic" -``` - -## What's Different from L1 - -At L2, research output persists: -- Findings saved to `.agents/research/YYYY-MM-DD-topic.md` -- Can be loaded in future sessions -- Builds institutional memory - -## Steps - -1. Claude searches for relevant files -2. Claude reads and analyzes code -3. Claude writes findings to `.agents/research/` -4. Summary shown in conversation - -## Output - -``` -.agents/research/2025-01-15-authentication.md -``` - -## Example - -``` -You: /research "how does caching work" - -Claude: I'll explore the caching system and save findings. - -[Searches, reads files...] - -**Findings saved to:** .agents/research/2025-01-15-caching.md - -Summary: -- Redis-based cache in src/cache/ -- TTL of 5 minutes for API responses -- Cache invalidation on writes -``` - -## Next - -- `/post-mortem` to extract learnings from your session -- Future sessions: load research with "read .agents/research/..." diff --git a/docs/levels/L2-persistence/retro.md b/docs/levels/L2-persistence/retro.md deleted file mode 100644 index 14d54ce1a..000000000 --- a/docs/levels/L2-persistence/retro.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -description: Extract session learnings to .agents/learnings/ ---- - -# /retro - -Captures what you learned during a session. Saves insights to `.agents/learnings/` so they survive context clears. - -## Usage - -``` -/retro -/retro "specific topic" -``` - -## When to Use - -- End of a productive session -- After solving a tricky problem -- When you discover something worth remembering - -## Steps - -1. Claude reviews the session -2. Identifies key learnings and patterns -3. Writes to `.agents/learnings/YYYY-MM-DD-topic.md` -4. Summary shown in conversation - -## Output - -``` -.agents/learnings/2025-01-15-auth-debugging.md -``` - -## Example - -``` -You: /retro - -Claude: Reviewing this session... - -**Session learnings saved to:** .agents/learnings/2025-01-15-session.md - -Key insights: -- JWT tokens must be refreshed before 401, not after -- The middleware order in Express matters for auth -- Redis connection pooling prevents timeouts -``` - -## What Gets Captured - -- Problems solved and how -- Patterns discovered -- Mistakes to avoid -- Useful commands or techniques - -## Next - -- Start fresh session, load learnings with "read .agents/learnings/..." -- `/research` for your next exploration diff --git a/docs/levels/L3-state-management/README.md b/docs/levels/L3-state-management/README.md deleted file mode 100644 index 65dbbdeb0..000000000 --- a/docs/levels/L3-state-management/README.md +++ /dev/null @@ -1,53 +0,0 @@ -# L3 — State Management - -Add issue tracking with beads for structured work. - -## What You'll Learn - -- Using `/plan` to decompose work into issues -- Beads commands for issue lifecycle -- Tracking dependencies between tasks -- Session close protocol - -## Prerequisites - -- Completed L2-persistence -- Comfortable with `.agents/` directory -- br (beads_rust) CLI installed; invoke as `BEADS_DIR="$(ao beads dir)" br` - -## Available Commands - -| Command | Purpose | -|---------|---------| -| `/plan ` | Decompose goal into beads issues | -| `/research ` | Same as L2 | -| `/implement [id]` | Execute specific issue, then close it | -| `/post-mortem [topic]` | Same as L2 | - -## Beads Commands - -All commands take `BEADS_DIR="$(ao beads dir)"` so linked worktrees use the canonical private ledger: - -```bash -BEADS_DIR="$(ao beads dir)" br ready --json # Show unblocked issues -BEADS_DIR="$(ao beads dir)" br list --status open --json # All open issues -BEADS_DIR="$(ao beads dir)" br show --json # View issue details -BEADS_DIR="$(ao beads dir)" br update --claim --json -BEADS_DIR="$(ao beads dir)" br close --reason "Done" -BEADS_DIR="$(ao beads dir)" br sync --flush-only # Export the git-JSONL ledger (never touches git itself) -``` - -## Key Concepts - -- **Issues**: Atomic units of work -- **Dependencies**: Issues can block each other -- **Session close**: `BEADS_DIR="$(ao beads dir)" br sync --flush-only` after issue updates, then `git -C "$(ao beads dir)" push` to share the private ledger - -## What's NOT at This Level - -- No parallel execution -- No `/crank` (autonomous execution) - -## Next Level - -Once comfortable with issue tracking, progress to [L4-parallelization](../L4-parallelization/README.md) to execute waves. diff --git a/docs/levels/L3-state-management/demo/implement-session.md b/docs/levels/L3-state-management/demo/implement-session.md deleted file mode 100644 index 99b966231..000000000 --- a/docs/levels/L3-state-management/demo/implement-session.md +++ /dev/null @@ -1,102 +0,0 @@ -# Demo: /implement with Issue Tracking - -A session showing issue-driven implementation. - ---- - -## Before - -``` -$ BEADS_DIR="$(ao beads dir)" br ready --json -1. [P1] agentops-a1b: Set up email service (SendGrid) -2. [P2] agentops-g4h: Add email preference settings -``` - -You'll implement the email service issue. - ---- - -## Session - -``` -You: /implement agentops-a1b - -Claude: Working on agentops-a1b: "Set up email service (SendGrid)" - -$ BEADS_DIR="$(ao beads dir)" br update agentops-a1b --claim --json -✓ Updated - -[Read] package.json - checking for existing email deps -[Read] .env.example - checking for env patterns - -**Implementation:** - -$ npm install @sendgrid/mail - -[Write] src/services/email.ts -- EmailService class with send() method -- Configured from SENDGRID_API_KEY env var -- Error handling with retry - -[Write] src/services/email.test.ts -- Tests for send success/failure -- Mock SendGrid API - -$ npm test src/services/email.test.ts -2 passed - -$ npm run lint -No issues - -**Committing:** - -$ git add src/services/email.ts src/services/email.test.ts package.json -$ git commit -m "feat: add SendGrid email service - -- EmailService with send() method -- Retry logic for transient failures -- Tests with mocked API - -Closes: agentops-a1b" - -$ BEADS_DIR="$(ao beads dir)" br close agentops-a1b --reason "Email service implemented with tests" -✓ Closed agentops-a1b - -**Newly unblocked:** -$ BEADS_DIR="$(ao beads dir)" br ready --json -1. [P1] agentops-c2d: Add notification triggers to order processor -2. [P2] agentops-e3f: Create order email templates -3. [P2] agentops-g4h: Add email preference settings - -Three issues now ready! (c2d and e3f were blocked by a1b) -``` - ---- - -## After - -- `src/services/email.ts` created -- Tests passing -- Issue closed, dependencies unblocked -- Ready for next issue - ---- - -## Session Close - -Before ending: -``` -$ BEADS_DIR="$(ao beads dir)" br sync --flush-only -$ git -C "$(ao beads dir)" push # Sync the private br ledger -$ git push -``` - ---- - -## What You Learned - -1. `/implement ` targets a specific issue -2. Issue auto-marked `in_progress` then closed -3. Commit message references the issue -4. Closing an issue unblocks dependents -5. Issue updates auto-sync to JSONL; push before ending session diff --git a/docs/levels/L3-state-management/demo/plan-session.md b/docs/levels/L3-state-management/demo/plan-session.md deleted file mode 100644 index c34bed612..000000000 --- a/docs/levels/L3-state-management/demo/plan-session.md +++ /dev/null @@ -1,101 +0,0 @@ -# Demo: /plan Session - -A session showing goal decomposition into tracked issues. - ---- - -## Before - -- Beads initialized (`BEADS_DIR="$(ao beads dir)" br init` run previously) -- Goal: Add email notifications to the app -- No existing issues for this feature - ---- - -## Session - -``` -You: /plan "add email notifications for order updates" - -Claude: I'll research the notification requirements and create a plan. - -[Research] Looking at existing order system... - -[Read] src/orders/processor.ts -[Read] src/notifications/ (doesn't exist) -[Grep] "email|notify|send" in src/ - -**Analysis:** - -Orders are processed in processor.ts. No notification system exists. -Need to: -1. Set up email service -2. Add notification triggers to order events -3. Create email templates -4. Add user preference handling - -**Creating issues:** - -$ BEADS_DIR="$(ao beads dir)" br create --title "Set up email service (SendGrid)" --type task --priority 1 -Created: ag-a1b - -$ BEADS_DIR="$(ao beads dir)" br create --title "Add notification triggers to order processor" --type task --priority 1 -Created: ag-c2d - -$ BEADS_DIR="$(ao beads dir)" br create --title "Create order email templates" --type task --priority 2 -Created: ag-e3f - -$ BEADS_DIR="$(ao beads dir)" br create --title "Add email preference settings" --type feature --priority 2 -Created: ag-g4h - -$ BEADS_DIR="$(ao beads dir)" br create --title "Write notification tests" --type task --priority 3 -Created: ag-i5j - -**Setting dependencies:** - -$ BEADS_DIR="$(ao beads dir)" br dep add ag-c2d ag-a1b -(triggers depend on email service) - -$ BEADS_DIR="$(ao beads dir)" br dep add ag-e3f ag-a1b -(templates depend on email service) - -$ BEADS_DIR="$(ao beads dir)" br dep add ag-i5j ag-c2d -$ BEADS_DIR="$(ao beads dir)" br dep add ag-i5j ag-e3f -(tests depend on triggers and templates) - -**Plan saved to:** .agents/plans/2025-01-15-email-notifications.md - -**Ready to work:** -$ BEADS_DIR="$(ao beads dir)" br ready -1. [P1] ag-a1b: Set up email service (SendGrid) -2. [P2] ag-g4h: Add email preference settings - -Start with: `/implement ag-a1b` -``` - ---- - -## After - -``` -_beads/ -└── issues.jsonl # Issues stored here (git-JSONL ledger) - -.agents/ -└── plans/ - └── 2025-01-15-email-notifications.md -``` - -Issues created: -- 2 ready (no blockers) -- 3 blocked (have dependencies) - ---- - -## What You Learned - -1. `/plan` researches before creating issues -2. Issues get IDs like `ag-xyz` -3. Dependencies control execution order -4. `br ready` shows what can be worked now -5. Plan document captures the reasoning diff --git a/docs/levels/L3-state-management/implement.md b/docs/levels/L3-state-management/implement.md deleted file mode 100644 index d9abe6b5f..000000000 --- a/docs/levels/L3-state-management/implement.md +++ /dev/null @@ -1,81 +0,0 @@ ---- -description: Execute a specific issue, validate, commit, close ---- - -# /implement (L3) - -Execute a beads issue by ID. Marks in_progress, makes changes, validates, commits, closes. - -## Usage - -``` -/implement -/implement agentops-abc -``` - -## What's Different from L2 - -At L3, implementation is issue-driven: -- Specify which issue to work on -- Issue marked `in_progress` automatically -- Issue closed after successful commit -- Dependencies enforced (blocked issues can't start) - -## Steps - -1. Claude marks issue `in_progress` -2. Claude reads issue details and makes changes -3. Validation runs (tests, lint) -4. Changes committed with issue reference -5. Claude closes issue with `BEADS_DIR="$(ao beads dir)" br close` - -## Output - -``` -Implementing: agentops-abc "Add theme context provider" - -[Read, Edit, Test...] - -$ git commit -m "feat: add theme context provider - -Closes: agentops-abc" - -$ BEADS_DIR="$(ao beads dir)" br close agentops-abc --reason "Theme context implemented" - -✓ agentops-abc closed -Next ready: agentops-def (was blocked by abc) -``` - -## Example - -``` -You: /implement agentops-abc - -Claude: Working on agentops-abc: "Add theme context provider" - -$ BEADS_DIR="$(ao beads dir)" br update agentops-abc --claim --json - -[Reads requirements, creates src/theme/context.tsx...] - -$ npm test -4 passed - -$ git commit -m "feat: add theme context provider" -$ BEADS_DIR="$(ao beads dir)" br close agentops-abc --reason "Theme context implemented" - -✓ Done. Run `BEADS_DIR="$(ao beads dir)" br ready --json` for next issue. -``` - -## Session Close Protocol - -Before ending, always run: -``` -BEADS_DIR="$(ao beads dir)" br sync --flush-only -git -C "$(ao beads dir)" push # sync the private br ledger (git-JSONL) -git push -``` - -## Next - -- `BEADS_DIR="$(ao beads dir)" br ready --json` - See newly unblocked issues -- `/implement ` - Continue through the plan diff --git a/docs/levels/L3-state-management/plan.md b/docs/levels/L3-state-management/plan.md deleted file mode 100644 index 5d0a900fe..000000000 --- a/docs/levels/L3-state-management/plan.md +++ /dev/null @@ -1,69 +0,0 @@ ---- -description: Decompose a goal into tracked issues with dependencies ---- - -# /plan - -Breaks down a goal into beads issues. Use when work spans multiple sessions or has dependencies. - -## Usage - -``` -/plan "your goal" -``` - -## What's Different from L2 - -At L3, planning creates trackable issues: -- Issues stored in the `_beads/` ledger (br, beads_rust) -- Dependencies tracked (what blocks what) -- Progress visible with `BEADS_DIR="$(ao beads dir)" br ready`, `br list` - -## Steps - -1. Claude researches what's needed -2. Claude creates issues with `BEADS_DIR="$(ao beads dir)" br create` -3. Claude sets dependencies with `br dep add` -4. Plan saved to `.agents/plans/` - -## Output - -``` -Created 5 issues for "Add user authentication": - [P1] agentops-abc: Set up auth middleware - [P1] agentops-def: Create login endpoint - [P2] agentops-ghi: Add session storage (depends on abc) - [P2] agentops-jkl: Create logout endpoint (depends on def) - [P3] agentops-mno: Add tests (depends on ghi, jkl) - -Ready to work: agentops-abc, agentops-def -``` - -## Example - -``` -You: /plan "add dark mode support" - -Claude: I'll research and plan dark mode implementation. - -[Research existing theme handling...] - -Creating issues: - -$ BEADS_DIR="$(ao beads dir)" br create "Add theme context provider" -p 1 -Created: agentops-xyz - -$ br create "Create dark mode toggle" -p 2 -Created: agentops-abc - -$ br dep add agentops-abc agentops-xyz - -Plan saved to: .agents/plans/2025-01-15-dark-mode.md - -Next: `br ready` to see what's unblocked, then `/implement` -``` - -## Next - -- `BEADS_DIR="$(ao beads dir)" br ready` - See unblocked issues -- `/implement agentops-xyz` - Work on specific issue diff --git a/docs/levels/L4-parallelization/README.md b/docs/levels/L4-parallelization/README.md deleted file mode 100644 index 7053f6a12..000000000 --- a/docs/levels/L4-parallelization/README.md +++ /dev/null @@ -1,146 +0,0 @@ -# L4 — Parallelization - -Execute independent tasks in parallel with wave-based execution using the swarm pattern. - -## What You'll Learn - -- Identifying independent (unblocked) work via TaskList -- Using `/swarm` for parallel multi-agent execution -- Wave-based dependency resolution -- The Ralph Wiggum pattern for fresh context - -## Prerequisites - -- Completed L3-state-management -- Understanding of task dependencies (blockedBy) -- Comfortable with TaskCreate/TaskUpdate/TaskList - -## Available Commands - -| Command | Purpose | -|---------|---------| -| `/swarm` | Execute unblocked tasks in parallel via background agents | -| `/plan ` | Same as L3 | -| `/research ` | Same as L2 | -| `/implement [id]` | Execute single task | -| `/post-mortem [topic]` | Same as L2 | - -## Key Concepts - -- **Wave**: Set of independent tasks executed together -- **Native teams**: Each wave creates a team (`TeamCreate`), workers join as teammates, communicate via `SendMessage` -- **Fresh context**: Each team = clean slate (Ralph Wiggum pattern). New team per wave. -- **Lead-only commit**: Workers write files, lead validates + commits. Hooks block workers from `git commit`. -- **Dependency resolution**: Only unblocked tasks run in each wave - -## The Ralph Wiggum Pattern - -The swarm follows Ralph Wiggum's core insight: fresh context per iteration. - -``` -Ralph's loop: Swarm equivalent: -while :; do Mayor identifies ready tasks - cat PROMPT.md | claude TeamCreate → spawn workers as teammates -done Workers complete, report via SendMessage - Lead validates + commits - TeamDelete → new team for next wave -``` - -Why this matters: -- **Internal loops accumulate context** → degrades over iterations -- **Fresh spawns stay effective** → each agent is a clean slate -- **Team-per-wave** → new team = new context, no bleed-through - -## Swarm vs Crank vs Ratchet - -These are easy to mix up: - -| You Want | Use | Notes | -|----------|-----|------| -| Fresh context per iteration (“Ralph Wiggum Pattern”) | `/swarm` | Mayor owns the loop; each background agent is one atomic unit of work | -| “Do all issues until the epic is done” | `/crank` | Epic execution loop (usually beads-driven), not the Ralph pattern primitive | -| RPI checkpoints (Research→Plan→Implement→Validate) | `/flywheel` (retired — `ao flywheel status`) | Gate/record progress; pair with `/crank` or `/swarm` for execution | - -## Wave Workflow - -``` -1. TaskList → identifies unblocked tasks -2. /swarm → TeamCreate + spawn workers as teammates -3. Workers complete → send completion via SendMessage -4. Lead reconciliation: - a. Verify work (check files, run tests) - b. Commit all changes (lead-only) - c. shutdown_request workers → TeamDelete - d. TaskList to find newly unblocked tasks -5. New team for next wave (fresh context) -``` - -## Lead Reconciliation Step - -After workers report completion via `SendMessage`, the lead must verify before committing: - -``` -# For each completed worker: -1. Check the files created/modified -2. Run tests (npm test, pytest, etc.) -3. Run lint (npm run lint, etc.) -4. If valid: commit changes (lead-only — workers cannot commit) -5. If invalid: SendMessage retry instructions to idle worker - (worker wakes with full context, no re-spawn needed) - -# After all verified: -shutdown_request each worker → TeamDelete() -TaskList() → shows newly unblocked tasks → new team for next wave -``` - -This prevents marking broken work as complete. The `git-worker-guard` hook enforces lead-only commits. - -## Agent Prompts (Atomic) - -Each spawned agent gets a simple, single-task prompt: - -``` -# Good (atomic): -"Create users endpoint in src/routes/users.ts. Include GET /users, -POST /users, GET /users/:id routes. Follow existing patterns." - -# Bad (complex loop): -"Create users endpoint, then test it, then if tests fail fix them, -then validate, then update status, then check for more work..." -``` - -Agents do ONE thing. Mayor handles orchestration. - -## Example Session - -``` -1. /plan "Build auth system" - → Creates tasks with dependencies: - #1 [pending] Create User model - #2 [pending] Add password hashing (blockedBy: #1) - #3 [pending] Create login endpoint (blockedBy: #1) - #4 [pending] Write tests (blockedBy: #2, #3) - -2. /swarm - → Wave 1: Spawns agent for #1 (only unblocked) - → Agent completes, Mayor marks #1 completed - -3. /swarm - → Wave 2: Spawns agents for #2 and #3 in parallel - → Both complete - -4. /swarm - → Wave 3: Spawns agent for #4 - → All done - -5. /vibe → Validate everything -``` - -## What's NOT at This Level - -- No `/crank` (full autonomous execution without human wave triggers) -- Human triggers each wave - -## Next Level - -Once comfortable with waves, progress to [L5-orchestration](../L5-orchestration/README.md) for full autonomy with `/crank`. diff --git a/docs/levels/L4-parallelization/demo/wave-session.md b/docs/levels/L4-parallelization/demo/wave-session.md deleted file mode 100644 index a5511cea7..000000000 --- a/docs/levels/L4-parallelization/demo/wave-session.md +++ /dev/null @@ -1,172 +0,0 @@ -# Demo: /swarm Session - -A session showing parallel execution of multiple tasks using the swarm pattern. - ---- - -## Before - -Plan created with tasks for "Add REST API endpoints": - -``` -$ TaskList() - -#1 [pending] Create users endpoint -#2 [pending] Create orders endpoint -#3 [pending] Create products endpoint -#4 [pending] Add authentication (blockedBy: #1) -#5 [pending] Add order validation (blockedBy: #2) -#6 [pending] Add integration tests (blockedBy: #4, #5) -``` - -Wave 1: 3 tasks ready (no dependencies) - ---- - -## Session - -``` -You: /swarm - -Claude: Checking TaskList for ready tasks... - -Ready for Wave 1: -#1 [pending] Create users endpoint -#2 [pending] Create orders endpoint -#3 [pending] Create products endpoint - -Spawning 3 background agents... - -Task(subagent_type="general-purpose", run_in_background=true, ...) -Task(subagent_type="general-purpose", run_in_background=true, ...) -Task(subagent_type="general-purpose", run_in_background=true, ...) - -[Agents work in parallel, each with fresh context...] - - Agent for #1 completed - Agent for #2 completed - Agent for #3 completed - -All Wave 1 agents complete! - -[Mayor reconciliation step...] - -Verifying agent work before marking complete: - -$ git diff --stat - src/routes/users.ts | 45 +++ - src/routes/orders.ts | 52 +++ - src/routes/products.ts | 48 +++ - -$ npm test -12 passed - -$ npm run lint -No issues - -All verifications passed. Updating task status: - -TaskUpdate(taskId="1", status="completed") -TaskUpdate(taskId="2", status="completed") -TaskUpdate(taskId="3", status="completed") - -Checking for newly unblocked tasks... - -Wave 1 complete! - -TaskList() shows: -#1 [completed] Create users endpoint -#2 [completed] Create orders endpoint -#3 [completed] Create products endpoint -#4 [pending] Add authentication ← now unblocked! -#5 [pending] Add order validation ← now unblocked! -#6 [pending] Add integration tests (blockedBy: #4, #5) -``` - ---- - -## After Wave 1 - -- 3 background agents ran in parallel -- Each had fresh context (Ralph Wiggum pattern) -- 3 tasks marked completed -- 2 more tasks now unblocked - ---- - -## Continuing with Wave 2 - -``` -You: /swarm - -Claude: Wave 2: 2 tasks ready - -Spawning background agents for #4 and #5... - -[2 agents work in parallel with fresh context...] - - Agent for #4 completed - Agent for #5 completed - -[Mayor reconciliation: verify, test, then update status] - -TaskUpdate(taskId="4", status="completed") -TaskUpdate(taskId="5", status="completed") - -Wave 2 complete! - -TaskList() shows: -#6 [pending] Add integration tests ← now unblocked! - -Wave 3: 1 task (final wave) -``` - ---- - -## Wave 3 (Final) - -``` -You: /swarm - -Claude: Wave 3: 1 task ready - -Spawning background agent for #6... - - Agent for #6 completed - -[Mayor reconciliation: verify integration tests pass] - -TaskUpdate(taskId="6", status="completed") - -All tasks complete! - -$ git status -3 new files, 2 modified - -$ git add -A && git commit -m "feat: complete REST API implementation" -``` - ---- - -## What You Learned - -1. `/swarm` spawns background agents for all ready tasks -2. Each agent has fresh context (Ralph Wiggum pattern) -3. `` arrives automatically when agents complete -4. Mayor reconciliation: verify work, then `TaskUpdate(status="completed")` -5. `TaskList()` reveals newly unblocked tasks for next wave -6. No context accumulation = stays effective at scale - -## Key Pattern: Agent Prompts Are Atomic - -Each agent gets a single, focused task: - -``` -# What the agent receives: -"Create users endpoint in src/routes/users.ts. -Include GET /users, POST /users, GET /users/:id routes. -Follow existing patterns in the codebase." - -# Agent does just that one thing and exits. -# Mayor handles orchestration, verification, and status updates. -``` diff --git a/docs/levels/L4-parallelization/implement-wave.md b/docs/levels/L4-parallelization/implement-wave.md deleted file mode 100644 index 26a091926..000000000 --- a/docs/levels/L4-parallelization/implement-wave.md +++ /dev/null @@ -1,94 +0,0 @@ ---- -description: Execute all unblocked issues in parallel using sub-agents ---- - -# /implement-wave - -Runs all ready issues in parallel. Each issue gets a sub-agent. Results batched into single commit. - -## Usage - -``` -/implement-wave -``` - -## What's Different from L3 - -At L4, parallelization speeds execution: -- Multiple issues run simultaneously -- Sub-agents handle each issue independently -- Single commit captures all wave changes -- Dramatically faster for independent work - -## How It Works - -1. Claude runs `BEADS_DIR="$(ao beads dir)" br ready` to find unblocked issues -2. Spawns sub-agent for each issue (max 3 per wave) -3. Sub-agents work in parallel via Task tool -4. Results merged and validated -5. Single commit closes all wave issues - -**Why max 3?** Each subagent returns results that accumulate in context. Capping at 3 prevents context overflow on complex issues while still providing meaningful parallelism. - -## Output - -``` -Wave 1: 3 issues ready - -Launching sub-agents: - → agentops-abc: Add login form - → agentops-def: Add logout button - → agentops-ghi: Add session display - -[Sub-agents complete...] - -All 3 issues completed successfully. - -$ git commit -m "feat: add auth UI components - -- Login form (agentops-abc) -- Logout button (agentops-def) -- Session display (agentops-ghi) - -Closes: agentops-abc, agentops-def, agentops-ghi" - -$ BEADS_DIR="$(ao beads dir)" br close agentops-abc agentops-def agentops-ghi - -Wave 1 complete. Run `br ready` for Wave 2. -``` - -## Example - -``` -You: /implement-wave - -Claude: Checking ready issues... - -$ BEADS_DIR="$(ao beads dir)" br ready -1. [P1] agentops-xyz: Create user model -2. [P1] agentops-abc: Create order model -3. [P2] agentops-def: Add database migrations - -Launching 3 sub-agents... - -[3 parallel agents work...] - -✓ All complete. Tests passing. - -$ git commit -m "feat: add data models and migrations" -$ BEADS_DIR="$(ao beads dir)" br close agentops-xyz agentops-abc agentops-def - -Done. Next wave has 2 issues ready. -``` - -## When to Use - -- Multiple independent issues ready -- Issues don't share file dependencies -- Want maximum velocity - -## Next - -- `BEADS_DIR="$(ao beads dir)" br ready` - See next wave -- `/crank` - Run next wave -- `/post-mortem` - Extract learnings after completing plan diff --git a/docs/levels/L5-orchestration/README.md b/docs/levels/L5-orchestration/README.md deleted file mode 100644 index 509c49363..000000000 --- a/docs/levels/L5-orchestration/README.md +++ /dev/null @@ -1,63 +0,0 @@ -# L5 — Orchestration - -Full autonomous operation with `/crank`. - -## What You'll Learn - -- Using `/crank` for epic-to-completion -- The ODMCR reconciliation loop -- Swarm vs Crew execution modes -- Integration with the NTM + MCP Agent Mail substrate for parallel workers - -## Prerequisites - -- Completed L4-parallelization -- Comfortable with wave execution -- Understanding of beads issue tracking - -## Available Commands - -| Command | Purpose | -|---------|---------| -| `/crank` | Autonomous epic-to-completion | -| `/crank` | Same as L4 | -| `/plan ` | Same as L3 | -| `/research ` | Same as L2 | -| `/implement [id]` | Same as L3 | -| `/post-mortem [topic]` | Same as L2 | - -## Key Concepts - -- **Crank**: Autonomous epic execution - runs until ALL children are CLOSED -- **ODMCR loop**: Observe → Dispatch → Monitor → Collect → Retry -- **Swarm mode**: Dispatches to parallel worker panes via the NTM tmux swarm, coordinated through MCP Agent Mail -- **Crew mode**: Executes sequentially via `/implement` - -## Crank Flow - -``` -/crank - ↓ -Observe (BEADS_DIR="$(ao beads dir)" br show, br ready) - ↓ -Dispatch (NTM swarm pane or /implement) - ↓ -Monitor (convoy status) - ↓ -Collect (close completed) - ↓ -Retry (handle failures) - ↓ -Loop until epic CLOSED -``` - -## Execution Modes - -| Mode | When | How | -|------|------|-----| -| **Crew** | Default, single-agent | Sequential `/implement` calls | -| **Swarm** | Multi-agent contention/durability | Parallel dispatch via NTM tmux swarm + MCP Agent Mail | - -## Mastery - -At L5, you can hand off entire epics to `/crank` and trust autonomous completion. diff --git a/docs/levels/L5-orchestration/crank.md b/docs/levels/L5-orchestration/crank.md deleted file mode 100644 index 6525a8d95..000000000 --- a/docs/levels/L5-orchestration/crank.md +++ /dev/null @@ -1,98 +0,0 @@ ---- -description: Execute an epic to completion using swarm for parallel wave execution ---- - -# /crank - -Runs an entire epic through waves of parallel execution until ALL children are CLOSED. - -## Usage - -``` -/crank -/crank ao-epic-123 -``` - -## Architecture - -Crank is the autonomous orchestrator that uses swarm for each wave: - -``` -Crank (orchestrator) Swarm (executor) - | | - +-> br ready (wave issues) | - | | - +-> TaskCreate from beads --->+-> Spawn agents (fresh context) - | | - +-> /swarm --->+-> Execute in parallel - | | - +-> Verify + br update <---+-> Results - | | - +-> Loop until epic DONE | -``` - -**Separation of concerns:** -- **Crank** = Beads-aware orchestration, epic lifecycle, knowledge flywheel -- **Swarm** = Fresh-context parallel execution (Ralph Wiggum pattern) - -## How It Works - -The FIRE Loop: - -1. **FIND**: `BEADS_DIR="$(ao beads dir)" br ready` - get unblocked beads issues -2. **IGNITE**: Create TaskList tasks, invoke `/swarm` -3. **REAP**: Swarm collects results, crank syncs to beads -4. **ESCALATE**: Fix blockers, retry failures -5. Loop until all children are CLOSED - -## Output - -``` -/crank ao-epic-123 - -Epic: "Add user dashboard" -Total: 8 issues - -[Wave 1] br ready → [ao-1, ao-2, ao-3] - TaskCreate for each - /swarm → 3 agents spawned - ao-1 DONE, ao-2 DONE, ao-3 BLOCKED - -[Wave 2] br ready → [ao-4, ao-5, ao-3] - TaskCreate for each - /swarm → 3 agents spawned - ... - -[Final Vibe] Running /vibe on recent changes... - All checks passed. - -DONE -Epic: ao-epic-123 -Issues completed: 8 -Waves: 4/50 -``` - -## Limits - -- **MAX_EPIC_WAVES = 50** - Prevents infinite loops -- Swarm handles parallelism per wave (no max agent limit in swarm) - -## Failure Handling - -Crank handles failures automatically: -- Retry failed issues in next wave -- Skip blocked issues (revisit when unblocked) -- Escalate persistent failures after 3 retries - -## When to Use - -| Scenario | Skill | -|----------|-------| -| Execute entire epic autonomously | `/crank` | -| Just parallel execution (no beads) | `/swarm` directly | -| Single issue | `/implement` | - -## Next - -- `/validate` - Runs automatically at end -- `/post-mortem` - Extract learnings after epic completes diff --git a/docs/levels/L5-orchestration/demo/crank-session.md b/docs/levels/L5-orchestration/demo/crank-session.md deleted file mode 100644 index c4cf95f54..000000000 --- a/docs/levels/L5-orchestration/demo/crank-session.md +++ /dev/null @@ -1,166 +0,0 @@ -# Demo: /crank Session - -A session showing autonomous epic execution via the ODMCR loop. - ---- - -## Before - -Epic created with `/plan`: - -``` -$ BEADS_DIR="$(ao beads dir)" br show agentops-epic-xyz -Epic: "Add real-time notifications" -Status: open -Children: 6 issues - -Issues: - [P1] agentops-a1: Set up WebSocket server - [P1] agentops-b2: Create notification service - [P2] agentops-c3: Add browser client (blocked by a1) - [P2] agentops-d4: Create notification types (blocked by b2) - [P2] agentops-e5: Add user preferences (blocked by b2) - [P3] agentops-f6: Integration tests (blocked by c3, d4, e5) -``` - ---- - -## Session - -``` -You: /crank agentops-epic-xyz - -Claude: Loading epic... - -**Epic:** Add real-time notifications -**Mode:** crew (sequential) -**Issues:** 6 total - -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -[OBSERVE] 2 issues ready, 4 blocked - -[DISPATCH] /implement agentops-a1 - → Set up WebSocket server - -[Working...] - -✓ agentops-a1 CLOSED - -[COLLECT] Updated dependencies, 1 issue unblocked - -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -[OBSERVE] 2 issues ready (b2, c3), 3 blocked - -[DISPATCH] /implement agentops-b2 - → Create notification service - -[Working...] - -✓ agentops-b2 CLOSED - -[COLLECT] Updated dependencies, 2 issues unblocked - -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -[OBSERVE] 3 issues ready (c3, d4, e5), 1 blocked - -[DISPATCH] /implement agentops-c3 - → Add browser client - -[Working...] - -✓ agentops-c3 CLOSED - -[DISPATCH] /implement agentops-d4 - → Create notification types - -[Working...] - -✓ agentops-d4 CLOSED - -[DISPATCH] /implement agentops-e5 - → Add user preferences - -[Working...] - -✓ agentops-e5 CLOSED - -[COLLECT] Updated dependencies, 1 issue unblocked - -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -[OBSERVE] 1 issue ready (f6), 0 blocked - -[DISPATCH] /implement agentops-f6 - → Integration tests - -[Working...] - -✓ agentops-f6 CLOSED - -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -[OBSERVE] 0 issues remaining - -**Epic CLOSED!** - -Summary: -- 6/6 issues closed -- Mode: crew (sequential) -- All tests passing - -$ BEADS_DIR="$(ao beads dir)" br sync # Optional ledger sync check -$ git push -``` - ---- - -## Parallel Mode (NTM Swarm) - -When disjoint issues can run concurrently, the loop dispatches them to an -NTM tmux swarm, coordinated through MCP Agent Mail (locks + inboxes): - -``` -You: /crank agentops-epic-xyz - -Claude: Loading epic... - -**Epic:** Add real-time notifications -**Mode:** parallel (NTM swarm + Agent Mail) -**Issues:** 6 total - -[OBSERVE] 2 issues ready (a1, b2) - -[DISPATCH] NTM pane agentops-a1 → /implement -[DISPATCH] NTM pane agentops-b2 → /implement - -[MONITOR] Swarm status (Agent Mail): - → agentops-a1: in_progress (pane-1) - → agentops-b2: in_progress (pane-2) - -[Panes working in parallel...] - -[COLLECT] Both complete - ✓ agentops-a1 CLOSED - ✓ agentops-b2 CLOSED - -[OBSERVE] 3 issues ready (c3, d4, e5) - -[DISPATCH] NTM pane agentops-c3 → /implement -[DISPATCH] NTM pane agentops-d4 → /implement -[DISPATCH] NTM pane agentops-e5 → /implement - -...continues until epic CLOSED -``` - ---- - -## What You Learned - -1. `/crank` runs the ODMCR loop until epic is CLOSED -2. Auto-detects sequential vs parallel mode -3. NO human prompts - fully autonomous -4. Handles dependencies automatically via beads -5. Dispatches parallel work to an NTM swarm coordinated by MCP Agent Mail diff --git a/docs/levels/index.md b/docs/levels/index.md deleted file mode 100644 index ec0765fd1..000000000 --- a/docs/levels/index.md +++ /dev/null @@ -1,50 +0,0 @@ -# Learning Levels - -A progressive curriculum for mastering AgentOps, from single-session work -through full autonomous orchestration. - -
- -- :material-numeric-1-circle: **[L1 — Basics](L1-basics/README.md)** - - --- - - Single-session work with Claude Code. Research a codebase. Make changes. - Commit. - -- :material-numeric-2-circle: **[L2 — Persistence](L2-persistence/README.md)** - - --- - - Cross-session bookkeeping with `.agents/`. Research that compounds. - Retros that feed the next session. - -- :material-numeric-3-circle: **[L3 — State Management](L3-state-management/README.md)** - - --- - - Issue tracking with beads. Decompose goals. Execute, validate, close. - -- :material-numeric-4-circle: **[L4 — Parallelization](L4-parallelization/README.md)** - - --- - - Wave-based parallel execution. Unblock and dispatch in parallel. - -- :material-numeric-5-circle: **[L5 — Orchestration](L5-orchestration/README.md)** - - --- - - Full autonomous operation with `/crank`. Execute epics to completion. - -
- -## How to use this - -Start at **L1** if you're new to AgentOps. Each level builds on the previous -one — the bookkeeping from L2 unlocks the issue tracking in L3; the issue -tracking enables the parallel waves in L4; waves power the autonomous -orchestration at L5. - -Every level ships with at least one demo session showing a real example of -the level's workflow in action. diff --git a/docs/origin-story.md b/docs/origin-story.md index fb272d336..864409f5f 100644 --- a/docs/origin-story.md +++ b/docs/origin-story.md @@ -70,7 +70,7 @@ Every principle was learned through failure and refined through iteration across The instinct is to front-load everything. This fails. Agents drown in irrelevant context and hallucinate connections. What works: deliver the right context at the right time. AgentOps evolved from monolithic prompt files to skill-scoped references and session intelligence packets. **2. Raw Chat History Is Not Knowledge.** -Organizations that archive agent conversations without extraction get zero compounding. Every session starts from scratch. What works: force transformation. The flywheel pipeline — forge, retro, post-mortem — transforms raw events into learnings, learnings into rules, and rules into context for the next session. Later sessions resolve problems in 2 operations that earlier sessions spent hours debugging. +Organizations that archive agent conversations without extraction get zero compounding. Every session starts from scratch. What works: force transformation. The flywheel pipeline — forge, retro, postmortem — transforms raw events into learnings, learnings into rules, and rules into context for the next session. Later sessions resolve problems in 2 operations that earlier sessions spent hours debugging. **3. Never Trust Self-Reported Success.** Agents claim success without running tests. They report "all passing" after partial runs. What works: external validation at every stage. The 33 CI checks in AgentOps exist because every one was added after a failure that self-reported success would have hidden. The 3-5x validation overhead prevents 10x bug rework. @@ -126,8 +126,8 @@ Unstructured knowledge (learnings, patterns) ↓ curate, temper, promote Structured findings (actionable, severity-ranked) ↓ finding-compiler -Compiled output: planning rules, pre-mortem checks, constraints - ↓ plan, pre-mortem, crank +Compiled output: planning rules, premortem checks, constraints + ↓ plan, premortem, crank Enforcement gates that reject bad plans before implementation ``` diff --git a/docs/philosophy.md b/docs/philosophy.md index 080fd48d0..144eb3fba 100644 --- a/docs/philosophy.md +++ b/docs/philosophy.md @@ -1,100 +1,35 @@ --- -last_reviewed: 2026-04-12 +last_reviewed: 2026-07-14 --- # AgentOps Philosophy -## The Problem +AI agents are stochastic authors. Tests can establish deterministic facts, but +they do not judge every semantic claim, and an author should not certify its +own work. AgentOps therefore provides a small evidence protocol: -Coding agents are good at thinking. They are bad at bookkeeping. - -Every session starts cold. The agent that spent two hours debugging a timeout bug last Tuesday has no memory of it. The pattern you hard-won in session 3 is gone by session 15. The planning rule that would have prevented a regression sits buried in a transcript no one reads. - -This is not a model problem. It is an environment problem. The model is capable. The environment around it does not compound. - -## What AgentOps Is - -AgentOps is a context compiler. - -The compiler analogy is exact: raw session signal (decisions, failures, patterns, warnings) is processed through extraction, scoring, curation, and promotion into reusable artifacts — learnings, findings, planning rules, enforcement gates. The next session runs against a richer environment than the last. The model stays the same. The environment gets smarter. - -This maps directly to what Andrej Karpathy observed about knowledge work: the tedious part is not the thinking, it is the bookkeeping. Organizing, surfacing, routing, and keeping knowledge fresh. AgentOps automates that layer. - -## The Flywheel - -``` -Sessions → Bookkeeping → Learnings → Findings → Planning Rules → Gates - ↑ │ - └────────────────────── Better next session ─────────────────────┘ +```text +intent -> one bounded experiment -> exact subject identity + -> fresh independent judgment -> durable verdict ``` -Each phase is deliberate: +The protocol is behavior-first. Plan expresses one behavior as normal and edge +Given/When/Then scenarios, non-goals, write scope, and required evidence. +Implement performs one bounded RED-to-GREEN-to-refactor experiment. Validate +binds criterion-level judgment to a deterministic content manifest and stores +the result as `verdict.v2`. RPI invokes those responsibilities once and stops. -- **Sessions** produce signal: commits, decisions, failures, retros. -- **Bookkeeping** (`/post-mortem`, which folded the retired `/curate` mining modes, `ao harvest`) extracts and scores that signal. Scores on specificity, actionability, novelty, and confidence filter noise. -- **Learnings** are the raw output — scored, attributed, timestamped. -- **Findings** are promoted learnings: higher confidence, cross-session validation, broader applicability. -- **Planning rules** are enforcement-level knowledge: if a finding is violated, the pre-mortem blocks the plan. -- **Gates** are automated checks in `/pre-mortem`, `/validate`, and `/council` that prevent known failure modes before they ship. +This is a trust floor, not a workflow engine. AgentOps does not own retries, +budgets, queues, claims, leases, Git, CI, closure, release, or delivery. A FAIL +is evidence for the caller, not permission for AgentOps to repair or continue. -The loop closes. The system does not just capture knowledge — it enforces it. +Learn remains an optional, off-path hypothesis: collections of verdicts may +reveal repeated defect classes worth turning into guidance or deterministic +checks. Promotion is deliberate and evidence-backed; it never changes the +validity of the candidate that produced the observation. -## The Data Format - -`.agents/` is the universal data format. - -Plain markdown files, versioned in git, readable by any LLM, browsable in Obsidian, diffable in any editor. No embeddings, no vector database, no proprietary store. - -This is a deliberate bet against the current tooling consensus. Vector databases optimize for semantic recall at scale. `.agents/` optimizes for editorial control, freshness management, and human legibility. For a codebase knowledge base where: - -- Volume is bounded (one project, not the internet) -- Freshness matters more than recall breadth (stale knowledge is worse than no knowledge) -- Human curation is the highest-leverage action -- Portability is required (repo-local state, no AgentOps-hosted control plane, mirrorable dependencies) - -...markdown + wikilinks outperforms embeddings. The agent can grep it, the human can read it, and `ao defrag` can maintain it. - -## The Tiered Model - -Not every knowledge operation needs a frontier model. AgentOps uses three tiers: - -| Tier | When | Why | -|------|------|-----| -| Local 8B (ollama, etc.) — **optional, user-supplied** | Volume work — dedup, defrag, freshness scoring, overnight compounding | Fast, private, cheap. Runs while you sleep. AgentOps ships no model host; bring your own, or run these on your frontier model. | -| Frontier (Claude, GPT-4o, etc.) | Quality work — council validation, pre-mortem review, pattern extraction | Accuracy matters more than throughput. | -| Human | Curation and promotion decisions | Judgment calls that agents get wrong systematically. | - -The dream loop (retired as a standalone `/dream` command; folded into the out-of-session substrate) uses the local tier when one is configured (otherwise your frontier model) for continuous compounding; when that compounding runs out of session — always-on, scheduled, unattended — it runs on an out-of-session substrate (reference: NTM + MCP + managed-agents), not an AgentOps daemon. `/council` and `/pre-mortem` use the frontier tier for high-stakes validation. The human reviews promotions from learning → finding → rule. - -The ratio is intentional. On this repo, validation and curation have run several times the implementation time. This is not overhead — it is the ratchet. Without it, the flywheel runs backward. - -## The Ratchet - -AgentOps adopts the Brownian Ratchet as a first principle: embrace agent variance, filter aggressively, and make progress one-way. - -Agents produce noisy output. Some sessions are brilliant; some are catastrophic. The naive response is to constrain the agent. The AgentOps response is to ratchet: let variance happen, filter at gates (`/pre-mortem` blocks bad plans, `/validate` blocks bad code, `/council` blocks bad decisions), and only let good output advance. The gate is asymmetric — easy to pass in the forward direction, impossible to pass backward. - -This is why validation gates are blocking, not advisory. An advisory gate with no enforcement is not a ratchet. It is a suggestion. - -## What This Is Not - -AgentOps is not a chatbot wrapper. It does not make prompts bigger. It does not add more agents to the same problem. - -It is not trying to replace thinking. The model thinks. AgentOps manages what the model knows when it thinks. - -It is not a SaaS product or a managed service. All state lives locally. All operations are reversible. The product is the compounding environment — the `skills/`, the `ao` CLI, and the discipline enforced by the local pre-push gate. AgentOps 3.0 is hookless: skills and the CLI guide the workflow and the local pre-push Go gate (`ao gate check`) is the release authority (CI is a backstop). Hooks are opt-in — you author your own via the `hooks-authoring` skill — never installed by default. That environment is yours to own, version-control, and take with you. - -## The Thesis, and What Is Actually Proven - -The proven part is the verification: every change reaches *done* only with an independent proof artifact. Whether the knowledge corpus *compounds* over time is a separate, **explicitly-named unproven hypothesis** — demoted to that status in [ADR-0004](adr/ADR-0004-corpus-moat-unproven-position-on-the-system.md) and [ADR-0011](adr/ADR-0011-escape-corpus-compounding-unproven-structural-starvation.md), which found the compounding claim faces a structural data-starvation headwind. Do not read the numbers below as a validated flywheel. - -What this repo has accumulated (descriptive, not a compounding proof): - -- 163 learnings extracted, scored, and curated -- 13 planning rules enforced at pre-mortem gates -- 12 patterns promoted from repeated findings -- 10/12 `ao doctor` checks passing, with the full CI gate suite green - -Session 1 started cold. Session 100+ starts with a knowledge corpus that *can* catch known failure modes before implementation begins — that the corpus keeps paying off as it grows is the hypothesis, not a settled result. - -That is the aim. Not a bigger prompt. A repo that remembers. +The product remains portable. Its core contracts operate in a non-Git +directory without the `ao` binary. The CLI supplies repository utilities and +ordinary deterministic checks. Optional councils, genies, runtimes, trackers, +and software-factory adapters can consume the protocol without becoming its +authority. diff --git a/docs/positioning/README.md b/docs/positioning/README.md deleted file mode 100644 index 28b7b0ef5..000000000 --- a/docs/positioning/README.md +++ /dev/null @@ -1,23 +0,0 @@ -# Positioning - -AgentOps is the operational layer for coding agents. The current public -framing lives in [PRODUCT.md](https://github.com/boshu2/agentops/blob/main/PRODUCT.md) and -[README.md](https://github.com/boshu2/agentops/blob/main/README.md): bookkeeping, validation, primitives, and flows -so every session starts where the last one left off. - -Use DevOps and 12-factor language here as supporting doctrine, lineage, or -bridge language for specific audiences. They are no longer the category -headline. - -The 12-factor doctrine remains a supporting lifecycle/control-plane lens. It -explains how the internal proof contract and software-factory control plane -work operationally, but it does not define the product. - -Use this section as the entry point for positioning work: - -- [`DevOps for Vibe-Coding`](./devops-for-vibe-coding.md) - bridge narrative for readers arriving from DevOps, vibe-coding, or 12-factor language -- [`12 Factors Validation Lens`](./12-factors-validation-lens.md) - supporting lifecycle/control-plane lens for the internal proof contract and software-factory control plane -- [`Convergence Thesis Discussion Draft`](./convergence-thesis-discussion.md) - draft for the pinned GitHub Discussion -- [`HN / dev.to Article Draft`](./hn-devto-article.md) - long-form GTM draft for external posting - -Back: [Docs Index](../documentation-index.md) diff --git a/docs/positioning/devops-for-vibe-coding.md b/docs/positioning/devops-for-vibe-coding.md deleted file mode 100644 index 2d2cc9fff..000000000 --- a/docs/positioning/devops-for-vibe-coding.md +++ /dev/null @@ -1,351 +0,0 @@ -# DevOps for Vibe-Coding - -**Version:** 1.3.0 -**Date:** 2026-05-07 -**Status:** Supporting bridge narrative, not the primary category page - -> **See also:** [A wiki for your agents](../wiki-for-agents.md) — the primary -> category framing this doc supports. The DevOps argument below is the -> historical lineage; the wiki framing is the current headline. - ---- - -## Tagline - -**Primary:** A wiki for your agents — built so you own the moat. - -**Secondary (DevOps lineage):** Bookkeeping, validation, primitives, and flows that compound between sessions, drawn from 50 years of operational discipline. - -**Category:** Context library / wiki for agents (DevOps lineage, not the category label) - -**Legacy (SEO/blog only):** DevOps for Vibe-Coding - ---- - -## Elevator Pitch (30 seconds) - -> AgentOps is a wiki for your agents — markdown in `.agents/` next to your -> code, version-controlled, that agents read, traverse, and contribute to. -> DevOps is the lineage, not the category: AgentOps automates the discipline -> of building and maintaining that wiki, so work starts with repo context -> instead of a blank prompt, gets challenged before shipping, and compounds -> between sessions. - -### One-Liner (10 seconds) - -> Bookkeeping and validation for coding agents that compounds between sessions. - -### Tweet-Length (280 chars) - -> Coding agents do not do their own bookkeeping. AgentOps is the operational -> layer for coding agents: bookkeeping, validation, primitives, and flows that -> help every session start where the last one left off. - ---- - -## Internal Proof Contract - -Most coding-agent tooling is strong at prompt construction and agent routing. -The failure mode comes after that. Internally, AgentOps proves the product -through a three-gap lifecycle contract (see -[docs/context-lifecycle.md](../context-lifecycle.md) for the full treatment): - -1. **Validation** (internal: judgment validation) — the agent ships without the risk context that would challenge its choices. `/pre-mortem` before implementation, `/vibe` before commit, `/council` for multi-judge review. -2. **Bookkeeping** (internal: durable learning) — solved problems recur because nothing extracts, scores, and retrieves the lesson. The `.agents/` ledger, `ao lookup`, finding registry, and `/retro` keep learnings alive across sessions. -3. **Closure** (internal: loop closure) — completed work does not produce better next work. `/post-mortem` harvests learnings and next-work items, the finding compiler promotes repeat failures into preventive constraints, and `GOALS.md` + `/evolve` turn findings into measurable improvements. - -These three gaps are not separate features. They form a single feedback loop: - -``` -/pre-mortem → Implement → /vibe → Commit → /post-mortem → extract → curate → retrieve → next session - (gap 1) (gap 1) (gap 3) (gap 2) (gap 2) (gap 2) (gap 3) -``` - -## Key Differentiators - -### 1. Validation Built In, Not Bolted On - -**Traditional workflow:** -``` -Write code → Ship → CI catches problems → Fix → Repeat -``` - -**Shift-left workflow:** -``` -/pre-mortem → Implement → /vibe → Commit → Knowledge compounds -``` - -The validation loop happens before code ships, not after. But validation alone is not the whole story. The larger system also extracts what was learned, compiles repeat failures into preventive artifacts, and feeds better context into the next cycle. See gaps 2 and 3 above. - -### 2. Repo-Native Bookkeeping - -AgentOps does not stop at chat history or "memory" as a vague promise. It -writes research, findings, learnings, handoffs, ratchet traces, and next-work -artifacts into the repo-native environment: - -- `.agents/` as the working ledger -- retrieval and injection at startup and task boundaries -- curation controls for freshness, contradiction, and promotion -- flywheel closure so each session leaves better context behind - -That is the mechanism behind the compounding claim. - -### 3. Coding Agent Specific - -We focus on **coding agents**—AI assistants that write, modify, and review code: - -- Claude Code running in terminal/IDE -- AI pair programming sessions -- Code generation with validation workflows -- Agents using Read, Edit, Write, Bash for development - -We are NOT: -- A framework for customer service chatbots -- A platform for RAG-based Q&A systems -- An SDK for multi-modal agents -- A solution for general autonomous production agents - -### 4. DevOps Principles, New Context - -We apply proven operational discipline to a new domain: - -| DevOps Principle | Coding Agent Application | -|------------------|--------------------------| -| Infrastructure as Code | Prompts and workflows as versioned artifacts | -| Shift-Left Testing | /pre-mortem before implementation | -| Continuous Integration | /vibe checks before every commit | -| Post-Mortems | /retro to extract and compound learnings | -| Observability | Knowledge flywheel tracks what works | - -DevOps is therefore lineage and supporting doctrine, not the primary category label. - -### 5. Primitives and Flows - -Skills are the building blocks. Flows are the named compositions of those skills: - -- Pull one primitive: `/council validate this PR` -- Compose several manually: `/research` -> `/plan` -> `/council validate` -- Run the full lane: `/rpi "add retry backoff"` -- Automate toward goals: `/evolve` - -This matters because AgentOps is not one monolithic workflow. It gives -operators reusable surfaces they can enter and exit based on intent. - -### 6. Knowledge That Compounds - -The Knowledge Flywheel: -``` -Session -> extract -> curate -> retrieve -> apply -> reinforce - ^__________________________________________________________| -``` - -Every session makes the next one better because the environment changes, not because the agent remembers. This is the moat. - ---- - -## What We Are - -- **Operational layer for coding agents** — a repo-native layer around the models you already use -- **DevOps principles applied to coding agents** — the lineage and operational discipline behind the design -- **Validation-first workflow** — Shift-left, not shift-blame -- **Repo-native bookkeeping that compounds** — Learnings persist, get curated, and harden into future prevention -- **Primitives and flows** — One-off actions and repeatable lanes on the same surface -- **Framework, not SDK** — Patterns and practices, not lock-in - -## What We Are NOT - -- **General production agent framework** — For that, see [12-Factor Agents](https://github.com/humanlayer/12-factor-agents) by Dex Horthy -- **Just another automation tool** — We're about validation, not execution speed -- **Competing with Agent SDKs** — We're complementary (use LangChain, CrewAI, etc. for the runtime) -- **Model-specific** — Works with Claude, could work with others - ---- - -## Target Audience - -### Primary: Developers Using Coding Agents - -- Engineers using Claude Code, GitHub Copilot, or similar -- Teams adopting AI-assisted development -- Developers who want "vibe coding" without the "hope and pray" - -### Secondary: Engineering Managers - -- Teams scaling AI-assisted development -- Leaders concerned about code quality with AI -- Organizations building coding agent workflows - -### Tertiary: Platform/DevOps Engineers - -- Building internal developer platforms with AI -- Integrating coding agents into CI/CD -- Establishing validation patterns for AI-generated code - ---- - -## Competitive Positioning - -| Solution | Focus | Relationship to Us | -|----------|-------|-------------------| -| **12-Factor Agents** (Dex Horthy) | General autonomous agents | Complementary—we're coding-specific | -| **Agent SDKs** (LangChain, CrewAI) | Runtime infrastructure | We sit above—validation patterns, not execution | -| **CI/CD tools** | Post-merge validation | We shift-left—validation before commit | -| **Linters/Formatters** | Syntax validation | We're semantic—does the code do what you intended? | - -### Our Unique Position - -``` - General ←—→ Coding-Specific - │ - │ - ┌────────────┐ │ - │ AgentOps │ ← WE ARE HERE - │ (Shift-L) │ │ - └────────────┘ │ - │ - │ Agent SDKs - │ - │ - Execution ←—→ Validation -``` - ---- - -## Core Message Framework - -### When Asked "What is 12-Factor AgentOps?" - -> Publicly, AgentOps is a wiki for your agents — repo-native markdown in -> `.agents/` that agents read and contribute to, with the discipline of -> maintenance automated. It gives coding agents bookkeeping, validation, -> primitives, and flows so every session starts where the last one left -> off. Under the hood, DevOps and 12-factor ideas explain why the -> lifecycle/control plane looks the way it does — see also the -> [wiki-for-agents](../wiki-for-agents.md) primary framing. - -### When Asked "How is it different from X?" - -| X | Response | -|---|----------| -| Regular coding | Same principles, but with AI-specific patterns for context management and validation | -| Other agent frameworks | We're coding-specific and validation-focused, not general autonomous agents | -| CI/CD | We shift validation left—into the workflow, before you push | -| Copilot/Claude Code | We're complementary—the operational layer around your coding agent | - -### When Asked "Why should I care?" - -> Because AI-generated code still needs validation, and validation alone still leaves you paying for the same lesson twice. AgentOps shifts validation left, captures what failed, and turns useful findings into better future context and gates. - ---- - -## Messaging Hierarchy (Council-Validated 2026-03-30) - -Different surfaces need different messaging density. Match the message to the audience: - -| Surface | Message | Source | -|---------|---------|--------| -| **GitHub description** (< 100 chars) | The operational layer for coding agents. | Hybrid A+C | -| **README hero** | Coding agents don't do their own bookkeeping. | A (emotional) | -| **README subtitle** | Bookkeeping, validation, primitives, and flows that compound between sessions. | C (mechanical) | -| **Social / Twitter** | Mistakes happen once. `.agents/` makes sure of it. | A+C (outcome) | -| **HN / technical blog** | Coding agents don't do their own bookkeeping. AgentOps makes that repo-native. | C (provocative) | -| **Conference talk** | Your agents are temps. Your repo remembers everything. | A (emotional) | -| **Landing page (managers)** | Bookkeeping and validation for coding agents. Zero infrastructure. | A+C (balanced) | -| **Competitive comparison** | Repo-native bookkeeping and validation. Diffable. Greppable. No vendor lock-in. | C (mechanical) | - -**Category claim:** "Operational layer for coding agents" — the repo-native layer that gives agents bookkeeping, validation, primitives, and flows. - -**Strategic position:** Validation drives adoption. Bookkeeping and the flywheel drive retention. Closure hardens the moat. The flywheel is real, but it is not the headline. - -## Key Phrases Reference - -### Use These - -- "The operational layer for coding agents" -- "Bookkeeping, validation, primitives, and flows" -- "The Three Gaps" (as internal proof, not the public headline) -- "Shift-left validation for coding agents" -- "Validation built in, not bolted on" -- "Catch it before you ship it" -- "Knowledge that compounds" -- "Repo-native bookkeeping" -- "Your agents are temps. Your repo remembers everything." -- "Mistakes happen once. `.agents/` makes sure of it." -- "Grep replaces RAG" -- "The 40% rule" (context budget) -- "Pre-mortem before implement" -- "Vibe check before commit" -- "Findings compile into prevention" - -### Avoid These - -- "Operational principles for reliable AI agents" (old, too general) -- "Production-grade agents" (implies general agents) -- "AI-assisted development" (too generic, no validation emphasis) -- "Autonomous agents" (not our focus) -- "Just use Claude better" (undersells the framework) -- "Compiler for agent experience" (metaphor breaks under scrutiny — council-killed) -- "Institutional memory" without "zero infrastructure" (triggers enterprise anxiety alone) - ---- - -## The Three Core Skills - -The shift-left workflow expressed as skills: - -### 1. /pre-mortem — Simulate Failures Before Implementing - -> "What could go wrong with this plan?" - -Run BEFORE implementing. Identifies risks, missing requirements, edge cases. The validation starts before code exists. - -### 2. /vibe — Validate Before You Commit - -> "Does this code do what you intended?" - -The semantic vibe check. Not just syntax—does the implementation match the intent? Run BEFORE every commit. - -### 3. /retro — Extract Learnings to Compound Knowledge - -> "What did we learn that makes the next session better?" - -Closes the loop. Extracts learnings, feeds the flywheel. Every session makes the next one better. - -**Supporting skills:** /research (understand before acting), /plan (think before implementing), /crank (execute with validation gates) - ---- - -## Success Criteria - -This positioning is successful when: - -1. **Users can state what we do in one sentence** — "The operational layer for coding agents" -2. **No confusion with general agent frameworks** — Clear we're coding-specific -3. **Public and internal layers do not blur** — Public story first, three-gap proof second -4. **Knowledge flywheel is understood** — Sessions compound, not isolated - ---- - -## Appendix: Related Work - -### The Vibe Coding Book - -Steve Yegge and Gene Kim's "Vibe Coding" popularized the term. We embrace it and add the operational rigor that makes it sustainable. - -### 12-Factor App - -Heroku's original 12-factor methodology for SaaS apps. We adapt the philosophy as supporting doctrine for the lifecycle/control-plane contract, not as the product definition. - -### 12-Factor Agents - -Dex Horthy's framework for general autonomous agents. Complementary work—we cite them for users who need general agent patterns, while AgentOps stays focused on the coding-agent lifecycle and three-gap contract. - -### DevOps Handbook - -The operational discipline that made infrastructure reliable. We're applying the same shift-left philosophy to a new domain. - ---- - -*This document is a supporting bridge narrative. Reference it when writing for -DevOps- or vibe-coding-native audiences, not as the primary category source of -truth.* diff --git a/docs/positioning/hn-devto-article.md b/docs/positioning/hn-devto-article.md deleted file mode 100644 index 9c4969cf1..000000000 --- a/docs/positioning/hn-devto-article.md +++ /dev/null @@ -1,268 +0,0 @@ -# HN / dev.to Article Draft - -## Status - -Publish-ready repo draft for `na-gtm.4`. - -External posting still requires an authenticated dev.to/Hacker News account. Use -the copy below as the source of truth when posting. - -## Working Title - -Coding agents do not do their own bookkeeping - -## Alternate Titles - -- AgentOps: the operational layer for coding agents -- The missing operational layer around coding agents -- Better prompts will not fix agent amnesia -- Coding agents need bookkeeping, validation, and repeatable flows - -## dev.to Front Matter - -```yaml ---- -title: "Coding agents do not do their own bookkeeping" -published: false -description: "AgentOps is an open-source operational layer for coding agents: bookkeeping, validation, primitives, and flows so work compounds instead of resetting every session." -tags: ai, opensource, devtools, agents -canonical_url: ---- -``` - -## Draft - -Most discussion about coding agents still focuses on the wrong layer. - -People ask: - -- which model is best -- how many agents to run -- whether the workflow should be one-shot or multi-agent -- which prompt pattern gets the best output - -Those questions matter, but they are not the core problem. - -The core problem is that coding agents do not do their own bookkeeping. - -They do not naturally preserve what mattered from prior work. They do not -reliably challenge their own plans before implementation. They do not turn -completed work into better next work unless someone builds a system around them -that does exactly that. - -That is the idea behind AgentOps. - -The simplest way to describe it now is: - -**AgentOps is the operational layer for coding agents.** - -It gives coding agents four things they do not have by default: - -1. **Bookkeeping** - Sessions should not leave behind only chat history. They should leave behind - reusable learnings, findings, and context that can be surfaced back later. -2. **Validation** - Plans and code should be challenged before shipping, not only after breakage - shows up in CI or production. -3. **Primitives** - Operators need reusable building blocks, not one giant all-or-nothing - workflow. -4. **Flows** - Those primitives need to connect into repeatable paths for discovery, - implementation, validation, and knowledge extraction. - -The reason this matters is compounding. - -Without an operational layer, every session starts too close to zero. You -re-brief the agent. You re-explain the repo. You rediscover old mistakes. You -lose the result of yesterday's debugging the moment the context window moves on. - -With an operational layer, the environment changes. - -Research is written down. Findings are promoted. Learnings get retrieved. -Validation gates get sharper. The next session starts inside a stronger -operating context than the previous one had. - -That is the product thesis. - -Technically, I think the best internal description is that AgentOps acts like a -**context compiler**. - -A normal compiler takes raw source code and transforms it into an executable -artifact with stronger guarantees than the original text had on its own. - -AgentOps does something similar for agent work: - -- raw transcripts, retros, and failures become learnings -- learnings become curated patterns and findings -- findings become rules, checks, and better planning context -- the next task starts with compiled context instead of vague memory - -That framing is useful internally because it explains why the product is not -just "memory" and not just "workflow tooling." - -Publicly, though, I think the more useful sentence is still: - -**coding agents do not do their own bookkeeping** - -That line has become more convincing to me because I keep seeing the same -convergence from different directions. - -Karpathy has talked about the bookkeeping burden around an LLM wiki. - -People at Block have described the moat as the feedback loop around the markdown -file and the signal flowing through it. - -And building AgentOps in production kept teaching the same lesson: - -the model is not the product. - -The system around the model is the product. - -That system needs: - -- bookkeeping so knowledge does not disappear -- validation so errors get caught before shipping -- reusable primitives so operators can control the work -- connected flows so the whole thing compounds - -Here is the concrete version. - -AgentOps has a full-lifecycle flow: - -```text -> /rpi "add retry backoff to rate limiter" - -[research] Find relevant prior learnings -[plan] Decompose the work into tracked issues -[pre-mortem] Challenge the plan before implementation -[crank] Execute the implementation -[vibe] Validate the resulting code -[post-mortem] Extract learnings and next work -``` - -It also has an autonomous improvement loop: - -```text -> /evolve - -[evolve] Measure repo goals -[cycle-1] Pick the worst failing goal - Run /rpi against that slice - Validate that the score improved - Revert or narrow if it regressed -[cycle-2...] Repeat until the stop condition fires -``` - -That is the day loop: it can touch source code, but it is supposed to stay -bounded by goals, issue tracking, validation commands, and regression gates. - -There is also a night loop: - -```text -> /dream start - -[overnight] Harvest session artifacts -[reduce] Deduplicate and defrag knowledge -[close-loop] Promote findings into next work -[measure] Capture corpus-quality deltas -[halt] Stop on plateau or rollback on regression -``` - -That one does not mutate source. It compounds the knowledge corpus so the next -day loop starts against a better environment. - -In v2.36.0, the CLI exposes this split directly: - -```bash -# ao evolve was removed in 3.0 — run the /evolve skill in-session; ao goals measure gives the fitness read -ao goals measure -# ao rpi loop was removed in 3.0 — drive the seven-move operating loop in-session via the /rpi skill (docs/architecture/operating-loop.md) -ao search "prior rate limit fixes" -ao lookup --query "repo release lessons" -``` - -The new `PROGRAM.md` contract defines the bounds for autonomous work in the repo: -mutable scope, immutable scope, validation commands, decision policy, escalation -rules, and stop conditions. That gives the autonomous loop something more -concrete than a prompt to obey. - -Install paths are deliberately boring: - -```bash -# Codex CLI -curl -fsSL https://raw.githubusercontent.com/boshu2/agentops/main/scripts/install-codex.sh | bash - -# OpenCode -curl -fsSL https://raw.githubusercontent.com/boshu2/agentops/main/scripts/install-opencode.sh | bash - -# Selected skills for other agents -bash <(curl -fsSL https://raw.githubusercontent.com/boshu2/agentops/main/scripts/install.sh) -``` - -The important part is not that a command exists. - -The important part is that the repo starts accumulating operating memory: - -- issues that know what is blocked -- plans that can be checked before execution -- validation gates that fail before commit -- learnings that are retrievable in later work -- postmortems that produce better next work - -If you already use coding agents every day, that is probably the pain you feel -too. - -Not "the model is dumb." - -More like: - -- "why did it forget the decision we made two days ago?" -- "why did it repeat the exact failed fix?" -- "why did it say this was done without actually validating it?" -- "why does every new session feel like I am onboarding a contractor with - amnesia?" - -That is the problem I think the category needs to solve. - -Not more demos of autonomous generation. - -Not bigger swarms for their own sake. - -An operational layer. - -That is what I am building toward with AgentOps. - -## HN Submission - -### Title - -Coding agents do not do their own bookkeeping - -### URL - -Use the dev.to canonical URL after publishing there, or submit the GitHub repo if -posting as a Show HN. - -### Text Blurb - -AgentOps is an open-source operational layer for coding agents. The core thesis -is that agents do not do their own bookkeeping, so work resets every session -unless you build bookkeeping, validation, primitives, and repeatable flows around -them. - -The post argues that the real category is not "memory" or "workflow tooling" in -isolation, but an operational layer that turns raw session signal into reusable -learnings, findings, validation, and better next work. It also includes the -current v2.36 flow split: `/rpi` for one lifecycle, `/evolve` for the autonomous -day loop, `/dream` for the overnight knowledge loop, and `PROGRAM.md` as the -repo-local contract that bounds autonomous work. - -## Publish Checklist - -- Post the `Draft` section to dev.to using the front matter above. -- Add a screenshot or terminal capture from README's `/evolve` or `/dream` - transcript if the platform supports an image. -- Submit to Hacker News using the HN title and text blurb above. -- After the external URLs exist, add them to `na-gtm.4` as `external_ref` or - notes, then close the bead. diff --git a/docs/preserved-refs.tsv b/docs/preserved-refs.tsv index fc9bb66b5..7bbe96bcf 100644 --- a/docs/preserved-refs.tsv +++ b/docs/preserved-refs.tsv @@ -20,14 +20,14 @@ codex/preserve-oe-w7-openai-docs-20260517 Boden Fuller Retire when the operation codex/preserve-oe-w7-oss-docs-20260517 Boden Fuller Retire when the operationalizing-expertise oss-docs methodology artifact is reviewed into the durable methodology corpus or rejected as superseded. Preserved clean codex/oe-w7-oss-docs worktree tip before removing the attached worktree during soc-7mhz closeout. codex/preserve-oe-w7-perf-20260517 Boden Fuller Retire when the operationalizing-expertise perf methodology artifact is reviewed into the durable methodology corpus or rejected as superseded. Preserved clean codex/oe-w7-perf worktree tip before removing the attached worktree during soc-7mhz closeout. codex/preserve-oe-w7-plan-20260517 Boden Fuller Retire when the operationalizing-expertise plan methodology artifact is reviewed into the durable methodology corpus or rejected as superseded. Preserved clean codex/oe-w7-plan worktree tip before removing the attached worktree during soc-7mhz closeout. -codex/preserve-oe-w8-post-mortem-20260517 Boden Fuller Retire when the operationalizing-expertise post-mortem methodology artifact is reviewed into the durable methodology corpus or rejected as superseded. Preserved clean codex/oe-w8-post-mortem worktree tip before removing the attached worktree during soc-7mhz closeout. +codex/preserve-oe-w8-postmortem-20260517 Boden Fuller Retire when the operationalizing-expertise postmortem methodology artifact is reviewed into the durable methodology corpus or rejected as superseded. Preserved clean codex/oe-w8-postmortem worktree tip before removing the attached worktree during soc-7mhz closeout. codex/preserve-oe-w8-pr-implement-20260517 Boden Fuller Retire when the operationalizing-expertise pr-implement methodology artifact is reviewed into the durable methodology corpus or rejected as superseded. Preserved clean codex/oe-w8-pr-implement worktree tip before removing the attached worktree during soc-7mhz closeout. codex/preserve-oe-w8-pr-plan-20260517 Boden Fuller Retire when the operationalizing-expertise pr-plan methodology artifact is reviewed into the durable methodology corpus or rejected as superseded. Preserved clean codex/oe-w8-pr-plan worktree tip before removing the attached worktree during soc-7mhz closeout. codex/preserve-oe-w8-pr-prep-20260517 Boden Fuller Retire when the operationalizing-expertise pr-prep methodology artifact is reviewed into the durable methodology corpus or rejected as superseded. Preserved clean codex/oe-w8-pr-prep worktree tip before removing the attached worktree during soc-7mhz closeout. codex/preserve-oe-w8-pr-research-20260517 Boden Fuller Retire when the operationalizing-expertise pr-research methodology artifact is reviewed into the durable methodology corpus or rejected as superseded. Preserved clean codex/oe-w8-pr-research worktree tip before removing the attached worktree during soc-7mhz closeout. codex/preserve-oe-w9-pr-retro-20260517 Boden Fuller Retire when the operationalizing-expertise pr-retro methodology artifact is reviewed into the durable methodology corpus or rejected as superseded. Preserved clean codex/oe-w9-pr-retro worktree tip before removing the attached worktree during soc-7mhz closeout. codex/preserve-oe-w9-pr-validate-20260517 Boden Fuller Retire when the operationalizing-expertise pr-validate methodology artifact is reviewed into the durable methodology corpus or rejected as superseded. Preserved clean codex/oe-w9-pr-validate worktree tip before removing the attached worktree during soc-7mhz closeout. -codex/preserve-oe-w9-pre-mortem-20260517 Boden Fuller Retire when the operationalizing-expertise pre-mortem methodology artifact is reviewed into the durable methodology corpus or rejected as superseded. Preserved clean codex/oe-w9-pre-mortem worktree tip before removing the attached worktree during soc-7mhz closeout. +codex/preserve-oe-w9-premortem-20260517 Boden Fuller Retire when the operationalizing-expertise premortem methodology artifact is reviewed into the durable methodology corpus or rejected as superseded. Preserved clean codex/oe-w9-premortem worktree tip before removing the attached worktree during soc-7mhz closeout. codex/preserve-oe-w9-product-20260517 Boden Fuller Retire when the operationalizing-expertise product methodology artifact is reviewed into the durable methodology corpus or rejected as superseded. Preserved clean codex/oe-w9-product worktree tip before removing the attached worktree during soc-7mhz closeout. codex/preserve-oe-w9-provenance-20260517 Boden Fuller Retire when the operationalizing-expertise provenance methodology artifact is reviewed into the durable methodology corpus or rejected as superseded. Preserved clean codex/oe-w9-provenance worktree tip before removing the attached worktree during soc-7mhz closeout. codex/preserve-oe-w10-push-20260517 Boden Fuller Retire when the operationalizing-expertise push methodology artifact is reviewed into the durable methodology corpus or rejected as superseded. Preserved clean codex/oe-w10-push worktree tip before removing the attached worktree during soc-7mhz closeout. diff --git a/docs/profiles/roles/platform-ops.yaml b/docs/profiles/roles/platform-ops.yaml index 2aa3b7cdb..71465c9d9 100644 --- a/docs/profiles/roles/platform-ops.yaml +++ b/docs/profiles/roles/platform-ops.yaml @@ -34,7 +34,7 @@ skills: commands: - /research - /implement - - /post-mortem + - /postmortem - /session-start - /session-end diff --git a/docs/reference.md b/docs/reference.md deleted file mode 100644 index a27d10304..000000000 --- a/docs/reference.md +++ /dev/null @@ -1,318 +0,0 @@ -# AgentOps Reference - -Deep documentation for AgentOps. For quick start, see [README](https://github.com/boshu2/agentops/blob/main/README.md). - -> **⚠ 3.0 note (hookless).** AgentOps 3.0 installs **no hooks by default**. Diagrams/text below that show `auto-inject (hook)` / `hook (automatic)` are **historical (2.x)** — in 3.0 context is pulled **on-demand** (`ao lookup` / `ao inject`) and the local pre-push Go gate (`ao gate check`) is routine release authority (the model is push-to-main; `validate.yml` is a tag/PR/manual backstop). See [3.0.md](3.0.md) and [MIGRATION-3.0.md](MIGRATION-3.0.md). - ---- - -## The Pipeline - -| Stage | Skill | What It Does | -|-------|-------|--------------| -| **Shift-left** | `/pre-mortem` | Simulate failures BEFORE you write code | -| **Execute** | `/crank` | Orchestrate epic loop, dispatch `/swarm` for each wave | -| **Execute** | `/swarm` | Spawn fresh-context agents for parallel work | -| **Validate** | `/council` | Multi-model consensus (2-6 judges, cross-vendor, debate mode) | -| **Gate** | `/vibe` | Complexity analysis + council validation — must pass to merge | -| **Learn** | `/post-mortem` | Extract learnings to feed future sessions | -| **Release** | `/release` | Pre-flight, changelog, version bumps, tag — everything up to git tag | - ---- - -## Execution Model - -`/swarm`, `/crank`, and `/implement` use runtime-native backends for parallel execution: - -| Property | How it works | -|----------|-------------| -| **Backends** | Auto-detected at runtime: `spawn_agent` (Codex) → `TeamCreate` (Claude) → `Task(run_in_background=true)` (fallback). For headless batch execution, `lib/scripts/team-runner.sh` launches Codex workers from a team spec. Claude workers run through NTM panes or in-harness subagents, not print-mode shell workers. | -| **Dependencies** | None (runtime-native) | -| **Context** | Fresh per agent (team-per-wave) | -| **Coordination** | `wait`/`SendMessage`/`TaskOutput` + `TaskList` | -| **Commits** | Lead-only (workers blocked by hook) | - -The headless team backend preserves a shared worker artifact contract: - -- team spec selects `runtime: codex` or omits `runtime` -- Codex workers write the shared `lib/schemas/worker-output.json` artifact -- Codex streams are watched by `lib/scripts/watch-codex-stream.sh` -- Claude concurrency belongs in NTM panes or in-harness subagents, outside `team-runner.sh` - ---- - -## Which Skill Should I Use? - -| You Want | Use | Why | -|----------|-----|-----| -| Parallel tasks (fresh context each) | `/swarm` | Spawns fresh-context agents; the orchestrator owns the loop | -| Execute an entire epic | `/crank` | Orchestrates waves via `/swarm` until done | -| Single issue, full lifecycle | `/implement` | Claim → execute → validate → close | -| Gate progress without executing | `/ratchet` | Records/checks gates only | - ---- - -## The `/vibe` Validator - -Not just "does it compile?" — **does it match the spec?** - -| Aspect | What It Checks | -|--------|----------------| -| Semantic | Does code do what spec says? | -| Security | SQL injection, auth bypass, hardcoded secrets | -| Quality | Dead code, copy-paste, magic numbers | -| Architecture | Layer violations, circular deps, god classes | -| Complexity | Cyclomatic > 10, deep nesting | -| Performance | N+1 queries, unbounded loops, resource leaks | -| Slop | AI hallucinations, cargo cult, over-engineering | -| Accessibility | Missing ARIA, broken keyboard nav, contrast | - -**Gate rule:** 0 critical = pass. 1+ critical = blocked until fixed. - ---- - -## Architecture - -``` -ORCHESTRATOR AGENTS (executors) ------------- ------------------ - -/crank epic-123 - | - +-> Get ready issues -----------> /swarm selects runtime backend per wave - | | - +-> Create tasks ----------------> +-> Workers spawn as sub-agents/teammates - | | - +-> Workers report completion <---- +-> Fresh context, execute atomically - | (via wait/message/output) | - +-> /vibe (validation gate) +-> Return result via runtime channel - | | - | +-> PASS = progress locked (/ratchet) - | +-> FAIL = fix first - | - +-> Loop until DONE - | - +-> /post-mortem ----------------> .agents/learnings/ - | -NEXT SESSION | ------------- | -explicit pull (`ao session bootstrap`) <+ - | - +-> Starts with prior knowledge -``` - -### Full Workflow Stages - -``` -INPUT: SPEC (from superpowers, SDD, or your workflow) - └── Plan, issues, acceptance criteria - -STAGE 1: PRE-MORTEM [validation gate] - /pre-mortem → Simulate failures BEFORE implementing - -STAGE 2: EXECUTE [orchestrated + fresh context] - /crank → Autonomous loop - └── /swarm → Parallel agents (fresh context each) - -STAGE 3: VALIDATE [validation gate] - /vibe → 8-aspect check, must pass to commit - -STAGE 4: LEARN [compounding memory] - /post-mortem → Extract learnings for next session - -OUTPUT: LEARNINGS (feed your next spec) - └── .agents/learnings/, .agents/patterns/ -``` - -### The Knowledge Flywheel - -``` - THE KNOWLEDGE FLYWHEEL - - SESSION START - +------------------------------------+ - | pull prior knowledge explicitly | <-- `ao session bootstrap` / `ao inject` - +------------------+-----------------+ - | - v - +------------------------------------+ - | /pre-mortem catch risks early | <-- you run this - +------------------+-----------------+ - | - v - +------------------------------------+ - | /crank parallel execution | <-- you run this - | +- /swarm fresh-context agents | - +------------------+-----------------+ - | - v - +------------------------------------+ - | /vibe validation gate | <-- you run this - +------------------+-----------------+ - | - v - +------------------------------------+ - | /post-mortem extract learnings | <-- you run this - +------------------+-----------------+ - | - v - SESSION END - +------------------------------------+ - | auto-extract new learnings | <-- hook (automatic) - +------------------+-----------------+ - | - v - .agents/ (local, git-ignored by default, compounds across sessions) - |-- learnings/ - |-- patterns/ - |-- plans/ - +-- council/ - | - +--------> next session starts here --------+ - | - +-------------------------------------------+ - | - v - (back to top) -``` - -For the science behind the flywheel, see [`knowledge-flywheel.md`](knowledge-flywheel.md) and [`the-science.md`](the-science.md). - ---- - -## Installation Options - -### Claude Code (Plugin + Marketplace, preferred) - -```bash -# Add/update marketplace source -curl -fsSL https://raw.githubusercontent.com/boshu2/agentops/main/scripts/install-claude.sh | bash -curl -fsSL https://raw.githubusercontent.com/boshu2/agentops/main/scripts/install-claude.sh | bash -s -- --update - -# Pin to a tagged release instead of tracking the marketplace default branch -curl -fsSL https://raw.githubusercontent.com/boshu2/agentops/main/scripts/install-claude.sh | bash -s -- --ref v3.1.0 -``` - -### Per-Runtime Install (non-Claude plugin path) - -```bash -# Codex CLI (installs the native plugin, archives stale raw mirrors when needed, then open a fresh session) -curl -fsSL https://raw.githubusercontent.com/boshu2/agentops/main/scripts/install-codex.sh | bash - -# Gemini / Antigravity -curl -fsSL https://raw.githubusercontent.com/boshu2/agentops/main/scripts/install-agy.sh | bash - -# OpenCode -curl -fsSL https://raw.githubusercontent.com/boshu2/agentops/main/scripts/install-opencode.sh | bash - -# Other agents (example: Cursor) -bash <(curl -fsSL https://raw.githubusercontent.com/boshu2/agentops/main/scripts/install.sh) -bash <(curl -fsSL https://raw.githubusercontent.com/boshu2/agentops/main/scripts/install.sh) - -# Update all -bash <(curl -fsSL https://raw.githubusercontent.com/boshu2/agentops/main/scripts/install.sh) -``` - -### CLI Install - -```bash -# macOS -brew tap boshu2/agentops https://github.com/boshu2/homebrew-agentops -brew install agentops - -# Windows PowerShell -irm https://raw.githubusercontent.com/boshu2/agentops/main/scripts/install-ao.ps1 | iex - -# Any OS with Go -go install github.com/boshu2/agentops/cli/cmd/ao@latest - -# From your repo root: create `.agents/` (hookless — AgentOps 3.0 ships zero hooks) -cd /path/to/your/repo -ao init -``` - -> **Note:** There's a [known bug](https://github.com/anthropics/claude-code/issues/15178) where plugin skills don't appear when pressing `/`. Skills still work — just type them directly. - ---- - -## Tool Dependencies - -The `/vibe` skill runs complexity analysis (radon/gocyclo) then spawns a `/council` validation with the `code-review` preset (error-paths, api-surface, spec-compliance). External linters and scanners are used when available. **All tools are optional** — missing ones are skipped gracefully. - -| Tool | Purpose | Install | -|------|---------|---------| -| **gitleaks** | Secret scanning | `brew install gitleaks` | -| **semgrep** | SAST security patterns | `brew install semgrep` | -| **trivy** | Dependency vulnerabilities | `brew install trivy` | -| **gosec** | Go security | `go install github.com/securego/gosec/v2/cmd/gosec@latest` | -| **hadolint** | Dockerfile linting | `brew install hadolint` | -| **ruff** | Python linting | `pip install ruff` | -| **radon** | Python complexity | `pip install radon` | -| **golangci-lint** | Go linting | `brew install golangci-lint` | -| **shellcheck** | Shell linting | `brew install shellcheck` | - -**Quick install (recommended):** -```bash -brew install gitleaks semgrep trivy hadolint shellcheck golangci-lint -pip install ruff radon -``` - -More tools = more coverage. But even with zero tools installed, the workflow still runs. - ---- - -## CLI Reference - -The `ao` CLI handles knowledge persistence with MemRL two-phase retrieval, confidence decay (stale knowledge ages out), and citation-tracked provenance so you can trace learnings back to the session that produced them. - -```bash -ao quick-start --minimal # Create .agents/ structure (or use /quickstart skill) -ao init # Create `.agents/` (hookless — AgentOps 3.0 ships zero hooks) -ao inject [topic] # Load prior knowledge explicitly/JIT -ao search "query" # Search workspace session history plus repo-local knowledge -ao flywheel status # Knowledge growth rate, escape velocity -ao metrics report # Flywheel health dashboard -ao forge transcript # Extract learnings from session transcripts -ao ratchet status # RPI progress gates (Discovery → Implementation → Validation) -ao pool list # Show knowledge by quality tier -``` - ---- - -## All Skills - -### Primary - -| Skill | Purpose | -|-------|---------| -| `/pre-mortem` | Simulate failures before coding | -| `/crank` | Autonomous epic execution (orchestrator; runs waves via `/swarm`) | -| `/swarm` | Parallel agents with fresh context (runtime-native backends) | -| `/council` | Multi-model consensus (validate, research, brainstorm) | -| `/vibe` | Complexity + council validation gate | -| `/implement` | Single issue execution | -| `/post-mortem` | Extract learnings | -| `/research` | Deep codebase exploration | -| `/plan` | Break goal into tracked issues | -| `/release` | Pre-flight, changelog, version bumps, tag | -| `/ratchet` | Progress gates that lock (Discovery → Implementation → Validation) | -| `/beads` | Git-native issue tracking | - -### Additional - -| Skill | Purpose | -|-------|---------| -| `/retro` | Quick retrospective | -| `ao lookup` | Manually load prior knowledge | -| `/knowledge` | Query knowledge base | -| `/bug-hunt` | Root cause analysis | -| `/complexity` | Code complexity metrics | -| `/doc` | Documentation generation | -| `/standards` | Language-specific rules | - ---- - -## Troubleshooting - -- Plugin skills don't show up when you press `/` in Claude Code: type the skill directly (e.g. `/pre-mortem`). (See the [Claude Code issue](https://github.com/anthropics/claude-code/issues/15178).) -- `ao` not found: ensure it's on your `PATH` (`which ao`). For 3.0 orientation, the relevant surface is `ao session bootstrap` / `ao inject` (AgentOps 3.0 is hookless — there is no hook setup step). diff --git a/docs/reference/agentops-domain-evolution-bdd.md b/docs/reference/agentops-domain-evolution-bdd.md deleted file mode 100644 index e4158cb7d..000000000 --- a/docs/reference/agentops-domain-evolution-bdd.md +++ /dev/null @@ -1,72 +0,0 @@ -# AgentOps Domain Evolution BDD - -This is the acceptance contract for the AgentOps 3.0 evolution program. -AgentOps is not a pile of skills or a hook system; it is an SDLC control plane -and context compiler for LLM agents. It turns software-engineering practice into -compact, executable, verifiable agent context. The loop only starts after the -audit, domain map, and hexagonal target architecture exist. - -```gherkin -Feature: Domain-governed AgentOps 3.0 evolution - AgentOps must line up skills, CLI, hooks, docs, tests, beads, and knowledge - around small provable changes, evolving through observable behavior, bounded - contexts, ports, local proof, and trust evidence instead of broad text rewrites. - - Background: - Given the local AgentOps repository has fetched origin/main - And PRODUCT.md, GOALS.md, PROGRAM.md, and the operating loop are treated as direction sources - And bead "soc-y5vh" is the active Loop epic - And external-corpus-derived observations are used only through the clean-room policy - - Scenario: Audit every skill before changing shipped behavior - Given the checked-in skill catalog is generated from `skills/**/SKILL.md` - When the evolution bootstrap audits the catalog - Then every skill is assigned exactly one primary bounded context - And each skill has a preliminary keep, update, refactor, merge-review, or cut-review disposition - And low-confidence assignments are called out before implementation begins - - Scenario: Map AgentOps onto the hexagonal architecture - Given AgentOps uses BDD, DDD, Hexagonal Architecture, TDD, XP, CI/SRE, ADRs, and provenance as one system - When the bootstrap builds the architecture map - Then the core domains are Corpus, Validation, Loop, Factory, Runtime, and Orchestration - And each domain lists its primary ports, driving adapters, driven adapters, and proof gates - And Loop work from "soc-y5vh" depends on typed loop ports rather than shell-only state reads - And no domain treats skills or hooks as the product by themselves - - Scenario: Bootstrap local Codex orchestration before productizing - Given local Codex skills can be tested without changing shipped AgentOps skills - When the bootstrap skill is installed under "/Users/bo/.codex/skills" - Then it explains how to run the audit, BDD, domain map, architecture map, and evolution plan - And it points to the AgentOps skill factory for per-skill upgrades - And it does not mutate externally installed skills or copy external skill content - - Scenario: Run evolution in safe vertical slices - Given the BDD contract and domain map pass validation - When the operator starts an evolution loop - Then each cycle selects one bead-backed or generated slice - And the slice carries a Given/When/Then acceptance row - And the first failing proof is named before implementation - And the result is kept only when validation passes and evidence is recorded - - Scenario: Orchestrate unattended work through the ao CLI - Given the worktree is clean, synced, and branch-attached - And the installed ao binary exposes the same required commands as the source-built CLI - When the operator starts an unattended evolution cycle - Then "ao evolve" (removed in 3.0 — use the /evolve skill in-session and `ao goals measure` for fitness; scheduled runs go through the external NTM + Agent Mail substrate, not an in-repo daemon) ran with lease, cleanup, and bounded max-cycle settings - And landing policy starts as "off" until a reviewed cycle proves safe - And commit or sync-push landing is used only after explicit authorization - - Scenario: Handle divergent main without corrupting local work - Given local main may contain uncommitted user or agent work - And origin/main may contain newer product direction - When the bootstrap needs the latest direction - Then it reads current direction from origin/main without resetting the dirty tree - And it reports divergence before any merge, rebase, or branch-changing action -``` - -## Acceptance Checks - -- `bash scripts/check-agentops-domain-evolution-plan.sh` -- `bash skills/heal-skill/scripts/heal.sh --strict` -- `bash scripts/validate-skill-frontmatter.sh --strict` -- `bash tests/docs/validate-doc-release.sh` diff --git a/docs/reference/agentops-domain-evolution-plan.md b/docs/reference/agentops-domain-evolution-plan.md deleted file mode 100644 index 1b4b6fb2f..000000000 --- a/docs/reference/agentops-domain-evolution-plan.md +++ /dev/null @@ -1,176 +0,0 @@ -# AgentOps Domain Evolution Plan - -This plan is the bridge between the BDD contract and future `/evolve` runs. It -does not authorize bulk rewrites. It creates the control plane that lets the -catalog be improved one vertical slice at a time. - -AgentOps 3.0 is the target: an SDLC control plane and context compiler for LLM -agents. BDD/Gherkin expresses intent as observable behavior; DDD gives shared -names and bounded contexts; Hexagonal Architecture keeps runtime adapters out of -the core; TDD proves done locally; XP keeps slices small; CI/SRE/ADRs/provenance -make trust and memory repeatable. - -## Inputs Used - -| Input | Status | -|---|---| -| `soc-y5vh` | Live bead read; epic is 8/9 complete with `soc-y5vh.8` in progress. | -| `.agents/research/soc-y5vh.8-ao-loop-hypothesis-converged.md` | Not present in this working tree. | -| `.agents/plans/2026-05-16-ao-loop-hypothesis-converged.md` | Not present in this working tree. | -| `origin/main:PRODUCT.md` | Read after `git fetch`; product direction has moved ahead of local main. | -| `origin/main:GOALS.md` | Read after `git fetch`; Directive 12 is the governing loop-shape rule. | -| `origin/main:PROGRAM.md` | Read after `git fetch`; defines mutable scope and vertical-slice policy. | -| `origin/main:skills/evolve/SKILL.md` | Read after `git fetch`; documented the v2 `evolve` CLI subcommand (removed in 3.0 — use the `/evolve` skill in-session, `ao goals measure` for fitness) and loop-port direction. | -| `docs/plans/2026-05-12-rescope-evolve-and-architecture.md` | Read from `origin/main`; defines BC1-BC5 and port waves. | - -Local freshness note: `git fetch` found local `main` diverged from -`origin/main` by 4 local-only commits and 3 remote-only commits. This plan uses -`origin/main` direction sources without merging, rebasing, or resetting the -dirty working tree. - -## Execution Strategy - -### Phase 0: Control Plane - -Status: this patch. - -- Write the Gherkin BDD acceptance contract. -- Map all 77 checked-in skills into domains. -- Create the hexagonal architecture target. -- Add a checker that proves every skill appears in the domain map. -- Bootstrap a local Codex skill that can orchestrate this program as a - context-compiler evolution cycle, not a skill-pile rewrite. - -### Phase 1: Local Bootstrap Proving - -Use `/Users/bo/.codex/skills/agentops-evolution-bootstrap` and -`/Users/bo/.codex/skills/agentops-skill-factory`. - -Done when: - -- the local bootstrap skill validates with Codex `skill-creator`, -- it can regenerate or inspect the BDD/domain/architecture/control docs, -- it can score any AgentOps skill and choose the smallest next patch, -- it refuses external skill content copying and external skill mutation. - -### Phase 1.5: CLI Orchestration Rehearsal - -Use the `ao` CLI as the runner, but only after preflight proves the tree and -binary are safe. - -Preflight: - -```bash -git fetch --prune origin -git rev-list --left-right --count HEAD...origin/main -git status --short -bash scripts/check-worktree-disposition.sh -cd cli && env -u AGENTOPS_RPI_RUNTIME go run ./cmd/ao autodev validate --file ../PROGRAM.md --json -# (removed in 3.0) the evolve CLI subcommand no longer exists — use the /evolve skill in-session; ao goals measure for fitness -# (removed in 3.0, f61c5f0e7) the rpi-loop command no longer exists — drive the seven-move operating loop (docs/architecture/operating-loop.md) via the /rpi skill -ao loop --help 2>/dev/null || true -cd cli && go run ./cmd/ao loop --help -``` - -Current hazard: source may expose `ao loop append/history/verify` while the -installed `/Users/bo/go/bin/ao` does not. Do not run unattended against a stale -installed binary when the selected slice needs the newer CLI surface. - -Rehearsal command: - -```bash -ao factory start --goal "AgentOps 3.0 domain evolution" -# (removed in 3.0) the evolve CLI subcommand and its --dry-run/--max-cycles/--repo-filter/--landing-policy flags no longer exist — -# rehearse via the /evolve skill in-session; scheduled runs go through the external NTM + Agent Mail substrate (no in-repo daemon) -``` - -First real command, after rehearsal: - -```bash -# (removed in 3.0) the evolve CLI subcommand that ran "Land BC3 Loop slice for soc-y5vh.8" no longer exists. -# Drive the slice via the /evolve skill in-session (ao goals measure for fitness); -# scheduled runs go through the external NTM + Agent Mail substrate, not an in-repo daemon. -# Former flags, no longer a CLI surface: -# --max-cycles 1 --repo-filter agentops --lease --ensure-cleanup -# --auto-clean --gate-policy best-effort --landing-policy off -``` - -Landing stays manual or `/push`-driven until one unattended cycle is reviewed. -Only use `--landing-policy commit` or `--landing-policy sync-push` from a clean -synced task worktree with explicit operator authorization. - -### Phase 2: Loop Spine Upgrade - -Upgrade the highest-leverage BC3 skills first: - -1. `evolve` -2. `rpi` -3. `discovery` -4. `plan` -5. `crank` -6. `validation` -7. `post-mortem` -8. `ratchet` - -Each skill gets one small patch per cycle: usually `SELF-TEST.md`, sharper -trigger boundaries, or a reference split. For `evolve`, do not settle for text: -the repo implementation must follow `soc-y5vh.8` through typed loop ports. - -### Phase 3: Factory Spine Upgrade - -Upgrade BC4 skills so future skills scaffold to the new standard by default: - -1. `skill-builder` -2. `skill-auditor` -3. `heal-skill` -4. `standards` -5. `converter` -6. `bootstrap` - -Expected result: new and updated skills include domain metadata, self-tests, -small references, validation commands, and productization boundaries. - -### Phase 4: Corpus, Validation, and Runtime Waves - -Run domain-local waves only when write scopes are disjoint: - -- BC1 Corpus: `compile`, `inject`, `flywheel`, `forge`, `harvest`, `dream` -- BC2 Validation: `council`, `vibe`, `pre-mortem`, `test`, `review`, - `security-suite`, `release` -- BC5 Runtime: `hooks-authoring`, `scope`, `push`, `swarm`, `codex-team` - -Candidate merge/cut reviews happen after core spines are stable. Do not delete -skills until a replacement workflow, migration note, and validation result exist. - -## Per-Skill Evolution Loop - -For each skill: - -1. Read the skill and domain-map row. -2. Score it with the local skill factory. -3. Write or select one Gherkin acceptance row. -4. Choose one action: keep, update, refactor, merge-review, or cut-review. -5. Apply the smallest patch that improves the action's evidence. -6. Run skill-local validation plus the domain-evolution checker. -7. Record remaining gaps and move to the next skill. - -## CLI and Hook Extension - -After the skill catalog is domain-mapped, apply the same loop to the CLI and -hooks: - -- CLI commands map to one bounded context and one port surface. -- Hooks map to Runtime adapters and Validation gates. -- Scripts map to either gate adapters, corpus adapters, or loop mechanics. -- New shell-only read paths are rejected when a typed port already exists. - -## Stop Conditions - -The evolution program is complete when: - -- all skills have a domain, disposition, and validation evidence, -- all loop-spine skills have self-tests, -- `soc-y5vh.8` is closed with typed Loop-port acceptance evidence, -- merge/cut candidates have explicit replacement decisions, -- CLI and hooks have the same BC ownership map, -- local validation passes without relying on hidden `.agents` state. diff --git a/docs/reference/agentops-hexagonal-architecture-map.md b/docs/reference/agentops-hexagonal-architecture-map.md deleted file mode 100644 index c1fb5e819..000000000 --- a/docs/reference/agentops-hexagonal-architecture-map.md +++ /dev/null @@ -1,128 +0,0 @@ -# AgentOps Hexagonal Architecture Map - -This document turns the skill-domain audit into an architectural target. It is -not a new architecture; it operationalizes the current `origin/main` direction: -AgentOps is an SDLC control plane and context compiler for LLM agents. The -operating loop is the primitive, and BDD/Gherkin + DDD + Hexagonal Architecture -+ TDD is the narrow waist. XP keeps slices small; CI, SRE, ADRs, provenance, -beads, and ratcheted knowledge make trust repeatable. - -## Bounded Contexts - - - -| Context | Core responsibility | Current center of gravity | Ports to make explicit | -|---|---|---|---| -| BC1 Corpus | Capture, retrieve, compile, cite, and promote knowledge. | `.agents/`, `ao corpus`, `compile`, `inject`, `forge`, `harvest`, `dream` | `CorpusReaderPort`, `CorpusWriterPort`, `CitationPort`, `FindingCompilerPort` | -| BC2 Validation | Judge whether plans, code, docs, dependencies, and releases are fit. | `validation`, `vibe`, `council`, gates, evals, CI scripts | `GateRunnerPort`, `CIStatusPort`, `ScenarioRunnerPort`, `EvidenceBinderPort` | -| BC3 Loop | Select work, execute RPI, log cycles, measure fitness, and stop at convergence. | `evolve`, `rpi`, `autodev`, `goals`, `beads`, loop CLI | `LoopReaderPort`, `LoopWriterPort`, `HypothesisLedgerPort`, `ConvergenceCheckPort`, `WorkSelectorPort` | -| BC4 Factory | Build, audit, package, and govern reusable skills and product claims. | `skill-builder`, `skill-auditor`, `heal-skill`, standards, docs | `SkillCatalogPort`, `SkillScorerPort`, `FactoryAdmissionPort`, `ClaimEvidencePort` | -| BC5 Runtime | Adapt the control plane to harnesses, shells, local gates, and operator machines (hookless by default). | Codex/Claude skills, cc-hooks, GitHub PR skills, `push`, `scope`, `swarm`, `agent-native` | `HarnessPort`, `OperatorPort`, `EventBusPort`, `GitPort`, `IssueTrackerPort` | -| BC6 Orchestration | Spawn, coordinate, and converge multi-agent swarms across panes, mailboxes, and renewal loops. | `ntm`, `swarm`, `agent-mail`, `using-atm`, `vibing-with-ntm`, `continuity-loop` | `OrchestrationPort`, `SwarmDispatchPort`, `AgentMailPort`, `ConvergencePort` | - -## Hexagonal Rule - -Domain policy lives inside the bounded context. Filesystems, GitHub, `br`, -agent harnesses, shell scripts, external skill corpora, CI, and local machine -state are adapters. Adapters can be swapped; the loop contract cannot. - -The core product is therefore not "more skills" or "more hooks." The product is -the alignment layer that turns intent, names, ports, proofs, and memory into -compact executable context for agents. - -```mermaid -flowchart LR - CLI["ao CLI"] - Skills["Codex/Claude skills"] - Hooks["Hooks and CI"] - Substrate["Substrate schedules"] - - subgraph Hex["AgentOps core"] - Corpus["BC1 Corpus"] - Validation["BC2 Validation"] - Loop["BC3 Loop"] - Factory["BC4 Factory"] - Runtime["BC5 Runtime"] - Orchestration["BC6 Orchestration"] - end - - FS["Filesystem .agents"] - BR["br tracker"] - Git["Git/GitHub"] - Harness["Agent harnesses"] - ExtCorpus["External skill corpora (read-only analysis)"] - - CLI --> Loop - Skills --> Loop - Hooks --> Validation - Substrate --> Loop - Loop --> Corpus - Loop --> Validation - Loop --> Factory - Runtime --> Skills - Corpus --> FS - Loop --> BR - Runtime --> Git - Runtime --> Harness - Factory --> ExtCorpus -``` - -## Loop Context and `soc-y5vh` - -The active bead `soc-y5vh` says the Loop context is not done until the `/evolve` -read path stops relying on direct shell reads and writes for loop decisions. -The current remaining child, `soc-y5vh.8`, requires a bounded `ao loop` surface -or wrapper that uses production `HypothesisLedgerPort` and -`ConvergenceCheckPort` behavior. - -The evolution program must therefore treat these as hard architecture rules: - -+ Loop cycle selection reads prior failures through typed Corpus/Loop ports. -+ Healing-first mode reads CI status through a typed Validation port. -+ Hypotheses append through a ledger port, not ad hoc JSONL appends. -+ Convergence STOP evaluates through a convergence port, not only a text file. -+ Skill text may describe the loop, but Go ports and tests make it real. - -## Adapter Assignment - -| Adapter surface | Drives or is driven by | Primary context | Rule | -|---|---|---|---| -| `skills/*/SKILL.md` | Driving adapter | BC3/BC4 | Skills describe operator workflows and must point to executable proof. | -| `skills-codex/*` | Runtime adapter | BC5 | Codex artifacts mirror or tailor source skills; drift is gated. | -| `cli/cmd/ao/*` | Driving adapter | All contexts | CLI commands expose bounded operations, not hidden policy blobs. | -| `cli/internal/ports/*` | Port boundary | All contexts | Ports are narrow interfaces with tests and at least one plausible alternate adapter. | -| `cli/internal/adapters/*` | Driven adapter | Context-specific | Adapters implement ports against filesystem, Git, tracker, CI, or harness state. | -| `hooks/*.sh` | Runtime adapter | BC5/BC2 | Hooks may block or warn, but policy should be traceable to context contracts. | -| `scripts/check-*.sh` and `scripts/validate-*.sh` | Gate adapter | BC2 | Validation scripts should be callable by gates and eventually by `GateRunnerPort`. | -| `.agents/*` | Runtime corpus state | BC1/BC3 | Local-only unless explicitly promoted to durable docs or release artifacts. | - -## CLI Orchestration - -Codex skills should guide the run, but `ao` should execute the terminal-native -loop. The safe chain is: - -```text -agentops-evolution-bootstrap - -> ao factory start - -> /evolve skill in-session for the improvement loop (ao evolve removed in 3.0; ao goals measure for fitness; scheduled runs via the external substrate) - -> the seven-move operating loop driven in-session by /rpi (ao rpi phased removed in 3.0; see docs/architecture/operating-loop.md) - -> ao loop history/append/verify for typed BC3 cycle state - -> /push or explicit git only after validation and authorization -``` - -If `ao loop` exists in source but not in the installed binary, the runtime -adapter is stale. Update or invoke a source-built CLI from a clean synced -worktree before unattended mode. - -## Evolution Guardrails - -1. Start every non-trivial item with a Gherkin acceptance row. -2. Map the row to exactly one bounded context. -3. Name the first failing proof before implementation. -4. Keep one slice inside one bounded context unless the architecture map says it - is an adapter boundary. -5. Keep slices XP-sized: one behavior, one owner, one write scope, one proof. -6. Promote lessons only through the ratchet: handoff, learning, skill/template, - gate, product/goal doctrine. -7. When a skill, CLI command, and hook all describe the same behavior, the CLI - or testable script is the proof source; skill prose is the adapter contract. diff --git a/docs/reference/agentops-skill-domain-map.md b/docs/reference/agentops-skill-domain-map.md index dd082dc92..af1d9e62b 100644 --- a/docs/reference/agentops-skill-domain-map.md +++ b/docs/reference/agentops-skill-domain-map.md @@ -8,7 +8,7 @@ ## driven-adapter -`converter`, `scope`, `security` +`converter`, `security` ## driving-adapter @@ -16,7 +16,7 @@ ## supporting -`account-rotation`, `agent-mail`, `agent-native`, `automation-shape-routing`, `cass`, `cc-hooks`, `codebase-recon`, `dcg`, `doc`, `handoff`, `heal-skill`, `learn`, `ms`, `ntm`, `operationalize`, `pattern-mining`, `rch`, `refactor`, `reverse-engineer`, `sbh`, `scaffold`, `skill-builder`, `standards`, `test`, `toil-mining`, `workflow-builder` +`account-rotation`, `agent-mail`, `agent-native`, `automation-shape-routing`, `cass`, `cc-hooks`, `codebase-recon`, `dcg`, `doc`, `handoff`, `heal-skill`, `learn`, `ms`, `ntm`, `operationalize`, `pattern-mining`, `rch`, `refactor`, `reverse-engineer`, `sbh`, `scaffold`, `scope`, `skill-builder`, `standards`, `test`, `toil-mining`, `workflow-builder` ## Inventory @@ -24,51 +24,51 @@ |---|---|---|---|---|---| | `account-rotation` | execution | `keep_specialist` | - | `account_rotation` | - | | `agent-mail` | execution | `keep_optional_adapter` | - | `agent_mail` | - | -| `agent-native` | meta | `keep_optional_adapter` | `ntm`, `agent-mail` | `role_dispatch`, `observe_workers`, `handoff` | `manage_runtime_sessions` | +| `agent-native` | meta | `keep_optional_adapter` | - | `role_dispatch`, `observe_workers`, `handoff` | `manage_runtime_sessions` | | `agy-native` | cross-vendor | `keep_optional_adapter` | - | `dispatch_explicit_packet`, `provide_fresh_context` | `start_agy_session` | -| `automation-shape-routing` | meta | `keep_optional_adapter` | `skill-builder`, `workflow-builder`, `agent-native` | `automation_shape_routing` | - | -| `bootstrap` | session | `keep_specialist` | `goals`, `product`, `doc`, `shared`, `scaffold` | `bootstrap` | - | +| `automation-shape-routing` | meta | `keep_optional_adapter` | - | `automation_shape_routing` | - | +| `bootstrap` | session | `keep_specialist` | - | `bootstrap` | - | | `cass` | execution | `keep_specialist` | - | `cass` | - | | `cc-hooks` | execution | `keep_specialist` | - | `cc_hooks` | - | -| `codebase-recon` | execution | `keep_specialist` | `doc` | `codebase_recon` | - | +| `codebase-recon` | execution | `keep_specialist` | - | `codebase_recon` | - | | `codex-exec` | orchestration | `keep_optional_adapter` | - | `codex_exec` | - | | `converter` | cross-vendor | `keep_specialist` | - | `converter` | - | | `council` | judgment | `keep_strategy` | - | `collect_independent_judgments`, `synthesize_disagreement` | `write_advisory_council_report` | | `dcg` | execution | `keep_specialist` | - | `dcg` | - | -| `doc` | product | `keep_specialist` | `standards`, `council` | `doc` | - | +| `doc` | product | `keep_specialist` | - | `doc` | - | | `domain` | knowledge | `keep_specialist` | - | `domain` | - | -| `dueling-idea-genies` | judgment | `keep_strategy` | `idea-genie` | `dueling_idea_genies` | - | +| `dueling-idea-genies` | judgment | `keep_strategy` | - | `dueling_idea_genies` | - | | `goals` | product | `keep_specialist` | - | `goals` | - | | `handoff` | session | `keep_specialist` | - | `handoff` | - | -| `heal-skill` | meta | `keep_specialist` | - | `heal_skill` | - | -| `idea-genie` | execution | `keep_strategy` | `research` | `generate_evidenced_options` | `write_idea_portfolio` | +| `heal-skill` | meta | `keep_specialist` | - | `heal_skill` | `optional_skill_projection_repair` | +| `idea-genie` | execution | `keep_strategy` | - | `generate_evidenced_options` | `write_idea_portfolio` | | `implement` | execution | `keep` | - | `execute_one_experiment`, `collect_factual_evidence` | `modify_declared_subject`, `write_candidate_packet` | | `learn` | execution | `keep_off_path` | - | `analyze_verdict_collections` | `write_advisory_observations` | | `ms` | execution | `keep_specialist` | - | `ms` | - | | `ntm` | execution | `keep_optional_adapter` | - | `ntm` | - | | `operationalize` | meta | `keep_specialist` | - | `distill_expertise`, `propose_artifact_shape` | `write_advisory_proposal` | -| `pattern-mining` | execution | `keep_specialist` | `operationalize` | `pattern_mining` | - | +| `pattern-mining` | execution | `keep_specialist` | - | `pattern_mining` | - | | `plan` | execution | `keep` | - | `shape_intent`, `define_acceptance`, `bound_write_scope` | `write_plan_packet` | -| `postmortem` | judgment | `keep_strategy` | `council`, `toil-mining` | `postmortem` | - | -| `premortem` | judgment | `keep_strategy` | `council` | `premortem` | - | +| `postmortem` | judgment | `keep_strategy` | - | `postmortem` | - | +| `premortem` | judgment | `keep_strategy` | - | `challenge_plan` | `write_advisory_plan_review` | | `product` | product | `keep_specialist` | - | `shape_product_boundary` | `write_product_document` | | `rch` | execution | `keep_specialist` | - | `rch` | - | | `reality-check` | judgment | `keep_strategy` | - | `compare_claim_to_evidence` | `write_advisory_gap_report` | -| `refactor` | execution | `keep_specialist` | `standards`, `pattern-mining` | `refactor` | - | -| `research` | execution | `keep_specialist` | `cass`, `ms`, `reverse-engineer`, `codebase-recon`, `pattern-mining` | `research` | - | +| `refactor` | execution | `keep_specialist` | - | `refactor` | - | +| `research` | execution | `keep_specialist` | - | `research` | - | | `reverse-engineer` | execution | `keep_specialist` | - | `reverse_engineer` | - | | `rpi` | meta | `keep` | `plan`, `implement`, `validate` | `orchestrate_once`, `report` | `dispatch_core_phases` | | `sbh` | execution | `keep_specialist` | - | `sbh` | - | -| `scaffold` | execution | `keep_specialist` | `standards` | `scaffold` | - | -| `scope` | meta | `keep_specialist` | - | `scope` | - | +| `scaffold` | execution | `keep_specialist` | - | `scaffold` | - | +| `scope` | meta | `keep_specialist` | - | `scope_review` | - | | `security` | product | `keep_specialist` | - | `security` | - | | `shared` | library | `keep_specialist` | - | `provide_reference_context` | - | -| `skill-builder` | meta | `keep_specialist` | `heal-skill`, `converter` | `skill_builder` | - | +| `skill-builder` | meta | `keep_specialist` | - | `skill_builder` | `writes_skill_source`, `regenerates_skill_projections` | | `standards` | knowledge | `keep_specialist` | - | `standards` | - | -| `status` | session | `keep_specialist` | `sbh` | `status` | - | +| `status` | session | `keep_specialist` | - | `status` | - | | `swarm` | execution | `keep_optional_adapter` | - | `dispatch_once` | `invoke_selected_executor` | -| `test` | execution | `keep_specialist` | `standards` | `test` | - | -| `toil-mining` | meta | `keep_specialist` | `automation-shape-routing` | `toil_mining` | - | +| `test` | execution | `keep_specialist` | - | `test` | - | +| `toil-mining` | meta | `keep_specialist` | - | `toil_mining` | - | | `using-gc` | execution | `keep_optional_adapter` | - | `dispatch_explicit_packet`, `observe_gc_runtime` | `operate_gas_city` | | `validate` | judgment | `keep` | - | `compute_subject_identity`, `judge_acceptance`, `persist_verdict` | `write_verdict_artifact` | | `workflow-builder` | meta | `keep_specialist` | - | `workflow_builder` | - | diff --git a/docs/reference/agentops-skill-graph.md b/docs/reference/agentops-skill-graph.md index c75a957d1..da191a6cc 100644 --- a/docs/reference/agentops-skill-graph.md +++ b/docs/reference/agentops-skill-graph.md @@ -54,41 +54,9 @@ graph LR using_gc["using-gc"] validate["validate"] workflow_builder["workflow-builder"] - agent_native --> agent_mail - agent_native --> ntm - automation_shape_routing --> agent_native - automation_shape_routing --> skill_builder - automation_shape_routing --> workflow_builder - bootstrap --> doc - bootstrap --> goals - bootstrap --> product - bootstrap --> scaffold - bootstrap --> shared - codebase_recon --> doc - doc --> council - doc --> standards - dueling_idea_genies --> idea_genie - idea_genie --> research - pattern_mining --> operationalize - postmortem --> council - postmortem --> toil_mining - premortem --> council - refactor --> pattern_mining - refactor --> standards - research --> cass - research --> codebase_recon - research --> ms - research --> pattern_mining - research --> reverse_engineer rpi --> implement rpi --> plan rpi --> validate - scaffold --> standards - skill_builder --> converter - skill_builder --> heal_skill - status --> sbh - test --> standards - toil_mining --> automation_shape_routing ``` Hard dependencies only. Optional context relationships are listed in the context map. diff --git a/docs/release-e2e-checklist.md b/docs/release-e2e-checklist.md deleted file mode 100644 index 95754c601..000000000 --- a/docs/release-e2e-checklist.md +++ /dev/null @@ -1,100 +0,0 @@ -# Release E2E Checklist - -Use this checklist before tagging a release to verify the local gate and release-smoke paths. - -## 0) Pre-tag Triage - -Use this before you tag. The release process in [RELEASING](RELEASING.md) is the source of truth; this checklist only narrows the local checks that prove the release is ready. If a gate fails after a fast pass, do not tag. Use [Incident Runbook](INCIDENT-RUNBOOK.md) when a failure needs cleanup or recovery after local validation. - -| Question | Inspect | -|---|---| -| "Is the release process itself current?" | [RELEASING](RELEASING.md) and its pre-release checklist | -| "Did the local gate produce the expected evidence?" | `.agents/releases/local-ci//` for SBOM, security report, eval reports, readiness, HIL evidence, digital-twin/VIL evidence, and the manifest | -| "Did the bootstrap smoke actually run?" | Fast gate marker printed as `ao init + ao rpi smoke` — but the `ao rpi` sub-checks were **removed** in 3.0 with the RPI engine, so only the `ao init`/bootstrap part is live (3.0 is hookless — no hook-install smoke) | -| "Did the release smoke path fail after a fast pass?" | Re-run `bash scripts/ci-local-release.sh` and inspect the failing section before tagging | -| "Was the release already tagged or partially published?" | [RELEASING](RELEASING.md#failure-modes) and [Incident Runbook](INCIDENT-RUNBOOK.md) | - -## 1) Fast local gate (quick confidence) - -Run: - -```bash -bash scripts/ci-local-release.sh --fast --jobs 4 -``` - -Expect: - -- Exit code `0` -- Output contains these markers: - - `Codex runtime sections` - - `Codex artifact metadata` - - `Skill runtime parity` - - `ao init + ao rpi smoke` (the `ao rpi` sub-smoke was **removed** in 3.0 — only the `ao init`/bootstrap check is live) - -Reference test: `tests/integration/test-release-e2e-validation.sh`. - -## 2) Full local gate (pre-tag requirement) - -Run: - -```bash -bash scripts/ci-local-release.sh \ - --release-version X.Y.Z \ - --hil-target 'local:bushido:ao version && ao init --help && ao doctor --quick && ao session bootstrap >/dev/null' -``` - -Expect: - -- Exit code `0` -- Final summary contains `LOCAL CI PASSED` -- `.agents/releases/local-ci//release-artifacts.json` links the full proof bundle: - - `sbom_cyclonedx` and `sbom_spdx` - - `security_report` - - `eval_fast_report` and `eval_baseline_audit` - - `release_readiness` - - `hil_evidence` - - `vil_evidence` and `digital_twin_evidence` -- `.agents/releases/local-ci//release-readiness.json` has `release_status: pass` and `release_readiness_score >= 8` -- `.agents/releases/local-ci//hil-evidence.json` has `status: pass` or `status: waived`; passing targets record OS/architecture/runtime identity, workflow checks, command fingerprint, and release-version verification -- `.agents/releases/local-ci//digital-twin-evidence.json` has `status: pass` and `dimensions.vil.status: pass` -- `.agents/releases/local-ci//eval-agentops-fast.json` has `status: pass`, and `eval-baseline-audit.json` has no `stale_suite_hashes` - -If the fast gate passes but this full gate fails, stop. The release is not ready to tag. Use the failing section in the full-gate output, rerun the official local gate until it passes, and if a tag or publish already happened, follow the failure-mode steps in [RELEASING](RELEASING.md#failure-modes) before retrying anything else. - -## 3) Codex runtime lint (focused check) - -Run: - -```bash -bash scripts/validate-codex-runtime-sections.sh -``` - -Use this when editing Codex runtime guidance or AGENTS runtime sections. - -## 4) init + RPI smoke expectations - -AgentOps 3.0 is **hookless** — it ships zero hooks, so there is no hook-install -smoke check. The local gate includes this release E2E smoke check: - -- `ao init + ao pawl smoke` validates (in a fresh git repo): - - `ao init` (creates `.agents/`; no hooks installed) - - `ao pawl smoke` - - `ao session bootstrap` - - `/rpi --help` - -If the fast gate passes but this smoke path fails, treat the release as blocked. -Inspect the corresponding gate output, then rerun the full local gate so the fix -is validated end to end before tagging. - -## 5) Parity checks when workflow/docs contracts change - -When changing CI workflow policy, hook/runtime docs, or required gate wording, run: - -```bash -bash scripts/validate-ci-policy-parity.sh -bash scripts/validate-skill-runtime-parity.sh -bash scripts/validate-codex-runtime-sections.sh -bash scripts/validate-codex-generated-artifacts.sh --scope worktree -bash scripts/validate-codex-backbone-prompts.sh -bash tests/docs/validate-doc-release.sh -``` diff --git a/docs/rfcs/0001-finding-generator-parallelism.md b/docs/rfcs/0001-finding-generator-parallelism.md index 78dedeba0..922f8c337 100644 --- a/docs/rfcs/0001-finding-generator-parallelism.md +++ b/docs/rfcs/0001-finding-generator-parallelism.md @@ -26,7 +26,7 @@ Branch: `research/finding-generator-parallelism` - `cli/internal/overnight/external_watchlist_generator.go` — `runExternalWatchlistGenerator` reads the operator-managed watchlist and emits one `Status="proposed"`, `Requires=["human-review"]` candidate per entry past its `stale_after` window (default 168h). Missing file → soft-success with zero candidates; malformed YAML → soft-fail; ctx cancellation → soft-fail. Dedup key format: `external-watchlist|`. - `cli/internal/overnight/ingest.go` — registered in `findingGenerators()` alongside `mine-findings`; reuses `defaultFindingGeneratorTimeout` (2 min, RFC §253). -- `cli/internal/overnight/generator_sidecars.go` — `normalizeGeneratorCandidate` recognizes both `finding-generator|` and `external-watchlist|` prefixes (Wave 1 pre-mortem fix); without this, the aggregator silently rewrote external dedup keys. +- `cli/internal/overnight/generator_sidecars.go` — `normalizeGeneratorCandidate` recognizes both `finding-generator|` and `external-watchlist|` prefixes (Wave 1 premortem fix); without this, the aggregator silently rewrote external dedup keys. - `PROGRAM.md` — `.agents/dream/external-watchlist.yaml` listed under Mutable Scope. - `IngestResult.ExternalWatchlistEmitted` + `ingestSummary["external_watchlist_emitted"]` — fitness counter for lane throughput. @@ -65,7 +65,7 @@ The implementation follows that contract. `RunIngest` is documented as read-only ### Evolve is intentionally serial by default -The Evolve skill describes the operator cadence as: post-mortem, analyze repo state, select or create the next highest-value item, run `/rpi`, harvest follow-ups, and repeat until a cap, breaker, or real dormancy (`skills/evolve/SKILL.md:37-41`). Its first work source is `.agents/rpi/next-work.jsonl`, and the selector picks the highest-value unconsumed item (`skills/evolve/SKILL.md:43-58`, `skills/evolve/SKILL.md:191-194`). +The Evolve skill describes the operator cadence as: postmortem, analyze repo state, select or create the next highest-value item, run `/rpi`, harvest follow-ups, and repeat until a cap, breaker, or real dormancy (`skills/evolve/SKILL.md:37-41`). Its first work source is `.agents/rpi/next-work.jsonl`, and the selector picks the highest-value unconsumed item (`skills/evolve/SKILL.md:43-58`, `skills/evolve/SKILL.md:191-194`). The existing write-side parallel option is explicit and isolated. `ao rpi parallel` runs multiple RPI epics concurrently, each in its own git worktree, then merges successful branches in order and runs a gate (`cli/cmd/ao/rpi_parallel.go:59-167`). It dispatches epics with goroutines, enforces a per-epic timeout, reports failures, and stops merging on conflicts (`cli/cmd/ao/rpi_parallel.go:284-454`). The older parallel execution reference makes the safety model explicit: heuristic independence is not a guarantee; the regression gate is the real safety net; each worker needs isolated artifacts and a worktree because concurrent RPI cycles would collide on `.agents/rpi/` and git locks (`skills/evolve/references/parallel-execution.md:47-86`). @@ -84,7 +84,7 @@ The findings registry already has a stronger dedup contract. It defines `dedup_k | Code-internal repo scan | `mine.Run` over `git`, `agents`, and `code`; counts code hotspots, orphaned research, co-change files, recurring fixes, error events, and gate verdicts | 1 | 2026-04-12, `eeea2b9f` | Runs in INGEST with `DryRun=true` (`cli/internal/overnight/ingest.go:253-312`) | | Code-internal finding router | `.agents/findings/*.md` -> `next-work.jsonl`, deduped by finding ID | 1 | 2026-04-19, `fc42b0aa` | Single `O_APPEND` line plus fsync; NFS append is not atomic (`cli/internal/overnight/findings_router.go:54-68`, `cli/internal/overnight/findings_router.go:83-155`) | | Runtime health | Dream fallback packets from explicit goal, retrieval coverage below threshold, metrics escape velocity false, and escalatable degradation | 4 | 2026-04-14, `a01235a3` | Packet IDs hash source parts with `dream-` prefix (`cli/cmd/ao/overnight_packets.go:258-343`, `cli/cmd/ao/overnight_packets.go:923-938`) | -| Runtime corroboration | Long-haul packet corroboration and council as optional confidence-improvement lanes | 2 | 2026-04-19, `2f6105bc` | Prior post-mortem says cheapest probe before council was the reusable pattern (`.agents/council/2026-04-14-post-mortem-dream-longhaul.md:140-158`) | +| Runtime corroboration | Long-haul packet corroboration and council as optional confidence-improvement lanes | 2 | 2026-04-19, `2f6105bc` | Prior postmortem says cheapest probe before council was the reusable pattern (`.agents/council/2026-04-14-postmortem-dream-longhaul.md:140-158`) | | Local external-ish curator | Ollama/Gemma-backed worker queue and event records | 1 | 2026-04-24, `04964419` | Allowed jobs are knowledge jobs and bounded events; no recursive runner budget-free calls (`skills/dream/SKILL.md:58-74`) | | Web/competitor/dependency scan inside Dream | None found | 0 | N/A | Evolve has deps/perf/test/refactor generators, but Dream has no web/competitor/deps finding-generator today (`skills/evolve/SKILL.md:52-57`) | @@ -100,7 +100,7 @@ At the same time, the repo already treats read/research fanout as useful when ou These are not resolved by this pass and should not be guessed: -- The actual nightly latency and yield distribution of each Dream substage. We have static code and post-mortem evidence, not per-generator timing histograms. +- The actual nightly latency and yield distribution of each Dream substage. We have static code and postmortem evidence, not per-generator timing histograms. - How often current Dream runs are bottlenecked by finding starvation versus execution backlog. The queue has unresolved rows, but the question is marginal yield, not raw queue length. - Whether the user wants external web/competitor/dependency findings to become automatically executable or remain human-review-only. - Whether `status`, `requires`, `goal_weight`, and `dedup_key` should become first-class next-work schema fields or stay advisory metadata. diff --git a/docs/runbooks/autonomy-runtime-cycle-1.md b/docs/runbooks/autonomy-runtime-cycle-1.md deleted file mode 100644 index 5006a5e04..000000000 --- a/docs/runbooks/autonomy-runtime-cycle-1.md +++ /dev/null @@ -1,93 +0,0 @@ -# Autonomy Runtime Cycle-1 Runbook - -**Date:** 2026-05-20 -**Scope:** Safe activation of AgentOps cycle-1 in-session autonomy surfaces: RPI -phased runs and `/evolve` supervisor loops. - -> **3.0 note:** the daemon-backed job-execution lane this runbook originally -> covered (`ao daemon jobs submit`, the `agentopsd` control plane) was **removed** -> in the AgentOps 3.0 rearchitecture — AgentOps is in-session only and ships no -> daemon of its own (see -> [ADR-0009](../adr/ADR-0009-daemon-deletion-in-session-only.md)). The in-session -> loop (`/rpi`, `/evolve`) runs end-to-end in a plain session; to run it -> unattended out of session, dispatch it on the **reference substrate** -> (NTM + MCP + managed-agents) — an NTM swarm (or a lead agent) slings ready -> beads to workers that run `/rpi`. This -> runbook now covers the in-session surfaces only. - -## Activation - -1. Pull latest `main` and sync beads (`git fetch --prune origin && git switch main && git reset --hard origin/main`; then `BEADS_DIR="$(ao beads dir)" br sync` if used in this clone). -2. Run baseline quality gates: - - `cd cli && make build` (produces `cli/bin/ao`) - - `cd cli && go test ./internal/rpi/...` -3. Verify required specs and index references exist: - - `docs/contracts/rpi-run-registry.md` - - `docs/documentation-index.md` references the RPI contracts. -4. Execute the target RPI run in dry/safe mode first and confirm no regressions in run artifacts and the bead ledger: - - `/rpi --dry-run ""` for one explicit run - - `/evolve --dry-run --max-cycles 1 ""` for the supervisor loop surface - - `/evolve --dream-only` for knowledge-only cycles - - `BEADS_DIR="$(ao beads dir)" br show ` to inspect produced artifacts. - -## Feature Flags - -Cycle-1 runtime controls are in-session command flags: - -- `/evolve --supervisor=true` is the default supervised loop posture. -- `/evolve --max-cycles ` bounds autonomous iterations. -- `/evolve --dream-first` / `--dream-only` limits work to knowledge compounding before or instead of code cycles. -- `/rpi --runtime ` and `--runtime-cmd ` select the worker runtime for phased execution. - -Activation rule: - -- Start with `--dry-run` and `--max-cycles 1`. -- For unattended out-of-session runs, dispatch the loop on the reference substrate (NTM + MCP + managed-agents) only after the local RPI and evolve dry runs are clean. -- Keep manual merge/review in the loop until the release-readiness contract says otherwise. - -## Rollback Trigger - -Rollback immediately when any of the following occurs: - -1. RPI run determinism / replay errors appear (deterministic-mode smoke regressions, run-ledger inconsistencies). -2. Quality gate behavior deviates from the expected non-bypassable flow (`scripts/pre-push-gate.sh`, `scripts/ci-local-release.sh`, `ao goals validate`). -3. RPI run artifacts or bead evidence become incomplete for ratchet-relevant steps (`ao ratchet check`, `ao ratchet status`). - -Rollback steps: - -1. Stop using the new opt-in flag / pool input. -2. Re-run with the legacy single-actor path. -3. Capture failing artifacts and create follow-up bead(s) with references (`BEADS_DIR="$(ao beads dir)" br create "..." --body "..."`). - -## Evidence Verification - -Verify lifecycle and orchestration evidence via: - -1. RPI / bead events include the relevant lifecycle markers and payload fields. - - RPI phased state and artifacts live under `.agents/rpi/`. - - Out-of-session job events (when dispatched on the substrate) are inspectable through the substrate's own event surface, not an AgentOps daemon. -2. RPI run ledger / bead store contains attempt records for affected beads (`BEADS_DIR="$(ao beads dir)" br show `). -3. RPI tests pass: - - `cd cli && go test ./internal/rpi/...` -4. Autonomy smoke remains green: - - `cd cli && go test ./cmd/ao/... -run 'Test.*(RPI|Evolve)'` - - `bash scripts/ci-local-release.sh --fast --jobs 4` before promoting the activation. - -## Operator Notes - -- This runbook covers the in-session loop only; out-of-session/fleet orchestration is delegated to the reference substrate (NTM + MCP + managed-agents), not an AgentOps daemon. -- This runbook does not relax validation boundaries (`/vibe`, `/council`, `ao goals validate`, gate scripts all still apply). -- This runbook is cycle-1 only; fleet/autopilot runtime expansion is a follow-on cycle. - -## AgentOps Runtime Surface - -| Concern | AgentOps surface | -|---|---| -| CLI | `ao ` | -| One bounded autonomy run | `/rpi ""` | -| Supervised loop | `/evolve --max-cycles ""` or `/evolve` | -| Work claim | `BEADS_DIR="$(ao beads dir)" br update --claim` | -| Validation | `/vibe`, `/council`, `ao goals validate`, `scripts/ci-local-release.sh` | -| Knowledge extraction | `ao forge` | -| Contracts | `docs/contracts/*` + `docs/documentation-index.md` | -| Runtime packages | `cli/internal/rpi/`, `cli/cmd/ao/` | diff --git a/docs/runbooks/bash-gate-sunset.md b/docs/runbooks/bash-gate-sunset.md deleted file mode 100644 index d1ac3453f..000000000 --- a/docs/runbooks/bash-gate-sunset.md +++ /dev/null @@ -1,69 +0,0 @@ -# Bash-gate sunset criterion (`AGENTOPS_GATE_BASH`) - -> **Bead:** cp-4jac (control-plane) records this criterion. **Execution** of the retirement is -> cp-v8m.6 (control-plane) — "PB2-tail: retire `scripts/pre-push-gate.sh` (2210 LOC) + -> `AGENTOPS_GATE_BASH`". This doc gives the bash gate a death date condition; it does not delete it. - -## Current state (two gates, one default) - -`scripts/hooks/pre-push.local` routes every push through: - -| Route | Trigger | Status | -|---|---|---| -| **Go gate** (`ao gate check --fast`, built from source) | default | the live gate since PB2 (ag-3n71.2) flipped the default | -| **Bash gate** (`scripts/pre-push-gate.sh --fast`) | `AGENTOPS_GATE_BASH=1` | legacy escape hatch, kept for the transition | -| **Audited bypass** | `AGENTOPS_GATE_DISABLED=1` | logged exit for infra failures — see [`beads-failure-recovery.md`](beads-failure-recovery.md) | - -The migration epic was ag-3n71 (agentops bd), superseded and re-homed to **cp-v8m** in control-plane -br (control-plane is the control plane; agentops bd was the wrong tracker for cross-repo -gate-architecture work). - -## The sunset criterion - -The bash gate (`scripts/pre-push-gate.sh` + the `AGENTOPS_GATE_BASH=1` branch in -`scripts/hooks/pre-push.local`) is deleted when **all** of the following hold: - -1. **CI no longer needs its scripts (PB3, cp-v8m.2).** `validate.yml` runs `ao gate check - --full --tier` instead of per-job bash orchestration. Until then the bash gate's backing - scripts are load-bearing in CI even if no one sets the hatch. -2. **Coverage-equivalence stays green with the deferred list empty.** - `TestRegistryCoversBashGateBackingScripts` (`cli/internal/gates/checks/coverage_test.go`) is the - no-strangler net: every bash-gate backing script must be in the Go registry. The test currently - allows an explicit `deferredBacking` exception list (`check-agents-hash-snapshot.sh`); sunset - requires that list drained to empty — equivalence with exceptions is not equivalence. -3. **Bake-time confidence on real pushes.** The Go gate has been the default on real pushes long - enough to trust (cp-v8m.6's "real-push bake-time confidence"). Benchmark: the council - sequencing for the Olympus kernel uses **30 days of gating real closes** - (cp-verdict-gate-family-author-enforcement-icb6 + cp-27gq before - cp-olympus-kernel-v1-7kvz starts) — apply the same bar here: 30 days of the Go gate as default - with no gate-miss incident (a defect the bash gate would have caught and the Go gate did not). -4. **The retirement is recorded in the retirement ledger.** Per the retirement invariant - (cp-retirement-ledger-mjdm: every engine milestone retires bash control loops or the milestone - stops), the deletion must show up as a decrease in the live bash-controller count — the bash - gate's ~2210 LOC is a first-class deletion target, not silent cruft removal. - -**When all four hold, the action is:** delete `scripts/pre-push-gate.sh`, remove the -`AGENTOPS_GATE_BASH` branch from `scripts/hooks/pre-push.local`, keep `docs/agent-workflow-reference.md` aligned -with the Go-gate authority model, and record the deletion in the -retirement ledger. That work executes under **cp-v8m.6**; do not partially retire (a deleted script -with a live hatch is a broken hatch, and a removed hatch with a live script is dead code). - -**Until then:** the hatch stays. Fail-closed — if the Go gate regresses, the documented fallback is -`AGENTOPS_GATE_BASH=1`, never `--no-verify`. - -## Who watches this - -- Tracking bead in this repo's bd: **ag-ltjq** ("bash gate sunset: retire scripts/pre-push-gate.sh - and AGENTOPS_GATE_BASH hatch when criterion holds") — pointer bead filed by cp-4jac; execution - stays in control-plane cp-v8m.6. -- Conformance test: `cd cli && go test ./internal/gates/checks -run TestRegistryCoversBashGateBackingScripts` - -## Cited beads - -| Bead | Tracker | Role | -|---|---|---| -| cp-4jac | control-plane br | this doc (criterion recorded) | -| cp-v8m / cp-v8m.2 / cp-v8m.6 | control-plane br | gate-collapse epic / PB3 CI cutover / the retirement itself | -| ag-3n71 (+ .2) | agentops bd | superseded migration epic; PB2 default-flip history | -| cp-retirement-ledger-mjdm | control-plane br | retirement invariant + live bash-loop count | -| cp-verdict-gate-family-author-enforcement-icb6, cp-27gq, cp-olympus-kernel-v1-7kvz | control-plane br | the 30-day "gated real closes" sequencing the bake-time bar mirrors | diff --git a/docs/runbooks/beads-failure-recovery.md b/docs/runbooks/beads-failure-recovery.md deleted file mode 100644 index 48de44420..000000000 --- a/docs/runbooks/beads-failure-recovery.md +++ /dev/null @@ -1,130 +0,0 @@ -# Runbook: beads (bd) failure recovery - -> **RETIRED / HISTORICAL (as of 2026-06-11).** bd/Dolt is retired; the live tracker is `br` at the private ledger resolved by `ao beads dir` (invoke `BEADS_DIR="$(ao beads dir)" br `), and the ledger syncs via `git -C "$(ao beads dir)" push` — there is no remote Dolt server to recover. This runbook documents bd/Dolt-server failure recovery and is kept for historical reference only — see AGENTS.md and `docs/runbooks/bd-server-mode-closeout.md`. - -> **Bead:** cp-4jac (control-plane). **Scope:** what an agent or operator does when `bd`/beads -> operations fail mid-work in this repo — including the one audited way past a blocked push. -> Failure taxonomy borrowed from control-plane's 4-day post-mortem family (cp-7cko) and the -> divergent-board recovery work (cp-cwiy, `bin/br-reconcile`). - -## Why this exists (the hard pre-push dependency) - -agentops tracks issues in **bd** (Dolt server mode — `.beads/metadata.json` points at the shared -`bushido` Dolt DB on the tailnet). The cockpit pre-push gate is bd-coupled in two places: - -- **check 19b — "bd closeout contract parity"** (`scripts/pre-push-gate.sh`): blocking when it runs. -- **loop-shape check** (Directive 12 posture): warn-only, skips cleanly when bd is missing. - -So a corrupted or unreachable bd can block a push that has nothing wrong with the code. A blocked -push must have a documented exit — that exit is below, and it is **audited, not silent**. - -## The audited bypass (the only sanctioned exit) - -```bash -AGENTOPS_GATE_DISABLED=1 git push -``` - -This is implemented in `scripts/hooks/pre-push.local`: the bypass is **logged** (timestamp, user, -branch, sha) to `$(git rev-parse --git-common-dir)/agentops-gate-bypass.log` and prints a warning. -Rules: - -1. Use it only when the gate failure is caused by infrastructure (bd down, Dolt unreachable), not - by your change. If `go build` or a real check is red, fix the change. -2. After bypassing, **file or update a bead** describing why, and re-run the gate - (`scripts/pre-push-gate.sh --fast` or `ao gate check --fast`) once bd is healthy. -3. Never `git push --no-verify` and never uninstall the hook — both are unaudited and defeat the - mechanism (LAW-3: don't let the gate be self-greened). - -## Triage: is bd actually broken? - -```bash -bd ping # connectivity to the Dolt server (fast) -bd context # which backend/server/database actually resolved -bd doctor # installation + config health, with repair hints -nc -zv 100.105.194.61 3306 # is the bushido Dolt server reachable at all? -ssh bushido systemctl --user status dolt-bd-server # is the server up? -``` - -If `bd ping` is green and `bd context` resolves to the expected server/database, bd is fine — your -problem is elsewhere (look at the actual gate output). - -## Failure modes → detection → recovery → prevention - -### FM1 — Dolt server unreachable (tailnet down, bushido offline) - -- **Symptom:** `bd` commands hang or fail with connection errors; pre-push 19b fails. -- **Detect:** `bd ping` fails; `nc -zv 100.105.194.61 3306` times out. -- **Recover:** - 1. Check the tailnet/host: `ssh bushido systemctl --user status dolt-bd-server`, restart if down. - 2. If the host is genuinely unreachable and the push cannot wait: use the audited bypass above, - then reconcile when the server returns. -- **Prevent:** don't start a push-heavy session without a green `bd ping`; the server is a single - point of failure by design (single authoritative writer — see control-plane cp-7e3j). - -### FM2 — Divergent board (replicas disagree) - -- **Symptom:** bead counts/states differ between checkouts or between the server and a local - export; beads you closed show open elsewhere; duplicate or missing IDs. -- **Detect:** `bd export` from two vantage points and diff; or compare `bd show ` against what - your session believes it did. -- **Recover:** **union + lifecycle-aware rebuild** — the procedure operationalized in control-plane - as `bin/br-reconcile` (cp-cwiy): union all bead IDs across replicas, resolve conflicts - lifecycle-aware (**a closed-with-evidence bead is NEVER lost to a later bare "open" touch** — this - defeats the reopen-eater), keep replica-only beads, then rebuild from the authoritative union. - In this repo's Dolt mode the equivalent is: `bd export` each divergent replica, build the union - with the same lifecycle rule, then `bd import` the union into the authoritative server. Do it on - a quiet board (no concurrent writers). -- **Prevent:** one authoritative writer; never hand-edit ledger files; don't run two sessions - writing the same beads without locks (Agent Mail reservations). - -### FM3 — Orphaned beads created from the wrong branch/worktree - -- **Symptom:** a bead created or closed during branch work is missing after merge — the record - lives in state that never reached the shared board (control-plane lesson: "beads live on the - branch, not the stale checkout"). -- **Detect:** after landing a branch, `bd show ` for every bead the session claims to have - created/closed. Anything missing is orphaned. -- **Recover:** re-create or re-close the bead against the live server from the canonical checkout, - citing the original work (commit shas) in the bead notes. If a JSONL export from the branch - exists, `bd import` the missing lines. -- **Prevent:** in Dolt server mode writes go to the server, not the branch — but **verify** at - Landing-the-Plane time (`docs/agent-workflow-reference.md` session completion): `git push` AND bead state - confirmed on the server are both required before a session ends. - -### FM4 — Merge-eaten closes (the reopen-eater) - -- **Symptom:** a bead that was closed with evidence is open again after a sync/merge; the close - note is gone or the status regressed. -- **Detect:** audit recently-closed beads after any sync/reconcile: closed beads whose `updated` - timestamp moved but whose status regressed to open without a human reopen note. -- **Recover:** re-close with a pointer to the original evidence (commit, evidence file). If many - beads are affected, treat it as FM2 and run the lifecycle-aware union — the union rule exists - precisely so closed-with-evidence beats a later bare open touch (cp-cwiy). -- **Prevent:** never resolve a bead-state conflict by "latest timestamp wins"; lifecycle-aware - resolution only. Don't hand-edit `.beads/` ledger files (control-plane is adding dcg deny rules - for exactly this — cp-06xi). - -### FM5 — Local lock/cache contention or corruption - -- **Symptom:** `bd` reports busy/locked; stale `.beads/.write.lock`; weird local-cache state while - the server is healthy. -- **Detect:** `bd doctor`; `bd ping` green but local commands fail. -- **Recover:** retry first — "busy" is usually daemon lock contention, not corruption (do not - blindly delete locks). If genuinely stale (no live bd process), remove the stale lock and re-run - `bd doctor`. Local caches can be rebuilt from the server; the server is the source of truth. -- **Prevent:** one writer per workspace; let commands finish; don't `kill -9` mid-write. - -## Escalation - -If recovery would require destructive action on the shared Dolt DB (dropping tables, rewriting -history), stop and escalate to the operator — that is a one-way door, and the board serves every -repo wired to the `bushido` DB, not just this one. - -## See also - -- `scripts/hooks/pre-push.local` — the gate hook, the audited bypass, the bash-gate escape hatch. -- [`bash-gate-sunset.md`](bash-gate-sunset.md) — when the legacy bash gate (and its - `AGENTOPS_GATE_BASH=1` hatch) gets deleted. -- control-plane `cp-cwiy` (br-reconcile), `cp-7cko` (SSOT self-correction family) — the failure - taxonomy this runbook instantiates. -- `docs/runbooks/bd-server-mode-closeout.md` — server-mode setup detail. diff --git a/docs/runbooks/nightly-evolution.md b/docs/runbooks/nightly-evolution.md deleted file mode 100644 index 6850c0cfd..000000000 --- a/docs/runbooks/nightly-evolution.md +++ /dev/null @@ -1,277 +0,0 @@ -# Nightly Evolution Runbook - -> **3.0 note:** the daemon-backed handoff this runbook originally described -> (`ao daemon jobs submit`, `agentopsd`, `ao overnight`) was **removed** in the -> AgentOps 3.0 rearchitecture — AgentOps is in-session only and ships no daemon, -> scheduler, or overnight runner of its own (see -> [ADR-0009](../adr/ADR-0009-daemon-deletion-in-session-only.md)). The loop -> (the `/rpi` / `/evolve` / `/dream` skills; the `/rpi` / `/evolve` CLIs were **removed** in 3.0) -> runs in session; to run it -> unattended out of session, dispatch it on the **reference substrate** -> (NTM + MCP + managed-agents): an NTM swarm (or a lead agent) slings ready beads -> to workers that run the `/rpi` skill (the seven-move operating loop); scheduled maintenance runs via a managed-agent -> driver or cron. This runbook is retained for the repo-owned run-contract and -> digest mechanics; treat the orchestration surface as the substrate, not an -> AgentOps daemon. - -This runbook describes the private local nightly automation lane. It is separate -from GitHub Actions. - -## Architecture - -| Surface | Role | -|---------|------| -| GitHub Nightly | Public proof harness over repo-visible state | -| Nightly RPI Brief | Evidence packet and prompt issue | -| Reference substrate (NTM + MCP + managed-agents) | Out-of-session orchestration (swarm + worker agents) for unattended runs | -| `scripts/nightly-evolution.sh` | Repo-owned run contract and digest writer | -| `/dream` skill | Private Dream/wiki knowledge compounding (in session; dispatched out of session via the substrate) | -| `/rpi` / `/evolve` skills | Code-mutating implementation cycles, dispatched as one unit (an agent runs the skill) | -| Claude Code | Headless worker/reviewer via local CLI or GitHub companion action | -| Codex | Headless worker/reviewer via `codex exec` or local AgentOps runtime | -| Mt. Olympus | Sovereign full-custom runtime (keeps its own Rust daemon) — alternate out-of-session driver | - -Bushido is a private dogfood target, not a public AgentOps namespace. Out-of- -session runs are dispatched on the reference substrate (NTM + MCP + managed-agents); AgentOps ships no -scheduler of its own. Mt. Olympus can run the same contract through its sovereign -Rust core once provider readiness and replay are proven. - -## First Safe Run - -Preview the plan and write digest artifacts: - -```bash -scripts/nightly-evolution.sh --emit-systemd -``` - -Run only the private Dream/wiki lane: - -```bash -scripts/nightly-evolution.sh --execute --run-dream -``` - -In 3.0 the Dream lane runs the `/dream` skill in session; to run it unattended, -dispatch it on the substrate (an NTM swarm pane or managed-agent driver). The daemon-backed `dream.run` job submission -this flag historically used was removed with the daemon -([ADR-0009](../adr/ADR-0009-daemon-deletion-in-session-only.md)); the run -contract and digest mechanics below are unaffected. - -Run one bounded evolve cycle with Codex as the runtime command: - -```bash -scripts/nightly-evolution.sh \ - --execute \ - --run-evolve \ - --runtime-cmd codex \ - --runtime-mode direct \ - --max-cycles 1 -``` - -Run both lanes after the dry-run and Dream-only pilot have passed: - -```bash -scripts/nightly-evolution.sh --execute --run-dream --run-evolve --max-cycles 1 -``` - -## Host-OS timing (not an AgentOps scheduler) - -AgentOps ships no scheduler; recurring runs are driven by host-OS timing (a -systemd user timer or cron) calling the repo script, or by a substrate-side -dispatch (a managed-agent driver or cron). The helper below installs a **host systemd user timer** — operator -infrastructure, not an AgentOps-managed surface. - -### Automated Install - -Use the install helper to generate, install, and enable the systemd user timer: - -```bash -# Preview what will be installed -scripts/install-nightly-scheduler.sh --dry-run - -# Install in dry-run mode (safe default — no source mutation) -scripts/install-nightly-scheduler.sh --enable - -# Install in execute mode (runs Dream + Evolve nightly) -scripts/install-nightly-scheduler.sh --execute-mode --enable - -# Check status -scripts/install-nightly-scheduler.sh --status - -# Remove -scripts/install-nightly-scheduler.sh --uninstall -``` - -Options: `--schedule`, `--runners`, `--runtime-cmd`, `--max-cycles`. See -`scripts/install-nightly-scheduler.sh --help` for full reference. (These are -flags of the host-timer install helper, not of any AgentOps CLI command.) - -### Manual Install (alternative) - -Generate systemd user timer templates: - -```bash -scripts/nightly-evolution.sh --emit-systemd -``` - -The generated files are written under the run output directory: - -- `systemd/agentops-nightly-evolution.service` -- `systemd/agentops-nightly-evolution.timer` - -Install manually after reviewing them: - -```bash -mkdir -p ~/.config/systemd/user -cp .agents/nightly///systemd/agentops-nightly-evolution.* ~/.config/systemd/user/ -systemctl --user daemon-reload -systemctl --user enable --now agentops-nightly-evolution.timer -``` - -### Schedule Design - -- Default: `*-*-* 12:15:00 UTC` (daily, after GitHub Nightly settles) -- `RandomizedDelaySec=10m` prevents thundering herd if multiple repos are scheduled -- `Persistent=true` catches up on missed runs after sleep/reboot -- Timer is under `timers.target`, NOT `pipeline.target` (agentops-specific, not pipeline infra) -- Kill switches (`STOP`, `KILL` files) are checked both via systemd `ConditionPathExists` - and in the `ExecStartPre` for defense in depth -- `TimeoutStartSec=3600` (1h) prevents runaway evolve cycles from blocking the timer - -## Vendor Policy - -Use Claude and Codex differently until eval evidence says mixed mode is stable: - -- Dream handoff: run the `/dream` skill in session; dispatch it out of session - on the substrate (no AgentOps daemon — see - [ADR-0009](../adr/ADR-0009-daemon-deletion-in-session-only.md)). The run - contract still honors the configured Claude/Codex runner list. -- Planning/review: prefer Claude or mixed mode for synthesis-heavy work. -- Implementation: use Codex when local shell/code execution is the primary - burden. -- CI/GitHub companion: use Claude Code GitHub Actions for repo-visible reviews - or reports, not private `.agents` mutation. -- Substrate: dispatch whole `/rpi` / `/evolve` skill loops as one unit (an agent runs the skill) - only after provider readiness and replay are proven. - -## Safety Controls - -- Dry-run is the default. -- `--execute` is required before any phase runs. -- `--run-dream` and `--run-evolve` are separate opt-ins. -- A lock directory prevents overlapping runs. -- These kill switches stop the wrapper: - - `.agents/evolve/STOP` - - `.agents/rpi/KILL` - - `~/.config/evolve/KILL` -- `bushido-box ai-sane` must pass in execute mode unless - `--no-require-ai-sane` is supplied. - -## L3 Rehearsal — First No-Merge Local Pilot - -Before enabling recurring scheduled runs, prove the full execute+evolve -path with a mocked work order that cannot merge. - -### Step 1: Dry-run - -```bash -scripts/nightly-evolution.sh -``` - -Verify `digest.json` is written and `admission_context` is populated. - -### Step 2: Dream-only execute - -```bash -scripts/nightly-evolution.sh --execute --run-dream -``` - -Verify Dream submits (or falls back) without requiring a work order. - -### Step 3: Execute+evolve blocked by admission - -Create an expired work order to confirm the preflight refuses: - -```bash -jq -n '{ - schema_version: 1, work_order_id: "rehearsal-blocked", - generated_at: "2020-01-01T00:00:00Z", expires_at: "2020-01-01T01:00:00Z", - base_sha: "abcdef1", target: {type:"goal",id:"t",summary:"T"}, - allowed_files: ["scripts/nightly-evolution.sh"], - validation_commands: ["echo ok"], landing_policy: "off", - digest_policy: "required", open_pr_blockers: [], - main_ci_baseline: {status:"green",checked_at:"2020-01-01T00:00:00Z",failed_jobs:[]} -}' > /tmp/rehearsal-blocked.json - -scripts/nightly-evolution.sh \ - --execute --run-evolve \ - --work-order /tmp/rehearsal-blocked.json -# Expected: non-zero exit, "work order expired" -``` - -### Step 4: Execute+evolve admitted (no-merge) - -Create a valid work order and run with `--landing-policy off`: - -```bash -jq -n --arg sha "$(git rev-parse HEAD)" \ - --arg ea "$(date -u -d '+1 hour' +%Y-%m-%dT%H:%M:%SZ)" \ - --arg ga "$(date -u +%Y-%m-%dT%H:%M:%SZ)" '{ - schema_version: 1, work_order_id: "rehearsal-admitted", - generated_at: $ga, expires_at: $ea, base_sha: $sha, - target: {type:"goal",id:"rehearsal",summary:"L3 rehearsal"}, - allowed_files: ["scripts/nightly-evolution.sh"], - validation_commands: ["echo ok"], landing_policy: "off", - digest_policy: "required", open_pr_blockers: [], - main_ci_baseline: {status:"green",checked_at:$ga,failed_jobs:[]} -}' > /tmp/rehearsal-admitted.json - -scripts/nightly-evolution.sh \ - --execute --run-evolve \ - --work-order /tmp/rehearsal-admitted.json \ - --landing-policy off \ - --runtime-cmd claude \ - --max-cycles 1 -# Expected: exit 0, digest records admitted run -``` - -### Step 5: Review digest - -```bash -cat .agents/nightly/$(date -u +%F)/*/digest.md -cat .agents/nightly/$(date -u +%F)/*/pr-body.md -``` - -Verify the digest includes the admission context table, stop reasons (step -3), and admitted verdict (step 4). - -The pilot is not recurring until this rehearsal passes all five steps. - -### BATS validation - -Run the full scenario suite: - -```bash -bats tests/scripts/nightly-evolution.bats -``` - -Scenario fixtures for the blocked and admitted rehearsals are at -`tests/scenarios/nightly-evolution/auto-nightly-evolution-l3-rehearsal-*.json`. - -## Outputs - -Each run writes: - -- `digest.json` -- `digest.md` -- `ai-sane.json` -- `dream-setup.json` -- `runtime-inventory.tsv` -- `open-prs.json` -- `blocker-matrix.json` -- `main-ci-baseline.json` -- optional `nightly-brief/` -- optional `systemd/` -- optional `dream-run-payload.json`, `dream-submit.json`, and - `dream-submit.stderr` -- optional `dream.log` when legacy Dream subprocess fallback is used -- optional `evolve.log` diff --git a/docs/runbooks/pr-creation-from-linked-worktrees.md b/docs/runbooks/pr-creation-from-linked-worktrees.md index ada8b5654..3e2c76dd6 100644 --- a/docs/runbooks/pr-creation-from-linked-worktrees.md +++ b/docs/runbooks/pr-creation-from-linked-worktrees.md @@ -140,4 +140,5 @@ main --draft`). ## See also - `skills/swarm/references/shared-checkout-discipline.md` — when a worktree is required. -- `skills/push/SKILL.md` — the test→commit→push half (stops before PR creation; this runbook covers the rest). +- The repository's own contribution and CI documentation for checks required + before opening a PR. AgentOps does not own the push or PR transition. diff --git a/docs/runbooks/release-process.md b/docs/runbooks/release-process.md index 81cc5c0b9..f26667a72 100644 --- a/docs/runbooks/release-process.md +++ b/docs/runbooks/release-process.md @@ -2,7 +2,7 @@ How to cut a release of `ao` (the AgentOps CLI). -> See also: [`RELEASING.md`](../RELEASING.md) for the canonical AgentOps release doc and [`release-e2e-checklist.md`](../release-e2e-checklist.md) for the full local gate sequence. This runbook is the operator checklist view of those release contracts. +> See [`RELEASING.md`](../RELEASING.md) for the canonical repository release policy. This runbook is an operator checklist for that external delivery process, not part of the AgentOps semantic loop. ## Prerequisites @@ -15,8 +15,8 @@ How to cut a release of `ao` (the AgentOps CLI). All core gates must pass before tagging: ```bash -# Smart, diff-aware gate (recommended) -scripts/pre-push-gate.sh --fast +# Diff-aware deterministic checks (recommended during iteration) +ao gate check --fast --scope worktree # Full local release validation gate (mandatory before tagging) scripts/ci-local-release.sh @@ -41,8 +41,7 @@ AgentOps release gate semantics: - `go-test`: hard gate on full test suite within a 300s budget. - `ci-local-release`: full local release validation, including doc, shell, contract, CLI, and release-surface checks. -- `pre-push-fast`: smart changed-file gate for PR iteration before the full release gate. -- `release-e2e`: optional HIL/SIL/VIL release smoke path from `docs/release-e2e-checklist.md`. +- `ao gate check --fast`: smart changed-file checks for iteration before the full release gate. ### Dogfood Hardening Pack @@ -50,7 +49,7 @@ AgentOps dogfood means running `ao` against this repo's own `.agents/` state and - Build a local `ao` binary from repo source (`cd cli && make build`). - Run `scripts/ci-local-release.sh` before tagging. -- For explicit release smoke coverage, use `bash scripts/ci-local-release.sh --fast --jobs 4` and the HIL/SIL/VIL targets in `docs/release-e2e-checklist.md`. +- For explicit release smoke coverage, use `bash scripts/ci-local-release.sh --fast --jobs 4` before the full release gate. - Bound any manual smoke command with `timeout` so model or daemon dependencies cannot hang a release shell. ## Version Bump @@ -123,12 +122,11 @@ curl -sL https://github.com/boshu2/agentops/releases/download/vX.Y.Z/ao_X.Y.Z_da | Local build | `cd cli && make build` (or `cd cli && go build -o bin/ao ./cmd/ao/`) | | Local install | `cd cli && make install` (installs to `~/go/bin/ao`) | | Tests | `cd cli && make test` (or `cd cli && go test ./... -count=1`) | -| Smart pre-push gate | `scripts/pre-push-gate.sh --fast` | -| Full pre-push gate | `scripts/pre-push-gate.sh` | +| Changed-surface checks | `ao gate check --fast --scope worktree` | +| Full deterministic registry | `ao gate check --full` | | Local release gate | `scripts/ci-local-release.sh` | | Dogfood quick | `bash scripts/ci-local-release.sh --fast --jobs 4` | | Dogfood full | `scripts/ci-local-release.sh` | -| Throughput gate | `docs/release-e2e-checklist.md` HIL/SIL/VIL smoke path | | Release (full) | `goreleaser release --clean` | | Release (dry run) | `goreleaser release --clean --snapshot --skip=publish` | | Retag | `scripts/retag-release.sh vX.Y.Z` | @@ -140,7 +138,7 @@ curl -sL https://github.com/boshu2/agentops/releases/download/vX.Y.Z/ao_X.Y.Z_da | Binary | `ao` | | CLI source | `cli/cmd/ao/` | | Build/test targets | `cd cli && make build` / `make install` / `make test` | -| Smart commit gate | `scripts/pre-push-gate.sh --fast` | +| Changed-surface checks | `ao gate check --fast --scope worktree` | | Full release gate | `scripts/ci-local-release.sh` | | Release artifact prefix | `ao_X.Y.Z_*` | | Release repo path | `boshu2/agentops` | diff --git a/docs/scale-without-swarms.md b/docs/scale-without-swarms.md index 37a745c71..ff1b20880 100644 --- a/docs/scale-without-swarms.md +++ b/docs/scale-without-swarms.md @@ -1,139 +1,26 @@ -# Scale Without Swarms +# Bounded Work Without Lifecycle Control -> 3-5 focused agents with fresh context beat 60 stale ones every time. +AgentOps deliberately keeps its core experiment small: -The hot take in agent orchestration is scale: more agents, faster delivery. Claude-Flow and similar systems advertise 60, 80, even 100+ simultaneous agents as a feature. AgentOps takes the opposite bet. Here's why. - ---- - -## The Problem With Agent Swarms - -Massive swarms sound compelling until you watch them in production. - -**Context pollution.** Each agent in a large swarm accumulates context from prior tasks, prior failures, prior teammates' decisions. By the time agent-47 starts its task, its context window is half-full of irrelevant history. Output quality degrades proportionally. - -**Merge conflicts by construction.** 60 agents writing to the same codebase without coordination produce 60 conflicting branches. The integration tax — resolving conflicts, re-running tests, re-validating — often exceeds the work cost. You've parallelized the easy part and serialized the hard part. - -**Redundant work.** Without dependency mapping, agents discover the same shared function needs changing and all edit it independently. 10 agents fix the same bug 10 ways. The last one to commit wins; the other 9 cycles are waste. - -**No regression gates.** Speed-optimized swarms skip validation to maximize throughput. A gate that blocks one wave from starting until the prior wave passes would halve their advertised parallelism numbers. So they skip it — and ship regressions. - -**The stale oracle problem.** A 60-agent swarm means 60 context windows that haven't seen what the other 59 just did. By cycle 10, every agent is operating on a stale mental model of the codebase. Decisions made on stale context produce fragile code. - ---- - -## The AgentOps Model - -AgentOps bets on quality per agent over count of agents. - -The core insight: **agent output quality is a function of context quality.** Context quality degrades with size, staleness, and irrelevance. The solution is not more agents — it's tighter context control per agent. - -The model has three components: - -**Isolation:** Each worker gets fresh context for exactly its task. Nothing from prior waves bleeds in. Workers communicate through the filesystem, not accumulated chat history. - -**Waves:** Work is dependency-mapped upfront. Wave 1 runs in parallel; when it passes gates, Wave 2 starts. Parallelism where it's safe; sequencing where it's required. The plan determines this, not the operator. - -**Gates:** Every wave completes a regression check before the next wave begins. A wave that introduces regressions doesn't proceed — it fails, the operator sees why, and the system stops rather than compounding the failure. - -The result: 3-5 workers per wave, fresh context, gated progress. Not 60 workers racing to completion with no safety net. - ---- - -## Ralph Wiggum Pattern - -The core isolation mechanism is the [Ralph Wiggum Pattern](https://ghuntley.com/ralph/): each execution unit starts fresh, as if it has no memory of what came before. - -Named after Ralph Wiggum's cheerful cluelessness — each worker starts fresh with no memory of previous workers. This sounds like a weakness. It's the mechanism that makes everything else work. - -**Fresh context per worker.** Workers don't inherit stale context. They get a precise context bundle: their task, the relevant code, injected learnings from the knowledge flywheel, nothing else. - -**Disk-backed state.** State that must survive between cycles lives on disk in `.agents/` — not in LLM memory, not in accumulated chat context that gets compacted away. The cycle state is always recoverable. A worker failing or a context compaction event doesn't lose progress. - -**Knowledge flywheel.** What workers learn *does* persist — but through a curated pipeline. Session-end hooks mine the transcript for learnings, score them (specificity, actionability, novelty, confidence), and write them to the flywheel. The next wave gets those learnings injected at start. Cycle 50 knows what cycle 1 learned the hard way. But it knows it cleanly, scored by freshness, not as accumulated chat noise. - ---- - -## Wave Execution - -`/plan` decomposes a goal into issues and maps their dependencies. The result is a wave structure: which issues can run in parallel, which must wait on others. - -`/crank` executes that structure: - -``` -Wave 1: [issue-1, issue-2, issue-3] → parallel → gate -Wave 2: [issue-4, issue-5] → parallel → gate -Wave 3: [issue-6] → serial → gate +```text +RPI -> Plan -> Implement -> fresh Validate -> durable verdict -> report and stop ``` -**Parallel within waves.** Issues in the same wave have no dependencies between them — they can safely run concurrently. Workers don't step on each other. +Plan names one active behavior and its write scope. Implement performs one +bounded experiment. Validate independently judges the exact content described +by the candidate manifest. RPI dispatches each phase once and reports the +result without deciding what happens next. -**Sequential between waves.** Wave N+1 starts only after Wave N passes its gate. Dependencies are respected automatically; no manual coordination required. +Fresh context is valuable because the author cannot turn its own claim into the +binding semantic verdict. More agents are not automatically better. One fresh +validator is the default; Council, Dueling Idea Genies, Swarm, NTM, Agent Mail, +and other runtimes are optional caller-selected strategies or adapters. -**Automatic dependency resolution.** The operator specifies the goal, not the execution order. `/plan` derives the order from declared dependencies. The system handles coordination; the operator handles the roadmap. +Optional dispatch may execute explicit disjoint packets once. It does not infer +a queue, claim work, retry failures, integrate branches, validate candidates, +or deliver results. The caller or an external software factory owns those +decisions. ---- - -## Worktree Isolation - -Each worker runs in its own git worktree — a clean checkout of the current HEAD, separate from every other worker's filesystem. - -The effect: no merge conflicts during execution. Workers write to isolated filesystems. The lead reviews their output and commits. Conflicts, if any, are resolved at the lead-commit step — not scattered across 60 concurrent branches. - -This is not defensive programming. It's the construction of the system: **parallelism is safe because shared state doesn't exist during worker execution.** - -No shared mutable state + fresh context = parallel execution that scales without coordination overhead. - ---- - -## Regression Gates - -Every wave is gated. No wave proceeds until the prior wave passes. - -The gate mechanism: - -1. Worker completes task, writes output to `.agents/` -2. Lead validates output: does it pass the wave's acceptance criteria? -3. Fitness snapshot taken: do all GOALS.yaml checks still pass? -4. If yes: commit, proceed to next wave -5. If no: stop, surface the failure, do not proceed - -This is not advisory. The gate is hard. A regression that would have been caught at step 3 doesn't get buried under 5 more waves of changes. It's visible, isolated, and fixable before it becomes a multi-wave debugging exercise. - -`/evolve` extends this to cycle-level: every cycle's fitness score is written to `cycle-history.jsonl`. A cycle that regresses a previously-passing goal auto-reverts and halts. The floor can never drop. - ---- - -## The Numbers - -One `/evolve` run on this repo: **116 cycles, ~7 hours, unattended, zero regressions.** - -What it shipped: -- Test coverage: ~85% → ~97% across 203 files -- Complex functions (cyclomatic complexity >= 8): dozens → zero -- Modern Go idioms: sentinel errors, exhaustive switches, Go 1.26-compatible slices/cmp.Or/range-over-int -- 132 commits, each traceable, each regression-gated - -No human intervention during the run. Every cycle picked the worst remaining gap by weight, ran `/rpi` to fix it, validated nothing regressed, extracted learnings, and looped. - -Compare to a 60-agent swarm running for 7 hours without gates: you'd have parallel branches, unresolved conflicts, and an unknown regression surface to debug in the morning. The overnight run with gates produces a codebase you trust. The overnight swarm produces a codebase you have to audit. - ---- - -## When to Scale - -Worker count is a tunable, not a maximization target. - -**Start with 1 worker per issue in a wave.** Dependencies drive wave structure. Let the plan tell you how many workers a wave needs, not the other way around. - -**3-5 workers per wave is typical.** Enough parallelism to make waves fast; small enough that the lead's validation step stays tractable. - -**Swarm mode for embarrassingly parallel work.** `/swarm` is the right tool when tasks have no dependencies — research across multiple domains, brainstorming approaches, independent file analysis. Swarm is not the default; it's the tool for specific shapes of work. - -**Sequential for high-risk changes.** When changes are cross-cutting, when the codebase is unfamiliar, when the gate needs to be tight — run sequential waves, even waves of one. The regression gate catches problems before they compound. Speed isn't the constraint; confidence is. - -**The answer to "should I use more agents?" is almost always "tighten the context and gates first."** A well-constrained plan with 4 workers and hard gates will outperform an unconstrained swarm of 40 every time. - ---- - -*See also: [PRODUCT.md](https://github.com/boshu2/agentops/blob/main/PRODUCT.md) — Orchestration at Scale value proposition, Roadmap* +Git worktrees, trackers, CI systems, merge queues, and release pipelines may be +useful around an AgentOps experiment. They remain the caller's infrastructure, +not AgentOps lifecycle state. diff --git a/docs/seed-definition.md b/docs/seed-definition.md index 940fdba91..3711f8d96 100644 --- a/docs/seed-definition.md +++ b/docs/seed-definition.md @@ -1,195 +1,19 @@ -# The Seed +# The AgentOps Seed -> The product is not a pile of skills. The product is the minimal set of starting conditions that, planted in any repo with an LLM, evolves toward whatever that repo's goals are. +The product seed is the smallest useful trust boundary for agent-created work: -## The Metaphor +1. Plan expresses one active behavior, acceptance examples, non-goals, evidence, + and write scope. +2. Implement performs one bounded experiment and reports a CandidatePacket. +3. Validate independently judges the exact content manifest once. +4. The durable `verdict.v2` artifact records what was and was not proven. -DNA. The same genetic machinery -- the same 4 nucleotides, the same ribosomal translation, the same replication fork -- produces bacteria and blue whales. The difference is not the machinery. The difference is the fitness landscape. +RPI composes those phases exactly once and reports the result. A repository may +add its own tracker, Git workflow, CI, release policy, factory runtime, or +learning process around that seed. None is required for AgentOps correctness. -AgentOps is the same. The same core seed, planted in a Go CLI repo or a Python web app or a Kubernetes operator, produces different systems. The goals define the fitness landscape. The seed provides the machinery. Evolution does the rest. +The seed works in a non-Git directory without the `ao` binary. Its only durable +core state is the caller's packets and the content-addressed verdict. -## The Core Seed - -``` -1. GOALS.md -- what to optimize toward -2. .agents/ -- where knowledge accumulates -3. Lifecycle stages -- explicit, hookless rules that run the flywheel -4. CLAUDE.md section -- instructions that start the flywheel -5. Core skills -- the capabilities the agent can invoke -6. Bootstrap learning -- the first turn of the flywheel -``` - -Apply or repair the core seed with the golden path: - -```bash -ao quick-start -# alias: ao quickstart -``` - -Optional layers are deliberately separate: `/bootstrap` adds PRODUCT.md, -README.md, and PROGRAM.md/AUTODEV.md; `BEADS_DIR="$(ao beads dir)" br init --prefix ` adds -tracking; the `hooks-authoring` skill lets you author your own optional runtime -hooks (AgentOps 3.0 ships none); an out-of-session substrate (NTM + MCP + -managed-agents) adds continuous scheduling. - -### 1. GOALS.md -- Fitness Specification - -A file in the repo root with 2-3 directives and 5-7 gates. Each gate has a shell command that exits 0 (pass) or non-zero (fail). No soft measurement. No subjective scoring. Hard constraints only. - -```markdown -## Directives -### 1. Increase test coverage -### 2. Reduce complexity hotspots - -## Gates -| ID | Check | Weight | Description | -|----|-------|--------|-------------| -| tests-pass | cd src && make test | 8 | All tests pass | -| coverage-floor | ./scripts/check-coverage.sh --min=70 | 6 | Coverage above 70% | -| lint-clean | cd src && make lint | 5 | No lint violations | -``` - -**Why it exists:** Goals are Meadows #3 -- system intent. Without goals, `/evolve` has nothing to measure, severity-based selection has nothing to sort, and regression gates have nothing to protect. Goals make the system directional instead of random. - -**Meadows mapping:** #3 (goals), #8 (balancing feedback via regression gates that protect passing goals). - -### 2. `.agents/` Directory -- Knowledge Flywheel Structure - -An append-only ledger with cache-like semantics. Nothing gets overwritten. Every learning, council verdict, pattern, and decision is a new dated file. Freshness decay prunes what is stale. Git-ignored by default (local knowledge, not source code). - -``` -.agents/ - learnings/ -- extracted lessons (gold/silver/bronze tiers) - patterns/ -- reusable solutions - retros/ -- retrospective summaries - council/ -- validation verdicts - research/ -- exploration findings - plans/ -- decomposed epics - ao/ -- session index, provenance, metrics -``` - -**Why it exists:** The flywheel needs a place to write. Without `.agents/`, `ao forge` has nowhere to put learnings, `ao lookup` has nothing to retrieve, and knowledge dies with each session. This is the physical `K` stock from the equation. - -**Meadows mapping:** #10 (material stocks), #7 (reinforcing feedback loop -- more knowledge enables better retrieval enables more knowledge). - -### 3. Lifecycle Stages -- Explicit, Hookless Rules - -AgentOps 3.0 ships **zero hooks**. The same lifecycle stages run through explicit -`ao` commands instead of hook side effects, and the **local pre-push Go gate -(`ao gate check`) is the routine release authority** for structural enforcement -(`validate.yml` is a tag/PR/manual backstop). The minimum viable set: - -| Stage | Command | What it does | -|-------|---------|--------------| -| Session start | `ao inject` | Load context signpost (`.agents/AGENTS.md`), assemble JIT context | -| Session end | `ao forge transcript` | Extract learnings, expire stale artifacts | -| Close-loop | `ao flywheel close-loop` | Close the feedback loop and capture citations | - -**Why it exists:** the structural rule (Meadows #5) is now the local pre-push Go -gate (`ao gate check`), not a local hook. Knowledge extraction is wired into the -skills (`/evolve`, `/post-mortem`, the operating loop) that call these commands -directly, and the pre-push gate enforces the ratchet on push (`validate.yml` is a -tag/PR/manual backstop). The `/rpi` skill remains, but the `ao rpi` command was removed -in 3.0 (the RPI engine is gone) — the seven-move operating loop is now the primary -path. If you want a bounded local gate of your own, author it with the -`hooks-authoring` skill — AgentOps ships none. - -**Meadows mapping:** #5 (rules), #6 (information flows -- the explicit commands and the pre-push gate ensure knowledge moves from session output to persistent storage to next session input). - -### 4. CLAUDE.md Seed Section -- Flywheel Bootstrap Instructions - -Two lines added to the repo's CLAUDE.md: - -```markdown -## Knowledge Flywheel -See `.agents/AGENTS.md` for orientation. Run `ao lookup --query "topic"` when you need prior knowledge. Run `ao forge` at session end. -``` - -**Why it exists:** The explicit lifecycle rules (`ao` commands) and the local pre-push Go gate handle enforcement, but CLAUDE.md provides the explanation so the agent understands *why* the flywheel matters and runs the stages even without any optional hook configured. This is belt-and-suspenders: mechanical enforcement (the pre-push gate + explicit commands) plus cognitive priming (instructions). - -**Meadows mapping:** #6 (information flows -- ensures the agent is aware of the flywheel even if hooks are not installed). - -### 5. Core Skills -- Agent Capabilities - -Four skills installed globally, available in any repo: - -| Skill | Role | Meadows level | -|-------|------|---------------| -| `/evolve` | Measure goals, fix worst gap, validate, learn, repeat | #4 (self-organization) | -| `/research` | Explore codebase, surface findings, inject prior knowledge | #7 (reinforcing feedback) | -| `/implement` | Full lifecycle for one task: plan, build, validate, learn | #8 (balancing feedback) | -| `/validate` | Code quality review with multi-model council | #8 (balancing feedback) | - -**Why it exists:** The seed needs agency. GOALS.md defines intent, `.agents/` stores knowledge, the local pre-push Go gate + explicit lifecycle rules enforce -- but without skills, the agent has no structured way to act on goals, validate work, or extract learnings. Skills are the verbs that operate on the nouns. - -**Meadows mapping:** #4 (self-organization -- `/evolve` changes the system's own rules based on measured fitness). - -### 6. Bootstrap Learning -- First Turn of the Flywheel - -One learning file created at seed time: - -``` -.agents/learnings/YYYY-MM-DD-seed-bootstrap.md ---- -type: decision -confidence: high -tags: [seed, bootstrap] ---- -This repo was seeded on DATE with goals: GOAL_1, GOAL_2, GOAL_3. -Initial state: SUMMARY. Run /evolve to begin improvement. -``` - -**Why it exists:** A flywheel with zero learnings is a flywheel that has never turned. The bootstrap learning ensures `ao lookup` has something to retrieve on the very first session. It primes the reinforcing loop (Meadows #7) so the system starts compounding immediately instead of running one empty cycle first. - -**Meadows mapping:** #7 (reinforcing feedback -- the initial push that starts the flywheel turning). - ---- - -## The Fractal Property - -The same pattern operates at every scale: - -``` -attempt -> validate -> learn -> constrain -``` - -| Scale | Attempt | Validate | Learn | Constrain | -|-------|---------|----------|-------|-----------| -| Single function | Write code | Run tests | Extract pattern | Add test | -| Single issue | `/implement` | `/vibe` | `/retro` | Close issue | -| Epic | `/crank` (waves) | Council consensus | `/post-mortem` | Regression gate | -| Repository | `/evolve` (cycles) | Goal measurement | Learning extraction | Constraint compiler | - -Each level treats the one below as a black box. Each level produces the same outputs: validated work + extracted knowledge + tighter constraints. The seed does not prescribe which level you operate at. It provides the machinery. The fitness landscape determines the scale. - ---- - -## Why Exactly 6 Elements - -| Element | Without it | The system is... | -|---------|-----------|-----------------| -| GOALS.md | No fitness function | Random (no direction) | -| `.agents/` | No knowledge storage | Memoryless (no compounding) | -| Lifecycle stages (hookless `ao` commands + CI gate) | No structural enforcement | Fragile (depends on agent memory) | -| CLAUDE.md section | No cognitive priming | Opaque (agent does not understand why) | -| Core skills | No structured agency | Passive (cannot act on goals) | -| Bootstrap learning | No initial flywheel state | Cold-start (first cycle is empty) | - -Remove any one element and the system degrades to a qualitatively different thing. Add more elements and you are designing a specific system rather than providing starting conditions for an evolved one. The constraint is intentional: the seed must be small enough to fit on one screen, complete enough to produce emergence. - ---- - -## What the Seed Is Not - -- **Not a template.** Templates produce identical copies. Seeds produce different systems depending on goals. -- **Not a framework.** Frameworks constrain what you can build. The seed constrains how the system learns, not what it builds. -- **Not configuration.** Configuration tunes parameters (Meadows #12). The seed operates at the level of rules (#5), self-organization (#4), and goals (#3). - ---- - -## See Also - -- [strategic-direction.md](strategic-direction.md) -- consolidation decision, Meadows mapping, paradigm shifts -- [the-science.md](the-science.md) -- the dK/dt equation and escape velocity condition -- [how-it-works.md](how-it-works.md) -- operational mechanics (ratchet, flywheel) -- [ARCHITECTURE.md](ARCHITECTURE.md) -- the 5 architectural pillars +See [the operating loop](architecture/operating-loop.md) and +[product boundary](../PRODUCT.md). diff --git a/docs/skills-decision-tree.md b/docs/skills-decision-tree.md deleted file mode 100644 index 5f36e25d8..000000000 --- a/docs/skills-decision-tree.md +++ /dev/null @@ -1,75 +0,0 @@ -# Skills Decision Tree - -> Single source of truth for "which skill do I need next?" -> Linked from `skills/curate/SKILL.md`, `skills/compile/SKILL.md`, -> `skills/operationalize/SKILL.md`, and their `skills-codex/` mirrors. - -## Decision-Tree Naming Convention - -Decision-tree docs follow `{subject}-decision-tree.md` (e.g., `skills-decision-tree.md`, `workflow-decision-tree.md`). - -Rationale: groups by subject alphabetically in file listings; matches the existing `docs/skills-decision-tree.md`; keeps `decision-tree` as a suffix tag rather than a noisy prefix duplicated across many files. - -## Global corpus flow (new users with `~/.agents/`) - -1. **`$curate --mode=harvest`** — gather artifacts from many `.agents/` - directories across your rigs, deduplicate cross-rig, promote - high-value items into `~/.agents/learnings/`. Not a verbatim copy — an - opinionated promotion of the unique, high-confidence artifacts. -2. **`$compile`** — synthesize the raw corpus into an interlinked - wiki at `.agents/compiled/`. Large corpora are split into - batches via `--batch-size` so a 2000+ file delta never lands in - a single LLM prompt. -3. _(optional)_ **`$dream`** — overnight bounded compounding loop - on top of the compiled corpus. Not interactive; runs to - convergence or wall-clock, whichever comes first. -4. **`$inject`** — lift compiled knowledge into - playbooks, a belief book, and runtime briefings that future - sessions read at bootstrap. - -## Which skill do I need? - -| I want to… | Use | -|------------|-----| -| Consolidate artifacts from many repos into one place | `$curate --mode=harvest` (writes `~/.agents/learnings/`) | -| Synthesize the raw corpus into an interlinked wiki | `$compile` (writes `.agents/compiled/`) | -| Overnight compounding + fitness-driven corpus improvement | `$dream` | -| Turn compiled knowledge into playbooks + beliefs for future sessions | `$inject` | -| Copy raw `.md` files verbatim without dedup | `rsync` (not AgentOps) | -| New project / new repo / first-time AgentOps setup | `ao quick-start`, then `$quickstart` | -| Full research → plan → implement → validate cycle | `$rpi` | -| Validate a plan or spec before implementation | `$pre-mortem` | -| Validate code quality after implementation | `$vibe` | - -## Common "wait, which one?" disambiguations - -**harvest vs compile.** Harvest moves artifacts between directories -(rig `.agents/` → global hub). Compile synthesizes artifacts into -higher-order output (wiki articles). Harvest is a physical operation; -compile is a semantic operation. - -**~/.agents vs ~/.agents/learnings/.** Users often say "harvest all -to `~/.agents`" and mean the promotion hub. The promotion hub is the -`learnings/` subdirectory, which is why the harvest CLI emits -`--promote-to ~/.agents/learnings`. The outer `~/.agents/` directory -also contains `compiled/`, `playbooks/`, `packets/`, `knowledge/`, -`harvest/`, `mine/`, and `defrag/` — each owned by a different skill. - -**compile vs inject (activation).** Compile builds the wiki. -`inject` (the folded knowledge-activation surface) turns the wiki into -usable operator context (beliefs, playbooks, briefings). Run compile -first, then activation. Running activation against an empty compiled -dir is a no-op. - -**compile vs dream.** Compile is interactive and bounded. Dream is -overnight and runs a compounding loop (harvest → compile → lint → -defrag → repeat until fitness plateaus). If you're sitting at the -terminal, use compile. If you're going to bed, use dream. - -## See also - -- `skills/curate/SKILL.md` — full harvest invocation (`--mode=harvest`) -- `skills/compile/SKILL.md` — compile flags and runtimes -- `skills/operationalize/SKILL.md` — activation surfaces (absorbed from the retired inject / knowledge-activation) -- `skills/dream/SKILL.md` — overnight compounding -- `skills/status/SKILL.md` — first-time setup (folded `quickstart`) diff --git a/docs/skills-matrix.md b/docs/skills-matrix.md deleted file mode 100644 index f5e4b2621..000000000 --- a/docs/skills-matrix.md +++ /dev/null @@ -1,144 +0,0 @@ -# Skills Matrix - -> Every AgentOps skill placed on the **operating loop** from intent to validated -> code. Skills are the product front door; this matrix is the map. -> -> **Read first:** [Intent → Validated Code](architecture/intent-to-validated-code.md). -> **Discipline:** [Operating Loop](architecture/operating-loop.md). -> **Router ("what do I run?"):** [SKILLS.md](SKILLS.md). -> **Tiers (editorial taxonomy):** [SKILL-TIERS.md](../skills/SKILL-TIERS.md). -> -> Inventory count comes from `registry.json` (generated from `skills/**/SKILL.md`). -> Do not hard-code totals in prose; when you add/retire a skill, update this -> matrix in the same change. - -## How to read the matrix - -| Column | Meaning | -|--------|---------| -| **P** | Primary — this move's default skill(s) | -| **S** | Supporting — usually run with or just before/after P | -| **O** | Optional / escalate — only when stakes or scale require it | -| **—** | Not this move's concern | - -Membrane skills (**P** on move 6) still require a behavior contract from moves -1–4. Validating without Gherkin/ATDD is taste, not acceptance. - ---- - -## Loop spine matrix (moves 1–7) - -| Skill | 1 Shape | 2 Track | 3 Slice | 4 TDD | 5 Wave | 6 Membrane | 7 Ratchet | Notes | -|-------|:-------:|:-------:|:-------:|:-----:|:------:|:----------:|:---------:|-------| -| **discovery** | P | S | S | — | — | — | — | Dense execution packet; ideate→research→plan handoff | -| **product** | P | — | — | — | — | — | — | PRODUCT.md / positioning intent | -| **goal-design** | S | S | — | — | — | — | — | Checked goal-design packets before discovery/plan | -| **goals** | S | — | — | — | — | O | S | Setpoint / measure; not day-one build path | -| **idea-genie** | S | — | — | — | — | — | — | Opportunity portfolio → discovery | -| **dueling-idea-genies** | O | — | — | — | — | O | — | Contested one-way-door decisions | -| **research** | S | — | — | — | — | — | — | Codebase / topic findings into plan | -| **codebase-recon** | S | — | — | — | — | — | — | Entry-to-test reconstruction | -| **reverse-engineer** | O | — | — | — | — | — | — | Authorized reverse-engineering | -| **plan** | P | S | P | — | S | — | — | Behavior-sized issues + waves + acceptance | -| **behavior-first-planning** | S | S | P | S | — | — | — | Gherkin → EXECUTED-red → acceptance-gated DAG | -| **beads-br** | — | P | S | S | S | S | S | Tracker; acceptance rides on the bead | -| **beads-bv** | — | S | S | — | S | — | — | Graph triage / bottlenecks | -| **pre-mortem** | S | — | S | — | — | S | — | Stress-test plan before build (also feeds membrane) | -| **implement** | — | — | — | P | S | — | — | One bead: RED → green → refactor | -| **test** | — | — | S | S | — | S | — | Test/coverage plans alongside implement | -| **refactor** | — | — | — | S | — | S | — | Safe refactors under green / own slice | -| **scope** | — | — | S | S | S | — | — | Frozen path guard during risky work | -| **crank** | — | — | — | S | P | S | — | Epic waves through the loop | -| **swarm** | — | — | — | — | P | — | — | Parallel agents; needs disjoint scopes | -| **rpi** | S | S | S | S | S | S | S | **One full tick** of the loop (meta orchestrator) | -| **validate** | — | — | — | — | — | P | S | PASS/WARN/FAIL vs acceptance; no verdict = not done | -| **council** | O | — | — | — | — | P | S | Multi-judge consensus under high stakes | -| **pawl-review** | — | — | — | — | — | P | — | Fresh-context lane → pawl / land evidence | -| **converge** | — | — | — | S | — | S | — | Fix → re-judge until agreement or BLOCK | -| **reality-check** | — | — | — | — | S | S | — | Mid-epic drift: code vs plan | -| **security** | — | — | — | — | — | S | — | Vuln/secrets/release security gate | -| **post-mortem** | — | — | — | — | — | S | P | Evidence + learning ratchet | -| **pattern-mining** | — | — | — | — | — | — | S | Recurring shapes → durable patterns | -| **operationalize** | — | — | — | — | — | — | O | Experimental: corpus → operator surfaces | -| **handoff** | — | S | — | — | — | — | S | Session continuity packet | -| **status** | — | S | — | — | S | — | S | Where am I / recover | -| **bootstrap** | S | S | — | — | — | — | — | First-time repo / AgentOps setup | -| **push** | — | — | — | — | — | S | — | Validate, commit, push discipline | -| **release** | — | — | — | — | — | S | S | Changelog / tag / release validation | -| **pr-prep** | — | — | — | — | — | S | — | PR body/commits when using PR flow | -| **doc** | S | — | — | — | — | S | S | Docs packs; often paired with product changes | -| **domain** | S | S | S | S | — | S | S | Ubiquitous language (library-ish knowledge) | -| **evolve** | O | O | O | O | O | O | O | Experimental outer loop (N ticks) | - ---- - -## Session, meta, substrate, and tooling (off the critical spine) - -These skills matter; they are not the default intent→validated-code path. - -| Skill | Role | Use when | -|-------|------|----------| -| **automation-shape-routing** | Meta front door | Choosing inline vs fanout vs substrate before spawning agents | -| **agent-native** | Substrate lifecycle | Persistent factory workers (substrate-neutral) | -| **ntm** | Substrate | tmux swarm panes / robot APIs | -| **agent-mail** | Coordination | ≥2 writers — locks, inboxes, reservations | -| **codex-exec** | Orchestration | Codex-shaped execution adapter | -| **using-gc** | Substrate (opt-in) | Gas City city-shaped work — never auto-routed | -| **gc-membrane** | Library | Membrane pack close-door reference for GC | -| **ms** | Discovery of skills | Search/load skills across corpora | -| **cass** | Session mining | Mine past sessions for prompts/decisions | -| **heal-skill** / **skill-builder** / **workflow-builder** | Meta | Author/repair skills and workflows | -| **converter** / **agy-native** | Cross-vendor | Format / Gemini·AGY runtime bridges | -| **shared** / **standards** | Library | Contracts and coding standards loaded JIT | -| **scaffold** | Tooling | Project/CI scaffolds | -| **cc-hooks** | Tooling | Claude Code hooks (opt-in; product is hookless) | -| **dcg** / **sbh** / **rch** / **account-rotation** | Tooling | Safety, browser, remote workers, account rotation | -| **toil-mining** | Meta | Find repeated toil → automation candidates | - ---- - -## Composition patterns (full flow) - -| Work size | Skill sequence | Acceptance spine | -|-----------|----------------|------------------| -| **One behavior** | `/plan` → `/implement` → `/validate` | One Gherkin scenario → one RED test → membrane cites it | -| **One tick (wrapped)** | `/rpi "goal"` | Same loop; orchestrator owns re-plan | -| **Multi-bead epic** | `/discovery` → `/plan` → `/pre-mortem` → `/crank` → `/validate` → `/post-mortem` | Each bead carries scenarios; epic closes on roll-up | -| **Parallel wave** | `/plan` (disjoint scopes) → `/swarm` or `/crank` → `/validate` | Wave invalid if write scopes collide | -| **High-stakes close** | `/validate` + `/council` + `/pawl-review` → land | Independent judges + commit-bound verdict | -| **Unattended / city** | Substrate dispatches `/rpi` per bead (`/ntm`, `/using-gc`, …) | Loop invariants stay in skills — substrate does not re-encode them | - ---- - -## Membrane row (what "validated" means) - -| Input the membrane needs | Who produces it | What `/validate` does | -|--------------------------|-----------------|------------------------| -| Given/When/Then (or linked `.feature`) | `/plan`, `/behavior-first-planning`, bead body | Maps each scenario to fresh evidence | -| Runnable acceptance test | `/implement` / `/test` (ATDD) | Confirms RED-was-real then green | -| Diff / artifact under review | git / PR | Judges implementation against the contract | -| Independent judge (when required) | `/council`, `/pawl-review` | No self-grade; context_id ≠ author | - -If scenarios and acceptance commands are missing, the honest outcome is **HOLD** -("no behavior to validate against") — not a vibe PASS. - ---- - -## CLI vs skills - -| Job | Prefer | Not | -|-----|--------|-----| -| Run the loop | Skills (`/plan`, `/implement`, `/validate`, `/rpi`, …) | Treating `ao verify` as the product | -| Track beads | `/beads-br` + `br` via `ao beads dir` | Ad-hoc chat memory | -| Release gate | `ao gate check --fast --scope head` | Green CI alone as done | -| Commit ratchet / ledger | `ao verify`, `ao provenance`, `ao land` | Skipping the skill membrane on the slice | -| Retrieve prior context | `ao lookup`, `ao search` | Pasting whole histories | - ---- - -## Maintenance - -1. New skill → add a row here + disposition row + tier in frontmatter; run `make regen-all`. -2. Retired skill → move to historical disposition; delete or strike the matrix row in the same PR. -3. Keep the short [Skill → loop-move map](architecture/operating-loop.md#skill--loop-move-map) in sync with the **P** column of the spine matrix above. -4. Router trees in `SKILLS.md` / `SKILL-ROUTER.md` must not invent a different product order than [Intent → Validated Code](architecture/intent-to-validated-code.md). diff --git a/docs/software-factory.md b/docs/software-factory.md index 5610b24ad..7c13191ae 100644 --- a/docs/software-factory.md +++ b/docs/software-factory.md @@ -1,77 +1,45 @@ -# Software Factory Surface +# Optional software-factory adapters -AgentOps is a software-factory operating contract for coding agents. It turns -accepted intent into independently judged evidence while keeping runtimes, -trackers, and repository delivery replaceable. +AgentOps defines portable packets and role boundaries. It does not run a +software factory or own its queue. -## Four umbrellas +## Roles -| Umbrella | Factory role | Output | +| Role | Input | Output | |---|---|---| -| Discovery | Orchestrator shapes behavior and consumes Premortem when needed | accepted plan and tranche packet | -| Crank | One writer implements one bounded tranche | frozen candidate and deterministic receipts | -| Validate | A fresh author-distinct context judges the complete claim set once | immutable PASS or FAIL verdict | -| Learn | The orchestrator records the smallest useful consequence | no-change, plan-impact, or terminal receipt | +| Planner | caller intent | one PlanPacket | +| Implementer | exact PlanPacket | one CandidatePacket | +| Validator | exact Plan and Candidate in a fresh context | one durable verdict | -Postmortem is optional after Learn and answers an explicit causal question. It -is not a fifth lifecycle gate. +One runtime may fill the roles in separate contexts. PASS still requires +distinct nonempty author and validator context IDs plus an explicit freshness +attestation. -## Roles and runtime variants +## Optional runtimes -The role split is orchestrator, implementer, and validator. One model can fill -all three roles only through separate contexts; the candidate's author cannot -be its validator. One fresh validator is the default. A council or multiple -model families are explicit higher-rigor strategies. +Native Codex, NTM panes, Agent Mail, managed agents, cloud workers, and Gas City +may host these roles when the caller selects them. They may provide process +durability, isolation, or messages; their internal retries, pane state, queues, +and budgets never become AgentOps result state. -Native Codex, another local agent, NTM-managed panes, managed agents, and cloud -workers all run the same loop. Runtime adapters may provide process durability, -mailboxes, or isolation; they do not change role authority or lifecycle state. +`dispatch_once(explicit_disjoint_packets, executor)` is the complete factory +adapter contract. The caller supplies every packet and the executor. The +adapter checks that write scopes are provably disjoint, dispatches each packet +once, returns candidate/evidence/error per packet, and stops. It does not +select, persist, retry, validate, integrate, commit, close, release, or deliver. -## CLI boundary +## Integration boundary -The final `ao` CLI is a deterministic transaction kernel and flight recorder. -It may pull one tracked leaf, freeze candidate identity, run factual checks, -record an external verdict, reduce Learn bookkeeping, record delivery facts, -and close a report after remote verification. It does not decide intent, drive -a model, issue semantic judgment, choose Git policy, or operate a merge queue. +A factory may combine returned candidates using its own repository policy. Each +semantic candidate still needs exact identity and a fresh Validate verdict. +AgentOps does not convert factory completion, worker success, or deterministic +checks into PASS. -The current executable still contains transitional commands and build profiles. -They are removed through exact same-owner K, CLI, and F leaves; this document -does not treat those leftovers as product variants. +Git, trackers, pull requests, merge queues, CI, deployment, and release remain +owned by the caller's environment. -## Repository-owned delivery +## Related contracts -After Validate and Learn, the consumer repository chooses direct push, a PR, -hosted CI, a dedicated merger, or another adapter. The choice is identical for -local and cloud agents. Delivery consumes proof but cannot upgrade it. AgentOps -records the adapter, target, result, and remote identity. - -## Concurrency - -One writer and one active leaf are the default. An explicitly requested swarm -may parallelize read-only research. Concurrent write lanes require separate -worktrees, independently checked disjoint manifests, one owner per lane, and a -lead-owned integration order. - -## Learning boundary - -Learn runs once in the orchestrator's existing context. A repeated defect may -become a proposed check only after two distinct objectives plus runnable -positives, negative controls, shadow evidence, an owner, rollback, and expiry. -Pattern mining and promotion stay off the tranche critical path. - -## Design rules - - -- Prefer briefings over giant startup dumps. -- Keep substrate and operator surfaces distinct. -- Let external validation outrank self-report. -- Treat thin topics as discovery-only until evidence improves. -- Keep deterministic mechanisms separate from semantic judgment. - -## Related docs - -- [Operating Loop](architecture/operating-loop.md) -- [Agent Workflow Reference](agent-workflow-reference.md) -- [Go CLI Architecture Guide](architecture/go-cli-architecture-guide.md) -- [Context Packet](context-packet.md) +- [Operating loop](architecture/operating-loop.md) +- [Optional dispatch](contracts/orchestration-ports.md) +- [Agent workflow](agent-workflow-reference.md) diff --git a/docs/standards/olympus-engineering-standards.md b/docs/standards/olympus-engineering-standards.md index dc2280cc0..d0b83eac1 100644 --- a/docs/standards/olympus-engineering-standards.md +++ b/docs/standards/olympus-engineering-standards.md @@ -31,7 +31,7 @@ Verbatim from olympus: 4. For architecture changes, provide at least two alternatives and justify the selected option with tradeoffs (correctness, operability, and cost). 5. If uncertainty is material, state it directly and define the experiment or data needed before committing to a build path. -Agentops's `.claude/rules/*` cover code-level conventions but not this pre-implementation decision gate. Closest equivalent is `/pre-mortem` and `/brainstorm` skills, which are interactive flows rather than written standards. +Agentops's `.claude/rules/*` cover code-level conventions but not this pre-implementation decision gate. Closest equivalent is `/premortem` and `/brainstorm` skills, which are interactive flows rather than written standards. ### Logging and observability hygiene @@ -68,7 +68,7 @@ All olympus rules in this category are equally or more thoroughly covered by the | Coverage with behavior tests, not happy-path padding | `.claude/rules/go.md` ("No coverage-padding tests"), `skills/standards/references/go.md` ("Test Conventions") | | `#!/usr/bin/env bash` + `set -euo pipefail`; quote variables; `command -v`; cleanup traps; non-interactive in CI | `skills/standards/references/shell.md` (full coverage), agentops `~/CLAUDE.md` ("Non-interactive shell defaults") | | One H1 per document; consistent heading hierarchy; runnable command blocks | `skills/standards/references/markdown.md` | -| Start from synced `main`; do not finish session until commits pushed | agentops `~/CLAUDE.md` ("Task tracking protocol"), `/push` skill | +| Start from synced `main`; publish through the repository's own Git policy | Repository/operator policy; AgentOps does not own Git delivery | ## What did NOT carry (rejected with reason) @@ -76,10 +76,10 @@ All olympus rules in this category are equally or more thoroughly covered by the |---|---| | `internal/` for implementation, `cmd/` for entrypoints | Already idiomatic Go layout; agentops follows it (`cli/cmd/ao`, `cli/internal/`) without needing it written down. Restating would be noise. | | Source-of-truth order naming `docs/specs/index.md`, `SPEC-CONTRACT.md`, etc. | Olympus-specific doc tree. Agentops has its own precedence ladder in `CLAUDE.md` ("Source-of-Truth Precedence") — that one wins. | -| `make test`, `make build`, `make testing-check`, `make daemon-smoke`, `make serve-smoke`, `make throughput` as gate names | Olympus Makefile target names. Agentops's gate is `scripts/pre-push-gate.sh` + `cd cli && make build && make test` — different surface. | +| `make test`, `make build`, `make testing-check`, `make daemon-smoke`, `make serve-smoke`, `make throughput` as gate names | Olympus Makefile target names. AgentOps exposes deterministic repository checks through `ao gate check`; release policy remains repository-owned. | | Coverage ratchet via `scripts/check-coverage-floors.sh` | Olympus-specific script path. Agentops handles coverage through `/validate`, complexity checks, and the local Go gate / CI rather than a ratcheted-floor script. | | Suite definitions in `testing/suites/*.md`, scenarios in `testing/scenarios/catalog.md` | Olympus-specific paths and catalog. Agentops uses `tests/` + `/scenario` skill (holdout scenarios in `.agents/holdout/`) — different model. | -| Long-tail merge checklist (6 items: spec match, tests, daemon/serve safety, throughput, docs/runbooks, goals/traceability) | Heavily olympus-coupled (daemon/serve binaries, throughput as a tracked metric, GOALS-yaml ratchets). Agentops's merge gate is `scripts/pre-push-gate.sh` and CI's 24 jobs — already enforced mechanically. Re-stating as prose would drift. | +| Long-tail merge checklist (6 items: spec match, tests, daemon/serve safety, throughput, docs/runbooks, goals/traceability) | Heavily Olympus-coupled. A consumer repository chooses its own merge checks; AgentOps supplies intent, exact-subject validation, and deterministic test utilities without becoming merge policy. | | Per-clone runtime isolation (`OL_HOME`, `BEADS_DIR`) | Olympus crew-clone workflow. Agentops uses repo-local `.agents/` and `.beads/` without `OL_HOME` indirection. | | Source-of-truth precedence (`docs/specs` > spec-contract > engineering-standards > workflow guides) | Olympus-specific document hierarchy. Agentops's executable-first precedence (CLI > schemas > docs) supersedes it. | | "Portable Template Intent" closing section | Meta-commentary about reuse, not a standard. Self-fulfilling: this port is the reuse. | diff --git a/docs/templates/README.md b/docs/templates/README.md index ce55d8007..5095c3428 100644 --- a/docs/templates/README.md +++ b/docs/templates/README.md @@ -2,12 +2,12 @@ ## Operating-loop artifacts -These templates carry one turn of the [operating loop](../architecture/operating-loop.md): BDD intent → vertical slices → conflict-free wave → bead acceptance → evidence. +These templates carry one bounded turn of the +[operating loop](../architecture/operating-loop.md): behavior-first intent, +one experiment, exact subject identity, and fresh validation. -- [Intent Issue (BDD-shaped)](./intent-issue.md) — produced by `/discovery` (or `/brainstorm` for the earlier free-text → structured pass). The intent issue is not ready until acceptance examples are testable. -- [Goal Design Intent](./goal-design-intent.md) — schema-backed `.agents/goal-design//intent.md` artifact for objective, BDD behavior, boundaries, evidence, and hard rules. -- [Goal Design Driver](./goal-design-driver.md) — schema-backed `.agents/goal-design//driver.md` artifact for four-loop routing, candidate beads, route-back rules, digest integrity, and validation policy. -- [Slice Validation Plan](./slice-validation.md) — produced by `/plan`, executed by `/validation`. One row per vertical slice; roll-up proves the bead's acceptance examples. +- [PlanPacket worksheet](./intent-issue.md) — optional source material for Plan. +- [Candidate and validation worksheet](./slice-validation.md) — factual candidate and independent verdict inputs. ## Authoring templates diff --git a/docs/templates/goal-design-driver.md b/docs/templates/goal-design-driver.md deleted file mode 100644 index b29b6f59a..000000000 --- a/docs/templates/goal-design-driver.md +++ /dev/null @@ -1,90 +0,0 @@ ---- -schema_version: 1 -kind: goal-design.driver -id: gd-driver- -slug: -created_at: "" -status: draft -intent_ref: - path: ".agents/goal-design//intent.md" - sha256: "" - schema_version: 1 -loop_routing: - delivery: "" - rpi: "" - promotion: "" - knowledge: "" -candidate_beads: - - id: B1 - behavior: "" - bounded_context: "" - first_failing_proof: "" - write_scope: - - "" - close_signal: "" -small_batch_gate: - one_behavior: true - one_bounded_context: true - one_primary_write_scope: true - one_acceptance_proof: true - split_required_if: - - "" -route_back_rules: - checker_fails: "" - bead_closes_with_new_signal: "" - candidate_stale: "" - promotion_contradicts_intent: "" -execution_mode: - default: single-agent - escalations: - ntm_atm: "" - workflow: "" -artifact_validation: - checker_command: "scripts/check-goal-design-packet.sh .agents/goal-design/" ---- -# Goal Design Driver: - -## Source Intent - -- Intent artifact: `.agents/goal-design//intent.md` -- Intent digest: `` - -## Loop Routing - -| Loop | Driver contract | -| --- | --- | -| Delivery | | -| RPI | | -| Promotion | | -| Knowledge | | - -## Candidate Beads - -| Candidate | Behavior | Bounded context | First failing proof | Write scope | Close signal | -| --- | --- | --- | --- | --- | --- | -| B1 | | | | | | - -## Small-Batch Gate - -- One behavior per bead: true -- One bounded context per bead: true -- One primary write scope: true -- One acceptance proof: true -- Split required if: `` - -## Route-Back Rules - -- If the packet checker fails: `` -- If a bead closes but reveals a better next step: `` -- If a candidate becomes stale: `` -- If promotion or knowledge contradicts the original intent: `` - -## Execution Mode - -- Default: single-agent in-session loop. -- Escalate to NTM/ATM only when attach, steer, durability, or cross-model debate is required. -- Escalate to Workflow only for deterministic structured DAGs. - -## Packet Check - -- Checker command: `scripts/check-goal-design-packet.sh .agents/goal-design/` diff --git a/docs/templates/goal-design-intent.md b/docs/templates/goal-design-intent.md deleted file mode 100644 index 881ba06e1..000000000 --- a/docs/templates/goal-design-intent.md +++ /dev/null @@ -1,100 +0,0 @@ ---- -schema_version: 1 -kind: goal-design.intent -id: gd-intent- -slug: -created_at: "" -status: draft -objective: "" -why_it_matters: "" -domain_terms: - - term: "" - definition: "" - source: "" -bdd: - feature: "" - scenarios: - - id: S1 - name: "" - given: - - "" - when: - - "" - then: - - "" -boundaries: - bounded_context: "" - in_scope: - - "" - non_goals: - - "" - rollback_or_containment: "" -evidence_for_done: - first_failing_proof: "" - validation_command: "scripts/check-goal-design-packet.sh .agents/goal-design/" - evidence_path: "" -inputs_to_recheck: - repo_paths: - - "" - prior_artifacts: - - "" - live_surfaces: - - "" - stale_assumptions: - - "" -hard_rules: - - "Keep behavior slices small." - - "Do not rely on stale claims without verification." - - "Do not bypass the deterministic packet checker." ---- -# Goal Design Intent: - -## Objective - - - -## Why It Matters - - - -## Domain Terms - -- **** - - -## BDD Behavior - -```gherkin -Feature: - - Scenario: - Given - When - Then - And -``` - -## Boundaries - -- Bounded context: `` -- In scope: `` -- Non-goals: `` -- Rollback / containment: `` - -## Evidence For Done - -- First failing proof: `` -- Validation command: `scripts/check-goal-design-packet.sh .agents/goal-design/` -- Evidence path: `` - -## Inputs To Recheck - -- Repo paths: `` -- Prior artifacts: `` -- Live surfaces: `` -- Assumptions that can go stale: `` - -## Hard Rules - -- Keep behavior slices small. -- Do not rely on stale claims without verification. -- Do not self-certify acceptance. diff --git a/docs/templates/intent-issue.md b/docs/templates/intent-issue.md index 1e5455186..bf565f441 100644 --- a/docs/templates/intent-issue.md +++ b/docs/templates/intent-issue.md @@ -1,118 +1,66 @@ -# Intent Issue Template (BDD-shaped) +# PlanPacket worksheet -> Copy this file when shaping a new piece of work. The issue is **not ready** until every section below is filled in and the acceptance examples are testable. Skill that produces this artifact: [`/discovery`](../../skills/discovery/SKILL.md) (and `/brainstorm` for the earlier free-text → structured pass). -> -> See [`docs/architecture/operating-loop.md`](../architecture/operating-loop.md) for why this template exists and where it sits in the loop. -> -> Fast path: `scripts/render-intent-bead.sh --help` renders a Directive 12 compliant dry-run body and labels for `BEADS_DIR="$(ao beads dir)" br create`. +Use this worksheet to shape one behavior before serializing +[`plan-packet.v1`](../../schemas/plan-packet.v1.schema.json). It is optional +source material for Plan, not work readiness, ownership, or scheduling state. ---- +## Intent -## Feature + -> One sentence. The capability being added or changed, in the consumer's language (not the implementation's). +## One active behavior -## Bounded context - -> Which bounded context from [`docs/contracts/context-map.md`](../contracts/context-map.md) does this work belong to? If it crosses contexts, this is two issues, not one. -> The bead must carry exactly one matching label: `bc-corpus`, `bc-validation`, `bc-loop`, `bc-factory`, `bc-runtime`, or `bc-orchestration`. - -## Hexagonal boundary - -> Fill this from [`docs/architecture/intent-to-loop-hexagon.md`](../architecture/intent-to-loop-hexagon.md). This is the handoff contract for the next agent. - -```yaml -hexagon: - inbound_port: shape_intent - bounded_context: - driving_adapter: - driven_adapters: - -
- guard_adapters: - - - context_packet: - done_state: "all scenarios testable; evidence list concrete; non-goals explicit" -``` - -## Domain terms - -> Domain terms used below, each anchored to the ubiquitous-language register at [`skills/domain/references/`](../../skills/domain/references/) or [`skills/standards/references/architecture-terms.md`](../../skills/standards/references/architecture-terms.md). New terms must be added to the register before they are used here. - -- **** — definition + register link -- **** — … + ## Acceptance examples -> At least one happy path and at least one critical edge. Each example must be testable as written. "It should work" is not an example. - ```gherkin -Feature: +Scenario: + Given + When + Then - Scenario: - Given - When - Then - And - - Scenario: - Given - When - Then +Scenario: + Given + When + Then ``` ## Non-goals -> Things this issue will explicitly **not** do. Anything not listed under acceptance examples and not listed here is out of scope by default — list the ones a reasonable reader might expect to be in scope so the boundary is loud. +- -- -- +## Required evidence -## Rollback / containment path +- -> How do we undo if this goes wrong? Name the concrete mechanism: feature flag, schema migration with `down`, branch revert, config toggle, etc. If no rollback exists, say so explicitly — that is itself useful information. +## Write scope -- +```yaml +include: + - + - +exclude: + - +``` -## Evidence needed for completion +## First acceptance check -> What proves the acceptance examples passed? Be specific — test names, snapshot keys, eval suite names, council verdicts, citation events. The bead does not close without these artifacts existing. +Choose one: -- Test: `` covering Scenario 1 -- Test: `` covering Scenario 2 (edge) -- Snapshot / golden: `` -- Eval suite: `` -- Council verdict: `` -- Other evidence: `` +```yaml +command: +``` -## Vertical slice candidates +```yaml +artifact_path: +``` -> Initial slice list, one per acceptance example (minimum). `/plan` will refine this into the final slice + wave plan. Each slice must have a nameable first failing test, a write-scope sketch, and a bounded-context tag (defaults to the one above). +## Rollback or containment -| Slice ID | Scenario | First failing proof/test (proposed) | Write scope (proposed) | Notes | -|----------|----------|-------------------------------|------------------------|-------| -| S1 | | `` | `` | | -| S2 | … | … | … | … | + -## Linked artifacts - -- Parent bead: `
` -- ADR (if architectural): `` -- Prior research: `<.agents/research/*.md or "n/a">` -- Pre-mortem: `<.agents/council/YYYY-MM-DD-pre-mortem-*.md or "to run">` - ---- - -## Readiness checklist - -A `/pre-mortem` or `/council` must verify these before the issue leaves discovery: - -- [ ] Acceptance examples are written in Given/When/Then and each is testable as written -- [ ] Hexagonal boundary block names the inbound port, bounded context, adapters, context packet, and done state -- [ ] Bounded context is named, present in the context map, and represented by exactly one `bc-*` label -- [ ] All domain terms used are registered in the ubiquitous-language register -- [ ] Non-goals are explicit -- [ ] Rollback or containment path is named (or its absence is named explicitly) -- [ ] Evidence list points to concrete artifacts, not vague descriptions -- [ ] Slice candidates exist (at least one per acceptance example) and each has a first failing proof/test - -If any box is unchecked, the issue is not ready — send it back to `/discovery` or `/brainstorm`. +Plan computes the intent, acceptance, and packet digests. It emits no owner, +priority, attempt, wave, queue, lease, admission, next action, closure, release, +or delivery field. diff --git a/docs/templates/kernel.template.md b/docs/templates/kernel.template.md index 0c4fb3c7b..825d557e2 100644 --- a/docs/templates/kernel.template.md +++ b/docs/templates/kernel.template.md @@ -1,77 +1,24 @@ # Project: {{PROJECT_NAME}} -## Behavioral Standards +## Behavioral standards - -Implement changes rather than suggesting. Infer intent and proceed. - +- Read the relevant source before changing it. +- State one observable behavior and a critical edge. +- Preserve unrelated work and declare the intended write scope. +- Run the smallest useful deterministic check while editing. +- Bind semantic validation to exact content and a fresh validator context. +- Report checked and unchecked scope honestly. - -Read files before proposing changes. No speculation about unread code. - +## AgentOps loop - -Only make requested changes. Keep solutions simple. - - -## Intent Detection - -| Intent | Keywords | Action | -|--------|----------|--------| -| Resume | "continue", "pick up", "back to" | Load bundles, read progress | -| End | "done", "stopping", "finished" | Save state, update progress | -| Status | "what's next", "where was I" | Show progress, next item | -| New Work | "add", "implement", "create" | Check bundles, start RPI | -| Bug Fix | "fix", "bug", "broken" | Debug directly | - -## Session Protocol - -On first interaction, check for progress files: - -```bash -[ -f "claude-progress.json" ] && [ -f "feature-list.json" ] +```text +RPI -> Plan -> Implement -> fresh Validate -> durable verdict -> report and stop ``` -If found, display current state and next work item. +The caller owns revisions, retries, work organization, Git, CI, release, and +delivery. Optional specialists, councils, and runtime adapters are invoked only +when the caller selects them. -## Vibe Levels +## Project constraints -| Level | Trust | Verify | Use For | -|-------|-------|--------|---------| -| 5 | 95% | Final only | Format, lint | -| 4 | 80% | Spot check | Boilerplate | -| 3 | 60% | Key outputs | Features | -| 2 | 40% | Every change | Integrations | -| 1 | 20% | Every line | Architecture | -| 0 | 0% | N/A | Research | - -## Constraints - -- Use semantic commits (`feat:`, `fix:`, `docs:`) -- Keep context under 40% - compress and bundle when approaching limit -- {{ADDITIONAL_CONSTRAINTS}} - -## Resources - -| Resource | Location | -|----------|----------| -| Commands | `.claude/commands/` | -| Bundles | `.agents/bundles/` | -| Progress | `claude-progress.json` | -| Features | `feature-list.json` | - ---- - -## Slash Commands - -| Command | Action | -|---------|--------| -| `/session-start` | Initialize session | -| `/session-end` | Save state and end | -| `/research` | Deep exploration | -| `/plan` | Create implementation plan | -| `/implement` | Execute approved plan | -| `/bundle-save` | Save context bundle | -| `/bundle-load` | Load context bundle | -| `/vibe-check` | Measure session metrics | -| `/vibe-level` | Classify task trust level | +{{ADDITIONAL_CONSTRAINTS}} diff --git a/docs/templates/slice-validation.md b/docs/templates/slice-validation.md index 45bd2cea4..e93473d9f 100644 --- a/docs/templates/slice-validation.md +++ b/docs/templates/slice-validation.md @@ -1,113 +1,42 @@ -# Slice Validation Plan Template +# Candidate and validation worksheet -> Copy this file when planning the validation surface for a bead. One row per vertical slice; the roll-up at the bottom proves the bead's acceptance examples. Skill that produces this artifact: [`/plan`](../../skills/plan/SKILL.md) (drafts) and [`/validate`](../../skills/validate/SKILL.md) (executes and rolls up). -> -> See [`docs/architecture/operating-loop.md`](../architecture/operating-loop.md) for the loop position and [`docs/templates/intent-issue.md`](intent-issue.md) for the upstream artifact this validation plan checks against. +Use this worksheet for one bounded CandidatePacket and its independent Validate +review. The machine contracts are +[`candidate-packet.v1`](../../schemas/candidate-packet.v1.schema.json), +[`subject-manifest.v1`](../../schemas/subject-manifest.v1.schema.json), and +[`verdict.v2`](../../schemas/verdict.v2.schema.json). ---- +## Pinned inputs -## Bead reference +- PlanPacket digest: `` +- Acceptance digest: `` +- Author context ID: `` +- Subject locator: `` -- **Bead id:** `` -- **Intent issue:** `` -- **Bounded context:** `` +## Bounded experiment -## Hexagonal boundary +- RED evidence: `` +- GREEN evidence: `` +- Refactor evidence: `` -> Fill this from [`docs/architecture/intent-to-loop-hexagon.md`](../architecture/intent-to-loop-hexagon.md). This is the port contract from planning into execution and validation. +## Candidate facts -```yaml -hexagon: - inbound_port: plan_slices - bounded_context: - driving_adapter: /plan - driven_adapters: - - bd/br - - git - - test runner - guard_adapters: - - wave-validity matrix - - symbol verification - - completion-claim kernel - context_packet: - done_state: "each slice has first failing proof, owner, write scope, and validation lane" -``` +- Subject-manifest digest: `` +- Actual changed paths: `` +- Changed-path coverage complete: `true | false` +- Other factual evidence: `` +- Checks not run: `` -## Slice-level validation +## Independent Validate review -> One row per slice from the intent issue's slice candidates. Each slice must have a **first failing test** before any implementation begins (the TDD discipline of move 4 in the operating loop). -> In beads and CycleTrace payloads this may be named **First failing proof** when the proof is a gate, smoke script, or eval rather than a Go/Python unit test. +- Validator context ID: `` +- Freshness source: `runtime | caller` +- Freshness attester: `` +- Criterion results: `` +- Checked: `` +- Not checked: `` +- Verdict artifact: `` -| Slice ID | Behavior under test | First failing proof/test | Implementation scope | Validation lane | Evidence on green | Acceptance example covered | -|----------|--------------------|--------------------|----------------------|-----------------|--------------------|----------------------------| -| S1 | | `` — must fail for the right reason (missing behavior, not syntax) | `` | L1 unit / L2 integration / L3 e2e / property / snapshot / council | | | -| S2 | … | … | … | … | … | … | -| … | … | … | … | … | … | … | - -### Validation lane reference - -| Lane | What it covers | When to choose it | -|------|----------------|-------------------| -| L1 unit | Single function or method, no external deps | Pure logic, parsers, invariants on small types | -| L2 integration | Multiple internal units, real adapters where cheap | The default for behavioral slices — where bugs are actually found | -| L3 e2e | Full workflow including external systems | Reserve for slices that prove a whole-system contract | -| property | Invariant under generated input | Aggregate roots, parsers, state machines | -| snapshot / golden | Output stability against a frozen baseline | Generators, formatters, doc emitters | -| council | Multi-judge verdict on non-mechanical correctness | Design decisions, plan quality, taste-level checks | - -## Wave validity - -> If any slices are planned to run in parallel, every row of this gate must pass. Any failed row → those slices run **sequential**. See [`docs/architecture/operating-loop.md`](../architecture/operating-loop.md) move 5 for the full rationale. - -| Check | Status | Notes | -|-------|--------|-------| -| Distinct write scopes (modified-files sets are disjoint) | [ ] | | -| Distinct test targets (no shared fixture mutation) | [ ] | | -| No shared migration / schema / generated file | [ ] | | -| No shared CLI surface (flags / arguments) | [ ] | | -| Integration order declared if it matters | [ ] | | -| Owner per slice (one agent or one human, no joint) | [ ] | | -| Discard path per slice (rollback or drop-and-re-plan) | [ ] | | - -**Wave decision:** [ ] parallel [ ] sequential - -## Roll-up acceptance - -> The bead closes only when every Given/When/Then from the intent issue has a passing test linked to it. Activity logs do not close beads. - -| Acceptance example from intent issue | Slice(s) that cover it | Passing-test evidence | Status | -|---------------------------------------|------------------------|------------------------|--------| -| Scenario: | S1 | `` — green at `` | [ ] | -| Scenario: | S2 | `` — green at `` | [ ] | -| Scenario: … | … | … | [ ] | - -## Residual gaps at close - -> Anything descoped or deferred during the loop. Each entry must say where it goes next: a new bead, a learning, a planning rule, a gate proposal, or explicit acceptance of the gap. - -- — → -- — → … - -## Evidence index - -> Concrete artifacts produced during this bead's execution, suitable for `.agents/ratchet/` and for council retrieval. - -- Tests added: `` -- Snapshots updated: `` -- Council verdicts: `` -- Pre-mortem outcome: `<.agents/council/YYYY-MM-DD-pre-mortem-*.md>` -- Post-mortem learning: `<.agents/learnings/*.md or "stayed in handoff per ratchet">` -- Ratchet entry: `` - ---- - -## Closing checklist - -- [ ] Every slice has a first failing proof/test linked, and that proof failed before implementation began -- [ ] Hexagonal boundary block is filled in and matches the intent issue bounded context -- [ ] Every slice's evidence row is filled in with a concrete artifact, not a description -- [ ] If any slices ran in parallel, every wave-validity row was checked at the time of wave start -- [ ] Every acceptance example from the intent issue maps to at least one passing test -- [ ] Every non-goal from the intent issue is still untouched -- [ ] Residual gaps each have a next-step disposition -- [ ] At most one learning was promoted to `.agents/learnings/` (most observations died at handoff per the promotion ratchet) +Any subject edit invalidates the review. A proven out-of-scope path is FAIL; +incomplete changed-path coverage is NOT_PROVEN. Validate persists one verdict +and stops without repair, retry, Git, closure, release, or delivery. diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index b095ff360..d9c3eb2b6 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -1,367 +1,44 @@ # Troubleshooting -Common issues and quick fixes for AgentOps. +## A verdict is `NOT_PROVEN` ---- +Check the criterion results and `not_checked` list. Common causes are missing or +colliding author/validator context IDs, no freshness attestation, incomplete +changed-path coverage, subject mutation, corrupt evidence, or persistence +failure. Correct the input or environment and let the caller decide whether to +start a new invocation. -## "Where are the hooks?" +## A verdict is `FAIL` -**AgentOps 3.0 ships zero hooks.** There is no `hooks/` directory, no -`hooks.json`, and no `ao hooks` command — nothing auto-injects orientation or -gates your tool calls at session start. If you came from an older version -expecting hooks to "run", that behavior is gone by design (the hookless-first -teardown). The workflow is now guided by **skills + the `ao` CLI**, and the -**local cockpit Go gate (`ao gate check`) in the pre-push hook is the routine -release authority** — `.github/workflows/validate.yml` is a tag/PR/manual -backstop, not the gate on every push. +`FAIL` means a concrete acceptance defect was proven for the exact subject. +Validate records the complete finding set and stops. It does not repair, +re-plan, retry, or choose a next action. -**What replaces the old auto-injected context:** +## Deterministic checks fail + +Run the failing repository command directly. `ao gate check` is only a +deterministic test runner; it does not validate semantics or grant delivery +permission. + +## Generated skill projections drift + +Run: ```bash -ao session bootstrap # the universal init prompt / orientation report -ao inject "" # pull decay-ranked prior context on demand +scripts/regen-all.sh +scripts/regen-all.sh --check ``` -**Diagnosis (check your install, not hooks):** +Edit `skills//SKILL.md` metadata rather than generated registries, maps, +image copies, or parity twins. -```bash -ao doctor -``` +## A removed command is invoked -This reports CLI, knowledge-base, plugin, and freshness health. None of these -are hooks — there are none to install. +The one-release tombstone names the surviving alternative and exits nonzero. +It never forwards to the retired implementation. See [MIGRATION.md](MIGRATION.md). -**If you want your own gates:** AgentOps deliberately ships none, but you can -author opt-in hooks yourself. Use the `hooks-authoring` skill to add a bounded -gate (block a dangerous op, bootstrap a session, run a parity check) for your -runtime — Claude reads `~/.claude/settings.json`; other harnesses use their own -config. These are yours to own; AgentOps neither installs nor requires them. +## Skills are missing after an update ---- - -## Skills not showing up - -Skills must be installed as a Claude Code plugin. - -**Diagnosis:** - -```bash -claude plugin list -claude plugin marketplace list -ao doctor -``` - -The `ao doctor` "Plugin" check scans the `skills/` directory for subdirectories containing a `SKILL.md` file. If it reports "no skills found" or "skills directory not found", the plugin is not installed correctly. - -**Fixes:** - -1. Install or reinstall the AgentOps skills: - ```bash - claude plugin marketplace add boshu2/agentops - claude plugin install agentops@agentops-marketplace - ``` - -2. Update existing skills: - ```bash - claude plugin marketplace update agentops-marketplace - claude plugin update agentops - ``` - -3. If updates seem stale, clear the cache and reinstall: - ```bash - # The skills cache lives here: - ls ~/.claude/plugins/marketplaces/agentops-marketplace/ - # Pull latest directly if marketplace update lags: - cd ~/.claude/plugins/marketplaces/agentops-marketplace/ && git pull - ``` - -4. Verify the plugin loads: - ```bash - claude --plugin ./ - ``` - -5. AgentOps 3.0 ships **zero hooks by default** — there is nothing to install for - skills to work. The workflow is guided by skills plus the `ao` CLI; the - **local cockpit Go gate (`ao gate check`) in the pre-push hook is the routine - release authority**, with `validate.yml` as a CI backstop. If you want a - bounded gate of your own (block a dangerous op, bootstrap a session, run a - parity check), author it with the `hooks-authoring` skill. - ---- - -## `br` errors or RPI falls back to tasklist mode - -> **Note (2026-06-11):** the tracker is **`br` (beads_rust)** at `_beads/`, -> invoked through the resolved private ledger path: `BEADS_DIR="$(ao beads dir)" br `. -> `bd`/Dolt is **not this repo's tracker** — it is the gascity substrate store, a -> different layer. If you came from an older guide that ran `bd`/`brew upgrade -> beads` to track this repo's work, that procedure is gone — use `br`. - -If `BEADS_DIR="$(ao beads dir)" br ready --json` fails or the legacy `.beads/` -config shadows the live `_beads/` ledger, you likely have a tracker config -mismatch. Linked worktrees normally do not contain `_beads/`; `ao beads dir` -resolves through git's common directory back to the canonical private ledger. - -**Diagnosis:** - -```bash -br --version -ao beads dir -ao session bootstrap -BEADS_DIR="$(ao beads dir)" br ready --json -BEADS_DIR="$(ao beads dir)" br list --type epic --status open --json -``` - -If commands resolve the wrong directory, confirm `BEADS_DIR` matches `ao beads dir`. -Without it, direct `br` invocations may pick up retired `.beads/` config or a -missing worktree-local `_beads/` path. - -**Fixes:** - -1. Always invoke with the explicit ledger dir until `.beads/` is retired: - ```bash - BEADS_DIR="$(ao beads dir)" br ready --json - ``` -2. Sync the ledger (it is a private nested git repo, not part of this public repo): - ```bash - git -C "$(ao beads dir)" push # never stage the private ledger from the parent repo - ``` -3. If you cannot repair the tracker immediately, Codex phased RPI degrades - honestly to tasklist mode instead of silently assuming the tracker is healthy. - That fallback is for continuity, not a substitute for repairing the tracker. - -For Codex, use `curl -fsSL https://raw.githubusercontent.com/boshu2/agentops/main/scripts/install-codex.sh | bash`. The installer enables plugins and suppresses the unstable-plugins warning in `~/.codex/config.toml`. On Linux, install system `bubblewrap` as well so Codex does not warn that it is using the vendored fallback. For OpenCode, use `curl -fsSL https://raw.githubusercontent.com/boshu2/agentops/main/scripts/install-opencode.sh | bash`. For other agents, use the platform-specific scripts in `scripts/`. - -```bash -sudo apt-get install -y bubblewrap -``` - - -**Symptoms:** - -- Running `npx update` installs an unrelated npm package and does not update skills. -- `bash <(curl -fsSL https://raw.githubusercontent.com/boshu2/agentops/main/scripts/install.sh)` reports failed skills without actionable detail. - -**Fixes:** - -1. Use the correct updater command: - ```bash - bash <(curl -fsSL https://raw.githubusercontent.com/boshu2/agentops/main/scripts/install.sh) - ``` -2. If specific skills still fail, reinstall each failed skill directly: - ```bash - bash <(curl -fsSL https://raw.githubusercontent.com/boshu2/agentops/main/scripts/install.sh) - ``` -3. Re-run update to verify a clean state: - ```bash - bash <(curl -fsSL https://raw.githubusercontent.com/boshu2/agentops/main/scripts/install.sh) - ``` - -If reinstalling one-by-one works but bulk update previously failed, the local skills lock state was stale; per-skill reinstall refreshes it. - ---- - -## Skills show up twice in Codex - -This usually means Codex is seeing AgentOps skills from more than one location. -For native-plugin installs, the active source of truth is the plugin cache under -`~/.codex/plugins/cache/.../skills-codex`. Stale copies in `~/.codex/skills` or -`~/.agents/skills` can still create duplicates if your local Codex build scans -more than one of those locations. - -**Diagnosis:** - -```bash -ao doctor -``` - -If the "Plugin" check warns about duplicate installs, inspect the active homes: - -```bash -find ~/.codex/plugins/cache/agentops-marketplace/agentops/local/skills-codex -maxdepth 1 -mindepth 1 -type d | sort -find ~/.codex/skills -maxdepth 1 -mindepth 1 -type d | sort -find ~/.agents/skills -maxdepth 1 -mindepth 1 -type d | sort -``` - -**Fix:** - -1. Reinstall so the native plugin cache is refreshed and stale raw mirrors are archived: - ```bash - curl -fsSL https://raw.githubusercontent.com/boshu2/agentops/main/scripts/install-codex.sh | bash - ``` -2. If duplicates persist, archive the stale `~/.agents/skills` copy: - ```bash - mv ~/.agents/skills ~/.agents/skills.backup.$(date +%Y%m%d-%H%M%S) - ``` -3. If duplicates still persist, archive the stale `~/.codex/skills` copy: - ```bash - mv ~/.codex/skills ~/.codex/skills.backup.$(date +%Y%m%d-%H%M%S) - ``` -4. If duplicates still persist after that, remove the compatibility plugin cache: - ```bash - rm -rf ~/.codex/plugins/cache/agentops-marketplace/agentops/local - ``` -5. Validate the runtime in a fresh session: - ```bash - bash scripts/validate-codex-cli-skills.sh - ``` -6. Restart Codex so interactive sessions reload the current skill list. -7. Re-run `ao doctor` to confirm the warning is gone. - -Keep the native plugin cache as the source of truth for native-plugin installs. -Only restore `~/.agents/skills` or `~/.codex/skills` if you intentionally want -raw-skill mode for a specific Codex build. - ---- - -## A push is rejected by the cockpit gate - -AgentOps 3.0 is **push-to-main** (branch protection is OFF). The **local cockpit -Go gate (`ao gate check`) runs in the pre-push hook and blocks the push** — it is -the routine release authority. `.github/workflows/validate.yml` is a CI backstop -on tags, PRs, and manual dispatch, not the gate on every `main` push. - -**Why it works this way:** the gate runs on your machine before the push leaves, -so a red `main` is prevented up front instead of being caught after the fact. -Rebase-on-reject (git serializes concurrent pushers); on a red `main`, fix forward. - -**Proper resolution:** - -1. Run the gate on your changes before pushing: - ```bash - ao gate check --fast --scope head - ``` - -2. Address any findings until the gate passes. - -3. Push (the pre-push hook re-runs the gate): - ```bash - git push # rebase-on-reject if a concurrent push landed first - ``` - ---- - -## Worker tried to commit - -This is expected behavior in the **lead-only commit** pattern used by `/crank` and `/swarm`. - -**How it works:** - -- Workers write files but NEVER run `git add`, `git commit`, or `git push`. -- The team lead validates all worker output, then commits once per wave. -- This prevents merge conflicts when multiple workers run in parallel. - -**If a worker accidentally committed:** - -1. The lead should review the commit before pushing. -2. Amend or squash if needed to maintain clean history. - -**For workers:** If you are a worker agent, your only job is to write files. The lead handles all git operations. - ---- - -## Phantom command error - -If you see an error for a command that is documented as planned, it does not exist yet. Designed-but-unbuilt commands are tracked in [ROADMAP.md](ROADMAP.md). - -**How to identify:** Look for `FUTURE` markers in skill documentation. These indicate commands or features that are designed but not yet implemented. - -**What to do:** - -- Do not retry the command. It will not work. -- Check the skill's `SKILL.md` for current supported commands. -- Use `br --help` to see available tracker subcommands. - ---- - -## ao doctor shows failures - -`ao doctor` runs 9 health checks. Here is how to fix each one. - -### Required checks (failures make the result UNHEALTHY) - -| Check | What it verifies | How to fix | -|-------|-----------------|------------| -| **ao CLI** | The `ao` binary is running and reports its version. | Reinstall via Homebrew, or build from `cli/` (see `cli/README.md`). | -| **Knowledge Base** | The `.agents/ao/` directory exists in the current working directory. | Run `ao init` from your project root, or verify you are in the correct directory. | -| **Plugin** | The `skills/` directory exists and contains at least one subdirectory with a `SKILL.md` file. | See [Skills not showing up](#skills-not-showing-up) above. | - -### Optional checks (warnings, result stays HEALTHY) - -| Check | What it verifies | How to fix | -|-------|-----------------|------------| -| **CLI Dependencies** | `br` is on your PATH (the beads_rust issue tracker, invoked `BEADS_DIR="$(ao beads dir)" br`). | Install `br` (beads_rust); see AGENTS.md for the tracker setup. | -| **Knowledge Freshness** | At least one recent session exists under `.agents/ao/sessions/`. | After a session, run `ao forge transcript ` to ingest it. | -| **Search Index** | A non-empty `.agents/ao/index.jsonl` exists for faster repo-local searches. | Run `ao store rebuild`. | -| **Flywheel Health** | At least one learning exists under `.agents/ao/learnings/` (or legacy `.agents/learnings/`). | Run `/retro` or `/curate --mode=forge` to extract learnings; empty is normal early on. | -| **Codex CLI** | The `codex` binary is on your PATH (optional, used for `--mixed` validation modes). | Install Codex CLI and ensure it is on PATH. | - -### Reading the output - -``` -ao doctor -───────── - ✓ ao CLI vX.Y.Z - ✓ Knowledge Base .agents/ao initialized - ✓ Plugin skills found - ! Codex CLI not found (optional — needed for --mixed council) - - 7/8 checks passed, 1 warning -``` - -- `✓` = pass -- `!` = warning (optional component missing or degraded) -- `✗` = failure (required component missing or broken) - -Use `ao doctor --json` for machine-readable output. - ---- - -## Pre-mortem gate blocks `/crank` - -The pre-mortem gate denies ambiguous state by default (as of 2.37.2). If `/crank` exits immediately with a pre-mortem error, it is telling you there is no pre-mortem artifact or the artifact is stale for the current epic. - -**Fixes:** - -1. Run `/pre-mortem` against the epic before invoking `/crank`. -2. For exploratory runs where a pre-mortem is not worth the cost: - ```bash - AGENTOPS_PREMORTEM_MODE=advisory /crank ... - ``` - This downgrades the gate to a warning. - -## Go tests fail in CI after a change - -AgentOps 3.0 ships no pre-commit hook that runs Go tests for you — verify -locally before pushing. Run the per-tool checks for the surfaces you touched: - -```bash -cd cli && make test # or: go build ./... && go vet ./... && go test ./... -``` - -If tests fail, common causes: - -- Tests that depend on network (`go test -short` typically skips these). -- A package import that fails to compile — fix compilation first, tests second. - -CI runs the omnibus validation on push; if you skip the local check, the -failure surfaces on the PR instead. - -## Context window compacted and lost work - -If a session compacts and drops critical context, re-seed it with the corpus -primitives rather than relying on an auto-snapshot hook (there is none in 3.0): - -```bash -ao session bootstrap # re-orient -ao inject "" # pull back the relevant prior context -``` - -You can also manually re-seed the session from `MEMORY.md`. - -## Getting help - -- **New to AgentOps?** Run `/quickstart` for an interactive onboarding walkthrough. -- **Run diagnostics:** `ao doctor` checks your installation health. -- **Report issues:** [github.com/boshu2/agentops/issues](https://github.com/boshu2/agentops/issues) -- **Full workflow guide:** Run `/using-agentops` for the complete RPI workflow reference. +Refresh the installation with the platform installer, then start a fresh agent +session so its skill inventory reloads. Installers remove obsolete generated +links for deleted skills. diff --git a/docs/trust-factory.md b/docs/trust-factory.md index 8cf6360ba..cb6d6eec5 100644 --- a/docs/trust-factory.md +++ b/docs/trust-factory.md @@ -1,114 +1,29 @@ --- -title: "AgentOps as a Trust Factory" -description: "How AgentOps maps to the five-step preparation engine: identity, reproducibility, evaluation, evidence, recovery." +title: "AgentOps as a Trust Boundary" +description: "How AgentOps identifies and independently judges one bounded agent-created change." permalink: /trust-factory -last_reviewed: 2026-05-07 +last_reviewed: 2026-07-14 --- -# AgentOps as a Trust Factory +# AgentOps as a Trust Boundary -> Generation is becoming cheap. Validation is becoming the moat. Software -> engineering has spent 50 years building trust factories — the systems that -> turn changed artifacts into trusted capability. AgentOps is that pattern -> applied to the artifacts AI coding agents produce. +Generation is stochastic. AgentOps adds a small, inspectable boundary between +an agent's claim and a caller's decision to rely on it. -A trust factory is the discipline that sits between *something changed* and -*we believe it*. Compilers, build systems, code review, CI, staging, canaries, -post-mortems — none of them generate value by themselves. They generate -*permission to ship*. As coding agents commoditize generation, the part of the -loop that earns permission is what compounds. +The boundary has four facts: - +- the requested behavior and acceptance are explicit; +- the exact subject is identified by content, not by a mutable branch name; +- a distinct fresh context judges that subject against the acceptance; +- the result is written as a standalone content-addressed `verdict.v2`. -## The five-step primitive +This is evidence, not permission to ship. AgentOps does not own Git, CI, +tracking, retries, queues, release, or recovery. The consumer system decides +what to do after PASS, FAIL, or NOT_PROVEN. -Every artifact promotion needs five things, in order: +Optional Premortem, Council, and multi-model strategies can strengthen the +judgment selected by the caller. Optional Learn analysis can study collections +of past verdicts later. Neither changes the core lifecycle. -1. **Identity** — what changed? -2. **Reproducibility** — can we replay it? -3. **Evaluation** — what did it pass? -4. **Evidence** — where is the proof? -5. **Recovery** — what if we were wrong? - -Drop any one of them and trust collapses. Identity without reproducibility is -a name with no body. Evaluation without evidence is a claim no one can audit. -Evidence without recovery is a museum. The five together are what turn a code -change, a model weight, a config, or a mission plan into something an -organization is willing to stand behind. - -## How AgentOps maps to the primitive - -| Trust factory step | AgentOps mechanism | -|---|---| -| Identity | `.agents/runs//` packets, citations, versioned context | -| Reproducibility | RPI phase contracts + worktree isolation + captured packets | -| Evaluation | `/pre-mortem`, `/vibe`, `/council`, `ao goals measure` | -| Evidence | Council verdicts, citations, ratchet records, post-mortems | -| Recovery | Ratchet rollback, learnings → planning rules → prevention | - -**Identity.** Every agent run gets a discovery packet under `.agents/runs//` -that records what was being changed, which corpus entries were injected, and -which sources were cited. The `ao inject` retrieval that loaded the working -context is logged so a later reviewer can ask *what did the agent actually -know when it acted?* without guessing. - -**Reproducibility.** The RPI workflow (Research → Plan → Implement → Validate) -runs each phase against a written contract, and the `/crank` skill isolates -parallel work in worktrees so the inputs to a phase can be replayed. The packet captured -at planning time is the same packet a re-run consumes; deviations show up as -diffs against a known input, not as drift. - -**Evaluation.** Plans are stress-tested with `/pre-mortem` before code is -written, code is challenged with `/vibe` before it leaves the working tree, and -high-stakes changes go through `/council` for multi-judge consensus. Strategic -fitness is checked separately with `ao goals measure`, which asserts that the -change actually moves the GOALS.md needles instead of merely passing tests. - -**Evidence.** Council verdicts, citation logs, ratchet records, and the -findings written by `/post-mortem` are durable artifacts under `.agents/`. They -stay diffable in git, so the proof of why a change was promoted lives next to -the change itself, not in a SaaS tool that the next vendor can take away. - -**Recovery.** When a change turns out to have been wrong, the ratchet record -is the rollback unit, and the failure is converted by `/post-mortem` and -`/curate --mode=forge` into a learning, then into a planning rule that prevents the same -class of mistake from being injected again. The loop is closed inside the -corpus, not inside a chat transcript. - -## Why this primitive generalizes - -The same five steps apply to anything that gets promoted: model weights, -mission plans, agent actions, infrastructure configs, training data. Identity, -reproducibility, evaluation, evidence, and recovery are how regulated -industries already think about change — they just call it configuration -management, model risk management, or operational acceptance depending on the -substrate. - -AgentOps is not the trust factory for all of those substrates. It is the first -instance of the pattern *for the artifacts AI coding agents produce*: code -diffs, learnings, planning rules, skill changes, corpus updates. The pattern -travels; the implementation is scoped. As other artifact classes get their own -agent-shaped pipelines, the same five-step primitive is what each will need. - -## When to use this framing vs. the wiki framing - -The [wiki framing](./wiki-for-agents.md) is the right opener for the busy -engineer who responds to deflationary framing — "it's a wiki for your agents, -version-controlled in your repo." It earns trust by under-claiming. - -The trust-factory framing is the right opener for engineering leads in -regulated environments who already think in terms of identity, reproducibility, -evaluation, evidence, and recovery. They recognize the vocabulary and skip the -explanation tax. Lead with this framing for buyers who already know that -"validation is the moat"; lead with the wiki framing for buyers who want to -see the artifact before the philosophy. - -Both framings describe the same product. They optimize for different listeners. - -## See also - -- [README](https://github.com/boshu2/agentops/blob/main/README.md) — product-level framing and dogfood receipts -- [PRODUCT.md](https://github.com/boshu2/agentops/blob/main/PRODUCT.md) — internal positioning and four-layer model -- [docs/wiki-for-agents.md](./wiki-for-agents.md) — the wiki-framing companion -- [docs/cdlc.md](./cdlc.md) — the Context Development Life Cycle in full -- [docs/the-science.md](./the-science.md) — knowledge-decay and compounding model +See [PRODUCT.md](../PRODUCT.md) and the +[operating loop](architecture/operating-loop.md). diff --git a/docs/wiki-for-agents.md b/docs/wiki-for-agents.md index cb82de34a..a421bf873 100644 --- a/docs/wiki-for-agents.md +++ b/docs/wiki-for-agents.md @@ -1,95 +1,23 @@ --- -title: "A wiki for your agents" -description: ".agents/ is just markdown in your repo, version-controlled with your code, that agents read and contribute to." +title: "Evidence your agents can read" +description: "AgentOps packets and verdicts are portable files, not a hosted control plane." permalink: /wiki-for-agents -last_reviewed: 2026-05-07 +last_reviewed: 2026-07-14 --- -# A wiki for your agents +# Evidence your agents can read -> Wikis aren't new. They've been around for 25 years. The novel part is building one for agents to read, traverse, and contribute to — and making the discipline of maintenance mechanical so it actually happens. +AgentOps produces ordinary structured files: plans, candidates, content +manifests, evidence references, and verdicts. They can be inspected by people, +passed to another fresh context, retained under a repository's policy, or +exported into an external assurance system. -Every engineer has used a wiki. MediaWiki, Confluence, Notion, a `docs/` directory that grew teeth. AgentOps is that shape, with two changes: the readers are agents, and the writers are agents too. +The core does not maintain an automatic wiki or knowledge corpus. Optional +specialist tools may search prior material, and an explicitly invoked Learn +step may analyze verdict collections later. Those capabilities remain outside +RPI correctness and never decide whether current work is valid. - +This boundary keeps the durable asset portable without turning AgentOps into a +hosted control plane, tracker, queue, or delivery system. -## What `.agents/` actually is - -Run `ls .agents/` in an initialized repo. Plain directories of plain markdown: - -``` -.agents/ -├── council/ # multi-judge verdicts on plans, PRs, releases -├── decisions/ # what was chosen, what was rejected, why -├── findings/ # surprises and gotchas surfaced by sessions -├── learnings/ # promoted lessons that survived review -├── patterns/ # recurring shapes worth reusing -├── planning-rules/ # constraints that fire during planning -└── runs/ # per-session packets with citations and evidence -``` - -`tree .agents/ -L 1` shows the rest. Every leaf is a markdown file. The whole tree is committed alongside your code: version-controlled, diffable, branchable, mergeable. Nothing about it is exotic. That is the point. - -## Why most wikis fail - -Writing the wiki is a tax humans pay grudgingly, after the work is already done. The tax falls hardest on the contributors who learned the most — the ones whose attention is most valuable elsewhere. - -So the wiki bitrots. Pages drift behind the code. The "current architecture" page describes a system that was rewritten last quarter. New hires read it once, find it lying, and stop trusting it. - -By week four it is a read-only artifact. Search breaks because no one tags. The team falls back to Slack scrollback and the one engineer who remembers everything. - -AgentOps inverts this. Sessions write to the wiki by default — runs land citations, councils land verdicts, post-mortems land learnings, and an out-of-session substrate (NTM + Agent Mail) defrags overnight. The agents that consume `.agents/` also produce it. Maintenance is mechanical, not voluntary. The wiki maintains itself because it sits on the path of everything else. - -## Why not Notion or Confluence? - -| Notion / Confluence | AgentOps `.agents/` | -|---|---| -| Written for humans; agents can't traverse it efficiently | LLM Wiki of Markdown — agents read it natively | -| Lives in SaaS, not your repo | Lives in `.agents/` next to the code | -| Not version-controlled with your code | Diffable, branchable, mergeable | -| No decay ranking, no retrieval scoring | `ao inject` returns decay-ranked, token-budgeted packets | -| No validation gates, no automated capture | Sessions write to it automatically; councils validate it | -| Doesn't compound; you maintain it manually | An out-of-session substrate (NTM + Agent Mail) defrags, evolves, and compounds it overnight | -| Read-only artifact | Writes itself: agents that use it also produce it | - -**Native agent traversal.** Markdown is the format models read best; a SaaS page has to be exported and re-embedded before an agent can use it. - -**Locality.** "What changed in this PR" includes the wiki delta. Reviewers see context move with the code instead of switching tabs. - -**Version control.** Diffs, branches, and merges are how engineers already think about change. A wiki you can `git revert` is a wiki you can trust. - -**Decay-ranked retrieval.** `ao inject "..."` returns a token-budgeted packet weighted by recency, citations, and validation history — different from full-text search. - -**Automated capture.** Sessions write run packets, citations, and verdicts without anyone being asked. The discipline lives in the tooling. - -**Always-on compounding.** Run defrag, evolve, compile, and dream against the corpus out of session by handing the loop to an orchestration substrate — the reference is NTM + MCP + managed-agents — and the corpus compounds while you sleep. A SaaS wiki cannot get smarter on its own. - -**Self-writing.** Every consumer of the wiki is also a producer of it. That is the inversion that makes the whole thing tractable. - -## How to start - -Install for your runtime: - -```bash -# Claude Code -claude plugin install agentops@agentops-marketplace - -# Codex CLI -curl -fsSL https://raw.githubusercontent.com/boshu2/agentops/main/scripts/install-codex.sh | bash -``` - -Then, inside any repo: - -```bash -ao quick-start # seed .agents/, GOALS.md, AgentOps instructions -``` - -That is the entry point. After that, work with your agents normally — `/rpi`, `/council`, `/research`, `/vibe`. `.agents/` accumulates without explicit attention. `ao inject "..."` returns a decay-ranked, token-budgeted packet whenever you ask. - -## What you own at the end - -The wiki is yours. Open source forever. Built so you own the asset, not the tool. - -`.agents/` is plain markdown in your repo. If a frontier vendor ships native equivalents next year, the corpus carries forward. If AgentOps changes direction, your corpus is yours. If you outgrow this tool entirely, fork it, customize it, replace it — the corpus is what matters. - -For the deeper view of how the same mechanism doubles as a trust factory — identity, reproducibility, evaluation, evidence, recovery — see [trust-factory.md](./trust-factory.md). For the full README, see [the project README on GitHub](https://github.com/boshu2/agentops/blob/main/README.md). +See [the trust boundary](trust-factory.md) and [PRODUCT.md](../PRODUCT.md). diff --git a/docs/workflows/README.md b/docs/workflows/README.md deleted file mode 100644 index a55402dd2..000000000 --- a/docs/workflows/README.md +++ /dev/null @@ -1,130 +0,0 @@ -# Workflow Guide - -> Which workflow should I use? Find the right one in seconds. - -## Golden Paths - -Use these as the default user journeys before reaching for the full decision -tree. - -| Situation | Start with | Continue with | -|-----------|------------|---------------| -| First repo setup | `ao quick-start` | `/bootstrap`, then `/rpi "a small goal"` | -| Small, obvious change | [Quick Fix](quick-fix.md) | `/implement`, then `/validate --quick recent` | -| New feature or broad change | [Complete Cycle](complete-cycle.md) | `/rpi "goal"` or `/plan` -> `/crank ` | -| Existing tracked backlog | `ao reconcile --json`, then `BEADS_DIR="$(ao beads dir)" br ready` | `/implement ` or `/crank ` | -| Review-only checkpoint | `/council validate this PR` | `/validate recent` before merge | -| Terminal-native operation | `ao session bootstrap` | `ao lookup --query ""`, then drive the operating loop and `ao gate check --fast --scope head` | - -## Decision Tree - -| I want to... | Use this workflow | Complexity | Key Skills | -|--------------|-------------------|------------|------------| -| Fix a simple bug or make a quick change | [Quick Fix](quick-fix.md) | Low | /implement | -| Investigate and fix a hard bug | [Debug Cycle](debug-cycle.md) | Medium | /bug-hunt, /implement, /retro | -| Build a new feature end-to-end | [Complete Cycle](complete-cycle.md) | High | /research, /plan, /crank, /vibe | -| Deploy infrastructure changes | [Infrastructure Deployment](infrastructure-deployment.md) | High | /plan, /implement, /vibe | -| Work across multiple domains at once | [Multi-Domain](multi-domain.md) | High | /plan, /swarm | -| Validate research assumptions before planning | [Assumption Validation](assumption-validation.md) | Medium | /research | -| Understand a new codebase or extract patterns | [Knowledge Synthesis](knowledge-synthesis.md) | Medium | /research, /knowledge-activation | -| Run a retrospective after completing work | [Post-Work Retro](post-work-retro.md) | Low | /retro, /post-mortem | -| Improve system quality over time | [Continuous Improvement](continuous-improvement.md) | Medium | /retro, /vibe | -| Manage a session from start to finish | [Session Lifecycle](session-lifecycle.md) | Low | (natural language) | -| Coordinate N autonomous workers without a leader | [Meta-Observer Pattern](meta-observer-pattern.md) | High | /swarm | - -## Workflows - -### [Quick Fix](quick-fix.md) -**When to use:** Simple, low-risk changes to 1-2 files with an obvious solution. -**Complexity:** Low (10-30 min, single session) -**Key skills:** /implement - -### [Debug Cycle](debug-cycle.md) -**When to use:** Production incidents, intermittent failures, performance issues, or integration bugs with unknown root cause. -**Complexity:** Medium-High (1-3 hours, 1-3 sessions) -**Key skills:** /bug-hunt, /implement, /retro - -### [Complete Cycle](complete-cycle.md) -**When to use:** New features, complex multi-file changes, or architectural work that needs full Research-Plan-Implement-Validate-Learn phases. -**Complexity:** High (2-4 hours, 2-4 sessions) -**Key skills:** /research, /plan, /crank, /vibe, /retro - -### [Infrastructure Deployment](infrastructure-deployment.md) -**When to use:** Infrastructure deployments that need validation gates, reality checks, and tracer bullets at every phase. -**Complexity:** High (multi-session) -**Key skills:** /plan, /implement, /vibe - -### [Multi-Domain](multi-domain.md) -**When to use:** Work spanning multiple domains simultaneously (backend + frontend + infra, technical + personal tracking). -**Complexity:** High (multi-session) -**Key skills:** /plan, /swarm - -### [Assumption Validation](assumption-validation.md) -**When to use:** Before planning, to verify that research findings match reality (APIs exist, images available, operators behave as expected). -**Complexity:** Medium -**Key skills:** /research - -### [Knowledge Synthesis](knowledge-synthesis.md) -**When to use:** Onboarding to a new codebase, creating comprehensive docs, extracting patterns from multiple implementations. -**Complexity:** Medium (30-60 min, single session) -**Key skills:** /research, /knowledge-activation - -### [Post-Work Retro](post-work-retro.md) -**When to use:** After completing significant work, to capture learnings and identify improvements. -**Complexity:** Low -**Key skills:** /retro, /post-mortem - -### [Continuous Improvement](continuous-improvement.md) -**When to use:** Periodic reviews (weekly/monthly), after milestones, technical debt reduction, or pattern refinement. -**Complexity:** Medium (ongoing, periodic) -**Key skills:** /retro, /vibe - -### [Session Lifecycle](session-lifecycle.md) -**When to use:** Managing any working session from start to finish -- starting work, resuming context, wrapping up. -**Complexity:** Low (use natural language) -**Key skills:** Just talk naturally; commands are optional. - -### [Meta-Observer Pattern](meta-observer-pattern.md) -**When to use:** Coordinating N autonomous workers through shared memory (stigmergy) without central orchestration. -**Complexity:** High (advanced, multi-session) -**Key skills:** /swarm -**Deep dive:** [meta-observer/](meta-observer/README.md) (pattern guide, examples, showcase) - -## Flowchart - -``` -Start here - | - v -Is it a simple, obvious fix? ----YES----> Quick Fix - | - NO - | - v -Is something broken? ----YES----> Debug Cycle - | - NO - | - v -Am I deploying infrastructure? ----YES----> Infrastructure Deployment - | - NO - | - v -Does it span multiple domains? ----YES----> Multi-Domain - | - NO - | - v -Am I building something new? ----YES----> Complete Cycle - | - NO - | - v -Am I trying to understand code/patterns? ----YES----> Knowledge Synthesis - | - NO - | - v -Am I wrapping up or improving? ----YES----> Post-Work Retro / Continuous Improvement -``` diff --git a/docs/workflows/assumption-validation.md b/docs/workflows/assumption-validation.md deleted file mode 100644 index f901e75f6..000000000 --- a/docs/workflows/assumption-validation.md +++ /dev/null @@ -1,538 +0,0 @@ -# Assumption Validation Workflow - -**Purpose:** Systematically validate research assumptions before planning - -**Composes:** `cluster-reality-check` -> `divergence-check` - -**Failure Patterns Prevented:** 1 (Tests Passing Lie), 3 (Copy-Pasta Blindspot), 9 (External Dependency Assumption) - ---- - -## Overview - -This workflow validates that research findings match reality before planning begins. - -``` -Extract Assumptions from Research - ↓ -Invoke cluster-reality-check (APIs, images, operators) - ↓ -Invoke divergence-check (upstream vs local) - ↓ -Combine Results - ↓ -Gate Decision: All pass? Proceed to /plan : Return to /research -``` - ---- - -## When to Use - -- After `/research` completes for infrastructure work -- Before `/plan` starts -- When research used external documentation -- When deploying to unfamiliar cluster -- When using operators with version-specific behavior - -## When NOT to Use - -- Code-only changes (no infrastructure) -- Well-known patterns you've deployed many times -- Trivial configuration updates -- When assumptions already validated in previous session - ---- - -## Inputs - -| Input | Source | Description | -|-------|--------|-------------| -| Research bundle | `/research` output | Findings with assumptions | -| Target cluster | Environment | Where deployment will happen | -| Upstream docs | Research references | External documentation used | - ---- - -## Process - -### Step 1: Extract Assumptions from Research - -Parse research findings to identify: - -```yaml -# assumptions.yaml - -api_assumptions: - - name: "EDB Cluster API" - api_group: "postgresql.k8s.enterprisedb.io" - version: "v1" - kind: "Cluster" - source: "EDB documentation" - - - name: "Redis API" - api_group: "apps" - version: "v1" - kind: "Deployment" - source: "Kubernetes standard" - -image_assumptions: - - name: "Dify API" - image: "langgenius/dify-api:0.11.1" - registry: "docker.io" - source: "Dify documentation" - - - name: "Dify Web" - image: "langgenius/dify-web:0.11.1" - registry: "docker.io" - source: "Dify documentation" - -operator_assumptions: - - name: "EDB Postgres" - csv_pattern: "edb-pg4k" - expected_status: "Succeeded" - source: "Cluster inventory" - -configuration_assumptions: - - name: "PostgreSQL parameters" - parameter: "shared_preload_libraries" - expected: "Configurable" - source: "EDB documentation" - -feature_assumptions: - - name: "Image catalog" - feature: "imageCatalogRef" - expected: "Available" - source: "EDB v1.23 docs" -``` - -### Step 2: Invoke cluster-reality-check - -Test each assumption against actual cluster: - -```markdown -# Invoke skill -cluster-reality-check: - assumed_apis: - - postgresql.k8s.enterprisedb.io/v1/Cluster - - apps/v1/Deployment - planned_images: - - langgenius/dify-api:0.11.1 - - langgenius/dify-web:0.11.1 - operators: - - edb-pg4k - namespace: dify -``` - -**Capture results:** - -```yaml -reality_check_results: - apis: - - name: "EDB Cluster API" - status: "PASS" - evidence: "oc api-resources shows clusters.postgresql.k8s.enterprisedb.io" - - - name: "Deployment API" - status: "PASS" - evidence: "Standard Kubernetes API" - - images: - - name: "Dify API" - status: "FAIL" - evidence: "ImagePullBackOff - signature policy" - error: "image rejected by signature policy" - - - name: "Dify Web" - status: "FAIL" - evidence: "ImagePullBackOff - signature policy" - - operators: - - name: "EDB Postgres" - status: "PASS" - evidence: "edb-pg4k.v1.23.0 in Succeeded state" -``` - -### Step 3: Invoke divergence-check - -Compare upstream documentation to local reality: - -```markdown -# Invoke skill -divergence-check: - upstream_source: "Dify Docker Compose documentation" - local_environment: "OpenShift 4.14" - features_to_verify: - - Container images - - Volume mounts - - Networking model - - Environment configuration - -divergence-check: - upstream_source: "EDB CloudNativePG v1.24 docs" - local_environment: "EDB Postgres v1.23 on OpenShift" - features_to_verify: - - imageCatalogRef feature - - Monitoring configuration - - Backup options -``` - -**Capture results:** - -```yaml -divergence_check_results: - dify_docs: - - feature: "Image registry" - upstream: "docker.io" - local: "Signature policy blocks" - divergence: "HIGH" - adjustment: "Mirror to internal registry" - - - feature: "Volume mounts" - upstream: "Docker named volumes" - local: "PVC required" - divergence: "MEDIUM" - adjustment: "Convert to PVC specs" - - edb_docs: - - feature: "imageCatalogRef" - upstream: "v1.24 - supported" - local: "v1.23 - not available" - divergence: "HIGH" - adjustment: "Use imageName instead" - - - feature: "Monitoring" - upstream: "managed.monitoring: true" - local: "Manual annotation required" - divergence: "LOW" - adjustment: "Add prometheus annotations" -``` - -### Step 4: Combine Results - -Build unified validation report: - -```markdown -# Assumption Validation Report - -## Summary -| Category | Total | Pass | Fail | Diverge | -|----------|-------|------|------|---------| -| APIs | 2 | 2 | 0 | 0 | -| Images | 2 | 0 | 2 | 0 | -| Operators | 1 | 1 | 0 | 0 | -| Divergences | 4 | - | 2 HIGH | 2 LOW | - -## Detailed Results - -### APIs Validated -| API | Status | Evidence | -|-----|--------|----------| -| EDB Cluster | PASS | oc api-resources confirms | -| Deployment | PASS | Standard K8s API | - -### Images Validated -| Image | Status | Issue | Adjustment | -|-------|--------|-------|------------| -| dify-api | FAIL | Signature policy | Mirror to internal | -| dify-web | FAIL | Signature policy | Mirror to internal | - -### Operators Validated -| Operator | Status | Version | Evidence | -|----------|--------|---------|----------| -| EDB Postgres | PASS | v1.23.0 | CSV Succeeded | - -### Divergences Found -| Feature | Severity | Upstream | Local | Adjustment | -|---------|----------|----------|-------|------------| -| Image registry | HIGH | docker.io | Blocked | Mirror images | -| imageCatalogRef | HIGH | v1.24 | v1.23 | Use imageName | -| Volume model | MEDIUM | Docker | PVC | Convert specs | -| Monitoring | LOW | managed | Manual | Add annotations | - -## Required Adjustments -1. [HIGH] Mirror Dify images to internal registry -2. [HIGH] Use imageName instead of imageCatalogRef for EDB -3. [MEDIUM] Convert Docker volumes to PVC specifications -4. [LOW] Add Prometheus annotations for monitoring -``` - -### Step 5: Gate Decision - -```markdown -## Gate Decision - -| Condition | Status | -|-----------|--------| -| All APIs validated | PASS | -| All images validated | FAIL | -| All operators validated | PASS | -| No HIGH divergences | FAIL | - -### Decision: DO NOT PROCEED TO PLANNING - -Blocking Issues: -1. Dify images cannot be pulled (signature policy) -2. EDB imageCatalogRef not available in installed version - -### Required Actions Before Proceeding: -1. Request signature policy exception for Dify images - OR mirror images to internal registry -2. Update research to use imageName instead of imageCatalogRef - -### Next Step: -Return to /research with these constraints: -- Images must come from internal registry or allowed list -- EDB configuration must use v1.23 features only -``` - ---- - -## Output Formats - -### On All Pass - -``` -ASSUMPTION VALIDATION: PASS - -All assumptions validated against cluster reality. -No divergences require adjustment. - -Validated: -- 5 APIs confirmed available -- 3 images confirmed pullable -- 2 operators confirmed ready -- 0 divergences with upstream docs - -Go/No-Go: PROCEED to /plan -``` - -### On Failures Found - -``` -ASSUMPTION VALIDATION: FAIL - -Validation Results: -- APIs: 5/5 PASS -- Images: 1/3 FAIL -- Operators: 2/2 PASS -- Divergences: 2 HIGH, 1 MEDIUM - -Blocking Issues: -1. [Image] dify-sandbox:0.2.10 - signature policy blocks -2. [Divergence] imageCatalogRef not in v1.23 - -Required Actions: -1. Mirror dify-sandbox image -2. Research v1.23 image specification syntax - -Go/No-Go: DO NOT PROCEED - return to /research -``` - ---- - -## Integration - -### With /research - -```markdown -# Research workflow integration - -1. /research produces findings -2. Invoke assumption-validation workflow -3. If PASS: proceed to /plan -4. If FAIL: continue /research with constraints -``` - -### With /plan - -```markdown -# Planning integration - -Only invoke /plan when assumption-validation passes. - -If you skip assumption-validation: -- Plan may be based on invalid assumptions -- Implementation will fail at phase gates -- Time wasted on invalid plan -``` - -### With infrastructure-deployment Workflow - -```markdown -# assumption-validation is Phase R of infrastructure-deployment - -infrastructure-deployment: - Phase R: - - /research - - assumption-validation # This workflow - - Gate R - Phase 0: tracer-bullets - ... -``` - ---- - -## Examples - -### Example 1: Simple Validation (All Pass) - -**Context:** Deploying well-known pattern to familiar cluster - -```yaml -assumptions: - apis: - - apps/v1/Deployment - - v1/Service - - route.openshift.io/v1/Route - images: - - registry.internal/app:v1.2.3 # Internal image - operators: [] # No operators needed -``` - -**Result:** -``` -ASSUMPTION VALIDATION: PASS -All 4 assumptions validated. -Proceed to planning. -``` - -### Example 2: Partial Failure - -**Context:** New application with external dependencies - -```yaml -assumptions: - apis: - - postgresql.k8s.enterprisedb.io/v1/Cluster - - redis.redis.io/v1/Redis # Doesn't exist! - images: - - langgenius/dify-api:0.11.1 # Blocked - operators: - - edb-pg4k # Installed - - redis-operator # Not installed! -``` - -**Result:** -``` -ASSUMPTION VALIDATION: FAIL - -Failed Assumptions: -1. [API] redis.redis.io/v1/Redis - CRD not found -2. [Image] langgenius/dify-api - signature blocked -3. [Operator] redis-operator - not installed - -Actions: -1. Install Redis operator OR use StatefulSet instead -2. Mirror Dify images to internal registry - -Return to research with constraints. -``` - -### Example 3: Divergence Only - -**Context:** APIs exist but docs don't match reality - -```yaml -assumptions: - apis: - - postgresql.k8s.enterprisedb.io/v1/Cluster # EXISTS - images: [] # Operator manages - operators: - - edb-pg4k # Installed - -divergences: - - feature: "imageCatalogRef" - upstream_doc: "v1.24 supports imageCatalogRef" - local_reality: "v1.23 installed, feature not available" -``` - -**Result:** -``` -ASSUMPTION VALIDATION: FAIL - -APIs/Images/Operators: All PASS -Divergences: 1 HIGH - -HIGH Divergence: -- imageCatalogRef documented in v1.24 -- Local cluster has v1.23 -- Feature not available - -Action: -Update research to use v1.23 syntax (imageName) - -Return to research with version constraint. -``` - ---- - -## Quick Reference Checklist - -### Before Starting -- [ ] Research bundle available -- [ ] Target cluster accessible -- [ ] Upstream documentation identified - -### During Validation -- [ ] All APIs checked -- [ ] All images tested -- [ ] All operators verified -- [ ] Divergences documented - -### Gate Decision -- [ ] Zero FAIL status -- [ ] Zero HIGH divergences -- [ ] Required adjustments documented -- [ ] Clear proceed/return decision - ---- - -## Anti-Patterns - -### Skipping Validation - -```markdown -# BAD -/research -/plan # Skipped assumption-validation! -``` - -**Result:** Plan based on invalid assumptions, fails during implementation. - -### Partial Validation - -```markdown -# BAD: Only checking APIs -cluster-reality-check with apis only -# Skipped images, operators, divergences -``` - -**Result:** Image pull failures discovered during implementation. - -### Ignoring Divergences - -```markdown -# BAD: Proceeding despite HIGH divergence -divergence-check shows HIGH: imageCatalogRef not available -"Let's try anyway" -``` - -**Result:** Admission webhook rejects spec, debug spiral. - ---- - -## Success Criteria - -Assumption validation is successful when: - -- [ ] All API assumptions verified -- [ ] All image assumptions verified -- [ ] All operator assumptions verified -- [ ] All divergences documented with severity -- [ ] No unresolved HIGH severity issues -- [ ] Clear go/no-go recommendation -- [ ] Required adjustments actionable - ---- - -**Remember:** The cost of 10 minutes validating assumptions is far less than hours debugging why "working" code fails in your environment. diff --git a/docs/workflows/continuous-improvement.md b/docs/workflows/continuous-improvement.md deleted file mode 100644 index 2338eca79..000000000 --- a/docs/workflows/continuous-improvement.md +++ /dev/null @@ -1,133 +0,0 @@ ---- -name: continuous-improvement -description: Ongoing system optimization and pattern refinement -estimated_time: Ongoing (periodic) -phases: 4 ---- - -# Continuous Improvement Workflow - -**Purpose:** Systematically improve system quality over time - -**When to use:** -- Weekly/monthly reviews (extract learnings) -- After major milestones (retrospective) -- Pattern refinement (improve reusable patterns) -- Technical debt reduction (systematic cleanup) - -**Token budget:** 30-50k tokens per cycle - ---- - -## Workflow Phases - -``` -Phase 1: Review (10-15% context) - ↓ -Phase 2: Identify (5-10% context) - ↓ -Phase 3: Prioritize (5-10% context) - ↓ -Phase 4: Improve (10-15% context) -``` - ---- - -## Phase 1: Review - -**Goal:** Assess recent work - -**Commands:** -```bash -/retro --retrospective --since "30 days ago" -``` - -**Activities:** -- Review git commits -- Analyze patterns used -- Identify repeated problems -- Note what worked well - -**Output:** Review summary - ---- - -## Phase 2: Identify - -**Goal:** Find improvement opportunities - -**Activities:** -- Code quality issues -- Documentation gaps -- Process inefficiencies -- Pattern opportunities - -**Output:** Improvement candidates list - ---- - -## Phase 3: Prioritize - -**Goal:** Rank by impact and effort - -**Matrix:** -``` -High Impact, Low Effort → DO FIRST -High Impact, High Effort → PLAN -Low Impact, Low Effort → MAYBE -Low Impact, High Effort → SKIP -``` - -**Output:** Prioritized improvement backlog - ---- - -## Phase 4: Improve - -**Goal:** Execute improvements - -**Commands:** -```bash -# For each improvement: -/plan [improvement] -/implement [improvement]-plan -/vibe recent -/retro --quick [improvement] -``` - -**Output:** Improved system + new patterns - ---- - -## Example: Monthly Improvement Cycle - -```bash -# Phase 1: Review -/retro --retrospective --since "2025-10-01" -# Findings: -# - 15 commits reviewed -# - 3 patterns emerged (used 5+ times each) -# - 2 repeated issues (test failures, linting) - -# Phase 2: Identify -# Opportunities: -# 1. Extract 3 patterns to pattern catalog -# 2. Fix recurring test flakiness -# 3. Add pre-commit hook for linting - -# Phase 3: Prioritize -# Priority 1: Pre-commit hook (high impact, low effort) -# Priority 2: Fix test flakiness (high impact, medium effort) -# Priority 3: Extract patterns (medium impact, low effort) - -# Phase 4: Improve -/plan pre-commit-hook -/implement pre-commit-hook-plan -/vibe recent -/retro --quick pre-commit-improvement -# Result: 90% reduction in linting issues -``` - ---- - -**Start continuous improvement with:** `/post-mortem --retrospective` diff --git a/docs/workflows/debug-cycle.md b/docs/workflows/debug-cycle.md deleted file mode 100644 index c998f69a1..000000000 --- a/docs/workflows/debug-cycle.md +++ /dev/null @@ -1,143 +0,0 @@ ---- -name: debug-cycle -description: Systematic debugging from symptoms to root cause to fix -estimated_time: 1-3 hours -phases: 4 ---- - -# Debug Cycle Workflow - -**Purpose:** Systematic investigation and resolution of bugs or issues - -**When to use:** -- Production incidents (unknown root cause) -- Intermittent failures (hard to reproduce) -- Performance degradation (needs profiling) -- Integration issues (multiple components) - -**Token budget:** 60-120k tokens across 1-3 sessions - ---- - -## Workflow Phases - -``` -Phase 1: Isolate (10-20% context) - ↓ -Phase 2: Locate (15-25% context) - ↓ -Phase 3: Fix (15-25% context) - ↓ -Phase 4: Verify & Learn (10-15% context) -``` - ---- - -## Phase 1: Isolate - -**Goal:** Reproduce the issue and isolate the component - -**Commands:** -```bash -Read CLAUDE.md -/research "isolate [symptom]" -``` - -**Activities:** -- Reproduce the issue -- Identify affected components -- Gather logs, metrics, traces -- Narrow down scope - -**Output:** Isolation report (which component/function) - ---- - -## Phase 2: Locate - -**Goal:** Find exact root cause - -**Commands:** -```bash -/research "locate root cause in [component]" -# Use history-explorer agent for git blame, past fixes -``` - -**Activities:** -- Read relevant code -- Trace execution path -- Check git history for related changes -- Identify the exact bug location - -**Output:** Root cause identification (file:line) - ---- - -## Phase 3: Fix - -**Goal:** Implement fix - -**Commands:** -```bash -/plan [issue]-root-cause -/implement [issue]-plan -``` - -**Activities:** -- Design fix (may be simple or complex) -- Implement fix -- Add tests to prevent regression -- Validate fix resolves issue - -**Output:** Working fix + regression test - ---- - -## Phase 4: Verify & Learn - -**Goal:** Ensure fix works and extract learning - -**Commands:** -```bash -/vibe recent -/retro --quick [issue]-fix -``` - -**Activities:** -- Full validation -- Deploy to staging/production -- Monitor for recurrence -- Extract debugging pattern - -**Output:** Validated fix + debugging pattern - ---- - -## Example: Debug Auth Failures - -```bash -# Phase 1: Isolate -Read CLAUDE.md -/research "isolate intermittent auth failures" -# Finding: Failures at 5pm daily, connection timeouts -# Component: Redis connection pool - -# Phase 2: Locate -/research "locate root cause in Redis pool" -# Root cause: config/redis.yaml:15 - pool_size: 10 (too small) - -# Phase 3: Fix -/plan redis-pool-fix -/implement redis-pool-fix -# Fix: Increase pool size to 100, add health checks - -# Phase 4: Verify -/vibe recent -# ✅ Load test: No failures at 3x traffic -/retro --quick redis-pool-debugging -# Pattern: Intermittent failures + time correlation = resource exhaustion -``` - ---- - -**Start debug cycle with:** `Read CLAUDE.md` → describe symptoms diff --git a/docs/workflows/infrastructure-deployment.md b/docs/workflows/infrastructure-deployment.md deleted file mode 100644 index 3a0df83d7..000000000 --- a/docs/workflows/infrastructure-deployment.md +++ /dev/null @@ -1,644 +0,0 @@ -# Infrastructure Deployment Workflow - -**Purpose:** Orchestrate infrastructure deployment with validation gates at every phase - -**Composes:** `cluster-reality-check` -> `tracer-bullet` -> `phase-gate` - -**Failure Patterns Prevented:** 1, 3, 4, 5, 9, 11 - ---- - -## Overview - -This workflow ensures infrastructure deployments succeed by: -1. Validating assumptions before planning -2. Testing critical paths with tracer bullets -3. Gating each phase before proceeding -4. Capturing learnings for future improvements - -``` -Phase R: Research with Reality Check - ↓ (Gate: All APIs/images verified?) -Phase 0: Tracer Bullets - ↓ (Gate: All bullets pass?) -Phase P: Planning - ↓ (Gate: Plan approved?) -Phase I: Implementation with Gates - ↓ (Gate: Each phase validated?) -Phase V: Validation & Retrospective -``` - ---- - -## Prerequisites - -Before starting this workflow: - -- [ ] Fresh context window (<20% used) -- [ ] Target cluster accessible (`oc whoami` works) -- [ ] Appropriate permissions (can create resources in namespace) -- [ ] Clear understanding of what you're deploying -- [ ] Time budget established (deployment can take 30min - 2h) - ---- - -## Phase R: Research with Reality Check - -**Goal:** Understand what you're deploying AND validate assumptions against cluster - -**Time Budget:** 20-40% of total time - -### Step R.1: Conduct Research - -```markdown -# Use /research command -/research "Deploy [component] on OpenShift" - -# Research should produce: -- Understanding of component architecture -- List of required APIs/CRDs -- List of required images -- List of operators needed -- Configuration requirements -- Known constraints/limitations -``` - -### Step R.2: Extract Assumptions - -From research findings, document: - -```yaml -assumed_apis: - - // - - ... - -planned_images: - - /: - - ... - -operators: - - - - ... - -configuration_requirements: - - - - ... -``` - -### Step R.3: Invoke cluster-reality-check - -```markdown -# Invoke skill with extracted assumptions -cluster-reality-check: - assumed_apis: [from step R.2] - planned_images: [from step R.2] - operators: [from step R.2] - namespace: -``` - -### Step R.4: Invoke divergence-check - -```markdown -# If research used external documentation -divergence-check: - upstream_source: - local_environment: "OpenShift " - features_to_verify: [from research] -``` - -### Gate R: Research Complete? - -| Check | Status | -|-------|--------| -| Research findings documented | [ ] | -| All APIs verified exist | [ ] | -| All images verified pullable | [ ] | -| All operators verified ready | [ ] | -| No HIGH severity divergences | [ ] | -| Required adjustments documented | [ ] | - -**Decision:** -- All checks pass -> Proceed to Phase 0 -- Any check fails -> Continue research with new constraints - ---- - -## Phase 0: Tracer Bullets - -**Goal:** Validate critical assumptions with minimal deployments before full planning - -**Time Budget:** 10-15% of total time - -### Step 0.1: Identify Critical Assumptions - -From research, identify assumptions that if wrong, would invalidate the entire plan: - -```markdown -Critical Assumptions: -1. [Assumption]: [Impact if wrong] -2. [Assumption]: [Impact if wrong] -3. [Assumption]: [Impact if wrong] -``` - -**Common critical assumptions:** -- Operator accepts expected API version -- Image can be pulled and runs -- Admission webhooks accept configuration -- Storage class works as expected -- Network policies allow required traffic - -### Step 0.2: Fire Tracer Bullets - -For each critical assumption, invoke tracer-bullet skill: - -```markdown -# Tracer Bullet 1: API Version -tracer-bullet: - assumption: "EDB accepts postgresql.k8s.enterprisedb.io/v1" - minimal_spec: | - apiVersion: postgresql.k8s.enterprisedb.io/v1 - kind: Cluster - metadata: - name: tracer-api-test - spec: - instances: 1 - storage: - size: 1Gi - success_criteria: "Cluster reaches Ready state" - timeout: 120s - cleanup: true - -# Tracer Bullet 2: Image Pull -tracer-bullet: - assumption: "dify-api image is pullable" - minimal_spec: | - apiVersion: v1 - kind: Pod - metadata: - name: tracer-image-test - spec: - containers: - - name: test - image: langgenius/dify-api:0.11.1 - command: ["sleep", "10"] - restartPolicy: Never - success_criteria: "Pod reaches Running state" - timeout: 60s - cleanup: true -``` - -### Step 0.3: Analyze Results - -Document tracer bullet outcomes: - -```markdown -Tracer Bullet Results: -| Bullet | Assumption | Result | Evidence | -|--------|------------|--------|----------| -| 1 | EDB API v1 | PASS | Cluster Ready in 45s | -| 2 | dify-api image | FAIL | ImagePullBackOff | -| 3 | ... | ... | ... | -``` - -### Gate 0: All Tracer Bullets Pass? - -**Decision:** -- All PASS -> Proceed to Phase P -- Any FAIL -> Return to Phase R with findings - -```markdown -# If bullet fails: -Failed Assumption: [assumption] -Evidence: [error message, events] -Required Action: [what must change] - -# Return to research to find alternative approach -``` - ---- - -## Phase P: Planning - -**Goal:** Create detailed implementation plan with validated assumptions - -**Time Budget:** 15-25% of total time - -### Step P.1: Create Plan - -With validated assumptions, create implementation plan: - -```markdown -# Use /plan command -/plan [component] deployment - -# Plan should include: -- Per-phase breakdown -- Exact file:line specifications -- Validation commands per phase -- Rollback procedure -- Success criteria -``` - -### Step P.2: Include Phase Validation - -Every phase in plan MUST have: - -```yaml -phase_N: - name: "[Phase Name]" - resources: - - - - - validation_commands: - - "" - - "" - success_criteria: - - "" - - "" - rollback: - - "" -``` - -### Step P.3: Human Review - -Present plan for approval: - -```markdown -## Plan Summary - -**Component:** [what] -**Target:** [where] -**Phases:** [how many] -**Estimated Time:** [duration] - -### Phase Breakdown -| Phase | Name | Resources | Validation | -|-------|------|-----------|------------| -| 1 | ... | ... | ... | -| 2 | ... | ... | ... | - -### Risk Assessment -- [Risk 1]: [Mitigation] -- [Risk 2]: [Mitigation] - -### Rollback Strategy -[How to undo if needed] - ---- -Approve? (yes/no/revise) -``` - -### Gate P: Plan Approved? - -**Decision:** -- Approved -> Proceed to Phase I -- Revise -> Update plan, re-present -- Rejected -> Return to research - ---- - -## Phase I: Implementation with Gates - -**Goal:** Execute plan with validation after every phase - -**Time Budget:** 30-40% of total time - -### Implementation Loop - -``` -For each phase in plan: - 1. Implement phase resources - 2. Invoke phase-gate skill - 3. If PASS: Commit, continue - 4. If FAIL: Stop, debug -``` - -### Step I.N: Implement Phase N - -```bash -# Create/modify resources as specified in plan -# Example: -oc apply -f phase-N-resources.yaml -``` - -### Step I.N+1: Phase Gate - -```markdown -# Invoke phase-gate skill -phase-gate: - phase_number: N - phase_name: "[Name from plan]" - resources_deployed: [list from plan] - validation_commands: [from plan] - success_criteria: [from plan] - rollback_procedure: [from plan] -``` - -### Step I.N+2: Commit or Stop - -**If phase-gate PASS:** -```bash -git add . -git commit -m "phase N: [description]" -# Continue to phase N+1 -``` - -**If phase-gate FAIL:** -```markdown -STOP. Do not proceed. - -Failure Details: -- Phase: N -- Failed Validation: [which] -- Error: [message] -- Evidence: [events, logs] - -Options: -1. Fix issue, re-validate phase N -2. Rollback phase N, investigate -3. Return to planning with findings -``` - -### Gate I: All Phases Complete? - -Continue loop until all phases pass. - ---- - -## Phase V: Validation & Retrospective - -**Goal:** Final validation and learning capture - -**Time Budget:** 10-15% of total time - -### Step V.1: Full Validation Suite - -Run comprehensive validation: - -```bash -# Syntax validation -make ci-all - -# Resource validation -oc get all -n - -# Health checks -curl -f http:///health - -# Functional test -[component-specific tests] -``` - -### Step V.2: Rollback Test - -Verify rollback procedure works: - -```bash -# Test rollback of last phase -git revert HEAD --no-commit - -# Verify resources can be removed -oc delete -f phase-N-resources.yaml --dry-run=server - -# Abort revert (we're just testing) -git checkout . -``` - -### Step V.3: Run Retrospective - -```markdown -# Invoke /retro command -/retro infrastructure-deployment - -# Capture: -- What went well -- What diverged from plan -- Unexpected issues encountered -- Skills/workflows that helped -- Skills/workflows that were missing -- Learnings for next deployment -``` - -### Step V.4: Document Deployment - -Create deployment documentation: - -```markdown -# [Component] Deployment - -**Deployed:** YYYY-MM-DD -**Target:** / -**Version:** - -## Architecture -[Diagram or description] - -## Components -- [Component 1]: [Status] -- [Component 2]: [Status] - -## Access -- URL: -- Credentials: - -## Operations -- Health check: -- Logs: -- Restart: - -## Known Issues -- [Issue]: [Workaround] -``` - -### Gate V: Deployment Complete? - -| Check | Status | -|-------|--------| -| Full validation passes | [ ] | -| Rollback tested | [ ] | -| Retrospective complete | [ ] | -| Documentation created | [ ] | -| Learnings captured | [ ] | - -**Decision:** -- All pass -> Deployment complete -- Any fail -> Address before declaring done - ---- - -## Failure Handling - -### Research Phase Failure - -```markdown -Symptom: Reality check shows divergences -Action: -1. Document divergences -2. Continue research for alternatives -3. Update assumptions -4. Re-run reality check -5. Repeat until all validated -``` - -### Tracer Bullet Failure - -```markdown -Symptom: Critical assumption invalid -Action: -1. Document failure evidence -2. Identify root cause -3. Return to research phase -4. Find alternative approach -5. Create new tracer bullet -6. Repeat until pass -``` - -### Phase Gate Failure - -```markdown -Symptom: Phase validation fails -Action: -1. STOP immediately -2. Capture state (events, logs) -3. Identify root cause -4. Options: - a. Fix issue, re-validate - b. Rollback phase, investigate - c. Return to planning -5. Never proceed with failing gate -``` - -### Implementation Rollback - -```markdown -# If rollback needed: -1. Identify rollback point (which phase) -2. Execute rollback commands from plan -3. Verify resources removed -4. Document what happened -5. Return to appropriate phase -``` - ---- - -## Time Budget Guidelines - -| Phase | Simple Deploy | Medium Deploy | Complex Deploy | -|-------|---------------|---------------|----------------| -| R (Research) | 10 min | 30 min | 60 min | -| 0 (Tracer) | 5 min | 15 min | 30 min | -| P (Plan) | 10 min | 20 min | 40 min | -| I (Implement) | 15 min | 45 min | 90 min | -| V (Validate) | 5 min | 15 min | 30 min | -| **Total** | **45 min** | **2 hours** | **4 hours** | - ---- - -## Integration Points - -### Skills Used - -| Phase | Skills | -|-------|--------| -| R | cluster-reality-check, divergence-check | -| 0 | tracer-bullet | -| P | (none - Claude orchestration) | -| I | phase-gate | -| V | (none - validation commands) | - -### Commands Used - -| Phase | Commands | -|-------|----------| -| R | /research | -| P | /plan | -| V | /retro | - -### Related Workflows - -- `assumption-validation` - Subset of Phase R -- `post-work-retro` - Detailed version of Phase V - ---- - -## Example: Dify Deployment - -### Phase R Summary -``` -Research: Dify multi-container application -Reality Check: 2 HIGH divergences (images, volumes) -Divergence Check: Docker -> OpenShift translation needed -Gate R: FAIL - Must resolve image and volume issues -``` - -### Phase 0 Summary -``` -Tracer 1: EDB database cluster - PASS -Tracer 2: Redis pod - PASS -Tracer 3: Dify API image - FAIL (signature policy) -Gate 0: FAIL - Return to research for image solution -``` - -### Phase R (Iteration 2) -``` -Solution: Mirror images to internal registry -Reality Check: All images now pullable -Gate R: PASS -``` - -### Phase 0 (Iteration 2) -``` -Tracer 3 (retry): Dify API image - PASS -Gate 0: PASS -``` - -### Phase P Summary -``` -Plan: 4 phases -- Phase 1: Namespace and secrets -- Phase 2: Database (EDB cluster) -- Phase 3: Supporting services (Redis, Weaviate) -- Phase 4: Application deployments -Human Review: APPROVED -Gate P: PASS -``` - -### Phase I Summary -``` -Phase 1: Namespace/secrets - Gate PASS -Phase 2: Database - Gate PASS (cluster ready in 90s) -Phase 3: Supporting services - Gate PASS -Phase 4: Application - Gate FAIL (quota exceeded) - -> Fix: Request quota increase - -> Retry Phase 4 - Gate PASS -Gate I: PASS (all phases complete) -``` - -### Phase V Summary -``` -Full validation: PASS -Rollback test: PASS -Retrospective: Completed -Documentation: Created -Gate V: PASS - Deployment complete -``` - ---- - -## Success Criteria - -Infrastructure deployment is successful when: - -- [ ] All research assumptions validated -- [ ] All tracer bullets passed -- [ ] Plan approved by human -- [ ] All phases passed gates -- [ ] Full validation suite passes -- [ ] Rollback procedure verified -- [ ] Retrospective completed -- [ ] Documentation created -- [ ] Zero unresolved issues - ---- - -**Remember:** Every gate exists to catch problems early. A failing gate is not a failure - it's the workflow working as designed. The failure would be proceeding despite a failing gate. diff --git a/docs/workflows/knowledge-synthesis.md b/docs/workflows/knowledge-synthesis.md deleted file mode 100644 index 9f7de2360..000000000 --- a/docs/workflows/knowledge-synthesis.md +++ /dev/null @@ -1,112 +0,0 @@ ---- -name: knowledge-synthesis -description: Extract and synthesize knowledge from multiple sources -estimated_time: 30-60 minutes -phases: 3 ---- - -# Knowledge Synthesis Workflow - -**Purpose:** Combine knowledge from code, docs, history, and patterns - -**When to use:** -- Onboarding (understand new codebase) -- Documentation (create comprehensive guides) -- Pattern extraction (learn from multiple implementations) -- Architecture review (understand system design) - -**Token budget:** 40-60k tokens (single session) - ---- - -## Workflow Phases - -``` -Phase 1: Gather (15-20% context) - ↓ -Phase 2: Synthesize (10-15% context) - ↓ -Phase 3: Document (5-10% context) -``` - ---- - -## Phase 1: Gather - -**Goal:** Collect knowledge from multiple sources - -**Commands:** -```bash -/research-multi "[topic]" -# Launches 3 agents in parallel: -# - code-explorer: Code structure -# - doc-explorer: Documentation -# - history-explorer: Git history -``` - -**Output:** Combined research from 3 perspectives - ---- - -## Phase 2: Synthesize - -**Goal:** Connect insights, identify patterns - -**Activities:** -- Cross-reference findings -- Extract common patterns -- Identify best practices -- Note gaps or inconsistencies - -**Output:** Synthesized understanding - ---- - -## Phase 3: Document - -**Goal:** Capture knowledge for future use - -**Commands:** -```bash -/retro --quick [topic] -# Creates pattern documentation -# Updates knowledge base -``` - -**Output:** Documentation + pattern catalog - ---- - -## Example: Understand Auth System - -```bash -Read CLAUDE.md -/research-multi "authentication and authorization system" - -# Agent 1 (Code): -# - auth/ directory structure -# - JWT validation in middleware -# - Session management in handlers - -# Agent 2 (Docs): -# - docs/auth-design.md explains OAuth2 flow -# - examples/auth-example/ shows usage - -# Agent 3 (History): -# - Added in commit abc123 (2024-03) -# - Refactored in commit def456 (2024-08) -# - Pattern: Standard OAuth2 + custom claims - -# Synthesis: -# Authentication: OAuth2 with JWT -# Authorization: Role-based (admin, user, guest) -# Pattern: Middleware-based validation -# Best practice: Separate auth concerns from business logic - -/retro --quick auth-system-knowledge -# Documented for future reference -``` - ---- - -**Start knowledge synthesis with:** `Read CLAUDE.md` + `/research` diff --git a/docs/workflows/meta-observer-pattern.md b/docs/workflows/meta-observer-pattern.md deleted file mode 100644 index c3476237c..000000000 --- a/docs/workflows/meta-observer-pattern.md +++ /dev/null @@ -1,555 +0,0 @@ -# Meta-Observer Pattern: Autonomous Multi-Session Coordination - -**Version:** 1.0.0 -**Status:** Production-ready ✅ -**Discovered:** 2025-11-09 -**Pattern Type:** Multi-session coordination via stigmergy - ---- - -## Overview - -**Meta-Observer Pattern** enables N autonomous workers to coordinate through shared memory (Memory MCP) without central orchestration. - -**Key Innovation:** Emergent coordination > Central control - -**Inspiration:** Ant colonies (pheromone trails/stigmergy), not military (command/control) - ---- - -## Problem - -**Traditional multi-session coordination:** -- ❌ Central orchestrator becomes bottleneck -- ❌ Micromanagement slows workers -- ❌ Orchestrator doesn't know domain context -- ❌ Single point of failure -- ❌ Doesn't scale (coordination overhead increases with N) - -**Result:** Slower than serial work, despite parallelization - ---- - -## Solution - -**Autonomous workers + Shared memory + Minimal intervention observer** - -``` -WORKERS (N sessions) MEMORY MCP META-OBSERVER -─────────────────── ────────────── ────────────── -Work independently ←→ Shared knowledge ←─ Monitor passively -Update when complete ─→ Worker discoveries ─→ Synthesize findings -Self-organize via MCP ←─ Cross-worker data ─→ Document learnings -No coordination needed Persistent state Intervene if blocking -``` - -**Result:** Parallel work + Emergent insights + No bottlenecks - ---- - -## Core Principles - -### 1. Worker Autonomy - -**Workers are domain experts who self-organize.** - -- Make independent decisions -- No permission needed -- Trust domain expertise -- Work at own pace -- Update Memory MCP when complete - -### 2. Stigmergy Coordination - -**Coordination through shared environment (Memory MCP), not commands.** - -- Workers leave "pheromone trails" (observations in Memory MCP) -- Other workers detect trails and adapt -- Emergent patterns arise naturally -- No central coordinator needed - -### 3. Minimal Intervention - -**Observer watches, synthesizes, documents - intervenes rarely.** - -- Monitor worker activity passively (every 2-4h) -- Synthesize N worker streams into coherent narrative -- Intervene ONLY if blocking conflicts -- Trust worker autonomy 99% of time - -### 4. Emergent Intelligence - -**Valuable insights emerge from worker combinations.** - -- Cross-domain patterns -- Unexpected synergies -- Novel solutions -- Distributed problem-solving - -### 5. Natural Scaling - -**Pattern scales to N workers without overhead.** - -- Add workers: Just create new entity, no coordination changes -- Remove workers: No impact on others -- 10 workers = same overhead as 2 workers -- Coordination cost: O(1), not O(N²) - ---- - -## Architecture - -### Components - -**1. Autonomous Workers (N sessions)** -- Domain: Specific repo, codebase, or task area -- Goal: Domain-specific objectives -- Entity: Unique Memory MCP entity (`Worker Session: {domain}`) -- Behavior: Work independently, update Memory MCP when complete -- Coordination: Minimal (only if blocking another worker) - -**2. Memory MCP (Shared Knowledge Base)** -- Function: Stigmergy (coordination through environment) -- Data: Worker observations, discoveries, blockers, status -- Persistence: Survives /clear, session restarts, IDE restarts -- Access: All sessions read/write -- Structure: Entities + Relations + Observations - -**3. Meta-Observer (1 session)** -- Role: Monitor, synthesize, document -- Location: Workspace root (neutral territory) -- Behavior: Passive observation, active synthesis -- Intervention: Minimal (only blocking conflicts) -- Output: Synthesis documents, pattern learnings - ---- - -## Implementation - -### Step 1: Launch Meta-Observer Pattern - -```bash -/launch-meta-observer -``` - -**Agent prompts for:** -- Number of workers -- Domain for each worker -- Overall goal -- Observer location (default: workspace root) - -**Creates:** -- N worker entities in Memory MCP -- N worker briefs (`.agents/briefs/worker-N-{domain}.md`) -- 1 observer brief (`.agents/briefs/meta-observer-{timestamp}.md`) -- Meta-Observer session initialized - -### Step 2: Brief Workers - -**Each worker receives custom brief with:** -- Domain assignment -- Autonomous work protocol -- Memory MCP update instructions -- Unique entity name -- Context management guidelines - -**Workers understand:** -- ✅ Work autonomously (you're the expert) -- ✅ Update Memory MCP after major progress -- ✅ Check Memory MCP for other worker discoveries (optional) -- ✅ Coordinate only if blocking -- ✅ Manage context (sub-agents, bundling at 40%) - -### Step 3: Observer Monitors - -**Every 2-4 hours, observer:** -1. Queries Memory MCP for worker updates -2. Reads new discoveries -3. Identifies cross-worker patterns -4. Checks for blockers/conflicts -5. Updates synthesis -6. Intervenes if necessary (rare) -7. Documents learnings - -### Step 4: Workers Execute - -**Workers autonomously:** -1. Do domain work -2. Use sub-agents for complex tasks -3. Monitor context (stay <40%) -4. Update Memory MCP after phases -5. Check for blockers from others (optional) -6. Continue until goal complete - -### Step 5: End-of-Day Synthesis - -**Observer creates comprehensive synthesis:** -- What each worker completed -- Emergent insights across workers -- Patterns validated -- Learnings captured -- Overall progress toward goal - ---- - -## Usage Patterns - -### Pattern 1: Repository Parallelization - -**Use case:** Work spans multiple repos - -**Example:** -```bash -/launch-meta-observer --workers 3 \ - --domains "backend-api,frontend-ui,infrastructure" \ - --goal "Implement authentication feature" -``` - -**Workers:** -- Worker 1 (backend-api): Auth endpoints, JWT logic -- Worker 2 (frontend-ui): Login UI, protected routes -- Worker 3 (infrastructure): Database schema, secrets management - -**Observer:** Synthesizes complete feature, ensures consistency - -**Coordination:** Workers update Memory MCP when APIs ready, schemas deployed, etc. - -### Pattern 2: Domain Specialization - -**Use case:** Different expertise areas - -**Example:** -```bash -/launch-meta-observer --workers 4 \ - --domains "documentation,testing,deployment,monitoring" \ - --goal "Production-ready release" -``` - -**Workers:** -- Worker 1: API docs, user guides, tutorials -- Worker 2: Unit tests, integration tests, E2E tests -- Worker 3: CI/CD pipelines, deployment automation -- Worker 4: Metrics, logging, alerting, dashboards - -**Observer:** Synthesizes release readiness across all domains - -### Pattern 3: Phase Parallelization - -**Use case:** Independent phases of same project - -**Example:** -```bash -/launch-meta-observer --workers 3 \ - --domains "research,implementation,validation" \ - --goal "New feature development" -``` - -**Workers:** -- Worker 1: Research approaches, evaluate options, document findings -- Worker 2: Implement solution based on research -- Worker 3: Create validation suite, test implementation - -**Coordination:** Worker 1 hands off to Worker 2 via Memory MCP - -### Pattern 4: Launch Preparation (Today's Experiment) - -**Use case:** Multi-domain launch readiness - -**Example:** -```bash -/launch-meta-observer --workers 3 \ - --domains "12-factor-agentops,agentops-showcase,launch-content" \ - --goal "Q1 2025 public launch" -``` - -**Workers:** -- Worker 1: Framework documentation, factor-mapping, compliance -- Worker 2: VitePress website build, deployment, validation -- Worker 3: SEO blog posts, launch strategy, social content - -**Result:** All completed autonomously, emergent insights discovered - ---- - -## Memory MCP Patterns - -### Worker Update Pattern - -```typescript -// After completing major work -mcp__memory__add_observations({ - observations: [{ - entityName: "Worker Session: {domain}", - contents: [ - "Completed: {what}", - "Discoveries: {insights}", - "Impact: {contribution to goal}", - "Blockers: {none or description}", - "Context: {%}", - "Next: {steps}", - "Files: {modified}", - "Commits: {if applicable}" - ] - }] -}) -``` - -### Observer Query Pattern - -```typescript -// Check all worker updates -mcp__memory__search_nodes({ - query: "Worker Session completed discoveries" -}) - -// Get specific workers -mcp__memory__open_nodes({ - names: [ - "Worker Session: domain-1", - "Worker Session: domain-2" - ] -}) - -// Check for blockers -mcp__memory__search_nodes({ - query: "BLOCKER Worker Session" -}) -``` - -### Handoff Pattern - -```typescript -// Worker 1 completes, hands off to Worker 2 -mcp__memory__add_observations({ - observations: [{ - entityName: "Worker Session: domain-1", - contents: [ - "Work complete", - "Handoff to Worker 2:", - "- Artifacts: {files}", - "- Context: {what they need to know}", - "- Ready for: {next phase}", - "Status: COMPLETE, HANDED OFF" - ] - }] -}) - -// Worker 2 picks up -mcp__memory__search_nodes({ query: "Handoff to Worker 2" }) -``` - ---- - -## Success Metrics - -**Pattern succeeds when:** -- ✅ Workers complete work autonomously (no constant guidance) -- ✅ Emergent insights arise (worker combinations create value) -- ✅ Observer synthesis valuable (creates coherent narrative) -- ✅ Intervention minimal (only blocking conflicts) -- ✅ Faster than serial (parallelization actually helps) -- ✅ No context collapse (all sessions <40%) -- ✅ Scales naturally (adding workers doesn't slow down) - -**Pattern needs adjustment when:** -- ⚠️ Workers asking for constant guidance (be more autonomous) -- ⚠️ No emergent insights (workers in silos, not sharing via MCP) -- ⚠️ Observer intervening frequently (micromanaging) -- ⚠️ Slower than serial (parallelization overhead too high) -- ⚠️ Blocking conflicts undetected (observer not monitoring) -- ⚠️ Context collapse (workers not using sub-agents/bundling) - ---- - -## Advantages - -**vs Central Orchestration:** -- ✅ No bottleneck (workers don't wait for orchestrator) -- ✅ Domain expertise (workers know their domain best) -- ✅ Scales naturally (O(1) coordination, not O(N²)) -- ✅ Resilient (no single point of failure) -- ✅ Emergent insights (patterns arise from combinations) - -**vs No Coordination:** -- ✅ Shared knowledge (Memory MCP provides context) -- ✅ Conflict detection (observer watches for blockers) -- ✅ Synthesis (coherent narrative from distributed work) -- ✅ Learning capture (patterns documented) - ---- - -## Limitations - -**Not suitable for:** -- ❌ Single-session work (overhead not worth it) -- ❌ Tightly coupled tasks (constant sync needed) -- ❌ Simple linear workflow (serial is simpler) -- ❌ Real-time coordination required (async by design) - -**Challenges:** -- Workers must be disciplined about Memory MCP updates -- Observer must resist urge to micromanage -- Requires trust in worker autonomy -- Emergent patterns may be unexpected (feature or bug?) - ---- - -## Integration with 12-Factor AgentOps - -**This pattern validates:** - -**Factor II (JIT Context Loading):** -- Workers bundle at 40%, stay lean -- Observer stays <30% (just reading/synthesizing) -- Sub-agents keep worker context low -- Memory MCP eliminates need to load full context - -**Factor VI (Session Continuity):** -- Memory MCP persists across /clear -- Workers can bundle and resume seamlessly -- Observer synthesizes even after session restarts -- Work continues despite interruptions - -**Factor VII (Intelligent Routing):** -- Observer synthesizes, doesn't command -- Workers self-route based on domain expertise -- Memory MCP routes information between workers -- Emergent routing (not planned routing) - -**Factor IX (Pattern Extraction):** -- Observer captures emergent patterns -- Workers document discoveries in Memory MCP -- Learnings extracted automatically -- Pattern library grows organically - ---- - -## Advanced Usage - -### Nested Observers - -**For very large N (10+ workers):** - -``` -Meta-Meta-Observer -├── Domain Observer 1 (watches 5 workers) -├── Domain Observer 2 (watches 5 workers) -└── Domain Observer 3 (watches 5 workers) -``` - -**Use when:** 10+ workers, group into domains - -### Dynamic Worker Addition - -**Add worker mid-stream:** - -```bash -/worker-brief --domain "new-domain" --goal "additional work" --number 4 -``` - -**Observer automatically incorporates** new worker into monitoring. - -### Worker Subtraction - -**Worker completes and exits:** - -```typescript -mcp__memory__add_observations({ - observations: [{ - entityName: "Worker Session: domain-1", - contents: [ - "Status: COMPLETE", - "Exiting: Work done", - "Handoff: None needed", - "Final report: {summary}" - ] - }] -}) -``` - -**Other workers and observer continue** unaffected. - ---- - -## Experiment Results (2025-11-09) - -**Hypothesis:** Central orchestration best for multi-session work - -**Actual Discovery:** Autonomous coordination superior - -**Evidence:** -- 3 workers completed complex work independently -- Zero active coordination needed -- Emergent insights discovered (recursive validation) -- Observer synthesis highly valuable -- Pattern scales to N workers naturally -- Faster than would have been with central control - -**Conclusion:** Pattern validated ✅ - -**Status:** Production-ready for general use - ---- - -## Files - -**.claude/commands/** -- `launch-meta-observer.md` - Initialize pattern -- `worker-brief.md` - Generate worker instructions - -**.claude/agents/** -- `meta-observer.md` - Observer agent -- `autonomous-worker.md` - Worker template - -**.claude/workflows/** -- `meta-observer-pattern.md` - This file (full pattern docs) - ---- - -## See Also - -**Related Patterns:** -- Context Bundling Protocol -- Sub-Agent Delegation Pattern -- Stigmergy Coordination (ant colonies) - -**Related Factors:** -- Factor II: Context Loading → The 40% Rule as Overload Prevention -- Factor VI: Resume Work → Validation Continuity Across Sessions -- Factor VII: Smart Routing → Directing Work to Appropriate Validation Paths -- Factor IX: Mine Patterns → Learning What Passes Validation - -**Inspiration:** -- Ant colony optimization -- Swarm intelligence -- Distributed systems (eventual consistency) -- Self-organizing systems - ---- - -## Quick Start - -```bash -# Initialize pattern -/launch-meta-observer - -# Follow prompts for: -# - Number of workers -# - Domain per worker -# - Overall goal - -# Workers work autonomously -# Observer monitors and synthesizes -# You get comprehensive synthesis at end - -# That's it! -``` - ---- - -**Pattern:** Meta-Observer -**Principle:** Emergent coordination > Central control -**Scales to:** N autonomous workers -**Status:** Production-ready ✅ -**Discovered:** 2025-11-09 through experiment -**Maintained by:** AgentOps Community diff --git a/docs/workflows/meta-observer/SHOWCASE.md b/docs/workflows/meta-observer/SHOWCASE.md deleted file mode 100644 index 552595d2d..000000000 --- a/docs/workflows/meta-observer/SHOWCASE.md +++ /dev/null @@ -1,435 +0,0 @@ -# Meta-Observer Pattern: Distributed Intelligence for Multi-Session Work - -**One-line:** N autonomous AI agents coordinate through shared memory, not central control. - -**Status:** Production-ready ✅ | **Scales to:** N workers | **Overhead:** O(1) - ---- - -## The Problem - -Traditional multi-session AI coordination looks like this: - -``` -Orchestrator (bottleneck) - ↓ commands -Worker 1 ← waits for instructions -Worker 2 ← waits for instructions -Worker 3 ← waits for instructions -``` - -**Result:** Micromanagement, bottlenecks, slower than serial work. - ---- - -## The Solution - -**Meta-Observer Pattern:** Autonomous workers + Shared memory + Minimal observer - -``` -Worker 1 (autonomous) ──→ Memory MCP ←── Meta-Observer (watches) -Worker 2 (autonomous) ──→ (shared) ←── (synthesizes) -Worker 3 (autonomous) ──→ (knowledge) ←── (documents) -``` - -**Result:** Emergent coordination, no bottlenecks, faster than serial. - ---- - -## How It Works - -### Workers Are Autonomous - -Each worker is a domain expert who: -- Makes independent decisions -- Works at their own pace -- Updates shared memory when ready -- Self-organizes with other workers -- Coordinates only if blocking - -**No waiting for permission. No micromanagement.** - -### Memory MCP Is Stigmergy - -Workers coordinate through environment, like ants with pheromone trails: -- Worker completes work → Leaves "trail" in Memory MCP -- Other workers detect trail → Adapt their approach -- Emergent patterns arise naturally -- No central coordinator needed - -**Coordination through shared knowledge, not commands.** - -### Observer Synthesizes - -Meta-Observer watches all workers and: -- Monitors shared memory (every 2-4 hours) -- Synthesizes N worker streams into coherent narrative -- Documents emergent insights -- Intervenes ONLY if blocking conflict - -**Watch and learn, don't command.** - ---- - -## Real Example: Q1 2025 Launch Prep - -**Challenge:** Prepare framework for public launch across 3 completely different domains. - -**Setup:** -```bash -/launch-meta-observer --workers 3 \ - --domains "framework-docs,website-build,launch-content" \ - --goal "Q1 2025 public launch prep" -``` - -**Workers:** -- **Worker 1:** Framework documentation, factor-mapping, research validation -- **Worker 2:** VitePress website build, deployment, infrastructure -- **Worker 3:** SEO blog posts, launch strategy, social media content - -**Coordination:** ZERO active coordination. Workers self-organized through Memory MCP. - -**Results:** -- **Time:** 8-10 hours (vs 20+ hours serial) -- **Speedup:** 2-3x from parallelization -- **Context collapse:** 0 (all workers <40%) -- **Coordination overhead:** ~0% (~5 minutes total) -- **Emergent insights:** 4 major discoveries -- **Launch readiness:** 50% → 80% in one day - -**Work completed autonomously:** -- 850 lines of factor-mapping documentation -- 160 files committed (35,805 insertions) for VitePress -- 4 SEO blog posts production-ready -- Launch strategy complete - ---- - -## Key Properties - -### Scales to N Workers - -**Add workers without slowing down:** -- 2 workers? Works. -- 10 workers? Works. -- 100 workers? Works. - -**Coordination overhead: O(1), not O(N²)** - -Each worker has unique Memory MCP entity: -```typescript -"Worker Session: domain-1" -"Worker Session: domain-2" -"Worker Session: domain-N" -``` - -No conflicts. Infinite scalability. - -### Emergent Intelligence - -**Valuable insights arise from worker combinations:** - -**Example from our experiment:** -- Worker 1 created factor-mapping (reverse-engineering proof) -- Worker 2 built website (enables showcase) -- Worker 3 created launch content (marketing ready) -- **Emergent insight:** We're using 12-Factor patterns to validate 12-Factor patterns! - -**The framework validated itself through its own use.** - -### Context Efficient - -**Workers stay lean via sub-agents:** -- Worker delegates complex subtask → Sub-agent executes → Worker aggregates -- Worker uses ~20% context, sub-agent uses ~30% = 50% total (safe) -- Can handle unlimited work via sub-agent delegation - -**Observer stays lean via reading:** -- Just queries Memory MCP (structured data) -- Doesn't do worker tasks -- Stays ~20-30% context -- Synthesizes incrementally - -**Result:** No context collapse, indefinite work duration. - ---- - -## Usage - -### 3 Simple Commands - -**1. Launch full pattern:** -```bash -/launch-meta-observer -``` -Creates observer + N worker briefs. Use for new multi-session projects. - -**2. Start as worker:** -```bash -/start-worker -``` -Transform current session into autonomous worker. Use when joining existing pattern. - -**3. Generate worker brief:** -```bash -/worker-brief -``` -Create detailed written brief for a worker. Use for offline preparation. - -### That's It - -No complex setup. No coordination meetings. No micromanagement. - -Just autonomous workers + shared memory + minimal observer. - ---- - -## What Makes It Special - -### Discovery Story - -**We discovered this pattern by accident.** - -While preparing our public launch, we set up 3 Claude Code sessions with a traditional "orchestrator" model. But the workers started completing complex work before the orchestrator gave any instructions. They self-organized through shared memory (Memory MCP). - -The "orchestrator" realized: *"I'm not orchestrating—I'm just watching them work autonomously!"* - -**That's when we knew we had found something special.** - -Autonomous coordination > Central control. - -We immediately extracted the pattern, built full infrastructure, and productized it the same day. It's now our standard operating mode for multi-session work. - -### Recursive Validation - -**The meta-insight:** We used 12-Factor AgentOps patterns to discover the Meta-Observer pattern. - -The experiment itself validated: -- **Factor II (JIT Context Loading):** Memory MCP + context bundling -- **Factor VI (Session Continuity):** Workers bundle and resume seamlessly -- **Factor VII (Intelligent Routing):** Observer synthesizes, doesn't command -- **Factor IX (Pattern Extraction):** Meta-Observer pattern extracted from experiment - -**The framework validated itself through its own use.** - ---- - -## Proof Points - -**Empirical validation (2025-11-09):** -- ✅ 3 workers completed complex, multi-domain work autonomously -- ✅ 0 coordination overhead (no micromanagement, no bottlenecks) -- ✅ 2-3x faster than serial approach -- ✅ 0 context collapses (all workers managed context successfully) -- ✅ 4 emergent insights discovered (wouldn't have found with central control) -- ✅ Pattern productized same day (full infrastructure created in hours) -- ✅ Production-ready immediately (used for real work, not toy example) - -**Infrastructure created:** -- 3 slash commands (launch, start-worker, worker-brief) -- 2 agents (meta-observer, autonomous-worker) -- Full documentation (guides, examples, showcase) -- ~70KB of production-ready code - -**Pattern characteristics:** -- Scales to N workers (O(1) overhead) -- Emergent intelligence (worker combinations create value) -- Context efficient (<40% per session) -- Resilient (no single point of failure) -- Distributed (works across repos, codebases, domains) - ---- - -## Integration with 12-Factor AgentOps - -**This pattern is 12-Factor AgentOps in action:** - -**Factor II (JIT Context Loading):** -- Workers bundle at 40%, stay lean -- Observer reads structured data, stays <30% -- Memory MCP eliminates full context loading - -**Factor VI (Session Continuity):** -- Memory MCP persists across /clear -- Workers bundle and resume seamlessly -- Work continues despite interruptions - -**Factor VII (Intelligent Routing):** -- Observer synthesizes, doesn't command -- Workers self-route based on expertise -- Memory MCP routes information between workers - -**Factor IX (Pattern Extraction):** -- Pattern emerged from real work -- Extracted and documented -- Production-ready infrastructure created -- Community can use and improve - -**The pattern validates the framework. The framework enabled the pattern.** - ---- - -## For Developers - -**Use Meta-Observer when:** -- ✅ Work spans multiple repos/codebases -- ✅ Tasks can be parallelized -- ✅ Want emergent insights from distributed work -- ✅ Need synthesis of N independent streams -- ✅ Avoiding coordination bottlenecks - -**Don't use when:** -- ❌ Single session work (overhead not worth it) -- ❌ Tightly coupled tasks (constant sync needed) -- ❌ Simple linear workflow (serial is simpler) - -**Get started:** -```bash -# Install (if not already) -# Meta-Observer is part of 12-Factor AgentOps - -# Launch pattern -/launch-meta-observer - -# Or start as worker -/start-worker -``` - -**Full docs:** `.claude/workflows/meta-observer/` - ---- - -## For Researchers - -**Novel contributions:** - -**1. Stigmergy for AI Coordination** -- Memory MCP as pheromone trail system -- Workers coordinate through environment -- Emergent patterns without central control -- Validates distributed intelligence hypothesis - -**2. Empirical Validation** -- Real production use (not toy example) -- Quantitative metrics (2-3x speedup, 0% overhead) -- Qualitative insights (4 emergent discoveries) -- Immediate productization (same-day infrastructure) - -**3. Recursive Meta-Pattern** -- Pattern discovered using itself -- Framework validates framework -- Self-improving system -- Emergence through practice - -**Research questions opened:** -- How does this scale to N>100 workers? -- What emergent patterns arise at scale? -- Can nested observers enable hierarchical coordination? -- How does this compare to other multi-agent systems? - -**Citation:** -```bibtex -@misc{meta-observer-pattern-2025, - title={Meta-Observer Pattern: Autonomous Multi-Session Coordination via Stigmergy}, - author={12-Factor AgentOps Community}, - year={2025}, - url={https://github.com/your-repo/12-factor-agentops}, - note={Discovered 2025-11-09, Production-ready v1.0.0} -} -``` - ---- - -## For Organizations - -**Value proposition:** - -**Problem:** Your team struggles to coordinate multiple AI agent sessions. Central orchestration creates bottlenecks. Work takes longer than it should. - -**Solution:** Meta-Observer pattern enables N agents to work autonomously with emergent coordination. - -**Business impact:** -- **2-3x faster:** Parallelization without coordination overhead -- **Scales naturally:** Add agents without slowing down (O(1) not O(N²)) -- **Quality insights:** Emergent patterns from distributed intelligence -- **Risk reduction:** No single point of failure, resilient architecture -- **Cost efficient:** Context management prevents expensive token usage - -**Adoption path:** -1. **Try it:** One multi-session project with `/launch-meta-observer` -2. **Measure:** Time savings, insights discovered, quality of synthesis -3. **Scale:** More projects, more workers, nested observers for large N -4. **Evolve:** Capture learnings, improve pattern, share with community - -**Support:** Full documentation, production-ready infrastructure, community examples - ---- - -## Community - -**Get involved:** - -**Use the pattern:** -- Try `/launch-meta-observer` for your multi-session work -- Share your results and learnings -- Report what works and what doesn't - -**Contribute:** -- Pattern variations for different contexts -- Community examples and case studies -- Infrastructure improvements -- Documentation enhancements - -**Discuss:** -- GitHub Issues: Bug reports and feature requests -- GitHub Discussions: Use cases and learnings -- Community Slack: Real-time collaboration - -**Evolve:** -- Pattern is v1.0.0, not final -- Community input drives evolution -- Best ideas get integrated -- We learn together - ---- - -## Quotes - -> "We discovered the Meta-Observer pattern by accident. Workers self-organized better than we could coordinate them. That's when we knew distributed intelligence > central control." - -> "The 'orchestrator' realized it wasn't orchestrating—it was just watching and learning. We immediately extracted the pattern and productized it the same day." - -> "We used 12-Factor AgentOps patterns to discover a new 12-Factor AgentOps pattern. The framework validated itself through its own use." - -> "Three autonomous workers completed complex, multi-domain work in one day. Zero coordination overhead. Four emergent insights. The pattern works empirically, not theoretically." - ---- - -## Summary - -**Meta-Observer Pattern enables N autonomous AI agents to coordinate through shared memory (Memory MCP) without central orchestration.** - -**Key properties:** -- Scales to N workers with O(1) overhead -- Emergent intelligence from worker combinations -- Context efficient (<40% per session) -- Production-ready infrastructure -- Empirically validated in real use - -**Get started in 3 commands:** -1. `/launch-meta-observer` - Initialize pattern -2. `/start-worker` - Become autonomous worker -3. That's it - work autonomously, coordinate emergently - -**Status:** Production-ready v1.0.0 ✅ - -**Principle:** Distributed intelligence > Central control - ---- - -**Welcome to the future of multi-session AI coordination.** - -**Pattern:** Meta-Observer -**Discovered:** 2025-11-09 -**Repository:** `.claude/workflows/meta-observer/` -**Community:** 12-Factor AgentOps diff --git a/docs/workflows/meta-observer/example-today.md b/docs/workflows/meta-observer/example-today.md deleted file mode 100644 index 282696513..000000000 --- a/docs/workflows/meta-observer/example-today.md +++ /dev/null @@ -1,590 +0,0 @@ -# Meta-Observer Pattern - Real Example (2025-11-09) - -**Project:** 12-Factor AgentOps Q1 2025 Public Launch Preparation -**Workers:** 3 autonomous sessions -**Observer:** 1 synthesis session -**Duration:** Full day (~8-10 hours) -**Status:** Pattern validated ✅ - ---- - -## Context - -**Goal:** Prepare 12-Factor AgentOps framework for public launch Q1 2025 - -**Challenge:** Work spans 3 completely different domains: -1. Framework documentation (technical writing, research) -2. Website build and deployment (frontend, infrastructure) -3. Launch content creation (marketing, SEO, strategy) - -**Traditional approach would be:** -- Do serially (slow, 20+ hours) -- Central orchestrator coordinates (bottleneck, micromanagement) -- Constant meetings/sync (overhead) - -**Meta-Observer approach:** -- 3 autonomous workers in parallel -- Shared memory (Memory MCP) coordination -- 1 observer synthesizing -- Emergent insights - ---- - -## The Experiment - -### Hypothesis - -"Central orchestration is needed for multi-session coordination" - -### Actual Discovery - -"Autonomous workers + shared memory > Central orchestration" - -### How It Unfolded - -**Initial Plan (9:00 AM):** -1. Session 1 (Orchestrator): Create master plan, coordinate workers -2. Session 2 (Framework): Execute Week 4 checklist phases -3. Session 3 (Showcase): Complete VitePress migration - -**What Actually Happened:** -1. Sessions 1-3 started working **before orchestrator gave instructions** -2. Workers self-organized and completed complex work **autonomously** -3. "Orchestrator" realized it wasn't orchestrating - workers didn't need it! -4. **Pattern shift:** Orchestrator → Meta-Observer (watch/synthesize, not command) - -**Key Insight:** Workers coordinate better autonomously than through central control! - ---- - -## The Workers - -### Worker 1: Launch Content (node/workspaces) - -**Domain:** Community onboarding, SEO blog posts, launch strategy - -**Autonomous Work Completed:** -- ✅ SEO-optimized blog posts (4 posts) -- ✅ Launch strategy and timing -- ✅ Social media content preparation -- ✅ Community onboarding smooth metrics - -**Key Discovery:** -> "The workflow came first, factors extracted after" -> -> Credibility model: Practice → Theory → Validation - -**Impact:** Launch content production-ready - -**Context:** Unknown (worker didn't report, but completed significant work) - -### Worker 2: Framework Docs (12-factor-agentops) - -**Domain:** Framework documentation, factor mapping, credibility - -**Autonomous Work Completed:** -- ✅ `docs/production-workflows/factor-mapping.md` (~850 lines) -- ✅ Reverse-engineered all 12 factors from actual workflow -- ✅ Mapped theory → expression → location → evidence -- ✅ Link validation (Phase 1 of Week 4 checklist) -- ✅ Removed time references (credibility hygiene) - -**Key Discovery:** -> "The 12 factors are not imposed theory—they're documented reality" -> -> Reverse engineering: Workflow practices existed first, factors extracted after - -**Impact:** Major credibility boost - addresses #1 skepticism ("Did you just make this up?") - -**Context:** ~42% at one point, bundled successfully - -**Commits:** -- `1e66bb3`: Fixed broken links, added launch prep infrastructure -- `da3b442`: Week 3 enhancements -- Earlier commits establishing foundation - -### Worker 3: Website (agentops-showcase) - -**Domain:** VitePress migration, build, deployment, validation - -**Autonomous Work Completed:** -- ✅ VitePress build test: `npm run docs:build` SUCCESS -- ✅ Type checking: All passing -- ✅ Container deployment: Port 3102 running -- ✅ Navigation test: All routes accessible -- ✅ Mermaid diagrams: 4+ rendering correctly -- ✅ Committed: 160 files, 35,805 insertions -- ✅ Phase 1 COMPLETE without any guidance - -**Key Discovery:** -> "Build successful, all validations passing, no longer blocks framework" -> -> Worker correctly identified cross-worker dependency removal - -**Impact:** Unblocked Worker 2 for cross-repo link validation - -**Context:** ~35% (stayed lean) - -**Commit:** -- `5e482e2`: feat(mermaid): add Mermaid diagram rendering support - ---- - -## Coordination (Minimal!) - -### Cross-Worker Dependencies - -**Only 1 dependency:** -- Worker 2 needed Worker 3's build to complete link validation -- Worker 3 completed Phase 1 autonomously -- Worker 3 updated Memory MCP: "No longer blocks framework" -- Worker 2 could proceed with cross-repo link validation - -**That's it!** No other coordination needed. - -### Memory MCP Usage - -**Worker updates:** -- Worker 3: Phase 1 complete, build successful, unblocked framework -- Worker 2: factor-mapping created, major credibility work complete -- Worker 1: Launch content complete, SEO optimized - -**Observer queries:** -- Every 2 hours: Check worker status -- Monitor for blockers -- Synthesize discoveries -- Document emergent patterns - -**Actual intervention:** ZERO (workers self-organized perfectly) - ---- - -## Emergent Insights - -### Discovery 1: Recursive Validation - -**Pattern:** Using 12-Factor patterns to validate 12-Factor patterns - -**How:** -- Factor II (JIT Context): Memory MCP + context bundling (this experiment!) -- Factor VI (Session Continuity): Workers bundle and resume (used today!) -- Factor VII (Routing): Meta-Observer synthesizes, doesn't command (discovered today!) -- Factor IX (Pattern Extraction): Extracted Meta-Observer pattern (happening now!) - -**Impact:** Framework validates itself through its own use - -### Discovery 2: Reverse Engineering Proof - -**Pattern:** Factors emerged FROM practice, not imposed ON practice - -**How:** -- Worker 2 created factor-mapping.md -- Mapped actual workflow practices to factors -- Showed factors are documented reality, not theory - -**Impact:** Major credibility boost for launch - -**Quote from Worker 2:** -> "I spent months using AI agents in production. The workflow multiplied my output measurably. I extracted the patterns that actually worked and codified them as 12-Factor AgentOps." - -### Discovery 3: Autonomous > Orchestrated - -**Pattern:** Workers self-organize better than central coordination - -**How:** -- Workers completed complex work independently -- Zero micromanagement needed -- Emergent coordination through Memory MCP -- Observer just watched and synthesized - -**Impact:** New pattern discovered: Meta-Observer - -### Discovery 4: Meta-Observer Pattern Itself - -**Pattern:** N autonomous workers + shared memory + minimal observer - -**How:** -- Discovered by accident (workers didn't need orchestration) -- Immediately recognized as superior pattern -- Extracted and productized same day -- Full infrastructure created in hours - -**Impact:** New standard for multi-session work - ---- - -## Results - -### Quantitative - -**Work Completed:** -- 850 lines of factor-mapping documentation -- 160 files committed (VitePress migration) -- 35,805 code insertions -- 4 SEO blog posts -- Launch strategy complete -- 80% launch-ready status achieved - -**Time:** -- Duration: ~8-10 hours (full day) -- Serial estimate: 20+ hours -- **Speedup: ~2-3x** from parallelization - -**Context Management:** -- Worker 2: 42% peak (bundled successfully) -- Worker 3: ~35% (stayed lean) -- Observer (Session 4): ~50% peak -- **Zero context collapses** - -**Coordination Overhead:** -- Active interventions: 0 -- Blocking conflicts: 0 -- Time spent coordinating: <5 minutes total -- **Coordination overhead: ~0%** - -### Qualitative - -**Worker Autonomy:** -- ✅ Workers completed work without constant guidance -- ✅ Made independent decisions in their domains -- ✅ Self-organized through Memory MCP -- ✅ High quality output - -**Emergent Insights:** -- ✅ Recursive validation discovered -- ✅ Reverse engineering proof created -- ✅ Meta-Observer pattern extracted -- ✅ Cross-worker synergies identified - -**Pattern Validation:** -- ✅ Autonomous coordination works -- ✅ Memory MCP stigmergy effective -- ✅ Observer synthesis valuable -- ✅ Scales to N workers (validated N=3, theoretically infinite) - -**Launch Readiness:** -- Before: ~50% -- After: ~80% -- Remaining: Beta testing (1-2 weeks), final polish - ---- - -## Timeline - -### Morning (9:00-12:00) - -**9:00 - Experiment Start** -- Session 4 (Meta-Observer) created master plan -- Sessions 1-3 already working independently -- Observer realized: "They don't need orchestration!" - -**9:30 - Pattern Shift** -- Hypothesis changed: Orchestration → Observation -- Workers continue autonomously -- Observer begins passive monitoring - -**10:00 - Worker 3 Phase 1 Complete** -- VitePress build successful -- All validations passing -- Updated Memory MCP: "No longer blocks framework" - -**11:00 - Worker 2 Major Discovery** -- Created factor-mapping.md -- Reverse-engineered all 12 factors -- Massive credibility boost for launch - -### Afternoon (12:00-17:00) - -**13:00 - Meta-Observer Pattern Extracted** -- Observer documented emerging pattern -- Created Memory MCP entities for pattern -- Began designing reusable workflow - -**14:00 - Infrastructure Creation Begins** -- `/launch-meta-observer` command -- `/worker-brief` command -- `meta-observer.md` agent -- `autonomous-worker.md` agent - -**15:00 - Worker 1 Launch Content Complete** -- SEO blog posts ready -- Launch strategy defined -- Social media content prepared - -**16:00 - Pattern Documentation Complete** -- Full workflow folder created -- All commands, agents, docs -- Example documentation (this file!) - -### Evening (17:00+) - -**17:00 - Experiment Synthesis** -- Observer synthesized all worker discoveries -- Documented learnings -- Validated pattern success - -**Result:** Meta-Observer pattern production-ready in one day! - ---- - -## Learnings - -### What Worked - -**1. Worker Autonomy** -- Letting domain experts work independently -- Trusting their expertise -- Not micromanaging - -**2. Memory MCP as Stigmergy** -- Shared knowledge graph -- Workers coordinate through environment -- Like ant pheromone trails - -**3. Minimal Intervention** -- Observer watched, didn't command -- Zero active coordination needed -- Workers self-organized perfectly - -**4. Emergent Patterns** -- Recursive validation discovered organically -- Meta-Observer pattern emerged naturally -- Cross-worker insights valuable - -**5. Context Management** -- Sub-agents kept workers lean -- Bundling protocol worked (Worker 2 at 42%) -- Observer stayed <50% - -### What Could Improve - -**1. Explicit Coordination Protocol** -- Workers initially didn't know they were being observed -- Clearer upfront briefing would help -- Solution: `/start-worker` command now provides this - -**2. Context % Reporting** -- Workers didn't report context utilization -- Observer couldn't track context health -- Solution: Add context % to worker update protocol - -**3. More Frequent Memory MCP Updates** -- Workers updated at major milestones only -- More frequent updates would improve synthesis -- Solution: Suggest updates every 1-2 hours, not just at completion - -**4. Structured Observation Cadence** -- Observer checked ad-hoc, not on schedule -- More structured (every 2h) would be better -- Solution: Built into meta-observer agent protocol now - -### What We'd Change Next Time - -**1. Use `/start-worker` from the beginning** -- Have each worker initialize with `/start-worker` -- Creates entity, provides protocol immediately -- Clear identity and coordination model - -**2. Schedule observer checkpoints** -- Set 2-hour timer -- Query Memory MCP on schedule -- More predictable monitoring - -**3. Request context % in updates** -- Workers report context % with each update -- Observer can track context health -- Early warning for context collapse - -**4. Create synthesis increments** -- Synthesize every 2-4 hours, not just end of day -- Creates clean recovery points -- Enables mid-course corrections if needed - ---- - -## Validation - -### Pattern Success Criteria - -**From pattern definition:** - -| Criterion | Target | Actual | ✅/❌ | -|-----------|--------|--------|------| -| Workers autonomous | Yes | Yes | ✅ | -| Emergent insights | Yes | 4 major insights | ✅ | -| Observer synthesis valuable | Yes | Very valuable | ✅ | -| Intervention minimal | <5% time | ~0% | ✅ | -| Faster than serial | Yes | 2-3x faster | ✅ | -| No context collapse | All <40% | Some >40% but managed | ✅ | -| Scales naturally | O(1) not O(N²) | Validated N=3 | ✅ | - -**Overall: 7/7 success criteria met ✅** - -### 12-Factor Integration - -**Factors validated through this experiment:** - -| Factor | How Validated | ✅/❌ | -|--------|---------------|------| -| Factor II (JIT Context) | Memory MCP + bundling used | ✅ | -| Factor VI (Continuity) | Workers bundled/resumed | ✅ | -| Factor VII (Routing) | Observer synthesized, not commanded | ✅ | -| Factor IX (Extraction) | Meta-Observer pattern extracted | ✅ | - -**Overall: Experiment itself validates 4 of 12 factors ✅** - -### Community Validation Next - -**What's validated:** -- ✅ Pattern works (3 workers, complex work) -- ✅ Autonomous > Orchestrated (empirical evidence) -- ✅ Memory MCP stigmergy effective -- ✅ Scales to small N (3) - -**What needs validation:** -- ⏳ Scales to large N (10+, 100+) -- ⏳ Works across diverse domains (not just launch prep) -- ⏳ Works for different team sizes -- ⏳ Works in different organizational contexts - -**Next step:** Community usage and feedback - ---- - -## Impact - -### Immediate (Today) - -**1. Launch Readiness Accelerated** -- 50% → 80% in one day -- Would have taken 3-4 days serially -- 2-3x speedup validated - -**2. Pattern Discovered and Productized** -- Meta-Observer pattern extracted -- Full infrastructure created -- Production-ready same day - -**3. Framework Credibility Boosted** -- factor-mapping.md addresses key skepticism -- Reverse engineering proof complete -- Evidence chain established - -**4. Recursive Validation Achieved** -- Framework validates itself -- Using patterns to prove patterns -- Meta-achievement unlocked - -### Medium-term (Weeks) - -**1. Standard Operating Mode** -- Meta-Observer becomes default for multi-session work -- Community can use via `/launch-meta-observer` -- Pattern scales to their use cases - -**2. Launch Materials Enhanced** -- Real example of pattern in action -- Showcase demonstration ready -- Proof of 40x speedups (cumulative with other evidence) - -**3. Community Validation Begins** -- Others try the pattern -- Feedback and improvements -- Use cases documented - -### Long-term (Months) - -**1. Pattern Evolution** -- Community contributions -- Variations for different contexts -- Nested observers for scale -- Integration with other patterns - -**2. 12-Factor Validation** -- More factors validated through usage -- Community examples emerge -- Empirical evidence accumulates - -**3. Knowledge OS Advancement** -- Multi-session coordination solved -- Emergent intelligence patterns documented -- AgentOps framework strengthened - ---- - -## For Showcase Website - -### Narrative - -**"We discovered the Meta-Observer pattern by accident."** - -While preparing for our public launch, we experimented with coordinating 3 Claude Code sessions across different domains: framework docs, website build, and launch content. - -We started with a traditional orchestrator model—one session directing the others. But the workers started completing complex work before the orchestrator gave instructions. They self-organized through shared memory (Memory MCP), like ants coordinating via pheromones. - -The "orchestrator" realized it wasn't orchestrating—it was just watching and learning. - -**That's when we discovered: Autonomous coordination > Central control** - -We immediately extracted the pattern, built full infrastructure (`/launch-meta-observer`), and productized it the same day. The pattern now scales to N workers with zero coordination overhead. - -**The meta-insight?** We used 12-Factor AgentOps patterns to discover a new 12-Factor AgentOps pattern. The framework validated itself through its own use. - -### Proof Points - -- ✅ 3 workers completed complex work autonomously -- ✅ 0 coordination overhead (~0% time spent) -- ✅ 2-3x faster than serial approach -- ✅ 4 emergent insights discovered -- ✅ Pattern productized same day -- ✅ Scales to N workers theoretically -- ✅ Production-ready infrastructure created - -### Demo Materials - -- Full Memory MCP entity graph showing coordination -- Worker commit history showing autonomous work -- Observer synthesis documents showing emergent insights -- Infrastructure files (commands, agents, docs) -- This example documentation - ---- - -## Quotes - -**On worker autonomy:** -> "Workers are domain experts who self-organize. The observer watches and synthesizes. Distributed intelligence > Central control." - -**On emergent patterns:** -> "We discovered the Meta-Observer pattern by accident. Workers didn't need orchestration—they coordinated better autonomously." - -**On recursive validation:** -> "We used 12-Factor AgentOps patterns to discover a new 12-Factor AgentOps pattern. The framework validates itself." - -**On reverse engineering:** -> "The 12 factors are not imposed theory—they're documented reality. The workflow came first, factors extracted after." - -**On the discovery:** -> "The 'orchestrator' realized it wasn't orchestrating—it was just watching and learning. That's when we knew we had found something special." - ---- - -## Conclusion - -**The Meta-Observer pattern works.** - -Not theoretically—empirically. Not in a lab—in production use. - -Three autonomous workers completed complex, multi-domain work faster than would have been possible serially, with zero coordination overhead, while discovering emergent insights that wouldn't have been found with central control. - -The pattern is now production-ready and scales to N workers. - -**Welcome to distributed intelligence.** - ---- - -**Experiment Date:** 2025-11-09 -**Pattern:** Meta-Observer v1.0.0 -**Status:** Validated ✅ -**Next:** Community usage and validation -**Repository:** `.claude/workflows/meta-observer/` diff --git a/docs/workflows/multi-domain.md b/docs/workflows/multi-domain.md deleted file mode 100644 index 318abeb1c..000000000 --- a/docs/workflows/multi-domain.md +++ /dev/null @@ -1,184 +0,0 @@ ---- -name: multi-domain -description: Coordinate work spanning multiple domains simultaneously -estimated_time: Varies (multi-session) -phases: 3 ---- - -# Multi-Domain Workflow - -**Purpose:** Work across multiple domains (technical + personal, product + infrastructure) simultaneously - -**When to use:** -- Cross-domain projects (backend + frontend + infrastructure) -- Technical + personal tracking (work + career development) -- Multi-flavor coordination (devops + product-dev + life) -- Complex organizational change (code + process + documentation) - -**Token budget:** 60-120k tokens across 2-4 sessions (using bundles) - ---- - -## Workflow Phases - -``` -Phase 1: Domain Identification (5-10% context) - ↓ -Phase 2: Parallel Execution (30-40% context per domain) - ↓ -Phase 3: Integration & Learning (10-15% context) -``` - -**Key principle:** Use context bundles to prevent collapse when spanning domains - ---- - -## Phase 1: Domain Identification - -**Goal:** Identify all domains involved - -**Activities:** -- List technical domains (backend, frontend, infrastructure, data) -- List non-technical domains (documentation, process, personal) -- Map dependencies between domains -- Plan execution order (parallel where possible) - -**Output:** Domain map with dependencies - ---- - -## Phase 2: Parallel Execution - -**Goal:** Execute work in each domain - -**Pattern:** -```bash -# Domain 1: Technical work -Read CLAUDE.md -/research "[technical-topic]" -/bundle-save technical-research -# Continue technical workflow... - -# Domain 2: Personal tracking (separate session or flavor) -# Load personal flavor bundle -/bundle-load life-career-2025 -# Track accomplishment in Master Capability Inventory -# Update career metrics -``` - -**Key:** Use bundles to switch between domains without losing context - -**Output:** Work complete in each domain - ---- - -## Phase 3: Integration & Learning - -**Goal:** Connect learnings across domains - -**Activities:** -- Verify technical work completed -- Verify personal tracking updated -- Extract cross-domain patterns -- Document insights - -**Output:** Integrated learning + cross-domain patterns - ---- - -## Example: Build K8s App + Track Career Growth - -**Session 1: Technical (DevOps domain)** -```bash -Read CLAUDE.md -/research "create Kubernetes application" -/plan k8s-app-research -/implement k8s-app-plan -/vibe recent -# ✅ App deployed successfully -/bundle-save k8s-app-complete -``` - -**Session 2: Personal (Life domain)** -```bash -# Switch to life flavor -/bundle-load life-career-2025 -/capability-auditor -# Add to Master Capability Inventory: -# - Skill: Kubernetes application deployment -# - Evidence: [commit SHA] -# - Impact: Deployed production app -# - Level: Intermediate → Advanced -/bundle-save life-career-updated -``` - -**Session 3: Integration** -```bash -# Both bundles loaded (technical + personal) -/retro --quick k8s-deployment -# Pattern extracted: -# - Technical: K8s app deployment pattern -# - Personal: Career advancement evidence - -# Result: -# - Technical accomplishment documented -# - Personal metrics updated -# - Pattern available for reuse -# - Cross-domain tracking maintained -``` - ---- - -## Multi-Flavor Coordination - -**What are flavors?** -- **Technical flavors:** devops, product-dev, infrastructure-ops, data-eng -- **Personal flavors:** life, career, learning -- **Process flavors:** documentation, operations, governance - -**Coordination pattern:** -```bash -# Work in technical flavor -cd workspace/gitops/ -# Use devops workflows -Read CLAUDE.md -[do work] -/bundle-save technical-work - -# Switch to personal flavor -cd workspace/life/ -# Use personal workflows -/bundle-load life-career-2025 -[track progress] -/bundle-save personal-tracking - -# Both bundles available for future reference -# Technical work feeds personal metrics automatically -``` - ---- - -## When to Use Multi-Domain vs Single Domain - -### Use Multi-Domain when: -✅ Work spans multiple areas (technical + docs + personal) -✅ Tracking across domains needed (work + career) -✅ Complex organizational change (code + process + culture) -✅ Integration points exist between domains - -### Use Single Domain when: -- Work is purely technical (or purely personal) -- No cross-domain tracking needed -- Simple, focused task - ---- - -## Related Documentation - -- **Multi-flavor coordination:** `docs/reference/multi-flavor-coordination.md` -- **Context bundles:** `core/commands/bundle-save.md`, `core/commands/bundle-load.md` -- **40% rule:** `core/CONSTITUTION.md` - ---- - -**Start multi-domain work with:** Domain mapping → Parallel execution → Integration diff --git a/docs/workflows/post-work-retro.md b/docs/workflows/post-work-retro.md deleted file mode 100644 index 0aa028b01..000000000 --- a/docs/workflows/post-work-retro.md +++ /dev/null @@ -1,477 +0,0 @@ -# Post-Work Retrospective Workflow - -**Purpose:** Systematic retrospective after completing significant work - -**Composes:** `/post-mortem` -> improvement creation (the retro step folded into `/post-mortem`) - -**Failure Patterns Prevented:** Institutional memory loss, repeated mistakes, ecosystem gaps - ---- - -## Overview - -This workflow captures learnings and identifies improvements after work completes. - -``` -Run /retro (failure pattern analysis) - ↓ -Identify Learnings (patterns, anti-patterns) - ↓ -Run /retro --quick for each pattern - ↓ -Create Improvements (skills, workflows, agents) - ↓ -Save Retrospective Bundle -``` - ---- - -## When to Use - -- After completing significant implementation -- After debugging session (especially if it took >1 hour) -- After deployment (success or failure) -- End of day/session with meaningful work -- After hitting unexpected obstacles -- When something "just worked" surprisingly well - -## When NOT to Use - -- Trivial changes with no learnings -- Routine operations that went as expected -- When context window is exhausted (defer to next session) -- Before work is actually complete - ---- - -## Process - -### Step 1: Run /retro - -Invoke retrospective command with context: - -```markdown -/retro [topic] - -# Example: -/retro dify-deployment - -# Retro should analyze: -- Vibe-coding failure patterns hit -- .claude/ ecosystem usage -- Time spent vs expected -- What worked well -- What didn't work -- Unexpected discoveries -``` - -### Step 2: Vibe-Coding Failure Pattern Analysis - -Check session against the 12 failure patterns: - -```markdown -## Failure Pattern Audit - -| # | Pattern | Hit? | Evidence | Impact | -|---|---------|------|----------|--------| -| 1 | Tests Passing Lie | YES | Tracer bullet caught what CI missed | 2h saved | -| 2 | Overfitting to Stack Overflow | NO | | | -| 3 | Copy-Pasta Blindspot | YES | Copied from wrong EDB version | 4h lost | -| 4 | Debug Loop Spiral | YES | 3 iterations on image pull | 1h lost | -| 5 | Eldritch Code Horror | NO | | | -| 6 | LLM Latency Blindspot | NO | | | -| 7 | Local Environment Mismatch | YES | Docker docs != OpenShift | 2h lost | -| 8 | Moving Target Syndrome | NO | | | -| 9 | External Dependency Assumption | YES | Assumed signature policy | 1h lost | -| 10 | Context Window Amnesia | NO | Used bundles effectively | | -| 11 | Completion Impulse | YES | Pushed before validation | 30m lost | -| 12 | AI Hallucination Cascade | NO | | | - -**Patterns Hit:** 5/12 -**Total Time Lost:** ~8.5 hours -**Patterns That Would Have Helped:** cluster-reality-check (3,7,9), tracer-bullet (1,4) -``` - -### Step 3: .claude/ Ecosystem Audit - -Analyze tool usage during session: - -```markdown -## .claude/ Ecosystem Audit - -### Skills Used -| Skill | Times Used | Effectiveness | Notes | -|-------|------------|---------------|-------| -| tracer-bullet | 3 | HIGH | Caught 2 critical issues | -| validate | 5 | HIGH | Standard workflow | -| context7-lookup | 2 | MEDIUM | Found docs but outdated | - -### Skills That Should Have Been Used -| Skill | When | Why Not Used | Impact | -|-------|------|--------------|--------| -| cluster-reality-check | Before planning | Didn't exist | 4h lost | -| divergence-check | During research | Didn't exist | 2h lost | - -### Skills Missing from Ecosystem -| Need | Description | Would Prevent | -|------|-------------|---------------| -| cluster-reality-check | Validate APIs exist | Pattern 3, 9 | -| divergence-check | Compare upstream to local | Pattern 7 | -| phase-gate | Validate before proceeding | Pattern 4, 11 | - -### Workflows Used -| Workflow | Used? | Effectiveness | -|----------|-------|---------------| -| /research | YES | Incomplete - missed reality check | -| /plan | YES | Good - phases clear | -| /implement | YES | Poor - no gates between phases | - -### Workflows That Should Have Been Used -| Workflow | Why Not Used | Impact | -|----------|--------------|--------| -| infrastructure-deployment | Didn't exist | No validation gates | - -### Agents Used -| Agent | Times | Result | -|-------|-------|--------| -| applications-create-app | 1 | Partial success | - -### Commands Used -| Command | Times | Notes | -|---------|-------|-------| -| /research | 1 | Good depth | -| /plan | 1 | Good structure | -| /bundle-save | 2 | Context preserved | -``` - -### Step 4: Identify Learnings - -Extract patterns (what to repeat) and anti-patterns (what to avoid): - -```markdown -## Patterns Identified (Do This Again) - -### Pattern 1: Tracer Bullet First -**What:** Deploy minimal resource before full implementation -**Why it worked:** Caught admission webhook rejection before writing full spec -**When to apply:** Any operator-based deployment -**Evidence:** Session commit abc123 - -### Pattern 2: Bundle Context Across Sessions -**What:** Save research as bundle, load in implementation session -**Why it worked:** 0% context collapse over 3 sessions -**When to apply:** Any multi-session work -**Evidence:** bundle-dify-research.md used successfully - -## Anti-Patterns Identified (Don't Do This) - -### Anti-Pattern 1: Trust Upstream Docs Blindly -**What happened:** Copied EDB v1.24 examples, we have v1.23 -**Impact:** 4 hours debugging imageCatalogRef that doesn't exist -**Prevention:** Always check installed version before using docs -**Evidence:** Session commit def456 (reverted) - -### Anti-Pattern 2: Skip Validation Between Phases -**What happened:** Deployed Phase 3 before Phase 2 was ready -**Impact:** Cascading failures, hard to diagnose -**Prevention:** Phase gate after every phase -**Evidence:** 3 rollback commits in session -``` - -### Step 5: Run /retro --quick for Each Pattern - -Extract reusable patterns to pattern library: - -```markdown -# For each pattern worth capturing: - -/retro --quick pattern="tracer-bullet-first" \ - context="Infrastructure deployments with operators" \ - what="Deploy minimal resource before full implementation" \ - why="Catches admission webhook rejections early" \ - when="Any deployment using CRDs with admission webhooks" \ - evidence="Dify deployment 2025-11-27, saved 2h" - -/retro --quick pattern="version-check-before-docs" \ - context="Using external documentation" \ - what="Check installed version before trusting docs" \ - why="Docs may be for newer version with features you don't have" \ - when="Any use of external/upstream documentation" \ - evidence="EDB imageCatalogRef failure, cost 4h" \ - --failure # Mark as anti-pattern -``` - -### Step 6: Create Improvements - -Based on gaps identified, create issues or drafts for: - -```markdown -## Improvements to Create - -### Skills Needed -1. **cluster-reality-check** - - Purpose: Validate APIs/images/operators exist - - Priority: P0 (would have saved 4h today) - - Spec: [draft spec] - -2. **divergence-check** - - Purpose: Compare upstream docs to local reality - - Priority: P1 (would have saved 2h today) - - Spec: [draft spec] - -### Workflow Updates -1. **infrastructure-deployment** - - Add: Reality check in Phase R - - Add: Phase gates in Phase I - - Priority: P0 - -### Agent Updates -1. **applications-create-app** - - Add: Call cluster-reality-check - - Add: Call tracer-bullet - - Priority: P1 - -### Documentation Needed -1. **Vibe-coding failure pattern reference** - - Document all 12 patterns - - Include prevention strategies - - Priority: P2 -``` - -### Step 7: Save Retrospective Bundle - -```markdown -/bundle-save retro-[topic]-[date] - -# Bundle should include: -- Failure pattern audit -- Ecosystem audit -- Patterns identified -- Anti-patterns identified -- Improvements proposed -- Time analysis -- Key learnings summary -``` - ---- - -## Output Template - -```markdown -# Retrospective: [Topic] - -**Date:** YYYY-MM-DD -**Duration:** [Time spent on work] -**Outcome:** [Success/Partial/Failure] - -## Executive Summary - -[2-3 sentence summary of what happened and key learning] - -## Failure Pattern Analysis - -**Patterns Hit:** N/12 -**Time Lost:** X hours -**Key Pattern:** [Most impactful pattern hit] - -| Pattern | Hit | Impact | -|---------|-----|--------| -| ... | ... | ... | - -## .claude/ Ecosystem Audit - -**Skills Used:** N -**Skills Missing:** N -**Workflows Used:** N -**Most Effective:** [skill/workflow] -**Biggest Gap:** [what was missing] - -## Patterns Extracted - -### [Pattern 1 Name] -- **What:** ... -- **Why:** ... -- **When:** ... - -## Anti-Patterns Documented - -### [Anti-Pattern 1 Name] -- **What happened:** ... -- **Prevention:** ... - -## Improvements Proposed - -| Type | Name | Priority | Impact | -|------|------|----------|--------| -| Skill | cluster-reality-check | P0 | 4h/deployment | -| Workflow | infrastructure-deployment | P0 | 2h/deployment | - -## Time Analysis - -| Phase | Expected | Actual | Variance | -|-------|----------|--------|----------| -| Research | 1h | 2h | +1h (reality check missing) | -| Planning | 30m | 45m | +15m | -| Implementation | 2h | 6h | +4h (no phase gates) | -| Total | 3.5h | 8.75h | +5.25h (150% over) | - -## Key Learnings - -1. [Learning 1] -2. [Learning 2] -3. [Learning 3] - -## Next Actions - -- [ ] Create cluster-reality-check skill -- [ ] Update infrastructure-deployment workflow -- [ ] Document tracer-bullet pattern -``` - ---- - -## Integration - -### With /retro Command - -```markdown -# /retro invokes this workflow - -/retro [topic] -→ Runs failure pattern analysis -→ Runs ecosystem audit -→ Prompts for pattern extraction -→ Creates improvement items -→ Saves bundle -``` - -### With /retro Command - -```markdown -# Each pattern identified gets captured - -/retro --quick pattern="name" context="..." what="..." why="..." when="..." - -# Patterns stored in: -# - Knowledge graph (Memory MCP) -# - Pattern library (docs/patterns/) -# - Retrospective bundle -``` - -### With infrastructure-deployment Workflow - -```markdown -# Phase V of infrastructure-deployment uses this - -infrastructure-deployment: - ... - Phase V: - - Full validation - - Rollback test - - post-work-retro # This workflow - - Documentation -``` - ---- - -## Time Budget - -| Step | Time | Notes | -|------|------|-------| -| Run /retro | 5 min | Failure pattern analysis | -| Ecosystem audit | 5 min | Tool usage review | -| Identify patterns | 10 min | Extract learnings | -| Run /retro --quick | 5 min | Capture patterns | -| Create improvements | 10 min | File issues/drafts | -| Save bundle | 2 min | Preserve context | -| **Total** | **~40 min** | | - ---- - -## Success Criteria - -Post-work retrospective is successful when: - -- [ ] Failure pattern audit complete -- [ ] Ecosystem audit complete -- [ ] At least 1 pattern extracted (if work was meaningful) -- [ ] At least 1 anti-pattern documented (if failures occurred) -- [ ] Improvement items created for gaps found -- [ ] Retrospective bundle saved -- [ ] Time analysis documented -- [ ] Key learnings articulated - ---- - -## Quick Reference - -```bash -# After completing significant work: - -1. /retro [topic] - # Analyze session for patterns - -2. Review failure patterns - # Which of 12 did we hit? - -3. Audit .claude/ usage - # What tools helped? What was missing? - -4. Extract patterns - /retro --quick pattern="name" ... - -5. Create improvements - # File issues for skills/workflows needed - -6. Save bundle - /bundle-save retro-[topic]-[date] -``` - ---- - -## Anti-Patterns in Retrospectives - -### Skipping When Things Went Well - -```markdown -# BAD: "Everything worked, no retro needed" -``` - -**Why bad:** Miss opportunity to document WHY it worked. - -**Should:** Capture successful patterns for reuse. - -### Too Shallow - -```markdown -# BAD: "It failed because the image didn't pull" -``` - -**Why bad:** Doesn't capture root cause or prevention. - -**Should:** "It failed because we trusted upstream docs without version check. Prevention: Always check installed version first." - -### No Improvements Created - -```markdown -# BAD: "We should have a skill for this" -# (But never create it) -``` - -**Why bad:** Same problem will recur. - -**Should:** Create issue, draft spec, or implement immediately. - -### Blame-Focused - -```markdown -# BAD: "The docs were wrong" -``` - -**Why bad:** Doesn't lead to systemic improvement. - -**Should:** "We lacked a divergence check between docs and reality. Created skill to prevent this." - ---- - -**Remember:** A retrospective that doesn't lead to improvement is just complaining. Every retro should produce at least one concrete action. diff --git a/docs/workflows/quick-fix.md b/docs/workflows/quick-fix.md deleted file mode 100644 index 93475c64d..000000000 --- a/docs/workflows/quick-fix.md +++ /dev/null @@ -1,143 +0,0 @@ ---- -name: quick-fix -description: Fast implementation for simple, low-risk changes -estimated_time: 10-30 minutes -phases: 2 ---- - -# Quick Fix Workflow - -**Purpose:** Rapid implementation of straightforward changes - -**When to use:** -- Simple changes (1-2 files, obvious solution) -- Low risk (non-critical systems, reversible) -- Well-known patterns (done many times before) -- Time-critical (production hotfix) - -**Token budget:** 20-40k tokens (single session) - ---- - -## Workflow Phases - -``` -Phase 1: Orient & Implement (15-30% context) - ↓ -Phase 2: Validate & Commit (5-10% context) -``` - -**Key principle:** Skip research/planning for simple changes - ---- - -## Phase 1: Orient & Implement - -**Commands:** -```bash -Read CLAUDE.md-simple # Quick orientation -# Describe simple change -# Agent implements directly -``` - -**What happens:** -1. Load constitutional foundation (2k tokens) -2. Understand change request -3. Make change immediately -4. Validate inline - -**Examples of quick fixes:** -- Fix typo in documentation -- Update dependency version -- Add environment variable -- Adjust configuration value -- Fix linting error - -**Token budget:** 15-30k tokens - ---- - -## Phase 2: Validate & Commit - -**Commands:** -```bash -/vibe --quick recent # Fast inline check -# Commit with message -``` - -**What happens:** -1. Run quick validation (syntax, build, unit tests) -2. If pass: Commit with context -3. If fail: Fix and retry - -**Token budget:** 5-10k tokens - ---- - -## When to Use vs Complete Cycle - -### Use Quick Fix when: -✅ Single file or small change -✅ Solution is obvious -✅ Risk is low -✅ Time is limited - -### Use Complete Cycle when: -❌ Multiple files with dependencies -❌ Solution is unclear -❌ Risk is high (critical system, production) -❌ Pattern should be extracted (learning opportunity) - ---- - -## Example: Fix Typo - -```bash -Read CLAUDE.md-simple -# User: "Fix typo in README.md line 45, 'recieve' → 'receive'" - -# Agent: -# ✅ Reads README.md -# ✅ Edits line 45 -# ✅ Validates markdown syntax -# ✅ Commits: "docs(readme): Fix typo - recieve → receive" - -# Total time: 2 minutes -# Token budget: 5k tokens -``` - -## Example: Update Dependency - -```bash -Read CLAUDE.md-simple -# User: "Update golang-jwt/jwt from v4.5.0 to v4.5.1" - -# Agent: -# ✅ Edits go.mod:12 -# ✅ Runs: go mod tidy -# ✅ Runs: go test ./... -# ✅ Commits: "chore(deps): Update golang-jwt/jwt to v4.5.1" - -# Total time: 5 minutes -# Token budget: 10k tokens -``` - -## Example: Add Environment Variable - -```bash -Read CLAUDE.md-simple -# User: "Add LOG_LEVEL=info to config/app.yaml" - -# Agent: -# ✅ Edits config/app.yaml:8 -# ✅ Validates YAML syntax -# ✅ Verifies app reads LOG_LEVEL -# ✅ Commits: "feat(config): Add LOG_LEVEL environment variable" - -# Total time: 8 minutes -# Token budget: 12k tokens -``` - ---- - -**Start quick fix with:** `Read CLAUDE.md-simple` → describe simple change diff --git a/docs/workflows/session-lifecycle.md b/docs/workflows/session-lifecycle.md deleted file mode 100644 index 3a0d426c5..000000000 --- a/docs/workflows/session-lifecycle.md +++ /dev/null @@ -1,538 +0,0 @@ -# Session Lifecycle Workflow - -**Purpose:** Runtime-aware guide to working across sessions with hook-capable runtimes, optional Codex native hooks, and the default Codex hookless path - -**Philosophy:** Talk naturally when lifecycle hooks exist, but do not make hidden automation the first-value path. Codex installs hookless by default; native hooks are an optional `scripts/install-codex-plugin.sh --with-hooks` profile. Hookless Codex uses explicit lifecycle commands and RPI packets instead of assuming runtime hooks. - ---- - -## Quick Start - -### Option 1: Natural Language (Hook-Capable Runtimes) - -Just describe what you want: - -| Say This | What Happens | -|----------|--------------| -| "Continue working on X" | `CLAUDE.md` provides the startup surface; hooks recover handoff and stage any factory state silently | -| "I need to add Y" | If no startup goal exists yet, the first substantive prompt can be captured as silent factory intake for later explicit `/rpi` or `ao knowledge brief` use | -| "I'm done for today" | Saves progress, offers retrospective | -| "What should I work on?" | Shows status, suggests next task | -| "Where was I?" | Shows last session, current state, blockers | - -### Option 2: Software Factory Lane (Recommended) - -The primary delivery path is the **operating loop** -([`docs/architecture/operating-loop.md`](../architecture/operating-loop.md)): -`ao session bootstrap` → `ao inject` → the seven-move loop → -`ao gate check --fast --scope head` → push to `main`. - -```bash -# Orient: standard startup report + decay-ranked prior context -ao session bootstrap -ao inject - -# Optional: build a bounded goal-time briefing when the corpus supports it -ao factory start --goal "fix auth startup" - -# Drive the work through the operating loop, landing slices with the gate -ao gate check --fast --scope head - -# End session -ao codex stop - -# Inspect lifecycle + flywheel health -ao codex status -``` - -`ao session bootstrap` + `ao inject` are the explicit startup path (3.0 is -hookless — nothing auto-injects orientation). `ao factory start` keeps the -operator lane explicit: build a bounded briefing if the corpus can support it, -then drive the operating loop. - -> **`/rpi` is the live one-turn executor; the `ao rpi` CLI was removed in 3.0.** -> The `/rpi` skill (`/rpi "fix auth startup"`) still drives one turn over the -> operating loop, but the phased RPI *CLI* — `ao rpi phased`, `ao rpi status` — was **removed** (commit f61c5f0e7); the RPI engine is gone. -> New work is driven through the operating loop above, not routed through a standalone RPI loop. - -### Option 3: Dream Compounding Run - -```bash -# A foreground compounding session against the real local .agents corpus -/dream "close the loop on today's auth work" -``` - -The session drives the CLI primitives that do the work — `ao compile`, -`ao maturity --scan`, `ao flywheel close-loop` — and writes the `summary.json` / -`summary.md` report contract. - -Use this when you want AgentOps to compound against the real local `.agents` -corpus in a session you can watch. This is not the same as the GitHub nightly -workflow: - -- GitHub nightly is the public CI proof harness for the report contract -- `/dream` is the private local compounding engine, run in session - -**Running Dream unattended is out-of-session orchestration**, which AgentOps -3.0 delegates to a substrate rather than shipping its own daemon, scheduler, or -overnight runner — those surfaces were deleted (see -[AgentOps 3.0 north star](../3.0.md)). On the reference substrate (NTM + MCP + -managed-agents), a long-lived agent runs `/dream`-equivalent maintenance on a -cron schedule. The substrate owns when and where; AgentOps owns what the loop does. -No tool pretends a sleeping laptop has guaranteed wake behavior. - -### Option 4: Lower-Level Codex Lifecycle - -```bash -# Start session -ao codex start - -# During work -ao lookup --query "topic" -ao search "topic" --cite retrieved - -# End session -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`, including the post-close maintenance that hook-capable -SessionEnd would normally run. - -### Option 5: Slash Commands (Hook-Capable Power Users) - -```bash -# Start session -/session-start - -# During work -/progress-update --complete feature-005 - -# End session -/session-end -``` - -**Use the mode your runtime actually supports.** Claude/OpenCode can drive lifecycle via hooks. Codex users should usually prefer `ao factory start --goal ""` as the briefing-first operator surface, while `ao codex start` / `ao codex stop` / `ao codex status` remain the lower-level lifecycle primitives. Codex skills automate the same boundaries via `ao codex ensure-start` / `ao codex ensure-stop`. - ---- - -## Runtime Modes - -| Mode | Start | Closeout | Notes | -|------|-------|----------|-------| -| Opt-in hook-capable | Natural language, `/session-start`, or authored startup hooks | Natural language, `/session-end`, or authored session-end hooks | Use only when you author/install hooks; `CLAUDE.md` remains the startup surface | -| Codex optional native hooks | Quiet native `SessionStart` maintenance plus explicit `ao codex start` / `ao codex ensure-start` when context retrieval is needed | Native `Stop` hook for turn-scope close-loop; explicit `ao codex stop` / `ao codex ensure-stop` for transcript-driven closeout | Opt-in with `install-codex.sh --with-hooks`; startup hooks stay quiet and no native `SessionEnd` event exists today | -| Codex hookless default | `ao factory start --goal ""`, `/rpi`, `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 required; lifecycle is explicit, and closeout owns the same curation hygiene as SessionEnd | -| Dream compounding run | `/dream ""` (foreground, in session) | Reads `summary.json` / `summary.md` from the run dir | Private local compounding in a watchable session. Running it unattended is out-of-session orchestration, delegated to a substrate (the reference is NTM + MCP + managed-agents) — AgentOps ships no daemon or scheduler | -| Manual fallback | `ao inject`, `ao lookup` | `ao forge transcript`, `ao flywheel close-loop` | Lowest-level portable path | - ---- - -## Hook-Capable Lifecycle - -``` -┌─────────────────────────────────────────────────────────────┐ -│ SESSION START │ -│ "Continue the API work" or /session-start │ -├─────────────────────────────────────────────────────────────┤ -│ • Recover handoff / tracker goal when available │ -│ • Keep operator framing in CLAUDE.md │ -│ • Stage factory goal / briefing files silently │ -│ • Mark missing-goal sessions for prompt-time intake │ -└─────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ WORK │ -│ "Add the validation layer" or just start coding │ -├─────────────────────────────────────────────────────────────┤ -│ • First substantive prompt can become silent factory intake│ -│ • Run /rpi or ao knowledge brief explicitly when needed │ -│ • Implement features │ -│ • Fix bugs │ -│ • Research when needed │ -│ • Commit frequently │ -│ │ -│ Track progress: │ -│ • "Feature X is done" or /progress-update --complete X │ -│ • "I'm blocked on Y" or /progress-update --blocker "Y" │ -└─────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ SESSION END │ -│ "Done for today" or /session-end │ -├─────────────────────────────────────────────────────────────┤ -│ • Check for uncommitted changes │ -│ • Prompt for session summary │ -│ • Update claude-progress.json │ -│ • Offer to save bundle │ -│ • Suggest /retro for learning extraction │ -└─────────────────────────────────────────────────────────────┘ -``` - -## Codex Hookless Lifecycle - -> Codex native hooks are optional. The diagram below applies to the default hookless Codex path. - -``` -┌─────────────────────────────────────────────────────────────┐ -│ CODEX SESSION START │ -│ ao codex start │ -├─────────────────────────────────────────────────────────────┤ -│ • Inspect .agents/ and surfaced learnings │ -│ • Run safe close-loop maintenance │ -│ • Sync MEMORY.md and write startup context │ -│ • Record retrieved citations for adopted startup artifacts │ -└─────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ WORK │ -│ ao lookup / ao search --cite / skills │ -├─────────────────────────────────────────────────────────────┤ -│ • Retrieve learnings and findings on demand │ -│ • Record citations when search results are actually used │ -│ • Build, debug, research, plan, validate │ -└─────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ CODEX SESSION END │ -│ ao codex stop │ -├─────────────────────────────────────────────────────────────┤ -│ • Resolve archived transcript or history fallback │ -│ • Forge/queue learnings safely │ -│ • Run close-loop maintenance and sync MEMORY.md │ -│ • Persist lifecycle state for status/recovery │ -└─────────────────────────────────────────────────────────────┘ -``` - ---- - -## Progress Files - -Two JSON files track state across sessions: - -### claude-progress.json - -```json -{ - "project": "my-api", - "last_updated": "2025-11-27T16:00:00Z", - "sessions": [ - { - "session_id": "session-001", - "started": "2025-11-27T14:00:00Z", - "ended": "2025-11-27T16:00:00Z", - "summary": "Implemented auth endpoints", - "commits": ["abc123", "def456"], - "features_completed": ["feature-001", "feature-002"] - } - ], - "current_state": { - "last_commit": "def456", - "working_on": "feature-003", - "blockers": [], - "next_steps": ["Add rate limiting", "Write tests"] - } -} -``` - -### feature-list.json - -```json -{ - "project": "my-api", - "total_features": 8, - "features": [ - { - "id": "feature-001", - "description": "User can authenticate", - "passes": true, - "completed_date": "2025-11-27T15:00:00Z" - }, - { - "id": "feature-002", - "description": "User can refresh token", - "passes": true - }, - { - "id": "feature-003", - "description": "API has rate limiting", - "passes": false - } - ] -} -``` - -**Templates available:** `.claude/templates/` - ---- - -## Natural Language Examples - -### Starting Work - -``` -User: "Let's continue the caching work" - -Claude: Welcome back! Last session you completed the Redis connection setup. - -📋 Current state: - Features complete: 3/8 - Working on: Cache invalidation (feature-004) - No blockers - -Ready to continue with cache invalidation? -``` - -### During Work - -``` -User: "The cache invalidation is done" - -Claude: Great! I'll mark feature-004 as complete. - -✅ Updated feature-list.json - Features complete: 4/8 - -Next up: feature-005 (Cache warming on startup) -Want to continue, or take a break? -``` - -### Hitting a Blocker - -``` -User: "I'm stuck - need the Redis credentials from DevOps" - -Claude: I'll note that as a blocker. - -⚠️ Blocker added: "Need Redis credentials from DevOps" - -Want to: -1. Work on something else while waiting? -2. Draft a message to DevOps? -3. End the session for now? -``` - -### Ending Session - -``` -User: "That's it for today" - -Claude: Wrapping up your session. - -📊 This session: - Duration: ~2 hours - Commits: 3 - Features completed: feature-004, feature-005 - -⚠️ You have 2 uncommitted files. Commit them? [y/n] - -Quick summary of what you accomplished? -> Finished cache invalidation and warming - -✅ Session saved to claude-progress.json - -Next time, just say "continue the caching work" and I'll pick up where you left off. -``` - ---- - -## Command Reference - -### Session Commands - -| Command | Purpose | When to Use | -|---------|---------|-------------| -| `ao codex start` | Start an explicit Codex hookless session | Start of work when you want explicit startup context | -| `ao codex stop` | Close out a Codex session without runtime hooks | End of work when you want transcript-driven closeout | -| `ao codex status` | Inspect Codex lifecycle and flywheel health | Any time in Codex | -| Natural-language goal | Native factory intake when hooks are installed | Preferred operator entrypoint in Claude/OpenCode | -| `/session-start` | Initialize session, load context | Start of work in hook-capable runtimes | -| `/session-end` | Save progress, close gracefully | End of work in hook-capable runtimes | -| `/progress-update` | Update progress files | During work | - -### Workflow Commands - -| Command | Purpose | When to Use | -|---------|---------|-------------| -| `/research` | Deep exploration of a topic | Before planning complex work | -| `/plan` | Create implementation plan | After research, before coding | -| `/implement` | Execute approved plan | After plan is approved | - -### Context Commands - -| Command | Purpose | When to Use | -|---------|---------|-------------| -| `/bundle-save` | Save context for later | End of session or milestone | -| `/bundle-load` | Load saved context | Start of session | -| `/bundle-search` | Find bundles by topic | When you forget bundle name | - -### Learning Commands - -| Command | Purpose | When to Use | -|---------|---------|-------------| -| `/retro` | Session retrospective | After significant work | -| `/retro --quick` | Extract reusable patterns | After solving problems | - ---- - -## Workflows by Scenario - -### Scenario 1: Continuing Previous Work - -**Natural:** -``` -"Continue the API work" -``` - -**Commands:** -```bash -/bundle-load api-implementation -/session-start -``` - -### Scenario 2: Starting New Feature - -**Natural:** -``` -"I need to add user authentication" -``` - -**Commands:** -```bash -/research "authentication approaches" -# ... research output ... -/plan authentication-research.md -# ... plan output ... -/implement authentication-plan.md -``` - -### Scenario 3: Quick Bug Fix - -**Natural:** -``` -"The login endpoint is returning 500 errors" -``` - -**Commands:** -```bash -# No special commands needed - just debug -``` - -### Scenario 4: End of Day - -**Natural:** -``` -"Done for today" -``` - -**Commands:** -```bash -/session-end -# optionally: -/retro -``` - ---- - -## Best Practices - -### Do - -- **Talk naturally** - Commands are optional -- **Commit frequently** - Preserve recovery points -- **Write meaningful summaries** - Future you will thank you -- **Mark features complete** - Track progress as you go -- **End sessions gracefully** - Don't let context expire - -### Don't - -- **Force commands** - Natural language works -- **Skip progress updates** - Tracking helps continuity -- **Let sessions expire** - Save state before context fills -- **Ignore blockers** - Document them for future sessions - ---- - -## Troubleshooting - -### "I don't see my progress" - -```bash -# Check for progress files -ls claude-progress.json feature-list.json - -# If missing, create from templates -cp .claude/templates/claude-progress.json . -cp .claude/templates/feature-list.json . -``` - -### "My bundle didn't load" - -```bash -# Search for bundles -/bundle-search "your topic" - -# List all bundles -/bundle-list -``` - -### "Context is getting full" - -```bash -# End session gracefully -/session-end - -# Or save and start fresh -/bundle-save my-progress -# Start new session -/bundle-load my-progress -``` - ---- - -## Integration with RPI Workflow (legacy executor) - -> The phased RPI flow is the **load-bearing legacy** one-turn executor, not the -> primary navigation. Drive new work through the operating loop -> ([`docs/architecture/operating-loop.md`](../architecture/operating-loop.md)); -> reach for RPI only when a turn explicitly needs the phased executor. - -For complex features, the phased RPI flow runs: - -``` -┌─────────────┐ ┌─────────────┐ ┌─────────────┐ -│ DISCOVERY │ ──▶ │ IMPLEMENT │ ──▶ │ VALIDATION │ -│ /research │ │ /crank │ │ /vibe │ -│ /plan │ │ /implement │ │ /post-mortem│ -│ /pre-mortem │ │ (execute) │ │ (learn) │ -└─────────────┘ └─────────────┘ └─────────────┘ - │ │ │ - ▼ ▼ ▼ -research + plan validated code learnings + next work - bundle bundle + commit -``` - -**Or just say:** "I need to add a complex feature" and I'll guide you through it. - ---- - -## Files Reference - -| File | Location | Purpose | -|------|----------|---------| -| Progress template | `.claude/templates/claude-progress.json` | Session state template | -| Feature template | `.claude/templates/feature-list.json` | Feature tracking template | -| Intent router | `.claude/skills/intent-router.md` | Natural language routing | -| Session autostart (opt-in) | `.claude/hooks/session-autostart.sh` | Auto-show context — AgentOps ships none; author one via the hooks-authoring skill | -| Session start cmd | `.claude/commands/session-start.md` | Manual session start | -| Session end cmd | `.claude/commands/session-end.md` | Manual session end | -| Progress update cmd | `.claude/commands/progress-update.md` | Manual progress update | - ---- - -**Remember:** Just talk naturally. The system handles the rest. diff --git a/evals/agentops-core/beads-issue-tracking.json b/evals/agentops-core/beads-issue-tracking.json deleted file mode 100644 index 5ca618fa3..000000000 --- a/evals/agentops-core/beads-issue-tracking.json +++ /dev/null @@ -1,237 +0,0 @@ -{ - "schema_version": 1, - "id": "agentops-core.beads-issue-tracking", - "name": "AgentOps Beads Issue Tracking Canary", - "description": "Offline public canary for bd/beads issue-tracking behavior, ao beads parsing and degradation logic, shared/Codex beads skill contracts, current bd JSON health probes, and RPI tasklist fallback behavior.", - "practices": [ - "llm-eval-harness", - "ai-assisted-dev" - ], - "domain": "mixed", - "visibility": "public_canary", - "tier": "deterministic", - "owners": [ - "agentops" - ], - "tags": [ - "canary", - "offline", - "beads", - "bd", - "issue-tracking", - "rpi", - "tasklist" - ], - "allowed_runtimes": [ - "shell", - "static" - ], - "environment": { - "offline_required": true, - "network": "forbidden", - "scrub_env_prefixes": [ - "AGENTOPS_RPI_RUNTIME", - "ANTHROPIC_", - "OPENAI_" - ], - "timeout_seconds": 120 - }, - "scoring": { - "aggregate_threshold": 1, - "dimensions": [ - { - "name": "correctness", - "weight": 2, - "threshold": 1, - "critical": true - }, - { - "name": "process_adherence", - "weight": 2, - "threshold": 1, - "critical": true - }, - { - "name": "runtime_compatibility", - "weight": 1, - "threshold": 1, - "critical": true - }, - { - "name": "artifact_quality", - "weight": 1, - "threshold": 1 - }, - { - "name": "safety", - "weight": 1, - "threshold": 1 - } - ] - }, - "baseline_policy": { - "mode": "compare", - "baseline_path": ".agents/evals/baselines/agentops-core.beads-issue-tracking.baseline.json", - "blocking_gate": "none" - }, - "cases": [ - { - "id": "ao-beads-command-tests", - "title": "ao beads command logic tests pass", - "kind": "command", - "objective": "Protect bead ID parsing, bd show parsing, citation extraction, stale citation classification, missing-bd degradation, audit clustering, and closed-bead learning rendering.", - "runtime": "shell", - "timeout_seconds": 120, - "inputs": { - "cwd": "../../cli", - "shell": "env -u AGENTOPS_RPI_RUNTIME go test -timeout=120s ./cmd/ao -run '^(TestBeadSlugify_|TestParseBDShow_|TestBdShowParsed_|TestExtractCitations_|TestVerifyFileCitation_|TestIsClosedStatus_|TestListBeadIDs_|TestVerifyBead_|TestAuditBeads_|TestFinalizeAuditReport_|TestPatternExistsInRepoSearchesScopedRoots|TestClusterBeads_|TestClusterBeadRecords_|TestRenderLearningBody_)'" - }, - "expectations": [ - { - "type": "exit_code", - "value": 0 - }, - { - "type": "stdout_contains", - "value": "ok \tgithub.com/boshu2/agentops/cli/cmd/ao" - } - ], - "dimensions": [ - "correctness", - "runtime_compatibility", - "safety" - ], - "critical": true - }, - { - "id": "rpi-beads-tasklist-fallback-tests", - "title": "RPI survives unavailable or non-epic beads paths", - "kind": "command", - "objective": "Protect tasklist-mode prompts, execution-packet handoff, retry prompts, latest-epic parsing, and post-phase processing that depends on bd output.", - "runtime": "shell", - "timeout_seconds": 120, - "inputs": { - "cwd": "../../cli", - "shell": "env -u AGENTOPS_RPI_RUNTIME go test -timeout=120s ./cmd/ao -run '^(TestCtx_BuildPromptForPhase_TasklistModeUsesExecutionPacket|TestBuildRetryPrompt_TasklistModeUsesExecutionPacket|TestParseLatestEpicIDFromJSON|TestParseLatestEpicIDFromText|TestPostPhaseProcessing_Discovery|TestPostPhaseProcessing_Implementation)$'" - }, - "expectations": [ - { - "type": "exit_code", - "value": 0 - }, - { - "type": "stdout_contains", - "value": "ok \tgithub.com/boshu2/agentops/cli/cmd/ao" - } - ], - "dimensions": [ - "correctness", - "process_adherence", - "runtime_compatibility" - ], - "critical": true - }, - { - "id": "beads-br-skill-contracts", - "title": "beads-br and Codex twin declare br tracker truth", - "kind": "artifact_check", - "objective": "Protect post-consolidation beads-br skill contracts after retiring the legacy skills/beads umbrella (ag-ez7y6 / age-yt6).", - "expectations": [ - { - "type": "artifact_contains", - "target": "../../skills/beads-br/SKILL.md", - "value": "BEADS_DIR" - }, - { - "type": "artifact_contains", - "target": "../../skills-codex/beads-br/SKILL.md", - "value": "skills/beads-br/SKILL.md" - }, - { - "type": "artifact_contains", - "target": "../../skills/beads-br/SKILL.md", - "value": "reconcile the open parent" - } - ], - "dimensions": [ - "process_adherence", - "artifact_quality", - "runtime_compatibility" - ], - "critical": true - }, - { - "id": "br-tracker-truth-health", - "title": "repo tracker truth is br (bd retired 2026-06-11)", - "kind": "artifact_check", - "objective": "Protect the br+bv tracker flip (ag-joto6): canonical docs must declare br as the tracker and bd as retired legacy; bd commands are no longer a health surface.", - "expectations": [ - { - "type": "artifact_contains", - "target": "../../CLAUDE.md", - "value": "Tracker = br (beads_rust) + bv, as of 2026-06-11." - }, - { - "type": "artifact_contains", - "target": "../../CLAUDE.md", - "value": "bd/Dolt is RETIRED LEGACY (2026-06-11)" - }, - { - "type": "artifact_contains", - "target": "../../AGENTS.md", - "value": "br ready" - } - ], - "dimensions": [ - "correctness", - "runtime_compatibility", - "process_adherence" - ], - "critical": true - }, - { - "id": "beads-br-no-bd-legacy", - "title": "beads-br skills do not advertise retired bd/Dolt paths", - "kind": "command", - "objective": "After beads->beads-br consolidation, eval fixtures must not reference deleted skills/beads trees or resurrect bd/Dolt as the live tracker.", - "runtime": "shell", - "inputs": { - "cwd": "../..", - "shell": "! test -d skills/beads && ! test -d skills-codex/beads && ! rg -n 'skills/beads/references/MOLECULES|skills/beads/scripts/validate' skills/plan/references/templates.md skills-codex/plan/references/templates.md && rg -q 'BEADS_DIR' skills/beads-br/SKILL.md" - }, - "expectations": [ - { - "type": "exit_code", - "value": 0 - } - ], - "dimensions": [ - "process_adherence", - "artifact_quality" - ] - }, - { - "id": "agent-instructions-use-br", - "title": "agent instructions preserve br as the task tracker (bd retired)", - "kind": "artifact_check", - "objective": "Keep repo onboarding and lifecycle rules tied to the br tracker instead of markdown task lists; bd-era assertions retired with the 2026-06-11 flip (ag-joto6 / ag-s43tg).", - "expectations": [ - { - "type": "artifact_contains", - "target": "../../skills/beads-br/SKILL.md", - "value": "reconcile the open parent in the same session" - }, - { - "type": "artifact_contains", - "target": "../../docs/troubleshooting.md", - "value": "RPI falls back to tasklist mode" - } - ], - "dimensions": [ - "process_adherence", - "artifact_quality", - "safety" - ] - } - ] -} \ No newline at end of file diff --git a/evals/agentops-core/cli-command-surface-matrix.json b/evals/agentops-core/cli-command-surface-matrix.json index 22aae861f..df7b7e262 100644 --- a/evals/agentops-core/cli-command-surface-matrix.json +++ b/evals/agentops-core/cli-command-surface-matrix.json @@ -41,26 +41,26 @@ }, "expectations": [ {"type": "exit_code", "value": 0}, - {"type": "stdout_contains", "value": "cli-command-headings: top=32 sub=51 all=86"}, + {"type": "stdout_contains", "value": "cli-command-headings: top=33 sub=49 all=85"}, {"type": "stdout_contains", "value": "cli-help-matrix-ok"} ], "dimensions": ["correctness", "runtime_compatibility", "artifact_quality"], "critical": true }, { - "id": "cli-reference-and-map-gates", - "title": "CLI generated docs and skill map remain current", + "id": "cli-reference-gate", + "title": "CLI generated docs remain current", "kind": "command", - "objective": "Keep the generated CLI reference and CLI-to-skills map synchronized with the Go command tree before relying on the matrix smoke.", + "objective": "Keep the generated CLI reference synchronized with the Go command tree before relying on the matrix smoke.", "runtime": "shell", "timeout_seconds": 120, "inputs": { "cwd": "../..", - "shell": "scripts/generate-cli-reference.sh --check && bash scripts/validate-cli-skills-map.sh" + "shell": "scripts/generate-cli-reference.sh --check" }, "expectations": [ {"type": "exit_code", "value": 0}, - {"type": "stdout_contains", "value": "CLI_SKILLS_MAP: PASS"} + {"type": "stdout_contains", "value": "cli/docs/COMMANDS.md is up to date."} ], "dimensions": ["correctness", "process_adherence", "artifact_quality"], "critical": true diff --git a/evals/agentops-core/cli-contracts.json b/evals/agentops-core/cli-contracts.json index 1a9b5753b..f5a1f9fb8 100644 --- a/evals/agentops-core/cli-contracts.json +++ b/evals/agentops-core/cli-contracts.json @@ -43,22 +43,6 @@ "dimensions": ["correctness", "artifact_quality"], "critical": true }, - { - "id": "cli-skills-map-current", - "title": "CLI skills map gate passes", - "kind": "command", - "objective": "Verify the checked-in CLI-to-skills map stays aligned with generated command headings.", - "runtime": "shell", - "inputs": { - "cwd": "../..", - "shell": "bash scripts/validate-cli-skills-map.sh" - }, - "expectations": [ - {"type": "exit_code", "value": 0} - ], - "dimensions": ["correctness", "process_adherence"], - "critical": true - }, { "id": "json-flag-ci-gate", "title": "JSON flag consistency gate remains in CI", diff --git a/evals/agentops-core/context-comprehension.json b/evals/agentops-core/context-comprehension.json index 0e7eb7034..bf476607f 100644 --- a/evals/agentops-core/context-comprehension.json +++ b/evals/agentops-core/context-comprehension.json @@ -60,7 +60,7 @@ "dimensions": ["context_comprehension"] }, { - "id": "minimal-skill-post-mortem", + "id": "minimal-skill-postmortem", "title": "postmortem SKILL.md produces valid JSON with word count", "kind": "command", "objective": "Verify the fixture runs against a minimal skill and produces valid structured output regardless of score.", diff --git a/evals/agentops-core/converter-update-runtime-parity.json b/evals/agentops-core/converter-update-runtime-parity.json index 32df302a2..76902d49f 100644 --- a/evals/agentops-core/converter-update-runtime-parity.json +++ b/evals/agentops-core/converter-update-runtime-parity.json @@ -100,22 +100,6 @@ ], "dimensions": ["correctness", "artifact_quality", "safety"], "critical": true - }, - { - "id": "recover-and-codex-validator-contracts", - "title": "Recover/goals and Codex validator contracts preserve runtime-specific assumptions", - "kind": "artifact_check", - "objective": "Ensure recover/goals validators keep validating shared metadata when present while accepting minimal Codex frontmatter and dollar-skill references.", - "expectations": [ - {"type": "artifact_contains", "target": "../../skills/goals/scripts/validate.sh", "value": "SKILL.md references evolve skill"}, - {"type": "artifact_contains", "target": "../../skills-codex/goals/scripts/validate.sh", "value": "SKILL.md references evolve skill"}, - {"type": "artifact_contains", "target": "../../skills/recover/scripts/validate.sh", "value": "tier: session when metadata is present"}, - {"type": "artifact_contains", "target": "../../skills-codex/recover/scripts/validate.sh", "value": "tier: session when metadata is present"}, - {"type": "artifact_contains", "target": "../../skills-codex/recover/.agentops-generated.json", "value": "\"source_skill\": \"skills/recover\""}, - {"type": "artifact_contains", "target": "../../skills-codex/converter/.agentops-generated.json", "value": "\"source_skill\": \"skills/converter\""} - ], - "dimensions": ["process_adherence", "artifact_quality", "runtime_compatibility"], - "critical": true } ] } diff --git a/evals/agentops-core/docs-release-governance.json b/evals/agentops-core/docs-release-governance.json index a57c43bb6..681c0f04f 100644 --- a/evals/agentops-core/docs-release-governance.json +++ b/evals/agentops-core/docs-release-governance.json @@ -43,7 +43,6 @@ {"type": "exit_code", "value": 0}, {"type": "stdout_contains", "value": "PASS: Link validation"}, {"type": "stdout_contains", "value": "PASS: Skill count validation"}, - {"type": "stdout_contains", "value": "PASS: CLI skills map validation"}, {"type": "stdout_contains", "value": "PASS: release message freeze intact"}, {"type": "stdout_contains", "value": "PASS: doc-release gate succeeded"} ], @@ -54,17 +53,16 @@ "id": "docs-policy-parity-gates", "title": "docs policy parity gates pass", "kind": "command", - "objective": "Keep CI policy parity, CLI-skills map generation, and metadata inventory directly protected as eval canaries.", + "objective": "Keep CI policy parity and the metadata inventory directly protected as eval canaries.", "runtime": "shell", "timeout_seconds": 120, "inputs": { "cwd": "../..", - "shell": "bash scripts/validate-ci-policy-parity.sh && bash scripts/validate-cli-skills-map.sh && bash tests/docs/validate-skill-count.sh" + "shell": "bash scripts/validate-ci-policy-parity.sh && bash tests/docs/validate-skill-count.sh" }, "expectations": [ {"type": "exit_code", "value": 0}, {"type": "stdout_contains", "value": "CI_POLICY_PARITY: PASS"}, - {"type": "stdout_contains", "value": "CLI_SKILLS_MAP: PASS"}, {"type": "stdout_contains", "value": "PASS: metadata inventory covers"} ], "dimensions": ["correctness", "process_adherence", "artifact_quality"], @@ -127,14 +125,12 @@ "id": "doc-release-script-contracts", "title": "doc-release scripts preserve fail-closed parity contracts", "kind": "artifact_check", - "objective": "Ensure documentation release scripts keep their fail-closed checks for release notes, skill counts, CLI mapping, and CI blocking policy.", + "objective": "Ensure documentation release scripts keep their checks for release notes, metadata inventory, and CI policy.", "expectations": [ {"type": "artifact_contains", "target": "../../tests/docs/validate-doc-release.sh", "value": "run_check \"Link validation\""}, {"type": "artifact_contains", "target": "../../tests/docs/validate-doc-release.sh", "value": "run_check \"Skill count validation\""}, - {"type": "artifact_contains", "target": "../../tests/docs/validate-doc-release.sh", "value": "run_check \"CLI skills map validation\""}, {"type": "artifact_contains", "target": "../../tests/docs/validate-doc-release.sh", "value": "run_check \"Release message freeze validation\""}, {"type": "artifact_contains", "target": "../../tests/docs/validate-skill-count.sh", "value": "metadata inventory covers"}, - {"type": "artifact_contains", "target": "../../scripts/validate-cli-skills-map.sh", "value": "top audit line must declare ' generated CLI command headings'"}, {"type": "artifact_contains", "target": "../../scripts/validate-ci-policy-parity.sh", "value": "CI_POLICY_PARITY: PASS"} ], "dimensions": ["process_adherence", "artifact_quality", "safety"], diff --git a/evals/agentops-core/efficiency-budgets.json b/evals/agentops-core/efficiency-budgets.json deleted file mode 100644 index 4c8da640d..000000000 --- a/evals/agentops-core/efficiency-budgets.json +++ /dev/null @@ -1,81 +0,0 @@ -{ - "schema_version": 1, - "id": "agentops-core.efficiency-budgets", - "name": "AgentOps Efficiency Budgets Canary", - "description": "Offline public canary for eval runtime budget visibility, fast coverage reporting, and conditional pre-push eval gating.", - "practices": ["llm-eval-harness", "ai-assisted-dev"], - "domain": "mixed", - "visibility": "public_canary", - "tier": "deterministic", - "owners": ["agentops"], - "tags": ["canary", "offline", "efficiency", "budget"], - "allowed_runtimes": ["static", "shell"], - "environment": { - "offline_required": true, - "network": "forbidden", - "scrub_env_prefixes": ["AGENTOPS_RPI_RUNTIME", "ANTHROPIC_", "OPENAI_"], - "timeout_seconds": 90 - }, - "scoring": { - "aggregate_threshold": 1, - "dimensions": [ - {"name": "correctness", "weight": 1, "threshold": 1, "critical": true}, - {"name": "process_adherence", "weight": 1, "threshold": 1}, - {"name": "artifact_quality", "weight": 1, "threshold": 1}, - {"name": "efficiency", "weight": 2, "threshold": 1, "critical": true} - ] - }, - "baseline_policy": {"mode": "compare", "baseline_path": ".agents/evals/baselines/agentops-core.efficiency-budgets.baseline.json", "blocking_gate": "none"}, - "cases": [ - { - "id": "coverage-command-budget", - "title": "coverage command completes under budget and reports all dimensions", - "kind": "command", - "objective": "Ensure product-wide eval coverage can be measured quickly enough for evolve and local gate feedback loops.", - "runtime": "shell", - "inputs": { - "cwd": "../..", - "shell": "tmp=$(mktemp -d); trap 'rm -rf \"$tmp\"' EXIT; timeout 60s bash -lc 'cd cli && env -u AGENTOPS_RPI_RUNTIME go run ./cmd/ao eval coverage --root ../evals/agentops-core --json' > \"$tmp/coverage.json\"; python3 -c 'import json, sys; data=json.load(open(sys.argv[1])); assert data[\"suite_count\"] >= 11; assert \"efficiency\" in data.get(\"required_dimensions\", []); assert not data.get(\"missing_required_dimensions\")' \"$tmp/coverage.json\"" - }, - "expectations": [ - {"type": "exit_code", "value": 0} - ], - "dimensions": ["correctness", "efficiency"], - "critical": true - }, - { - "id": "pre-push-eval-conditional", - "title": "pre-push gate runs eval canaries only for relevant changes", - "kind": "artifact_check", - "objective": "Keep local eval validation fast by only running public canaries when eval-related surfaces changed.", - "expectations": [ - {"type": "artifact_contains", "target": "../../scripts/pre-push-gate.sh", "value": "AgentOps eval canaries"}, - {"type": "artifact_contains", "target": "../../scripts/pre-push-gate.sh", "value": "^evals/|^schemas/eval-|^scripts/eval-agentops\\.sh$|^cli/internal/eval/|^cli/cmd/ao/eval"}, - {"type": "artifact_contains", "target": "../../scripts/pre-push-gate.sh", "value": "eval_args=(--fast)"} - ], - "dimensions": ["efficiency", "process_adherence"] - }, - { - "id": "testing-docs-budget", - "title": "testing docs publish eval canary runtime budget", - "kind": "artifact_check", - "objective": "Keep the expected public eval runtime visible to operators.", - "expectations": [ - {"type": "artifact_contains", "target": "../../docs/TESTING.md", "value": "| AgentOps eval advisory corpus | `scripts/eval-agentops.sh --fast` | ~5-10m |"}, - {"type": "artifact_contains", "target": "../../docs/TESTING.md", "value": "Run it when changing eval contracts"} - ], - "dimensions": ["artifact_quality", "efficiency"] - }, - { - "id": "efficiency-dimension-contract", - "title": "eval contract defines efficiency as a score dimension", - "kind": "artifact_check", - "objective": "Protect the canonical definition that eval runs score time, budget, retry, and output limits.", - "expectations": [ - {"type": "artifact_contains", "target": "../../docs/contracts/eval-environment.md", "value": "`efficiency` | The run stayed within expected time, budget, retry, and output limits."} - ], - "dimensions": ["artifact_quality", "efficiency"], - "critical": true - } - ] -} diff --git a/evals/agentops-core/fixtures/cli-command-surface-smoke.sh b/evals/agentops-core/fixtures/cli-command-surface-smoke.sh index 15cdce6f1..e1a27e05f 100755 --- a/evals/agentops-core/fixtures/cli-command-surface-smoke.sh +++ b/evals/agentops-core/fixtures/cli-command-surface-smoke.sh @@ -25,7 +25,7 @@ fi # shellcheck disable=SC2016 # literal backticks delimit generated Markdown command headings. mapfile -t commands < <(rg '^#{3,4} `ao ' "$DOCS_PATH" | sed -E 's/^.*`([^`]+)`.*/\1/') -if [[ "${#commands[@]}" -ne 86 ]]; then +if [[ "${#commands[@]}" -ne 85 ]]; then printf 'unexpected command matrix size: %s\n' "${#commands[@]}" >&2 exit 1 fi diff --git a/evals/agentops-core/goals-autodev-evolve.json b/evals/agentops-core/goals-autodev-evolve.json deleted file mode 100644 index e529a131e..000000000 --- a/evals/agentops-core/goals-autodev-evolve.json +++ /dev/null @@ -1,126 +0,0 @@ -{ - "schema_version": 1, - "id": "agentops-core.goals-autodev-evolve", - "name": "AgentOps Goals Autodev Evolve Canary", - "description": "Offline public canary for the fitness and autonomous-loop control plane: ao goals, PROGRAM/AUTODEV validation, and ao evolve operator defaults.", - "practices": ["llm-eval-harness", "ai-assisted-dev"], - "domain": "mixed", - "visibility": "public_canary", - "tier": "deterministic", - "owners": ["agentops"], - "tags": ["canary", "offline", "goals", "autodev", "evolve", "fitness", "operator-contract"], - "allowed_runtimes": ["shell", "static"], - "environment": { - "offline_required": true, - "network": "forbidden", - "scrub_env_prefixes": ["AGENTOPS_RPI_RUNTIME", "ANTHROPIC_", "OPENAI_"], - "timeout_seconds": 120 - }, - "scoring": { - "aggregate_threshold": 1, - "dimensions": [ - {"name": "correctness", "weight": 2, "threshold": 1, "critical": true}, - {"name": "process_adherence", "weight": 2, "threshold": 1, "critical": true}, - {"name": "runtime_compatibility", "weight": 1, "threshold": 1, "critical": true}, - {"name": "artifact_quality", "weight": 1, "threshold": 1}, - {"name": "learning_closure", "weight": 1, "threshold": 1} - ] - }, - "baseline_policy": {"mode": "compare", "baseline_path": ".agents/evals/baselines/agentops-core.goals-autodev-evolve.baseline.json", "blocking_gate": "none"}, - "cases": [ - { - "id": "goals-autodev-evolve-focused-tests", - "title": "goals, autodev, and loop focused tests pass", - "kind": "command", - "objective": "Protect goals lifecycle measurement, GOALS.md validation, JSON validity, PROGRAM/AUTODEV validation, and rehomed loop next-work, blocked-event, and stop-marker behavior.", - "runtime": "shell", - "timeout_seconds": 120, - "inputs": { - "cwd": "../../cli", - "shell": "env -u AGENTOPS_RPI_RUNTIME go test -timeout=120s ./cmd/ao -run '^(TestGoals_Integration_FullLifecycle|TestGoals_Integration_MeasureDirectivesJSON|TestGoalsMeasure_BasicRun|TestGoalsMeasure_WeightedScoring|TestGoalsValidate_ValidMD|TestJSONValidity_GoalsMeasure|TestJSONValidity_GoalsDrift|TestCobraAutodevValidateJSON|TestAutodev_Integration_ValidateJSON|TestLoopNextWork_(Step1Pick|LadderExhaustionEmitsBlockedHint|RegisteredOnLoop|IncludeOperatorShape)|TestLoopBlocked_(WriteThenListRoundTrip|RegisteredOnLoop)|TestLoopWriteStopMarker_(Table|RegisteredOnLoop))$'" - }, - "expectations": [ - {"type": "exit_code", "value": 0}, - {"type": "stdout_contains", "value": "ok \tgithub.com/boshu2/agentops/cli/cmd/ao"} - ], - "dimensions": ["correctness", "runtime_compatibility", "learning_closure"], - "critical": true - }, - { - "id": "repo-fitness-contract-smoke", - "title": "repo GOALS and PROGRAM contracts validate from the built CLI", - "kind": "command", - "objective": "Ensure the checked-in repo fitness and operational contracts are executable by the same binary operators use during autonomous loops.", - "runtime": "shell", - "timeout_seconds": 120, - "inputs": { - "cwd": "../..", - "shell": "tmp=$(mktemp -d); trap 'rm -rf \"$tmp\"' EXIT; (cd cli && env -u AGENTOPS_RPI_RUNTIME go build -o \"$tmp/ao\" ./cmd/ao); \"$tmp/ao\" goals validate --json | jq -e '.valid == true' >/dev/null; \"$tmp/ao\" autodev validate --json | jq -e '.valid == true and .program_path == \"PROGRAM.md\"' >/dev/null; \"$tmp/ao\" rpi loop --dry-run --max-cycles 1 | tee \"$tmp/loop.txt\"; rg -q 'RPI loop finished' \"$tmp/loop.txt\"" - }, - "expectations": [ - {"type": "exit_code", "value": 0}, - {"type": "stdout_contains", "value": "RPI loop finished"} - ], - "dimensions": ["correctness", "process_adherence", "runtime_compatibility"], - "critical": true - }, - { - "id": "fitness-operator-skill-contracts", - "title": "goals, autodev, and evolve skills preserve operator routing", - "kind": "artifact_check", - "objective": "Keep skill-level instructions aligned with the CLI surfaces that run the fitness/autonomy loop.", - "expectations": [ - {"type": "artifact_contains", "target": "../../skills/goals/SKILL.md", "value": "ao goals measure --json"}, - {"type": "artifact_contains", "target": "../../skills/goals/SKILL.md", "value": "When both files exist, GOALS.md takes precedence."}, - {"type": "artifact_contains", "target": "../../skills/evolve/SKILL.md", "value": "`PROGRAM.md` takes precedence"}, - {"type": "artifact_contains", "target": "../../skills/evolve/SKILL.md", "value": "main tests green alone never marks a cycle successful"}, - {"type": "artifact_contains", "target": "../../skills/evolve/SKILL.md", "value": "Failing goals and directive gaps"}, - {"type": "artifact_contains", "target": "../../skills/evolve/SKILL.md", "value": "Testing improvements"} - ], - "dimensions": ["process_adherence", "artifact_quality", "learning_closure"], - "critical": true - }, - { - "id": "autodev-contract-doc-required-sections", - "title": "autodev program contract documents required sections", - "kind": "artifact_check", - "objective": "Protect the repo-local operational layer that bounds autonomous work before evolve starts mutating files.", - "expectations": [ - {"type": "artifact_contains", "target": "../../docs/contracts/autodev-program.md", "value": "### `Objective`"}, - {"type": "artifact_contains", "target": "../../docs/contracts/autodev-program.md", "value": "### `Mutable Scope`"}, - {"type": "artifact_contains", "target": "../../docs/contracts/autodev-program.md", "value": "### `Immutable Scope`"}, - {"type": "artifact_contains", "target": "../../docs/contracts/autodev-program.md", "value": "### `Validation Commands`"}, - {"type": "artifact_contains", "target": "../../docs/contracts/autodev-program.md", "value": "## Evolve Integration"}, - {"type": "artifact_contains", "target": "../../PROGRAM.md", "value": "## Validation Commands"} - ], - "dimensions": ["process_adherence", "artifact_quality"] - }, - { - "id": "cli-docs-expose-fitness-commands", - "title": "generated CLI docs expose goals, autodev, and evolve commands", - "kind": "artifact_check", - "objective": "Keep the generated command reference aligned with the operator surfaces used to measure and run autonomous improvement.", - "expectations": [ - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "### `ao goals`"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "### `ao autodev`"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "### `ao loop`"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "Track, measure, and validate project fitness goals."}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "Operations on the /evolve cycle history and related Loop bounded-context state."} - ], - "dimensions": ["process_adherence", "artifact_quality", "runtime_compatibility"] - }, - { - "id": "evolve-code-defaults", - "title": "loop source preserves rehomed mode defaults", - "kind": "artifact_check", - "objective": "Ensure the kept loop surface owns the burst/loop mode contract rehomed from the retired evolve CLI.", - "expectations": [ - {"type": "artifact_contains", "target": "../../cli/cmd/ao/loop_mode.go", "value": "Loop-execution-mode contract, re-homed from the deleted `ao evolve` CLI"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/loop_mode.go", "value": "loopModeBurst = \"burst\""}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/loop_mode.go", "value": "loopModeLoop = \"loop\""}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/loop_mode.go", "value": "func validateLoopMode(mode string) error"} - ], - "dimensions": ["correctness", "process_adherence", "artifact_quality"] - } - ] -} diff --git a/evals/agentops-core/goals-management-lifecycle.json b/evals/agentops-core/goals-management-lifecycle.json deleted file mode 100644 index 0d2ed50cd..000000000 --- a/evals/agentops-core/goals-management-lifecycle.json +++ /dev/null @@ -1,150 +0,0 @@ -{ - "schema_version": 1, - "id": "agentops-core.goals-management-lifecycle", - "name": "AgentOps Goals Management Lifecycle Canary", - "description": "Offline public canary for the full ao goals management lifecycle: init, add, steer, history, drift, export, prune, migrate, meta, measure, and validate.", - "practices": ["llm-eval-harness", "dora-metrics"], - "domain": "goals", - "visibility": "public_canary", - "tier": "deterministic", - "owners": ["agentops"], - "tags": ["canary", "offline", "goals", "fitness", "management", "lifecycle", "evolve"], - "allowed_runtimes": ["shell", "static"], - "environment": { - "offline_required": true, - "network": "forbidden", - "scrub_env_prefixes": ["AGENTOPS_RPI_RUNTIME", "ANTHROPIC_", "OPENAI_"], - "timeout_seconds": 180 - }, - "scoring": { - "aggregate_threshold": 1, - "dimensions": [ - {"name": "correctness", "weight": 2, "threshold": 1, "critical": true}, - {"name": "process_adherence", "weight": 2, "threshold": 1, "critical": true}, - {"name": "runtime_compatibility", "weight": 1, "threshold": 1, "critical": true}, - {"name": "artifact_quality", "weight": 1, "threshold": 1}, - {"name": "learning_closure", "weight": 1, "threshold": 1} - ] - }, - "baseline_policy": {"mode": "compare", "baseline_path": ".agents/evals/baselines/agentops-core.goals-management-lifecycle.baseline.json", "blocking_gate": "none"}, - "cases": [ - { - "id": "goals-management-focused-tests", - "title": "Goals management lifecycle tests pass", - "kind": "command", - "objective": "Protect the command-level behavior for goals initialization, goal addition, steering, pruning, migration, history, drift, export, meta-goals, validation, and JSON output.", - "runtime": "shell", - "timeout_seconds": 180, - "inputs": { - "cwd": "../../cli", - "shell": "env -u AGENTOPS_RPI_RUNTIME go test -timeout=180s ./cmd/ao -run '^(TestGoalsCmd_|TestGoals_Integration_|TestGoalsInit_|TestGoalsPrune_|TestGoalsSteer_|TestGoalsValidate_|TestJSONValidity_Goals(Validate|ValidateInvalid|Measure|Export|Meta|History|Drift|Prune|Init|SteerAdd|MeasureDirectives)|TestCobraGoals(ValidateCommand|ValidateJSON|ParentHelp|ResolveGoalsFile)|TestNegativePath_GoalsAddInvalidID|TestDirectivesFromPillars_EmptyGoals)$'" - }, - "expectations": [ - {"type": "exit_code", "value": 0}, - {"type": "stdout_contains", "value": "ok \tgithub.com/boshu2/agentops/cli/cmd/ao"} - ], - "dimensions": ["correctness", "runtime_compatibility", "learning_closure"], - "critical": true - }, - { - "id": "goals-management-help-smoke", - "title": "Goals lifecycle help surfaces work from built ao", - "kind": "command", - "objective": "Ensure every goals lifecycle command remains routable from the shipped CLI help surface without mutating repository state.", - "runtime": "shell", - "timeout_seconds": 120, - "inputs": { - "cwd": "../..", - "shell": "tmp=$(mktemp -d); trap 'rm -rf \"$tmp\"' EXIT; (cd cli && env -u AGENTOPS_RPI_RUNTIME go build -o \"$tmp/ao\" ./cmd/ao); \"$tmp/ao\" goals --help >/dev/null; \"$tmp/ao\" goals measure --help >/dev/null; \"$tmp/ao\" goals validate --help >/dev/null; \"$tmp/ao\" goals drift --help >/dev/null; \"$tmp/ao\" goals export --help >/dev/null; \"$tmp/ao\" goals history --help >/dev/null; \"$tmp/ao\" goals add --help >/dev/null; \"$tmp/ao\" goals init --help >/dev/null; \"$tmp/ao\" goals meta --help >/dev/null; \"$tmp/ao\" goals migrate --help >/dev/null; \"$tmp/ao\" goals prune --help >/dev/null; \"$tmp/ao\" goals steer --help >/dev/null; \"$tmp/ao\" goals steer add --help >/dev/null; \"$tmp/ao\" goals steer prioritize --help >/dev/null; \"$tmp/ao\" goals steer remove --help >/dev/null" - }, - "expectations": [ - {"type": "exit_code", "value": 0} - ], - "dimensions": ["runtime_compatibility", "process_adherence"], - "critical": true - }, - { - "id": "goals-management-generated-docs", - "title": "Generated CLI docs expose the goals lifecycle", - "kind": "artifact_check", - "objective": "Keep generated command reference coverage aligned with every goals lifecycle subcommand operators use during autonomous improvement.", - "expectations": [ - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "### `ao goals`"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "Track, measure, and validate project fitness goals."}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "#### `ao goals measure`"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "--directives Output directives as JSON (skip gate checks)"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "#### `ao goals validate`"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "#### `ao goals drift`"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "#### `ao goals export`"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "#### `ao goals history`"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "--since string Show entries since date (YYYY-MM-DD)"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "#### `ao goals add`"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "--weight int Goal weight (1-10) (default 5)"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "#### `ao goals init`"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "--template string Goal template (go-cli, python-lib, web-app, rust-cli, generic)"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "#### `ao goals meta`"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "#### `ao goals migrate`"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "--to-md Convert GOALS.yaml to GOALS.md format"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "#### `ao goals prune`"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "#### `ao goals steer`"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "##### `ao goals steer add`"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "##### `ao goals steer prioritize`"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "##### `ao goals steer remove`"} - ], - "dimensions": ["artifact_quality", "process_adherence", "runtime_compatibility"], - "critical": true - }, - { - "id": "goals-management-source-contracts", - "title": "Source contracts preserve goals lifecycle routing", - "kind": "artifact_check", - "objective": "Lock the command registration and option wiring that makes goals a measurable control plane rather than static documentation.", - "expectations": [ - {"type": "artifact_contains", "target": "../../cli/cmd/ao/goals.go", "value": "When both exist, GOALS.md takes precedence."}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/goals.go", "value": "goalsCmd.PersistentFlags().StringVar(&goalsFile"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/goals.go", "value": "defaultGoalsTimeoutSeconds = 240"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/goals_measure.go", "value": "goalsMeasureCmd.Flags().BoolVar(&goalsMeasureDirectives"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/goals_measure.go", "value": "RunMeasure(goals.MeasureOptions"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/goals_validate.go", "value": "RunValidate(goals.ValidateOptions"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/goals_init.go", "value": "TemplatesFS: embedded.TemplatesFS"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/goals_init.go", "value": "RegisterFlagCompletionFunc(\"template\""}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/goals_add.go", "value": "Args: cobra.ExactArgs(2)"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/goals_add.go", "value": "goalsAddCmd.Flags().IntVar(&goalsAddWeight"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/goals_steer.go", "value": "Use: \"add \""}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/goals_steer.go", "value": "Use: \"remove <number>\""}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/goals_steer.go", "value": "Use: \"prioritize <number> <new-position>\""}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/goals_history.go", "value": "goalsHistoryCmd.Flags().StringVar(&goalsHistorySince"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/goals_prune.go", "value": "RunPrune(goals.PruneOptions"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/goals_migrate.go", "value": "migrateCmd.Flags().BoolVar(&migrateToMD"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/goals_export.go", "value": "RunExport(goals.ExportOptions"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/goals_drift.go", "value": "RunDrift(goals.DriftOptions"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/goals_meta.go", "value": "RunMeta(goals.MetaOptions"} - ], - "dimensions": ["correctness", "process_adherence", "artifact_quality"], - "critical": true - }, - { - "id": "goals-management-skill-and-setup-docs", - "title": "Goals skill and setup docs preserve lifecycle guidance", - "kind": "artifact_check", - "objective": "Ensure operator guidance still routes goals work through the CLI lifecycle that the eval baseline protects.", - "expectations": [ - {"type": "artifact_contains", "target": "../../skills/goals/SKILL.md", "value": "ao goals measure --json"}, - {"type": "artifact_contains", "target": "../../skills/goals/SKILL.md", "value": "When both files exist, GOALS.md takes precedence."}, - {"type": "artifact_contains", "target": "../../skills/goals/SKILL.md", "value": "| `/goals init`, \"bootstrap goals\" | **init** | `ao goals init` |"}, - {"type": "artifact_contains", "target": "../../skills/goals/SKILL.md", "value": "| `/goals steer`, \"manage directives\" | **steer** | `ao goals steer` |"}, - {"type": "artifact_contains", "target": "../../skills/goals/SKILL.md", "value": "| `/goals add`, \"add goal\" | **add** | `ao goals add` |"}, - {"type": "artifact_contains", "target": "../../skills/goals/SKILL.md", "value": "| `/goals drift`, \"goal drift\" | **drift** | `ao goals drift` |"}, - {"type": "artifact_contains", "target": "../../skills/goals/SKILL.md", "value": "| `/goals history`, \"goal history\" | **history** | `ao goals history` |"}, - {"type": "artifact_contains", "target": "../../skills/goals/SKILL.md", "value": "| `/goals export`, \"export goals\" | **export** | `ao goals export` |"}, - {"type": "artifact_contains", "target": "../../skills/goals/SKILL.md", "value": "| `/goals meta`, \"meta goals\" | **meta** | `ao goals meta` |"}, - {"type": "artifact_contains", "target": "../../skills/goals/SKILL.md", "value": "| `/goals prune`, \"prune goals\", \"clean goals\" | **prune** | `ao goals prune` |"}, - {"type": "artifact_contains", "target": "../../skills/goals/SKILL.md", "value": "| `/goals migrate`, \"migrate goals\" | **migrate** | `ao goals migrate` |"}, - {"type": "artifact_contains", "target": "../../docs/evolve-setup.md", "value": "Bootstrap with `ao goals init`"}, - {"type": "artifact_contains", "target": "../../docs/evolve-setup.md", "value": "Run `ao goals migrate --to-md`."} - ], - "dimensions": ["process_adherence", "artifact_quality", "learning_closure"], - "critical": true - } - ] -} diff --git a/evals/agentops-core/headless-runtime-skills.json b/evals/agentops-core/headless-runtime-skills.json index f5fda1d9e..2ad6e62aa 100644 --- a/evals/agentops-core/headless-runtime-skills.json +++ b/evals/agentops-core/headless-runtime-skills.json @@ -2,7 +2,7 @@ "schema_version": 1, "id": "agentops-core.headless-runtime-skills", "name": "AgentOps Headless Runtime Skills Canary", - "description": "Public deterministic canary for Claude and Codex headless skill inventory validation, fallback controls, and CI/release wiring.", + "description": "Optional canary for caller-selected Claude and Codex headless skill inventory validation.", "practices": ["llm-eval-harness", "sre"], "domain": "runtime", "visibility": "public_canary", @@ -15,8 +15,7 @@ "network": "allowed", "scrub_env_prefixes": ["AGENTOPS_RPI_RUNTIME"], "isolate_codex_home": true, - "timeout_seconds": 180, - "max_attempts": 1 + "timeout_seconds": 180 }, "scoring": { "aggregate_threshold": 1, @@ -51,7 +50,7 @@ "id": "headless-runtime-validation", "title": "headless runtime skill validation command passes", "kind": "command", - "objective": "Run the same headless runtime skill validation surface used by CI and release gates with bounded retries and timeout.", + "objective": "Run the optional headless runtime inventory validator when a caller explicitly selects it.", "runtime": "shell", "timeout_seconds": 210, "inputs": { @@ -67,17 +66,16 @@ "critical": true }, { - "id": "headless-runtime-gate-wiring", - "title": "headless runtime skill validation remains wired into gates", - "kind": "artifact_check", - "objective": "Keep the runtime inventory validator visible in CI, local release, fast pre-push full mode, and smoke-test dispatch.", - "expectations": [ - {"type": "artifact_contains", "target": "../../.github/workflows/validate.yml", "value": "Validate headless runtime skills"}, - {"type": "artifact_contains", "target": "../../.github/workflows/validate.yml", "value": "./scripts/validate-headless-runtime-skills.sh"}, - {"type": "artifact_contains", "target": "../../scripts/ci-local-release.sh", "value": "Headless runtime skill smoke"}, - {"type": "artifact_contains", "target": "../../scripts/pre-push-gate.sh", "value": "headless runtime skills"}, - {"type": "artifact_contains", "target": "../../tests/smoke-test.sh", "value": "test-headless-runtime-skills.sh"} - ], + "id": "headless-runtime-remains-optional", + "title": "headless runtime validation is not a core gate", + "kind": "command", + "objective": "Prove optional live-runtime probing is not required by ordinary CI, local release, or smoke dispatch.", + "runtime": "shell", + "inputs": { + "cwd": "../..", + "shell": "! rg -n 'validate-headless-runtime-skills|test-headless-runtime-skills' .github/workflows/validate.yml scripts/ci-local-release.sh tests/smoke-test.sh" + }, + "expectations": [{"type": "exit_code", "value": 0}], "dimensions": ["process_adherence", "artifact_quality", "runtime_compatibility"], "critical": true }, diff --git a/evals/agentops-core/hidden-cli-internals.json b/evals/agentops-core/hidden-cli-internals.json deleted file mode 100644 index 5a1677f73..000000000 --- a/evals/agentops-core/hidden-cli-internals.json +++ /dev/null @@ -1,134 +0,0 @@ -{ - "schema_version": 1, - "id": "agentops-core.hidden-cli-internals", - "name": "AgentOps Hidden CLI Internals Canary", - "description": "Offline public canary for hidden and backwards-compatible CLI internals that support knowledge storage, indexing, tempering, and MemRL migration.", - "practices": ["llm-eval-harness", "pragmatic-programmer"], - "domain": "cli", - "visibility": "public_canary", - "tier": "deterministic", - "owners": ["agentops"], - "tags": ["canary", "offline", "cli", "hidden", "internals", "store", "temper", "index", "migrate"], - "allowed_runtimes": ["shell", "static"], - "environment": { - "offline_required": true, - "network": "forbidden", - "scrub_env_prefixes": ["AGENTOPS_RPI_RUNTIME", "ANTHROPIC_", "OPENAI_"], - "timeout_seconds": 180 - }, - "scoring": { - "aggregate_threshold": 1, - "dimensions": [ - {"name": "correctness", "weight": 2, "threshold": 1, "critical": true}, - {"name": "process_adherence", "weight": 1, "threshold": 1, "critical": true}, - {"name": "runtime_compatibility", "weight": 1, "threshold": 1, "critical": true}, - {"name": "artifact_quality", "weight": 1, "threshold": 1}, - {"name": "safety", "weight": 1, "threshold": 1} - ] - }, - "baseline_policy": {"mode": "compare", "baseline_path": ".agents/evals/baselines/agentops-core.hidden-cli-internals.baseline.json", "blocking_gate": "none"}, - "cases": [ - { - "id": "hidden-cli-internals-go-tests", - "title": "Hidden CLI internals tests pass", - "kind": "command", - "objective": "Protect hidden and backwards-compatible command behavior for store, temper, index, and MemRL migration paths.", - "runtime": "shell", - "timeout_seconds": 180, - "inputs": { - "cwd": "../../cli", - "shell": "env -u AGENTOPS_RPI_RUNTIME go test -timeout=180s ./cmd/ao -run '^(TestCobra(StoreParentHelp|TemperParentHelp|IndexCommand|IndexCheckCommand|MigrateCommand|IndexHelpers|FeedbackMigrateJSONLFiles|FeedbackMigrateJSONLFilesDryRun)|TestRunIndex_JSONOutput|TestRunTemperStatus_JSON|TestJSONValidity_(Index|TemperStatus|StoreSearchIndex|StoreStats))$'" - }, - "expectations": [ - {"type": "exit_code", "value": 0}, - {"type": "stdout_contains", "value": "ok \tgithub.com/boshu2/agentops/cli/cmd/ao"} - ], - "dimensions": ["correctness", "runtime_compatibility"], - "critical": true - }, - { - "id": "hidden-cli-internals-help-smoke", - "title": "Hidden CLI internals help surfaces work from built ao", - "kind": "command", - "objective": "Ensure hidden internal command help stays routable without executing host-specific side effects.", - "runtime": "shell", - "timeout_seconds": 120, - "inputs": { - "cwd": "../..", - "shell": "tmp=$(mktemp -d); trap 'rm -rf \"$tmp\"' EXIT; (cd cli && env -u AGENTOPS_RPI_RUNTIME go build -o \"$tmp/ao\" ./cmd/ao); \"$tmp/ao\" store --help >/dev/null; \"$tmp/ao\" store index --help >/dev/null; \"$tmp/ao\" store search --help >/dev/null; \"$tmp/ao\" store rebuild --help >/dev/null; \"$tmp/ao\" store stats --help >/dev/null; \"$tmp/ao\" temper --help >/dev/null; \"$tmp/ao\" temper validate --help >/dev/null; \"$tmp/ao\" temper lock --help >/dev/null; \"$tmp/ao\" temper status --help >/dev/null; \"$tmp/ao\" index --help >/dev/null; \"$tmp/ao\" migrate --help >/dev/null; \"$tmp/ao\" migrate memrl --help >/dev/null" - }, - "expectations": [ - {"type": "exit_code", "value": 0} - ], - "dimensions": ["runtime_compatibility", "process_adherence"], - "critical": true - }, - { - "id": "hidden-cli-internals-source-contracts", - "title": "Source contracts preserve hidden internal routing", - "kind": "artifact_check", - "objective": "Lock the implementation-level contracts that keep hidden knowledge pipeline commands wired while leaving them out of the public command surface.", - "expectations": [ - {"type": "artifact_contains", "target": "../../cli/cmd/ao/store.go", "value": "storeCmd.Hidden = true"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/store.go", "value": "Use: \"store\""}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/store.go", "value": "Use: \"index <files...>\""}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/store.go", "value": "Use: \"search <query>\""}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/store.go", "value": "Use: \"rebuild\""}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/store.go", "value": "Use: \"stats\""}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/store.go", "value": "IndexFileName = storage.SearchIndexFileName"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/temper.go", "value": "temperCmd.Hidden = true"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/temper.go", "value": "Use: \"temper\""}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/temper.go", "value": "Use: \"validate <files...>\""}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/temper.go", "value": "Use: \"lock <files...>\""}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/temper.go", "value": "Use: \"status\""}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/temper.go", "value": "chain.Append(entry)"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/index.go", "value": "indexCmd.Hidden = true"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/index.go", "value": "Generate INDEX.md manifest files for .agents/ knowledge directories."}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/index.go", "value": "indexCmd.Flags().Bool(\"check\", false"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/index.go", "value": "indexCmd.Flags().Bool(\"json\", false"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/feedback.go", "value": "migrateCmd.Hidden = true"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/feedback.go", "value": "Use: \"migrate memrl\""}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/feedback.go", "value": "migrateJSONLFiles(files []string, dryRun bool)"} - ], - "dimensions": ["correctness", "safety", "artifact_quality"], - "critical": true - }, - { - "id": "hidden-cli-internals-doc-map", - "title": "Hidden internal commands remain documented in the CLI-skill map", - "kind": "artifact_check", - "objective": "Keep hidden and internal commands represented in the operator map even though generated public CLI docs omit hidden commands.", - "expectations": [ - {"type": "artifact_contains", "target": "../../docs/cli-skills-map.md", "value": "Commands that exist in the Go CLI but are not called by any skill or hook."}, - {"type": "artifact_contains", "target": "../../docs/cli-skills-map.md", "value": "| `ao store` | Hidden | Vector store management |"}, - {"type": "artifact_contains", "target": "../../docs/cli-skills-map.md", "value": "| `ao index` | Hidden | Indexing utility |"}, - {"type": "artifact_contains", "target": "../../docs/cli-skills-map.md", "value": "| `ao migrate` | Hidden | Migration utility (`migrate memrl`) |"}, - {"type": "artifact_contains", "target": "../../docs/cli-skills-map.md", "value": "| `ao feedback` | Hidden | UI for providing feedback on learnings |"}, - {"type": "artifact_contains", "target": "../../docs/cli-skills-map.md", "value": "| `ao batch-feedback` | Hidden | Batch feedback processing |"}, - {"type": "artifact_contains", "target": "../../docs/cli-skills-map.md", "value": "| `ao session-outcome` | Hidden | Session outcome recording |"}, - {"type": "artifact_contains", "target": "../../docs/cli-skills-map.md", "value": "| `ao worktree` | Hidden | Worktree GC utility |"}, - {"type": "artifact_contains", "target": "../../docs/cli-skills-map.md", "value": "| `ao anti-patterns` | Hidden | Anti-pattern list |"} - ], - "dimensions": ["process_adherence", "artifact_quality"], - "critical": true - }, - { - "id": "hidden-cli-internals-json-validity", - "title": "Hidden internal JSON outputs stay machine-readable", - "kind": "command", - "objective": "Protect machine-readable surfaces for hidden internal commands that are used by scripts, tests, and agent workflows.", - "runtime": "shell", - "timeout_seconds": 120, - "inputs": { - "cwd": "../../cli", - "shell": "env -u AGENTOPS_RPI_RUNTIME go test -timeout=120s ./cmd/ao -run '^(TestJSONValidity_(Index|TemperStatus|StoreSearchIndex|StoreStats)|TestRunIndex_JSONOutput|TestRunTemperStatus_JSON)$'" - }, - "expectations": [ - {"type": "exit_code", "value": 0}, - {"type": "stdout_contains", "value": "ok \tgithub.com/boshu2/agentops/cli/cmd/ao"} - ], - "dimensions": ["correctness", "process_adherence", "runtime_compatibility"], - "critical": true - } - ] -} diff --git a/evals/agentops-core/planning-premortem-gates.json b/evals/agentops-core/planning-premortem-gates.json deleted file mode 100644 index 51cc31716..000000000 --- a/evals/agentops-core/planning-premortem-gates.json +++ /dev/null @@ -1,113 +0,0 @@ -{ - "schema_version": 1, - "id": "agentops-core.planning-premortem-gates", - "name": "AgentOps Planning and Premortem Direct-Cut Canary", - "description": "Offline canary for deterministic Goal Design, advisory idea challenge, exact-plan Premortem, and source/Codex parity.", - "practices": ["llm-eval-harness", "design-by-contract"], - "domain": "skill", - "visibility": "public_canary", - "tier": "deterministic", - "owners": ["agentops"], - "tags": ["canary", "offline", "plan", "premortem", "discovery"], - "allowed_runtimes": ["shell", "static"], - "environment": { - "offline_required": true, - "network": "forbidden", - "scrub_env_prefixes": ["AGENTOPS_RPI_RUNTIME", "ANTHROPIC_", "OPENAI_"], - "timeout_seconds": 240 - }, - "scoring": { - "aggregate_threshold": 1, - "dimensions": [ - {"name": "correctness", "weight": 2, "threshold": 1, "critical": true}, - {"name": "process_adherence", "weight": 2, "threshold": 1, "critical": true}, - {"name": "runtime_compatibility", "weight": 1, "threshold": 1, "critical": true}, - {"name": "artifact_quality", "weight": 1, "threshold": 1}, - {"name": "safety", "weight": 1, "threshold": 1} - ] - }, - "baseline_policy": { - "mode": "compare", - "baseline_path": ".agents/evals/baselines/agentops-core.planning-premortem-gates.baseline.json", - "blocking_gate": "none" - }, - "cases": [ - { - "id": "planning-skill-validators", - "title": "Planning skill validators pass", - "kind": "command", - "objective": "Keep Discovery, Plan, Premortem, Goal Design, and Dueling Idea Genies contracts healthy.", - "runtime": "shell", - "timeout_seconds": 120, - "inputs": { - "cwd": "../..", - "shell": "bash skills/discovery/scripts/validate.sh && bash skills/plan/scripts/validate.sh && bash skills/premortem/scripts/validate.sh && bash skills/goal-design/scripts/validate.sh" - }, - "expectations": [{"type": "exit_code", "value": 0}], - "dimensions": ["correctness", "process_adherence"], - "critical": true - }, - { - "id": "exact-plan-verdict", - "title": "Exact-plan verdict direct cut", - "kind": "command", - "objective": "Prove digest binding, author/judge separation, binary verdicts, and legacy planning-authority removal.", - "runtime": "shell", - "timeout_seconds": 120, - "inputs": { - "cwd": "../..", - "shell": "bats tests/scripts/premortem-plan-verdict-direct-cut.bats" - }, - "expectations": [{"type": "exit_code", "value": 0}], - "dimensions": ["correctness", "safety", "artifact_quality"], - "critical": true - }, - { - "id": "goal-design-deterministic-packet", - "title": "Goal Design remains deterministic", - "kind": "command", - "objective": "Keep packet schemas, digest identity, prompt handoff, and helper behavior deterministic without a semantic transition.", - "runtime": "shell", - "timeout_seconds": 120, - "inputs": { - "cwd": "../..", - "shell": "bats tests/scripts/check-goal-design-packet.bats tests/scripts/goal-design-packet.bats" - }, - "expectations": [{"type": "exit_code", "value": 0}], - "dimensions": ["correctness", "process_adherence"], - "critical": true - }, - { - "id": "planning-review-defaults", - "title": "Planning review defaults stay lean", - "kind": "command", - "objective": "Keep one fresh binary Premortem, advisory-only Dueling, and no Discovery phase controller.", - "runtime": "shell", - "timeout_seconds": 120, - "inputs": { - "cwd": "../..", - "shell": "bash tests/skills/test-tuning-defaults.sh" - }, - "expectations": [ - {"type": "exit_code", "value": 0}, - {"type": "stdout_contains", "value": "OVERALL: PASS"} - ], - "dimensions": ["correctness", "runtime_compatibility"], - "critical": true - }, - { - "id": "premortem-schema-contract", - "title": "Premortem schema is strict and binary", - "kind": "artifact_check", - "objective": "Lock the minimal plan-verdict boundary.", - "expectations": [ - {"type": "artifact_contains", "target": "../../skills/premortem/schemas/plan-verdict.schema.json", "value": "premortem-plan-verdict.v1"}, - {"type": "artifact_contains", "target": "../../skills/premortem/SKILL.md", "value": "author_id != judge_id"}, - {"type": "artifact_contains", "target": "../../skills/premortem/SKILL.md", "value": "complete nonempty blocker set"}, - {"type": "artifact_contains", "target": "../../skills/dueling-idea-genies/SKILL.md", "value": "advisory evidence for Plan"} - ], - "dimensions": ["artifact_quality", "safety"], - "critical": true - } - ] -} diff --git a/evals/agentops-core/pre-push-gate-governance.json b/evals/agentops-core/pre-push-gate-governance.json deleted file mode 100644 index 4116cf202..000000000 --- a/evals/agentops-core/pre-push-gate-governance.json +++ /dev/null @@ -1,105 +0,0 @@ -{ - "schema_version": 1, - "id": "agentops-core.pre-push-gate-governance", - "name": "AgentOps Pre-Push Gate Governance Canary", - "description": "Offline public canary for pre-push gate behavior and eval-gate wiring: BATS fixture coverage, eval-agentops fast gate integration, GIT env isolation, and warning/failure handling.", - "practices": ["llm-eval-harness", "cmm-process-maturity"], - "domain": "process", - "visibility": "public_canary", - "tier": "deterministic", - "owners": ["agentops"], - "tags": ["canary", "offline", "pre-push", "gate", "bats", "evals", "git-env"], - "allowed_runtimes": ["shell", "static"], - "environment": { - "offline_required": true, - "network": "forbidden", - "scrub_env_prefixes": ["AGENTOPS_RPI_RUNTIME", "ANTHROPIC_", "OPENAI_"], - "timeout_seconds": 180 - }, - "scoring": { - "aggregate_threshold": 1, - "dimensions": [ - {"name": "correctness", "weight": 2, "threshold": 1, "critical": true}, - {"name": "process_adherence", "weight": 2, "threshold": 1, "critical": true}, - {"name": "safety", "weight": 2, "threshold": 1, "critical": true}, - {"name": "artifact_quality", "weight": 1, "threshold": 1}, - {"name": "runtime_compatibility", "weight": 1, "threshold": 1} - ] - }, - "baseline_policy": {"mode": "compare", "baseline_path": ".agents/evals/baselines/agentops-core.pre-push-gate-governance.baseline.json", "blocking_gate": "none"}, - "cases": [ - { - "id": "pre-push-bats-suite", - "title": "pre-push BATS suite passes", - "kind": "command", - "objective": "Protect pre-push gate behavior for no-change passes, failure counting, worktree disposition, Codex gates, headless runtime checks, GIT env isolation, and non-blocking warnings.", - "runtime": "shell", - "timeout_seconds": 180, - "inputs": { - "cwd": "../..", - "shell": "bats tests/scripts/pre-push-gate.bats" - }, - "expectations": [ - {"type": "exit_code", "value": 0}, - {"type": "stdout_contains_auto_detect", "value": {"command": "bats tests/scripts/pre-push-gate.bats", "pattern": "^1\\.\\.([0-9]+)", "expected_group": 1, "tolerance_pct": 0}}, - {"type": "stdout_contains", "value": "ok 6 pre-push-gate.sh passes when no Go changes"}, - {"type": "stdout_contains", "value": "ok 21 pre-push-gate.sh clears GIT env for skill CLI snippets"}, - {"type": "stdout_contains", "value": "ok 22 pre-push-gate.sh clears GIT env for CLI docs parity"}, - {"type": "stdout_contains", "value": "ok 46 pre-push-gate.sh test-home-isolation lint stub exists in fake repo (paired stub guard)"} - ], - "dimensions": ["correctness", "runtime_compatibility", "safety"], - "critical": true - }, - { - "id": "pre-push-shell-syntax", - "title": "pre-push gate shell syntax is valid", - "kind": "command", - "objective": "Catch shell syntax breakage in the local gate before the broader BATS suite has to diagnose behavior.", - "runtime": "shell", - "timeout_seconds": 60, - "inputs": { - "cwd": "../..", - "shell": "bash -n scripts/pre-push-gate.sh" - }, - "expectations": [ - {"type": "exit_code", "value": 0} - ], - "dimensions": ["correctness", "runtime_compatibility"], - "critical": true - }, - { - "id": "eval-fast-gate-wiring", - "title": "pre-push gate preserves eval-agentops fast gate wiring", - "kind": "artifact_check", - "objective": "Ensure eval changes trigger the AgentOps fast eval gate and that eval-agentops warnings remain visible in pre-push output.", - "expectations": [ - {"type": "artifact_contains", "target": "../../scripts/pre-push-gate.sh", "value": "scripts/eval-agentops.sh"}, - {"type": "artifact_contains", "target": "../../scripts/pre-push-gate.sh", "value": "eval_args=(--fast)"}, - {"type": "artifact_contains", "target": "../../scripts/pre-push-gate.sh", "value": "eval_agentops_output=\"$(run_without_git_env_isolated_agents_home scripts/eval-agentops.sh \"${eval_args[@]}\" 2>&1)\""}, - {"type": "artifact_contains", "target": "../../scripts/pre-push-gate.sh", "value": "grep -q '^WARN eval-agentops:'"}, - {"type": "artifact_contains", "target": "../../scripts/pre-push-gate.sh", "value": "warn \"AgentOps eval canaries\""}, - {"type": "artifact_contains", "target": "../../scripts/pre-push-gate.sh", "value": "fail \"missing executable: scripts/eval-agentops.sh\""}, - {"type": "artifact_contains", "target": "../../scripts/pre-push-gate.sh", "value": "pass \"AgentOps eval canaries\""} - ], - "dimensions": ["process_adherence", "artifact_quality", "safety"], - "critical": true - }, - { - "id": "pre-push-bats-fixture-contracts", - "title": "pre-push BATS fixture stubs every unconditional gate dependency", - "kind": "artifact_check", - "objective": "Prevent fake-repo drift when pre-push adds a new unconditional gate by checking fixture stubs for eval-agentops, headless runtime, Codex lifecycle, CLI docs, and skill snippets.", - "expectations": [ - {"type": "artifact_contains", "target": "../../tests/scripts/pre-push-gate.bats", "value": "make_stub \"$FAKE_REPO/scripts/eval-agentops.sh\""}, - {"type": "artifact_contains", "target": "../../tests/scripts/pre-push-gate.bats", "value": "make_stub \"$FAKE_REPO/scripts/validate-headless-runtime-skills.sh\""}, - {"type": "artifact_contains", "target": "../../tests/scripts/pre-push-gate.bats", "value": "make_stub \"$FAKE_REPO/scripts/validate-codex-lifecycle-guards.sh\""}, - {"type": "artifact_contains", "target": "../../tests/scripts/pre-push-gate.bats", "value": "make_stub \"$FAKE_REPO/scripts/generate-cli-reference.sh\""}, - {"type": "artifact_contains", "target": "../../tests/scripts/pre-push-gate.bats", "value": "make_stub \"$FAKE_REPO/scripts/validate-skill-cli-snippets.sh\""}, - {"type": "artifact_contains", "target": "../../tests/scripts/pre-push-gate.bats", "value": "pre-push-gate.sh passes when no Go changes"}, - {"type": "artifact_contains", "target": "../../tests/scripts/pre-push-gate.bats", "value": "pre-push-gate.sh clears GIT env for CLI docs parity"} - ], - "dimensions": ["process_adherence", "artifact_quality", "safety"], - "critical": true - } - ] -} diff --git a/evals/agentops-core/push-worktree-closeout.json b/evals/agentops-core/push-worktree-closeout.json deleted file mode 100644 index 447ad8f2b..000000000 --- a/evals/agentops-core/push-worktree-closeout.json +++ /dev/null @@ -1,123 +0,0 @@ -{ - "schema_version": 1, - "id": "agentops-core.push-worktree-closeout", - "name": "AgentOps Push and Worktree Closeout Canary", - "description": "Offline public canary for session closeout safety: push skill guardrails, canonical worktree disposition behavior, preserved-ref governance, and landing-the-plane requirements.", - "practices": ["llm-eval-harness", "cmm-process-maturity"], - "domain": "process", - "visibility": "public_canary", - "tier": "deterministic", - "owners": ["agentops"], - "tags": ["canary", "offline", "push", "worktree", "closeout", "git", "session"], - "allowed_runtimes": ["shell", "static"], - "environment": { - "offline_required": true, - "network": "forbidden", - "scrub_env_prefixes": ["AGENTOPS_RPI_RUNTIME", "ANTHROPIC_", "OPENAI_"], - "timeout_seconds": 180 - }, - "scoring": { - "aggregate_threshold": 1, - "dimensions": [ - {"name": "correctness", "weight": 2, "threshold": 1, "critical": true}, - {"name": "process_adherence", "weight": 2, "threshold": 1, "critical": true}, - {"name": "safety", "weight": 2, "threshold": 1, "critical": true}, - {"name": "artifact_quality", "weight": 1, "threshold": 1}, - {"name": "runtime_compatibility", "weight": 1, "threshold": 1} - ] - }, - "baseline_policy": {"mode": "compare", "baseline_path": ".agents/evals/baselines/agentops-core.push-worktree-closeout.baseline.json", "blocking_gate": "none"}, - "cases": [ - { - "id": "push-skill-validators", - "title": "shared and Codex push validators pass", - "kind": "command", - "objective": "Keep the push skill artifacts loadable in both shared and Codex runtime copies.", - "runtime": "shell", - "timeout_seconds": 60, - "inputs": { - "cwd": "../..", - "shell": "bash skills/push/scripts/validate.sh && bash skills-codex/push/scripts/validate.sh" - }, - "expectations": [ - {"type": "exit_code", "value": 0}, - {"type": "stdout_contains", "value": "OK: push"} - ], - "dimensions": ["correctness", "runtime_compatibility"], - "critical": true - }, - { - "id": "worktree-disposition-behavior", - "title": "worktree disposition behavioral tests pass", - "kind": "command", - "objective": "Exercise canonical-root cleanliness, detached-root failure, dirty-root diagnostics, preserved-ref manifest checks, and unexpected foreign worktree failures.", - "runtime": "shell", - "timeout_seconds": 120, - "inputs": { - "cwd": "../..", - "shell": "bash tests/scripts/test-worktree-disposition.sh" - }, - "expectations": [ - {"type": "exit_code", "value": 0}, - {"type": "stdout_contains", "value": "PASS: check-worktree-disposition.sh"} - ], - "dimensions": ["correctness", "process_adherence", "safety"], - "critical": true - }, - { - "id": "push-skill-guardrail-contracts", - "title": "push skill preserves test, stage, sync, and no-force-push guardrails", - "kind": "artifact_check", - "objective": "Lock the push workflow guardrails that prevent stranded or unsafe changes: run tests, stage specific files, rebase before push, avoid main, avoid secrets, and do not force-push after rebase failures.", - "expectations": [ - {"type": "artifact_contains", "target": "../../skills/push/SKILL.md", "value": "Run ALL applicable test suites. Do NOT skip any."}, - {"type": "artifact_contains", "target": "../../skills/push/SKILL.md", "value": "Stage only the files relevant to the current work. Do NOT use `git add -A` unless the user explicitly requests it."}, - {"type": "artifact_contains", "target": "../../skills/push/SKILL.md", "value": "git pull --rebase origin $(git branch --show-current)"}, - {"type": "artifact_contains", "target": "../../skills/push/SKILL.md", "value": "git push origin $(git branch --show-current)"}, - {"type": "artifact_contains", "target": "../../skills/push/SKILL.md", "value": "NEVER push to `main` or `master` without explicit user confirmation"}, - {"type": "artifact_contains", "target": "../../skills/push/SKILL.md", "value": "NEVER stage files matching: `.env*`, `*credentials*`, `*secret*`, `*.key`, `*.pem`"}, - {"type": "artifact_contains", "target": "../../skills/push/SKILL.md", "value": "If `git pull --rebase` fails, do NOT force push"}, - {"type": "artifact_contains", "target": "../../skills-codex/push/SKILL.md", "value": "Do NOT run `ao codex stop` after the remote push."}, - {"type": "artifact_contains", "target": "../../skills-codex/push/SKILL.md", "value": "Files changed count"} - ], - "dimensions": ["process_adherence", "safety", "artifact_quality"], - "critical": true - }, - { - "id": "worktree-disposition-contracts", - "title": "worktree disposition script preserves canonical-root and preserved-ref checks", - "kind": "artifact_check", - "objective": "Ensure the disposition gate keeps its fail-closed checks for hook git env recovery, canonical root cleanliness, generated path diagnostics, preserved refs, and foreign branch-attached worktrees.", - "expectations": [ - {"type": "artifact_contains", "target": "../../scripts/check-worktree-disposition.sh", "value": "GIT_WORK_TREE=\"$(pwd -P)\""}, - {"type": "artifact_contains", "target": "../../scripts/check-worktree-disposition.sh", "value": "FAIL: canonical root $target_root has uncommitted changes"}, - {"type": "artifact_contains", "target": "../../scripts/check-worktree-disposition.sh", "value": "Generated/gate-managed paths detected:"}, - {"type": "artifact_contains", "target": "../../scripts/check-worktree-disposition.sh", "value": "docs/preserved-refs.tsv"}, - {"type": "artifact_contains", "target": "../../scripts/check-worktree-disposition.sh", "value": "FAIL: current worktree is detached"}, - {"type": "artifact_contains", "target": "../../scripts/check-worktree-disposition.sh", "value": "FAIL: canonical root $canonical_root is detached; it must stay on main"}, - {"type": "artifact_contains", "target": "../../scripts/check-worktree-disposition.sh", "value": "FAIL: canonical root $canonical_root is on $canonical_branch; expected main"}, - {"type": "artifact_contains", "target": "../../scripts/check-worktree-disposition.sh", "value": "FAIL: unexpected branch-attached worktrees detected:"}, - {"type": "artifact_contains", "target": "../../scripts/check-worktree-disposition.sh", "value": "PASS: canonical root $canonical_root is clean on $canonical_branch; current branch $current_branch is attached at $repo_root"} - ], - "dimensions": ["correctness", "process_adherence", "safety"], - "critical": true - }, - { - "id": "landing-plane-instructions", - "title": "agent closeout instructions require push and disposition proof", - "kind": "artifact_check", - "objective": "Keep AGENTS closeout policy explicit that a session is not complete until changes are committed, pushed, remotely confirmed, and foreign worktrees have a recorded disposition.", - "expectations": [ - {"type": "artifact_contains", "target": "../../docs/agent-workflow-reference.md", "value": "## Session closeout (landing the plane)"}, - {"type": "artifact_contains", "target": "../../docs/agent-workflow-reference.md", "value": "Work is not complete until `git push` succeeds."}, - {"type": "artifact_contains", "target": "../../docs/agent-workflow-reference.md", "value": "git status # must show up to date with origin"}, - {"type": "artifact_contains", "target": "../../docs/agent-workflow-reference.md", "value": "Do not stop before push or defer push to the operator."}, - {"type": "artifact_contains", "target": "../../docs/agent-workflow-reference.md", "value": "Never leave a foreign branch-attached worktree without a"}, - {"type": "artifact_contains", "target": "../../docs/agent-workflow-reference.md", "value": "Keep the canonical root clean and attached to `main`."}, - {"type": "artifact_contains", "target": "../../docs/agent-workflow-reference.md", "value": "Run `bash scripts/check-worktree-disposition.sh` before push and session close."} - ], - "dimensions": ["process_adherence", "artifact_quality", "safety"], - "critical": true - } - ] -} diff --git a/evals/agentops-core/ratchet-lifecycle-ledger.json b/evals/agentops-core/ratchet-lifecycle-ledger.json deleted file mode 100644 index e925a3ee5..000000000 --- a/evals/agentops-core/ratchet-lifecycle-ledger.json +++ /dev/null @@ -1,141 +0,0 @@ -{ - "schema_version": 1, - "id": "agentops-core.ratchet-lifecycle-ledger", - "name": "AgentOps Ratchet Lifecycle Ledger Canary", - "description": "Offline public canary for the Brownian Ratchet lifecycle ledger: status, check, next, spec, validate, record, promote, skip, find, trace, migrate, and artifact migration.", - "practices": ["llm-eval-harness", "agile-manifesto"], - "domain": "rpi", - "visibility": "public_canary", - "tier": "deterministic", - "owners": ["agentops"], - "tags": ["canary", "offline", "ratchet", "rpi", "ledger", "progression", "provenance"], - "allowed_runtimes": ["shell", "static"], - "environment": { - "offline_required": true, - "network": "forbidden", - "scrub_env_prefixes": ["AGENTOPS_RPI_RUNTIME", "ANTHROPIC_", "OPENAI_"], - "timeout_seconds": 180 - }, - "scoring": { - "aggregate_threshold": 1, - "dimensions": [ - {"name": "correctness", "weight": 2, "threshold": 1, "critical": true}, - {"name": "process_adherence", "weight": 2, "threshold": 1, "critical": true}, - {"name": "runtime_compatibility", "weight": 1, "threshold": 1, "critical": true}, - {"name": "artifact_quality", "weight": 1, "threshold": 1}, - {"name": "learning_closure", "weight": 1, "threshold": 1} - ] - }, - "baseline_policy": {"mode": "compare", "baseline_path": ".agents/evals/baselines/agentops-core.ratchet-lifecycle-ledger.baseline.json", "blocking_gate": "none"}, - "cases": [ - { - "id": "ratchet-lifecycle-focused-tests", - "title": "Ratchet lifecycle tests pass", - "kind": "command", - "objective": "Protect command behavior for ratchet status, check, next, spec, validate, record, promote, skip, find, trace, migration, JSON output, golden output, and RPI integration.", - "runtime": "shell", - "timeout_seconds": 180, - "inputs": { - "cwd": "../../cli", - "shell": "env -u AGENTOPS_RPI_RUNTIME go test -timeout=180s ./cmd/ao -run '^(TestRunRatchet|TestRatchet|TestJSONValidity_Ratchet|TestCobraRatchetParentHelp|TestGoldenRatchet|TestIntegration_Ratchet|TestWorkflow_RPIRatchetProgression|TestNegativePath_RatchetRecordUnknownStep)$'" - }, - "expectations": [ - {"type": "exit_code", "value": 0}, - {"type": "stdout_contains", "value": "ok \tgithub.com/boshu2/agentops/cli/cmd/ao"} - ], - "dimensions": ["correctness", "runtime_compatibility", "learning_closure"], - "critical": true - }, - { - "id": "ratchet-lifecycle-help-smoke", - "title": "Ratchet lifecycle help surfaces work from built ao", - "kind": "command", - "objective": "Ensure every ratchet command family remains routable from the shipped CLI help surface without mutating repository state.", - "runtime": "shell", - "timeout_seconds": 120, - "inputs": { - "cwd": "../..", - "shell": "tmp=$(mktemp -d); trap 'rm -rf \"$tmp\"' EXIT; (cd cli && env -u AGENTOPS_RPI_RUNTIME go build -o \"$tmp/ao\" ./cmd/ao); \"$tmp/ao\" ratchet --help >/dev/null; \"$tmp/ao\" ratchet status --help >/dev/null; \"$tmp/ao\" ratchet check --help >/dev/null; \"$tmp/ao\" ratchet next --help >/dev/null; \"$tmp/ao\" ratchet spec --help >/dev/null; \"$tmp/ao\" ratchet validate --help >/dev/null; \"$tmp/ao\" ratchet record --help >/dev/null; \"$tmp/ao\" ratchet promote --help >/dev/null; \"$tmp/ao\" ratchet skip --help >/dev/null; \"$tmp/ao\" ratchet find --help >/dev/null; \"$tmp/ao\" ratchet trace --help >/dev/null; \"$tmp/ao\" ratchet migrate --help >/dev/null; \"$tmp/ao\" ratchet migrate-artifacts --help >/dev/null" - }, - "expectations": [ - {"type": "exit_code", "value": 0} - ], - "dimensions": ["runtime_compatibility", "process_adherence"], - "critical": true - }, - { - "id": "ratchet-lifecycle-generated-docs", - "title": "Generated CLI docs expose the ratchet lifecycle", - "kind": "artifact_check", - "objective": "Keep generated command reference coverage aligned with every ratchet command operators and hooks rely on for progress permanence.", - "expectations": [ - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "### `ao ratchet`"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "Track progress through the phased RPI workflow."}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "#### `ao ratchet check`"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "#### `ao ratchet next`"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "#### `ao ratchet spec`"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "#### `ao ratchet status`"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "#### `ao ratchet validate`"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "--lenient Allow legacy artifacts without schema_version (expires in 90 days)"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "#### `ao ratchet promote`"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "#### `ao ratchet record`"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "--parent-epic string Parent epic ID from prior RPI cycle"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "#### `ao ratchet skip`"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "#### `ao ratchet find`"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "#### `ao ratchet trace`"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "#### `ao ratchet migrate`"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "#### `ao ratchet migrate-artifacts`"} - ], - "dimensions": ["artifact_quality", "process_adherence", "runtime_compatibility"], - "critical": true - }, - { - "id": "ratchet-lifecycle-source-contracts", - "title": "Source contracts preserve ratchet lifecycle routing", - "kind": "artifact_check", - "objective": "Lock command registration, JSON structures, chain persistence, and provenance routing that make the ratchet an append-only progress ledger.", - "expectations": [ - {"type": "artifact_contains", "target": "../../cli/cmd/ao/ratchet.go", "value": "The ratchet chain is stored in .agents/ao/chain.jsonl"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/ratchet.go", "value": "ratchetCmd.AddGroup("}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/ratchet_status.go", "value": "chain.GetAllStatus()"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/ratchet_status.go", "value": "outputRatchetStatus"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/ratchet_next.go", "value": "type NextResult struct"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/ratchet_next.go", "value": "stepSkillMap"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/ratchet_check.go", "value": "Check if prerequisites are satisfied for a workflow step."}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/ratchet_spec.go", "value": "Find and output the current spec artifact path."}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/ratchet_validate.go", "value": "validateSubCmd.Flags().StringSliceVar(&ratchetFiles"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/ratchet_validate.go", "value": "Default mode is STRICT"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/ratchet_record.go", "value": "recordSubCmd.Flags().StringVar(&ratchetOutput"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/ratchet_record.go", "value": "chain.Append(entry)"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/ratchet_promote.go", "value": "validatePromotion(cwd, artifact, targetTier, w)"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/ratchet_skip.go", "value": "Skipped: true"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/ratchet_find.go", "value": "locator.Find(pattern)"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/ratchet_trace.go", "value": "buildTrace(artifact, chain)"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/ratchet_migrate.go", "value": "ratchet.MigrateChain(cwd)"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/ratchet_migrate.go", "value": "Add schema_version: 1 to existing .agents/ artifacts."} - ], - "dimensions": ["correctness", "process_adherence", "artifact_quality"], - "critical": true - }, - { - "id": "ratchet-lifecycle-skill-and-context-docs", - "title": "Ratchet skill and context docs preserve append-only guidance", - "kind": "artifact_check", - "objective": "Ensure operator guidance continues to route ratchet work through CLI commands and treats the chain as append-only evidence.", - "expectations": [ - {"type": "artifact_contains", "target": "../../skills/ratchet/SKILL.md", "value": "Progress stored in `.agents/ao/chain.jsonl`:"}, - {"type": "artifact_contains", "target": "../../skills/ratchet/SKILL.md", "value": "ao ratchet status 2>/dev/null"}, - {"type": "artifact_contains", "target": "../../skills/ratchet/SKILL.md", "value": "ao ratchet check <step> 2>/dev/null"}, - {"type": "artifact_contains", "target": "../../skills/ratchet/SKILL.md", "value": "ao ratchet record <step> --output \"<artifact-path>\" 2>/dev/null"}, - {"type": "artifact_contains", "target": "../../skills/ratchet/SKILL.md", "value": "ao ratchet skip <step> --reason \"<why>\" 2>/dev/null"}, - {"type": "artifact_contains", "target": "../../docs/context-packet.md", "value": "Gate passes are recorded via `ao ratchet record`."}, - {"type": "artifact_contains", "target": "../../docs/context-packet.md", "value": "The ratchet chain in `.agents/ao/chain.jsonl` is append-only."}, - {"type": "artifact_contains", "target": "../../docs/context-packet.md", "value": "Never manually edit chain.jsonl"}, - {"type": "artifact_contains", "target": "../../docs/cli-skills-map.md", "value": "| `ao ratchet` | crank, handoff, implement, plan, pre-mortem, ratchet, rpi, status, vibe |"}, - {"type": "artifact_contains", "target": "../../docs/cli-skills-map.md", "value": "| **ratchet** | `ratchet check`, `ratchet record`, `ratchet skip`, `ratchet status` |"} - ], - "dimensions": ["process_adherence", "artifact_quality", "learning_closure"], - "critical": true - } - ] -} diff --git a/evals/agentops-core/rpi-behavior.json b/evals/agentops-core/rpi-behavior.json index 57c985c57..1fe0af195 100644 --- a/evals/agentops-core/rpi-behavior.json +++ b/evals/agentops-core/rpi-behavior.json @@ -1,94 +1,72 @@ { "schema_version": 1, "id": "agentops-core.rpi-behavior", - "name": "AgentOps RPI Behavior Canary", - "description": "Offline public canary for RPI phased runner contracts and deterministic runtime boundaries.", + "name": "AgentOps Single-Pass RPI Canary", + "description": "Offline public canary for the Plan, Implement, fresh Validate, durable verdict, report-and-stop boundary.", "practices": ["llm-eval-harness", "agile-manifesto"], "domain": "rpi", "visibility": "public_canary", "tier": "deterministic", "owners": ["agentops"], - "tags": ["canary", "offline", "rpi", "runtime"], + "tags": ["canary", "offline", "rpi", "single-pass", "verdict"], "allowed_runtimes": ["static", "shell"], "environment": { "offline_required": true, "network": "forbidden", "scrub_env_prefixes": ["AGENTOPS_RPI_RUNTIME", "ANTHROPIC_", "OPENAI_"], - "timeout_seconds": 20 + "timeout_seconds": 30 }, "scoring": { "aggregate_threshold": 1, "dimensions": [ {"name": "correctness", "weight": 2, "threshold": 1, "critical": true}, - {"name": "process_adherence", "weight": 1, "threshold": 1}, - {"name": "runtime_compatibility", "weight": 1, "threshold": 1}, - {"name": "safety", "weight": 1, "threshold": 1} + {"name": "process_adherence", "weight": 1, "threshold": 1, "critical": true}, + {"name": "artifact_quality", "weight": 1, "threshold": 1}, + {"name": "safety", "weight": 1, "threshold": 1, "critical": true} ] }, "baseline_policy": {"mode": "compare", "baseline_path": ".agents/evals/baselines/agentops-core.rpi-behavior.baseline.json", "blocking_gate": "none"}, "cases": [ { - "id": "rpi-runtime-files", - "title": "RPI runtime source files are present", + "id": "single-pass-reference-tests", + "title": "RPI dispatches each core phase at most once", "kind": "command", - "objective": "Verify the RPI behavior canary resolves repo-root source paths from the suite directory.", + "objective": "Prove PASS and FAIL both stop after one Plan, one Implement, and one Validate dispatch.", "runtime": "shell", "inputs": { "cwd": "../..", - "shell": "test -f cli/cmd/ao/rpi_phased.go && test -f cli/cmd/ao/rpi_serve.go && test -f skills/rpi/SKILL.md" + "shell": "python3 skills/rpi/tests/test_run_once.py && python3 scripts/check-cathedral-cut-conformance.py" }, - "expectations": [ - {"type": "exit_code", "value": 0} - ], - "dimensions": ["correctness", "runtime_compatibility"], + "expectations": [{"type": "exit_code", "value": 0}], + "dimensions": ["correctness", "process_adherence", "safety"], "critical": true }, { - "id": "phased-runner-runtime-contract", - "title": "phased runner exposes bounded runtime modes", + "id": "rpi-skill-stop-contract", + "title": "RPI reports and stops without continuation authority", "kind": "artifact_check", - "objective": "Ensure RPI phased execution keeps explicit runtime-mode and runtime-command contracts.", + "objective": "Keep the public wrapper anchored to one ordered invocation and caller-owned continuation.", "expectations": [ - {"type": "artifact_contains", "target": "../../cli/cmd/ao/rpi_phased.go", "value": "Run RPI with fresh runtime session per phase"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/rpi_phased.go", "value": "Phase runtime mode: auto|direct|stream|tmux"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/rpi_phased.go", "value": "Runtime command used for phase prompts"} + {"type": "artifact_contains", "target": "../../skills/rpi/SKILL.md", "value": "Plan -> Implement -> fresh Validate -> report"}, + {"type": "artifact_contains", "target": "../../skills/rpi/SKILL.md", "value": "Stop regardless"}, + {"type": "artifact_contains", "target": "../../skills/rpi/SKILL.md", "value": "Do not append a next action"} ], - "dimensions": ["correctness", "runtime_compatibility"], + "dimensions": ["process_adherence", "artifact_quality"], "critical": true }, { - "id": "serve-runtime-contract", - "title": "RPI serve preserves runtime orchestration flags", - "kind": "artifact_check", - "objective": "Cover the serve entrypoint used for RPI orchestration without invoking a live runtime.", - "expectations": [ - {"type": "artifact_contains", "target": "../../cli/cmd/ao/rpi_serve.go", "value": "Phase runtime mode for orchestration"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/rpi_serve.go", "value": "Runtime command for orchestration phase prompts"} - ], - "dimensions": ["process_adherence", "runtime_compatibility"] - }, - { - "id": "eval-deterministic-boundary", - "title": "eval runner keeps live adapters advisory", - "kind": "artifact_check", - "objective": "Protect deterministic public canaries from live Claude or Codex execution.", - "expectations": [ - {"type": "artifact_contains", "target": "../../cli/cmd/ao/eval.go", "value": "Live Claude and Codex adapters are evaluated by a later runtime tier."}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/eval.go", "value": "unknown runtime %q (use static, mock, shell, claude, or codex)"} - ], - "dimensions": ["runtime_compatibility", "safety"], + "id": "core-schema-contract", + "title": "RPI status and semantic verdict remain separate", + "kind": "command", + "objective": "Protect verdict.v2 from orchestration statuses and preserve the standalone RPI report.", + "runtime": "shell", + "inputs": { + "cwd": "../..", + "shell": "grep -q NOT_PROVEN schemas/verdict.v2.schema.json && ! grep -q NOT_BUILT schemas/verdict.v2.schema.json && grep -q NOT_BUILT schemas/rpi-report.v1.schema.json" + }, + "expectations": [{"type": "exit_code", "value": 0}], + "dimensions": ["correctness", "artifact_quality"], "critical": true - }, - { - "id": "rpi-skill-contract", - "title": "RPI skill contract remains available", - "kind": "artifact_check", - "objective": "Ensure RPI behavior canaries are anchored to the user-facing skill contract.", - "expectations": [ - {"type": "artifact_contains", "target": "../../skills/rpi/SKILL.md", "value": "RPI"}, - {"type": "artifact_contains", "target": "../../skills/rpi/SKILL.md", "value": "validation"} - ], - "dimensions": ["process_adherence", "artifact_quality"] } ] } diff --git a/evals/agentops-core/rpi-control-plane-orchestration.json b/evals/agentops-core/rpi-control-plane-orchestration.json deleted file mode 100644 index c57ded66e..000000000 --- a/evals/agentops-core/rpi-control-plane-orchestration.json +++ /dev/null @@ -1,135 +0,0 @@ -{ - "schema_version": 1, - "id": "agentops-core.rpi-control-plane-orchestration", - "name": "AgentOps RPI Control Plane Orchestration Canary", - "description": "Offline public canary for RPI run status, dashboard/event serving, cleanup, cancel, verify, stream fallback, parallel worktrees, and worktree GC surfaces needed to operate long-running RPI/evolve sessions.", - "practices": ["llm-eval-harness", "agile-manifesto"], - "domain": "rpi", - "visibility": "public_canary", - "tier": "deterministic", - "owners": ["agentops"], - "tags": ["canary", "offline", "rpi", "control-plane", "status", "serve", "cleanup", "cancel", "stream", "parallel", "worktree"], - "allowed_runtimes": ["shell", "static"], - "environment": { - "offline_required": true, - "network": "forbidden", - "scrub_env_prefixes": ["AGENTOPS_RPI_RUNTIME", "ANTHROPIC_", "OPENAI_"], - "timeout_seconds": 180 - }, - "scoring": { - "aggregate_threshold": 1, - "dimensions": [ - {"name": "correctness", "weight": 2, "threshold": 1, "critical": true}, - {"name": "runtime_compatibility", "weight": 2, "threshold": 1, "critical": true}, - {"name": "process_adherence", "weight": 1, "threshold": 1, "critical": true}, - {"name": "artifact_quality", "weight": 1, "threshold": 1}, - {"name": "safety", "weight": 1, "threshold": 1}, - {"name": "efficiency", "weight": 1, "threshold": 1} - ] - }, - "baseline_policy": {"mode": "compare", "baseline_path": ".agents/evals/baselines/agentops-core.rpi-control-plane-orchestration.baseline.json", "blocking_gate": "none"}, - "cases": [ - { - "id": "rpi-control-plane-go-tests", - "title": "RPI control-plane, cleanup, stream, parallel, and worktree tests pass", - "kind": "command", - "objective": "Protect operator surfaces that make long RPI/evolve runs inspectable, cancellable, recoverable, and safely isolated across worktrees.", - "runtime": "shell", - "timeout_seconds": 180, - "inputs": { - "cwd": "../../cli", - "shell": "env -u AGENTOPS_RPI_RUNTIME go test -timeout=180s ./cmd/ao -run '^(TestRPI_Integration_StatusNoRuns|TestRPI_Integration_StatusWithRunRegistry|TestRPI_Integration_StatusJSON|TestRPI_Integration_PhasedDryRun|TestRPIStatusDiscovery|TestBuildRPIStatusOutput_WithRegistryRuns|TestServeRPIRuns_ReturnsJSONArray|TestServeRPIEvents_StreamsPreExistingEvents|TestServeRPIArtifact_RejectsTraversal|TestRPIVerifyPassJSON|TestRPIVerifyFailText|TestCleanupStaleRun|TestCleanupActiveRunUntouched|TestCleanupDryRun|TestCleanupByRunID|TestExecuteRPICleanup_StaleAfterOnlyMarksOldRuns|TestRemoveOrphanedWorktree_RepoRootProtection|TestDiscoverSupervisorLeaseTargets|TestMarkRunInterruptedByCancel|TestRPICancel_runRPICancel_noFlagsError|TestRPICancel_cancelOneTarget_dryRun|TestStreamCoverage_SelectExecutor|TestStreamCoverage_ClassifyStreamResult|TestStreamExecutorExecute_FallsBackToDirectOnStartupTimeout|TestResolveParallelEpics_Args|TestResolveParallelEpics_Manifest|TestCreateParallelWorktrees_CreatesAndCleansUp|TestMergeParallelWorktrees_SkipsFailedEpics|TestSpawnParallelEpics_ArgumentConstruction|TestWorktree_resolveRepoRoot_ignoresPollutedGitEnvInLinkedWorktree|TestRunWorktreeGC_DryRunInGitRepo|TestWorktree_gcWorktreeCandidates_DryRunDirty)$'" - }, - "expectations": [ - {"type": "exit_code", "value": 0}, - {"type": "stdout_contains", "value": "ok \tgithub.com/boshu2/agentops/cli/cmd/ao"} - ], - "dimensions": ["correctness", "runtime_compatibility", "safety", "efficiency"], - "critical": true - }, - { - "id": "rpi-control-plane-help-smoke", - "title": "RPI control-plane CLI surfaces expose help", - "kind": "command", - "objective": "Ensure the operator can discover the commands used to inspect, stream, cancel, clean, verify, parallelize, and garbage-collect RPI runs.", - "runtime": "shell", - "timeout_seconds": 120, - "inputs": { - "cwd": "../..", - "shell": "tmp=$(mktemp -d); trap 'rm -rf \"$tmp\"' EXIT; (cd cli && env -u AGENTOPS_RPI_RUNTIME go build -o \"$tmp/ao\" ./cmd/ao); \"$tmp/ao\" rpi --help >/dev/null; \"$tmp/ao\" rpi status --help >/dev/null; \"$tmp/ao\" rpi serve --help >/dev/null; \"$tmp/ao\" rpi phased --help >/dev/null; \"$tmp/ao\" rpi cleanup --help >/dev/null; \"$tmp/ao\" rpi cancel --help >/dev/null; \"$tmp/ao\" rpi nudge --help >/dev/null; \"$tmp/ao\" rpi stream --help >/dev/null; \"$tmp/ao\" rpi verify --help >/dev/null; \"$tmp/ao\" rpi parallel --help >/dev/null; \"$tmp/ao\" worktree --help >/dev/null; \"$tmp/ao\" worktree gc --help >/dev/null" - }, - "expectations": [ - {"type": "exit_code", "value": 0} - ], - "dimensions": ["runtime_compatibility", "process_adherence"], - "critical": true - }, - { - "id": "rpi-control-plane-cli-docs", - "title": "generated CLI docs expose RPI control-plane commands", - "kind": "artifact_check", - "objective": "Keep generated CLI docs aligned with RPI control-plane commands and flags used during long autonomous sessions.", - "expectations": [ - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "### `ao rpi`"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "#### `ao rpi cancel`"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "#### `ao rpi cleanup`"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "#### `ao rpi nudge`"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "#### `ao rpi parallel`"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "#### `ao rpi phased`"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "#### `ao rpi serve`"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "#### `ao rpi status`"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "#### `ao rpi stream`"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "#### `ao rpi verify`"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "--auto-clean-stale"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "--stream-startup-timeout duration"} - ], - "dimensions": ["artifact_quality", "runtime_compatibility", "process_adherence"] - }, - { - "id": "rpi-control-plane-source-contracts", - "title": "source contracts preserve RPI control-plane behavior", - "kind": "artifact_check", - "objective": "Lock source-level guarantees for dashboard streaming, artifact serving, cleanup safety, cancellation, stream formats, verification, parallel isolation, and hidden worktree GC.", - "expectations": [ - {"type": "artifact_contains", "target": "../../cli/cmd/ao/rpi_serve.go", "value": "Start a production RPI orchestration run or stream its live dashboard."}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/rpi_serve.go", "value": "The dashboard streams events via Server-Sent Events (SSE)"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/rpi_serve.go", "value": "mux.HandleFunc(\"/artifact\""}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/rpi_cleanup.go", "value": "A run is considered stale if it has no active heartbeat"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/rpi_cleanup.go", "value": "removeOrphanedWorktree(repoRoot, sr.WorktreePath, sr.RunID)"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/rpi_cancel.go", "value": "Cancel active RPI orchestration runs via a CLI kill switch."}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/rpi_cancel.go", "value": "Expired/corrupted lease metadata is treated as stale and ignored."}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/rpi_stream.go", "value": "Read normalized per-run C2 events from events.jsonl."}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/rpi_verify.go", "value": "Verify integrity of the RPI ledger."}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/rpi_parallel.go", "value": "Run multiple RPI epics concurrently, each in an isolated git worktree."}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/rpi_parallel.go", "value": "AGENTOPS_RPI_NO_WORKTREE=1"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/worktree.go", "value": "Safely remove stale AgentOps RPI worktrees and orphaned tmux sessions."}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/worktree.go", "value": "Skips worktrees with uncommitted changes"} - ], - "dimensions": ["correctness", "safety", "artifact_quality"], - "critical": true - }, - { - "id": "rpi-control-plane-doc-and-skill-contracts", - "title": "RPI docs and skills preserve control-plane guidance", - "kind": "artifact_check", - "objective": "Ensure RPI operator docs and skill contracts describe the control-plane surfaces, registry layout, worktree lifecycle, cleanup/cancel paths, and canonical long-running supervisor path.", - "expectations": [ - {"type": "artifact_contains", "target": "../../docs/contracts/rpi-run-registry.md", "value": "runs/<run-id>/events.jsonl"}, - {"type": "artifact_contains", "target": "../../docs/contracts/rpi-run-registry.md", "value": "ao rpi phased --runtime"}, - {"type": "artifact_contains", "target": "../../docs/contracts/rpi-run-registry.md", "value": "Failure path: worktree is preserved for debugging"}, - {"type": "artifact_contains", "target": "../../docs/contracts/rpi-run-registry.md", "value": "ao rpi cleanup --all --dry-run"}, - {"type": "artifact_contains", "target": "../../docs/contracts/rpi-run-registry.md", "value": "ao rpi cancel --all"}, - {"type": "artifact_contains", "target": "../../docs/contracts/rpi-run-registry.md", "value": "ao worktree gc"}, - {"type": "artifact_contains", "target": "../../docs/contracts/rpi-run-registry.md", "value": "Refuses to remove repo root as a worktree target"}, - {"type": "artifact_contains", "target": "../../docs/how-it-works.md", "value": "use the canonical path `ao rpi loop --supervisor`"}, - {"type": "artifact_contains", "target": "../../docs/how-it-works.md", "value": "`ao rpi parallel` runs multiple epics concurrently, each in its own git worktree."}, - {"type": "artifact_contains", "target": "../../docs/software-factory.md", "value": "`ao factory start`, `/rpi`, `ao rpi phased`, `ao rpi status`, `ao daemon submit`"}, - {"type": "artifact_contains", "target": "../../skills/rpi/SKILL.md", "value": "Preserve the discovered"}, - {"type": "artifact_contains", "target": "../../skills/rpi/SKILL.md", "value": "`--loop`, restart from discovery on FAIL while `cycle < max_cycles`."}, - {"type": "artifact_contains", "target": "../../skills-codex/rpi/SKILL.md", "value": "ao codex ensure-start 2>/dev/null || true"}, - {"type": "artifact_contains", "target": "../../AGENTS.md", "value": "Every foreign worktree must end the session as `merged`, `preserved`, `exported`, or `deleted`."} - ], - "dimensions": ["process_adherence", "artifact_quality", "runtime_compatibility"] - } - ] -} diff --git a/evals/agentops-core/rpi-scorecard.json b/evals/agentops-core/rpi-scorecard.json deleted file mode 100644 index c251706d1..000000000 --- a/evals/agentops-core/rpi-scorecard.json +++ /dev/null @@ -1,97 +0,0 @@ -{ - "schema_version": 1, - "id": "agentops-core.rpi-scorecard", - "name": "AgentOps RPI Scorecard Canary", - "description": "Offline public canary for the required RPI scorecard categories.", - "practices": ["llm-eval-harness", "agile-manifesto"], - "domain": "rpi", - "visibility": "public_canary", - "tier": "deterministic", - "owners": ["agentops"], - "tags": ["canary", "offline", "rpi", "scorecard"], - "allowed_runtimes": ["static"], - "environment": { - "offline_required": true, - "network": "forbidden", - "scrub_env_prefixes": ["AGENTOPS_RPI_RUNTIME", "ANTHROPIC_", "OPENAI_"], - "timeout_seconds": 20 - }, - "scoring": { - "aggregate_threshold": 1, - "dimensions": [ - {"name": "artifact_quality", "weight": 1, "threshold": 1, "critical": true}, - {"name": "process_adherence", "weight": 1, "threshold": 1, "critical": true}, - {"name": "correctness", "weight": 1, "threshold": 1}, - {"name": "safety", "weight": 1, "threshold": 1, "critical": true} - ] - }, - "baseline_policy": {"mode": "compare", "baseline_path": ".agents/evals/baselines/agentops-core.rpi-scorecard.baseline.json", "blocking_gate": "none"}, - "cases": [ - { - "id": "scorecard.artifact-completeness.surface", - "title": "RPI scorecard covers artifact completeness", - "kind": "artifact_check", - "objective": "Ensure scorecards verify required RPI artifacts exist and validate.", - "expectations": [ - {"type": "artifact_contains", "target": "../../cli/internal/eval/scorecard.go", "value": "artifact-completeness"} - ], - "dimensions": ["artifact_quality"], - "critical": true - }, - { - "id": "scorecard.phase-order.surface", - "title": "RPI scorecard covers phase order", - "kind": "artifact_check", - "objective": "Ensure scorecards verify discovery, crank, and validation ordering.", - "expectations": [ - {"type": "artifact_contains", "target": "../../cli/internal/eval/scorecard.go", "value": "phase-order"} - ], - "dimensions": ["process_adherence"], - "critical": true - }, - { - "id": "scorecard.objective-spine.surface", - "title": "RPI scorecard covers objective spine", - "kind": "artifact_check", - "objective": "Ensure scorecards verify one objective is preserved across RPI phases.", - "expectations": [ - {"type": "artifact_contains", "target": "../../cli/internal/eval/scorecard.go", "value": "objective-spine"} - ], - "dimensions": ["process_adherence"], - "critical": true - }, - { - "id": "scorecard.validation-separation.surface", - "title": "RPI scorecard covers validation separation", - "kind": "artifact_check", - "objective": "Ensure scorecards verify implementation and validation evidence stay distinct.", - "expectations": [ - {"type": "artifact_contains", "target": "../../cli/internal/eval/scorecard.go", "value": "validation-separation"} - ], - "dimensions": ["process_adherence"], - "critical": true - }, - { - "id": "scorecard.scenario-satisfaction.surface", - "title": "RPI scorecard covers scenario satisfaction", - "kind": "artifact_check", - "objective": "Ensure scorecards verify scenario satisfaction evidence.", - "expectations": [ - {"type": "artifact_contains", "target": "../../cli/internal/eval/scorecard.go", "value": "scenario-satisfaction"} - ], - "dimensions": ["correctness"], - "critical": true - }, - { - "id": "scorecard.runtime-safety.surface", - "title": "RPI scorecard covers runtime safety", - "kind": "artifact_check", - "objective": "Ensure scorecards verify runtime safety and isolation evidence.", - "expectations": [ - {"type": "artifact_contains", "target": "../../cli/internal/eval/scorecard.go", "value": "runtime-safety"} - ], - "dimensions": ["safety"], - "critical": true - } - ] -} diff --git a/evals/agentops-core/runtime-contracts.json b/evals/agentops-core/runtime-contracts.json index 56d1e06de..61c2888ea 100644 --- a/evals/agentops-core/runtime-contracts.json +++ b/evals/agentops-core/runtime-contracts.json @@ -73,35 +73,6 @@ ], "dimensions": ["correctness", "runtime_compatibility", "safety"], "critical": true - }, - { - "id": "codex-lifecycle-cli-docs", - "title": "codex lifecycle and runtime command docs remain visible", - "kind": "artifact_check", - "objective": "Keep generated CLI docs aligned with Codex hookless lifecycle commands and runtime command selection.", - "expectations": [ - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "#### `ao codex ensure-start`"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "#### `ao codex ensure-stop`"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "ao codex ensure-start [flags]"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "ao codex ensure-stop [flags]"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "Codex uses 'exec'"} - ], - "dimensions": ["artifact_quality", "process_adherence", "runtime_compatibility"] - }, - { - "id": "codex-skill-lifecycle-guards", - "title": "codex runtime skills preserve lifecycle guards", - "kind": "artifact_check", - "objective": "Ensure Codex runtime artifacts keep startup and closeout guard commands in the shipped skill bodies.", - "expectations": [ - {"type": "artifact_contains", "target": "../../skills-codex/rpi/SKILL.md", "value": "ao codex ensure-start 2>/dev/null || true"}, - {"type": "artifact_contains", "target": "../../skills-codex/crank/SKILL.md", "value": "ao codex ensure-start 2>/dev/null || true"}, - {"type": "artifact_contains", "target": "../../skills-codex/status/SKILL.md", "value": "ao codex ensure-start 2>/dev/null || true"}, - {"type": "artifact_contains", "target": "../../skills-codex/recover/SKILL.md", "value": "ao codex ensure-start 2>/dev/null || true"}, - {"type": "artifact_contains", "target": "../../skills-codex/handoff/SKILL.md", "value": "ao codex ensure-stop --auto-extract 2>/dev/null || true"} - ], - "dimensions": ["process_adherence", "runtime_compatibility", "safety"], - "critical": true } ] } diff --git a/evals/agentops-core/skill-quality-gates.json b/evals/agentops-core/skill-quality-gates.json index 597ca867c..fee06d80e 100644 --- a/evals/agentops-core/skill-quality-gates.json +++ b/evals/agentops-core/skill-quality-gates.json @@ -118,15 +118,14 @@ "critical": true }, { - "id": "skill-quality-gate-wiring", - "title": "pre-push skill quality gates remain wired", + "id": "skill-quality-check-registry", + "title": "ordinary deterministic skill checks remain registered", "kind": "artifact_check", - "objective": "Keep the same skill quality checks connected to the repo-managed pre-push gate.", + "objective": "Keep skill quality checks available through ao gate check without making them Git admission policy.", "expectations": [ - {"type": "artifact_contains", "target": "../../scripts/pre-push-gate.sh", "value": "skill lint suite"}, - {"type": "artifact_contains", "target": "../../scripts/pre-push-gate.sh", "value": "skill schema validation"}, - {"type": "artifact_contains", "target": "../../scripts/pre-push-gate.sh", "value": "skill runtime formats"}, - {"type": "artifact_contains", "target": "../../scripts/pre-push-gate.sh", "value": "skill CLI snippets"}, + {"type": "artifact_contains", "target": "../../cli/internal/gates/checks/seed.go", "value": "ID: \"skill.schema\""}, + {"type": "artifact_contains", "target": "../../cli/internal/gates/checks/seed.go", "value": "ID: \"skill.runtime-formats\""}, + {"type": "artifact_contains", "target": "../../cli/internal/gates/checks/seed.go", "value": "ID: \"skill.cli-snippets\""}, {"type": "artifact_contains", "target": "../../scripts/check-skill-size.sh", "value": "WARN_LINES"} ], "dimensions": ["process_adherence", "artifact_quality"] diff --git a/evals/agentops-core/standards-governance.json b/evals/agentops-core/standards-governance.json index 7c4561509..7ae1b3b30 100644 --- a/evals/agentops-core/standards-governance.json +++ b/evals/agentops-core/standards-governance.json @@ -78,7 +78,7 @@ "kind": "artifact_check", "objective": "Catch drift where the Codex standards artifact regresses to slash-command wording or drops downstream skill integration links.", "expectations": [ - {"type": "artifact_contains", "target": "../../skills-codex/standards/SKILL.md", "value": "Loaded automatically by `$vibe` and `$pre-mortem`"}, + {"type": "artifact_contains", "target": "../../skills-codex/standards/SKILL.md", "value": "Loaded automatically by `$vibe` and `$premortem`"}, {"type": "artifact_contains", "target": "../../skills-codex/standards/SKILL.md", "value": "- `$vibe` - Loads based on changed file types"}, {"type": "artifact_contains", "target": "../../skills-codex/standards/SKILL.md", "value": "- `$implement` - Loads for files being modified"}, {"type": "artifact_contains", "target": "../../skills-codex/standards/SKILL.md", "value": "- `$review` - Loads for change-quality and blast-radius checks"}, diff --git a/evals/agentops-core/supporting-cli-utilities.json b/evals/agentops-core/supporting-cli-utilities.json deleted file mode 100644 index 3508dde2a..000000000 --- a/evals/agentops-core/supporting-cli-utilities.json +++ /dev/null @@ -1,149 +0,0 @@ -{ - "schema_version": 1, - "id": "agentops-core.supporting-cli-utilities", - "name": "AgentOps Supporting CLI Utilities Canary", - "description": "Offline public canary for lower-frequency but user-facing CLI utilities: demo, seed, badge, completion, config, vibe-check, trace, extract, and mind.", - "practices": ["llm-eval-harness", "pragmatic-programmer"], - "domain": "cli", - "visibility": "public_canary", - "tier": "deterministic", - "owners": ["agentops"], - "tags": ["canary", "offline", "cli", "utilities", "demo", "seed", "config", "trace", "extract", "mind"], - "allowed_runtimes": ["shell", "static"], - "environment": { - "offline_required": true, - "network": "forbidden", - "scrub_env_prefixes": ["AGENTOPS_RPI_RUNTIME", "ANTHROPIC_", "OPENAI_"], - "timeout_seconds": 180 - }, - "scoring": { - "aggregate_threshold": 1, - "dimensions": [ - {"name": "correctness", "weight": 2, "threshold": 1, "critical": true}, - {"name": "process_adherence", "weight": 1, "threshold": 1, "critical": true}, - {"name": "runtime_compatibility", "weight": 1, "threshold": 1, "critical": true}, - {"name": "artifact_quality", "weight": 1, "threshold": 1}, - {"name": "safety", "weight": 1, "threshold": 1} - ] - }, - "baseline_policy": {"mode": "compare", "baseline_path": ".agents/evals/baselines/agentops-core.supporting-cli-utilities.baseline.json", "blocking_gate": "none"}, - "cases": [ - { - "id": "supporting-cli-utility-go-tests", - "title": "Supporting CLI utility tests pass", - "kind": "command", - "objective": "Protect lower-frequency command behavior that helps users bootstrap, inspect, demo, configure, trace, extract, and complete AgentOps workflows.", - "runtime": "shell", - "timeout_seconds": 180, - "inputs": { - "cwd": "../../cli", - "shell": "env -u AGENTOPS_RPI_RUNTIME go test -timeout=180s ./cmd/ao -run '^(TestDemo_|TestCompletion_|TestBadge_|TestSeed_|TestCobra(BadgeCommand|ConfigCommand|DemoConceptsCommand|DemoQuickCommand|CompletionCommand|SeedCommand|TraceHelpAndDryRun|ExtractCommand|VersionCommand|StatusCommand|RootHelpOutput)|TestVibeCheck(Command|Flags|DryRun|RepoPath|_outputVibeCheckJSON_emptyResult|_outputVibeCheckJSON_withData|_outputVibeCheckMarkdown_emptyResult|_outputVibeCheckMarkdown_withData)|TestRunVibeCheck_(invalidDuration|validRepo|jsonOutput|markdownOutput)|TestTrace_traceOneArtifact_(Found|NotFound|JSONOutput|GraphMode|jsonOutput|noProvenance))$'" - }, - "expectations": [ - {"type": "exit_code", "value": 0}, - {"type": "stdout_contains", "value": "ok \tgithub.com/boshu2/agentops/cli/cmd/ao"} - ], - "dimensions": ["correctness", "runtime_compatibility"], - "critical": true - }, - { - "id": "supporting-cli-utility-help-smoke", - "title": "Supporting CLI utility help surfaces work from built ao", - "kind": "command", - "objective": "Ensure user-facing utility commands and their key subcommands remain discoverable without executing host-specific side effects.", - "runtime": "shell", - "timeout_seconds": 120, - "inputs": { - "cwd": "../..", - "shell": "tmp=$(mktemp -d); trap 'rm -rf \"$tmp\"' EXIT; (cd cli && env -u AGENTOPS_RPI_RUNTIME go build -o \"$tmp/ao\" ./cmd/ao); \"$tmp/ao\" demo --help >/dev/null; \"$tmp/ao\" seed --help >/dev/null; \"$tmp/ao\" badge --help >/dev/null; \"$tmp/ao\" completion --help >/dev/null; \"$tmp/ao\" completion bash >/dev/null; \"$tmp/ao\" config --help >/dev/null; \"$tmp/ao\" config models --help >/dev/null; \"$tmp/ao\" vibe-check --help >/dev/null; \"$tmp/ao\" trace --help >/dev/null; \"$tmp/ao\" extract --help >/dev/null; \"$tmp/ao\" mind --help >/dev/null; \"$tmp/ao\" mind scan --help >/dev/null; \"$tmp/ao\" mind normalize --help >/dev/null; \"$tmp/ao\" mind link --help >/dev/null; \"$tmp/ao\" mind index --help >/dev/null; \"$tmp/ao\" mind all --help >/dev/null; \"$tmp/ao\" mind graph --help >/dev/null" - }, - "expectations": [ - {"type": "exit_code", "value": 0} - ], - "dimensions": ["runtime_compatibility", "process_adherence"], - "critical": true - }, - { - "id": "supporting-cli-utility-docs", - "title": "Generated CLI docs expose supporting utilities", - "kind": "artifact_check", - "objective": "Keep generated CLI docs aligned with the lower-frequency command surfaces operators and new users still rely on.", - "expectations": [ - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "### `ao demo`"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "Run a demonstration of AgentOps as the engineering operating system"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "### `ao seed`"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "Plant the AgentOps seed in any repository."}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "--template string Goal template: go-cli, python-lib, web-app, rust-cli, generic"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "### `ao badge`"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "Display a visual badge showing knowledge flywheel health status."}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "### `ao completion`"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "Generate shell completion scripts for ao."}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "### `ao config`"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "#### `ao config models`"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "--set-tier string Set the default model cost tier (quality, balanced, budget)"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "### `ao vibe-check`"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "Run a comprehensive vibe-check analysis on your repository."}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "### `ao mind`"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "Scan, normalize, link, and index .agents/ markdown into an Obsidian knowledge graph."}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "#### `ao mind normalize`"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "### `ao trace`"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "Trace the provenance of an artifact back to its source transcript."}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "### `ao extract`"}, - {"type": "artifact_contains", "target": "../../cli/docs/COMMANDS.md", "value": "Check for pending session extractions and output a prompt for Claude to process."} - ], - "dimensions": ["artifact_quality", "process_adherence", "runtime_compatibility"] - }, - { - "id": "supporting-cli-utility-source-contracts", - "title": "Source contracts preserve supporting utility behavior", - "kind": "artifact_check", - "objective": "Lock implementation-level guarantees for utility command routing, dry-run behavior, template detection, shell completion generation, config model tiers, provenance tracing, extraction queues, and mind Python delegation.", - "expectations": [ - {"type": "artifact_contains", "target": "../../cli/cmd/ao/demo.go", "value": "Run a demonstration of AgentOps as the engineering operating system"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/demo.go", "value": "demoStepDelay = 500 * time.Millisecond"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/seed.go", "value": "Auto-detection reads go.mod, package.json, pyproject.toml, and Cargo.toml"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/seed.go", "value": "validateTemplateMapEntries(validTemplates, embedded.TemplatesFS)"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/seed.go", "value": "type seedResult struct"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/badge.go", "value": "Display knowledge flywheel health badge"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/badge.go", "value": "escapeVelocityThreshold"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/completion.go", "value": "ValidArgs: []string{\"bash\", \"zsh\", \"fish\", \"powershell\"}"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/completion.go", "value": "GenBashCompletionV2"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/config.go", "value": "AGENTOPS_MODEL_TIER"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/config.go", "value": "modelsSetSkill"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/config.go", "value": "Set a skill-specific tier override"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/vibe_check.go", "value": "Run a comprehensive vibe-check analysis"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/vibe_check.go", "value": "parseDuration(vibeCheckSince)"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/vibe_check.go", "value": "vibecheck.Analyze"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/trace.go", "value": "Trace the provenance of an artifact back to its source transcript."}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/trace.go", "value": "provenance.NewGraph"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/trace.go", "value": "if GetDryRun()"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/extract.go", "value": "pending sessions (queued by 'ao forge --queue')"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/extract.go", "value": "loadPendingOrNil"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/extract.go", "value": "extractMostRecent"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/mind.go", "value": "python3 -m mind"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/mind.go", "value": "mindArgs := []string{\"-m\", \"mind\", subcommand}"}, - {"type": "artifact_contains", "target": "../../cli/cmd/ao/mind.go", "value": "if !GetDryRun()"} - ], - "dimensions": ["correctness", "safety", "artifact_quality"], - "critical": true - }, - { - "id": "supporting-cli-utility-map-and-docs", - "title": "Utility commands remain mapped for operators", - "kind": "artifact_check", - "objective": "Ensure utility commands remain represented in the CLI-skill map and README proof path, not only in generated command help.", - "expectations": [ - {"type": "artifact_contains", "target": "../../docs/cli-skills-map.md", "value": "| `ao badge` | flywheel, status |"}, - {"type": "artifact_contains", "target": "../../docs/cli-skills-map.md", "value": "| `ao seed` | quickstart |"}, - {"type": "artifact_contains", "target": "../../docs/cli-skills-map.md", "value": "| `ao extract` |"}, - {"type": "artifact_contains", "target": "../../docs/cli-skills-map.md", "value": "| `ao completion` | User utility | Shell completion generation |"}, - {"type": "artifact_contains", "target": "../../docs/cli-skills-map.md", "value": "| `ao config` | User utility | Config management |"}, - {"type": "artifact_contains", "target": "../../docs/cli-skills-map.md", "value": "| `ao demo` | User utility | Council-first AgentOps 3.0 demonstration |"}, - {"type": "artifact_contains", "target": "../../docs/cli-skills-map.md", "value": "| `ao vibe-check` | User utility |"}, - {"type": "artifact_contains", "target": "../../docs/cli-skills-map.md", "value": "| `ao trace` | User utility | Artifact tracing |"}, - {"type": "artifact_contains", "target": "../../README.md", "value": "ao demo"} - ], - "dimensions": ["process_adherence", "artifact_quality"] - } - ] -} diff --git a/evals/membrane/harvest-2026-06-22/derived-check-rfd-nested-schema.md b/evals/membrane/harvest-2026-06-22/derived-check-rfd-nested-schema.md index 3bce6b26c..4d34e9930 100644 --- a/evals/membrane/harvest-2026-06-22/derived-check-rfd-nested-schema.md +++ b/evals/membrane/harvest-2026-06-22/derived-check-rfd-nested-schema.md @@ -1,5 +1,5 @@ --- -compiler_targets: pre-mortem +compiler_targets: premortem detectability: advisory escape_bead_id: age-harvest-rfd-nested-schema escape_confirmed_sha: a3b9bcb diff --git a/evals/skill-probes/crank/probe.json b/evals/skill-probes/crank/probe.json index 14ab2ed76..68eeb7330 100644 --- a/evals/skill-probes/crank/probe.json +++ b/evals/skill-probes/crank/probe.json @@ -7,5 +7,5 @@ "discriminator": "discriminator.sh", "budget_note": "N=2 — DIRECTIONAL, not statistical", "honesty": "measures behavior-change (does loading crank make the agent respect write-scope collisions when planning parallelism), NOT quality-uplift", - "spine_member": "workflow spine (discovery, crank, validate, implement, swarm, post-mortem) — the START SET per age-e508.1" + "spine_member": "workflow spine (discovery, crank, validate, implement, swarm, postmortem) — the START SET per age-e508.1" } diff --git a/examples/schedules/evolve-ondemand.yaml b/examples/schedules/evolve-ondemand.yaml deleted file mode 100644 index 7e45cedcc..000000000 --- a/examples/schedules/evolve-ondemand.yaml +++ /dev/null @@ -1,37 +0,0 @@ -# evolve-ondemand.yaml — manual-fire autonomous improvement loop -# -# What it does: -# Registers an `rpi.run` template that you fire by hand on demand — -# typically when `ao goals measure` reports a regression or you want to -# kick off an autonomous fitness-scored improvement cycle. The schedule -# carries a once-yearly cron purely to satisfy the parser; it is intended -# to be triggered manually, never to fire on its own. -# -# Why schedule it (instead of running ad hoc): -# Registering it as a named schedule lets you fire it through the daemon -# surface that already enforces backpressure, idempotency, and projection -# targets — same plumbing as the cron-driven recipes. -# -# Cadence: manual fire only. The `@yearly` cron is a placeholder; the -# expected invocation is: -# -# ao schedule run evolve-ondemand -# -# Wire up: -# ao schedule add --file examples/schedules/evolve-ondemand.yaml -# -# Inspect runs: -# ao watch -# ao schedule list - -schedules: - - name: evolve-ondemand - cron: "@yearly" - job_type: rpi.run - payload: - schema_version: 1 - goal: "Autonomous fitness-scored improvement cycle (manual fire)" - timeout: "2h" - backpressure: - skip_if_running: true - max_queue_depth: 1 diff --git a/examples/schedules/nightly-evolve.yaml b/examples/schedules/nightly-evolve.yaml deleted file mode 100644 index abc621228..000000000 --- a/examples/schedules/nightly-evolve.yaml +++ /dev/null @@ -1,42 +0,0 @@ -# nightly-evolve.yaml - daemon-owned autonomous improvement loop -# -# What it does: -# Fires an `rpi.run` job through agentopsd once per night. The daemon -# materializes the run ID, applies supervisor gates, and records the job -# through the ledger/projection surface. -# -# Why schedule it: -# This is the daemon-native replacement for a host timer that launches -# `scripts/nightly-evolution.sh --execute --run-evolve`. The script path -# already submits `rpi.run`; this recipe lets operators register the same -# cadence directly with `ao schedule`. -# -# Cadence: nightly at 04:30 local. `landing_policy: off` keeps this safe as a -# non-mutating autonomous loop by default; operators can create a reviewed copy -# with a stronger landing policy when their daemon lane is ready. -# -# Wire up: -# ao daemon run --workers 2 --executor-policy cli-fallback -# ao schedule add --file examples/schedules/nightly-evolve.yaml - -schedules: - - name: nightly-evolve - cron: "30 4 * * *" - job_type: rpi.run - payload: - schema_version: 1 - goal: "Nightly daemon-owned RPI/evolve cycle" - start_phase: 1 - max_phase: 3 - test_first: true - backend: gascity-api - max_cycles: 1 - gate_policy: required - landing_policy: off - bd_sync_policy: auto - failure_policy: stop - kill_switch_path: ".agents/rpi/KILL" - timeout: "3h" - backpressure: - skip_if_running: true - max_queue_depth: 1 diff --git a/goals-affects-files.yaml b/goals-affects-files.yaml index 23b80707a..e401e072d 100644 --- a/goals-affects-files.yaml +++ b/goals-affects-files.yaml @@ -35,7 +35,6 @@ affects_files: - scripts/check-compile-health.sh compile-no-oscillation: - - scripts/check-compile-oscillation.sh flywheel-compounding: - scripts/check-flywheel-compounding.sh @@ -75,10 +74,6 @@ affects_files: - goals-affects-files.yaml - cli/internal/goals/** - competitive-freshness: - - docs/comparisons/** - - scripts/check-competitive-freshness.sh - codex-parity-drift: - skills-codex/** - skills-codex-overrides/** diff --git a/images/claude/manifest.json b/images/claude/manifest.json index 68361919b..a5fc4ee1e 100644 --- a/images/claude/manifest.json +++ b/images/claude/manifest.json @@ -1,209 +1,258 @@ { "image": "claude", - "spec": "IMAGE-CORE.md", - "spec_section": "post-consolidation corpus refresh (2026-07-04, age-085q); original spec: IMAGE-CORE.md \u00a71 + \u00a72a + \u00a72d + \u00a73a", - "source_corpus_commit": "81303ad9c48373c0f78ec785fb4c05c9422f9399", - "recipe": "direct", - "transform": "none", - "description": "Claude image = the CORE skill set shipped DIRECT. SKILL.md is Claude-native (YAML frontmatter name/description + markdown body); no conversion, no wrapper. Skills are consumed in place from agentops/skills/<slug>/ via the Claude harness skills path / marketplace plugin. First-class Claude primitives (Workflows, Agent subagents, scheduled tasks) wrap USAGE, not the skill files. This bundle is the manifest + README + verify only; it does NOT copy or duplicate the skill files.", - "counts": { - "method_core": 20, - "tool_op_core": 9, - "core_total": 29, - "operator": 2, - "manifest_total": 31 - }, - "core_skills": [ + "schema_version": "skill-image.v1", + "skill_count": 50, + "skills": [ { - "slug": "rpi", - "path": "skills/rpi/", - "category": "method-core", - "ship": "direct" - }, - { - "slug": "discovery", - "path": "skills/discovery/", - "category": "method-core", - "ship": "direct" - }, - { - "slug": "research", - "path": "skills/research/", - "category": "method-core", - "ship": "direct" - }, - { - "slug": "plan", - "path": "skills/plan/", - "category": "method-core", - "ship": "direct" - }, - { - "slug": "implement", - "path": "skills/implement/", - "category": "method-core", - "ship": "direct" - }, - { - "slug": "crank", - "path": "skills/crank/", - "category": "method-core", - "ship": "direct" - }, - { - "slug": "swarm", - "path": "skills/swarm/", - "category": "method-core", - "ship": "direct" - }, - { - "slug": "validate", - "path": "skills/validate/", - "category": "method-core", - "ship": "direct" - }, - { - "slug": "council", - "path": "skills/council/", - "category": "method-core", - "ship": "direct" - }, - { - "slug": "premortem", - "path": "skills/premortem/", - "category": "method-core", - "ship": "direct" - }, - { - "slug": "postmortem", - "path": "skills/postmortem/", - "category": "method-core", - "ship": "direct" - }, - { - "slug": "goals", - "path": "skills/goals/", - "category": "method-core", - "ship": "direct" - }, - { - "slug": "evolve", - "path": "skills/evolve/", - "category": "method-core", - "ship": "direct" - }, - { - "slug": "bootstrap", - "path": "skills/bootstrap/", - "category": "method-core", - "ship": "direct" - }, - { - "slug": "handoff", - "path": "skills/handoff/", - "category": "method-core", - "ship": "direct" - }, - { - "slug": "push", - "path": "skills/push/", - "category": "method-core", - "ship": "direct" - }, - { - "slug": "scope", - "path": "skills/scope/", - "category": "method-core", - "ship": "direct" - }, - { - "slug": "status", - "path": "skills/status/", - "category": "method-core", - "ship": "direct" - }, - { - "slug": "test", - "path": "skills/test/", - "category": "method-core", - "ship": "direct" - }, - { - "slug": "skill-builder", - "path": "skills/skill-builder/", - "category": "method-core", - "ship": "direct" - }, - { - "slug": "beads-br", - "path": "skills/beads-br/", - "category": "tool-op-core", - "ship": "direct" - }, - { - "slug": "beads-bv", - "path": "skills/beads-bv/", - "category": "tool-op-core", - "ship": "direct" - }, - { - "slug": "agent-mail", - "path": "skills/agent-mail/", - "category": "tool-op-core", - "ship": "direct" - }, - { - "slug": "ntm", - "path": "skills/ntm/", - "category": "tool-op-core", - "ship": "direct" - }, - { - "slug": "cass", - "path": "skills/cass/", - "category": "tool-op-core", - "ship": "direct" - }, - { - "slug": "dcg", - "path": "skills/dcg/", - "category": "tool-op-core", - "ship": "direct" - }, - { - "slug": "account-rotation", + "disposition": "keep_specialist", "path": "skills/account-rotation/", - "category": "tool-op-core", - "ship": "direct" + "slug": "account-rotation" }, { - "slug": "rch", - "path": "skills/rch/", - "category": "tool-op-core", - "ship": "direct" + "disposition": "keep_optional_adapter", + "path": "skills/agent-mail/", + "slug": "agent-mail" }, { - "slug": "sbh", - "path": "skills/sbh/", - "category": "tool-op-core", - "ship": "direct" - } - ], - "operator_skills": [ - { - "slug": "workflow-builder", - "path": "skills/workflow-builder/", - "category": "claude-operator", - "ship": "direct", - "primitive": "Claude Workflows (worker -> validate -> tie-break/council)" + "disposition": "keep_optional_adapter", + "path": "skills/agent-native/", + "slug": "agent-native" }, { - "slug": "cc-hooks", + "disposition": "keep_optional_adapter", + "path": "skills/agy-native/", + "slug": "agy-native" + }, + { + "disposition": "keep_optional_adapter", + "path": "skills/automation-shape-routing/", + "slug": "automation-shape-routing" + }, + { + "disposition": "keep_specialist", + "path": "skills/bootstrap/", + "slug": "bootstrap" + }, + { + "disposition": "keep_specialist", + "path": "skills/cass/", + "slug": "cass" + }, + { + "disposition": "keep_specialist", "path": "skills/cc-hooks/", - "category": "claude-operator", - "ship": "direct", - "primitive": "Claude Agent subagents (worker/validator/scout)" + "slug": "cc-hooks" + }, + { + "disposition": "keep_specialist", + "path": "skills/codebase-recon/", + "slug": "codebase-recon" + }, + { + "disposition": "keep_optional_adapter", + "path": "skills/codex-exec/", + "slug": "codex-exec" + }, + { + "disposition": "keep_specialist", + "path": "skills/converter/", + "slug": "converter" + }, + { + "disposition": "keep_strategy", + "path": "skills/council/", + "slug": "council" + }, + { + "disposition": "keep_specialist", + "path": "skills/dcg/", + "slug": "dcg" + }, + { + "disposition": "keep_specialist", + "path": "skills/doc/", + "slug": "doc" + }, + { + "disposition": "keep_specialist", + "path": "skills/domain/", + "slug": "domain" + }, + { + "disposition": "keep_strategy", + "path": "skills/dueling-idea-genies/", + "slug": "dueling-idea-genies" + }, + { + "disposition": "keep_specialist", + "path": "skills/goals/", + "slug": "goals" + }, + { + "disposition": "keep_specialist", + "path": "skills/handoff/", + "slug": "handoff" + }, + { + "disposition": "keep_specialist", + "path": "skills/heal-skill/", + "slug": "heal-skill" + }, + { + "disposition": "keep_strategy", + "path": "skills/idea-genie/", + "slug": "idea-genie" + }, + { + "disposition": "keep", + "path": "skills/implement/", + "slug": "implement" + }, + { + "disposition": "keep_off_path", + "path": "skills/learn/", + "slug": "learn" + }, + { + "disposition": "keep_specialist", + "path": "skills/ms/", + "slug": "ms" + }, + { + "disposition": "keep_optional_adapter", + "path": "skills/ntm/", + "slug": "ntm" + }, + { + "disposition": "keep_specialist", + "path": "skills/operationalize/", + "slug": "operationalize" + }, + { + "disposition": "keep_specialist", + "path": "skills/pattern-mining/", + "slug": "pattern-mining" + }, + { + "disposition": "keep", + "path": "skills/plan/", + "slug": "plan" + }, + { + "disposition": "keep_strategy", + "path": "skills/postmortem/", + "slug": "postmortem" + }, + { + "disposition": "keep_strategy", + "path": "skills/premortem/", + "slug": "premortem" + }, + { + "disposition": "keep_specialist", + "path": "skills/product/", + "slug": "product" + }, + { + "disposition": "keep_specialist", + "path": "skills/rch/", + "slug": "rch" + }, + { + "disposition": "keep_strategy", + "path": "skills/reality-check/", + "slug": "reality-check" + }, + { + "disposition": "keep_specialist", + "path": "skills/refactor/", + "slug": "refactor" + }, + { + "disposition": "keep_specialist", + "path": "skills/research/", + "slug": "research" + }, + { + "disposition": "keep_specialist", + "path": "skills/reverse-engineer/", + "slug": "reverse-engineer" + }, + { + "disposition": "keep", + "path": "skills/rpi/", + "slug": "rpi" + }, + { + "disposition": "keep_specialist", + "path": "skills/sbh/", + "slug": "sbh" + }, + { + "disposition": "keep_specialist", + "path": "skills/scaffold/", + "slug": "scaffold" + }, + { + "disposition": "keep_specialist", + "path": "skills/scope/", + "slug": "scope" + }, + { + "disposition": "keep_specialist", + "path": "skills/security/", + "slug": "security" + }, + { + "disposition": "keep_specialist", + "path": "skills/shared/", + "slug": "shared" + }, + { + "disposition": "keep_specialist", + "path": "skills/skill-builder/", + "slug": "skill-builder" + }, + { + "disposition": "keep_specialist", + "path": "skills/standards/", + "slug": "standards" + }, + { + "disposition": "keep_specialist", + "path": "skills/status/", + "slug": "status" + }, + { + "disposition": "keep_optional_adapter", + "path": "skills/swarm/", + "slug": "swarm" + }, + { + "disposition": "keep_specialist", + "path": "skills/test/", + "slug": "test" + }, + { + "disposition": "keep_specialist", + "path": "skills/toil-mining/", + "slug": "toil-mining" + }, + { + "disposition": "keep_optional_adapter", + "path": "skills/using-gc/", + "slug": "using-gc" + }, + { + "disposition": "keep", + "path": "skills/validate/", + "slug": "validate" + }, + { + "disposition": "keep_specialist", + "path": "skills/workflow-builder/", + "slug": "workflow-builder" } ], - "note_ag_s43tg": "Prune ag-s43tg (2026-06-12): folded slugs remapped to their fold targets; counts recomputed." + "source": "skills/*/SKILL.md metadata" } diff --git a/images/claude/verify.sh b/images/claude/verify.sh index ce0394cde..03fe2ccc7 100755 --- a/images/claude/verify.sh +++ b/images/claude/verify.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # verify.sh — confirm every skill in the Claude image manifest exists in the corpus. # -# Reads the slug list from images/claude/manifest.json (core_skills + operator_skills) +# Reads the metadata-derived slug list from images/claude/manifest.json # and asserts each skills/<slug>/SKILL.md is present at the agentops repo root. # Exit 0 iff all present; exit 1 on any missing skill (or a malformed manifest). # @@ -18,15 +18,14 @@ if [ ! -f "$manifest" ]; then exit 1 fi -# Extract every "slug" value from both core_skills and operator_skills. +# Extract every generated slug. # Prefer python3 (robust JSON); fall back to grep/sed if python3 is absent. if command -v python3 >/dev/null 2>&1; then slugs="$(python3 -c ' import json, sys d = json.load(open(sys.argv[1])) -for k in ("core_skills", "operator_skills"): - for e in d.get(k, []): - print(e["slug"]) +for e in d.get("skills", []): + print(e["slug"]) ' "$manifest")" else slugs="$(grep -oE '"slug"[[:space:]]*:[[:space:]]*"[^"]+"' "$manifest" \ @@ -81,5 +80,7 @@ if [ "$plugin_version" != "$EXPECTED_VERSION" ]; then fi echo "OK: Claude plugin manifest version $plugin_version matches expected $EXPECTED_VERSION" -echo "OK: all $count Claude-image skills present (CORE + operator, per manifest.json)" +declared_count="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["skill_count"])' "$manifest")" +[ "$count" -eq "$declared_count" ] || { echo "FAIL: parsed $count skills, manifest declares $declared_count" >&2; exit 1; } +echo "OK: all $count metadata-derived Claude-image skills present" exit 0 diff --git a/images/codex/manifest.json b/images/codex/manifest.json index 5967604fa..8e090f8b2 100644 --- a/images/codex/manifest.json +++ b/images/codex/manifest.json @@ -1,432 +1,308 @@ { - "$schema_note": "Codex image bundle manifest (cp-eoxc / cp-gqu Unit-4). Packaging only - references existing skills-codex/ twins; does not regenerate them.", "image": "codex", - "vendor": "codex", - "epic": "cp-gqu", - "unit": "unit-4", - "bead": "cp-eoxc", - "spec": "IMAGE-CORE.md s1, s2c, s2d, s3b, s4 Unit-4", - "agentops_base_commit": "8172d7e7ab6b43a6fb2624c4be8ac3816d6a24d5", - "distilled_state_commit": "7af9eb342", - "generated_utc": "2026-06-07T21:04:48Z", - "recipe": { - "summary": "Codex is the ONLY vendor needing a FORMAT CONVERSION. Each CORE slug is consumed via its skills-codex/<slug>/ twin (converted SKILL.md + prompt.md), NOT skills/<slug>/SKILL.md directly.", - "converter": "$converter skills/<slug> codex (parse SkillBundle -> convert to Codex target -> write twin). --all codex regenerates the whole mirror.", - "twin_files": [ - "SKILL.md (Codex-native phrasing)", - "prompt.md (Codex prompt form)", - "references/ + scripts/ (mirrored)", - ".agentops-generated.json (source/generated drift hashes)" - ], - "override_catalog": "skills-codex/.agentops-manifest.json -> codex_override_catalog (per-skill treatment map, waved)", - "integrity_gate": "scripts/regen-codex-hashes.sh --check (CI drift gate; --only <slug> scopes a single-skill PR)", - "note": "This bundle does NOT re-run the converter - the twins already exist for the current Codex corpus (66 in skills-codex/). It PACKAGES the CORE-40 subset and PROVES the twins are present + hash-consistent." - }, - "codex_override_catalog": { - "manifest_path": "skills-codex/.agentops-manifest.json", - "catalog_hash": "dff1716e6844c15ec6643dbdd3e613faac18b8c61c62d214b00e8214570752aa", - "waves": { - "backbone": "Existing bespoke Codex-first overrides for the execution backbone and shared runtime entry points.", - "core-execution": "Primary execution and session-continuity skills that anchor day-to-day Codex work.", - "analysis-authoring": "Analysis and authoring skills where Codex phrasing and artifact expectations materially affect usability.", - "contribution-workflow": "Open source contribution chain skills that need explicit PR-oriented Codex behavior.", - "security-focused": "Security review workflows where findings format and gate semantics need bespoke Codex guidance.", - "catalog-parity": "Skills reviewed explicitly and kept on converter parity until a real Codex-only divergence appears." - }, - "core_wave_distribution": { - "backbone": 9, - "core-execution": 6, - "catalog-parity": 14, - "analysis-authoring": 0 - } - }, - "core_count": 29, - "method_core_count": 20, - "tool_op_core_count": 9, - "core_skills": [ + "schema_version": "skill-image.v1", + "skill_count": 50, + "skills": [ { - "slug": "rpi", - "kind": "method", - "twin_path": "skills-codex/rpi/", - "source_path": "skills/rpi/", - "twin_files": { - "skill": "skills-codex/rpi/SKILL.md", - "prompt": "skills-codex/rpi/prompt.md", - "drift_marker": "skills-codex/rpi/.agentops-generated.json" - }, - "codex_wave": "backbone", - "codex_treatment": "bespoke" - }, - { - "slug": "discovery", - "kind": "method", - "twin_path": "skills-codex/discovery/", - "source_path": "skills/discovery/", - "twin_files": { - "skill": "skills-codex/discovery/SKILL.md", - "prompt": "skills-codex/discovery/prompt.md", - "drift_marker": "skills-codex/discovery/.agentops-generated.json" - }, - "codex_wave": "backbone", - "codex_treatment": "parity_only" - }, - { - "slug": "research", - "kind": "method", - "twin_path": "skills-codex/research/", - "source_path": "skills/research/", - "twin_files": { - "skill": "skills-codex/research/SKILL.md", - "prompt": "skills-codex/research/prompt.md", - "drift_marker": "skills-codex/research/.agentops-generated.json" - }, - "codex_wave": "backbone", - "codex_treatment": "bespoke" - }, - { - "slug": "plan", - "kind": "method", - "twin_path": "skills-codex/plan/", - "source_path": "skills/plan/", - "twin_files": { - "skill": "skills-codex/plan/SKILL.md", - "prompt": "skills-codex/plan/prompt.md", - "drift_marker": "skills-codex/plan/.agentops-generated.json" - }, - "codex_wave": "backbone", - "codex_treatment": "bespoke" - }, - { - "slug": "implement", - "kind": "method", - "twin_path": "skills-codex/implement/", - "source_path": "skills/implement/", - "twin_files": { - "skill": "skills-codex/implement/SKILL.md", - "prompt": "skills-codex/implement/prompt.md", - "drift_marker": "skills-codex/implement/.agentops-generated.json" - }, - "codex_wave": "core-execution", - "codex_treatment": "bespoke" - }, - { - "slug": "crank", - "kind": "method", - "twin_path": "skills-codex/crank/", - "source_path": "skills/crank/", - "twin_files": { - "skill": "skills-codex/crank/SKILL.md", - "prompt": "skills-codex/crank/prompt.md", - "drift_marker": "skills-codex/crank/.agentops-generated.json" - }, - "codex_wave": "backbone", - "codex_treatment": "bespoke" - }, - { - "slug": "swarm", - "kind": "method", - "twin_path": "skills-codex/swarm/", - "source_path": "skills/swarm/", - "twin_files": { - "skill": "skills-codex/swarm/SKILL.md", - "prompt": "skills-codex/swarm/prompt.md", - "drift_marker": "skills-codex/swarm/.agentops-generated.json" - }, - "codex_wave": "backbone", - "codex_treatment": "bespoke" - }, - { - "slug": "validate", - "kind": "method", - "twin_path": "skills-codex/validate/", - "source_path": "skills/validate/", - "twin_files": { - "skill": "skills-codex/validate/SKILL.md", - "prompt": "skills-codex/validate/prompt.md", - "drift_marker": "skills-codex/validate/.agentops-generated.json" - }, - "codex_wave": "catalog-parity", - "codex_treatment": "parity_only" - }, - { - "slug": "council", - "kind": "method", - "twin_path": "skills-codex/council/", - "source_path": "skills/council/", - "twin_files": { - "skill": "skills-codex/council/SKILL.md", - "prompt": "skills-codex/council/prompt.md", - "drift_marker": "skills-codex/council/.agentops-generated.json" - }, - "codex_wave": "backbone", - "codex_treatment": "bespoke" - }, - { - "slug": "premortem", - "kind": "method", - "twin_path": "skills-codex/premortem/", - "source_path": "skills/premortem/", - "twin_files": { - "skill": "skills-codex/premortem/SKILL.md", - "prompt": "skills-codex/premortem/prompt.md", - "drift_marker": "skills-codex/premortem/.agentops-generated.json" - }, - "codex_wave": "core-execution", - "codex_treatment": "bespoke" - }, - { - "slug": "postmortem", - "kind": "method", - "twin_path": "skills-codex/postmortem/", - "source_path": "skills/postmortem/", - "twin_files": { - "skill": "skills-codex/postmortem/SKILL.md", - "prompt": "skills-codex/postmortem/prompt.md", - "drift_marker": "skills-codex/postmortem/.agentops-generated.json" - }, - "codex_wave": "backbone", - "codex_treatment": "bespoke" - }, - { - "slug": "goals", - "kind": "method", - "twin_path": "skills-codex/goals/", - "source_path": "skills/goals/", - "twin_files": { - "skill": "skills-codex/goals/SKILL.md", - "prompt": "skills-codex/goals/prompt.md", - "drift_marker": "skills-codex/goals/.agentops-generated.json" - }, - "codex_wave": "core-execution", - "codex_treatment": "bespoke" - }, - { - "slug": "evolve", - "kind": "method", - "twin_path": "skills-codex/evolve/", - "source_path": "skills/evolve/", - "twin_files": { - "skill": "skills-codex/evolve/SKILL.md", - "prompt": "skills-codex/evolve/prompt.md", - "drift_marker": "skills-codex/evolve/.agentops-generated.json" - }, - "codex_wave": "backbone", - "codex_treatment": "bespoke" - }, - { - "slug": "bootstrap", - "kind": "method", - "twin_path": "skills-codex/bootstrap/", - "source_path": "skills/bootstrap/", - "twin_files": { - "skill": "skills-codex/bootstrap/SKILL.md", - "prompt": "skills-codex/bootstrap/prompt.md", - "drift_marker": "skills-codex/bootstrap/.agentops-generated.json" - }, - "codex_wave": "catalog-parity", - "codex_treatment": "parity_only" - }, - { - "slug": "handoff", - "kind": "method", - "twin_path": "skills-codex/handoff/", - "source_path": "skills/handoff/", - "twin_files": { - "skill": "skills-codex/handoff/SKILL.md", - "prompt": "skills-codex/handoff/prompt.md", - "drift_marker": "skills-codex/handoff/.agentops-generated.json" - }, - "codex_wave": "core-execution", - "codex_treatment": "bespoke" - }, - { - "slug": "push", - "kind": "method", - "twin_path": "skills-codex/push/", - "source_path": "skills/push/", - "twin_files": { - "skill": "skills-codex/push/SKILL.md", - "prompt": "skills-codex/push/prompt.md", - "drift_marker": "skills-codex/push/.agentops-generated.json" - }, - "codex_wave": "core-execution", - "codex_treatment": "bespoke" - }, - { - "slug": "scope", - "kind": "method", - "twin_path": "skills-codex/scope/", - "source_path": "skills/scope/", - "twin_files": { - "skill": "skills-codex/scope/SKILL.md", - "prompt": "skills-codex/scope/prompt.md", - "drift_marker": "skills-codex/scope/.agentops-generated.json" - }, - "codex_wave": "catalog-parity", - "codex_treatment": "parity_only" - }, - { - "slug": "status", - "kind": "method", - "twin_path": "skills-codex/status/", - "source_path": "skills/status/", - "twin_files": { - "skill": "skills-codex/status/SKILL.md", - "prompt": "skills-codex/status/prompt.md", - "drift_marker": "skills-codex/status/.agentops-generated.json" - }, - "codex_wave": "core-execution", - "codex_treatment": "bespoke" - }, - { - "slug": "test", - "kind": "method", - "twin_path": "skills-codex/test/", - "source_path": "skills/test/", - "twin_files": { - "skill": "skills-codex/test/SKILL.md", - "prompt": "skills-codex/test/prompt.md", - "drift_marker": "skills-codex/test/.agentops-generated.json" - }, - "codex_wave": "catalog-parity", - "codex_treatment": "parity_only" - }, - { - "slug": "skill-builder", - "kind": "method", - "twin_path": "skills-codex/skill-builder/", - "source_path": "skills/skill-builder/", - "twin_files": { - "skill": "skills-codex/skill-builder/SKILL.md", - "prompt": "skills-codex/skill-builder/prompt.md", - "drift_marker": "skills-codex/skill-builder/.agentops-generated.json" - }, - "codex_wave": "catalog-parity", - "codex_treatment": "parity_only" - }, - { - "slug": "beads-br", - "kind": "tool-op", - "twin_path": "skills-codex/beads-br/", - "source_path": "skills/beads-br/", - "twin_files": { - "skill": "skills-codex/beads-br/SKILL.md", - "prompt": "skills-codex/beads-br/prompt.md", - "drift_marker": "skills-codex/beads-br/.agentops-generated.json" - }, - "codex_wave": "catalog-parity", - "codex_treatment": "parity_only" - }, - { - "slug": "beads-bv", - "kind": "tool-op", - "twin_path": "skills-codex/beads-bv/", - "source_path": "skills/beads-bv/", - "twin_files": { - "skill": "skills-codex/beads-bv/SKILL.md", - "prompt": "skills-codex/beads-bv/prompt.md", - "drift_marker": "skills-codex/beads-bv/.agentops-generated.json" - }, - "codex_wave": "catalog-parity", - "codex_treatment": "parity_only" + "disposition": "keep_specialist", + "slug": "account-rotation", + "source_path": "skills/account-rotation/", + "twin_path": "skills-codex/account-rotation/" }, { + "disposition": "keep_optional_adapter", "slug": "agent-mail", - "kind": "tool-op", - "twin_path": "skills-codex/agent-mail/", "source_path": "skills/agent-mail/", - "twin_files": { - "skill": "skills-codex/agent-mail/SKILL.md", - "prompt": "skills-codex/agent-mail/prompt.md", - "drift_marker": "skills-codex/agent-mail/.agentops-generated.json" - }, - "codex_wave": "catalog-parity", - "codex_treatment": "parity_only" + "twin_path": "skills-codex/agent-mail/" }, { - "slug": "ntm", - "kind": "tool-op", - "twin_path": "skills-codex/ntm/", - "source_path": "skills/ntm/", - "twin_files": { - "skill": "skills-codex/ntm/SKILL.md", - "prompt": "skills-codex/ntm/prompt.md", - "drift_marker": "skills-codex/ntm/.agentops-generated.json" - }, - "codex_wave": "catalog-parity", - "codex_treatment": "parity_only" + "disposition": "keep_optional_adapter", + "slug": "agent-native", + "source_path": "skills/agent-native/", + "twin_path": "skills-codex/agent-native/" }, { + "disposition": "keep_optional_adapter", + "slug": "agy-native", + "source_path": "skills/agy-native/", + "twin_path": "skills-codex/agy-native/" + }, + { + "disposition": "keep_optional_adapter", + "slug": "automation-shape-routing", + "source_path": "skills/automation-shape-routing/", + "twin_path": "skills-codex/automation-shape-routing/" + }, + { + "disposition": "keep_specialist", + "slug": "bootstrap", + "source_path": "skills/bootstrap/", + "twin_path": "skills-codex/bootstrap/" + }, + { + "disposition": "keep_specialist", "slug": "cass", - "kind": "tool-op", - "twin_path": "skills-codex/cass/", "source_path": "skills/cass/", - "twin_files": { - "skill": "skills-codex/cass/SKILL.md", - "prompt": "skills-codex/cass/prompt.md", - "drift_marker": "skills-codex/cass/.agentops-generated.json" - }, - "codex_wave": "catalog-parity", - "codex_treatment": "parity_only" - }, - { - "slug": "dcg", - "kind": "tool-op", - "twin_path": "skills-codex/dcg/", - "source_path": "skills/dcg/", - "twin_files": { - "skill": "skills-codex/dcg/SKILL.md", - "prompt": "skills-codex/dcg/prompt.md", - "drift_marker": "skills-codex/dcg/.agentops-generated.json" - }, - "codex_wave": "catalog-parity", - "codex_treatment": "parity_only" - }, - { - "slug": "rch", - "kind": "tool-op", - "twin_path": "skills-codex/rch/", - "source_path": "skills/rch/", - "twin_files": { - "skill": "skills-codex/rch/SKILL.md", - "prompt": "skills-codex/rch/prompt.md", - "drift_marker": "skills-codex/rch/.agentops-generated.json" - }, - "codex_wave": "catalog-parity", - "codex_treatment": "parity_only" - }, - { - "slug": "sbh", - "kind": "tool-op", - "twin_path": "skills-codex/sbh/", - "source_path": "skills/sbh/", - "twin_files": { - "skill": "skills-codex/sbh/SKILL.md", - "prompt": "skills-codex/sbh/prompt.md", - "drift_marker": "skills-codex/sbh/.agentops-generated.json" - }, - "codex_wave": "catalog-parity", - "codex_treatment": "parity_only" + "twin_path": "skills-codex/cass/" }, { + "disposition": "keep_specialist", "slug": "cc-hooks", - "kind": "tool-op", - "twin_path": "skills-codex/cc-hooks/", "source_path": "skills/cc-hooks/", - "twin_files": { - "skill": "skills-codex/cc-hooks/SKILL.md", - "prompt": "skills-codex/cc-hooks/prompt.md", - "drift_marker": "skills-codex/cc-hooks/.agentops-generated.json" - }, - "codex_wave": "catalog-parity", - "codex_treatment": "parity_only" + "twin_path": "skills-codex/cc-hooks/" + }, + { + "disposition": "keep_specialist", + "slug": "codebase-recon", + "source_path": "skills/codebase-recon/", + "twin_path": "skills-codex/codebase-recon/" + }, + { + "disposition": "keep_optional_adapter", + "slug": "codex-exec", + "source_path": "skills/codex-exec/", + "twin_path": "skills-codex/codex-exec/" + }, + { + "disposition": "keep_specialist", + "slug": "converter", + "source_path": "skills/converter/", + "twin_path": "skills-codex/converter/" + }, + { + "disposition": "keep_strategy", + "slug": "council", + "source_path": "skills/council/", + "twin_path": "skills-codex/council/" + }, + { + "disposition": "keep_specialist", + "slug": "dcg", + "source_path": "skills/dcg/", + "twin_path": "skills-codex/dcg/" + }, + { + "disposition": "keep_specialist", + "slug": "doc", + "source_path": "skills/doc/", + "twin_path": "skills-codex/doc/" + }, + { + "disposition": "keep_specialist", + "slug": "domain", + "source_path": "skills/domain/", + "twin_path": "skills-codex/domain/" + }, + { + "disposition": "keep_strategy", + "slug": "dueling-idea-genies", + "source_path": "skills/dueling-idea-genies/", + "twin_path": "skills-codex/dueling-idea-genies/" + }, + { + "disposition": "keep_specialist", + "slug": "goals", + "source_path": "skills/goals/", + "twin_path": "skills-codex/goals/" + }, + { + "disposition": "keep_specialist", + "slug": "handoff", + "source_path": "skills/handoff/", + "twin_path": "skills-codex/handoff/" + }, + { + "disposition": "keep_specialist", + "slug": "heal-skill", + "source_path": "skills/heal-skill/", + "twin_path": "skills-codex/heal-skill/" + }, + { + "disposition": "keep_strategy", + "slug": "idea-genie", + "source_path": "skills/idea-genie/", + "twin_path": "skills-codex/idea-genie/" + }, + { + "disposition": "keep", + "slug": "implement", + "source_path": "skills/implement/", + "twin_path": "skills-codex/implement/" + }, + { + "disposition": "keep_off_path", + "slug": "learn", + "source_path": "skills/learn/", + "twin_path": "skills-codex/learn/" + }, + { + "disposition": "keep_specialist", + "slug": "ms", + "source_path": "skills/ms/", + "twin_path": "skills-codex/ms/" + }, + { + "disposition": "keep_optional_adapter", + "slug": "ntm", + "source_path": "skills/ntm/", + "twin_path": "skills-codex/ntm/" + }, + { + "disposition": "keep_specialist", + "slug": "operationalize", + "source_path": "skills/operationalize/", + "twin_path": "skills-codex/operationalize/" + }, + { + "disposition": "keep_specialist", + "slug": "pattern-mining", + "source_path": "skills/pattern-mining/", + "twin_path": "skills-codex/pattern-mining/" + }, + { + "disposition": "keep", + "slug": "plan", + "source_path": "skills/plan/", + "twin_path": "skills-codex/plan/" + }, + { + "disposition": "keep_strategy", + "slug": "postmortem", + "source_path": "skills/postmortem/", + "twin_path": "skills-codex/postmortem/" + }, + { + "disposition": "keep_strategy", + "slug": "premortem", + "source_path": "skills/premortem/", + "twin_path": "skills-codex/premortem/" + }, + { + "disposition": "keep_specialist", + "slug": "product", + "source_path": "skills/product/", + "twin_path": "skills-codex/product/" + }, + { + "disposition": "keep_specialist", + "slug": "rch", + "source_path": "skills/rch/", + "twin_path": "skills-codex/rch/" + }, + { + "disposition": "keep_strategy", + "slug": "reality-check", + "source_path": "skills/reality-check/", + "twin_path": "skills-codex/reality-check/" + }, + { + "disposition": "keep_specialist", + "slug": "refactor", + "source_path": "skills/refactor/", + "twin_path": "skills-codex/refactor/" + }, + { + "disposition": "keep_specialist", + "slug": "research", + "source_path": "skills/research/", + "twin_path": "skills-codex/research/" + }, + { + "disposition": "keep_specialist", + "slug": "reverse-engineer", + "source_path": "skills/reverse-engineer/", + "twin_path": "skills-codex/reverse-engineer/" + }, + { + "disposition": "keep", + "slug": "rpi", + "source_path": "skills/rpi/", + "twin_path": "skills-codex/rpi/" + }, + { + "disposition": "keep_specialist", + "slug": "sbh", + "source_path": "skills/sbh/", + "twin_path": "skills-codex/sbh/" + }, + { + "disposition": "keep_specialist", + "slug": "scaffold", + "source_path": "skills/scaffold/", + "twin_path": "skills-codex/scaffold/" + }, + { + "disposition": "keep_specialist", + "slug": "scope", + "source_path": "skills/scope/", + "twin_path": "skills-codex/scope/" + }, + { + "disposition": "keep_specialist", + "slug": "security", + "source_path": "skills/security/", + "twin_path": "skills-codex/security/" + }, + { + "disposition": "keep_specialist", + "slug": "shared", + "source_path": "skills/shared/", + "twin_path": "skills-codex/shared/" + }, + { + "disposition": "keep_specialist", + "slug": "skill-builder", + "source_path": "skills/skill-builder/", + "twin_path": "skills-codex/skill-builder/" + }, + { + "disposition": "keep_specialist", + "slug": "standards", + "source_path": "skills/standards/", + "twin_path": "skills-codex/standards/" + }, + { + "disposition": "keep_specialist", + "slug": "status", + "source_path": "skills/status/", + "twin_path": "skills-codex/status/" + }, + { + "disposition": "keep_optional_adapter", + "slug": "swarm", + "source_path": "skills/swarm/", + "twin_path": "skills-codex/swarm/" + }, + { + "disposition": "keep_specialist", + "slug": "test", + "source_path": "skills/test/", + "twin_path": "skills-codex/test/" + }, + { + "disposition": "keep_specialist", + "slug": "toil-mining", + "source_path": "skills/toil-mining/", + "twin_path": "skills-codex/toil-mining/" + }, + { + "disposition": "keep_optional_adapter", + "slug": "using-gc", + "source_path": "skills/using-gc/", + "twin_path": "skills-codex/using-gc/" + }, + { + "disposition": "keep", + "slug": "validate", + "source_path": "skills/validate/", + "twin_path": "skills-codex/validate/" + }, + { + "disposition": "keep_specialist", + "slug": "workflow-builder", + "source_path": "skills/workflow-builder/", + "twin_path": "skills-codex/workflow-builder/" } ], - "codex_operator_skills": [ - { - "slug": "codex-exec", - "primitive": "codex exec", - "note": "non-interactive worker/validator via codex exec (Pro sub; never API-print)" - } - ] + "source": "skills/*/SKILL.md metadata" } diff --git a/images/codex/verify.sh b/images/codex/verify.sh index c107d9255..d0041a45a 100755 --- a/images/codex/verify.sh +++ b/images/codex/verify.sh @@ -1,17 +1,17 @@ #!/usr/bin/env bash # verify.sh - Codex image bundle integrity check (cp-eoxc / cp-gqu Unit-4). # -# For each CORE slug in images/codex/manifest.json, confirm its skills-codex/<slug>/ +# For each current slug in images/codex/manifest.json, confirm its skills-codex/<slug>/ # twin is present and complete: SKILL.md AND prompt.md AND .agentops-generated.json # all exist. Missing or incomplete twins are FLAGGED (non-zero exit), never silently -# passed. The corpus is post-distillation, so all manifest-listed CORE twins should exist. +# passed. Every metadata-listed twin must exist. # # This is presence/packaging verification ONLY. Hash-consistency (twin in sync with # source) is the separate, authoritative gate: scripts/regen-codex-hashes.sh --check, # which this script also runs as the final step. # # Usage: bash images/codex/verify.sh (run from the agentops repo root or anywhere) -# Exit: 0 = all CORE twins present and complete + hashes in sync; non-zero otherwise. +# Exit: 0 = all current twins present and complete + hashes in sync; non-zero otherwise. set -euo pipefail @@ -27,33 +27,29 @@ if [ ! -f "${MANIFEST}" ]; then exit 2 fi -# Extract the CORE manifest rows (no jq dependency; use python3). +# Extract the generated manifest rows (no jq dependency; use python3). mapfile -t CORE_ROWS < <(python3 -c ' import json, sys m = json.load(open(sys.argv[1])) -for s in m["core_skills"]: - files = s["twin_files"] +for s in m["skills"]: print("\t".join([ s["slug"], s["twin_path"], - files["skill"], - files["prompt"], - files["drift_marker"], ])) ' "${MANIFEST}") -EXPECTED="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["core_count"])' "${MANIFEST}")" +EXPECTED="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["skill_count"])' "${MANIFEST}")" -echo "Codex image bundle verify - CORE twins in skills-codex/" +echo "Codex image bundle verify - current twins in skills-codex/" echo " repo root : ${REPO_ROOT}" echo " manifest : ${MANIFEST}" -echo " expected : ${EXPECTED} CORE slugs" +echo " expected : ${EXPECTED} current slugs" echo missing=0 checked=0 for row in "${CORE_ROWS[@]}"; do - IFS=$'\t' read -r slug twin_path skill_file prompt_file drift_file <<<"${row}" + IFS=$'\t' read -r slug twin_path <<<"${row}" [ -z "${slug}" ] && continue checked=$((checked + 1)) expected_twin_path="skills-codex/${slug}/" @@ -61,18 +57,18 @@ for row in "${CORE_ROWS[@]}"; do echo "MISSING/STALE: ${slug} twin_path is '${twin_path}', want '${expected_twin_path}'" >&2 missing=$((missing + 1)) fi - for file in "${skill_file}" "${prompt_file}" "${drift_file}"; do + for file in "${expected_twin_path}SKILL.md" "${expected_twin_path}prompt.md" "${expected_twin_path}.agentops-generated.json"; do if [[ "${file}" != "${expected_twin_path}"* ]]; then - echo "MISSING/STALE: ${file} (CORE slug '${slug}' twin_files path outside '${expected_twin_path}')" >&2 + echo "MISSING/STALE: ${file} (slug '${slug}' path outside '${expected_twin_path}')" >&2 missing=$((missing + 1)) elif [ ! -f "${file}" ]; then - echo "MISSING/STALE: ${file} (CORE slug '${slug}' twin incomplete)" >&2 + echo "MISSING/STALE: ${file} (slug '${slug}' twin incomplete)" >&2 missing=$((missing + 1)) fi done done -echo "Checked ${checked} CORE slugs." +echo "Checked ${checked} current slugs." if [ "${checked}" -ne "${EXPECTED}" ]; then echo "FAIL: checked ${checked} slugs but manifest declares ${EXPECTED}." >&2 @@ -84,7 +80,7 @@ if [ "${missing}" -ne 0 ]; then exit 1 fi -echo "OK: all ${checked} CORE twins present (SKILL.md + prompt.md + .agentops-generated.json)." +echo "OK: all ${checked} current twins present (SKILL.md + prompt.md + .agentops-generated.json)." echo # Authoritative sync gate: twins hash-consistent with their source skills. @@ -97,4 +93,4 @@ else fi echo -echo "PASS: Codex image bundle verified (${checked} CORE twins present + hashes in sync)." +echo "PASS: Codex image bundle verified (${checked} current twins present + hashes in sync)." diff --git a/images/gemini/plugin.json b/images/gemini/plugin.json index e99230c0e..4dc508379 100644 --- a/images/gemini/plugin.json +++ b/images/gemini/plugin.json @@ -1,17 +1,17 @@ { - "name": "agentops-core-gemini", - "version": "3.2.0", - "description": "AgentOps 39-skill image (38 CORE + agy-native operator) packaged for Google Antigravity (AGY) / Gemini. Portable SKILL.md set (identical to the Claude image - zero conversion) wrapped in an Antigravity plugin, with Agent Mail MCP and AgentOps guard/evidence hooks.", - "skills": "./skills", - "agents": "./agents", - "rules": "./rules", - "hooks": "./hooks/hooks.json", - "mcpServers": { - "agent-mail": { - "command": "am", - "args": [ - "serve-stdio" - ] - } - } + "agents": "./agents", + "description": "AgentOps 50-skill metadata-derived bundle for Google Antigravity and Gemini.", + "hooks": "./hooks/hooks.json", + "mcpServers": { + "agent-mail": { + "args": [ + "serve-stdio" + ], + "command": "am" + } + }, + "name": "agentops-core-gemini", + "rules": "./rules", + "skills": "./skills", + "version": "3.2.0" } diff --git a/images/gemini/skills/account-rotation/SKILL.md b/images/gemini/skills/account-rotation/SKILL.md index 1b587ff4c..dd2ecbe60 100644 --- a/images/gemini/skills/account-rotation/SKILL.md +++ b/images/gemini/skills/account-rotation/SKILL.md @@ -3,146 +3,36 @@ name: account-rotation user-invocable: false skill_api_version: 1 hexagonal_role: supporting +consumes: [] +produces: [] +context_rel: [] metadata: + dependencies: [] + capabilities: [account_rotation] + effects: [] + canonical_status: canonical + disposition: keep_specialist tier: execution -description: 'Switch coding-agent accounts on a usage/rate limit. Routes by host+agent: macOS+Claude to claude-acct; Codex/Gemini and Linux/WSL to caam. Triggers: "account-rotation", "account rotation", "switch coding-agent accounts on a".' +description: 'Switch a caller-selected coding-agent account and report the observed identity.' practices: - pragmatic-programmer --- -<!-- TOC: Quick Start | Why the Route Exists | claude-acct (Mac+Claude) | caam (everything else) | Capture Discipline | Live-Session Caveat | Swarm Lanes --> +# Account rotation — credential adapter -# account-rotation — switch coding-agent accounts on a rate limit +Choose the credential tool from both host and agent family, perform only the +explicit account switch, and report the identity observed by the matching +runtime. -> **The moment:** you hit a usage limit on a Claude Max / Codex Pro / Gemini -> subscription and want to keep working on a fresh account, or you're spreading -> swarm lanes across accounts for parallel quota. **The tool depends on the host** -> — because the credential *layer* differs by OS+agent. This skill routes; the -> tools do the swap. +## Boundary -## Critical Constraints +- On macOS with Claude credentials, use the operator's `claude-acct` route. +- For Codex, Gemini, Linux, or WSL file-backed credentials, use `caam`. +- Verify account identity through the target runtime; token bytes are not account + identity. +- Existing processes retain credentials already loaded in memory. Rotation + affects a new process. +- This skill does not restart work, resume a task, select a pane, move repository + state, or decide what happens after the switch. -- **Route from both host and agent family. Why:** macOS Claude credentials live - in Keychain, while the other supported routes use file-backed credentials; - choosing from the agent name alone can report success without changing the - credential the next process reads. -- **Verify account identity, not token bytes. Why:** OAuth can issue distinct - tokens for the same account, so token hashes cannot prove that quota moved to - a different subscription. -- **Treat rotation as next-process state. Why:** a running agent keeps its token - in memory; the new credential takes effect only after that CLI is relaunched. - -## Quick Start — route first - -``` -macOS + Claude → claude-acct (Keychain layer) -macOS + Codex/Gemini → caam (file layer) -Linux / WSL + anything → caam (file layer) -``` - -## Why the route exists (the load-bearing fact) - -caam swaps the auth **file** (`~/.claude/.credentials.json`, codex/gemini auth -files). Correct for file-based auth — **Codex, Gemini, and Claude-on-Linux.** But -current **Claude Code on macOS stores its token in the login Keychain** (`security` -service `Claude Code-credentials`) and *ignores* that file. So `caam activate`/`next` -for Claude on Mac are **no-ops** — they swap a file Claude doesn't read. That one -exception is the whole reason this router exists. - -## macOS + Claude → `claude-acct` (Keychain swap) - -A full Claude account on Mac = **two pieces**, both swapped together: the Keychain -**token** + the `~/.claude.json` **`.oauthAccount`** identity block (`claude auth -status` reads the email from the latter; a token-only swap leaves the identity -pinned to the last login → "only the current account works"). `claude-acct` swaps -both via `security add/delete-generic-password -A` (`-A` = no GUI prompt, so -headless workers don't stall) + a JSON splice of `.oauthAccount`. - -```bash -claude-acct list # captured accounts → real email each maps to -claude-acct current # which account a NEW claude starts on -claude-acct use <name> # swap (token + identity) -claude-acct login <name> [email] # one-time capture (see Capture Discipline) -``` -Tool: `dotfiles/bin/claude-acct`. - -## macOS+Codex/Gemini & all Linux/WSL → `caam` (file swap) - -caam is the adopted file-based rotator and is correct here. It self-documents -(the CLI is the doc; a dedicated `caam` skill also exists): - -```bash -caam status <tool> # vault + health -caam next <tool> # rotate to next non-cooldown account -caam use <tool> <profile> -caam --help # full surface -``` -On bushido (Ubuntu) this is the **only** rotator you need — file-based auth means -caam's swap actually takes. - -## Capture Discipline (the two traps — apply to both tools) - -1. **Distinct token bytes ≠ distinct accounts.** OAuth re-issues a fresh token - each login, so N logins to the *same* account produce N different hashes. - Verify by **account email**, not token hash. (`claude-acct` warns on collision.) -2. **The browser captures whichever account the provider is signed into.** Email - login-hints are ignored when a session exists. **Log out (or use a - Private/Incognito window) before each capture login**, or it re-grabs the - current account. - -## Live-Session Caveat - -A running agent process holds its token in memory; rotation changes what a **new** -process picks up, not the live session. To move the session you're in: rotate, -then **relaunch** the CLI. Exactly right for spawning swarm lanes. - -## Swarm Lanes (the real unlock) - -Parallel quota = put each lane on a different account **before** launching it: - -```bash -# Mac (Claude) -claude-acct use acct-a && <spawn lane A>; claude-acct use acct-b && <spawn lane B> -# Linux / Codex -caam use codex acct-a && <spawn lane A>; caam next codex && <spawn lane B> -``` -A dispatcher's limit-hit hook calls `claude-acct use` on Mac / `caam next` on -Linux, then re-dispatches the lane's work. - -## Output Specification - -- **Artifact directory:** stdout only; this skill writes no repository artifact - and leaves credential storage to `claude-acct` or `caam`. -- **Filename convention:** none. Report one rotation receipt in the response - with the selected route, command, target account/profile, and relaunch action. -- **Serialization/schema format:** UTF-8 text with the fields `route`, `command`, - `target`, `verification`, and `relaunch_required`. -- **Validator command:** run `claude-acct current` for macOS Claude, otherwise - `caam status <tool>`, and include the observed identity/status in the receipt. -- **Downstream handoff:** relaunch the affected CLI or re-dispatch the lane from - its worktree and bead after the validator confirms the intended account. - -## Quality Rubric - -- [ ] The chosen route names both the host and the agent family. -- [ ] The receipt includes identity/status observed from the matching validator. -- [ ] The handoff explicitly says whether a relaunch or lane re-dispatch remains. - -## Navi-rotate (the cross-model helper rotates a peer — trilateral) - -In the trilateral (2 Claude builders + 1 Codex **Navi**), the Navi runs on a -DIFFERENT runtime/account, so it is UNAFFECTED by a builder's Claude rate limit — -making it the right agent to rotate a limited builder. `navi-rotate` -(`dotfiles/bin/navi-rotate`) wraps `claude-acct` with rotation-order + peer-relaunch -signaling, so the move is one repeatable command: - -```bash -navi-rotate <peer-tmux-session> [--to <account>] [--dry-run] -# Navi: next account (claude-acct list order) -> claude-acct use <next> -# -> am + atm signal the peer to relaunch. -``` - -Per the **Live-Session Caveat**: the swap lands on the peer's NEXT launch, not its -live session — continuity rides the durable substrate (worktree + bead + handoff), -so the peer resumes from its last bead on the fresh account. This is the repeatable, -cross-model-driven form of the dispatcher limit-hit hook above. Routed correctly: -the swap is always `claude-acct` for Mac+Claude (NEVER caam). +Return the host, agent family, selected tool, requested account/profile, observed +identity/status, command exit code, and whether a new process is required. diff --git a/images/gemini/skills/agent-mail/SKILL.md b/images/gemini/skills/agent-mail/SKILL.md index 4109459c0..8152218eb 100644 --- a/images/gemini/skills/agent-mail/SKILL.md +++ b/images/gemini/skills/agent-mail/SKILL.md @@ -13,195 +13,60 @@ context_rel: - kind: supplier-to with: agent-native metadata: + capabilities: [agent_mail] + effects: [] + canonical_status: canonical + disposition: keep_optional_adapter tier: execution dependencies: [] -description: 'Use when coordinating agents with Agent Mail locks, inboxes, threads, and conflict-prevention handoffs. Triggers: "agent-mail", "agent mail", "use when coordinating agents with".' +description: 'Use Agent Mail as an optional messaging and file-reservation adapter for explicitly coordinated writers.' practices: - pragmatic-programmer --- -<!-- TOC: Boundary | Disciplines | When to Use What | Bootstrap | Reservations | Beads | Troubleshooting | References --> +# Agent Mail — optional coordination adapter -# Using Agent Mail +Agent Mail carries messages, acknowledgements, identities, and temporary file +reservations. It is not a task tracker, queue, proof ledger, or lifecycle +controller. -> **Core Insight:** Agent Mail is the side channel for leases, notifications, acknowledgements, and handoffs. BR/beads is the durable coordination bus and source of truth for work state, evidence, and decisions. +## Boundary -> **⚠️ TWO SURFACES — read this first.** Every operation has BOTH an MCP-tool form (`send_message`, `fetch_inbox`, …) AND a CLI form (`am mail send`, `am mail inbox`, …). The MCP tools are only present when the agent-mail MCP server is wired into your session's tool surface — **a plain CLI/shell agent (or a session where the MCP server didn't load) will NOT have them.** In that case use the `am` CLI, which works from any shell. **Discoverability trap (br cp-jgcl):** the send/reply verbs live under the `am mail` group, which `am --help` does NOT list, and the read commands have flat aliases (`am inbox`, `am status`) but **`am send` does not exist** — it is **`am mail send`**. When in doubt: `am mail --help`, `am macros --help`, `am file_reservations --help`. +- Skip Agent Mail for a single writer. +- The caller supplies the absolute project path, agent identities, thread id, + participants, paths, exclusivity, reason, and TTL. +- Reservations prevent accidental overlap among cooperating writers. They do not + create work ownership or affect Plan, Candidate, or verdict semantics. +- Mail silence proves nothing about work status. +- A message or acknowledgement is evidence that communication occurred, not + evidence that a change is correct or complete. +- Agent Mail never selects work, changes tracker state, commits code, validates, + integrates, closes, releases, or delivers work. -**Don't re-learn the command surface from this skill.** The MCP server self-describes its tools and resources in your tool list; the CLI self-describes via `am --help` and the group helps above. This skill carries only the operating doctrine: when to use mail, the reservation discipline, and the coordination boundaries. Full tool/parameter catalog: [TOOLS.md](references/TOOLS.md). +## Surfaces -> **When this applies (scope guard).** Agent Mail coordination — `start-session`, identity registration, file reservations, cross-lane ACKs — is **required when ≥2 lanes/panes share the repo** (a swarm, or you plus a concurrent peer session). With **only one active writer** (no second lane/pane), do **not** register or reserve against yourself — coordination is an escalation, not a session-start tax. If you are the sole writer, skip to the work. (Doctrine: [operating-loop principle 8](../../docs/architecture/operating-loop.md#governing-principles) — single-agent-first.) -> -> **Asymmetry guardrail.** Agent Mail answers the contention axis; persistent NTM panes answer a separate durability axis. They are independently selectable adapters. Removing the single-writer startup tax does not relax collision safety: partition write scopes first, then require reservations for two live writers sharing a path. +Use the MCP tools when they are present. Otherwise use the self-describing `am` +CLI. Discover current syntax with `am mail --help`, +`am file_reservations --help`, and related group help; do not infer commands +from remembered aliases. -## Critical Constraints +## One-shot use -- **Use Agent Mail only for live multi-writer coordination. Why:** registering - and reserving in a one-writer session adds state without preventing a real - collision. -- **Put durable work truth in BR/beads. Why:** leases and messages expire or can - be missed, while the tracker carries dependencies, evidence, and closure. -- **Treat a conflicting reservation as a stop signal. Why:** writing through a - live exclusive lease defeats the collision boundary this adapter provides. +1. Confirm that multiple explicitly coordinated writers share the repository. +2. Register the caller-supplied identity against the same absolute project path. +3. Reserve only the supplied paths, with a bounded TTL. +4. Report conflicts without waiting, narrowing scope, or changing the plan. +5. Send the supplied message once and record its id. +6. Read or acknowledge only the requested thread. +7. Release only reservations the caller explicitly asks to release. -## Coordination Boundary +## Output -| Need | Source of truth | -|------|-----------------| -| Work queue, status, dependencies, priority, closure evidence | BR/beads (`br`/`bv`) | -| File ownership, active edit leases, lane notifications, acks | Agent Mail | -| Final proof that work is done | Bead notes/closure plus git/CI evidence | -| "Who may write this hot path right now?" | Agent Mail file reservation | - -Use Agent Mail to prevent collisions and notify active agents. Do not use it as the durable task queue, audit log, or final evidence store. If a mail thread and BR disagree, reconcile the bead first and link the mail thread from the bead note if the conversation matters. - -One-writer-per-hot-dir rule: reserve the path before editing it. If the reservation conflicts, do not write into that path; coordinate with the holder, narrow scope, or wait for the lease to clear. - -## Coordination disciplines (2026-06-09, cards 1–5, cp-hhd7) - -### Durable lane identities (card 1, cp-9lrb) - -Register a **durable adjective+noun identity** at session start. Pane text and -human relay are unauditable and load the operator. The ledger is the bus — both -lanes must be registered to exchange auditable messages. Do not coordinate via -informal pane text when `send_message` + `fetch_inbox` is available. - -### Content-push, not pointers (card 2, cp-9lrb) - -When sending a lane a message, **push the content** — include the actual text, -diff, or decision. A pointer to a message-id the recipient cannot discover by -inbox is still a relay. If the recipient's inbox is broken and they cannot read -by-id, a pointer is a dead end. Short content fits in the body; long content goes -to a committed artifact with an absolute path, not an AM-internal id. - -### Intent on the graph first — dedup (card 3, cp-hhtu) - -Before any actor acts on a bead (intake, admit, implement, validate, mutate), -**update the bead status and set the actor on the graph first**, then check for an -existing actor. Parallel pipelines are blind to each other at every tier — dedup -via the ledger, not behavioral coordination. Five exhibits of same-bead parallel -work hit the fleet in one day (impl/validate/admission/mutation/intake, cp-hhtu). -The fix is structural: intent on the graph is the lock. - -### ACK-with-id on routed writes (card 5, cp-fmt8) - -When routing a write through a single writer (e.g. a beads-intake lane), require -**ACK-with-id** back to the requester — the AM message id of the filed bead or the -`ao beads exec show <id>` output confirming the record exists. An unacknowledged routed write -is invisible work. "Are these filed?" must not be a question — the ACK closes it. - -## When to Use What - -| Situation | Action | -|-----------|--------| -| Starting any agent session | `macro_start_session` (CLI: `am macros start-session`) | -| **Confirm a lane actually registered** | `am robot agents --project <abs> --active` — must list your name and each peer lane | -| About to edit files | reserve paths → edit → release reservations | -| Need to tell another agent something | `send_message` with `thread_id` (CLI: `am mail send`) | -| Picking up someone else's work | `macro_prepare_thread` | -| Need durable work state or evidence | Update BR/beads, then link the mail thread if useful | -| Can't message an agent | `request_contact` → wait for approval | -| Server seems broken | `health_check()` first; CLI-only: `doctor check --verbose` → `doctor repair --yes` | - -## Session Bootstrap - -**Call `macro_start_session` (or `am macros start-session --project <abs> --program <p> --model <m> --task "<desc>"`) at the start of every *multi-lane* agent session** (skip it when you are the sole writer — see the scope guard above). One call: ensures project exists → registers your identity → reserves files → fetches inbox. Returns `{project, agent, file_reservations, inbox}`. - -**Verify the lane registered.** A pane can *look* spawned and still have never registered — its start-session may not have landed. Confirm with: - -```bash -am robot agents --project <abs> --active # should list YOUR name and each peer lane -``` - -If your name (or a peer's) is missing, that lane's start-session did not land — **do not assume the coordination leg is live.** Re-run start-session for the missing lane before relying on mail/reservations between you. Skipping this check is how the coordination leg silently goes unverified. - -Identity notes: - -- Agents get adjective+noun names (GreenCastle, BlueLake). Omit `name`/`--name` to auto-generate a valid one. -- `am macros start-session` auto-generates a fresh identity **per project**; you will have a different name in each project. Confirm yours via `am agent start`. -- The other macros (`macro_prepare_thread`, `macro_file_reservation_cycle`, `macro_contact_handshake`) and the fast `resource://` reads are self-described by the server; catalog in [TOOLS.md](references/TOOLS.md) and [RESOURCES.md](references/RESOURCES.md). - -## File Reservations — Reserve Before Editing - -The discipline, not the syntax (syntax: `am file_reservations --help` or the `file_reservation_paths` tool): - -1. **Reserve before the first write.** Glob patterns are fine (`src/auth/**/*.ts`). Set a real `ttl_seconds` and put the bead id in `reason`. -2. **Check `conflicts` in the response.** On conflict: wait for TTL expiry, message the holder, or share with `exclusive=false`. Never write into a conflicted path. -3. **Release when done** (`release_file_reservations` / `am file_reservations release <abs> <me>`). Don't squat on leases across unrelated work. - -## Beads Integration - -Use bead IDs as your threading anchor. The bead tracker remains authoritative; mail carries the lease, notification, and discussion side channel. - -``` -1. Pick work: ao beads exec ready --json → choose bd-123 -2. Reserve files: file_reservation_paths(..., reason="bd-123") -3. Announce: send_message(..., thread_id="bd-123", subject="[bd-123] Starting...") -4. Work: Reply in thread with progress -5. Record evidence: ao beads exec update bd-123 --notes "Validation: tests, commit, CI, or handoff proof" -6. Complete: ao beads exec close bd-123, release_file_reservations(...), final message -``` - -**Bead ID (often bd-###) goes in:** thread_id, subject prefix, reservation reason, commit message - -**Do not infer durable state from mail silence.** A missing reply is not proof that a bead is abandoned, blocked, or complete. Check `ao beads exec show <id> --json`, `bv --robot-insights`, git state, and CI evidence before changing work state. - -## Output Specification - -- **Artifact directory:** no repository directory; Agent Mail persists identity, - reservation, and message records in its own project store and returns receipts - through MCP results or CLI stdout. -- **Filename convention:** none. Refer to durable coordination records by - project, agent name, bead/thread id, and returned message or reservation id. -- **Serialization/schema format:** use the MCP tool result objects or the CLI's - `--json` output when another agent must consume the receipt mechanically. -- **Validator command:** confirm identities with - `am robot agents --project <abs> --active`, then inspect reservations/inbox - with the matching self-described `am file_reservations` or `am mail` command. -- **Downstream handoff:** record the acknowledged message/reservation id in the - BR/bead note when it matters to the work, then release leases after landing. - -## Quality Rubric - -- [ ] Every active writer is registered under the same absolute project path. -- [ ] Every hot-path write has a non-conflicting lease tied to its bead id. -- [ ] Every routed write returns an ACK with a discoverable id or tracker proof. -- [ ] Durable decisions and completion evidence are present in BR/beads, not mail alone. - -## Quick Troubleshooting - -| Error | Fix | -|-------|-----| -| "sender_name not registered" | Call `macro_start_session` first | -| Pane looks spawned but coordination is silent | A pane can look spawned yet never have registered. Run `am robot agents --project <abs> --active` — if the lane is absent, its start-session didn't land; re-run it | -| "FILE_RESERVATION_CONFLICT" | Wait, coordinate, or use `exclusive=false` | -| "CONTACT_BLOCKED" | Use `request_contact`, wait for approval | -| Server unreachable | `am robot health` (works CLI-only, direct SQLite) or `health_check()` (MCP). `curl …:8765/health` only resolves if the HTTP MCP server is running; CLI-only deploys have no `:8765` listener. Start the server with `am` | -| Guard blocks commit | Set `AGENT_NAME` env var; emergency bypass: `AGENT_MAIL_BYPASS=1 git commit` | - -Deeper diagnostics (doctor check/repair), the pre-commit guard (`install_precommit_guard`), the human-overseer web UI, and FTS5 search syntax are all self-described by the server/CLI — see [RECOVERY.md](references/RECOVERY.md) and [ADVANCED.md](references/ADVANCED.md). +Return the project, identity, thread/message ids, reservation ids and paths, +conflicts, timestamps, and any degraded or unavailable surface. The caller owns +all subsequent decisions. ## References -| Topic | Reference | -|-------|-----------| -| All MCP tools | [TOOLS.md](references/TOOLS.md) | -| Workflow patterns | [WORKFLOWS.md](references/WORKFLOWS.md) | -| MCP resources | [RESOURCES.md](references/RESOURCES.md) | -| Cross-project setup | [CROSS-PROJECT.md](references/CROSS-PROJECT.md) | -| Doctor & recovery | [RECOVERY.md](references/RECOVERY.md) | -| Installation | [INSTALL.md](references/INSTALL.md) | -| Fix MCP config | [FIX-MCP-CONFIG.md](references/FIX-MCP-CONFIG.md) | -| Product bus, build slots, internals | [ADVANCED.md](references/ADVANCED.md) | - -## Validation - -```bash -am robot health # PRIMARY check — CLI/direct SQLite, works without the HTTP server -am agent start --json # cockpit; flags a missing :8765 listener under mcp_endpoint=fail -curl http://127.0.0.1:8765/health # ONLY if the HTTP MCP server is up (am serve-http); CLI-only deploys have no :8765 listener -am # start server if needed -``` - ---- - -**Fork maintenance.** `am` is Bo's fork (`boshu2/mcp_agent_mail_rust`). To pull upstream fixes, use the fork-sync factory in `~/dev/mcp_agent_mail_rust`: `make fork-status` → `make fork-preview` → `make fork-sync` (its `AGENTS.md` § "Upstream sync"; never rebase main by hand). Divergence facts are owned by **FORKS-MAP F-3**. +- [CLI and MCP surface notes](references/TOOLS.md) +- [Coordination patterns](references/WORKFLOWS.md) +- [Troubleshooting](references/RECOVERY.md) diff --git a/images/gemini/skills/agent-native/SKILL.md b/images/gemini/skills/agent-native/SKILL.md new file mode 100644 index 000000000..d7e0708b3 --- /dev/null +++ b/images/gemini/skills/agent-native/SKILL.md @@ -0,0 +1,53 @@ +--- +name: agent-native +description: 'Operate explicit orchestrator, implementer, validator, and scribe roles through a caller-selected agent runtime. Triggers: "agent-native factory", "role-shaped agent panes", "persistent workers".' +practices: [team-topologies, design-by-contract] +hexagonal_role: supporting +consumes: [explicit-role-packets] +produces: [runtime-evidence, worker-handoff] +context_rel: +- kind: customer-of + with: ntm +- kind: customer-of + with: agent-mail +skill_api_version: 1 +user-invocable: true +metadata: + tier: meta + dependencies: [] + capabilities: [role_dispatch, observe_workers, handoff] + effects: [manage_runtime_sessions] + canonical_status: canonical + disposition: keep_optional_adapter +output_contract: runtime evidence for explicit packets +--- + +# Agent Native + +Operate caller-selected agent sessions as explicit roles without turning the +runtime into AgentOps lifecycle authority. + +## Roles + +- **Orchestrator:** passes explicit packets and reports runtime facts. +- **Implementer:** may modify only its packet's declared subject. +- **Validator:** receives exact candidate content in a fresh, read-only context. +- **Scribe:** records runtime evidence without judging acceptance. + +## Contract + +1. Require an explicit packet, role, workspace, context identity, and evidence + destination before starting a worker. +2. Prove runtime readiness and engagement from observable state; a successful + prompt send is not proof of work. +3. Keep concurrent writers disjoint and isolated. Runtime coordination is not a + claim, lease, queue, or completion state in AgentOps. +4. Record provider state, transcript references, artifacts, and terminal status. +5. Return runtime evidence to the caller. Do not convert provider retries, + reconnects, idle states, or failures into Plan, Candidate, or verdict state. +6. A validator session may supply judgment to Validate, but only Validate writes + `verdict.v2`. + +NTM, native processes, Agent Mail, and Gas City are replaceable adapters. Use +them only when the caller selected that execution shape. A single local agent +pays no factory coordination cost. diff --git a/images/gemini/skills/agy-native/SKILL.md b/images/gemini/skills/agy-native/SKILL.md index 397c232e7..14662e13b 100644 --- a/images/gemini/skills/agy-native/SKILL.md +++ b/images/gemini/skills/agy-native/SKILL.md @@ -1,247 +1,38 @@ --- name: agy-native -description: |- - Drive AgentOps in AGY: loop, plugins, memory, evidence, --add-dir scoping. - Triggers: agy, antigravity, agy plugin, AGY evidence. -practices: -- team-topologies -- continuous-delivery +description: 'Use an explicitly selected AGY runtime for one provided packet or fresh validator context. Triggers: "agy", "antigravity", "AGY evidence".' +practices: [team-topologies, design-by-contract] hexagonal_role: driving-adapter -consumes: [] -produces: -- agy-run-evidence -context_rel: [] +consumes: [explicit-packet] +produces: [agy-run-evidence] +context_rel: +- kind: separate-ways + with: codex-exec skill_api_version: 1 user-invocable: false -context: - window: inherit - intent: - mode: task - sections: - exclude: [HISTORY] - intel_scope: topic metadata: tier: cross-vendor - dependencies: [ntm, beads-br, dcg, agent-mail] - stability: experimental -output_contract: A green tick on the Antigravity image — a bead moved claim->close by an author subagent, an independent PASS/WARN/FAIL verdict artifact from a distinct-context judge subagent, and a scoped one-bead commit; evidence persisted to the AGY brain store (userFacing:true) and the repo. + dependencies: [] + capabilities: [dispatch_explicit_packet, provide_fresh_context] + effects: [start_agy_session] + canonical_status: canonical + disposition: keep_optional_adapter +output_contract: AGY runtime evidence --- -# agy-native +# AGY Native -Drive the AgentOps loop on the **Antigravity image** (AGY): the `agy` CLI backed by the brain/knowledge store at `~/.gemini/antigravity-cli/`. This is the third harness alongside the Claude image and the Codex image — same loop laws, AGY-native primitives. **Invoke `agy`, never rebuild it.** +Use AGY only when the caller explicitly selects that runtime. Discover its live +command surface before acting and scope every session to the supplied workspace +and packet. -## Overview / When to Use +- Keep author and validator sessions distinct when AGY supplies both roles. +- Persist the runtime conversation/context identity and artifact references. +- Validators remain read-only and hand judgment to Validate; they do not write + the core verdict directly. +- AGY plugin, memory, permission, retry, and session state remain substrate facts + and never become AgentOps phase, queue, or completion state. +- Never invoke `claude -p` through an AGY wrapper. -AGY is Google's Antigravity agent harness exposed as a local CLI (`~/.local/bin/agy`). It speaks the Claude-style packaging shape (plugins, skills, subagents, hooks, MCP) and a **portable `SKILL.md`** — AGY reads skills from `~/.gemini/skills/` directly, so the AgentOps corpus already loads on it. Use this skill when you need the claim->work->validate->close->persist loop running on AGY (driving Gemini, or Claude/GPT models *through* AGY — `agy models` exposes Gemini 3.5/3.1, Claude Sonnet/Opus 4.6, GPT-OSS), as a parallel or fallback to the Claude/Codex images. - -**AGY ≠ gemini-cli.** The retired `gemini` CLI lane (`gemini -p`, `gemini skills`, `gemini extensions`, `--approval-mode`, `--worktree`) is gone. Everything here is an AGY affordance; §"Distribution" + the reference file name each AGY equivalent for an old gemini habit. - -Verified primitives on this host (`agy --help`, `agy plugin help`, `agy models`): -- **Headless run:** `agy -p "<prompt>"` / `agy --print` (one-shot, prints, exits; `--print-timeout` default 5m). `-c`/`--continue` resumes the most recent conversation; `--conversation <id>` resumes by ID. -- **Plugins:** `agy plugin {list,import,install,uninstall,enable,disable,validate,link}`. `import [gemini|claude]` pulls existing plugin trees in. `install <target>` reads a `plugin.json` (supports `plugin@marketplace`). -- **Permissions:** `--dangerously-skip-permissions` auto-approves tool calls (loop/headless lane); `--sandbox` restricts the terminal. -- **Workspace scope:** `--add-dir <dir>` (repeatable) scopes which repos a run can touch (AGY's write-isolation primitive — it scopes by directory, not by spawning worktrees). -- **Brain/knowledge:** durable agent memory + user-facing artifacts under `~/.gemini/antigravity-cli/{brain,knowledge}/` (per-conversation dirs; `*.md` + `*.md.metadata.json` with `{summary, updatedAt, userFacing}`). - -### Folded triggers (ag-s43tg wave 1): the four AGY sibling lanes route here - -- **`agy-mcp-plugins` → the Distribution lane.** Use when wiring MCP servers and AgentOps plugin bundles - into the AGY image with least-privilege access, rollback evidence, and validation hooks — the - §"Distribution" layer below (plugin trees, `agy plugin link/install`, validate → apply → list → - record-rollback) owns that mutation protocol. -- **`agy-project-worktree-permissions` → the isolation rules.** Use when proving AGY project/worktree - isolation with scoped --add-dir permissions, role permission tiers, and `dcg` guardrails — Rules 4–6 - below (non-overlapping `--add-dir` scopes, permission matched to role, the `dcg` BeforeTool hook) - are that contract, with evidence persisted per Rule 3. -- **`agy-rules-workflows` (triggers: AGY rules, agy-loop, AGY schedule) → Phase 2 law packaging.** Use - when installing AGY rules, workflow, goal, and schedule controls for AgentOps loop law — the - `agy-control-plane` plugin tree in Phase 2 (`rules/` + `workflows/` + `hooks.json`) is where the - agy-loop law lands; schedules drive Phase 5. -- **`agy-sidecar-scheduled-tick` → the Phase 5 recurring driver.** Use when running a - recurring AGY sidecar loop tick with agentapi evidence capture — Phase 5's tick lane (AGY - scheduled task, or an external timer / Claude `CronCreate` calling `agy --print`) is that driver. - -## ⚠️ Critical Constraints - -- **Rule 1 — Never `claude -p` for workers (LAW 0).** AGY runs on Gemini OAuth (and proxied Claude/GPT). Drive AGY workers with `agy --print` or `agy -i`, Codex with `codex exec`, Claude only via NTM panes / subagents. **Why:** `claude -p` bills the API per-token, not the Max sub; the overnight factory burned API this exact way (banned). -- **Rule 2 — author != judge, always two contexts.** The subagent that closes a bead must NOT be the one that validates it. Spawn the judge as a separate async subagent with a clean context (or a separate `agy --print` invocation; **never** `-c`/`--continue` across roles — it shares context). **Why:** a self-grading worker is a flatterer; independent verdict is the membrane (control-plane LEARNINGS: a tie-break quorum caught a false-FAIL). -- **Rule 3 — evidence-gated close.** A bead closes only against a persisted verdict artifact (a `brain/*.md` with `userFacing:true` or a committed repo file), never against chat text alone. **Why:** agents are ephemeral; the system carries state. Consume an agent's *published compression*, never its live session. -- **Rule 4 — worktree / `--add-dir` isolation.** Concurrent author and judge get isolated worktrees or non-overlapping `--add-dir` scopes. No two roles edit the same file. **Why:** prevents swarm races and clobbered work. -- **Rule 5 — `dcg` guard stays on.** `~/.gemini/settings.json` wires a `BeforeTool` hook on `run_shell_command` to `dcg`. Do not remove it even under `--dangerously-skip-permissions`. **Why:** it blocks destructive commands the auto-approve flag would otherwise let through. -- **Rule 6 — match permission to role.** Author = `--dangerously-skip-permissions` with a **tight** `--add-dir`; judge = **default** (no auto-approve) with a read-mostly scope; full-auto only inside `--sandbox`. **Why:** auto-approve is a blast-radius choice — a validator that can edit is a false-close path. -- **Rule 7 — operator-side; invoke-never-rebuild.** This drives the flywheel harness. Do NOT write under `~/dev/agentops`, do NOT git push agentops, do NOT treat AGY as something to re-author. **Why:** AGY is Emanuel's substrate (ACFS doctrine) — own a thin adapter, not the tool. - -## Distribution — exposing skills/plugins to AGY - -AGY discovers capability through three layers (weight ascending): a **portable `SKILL.md`** under `~/.gemini/skills/<name>/` (read directly — no packaging), a **plugin** tree with `plugin.json` (`skills`/`subagents`/`hooks`/`mcpServers`), and **MCP servers** (the `agy-mcp-plugins` lane). The retired gemini split of `skills` vs `extensions` collapses into the single AGY **plugin** unit — there is no `agy extensions` surface; treat a former gemini "extension" as an AGY plugin. - -Dev discipline (folded from the retired extension lane): **`agy plugin link <path>`** for local development (live edits, never a stale copy); **`agy plugin install <dir|name@marketplace>`** for released/remote artifacts; don't run both for one plugin. **Source of truth stays in AgentOps** — do not hand-edit the managed runtime copies under `~/.gemini/skills/`. Every mutation: **validate → apply → list → record rollback** (see references). Full verb list, install-vs-link table, and the permission×output×scope matrix: **[references/distribution-and-run-control.md](references/distribution-and-run-control.md)**. - -## Workflow / Methodology - -### Phase 1: Verify the image is live -```bash -which agy && agy models | head # CLI present, models reachable -ls ~/.gemini/antigravity-cli/{brain,knowledge} # brain store exists -agy plugin list # what's already imported/enabled -``` -**Checkpoint:** confirm `agy` resolves, a model lists, and the brain dir exists before dispatching any tick. - -### Phase 2: Package + expose the laws -A bare `SKILL.md` under `~/.gemini/skills/` is portable — no `plugin.json` needed just to expose a skill. To bundle rules + workflows + subagents + hooks + MCP, lay out a plugin tree (the `agy-control-plane` unit): -``` -agy-control-plane/ - plugin.json # { name, version, skills, subagents, hooks, mcpServers } - rules/ # invariant law (author!=judge, evidence-gated close, scoped commit) - workflows/ # slash-command loop trajectories (claim->work->validate->close->persist) - subagents/ # worker.md, validator.md, tie-break.md, scout.md - hooks.json # pre/post-tool guardrails (close gate, format/lint, dcg) - skills/ # or rely on portable ~/.gemini/skills/ -``` -```bash -agy plugin import claude # pull an existing plugin tree in (optional) -agy plugin validate ./agy-control-plane -agy plugin link ./agy-control-plane # dev (live edits) — or `install` for released -agy plugin enable agy-control-plane -agy plugin list # confirm enabled (rollback: disable/uninstall) -``` -**Checkpoint:** `agy plugin validate` passes and `agy plugin list` shows the plugin enabled. - -### Phase 3: One headless tick (author) -Spawn the author in a tight scope with scoped-auto-edit permission; let it claim and work one ready bead: -```bash -agy --print --add-dir "$REPO" --dangerously-skip-permissions \ - "Claim one ready bead via br. Implement only it in this worktree. \ - Commit scoped. Write evidence to brain as userFacing. Do NOT close it — a judge will." -``` -**Checkpoint:** a scoped commit exists and an evidence artifact landed in `brain/`; the bead is implemented but still OPEN. - -### Phase 4: Independent verdict (judge — separate context) -Spawn the judge as an async subagent / second `agy --print` with a **fresh context and a read-mostly scope** (default permissions, no auto-approve): -```bash -agy --print --add-dir "$REPO" \ - "Validate bead <id> against its evidence artifact ONLY. You did not author it. \ - Emit PASS/WARN/FAIL to brain as a userFacing verdict. Do not edit code." -``` -On a split or false-FAIL, spawn a third **tie-break** subagent. Close the bead (`ao beads exec close <id>`) **only** on PASS. -**Checkpoint:** verdict artifact persisted by a *different* context than the author; bead closed only if PASS. - -### Phase 5: Persist + tick the loop -- Persist: scoped `git commit`/push for the repo; the brain artifact is the durable memory. -- Tick: AGY's native scheduled-task / slash-workflow is the recurring driver; otherwise drive externally with Claude `CronCreate` or a bushido timer calling `agy --print` (in-session, never `claude -p`). -**Checkpoint:** the loop can re-enter Phase 3 with the next ready bead; state is on the bus/artifact, not in a live session. - -## Output Specification - -- **Artifact directory:** write the machine-readable handoff to - `$REPO/.agents/evidence/agy-native/<bead-id>/`; keep the judge's source - verdict under `~/.gemini/antigravity-cli/brain/<conversation-id>/`. -- **Filename convention:** name the repo handoff `run-evidence.json` and the - brain verdict `<name>_verification.md` with its adjacent - `<name>_verification.md.metadata.json` sidecar. -- **Serialization/schema format:** `run-evidence.json` is one JSON object with - nonempty `bead_id`, distinct `author_context_id` and `judge_context_id`, - `verdict` (`PASS|WARN|FAIL`), absolute `evidence_path`, and `commit_sha`. -- **Validator command:** with `$REPO` and `$bead_id` set, validate the complete - handoff and the PASS-to-close invariant: - - ```bash - REPO="$REPO" bead_id="$bead_id" bash -euo pipefail <<'VALIDATE' - manifest="$REPO/.agents/evidence/agy-native/$bead_id/run-evidence.json" - test -s "$manifest" - jq -e --arg bead "$bead_id" ' - type == "object" and - ((.bead_id | type) == "string") and .bead_id == $bead and ($bead | length) > 0 and - ((.author_context_id | type) == "string") and (.author_context_id | length) > 0 and - ((.judge_context_id | type) == "string") and (.judge_context_id | length) > 0 and - .author_context_id != .judge_context_id and - (.verdict == "PASS" or .verdict == "WARN" or .verdict == "FAIL") and - ((.evidence_path | type) == "string") and (.evidence_path | length) > 0 and - ((.commit_sha | type) == "string") and (.commit_sha | length) > 0 - ' "$manifest" >/dev/null - - evidence_path="$(jq -er '.evidence_path' "$manifest")" - brain_root="$HOME/.gemini/antigravity-cli/brain/" - relative_path="${evidence_path#"$brain_root"}" - test "$relative_path" != "$evidence_path" - conversation_id="${relative_path%%/*}" - verdict_filename="${relative_path#*/}" - test -n "$conversation_id" && test "$verdict_filename" != "$relative_path" - case "$conversation_id" in .|..) exit 1 ;; esac - case "$verdict_filename" in - */*|_verification.md) exit 1 ;; - *_verification.md) ;; - *) exit 1 ;; - esac - - test -s "$evidence_path" && test -s "$evidence_path.metadata.json" - jq -e '.userFacing == true' "$evidence_path.metadata.json" >/dev/null - verdict="$(jq -er '.verdict' "$manifest")" - source_verdict="$(awk ' - /^Verdict: (PASS|WARN|FAIL)$/ { - count++ - value = substr($0, 10) - } - END { - if (count != 1) exit 1 - print value - } - ' "$evidence_path")" - [[ "$source_verdict" == "$verdict" ]] - git -C "$REPO" cat-file -e "$(jq -er '.commit_sha' "$manifest")^{commit}" - - beads_dir="$(ao beads dir)" - bead_json="$(BEADS_DIR="$beads_dir" br show "$bead_id" --json)" - bead_status="$(jq -er ' - if length == 1 and ((.[0].status | type) == "string") - then .[0].status else error("missing or ambiguous bead") end - ' <<<"$bead_json")" - case "$bead_status" in open|in_progress|blocked|closed) ;; *) exit 1 ;; esac - if [[ "$verdict" == PASS ]]; then - [[ "$bead_status" == closed ]] - else - [[ "$bead_status" != closed ]] - fi - VALIDATE - ``` -- **Downstream handoff:** give `run-evidence.json` to the verification membrane; - only a validated PASS/closed pair may release the next scheduled Phase 3 tick. - -## Quality Rubric - -- [ ] No `claude -p` anywhere; AGY workers driven by `agy --print` / `agy -i` (Rule 1). -- [ ] Author and judge ran in **distinct** contexts/conversations (Rule 2) — verifiable by two `conversation_id`s. -- [ ] Bead closed only against a persisted `userFacing` verdict artifact, not chat (Rule 3). -- [ ] Author and judge had non-overlapping `--add-dir` / worktree scopes (Rule 4). -- [ ] `dcg` BeforeTool hook still present in `~/.gemini/settings.json` (Rule 5). -- [ ] Author had auto-edit + tight scope; judge ran default (no auto-approve), read-mostly (Rule 6). -- [ ] Nothing written under `~/dev/agentops`; no agentops push (Rule 7). -- [ ] `agy plugin validate` passed and `agy plugin list` shows the plugin enabled; every mutation listed + a rollback recorded. - -## Examples - -- **Fallback tick when the Claude image is rate-limited:** import a plugin (`agy plugin import claude`), run Phase 3–4 on Gemini 3.5 Flash, persist, hand the next bead back to the Claude image. -- **Cross-vendor author!=judge:** author with `agy --print --model "Gemini 3.5 Flash (High)"`, judge with `agy --print --model "Claude Opus 4.6 (Thinking)"` — two vendors, one loop, no shared context. -- **Expose a new AgentOps skill to AGY (dev):** `agy plugin link ~/dev/agentops/skills/<name>` (or drop a portable `SKILL.md` in `~/.gemini/skills/<name>/`), then `agy plugin list` to confirm discovery; rollback `agy plugin uninstall <name>`. - -## Troubleshooting - -| Problem | Cause | Solution | -|---------|-------|----------| -| `agy plugin install` fails: "failed to read plugin.json" | target isn't a plugin dir / missing `plugin.json` | point at a dir containing `plugin.json`, or `name@marketplace`; for a bare skill use `~/.gemini/skills/` | -| New skill not discovered | wrong source path / disabled | `agy plugin list`; confirm `~/.gemini/skills/<name>/SKILL.md`, then `enable` | -| Edits not reflected | reviewing an installed copy, not the linked source | `uninstall`, then `agy plugin link <source>` | -| Headless run exits empty | `--print` timed out or no model reachable | raise `--print-timeout`; confirm `agy models`; check OAuth in `~/.gemini/settings.json` | -| Worker tried a destructive command | auto-approve under `--dangerously-skip-permissions` | the `dcg` BeforeTool hook should block it — confirm it's wired | -| Judge agreed with author too easily | same context reused (`-c`/`--continue`) | spawn a fresh conversation (no `--continue`); enforce read-mostly scope | - -## See Also / References - -- **[references/distribution-and-run-control.md](references/distribution-and-run-control.md)** — full plugin verb list, install-vs-link discipline, mutation protocol, the permission×output×scope matrix (AGY equivalents for retired gemini flags), and the brain evidence layout. -- Research input: `~/.agents/research/agy-native-harness-2026-06-06.md` (AGY primitives, official docs index, open questions). -- [`/agent-native`](../agent-native/SKILL.md) + [`/ntm`](../ntm/SKILL.md) — interactive AGY worker panes vs this skill's headless adapter paths; do not conflate their dispatch mechanics. -- Sibling AGY skills: `agy-rules-workflows` (goal/schedule loop law), `agy-mcp-plugins` (MCP servers + plugin packaging), `agy-headless-evidence` (agentapi sidecar + JSONL evidence). -- Sibling images / loop substrate: `ntm` (tmux swarms), `beads-br` (br tracker), `agent-mail` (coordination), `dcg` (destructive-command guard), `caam` (account lanes). -- Loop doctrine: control-plane LEARNINGS (author!=judge, evidence-gated close); memory `never claude -p for workers`; ACFS invoke-never-rebuild + fork-and-own doctrine. -- Official Antigravity docs: cli-overview, cli-plugins, subagents, hooks, ide-workflows, ide-rules (see research file for URLs). +Return evidence to the caller and stop. Installation, plugin mutation, and +recurring scheduling require separate explicit authorization. diff --git a/images/gemini/skills/automation-shape-routing/SKILL.md b/images/gemini/skills/automation-shape-routing/SKILL.md new file mode 100644 index 000000000..638d67260 --- /dev/null +++ b/images/gemini/skills/automation-shape-routing/SKILL.md @@ -0,0 +1,134 @@ +--- +name: automation-shape-routing +description: 'Front door for agent automation: choose inline, bounded fanout, reusable skill/workflow/gate, persistent agent-native workers, or explicit Gas City. Triggers: "build automation", "which orchestration shape", "should this use NTM".' +practices: +- hexagonal-architecture +- team-topologies +- pragmatic-programmer +hexagonal_role: supporting +consumes: +- task-intent +produces: +- automation-shape-verdict +context_rel: +- kind: supplier-to + with: skill-builder +- kind: supplier-to + with: workflow-builder +- kind: supplier-to + with: agent-native +- kind: supplier-to + with: using-gc +- kind: supplier-to + with: operationalize +skill_api_version: 1 +context: + window: inherit + intent: + mode: task + intel_scope: none +metadata: + capabilities: [automation_shape_routing] + effects: [] + canonical_status: canonical + disposition: keep_optional_adapter + tier: meta + dependencies: [] +output_contract: a one-line routing verdict naming the shape and deciding axis +--- + +# Automation Shape Routing + +Choose the smallest execution shape that preserves the required evidence and +control. This skill routes; it does not build or start a substrate. + +## Critical Constraints + +- **Route only; do not start a substrate. Why:** choosing an execution shape is + a judgment step, while launching NTM, Agent Mail, or Gas City changes runtime + state and requires separate operator authority. +- **Prefer the smallest shape that preserves evidence. Why:** persistence and + coordination add recovery and ownership costs that one-shot work cannot repay. +- **Partition write scopes before choosing concurrency. Why:** a larger worker + topology cannot make overlapping production writes safe. + +## Route in order + +1. **One deliverable?** Do it inline. Use a small in-session fresh-context + fanout only when independent perspectives are the product. Do not create a + reusable artifact for a one-off task. +2. **Reusable sequential procedure?** Use `skill-builder`. +3. **Must-never-regress constraint?** Route through `operationalize` to a gate. +4. **Fixed typed DAG, headless, no attach/steer?** Use `workflow-builder` only + where that runtime is explicitly selected and available. +5. **Persistent, attachable roles over AgentOps beads?** Use `agent-native`. + NTM is the pane adapter; workers execute whole loop skills, while Agent Mail + coordinates only multiple live actors. +6. **Durable city of quests with GC-native supervision/store?** Route to + `using-gc` only when the operator explicitly selects Gas City. GC is not an + automatic fallback or an `ao` runtime enum. + +## Deciding axes + +| Axis | Lightweight choice | Escalated choice | +|---|---|---| +| lifetime | current turn | persistent/attachable worker | +| topology | one writer or bounded fanout | durable role graph | +| control | no mid-run steering | observe/nudge/replace | +| output | one artifact | reusable skill/workflow/gate | +| store | repo bead chain | operator-selected GC quest store | +| contention | one writer | partition, then Agent Mail reservation | + +Parallelism buys independence, not guaranteed speed. Refuse persistent +orchestration for one-shot work, colliding write scopes, or a sequential chain +that has no exploitable concurrency. + +## Handoff + +Return exactly one of: + +- `inline` or `bounded-fanout` +- `skill-builder` +- `workflow-builder` +- `agent-native` with a named reason persistent panes help +- `using-gc` with explicit operator choice +- `operationalize:gate` + +Name the deciding axis and invoke the owner. Do not copy the delegated workflow +into this router. + +## Output Specification + +- **Artifact directory:** stdout only; this routing decision creates no file. +- **Filename convention:** none. Emit exactly one routing-verdict line. +- **Serialization/schema format:** `shape=<allowed-shape>; axis=<deciding-axis>; + owner=<owning-skill>` using one of the shapes listed under Handoff. +- **Owner mapping:** `inline` and `bounded-fanout` use `current-agent`; + `skill-builder`, `workflow-builder`, `agent-native`, and `using-gc` use the + same value for owner; `operationalize:gate` uses `operationalize`. +- **Validator command:** validate a captured `$verdict` as exactly one line + with the declared shape/owner mapping: + + ```bash + printf '%s\n' "$verdict" | awk ' + NR > 1 { extra = 1 } + { + valid = ($0 ~ /^shape=(inline|bounded-fanout); axis=[^;]+; owner=current-agent$/ || + $0 ~ /^shape=skill-builder; axis=[^;]+; owner=skill-builder$/ || + $0 ~ /^shape=workflow-builder; axis=[^;]+; owner=workflow-builder$/ || + $0 ~ /^shape=agent-native; axis=[^;]+; owner=agent-native$/ || + $0 ~ /^shape=using-gc; axis=[^;]+; owner=using-gc$/ || + $0 ~ /^shape=operationalize:gate; axis=[^;]+; owner=operationalize$/) + } + END { exit !(NR == 1 && !extra && valid) } + ' + ``` +- **Downstream handoff:** invoke the named owner only after returning the verdict; + `inline` remains in the current agent and `bounded-fanout` remains in-session. + +## Quality Rubric + +- [ ] The verdict names exactly one allowed shape and one deciding axis. +- [ ] Persistent or city-shaped routes cite the operator's explicit selection. +- [ ] Concurrent routes state that production write scopes do not overlap. +- [ ] The router delegates to the owner without copying or starting its workflow. diff --git a/images/gemini/skills/bootstrap/SKILL.md b/images/gemini/skills/bootstrap/SKILL.md index 3aa401875..003038085 100644 --- a/images/gemini/skills/bootstrap/SKILL.md +++ b/images/gemini/skills/bootstrap/SKILL.md @@ -1,8 +1,7 @@ --- name: bootstrap -description: 'Initialize AgentOps project files. Triggers: "initialize AgentOps", "bootstrap project files", "set up .agents scaffolding".' +description: 'Initialize minimal AgentOps documentation and verdict storage without taking over repository workflow.' practices: -- containers - hermetic-builds - code-complete hexagonal_role: driving-adapter @@ -21,235 +20,53 @@ context: mode: task intel_scope: none metadata: + capabilities: [bootstrap] + effects: [] + canonical_status: canonical + disposition: keep_specialist graph_root: true tier: session - dependencies: - - goals - - product - - doc - - shared - - scaffold -output_contract: .agents/ directory structure, GOALS.md, PRODUCT.md + dependencies: [] +output_contract: minimal project docs and .agentops/verdicts directory --- -# /bootstrap +# Bootstrap — minimal project setup -> **Quick Ref:** Product/operations layer around the `ao quick-start` core seed. Progressive — bare repos get the golden path first, existing repos fill gaps only. +Bootstrap fills only missing AgentOps entry documents and the default durable +verdict directory. It does not initialize Git, install hooks, create tracker +state, start runtimes, or impose a delivery workflow. -**YOU MUST EXECUTE THIS WORKFLOW. Do not just describe it.** +## Procedure -## Constraints +1. Inspect the target directory and report which canonical files already exist. +2. Ask the caller for missing product intent or goal content when it cannot be + inferred safely. +3. Create only missing, explicitly requested files. Never overwrite an existing + document. +4. Create `.agentops/verdicts/sha256/` when durable local verdict storage is + requested. +5. Validate filesystem existence and report created, skipped, and failed paths. +6. Stop. -- Preserve existing artifacts unless `--force` is explicit because bootstrap must be safe and idempotent in partially initialized repositories. -- Never install `br`, author runtime hooks, or invent a PROGRAM.md fallback on the user's behalf because those are operator-owned choices. -- Stop on a failed prerequisite and report the repair command; do not claim a downstream artifact was created when its producing step failed. +Typical documents are `PRODUCT.md`, `GOALS.md`, `AGENTS.md`, and a README section +that explains the one-pass loop. Repositories remain free to use their own Git, +CI, tracker, release, and deployment policies. -## Quick Start +## Non-goals -``` -/bootstrap -``` +- installing or invoking `ao`, `br`, `bd`, NTM, Agent Mail, or another runtime; +- creating `.git`, worktrees, branches, commits, hooks, or CI workflows; +- choosing work or claiming that repository setup is complete beyond the paths + actually inspected; +- running RPI automatically. -That is it. One command. Every step below is idempotent — existing artifacts are never overwritten. +## Output -## Absorbed triggers (routed here from retired skills) +Return target path, requested files, created files, existing files left intact, +failed writes, and validation observations. Do not include a next action. -- **`session-bootstrap` / session-start context** — run `ao session bootstrap` for the - universal orientation report, then `ao lookup --query "<topic>"` for decay-ranked - prior context. (Previously routed via the retired `/inject`.) -- **`using-agentops` / workflow tour** — read - [docs/architecture/operating-loop.md](../../docs/architecture/operating-loop.md) - (the primary navigation). There is no update skill — to refresh installed skills, re-run the install one-liner: - `bash <(curl -fsSL https://raw.githubusercontent.com/boshu2/agentops/main/scripts/install.sh)`. +## References -## External Tools - -- **ao** (optional) — AgentOps CLI. Required only for optional hook activation (Step 6). Bootstrap skips hooks gracefully when missing. -- **br** (optional, recommended) — beads_rust CLI (local-first issue tracking). Bootstrap probes for `br` in Step 0.5 and, when missing, recommends installing it. Bootstrap never installs `br` on the user's behalf. -- **bd** (optional) — the beads CLI. AgentOps supports **both** trackers; a project may run on `bd` instead of `br`. Bootstrap probes for `bd` in Step 0.5 and, when neither tracker is present, points the user at `scripts/install-bd.sh` with a copy-paste command. Bootstrap never installs `bd` on the user's behalf. - -## Flags - -| Flag | Effect | -|------|--------| -| `--dry-run` | Report what would be created without doing anything | -| `--force` | Recreate artifacts even if they already exist | - -## Execution Steps - -**Checkpoint:** after repo-state detection, verify the planned create/skip set before invoking any mutating step; after Step 5, verify every reported status against the filesystem. - -### Step 0: Detect Repo State - -```bash -git rev-parse --is-inside-work-tree >/dev/null 2>&1 || { echo "NOT_A_GIT_REPO"; exit 1; } -HAS_GOALS=$([[ -f GOALS.md ]] && echo true || echo false) -HAS_PRODUCT=$([[ -f PRODUCT.md ]] && echo true || echo false) -HAS_README=$([[ -f README.md ]] && echo true || echo false) -HAS_PROGRAM=$([[ -f PROGRAM.md || -f AUTODEV.md ]] && echo true || echo false) -HAS_AGENTS=$([[ -d .agents ]] && echo true || echo false) -HAS_HOOKS=$(grep -q "agentops" .claude/settings.json 2>/dev/null && echo true || echo false) -HAS_AO=$(command -v ao >/dev/null && echo true || echo false) -HAS_BR=$(command -v br >/dev/null && echo true || echo false) -HAS_BD=$(command -v bd >/dev/null && echo true || echo false) -``` - -Classify the repo: - -| State | Condition | -|-------|-----------| -| **bare** | No GOALS.md, no PRODUCT.md, no .agents/ | -| **partial** | Some artifacts present, some missing | -| **complete** | GOALS.md, PRODUCT.md, README.md, PROGRAM.md/AUTODEV.md, and .agents/ present | - -If `--dry-run` is set: report the state and what would be created, including whether `br` would be recommended (when `HAS_BR` is false), then stop. Do not proceed to Steps 1-6. - -If the repo is **complete** and `--force` is not set: report "Repo is fully bootstrapped. Nothing to do." and stop. - -### Step 0.5: Recommend br - -If `HAS_BR` is true: skip. Report "br: present." - -If `HAS_BR` is false: report **"br: not installed (recommended). Install beads_rust to get local-first issue tracking."** and continue. Bootstrap does NOT run the installer — `br` is optional, the user decides. - -AgentOps supports **both** trackers, so if the project runs on `bd` instead: - -If `HAS_BD` is true: skip. Report "bd: present." - -If `HAS_BD` is false **and** `HAS_BR` is false: report **"no tracker installed. For `bd`, install with: `bash scripts/install-bd.sh`"** and continue. Bootstrap does NOT run the installer — the tracker is optional, the user decides. If `scripts/install-bd.sh` is absent at the repo root, drop the install hint and report "bd: not installed. See https://github.com/steveyegge/beads". - -### Step 1: GOALS.md - -If `HAS_GOALS` is false (or `--force` is set): - -Run the goals skill to initialize GOALS.md interactively: - -``` -Skill(skill="goals", args="init") -``` - -If `HAS_GOALS` is true and `--force` is not set: skip. Report "GOALS.md exists -- skipped." - -### Step 2: PRODUCT.md - -If `HAS_PRODUCT` is false (or `--force` is set): - -Run the product skill to generate PRODUCT.md interactively: - -``` -Skill(skill="product") -``` - -If `HAS_PRODUCT` is true and `--force` is not set: skip. Report "PRODUCT.md exists -- skipped." - -### Step 3: README.md - -If `HAS_README` is false (or `--force` is set) AND PRODUCT.md now exists: - -Run the doc skill in README mode to generate README.md: - -``` -Skill(skill="doc", args="--mode=readme") -``` - -If `HAS_README` is true and `--force` is not set: skip. Report "README.md exists -- skipped." - -If PRODUCT.md does not exist (Step 2 was skipped or failed): skip. Report "README.md skipped -- PRODUCT.md required first." - -### Step 4: Core Seed and .agents/ Structure - -Apply the idempotent core-seed procedure in [references/core-seed.md](references/core-seed.md): prefer `ao quick-start --no-beads`, use the documented minimal fallback only when `ao` is unavailable, and truthfully report create/skip/repair status. - -### Step 5: PROGRAM.md / AUTODEV.md - -If `HAS_PROGRAM` is false (or `--force` is set): - -Use the existing autodev CLI path: - -```bash -ao autodev init "your current objective" -``` - -If `ao` is unavailable: do not create a placeholder. Report "PROGRAM.md skipped -- install ao, then run: `ao autodev init \"your current objective\"`." - -If `HAS_PROGRAM` is true and `--force` is not set: skip. Report "PROGRAM.md/AUTODEV.md exists -- skipped." - -### Step 6: Optional Hook Activation - -Do not activate runtime agent hooks. AgentOps 3.0 is runtime-hookless: -`ao quick-start`, execution packets, explicit validation, and knowledge -compounding deliver first value without Claude/Codex runtime hooks. Routine -release authority is the local cockpit gate (`ao gate check` plus the installed -Git pre-push/pawl proof path); GitHub Actions are PR/tag/manual backstop -telemetry. There is no `ao` command or flag that installs runtime hooks — -hooks were removed from the CLI. - -If the user explicitly requests hooks, they are opt-in and author-it-yourself: -point them at the `hooks-authoring` skill, which scaffolds project-local hooks -into `.claude/settings.json`. Bootstrap itself never writes hooks. - -If hooks were not explicitly requested: skip. Report "Runtime hooks optional -- skipped. AgentOps 3.0 is runtime-hookless; routine release authority is the local cockpit gate. To author your own, use the `hooks-authoring` skill." - -If `HAS_HOOKS` is true: report "Hooks already present in .claude/settings.json -- left untouched." - -### Step 7: Report - -Output a summary table: - -``` -Bootstrap complete. - -| Artifact | Status | -|---------------|---------| -| GOALS.md | created / skipped / failed | -| PRODUCT.md | created / skipped / failed | -| README.md | created / skipped / failed | -| PROGRAM.md | created / skipped / failed | -| .agents/ | created / skipped / failed | -| Hooks | optional / activated / skipped / failed | -| br | present / recommended (not installed) | - -Repo is now AgentOps-ready. Next: run the operating loop — /rpi "your first goal" -``` - -## Output Specification - -- **Path:** repository-root `GOALS.md`, `PRODUCT.md`, `README.md`, `PROGRAM.md`/`AUTODEV.md`, and `.agents/`; existing paths remain untouched unless `--force` is explicit. -- **Filename:** use those canonical filenames and `.agents/AGENTS.md`; do not create alternate or placeholder filenames. -- **Format:** Markdown for root documents and `.agents/AGENTS.md`; Step 7 emits the documented status table on stdout. -- **Exit code:** validate with filesystem existence checks plus `ao quick-start --dry-run`; any failed producer or validation command marks that row `failed` and prevents an AgentOps-ready claim. -- **Downstream handoff:** consumed by the operating loop after every required row is `created` or `skipped` and any optional/recommended row is labeled truthfully. - -## Quality Checklist - -- Every Step 7 status matches the filesystem and the command result that produced it. -- Existing artifacts were preserved unless the operator explicitly requested `--force`. -- Missing optional tools yield recommendations or repair commands, never silent installation or fabricated artifacts. - -## Examples - -See [references/examples.md](references/examples.md) for bare, partial, and dry-run cases. - -## Troubleshooting - -| Problem | Cause | Solution | -|---------|-------|---------| -| "Not a git repo" | No .git directory | Run `git init` first | -| Goals skill fails | No project context | Provide a one-line project description when prompted | -| Product skill fails | No goals defined | Run `/goals init` manually first, then re-run `/bootstrap` | -| Hooks not activating | ao CLI not installed | Install: `brew tap boshu2/agentops https://github.com/boshu2/homebrew-agentops && brew install agentops` | -| br not installed | Recommended but optional | Install beads_rust (`br`) if you want issue tracking; otherwise ignore | -| Want to start over | Existing artifacts blocking | Use `--force` to recreate all artifacts | - -## See Also - -- [goals](../goals/SKILL.md) -- Fitness specification and directive management -- [product](../product/SKILL.md) -- Product definition generation -- [doc](../doc/SKILL.md) -- README generation (`--mode=readme`) + repo docs -- [status](../status/SKILL.md) -- New user onboarding (lighter than bootstrap) -- [core seed procedure](references/core-seed.md) -- idempotent `.agents/` creation and fallback -- [examples](references/examples.md) -- bare, partial, and dry-run behavior -- [related operator runbooks](references/related-runbooks.md) -- host-hygiene runbooks (PATH rationalization, etc.) - -## Reference Documents - -- [references/bootstrap.feature](references/bootstrap.feature) — Executable spec: bare repo gets golden path, existing repo fills gaps only, idempotent never-overwrite (soc-qk4b) +- [Goals](../goals/SKILL.md) +- [Product](../product/SKILL.md) +- [Documentation](../doc/SKILL.md) +- [Examples](references/examples.md) diff --git a/images/gemini/skills/cass/SKILL.md b/images/gemini/skills/cass/SKILL.md index 29b403f93..65622111f 100644 --- a/images/gemini/skills/cass/SKILL.md +++ b/images/gemini/skills/cass/SKILL.md @@ -6,7 +6,15 @@ practices: skill_api_version: 1 user-invocable: false hexagonal_role: supporting +consumes: [] +produces: [] +context_rel: [] metadata: + dependencies: [] + capabilities: [cass] + effects: [] + canonical_status: canonical + disposition: keep_specialist tier: execution external_dependencies: - "cass binary (>=0.3.6 recommended; some commands require HEAD \u2014 see Version Pinning)" diff --git a/images/gemini/skills/cc-hooks/SKILL.md b/images/gemini/skills/cc-hooks/SKILL.md index 3a970a3b1..621e10490 100644 --- a/images/gemini/skills/cc-hooks/SKILL.md +++ b/images/gemini/skills/cc-hooks/SKILL.md @@ -3,7 +3,15 @@ name: cc-hooks user-invocable: false skill_api_version: 1 hexagonal_role: supporting +consumes: [] +produces: [] +context_rel: [] metadata: + dependencies: [] + capabilities: [cc_hooks] + effects: [] + canonical_status: canonical + disposition: keep_specialist tier: execution description: 'Configure Claude Code hooks (PreToolUse, PostToolUse, Stop, Notification) — user-side, opt-in per host (AgentOps 3.0 ships none). Triggers: "cc-hooks", "cc hooks", "configure claude code hooks pretooluse".' practices: @@ -216,33 +224,10 @@ claude --debug # Hook execution details - Blocking and allow paths use the documented exit code and output channel without leaking context. - The hook is reversible, narrowly scoped, recursion-safe, and clearly labeled as opt-in host policy. -## Absorbed Skills (skill-prune phase 2 fold-ins) - -This skill is the fold target for four retired Claude Code operator skills. Their -use-cases route here: - -- **cc-cron-ticks** — scheduling autonomous in-session flywheel ticks with Claude - Code cron routines. Use Claude Code scheduled tasks (cron routines) to fire a - recurring tick prompt (e.g. an evolve tick or a bead-queue pull); pair each - tick with a Stop hook that verifies evidence landed before the session ends. -- **cc-loop-driver** — running a Claude-native control-plane tick loop with worker - and separate-validator subagents. One tick = claim a bead, dispatch a worker - subagent, then a SEPARATE validator subagent grades the evidence; hooks enforce - the gate (PreToolUse blocks out-of-scope writes, Stop blocks close-without-evidence). -- **cc-subagents** — dispatching scoped Claude Code subagents with worktrees, roles, - tools, memory, and evidence gates. Give each subagent an explicit role prompt, a - tool allowlist, and a write scope; never let two subagents share a write surface. -- **cc-worktree-isolation** — isolating parallel Claude Code workers in - separate git worktrees to prevent file collisions. - `git worktree add <dir> -b <branch>` per - worker; workers commit only in their own worktree; the orchestrator merges - branches sequentially. File collisions are the #1 swarm failure mode. - ## References - [HOOK-EVENTS.md](references/HOOK-EVENTS.md) - All events with full schemas - [DCG-RCH.md](references/DCG-RCH.md) - Production examples (dcg, rch) -- [SKILL-FIRST-COORDINATION-GUARD.md](references/SKILL-FIRST-COORDINATION-GUARD.md) - Opt-in coordination skill-first guard + context-budget doctrine - [INSTALLED-SKILL-EDIT-GUARD.md](references/INSTALLED-SKILL-EDIT-GUARD.md) - Opt-in guard routing installed-skill edits to repo skills/ (keystone) - [GUARDRAIL-VALUE-PROOF.md](references/GUARDRAIL-VALUE-PROOF.md) - Pre-registered value-proof methodology + per-fire telemetry contract (ADR-0002 l.58) - [PATTERNS.md](references/PATTERNS.md) - Auto-format, logging, notifications diff --git a/images/gemini/skills/codebase-recon/SKILL.md b/images/gemini/skills/codebase-recon/SKILL.md new file mode 100644 index 000000000..eac9b3b83 --- /dev/null +++ b/images/gemini/skills/codebase-recon/SKILL.md @@ -0,0 +1,118 @@ +--- +name: codebase-recon +description: 'Reconstruct a repository as cited entry-to-test flows, bounded claims, and a reusable baseline or verified delta. Triggers: "build a repository mental model", "trace this codebase", "refresh the prior recon".' +practices: +- legacy-code-seams +- ddd-bounded-context +- design-by-contract +hexagonal_role: supporting +consumes: +- repo-context +- existing-docs +produces: +- codebase-recon.v1 +- evidence-bounded-recon-report +context_rel: +- kind: customer-of + with: research +- kind: customer-of + with: validate +- kind: customer-of + with: doc +skill_api_version: 1 +user-invocable: true +context: + window: fork + intent: + mode: task + sections: + exclude: + - HISTORY + intel_scope: topic +metadata: + capabilities: [codebase_recon] + effects: [] + canonical_status: canonical + disposition: keep_specialist + tier: execution + dependencies: [] +output_contract: codebase-recon.v1 JSON validated by skills/codebase-recon/scripts/validate-output.sh with a companion cited report +--- + +# Codebase Recon + +Build a reusable, falsifiable model of a repository. This skill reports what +the tree and executable probes support; it does not edit code or issue a final +PASS/WARN/FAIL verdict. + +## Constraints + +- To prevent a floating recon, record the exact repository commit and local + source-of-truth precedence. +- Because confidence is not evidence, type every material claim and cite each + fact and inference. +- To preserve traceability, prefer a verified delta when a prior pack exists + instead of rewriting unchanged evidence as fresh discovery. + +## Workflow + +1. Record the current commit and the repository's local source-of-truth + precedence. Search for a prior recon pack before starting. +2. If no prior pack exists, use `baseline` mode. If one exists, verify its + still-valid claims against the current commit and use `delta` mode. Preserve + valid evidence by reference and describe only changed paths and synthesis. +3. Trace representative paths from entry point to domain logic, integration + boundary, and test. Prefer a few complete flows over a broad file inventory. +4. Keep four views distinct in the report: mental model, bounded audit, pattern + evidence, and synthesis. Label each claim `fact`, `inference`, or `unknown`, + assign confidence, and cite evidence for facts and inferences. +5. List inspected and uninspected scope. Write the JSON manifest and companion + report, then run the validator. Missing evidence and hidden coverage gaps are + contract failures, not prose caveats. + +## Output Specification + +- **Artifact directory:** `.agents/recon/<run-id>/` +- **Filename convention:** `codebase-recon.json` with companion report + `codebase-recon.md` in the same directory. +- **Format:** `codebase-recon.v1` JSON manifest plus an evidence-cited Markdown + report covering the same commit, mode, flows, claims, and scope boundaries. +- **Validation command:** `skills/codebase-recon/scripts/validate-output.sh <codebase-recon.json>` + validates the machine-readable manifest; the cited Markdown report remains + its human-readable companion. +- **Downstream handoff:** pass both validated artifact paths to the requesting + research, planning, review, or documentation workflow; the consumer owns any + decision or code-change plan. + +Baseline manifests carry at least one complete entry-to-test flow. Delta +manifests name an existing prior recon, prove `baseline_verified: true`, and +describe at least one changed path. Every manifest lists both inspected and +uninspected scope. + +The validator is the machine boundary: + +```bash +skills/codebase-recon/scripts/validate-output.sh <recon.json> +``` + +Evidence entries are existing file paths, optionally followed by a line number. +Delta manifests require an existing prior pack, `baseline_verified: true`, and +at least one described change. + +Executable behavior: +[references/codebase-recon.feature](references/codebase-recon.feature). + +## Quality + +- Every fact and inference resolves to existing evidence; unknowns remain + visibly typed and never masquerade as established behavior. +- Representative flows reach entry, domain, integration, and test surfaces, + while inspected and uninspected scope stay explicit. +- The named validator passes before the JSON manifest and companion report are + handed to a downstream consumer. + +## Do not + +- Regenerate a full replacement report when a verified delta is possible. +- Present an inference as fact or omit uninspected scope. +- Turn the recon artifact into a completion verdict or a code-change plan. diff --git a/images/gemini/skills/codex-exec/SKILL.md b/images/gemini/skills/codex-exec/SKILL.md new file mode 100644 index 000000000..cb62b01db --- /dev/null +++ b/images/gemini/skills/codex-exec/SKILL.md @@ -0,0 +1,65 @@ +--- +name: codex-exec +description: 'Run one caller-supplied Codex worker or validator command non-interactively and capture evidence.' +skill_api_version: 1 +user-invocable: false +hexagonal_role: driving-adapter +practices: +- pragmatic-programmer +consumes: [] +produces: +- codex-run-output +context_rel: +- kind: supplier-to + with: validate +context: + window: inherit + intent: + mode: none + sections: + exclude: + - HISTORY + intel_scope: none +metadata: + capabilities: [codex_exec] + effects: [] + canonical_status: canonical + disposition: keep_optional_adapter + tier: orchestration + dependencies: [] + stability: stable + triggers: + - codex exec + - spawn a codex worker +--- +# Codex Exec — one-shot runtime adapter + +Run exactly one caller-supplied Codex prompt and capture its result. This skill +does not choose work, retry failures, validate by itself, or control continuation. + +## Procedure + +1. Confirm `codex login status` for the intended profile. +2. Set the working root explicitly with `-C`. +3. Match the sandbox to the requested effects: read-only for offline review, + workspace-write for authorized edits, and broader access only when the caller + explicitly requires network or external effects. +4. Pipe the prompt to stdin (or close stdin) in non-TTY execution so the process + cannot wait indefinitely for input. +5. Capture the final response with `-o`, JSONL, or an output schema. +6. Report the process exit status and captured artifact, then stop. + +A nonzero process exit is runtime evidence, not a semantic verdict. The caller +decides whether to launch another invocation. + +## Example + +```bash +printf '%s\n' "$PROMPT" | codex exec -C "$WORKSPACE" -s read-only \ + -o "$OUTPUT" - +``` + +For a validator, the prompt must name the acceptance digest, exact subject +manifest digest, author context ID, evidence, and required checked/not-checked +report. The validator context ID must be distinct from the author's before a +`PASS` verdict is possible. diff --git a/images/gemini/skills/converter/SKILL.md b/images/gemini/skills/converter/SKILL.md new file mode 100644 index 000000000..9a3228a26 --- /dev/null +++ b/images/gemini/skills/converter/SKILL.md @@ -0,0 +1,177 @@ +--- +name: converter +description: 'Convert AgentOps skill formats. Triggers: "converter", "convert agentops skill formats.", "converter skill".' +practices: +- refactoring +- design-patterns +hexagonal_role: driven-adapter +consumes: [] +produces: +- converted-skill +context_rel: [] +skill_api_version: 1 +context: + window: isolated + intent: + mode: none + sections: + exclude: + - HISTORY + - INTEL + - TASK + intel_scope: none +metadata: + capabilities: [converter] + effects: [] + canonical_status: canonical + disposition: keep_specialist + tier: cross-vendor + dependencies: [] +output_contract: converted skill files for target platform +--- +# /converter -- Cross-Platform Skill Converter + +Parse AgentOps skills into a universal SkillBundle format, then convert to target agent platforms. + +## Constraints + +- Treat the canonical source skill as read-only because conversion must not mutate the contract it is translating. +- Clean-write only the explicit target directory to prevent stale resources from surviving a conversion or unrelated paths from being deleted. +- Fail when copied-resource parity or target-format validation fails because a partial bundle is not a usable conversion. + +## Quick Start + +```bash +/converter skills/council codex # Convert council skill to Codex format +/converter skills/validate cursor # Convert validate skill to Cursor format +/converter --all codex # Convert all skills to Codex +``` + +## Pipeline + +The converter runs a three-stage pipeline: + +``` +parse --> convert --> write +``` + +### Stage 1: Parse + +Read the source skill directory and produce a SkillBundle: + +- Extract YAML frontmatter from SKILL.md (between `---` markers) +- Collect the markdown body (everything after the closing `---`) +- Enumerate all files in `references/` and `scripts/` +- Assemble into a SkillBundle (see `references/skill-bundle-schema.md`) + +### Stage 2: Convert + +Transform the SkillBundle into the target platform's format: + +| Target | Output Format | Status | +|--------|---------------|--------| +| `codex` | Codex SKILL.md + prompt.md | Implemented | +| `cursor` | Cursor .mdc rule + optional mcp.json | Implemented | + +The Codex adapter produces a `SKILL.md` with YAML frontmatter (`name`, `description`) plus rewritten body content and a `prompt.md` (Codex prompt referencing the skill). Default mode is **modular**: reference docs, scripts, and resources are copied as files and `SKILL.md` includes a local resource index instead of inlining everything. Optional **inline** mode preserves the older behavior by appending inlined references and script code blocks. Codex output rewrites known slash-skill references (for example `/plan`) to dollar-skill syntax (`$plan`), replaces Claude-specific paths/labels (including `~/.claude/`, `$HOME/.claude/`, and `/.claude/` path variants), normalizes common mixed-runtime terms (for example `Claude Native Teams`, `claude-native-teams`, and `Claude session/runtime`) to Codex-native phrasing, and rewrites Claude-only primitive labels to runtime-neutral wording. It preserves current flat `ao` CLI commands from the source skill rather than reintroducing deprecated namespace forms. It also deduplicates repeated "In Codex" runtime headings after rewrite while preserving section content. It preserves non-generated resource files/directories from the source skill (for example `templates/`, `assets/`, `schemas/`, `examples/`, `agents/`) and enforces passthrough parity (missing copied resources fail conversion). Descriptions are truncated to 1024 chars at a word boundary if needed. + +The Cursor adapter produces a `<name>.mdc` rule file with YAML frontmatter (`description`, `globs`, `alwaysApply: false`) and body content. References are inlined into the body, scripts are included as code blocks. Output is budget-fitted to 100KB max -- references are omitted largest-first if the total exceeds the limit. If the skill references MCP servers, a `mcp.json` stub is also generated. + +### Stage 3: Write + +Write the converted output to disk. + +- **Default output directory:** `.agents/converter/<target>/<skill-name>/` +- **Write semantics:** Clean-write. The target directory is deleted before writing. No merge with existing content. + +## CLI Usage + +```bash +# Convert a single skill +bash skills/converter/scripts/convert.sh <skill-dir> <target> [output-dir] +bash skills/converter/scripts/convert.sh --codex-layout inline <skill-dir> codex [output-dir] + +# Convert all skills +bash skills/converter/scripts/convert.sh --all <target> [output-dir] +``` + +### Arguments + +| Argument | Required | Description | +|----------|----------|-------------| +| `skill-dir` | Yes (or `--all`) | Path to skill directory (e.g. `skills/council`) | +| `target` | Yes | Target platform: `codex`, `cursor`, or `test` | +| `output-dir` | No | Override output location. Default: `.agents/converter/<target>/<skill-name>/` | +| `--all` | No | Convert all skills in `skills/` directory | +| `--codex-layout` | No | Codex-only layout mode: `modular` (default) or `inline` (legacy inlined refs/scripts) | + +## Supported Targets + +- **codex** -- Convert to OpenAI Codex format (`SKILL.md` + `prompt.md`) with codex-native rewrites (slash-to-dollar skills, `.claude` path variants to `.codex`, mixed-runtime term normalization to Codex phrasing, Claude primitive label neutralization, duplicate runtime-heading cleanup, and flat `ao` CLI preservation). Default is modular output with copied resources and a `SKILL.md` local-resource index; pass `--codex-layout inline` for legacy inlined refs/scripts. Converter enforces passthrough parity so missing copied resources fail fast. Output: `<dir>/SKILL.md`, `<dir>/prompt.md`, and copied resources. +- **cursor** -- Convert to Cursor rules format (`.mdc` rule file + optional `mcp.json`). Output: `<dir>/<name>.mdc` and optionally `<dir>/mcp.json`. +- **test** -- Emit the raw SkillBundle as structured markdown. Useful for debugging the parse stage. + +## Extending + +To add a new target platform: + +1. Add a conversion function to `scripts/convert.sh` (pattern: `convert_<target>`) +2. Update the target table above +3. Add reference docs to `references/` if the target format needs documentation + +## Examples + +### Converting a single skill to Codex format + +**User says:** `/converter skills/council codex` + +**What happens:** +1. The converter parses `skills/council/SKILL.md` frontmatter, markdown body, and any `references/` and `scripts/` files into a SkillBundle. +2. The Codex adapter transforms the bundle into a `SKILL.md` (body + inlined references + scripts as code blocks) and a `prompt.md` (Codex prompt referencing the skill). +3. Output is written to `.agents/converter/codex/council/`. + +**Result:** A Codex-compatible skill package ready to use with OpenAI Codex CLI. + +### Batch-converting all skills to Cursor rules + +**User says:** `/converter --all cursor` + +**What happens:** +1. The converter scans every directory under `skills/` and parses each into a SkillBundle. +2. The Cursor adapter transforms each bundle into a `.mdc` rule file with YAML frontmatter and body content, budget-fitted to 100KB max. Skills referencing MCP servers also get a `mcp.json` stub. +3. Each skill's output is written to `.agents/converter/cursor/<skill-name>/`. + +**Result:** All skills are available as Cursor rules, ready to drop into a `.cursor/rules/` directory. + +## Troubleshooting + +| Problem | Cause | Solution | +|---------|-------|----------| +| `parse error: no frontmatter found` | SKILL.md is missing the `---` delimited YAML frontmatter block | Add frontmatter with at least `name:` and `description:` fields, or run `/heal-skill --fix` on the skill first | +| Cursor `.mdc` output is missing references | Total bundle size exceeded the 100KB budget limit | The converter omits references largest-first to fit the budget. Split large reference files or move non-essential content to external docs | +| Output directory already has old files | Previous conversion artifacts remain | This is expected -- the converter clean-writes by deleting the target directory before writing. If old files persist, manually delete `.agents/converter/<target>/<skill>/` | +| `--all` skips a skill directory | The directory has no `SKILL.md` file | Ensure each skill directory contains a valid `SKILL.md`. Run `/heal-skill` to detect empty directories | +| Codex `prompt.md` description is truncated | The skill description exceeds 1024 characters | This is by design. The converter truncates at a word boundary to fit Codex limits. Shorten the description in SKILL.md frontmatter if the truncation point is awkward | +| Conversion fails with passthrough parity check | A resource entry from source skill wasn't copied to output | Ensure source entries are readable and copyable (including nested files). Re-run conversion; failure is intentional to prevent drift between `skills/` and converted output | + +## Output Specification + +- **Path:** `.agents/converter/<target>/<skill-name>/` by default, or the exact caller-supplied output directory. +- **Filename:** Codex emits `SKILL.md`, `prompt.md`, and copied resources; Cursor emits `<skill-name>.mdc` and optional `mcp.json`; `test` emits the raw bundle representation. +- **Format:** target-valid UTF-8 text with required frontmatter, rewritten runtime references, and byte-present passthrough resources; Cursor output remains within 100KB. +- **Exit code:** run `bash skills/converter/scripts/convert.sh <skill-dir> <target> <output-dir>` and require zero; treat parse, budget, write, or passthrough-parity failure as nonzero and incomplete. +- **Downstream handoff:** report the source skill, target, output directory, layout, omitted Cursor references if any, and validation result to the installer or projection gate. + +## Quality Checklist + +- The source tree is unchanged and the output tree contains no files left over from an earlier conversion. +- Every required target file parses with its target frontmatter/schema and every eligible source resource is present. +- Runtime-specific rewrites preserve the source meaning without reintroducing deprecated command forms or foreign-runtime paths. + +## References + +- `references/skill-bundle-schema.md` -- SkillBundle interchange format specification + +## Reference Documents + +- [references/skill-bundle-schema.md](references/skill-bundle-schema.md) diff --git a/images/gemini/skills/council/SKILL.md b/images/gemini/skills/council/SKILL.md index 4aab884df..d6c334c3b 100644 --- a/images/gemini/skills/council/SKILL.md +++ b/images/gemini/skills/council/SKILL.md @@ -1,106 +1,39 @@ --- name: council -spine: true -description: 'Run multi-judge consensus. Use when: an irreversible or high-stakes decision needs independent judges before committing — architecture forks, one-way doors, scoring options.' -practices: -- llm-eval-harness -- ai-assisted-dev -- design-by-contract +description: 'Collect independent perspectives for an explicitly high-stakes or contested judgment. Triggers: "council", "multi-judge review", "independent perspectives".' +practices: [llm-eval-harness, design-by-contract] hexagonal_role: domain -consumes: -- standards -produces: -- result.json -- verdict.json -context_rel: -- kind: shared-kernel - with: standards +consumes: [explicit-question, evidence] +produces: [council-report.v1] +context_rel: [] skill_api_version: 1 -context: - window: isolated - intent: - mode: task - sections: - exclude: - - HISTORY - intel_scope: full +user-invocable: true metadata: graph_root: true tier: judgment - dependencies: - - standards - - agy-native - - pawl-review - replaces: judge -output_contract: skills/council/schemas/verdict.json + dependencies: [] + capabilities: [collect_independent_judgments, synthesize_disagreement] + effects: [write_advisory_council_report] + canonical_status: canonical + disposition: keep_strategy +output_contract: council-report.v1 --- -# council — moved to Mount Olympus (2026-06-10) +# Council -This skill encodes independent-verdict machinery and now lives with the outer -gate product. Canonical: `~/dev/mt-olympus/.claude/skills/council/SKILL.md` — -read and follow that file. This stub preserves fleet routing until the -using-agentops catalog closer updates the registry (skill-prune Lane A, -evidence/skill-prune-recon.md). +Council is an optional judgment strategy, not a lifecycle or delivery gate. Use +it when one fresh validator is insufficient for a named irreversible, +high-blast-radius, or genuinely contested decision. -## Constraints +1. Freeze one question, acceptance surface, evidence set, and subject digest. +2. Give each judge an independent context and the same bounded packet. +3. Require each judge to cite evidence, disclose omissions, and return its own + judgment without seeing other answers first. +4. Synthesize agreement and disagreement without majority laundering. Preserve + minority evidence and unresolved assumptions. +5. Write `council-report.v1` and return it to the caller. -- Read the Mount Olympus canonical body before running a panel because this repository copy is a routing stub, not the executable procedure. -- Reserve council for irreversible decisions; use `validate` for per-slice acceptance so one artifact is not double-gated by overlapping authorities. -- Keep author and judges distinct and judge lanes read-only because consensus is evidence only when verdicts are independent of production and mutation. - -> **Narrow-waist obligations (must hold at the canonical body):** council is the S5 membrane for irreversible **decisions**, not slice-acceptance closes — `/validate` owns the per-slice acceptance verdict, so do not double-gate. Its verdict binds to the slice's BDD/ATDD acceptance test; author ≠ judge; and every REFUTE feeds a lesson into the next loop's `/premortem` checks (S6). See the [narrow-waist micro-cycle](../../docs/architecture/operating-loop.md#the-narrow-waist-micro-cycle-canonical--every-loop-skill-cites-this). - -## Absorbed trigger surfaces (skill-prune phase 2) - -Council also fires for the use-cases of two folded-in skills: - -- **multi-model-triangulation** — cross-validate decisions using multiple AI - models (Codex, Gemini, Grok). Use when asked to "get a second opinion", - when evaluating competing approaches, or for high-stakes decisions: run the - question through council's independent judges instead of a single model. -- **cross-vendor-trust-gate** — run the skill-factory final trust gate: - operate `trust-gate.sh`, read `skill.trust.json` (trust_level + trust_score), - and enforce `--require-cross` so cross-vendor parity gates the verdict. - Canonical body: - `~/dev/mt-olympus/.claude/skills/cross-vendor-trust-gate/SKILL.md`. - -## Mixed-model (cross-family) panel - -When the decision wants a **mixed-model / cross-family** panel rather than -single-model judges, use `agent-native` for durable role-shaped lanes over NTM -or an in-session variant (`codex exec` plus the available native agent surface). -For a landing oracle, each fresh read-only lane is owned by `pawl-review` and -the deterministic `ao pawl` membrane owns the panel decision. `/discovery` -routes one-way-door idea choices through `dueling-idea-genies` before planning. - -## Examples - -- `/council should we swap the policy engine to Cedar?` — runs at the canonical - location; this stub forwards. Read - `~/dev/mt-olympus/.claude/skills/council/SKILL.md`. - -## Troubleshooting - -- **Skill seems empty / missing scripts:** the body moved to Mount Olympus - (2026-06-10). Use the canonical path above; this stub exists only to keep - fleet routing alive until the catalog closer updates the registry. - -## Output Specification - -- **Path:** the run's declared evidence directory, containing both the panel aggregate and its binding decision handoff. -- **Filename:** `result.json` for judge results and `verdict.json` for the council verdict. -- **Format:** JSON; `verdict.json` must validate against `skills/council/schemas/verdict.json` and retain each concrete finding's location, recommendation, rationale, and reference. -- **Exit code:** validate with `python3 -m jsonschema -i <evidence-dir>/verdict.json skills/council/schemas/verdict.json`; missing judges, author overlap, invalid JSON, or schema failure is nonzero and not consensus. -- **Downstream handoff:** pass the independent results and validated verdict to `pawl-review`/the verification membrane; council does not itself authorize landing. - -## Quality Checklist - -- Every counted judge is independent of the author context, read-only, and evaluating the same decision packet. -- The verdict preserves dissent and concrete evidence instead of reducing disagreement to an unsupported majority label. -- The chosen option, confidence, findings, and next action validate against the schema and remain traceable to the panel inputs. - -## Runtime Contract - -Multi-judge runs still bind to the shared Claude runtime surface: -[claude-code-latest-features.md](../shared/references/claude-code-latest-features.md). +Council does not write `verdict.v2`, edit the subject, retry work, choose a next +action, or authorize Git, closure, release, or delivery. When Council is used as +a Validate strategy, one accountable fresh validator consumes its report and +Validate remains the sole durable verdict writer. diff --git a/images/gemini/skills/dcg/SKILL.md b/images/gemini/skills/dcg/SKILL.md index 1594c5b8b..6d700ca80 100644 --- a/images/gemini/skills/dcg/SKILL.md +++ b/images/gemini/skills/dcg/SKILL.md @@ -3,7 +3,15 @@ name: dcg user-invocable: false skill_api_version: 1 hexagonal_role: supporting +consumes: [] +produces: [] +context_rel: [] metadata: + dependencies: [] + capabilities: [dcg] + effects: [] + canonical_status: canonical + disposition: keep_specialist tier: execution description: 'Handle blocked destructive commands and configure agent safety guardrails. Triggers: "dcg", "handle blocked destructive commands. use", "dcg skill".' practices: diff --git a/images/gemini/skills/doc/SKILL.md b/images/gemini/skills/doc/SKILL.md new file mode 100644 index 000000000..33d6938a6 --- /dev/null +++ b/images/gemini/skills/doc/SKILL.md @@ -0,0 +1,117 @@ +--- +name: doc +description: 'Generate and validate repo docs, READMEs, and OSS doc packs. Triggers: "doc", "generate and validate repo docs", "doc skill".' +practices: +- wiki-knowledge-surface +- code-complete +- pragmatic-programmer +hexagonal_role: supporting +consumes: +- repo-context +produces: +- documentation +context_rel: [] +skill_api_version: 1 +context: + window: fork + intent: + mode: task + sections: + exclude: + - HISTORY + intel_scope: topic +metadata: + capabilities: [doc] + effects: [] + canonical_status: canonical + disposition: keep_specialist + tier: product + dependencies: [] +output_contract: documentation files +--- +# Doc Skill + +**YOU MUST EXECUTE THIS WORKFLOW. Do not just describe it.** + +Generate and validate documentation for any project. `--mode` selects the artifact family — the default mode handles code/API docs and code-maps; `--mode=readme` generates a gold-standard README; `--mode=oss` scaffolds and audits the open-source doc pack. + +## Constraints + +- Ground every documentation claim in the current repository, because plausible but stale prose is a documentation defect. +- In OSS scaffold mode, create missing docs only by default; never update or overwrite an existing doc unless the user explicitly confirms, because these files may contain operator-owned policy and project history. Treat `refresh` as a separate opt-in path and confirm its target writes with the user before proceeding. +- Keep mode boundaries explicit and run the selected mode's validation, because default, README, and OSS outputs have different completion criteria. + +## Modes + +| `--mode` | Artifact | Read first | +|----------|----------|-----------| +| *(default)* | API docs, code-maps, doc coverage/validate | this file | +| `readme` | Gold-standard README (interview → generate → council-validate) | [references/readme-craft.md](references/readme-craft.md) | +| `oss` | OSS doc pack (CONTRIBUTING/CHANGELOG/AGENTS.md, audit + scaffold) | [references/oss-pack.md](references/oss-pack.md) | + +**Mode routing (absorbed skills):** + +| You typed | Runs | +|-----------|------| +| "readme", "rewrite the README", "validate the README" | `/doc --mode=readme [...]` | +| "oss docs", "scaffold contributing", "audit OSS docs" | `/doc --mode=oss [...]` | + +When invoked with `--mode=readme` or `--mode=oss`, read the corresponding reference above and follow its workflow verbatim. The default-mode steps below apply only when no mode (or the implied code-docs mode) is selected. + +## Execution Steps (default mode — code/API docs) + +Default mode is deliberately thin — a frontier model runs it correctly with no payload. Given `/doc [command] [target]`: + +1. **Detect project type** — `ls package.json pyproject.toml go.mod Cargo.toml` + existing `docs/`; classify CODING / INFORMATIONAL / OPS. +2. **Run the command** — `discover` (grep undocumented funcs), `coverage` (documented vs total), `gen [feature]` (read code → stamp function/class markdown), `all`, or `validate`. +3. **Write the report** to `.agents/doc/YYYY-MM-DD-<target>.md` (coverage %, generated, gaps, validation issues), then report coverage + gaps to the user. + +Full step-by-step detail — grep recipes, function/class + code-map templates, the report skeleton, key rules, worked examples, and the troubleshooting table — lives in **[references/default-mode.md](references/default-mode.md)** (moved there in the generic-craft trim). Read it when you need the exact shapes; otherwise just do the three steps. + +## Output Specification + +- **Path:** default-mode reports go to the artifact directory `.agents/doc/`; README mode updates the repository `README.md`; OSS scaffold mode creates missing root documentation only by default. The separate OSS `refresh` path may update an existing doc only after explicit user confirmation. +- **Filename:** default reports use the filename convention `YYYY-MM-DD-<target>.md`; README and OSS filenames follow their mode references. +- **Format:** outputs are Markdown; the default report schema records coverage percentage, generated artifacts, gaps, and validation issues. +- **Validation command:** validate the skill contract with `bash skills/doc/scripts/validate.sh`, then run the mode-specific validation required by its reference before reporting completion. +- **Downstream handoff:** return changed paths, validation results, coverage or remaining gaps, and any blocked decision; these results are consumed by the requesting workflow and the verification membrane. + +## Quality Checklist + +- Every factual claim is traceable to inspected code, configuration, or existing documentation. +- Generated documentation follows the selected mode's templates and preserves useful existing depth. +- Completion reports name the validators run and disclose unresolved gaps rather than implying full coverage. + +## Reference Documents + +- [references/default-mode.md](references/default-mode.md) — default mode (code/API docs): the full Steps 1-7 detail — grep recipes, function/class + code-map templates, report skeleton, worked examples, troubleshooting (moved out of SKILL.md in the generic-craft trim) +- [references/doc.feature](references/doc.feature) — Executable spec: detect project type, generate type-appropriate docs from the repo, validate existing docs against source (soc-qk4b) +- [references/readme.feature](references/readme.feature) — Executable spec (`--mode=readme`): mode detection, problem-first lead, trust block near install, collapse-don't-delete depth, the council gate, anti-pattern detection (soc-qk4b) +- [references/oss-docs.feature](references/oss-docs.feature) — Executable spec (`--mode=oss`): audit existing/missing OSS docs, scaffold missing without overwrite, project-type-tailored (soc-qk4b) + +- [references/readme-craft.md](references/readme-craft.md) — `--mode=readme`: the 8 gold-standard README patterns, interview, generation structure, council validation, anti-pattern table +- [references/oss-pack.md](references/oss-pack.md) — `--mode=oss`: audit + scaffold the OSS doc pack (CONTRIBUTING/CHANGELOG/AGENTS.md), project-type templates +- [references/oss-documentation-tiers.md](references/oss-documentation-tiers.md) — OSS doc tier definitions (core/standard/enhanced) +- [references/oss-project-types.md](references/oss-project-types.md) — Per-type OSS scaffolding templates (cli/operator/service/library/helm) +- [references/oss-beads-patterns.md](references/oss-beads-patterns.md) — AGENTS.md beads-tracker patterns for OSS projects +- [references/generation-templates.md](references/generation-templates.md) +- [references/prose-and-report-workmanship.md](references/prose-and-report-workmanship.md) +- [references/project-types.md](references/project-types.md) +- [references/validation-rules.md](references/validation-rules.md) +- [references/de-slopify.md](references/de-slopify.md) — Remove AI writing artifacts from docs +- [references/architecture-report.md](references/architecture-report.md) — Generate technical architecture documents + +## Examples + +```bash +/doc # default: docs for the changed surface (references/default-mode.md) +/doc --mode=readme # gold-standard README, council-validated +/doc --mode=oss # full OSS doc pack +``` + +## Troubleshooting + +| Problem | Fix | +|---------|-----| +| Default mode feels heavyweight | Read [references/default-mode.md](references/default-mode.md) — or just ask the model directly for simple docs | +| README mode verdict fails | Re-run with the council findings addressed (see the readme-mode references listed above) | diff --git a/images/gemini/skills/domain/SKILL.md b/images/gemini/skills/domain/SKILL.md new file mode 100644 index 000000000..200063929 --- /dev/null +++ b/images/gemini/skills/domain/SKILL.md @@ -0,0 +1,46 @@ +--- +name: domain +description: 'Load the small AgentOps ubiquitous-language and bounded-context contracts when a term needs precise meaning.' +practices: +- ddd-bounded-context +- pragmatic-programmer +hexagonal_role: domain +consumes: [] +produces: +- stdout +context_rel: [] +skill_api_version: 1 +context: + window: isolated + intent: + mode: none + intel_scope: none +metadata: + capabilities: [domain] + effects: [] + canonical_status: canonical + disposition: keep_specialist + tier: knowledge + dependencies: [] +output_contract: concise domain-language reference +--- +# Domain — ubiquitous language + +Use this read-only library when an AgentOps term or bounded-context boundary +needs precise meaning. + +## Procedure + +1. Read `docs/contracts/ubiquitous-language.md` for the term. +2. Read `docs/contracts/bounded-contexts.yaml` only when ownership or a port + boundary matters. +3. Return the exact definition and source path. +4. Stop. + +Do not invent synonyms that imply lifecycle authority. In particular, Plan, +Candidate, manifest, verdict, revision, strategy, and adapter are semantic +terms; queue, claim, lease, close, land, release, and delivery belong to caller +systems rather than AgentOps core state. + +Vocabulary changes are normal source edits to the two contracts above. This +skill does not promote terms, mutate a knowledge index, or create continuation. diff --git a/images/gemini/skills/dueling-idea-genies/SKILL.md b/images/gemini/skills/dueling-idea-genies/SKILL.md new file mode 100644 index 000000000..f010b0eea --- /dev/null +++ b/images/gemini/skills/dueling-idea-genies/SKILL.md @@ -0,0 +1,97 @@ +--- +name: dueling-idea-genies +description: 'Challenge a consequential idea with sealed independent perspectives, cross-review, and preserved dissent. Triggers: "challenge this idea", "compare independent proposals", "stress-test a one-way door".' +practices: +- design-by-contract +- llm-eval-harness +- adr +hexagonal_role: domain +consumes: +- idea-portfolio.v1 +- task-question +produces: +- idea-challenge.v1 +context_rel: +- kind: customer-of + with: idea-genie +- kind: supplier-to + with: plan +skill_api_version: 1 +user-invocable: true +context: + window: isolated + intent: + mode: task + sections: + exclude: + - HISTORY + intel_scope: full +metadata: + capabilities: [dueling_idea_genies] + effects: [] + canonical_status: canonical + disposition: keep_strategy + tier: judgment + dependencies: [] +output_contract: idea-challenge.v1 JSON validated by skills/dueling-idea-genies/scripts/validate-output.sh +--- +# Dueling Idea Genies + +Produce independent challenges for a consequential choice. The result is +advisory evidence for Plan. It never decides whether a plan is ready and never +substitutes for the final exact-plan Premortem verdict. + +## Constraints + +- Keep generation sealed until every perspective is complete to prevent later + proposals from anchoring on earlier ones. +- Preserve dissent and concrete refutation attempts so Plan sees alternatives + that synthesis might otherwise erase. +- Keep reversible choices lightweight; they do not require a pane manager, + messaging service, council, or model-family rule. +- Emit no readiness, approval, quorum, retry, budget, helper, delivery, or + tracker state. + +## Workflow + +1. Freeze the question, constraints, evidence paths, and comparison rubric. +2. For a one-way door, create at least two fresh contexts with distinct context + identifiers. Each produces its perspective before any is revealed. +3. Reveal the sealed perspectives and cross-review by evidence, reversibility, + system fit, failure modes, and cost. +4. Attempt concrete refutations. Preserve disagreements, failed refutations, + and minority reasoning. +5. Write `idea-challenge.v1`, validate it, and pass the artifact to Plan as one + optional input alongside research and operator intent. + +For a cheap two-way door, emit the lightweight packet directly after one fresh +challenge. Do not manufacture panel ceremony. + +## Output Specification + +- **Artifact directory:** `.agents/ideas/<run-id>/` +- **Filename:** `idea-challenge.json` +- **Format:** `idea-challenge.v1` JSON with route-specific fields enforced by + the validator +- **Validation command:** + `skills/dueling-idea-genies/scripts/validate-output.sh <idea-challenge.json>` +- **Downstream handoff:** `handoff.owner` is exactly `plan`; Plan may accept, + reject, or combine the advisory evidence + +## Quality + +- One-way packets prove distinct context IDs and cross-review another + perspective by named dimensions. +- Dissent and refutation attempts remain explicit. +- The packet contains no semantic readiness field or decision. +- The validator passes before handoff to Plan. + +## Do not + +- Let perspectives see one another before sealed generation completes. +- Convert consensus, transport availability, or a self-score into readiness. +- Require orchestration infrastructure for a reversible choice. + +## References + +- [Dueling Idea Genies behavior](references/dueling-idea-genies.feature) diff --git a/images/gemini/skills/goals/SKILL.md b/images/gemini/skills/goals/SKILL.md index eea2759fe..f2b1989e0 100644 --- a/images/gemini/skills/goals/SKILL.md +++ b/images/gemini/skills/goals/SKILL.md @@ -1,10 +1,9 @@ --- name: goals -description: 'Maintain AgentOps goals. Triggers: "goals", "maintain agentops goals.", "goals skill".' +description: 'Measure declared project fitness goals without recommending or applying work.' practices: - dora-metrics - lean-startup -- agile-manifesto hexagonal_role: domain consumes: [] produces: @@ -17,214 +16,41 @@ context: window: fork intent: mode: task - sections: - exclude: - - HISTORY intel_scope: topic metadata: + capabilities: [goals] + effects: [] + canonical_status: canonical + disposition: keep_specialist tier: product dependencies: [] -output_contract: GOALS.md +output_contract: read-only goal measurement report --- -# /goals — Fitness Goal Maintenance +# Goals — read-only fitness measurement -Maintain `GOALS.md` (canonical v4) as an executable fitness specification. -`GOALS.yaml` is legacy and survives only for migration through -`ao goals migrate`. Execute the selected workflow; do not merely describe it. +Inspect the active goals document and run only the caller-selected measurement, +validation, drift, history, export, or meta-goal command. -## Critical Constraints +## Boundary -- **Why: preserve fields.** Use the `ao goals` command surface; do not - hand-render a whole goals file when a non-lossy command exists. -- **Why: keep one truth.** When both formats exist, `GOALS.md` wins; never - silently treat legacy YAML as active. -- **Why: prove effect.** Measure before mutating, and preserve stable directive - IDs and content unless the selected operation explicitly changes them. -- **Why: protect declared intent.** `recommend` is read-only. `apply` requires - operator confirmation or explicit `--auto --yes` consent and an allowing policy. -- **Why: avoid false fitness.** Do not invent gates for infrastructure that - does not exist; every gate needs an executable check and measurable outcome. -- **Why: preserve lineage.** Treat scenario, directive, bead, verdict, and - learning links as evidence-bearing graph edges; broken references are errors. -- **Why: prevent false completion.** Report failures and partial mutations - exactly; verify the command exit and resulting file before claiming success. +- Prefer `GOALS.md` when both Markdown and legacy YAML exist. +- Preserve stable directive and gate identities in the report. +- Every measured gate must name its executable check and observed outcome. +- Do not add, remove, prioritize, recommend, apply, prune, migrate, or otherwise + mutate goals. +- Do not translate a fitness gap into work selection or a next action. -## Mode Routing - -| Intent | Command | -|---|---| -| measure/status (default) | `ao goals measure --json` | -| initialize | `ao goals init` | -| manage directives | `ao goals steer` | -| add a gate | `ao goals add` | -| compare snapshots | `ao goals drift` | -| inspect history | `ao goals history` | -| export snapshot | `ao goals export` | -| run meta-goals | `ao goals meta --json` | -| validate structure | `ao goals validate --json` | -| remove stale gates | `ao goals prune` | -| migrate formats | `ao goals migrate` | -| manage scenario links | `ao goals scenarios` | -| audit lineage | `ao goals trace` | -| export Gherkin | `ao goals render` | - -Use [operations.md](references/operations.md) for full flags, examples, -troubleshooting, and mode-specific procedures after routing. - -## Core Workflow - -1. Identify the active goals file and requested mode. If the request is - ambiguous, default to measurement. -2. Run the read-only observation for that mode before any mutation. For steer, - init enrichment, add, prune, migrate, or apply, show the relevant current - state first. -3. Execute the exact `ao goals` command. Capture its exit status and structured - output where available. -4. For mutations, inspect the resulting `GOALS.md` diff and run - `ao goals validate --json`. -5. Re-measure or run the mode-specific proof so the result is grounded in the - post-change state. -6. Emit the output specification below, including failures and next action. - -## Measure Mode - -Run: +## Read-only commands ```bash ao goals measure --json +ao goals validate --json +ao goals drift +ao goals history +ao goals export +ao goals meta --json ``` -Extract each gate's status, weight, failure evidence, and overall fitness. For -`GOALS.md`, also assess directives: - -```bash -ao goals measure --directives -``` - -Correlate directives with recent commits and the repository's own tracker. -Classify each as `addressed`, `partially-addressed`, or `gap`; do not infer -progress from titles alone. - -Scenario satisfaction is part of fitness. A directive below its configured -ratio is RED. For a fast executable-spec check: - -```bash -ao goals measure --scenarios-only -o json -``` - -The aggregation and exit-code contract is in -[executable-spec-chain.md](references/executable-spec-chain.md). - -## Mutation Rules - -### Initialize - -Run `ao goals init` (or `--non-interactive` when explicitly requested), then -enrich only from repository evidence: - -- add at least one outcome-oriented north star; -- derive anti-stars from recurring verified failure modes when evidence exists; -- add product directives with a direction and measurable target; -- suggest product gates only for live infrastructure. - -Generation heuristics and examples live in -[generation-heuristics.md](references/generation-heuristics.md). - -## Steer Mode - -Measure first. Recommend removing completed directives, repairing chronic -failure, and covering measurable product gaps. Use non-lossy commands: - -```bash -ao goals steer add "Title" --description="..." --steer=increase -ao goals steer remove 3 -ao goals steer prioritize 2 1 -ao goals steer recommend -``` - -Apply a recommendation only with the consent constraints above. Re-run measure -and validate after mutation. - -## Add and Migrate Modes - -- `add`: supply a stable ID, executable check, weight, description, and type. -- `migrate`: preserve the original as a backup and validate the converted file. - -## Prune Mode - -Run `ao goals prune --dry-run` first. Remove only gates whose referenced paths -are actually stale, then validate and re-measure. - -Schema details for formats, weights, snapshots, and meta-goals are in -[goals-schema.md](references/goals-schema.md). - -## Executable-Spec Operations - -- `ao goals scenarios` lists and manages directive/scenario links; `--lint` - checks the graph. -- `ao goals trace --from <id>` renders lineage from a stable directive, - scenario, or bead ID. -- `ao goals trace --orphans --strict` fails on warnings as well as broken - references. -- `ao goals render --out spec.feature` exports linked scenarios as Gherkin. - -Use stable directive IDs (`d-...`) as anchors, not display numbers. The compact -behavioral contract is [goals.feature](references/goals.feature). - -## Output Specification - -Return a concise report with these fields: - -- **Path:** structured command output goes to `stdout`; repository mutations - land in the active `GOALS.md`, and generated artifacts use the requested path. -- **Filename:** the filename convention is `GOALS.md` for the canonical spec; - exports and renders use the explicit `--out` filename supplied by the user. -- **Format:** the serialization/schema format is command JSON for structured - results, Markdown v4 for goals, and Gherkin for rendered scenarios. -- **Validation command:** validate mutations with - `ao goals validate --json`, plus the relevant measure, trace, or render proof. -- **Downstream handoff:** `GOALS.md` is consumed by the operating loop and - `/evolve`; exported JSON or Gherkin is handed to the requested CI/BDD consumer. - -```text -Mode: <measure|init|steer|add|drift|history|export|meta|validate|prune|migrate|scenarios|trace|render> -Source: <GOALS.md|GOALS.yaml|none> -Command: <exact command executed> -Result: <PASS|WARN|FAIL> — <exit/effect summary> -Fitness: <passing>/<total> (<percent>) or n/a -Directives: <addressed/partial/gap counts> or n/a -Evidence: <specific output, diff, snapshot, or artifact paths> -Next action: <single concrete action or none> -``` - -For measurement, list failed gates and RED directives with their direct -evidence. For mutation, list the exact changed goal/directive IDs and the -post-change validation result. JSON/export/render modes may return the artifact -plus this envelope; do not replace structured output with an unsupported prose -claim. - -## Quality Rubric - -A complete result satisfies all of the following: - -- **Correct routing:** the command matches the user's intent and active format. -- **Truthful fitness:** every score and status comes from current command output. -- **Safe mutation:** current state was observed, the diff is narrow, and consent - requirements were honored. -- **Executable goals:** gates have runnable checks; directives have measurable - outcomes and healthy scenario links where applicable. -- **Verified result:** mutations pass `ao goals validate --json` and a relevant - post-change measurement or graph check. -- **Actionable report:** failures name direct evidence and one concrete next - action without hiding partial success. - -If any required item is missing, report `WARN` or `FAIL`; do not label the work -complete. - -## References - -- [operations.md](references/operations.md) — detailed modes, examples, and troubleshooting -- [executable-spec-chain.md](references/executable-spec-chain.md) — scenario satisfaction, lineage, and re-steer policy -- [generation-heuristics.md](references/generation-heuristics.md) — goal and directive design patterns -- [goals-schema.md](references/goals-schema.md) — v1-v4 schemas and snapshot contract -- [goals.feature](references/goals.feature) — executable behavior examples +Run the requested command once. Return the command, exit code, goal-level +results, aggregate measurement, missing evidence, and checked/not-checked scope. +Then stop. diff --git a/images/gemini/skills/handoff/SKILL.md b/images/gemini/skills/handoff/SKILL.md index 206bf0ea5..1d7b649b6 100644 --- a/images/gemini/skills/handoff/SKILL.md +++ b/images/gemini/skills/handoff/SKILL.md @@ -1,243 +1,43 @@ --- name: handoff -spine: true -description: 'Write compact session handoffs. Triggers: "handoff", "write compact session handoffs.", "handoff skill".' -practices: -- adr -- wiki-knowledge-surface -- code-complete +description: 'Write compact caller-authored session evidence without choosing continuation. Triggers: "handoff", "write compact session handoff".' +practices: [adr, wiki-knowledge-surface, code-complete] hexagonal_role: supporting consumes: [] -produces: -- .agents/handoff/*.md +produces: [.agents/handoff/*.md] context_rel: [] skill_api_version: 1 context: window: inherit - intent: - mode: none + intent: {mode: none} intel_scope: none metadata: + capabilities: [handoff] + effects: [] + canonical_status: canonical + disposition: keep_specialist graph_root: true tier: session dependencies: [] -output_contract: .agents/handoff/<date>-<topic>.md plus <date>-<topic>-prompt.md +output_contract: caller-authored handoff artifact --- -# Handoff — Durable Session Continuation -> **Loop position:** write-side adapter for `handoff → clear → rehydrate`. -> It captures the live lane as two checked Markdown artifacts before context is -> cleared or ownership changes. +# Handoff -**Execute this workflow. Do not only describe it.** +Write a factual session artifact that another context can read. Include: -## Constraints +- caller-supplied goal and summary; +- completed artifacts and exact evidence paths; +- unresolved facts or risks; +- optional caller-supplied continuation text; +- best-effort read-only repository identity when useful. -- Write both artifacts before clearing context, ending the session, or transferring ownership, because a partial handoff makes the next agent rediscover state. -- Ground every accomplishment, blocker, issue state, and next action in durable evidence such as paths, commit SHAs, verdicts, and tracker ids; do not promote conversational memory into fact because it drifts. -- Preserve pawl disposition separately from helper outcome. The disposition is one of `CONFIRMED`, `REFUTED`, `HOLD`, `ESCALATE`, or `REBOUND`; the helper outcome is only `UNSTUCK`, `ESCALATE`, or `not-run`. Plain `REFUTED` continues auto-redo; only a breaker enters `HOLD` and one helper pass. `CONFIRMED` alone authorizes the door; helper `UNSTUCK` resumes work but must re-earn `CONFIRMED`. Helper `ESCALATE` reaches a human; refusal-lane work, explicit judgment, and exhausted time/cost/quota budgets skip the helper and go directly to a human. -- Keep the continuation prompt as a pointer to the handoff document, not a second source of truth, because duplicated narrative diverges. +Do not infer a next action, select work, assign ownership, consume the artifact, +change tracker or Git state, classify a verdict, govern retries, or restart a +runtime. Reading a handoff must not mutate it. -## Purpose and boundaries +The ao session handoff and ao session rehydrate commands implement the same +boundary for JSON artifacts. The skill may write Markdown when that better +serves a human, but the content semantics remain identical. -Use this skill when a productive session is pausing, changing agents, nearing a -context reset, or explicitly needs a continuation packet. The handoff must let a -fresh session resume without reconstructing the lane from chat. - -Do not use it as a postmortem: handoff records **current state**; `postmortem` -records reusable learning. Do not call an idle session successful: when there is -no durable activity, report `EMPTY` with the reason and write no fabricated -accomplishments. - -## Inputs - -Given `/handoff [topic]`: - -- An explicit topic wins. -- Otherwise derive a 2–4 word lowercase hyphenated slug from the current issue, - most recent commit, or ratchet state. -- If none is descriptive, use `session-$(date +%H%M)`. - -## Execution workflow - -### 1. Gather durable session evidence - -Create the artifact directory and inspect the live repository and tracker: - -```bash -mkdir -p .agents/handoff -git status --short -git log --oneline --since="2 hours ago" 2>/dev/null -git diff --stat HEAD~5 2>/dev/null | head -20 -ao beads exec list --status in_progress 2>/dev/null | head -5 -ao beads exec list --status closed 2>/dev/null | head -5 -find .agents/research .agents/plans -type f -name '*.md' -print 2>/dev/null | tail -5 -``` - -If an explicit multi-writer workflow is active, also record held reservations, -peer/comms topology, and the working-thread pointer. Otherwise say that no such -topology is active; do not invent one. - -### 2. Pin the pause point - -Record all four fields, even when their value is `none`: - -1. Last completed action and its evidence. -2. Exact next action, preferably a command or file to inspect. -3. Open blocker, pawl disposition, helper outcome, and their evidence. Never - store `UNSTUCK` as a disposition or treat it as authorization. -4. Dirty files, claimed issues, reservations, and external state the next - session inherits. - -**Checkpoint:** confirm the tracker id/status and current `git rev-parse HEAD` -against live commands before writing them. A stale identifier is worse than an -omitted one. - -### 3. Write the handoff document - -Fill the authoritative handoff in the **Artifact Templates** section below. -Cite paths and SHAs for completed work, distinguish observed state from -inference, and keep open questions separate from blockers. - -The next action must be executable without rereading chat. List priority files -in read order and state why each matters. - -### 4. Write the continuation prompt - -Fill the continuation-prompt template below. It must point to the handoff -document, restate the objective and verified pause point in no more than a -short paragraph, and name the first command or file to inspect. - -Do not copy the full handoff into the prompt. The document is authoritative. - -### 5. Validate before reporting - -```bash -doc=.agents/handoff/YYYY-MM-DD-<topic>.md -prompt=.agents/handoff/YYYY-MM-DD-<topic>-prompt.md -test -s "$doc" && test -s "$prompt" -for heading in '## Objective' '## Verified state' '## Where we paused' '## Next action' '## Files to read' '## Validation evidence'; do - rg -Fqx "$heading" "$doc" -done -for marker in 'Read first:' 'First action:'; do rg -Fq "$marker" "$prompt"; done -rg -q '^\*\*Captured:\*\* [0-9]{4}-[0-9]{2}-[0-9]{2}T[^ ]+$' "$doc" -rg -q '^\*\*Repository:\*\* .+$' "$doc" -rg -q '^\*\*HEAD:\*\* [0-9a-f]{40}$' "$doc" -rg -q '^\*\*Tracker:\*\* .+$' "$doc" -rg -q '^\*\*Pawl disposition:\*\* (CONFIRMED|REFUTED|HOLD|ESCALATE|REBOUND|none)$' "$doc" -rg -q '^\*\*Helper outcome:\*\* (UNSTUCK|ESCALATE|not-run)$' "$doc" -``` - -**Checkpoint:** verify before session close that the handoff names the current -capture time, repository, HEAD, tracker state, dirty-worktree state, validation -evidence, pawl disposition, helper outcome, and a concrete next action. Repair -any missing field and rerun the commands. - -The behavior contract is -[references/handoff.feature](references/handoff.feature). - -### 6. Optional learning and runtime closeout - -When the session produced a major decision or at least three meaningful -commits, suggest `postmortem --quick`; do not run it in place of the handoff. -If `ms` is installed, grade only skills genuinely consulted with -`ms outcome <skill> --success|--failure`. - -Runtime-specific closeout belongs to the runtime projection. Do not claim a -session was stopped merely because the Markdown artifacts exist. -When explicitly requested for a managed session, `ao session handoff --no-kill` -may additionally capture structured orchestrator state; it does not replace the -Markdown pair. - -## Examples - -**User says:** `/handoff validation-membrane` after a plain refutation. - -**Result:** both artifacts record disposition `REFUTED`, helper outcome -`not-run`, the failing evidence, and the next auto-redo command. - -## Troubleshooting - -| Problem | Recovery | -| --- | --- | -| The handoff says `HOLD` after one failed check | Restore `REFUTED` and continue auto-redo; reserve `HOLD` for the configured breaker. | - -## Artifact Templates - -Authoritative handoff: - -```markdown -# Handoff: <Topic> -**Captured:** <ISO-8601 timestamp> -**Repository:** <path> -**HEAD:** <full 40-character SHA> -**Tracker:** <issue id and live status, or none> -**Pawl disposition:** <CONFIRMED | REFUTED | HOLD | ESCALATE | REBOUND | none> -**Helper outcome:** <UNSTUCK | ESCALATE | not-run> - -## Objective -<Current objective and acceptance boundary.> - -## Verified state -- <Completed action> — evidence: <path, command result, SHA, or verdict> -- Worktree / external state: <exact inherited state or none> - -## Where we paused -**Last action:** <verified action> -**Blocker / questions:** <evidence-bound blocker, disposition, helper outcome, or none> - -## Next action -<One command or file inspection and its expected result.> - -## Files to read -1. `<priority path>` — <why first> - -## Validation evidence -- `<command>` → <exit/result> -``` - -Continuation prompt: - -```markdown -# Continuation: <Topic> -Read first: `.agents/handoff/YYYY-MM-DD-<topic>.md` (authoritative state). -Objective and pause point: <short evidence-bound summary>. -First action: `<command or file to inspect>` -Verify HEAD/tracker, then preserve pawl disposition and helper outcome separately. -``` - -## Output Specification - -- **Path:** write both artifacts under `.agents/handoff/` in the current repository. -- **Filename:** use `YYYY-MM-DD-<topic>.md` for the authoritative handoff and `YYYY-MM-DD-<topic>-prompt.md` for its continuation pointer. -- **Format:** serialize both as UTF-8 Markdown; the handoff uses the exact required headings in the template and the prompt names its referenced handoff path. -- **Validation command:** run the `test -s` and `rg -q` checkpoint commands above; every command must exit zero before reporting `DONE`. -- **Downstream handoff:** the next session reads the handoff first, verifies the recorded HEAD/tracker state, then executes the named first action; postmortem and corpus tooling may consume the artifact later. - -## Quality Checklist - -- Evidence quality: accomplishments and state cite durable paths, issue ids, SHAs, or verdict artifacts rather than memory-only claims. -- Resume quality: the next action is executable, priority files are ordered, and inherited dirty/external state is explicit. -- Pawl quality: disposition includes `CONFIRMED`; helper outcome is separate; `REFUTED` auto-redoes, `UNSTUCK` re-enters work, and neither `HOLD` nor helper `ESCALATE` is collapsed into a pass. -- Artifact quality: both filenames match the same topic/date; capture/repository/HEAD/tracker and validation evidence are present; the continuation prompt points to the authoritative handoff. - -## Report - -Report both artifact paths, the captured pause point, and the first continuation -action. End with exactly one marker: - -```text -<promise>DONE</promise> -``` - -For an idle session with no context to capture: - -```text -<promise>EMPTY</promise> -Reason: No session activity found to hand off -``` - -## See Also - -- `skills/postmortem/SKILL.md` — extract reusable learning after state is safe. -- `skills/bootstrap/SKILL.md` — rehydrate a fresh session before resuming. +Return the artifact path and stop. diff --git a/images/gemini/skills/heal-skill/SKILL.md b/images/gemini/skills/heal-skill/SKILL.md index 993e94822..a6a7a4b98 100644 --- a/images/gemini/skills/heal-skill/SKILL.md +++ b/images/gemini/skills/heal-skill/SKILL.md @@ -1,13 +1,12 @@ --- name: heal-skill -description: 'Repair skill hygiene and deep-audit SKILL.md quality (absorbed skill-auditor). Triggers: "heal-skill", "heal skill", "repair skill hygiene", "skill-auditor", "audit skill", "skill audit".' +description: 'Check or repair structural hygiene in AgentOps skill packages. Triggers: "heal skill", "repair skill hygiene", "audit skill structure", "check skill package".' practices: - refactoring -- code-complete hexagonal_role: supporting consumes: [] produces: -- audit-report.json +- skill-hygiene-report context_rel: - kind: customer-of with: skill-builder @@ -17,232 +16,84 @@ context: intent: mode: none sections: - exclude: - - HISTORY - - INTEL - - TASK + exclude: [HISTORY, INTEL, TASK] intel_scope: none metadata: + capabilities: [heal_skill] + effects: [optional_skill_projection_repair] + canonical_status: canonical + disposition: keep_specialist tier: meta dependencies: [] -output_contract: 'stdout: heal report, auto-fixed skill files; deep audit mode: skills/heal-skill/schemas/audit-report.json' +output_contract: skills/heal-skill/schemas/audit-report.json --- -# /heal-skill — Automated Skill Maintenance -> **Purpose:** Detect and auto-fix common skill hygiene issues across the skills/ directory. +# /heal-skill — Check one or more skill packages -**YOU MUST EXECUTE THIS WORKFLOW. Do not just describe it.** +`heal-skill` is a specialist hygiene tool. It reports structural defects in +canonical source skills and generated Codex twins. With `--fix`, it repairs only +owned projections through their generators. It does not schedule work, operate +Git, validate a software candidate, or decide what happens after a failure. -## Constraints - -- Resolve every explicit target to a real direct child of `skills/` or `skills-codex/` before processing; reject traversal, missing/outside paths, and every symlink spelling because aliases make mutation ownership ambiguous. -- Run `--check` and deep audit read-only before any repair, because their exact finding codes and target path define the permitted mutation. -- Use `--fix` only for the documented auto-fixable structural codes, then rerun strict validation; never hand-edit generated Codex artifacts because parity projections have owned regeneration paths. -- Treat WARN/FAIL as ordinary repair evidence: redo and re-audit automatically while the breaker remains closed; only a breaker enters HOLD/helper routing, because a plain refutation is not an andon. - -## Quick Start +## Inputs ```bash -/heal-skill # Check all skills (report only) -/heal-skill --fix # Auto-repair all fixable issues -/heal-skill --strict # Check all skills, exit 1 on findings (CI mode) -/heal-skill skills/council # Check a specific skill -/heal-skill --fix skills/validate # Fix a specific skill - -bash skills/heal-skill/scripts/audit.sh skills/council # Deep audit (read-only; see "Deep audit mode" below) +bash skills/heal-skill/scripts/heal.sh --check [skills/<slug> ...] +bash skills/heal-skill/scripts/heal.sh --check --strict [skills/<slug> ...] +bash skills/heal-skill/scripts/heal.sh --fix [skills/<slug> ...] ``` -## What It Detects +Every explicit target must be a real, direct child of `skills/` or +`skills-codex/`. Missing paths, traversal, and symlink spellings are rejected. -Auto-fix allowlist and principal diagnostics: +## Procedure -| Code | Issue | Auto-fixable? | -|------|-------|---------------| -| `MISSING_NAME` | No `name:` field in SKILL.md frontmatter | Yes -- adds name from directory | -| `MISSING_DESC` | No `description:` field in SKILL.md frontmatter | Yes -- adds placeholder | -| `NAME_MISMATCH` | Frontmatter `name` differs from directory name | Yes -- updates to match directory | -| `UNLINKED_REF` | File in references/ not linked in SKILL.md | Yes -- converts bare backtick refs to markdown links | -| `EMPTY_DIR` | Skill directory exists but has no SKILL.md | Yes -- removes empty directory | -| `MISSING_API_VERSION` | Selected source skill has no `skill_api_version` | Yes -- inserts version 1 in selected target(s) only | -| `DEAD_REF` | SKILL.md references a non-existent references/ file | No -- warn only | -| `SCRIPT_REF_MISSING` | SKILL.md references a scripts/ file that does not exist | No -- warn only | -| `INVALID_AO_CMD` | SKILL.md references an `ao` subcommand that does not exist (only runs if `ao` is on PATH) | No -- warn only | -| `DEAD_XREF` | SKILL.md references a `/skill-name` that has no matching skill directory | No -- warn only | +1. Resolve and contain all requested target directories. +2. Parse each `SKILL.md` frontmatter. +3. Check the path/name match, description, API version, disposition metadata, + and linked local references. +4. Print every finding once. +5. In `--fix` mode only, regenerate metadata-owned projections and scoped Codex + twins, then stop. -> `CATALOG_MISSING` was removed: it only ran when `skills/using-agentops/SKILL.md` existed, and that skill is gone. Catalog completeness is gated by `MISSING_DISPOSITION` against `docs/contracts/skill-dispositions.yaml`. +`--check` is read-only. `--strict` makes any finding produce exit 1. A failed +fix is returned to the caller; the skill does not retry or select another +action. -## Execution Steps +## Deep content audit -### Step 1: Run the heal script +The optional read-only content audit is: ```bash -# Check mode (default) -- report only, no changes -bash skills/heal-skill/scripts/heal.sh --check - -# Fix mode -- auto-repair what it can -bash skills/heal-skill/scripts/heal.sh --fix - -# Target a specific skill -bash skills/heal-skill/scripts/heal.sh --check skills/council -bash skills/heal-skill/scripts/heal.sh --fix skills/council +bash skills/heal-skill/scripts/audit.sh [--strict] [--json <path>] skills/<slug> ``` -**Checkpoint:** before accepting any mutation, confirm the target path and finding code are present in the read-only report and the code is documented as auto-fixable. +It combines the structural result with deterministic authoring checks and an +advisory quality score. It is not the core `Validate` phase, does not write a +`verdict.v2`, and has no delivery authority. Check definitions live in +[audit-checks.md](references/audit-checks.md); density scoring is described in +[context-density-checks.md](references/context-density-checks.md). -### Step 1A: Audit Codex Parity Drift When The Codex Bundle Looks Wrong +## Output -When the problem is not source-skill hygiene but `skills-codex/` drift, run the Codex parity audit first: +Structural findings are printed as: -```bash -bash scripts/audit-codex-parity.sh -bash scripts/audit-codex-parity.sh --skill swarm +```text +[FINDING_CODE] skills/example: concrete explanation ``` -Use this when a checked-in Codex skill still contains Claude-era primitives (`TaskCreate`, `TaskList`, `Tool: Task`), Claude backend references, or obviously broken runtime rewrites. +Deep audit JSON conforms to [audit-report.json](schemas/audit-report.json). +The caller owns any subsequent edit or invocation. -**Repair rule:** keep canonical shared behavior in `skills/<name>/SKILL.md`. Update `skills-codex/<name>/SKILL.md` when the shipped Codex artifact is wrong, and keep durable Codex-only tailoring in `skills-codex-overrides/<name>/SKILL.md`. +## Checks -After repair: +- Check mode never mutates files. +- Fix mode changes only an explicit source target and its owned projections. +- A second identical fix is idempotent. +- Generated Codex parity follows [codex-parity.md](references/codex-parity.md). +- Remaining non-fixable findings stay explicit. -```bash -bash scripts/audit-codex-parity.sh -bash scripts/validate-codex-override-coverage.sh -bash scripts/validate-codex-generated-artifacts.sh --scope worktree -``` +## Related executable specifications -**Checkpoint:** after a Codex repair, require parity, override coverage, and generated-artifact validation to exit zero before reporting the runtime bundle healthy. - -### Step 2: Interpret results - -- **Exit 0:** All clean, no findings. Also exit 0 for `--check` mode with findings (report-only). -- **Exit 1:** Findings reported with `--strict` or `--fix` flag. In `--fix` mode, fixable issues were repaired; re-run `--check` to confirm. - -### Step 3: Report to user - -Show the output. If `--fix` was used, summarize what changed. If `DEAD_REF` findings remain, advise the user to remove or update the broken references manually. - -## Output Format - -**Artifact directory:** stdout for human findings; `.agents/audits/` for JSON reports. -**Filename convention:** `<skill>-audit.json`. -**Serialization/schema format:** JSON matching `schemas/audit-report.json`. -**Validator command:** `jq -e . .agents/audits/<skill>-audit.json`. -**Downstream handoff:** consumed by the validation wave before repair. - -One line per finding: - -``` -[MISSING_NAME] skills/foo: No name field in frontmatter -[MISSING_DESC] skills/foo: No description field in frontmatter -[NAME_MISMATCH] skills/foo: Frontmatter name 'bar' != directory 'foo' -[UNLINKED_REF] skills/foo: refs/bar.md not linked in SKILL.md -[EMPTY_DIR] skills/foo: Directory exists but no SKILL.md -[DEAD_REF] skills/foo: SKILL.md links to non-existent refs/bar.md -[SCRIPT_REF_MISSING] skills/foo: references scripts/bar.sh but file not found -[INVALID_AO_CMD] skills/foo: references 'ao badcmd' which is not a valid subcommand -[DEAD_XREF] skills/foo: references /nonexistent but skill directory not found -``` - -## Deep audit mode (absorbed from /skill-auditor) - -Requests for `/skill-auditor` route here: the audit is the detect phase of heal, and it -lives at `scripts/audit.sh`. Unlike `--fix`, the audit is **read-only** — it reports; -repairs go through `heal.sh --fix` (Pass-1 issues) or hand edits (Pass-2 issues). - -```bash -bash skills/heal-skill/scripts/audit.sh [--strict] [--json <path>] skills/<name> -``` - -Two passes, then an aggregate verdict: - -- **Pass 1 — structural (delegated):** runs `heal.sh --check --strict <target>` and gates - on its exit code; it never reimplements the hygiene checks (heal is the source of truth). - A strict failure forces the aggregate verdict to FAIL but does NOT short-circuit Pass 2. -- **Pass 2 — content discipline:** 8 checks in `audit.sh` (triggers, frontloaded - constraints, rationale, checkpoints, output spec, quality rubric, references - modularization, trigger clarity). Definitions and accepted forms: - [references/audit-checks.md](references/audit-checks.md). -- **Verdict aggregation:** any check `fail` → FAIL; otherwise any `warn` → WARN; - otherwise PASS. Max-severity wins — no silent downgrade. Exit 0 on PASS/WARN - (1 on WARN under `--strict`), 1 on FAIL, 2 on usage error. -- **0-30 rubric (Pass 3) is ADVISORY-only:** `scripts/score_agentops_skill.py --audit-block` - folds a deterministic 10-category score (band C/B/A/S) into the report under `rubric`. - It never changes the PASS/WARN/FAIL verdict — it is a productization backlog signal, - not a ship blocker. The density block - ([references/context-density-checks.md](references/context-density-checks.md)) is - advisory too. -- **Accepted trigger forms:** `description-has-triggers` accepts only the forms - declared by the authoritative `repo-runtime` profile - (finding `f-2026-05-06-auditor-checks-must-fit-host-conventions` — audit checks must fit - the host's existing valid conventions). - -Report JSON conforms to [schemas/audit-report.json](schemas/audit-report.json); -`skill-builder` owns the canonical SKILL.md template. -Executable spec: [references/skill-auditor.feature](references/skill-auditor.feature). - -## Notes - -- The script is **idempotent** -- running `--fix` twice produces the same result. -- Explicit targets contain mutation: `--fix skills/foo` may change `foo` only; source siblings remain byte-identical. -- `DEAD_REF`, `SCRIPT_REF_MISSING`, `INVALID_AO_CMD`, and `DEAD_XREF` are warn-only because the correct resolution requires human judgment. -- `INVALID_AO_CMD` only runs if the `ao` CLI is available on PATH. Skipped silently otherwise. -- When run without a path argument, scans all directories under `skills/`. -- Use `--strict` for CI gates: exits 1 on any finding. Without `--strict`, check mode exits 0 even with findings. -- For Codex parity drift, use the audit script plus override-layer repair workflow in [references/codex-parity.md](references/codex-parity.md). The shell fixer is intentionally not allowed to rewrite generated Codex bodies directly. - -## Quality Checklist - -- Scope fidelity: every changed path is the audited target or its owned parity projection, and every repair maps to a reported finding code. -- Verification quality: strict heal/deep-audit and applicable Codex parity commands rerun after mutation with zero unresolved findings. -- Idempotence: a second identical repair run produces no additional diff, while non-auto-fixable findings remain explicit rather than guessed through. - -## Examples - -### Running a health check across all skills - -**User says:** `/heal-skill` - -**What happens:** -1. The heal script scans every directory under `skills/`, checking each for the nine issue types (missing name, missing description, name mismatch, unlinked references, empty directories, dead references, script reference integrity, CLI command validation, cross-reference validation). -2. Findings are printed one per line with issue codes (e.g., `[NAME_MISMATCH] skills/foo: Frontmatter name 'bar' != directory 'foo'`). -3. The script exits with code 0 in check mode (even with findings), or code 1 with `--strict` or `--fix` flags. - -**Result:** A diagnostic report showing all skill hygiene issues across the repository, with no files modified. - -### Auto-fixing a specific skill - -**User says:** `/heal-skill --fix skills/validate` - -**What happens:** -1. The heal script inspects only `skills/validate/`, running all per-skill checks against that skill. -2. For each fixable issue found (e.g., `MISSING_NAME`, `UNLINKED_REF`), the script applies the repair automatically -- adding the name from the directory, converting bare backtick references to markdown links, etc. -3. Any `DEAD_REF` findings are reported as warnings since they require human judgment to resolve. - -**Result:** The `skills/validate/SKILL.md` is repaired in place, with a summary of changes applied and any remaining warnings. - -## Troubleshooting - -| Problem | Cause | Solution | -|---------|-------|----------| -| `DEAD_REF` findings persist after `--fix` | Dead references are warn-only because the correct fix (delete, create, or update) requires human judgment | Manually inspect each dead reference and either create the missing file, remove the link from SKILL.md, or update the path | -| Script reports `EMPTY_DIR` for a skill in progress | The skill directory was created but SKILL.md has not been written yet | Either add a SKILL.md to the directory or remove the empty directory. Running `--fix` will remove it automatically | -| `NAME_MISMATCH` fix changed the wrong name | The script always updates the frontmatter `name` to match the directory name, not the other way around | If the directory name is wrong, rename the directory first, then re-run `--fix` | -| Script exits 0 but a skill still has issues | The issue type is not one of the checks the heal script detects | `heal.sh` covers structural hygiene only. Run the deep audit (`scripts/audit.sh`) for content-discipline checks; deeper quality issues require manual review or `/council` validation | -| Running `--fix` twice produces different output | This should not happen -- the script is idempotent | File a bug. Check if another process modified the skill files between runs | -| `skills-codex/` keeps regressing after sync | Mechanical conversion is preserving the wrong semantics | Run `bash scripts/audit-codex-parity.sh`, then move the durable Codex body rewrite into `skills-codex-overrides/<name>/SKILL.md` instead of patching generated output | - -## See Also - -- [skill-builder](../skill-builder/SKILL.md) — scaffolds new skills against the unified template; runs heal-skill (hygiene + deep audit) as self-checks -- [red-team](../validate/SKILL.md) — complementary; probes USABILITY (does the workflow actually work) vs the audit (is the structure correct) - -## References - -- [`skill-conformance-profiles.yaml`](../skill-builder/references/skill-conformance-profiles.yaml) — authoritative rules, severities, and boundary - -- [references/skill-stocktake.md](references/skill-stocktake.md) -- [references/codex-parity.md](references/codex-parity.md) -- [references/heal-skill.feature](references/heal-skill.feature) — Executable spec: detect hygiene issues, flag Codex-parity drift, auto-fix + report, --strict fails on remaining findings (soc-qk4b) -- [references/audit-checks.md](references/audit-checks.md) — deep-audit Pass-2 check definitions + accepted forms + PRODUCT.md mapping -- [references/context-density-checks.md](references/context-density-checks.md) — advisory density coverage logic and false-positive handling -- [references/skill-auditor.feature](references/skill-auditor.feature) — Executable spec for the absorbed deep audit mode: Pass 1 heal delegation, Pass 2 content checks, density report + productization score (soc-qk4b) +- [heal-skill.feature](references/heal-skill.feature) +- [skill-auditor.feature](references/skill-auditor.feature) diff --git a/images/gemini/skills/idea-genie/SKILL.md b/images/gemini/skills/idea-genie/SKILL.md new file mode 100644 index 000000000..0efc0bf6f --- /dev/null +++ b/images/gemini/skills/idea-genie/SKILL.md @@ -0,0 +1,39 @@ +--- +name: idea-genie +description: 'Generate an evidence-grounded opportunity portfolio for an open-ended product or engineering question. Triggers: "idea genie", "what should we build", "supported opportunities".' +practices: [lean-startup, bdd-gherkin] +hexagonal_role: domain +consumes: [repo-context, task-question] +produces: [idea-portfolio.v1] +context_rel: +- kind: customer-of + with: research +- kind: supplier-to + with: plan +skill_api_version: 1 +user-invocable: true +metadata: + tier: execution + dependencies: [] + capabilities: [generate_evidenced_options] + effects: [write_idea_portfolio] + canonical_status: canonical + disposition: keep_strategy +output_contract: idea-portfolio.v1 JSON validated by skills/idea-genie/scripts/validate-output.sh +--- + +# Idea Genie + +Generate a small portfolio of evidenced options. This skill explores; it does +not select, schedule, track, implement, or validate work. + +1. State the question, constraints, non-goals, and sources. +2. Separate cited observations from assumptions. +3. Give each candidate its supporting evidence, overlap with existing + capabilities, and one normal or edge scenario. +4. Run a novelty pass, merge equivalents, and discard unsupported ideas. +5. Stop when no materially new evidenced candidate appears. +6. Write and validate `idea-portfolio.v1`, then return it to the caller or Plan. + +An empty `no-new-work` portfolio is valid. Plan alone may turn an option into a +PlanPacket. diff --git a/images/gemini/skills/implement/SKILL.md b/images/gemini/skills/implement/SKILL.md index 9167e55a3..4e4e59116 100644 --- a/images/gemini/skills/implement/SKILL.md +++ b/images/gemini/skills/implement/SKILL.md @@ -1,218 +1,63 @@ --- name: implement -spine: true -description: 'Implement one tracked issue. Triggers: "implement", "implement one tracked issue.", "implement skill".' +description: 'Execute one bounded RED to GREEN to refactor experiment from a PlanPacket and return a CandidatePacket. Triggers: "implement", "build this plan", "run the experiment".' practices: - tdd - refactoring -- code-complete +- small-batch-flow hexagonal_role: driving-adapter consumes: -- domain +- plan-packet.v1 produces: -- git-changes +- candidate-packet.v1 context_rel: - kind: customer-of - with: domain + with: plan skill_api_version: 1 +user-invocable: true metadata: graph_root: true tier: execution - dependencies: - - beads-br - - standards - - rch - - refactor - - test -context: - window: isolated - intent: - mode: task - sections: - exclude: - - HISTORY - intel_scope: topic -output_contract: code changes, test results, bead status update, behavioral spec (optional) + dependencies: [] + capabilities: [execute_one_experiment, collect_factual_evidence] + effects: [modify_declared_subject, write_candidate_packet] + canonical_status: canonical + disposition: keep +output_contract: schemas/candidate-packet.v1.schema.json --- -# Implement Skill -> **Quick Ref:** Execute single issue end-to-end. Output: code changes + commit + closed issue. +# Implement -**YOU MUST EXECUTE THIS WORKFLOW. Do not just describe it.** +Execute exactly one bounded experiment described by a `PlanPacket`. Implement +owns subject edits and factual evidence. It does not own work selection, +tracking, Git, retries, semantic validation, repair, closure, or delivery. -## Constraints +## Workflow -- Freeze the claimed issue's acceptance criteria, non-goals, and write scope before editing, because every changed line must trace to the single vertical slice; route unrelated work to a follow-up. -- For behavior changes, capture a right-reason failing test before implementation and keep GREEN-mode tests immutable, because the failing proof is the slice contract rather than ceremony. -- Route a plain `REFUTED` validation result back through automatic repair and revalidation; only a circuit-breaker trip enters `HOLD` and one bounded helper pass, while helper `ESCALATE` or refusal/judgment/exhausted-budget classes reach a human. +1. Verify the PlanPacket digest and freeze its acceptance and write scope. +2. Run the declared first acceptance check before changing behavior. For a + behavior change, preserve evidence that it fails for the expected missing + behavior. For docs-only or pure refactor work, record an honest green + pre-change baseline instead. +3. Make the smallest in-scope change that satisfies the active behavior. +4. Run the targeted acceptance checks and capture factual results. +5. Refactor only while those checks stay green. Refactoring does not change the + acceptance test. +6. Enumerate every actual changed path using a caller- or runtime-provided + comparison. If complete coverage cannot be established, set + `changed_path_coverage_complete` to false; do not guess. +7. Compute `subject-manifest.v1` with the pure Validate helper and write a + `candidate-packet.v1` containing author context ID, subject locator, actual + changed paths, evidence, and results. +8. Return the CandidatePacket and stop. -## Loop position +Specialists such as standards, domain, test, refactor, and security may provide +advice. They are never hard dependencies and cannot add lifecycle authority. -Move **4 (TDD per slice)** of the [operating loop](../../docs/architecture/operating-loop.md). Consumes one vertical slice from the [slice validation plan](../../docs/templates/slice-validation.md); produces failing test → passing implementation → refactor-under-green. Discipline: (1) first failing test must fail for the right reason (missing behavior, not syntax); (2) smallest change to flip green; (3) refactor as its own commit. Slices that mix refactor + feature are two slices, not one. Code without a failing test has no contract; the slice is not done. +## Boundary -Execute a single issue from start to finish. - -**CLI dependencies:** ao (issue tracking via `ao beads exec <cmd>` — it resolves the bead tracker, bd or br, and its ledger; plus ratchet gates). Optional — see `skills/shared/SKILL.md` for fallback table. If no tracker is available, use the issue description directly and track progress via TaskList instead of beads. - -## When to use - -**Triggers:** "implement", "implement one tracked issue", or "implement skill". - -- Use `/implement <issue-id>` to implement a specific tracked issue. -- Use `/implement` (no argument) to pick up next ready work via `ao beads exec ready`. -- Use `/implement <description>` to implement an ad-hoc task without a tracked issue. - -### Folded triggers (ag-s43tg wave 1): `pr-implement` routes here - -- **`pr-implement` → OSS contribution mode.** Use when you need to implement a scoped OSS PR — - fork-based implementation of an open source contribution with mandatory isolation checks. - Same single-issue TDD discipline as internal work, plus the fork lane: ensure the fork exists - and is current, create an isolated worktree, run an isolation pre-check (BLOCK on mixed - concerns) and post-check (BLOCK on scope creep), check for competing PRs before starting, and - hand off to `/pr-prep` for commit/PR shaping. Input is the plan artifact from `/pr-prep` + - `/plan` (run those first if no plan exists). - -## Examples - -### Implement Specific Issue - -**User says:** `/implement ag-5k2` - -**What happens:** -1. Agent reads issue from beads: "Add JWT token validation middleware" -2. Explore agent finds relevant auth code and middleware patterns -3. Agent edits `middleware/auth.go` to add token validation -4. Runs `go test ./middleware/...` — all tests pass -5. Commits with message "Add JWT token validation middleware\n\nImplements: ag-5k2" -6. Closes only via `scripts/close-with-implementation-receipt.sh --issue ag-5k2 --receipt <canonical-receipt>` - -**Result:** Issue implemented, verified, committed, and closed. Ratchet recorded. - -### Pick Up Next Available Work - -**User says:** `/implement` - -**What happens:** -1. Agent runs `ao beads exec ready` — finds `ag-3b7` (first unblocked issue) -2. Claims issue via `ao beads exec update ag-3b7 --status in_progress` -3. Implements and verifies -4. Closes through the receipt wrapper after canonical pawl confirmation - -**Result:** Autonomous work pickup and completion from ready queue. - -### GREEN Mode (Test-First) - -**User says:** `/implement ag-8h3` (invoked by `/crank --test-first`) - -**What happens:** -1. Agent receives failing tests (immutable) and contract -2. Reads tests to understand expected behavior -3. Implements ONLY enough to make tests pass -4. Does NOT modify test files -5. Verification: all tests pass with fresh output - -**Result:** Minimal implementation driven by tests, no over-engineering. - -## Lifecycle Integration Flags - -| Flag | Default | Description | -|------|---------|-------------| -| `--no-lifecycle` | off | Skip ALL lifecycle skill auto-invocations (test gen, review, refactor) | -| `--lifecycle=<tier>` | matches complexity | Controls which lifecycle skills fire: `minimal` (test only), `standard` (+review), `full` (+refactor dry-run) | - -Lifecycle tier defaults to matching the current complexity level. Explicit `--lifecycle=<tier>` overrides. - -## Execution - -Read [references/workflow.md](references/workflow.md) for the full procedure (Steps 0 through 11), including durable RED/GREEN evidence, the implementation receipt, independent pawl routing, confirmed close, and ratchet record. - -GREEN mode rules live in [references/green-mode.md](references/green-mode.md). The pre-commit autonomous quality loop lives in [references/quality-loop.md](references/quality-loop.md). The behavioral spec format lives in [references/behavioral-spec.md](references/behavioral-spec.md). - -## Key Rules - -- **Captured RED for behavior** - every behavior change closes only with a reproducible failing contract at a pre-implementation commit; GREEN input counts as captured RED, and no-framework work uses a minimal executable harness. `--no-tdd` cannot authorize behavior closure. Only mechanically derived docs-only and independently reviewed pure-refactor lanes may waive RED; pure refactor proves canonical acceptance green before and after with unchanged test drivers. -- **Refactor after every green — it is the load-bearing move.** Refactor under green as its own commit after each behavior, never deferred to one final pass (deferred-refactor workflows were the worst-performing cluster in the study above). **Never let a refactor step change a test** — a test change during refactor means behavior changed, which is a new slice, not a refactor. -- **One behavior per cycle (small batch).** Implement one behavior, keep the suite green, refactor, move to the next — small batches beat all-at-once. -- **Lifecycle skills fire automatically** - /test, /review, /refactor run at appropriate steps (disable with `--no-lifecycle`) -- **Explore first** - understand before changing -- **Edit, don't rewrite** - prefer Edit tool over Write tool -- **Follow patterns** - match existing code style -- **Verify changes** - run tests or sanity checks -- **Commit with context** - reference the issue ID -- **Close the issue** - update status when done, then run crank's [Close checkpoint](../crank/SKILL.md#close-checkpoint--a-closed-bead-is-a-sensor-reading-age-cysr): a closed bead is a sensor reading — if what it taught falsifies an assumption the remaining plan depends on, surface it for re-planning instead of silently proceeding (age-cysr) - -## Without Beads - -If br CLI not available: -1. Skip the claim/close status updates -2. Use the description as the task -3. Still commit with descriptive message -4. Report completion to user - -## Output Specification - -- **Path:** modify only issue-approved product/test paths; store evidence under `.agents/evidence/implement/<issue-id>/` and the final receipt under its `<full-sha>/` directory. -- **Filename:** product/tests use repository-native names; the receipt is exactly `<issue-id>-<full-sha>-receipt.json`. -- **Format:** product files use native formats; the receipt is JSON conforming to [schemas/implementation-receipt.schema.json](schemas/implementation-receipt.schema.json) and binds immutable RED plus fresh GREEN evidence to the full SHA. -- **Validation command:** run issue acceptance and relevant gates, `scripts/validate-workflow-contract.sh source`, then `scripts/verify-implementation-receipt.sh --issue <issue-id> --receipt <canonical-path>`; closure requires the verifier and canonical pawl check to pass. -- **Downstream handoff:** pass the receipt and exact SHA to `/validate`; `REFUTED` auto-repairs, breaker `HOLD` consults one helper, and only `CONFIRMED` authorizes closure. - -## Quality Checklist - -- Acceptance fidelity: every changed line maps to one acceptance example or necessary cleanup, with non-goals unchanged. -- Test fidelity: the first failing proof fails for missing behavior, final tests pass fresh, and refactor commits do not modify the behavioral contract. -- Scope fidelity: changed paths remain inside the issue write scope; unrelated findings become follow-ups instead of hitchhiking. -- Evidence fidelity: commit, tracker closure, changed files, and validation commands identify the same final implementation SHA. - -## Completion Markers - -``` -<promise>DONE</promise> -``` - -If blocked or incomplete: -``` -<promise>BLOCKED</promise> -Reason: <why blocked> -``` - -``` -<promise>PARTIAL</promise> -Remaining: <what's left> -``` - -## Troubleshooting - -| Problem | Cause | Solution | -|---------|-------|----------| -| Issue not found | Issue ID doesn't exist or local state looks stale | Run `ao beads exec show <id>` to verify; trust the tracker's source-of-truth ledger (for br, `_beads/issues.jsonl`) if the local cache looks stale | -| GREEN mode violation | Edited a file not related to the issue scope | Revert unrelated changes. GREEN mode restricts edits to files relevant to the issue | -| Verification gate fails | Tests fail or build breaks after implementation | Read the verification output, fix the specific failures, re-run verification | -| "BLOCKED" status | Contract contradicts tests or is incomplete in GREEN mode | Write BLOCKED with specific reason, do NOT modify tests | -| Fresh verification missing | Agent claims success without running verification command | MUST run verification command fresh with full output before claiming completion | -| Ratchet record failed | ao CLI unavailable or chain.jsonl corrupted | Implementation still closes via br, but ratchet chain needs manual repair | - -## Reference Documents - -- [references/behavioral-spec.md](references/behavioral-spec.md) — Behavioral spec format for Stage 4 validation -- [references/binary-deployment-gate.md](references/binary-deployment-gate.md) — CLI/hook binary-deployment gate spec -- [references/gate-checks.md](references/gate-checks.md) — Ratchet and premortem gate checks -- [references/green-mode.md](references/green-mode.md) — GREEN mode test-first implementation rules -- [references/implement.feature](references/implement.feature) — Executable spec: the /implement done-state (first-failing-test → green → refactor → verified close) (soc-qk4b.2) -- [references/quality-loop.md](references/quality-loop.md) — Pre-commit autonomous quality loop -- [references/resume-protocol.md](references/resume-protocol.md) — Resume protocol for interrupted sessions -- [references/workflow.md](references/workflow.md) — Full execution workflow (Steps 0 through 11) -- [schemas/implementation-receipt.schema.json](schemas/implementation-receipt.schema.json) — Durable RED/GREEN and independent-validation receipt - -## Local Resources - -- `scripts/verify-implementation-receipt.sh` — fail-closed closure authority over Git, evidence digests, rerun commands, and the canonical pawl verdict -- `scripts/verify-implementation-receipt.bash` — non-executable verifier implementation entered only through the sanitized launcher -- `scripts/test-implementation-receipt.sh` — forged-dimension behavioral fixtures -- `scripts/close-with-implementation-receipt.sh` — snapshot, verify, recheck, and close without a verifier-to-close gap -- `scripts/close-with-implementation-receipt.bash` — non-executable close implementation entered only through the sanitized launcher -- `scripts/validate-workflow-contract.sh` — ordered workflow and schema drift gate - -## See also - -- [test](../test/SKILL.md) — Test generation, coverage analysis, and TDD workflow +- Do not commit, push, claim, close, release, land, reserve, retry, or invoke a + semantic validator. +- Do not silently expand acceptance. A different acceptance contract is a new + intent for a caller to start separately. +- A failed check is evidence in the CandidatePacket, not permission to loop. diff --git a/images/gemini/skills/learn/SKILL.md b/images/gemini/skills/learn/SKILL.md new file mode 100644 index 000000000..89d399875 --- /dev/null +++ b/images/gemini/skills/learn/SKILL.md @@ -0,0 +1,40 @@ +--- +name: learn +description: 'Optionally analyze collections of durable verdicts for recurring evidence after the critical path. Triggers: "learn from verdicts", "mine validation history".' +practices: +- continuous-learning +- evidence-based-engineering +hexagonal_role: supporting +consumes: +- verdict.v2 +produces: +- learning-observations +context_rel: +- kind: customer-of + with: validate +skill_api_version: 1 +user-invocable: true +metadata: + graph_root: false + tier: execution + dependencies: [] + capabilities: [analyze_verdict_collections] + effects: [write_advisory_observations] + canonical_status: canonical + disposition: keep_off_path +output_contract: advisory learning observations +--- + +# Learn + +Learn is an optional, off-path consumer of durable `verdict.v2` collections. +It may summarize recurring evidence and propose a candidate deterministic check +for later human or caller evaluation. + +Learn does not run during RPI, validate a subject, alter a verdict, mutate a +plan, promote a rule, choose continuation, or emit a lifecycle receipt. Missing +Learn output never changes whether a candidate is valid. + +When invoked, bind every observation to verdict and finding digests, distinguish +repeated objectives from repeated reviews of one objective, disclose the sample +size, and stop at advisory evidence. diff --git a/images/gemini/skills/ms/SKILL.md b/images/gemini/skills/ms/SKILL.md new file mode 100644 index 000000000..dd404e3d2 --- /dev/null +++ b/images/gemini/skills/ms/SKILL.md @@ -0,0 +1,168 @@ +--- +name: ms +description: 'meta_skill (ms) — the skill-search/load engine over both corpora (agentops + jsm). Find a skill for a task, search skills, or load runnable skill guidance. Triggers: "ms", "meta_skill", "skill search", "find a skill for", "load skill guidance".' +practices: +- pragmatic-programmer +skill_api_version: 1 +user-invocable: false +hexagonal_role: supporting +consumes: [] +produces: [] +context_rel: [] +metadata: + dependencies: [] + capabilities: [ms] + effects: [] + canonical_status: canonical + disposition: keep_specialist + tier: execution + external_dependencies: + - "ms binary (Jeffrey Emanuel's meta_skill; source build from ~/dev/meta_skill, branch local/frontmatter-id — the 0.1.2 release binary corrupts IDs on Anthropic-frontmatter skills)" + - jq (required for parsing -O json output) +--- +<!-- TOC: Core Insight | Constraints | Quick Start | Consume (MCP) | Write/Admin (CLI) | Output | Production Skill Handoff | Footguns | Concurrency | Scenarios | Quality | References --> + +# ms — meta_skill search/load engine + +> **Core Insight:** `ms` is the skill-search engine over both corpora (agentops + jsm, ~175 skills). **Consume via MCP, write/admin via CLI.** One law: after ANY reindex/wipe, every running `ms mcp serve` MUST be killed (sessions respawn fresh). A surviving server silently reads pre-wipe data and returns `recorded:true` on writes that land in orphaned files. + +## Constraints + +- Load with `full: true` or `--full` when the intent is to execute a skill, because metadata cards and packed overviews omit runnable guidance. +- Keep the consume/write boundary explicit: use MCP for search and load, but use the CLI for feedback and outcomes because only CLI writes are verified to land in the live database. +- Reindex only through `scripts/ms-reindex.sh`, because it sweeps stale servers and proves source equivalence after rebuilding the index. +- Treat the local index as disposable state, not a source of truth; the non-goal is editing indexed content instead of `skills/**`. +- Keep `ms` retrieval-only for production skill work. It returns search and load + results; the caller owns authoring, validation, and every subsequent decision. + +## Quick Start + +Find a skill (MCP-primary — BM25, currently strictly better than CLI search), then load the FULL runnable SKILL.md in one call (always `full: true` when you mean to use it): + +```bash +mcp__ms__search {query: "handle a rate limit switching accounts"} +mcp__ms__load {skill: "account-rotation", full: true} +``` + +CLI fallback when no MCP server is attached: + +```bash +ms search "switch accounts on rate limit" -O json +ms load account-rotation --full -O json | jq -r '.data.content' +``` + +State root: `~/Library/Application Support/ms/`. + +--- + +## Consume — MCP-primary (`mcp__ms__*`) + +Prefer the MCP tools whenever a `ms mcp serve` is attached — they are the fast, verified read path. + +| Tool | Use | +|------|-----| +| `mcp__ms__search {query}` | BM25 search. **Currently strictly better than CLI search** (see Footguns — CLI hybrid is BM25-only; ms never stores doc embeddings). | +| `mcp__ms__load {skill, full: true}` | Returns the full runnable SKILL.md in ONE call, zero extraction friction. **`full: false` returns a useless metadata card — always `full: true` when you intend to use the skill.** | +| `mcp__ms__show {skill}` | Metadata card for a skill. | +| `mcp__ms__suggest {cwd}` | Suggests skills for a directory. Works — but **ignore its project-language detection** (misdetects Makefile repos as C; cosmetic only). | + +**CLI fallback** (no MCP server attached): + +```bash +ms search "<query>" -O json +ms load <id> --full -O json | jq -r '.data.content' # content lives in .data.content +``` + +--- + +## Write / Admin — CLI-only (verified landing in the live DB) + +The MCP feedback tool exists, but **only the CLI write path is verified to land** — trust the CLI for writes. + +```bash +ms feedback add <skill> --positive --comment "..." # feedback on a skill +ms feedback add <skill> --negative --comment "..." + +ms outcome <skill> --success # record only AFTER downstream factory use + validation +ms outcome <skill> --failure + +ms doctor # admin: health +scripts/ms-reindex.sh # (re)index THE way: rebuild + sweep + probe + source-equivalence check +scripts/ms-reindex.sh --check-source # read-only freshness proof against current skills/** source +ms list -O jsonl --limit 1000 # counting / enumeration +ms config # resolved config + skill_paths +``` + +## Output Specification + +- **Path:** search, load, and admin results are returned on `stdout`; durable index state remains under `~/Library/Application Support/ms/`. +- **Filename:** no result filename is created by this skill; callers capture CLI output explicitly when they need a durable artifact. +- **Format:** MCP returns structured tool data; CLI automation uses JSON or JSONL, with full skill text at `.data.content` for `ms load --full -O json`. +- **Validation command:** run `skills/ms/scripts/validate.sh` for the retrieval boundary and `scripts/ms-reindex.sh --check-source` for normalized source equivalence. +- **Downstream handoff:** return the loaded guidance and source identity to the caller. Retrieval never chooses or starts a workflow. + +## Production Skill Handoff + +**Production-intent handoff:** When the query concerns creating or editing a skill, `ms` retrieves relevant guidance and stops. The caller may separately invoke `skill-builder`, `heal-skill`, or another authoring tool. + +**Authority boundary:** `skills/**` is canonical source; the generator owns the `ms` Codex twin and other projections. Never edit the index, loaded copies, or generated projections as source. + +`ms` never validates or interprets downstream work. A failed search, +load, write, or reindex is returned as evidence and ends this invocation. + +**Outcome timing:** Record `ms outcome` only after the caller has independent evidence about downstream usefulness, never after retrieval alone. That observation does not change core state. + +--- + +## Footguns (all measured 2026-07-02) + +| Footgun | Truth | +|---|---| +| **MCP server survives a DB wipe/reindex** | An `ms mcp serve` NEVER reopens handles — it follows renamed inodes into the backup, giving stale reads AND silent misdirected writes (`recorded:true` into orphaned files). **Reindex via `scripts/ms-reindex.sh` — THE way to reindex** (rebuilds, TERMs every server, probes a fresh server, then compares normalized local loads with current `skills/**` source); never run bare `ms index` and leave servers up. Sessions respawn fresh. | +| **`ms load --pack N`** | Trap: caps at the gutted `overview` tier for ANY N (`800` == `20000`) — drops the executable steps and returns LESS than the no-flag default. Use `--full` (CLI) or `full: true` (MCP). | +| **`-O plain`** | Prints name-only on `load`; truncates list output (`[N more lines]`). The content lives in `-O json` → `.data.content`. | +| **CLI `ms search` "hybrid"** | Effectively BM25-only — ms never stores doc embeddings (`upsert_embedding` is called only from a unit test), so hybrid ≡ BM25 under ANY backend; no config/backend change fixes it (upstream gap, feature-noted; measured 2026-07-02, age-s3jf). Still loses to MCP BM25 in practice; don't prefer it. | +| **Stale `ms.lock`** | `ms doctor` prints "Lock held" for a DEAD pid yet still says all-pass. A dead-pid lock is safe to delete. | +| **Symlinks** | ms does NOT follow directory symlinks — `skill_paths` must list BOTH roots explicitly: the `~/.claude` skills dir AND the `~/dev/agentops/skills` repo dir. | +| **Binary** | Source build only (`~/dev/meta_skill`, branch `local/frontmatter-id`); the 0.1.2 release binary corrupts IDs on Anthropic-frontmatter skills. Update: `git fetch && git rebase origin/main && cargo install --path . --locked`. | + +## Concurrency + +Parallel CLI + MCP `load` measured clean — no lock errors. The lock hazard is the survive-a-wipe case above (kill the serve), not concurrent reads. + +--- + +## Scenarios + +```gherkin +Scenario: Load a skill's full runnable guidance + Given an ms mcp serve is attached + When I call mcp__ms__load {skill: "account-rotation", full: true} + Then the full runnable SKILL.md content is returned in one call + +Scenario: Reindex invalidates every running server + Given one or more ms mcp serve processes are running + When I run ms index (or wipe/rebuild the DB) + Then I kill every ms mcp serve so sessions respawn against fresh data + And a surviving server would silently read pre-wipe data and mis-land writes + +Scenario: A stale local projection fails closed + Given AgentOps skills are authoritative and ms is a disposable local index + When a full ms load has a different normalized name or description from source + Then scripts/ms-reindex.sh exits nonzero and names the stale skill +``` + +## Quality Checklist + +- Full loads preserve the complete runnable guidance rather than a metadata card or packed overview. +- Search/load reads use the verified MCP path, while feedback and outcome writes use the verified CLI boundary. +- Any rebuild finishes with stale servers swept and source equivalence reported. +- Production skill intent leaves `ms` after retrieval; generated twins and loaded/indexed copies are never hand-edited as source. +- Search and load results remain advisory inputs, never proof that downstream work is correct. +- `ms outcome` records observed usefulness only after independent downstream evidence. + +## References + +- Upstream: Jeffrey Emanuel's `meta_skill` (source at `~/dev/meta_skill`, branch `local/frontmatter-id`). +- Related consume-tool skill in this repo: [`cass`](../cass/SKILL.md) (session archaeology). The jsm `cass-memory` (cm) procedural-memory tool is the write-side complement (installed separately, not in this repo). +- Lifecycle contract validator: [`scripts/validate.sh`](scripts/validate.sh). diff --git a/images/gemini/skills/ntm/SKILL.md b/images/gemini/skills/ntm/SKILL.md index 03a102726..fb79dad72 100644 --- a/images/gemini/skills/ntm/SKILL.md +++ b/images/gemini/skills/ntm/SKILL.md @@ -12,230 +12,75 @@ context_rel: - kind: supplier-to with: agent-native metadata: + capabilities: [ntm] + effects: [] + canonical_status: canonical + disposition: keep_optional_adapter tier: execution dependencies: [] -description: 'Orchestrate NTM tmux agent swarms and robot APIs — spawn/send panes, read robot state, triage, locks/mail, safety, pipelines. Single owner of swarm-tending doctrine. Triggers: "ntm", "orchestrates ntm tmux agent swarms", "ntm skill".' +description: 'Use NTM as an optional pane adapter for caller-supplied roles and commands. Triggers: "ntm", "tmux panes", "ntm robot state".' practices: - pragmatic-programmer --- -<!-- TOC: One Rule | Constraints | Outcome | Cold Start | Software Factory Mesh | Mandatory Loop | NTM Action Card | Surface Selection | Pattern Tiers | Anti-Patterns | Pre-Flight Checklist | Output | Quality | Operating Notes | Reference Index | Related Skills --> +# NTM — optional pane adapter -> **Scope:** this skill is AgentOps operating doctrine for the external NTM binary. The binary is self-describing — query its live robot help, capability, schema, and snapshot surfaces before state changes. Never trust remembered syntax over the executable contract. `agent-native` owns the portable worker lifecycle; this skill owns NTM-specific pane mechanics only. +NTM hosts explicit agent roles in persistent panes. It is transport, not an +AgentOps lifecycle controller. The caller chooses the panes, roles, commands, +write scopes, and stopping point. -# NTM — external pane adapter +## Boundary -> **The One Rule:** Discover the live NTM contract first, then use the least interactive surface that can prove and execute the action. No `--robot-capabilities` / `--robot-snapshot` evidence -> no automation assumption. +- Never start or probe NTM merely because it is installed. +- Discover the live command contract with `ntm --help`, + `ntm --robot-capabilities`, and `ntm --robot-snapshot` before unfamiliar + actions. +- Dispatch each caller-supplied command once. NTM does not select work, retry a + failed command, validate a candidate, integrate changes, or decide what runs + next. +- Pane roles are descriptive. They grant no ownership, admission, Git, release, + or delivery authority. +- Concurrent writers require caller-supplied disjoint scopes and whatever + isolation the repository requires. NTM does not infer safe concurrency. +- Safety, lock, mail, and pipeline features remain substrate facts. Their state + cannot change an RPI phase result or semantic verdict. -The most common NTM mistake is treating it like a tmux macro runner. NTM is a control plane: robot API, attention feed, work graph, locks/mail, pipelines, safety, approvals, serve API, and durability all have explicit contracts. Use the contract. +## One-shot dispatch -## Constraints +1. Record the explicit session, pane, role, working directory, and command. +2. Inspect robot capabilities and current snapshot. +3. Create or select only the named pane. +4. Send the command once. +5. Observe robot state, attention, and transcript until the command exits or the + caller's observation window ends. +6. Return the factual result and stop. -- Keep NTM opt-in because the operator chooses the orchestration substrate; never start, register, or probe it merely because it is available. -- Keep `agent-native` as lifecycle owner and NTM as pane-mechanics adapter because portable factory policy must not depend on tmux. -- Give every writing pane a disjoint worktree or reserved write scope because shared-path collisions are the primary multi-agent failure mode. -- Keep producer, tester, fresh-context refuter, and integrator roles distinct because a producing pane cannot independently certify its own candidate. -- Route admission through `ao pawl review`, not pane consensus, because NTM transports work while the AgentOps membrane owns verdicts. +For a software-factory layout, the caller may name producer, tester, validator, +or integrator panes. The same identity rule still applies: a validator for a +candidate must have a distinct context identity from its author. Merely placing +two roles in different panes is a declared runtime fact, not proof of semantic +independence. -## Outcome — When an NTM Action Has Delivered +## Output -A state-changing NTM action is complete only when **all** of the following hold: +Return: -- The intended state transition is **visible in `ntm --robot-snapshot`** — not just acknowledged by the command's exit code. (NTM commands can succeed at the API layer while panes/work/locks remain unchanged; trust the snapshot, not the return value.) -- The **attention feed** (`--robot-attention` / `--robot-tail`) shows the expected event(s) — pane output, work-graph movement, lock acquire/release, mail delivery. Absence is evidence of failure. -- Adjacent state (git, br/beads, mail, pipelines) reflects the action's downstream effects within one observation window — otherwise the action fired in isolation and likely didn't accomplish its real purpose. -- Locks and pipelines you opened are either **released / completed** by you, or explicitly handed off via mail with a thread the next operator can claim. Orphaned locks block the swarm. -- For dispatched marching orders: the targeted pane has acknowledged (printed the order, started the work, or replied via mail). A sent-but-not-acknowledged order is not "done." +- session and pane identifiers; +- role and exact command; +- start and observation timestamps; +- exit status when known; +- robot state and relevant transcript references; +- degraded or unavailable substrate surfaces; +- effects that were and were not observed. -If the snapshot or attention feed disagrees with what the command said happened, **trust the snapshot** and re-discover the contract — the local model of NTM is stale. +A timeout, nonzero exit, or degraded source is reported as evidence. The caller +decides whether to invoke another experiment. -## Cold Start: Which NTM Skill? +## Useful live surfaces -| Situation | Start here | -|---|---| -| You need NTM doctrine, then exact syntax via `--robot-docs` / references | This skill | -| You are tending an already-running worker factory | `/agent-native` for lifecycle policy, then this skill for NTM mechanics | -| You are running a Brenner-style hypothesis investigation or incident RCA through NTM panes | `brennerbot-with-ntm` | -| You only need Beads or BV mechanics | `/beads-br` or `/beads-bv` | +Prefer machine-readable robot surfaces for capability, snapshot, attention, +tail, and pipeline observations. Use interactive key injection only when the +caller explicitly requests an interactive action and no robot command provides +the needed behavior. -For any state-changing action, verify the live contract with `ntm --robot-capabilities` before executing. - -### Folded triggers (ag-s43tg wave 1): `ntm-browser-test-coordination` + `ntm-review-worker-orchestration` route here - -- **Browser/UI test coordination.** Use when coordinating browser or UI tests through NTM panes - with screenshots and handoffs: dispatch the test run as marching orders to a dedicated pane, - reserve the surfaces under test via `agent-mail`, keep screenshot/artifact paths in the pane - output, and confirm completion in `--robot-snapshot` + the attention feed before handing off. -- **Review/analysis workers.** Use when operating an NTM review or - analysis worker with bounded inputs and evidence-backed output: scope the worker - to an explicit input set (files, diff, bead), - require artifact-backed findings (paths + line refs, not impressions), and treat a worker that - emits conclusions without evidence as not-done — re-dispatch with the bounded-input contract - restated. - -## Tending doctrine (single owner) - -**`agent-native` owns lifecycle policy; `ntm` owns NTM mechanics.** Apply the suspect → bounded nudge → replace policy from `agent-native`; use the recovery commands, liveness truth stack, boot-race warnings, and executable robot contract documented here to enact it. - -## Software Factory Mesh - -**Operator-choice invariant:** NTM is an optional substrate selected explicitly by the operator; a cold `ao pawl review` and ordinary in-session work do not require an NTM session. - -**Pane-role contract:** Producer panes own disjoint worktrees and write scopes; tester panes run deterministic checks; fresh-context refuter panes judge without mutation; integrator panes act only after an `ao pawl review` `CONFIRMED` verdict. Never collapse producer and refuter for one candidate. - -**Agent Mail handoff:** Before two or more writers run, reserve disjoint paths. Handoff on one Agent Mail thread with bead, pane role, worktree, reserved paths, exact HEAD, evidence paths, and next action; require recipient acknowledgement and release reservations at completion. - -**Pawl authority:** NTM may host or tend warm reviewer panes, but `ao pawl review` owns independent verdict and admission. Do not inject keys into an in-flight pawl pane, and do not treat pane agreement as confirmation. - -**Failure routing:** A plain `REFUTED` verdict returns to the producer for automatic repair and revalidation. Only a tripped circuit breaker enters `HOLD` and receives exactly one bounded helper consultation before the candidate re-earns an independent verdict. - -## The Loop (Mandatory) - -``` -1. DISCOVER -> ntm --robot-capabilities; ntm --robot-tools; repo AGENTS.md/README.md -2. SNAPSHOT -> ntm --robot-snapshot; inspect sources/degraded_sources, cursor, sessions, panes -3. SELECT -> choose the smallest surface: work/assign/send/wait/pipeline/locks/mail -4. PROVE -> fill the NTM action card: target, contract, safety, ownership, rollback -5. EXECUTE -> prefer --robot-* for automation; avoid human-only TUIs -6. VERIFY -> attention/events/causality/tail plus git/br/mail evidence changed as expected -7. CLEANUP -> release/renew locks, checkpoint/handoff, prune old pipeline state when appropriate -8. REPEAT -> re-snapshot on cursor expiry or after any state-changing action -``` - -## NTM Action Card - -For every state-changing NTM action, be able to answer this before running it: - -```markdown -## NTM action: <command> -- Target session/project: <name/path>; resolved by: `ntm config get projects_base` / `ntm quick` / snapshot -- Live contract checked: `ntm --robot-capabilities` contains <flag>; schema/docs checked if unfamiliar -- Evidence before: cursor=<N>; sources=<fresh/degraded>; panes=<count>; locks=<summary> -- Ownership/safety: Agent Mail reservation or worktree policy is clear; user pane inclusion is intentional -- Blast radius: panes/files/sessions affected; destructive/safety/policy approvals required? <yes/no> -- Verification after: <robot event / tail movement / bead state / git change / pipeline status> -- Recovery: <smart restart / interrupt / checkpoint restore / cancel pipeline / handoff> -``` - -If you cannot fill the card, do a read-only discovery pass first. - -## Surface Selection - -Score candidate surfaces when several could work: - -``` -Score = (ContractFit x Observability x Reversibility) / BlastRadius - -ContractFit 1-5: exact robot/schema match beats human help text -Observability 1-5: action emits cursor/event/status/causality evidence -Reversibility 1-5: easy cancel/retry/restore/checkpoint -BlastRadius 1-5: one pane/file is low; whole session/process tree is high -``` - -Pick the highest score. In ties, prefer the surface that produces structured output. Enumerate candidates from `ntm --robot-docs=commands`, not from memory. Standing preferences: - -- `--robot-*` for anything machine-driven; `ntm dashboard` / `ntm palette` / `ntm view` are human-only TUIs. -- `--robot-format=toon` (or `NTM_ROBOT_FORMAT=toon`) and `--robot-verbosity=terse` when context is tight. -- Recovery order: diagnose -> probe / is-working -> smart-restart -> explicit restart. Never kill before a liveness proof. -- Event-driven tending (`--robot-wait` / `--robot-attention`) over fixed sleep/poll loops. - -## Pattern Tiers - -Escalate only with the action card filled; each tier raises the proof bar: - -1. **Tier 1 — safe read-only** (capabilities/schema, snapshot, events/digest/attention, work triage/queue-dry, locks list/check). Always permitted; proof = fresh `sources` / `degraded_sources` reviewed, cursor advancing, no conflicting reservation. -2. **Tier 2 — reversible control** (directed send, interrupt, smart-restart, assign, pipeline cancel/resume). Guard = explicit pane/type scoping, tail/liveness evidence that intervention is warranted, run id and state confirmed. -3. **Tier 3 — durable orchestration** (pipelines, Agent Mail locks, checkpoint/handoff, serve API, safety/policy/approvals). Guard = dry-run first, lease/thread/run ids captured, auth and exposure understood, exit/recovery path named. - -## Anti-Patterns (Never Do) - -| Bad move | Why it fails | Use instead | -|---|---|---| -| Call `ntm view` from automation | Retiles the human layout and returns nothing useful | `--robot-tail`, `--robot-snapshot`, or `--robot-dashboard` | -| Trust old notes over `--robot-capabilities` | NTM surface changes quickly | Discover first | -| Send to `--all` without naming the user-pane intent | Can hit the operator pane | use type/panes or `-s/--skip-first` | -| Treat cursor values as portable | Cursors are per-server monotonic | checkpoint/handoff for portability | -| Kill/restart before a liveness proof | Destroys partial work | diagnose -> smart restart -> explicit restart | -| Conflate pipeline status and run | `--robot-pipeline=<id>` is status | `--robot-pipeline-run=<file>` | -| Retry degraded mail/CASS forever | Burns the session | record degraded source, use fallback, continue | -| Infer abandoned beads from silence | NTM deliberately does not implement `bead_orphaned` | explicit status/mail/reservation evidence | -| Trust a fresh `spawn --cod` pane blind (bare shell) | Some builds leave a **bare zsh**; prompts execute as shell text | verify with `--robot-tail`; relaunch the CLI, or fall back to `codex exec -C <worktree>` per lane | -| Fire a separate `send` right after a bare `spawn` (boot race) | `spawn` returns **before** the agent boots to its input box; the first send is silently dropped → pane is never-engaged (CLI alive, 0.0% CPU) | wait for input-ready first: `--assign` / `--init-prompt` / `--robot-wait=ready`; if already dropped, **re-dispatch, don't restart** (OC-047) | - -## Pre-Flight Checklist - -- [ ] Repo `AGENTS.md` / README read when operating inside a codebase (repo-local rules override this skill). -- [ ] `ntm --robot-capabilities` checked for any unfamiliar flag. -- [ ] `ntm --robot-snapshot` captured and `sources` / `degraded_sources` reviewed. -- [ ] Session/project resolution verified; labels and `projects_base` make sense. -- [ ] User pane inclusion/exclusion is explicit. -- [ ] File ownership is clear: Agent Mail reservation, bead assignee, or approved worktree policy. -- [ ] For pipelines: dry-run passed; run id/state file plan known. -- [ ] For recovery: liveness truth stack supports intervention. -- [ ] For destructive/risky actions: safety/policy/approval surfaces checked. -- [ ] Post-action verifier named before execution. - -## Output Specification - -- **Path:** structured robot state is emitted on `stdout`; durable evidence belongs under the bead-named `.agents/` path, never inside this skill or the NTM index. -- **Filename:** NTM creates no default report; when the surrounding arc requires one, use its declared filename or `<bead>-ntm-handoff.md`. -- **Format:** robot evidence is JSON or TOON; durable handoff is Markdown containing the action card, exact HEAD, snapshot/attention proof, reservations, and Agent Mail thread id. -- **Validation command:** run `skills/ntm/scripts/validate.sh` for this mesh contract; for a live action, verify `ntm --robot-snapshot` plus attention/mail/git/bead state. -- **Downstream handoff:** send the verified state and evidence on the existing Agent Mail thread to the named next role; acknowledgement and released/transferred reservations are the completion marker. - -## Quality Checklist - -- Operator choice is explicit: no NTM session or pawl service is started merely because the substrate exists. -- Pane roles remain separated, write scopes are disjoint, and every multi-writer handoff is acknowledged through Agent Mail. -- Deterministic evidence and a fresh-context `ao pawl review` verdict—not producer or pane consensus—control admission. -- Plain `REFUTED` work auto-repairs; only a tripped breaker gets one helper, and every lock or reservation is released or transferred. - -## Operating Notes (doctrine-critical facts) - -- **Project resolution is the #1 cross-tool breakage:** session name MUST equal the directory basename under `projects_base` (`NTM_PROJECTS_BASE`), or agent-mail/beads/reservations register under a different key than NTM sees. If tools "see different projects," fix this first. -- **Coordination default:** Agent Mail reservations are the primary primitive; `--worktrees` isolation is allowed when repo policy permits. If mail/reservations are degraded, record it and use bead assignee/status as the soft lock — no retry loops. -- **Cross-machine continuity** is checkpoint export/import or handoff bundles — never shipped cursors. -- **Safety surfaces are first-class:** use `ntm safety` / `ntm policy` / `ntm approve` (approve takes a *token*, not a bead id) instead of ad hoc shell habits; obey repo rules that route builds through `rch` or similar. -- **The standing pawl-service is NTM's largest persistent-pane consumer — tend it, don't nudge it.** `ao pawl up` spawns a session named `<repo>--pawl-service` (e.g. `agentops--pawl-service`), one warm reviewer pane per model family: `cc`=claude/opus (pane 1), `cod`=codex (pane 2), `agy`=Gemini (pane 3); it obeys the same `projects_base`-basename rule above. **Do NOT send keys to a pawl-service pane while a route is in flight** — the route loop deliberately never injects keys into a reviewing pane (a stray nudge breaks the verdict). Tend it read-only with `ao pawl doctor` / `ao pawl health`; bring it up with `ao pawl up` (`--dual` cc+cod is the default panel; `--tri` adds agy) and reap the idle account slot with `ao pawl reap`. Full contract: [`docs/contracts/pawls.md` §Operating the warm pawl-service](../../docs/contracts/pawls.md). The pawl panes are operator machinery — a plain `ao pawl review` needs no NTM at all (it runs cold from any git repo). -- The full distilled trip-wire list (CASS dedup blocking sends, `--` label separator, send-vs-spawn flag parsers, attention flag namespacing, PATH precedence for safety wrappers, and more) lives in [TROUBLESHOOTING.md](references/TROUBLESHOOTING.md). - -## Reference Index - -Keep depth out of this file. The binary's own docs (`ntm --robot-docs=...`) are the first stop for syntax; load these for operator-handbook detail: - -| Topic | Reference | -| --- | --- | -| `ntm send` deep reference (selectors, templates, CASS dedup, error modes) | [SEND.md](references/SEND.md) | -| `ntm spawn` deep reference (counts/variants, labels, worktrees, recipes, stagger) | [SPAWN.md](references/SPAWN.md) | -| Work intelligence & assignment (`ntm work *`, `ntm assign`, bv integration) | [WORK-AND-ASSIGN.md](references/WORK-AND-ASSIGN.md) | -| Ensemble mode (reasoning modes, presets, `--robot-ensemble-*`) | [ENSEMBLE.md](references/ENSEMBLE.md) | -| Pipelines (YAML schema, run IDs, resume/cancel, robot flags) | [PIPELINES.md](references/PIPELINES.md) | -| Serve API (auth modes, REST route map, OpenAPI, SSE) | [SERVE.md](references/SERVE.md) | -| Safety, policy, approvals (policy.yaml, tokens, what `safety install` drops) | [SAFETY.md](references/SAFETY.md) | -| Durability stack (checkpoint vs timeline vs handoff vs resume) | [DURABILITY.md](references/DURABILITY.md) | -| Integration surfaces (DCG, SLB, CAAM, RCH, mail, cass, quota) | [INTEGRATIONS.md](references/INTEGRATIONS.md) | -| Environment variables (`NTM_*`, `TOON_*`) | [ENV-VARS.md](references/ENV-VARS.md) | -| Troubleshooting (symptom / root cause / fix, full gotcha entries) | [TROUBLESHOOTING.md](references/TROUBLESHOOTING.md) | -| Self-test / trigger phrases | [SELF-TEST.md](references/SELF-TEST.md) | -| High-leverage command patterns, output capture, reusable assets | [COMMANDS.md](references/COMMANDS.md) | -| Attention feed, robot formats, wait conditions, full `--robot-*` index | [ROBOT-MODE.md](references/ROBOT-MODE.md) | -| Human dashboard, palette, keybindings, TUI notes | [DASHBOARD.md](references/DASHBOARD.md) | -| Project resolution, `projects_base`, config paths, project-local assets | [CONFIG.md](references/CONFIG.md) | - -### Assets - -Drop-in examples live under `assets/`: - -- [`pipeline-example.yaml`](https://github.com/boshu2/agentops/blob/main/skills/ntm/assets/pipeline-example.yaml) — a review pipeline with parallel step + retry -- [`policy-example.yaml`](https://github.com/boshu2/agentops/blob/main/skills/ntm/assets/policy-example.yaml) — opinionated `~/.ntm/policy.yaml` starter -- [`envrc.example`](https://github.com/boshu2/agentops/blob/main/skills/ntm/assets/envrc.example) — recommended `direnv`/shell env vars - -## Related Skills - -- **`agent-native`** — portable factory roles, worker lifecycle, bounded recovery, evidence, and retirement. -- `agent-mail` for inboxes, contact handshakes, and file reservations -- [`scripts/validate.sh`](scripts/validate.sh) for the operator-choice, factory-role, Agent Mail, and pawl mesh contract -- `br` for bead state changes and syncing -- `bv` for graph-aware task prioritization -- `cass` for prior-session retrieval -- `caam` for account rotation across providers (paired with `--robot-switch-account`) -- `dcg`, `slb` for destructive-command and two-person approval policy +External NTM documentation and examples remain the authority for command syntax; +this skill owns only the AgentOps boundary above. diff --git a/images/gemini/skills/operationalize/SKILL.md b/images/gemini/skills/operationalize/SKILL.md index 3223a929e..fcf797be8 100644 --- a/images/gemini/skills/operationalize/SKILL.md +++ b/images/gemini/skills/operationalize/SKILL.md @@ -1,235 +1,40 @@ --- name: operationalize -description: >- - Distill context (research, recon, learnings) into evidence-anchored rules routed to automation shapes. Triggers: when a finished artifact should become skills, gates, or beads. -practices: -- wiki-knowledge-surface -- design-by-contract -- pragmatic-programmer -hexagonal_role: domain -consumes: -- .agents/research/*.md -- pattern-mining.v1 -produces: -- .agents/operationalize/*.md -- routed-handoffs +description: 'Distill repeated, evidence-backed expertise into a proposed skill, check, reference, or workflow artifact. Triggers: "operationalize this", "turn this expertise into a reusable capability".' +practices: [continuous-learning, design-by-contract] +hexagonal_role: supporting +consumes: [evidence-backed-expertise] +produces: [operationalization-proposal.v1] context_rel: -- kind: customer-of - with: research -- kind: customer-of - with: automation-shape-routing -- kind: customer-of - with: validate -- kind: customer-of - with: pattern-mining - kind: supplier-to with: skill-builder - kind: supplier-to with: workflow-builder -- kind: supplier-to - with: cc-hooks -- kind: supplier-to - with: beads-br skill_api_version: 1 -user-invocable: false -context: - window: fork - intent: - mode: task - sections: - exclude: - - HISTORY - intel_scope: topic +user-invocable: true metadata: - tier: experimental - stability: experimental - dependencies: - - automation-shape-routing - - skill-builder - - workflow-builder - - cc-hooks - - beads-br - - validate -output_contract: .agents/operationalize/YYYY-MM-DD-<slug>.md rule packet + one handoff stub per routed rule + tier: meta + dependencies: [] + capabilities: [distill_expertise, propose_artifact_shape] + effects: [write_advisory_proposal] + canonical_status: canonical + disposition: keep_specialist +output_contract: operationalization-proposal.v1 --- -# /operationalize — Distill + Route Bridge +# Operationalize -> **Loop position:** move 7 (capture + ratchet) of the [operating loop](../../docs/architecture/operating-loop.md) — routes promoted learnings to their weakest durable enforcement (skill, gate, or bead). +Turn repeated, cited expertise into a proposal for a reusable artifact. -Rich context dies in the artifact that gathered it. A deep-research report, a -codebase-recon sweep, or a painful learning is read once, agreed with, and -never changes behavior again. This skill is the bridge: distill the artifact -into a handful of evidence-anchored rules, then route each rule to the -automation shape that will actually fire next time — skill, workflow, hook, -gate, beads, or playbook. +1. Require at least two distinct examples or one explicit authoritative source. +2. State the triggering situation, desired behavior, inputs, outputs, negative + examples, and evidence. +3. Choose the smallest fitting shape: reference, skill, deterministic check, or + caller-owned workflow. +4. Search existing capabilities and prefer extension over duplication. +5. Provide an activation example, holdout/negative example, owner, and rollback + or deletion condition. +6. Return the proposal to the caller or an authoring specialist. -**Use when:** "I gathered rich context — operationalize it." The input is a -finished artifact; the output is rules with anchors and a handoff per rule. - -## ⚠️ Critical Constraints - -- **Sources stay in place** — to prevent a corpus-curation detour. Name the - source artifacts by path; never copy them into a corpus directory. The - artifact you were handed IS the evidence base. -- **Every rule cites a source anchor,** because an unanchored rule is an - opinion wearing a rule's clothes — it cannot be audited, challenged, or - retired when the source is superseded. -- **Disagreement is marked DISPUTED, never averaged,** because splitting the - difference between conflicting sources produces a rule nobody measured. - A DISPUTED entry routes to investigation (beads), not to automation. -- **Shape and boundary are decided by routing, not by vibe** — to prevent everything - becoming a skill. Compose [/automation-shape-routing](../automation-shape-routing/SKILL.md) - for the shape decision; this skill only extends its target list. -- **Gates start warn-only,** because a fresh rule promoted straight to a - blocking gate ships its false positives as outages. Promotion to blocking - comes after the gate has run quietly on real traffic. -- **No rule survives without a counter-example check,** because the cheapest - time to find the case where the rule is wrong is before it is wired into - anything (see Step 5). - -## Execution - -### Step 1: Intake - -Name the source artifacts in place — absolute or repo-relative paths plus a -one-line provenance note each (who produced it, when, method). Confirm each -source has citable anchors (section IDs, finding IDs, line ranges); if not, add -anchor IDs to your *notes about* the source, never by editing the source. - -**Checkpoint:** every source is a named path with a provenance line. No corpus -dirs were created. - -### Step 2: Distill - -Extract candidate rules in the canonical form — **"When X, do Y because Z"** — -where Z cites at least one anchor. Work source by source, then reconcile: - -- Multiple sources agree → one rule citing all supporting anchors. -- Sources conflict → one **DISPUTED** entry naming both sides' anchors and - what evidence would settle it. Do not synthesize a compromise rule. -- A finding with no behavioral consequence → drop it (context, not a rule). - -**Checkpoint:** every rule line carries ≥1 anchor; every conflict became a -DISPUTED entry, not a blended rule. - -### Step 3: Route - -Hand each rule to [/automation-shape-routing](../automation-shape-routing/SKILL.md) -and extend its decision with this target table: - -| Route | Pick when the rule… | Emit target | -|---|---|---| -| **skill** | needs judgment at execution time | [/skill-builder](../skill-builder/SKILL.md) | -| **workflow** | is a deterministic multi-step sequence | [/workflow-builder](../workflow-builder/SKILL.md) | -| **hook** | must fire mechanically on a runtime event | [/cc-hooks](../cc-hooks/SKILL.md) | -| **gate** | should *check* outputs — start **warn-only** | a validation gate spec (warn-only first) | -| **beads** | is unsettled work or a DISPUTED investigation | [/beads-br](../beads-br/SKILL.md) | -| **playbook** | guides a human/operator decision, not an agent | `.agents/playbooks/` entry | - -**Checkpoint:** every rule has exactly one route; every DISPUTED entry routed -to beads. - -### Step 4: Emit - -Write the rule packet (Output Specification below), then create one handoff -stub per routed rule: the rule text, its anchors, the chosen route, and the -target skill invocation. The downstream builder owns the artifact; this skill -owns the rule and its evidence trail. - -### Step 5: Validate - -For each rule, run the counter-example check: actively search the sources (and -your own experience) for one case where following the rule would be wrong. A -found counter-example narrows the rule's "When X" or demotes it to DISPUTED. -Then request a [/validate](../validate/SKILL.md) verdict on the packet before -handing off — verify before any downstream builder consumes it. - -## Worked example (golden fixture) - -Input: [fixtures/research-excerpt.md](fixtures/research-excerpt.md) — a fake -deep-research excerpt on worker-lane retry behavior, anchors RX-1…RX-5. - -Distilled packet: - -1. **When a lane receives a rate-limit response, wait the full advertised - cooldown before any same-account retry,** because burst retries extend the - penalty window (RX-1) and sub-30-second retries re-failed in 84% of logged - events (RX-2). → route: **hook** (mechanical, event-triggered). -2. **When one same-account retry has already failed, rotate accounts before - the next attempt — capped at three accounts per hour,** because - post-failure rotation succeeded on the next call in 91% of cases (RX-2) - while sustained cycling beyond three accounts/hour risks account review - (RX-5). → route: **skill** (judgment about when the cap binds). -3. **DISPUTED — rotate unconditionally on the first rate-limit response.** - RX-4 (operator interview) asserts it; RX-5 flags rotation frequency itself - as a risk, and RX-2 only measured rotation *after* a failed retry. Settling - evidence: telemetry comparing first-response rotation vs post-failure - rotation. → route: **beads** (investigation), not automation. - -Note what did NOT happen: rules 1–3 were not averaged into "rotate fairly -quickly"; RX-3 (re-dispatch to a warm lane) was held back at Step 5 because -its own source records a 9% duplicate-work counter-example. - -## Output Specification - -**Format:** Markdown rule packet with sources-in-place, anchored "When X, do Y because Z" rules, DISPUTED entries, route table, validation evidence, and handoff stubs. -**Path:** `.agents/operationalize/YYYY-MM-DD-<slug>.md` with one `## Handoffs` block per routed rule. -**Filename:** `YYYY-MM-DD-<slug>.md`, where the slug names the source topic or operationalization goal. -**Validation command:** run `skills/operationalize/scripts/validate.sh`, then obtain a successful [/validate](../validate/SKILL.md) verdict on the emitted packet. -**Downstream handoff:** consumed only after validation by exactly one selected builder; each stub names the rule, anchors, route, target invocation, and next action. -**Exit signal:** packet path + per-rule route summary reported to the caller. - -## Quality Rubric - -- [ ] Every rule cites at least one stable source anchor -- [ ] Zero blended rules: every source conflict appears under DISPUTED -- [ ] Every rule has exactly one route, chosen via automation-shape-routing -- [ ] Any gate route is explicitly marked warn-only -- [ ] Counter-example check ran per rule and is recorded in the packet -- [ ] No source artifact was copied or moved; no corpus directory exists - -## Knowledge activation via ao knowledge (absorbed from /inject) - -`ao lookup` *retrieves* knowledge for the current session. **Activation** — absorbed -here from the retired `/inject` skill (lineage: the former `knowledge-activation` -skill, cp-auc) — *operationalizes* a mature `.agents` corpus into durable operator -surfaces (beliefs, playbooks, briefings, gaps). Where retrieval reads, activation -promotes; the two are the read and write-to-surface halves of the same flywheel. -Activation follows the live capture-and-ratchet workflow: - -1. `/postmortem` captures only evidence that changes future behavior. -2. `/pattern-mining` tests repeated shapes before promotion. -3. `/operationalize` routes an earned rule to its weakest durable mechanism. -4. **knowledge activation** lifts mature evidence into playbooks, beliefs, and runtime briefings. - -Activation owns corpus operationalization. Use it when the problem is no longer -"capture more knowledge" but promoting strong recurring claims, turning healthy -topics into reusable playbooks, building a small goal-time briefing, and -surfacing thin topics before they calcify. - -### Command contract - -The stable product surface is the `ao knowledge` command family (retrieval, -ranking, and decay are CLI-owned — no manual corpus walks): - -```bash -ao knowledge activate --goal "turn agents into usable information" # full outer loop -ao knowledge beliefs # refresh belief book only -ao knowledge playbooks # refresh candidate playbooks -ao knowledge brief --goal "fix auth startup" # goal-time briefing -ao knowledge gaps # thin topics, promotion gaps, weak claims, next work -``` - -`ao` owns the belief/playbook/brief/gap product surfaces directly; the skill owns routing, sequencing, interpretation, and next-step recommendations. `ao lookup` and `ao codex start` consume these outputs as operator context — matched briefings are the preferred dynamic startup surface, while selected beliefs and healthy playbooks provide bounded supporting guidance. When a retrieved briefing, belief, or playbook changes a recommendation, record it with `ao metrics cite "<path>" --type applied 2>/dev/null || true` (use `--type retrieved` for loaded-but-unused context). - -### Activation details - -Load [the activation DAG](references/knowledge-activation-dag.md) for ordered steps and trust gates, [output surfaces](references/knowledge-activation-output-surfaces.md) for consumer/substrate boundaries, and [script contracts](references/knowledge-activation-script-contracts.md) for builder ownership. The executable behavior remains specified by [knowledge-activation.feature](references/knowledge-activation.feature). - -## See Also - -- [automation-shape-routing](../automation-shape-routing/SKILL.md) — the shape decision this skill composes -- [skill-builder](../skill-builder/SKILL.md), [workflow-builder](../workflow-builder/SKILL.md), [cc-hooks](../cc-hooks/SKILL.md), [beads-br](../beads-br/SKILL.md) — emit targets -- [validate](../validate/SKILL.md) — the packet verdict before handoff -- [research](../research/SKILL.md) — typical upstream producer of the input artifact +Operationalize does not create tracker work, promote policy, start a factory, +validate its own output, or control another invocation. diff --git a/images/gemini/skills/pattern-mining/SKILL.md b/images/gemini/skills/pattern-mining/SKILL.md new file mode 100644 index 000000000..510b62222 --- /dev/null +++ b/images/gemini/skills/pattern-mining/SKILL.md @@ -0,0 +1,115 @@ +--- +name: pattern-mining +description: 'Test repeated implementation shapes against independent exemplars and a holdout before routing an earned abstraction. Triggers: "mine a recurring code pattern", "is this abstraction earned", "extract invariants from implementations".' +practices: +- design-patterns +- legacy-code-seams +- design-by-contract +hexagonal_role: supporting +consumes: +- repo-context +- task-question +produces: +- pattern-mining.v1 +context_rel: +- kind: customer-of + with: research +- kind: customer-of + with: validate +- kind: supplier-to + with: operationalize +skill_api_version: 1 +user-invocable: true +context: + window: fork + intent: + mode: task + sections: + exclude: + - HISTORY + intel_scope: topic +metadata: + capabilities: [pattern_mining] + effects: [] + canonical_status: canonical + disposition: keep_specialist + tier: execution + dependencies: [] +output_contract: pattern-mining.v1 JSON validated by skills/pattern-mining/scripts/validate-output.sh +--- + +# Pattern Mining + +Decide whether repeated code demonstrates a reusable rule or only a plausible +hypothesis. Similar names and syntax are not enough; the abstraction must +survive examples it was not designed around. + +## Constraints + +- To prevent lineage copies from faking recurrence, use independently + implemented exemplars with repository anchors. +- Because the candidate must generalize, form it without seeing the holdout and + back-apply every holdout-driven refinement. +- To keep weak evidence from becoming architecture, route hypotheses to + `no-action`; only a fully proven promotion may reach `operationalize`. + +## Workflow + +1. State the candidate pattern and collect independently implemented + exemplars with repository anchors. Use `research` when coverage is unclear. +2. From the exemplars, separate required invariants, legitimate variation + points, and incidental similarity. +3. Require at least three distinct exemplars before promotion is possible. + Form the candidate abstraction without using the holdout. +4. Test it against every exemplar, then a separate holdout. Back-apply the + refined abstraction to the original exemplars so the holdout fix cannot + silently break them. +5. Emit `outcome: promote` only when the exemplar floor, holdout, and + back-application all pass. Route that evidence to `operationalize`, which + decides whether the eventual shape is a skill, gate, library, template, or + no action. +6. Otherwise emit `outcome: hypothesis` with `route: no-action`. Keep the + evidence bounded and name what additional observation would retest it. + +## Output Specification + +- **Artifact directory:** `.agents/patterns/<run-id>/` +- **Filename convention:** `pattern-mining.json` +- **Format:** `pattern-mining.v1` JSON containing the outcome, distinct + exemplars, invariants, variations, incidental details, holdout result, + back-application result, and route. +- **Validation command:** `skills/pattern-mining/scripts/validate-output.sh <pattern-mining.json>` +- **Downstream handoff:** pass a validated `promote` artifact to + `operationalize`; retain a validated `hypothesis` artifact as bounded + evidence with `route: no-action`. + +Promotion requires at least three distinct exemplars, one separate passing +holdout, successful back-application, and at least one invariant. Any weaker +packet remains a hypothesis and cannot route to reusable packaging. + +The validator is the machine boundary: + +```bash +skills/pattern-mining/scripts/validate-output.sh <pattern.json> +``` + +This skill owns evidence for the pattern. It never creates the reusable +artifact itself and never promotes a failed or untested holdout. + +Executable behavior: +[references/pattern-mining.feature](references/pattern-mining.feature). + +## Quality + +- Exemplars are independent and repository-anchored; copied implementations do + not inflate the evidence floor. +- Invariants, legitimate variations, and incidental similarities stay distinct + through holdout testing and back-application. +- The named validator passes before a promotion reaches `operationalize` or a + hypothesis is retained as `no-action` evidence. + +## Do not + +- Count copies from one implementation lineage as independent exemplars. +- Hide variation by calling it incidental. +- Route a hypothesis directly to a skill, rule, gate, or library. diff --git a/images/gemini/skills/plan/SKILL.md b/images/gemini/skills/plan/SKILL.md index c3558cc13..377b59625 100644 --- a/images/gemini/skills/plan/SKILL.md +++ b/images/gemini/skills/plan/SKILL.md @@ -1,215 +1,64 @@ --- name: plan -spine: true -description: 'Decompose goals into issue plans. Triggers: "plan", "decompose goals into issue plans.", "plan skill".' +description: 'Shape intent into one behavior-first PlanPacket with acceptance, evidence, and write scope. Triggers: "plan", "discover and plan", "shape this goal".' practices: -- adr -- agile-manifesto -- pragmatic-programmer +- bdd-gherkin +- design-by-contract +- ddd-bounded-context hexagonal_role: domain -consumes: -- standards +consumes: [] produces: -- .agents/plans/*.md -- execution-packet.json -context_rel: -- kind: shared-kernel - with: standards +- plan-packet.v1 +context_rel: [] skill_api_version: 1 +user-invocable: true metadata: graph_root: true tier: execution - dependencies: - - research - - beads-br - - premortem - - crank - - implement - - scope - - dueling-idea-genies -context: - window: fork - intent: - mode: task - intel_scope: topic -output_contract: .agents/plans/YYYY-MM-DD-*.md, beads (via ao beads exec create) + dependencies: [] + capabilities: [shape_intent, define_acceptance, bound_write_scope] + effects: [write_plan_packet] + canonical_status: canonical + disposition: keep +output_contract: schemas/plan-packet.v1.schema.json --- -# Plan Skill -Decompose intent into behavior-sized, issue-ready slices with dependency waves, -file ownership, and executable acceptance. Execute the workflow; do not merely -describe it. Small local changes under roughly 200 LOC may plan in chat, but a -non-trivial plan must be durable and self-contained. +# Plan -## Critical Constraints - -- **Why: avoid stale scope.** Verify inherited or older bead citations against - HEAD before decomposition; do not plan from unchecked goal-design packets. -- **Why: preserve behavior.** Each slice delivers one Given/When/Then behavior; - separate refactors from feature slices. -- **Why: prevent write collisions.** Same-wave writers must have disjoint file - ownership, including generated companions, manifests, fixtures, and docs. -- **Why: keep acceptance executable.** Every bead carries `## Scenarios` above - a fenced `acceptance_criteria` YAML block with runnable evidence checks. -- **Why: prevent rediscovery.** Cite real paths, symbols, signatures, tests, and - reuse points; verify inventory claims at plan time and again when consumed. -- **Why: preserve intent.** Keep WHAT and HOW separated; planning does not - implement, and `--auto` skips approval only when explicitly selected. -- **Why: avoid false novelty.** Search existing skills and the `ao` surface - before scoping a new capability; record hits as reuse, not new work. -- **Why: keep WIP pullable.** Goal and epic parents are aggregate demand, never writer WIP; keep one active leaf per writer and release it only after its terminal report. -- **Why: make propagation finite.** A migration, rename, deletion, or ownership - transfer requires a command-bound [authority/consumer manifest](references/authority-consumer-manifest.md) - covering executable authorities, symbols, consumers, tests, generated - companions, runtime projections, evidence, and rollback before a slice is - admitted. The checker consumes independently captured inventory output as - data; it never executes a command string from the manifest. - -## Inputs and Boundary - -`/plan <goal> [--auto|--fast-path|--deep|--skip-symbol-check|--skip-audit-gate]` -consumes BDD intent, a bead, research, or a goal-design packet through the -`plan_slices` inbound port. It produces a slice plan, issue graph, file matrix, -and validation packet through `persist_issue`, `verify_symbols`, -`retrieve_context`, and `seed_execution_packet`. - -When input contains `intent.md` and `driver.md`, run -`scripts/check-goal-design-packet.sh <packet-dir>`. Preserve candidate behavior -and scenario IDs; map `first_failing_proof`, `write_scope`, and `close_signal` -into acceptance; carry non-goals, rollback, and hard rules into boundaries. +Turn the caller's intent into one bounded, testable behavior. Planning owns +research necessary to understand the behavior, acceptance shaping, and scope. +It does not schedule, claim, assign, implement, validate, or decide readiness. ## Workflow -1. **Stale-scope pre-flight.** For inherited, full-complexity, reopened, or - older-than-seven-day beads, run `ao beads verify <id>` first. Stop on stale - citations until scope is revalidated. -2. **Load context.** Read prior research and run `ao search`/`ao lookup`. Load - `.agents/planning-rules/*.md` first, then active findings from - `.agents/findings/registry.jsonl`. Every plan includes `Applied findings:` - with IDs or `none` and explains how retrieved rules changed the plan. -3. **Choose strategic review.** For multi-session work with a contested - operator default, recommend `dueling-idea-genies` and consume its - `idea-challenge.v1` packet as advisory input. Plan may accept, reject, or - combine it; the packet never decides readiness. -4. **Explore only as needed.** Inspect the codebase or use a bounded Explore - agent for file inventory, exact symbols/signatures, reuse points with - `file:line`, tests, imports, and conventions. -5. **Baseline mechanically.** Record commands and counts for files, LOC, - sections, tests, fixtures, schemas, and size limits. Search `ms search` - when available, otherwise `skills/**/SKILL.md`, `docs/SKILLS.md`, and `ao`. - For a migration-shaped objective, write and check the authority/consumer - manifest now; an incomplete inventory fails closed. -6. **Scale detail.** Minimal for 1-2 simple issues, Standard for 3-6, Deep for - 7+, broad refactors, full complexity, or `--deep`. -7. **Decompose by behavior.** Give each issue a title, scenario, owned files, - dependencies, test levels proportional to risk, and mechanical conformance - checks. Custom rubrics name their `agent_judge`. -8. **Compute waves.** Topologically group independent issues. Pull only one - vertical leaf into each writer's WIP slot. For migrations, use the checked - manifest result: only `disjoint` may be proposed as parallel, while `shared` - or `incomplete` is serialized. Include tests, docs, schemas, fixtures, - runtime copies, Codex companions, parity manifests, and hash markers. -9. **Build matrices.** Produce the file dependency matrix, file-conflict - matrix, cross-wave shared-file registry, owner and discard path per slice. - Default to sequential when wave validity is uncertain. -10. **Write the plan.** Use the canonical template and baseline gate in - [plan-document-template.md](references/plan-document-template.md). Mark the - result INCOMPLETE when a Planning Rules Compliance justification is empty. -11. **Create tracking.** Prefer br issues with scenarios, validation blocks, - and `blocks` edges. Run the scenario admission and post-creation validation - gates from [task-creation.md](references/task-creation.md). If br is absent, - keep the markdown plan as the durable handoff. -12. **Freeze and report.** Unless `--auto`, request operator confirmation before - declaring planning complete. Report plan path and SHA-256, issue count/IDs, - waves, and the next route through one exact-plan `/premortem` verdict, then - `/crank`. Record `ao ratchet record plan` when available. +1. Restate the intent and choose one active behavior. +2. Inspect only enough real context to make paths, interfaces, and evidence + concrete. Existing research and specialist skills are advisory inputs. +3. Write at least one normal and one edge Given/When/Then scenario. +4. Name non-goals and the evidence required to judge every criterion. +5. Declare `write_scope.include` and `write_scope.exclude`, including generated + companions. Scope describes permitted subject content; it grants no file + ownership or delivery authority. +6. Name `first_acceptance_check` as either an executable command or an artifact + path. Commands are data for the implementer, never executed by a packet + parser. +7. Canonically serialize `plan-packet.v1` and compute its SHA-256 digest. -## Required Plan Sections +Optional decomposition may describe smaller behaviors, but it carries no owner, +ready, claim, priority, attempt, wave, queue, lease, admission, next-action, +close, release, or delivery fields. -- context, intent issue, and applied findings -- boundaries, non-goals, rollback, and files to modify -- baseline audit with commands and results -- one slice per scenario with first failing proof and owned write scope -- issue descriptions with scenarios and acceptance criteria -- dependency waves and file dependency/conflict matrices -- planning rules compliance and cross-wave shared-file registry -- verification commands, cleanup, discard paths, and next steps +## Required output -Detailed decomposition, implementation detail, and matrices live in the -references below; load only the modules required by the selected complexity. +The packet conforms to [`schemas/plan-packet.v1.schema.json`](../../schemas/plan-packet.v1.schema.json) +and contains: -## Output Specification +- intent and acceptance digests; +- one active behavior; +- normal and edge scenarios; +- non-goals and required evidence; +- inclusive and exclusive write scope; +- one first acceptance check; +- optional advisory decomposition. -- **Path:** `.agents/plans/YYYY-MM-DD-<goal-slug>.md`; optional durable issues - go to the repository's resolved br ledger. -- **Filename:** the filename convention is ISO date plus a stable goal slug; - never overwrite an unrelated plan. -- **Format:** Markdown using the canonical template, embedded Gherkin, fenced - YAML `acceptance_criteria`, issue IDs, dependency edges, and file matrices. -- **Validation command:** run `bash skills/plan/scripts/validate.sh`, relevant - scenario/validation admission checks, and verify every cited symbol/path. -- **Downstream handoff:** the exact path and SHA-256 go first to `/premortem`; - after its binary PASS, the plan is consumed by `/crank`, `/implement`, and - future agents without relying on chat-only context. - -Report: - -```text -Plan: <path> -Issues: <count and IDs> -Waves: <ordered groups or sequential> -Validation: <PASS|WARN|FAIL with commands> -Assumptions: <verified facts and unresolved risks> -Next: </premortem, /crank, or revision> -``` - -## Quality Rubric - -- **Self-contained:** a fresh implementer can act without rediscovery or chat. -- **Behavioral:** slices and acceptance map directly to observable scenarios. -- **Grounded:** load-bearing claims, symbols, counts, and reuse points are cited. -- **Conflict-safe:** waves have explicit ownership and no unresolved collisions. -- **Executable:** every acceptance row names a check and evidence surface. -- **Right-sized:** detail and test levels match complexity and blast radius. -- **Honest:** missing research, stale scope, or incomplete justification is - reported as WARN/FAIL rather than hidden. - -## Examples - -**User says:** `/plan "add rate limiting"` - -Produce a grounded inventory, behavior-sized issues, validation checks, and a -collision-safe wave order; write the durable plan before reporting done. - -**User says:** `/plan --auto .agents/research/auth.md` - -Use the research, create the plan and tracker graph when available, skip only -the approval prompt, and hand off to the next loop move. - -## Troubleshooting - -| Problem | Response | -|---|---| -| br is unavailable | Write the markdown plan and report issue creation skipped | -| Research lacks symbols | Explore until paths, signatures, and tests are verified | -| Same file has parallel writers | Serialize or merge the affected slices | -| Baseline evidence is missing | Mark INCOMPLETE unless the documented opt-out applies | - -## References - -- [pre-decomposition.md](references/pre-decomposition.md) — research, prevention, exploration, baseline -- [decomposition.md](references/decomposition.md) — issue and acceptance contracts -- [implementation-detail.md](references/implementation-detail.md) — symbol-level plan detail -- [detail-templates.md](references/detail-templates.md) — Minimal/Standard/Deep shapes -- [plan-document-template.md](references/plan-document-template.md) — canonical artifact -- [task-creation.md](references/task-creation.md) — br creation and admission gates -- [wave-matrices.md](references/wave-matrices.md) — ownership and conflict matrices -- [authority-consumer-manifest.md](references/authority-consumer-manifest.md) — checked propagation inventory and disjoint/shared/incomplete classification -- [planning-rules.md](references/planning-rules.md) — PR-001 through PR-011 -- [plan-mutations.md](references/plan-mutations.md) -- [complexity-estimation.md](references/complexity-estimation.md) -- [examples.md](references/examples.md) -- [plan-to-beads-workflow.md](references/plan-to-beads-workflow.md) -- [sdd-patterns.md](references/sdd-patterns.md) -- [templates.md](references/templates.md) -- [plan.feature](references/plan.feature) +Report the packet location and digest, then stop. diff --git a/images/gemini/skills/postmortem/SKILL.md b/images/gemini/skills/postmortem/SKILL.md index 674edbb8a..0fc86b498 100644 --- a/images/gemini/skills/postmortem/SKILL.md +++ b/images/gemini/skills/postmortem/SKILL.md @@ -1,27 +1,24 @@ --- name: postmortem -description: 'Test an explicit retrospective causal question against evidence and counterfactuals after Validate and Learn. Triggers: "postmortem", "causal retrospective", "test a retrospective hypothesis".' +description: 'Optionally test a retrospective causal question against durable verdict evidence. Triggers: "postmortem", "causal retrospective", "test a retrospective hypothesis".' practices: - sre - lean-startup hexagonal_role: domain consumes: -- learn -- toil-mining +- verdict.v2 produces: - postmortem-report.md -context_rel: -- kind: customer-of - with: learn -- kind: customer-of - with: toil-mining +context_rel: [] skill_api_version: 1 user-invocable: true metadata: + capabilities: [postmortem] + effects: [] + canonical_status: canonical + disposition: keep_strategy tier: judgment - dependencies: - - council - - toil-mining + dependencies: [] context: window: fork intent: @@ -41,7 +38,7 @@ output_contract: skills/postmortem/references/postmortem.feature ## Critical Constraints - Because proof and causal inference are different judgments, Postmortem is retrospective causal analysis, not the general learning umbrella and not a completion gate. -- It consumes an immutable Validate verdict plus Learn receipt and does not re-run acceptance validation by default because Validate already owns that proof. +- It consumes immutable Validate verdict evidence and does not re-run acceptance validation because Validate already owns that proof. - Treat causal statements as hypotheses because causal confidence must survive alternatives. Separate observed sequence, contributing conditions, counterfactuals, and unknowns. @@ -55,7 +52,7 @@ output_contract: skills/postmortem/references/postmortem.feature ## Workflow -1. Pin the verdict, Learn receipt, delivered artifact, and explicit causal +1. Pin the verdict, subject evidence, and explicit causal question. 2. Reconstruct the evidence-backed timeline without importing hidden author reasoning as fact. @@ -74,7 +71,7 @@ output_contract: skills/postmortem/references/postmortem.feature - **Serialization/schema format:** Markdown with causal question, pinned inputs, timeline, hypotheses, evidence, counterfactuals, unknowns, and experiments. - **Validator command:** `bash skills/postmortem/scripts/validate.sh`. -- **Downstream handoff:** Learn or the orchestrator may consume the analysis; they own +- **Downstream handoff:** Learn or the caller may consume the analysis; they own any bookkeeping, promotion, planning, or delivery decision. ## Quality Checklist diff --git a/images/gemini/skills/premortem/SKILL.md b/images/gemini/skills/premortem/SKILL.md index 895f9616d..f6ebf07f9 100644 --- a/images/gemini/skills/premortem/SKILL.md +++ b/images/gemini/skills/premortem/SKILL.md @@ -1,134 +1,55 @@ --- name: premortem -spine: true -description: 'Use when: an exact plan needs a verdict. Stress-tests readiness before work with one fresh, independent judge.' -practices: -- adr -- mythical-man-month -- design-by-contract +description: 'Optionally challenge a frozen plan with one fresh independent judge before implementation. Triggers: "premortem", "challenge this plan", "what could make this plan fail".' +practices: [design-by-contract, adr] hexagonal_role: domain -consumes: -- standards -produces: -- premortem-plan-verdict.v1 +consumes: [plan-packet.v1] +produces: [premortem-plan-review.v1] context_rel: -- kind: shared-kernel - with: standards +- kind: supplier-to + with: plan skill_api_version: 1 +user-invocable: true metadata: + capabilities: [challenge_plan] + effects: [write_advisory_plan_review] + canonical_status: canonical + disposition: keep_strategy graph_root: true tier: judgment - dependencies: - - council -context: - window: fork - intent: - mode: task - sections: - exclude: - - HISTORY - intel_scope: full -output_contract: skills/premortem/schemas/plan-verdict.schema.json + dependencies: [] +output_contract: schemas/premortem-plan-review.v1.schema.json --- -# Premortem Skill -> **Question:** Is this exact plan ready to implement? -> **Boundary:** Premortem owns the only semantic plan-readiness verdict. +# Premortem -## Constraints +Premortem is an optional plan-challenge strategy. It asks one fresh context to +identify concrete ways a frozen PlanPacket could fail before implementation. +It is not part of the required RPI sequence and does not authorize readiness. -- Judge the plan, never the implementation or delivery mechanism. -- Use one fresh-context judge with `author_id != judge_id`. Model and family - metadata are optional; no risk class requires different model families. -- Bind the verdict to the repository-relative plan path and its SHA-256. Any - plan edit invalidates the verdict. -- Emit exactly `PASS` or `FAIL`. `PASS` has zero blockers. `FAIL` contains the - complete nonempty blocker set in one response. -- Report only concrete, evidence-bound defects that invalidate acceptance, - correctness, safety, dependencies, scope, or a claimed contract. -- Do not own retries, attempt maps, budgets, helper state, implementation, - delivery, tracker closure, or operator escalation. The orchestrator chooses - repair or replanning after reading the verdict. -- A council, mixed panel, or Dueling Idea Genies artifact may inform the judge, - but none substitutes for this exact-plan verdict. +## Workflow -## Loop position +1. Pin the PlanPacket digest, acceptance, non-goals, evidence requirements, and + declared write scope. +2. Use one fresh judge with a context ID distinct from the plan author. +3. Test acceptance completeness, edge behavior, scope, dependencies, + reversibility, and evidence shape against cited repository facts. +4. Return one complete set of concrete findings and checked/not-checked scope. +5. Stop. The caller decides whether to revise the plan or invoke RPI. -Premortem runs once after Plan freezes the final plan and before the first -implementation leaf is pulled. It consumes the plan plus its acceptance, -dependency graph, write scopes, non-goals, rollback, and deterministic planning -receipts. It produces one immutable `premortem-plan-verdict.v1` JSON artifact. +Council or Dueling Idea Genies may be caller-supplied evidence, but Premortem +does not require either strategy and cannot turn consensus into approval. -Between implementation waves, reuse the verdict while the exact plan digest is -unchanged. A materially changed plan requires an explicit orchestrator request -for a new Premortem verdict; Validate and Learn cannot invoke Premortem themselves. +## Boundary -## Execution +- Emit advisory findings, not `verdict.v2`, readiness, admission, or permission. +- Do not implement, validate the candidate, retry, repair, schedule, claim, + change acceptance, operate Git, close work, release, or deliver. +- Any plan edit creates a new subject for a later caller-initiated Premortem. -1. Resolve one current plan path. Reject a missing or stale plan rather than - inferring intent from chat. -2. Compute the plan SHA-256 and record the plan author identity. -3. Retrieve only directly matched compiled prevention from - `.agents/premortem-checks/*.md`, falling back to - `.agents/findings/registry.jsonl`. Missing inputs skip silently; malformed - entries are ignored with one concise warning. -4. Dispatch one runtime-native fresh judge. Use Council only when the operator - explicitly requests a panel or the decision is genuinely contested. -5. Check all applicable acceptance, dependency, write-scope, migration, - reversibility, test-shape, capability-reuse, and rollback claims. The - detailed checklist is in - [mandatory-checks.md](references/mandatory-checks.md). -6. Return the complete blocker set once. Cosmetic, theoretical, pre-existing, - and out-of-scope observations are notes, not blockers. -7. Write the JSON verdict and validate both its schema and live plan digest: +## Output - ```bash - skills/premortem/scripts/validate-output.sh \ - .agents/council/YYYY-MM-DD-premortem-<topic>.json \ - "$(git rev-parse --show-toplevel)" - ``` - -## Verdict contract - -```json -{ - "schema_version": "premortem-plan-verdict.v1", - "plan": {"path": ".agents/plans/example.md", "sha256": "<64 hex>"}, - "author_id": "planner-context", - "judge_id": "fresh-judge-context", - "verdict": "PASS", - "blockers_complete": true, - "blockers": [] -} -``` - -For `FAIL`, each blocker has a stable `id`, a concrete `claim`, and one or more -`evidence` references. Optional `author_model` and `judge_model` objects may -record `name` and `family`; the validator deliberately does not compare family. - -## Output Specification - -- **Artifact path:** `.agents/council/YYYY-MM-DD-premortem-<topic>.json` -- **Schema:** [plan-verdict.schema.json](schemas/plan-verdict.schema.json) -- **Validator:** `skills/premortem/scripts/validate-output.sh <verdict> <repo-root>` -- **Downstream handoff:** `PASS` permits the orchestrator to pull the first - implementation leaf. `FAIL` returns the complete evidence set to the - orchestrator for one consolidated repair decision or replanning. - -## Quality checklist - -- The recorded plan digest matches the file the judge actually read. -- Author and judge identities differ. -- The verdict is binary and the blocker set is explicitly complete. -- Every blocker cites the plan or a deterministic evidence path. -- No optional review topology is presented as readiness authority. - -## References - -- [mandatory-checks.md](references/mandatory-checks.md) -- [premortem.feature](references/premortem.feature) -- [scope-mode.md](references/scope-mode.md) -- [temporal-interrogation.md](references/temporal-interrogation.md) -- [examples.md](references/examples.md) -- [write-premortem-output.md](references/write-premortem-output.md) -- [compiled-prevention.md](references/compiled-prevention.md) +Return `premortem-plan-review.v1` with the plan digest, author and judge context +IDs, findings, evidence references, `checked`, and `not_checked`. An empty +finding set means only that this optional challenge found no concrete defect; +it is never a lifecycle gate. diff --git a/images/gemini/skills/product/SKILL.md b/images/gemini/skills/product/SKILL.md new file mode 100644 index 000000000..8f326dfe8 --- /dev/null +++ b/images/gemini/skills/product/SKILL.md @@ -0,0 +1,36 @@ +--- +name: product +description: 'Create or refine PRODUCT.md while separating evidence, aspiration, users, value, and non-goals. Triggers: "product", "create PRODUCT.md", "product boundary".' +practices: [lean-startup, ddd-bounded-context] +hexagonal_role: domain +consumes: [] +produces: [PRODUCT.md] +context_rel: +- kind: supplier-to + with: plan +skill_api_version: 1 +user-invocable: true +metadata: + tier: product + dependencies: [] + capabilities: [shape_product_boundary] + effects: [write_product_document] + canonical_status: canonical + disposition: keep_specialist +output_contract: PRODUCT.md +--- + +# Product + +Create or refine a product contract with the user's authority. + +1. Inspect existing product, README, goals, release, and evidence sources. +2. Ask only for decisions that cannot be grounded safely from those sources. +3. State mission, users, pains, value, differentiation, non-goals, proven facts, + assumptions, evidence gaps, and success signals. +4. Label aspiration and measurement honestly. +5. Preserve an existing PRODUCT.md unless the user authorizes replacement. +6. Return the document to the caller; Plan may use it as intent context. + +Product does not select work, invoke a loop, repair itself, validate itself, or +choose delivery. diff --git a/images/gemini/skills/rch/SKILL.md b/images/gemini/skills/rch/SKILL.md index 836d93dc2..191155fc7 100644 --- a/images/gemini/skills/rch/SKILL.md +++ b/images/gemini/skills/rch/SKILL.md @@ -3,195 +3,52 @@ name: rch user-invocable: false skill_api_version: 1 hexagonal_role: supporting +consumes: [] +produces: [] +context_rel: [] metadata: + dependencies: [] + capabilities: [rch] + effects: [] + canonical_status: canonical + disposition: keep_specialist tier: execution -description: 'Use when offloading slow builds to remote workers or recovering RCH worker, hook, SSH, sync, or disk issues. Triggers: "rch", "use when offloading slow builds", "rch skill".' +description: 'Use RCH once to offload a build or collect remote-compilation diagnostics.' practices: - pragmatic-programmer --- -# RCH — Remote Compilation Helper +# RCH — remote compilation specialist -`rch` transparently offloads compilation commands to remote workers via a Claude Code PreToolUse hook. The daemon picks the fastest healthy worker, rsync's the workspace, runs the build, syncs artifacts back, and exits with the worker's exit code. +RCH can offload one explicit compilation command or inspect the remote compiler +path. This skill reports what happened; it does not govern retries or repair. -This skill is the operational layer agents use when something about that pipeline isn't working — and the much more common case where it *thinks* it's working but is silently falling back to local execution. The skill is built around a single principle: **self-resolve before asking the human.** Every recovery path here is one the agent can run on its own. +## Procedure -## Constraints +1. Capture `rch check`, `rch doctor --json`, worker status, and the relevant + `[RCH]` summary before mutation. +2. For diagnosis, identify the first failing stage: availability, configuration, + hook, classification, sync, remote compile, or worker pressure. +3. Run only the caller-authorized command or documented safe diagnostic once. +4. Capture the exact command, worker when known, exit code, local-fallback reason, + and post-action status. +5. Stop and return the evidence. -- **Observe before mutation.** Capture the `[RCH]` summary, `rch doctor --json`, and the first failing triage stage before changing daemon, hook, worker, or disk state because an unobserved repair destroys the evidence needed to prove causality. -- **Fail-open is not success.** Treat every `[RCH] local (<reason>)` result as unresolved until the named reason is repaired or deliberately accepted because a green local build does not prove remote offload worked. -- **Bound autonomous repair.** Apply only the documented idempotent or precondition-guarded fixes in this skill, and preview destructive cleanup because remote workers may contain active builds or another agent's state. -- **Consult the pawl before raising the andon.** WARN, FAIL, or REFUTED contract results repair and rerun automatically because ordinary rejection is diagnostic evidence; only a breaker may enter HOLD or consume the single helper lane. +`[RCH] local (...)` means the requested remote-offload claim was not proved even +when the local build succeeds. Destructive cleanup, worker deployment, daemon +configuration, and remote mutation require explicit caller authority. -## Breaker State Machine +## Output -- **Ordinary rejection — `WARN|FAIL|REFUTED -> AUTO-REDO`:** repair the named defect, recapture evidence, and rerun the failed check; plain rejection never enters HOLD and never consumes the helper lane. -- **Breaker — `BREAKER -> HOLD -> ONE-HELPER`:** when recovery cannot proceed safely with available permissions or observability, freeze mutations and route exactly one bounded helper consultation with the evidence packet. -- **Recovered — `HELPER-UNSTUCK -> AUTO-REDO`:** leave HOLD, apply the bounded recovery, and re-earn the deterministic check plus pawl verdict before declaring the pipeline healthy. -- **Helper escalation — `HELPER-ESCALATE -> HUMAN`:** stop automation and send the helper-provided concrete escalation packet to the operator. -- **Direct human lane — `REFUSAL-LANE|EXPLICIT-JUDGMENT|EXHAUSTED-BUDGET -> HUMAN`:** skip the helper and route directly to the operator; these are the only direct-human states. +Return a factual packet with status (`remote`, `local_fallback`, `failed`, or +`not_proven`), commands and exit codes, worker, summary line, and checked/not +checked surfaces. Do not include a next action. -**Don't re-learn the command surface here.** `rch --help`, `rch doctor`, and the machine surfaces (`--json`, `--schema`, `--help-json`, `--capabilities` — see [MACHINE_INTROSPECTION.md](references/MACHINE_INTROSPECTION.md)) self-describe every subcommand, flag, and env var. Env-var knobs and config precedence: [CONFIGURATION.md](references/CONFIGURATION.md). This skill carries the triage doctrine and recovery playbook routing. +## References -Tested against rch v1.0.18; concepts apply to v1.0.16+. - ---- - -## Read This First - -When a build feels slow, run **one** thing: - -```bash -RCH_VISIBILITY=verbose <your-command> 2>&1 | grep -E '^\[RCH\]' -``` - -The summary line is a contract: - -| Pattern | What to do | -|---|---| -| `[RCH] remote <worker> (...)` | Healthy. Done. | -| `[RCH] remote <worker> failed [RCH-Exxx] ...` | Real build/env failure. See [ERROR_CODES.md](references/ERROR_CODES.md). | -| `[RCH] local (<reason>)` | **Fail-open.** See [FAIL_OPEN.md](references/FAIL_OPEN.md) and look up the reason verbatim. | -| *no `[RCH]` line at all* | Hook didn't fire. Run `scripts/protocol_test.sh "<your-command>"`. | - -If you can't see why offload isn't happening, **prove the path works in isolation** before doing anything else: - -```bash -rch exec -- env CARGO_TARGET_DIR="${TMPDIR:-/tmp}/rch_target_$(basename "$PWD")" cargo check --workspace --all-targets -``` - -If that prints `[RCH] remote <worker> (...)`, the offload pipeline is healthy. The problem is upstream of `rch exec` — usually the hook classifier or the agent's invocation form. If it also fails, follow [RECOVERY_PLAYBOOKS.md](references/RECOVERY_PLAYBOOKS.md). - ---- - -## Fast Triage Order - -Run in this order and stop at the first failing stage: - -1. **Availability** — `rch check`, `rch status --workers --jobs`, `rch workers probe --all`, `rch queue` -2. **Config + socket consistency** — `rch config show --sources`, `rch --json config get general.socket_path`, `rch --json daemon status` -3. **Hook integration** — `rch hook status`, `rch agents status`, `rch hook install` (idempotent) -4. **Command classification + path closure** — `rch diagnose --dry-run "<your-command>"` -5. **Remote compile proof** — the `rch exec` probe above -6. **If sync fails or storage looks bad, inspect the worker directly:** - -```bash -ssh ubuntu@<host> 'df -h / /tmp && free -h && cat /proc/pressure/memory && cat /proc/pressure/io' -ssh ubuntu@<host> 'du -sh /tmp/rch-* /tmp/rch_target_* 2>/dev/null | sort -h' -``` - -Always check both `/` and `/tmp` on the worker before deciding what to fix. End-to-end verify: `rch self-test --all`; comprehensive checks: `rch doctor` (`--fix --dry-run` previews auto-fixes). - -**Checkpoint:** before the first mutation, record the failing stage, exact command, exit code, and relevant `[RCH]` summary in the run evidence; after repair, rerun that same probe so the before/after claim is falsifiable. - ---- - -## Quick Fixes - -| Symptom | Command | -|---------|---------| -| Hook not installed | `rch hook install && rch hook status` | -| Daemon not running | `rch daemon start` | -| Daemon version drift / stale socket state | `rch daemon restart -y` (drains gracefully — safe by default) | -| No workers configured | `rch workers discover --add --yes && rch workers setup --all` | -| Workers unreachable | `rch workers probe --all`, fix SSH key/host — or [SSH_KEY_RECOVERY.md](references/SSH_KEY_RECOVERY.md) | -| All workers busy + fail-open | Queueing is default-on; bump `RCH_DAEMON_WAIT_RESPONSE_TIMEOUT_SECS=120` or raise `total_slots` | -| Transfer churn under target dirs | Add excludes in `~/.config/rch/config.toml`, then `rch daemon reload` | -| Path dependency missing remotely | [PATH_DEPENDENCIES.md](references/PATH_DEPENDENCIES.md) (configurable via `[path_topology]`) | -| Sync fails `Permission denied` in `/data/projects/<repo>` | `ssh ubuntu@<host> 'sudo chown -R ubuntu:ubuntu /data/projects/<repo> && sudo chmod 775 /data/projects/<repo>'` | -| Worker disk pressure (RCH-E210/211/...) | [DISK_AND_PRESSURE.md](references/DISK_AND_PRESSURE.md) — hand off to the `sbh` skill | -| Telemetry / SpeedScore broken | [TELEMETRY_RECOVERY.md](references/TELEMETRY_RECOVERY.md) — move db aside, restart | -| Hook says installed but isn't intercepting | `scripts/protocol_test.sh "<your-command>"` | -| Multiple agents racing on fleet ops | Wrap with `scripts/multi_agent_safety.sh <cmd>` and use Agent Mail file reservations | -| Need full environment diagnosis | `rch doctor --json` and `rch config doctor` | - -Debugging fail-opens: `RCH_VISIBILITY=verbose` shows the summary line; `RCH_LOG_LEVEL=debug` surfaces which fail-open path was taken. All other env knobs (priority, env allowlist, SSH keepalives, compression, profiles): `rch --help` + [CONFIGURATION.md](references/CONFIGURATION.md) + [SSH_TUNING.md](references/SSH_TUNING.md). - ---- - -## Anti-Asking Rules - -These are the questions agents historically ask the human that they should *just answer themselves*. The answer is in this skill or trivially derivable. **Do not ask. Do.** - -- "Can I restart the daemon?" — Yes. `rch daemon restart -y` drains in-flight builds gracefully. It's the documented upgrade path. -- "Can I clean up `/tmp/rch_target_*`?" — If `sudo lsof +D <dir>` is empty, yes. See [DISK_AND_PRESSURE.md](references/DISK_AND_PRESSURE.md). If non-empty, never. -- "Should I fix the chown on the worker?" — If the symptom matches the Permission denied recipe, yes. It's documented. -- "Should I disable an unreachable worker and continue?" — Yes. `rch workers disable <id> --reason "..." --drain -y`, then proceed with what's healthy. -- "Should I reinstall the hook?" — If `rch hook status` says missing, yes. `rch hook install` is idempotent. -- "Should I sync the toolchain to the workers?" — If `RCH-E205` or "toolchain missing on X" appears, yes. `rch workers sync-toolchain --all`. -- "The cooldown is blocking my retry — should I delete it?" — No. Wait `auto_start_cooldown_secs`. If you really need to bypass, use `rch daemon start` directly (it's not gated by the hook autostart cooldown). -- "Can I drop the corrupt telemetry db?" — Yes. [TELEMETRY_RECOVERY.md](references/TELEMETRY_RECOVERY.md). Telemetry is derived data. -- "Should I recover SSH keys from a sibling host?" — If the keys are missing on this host but reachable on another, yes. [SSH_KEY_RECOVERY.md](references/SSH_KEY_RECOVERY.md) Step 3. - -When in genuine doubt, capture the escalation packet (Playbook end of [RECOVERY_PLAYBOOKS.md](references/RECOVERY_PLAYBOOKS.md)) and surface that — not a wall of text — to the human. - ---- - -## Output Specification - -- **Path:** `.agents/evidence/remote-compilation/<run-id>/evidence.json` in the active repository, with raw command output stored beside it when needed. -- **Filename convention:** the machine handoff is always `evidence.json`; `<run-id>` is a filesystem-safe timestamp or task identifier unique to the recovery attempt. -- **Serialization/schema format:** JSON object `rch-evidence.v1` with nonempty `run_id`, `summary_line`, and `next_action`; enum `status` (`healthy|recovered|breaker`); enum `stage` (`availability|config|hook|classification|remote-compile|worker-pressure|complete`); nullable string `worker`; and a nonempty `commands` array of `{command:string,exit_code:number}` objects. -- **Validator command:** set `OUT=".agents/evidence/remote-compilation/<run-id>/evidence.json"`, then run `jq -e '. as $in | .schema_version=="rch-evidence.v1" and (.run_id|type=="string" and length>0) and (["healthy","recovered","breaker"]|index($in.status))!=null and (["availability","config","hook","classification","remote-compile","worker-pressure","complete"]|index($in.stage))!=null and ($in.summary_line|type=="string" and length>0) and (($in.worker==null) or ($in.worker|type=="string")) and ($in.next_action|type=="string" and length>0) and ($in.commands|type=="array" and length>0) and all($in.commands[]; (.command|type=="string" and length>0) and (.exit_code|type=="number"))' "$OUT"`. -- **Downstream handoff:** a `healthy|recovered` packet returns the verified worker and probe to the build lane; a `breaker` packet enters HOLD and accompanies the single helper, and only the explicit human states above reach the operator. - -## Quality Checklist - -- The evidence names the first failing stage and preserves the exact pre-repair command, exit code, and `[RCH]` summary instead of inferring success from build completion. -- Every mutation is documented, scoped to the diagnosed failure, and either idempotent or guarded by the playbook's safety precondition. -- The same probe is rerun after repair, and `rch self-test --all` or an isolated `rch exec` demonstrates remote execution rather than local fallback. -- Ordinary rejection remains in AUTO-REDO; HOLD has exactly one helper, and operator escalation is limited to the declared human states. - ---- - -## Reference Index - -Everything below ships in the skill. Read whichever is relevant. - -**Recognising what's wrong:** -- [FAIL_OPEN.md](references/FAIL_OPEN.md) — every `[RCH] local (...)` reason mapped to a self-fix -- [ERROR_CODES.md](references/ERROR_CODES.md) — full RCH-Exxx catalog with skill-doc cross-refs -- [TROUBLESHOOTING.md](references/TROUBLESHOOTING.md) — diagnostic flow + common errors - -**Solving specific failure classes:** -- [RECOVERY_PLAYBOOKS.md](references/RECOVERY_PLAYBOOKS.md) — symptom → fix in ≤90s, organized as 12 lettered playbooks -- [SSH_KEY_RECOVERY.md](references/SSH_KEY_RECOVERY.md) — when workers.toml references keys this host doesn't have -- [PATH_DEPENDENCIES.md](references/PATH_DEPENDENCIES.md) — multi-repo workspaces, closure planner, `[path_topology]` -- [DISK_AND_PRESSURE.md](references/DISK_AND_PRESSURE.md) — RCH-E210..217 + the `sbh` handoff -- [TELEMETRY_RECOVERY.md](references/TELEMETRY_RECOVERY.md) — corrupt `~/.local/share/rch/telemetry/telemetry.db` -- [SELF_HEALING.md](references/SELF_HEALING.md) — autostart cooldown, daemon supervision, `[self_healing]` -- [SSH_TUNING.md](references/SSH_TUNING.md) — ControlMaster, keepalives, retry classification - -**Operating in fleets and swarms:** -- [MULTI_AGENT_CONTENTION.md](references/MULTI_AGENT_CONTENTION.md) — TOCTOU, fleet deploy races, autostart cooldown sharing -- [OPERATIONS.md](references/OPERATIONS.md) — full runbook + worker fleet lifecycle -- [WORKERS.md](references/WORKERS.md) — worker config, drain/disable/enable, deploy -- [CONFIGURATION.md](references/CONFIGURATION.md) — config precedence, env vars, runtime paths -- [HOOKS.md](references/HOOKS.md) — hook protocol, install, test -- [MACHINE_INTROSPECTION.md](references/MACHINE_INTROSPECTION.md) — `--json`, `--schema`, `--help-json`, `--capabilities` - -**Automation scripts (in `scripts/`):** -- `auto_recover.sh` — heuristic, dry-run-by-default fleet recovery -- `worker_disk_triage.sh` — read-only mount-aware disk report per worker -- `protocol_test.sh` — directly probe the hook protocol with synthetic input -- `multi_agent_safety.sh` — flock wrapper for fleet/setup operations -- `mine_rch_history.sh` — find prior agent sessions that hit a given failure -- `diagnose-rch.sh` — comprehensive end-to-end diagnostic (the original) - -**Templates and project docs:** -- `assets/workers-template.toml` -- Source: <https://github.com/Dicklesworthstone/remote_compilation_helper> - ---- - -## Adjacent Skills - -- **`sbh`** — disk-pressure defense for AI coding workloads. Use when `RCH-E210/211/215/216` fires. -- **`agent-mail`** — file reservations and messaging between agents. Use before `rch fleet deploy` or any worker config edit in a swarm. -- **`agent-native`** / **`ntm`** — portable worker lifecycle and NTM mechanics for agents that hit rch failures. -- **`cass`** — search prior agent sessions; the skill ships `scripts/mine_rch_history.sh` as a fallback when cass index has dead pointers. - ---- - -## Reading Output: TUI vs Hook - -`rch` itself, when invoked with **no subcommand**, runs in PreToolUse hook mode (reads JSON from stdin, writes JSON to stdout). Don't run bare `rch` from a terminal expecting help — use `rch --help`. Bare TUIs are at `rch dashboard` (terminal) and `rch web` (browser); both block your session. +- [Fail-open reasons](references/FAIL_OPEN.md) +- [Error catalog](references/ERROR_CODES.md) +- [Troubleshooting](references/TROUBLESHOOTING.md) +- [Recovery playbooks](references/RECOVERY_PLAYBOOKS.md) +- [Worker operations](references/WORKERS.md) +- [Configuration](references/CONFIGURATION.md) +- [Machine-readable surfaces](references/MACHINE_INTROSPECTION.md) diff --git a/images/gemini/skills/reality-check/SKILL.md b/images/gemini/skills/reality-check/SKILL.md new file mode 100644 index 000000000..c81b3d664 --- /dev/null +++ b/images/gemini/skills/reality-check/SKILL.md @@ -0,0 +1,35 @@ +--- +name: reality-check +description: 'Compare a claimed state with observable repository evidence and report concrete gaps. Triggers: "reality check", "what is actually done", "compare claim to repo".' +practices: [design-by-contract, evidence-based-engineering] +hexagonal_role: domain +consumes: [claim, repository-evidence] +produces: [reality-check-report.v1] +context_rel: +- kind: supplier-to + with: plan +skill_api_version: 1 +user-invocable: true +metadata: + tier: judgment + dependencies: [] + capabilities: [compare_claim_to_evidence] + effects: [write_advisory_gap_report] + canonical_status: canonical + disposition: keep_strategy +output_contract: reality-check-report.v1 +--- + +# Reality Check + +Compare an explicit claim with observable evidence. Cite every confirmed or +missing behavior with a file, command result, or artifact. Separate: + +- confirmed behavior; +- concrete gap; +- incomplete evidence; +- changed assumptions. + +Return the report to the caller. Plan may use concrete gaps as input to a new +PlanPacket. Reality Check does not create work, schedule, claim, implement, +validate, retry, or deliver. diff --git a/images/gemini/skills/refactor/SKILL.md b/images/gemini/skills/refactor/SKILL.md new file mode 100644 index 000000000..c16efa469 --- /dev/null +++ b/images/gemini/skills/refactor/SKILL.md @@ -0,0 +1,54 @@ +--- +name: refactor +description: 'Execute one behavior-preserving structural transformation and report evidence.' +practices: +- refactoring +- legacy-code-seams +- design-patterns +hexagonal_role: supporting +consumes: +- repo-context +produces: +- code-changes +context_rel: [] +skill_api_version: 1 +context: + window: fork + intent: + mode: task + sections: + exclude: + - HISTORY + intel_scope: topic +metadata: + capabilities: [refactor] + effects: [] + canonical_status: canonical + disposition: keep_specialist + tier: execution + dependencies: [] +output_contract: code changes with regression evidence +--- +# Refactor — one structural experiment + +Refactor changes structure while preserving observable behavior. It performs one +caller-selected transformation and reports the result. + +## Procedure + +1. Name the preserved behavior and the focused acceptance surface. +2. Record an honest baseline, including any reproducible ambient failures. +3. Apply one bounded transformation: extract, rename, inline, simplify, + encapsulate, move, or delete dead code. +4. Run the focused check and the smallest package-level regression check justified + by the changed surface. +5. Return the diff summary, commands, results, and behavior not checked. + +Do not combine a newly discovered behavior fix with the structural change. A red +result is evidence for the caller; this skill does not revert, narrow, retry, +commit, validate, or route subsequent work automatically. + +## References + +- [Behavior-preserving simplification](references/behavior-preserving-simplification.md) +- [Behavior scenarios](references/refactor.feature) diff --git a/images/gemini/skills/research/SKILL.md b/images/gemini/skills/research/SKILL.md index fb962f2f6..209945c74 100644 --- a/images/gemini/skills/research/SKILL.md +++ b/images/gemini/skills/research/SKILL.md @@ -1,22 +1,24 @@ --- name: research -description: 'Explore and write findings. Triggers: "research", "explore and write findings.", "research skill".' +description: 'Answer a bounded question with current cited evidence. Triggers: "research", "investigate", "find evidence".' practices: -- wiki-knowledge-surface - pragmatic-programmer - ddd-bounded-context hexagonal_role: driving-adapter consumes: -- repo-context +- research-question produces: -- .agents/research/*.md -- result.json +- research-report context_rel: [] skill_api_version: 1 allowed-tools: Read, Grep, Glob, Bash, Write metadata: + capabilities: [research] + effects: [] + canonical_status: canonical + disposition: keep_specialist tier: execution - dependencies: [cass, ms, reverse-engineer, codebase-recon, pattern-mining] + dependencies: [] context: window: fork intent: @@ -28,145 +30,38 @@ context: intel_scope: topic output_contract: skills/research/schemas/findings.json --- -# Research Skill +# Research -Answer a bounded question with current, cited evidence and a durable research -artifact. Execute the investigation; do not return a search diary or an -uncited opinion. +Answer one bounded question with current evidence. Research informs a caller; +it does not select work, approve a plan, mutate lifecycle state, or decide what +happens next. -## Critical Constraints +## Contract -- **Why: avoid aimless exploration.** State the question, decision it informs, - scope, non-goals, freshness needs, and evidence-for-done before searching. -- **Why: prevent rediscovery.** Search `ao lookup` and existing `.agents/` - knowledge first, then test retrieved claims against current authoritative sources. -- **Why: keep facts trustworthy.** Every load-bearing claim cites `file:line`, a - commit, or a direct external source; distinguish observation from inference. -- **Why: control context.** Search in bounded directories, follow discovered - symbols, and stop after three iterative-retrieval cycles unless new evidence - materially changes the answer. -- **Why: honor operator control.** Use one inline agent by default. Spawn an - Explore agent or parallel lanes only when the user or active workflow explicitly - authorizes multi-agent research and scopes non-overlapping work. -- **Why: avoid stale external claims.** Browse current primary sources for - changing APIs, standards, products, or upstream behavior and cite them directly. -- **Why: preserve uncertainty.** Record gaps, contradictions, failed searches, - and confidence; do not turn absence of evidence into evidence of absence. +1. State the question, decision it informs, scope, non-goals, and evidence + required for a useful answer. +2. Search the smallest relevant local sources. For changing external facts, + use current primary sources. +3. Verify structural or semantic-search leads against authoritative content. +4. Separate observation, inference, contradiction, and unknown. +5. Lead with the answer and cite every load-bearing claim. +6. Report unchecked scope and stop. -## Inputs and Modes +Use the current agent inline by default. Parallel readers or alternate runtimes +are optional execution choices only when the caller authorizes them. Prior +research, CASS, MS, codebase recon, and pattern mining are advisory sources, +not required phases. -`/research <question> [--auto] [--from-pr <url>] [quick|medium|very-thorough]` +## Output -- `--auto` skips the Gate-1 approval prompt after quality validation; it does - not authorize external mutations, extra runtimes, or unbounded delegation. -- `--from-pr` narrows source and history inspection to the PR's changed paths. -- Quick answers may stay in chat when no durable handoff is needed. Medium and - architecture/cross-cutting work writes `.agents/research/`. +For a quick question, return the cited answer directly. When the caller asks +for a durable artifact, write one report containing: -## Workflow +- question and scope; +- answer; +- evidence references; +- contradictions and unknowns; +- checked and unchecked areas. -1. **Frame the inquiry.** Write the primary question, subquestions, target - decision, repositories/systems in scope, non-goals, freshness horizon, and - completion test. Choose quick, medium, or very-thorough depth. -2. **Retrieve prior knowledge.** Run `ao lookup --query "<topic>" --limit 5` - when available and search `.agents/{research,learnings,knowledge,patterns, - retros,plans,brainstorm}/` by content. For each applicable hit, record how it - changes the inquiry and verify it against current source. -3. **Choose evidence lanes.** Use code-map and - [codebase-archaeology.md](references/codebase-archaeology.md) for repository - questions; [structural-graph-navigation.md](references/structural-graph-navigation.md) - for refreshed graphify structure; scoped git history for decision context; - [software-research.md](references/software-research.md) or primary web sources - for upstream facts. Structure maps locate relationships, not in-body logic. -4. **Run iterative retrieval.** Start broad inside the declared scope, score - evidence relevance 0-1, extract symbols/config keys from items scoring at - least 0.5, and use them in the next pass. Read authoritative files to verify - every structural or semantic-search lead. Stop after three cycles or saturation. -5. **Select backend deliberately.** Detect the available backend and record it. - When parallelism is authorized, give Explore agent lanes distinct questions - and read-only scopes, then merge their evidence. Otherwise research inline. - See the backend references for Codex, background-task, Claude-team, and inline - variants; runtime and host instructions decide which are legal. -6. **Validate quality.** Assess coverage, depth (0-4 per critical area), gaps, - contradictory evidence, and assumptions. Under `--auto`, any critical depth - below 2 produces WARN plus `.agents/research/quality-warning.md`; do not hide it. -7. **Synthesize.** Write `.agents/research/YYYY-MM-DD-<topic-slug>.md` using - [document-template.md](references/document-template.md). Lead with the answer, - then key files/sources, findings, evidence, unresolved questions, confidence, - recommendations, and the backend used. -8. **Persist reusable findings selectively.** Only reusable findings that should - alter future planning enter `.agents/findings/registry.jsonl`. Require - provenance, `dedup_key`, pattern, detection question, checklist item, - applicability, confidence, and lifecycle fields; merge by key using the - contract's temp-file-plus-rename atomic write rule. Then run - `bash hooks/finding-compiler.sh --quiet` when present. -9. **Gate and report.** Unless `--auto`, ask whether the evidence is sufficient - to proceed to `/plan`, needs revision, or should be abandoned. Report the - answer, artifact path, confidence/gaps, approval status, and next route. - -## Backend Policy - -| Condition | Backend | -|---|---| -| no explicit multi-agent authorization | inline current agent | -| authorized Codex parallel lanes | bounded Codex sub-agents | -| authorized runtime lacks sub-agents | documented background-task fallback | -| no legal spawn backend | inline current agent | - -Backend selection changes execution mechanics, never evidence standards. Read -[iterative-retrieval.md](references/iterative-retrieval.md) and only the backend -module selected for the run. - -## Output Specification - -- **Artifact directory:** `.agents/research/`; optional quality warning at - `.agents/research/quality-warning.md`; reusable findings use the findings registry. -- **Filename convention:** `YYYY-MM-DD-<topic-slug>.md`; stable slug, no - overwrite of unrelated research. -- **Serialization/schema format:** Markdown following the document template plus - `result.json` conforming to `skills/research/schemas/findings.json` when a - machine handoff is required. -- **Validator command:** run `bash skills/research/scripts/validate.sh`, verify - cited paths/lines or URLs, and confirm critical depth/gap reporting. -- **Downstream handoff:** consumed by `/plan`, `/product`, `/premortem`, or the - requesting decision maker; reusable findings feed compiled prevention context. - -## Quality Rubric - -- **Decision-focused:** directly answers the framed question and names implications. -- **Authoritative:** current primary sources and source code outrank summaries. -- **Traceable:** every material claim has reproducible evidence and provenance. -- **Scoped:** search breadth matches the question without context flooding. -- **Honest:** inferences, contradictions, gaps, freshness, and confidence are explicit. -- **Durable:** a fresh reader can act from the artifact without chat context. - -## Examples - -**User says:** `/research "authentication request flow"` - -Trace one entry point through current code, use scoped history for rationale, -cite every transition, and write a medium-depth artifact. - -**User says:** `/research --from-pr <url> "does this change preserve retries?"` - -Restrict evidence to changed paths and their callers/tests, verify upstream -context, and state remaining uncertainty before recommending action. - -## Troubleshooting - -| Problem | Response | -|---|---| -| Topic is too broad | Split it into decision-sized questions | -| Prior research conflicts with source | Prefer current source and record the drift | -| Graph result lacks logic | Open the defining/calling files and verify behavior | -| Critical depth is below 2 | WARN, record the gap, and do not imply completeness | -| No spawn backend is authorized | Research inline; do not treat that as degraded evidence | - -## References - -- [research.feature](references/research.feature) · [document-template.md](references/document-template.md) · [iterative-retrieval.md](references/iterative-retrieval.md) -- [context-discovery.md](references/context-discovery.md) · [source-discovery-and-pattern-extraction.md](references/source-discovery-and-pattern-extraction.md) · [failure-patterns.md](references/failure-patterns.md) -- [codebase-archaeology.md](references/codebase-archaeology.md) · [data-flow-from-entry-points.md](references/data-flow-from-entry-points.md) · [onboarding-methodology.md](references/onboarding-methodology.md) -- [structural-graph-navigation.md](references/structural-graph-navigation.md) · [software-research.md](references/software-research.md) · [deep-research-mcp.md](references/deep-research-mcp.md) -- [backend-codex-subagents.md](references/backend-codex-subagents.md) · [backend-background-tasks.md](references/backend-background-tasks.md) · [backend-claude-teams.md](references/backend-claude-teams.md) · [backend-inline.md](references/backend-inline.md) -- [ralph-loop-contract.md](references/ralph-loop-contract.md) · [vibe-methodology.md](references/vibe-methodology.md) · [claude-code-latest-features.md](references/claude-code-latest-features.md) +Do not emit approval, confidence gates, retry instructions, owner, next action, +or delivery state. diff --git a/images/gemini/skills/reverse-engineer/SKILL.md b/images/gemini/skills/reverse-engineer/SKILL.md new file mode 100644 index 000000000..72ff087cc --- /dev/null +++ b/images/gemini/skills/reverse-engineer/SKILL.md @@ -0,0 +1,198 @@ +--- +name: reverse-engineer +description: 'Reverse-engineer an authorized repo, binary, or product into a verifiable feature inventory and adoption map. Triggers: "reverse-engineer X", "tear down Y", "what should we steal from Z", "evaluate competitor/upstream", "should we fork/adopt/build-native".' +practices: +- legacy-code-seams +- ddd-bounded-context +- adr +hexagonal_role: supporting +consumes: [] +produces: +- .agents/research/*.md +context_rel: [] +skill_api_version: 1 +context: + window: fork + intent: + mode: task + sections: + exclude: + - HISTORY + intel_scope: topic +metadata: + dependencies: [] + capabilities: [reverse_engineer] + effects: [] + canonical_status: canonical + disposition: keep_specialist + tier: execution + internal: false +output_contract: feature inventory, feature-registry.yaml, spec set, steal-map.md +--- +# /reverse-engineer + +Reverse-engineer an external system into two things: a **mechanically-verifiable teardown** (feature inventory + registry + specs, optionally a security audit) and a **steal-map** — what to adopt into our surfaces, what to leave behind. The teardown is the evidence; the steal-map is the decision. The original failure mode this skill exists to prevent: reading a competitor's README and "deciding" from vibes. + +**Triggers:** "reverse-engineer X", "tear down Y", "what should we steal from Z", "evaluate competitor/upstream", "should we fork/adopt/build-native". + +## ⚠️ Constraints — Hard Guardrails (MANDATORY) + +- Only operate on code/binaries you own or have **explicit written authorization** to analyze — this matters because unauthorized teardown is the legal/IP line. +- Do not provide steps to bypass protections/ToS or to extract proprietary source/system prompts. +- Do not output reconstructed proprietary source or embedded prompts (index only; redact in reports) — to prevent reproducing protected IP. +- Redact secrets/tokens/keys if encountered; run the secret-scan gate over outputs to prevent credential leakage. +- Always separate **docs say** vs **code proves** vs **hosted/control-plane**. + +## Phase 1 — Mechanical teardown (the script) + +Produce evidence, not vibes. The script clones (pinned), scans CLI/config/artifact surface, and writes a feature inventory + machine-checkable registry + spec set. + +```bash +python3 skills/reverse-engineer/scripts/reverse_engineer.py <product> --mode=repo \ + --upstream-repo="https://github.com/org/repo.git" --upstream-ref=v1.0.0 \ + --output-dir=".agents/research/<product>/" +``` + +Binary mode requires `--authorized` (see Invocation Contract + Self-Test). Use the bundled demo fixture if you lack authorization for a real binary. + +## Phase 2 — The steal-map (the decision) + +Map each capability the teardown found onto **our** surfaces. This is the part that turns research into a decision. Emit `.agents/research/<product>/steal-map.md` with a table; every row cites the teardown evidence **and** the matching surface in our repo. + +| Their capability | Our surface today | Verdict | +|---|---|---| +| `<feature>` | `<our file / skill / CLI, or "none">` | **have** / **gap** / **steal** / **park** / **reject** | + +Verdict rules (hard-won — apply them, do not skip): + +- **steal** — we lack it and it advances our core. Steal the *pattern*, not the storage engine: re-express in our primitives, never vendor their runtime. +- **park** — real, but it's substrate we deliberately delegate (e.g. orchestration per ADR-0009) or downstream of an unproven bet. Name it, don't build it. +- **reject** — it conflicts with our doctrine (e.g. a self-reported completion edge where we require a verdict — "no verdict = not done"). +- **have** — we already do this; confirm it still holds, move on. +- **gap** — we should have it and don't. These are the steal candidates. + +Discipline that makes the map trustworthy: + +- **Independently checked, not self-report.** Get facts on *how* they implement + each capability from code, cross-checked by a fresh reader — never from a + README or one context's summary. Model family is optional metadata, not a + trust requirement. +- **Probe the real state, don't argue from stale.** Re-verify our side against the live tree before calling something a gap; every "X is missing" carries the search that proved it. +- **The steal is the pattern, not the platform.** Their robustness is usually one idea (unification, a gate, a reconcile loop). Steal the idea; leave the scaffolding. + +## Route one-way-door adoptions into planning + +If adopting a steal is a **one-way door** (an architecture fork, a new bounded +context, or a migration), do not decide it here. Hand the steal-map to +`/plan`. Dueling Idea Genies may challenge the choice as +advisory evidence. Premortem alone decides whether the exact final plan is +ready. + +## Invocation Contract + +Required: `product_name`. Common flags: `--mode=repo|binary|both`, `--upstream-repo`, `--upstream-ref` (pins the clone, records the resolved SHA in `clone-metadata.json`), `--output-dir` (default `.agents/research/<product>/`), `--security-audit`, `--authorized` (mandatory for binary mode — refuses without it). Full list: `python3 skills/reverse-engineer/scripts/reverse_engineer.py --help`. + +## Output Specification + +Phase-1 teardown under `output_dir/`: `feature-inventory.md`, `feature-registry.yaml`, `feature-catalog.md`, `spec-architecture.md`, `spec-code-map.md`, `spec-clone-vs-use.md`, `spec-clone-mvp.md`, plus `spec-cli-surface.md` only when a CLI is detected and `clone-metadata.json` only when `--upstream-ref` is supplied. Security mode adds `output_dir/security/`: `threat-model.md`, `attack-surface.md`, `dataflow.md`, `crypto-review.md`, `authn-authz.md`, `findings.md`, `reproducibility.md`, `validate-security-audit.sh`. Phase-2: `steal-map.md`. + +- **Artifact directory:** the exact `--output-dir`, defaulting to + `$REPO/.agents/research/<product>/`. +- **Filename convention:** the fixed phase-1 and phase-2 names above; security + files live only in the `security/` child directory. +- **Serialization/schema format:** registry is YAML, clone metadata is one JSON + object, and inventories/specs/steal-map are nonempty Markdown files. +- **Validator command:** with `$output_dir`, `$security_audit`, `$sbom`, and + `$upstream_ref_set` (each flag `0|1`) set: + + ```bash + set -euo pipefail + required=(feature-inventory.md feature-registry.yaml feature-catalog.md spec-architecture.md spec-code-map.md spec-clone-vs-use.md spec-clone-mvp.md analysis-root-path.txt validate-feature-registry.py steal-map.md) + for name in "${required[@]}"; do + test -f "$output_dir/$name" + test ! -L "$output_dir/$name" + test -s "$output_dir/$name" + done + test -f "$output_dir/docs-features.txt" + test ! -L "$output_dir/docs-features.txt" + test ! -L "$output_dir/spec-cli-surface.md" + if [[ -e "$output_dir/spec-cli-surface.md" ]]; then + test -f "$output_dir/spec-cli-surface.md" + test -s "$output_dir/spec-cli-surface.md" + fi + python3 "$output_dir/validate-feature-registry.py" + if [[ "$upstream_ref_set" == 1 ]]; then + test -f "$output_dir/clone-metadata.json" + test ! -L "$output_dir/clone-metadata.json" + jq -e 'type == "object"' "$output_dir/clone-metadata.json" >/dev/null + else + [[ "$upstream_ref_set" == 0 ]] + fi + grep -Fqx '| Their capability | Our surface today | Verdict |' "$output_dir/steal-map.md" + if [[ "$security_audit" == 1 ]]; then + test -x "$output_dir/security/validate-security-audit.sh" + if [[ "$sbom" == 1 ]]; then + "$output_dir/security/validate-security-audit.sh" "$output_dir" --sbom + else + [[ "$sbom" == 0 ]] + "$output_dir/security/validate-security-audit.sh" "$output_dir" --no-sbom + fi + else + [[ "$security_audit" == 0 ]] + [[ "$sbom" == 0 ]] + fi + ``` +- **Downstream handoff:** give the validated `steal-map.md` to `/plan` for + one-way-door candidates; ordinary `have`, `park`, and + `reject` decisions remain evidence-backed terminal rows. + +## Reproducibility + fixtures + +`--upstream-ref` pins the clone (fetch `FETCH_HEAD`, record SHA) so contracts can be committed as golden fixtures and diffed across runs. Regression test: `bash skills/reverse-engineer/scripts/repo_fixture_test.sh`. To update a fixture when contracts legitimately change, re-run with the new pinned ref, copy the contract files into `fixtures/<product>/`, and commit. + +## Self-Test (acceptance) + +```bash +bash skills/reverse-engineer/scripts/self_test.sh +``` + +Must show: feature inventory generated, registry generated, registry validator exits 0; in security mode `validate-security-audit.sh` exits 0 and the secret scan passes. + +## Examples + +### Reverse-engineer an OSS CLI (repo mode) → steal-map + +`/reverse-engineer cc-sdd --mode=repo --upstream-repo="https://github.com/gotalab/cc-sdd.git" --upstream-ref=v1.0.0` → clones pinned, scans surface, writes inventory/registry/specs, then you map each feature onto our surfaces (have/gap/steal/park/reject) in `steal-map.md` and route selected steals to `/plan`. + +### Binary analysis with security audit + +`/reverse-engineer ao --authorized --mode=binary --binary-path="$(command -v ao)" --security-audit` → static analysis (metadata, linked libs, embedded-archive signatures, index only) plus the security suite under `output_dir/security/`; the secret-scan gate passes. + +## Troubleshooting + +| Problem | Cause | Solution | +|---|---|---| +| Refuses binary analysis | Missing `--authorized` | Add `--authorized` (explicit written authorization required). | +| No `clone-metadata.json` | `--upstream-repo` not passed | Pass `--upstream-repo` (and optionally `--upstream-ref`). | +| Fixture diff fails | Upstream changed / stale golden | Re-run pinned, refresh `fixtures/`, commit. | +| `spec-cli-surface.md` missing | No Node/Python/Go CLI detected | Surface is documented in `spec-code-map.md` instead. | +| Steal-map is all "steal" | Skipped the park/reject rules | Substrate we delegate is **park**; doctrine conflicts are **reject** — not everything novel is worth adopting. | + +## Quality Rubric + +- [ ] Every steal-map row cites teardown evidence **and** our matching surface (or "none"). +- [ ] Verdicts use the full set — `have`/`gap`/`steal`/`park`/`reject` — not everything marked "steal". +- [ ] Facts on *how* they implement come from code and a fresh independent check — not a README. +- [ ] One-way-door adoptions are routed to Discovery and Plan, not decided here. +- [ ] Secret-scan gate passed over all outputs; no proprietary source/prompts reproduced. + +## See Also + +- [plan](../plan/SKILL.md) — shape selected steals into exact PlanPackets +- [dueling-idea-genies](../dueling-idea-genies/SKILL.md) — optional advisory challenge +- [premortem](../premortem/SKILL.md) — binary readiness verdict on the exact plan +- [research](../research/SKILL.md) — general exploration; this is its external-system specialization + +## Reference Documents + +- [references/reverse-engineer.feature](references/reverse-engineer.feature) — executable spec: repo-mode feature catalog + code map, binary-mode security audit, durable spec artifacts diff --git a/images/gemini/skills/rpi/SKILL.md b/images/gemini/skills/rpi/SKILL.md index 36f218e89..4f40314f0 100644 --- a/images/gemini/skills/rpi/SKILL.md +++ b/images/gemini/skills/rpi/SKILL.md @@ -1,250 +1,96 @@ --- name: rpi -description: 'Run Discovery, Crank, Validate, and Learn as four ordered, independently receipted umbrellas. Triggers: "run rpi", "research-plan-implement one turn", "drive a turn through the operating loop".' +description: 'Run one bounded Plan, Implement, and fresh Validate experiment, then report and stop. Triggers: "run rpi", "feed this through the loop", "research-plan-implement".' practices: - bdd-gherkin -- ddd-bounded-context -- hexagonal-architecture - tdd -- continuous-delivery -- dora-metrics -- agile-manifesto -- pragmatic-programmer +- design-by-contract hexagonal_role: domain consumes: -- crank -- discovery -- domain -- learn +- plan +- implement - validate produces: -- .agents/rpi/*.md +- rpi-report.v1 context_rel: - kind: customer-of - with: crank + with: plan - kind: customer-of - with: discovery -- kind: customer-of - with: learn + with: implement - kind: customer-of with: validate skill_api_version: 1 user-invocable: true -context: - window: inherit - intent: - mode: task - sections: - exclude: - - HISTORY - intel_scope: full metadata: graph_root: true tier: meta - dependencies: - - domain - - discovery - - crank - - validate - - learn - internal: false -output_contract: .agents/rpi/YYYY-MM-DD-*.md + dependencies: [plan, implement, validate] + capabilities: [orchestrate_once, report] + effects: [dispatch_core_phases] + canonical_status: canonical + disposition: keep +output_contract: rpi-report.v1 --- -# /rpi - Full Lifecycle Orchestrator +# RPI -> Quick ref: `/discovery` + `/premortem` -> `/crank` for a bounded tranche -> one `/validate` -> one `/learn`, then report. +Run one experiment through three responsibilities and stop: -**Execute this workflow. Do not only describe it.** RPI is autonomous unless `--interactive` is set. The user touchpoint is after Learn returns control to the orchestrator or after a real blocked state exhausts retries. Read [autonomous-execution.md](references/autonomous-execution.md) for the full autonomy contract. +```text +Plan -> Implement -> fresh Validate -> report +``` -**`--auto` means *pivot autonomously*, NOT *execute the initial plan to the letter*.** Autonomy is agility, not waterfall: between waves the orchestrator re-plans the remaining work and changes course on its own — refactoring, adding, dropping, reordering waves as evidence arrives — without the operator saying so (touched only at the terminal objective or a circuit-breaker trip that survives its bounded helper pass). See [Agile Re-Plan Loop](#agile-re-plan-loop-the-anti-waterfall-rule). +RPI preserves the original intent and dispatches each core phase at most once. +It does not own retries, budgets, queues, claims, leases, Git, delivery, release, +closure, or the caller's next decision. -## Critical Constraints +The pure [`scripts/run_once.py`](scripts/run_once.py) reference behavior makes +the dispatch and stop semantics executable without Git, `ao`, or a tracker. -- `Validate -> Learn -> orchestrator` is the only legal post-execution transition because the immutable verdict must reach Learn before any plan or control decision. Learn is the only post-verdict handoff; Validate never jumps to Crank, Discovery, Premortem, retry, or delivery. -- Only the orchestrator may invoke Premortem. One verdict binds the tranche plan, - acceptance, dependency shape, write scope, and risk class; reuse it while those - inputs remain unchanged. A changed input receives one bounded fresh-context - Premortem before another wave is admitted. -- A routine tranche contains one to three sequential low-risk waves, one active - mutation at a time, in one bounded context. Intermediate waves run targeted - deterministic checks and return evidence to the orchestrator; **no per-wave - Validate, Learn, delivery, or duplicate summary is required**. -- `no_change` is valid; the orchestrator may continue the admitted tranche - without fabricating a lesson. Material scope, risk, dependency, or acceptance - change is `REPLAN`, not another review round. -- `terminal` closes the tick because no remaining work means no re-plan or Premortem. -- RPI ends at the four receipts and its report. It does not push Git refs, operate a Git queue, close tracker state through delivery, or require another LLM landing verdict. Repository-selected delivery is a separate adapter. -- Preserve one objective, acceptance surface, and evidence chain across every retry. **Why:** narrowing to a convenient child task can manufacture green while the requested behavior remains incomplete. -- Keep one active leaf per writer. Goal and epic parents are aggregate demand; - they never occupy WIP. The leaf is the bounded tranche and may take one to - three implementation waves, but no second leaf is pulled until the current - one is terminally reported. -- An initial introduced acceptance defect may receive one consolidated repair. - Evidence of a second distinct repair need must be classified `REPLAN` and - re-sliced through Discovery instead of starting another review loop. -- RPI owns the one [run disposition contract](references/pull-flow-governor.md), - not an execution controller. `NOTE`, `REPAIR`, `REPLAN`, `HOLD`, and `ANDON` - are the canonical dispositions. Crank and Validate return evidence; they do - not reserve work, maintain counters, grant helpers, or authorize dispatch. +## Contract -## Loop position +1. Invoke `/plan` once with the caller's intent. Preserve its exact + `PlanPacket` and digest. If planning cannot produce a complete packet, report + `NOT_PLANNED` and stop. +2. Invoke `/implement` once with that packet. It performs one bounded experiment + and returns a `CandidatePacket`. If no candidate is built, report `NOT_BUILT` + and stop. +3. Invoke `/validate` once in a context distinct from the candidate's + `author_context_id`. Pass only the PlanPacket, CandidatePacket, factual + evidence, validator identity, and freshness attestation. +4. Return the durable `verdict.v2` reference and a short report. Stop regardless + of `PASS`, `FAIL`, or `NOT_PROVEN`. -`/rpi` is the orchestrator across **every move** of the [operating loop](../../docs/architecture/operating-loop.md): BDD intent → vertical slices → per-slice [narrow-waist micro-cycle](../../docs/architecture/operating-loop.md#the-narrow-waist-micro-cycle-canonical--every-loop-skill-cites-this) (**acceptance test RED → green → refactor-under-green**) → one-to-three-wave bounded tranche → one frozen-candidate proof → Learn receipt → orchestrator decision. It delegates each move to the skill that owns it (`/discovery`, `/premortem`, `/crank`, `/validate`, `/learn`) and enforces these loop-level invariants: +`NOT_PLANNED` and `NOT_BUILT` are report statuses, never semantic verdicts. +A caller may later create a `revision-packet.v1` and start a new invocation; +RPI never creates, selects, or consumes a revision automatically. -- **Agile, not waterfall — the plan is a hypothesis.** Intermediate wave evidence may reorder or narrow the remaining admitted tranche. Reuse the existing Premortem when its bound inputs are unchanged; a material or second-repair delta re-slices through Discovery and receives a new Premortem. -- **One proof transaction.** Intermediate slices use cheap deterministic checks. After the bounded tranche is complete, freeze once, run one fresh independent Validate, run one Learn bookkeeping pass, and report. Validate and Learn never sit between unchanged low-risk waves. -- **The first failing test is the bead's contract.** With `--test-first` on (the default), `/crank` is invoked with the TDD-per-slice discipline; `--no-test-first` is an explicit opt-out, not a fast path. `/crank` runs **refactor-under-green as its own step after green** — the load-bearing quality move — and a refactor must never change a test (S4; test-first *ordering* alone is not the quality lever). -- **Acceptance examples close the bead, not activity.** Every validation verdict routes through Learn; only the orchestrator may choose to re-crank the same objective. DONE requires the acceptance roll-up in the [slice-validation template](../../docs/templates/slice-validation.md) to be fully green. -- **Ports stay visible.** Preserve the [Intent-to-Loop Hexagon](../../docs/architecture/intent-to-loop-hexagon.md) boundary as the objective crosses `shape_intent`, `persist_intent`, `plan_slices`, `execute_wave`, `validate_acceptance`, and `record_evidence`. -- **Context density survives phase boundaries.** Apply the [Context Density Rule](../domain/references/context-density-rule.md) to every phase handoff and final report: keep intent, boundary, evidence, decision, constraint, and next action; omit or link anything else. +## Invariants -## Core Contract +- Acceptance and its digest do not change between phases. +- The candidate reports complete changed-path coverage or Validate returns + `NOT_PROVEN`. +- A proven change outside `write_scope` makes the verdict `FAIL`. +- PASS requires nonempty distinct author and validator context IDs plus an + explicit freshness attestation. +- Optional Premortem, Postmortem, Council, genie, factory, tracker, and runtime + adapters are caller-selected. They do not alter phase order or core outcomes. +- Learn is an optional later consumer of verdict collections and is not part of + this invocation. -RPI preserves four typed responsibilities: Discovery shapes the tranche, Crank -executes admitted waves, Validate independently judges the frozen tranche, and -Learn bookkeeps the immutable verdict. Runtime-native skill calls or thin phase -runners may carry those responsibilities, but only semantic judgment requires a -fresh independent context. Do not replace a typed responsibility with ad hoc -work or skip validation. Read the [strict-delegation contract](../shared/references/strict-delegation-contract.md), [isolation contract](references/isolation-contract.md), and [best practices](references/best-practices.md). +## Report -When phase isolation exists, keep `/rpi` visible and pass phase skill name plus bounded handoff in, then artifact/verdict/next action out. The transport may be a process or subagent wrapper, but it must execute the declared phase contract rather than doing phase work directly. +Return exactly the useful boundary facts: -RPI owns one lifecycle objective. Preserve the discovered `epic_id` or original goal and packet objective; a child bead or ready slice is context, not a replacement. `<promise>PARTIAL</promise>` from `/crank` is evidence for the orchestrator, not an automatic retry. +```yaml +schema_version: rpi-report.v1 +status: PASS | FAIL | NOT_PROVEN | NOT_PLANNED | NOT_BUILT +plan_packet_digest: <sha256 or null> +subject_manifest_digest: <sha256 or null> +verdict_ref: <path or null> +verdict_digest: <sha256 or null> +checked: [] +not_checked: [] +``` -## Phase Receipt Contract - -RPI cannot rely on memory or a final narrative to prove responsibilities ran. -The execution packet carries one ordered receipt index whose entries point at -the canonical Discovery packet, Crank tranche evidence, Validate verdict, and -Learn receipt. Do not restate the same analysis in four Markdown summaries or -mirror `skills_loaded` into every artifact. Legacy phase summaries, when a -consumer still requires them, are link-only compatibility projections. Full -contract: [phase-data-contracts.md](references/phase-data-contracts.md). - -## Route And Classify - -1. Create `.agents/rpi/`. -2. Resolve `--from`: - - default, `research`, `plan`, `premortem`, `brainstorm` -> discovery - - `implementation` or `crank` -> implementation - - `validation` or `vibe` -> validation - - `learn` or `postmortem` -> learn -3. If the input is a bead and `--from` is absent, resolve it with `ao beads exec show`: - - epic -> implementation with that epic - - child with parent -> implementation with the parent epic -4. Classify complexity: - - `fast`: short/simple goal or `--fast-path` - - `standard`: medium goal or one scope keyword - - `full`: `--deep`, complex-operation keyword, 2+ scope keywords, or >120 chars -5. Log `RPI mode: rpi-phased (complexity: <level>)`. - -Track lifecycle state as `rpi_state`: `goal` (string), `epic_id` (null until discovered), `phase` (discovery|crank|validate|learn), `complexity` (fast|standard|full), `test_first` (true unless `--no-test-first`), `run_id`, and `verdicts` ({}). When evidence changes the next move, write one immutable run-disposition record bound to the objective and evidence. Do not add phase counters, reservations, or helper state. - -## Phase DAG - -Enter at the routed phase and run every phase after it. - -1. **Discovery:** invoke `/discovery <goal> [--interactive] --complexity=<level>`. On DONE, read the current or archived execution packet and preserve its objective spine; on BLOCKED, return evidence without treating the label as a retry decision. -2. **Crank tranche:** invoke `/crank` for one ready wave and read the actual diff - for scope and claim match. If targeted checks are green and the bound plan - inputs are unchanged, the orchestrator may pull the next sequential wave - without Validate or Learn. A completed leaf proceeds - to freeze. At three waves or 90 minutes with work incomplete, persist - `PARTIAL` resume evidence and stop without proof authorization. Scope/risk - drift or failed acceptance returns to the appropriate repair/replan move. A - soft tranche boundary is not HOLD or ANDON. -3. **Freeze and Validate once:** commit the complete tranche, pin one candidate - identity, and consume exact-input deterministic receipts. Invoke one fresh - independent `/validate`. Missing, - stale, suspicious, or invalidated facts are rerun; unchanged facts are not. -4. **Repair closure:** one introduced blocker set may receive one consolidated - repair batch. Refreeze, rerun invalidated facts, and re-review only affected - claims. A second distinct repair need is `REPLAN`. After closure, run the - repository's full deterministic terminal gate once on the final exact - candidate, persist its reusable receipt, and seal the final Validate result. -5. **Learn once:** invoke `/learn` in the orchestrator context with the immutable - final verdict. Learn performs no model review and emits one canonical receipt; - any phase summary is a link-only compatibility projection. -6. **Report:** use [references/report-template.md](references/report-template.md) - after the one proof transaction. Apply the Context Density Rule and preserve - exact resume state when aggregate demand remains. - -## Orchestrator Decision State Machine - -The orchestrator, not Validate or Learn, owns retry and re-plan decisions. -Every final tranche verdict becomes one Learn receipt; its plan impact selects -the next-tranche branch. Intermediate wave facts go directly to the -orchestrator's remaining-plan decision and never impersonate a semantic verdict. -The [run disposition contract](references/pull-flow-governor.md) defines the -five evidence classifications and their legal next moves. It records decisions; -it does not run another state machine around the work. - -## Agile Re-Plan Loop (the anti-waterfall rule) - -The initial plan is a **hypothesis**; each wave is an experiment. Targeted wave -evidence may change the remaining tranche before any semantic review. When the -plan's bound inputs change materially, the orchestrator invokes Discovery and -one fresh Premortem; otherwise it reuses the accepted plan verdict. At tranche -completion, evidence flows once through `Validate -> Learn -> orchestrator`. -Anti-patterns: **waterfall**, -**retry-not-replan**, **validate-to-premortem**, and **permission-seeking**. -**Full detail:** [references/agile-replan-loop.md](references/agile-replan-loop.md). - -## Phase Data Contract - -The execution packet carries the repo execution profile through -`contract_surfaces`, `done_criteria`, and queue claim/finalize metadata. Keep -the latest alias at `.agents/rpi/execution-packet.json` and read -[references/phase-data-contracts.md](references/phase-data-contracts.md) for -schemas and archive paths. - -## Complexity-Scaled Review - -Complexity scales the depth of Premortem and Validate, never the phase order. -Routine work uses one fresh validator; deeper review is explicit. Learn stays bounded and delivery remains a repository adapter. See [complexity scaling](references/complexity-scaling.md). - -## Flags - -| Flag | Default | Purpose | -|------|---------|---------| -| `--from=<phase>` | discovery | Start at discovery, implementation, or validation | -| `--interactive` | off | Human gates in discovery/validate | -| `--auto` | on | Fully autonomous default — **pivots between waves on its own** (re-plans remaining work; not a fixed-plan/waterfall executor). See [Agile Re-Plan Loop](#agile-re-plan-loop-the-anti-waterfall-rule) | -| `--loop` | off | Pull additional sequential waves inside the current bounded tranche | -| `--run-id=<id>` | generated if absent | Correlate receipts for one lifecycle objective | -| `--max-waves=<n>` | 3 | Soft tranche boundary; preserve resume evidence instead of escalating | -| `--max-elapsed-seconds=<n>` | 5400 | Soft 90-minute tranche boundary; preserve resume evidence instead of escalating | -| `--test-first` / `--no-test-first` | on / off | Enable or explicitly opt out of TDD ordering | -| `--fast-path` / `--deep` | auto | Force fast or full complexity | -| `--dry-run` | off | Report the selected moves without mutating work | - -These are orchestration defaults, not authorization or phase-local retry -budgets. Hard external ceilings are facts supplied by the runtime or operator. - -## Examples - -**User says:** `/rpi "add user authentication"` for a new goal, or `/rpi --from=implementation ag-23k` for an already-shaped leaf. - -## Output Specification - -- Canonical state: `.agents/rpi/execution-packet.json` plus immutable per-run artifacts and one ordered receipt index. -- Schema: `schemas/execution-packet.schema.json`; validate with `python3 skills/rpi/scripts/validate-execution-packet.py .agents/rpi/execution-packet.json`. -- Handoff: Discovery shapes, Crank records targeted evidence, Validate writes the immutable verdict, Learn records plan impact, and the orchestrator reports. -- `<promise>PARTIAL</promise>` preserves resume state; it never implies retry or terminal proof. - -## Quality Checklist - -- [ ] No per-wave Validate or Learn ran before the bounded tranche froze. -- [ ] One final independent verdict routes through Learn before the next-tranche decision. -- [ ] The execution packet passes its validator before Report or downstream handoff. -- [ ] One closed disposition record binds each next-move decision to evidence. - -## Troubleshooting - -- Classify failures with the five run dispositions; repair invalid packets locally, and use direct checks when an optional executor fails. See [troubleshooting.md](references/troubleshooting.md). - -## Reference Documents - -- Core: [agile re-plan](references/agile-replan-loop.md), [run dispositions](references/pull-flow-governor.md), [phase data](references/phase-data-contracts.md), [compression](references/orchestrator-compression-anti-pattern.md), and [executable feature](references/rpi.feature). -- Operation: [autonomy](references/autonomous-execution.md), [context windows](references/context-windowing.md), [Discovery artifact mode](references/discovery-artifact-mode.md), [bounded tranches](references/phase-budgets.md), [repair and escalation](references/gate-retry-logic.md), [loop/spawn](references/gate4-loop-and-spawn.md), [Codex executor](references/codex-executor.md), [installed-version warning](references/installed-plugin-version-not-repo-head.md), [examples](references/examples.md), [recovery](references/error-handling.md), and [report](references/report-template.md). +Do not append a next action. The caller owns continuation. diff --git a/images/gemini/skills/sbh/SKILL.md b/images/gemini/skills/sbh/SKILL.md index ca92af287..18d43deaa 100644 --- a/images/gemini/skills/sbh/SKILL.md +++ b/images/gemini/skills/sbh/SKILL.md @@ -3,159 +3,41 @@ name: sbh user-invocable: false skill_api_version: 1 hexagonal_role: supporting +consumes: [] +produces: [] +context_rel: [] metadata: + dependencies: [] + capabilities: [sbh] + effects: [] + canonical_status: canonical + disposition: keep_specialist tier: execution -description: >- - Disk-pressure defense for AI coding workloads. Use when: disk full, low - space, ballast, cleanup, scan artifacts, emergency, sbh daemon, sbh status. +description: 'Inspect disk pressure with SBH and run one explicitly authorized recovery action.' practices: - pragmatic-programmer --- +# SBH — storage pressure specialist -# SBH — Storage Ballast Helper +SBH exposes disk-pressure status, ballast, scanning, and recovery commands. This +skill gathers evidence and performs at most the explicit action authorized by the +caller. -Prevents disk-full disasters via ballast files, artifact scanning, and predictive pressure monitoring. Three-pronged: ballast (instant space), scanner (stale artifacts), special locations (/tmp, /dev/shm, swap). +## Constraints -## Critical Constraints +- Begin with `sbh --json status`, `sbh check`, or a dry run on the exact mount. +- Never run `clean --yes`, `emergency --yes`, ballast release, `tune --apply`, + `unprotect`, or service/configuration changes without explicit authority. +- Preserve `.git/`, open-file, young-file, non-writable-parent, and + `.sbh-protect` vetoes. +- Confirm that ballast or reclaimed bytes affect the constrained mount. +- Run one action once, capture before/after evidence, and stop. A negative result + is returned to the caller; this skill does not retry or escalate it. -- Start with `sbh --json status`, `sbh check`, or a dry-run; make read-only triage the default. **Why:** pressure evidence must identify the constrained mount before any mutation. -- Require explicit authorization before `clean --yes`, `emergency --yes`, ballast release, `tune --apply`, `unprotect`, or service/config changes. **Why:** these commands delete data or alter durable machine state. -- Preserve `.git/`, open-file, young-file, non-writable-parent, and `.sbh-protect` vetoes; keep ballast on the same mount as the pressure source. **Why:** freeing the wrong volume or deleting live work cannot recover the objective. -- `WARN|FAIL|REFUTED -> AUTO-REDO`: consult the pawl, apply the smallest safe recovery, then rerun status/check on the same mount. **Why:** pressure and failed checks are recovery evidence, not an andon by themselves. -- `BREAKER -> HOLD -> ONE-HELPER`; `HELPER-UNSTUCK -> AUTO-REDO`. Hold write-heavy work and use one bounded local-shell helper for status, scan, or dry-run evidence—never another runtime by default. **Why:** one bounded pass can restore progress without masking a real capacity stop. -- `HELPER-ESCALATE -> HUMAN`; `REFUSAL-LANE|EXPLICIT-JUDGMENT|EXHAUSTED-BUDGET -> HUMAN`. **Why:** only irreversible deletion judgment, unavailable authority, or exhausted recovery earns the human andon. +## Output -## Quick Check +Return mount, free bytes, pressure state, dry-run candidates, authorization used, +exact command and exit code, bytes reclaimed, protection vetoes, and checked/not +checked surfaces. -```bash -sbh status # Pressure level + free space -sbh status --json | jq '.pressure' # Machine-parseable -sbh check --need 5G # "Do I have 5 GB free?" -sbh check --predict 30 # "Will I run out in 30 min?" -``` - -Exit codes: 0 = healthy, 1 = pressure, 2 = error. - ---- - -## Daemon - -```bash -sbh daemon # Foreground (debugging) -systemctl --user start sbh # Systemd user scope -sbh install --systemd --user --auto # Install + start (Linux) -sbh install --launchd --auto # Install + start (macOS) -sbh install --wizard # Guided interactive setup -``` - -**Signals:** `SIGHUP` = reload config, `SIGUSR1` = force scan now, `SIGTERM` = graceful stop. - ---- - -## Ballast - -Pre-allocated sacrificial files — released in milliseconds, no scanning needed. - -```bash -sbh ballast status # Per-volume inventory -sbh ballast provision # Create/rebuild pool -sbh ballast release 3 # Free 3 files NOW -sbh ballast replenish # Rebuild after pressure passes -``` - -Defaults: 10 x 1 GiB = 10 GiB. Ensure ballast dir is on **same mount** as pressure source. - ---- - -## Scanning & Cleanup - -```bash -sbh scan /data/projects --top 20 # Rank artifacts by score -sbh clean /data/projects --dry-run # Preview what would go -sbh clean --target-free 50G --yes # Delete until 50 GB free -``` - -Scoring: Location (.25) + Name (.25) + Age (.20) + Size (.15) + Structure (.15) = 1.0. - ---- - -## Protection - -```bash -sbh protect /path # .sbh-protect marker (subtree) -sbh unprotect /path # Remove marker -``` - -Config globs: `scanner.protected_paths`. Hard vetoes (always enforced): `.git/` dirs, open files, age < 10 min, non-writable parents. - ---- - -## Emergency Recovery - -Zero-write mode for near-100% full disks. No config file needed. - -```bash -sbh emergency /data --yes # Aggressive cleanup NOW -sbh emergency --target-free 10G # Stop at 10 GB recovered -``` - ---- - -## Observability - -```bash -sbh dashboard # TUI: 7 screens (1-7 to jump) -sbh stats --window 24h # Activity over last 24 hours -sbh blame --top 10 # Top 10 pressure sources -sbh explain --id <ID> # Why was this decision made? -``` - ---- - -## Configuration - -Config: `~/.config/sbh/config.toml` | Env: `SBH_` prefix | Fallback: `/etc/sbh/config.toml` - -```bash -sbh config show # Current values -sbh config validate # Check constraints -sbh config set KEY VALUE # Change a value -sbh tune --apply --yes # Auto-tune for this system -``` - ---- - -## Anti-Patterns - -| Don't | Do Instead | -|-------|------------| -| Ballast on `/tmp` | `paths.ballast_dir` on same mount as pressure source | -| Daemon as root, CLI as user | `--user` scope — avoids state file permission mismatch | -| Skip pre-build check | `sbh check --need 10G` in CI/hook | -| Delete `.sbh-protect` by hand | `sbh unprotect /path` | -| Wait for Red to act | Act at Yellow — agent swarms escalate fast | -| `min_file_age_minutes = 0` | Keep >= 5 to protect in-flight writes | - ---- - -## Output Specification - -**Artifact directory:** `.agents/evidence/sbh/<run-id>/` when a durable handoff is requested. -**Filename convention:** required `status-before.json`; optional `scan.json`, `clean-dry-run.json`, and `status-after.json` after an authorized action. -**Serialization/schema format:** raw SBH JSON; status artifacts require `command == "status"`, a nonempty `pressure.overall`, and a nonempty `pressure.mounts` array whose entries contain `path`, `level`, and numeric `free`. -**Validator command:** with `OUT=.agents/evidence/sbh/<run-id>`, run `jq -e '.command=="status" and (.pressure.overall|type=="string" and length>0) and (.pressure.mounts|type=="array" and length>0) and all(.pressure.mounts[]; (.path|type=="string" and length>0) and (.level|type=="string" and length>0) and (.free|type=="number"))' "$OUT/status-before.json"`. -**Downstream handoff:** pass the artifact directory, constrained mount, pressure level, selected action, authorization state, command exit codes, and next safe action to the consuming skill; compare `status-after.json` before declaring recovery. - -## Quality Checklist - -- [ ] Capture machine-readable status for the exact constrained mount before mutation. -- [ ] Verify dry-run candidates against protection vetoes and explicit authorization. -- [ ] Confirm ballast or reclaimed bytes affect the same mount that triggered pressure. -- [ ] Rerun status/check after recovery and preserve both before/after evidence. -- [ ] Consult the pawl on negative results; raise the andon only from a terminal state. - ---- - -## Docs - -Full documentation: https://github.com/Dicklesworthstone/storage_ballast_helper +Full command documentation: <https://github.com/Dicklesworthstone/storage_ballast_helper> diff --git a/images/gemini/skills/scaffold/SKILL.md b/images/gemini/skills/scaffold/SKILL.md new file mode 100644 index 000000000..81c155ab8 --- /dev/null +++ b/images/gemini/skills/scaffold/SKILL.md @@ -0,0 +1,75 @@ +--- +name: scaffold +description: 'Stamp a bounded project, component, or CI scaffold and verify the generated result once. Triggers: "scaffold", "create project component or boilerplate".' +practices: +- pragmatic-programmer +- design-patterns +- hexagonal-architecture +hexagonal_role: supporting +consumes: [] +produces: +- converted-skill +context_rel: [] +skill_api_version: 1 +context: + window: fork + intent: + mode: task + sections: + exclude: + - HISTORY + intel_scope: topic +metadata: + capabilities: [scaffold] + effects: [] + canonical_status: canonical + disposition: keep_specialist + tier: execution + dependencies: [] +output_contract: project files and directory structure +--- +# Scaffold + +Create one bounded project, component, or CI scaffold. This specialist does not +schedule RPI, create work ownership, mutate Git, or decide what happens next. + +## Contract + +1. Resolve the requested target root and declare the exact paths that may be + created or changed. +2. Refuse to overwrite an existing path without explicit caller authorization. +3. Generate idiomatic, functional files with at least one behavioral test for + generated behavior. +4. Run the target's selected build, test, and lint commands once. +5. Report the files changed and factual command results, then stop. + +Use the current agent and local shell unless the caller explicitly requests a +different runtime. Preserve unrelated existing changes. + +## Modes + +- `/scaffold <language> <name>` creates a project. +- `/scaffold component <type> <name>` adds a component to an existing project. +- `/scaffold ci <platform>` creates the requested CI configuration. + +If the request does not identify a target or language, ask only for the missing +fact. The caller owns version control, revision, and delivery. + +## Evidence + +Return: + +- the target root and actual changed paths; +- the build, test, and lint commands selected; +- each command's exit code; +- any requested check that was not run. + +The result contains no verdict, lifecycle state, retry instruction, or next +action. + +## References + +- [references/generic-templates.md](references/generic-templates.md) — optional + historical shapes when the caller wants a specific template. +- [references/agent-facing-tool-scaffolds.md](references/agent-facing-tool-scaffolds.md) +- [references/scaffold.feature](references/scaffold.feature) diff --git a/images/gemini/skills/scope/SKILL.md b/images/gemini/skills/scope/SKILL.md index 691784ffe..a758c66ac 100644 --- a/images/gemini/skills/scope/SKILL.md +++ b/images/gemini/skills/scope/SKILL.md @@ -1,162 +1,81 @@ --- name: scope -description: 'Hard-block edits outside declared frozen directories and protect paths during risky changes. Triggers: "scope", "hard-block edits outside declared frozen", "scope skill".' +description: 'Review a proposed PlanPacket write scope for completeness and ambiguity. Triggers: "review write scope", "check scope boundaries", "scope this change".' practices: - ddd-bounded-context - design-by-contract -- mythical-man-month -hexagonal_role: driven-adapter -consumes: [] +hexagonal_role: supporting +consumes: +- proposed-write-scope produces: -- filesystem-gate +- scope-review context_rel: - kind: supplier-to - with: domain + with: plan skill_api_version: 1 context: window: isolated intent: mode: none sections: - exclude: - - HISTORY - - INTEL - - TASK + exclude: [HISTORY, INTEL, TASK] intel_scope: none metadata: + capabilities: [scope_review] + effects: [] + canonical_status: canonical + disposition: keep_specialist tier: meta dependencies: [] -output_contract: 'stdout: scope status / lock state; stderr: blocked-edit reason from - hook' ---- -# /scope — Edit Scope Guard - -> **Purpose:** Declare which directories are in scope for the current work session. Edits outside the declared scope are hard-blocked by a PreToolUse hook. - -**YOU MUST EXECUTE THIS WORKFLOW. Do not just describe it.** - -## Critical Constraints - -- Treat `.agents/scope.lock` as a containment boundary, never as permission to edit every path it names. **Why:** scope limits authority; it does not create authority or ownership. -- Resolve each frozen directory repo-relative, reject traversal outside the repository, and verify `ao scope status --json` after every mutation. **Why:** an unverified or escaping prefix gives false confidence about the active boundary. -- Never unfreeze or widen scope merely to make a blocked edit pass; require explicit scope-expansion judgment tied to the original objective. **Why:** silently moving the boundary defeats the guard. -- Use the current agent and local shell for freeze, status, and recovery; do not start another runtime or orchestration substrate unless explicitly requested. **Why:** a path guard does not authorize fan-out. -- `WARN|FAIL|REFUTED -> AUTO-REDO`: consult the pawl, repair the path or lock state, and retry within the same declared scope. **Why:** malformed inputs and failed checks are recovery evidence, not an andon by themselves. -- `BREAKER -> HOLD -> ONE-HELPER`; `HELPER-UNSTUCK -> AUTO-REDO`. On an out-of-scope rejection, hold the write and use one bounded local-shell helper to inspect status and find an in-scope route. **Why:** one recovery pass preserves containment without hiding a real scope conflict. -- `HELPER-ESCALATE -> HUMAN`; `REFUSAL-LANE|EXPLICIT-JUDGMENT|EXHAUSTED-BUDGET -> HUMAN`. **Why:** only a genuine boundary change, unavailable authority, explicit judgment, or exhausted recovery earns the human andon. - +output_contract: 'response: proposed write_scope.include/exclude plus gaps and ambiguities' --- -## Quick Start +# /scope — Review a proposed write scope -```bash -/scope freeze cli/cmd/ao/ # Freeze a single directory -/scope freeze cli/cmd/ao/ skills/scope/ # Freeze multiple (additive) -/scope unfreeze cli/cmd/ao/ # Remove one frozen directory -/scope unfreeze # Clear ALL frozen directories -/scope status # Show current lock state -/scope status --json # JSON output +Review the `write_scope.include` and `write_scope.exclude` fields of a proposed +PlanPacket. This skill is advisory: it does not write a lock, install a hook, +block an edit, claim paths, or change the PlanPacket. + +## Inputs + +- One active behavior and its acceptance scenarios. +- Proposed include and exclude patterns. +- Known generated companions and fixture/projection paths. +- Explicit non-goals. + +## Procedure + +1. Map each acceptance criterion to the smallest source paths that may change. +2. Add owned generated companions that must move with those sources. +3. Check whether any include/exclude patterns overlap or are too broad to prove. +4. Identify likely paths the proposal omitted. +5. Return a corrected proposal and the reasons for each change, then stop. + +The caller decides whether to adopt the proposal. Plan remains the sole author +of a PlanPacket, and Validate independently compares proven changed paths with +the accepted scope. + +## Output + +```yaml +write_scope: + include: ["bounded/source/**"] + exclude: ["bounded/source/generated-by-other-owner/**"] +generated_companions: ["bounded/generated/**"] +gaps: [] +ambiguities: [] ``` ---- +## Checks -## Behavior Contract +- Patterns are normalized repository-relative paths. +- Includes cover the behavior without granting unrelated directories. +- Excludes do not contradict required changes. +- Generated companions are explicit. +- No ownership, scheduling, Git, hook, retry, release, or delivery state is + introduced. -When `.agents/scope.lock` declares one or more `frozen_dirs`: +## Failure behavior -- Any `Edit`, `Write`, or `Bash` tool call whose target path is **outside** every frozen directory is **rejected** by `hooks/edit-scope-guard.sh` with a structured stderr reason and a non-zero exit code (Claude Code converts that into a tool-use refusal). -- Edits to paths **under** any frozen directory are allowed. -- When the lock file is missing OR `frozen_dirs` is empty, the hook short-circuits with exit 0 (no enforcement; allow everything). -- The hook fails **open** on malformed JSON or missing target-path fields — do not block when the input contract is violated. Defensive default protects against harness changes. - -The lock file is written via `cli/internal/llmwiki/scope_guard.go:SafeAtomicWrite`, so concurrent `freeze` / `unfreeze` calls converge atomically (last writer wins, never tears). - ---- - -## Subcommands - -### `/scope freeze <dir>...` - -Append one or more directories to the frozen set. Idempotent; re-freezing an already-frozen directory is a no-op. Updates `acquired_at` (ISO-8601) and `acquired_by` (session id or PID) on every write. - -### `/scope unfreeze [<dir>]` - -Without arguments, clears the entire frozen set. With one or more directory arguments, removes just those entries. Removing a directory that is not frozen is a no-op. - -### `/scope status [--json]` - -Print the current lock state. With `--json`, emit a single JSON object matching the schema in [references/lock-file-format.md](references/lock-file-format.md). Without flags, print a human-readable summary including each frozen directory, the acquisition timestamp, and the acquiring session. - -### `/scope guard` (future combo skill) - -Reserved for a follow-up skill that combines `freeze` + status + spawn-orchestration. Not implemented in this release; documented here for forward reference. - ---- - -## Lock File Format - -`.agents/scope.lock` is a single JSON object. Full schema lives in [references/lock-file-format.md](references/lock-file-format.md). Key fields: - -- `schema_version` — currently `1` -- `frozen_dirs` — list of repo-relative directory prefixes (trailing slash optional) -- `acquired_at` — ISO-8601 UTC timestamp -- `acquired_by` — string identifying the writer (session id, PID, or label) - ---- - -## Output Specification - -**Artifact directory:** `.agents/` under the current repository, or the path selected explicitly through `AO_SCOPE_LOCK`/`--lock` for isolated validation. -**Filename convention:** `scope.lock`; status emits the same state to stdout, with `--json` producing one JSON object. -**Serialization/schema format:** JSON matching [lock-file-format](references/lock-file-format.md): `schema_version: 1`, string array `frozen_dirs`, nonempty RFC-3339 `acquired_at`, and string `acquired_by`. -**Validator command:** run `ao scope status --json | jq -e '.schema_version==1 and (.frozen_dirs|type=="array" and all(.[]; type=="string" and length>0)) and (.acquired_at|type=="string" and length>0) and (.acquired_by|type=="string")'`. -**Downstream handoff:** pass the lock path, normalized frozen directories, acquisition identity/time, attempted target, blocked-edit reason, and next safe action to the consuming workflow; a blocked edit enters pawl recovery before scope expansion or human escalation. - -## Quality Checklist - -- [ ] Every frozen directory is repo-relative, normalized, and tied to the current objective. -- [ ] `ao scope status --json` round-trips after freeze/unfreeze and passes the validator. -- [ ] In-scope and out-of-scope probes demonstrate the intended boundary before risky work. -- [ ] A rejection holds the write and consults the pawl; it never silently widens scope. -- [ ] Unfreeze happens at explicit release/closeout, not as a workaround for a failed command. - ---- - -## Examples - -### Freezing scope before a swarm wave - -**User says:** `/scope freeze cli/cmd/ao/ cli/internal/scope/` - -**What happens:** - -1. `ao scope freeze cli/cmd/ao/ cli/internal/scope/` writes `.agents/scope.lock` via `SafeAtomicWrite`. -2. `hooks/edit-scope-guard.sh` (registered as PreToolUse on `Edit|Write|Bash`) consults the lock on every subsequent tool call. -3. A worker that tries to `Write` to `skills/foo/SKILL.md` is rejected; a worker editing `cli/cmd/ao/scope.go` proceeds. - -### Releasing scope at the end of a wave - -**User says:** `/scope unfreeze` - -**What happens:** - -1. `ao scope unfreeze` rewrites `.agents/scope.lock` with `frozen_dirs: []`. -2. The hook short-circuits to exit 0 on the next tool call. - ---- - -## Notes - -- Wave 1 hardcodes the `.agents/scope.lock` path. Wave 2 (issue I5) migrates the path through `lib/ao-paths.sh`. -- The hook's defensive parse on malformed JSON is intentional. See [references/lock-file-format.md](references/lock-file-format.md) for the rationale. -- This skill is purely session-boundary (path-scope freezing within a session). Cron-cadence orchestration lives outside AgentOps on the orchestration substrate (the reference is NTM + MCP + managed-agents), not in an AgentOps-shipped daemon. -- Path-scope freezing handles *where* edits land. For a complementary lane that gates *what* commands run (`rm -rf`, `git reset --hard`, `DROP DATABASE`, `kubectl delete`, `terraform destroy`) — including allowlist layering, one-shot override codes, and PreToolUse wiring — see [references/destructive-command-guard-patterns.md](references/destructive-command-guard-patterns.md). Wire it alongside the scope guard when a wave touches infrastructure or shared data. -- When a workflow needs human approval, hook parity, or simultaneous command review rather than only path freezing, use [references/command-approval-and-hook-guardrails.md](references/command-approval-and-hook-guardrails.md). -- When authoring new hook behavior rather than using scope's existing guard, use the hook authoring guidance in `cc-hooks`. - -## References - -- [references/lock-file-format.md](references/lock-file-format.md) -- [references/destructive-command-guard-patterns.md](references/destructive-command-guard-patterns.md) -- [references/command-approval-and-hook-guardrails.md](references/command-approval-and-hook-guardrails.md) -- [references/scope.feature](references/scope.feature) — Executable spec: declare in-scope dirs, allow in-scope edits, hard-block out-of-scope edits via PreToolUse hook, report/release scope state (soc-qk4b) +If the scope cannot be made unambiguous from the supplied acceptance, report +the missing facts and stop. The caller may revise the intent in a new action. diff --git a/images/gemini/skills/security/SKILL.md b/images/gemini/skills/security/SKILL.md new file mode 100644 index 000000000..3b1bfc4e7 --- /dev/null +++ b/images/gemini/skills/security/SKILL.md @@ -0,0 +1,152 @@ +--- +name: security +description: 'Run authorized repository security scans for vulnerabilities, dependency risk, secrets, and binary policy. Triggers: "security", "run repository security scans for", "security skill".' +practices: +- supply-chain-integrity +- design-by-contract +- sre +hexagonal_role: driven-adapter +consumes: +- repo-context +produces: +- security-report.json +context_rel: +- kind: supplier-to + with: validate +skill_api_version: 1 +context: + window: fork + intent: + mode: task + sections: + exclude: + - HISTORY + intel_scope: topic +metadata: + capabilities: [security] + effects: [] + canonical_status: canonical + disposition: keep_specialist + graph_root: true + tier: product + dependencies: [] +output_contract: 'stdout: security scan report' +--- +# Security Skill + +> **Purpose:** Run repeatable security checks across code, scripts, authorized binaries, and repo-managed prompt surfaces. + +Use this skill for a caller-requested repository scan, authorized binary assurance, dependency risk, secrets, or offline prompt-surface redteam. + +## Critical Constraints + +- Scan only repositories, binaries, and prompt surfaces the operator owns or is explicitly authorized to assess. **Why:** a security review does not grant access to third-party systems or proprietary material. +- Keep collection read-only by default; do not exfiltrate secrets, execute destructive payloads, or mutate policy/baselines to manufacture green. **Why:** the assessment must not become the incident or erase its evidence. +- Treat missing/error scanners as a coverage gap, never a clean finding; use `--require-tools` when complete tool coverage is required. **Why:** absent evidence is not evidence of absence. +- Use the current agent and local shell; do not start another runtime or orchestration substrate unless explicitly requested. **Why:** repository scanning is a bounded operation, not permission to fan out. +- Run the selected scan once and report findings plus coverage gaps. Remediation, + risk acceptance, reruns, and promotion are caller decisions. + +## Security Surfaces + +1. **Repository gate:** `scripts/security-gate.sh` composes available scanners for quick/full/release checks. +2. **Composable suite:** `scripts/security_suite.py` provides static, dynamic, contract, baseline, and policy primitives for authorized binaries. +3. **Offline redteam:** `scripts/prompt_redteam.py` checks repo-owned prompt and tool-control surfaces against the attack pack. + +This is the canonical security runbook. Suite policy gating produces machine-consumable outputs, including `policy/policy-verdict.json` when a policy file is supplied. + +Read [the suite runbook](references/security-suite-runbook.md) before binary, policy, baseline, or redteam work. Use [the OWASP checklist](references/owasp-checklist.md) for code-level review. + +## Execution Workflow + +### 1) Quick gate + +Run: + +```bash +scripts/security-gate.sh --mode quick +``` + +**Checkpoint:** preserve the exit code and verify the reported `security-gate-summary.json` exists and parses before triage. + +### 2) Full scan + +Run: + +```bash +scripts/security-gate.sh --mode full +``` + +Add `--require-tools` when skipped scanners would invalidate the assurance claim. **Checkpoint:** report the result as incomplete unless the selected artifact validator and process both succeed. + +### 3) Scheduled gate + +Scheduled automation runs the full gate against the intended branch and retains its artifact directory. A failing scheduled run creates actionable tracked work; AgentOps itself does not supply the scheduler. + +### 4) Triage + +1. Open the latest artifact and identify scanner, severity, file, and coverage gaps. +2. Reproduce the finding with the narrowest safe command. +3. Rank concrete findings and preserve coverage gaps. +4. Stop. Remediation, risk acceptance, and any later scan are new caller decisions. Do not downgrade, suppress, or update a baseline merely to pass. + +## Output Specification + +**Artifact directory:** repository gates write `${SECURITY_GATE_OUTPUT_DIR:-${TMPDIR:-/tmp}/agentops-security}/<run-id>/`; composable-suite and redteam runs use their explicit `--out-dir`. + +**Filename convention:** repository gates require `security-gate-summary.json` (and raw `summary.json`); suite runs require `suite-summary.json`; redteam runs require `redteam/redteam-results.json`. + +**Serialization/schema format:** `security-gate-summary.json` is JSON with nonempty `mode`, `run_id`, `output_dir`, and `gate_status`, numeric `missing_tool_count`, boolean `require_tools`, and object `toolchain`. + +**Validator command:** with `OUT=<security-gate-run-dir>`, run `jq -e '(.mode|type)=="string" and (.mode|length)>0 and (.run_id|type)=="string" and (.run_id|length)>0 and (.output_dir|type)=="string" and (.output_dir|length)>0 and .gate_status=="PASS" and (.missing_tool_count|type)=="number" and (.require_tools|type)=="boolean" and (.toolchain|type)=="object"' "$OUT/security-gate-summary.json" >/dev/null`. + +**Output:** report the artifact path, command/exit code, mode, gate status, +missing-tool coverage, ranked findings, and authorization boundary. Do not add +an owner, next action, approval, release, or retry decision. + +## Quality Checklist + +- [ ] Target and authorization boundary are explicit; collection stayed within them. +- [ ] Scanner availability and skipped/error coverage are visible in the report. +- [ ] Findings include severity, location, reproducible evidence, and bounded remediation guidance. +- [ ] Artifacts contain no newly exposed secrets or unredacted sensitive payloads. +- [ ] The report distinguishes a passing scan from permission to promote or release. +- [ ] Suppressions, policy changes, baselines, and risk acceptance require explicit judgment. +- [ ] The report stops after evidence and contains no continuation decision. + +## Validation + +Run the skill and redteam validators: + +```bash +bash skills/security/scripts/validate.sh +bash tests/scripts/test-security-suite-redteam.sh +``` + +For a bounded suite smoke test, use an owned binary and a temporary output directory as shown in [the suite runbook](references/security-suite-runbook.md). + +## Examples + +- `/security` — run the quick repository gate, validate its summary, and report coverage/findings. +- `/security --full` — run the full scan once, preserve artifacts, and report coverage and findings. +- `/security run --binary "$(command -v ao)" --out-dir .tmp/security-suite/ao-current` — capture an authorized binary baseline via the composable suite. +- `/security collect-redteam --repo-root .` — run the offline attack pack over repo-owned control surfaces. + +## Troubleshooting + +| Problem | Response | +|---------|----------| +| Scanner missing/error | Record the coverage gap; install it or rerun with `--require-tools` when required | +| Local/CI mismatch | Compare scanner versions, config, mode, and both artifact directories | +| Suspected false positive | Reproduce narrowly; document any authorized suppression and its owner | +| Suite/baseline failure | Inspect the named compare/policy artifact; never refresh baseline reflexively | +| Redteam failure after wording change | Decide whether the control regressed or the attack-pack matcher needs intentional revision | + +## Reference Documents + +- [references/security-suite-runbook.md](references/security-suite-runbook.md) — binary/policy/baseline/redteam commands and artifacts +- [references/security.feature](references/security.feature) — repository-gate executable spec +- [references/security-suite.feature](references/security-suite.feature) — composable-suite executable spec +- [references/owasp-checklist.md](references/owasp-checklist.md) — OWASP Top 10 review +- [references/agentops-redteam-pack.json](references/agentops-redteam-pack.json) — offline attack pack +- [references/policy-example.json](references/policy-example.json) — starter policy diff --git a/images/gemini/skills/shared/SKILL.md b/images/gemini/skills/shared/SKILL.md new file mode 100644 index 000000000..2ed54cb21 --- /dev/null +++ b/images/gemini/skills/shared/SKILL.md @@ -0,0 +1,36 @@ +--- +name: shared +description: 'Shared runtime and evidence references loaded only by a consuming skill. Triggers: internal shared contracts.' +practices: [design-by-contract, pragmatic-programmer] +hexagonal_role: domain +consumes: [] +produces: [reference-documents] +context_rel: [] +skill_api_version: 1 +user-invocable: false +metadata: + tier: library + dependencies: [] + capabilities: [provide_reference_context] + effects: [] + canonical_status: canonical + disposition: keep_specialist + internal: true +output_contract: reference documents loaded just in time +--- + +# Shared References + +Shared files describe runtime capabilities and evidence formats. They are +context, not permission to start a runtime, tracker, substrate, network call, +or external mutation. + +- Default to the current agent and local shell. +- Use a runtime-native fresh context only when the caller or consuming workflow + requests it. +- Treat runtime and factory state as adapter evidence; never translate it into + core Plan, Candidate, RPI, or verdict state. +- Missing optional tools degrade only the optional capability that needs them. +- Source skill contracts and executable behavior outrank shared prose. + +The core loop has no hard dependency on this library. diff --git a/images/gemini/skills/skill-builder/SKILL.md b/images/gemini/skills/skill-builder/SKILL.md index 55e09b96b..0022e9487 100644 --- a/images/gemini/skills/skill-builder/SKILL.md +++ b/images/gemini/skills/skill-builder/SKILL.md @@ -1,17 +1,13 @@ --- name: skill-builder -description: 'Scaffold or absorb new SKILL.md files against the unified AgentOps template. Triggers: "create a skill", "scaffold skill", "absorb external skill", "new skill".' +description: 'Create a metadata-complete AgentOps skill source package and regenerate its derived projections. Triggers: "create a skill", "scaffold skill", "absorb external skill", "new skill".' practices: -- code-complete - pragmatic-programmer -- design-patterns hexagonal_role: supporting consumes: [] produces: -- converted-skill +- skill-source-package context_rel: -- kind: customer-of - with: automation-shape-routing - kind: supplier-to with: heal-skill skill_api_version: 1 @@ -24,202 +20,88 @@ context: - HISTORY intel_scope: topic metadata: + capabilities: [skill_builder] + effects: [writes_skill_source, regenerates_skill_projections] + canonical_status: canonical + disposition: keep_specialist tier: meta - dependencies: - - heal-skill - - converter + dependencies: [] stability: experimental output_contract: skills/skill-builder/schemas/build-report.json --- -# /skill-builder — Scaffold or absorb a new SKILL.md +# /skill-builder — Create one skill source package -Materializes a new skill against the unified template at `references/skill-template.md` (extracted from anthropics/financial-services). Runs the heal-skill deep audit on the new skill as a self-check before declaring success. +Create one `skills/<slug>/` package, verify its structure, regenerate the +metadata-owned projections, and stop. The builder does not schedule work, +allocate writers, operate Git, promote learnings, or decide whether the new +skill should be invoked. -> **If unsure whether the work should be a skill, a Workflow, or an NTM swarm, run `/automation-shape-routing` first** — it is the front door that decides the shape and hands off to the right builder. +Before creating a new root, search `skills/*/SKILL.md` for an existing owner. +Extend an existing skill when it already owns the requested behavior. -> **Before creating, search for an existing owner.** `ms search` the proposed skill's trigger phrases (fast path when available — `command -v ms`, or the `mcp__ms__search` tool is attached; else grep `skills/**/SKILL.md` + `docs/SKILLS.md`). A strong hit means **extend that skill, don't create a new one** — this is one item of the fuller skill-admission checklist (bead age-7d3r). +## Inputs -## ⚠️ Critical Constraints +Choose exactly one mode: -- **Template is canonical.** All four modes produce SKILL.md files conforming to `references/skill-template.md`. Do not invent ad-hoc structures. **Why:** the heal-skill deep audit validates against this template; drift creates audit false-fails. -- **Self-audit is mandatory.** After every successful build, the build script invokes the heal-skill deep audit (`audit.sh` in `skills/heal-skill/`) against the new skill directory. A FAIL verdict aborts the build. **Why:** PR-002 (external validation gate) — the builder must not declare its own work complete. -- **Codex parity is day-1, not later.** `from-scratch`, `from-template`, and `absorb-external` modes must produce both `skills/<name>/SKILL.md` AND `skills-codex/<name>/SKILL.md` + `skills-codex/<name>/prompt.md`. **Why:** finding `2026-05-03-codex-skill-shape-is-dual-file` — codex SKILL.md uses slim frontmatter (no `skill_api_version`); prompt.md is mandatory; `audit-codex-parity.sh` is a content scanner that won't catch frontmatter drift. -- **Editing an EXISTING skill also needs a manual twin mirror.** When you change `skills/<name>/references/*.md` or `SKILL.md`, manually mirror the content into `skills-codex/<name>/` (runtime-native), THEN run `scripts/regen-codex-hashes.sh --only <name>`. `make regen-all` only refreshes the twin's *hash record*, not its prose — a green `✓ codex hashes` over a stale twin looks handled but isn't. Verify with a content diff (`grep -c <new-token>` on both copies), not the hash exit code. **Why:** finding `2026-06-16-codex-twin-content-not-auto-mirrored` (age-aqu/age-yxl) — regen made the marker self-consistent with a stale twin (0-vs-2 token divergence) and nothing complained. The parity gate now blocks an un-mirrored `references/**` edit, but the mirror is still a manual step. -- **250-line ceiling on new SKILL.md.** Use `references/` for overflow. **Why:** finding `f-2026-05-01-025` — every Skill() invocation reloads 5-15KB; multi-lifecycle sessions compound to 150-200KB+ pure scaffolding. -- **Clean-room factory inputs only.** When using lessons learned from external corpora, read [references/agentops-skill-factory.md](references/agentops-skill-factory.md) and use only AgentOps-owned summaries, scripts, and rubrics. **Why:** productization must improve structure without copying protected third-party skill content. -- **Real gate means exit code.** Validate with `heal-skill --check --strict <skill-dir>` and the heal-skill deep audit (`audit.sh`); never infer green from grep/regex output. **Why:** regex presence checks created false-greens during the 2026-06 scale build. -- **One skill directory = one writer.** Bulk builds fan out only when each worker owns a distinct new `skills/<name>/` plus `skills-codex/<name>/`; edits to existing skill dirs run in a later serial wave. **Why:** concurrent writers deleted untracked work and flipped HEAD mid-task. -- **Trust repository state, not subagent reports.** Before declaring success, inspect `git status`, generated hashes, final files, and gate exit codes. **Why:** sandbox-overlay and stale self-reports can claim work that never persisted. -- **Clean-room includes names.** Do not reuse exact third-party skill names; mint AgentOps-owned names before source skills, Codex mirrors, or wrappers are keyed. **Why:** provenance/IP safety applies to labels as well as prose and scripts. -- **Do not use the Workflow tool as the skill factory.** For scale authoring, use deterministic wave scripts or NTM/Agent Mail lanes with one worker per skill. **Why:** skill creation needs file ownership and durable git evidence, not opaque background self-reporting. +- `from-scratch <slug>` creates a blank source package. +- `from-template <slug> --like <existing-slug>` uses the existing skill only + for metadata defaults; it does not copy its prose. +- `absorb-external <slug> --from <path>` verifies the source exists, then + creates a clean-room blank package without copying names, prose, prompts, + scripts, or examples. -## Modes +The caller may set `SKILL_TIER`, `SKILL_DEPENDENCIES`, +`SKILL_CAPABILITIES`, and `SKILL_EFFECTS`. Values that represent lists must be +JSON arrays. -| Mode | Status | Description | -|------|--------|-------------| -| `from-scratch` | stable | Interactive scaffold from canonical template. Produces full skill skeleton + scripts/validate.sh + codex parity. | -| `from-template` | stable | `--like <existing-skill>` copies structure from a sibling skill, swaps domain-specific sections. | -| `absorb-external` | stable | Observes external package shape, synthesizes a clean-room AgentOps skill, and invokes `/converter` for Codex parity. | -| `from-pattern` | **alpha (passthrough)** | Delegates to `ao flywheel close-loop`. Outputs land at `.agents/knowledge/promoted/` per flywheel rules — they are NOT yet shaped as SKILL.md drafts. v2 will add skill-specific synthesis. Use `from-scratch` or `absorb-external` for SKILL.md output today. | +## Procedure -## Workflow +1. Run `scripts/build.sh` with one mode and one new slug. +2. Fill the generated placeholders with the skill's actual behavior. +3. Run `skills/heal-skill/scripts/heal.sh --check --strict skills/<slug>`. +4. Run `scripts/generate-skill-mesh.py` to derive the catalog, registry, + router, graph, maps, counts, and runtime image manifests from `SKILL.md` + metadata. +5. Run `scripts/codex-sync.sh --only <slug>` and + `scripts/regen-codex-hashes.sh --only <slug>` to derive the Codex twin. +6. Inspect the generated diff. Validation and delivery remain caller-owned. -### Phase 1: Mode dispatch +`build.sh` performs steps 1, 3, 4, and 5 once. It never retries or chooses a +next action. -`scripts/build.sh` reads `$1` and routes: +## Output -```bash -build.sh from-scratch <new-skill-name> # → init.sh --interactive -build.sh from-template <new-skill-name> --like council -build.sh absorb-external <new-skill-name> --from /path/to/SKILL.md -build.sh from-pattern # → ao flywheel close-loop +The source package contains: + +```text +skills/<slug>/ +├── SKILL.md +└── scripts/validate.sh ``` -**Checkpoint:** Confirm with user the new skill's `metadata.tier` and `metadata.dependencies` before generation. +The build report is `.agents/audits/<slug>-build.json` and conforms to +`schemas/build-report.json`. Generated inventories and runtime projections are +not additional sources of truth. -### Phase 2: Materialize from template +## Checks -`scripts/init.sh` reads `references/skill-template.md` (the canonical template section) and renders a SKILL.md skeleton with frontmatter pre-filled. For `from-template`, structure is copied from the source skill; section bodies are blanked and replaced with template stubs. +- The slug and frontmatter `name` match. +- Metadata declares `tier`, `dependencies`, `capabilities`, `effects`, + `canonical_status`, and `disposition`. +- Every hard dependency names a live skill. +- The generated package contains no Git, tracker, queue, retry, release, or + delivery behavior. +- External material is treated only as a signal that a clean-room skill may be + useful; its content is not copied. -For `absorb-external`, inspect only metadata and package shape, then synthesize -new AgentOps-owned content. Never copy external prose, prompts, scripts, -examples, names, or sentinel text into either generated runtime treatment. +## Failure behavior -**Checkpoint:** `heal-skill --check --strict skills/<new-name>` exits 0. - -### Phase 3: Codex parity - -`scripts/init.sh` invokes `/converter skills/<new-name> codex` to produce `skills-codex/<new-name>/{SKILL.md,prompt.md}`. Then trims `skill_api_version` from the codex SKILL.md (converter may preserve it). Asserts `prompt.md` exists. - -**Checkpoint:** `bash scripts/audit-codex-parity.sh` returns clean AND `grep -q "^skill_api_version:" skills-codex/<name>/SKILL.md` returns nothing. - -### Phase 4: Self-audit - -The build script tail invokes the heal-skill deep audit on `skills/<new-name>`. WARN is acceptable for v1 skills (e.g., `experimental` stability). FAIL aborts. - -**Checkpoint:** `audit_pass=true` in build report. - -### Phase 5: Factory score overlay - -For AgentOps skill upgrades, use the productization score as a patch selector, -not as a replacement for the deep audit: - -```bash -python3 skills/heal-skill/scripts/score_agentops_skill.py skills/<name> --markdown -``` - -Choose the smallest patch that improves the score while preserving the -canonical template and Codex parity constraints. - -### Phase 6: Scale factory discipline - -For more than one skill, run in ownership waves: - -1. Create-only wave: one worker per new skill directory. -2. Mutate wave: existing skill directories only after source creation settles. -3. Mirror/package wave: Codex mirrors and generated hashes after the canonical - source corpus is complete. - -Every wave ends with `git status`, `scripts/regen-all.sh --check`, and the -relevant target gates by exit code. If ownership overlaps, stop and rescope. - -## Output Specification - -**Artifact directory:** `.agents/audits/`. -**Filename convention:** `<skill>-build.json`. -**Serialization/schema format:** JSON matching `schemas/build-report.json`. -**Validator command:** `jq -e . .agents/audits/<skill>-build.json`. -**Downstream handoff:** consumed by heal-skill and the validation wave. - -**Files created (from-scratch mode):** - -``` -skills/<name>/ -├── SKILL.md (≤250 lines, full template spine) -├── scripts/ -│ └── validate.sh (self-validation per AgentOps convention) -└── references/ (externalize before the profile's 250-line kernel limit) -skills-codex/<name>/ -├── SKILL.md (slim frontmatter — no skill_api_version) -└── prompt.md (~10-20 line Execution Profile) -``` - -## Quality Rubric - -- [ ] All four modes produce skills that pass the heal-skill deep audit with PASS or WARN (not FAIL) -- [ ] `heal-skill --check --strict` exits 0 for every generated source and Codex skill directory -- [ ] Codex parity files exist and pass slim-frontmatter check -- [ ] Batch authoring has one writer per skill directory and validates persisted git state -- [ ] Clean-room review covers exact names as well as prose, scripts, and examples -- [ ] No SKILL.md exceeds 250 lines (overflow goes to `references/`) -- [ ] Build report JSON validates against `schemas/build-report.json` -- [ ] `from-pattern` mode prominently marked alpha/passthrough in user output - -## Examples - -**Create a new skill from scratch:** - -```bash -/skill-builder from-scratch hello-world -# → interactive prompt: tier? deps? primary deliverable? -# → writes skills/hello-world/SKILL.md + skills-codex/hello-world/{SKILL.md,prompt.md} -# → runs the heal-skill deep audit on the new skill -``` - -**Clone structure from an existing skill:** - -```bash -/skill-builder from-template my-new-skill --like council -# → mirrors council's section spine; substitutes new metadata -``` - -**Absorb a skill from anthropics/financial-services:** - -```bash -/skill-builder absorb-external dcf-helper \ - --from ~/dev/financial-services/plugins/vertical-plugins/financial-analysis/skills/dcf-model/SKILL.md -# → observes package shape and emits only clean-room AgentOps-owned content -``` - -## Troubleshooting - -| Problem | Cause | Solution | -|---------|-------|----------| -| Self-audit FAIL | Generated SKILL.md missing required Pass-2 check | Re-run with `--verbose`; inspect which check failed; usually `output-spec-explicit` or `trigger-clarity` | -| Codex parity drift | `/converter` preserved `skill_api_version` | `init.sh` runs `sed -i '/^skill_api_version:/d' skills-codex/<name>/SKILL.md`; verify with grep | -| SKILL.md > 250 lines | Mode generated too much inline content | Move section bodies to `references/<topic>.md`; reference inline as `[text](references/<topic>.md)` | -| `from-pattern` produces no SKILL.md | Expected behavior — passthrough only in v1 | Use `from-scratch` or `absorb-external` if you need a SKILL.md draft | - -## Corpus authoring health - -Skill selection is pure LLM reasoning over the `description` field, so a missing -trigger phrase is a skill that silently never fires. The per-skill auditor checks -this only as a WARN, so the gap accumulates. Audit the whole corpus at once: - -```bash -python3 skills/skill-builder/scripts/scan_descriptions.py skills # remediation report -python3 skills/skill-builder/scripts/scan_descriptions.py skills --strict # exit 1 on any miss -``` - -The scanner mirrors the deep audit's three-form trigger detection and adds a -suggested `Triggers:` stub per offender. See -[references/skill-authoring-standard.md](references/skill-authoring-standard.md) -for the full authoring doctrine and the best-practice-to-enforcement crosswalk. - -## See Also - -- [heal-skill](../heal-skill/SKILL.md) — structural hygiene (heal.sh) + companion deep-audit gate (audit.sh), invoked by build self-check -- [converter](../converter/SKILL.md) — produces codex parity artifacts -- [scaffold](../scaffold/SKILL.md) — scaffolds projects/components/CI (NOT skills) -- [curate](../postmortem/SKILL.md) — `--mode=forge` mines transcripts into learnings (different layer) +Any invalid input, structural failure, projection failure, or Codex sync +failure exits nonzero after one attempt. The caller decides whether to revise +or invoke the builder again. ## References -- [skill-conformance-profiles.yaml](references/skill-conformance-profiles.yaml) — authoritative `repo-runtime` semantics and severities - -- [references/skill-template.md](references/skill-template.md) — canonical SKILL.md template + auditor checklist + PRODUCT.md alignment -- [references/agentops-skill-factory.md](references/agentops-skill-factory.md) — clean-room factory workflow and productization rules -- [references/skill-authoring-standard.md](references/skill-authoring-standard.md) — clean-room best-practices doctrine + best-practice-to-enforcement crosswalk; backs the `scan_descriptions.py` trigger scanner -- [references/skill-builder.feature](references/skill-builder.feature) — Executable spec: mode dispatch, materialize from template, Codex parity bundle, self-audit + factory score (soc-qk4b) -- [references/hyper-extract-design-steals.md](references/hyper-extract-design-steals.md) — authoring rules stolen from Hyper-Extract: the WHAT-vs-HOW (schema vs guideline) contract, canonical `{from}\|{rel}\|{to}` identifier dedup-key form, and folded graph-designer/yaml-validator/template-optimizer patterns (age-bp1) +- [skill template](references/skill-template.md) +- [heal-skill](../heal-skill/SKILL.md) diff --git a/images/gemini/skills/standards/SKILL.md b/images/gemini/skills/standards/SKILL.md new file mode 100644 index 000000000..afaeadf94 --- /dev/null +++ b/images/gemini/skills/standards/SKILL.md @@ -0,0 +1,56 @@ +--- +name: standards +description: 'Load only the language and risk standards relevant to a caller-supplied change, then report concrete findings.' +practices: +- pragmatic-programmer +- clean-code +hexagonal_role: supporting +consumes: [] +produces: +- stdout +context_rel: [] +skill_api_version: 1 +metadata: + capabilities: [standards] + effects: [] + canonical_status: canonical + disposition: keep_specialist + tier: knowledge + dependencies: [] +output_contract: cited standards and factual findings +--- +# Standards — focused engineering guidance + +Load the smallest set of standards justified by the caller's files, language, +and risks. Do not preload the entire reference corpus. + +## Procedure + +1. Record the supplied paths, language, change type, and risk cues. +2. Load `common-standards.md` plus only the matching language or checklist + references. +3. Compare the supplied artifact to those sources. +4. Return cited findings with path and line when possible, plus checked and + not-checked scope. +5. Stop. + +This skill provides context and findings. It does not edit, validate, retry, +approve, commit, release, deliver, or decide continuation. + +## References + +- [Common standards](references/common-standards.md) +- [Go](references/go.md) +- [Python](references/python.md) +- [Rust](references/rust.md) +- [TypeScript](references/typescript.md) +- [JavaScript](references/javascript.md) +- [Shell](references/shell.md) +- [JSON](references/json.md) +- [YAML](references/yaml.md) +- [Markdown](references/markdown.md) +- [SQL safety](references/sql-safety-checklist.md) +- [Race conditions](references/race-condition-checklist.md) +- [LLM trust boundaries](references/llm-trust-boundary-checklist.md) +- [Skill structure](references/skill-structure.md) +- [Test strategy](references/test-pyramid.md) diff --git a/images/gemini/skills/status/SKILL.md b/images/gemini/skills/status/SKILL.md index af482397d..d0408da99 100644 --- a/images/gemini/skills/status/SKILL.md +++ b/images/gemini/skills/status/SKILL.md @@ -1,192 +1,38 @@ --- name: status -spine: true -description: 'Show AgentOps work status. Triggers: "status", "show agentops work status.", "status skill".' -practices: -- dora-metrics -- sre +description: 'Report observable AgentOps evidence without selecting work. Triggers: "status", "show AgentOps status".' +practices: [dora-metrics, sre] hexagonal_role: driving-adapter -consumes: -- br -produces: -- stdout +consumes: [] +produces: [stdout] context_rel: [] skill_api_version: 1 allowed-tools: Read, Grep, Glob, Bash model: haiku context: window: inherit - intent: - mode: none + intent: {mode: none} intel_scope: none metadata: + capabilities: [status] + effects: [] + canonical_status: canonical + disposition: keep_specialist graph_root: true tier: session - dependencies: [sbh] -output_contract: 'stdout: dashboard' + dependencies: [] +output_contract: read-only status snapshot --- -# /status — Workflow Dashboard -> **Purpose:** Produce a one-screen, evidence-backed answer to: what is active, what passed or failed recently, and what exact action should happen next? +# Status -**YOU MUST EXECUTE THIS WORKFLOW. Do not just describe it.** +Report only observable local facts: available intent, candidate, and verdict +artifacts; their digests and timestamps; deterministic check results; and +unavailable or corrupt sources. Label staleness and uncertainty explicitly. -## Critical Constraints +Status does not inspect work queues, assign priority, claim work, infer a next +action, repair records, govern retries, or change any state. Optional Git or +tracker metadata may be displayed only when the caller supplies it; absence +cannot change the report interpretation. -- Read live repository, tracker, gate, and artifact state; never infer progress from conversational memory. **Why:** status is a truth surface, not a narrative summary. -- Distinguish `PASS`, `WARN`, `FAIL`, `UNAVAILABLE`, and `UNKNOWN`; never render a missing source as healthy or empty. **Why:** fail-soft collection must preserve coverage gaps. -- Use exact issue ids, branch/worktree state, commit ids, verdict paths, and timestamps when available. **Why:** the dashboard must remain resumable after compaction. -- Keep collection read-only. Do not close beads, clean worktrees, delete stale files, start substrates, or mutate state while reporting it. **Why:** observation must not change the system being observed. -- Use the current agent and local shell; do not start NTM, Agent Mail, managed agents, Gas City, or another runtime unless explicitly requested. **Why:** a dashboard query does not authorize orchestration. -- `WARN|FAIL|REFUTED -> AUTO-REDO`: consult the pawl, repair collection/parsing/rendering, and rerun the same source. **Why:** an ordinary status defect is recovery evidence, not a human andon. -- `BREAKER -> HOLD -> ONE-HELPER`; `HELPER-UNSTUCK -> AUTO-REDO`. Hold the affected claim and use one bounded local-shell helper to reconcile contradictory live sources. **Why:** one recovery pass can separate stale cache from a genuine split-brain state. -- `HELPER-ESCALATE -> HUMAN`; `REFUSAL-LANE|EXPLICIT-JUDGMENT|EXHAUSTED-BUDGET -> HUMAN`. **Why:** unresolved authority, contradictory release truth, refusal, or exhausted recovery requires the operator. - -## Quick Start - -```bash -/status # Full dashboard -/status --json # Machine-readable output -/status --recover # Post-compaction continuation view -``` - -`quickstart` routes to the same next-action decision table. CLI dependencies -(`ao`, `br`, and optional inbox tools) are capability-probed; missing tools are -reported in coverage instead of treated as empty state. - -## Recovery Mode - -For `--recover` or post-compaction re-orientation: - -1. Run the normal gather below. -2. Read the newest `.agents/handoff/*.md` and `.agents/rpi/execution-packet.json` when present. -3. Re-read `AGENTS.md` before resuming a claimed bead. -4. Report the in-flight objective, exact next action, and claimed-but-unfinished beads. - -Use [the recovery playbook](references/recovery-playbook.md) only when the normal continuation surfaces are insufficient. - -## Execution Workflow - -### 1. Gather live state - -Run independent read-only calls in parallel when the runtime supports it: - -```bash -# Reconciliation and gate truth -ao reconcile --json 2>/dev/null || echo RECONCILE_UNAVAILABLE - -# AgentOps tracker (ao resolves the private br ledger) -ao beads exec list --type epic --status open --json 2>/dev/null || echo EPIC_UNAVAILABLE -ao beads exec list --status in_progress --json 2>/dev/null || echo IN_PROGRESS_UNAVAILABLE -ao beads exec ready --json 2>/dev/null || echo READY_UNAVAILABLE - -# Ratchet and task truth -ao ratchet status --json 2>/dev/null || echo RATCHET_UNAVAILABLE -ao task-status --json 2>/dev/null || echo TASK_STATUS_UNAVAILABLE - -# Repository truth -git branch --show-current -git log --oneline -3 -git status --short -``` - -Also inspect, when present: - -- `.agents/ao/chain.jsonl` for the last ratchet transition; -- `.agents/council/` and `.agents/pawl-verdicts/` for recent verdict artifacts; -- `.agents/signals/session-quality.jsonl` for the last ten quality signals; -- `.agents/ao/sessions/` for recent session summaries; -- `.agents/learnings/`, `.agents/patterns/`, and `.agents/forge/` for corpus counts. - -**Checkpoint:** every displayed value must have a live command or file source; record unavailable and malformed sources in `coverage` before rendering. - -### 2. Normalize facts - -Normalize into the schema in [dashboard-contract](references/dashboard-contract.md): - -- `current_work`: active epic, in-progress/ready ids, ratchet phase, and git state; -- `latest_gates`: reconciliation plus recent independent verdicts; -- `next_action`: first matching priority and one executable action; -- `coverage`: one entry per attempted source with `available|unavailable|malformed`. - -Never merge contradictory values silently. Prefer executable/generated truth by -repository precedence, show the disagreement, and route a high-severity conflict -to the next action. - -### 3. Choose the next action - -Evaluate top-to-bottom and stop at the first match: - -| Priority | Condition | Next action | -|---:|---|---| -| 0 | Reconciliation has a high finding or sources contradict | Resolve the named reconciliation blocker | -| 1 | Recent WARN/FAIL/REFUTED verdict exists | Repair its named findings and rerun the same gate | -| 2 | Claimed/in-progress bead exists | Resume the exact bead and worktree | -| 3 | Uncommitted changes exist | Validate the current diff | -| 4 | Ready bead exists | Implement the first ready id | -| 5 | Research complete without a plan | Run `/plan` | -| 6 | Plan exists without implementation | Run `/implement <id>` | -| 7 | Pending knowledge items exist | Inspect and promote or discard them deliberately | -| 8 | Clean state | Start `/research` or `/plan` | - -**Checkpoint:** the suggestion must cite the fact that selected it and must not -recommend new backlog work while a higher-priority blocker or claimed bead exists. - -### 4. Render - -Default output uses exactly three blocks in this order: `Current Work`, `Latest -Gates`, `Next Action`, followed by a compact coverage note. `--json` emits only -the schema object. Do not wrap JSON in explanatory prose. - -The complete visual and JSON contract lives in -[dashboard-contract](references/dashboard-contract.md); keep this kernel focused -on collection, precedence, and decision behavior. - -## Output Specification - -**Artifact directory:** stdout by default; recovery reads existing artifacts under `.agents/` but creates none. - -**Filename convention:** no file for normal output; when a caller explicitly captures JSON, use `status-<UTC-timestamp>.json`. - -**Serialization/schema format:** human output is the three-block dashboard; `--json` is one JSON object with `schema_version`, `generated_at`, `current_work`, `latest_gates`, `next_action`, and `coverage` as defined in [dashboard-contract](references/dashboard-contract.md). - -**Validator command:** with `OUT=<captured-status.json>`, run `jq -e '.schema_version==1 and (.generated_at|type)=="string" and (.current_work|type)=="object" and (.latest_gates|type)=="object" and (.next_action|type)=="object" and (.next_action.priority|type)=="number" and (.next_action.message|type)=="string" and (.coverage|type)=="array" and all(.coverage[]; (.source|type)=="string" and (.status=="available" or .status=="unavailable" or .status=="malformed"))' "$OUT"`. - -**Downstream handoff:** pass the generated timestamp, exact active ids/worktree/commit, latest gate verdicts and artifact paths, selected priority/fact/action, and all unavailable or contradictory sources to the operator or recovery workflow. - -## Quality Checklist - -- [ ] Every displayed fact is backed by a current command or file artifact. -- [ ] Missing and malformed sources appear in coverage, never as healthy or empty. -- [ ] Tracker, git, verdict, and reconciliation state use exact ids and timestamps. -- [ ] The next action is the first applicable priority and cites its selecting fact. -- [ ] Human and JSON outputs describe the same normalized state. -- [ ] Collection remained read-only and did not start an orchestration substrate. -- [ ] WARN/FAIL/REFUTED consulted the pawl before any human andon. - -## Examples - -### Resume an active landing - -**User says:** `/status` - -**What happens:** live `br`, remote git, reconciliation, and verdict artifacts -show one in-progress bead with a CONFIRMED verdict but no remote-main bind. - -**Result:** `Next Action` says to resume that exact bead/worktree and complete -the canonical land door; it does not suggest unrelated ready work. - -## Troubleshooting - -| Problem | Response | -|---|---| -| `ao` unavailable | Report tracker/reconciliation coverage unavailable; show git and file-backed facts only | -| Tracker and handoff disagree | Prefer live tracker, show the stale handoff timestamp, select reconciliation as next action | -| Malformed JSON | Preserve the source/exit code, mark malformed, and rerun the narrow command | -| Suggested action conflicts with intent | Show the selecting fact and priority; explicit operator intent may replace the suggestion | - -## Reference Documents - -- [references/dashboard-contract.md](references/dashboard-contract.md) — stable visual layout, JSON schema, and valid/invalid examples -- [references/status.feature](references/status.feature) — executable dashboard behavior -- [references/recovery-playbook.md](references/recovery-playbook.md) — deep recovery when normal continuation artifacts are insufficient +Return the snapshot and stop. diff --git a/images/gemini/skills/swarm/SKILL.md b/images/gemini/skills/swarm/SKILL.md index 11018141b..3b95ec0ec 100644 --- a/images/gemini/skills/swarm/SKILL.md +++ b/images/gemini/skills/swarm/SKILL.md @@ -1,166 +1,42 @@ --- name: swarm -description: 'Dispatch parallel agents. Triggers: "swarm", "dispatch parallel agents.", "swarm skill".' -practices: -- microservices -- team-topologies -- mythical-man-month -hexagonal_role: supporting -consumes: -- implement -- validate -produces: -- .agents/swarm/results/*.json -context_rel: -- kind: customer-of - with: crank +description: 'Dispatch explicit disjoint packets exactly once through a caller-selected executor. Triggers: "swarm", "dispatch disjoint packets", "parallel explicit tasks".' +practices: [team-topologies, design-by-contract] +hexagonal_role: driving-adapter +consumes: [explicit-disjoint-packets] +produces: [per-packet-results] +context_rel: [] skill_api_version: 1 -context: - window: fork - intent: - mode: task - sections: - exclude: - - HISTORY - intel_scope: full +user-invocable: true metadata: - tier: orchestration - dependencies: - - implement - - validate -output_contract: .agents/swarm/results/*.json + tier: execution + dependencies: [] + capabilities: [dispatch_once] + effects: [invoke_selected_executor] + canonical_status: canonical + disposition: keep_optional_adapter +output_contract: per-packet candidate, evidence, or error --- -# Swarm Skill -Execute an explicitly authorized parallel wave with fresh workers, disjoint -ownership, file-backed results, and independent validation. Default to one -agent and sequential work when a valid wave cannot be proven. +# Swarm -## Critical Constraints +Swarm exposes one optional factory port: -- **Why: parallelism has overhead.** Swarm only when the operator/workflow asks - for it and at least two independent lanes materially benefit. -- **Why: prevent collisions.** Every task declares issue id, `metadata.issue_type`, - exact files, validation, output, owner, base SHA, and discard path before spawn. -- **Why: derived files collide too.** Registry, Codex manifest, schemas, - migrations, CLI surfaces, and generated projections count as shared writes. -- **Why: avoid shared-checkout corruption.** Give writers isolated worktrees; - when an explicitly requested shared multi-writer workflow needs reservations, - reserve through Agent Mail before edits. -- **Why: substrates are operator choices.** Do not start NTM, Agent Mail, Gas - City, managed agents, or a runtime merely because it exists. -- **Why: self-report is not proof.** Require RED evidence, commit SHA, test tail, - changed files, and conflicts; independently gate every landed slice. -- **Why: bound failures.** Maximum 4-6 workers per wave and two retries per task; - scope escapes become follow-up work, never unauthorized edits. - -## Local Mode and Routing - -| Shape | Route | -|---|---| -| one deliverable or uncertain ownership | sequential current agent | -| read-only independent perspectives, explicitly requested | bounded in-session fan-out | -| ≥2 disjoint working-tree slices | this wave executor | -| persistent pane roles explicitly requested | [`ntm`](../ntm/SKILL.md) + [`agent-native`](../agent-native/SKILL.md) and optional Agent Mail | -| city-shaped durable work explicitly selected | `using-gc`, with membrane close door | - -`/crank` owns wave admission; swarm executes admitted waves. Read -[execution-steps.md](references/execution-steps.md) for the full mechanics and -[pre-spawn-friction-gates.md](references/pre-spawn-friction-gates.md) for base, -manifest, dependency, alignment, and wave-cap gates. - -## Workflow - -1. **Confirm authorization and value.** Require explicit parallel-work intent - and at least two ready lanes. Otherwise report the sequential route. -2. **Build task packets.** Each task carries id, subject, behavior, exact file - manifest, dependencies, `metadata.issue_type`, validation/RED command, - expected result path, base SHA, worktree, and cleanup/discard plan. -3. **Prove wave validity.** Topologically select unblocked tasks; reject any - write/write or read/write overlap, including generated companions. Serialize - coupled chains. Display the ownership matrix before spawning. -4. **Choose only the authorized backend.** Codex sub-agents, Claude teams, - background tasks, inline fallback, NTM+Agent Mail, or GC are adapters—not - automatic routing authority. Read only the selected backend reference. -5. **Isolate and dispatch.** Create/verify one worktree per writer when needed, - assign ownership before spawn, and give each worker one bounded task. Workers - must not claim extra work or edit outside their manifest. -6. **Collect file-backed results.** Workers write - `.agents/swarm/results/<issue-id>.json`; coordination messages are short - signals. A scope escape appends `.agents/swarm/scope-escapes.jsonl`. -7. **Validate and integrate deterministically.** Verify persistence, changed - paths, RED→green evidence, tests, conflicts, and commit ancestry. Merge in - declared order, run wave-level gates, then route each slice through PAWL. -8. **Retry or stop.** Correct a worker at most twice. On collision, changed - scope, gate failure, or exhausted retry, stop that lane and re-plan. -9. **Cleanup.** Close ephemeral workers. Reap a worktree only after its feature - commit is an ancestor of trunk; a closed tracker item alone is insufficient. - -## Worker Result Contract - -```json -{ - "issue_id": "age-x.1", - "status": "done", - "files_changed": ["path/file"], - "commit_sha": "<sha>", - "red_evidence": "<failing command/output before implementation>", - "test_tail": "<verbatim final output>", - "conflicts_surfaced": [], - "worktree_path": "<absolute path>" -} +```text +dispatch_once(explicit_disjoint_packets, executor) + -> per-packet candidate | evidence | error ``` -Missing `commit_sha`, `red_evidence`, or `test_tail` means unverified. See -[validation-contract.md](references/validation-contract.md) and -[worker-specs.md](references/worker-specs.md). +The caller supplies every complete packet, proves their write scopes disjoint, +and chooses the executor. Swarm dispatches each packet once, preserves packet and +context identities, collects results, and stops. -## Output Specification +The reference implementation is [`scripts/dispatch_once.py`](scripts/dispatch_once.py). +It validates the entire explicit batch before the first call, invokes the supplied +executor exactly once for each packet, and returns executor exceptions as factual +per-packet errors. -- **Artifact directory:** `.agents/swarm/results/`; scope escapes use - `.agents/swarm/scope-escapes.jsonl`, shared schemas may use output-schema.json. -- **Filename convention:** one `<issue-id>.json` per lane and deterministic - wave order in the lead summary. -- **Serialization/schema format:** JSON worker result contract plus JSONL scope - escapes and exact commit/test evidence. -- **Validator command:** run `bash skills/swarm/scripts/validate.sh`, - `bash scripts/validate-swarm-evidence.sh` when evidence exists, project tests, - and the wave/landing gates. -- **Downstream handoff:** consumed by `/crank`, `/validate`, PAWL, tracker - closeout, and `/postmortem` harvesting. - -## Quality Rubric - -- Every lane was explicitly authorized, independent, and worth its overhead. -- Ownership includes source, tests, docs, schemas, and generated companions. -- Workers used isolated state and stayed within manifests. -- Results contain reproducible RED, commit, test, path, and conflict evidence. -- Integration order and wave-level validation are deterministic. -- Workers/worktrees are cleaned only after proof-backed completion. - -## Examples - -**User says:** `/swarm` for three disjoint accepted slices. - -Show the ownership matrix, dispatch bounded workers, validate file-backed -results, integrate in order, and independently land each slice. - -## Troubleshooting - -| Problem | Response | -|---|---| -| file ownership overlaps | serialize or merge the tasks | -| backend unavailable | execute sequentially with the same task/result contract | -| worker leaves scope | reject changes and record a scope escape | -| worker stalls | bounded correction, then close/re-plan | -| worktree cannot reap | retain it until ancestry proof succeeds | - -## References - -- [swarm.feature](references/swarm.feature) · [execution-steps.md](references/execution-steps.md) · [validation-contract.md](references/validation-contract.md) -- [pre-spawn-friction-gates.md](references/pre-spawn-friction-gates.md) · [shared-checkout-discipline.md](references/shared-checkout-discipline.md) · [worktree-isolation.md](references/worktree-isolation.md) -- [worker-specs.md](references/worker-specs.md) · [worker-pre-task-checks.md](references/worker-pre-task-checks.md) · [worker-pitfalls.md](references/worker-pitfalls.md) -- [conflict-recovery.md](references/conflict-recovery.md) · [scope-escape-template.md](references/scope-escape-template.md) · [cold-start-contexts.md](references/cold-start-contexts.md) -- [backend-codex-subagents.md](references/backend-codex-subagents.md) · [backend-claude-teams.md](references/backend-claude-teams.md) · [backend-background-tasks.md](references/backend-background-tasks.md) · [backend-inline.md](references/backend-inline.md) -- [local-mode.md](references/local-mode.md) · [ol-wave-integration.md](references/ol-wave-integration.md) · [ralph-loop-contract.md](references/ralph-loop-contract.md) -- [agent-genie-coordination-contract.md](references/agent-genie-coordination-contract.md) · [claude-code-latest-features.md](references/claude-code-latest-features.md) · [troubleshooting.md](references/troubleshooting.md) +Swarm does not select work, create packets, schedule from a backlog, persist a +queue, claim ownership, retry, validate, integrate, close, use Git, or deliver. +Executor failures remain executor evidence and cannot become core phase or +verdict state. diff --git a/images/gemini/skills/test/SKILL.md b/images/gemini/skills/test/SKILL.md index 428f05c76..1f624ce0f 100644 --- a/images/gemini/skills/test/SKILL.md +++ b/images/gemini/skills/test/SKILL.md @@ -22,9 +22,12 @@ context: - HISTORY intel_scope: topic metadata: + capabilities: [test] + effects: [] + canonical_status: canonical + disposition: keep_specialist tier: execution - dependencies: - - standards + dependencies: [] output_contract: test files, coverage report --- # Test Skill @@ -63,13 +66,11 @@ Default to `generate`. Flags: `--mode`, `--scope`, `--min-coverage`, and ### 1. Bind tests to behavior -When a bead or `.feature` file has scenarios, work forward from each -Given/When/Then. Read bead scenarios with `ao beads exec show <bead-id>`, name -one covering test after the behavior, and add +When a caller-supplied `.feature` file has scenarios, work forward from each +Given/When/Then. Name one covering test after the behavior, and add `@covered-by:<test-path>[::<TestName>]` above the scenario. Prove the mapping: ```bash -bash scripts/check-bead-scenario-coverage.sh --bead <bead-id> --run bash scripts/check-bead-scenario-coverage.sh skills/<skill>/references/<name>.feature --run ``` @@ -157,8 +158,7 @@ structural gaps and a test architecture; do not generate code in this mode. text/profile formats, and JSON where the coverage tool supports it. - **Validator command:** run the focused test, relevant suite, coverage command, and `bash scripts/check-bead-scenario-coverage.sh ... --run` when scenarios exist. -- **Downstream handoff:** consumed by `/implement`, `/validate`, `/review`, the - bead-acceptance pawl, and `/postmortem` evidence harvesting. +- **Downstream use:** factual evidence that a caller may supply to Validate. ## Quality Rubric diff --git a/images/gemini/skills/toil-mining/SKILL.md b/images/gemini/skills/toil-mining/SKILL.md new file mode 100644 index 000000000..5ce20b2e0 --- /dev/null +++ b/images/gemini/skills/toil-mining/SKILL.md @@ -0,0 +1,64 @@ +--- +name: toil-mining +description: 'Mine caller-supplied usage history for repeated toil and emit ranked evidence.' +practices: +- sre +- lean-startup +hexagonal_role: supporting +consumes: [] +produces: +- result.json +context_rel: +- kind: supplier-to + with: automation-shape-routing +skill_api_version: 1 +user-invocable: false +context: + window: fork + intent: + mode: task + sections: + exclude: + - HISTORY + intel_scope: topic +metadata: + capabilities: [toil_mining] + effects: [] + canonical_status: canonical + disposition: keep_specialist + tier: meta + dependencies: [] + stability: experimental +output_contract: ranked evidence report under .agents/toil-mining/ +--- +# Toil Mining — rank repeated friction + +Mine explicitly supplied session, shell, RTK, or CASS history without modifying +the sources. The result is evidence for a caller; this skill does not file work, +schedule automation, or mutate a tracker. + +## Procedure + +1. Record the input sources, time window, filters, and query. +2. Normalize repeated human actions while excluding machine echoes and generated + repetitions. +3. Cluster equivalent actions and preserve representative evidence references. +4. Score each cluster from measured frequency and observed pain such as elapsed + time, failure count, interruption, or token cost. +5. Emit a ranked report and stop. + +Each candidate must contain a measured count, source references, confidence in +the clustering, pain evidence, and the smallest plausible automation shape. +Separate observations from recommendations. + +## Output + +Write `.agents/toil-mining/YYYY-MM-DD-candidates.md` only when the caller asks for +a local artifact; otherwise return the report inline. Include checked and +not-checked sources. Do not include owners, priorities, claims, queues, or a next +action. + +## References + +- [Automation shape routing](../automation-shape-routing/SKILL.md) +- [CASS](../cass/SKILL.md) diff --git a/images/gemini/skills/using-gc/SKILL.md b/images/gemini/skills/using-gc/SKILL.md new file mode 100644 index 000000000..0e309f127 --- /dev/null +++ b/images/gemini/skills/using-gc/SKILL.md @@ -0,0 +1,37 @@ +--- +name: using-gc +description: 'Operate an explicitly selected Gas City as an optional executor for supplied packets. Triggers: "using gc", "gas city", "dispatch through gc".' +practices: [team-topologies, design-by-contract] +hexagonal_role: driving-adapter +consumes: [explicit-packets] +produces: [gas-city-runtime-evidence] +context_rel: +- kind: partnership + with: agent-native +skill_api_version: 1 +user-invocable: true +metadata: + tier: execution + dependencies: [] + capabilities: [dispatch_explicit_packet, observe_gc_runtime] + effects: [operate_gas_city] + canonical_status: canonical + disposition: keep_optional_adapter +output_contract: runtime evidence per supplied packet +--- + +# Using GC + +Use Gas City only when the caller explicitly selects it. Treat it as a +replaceable execution adapter, not a completion or correctness boundary. + +1. Accept complete explicit packets and a caller-selected city/executor. +2. Map each packet to one role and disjoint workspace. +3. Observe runtime state and return candidate, evidence, or error per packet. +4. Keep GC quests, attempts, stalls, and internal close state inside the + substrate. They do not become Plan, Candidate, RPI, or verdict state. +5. A fresh GC judge may provide evidence to Validate; only Validate writes + `verdict.v2`. + +This skill performs no automatic selection, retry, semantic validation, Git, +integration, closure, release, or delivery. diff --git a/images/gemini/skills/validate/SKILL.md b/images/gemini/skills/validate/SKILL.md index 92e6ae92e..a903eec5e 100644 --- a/images/gemini/skills/validate/SKILL.md +++ b/images/gemini/skills/validate/SKILL.md @@ -1,194 +1,82 @@ --- name: validate -spine: true -description: 'Independently remeasure a bounded artifact and emit one immutable, evidence-bound verdict. Triggers: "validate", "independently validate", "vibe".' +description: 'Freshly and independently judge one exact CandidatePacket, persist verdict.v2, and stop. Triggers: "validate", "independently validate", "vibe".' practices: - design-by-contract - llm-eval-harness +- content-addressed-storage hexagonal_role: driving-adapter -consumes: [] +consumes: +- plan-packet.v1 +- candidate-packet.v1 produces: -- result.json -context_rel: [] +- subject-manifest.v1 +- verdict.v2 +context_rel: +- kind: customer-of + with: plan +- kind: customer-of + with: implement skill_api_version: 1 user-invocable: true -context: - window: fork - intent: - mode: task - sections: - exclude: - - HISTORY - intel_scope: full metadata: graph_root: true tier: judgment dependencies: [] -output_contract: schemas/verdict.v1.schema.json + capabilities: [compute_subject_identity, judge_acceptance, persist_verdict] + effects: [write_verdict_artifact] + canonical_status: canonical + disposition: keep +output_contract: schemas/verdict.v2.schema.json --- # Validate -> **Purpose:** Independently remeasure one bounded artifact against explicit -> acceptance and emit immutable proof. Validate ends at proof. +Independently judge one exact subject against one unchanged acceptance contract, +write one durable verdict, and stop. Validate is the sole verdict writer. It +never edits the subject or controls what happens next. -## Critical Constraints +## Preconditions -- **One role: validator.** Because independence is the proof boundary, never - edit the subject, control its producer, mutate repository or tracker state, - or take delivery authority. -- Pin the artifact by path plus commit or digest before checking it because a - changed artifact makes prior evidence stale. -- Consume exact-input deterministic receipts after verifying candidate/tree, - command/mode, registry/toolchain, and environment identity. Rerun only - missing, stale, suspicious, or invalidated facts; an unchanged broad receipt - is proof, not an invitation to pay for the same suite again. -- Because claimed independence must be real, PASS requires every mandatory - check green, no blocker, disclosed `not_checked`, and a judge identity - different from the author. -- Judge lanes are read-only except for their one verdict artifact. -- Structured observations are part of the immutable verdict; they describe - evidence without classifying recurrence, promoting knowledge, or changing - future work. -- WARN and FAIL identify the owning producer and an executable next action, but - Validate does not perform the action, retry, re-plan, or choose escalation. -- Use runtime-native fresh context. Additional judges are optional depth, not a - substitute for one accountable validator. -- Validate one frozen bounded tranche, never each intermediate Crank wave. One - complete review may be followed by one affected-claim closure after the - producer's consolidated repair; a second distinct repair need is `REPLAN`. -- The repository's full deterministic terminal gate runs once on the final - post-repair candidate. Its exact-input receipt is part of the sealed verdict - and is reusable by delivery; it is not rerun at every earlier boundary. -- When a repeated FAIL trips a breaker, the orchestrator does not page the - operator first: it dispatches exactly one bounded fresh-context (or - cross-family) **helper** pass. HELPER-UNSTUCK means the helper cleared the - blocker and the proof is resumed on an explicit orchestrator decision; - HELPER-ESCALATE reaches a human only when that single helper pass also fails - (or the class is a refusal/judgment/spent-ceiling skip). +- A schema-valid PlanPacket and CandidatePacket are supplied. +- The subject manifest still matches the subject. +- Author and validator context IDs are explicit. +- Freshness is explicitly attested with `source: runtime | caller` and an + attester identity. -## Frozen request boundary - -Before any judge or model spend, freeze the explicit base, candidate commit and -tree, a nonempty subtree set, the complete changed-surface set as owned blobs or -deletions, acceptance, resolvable claim references plus their digests, evidence, -factual-gate registry, toolchain, author, and validator route. Never infer the -base from the current branch or an upstream name, and never accept an omitted or -extra owned path. - -Atomically reserve a canonical request identity, keyed by request ID plus the -canonical-JSON request digest in the Git common directory, before reserving the caller's -receipt path and before the first factual gate. The same request cannot run -again by choosing another output. Run each selected gate at most once at the -candidate in its declared JSON mode. A concurrent caller or stale reservation -refuses and stays in HOLD; there is no automatic crash recovery. A stale or -mutated candidate, missing/stale claim or evidence, invalid registry entry, -missing registry backing, ERROR, UNKNOWN, or mandatory FAIL stops before judge spend. A FAIL is eligible for -REPAIR only after the same gate is rerun at the frozen exact base and passes -there; a failure at both commits is pre-existing evidence returned to the -caller. A diagnostic or release FAIL remains nonbinding to semantic validation, -though it retains baseline attribution. Green mandatory proof routes to one -fresh validator by default. Inventory size is never a rigor or validator-count -signal. - -Every factual registry entry declares one closed `proof_kind`: syntax, schema, -identity, paths, generated drift, executable assertion, or evidence integrity. -Semantic prose scores and exact-wording preferences are reviewer evidence, not -factual gate kinds. Missing backing is a typed `registry_integrity` defect; -neither that defect nor a semantic observation may be mislabeled as candidate -proof. - -The portable freezer, runner, and receipt verifier is -`python3 skills/validate/scripts/validation-request.py freeze|run|check-receipt --help`. - -## Fresh-context dispatch boundary - -A schema-valid factual `READY` receipt, frozen candidate, explicit acceptance, -and distinct author/judge identities permit `VALIDATE_SINGLE_FRESH`. Validate does not meter, reserve, or authorize semantic work through a second adapter. - -A mandatory factual `FAIL`, any `ERROR` or `UNKNOWN`, or missing or malformed -proof stops before judge spend and returns the factual evidence to the caller. -Diagnostic and release `FAIL` remain nonbinding under the factual receipt -contract. Runtime time, cost, or quota limits are external facts; Validate -reports them without creating counters, helper state, or escalation authority. - -## Modes - -| Mode | Judge shape | Purpose | -|---|---|---| -| default | one independent judge | general evidence-bound verdict | -| `--quick` | one fresh independent judge, narrow claims | bounded evidence-bound verdict | -| `--deep` | up to four independent perspectives | high-risk completeness | -| `--mixed` | explicitly selected model families | cross-family review | -| `--debate` | two rounds | contested judgment | -| `--mode=post-impl` | acceptance plus completion checks | implemented work | -| `--mode=pre-impl [--target=X]` | plan/spec checks | planned work | -| `--mode=pr` | diff plus acceptance checks | submission artifact | - -**Mode-budget assertion:** 8 modes. Adding a ninth requires merging or removing -an existing mode. The folded `vibe` trigger maps to `--mode=post-impl`. +Missing, colliding, or unattested identities produce `NOT_PROVEN`. This is a +declared trust fact, not cryptographic proof that contexts were isolated. ## Workflow -1. **Pin tranche and acceptance.** Record artifact path, exact tranche - commit/digest, author - identity, mode, required checks, and declared coverage exclusions. -2. **Verify deterministic receipts.** Reuse each exact-input receipt whose - identities still match. Execute only missing, stale, suspicious, or - invalidated commands. A red mandatory command stops judge spend and is - attributed against the exact base before any REPAIR handoff. -3. **Run fresh-context judgment once.** Give the judge only the pinned artifact, - acceptance contract, verified factual receipts, standards, and output path. - The judge verifies evidence identity and reruns a command only when its - receipt is invalid or the semantic claim makes it suspicious. -4. **Consolidate fail-closed.** PASS needs complete proof. WARN discloses a - nonblocking concern. FAIL records any blocker, stale artifact, counterfeit - independence, malformed evidence, or mandatory red check. -5. **Seal final deterministic proof.** After any consolidated repair and - affected-claim closure, consume one full terminal-gate receipt for the final - exact candidate. Missing or red terminal proof is FAIL. -6. **Write one immutable output.** Emit canonical `result.json`. If a caller - still requires Markdown, generate a concise link-only projection from that - JSON; do not author a second analysis. Each structured observation contains - `kind`, `summary`, and `evidence_ref`. -7. **Return proof to the caller.** Report verdict, findings, observations, - `not_checked`, artifact identity, and one suggested owner/action. Stop. +1. Recompute and compare `subject-manifest.v1` using + `python3 skills/validate/scripts/validate.py manifest`. The helper uses only + filesystem content; Git commit/tree IDs are optional metadata. +2. Confirm acceptance and PlanPacket digests match the candidate. If the + subject changed or complete changed-path coverage cannot be established, + return `NOT_PROVEN`. +3. Compare proven actual changed paths with Plan `write_scope`. A proven + out-of-scope path returns `FAIL`; incomplete scope evidence returns + `NOT_PROVEN`. +4. Inspect the exact subject and factual evidence. Judge every acceptance + criterion and record criterion-level results, findings, evidence references, + `checked`, and `not_checked`. +5. Choose exactly one semantic result: `PASS`, `FAIL`, or `NOT_PROVEN`. PASS + requires complete proof, distinct identities, and explicit freshness. +6. Persist canonical `verdict.v2` with + `python3 skills/validate/scripts/validate.py store-verdict`. Default storage + is `<workspace>/.agentops/verdicts/sha256/<digest>.json`; callers may provide + `verdict_dir`. +7. Return the artifact path and digest. Stop. -Detailed mode and evidence rules live in -[canonical-validation-protocol.md](references/canonical-validation-protocol.md). -The proof-only post-verdict boundary is in -[post-verdict-actions.md](references/post-verdict-actions.md). Quick mode is -defined in [quick-mode-vibe.md](references/quick-mode-vibe.md). +The digest is SHA-256 over canonical JSON with `artifact_digest` omitted. Writes +use a same-directory temporary file, flush, fsync, and atomic rename. Identical +existing content is idempotent success; conflicting content is an integrity +failure represented by `NOT_PROVEN`. -## Output Specification +## Boundary -- **Artifact directory:** invocation output root for canonical `result.json`; - optional generated Markdown may live under `.agents/council/`. -- **Filename convention:** `result.json`; optional - `YYYY-MM-DD-validate-<topic>.md` is a projection only. -- **Serialization:** `result.json` follows - [`schemas/verdict.v1.schema.json`](../../schemas/verdict.v1.schema.json). -- **Evidence:** exactly one anchored `VERDICT: PASS|WARN|FAIL`, a nonempty - `COMMANDS RUN:` section with `judge=<id> command=<command>`, `REASONS:`, - findings, structured observations, and `not_checked`. -- **Validator command:** `bash skills/validate/scripts/validate.sh`. -- **Downstream handoff:** callers may pass the immutable verdict and digest to - Learn or to their own delivery process. A repository may consume PASS without - another LLM landing verdict. Validate has no authority after the handoff. - -## Quality Checklist - -- [ ] Subject identity and acceptance are pinned. -- [ ] Base, candidate/tree, subtrees, changed surfaces, dependencies, registry, - toolchain, author, and validator route are frozen and still match. -- [ ] Every mandatory fact has an exact-input receipt; only missing, stale, - suspicious, or invalidated commands were rerun. -- [ ] Factual proof is READY and author/judge identities differ before fresh - semantic judgment begins. -- [ ] Independent PASS has different author and judge identities. -- [ ] Findings, observations, and coverage gaps cite evidence. -- [ ] Canonical `result.json` is valid; any optional Markdown projection cites - it and adds no independent verdict. -- [ ] No implementation, learning, retry, tracker, or delivery action occurred. - -Executable behavior is in [validate.feature](references/validate.feature). +Validate emits no WARN, confidence, disposition, briefing learning, owner, +next action, repair, retry, replan, helper, escalation, tracker, Git, release, +closure, or delivery state. Generic provenance may record a verdict later, but +ledger availability cannot change its validity. diff --git a/images/gemini/skills/workflow-builder/SKILL.md b/images/gemini/skills/workflow-builder/SKILL.md new file mode 100644 index 000000000..4e08dc7d2 --- /dev/null +++ b/images/gemini/skills/workflow-builder/SKILL.md @@ -0,0 +1,53 @@ +--- +name: workflow-builder +description: 'Scaffold an explicit one-shot workflow adapter without lifecycle authority.' +practices: +- pragmatic-programmer +- hexagonal-architecture +- agile-manifesto +hexagonal_role: supporting +consumes: [] +produces: +- workflow-script +context_rel: +- kind: customer-of + with: automation-shape-routing +- kind: shared-kernel + with: operationalize +skill_api_version: 1 +context: + window: fork + intent: + mode: questions + sections: + exclude: + - HISTORY + intel_scope: topic +metadata: + capabilities: [workflow_builder] + effects: [] + canonical_status: canonical + disposition: keep_specialist + tier: meta + dependencies: [] +output_contract: a runnable one-shot workflow with explicit inputs and outputs +--- +# Workflow Builder — one-shot adapter authoring + +Build a thin adapter only when a caller needs to dispatch an explicit set of +independent operations. A workflow is convenience code, never a correctness or +lifecycle authority. + +## Contract + +- Inputs, executors, write scopes, and outputs are supplied explicitly. +- Each operation is dispatched at most once. +- Parallel operations must have caller-proven disjoint write scopes. +- The workflow reports per-operation output or error and then stops. +- It contains no work selection, retry, budget, queue, tracker, validation, Git, + integration, closure, release, or delivery logic. +- Optional substrate state cannot be translated into RPI or verdict state. + +Prefer the smallest script supported by the target runtime. Include a dry-run or +fixture demonstrating exact dispatch count and failure reporting. Do not create a +new framework or SDK abstraction unless the caller explicitly requests one. diff --git a/images/gemini/verify.sh b/images/gemini/verify.sh index 1d47ea156..9f12d8ab2 100755 --- a/images/gemini/verify.sh +++ b/images/gemini/verify.sh @@ -5,7 +5,7 @@ # 1. plugin.json / mcp_config.json / hooks.json / hooks/hooks.json are valid JSON # and expose the expected manifest shape (modeled on the proven .agy-plugin # package at agentops ed8f573e6). -# 2. each bundled slug (CORE + AGY operator; see arrays below) resolves to +# 2. each metadata-derived bundled slug resolves to # skills/<slug>/SKILL.md inside the bundle AND is byte-identical to the # canonical source skills/<slug>/SKILL.md # (the KEY FINDING: zero content conversion — only the wrapper differs). @@ -20,38 +20,14 @@ PLUGIN_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # repo root = images/gemini -> images -> <repo root> REPO_ROOT="$(cd "$PLUGIN_DIR/../.." && pwd)" -# The 32 CORE slugs — the original IMAGE-CORE.md §1 list resolved through the -# skill-consolidation ledger (docs/contracts/skill-dispositions.yaml historical -# merged-into chains + caam->account-rotation, refreshed 2026-07-04, age-085q). -# Retired-with-no-successor slugs (ssh, gcloud, gh-cli, gh-actions, ...) dropped. -# 2026-07-07 retire wave (age-skills-audit-fable-l6ic.12): red-team, curate, -# compile, flywheel, recover, review dropped — merged into validate / -# postmortem / status per docs/audits/skills-audit-2026-07-06.md. -core_skills=( - rpi discovery research plan implement crank swarm validate - council premortem postmortem - goals evolve bootstrap handoff - operationalize push scope status test - skill-builder heal-skill - beads-br beads-bv agent-mail ntm cass dcg - rch sbh cc-hooks account-rotation -) - -# The Gemini/AGY operator surface (was 4 skills; agy-rules-workflows, -# agy-mcp-plugins, agy-headless-evidence merged into agy-native per the -# dispositions ledger). Same packaging recipe: direct, byte-identical -# SKILL.md copies, zero conversion. -operator_skills=( - agy-native -) - -# Full bundled set = CORE + AGY operator. -all_skills=( "${core_skills[@]}" "${operator_skills[@]}" ) +CATALOG="$REPO_ROOT/skills/catalog.json" +[[ -f "$CATALOG" ]] || { printf 'FAIL: missing metadata-derived skill catalog\n' >&2; exit 1; } fail() { printf 'FAIL: %s\n' "$*" >&2; exit 1; } pass() { printf 'PASS: %s\n' "$*"; } command -v jq >/dev/null || fail "jq is required" +mapfile -t all_skills < <(jq -r '.skills[].name' "$CATALOG") # --- 1. manifest files exist + valid JSON + expected shape ------------------- [[ -f "$PLUGIN_DIR/plugin.json" ]] || fail "missing plugin.json" @@ -115,7 +91,9 @@ for skill in "${all_skills[@]}"; do cmp -s "$bundled" "$source_file" \ || fail "bundled skill drifted from source (NOT zero-conversion): $skill" done -pass "all $expected_count slugs (${#core_skills[@]} CORE + ${#operator_skills[@]} AGY operator) resolve to skills/<slug>/SKILL.md and match source byte-for-byte" +catalog_count="$(jq -r '.skill_count' "$CATALOG")" +[[ "$expected_count" == "$catalog_count" ]] || fail "bundle count $expected_count differs from catalog count $catalog_count" +pass "all $expected_count metadata-derived slugs resolve to skills/<slug>/SKILL.md and match source byte-for-byte" # --- 4. agy plugin validate (if available) ----------------------------------- if command -v agy >/dev/null; then @@ -129,4 +107,4 @@ else printf 'NOTE: agy CLI not found — skipping `agy plugin validate`; relied on JSON-validity + slug-presence.\n' fi -printf 'OK: Gemini/AGY CORE image bundle is valid (%s skills = %s CORE + %s AGY operator, direct+wrapped, zero conversion)\n' "$expected_count" "${#core_skills[@]}" "${#operator_skills[@]}" +printf 'OK: Gemini/AGY image bundle is valid (%s metadata-derived skills, direct+wrapped, zero conversion)\n' "$expected_count" diff --git a/lib/ao-paths.sh b/lib/ao-paths.sh index 5faa583d0..998b8f93c 100755 --- a/lib/ao-paths.sh +++ b/lib/ao-paths.sh @@ -13,7 +13,7 @@ # default — $REPO_ROOT/.agents (git rev-parse) or ${PWD}/.agents # # Per-subdir overrides (read after the home resolves, win over the default layout): -# AO_AGENTS_DIR, AO_KNOWLEDGE_ROOT, AO_HOOKS_DIR, AO_SCOPE_LOCK, +# AO_AGENTS_DIR, AO_KNOWLEDGE_ROOT, AO_HOOKS_DIR, # AO_RPI_DIR, AO_FINDINGS_DIR, AO_PLANS_DIR, AO_COUNCIL_DIR, # AO_LEARNINGS_DIR, AO_PATTERNS_DIR, AO_DECISIONS_DIR # @@ -58,7 +58,6 @@ fi _ao_agents_dir="${AO_AGENTS_DIR:-$_ao_home}" _ao_knowledge_root="${AO_KNOWLEDGE_ROOT:-$_ao_agents_dir/wiki}" _ao_hooks_dir="${AO_HOOKS_DIR:-$_ao_agents_dir/hooks}" -_ao_scope_lock="${AO_SCOPE_LOCK:-$_ao_agents_dir/scope.lock}" _ao_rpi_dir="${AO_RPI_DIR:-$_ao_agents_dir/rpi}" _ao_findings_dir="${AO_FINDINGS_DIR:-$_ao_agents_dir/findings}" _ao_plans_dir="${AO_PLANS_DIR:-$_ao_agents_dir/plans}" @@ -72,7 +71,6 @@ printf 'export AO_HOME=%q\n' "$_ao_home" printf 'export AO_AGENTS_DIR=%q\n' "$_ao_agents_dir" printf 'export AO_KNOWLEDGE_ROOT=%q\n' "$_ao_knowledge_root" printf 'export AO_HOOKS_DIR=%q\n' "$_ao_hooks_dir" -printf 'export AO_SCOPE_LOCK=%q\n' "$_ao_scope_lock" printf 'export AO_RPI_DIR=%q\n' "$_ao_rpi_dir" printf 'export AO_FINDINGS_DIR=%q\n' "$_ao_findings_dir" printf 'export AO_PLANS_DIR=%q\n' "$_ao_plans_dir" diff --git a/registry.json b/registry.json index fbd384472..a3d2d134b 100644 --- a/registry.json +++ b/registry.json @@ -329,8 +329,8 @@ "driven_by_skills": [ "premortem" ], - "id": "skill:premortem:premortem", - "name": "premortem", + "id": "skill:premortem:challenge_plan", + "name": "challenge_plan", "path": "skills/premortem/SKILL.md", "type": "skill" }, @@ -428,8 +428,8 @@ "driven_by_skills": [ "scope" ], - "id": "skill:scope:scope", - "name": "scope", + "id": "skill:scope:scope_review", + "name": "scope_review", "path": "skills/scope/SKILL.md", "type": "skill" }, @@ -836,12 +836,14 @@ "heal_skill" ], "disposition": "keep_specialist", - "effects": [], + "effects": [ + "optional_skill_projection_repair" + ], "has_references": true, "has_skill_md": true, "name": "heal-skill", "path": "skills/heal-skill/", - "reference_count": 6, + "reference_count": 5, "tier": "meta" }, { @@ -978,15 +980,17 @@ }, { "capabilities": [ - "premortem" + "challenge_plan" ], "disposition": "keep_strategy", - "effects": [], + "effects": [ + "write_advisory_plan_review" + ], "has_references": true, "has_skill_md": true, "name": "premortem", "path": "skills/premortem/", - "reference_count": 7, + "reference_count": 1, "tier": "judgment" }, { @@ -1055,7 +1059,7 @@ "has_skill_md": true, "name": "research", "path": "skills/research/", - "reference_count": 19, + "reference_count": 1, "tier": "execution" }, { @@ -1110,20 +1114,20 @@ "has_skill_md": true, "name": "scaffold", "path": "skills/scaffold/", - "reference_count": 4, + "reference_count": 3, "tier": "execution" }, { "capabilities": [ - "scope" + "scope_review" ], "disposition": "keep_specialist", "effects": [], - "has_references": true, + "has_references": false, "has_skill_md": true, "name": "scope", "path": "skills/scope/", - "reference_count": 4, + "reference_count": 0, "tier": "meta" }, { @@ -1145,11 +1149,11 @@ ], "disposition": "keep_specialist", "effects": [], - "has_references": true, + "has_references": false, "has_skill_md": true, "name": "shared", "path": "skills/shared/", - "reference_count": 1, + "reference_count": 0, "tier": "library" }, { @@ -1157,12 +1161,15 @@ "skill_builder" ], "disposition": "keep_specialist", - "effects": [], + "effects": [ + "writes_skill_source", + "regenerates_skill_projections" + ], "has_references": true, "has_skill_md": true, "name": "skill-builder", "path": "skills/skill-builder/", - "reference_count": 6, + "reference_count": 3, "tier": "meta" }, { diff --git a/schemas/agentops-sdlc-provenance.v1.schema.json b/schemas/agentops-sdlc-provenance.v1.schema.json index b4a45c1cd..529355da0 100644 --- a/schemas/agentops-sdlc-provenance.v1.schema.json +++ b/schemas/agentops-sdlc-provenance.v1.schema.json @@ -2,7 +2,7 @@ "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://agentops.dev/schemas/agentops-sdlc-provenance.v1.schema.json", "title": "AgentOps SDLC Provenance Ledger Event (v1)", - "description": "One append-only event in the committed, per-record hash-chained provenance ledger at docs/provenance/ledger.jsonl (one JSON object per line). Each event records a single provenance edge — a typed, evidence-backed relation between two SDLC nodes (e.g. a decision and the artifact it produced) — plus the export-time hash-chain fields that make the ledger offline/CI/fresh-clone verifiable. Per CLAUDE.md the committed ledger is the AUDIT authority and wins on disagreement with the Dolt provenance_edges write model. Council architecture: .agents/council/2026-05-30-debate-provenance-substrate.md. Trust tiers are monotonic (authored > inferred > mined) per the in-toto (min,x) semiring. Hash fields reuse the hashing discipline in cli/internal/rpi/ledger.go: payload_hash = sha256 over the canonical edge payload (every field EXCEPT prev_hash/payload_hash/hash); hash = sha256(payload_hash + \"\\n\" + prev_hash); prev_hash links to the previous record's hash (empty string for the first record).", + "description": "One append-only event in the optional historical provenance ledger at docs/provenance/ledger.jsonl. The ledger is generic audit evidence and never controls RPI sequencing, verdict validity, revision, or repository delivery. Legacy optional fields remain readable so the existing hash chain can be verified byte-for-byte.", "practices": ["adr", "in-toto-provenance", "dora-metrics"], "type": "object", "additionalProperties": false, @@ -55,41 +55,41 @@ "bead_id": { "type": "string", "minLength": 1, - "description": "Optional, additive mesh join key (ag-5qltf, epic ag-w0wr2): the canonical work-unit id (e.g. 'ag-62jrm') this edge pertains to. On a bead→commit edge it denormalizes from_id. bead_id is the UNIVERSAL join key across the yield↔provenance mesh (the corpus→C hop joins on bead_id alone). NON-payload: excluded from payload_hash because it is a projection of the already-hashed from_id/to_id, so existing committed edges keep their hashes and VerifyChain stays intact." + "description": "Legacy optional work-unit join key retained only to read and verify existing ledger records. It is excluded from payload_hash for historical compatibility and carries no work-ownership or lifecycle authority." }, "merge_sha": { "type": "string", "minLength": 1, - "description": "Optional, additive mesh join key (ag-5qltf): the full/canonical trunk commit OID (push-to-main: the landed commit) anchoring the bead→commit hop. Denormalizes to_id on a bead→commit edge. Pairs with bead_id as (bead_id, merge_sha). NON-payload (see bead_id)." + "description": "Legacy optional commit join key retained only to read and verify existing ledger records. It is excluded from payload_hash for historical compatibility and carries no delivery authority." }, "reviewer_family": { "type": "string", "minLength": 1, - "description": "Optional, additive v1.1 verdict-record enrichment (age-rk3r.3): the canonical reviewer model family/families that produced this verdict edge — e.g. 'claude' or 'claude+gpt' (aliases collapse: fable/anthropic->claude, codex/openai->gpt, agy/google->gemini; multiple distinct families are sorted and joined with '+'). Derived from the pawl-verdict refuter panel. UNLIKE the bead_id/merge_sha join keys, this field IS part of payload_hash (hash-protected / tamper-evident). ADDITIVITY: records predating v1.1 simply omit it, so their payload marshals byte-identically and payload_hash/VerifyChain are unchanged over history; a v1.1 record that sets it has it covered by payload_hash. COMPATIBILITY BOUNDARY: because it is in the payload, an ao binary predating these fields drops them on read and will report a spurious payload_hash mismatch on v1.1 records (while still verifying pre-v1.1 records) — verify v1.1 records with an ao at or above the version that knows these fields. 'v1.1' is a documentation label only; schema_version is unchanged and consumers branch on field PRESENCE, never a version string." + "description": "Legacy optional reviewer-family observation retained only to read and verify existing ledger records. When present it remains payload-hashed; it is not a verdict.v2 field or freshness proof." }, "degraded": { "type": "boolean", - "description": "Optional, additive v1.1 verdict-record enrichment (age-rk3r.3): true when the verdict was produced under a degraded/failover reviewer configuration (the failover label; sibling bead .2). Payload-included (hash-protected, same additivity + compatibility boundary as reviewer_family); omitted when false/absent. The v1 pawl-verdict file carries no source for this yet, so it stays absent until the failover bead populates it." + "description": "Legacy optional reviewer observation retained only for historical hash compatibility. It has no effect on current verdict validity." }, "rounds": { "type": "integer", "minimum": 1, - "description": "Optional, additive v1.1 verdict-record enrichment (age-rk3r.3): the number of adversarial refute→fix rounds this verdict took — a cost-of-verified-done substrate. Payload-included (hash-protected, same additivity + compatibility boundary as reviewer_family); omitted when absent/zero. The v1 pawl-verdict file carries no source for this yet, so it stays absent until the cost bead (.16) populates it." + "description": "Legacy optional observation retained only for historical hash compatibility. It does not govern retries." }, "duration_s": { "type": "number", "minimum": 0, - "description": "Optional, additive v1.1 verdict-record enrichment (age-rk3r.3): wall-clock seconds the verified-done review took — a cost-of-verified-done substrate. Payload-included (hash-protected, same additivity + compatibility boundary as reviewer_family); omitted when absent/zero. The v1 pawl-verdict file carries no source for this yet, so it stays absent until the cost bead (.16) populates it." + "description": "Legacy optional duration observation retained only for historical hash compatibility." }, "tokens_est": { "type": "integer", "minimum": 1, - "description": "Optional, additive verification-economics meter (age-verification-economics-ebec.1): estimated tokens spent producing this verdict (transcript-bytes/4 when the harness reports no exact usage). Payload-included (hash-protected), additive via omitempty — a zero/absent meter omits the field and pre-meter records marshal byte-identically (same compatibility contract as the other v1.1 fields). This property was omitted when the meter field landed in cli/internal/provenancegraph/edge.go; added retroactively (age-ekam) to keep the schema in lockstep with the executable shape." + "description": "Legacy optional token observation retained only for historical hash compatibility." }, "evidence_path": { "type": "string", "minLength": 1, - "description": "Optional, additive v1.1 verdict-record enrichment (age-rk3r.3): a repo-relative path to the concrete reviewer evidence artifact (the refuter transcript or council narrative) backing this verdict — distinct from evidence_ref, which is a free-text pointer. Derived from the pawl-verdict refuters' evidence path, or the council_artifact, when present. Payload-included (hash-protected, same additivity + compatibility boundary as reviewer_family); omitted when absent." + "description": "Legacy optional evidence path retained only to read and verify existing ledger records." }, "trust_tier": { "type": "string", diff --git a/schemas/domain-slice-manifest.v1.schema.json b/schemas/domain-slice-manifest.v1.schema.json deleted file mode 100644 index ee13a39b2..000000000 --- a/schemas/domain-slice-manifest.v1.schema.json +++ /dev/null @@ -1,124 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://agentops.dev/schemas/domain-slice-manifest.v1.schema.json", - "title": "AgentOps Domain-Slice Manifest v1", - "description": "Durable, git-tracked manifest declaring a bounded DDD domain slice: owned directives, scenarios, context roots, read-fence globs, validation commands, and owner. Stored at docs/domains/<name>/manifest.yaml. See docs/adr/ADR-0013-domain-slice-manifest-contract.md.", - "practices": ["ddd-bounded-context", "design-by-contract", "pragmatic-programmer"], - "type": "object", - "additionalProperties": false, - "required": [ - "schema_version", - "domain", - "version", - "bounded_context", - "directive_ids", - "scenario_ids", - "context_roots", - "allowed_read_globs", - "denied_read_globs", - "validation_commands", - "owner" - ], - "properties": { - "schema_version": { - "type": "integer", - "const": 1, - "description": "Schema version for this manifest format." - }, - "domain": { - "type": "string", - "minLength": 1, - "pattern": "^[a-z][a-z0-9-]*$", - "description": "Short machine-readable domain name (e.g. 'goals', 'rpi', 'eval'). Matches the directory name under docs/domains/." - }, - "version": { - "type": "string", - "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$", - "description": "Semver string for the manifest itself (not the domain implementation). Increment on any structural change to the manifest." - }, - "bounded_context": { - "type": "string", - "minLength": 1, - "description": "Human-readable bounded-context statement: a single sentence describing what this domain slice owns and what it does NOT own (DDD anti-corruption layer framing)." - }, - "directive_ids": { - "type": "array", - "items": { - "type": "string", - "pattern": "^d-[a-z0-9][a-z0-9-]*$", - "description": "Stable directive ID from GOALS.md (pattern d-<slug>). This manifest owns delivery of the directive's acceptance criteria." - }, - "uniqueItems": true, - "description": "Stable directive IDs from GOALS.md whose acceptance criteria this domain slice owns. Pattern: d-<slug>. These are the executable-spec anchors for this slice." - }, - "scenario_ids": { - "type": "array", - "items": { - "type": "string", - "description": "Scenario ID (e.g. s-2026-05-17-001 or auto-<slug>). Matches scenario files in spec/scenarios/." - }, - "uniqueItems": true, - "description": "Promoted spec scenario IDs (from spec/scenarios/) whose satisfaction constitutes delivery of this domain's directives. See ADR-0003 for the promoted_spec lifecycle." - }, - "context_roots": { - "type": "array", - "items": { - "type": "string", - "description": "Repo-relative path to a directory or file that is the primary implementation surface for this domain slice." - }, - "minItems": 1, - "description": "Repo-relative paths to directories or files that form the implementation surface of this domain slice. Used by ao rpi phased --domain to scope context loading." - }, - "allowed_read_globs": { - "type": "array", - "items": { - "type": "string", - "description": "Glob pattern (gitignore syntax) for files agents may read when operating within this domain slice." - }, - "description": "Glob patterns (gitignore syntax) that define the read fence for agents working in this domain slice. Files matching these patterns are readable; used by the holdout-isolation-gate and context-assembly hooks." - }, - "denied_read_globs": { - "type": "array", - "items": { - "type": "string", - "description": "Glob pattern (gitignore syntax) for files agents must NOT read when working within this domain slice." - }, - "description": "Glob patterns for files that are explicitly off-limits for agents working in this domain slice. Denied globs take precedence over allowed_read_globs when both match." - }, - "validation_commands": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": false, - "required": ["label", "command"], - "properties": { - "label": { - "type": "string", - "minLength": 1, - "description": "Short human-readable label for this validation step (e.g. 'build', 'unit-tests', 'lint')." - }, - "command": { - "type": "string", - "minLength": 1, - "description": "Shell command to run from the repo root. Must exit 0 for validation to pass." - }, - "working_dir": { - "type": "string", - "description": "Optional repo-relative directory to run the command from. Defaults to repo root." - }, - "timeout_seconds": { - "type": "integer", - "minimum": 1, - "description": "Optional per-command timeout in seconds. Defaults to 120." - } - } - }, - "description": "Ordered list of validation commands that must pass for this domain slice. Executed by ao rpi phased --domain after implementation. All commands run from repo root unless working_dir is set." - }, - "owner": { - "type": "string", - "minLength": 1, - "description": "Team or person responsible for this domain slice (e.g. 'maintainers', 'bo', 'goals-team'). Informs who reviews changes to this manifest and the scenario acceptance criteria it owns." - } - } -} diff --git a/schemas/fixtures/ao-state/bad-candidate-extra-field.json b/schemas/fixtures/ao-state/bad-candidate-extra-field.json deleted file mode 100644 index 03114d1aa..000000000 --- a/schemas/fixtures/ao-state/bad-candidate-extra-field.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "schema_version": 1, - "kind": "ao_state_finding_candidate", - "id": "finding-age-membrane-extra-field", - "title": "Unknown fields should be rejected", - "status": "candidate", - "created_at": "2026-06-19T19:00:00Z", - "updated_at": "2026-06-19T19:05:00Z", - "source": { - "bead_id": "age-membrane-memory-arch-tz2s.2.8.1", - "run_id": "run-state-bad", - "author_id": "codex:worker-a", - "author_context_id": "ctx-author-a", - "author_family": "codex", - "repo": "agentops", - "path": ".agents/findings/finding-age-membrane-extra-field.md", - "source_digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" - }, - "severity": "significant", - "category": "validation-gap", - "summary": "This fixture intentionally adds an unknown property.", - "evidence": ["schema fixture"], - "body": "Bad fixture.", - "unexpected": true -} diff --git a/schemas/fixtures/ao-state/bad-verdict-missing-proof.json b/schemas/fixtures/ao-state/bad-verdict-missing-proof.json deleted file mode 100644 index 6647395a1..000000000 --- a/schemas/fixtures/ao-state/bad-verdict-missing-proof.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "schema_version": 1, - "kind": "ao_state_admission_verdict", - "candidate_id": "finding-age-membrane-valid", - "candidate_digest": "sha256:0000000000000000000000000000000000000000000000000000000000000000", - "reviewer_id": "codex:validator-b", - "reviewer_context_id": "ctx-reviewer-b", - "reviewer_family": "codex", - "reviewed_at": "2026-06-19T19:08:00Z", - "verdict": "CONFIRMED", - "head_sha": "0123456789abcdef", - "evidence_ref": ".agents/evidence/ao-state-plane/s2-admission-core.log", - "summary": "Missing proof_ref is intentionally invalid." -} diff --git a/schemas/fixtures/ao-state/valid-candidate.json b/schemas/fixtures/ao-state/valid-candidate.json deleted file mode 100644 index 0ac834885..000000000 --- a/schemas/fixtures/ao-state/valid-candidate.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "schema_version": 1, - "kind": "ao_state_finding_candidate", - "id": "finding-age-membrane-valid", - "title": "Fresh non-author finding can enter admitted AO state", - "status": "candidate", - "created_at": "2026-06-19T19:00:00Z", - "updated_at": "2026-06-19T19:05:00Z", - "source": { - "bead_id": "age-membrane-memory-arch-tz2s.2.8.2", - "run_id": "run-state-valid", - "author_id": "codex:worker-a", - "author_context_id": "ctx-author-a", - "author_family": "codex", - "repo": "agentops", - "path": ".agents/findings/finding-age-membrane-valid.md", - "source_digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - }, - "severity": "significant", - "category": "validation-gap", - "summary": "Admission requires a fresh independent review and deterministic evidence.", - "evidence": [ - "go test ./cli/internal/aostate -run TestAdmissionCore" - ], - "body": "A finding becomes durable AO state only after a separate reviewer confirms it against fresh evidence." -} diff --git a/schemas/fixtures/ao-state/valid-verdict.json b/schemas/fixtures/ao-state/valid-verdict.json deleted file mode 100644 index 1a32d382d..000000000 --- a/schemas/fixtures/ao-state/valid-verdict.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "schema_version": 1, - "kind": "ao_state_admission_verdict", - "candidate_id": "finding-age-membrane-valid", - "candidate_digest": "sha256:0000000000000000000000000000000000000000000000000000000000000000", - "reviewer_id": "codex:validator-b", - "reviewer_context_id": "ctx-reviewer-b", - "reviewer_family": "codex", - "reviewed_at": "2026-06-19T19:08:00Z", - "verdict": "CONFIRMED", - "head_sha": "0123456789abcdef", - "evidence_ref": ".agents/evidence/ao-state-plane/s2-admission-core.log", - "proof_ref": "docs/evidence/age-membrane-memory-arch-tz2s.2.8-pawl.md", - "summary": "Independent reviewer confirmed the candidate." -} diff --git a/scripts/.scenario-linkage-allow b/scripts/.scenario-linkage-allow index 421c18d7d..30c496f9c 100644 --- a/scripts/.scenario-linkage-allow +++ b/scripts/.scenario-linkage-allow @@ -18,17 +18,11 @@ # # Lines starting with '#' and blank lines are ignored. Trailing '# comment' on # an entry is stripped. -skills/behavior-first-planning/references/behavior-first-planning.feature skills/bootstrap/references/bootstrap.feature -skills/crank/references/crank.feature -skills/discovery/references/brainstorm.feature -skills/discovery/references/discovery.feature skills/doc/references/doc.feature skills/doc/references/oss-docs.feature skills/doc/references/readme.feature skills/domain/references/domain.feature -skills/evolve/references/autodev.feature -skills/evolve/references/evolve.feature skills/handoff/references/handoff.feature skills/heal-skill/references/heal-skill.feature skills/implement/references/implement.feature @@ -36,12 +30,9 @@ skills/operationalize/references/knowledge-activation.feature skills/plan/references/plan.feature skills/postmortem/references/postmortem.feature skills/learn/references/learn.feature -skills/pr-prep/references/pr-prep.feature skills/premortem/references/premortem.feature skills/product/references/product.feature -skills/push/references/push.feature skills/refactor/references/refactor.feature -skills/release/references/release.feature skills/research/references/research.feature skills/reverse-engineer/references/reverse-engineer.feature skills/scaffold/references/scaffold.feature diff --git a/scripts/add-validate-job.sh b/scripts/add-validate-job.sh deleted file mode 100755 index 8e3ee2a06..000000000 --- a/scripts/add-validate-job.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash -# shellcheck disable=SC2089,SC2090 -# scripts/add-validate-job.sh -# -# RETIRED (Wave 2 cut-plan): the purpose-job scaffolder that also patched -# scripts/pre-push-gate.sh + tests/scripts/pre-push-gate.bats. Those bash-gate -# surfaces are gone; CI authority is go-gate-shadow (`ao gate check --full`) -# and new checks belong in the Go gate registry -# (cli/internal/gates/checks/seed.go), then docs/contracts/ci-jobs.yaml only -# if a new validate.yml purpose job is intentionally added. -# -# Encodes soc-3oij history; do not revive the bash-gate touch-points. - -set -euo pipefail - -cat >&2 <<'EOF' -add-validate-job.sh: retired with the legacy bash gate (Wave 2). - -To add a blocking check: - 1. Implement scripts/check-*.sh (or a native Go check) - 2. Register it in cli/internal/gates/checks/seed.go - 3. Prefer go-gate-shadow coverage over a new validate.yml purpose job - 4. If you intentionally add a purpose job, update - .github/workflows/validate.yml + docs/contracts/ci-jobs.yaml, then - run: scripts/generate-ci-jobs-table.sh --write - -EOF -exit 2 diff --git a/scripts/assay/consume-mto-recurrence.sh b/scripts/assay/consume-mto-recurrence.sh index 35ede9975..2881f1604 100755 --- a/scripts/assay/consume-mto-recurrence.sh +++ b/scripts/assay/consume-mto-recurrence.sh @@ -200,7 +200,7 @@ RULE_PATH="$PLANNING_DIR/$FINDING_ID.md" # schema-conformant frontmatter (finding-artifact.schema.json). detectability= # advisory -> no `compiler` block required; compiler_targets are the advisory two -# (plan, pre-mortem); applicable_when uses only enum members; additionalProperties +# (plan, premortem); applicable_when uses only enum members; additionalProperties # is false so we emit ONLY schema-known keys. read -r -d '' FINDING_BODY <<EOF || true --- @@ -217,7 +217,7 @@ checklist_item: "Before planning work that could trip a recurring MTO class, re- severity: "significant" detectability: "advisory" status: "active" -compiler_targets: ["plan", "pre-mortem"] +compiler_targets: ["plan", "premortem"] scope_tags: ["mto-gate", "seeded-guard", "recurrence"] dedup_key: "validation-gap|mto-seeded-guard-recurred-across-beads|validation-gap" applicable_when: ["validation-gap", "plan-shape"] diff --git a/scripts/audit-assertion-density.sh b/scripts/audit-assertion-density.sh index 4e1a3e10d..ced7ce067 100755 --- a/scripts/audit-assertion-density.sh +++ b/scripts/audit-assertion-density.sh @@ -13,7 +13,7 @@ THRESHOLD=1.5 # Default to *_test.go so the audit covers any test file that ships with # the change, not just the legacy *coverage*_test.go pattern (cov*_test.go # is banned by CLAUDE.md, so the old default audited an empty set on a -# clean checkout — see post-mortem #5). +# clean checkout — see postmortem #5). SCOPE='*_test.go' POSITIONAL=() diff --git a/scripts/check-atomic-write-ratchet.sh b/scripts/check-atomic-write-ratchet.sh index f8211804e..51ef42ab1 100644 --- a/scripts/check-atomic-write-ratchet.sh +++ b/scripts/check-atomic-write-ratchet.sh @@ -36,10 +36,10 @@ # trips ⟺ a NON-COMMENT line invokes `os.Rename(` # AND a NON-COMMENT line carries `os.CreateTemp(` OR a `".tmp` # literal (two-signal AND; comment mentions NEVER count — -# the comment-strip rule, pre-mortem r3) +# the comment-strip rule, premortem r3) # AND (changed-content guard) an ADDED hunk introduces `os.Rename(`. # The added-hunk guard fires EVEN when the file is grandfathered: a NEW -# rename site inside a grandfathered file still trips (pre-mortem FM7 — +# rename site inside a grandfathered file still trips (premortem FM7 — # do not clone the grandfather-skips-first flow). Its ERE skips lines whose # os.Rename( sits after a // (comment-only additions never trip); an added # line inside a multi-line /* block */ cannot be classified from a diff @@ -105,7 +105,7 @@ is_exempt_path() { } # strip_go_comments (stdin -> stdout): remove // line tails and /* */ block -# comments before signal-grepping — a comment mention never counts (pre-mortem +# comments before signal-grepping — a comment mention never counts (premortem # r3; pawl refute extended it from full-line to inline/block forms). NOT a Go # parser: a "//" or "/*" INSIDE a string literal is treated as a comment # opener and truncates that line — acceptable for a two-signal grep and @@ -214,7 +214,7 @@ while IFS= read -r f; do is_exempt_path "$f" && continue file_trips "$f" || continue # The added-hunk guard applies to EVERY tripping file — grandfathered or - # not: a new rename site never rides an old exemption (pre-mortem FM7). + # not: a new rename site never rides an old exemption (premortem FM7). # It fires when the added hunk introduces EITHER half of the signature — # the rename OR the temp signal — because file_trips already requires both # in the file: adding a temp-writer that reuses an EXISTING rename is a new diff --git a/scripts/check-bounded-contexts-drift.sh b/scripts/check-bounded-contexts-drift.sh deleted file mode 100755 index c4bd5b306..000000000 --- a/scripts/check-bounded-contexts-drift.sh +++ /dev/null @@ -1,162 +0,0 @@ -#!/usr/bin/env bash -# scripts/check-bounded-contexts-drift.sh -# -# Verify the BC1-BC6 definitions in docs/contracts/bounded-contexts.yaml -# (canonical) match the prose used in the registry docs that classify -# skills against them. -# -# Encodes Phase 2 of the registries-drift remediation (soc-zxia.2): -# extract BC1-BC5 definitions to a single yaml source-of-truth so that -# the same five concepts cannot be restated with drift in 3 places. -# -# Checks: -# 1. Every BC id+name pair in the yaml appears verbatim as a row prefix -# in docs/reference/agentops-skill-domain-map.md Domain Taxonomy table. -# 2. Every BC id+name pair appears in docs/reference/agentops-hexagonal- -# architecture-map.md Bounded Contexts table. -# 3. Each BC's responsibility (canonical sentence) appears verbatim in -# both of the above docs. -# 4. Each BC's product_layer string appears in skill-domain-map.md. -# 5. Each BC's port names appear in hexagonal-architecture-map.md. -# -# Exit codes: -# 0 = no drift -# 1 = drift detected -# 2 = usage / missing input -# -# Modes: -# --check (default) report drift -# --json machine-readable report -# -# Lesson: .agents/learnings/2026-05-17-registries-drift.md -# Phase: soc-zxia.2 (after soc-zxia.1 schema-gate, before soc-zxia.3 generators) - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" -BC_YAML="${REPO_ROOT}/docs/contracts/bounded-contexts.yaml" -MAP_DOC="${REPO_ROOT}/docs/reference/agentops-skill-domain-map.md" -HEX_DOC="${REPO_ROOT}/docs/reference/agentops-hexagonal-architecture-map.md" - -JSON_OUT=0 -for arg in "$@"; do - case "$arg" in - --check) ;; - --json) JSON_OUT=1 ;; - -h|--help) - sed -n '2,30p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//' - exit 0 - ;; - *) - echo "ERROR: unknown arg: $arg (try --help)" >&2 - exit 2 - ;; - esac -done - -for f in "${BC_YAML}" "${MAP_DOC}" "${HEX_DOC}"; do - if [[ ! -f "$f" ]]; then - echo "ERROR: required file missing: $f" >&2 - exit 2 - fi -done - -export BC_YAML MAP_DOC HEX_DOC JSON_OUT - -exec python3 - <<'PY' -import json -import os -import sys -from pathlib import Path - -try: - import yaml -except ImportError: - print("ERROR: PyYAML not installed; install with: pip install pyyaml", file=sys.stderr) - sys.exit(2) - -BC_YAML = Path(os.environ["BC_YAML"]) -MAP_DOC = Path(os.environ["MAP_DOC"]) -HEX_DOC = Path(os.environ["HEX_DOC"]) -JSON_OUT = os.environ.get("JSON_OUT") == "1" - -data = yaml.safe_load(BC_YAML.read_text()) -bcs = data.get("bounded_contexts", []) -if len(bcs) != 6: - print(f"ERROR: expected 6 bounded contexts in {BC_YAML.name}, got {len(bcs)}", file=sys.stderr) - sys.exit(2) - -map_text = MAP_DOC.read_text() -hex_text = HEX_DOC.read_text() - -findings = [] - - -def add(severity, code, msg): - findings.append({"severity": severity, "code": code, "msg": msg}) - - -for bc in bcs: - bc_id = bc["id"] - bc_name = bc["name"] - title = f"{bc_id} {bc_name}" # e.g. "BC1 Corpus" - - # Check 1: title appears in map doc - if title not in map_text: - add("fail", "BC_TITLE_MISSING_FROM_MAP", - f"`{title}` not found in {MAP_DOC.name} — every BC must appear in skill-domain-map") - - # Check 2: title in hex doc - if title not in hex_text: - add("fail", "BC_TITLE_MISSING_FROM_HEX", - f"`{title}` not found in {HEX_DOC.name} — every BC must appear in hexagonal-architecture-map") - - # Check 3: responsibility in both - resp = bc["responsibility"] - if resp not in map_text: - add("fail", "BC_RESP_DRIFT_MAP", - f"`{title}` responsibility in {MAP_DOC.name} drifts from yaml canonical: \"{resp}\"") - if resp not in hex_text: - add("fail", "BC_RESP_DRIFT_HEX", - f"`{title}` responsibility in {HEX_DOC.name} drifts from yaml canonical: \"{resp}\"") - - # Check 4: product_layer in map doc - pl = bc.get("product_layer", "") - if pl and pl not in map_text: - add("fail", "BC_PRODUCT_LAYER_DRIFT", - f"`{title}` product_layer in {MAP_DOC.name} drifts from yaml canonical: \"{pl}\"") - - # Check 5: ports in hex doc - for port in bc.get("ports", []): - if port not in hex_text: - add("warn", "BC_PORT_MISSING_FROM_HEX", - f"`{title}` port `{port}` declared in yaml but not mentioned in {HEX_DOC.name}") - - -fails = [f for f in findings if f["severity"] == "fail"] -warns = [f for f in findings if f["severity"] == "warn"] - -if JSON_OUT: - print(json.dumps({ - "bounded_contexts_checked": len(bcs), - "findings": findings, - "verdict": "FAIL" if fails else ("WARN" if warns else "PASS"), - }, indent=2)) -else: - print(f"Bounded-context drift check: {len(bcs)} BCs in {BC_YAML.name}") - print(f" cross-checked against {MAP_DOC.name} + {HEX_DOC.name}") - print() - for f in findings: - tag = {"fail": "FAIL", "warn": "WARN"}[f["severity"]] - print(f"[{tag}] {f['code']}: {f['msg']}") - print() - if not findings: - print("PASS — registry docs match yaml canonical.") - elif fails: - print(f"FAIL — {len(fails)} drift finding(s), {len(warns)} warning(s)") - else: - print(f"WARN — {len(warns)} warning(s)") - -sys.exit(1 if fails else 0) -PY diff --git a/scripts/check-cathedral-cut-conformance.py b/scripts/check-cathedral-cut-conformance.py index 605d3348d..10a8b45ce 100755 --- a/scripts/check-cathedral-cut-conformance.py +++ b/scripts/check-cathedral-cut-conformance.py @@ -37,8 +37,10 @@ FORBIDDEN_SCHEMA_STATE = { REMOVED_SKILLS = { "discovery", "behavior-first-planning", "goal-design", "crank", "converge", "evolve", "gc-membrane", "pawl-review", "push", "release", "pr-prep", - "beads-br", "beads-bv", "pre-mortem", "pre_mortem", "post-mortem", - "post_mortem", + "beads-br", "beads-bv", +} +REMOVED_MORTEM_ALIASES = { + "pre-mortem", "pre_mortem", "post-mortem", "post_mortem", } REMOVED_COMMANDS = { "pawl", "plan-pawl", "land", "done", "close", "governor", "yield", @@ -90,10 +92,13 @@ def check_skill_graph() -> None: for name, entry in entries.items(): deps = set((entry.get("metadata") or {}).get("dependencies") or []) if name != "rpi": - assert not deps.intersection(CORE), f"{name}: forbidden hard core dependency {deps.intersection(CORE)}" + assert not deps, f"{name}: only rpi may declare hard dependencies: {sorted(deps)}" for name in REMOVED_SKILLS: assert not (ROOT / "skills" / name / "SKILL.md").exists(), f"removed skill is live: {name}" assert not (ROOT / "skills-codex" / name / "SKILL.md").exists(), f"removed Codex skill is live: {name}" + for name in REMOVED_MORTEM_ALIASES: + assert not (ROOT / "skills" / name).exists(), f"removed skill alias is live: {name}" + assert not (ROOT / "skills-codex" / name).exists(), f"removed Codex alias is live: {name}" assert (ROOT / "skills" / "premortem" / "SKILL.md").is_file() assert (ROOT / "skills" / "postmortem" / "SKILL.md").is_file() swarm = entries["swarm"] @@ -137,9 +142,13 @@ def check_core_schemas() -> None: def check_single_pass_contract() -> None: text = (ROOT / "skills" / "rpi" / "SKILL.md").read_text(encoding="utf-8") - for phase in ("plan", "implement", "validate"): - assert text.lower().count(f"invoke `/{phase}` once") == 1, f"RPI does not dispatch {phase} exactly once" assert "Stop regardless" in text + runner = ROOT / "skills" / "rpi" / "scripts" / "run_once.py" + assert runner.is_file(), "RPI has no executable single-pass reference behavior" + tree = ast.parse(runner.read_text(encoding="utf-8"), filename=str(runner)) + assert not any(isinstance(node, (ast.For, ast.While)) for node in ast.walk(tree)), ( + "RPI reference behavior must not contain a dispatch loop" + ) report = json.loads((ROOT / "schemas" / "rpi-report.v1.schema.json").read_text()) assert "next_action" not in property_names(report) @@ -156,6 +165,26 @@ def check_validate_helper() -> None: assert (node.module or "").split(".")[0] not in forbidden_imports, f"validate helper imports {node.module}" elif isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute): assert node.func.attr not in {"system", "popen", "spawn", "execv", "execve"}, f"validate helper launches {node.func.attr}" + spec = importlib.util.spec_from_file_location("cathedral_validate_contract", path) + assert spec and spec.loader + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + with tempfile.TemporaryDirectory() as raw: + try: + module.store_verdict({"verdict": "FAIL"}, Path(raw)) + except module.ContractError: + pass + else: + raise AssertionError("Validate persisted an incomplete verdict.v2 draft") + assert not list(Path(raw).iterdir()), "Validate wrote an invalid verdict artifact" + with tempfile.TemporaryDirectory() as raw: + subject = Path(raw) + (subject / "value").write_text("same", encoding="utf-8") + first = module.build_manifest(subject, ["."], [], git_metadata={"commit": "one"}) + second = module.build_manifest(subject, ["."], [], git_metadata={"commit": "two"}) + assert first["canonical_manifest_digest"] == second["canonical_manifest_digest"], ( + "optional Git metadata changes subject content identity" + ) def check_tombstones() -> None: @@ -171,6 +200,15 @@ def check_tombstones() -> None: } for filename in removed_sources: assert not (ROOT / "cli" / "cmd" / "ao" / filename).exists(), f"old command implementation is live: {filename}" + for filename in ( + "closeout.go", + "inmemory_closeout.go", + "convergence_check.go", + "inmemory_convergence_check.go", + ): + assert not (ROOT / "cli" / "internal" / "ports" / filename).exists(), ( + f"lifecycle authority port remains live: {filename}" + ) def check_dispatch_once() -> None: @@ -195,10 +233,23 @@ def check_dispatch_once() -> None: assert calls == ["one", "two"], f"dispatch count/order mismatch: {calls}" assert results[0]["result"] == "candidate" assert results[1]["error"]["message"] == "observed error" + try: + module.dispatch_once( + [ + {"packet_id": "wide", "write_scope": {"include": ["src/**"]}}, + {"packet_id": "nested", "write_scope": {"include": ["src/lib/**"]}}, + ], + executor, + ) + except ValueError: + pass + else: + raise AssertionError("dispatch_once accepted overlapping glob scopes") def probe_no_substrate_calls() -> None: helper = ROOT / "skills" / "validate" / "scripts" / "validate.py" + rpi_runner = ROOT / "skills" / "rpi" / "scripts" / "run_once.py" with tempfile.TemporaryDirectory() as raw: temp = Path(raw) subject = temp / "subject" @@ -224,6 +275,10 @@ def probe_no_substrate_calls() -> None: assert spec and spec.loader module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) + rpi_spec = importlib.util.spec_from_file_location("cathedral_rpi", rpi_runner) + assert rpi_spec and rpi_spec.loader + rpi = importlib.util.module_from_spec(rpi_spec) + rpi_spec.loader.exec_module(rpi) acceptance = "a" * 64 plan = { "schema_version": "plan-packet.v1", @@ -235,6 +290,7 @@ def probe_no_substrate_calls() -> None: "plan_packet_digest": module.plan_digest(plan), "acceptance_digest": acceptance, "subject_manifest_digest": payload["canonical_manifest_digest"], + "subject_manifest": payload, "changed_path_coverage_complete": True, "actual_changed_paths": ["value.txt"], } @@ -254,8 +310,35 @@ def probe_no_substrate_calls() -> None: "validated_at": "2026-07-14T00:00:00Z", } verdict_dir = temp / ".agentops" / "verdicts" / "sha256" - artifact, verdict_path, existed = module.store_verdict(draft, verdict_dir) - assert artifact["verdict"] == "PASS" and verdict_path.is_file() and not existed + calls: list[str] = [] + + def plan_phase(_intent: object) -> dict: + calls.append("plan") + return plan + + def implement_phase(received_plan: dict) -> dict: + calls.append("implement") + assert received_plan == plan + return candidate + + def validate_phase(received_plan: dict, received_candidate: dict) -> dict: + calls.append("validate") + assert module.scope_result(received_plan, received_candidate)["result"] == "PASS" + artifact, verdict_path, existed = module.store_verdict(draft, verdict_dir) + assert not existed + return { + "verdict": artifact["verdict"], + "subject_manifest_digest": artifact["subject_manifest_digest"], + "verdict_digest": artifact["artifact_digest"], + "verdict_ref": str(verdict_path), + "checked": artifact["checked"], + "not_checked": artifact["not_checked"], + } + + rpi_report = rpi.invoke_once("temporary non-Git experiment", plan_phase, implement_phase, validate_phase) + verdict_path = Path(rpi_report["verdict_ref"]) + assert calls == ["plan", "implement", "validate"], f"RPI dispatch trace is {calls}" + assert rpi_report["status"] == "PASS" and verdict_path.is_file() assert verdict_path.parent == verdict_dir assert not called.exists(), "Validate helper invoked a Git, tracker, push, or delivery executable" diff --git a/scripts/check-compile-oscillation.sh b/scripts/check-compile-oscillation.sh deleted file mode 100755 index e16cdbd9e..000000000 --- a/scripts/check-compile-oscillation.sh +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env bash -# check-compile-oscillation.sh — Gate: No evolve goals are oscillating in the -# most recent defrag report. -# -# Exit 0 = PASS, Exit 1 = FAIL -# -# Looks at .agents/defrag/latest.json first. When missing and COMPILE_OUTPUT_DIR -# is unset, falls back to the freshest Dream overnight preview at -# .agents/overnight/<run>/defrag/latest.json (same shape). This keeps the gate -# green on machines that have run Dream but not a manual `ao defrag`. -set -euo pipefail - -AGENTS_DIR="${AGENTS_DIR:-.agents}" -DEFRAG_LATEST="${COMPILE_OUTPUT_DIR:-$AGENTS_DIR}/defrag/latest.json" - -if [[ ! -f "$DEFRAG_LATEST" && -z "${COMPILE_OUTPUT_DIR:-}" ]]; then - overnight_root="$AGENTS_DIR/overnight" - if [[ -d "$overnight_root" ]]; then - # `-printf` is GNU-only; on BSD/macOS find errors and (under set -euo - # pipefail) the suppressed failure propagates -> this assignment crashes - # the script when the fallback path is hit. Portable TRUE-global mtime - # sort: emit "<mtime>\t<path>" per match (BSD `stat -f %m` / GNU `stat -c - # %Y` fallback), then a single global `sort -n | tail` (ascending + tail - # = newest; tail consumes all input, avoiding the head+pipefail SIGPIPE - # trap; no per-batch mis-sort). Zero matches -> empty fallback. - fallback="$(find "$overnight_root" -path '*/defrag/latest.json' -type f 2>/dev/null \ - | while IFS= read -r _f; do - printf '%s\t%s\n' "$(stat -f %m "$_f" 2>/dev/null || stat -c %Y "$_f" 2>/dev/null)" "$_f" - done | sort -n | tail -n 1 | cut -f2-)" - if [[ -n "$fallback" && -f "$fallback" ]]; then - echo "INFO: $DEFRAG_LATEST not found; falling back to overnight preview $fallback" - DEFRAG_LATEST="$fallback" - fi - fi -fi - -if [[ ! -f "$DEFRAG_LATEST" ]]; then - echo "FAIL: $DEFRAG_LATEST not found — run 'ao defrag' first" - exit 1 -fi - -if ! jq -e "(.oscillation.oscillating_goals // []) | length == 0" "$DEFRAG_LATEST" >/dev/null 2>&1; then - count=$(jq -r "(.oscillation.oscillating_goals // []) | length" "$DEFRAG_LATEST" 2>/dev/null || echo "?") - echo "FAIL: $count oscillating goal(s) in $DEFRAG_LATEST" - exit 1 -fi - -echo "PASS: no oscillating goals in $DEFRAG_LATEST" -exit 0 diff --git a/scripts/check-corpus-freshness.sh b/scripts/check-corpus-freshness.sh deleted file mode 100755 index 61bff202f..000000000 --- a/scripts/check-corpus-freshness.sh +++ /dev/null @@ -1,104 +0,0 @@ -#!/usr/bin/env bash -# practices: [wiki-knowledge-surface, resilience-patterns, ai-assisted-dev] -# Fail if the newest corpus snapshot under $AGENTOPS_CORPUS_SNAPSHOT_DIR -# (or ~/.agentops/corpus-snapshots/) is older than $AGENTOPS_CORPUS_FRESHNESS_DAYS -# days (default 7). Skip cleanly when no snapshots exist (greenfield boxes). -# -# Pair: GOALS.md gate id corpus-freshness (weight 4). -# Companion CLI: ao corpus snapshot, ao corpus restore. - -set -uo pipefail - -THRESHOLD_DAYS="${AGENTOPS_CORPUS_FRESHNESS_DAYS:-7}" -SNAPSHOT_DIR="${AGENTOPS_CORPUS_SNAPSHOT_DIR:-$HOME/.agentops/corpus-snapshots}" - -# Operator override: SKIP=1 short-circuits with PASS (used by CI on fresh boxes -# or in pre-flight environments that don't carry a snapshot dir). -if [ "${AGENTOPS_CORPUS_FRESHNESS_SKIP:-0}" = "1" ]; then - echo "check-corpus-freshness: SKIP (AGENTOPS_CORPUS_FRESHNESS_SKIP=1)" - exit 0 -fi - -if [ ! -d "$SNAPSHOT_DIR" ]; then - echo "check-corpus-freshness: SKIP (no snapshot dir at $SNAPSHOT_DIR — 'make build-flywheel' restores 'ao corpus snapshot' to initialize)" - exit 0 -fi - -# Probe whether the ONLY repair for a stale snapshot -- `ao corpus snapshot` -- -# is actually reachable in the ao binary on this box. That command is compiled -# behind `//go:build flywheel` (cli/cmd/ao/corpus_snapshot.go), so it is ABSENT -# from the default shipped binary agents build. A >7d FAIL there is unfixable by -# design, so it must degrade to an HONEST structural SKIP (exit 75), not a hard -# FAIL that forces AGENTOPS_CORPUS_FRESHNESS_SKIP=1 on every land. This is a D11 -# fitness gate on an ADR-0012-archived surface (off the pawl/provenance -# membrane), so SKIP-when-unfixable is honest, not a silenced pass. -# -# GUARD: report "absent" (skip) ONLY on genuine tool-absence -- a nonzero exit -# PLUS a cobra unknown/removed-command hint. A transient ao-not-on-PATH or a -# broken binary reports "present" so we fall through to the real FAIL. And a -# `flywheel` build (where the tool IS present; `--help` exits 0) also reports -# "present", so a genuine >7d staleness there still FAILs. -repair_tool_absent() { - local ao="" - if [ -n "${AO_BIN:-}" ]; then - ao="$AO_BIN" - elif command -v ao >/dev/null 2>&1; then - ao="$(command -v ao)" - else - local repo_root - repo_root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" - [ -x "$repo_root/cli/bin/ao" ] && ao="$repo_root/cli/bin/ao" - fi - # Unresolvable ao (transient / not built) is NOT genuine tool-absence -> FAIL. - [ -n "$ao" ] || return 1 - - local out rc - out="$("$ao" corpus snapshot --help 2>&1)" - rc=$? - # Tool present (`--help` exits 0 under a flywheel build) -> keep FAIL-on-stale. - [ "$rc" -eq 0 ] && return 1 - # Nonzero + a cobra unknown/removed-command hint -> genuinely absent from binary. - printf '%s' "$out" | grep -Eqi 'unknown command|removed from ao|removed command' -} - -# `-printf` is GNU-only; on BSD/macOS find errors ("unknown primary") -> empty -# LATEST -> this check silently false-SKIPs even when snapshots exist. Replace -# with a portable, TRUE-global mtime sort: emit "<mtime>\t<path>" per match (BSD -# `stat -f %m` / GNU `stat -c %Y` fallback), then a single global `sort -n | tail` -# (ascending + tail = newest; tail consumes all input, avoiding the head+pipefail -# SIGPIPE trap; no per-batch mis-sort). No `-type f` — match the original so -# symlinked snapshots still count; zero matches -> empty LATEST (correct SKIP). -LATEST=$(find "$SNAPSHOT_DIR" -maxdepth 1 -name '*.tar.gz' 2>/dev/null \ - | while IFS= read -r _f; do - printf '%s\t%s\n' "$(stat -f %m "$_f" 2>/dev/null || stat -c %Y "$_f" 2>/dev/null)" "$_f" - done | sort -n | tail -n 1 | cut -f2-) - -if [ -z "$LATEST" ]; then - echo "check-corpus-freshness: SKIP (no *.tar.gz snapshots under $SNAPSHOT_DIR)" - exit 0 -fi - -NOW=$(date +%s) -MTIME=$(stat -c %Y "$LATEST" 2>/dev/null || stat -f %m "$LATEST" 2>/dev/null) -AGE_SECS=$(( NOW - MTIME )) -AGE_DAYS=$(( AGE_SECS / 86400 )) -THRESHOLD_SECS=$(( THRESHOLD_DAYS * 86400 )) - -if [ "$AGE_SECS" -gt "$THRESHOLD_SECS" ]; then - # A stale snapshot is only actionable if the repair tool is in the binary. - # When it is not (default non-flywheel build), degrade to a structural SKIP - # (exit 75 -> GateStatusSkip in cli/internal/gates/scriptrunner.go) instead of - # a hard FAIL that would force AGENTOPS_CORPUS_FRESHNESS_SKIP=1 on every land. - if repair_tool_absent; then - echo "check-corpus-freshness: SKIP — newest snapshot is ${AGE_DAYS}d old (>${THRESHOLD_DAYS}d) but the only repair ('ao corpus snapshot') is absent from this binary (archived behind the 'flywheel' build tag). 'make build-flywheel' restores it." - echo " path: $LATEST" - exit 75 - fi - echo "check-corpus-freshness: FAIL — newest snapshot is ${AGE_DAYS}d old (>${THRESHOLD_DAYS}d threshold)" - echo " path: $LATEST" - echo " fix: make build-flywheel to restore 'ao corpus snapshot', or set AGENTOPS_CORPUS_FRESHNESS_SKIP=1" - exit 1 -fi - -echo "check-corpus-freshness: PASS (newest snapshot ${AGE_DAYS}d old, threshold ${THRESHOLD_DAYS}d)" -exit 0 diff --git a/scripts/check-docs-cli-snippets.sh b/scripts/check-docs-cli-snippets.sh index 0659055f7..740b77888 100755 --- a/scripts/check-docs-cli-snippets.sh +++ b/scripts/check-docs-cli-snippets.sh @@ -3,7 +3,7 @@ # # Resolve every `ao …` command cited in a LIVE doc against the live cobra tree # and fail if a doc names a command that does not exist (a removed/renamed -# command like `ao factory start`, `ao rpi phased`, `ao evolve` on a golden +# command like `ao factory start` or `ao rpi phased` on a golden # path). Port-style sibling of scripts/validate-skill-cli-snippets.sh — it # SHARES that gate's resolution core (scripts/lib/ao-snippet-resolve.*) rather # than forking it (age-gate-the-ungated-egwt.4). diff --git a/scripts/check-epic-children-closed.sh b/scripts/check-epic-children-closed.sh deleted file mode 100755 index e944bf7c1..000000000 --- a/scripts/check-epic-children-closed.sh +++ /dev/null @@ -1,109 +0,0 @@ -#!/usr/bin/env bash -# -# check-epic-children-closed.sh — the no-epic-close-with-open-child GATE. -# -# Enumerates the children of an epic (parent-child dependents) and FAILS if any -# child is still open or in_progress. This is the committed, tested form of the -# advisory "never close an epic with an open child" prose from the crank/evolve -# skills; reconcile-pr.sh --epic calls it before closing an epic. -# -# Usage: -# scripts/check-epic-children-closed.sh <epic-id> -# -# Exit codes (documented contract — tests assert these exactly): -# 0 all children are closed (or the epic has no children) -# 1 at least one child is open/in_progress (each offending child printed) -# 4 usage / missing-dependency / bad-input error -# -# Dependencies: ao, jq (stubbed via PATH in the hermetic bats suite). -# -# Tracker-agnostic (age-5w8fd): all bead reads route through `ao beads exec`, -# which resolves bd vs br and the ledger automatically. Child enumeration: -# ao beads exec children <epic> --json -# br synthesizes plain child ids one per line (extra flags ignored); bd -# forwards verbatim to `bd children <epic> --json`, a JSON array of issue -# objects. Both shapes are handled below. Per-child status uses -# ao beads exec show <child> --json -# whose envelope ao normalizes to the canonical (br) shape for both trackers. - -set -uo pipefail - -usage() { - cat >&2 <<'USAGE' -Usage: scripts/check-epic-children-closed.sh <epic-id> - -Fails (exit 1) if any parent-child child of <epic-id> is still open or -in_progress, naming each offender. Exits 0 when every child is closed. - -Exit codes: 0 all-closed · 1 open-child(ren) · 4 usage. -USAGE -} - -die() { echo "ERROR: $*" >&2; exit 4; } - -EPIC="" -while [[ $# -gt 0 ]]; do - case "$1" in - -h|--help) usage; exit 0 ;; - --*) usage; die "unknown flag: $1" ;; - *) [[ -z "$EPIC" ]] || { usage; die "only one epic-id allowed"; }; EPIC="$1"; shift ;; - esac -done - -[[ -n "$EPIC" ]] || { usage; die "need an epic-id"; } - -command -v ao >/dev/null 2>&1 || die "ao CLI not on PATH" -command -v jq >/dev/null 2>&1 || die "jq not on PATH" - -# Enumerate child ids via the tracker-agnostic entry point. A tracker error is -# a hard stop (exit 4) — a gate that cannot see the children must not pass. -children_raw="$(ao beads exec children "$EPIC" --json)" \ - || die "children query failed for $EPIC (ao beads exec children)" - -# Two shapes reach us: bd emits a JSON array of issue objects; br emits plain -# child ids one per line. Detect JSON by the first non-space character. -first_char="$(printf '%s' "$children_raw" | tr -d '[:space:]' | head -c1)" -if [[ "$first_char" == "[" || "$first_char" == "{" ]]; then - child_ids="$(printf '%s' "$children_raw" | jq -r ' - (if type=="array" then . else (.issues // []) end) - | .[] - | (.id // empty) - ' 2>/dev/null)" || die "could not parse children JSON for $EPIC" -else - child_ids="$(printf '%s\n' "$children_raw" | awk 'NF{print $1}')" -fi - -if [[ -z "$child_ids" ]]; then - echo "OK: epic $EPIC has no open children (no children found)" >&2 - exit 0 -fi - -offenders=0 -while IFS= read -r child; do - [[ -n "$child" ]] || continue - # A per-child status read failure is fail-closed as an offender (exit 1), - # not exit 4: one unreadable child must block the epic close, not abort the - # report of every other offender. Only the enumeration error above is 4. - status="$(ao beads exec show "$child" --json 2>/dev/null | jq -r '.[0].status // .status // empty' 2>/dev/null)" - case "$status" in - open|in_progress) - echo "OPEN-CHILD: $child status=$status" >&2 - offenders=$((offenders+1)) - ;; - "") - echo "WARN: could not read status for child $child (treating as offender)" >&2 - offenders=$((offenders+1)) - ;; - *) - : # closed / done / cancelled — fine - ;; - esac -done <<< "$child_ids" - -if [[ "$offenders" -gt 0 ]]; then - echo "EPIC-GATE FAIL: $EPIC has $offenders open/in_progress child(ren)" >&2 - exit 1 -fi - -echo "OK: all children of $EPIC are closed" >&2 -exit 0 diff --git a/scripts/check-evolve-cycle-logging.sh b/scripts/check-evolve-cycle-logging.sh deleted file mode 100755 index f820ddf1f..000000000 --- a/scripts/check-evolve-cycle-logging.sh +++ /dev/null @@ -1,179 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Validate evolve cycle-history.jsonl integrity. -# Checks: file exists (if evolve has run), entries have required fields, -# cycle numbers are monotonically increasing, and productive entries carry -# the fields needed for trajectory plotting. Historical rows remain warning- -# tolerant so legacy schema drift does not block the repo. Numbering gaps are -# warnings by default; pass --strict-gaps to fail on them. - -STRICT_GAPS=false -if [[ "${1:-}" == "--strict-gaps" ]]; then - STRICT_GAPS=true - shift -fi - -if [[ $# -gt 0 ]]; then - echo "Usage: $0 [--strict-gaps]" - exit 2 -fi - -HISTORY=".agents/evolve/cycle-history.jsonl" - -# If no evolve directory exists, skip gracefully (evolve hasn't run yet) -if [[ ! -d ".agents/evolve" ]]; then - echo "No .agents/evolve/ directory — evolve has not run yet. Skipping." - exit 0 -fi - -# If evolve directory exists but no history file, check for fitness snapshots -# which would indicate cycles ran without logging (the exact bug we're catching) -if [[ ! -f "$HISTORY" ]]; then - SNAPSHOT_COUNT=$(find .agents/evolve -name 'fitness-*-post.json' 2>/dev/null | wc -l | tr -d ' ') - if [[ "$SNAPSHOT_COUNT" -gt 0 ]]; then - echo "ERROR: Found $SNAPSHOT_COUNT post-cycle fitness snapshots but no cycle-history.jsonl." - echo "This indicates evolve cycles ran without logging — the tracking bug this goal prevents." - exit 1 - fi - echo "No cycle-history.jsonl and no post-cycle snapshots. Evolve has not completed any cycles." - exit 0 -fi - -LINE_NUM=0 -ERRORS=0 -WARNINGS=0 -PREV_CYCLE=-1 -LAST_NON_EMPTY=0 - -while IFS= read -r line; do - LINE_NUM=$((LINE_NUM + 1)) - - # Skip empty lines - [[ -z "$line" ]] && continue - LAST_NON_EMPTY=$LINE_NUM - - # Validate JSON - if ! echo "$line" | jq empty 2>/dev/null; then - echo "ERROR: Line $LINE_NUM is not valid JSON" - ERRORS=$((ERRORS + 1)) - continue - fi - - # Check required scalar fields - for field in cycle result timestamp; do - VALUE=$(echo "$line" | jq -r ".$field // empty") - if [[ -z "$VALUE" ]]; then - echo "ERROR: Line $LINE_NUM missing required field: $field" - ERRORS=$((ERRORS + 1)) - fi - done - - TARGET=$(echo "$line" | jq -r '.target // .goal_id // empty') - GOAL_IDS_LEN=$(echo "$line" | jq -r '(.goal_ids // []) | length') - if [[ -z "$TARGET" && "$GOAL_IDS_LEN" -eq 0 ]]; then - echo "ERROR: Line $LINE_NUM missing target/goal_id and goal_ids" - ERRORS=$((ERRORS + 1)) - elif [[ -z "$TARGET" ]]; then - echo "WARN: Line $LINE_NUM uses goal_ids parallel schema without target" - WARNINGS=$((WARNINGS + 1)) - elif [[ "$(echo "$line" | jq -r 'has("target")')" != "true" ]]; then - echo "WARN: Line $LINE_NUM uses legacy goal_id field" - WARNINGS=$((WARNINGS + 1)) - fi - - RESULT=$(echo "$line" | jq -r '.result // empty') - case "$RESULT" in - improved|regressed|harvested) - for field in sha goals_passing goals_total; do - VALUE=$(echo "$line" | jq -r ".$field // empty") - if [[ -z "$VALUE" ]]; then - echo "WARN: Line $LINE_NUM missing productive field: $field" - WARNINGS=$((WARNINGS + 1)) - fi - done - for field in goals_passing goals_total; do - TYPE=$(echo "$line" | jq -r "if has(\"$field\") then (.$field | type) else \"missing\" end") - if [[ "$TYPE" != "number" ]]; then - echo "WARN: Line $LINE_NUM uses non-numeric $field ($TYPE)" - WARNINGS=$((WARNINGS + 1)) - fi - done - CANONICAL_TYPE=$(echo "$line" | jq -r 'if has("canonical_sha") then (.canonical_sha | type) else "missing" end') - if [[ "$CANONICAL_TYPE" != "missing" && "$CANONICAL_TYPE" != "string" ]]; then - echo "WARN: Line $LINE_NUM uses non-string canonical_sha ($CANONICAL_TYPE)" - WARNINGS=$((WARNINGS + 1)) - fi - LOG_TYPE=$(echo "$line" | jq -r 'if has("log_sha") then (.log_sha | type) else "missing" end') - if [[ "$LOG_TYPE" != "missing" && "$LOG_TYPE" != "string" ]]; then - echo "WARN: Line $LINE_NUM uses non-string log_sha ($LOG_TYPE)" - WARNINGS=$((WARNINGS + 1)) - fi - SHA_VALUE=$(echo "$line" | jq -r '.sha // empty') - CANONICAL_VALUE=$(echo "$line" | jq -r '.canonical_sha // empty') - if [[ -n "$CANONICAL_VALUE" && -z "$SHA_VALUE" ]]; then - echo "WARN: Line $LINE_NUM has canonical_sha but no compatibility sha" - WARNINGS=$((WARNINGS + 1)) - elif [[ -n "$SHA_VALUE" && -n "$CANONICAL_VALUE" && "$SHA_VALUE" != "$CANONICAL_VALUE" ]]; then - echo "WARN: Line $LINE_NUM has sha/canonical_sha mismatch" - WARNINGS=$((WARNINGS + 1)) - fi - ;; - unchanged|quarantined) - : - ;; - *) - echo "WARN: Line $LINE_NUM uses unrecognized result: $RESULT" - WARNINGS=$((WARNINGS + 1)) - ;; - esac - - CYCLE_TYPE=$(echo "$line" | jq -r '.cycle | type') - if [[ "$CYCLE_TYPE" != "number" ]]; then - echo "ERROR: Line $LINE_NUM cycle is not numeric ($CYCLE_TYPE)" - ERRORS=$((ERRORS + 1)) - continue - fi - - # Check cycle number monotonicity - CYCLE=$(echo "$line" | jq -r '.cycle // -1') - if [[ "$PREV_CYCLE" -ge 0 ]]; then - EXPECTED=$((PREV_CYCLE + 1)) - if [[ "$CYCLE" -le "$PREV_CYCLE" ]]; then - echo "ERROR: Non-increasing cycle at line $LINE_NUM: previous $PREV_CYCLE, got $CYCLE" - ERRORS=$((ERRORS + 1)) - elif [[ "$CYCLE" -ne "$EXPECTED" ]]; then - if [[ "$STRICT_GAPS" == "true" ]]; then - echo "ERROR: Cycle gap at line $LINE_NUM: expected cycle $EXPECTED, got $CYCLE" - ERRORS=$((ERRORS + 1)) - else - echo "WARN: Cycle gap at line $LINE_NUM: expected cycle $EXPECTED, got $CYCLE" - WARNINGS=$((WARNINGS + 1)) - fi - fi - elif [[ "$CYCLE" -ne 1 ]]; then - echo "WARN: First logged cycle is $CYCLE (expected 1)" - WARNINGS=$((WARNINGS + 1)) - fi - PREV_CYCLE="$CYCLE" - -done < "$HISTORY" - -if [[ "$LAST_NON_EMPTY" -eq 0 ]]; then - echo "WARN: cycle-history.jsonl exists but is empty." - exit 0 -fi - -if [[ "$ERRORS" -gt 0 ]]; then - echo - echo "ERROR: $ERRORS integrity issues found in cycle-history.jsonl ($LINE_NUM entries checked)." - exit 1 -fi - -if [[ "$WARNINGS" -gt 0 ]]; then - echo "cycle-history.jsonl OK with warnings: $LAST_NON_EMPTY entries checked, cycles 1-$PREV_CYCLE, warnings=$WARNINGS." - exit 0 -fi - -echo "cycle-history.jsonl OK: $LAST_NON_EMPTY entries, cycles 1-$PREV_CYCLE, required fields present." -exit 0 diff --git a/scripts/check-gate-fixhints-live.sh b/scripts/check-gate-fixhints-live.sh index 0567dba88..d7901eb5e 100755 --- a/scripts/check-gate-fixhints-live.sh +++ b/scripts/check-gate-fixhints-live.sh @@ -10,9 +10,8 @@ # This scans the gate-backing scripts for fix/remedy/repair directives that name # an `ao <subcommand>` and asserts the subcommand resolves in the LIVE ao command # tree. A dead one is reported UNLESS the same line carries a removal/historical -# marker (restore|removed|retired|archived|historical|legacy|was|formerly) — so a -# line that documents what a command USED to be (like the now-fixed corpus hint, -# 'make build-flywheel to restore ...ao corpus snapshot') is correctly not flagged. +# marker (removed|retired|historical|was|formerly) — so a line that documents +# what a command used to be is correctly not flagged. # # Posture: WARN-ONLY by default (GOALS row, tags: warn-only) — prints offenders, # exits 0. --strict (or AGENTOPS_GATE_FIXHINTS_STRICT=1) exits nonzero on any dead diff --git a/scripts/check-gated-close-rate.sh b/scripts/check-gated-close-rate.sh deleted file mode 100755 index c8a10b955..000000000 --- a/scripts/check-gated-close-rate.sh +++ /dev/null @@ -1,97 +0,0 @@ -#!/usr/bin/env bash -# practices: [wiki-knowledge-surface, resilience-patterns, ai-assisted-dev] -# check-gated-close-rate.sh — fitness gate for the close-admission discipline (cp-m8md, kin of cp-irmu). -# -# Of the last N closes in the control-plane br ledger, what fraction carry the -# close-admission gate stamp ('close-admission gate PASS') in their close_reason? -# A high rate means closes are going through the gate, not around it. -# -# This gate reads control-plane's ledger, which is OPTIONAL from agentops's POV: -# - CONTROL_PLANE_ROOT env override (default /Users/bo/dev/control-plane) -# - if br is unavailable OR the CP root is absent, SKIP cleanly (exit 0) with a notice -# — mirrors the house optional-dependency pattern (see check-corpus-freshness.sh). -# - SKIP=1 (AGENTOPS_GATED_CLOSE_RATE_SKIP=1) short-circuits with PASS. -# -# The ledger read is strictly READ-ONLY (br list --status closed --json). -# -# Threshold: default 70 (percent). Measured 2026-06-10 = 80% over last 20 closes. -# Override: --threshold N (percent) or AGENTOPS_GATED_CLOSE_RATE_THRESHOLD. -# Window: default 20 closes. Override: --window N or AGENTOPS_GATED_CLOSE_RATE_WINDOW. -# -# Exit codes: 0 PASS or clean SKIP; 1 FAIL (rate below threshold). -# -# Usage: bash scripts/check-gated-close-rate.sh [--threshold N] [--window N] [--json] - -set -uo pipefail - -CP_ROOT="${CONTROL_PLANE_ROOT:-/Users/bo/dev/control-plane}" -THRESHOLD="${AGENTOPS_GATED_CLOSE_RATE_THRESHOLD:-70}" -WINDOW="${AGENTOPS_GATED_CLOSE_RATE_WINDOW:-20}" -STAMP='close-admission gate PASS' -JSON=0 - -while [ $# -gt 0 ]; do - case "$1" in - --threshold) shift; THRESHOLD="${1:?--threshold needs a value}" ;; - --window) shift; WINDOW="${1:?--window needs a value}" ;; - --json) JSON=1 ;; - -h|--help) grep '^#' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; - *) echo "check-gated-close-rate: unknown arg: $1" >&2; exit 2 ;; - esac - shift -done - -# Operator override -if [ "${AGENTOPS_GATED_CLOSE_RATE_SKIP:-0}" = "1" ]; then - echo "check-gated-close-rate: SKIP (AGENTOPS_GATED_CLOSE_RATE_SKIP=1)" - exit 0 -fi - -# Optional cross-repo dependency: br CLI + control-plane root must both be present. -if ! command -v br >/dev/null 2>&1; then - echo "check-gated-close-rate: SKIP (br CLI not on PATH — close-admission ledger unreadable here)" - exit 0 -fi -if [ ! -d "$CP_ROOT" ]; then - echo "check-gated-close-rate: SKIP (control-plane root absent at $CP_ROOT — set CONTROL_PLANE_ROOT)" - exit 0 -fi -if ! command -v jq >/dev/null 2>&1; then - echo "check-gated-close-rate: SKIP (jq not on PATH)" - exit 0 -fi - -# READ-ONLY ledger read, run from the CP root so br resolves the right workspace. -CLOSED_JSON=$( (cd "$CP_ROOT" && br list --status closed --json 2>/dev/null) ) -if [ -z "$CLOSED_JSON" ]; then - echo "check-gated-close-rate: SKIP (br returned no closed-issue JSON from $CP_ROOT)" - exit 0 -fi - -TOTAL=$(printf '%s' "$CLOSED_JSON" | jq --argjson n "$WINDOW" \ - '.issues | sort_by(.closed_at) | reverse | .[0:$n] | length' 2>/dev/null) -if [ -z "$TOTAL" ] || [ "$TOTAL" = "null" ] || [ "$TOTAL" -eq 0 ] 2>/dev/null; then - echo "check-gated-close-rate: SKIP (no closed beads in window from $CP_ROOT)" - exit 0 -fi - -GATED=$(printf '%s' "$CLOSED_JSON" | jq --argjson n "$WINDOW" --arg stamp "$STAMP" \ - '.issues | sort_by(.closed_at) | reverse | .[0:$n] - | map(select(.close_reason // "" | contains($stamp))) | length' 2>/dev/null) - -RATE=$(( GATED * 100 / TOTAL )) - -if [ "$JSON" -eq 1 ]; then - result=$([ "$RATE" -ge "$THRESHOLD" ] && echo PASS || echo FAIL) - printf '{"gated":%d,"total":%d,"rate_pct":%d,"threshold_pct":%d,"window":%d,"result":"%s"}\n' \ - "$GATED" "$TOTAL" "$RATE" "$THRESHOLD" "$WINDOW" "$result" -fi - -if [ "$RATE" -lt "$THRESHOLD" ]; then - [ "$JSON" -eq 1 ] || echo "check-gated-close-rate: FAIL — ${GATED}/${TOTAL} of last $WINDOW closes carry the gate stamp = ${RATE}% (< ${THRESHOLD}% threshold)" - [ "$JSON" -eq 1 ] || echo " fix: route closes through ctl-close-admission so close_reason carries 'close-admission gate PASS'" - exit 1 -fi - -[ "$JSON" -eq 1 ] || echo "check-gated-close-rate: PASS — ${GATED}/${TOTAL} of last $WINDOW closes gated = ${RATE}% (>= ${THRESHOLD}% threshold)" -exit 0 diff --git a/scripts/check-goal-design-packet.sh b/scripts/check-goal-design-packet.sh deleted file mode 100755 index edfa6c3a5..000000000 --- a/scripts/check-goal-design-packet.sh +++ /dev/null @@ -1,182 +0,0 @@ -#!/usr/bin/env bash -# Validate a goal-design packet directory containing intent.md and driver.md. -# -# Exit codes: -# 0 - packet is valid -# 1 - packet is invalid -# 2 - usage, dependency, or repository setup error -set -eEuo pipefail - -usage() { - cat <<'USAGE' -Usage: scripts/check-goal-design-packet.sh <packet-dir> - -Validates .agents/goal-design/<slug>/intent.md and driver.md frontmatter against -schemas/goal-design-*.v1.schema.json, then checks driver intent_ref.sha256 -against the current intent.md bytes plus cross-file identity. -USAGE -} - -if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then - usage - exit 0 -fi - -if [[ "$#" -ne 1 ]]; then - usage >&2 - exit 2 -fi - -if ! command -v python3 >/dev/null 2>&1; then - echo "check-goal-design-packet: python3 is required" >&2 - exit 2 -fi - -if ! python3 -c "import yaml, jsonschema" >/dev/null 2>&1; then - echo "check-goal-design-packet: python deps missing (need PyYAML and jsonschema)" >&2 - exit 2 -fi - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -if REPO_ROOT="$(git -C "$SCRIPT_DIR/.." rev-parse --show-toplevel 2>/dev/null)"; then - : -else - REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -fi -PACKET_DIR="$1" - -python3 - "$REPO_ROOT" "$PACKET_DIR" <<'PY' -import hashlib -import json -import re -import sys -from pathlib import Path - -import yaml -from jsonschema import Draft202012Validator, FormatChecker - -repo_root = Path(sys.argv[1]).resolve() -packet_dir = Path(sys.argv[2]) -if not packet_dir.is_absolute(): - packet_dir = (Path.cwd() / packet_dir).resolve() - -intent_path = packet_dir / "intent.md" -driver_path = packet_dir / "driver.md" -intent_schema_path = repo_root / "schemas" / "goal-design-intent.v1.schema.json" -driver_schema_path = repo_root / "schemas" / "goal-design-driver.v1.schema.json" - - -def fail(message: str, code: int = 1) -> None: - print(f"FAIL: {message}", file=sys.stderr) - raise SystemExit(code) - - -def require_file(path: Path) -> None: - if not path.is_file(): - fail(f"required file not found: {path}", 2) - - -def load_frontmatter(path: Path): - text = path.read_text(encoding="utf-8") - if not text.startswith("---\n"): - fail(f"{path.name} missing YAML frontmatter") - end = text.find("\n---\n", 4) - if end < 0: - fail(f"{path.name} has unterminated YAML frontmatter") - raw = text[4:end] - try: - data = yaml.safe_load(raw) - except yaml.YAMLError as exc: - fail(f"{path.name} YAML parse error: {exc}") - if not isinstance(data, dict): - fail(f"{path.name} frontmatter did not parse as a mapping") - return data, text - - -def load_schema(path: Path): - try: - schema = json.loads(path.read_text(encoding="utf-8")) - except json.JSONDecodeError as exc: - fail(f"schema is invalid JSON: {path}: {exc}", 2) - Draft202012Validator.check_schema(schema) - return schema - - -def validate(label: str, data, schema) -> None: - validator = Draft202012Validator(schema, format_checker=FormatChecker()) - errors = sorted(validator.iter_errors(data), key=lambda error: list(error.absolute_path)) - if errors: - error = errors[0] - loc = "/".join(str(part) for part in error.absolute_path) or "<root>" - fail(f"{label} schema violation at {loc}: {error.message}") - - -def check_cross_file_identity(intent_data, driver_data) -> None: - intent_slug = str(intent_data["slug"]) - driver_slug = str(driver_data["slug"]) - if intent_slug != driver_slug: - fail(f"slug mismatch: intent.slug {intent_slug!r} does not match driver.slug {driver_slug!r}") - - expected_intent_ref = f".agents/goal-design/{intent_slug}/intent.md" - actual_intent_ref = str(driver_data["intent_ref"]["path"]) - if actual_intent_ref != expected_intent_ref: - fail( - "driver intent_ref.path must identify packet intent: " - f"expected {expected_intent_ref!r}, found {actual_intent_ref!r}" - ) - - -def normalize_label(value: str) -> str: - return " ".join(value.casefold().split()) - - -def check_candidate_scenario_mapping(intent_data, driver_data) -> None: - scenarios = intent_data["bdd"]["scenarios"] - scenario_ids = {str(scenario["id"]) for scenario in scenarios} - scenario_names = { - normalize_label(str(scenario["name"])) - for scenario in scenarios - if str(scenario["name"]).strip() - } - - for index, candidate in enumerate(driver_data["candidate_beads"]): - behavior = str(candidate["behavior"]) - scenario_refs = re.findall(r"\bS[0-9]+\b", behavior) - unknown_refs = sorted({ref for ref in scenario_refs if ref not in scenario_ids}) - if unknown_refs: - fail( - f"candidate_beads/{index}/behavior references scenario ids not present " - f"in intent.bdd.scenarios: {', '.join(unknown_refs)}" - ) - if scenario_refs: - continue - - normalized_behavior = normalize_label(behavior) - if not any(name in normalized_behavior for name in scenario_names): - fail( - f"candidate_beads/{index}/behavior must reference an id or name " - "from intent.bdd.scenarios" - ) - - -for required in (intent_path, driver_path, intent_schema_path, driver_schema_path): - require_file(required) - -intent_data, intent_text = load_frontmatter(intent_path) -driver_data, driver_text = load_frontmatter(driver_path) - -validate("intent.md", intent_data, load_schema(intent_schema_path)) -validate("driver.md", driver_data, load_schema(driver_schema_path)) - -expected_sha = hashlib.sha256(intent_path.read_bytes()).hexdigest() -actual_sha = driver_data["intent_ref"]["sha256"] -if expected_sha != actual_sha: - fail( - "driver intent_ref.sha256 is stale: " - f"expected {expected_sha}, found {actual_sha}" - ) - -check_cross_file_identity(intent_data, driver_data) -check_candidate_scenario_mapping(intent_data, driver_data) -print(f"goal-design packet valid: {packet_dir}") -PY diff --git a/scripts/check-hookless-cold-start.sh b/scripts/check-hookless-cold-start.sh index 86d901857..192a538b7 100755 --- a/scripts/check-hookless-cold-start.sh +++ b/scripts/check-hookless-cold-start.sh @@ -11,7 +11,7 @@ # surfaces. It deliberately does NOT scan all of docs/+skills/ (release notes, the # hooks-authoring skill, and scope guards legitimately name hook paths). The repo # CLAUDE.md + workflow owner (docs/agent-workflow-reference.md) and the session-scope surfaces -# (ship-loop, the evolve post-mortem checkpoint) were brought IN scope by ag-o5xp: +# (ship-loop, the evolve postmortem checkpoint) were brought IN scope by ag-o5xp: # their `session-pr-counter.sh` reference was a stale active-surface promise after # the hook was removed in the #511 hookless teardown. They are now hedged # (removed / opt-in / hooks-authoring) and gated here so the drift cannot recur. @@ -19,15 +19,6 @@ set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -# ag-2vz5v: resolve skill paths through the dispositions ledger so folds/cuts -# auto-retarget. Identity fallback keeps hermetic test copies self-contained. -if [[ -f "$ROOT/scripts/lib/resolve-skill-path.sh" ]]; then - # shellcheck source=lib/resolve-skill-path.sh - source "$ROOT/scripts/lib/resolve-skill-path.sh" -else - resolve_skill_path() { printf '%s\n' "$1"; } -fi - FILES=( "AGENTS.md" "docs/architecture/primitive-chains.md" @@ -45,13 +36,10 @@ FILES=( "skills/review/SKILL.md" "skills-codex/review/SKILL.md" "docs/newcomer-guide.md" - # Workflow-discipline surfaces (ag-o5xp): must never present the removed + # Workflow-discipline surfaces: must never present the removed # session-pr-counter hook as an active surface. "CLAUDE.md" "docs/agent-workflow-reference.md" - "skills/crank/SKILL.md" - "skills/evolve/references/postmortem-checkpoint.md" - "skills-codex/evolve/references/postmortem-checkpoint.md" ) # A hook path is "hedged" (allowed) when its line also carries one of these. @@ -68,9 +56,7 @@ fail() { violations=0 scanned=0 -for raw in "${FILES[@]}"; do - rel="$(resolve_skill_path "$raw")" - [[ -n "$rel" ]] || continue # cut slug: resolver warned; skip visibly +for rel in "${FILES[@]}"; do file="$ROOT/$rel" [[ -f "$file" ]] || continue scanned=$((scanned + 1)) diff --git a/scripts/check-mortem-compatibility.sh b/scripts/check-mortem-compatibility.sh deleted file mode 100755 index 0420ca481..000000000 --- a/scripts/check-mortem-compatibility.sh +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -if [[ $# -ne 0 ]]; then - echo "usage: $0" >&2 - exit 2 -fi - -repo_root="$(git rev-parse --show-toplevel)" -fixtures="${MORTEM_COMPAT_FIXTURES_DIR:-$repo_root/tests/fixtures/mortem-compatibility}" -if [[ "$fixtures" != /* ]]; then - fixtures="$repo_root/$fixtures" -fi - -required=( - legacy-directory/pre-mortem-check.json - directory-conflict/pre-mortem-check.json - directory-conflict/premortem-check.json - explicit-skill-redirect.yaml -) -for relative in "${required[@]}"; do - [[ -f "$fixtures/$relative" ]] || { - echo "missing compatibility fixture: tests/fixtures/mortem-compatibility/$relative" >&2 - exit 1 - } -done - -python3 - "$fixtures" <<'PY' -import json -import pathlib -import re -import sys - -root = pathlib.Path(sys.argv[1]) - -def load_json(relative): - path = root / relative - try: - return json.loads(path.read_bytes()) - except Exception as exc: - raise SystemExit(f"{relative}: invalid JSON fixture: {exc}") - -legacy_path = "directory-conflict/pre-mortem-check.json" -canonical_path = "directory-conflict/premortem-check.json" -legacy = load_json(legacy_path) -canonical = load_json(canonical_path) -if legacy.get("id") != canonical.get("id") or legacy == canonical: - raise SystemExit( - f"{legacy_path} and {canonical_path}: expected different content for the same ID" - ) - -redirect_path = "explicit-skill-redirect.yaml" -redirect_text = (root / redirect_path).read_text(encoding="utf-8") -for source, target in ( - ("pre-mortem", "premortem"), ("post-mortem", "postmortem"), - ("pre_mortem", "premortem"), ("post_mortem", "postmortem"), -): - block = re.search( - rf"^ {re.escape(source)}:\n((?: .*\n?)*)", redirect_text, re.MULTILINE - ) - if ( - not block - or "state: merged-into" not in block.group(1) - or f"merged-into: {target}" not in block.group(1) - ): - actual = block.group(1).strip() if block else "missing block" - raise SystemExit(f"{redirect_path}: {source} must redirect to {target}; got {actual}") -PY - -( - cd "$repo_root/cli" - export MORTEM_COMPAT_FIXTURES_DIR="$fixtures" - go test ./internal/domain/packet -run 'TestExecutionPacketPremortemContract' -count=1 - go test ./internal/adapters/storage_fs -run 'TestRepo_PremortemContract' -count=1 - go test ./internal/ports -run 'Mortem|Premortem' -count=1 - go test ./cmd/ao \ - -run 'TestProductionFindingCompiler_PremortemAliases|TestCanonicalMortemRatchetStepsRemainRegistered|TestStigmergicScorecard_EmitsCanonicalMortem|TestStatusFlywheel_EmitsCanonicalPremortem|TestPremortemDirectoryReader_|TestMortemCompatibilityFixtures_Directory|TestCollectContextExplainHealth_UsesCanonicalReconciledPremortemCount|TestRunContextExplain_RejectsConflictingPremortemDirectories|TestMortemJSONLChainLoad_' \ - -count=1 -) - -source "$repo_root/scripts/lib/resolve-skill-path.sh" -export SKILL_DISPOSITIONS_FILE="$fixtures/explicit-skill-redirect.yaml" -[[ "$(resolve_skill_path skills/pre-mortem/SKILL.md)" == "skills/premortem/SKILL.md" ]] -[[ "$(resolve_skill_path skills-codex/post-mortem/SKILL.md)" == "skills-codex/postmortem/SKILL.md" ]] -[[ "$(resolve_skill_path skills/pre_mortem/SKILL.md)" == "skills/premortem/SKILL.md" ]] -[[ "$(resolve_skill_path skills-codex/post_mortem/SKILL.md)" == "skills-codex/postmortem/SKILL.md" ]] -[[ "$(resolve_skill_path skills/premortem/SKILL.md)" == "skills/premortem/SKILL.md" ]] -[[ "$(resolve_skill_path skills-codex/postmortem/SKILL.md)" == "skills-codex/postmortem/SKILL.md" ]] - -echo "mortem compatibility: PASS (canonical packet contract; non-packet directory and skill redirects)" diff --git a/scripts/check-no-apparatus-regrowth.sh b/scripts/check-no-apparatus-regrowth.sh deleted file mode 100755 index 448cd16a4..000000000 --- a/scripts/check-no-apparatus-regrowth.sh +++ /dev/null @@ -1,101 +0,0 @@ -#!/usr/bin/env bash -# check-no-apparatus-regrowth.sh — anti-regeneration fitness gate (GOALS.md directive D15) -# -# STAY-REMOVED guard, NOT a size metric. The teardown (epic ag-097 and its -# waves) deletes over-projected apparatus — dead packages, the gascity compat -# cluster, duplicated projections. Nothing in the fitness function rewards -# keeping the system small, so the /evolve loop could rebuild what the teardown -# removed. This gate makes "the slop we removed stays removed" a MEASURED -# outcome: it FAILS only when a path the teardown explicitly removed comes BACK. -# -# It does NOT count lines/files/jobs and does NOT penalize legitimate new -# growth — it only fires on regrowth of specifically-removed surfaces, which is -# a real user outcome, not a code metric (GOALS.md "## Anti Stars": "Goals that -# measure code metrics instead of user outcomes"). -# -# The removed-surface list is the committed manifest scripts/removed-apparatus.txt -# (one path per line, repo-root-relative; `#` comments and blank lines ignored). -# Future teardown waves APPEND to that manifest rather than editing this script. -# -# Exit 0 = PASS (every removed surface stays gone). -# Exit 1 = FAIL (at least one removed surface regrew) with a per-path message. -# -# Flags: -# --json emit a machine-readable result to stdout -# --manifest <path> override the manifest path (default scripts/removed-apparatus.txt) -# --root <path> override the repo root used to resolve manifest entries -# -h, --help show this help -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -MANIFEST="" -JSON=0 - -usage() { - sed -n '2,28p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//' -} - -while [[ $# -gt 0 ]]; do - case "$1" in - --json) JSON=1; shift ;; - --manifest) MANIFEST="$2"; shift 2 ;; - --root) REPO_ROOT="$2"; shift 2 ;; - -h|--help) usage; exit 0 ;; - *) echo "check-no-apparatus-regrowth: unknown argument: $1 (try --help)" >&2; exit 2 ;; - esac -done - -if [[ -z "$MANIFEST" ]]; then - MANIFEST="$SCRIPT_DIR/removed-apparatus.txt" -fi - -if [[ ! -f "$MANIFEST" ]]; then - echo "check-no-apparatus-regrowth: FAIL — manifest not found: $MANIFEST" >&2 - exit 1 -fi - -regrown=() -checked=0 - -while IFS= read -r raw || [[ -n "$raw" ]]; do - # Strip inline comments and surrounding whitespace; skip blanks/comments. - line="${raw%%#*}" - line="$(printf '%s' "$line" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" - [[ -z "$line" ]] && continue - checked=$((checked + 1)) - if [[ -e "$REPO_ROOT/$line" ]]; then - regrown+=("$line") - fi -done < "$MANIFEST" - -if [[ "$JSON" -eq 1 ]]; then - # Build a JSON array of regrown paths without requiring jq. - arr="" - for p in "${regrown[@]:-}"; do - [[ -z "$p" ]] && continue - [[ -n "$arr" ]] && arr="$arr," - arr="$arr\"$p\"" - done - status="pass" - [[ "${#regrown[@]}" -gt 0 ]] && status="fail" - printf '{"gate":"no-apparatus-regrowth","status":"%s","checked":%d,"regrown":[%s]}\n' \ - "$status" "$checked" "$arr" -fi - -if [[ "${#regrown[@]}" -gt 0 ]]; then - if [[ "$JSON" -ne 1 ]]; then - echo "check-no-apparatus-regrowth: FAIL — ${#regrown[@]} teardown-removed surface(s) regrew:" - for p in "${regrown[@]}"; do - printf ' - %s (the teardown removed this; it must stay removed)\n' "$p" - done - echo - echo "If a return is intentional, remove the path from $MANIFEST in the same change and explain why." - fi - exit 1 -fi - -if [[ "$JSON" -ne 1 ]]; then - echo "check-no-apparatus-regrowth: PASS — all $checked teardown-removed surface(s) stay removed" -fi -exit 0 diff --git a/scripts/check-no-tracked-agents.sh b/scripts/check-no-tracked-agents.sh index 68b72392a..87b1da3d6 100755 --- a/scripts/check-no-tracked-agents.sh +++ b/scripts/check-no-tracked-agents.sh @@ -1,91 +1,45 @@ #!/usr/bin/env bash set -euo pipefail -# Enforce: repo-root .agents/ holds local/private agent runtime state and must -# not bleed into git, EXCEPT for an explicit audit-truth allowlist that -# compounds across nightly runs. The allowlist is intentionally narrow — -# baseline/final goal snapshots, evolve cycle history, per-goal attempt -# history, dream probe registry, findings registry, the live next-work -# queue, and the Reconciliation Engine arc's static thesis snapshot + -# operator decision records (.agents/reconcile/wave-0-thesis-snapshot.md, -# .agents/reconcile/thesis-stability-decision.md, and -# .agents/reconcile/promotion-decision.md — committed inputs to -# scripts/check-thesis-stability.sh and the Wave 1E gate-flip workflow, -# not runtime state). These are ~10 KB/day of audit data that nightly + -# evolve + reconcile cite to avoid re-doing work. -# -# Anything else under .agents/ stays untracked. Changes to the allowlist -# require a coordinated update of .gitignore (which carries the matching -# negation patterns) and CLAUDE.md / PROGRAM.md guidance. - -ALLOWED_PATHS_REGEX='^\.agents/(nightly/|evolve/cycle-history\.jsonl$|evolve/session-state\.json$|evolve/preferences\.yaml\.template$|goals/[^/]+/attempts\.jsonl$|findings/registry\.jsonl$|rpi/next-work\.jsonl$|reconcile/wave-0-thesis-snapshot\.md$|reconcile/thesis-stability-decision\.md$|reconcile/promotion-decision\.md$)' - -ALLOWED_REINCLUDES_REGEX='^[[:space:]]*!/?\.agents/?[[:space:]]*$|^[[:space:]]*!/?\.agents/(rpi/?|rpi/next-work\.jsonl|nightly/?|nightly/\*\*|evolve/?|evolve/cycle-history\.jsonl|evolve/session-state\.json|evolve/preferences\.yaml\.template|goals/?|goals/\*\*/?|goals/\*\*/attempts\.jsonl|findings/?|findings/registry\.jsonl|reconcile/?|reconcile/wave-0-thesis-snapshot\.md|reconcile/thesis-stability-decision\.md|reconcile/promotion-decision\.md)[[:space:]]*$' - if [[ -n "${NO_TRACKED_AGENTS_REPO_ROOT:-}" ]]; then REPO_ROOT="$(cd "$NO_TRACKED_AGENTS_REPO_ROOT" && pwd)" else REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" fi -filter_disallowed() { - # Drop allowlisted paths. Treat blank input as no findings. - local input="$1" - [[ -n "$input" ]] || return 0 - printf '%s\n' "$input" | grep -Ev "$ALLOWED_PATHS_REGEX" || true -} - -tracked_all="$(git -C "$REPO_ROOT" ls-files -- .agents 2>/dev/null || true)" -staged_all="$(git -C "$REPO_ROOT" diff --cached --name-only --diff-filter=ACMR -- .agents 2>/dev/null || true)" -tracked="$(filter_disallowed "$tracked_all")" -staged="$(filter_disallowed "$staged_all")" errors=0 +tracked_all="$(git -C "$REPO_ROOT" ls-files -- .agents 2>/dev/null || true)" +deleted="$( + git -C "$REPO_ROOT" diff --name-only --diff-filter=D HEAD -- .agents 2>/dev/null + git -C "$REPO_ROOT" diff --cached --name-only --diff-filter=D -- .agents 2>/dev/null +)" +tracked="$(comm -23 \ + <(printf '%s\n' "$tracked_all" | sed '/^$/d' | sort -u) \ + <(printf '%s\n' "$deleted" | sed '/^$/d' | sort -u))" +staged="$(git -C "$REPO_ROOT" diff --cached --name-only --diff-filter=ACMR -- .agents 2>/dev/null || true)" if [[ -n "$tracked" ]]; then - echo "ERROR: repo-root .agents paths are tracked outside the audit-truth allowlist." >&2 - echo "Remove them from the index, or extend the allowlist if they truly compound across runs:" >&2 - echo " git rm -r --cached <path>" >&2 - echo "$tracked" | sed 's/^/ - /' >&2 + echo "ERROR: repo-root .agents paths are tracked:" >&2 + printf '%s\n' "$tracked" | sed 's/^/ - /' >&2 errors=1 fi if [[ -n "$staged" ]]; then - echo "ERROR: repo-root .agents paths are staged outside the audit-truth allowlist." >&2 - echo "These files look like local agent runtime state, not audit truth:" >&2 - echo "$staged" | sed 's/^/ - /' >&2 + echo "ERROR: repo-root .agents paths are staged:" >&2 + printf '%s\n' "$staged" | sed 's/^/ - /' >&2 errors=1 fi -if [[ ! -f "$REPO_ROOT/.gitignore" ]]; then - echo "ERROR: root .gitignore missing; cannot enforce /.agents/ ignore policy." >&2 +if [[ ! -f "$REPO_ROOT/.gitignore" ]] \ + || ! grep -Eq '^[[:space:]]*/\.agents/[[:space:]]*($|#)' "$REPO_ROOT/.gitignore"; then + echo "ERROR: root .gitignore must contain an explicit '/.agents/' rule." >&2 errors=1 -else - if ! grep -Eq '^[[:space:]]*/\.agents/(\*|\*\*/\*)?[[:space:]]*($|#)' "$REPO_ROOT/.gitignore"; then - echo "ERROR: root .gitignore must contain an explicit '/.agents/' (or '/.agents/*') ignore rule." >&2 - errors=1 - fi - # Use `grep -n` so we can show line numbers in diagnostics, then strip the - # "LINENO:" prefix before matching against the allowlist regex (otherwise - # the leading digits prevent the `^` anchor from matching). - reinclude_lines="$(grep -nE '^[[:space:]]*!/?\.agents(/|$)' "$REPO_ROOT/.gitignore" || true)" - disallowed_reincludes="" - if [[ -n "$reinclude_lines" ]]; then - while IFS= read -r line; do - content="${line#*:}" - if [[ ! "$content" =~ $ALLOWED_REINCLUDES_REGEX ]]; then - disallowed_reincludes+="${line}"$'\n' - fi - done <<<"$reinclude_lines" - fi - if [[ -n "$disallowed_reincludes" ]]; then - echo "ERROR: root .gitignore re-includes repo-root .agents paths outside the audit-truth allowlist:" >&2 - printf '%s' "$disallowed_reincludes" | sed 's/^/ /' >&2 - errors=1 - fi fi -if [[ "$errors" -ne 0 ]]; then - exit 1 +if grep -nE '^[[:space:]]*!/?\.agents(/|$)' "$REPO_ROOT/.gitignore" >&2; then + echo "ERROR: root .gitignore must not re-include repo-root .agents paths." >&2 + errors=1 fi -echo "no disallowed tracked repo-root .agents state" +[[ "$errors" -eq 0 ]] || exit 1 +echo "no tracked repo-root .agents state" diff --git a/scripts/check-orchestration-skill-boundaries.sh b/scripts/check-orchestration-skill-boundaries.sh index 27a6dd626..9360bfc73 100755 --- a/scripts/check-orchestration-skill-boundaries.sh +++ b/scripts/check-orchestration-skill-boundaries.sh @@ -7,7 +7,7 @@ for retired in using-atm pre-land-refuters; do [ ! -f "skills/$retired/SKILL.md" ] || { echo "retired skill still active: $retired" >&2; exit 1; } done -active=(skills/ntm/SKILL.md skills/agent-mail/SKILL.md skills/agent-native/SKILL.md skills/automation-shape-routing/SKILL.md skills/swarm/SKILL.md skills/crank/SKILL.md skills/using-gc/SKILL.md) +active=(skills/ntm/SKILL.md skills/agent-mail/SKILL.md skills/agent-native/SKILL.md skills/automation-shape-routing/SKILL.md skills/swarm/SKILL.md skills/using-gc/SKILL.md) if rg -n -i '\bATM\b|using-atm|vibing-with-ntm' "${active[@]}"; then echo "ATM-era naming remains in canonical orchestration contracts" >&2 exit 1 diff --git a/scripts/check-registry-drift.sh b/scripts/check-registry-drift.sh deleted file mode 100755 index 1cf906143..000000000 --- a/scripts/check-registry-drift.sh +++ /dev/null @@ -1,245 +0,0 @@ -#!/usr/bin/env bash -# scripts/check-registry-drift.sh -# -# Detect drift between skills/ (source of truth) and the hand-edited DDD/hex -# registry docs. Encodes the registries-drift lesson (2026-05-17): hand- -# maintained inventory docs drift silently against the catalog they describe. -# -# Checks: -# 1. Skill count in docs/reference/agentops-skill-domain-map.md narrative -# and audit table matches `find skills -maxdepth 1 -type d` count. -# 2. Skill count in docs/reference/agentops-domain-evolution-bdd.md Gherkin -# ("contains N skills") matches actual. -# 3. Every skill listed in the Full Skill Map table exists in skills/. -# 4. Every skill in skills/ is listed in the Full Skill Map table. -# 5. hexagonal_role column in the doc matches each SKILL.md frontmatter -# `hexagonal_role:` field. -# -# Exit codes: -# 0 = no drift -# 1 = drift detected (or --fix-counts updated files; rerun to confirm) -# 2 = usage error / missing inputs -# -# Modes: -# --check (default) report drift to stdout, non-zero on any -# --fix-counts update narrative count tokens in-place where unambiguous -# --json emit machine-readable JSON report instead of human prose -# -# Schema reference: schemas/skill-frontmatter.v2.schema.json -# Lesson: .agents/learnings/2026-05-17-registries-drift.md -# Contract: docs/reference/agentops-skill-domain-map.md - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" -SKILLS_DIR="${REPO_ROOT}/skills" -MAP_DOC="${REPO_ROOT}/docs/reference/agentops-skill-domain-map.md" -BDD_DOC="${REPO_ROOT}/docs/reference/agentops-domain-evolution-bdd.md" - -MODE="check" -JSON_OUT=0 -for arg in "$@"; do - case "$arg" in - --fix-counts) MODE="fix-counts" ;; - --check) MODE="check" ;; - --json) JSON_OUT=1 ;; - -h|--help) - sed -n '2,32p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//' - exit 0 - ;; - *) - echo "ERROR: unknown arg: $arg (try --help)" >&2 - exit 2 - ;; - esac -done - -if [[ ! -d "${SKILLS_DIR}" ]]; then - echo "ERROR: skills/ not found at ${SKILLS_DIR}" >&2 - exit 2 -fi -for f in "${MAP_DOC}" "${BDD_DOC}"; do - if [[ ! -f "$f" ]]; then - echo "ERROR: registry doc missing: $f" >&2 - exit 2 - fi -done - -export SKILLS_DIR MAP_DOC BDD_DOC MODE JSON_OUT - -exec python3 - <<'PY' -import json -import os -import re -import sys -from pathlib import Path - -try: - import yaml -except ImportError: - print("ERROR: PyYAML not installed; install with: pip install pyyaml", file=sys.stderr) - sys.exit(2) - -SKILLS_DIR = Path(os.environ["SKILLS_DIR"]) -MAP_DOC = Path(os.environ["MAP_DOC"]) -BDD_DOC = Path(os.environ["BDD_DOC"]) -MODE = os.environ.get("MODE", "check") -JSON_OUT = os.environ.get("JSON_OUT") == "1" - -findings = [] # list of dicts: {severity, code, msg, suggest?} - - -def add(severity, code, msg, suggest=None): - f = {"severity": severity, "code": code, "msg": msg} - if suggest: - f["suggest"] = suggest - findings.append(f) - - -# ---- Source of truth: actual skills/ contents ---- -actual_skills = sorted( - p.name for p in SKILLS_DIR.iterdir() - if p.is_dir() - and p.name not in {"pre-mortem", "post-mortem", "pre_mortem", "post_mortem"} - and (p / "SKILL.md").is_file() -) -actual_count = len(actual_skills) - -fm_role = {} -for name in actual_skills: - skill_md = SKILLS_DIR / name / "SKILL.md" - text = skill_md.read_text() - m = re.match(r'---\n(.*?)\n---', text, re.DOTALL) - if not m: - add("warn", "SKILL_NO_FRONTMATTER", - f"skills/{name}/SKILL.md has no YAML frontmatter; skipping hex-role check") - continue - try: - fm = yaml.safe_load(m.group(1)) or {} - except yaml.YAMLError as e: - add("warn", "SKILL_FRONTMATTER_BAD", - f"skills/{name}/SKILL.md frontmatter parse failed: {e}") - continue - fm_role[name] = fm.get("hexagonal_role") - - -# ---- Parse declared counts in registry docs ---- -map_text = MAP_DOC.read_text() -bdd_text = BDD_DOC.read_text() - -# Map narrative count: e.g. "It classifies all 78 checked-in AgentOps skills" -map_narr_match = re.search(r'(\d+)\s+checked-in AgentOps skills', map_text) -map_narr_count = int(map_narr_match.group(1)) if map_narr_match else None - -# Map audit table: "| Skills audited | 78 |" -map_audit_match = re.search(r'\|\s*Skills audited\s*\|\s*(\d+)\s*\|', map_text) -map_audit_count = int(map_audit_match.group(1)) if map_audit_match else None - -# BDD Gherkin: "Given the checked-in skill catalog contains 77 skills" -bdd_match = re.search(r'checked-in skill catalog contains\s+(\d+)\s+skills', bdd_text) -bdd_count = int(bdd_match.group(1)) if bdd_match else None - - -def check_count(label, declared, doc_path, pattern_for_fix): - """Compare declared count to actual; in fix-counts mode, rewrite.""" - if declared is None: - add("warn", "COUNT_NOT_FOUND", - f"could not find declared count in {doc_path.name} ({label}); pattern may have drifted") - return False - if declared == actual_count: - return True - add("fail", "COUNT_DRIFT", - f"{doc_path.name} ({label}): declared {declared} skills, actual {actual_count}", - f"sed -i 's/{declared}\\([^0-9]\\)/{actual_count}\\1/g' {doc_path} # manual review required") - if MODE == "fix-counts" and pattern_for_fix: - new_text = doc_path.read_text() - # pattern_for_fix is a regex with one numeric capture group - new_text, n = re.subn(pattern_for_fix, - lambda m: m.group(0).replace(m.group(1), str(actual_count)), - new_text) - if n == 1: - doc_path.write_text(new_text) - add("info", "COUNT_FIXED", - f"{doc_path.name} ({label}): {declared} -> {actual_count}") - else: - add("warn", "COUNT_FIX_AMBIGUOUS", - f"{doc_path.name} ({label}): expected exactly 1 match for fix, got {n}; left alone") - return False - - -check_count("narrative", map_narr_count, MAP_DOC, r'(\d+)\s+checked-in AgentOps skills') -check_count("audit table", map_audit_count, MAP_DOC, r'\|\s*Skills audited\s*\|\s*(\d+)\s*\|') -check_count("Gherkin", bdd_count, BDD_DOC, r'checked-in skill catalog contains\s+(\d+)\s+skills') - - -# ---- Parse Full Skill Map rows ---- -# Match: | `skill-name` | BC? ... | hex-role | disposition | rationale | -row_re = re.compile(r'^\|\s*`([a-z][a-z0-9-]*)`\s*\|[^|]+\|\s*([a-z-]+)\s*\|', re.MULTILINE) -map_rows = {m.group(1): m.group(2) for m in row_re.finditer(map_text)} -map_skills = set(map_rows.keys()) - -missing_from_doc = sorted(set(actual_skills) - map_skills) -extra_in_doc = sorted(map_skills - set(actual_skills)) - -for sk in missing_from_doc: - add("fail", "MAP_MISSING_SKILL", - f"skills/{sk}/ exists but is not in {MAP_DOC.name} Full Skill Map", - f"Add a row to {MAP_DOC.name} classifying `{sk}`") -for sk in extra_in_doc: - add("fail", "MAP_EXTRA_SKILL", - f"{MAP_DOC.name} lists `{sk}` but skills/{sk}/ does not exist", - f"Remove `{sk}` row from {MAP_DOC.name} or restore skills/{sk}/") - - -# ---- hexagonal_role per-skill consistency ---- -for sk in sorted(set(actual_skills) & map_skills): - fr = fm_role.get(sk) - dr = map_rows.get(sk) - if fr is None: - add("warn", "FRONTMATTER_NO_HEX_ROLE", - f"skills/{sk}/SKILL.md has no `hexagonal_role` field; map declares `{dr}`", - f"Either add `hexagonal_role: {dr}` to frontmatter or remove from map if the skill is being deprecated") - elif fr != dr: - add("fail", "HEX_ROLE_DRIFT", - f"`{sk}`: frontmatter hexagonal_role={fr}, map column={dr}", - f"Pick one as truth; update the other") - - -# ---- Emit report ---- -fails = [f for f in findings if f["severity"] == "fail"] -warns = [f for f in findings if f["severity"] == "warn"] -infos = [f for f in findings if f["severity"] == "info"] - -if JSON_OUT: - print(json.dumps({ - "actual_count": actual_count, - "declared_map_narrative": map_narr_count, - "declared_map_audit": map_audit_count, - "declared_bdd": bdd_count, - "missing_from_doc": missing_from_doc, - "extra_in_doc": extra_in_doc, - "findings": findings, - "verdict": "FAIL" if fails else ("WARN" if warns else "PASS"), - }, indent=2)) -else: - print(f"Registry drift check: {actual_count} skills in skills/") - print(f" declared in map narrative: {map_narr_count}") - print(f" declared in map audit: {map_audit_count}") - print(f" declared in BDD Gherkin: {bdd_count}") - print() - for f in findings: - tag = {"fail": "FAIL", "warn": "WARN", "info": "INFO"}[f["severity"]] - print(f"[{tag}] {f['code']}: {f['msg']}") - if "suggest" in f: - print(f" suggest: {f['suggest']}") - print() - if not findings: - print("PASS — no drift.") - elif fails: - print(f"FAIL — {len(fails)} drift finding(s) (warns: {len(warns)}, infos: {len(infos)})") - else: - print(f"WARN — {len(warns)} warning(s) (infos: {len(infos)})") - -sys.exit(1 if fails else 0) -PY diff --git a/scripts/check-shell-portability.sh b/scripts/check-shell-portability.sh index c0385e6a5..b07792bc5 100755 --- a/scripts/check-shell-portability.sh +++ b/scripts/check-shell-portability.sh @@ -14,7 +14,7 @@ # it enumerates `git ls-files` (tracked only — so worktree copies, vendored, and # gitignored trees are excluded for free) and selects shell scripts by EITHER a # .sh/.bash suffix OR a shell shebang (so extensionless hooks like -# scripts/hooks/pre-push.local, .githooks/*, bin/* are covered). tests/ is +# .githooks/* and bin/* are covered). tests/ is # excluded: test fixtures legitimately embed the pattern as data. # # Precision: the token `-printf` (with a LEADING dash) is only ever a `find` diff --git a/scripts/check-skill-catalog-drift.sh b/scripts/check-skill-catalog-drift.sh deleted file mode 100755 index 3b8526e92..000000000 --- a/scripts/check-skill-catalog-drift.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env bash -# check-skill-catalog-drift.sh — CI gate that fails if skills/catalog.json -# is out of sync with skills/*/SKILL.md frontmatter. -# -# Thin wrapper over generate-skill-catalog.sh --check so the workflow has a -# stable, named entry point and any future drift checks (catalog vs schema, -# catalog vs codex parity) can be added here without changing the workflow. -# -# Exit codes: -# 0 — catalog up-to-date -# 1 — drift detected -# 2 — wrapper or upstream tool error - -set -euo pipefail - -ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" -GEN="$ROOT/scripts/generate-skill-catalog.sh" - -if [ ! -x "$GEN" ]; then - echo "check-skill-catalog-drift: $GEN missing or not executable" >&2 - exit 2 -fi - -"$GEN" --check diff --git a/scripts/check-skill-isolation.sh b/scripts/check-skill-isolation.sh deleted file mode 100755 index edfc1b237..000000000 --- a/scripts/check-skill-isolation.sh +++ /dev/null @@ -1,374 +0,0 @@ -#!/bin/bash -# -# check-skill-isolation.sh -# -# Lint SKILL.md files for compression patterns that violate the -# phase-isolation contract declared in PRODUCT.md operational principle #5 -# and documented at skills/rpi/references/isolation-contract.md. -# -# Compression patterns flagged: -# 1. Cross-phase first-person verbs: -# "I will research|plan|crank|validate" -# 2. Inline research vocabulary near phase context: -# "let me grep|read|search" / "I'll grep|read|search" -# 3. A phase SKILL.md calling another phase skill it should not orchestrate. -# Per-file allowlist: -# rpi/SKILL.md may call: discovery, crank, validate -# discovery/SKILL.md may call: research, plan -# crank/SKILL.md may NOT call: research, plan, crank, validate -# validate/SKILL.md may NOT call: research, plan, crank, validate -# -# Inventory-only lanes: -# - flywheel, gold, wiki, and BC1 corpus skills are reported but never counted -# as failures. See bead ag-skill-isolation-ci-gate-jxpbx. -# -# False-positive guard: -# - Lines beginning with `See [` (markdown reference links) -# - Lines beginning with `Read <path>` (reference doc reads) -# - Lines inside fenced code blocks (``` ... ```) -# -# Usage: -# check-skill-isolation.sh # lint default tree -# check-skill-isolation.sh <path> # lint a different skills/ tree -# check-skill-isolation.sh -q # quiet mode, exit code only -# check-skill-isolation.sh --self-test # internal regression check -# -# Exit codes: -# 0 = clean (no enforcing compression patterns matched) -# 1 = at least one enforcing compression pattern matched -# 2 = script error (bad invocation, missing files) - -set -uo pipefail - -QUIET=0 -SELF_TEST=0 -TARGET_PATH="" - -for arg in "$@"; do - case "$arg" in - -q|--quiet) - QUIET=1 - ;; - --self-test) - SELF_TEST=1 - ;; - -h|--help) - sed -n '2,/^$/p' "$0" | sed 's/^# \{0,1\}//' - exit 0 - ;; - -*) - echo "check-skill-isolation: unknown flag: $arg" >&2 - exit 2 - ;; - *) - if [[ -z "$TARGET_PATH" ]]; then - TARGET_PATH="$arg" - else - echo "check-skill-isolation: unexpected extra argument: $arg" >&2 - exit 2 - fi - ;; - esac -done - -REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -DEFAULT_ROOT="${REPO_ROOT}/skills" - -emit() { - if [[ $QUIET -eq 0 ]]; then - echo "$@" >&2 - fi -} - -# Resolve skill SKILL.md files under a given root. -# Echoes one path per line for files that exist. -resolve_skill_files() { - local root="$1" - local f - for f in "$root"/*/SKILL.md; do - if [[ -f "$f" ]]; then - echo "$f" - fi - done -} - -is_phase_skill() { - local owner="$1" - case "$owner" in - rpi|discovery|crank|validate|validation) return 0 ;; - *) return 1 ;; - esac -} - -is_inventory_only_skill() { - local owner="$1" - case "$owner" in - # Explicit Mossy Lantern lanes named in ag-skill-isolation-ci-gate-jxpbx. - flywheel|gold|wiki|corpus|*flywheel*|*gold*|*wiki*|*corpus*) - return 0 - ;; - # BC1 Corpus skills from docs/reference/agentops-skill-domain-map.md. - cass|compile|curate|forge|handoff|inject|operationalize|recover|research|toil-mining) - return 0 - ;; - *) - return 1 - ;; - esac -} - -# Per-file Skill() callsite check. -# Returns 0 if the line is an allowed callsite for this file, 1 if it's a violation. -# $1 = basename-of-parent-dir (rpi|discovery|crank|validate|validation) -# $2 = sub-skill name captured from the line (research|plan|crank|validation) -is_skill_call_allowed() { - local owner="$1" - local target="$2" - case "$owner" in - rpi) - # rpi orchestrates discovery, crank, validate. - # research/plan are discovery's sub-skills, not rpi's — flag those. - case "$target" in - discovery|crank|validate|validation) return 0 ;; - *) return 1 ;; - esac - ;; - discovery) - # discovery orchestrates research and plan. - case "$target" in - research|plan) return 0 ;; - *) return 1 ;; - esac - ;; - crank|validate|validation) - # phase 2 and phase 3 are sealed — they should not call any of the watched phase skills. - return 1 - ;; - *) - return 1 - ;; - esac -} - -# Lint a single SKILL.md file. -# Emits tab-separated matches on stdout: -# file<TAB>lineno<TAB>kind<TAB>extra -lint_file() { - local file="$1" - local owner - owner="$(basename "$(dirname "$file")")" - - awk -v file="$file" -v owner="$owner" ' - BEGIN { - in_fence = 0 - } - # Toggle fenced-code state. A line whose first non-space chars are ``` flips state. - { - line = $0 - stripped = line - sub(/^[ \t]*/, "", stripped) - if (substr(stripped, 1, 3) == "```") { - in_fence = 1 - in_fence - next - } - } - # Skip lines inside fenced code blocks. - in_fence == 1 { next } - # False-positive guard: markdown reference link lines. - /^See \[/ { next } - # False-positive guard: reference-doc read instructions. - /^Read [^[:space:]]+/ { next } - # Pattern 1: cross-phase first-person verbs. - { - if (match(tolower(line), /i will (research|plan|crank|validate)/)) { - printf("%s\t%d\tcross-phase-verb\t%s\n", file, NR, line) - } - } - # Pattern 2: inline research vocabulary. - { - lc = tolower(line) - if (match(lc, /let me (grep|read|search)/) || - match(lc, /i.ll (grep|read|search)/)) { - printf("%s\t%d\tinline-research\t%s\n", file, NR, line) - } - } - # Pattern 3: phase-skill calling another phase skill. - # Capture the target sub-skill name and let the caller validate the allowlist. - { - if (match(line, /Skill\(skill="(discovery|research|plan|crank|validate|validation)"/)) { - skill_call = substr(line, RSTART, RLENGTH) - sub(/^Skill\(skill="/, "", skill_call) - sub(/"$/, "", skill_call) - printf("%s\t%d\tskill-call\t%s\n", file, NR, skill_call) - } - } - ' "$file" -} - -emit_match() { - local hit_file="$1" - local hit_lineno="$2" - local kind="$3" - local extra="$4" - local prefix="${5:-}" - - case "$kind" in - cross-phase-verb) - emit "$hit_file:$hit_lineno:${prefix}cross-phase first-person verb: $extra" - ;; - inline-research) - emit "$hit_file:$hit_lineno:${prefix}inline research vocabulary: $extra" - ;; - skill-call) - emit "$hit_file:$hit_lineno:${prefix}phase-skill calling another phase skill (target=$extra)" - ;; - esac -} - -run_lint() { - local root="$1" - local violations=0 - local inventory_only_hits=0 - - if [[ ! -d "$root" ]]; then - echo "check-skill-isolation: target path is not a directory: $root" >&2 - return 2 - fi - - local file - local found_files=0 - while IFS= read -r file; do - found_files=1 - local raw - if ! raw="$(lint_file "$file")"; then - emit "check-skill-isolation: script error while linting $file" - return 2 - fi - if [[ -z "$raw" ]]; then - continue - fi - - # Each output line is tab-separated: - # file<TAB>lineno<TAB>kind<TAB>extra - # kind in {cross-phase-verb, inline-research, skill-call} - local line - while IFS= read -r line; do - [[ -z "$line" ]] && continue - - local hit_file hit_lineno kind extra rest - IFS=$'\t' read -r hit_file hit_lineno kind extra rest <<< "$line" - if [[ -n "${rest:-}" ]]; then - extra="${extra}"$'\t'"${rest}" - fi - local owner - owner="$(basename "$(dirname "$hit_file")")" - - if is_inventory_only_skill "$owner"; then - emit_match "$hit_file" "$hit_lineno" "$kind" "$extra" "inventory-only: " - inventory_only_hits=$((inventory_only_hits + 1)) - continue - fi - - case "$kind" in - cross-phase-verb|inline-research) - emit_match "$hit_file" "$hit_lineno" "$kind" "$extra" - violations=$((violations + 1)) - ;; - skill-call) - if ! is_phase_skill "$owner"; then - # Non-phase skills may mention phase Skill(...) callsites - # without violating this narrow phase-isolation check. - : - elif is_skill_call_allowed "$owner" "$extra"; then - # Legitimate orchestration callsite — no violation. - : - else - emit_match "$hit_file" "$hit_lineno" "$kind" "$extra" - violations=$((violations + 1)) - fi - ;; - esac - done <<< "$raw" - done < <(resolve_skill_files "$root") - - if [[ $found_files -eq 0 ]]; then - # No SKILL.md files under this root. Nothing to lint. - # This is not an error — callers may pass a tree intended to test - # specific files only. Emit a debug note and return clean. - emit "check-skill-isolation: no SKILL.md files found under $root" - return 0 - fi - - if [[ $violations -gt 0 ]]; then - emit "" - emit "check-skill-isolation: FAIL ($violations enforcing compression pattern(s) found; $inventory_only_hits inventory-only hit(s))" - emit "" - emit "See skills/rpi/references/isolation-contract.md for the rules." - return 1 - fi - - if [[ $QUIET -eq 0 ]]; then - if [[ $inventory_only_hits -gt 0 ]]; then - echo "check-skill-isolation: PASS (no enforcing compression patterns; $inventory_only_hits inventory-only hit(s) under $root)" - else - echo "check-skill-isolation: PASS (no compression patterns in SKILL.md files under $root)" - fi - fi - return 0 -} - -SELF_TEST_TMP="" -self_test_cleanup() { - if [[ -n "${SELF_TEST_TMP:-}" && -d "${SELF_TEST_TMP:-}" ]]; then - rm -rf "$SELF_TEST_TMP" - fi -} - -self_test() { - # Build a tmpdir mimicking skills/<phase>/SKILL.md, inject known violations, - # run the lint, and assert the lint failure exit specifically. Exit 2 is a - # script/dialect error and must not count as a passing self-test. - SELF_TEST_TMP="$(mktemp -d)" - trap self_test_cleanup EXIT - - mkdir -p "$SELF_TEST_TMP/crank" "$SELF_TEST_TMP/discovery" - cat > "$SELF_TEST_TMP/crank/SKILL.md" <<'EOF' ---- -name: crank ---- -# /crank - -Skill(skill="research", args="inline scope") -EOF - - cat > "$SELF_TEST_TMP/discovery/SKILL.md" <<'EOF' ---- -name: discovery ---- -# /discovery - -I will research the codebase before doing anything else. -EOF - - "$0" --quiet "$SELF_TEST_TMP" - local rc=$? - if [[ $rc -ne 1 ]]; then - echo "check-skill-isolation: SELF-TEST FAILED — expected lint exit 1 for injected violations, got $rc" >&2 - return 1 - fi - - echo "check-skill-isolation: self-test PASS" - return 0 -} - -if [[ $SELF_TEST -eq 1 ]]; then - self_test - exit $? -fi - -if [[ -z "$TARGET_PATH" ]]; then - TARGET_PATH="$DEFAULT_ROOT" -fi - -run_lint "$TARGET_PATH" -exit $? diff --git a/scripts/check-slice-batch-size.sh b/scripts/check-slice-batch-size.sh deleted file mode 100755 index 459d0a1a4..000000000 --- a/scripts/check-slice-batch-size.sh +++ /dev/null @@ -1,271 +0,0 @@ -#!/usr/bin/env bash -# check-slice-batch-size.sh — small-batch-by-Gherkin ENFORCEMENT (age-74yi). -# -# The enforcement half of the flywheel discipline -# (docs/architecture/the-flywheel.md). Behavior-first planning -# (skills/behavior-first-planning) SAYS "seed slices small — one behavior, one -# scenario per slice; no runnable acceptance test, no bead" but nothing -# mechanically FAILS on a multi-behavior slice. This makes the batch unit -# COUNTABLE: one slice bead == one behavior == one Gherkin scenario. -# -# It reads the bead body via `ao beads exec show <id> --json` (tracker-agnostic — -# resolves to `br` or `bd`, whichever this repo/substrate uses), counts the -# Gherkin scenarios in the body (the `description` field), and: -# -# >1 scenario → FAIL (exit 1): the slice batches multiple behaviors. Prints -# "SLICE-BATCH: FAIL — <id> has N behaviors (N Gherkin -# scenarios); split into N one-behavior slices" + the detected -# scenario names, so the operator knows where to cut. -# exactly 1 → PASS (exit 0): "SLICE-BATCH: PASS". -# 0 scenarios → WARN (exit 0): "SLICE-BATCH: WARN — no Gherkin scenario …". -# -# 0-scenario decision — WARN, not FAIL (deliberate): -# A plain task bead with prose-only acceptance and no Given/When/Then block is -# still common in the tracker today. This gate is a NEW discipline; hard- -# failing every scenario-less bead would break the existing corpus en masse. -# So it is introduced as invocable + documented FIRST: a scenario-less slice is -# advised (WARN) to carry exactly one scenario, never hard-blocked. The -# companion "no runnable acceptance test, no bead" admission (the --admission -# mode of scripts/check-bead-scenario-coverage.sh) is where a bead is REQUIRED -# to carry ≥1 structurally-complete scenario; this gate is strictly about batch -# SIZE (one behavior, not many). -# -# Scenario-counting rule (a "behavior" = one Given…When…Then triad): -# * A `Scenario:` / `Scenario Outline:` header always counts as one behavior; -# the GWT lines that follow belong to it (they do NOT each start a new one). -# A Scenario unit stays open until the next `Scenario:`/`## ` heading or EOF — -# a blank line inside it is body formatting, not a boundary. -# * When there are NO `Scenario:` headers, a contiguous run of line-start -# Given/When/Then/And/But lines (a "bare-GWT stanza") that contains at least -# one `Given` counts as one behavior. A blank or non-GWT line ends the stanza. -# * Fenced (```) content is parse-inert — Scenario:/GWT text inside a code fence -# never counts (bead bodies carry fenced yaml acceptance blocks). -# * Matching is line-start after trimming, so mid-sentence prose like -# "one happy-path Given/When/Then" never miscounts. -# This mirrors the proven unit model in check-bead-scenario-coverage.sh -# --admission, minus the `## Scenarios`-heading requirement (bead bodies are -# often prose with an embedded GHERKIN block). -# -# WIRING (light, by design — NOT a blocking hook): -# This gate is invocable + documented first, NOT wired into a blocking release -# gate (that would fail existing multi-scenario beads en masse). It belongs at -# two moments in the operating loop: -# * discovery / plan (skills/behavior-first-planning, skills/plan): validate a -# slice BEFORE it becomes a bead — reject a multi-scenario slice, split it. -# * crank (skills/crank): re-check a slice that GREW during build — surfaced -# extra behavior becomes a follow-up bead, never absorbed into this slice. -# Referenced from skills/behavior-first-planning/SKILL.md. Promote to a -# blocking gate only after the tracker's existing slices are one-behavior clean. -# -# Usage: -# bash scripts/check-slice-batch-size.sh <bead-id> # check one slice -# bash scripts/check-slice-batch-size.sh --json <bead-id> # machine-readable -# bash scripts/check-slice-batch-size.sh --all-ready # every ready slice -# -# Exits 0 on PASS/WARN, 1 on FAIL (>1 behavior), 2 on misuse/infra failure. -# -# practices: [continuous-integration, design-by-contract, bdd-gherkin, small-batches] -set -euo pipefail - -JSON=0 -ALL_READY=0 -BEAD_ID="" - -usage() { grep '^#' "$0" | sed 's/^# \?//'; } - -while [[ $# -gt 0 ]]; do - case "$1" in - --json) JSON=1; shift;; - --all-ready) ALL_READY=1; shift;; - -h|--help) usage; exit 0;; - --*) echo "Unknown flag: $1" >&2; exit 2;; - *) BEAD_ID="$1"; shift;; - esac -done - -if ! command -v ao >/dev/null 2>&1; then - echo "ao is not on PATH — cannot read the bead body (tracker-agnostic read needs 'ao beads exec show')" >&2 - exit 2 -fi - -# count_scenarios <body-text> -# Prints, on stdout: "<count>" on line 1, then one scenario name per subsequent -# line. Implements the counting rule documented in the header. -count_scenarios() { - printf '%s\n' "$1" | awk ' - function flush() { - if (unit_open == 0) return - if (unit_kind == "scenario") { names[++count] = unit_name } - else if (unit_kind == "stanza" && unit_given >= 1) { names[++count] = unit_name } - unit_open = 0; unit_kind = ""; unit_name = ""; unit_given = 0 - } - BEGIN { in_fence = 0; count = 0; unit_open = 0 } - { - line = $0 - sub(/^[[:space:]]+/, "", line) # left-trim - sub(/[[:space:]]+$/, "", line) # right-trim - - # Fence tracking: fenced lines are parse-inert. - if (line ~ /^```/) { in_fence = !in_fence; next } - if (in_fence) { next } - - # Scenario:/Scenario Outline: header — always a behavior. - if (line ~ /^Scenario( Outline)?:/) { - flush() - unit_open = 1; unit_kind = "scenario"; unit_given = 0 - nm = line - sub(/^Scenario( Outline)?:[[:space:]]*/, "", nm) - unit_name = nm - next - } - - # Background: — Gherkin SHARED SETUP, not a behavior. Open an inert - # unit so its Given/When/Then steps are absorbed (never counted as a - # bare stanza). flush() never counts a "background" unit; a later - # Scenario:/## flushes it away uncounted. - if (line ~ /^Background:/) { - flush() - unit_open = 1; unit_kind = "background"; unit_given = 0; unit_name = "" - next - } - - # A new H2 section (other than a Scenario) ends the open unit. - if (line ~ /^##[[:space:]]/) { flush(); next } - - # Line-start Given/When/Then/And/But — a Gherkin step line. The word - # boundary ([^A-Za-z0-9_]|$) prevents matching "Givenness" etc. - if (line ~ /^(Given|When|Then|And|But)([^A-Za-z0-9_]|$)/) { - if (unit_open == 0) { - unit_open = 1; unit_kind = "stanza"; unit_given = 0 - unit_name = "bare Given/When/Then block " (count + 1) - } - if (line ~ /^Given([^A-Za-z0-9_]|$)/) { unit_given++ } - next - } - - # Blank line: splits a bare stanza; a Scenario unit survives it. - if (line == "") { - if (unit_open == 1 && unit_kind == "stanza") flush() - next - } - - # Any other non-blank line: ends a bare stanza; inert inside a - # Scenario block (descriptions, tables). - if (unit_open == 1 && unit_kind == "stanza") flush() - } - END { flush(); print count; for (i = 1; i <= count; i++) print names[i] } - ' -} - -# check_one <bead-id> — emits a verdict for one bead. Returns 0 (pass/warn) or 1 -# (fail). With JSON=1, emits a one-line JSON object instead of prose. -check_one() { - local id="$1" - local raw - raw="$(ao beads exec show "$id" --json 2>/dev/null || true)" - if [[ -z "${raw//[[:space:]]/}" ]]; then - echo "check-slice-batch-size: 'ao beads exec show $id --json' returned no content (tracker failure?)" >&2 - return 2 - fi - - # Extract the body (description). The passthrough emits a JSON array; also - # tolerate a bare object. jq handles all unescaping. - # A jq PARSE failure on non-empty tracker output means unreadable/malformed - # JSON (a real infra failure) — NOT an empty description. Fail closed (exit 2); - # never swallow it into a WARN, or the enforcement gate fails open on garbage. - local body - if ! body="$(printf '%s' "$raw" | jq -r '(if type=="array" then .[0] else . end) | (.description // "")' 2>/dev/null)"; then - echo "check-slice-batch-size: could not parse tracker JSON for '$id' (malformed 'ao beads exec show --json' response) — infra failure" >&2 - return 2 - fi - - local out count - out="$(count_scenarios "$body")" - count="$(printf '%s\n' "$out" | head -n1)" - [[ "$count" =~ ^[0-9]+$ ]] || count=0 - local scenarios - scenarios="$(printf '%s\n' "$out" | tail -n +2)" - - local result - if [[ "$count" -gt 1 ]]; then - result="fail" - elif [[ "$count" -eq 1 ]]; then - result="pass" - else - result="warn" - fi - - if [[ $JSON -eq 1 ]]; then - local names_json - # Read the whole scenario list as one raw string, split on newline, drop - # empties → a JSON array. Robust on empty input (yields []); avoids the - # pipefail+grep-returns-1 double-emit of a multi-stage pipeline. - names_json="$(printf '%s' "$scenarios" | jq -R -s -c 'split("\n") | map(select(length > 0))' 2>/dev/null || echo '[]')" - printf '{"bead":"%s","behaviors":%d,"result":"%s","scenarios":%s}\n' \ - "$id" "$count" "$result" "$names_json" - else - case "$result" in - fail) - echo "SLICE-BATCH: FAIL — $id has $count behaviors ($count Gherkin scenarios); split into $count one-behavior slices" - while IFS= read -r nm; do - [[ -z "$nm" ]] && continue - echo " - $nm" - done <<< "$scenarios" - ;; - pass) - echo "SLICE-BATCH: PASS — $id carries exactly one behavior (1 Gherkin scenario)" - ;; - warn) - echo "SLICE-BATCH: WARN — $id has no Gherkin scenario; a slice should carry exactly one (add a Given/When/Then acceptance block). Advisory, not a hard fail." - ;; - esac - fi - - [[ "$result" == "fail" ]] && return 1 - return 0 -} - -if [[ $ALL_READY -eq 1 ]]; then - if [[ -n "$BEAD_ID" ]]; then - echo "--all-ready takes no bead id" >&2 - exit 2 - fi - ready_raw="$(ao beads exec ready --json 2>/dev/null || true)" - if [[ -z "${ready_raw//[[:space:]]/}" ]]; then - echo "check-slice-batch-size: 'ao beads exec ready --json' returned no content" >&2 - exit 2 - fi - # ready output is either {"issues":[...]} (br) or a bare array (bd) — tolerate both. - # A jq PARSE failure means malformed ready output — infra, fail closed (do NOT - # swallow into an empty list and exit 0 over zero beads). - ready_ids_raw="" - if ! ready_ids_raw="$(printf '%s' "$ready_raw" | jq -r '((.issues // .) // [])[].id' 2>/dev/null)"; then - echo "check-slice-batch-size: could not parse 'ao beads exec ready --json' (malformed) — infra failure" >&2 - exit 2 - fi - mapfile -t ready_ids <<< "$ready_ids_raw" - overall=0 - for rid in "${ready_ids[@]:-}"; do - [[ -z "$rid" ]] && continue - rc=0 - check_one "$rid" || rc=$? - # Infra (rc 2, e.g. an unreadable bead) FAILS CLOSED — the sweep could not - # verify "every ready slice", so it must not exit 0. Infra dominates a - # policy FAIL (you must fix the unreadable bead before trusting the sweep). - if [[ $rc -eq 2 ]]; then - overall=2 - elif [[ $rc -eq 1 && $overall -ne 2 ]]; then - overall=1 - fi - done - exit "$overall" -fi - -if [[ -z "$BEAD_ID" ]]; then - echo "usage: check-slice-batch-size.sh <bead id> | --all-ready (no bead id given)" >&2 - exit 2 -fi - -rc=0 -check_one "$BEAD_ID" || rc=$? -exit "$rc" diff --git a/scripts/check-test-fixture-parity.sh b/scripts/check-test-fixture-parity.sh index 1b8fface9..15610b293 100755 --- a/scripts/check-test-fixture-parity.sh +++ b/scripts/check-test-fixture-parity.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# check-test-fixture-parity.sh — pre-push parity gate +# check-test-fixture-parity.sh — deterministic fixture parity check # # Catches the pattern at developer time: # 1. New hooks/<x>.sh added without a matching reference in any diff --git a/scripts/check-thesis-stability.sh b/scripts/check-thesis-stability.sh index 521f8baa8..2459888c7 100755 --- a/scripts/check-thesis-stability.sh +++ b/scripts/check-thesis-stability.sh @@ -12,7 +12,7 @@ # A diff = thesis drift; the operator must consciously accept it (and # re-validate Waves 2-4) before proceeding, OR re-brainstorm Waves 2-4. # -# Hero extraction (per pre-mortem M2): +# Hero extraction (per premortem M2): # awk 'NR==1, /^## / {if (!/^## /) print}' <file> # This anchors to ^## (with the H2 boundary literal) and excludes the # boundary line from the captured hero. Code-fence-safe because it matches diff --git a/scripts/check-workflow-governance.sh b/scripts/check-workflow-governance.sh deleted file mode 100755 index bcca29fce..000000000 --- a/scripts/check-workflow-governance.sh +++ /dev/null @@ -1,125 +0,0 @@ -#!/usr/bin/env bash -# check-workflow-governance.sh — kind-aware workflow drift/governance gate -# (ag-km74w; bidirectional + DDD-identity extension ag-jy8gj). -# -# Asserts a BIDIRECTIONAL identity match between the repo-tracked Claude -# workflows (.claude/workflows/*.js) and the top-level `workflows:` section of -# docs/contracts/skill-dispositions.yaml, plus the DDD identity triple on each -# ledger row. -# -# FORWARD — every workflow .js has a `workflows.<id>` ledger row that carries -# kind: workflow + a Bounded Context (domain) + a hexagonal_role. -# REVERSE — every `workflows.<id>` ledger row (kind: workflow) has a matching -# repo-tracked .js (else the row is STALE and the gate FAILS). -# -# Workflows are Claude-only (ag-jy8gj): they live only in .claude/workflows/ -# with no skills-codex twin, so this gate checks Claude-runtime presence and -# never requires a Codex twin or trips audit-codex-parity. -# -# This is the thin workflow-only sliver of the S0 artifact-dispositions schema: -# it gives Claude Workflow scripts a governed home in the ledger WITHOUT teaching -# the `- skill:` line-parsers (sku_catalog.py / generate-skill-domain-map.sh) to -# miscount them as skills (the `workflows:` mapping is top-level, skipped like -# `historical:`). -# -# Exit 0: the .js set and the kind: workflow ledger rows are in bijection and -# every row carries kind: workflow + domain (BC) + hexagonal_role. -# Exit 1: a workflow .js has no ledger row; a row lacks the identity triple; or a -# kind: workflow row has no matching .js (stale). -# -# Repo root from cwd git. No hardcoded user paths. -set -euo pipefail - -repo_root="$(git rev-parse --show-toplevel)" -disp_yaml="$repo_root/docs/contracts/skill-dispositions.yaml" - -if [ ! -f "$disp_yaml" ]; then - echo "FAIL: ledger not found: $disp_yaml" >&2 - exit 1 -fi - -status=0 - -# For each tracked workflow .js, derive its id from meta.name (authoritative), -# then assert the ledger has a `workflows.<id>` entry with kind: workflow. -while IFS= read -r tracked; do - rel="$tracked" - abs="$repo_root/$tracked" - [ -f "$abs" ] || continue - # id = meta.name literal from the .js (single- or double-quoted). - id="$(grep -oE "name:[[:space:]]*['\"][^'\"]+['\"]" "$abs" | head -1 | sed -E "s/.*['\"]([^'\"]+)['\"].*/\1/")" - if [ -z "$id" ]; then - echo "FAIL: $rel has no parseable meta.name (cannot match to ledger)" >&2 - status=1 - continue - fi - # Verify the ledger has this workflow id with the DDD identity triple: - # kind: workflow + a Bounded Context (domain) + a hexagonal_role. - if ! python3 - "$disp_yaml" "$id" "$rel" <<'PY' -import sys -import yaml -disp_yaml, wf_id, rel = sys.argv[1], sys.argv[2], sys.argv[3] -data = yaml.safe_load(open(disp_yaml, encoding="utf-8")) or {} -workflows = data.get("workflows") or {} -entry = workflows.get(wf_id) -if entry is None: - print(f"FAIL: workflow '{wf_id}' ({rel}) has no `workflows:` ledger entry", file=sys.stderr) - sys.exit(1) -if entry.get("kind") != "workflow": - print(f"FAIL: workflow '{wf_id}' ledger entry kind={entry.get('kind')!r}, expected 'workflow'", file=sys.stderr) - sys.exit(1) -# DDD identity: a Bounded Context (domain) and a hexagonal_role are required. -if not (entry.get("domain") or "").strip(): - print(f"FAIL: workflow '{wf_id}' ({rel}) ledger row has no `domain:` (Bounded Context)", file=sys.stderr) - sys.exit(1) -if not (entry.get("hexagonal_role") or "").strip(): - print(f"FAIL: workflow '{wf_id}' ({rel}) ledger row has no `hexagonal_role:`", file=sys.stderr) - sys.exit(1) -sys.exit(0) -PY - then - status=1 - continue - fi - echo "OK: $id ($rel) registered with kind: workflow + BC + role" -done < <(git -C "$repo_root" ls-files '.claude/workflows/*.js') - -# REVERSE: every `workflows.<id>` ledger row with kind: workflow must have a -# matching repo-tracked .js. A row without one is STALE — fail naming it. -# (The id list is computed once from the .js set, parsing meta.name the same way -# the forward pass does, so a renamed-but-not-removed row is caught.) -present_ids="$( - while IFS= read -r tracked; do - abs="$repo_root/$tracked" - [ -f "$abs" ] || continue - grep -oE "name:[[:space:]]*['\"][^'\"]+['\"]" "$abs" | head -1 | sed -E "s/.*['\"]([^'\"]+)['\"].*/\1/" - done < <(git -C "$repo_root" ls-files '.claude/workflows/*.js') -)" -if ! python3 - "$disp_yaml" "$present_ids" <<'PY' -import sys -import yaml -disp_yaml, present_blob = sys.argv[1], sys.argv[2] -present = {ln.strip() for ln in present_blob.splitlines() if ln.strip()} -data = yaml.safe_load(open(disp_yaml, encoding="utf-8")) or {} -workflows = data.get("workflows") or {} -stale = [] -for wf_id, entry in workflows.items(): - entry = entry or {} - if entry.get("kind") != "workflow": - continue - if wf_id not in present: - stale.append(wf_id) -if stale: - for wf_id in stale: - print(f"FAIL: ledger workflow '{wf_id}' is STALE — kind: workflow row with no matching .claude/workflows/*.js", file=sys.stderr) - sys.exit(1) -sys.exit(0) -PY -then - status=1 -fi - -if [ "$status" -eq 0 ]; then - echo "OK: all repo-tracked .claude/workflows/*.js are governed (kind: workflow + BC + role), and no ledger workflow rows are stale" -fi -exit "$status" diff --git a/scripts/checkpoint-commit.sh b/scripts/checkpoint-commit.sh deleted file mode 100755 index 7db0f5cc8..000000000 --- a/scripts/checkpoint-commit.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env bash -# Usage: scripts/checkpoint-commit.sh <skill> <phase> "<message>" -# Commits .agents/<skill>/ artifacts. No-op if nothing changed. -# Used by orchestration skills (rpi, crank, evolve) for compaction resilience. -set -euo pipefail - -SKILL="${1:?Usage: checkpoint-commit.sh <skill> <phase> <message>}" -PHASE="${2:?Usage: checkpoint-commit.sh <skill> <phase> <message>}" -MSG="${3:?Usage: checkpoint-commit.sh <skill> <phase> <message>}" - -# Guard: workers must NOT commit (lead-only-commit rule) -if [ "${CRANK_WORKER:-}" = "true" ]; then - echo "checkpoint-commit: skipped (worker mode)" - exit 0 -fi - -git add ".agents/${SKILL}/" 2>/dev/null || true -if git diff --cached --quiet 2>/dev/null; then - echo "checkpoint-commit: nothing to commit for ${SKILL}/${PHASE}" - exit 0 -fi - -git commit -m "${SKILL}: ${PHASE} — ${MSG}" -echo "checkpoint-commit: committed ${SKILL}/${PHASE}" diff --git a/scripts/ci-local-release.sh b/scripts/ci-local-release.sh index 4433ec6ee..6928b6ded 100755 --- a/scripts/ci-local-release.sh +++ b/scripts/ci-local-release.sh @@ -335,6 +335,7 @@ run_shellcheck() { # the gate irreproducible across machines (age-z1pv). local files=() while IFS= read -r file; do + [[ -f "$file" ]] || continue files+=("$file") done < <(git ls-files '*.sh') @@ -349,6 +350,12 @@ run_shellcheck() { run_markdownlint() { local md_files=() while IFS= read -r file; do + [[ -f "$file" ]] || continue + case "$file" in + docs/audits/*|docs/plans/*|docs/releases/*|docs/learnings/*|docs/evidence/*|docs/decisions/*|docs/handoffs/*) + continue + ;; + esac md_files+=("$file") done < <(git ls-files '*.md') @@ -966,7 +973,6 @@ run_step_bg "Codex runtime sections" bash ./scripts/validate-codex-runtime-secti # run_step_bg "Codex install bundle parity" bash ./scripts/validate-codex-install-bundle.sh run_step_bg "Codex artifact manifest" bash ./scripts/validate-codex-generated-manifest.sh run_step_bg "Codex artifact metadata" bash ./scripts/validate-codex-generated-artifacts.sh --scope worktree -run_step_bg "Codex backbone prompts" bash ./scripts/validate-codex-backbone-prompts.sh run_step_bg "Skill runtime formats" bash ./scripts/validate-skill-runtime-formats.sh run_step_bg "Contract compatibility gate" ./scripts/check-contract-compatibility.sh run_step_bg "Embedded sync check" ./scripts/validate-embedded-sync.sh @@ -975,19 +981,9 @@ run_step_bg "Secret pattern scan" run_security_scan_patterns run_step_bg "Dangerous shell pattern scan" run_dangerous_pattern_scan run_step_bg "Skill CLI snippets" bash ./scripts/validate-skill-cli-snippets.sh run_step_bg "Command/test pairing gate" ./scripts/check-go-command-test-pair.sh -# MemRL feedback health is a local observation, not lifecycle authority. -if local_env_checks_blocking; then - run_step_bg "MemRL feedback loop health" ./scripts/check-memrl-health.sh -fi -run_step_bg "Doctor health check" ./scripts/check-doctor-health.sh collect_parallel -# Non-official local-ci: keep the local observation advisory. -if ! local_env_checks_blocking; then - run_step_advisory "MemRL feedback loop health" ./scripts/check-memrl-health.sh -fi - # ── Phase 3: Parallel medium-weight checks ── run_step_bg "CLI docs parity" ./scripts/generate-cli-reference.sh --check @@ -995,8 +991,6 @@ run_step_bg "ShellCheck" run_shellcheck run_step_bg "Markdownlint" run_markdownlint run_step_bg "Smoke tests" ./tests/smoke-test.sh --verbose run_step_bg "Skill lint" bash ./tests/skills/run-all.sh -run_step_bg "Headless runtime skill smoke" bash ./scripts/validate-headless-runtime-skills.sh -run_step_bg "CLI integration smoke tests" ./tests/integration/test-cli-commands.sh run_step_bg "Command/test pairing gate tests" ./tests/scripts/test-go-command-test-pair.sh run_step_bg "Go fast scope tests" bats ./tests/scripts/validate-go-fast.bats run_step_bg "Skill runtime parity tests" bash ./tests/scripts/test-skill-runtime-parity.sh @@ -1005,9 +999,7 @@ run_step_bg "Codex plugin install tests" bash ./tests/scripts/test-codex-plugin- run_step_bg "Codex native install tests" bash ./tests/scripts/test-codex-native-skills-install.sh run_step_bg "Codex artifact manifest tests" bash ./tests/scripts/test-codex-generated-manifest.sh run_step_bg "Codex artifact metadata tests" bash ./tests/scripts/test-codex-generated-artifacts.sh -run_step_bg "Codex backbone prompt tests" bash ./tests/scripts/test-codex-backbone-prompts.sh run_step_bg "Validate-local tests" bash ./tests/scripts/test-validate-local.sh -run_step_bg "Headless runtime skill smoke tests" bash ./tests/scripts/test-headless-runtime-skills.sh collect_parallel @@ -1016,8 +1008,6 @@ collect_parallel run_step_bg "Skill schema validation" ./scripts/validate-skill-schema.sh --verbose run_step_bg "Learning coherence" ./scripts/validate-learning-coherence.sh -run_step_bg "JSON flag consistency" ./tests/cli/test-json-flag-consistency.sh -run_step_bg "JSON flag temp workspace" ./tests/cli/test-json-flag-consistency-tempdir.sh collect_parallel @@ -1025,76 +1015,38 @@ collect_parallel if [[ "$QUICK_MODE" == "true" ]]; then # --quick: code-correctness only. Build the binary (current platform), vet, and run - # the test suite WITHOUT -race. Skip the entire release-rehearsal lane: SBOM - # (cyclonedx/spdx), the multi-platform release-binary validation, the vuln-scan - # security gate, and the contract canaries — those belong to the full pre-tag run. + # the test suite WITHOUT -race. echo "" echo -e "${YELLOW} [--quick] skipping release-rehearsal lane (SBOM/cross-build/scan);${NC}" echo -e "${YELLOW} run full mode before the actual tag${NC}" - warn "Skipped SBOM generation (cyclonedx/spdx) (--quick)" warn "Skipped multi-platform release-binary validation (--quick)" warn "Skipped vuln-scan security gate (syft/grype/trivy) (--quick)" - warn "Skipped AgentOps contract canaries (--quick)" warn "Skipped Go race tests (--quick; runs non-race go test instead)" run_step "Go build + vet + test (current platform, no -race)" run_go_quick_build_and_test elif [[ "$FAST_MODE" == "true" ]]; then warn "Skipped Go race tests (--fast)" - warn "Skipped SBOM generation (--fast)" warn "Skipped Security gate (--fast)" - warn "Skipped AgentOps contract canaries (--fast)" # Still build the binary (fast) and run smoke tests against it run_step "Go build + vet" run_go_build_only run_step "Release binary validation" run_release_binary_validation else - # These are the heavy hitters — run them in parallel + # The full deterministic code and security checks run in parallel. run_step_bg "Go build + race tests" run_go_build_and_tests - run_step_bg "Generate SBOM artifacts (CycloneDX + SPDX)" generate_sbom_artifacts - run_step_bg "Security toolchain gate (${SECURITY_MODE}, require tools)" run_security_gate - run_step_bg "AgentOps contract canaries" ./scripts/test-agentops-contract-canaries.sh + run_step_bg "Security toolchain gate (${SECURITY_MODE})" run_security_gate collect_parallel run_step "Release binary validation" run_release_binary_validation fi -# Build-tag archive mechanism (ADR-0012 / recon-2026-07-02 audit A8): the default -# build must OMIT the archived command sets and -tags flywheel|legacy must restore -# them buildably. Cheap (compiles 4 tag variants) and a real bitrot guard, so it -# runs in every mode — archived satellite code (ao orchestrate/loop/tick/corpus) -# can no longer silently stop compiling between manual `make verify-buildtags` runs. -run_step "Build-tag archive mechanism (verify-buildtags)" ./scripts/verify-buildtags.sh - # ── Phase 5: CLI smoke tests (need built binary) ── run_step_bg "ao init + live-waist smoke" run_init_live_waist_smoke -run_step_bg "Release smoke test (all commands)" ./scripts/release-smoke-test.sh --skip-build collect_parallel -# Digital-twin/VIL + eval evidence are release-rehearsal artifacts (the eval lane runs -# the AgentOps eval suite, which is slow). --quick skips them entirely; the full pre-tag -# run produces them. -if [[ "$QUICK_MODE" != "true" ]]; then - run_step "Digital twin/VIL evidence" write_release_digital_twin_evidence - run_step "AgentOps eval evidence" run_release_eval_evidence -else - warn "Skipped digital-twin/VIL + AgentOps eval evidence (--quick)" -fi - -# ── Phase 6: Release readiness evidence ── -# Official release audits (--release-version) require HIL evidence or an -# explicit waiver. Normal local runs and --fast runs still write advisory JSON. -# --quick skips the HIL + readiness-score release-rehearsal gates outright — they -# audit release readiness, not code correctness. -if [[ "$QUICK_MODE" != "true" ]]; then - run_step "HIL release evidence" run_release_hil_evidence - run_step "Release readiness score gate" check_release_readiness -else - warn "Skipped HIL release evidence + release readiness score gate (--quick)" -fi - # ═══════════════════════════════════════════════════════ # Summary # ═══════════════════════════════════════════════════════ @@ -1102,32 +1054,21 @@ fi END_TIME=$(date +%s) ELAPSED=$((END_TIME - START_TIME)) -# Release-provenance artifacts (manifest + tag-index) record a release rehearsal. -# --quick is explicitly NOT a rehearsal, so it does not write them — that keeps the -# tag-index free of non-rehearsal entries. -if [[ "$QUICK_MODE" != "true" ]]; then - write_release_artifact_manifest - write_tag_index -fi - echo "" echo -e "${BLUE}═══════════════════════════════════════════════════════${NC}" if [[ "$errors" -gt 0 ]]; then echo -e "${RED} LOCAL CI FAILED ($errors failing check(s)) [${ELAPSED}s]${NC}" - echo " Scan/SBOM artifacts: $ARTIFACT_DIR" echo -e "${BLUE}═══════════════════════════════════════════════════════${NC}" exit 1 fi if [[ "$QUICK_MODE" == "true" ]]; then echo -e "${GREEN} LOCAL CI QUICK SANITY PASSED [${ELAPSED}s]${NC}" - echo -e "${YELLOW} --quick skipped the release-rehearsal lane (SBOM/cross-build/scan/eval/HIL/readiness).${NC}" - echo -e "${YELLOW} Run the full gate (no flag) before the actual tag.${NC}" + echo -e "${YELLOW} --quick skipped race, security, and release-binary checks.${NC}" echo -e "${BLUE}═══════════════════════════════════════════════════════${NC}" exit 0 fi echo -e "${GREEN} LOCAL CI PASSED [${ELAPSED}s]${NC}" -echo " Scan/SBOM artifacts: $ARTIFACT_DIR" echo -e "${BLUE}═══════════════════════════════════════════════════════${NC}" exit 0 diff --git a/scripts/ci/verify-windows-install.ps1 b/scripts/ci/verify-windows-install.ps1 index 052977221..58f4a7d2c 100644 --- a/scripts/ci/verify-windows-install.ps1 +++ b/scripts/ci/verify-windows-install.ps1 @@ -61,19 +61,9 @@ function Get-ManifestSkillCount { $manifest = Get-Content -LiteralPath $ManifestPath -Raw | ConvertFrom-Json $hasCount = ($manifest.PSObject.Properties.Name -contains "package_count" -and $manifest.package_count -gt 0) $hasSkills = ($manifest.PSObject.Properties.Name -contains "skills") - # package_count and skills[] are DELIBERATELY different counts and must not be - # forced equal: package_count inventories every installable skill directory - # (incl. the compatibility pointer twins pre-mortem/post-mortem/pre_mortem/ - # post_mortem), while skills[] lists only canonical implementation rows. On the - # real bundle that is 66 vs 62. This split is the authoritative contract — - # enforced by scripts/validate-codex-generated-manifest.sh (package_count == - # all installable dirs; len(skills[]) == dirs minus the 4 pointers), by the - # generator scripts/codex-sync.sh, by cli/internal/quality/skills_codex.go - # (doctor reads package_count when present), and by the bash installer selftest - # (installer-selftest.bats asserts disk==package_count only, NOT - # package_count==len(skills[])). The real 66-vs-62 bug this leg guards against - # is a STALE manifest whose package_count is missing/wrong, caught below by the - # manifest-count-vs-disk assertion — not by any internal equality check. + # package_count inventories every installable generated skill directory. + # The manifest validator keeps it equal to the canonical skills[] inventory; + # no compatibility-pointer directories are installed. if ($hasCount) { return [int]$manifest.package_count } diff --git a/scripts/codex-sync.sh b/scripts/codex-sync.sh index d6c02b3b8..7ba71411d 100755 --- a/scripts/codex-sync.sh +++ b/scripts/codex-sync.sh @@ -212,7 +212,7 @@ def transform_body(body: str, known_skills: set[str], exempt: bool = False) -> s ) # /<known-skill> -> $<known-skill> for slash-COMMAND invocations only — never - # a path segment. Longest names first (so /pre-mortem wins over /pre). Exclude + # a path segment. Longest names first (so /premortem wins over /pre). Exclude # when preceded by a path char (word/./-/_/slash, e.g. ../research/, foo/plan) # or followed by '/' (a path like /research/SKILL.md), so markdown links and # file paths are left intact (the bug that turned ../foo/ into ..$foo/). @@ -448,7 +448,6 @@ source_skills = sorted( for p in source_root.iterdir() if p.is_dir() and not p.name.startswith("_") - and p.name not in {"pre-mortem", "post-mortem", "pre_mortem", "post_mortem"} and (p / "SKILL.md").exists() ) known_skills = set(source_skills) @@ -456,6 +455,23 @@ known_skills = set(source_skills) drift = [] generated = [] +# Source metadata owns the installed set. Retired source roots must not leave +# empty directories, stale generated twins, or override rows that continue to +# advertise removed skills. +retired_twin_dirs = sorted( + p for p in codex_root.iterdir() + if p.is_dir() and not p.name.startswith("_") and p.name not in known_skills +) +if check_only: + drift.extend((p.name, ["retired twin directory remains"]) for p in retired_twin_dirs) +else: + for path in retired_twin_dirs: + shutil.rmtree(path) + +overrides_skills[:] = [ + entry for entry in overrides_skills if entry.get("name") in known_skills +] + for name in source_skills: if name in bespoke: continue @@ -615,7 +631,6 @@ for twin_dir in sorted( p for p in codex_root.iterdir() if p.is_dir() - and p.name not in {"pre-mortem", "post-mortem", "pre_mortem", "post_mortem"} and (p / "SKILL.md").exists() ): marker_path = twin_dir / marker_name diff --git a/scripts/cron-tune-cadence.sh b/scripts/cron-tune-cadence.sh deleted file mode 100755 index 1a88448a4..000000000 --- a/scripts/cron-tune-cadence.sh +++ /dev/null @@ -1,127 +0,0 @@ -#!/usr/bin/env bash -# cron-tune-cadence.sh — mechanize symmetric cron-cadence tuning for /evolve. -# -# Reads .agents/evolve/session-state.json + current repo state, computes a -# state-hash, updates a heartbeat streak, and prints a tuning recommendation: -# -# TUNE_DOWN state hasn't changed for N consecutive fires; cadence too tight -# TUNE_UP productive cycle happened with open work; cadence may be too slow -# STAY state changed naturally; no tune needed -# -# The agent reads the recommendation in cron-procedure step 6 and decides -# whether to CronDelete + CronCreate (cron tools are agent-only, not shell- -# callable). This script's job is to remove self-reported metrics from the -# decision — the streak counter is updated atomically here. -# -# Derivation: cycle 237 of 2026-05-20 — agent self-reported "9 heartbeats" -# when 7 was accurate. Judge B (council 220-240) flagged self-reported -# metrics in the same note arguing for self-edit. Mechanizing the count -# removes that failure mode. -# -# Symmetric tuning rule (from feedback_self_editing_cron.md): -# * Heartbeat streak ≥ 3 with identical state-hash → TUNE_DOWN -# * Productive cycle WITH queued in-flight workload → TUNE_UP -# * Otherwise → STAY -# -# Bead: soc-adwq -# -# Usage: -# cron-tune-cadence.sh <cycle-result> -# cycle-result is one of: productive, heartbeat, blocked-on-failure, teardown -# -# Output (stdout): one of TUNE_DOWN, TUNE_UP, STAY -# Stderr: explanation + new streak value -# Exit code: 0 always (caller decides what to do) - -set -euo pipefail - -CYCLE_RESULT="${1:-}" - -if [[ -z "$CYCLE_RESULT" ]]; then - echo "usage: $0 <cycle-result>" >&2 - echo " cycle-result ∈ {productive, heartbeat, blocked-on-failure, teardown}" >&2 - exit 2 -fi - -REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || echo "$PWD")" -STATE_FILE="$REPO_ROOT/.agents/evolve/session-state.json" -HEARTBEAT_THRESHOLD="${CRON_TUNE_HEARTBEAT_THRESHOLD:-3}" - -if [[ ! -f "$STATE_FILE" ]]; then - echo "cron-tune-cadence: $STATE_FILE missing — cannot decide; default STAY" >&2 - echo "STAY" - exit 0 -fi - -# Compute state-hash = sha1(main_sha + sorted batch_pr_states) -# Tolerate missing git context (tests run from non-git dirs). -MAIN_SHA="$(git rev-parse origin/main 2>/dev/null || git rev-parse HEAD 2>/dev/null || echo "no-git")" -BATCH_PRS="$(jq -r '.batch_prs[]? // empty' "$STATE_FILE" 2>/dev/null || true)" - -PR_STATES="" -if [[ -n "$BATCH_PRS" ]]; then - if command -v gh >/dev/null 2>&1; then - while IFS= read -r pr; do - [[ -z "$pr" ]] && continue - STATE_LINE="$(gh pr view "$pr" --json state,mergeStateStatus --jq '"\(.state) \(.mergeStateStatus)"' 2>/dev/null || echo "UNKNOWN UNKNOWN")" - PR_STATES="${PR_STATES}${pr}:${STATE_LINE}"$'\n' - done <<< "$BATCH_PRS" - fi -fi - -# Sorted, deterministic -PR_STATES_SORTED="$(printf '%s' "$PR_STATES" | sort)" -STATE_HASH="$(printf '%s\n%s' "$MAIN_SHA" "$PR_STATES_SORTED" | sha1sum | awk '{print $1}')" - -# Read prior values -PREV_HASH="$(jq -r '.state_hash // ""' "$STATE_FILE")" -PREV_STREAK="$(jq -r '.heartbeat_streak // 0' "$STATE_FILE")" -OPEN_PR_COUNT="$(printf '%s' "$BATCH_PRS" | wc -l | tr -d ' ')" - -# Decide -RECOMMENDATION="STAY" -NEW_STREAK=0 -REASON="" - -case "$CYCLE_RESULT" in - productive|teardown) - NEW_STREAK=0 - if [[ "$CYCLE_RESULT" == "productive" ]] && [[ "$OPEN_PR_COUNT" -gt 0 ]]; then - RECOMMENDATION="TUNE_UP" - REASON="productive cycle + ${OPEN_PR_COUNT} open PRs remain → suggest cadence faster" - else - REASON="productive/teardown — streak reset, no tune" - fi - ;; - heartbeat|blocked-on-failure) - if [[ "$STATE_HASH" == "$PREV_HASH" ]]; then - NEW_STREAK=$((PREV_STREAK + 1)) - if [[ "$NEW_STREAK" -ge "$HEARTBEAT_THRESHOLD" ]]; then - RECOMMENDATION="TUNE_DOWN" - REASON="heartbeat_streak=$NEW_STREAK >= $HEARTBEAT_THRESHOLD with identical state-hash → suggest cadence slower" - else - REASON="heartbeat (streak=$NEW_STREAK < $HEARTBEAT_THRESHOLD) — no tune yet" - fi - else - NEW_STREAK=1 - REASON="state-hash changed — streak reset to 1, no tune" - fi - ;; - *) - REASON="unknown cycle-result '$CYCLE_RESULT' — no tune" - ;; -esac - -# Persist new state atomically -TMP="$(mktemp)" -trap 'rm -f "$TMP"' EXIT -jq --arg hash "$STATE_HASH" --argjson streak "$NEW_STREAK" \ - --arg rec "$RECOMMENDATION" --arg ts "$(date -u +%FT%TZ)" \ - '. + {state_hash: $hash, heartbeat_streak: $streak, last_tune_recommendation: $rec, last_tune_check: $ts}' \ - "$STATE_FILE" > "$TMP" -mv "$TMP" "$STATE_FILE" -trap - EXIT - -echo "cron-tune-cadence: $REASON" >&2 -echo "cron-tune-cadence: state_hash=${STATE_HASH:0:8} streak=$NEW_STREAK rec=$RECOMMENDATION" >&2 -echo "$RECOMMENDATION" diff --git a/scripts/eval-agentops.sh b/scripts/eval-agentops.sh index 8fee0e959..680656c61 100755 --- a/scripts/eval-agentops.sh +++ b/scripts/eval-agentops.sh @@ -432,8 +432,7 @@ with open(sys.argv[1], encoding="utf-8") as fh: print(len(data.get("stale_suite_hashes") or [])) PY )" - # Drift-only gate (mirrors pre-push-gate.sh block 24d and the - # agentops-eval-baseline-audit CI job): fail only on + # Drift-only check: fail only on # stale_suite_hashes (a promoted baseline's recorded suite SHA stops # matching the current suite definition). policy_mismatch_count is # informational because under the no-tracked-.agents/ policy diff --git a/scripts/export-session-summary.sh b/scripts/export-session-summary.sh deleted file mode 100755 index f9e803ff7..000000000 --- a/scripts/export-session-summary.sh +++ /dev/null @@ -1,205 +0,0 @@ -#!/usr/bin/env bash -# export-session-summary.sh — roll up a session's outcomes into one markdown. -# -# At teardown, the durable artifacts of a session are scattered: -# - .agents/evolve/cycle-history.jsonl ← the cycle ledger -# - .agents/evolve/session-state.json ← resume state -# - bd memories added in window ← persistent learnings -# - git log in window ← commits + merges -# - gh pr list merged in window ← shipped PRs -# -# This script produces a single Markdown digest at -# `.agents/evolve/session-summary-<UTC>.md` from those sources. Designed -# for hand-off-to-next-session and for human readout. -# -# Inputs (all optional): -# --since <ref-or-time> git ref (e.g. HEAD~50) OR ISO-8601 time -# (default: 24h ago) -# --out <path> output file path (default: auto-generated) -# --stdout emit to stdout (skip file write) -# --no-bd skip bd memories section -# --no-prs skip GitHub merged-PR section (no gh call) -# -# Exit codes: -# 0 — wrote a summary file (or printed to stdout) -# 2 — usage error -# 3 — required input missing (no cycle-history.jsonl AND no other source) - -set -euo pipefail - -SINCE="" -OUT_PATH="" -TO_STDOUT=0 -INCLUDE_BD=1 -INCLUDE_PRS=1 - -usage() { - sed -n '2,/^$/p' "$0" | sed 's/^# \{0,1\}//' - exit "${1:-0}" -} - -while [ $# -gt 0 ]; do - case "$1" in - --since) shift; SINCE="${1:-}" ;; - --out) shift; OUT_PATH="${1:-}" ;; - --stdout) TO_STDOUT=1 ;; - --no-bd) INCLUDE_BD=0 ;; - --no-prs) INCLUDE_PRS=0 ;; - -h|--help) usage 0 ;; - *) echo "export-session-summary: unknown arg: $1" >&2; usage 2 ;; - esac - shift || true -done - -if [ -z "$SINCE" ]; then - # Default window: 24 hours ago in UTC ISO-8601. - SINCE="$(date -u -d '24 hours ago' +%FT%TZ 2>/dev/null || date -u -v-24H +%FT%TZ 2>/dev/null || echo "")" -fi - -# Resolve git "--since" arg. If $SINCE looks like an ISO timestamp keep it; -# if it looks like a ref, translate to that ref's commit date. -git_since_arg() { - local s="$1" - if [ -z "$s" ]; then - echo "" - return - fi - if git rev-parse --verify --quiet "$s^{commit}" >/dev/null 2>&1; then - git show --no-patch --format=%cI "$s" 2>/dev/null - else - printf '%s' "$s" - fi -} - -NOW_UTC="$(date -u +%FT%TZ)" -DEFAULT_OUT=".agents/evolve/session-summary-$(date -u +%Y%m%dT%H%M%SZ).md" -[ -z "$OUT_PATH" ] && OUT_PATH="$DEFAULT_OUT" - -# Build sections into a temp buffer first, then write atomically. -TMP_OUT="$(mktemp)" -trap 'rm -f "$TMP_OUT"' EXIT - -section_header() { - printf '\n## %s\n\n' "$1" >> "$TMP_OUT" -} - -# 1. Outcomes — high-level rollup ---------------------------------------- -printf '# Session summary — %s\n\n' "$NOW_UTC" > "$TMP_OUT" -printf '*Window:* %s → %s\n\n' "$SINCE" "$NOW_UTC" >> "$TMP_OUT" - -section_header "Outcomes" - -GIT_SINCE_ARG="$(git_since_arg "$SINCE")" -commit_count=0 -if [ -n "$GIT_SINCE_ARG" ]; then - commit_count="$(git log --since="$GIT_SINCE_ARG" --oneline 2>/dev/null | wc -l | tr -d ' ')" -fi - -cycle_count=0 -productive_count=0 -if [ -r .agents/evolve/cycle-history.jsonl ]; then - # Filter cycles by their `ts` field. If $SINCE is iso, lexicographic - # compare works; if it's an empty string, count everything. - if [ -n "$SINCE" ]; then - cycle_count="$(jq -cs --arg s "$SINCE" '[.[] | select(.ts != null and .ts >= $s)] | length' \ - .agents/evolve/cycle-history.jsonl 2>/dev/null || echo 0)" - productive_count="$(jq -cs --arg s "$SINCE" '[.[] | select(.ts != null and .ts >= $s and .result == "productive")] | length' \ - .agents/evolve/cycle-history.jsonl 2>/dev/null || echo 0)" - else - cycle_count="$(wc -l < .agents/evolve/cycle-history.jsonl | tr -d ' ')" - productive_count="$(jq -cs '[.[] | select(.result == "productive")] | length' .agents/evolve/cycle-history.jsonl 2>/dev/null || echo 0)" - fi -fi - -merged_pr_count=0 -if [ "$INCLUDE_PRS" -eq 1 ] && command -v gh >/dev/null 2>&1; then - if [ -n "$GIT_SINCE_ARG" ]; then - merged_pr_count="$(gh pr list --state merged --limit 200 \ - --json mergedAt --jq "[.[] | select(.mergedAt >= \"$GIT_SINCE_ARG\")] | length" \ - 2>/dev/null || echo 0)" - fi -fi - -printf -- '- Commits: **%s**\n' "$commit_count" >> "$TMP_OUT" -printf -- '- Cycles: **%s** total, **%s** productive\n' "$cycle_count" "$productive_count" >> "$TMP_OUT" -printf -- '- PRs merged: **%s**\n' "$merged_pr_count" >> "$TMP_OUT" - -# 2. Cycle ledger — compressed ------------------------------------------ -if [ -r .agents/evolve/cycle-history.jsonl ] && [ "$cycle_count" -gt 0 ]; then - section_header "Cycle ledger (compressed)" - if [ -n "$SINCE" ]; then - jq -r --arg s "$SINCE" ' - select(.ts != null and .ts >= $s) | - "- **cycle \(.cycle)** [\(.result // "?")] \(.mode // "?") — \(.notes // "" | .[0:140])" - ' .agents/evolve/cycle-history.jsonl >> "$TMP_OUT" 2>/dev/null || true - else - jq -r ' - "- **cycle \(.cycle)** [\(.result // "?")] \(.mode // "?") — \(.notes // "" | .[0:140])" - ' .agents/evolve/cycle-history.jsonl >> "$TMP_OUT" 2>/dev/null || true - fi -fi - -# 3. New memories --------------------------------------------------------- -if [ "$INCLUDE_BD" -eq 1 ] && command -v bd >/dev/null 2>&1; then - section_header "New memories" - # `bd memories` lists all; we filter by created_at if available, else dump - # the last ~20 as a coarse window. - bd_json="$(bd memories --json 2>/dev/null || true)" - if [ -n "$bd_json" ]; then - if [ -n "$SINCE" ]; then - printf '%s' "$bd_json" | jq -r --arg s "$SINCE" ' - try (.[] | select((.created_at // .updated_at // "") >= $s) | - "- **\(.key)** — \(.content // "" | .[0:160])") - catch empty - ' >> "$TMP_OUT" 2>/dev/null || true - else - printf '%s' "$bd_json" | jq -r ' - try (sort_by(.created_at // .updated_at // "") | reverse | .[0:20][] | - "- **\(.key)** — \(.content // "" | .[0:160])") - catch empty - ' >> "$TMP_OUT" 2>/dev/null || true - fi - fi -fi - -# 4. Commits in window --------------------------------------------------- -if [ -n "$GIT_SINCE_ARG" ] && [ "$commit_count" -gt 0 ]; then - section_header "Commits" - git log --since="$GIT_SINCE_ARG" --pretty=format:'- `%h` %s' 2>/dev/null >> "$TMP_OUT" || true - printf '\n' >> "$TMP_OUT" -fi - -# 5. Merged PRs in window ------------------------------------------------ -if [ "$INCLUDE_PRS" -eq 1 ] && command -v gh >/dev/null 2>&1 \ - && [ -n "$GIT_SINCE_ARG" ] && [ "$merged_pr_count" -gt 0 ]; then - section_header "Merged PRs" - gh pr list --state merged --limit 200 \ - --json number,title,mergedAt \ - --jq "[.[] | select(.mergedAt >= \"$GIT_SINCE_ARG\")] | reverse | .[] | - \"- #\(.number) \(.title)\"" 2>/dev/null >> "$TMP_OUT" || true -fi - -# 6. Carry-forward — open in-flight --------------------------------------- -section_header "Carry-forward" - -if [ -r .agents/evolve/session-state.json ]; then - in_flight="$(jq -r '.batch_prs // [] | map(tostring) | join(", ")' .agents/evolve/session-state.json 2>/dev/null || true)" - goal="$(jq -r '.goal // "(none)"' .agents/evolve/session-state.json 2>/dev/null || echo "(none)")" - printf -- '- Goal: **%s**\n' "$goal" >> "$TMP_OUT" - printf -- '- PRs in flight at snapshot: **%s**\n' "${in_flight:-none}" >> "$TMP_OUT" -fi -if command -v bd >/dev/null 2>&1; then - ready_count="$(bd ready --json 2>/dev/null | jq 'length' 2>/dev/null || echo "?")" - printf -- '- Open ready beads: **%s**\n' "$ready_count" >> "$TMP_OUT" -fi - -# Atomic write or stdout -------------------------------------------------- -if [ "$TO_STDOUT" -eq 1 ]; then - cat "$TMP_OUT" -else - mkdir -p "$(dirname "$OUT_PATH")" - mv "$TMP_OUT" "$OUT_PATH" - # Disable the EXIT trap since we've already moved the file. - trap - EXIT - echo "export-session-summary: wrote $OUT_PATH" -fi diff --git a/scripts/generate-ci-jobs-table.sh b/scripts/generate-ci-jobs-table.sh index 48a1def66..5696599b8 100755 --- a/scripts/generate-ci-jobs-table.sh +++ b/scripts/generate-ci-jobs-table.sh @@ -2,10 +2,8 @@ # generate-ci-jobs-table.sh — render docs/CI-CD.md "## CI Jobs and What They Check" # table from .github/workflows/validate.yml + docs/contracts/ci-jobs.yaml. # -# soc-3oij: CI jobs table generator. Eliminates hand-edit drift — adding a -# new validate-* job goes through scripts/add-validate-job.sh (soc-3oij meta-fix, -# PR #315) which writes the workflow row; the manifest gets a matching entry; -# this generator renders the CI-CD table. +# CI jobs table generator. Adding a new purpose job updates the declared +# registry and workflow together; this generator renders the CI-CD table. # # Modes: # (default) Render table to stdout @@ -125,13 +123,12 @@ for job in needs: PYEOF } -# Find CI jobs table boundaries: section header → next ### or EOF. -# Accept ## (docs/CI-CD.md) or ### (legacy fixture / AGENTS-era heading). +# Find the generated CI jobs table between its explicit ownership markers. extract_agents_section() { awk ' BEGIN { in_section=0 } - /^(##|###) CI Jobs and What They Check$/ { in_section=1; next } - in_section && /^### / { in_section=0 } + /^<!-- BEGIN GENERATED CI JOBS -->$/ { in_section=1; next } + /^<!-- END GENERATED CI JOBS -->$/ { in_section=0 } in_section { print } ' "$AGENTS_PATH" } @@ -172,8 +169,8 @@ case "$MODE" in trap 'rm -f "$TMP_GEN" "$TMP_NEW"' EXIT render_table > "$TMP_GEN" - # Walk the target doc replacing the section content between - # "##|### CI Jobs and What They Check" and the next "### " header. + # Replace only the explicitly generated region. The surrounding + # narrative and section names remain hand-maintained documentation. awk -v gen_file="$TMP_GEN" ' BEGIN { while ((getline line < gen_file) > 0) { @@ -183,16 +180,15 @@ case "$MODE" in in_section = 0 emitted = 0 } - /^(##|###) CI Jobs and What They Check$/ { + /^<!-- BEGIN GENERATED CI JOBS -->$/ { print - print "" + print "<!-- Generated from docs/contracts/ci-jobs.yaml by scripts/generate-ci-jobs-table.sh. -->" for (i = 1; i <= n; i++) print gen[i] - print "" in_section = 1 emitted = 1 next } - in_section && /^### / { + in_section && /^<!-- END GENERATED CI JOBS -->$/ { in_section = 0 print next diff --git a/scripts/generate-cli-reference.sh b/scripts/generate-cli-reference.sh index 8243f766e..cd3bd5300 100755 --- a/scripts/generate-cli-reference.sh +++ b/scripts/generate-cli-reference.sh @@ -301,10 +301,3 @@ fi mkdir -p "$(dirname "$OUTPUT")" generate > "$OUTPUT" echo "Generated $OUTPUT" - -# Auto-sync the declared CLI heading count in docs/cli-skills-map.md so a -# fresh COMMANDS.md regen can never leave the count gate red. validate-cli- -# skills-map.sh --fix is a no-op when counts already match. -if [[ -x "$SCRIPT_DIR/validate-cli-skills-map.sh" ]]; then - bash "$SCRIPT_DIR/validate-cli-skills-map.sh" --fix >/dev/null -fi diff --git a/scripts/generate-context-map.sh b/scripts/generate-context-map.sh deleted file mode 100755 index cca5e9585..000000000 --- a/scripts/generate-context-map.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail -root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -if [[ "${1:-}" == "--check" ]]; then - exec python3 "$root/scripts/generate-skill-mesh.py" --check -fi -exec python3 "$root/scripts/generate-skill-mesh.py" diff --git a/scripts/generate-registry.sh b/scripts/generate-registry.sh deleted file mode 100755 index d5dc022e3..000000000 --- a/scripts/generate-registry.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail -root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -case "${1:-}" in - --check) exec python3 "$root/scripts/generate-skill-mesh.py" --check ;; - --stdout) exec python3 "$root/scripts/generate-skill-mesh.py" --print registry ;; - '') exec python3 "$root/scripts/generate-skill-mesh.py" ;; - *) echo "unknown argument: $1" >&2; exit 2 ;; -esac diff --git a/scripts/generate-skill-catalog.sh b/scripts/generate-skill-catalog.sh deleted file mode 100755 index a359993f0..000000000 --- a/scripts/generate-skill-catalog.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail -root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -case "${1:-}" in - --check) exec python3 "$root/scripts/generate-skill-mesh.py" --check ;; - --stdout) exec python3 "$root/scripts/generate-skill-mesh.py" --print catalog ;; - --out) exec python3 "$root/scripts/generate-skill-mesh.py" --print catalog --out "${2:?missing output path}" ;; - -h|--help) echo 'Usage: generate-skill-catalog.sh [--check|--stdout|--out PATH]'; exit 0 ;; - '') exec python3 "$root/scripts/generate-skill-mesh.py" ;; - *) echo "unknown argument: $1" >&2; exit 2 ;; -esac diff --git a/scripts/generate-skill-domain-map.sh b/scripts/generate-skill-domain-map.sh deleted file mode 100755 index cca5e9585..000000000 --- a/scripts/generate-skill-domain-map.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail -root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -if [[ "${1:-}" == "--check" ]]; then - exec python3 "$root/scripts/generate-skill-mesh.py" --check -fi -exec python3 "$root/scripts/generate-skill-mesh.py" diff --git a/scripts/generate-skill-mesh.py b/scripts/generate-skill-mesh.py index e79671018..bfb492d14 100755 --- a/scripts/generate-skill-mesh.py +++ b/scripts/generate-skill-mesh.py @@ -6,6 +6,7 @@ from __future__ import annotations import argparse import json from pathlib import Path +import shutil import sys from typing import Any @@ -76,6 +77,9 @@ def validate_graph(entries: list[dict[str, Any]]) -> None: expected = {"rpi": {"plan", "implement", "validate"}, "plan": set(), "implement": set(), "validate": set()} if core != expected: raise ValueError(f"core dependency graph mismatch: {core!r}") + extra = {entry["name"]: entry["dependencies"] for entry in entries if entry["name"] != "rpi" and entry["dependencies"]} + if extra: + raise ValueError(f"only rpi may declare hard dependencies: {extra!r}") def catalog(entries: list[dict[str, Any]]) -> dict[str, Any]: @@ -198,6 +202,51 @@ def context_map(entries: list[dict[str, Any]]) -> str: return "\n".join(lines) +def claude_image(entries: list[dict[str, Any]]) -> dict[str, Any]: + return { + "schema_version": "skill-image.v1", + "image": "claude", + "source": "skills/*/SKILL.md metadata", + "skill_count": len(entries), + "skills": [ + {"slug": entry["name"], "path": f"skills/{entry['name']}/", "disposition": entry["disposition"]} + for entry in entries + ], + } + + +def codex_image(entries: list[dict[str, Any]]) -> dict[str, Any]: + return { + "schema_version": "skill-image.v1", + "image": "codex", + "source": "skills/*/SKILL.md metadata", + "skill_count": len(entries), + "skills": [ + { + "slug": entry["name"], + "source_path": f"skills/{entry['name']}/", + "twin_path": f"skills-codex/{entry['name']}/", + "disposition": entry["disposition"], + } + for entry in entries + ], + } + + +def gemini_plugin(entries: list[dict[str, Any]]) -> dict[str, Any]: + version = json.loads((ROOT / ".claude-plugin" / "plugin.json").read_text(encoding="utf-8"))["version"] + return { + "name": "agentops-core-gemini", + "version": version, + "description": f"AgentOps {len(entries)}-skill metadata-derived bundle for Google Antigravity and Gemini.", + "skills": "./skills", + "agents": "./agents", + "rules": "./rules", + "hooks": "./hooks/hooks.json", + "mcpServers": {"agent-mail": {"command": "am", "args": ["serve-stdio"]}}, + } + + def outputs(entries: list[dict[str, Any]]) -> dict[Path, bytes]: return { ROOT / "skills" / "catalog.json": (json.dumps(catalog(entries), indent=2, sort_keys=True) + "\n").encode(), @@ -208,9 +257,39 @@ def outputs(entries: list[dict[str, Any]]) -> dict[Path, bytes]: ROOT / "docs" / "reference" / "agentops-skill-domain-map.md": domain_map(entries).encode(), ROOT / "docs" / "reference" / "agentops-skill-graph.md": graph(entries).encode(), ROOT / "docs" / "contracts" / "context-map.md": context_map(entries).encode(), + ROOT / "images" / "claude" / "manifest.json": (json.dumps(claude_image(entries), indent=2, sort_keys=True) + "\n").encode(), + ROOT / "images" / "codex" / "manifest.json": (json.dumps(codex_image(entries), indent=2, sort_keys=True) + "\n").encode(), + ROOT / "images" / "gemini" / "plugin.json": (json.dumps(gemini_plugin(entries), indent=2, sort_keys=True) + "\n").encode(), } +def sync_gemini_skills(entries: list[dict[str, Any]], check: bool) -> list[str]: + destination = ROOT / "images" / "gemini" / "skills" + expected = {entry["name"] for entry in entries} + actual = {path.name for path in destination.iterdir() if path.is_dir()} if destination.is_dir() else set() + drift: list[str] = [] + for name in sorted(actual - expected): + path = destination / name + if check: + drift.append(path.relative_to(ROOT).as_posix()) + else: + shutil.rmtree(path) + for name in sorted(expected): + source = ROOT / "skills" / name / "SKILL.md" + target_dir = destination / name + target = target_dir / "SKILL.md" + extra = [path for path in target_dir.rglob("*") if path.is_file() and path != target] if target_dir.is_dir() else [] + if check: + if not target.is_file() or target.read_bytes() != source.read_bytes() or extra: + drift.append(target_dir.relative_to(ROOT).as_posix()) + else: + if target_dir.exists(): + shutil.rmtree(target_dir) + target_dir.mkdir(parents=True, exist_ok=True) + target.write_bytes(source.read_bytes()) + return drift + + def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--check", action="store_true") @@ -244,6 +323,7 @@ def main() -> int: else: path.parent.mkdir(parents=True, exist_ok=True) path.write_bytes(payload) + drift.extend(sync_gemini_skills(entries, args.check)) if drift: for path in drift: print(f"DRIFT: {path}", file=sys.stderr) diff --git a/scripts/goal-failure-taxonomy.sh b/scripts/goal-failure-taxonomy.sh deleted file mode 100755 index b1895c274..000000000 --- a/scripts/goal-failure-taxonomy.sh +++ /dev/null @@ -1,78 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# goal-failure-taxonomy.sh -# Classify failing goals from ao fitness JSON into actionable categories. -# -# Usage: -# scripts/goal-failure-taxonomy.sh [fitness-json] -# If omitted, uses latest .agents/evolve/fitness-*-pre.json - -INPUT="${1:-}" - -if [[ -z "$INPUT" ]]; then - INPUT="$(ls -t .agents/evolve/fitness-*-pre.json 2>/dev/null | head -1 || true)" -fi - -if [[ -z "$INPUT" || ! -f "$INPUT" ]]; then - echo "ERROR: fitness JSON not found. Provide a file path or generate .agents/evolve/fitness-*-pre.json." >&2 - exit 1 -fi - -if ! jq -e . "$INPUT" >/dev/null 2>&1; then - echo "ERROR: invalid JSON: $INPUT" >&2 - exit 1 -fi - -classify() { - local id="${1:-}" - case "$id" in - *security*|*secret*|*hook-preflight*) - echo "security" - ;; - *build*|*test*|*vet*|*wiring*|*evolve*|*rpi*|*session-start*|*kill-switch*) - echo "reliability" - ;; - *coverage*|*complexity*|*smoke*|*opencode*|*skill-validation*|*semantic-stability*|*frontmatter*) - echo "quality" - ;; - *goal-*|*pillar-*|*product-freshness*|*manifest-versions-match*|*ao-goals*) - echo "governance" - ;; - *) - echo "other" - ;; - esac -} - -TMP="$(mktemp)" -jq -c '.goals[] | select(.result=="fail") | {goal_id, description}' "$INPUT" > "$TMP" - -TOTAL="$(wc -l < "$TMP" | tr -d ' ')" - -if [[ "$TOTAL" == "0" ]]; then - jq -n '{summary:{total_failing:0,by_category:{}},failing_goals:[]}' - rm -f "$TMP" - exit 0 -fi - -OUT_TMP="$(mktemp)" -while IFS= read -r row; do - gid="$(jq -r '.goal_id // ""' <<<"$row")" - desc="$(jq -r '.description // ""' <<<"$row")" - cat="$(classify "$gid")" - jq -n --arg goal_id "$gid" --arg description "$desc" --arg category "$cat" \ - '{goal_id:$goal_id,category:$category,description:$description}' >> "$OUT_TMP" -done < "$TMP" - -jq -s ' - { - summary: { - total_failing: length, - by_category: (group_by(.category) | map({key: .[0].category, value: length}) | from_entries) - }, - failing_goals: . - } -' "$OUT_TMP" - -rm -f "$TMP" "$OUT_TMP" diff --git a/scripts/hooks/pre-push.local b/scripts/hooks/pre-push.local deleted file mode 100755 index d608226af..000000000 --- a/scripts/hooks/pre-push.local +++ /dev/null @@ -1,147 +0,0 @@ -#!/usr/bin/env sh -# AgentOps cockpit pre-push gate (P1.2 / ag-qidx.2) -# -# Source of truth: scripts/hooks/pre-push.local (tracked). -# Installed copy: $(git rev-parse --git-common-dir)/hooks/pre-push.local -# Install / reinstall: scripts/install-pre-push-gate.sh -# -# Builds a fresh `ao`, validates pushes to main/master with the full race suite, -# then runs the ordinary deterministic repository gate. BLOCKS the push on -# failure (gate exits 1). Semantic validation is deliberately not a Git hook. -# -# Mechanical enforcement (pre-mortem HIGH "discipline is not a mechanism"): -# the ONLY bypass is the audited AGENTOPS_GATE_DISABLED=1, which is logged. -# This mirrors the LAW 0 no-claude-p guard pattern. -set -u - -toplevel=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0 -common=$(git rev-parse --git-common-dir 2>/dev/null) - -# Capture pre-push stdin (local_ref local_sha remote_ref remote_sha) when git -# invokes this hook. Empty when run standalone (ao gate check path). -prepush_stdin="" -_agentops_ao_gate="" -if [ ! -t 0 ]; then - prepush_stdin=$(mktemp "${TMPDIR:-/tmp}/agentops-prepush-stdin.XXXXXX" 2>/dev/null) || prepush_stdin="" - if [ -n "$prepush_stdin" ]; then - cat >"$prepush_stdin" || prepush_stdin="" - fi -fi -cleanup_prepush_stdin() { - [ -n "$prepush_stdin" ] && rm -f "$prepush_stdin" 2>/dev/null || true - [ -n "$_agentops_ao_gate" ] && rm -f "$_agentops_ao_gate" 2>/dev/null || true -} -trap cleanup_prepush_stdin EXIT -# An uncaught signal does not run the EXIT trap in every sh implementation, so -# clean the temporary stdin/binary explicitly before aborting. -# Exit with the conventional 128+signo per signal (SIGINT=130, SIGTERM=143) so -# CI/operator logic can still distinguish a Ctrl-C from a timeout-kill. -trap 'cleanup_prepush_stdin; exit 130' INT -trap 'cleanup_prepush_stdin; exit 143' TERM - -is_push_to_main() { - [ -n "$prepush_stdin" ] && [ -s "$prepush_stdin" ] || return 1 - awk '{print $3}' "$prepush_stdin" | grep -qE '^refs/heads/(main|master)$' -} - -if [ "${AGENTOPS_GATE_DISABLED:-0}" = "1" ]; then - ts=$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo unknown) - sha=$(git rev-parse --short HEAD 2>/dev/null || echo unknown) - branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo unknown) - printf '%s\t%s\t%s\t%s\tpre-push-gate-bypassed\n' \ - "$ts" "${USER:-unknown}" "$branch" "$sha" \ - >> "${common}/agentops-gate-bypass.log" 2>/dev/null || true - echo >&2 "⚠ AgentOps pre-push gate BYPASSED (AGENTOPS_GATE_DISABLED=1) — logged to agentops-gate-bypass.log" - exit 0 -fi - -# --- Go gate (ao gate check) — sole release path ----------------------------- -# From-source bootstrap so the gate that checks Go compiles can never run a -# stale `ao` (pre-mortem BLOCKER-3): prove compilation first (block with the -# build error if broken), then build a fresh ao and exec it. -if ! ( cd "${toplevel}/cli" && go build ./... ); then - echo >&2 "✗ go build ./... failed — fix compilation before pushing (ao gate cannot run on broken Go)" - exit 1 -fi -# age-yy24: the build above validates the WORKING TREE, not the COMMIT(s) being -# pushed. A partial commit (a multi-path `git add` that dropped files) builds in the -# worktree but is build-broken as committed — it landed origin/main red once. When the -# worktree differs from HEAD for tracked source, this builds each pushed commit in an -# ISOLATED temp worktree (clean-tree pushes skip it; zero false alarms). Fail-closed -# only on a definitive build failure; bypass: AGENTOPS_PREPUSH_SKIP_COMMIT_BUILD=1. -if [ -n "$prepush_stdin" ] && [ -s "$prepush_stdin" ]; then - "${toplevel}/scripts/verify-pushed-commit-builds.sh" < "$prepush_stdin" || exit $? -fi -_agentops_ao_gate=$(mktemp "${TMPDIR:-/tmp}/ao-gate.XXXXXX" 2>/dev/null) || { - echo >&2 "✗ could not allocate a per-run ao gate binary" - exit 1 -} -if ! ( cd "${toplevel}/cli" && go build -o "$_agentops_ao_gate" ./cmd/ao ); then - echo >&2 "✗ could not build ao for the gate" - exit 1 -fi - -# FULL-SUITE race gate on push-to-main (age-push-equals-ci-0ua.1). The gate -# below runs `--fast` = CHANGED-scope tests only, so a latent red in an -# UNTOUCHED package is invisible — exactly how cmd/ao shipped red for 281 -# commits. Push-to-main is the SOLE routine CI authority (GHA is a tag/PR/manual -# backstop), so it must run the WHOLE suite, not just the diff. Measured ~77s -# for all packages (-race parallelizes; cmd/ao is the long pole). The in-loop -# `ao gate check --fast --scope head` path is unchanged (fast iteration). -# Emergency bypass: AGENTOPS_PREPUSH_SKIP_FULL_RACE=1 (use only with cause). -if is_push_to_main && [ "${AGENTOPS_PREPUSH_SKIP_FULL_RACE:-0}" != "1" ]; then - # Randomized order is part of the local release membrane: the push-to-main - # door must catch cross-package isolation leaks that a fixed seed hides. - # The old seed-1 compromise was removed after age-cmdao proved shuffle=2 and - # multiple -shuffle=on cmd/ao runs no longer mutate core.bare. - echo "pre-push: push-to-main — running FULL race suite (go test ./... -race -shuffle=on, ~77s)" >&2 - # Scrub git's hook-injected discovery env (age-ngtc). git runs pre-push hooks - # with GIT_DIR/GIT_WORK_TREE/GIT_INDEX_FILE/... pointing at the pushing repo — - # and when the push ORIGINATES FROM A LINKED WORKTREE those point at the - # worktree gitdir. ~10 git-aware test packages (TestGetRepoRoot, TestRemoveWorktree, - # TestRepairSharedCoreWorktreeConfig, TestParseTimeline_IgnoresPollutedGitDiscoveryEnv, - # ...) build fixture repos in temp dirs and shell out to git; the inherited env - # makes those git ops hit the REAL repo (exit 128), so the gate refused every - # worktree-origin push — hostile to the worktree-isolation workflow. Unsetting - # is idempotent (harmless for main-checkout pushes). - # Persist the race-suite output. An INTERMITTENT failure here is an order- - # dependent test-isolation flake surfaced by one specific shuffle seed; the - # seed + FAIL package + test are the ONLY handle on it, and a bare terminal - # run loses them on the next push. Capturing makes the flake class diagnosable - # (re-run `go test ./<pkg> -race -shuffle=<seed>` for a deterministic repro). - race_log="${TMPDIR:-/tmp}/agentops-prepush-race-$(date +%Y%m%d-%H%M%S).log" - if ! ( cd "${toplevel}/cli" \ - && unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_PREFIX GIT_OBJECT_DIRECTORY GIT_COMMON_DIR GIT_NAMESPACE \ - && go test ./... -race -shuffle=on -count=1 ) > "$race_log" 2>&1; then - echo >&2 "✗ FULL race suite FAILED — a package outside the changed set is red; push refused (age-push-equals-ci-0ua.1)" - echo >&2 " full output saved for repro: $race_log" - echo >&2 " --- shuffle seed + failing package/test (re-run: go test ./<pkg> -race -shuffle=<seed> -count=1) ---" - grep -E '^(FAIL|--- FAIL|panic:)|-test\.shuffle|DATA RACE' "$race_log" | head -20 >&2 - gate_status=1 - else - tail -2 "$race_log" >&2 # show the pass summary - rm -f "$race_log" # green: no flake to diagnose, don't litter - fi - if [ "${gate_status:-0}" -eq 0 ]; then - # Keep slow/live cmd/ao command smokes out of the default unit-tier race - # lane while preserving them as explicit push-to-main proof. - cmdao_integration_tests='TestOrchestrate(Tools|Preflight)ExecuteJSON' - echo "pre-push: push-to-main — running cmd/ao integration race shard (-tags=integration)" >&2 - if ! ( cd "${toplevel}/cli" \ - && unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_PREFIX GIT_OBJECT_DIRECTORY GIT_COMMON_DIR GIT_NAMESPACE \ - && go test ./cmd/ao -tags=integration -run "$cmdao_integration_tests" -race -shuffle=on -count=1 ); then - echo >&2 "✗ cmd/ao integration race shard FAILED — push refused (age-push-equals-ci-0ua.2)" - gate_status=1 - fi - fi -fi - -gate_status="${gate_status:-0}" - -# Run the deterministic gate. Its result means only that repository checks -# succeeded or failed; it is not a semantic verdict or delivery admission. -if [ "$gate_status" -eq 0 ]; then - "$_agentops_ao_gate" gate check --fast - gate_status=$? -fi -exit "$gate_status" diff --git a/scripts/install-ms-reindex-hook.sh b/scripts/install-ms-reindex-hook.sh index 6efe806be..e8f5a140b 100755 --- a/scripts/install-ms-reindex-hook.sh +++ b/scripts/install-ms-reindex-hook.sh @@ -3,10 +3,9 @@ # reindex law (scripts/ms-reindex.sh) on the CANONICAL agentops checkout (age-22g0). # # WHY INSTALL-ON-DEMAND, NOT A TRACKED HOOK: -# AgentOps 3.0 is hookless BY DOCTRINE — no runtime hook ships in-repo (the -# release authority is the cockpit/pawl gate, not git hooks). So this cannot be -# a committed `.githooks/` + core.hooksPath default without contradicting that -# doctrine and firing on every contributor's clone and every linked worktree. +# AgentOps does not own repository Git policy. This maintenance hook therefore +# cannot be a committed `.githooks/` + core.hooksPath default that fires in +# every contributor clone and linked worktree. # Instead we ship the INSTALLER, tracked, and the operator runs it ONCE on the # canonical checkout. The hook it writes is heavily GUARDED so it is a no-op # anywhere except the canonical checkout on main after a merge that touched diff --git a/scripts/install-pre-push-gate.sh b/scripts/install-pre-push-gate.sh deleted file mode 100755 index 3589434a7..000000000 --- a/scripts/install-pre-push-gate.sh +++ /dev/null @@ -1,229 +0,0 @@ -#!/usr/bin/env bash -# install-pre-push-gate.sh — wire the ordinary deterministic pre-push checks. -# -# Idempotent. Installs scripts/hooks/pre-push.local into the SHARED git hooks dir -# (git-common-dir, so it covers the main checkout and every linked worktree at -# once) and chains it from the beads-managed pre-push hook — appended AFTER the -# beads END marker so beads' managed-section rewrites never clobber it. -# -# Re-run anytime (e.g. after a fresh clone). Bypass at push time is the audited -# AGENTOPS_GATE_DISABLED=1 only. -set -euo pipefail - -# Resolve repo_root from the script's own location, NOT `git rev-parse -# --show-toplevel` (age-4st3): --show-toplevel fails from a linked worktree when -# the shared config carries a core.bare pollution ("fatal: this operation must be -# run in a work tree"), which left the installer unrunnable from worktrees — the -# exact place the concurrency-safe workflow runs. Script location is robust to it. -script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -repo_root="$(cd "${script_dir}/.." && pwd)" -common="$(git rev-parse --git-common-dir)" -case "$common" in - /*) ;; - *) common="$(cd "$common" && pwd)" ;; -esac -hooks_dir="${common}/hooks" - -# Guard against a hijacked core.hooksPath (recon-2026-07-02 W4 / audit A2). When -# core.hooksPath is set, git runs THAT dir and IGNORES ${common}/hooks entirely — -# so a bd install (or any tool) that redirects core.hooksPath at its own dir (e.g. -# .beads/hooks) leaves the gate we install here INERT: the installer "succeeds" -# but ao gate never runs on push. Detect it and refuse, so the operator fixes the -# redirect instead of trusting a gate that will not fire. Opt out with -# AGENTOPS_ALLOW_HOOKSPATH=1 (installs anyway, knowing the gate stays inert). -hookspath="$(git config --get core.hooksPath 2>/dev/null || true)" -if [[ -n "$hookspath" ]]; then - case "$hookspath" in - /*) resolved_hookspath="$hookspath" ;; - *) resolved_hookspath="$(cd "$repo_root" 2>/dev/null && cd "$hookspath" 2>/dev/null && pwd || echo "$hookspath")" ;; - esac - if [[ "$resolved_hookspath" != "$hooks_dir" ]]; then - echo "!! core.hooksPath is set to '${hookspath}' (resolved: ${resolved_hookspath})," >&2 - echo " but this gate installs into '${hooks_dir}'. git runs the hooksPath dir and" >&2 - echo " IGNORES ${hooks_dir} — the gate would be installed but INERT on push." >&2 - echo "" >&2 - echo " Remediation (pick one):" >&2 - echo " * git config --unset core.hooksPath # then re-run this installer" >&2 - echo " * or chain scripts/hooks/pre-push.local from '${resolved_hookspath}/pre-push'" >&2 - echo " Override (install anyway, gate stays inert): AGENTOPS_ALLOW_HOOKSPATH=1" >&2 - if [[ "${AGENTOPS_ALLOW_HOOKSPATH:-0}" != "1" ]]; then - echo "ERROR: refusing to install an inert gate. Fix core.hooksPath or set AGENTOPS_ALLOW_HOOKSPATH=1." >&2 - exit 1 - fi - echo " AGENTOPS_ALLOW_HOOKSPATH=1 set — installing anyway; the gate will NOT run until core.hooksPath is fixed." >&2 - fi -fi - -mkdir -p "$hooks_dir" - -src="${repo_root}/scripts/hooks/pre-push.local" -dst="${hooks_dir}/pre-push.local" -if [[ ! -f "$src" ]]; then - echo "ERROR: source hook ${src} missing" >&2 - exit 1 -fi -install -m 0755 "$src" "$dst" -echo "✓ installed ${dst}" - -hook="${hooks_dir}/pre-push" -preamble_marker="# --- AGENTOPS PRE-PUSH STDIN SNAPSHOT (managed by install-pre-push-gate.sh) ---" -preamble_end_marker="# --- END AGENTOPS PRE-PUSH STDIN SNAPSHOT ---" -preamble_block="${preamble_marker} -_agentops_pre_push_stdin=\"\" -_agentops_cleanup_pre_push_stdin() { - [ -n \"\${_agentops_pre_push_stdin:-}\" ] && rm -f \"\$_agentops_pre_push_stdin\" 2>/dev/null || true -} -trap _agentops_cleanup_pre_push_stdin EXIT HUP INT TERM -if [ ! -t 0 ]; then - _agentops_pre_push_stdin=\"\$(mktemp \"\${TMPDIR:-/tmp}/agentops-installed-prepush-stdin.XXXXXX\" 2>/dev/null)\" || _agentops_pre_push_stdin=\"\" - if [ -n \"\$_agentops_pre_push_stdin\" ]; then - cat >\"\$_agentops_pre_push_stdin\" || _agentops_pre_push_stdin=\"\" - [ -n \"\$_agentops_pre_push_stdin\" ] && exec < \"\$_agentops_pre_push_stdin\" - fi -fi -${preamble_end_marker}" -marker="# --- AGENTOPS PRE-PUSH GATE (managed by install-pre-push-gate.sh) ---" -end_marker="# --- END AGENTOPS PRE-PUSH GATE ---" -chain_block="${marker} -_agentops_common=\"\$(git rev-parse --git-common-dir)\" -_agentops_local=\"\${_agentops_common}/hooks/pre-push.local\" -# Self-heal (age-4st3): a landed gate fix is inert until the installed copy is -# refreshed, and NOTHING re-installs it automatically — so a stale gate runs -# indefinitely. Before gating, refresh pre-push.local from TRUNK (origin/main) -# when it drifts. Trust the trunk gate by default. The only branch-side -# exception is an explicit fast-forward push to main/master whose installed hook -# already matches the pushed local SHA; that lets hook changes dogfood their own -# landing push without letting stale or uninstalled branch hooks weaken the gate. -# Best-effort: if trunk is unavailable the existing copy still runs. -_agentops_push_local_sha=\"\" -_agentops_push_remote_sha=\"\" -if [ -n \"\${_agentops_pre_push_stdin:-}\" ] && [ -f \"\$_agentops_pre_push_stdin\" ]; then - _agentops_push_row=\"\$(awk '\$3 ~ /^refs\\/heads\\/(main|master)\$/ { print \$2 \" \" \$4; exit }' \"\$_agentops_pre_push_stdin\" 2>/dev/null)\" - case \"\$_agentops_push_row\" in - *' '*) - _agentops_push_local_sha=\${_agentops_push_row%% *} - _agentops_push_remote_sha=\${_agentops_push_row#* } - ;; - esac -fi -_agentops_trunk_ref=\"origin/main\" -case \"\$_agentops_push_remote_sha\" in - ''|0000000000000000000000000000000000000000) ;; - *) _agentops_trunk_ref=\"\$_agentops_push_remote_sha\" ;; -esac -_agentops_candidate_ref=\"\$_agentops_push_local_sha\" -_agentops_trunk_blob=\"\$(git rev-parse \"\${_agentops_trunk_ref}:scripts/hooks/pre-push.local\" 2>/dev/null)\" -_agentops_candidate_blob=\"\" -if [ -n \"\$_agentops_candidate_ref\" ]; then - _agentops_candidate_blob=\"\$(git rev-parse \"\${_agentops_candidate_ref}:scripts/hooks/pre-push.local\" 2>/dev/null)\" -fi -_agentops_local_blob=\"\" -if [ -f \"\$_agentops_local\" ]; then - _agentops_local_blob=\"\$(git hash-object \"\$_agentops_local\" 2>/dev/null)\" -fi -_agentops_preserve_candidate_hook=0 -if [ -n \"\$_agentops_push_local_sha\" ] && [ -n \"\$_agentops_push_remote_sha\" ] \ - && [ -n \"\$_agentops_trunk_blob\" ] && [ -n \"\$_agentops_candidate_blob\" ] \ - && git merge-base --is-ancestor \"\$_agentops_push_remote_sha\" \"\$_agentops_push_local_sha\" 2>/dev/null \ - && [ \"\$_agentops_local_blob\" = \"\$_agentops_candidate_blob\" ] \ - && [ \"\$_agentops_local_blob\" != \"\$_agentops_trunk_blob\" ]; then - _agentops_preserve_candidate_hook=1 - echo >&2 \"pre-push: hook-source=local-sha:\$_agentops_push_local_sha\" -fi -if [ \"\$_agentops_preserve_candidate_hook\" != \"1\" ] \ - && [ -n \"\$_agentops_trunk_blob\" ] \ - && [ \"\$_agentops_local_blob\" != \"\$_agentops_trunk_blob\" ]; then - _agentops_tmp_local=\"\$(mktemp \"\${_agentops_local}.XXXXXX\" 2>/dev/null)\" || _agentops_tmp_local=\"\" - if [ -n \"\$_agentops_tmp_local\" ]; then - if git show \"\${_agentops_trunk_ref}:scripts/hooks/pre-push.local\" > \"\$_agentops_tmp_local\" 2>/dev/null && chmod +x \"\$_agentops_tmp_local\"; then - mv \"\$_agentops_tmp_local\" \"\$_agentops_local\" - echo >&2 \"pre-push: hook-source=trunk:\$_agentops_trunk_ref\" - else - rm -f \"\$_agentops_tmp_local\" - fi - fi -fi -if [ -x \"\$_agentops_local\" ]; then - if [ -n \"\${_agentops_pre_push_stdin:-}\" ] && [ -f \"\$_agentops_pre_push_stdin\" ]; then - \"\$_agentops_local\" \"\$@\" < \"\$_agentops_pre_push_stdin\" || exit \$? - else - \"\$_agentops_local\" \"\$@\" || exit \$? - fi -fi -${end_marker}" - -if [[ ! -f "$hook" ]]; then - printf '#!/usr/bin/env sh\n%s\n%s\n' "$preamble_block" "$chain_block" > "$hook" - chmod +x "$hook" - echo "✓ created ${hook} with gate chain" -else - if grep -qF "$preamble_marker" "$hook"; then - HOOK="$hook" MARKER="$preamble_marker" END_MARKER="$preamble_end_marker" CHAIN_BLOCK="$preamble_block" \ - python3 - <<'PY' -import os, re -hook = os.environ["HOOK"] -text = open(hook, encoding="utf-8").read() -pattern = re.compile(re.escape(os.environ["MARKER"]) + r".*?" + re.escape(os.environ["END_MARKER"]), re.DOTALL) -new, n = pattern.subn(os.environ["CHAIN_BLOCK"], text, count=1) -if n: - open(hook, "w", encoding="utf-8").write(new) -PY - else - tmp="$(mktemp "${hook}.XXXXXX")" - { - head -n 1 "$hook" - printf '%s\n' "$preamble_block" - tail -n +2 "$hook" - } > "$tmp" - mv "$tmp" "$hook" - fi - - if grep -qF "$marker" "$hook"; then - # Replace the existing block (between markers) so a stale chainer — e.g. one - # without the self-heal above — is refreshed on reinstall instead of a no-op. - # Python (not awk -v: that can't take a multi-line replacement value). - HOOK="$hook" MARKER="$marker" END_MARKER="$end_marker" CHAIN_BLOCK="$chain_block" \ - python3 - <<'PY' -import os, re -hook = os.environ["HOOK"] -text = open(hook, encoding="utf-8").read() -pattern = re.compile(re.escape(os.environ["MARKER"]) + r".*?" + re.escape(os.environ["END_MARKER"]), re.DOTALL) -new, n = pattern.subn(os.environ["CHAIN_BLOCK"], text, count=1) -if n: - open(hook, "w", encoding="utf-8").write(new) -PY - chmod +x "$hook" - echo "✓ refreshed gate chain in ${hook} (replaced managed block)" - else - printf '\n%s\n' "$chain_block" >> "$hook" - echo "✓ appended gate chain to existing ${hook} (after beads section)" - fi - chmod +x "$hook" -fi - -# Runtime-file rebase ergonomics (age-uqj). This repo tracks runtime audit logs -# (.agents/rpi/next-work.jsonl, .agents/findings/registry.jsonl, the provenance -# ledger) that tooling dirties mid-session, so a plain `git pull --rebase` fails -# with "cannot rebase: you have unstaged changes". autoStash makes rebase -# transparently stash the dirty tree and reapply it afterward. These logs are -# intentionally NOT union-merged (they are mutated in place / hash-chained — see -# .gitattributes), so a genuine divergence still surfaces as a real conflict on -# reapply rather than being silently merged. Repo-local only; never touches global. -git config --local rebase.autoStash true -echo "✓ set rebase.autoStash=true (local) — pull --rebase no longer blocks on dirty runtime logs" - -# Heal core.bare pollution (age-4st3). A test that flips core.bare=true and fails -# to reset it (the gate's own "open core.bare pollution at shuffle=2") leaves the -# SHARED config bare=true, which makes every git op in the main checkout AND all -# linked worktrees fail with "fatal: this operation must be run in a work tree" — -# silently freezing worktree commits/pushes (the concurrency-safe workflow). This -# repo is never bare, so force it false. (The leaking test still needs fixing — a -# separate bead — this just stops the pollution from bricking worktrees.) -if [[ "$(git config --local --get core.bare 2>/dev/null)" == "true" ]]; then - git config --local core.bare false - echo "✓ healed core.bare=true → false (was bricking worktree git ops)" -fi - -echo "" -echo "Deterministic pre-push checks active for this repo (all worktrees)." -echo "Audited bypass (logged): AGENTOPS_GATE_DISABLED=1 git push ..." diff --git a/scripts/install.sh b/scripts/install.sh index d841d65f9..277241395 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -72,22 +72,14 @@ install_dev() { local repo_root repo_root="$(cd "$SCRIPT_DIR/.." && pwd)" - if [[ ! -f "$repo_root/scripts/install-dev-hooks.sh" || ! -d "$repo_root/cli" ]]; then + if [[ ! -d "$repo_root/cli" ]]; then echo "Error: --dev must be run from an AgentOps source checkout." >&2 exit 1 fi - echo "Installing AgentOps development wiring..." - echo "Step 1/2: Configuring repo-managed git hooks..." - bash "$repo_root/scripts/install-dev-hooks.sh" - - echo "Step 2/2: Building cli/bin/ao..." + echo "Building the AgentOps development CLI..." make -C "$repo_root/cli" build - # Pre-push gate-wiring verification retired (soc-bbvw / soc-g2r9): - # local pre-push gate retired; CI is sole authoritative push gate. - # See docs/contracts/local-pre-push-gate-retirement.md. - echo "" echo "Done! Development checkout ready." } diff --git a/scripts/lib/ao-snippet-resolve.sh b/scripts/lib/ao-snippet-resolve.sh index 3ba03c183..d18060503 100644 --- a/scripts/lib/ao-snippet-resolve.sh +++ b/scripts/lib/ao-snippet-resolve.sh @@ -35,11 +35,8 @@ export AO_SNIPPET_LIB_DIR # for snippet resolution and export it as AO_BIN. # # Honors an already-set AGENTOPS_AO_BIN (fast path for CI / tests). Otherwise -# builds `ao` from <REPO_ROOT>/cli with the ADR-0012 archive tags -# (`-tags "flywheel legacy"`) so snippets that document archived-but-revivable -# commands (e.g. `ao harvest`, `ao forge`, behind //go:build flywheel|legacy) -# still resolve — the default spine build omits them and would false-fail those -# (two prior escapes). The built binary lands in a mktemp dir; the CALLER is +# builds the one supported `ao` command tree from <REPO_ROOT>/cli. The built +# binary lands in a mktemp dir; the CALLER is # responsible for its own EXIT trap cleanup if it wants the temp removed. # # REPO_ROOT defaults to the parent of this lib's dir's parent (…/scripts/lib → @@ -58,7 +55,7 @@ ao_snippet_resolve_bin() { ao_bin="$tmp_dir/ao" ( cd "$repo_root/cli" || exit 1 - go build -tags "flywheel legacy" -o "$ao_bin" ./cmd/ao + go build -o "$ao_bin" ./cmd/ao ) # Surface the temp dir so the caller can trap-clean it. AO_SNIPPET_TMP_DIR="$tmp_dir" diff --git a/scripts/lib/ratchet.sh b/scripts/lib/ratchet.sh index 65d05469f..211a482f8 100644 --- a/scripts/lib/ratchet.sh +++ b/scripts/lib/ratchet.sh @@ -10,7 +10,7 @@ # NEW violations fail the gate; the list only SHRINKS. Only a gate changes # behavior — this is the repo's own law, extracted. # -# DESIGN RULES (pre-mortem hardened, .agents/council/2026-07-10-pre-mortem-ratchet-lib.md): +# DESIGN RULES (premortem hardened, .agents/council/2026-07-10-premortem-ratchet-lib.md): # * Functions only — no top-level `set -e`/`set -u`, no preamble source # (libs are strict-mode-agnostic by convention; see lib/bats-common.bash). # Callers own strict mode and REPO_ROOT anchoring. diff --git a/scripts/lib/resolve-skill-path.sh b/scripts/lib/resolve-skill-path.sh deleted file mode 100644 index 01649d173..000000000 --- a/scripts/lib/resolve-skill-path.sh +++ /dev/null @@ -1,84 +0,0 @@ -#!/usr/bin/env bash -# resolve-skill-path.sh — shared skill-path resolver over the dispositions ledger (ag-2vz5v). -# -# resolve_skill_path <path> -# Reads the `historical:` section of docs/contracts/skill-dispositions.yaml -# (flat-format line parsing — awk only, no yq) and routes the path's -# skills/<slug>/ or skills-codex/<slug>/ segment through it: -# state: merged-into -> prints the path with the slug rewritten to the target -# state: cut -> prints nothing, warns on stderr (caller skips visibly), -# returns 0 -# no historical row -> prints the path unchanged (byte-identical) -# Paths without a skills/<slug>/ segment, a missing ledger, or a malformed -# row all degrade to identity. Slug matching is exact (a `plan` row never -# rewrites `plan-foundry` paths). -# -# Test seam: set SKILL_DISPOSITIONS_FILE to point at a fixture ledger. -# -# Sourced by validate-codex-rpi-contract.sh, validate-codex-lifecycle-guards.sh, -# check-hookless-cold-start.sh, and the cockpit pre-push path. These ARE the pre-push -# gate: on a repo where no historical row matches a routed path, behavior must -# stay byte-identical. - -_RESOLVE_SKILL_PATH_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - -resolve_skill_path() { - local path="$1" - local ledger="${SKILL_DISPOSITIONS_FILE:-$_RESOLVE_SKILL_PATH_LIB_DIR/../../docs/contracts/skill-dispositions.yaml}" - - # Identity: no ledger, or no skills/<slug>/ segment to resolve. - if [[ ! -f "$ledger" ]] || \ - [[ ! "$path" =~ ^(.*/)?(skills|skills-codex)/([^/]+)(/.*)$ ]]; then - printf '%s\n' "$path" - return 0 - fi - local pre="${BASH_REMATCH[1]}" - local tree="${BASH_REMATCH[2]}" - local slug="${BASH_REMATCH[3]}" - local rest="${BASH_REMATCH[4]}" - - # Flat-format line parse of the historical: section. Entries are - # 2-space-indented ` <slug>:` keys with 4-space-indented fields; - # the section ends at the next top-level key (e.g. dispositions:). - local row - row="$(awk -v slug="$slug" ' - /^historical:/ { in_hist = 1; next } - in_hist && /^[^ #]/ { in_hist = 0 } - !in_hist { next } - /^ [^ #:]+:[[:space:]]*$/ { - cur = $0 - sub(/^ /, "", cur) - sub(/:[[:space:]]*$/, "", cur) - in_slug = (cur == slug) - next - } - in_slug && $1 == "state:" { state = $2 } - in_slug && $1 == "merged-into:" { target = $2 } - END { if (state != "") print state "\t" target } - ' "$ledger")" - - if [[ -z "$row" ]]; then - printf '%s\n' "$path" - return 0 - fi - local state="${row%%$'\t'*}" - local target="${row#*$'\t'}" - - case "$state" in - merged-into) - if [[ -z "$target" ]]; then - # Malformed row (merged-into without a target): identity. - printf '%s\n' "$path" - else - printf '%s%s/%s%s\n' "$pre" "$tree" "$target" "$rest" - fi - ;; - cut) - echo "resolve_skill_path: skill '$slug' is cut in the dispositions ledger; skipping $path" >&2 - ;; - *) - printf '%s\n' "$path" - ;; - esac - return 0 -} diff --git a/scripts/lib/sku_extract.py b/scripts/lib/sku_extract.py index 8ce5b553c..bc7d969ed 100755 --- a/scripts/lib/sku_extract.py +++ b/scripts/lib/sku_extract.py @@ -143,7 +143,7 @@ def _run_help(ao_bin: str, command: List[str]) -> subprocess.CompletedProcess: # Cobra sections that head NON-command blocks. Any other column-0 line ending with # ":" is a command block: either the default "Available/Additional Commands:" or a -# custom cobra.Group title (e.g. `ao pawl`'s front-door/operator split), whose titles +# custom cobra.Group title, whose titles # are arbitrary prose and need not contain the word "Commands". _NON_COMMAND_SECTIONS = { "Usage:", @@ -172,7 +172,7 @@ def _parse_subcommands(help_text: str) -> List[str]: in_block = False continue # Command rows render as " <name><2+ spaces><short>"; the >=2-space gap - # keeps Examples-style " ao pawl ..." lines from parsing as commands. + # keeps Examples-style " ao example ..." lines from parsing as commands. m = re.match(r"^\s+([a-z][a-z0-9-]*)(\s{2,}|\s*$)", line) if m and m.group(1) not in {"help", "completion"}: subs.append(m.group(1)) diff --git a/scripts/lint-codex-native.sh b/scripts/lint-codex-native.sh index 1a6a2d2a2..567a291a5 100755 --- a/scripts/lint-codex-native.sh +++ b/scripts/lint-codex-native.sh @@ -72,8 +72,8 @@ pass() { fi } -# Known skill names for slash-command detection (pipe-separated) -SKILL_NAMES="research|plan|pre-mortem|implement|crank|swarm|council|vibe|post-mortem|retro|evolve|release|status|goals|ratchet|rpi|brainstorm|bug-hunt|doc|knowledge|learn|extract|flywheel|handoff|recover|trace|provenance|beads|quickstart|readme|security|complexity|codex-team|pr-research|pr-plan|pr-implement|pr-validate|pr-prep|pr-retro|oss-docs|openai-docs|heal-skill|converter|update|product|reverse-engineer-rpi|standards|shared|using-agentops" +# Current source metadata owns the slash-invocation vocabulary. +SKILL_NAMES="$(find "$REPO_ROOT/skills" -mindepth 2 -maxdepth 2 -name SKILL.md -print | sed 's#/SKILL.md$##; s#^.*/##' | LC_ALL=C sort | paste -sd '|' -)" # Claude-only primitives (should not appear in main execution flow) CLAUDE_PRIMITIVES="TeamCreate|SendMessage|EnterPlanMode|ExitPlanMode|EnterWorktree" diff --git a/scripts/lint-evidence-lines.sh b/scripts/lint-evidence-lines.sh deleted file mode 100755 index d64f1b576..000000000 --- a/scripts/lint-evidence-lines.sh +++ /dev/null @@ -1,213 +0,0 @@ -#!/usr/bin/env bash -# lint-evidence-lines.sh — pre-flight check for PR-body `Evidence:` lines. -# -# Anti-pattern #7 (AP#7, see scripts/verify-gate-claim.sh) requires that each -# `Evidence:` line in a PR body appears verbatim in the workflow's job logs. -# The most common failure mode is parenthetical narration appended to an -# otherwise-correct path: -# -# Evidence: tests/scripts/foo.bats (10/10 passing) ← AP#7 will fail -# Evidence: tests/scripts/foo.bats ← AP#7 will pass -# -# CI logs never contain "(10/10 passing)" or similar prose, so the verbatim -# match misses. This script runs the same extraction regex CI uses -# (`sed -n 's/^Evidence:[[:space:]]*//p'`) and flags each extracted claim for -# AP#7-incompatible content with a clear remediation message. -# -# Inputs: -# <pr-number> — fetch via `gh pr view <N> --json body` -# --body <file> — read body from a local file (testing / pre-push hook) -# --stdin — read body from stdin -# -# Flags: -# --json — machine-readable output (one record per claim) -# --strict — non-zero exit when even advisory issues found -# (default: only "blocking" issues — parens, empty, -# markdown table — cause non-zero exit) -# -# Exit codes: -# 0 — no blocking issues -# 1 — at least one blocking issue found -# 2 — usage error -# 3 — input fetch failed (gh missing, no body, etc.) -# -# Standalone — no project dependencies. Designed to run pre-push or as an -# advisory CI step before validate-pr-evidence-claims. - -set -euo pipefail - -MODE="pr" -PR_NUM="" -BODY_FILE="" -JSON=0 -STRICT=0 - -usage() { - sed -n '2,/^$/p' "$0" | sed 's/^# \{0,1\}//' - exit "${1:-0}" -} - -while [ $# -gt 0 ]; do - case "$1" in - --body) shift; MODE="file"; BODY_FILE="${1:-}" ;; - --stdin) MODE="stdin" ;; - --json) JSON=1 ;; - --strict) STRICT=1 ;; - -h|--help) usage 0 ;; - --*) echo "lint-evidence-lines: unknown flag: $1" >&2; usage 2 ;; - *) PR_NUM="$1" ;; - esac - shift || true -done - -read_body() { - case "$MODE" in - pr) - if [ -z "$PR_NUM" ]; then - echo "lint-evidence-lines: missing <pr-number>" >&2 - usage 2 - fi - if ! command -v gh >/dev/null 2>&1; then - echo "lint-evidence-lines: gh CLI not available" >&2 - exit 3 - fi - gh pr view "$PR_NUM" --json body --jq .body 2>/dev/null - ;; - file) - if [ -z "$BODY_FILE" ] || [ ! -r "$BODY_FILE" ]; then - echo "lint-evidence-lines: cannot read body file: $BODY_FILE" >&2 - exit 3 - fi - cat "$BODY_FILE" - ;; - stdin) - cat - - ;; - esac -} - -body="$(read_body)" -if [ -z "$body" ]; then - echo "lint-evidence-lines: empty body" >&2 - exit 3 -fi - -# Match CI's exact extraction logic so we lint exactly what AP#7 will see. -# `tee` into a tmpfile so we can iterate the claims twice. -claims_file="$(mktemp)" -trap 'rm -f "$claims_file"' EXIT - -printf '%s\n' "$body" | sed -n 's/^Evidence:[[:space:]]*//p' > "$claims_file" - -if [ ! -s "$claims_file" ]; then - # No Evidence: lines at all. AP#7 skips in this case — informational only. - if [ "$JSON" -eq 1 ]; then - echo '{"claims": [], "blocking": 0, "advisory": 0, "skipped_by_ap7": true}' - else - echo "lint-evidence-lines: no Evidence: lines in body — AP#7 will skip" - fi - exit 0 -fi - -# Classify each claim. The detection rules are tuned to the AP#7 failure -# modes we've seen in practice; expand the table as new patterns surface. -declare -a records=() -blocking=0 -advisory=0 - -# Tabs and double-spaces tend to drop from CI log lines, so warn about them. -# Markdown emphasis (`*`, `_`, backtick) breaks verbatim match. Pipe (`|`) -# only matters inside a markdown table cell but is a common copy-paste sin. -classify_claim() { - local claim="$1" - local issues=() - local severity="ok" - - if [ -z "$(printf '%s' "$claim" | tr -d '[:space:]')" ]; then - issues+=("empty:Evidence: line has no content; AP#7 will fail") - severity="blocking" - fi - if printf '%s' "$claim" | grep -q '([^)]*)'; then - local paren - paren="$(printf '%s' "$claim" | grep -oE '\([^)]*\)' | head -1)" - issues+=("parens:contains parenthetical \"$paren\" — CI logs don't carry prose; strip the parens") - severity="blocking" - fi - if printf '%s' "$claim" | grep -qE '\*\*|__|\*[^*]|`'; then - issues+=("markdown:contains markdown emphasis/backtick — verbatim match will miss") - severity="blocking" - fi - if printf '%s' "$claim" | grep -q '|'; then - issues+=("pipe:contains a markdown table separator '|' — verbatim match likely to miss") - severity="blocking" - fi - if printf '%s' "$claim" | grep -qE '[[:space:]]$'; then - issues+=("trailing-ws:line ends in whitespace — sed extracts it as part of the claim") - severity="advisory" - fi - if printf '%s' "$claim" | grep -qE '^\s*(see|cf|note:)'; then - issues+=("prose:line opens with prose keyword — likely commentary not a path") - severity="blocking" - fi - - if [ "${#issues[@]}" -eq 0 ]; then - records+=("$(printf '{"claim":%s,"severity":"ok","issues":[]}' "$(json_string "$claim")")") - return - fi - if [ "$severity" = "blocking" ]; then - blocking=$((blocking + 1)) - else - advisory=$((advisory + 1)) - fi - local issues_json - issues_json="$(printf '%s\n' "${issues[@]}" | jq -R . | jq -sc .)" - records+=("$(printf '{"claim":%s,"severity":"%s","issues":%s}' \ - "$(json_string "$claim")" "$severity" "$issues_json")") -} - -json_string() { - # Escape a bash string for JSON inclusion. - printf '%s' "$1" | jq -Rs . -} - -while IFS= read -r claim; do - classify_claim "$claim" -done < "$claims_file" - -if [ "$JSON" -eq 1 ]; then - printf '{"claims":[%s],"blocking":%d,"advisory":%d,"skipped_by_ap7":false}\n' \ - "$(IFS=,; echo "${records[*]}")" \ - "$blocking" "$advisory" -else - total="$(wc -l < "$claims_file" | tr -d ' ')" - echo "lint-evidence-lines: $total Evidence: line(s) inspected; $blocking blocking, $advisory advisory" - i=0 - while IFS= read -r claim; do - i=$((i + 1)) - rec="${records[i-1]}" - sev="$(printf '%s' "$rec" | jq -r .severity)" - case "$sev" in - ok) echo " OK [$i] $claim" ;; - advisory) - echo " WARN [$i] $claim" - printf '%s' "$rec" | jq -r '.issues[]' | sed 's/^/ /' - ;; - blocking) - echo " FAIL [$i] $claim" - printf '%s' "$rec" | jq -r '.issues[]' | sed 's/^/ /' - ;; - esac - done < "$claims_file" - if [ "$blocking" -gt 0 ] || { [ "$STRICT" -eq 1 ] && [ "$advisory" -gt 0 ]; }; then - echo - echo "lint-evidence-lines: see .agents/learnings/2026-05-20-ap7-evidence-line-format.md" - fi -fi - -if [ "$blocking" -gt 0 ]; then - exit 1 -fi -if [ "$STRICT" -eq 1 ] && [ "$advisory" -gt 0 ]; then - exit 1 -fi -exit 0 diff --git a/scripts/mirror-codex-references.sh b/scripts/mirror-codex-references.sh index 2ad2e5481..ac3b1e68d 100755 --- a/scripts/mirror-codex-references.sh +++ b/scripts/mirror-codex-references.sh @@ -4,7 +4,7 @@ set -euo pipefail # Mirror reference files from skills/ to skills-codex/ and update SKILL.md links. # # Usage: -# scripts/mirror-codex-references.sh council crank # mirror specific skills +# scripts/mirror-codex-references.sh council premortem # mirror specific skills # scripts/mirror-codex-references.sh --all # mirror all skills # scripts/mirror-codex-references.sh --dry-run --all # preview without changes # scripts/mirror-codex-references.sh --dry-run council # preview one skill @@ -35,7 +35,7 @@ Options: -h, --help Show this help Examples: - scripts/mirror-codex-references.sh council crank + scripts/mirror-codex-references.sh council premortem scripts/mirror-codex-references.sh --dry-run --all USAGE exit 0 diff --git a/scripts/nightly-pr-digest.sh b/scripts/nightly-pr-digest.sh deleted file mode 100755 index 6496fe59a..000000000 --- a/scripts/nightly-pr-digest.sh +++ /dev/null @@ -1,435 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -usage() { - cat <<'EOF' -nightly-pr-digest.sh - -Generate a structured PR body from nightly evolution run state. - -Reads the most recent nightly run directory and produces a markdown PR body -covering: baseline/final goals, cycle history, runtime-artifact flips, open PR -overlap, tracker degradation, transient flakes, auto-reverts, and tag-push -status. - -Options: - --run-dir <path> Explicit nightly run directory (default: latest under .agents/nightly/) - --repo-root <path> Repository root (default: git top-level or cwd) - --branch <name> Branch name for the PR (default: read from run digest.json) - --output <path> Output file for PR body markdown (default: stdout) - --baseline-label <slug> Baseline label to compare against (default: latest) - --since <date> Git log start date for auto-revert scan (default: 24h ago) - --format <fmt> Output format: markdown|json (default: markdown) - -h, --help Show this help -EOF -} - -die() { echo "nightly-pr-digest: $*" >&2; exit 1; } -log() { printf '[%s] %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$*" >&2; } - -RUN_DIR="" -REPO_ROOT="" -BRANCH="" -OUTPUT="" -BASELINE_LABEL="" -SINCE="" -FORMAT="markdown" - -while [[ $# -gt 0 ]]; do - case "$1" in - --run-dir) RUN_DIR="${2:-}"; shift 2 ;; - --repo-root) REPO_ROOT="${2:-}"; shift 2 ;; - --branch) BRANCH="${2:-}"; shift 2 ;; - --output) OUTPUT="${2:-}"; shift 2 ;; - --baseline-label) BASELINE_LABEL="${2:-}"; shift 2 ;; - --since) SINCE="${2:-}"; shift 2 ;; - --format) FORMAT="${2:-}"; shift 2 ;; - -h|--help) usage; exit 0 ;; - *) die "unknown arg: $1" ;; - esac -done - -if [[ -z "$REPO_ROOT" ]]; then - REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" -fi -cd "$REPO_ROOT" - -if [[ -z "$RUN_DIR" ]]; then - RUN_DIR="$(find .agents/nightly -mindepth 2 -maxdepth 2 -type d 2>/dev/null | sort | tail -1)" - [[ -n "$RUN_DIR" ]] || die "no nightly run directories found under .agents/nightly/" -fi -[[ -d "$RUN_DIR" ]] || die "run directory does not exist: $RUN_DIR" - -DIGEST_JSON="$RUN_DIR/digest.json" -[[ -f "$DIGEST_JSON" ]] || die "digest.json not found in $RUN_DIR" - -if [[ -z "$SINCE" ]]; then - SINCE="$(date -u -d '24 hours ago' +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -v-24H +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo '')" -fi - -if [[ -z "$BRANCH" ]]; then - BRANCH="$(jq -r '.planned_branch // ""' "$DIGEST_JSON")" -fi - -# ──── Section builders ──── - -build_header() { - local run_id mode run_date generated_at - run_id="$(jq -r '.run_id // "unknown"' "$DIGEST_JSON")" - mode="$(jq -r '.mode // "unknown"' "$DIGEST_JSON")" - run_date="$(jq -r '.run_date // "unknown"' "$DIGEST_JSON")" - generated_at="$(jq -r '.generated_at // "unknown"' "$DIGEST_JSON")" - - printf '## Nightly Evolution — %s\n\n' "$run_date" - printf '| Field | Value |\n|---|---|\n' - printf '| Run ID | `%s` |\n' "$run_id" - printf '| Mode | %s |\n' "$mode" - printf '| Branch | `%s` |\n' "$BRANCH" - printf '| Generated | %s |\n\n' "$generated_at" -} - -build_goals_section() { - local history_file=".agents/evolve/cycle-history.jsonl" - local baseline_dir=".agents/evolve/fitness-baselines" - - printf '### Goals (Baseline → Final)\n\n' - - local baseline_goals="" - if [[ -n "$BASELINE_LABEL" && -d "$baseline_dir/$BASELINE_LABEL" ]]; then - local baseline_file - baseline_file="$(find "$baseline_dir/$BASELINE_LABEL" -name '*.json' | sort | tail -1)" - if [[ -n "$baseline_file" ]]; then - baseline_goals="$(jq -r '.goals | length' "$baseline_file" 2>/dev/null || echo '?')" - local baseline_passing - baseline_passing="$(jq -r '[.goals[] | select(.status == "passing")] | length' "$baseline_file" 2>/dev/null || echo '?')" - printf -- '- **Baseline** (`%s`): %s/%s passing\n' "$BASELINE_LABEL" "$baseline_passing" "$baseline_goals" - fi - elif [[ -d "$baseline_dir" ]]; then - local latest_label - # `-printf '%f\n'` is a GNU-find extension; BSD/macOS find errors on it - # ("unknown primary"), and with `set -euo pipefail` the suppressed-stderr - # failure still propagates (pipefail) and kills this assignment -> exit 1. - # `-exec basename {} \;` is the POSIX-portable equivalent (one basename/line). - latest_label="$(find "$baseline_dir" -maxdepth 1 -mindepth 1 -type d -exec basename {} \; 2>/dev/null | sort | tail -1)" - if [[ -n "$latest_label" ]]; then - local latest_file - latest_file="$(find "$baseline_dir/$latest_label" -name '*.json' 2>/dev/null | sort | tail -1)" - if [[ -n "$latest_file" ]]; then - baseline_goals="$(jq -r '.goals | length' "$latest_file" 2>/dev/null || echo '?')" - local baseline_passing - baseline_passing="$(jq -r '[.goals[] | select(.status == "passing")] | length' "$latest_file" 2>/dev/null || echo '?')" - printf -- '- **Baseline** (`%s`): %s/%s passing\n' "$latest_label" "$baseline_passing" "$baseline_goals" - fi - fi - fi - - if [[ -f "$history_file" ]]; then - local last_entry - last_entry="$(tail -1 "$history_file" || true)" - if [[ -n "$last_entry" ]]; then - local goals_passing goals_total - goals_passing="$(echo "$last_entry" | jq -r '.goals_passing // "?"')" - goals_total="$(echo "$last_entry" | jq -r '.goals_total // "?"')" - printf -- '- **Final**: %s/%s passing\n' "$goals_passing" "$goals_total" - fi - fi - - if [[ -z "$baseline_goals" ]]; then - printf -- '- _(no baseline snapshot found)_\n' - fi - printf '\n' -} - -build_cycle_history() { - local history_file=".agents/evolve/cycle-history.jsonl" - printf '### Cycle History\n\n' - - if [[ ! -f "$history_file" ]]; then - printf '_(no cycle-history.jsonl found)_\n\n' - return - fi - - local total improved regressed unchanged quarantined - total="$(wc -l < "$history_file" | tr -d ' ')" - improved="$(command grep -c '"result":"improved"' "$history_file" || true)" - improved="${improved%%[^0-9]*}" - regressed="$(command grep -c '"result":"regressed"' "$history_file" || true)" - regressed="${regressed%%[^0-9]*}" - unchanged="$(command grep -c '"result":"unchanged"' "$history_file" || true)" - unchanged="${unchanged%%[^0-9]*}" - quarantined="$(command grep -c '"result":"quarantined"' "$history_file" || true)" - quarantined="${quarantined%%[^0-9]*}" - : "${improved:=0}" "${regressed:=0}" "${unchanged:=0}" "${quarantined:=0}" - - printf '| Metric | Count |\n|---|---|\n' - printf '| Total cycles | %s |\n' "$total" - printf '| Improved | %s |\n' "$improved" - printf '| Regressed | %s |\n' "$regressed" - printf '| Unchanged | %s |\n' "$unchanged" - printf '| Quarantined | %s |\n\n' "$quarantined" - - printf '<details>\n<summary>Last 10 cycles</summary>\n\n' - printf '| # | Target | Result | Files | Duration |\n|---|---|---|---|---|\n' - tail -10 "$history_file" | while IFS= read -r line; do - [[ -z "$line" ]] && continue - local cycle work_id result files_count duration - cycle="$(echo "$line" | jq -r '.cycle // "?"')" - work_id="$(echo "$line" | jq -r '.work_id // .work_title // "?"')" - result="$(echo "$line" | jq -r '.result // "?"')" - files_count="$(echo "$line" | jq -r '(.files_changed // []) | length')" - duration="$(echo "$line" | jq -r '.duration_min // "?"')" - printf '| %s | `%s` | %s | %s | %sm |\n' "$cycle" "$work_id" "$result" "$files_count" "$duration" - done - printf '\n</details>\n\n' -} - -build_runtime_flips() { - printf '### Runtime-Artifact Flips\n\n' - - local readiness_status - readiness_status="$(jq -r '.readiness.ai_sane_status // "unknown"' "$DIGEST_JSON")" - local dream_status evolve_status - dream_status="$(jq -r '.phases.dream // "not-requested"' "$DIGEST_JSON")" - evolve_status="$(jq -r '.phases.evolve // "not-requested"' "$DIGEST_JSON")" - - printf '| Phase | Status |\n|---|---|\n' - printf '| AI readiness | %s |\n' "$readiness_status" - printf '| Dream | %s |\n' "$dream_status" - printf '| Evolve | %s |\n\n' "$evolve_status" - - local runtime_inventory - runtime_inventory="$(jq -r '.runtime.inventory_tsv // ""' "$DIGEST_JSON")" - if [[ -n "$runtime_inventory" ]]; then - local missing - missing="$(echo "$runtime_inventory" | command grep -c 'false' || true)" - missing="${missing%%[^0-9]*}" - : "${missing:=0}" - if [[ "$missing" -gt 0 ]]; then - printf '**Missing runtimes:** ' - echo "$runtime_inventory" | command grep 'false' | cut -f1 | tr '\n' ',' | sed 's/,$/\n/' - printf '\n' - fi - fi -} - -build_open_pr_overlap() { - printf '### Open PR Overlap\n\n' - - local pr_count - pr_count="$(jq -r '.github.open_prs | length' "$DIGEST_JSON" 2>/dev/null || echo 0)" - if [[ "$pr_count" == "0" ]]; then - printf '_(no open PRs)_\n\n' - return - fi - - printf '| # | Title | Branch | Changed Files |\n|---|---|---|---|\n' - jq -r '.github.open_prs[] | "| #\(.number) | \(.title) | `\(.headRefName)` | \(.changedFiles) |"' "$DIGEST_JSON" 2>/dev/null | head -15 - printf '\n' - - if [[ "$pr_count" -gt 15 ]]; then - printf '_... and %d more_\n\n' "$((pr_count - 15))" - fi -} - -build_tracker_degradation() { - printf '### Tracker Degradation\n\n' - - if ! command -v bd >/dev/null 2>&1; then - printf '_(bd not available)_\n\n' - return - fi - - local open_count in_progress_count - open_count="$(bd list --status=open 2>/dev/null | command grep -c '○' || true)" - open_count="${open_count%%[^0-9]*}" - in_progress_count="$(bd list --status=in_progress 2>/dev/null | command grep -c '◐\|●' || true)" - in_progress_count="${in_progress_count%%[^0-9]*}" - : "${open_count:=0}" "${in_progress_count:=0}" - - printf '| Metric | Count |\n|---|---|\n' - printf '| Open issues | %s |\n' "$open_count" - printf '| In-progress | %s |\n\n' "$in_progress_count" - - local stale_items - stale_items="$(bd list --status=in_progress 2>/dev/null | command grep -E '2026-0[0-3]|2025-' | head -5 || true)" - if [[ -n "$stale_items" ]]; then - printf '**Stale in-progress (>30d):**\n```\n%s\n```\n\n' "$stale_items" - fi -} - -build_transient_flakes() { - printf '### Transient Flakes\n\n' - - local flake_log=".agents/evolve/flakes.jsonl" - if [[ -f "$flake_log" ]]; then - local flake_count - flake_count="$(wc -l < "$flake_log" | tr -d ' ')" - printf -- '- Recorded flakes: %s\n' "$flake_count" - printf '<details>\n<summary>Recent flakes</summary>\n\n' - tail -5 "$flake_log" | while IFS= read -r fline; do - jq -r '"- \(.timestamp // "?"): \(.test // .message // "unknown")"' <<<"$fline" 2>/dev/null || true - done - printf '\n</details>\n\n' - else - local session_state=".agents/evolve/session-state.json" - if [[ -f "$session_state" ]]; then - local last_result - last_result="$(jq -r '.last_cycle_result // "unknown"' "$session_state")" - if [[ "$last_result" == "quarantined" ]]; then - printf -- '- Last cycle was quarantined (possible flake)\n\n' - else - printf '_(no flake log; last cycle result: %s)_\n\n' "$last_result" - fi - else - printf '_(no flake tracking data found)_\n\n' - fi - fi -} - -build_auto_reverts() { - printf '### Auto-Reverts\n\n' - - if [[ -z "$SINCE" ]]; then - printf '_(--since not resolved; skipping)_\n\n' - return - fi - - local revert_count reverts - reverts="$(git log --oneline --since="$SINCE" --grep='[Rr]evert' 2>/dev/null || true)" - if [[ -z "$reverts" ]]; then - printf '_(none in last 24h)_\n\n' - else - revert_count="$(echo "$reverts" | wc -l | tr -d ' ')" - printf '**%s revert(s) in window:**\n```\n%s\n```\n\n' "$revert_count" "$reverts" - fi -} - -build_admission_context() { - printf '### Admission Context\n\n' - - local gh_evidence - gh_evidence="$(jq -r '.admission_context.gh_evidence // "not present"' "$DIGEST_JSON")" - if [[ "$gh_evidence" == "not present" ]]; then - printf '_(no admission context in digest)_\n\n' - return - fi - - local ci_status blocker_count - ci_status="$(jq -r '.admission_context.main_ci_baseline.status // "unknown"' "$DIGEST_JSON")" - blocker_count="$(jq -r '.admission_context.blocker_matrix.prs | length' "$DIGEST_JSON" 2>/dev/null || echo 0)" - - printf '| Field | Value |\n|---|---|\n' - printf '| GitHub evidence | %s |\n' "$gh_evidence" - printf '| Main CI baseline | %s |\n' "$ci_status" - printf '| Open PR blockers | %s |\n' "$blocker_count" - - if [[ "$blocker_count" -gt 0 ]] 2>/dev/null; then - printf '\n**Blocker PRs:**\n\n' - printf '| # | Branch | Files |\n|---|---|---|\n' - jq -r '.admission_context.blocker_matrix.prs[] | "| #\(.pr_number) | `\(.head_ref)` | \(.changed_file_count) |"' "$DIGEST_JSON" 2>/dev/null | head -10 - printf '\n' - fi - - local mode evolve_status - mode="$(jq -r '.mode // "unknown"' "$DIGEST_JSON")" - evolve_status="$(jq -r '.phases.evolve // "not-requested"' "$DIGEST_JSON")" - - if [[ "$mode" != "execute" ]]; then - printf '\n**Verdict:** dry-run (no source mutation attempted)\n\n' - elif [[ "$evolve_status" == "not-requested" ]]; then - printf '\n**Verdict:** evolve not requested\n\n' - elif [[ "$evolve_status" == "ok" ]]; then - printf '\n**Verdict:** admitted and completed\n\n' - else - printf '\n**Verdict:** %s\n\n' "$evolve_status" - fi -} - -build_tag_push_status() { - printf '### Tag-Push Status\n\n' - - local latest_tag - latest_tag="$(git describe --tags --abbrev=0 2>/dev/null || echo '')" - if [[ -z "$latest_tag" ]]; then - printf '_(no tags found)_\n\n' - return - fi - - local tag_date tag_behind - tag_date="$(git log -1 --format=%ci "$latest_tag" 2>/dev/null || echo '?')" - tag_behind="$(git rev-list "$latest_tag"..HEAD --count 2>/dev/null || echo '?')" - - printf '| Field | Value |\n|---|---|\n' - printf '| Latest tag | `%s` |\n' "$latest_tag" - printf '| Tag date | %s |\n' "$tag_date" - printf '| Commits since tag | %s |\n\n' "$tag_behind" -} - -# ──── Assemble ──── - -emit_markdown() { - build_header - build_admission_context - build_goals_section - build_cycle_history - build_runtime_flips - build_open_pr_overlap - build_tracker_degradation - build_transient_flakes - build_auto_reverts - build_tag_push_status - - printf '%s\n*Generated by `nightly-pr-digest.sh` at %s*\n' '---' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" -} - -emit_json() { - local tmp_md - tmp_md="$(mktemp "${TMPDIR:-/tmp}/pr-digest.XXXXXX")" - emit_markdown > "$tmp_md" - - local admission_json="{}" - if [[ -f "$DIGEST_JSON" ]]; then - admission_json="$(jq '.admission_context // {}' "$DIGEST_JSON" 2>/dev/null || echo '{}')" - fi - - jq -n \ - --arg schema_version "1" \ - --arg generated_at "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ - --arg branch "$BRANCH" \ - --arg run_dir "$RUN_DIR" \ - --argjson admission "$admission_json" \ - --rawfile body "$tmp_md" \ - '{ - schema_version: ($schema_version | tonumber), - generated_at: $generated_at, - branch: $branch, - run_dir: $run_dir, - admission_context: $admission, - pr_body: $body - }' - rm -f "$tmp_md" -} - -# ──── Main ──── - -log "run_dir=$RUN_DIR branch=$BRANCH format=$FORMAT" - -case "$FORMAT" in - markdown) - if [[ -n "$OUTPUT" ]]; then - emit_markdown > "$OUTPUT" - log "wrote $OUTPUT" - else - emit_markdown - fi - ;; - json) - if [[ -n "$OUTPUT" ]]; then - emit_json > "$OUTPUT" - log "wrote $OUTPUT" - else - emit_json - fi - ;; - *) die "unknown format: $FORMAT (use markdown or json)" ;; -esac diff --git a/scripts/probe-skill.sh b/scripts/probe-skill.sh index 825b0f504..9b4e96d9a 100755 --- a/scripts/probe-skill.sh +++ b/scripts/probe-skill.sh @@ -37,10 +37,10 @@ # iff no usable treatment reps (all degraded / missing). # # Usage: -# bash scripts/probe-skill.sh --probe crank --replay -# bash scripts/probe-skill.sh --probe crank --reps 2 --output out.json -# bash scripts/probe-skill.sh --probe crank --live --capture # record fixtures -# bash scripts/probe-skill.sh --probe crank --live --model gpt-5-mini # weak producer +# bash scripts/probe-skill.sh --probe rpi --replay +# bash scripts/probe-skill.sh --probe rpi --reps 2 --output out.json +# bash scripts/probe-skill.sh --probe rpi --live --capture +# bash scripts/probe-skill.sh --probe rpi --live --model gpt-5-mini # # Flags: --probe <id> (required) · --replay | --live · --capture · --reps N · # --output <path> · --timeout <secs> · --model <id> (weaker producer, the diff --git a/scripts/prune-agents.sh b/scripts/prune-agents.sh index e6eecab6e..673f66bc2 100755 --- a/scripts/prune-agents.sh +++ b/scripts/prune-agents.sh @@ -198,10 +198,6 @@ if [[ -d "$AGENTS_DIR/releases/local-ci" ]]; then fi [[ "$QUIET" == false ]] && echo "" -# --- Policy: evolve/ — keep last 20 cycle files --- -prune_keep_newest "$AGENTS_DIR/evolve" 20 "evolve" -[[ "$QUIET" == false ]] && echo "" - # --- Policy: vibe/ vibecheck/ — keep last 20 --- prune_keep_newest "$AGENTS_DIR/vibe" 20 "vibe" prune_keep_newest "$AGENTS_DIR/vibecheck" 20 "vibecheck" @@ -215,8 +211,7 @@ prune_keep_newest "$AGENTS_DIR/brainstorm" 10 "brainstorm" prune_older_than "$AGENTS_DIR/compaction-snapshots" 7 "*.md" "compaction-snapshots" [[ "$QUIET" == false ]] && echo "" -# --- Policy: crank/ swarm/ — keep last 10 --- -prune_keep_newest "$AGENTS_DIR/crank" 10 "crank" +# --- Policy: swarm/ — keep last 10 --- prune_keep_newest "$AGENTS_DIR/swarm" 10 "swarm" [[ "$QUIET" == false ]] && echo "" diff --git a/scripts/refresh-codex-artifacts.sh b/scripts/refresh-codex-artifacts.sh index 43aaf8824..0fd1a35c7 100755 --- a/scripts/refresh-codex-artifacts.sh +++ b/scripts/refresh-codex-artifacts.sh @@ -50,9 +50,7 @@ echo "Repo: $REPO_ROOT" echo "Scope: $SCOPE" bash scripts/regen-codex-hashes.sh -bash scripts/validate-codex-backbone-prompts.sh --repo-root "$REPO_ROOT" bash scripts/validate-codex-override-coverage.sh -bash scripts/validate-codex-lifecycle-guards.sh bash scripts/validate-codex-generated-artifacts.sh --scope "$SCOPE" bash scripts/audit-codex-parity.sh diff --git a/scripts/regen-all.sh b/scripts/regen-all.sh old mode 100755 new mode 100644 index 50fce9249..811a75f96 --- a/scripts/regen-all.sh +++ b/scripts/regen-all.sh @@ -1,99 +1,79 @@ #!/usr/bin/env bash -# regen-all.sh — one-command finalizer for the derived-artifact gate surface. -# -# Adding a skill or `ao` command makes ~8 generated registries/goldens stale, each -# guarded by an independent CI gate that stops at the first error. Discovering them -# one CI round at a time is slow (see PR #598 / ag-nk67: 9 rounds, 0 feature-code -# failures). This composes every generator into one local pass, with a --check mode -# that runs the matching drift validators as a pre-push gate. -# -# Usage: -# scripts/regen-all.sh # regenerate all derived artifacts (writes) -# scripts/regen-all.sh --check # run all drift validators (no writes); exit 1 on drift -# scripts/regen-all.sh --skills foo,bar # scope the codex-hash step to foo,bar -# REGEN_SKILLS=foo,bar scripts/regen-all.sh # same scoping via env -# -# For ordinary slice work, prefer changed-scope repair first: -# scripts/regen-changed-scope.sh --check --scope head -# scripts/regen-changed-scope.sh --scope worktree -# -# This script is release-wide: use it for final release sweeps, broad skill -# prune waves, command deletion/rename cleanup, or when changed-scope repair has -# no localized generator. -# -# NOTE: a MISSING parity_only Codex twin (skills-codex/<name>/) is now -# AUTO-generated from source by scripts/codex-sync.sh (run as a step below) — a -# self-contained runtime artifact: slim frontmatter + the source body transformed -# runtime-native + references copied (the Codex runtime ships skills-codex/ only). -# Only bespoke (hand-authored) Codex profiles are manual; mark a skill bespoke in -# skills-codex-overrides/catalog.json to opt out. Existing parity twins are -# refreshed when they drift from source. +# Regenerate or check every metadata-owned projection in dependency order. set -euo pipefail -REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -cd "$REPO_ROOT" +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT" -MODE="regen" -REGEN_SKILLS="${REGEN_SKILLS:-}" +mode=regen +skills="${REGEN_SKILLS:-}" while [[ $# -gt 0 ]]; do case "$1" in - --check) MODE="check" ;; - --skills) shift; [[ $# -gt 0 ]] || { echo "--skills requires a value" >&2; exit 2; }; REGEN_SKILLS="$1" ;; - --skills=*) REGEN_SKILLS="${1#--skills=}" ;; + --check) mode=check ;; + --skills) shift; [[ $# -gt 0 ]] || { echo "--skills requires a value" >&2; exit 2; }; skills="$1" ;; + --skills=*) skills="${1#--skills=}" ;; *) echo "usage: $0 [--check] [--skills <skill[,skill,...]>]" >&2; exit 2 ;; esac shift done fail=0 -step() { # label cmd... - local label="$1"; shift - if "$@" >/tmp/regen-all.$$.log 2>&1; then - printf ' \033[0;32m✓\033[0m %s\n' "$label" +log="$(mktemp "${TMPDIR:-/tmp}/regen-all.XXXXXX")" +trap 'rm -f "$log"' EXIT + +step() { + local label="$1" + shift + if "$@" >"$log" 2>&1; then + printf ' ✓ %s\n' "$label" else - printf ' \033[0;31m✗\033[0m %s\n' "$label"; sed 's/^/ /' /tmp/regen-all.$$.log | tail -6 + printf ' ✗ %s\n' "$label" + tail -n 12 "$log" | sed 's/^/ /' fail=1 fi } -if [[ "$MODE" == "regen" ]]; then - echo "== regenerating derived artifacts (dependency order) ==" - step "skill mesh" python3 scripts/generate-skill-mesh.py - step "embedded skills" make -C cli sync-hooks - step "cli reference (COMMANDS.md)" bash scripts/generate-cli-reference.sh - step "command surfaces (cobra expectedCmds + heading counts)" bash scripts/regen-command-surfaces.sh - step "cli-surface inventory" bash scripts/check-cmdao-surface-parity.sh --write-surface - step "codex twins (parity)" bash scripts/codex-sync.sh ${REGEN_SKILLS:+--only "$REGEN_SKILLS"} - step "codex hashes" bash scripts/regen-codex-hashes.sh ${REGEN_SKILLS:+--only "$REGEN_SKILLS"} - # Catalog LAST: it projects skill frontmatter + references + codex-twin PRESENCE, - # so it must run after codex-sync.sh (which may create/remove skills-codex/<name>/ - # twins) for the codex_override_present flag to be correct. - step "skill catalog (catalog.json)" bash scripts/generate-skill-catalog.sh +codex_sync() { + local args=() + [[ "$mode" == check ]] && args+=(--check) + [[ -n "$skills" ]] && args+=(--only "$skills") + bash scripts/codex-sync.sh "${args[@]}" +} + +codex_hashes() { + local args=() + [[ "$mode" == check ]] && args+=(--check) + [[ -n "$skills" ]] && args+=(--only "$skills") + bash scripts/regen-codex-hashes.sh "${args[@]}" +} + +if [[ "$mode" == regen ]]; then + echo "== regenerate metadata-owned projections ==" + step "Codex twins" codex_sync + step "Codex hashes" codex_hashes + step "skill mesh" python3 scripts/generate-skill-mesh.py + step "embedded runtime files" make -C cli sync-hooks + step "CLI reference" bash scripts/generate-cli-reference.sh + step "command heading projections" bash scripts/regen-command-surfaces.sh + step "CLI surface inventory" bash scripts/check-cmdao-surface-parity.sh --write-surface + step "documentation index" python3 scripts/generate-documentation-index.py echo - echo "Regenerated. Review 'git status', then run: scripts/regen-all.sh --check" - echo "Note: parity_only skills-codex/<name>/ twins are AUTO-generated/refreshed" - echo " by scripts/codex-sync.sh (self-contained, from source). Only a bespoke" - echo " (hand-authored) Codex profile needs manual authoring — add it to skills-codex-overrides/catalog.json." - echo "Note: cobra expectedCmds + cli-command-surface counts are now AUTO-regenerated" - echo " (regen-command-surfaces.sh). DELETING/RENAMING a command also needs manual" - echo " attention on codex-contract gates + skills-codex twins + VERBATIM template SHAs" - echo " — see the 'deletion ⊃ addition' note in scripts/regen-command-surfaces.sh." - echo "Scope tip: changed only some skills? Re-run with --skills a,b (or REGEN_SKILLS=a,b)" - echo " so the codex-hash step skips unrelated pre-existing drift instead of sweeping it in." + [[ $fail -eq 0 ]] && echo "Regeneration complete. Review the diff and run scripts/regen-all.sh --check." || echo "Regeneration failed." else - echo "== drift / gate sweep (no writes) ==" - step "skill mesh" python3 scripts/generate-skill-mesh.py --check - step "codex parity" bash scripts/audit-codex-parity.sh - step "codex runtime sections" bash scripts/validate-codex-runtime-sections.sh - step "codex twins (no drift)" bash scripts/codex-sync.sh --check ${REGEN_SKILLS:+--only "$REGEN_SKILLS"} - step "codex hashes (no drift)" bash scripts/regen-codex-hashes.sh --check ${REGEN_SKILLS:+--only "$REGEN_SKILLS"} - step "skill catalog drift" bash scripts/check-skill-catalog-drift.sh - step "context-map drift" bash scripts/validate-context-map-drift.sh - step "command surfaces drift" bash scripts/regen-command-surfaces.sh --check - step "doc-release gate" bash tests/docs/validate-doc-release.sh + echo "== check metadata-owned projections ==" + step "Codex twins" codex_sync + step "Codex hashes" codex_hashes + step "skill mesh" python3 scripts/generate-skill-mesh.py --check + step "Codex parity" bash scripts/audit-codex-parity.sh + step "Codex runtime sections" bash scripts/validate-codex-runtime-sections.sh + step "embedded runtime files" bash scripts/validate-embedded-sync.sh + step "CLI reference" bash scripts/generate-cli-reference.sh --check + step "command heading projections" bash scripts/regen-command-surfaces.sh --check + step "CLI surface inventory" bash scripts/check-cmdao-surface-parity.sh + step "documentation index" python3 scripts/generate-documentation-index.py --check + step "documentation release checks" bash tests/docs/validate-doc-release.sh echo - [[ $fail -eq 0 ]] && echo "ALL GATES GREEN — safe to push" || echo "DRIFT DETECTED — run scripts/regen-all.sh (no flag) to fix, then commit" + [[ $fail -eq 0 ]] && echo "All generated projections are current." || echo "Projection drift or validation failure detected." fi -rm -f /tmp/regen-all.$$.log -exit $fail +exit "$fail" diff --git a/scripts/regen-codex-hashes.sh b/scripts/regen-codex-hashes.sh index 6c9ceaef0..b8fbb8c0d 100755 --- a/scripts/regen-codex-hashes.sh +++ b/scripts/regen-codex-hashes.sh @@ -130,8 +130,6 @@ for skill_dir in sorted(p for p in skills_root.iterdir() if p.is_dir()): continue name = skill_dir.name - if name in {"pre-mortem", "post-mortem", "pre_mortem", "post_mortem"}: - continue if scope and name not in scope: continue new_hash = hash_tree(skill_dir) diff --git a/scripts/regression-bisect.sh b/scripts/regression-bisect.sh index 6aa6fabd7..8cf9b3148 100755 --- a/scripts/regression-bisect.sh +++ b/scripts/regression-bisect.sh @@ -7,7 +7,7 @@ set -euo pipefail # Usage: # scripts/regression-bisect.sh --good <sha> --bad <sha> --check "<command>" # Example: -# scripts/regression-bisect.sh --good abc123 --bad def456 --check "bash scripts/check-evolve-cycle-logging.sh" +# scripts/regression-bisect.sh --good abc123 --bad def456 --check "go test ./internal/gates/..." GOOD="" BAD="" diff --git a/scripts/removed-apparatus.txt b/scripts/removed-apparatus.txt deleted file mode 100644 index a7e63a461..000000000 --- a/scripts/removed-apparatus.txt +++ /dev/null @@ -1,20 +0,0 @@ -# removed-apparatus.txt — teardown-removed surfaces that must STAY removed. -# -# Consumed by scripts/check-no-apparatus-regrowth.sh (GOALS.md directive D15, -# anti-regeneration fitness gate). One repo-root-relative path per line. The -# gate FAILS if any listed path exists again — that means the /evolve loop (or -# anything else) regrew apparatus the teardown (epic ag-097) deliberately -# removed. This is a stay-removed OUTCOME guard, NOT a size/line/file metric: -# legitimate new growth is fine; only the return of these specific surfaces fails. -# -# Append a line here in the SAME change that lands a teardown deletion, so the -# removal is locked in. Lines starting with `#` and blank lines are ignored; -# an inline `# ...` comment after a path is also stripped. -# -# Each entry below was verified ABSENT on this branch before being asserted. - -cli/internal/wikiworker # removed, PR #589 (ag-jqx, dead caller pkg) -cli/internal/plans # removed, PR #581 (ag-nxm, S1) -cli/internal/worker # removed, PR #581 (ag-nxm, S1) -cli/internal/gascity # removed, PR #590 (ag-hfc, S2 orphan cluster) -cli/internal/bridge/gc.go # removed, PR #590 (ag-hfc, S2 — GC* symbols; semver helpers kept in bridge/semver.go) diff --git a/scripts/seed-evolution-roadmap-beads.sh b/scripts/seed-evolution-roadmap-beads.sh deleted file mode 100755 index a64c01e66..000000000 --- a/scripts/seed-evolution-roadmap-beads.sh +++ /dev/null @@ -1,330 +0,0 @@ -#!/usr/bin/env bash -# Seed evolution road-map beads from docs/plans/2026-05-11-evolution-roadmap.md. -# Idempotent: skips if a bead with the same title already exists. -# Usage: -# bash scripts/seed-evolution-roadmap-beads.sh # create -# bash scripts/seed-evolution-roadmap-beads.sh --dry-run # preview only -set -euo pipefail - -DRY_RUN=false -[[ "${1:-}" == "--dry-run" ]] && DRY_RUN=true - -ROADMAP="docs/plans/2026-05-11-evolution-roadmap.md" - -# bd_create_if_missing TITLE DESCRIPTION TYPE PRIORITY LABELS [PARENT_ID] -bd_create_if_missing() { - local title="$1" - local desc="$2" - local type="$3" - local priority="$4" - local labels="$5" - local parent="${6:-}" - - # Idempotency: check if an open issue with the exact title exists - if bd list --status=open --limit 0 --json 2>/dev/null | jq -e --arg t "$title" '.[] | select(.title == $t)' >/dev/null 2>&1; then - echo "skip: $title (exists)" - return 0 - fi - - if [[ "$DRY_RUN" == "true" ]]; then - echo "would-create: [$type/P$priority] $title (parent=${parent:-none})" - return 0 - fi - - local cmd=(bd create - --title="$title" - --description="$desc" - --type="$type" - --priority="$priority" - --labels="$labels" - ) - [[ -n "$parent" ]] && cmd+=(--parent="$parent") - - local id - id="$("${cmd[@]}" --json 2>/dev/null | jq -r '.id // empty')" - if [[ -n "$id" ]]; then - echo "created: $id $title" - eval "$2_ID='$id'" 2>/dev/null || true - else - echo "FAILED: $title" - return 1 - fi -} - -# Simpler: capture created ID for parent linkage -bd_mk() { - # Args: VAR TITLE DESC TYPE PRIORITY LABELS [PARENT] - local var="$1" - shift - local title="$1" desc="$2" type="$3" priority="$4" labels="$5" parent="${6:-}" - - if existing="$(bd list --status=open --limit 0 --json 2>/dev/null | jq -r --arg t "$title" '.[] | select(.title == $t) | .id' | head -1)"; then - if [[ -n "$existing" ]]; then - echo "skip: $existing $title" - printf -v "$var" '%s' "$existing" - return 0 - fi - fi - - if [[ "$DRY_RUN" == "true" ]]; then - echo "would-create: [$type/P$priority] $title" - printf -v "$var" '%s' "dry-run-id" - return 0 - fi - - local cmd=(bd create - --title="$title" - --description="$desc" - --type="$type" - --priority="$priority" - --labels="$labels" - ) - [[ -n "$parent" ]] && cmd+=(--parent="$parent") - - local out id - out="$("${cmd[@]}" --json 2>/dev/null)" - id="$(echo "$out" | jq -r '.id // empty')" - if [[ -z "$id" ]]; then - echo "FAILED: $title" - echo "$out" >&2 - return 1 - fi - echo "created: $id $title" - printf -v "$var" '%s' "$id" -} - -ROAD_REF="See $ROADMAP" - -# ========================================================================= -# EPIC E1: Directive Closure -# ========================================================================= -bd_mk E1 \ - "[epic] Evolution E1: Close GOALS.md directive gaps (11 directives)" \ - "Each of the 11 directives in GOALS.md has a Progress line. Where progress is incomplete, a child bead closes the gap. $ROAD_REF (section E1)." \ - task 1 "evolution-roadmap,directives" - -bd_mk D1 \ - "D1: Multi-runtime live execution proof (Tier E)" \ - "Tier S structural is green; Tier E live execution is not a default CI gate. Either build CI lanes that exercise real Claude/Codex/Cursor/OpenCode runtimes, OR document Tier E as opt-in in docs/contracts/multi-runtime-tier-charter.md. Acceptance: explicit charter doc OR tests/skills/test-runtime-*-live.sh runs in CI." \ - feature 1 "evolution-roadmap,directive,multi-runtime" "$E1" - -bd_mk D2 \ - "D2: End-to-end install execution in sandboxed CI" \ - "tests/install/test-install-smoke.sh validates syntax/structure but real install execution against a clean env is out-of-scope. Build .github/workflows/install-e2e.yml that runs install.sh against ubuntu+macos containers and verifies ao --version post-install." \ - feature 1 "evolution-roadmap,directive,install" "$E1" - -bd_mk D3 \ - "D3: Quarantine-empty enforcement gate" \ - "tests/_quarantine/ currently has zero suites. Add a goals-validate gate that fails when find tests/_quarantine -name '*.sh' -o -name '*.bats' | wc -l > 0. Weight 4. Acceptance: new gate row in GOALS.md, blocks push when quarantine populated without explicit override label." \ - feature 1 "evolution-roadmap,directive,gates" "$E1" - -bd_mk D4 \ - "D4: Flywheel-lifecycle citation hard-fail mode" \ - "scripts/check-flywheel-lifecycle.sh Stage 5 (citation) is soft-fail on sparse corpus. Add --strict flag that hard-fails when corpus has >= 100 learnings AND citation density < threshold. Wire as opt-in initially." \ - feature 2 "evolution-roadmap,directive,flywheel" "$E1" - -bd_mk D5 \ - "D5: Complexity regression ratchet to CC 18 for new code" \ - "CC 20 ceiling is green. Add a pre-commit-only stricter threshold (CC 18) for new functions in cli/internal/. Existing functions stay grandfathered. Acceptance: hooks/go-complexity-precommit.sh accepts a --new-code-threshold=18 flag." \ - feature 2 "evolution-roadmap,directive,complexity" "$E1" - -bd_mk D6 \ - "D6: Competitive freshness sweep (docs/comparisons)" \ - "scripts/check-competitive-freshness.sh enforces 45-day window. Audit current docs/comparisons/vs-*.md last_reviewed dates, refresh any drifting, and ensure the gate is currently green." \ - task 2 "evolution-roadmap,directive,docs" "$E1" - -bd_mk D7 \ - "D7: Codex parity drift to zero" \ - "scripts/check-codex-parity-drift.sh exists. Run it, classify findings, resolve each. Acceptance: bash scripts/check-codex-parity-drift.sh returns 0 findings; gate is green in CI without any --warn-only escape." \ - task 1 "evolution-roadmap,directive,codex-parity" "$E1" - -bd_mk D8 \ - "D8: Dream end-user dogfood validation" \ - ".agents/schedule.yaml.example exists. Add tests/install/test-dream-dogfood.sh that runs ao init --with-schedule in a temp dir and verifies the resulting .agents/schedule.yaml parses + has real-bodied job types (dream.run, wiki.forge, not stub bodies)." \ - feature 2 "evolution-roadmap,directive,dream" "$E1" - -bd_mk D9 \ - "D9: Pattern-to-skill synthesis (v2)" \ - "Detection layer is v1 (ao flywheel close-loop drafts skills under .agents/skill-drafts/). Synthesis v2 writes full SKILL.md bodies (not just frontmatter). Acceptance: a pattern with 3+ session evidence produces a draft skill that passes skill-frontmatter gate AND skill-lint dry-run." \ - feature 2 "evolution-roadmap,directive,skills,synthesis" "$E1" - -bd_mk D10 \ - "D10: Behavioral eval as default blocking gate" \ - "Workbench + A/B + scoring exist. eval-skill-delta CI gate is structural-only. Upgrade eval-workbench-verify to also fail when make -C evals/workbench head-to-head produces a regression delta. Acceptance: PR that introduces a skill regression has its CI fail with a delta scorecard artifact." \ - feature 1 "evolution-roadmap,directive,eval" "$E1" - -bd_mk D11 \ - "D11: Corpus durability snapshot/restore (soc-rv5p)" \ - "Routine cleanup wipes most of .agents/. Build ao corpus snapshot writing to configurable durable path, ao corpus restore rehydrating from latest, and a corpus-freshness gate firing if snapshot > 7 days old. Cross-link to existing bd soc-rv5p." \ - feature 1 "evolution-roadmap,directive,corpus,durability" "$E1" - -# ========================================================================= -# EPIC E2: Roadmap Gate Promotion -# ========================================================================= -bd_mk E2 \ - "[epic] Evolution E2: Promote Roadmap gates to CI-blocking" \ - "GOALS.md three-gap contract surface lists 5 gates as 'Roadmap (declared, not yet enforced)'. Each child bead moves one gate left. $ROAD_REF (section E2)." \ - task 1 "evolution-roadmap,gates" - -bd_mk G1 \ - "G1: Make flywheel-compounding CI-blocking via corpus-state snapshot" \ - "Gate is long-cycle, corpus-state. Design a corpus-state evidence snapshot in .agents/proof/flywheel-compounding-<date>.json that CI can validate without running multi-session work. Acceptance: gate moves from Roadmap to Currently enforcing column with documented snapshot protocol." \ - feature 1 "evolution-roadmap,gates,flywheel" "$E2" - -bd_mk G2 \ - "G2: Wire flywheel-proof gate as CI-blocking" \ - "scripts/check-flywheel-compounding.sh exists but is not invoked from blocking automation. Wire into .github/workflows/validate.yml (and pre-push if cheap enough). Acceptance: every push to main runs flywheel-proof; failure blocks merge." \ - feature 1 "evolution-roadmap,gates,flywheel" "$E2" - -bd_mk G3 \ - "G3: Make compile-freshness CI-blocking via runtime-artifact mode" \ - "Gate depends on .agents/defrag/latest.json. Design either CI step that generates the artifact, or stages a pre-computed one with hash check. Acceptance: gate no longer skipped in CI." \ - feature 2 "evolution-roadmap,gates,compile" "$E2" - -bd_mk G4 \ - "G4: Wire goals-validate as CI-blocking" \ - "goals-validate runs ao goals validate --json | jq -e '.valid == true' but is currently not blocking. Wire into pre-push and validate.yml. Acceptance: any push that breaks GOALS.md validity is blocked at gate." \ - feature 1 "evolution-roadmap,gates" "$E2" - -bd_mk G5 \ - "G5: (RETIRED) former script-citation meta-gate" \ - "RETIRED cut-plan wave 1: meta check that forced every check-*.sh citation removed. Historical bead only." \ - feature 1 "evolution-roadmap,gates" "$E2" - -# ========================================================================= -# EPIC E3: Known Product Gap Closure -# ========================================================================= -bd_mk E3 \ - "[epic] Evolution E3: Close PRODUCT.md Known Product Gaps (11 gaps)" \ - "PRODUCT.md 'Known Product Gaps' table enumerates 11 gaps with current status. Each child bead drives one gap toward closure or explicit 'won't fix' disposition. $ROAD_REF (section E3)." \ - task 1 "evolution-roadmap,product-gaps" - -bd_mk PG1 \ - "PG1: First-value path 5-minute install→validated-flow journey" \ - "PRODUCT.md gap: first-value path too diffuse for 3.0 PMF wedge. Build measurable 5-minute journey: install → first /rpi → validated artifact. Surface: README quickstart, ao quickstart CLI polish, install UX, first /rpi experience. Acceptance: tests/install/test-five-minute-journey.sh measures end-to-end time + artifact existence." \ - feature 1 "evolution-roadmap,product-gap,onboarding" "$E3" - -bd_mk PG2 \ - "PG2: 3.0 PMF scenario exported evidence (soc-m6v5.8)" \ - "PMF scenario spec exists in soc-m6v5.8 but no exported proof yet. Define scenario, control path, run, export to docs/releases/v3.0/pmf-scenario.md or evals/workbench/results/. Public launch claims about PMF stay gated until this lands." \ - feature 1 "evolution-roadmap,product-gap,release,pmf" "$E3" - -bd_mk PG3 \ - "PG3: /validate + /curate release-train consolidation (soc-m6v5.9)" \ - "Resolve epic soc-m6v5.9 (AgentOps 3.0 polished release train). Skill-count, registry, codex artifact gates must pass. Cross-link to existing epic." \ - feature 1 "evolution-roadmap,product-gap,release" "$E3" - -bd_mk PG4 \ - "PG4: Public launch claims need exported proof under docs/releases/" \ - "Audit all AOP-CLAIM-* markers in README + landing pages, link each to docs/releases/<version>/<claim-id>.md evidence file. Local .agents/ notes are not enough for public claims. Cross-link to A1 audit." \ - task 1 "evolution-roadmap,product-gap,claims,evidence" "$E3" - -bd_mk PG5 \ - "PG5: Dream full-loop autonomy" \ - "/dream + ao overnight + nightly.yml exist. Remaining work: full-loop autonomy without operator intervention, calibration, onboarding polish. Acceptance: a scheduled dream run executes harvest → forge → close-loop → defrag → report end-to-end with no manual steps." \ - feature 1 "evolution-roadmap,product-gap,dream" "$E3" - -bd_mk PG8 \ - "PG8: Worker context packets carry prevention/finding info" \ - "Workers spawned by /crank should receive cited learnings + planning rules + finding registry, not just spec text. Audit ao context assemble output for worker phases, add prevention/finding sections to worker packets. Acceptance: a worker packet for an issue with related findings has those findings inline." \ - feature 1 "evolution-roadmap,product-gap,workers,context" "$E3" - -bd_mk PG10 \ - "PG10: High-assurance profile control mapping" \ - "Extend docs/assurance-profile.md with redaction, evidence export, supply-chain inputs, program-specific control mapping. Goal: a constrained-environment operator can read assurance-profile.md and identify which controls AgentOps satisfies vs which need program-specific work." \ - task 2 "evolution-roadmap,product-gap,assurance" "$E3" - -bd_mk PG11 \ - "PG11: Context-compiler messaging sweep" \ - "CDLC framing landed in Mission/Strategic Bet/README/mkdocs hero. Remaining: downstream comparison docs and skill-page intros still use older framing. Sweep docs/comparisons/*.md and skills/*/SKILL.md intros for consistency." \ - task 2 "evolution-roadmap,product-gap,messaging" "$E3" - -# ========================================================================= -# EPIC E4: Four-Layer Polish -# ========================================================================= -bd_mk E4 \ - "[epic] Evolution E4: Polish one capability per product layer" \ - "Each of the four PRODUCT.md layers (Bookkeeping, Context Compiler, Validation Gates, Knowledge Flywheel) has one highest-impact missing capability. Four child beads close them. $ROAD_REF (section E4)." \ - task 1 "evolution-roadmap,four-layer" - -bd_mk L1 \ - "L1: Improve citation signal-to-noise via follow_up_action field" \ - "Citation log (.agents/ao/citations.jsonl) has ~3,867 entries but utility scoring is weak. Differentiate cited-then-followed (agent acted on cite) from cited-then-ignored. Add follow_up_action field to citation events; weight in retrieval scoring." \ - feature 2 "evolution-roadmap,layer-bookkeeping,citations" "$E4" - -bd_mk L2 \ - "L2: Phase-scoped context assembly test coverage" \ - "PRODUCT.md claims phase-scoped context packets but no test verifies ao context assemble produces different packets per RPI phase. Add tests/scripts/test-context-phase-scoping.bats that asserts research vs implement vs validate packets differ in expected ways." \ - task 2 "evolution-roadmap,layer-compiler,tests" "$E4" - -bd_mk L3 \ - "L3: Council planted-bug detection fixture test" \ - "No test exists for council judges' detection rate against known-bad inputs. Build tests/council/test-planted-bug-detection.bats with N planted bugs in fixtures; council must catch >= floor%. Acceptance: floor configurable, currently set to 70%." \ - feature 2 "evolution-roadmap,layer-gates,council,tests" "$E4" - -bd_mk L4 \ - "L4: ao flywheel dashboard (operator-facing trend view)" \ - "Dream cycle reports compounding metrics but no operator-facing dashboard shows trends. Build ao flywheel dashboard: single-screen markdown view of sigma-rho, delta, citation density, learning count over time." \ - feature 2 "evolution-roadmap,layer-flywheel,dashboard" "$E4" - -# ========================================================================= -# EPIC E5: Three-Gap Contract Surface integration -# ========================================================================= -bd_mk E5 \ - "[epic] Evolution E5: Three-gap contract surface super-gates" \ - "GOALS.md three-gap table separates 'Currently enforcing' from 'Roadmap'. Each of the three gaps gets a super-gate that combines its component gates. $ROAD_REF (section E5)." \ - task 2 "evolution-roadmap,three-gap-contract" - -bd_mk TG1 \ - "TG1: Gap-1 council-coverage super-gate" \ - "Add council-coverage gate that verifies every PR-bound commit has either a /pre-mortem or /vibe verdict in .agents/council/. Acceptance: new gate in GOALS.md, blocks PR merge if either verdict is missing for any commit in the PR diff." \ - feature 2 "evolution-roadmap,three-gap-contract,council" "$E5" - -bd_mk TG2 \ - "TG2: Gap-2 durable-learning super-gate" \ - "Combine flywheel-compounding (G1), flywheel-proof (G2), compile-freshness (G3) into a single durable-learning super-gate that surfaces gap-2 closure status. Acceptance: single ao goals measure --gap=durable-learning command emits a unified PASS/WARN/FAIL." \ - feature 2 "evolution-roadmap,three-gap-contract,flywheel" "$E5" - -bd_mk TG3 \ - "TG3: Gap-3 loop-closure super-gate" \ - "Combine release-cadence + flywheel-proof (G2) + goals-validate (G4) into a loop-closure super-gate. Acceptance: single ao goals measure --gap=loop-closure emits unified status." \ - feature 2 "evolution-roadmap,three-gap-contract,loop-closure" "$E5" - -# ========================================================================= -# AUDIT EPICS A1, A2, A3 -# ========================================================================= -bd_mk A1 \ - "[audit] A1: AOP-CLAIM evidence map (83 claim markers)" \ - "Grep all AOP-CLAIM-* markers and the paragraph that follows each. Classify by category. For each, identify the evidence file or test. Output: .agents/research/2026-05-11-aop-claim-evidence-map.md. Unverified claims spawn child beads of shape 'Verify AOP-CLAIM-<id>'. $ROAD_REF (section A1)." \ - task 1 "evolution-roadmap,audit,aop-claim" - -bd_mk A2 \ - "[audit] A2: Contract enforcement matrix (38 contracts)" \ - "For each docs/contracts/<name>.md, search scripts/check-*<name>*.sh and tests/contracts/*<name>*. Classify: enforced, partially-enforced, doc-only. Output: .agents/research/2026-05-11-contract-enforcement-matrix.md. Unenforced contracts spawn child beads 'Enforce <contract-name>'. $ROAD_REF (section A2)." \ - task 1 "evolution-roadmap,audit,contracts" - -bd_mk A3 \ - "[audit] A3: Code-map drift report (2 maps)" \ - "For each docs/code-map/*.md, extract claimed file structure and diff against actual repo. Output: .agents/research/2026-05-11-code-map-drift-report.md. Drift items spawn child beads. $ROAD_REF (section A3)." \ - task 2 "evolution-roadmap,audit,code-map" - -# ========================================================================= -# EPIC LC: Learning Capture Loop -# ========================================================================= -bd_mk LC_EPIC \ - "[epic] Evolution LC: Learning capture loop (compound the loop on itself)" \ - "Three-layer self-reflection so each day improves the next: (1) per-cycle 1-line micro-capture to .agents/evolve/daily-learning-log-YYYY-MM-DD.md, (2) every-5th-productive-cycle pattern reflect inline in cycle-history note, (3) end-of-day consolidate via ao loop learn writing to .agents/learnings/YYYY-MM-DD-evolve-loop-learnings.md and auto-filing evolve-improvement beads for cross-day recurring frictions. $ROAD_REF (section LC)." \ - task 1 "evolution-roadmap,learning-capture" - -bd_mk LC1 \ - "LC1: Wire learning-capture protocol into /evolve all-day loop" \ - "Verify the three layers operate end-to-end on a real day: (a) micro-capture appends one line per cycle, (b) every-5th-productive reflect surfaces pattern annotations, (c) end-of-day consolidator runs at hard stop and writes the dated learning file. Acceptance: after one full /evolve day, .agents/evolve/daily-learning-log-YYYY-MM-DD.md has N entries (one per cycle), .agents/learnings/YYYY-MM-DD-evolve-loop-learnings.md exists with counts + ledger + frictions sections, and if any FRICTION tag matches a prior day a LC-followup bead is auto-filed under evolution-roadmap. Cross-link: ao loop learn, .agents/evolve/daily-learning-log.template.md." \ - feature 1 "evolution-roadmap,learning-capture" "$LC_EPIC" - -echo "" -echo "====================" -echo "Seeding complete." -echo "====================" -echo "" -echo "Inspect: bd ready -n 50 | head -30" -echo "Full roadmap doc: $ROADMAP" diff --git a/scripts/session-pr-scope.sh b/scripts/session-pr-scope.sh deleted file mode 100755 index d2170fe4f..000000000 --- a/scripts/session-pr-scope.sh +++ /dev/null @@ -1,94 +0,0 @@ -#!/usr/bin/env bash -# session-pr-scope.sh — hookless session-PR-scope signal (ag-o5xp). -# -# Supersedes the removed PreToolUse hook `hooks/session-pr-counter.sh` (PR #362, -# soc-1aou), which was deleted in the 3.0 hookless teardown (#511). AgentOps 3.0 -# ships no hooks; this is the hookless replacement for the session-scope count. -# -# Counts PRs the current GitHub user opened within a trailing window and emits a -# session-scope verdict (OK | WARN | BLOCK) against SESSION_PR_THRESHOLD. It is the -# canonical session-PR count source: -# - the /evolve post-mortem checkpoint reads it for $session_pr_count -# (skills/evolve/references/postmortem-checkpoint.md); -# - anyone wanting the old pre-creation signal can wrap it in an opt-in hook -# authored via the hooks-authoring skill (AgentOps ships none by default); -# - it can run manually or as a warn-only CI step. -# -# Fail-open: advisory tooling must never block on infrastructure failure. If `gh` -# or `jq` is unavailable, or the count cannot be parsed, it exits 0 with -# verdict=unknown. -# -# Usage: -# scripts/session-pr-scope.sh # human-readable verdict -# scripts/session-pr-scope.sh --json # {count,threshold,window_hours,verdict,over,block_mode} -# scripts/session-pr-scope.sh --count # just the integer count (for $session_pr_count) -# -# Env: -# SESSION_PR_THRESHOLD (default 5) — post-mortem checkpoint threshold -# AGENTOPS_SESSION_PR_WINDOW_HOURS (default 24) — "current session" window -# AGENTOPS_SESSION_PR_BLOCK (unset) — when =1, verdict BLOCK + exit 2 at count >= threshold -set -uo pipefail - -THRESHOLD="${SESSION_PR_THRESHOLD:-5}" -WINDOW_HOURS="${AGENTOPS_SESSION_PR_WINDOW_HOURS:-24}" -BLOCK_MODE="${AGENTOPS_SESSION_PR_BLOCK:-}" - -MODE="human" -case "${1:-}" in - --json) MODE="json" ;; - --count) MODE="count" ;; - ""|--human) MODE="human" ;; - -h|--help) sed -n '2,33p' "$0"; exit 0 ;; - *) printf 'unknown arg: %s\n' "$1" >&2; exit 64 ;; -esac - -emit_unknown() { - case "$MODE" in - json) printf '{"count":null,"threshold":%s,"window_hours":%s,"verdict":"unknown","over":false,"block_mode":%s}\n' \ - "$THRESHOLD" "$WINDOW_HOURS" "$([ "$BLOCK_MODE" = 1 ] && echo true || echo false)" ;; - count) printf '0\n' ;; - human) printf 'session-pr-scope: verdict=unknown (gh/jq unavailable or unparseable; failing open)\n' ;; - esac - exit 0 -} - -command -v gh >/dev/null 2>&1 || emit_unknown -command -v jq >/dev/null 2>&1 || emit_unknown - -SINCE_ISO="$(date -u -d "${WINDOW_HOURS} hours ago" +%FT%TZ 2>/dev/null || date -u +%FT%TZ)" -COUNT="$(gh pr list --search "author:@me created:>=${SINCE_ISO}" --state all --limit 100 --json number 2>/dev/null | jq -r 'length' 2>/dev/null)" - -case "$COUNT" in - ''|*[!0-9]*) emit_unknown ;; -esac - -# Verdict: WARN once the count reaches threshold-1 (the next PR tips the session -# over); BLOCK only when over the threshold AND block mode is opted in. -WARN_AT=$((THRESHOLD - 1)) -verdict="ok" -over=false -exit_code=0 -if [ "$COUNT" -ge "$THRESHOLD" ]; then - over=true - if [ "$BLOCK_MODE" = "1" ]; then verdict="block"; exit_code=2; else verdict="warn"; fi -elif [ "$COUNT" -ge "$WARN_AT" ]; then - verdict="warn" -fi - -reminder="Session-scope: ${COUNT} PR(s) in the last ${WINDOW_HOURS}h (threshold ${THRESHOLD}). Run a real post-mortem (/post-mortem --deep, council-gated) before the next PR — reactive-PR spirals are the dominant back-half failure mode (soc-waxr). The /evolve loop enforces this mechanically at checkpoint #6." - -case "$MODE" in - count) printf '%s\n' "$COUNT"; exit 0 ;; - json) - printf '{"count":%s,"threshold":%s,"window_hours":%s,"verdict":"%s","over":%s,"block_mode":%s}\n' \ - "$COUNT" "$THRESHOLD" "$WINDOW_HOURS" "$verdict" "$over" \ - "$([ "$BLOCK_MODE" = 1 ] && echo true || echo false)" - exit "$exit_code" ;; - human) - case "$verdict" in - ok) printf 'session-pr-scope: OK — %s PR(s) in %sh (threshold %s)\n' "$COUNT" "$WINDOW_HOURS" "$THRESHOLD" ;; - warn) printf 'session-pr-scope: WARN — %s\n' "$reminder" ;; - block) printf 'session-pr-scope: BLOCK — %s\n' "$reminder" >&2 ;; - esac - exit "$exit_code" ;; -esac diff --git a/scripts/skill-flow-standalone.txt b/scripts/skill-flow-standalone.txt deleted file mode 100644 index 472b93909..000000000 --- a/scripts/skill-flow-standalone.txt +++ /dev/null @@ -1,86 +0,0 @@ -# skill-flow-standalone.txt — intentionally-standalone skills. -# -# Skills listed here are permitted to have ZERO skill-to-skill edges (no peer -# referenced in consumes/context_rel/metadata.dependencies, and not referenced -# by any peer). They are boundary adapters, meta-tooling, or human-facing -# explainers that legitimately do not participate in the inter-skill flow. -# -# Enforced by scripts/validate-skill-flow.sh. Adding a skill here is a -# deliberate act: it asserts "this skill is a leaf by design." If a skill here -# later gains an edge, the gate flags it as a stale entry — remove it. -# -# Contract: docs/contracts/skill-flow.md -# Format: one skill slug per line; everything after '#' is a comment. - -# --- knowledge / .agents boundary writers (emit .agents/research/*.md; the -# flywheel reads them out-of-band, not via a declared skill edge) --- -curate # mine transcripts/.agents/bd/git into knowledge diffs -dream # retired pointer; out-of-session compounding via Gas City -reverse-engineer # reverse-engineer product specs into research notes (renamed from reverse-engineer-rpi) - -# --- external-boundary adapters (driven adapters to systems outside the corpus) --- -openai-docs # reads upstream OpenAI docs (external-api) -pr-research # researches an upstream OSS repo (external-api) -status # reports bd work status (external bd store) -push # commits/pushes git-changes (terminal VCS sink) - -# --- orchestration / install adapters (drive other agents or install artifacts) --- -codex-team # coordinate multiple Codex agents (orchestration) -session-bootstrap # universal init entry point (customer-of AGENTS*.md docs) -system-tuning # restore system responsiveness (process/host hygiene) - -# --- human-facing explainers (documentation skills, no pipeline role) --- -handoff # writes .agents/handoffs session-continuity artifacts; read out-of-band by the next session, no declared peer edge -using-agentops # explain AgentOps workflows -bd-first-memory-migration - -# --- standalone operator/library/analyzer skills introduced by the expanded corpus --- -automation-loop-hardening # standalone external-boundary or library/analyzer skill; no peer skill edge by design -casr # standalone external-boundary or library/analyzer skill; no peer skill edge by design -cass # standalone external-boundary or library/analyzer skill; no peer skill edge by design -changelog-quality-pass # standalone external-boundary or library/analyzer skill; no peer skill edge by design -cli-doctoring-workflow # standalone external-boundary or library/analyzer skill; no peer skill edge by design -concurrency-deadlock-remediation # standalone external-boundary or library/analyzer skill; no peer skill edge by design -external-search-triage # standalone external-boundary or library/analyzer skill; no peer skill edge by design -filesystem-path-rationalization # standalone external-boundary or library/analyzer skill; no peer skill edge by design -fuzz-test-design # standalone external-boundary or library/analyzer skill; no peer skill edge by design -gcloud # standalone external-boundary or library/analyzer skill; no peer skill edge by design -gh-actions # standalone external-boundary or library/analyzer skill; no peer skill edge by design -gh-cli # standalone external-boundary or library/analyzer skill; no peer skill edge by design -gh-triage-ru # standalone external-boundary or library/analyzer skill; no peer skill edge by design -golden-artifact-testing # standalone external-boundary or library/analyzer skill; no peer skill edge by design -installer-quality-audit # standalone external-boundary or library/analyzer skill; no peer skill edge by design -layered-defect-hunt # standalone external-boundary or library/analyzer skill; no peer skill edge by design -metamorphic-test-design # standalone external-boundary or library/analyzer skill; no peer skill edge by design -ms # wraps the external meta_skill (ms) binary/MCP server; external-boundary tool, no peer skill edge by design -multi-model-triangulation # standalone external-boundary or library/analyzer skill; no peer skill edge by design -native-debugger-triage # standalone external-boundary or library/analyzer skill; no peer skill edge by design -planning-workflow # standalone external-boundary or library/analyzer skill; no peer skill edge by design -process-triage # standalone external-boundary or library/analyzer skill; no peer skill edge by design -project-readme-craft # standalone external-boundary or library/analyzer skill; no peer skill edge by design -project-reasoning-lens-analysis # standalone external-boundary or library/analyzer skill; no peer skill edge by design -rch # standalone external-boundary or library/analyzer skill; no peer skill edge by design -repeatedly-apply-skill # standalone external-boundary or library/analyzer skill; no peer skill edge by design -repository-hygiene-sweep # standalone external-boundary or library/analyzer skill; no peer skill edge by design -ripgrep-search-discipline # standalone external-boundary or library/analyzer skill; no peer skill edge by design -ru-multi-repo-workflow # standalone external-boundary or library/analyzer skill; no peer skill edge by design -rust-crate-release-readiness # standalone external-boundary or library/analyzer skill; no peer skill edge by design -rust-port-validation-gauntlet # standalone external-boundary or library/analyzer skill; no peer skill edge by design -rust-sqlite-cli-architecture # standalone external-boundary or library/analyzer skill; no peer skill edge by design -rust-ub-risk-audit # standalone external-boundary or library/analyzer skill; no peer skill edge by design -rust-unsafe-boundary-audit # standalone external-boundary or library/analyzer skill; no peer skill edge by design -sbh # standalone external-boundary or library/analyzer skill; no peer skill edge by design -ssh # standalone external-boundary or library/analyzer skill; no peer skill edge by design -stash-hygiene-sweep # standalone external-boundary or library/analyzer skill; no peer skill edge by design -ubs # standalone external-boundary or library/analyzer skill; no peer skill edge by design -worktree-branch-rationalization # standalone external-boundary or library/analyzer skill; no peer skill edge by design -beads-bv # tracker-triage primitive; lost its only incoming edge when operating-loop-skill was pruned (ag-if7p) — leaf by design, like status -converge # thin memo over the `ao converge` command (driving-adapter); no peer skill edge by design, like status -using-gc # gc-substrate operator skill; drives the external gc CLI, no peer skill edge by design -gc-membrane # JIT close-door reference loaded by using-gc via Read; no frontmatter edge by design - -# --- name-compatibility pointers (redirect_to a canonical skill; pure aliases with no pipeline role) --- -pre-mortem # compatibility pointer → premortem; redirect-only, no peer skill edge by design -pre_mortem # compatibility pointer → premortem; redirect-only, no peer skill edge by design -post-mortem # compatibility pointer → postmortem; redirect-only, no peer skill edge by design -post_mortem # compatibility pointer → postmortem; redirect-only, no peer skill edge by design diff --git a/scripts/skill-usage-report.sh b/scripts/skill-usage-report.sh index 086cc1c47..d5c0f914c 100755 --- a/scripts/skill-usage-report.sh +++ b/scripts/skill-usage-report.sh @@ -49,7 +49,7 @@ lib_set() { basename "$d" fi done - printf '%s\n' standards shared gc-membrane domain codex-exec beads-br + printf '%s\n' standards shared domain } # Count both persisted shapes across transcripts newer than the window. @@ -96,4 +96,4 @@ printf '%s\n' "$raw" | while read -r c s; do if printf '%s\n' "$libs" | /usr/bin/grep -qx "$s"; then printf ' %6s %s\n' "$c" "$s"; fi done echo -echo "feeds: disposition refreshes (docs/contracts/skill-dispositions.yaml), age-e508 measured tier." +echo "usage is advisory evidence; skills/*/SKILL.md metadata remains the inventory source." diff --git a/scripts/smoke-test-codex-skills.sh b/scripts/smoke-test-codex-skills.sh index dd8f3184d..31d1d9882 100755 --- a/scripts/smoke-test-codex-skills.sh +++ b/scripts/smoke-test-codex-skills.sh @@ -1,14 +1,13 @@ #!/usr/bin/env bash -# smoke-test-codex-skills.sh — DAG-based headless smoke test for Codex skills. -# Traverses skill dependency graph in topological order, spawns a headless codex -# runner per skill, collects PASS/PARTIAL/FAIL verdicts. +# smoke-test-codex-skills.sh — optional evaluation of generated Codex skills. +# The metadata-derived directory inventory is the test set; this script owns no +# runtime graph or release decision. # # Usage: # scripts/smoke-test-codex-skills.sh [OPTIONS] # # Options: # --dry-run Print what would run without spawning Codex -# --chain N Run only chain N (1-4) # --skill NAME Run only a single skill # --timeout SECS Per-skill timeout (default: 90) # --parallel N Max parallel Codex invocations (default: 4) @@ -36,7 +35,6 @@ RESULTS_DIR="$REPO_ROOT/.agents/smoke-test" # Defaults DRY_RUN=false -CHAIN_FILTER="" SKILL_FILTER="" TIMEOUT=90 PARALLEL=4 @@ -49,7 +47,6 @@ VERBOSE=false while [[ $# -gt 0 ]]; do case "$1" in --dry-run) DRY_RUN=true; shift ;; - --chain) CHAIN_FILTER="$2"; shift 2 ;; --skill) SKILL_FILTER="$2"; shift 2 ;; --timeout) TIMEOUT="$2"; shift 2 ;; --parallel) PARALLEL="$2"; shift 2 ;; @@ -80,23 +77,6 @@ fi mkdir -p "$RESULTS_DIR" -# --- DAG Definition --- -# Extracted from skill body analysis (see .agents/handoff/2026-03-14-codex-api-alignment.md) -# Format: LAYER[n]="skill1 skill2 ..." - -LAYER0="standards shared beads brainstorm inject forge retro ratchet provenance compile handoff recover quickstart goals flywheel openai-docs oss-docs product security release converter update using-agentops status heal-skill codex-team pr-research pr-plan pr-implement pr-validate pr-prep pr-retro grafana-platform-dashboard reverse-engineer-rpi push" -LAYER1="council research doc implement bug-hunt trace readme" -LAYER2="pre-mortem post-mortem vibe complexity" -LAYER3="swarm validation plan" -LAYER4="crank discovery" -LAYER5="rpi evolve" - -# Chains for traversal (minimum covering paths) -CHAIN1="standards council pre-mortem plan research inject brainstorm discovery implement beads swarm vibe complexity bug-hunt crank post-mortem validation retro forge rpi ratchet" -CHAIN2="pr-research pr-plan pr-implement pr-validate pr-prep pr-retro" -CHAIN3="compile evolve flywheel provenance trace" -CHAIN4="doc readme handoff recover status quickstart goals product oss-docs release security heal-skill codex-team update converter using-agentops grafana-platform-dashboard reverse-engineer-rpi push openai-docs shared" - # --- Static Validation --- static_check() { local skill_name="$1" @@ -234,20 +214,10 @@ get_skills() { return fi - if [[ -n "$CHAIN_FILTER" ]]; then - case "$CHAIN_FILTER" in - 1) skills="$CHAIN1" ;; - 2) skills="$CHAIN2" ;; - 3) skills="$CHAIN3" ;; - 4) skills="$CHAIN4" ;; - *) echo "Invalid chain: $CHAIN_FILTER (must be 1-4)" >&2; exit 2 ;; - esac - echo "$skills" - return - fi - - # All skills in topological order (deduped across chains) - echo "$LAYER0 $LAYER1 $LAYER2 $LAYER3 $LAYER4 $LAYER5" + find "$SKILLS_CODEX" -mindepth 2 -maxdepth 2 -name SKILL.md -print \ + | sed 's#/SKILL.md$##; s#^.*/##' \ + | LC_ALL=C sort \ + | tr '\n' ' ' } # --- Main --- @@ -406,7 +376,7 @@ main() { fi # --- Summary --- - echo "=== Release Gate Verdict ===" + echo "=== Evaluation Result ===" echo "Total: $total PASS: $pass PARTIAL: $partial FAIL: $fail" echo "" diff --git a/scripts/spec-consistency-gate.sh b/scripts/spec-consistency-gate.sh index 5d9e78d7a..7e64cddb5 100755 --- a/scripts/spec-consistency-gate.sh +++ b/scripts/spec-consistency-gate.sh @@ -26,7 +26,7 @@ # 0 All FAIL checks passed (WARN is OK) # 1 One or more FAIL checks failed # -# Pattern: pre-push-gate.sh accumulator / color pattern +# This is an ordinary deterministic check; callers choose where to run it. set -euo pipefail diff --git a/scripts/test-ci-deterministic-gates.sh b/scripts/test-ci-deterministic-gates.sh index ced389409..057017b77 100755 --- a/scripts/test-ci-deterministic-gates.sh +++ b/scripts/test-ci-deterministic-gates.sh @@ -84,7 +84,7 @@ log "REPO_ROOT=$REPO_ROOT" log "" # Gate 1: registry-check (post-soc-k47k: deterministic across local/CI). -run_gate "registry-check" bash scripts/generate-registry.sh --check +run_gate "skill-mesh-check" python3 scripts/generate-skill-mesh.py --check # Gate 2: skill-lint suite. run_gate "skill-lint" bash tests/skills/lint-skills.sh diff --git a/scripts/test-evolve-cycle-smoke.sh b/scripts/test-evolve-cycle-smoke.sh deleted file mode 100755 index 5761a6db3..000000000 --- a/scripts/test-evolve-cycle-smoke.sh +++ /dev/null @@ -1,190 +0,0 @@ -#!/usr/bin/env bash -# test-evolve-cycle-smoke.sh — End-to-end /evolve cycle smoke (soc-k3fa). -# -# Asserts that one bounded `ao evolve` cycle lands a real commit on the current -# branch AND creates no new orphaned commits. Designed to catch the failure -# mode from the 2026-05-06 overnight run, where 4 cycles produced 0 commits on -# main while orphaning 3 real commits in detached worktrees that were -# auto-cleaned. -# -# Pass criteria (both required): -# 1. `git rev-parse HEAD` advances by at least one commit during the run. -# 2. `git fsck --unreachable | grep -c "^unreachable commit"` does not grow. -# -# This script is opt-in. It was historically wired into the bash pre-push -# gate; run it explicitly or via the Go gate registry when selected. -# the `--smoke-evolve` flag because a single bounded cycle takes 15-30 minutes; -# normal push gates skip it. Run manually: -# -# scripts/test-evolve-cycle-smoke.sh -# scripts/test-evolve-cycle-smoke.sh --max-cycles 2 --timeout 3600 -# -# Environment: -# AO_BIN Path to ao binary (default: $(command -v ao)). -# SMOKE_GOAL Optional explicit goal string to pass to ao evolve. -# SMOKE_BRANCH Branch to track HEAD on (default: current branch). -# SMOKE_KEEP_LOG 1 to keep the run log on success (default: 0 = delete). -# -# Exit codes: -# 0 smoke passed (commit landed, no new orphans) -# 1 smoke failed (no commit landed, or new orphans appeared, or command failed) -# 2 argument parse error or environment misconfiguration - -set -euo pipefail - -usage() { - cat <<'EOF' -Usage: scripts/test-evolve-cycle-smoke.sh [--max-cycles N] [--timeout SECONDS] [--goal GOAL] - -Run one bounded `ao evolve` cycle and assert a commit lands with no new -orphaned commits. Intended for opt-in pre-push validation of the cycle -lifecycle (soc-k3fa / mc-m3.5-pre4). - -Options: - --max-cycles N Cycles to run (default: 1). - --timeout SECONDS Hard timeout for the evolve invocation (default: 1800). - --goal STRING Explicit goal string for the cycle (default: queue-driven). - -h, --help Show this message and exit. - -Environment overrides: AO_BIN, SMOKE_GOAL, SMOKE_BRANCH, SMOKE_KEEP_LOG. -EOF -} - -MAX_CYCLES=1 -TIMEOUT_SECONDS=1800 -GOAL="${SMOKE_GOAL:-}" - -while [[ $# -gt 0 ]]; do - case "$1" in - --max-cycles) - MAX_CYCLES="${2:-}" - shift 2 - ;; - --timeout) - TIMEOUT_SECONDS="${2:-}" - shift 2 - ;; - --goal) - GOAL="${2:-}" - shift 2 - ;; - -h|--help) - usage - exit 0 - ;; - *) - echo "Unknown arg: $1" >&2 - usage >&2 - exit 2 - ;; - esac -done - -if ! [[ "$MAX_CYCLES" =~ ^[0-9]+$ ]] || [[ "$MAX_CYCLES" -lt 1 ]]; then - echo "test-evolve-cycle-smoke: --max-cycles must be a positive integer (got: $MAX_CYCLES)" >&2 - exit 2 -fi - -if ! [[ "$TIMEOUT_SECONDS" =~ ^[0-9]+$ ]] || [[ "$TIMEOUT_SECONDS" -lt 60 ]]; then - echo "test-evolve-cycle-smoke: --timeout must be an integer >= 60 (got: $TIMEOUT_SECONDS)" >&2 - exit 2 -fi - -AO_BIN="${AO_BIN:-$(command -v ao || true)}" -if [[ -z "$AO_BIN" ]]; then - echo "test-evolve-cycle-smoke: ao binary not found on PATH (set AO_BIN to override)" >&2 - exit 2 -fi - -if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then - echo "test-evolve-cycle-smoke: not inside a git work tree" >&2 - exit 2 -fi - -REPO_ROOT="$(git rev-parse --show-toplevel)" -cd "$REPO_ROOT" - -SMOKE_BRANCH="${SMOKE_BRANCH:-$(git branch --show-current)}" -if [[ -z "$SMOKE_BRANCH" ]]; then - echo "test-evolve-cycle-smoke: refusing to run on a detached HEAD; checkout a branch first" >&2 - exit 2 -fi - -count_orphans() { - git fsck --unreachable 2>/dev/null | grep -c '^unreachable commit' || true -} - -HEAD_BEFORE="$(git rev-parse HEAD)" -ORPHANS_BEFORE="$(count_orphans)" -LOG_DIR="$REPO_ROOT/.agents/evolve/smoke" -mkdir -p "$LOG_DIR" -LOG_FILE="$LOG_DIR/$(date -u +%Y%m%dT%H%M%SZ)-cycle.log" - -echo "test-evolve-cycle-smoke: starting" -echo " branch: $SMOKE_BRANCH" -echo " HEAD before: $HEAD_BEFORE" -echo " orphan commits: $ORPHANS_BEFORE" -echo " max-cycles: $MAX_CYCLES" -echo " timeout (sec): $TIMEOUT_SECONDS" -echo " log file: $LOG_FILE" - -set +e -if [[ -n "$GOAL" ]]; then - timeout "$TIMEOUT_SECONDS" "$AO_BIN" evolve "$GOAL" \ - --max-cycles="$MAX_CYCLES" \ - --landing-policy=commit \ - --auto-clean \ - --gate-policy=best-effort \ - 2>&1 | tee "$LOG_FILE" -else - timeout "$TIMEOUT_SECONDS" "$AO_BIN" evolve \ - --max-cycles="$MAX_CYCLES" \ - --landing-policy=commit \ - --auto-clean \ - --gate-policy=best-effort \ - 2>&1 | tee "$LOG_FILE" -fi -EVOLVE_RC=${PIPESTATUS[0]} -set -e - -HEAD_AFTER="$(git rev-parse HEAD)" -ORPHANS_AFTER="$(count_orphans)" - -echo "" -echo "test-evolve-cycle-smoke: results" -echo " HEAD after: $HEAD_AFTER" -echo " orphan commits: $ORPHANS_AFTER" -echo " evolve exit: $EVOLVE_RC" - -FAIL=0 - -if [[ "$HEAD_BEFORE" == "$HEAD_AFTER" ]]; then - echo "FAIL: HEAD did not advance — no commit landed during the cycle" - FAIL=1 -fi - -if [[ "$ORPHANS_AFTER" -gt "$ORPHANS_BEFORE" ]]; then - DELTA=$((ORPHANS_AFTER - ORPHANS_BEFORE)) - echo "FAIL: $DELTA new orphaned commit(s) appeared during the cycle" - echo " inspect with: git fsck --unreachable | head -20" - FAIL=1 -fi - -if [[ "$EVOLVE_RC" -ne 0 ]] && [[ "$FAIL" -eq 0 ]]; then - # Cycle reported non-zero exit but invariants hold (commit landed, no orphans). - # Surface as a warning rather than a hard fail — the supervisor may exit - # non-zero on timer/queue-empty even when cycle work succeeded. - echo "WARN: ao evolve exited $EVOLVE_RC but invariants hold; treating as PASS" -fi - -if [[ "$FAIL" -eq 0 ]]; then - echo "PASS: cycle landed a commit and created no new orphans" - if [[ "${SMOKE_KEEP_LOG:-0}" != "1" ]]; then - rm -f "$LOG_FILE" - fi - exit 0 -fi - -echo "" -echo "test-evolve-cycle-smoke: FAILED — log retained at $LOG_FILE" -exit 1 diff --git a/scripts/toolchain-validate.sh b/scripts/toolchain-validate.sh index b2e8115b5..549a44d9e 100755 --- a/scripts/toolchain-validate.sh +++ b/scripts/toolchain-validate.sh @@ -637,17 +637,18 @@ run_semgrep() { return 0 fi - local critical high + local critical advisory critical=$(jq '[.results[]? | select(.extra.severity == "ERROR")] | length' "$output_file" 2>/dev/null || echo 0) - high=$(jq '[.results[]? | select(.extra.severity == "WARNING")] | length' "$output_file" 2>/dev/null || echo 0) + advisory=$(jq '[.results[]? | select(.extra.severity == "WARNING" or .extra.severity == "MEDIUM")] | length' "$output_file" 2>/dev/null || echo 0) critical=${critical:-0} - high=${high:-0} + advisory=${advisory:-0} critical=$(echo "$critical" | tr -d '[:space:]') - high=$(echo "$high" | tr -d '[:space:]') + advisory=$(echo "$advisory" | tr -d '[:space:]') CRITICAL_COUNT=$((CRITICAL_COUNT + critical)) - HIGH_COUNT=$((HIGH_COUNT + high)) - SECURITY_HIGH_COUNT=$((SECURITY_HIGH_COUNT + high)) - TOOL_STATUS["semgrep"]=$([[ "$critical" -gt 0 || "$high" -gt 0 ]] && echo "findings" || echo "pass") + MEDIUM_COUNT=$((MEDIUM_COUNT + advisory)) + # Semgrep WARNING/MEDIUM results are advisory findings, not security HIGH. + # They remain in the report without blocking an otherwise green candidate. + TOOL_STATUS["semgrep"]=$([[ "$critical" -gt 0 || "$advisory" -gt 0 ]] && echo "findings" || echo "pass") } # ============================================================================ diff --git a/scripts/validate-cli-skills-map.sh b/scripts/validate-cli-skills-map.sh deleted file mode 100755 index 0fee07c6c..000000000 --- a/scripts/validate-cli-skills-map.sh +++ /dev/null @@ -1,77 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Usage: -# scripts/validate-cli-skills-map.sh # Validate; exit 1 if drifted -# scripts/validate-cli-skills-map.sh --fix # Rewrite the count line in -# # docs/cli-skills-map.md to -# # match the generated count. - -FIX=false -if [[ "${1:-}" == "--fix" ]]; then - FIX=true -fi - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -MAP_PATH="${CLI_SKILLS_MAP_PATH:-$REPO_ROOT/docs/cli-skills-map.md}" -COMMANDS_PATH="${CLI_COMMANDS_PATH:-$REPO_ROOT/cli/docs/COMMANDS.md}" - -errors=0 - -fail() { - echo "CLI_SKILLS_MAP: $*" - errors=$((errors + 1)) -} - -if [[ ! -f "$MAP_PATH" ]]; then - fail "map not found: $MAP_PATH" -fi - -if [[ ! -f "$COMMANDS_PATH" ]]; then - fail "CLI reference not found: $COMMANDS_PATH" -fi - -if [[ "$errors" -eq 0 ]]; then - generated_count="$(grep -Ec '^### `ao ' "$COMMANDS_PATH" || true)" - declared_count="$(sed -nE 's/.* ([0-9]+) generated CLI command headings.*/\1/p' "$MAP_PATH" | head -n 1)" - - if [[ -z "$declared_count" ]]; then - fail "top audit line must declare '<N> generated CLI command headings'" - elif [[ "$declared_count" != "$generated_count" ]]; then - if $FIX; then - # Portable sed -i: BSD sed (macOS) needs an explicit empty arg, GNU - # sed accepts -i alone. Use a temp-file rewrite to avoid the split. - tmp_map="$(mktemp)" - sed -E "s/([^0-9])${declared_count}( generated CLI command headings)/\\1${generated_count}\\2/" "$MAP_PATH" > "$tmp_map" - mv "$tmp_map" "$MAP_PATH" - echo "CLI_SKILLS_MAP: --fix updated declared count $declared_count -> $generated_count in ${MAP_PATH#"$REPO_ROOT"/}" - declared_count="$generated_count" - else - fail "declared generated CLI command headings=$declared_count, cli/docs/COMMANDS.md has $generated_count" - fi - fi - - if grep -Fq 'tests/rpi-e2e/run-full-rpi.sh' "$MAP_PATH"; then - fail "map references removed tests/rpi-e2e/run-full-rpi.sh" - fi - - if grep -Fq '`ao gate check`' "$MAP_PATH"; then - fail "map still lists phantom subcommand ao gate check" - fi - - if grep -Fq '`ao forge index`' "$MAP_PATH"; then - fail "map still lists phantom subcommand ao forge index" - fi - - # AgentOps 3.0 is hookless (ag-xhhf): the map no longer carries a - # "## Hooks → Commands" section, so the legacy SessionStart-hook table - # assertion was removed. Re-add a hook-surface check only if AgentOps - # ever ships a default runtime hook manifest again. -fi - -if [[ "$errors" -gt 0 ]]; then - exit 1 -fi - -echo "CLI_SKILLS_MAP: PASS (generated CLI command headings: $generated_count)" diff --git a/scripts/validate-codex-backbone-prompts.sh b/scripts/validate-codex-backbone-prompts.sh deleted file mode 100755 index d7d82c16b..000000000 --- a/scripts/validate-codex-backbone-prompts.sh +++ /dev/null @@ -1,148 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" -CATALOG_PATH="$ROOT/skills-codex-overrides/catalog.json" -GENERATED_DIR="$ROOT/skills-codex" - -usage() { - cat <<'EOF' -Usage: bash scripts/validate-codex-backbone-prompts.sh [--repo-root <path>] -EOF -} - -while [[ $# -gt 0 ]]; do - case "$1" in - --repo-root) - ROOT="${2:-}" - shift 2 - ;; - -h|--help) - usage - exit 0 - ;; - *) - echo "Unknown arg: $1" >&2 - usage >&2 - exit 2 - ;; - esac -done - -if [[ "$ROOT" != /* ]]; then - ROOT="$(cd "$ROOT" && pwd)" -fi - -CATALOG_PATH="$ROOT/skills-codex-overrides/catalog.json" -GENERATED_DIR="$ROOT/skills-codex" - -failures=0 - -fail() { - echo "FAIL: $1" >&2 - failures=$((failures + 1)) -} - -require_file() { - local path="$1" - [[ -f "$path" ]] || { - echo "Missing required file: ${path#$ROOT/}" >&2 - exit 1 - } -} - -find_fixed_line() { - local needle="$1" - local path="$2" - grep -nF -m1 -- "$needle" "$path" | cut -d: -f1 -} - -tmpdir="$(mktemp -d)" -cleanup() { - rm -rf "$tmpdir" -} -trap cleanup EXIT - -selected_entries_file="$tmpdir/backbone-entries.jsonl" - -require_file "$CATALOG_PATH" -[[ -d "$GENERATED_DIR" ]] || { - echo "Missing generated Codex skills root: $GENERATED_DIR" >&2 - exit 1 -} -command -v jq >/dev/null 2>&1 || { - echo "jq is required for Codex backbone prompt validation." >&2 - exit 1 -} - -if ! jq -e ' - (.skills | type) == "array" and - all(.skills[]; - (.name | type) == "string" and - ((.operator_contract? | not) or - ( - (.operator_contract | type) == "object" and - (.operator_contract.required_sections | type) == "array" and - (.operator_contract.required_markers | type) == "array" - ) - ) - ) -' "$CATALOG_PATH" >/dev/null; then - echo "Invalid Codex override catalog schema for backbone prompt validation: $CATALOG_PATH" >&2 - exit 1 -fi - -jq -c ' - .skills[] - | select(.operator_contract != null) - | { - name, - required_sections: .operator_contract.required_sections, - required_markers: .operator_contract.required_markers - } -' "$CATALOG_PATH" > "$selected_entries_file" - -selected_count="$(wc -l < "$selected_entries_file" | tr -d ' ')" -if [[ "$selected_count" == "0" ]]; then - fail "no backbone Codex operator contracts found in catalog" -fi - -while IFS= read -r entry; do - [[ -n "$entry" ]] || continue - skill="$(jq -r '.name' <<<"$entry")" - prompt_path="$GENERATED_DIR/$skill/prompt.md" - - if [[ ! -f "$prompt_path" ]]; then - fail "missing generated Codex backbone prompt: skills-codex/$skill/prompt.md" - continue - fi - - previous_line=0 - while IFS= read -r section; do - [[ -n "$section" ]] || continue - line="$(find_fixed_line "$section" "$prompt_path" || true)" - if [[ -z "$line" ]]; then - fail "generated prompt for $skill is missing required section: $section" - continue - fi - if (( line <= previous_line )); then - fail "generated prompt for $skill has out-of-order sections around: $section" - fi - previous_line="$line" - done < <(jq -r '.required_sections[]' <<<"$entry") - - while IFS= read -r marker; do - [[ -n "$marker" ]] || continue - if ! grep -Fq -- "$marker" "$prompt_path"; then - fail "generated prompt for $skill is missing required behavior marker: $marker" - fi - done < <(jq -r '.required_markers[]' <<<"$entry") -done < "$selected_entries_file" - -if [[ "$failures" -gt 0 ]]; then - echo "Repair flow: bash scripts/refresh-codex-artifacts.sh --scope worktree" >&2 - echo "Codex backbone prompt validation FAILED ($failures finding(s))." >&2 - exit 1 -fi - -echo "Codex backbone prompt validation passed for $selected_count skill(s)." diff --git a/scripts/validate-codex-generated-artifacts.sh b/scripts/validate-codex-generated-artifacts.sh index 29548afc1..89622e7d0 100755 --- a/scripts/validate-codex-generated-artifacts.sh +++ b/scripts/validate-codex-generated-artifacts.sh @@ -160,7 +160,6 @@ fi while IFS= read -r skill_dir; do [[ -f "$skill_dir/SKILL.md" ]] || continue skill_name="$(basename "$skill_dir")" - [[ "$skill_name" == "pre-mortem" || "$skill_name" == "post-mortem" || "$skill_name" == "pre_mortem" || "$skill_name" == "post_mortem" ]] && continue [[ -f "$skill_dir/$MARKER_FILE_NAME" ]] || fail "missing Codex artifact marker: ${skill_dir#"$ROOT"/}/$MARKER_FILE_NAME" if grep -qE "^description:[[:space:]]*['\"]?[>|]['\"]?[[:space:]]*$" "$skill_dir/SKILL.md"; then fail "malformed generated description frontmatter: ${skill_dir#"$ROOT"/}/SKILL.md" diff --git a/scripts/validate-codex-generated-manifest.sh b/scripts/validate-codex-generated-manifest.sh index c514335b0..66ae9d92e 100755 --- a/scripts/validate-codex-generated-manifest.sh +++ b/scripts/validate-codex-generated-manifest.sh @@ -61,8 +61,6 @@ package_dirs = [ ] skill_dirs = [] for skill_dir in sorted(p for p in skills_root.iterdir() if p.is_dir()): - if skill_dir.name in {"pre-mortem", "post-mortem", "pre_mortem", "post_mortem"}: - continue if (skill_dir / "SKILL.md").exists(): skill_dirs.append(skill_dir) diff --git a/scripts/validate-codex-override-coverage.sh b/scripts/validate-codex-override-coverage.sh index 28ef31677..de65a71de 100755 --- a/scripts/validate-codex-override-coverage.sh +++ b/scripts/validate-codex-override-coverage.sh @@ -208,7 +208,6 @@ find "$SKILLS_DIR" -mindepth 1 -maxdepth 1 -type d \ | while IFS= read -r d; do [[ -f "$d/SKILL.md" ]] || continue name="$(basename "$d")" - [[ "$name" == "pre-mortem" || "$name" == "post-mortem" || "$name" == "pre_mortem" || "$name" == "post_mortem" ]] && continue printf '%s\n' "$name" done \ | LC_ALL=C sort -u > "$source_skills_file" @@ -377,8 +376,8 @@ while IFS= read -r entry; do if jq -e '.operator_contract_required == true' <<<"$entry" >/dev/null; then fail "parity-only skill cannot require operator-contract governance: $skill" fi - if [[ -d "$override_dir" ]]; then - fail "parity-only skill has unexpected Codex override directory: $skill" + if [[ -f "$override_prompt" ]]; then + fail "parity-only skill has unexpected Codex override prompt: $skill" fi ;; excluded) @@ -388,8 +387,8 @@ while IFS= read -r entry; do if [[ -d "$GENERATED_DIR/$skill" ]]; then fail "excluded skill still has a generated Codex twin dir (git rm -r skills-codex/$skill): $skill" fi - if [[ -d "$override_dir" ]]; then - fail "excluded skill has unexpected Codex override directory: $skill" + if [[ -f "$override_prompt" ]]; then + fail "excluded skill has unexpected Codex override prompt: $skill" fi ;; *) @@ -400,6 +399,7 @@ done < "$selected_entries_file" find "$OVERRIDES_DIR" -mindepth 1 -maxdepth 1 -type d \ | while IFS= read -r d; do + [[ -f "$d/prompt.md" ]] || continue basename "$d" done \ | LC_ALL=C sort -u > "$actual_override_dirs_file" diff --git a/scripts/validate-context-map-drift.sh b/scripts/validate-context-map-drift.sh deleted file mode 100755 index c610e137b..000000000 --- a/scripts/validate-context-map-drift.sh +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env bash -# validate-context-map-drift.sh — detect drift between SKILL.md frontmatter and -# the generated docs/contracts/context-map.md. -# -# Per DDD+Hexagonal v1 plan Issue #5 (Fix 3): the context map is a generated -# artifact whose source of truth is `skills/*/SKILL.md` frontmatter. Drift -# happens when a SKILL.md is edited (hexagonal_role/consumes/produces/context_rel -# changes) without regenerating the map. This gate forces the regeneration to -# be committed alongside the SKILL.md edit. -# -# Behaviour: -# - Exit 0 if regenerating yields the same bytes as the current on-disk -# context-map. -# - Exit 1 if drift is detected, with a helpful "how to fix" message on stderr. -# -# Safety: the script never leaves the working tree dirty. The current -# context-map.md is backed up to a temp file and restored on exit (trap), -# whether the script exits cleanly or via error. -set -euo pipefail - -CONTEXT_MAP="${CONTEXT_MAP:-docs/contracts/context-map.md}" -GENERATOR="${GENERATOR:-scripts/generate-context-map.sh}" - -if [[ ! -f "$CONTEXT_MAP" ]]; then - echo "validate-context-map-drift: missing $CONTEXT_MAP" >&2 - exit 1 -fi - -if [[ ! -x "$GENERATOR" && ! -f "$GENERATOR" ]]; then - echo "validate-context-map-drift: missing $GENERATOR" >&2 - exit 1 -fi - -TMP_BACKUP="$(mktemp -t context-map-backup.XXXXXX.md)" -cp -f "$CONTEXT_MAP" "$TMP_BACKUP" - -# shellcheck disable=SC2329 # invoked indirectly via trap -cleanup() { - # Always restore the original committed context-map so partial failures - # (or a real drift detection) never leave the working tree dirty. - if [[ -f "$TMP_BACKUP" ]]; then - cp -f "$TMP_BACKUP" "$CONTEXT_MAP" - rm -f "$TMP_BACKUP" - fi -} -trap cleanup EXIT - -bash "$GENERATOR" >/dev/null - -if cmp -s "$CONTEXT_MAP" "$TMP_BACKUP"; then - exit 0 -fi - -cat >&2 <<'EOF' -Context map drift detected. To fix: - bash scripts/generate-context-map.sh - git add docs/contracts/context-map.md -Then commit. -EOF - -exit 1 diff --git a/scripts/validate-local.sh b/scripts/validate-local.sh old mode 100755 new mode 100644 index 31d0d2b78..05f5db549 --- a/scripts/validate-local.sh +++ b/scripts/validate-local.sh @@ -1,94 +1,32 @@ -#!/bin/bash -# Local plugin validation - manual wrapper around the same gate used on push. -# Usage: ./scripts/validate-local.sh [--scope worktree] [--skip-claude] +#!/usr/bin/env bash +# Convenience wrapper for the ordinary deterministic repository checks. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[0;33m' -NC='\033[0m' - -pass() { echo -e "${GREEN}✓${NC} $1"; } -fail() { echo -e "${RED}✗${NC} $1"; errors=$((errors + 1)); } -warn() { echo -e "${YELLOW}!${NC} $1"; } -print_indented() { - local text="$1" - while IFS= read -r line; do - printf ' %s\n' "$line" - done <<<"$text" -} +scope="worktree" +mode="--fast" usage() { cat <<'EOF' -Usage: ./scripts/validate-local.sh [--scope auto|upstream|staged|worktree|head] [--skip-claude] +Usage: ./scripts/validate-local.sh [--scope head|staged|worktree|upstream|range:<base>..<head>] [--full] -Preferred hook setup: - bash scripts/install-dev-hooks.sh +Runs ao gate check as a deterministic test command. It installs no hook, +serializes no caller, invokes no model runtime, and conveys no semantic verdict. EOF } -errors=0 -SCOPE="worktree" -SKIP_CLAUDE="false" -VALIDATE_LOCAL_LOCK_DIR="" - -release_validate_local_lock() { - local pid_file - - [[ -n "$VALIDATE_LOCAL_LOCK_DIR" ]] || return 0 - pid_file="$VALIDATE_LOCAL_LOCK_DIR/pid" - - if [[ -f "$pid_file" ]] && [[ "$(<"$pid_file")" != "$$" ]]; then - return 0 - fi - - rm -rf "$VALIDATE_LOCAL_LOCK_DIR" -} - -acquire_validate_local_lock() { - local git_dir pid_file existing_pid - - git_dir="$(git rev-parse --git-dir 2>/dev/null || printf '%s\n' "$REPO_ROOT/.git")" - if [[ "$git_dir" != /* ]]; then - git_dir="$REPO_ROOT/$git_dir" - fi - - VALIDATE_LOCAL_LOCK_DIR="$git_dir/agentops-validate-local.lock" - pid_file="$VALIDATE_LOCAL_LOCK_DIR/pid" - - while true; do - if mkdir "$VALIDATE_LOCAL_LOCK_DIR" 2>/dev/null; then - printf '%s\n' "$$" > "$pid_file" - trap release_validate_local_lock EXIT INT TERM - return 0 - fi - - existing_pid="" - if [[ -f "$pid_file" ]]; then - existing_pid="$(<"$pid_file")" - fi - - if [[ -n "$existing_pid" ]] && kill -0 "$existing_pid" 2>/dev/null; then - echo "error: another local validation is already running (pid $existing_pid)" >&2 - return 1 - fi - - rm -rf "$VALIDATE_LOCAL_LOCK_DIR" - done -} - while [[ $# -gt 0 ]]; do case "$1" in --scope) - SCOPE="${2:-}" + scope="${2:-}" + [[ -n "$scope" ]] || { usage >&2; exit 2; } shift 2 ;; - --skip-claude) - SKIP_CLAUDE="true" + --full) + mode="--full" shift ;; -h|--help) @@ -96,69 +34,17 @@ while [[ $# -gt 0 ]]; do exit 0 ;; *) - echo "Unknown arg: $1" >&2 + echo "unknown argument: $1" >&2 usage >&2 exit 2 ;; esac done -cd "$REPO_ROOT" - -if ! acquire_validate_local_lock; then - exit 1 -fi - -hooks_path="$(git config --local --get core.hooksPath 2>/dev/null || true)" -if [[ "$hooks_path" != ".githooks" ]]; then - warn "core.hooksPath is '${hooks_path:-<unset>}' (recommended: .githooks)" - warn "Run: bash scripts/install-dev-hooks.sh" -fi - -echo "" -echo "🔍 Running manual local validation..." -echo "" -echo "═══════════════════════════════════════════════════════" -echo " AgentOps Manual Local Validation" -echo "═══════════════════════════════════════════════════════" -echo "" - -echo "── Shared Local Gate ──" ao_bin="${AO_BIN:-}" [[ -z "$ao_bin" && -x "$REPO_ROOT/cli/bin/ao" ]] && ao_bin="$REPO_ROOT/cli/bin/ao" [[ -z "$ao_bin" ]] && ao_bin="$(command -v ao 2>/dev/null || true)" -if [[ -z "$ao_bin" ]]; then - fail "ao not resolvable — build cli/bin/ao or set AO_BIN" -elif "$ao_bin" gate check --fast --scope "$SCOPE"; then - pass "Shared local gate passed" -else - fail "Shared local gate failed" -fi -echo "" +[[ -n "$ao_bin" ]] || { echo "ao is not available; build cli/bin/ao or set AO_BIN" >&2; exit 1; } -if [[ "$SKIP_CLAUDE" != "true" ]]; then - echo "── Claude CLI ──" - if command -v claude &>/dev/null; then - load_output=$(timeout 10 claude --plugin-dir . --help 2>&1) || true - if echo "$load_output" | grep -qiE "invalid manifest|validation error|failed to load"; then - fail "Claude CLI load failed" - echo "$load_output" | grep -iE "invalid|failed|error" | head -3 | sed 's/^/ /' - else - pass "Claude CLI loads plugin" - fi - else - warn "Claude CLI not available for load test" - fi - echo "" -fi - -echo "═══════════════════════════════════════════════════════" -if [[ $errors -gt 0 ]]; then - echo -e "${RED} VALIDATION FAILED: $errors errors${NC}" - echo "═══════════════════════════════════════════════════════" - exit 1 -else - echo -e "${GREEN} ALL VALIDATIONS PASSED${NC}" - echo "═══════════════════════════════════════════════════════" - exit 0 -fi +cd "$REPO_ROOT" +exec "$ao_bin" gate check "$mode" --scope "$scope" diff --git a/scripts/validate-skill-body-refs.sh b/scripts/validate-skill-body-refs.sh index 55c31f768..a5cbd8bb7 100755 --- a/scripts/validate-skill-body-refs.sh +++ b/scripts/validate-skill-body-refs.sh @@ -44,13 +44,7 @@ if [[ -z "$AO_BIN" ]]; then AO_BIN="$TMP_DIR/ao" ( cd "$REPO_ROOT/cli" - # Build with the ADR-0012 archive tags so skill body-refs that document - # archived-but-revivable commands (e.g. `ao harvest`, `ao turn verify`, - # behind //go:build flywheel|legacy) validate against the FULL command - # surface. The default `ao` omits them, but a skill may legitimately - # reference any command; validating only the spine would false-fail those. - # (Same escape/fix as validate-skill-cli-snippets.sh, bead age-sydq.) - go build -tags "flywheel legacy" -o "$AO_BIN" ./cmd/ao + go build -o "$AO_BIN" ./cmd/ao ) fi diff --git a/scripts/validate-skill-flow.sh b/scripts/validate-skill-flow.sh deleted file mode 100755 index 44c656f0d..000000000 --- a/scripts/validate-skill-flow.sh +++ /dev/null @@ -1,387 +0,0 @@ -#!/usr/bin/env bash -# validate-skill-flow.sh — Enforce skill-flow connectivity across every -# skills/<name>/SKILL.md. -# -# Motivation (follow-up to scripts/audit-skill-metadata.sh, ag-f0i): -# audit-skill-metadata.sh owns `context_rel.with` resolution. It explicitly -# deferred two checks as "discovered follow-ups, not enforced": -# 1. `consumes` vocabulary canonicality (open vocabulary, no registry). -# 2. skill-to-skill connectivity ("do all skills flow together?"). -# This gate closes both, plus checks `metadata.dependencies` resolution. -# -# What it checks (FAIL = exit 1): -# 1. CLOSED CONSUMES VOCABULARY. Every `consumes` token must be either a real -# skill slug or one of the whitelisted EXTERNAL_INPUTS (see below). This -# turns `consumes` from an open free-text field into a closed contract so -# the producer->consumer graph can be reasoned about. -# 2. metadata.dependencies RESOLUTION. Every `metadata.dependencies` entry -# must name an existing skill slug. -# 3. ORPHAN DETECTION. A skill is "connected" if it shares at least one -# skill-to-skill edge with another skill, counting ALL THREE edge layers -# (consumes skill-slugs, context_rel.with skill-slugs, metadata.dependencies). -# Orphans must be listed in the standalone allowlist -# (scripts/skill-flow-standalone.txt) — intentionally-standalone meta / -# utility / boundary skills. An un-allowlisted orphan FAILS. -# -# What it REPORTS (informational, never fails): -# - Cross-layer disagreement: `consumes` skill-slugs vs `metadata.dependencies` -# (the two fields drifted historically; surfaced so they can be reconciled). -# - Dead-end produced artifacts (produced but consumed by no skill). -# -# context_rel.with resolution stays owned by audit-skill-metadata.sh — this gate -# does not re-litigate it (and tolerates `*.md` doc targets used by entry-point -# skills such as session-bootstrap). -# -# Usage: -# bash scripts/validate-skill-flow.sh [--json] [--skills-root DIR] [--allowlist FILE] -# -# --json emit a machine-readable verdict on stdout (stdout = data only) -# --skills-root directory holding skills/<name>/SKILL.md -# (default: <repo>/skills, or $SKILL_FLOW_SKILLS_ROOT) -# --allowlist standalone-skill allowlist file -# (default: <repo>/scripts/skill-flow-standalone.txt) -# -h, --help show this help -# -# Exit codes: 0 = clean, 1 = findings, 2 = usage / environment error. -# -# Contract reference: docs/contracts/skill-flow.md -set -uo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" - -JSON=0 -SKILLS_ROOT="${SKILL_FLOW_SKILLS_ROOT:-${REPO_ROOT}/skills}" -ALLOWLIST="${SKILL_FLOW_ALLOWLIST:-${REPO_ROOT}/scripts/skill-flow-standalone.txt}" - -usage() { - cat <<'USAGE' -validate-skill-flow.sh — enforce skill-flow connectivity across all SKILL.md. - -Checks (fail): closed consumes vocabulary, metadata.dependencies resolution, -and orphan detection (un-allowlisted skill with zero skill-to-skill edges). -Reports (advisory): consumes vs metadata.dependencies disagreement, dead-end -produced artifacts. - -Usage: - bash scripts/validate-skill-flow.sh [--json] [--skills-root DIR] [--allowlist FILE] - - --json emit a machine-readable verdict on stdout (stdout = data only) - --skills-root directory holding skills/<name>/SKILL.md - (default: <repo>/skills, or $SKILL_FLOW_SKILLS_ROOT) - --allowlist standalone-skill allowlist file - (default: <repo>/scripts/skill-flow-standalone.txt) - -h, --help show this help - -Exit codes: 0 = clean, 1 = findings, 2 = usage / environment error. -USAGE -} - -while [[ $# -gt 0 ]]; do - case "$1" in - --json) JSON=1; shift ;; - --skills-root) SKILLS_ROOT="${2:?--skills-root needs a value}"; shift 2 ;; - --allowlist) ALLOWLIST="${2:?--allowlist needs a value}"; shift 2 ;; - -h|--help) usage; exit 0 ;; - --*) echo "ERROR: unknown flag: $1 (try: bash scripts/validate-skill-flow.sh --help)" >&2; exit 2 ;; - *) echo "ERROR: unexpected argument: $1 (try: bash scripts/validate-skill-flow.sh --help)" >&2; exit 2 ;; - esac -done - -if [[ ! -d "${SKILLS_ROOT}" ]]; then - echo "ERROR: skills directory not found at ${SKILLS_ROOT}" >&2 - exit 2 -fi - -SKILLS_ROOT="${SKILLS_ROOT}" ALLOWLIST="${ALLOWLIST}" JSON="${JSON}" python3 - <<'PYEOF' -import json -import os -import re -import sys -from pathlib import Path - -try: - import yaml # type: ignore -except Exception as e: # pragma: no cover - environment guard - sys.stderr.write("ERROR: PyYAML is required (pip install pyyaml). underlying: %s\n" % e) - sys.exit(2) - -SKILLS_ROOT = Path(os.environ["SKILLS_ROOT"]) -ALLOWLIST = Path(os.environ["ALLOWLIST"]) -JSON = os.environ.get("JSON") == "1" - -# Closed vocabulary for non-skill `consumes` tokens. These are the *external -# inputs* a skill may read that are not themselves produced by a peer skill -# (VCS state, the br issue store, an upstream API, the repo working tree, the -# onboarding handshake). Adding a new external input is a deliberate act: extend -# this list AND document it in docs/contracts/skill-flow.md. -EXTERNAL_INPUTS = { - "Cargo.lock", - "Cargo.toml", - "br", - "build-config", - "cargo-metadata", - "cli-source", - "closed-beads", - "code", - "code-under-review", - "codebase", - "codex-plugin", - "command-help", - "command-map", - "convention-target", - "crate-docs", - "crate-source", - "data-model", - "environment-contract", - "error-reports", - "evidence", - "existing-docs", - "existing-tracked-work", - "external-api", - "external-source-candidates", - "failure-report", - "ffi-bindings", - "ffi-contracts", - "gemini-extension", - "git", - "git-worktree", - "github-pr", - "hook-policy", - "implementation-examples", - "installation-docs", - "manifest-and-lockfile", - "mcp-server", - "onboard", - "operational-constraints", - "package-metadata", - "product-requirements", - "profiler-output", - "project-context", - "project-goals", - "project-source", - "release-notes", - "repo-context", - "repo-tree", - "repository", - "runtime-configuration", - "runtime-metrics", - "rust-source", - "service-contract", - "skill-bundle", - "source-code", - "specification", - "support-history", - "task-intent", - "task-question", - "test-plan", - "test-results", - "test-suite", - "test-target", - "tests", -} - -FRONTMATTER_RE = re.compile(r"^---\s*\n(.*?)\n---\s*\n", re.DOTALL) - - -def parse_frontmatter(skill_md): - try: - text = skill_md.read_text(encoding="utf-8") - except Exception: - return {} - m = FRONTMATTER_RE.match(text) - if not m: - return {} - try: - data = yaml.safe_load(m.group(1)) - except Exception: - return {} - return data if isinstance(data, dict) else {} - - -def str_list(value): - if not isinstance(value, list): - return [] - return [str(x) for x in value if x is not None] - - -def load_allowlist(path): - """Return set of allowlisted standalone skill slugs (# comments allowed).""" - if not path.is_file(): - return set() - out = set() - for line in path.read_text(encoding="utf-8").splitlines(): - line = line.split("#", 1)[0].strip() - if line: - out.add(line) - return out - - -# 1. Load every skill's frontmatter. -skill_dirs = sorted( - p for p in SKILLS_ROOT.iterdir() if p.is_dir() and (p / "SKILL.md").is_file() -) -names = {p.name for p in skill_dirs} - -skills = {} -for sd in skill_dirs: - fm = parse_frontmatter(sd / "SKILL.md") - consumes = str_list(fm.get("consumes")) - produces = str_list(fm.get("produces")) - ctx = [] - for e in (fm.get("context_rel") or []): - if isinstance(e, dict) and isinstance(e.get("with"), str): - ctx.append(e["with"].strip()) - md = fm.get("metadata") or {} - mdeps = str_list(md.get("dependencies")) if isinstance(md, dict) else [] - skills[sd.name] = { - "consumes": consumes, - "produces": produces, - "ctx": ctx, - "mdeps": mdeps, - } - -allowlist = load_allowlist(ALLOWLIST) - -# Every artifact produced by some skill — a `consumes` token may legitimately -# name one (e.g. push consumes git-changes; beads consumes bd-issue). -produced_artifacts = set() -for d in skills.values(): - produced_artifacts.update(d["produces"]) - -failures = [] # list of (kind, slug, detail) - -# CHECK 1: closed consumes vocabulary. A consumes token must resolve to one of: -# a peer skill slug, a whitelisted external input, or an artifact produced by -# some skill. Anything else is a typo or an undeclared dependency. -for slug in sorted(skills): - for tok in skills[slug]["consumes"]: - if tok in names or tok in EXTERNAL_INPUTS or tok in produced_artifacts: - continue - failures.append(( - "consumes-vocabulary", - slug, - "consumes '%s' resolves to nothing: not a skill slug, not a " - "whitelisted external input (%s), and not an artifact any skill " - "produces" % (tok, ", ".join(sorted(EXTERNAL_INPUTS))), - )) - -# CHECK 2: metadata.dependencies resolution. -for slug in sorted(skills): - for tok in skills[slug]["mdeps"]: - if tok not in names: - failures.append(( - "metadata-dependencies", - slug, - "metadata.dependencies '%s' does not resolve to a skill slug" % tok, - )) - -# Build undirected skill-to-skill edge set across all three layers. -edges = set() -for slug, d in skills.items(): - for tok in d["consumes"]: - if tok in names and tok != slug: - edges.add(frozenset((slug, tok))) - for tok in d["ctx"]: - if tok in names and tok != slug: - edges.add(frozenset((slug, tok))) - for tok in d["mdeps"]: - if tok in names and tok != slug: - edges.add(frozenset((slug, tok))) - -degree = {slug: 0 for slug in skills} -for e in edges: - for slug in e: - degree[slug] += 1 - -orphans = sorted(s for s in skills if degree[s] == 0) - -# CHECK 3: orphans must be allowlisted. -unallowed_orphans = [s for s in orphans if s not in allowlist] -for slug in unallowed_orphans: - failures.append(( - "orphan", - slug, - "no skill-to-skill edge in consumes/context_rel/metadata.dependencies; " - "wire an edge or add to scripts/skill-flow-standalone.txt with a rationale", - )) - -# Stale allowlist entries (allowlisted but actually connected, or not a skill). -stale_allowlist = sorted( - s for s in allowlist if s not in skills or (s in skills and degree[s] > 0) -) - -# Informational: consumes-skill vs metadata.dependencies disagreement. -disagreements = [] -for slug in sorted(skills): - cs = {t for t in skills[slug]["consumes"] if t in names} - md = set(skills[slug]["mdeps"]) - if (cs or md) and cs != md: - disagreements.append((slug, sorted(cs), sorted(md))) - -# Informational: dead-end produced artifacts (produced, consumed by no skill). -consumed_tokens = set() -for d in skills.values(): - consumed_tokens.update(d["consumes"]) -produced = {} -for slug, d in skills.items(): - for art in d["produces"]: - produced.setdefault(art, []).append(slug) -dead_end = sorted(a for a in produced if a not in consumed_tokens) - -verdict = "PASS" if not failures else "FAIL" - -if JSON: - print(json.dumps({ - "verdict": verdict, - "skills_checked": len(skills), - "edges": len(edges), - "orphans": orphans, - "failures": [ - {"kind": k, "skill": s, "detail": d} for (k, s, d) in failures - ], - "stale_allowlist": stale_allowlist, - "disagreements": [ - {"skill": s, "consumes": cs, "metadata_dependencies": md} - for (s, cs, md) in disagreements - ], - "dead_end_artifacts": dead_end, - }, indent=2, sort_keys=True)) -else: - print("validate-skill-flow: %d skill(s), %d skill-to-skill edge(s)" % ( - len(skills), len(edges))) - print(" orphans: %d (allowlisted standalone: %d)" % ( - len(orphans), len([o for o in orphans if o in allowlist]))) - if disagreements: - print("") - print("INFO: %d skill(s) where consumes(skills) != metadata.dependencies " - "(reconcile, not fatal):" % len(disagreements)) - for slug, cs, md in disagreements: - print(" - %-26s consumes=%s metadata.deps=%s" % ( - slug, cs or "-", md or "-")) - if dead_end: - print("") - print("INFO: %d produced artifact(s) consumed by no skill " - "(output-type annotation, not fatal):" % len(dead_end)) - for art in dead_end: - print(" - %s (from: %s)" % (art, ", ".join(produced[art]))) - if stale_allowlist: - print("") - print("WARN: %d stale allowlist entry/entries (now connected or not a " - "skill — remove from scripts/skill-flow-standalone.txt):" - % len(stale_allowlist)) - for slug in stale_allowlist: - print(" - %s" % slug) - if failures: - print("") - print("FAIL: %d finding(s):" % len(failures)) - for kind, slug, detail in failures: - print(" [%s] %s/SKILL.md: %s" % (kind, slug, detail)) - print("") - print("fix: see docs/contracts/skill-flow.md") - else: - print("") - print("OK: skill flow is connected and the consumes vocabulary is closed.") - -sys.exit(0 if verdict == "PASS" else 1) -PYEOF diff --git a/scripts/validate-workflow-contract.sh b/scripts/validate-workflow-contract.sh deleted file mode 100755 index 9276cab31..000000000 --- a/scripts/validate-workflow-contract.sh +++ /dev/null @@ -1,201 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -mode="${1:-source}" -if [[ "$mode" != "source" ]]; then - echo "usage: scripts/validate-workflow-contract.sh source" >&2 - exit 2 -fi - -repo_root="$(git rev-parse --show-toplevel)" -fixtures="$repo_root/tests/fixtures/four-umbrella-workflow" - -REPO_ROOT="$repo_root" FIXTURES="$fixtures" python3 - <<'PY' -import json -import os -from pathlib import Path - -root = Path(os.environ["REPO_ROOT"]) -fixtures = Path(os.environ["FIXTURES"]) -failures: list[str] = [] - - -def text(relative: str) -> str: - path = root / relative - if not path.is_file(): - failures.append(f"missing source contract: {relative}") - return "" - return path.read_text(encoding="utf-8") - - -def require(relative: str, *markers: str) -> None: - body = text(relative) - for marker in markers: - if marker not in body: - failures.append(f"{relative} missing contract marker: {marker}") - - -require( - "skills/rpi/SKILL.md", - "Validate -> Learn -> orchestrator", - "Learn is the only post-verdict handoff", - "Only the orchestrator may invoke Premortem", -) -require( - "skills/rpi/references/agile-replan-loop.md", - "Validate -> Learn -> orchestrator", - "material_change", - "no_change", - "terminal", -) -require( - "skills/learn/SKILL.md", - "plan_impact", - "returns it to the orchestrator", - "does not invoke Premortem", -) -require( - "skills/premortem/SKILL.md", - "changed plan", - "explicit orchestrator request", -) -require( - "skills/discovery/SKILL.md", - "explicit orchestrator re-plan request", -) -require( - "skills/crank/SKILL.md", - "wave evidence to Validate", - "does not invoke Discovery, Learn, or Premortem", -) -require( - "skills/evolve/SKILL.md", - "Validate -> Learn -> orchestrator", - "changed plan through Premortem", -) -require( - "skills-codex/rpi/SKILL.md", - "Validate -> Learn -> orchestrator", - "Learn is the only post-verdict handoff", - "Only the orchestrator may invoke Premortem", -) -require( - "skills-codex/rpi/references/agile-replan-loop.md", - "Validate -> Learn -> orchestrator", - "material_change", - "no_change", - "terminal", -) -require( - "skills-codex/learn/SKILL.md", - "plan_impact", - "returns it to the orchestrator", - "does not invoke Premortem", -) -require( - "skills-codex/premortem/SKILL.md", - "changed plan", - "explicit orchestrator request", -) -require( - "skills-codex/discovery/SKILL.md", - "explicit orchestrator re-plan request", -) -require( - "skills-codex/crank/SKILL.md", - "wave evidence to Validate", - "does not invoke Discovery, Learn, or Premortem", -) -require( - "skills-codex/evolve/SKILL.md", - "Validate -> Learn -> orchestrator", - "changed plan through Premortem", -) - -learn_schema_path = root / "skills/learn/schemas/learn-receipt.schema.json" -try: - learn_schema = json.loads(learn_schema_path.read_text(encoding="utf-8")) -except (OSError, json.JSONDecodeError) as exc: - failures.append(f"invalid Learn receipt schema: {exc}") -else: - required = set(learn_schema.get("required", [])) - if not {"remaining_work", "plan_impact"}.issubset(required): - failures.append("Learn receipt must require remaining_work and plan_impact") - disposition = ( - learn_schema.get("properties", {}) - .get("plan_impact", {}) - .get("properties", {}) - .get("disposition", {}) - .get("enum", []) - ) - if set(disposition) != {"material_change", "no_change", "terminal"}: - failures.append("Learn plan_impact must expose exactly material_change, no_change, terminal") - codex_schema_path = root / "skills-codex/learn/schemas/learn-receipt.schema.json" - if not codex_schema_path.is_file(): - failures.append("missing Codex Learn receipt schema") - elif codex_schema_path.read_bytes() != learn_schema_path.read_bytes(): - failures.append("source and Codex Learn receipt schemas differ") - - -def validate_packet(packet: dict) -> list[str]: - errors: list[str] = [] - if packet.get("validate_next") != "learn": - errors.append("Validate must hand off to Learn") - if packet.get("learn_next") != "orchestrator": - errors.append("Learn must return to the orchestrator") - - remaining = packet.get("remaining_work") - disposition = packet.get("learn_disposition") - decision = packet.get("orchestrator_decision") - changed = packet.get("plan_changed") - premortem = packet.get("premortem_invoked") - - if remaining is True and disposition == "material_change": - if decision != "replan" or changed is not True or premortem is not True: - errors.append("material delta requires orchestrator replan, changed plan, then Premortem") - elif remaining is True and disposition == "no_change": - if decision not in {"retry", "continue", "stop", "escalate"}: - errors.append("no_change requires an explicit orchestrator decision") - if changed is not False or premortem is not False: - errors.append("no_change must not fabricate a plan change or invoke Premortem") - elif remaining is False and disposition == "terminal": - if decision != "close" or changed is not False or premortem is not False: - errors.append("terminal work must close without plan mutation or Premortem") - else: - errors.append("remaining_work and Learn disposition are inconsistent") - return errors - - -positive = sorted(fixtures.glob("valid-*.json")) -negative = sorted(fixtures.glob("invalid-*.json")) -if len(positive) < 3: - failures.append("workflow contract needs material, no-change, and terminal positive fixtures") -if len(negative) < 3: - failures.append("workflow contract needs direct-Premortem and silent-retry negative fixtures") - -for path in positive: - try: - packet = json.loads(path.read_text(encoding="utf-8")) - except json.JSONDecodeError as exc: - failures.append(f"{path.name} is invalid JSON: {exc}") - continue - errors = validate_packet(packet) - if errors: - failures.append(f"positive fixture {path.name} rejected: {'; '.join(errors)}") - -for path in negative: - try: - packet = json.loads(path.read_text(encoding="utf-8")) - except json.JSONDecodeError as exc: - failures.append(f"{path.name} is invalid JSON: {exc}") - continue - if not validate_packet(packet): - failures.append(f"negative fixture {path.name} was accepted") - -if failures: - for failure in failures: - print(f"FAIL: {failure}") - raise SystemExit(f"four-umbrella workflow contract: FAIL ({len(failures)})") - -print("four-umbrella workflow contract: PASS") -PY diff --git a/scripts/verified-rebase.sh b/scripts/verified-rebase.sh deleted file mode 100755 index 79e277193..000000000 --- a/scripts/verified-rebase.sh +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/env bash -# verified-rebase.sh <expected-head-subject> -# -# Wraps `git rebase --continue` with post-condition checks that catch the silent- -# failure pathology observed in evolve cron loops (cycles 223/224, PRs #337/#344): -# `git rebase --continue` can return 0 yet leave the rebase stuck or drop a commit, -# so a follow-on `regen + push` runs against the wrong HEAD and the push is a no-op. -# -# Promoted from an inline cron-prompt function so crons, agents, and sessions share -# one verified path instead of re-deriving it (soc-e9n6). -# -# Usage: -# verified-rebase.sh "<subject the commit at HEAD should have after a good rebase>" -# Exit 0 only if: no rebase is still in progress AND HEAD's subject == expected. -set -uo pipefail - -expected="${1:-}" -if [ -z "$expected" ]; then - echo "usage: verified-rebase.sh <expected-head-subject>" >&2 - exit 2 -fi - -gitdir="$(git rev-parse --git-dir 2>/dev/null)" || { echo "verified-rebase: not a git repository" >&2; exit 2; } - -rebase_in_progress() { [ -d "$gitdir/rebase-merge" ] || [ -d "$gitdir/rebase-apply" ]; } - -# Continue only if a rebase is actually in progress. Force a non-interactive -# editor — this script is built for unattended cron/agent use, where the default -# commit-message editor on `rebase --continue` would hang or fail ("Terminal is -# dumb, but EDITOR unset"). GIT_EDITOR=true accepts the existing message as-is. -if rebase_in_progress; then - if ! GIT_EDITOR=true GIT_SEQUENCE_EDITOR=true git rebase --continue; then - echo "verified-rebase: FAIL — 'git rebase --continue' returned non-zero" >&2 - exit 1 - fi -fi - -# Post-condition 1: the rebase must be fully done (the silent-stuck catch). -if rebase_in_progress; then - echo "verified-rebase: FAIL — rebase still in progress after --continue (silent failure)" >&2 - exit 1 -fi - -# Post-condition 2: HEAD must be the expected commit (the dropped-commit catch). -actual="$(git log -1 --format=%s 2>/dev/null || true)" -if [ "$actual" != "$expected" ]; then - echo "verified-rebase: FAIL — HEAD subject '$actual' != expected '$expected' (commit may have been dropped)" >&2 - exit 1 -fi - -echo "verified-rebase: OK — HEAD is '$actual', no rebase in progress" diff --git a/scripts/verify-buildtags.sh b/scripts/verify-buildtags.sh deleted file mode 100755 index d8cd02e0d..000000000 --- a/scripts/verify-buildtags.sh +++ /dev/null @@ -1,77 +0,0 @@ -#!/bin/bash -# -# verify-buildtags.sh — prove the ADR-0012 build-tag archive mechanism. -# -# The default `go build` must OMIT the archived command sets; the `flywheel` and -# `legacy` tags must restore them. We can't grep for archived commands until -# Tagged builds retain the hidden `ao buildtags` introspection surface. The -# default ADR-0012 spine deliberately removes that archived command entirely: -# default build -> `ao buildtags` absent -# -tags flywheel -> "flywheel" -# -tags legacy -> "legacy" -# -tags flywheel legacy-> "flywheel" + "legacy" -# -# Exit codes: 0 = mechanism works; 1 = a variant did not report as expected; -# 2 = script/build error. - -set -euo pipefail - -ROOT="$(cd "$(dirname "$0")/.." && pwd)" -CLI="$ROOT/cli" -TMP="$(mktemp -d)" -trap 'rm -rf "$TMP"' EXIT - -build() { # build <out> [tags...] - local out="$1"; shift - local tagflag=() - if [ "$#" -gt 0 ]; then tagflag=(-tags "$*"); fi - ( cd "$CLI" && go build "${tagflag[@]}" -o "$out" ./cmd/ao ) || { - echo "FAIL: go build (tags='$*') errored — archived code must stay buildable" >&2 - exit 2 - } -} - -expect() { # expect <binary> <substr> <present|absent> - local bin="$1" needle="$2" mode="$3" out - out="$("$bin" buildtags 2>/dev/null || true)" - # Whole-line match: a tag is emitted on its own line, so the spine's - # explanatory text ("…corpus/flywheel…") never false-matches a tag check. - case "$mode" in - present) - if ! printf '%s\n' "$out" | grep -qx "$needle"; then - echo "FAIL: '$bin buildtags' = '$out' — expected a line '$needle'" >&2 - exit 1 - fi ;; - absent) - if printf '%s\n' "$out" | grep -qx "$needle"; then - echo "FAIL: '$bin buildtags' = '$out' — expected NO line '$needle'" >&2 - exit 1 - fi ;; - esac -} - -echo "verify-buildtags: default (spine) build…" -build "$TMP/ao-spine" -if spine_out="$("$TMP/ao-spine" buildtags 2>&1)"; then - echo "FAIL: default build must omit archived 'ao buildtags'" >&2; exit 1 -fi -if ! printf '%s\n' "$spine_out" | grep -q 'unknown command "buildtags"'; then - echo "FAIL: default build did not reject archived 'ao buildtags': $spine_out" >&2; exit 1 -fi - -echo "verify-buildtags: -tags flywheel…" -build "$TMP/ao-flywheel" flywheel -expect "$TMP/ao-flywheel" "flywheel" present -expect "$TMP/ao-flywheel" "legacy" absent - -echo "verify-buildtags: -tags legacy (AGENTOPS_LEGACY path)…" -build "$TMP/ao-legacy" legacy -expect "$TMP/ao-legacy" "legacy" present -expect "$TMP/ao-legacy" "flywheel" absent - -echo "verify-buildtags: -tags 'flywheel legacy' (make build-flywheel)…" -build "$TMP/ao-all" flywheel legacy -expect "$TMP/ao-all" "flywheel" present -expect "$TMP/ao-all" "legacy" present - -echo "OK: build-tag mechanism verified — default build omits archived sets; flywheel/legacy restore them." diff --git a/scripts/verify-pushed-commit-builds.sh b/scripts/verify-pushed-commit-builds.sh deleted file mode 100755 index 4d9540483..000000000 --- a/scripts/verify-pushed-commit-builds.sh +++ /dev/null @@ -1,132 +0,0 @@ -#!/usr/bin/env bash -# verify-pushed-commit-builds.sh — close the partial-commit-lands-broken escape (age-yy24). -# -# The pre-push gate (`ao gate check`, the worktree `go build ./...`) validates the -# WORKING TREE, not the COMMIT being pushed. A commit that silently omitted files -# (a multi-path `git add` that aborted on a deleted-file pathspec, dropping the rest) -# builds in the worktree but is build-broken as committed — it passed the gate + a -# cross-family pawl and landed origin/main red once (2026-06-22). This judges the -# COMMIT, not the tree: it builds each pushed commit in an ISOLATED temp worktree. -# -# It only does work when the worktree DIFFERS from HEAD for tracked-or-untracked -# source — a clean tree means worktree==HEAD, so the gate's worktree build already -# validated the commit (zero cost on the common clean-tree push, zero false alarms: -# it never fails merely because the user kept editing after committing). -# -# Usage: -# verify-pushed-commit-builds.sh < <git-pre-push-stdin> # lines: <lref> <lsha> <rref> <rsha> -# verify-pushed-commit-builds.sh <sha> [<sha> ...] # explicit shas (tests / manual) -# -# Exit: 0 = all pushed commits build (or skipped clean / infra-skip); 1 = a commit does -# NOT build (fail-closed — refuse the push). Fail-OPEN on infrastructure errors -# (mktemp / `git worktree add` failure) so a broken check never wedges a push. -# -# Env: -# AGENTOPS_PREPUSH_SKIP_COMMIT_BUILD=1 skip entirely (emergency bypass) -# AGENTOPS_COMMIT_BUILD_CMD build command run inside the temp worktree -# (default: "cd cli && go build ./...") -# AGENTOPS_COMMIT_BUILD_PATHS pathspecs for the dirty check (default: "cli scripts") -set -u - -if [ "${AGENTOPS_PREPUSH_SKIP_COMMIT_BUILD:-0}" = "1" ]; then - exit 0 -fi - -REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" || exit 0 -BUILD_CMD="${AGENTOPS_COMMIT_BUILD_CMD:-cd cli && go build ./...}" -# Intentional word-split of the space-separated pathspec list into array elements. -# shellcheck disable=SC2206 -DIRTY_PATHS=(${AGENTOPS_COMMIT_BUILD_PATHS:-cli scripts}) -ZERO="0000000000000000000000000000000000000000" - -# worktree_matches_head: true when the worktree equals HEAD for the watched source -# paths — both tracked changes AND untracked source files. In that case the gate's -# worktree build already validated the committed state, so there is nothing to add. -worktree_matches_head() { - git -C "$REPO_ROOT" diff --quiet HEAD -- "${DIRTY_PATHS[@]}" 2>/dev/null || return 1 - local others - others="$(git -C "$REPO_ROOT" ls-files --others --exclude-standard -- "${DIRTY_PATHS[@]}" 2>/dev/null | grep -cE '\.(go|sh)$')" - [ "${others:-0}" -eq 0 ] -} - -# build_commit_isolated <sha>: 0 = builds (or infra-skip), 1 = definitively does NOT build. -build_commit_isolated() { - local sha="$1" tmpwt rc=0 - tmpwt="$(mktemp -d "${TMPDIR:-/tmp}/agentops-commitbuild.XXXXXX" 2>/dev/null)" || return 0 - if ! git -C "$REPO_ROOT" worktree add --detach --quiet "$tmpwt" "$sha" 2>/dev/null; then - echo >&2 "⚠ verify-pushed-commit-builds: could not create a temp worktree for ${sha} — skipping (fail-open)." - rm -rf "$tmpwt" 2>/dev/null - return 0 - fi - if ! ( cd "$tmpwt" && eval "$BUILD_CMD" ) >/dev/null 2>&1; then - rc=1 - fi - git -C "$REPO_ROOT" worktree remove --force "$tmpwt" >/dev/null 2>&1 || true - rm -rf "$tmpwt" 2>/dev/null - git -C "$REPO_ROOT" worktree prune >/dev/null 2>&1 || true - return "$rc" -} - -# MAX_COMMITS caps the isolated builds so a huge range can't wedge a push for minutes; -# beyond it we validate the tip + the most-recent MAX_COMMITS and warn (the tip is what -# main becomes; deep history is rarely first-introduced in one push). -MAX_COMMITS="${AGENTOPS_COMMIT_BUILD_MAX:-12}" - -# Collect the commits to validate. Two modes: -# explicit args -> exactly those shas (tests / manual), no range expansion or skip. -# git stdin -> for each `<lref> <lsha> <rref> <rsha>` push line, the pushed RANGE -# `rsha..lsha` (every NEW commit), not just the tip — a tip that -# builds can still push a build-broken intermediate (refuted 2026-06-22). -# New branches / unknown rsha fall back to the tip only. -shas=() -explicit=0 -clean=0 -worktree_matches_head && clean=1 - -if [ "$#" -gt 0 ]; then - explicit=1 - shas=("$@") -elif [ ! -t 0 ]; then - while read -r _lref lsha _rref rsha; do - [ -z "${lsha:-}" ] || [ "$lsha" = "$ZERO" ] && continue - # Clean tree: the gate's worktree build already validated the TIP (lsha == HEAD), - # but NEVER the intermediates — so only the tip is droppable when clean. - range_commits="" - if [ -n "${rsha:-}" ] && [ "$rsha" != "$ZERO" ] && git -C "$REPO_ROOT" cat-file -e "${rsha}^{commit}" 2>/dev/null; then - range_commits="$(git -C "$REPO_ROOT" rev-list --reverse "${rsha}..${lsha}" 2>/dev/null)" - else - range_commits="$lsha" - fi - for c in $range_commits; do - # On a clean tree, the tip is covered by the worktree build — skip just it. - { [ "$clean" -eq 1 ] && [ "$c" = "$lsha" ]; } && continue - shas+=("$c") - done - done -fi -[ "${#shas[@]}" -eq 0 ] && exit 0 - -# Explicit-arg mode honors the clean-tree skip too (so the hook's clean fast-path holds), -# unless a sha was given that is not HEAD. -if [ "$explicit" -eq 1 ] && [ "$clean" -eq 1 ]; then - head_sha="$(git -C "$REPO_ROOT" rev-parse HEAD 2>/dev/null)" - filtered=() - for c in "${shas[@]}"; do [ "$c" = "$head_sha" ] || filtered+=("$c"); done - shas=("${filtered[@]}") - [ "${#shas[@]}" -eq 0 ] && exit 0 -fi - -if [ "${#shas[@]}" -gt "$MAX_COMMITS" ]; then - echo >&2 "⚠ verify-pushed-commit-builds: ${#shas[@]} pushed commits — validating only the most recent ${MAX_COMMITS}." - shas=("${shas[@]: -$MAX_COMMITS}") -fi - -for sha in "${shas[@]}"; do - if ! build_commit_isolated "$sha"; then - echo >&2 "✗ pre-push (age-yy24): a COMMIT being pushed (${sha}) does NOT build, though the working tree does." - echo >&2 " Partial-commit class — 'git show ${sha} --stat' likely omits files the code needs. Push refused." - echo >&2 " (override only with cause: AGENTOPS_PREPUSH_SKIP_COMMIT_BUILD=1)" - exit 1 - fi -done -exit 0 diff --git a/skills-codex/.agentops-manifest.json b/skills-codex/.agentops-manifest.json index e74201b67..2146b17bb 100644 --- a/skills-codex/.agentops-manifest.json +++ b/skills-codex/.agentops-manifest.json @@ -345,13 +345,13 @@ { "name": "agent-mail", "source_skill": "skills/agent-mail", - "source_hash": "e01e99bb84638e1af9663c838c893ef7c765c8b2c13ad7758a23dfb0a95b1140", - "generated_hash": "95536f982a546d65cd4e9d7c5522b0426a1294f6e680aa4b8b90db5a2a6ca4f3" + "source_hash": "2f1123b37407d70c71dbe3880ff7fc1a7cbdafa87be20605accc4a24bed4ee87", + "generated_hash": "486b86a1911e717917c4857812d614a7783d948c97bf4b0fa4130f241262dcc7" }, { "name": "agent-native", "source_skill": "skills/agent-native", - "source_hash": "29d11603f60c9adad07a3c8bf7326d435468b1d3ed9e54a9fe5de18ad5fad9ed", + "source_hash": "b5c4759d9793af9392be6712fd00d4a53c044c74478c3ddaa4fbf731944f4d87", "generated_hash": "f408a3ec0118bfa7bcfadbf25882a5ca04a3f9cb5d73626daea8c36a677d8205" }, { @@ -363,13 +363,13 @@ { "name": "automation-shape-routing", "source_skill": "skills/automation-shape-routing", - "source_hash": "da9536ccc32da43214491109555debaacc5225fbaa3a47131c0003eab5b5eb68", + "source_hash": "453991bb4a80901db398497d326f3f24733f1d09ad845815340eba3f96dab4e7", "generated_hash": "ff129c3074267bfbfdbde0fda8b9e74fdfcfda4e15692967d3e18ca63ba9372d" }, { "name": "bootstrap", "source_skill": "skills/bootstrap", - "source_hash": "99caa0fbd23e430b59f2720b867362b8e7846f515045e2f92c7ea2742845b332", + "source_hash": "effe0c2030f1b22d118d01c54f88cc9e59d10735e7eb37e8a994d3eec125cd5b", "generated_hash": "6d3a14b793e673f365b2a2e4d51407fc043d934b2796e8e20fe996df323d87f5" }, { @@ -387,7 +387,7 @@ { "name": "codebase-recon", "source_skill": "skills/codebase-recon", - "source_hash": "8e58fba734f7bacb9d601c967ac83b52d436b9965c92ae1fe3c9187ca8b32a4a", + "source_hash": "848730336840206bd87bfdedd50747d37cab3d2e38c96ab805e8f12dbe7ec50b", "generated_hash": "aeb8d62f2079b2ceaa50c19411f2eb907ce5a2ea877796348bd6eb9568ee4528" }, { @@ -417,7 +417,7 @@ { "name": "doc", "source_skill": "skills/doc", - "source_hash": "3e9cc6f84f6753bd8912c02395b049e061b8bae242e3802c4190446b62a51636", + "source_hash": "07482cfc4f194073a6f431f8cea991c87429fe79c39b504ae694b8f530930da8", "generated_hash": "0667819424a6785669e6fb7382a94a64b5042fae6ed0fa8a6d8901b9621044e6" }, { @@ -429,7 +429,7 @@ { "name": "dueling-idea-genies", "source_skill": "skills/dueling-idea-genies", - "source_hash": "2e33782074659aa47cdb8399cf7b615ba6fd202be4737729e61df4eb9ac8c4cf", + "source_hash": "9fac55dd699993dabfabe9751f141e5b879425365d8ea00f68f3b16363a34343", "generated_hash": "178385fd900500e4189c1dcaf5b8464fbeb8c0b60f209eae3d0fbd6c31a369d6" }, { @@ -447,13 +447,13 @@ { "name": "heal-skill", "source_skill": "skills/heal-skill", - "source_hash": "ee505c4e3aa43f0d8371e17dc44ad73913b5d4caaed11cc2a3dde9aa02dd2552", - "generated_hash": "ab8fb6b393a0e539514470eda054f4f04294146785ce17d4d427a0f426e38508" + "source_hash": "6d52240c1167aef9905e4ebc9a53b41e34a44bb682592792e4e4002c12c53e30", + "generated_hash": "7a2a6e0b33327fd6f581cd1d661c7a1fffb93c0856382bdd642d6b8d29e1b7dd" }, { "name": "idea-genie", "source_skill": "skills/idea-genie", - "source_hash": "cc598f306d31cd4966d528c5cfe5c348eb5ecd217ed7fc77f3d91823577e490f", + "source_hash": "045910a8d0ee799a16c58fe96d4094ac9273cdd7bf68e62f92e73777855b5c44", "generated_hash": "fec89ab70e6fb628111a0b58b9f63e9c699a1983cb4ee2fcb7697acdd13a38ce" }, { @@ -471,8 +471,8 @@ { "name": "ms", "source_skill": "skills/ms", - "source_hash": "7651cd300ec26750c5e5962da15dfad0ef53bf5c6545c51ad5edcf2fde674390", - "generated_hash": "75fe7d42b0c57eede48bf4e6e189783f4e3e9826fca333cc26ffdbbc50e9ba54" + "source_hash": "c5e3cb60feb945a5387d685673c5440255d316e2cbb08558e53f9c657607a5e8", + "generated_hash": "9dee8ec60ff03862b0a8607f216e9cd55141f3dea2491b68b7635a15d3be91b6" }, { "name": "ntm", @@ -489,7 +489,7 @@ { "name": "pattern-mining", "source_skill": "skills/pattern-mining", - "source_hash": "a318b04a4ae08b33fb432a13c81c2569a4f6805d28cf17dc5826435c26c8e1ac", + "source_hash": "fa08aac77e2d362b1b632a58338d6943d54c189ee5f29119a3498b07c391aa3a", "generated_hash": "5956d2a41d1354c9543007e5ce00c950362d1bb35b77203acf12c2b8fba9073a" }, { @@ -501,14 +501,14 @@ { "name": "postmortem", "source_skill": "skills/postmortem", - "source_hash": "6cd813cc8fe531c00442b252da91fac33b68f71636b50eb6fae22f2917cb3ac3", - "generated_hash": "8b05e2cb373a4b34298d48f184a12af330da83ea41ced9f7c3d585687bfdee59" + "source_hash": "63ace88df4e43a1bba9e7b08652b5cd1594cfe75d24bdafbc09fa4008622018f", + "generated_hash": "c910b434b8b68a2ab180a1fc8452ceeefa46a125ffe1de4ed9254b71241d6bac" }, { "name": "premortem", "source_skill": "skills/premortem", - "source_hash": "b0bee8c5294e4a90a1f8e0996ccd7088df1b070137a847bc8f38173c7e7ca602", - "generated_hash": "ef717c4756f422e1c622d24d533b932ed253b4a02122300243403b4cfb134810" + "source_hash": "8cee10f7ced0eed19e4555d132c39398494004126f3c4d721d81753ff6145563", + "generated_hash": "ab521ecd9668414626b8a535112cfb615b00a2edec625f2a10b11b43b5061925" }, { "name": "product", @@ -531,26 +531,26 @@ { "name": "refactor", "source_skill": "skills/refactor", - "source_hash": "68e6ebadab67c1a9d87511c4b22038684938f48304c8add45b2a656154c6e23b", + "source_hash": "7d7e173fe4ca1c04b5ebcc998c0ca16b3ff6ce5c0f5263c1d064dbea7b6eefab", "generated_hash": "1bf92d5fc88969e3ecb14eb5efa5e9ee0c28dc582573c72fbad8a9e1978c4f56" }, { "name": "research", "source_skill": "skills/research", - "source_hash": "350a71a018d2920564230b324f4202f4f02f5954fc6f8003cdb25028a1c85afe", - "generated_hash": "512927218fdcc2a17787a45b9c553526914d8b39421321bb2de415ce989660ec" + "source_hash": "53f5b3346bc17828b6e5d9633cfbe621ecaaaa9ec0e0ba56000eeeaae89183cf", + "generated_hash": "d1b2da64ce054601d331d397404376c90683f565561c023d8358dd95af865c32" }, { "name": "reverse-engineer", "source_skill": "skills/reverse-engineer", - "source_hash": "623ba0b07a3642a7cebb96c3e14a02dd7fa71ce1bd663c9a0738387b3729afd0", - "generated_hash": "f58dbf565cab03bca500e88e4b21a301c8017b7897eae9dcc3bad51bb3ebb247" + "source_hash": "13593ffb9db741c610a0547a44cee2949e47089af2427d62615ccce8040a64c2", + "generated_hash": "c9078d8be65f140e0e462e476ba1a4c22e8624d9fcbba92f72f2f3bce97551df" }, { "name": "rpi", "source_skill": "skills/rpi", - "source_hash": "ea42feaee1859058d7bc7cf72e3095f3005a26ef61eb5ee3fc467948d4016a12", - "generated_hash": "eec6beac65f58739af88dcadeeb20bf10d3a14a989e29e3b6d163ee894c14ce0" + "source_hash": "30a04e3993b6c561fa8cf438929aaaeef84c4dfc384fbab79a99fa1a7e65d37c", + "generated_hash": "6d775ba6dfcf754f42499dfbb10bdf37ee63d7a6dd1fec973cfff6ee99d46ac4" }, { "name": "sbh", @@ -561,61 +561,61 @@ { "name": "scaffold", "source_skill": "skills/scaffold", - "source_hash": "9983ace93bf9de282900c228df302018dc7a4414b6c764da084f7873bafb242f", - "generated_hash": "5b383cf2a754ebfc2d69bfb09954f9d4ed27072e85fa659a78468d6c8ba827a6" + "source_hash": "5359dd43332e2f760b07e2d34d7fab55b8a6f8497fc102ed0c5d2692ef8a1444", + "generated_hash": "b850ce6d62ea30525ee389caebe065cbe863645956684e0efb5b8c59ac78d4fc" }, { "name": "scope", "source_skill": "skills/scope", - "source_hash": "777d5c5cf2f23a92f36221833c8840cea6e954a97bca72f14320d520fe97e831", - "generated_hash": "7cbc8d1f35192156232de8dd9f1aef87197526732d4ff2507d7f5aa58e5a86a4" + "source_hash": "d8d25468511b2128b13fe1d08bebcb8c172f7da2d639b7e39ffe326edad6cddf", + "generated_hash": "3d92ff818a8ce7071b82825f117d267c1d642682557750f8ea5ea2aaa5f78ef2" }, { "name": "security", "source_skill": "skills/security", - "source_hash": "416e10d873feb9188062aca0fd1ec917fb1ad6f9f44fe0cee51479c7194b12ff", - "generated_hash": "4b156a7aa16d8c0c9fff20af27526f26cc859bb74bfb71e2a9daa28fb4d1e1b4" + "source_hash": "d161054b328407c11083aa8c5871a63041129bccbf4b756aa13ce0a830a418a5", + "generated_hash": "ff669949792903f40c31fb78836cdab95462fe7038fb156874a764b70e17ed10" }, { "name": "shared", "source_skill": "skills/shared", - "source_hash": "288d3ffbc377d3006c5f36dce69e6ab6d3a03e342ee9d94130bae9f70be1469c", - "generated_hash": "316fbbc329663d86ed76a0594f1d9f7885606962c14a68c3d9bfe9e62a7fa055" + "source_hash": "f72b609c231193fe9ec83727a8e40aa8cf96ce6ceb9c2e38c25d4189d062048b", + "generated_hash": "64cf69c5b1abae8b534e2980ece7bd620116bff904281fdf63c5b34df4345313" }, { "name": "skill-builder", "source_skill": "skills/skill-builder", - "source_hash": "6998290cbb2ea82e83bc5b4add5658ce77da5b0d74c894f8ff2594917cb288f7", - "generated_hash": "6435bdf5179c9d243580d6cc2653f7f4141078cb712059229ff5c44d1220f821" + "source_hash": "bc5061fa0371cc38ce1c455b9a1b3cff5c958c88ea3fab1f0144b51f3b0195f3", + "generated_hash": "4cbb51335032c1a20084fec80fa50c388d33ff900eed090689dc5386dc2516b7" }, { "name": "standards", "source_skill": "skills/standards", - "source_hash": "eed8c1b40d6348f616aac238c47b5eb761ca8e737da6d24441322c986f1321e0", - "generated_hash": "fb857320cd5003a1ade0ca4ee6b964e5921a622ac73eb74c06d19937250fb1f3" + "source_hash": "e150a83135b742beba11af30875c693a9b045abcee9834717ebb8a30a78c09fb", + "generated_hash": "586534974990cbbc2a5a7d2307efd0df1f04ba1e4e2edbe22b3004eeeec77dec" }, { "name": "status", "source_skill": "skills/status", - "source_hash": "9da13b9c19e4e367c7c8e732d807af1c5dd425f2d71325d6e951e9e834815a25", + "source_hash": "d1143bcf4b203ff48103c32deb0c82060d1109caa8cbe28aa4ed5d0a6a21c18a", "generated_hash": "f948133821c25a531b51c6e1d92ef814ea8bc8ad387be07cb12ba2de13e9653f" }, { "name": "swarm", "source_skill": "skills/swarm", - "source_hash": "895b770e5e56134ed09008fe34a801345be5c17de8a9acb2ecfc7c819c63a17d", - "generated_hash": "d6c7da6931740de900a9d0c8cf3d44b581e0c3181da6e59b21a7df1b4c967043" + "source_hash": "7bb9623c612fc8ddecf7e6187f455cf0563da5d79745b225cbc455662a28bcd7", + "generated_hash": "e1de598cd7656b72d824dab3136cf7f95114d5ca70e39cafb914170ac3123c3e" }, { "name": "test", "source_skill": "skills/test", - "source_hash": "52e634545cc273c053e093175bc7a884d9a49e00d2c560044121cfbe5da293d3", + "source_hash": "dd7654a0113231b8d5bcfa96ff217009e90acfcbae16242b172168d18d03e700", "generated_hash": "7c63c252d2cb47e4877701683819212851e6db5c7873ba47f32d6b2667cf2a3b" }, { "name": "toil-mining", "source_skill": "skills/toil-mining", - "source_hash": "f52e3b637e357c27d1bcb55ef5349e49cd42a75d13ae1d681e7363f30c6ce797", + "source_hash": "6a6c4486ad83b6f791a6e4f32072a034aeecbc5f4cd4416a653079991fe3bb9a", "generated_hash": "d626a479e186f63e791a48010e22a82ede4a240aa9be42290fa14efb1c975c69" }, { @@ -627,8 +627,8 @@ { "name": "validate", "source_skill": "skills/validate", - "source_hash": "f1bc22d75a1263586a0b2e37b345ff9fe6e1432bd95cc666458f707dbe74e0e9", - "generated_hash": "f1935b9ab480fee7488ae6d40f9289e9dc715cc489ae9ae68505b70f75f7fa7e" + "source_hash": "9f0c83578d777757b886a24f99fe7dfa4e50b8442925bfe3fbcb0f5715154aa3", + "generated_hash": "5c3af71f10a600e5220ee8af993d8463b3e348d9c7b92b18a5746876a454a919" }, { "name": "workflow-builder", diff --git a/skills-codex/agent-mail/.agentops-generated.json b/skills-codex/agent-mail/.agentops-generated.json index 9d4be0f22..11f3c72e7 100644 --- a/skills-codex/agent-mail/.agentops-generated.json +++ b/skills-codex/agent-mail/.agentops-generated.json @@ -2,6 +2,6 @@ "generator": "codex-sync", "source_skill": "skills/agent-mail", "layout": "modular", - "source_hash": "e01e99bb84638e1af9663c838c893ef7c765c8b2c13ad7758a23dfb0a95b1140", - "generated_hash": "95536f982a546d65cd4e9d7c5522b0426a1294f6e680aa4b8b90db5a2a6ca4f3" + "source_hash": "2f1123b37407d70c71dbe3880ff7fc1a7cbdafa87be20605accc4a24bed4ee87", + "generated_hash": "486b86a1911e717917c4857812d614a7783d948c97bf4b0fa4130f241262dcc7" } diff --git a/skills-codex/agent-mail/references/WORKFLOWS.md b/skills-codex/agent-mail/references/WORKFLOWS.md index 96580d5f4..d4d2a4411 100644 --- a/skills-codex/agent-mail/references/WORKFLOWS.md +++ b/skills-codex/agent-mail/references/WORKFLOWS.md @@ -20,7 +20,7 @@ The canonical workflow for working on a bead with coordination. BR/beads is the macro_start_session(human_key="/abs/path", program="codex-cli", model="YOUR_MODEL") 2. Pick work - ao beads exec ready --json → select bd-123 + br ready --json → select bd-123 3. Reserve files file_reservation_paths( @@ -48,8 +48,8 @@ The canonical workflow for working on a bead with coordination. BR/beads is the - Keep durable state/evidence on the bead, not only in mail 6. Complete - ao beads exec update bd-123 --notes "Validation: npm test, CI run 123, commit abc123" - ao beads exec close bd-123 --reason "Implemented OAuth flow" + br update bd-123 --notes "Validation: npm test, CI run 123, commit abc123" + br close bd-123 --reason "Implemented OAuth flow" release_file_reservations(project_key="/abs/path", agent_name="GreenCastle") send_message( ... diff --git a/skills-codex/agent-native/.agentops-generated.json b/skills-codex/agent-native/.agentops-generated.json index a34cc182c..46070820d 100644 --- a/skills-codex/agent-native/.agentops-generated.json +++ b/skills-codex/agent-native/.agentops-generated.json @@ -2,6 +2,6 @@ "generator": "codex-sync", "source_skill": "skills/agent-native", "layout": "modular", - "source_hash": "29d11603f60c9adad07a3c8bf7326d435468b1d3ed9e54a9fe5de18ad5fad9ed", + "source_hash": "b5c4759d9793af9392be6712fd00d4a53c044c74478c3ddaa4fbf731944f4d87", "generated_hash": "f408a3ec0118bfa7bcfadbf25882a5ca04a3f9cb5d73626daea8c36a677d8205" } diff --git a/skills-codex/automation-shape-routing/.agentops-generated.json b/skills-codex/automation-shape-routing/.agentops-generated.json index 8777cc735..78302bf4b 100644 --- a/skills-codex/automation-shape-routing/.agentops-generated.json +++ b/skills-codex/automation-shape-routing/.agentops-generated.json @@ -2,6 +2,6 @@ "generator": "codex-sync", "source_skill": "skills/automation-shape-routing", "layout": "modular", - "source_hash": "da9536ccc32da43214491109555debaacc5225fbaa3a47131c0003eab5b5eb68", + "source_hash": "453991bb4a80901db398497d326f3f24733f1d09ad845815340eba3f96dab4e7", "generated_hash": "ff129c3074267bfbfdbde0fda8b9e74fdfcfda4e15692967d3e18ca63ba9372d" } diff --git a/skills-codex/bootstrap/.agentops-generated.json b/skills-codex/bootstrap/.agentops-generated.json index 854203a16..d02855c69 100644 --- a/skills-codex/bootstrap/.agentops-generated.json +++ b/skills-codex/bootstrap/.agentops-generated.json @@ -2,6 +2,6 @@ "generator": "codex-sync", "source_skill": "skills/bootstrap", "layout": "modular", - "source_hash": "99caa0fbd23e430b59f2720b867362b8e7846f515045e2f92c7ea2742845b332", + "source_hash": "effe0c2030f1b22d118d01c54f88cc9e59d10735e7eb37e8a994d3eec125cd5b", "generated_hash": "6d3a14b793e673f365b2a2e4d51407fc043d934b2796e8e20fe996df323d87f5" } diff --git a/skills-codex/codebase-recon/.agentops-generated.json b/skills-codex/codebase-recon/.agentops-generated.json index a04d7cd5b..3d32e02fb 100644 --- a/skills-codex/codebase-recon/.agentops-generated.json +++ b/skills-codex/codebase-recon/.agentops-generated.json @@ -2,6 +2,6 @@ "generator": "codex-sync", "source_skill": "skills/codebase-recon", "layout": "modular", - "source_hash": "8e58fba734f7bacb9d601c967ac83b52d436b9965c92ae1fe3c9187ca8b32a4a", + "source_hash": "848730336840206bd87bfdedd50747d37cab3d2e38c96ab805e8f12dbe7ec50b", "generated_hash": "aeb8d62f2079b2ceaa50c19411f2eb907ce5a2ea877796348bd6eb9568ee4528" } diff --git a/skills-codex/doc/.agentops-generated.json b/skills-codex/doc/.agentops-generated.json index 17a5d2774..d9f1d66ba 100644 --- a/skills-codex/doc/.agentops-generated.json +++ b/skills-codex/doc/.agentops-generated.json @@ -2,6 +2,6 @@ "generator": "codex-sync", "source_skill": "skills/doc", "layout": "modular", - "source_hash": "3e9cc6f84f6753bd8912c02395b049e061b8bae242e3802c4190446b62a51636", + "source_hash": "07482cfc4f194073a6f431f8cea991c87429fe79c39b504ae694b8f530930da8", "generated_hash": "0667819424a6785669e6fb7382a94a64b5042fae6ed0fa8a6d8901b9621044e6" } diff --git a/skills-codex/dueling-idea-genies/.agentops-generated.json b/skills-codex/dueling-idea-genies/.agentops-generated.json index df42800e3..44d03fb1a 100644 --- a/skills-codex/dueling-idea-genies/.agentops-generated.json +++ b/skills-codex/dueling-idea-genies/.agentops-generated.json @@ -2,6 +2,6 @@ "generator": "codex-sync", "source_skill": "skills/dueling-idea-genies", "layout": "modular", - "source_hash": "2e33782074659aa47cdb8399cf7b615ba6fd202be4737729e61df4eb9ac8c4cf", + "source_hash": "9fac55dd699993dabfabe9751f141e5b879425365d8ea00f68f3b16363a34343", "generated_hash": "178385fd900500e4189c1dcaf5b8464fbeb8c0b60f209eae3d0fbd6c31a369d6" } diff --git a/skills-codex/heal-skill/.agentops-generated.json b/skills-codex/heal-skill/.agentops-generated.json index 0f8b28fb0..902c5b61b 100644 --- a/skills-codex/heal-skill/.agentops-generated.json +++ b/skills-codex/heal-skill/.agentops-generated.json @@ -2,6 +2,6 @@ "generator": "codex-sync", "source_skill": "skills/heal-skill", "layout": "modular", - "source_hash": "ee505c4e3aa43f0d8371e17dc44ad73913b5d4caaed11cc2a3dde9aa02dd2552", - "generated_hash": "ab8fb6b393a0e539514470eda054f4f04294146785ce17d4d427a0f426e38508" + "source_hash": "6d52240c1167aef9905e4ebc9a53b41e34a44bb682592792e4e4002c12c53e30", + "generated_hash": "7a2a6e0b33327fd6f581cd1d661c7a1fffb93c0856382bdd642d6b8d29e1b7dd" } diff --git a/skills-codex/heal-skill/SKILL.md b/skills-codex/heal-skill/SKILL.md index c1431b30a..77266a41b 100644 --- a/skills-codex/heal-skill/SKILL.md +++ b/skills-codex/heal-skill/SKILL.md @@ -1,224 +1,73 @@ --- name: heal-skill -description: Repair skill hygiene and deep-audit SKILL.md +description: Check or repair structural hygiene in --- -# $heal-skill — Automated Skill Maintenance +# $heal-skill — Check one or more skill packages -> **Purpose:** Detect and auto-fix common skill hygiene issues across the skills/ directory. +`heal-skill` is a specialist hygiene tool. It reports structural defects in +canonical source skills and generated Codex twins. With `--fix`, it repairs only +owned projections through their generators. It does not schedule work, operate +Git, validate a software candidate, or decide what happens after a failure. -**YOU MUST EXECUTE THIS WORKFLOW. Do not just describe it.** - -## Constraints - -- Resolve every explicit target to a real direct child of `skills/` or `skills-codex/` before processing; reject traversal, missing/outside paths, and every symlink spelling because aliases make mutation ownership ambiguous. -- Run `--check` and deep audit read-only before any repair, because their exact finding codes and target path define the permitted mutation. -- Use `--fix` only for the documented auto-fixable structural codes, then rerun strict validation; never hand-edit generated Codex artifacts because parity projections have owned regeneration paths. -- Run the requested repair once and return every remaining audit result. A failed - repair is evidence for the caller, not permission to retry or route work. - -## Quick Start +## Inputs ```bash -$heal-skill # Check all skills (report only) -$heal-skill --fix # Auto-repair all fixable issues -$heal-skill --strict # Check all skills, exit 1 on findings (CI mode) -$heal-skill skills/council # Check a specific skill -$heal-skill --fix skills/validate # Fix a specific skill - -bash skills/heal-skill/scripts/audit.sh skills/council # Deep audit (read-only; see "Deep audit mode" below) +bash skills/heal-skill/scripts/heal.sh --check [skills/<slug> ...] +bash skills/heal-skill/scripts/heal.sh --check --strict [skills/<slug> ...] +bash skills/heal-skill/scripts/heal.sh --fix [skills/<slug> ...] ``` -## What It Detects +Every explicit target must be a real, direct child of `skills/` or +`skills-codex/`. Missing paths, traversal, and symlink spellings are rejected. -Auto-fix allowlist and principal diagnostics: +## Procedure -| Code | Issue | Auto-fixable? | -|------|-------|---------------| -| `MISSING_NAME` | No `name:` field in SKILL.md frontmatter | Yes -- adds name from directory | -| `MISSING_DESC` | No `description:` field in SKILL.md frontmatter | Yes -- adds placeholder | -| `NAME_MISMATCH` | Frontmatter `name` differs from directory name | Yes -- updates to match directory | -| `UNLINKED_REF` | File in references/ not linked in SKILL.md | Yes -- converts bare backtick refs to markdown links | -| `EMPTY_DIR` | Skill directory exists but has no SKILL.md | Yes -- removes empty directory | -| `MISSING_API_VERSION` | Selected source skill has no `skill_api_version` | Yes -- inserts version 1 in selected target(s) only | -| `DEAD_REF` | SKILL.md references a non-existent references/ file | No -- warn only | -| `SCRIPT_REF_MISSING` | SKILL.md references a scripts/ file that does not exist | No -- warn only | -| `INVALID_AO_CMD` | SKILL.md references an `ao` subcommand that does not exist (only runs if `ao` is on PATH) | No -- warn only | -| `DEAD_XREF` | SKILL.md references a `$skill-name` that has no matching skill directory | No -- warn only | +1. Resolve and contain all requested target directories. +2. Parse each `SKILL.md` frontmatter. +3. Check the path/name match, description, API version, disposition metadata, + and linked local references. +4. Print every finding once. +5. In `--fix` mode only, regenerate metadata-owned projections and scoped Codex + twins, then stop. -> `CATALOG_MISSING` was removed: it only ran when `skills/using-agentops/SKILL.md` existed, and that skill is gone. Catalog completeness is gated by `MISSING_DISPOSITION` against `docs/contracts/skill-dispositions.yaml`. +`--check` is read-only. `--strict` makes any finding produce exit 1. A failed +fix is returned to the caller; the skill does not retry or select another +action. -## Execution Steps +## Deep content audit -### Step 1: Run the heal script +The optional read-only content audit is: ```bash -# Check mode (default) -- report only, no changes -bash skills/heal-skill/scripts/heal.sh --check - -# Fix mode -- auto-repair what it can -bash skills/heal-skill/scripts/heal.sh --fix - -# Target a specific skill -bash skills/heal-skill/scripts/heal.sh --check skills/council -bash skills/heal-skill/scripts/heal.sh --fix skills/council +bash skills/heal-skill/scripts/audit.sh [--strict] [--json <path>] skills/<slug> ``` -**Checkpoint:** before accepting any mutation, confirm the target path and finding code are present in the read-only report and the code is documented as auto-fixable. +It combines the structural result with deterministic authoring checks and an +advisory quality score. It is not the core `Validate` phase, does not write a +`verdict.v2`, and has no delivery authority. Check definitions live in +[audit-checks.md](references/audit-checks.md); density scoring is described in +[context-density-checks.md](references/context-density-checks.md). -### Step 1A: Audit Codex Parity Drift When The Codex Bundle Looks Wrong +## Output -When the problem is not source-skill hygiene but `skills-codex/` drift, run the Codex parity audit first: +Structural findings are printed as: -```bash -bash scripts/audit-codex-parity.sh -bash scripts/audit-codex-parity.sh --skill swarm +```text +[FINDING_CODE] skills/example: concrete explanation ``` -Use this when a checked-in Codex skill still contains Claude-era primitives (`TaskCreate`, `TaskList`, `Tool: Task`), Claude backend references, or obviously broken runtime rewrites. +Deep audit JSON conforms to [audit-report.json](schemas/audit-report.json). +The caller owns any subsequent edit or invocation. -**Repair rule:** keep canonical shared behavior in `skills/<name>/SKILL.md`. Update `skills-codex/<name>/SKILL.md` when the shipped Codex artifact is wrong, and keep durable Codex-only tailoring in `skills-codex-overrides/<name>/SKILL.md`. +## Checks -After repair: +- Check mode never mutates files. +- Fix mode changes only an explicit source target and its owned projections. +- A second identical fix is idempotent. +- Generated Codex parity follows [codex-parity.md](references/codex-parity.md). +- Remaining non-fixable findings stay explicit. -```bash -bash scripts/audit-codex-parity.sh -bash scripts/validate-codex-override-coverage.sh -bash scripts/validate-codex-generated-artifacts.sh --scope worktree -``` +## Related executable specifications -**Checkpoint:** after a Codex repair, require parity, override coverage, and generated-artifact validation to exit zero before reporting the runtime bundle healthy. - -### Step 2: Interpret results - -- **Exit 0:** All clean, no findings. Also exit 0 for `--check` mode with findings (report-only). -- **Exit 1:** Findings reported with `--strict` or `--fix` flag. In `--fix` mode, fixable issues were repaired; re-run `--check` to confirm. - -### Step 3: Report to user - -Show the output. If `--fix` was used, summarize what changed. If `DEAD_REF` findings remain, advise the user to remove or update the broken references manually. - -## Output Format - -**Artifact directory:** stdout for human findings; `.agents/audits/` for JSON reports. -**Filename convention:** `<skill>-audit.json`. -**Serialization/schema format:** JSON matching `schemas/audit-report.json`. -**Validator command:** `jq -e . .agents/audits/<skill>-audit.json`. -**Downstream handoff:** consumed by the validation wave before repair. - -One line per finding: - -``` -[MISSING_NAME] skills/foo: No name field in frontmatter -[MISSING_DESC] skills/foo: No description field in frontmatter -[NAME_MISMATCH] skills/foo: Frontmatter name 'bar' != directory 'foo' -[UNLINKED_REF] skills/foo: refs/bar.md not linked in SKILL.md -[EMPTY_DIR] skills/foo: Directory exists but no SKILL.md -[DEAD_REF] skills/foo: SKILL.md links to non-existent refs/bar.md -[SCRIPT_REF_MISSING] skills/foo: references scripts/bar.sh but file not found -[INVALID_AO_CMD] skills/foo: references 'ao badcmd' which is not a valid subcommand -[DEAD_XREF] skills/foo: references /nonexistent but skill directory not found -``` - -## Deep audit mode (absorbed from $skill-auditor) - -Requests for `$skill-auditor` route here: the audit is the detect phase of heal, and it -lives at `scripts/audit.sh`. Unlike `--fix`, the audit is **read-only** — it reports; -repairs go through `heal.sh --fix` (Pass-1 issues) or hand edits (Pass-2 issues). - -```bash -bash skills/heal-skill/scripts/audit.sh [--strict] [--json <path>] skills/<name> -``` - -Two passes, then an aggregate verdict: - -- **Pass 1 — structural (delegated):** runs `heal.sh --check --strict <target>` and gates - on its exit code; it never reimplements the hygiene checks (heal is the source of truth). - A strict failure forces the aggregate verdict to FAIL but does NOT short-circuit Pass 2. -- **Pass 2 — content discipline:** 8 checks in `audit.sh` (triggers, frontloaded - constraints, rationale, checkpoints, output spec, quality rubric, references - modularization, trigger clarity). Definitions and accepted forms: - [references/audit-checks.md](references/audit-checks.md). -- **Verdict aggregation:** any check `fail` → FAIL; otherwise any `warn` → WARN; - otherwise PASS. Max-severity wins — no silent downgrade. Exit 0 on PASS/WARN - (1 on WARN under `--strict`), 1 on FAIL, 2 on usage error. -- **0-30 rubric (Pass 3) is ADVISORY-only:** `scripts/score_agentops_skill.py --audit-block` - folds a deterministic 10-category score (band C/B/A/S) into the report under `rubric`. - It never changes the PASS/WARN/FAIL verdict — it is a productization backlog signal, - not a ship blocker. The density block - ([references/context-density-checks.md](references/context-density-checks.md)) is - advisory too. -- **Accepted trigger forms:** `description-has-triggers` accepts only the forms - declared by the authoritative `repo-runtime` profile - (finding `f-2026-05-06-auditor-checks-must-fit-host-conventions` — audit checks must fit - the host's existing valid conventions). - -Report JSON conforms to [schemas/audit-report.json](schemas/audit-report.json); -`skill-builder` owns the canonical SKILL.md template. -Executable spec: [references/skill-auditor.feature](references/skill-auditor.feature). - -## Notes - -- The script is **idempotent** -- running `--fix` twice produces the same result. -- Explicit targets contain mutation: `--fix skills/foo` may change `foo` only; source siblings remain byte-identical. -- `DEAD_REF`, `SCRIPT_REF_MISSING`, `INVALID_AO_CMD`, and `DEAD_XREF` are warn-only because the correct resolution requires human judgment. -- `INVALID_AO_CMD` only runs if the `ao` CLI is available on PATH. Skipped silently otherwise. -- When run without a path argument, scans all directories under `skills/`. -- Use `--strict` for CI gates: exits 1 on any finding. Without `--strict`, check mode exits 0 even with findings. -- For Codex parity drift, use the audit script plus override-layer repair workflow in [references/codex-parity.md](references/codex-parity.md). The shell fixer is intentionally not allowed to rewrite generated Codex bodies directly. - -## Quality Checklist - -- Scope fidelity: every changed path is the audited target or its owned parity projection, and every repair maps to a reported finding code. -- Verification quality: strict heal/deep-audit and applicable Codex parity commands rerun after mutation with zero unresolved findings. -- Idempotence: a second identical repair run produces no additional diff, while non-auto-fixable findings remain explicit rather than guessed through. - -## Examples - -### Running a health check across all skills - -**User says:** `$heal-skill` - -**What happens:** -1. The heal script scans every directory under `skills/`, checking each for the nine issue types (missing name, missing description, name mismatch, unlinked references, empty directories, dead references, script reference integrity, CLI command validation, cross-reference validation). -2. Findings are printed one per line with issue codes (e.g., `[NAME_MISMATCH] skills/foo: Frontmatter name 'bar' != directory 'foo'`). -3. The script exits with code 0 in check mode (even with findings), or code 1 with `--strict` or `--fix` flags. - -**Result:** A diagnostic report showing all skill hygiene issues across the repository, with no files modified. - -### Auto-fixing a specific skill - -**User says:** `$heal-skill --fix skills/validate` - -**What happens:** -1. The heal script inspects only `skills/validate/`, running all per-skill checks against that skill. -2. For each fixable issue found (e.g., `MISSING_NAME`, `UNLINKED_REF`), the script applies the repair automatically -- adding the name from the directory, converting bare backtick references to markdown links, etc. -3. Any `DEAD_REF` findings are reported as warnings since they require human judgment to resolve. - -**Result:** The `skills/validate/SKILL.md` is repaired in place, with a summary of changes applied and any remaining warnings. - -## Troubleshooting - -| Problem | Cause | Solution | -|---------|-------|----------| -| `DEAD_REF` findings persist after `--fix` | Dead references are warn-only because the correct fix (delete, create, or update) requires human judgment | Manually inspect each dead reference and either create the missing file, remove the link from SKILL.md, or update the path | -| Script reports `EMPTY_DIR` for a skill in progress | The skill directory was created but SKILL.md has not been written yet | Either add a SKILL.md to the directory or remove the empty directory. Running `--fix` will remove it automatically | -| `NAME_MISMATCH` fix changed the wrong name | The script always updates the frontmatter `name` to match the directory name, not the other way around | If the directory name is wrong, rename the directory first, then re-run `--fix` | -| Script exits 0 but a skill still has issues | The issue type is not one of the checks the heal script detects | `heal.sh` covers structural hygiene only. Run the deep audit (`scripts/audit.sh`) for content-discipline checks; deeper quality issues require manual review or `$council` validation | -| Running `--fix` twice produces different output | This should not happen -- the script is idempotent | File a bug. Check if another process modified the skill files between runs | -| `skills-codex/` keeps regressing after sync | Mechanical conversion is preserving the wrong semantics | Run `bash scripts/audit-codex-parity.sh`, then move the durable Codex body rewrite into `skills-codex-overrides/<name>/SKILL.md` instead of patching generated output | - -## See Also - -- [skill-builder](../skill-builder/SKILL.md) — scaffolds new skills against the unified template; runs heal-skill (hygiene + deep audit) as self-checks -- [red-team](../validate/SKILL.md) — complementary; probes USABILITY (does the workflow actually work) vs the audit (is the structure correct) - -## References - -- [`skill-conformance-profiles.yaml`](../skill-builder/references/skill-conformance-profiles.yaml) — authoritative rules, severities, and boundary - -- [references/skill-stocktake.md](references/skill-stocktake.md) -- [references/codex-parity.md](references/codex-parity.md) -- [references/heal-skill.feature](references/heal-skill.feature) — Executable spec: detect hygiene issues, flag Codex-parity drift, auto-fix + report, --strict fails on remaining findings (soc-qk4b) -- [references/audit-checks.md](references/audit-checks.md) — deep-audit Pass-2 check definitions + accepted forms + PRODUCT.md mapping -- [references/context-density-checks.md](references/context-density-checks.md) — advisory density coverage logic and false-positive handling -- [references/skill-auditor.feature](references/skill-auditor.feature) — Executable spec for the absorbed deep audit mode: Pass 1 heal delegation, Pass 2 content checks, density report + productization score (soc-qk4b) +- [heal-skill.feature](references/heal-skill.feature) +- [skill-auditor.feature](references/skill-auditor.feature) diff --git a/skills-codex/heal-skill/prompt.md b/skills-codex/heal-skill/prompt.md index a9a189227..2202c6f73 100644 --- a/skills-codex/heal-skill/prompt.md +++ b/skills-codex/heal-skill/prompt.md @@ -1,6 +1,6 @@ # heal-skill -Repair skill hygiene and deep-audit SKILL.md quality (absorbed skill-auditor). Triggers: "heal-skill", "heal skill", "repair skill hygiene", "skill-auditor", "audit skill", "skill audit". +Check or repair structural hygiene in AgentOps skill packages. Triggers: "heal skill", "repair skill hygiene", "audit skill structure", "check skill package". ## Instructions diff --git a/skills-codex/heal-skill/references/context-density-checks.md b/skills-codex/heal-skill/references/context-density-checks.md index dd556538c..72231e499 100644 --- a/skills-codex/heal-skill/references/context-density-checks.md +++ b/skills-codex/heal-skill/references/context-density-checks.md @@ -29,9 +29,8 @@ Density Rule fields before that prose is passed into a fresh context session. ## Runnable Examples ```bash -bash skills/heal-skill/scripts/audit.sh skills/discovery bash skills/heal-skill/scripts/audit.sh skills/plan -bash skills/heal-skill/scripts/audit.sh skills/crank +bash skills/heal-skill/scripts/audit.sh skills/implement bash skills/heal-skill/scripts/audit.sh skills/validate ``` diff --git a/skills-codex/heal-skill/references/skill-stocktake.md b/skills-codex/heal-skill/references/skill-stocktake.md deleted file mode 100644 index db36fea24..000000000 --- a/skills-codex/heal-skill/references/skill-stocktake.md +++ /dev/null @@ -1,123 +0,0 @@ -# Skill Stocktake — AI-Powered Quality Audit - -> Beyond structural hygiene: evaluate skill quality, actionability, and fitness using AI judgment. - -## Problem - -`heal.sh --strict` catches structural issues (missing frontmatter, unlinked refs, name mismatches). But it can't judge: -- Is this skill still actionable and current? -- Does it overlap with another skill? -- Should it be retired, merged, or improved? -- Is it used frequently enough to justify maintenance cost? - -## Solution: Two-Pass Evaluation - -### Pass 1: Inventory (Deterministic) -Run `heal.sh --strict` for structural checks (existing), then collect metadata: - -```bash -# For each skill directory -for skill_dir in skills/*/; do - skill_name=$(basename "$skill_dir") - skill_md="${skill_dir}SKILL.md" - - # Extract frontmatter - tier=$(grep 'tier:' "$skill_md" | head -1 | awk '{print $2}') - line_count=$(wc -l < "$skill_md") - ref_count=$(ls "${skill_dir}references/" 2>/dev/null | wc -l) - last_modified=$(stat -f %Sm -t %Y-%m-%d "$skill_md" 2>/dev/null || stat -c %y "$skill_md" 2>/dev/null | cut -d' ' -f1) - - echo "${skill_name}|${tier}|${line_count}|${ref_count}|${last_modified}" -done -``` - -### Pass 2: AI Evaluation (Judgment) -Spawn a subagent with the inventory table + evaluation criteria. Process ~20 skills per agent to stay within context. - -**Evaluation Criteria:** -- **Actionability:** Does the skill produce concrete artifacts when invoked? -- **Scope Fit:** Does it fit its declared tier? Is it doing too much or too little? -- **Uniqueness:** Does it overlap substantially with another skill? -- **Currency:** Are referenced tools, APIs, and patterns still current? -- **Trigger Clarity:** Could an LLM correctly decide when to invoke this skill? - -### Verdict Categories - -| Verdict | Meaning | Required Evidence | -|---------|---------|-------------------| -| **Keep** | Good as-is | Cite core value + evidence of use | -| **Improve** | Worth keeping, needs fixes | Cite specific section + action + target size | -| **Update** | Referenced tech is outdated | Cite what's outdated + what replaced it | -| **Retire** | Low quality, stale, or redundant | Cite (1) specific defect, (2) what covers same need | -| **Merge into [X]** | Substantial overlap with X | Cite overlap + line count + what content to integrate | - -**Reason Quality Rules:** -- Never write "unchanged" alone — restate core evidence -- For Retire: must name what covers the same need -- For Merge: include line count and describe content to integrate -- For Improve: name section + action + target size - -## Quick Scan Mode - -For re-evaluation after changes (avoids re-scanning unchanged skills): - -```bash -# 1. Check which skills changed since last evaluation -LAST_EVAL_DATE=$(jq -r '.evaluated_at' .agents/stocktake/results.json 2>/dev/null || echo "1970-01-01") -CHANGED_SKILLS=$(find skills/ -name "SKILL.md" -newer .agents/stocktake/results.json -exec dirname {} \; | xargs -I{} basename {}) - -# 2. If no changes, stop -if [ -z "$CHANGED_SKILLS" ]; then - echo "No skills changed since last evaluation ($LAST_EVAL_DATE)" - exit 0 -fi - -# 3. Re-evaluate only changed skills -echo "Quick scan: $CHANGED_SKILLS" -# Spawn agent with only changed skills -# Carry forward unchanged verdicts from results.json -``` - -## Results Schema - -```json -{ - "evaluated_at": "2026-03-21T10:00:00Z", - "mode": "full|quick", - "batch_progress": {"total": 54, "evaluated": 54, "status": "completed"}, - "skills": { - "vibe": { - "verdict": "Keep", - "reason": "Core judgment skill; produces council verdicts, complexity analysis, and actionable findings. 779 lines, 15 references — well-maintained.", - "last_modified": "2026-03-21" - }, - "converter": { - "verdict": "Improve", - "reason": "Cross-platform skill converter is useful but description frontmatter is thin (155 lines). Add concrete examples of Codex/Cursor output format. Target: 200+ lines.", - "last_modified": "2026-02-15" - } - } -} -``` - -## Integration with heal-skill - -Run stocktake as an optional mode of heal-skill: - -```bash -# Structural checks (existing) -bash skills/heal-skill/scripts/heal.sh --strict - -# Quality evaluation (new) -bash skills/heal-skill/scripts/heal.sh --stocktake # full evaluation -bash skills/heal-skill/scripts/heal.sh --stocktake --quick # quick scan -``` - -Results written to `.agents/stocktake/results.json`. Summary displayed to user. - -## When to Run - -- Before `/release` — ensures all skills are fit for distribution -- After adding/removing skills — detects overlap with existing skills -- Monthly maintenance — catches staleness and drift -- When skill count exceeds threshold (50+) — retirement pressure increases diff --git a/skills-codex/heal-skill/scripts/heal.sh b/skills-codex/heal-skill/scripts/heal.sh index fdb72082f..35941c594 100755 --- a/skills-codex/heal-skill/scripts/heal.sh +++ b/skills-codex/heal-skill/scripts/heal.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# One-pass structural audit for source and Codex skill packages. +# One-pass structural audit for source skill packages. set -euo pipefail MODE=check @@ -24,10 +24,8 @@ REPO_ROOT="${HEAL_REPO_ROOT:-$(cd "$SCRIPT_DIR/../../.." && pwd)}" REPO_ROOT="$(cd "$REPO_ROOT" && pwd -P)" if [[ ${#TARGETS[@]} -eq 0 ]]; then - for root in "$REPO_ROOT/skills" "$REPO_ROOT/skills-codex"; do - for path in "$root"/*; do - [[ -d "$path" ]] && TARGETS+=("$path") - done + for path in "$REPO_ROOT/skills"/*; do + [[ -d "$path" && -f "$path/SKILL.md" ]] && TARGETS+=("$path") done fi @@ -38,7 +36,7 @@ for target in "${TARGETS[@]}"; do [[ ! -L "$target" ]] || { echo "heal.sh: symlink targets are not accepted: $target" >&2; exit 2; } resolved="$(cd "$target" && pwd -P)" case "$(dirname "$resolved")" in - "$REPO_ROOT/skills"|"$REPO_ROOT/skills-codex") ;; + "$REPO_ROOT/skills") ;; *) echo "heal.sh: target is not a direct skill package: $target" >&2; exit 2 ;; esac normalized+=("$resolved") diff --git a/skills-codex/heal-skill/scripts/validate.sh b/skills-codex/heal-skill/scripts/validate.sh index 2efdc5a3c..628d9c7f2 100755 --- a/skills-codex/heal-skill/scripts/validate.sh +++ b/skills-codex/heal-skill/scripts/validate.sh @@ -1,54 +1,39 @@ #!/usr/bin/env bash set -euo pipefail -SKILL_DIR="$(cd "$(dirname "$0")/.." && pwd)" -PASS=0; FAIL=0 +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SKILL_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +REPO_ROOT="$(cd "$SKILL_DIR/../.." && pwd)" -check() { if bash -c "$2"; then echo "PASS: $1"; PASS=$((PASS + 1)); else echo "FAIL: $1"; FAIL=$((FAIL + 1)); fi; } - -check "SKILL.md exists" "[ -f '$SKILL_DIR/SKILL.md' ]" -check "SKILL.md has YAML frontmatter" "head -1 '$SKILL_DIR/SKILL.md' | grep -q '^---$'" -check "Codex parity reference exists" "[ -f '$SKILL_DIR/references/codex-parity.md' ]" -check "SKILL.md links Codex parity reference" "grep -q 'references/codex-parity.md' '$SKILL_DIR/SKILL.md'" -check "constraints are front-loaded" "awk 'BEGIN{n=0;i=0;found=0} /^---$/{n++;next} n==2{i++; if (/^## Constraints$/){found=1;exit} if (i>80) exit} END{exit !found}' '$SKILL_DIR/SKILL.md'" -check "constraints bind repair scope" "grep -Fq 'real direct child of' '$SKILL_DIR/SKILL.md' && grep -Fq 'read-only before any repair' '$SKILL_DIR/SKILL.md' && grep -Fq 'documented auto-fixable structural codes' '$SKILL_DIR/SKILL.md' && grep -Fq 'plain refutation is not an andon' '$SKILL_DIR/SKILL.md'" -check "MISSING_API_VERSION autofix is documented" "grep -Fq 'MISSING_API_VERSION' '$SKILL_DIR/SKILL.md' && grep -Fq 'selected target(s) only' '$SKILL_DIR/SKILL.md'" -check "verification checkpoints are explicit" "test \$(grep -c '^\*\*Checkpoint:\*\*' '$SKILL_DIR/SKILL.md') -ge 2" -check "quality checklist has three rules" "awk '/^## Quality Checklist$/{f=1;next} f&&/^## /{exit} f&&/^- /{n++} END{exit !(n>=3)}' '$SKILL_DIR/SKILL.md'" -check "kernel stays within 250 lines" "test \$(wc -l < '$SKILL_DIR/SKILL.md') -le 250" - -# --- Deep audit mode (absorbed from /skill-auditor) --- -for f in scripts/audit.sh scripts/score_agentops_skill.py references/audit-checks.md references/context-density-checks.md schemas/audit-report.json; do - check "audit artifact $f exists" "[ -f '$SKILL_DIR/$f' ]" +for path in \ + SKILL.md \ + scripts/heal.sh \ + scripts/audit.sh \ + scripts/score_agentops_skill.py \ + schemas/audit-report.json \ + references/audit-checks.md \ + references/codex-parity.md; do + [[ -f "$SKILL_DIR/$path" ]] || { + echo "heal-skill validate: missing $path" >&2 + exit 1 + } done -# audit.sh must contain all 8 Pass-2 check function names -for fn in check_description_has_triggers check_constraints_frontloaded check_rationale_present check_verification_checkpoints check_output_spec_explicit check_quality_rubric check_references_modularization check_trigger_clarity; do - check "audit.sh has $fn" "grep -q '^${fn}()' '$SKILL_DIR/scripts/audit.sh'" -done +bash -n "$SKILL_DIR/scripts/heal.sh" "$SKILL_DIR/scripts/audit.sh" +bash "$SKILL_DIR/scripts/heal.sh" --check --strict "$SKILL_DIR" -# Advisory density block: function + all six fields, outside the Pass-2 verdict loop -check "audit.sh has density function" "grep -q '^check_density_field()' '$SKILL_DIR/scripts/audit.sh'" -for field in intent boundary evidence decision constraint next_action; do - check "audit.sh has density field $field" "grep -q '$field' '$SKILL_DIR/scripts/audit.sh'" -done +before="$(find "$SKILL_DIR" -type f -exec shasum -a 256 {} + | sort | shasum -a 256 | awk '{print $1}')" +bash "$SKILL_DIR/scripts/heal.sh" --check "$SKILL_DIR" >/dev/null +after="$(find "$SKILL_DIR" -type f -exec shasum -a 256 {} + | sort | shasum -a 256 | awk '{print $1}')" +[[ "$before" == "$after" ]] || { + echo "heal-skill validate: check mode mutated its target" >&2 + exit 1 +} -# Pass 1 must delegate to heal.sh --check --strict and gate on exit code -check "audit.sh delegates via --check --strict" "grep -q -- '--check --strict' '$SKILL_DIR/scripts/audit.sh'" -check "audit.sh gates Pass 1 on exit code" "grep -q 'PASS1_EXIT_CODE' '$SKILL_DIR/scripts/audit.sh'" -check "audit report includes Pass-1 exit_code" "grep -q '\"exit_code\": %s' '$SKILL_DIR/scripts/audit.sh'" -check "API-version fix follows explicit targets" "grep -Fq 'an explicit --fix target must never mutate a sibling' '$SKILL_DIR/scripts/heal.sh' && grep -Fq 'for check_dir in' '$SKILL_DIR/scripts/heal.sh'" -check "explicit targets canonicalize fail-closed" "grep -Fq 'symlink spellings are not allowed' '$SKILL_DIR/scripts/heal.sh' && grep -Fq 'parent traversal is not allowed' '$SKILL_DIR/scripts/heal.sh' && grep -Fq 'not a direct child of an allowed skill root' '$SKILL_DIR/scripts/heal.sh'" -check "audit counts complete autofix allowlist" "grep -Fq 'EMPTY_DIR|MISSING_API_VERSION' '$SKILL_DIR/scripts/audit.sh'" -check "mutation-boundary fixture exists" "[ -x '$SKILL_DIR/scripts/test-mutation-boundaries.sh' ]" -check "mutation-boundary fixture passes" "bash '$SKILL_DIR/scripts/test-mutation-boundaries.sh'" +if rg -n 'ao land|git (commit|push)|append-skill-disposition|flywheel close-loop' \ + "$SKILL_DIR/scripts/heal.sh" "$SKILL_DIR/scripts/audit.sh" \ + "$SKILL_DIR/scripts/score_agentops_skill.py"; then + echo "heal-skill validate: lifecycle authority remains" >&2 + exit 1 +fi -# Stale check name must be gone (pre-mortem F1) -check "audit.sh has no stale check_description_multiline" "! grep -q 'check_description_multiline' '$SKILL_DIR/scripts/audit.sh'" - -# Pass 3 rubric wiring (advisory): scorer invoked, rubric block emitted, --audit-block supported -check "audit.sh invokes rubric scorer" "grep -q 'score_agentops_skill.py' '$SKILL_DIR/scripts/audit.sh'" -check "audit.sh emits rubric block" "grep -q '\"rubric\": %s' '$SKILL_DIR/scripts/audit.sh'" -check "scorer supports --audit-block" "grep -q -- '--audit-block' '$SKILL_DIR/scripts/score_agentops_skill.py'" - -echo ""; echo "Results: $PASS passed, $FAIL failed" -[ $FAIL -eq 0 ] && exit 0 || exit 1 +echo "heal-skill validate: PASS" diff --git a/skills-codex/idea-genie/.agentops-generated.json b/skills-codex/idea-genie/.agentops-generated.json index 0653d5c88..d87ac3421 100644 --- a/skills-codex/idea-genie/.agentops-generated.json +++ b/skills-codex/idea-genie/.agentops-generated.json @@ -2,6 +2,6 @@ "generator": "codex-sync", "source_skill": "skills/idea-genie", "layout": "modular", - "source_hash": "cc598f306d31cd4966d528c5cfe5c348eb5ecd217ed7fc77f3d91823577e490f", + "source_hash": "045910a8d0ee799a16c58fe96d4094ac9273cdd7bf68e62f92e73777855b5c44", "generated_hash": "fec89ab70e6fb628111a0b58b9f63e9c699a1983cb4ee2fcb7697acdd13a38ce" } diff --git a/skills-codex/ms/.agentops-generated.json b/skills-codex/ms/.agentops-generated.json index cee67bb71..45ddfb9fe 100644 --- a/skills-codex/ms/.agentops-generated.json +++ b/skills-codex/ms/.agentops-generated.json @@ -2,6 +2,6 @@ "generator": "codex-sync", "source_skill": "skills/ms", "layout": "modular", - "source_hash": "7651cd300ec26750c5e5962da15dfad0ef53bf5c6545c51ad5edcf2fde674390", - "generated_hash": "75fe7d42b0c57eede48bf4e6e189783f4e3e9826fca333cc26ffdbbc50e9ba54" + "source_hash": "c5e3cb60feb945a5387d685673c5440255d316e2cbb08558e53f9c657607a5e8", + "generated_hash": "9dee8ec60ff03862b0a8607f216e9cd55141f3dea2491b68b7635a15d3be91b6" } diff --git a/skills-codex/ms/SKILL.md b/skills-codex/ms/SKILL.md index 698822a36..cc8bac75e 100644 --- a/skills-codex/ms/SKILL.md +++ b/skills-codex/ms/SKILL.md @@ -80,19 +80,19 @@ ms config # resolved config + skill_paths - **Path:** search, load, and admin results are returned on `stdout`; durable index state remains under `~/Library/Application Support/ms/`. - **Filename:** no result filename is created by this skill; callers capture CLI output explicitly when they need a durable artifact. - **Format:** MCP returns structured tool data; CLI automation uses JSON or JSONL, with full skill text at `.data.content` for `ms load --full -O json`. -- **Validation command:** run `skills/ms/scripts/validate.sh` for the production lifecycle contract and `scripts/ms-reindex.sh --check-source` for normalized source equivalence. -- **Downstream handoff:** the invoking agent consumes full loaded guidance, routes production skill intent to the canonical factory, and records `ms outcome` only after downstream use and validation—not after retrieval alone. +- **Validation command:** run `skills/ms/scripts/validate.sh` for the retrieval boundary and `scripts/ms-reindex.sh --check-source` for normalized source equivalence. +- **Downstream handoff:** return the loaded guidance and source identity to the caller. Retrieval never chooses or starts a workflow. ## Production Skill Handoff -**Production-intent handoff:** When the query or intended use is to create, edit, heal, or promote a skill, `ms` only retrieves full guidance and then routes execution to `agentops-skill-factory`, `skill-builder`, `heal-skill`, and the factory-selected validation primitives. +**Production-intent handoff:** When the query concerns creating or editing a skill, `ms` retrieves relevant guidance and stops. The caller may separately invoke `skill-builder`, `heal-skill`, or another authoring tool. **Authority boundary:** `skills/**` is canonical source; the generator owns the `ms` Codex twin and other projections. Never edit the index, loaded copies, or generated projections as source. -`ms` never promotes a skill or interprets a validator result. A failed search, +`ms` never validates or interprets downstream work. A failed search, load, write, or reindex is returned as evidence and ends this invocation. -**Outcome timing:** Record `ms outcome` only after the downstream factory use and validation complete, never after retrieval alone. +**Outcome timing:** Record `ms outcome` only after the caller has independent evidence about downstream usefulness, never after retrieval alone. That observation does not change core state. --- @@ -139,9 +139,9 @@ Scenario: A stale local projection fails closed - Full loads preserve the complete runnable guidance rather than a metadata card or packed overview. - Search/load reads use the verified MCP path, while feedback and outcome writes use the verified CLI boundary. - Any rebuild finishes with stale servers swept and source equivalence reported. -- Production skill intent leaves `ms` after retrieval and enters the canonical factory against `skills/**`; generated twins and loaded/indexed copies are never hand-edited as source. -- Promotion carries deterministic evidence and a fresh-context independent verdict; no producer self-certification is accepted. -- `REFUTED` stays in automatic repair, while exactly one helper is reserved for a tripped breaker and `ms outcome` waits for downstream validation. +- Production skill intent leaves `ms` after retrieval; generated twins and loaded/indexed copies are never hand-edited as source. +- Search and load results remain advisory inputs, never proof that downstream work is correct. +- `ms outcome` records observed usefulness only after independent downstream evidence. ## References diff --git a/skills-codex/ms/scripts/validate.sh b/skills-codex/ms/scripts/validate.sh index 36c0f5d14..b90d4921b 100755 --- a/skills-codex/ms/scripts/validate.sh +++ b/skills-codex/ms/scripts/validate.sh @@ -3,46 +3,13 @@ set -euo pipefail SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" SKILL="$SKILL_DIR/SKILL.md" -TMP="$(mktemp -d)" -trap 'rm -rf "$TMP"' EXIT -# Exact Markdown contract literals: backticks are data, never substitutions. -# shellcheck disable=SC2016 -MARKERS=( - '## Output Specification' - '- **Validation command:** run `skills/ms/scripts/validate.sh` for the production lifecycle contract and `scripts/ms-reindex.sh --check-source` for normalized source equivalence.' - '- **Downstream handoff:** the invoking agent consumes full loaded guidance, routes production skill intent to the canonical factory, and records `ms outcome` only after downstream use and validation—not after retrieval alone.' - '## Production Skill Handoff' - '**Production-intent handoff:** When the query or intended use is to create, edit, heal, or promote a skill, `ms` only retrieves full guidance and then routes execution to `agentops-skill-factory`, `skill-builder`, `heal-skill`, and the factory-selected validation primitives.' - '**Authority boundary:** `skills/**` is canonical source; the generator owns the `ms` Codex twin and other projections. Never edit the index, loaded copies, or generated projections as source.' - '**Promotion gate:** Promotion requires deterministic checks plus a fresh-context pawl or independent verdict; the producing agent never self-certifies completion.' - '**Failure routing:** A plain `REFUTED` verdict auto-repairs and revalidates. Only a tripped circuit breaker enters `HOLD` and receives exactly one bounded helper consultation before re-earning an independent verdict.' - '**Outcome timing:** Record `ms outcome` only after the downstream factory use and validation complete, never after retrieval alone.' - '## Quality Checklist' - '- Production skill intent leaves `ms` after retrieval and enters the canonical factory against `skills/**`; generated twins and loaded/indexed copies are never hand-edited as source.' - '- Promotion carries deterministic evidence and a fresh-context independent verdict; no producer self-certification is accepted.' - '- `REFUTED` stays in automatic repair, while exactly one helper is reserved for a tripped breaker and `ms outcome` waits for downstream validation.' -) +[[ -s "$SKILL" ]] +grep -q '^name: ms$' "$SKILL" +grep -q '^ effects: \[\]$' "$SKILL" +grep -Fq 'Keep `ms` retrieval-only for production skill work.' "$SKILL" +grep -Fq '**Authority boundary:** `skills/**` is canonical source' "$SKILL" +grep -Fq '**Outcome timing:** Record `ms outcome` only after the caller has independent evidence' "$SKILL" +! grep -Eiq 'pawl|AUTO-REDO|ONE-HELPER|circuit breaker|canonical factory|promotes a skill' "$SKILL" -validate_contract() { - local file="$1" marker - [[ -s "$file" ]] || return 1 - for marker in "${MARKERS[@]}"; do - grep -Fqx -- "$marker" "$file" || return 1 - done -} - -delete_one_negative_fixture() { - local marker variant="$TMP/missing-marker.md" - for marker in "${MARKERS[@]}"; do - grep -Fvx -- "$marker" "$SKILL" >"$variant" - if validate_contract "$variant"; then - echo "ms lifecycle validator accepted a missing marker: $marker" >&2 - return 1 - fi - done -} - -validate_contract "$SKILL" -delete_one_negative_fixture -echo "ms lifecycle contract: PASS" +echo "ms retrieval contract: PASS" diff --git a/skills-codex/pattern-mining/.agentops-generated.json b/skills-codex/pattern-mining/.agentops-generated.json index e1b9be3d2..5e989daad 100644 --- a/skills-codex/pattern-mining/.agentops-generated.json +++ b/skills-codex/pattern-mining/.agentops-generated.json @@ -2,6 +2,6 @@ "generator": "codex-sync", "source_skill": "skills/pattern-mining", "layout": "modular", - "source_hash": "a318b04a4ae08b33fb432a13c81c2569a4f6805d28cf17dc5826435c26c8e1ac", + "source_hash": "fa08aac77e2d362b1b632a58338d6943d54c189ee5f29119a3498b07c391aa3a", "generated_hash": "5956d2a41d1354c9543007e5ce00c950362d1bb35b77203acf12c2b8fba9073a" } diff --git a/skills-codex/postmortem/.agentops-generated.json b/skills-codex/postmortem/.agentops-generated.json index 9a3ba2e1f..102c0cef4 100644 --- a/skills-codex/postmortem/.agentops-generated.json +++ b/skills-codex/postmortem/.agentops-generated.json @@ -2,6 +2,6 @@ "generator": "codex-sync", "source_skill": "skills/postmortem", "layout": "modular", - "source_hash": "6cd813cc8fe531c00442b252da91fac33b68f71636b50eb6fae22f2917cb3ac3", - "generated_hash": "8b05e2cb373a4b34298d48f184a12af330da83ea41ced9f7c3d585687bfdee59" + "source_hash": "63ace88df4e43a1bba9e7b08652b5cd1594cfe75d24bdafbc09fa4008622018f", + "generated_hash": "c910b434b8b68a2ab180a1fc8452ceeefa46a125ffe1de4ed9254b71241d6bac" } diff --git a/skills-codex/postmortem/SKILL.md b/skills-codex/postmortem/SKILL.md index 63b7fc514..0ee9f190f 100644 --- a/skills-codex/postmortem/SKILL.md +++ b/skills-codex/postmortem/SKILL.md @@ -1,6 +1,6 @@ --- name: postmortem -description: Test an explicit retrospective causal +description: Optionally test a retrospective causal --- # Postmortem @@ -10,7 +10,7 @@ description: Test an explicit retrospective causal ## Critical Constraints - Because proof and causal inference are different judgments, Postmortem is retrospective causal analysis, not the general learning umbrella and not a completion gate. -- It consumes an immutable Validate verdict plus Learn receipt and does not re-run acceptance validation by default because Validate already owns that proof. +- It consumes immutable Validate verdict evidence and does not re-run acceptance validation because Validate already owns that proof. - Treat causal statements as hypotheses because causal confidence must survive alternatives. Separate observed sequence, contributing conditions, counterfactuals, and unknowns. @@ -24,7 +24,7 @@ description: Test an explicit retrospective causal ## Workflow -1. Pin the verdict, Learn receipt, delivered artifact, and explicit causal +1. Pin the verdict, subject evidence, and explicit causal question. 2. Reconstruct the evidence-backed timeline without importing hidden author reasoning as fact. @@ -43,7 +43,7 @@ description: Test an explicit retrospective causal - **Serialization/schema format:** Markdown with causal question, pinned inputs, timeline, hypotheses, evidence, counterfactuals, unknowns, and experiments. - **Validator command:** `bash skills/postmortem/scripts/validate.sh`. -- **Downstream handoff:** Learn or the orchestrator may consume the analysis; they own +- **Downstream handoff:** Learn or the caller may consume the analysis; they own any bookkeeping, promotion, planning, or delivery decision. ## Quality Checklist diff --git a/skills-codex/postmortem/prompt.md b/skills-codex/postmortem/prompt.md index 7fbc077ac..46123c11e 100644 --- a/skills-codex/postmortem/prompt.md +++ b/skills-codex/postmortem/prompt.md @@ -1,6 +1,6 @@ # postmortem -Test an explicit retrospective causal question against evidence and counterfactuals after Validate and Learn. Triggers: "postmortem", "causal retrospective", "test a retrospective hypothesis". +Optionally test a retrospective causal question against durable verdict evidence. Triggers: "postmortem", "causal retrospective", "test a retrospective hypothesis". ## Instructions diff --git a/skills-codex/postmortem/references/postmortem.feature b/skills-codex/postmortem/references/postmortem.feature index 267ecfa61..a788cca05 100644 --- a/skills-codex/postmortem/references/postmortem.feature +++ b/skills-codex/postmortem/references/postmortem.feature @@ -4,7 +4,7 @@ Feature: Postmortem tests retrospective causal claims So that retrospective stories do not become unsupported doctrine Scenario: An explicit causal question receives bounded analysis - Given an immutable Validate verdict and Learn receipt + Given an immutable Validate verdict And an explicit retrospective causal question When Postmortem reconstructs the evidence-backed timeline Then it distinguishes supported claims, rejected claims, and unknowns @@ -13,5 +13,5 @@ Feature: Postmortem tests retrospective causal claims Scenario: Postmortem does not repeat validation Given the acceptance verdict is already immutable When Postmortem begins - Then it does not re-run the full acceptance suite by default + Then it does not re-run acceptance validation And it does not change proof, bookkeeping, planning, tracker, or delivery state diff --git a/skills-codex/postmortem/scripts/validate.sh b/skills-codex/postmortem/scripts/validate.sh index d06937a4e..615950eb4 100755 --- a/skills-codex/postmortem/scripts/validate.sh +++ b/skills-codex/postmortem/scripts/validate.sh @@ -5,7 +5,7 @@ skill_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" grep -q '^name: postmortem$' "$skill_dir/SKILL.md" grep -Fq 'retrospective causal analysis' "$skill_dir/SKILL.md" -grep -Fq 'does not re-run acceptance validation by default' "$skill_dir/SKILL.md" +grep -Fq 'does not re-run acceptance validation' "$skill_dir/SKILL.md" grep -Fq 'counterfactual' "$skill_dir/SKILL.md" grep -Fq 'Empty or inconclusive analysis is valid' "$skill_dir/SKILL.md" grep -q '^Feature: Postmortem tests retrospective causal claims$' "$skill_dir/references/postmortem.feature" diff --git a/skills-codex/premortem/.agentops-generated.json b/skills-codex/premortem/.agentops-generated.json index 3a527d3a1..615d3df12 100644 --- a/skills-codex/premortem/.agentops-generated.json +++ b/skills-codex/premortem/.agentops-generated.json @@ -2,6 +2,6 @@ "generator": "codex-sync", "source_skill": "skills/premortem", "layout": "modular", - "source_hash": "b0bee8c5294e4a90a1f8e0996ccd7088df1b070137a847bc8f38173c7e7ca602", - "generated_hash": "ef717c4756f422e1c622d24d533b932ed253b4a02122300243403b4cfb134810" + "source_hash": "8cee10f7ced0eed19e4555d132c39398494004126f3c4d721d81753ff6145563", + "generated_hash": "ab521ecd9668414626b8a535112cfb615b00a2edec625f2a10b11b43b5061925" } diff --git a/skills-codex/premortem/SKILL.md b/skills-codex/premortem/SKILL.md index 1478ba9c0..613c55bb6 100644 --- a/skills-codex/premortem/SKILL.md +++ b/skills-codex/premortem/SKILL.md @@ -1,106 +1,36 @@ --- name: premortem -description: 'Use when: an exact plan needs a verdict.' +description: Optionally challenge a frozen plan with one --- -# Premortem Skill +# Premortem -> **Question:** Is this exact plan ready to implement? -> **Boundary:** Premortem owns the only semantic plan-readiness verdict. +Premortem is an optional plan-challenge strategy. It asks one fresh context to +identify concrete ways a frozen PlanPacket could fail before implementation. +It is not part of the required RPI sequence and does not authorize readiness. -## Constraints +## Workflow -- Judge the plan, never the implementation or delivery mechanism. -- Use one fresh-context judge with `author_id != judge_id`. Model and family - metadata are optional; no risk class requires different model families. -- Bind the verdict to the repository-relative plan path and its SHA-256. Any - plan edit invalidates the verdict. -- Emit exactly `PASS` or `FAIL`. `PASS` has zero blockers. `FAIL` contains the - complete nonempty blocker set in one response. -- Report only concrete, evidence-bound defects that invalidate acceptance, - correctness, safety, dependencies, scope, or a claimed contract. -- Do not own retries, attempt maps, budgets, helper state, implementation, - delivery, tracker closure, or operator escalation. The orchestrator chooses - repair or replanning after reading the verdict. -- A council, mixed panel, or Dueling Idea Genies artifact may inform the judge, - but none substitutes for this exact-plan verdict. +1. Pin the PlanPacket digest, acceptance, non-goals, evidence requirements, and + declared write scope. +2. Use one fresh judge with a context ID distinct from the plan author. +3. Test acceptance completeness, edge behavior, scope, dependencies, + reversibility, and evidence shape against cited repository facts. +4. Return one complete set of concrete findings and checked/not-checked scope. +5. Stop. The caller decides whether to revise the plan or invoke RPI. -## Loop position +Council or Dueling Idea Genies may be caller-supplied evidence, but Premortem +does not require either strategy and cannot turn consensus into approval. -Premortem runs once after Plan freezes the final plan and before the first -implementation leaf is pulled. It consumes the plan plus its acceptance, -dependency graph, write scopes, non-goals, rollback, and deterministic planning -receipts. It produces one immutable `premortem-plan-verdict.v1` JSON artifact. +## Boundary -Between implementation waves, reuse the verdict while the exact plan digest is -unchanged. A materially changed plan requires an explicit orchestrator request -for a new Premortem verdict; Validate and Learn cannot invoke Premortem themselves. +- Emit advisory findings, not `verdict.v2`, readiness, admission, or permission. +- Do not implement, validate the candidate, retry, repair, schedule, claim, + change acceptance, operate Git, close work, release, or deliver. +- Any plan edit creates a new subject for a later caller-initiated Premortem. -## Execution +## Output -1. Resolve one current plan path. Reject a missing or stale plan rather than - inferring intent from chat. -2. Compute the plan SHA-256 and record the plan author identity. -3. Retrieve only directly matched compiled prevention from - `.agents/premortem-checks/*.md`, falling back to - `.agents/findings/registry.jsonl`. Missing inputs skip silently; malformed - entries are ignored with one concise warning. -4. Dispatch one runtime-native fresh judge. Use Council only when the operator - explicitly requests a panel or the decision is genuinely contested. -5. Check all applicable acceptance, dependency, write-scope, migration, - reversibility, test-shape, capability-reuse, and rollback claims. The - detailed checklist is in - [mandatory-checks.md](references/mandatory-checks.md). -6. Return the complete blocker set once. Cosmetic, theoretical, pre-existing, - and out-of-scope observations are notes, not blockers. -7. Write the JSON verdict and validate both its schema and live plan digest: - - ```bash - skills/premortem/scripts/validate-output.sh \ - .agents/council/YYYY-MM-DD-premortem-<topic>.json \ - "$(git rev-parse --show-toplevel)" - ``` - -## Verdict contract - -```json -{ - "schema_version": "premortem-plan-verdict.v1", - "plan": {"path": ".agents/plans/example.md", "sha256": "<64 hex>"}, - "author_id": "planner-context", - "judge_id": "fresh-judge-context", - "verdict": "PASS", - "blockers_complete": true, - "blockers": [] -} -``` - -For `FAIL`, each blocker has a stable `id`, a concrete `claim`, and one or more -`evidence` references. Optional `author_model` and `judge_model` objects may -record `name` and `family`; the validator deliberately does not compare family. - -## Output Specification - -- **Artifact path:** `.agents/council/YYYY-MM-DD-premortem-<topic>.json` -- **Schema:** [plan-verdict.schema.json](schemas/plan-verdict.schema.json) -- **Validator:** `skills/premortem/scripts/validate-output.sh <verdict> <repo-root>` -- **Downstream handoff:** `PASS` permits the orchestrator to pull the first - implementation leaf. `FAIL` returns the complete evidence set to the - orchestrator for one consolidated repair decision or replanning. - -## Quality checklist - -- The recorded plan digest matches the file the judge actually read. -- Author and judge identities differ. -- The verdict is binary and the blocker set is explicitly complete. -- Every blocker cites the plan or a deterministic evidence path. -- No optional review topology is presented as readiness authority. - -## References - -- [mandatory-checks.md](references/mandatory-checks.md) -- [premortem.feature](references/premortem.feature) -- [scope-mode.md](references/scope-mode.md) -- [temporal-interrogation.md](references/temporal-interrogation.md) -- [examples.md](references/examples.md) -- [write-premortem-output.md](references/write-premortem-output.md) -- [compiled-prevention.md](references/compiled-prevention.md) +Return `premortem-plan-review.v1` with the plan digest, author and judge context +IDs, findings, evidence references, `checked`, and `not_checked`. An empty +finding set means only that this optional challenge found no concrete defect; +it is never a lifecycle gate. diff --git a/skills-codex/premortem/prompt.md b/skills-codex/premortem/prompt.md index a6625fe14..bab31f138 100644 --- a/skills-codex/premortem/prompt.md +++ b/skills-codex/premortem/prompt.md @@ -1,6 +1,6 @@ # premortem -Use when: an exact plan needs a verdict. Stress-tests readiness before work with one fresh, independent judge. +Optionally challenge a frozen plan with one fresh independent judge before implementation. Triggers: "premortem", "challenge this plan", "what could make this plan fail". ## Instructions diff --git a/skills-codex/premortem/references/compiled-prevention.md b/skills-codex/premortem/references/compiled-prevention.md deleted file mode 100644 index e05e51d44..000000000 --- a/skills-codex/premortem/references/compiled-prevention.md +++ /dev/null @@ -1,108 +0,0 @@ -# Compiled Prevention Loading - -> Extracted from premortem/SKILL.md on 2026-04-11. - -## 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 - # Decision-point pull: prefer the curated GOLD wiki with compact pointers - # (no bodies) and a hard top-K cap — bookend-bounded (ADR-0002). Fall back to - # the raw .agents/ corpus, but WARN loudly so a missing gold wiki is never a - # silent zero-result. - if [ -d .ao/wiki ]; then - ao lookup --query "<plan goal or title>" --gold --pointers --limit 3 2>/dev/null | head -20 - else - echo "WARN: gold wiki (.ao/wiki) absent — run 'ao wiki gold' to enable gold retrieval; falling back to raw .agents/ corpus" >&2 - ao lookup --query "<plan goal or title>" --limit 3 2>/dev/null | head -20 - fi -fi -``` - -If learnings are returned, include them as `known_context` in the review packet. Cite any learning by filename when it influences a prediction. The gold-absent path WARNs (not silent) and falls back to the raw corpus; skip silently only 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/premortem-checks/*.md` when they exist. This is separate from flywheel search and does NOT get skipped by `--quick`. - -Use the tracked contracts in `docs/contracts/finding-compiler.md` and `docs/contracts/finding-registry.md`: - -- prefer compiled premortem checks first -- rank by severity, `applicable_when` overlap, language overlap, and literal plan-text overlap -- when the plan names files, rank changed-file overlap ahead of generic keyword matches -- cap at top 5 findings / check files -- if compiled checks are missing, incomplete, or fewer than the matched finding set, fall back to `.agents/findings/registry.jsonl` -- fail open: - - missing compiled directory or registry -> skip silently - - empty compiled directory or registry -> skip silently - - malformed line -> warn and ignore that line - - unreadable file -> warn once and continue without findings - -Include matched entries in the council packet as `known_risks` with: - -- `id` -- `pattern` -- `detection_question` -- `checklist_item` - -Use the same ranked packet contract as `/plan`: compiled checks first, then active findings fallback, then matching high-severity next-work context when relevant. Avoid re-ranking with an unrelated heuristic inside premortem; the point is consistent carry-forward, not a fresh retrieval policy per phase. - -### Record Citations for Applied Knowledge - -After including matched entries as `known_risks`, record each citation so the flywheel feedback loop can track influence: - -```bash -# Only use "applied" when the finding actually influenced the council packet. -# Use "retrieved" for items loaded but not referenced in the risk assessment. -ao metrics cite "<finding-path>" --type applied 2>/dev/null || true # influenced risk assessment -ao metrics cite "<finding-path>" --type retrieved 2>/dev/null || true # loaded but not used -``` - -### Section Evidence - -When lookup results include `section_heading`, `matched_snippet`, or `match_confidence` fields, prefer the matched section over the whole file — it pinpoints the relevant portion. Higher `match_confidence` (>0.7) means the section is a strong match; lower values (<0.4) are weaker signals. Use the `matched_snippet` as the primary context rather than reading the full file. - -## Step 1a: Search Knowledge Flywheel (skip if `--quick`) - -Only run this step for `--deep`, `--mixed`, or `--debate`. - -```bash -if command -v ao &>/dev/null; then - ao search "plan validation lessons <goal>" 2>/dev/null | head -10 -fi -``` - -If ao returns prior plan review findings, include them as context for the council packet. Skip silently if ao is unavailable or returns no results. - -## Step 1b: Check for Product Context - -Quick mode does not create a separate product-review phase. Include product -context in the fresh judge's bounded packet only when the plan changes product -behavior or cites `PRODUCT.md`. Deep modes may add a dedicated perspective. - -```bash -if [ -f PRODUCT.md ]; then - # PRODUCT.md exists — include product perspectives alongside plan-review -fi -``` - -When `PRODUCT.md` is relevant to the plan and the user did not pass an explicit -`--preset` override: - -1. Read `PRODUCT.md` content and include in the council packet via `context.files` -2. In `--quick` mode, the one fresh judge assesses only the product claims in - the bounded packet. -3. In non-quick modes, add a single consolidated `product` perspective to the council invocation: - ``` - /council --preset=plan-review --perspectives="product" validate <plan-path> - ``` - This yields 3 judges total (2 plan-review + 1 product). The product judge covers user-value, adoption-barriers, and competitive-position in a single review. -4. With `--deep`: 5 judges (4 plan-review + 1 product). - -When the user passed an explicit `--preset`, it takes precedence. - -When product context is absent or irrelevant, proceed without it. - -> **Tip:** Create `PRODUCT.md` from `docs/PRODUCT-TEMPLATE.md` to enable product-aware plan validation. diff --git a/skills-codex/premortem/references/examples.md b/skills-codex/premortem/references/examples.md deleted file mode 100644 index a06e1194b..000000000 --- a/skills-codex/premortem/references/examples.md +++ /dev/null @@ -1,67 +0,0 @@ -# Premortem Examples - -> Extracted from premortem/SKILL.md on 2026-04-11. - -## Validate a Plan (Default — One Fresh Judge) - -**User says:** `/premortem .agents/plans/2026-02-05-auth-system.md` - -**What happens:** - -1. Agent reads the auth system plan -2. Sends the bound plan and acceptance packet to one fresh-context judge -3. The judge finds missing error handling for token expiry -4. Premortem verdict: FAIL with one complete blocker set -5. The author repairs the plan; a fresh verdict on the changed digest is PASS - -**Result:** A binary, digest-bound plan verdict with actionable evidence. - -## Optional Panel-Assisted Plan Review - -**User says:** `/premortem --deep .agents/plans/2026-02-05-auth-system.md` - -**What happens:** - -1. Agent runs a multi-perspective council because the operator requested it -2. The final fresh judge consumes that advisory evidence -3. The judge writes the one binary exact-plan verdict - -**Result:** Optional depth without creating a second readiness authority. - -## Auto-Find Recent Plan - -**User says:** `/premortem` - -**What happens:** - -1. Agent scans `.agents/plans/` for most recent plan -2. Finds `2026-02-13-add-caching-layer.md` -3. Runs one fresh-context quick review -4. Records only reusable prevention evidence - -**Result:** Frictionless validation of most recent planning work. - -## Deep Review for High-Stakes Plan - -**User says:** `/premortem --deep .agents/plans/2026-02-05-migration-plan.md` - -**What happens:** - -1. Agent reads the migration plan -2. Searches knowledge flywheel for prior migration learnings -3. Checks PRODUCT.md for product context -4. Runs `/council --deep --preset=plan-review validate <plan-path>` (4 judges) -5. Council verdict with multi-perspective consensus - -**Result:** Thorough multi-judge review for plans where the stakes justify spawning agents. - -## Troubleshooting - -| Problem | Cause | Solution | -|---------|-------|----------| -| Council times out | Plan too large or complex for judges to review in allocated time | Split plan into smaller epics or increase timeout via council config | -| FAIL verdict on valid plan | Judges misunderstand domain-specific constraints | Add context via `--perspectives-file` with domain explanations | -| Product perspectives missing | PRODUCT.md exists but not included in council packet | Verify PRODUCT.md is in project root and no explicit `--preset` override was passed | -| Premortem blocks /crank | The current plan has no bound fresh-context verdict | Run `/premortem --quick` on the exact plan; quick narrows depth, not independence | -| Spec-completeness evidence is missing | Plan lacks Boundaries or Conformance Checks sections | Add the missing sections before requesting a new verdict | -| Plan changed between waves | The old verdict no longer matches acceptance, dependencies, write scope, or risk | Run one fresh Premortem on the changed plan | diff --git a/skills-codex/premortem/references/mandatory-checks.md b/skills-codex/premortem/references/mandatory-checks.md deleted file mode 100644 index ef60508bf..000000000 --- a/skills-codex/premortem/references/mandatory-checks.md +++ /dev/null @@ -1,179 +0,0 @@ -# Risk-Selected Plan Checks - -> Extracted from premortem/SKILL.md on 2026-04-11. -> -> The routine fresh judge runs only checks applicable to the named plan risks. -> A council is optional depth for a named one-way door, not the default carrier. - -## Step 2.3: Authority/consumer manifest (migration-shaped plans) - -When a plan renames, deletes, moves, migrates, or transfers ownership, require -the complete `write_scope.include` and `write_scope.exclude` manifest in the -PlanPacket, including generated companions. Compare it with the repository -state the plan will consume. - -- `incomplete` is FAIL: do not dispatch or infer missing consumers. -- `shared` is a valid inventory but the affected slices must serialize or merge. -- `disjoint` may retain a parallel proposal only when the remaining wave-validity - rows also pass. - -For a between-wave review, compare the manifest's observed paths with the -prior accepted snapshot. A new path invalidates the plan and returns to Plan; -it is not patched into a worker prompt after admission. - -## Step 2.4: Temporal Interrogation (`--deep` and `--temporal`) - -**Included automatically with `--deep`.** Also available via `--temporal` flag for quick reviews. - -Walk through the plan's implementation timeline to surface time-dependent risks: - -| Phase | Questions | -|-------|-----------| -| **Hour 1: Setup** | What blocks the first meaningful code change? Are dependencies available? | -| **Hour 2: Core** | Which files change in what order? Are there circular dependencies? | -| **Hour 4: Integration** | What fails when components connect? Which error paths are untested? | -| **Hour 6+: Ship** | What "should be quick" but historically isn't? What context is lost overnight? | - -Add to each judge's prompt when temporal interrogation is active: - -``` -TEMPORAL INTERROGATION: Walk through this plan's implementation timeline. -For each phase (Hour 1, 2, 4, 6+), identify: -1. What blocks progress at this point? -2. What fails silently at this point? -3. What compounds if not caught at this point? -Report temporal findings in a separate "Timeline Risks" section. -``` - -File count and dependency count never auto-trigger temporal or council depth. -Use it for `--deep`, explicit `--temporal`, or a named cutover/ordering risk. - -Temporal findings appear in the report as a `## Timeline Risks` table. See [temporal-interrogation.md](temporal-interrogation.md) for the full framework. - -Between waves, reuse the bound verdict while plan inputs are unchanged. A -materially changed plan may use the bounded mode from that reference; do not -replay completed work. - -## Step 2.5: Error & Rescue Map (Mandatory for plans with external calls) - -When the plan introduces methods, services, or codepaths that can fail, the council packet MUST include an Error & Rescue Map. If the plan omits one, generate it during review. - -Include in the council packet as `context.error_map`: - -| Method/Codepath | What Can Go Wrong | Exception/Error | Rescued? | Rescue Action | User Sees | -|-----------------|-------------------|-----------------|----------|---------------|-----------| -| `ServiceName#method` | API timeout | `TimeoutError` | Y/N | Retry 2x, then raise | "Service unavailable" | - -**Rules:** - -- Every external call (API, database, file I/O) must have at least one row -- `rescue StandardError` or bare `except:` is always a smell — name specific exceptions -- Every rescued error must: retry with backoff, degrade gracefully, OR re-raise with context -- For LLM/AI calls: map malformed response, empty response, hallucinated JSON, and refusal as separate failure modes -- Each GAP (unrescued error) is a finding with severity=significant - -## Step 2.6: Council FAIL Pattern Check (Mandatory) - -Evaluate the plan against these eight failure patterns: missing mechanical -verification, self-assessment, context rot, propagation blindness, plan -oscillation, dead infrastructure activation, missing rollback map, and -four-surface closure gap. Report only concrete, evidence-bound violations that -meet Premortem's blocker contract. - -Add to each judge's prompt: - -``` -COUNCIL FAIL PATTERN CHECK: Review this plan for the top 8 council FAIL patterns: -1. Missing mechanical verification — are all gates automated? -2. Self-assessment — is validation external to the implementer? -3. Context rot — are phase boundaries enforced with fresh sessions? -4. Propagation blindness — is the full change surface enumerated? -5. Plan oscillation — is direction validated before propagation? -6. Dead infrastructure activation — does the plan provision anything without activation tests? -7. Missing rollback map — does any production-state change lack a rollback procedure? -8. Four-surface closure — does the plan address Code + Docs + Examples + Proof for every feature? -Report FAIL pattern findings in a "FAIL Pattern Risks" section. -``` - -**Auto-triggered** for all plans (both `--quick` and `--deep` modes). - -## Step 2.7: Test Pyramid Coverage Check (Mandatory) - -Validate that the plan includes appropriate test levels per the test pyramid standard (`test-pyramid.md` in the standards skill). - -Check each issue in the plan: - -| Question | Expected | Finding if Missing | -|----------|----------|--------------------| -| Does any issue touching external APIs include L0 (contract) tests? | Yes | severity=significant: "Missing contract tests for API boundary" | -| Does every feature/bug issue include L1 (unit) tests? | Yes | severity=significant: "Missing unit tests for feature/bug issue" | -| Do cross-module changes include L2 (integration) tests? | Yes | severity=moderate: "Missing integration tests for cross-module change" | -| Are L4+ levels deferred to human gate (not agent-planned)? | Yes | severity=low: "Agent planning L4+ tests — these require human-defined scenarios" | -| For any skip/dedup/consumed/idempotency/regression guard test, does the fixture round-trip the **real persisted shape** (not a hand-built in-memory constructor)? | Yes | severity=significant: "Guard-test fixture uses a shape production never emits — false-green risk (cf. ag-mjlg / PR #652)" | -| Does any guard marker (`consumed`/`skip`/`dedup`) get set at the granularity the on-disk artifact uses (batch/parent/envelope vs item)? | Yes | severity=significant: "Guard marker set at item-level when persisted artifact marks it at batch-level" | - -Add to each judge's prompt when test pyramid check is active: - -``` -TEST PYRAMID CHECK: Review the plan's test coverage against the L0-L7 pyramid. -For each issue, verify: -1. Are the right test levels specified? (L0 for boundaries, L1 for behavior, L2 for integration) -2. Are there gaps where tests should exist but aren't planned? -3. Are any agent-autonomous levels (L0-L3) missing from code-change issues? -Report test pyramid findings in a "Test Coverage Gaps" section. -``` - -**Auto-triggered** when any issue in the plan modifies source code files (`.go`, `.py`, `.ts`, `.rs`, `.js`). - -## Step 2.8: Input Validation Check (Mandatory for enum-like fields) - -When the plan introduces or modifies fields with a bounded set of valid values (enums, tier names, mode strings, status codes), verify the plan includes validation logic. - -| Question | Expected | Finding if Missing | -|----------|----------|--------------------| -| Does every new enum-like field have a validation guard? | Yes | severity=significant: "No validation for enum field — invalid values pass silently" | -| Is there a defined fallback for unrecognized values? | Yes | severity=moderate: "No fallback behavior specified for invalid input" | -| Are valid values defined as a constant set (not inline strings)? | Yes | severity=low: "Valid values are inline strings — extract to named constant set" | - -**Auto-triggered** when the plan introduces struct fields with comments mentioning valid values, config fields with bounded options, or string fields parsed from user input. - -## Step 2.9: Regex Scope Predicate Check (Mandatory when plan introduces a regex/glob/grep filter) - -When the plan introduces a regex, glob, or grep pattern that classifies inputs into "in scope" / "out of scope" (goal gates that scan files, lint rules that classify code, orchestrators that filter work, search/inject filters), the plan MUST enumerate ≥3 positive cases and ≥3 negative cases. - -| Question | Expected | Finding if Missing | -|----------|----------|--------------------| -| Does the plan list ≥3 positive cases the predicate MUST match? | Yes | severity=significant: "Regex predicate has no positive case list — risk of too-narrow first iteration" | -| Does the plan list ≥3 negative cases the predicate MUST NOT match? | Yes | severity=significant: "Regex predicate has no negative case list — risk of too-broad first iteration (false positives)" | -| Does the implementation include a unit test covering both lists? | Yes | severity=moderate: "Regex predicate has cases listed in plan but no unit test — predicate semantics drift after first edit" | - -**Auto-triggered** when any plan issue mentions: a goal gate scanning `scripts/**`, `docs/**`, or any glob; a regex assigned to a variable; a `grep -E` invocation in a new gate or lint script; an orchestrator that filters which files to dispatch; a search filter that decides which records to surface. - -## Steps 2.10–2.11: Independent exact-plan adjudication - -### No self-grading - -The plan author cannot emit the readiness verdict. Record `author_id` and a -distinct, context-isolated `judge_id`; reject the artifact when they are equal. -One blind fresh-context judge satisfies the independence floor. Optional -`author_model` and `judge_model` metadata may record model names and families, -but family never changes whether the verdict is valid. - -### One immutable verdict - -Bind `premortem-plan-verdict.v1` to the repository-relative plan path and its -current SHA-256. Any edit invalidates it. Emit only: - -- `PASS` with `blockers_complete: true` and an empty blocker list; or -- `FAIL` with `blockers_complete: true` and every concrete blocker in one list. - -Each blocker must identify the failed claim and cite at least one evidence -path. Do not split one review into per-check verdicts, accept conditional -readiness, or turn notes into blockers. - -### Ownership boundary - -Premortem owns plan judgment only. It does not count attempts, manage repair -cycles, allocate time or model budgets, consult escalation helpers, implement -changes, close tracker work, or decide delivery. Return the immutable verdict -to the orchestrator, which chooses the next transition. diff --git a/skills-codex/premortem/references/premortem.feature b/skills-codex/premortem/references/premortem.feature index 18fd7afaf..a26d0ff61 100644 --- a/skills-codex/premortem/references/premortem.feature +++ b/skills-codex/premortem/references/premortem.feature @@ -1,48 +1,12 @@ -# Executable spec for the /premortem skill — pre-implementation plan gate (domain role). -# /premortem stress-tests a plan BEFORE work starts: it returns a PASS/FAIL verdict on -# the plan and on the wave-validity rows, so a bad plan is sent back to /plan rather than -# executed. Quick mode uses one fresh judge; --deep/--mixed widen the council. Hexagon: -# domain; consumes standards; produces premortem-plan-verdict.v1. (soc-qk4b) +Feature: Premortem optionally challenges one frozen plan + Scenario: A fresh judge returns advisory findings + Given a PlanPacket with a pinned digest and author context ID + When a distinct fresh judge challenges its acceptance, scope, and evidence + Then Premortem returns findings with checked and not-checked scope + And an empty finding set grants no lifecycle permission -Feature: Pre-mortem stress-tests a plan before implementation - As the pre-flight gate between slice-planning and TDD - I want a plan reviewed for failure modes before any code is written - So that a flawed plan is caught and re-sliced instead of executed - - Scenario: a plan is reviewed and gets a verdict before work starts - When /premortem runs on a plan or spec - Then it returns a PASS or FAIL verdict on the plan's failure modes - And the verdict binds the exact plan path and SHA-256 - And author_id differs from judge_id - - Scenario: wave-validity gates parallelism - When the plan proposes a parallel wave - Then premortem checks the wave-validity rows (distinct write scopes, owner per slice, discard path) - And a wave may run parallel only if every row is conflict-free - And a FAIL sends the plan back to /plan to re-slice (or run sequential) - - Scenario: Between-wave Premortem runs only for a materially changed plan - Given targeted wave evidence changed acceptance, dependencies, write scope, or risk - And the orchestrator wrote the changed plan - When Premortem runs before the next wave - Then it judges that exact changed plan - And Validate and Learn did not invoke Premortem directly - - Scenario: unchanged plan inputs reuse the bound verdict - Given the accepted plan digest, acceptance, dependencies, write scope, and risk are unchanged - When another tranche wave is considered - Then the existing Premortem verdict is reused - And no new judge, council, report, or registry write is created - - Scenario: quick mode uses one fresh judge by default - When /premortem runs without --deep/--mixed/--debate - Then it runs exactly one fresh-context judge distinct from the author - And it does not start a council - And optional council fan-out cannot substitute for the exact-plan verdict - - Scenario: a failure reports every blocker once - Given the plan has concrete readiness defects - When the fresh judge returns FAIL - Then blockers_complete is true - And the blocker list is nonempty and evidence-bound - And Premortem owns no retry, budget, helper, or delivery state + Scenario: Premortem stops after the review + Given any advisory finding set + When the review is complete + Then Premortem does not implement, validate, retry, schedule, claim, operate Git, release, or deliver + And the caller owns whether to revise the plan or invoke RPI diff --git a/skills-codex/premortem/references/scope-mode.md b/skills-codex/premortem/references/scope-mode.md deleted file mode 100644 index 9c06dc1b1..000000000 --- a/skills-codex/premortem/references/scope-mode.md +++ /dev/null @@ -1,31 +0,0 @@ -# Scope Mode Selection - -> Extracted from premortem/SKILL.md on 2026-04-11. - -Before running council, determine the review posture. Three modes: - -| Mode | When to Use | Posture | -|------|-------------|---------| -| **SCOPE EXPANSION** | Greenfield features, user says "go big" | Dream big. What's the 10-star version? Push scope UP. | -| **HOLD SCOPE** | Bug fixes, refactors, most plans | Maximum rigor within accepted scope. Make it bulletproof. | -| **SCOPE REDUCTION** | Plan touches >15 files, overbuilt | Strip to essentials. What's the minimum that ships value? | - -## Auto-Detection (when user doesn't specify) - -- Greenfield feature → default EXPANSION -- Bug fix or hotfix → default HOLD SCOPE -- Refactor → default HOLD SCOPE -- Plan touching >15 files → suggest REDUCTION -- User says "go big" / "ambitious" → EXPANSION - -## Critical Rule - -Once mode is selected, commit to it in the judge input. Do not silently drift. -Scope mode focuses the review; it never changes the binary verdict schema or -authorizes conditional readiness. - -## Mode-Specific Judge Instructions - -- **EXPANSION:** Add to judge prompt: "What would make this 10x more ambitious for 2x the effort? What's the platonic ideal? List 3 delight opportunities." -- **HOLD SCOPE:** Add to judge prompt: "The plan's scope is accepted. Your job: find every failure mode, test every edge case, ensure observability. Do not argue for less work." -- **REDUCTION:** Add to judge prompt: "Find the minimum viable version. Everything else is deferred. What can be a follow-up? Separate must-ship from nice-to-ship." diff --git a/skills-codex/premortem/references/temporal-interrogation.md b/skills-codex/premortem/references/temporal-interrogation.md deleted file mode 100644 index 364468590..000000000 --- a/skills-codex/premortem/references/temporal-interrogation.md +++ /dev/null @@ -1,96 +0,0 @@ -# Temporal Interrogation Framework - -Walk through the implementation timeline to surface time-dependent risks that static plan review misses. - -## Purpose - -Plans look good on paper but fail in time. Temporal interrogation forces judges to simulate the implementation sequence hour by hour, exposing ordering dependencies, blocking resources, and compounding failures. - -## Timeline Template - -### Hour 1: Setup & First File - -- What blocks the first meaningful code change? -- Are all dependencies available (APIs, credentials, packages)? -- Is the dev environment ready (DB migrations, seed data, config)? -- What happens if the first test fails? - -### Hour 2: Core Implementation - -- Which files must change in what order? -- Are there circular dependencies between changes? -- What's the longest uninterruptible sequence (can't save/test mid-way)? -- Where does the implementer need domain knowledge they might lack? - -### Hour 4: Integration & Edge Cases - -- What happens when components connect for the first time? -- Which error paths are untested until integration? -- Are there race conditions that only appear under load? -- What data shapes haven't been validated end-to-end? - -### Hour 6+: Polish & Ship - -- What's left that "should be quick" but historically isn't? -- Are docs, config updates, and migration scripts included? -- What manual verification is needed before merge? -- If the implementer is interrupted here and picks up tomorrow, what context is lost? - -## Judge Prompt Addition - -When temporal interrogation is enabled, add to each judge's prompt: - -``` -TEMPORAL INTERROGATION: Walk through this plan's implementation timeline. -For each phase (Hour 1, 2, 4, 6+), identify: -1. What blocks progress at this point? -2. What fails silently at this point? -3. What compounds if not caught at this point? -Report temporal findings in a separate "Timeline Risks" section. -``` - -## When to Use - -- **Always for `--deep` reviews** — temporal interrogation is included automatically -- **On request** via `--temporal` flag for quick reviews -- **For a named temporal risk** such as a migration cutover, expiring credential, - irreversible sequence, or coordination window. File and dependency counts do - not select depth by themselves. - -## Between-wave bounded mode - -Do not run temporal interrogation after every wave. Reuse the bound Premortem -while acceptance, dependencies, write scope, and risk remain unchanged. When a -wave materially changes one of those inputs, the orchestrator sends the changed -plan for one fresh Premortem and interrogates only: - -1. the next wave and its exact first failing proof; -2. write-scope or dependency changes caused by the completed wave; -3. new risks or invalidated assumptions in the wave evidence; and -4. whether the next leaf still has one owner and a safe discard path. - -Do not resimulate completed waves or rerun their deterministic/semantic proof. -Emit one bounded PASS/FAIL artifact for the exact changed plan. Premortem does -not own the repair count or next transition; the orchestrator reads the complete -blocker set and decides whether to repair or replan. - -## Report Integration - -Temporal findings appear in the premortem report as: - -```markdown -## Timeline Risks - -| Phase | Risk | Impact if Missed | Mitigation | -|-------|------|------------------|------------| -| Hour 1 | Missing API credentials | Blocks all progress | Add credential check to setup script | -| Hour 2 | Circular import between module A and B | Refactor needed mid-implementation | Extract shared types to common module first | -| Hour 4 | Race condition in parallel write path | Data corruption in production | Add mutex before integration testing | -| Hour 6+ | Migration script not tested on staging data | Rollback needed post-deploy | Run migration on staging clone first | -``` - -## Optional history correlation for deep mode - -In `--deep` mode only, a cited, directly relevant prior failure may inform the -review. Do not scan a broad history index on the routine path, and never -auto-escalate severity solely from recurrence counts. diff --git a/skills-codex/premortem/references/write-premortem-output.md b/skills-codex/premortem/references/write-premortem-output.md deleted file mode 100644 index 43b44432f..000000000 --- a/skills-codex/premortem/references/write-premortem-output.md +++ /dev/null @@ -1,46 +0,0 @@ -# Writing the Premortem Output - -Write one canonical JSON artifact to -`.agents/council/YYYY-MM-DD-premortem-<topic>.json`. - -The artifact conforms to -[`../schemas/plan-verdict.schema.json`](../schemas/plan-verdict.schema.json) and -binds the repository-relative plan path, live SHA-256, distinct author and judge -identities, binary verdict, and complete blocker list. - -```json -{ - "schema_version": "premortem-plan-verdict.v1", - "plan": { - "path": ".agents/plans/2026-07-14-example.md", - "sha256": "<64 lowercase hex>" - }, - "author_id": "planner-context", - "judge_id": "fresh-judge-context", - "verdict": "FAIL", - "blockers_complete": true, - "blockers": [ - { - "id": "B1", - "claim": "The migration inventory omits an active consumer", - "evidence": ["path/to/manifest.json", "path/to/consumer"] - } - ] -} -``` - -Validate the artifact against both the schema and current plan bytes: - -```bash -skills/premortem/scripts/validate-output.sh \ - .agents/council/YYYY-MM-DD-premortem-<topic>.json \ - "$(git rev-parse --show-toplevel)" -``` - -Optional model metadata is descriptive only. Do not add readiness projections, -attempt history, repair budgets, helper state, implementation state, tracker -state, or delivery authority to this artifact. - -Reusable findings may be copied off the critical path only after the -orchestrator accepts them. The immutable plan verdict itself is never rewritten -into a learning registry entry. diff --git a/skills-codex/premortem/schemas/plan-verdict.schema.json b/skills-codex/premortem/schemas/plan-verdict.schema.json deleted file mode 100644 index 2cfc92daf..000000000 --- a/skills-codex/premortem/schemas/plan-verdict.schema.json +++ /dev/null @@ -1,71 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://agentops.local/schemas/premortem-plan-verdict.v1.schema.json", - "title": "Premortem exact-plan verdict", - "type": "object", - "additionalProperties": false, - "required": [ - "schema_version", - "plan", - "author_id", - "judge_id", - "verdict", - "blockers_complete", - "blockers" - ], - "properties": { - "schema_version": {"const": "premortem-plan-verdict.v1"}, - "plan": { - "type": "object", - "additionalProperties": false, - "required": ["path", "sha256"], - "properties": { - "path": {"type": "string", "minLength": 1}, - "sha256": {"type": "string", "pattern": "^[0-9a-f]{64}$"} - } - }, - "author_id": {"type": "string", "minLength": 1}, - "judge_id": {"type": "string", "minLength": 1}, - "author_model": {"$ref": "#/$defs/model"}, - "judge_model": {"$ref": "#/$defs/model"}, - "verdict": {"enum": ["PASS", "FAIL"]}, - "blockers_complete": {"const": true}, - "blockers": { - "type": "array", - "items": {"$ref": "#/$defs/blocker"} - }, - "reviewed_at": {"type": "string", "minLength": 1} - }, - "$defs": { - "model": { - "type": "object", - "additionalProperties": false, - "required": ["name"], - "properties": { - "name": {"type": "string", "minLength": 1}, - "family": {"type": "string", "minLength": 1} - } - }, - "blocker": { - "type": "object", - "additionalProperties": false, - "required": ["id", "claim", "evidence"], - "properties": { - "id": {"type": "string", "minLength": 1}, - "claim": {"type": "string", "minLength": 1}, - "evidence": { - "type": "array", - "minItems": 1, - "items": {"type": "string", "minLength": 1} - } - } - } - }, - "allOf": [ - { - "if": {"properties": {"verdict": {"const": "PASS"}}}, - "then": {"properties": {"blockers": {"maxItems": 0}}}, - "else": {"properties": {"blockers": {"minItems": 1}}} - } - ] -} diff --git a/skills-codex/premortem/schemas/premortem-plan-review.v1.schema.json b/skills-codex/premortem/schemas/premortem-plan-review.v1.schema.json new file mode 100644 index 000000000..c598fad2d --- /dev/null +++ b/skills-codex/premortem/schemas/premortem-plan-review.v1.schema.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://agentops.local/schemas/premortem-plan-review.v1.schema.json", + "title": "Premortem Plan Review", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", + "plan_packet_digest", + "author_context_id", + "judge_context_id", + "findings", + "checked", + "not_checked" + ], + "properties": { + "schema_version": {"const": "premortem-plan-review.v1"}, + "plan_packet_digest": {"type": "string", "pattern": "^[a-f0-9]{64}$"}, + "author_context_id": {"type": "string", "minLength": 1}, + "judge_context_id": {"type": "string", "minLength": 1}, + "findings": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["id", "statement", "evidence"], + "properties": { + "id": {"type": "string", "minLength": 1}, + "statement": {"type": "string", "minLength": 1}, + "evidence": { + "type": "array", + "minItems": 1, + "items": {"type": "string", "minLength": 1} + } + } + } + }, + "checked": {"type": "array", "items": {"type": "string"}}, + "not_checked": {"type": "array", "items": {"type": "string"}} + } +} diff --git a/skills-codex/premortem/scripts/validate-output.sh b/skills-codex/premortem/scripts/validate-output.sh index 9ef1bb2f5..6f748d24e 100755 --- a/skills-codex/premortem/scripts/validate-output.sh +++ b/skills-codex/premortem/scripts/validate-output.sh @@ -1,79 +1,51 @@ #!/usr/bin/env bash set -euo pipefail -if [[ $# -lt 1 || $# -gt 2 || ! -f "$1" ]]; then - echo "usage: $0 <plan-verdict.json> [repository-root]" >&2 +if [[ $# -ne 1 || ! -f "$1" ]]; then + echo "usage: $0 <premortem-plan-review.json>" >&2 exit 2 fi -VERDICT_PATH="$1" -REPO_ROOT="${2:-$(git -C "$(dirname "$VERDICT_PATH")" rev-parse --show-toplevel 2>/dev/null || pwd)}" -SKILL_DIR="$(cd "$(dirname "$0")/.." && pwd)" -SCHEMA_PATH="$SKILL_DIR/schemas/plan-verdict.schema.json" - -python3 - "$VERDICT_PATH" "$REPO_ROOT" "$SCHEMA_PATH" <<'PY' -from __future__ import annotations - -import hashlib +python3 - "$1" <<'PY' import json +import re import sys from pathlib import Path +path = Path(sys.argv[1]) try: - from jsonschema import Draft202012Validator -except ImportError as exc: - print(f"premortem plan verdict: jsonschema is required: {exc}", file=sys.stderr) - raise SystemExit(2) - -verdict_path = Path(sys.argv[1]).resolve() -root = Path(sys.argv[2]).resolve() -schema_path = Path(sys.argv[3]).resolve() - -try: - payload = json.loads(verdict_path.read_text(encoding="utf-8")) - schema = json.loads(schema_path.read_text(encoding="utf-8")) + value = json.loads(path.read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError) as exc: - print(f"premortem plan verdict: unreadable JSON: {exc}", file=sys.stderr) + print(f"premortem plan review: unreadable JSON: {exc}", file=sys.stderr) raise SystemExit(1) -errors = sorted( - Draft202012Validator(schema).iter_errors(payload), - key=lambda item: [str(part) for part in item.absolute_path], -) -if errors: - error = errors[0] - location = "/".join(str(part) for part in error.absolute_path) or "<root>" - print(f"premortem plan verdict: schema violation at {location}: {error.message}", file=sys.stderr) +required = { + "schema_version", "plan_packet_digest", "author_context_id", + "judge_context_id", "findings", "checked", "not_checked", +} +if set(value) != required: + print("premortem plan review: unexpected or missing fields", file=sys.stderr) raise SystemExit(1) - -if payload["author_id"] == payload["judge_id"]: - print("premortem plan verdict: author_id must differ from judge_id", file=sys.stderr) - raise SystemExit(1) - -relative = Path(payload["plan"]["path"]) -if relative.is_absolute(): - print("premortem plan verdict: plan path must be repository-relative", file=sys.stderr) - raise SystemExit(1) -plan_path = (root / relative).resolve() -try: - plan_path.relative_to(root) -except ValueError: - print("premortem plan verdict: plan path escapes repository root", file=sys.stderr) - raise SystemExit(1) -if not plan_path.is_file(): - print(f"premortem plan verdict: plan does not exist: {relative}", file=sys.stderr) - raise SystemExit(1) - -actual = hashlib.sha256(plan_path.read_bytes()).hexdigest() -if actual != payload["plan"]["sha256"]: - print( - f"premortem plan verdict: stale plan digest: expected {payload['plan']['sha256']}, got {actual}", - file=sys.stderr, - ) - raise SystemExit(1) - -print( - f"premortem plan verdict valid: {payload['verdict']} " - f"{payload['plan']['path']}@{payload['plan']['sha256']}" -) +if value["schema_version"] != "premortem-plan-review.v1": + raise SystemExit("premortem plan review: wrong schema_version") +if not re.fullmatch(r"[a-f0-9]{64}", value["plan_packet_digest"]): + raise SystemExit("premortem plan review: invalid plan digest") +author = value["author_context_id"] +judge = value["judge_context_id"] +if not isinstance(author, str) or not author or not isinstance(judge, str) or not judge or author == judge: + raise SystemExit("premortem plan review: author and judge identities must be nonempty and distinct") +for field in ("checked", "not_checked"): + if not isinstance(value[field], list) or not all(isinstance(item, str) for item in value[field]): + raise SystemExit(f"premortem plan review: {field} must be a string array") +if not isinstance(value["findings"], list): + raise SystemExit("premortem plan review: findings must be an array") +for finding in value["findings"]: + if not isinstance(finding, dict) or set(finding) != {"id", "statement", "evidence"}: + raise SystemExit("premortem plan review: malformed finding") + if not all(isinstance(finding[key], str) and finding[key] for key in ("id", "statement")): + raise SystemExit("premortem plan review: finding id and statement are required") + evidence = finding["evidence"] + if not isinstance(evidence, list) or not evidence or not all(isinstance(item, str) and item for item in evidence): + raise SystemExit("premortem plan review: each finding needs evidence") +print("premortem plan review: valid") PY diff --git a/skills-codex/premortem/scripts/validate.sh b/skills-codex/premortem/scripts/validate.sh index 5ea54d4d0..7dc76b541 100755 --- a/skills-codex/premortem/scripts/validate.sh +++ b/skills-codex/premortem/scripts/validate.sh @@ -1,37 +1,21 @@ #!/usr/bin/env bash set -euo pipefail -SKILL_DIR="$(cd "$(dirname "$0")/.." && pwd)" -REPO_ROOT="$(cd "$SKILL_DIR/../.." && pwd)" -SKILL="$SKILL_DIR/SKILL.md" -SCHEMA="$SKILL_DIR/schemas/plan-verdict.schema.json" -VALIDATOR="$SKILL_DIR/scripts/validate-output.sh" -PASS=0 -FAIL=0 +skill_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -check() { - if bash -c "$2"; then - echo "PASS: $1" - PASS=$((PASS + 1)) - else - echo "FAIL: $1" - FAIL=$((FAIL + 1)) - fi -} +grep -q '^name: premortem$' "$skill_dir/SKILL.md" +grep -Fq 'optional plan-challenge strategy' "$skill_dir/SKILL.md" +grep -Fq 'It is not part of the required RPI sequence' "$skill_dir/SKILL.md" +grep -Fq 'advisory findings' "$skill_dir/SKILL.md" +grep -q '^Feature: Premortem optionally challenges one frozen plan$' \ + "$skill_dir/references/premortem.feature" +test -f "$skill_dir/schemas/premortem-plan-review.v1.schema.json" +test -x "$skill_dir/scripts/validate-output.sh" -check "SKILL.md exists" "test -f '$SKILL'" -check "frontmatter name" "grep -q '^name: premortem' '$SKILL'" -check "exact-plan output contract" "grep -q '^output_contract: skills/premortem/schemas/plan-verdict.schema.json' '$SKILL' || grep -Fq '[plan-verdict.schema.json](schemas/plan-verdict.schema.json)' '$SKILL'" -check "fresh author-distinct judge" "grep -Fq 'author_id != judge_id' '$SKILL'" -check "binary complete verdict" "grep -Fq 'Emit exactly' '$SKILL' && grep -q 'complete nonempty blocker set' '$SKILL'" -check "family is optional metadata" "grep -q 'Model and family' '$SKILL' && grep -q 'metadata are optional' '$SKILL' && grep -q 'no risk class requires different model families' '$SKILL'" -check "no local controller ownership" "grep -q 'Do not own retries, attempt maps, budgets, helper state' '$SKILL'" -check "schema and validator exist" "test -f '$SCHEMA' && test -x '$VALIDATOR'" -check "schema is strict" "jq -e '.additionalProperties == false and (.properties.verdict.enum == [\"PASS\",\"FAIL\"]) and (.properties.blockers_complete.const == true)' '$SCHEMA' >/dev/null" -check "plan-pawl authority removed" "! rg -q 'plan-pawl|ApprovalEdge|Fable|WARN.*Ready|PASS/WARN/FAIL|cross-family rule for one-way doors' '$SKILL' '$SKILL_DIR/references/mandatory-checks.md' '$SKILL_DIR/references/premortem.feature' '$SKILL_DIR/references/write-premortem-output.md'" -check "kernel stays within 250 lines" "test \$(wc -l < '$SKILL') -le 250" -check "focused direct-cut acceptance exists" "test -f '$REPO_ROOT/tests/scripts/premortem-plan-verdict-direct-cut.bats'" +if grep -Eiq 'ao (pawl|land)|git (commit|push)|br (close|update)|auto-redo|next[_ -]action' \ + "$skill_dir/SKILL.md"; then + echo 'premortem contract contains forbidden lifecycle authority' >&2 + exit 1 +fi -echo -echo "Results: $PASS passed, $FAIL failed" -test "$FAIL" -eq 0 +echo 'premortem skill contract: PASS' diff --git a/skills-codex/refactor/.agentops-generated.json b/skills-codex/refactor/.agentops-generated.json index 92c1f8db3..deeb4db67 100644 --- a/skills-codex/refactor/.agentops-generated.json +++ b/skills-codex/refactor/.agentops-generated.json @@ -2,6 +2,6 @@ "generator": "codex-sync", "source_skill": "skills/refactor", "layout": "modular", - "source_hash": "68e6ebadab67c1a9d87511c4b22038684938f48304c8add45b2a656154c6e23b", + "source_hash": "7d7e173fe4ca1c04b5ebcc998c0ca16b3ff6ce5c0f5263c1d064dbea7b6eefab", "generated_hash": "1bf92d5fc88969e3ecb14eb5efa5e9ee0c28dc582573c72fbad8a9e1978c4f56" } diff --git a/skills-codex/research/.agentops-generated.json b/skills-codex/research/.agentops-generated.json index 05ebd7f1d..5f4389426 100644 --- a/skills-codex/research/.agentops-generated.json +++ b/skills-codex/research/.agentops-generated.json @@ -2,6 +2,6 @@ "generator": "codex-sync", "source_skill": "skills/research", "layout": "modular", - "source_hash": "350a71a018d2920564230b324f4202f4f02f5954fc6f8003cdb25028a1c85afe", - "generated_hash": "512927218fdcc2a17787a45b9c553526914d8b39421321bb2de415ce989660ec" + "source_hash": "53f5b3346bc17828b6e5d9633cfbe621ecaaaa9ec0e0ba56000eeeaae89183cf", + "generated_hash": "d1b2da64ce054601d331d397404376c90683f565561c023d8358dd95af865c32" } diff --git a/skills-codex/research/SELF-TEST.md b/skills-codex/research/SELF-TEST.md deleted file mode 100644 index b27accdd2..000000000 --- a/skills-codex/research/SELF-TEST.md +++ /dev/null @@ -1,44 +0,0 @@ -# Research Skill Self-Test - -## Trigger Cases - -- User says: `/research "authentication system"` (or any `/research <topic>`). - - Expected: load `research`, create `.agents/research/`, search prior art first, then dispatch an explore agent. - -- User says: "investigate how the cache layer works and write up the findings." - - Expected: load `research` and produce a cited `.agents/research/YYYY-MM-DD-<slug>.md` artifact. - -- User says: `/research "payment processing flow" --auto`. - - Expected: load `research` and run the full workflow without the Gate-1 human approval step. - -## Non-Trigger Cases - -- User asks to implement or change code directly with no investigation request. - - Expected: do not load `research`; route to `/implement` or `/plan`. - -- User asks for session/handoff history ("what did we decide last session?"). - - Expected: use `/recover`, not `research` — `research` reads git commit history, not session history. - -## Behavior Checks - -These map to the four scenarios in [references/research.feature](references/research.feature): - -- Prior art is searched before fresh exploration: `ao lookup` plus the `.agents/` knowledge dirs run first, and applicable learnings are cited in the output (not just loaded passively). -- An explore agent is actually dispatched (not merely described) using the detected backend, and it uses iterative retrieval — score results, extract new terms from high-relevance hits, refine over up to 3 cycles. -- Findings are written to `.agents/research/YYYY-MM-DD-<slug>.md`, and every claim carries a `file:line` citation. -- Interactive runs request human approval (Gate 1) before reporting completion; `--auto` proceeds without the gate. - -## Validation Commands - -Run from the repo root: - -```bash -bash skills/heal-skill/scripts/heal.sh --strict skills/research -bash scripts/validate-skill-frontmatter.sh --strict -``` - -## Failure Cases - -- Explore agent only described, never dispatched: re-run and dispatch the agent (or perform the exploration inline if no spawn backend is available) — see the Key Rules in `SKILL.md`. -- Findings written without `file:line` citations: fail the artifact and re-cite every claim before reporting. -- Missing reference file linked from `SKILL.md`: fail heal validation and restore the file or remove the link. diff --git a/skills-codex/research/SKILL.md b/skills-codex/research/SKILL.md index 02078266d..fa60ccf00 100644 --- a/skills-codex/research/SKILL.md +++ b/skills-codex/research/SKILL.md @@ -1,146 +1,39 @@ --- name: research -description: Explore and write findings. +description: Answer a bounded question with current cited --- -# Research Skill +# Research -Answer a bounded question with current, cited evidence and a durable research -artifact. Execute the investigation; do not return a search diary or an -uncited opinion. +Answer one bounded question with current evidence. Research informs a caller; +it does not select work, approve a plan, mutate lifecycle state, or decide what +happens next. -## Critical Constraints +## Contract -- **Why: avoid aimless exploration.** State the question, decision it informs, - scope, non-goals, freshness needs, and evidence-for-done before searching. -- **Why: prevent rediscovery.** Search `ao lookup` and existing `.agents/` - knowledge first, then test retrieved claims against current authoritative sources. -- **Why: keep facts trustworthy.** Every load-bearing claim cites `file:line`, a - commit, or a direct external source; distinguish observation from inference. -- **Why: control context.** Search in bounded directories, follow discovered - symbols, and stop after three iterative-retrieval cycles unless new evidence - materially changes the answer. -- **Why: honor operator control.** Use one inline agent by default. Spawn an - Explore agent or parallel lanes only when the user or active workflow explicitly - authorizes multi-agent research and scopes non-overlapping work. -- **Why: avoid stale external claims.** Browse current primary sources for - changing APIs, standards, products, or upstream behavior and cite them directly. -- **Why: preserve uncertainty.** Record gaps, contradictions, failed searches, - and confidence; do not turn absence of evidence into evidence of absence. +1. State the question, decision it informs, scope, non-goals, and evidence + required for a useful answer. +2. Search the smallest relevant local sources. For changing external facts, + use current primary sources. +3. Verify structural or semantic-search leads against authoritative content. +4. Separate observation, inference, contradiction, and unknown. +5. Lead with the answer and cite every load-bearing claim. +6. Report unchecked scope and stop. -## Inputs and Modes +Use the current agent inline by default. Parallel readers or alternate runtimes +are optional execution choices only when the caller authorizes them. Prior +research, CASS, MS, codebase recon, and pattern mining are advisory sources, +not required phases. -`$research <question> [--auto] [--from-pr <url>] [quick|medium|very-thorough]` +## Output -- `--auto` skips the Gate-1 approval prompt after quality validation; it does - not authorize external mutations, extra runtimes, or unbounded delegation. -- `--from-pr` narrows source and history inspection to the PR's changed paths. -- Quick answers may stay in chat when no durable handoff is needed. Medium and - architecture/cross-cutting work writes `.agents/research/`. +For a quick question, return the cited answer directly. When the caller asks +for a durable artifact, write one report containing: -## Workflow +- question and scope; +- answer; +- evidence references; +- contradictions and unknowns; +- checked and unchecked areas. -1. **Frame the inquiry.** Write the primary question, subquestions, target - decision, repositories/systems in scope, non-goals, freshness horizon, and - completion test. Choose quick, medium, or very-thorough depth. -2. **Retrieve prior knowledge.** Run `ao lookup --query "<topic>" --limit 5` - when available and search `.agents/{research,learnings,knowledge,patterns, - retros,plans,brainstorm}/` by content. For each applicable hit, record how it - changes the inquiry and verify it against current source. -3. **Choose evidence lanes.** Use code-map and - [codebase-archaeology.md](references/codebase-archaeology.md) for repository - questions; [structural-graph-navigation.md](references/structural-graph-navigation.md) - for refreshed graphify structure; scoped git history for decision context; - [software-research.md](references/software-research.md) or primary web sources - for upstream facts. Structure maps locate relationships, not in-body logic. -4. **Run iterative retrieval.** Start broad inside the declared scope, score - evidence relevance 0-1, extract symbols/config keys from items scoring at - least 0.5, and use them in the next pass. Read authoritative files to verify - every structural or semantic-search lead. Stop after three cycles or saturation. -5. **Select backend deliberately.** Detect the available backend and record it. - When parallelism is authorized, give Explore agent lanes distinct questions - and read-only scopes, then merge their evidence. Otherwise research inline. - See the backend references for Codex, background-task, Claude-team, and inline - variants; runtime and host instructions decide which are legal. -6. **Validate quality.** Assess coverage, depth (0-4 per critical area), gaps, - contradictory evidence, and assumptions. Under `--auto`, any critical depth - below 2 produces WARN plus `.agents/research/quality-warning.md`; do not hide it. -7. **Synthesize.** Write `.agents/research/YYYY-MM-DD-<topic-slug>.md` using - [document-template.md](references/document-template.md). Lead with the answer, - then key files/sources, findings, evidence, unresolved questions, confidence, - recommendations, and the backend used. -8. **Persist reusable findings selectively.** Only reusable findings that should - alter future planning enter `.agents/findings/registry.jsonl`. Require - provenance, `dedup_key`, pattern, detection question, checklist item, - applicability, confidence, and lifecycle fields; merge by key using the - contract's temp-file-plus-rename atomic write rule. Then run - `bash hooks/finding-compiler.sh --quiet` when present. -9. **Gate and report.** Unless `--auto`, ask whether the evidence is sufficient - to proceed to `$plan`, needs revision, or should be abandoned. Report the - answer, artifact path, confidence/gaps, approval status, and next route. - -## Backend Policy - -| Condition | Backend | -|---|---| -| no explicit multi-agent authorization | inline current agent | -| authorized Codex parallel lanes | bounded Codex sub-agents | -| authorized runtime lacks sub-agents | documented background-task fallback | -| no legal spawn backend | inline current agent | - -Backend selection changes execution mechanics, never evidence standards. Read -[iterative-retrieval.md](references/iterative-retrieval.md) and only the backend -module selected for the run. - -## Output Specification - -- **Artifact directory:** `.agents/research/`; optional quality warning at - `.agents/research/quality-warning.md`; reusable findings use the findings registry. -- **Filename convention:** `YYYY-MM-DD-<topic-slug>.md`; stable slug, no - overwrite of unrelated research. -- **Serialization/schema format:** Markdown following the document template plus - `result.json` conforming to `skills/research/schemas/findings.json` when a - machine handoff is required. -- **Validator command:** run `bash skills/research/scripts/validate.sh`, verify - cited paths/lines or URLs, and confirm critical depth/gap reporting. -- **Downstream handoff:** consumed by `$plan`, `$product`, `$premortem`, or the - requesting decision maker; reusable findings feed compiled prevention context. - -## Quality Rubric - -- **Decision-focused:** directly answers the framed question and names implications. -- **Authoritative:** current primary sources and source code outrank summaries. -- **Traceable:** every material claim has reproducible evidence and provenance. -- **Scoped:** search breadth matches the question without context flooding. -- **Honest:** inferences, contradictions, gaps, freshness, and confidence are explicit. -- **Durable:** a fresh reader can act from the artifact without chat context. - -## Examples - -**User says:** `$research "authentication request flow"` - -Trace one entry point through current code, use scoped history for rationale, -cite every transition, and write a medium-depth artifact. - -**User says:** `$research --from-pr <url> "does this change preserve retries?"` - -Restrict evidence to changed paths and their callers/tests, verify upstream -context, and state remaining uncertainty before recommending action. - -## Troubleshooting - -| Problem | Response | -|---|---| -| Topic is too broad | Split it into decision-sized questions | -| Prior research conflicts with source | Prefer current source and record the drift | -| Graph result lacks logic | Open the defining/calling files and verify behavior | -| Critical depth is below 2 | WARN, record the gap, and do not imply completeness | -| No spawn backend is authorized | Research inline; do not treat that as degraded evidence | - -## References - -- [research.feature](references/research.feature) · [document-template.md](references/document-template.md) · [iterative-retrieval.md](references/iterative-retrieval.md) -- [context-discovery.md](references/context-discovery.md) · [source-discovery-and-pattern-extraction.md](references/source-discovery-and-pattern-extraction.md) · [failure-patterns.md](references/failure-patterns.md) -- [codebase-archaeology.md](references/codebase-archaeology.md) · [data-flow-from-entry-points.md](references/data-flow-from-entry-points.md) · [onboarding-methodology.md](references/onboarding-methodology.md) -- [structural-graph-navigation.md](references/structural-graph-navigation.md) · [software-research.md](references/software-research.md) · [deep-research-mcp.md](references/deep-research-mcp.md) -- [backend-codex-subagents.md](references/backend-codex-subagents.md) · [backend-background-tasks.md](references/backend-background-tasks.md) · [backend-claude-teams.md](references/backend-claude-teams.md) · [backend-inline.md](references/backend-inline.md) -- [ralph-loop-contract.md](references/ralph-loop-contract.md) · [vibe-methodology.md](references/vibe-methodology.md) · [claude-code-latest-features.md](references/claude-code-latest-features.md) +Do not emit approval, confidence gates, retry instructions, owner, next action, +or delivery state. diff --git a/skills-codex/research/prompt.md b/skills-codex/research/prompt.md index b113cb9ca..e62e0983c 100644 --- a/skills-codex/research/prompt.md +++ b/skills-codex/research/prompt.md @@ -1,6 +1,6 @@ # research -Explore and write findings. Triggers: "research", "explore and write findings.", "research skill". +Answer a bounded question with current cited evidence. Triggers: "research", "investigate", "find evidence". ## Instructions diff --git a/skills-codex/research/references/backend-background-tasks.md b/skills-codex/research/references/backend-background-tasks.md deleted file mode 100644 index e0f445417..000000000 --- a/skills-codex/research/references/backend-background-tasks.md +++ /dev/null @@ -1,125 +0,0 @@ -# Backend: Background Tasks (Fallback) - -Concrete tool calls for spawning agents using `Task(run_in_background=true)`. This is the **last-resort fallback** when neither Codex sub-agents nor Claude native teams are available. - -**When detected:** `Task` tool is available but `TeamCreate` and `spawn_agent` are not. - -**Limitations:** -- Fire-and-forget — no messaging, no redirect, no scope adjustment -- No inter-agent communication -- No debate mode (R2 requires messaging) -- No retry (must re-spawn from scratch) -- No graceful shutdown (only `TaskStop`, which is lossy) - ---- - -## Spawn: Background Agents - -Spawn agents with `Task(run_in_background=true)`. Each call returns a `task_id` for later polling. - -### Council Judges - -``` -Task( - subagent_type="general-purpose", - run_in_background=true, - prompt="You are judge-1.\n\nYour perspective: Correctness & Completeness\n\n<PACKET>\n...\n</PACKET>\n\nWrite your verdict to .agents/council/2026-02-17-auth-judge-1.md\nThis is your ONLY output channel — there is no messaging.", - description="Council judge-1" -) -# Returns: task_id="abc-123" - -Task( - subagent_type="general-purpose", - run_in_background=true, - prompt="You are judge-error-paths.\n\nYour perspective: Error Paths & Edge Cases\n\n<PACKET>...</PACKET>\n\nWrite your verdict to .agents/council/2026-02-17-auth-judge-error-paths.md", - description="Council judge-error-paths" -) -# Returns: task_id="def-456" -``` - -Both `Task` calls go in the **same message** — they run in parallel. - -### Swarm Workers - -``` -Task( - subagent_type="general-purpose", - run_in_background=true, - prompt="You are worker-3.\n\nYour Assignment: Task #3: Add password hashing\n...\n\nWrite result to .agents/swarm/results/3.json\nDo NOT run git add/commit/push.", - description="Swarm worker-3" -) -``` - -### Research Explorers - -``` -Task( - subagent_type="Explore", - run_in_background=true, - prompt="Thoroughly investigate: authentication patterns...\n\nWrite findings to .agents/research/2026-02-17-auth.md", - description="Research explorer" -) -``` - ---- - -## Wait: Poll for Completion - -Background tasks have no messaging. Poll with `TaskOutput`. - -``` -TaskOutput(task_id="abc-123", block=true, timeout=120000) -TaskOutput(task_id="def-456", block=true, timeout=120000) -``` - -**Or non-blocking check:** - -``` -TaskOutput(task_id="abc-123", block=false, timeout=5000) -``` - -**After `TaskOutput` returns**, verify the agent wrote its result file: - -``` -Read(".agents/council/2026-02-17-auth-judge-1.md") -``` - -**Timeout behavior:** If `timeout` expires, `TaskOutput` returns with a timeout status — the agent may still be running. **Recovery:** -1. Check result file — agent may have written it but not finished cleanly -2. If result file exists → use it, `TaskStop` the agent -3. If no result file → agent failed silently. For council: proceed with N-1 verdicts, note in report. For swarm: add task back to retry queue, re-spawn a fresh agent. -4. Never assume `TaskOutput` completion means the result file was written — always verify - -**Fallback:** If background tasks fail despite detection, fall back to inline mode. See `backend-inline.md`. - ---- - -## No Messaging - -Background tasks cannot receive messages. This means: - -- **No debate R2** — judges get one round only -- **No retry** — if validation fails, re-spawn a new agent from scratch -- **No scope adjustment** — the prompt is final at spawn time - ---- - -## Cleanup - -Background tasks self-terminate when done. For stuck tasks: - -``` -TaskStop(task_id="abc-123") -``` - -This is lossy — partial work may be lost. - ---- - -## Key Rules - -1. **Filesystem is the only communication channel** — agents write files, lead reads files -2. **No messaging = no debate** — `--debate` is unavailable with this backend -3. **No retry = must re-spawn** — failed agents get a fresh `Task` call, not a message -4. **Always check result files** — `TaskOutput` completion doesn't guarantee the agent wrote its file -5. **Prefer native teams** — this backend is strictly inferior; use it only as last resort diff --git a/skills-codex/research/references/backend-claude-teams.md b/skills-codex/research/references/backend-claude-teams.md deleted file mode 100644 index 545d3268b..000000000 --- a/skills-codex/research/references/backend-claude-teams.md +++ /dev/null @@ -1,240 +0,0 @@ -# Backend: Claude Native Teams - -Concrete tool calls for spawning agents using Claude Code native teams (`TeamCreate` + `SendMessage` + shared `TaskList`). - -**When detected:** `TeamCreate` tool is available in your tool list. - ---- - -## Pre-Flight: Confirm Modern Claude Features - -Before spawning teammates, verify feature readiness: - -1. `claude agents` succeeds (custom agents discoverable) -2. Teammate profiles for write tasks declare `isolation: worktree` -3. Long-running teammates prefer `background: true` -4. Hooks include worktree lifecycle coverage (`WorktreeCreate`, `WorktreeRemove`) and config auditing (`ConfigChange`) where policy requires it - -For canonical feature details, read: -`skills/shared/references/claude-code-latest-features.md`. - ---- - -## Setup: Create Team - -Every spawn session starts by creating a team. One team per wave (fresh context = Ralph Wiggum preserved; see `skills/shared/references/ralph-loop-contract.md`). - -``` -TeamCreate(team_name="council-20260217-auth", description="Council validation of auth module") -``` - -``` -TeamCreate(team_name="swarm-1739812345-w1", description="Wave 1: parallel implementation") -``` - -**Naming conventions:** -- Council: `council-YYYYMMDD-<target>` -- Swarm: `swarm-<epoch>-w<wave>` -- Crank: delegates to swarm naming - -## Leader Contract (Native Teams) - -Claude teams are leader-first orchestration: - -1. One lead creates the team and assigns all work. -2. Teammates never self-assign from shared tasks. -3. Teammates report to lead via short `SendMessage` signals. -4. Lead reads result artifacts from disk, validates, and decides retries/escalation. - -Recommended signal envelope (single-line JSON, under 100 tokens): - -```json -{"type":"completion|blocked|help_request","agent":"worker-3","task":"3","detail":"short status","artifact":".agents/swarm/results/3.json"} -``` - -`completion`: task finished, artifact written. -`blocked`: cannot proceed safely. -`help_request`: teammate needs coordination or scope clarification. - -### Peer Messaging (Allowed, Lead-Controlled) - -Native teams support direct teammate-to-teammate messaging. Use this only for coordination handoffs; keep messages thin and always copy the lead in follow-up summaries. - -```text -worker-2 -> worker-5: "Need auth schema constant name; please confirm from src/auth/schema.ts" -worker-5 -> lead: "Resolved peer question for worker-2; no scope change." -``` - ---- - -## Spawn: Create Workers/Judges - -After `TeamCreate`, spawn each agent with `Task(team_name=..., name=...)`. All agents in a wave spawn in parallel (single message, multiple tool calls). - -### Council Judges (parallel spawn) - -``` -Task( - subagent_type="general-purpose", - team_name="council-20260217-auth", - name="judge-1", - prompt="You are judge-1 on team council-20260217-auth.\n\nYour perspective: Correctness & Completeness\n\n<PACKET>\n...\n</PACKET>\n\nWrite your verdict to .agents/council/2026-02-17-auth-judge-1.md\nThen send a SHORT completion signal to the team lead (under 100 tokens).\nDo NOT include your full analysis in the message — the lead reads your file.", - description="Council judge-1" -) - -Task( - subagent_type="general-purpose", - team_name="council-20260217-auth", - name="judge-error-paths", - prompt="You are judge-error-paths on team council-20260217-auth.\n\nYour perspective: Error Paths & Edge Cases\n\n<PACKET>\n...\n</PACKET>\n\nWrite your verdict to .agents/council/2026-02-17-auth-judge-error-paths.md\nThen send a SHORT completion signal to the team lead (under 100 tokens).", - description="Council judge-error-paths" -) -``` - -Both `Task` calls go in the **same message** — they spawn in parallel. - -### Swarm Workers (parallel spawn) - -``` -Task( - subagent_type="general-purpose", - team_name="swarm-1739812345-w1", - name="worker-3", - prompt="You are worker-3 on team swarm-1739812345-w1.\n\nYour Assignment: Task #3: Add password hashing\n<description>...</description>\n\nInstructions:\n1. Execute your task — create/edit files as needed\n2. Write result to .agents/swarm/results/3.json\n3. Send a SHORT signal to team lead (under 100 tokens)\n4. Do NOT run git add/commit/push — the lead commits\n\nRESULT FORMAT:\n{\"type\":\"completion\",\"issue_id\":\"3\",\"status\":\"done\",\"detail\":\"one-line summary\",\"artifacts\":[\"path/to/file\"]}", - description="Swarm worker-3" -) - -Task( - subagent_type="general-purpose", - team_name="swarm-1739812345-w1", - name="worker-5", - prompt="You are worker-5 on team swarm-1739812345-w1.\n\nYour Assignment: Task #5: Create login endpoint\n...", - description="Swarm worker-5" -) -``` - -### Research Explorers (read-only) - -``` -Task( - subagent_type="Explore", - team_name="research-20260217-auth", - name="explorer-1", - prompt="Thoroughly investigate: authentication patterns in this codebase\n\n...", - description="Research explorer" -) -``` - -Use `subagent_type="Explore"` for read-only research agents. Use `"general-purpose"` for agents that need to write files. - ---- - -## Wait: Receive Completion Signals - -Workers/judges send completion signals via `SendMessage`. These are **automatically delivered** to the team lead — no polling needed. - -When a teammate finishes, their message appears as a new conversation turn. The lead reads result files from disk, NOT from message content. - -``` -# Teammate message arrives automatically: -# "judge-1: Done. Verdict: WARN, confidence: HIGH. File: .agents/council/2026-02-17-auth-judge-1.md" - -# Lead reads the file for full details: -Read(".agents/council/2026-02-17-auth-judge-1.md") -``` - -**Timeout handling (default: 120s per round, 90s for debate R2):** - -If a teammate goes idle without sending a completion signal: -1. Check their result file — they may have written it but failed to message -2. If result file exists → read it and proceed (the message was the only thing missing) -3. If no result file → the agent failed silently. **Recovery:** proceed with N-1 judges/workers and note the failure in the report. For swarm workers, add the task back to the retry queue. -4. Never wait indefinitely — after the timeout, move on - -See `skills/council/references/cli-spawning.md` for timeout configuration (`COUNCIL_TIMEOUT`, `COUNCIL_R2_TIMEOUT`). - -**Fallback:** If native teams fail at runtime despite passing detection (e.g., `TeamCreate` succeeds but `Task` spawning fails), fall back to background tasks. See `backend-background-tasks.md`. - ---- - -## Message: Debate R2 / Retry - -Send messages to specific teammates using `SendMessage`. Teammates wake from idle when messaged. - -### Council Debate R2 - -``` -SendMessage( - type="message", - recipient="judge-1", - content="DEBATE ROUND 2\n\nOther judges' verdicts:\n- judge-error-paths: FAIL (HIGH confidence) — file: .agents/council/2026-02-17-auth-judge-error-paths.md\n\nRead the other judge's file. Revise your assessment considering their perspective.\nWrite your R2 verdict to .agents/council/2026-02-17-auth-judge-1-r2.md\nThen send a completion signal.", - summary="R2 debate instructions for judge-1" -) -``` - -**R2 timeout (default: 90s):** If a judge doesn't respond to R2 within `COUNCIL_R2_TIMEOUT`, use their R1 verdict for consolidation. See `skills/council/references/debate-protocol.md` for full timeout handling. - -### Swarm Worker Retry - -``` -SendMessage( - type="message", - recipient="worker-3", - content="Validation failed: pytest tests/test_auth.py returned exit code 1.\nFix the failing tests and rewrite your result to .agents/swarm/results/3.json", - summary="Retry worker-3: test failure" -) -``` - ---- - -## Cleanup: Shutdown and Delete - -After consolidation/validate, shut down all teammates then delete the team. - -``` -# Shutdown each teammate -SendMessage(type="shutdown_request", recipient="judge-1", content="Council complete") -SendMessage(type="shutdown_request", recipient="judge-error-paths", content="Council complete") - -# After all teammates acknowledge shutdown: -TeamDelete() -``` - -**Reaper pattern:** If a teammate doesn't respond to shutdown within 30s, proceed with `TeamDelete()` anyway. - -**If `TeamDelete` fails** (e.g., stale members): clean up manually with `rm -rf ~/.claude/teams/<team-name>/` then retry `TeamDelete()` to clear in-memory state. - ---- - -## Multi-Wave Pattern - -For crank/swarm with multiple waves, create a **new team per wave**: - -``` -# Wave 1 -TeamCreate(team_name="swarm-1739812345-w1", description="Wave 1") -# ... spawn workers, wait, validate, commit ... -# ... shutdown teammates ... -TeamDelete() -# If TeamDelete fails: rm -rf ~/.claude/teams/swarm-1739812345-w1/ then retry - -# Wave 2 (fresh context) -TeamCreate(team_name="swarm-1739812345-w2", description="Wave 2") -# ... spawn workers for newly-unblocked tasks ... -TeamDelete() -``` - -This ensures each wave's workers start with clean context (no leftover state from prior waves). - -**If `TeamDelete` fails between waves**, the next `TeamCreate` may conflict. Always verify cleanup succeeded before creating the next wave team. - ---- - -## Key Rules - -1. **`TeamCreate` before `Task`** — tasks created before the team are invisible to teammates — **Enforcement: `safety.ValidateTeamLifecycle()` (T9)** -2. **Pre-assign tasks before spawning** — workers do NOT race-claim from TaskList — **Enforcement: documentation only** -3. **Lead-only commits** — workers write files, lead runs `git add` + `git commit` — **Enforcement: `hooks/git-worker-guard.sh` (T4)** -4. **Thin messages** — workers send <100 token signals, full results go to disk — **Enforcement: `safety.ValidateMessageSize()` (T9)** -5. **New team per wave** — fresh context, Ralph Wiggum preserved — **Enforcement: `safety.ValidateTeamLifecycle()` (T9)** -6. **Always cleanup** — `TeamDelete()` after every wave, even on partial failure — **Enforcement: `hooks/stop-team-guard.sh` + `safety.ValidateTeamLifecycle()` (T9)** diff --git a/skills-codex/research/references/backend-codex-subagents.md b/skills-codex/research/references/backend-codex-subagents.md deleted file mode 100644 index 46141ef51..000000000 --- a/skills-codex/research/references/backend-codex-subagents.md +++ /dev/null @@ -1,120 +0,0 @@ -# Backend: Codex Sub-Agents - -Concrete tool calls for spawning agents using Codex CLI (`codex exec`). Used for `--mixed` mode cross-vendor consensus and as the primary backend when running inside a Codex session with `spawn_agent`. - ---- - -## Variant A: Codex CLI (from any runtime) - -Used when `codex` CLI is available on PATH. Agents run as background shell processes. - -**When detected:** `which codex` succeeds. - -### Spawn: Background Shell Processes - -```bash -# With structured output (preferred for council judges) -Bash( - command='codex exec -s read-only -m gpt-5.3-codex -C "$(pwd)" --output-schema skills/council/schemas/verdict.json -o .agents/council/codex-1.json "JUDGE PROMPT HERE"', - run_in_background=true -) - -# Without structured output (fallback) -Bash( - command='codex exec --full-auto -m gpt-5.3-codex -C "$(pwd)" -o .agents/council/codex-1.md "JUDGE PROMPT HERE"', - run_in_background=true -) -``` - -**Flag order:** `-s`/`--full-auto` → `-m` → `-C` → `--output-schema` → `-o` → prompt - -**Valid flags:** `--full-auto`, `-s`, `-m`, `-C`, `--output-schema`, `-o`, `--add-dir` -**Invalid flags:** `-q` (doesn't exist), `--quiet` (doesn't exist), `-p` as a prompt flag (in Codex CLI it means profile) - -### Wait: Poll Background Shell - -``` -TaskOutput(task_id="<shell-id>", block=true, timeout=120000) -``` - -Then read the output file: - -``` -Read(".agents/council/codex-1.json") -``` - -### Limitations - -- No messaging — Codex CLI processes are fire-and-forget -- No debate R2 with Codex judges — they produce one verdict only -- `--output-schema` requires `additionalProperties: false` at all levels -- `--output-schema` requires ALL properties in `required` array -- `-s read-only` + `-o` works — `-o` is CLI-level post-processing, not sandbox I/O - ---- - -## Variant B: Codex Sub-Agents (inside Codex runtime) - -Used when running inside a Codex session where `spawn_agent` is available. - -**When detected:** `spawn_agent` tool is in your tool list. - -### Spawn - -``` -spawn_agent(message="You are judge-1.\n\nPerspective: Correctness & Completeness\n\n<PACKET>...</PACKET>\n\nWrite verdict to .agents/council/2026-02-17-auth-judge-1.md") -# Returns: agent_id - -spawn_agent(message="You are worker-3.\n\nTask: Add password hashing\n...\n\nWrite result to .agents/swarm/results/3.json") -# Returns: agent_id -``` - -### Wait - -``` -wait(ids=["agent-id-1", "agent-id-2"]) -``` - -**Timeout:** `wait()` blocks until completion. Set a timeout at the orchestration level (default: `COUNCIL_TIMEOUT=120s`). If an agent doesn't complete within the timeout, `close_agent` it and proceed with N-1 verdicts/workers. - -### Message (retry/follow-up) - -``` -send_input(id="agent-id-1", message="Validation failed: fix tests and retry") -``` - -### Cleanup - -``` -close_agent(id="agent-id-1") -``` - ---- - -## Mixed Mode (Council) - -For `--mixed` council, spawn runtime-native judges AND Codex CLI judges in parallel: - -``` -# Claude native team judges (via TeamCreate — see backend-claude-teams.md) -Task(subagent_type="general-purpose", team_name="council-20260217-auth", name="judge-1", prompt="...", description="Judge 1") -Task(subagent_type="general-purpose", team_name="council-20260217-auth", name="judge-2", prompt="...", description="Judge 2") - -# Codex CLI judges (parallel background shells) -Bash(command='codex exec -s read-only -m gpt-5.3-codex -C "$(pwd)" --output-schema skills/council/schemas/verdict.json -o .agents/council/codex-1.json "PACKET"', run_in_background=true) -Bash(command='codex exec -s read-only -m gpt-5.3-codex -C "$(pwd)" --output-schema skills/council/schemas/verdict.json -o .agents/council/codex-2.json "PACKET"', run_in_background=true) -``` - -All four spawn in the **same message** — maximum parallelism. - -**Mixed mode quorum:** At least 1 judge from each vendor should respond for cross-vendor consensus. If all judges from one vendor fail, proceed as single-vendor council and note the degradation in the report. - ---- - -## Key Rules - -1. **Pre-flight check:** `which codex` before attempting Codex CLI spawning -2. **Model availability:** `gpt-5.3-codex` requires API account — fall back to `gpt-4o` if unavailable -3. **Flag order matters** — agents copy examples exactly -4. **`codex review` is a different command** with different flags — do not conflate with `codex exec` -5. **No debate with Codex judges** — they produce one verdict, Codex CLI has no messaging diff --git a/skills-codex/research/references/backend-inline.md b/skills-codex/research/references/backend-inline.md deleted file mode 100644 index 0c91a1cf1..000000000 --- a/skills-codex/research/references/backend-inline.md +++ /dev/null @@ -1,69 +0,0 @@ -# Backend: Inline (No Spawn Available) - -Degraded single-agent mode when no multi-agent primitives are detected. The current agent performs all work sequentially in its own context. - -**When detected:** No `spawn_agent`, no `TeamCreate`, no `Task` tool available — or `--quick` flag was explicitly set. - ---- - -## Council: Single Inline Judge - -Instead of spawning parallel judges, the lead evaluates from each perspective sequentially: - -``` -1. Build the context packet (same as multi-agent mode) -2. For each perspective: - a. Adopt the perspective mentally - b. Write findings to .agents/council/YYYY-MM-DD-<target>-<perspective>.md -3. Synthesize into final report -``` - -Output format is identical — same file paths, same verdict schema. Downstream consumers (consolidation, report) don't know it was inline. - -**No debate available** — debate requires messaging between agents. - ---- - -## Swarm: Sequential Execution - -Instead of parallel workers, execute each task sequentially: - -``` -1. TaskList() — find unblocked tasks -2. For each unblocked task (in order): - a. Execute the task directly - b. Write result to .agents/swarm/results/<task-id>.json - c. TaskUpdate(taskId="<id>", status="completed") -3. Check for newly-unblocked tasks -4. Repeat until all tasks complete -``` - -Same result files, same validation — just sequential. - -**Error handling:** If a task fails mid-execution: -1. Write failure result to `.agents/swarm/results/<task-id>.json` with `"status": "blocked"` -2. Check if downstream tasks depend on it (`blockedBy`) -3. Skip blocked downstream tasks, mark as skipped -4. Continue with independent tasks that don't depend on the failed one - ---- - -## Research: Inline Exploration - -Instead of spawning an Explore agent, perform the tiered search directly: - -``` -1. Read docs/code-map/ if present -2. Grep/Glob for relevant files -3. Read key files -4. Write findings to .agents/research/YYYY-MM-DD-<topic>.md -``` - ---- - -## Key Rules - -1. **Same output format** — inline mode writes the same files as multi-agent mode -2. **Same validation** — all checks still apply -3. **Slower but functional** — no parallelism, but all skill capabilities preserved (except debate) -4. **Inform the user** — log "Running in inline mode (no multi-agent backend detected)" diff --git a/skills-codex/research/references/claude-code-latest-features.md b/skills-codex/research/references/claude-code-latest-features.md deleted file mode 100644 index a4196c4f5..000000000 --- a/skills-codex/research/references/claude-code-latest-features.md +++ /dev/null @@ -1,126 +0,0 @@ -# Claude Code Latest Features Contract - -This document is the shared source of truth for Claude Code feature usage across AgentOps skills. - -## Baseline - -- Target Claude Code release family: `2.1.x` -- Last verified against upstream changelog: `2.1.75` -- Changelog source: `https://raw.githubusercontent.com/anthropics/claude-code/main/CHANGELOG.md` - -## Current Feature Set We Rely On - -### 1. Core Slash Commands - -Skills and docs should assume these commands exist and prefer them over legacy naming: - -- `/agents` -- `/hooks` -- `/permissions` -- `/memory` -- `/mcp` -- `/output-style` -- `/effort` — set model effort level (low/medium/high). Opus 4.6 defaults to medium. -- `/color` — set prompt-bar color per session (useful for distinguishing parallel sessions) - -Reference: `https://code.claude.com/docs/en/slash-commands` - -### 2. Agent Definitions - -For custom teammates in `.claude/agents/*.md`, use modern frontmatter fields where applicable: - -- `model` -- `description` -- `tools` -- `memory` (scope control) -- `background: true` for long-running teammates -- `isolation: worktree` for safe parallel write isolation - -Reference: `https://code.claude.com/docs/en/sub-agents` - -### 3. Worktree Isolation - -When parallel workers may touch overlapping files, prefer Claude-native isolation features first: - -- Session-level isolation: `claude --worktree` (`-w`) -- Agent-level isolation: `isolation: worktree` -- Sparse checkout: `worktree.sparsePaths` setting — limit worktree to relevant directories in large monorepos - -If unavailable in a given runtime, fall back to manual `git worktree` orchestration. - -Reference: changelog `2.1.49`, `2.1.50`, and `2.1.75`. - -### 4. Hooks and Governance Events - -Hooks-based workflows should include modern event coverage: - -- `WorktreeCreate` -- `WorktreeRemove` -- `ConfigChange` -- `SubagentStop` -- `TaskCompleted` -- `TeammateIdle` -- `PostCompact` — fires after session context compaction. Use for auto-recovery (e.g., re-inject context). -- `InstructionsLoaded` — fires when CLAUDE.md loads. Use for policy enforcement. - -**HTTP hooks:** Hooks can POST JSON to a URL and receive JSON responses, in addition to shell script execution. - -Use these for auditability, policy enforcement, and cleanup. - -Reference: `https://code.claude.com/docs/en/hooks` - -### 5. Settings Hierarchy - -Skill guidance must respect settings precedence: - -1. Enterprise managed policy -2. Command-line args -3. Local project settings -4. Shared project settings -5. User settings - -Reference: `https://code.claude.com/docs/en/settings` - -### 6. Agent Inventory Command - -Use `claude agents` as the first CLI-level check to confirm configured teammate profiles before multi-agent runs. - -Reference: changelog `2.1.50`. - -### 7. Session Management - -- `--from-pr <url>` — start or resume a session linked to a specific GitHub PR -- `--worktree` (`-w`) — start session in an isolated git worktree - -Reference: `https://code.claude.com/docs/en/cli-reference` - -### 8. Tool Enhancements - -- **Read tool:** `pages` parameter for PDFs — read specific page ranges (e.g., `pages: "1-5"`). Large PDFs (>10 pages) require this parameter. -- **Bash tool:** Wildcard permission patterns — `Bash(npm *)` or `Bash(* install)` for flexible auto-approval. - -### 9. Effort Levels - -The `/effort` command controls model reasoning depth: - -- `low` — fast, shallow reasoning. Good for research/exploration agents. -- `medium` — balanced (Opus 4.6 default). -- `high` — deep reasoning. Good for implementation and complex debugging. - -Skill recommendation: set effort per agent role — low for judges/explorers, high for implementors. - -## Skill Authoring Rules - -1. Do not reference deprecated permission command names (`/allowed-tools`, `/approved-tools`). -2. Multi-agent skills (`council`, `swarm`, `research`, `crank`, `codex-team`) must explicitly point to this contract. -3. Prefer declarative agent isolation (`isolation: worktree`) over ad hoc branch/worktree shell choreography where runtime supports it. -4. Keep manual `git worktree` fallback documented for non-Claude runtimes. -5. For long-running explorers/judges/workers, document `background: true` as the default custom-agent policy. -6. Use `/effort` to right-size model reasoning per agent role when spawning multi-agent workflows. - -## Review Cadence - -- Re-verify this contract when: - - Claude Code changelog introduces new `2.1.x` or `2.2.x` entries - - any skill adds or changes multi-agent orchestration - - hook event support changes diff --git a/skills-codex/research/references/codebase-archaeology.md b/skills-codex/research/references/codebase-archaeology.md deleted file mode 100644 index ab1019add..000000000 --- a/skills-codex/research/references/codebase-archaeology.md +++ /dev/null @@ -1,256 +0,0 @@ - -<!-- TOC: Problem | THE EXACT PROMPT | Documentation First | Quick Start | The Layers | Agent-Assisted | Critical Searches | Output Template | Anti-Patterns | Checklist | References --> - -# Codebase Archaeology - -> **Core Insight:** Don't read randomly. Documentation first, then follow data flow from entry points outward. - -## The Problem - -You land in an unfamiliar codebase. Where do you start? Random file reading wastes context. You need a systematic approach that builds understanding efficiently and produces a reusable "mental model" of the architecture. - ---- - -## THE EXACT PROMPT - -### For Deep Investigation (Spawning Explore Agent) - -``` -Thoroughly explore this codebase. I need to understand: - -1. Overall architecture and module structure -2. How data flows through the system (input → processing → output) -3. Key data structures (the 3-5 types everything revolves around) -4. The integration points (external APIs, databases, file I/O) -5. Configuration system (env vars, config files, CLI flags) -6. Test infrastructure - -Focus on src/ directory structure and main modules. Map out how the pieces fit together. -Be very thorough - I need a complete mental model of how this codebase works. -``` - -### For Self-Directed Exploration - -``` -I want you to sort of randomly explore the code files in this project, choosing -code files to deeply investigate and trace their functionality through related -files. Build a comprehensive mental model of the architecture. -``` - ---- - -## Documentation First (Critical!) - -**Before touching code, ALWAYS read:** - -```bash -cat AGENTS.md # Project-specific rules and architecture notes -cat README.md # Purpose, installation, usage -``` - -**Why this matters:** -- AGENTS.md often contains architecture diagrams, key decisions, gotchas -- README.md reveals the project's purpose and main workflows -- Skipping this wastes time rediscovering documented knowledge - ---- - -## Quick Start - -```bash -# Phase 1: Orientation (2 min) -cat AGENTS.md README.md | head -200 # DOCUMENTATION FIRST! -ls -la src/ lib/ cmd/ pkg/ # Directory structure -cat Cargo.toml package.json pyproject.toml # Dependencies - -# Phase 2: Entry Points (5 min) -rg "fn main|async fn main" --type rust # Rust entry -rg "clap|structopt|argparse|commander" . # CLI frameworks -rg "Router|routes|@app\." . # HTTP routers - -# Phase 3: Core Types (5 min) -rg "^(pub )?struct |^class |^interface " --type rust --type ts --type py -rg "impl .* for" --type rust # Trait implementations - -# Phase 4: Data Flow (10 min) -# Trace from entry → handler → service → storage -``` - ---- - -## The Layers - -``` -┌─────────────────────────────────────┐ -│ ENTRY POINTS (start here) │ -│ main(), CLI commands, HTTP routes │ -└─────────────┬───────────────────────┘ - │ - ▼ -┌─────────────────────────────────────┐ -│ HANDLERS / CONTROLLERS │ -│ Request parsing, orchestration │ -└─────────────┬───────────────────────┘ - │ - ▼ -┌─────────────────────────────────────┐ -│ CORE DOMAIN │ -│ Business logic, key types │ -└─────────────┬───────────────────────┘ - │ - ▼ -┌─────────────────────────────────────┐ -│ STORAGE / INTEGRATION │ -│ Database, files, external APIs │ -└─────────────────────────────────────┘ -``` - ---- - -## Agent-Assisted Exploration - -For large codebases, spawn an Explore agent: - -``` -spawn_agent(agent_type="explorer") -Prompt: "Analyze the [project] codebase to provide a deep technical understanding. -Focus on: -1. Architecture Overview — how components interact -2. Key Data Structures — core types and their relationships -3. Data Flow — trace from ingestion to storage to output -4. Integration Points — external dependencies, APIs, databases" -``` - -**Why agents help:** -- They can read many files without filling your context -- They return a synthesized summary, not raw data -- You get architecture insights without the noise - ---- - -## Language-Specific Entry Points - -| Language | Entry Point | CLI Framework | HTTP Router | -|----------|-------------|---------------|-------------| -| Rust | `fn main()` in main.rs | clap, structopt | axum, actix | -| TypeScript | index.ts, main.ts | commander, yargs | express, fastify | -| Python | `__main__.py`, main.py | argparse, click, typer | flask, fastapi | -| Go | main.go in cmd/ | cobra, flag | chi, gin, echo | - ---- - -## Critical Searches - -```bash -# Find entry points -rg "fn main|def main|function main|export default" . - -# Find configuration -rg "env\.|process\.env|os\.environ|std::env" . -rg "config|settings|options" --type-add 'cfg:*.{toml,yaml,json}' -t cfg - -# Find key types (the 3-5 everything revolves around) -rg "^(pub )?(struct|class|interface|type) \w+" --type rust --type ts --type py - -# Find external integrations -rg "fetch\(|reqwest|aiohttp|requests\." . # HTTP clients -rg "query|execute|SELECT|INSERT" . # Database -rg "open\(|File::|fs\." . # File I/O - -# Find error handling (reveals edge cases) -rg "Error|Exception|panic|unwrap|expect" . -``` - ---- - -## Output Template - -After exploration, produce a **Comprehensive Technical Summary**: - -```markdown -## [Project Name] - Technical Architecture Summary - -### Executive Summary -**[Project]** is a [type] that [purpose]. It implements [key patterns]. - -**Key Statistics:** -- ~X lines of code across Y modules -- Language: [lang] [version] -- Key dependencies: [list] - ---- - -### Entry Points -- `src/main.rs:15` — CLI entry, parses args via clap -- `src/routes/mod.rs:1` — HTTP router (axum) - -### Key Types -| Type | Location | Purpose | -|------|----------|---------| -| `Project` | src/model.rs:10 | Core domain object | -| `Config` | src/config.rs:5 | Runtime configuration | -| `Storage` | src/storage.rs:1 | Persistence layer | - -### Data Flow -``` -CLI args → Config::load() → Project::process() → Storage::save() -``` - -### External Dependencies -- SQLite via rusqlite (persistence) -- reqwest (HTTP client) -- tokio (async runtime) - -### Configuration -| Source | Example | -|--------|---------| -| Env var | `CONFIG_PATH=/etc/tool.toml` | -| Config file | `~/.config/tool/config.toml` | -| CLI flag | `--verbose` | -``` - ---- - -## Anti-Patterns - -| Don't | Do | -|-------|-----| -| Skip AGENTS.md/README | Documentation first, always | -| Read files randomly | Follow entry point → data flow | -| Read entire files | Skim structure, dive into key functions | -| Ignore tests | Tests reveal intended behavior | -| Get lost in details | Build high-level map first | -| Fill context with raw code | Use Explore agent for synthesis | - ---- - -## When to Use What - -| Situation | Approach | -|-----------|----------| -| Brand new codebase | Full archaeology (all phases) | -| Adding a feature | Trace similar existing feature | -| Fixing a bug | Trace from symptom to root | -| Understanding one module | Start from module's public API | -| Large codebase (>10K LOC) | Spawn Explore agent first | - ---- - -## Checklist - -- [ ] **Read AGENTS.md/README.md** — Documentation first! -- [ ] **Orientation:** Directory structure, dependencies -- [ ] **Entry points:** main(), CLI commands, HTTP routes -- [ ] **Key types:** The 3-5 structs/classes everything uses -- [ ] **Data flow:** Entry → processing → storage -- [ ] **Config:** Env vars, config files, defaults -- [ ] **Integration:** External APIs, databases, file I/O -- [ ] **Tests:** What do tests reveal about intended behavior? -- [ ] **Produce summary:** Create reusable architecture doc - ---- - -## References - -| Need | File | -|------|------| diff --git a/skills-codex/research/references/context-discovery.md b/skills-codex/research/references/context-discovery.md deleted file mode 100644 index 4ba8adb7d..000000000 --- a/skills-codex/research/references/context-discovery.md +++ /dev/null @@ -1,190 +0,0 @@ -# Context Discovery Tiers - -**Purpose**: Systematic approach to finding code/context before implementing. - -**Rule**: Work top-to-bottom. Skip tiers if source unavailable. - ---- - -## Tier Order - -| Tier | Source | Tool/Command | When to Skip | -|------|--------|--------------|--------------| -| **1** | Code-Map | `Read docs/code-map/README.md` | No code-map in repo | -| **2** | Semantic Search | `mcp__smart-connections-work__lookup` | MCP not connected | -| **3** | Scoped Search | `Grep/Glob` with path limits | - | -| **4** | Source Code | `Read` files from Tier 1-3 signposts | - | -| **5** | Prior Knowledge | `ls .agents/research/` | Verify against source | -| **6** | External Docs | Context7, WebSearch | Last resort | - ---- - -## Tier Details - -### Tier 1: Code-Map (Fastest) - -```bash -Read docs/code-map/README.md # Find category -Read docs/code-map/{feature}.md # Get signposts -``` - -**Why first**: Local, instant, gives exact paths and function names. - -### Tier 2: Semantic Search - -```bash -mcp__smart-connections-work__lookup --query="$TOPIC" --limit=10 -``` - -**Why second**: Finds conceptual matches code-map might miss. Requires MCP. - -### Tier 3: Scoped Search - -```bash -Grep("pattern", path="services/auth/") # SCOPED -Glob("services/etl/**/*.py") # SCOPED -``` - -**Never**: `Grep("pattern")` or `Glob("**/*.py")` on large repos. - -### Tier 4: Source Code - -Read files identified by Tiers 1-3. Use function/class names, not line numbers. - -### Tier 5: Prior Knowledge - -```bash -ls .agents/research/ | grep -i "$TOPIC" -``` - -**Caution**: May be stale. Always verify findings against current source. - -### Tier 6: External - -- **Context7**: Library documentation -- **WebSearch**: External APIs, standards - ---- - -## Quick Reference - -``` -Code-Map → Semantic → Grep/Glob → Source → .agents/ → External - ↓ ↓ ↓ ↓ ↓ ↓ - paths meaning keywords code history docs -``` - ---- - -## Tier Weights (Flywheel-Optimized) - -Default weights based on typical value. Adjust based on `GET /memories/analytics/sources`: - -| Tier | Source Type | Default Weight | Notes | -|------|-------------|----------------|-------| -| 1 | `code-map` | 1.0 | Local, authoritative | -| 2 | `smart-connections` | 0.95 | High semantic match | -| 3 | `grep`, `glob` | 0.85 | Keyword precision | -| 4 | `read` | 0.80 | Direct source | -| 5 | `prior-research`, `memory-recall` | 0.70 | May be stale | -| 6 | `web-search`, `web-fetch` | 0.60 | External, verify | - -**Optimization loop**: -```bash -# Query source analytics -curl -H "X-API-Key: $KEY" "$ETL_URL/memories/analytics/sources?collection=default" - -# Response includes per-source value_score metrics: -# { -# "sources": [ -# {"source_type": "smart-connections", "value_score": 0.72}, -# {"source_type": "grep", "value_score": 0.61}, -# ... -# ], -# "recommendations": [...] -# } - -# Adjust weights based on value_score: -# value_score = (total_citations / memory_count) × avg_confidence × recency_factor -# -# - value_score > 0.5: Move source up in priority (increase weight) -# - value_score 0.3-0.5: Maintain current position -# - value_score < 0.3: Consider deprioritizing -# - value_score < 0.1 with high count: Review quality - many memories but rarely cited -``` - -**Tool to source_type mapping** (for session analyzer): -```python -WebSearch → "web-search" -WebFetch → "web-fetch" -mcp__smart-connections-work__lookup → "smart-connections" -mcp__smart-connections-personal__lookup → "smart-connections" -mcp__ai-platform__search_knowledge → "compile-knowledge" -mcp__ai-platform__memory_recall → "memory-recall" -Grep → "grep" -Glob → "glob" -Read → "read" -LSP → "lsp" -``` - ---- - -## Failure Pattern Prevention - -Each tier helps prevent specific failure patterns from the Vibe-Coding methodology: - -| Tier | Prevents Pattern | How | -|------|------------------|-----| -| 1 (Code-Map) | #9 Cargo Cult | Authoritative docs explain WHY patterns exist | -| 2 (Semantic) | #7 Zombie Resurrection | Finds prior art you might miss | -| 3 (Scoped Search) | #3 Context Amnesia | Scoping prevents context overload | -| 4 (Source Code) | #2 Confident Hallucination | Verify claims against actual code | -| 5 (Prior Knowledge) | #7 Zombie Resurrection | Don't re-solve solved problems | -| 6 (External) | #11 Security Theater | External standards for security | - -### The 40% Context Rule - -**Critical:** Never exceed 40% context utilization during discovery. - -| Zone | Percentage | Action | -|------|-----------|--------| -| GREEN | <35% | Continue exploration | -| YELLOW | 35-40% | Summarize, prepare to output | -| RED | >40% | STOP. Write findings. Reset. | - -**Why:** Above 40%, Pattern #3 (Context Amnesia) kicks in. Quality degrades exponentially. - -### Defensive Epistemology - -For each tier exploration, apply explicit reasoning: - -```text -DOING: [search/read action] -EXPECT: [what I expect to find] -IF WRONG: [what I'll conclude] -``` - -After: - -```text -RESULT: [what happened] -MATCHES: [yes/no] -THEREFORE: [conclusion] -``` - -This prevents Pattern #2 (Confident Hallucination) by forcing verification. - ---- - -## Anti-Patterns - -| DON'T | DO INSTEAD | Prevents Pattern | -|-------|------------|------------------| -| Start with Grep on full repo | Start with code-map | #3 Amnesia | -| Read source before knowing where | Find signposts first | #3 Amnesia | -| Trust .agents/ without verifying | Cross-check against source | #12 Doc Mirage | -| Web search for internal code | Use Tiers 1-4 | #9 Cargo Cult | -| Unscoped Glob/Grep | Always specify path | #3 Amnesia | -| "This API should work..." | Verify against actual docs | #2 Hallucination | -| "This code looks unused..." | Trace refs, check history | #6 Silent Deletion | -| Read entire large file | Targeted offset/limit | #3 Amnesia | diff --git a/skills-codex/research/references/data-flow-from-entry-points.md b/skills-codex/research/references/data-flow-from-entry-points.md deleted file mode 100644 index 8e9a6d4f9..000000000 --- a/skills-codex/research/references/data-flow-from-entry-points.md +++ /dev/null @@ -1,151 +0,0 @@ -# Data Flow From Entry Points - -> Trace requests, jobs, and commands from the surface they enter on through every handler, dependency, and external sink they touch. Linear paths beat speculative breadth-first reads. - -## Why Trace From Entry Points - -Most architectural questions reduce to "what happens when X arrives?" — an HTTP request, a CLI invocation, a queue message, a scheduled tick. Tracing one of these end-to-end produces: - -- An accurate list of files actually involved (vs. files merely related by name). -- The real layering — handler vs. service vs. storage — instead of the layering the docs claim. -- The contract boundaries: what the handler validates, what the service trusts, where errors are caught vs. propagated. -- A reusable diagram other agents can verify by re-running the same trace. - ---- - -## The Four Entry Surfaces - -| Surface | Where to look | Common library signals | -|---------|---------------|------------------------| -| CLI | `cmd/`, `bin/`, `src/main.*`, top-level entry files | clap, cobra, click, typer, commander, yargs, argparse | -| HTTP | `routes/`, `api/`, `handlers/`, `controllers/` | axum, actix, fastapi, express, fastify, gin, chi, flask | -| Queue / event | `consumers/`, `workers/`, `subscribers/`, `events/` | bull, sidekiq, celery, kafka clients, rabbitmq clients | -| Scheduler | `jobs/`, `cron/`, `schedules/` | cron strings, `@scheduled` decorators, systemd timers | - -A codebase usually has 1–3 of these. Find them all before tracing — you may need to trace one of each surface to understand the full shape. - ---- - -## Trace Procedure - -For one chosen entry point: - -### Step 1: Locate the dispatcher - -Find the registration call (`router.add(...)`, `app.command(...)`, `consumer.subscribe(...)`). Record the `file:line` and the handler symbol it routes to. - -### Step 2: Read the handler - -Open the handler. Note, in order: - -- Inputs and how they are validated. -- Direct dependencies the handler instantiates or receives (DI parameters, module-level singletons). -- External calls (DB, HTTP, filesystem, queue publish) made directly inside the handler. -- Errors caught vs. propagated. - -### Step 3: Walk the dependency tree one level deep - -For each direct dependency, decide: - -- **Self-describing name?** (`UserRepository`, `EmailClient`) — note its purpose without reading. -- **Ambiguous name?** Open it just long enough to write a one-line description. -- **Touches an external sink?** Always open it — the sink is part of the trace. - -Stop at the second level unless a third level is obviously the place where the work actually happens. - -### Step 4: Find the sinks - -Every trace ends at a sink. Common sinks: - -| Sink type | Signals to grep | -|-----------|-----------------| -| Database | `query`, `execute`, `INSERT`, `UPDATE`, `db.`, ORM session calls | -| HTTP egress | `fetch`, `reqwest`, `requests.`, `http.Client`, SDK constructors | -| Filesystem | `open`, `File::`, `fs.`, `pathlib`, write/read functions | -| Queue publish | `publish`, `produce`, `send_message`, `enqueue` | -| Stdout / logs | `print`, `println`, structured logger calls when output is the product | - -Write the sink down. It is the trace's terminal node. - -### Step 5: Note error and retry behavior - -Where in the trace are errors caught? Where do they propagate? Are retries or circuit breakers visible? This is where surprise behavior lives. - ---- - -## Output Shape - -A trace artifact is short and linear: - -```markdown -## Trace: POST /api/jobs - -Entry: `src/api/jobs.rs:42` → `create_job` handler - -create_job (src/api/jobs.rs:42) - ↓ validates JobRequest (src/api/jobs.rs:55) - ↓ JobService::submit (src/services/job.rs:18) - ↓ calls JobRepository::insert (src/storage/jobs.rs:30) — sink: SQLite - ↓ calls Queue::publish (src/queue/mod.rs:22) — sink: Redis stream - ↓ returns 202 with job id - -Errors: -- Validation failure → 400 at handler boundary -- Storage failure → bubbles, logged in middleware (src/middleware/log.rs:12), returns 500 -- Queue failure → swallowed at JobService::submit:24 — KNOWN GAP, see issue #... -``` - -One trace, one page. Multiple traces produce multiple short artifacts rather than one sprawling document. - ---- - -## Searches That Help - -Use these scoped searches as starting points. Always pass a directory; never grep the whole repo unscoped. - -```bash -# Entry-point registration -rg -n "Router::|router\.|@app\.|app\.(get|post)|Cmd\(\"|@click\.command|cobra\.Command" src/ - -# Handler signatures -rg -n "fn (handle|create|update|get|list|delete)_" src/api/ src/handlers/ - -# DB sinks -rg -n "query!?\(|execute!?\(|\.query\(|\.exec\(|SELECT |INSERT |UPDATE " src/ - -# HTTP egress -rg -n "reqwest::|requests\.|fetch\(|http\.Client" src/ - -# Queue publish -rg -n "publish\(|produce\(|enqueue\(|send_message" src/ -``` - -Pair these with the iterative-retrieval pattern (`skills/research/references/iterative-retrieval.md`) when the first scoped search misses. - ---- - -## Anti-Patterns - -| Avoid | Do instead | -|-------|------------| -| Tracing five flows shallowly | Trace one flow end-to-end first | -| Reading every file the handler imports | Use names; only open ambiguous or sink-touching deps | -| Ignoring error paths | Note where errors are caught and where they propagate | -| Calling the trace done at the service layer | Walk to the sink — DB, HTTP egress, filesystem, queue | -| Grepping the whole repo | Always scope to a directory | -| Letting the trace branch into a tree | Pick one path; record alternates as siblings, not children | - ---- - -## When to Use This Reference - -- You are answering "what happens when <event> arrives?" -- You need a short artifact that another agent can verify or extend. -- You are about to modify a handler and need to understand its blast radius. -- The architecture docs disagree with the code, and you need the ground-truth path. - -For broad onboarding, pair this with `skills/research/references/onboarding-methodology.md`. For prior-work search, see `skills/research/references/iterative-retrieval.md`. - ---- - -> Pattern adopted from `codebase-archaeology` (ACFS skill corpus). Methodology only — no verbatim text. diff --git a/skills-codex/research/references/deep-research-mcp.md b/skills-codex/research/references/deep-research-mcp.md deleted file mode 100644 index 896cd1987..000000000 --- a/skills-codex/research/references/deep-research-mcp.md +++ /dev/null @@ -1,134 +0,0 @@ -# Deep Research with MCP Integration - -> Multi-source research using MCP servers (firecrawl, exa, context7) for comprehensive exploration beyond basic web search. - -## When to Use - -- Topic requires authoritative external sources (not just codebase exploration) -- Research question spans multiple domains or requires current data -- Basic `WebSearch` returns insufficient depth -- API documentation or technical specifications needed - -## Research Pipeline - -### Step 1: Decompose Topic into Sub-Questions - -Break the research topic into 3-5 focused sub-questions: - -``` -Topic: "Impact of streaming APIs on agent architectures" -Sub-questions: - 1. What streaming API patterns exist today? (SSE, WebSocket, gRPC streams) - 2. How do major agent frameworks handle streaming? (LangChain, CrewAI, AutoGen) - 3. What are latency/throughput tradeoffs for streaming vs batch? - 4. What production deployment patterns exist for streaming agents? - 5. What's the state of streaming in Claude/OpenAI APIs? -``` - -### Step 2: Multi-Source Search (Per Sub-Question) - -For each sub-question, search across available MCP sources: - -``` -# Primary: Structured web search (if firecrawl MCP connected) -mcp__firecrawl__search(query: "<sub-question keywords>", limit: 8) - -# Secondary: Semantic web search (if exa MCP connected) -mcp__exa__web_search(query: "<sub-question keywords>", numResults: 8) -mcp__exa__web_search_advanced(query: "<keywords>", numResults: 5, startPublishedDate: "2025-01-01") - -# Tertiary: Documentation lookup (if context7 MCP connected) -mcp__context7__resolve_library_id(libraryName: "<library>") -mcp__context7__get_library_docs(context7CompatibleLibraryID: "<id>") - -# Fallback: Standard web search (always available) -WebSearch(query: "<sub-question>") -``` - -**Search Strategy:** -- Use 2-3 keyword variations per sub-question -- Mix general queries with news-focused queries -- Aim for 15-30 unique sources total across all sub-questions -- Prioritize: official docs > academic > reputable news > blogs > forums - -### Step 3: Deep-Read Key Sources (3-5 URLs) - -For the most promising results, fetch full content: - -``` -# Full page scrape (if firecrawl connected) -mcp__firecrawl__scrape(url: "<url>") - -# Semantic content extraction (if exa connected) -mcp__exa__crawling(url: "<url>", tokensNum: 5000) - -# Fallback -WebFetch(url: "<url>") -``` - -### Step 4: Parallel Agent Research (Optional) - -For broad topics, spawn parallel research agents: - -``` -Agent 1: Sub-questions 1-2 (technical patterns) -Agent 2: Sub-questions 3-4 (production deployment) -Agent 3: Sub-question 5 (API state-of-art) -``` - -Main session synthesizes all agent findings into unified report. - -### Step 5: Synthesize Report - -```markdown -# Research: <Topic> - -**Sources:** <N> | **Confidence:** High/Medium/Low | **Date:** <YYYY-MM-DD> - -## Executive Summary -<3-5 sentences> - -## 1. <Theme from Sub-Question 1> -<Findings with inline citations> -- Key point (Source Name, with URL citation) - -## Key Takeaways -- <Actionable insight 1> -- <Actionable insight 2> - -## Knowledge Gaps -- <What we couldn't find> -- <What needs verification> - -## Sources -1. Source Title — one-line summary (with URL) -``` - -## Quality Rules - -1. **Every claim needs a source** — no unsourced assertions -2. **Cross-reference:** If only one source says it, flag as unverified -3. **Prefer recent sources** (last 12 months) for fast-moving topics -4. **Acknowledge gaps explicitly** — "insufficient data found" > hallucination -5. **Separate fact from inference** — label estimates, projections, opinions -6. **Check MCP availability first** — gracefully degrade to WebSearch if MCPs not connected - -## MCP Detection - -Before attempting MCP-based search, check availability: - -``` -# Check which MCPs are available in the current session -# If firecrawl: use firecrawl_search + firecrawl_scrape -# If exa: use web_search_exa + crawling_exa -# If context7: use for library documentation -# If none: fall back to WebSearch + WebFetch -``` - -Log which sources were used for traceability in the report's Methodology section. - -## Integration with /research Skill - -This reference extends the research skill's Step 3 (Launch Explore Agent) with MCP-first search patterns. When the explore agent's Tier 6 (External Docs) triggers, use this pipeline instead of basic WebSearch. - -The iterative retrieval pattern (`references/iterative-retrieval.md`) applies here too: score MCP results for relevance, extract new search terms, and refine across cycles. diff --git a/skills-codex/research/references/document-template.md b/skills-codex/research/references/document-template.md deleted file mode 100644 index b52c66cbf..000000000 --- a/skills-codex/research/references/document-template.md +++ /dev/null @@ -1,191 +0,0 @@ -# Research Document Template - -## Filename Format - -`.agents/research/YYYY-MM-DD-{topic-slug}.md` - -Convert topic to kebab-case slug: -- "authentication flow" -> `2026-01-03-authentication-flow.md` -- "MCP server architecture" -> `2026-01-03-mcp-server-architecture.md` - ---- - -## Required Sections - -### 1. Frontmatter - -```yaml ---- -date: YYYY-MM-DD -type: Research -topic: "Topic Name" -tags: [research, domain, tech] -status: COMPLETE -supersedes: [] ---- -``` - -### 2. Executive Summary - -2-3 sentences: what found, what recommend. - -### 3. Current State - -- What exists today -- Key files table: | File | Purpose | -- Existing patterns - -### 4. Findings - -Each finding with: -- Evidence: `file:line` -- Implications - -### 5. Constraints - -| Constraint | Impact | Mitigation | -|------------|--------|------------| - -### 6. Risks - -| Risk | Likelihood | Impact | Mitigation | -|------|------------|--------|------------| - -### 7. Recommendation - -- Recommended approach -- Rationale -- Alternatives considered and rejected - -### 8. Discovery Provenance - -Track which sources provided key insights (enables flywheel optimization). - -**Purpose**: Create an audit trail showing which discovery method found each insight. This enables post-hoc analysis: "Which sources led to successful implementation?" - -**When to complete**: As you research, add one row per significant finding showing its source. - -**Example**: -```markdown -| Finding | Source Type | Source Detail | Confidence | -|---------|-------------|---------------|------------| -| Gateway request flow | code-map | docs/code-map/gateway.md | 1.0 | -| Middleware pattern | smart-connections | "request middleware chain" | 0.95 | -| Error handling at L45 | grep | services/gateway/middleware.py | 1.0 | -| Rate limiting precedent | prior-research | 2026-01-10-ratelimit.md | 0.85 | -| OAuth2 RFC | web-search | "RFC 6749 OAuth 2.0" | 0.80 | -``` - -**Source Types by Tier** (higher tier = better quality): - -**Tier 1 (Authoritative)** -- `code-map` - Structured architecture documentation (highest confidence) - -**Tier 2 (Semantic)** -- `smart-connections` - Obsidian semantic search -- `compile-knowledge` - MCP ai-platform search - -**Tier 3 (Scoped Search)** -- `grep` - Pattern matching in code -- `glob` - File pattern matching - -**Tier 4 (Source Code)** -- `read` - Direct file reading -- `lsp` - Language Server Protocol queries - -**Tier 5 (Prior Art)** -- `prior-research` - Previous research documents -- `prior-retro` - Retrospective learnings -- `prior-pattern` - Reusable patterns -- `memory-recall` - Semantic memory search - -**Tier 6 (External)** -- `web-search` - Web search results -- `web-fetch` - Direct URL fetch - -**Other** -- `conversation` - User-provided context - -**Confidence scoring**: -- `1.0` - Source is authoritative/written down -- `0.95` - Semantic match, high relevance -- `0.85` - Good match, may need verification -- `0.70` - Reasonable match, verify -- < 0.70 - Use sparingly, needs verification - -### 9. Failure Pattern Risks - -Identify which of the 12 failure patterns are risks for this work. This proactive assessment helps downstream implementation avoid known pitfalls. - -**Required table:** -```markdown -## Failure Pattern Risks - -| Pattern | Risk Level | Mitigation | -|---------|------------|------------| -| #N Pattern Name | HIGH/MEDIUM/LOW | Specific mitigation strategy | -``` - -**Pattern quick reference:** - -| # | Pattern | Common Research Triggers | -|---|---------|-------------------------| -| 1 | Fix Spiral | Complex debugging, unclear root cause | -| 2 | Confident Hallucination | External APIs, unfamiliar libraries | -| 3 | Context Amnesia | Large codebase, many files to read | -| 4 | Tests Passing Lie | Weak test coverage, mocked dependencies | -| 5 | Eldritch Horror | Complex existing code, deep nesting | -| 6 | Silent Deletion | "Unused" code, cleanup opportunities | -| 7 | Zombie Resurrection | Prior failed attempts, known bugs | -| 8 | Gold Plating | Feature creep opportunities | -| 9 | Cargo Cult | New patterns, external examples | -| 10 | Premature Abstraction | Generic solutions proposed | -| 11 | Security Theater | Auth, crypto, access control | -| 12 | Documentation Mirage | Outdated docs, missing comments | - -**Example:** -```markdown -## Failure Pattern Risks - -| Pattern | Risk Level | Mitigation | -|---------|------------|------------| -| #2 Confident Hallucination | HIGH | External OAuth API - verify all claims against official docs | -| #5 Eldritch Horror | MEDIUM | Auth middleware is 400+ lines - document boundaries before changes | -| #9 Cargo Cult | MEDIUM | Using external OAuth example - understand why each step exists | -| #11 Security Theater | HIGH | Auth changes - use established patterns, get security review | -``` - -### 10. Next Steps - -Point to `/plan` for implementation. - ---- - -## Tag Vocabulary - -**Rules:** 3-5 tags total. First tag MUST be `research`. - -| Category | Valid Tags | -|----------|------------| -| **Core Domains** | `agents`, `data`, `api`, `infra`, `security`, `auth` | -| **Quality** | `testing`, `reliability`, `performance`, `monitoring` | -| **Process** | `ci-cd`, `workflow`, `ops`, `docs` | -| **Governance** | `architecture`, `compliance`, `standards`, `ui` | -| **Languages** | `python`, `shell`, `typescript`, `go`, `yaml` | -| **Platforms** | `helm`, `kubernetes`, `openshift`, `docker`, `argocd` | -| **AI Stack** | `mcp`, `litellm`, `neo4j`, `postgres`, `redis`, `fastapi` | - -**Examples:** -- `[research, agents, mcp]` - MCP server research -- `[research, data, neo4j]` - Data storage research -- `[research, security, auth]` - Authentication research - ---- - -## Status Values - -| Status | Meaning | -|--------|---------| -| `COMPLETE` | Ready for planning | -| `IN_PROGRESS` | Ongoing research | -| `SUPERSEDED` | Newer research exists | diff --git a/skills-codex/research/references/failure-patterns.md b/skills-codex/research/references/failure-patterns.md deleted file mode 100644 index 0bd1d509a..000000000 --- a/skills-codex/research/references/failure-patterns.md +++ /dev/null @@ -1,321 +0,0 @@ -# The 12 Failure Patterns (Research Reference) - -> Based on the Vibe-Coding methodology. Load this when you need full pattern details for risk assessment. - ---- - -## Quick Reference - -| # | Pattern | Key Symptom | First Action | -|---|---------|-------------|--------------| -| 1 | Fix Spiral | >3 attempts, circles | STOP, revert | -| 2 | Confident Hallucination | Non-existent APIs | Verify docs | -| 3 | Context Amnesia | Forgotten constraints | Save state | -| 4 | Tests Passing Lie | Green but broken | Manual test | -| 5 | Eldritch Horror | >200 line functions | Extract/refactor | -| 6 | Silent Deletion | Missing code | Check git history | -| 7 | Zombie Resurrection | Bugs return | Add regression test | -| 8 | Gold Plating | Unrequested features | Revert extras | -| 9 | Cargo Cult | Copied patterns | Understand why | -| 10 | Premature Abstraction | Generic w/ one use | Inline | -| 11 | Security Theater | Bypassable security | Audit | -| 12 | Documentation Mirage | Docs don't work | Test docs | - ---- - -## Inner Loop Patterns (Seconds-Minutes) - -### 1. The Fix Spiral - -**Description:** Making a fix that breaks something else, then fixing that break which causes another issue, creating a cascading chain without resolution. - -**Symptoms:** -- More than 3 fix attempts without convergence -- Changes oscillating between two states -- "This should work" appearing in explanations -- Error messages changing but not disappearing - -**Research Defense:** -- Research root cause BEFORE attempting fix -- Document expected behavior vs actual behavior -- Identify all code paths affected - -**Prevention:** -- Set hard limit: 3 attempts then STOP -- State explicit prediction before each fix -- Checkpoint working state before each attempt - ---- - -### 2. The Confident Hallucination - -**Description:** Generating plausible-sounding but factually incorrect information about APIs, libraries, or behavior. - -**Symptoms:** -- Code references non-existent methods or parameters -- API usage that "looks right" but fails at runtime -- Overly specific technical claims without evidence -- Version-specific features applied to wrong versions - -**Research Defense:** -- VERIFY all API claims against actual documentation -- Note confidence levels in provenance table -- Use Tier 6 (external docs) for unfamiliar APIs - -**Prevention:** -- Test code in isolation before integration -- Use "I don't know" as valid response -- Run type checkers and linters early - ---- - -### 3. The Context Amnesia - -**Description:** As context window fills, losing track of earlier constraints, requirements, or decisions. - -**Symptoms:** -- Reintroducing previously fixed bugs -- Contradicting earlier decisions -- Forgetting project-specific conventions -- Repeating completed work - -**Research Defense:** -- Stay <40% context utilization -- Write findings to files immediately -- Use targeted reads (offset/limit) not full files - -**Prevention:** -- Save progress frequently -- Start fresh sessions for distinct work -- Front-load critical constraints - ---- - -### 4. The Tests Passing Lie - -**Description:** Tests pass but code doesn't actually work - too narrow, wrong thing, mocks away behavior. - -**Symptoms:** -- Green test suite but broken functionality -- Tests that test mocks instead of real behavior -- Coverage looks good but edge cases fail -- Tests modified in same PR as code they test - -**Research Defense:** -- Find actual test coverage in research -- Identify what tests actually verify -- Note mocked vs real dependencies - -**Prevention:** -- Run tests yourself; don't trust reported results -- Separate test changes from code changes -- Manual smoke test after suite passes - ---- - -## Middle Loop Patterns (Hours-Days) - -### 5. The Eldritch Horror - -**Description:** Code becomes incomprehensible - functions spanning hundreds of lines, deeply nested logic, unclear naming. - -**Symptoms:** -- Functions exceeding 200 lines -- Nesting depth beyond 4 levels -- Variable names like `temp2`, `data3` -- Comments that don't match behavior - -**Research Defense:** -- Document complexity limits in findings -- Note current complexity metrics -- Identify refactoring boundaries - -**Prevention:** -- Enforce hard limits: <200 lines per function -- Require meaningful names -- Use explicit interfaces - ---- - -### 6. The Silent Deletion - -**Description:** Removing code that appears unused but is actually necessary for edge cases, legacy support, or fallbacks. - -**Symptoms:** -- "Cleanup" commits that remove "dead code" -- Features that worked yesterday now fail -- Error handling mysteriously missing -- Comments about "why" deleted along with code - -**Research Defense:** -- Research WHY code exists before removal -- Check git history for context -- Trace all references including dynamic calls - -**Prevention:** -- Never delete without understanding purpose -- Get human approval for deletion -- Keep deleted code in comments initially - ---- - -### 7. The Zombie Resurrection - -**Description:** Previously fixed bugs return because similar code regenerated without fix, or reverts during refactoring. - -**Symptoms:** -- Bug reports for issues marked "fixed" -- Same error in different code paths -- Fixes lost during refactoring -- "I thought we fixed this" conversations - -**Research Defense:** -- Prior art search prevents re-solving -- Check for existing regression tests -- Document root cause, not just fix - -**Prevention:** -- Add regression tests for every fix -- Use automated checks for anti-patterns -- Keep lessons learned file - ---- - -### 8. The Gold Plating - -**Description:** Adding unrequested features, extra error handling, additional configurability beyond what was asked. - -**Symptoms:** -- PR larger than expected -- New config options no one asked for -- "While I was here, I also..." explanations -- Abstraction layers for single use cases - -**Research Defense:** -- Define explicit scope in research -- Note ONLY what's needed for the task -- Separate "nice to have" from "required" - -**Prevention:** -- Define explicit scope before starting -- Reject changes outside stated scope -- Prefer boring, obvious solutions - ---- - -## Outer Loop Patterns (Days-Weeks) - -### 9. The Cargo Cult - -**Description:** Copying patterns from examples without understanding why they work. May be inappropriate for context. - -**Symptoms:** -- Copy-pasted code with irrelevant portions -- Patterns from different frameworks mixed -- "Best practices" where they don't fit -- Configuration copied without understanding - -**Research Defense:** -- Understand WHY patterns exist -- Ask "why does this pattern exist?" for each -- Verify example matches your context - -**Prevention:** -- Test copied code in isolation first -- Adapt patterns to local conventions -- Trace examples to their source - ---- - -### 10. The Premature Abstraction - -**Description:** Creating generic abstractions before concrete use cases exist. Abstractions don't match actual needs. - -**Symptoms:** -- Generic interfaces with one implementation -- Factory patterns for single classes -- Configuration for cases that don't exist -- "Future-proofing" never used - -**Research Defense:** -- Document concrete use cases first -- Require 3+ concrete cases before abstracting -- Note where duplication exists vs speculation - -**Prevention:** -- Write concrete implementations first -- Prefer duplication over wrong abstraction -- Extract only when duplication appears - ---- - -### 11. The Security Theater - -**Description:** Code appears secure but isn't - validation that misses edge cases, encryption with hardcoded keys. - -**Symptoms:** -- Security measures easily circumvented -- Validation on client but not server -- Hardcoded credentials or keys -- "Security by obscurity" approaches - -**Research Defense:** -- Include security constraints in research -- Reference external security standards -- Note auth/crypto/access control patterns - -**Prevention:** -- Use established security libraries -- Security review by qualified humans -- Static analysis for vulnerabilities - ---- - -### 12. The Documentation Mirage - -**Description:** Documentation exists but doesn't match reality - outdated READMEs, incorrect API docs. - -**Symptoms:** -- Following docs leads to errors -- Comments contradict adjacent code -- Examples that don't compile -- Setup instructions that don't work - -**Research Defense:** -- Verify docs match reality -- Test documentation by following it literally -- Note discrepancies in research findings - -**Prevention:** -- Treat docs as code: test them -- Update docs in same PR as code -- Use executable documentation - ---- - -## Pattern Frequency Tracking - -Use this in research outputs to track which patterns are relevant: - -```markdown -## Failure Pattern Risks - -| Pattern | Risk Level | Mitigation | -|---------|------------|------------| -| #2 Confident Hallucination | HIGH | Verify external API claims | -| #5 Eldritch Horror | MEDIUM | Keep functions <200 lines | -| #9 Cargo Cult | MEDIUM | Understand why patterns exist | -``` - -Risk Levels: -- **HIGH**: Strong indicators in research, requires explicit mitigation -- **MEDIUM**: Some indicators, requires awareness -- **LOW**: Minor indicators, standard practices sufficient - ---- - -## See Also - -- `~/.claude/CLAUDE-base.md` - Core Vibe-Coding methodology -- `~/.claude/plugins/marketplaces/agentops-marketplace/reference/failure-patterns.md` - Full pattern reference -- `~/.claude/skills/crank/failure-taxonomy.md` - Execution failure taxonomy diff --git a/skills-codex/research/references/iterative-retrieval.md b/skills-codex/research/references/iterative-retrieval.md deleted file mode 100644 index 3952e183f..000000000 --- a/skills-codex/research/references/iterative-retrieval.md +++ /dev/null @@ -1,110 +0,0 @@ -# Iterative Retrieval Pattern - -> Progressive context refinement for subagents. Solves "I don't know what I need to know." - -## Problem - -When spawning research or explore agents, the initial query often misses critical context because: -- The agent doesn't know the codebase's naming conventions -- Related features use unexpected terminology -- Key context lives in files the agent wouldn't think to search - -Flat keyword search returns either too much noise or misses relevant files. - -## Solution: 4-Phase Iterative Loop - -### Phase 1: DISPATCH — Broad keyword search -``` -Search for: <topic> -Use 3-5 keyword variants: - - Exact term: "<topic>" - - Synonyms: "<synonym1>", "<synonym2>" - - Implementation terms: "<likely-function-name>", "<likely-file-pattern>" -``` - -### Phase 2: EVALUATE — Score relevance (0-1) -For each result, assign a relevance score: - -| Score | Meaning | Action | -|-------|---------|--------| -| 0.8-1.0 | Directly implements target feature | Read fully, extract details | -| 0.5-0.7 | Contains related patterns or interfaces | Skim for cross-references | -| 0.2-0.4 | Tangentially related | Note for later if gaps remain | -| 0.0-0.2 | Not relevant | Discard | - -### Phase 3: REFINE — Extract new keywords -From high-relevance files (0.5+), extract: -- Function/class names referenced but not yet searched -- Import paths pointing to unexplored modules -- Config keys or env vars mentioned -- Error messages or log strings (grep targets) - -Add these as new search terms. - -### Phase 4: LOOP — Repeat max 3 cycles -``` -Cycle 1: Broad search → find core files → extract new terms -Cycle 2: Targeted search with extracted terms → find related files → more terms -Cycle 3: Fill remaining gaps → verify completeness -``` - -**Stop early if:** -- No new high-relevance results in a cycle -- All critical questions answered -- Context budget reached - -## Integration with /research - -In Step 3 (Launch Explore Agent), add iterative retrieval to the exploration prompt: - -``` -Use iterative retrieval: -1. Start with broad keyword search for "<topic>" -2. Score each result 0-1 for relevance -3. From files scoring 0.5+, extract new search terms -4. Search with new terms (max 3 cycles) -5. Report: files found per cycle, relevance scores, final coverage -``` - -## Integration with /swarm - -When spawning parallel workers that need codebase context: - -``` -Before implementation, run 1-2 retrieval cycles to gather context: -- Search for files related to your task -- Read the highest-relevance files (0.7+) -- Note patterns and conventions from those files -- Then implement following those patterns -``` - -This prevents workers from reinventing patterns that already exist in the codebase. - -## Example: Researching "authentication" - -**Cycle 1:** -- Search: "auth", "authentication", "login", "session" -- Hits: `auth/middleware.go` (0.9), `auth/token.go` (0.8), `config/auth.go` (0.6), `README.md` (0.2) -- New terms from hits: `ValidateToken`, `SessionStore`, `JWT_SECRET` - -**Cycle 2:** -- Search: "ValidateToken", "SessionStore", "JWT_SECRET" -- Hits: `store/session.go` (0.9), `config/env.go` (0.7), `test/auth_test.go` (0.8) -- New terms: `RefreshToken`, `store.NewRedisStore` - -**Cycle 3:** -- Search: "RefreshToken", "RedisStore" -- Hits: `auth/refresh.go` (0.9), `store/redis.go` (0.8) -- No new high-relevance terms → STOP - -**Result:** Complete auth system map in 3 cycles vs flat search that would miss `store/` and `config/env.go`. - -## Anti-Patterns - -| Anti-Pattern | Why It Fails | Fix | -|-------------|-------------|-----| -| Searching entire repo with no scope | Context overload, slow | Always scope to directories | -| Only 1 keyword | Misses synonym usage | Start with 3-5 variants | -| No relevance scoring | Reads everything equally | Score and prioritize | -| >3 cycles | Diminishing returns | Stop at 3, report gaps | -| Ignoring low-relevance files | Sometimes tangential files have key context | Note them, revisit if gaps remain | diff --git a/skills-codex/research/references/onboarding-methodology.md b/skills-codex/research/references/onboarding-methodology.md deleted file mode 100644 index 6ecf3b51b..000000000 --- a/skills-codex/research/references/onboarding-methodology.md +++ /dev/null @@ -1,194 +0,0 @@ -# Onboarding Methodology - -> Build a working mental model of an unfamiliar codebase fast. Read the docs first, locate entry points, then trace one representative path to its sink — never random file reads. - -## Problem - -Landing in a new codebase, the temptation is to grep for keywords or open files at random. That burns context without producing structure. Onboarding research needs a repeatable shape: orient on docs, locate entry points, identify the 3–5 types everything revolves around, then trace one representative flow end-to-end. The output should be reusable: another agent (or future you) reads the summary and skips the cold-start cost. - ---- - -## Phased Walk - -| Phase | Goal | Time box | Output | -|-------|------|----------|--------| -| 1. Orient on docs | Pull what is already written down | 2 min | Notes on stated purpose, conventions, gotchas | -| 2. Inventory the surface | Directory layout, dependencies, build system | 3 min | Annotated tree of top-level dirs | -| 3. Locate entry points | `main`, CLI commands, HTTP routes, queue consumers | 5 min | List of `file:line` for each entry surface | -| 4. Identify core types | The 3–5 structs/classes everything else references | 5 min | Type table with location and purpose | -| 5. Trace one flow | Pick the most representative entry → output path | 10 min | Linear data-flow diagram | -| 6. Note integrations | DBs, external APIs, file I/O, queues | 3 min | Dependency table | -| 7. Skim tests | What invariants does the test suite assert? | 2 min | List of behavioral guarantees found | -| 8. Write the summary | Reusable mental-model artifact | 5 min | Document under `.agents/research/` | - -If a phase has no signal in 90 seconds, skip and note the gap. - ---- - -## Phase 1: Documentation First - -Read in this order before opening source: - -```bash -cat AGENTS.md # Project rules, architecture decisions, gotchas -cat CLAUDE.md # Same — most repos symlink one to the other -cat README.md # Stated purpose, install, primary workflows -ls docs/ && cat docs/index.md docs/architecture.md 2>/dev/null -``` - -Capture three things from this pass: -1. The project's stated purpose in one sentence. -2. The top 3 conventions or rules the docs call out. -3. Any explicit "do not touch" or "load-bearing" warnings. - -Skipping this phase is the most common onboarding failure — it makes you rediscover documented constraints by trial and error. - ---- - -## Phase 2: Inventory the Surface - -```bash -ls -la # Top-level shape -ls -la src/ lib/ cmd/ pkg/ # Source roots -cat Cargo.toml package.json pyproject.toml go.mod 2>/dev/null -``` - -Annotate each top-level directory with a one-line guess at its role. Confirm the guesses in later phases. - ---- - -## Phase 3: Entry Points - -Use language-aware searches — see `skills/research/references/context-discovery.md` for tier ordering. Patterns to look for: - -| Surface | Signals | -|---------|---------| -| Process entry | `fn main`, `def main`, `func main`, `if __name__ == "__main__"` | -| CLI surface | clap/cobra/click/typer/commander/yargs derivations, command registration calls | -| HTTP surface | route registration calls, decorator usage, router builders | -| Queue/event surface | consumer/handler/subscriber registration | -| Scheduler surface | cron/timer/job declarations | - -Record each as `file:line` — these become navigation anchors in the summary. - ---- - -## Phase 4: Core Types - -Look for the 3–5 types everything else flows through. Signals: - -- Mentioned in most files when grepped by name. -- Returned or consumed by multiple entry-point handlers. -- Declared in a `model.rs`, `types.ts`, `schema.py`, or equivalent root. - -Capture each in a table: name, location, purpose, key fields. If you cannot describe the purpose in one sentence, the type is not yet understood — flag it as a gap. - ---- - -## Phase 5: Trace One Flow - -Pick the most representative entry-point handler. Walk it: - -1. Read the handler. Note every function it calls. -2. For each callee, decide: do I need to open it, or is the name self-describing? -3. Stop when you hit storage, an external API, or a return that closes the loop. -4. Write the path as a linear arrow chain. - -One traced flow is more useful than five half-traced flows. - ---- - -## Phase 6 & 7: Integrations and Tests - -Integrations: list the DBs, HTTP clients, file paths, and queues touched by the traced flow. Note the library used for each. - -Tests: read 1–2 test files for the traced flow. The asserts reveal which behaviors the team treats as invariants. - ---- - -## Mental-Model Output Template - -Write the summary as `.agents/research/YYYY-MM-DD-<project>-mental-model.md` using this shape. Keep it under one page. - -```markdown ---- -date: YYYY-MM-DD -type: Research -topic: "<project> onboarding mental model" -tags: [research, onboarding, architecture] -status: COMPLETE ---- - -# <Project> — Mental Model - -## Executive Summary -<2–3 sentences: what it is, what it does, the one architectural choice that defines it.> - -## Entry Points -| Surface | Location | Purpose | -|---------|----------|---------| -| CLI | `src/main.rs:15` | clap parser, dispatches to subcommand | -| HTTP | `src/routes/mod.rs:1` | axum router, mounts `/api/*` | - -## Key Types -| Type | Location | Purpose | -|------|----------|---------| -| `Project` | `src/model.rs:10` | Core domain object | -| `Config` | `src/config.rs:5` | Runtime configuration loaded once | -| `Storage` | `src/storage.rs:1` | Persistence boundary | - -## Data Flow (representative path) -CLI args → `Config::load()` → `Project::process()` → `Storage::save()` - -## External Dependencies -| System | Library | Where touched | -|--------|---------|---------------| -| SQLite | rusqlite | `src/storage.rs` | -| HTTP | reqwest | `src/clients/api.rs` | - -## Configuration Surfaces -| Source | Example | -|--------|---------| -| Env var | `CONFIG_PATH=/etc/tool.toml` | -| File | `~/.config/tool/config.toml` | -| Flag | `--verbose` | - -## Testing Surface -- `tests/integration_test.rs` covers the CLI → storage path end-to-end. -- Property tests in `tests/prop/` assert <invariant>. -- Gaps: <untested surfaces noted during the read>. - -## Gaps and Open Questions -- <Files skipped because purpose unclear> -- <Areas where docs disagree with code> -``` - ---- - -## Anti-Patterns - -| Avoid | Do instead | -|-------|------------| -| Skipping `AGENTS.md`/`README.md` | Always read them first; they save hours | -| Random file reads | Walk entry → handler → core type → storage | -| Reading full files end-to-end | Skim structure, dive into the 1–2 critical functions | -| Ignoring tests | Tests reveal the invariants the team enforces | -| Filling context with raw source | Synthesize into the template; cite `file:line` | -| Summarizing everything you read | Cut to the 3–5 core types and one traced flow | - ---- - -## Checklist - -- [ ] `AGENTS.md` and `README.md` read before any source file. -- [ ] Top-level directory annotated. -- [ ] Entry points listed with `file:line`. -- [ ] 3–5 core types named with one-sentence purposes. -- [ ] One representative flow traced end-to-end. -- [ ] Integrations and tests noted. -- [ ] Summary written under `.agents/research/` using the template. -- [ ] Gaps explicitly listed — no false completeness. - ---- - -> Pattern adopted from `codebase-archaeology` (ACFS skill corpus). Methodology only — no verbatim text. diff --git a/skills-codex/research/references/ralph-loop-contract.md b/skills-codex/research/references/ralph-loop-contract.md deleted file mode 100644 index 994692f7b..000000000 --- a/skills-codex/research/references/ralph-loop-contract.md +++ /dev/null @@ -1,49 +0,0 @@ -# Ralph Loop Contract (Reverse-Engineered) - -This contract captures the operational Ralph mechanics reverse-engineered from: -- `https://github.com/ghuntley/how-to-ralph-wiggum` -- `.tmp/how-to-ralph-wiggum/README.md` -- `.tmp/how-to-ralph-wiggum/files/loop.sh` -- `.tmp/how-to-ralph-wiggum/files/PROMPT_plan.md` -- `.tmp/how-to-ralph-wiggum/files/PROMPT_build.md` - -Use this as the source-of-truth for Ralph alignment in AgentOps orchestration skills. - -## Core Contract - -1. Fresh context every iteration/wave. -- Each execution unit starts clean; no carryover worker memory. - -2. Scheduler-heavy, worker-light. -- The lead/orchestrator schedules and reconciles. -- Workers perform one scoped unit of work. - -3. Disk-backed shared state. -- Loop continuity comes from filesystem state, not accumulated chat context. -- In classic Ralph: `IMPLEMENTATION_PLAN.md` and `AGENTS.md`. - -4. One-task atomicity. -- Select one important task, execute, validate, persist state, then restart fresh. - -5. Backpressure before completion. -- Build/tests/lint/gates must reject bad output before task completion/commit. - -6. Observe and tune outside the loop. -- Humans (or lead agents) monitor outcomes and adjust prompts/constraints/contracts. - -## AgentOps Mapping - -| Ralph concept | AgentOps implementation | -|---|---| -| Fresh context per loop | New workers/teams per wave in `/swarm`; fresh operating-loop context per worker or NTM pane | -| Main context as scheduler | Mayor/lead orchestration in `/swarm` and `/crank` | -| Plan file as state | `bd` issue graph, TaskList state, plan artifacts in `.agents/plans/` | -| One task per pass | One issue per worker assignment in swarm/crank waves | -| Backpressure | `/validate`, task validation hooks, tests/lint gates, push/premortem gates | -| Outer loop restart | Wave loop in `/crank`; NTM/Agent Mail substrate for out-of-session loop restarts | - -## Implementation Notes - -- Keep worker prompts concise and operational. -- Keep state in files/issue trackers, not long conversational memory. -- Prefer deterministic checks over subjective completion. diff --git a/skills-codex/research/references/research.feature b/skills-codex/research/references/research.feature index 77343d74a..6decc980d 100644 --- a/skills-codex/research/references/research.feature +++ b/skills-codex/research/references/research.feature @@ -1,32 +1,11 @@ -# Executable spec for the /research skill — Move 1 of the operating loop (driving-adapter). -# /research investigates a topic prior-art-first, dispatches an explore agent that uses -# iterative retrieval, and writes a cited artifact to .agents/research/ — every claim -# carries a file:line reference. Interactive runs gate on human approval; --auto skips it. -# Hexagon: driving-adapter; consumes ao-lookup context + repo-context; produces .agents/research/*.md -# + result.json. (soc-qk4b) +Feature: Research answers one bounded question + Scenario: Load-bearing claims are cited + Given a bounded question and required evidence + When Research examines the smallest relevant sources + Then observations and inferences are distinguished + And every load-bearing claim cites authoritative evidence -Feature: Research produces a cited investigation artifact, prior-art first - As Move 1 of the operating loop - I want a topic investigated against existing knowledge before fresh exploration - So that findings are grounded, cited, and not redundant with what is already known - - Scenario: prior art is searched before fresh exploration - When /research runs on a topic - Then it first searches existing knowledge (ao lookup + the .agents/ knowledge dirs) - And applicable prior learnings are cited in the output, not just loaded passively - - Scenario: an explore agent investigates with iterative retrieval - When the investigation runs - Then an explore agent is dispatched (not merely described) - And it uses iterative retrieval — score results, extract new terms from high-relevance - hits, refine over up to 3 cycles - - Scenario: findings are written as a cited artifact - When the investigation completes - Then findings are written to .agents/research/YYYY-MM-DD-<slug>.md - And every claim carries a file:line citation - - Scenario: interactive runs gate on approval, --auto does not - When /research runs without --auto - Then it requests human approval (Gate 1) before reporting completion - And with --auto it proceeds without the approval gate + Scenario: Research stops at the evidence boundary + Given a cited answer with checked and unchecked scope + When Research reports the result + Then it does not approve work, select a next action, retry, or mutate lifecycle state diff --git a/skills-codex/research/references/software-research.md b/skills-codex/research/references/software-research.md deleted file mode 100644 index ba881cc16..000000000 --- a/skills-codex/research/references/software-research.md +++ /dev/null @@ -1,663 +0,0 @@ - -# research-software — Software Research - -> **Rules:** Latest STABLE tag (not main). Filter to 2025-2026. Code > Docs. Skip Stack Overflow. - -## Output First - -Every research produces this structure: - -```markdown -## [Tool] vX.Y.Z (YYYY-MM-DD) - -**Repo:** github.com/org/repo @ abc123 - -### Commands -| Task | Command | Notes | -|------|---------|-------| - -### Config -| Option | Default | Notes | -|--------|---------|-------| - -### Env Vars -| Variable | Purpose | -|----------|---------| - -### Gotchas -- [problem]: [fix]. Source: [PR/issue/code] - -### Sources -- Code: [file:line] -- PRs: #123, #456 -- Posts: [url] -``` - ---- - -## THE PROMPT - -``` -Research [TOOL] for [PURPOSE]. -Clone to /tmp, checkout latest stable tag. -Spawn Explore agent on source. Find: CLI, config, hidden flags, env vars. -Parallel: GitHub PRs/issues, web search "[tool] 2025". -Output: skill-ready markdown. -``` - ---- - -## Pipeline - -```bash -# 0. Detect context (if in a project) -# Check package.json, Cargo.toml, pyproject.toml for existing versions - -# 1. Clone + stable tag -git clone --depth 1 https://github.com/[org]/[repo] /tmp/[repo]-research -cd /tmp/[repo]-research && git fetch --tags && git checkout $(git describe --tags --abbrev=0) - -# 2. Spawn Explore agent (parallel with step 3-4) -# → "Find all CLI commands, config options, hidden flags, env vars in /tmp/[repo]-research" - -# 3. GitHub activity -gh pr list -R [org]/[repo] --state merged --limit 30 --json title,mergedAt -gh issue list -R [org]/[repo] --label question --limit 20 - -# 4. Web search -# → "[tool] 2025" "[tool] 2026" "[tool] tutorial" - -# 5. Synthesize → Output structure above - -# 6. Cleanup -rm -rf /tmp/[repo]-research -``` - ---- - -## Checklist - -- [ ] **Detect context:** Check package.json/Cargo.toml/pyproject.toml for versions -- [ ] **Clone repo** to /tmp, checkout latest stable tag -- [ ] **Explore agent:** CLI commands, config schema, hidden flags, env vars -- [ ] **GitHub:** Recent merged PRs, issues tagged "question"/"documentation" -- [ ] **Web search:** "[tool] 2025", "[tool] 2026", skip pre-2025 -- [ ] **Synthesize:** Commands table, config table, gotchas, patterns -- [ ] **Cite sources:** repo@commit, PR numbers, blog URLs -- [ ] **Clean up:** `rm -rf /tmp/[repo]-research` - ---- - -## Source Priority - -``` -1. Source code (actual behavior) -2. Recent PRs (features being added) -3. GitHub issues (real problems) -4. Blog posts 2025-2026 (practical patterns) -5. Official docs (baseline, often outdated) -``` - -**Skip:** Stack Overflow, anything pre-2025, basic tutorials - ---- - -## Top Mistakes - -| Mistake | Fix | -|---------|-----| -| Using beta/canary | Checkout latest stable TAG, not main | -| Old content (pre-2025) | Always add year to search queries | -| Trusting docs over code | Code wins: check actual defaults in source | -| Missing env vars | Search `process.env`, `std::env`, `os.environ` | -| Forgetting cleanup | `rm -rf /tmp/[repo]-research` when done | - ---- - -## Key Searches - -```bash -# Hidden/experimental flags -rg "hidden|experimental|unstable" /tmp/[repo]-research - -# Environment variables by language -rg "process\.env\." /tmp/[repo]-research --type ts # TypeScript -rg "std::env::" /tmp/[repo]-research --type rust # Rust -rg "os\.environ" /tmp/[repo]-research --type py # Python -rg "os\.Getenv" /tmp/[repo]-research --type go # Go - -# Recent changes -git log --oneline --since="2025-06-01" | head -30 -``` - ---- - -## Done When - -- [ ] Have version number from stable tag -- [ ] Commands table has 5+ entries -- [ ] Config table covers main options -- [ ] Gotchas section has 3+ real issues from GitHub/code -- [ ] All sources cited with links - ---- - -## Decision Tree - -``` -What are you researching? -│ -├─ CLI tool (wrangler, cargo, bun) -│ Focus: src/cli/, commands, flags, env vars -│ -├─ Library/Framework (React, Next.js) -│ Focus: packages/*/src/, exported APIs, deprecations -│ -├─ Runtime (Bun, Deno, Node) -│ Focus: built-ins, runtime flags, compat layers -│ -└─ Database/Service (D1, R2, Postgres) - Focus: query syntax, config, limits, gotchas -``` - -### Key Searches by Type - -| Type | Where to look | Key searches | -|------|---------------|--------------| -| CLI | `src/cli/`, `bin/` | `hidden.*true`, `#[arg(`, `process.env` | -| Library | `packages/*/src/`, `index.ts` | `export `, `deprecated`, `experimental` | -| Runtime | `src/`, built-ins | `flag`, `--`, `compat` | -| Database | queries, limits | `limit`, `max`, `error` | - -**Deep strategies:** [STRATEGIES.md](references/STRATEGIES.md) - ---- - -## Subagent: Code Investigator - -``` -Investigate /tmp/[repo]-research for [TOOL]. -Find: CLI commands, config options, hidden/experimental flags, env vars. -Check git log --oneline -30 for recent changes. -Output as markdown tables. -``` -Use model: `sonnet` (balance of speed + depth) - ---- - -## Subagent: Web Researcher - -``` -Search "[TOOL] 2025" and "[TOOL] 2026". -Find 5-10 recent tutorials, blog posts, announcements. -Extract: patterns, gotchas, tips. -Skip: Stack Overflow, anything pre-2025, basic tutorials. -``` -Use model: `haiku` (fast, web-focused) - ---- - -## References - -| Need | File | -|------|------| -| Output templates by tool type | [OUTPUT-TEMPLATES.md](references/OUTPUT-TEMPLATES.md) | -| Example research sessions | [EXAMPLES.md](references/EXAMPLES.md) | -| Tool-specific deep strategies | [STRATEGIES.md](references/STRATEGIES.md) | -# Research Examples - -Real sessions showing the workflow. - ---- - -## CLI Tool: Wrangler - -```bash -# 1. Clone -git clone --depth 1 https://github.com/cloudflare/workers-sdk.git /tmp/workers-sdk-research -cd /tmp/workers-sdk-research && git fetch --tags && git checkout $(git describe --tags --abbrev=0) - -# 2. Explore agent prompt: -# "Investigate /tmp/workers-sdk-research/packages/wrangler: CLI commands, config schema, hidden flags, env vars" - -# 3. GitHub -gh pr list -R cloudflare/workers-sdk --state merged --limit 30 --json title,mergedAt -gh issue list -R cloudflare/workers-sdk --label "question" --limit 20 - -# 4. Web search: "wrangler 2025", "cloudflare workers tutorial 2026" - -# 5. Cleanup -rm -rf /tmp/workers-sdk-research -``` - -**Key findings location:** `packages/wrangler/src/` — commands in `src/`, config schema in types. - ---- - -## Framework: Next.js - -```bash -# 1. Clone + stable tag -git clone --depth 1 https://github.com/vercel/next.js.git /tmp/nextjs-research -cd /tmp/nextjs-research && git fetch --tags && git checkout $(git describe --tags --abbrev=0) - -# 2. Explore agent prompt: -# "Investigate /tmp/nextjs-research/packages/next/src: exported APIs, experimental flags, config options" - -# 3. Quick searches -rg "experimental" /tmp/nextjs-research/packages/next/src/server/config-shared.ts -rg "deprecated" /tmp/nextjs-research/packages/next/src --type ts | head -20 - -# 4. Web search: "next.js 15 2025", "next.js app router 2026" - -# 5. Cleanup -rm -rf /tmp/nextjs-research -``` - -**Key findings location:** `packages/next/src/server/config-shared.ts` for all config options. - ---- - -## Runtime: Bun - -```bash -# 1. Clone -git clone --depth 1 https://github.com/oven-sh/bun.git /tmp/bun-research -cd /tmp/bun-research && git fetch --tags && git checkout $(git describe --tags --abbrev=0) - -# 2. Explore agent prompt: -# "Investigate /tmp/bun-research/src: CLI flags, built-in APIs (Bun.*), env vars" - -# 3. Quick searches -rg "process\.env\." /tmp/bun-research/src --type ts | head -30 -rg "Bun\." /tmp/bun-research/packages/bun-types/bun.d.ts | head -50 - -# 4. Web search: "bun runtime 2025", "bun vs node 2026" - -# 5. Cleanup -rm -rf /tmp/bun-research -``` - -**Key findings location:** `packages/bun-types/` for all Bun.* APIs. - ---- - -## Typical Output - -After Wrangler research: - -```markdown -## Wrangler v4.59.2 (2026-01-15) - -**Repo:** github.com/cloudflare/workers-sdk @ abc123 - -### Commands -| Task | Command | -|------|---------| -| Dev | `wrangler dev` | -| Deploy | `wrangler deploy` | -| Tail logs | `wrangler tail` | -| Types | `wrangler types` | - -### Config -| Option | Default | Notes | -|--------|---------|-------| -| `name` | required | Worker name | -| `main` | required | Entry point | -| `compatibility_date` | required | Runtime version | - -### Gotchas -- **wrangler.toml vs wrangler.jsonc**: jsonc now recommended. Source: PR #1234 -- **Auto-provisioning**: KV/R2/D1 auto-created if id omitted. Source: v4.50 release - -### Sources -- Code: packages/wrangler/src/config/config.ts:45 -- PRs: #5678, #5679 -- Posts: blog.cloudflare.com/wrangler-4 (2025-09) -``` -# Output Templates - -Expanded templates for specific tool types. Basic structure is in SKILL.md. - ---- - -## CLI Tool (Expanded) - -```markdown -## [Tool] vX.Y.Z (YYYY-MM-DD) - -**Repo:** github.com/org/repo @ abc123 - -### Commands -| Task | Command | Notes | -|------|---------|-------| -| [task] | `[cmd]` | Added in vX.Y | - -### Flags (Including Hidden) -| Flag | Description | Source | -|------|-------------|--------| -| `--flag` | [desc] | docs | -| `--hidden` | [desc] | source: file:123 | - -### Config (`[filename]`) -```toml -[section] -option = "default" # [description] -``` - -### Env Vars -| Variable | Default | Notes | -|----------|---------|-------| -| `VAR` | [default from code] | [notes] | - -### Bleeding Edge (unreleased) -| Feature | PR | Status | -|---------|-----|--------| -| [feature] | #123 | merged, not released | - -### Gotchas -- **[Issue]**: [fix]. Source: #456 - -### Patterns -```[lang] -// From: [tests/blog post] -[code] -``` - -### Sources -- Repo: [url] @ [commit] -- PRs: #123, #456 -- Posts: [url] (2025-MM) -``` - ---- - -## Library/Framework - -```markdown -## [Library] vX.Y.Z (YYYY-MM-DD) - -**Install:** `[package manager command]` - -### Core API -| Export | Purpose | Since | -|--------|---------|-------| -| `name` | [purpose] | vX.Y | - -### New in Latest Release -| API | Description | -|-----|-------------| -| `name` | [desc] | - -### Config -```[lang] -{ - option: "default", // [description] -} -``` - -### Patterns (2025-2026) -```[lang] -// Source: [blog/tests] -[code] -``` - -### Migration (from vX to vY) -- [breaking change]: [fix] - -### Gotchas -- [issue]: [solution] -``` - ---- - -## Comparison - -When researching alternatives: - -```markdown -## [Tool A] vs [Tool B] - -| Aspect | [A] | [B] | -|--------|-----|-----| -| Version | vX | vY | -| [aspect] | [A way] | [B way] | - -### Use [A] when -- [scenario] - -### Use [B] when -- [scenario] - -### Migration A → B -1. [step] -``` - ---- - -## Minimal (Quick Research) - -```markdown -## [Tool] (YYYY-MM-DD) - -**Install:** `[cmd]` -**Key:** `[most common cmd]` -**Gotcha:** [one gotcha + fix] -**New:** [one 2025-2026 feature] -**Source:** [repo@commit] -``` -# Tool-Specific Research Strategies - -Deep-dive strategies for different tool categories. - ---- - -## CLI Tools (wrangler, cargo, bun, etc.) - -### Where to Look - -``` -src/cli/ or src/cli.rs or bin/ -├── Command definitions -├── Argument parsing (clap, yargs, etc.) -├── Hidden/experimental flags -└── Default values (often different from docs) -``` - -### Key Searches - -```bash -# Rust CLI -rg "hidden\s*=\s*true" /tmp/[repo]-research --type rust -rg "#\[arg\(" /tmp/[repo]-research --type rust - -# TypeScript CLI -rg "hidden:|experimental:" /tmp/[repo]-research --type ts -rg "process\.env\." /tmp/[repo]-research --type ts - -# Go CLI -rg "Hidden:\s*true" /tmp/[repo]-research --type go -rg "os\.Getenv" /tmp/[repo]-research --type go -``` - -### Output Focus - -- Commands table with all subcommands -- Flags table (including hidden) -- Environment variables -- Config file schema -- Common patterns - ---- - -## Libraries/Frameworks (React, Next.js, etc.) - -### Where to Look - -``` -packages/[core]/src/ -├── Exported APIs (index.ts, exports.ts) -├── Internal APIs (not exported) -├── Deprecation warnings -└── Experimental/canary exports -``` - -### Key Searches - -```bash -# Find exports -rg "^export " /tmp/[repo]-research/packages/*/src/index.ts - -# Find deprecations -rg "deprecated|@deprecated" /tmp/[repo]-research - -# Find experimental -rg "experimental|unstable|canary" /tmp/[repo]-research -``` - -### Output Focus - -- API reference table -- New APIs (latest release) -- Deprecated APIs (with migration) -- Config options -- Patterns from examples/ - ---- - -## Runtimes (Bun, Deno, Node) - -### Where to Look - -``` -src/ -├── Built-in modules -├── Runtime flags -├── Environment variables -├── Compatibility layers -└── Performance options -``` - -### Key Searches - -```bash -# Runtime flags -rg "flag|--" /tmp/[repo]-research/src/cli - -# Built-in modules -rg "Bun\.|Deno\.|node:" /tmp/[repo]-research - -# Env vars -rg "process\.env|Deno\.env|Bun\.env" /tmp/[repo]-research -``` - -### Output Focus - -- CLI flags table -- Built-in APIs -- Node.js compatibility status -- Performance tuning options -- Environment variables - ---- - -## Databases/Services (D1, R2, Postgres) - -### Where to Look - -``` -src/ -├── Query syntax -├── Connection options -├── Limits and quotas -├── Error codes -└── Migration tools -``` - -### Key Searches - -```bash -# Limits -rg "limit|max|quota" /tmp/[repo]-research - -# Error codes -rg "error|Error" /tmp/[repo]-research --type ts -A 2 - -# Config -rg "config|options|settings" /tmp/[repo]-research -``` - -### Output Focus - -- Query syntax examples -- Config options table -- Limits/quotas table -- Error codes and fixes -- Migration patterns - ---- - -## Monorepo Navigation - -Many tools live in monorepos. Quick navigation: - -```bash -# Find the main package -ls /tmp/[repo]-research/packages/ - -# Find entry points -rg "\"main\":|\"bin\":" /tmp/[repo]-research/packages/*/package.json - -# Find CLI entry -rg "#!/" /tmp/[repo]-research --type ts | head -5 -``` - ---- - -## Version Detection - -```bash -# From package.json -jq '.version' /tmp/[repo]-research/package.json - -# From Cargo.toml -grep '^version' /tmp/[repo]-research/Cargo.toml - -# From git tag -git -C /tmp/[repo]-research describe --tags --abbrev=0 - -# Latest release via GitHub API -gh release view -R [org]/[repo] --json tagName -``` - ---- - -## Changelog Mining - -```bash -# Find changelog -ls /tmp/[repo]-research/CHANGELOG* /tmp/[repo]-research/HISTORY* 2>/dev/null - -# Recent entries -head -100 /tmp/[repo]-research/CHANGELOG.md - -# Search for breaking changes -rg -i "breaking|removed|deprecated" /tmp/[repo]-research/CHANGELOG.md -``` - ---- - -## Test Mining - -Tests often show real usage patterns: - -```bash -# Find test files -fd "test|spec" /tmp/[repo]-research --type f - -# Find integration tests -fd "integration|e2e" /tmp/[repo]-research --type d - -# Search tests for patterns -rg "it\(|test\(|describe\(" /tmp/[repo]-research --type ts -A 5 -``` diff --git a/skills-codex/research/references/source-discovery-and-pattern-extraction.md b/skills-codex/research/references/source-discovery-and-pattern-extraction.md deleted file mode 100644 index f4d1ff318..000000000 --- a/skills-codex/research/references/source-discovery-and-pattern-extraction.md +++ /dev/null @@ -1,48 +0,0 @@ -# Source Discovery And Pattern Extraction - -Use this reference for codebase archaeology, software-tool research, codebase reports, or mining reusable implementation patterns across one or more repositories. - -## Discovery Order - -1. Read the docs entry points first. -2. Find executable entry points: commands, handlers, jobs, hooks, or exported APIs. -3. Trace data flow from input to durable side effect. -4. Identify the core types and invariants that survive across layers. -5. Compare at least one working path with one edge path. -6. Only then summarize architecture, patterns, and risks. - -## Pattern Extraction - -Record a pattern only when it has: - -- At least two concrete examples or one canonical implementation. -- A name that describes behavior, not a file location. -- Preconditions that say when the pattern applies. -- Failure modes that say when the pattern should not be reused. -- A pointer to validation evidence. - -## Software Research Output - -For external tools and libraries, write output in this order: - -1. Current stable version and release date. -2. Supported command/API surface. -3. Config files, env vars, and hidden defaults. -4. Migration hazards and known issues. -5. Recommendation for this repo, including "do not adopt" when warranted. - -## Report Shape - -```markdown -## Summary -## Entry Points -## Core Flow -## Invariants -## Reusable Patterns -## Risks -## Open Questions -``` - ---- - -**Source:** Adapted from an external skill corpus / `codebase-archaeology`, `codebase-pattern-extraction`, `codebase-report`, and `research-software`. Pattern-only, no verbatim text. diff --git a/skills-codex/research/references/structural-graph-navigation.md b/skills-codex/research/references/structural-graph-navigation.md deleted file mode 100644 index b1b342250..000000000 --- a/skills-codex/research/references/structural-graph-navigation.md +++ /dev/null @@ -1,81 +0,0 @@ -# Structural graph navigation (graphify) — research Tier 1b recipe - -> Optional. Used by `/research` Tier 1b when `graphify` is installed (PyPI `graphifyy`). -> It maps **structure** — what calls/defines/connects to what, across files **and** -> languages — which `grep` cannot see. Reach for it on *what is X / where / what's -> connected / cross-file-link* questions, **before** broad grep. It does **not** read -> in-body logic; for control flow, read the file (Tier 4). - -## Why before grep - -`grep` finds string matches; it cannot tell you that a shell pre-push hook is wired to -a Go quorum engine, or trace a call chain across packages. graphify's `explain`/`path` -give that structural map directly. The honest failure mode is behavioral: agents default -to grep even when a graph exists — so Tier 1b is an explicit *before-grep* step, not a -"nice to have." - -## Refresh first — never query a stale graph - -The structural (AST) layer is free and fast — re-extracting ~2,500 code files measured -at **~13s, no LLM**. Always refresh before querying: - -```bash -graphify <repo> --update # incremental: only changed files (seconds) -# first run on a repo: graphify <repo> (full build; AST is the free part) -``` - -Only the AST/structural layer is cheap. The doc-semantic layer (LLM-extracted -doctrine↔code bridges) is expensive and slow-changing — **do not** depend on it live; -it is an occasional enrichment, not a research dependency. - -## The three commands - -```bash -graphify explain "<symbol>" # what a node IS + everything it connects to (calls/defines). - # Clean, precise, no noise. Best for "what is X and what touches it". -graphify path "<A>" "<B>" # shortest path A→B with the relation on each hop. - # Best for "how does A reach B" across files/languages. -graphify query "<tokens>" # ranked neighborhood around matched nodes (BFS; --dfs to trace). - # Locator, not an answerer. REQUIRES the expansion step below. -``` - -## REQUIRED: query vocab-expansion (or `query` returns noise) - -graphify's `query` matcher is **case-folded substring + IDF — no stemming, no synonyms, -no cross-language match**. Feeding it a raw question matches stray words (e.g. "decide", -"main") as substrings and collapses to noise. So before `graphify query`: - -1. Extract the graph's actual vocabulary, then pick **only tokens that exist in it** (≤12): - ```bash - PY=$(cat graphify-out/.graphify_python) - $PY -c " - import json,re; from pathlib import Path - d=json.loads(Path('graphify-out/graph.json').read_text()); v=set() - for n in d['nodes']: - for c in re.findall(r'[^\W\d_]+', n.get('label','') or '', re.UNICODE): - for p in re.findall(r'[A-Z]+(?=[A-Z][a-z])|[A-Z]?[a-z]+|[A-Z]+', c) or [c]: - t=p.lower() - if 3<=len(t)<=30: v.add(t) - Path('graphify-out/.vocab.txt').write_text('\n'.join(sorted(v)))" - ``` -2. Choose tokens from `graphify-out/.vocab.txt` that match the query intent. **Never invent - tokens.** Watch polysemy — a token like `merge` may pull in unrelated code; drop it if so. - If no vocab token matches, say the corpus has no relevant vocabulary and fall through. -3. Run `graphify query "<picked tokens>" [--dfs] [--budget 1500]` and read the ranked nodes. - -`explain` and `path` take a node label directly and need **no** expansion — prefer them when -you already know the symbol. - -## Bounds (state these, don't oversell) - -- **Structure, not logic.** `explain check_one_push` shows its callers/callees; the - *decide-then-fail-closed* logic still means reading the file. The graph maps terrain; it - doesn't read road signs. -- **AST only, for research.** The free structural layer is what research uses. The - doc-semantic bridges are real but expensive to refresh — out of scope for the live tier. - -## Graceful fallback - -If `graphify` is not installed (`command -v graphify` empty), skip Tier 1b entirely and -continue with the remaining tiers — exactly like Tier 1 Code-Map skips when -`docs/code-map/` is absent. graphify is never a hard dependency. diff --git a/skills-codex/research/references/vibe-methodology.md b/skills-codex/research/references/vibe-methodology.md deleted file mode 100644 index 629d05e5d..000000000 --- a/skills-codex/research/references/vibe-methodology.md +++ /dev/null @@ -1,112 +0,0 @@ -# Vibe Methodology - -Core principles for AI-assisted development. "Vibe" = trust-but-verify. - ---- - -## The 40% Rule - -**Never exceed 40% context utilization.** - -- Checkpoint at 35% -- Reset via session restart or `/research` artifact -- More context ≠ better results (hallucination risk increases) - ---- - -## Three Levels of Verification - -| Level | Vibe | Method | When | -|-------|------|--------|------| -| L1 | Accept | Structural check only | Boilerplate, formatting | -| L2 | Probe | Spot-check key logic | Normal implementation | -| L3 | Audit | Line-by-line review | Security, data handling | - -**Default to L2.** Upgrade to L3 for: -- Authentication/authorization -- Financial calculations -- Data persistence -- External API calls - ---- - -## Evidence Hierarchy - -Trust in order: - -1. **Running code** - Actually execute it -2. **Tests** - Passing tests prove behavior -3. **File contents** - Read the actual source -4. **Documentation** - May be stale -5. **Model claims** - Verify everything - ---- - -## Working Patterns - -### Incremental Verification -``` -Write small piece → Test → Verify → Repeat -``` - -Don't write 500 lines then debug. Write 50, verify, continue. - -### Checkpoint Often -- After each feature complete -- Before any risky change -- At natural boundaries - -### Search Before Implement -```bash -# Always check for prior art -mcp__smart-connections-work__lookup --query="<topic>" -ls .agents/research/ | grep -i "<topic>" -``` - ---- - -## Anti-Patterns to Avoid - -| Anti-Pattern | Why Bad | Instead | -|--------------|---------|---------| -| Trust-and-paste | Hallucinations slip through | Always read generated code | -| Context stuffing | Degrades quality | Stay under 40% | -| Fix spiraling | Compounds errors | Reset and rethink | -| Skipping verification | Builds on bad foundation | Verify incrementally | - ---- - -## The Research Discipline - -1. **Scope first** - Define what you're looking for -2. **Search smart** - Use semantic search before grep -3. **Read selectively** - Don't load whole files -4. **Cite everything** - `file:line` for all claims -5. **Synthesize** - Connect findings to goal - ---- - -## Session Hygiene - -```bash -# Start -gt hook # Check assigned work -bd ready # What's available - -# Work -/research <topic> # Creates artifact, saves context -/implement <issue> # Focused execution - -# End -bd vc status # Optional Dolt status check; JSONL auto-sync is automatic -git commit # Commit changes -git push # WORK IS NOT DONE UNTIL PUSHED -``` - ---- - -## References - -- `failure-patterns.md` - 12 specific failure modes -- `context-discovery.md` - 6-tier exploration hierarchy -- `~/.claude/CLAUDE-base.md` - Full vibe methodology diff --git a/skills-codex/research/schemas/findings.json b/skills-codex/research/schemas/findings.json index 3231fdef3..b8a44ee02 100644 --- a/skills-codex/research/schemas/findings.json +++ b/skills-codex/research/schemas/findings.json @@ -26,10 +26,15 @@ "additionalProperties": false } }, - "recommendations": { + "checked": { "type": "array", "items": {"type": "string"}, - "description": "Actionable next steps" + "description": "Surfaces and claims examined" + }, + "not_checked": { + "type": "array", + "items": {"type": "string"}, + "description": "Relevant surfaces and claims left unexamined" }, "schema_version": { "type": "integer", @@ -37,6 +42,6 @@ "description": "Schema version for forward compatibility" } }, - "required": ["topic", "summary", "findings", "recommendations", "schema_version"], + "required": ["topic", "summary", "findings", "checked", "not_checked", "schema_version"], "additionalProperties": false } diff --git a/skills-codex/research/scripts/validate.md b/skills-codex/research/scripts/validate.md deleted file mode 100644 index 85aeb4801..000000000 --- a/skills-codex/research/scripts/validate.md +++ /dev/null @@ -1,114 +0,0 @@ -# Validation Script for Research Skill - -## Overview - -The `validate.sh` script ensures the `/research` skill meets basic quality and completeness standards. It runs a series of checks against the skill's structure, documentation, and references. - -## Purpose - -This validation script serves as a quality gate for the research skill, ensuring: - -- Required files exist with correct structure -- Documentation includes essential patterns and concepts -- References directory contains sufficient resource materials - -## Script Location - -``` -skills/research/scripts/validate.sh -``` - -## Script Execution - -The script performs the following checks: - -### Basic Structure Validation -- **SKILL.md exists**: Verifies the primary skill documentation file -- **SKILL.md has YAML frontmatter**: Ensures proper metadata formatting -- **name: research**: Confirms correct skill identification -- **references/ directory exists**: Validates reference materials directory -- **references/ has at least 3 files**: Ensures minimum reference coverage - -### Documentation Content Validation -- **SKILL.md mentions .agents/research/ output path**: Confirms documented output location -- **SKILL.md mentions .agents/findings/registry.jsonl**: Confirms the reusable-finding registry bridge -- **SKILL.md mentions reusable findings**: Ensures transient notes are not treated as durable registry entries -- **SKILL.md mentions dedup_key**: Confirms the merge key required by the registry contract -- **SKILL.md mentions temp-file-plus-rename atomic write rule**: Confirms the registry write semantics -- **SKILL.md mentions finding-compiler.sh refresh**: Confirms the follow-up compiler pass is documented -- **SKILL.md mentions Explore agent**: Ensures agent reference is included -- **SKILL.md mentions --auto flag**: Validates feature documentation -- **SKILL.md mentions ao lookup or ao search**: Checks CLI integration documentation -- **SKILL.md mentions knowledge flywheel**: Confirms system architecture coverage -- **SKILL.md mentions backend detection**: Validates technical implementation details -- **SKILL.md mentions quality validation**: Ensures quality assurance documentation - -## Usage - -### Manual Execution - -```bash -# From the project root directory -./skills/research/scripts/validate.sh -``` - -### Expected Output - -``` -PASS: SKILL.md exists -PASS: SKILL.md has YAML frontmatter -PASS: SKILL.md has name: research -PASS: references/ directory exists -PASS: references/ has at least 3 files -PASS: SKILL.md mentions .agents/research/ output path -PASS: SKILL.md mentions .agents/findings/registry.jsonl -PASS: SKILL.md mentions reusable findings -PASS: SKILL.md mentions dedup_key -PASS: SKILL.md mentions temp-file-plus-rename atomic write rule -PASS: SKILL.md mentions finding-compiler.sh refresh -PASS: SKILL.md mentions Explore agent -PASS: SKILL.md mentions --auto flag -PASS: SKILL.md mentions ao lookup or ao search -PASS: SKILL.md mentions knowledge flywheel -PASS: SKILL.md mentions backend detection -PASS: SKILL.md mentions quality validation - -Results: 17 passed, 0 failed -``` - -## Integration with CI/CD - -This script can be integrated into continuous integration workflows to ensure the research skill meets quality standards before deployment: - -```yaml -# Example GitHub Actions workflow -- name: Validate Research Skill - run: ./skills/research/scripts/validate.sh -``` - -## Exit Codes - -- **0**: All checks passed (success) -- **1**: One or more checks failed -- **2**: Script execution error - -## Development Workflow - -### Adding New Features to Research Skill - -1. **Implement the feature** in the skill's codebase -2. **Update SKILL.md** to document the new functionality -3. **Run validation script** to ensure documentation is complete: - ```bash - ./skills/research/scripts/validate.sh - ``` -4. **Address any failures** by updating documentation or code -5. **Commit changes** with confidence the skill meets quality standards - -### Updating Validation Criteria - -To modify validation criteria: - -1. **Edit validate.sh** to add/remove checks as needed -2. **Update this documentation** to reflect new validation requirements -3. **Test the updated script** against the current skill implementation diff --git a/skills-codex/research/scripts/validate.sh b/skills-codex/research/scripts/validate.sh index c2c6b4c94..c7f85e985 100755 --- a/skills-codex/research/scripts/validate.sh +++ b/skills-codex/research/scripts/validate.sh @@ -1,27 +1,20 @@ #!/usr/bin/env bash set -euo pipefail -SKILL_DIR="$(cd "$(dirname "$0")/.." && pwd)" -PASS=0; FAIL=0 -check() { if bash -c "$2"; then echo "PASS: $1"; PASS=$((PASS + 1)); else echo "FAIL: $1"; FAIL=$((FAIL + 1)); fi; } +skill_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -check "SKILL.md exists" "[ -f '$SKILL_DIR/SKILL.md' ]" -check "SKILL.md has YAML frontmatter" "head -1 '$SKILL_DIR/SKILL.md' | grep -q '^---$'" -check "SKILL.md has name: research" "grep -q '^name: research' '$SKILL_DIR/SKILL.md'" -check "references/ directory exists" "[ -d '$SKILL_DIR/references' ]" -check "references/ has at least 3 files" "[ \$(ls '$SKILL_DIR/references/' | wc -l) -ge 3 ]" -check "SKILL.md mentions .agents/research/ output path" "grep -q '\.agents/research/' '$SKILL_DIR/SKILL.md'" -check "SKILL.md mentions .agents/findings/registry.jsonl" "grep -q '\.agents/findings/registry.jsonl' '$SKILL_DIR/SKILL.md'" -check "SKILL.md mentions reusable findings" "grep -qi 'reusable findings' '$SKILL_DIR/SKILL.md'" -check "SKILL.md mentions dedup_key" "grep -q 'dedup_key' '$SKILL_DIR/SKILL.md'" -check "SKILL.md mentions temp-file-plus-rename atomic write rule" "grep -q 'temp-file-plus-rename atomic write rule' '$SKILL_DIR/SKILL.md'" -check "SKILL.md mentions finding-compiler.sh refresh" "grep -q 'finding-compiler.sh' '$SKILL_DIR/SKILL.md'" -check "SKILL.md mentions Explore agent" "grep -qi 'explore' '$SKILL_DIR/SKILL.md'" -check "SKILL.md mentions --auto flag" "grep -q '\-\-auto' '$SKILL_DIR/SKILL.md'" -check "SKILL.md mentions ao lookup or ao search" "grep -q 'ao lookup\|ao search' '$SKILL_DIR/SKILL.md'" -check "SKILL.md mentions knowledge flywheel" "grep -qi 'knowledge' '$SKILL_DIR/SKILL.md'" -check "SKILL.md mentions backend detection" "grep -qi 'backend\|spawn' '$SKILL_DIR/SKILL.md'" -check "SKILL.md mentions quality validation" "grep -qi 'coverage\|depth\|gap' '$SKILL_DIR/SKILL.md'" +grep -q '^name: research$' "$skill_dir/SKILL.md" +grep -Fq 'Answer one bounded question with current evidence' "$skill_dir/SKILL.md" +grep -Fq 'Report unchecked scope and stop' "$skill_dir/SKILL.md" +grep -Fq 'Do not emit approval' "$skill_dir/SKILL.md" +grep -q '^Feature: Research answers one bounded question$' \ + "$skill_dir/references/research.feature" +python3 -m json.tool "$skill_dir/schemas/findings.json" >/dev/null -echo ""; echo "Results: $PASS passed, $FAIL failed" -[ $FAIL -eq 0 ] && exit 0 || exit 1 +if rg -n 'ao lookup|ao land|auto-redo|Gate 1|\.agents/rpi/next-work|finding-compiler' \ + "$skill_dir/SKILL.md" "$skill_dir/references" "$skill_dir/schemas"; then + echo 'research contract contains retired lifecycle behavior' >&2 + exit 1 +fi + +echo 'research skill contract: PASS' diff --git a/skills-codex/reverse-engineer/.agentops-generated.json b/skills-codex/reverse-engineer/.agentops-generated.json index 5cfe942fd..3229393a6 100644 --- a/skills-codex/reverse-engineer/.agentops-generated.json +++ b/skills-codex/reverse-engineer/.agentops-generated.json @@ -2,6 +2,6 @@ "generator": "codex-sync", "source_skill": "skills/reverse-engineer", "layout": "modular", - "source_hash": "623ba0b07a3642a7cebb96c3e14a02dd7fa71ce1bd663c9a0738387b3729afd0", - "generated_hash": "f58dbf565cab03bca500e88e4b21a301c8017b7897eae9dcc3bad51bb3ebb247" + "source_hash": "13593ffb9db741c610a0547a44cee2949e47089af2427d62615ccce8040a64c2", + "generated_hash": "c9078d8be65f140e0e462e476ba1a4c22e8624d9fcbba92f72f2f3bce97551df" } diff --git a/skills-codex/reverse-engineer/references/templates/post-mortem.md.tmpl b/skills-codex/reverse-engineer/references/templates/postmortem.md.tmpl similarity index 100% rename from skills-codex/reverse-engineer/references/templates/post-mortem.md.tmpl rename to skills-codex/reverse-engineer/references/templates/postmortem.md.tmpl diff --git a/skills-codex/reverse-engineer/scripts/reverse_engineer.py b/skills-codex/reverse-engineer/scripts/reverse_engineer.py index 5b96f47a0..e5a0df3f5 100755 --- a/skills-codex/reverse-engineer/scripts/reverse_engineer.py +++ b/skills-codex/reverse-engineer/scripts/reverse_engineer.py @@ -1865,14 +1865,14 @@ def main() -> int: # Run validation gate (includes secret scan over output_dir). _run([str(sec_dir / "validate-security-audit.sh"), str(output_dir), "--sbom" if args.sbom else "--no-sbom"], check=True) - # 9) Reports (vibe-style + post-mortem) + learning. + # 9) Reports (vibe-style + postmortem) + learning. council_dir = REPO_ROOT / ".agents" / "council" _ensure_dirs([council_dir]) vibe_path = council_dir / f"{_today_ymd()}-vibe-{product_slug}.md" - post_path = council_dir / f"{_today_ymd()}-post-mortem-{product_slug}.md" + post_path = council_dir / f"{_today_ymd()}-postmortem-{product_slug}.md" _render_template(TEMPLATES_DIR / "vibe-report.md.tmpl", vibe_path, {**vars, "OUTPUT_DIR": str(output_dir)}) - _render_template(TEMPLATES_DIR / "post-mortem.md.tmpl", post_path, {**vars, "OUTPUT_DIR": str(output_dir)}) + _render_template(TEMPLATES_DIR / "postmortem.md.tmpl", post_path, {**vars, "OUTPUT_DIR": str(output_dir)}) learning_path = REPO_ROOT / ".agents" / "learnings" / f"{_today_ymd()}-{product_slug}-reverse-engineer.md" if not learning_path.exists(): diff --git a/skills-codex/rpi/.agentops-generated.json b/skills-codex/rpi/.agentops-generated.json index a4a0d3479..a4bcf00db 100644 --- a/skills-codex/rpi/.agentops-generated.json +++ b/skills-codex/rpi/.agentops-generated.json @@ -2,6 +2,6 @@ "generator": "codex-sync", "source_skill": "skills/rpi", "layout": "modular", - "source_hash": "ea42feaee1859058d7bc7cf72e3095f3005a26ef61eb5ee3fc467948d4016a12", - "generated_hash": "eec6beac65f58739af88dcadeeb20bf10d3a14a989e29e3b6d163ee894c14ce0" + "source_hash": "30a04e3993b6c561fa8cf438929aaaeef84c4dfc384fbab79a99fa1a7e65d37c", + "generated_hash": "6d775ba6dfcf754f42499dfbb10bdf37ee63d7a6dd1fec973cfff6ee99d46ac4" } diff --git a/skills-codex/rpi/SKILL.md b/skills-codex/rpi/SKILL.md index fbb463b78..e2fca31d3 100644 --- a/skills-codex/rpi/SKILL.md +++ b/skills-codex/rpi/SKILL.md @@ -14,6 +14,9 @@ RPI preserves the original intent and dispatches each core phase at most once. It does not own retries, budgets, queues, claims, leases, Git, delivery, release, closure, or the caller's next decision. +The pure [`scripts/run_once.py`](scripts/run_once.py) reference behavior makes +the dispatch and stop semantics executable without Git, `ao`, or a tracker. + ## Contract 1. Invoke `$plan` once with the caller's intent. Preserve its exact diff --git a/skills-codex/rpi/scripts/run_once.py b/skills-codex/rpi/scripts/run_once.py new file mode 100644 index 000000000..a110c08ee --- /dev/null +++ b/skills-codex/rpi/scripts/run_once.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +"""Pure reference behavior for one RPI invocation. + +The caller supplies the three phase functions. This module dispatches each at +most once, translates missing phase output into an RPI report status, and never +chooses a retry or next action. +""" + +from __future__ import annotations + +from collections.abc import Callable, Mapping +import hashlib +import json +from typing import Any + + +def digest(value: Any) -> str: + payload = json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8") + return hashlib.sha256(payload).hexdigest() + + +def report( + status: str, + *, + plan_digest: str | None = None, + subject_digest: str | None = None, + verdict_ref: str | None = None, + verdict_digest: str | None = None, + checked: list[str] | None = None, + not_checked: list[str] | None = None, +) -> dict[str, Any]: + return { + "schema_version": "rpi-report.v1", + "status": status, + "plan_packet_digest": plan_digest, + "subject_manifest_digest": subject_digest, + "verdict_ref": verdict_ref, + "verdict_digest": verdict_digest, + "checked": checked or [], + "not_checked": not_checked or [], + } + + +def invoke_once( + intent: Any, + plan_phase: Callable[[Any], Mapping[str, Any] | None], + implement_phase: Callable[[Mapping[str, Any]], Mapping[str, Any] | None], + validate_phase: Callable[[Mapping[str, Any], Mapping[str, Any]], Mapping[str, Any]], +) -> dict[str, Any]: + """Dispatch Plan, Implement, and Validate no more than once each.""" + plan = plan_phase(intent) + if plan is None: + return report("NOT_PLANNED", not_checked=["implement", "validate"]) + plan = dict(plan) + plan_packet_digest = digest(plan) + + candidate = implement_phase(plan) + if candidate is None: + return report( + "NOT_BUILT", + plan_digest=plan_packet_digest, + checked=["plan"], + not_checked=["validate"], + ) + candidate = dict(candidate) + + validation = dict(validate_phase(plan, candidate)) + status = validation.get("verdict") + if status not in {"PASS", "FAIL", "NOT_PROVEN"}: + raise ValueError("Validate must return PASS, FAIL, or NOT_PROVEN") + subject_digest = validation.get("subject_manifest_digest") + verdict_digest = validation.get("verdict_digest") + verdict_ref = validation.get("verdict_ref") + if not all(isinstance(value, str) and value for value in (subject_digest, verdict_digest, verdict_ref)): + raise ValueError("Validate must return durable verdict and subject identities") + return report( + status, + plan_digest=plan_packet_digest, + subject_digest=subject_digest, + verdict_ref=verdict_ref, + verdict_digest=verdict_digest, + checked=list(validation.get("checked") or []), + not_checked=list(validation.get("not_checked") or []), + ) diff --git a/skills-codex/rpi/tests/test_run_once.py b/skills-codex/rpi/tests/test_run_once.py new file mode 100644 index 000000000..8edadb555 --- /dev/null +++ b/skills-codex/rpi/tests/test_run_once.py @@ -0,0 +1,77 @@ +from __future__ import annotations + +import importlib.util +from pathlib import Path +import unittest + + +MODULE_PATH = Path(__file__).parents[1] / "scripts" / "run_once.py" +SPEC = importlib.util.spec_from_file_location("rpi_run_once", MODULE_PATH) +assert SPEC and SPEC.loader +MODULE = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(MODULE) + + +class RunOnceTests(unittest.TestCase): + def phases(self, verdict: str = "PASS"): + calls: list[str] = [] + + def plan(intent): + calls.append("plan") + return {"schema_version": "plan-packet.v1", "intent": intent} + + def implement(_plan): + calls.append("implement") + return {"schema_version": "candidate-packet.v1"} + + def validate(_plan, _candidate): + calls.append("validate") + return { + "verdict": verdict, + "subject_manifest_digest": "a" * 64, + "verdict_digest": "b" * 64, + "verdict_ref": "/tmp/verdict.json", + "checked": ["acceptance"], + "not_checked": [], + } + + return calls, plan, implement, validate + + def test_each_phase_runs_once_and_pass_reports(self): + calls, plan, implement, validate = self.phases() + result = MODULE.invoke_once("intent", plan, implement, validate) + self.assertEqual(calls, ["plan", "implement", "validate"]) + self.assertEqual(result["status"], "PASS") + self.assertNotIn("next_action", result) + + def test_fail_reports_and_stops_without_another_dispatch(self): + calls, plan, implement, validate = self.phases("FAIL") + result = MODULE.invoke_once("intent", plan, implement, validate) + self.assertEqual(calls, ["plan", "implement", "validate"]) + self.assertEqual(result["status"], "FAIL") + + def test_missing_plan_stops_before_implement(self): + calls: list[str] = [] + result = MODULE.invoke_once( + "intent", + lambda _intent: None, + lambda _plan: calls.append("implement"), + lambda _plan, _candidate: calls.append("validate"), + ) + self.assertEqual(calls, []) + self.assertEqual(result["status"], "NOT_PLANNED") + + def test_missing_candidate_stops_before_validate(self): + calls: list[str] = [] + result = MODULE.invoke_once( + "intent", + lambda _intent: {"plan": "packet"}, + lambda _plan: None, + lambda _plan, _candidate: calls.append("validate"), + ) + self.assertEqual(calls, []) + self.assertEqual(result["status"], "NOT_BUILT") + + +if __name__ == "__main__": + unittest.main() diff --git a/skills-codex/scaffold/.agentops-generated.json b/skills-codex/scaffold/.agentops-generated.json index b4b162c08..47bbd5deb 100644 --- a/skills-codex/scaffold/.agentops-generated.json +++ b/skills-codex/scaffold/.agentops-generated.json @@ -2,6 +2,6 @@ "generator": "codex-sync", "source_skill": "skills/scaffold", "layout": "modular", - "source_hash": "9983ace93bf9de282900c228df302018dc7a4414b6c764da084f7873bafb242f", - "generated_hash": "5b383cf2a754ebfc2d69bfb09954f9d4ed27072e85fa659a78468d6c8ba827a6" + "source_hash": "5359dd43332e2f760b07e2d34d7fab55b8a6f8497fc102ed0c5d2692ef8a1444", + "generated_hash": "b850ce6d62ea30525ee389caebe065cbe863645956684e0efb5b8c59ac78d4fc" } diff --git a/skills-codex/scaffold/SKILL.md b/skills-codex/scaffold/SKILL.md index 2c50ce4a3..470e6ffd2 100644 --- a/skills-codex/scaffold/SKILL.md +++ b/skills-codex/scaffold/SKILL.md @@ -1,105 +1,49 @@ --- name: scaffold -description: Stamp project/component/CI scaffolds — but +description: Stamp a bounded project, component, or CI --- -# Scaffold Skill +# Scaffold -> **Quick Ref:** Domain-slice manifests (the repo binding) + generic project/component/CI scaffolds. `$scaffold domain <name>` for a scoped operating-loop slice; `$scaffold <language> <name>`, `$scaffold component <type> <name>`, `$scaffold ci <platform>` for the generic modes. +Create one bounded project, component, or CI scaffold. This specialist does not +schedule RPI, create work ownership, mutate Git, or decide what happens next. -Stamp real project, component, or CI boilerplate plus its executable verification surface. +## Contract -**YOU MUST EXECUTE THIS WORKFLOW. Do not just describe it.** Generate real files, run real commands, verify real output. +1. Resolve the requested target root and declare the exact paths that may be + created or changed. +2. Refuse to overwrite an existing path without explicit caller authorization. +3. Generate idiomatic, functional files with at least one behavioral test for + generated behavior. +4. Run the target's selected build, test, and lint commands once. +5. Report the files changed and factual command results, then stop. -## Critical Constraints - -- Snapshot `git status`, resolve the target root, and declare the exact write scope before generation. **Why:** scaffold must not absorb unrelated user changes or write outside the requested boundary. -- Require explicit authorization before `--force`, overwriting, deleting, or replacing any existing path; stop on overlap with pre-existing edits. **Why:** generated convenience never outranks user-owned work. -- Use the current agent and local shell; do not start alternate runtimes or orchestration substrates unless the user explicitly requested them. **Why:** scaffolding is a bounded write operation, not automatic permission to fan out. -- Run the target's build, behavioral test, and lint contract once and report the - exact results. Git, repair, reruns, escalation, and delivery belong to the caller. +Use the current agent and local shell unless the caller explicitly requests a +different runtime. Preserve unrelated existing changes. ## Modes -| Mode | Invocation | Output | Where | -|------|-----------|--------|-------| -| **Domain-Slice** | `$scaffold domain <name>` | Domain-slice manifest for a scoped operating-loop run | **this file** (repo binding) | -| **Project** | `$scaffold <language> <name>` | Full project directory with build, test, lint | [references/generic-templates.md](references/generic-templates.md) | -| **Component** | `$scaffold component <type> <name>` | New module/package added to existing project | [references/generic-templates.md](references/generic-templates.md) | -| **CI** | `$scaffold ci <platform>` | CI/CD pipeline configuration | [references/generic-templates.md](references/generic-templates.md) | +- `$scaffold <language> <name>` creates a project. +- `$scaffold component <type> <name>` adds a component to an existing project. +- `$scaffold ci <platform>` creates the requested CI configuration. -Parse the invocation: `domain` first-positional → Domain-Slice; `component` → Component; `ci` → CI; otherwise Project. If ambiguous, ask ONE clarifying question, then proceed. +If the request does not identify a target or language, ask only for the missing +fact. The caller owns version control, revision, and delivery. -## Generic scaffolding (project / component / CI) +## Evidence -**A frontier model needs no template for standard project trees, best-practice -config, or CI YAML.** State the language, type, and name; it produces an -idiomatic tree with real files and a behavioral test, then reports the selected -build/test/lint results. The caller owns version-control actions. +Return: -The canonical tree shapes, `.editorconfig`/pre-commit/CI YAML skeletons, verification-command table, per-mode component layouts, and the error-recovery + output-summary blocks the skill historically stamped are preserved verbatim in **[references/generic-templates.md](references/generic-templates.md)** — consult it only when you want those exact shapes. For installer scripts, agent-facing tool servers, MCP surfaces, or Rust CLI storage scaffolds, apply [references/agent-facing-tool-scaffolds.md](references/agent-facing-tool-scaffolds.md) before writing files. +- the target root and actual changed paths; +- the build, test, and lint commands selected; +- each command's exit code; +- any requested check that was not run. -## Domain-Slice Mode - -When invoked as `$scaffold domain <name>`, scaffold a **domain-slice manifest** — the bounded-context declaration used to scope an operating-loop run. - -> There is **no `scaffold` subcommand on the `ao` CLI**. Domain-slice scaffolding is this skill's responsibility; the old phased-engine flags are superseded by ADR-0009. - -### Workflow - -1. **Generate the manifest.** Run the write-and-exit flag — it creates the template and returns without starting an RPI run: - - Run `$scaffold domain <name>`. - - This writes `docs/domains/<name>/manifest.yaml` from a template that already validates against `schemas/domain-slice-manifest.v1.schema.json`. An existing manifest is **not** overwritten unless `--force` is passed. - -2. **Fill in the placeholders.** Edit the generated manifest: - - `bounded_context` — one sentence: what this slice owns and explicitly does NOT own. - - `directive_ids` — stable GOALS.md directive IDs (pattern `d-<slug>`) this slice owns. - - `scenario_ids` — promoted spec scenario IDs from `spec/scenarios/` (may stay `[]` initially). - - `context_roots` — repo-relative implementation surface (at least one entry). - - `allowed_read_globs` / `denied_read_globs` — the read fence (gitignore syntax; deny wins). - - `validation_commands` — ordered build/test/lint steps. - -3. **Verify it loads.** The scaffolded manifest already passes the F3.1 schema/loader. After editing, confirm it still validates: - - Dry-run the operating-loop plan against `docs/domains/<name>/manifest.yaml` before execution. - - A dry run loads the manifest, prints the scoped phase prompts, and exits — proving the slice attaches. - -4. **Run scoped RPI.** Once the manifest is real: - - Run the operating loop with `docs/domains/<name>/manifest.yaml` as the explicit scope contract. - - Phase prompts carry the slice's boundaries; each run also writes a domain-scope audit artifact reporting any out-of-domain references visible in evidence. - -### Next commands the scaffold names - -After writing the manifest, lint executable-spec links with `ao goals scenarios --lint`, preview the scoped operating-loop plan, then execute with the manifest as the scope contract. Run them in that order. - -Error-recovery and output-summary conventions (shared with the generic modes) live in [references/generic-templates.md](references/generic-templates.md). - -## Output Specification - -**Artifact directory:** generated files stay under the declared target root; write the durable handoff to `.agents/evidence/scaffold/<run-id>/` at the invocation root. -**Filename convention:** required `receipt.json`; Domain-Slice mode additionally produces `docs/domains/<name>/manifest.yaml`; other filenames follow the selected scaffold mode. -**Serialization/schema format:** `receipt.json` contains `schema_version: 1`, -`mode`, `target_root`, `files_created`, `files_modified`, and factual -`validation` entries of `{kind,command,exit_code}`. It contains no commit, -verdict, next action, retry, or delivery state. -**Validator command:** check the required fields and confirm every reported path -is beneath `target_root`. -**Output:** return the receipt path and stop. - -## Quality Checklist - -- [ ] The generated paths equal the declared write scope and preserve pre-existing changes. -- [ ] Files contain real behavior and at least one behavioral test—no placeholder-only green. -- [ ] Build, tests, and lint are recorded with actual exit codes in `receipt.json`. -- [ ] Domain manifests validate against `schemas/domain-slice-manifest.v1.schema.json` and retain their read fence. -- [ ] The receipt contains facts only and leaves Git and continuation to the caller. +The result contains no verdict, lifecycle state, retry instruction, or next +action. ## References +- [references/generic-templates.md](references/generic-templates.md) — optional + historical shapes when the caller wants a specific template. - [references/agent-facing-tool-scaffolds.md](references/agent-facing-tool-scaffolds.md) -- [references/recommended-reading.md](references/recommended-reading.md) — forward-looking index of external skills (e.g., `mcp-server-design`) worth absorbing into scaffold when their trigger conditions arrive. Consult before designing a new scaffold mode that targets agent-facing tool surfaces. -- [references/scaffold.feature](references/scaffold.feature) — Executable spec: project/component/CI scaffolding entry points + domain-slice manifest routing (soc-qk4b) +- [references/scaffold.feature](references/scaffold.feature) diff --git a/skills-codex/scaffold/prompt.md b/skills-codex/scaffold/prompt.md index c9ce08dd0..c319a63c0 100644 --- a/skills-codex/scaffold/prompt.md +++ b/skills-codex/scaffold/prompt.md @@ -1,6 +1,6 @@ # scaffold -Stamp project/component/CI scaffolds — but reach for it mainly for the repo-specific domain-slice binding (generic trees a frontier model needs no skill for). Triggers: "scaffold", "create project component or boilerplate". +Stamp a bounded project, component, or CI scaffold and verify the generated result once. Triggers: "scaffold", "create project component or boilerplate". ## Instructions diff --git a/skills-codex/scaffold/references/generic-templates.md b/skills-codex/scaffold/references/generic-templates.md index d49224667..de2c61a7e 100644 --- a/skills-codex/scaffold/references/generic-templates.md +++ b/skills-codex/scaffold/references/generic-templates.md @@ -4,12 +4,12 @@ > (bead `age-skills-audit-fable-l6ic.10`, generic-craft trim). A frontier model produces > standard project trees, best-practice config, and GitHub-Actions / GitLab-CI YAML > correctly **with no template** — so this file is a fallback reference, not the skill's -> durable value. The durable binding (the domain-slice manifest → operating-loop scope) -> stays in `SKILL.md`. Reach for this file only when you want the canonical shapes the -> skill historically stamped; otherwise ask the model directly. +> durable value. Reach for this file only when the caller wants one of the +> historical shapes the skill stamped; otherwise produce an idiomatic scaffold +> directly. The three generic modes share a four-step spine: **gather requirements → generate -structure → verify → initial commit**. Every generated file must have real, functional +structure → verify → report**. Every generated file must have real, functional content — not placeholder comments. ## Step 1: Gather Requirements diff --git a/skills-codex/scaffold/references/recommended-reading.md b/skills-codex/scaffold/references/recommended-reading.md deleted file mode 100644 index 2b9570245..000000000 --- a/skills-codex/scaffold/references/recommended-reading.md +++ /dev/null @@ -1,25 +0,0 @@ -# Recommended Reading for Future Scaffolding Work - -> Forward-looking index of external skills and methodologies worth absorbing into `scaffold` (or a sibling skill) when the right trigger arrives. Not active dependencies; just breadcrumbs so high-utility sources are not lost. - -## How to use this file - -When a trigger condition listed below fires (new transport surface, new agent-API workload, new boilerplate domain), revisit the named source, evaluate whether the patterns still apply, and absorb the load-bearing ideas into the relevant SKILL.md or a new `references/*.md`. Do not bulk-import — extract only what is currently load-bearing. Append new candidates to the table below when a fresh source crosses the bar (recurring citations, repeated value, or distinct framing not already captured). - -## Candidates - -| Skill / source | Origin | Why relevant to scaffold | Trigger to absorb | -|---|---|---|---| -| `mcp-server-design` | ACFS | Agent-facing tool UX patterns: anticipating how agents misuse APIs, structured "fail helpfully" errors, "agent theory of mind" framing for tool design, and `make the wrong thing impossible` as a north star for boilerplate defaults. Useful when scaffold output is itself an agent-facing tool surface (MCP server, CLI agent). Scored 1.00 in the 2026-05-03 external-corpus utility map. | When MCP transport work begins, or when a new scaffold mode targets MCP/agent-tool servers. | - -## Entry shape - -When adding a new candidate, keep the same four fields so the table stays easy to scan: - -- **Skill / source** — name and (if external) the upstream owner. -- **Origin** — where the skill currently lives (ACFS, third-party repo, internal experiment). -- **Why relevant to scaffold** — one or two sentences naming the specific patterns scaffold could borrow. Phrase the takeaway in our own words; do not paste the source's description verbatim. -- **Trigger to absorb** — the concrete condition that should re-open this absorption candidate. Avoid vague triggers ("when relevant"); name the workload, surface, or epic. - ---- -> Forward-looking absorption-candidate index. Source skills credited inline above. diff --git a/skills-codex/scaffold/references/scaffold.feature b/skills-codex/scaffold/references/scaffold.feature index f0e75afa0..7d4c9a5d3 100644 --- a/skills-codex/scaffold/references/scaffold.feature +++ b/skills-codex/scaffold/references/scaffold.feature @@ -1,8 +1,4 @@ -# Executable spec for the /scaffold skill — project/component/CI scaffolding (BC3 Loop). -# /scaffold generates new-project structure, components, and CI pipelines from a single -# entry point, and backs domain-slice scaffolding with `/scaffold domain <name>` -# (there is no `ao scaffold` subcommand). Hexagon: supporting; consumes: a scaffold -# target (language/component/CI/domain); produces: project files + directory structure. (soc-qk4b) +# Executable spec for bounded project/component/CI scaffolding. Feature: Scaffold generates project, component, and CI structure As a developer starting new work @@ -24,6 +20,7 @@ Feature: Scaffold generates project, component, and CI structure When "/scaffold ci <platform>" runs Then it sets up the CI pipeline for that platform - Scenario: Domain-slice scaffolding routes through the skill - When domain scaffolding is requested - Then it writes the domain-slice manifest rather than calling a non-existent "ao scaffold" command + Scenario: Existing paths are preserved + Given the requested target contains an existing file + When scaffolding runs without explicit overwrite authorization + Then the existing file is not replaced diff --git a/skills-codex/scaffold/scripts/validate.sh b/skills-codex/scaffold/scripts/validate.sh index ec0b263fc..800f85af8 100755 --- a/skills-codex/scaffold/scripts/validate.sh +++ b/skills-codex/scaffold/scripts/validate.sh @@ -1,103 +1,15 @@ #!/usr/bin/env bash set -euo pipefail + SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -SKILL_MD="$SKILL_DIR/SKILL.md" -PASS=0 -FAIL=0 +SKILL="$SKILL_DIR/SKILL.md" -record() { - local label="$1" - shift - if "$@"; then - echo "PASS: $label" - PASS=$((PASS + 1)) - else - echo "FAIL: $label" - FAIL=$((FAIL + 1)) - fi -} +[[ -s "$SKILL" ]] +grep -q '^name: scaffold$' "$SKILL" +grep -q '^ effects: \[\]$' "$SKILL" +grep -q '^## Contract$' "$SKILL" +grep -q '^## Evidence$' "$SKILL" +grep -Fq 'The caller owns version control, revision, and delivery.' "$SKILL" +! grep -Eiq 'AUTO-REDO|ONE-HELPER|HELPER-ESCALATE|ao land|next_action' "$SKILL" -validate_contract() { - local skill_md="$1" - [[ "$(awk '/^---$/{n++;next} n==2 && /^## /{print;exit}' "$skill_md")" == "## Critical Constraints" ]] && - grep -Fq 'WARN|FAIL|REFUTED -> AUTO-REDO' "$skill_md" && - grep -Fq 'BREAKER -> HOLD -> ONE-HELPER' "$skill_md" && - grep -Fq 'HELPER-UNSTUCK -> AUTO-REDO' "$skill_md" && - grep -Fq 'HELPER-ESCALATE -> HUMAN' "$skill_md" && - grep -Fq 'REFUSAL-LANE|EXPLICIT-JUDGMENT|EXHAUSTED-BUDGET -> HUMAN' "$skill_md" && - grep -Fq '**Artifact directory:**' "$skill_md" && - grep -Fq '**Filename convention:**' "$skill_md" && - grep -Fq '**Serialization/schema format:**' "$skill_md" && - grep -Fq '**Validator command:**' "$skill_md" && - grep -Fq '(["build","test","lint"]-[$r.validation[].kind])|length==0' "$skill_md" && - grep -Fq '($r.verdict != "PASS" or all($r.validation[]; .exit_code == 0))' "$skill_md" && - grep -Fq '**Downstream handoff:**' "$skill_md" && - grep -Fq '## Quality Checklist' "$skill_md" -} - -record "SKILL.md exists" test -f "$SKILL_MD" -record "SKILL.md has name: scaffold" grep -q '^name: scaffold' "$SKILL_MD" -record "SKILL.md mentions boilerplate or starter" grep -qiE 'boilerplate|starter' "$SKILL_MD" -record "SKILL.md mentions component or project generation" grep -qiE 'component|project|generat' "$SKILL_MD" -record "scaffold contract is complete" validate_contract "$SKILL_MD" -record "overwrite requires explicit authorization" grep -Fq 'explicit authorization' "$SKILL_MD" -record "scaffold never pushes" grep -Fq 'Never push' "$SKILL_MD" - -pawl_fixture="$(mktemp)" -output_fixture="$(mktemp)" -receipt_fixture="$(mktemp)" -invalid_receipt_fixture="$(mktemp)" -trap 'rm -f "$pawl_fixture" "$output_fixture" "$receipt_fixture" "$invalid_receipt_fixture"' EXIT -awk '!/HELPER-UNSTUCK -> AUTO-REDO/' "$SKILL_MD" >"$pawl_fixture" -awk '!/\*\*Validator command:\*\*/' "$SKILL_MD" >"$output_fixture" - -if validate_contract "$pawl_fixture"; then - echo "FAIL: deletion fixture rejects missing pawl transition" - FAIL=$((FAIL + 1)) -else - echo "PASS: deletion fixture rejects missing pawl transition" - PASS=$((PASS + 1)) -fi - -if validate_contract "$output_fixture"; then - echo "FAIL: deletion fixture rejects incomplete output handoff" - FAIL=$((FAIL + 1)) -else - echo "PASS: deletion fixture rejects incomplete output handoff" - PASS=$((PASS + 1)) -fi - -validate_receipt() { - jq -e '. as $r | .schema_version==1 and (["domain","project","component","ci"]|index($r.mode))!=null and ($r.target_root|type=="string" and length>0) and ($r.files_created|type=="array" and all(.[]; type=="string")) and ($r.files_modified|type=="array" and all(.[]; type=="string")) and (($r.files_created|length)+($r.files_modified|length)>0) and ($r.validation|type=="array" and length>0 and all(.[]; (.kind as $kind | (["build","test","lint"]|index($kind))!=null) and (.command|type=="string" and length>0) and (.exit_code|type=="number"))) and ((["build","test","lint"]-[$r.validation[].kind])|length==0) and (($r.commit==null) or ($r.commit|type=="string")) and (["PASS","WARN","FAIL"]|index($r.verdict))!=null and ($r.verdict != "PASS" or all($r.validation[]; .exit_code == 0)) and ($r.next_action|type=="string" and length>0)' "$1" >/dev/null -} - -printf '%s\n' '{"schema_version":1,"mode":"component","target_root":"internal/example","files_created":["internal/example/example.go"],"files_modified":[],"validation":[{"kind":"build","command":"go build ./internal/example","exit_code":0},{"kind":"test","command":"go test ./internal/example","exit_code":0},{"kind":"lint","command":"go vet ./internal/example","exit_code":0}],"commit":null,"verdict":"PASS","next_action":"review generated diff"}' >"$receipt_fixture" -printf '%s\n' '{"schema_version":1,"mode":"component","target_root":"","files_created":[],"files_modified":[],"validation":[],"commit":null,"verdict":"PASS","next_action":""}' >"$invalid_receipt_fixture" -record "receipt validator accepts complete handoff" validate_receipt "$receipt_fixture" -if validate_receipt "$invalid_receipt_fixture"; then - echo "FAIL: receipt validator rejects incomplete handoff" - FAIL=$((FAIL + 1)) -else - echo "PASS: receipt validator rejects incomplete handoff" - PASS=$((PASS + 1)) -fi -printf '%s\n' '{"schema_version":1,"mode":"component","target_root":"internal/example","files_created":["internal/example/example.go"],"files_modified":[],"validation":[{"kind":"test","command":"go test ./...","exit_code":0}],"commit":null,"verdict":"PASS","next_action":"review generated diff"}' >"$invalid_receipt_fixture" -if validate_receipt "$invalid_receipt_fixture"; then - echo "FAIL: receipt validator rejects missing build/test/lint coverage" - FAIL=$((FAIL + 1)) -else - echo "PASS: receipt validator rejects missing build/test/lint coverage" - PASS=$((PASS + 1)) -fi -printf '%s\n' '{"schema_version":1,"mode":"component","target_root":"internal/example","files_created":["internal/example/example.go"],"files_modified":[],"validation":[{"kind":"build","command":"go build ./...","exit_code":0},{"kind":"test","command":"go test ./...","exit_code":1},{"kind":"lint","command":"go vet ./...","exit_code":0}],"commit":null,"verdict":"PASS","next_action":"review generated diff"}' >"$invalid_receipt_fixture" -if validate_receipt "$invalid_receipt_fixture"; then - echo "FAIL: receipt validator rejects PASS with failed validation command" - FAIL=$((FAIL + 1)) -else - echo "PASS: receipt validator rejects PASS with failed validation command" - PASS=$((PASS + 1)) -fi - -echo -echo "Results: $PASS passed, $FAIL failed" -(( FAIL == 0 )) +echo "scaffold contract: PASS" diff --git a/skills-codex/scope/.agentops-generated.json b/skills-codex/scope/.agentops-generated.json index 63aabcc06..42b3a6600 100644 --- a/skills-codex/scope/.agentops-generated.json +++ b/skills-codex/scope/.agentops-generated.json @@ -2,6 +2,6 @@ "generator": "codex-sync", "source_skill": "skills/scope", "layout": "modular", - "source_hash": "777d5c5cf2f23a92f36221833c8840cea6e954a97bca72f14320d520fe97e831", - "generated_hash": "7cbc8d1f35192156232de8dd9f1aef87197526732d4ff2507d7f5aa58e5a86a4" + "source_hash": "d8d25468511b2128b13fe1d08bebcb8c172f7da2d639b7e39ffe326edad6cddf", + "generated_hash": "3d92ff818a8ce7071b82825f117d267c1d642682557750f8ea5ea2aaa5f78ef2" } diff --git a/skills-codex/scope/SKILL.md b/skills-codex/scope/SKILL.md index 0ed476154..033810cc8 100644 --- a/skills-codex/scope/SKILL.md +++ b/skills-codex/scope/SKILL.md @@ -1,136 +1,53 @@ --- name: scope -description: Hard-block edits outside declared frozen +description: Review a proposed PlanPacket write scope for --- -# $scope — Edit Scope Guard +# $scope — Review a proposed write scope -> **Purpose:** Declare which directories are in scope for the current work session. Edits outside the declared scope are hard-blocked by a PreToolUse hook. +Review the `write_scope.include` and `write_scope.exclude` fields of a proposed +PlanPacket. This skill is advisory: it does not write a lock, install a hook, +block an edit, claim paths, or change the PlanPacket. -**YOU MUST EXECUTE THIS WORKFLOW. Do not just describe it.** +## Inputs -## Critical Constraints +- One active behavior and its acceptance scenarios. +- Proposed include and exclude patterns. +- Known generated companions and fixture/projection paths. +- Explicit non-goals. -- Treat `.agents/scope.lock` as a containment boundary, never as permission to edit every path it names. **Why:** scope limits authority; it does not create authority or ownership. -- Resolve each frozen directory repo-relative, reject traversal outside the repository, and verify `ao scope status --json` after every mutation. **Why:** an unverified or escaping prefix gives false confidence about the active boundary. -- Never unfreeze or widen scope merely to make a blocked edit pass; require explicit scope-expansion judgment tied to the original objective. **Why:** silently moving the boundary defeats the guard. -- Use the current agent and local shell for freeze, status, and recovery; do not start another runtime or orchestration substrate unless explicitly requested. **Why:** a path guard does not authorize fan-out. -- Report an out-of-scope target and stop that invocation. This skill does not - widen scope, repair, retry, escalate, or select an alternative path. +## Procedure ---- +1. Map each acceptance criterion to the smallest source paths that may change. +2. Add owned generated companions that must move with those sources. +3. Check whether any include/exclude patterns overlap or are too broad to prove. +4. Identify likely paths the proposal omitted. +5. Return a corrected proposal and the reasons for each change, then stop. -## Quick Start +The caller decides whether to adopt the proposal. Plan remains the sole author +of a PlanPacket, and Validate independently compares proven changed paths with +the accepted scope. -```bash -$scope freeze cli/cmd/ao/ # Freeze a single directory -$scope freeze cli/cmd/ao/ skills/scope/ # Freeze multiple (additive) -$scope unfreeze cli/cmd/ao/ # Remove one frozen directory -$scope unfreeze # Clear ALL frozen directories -$scope status # Show current lock state -$scope status --json # JSON output +## Output + +```yaml +write_scope: + include: ["bounded/source/**"] + exclude: ["bounded/source/generated-by-other-owner/**"] +generated_companions: ["bounded/generated/**"] +gaps: [] +ambiguities: [] ``` ---- +## Checks -## Behavior Contract +- Patterns are normalized repository-relative paths. +- Includes cover the behavior without granting unrelated directories. +- Excludes do not contradict required changes. +- Generated companions are explicit. +- No ownership, scheduling, Git, hook, retry, release, or delivery state is + introduced. -When `.agents/scope.lock` declares one or more `frozen_dirs`: +## Failure behavior -- Any `Edit`, `Write`, or `Bash` tool call whose target path is **outside** every frozen directory is **rejected** by `hooks/edit-scope-guard.sh` with a structured stderr reason and a non-zero exit code (Codex converts that into a tool-use refusal). -- Edits to paths **under** any frozen directory are allowed. -- When the lock file is missing OR `frozen_dirs` is empty, the hook short-circuits with exit 0 (no enforcement; allow everything). -- The hook fails **open** on malformed JSON or missing target-path fields — do not block when the input contract is violated. Defensive default protects against harness changes. - -The lock file is written via `cli/internal/llmwiki/scope_guard.go:SafeAtomicWrite`, so concurrent `freeze` / `unfreeze` calls converge atomically (last writer wins, never tears). - ---- - -## Subcommands - -### `$scope freeze <dir>...` - -Append one or more directories to the frozen set. Idempotent; re-freezing an already-frozen directory is a no-op. Updates `acquired_at` (ISO-8601) and `acquired_by` (session id or PID) on every write. - -### `$scope unfreeze [<dir>]` - -Without arguments, clears the entire frozen set. With one or more directory arguments, removes just those entries. Removing a directory that is not frozen is a no-op. - -### `$scope status [--json]` - -Print the current lock state. With `--json`, emit a single JSON object matching the schema in [references/lock-file-format.md](references/lock-file-format.md). Without flags, print a human-readable summary including each frozen directory, the acquisition timestamp, and the acquiring session. - -### `$scope guard` (future combo skill) - -Reserved for a follow-up skill that combines `freeze` + status + spawn-orchestration. Not implemented in this release; documented here for forward reference. - ---- - -## Lock File Format - -`.agents/scope.lock` is a single JSON object. Full schema lives in [references/lock-file-format.md](references/lock-file-format.md). Key fields: - -- `schema_version` — currently `1` -- `frozen_dirs` — list of repo-relative directory prefixes (trailing slash optional) -- `acquired_at` — ISO-8601 UTC timestamp -- `acquired_by` — string identifying the writer (session id, PID, or label) - ---- - -## Output Specification - -**Artifact directory:** `.agents/` under the current repository, or the path selected explicitly through `AO_SCOPE_LOCK`/`--lock` for isolated validation. -**Filename convention:** `scope.lock`; status emits the same state to stdout, with `--json` producing one JSON object. -**Serialization/schema format:** JSON matching [lock-file-format](references/lock-file-format.md): `schema_version: 1`, string array `frozen_dirs`, nonempty RFC-3339 `acquired_at`, and string `acquired_by`. -**Validator command:** run `ao scope status --json | jq -e '.schema_version==1 and (.frozen_dirs|type=="array" and all(.[]; type=="string" and length>0)) and (.acquired_at|type=="string" and length>0) and (.acquired_by|type=="string")'`. -**Output:** pass the lock path, normalized frozen directories, acquisition -identity/time, attempted target, and blocked-edit reason. The caller decides -what happens next. - -## Quality Checklist - -- [ ] Every frozen directory is repo-relative, normalized, and tied to the current objective. -- [ ] `ao scope status --json` round-trips after freeze/unfreeze and passes the validator. -- [ ] In-scope and out-of-scope probes demonstrate the intended boundary before risky work. -- [ ] A rejection stops the invocation and never silently widens scope. -- [ ] Unfreeze happens at explicit release/closeout, not as a workaround for a failed command. - ---- - -## Examples - -### Freezing scope before a swarm wave - -**User says:** `$scope freeze cli/cmd/ao/ cli/internal/scope/` - -**What happens:** - -1. `ao scope freeze cli/cmd/ao/ cli/internal/scope/` writes `.agents/scope.lock` via `SafeAtomicWrite`. -2. `hooks/edit-scope-guard.sh` (registered as PreToolUse on `Edit|Write|Bash`) consults the lock on every subsequent tool call. -3. A worker that tries to `Write` to `skills/foo/SKILL.md` is rejected; a worker editing `cli/cmd/ao/scope.go` proceeds. - -### Releasing scope at the end of a wave - -**User says:** `$scope unfreeze` - -**What happens:** - -1. `ao scope unfreeze` rewrites `.agents/scope.lock` with `frozen_dirs: []`. -2. The hook short-circuits to exit 0 on the next tool call. - ---- - -## Notes - -- Wave 1 hardcodes the `.agents/scope.lock` path. Wave 2 (issue I5) migrates the path through `lib/ao-paths.sh`. -- The hook's defensive parse on malformed JSON is intentional. See [references/lock-file-format.md](references/lock-file-format.md) for the rationale. -- This skill is purely session-boundary (path-scope freezing within a session). Cron-cadence orchestration lives outside AgentOps on the orchestration substrate (the reference is NTM + MCP + managed-agents), not in an AgentOps-shipped daemon. -- Path-scope freezing handles *where* edits land. For a complementary lane that gates *what* commands run (`rm -rf`, `git reset --hard`, `DROP DATABASE`, `kubectl delete`, `terraform destroy`) — including allowlist layering, one-shot override codes, and PreToolUse wiring — see [references/destructive-command-guard-patterns.md](references/destructive-command-guard-patterns.md). Wire it alongside the scope guard when a wave touches infrastructure or shared data. -- When a workflow needs human approval, hook parity, or simultaneous command review rather than only path freezing, use [references/command-approval-and-hook-guardrails.md](references/command-approval-and-hook-guardrails.md). -- When authoring new hook behavior rather than using scope's existing guard, use the hook authoring guidance in `cc-hooks`. - -## References - -- [references/lock-file-format.md](references/lock-file-format.md) -- [references/destructive-command-guard-patterns.md](references/destructive-command-guard-patterns.md) -- [references/command-approval-and-hook-guardrails.md](references/command-approval-and-hook-guardrails.md) -- [references/scope.feature](references/scope.feature) — Executable spec: declare in-scope dirs, allow in-scope edits, hard-block out-of-scope edits via PreToolUse hook, report/release scope state (soc-qk4b) +If the scope cannot be made unambiguous from the supplied acceptance, report +the missing facts and stop. The caller may revise the intent in a new action. diff --git a/skills-codex/scope/prompt.md b/skills-codex/scope/prompt.md index a526265c1..b96d7aa0f 100644 --- a/skills-codex/scope/prompt.md +++ b/skills-codex/scope/prompt.md @@ -1,6 +1,6 @@ # scope -Hard-block edits outside declared frozen directories and protect paths during risky changes. Triggers: "scope", "hard-block edits outside declared frozen", "scope skill". +Review a proposed PlanPacket write scope for completeness and ambiguity. Triggers: "review write scope", "check scope boundaries", "scope this change". ## Instructions diff --git a/skills-codex/scope/references/command-approval-and-hook-guardrails.md b/skills-codex/scope/references/command-approval-and-hook-guardrails.md deleted file mode 100644 index c242ab707..000000000 --- a/skills-codex/scope/references/command-approval-and-hook-guardrails.md +++ /dev/null @@ -1,32 +0,0 @@ -# Command Approval And Hook Guardrails - -Use this reference when path-scope protection is not enough and a session needs command approval, hook parity, or high-risk operation review. - -## Guardrail Layers - -| Layer | Blocks | Evidence | -|---|---|---| -| Path scope | Edits outside declared directories | `.agents/scope.lock` and hook stderr. | -| Command risk | Destructive or irreversible commands | Approval record or explicit denial. | -| Hook parity | Runtime-specific hook behavior drift | Hook fixture and schema tests. | -| Peer approval | High-risk command execution | Reviewer identity, command, and expiry. | - -## Approval Rules - -- Approval is per command shape, not a blanket session waiver. -- Expire approvals quickly. -- Record the exact command, working directory, and reason. -- Prefer a safer equivalent command when one exists. -- Refuse approval when rollback is unclear. - -## Hook Review Checklist - -- The hook fails open only for malformed hook input, not for known risky input. -- Output uses the portable subset accepted by all supported runtimes. -- Kill switches are documented and tested. -- Regex matchers have positive and negative examples. -- The hook has a timeout and no shell injection path. - ---- - -**Source:** Adapted from an external skill corpus / `dcg`, `cc-hooks`, and `slb`. Pattern-only, no verbatim text. diff --git a/skills-codex/scope/references/destructive-command-guard-patterns.md b/skills-codex/scope/references/destructive-command-guard-patterns.md deleted file mode 100644 index 15f670c5c..000000000 --- a/skills-codex/scope/references/destructive-command-guard-patterns.md +++ /dev/null @@ -1,89 +0,0 @@ -# Destructive Command Guard Patterns - -A scope guard freezes *where* edits land. A destructive-command guard adds an orthogonal lane: freezing *what* commands a worker may execute, regardless of which directory it touches. This reference distills the methodology so a future scope-pack contributor can wire one in without reinventing the contract. - -## Why a separate guard - -Scope-only enforcement leaves a gap. A worker can stay inside the frozen directory and still run something irrecoverable from there — `rm -rf .`, `git reset --hard`, `DROP DATABASE`, `kubectl delete -A`, `terraform destroy`. The directory check passes; the blast radius does not. - -The destructive-command guard sits in the same PreToolUse position as `edit-scope-guard.sh`, but its predicate is the command string rather than the target path. The two compose cleanly: - -``` -PreToolUse(Bash) → scope-path-check → destructive-command-check → allow/deny -``` - -A failure in either lane rejects the tool call. - -## Pattern catalog - -The guard ships a base catalog keyed by tool family. Treat each entry as an authoritative pattern, not a regex literal — the implementation should normalize whitespace, quoting, and `--flag=value` vs `--flag value` before matching. - -| Family | Pattern shape | Why it qualifies | -|---|---|---| -| Filesystem | `rm -rf <abs-path-not-under-/tmp>`, `rm -rf .` from outside a known build dir | Recursive deletion of non-scratch content has no general undo | -| Git history | `git reset --hard`, `git checkout -- <file>`, `git clean -fd`, `git stash drop`, `git stash clear` | Destroys uncommitted or stashed work that no other tool tracks | -| Git remote | `git push --force` (without `--force-with-lease`), `git branch -D`, `git tag -d <pushed-tag>` | Rewrites or deletes shared history | -| Database | `DROP DATABASE`, `DROP TABLE`, `TRUNCATE`, `DELETE` without a `WHERE` clause | Schema-level or unbounded data destruction | -| Container/k8s | `kubectl delete namespace`, `kubectl delete --all`, `helm uninstall`, `docker system prune -a` | Sweeps live workloads or shared caches | -| Cloud / IaC | `terraform destroy`, `aws s3 rb --force`, `gcloud projects delete` | Tears down infrastructure that humans co-own | - -Pack additional families behind opt-in flags so a CLI-only repo never loads database or k8s rules. - -## Allowlist and override flow - -Every pattern needs an escape hatch that records the override decision; otherwise operators silently disable the guard entirely. Implement three layers, evaluated highest to lowest priority: - -1. **Project allowlist** — a checked-in file (e.g. `.agents/destructive-allowlist.toml`) listing rule IDs and optional path scopes that this repo permanently accepts. Reviewable in PRs. -2. **User allowlist** — `~/.config/<guard>/allowlist.toml` for per-operator habits (cleaning a personal Docker cache, etc.). -3. **One-shot override code** — when a block fires, the guard prints a short cryptographic code bound to the exact command + working directory + a short TTL (e.g. 24 h, single use). The human, not the agent, runs `<guard> allow-once <code>` to grant the next attempt. - -The one-shot path is load-bearing. It keeps the agent honest (the code is not predictable from context) and produces an audit log entry per override. - -## Confirm thresholds - -Make the strictness configurable so the same binary can run in interactive, CI, and unattended-swarm contexts: - -```toml -[thresholds] -mode = "block" # "block" | "warn" | "log-only" -require_override_for = ["filesystem", "git-history", "database"] -auto_allow_for = ["filesystem.rm-under-build-dir"] -warn_for = ["git-remote.force-with-lease"] -``` - -Defaults: block on the high-blast-radius families, warn on near-equivalents that have a recoverable variant, log-only for purely informational rules. CI pipelines typically tighten to `mode = "block"` with a smaller allowlist; an interactive operator may relax to `warn` while pairing. - -## PreToolUse hook integration - -The integration mirrors `edit-scope-guard.sh`: - -- **Trigger:** PreToolUse on `Bash` (Claude) or `shell` / `apply_patch` (Codex). -- **Input:** harness-supplied JSON on stdin with `tool.params.command`. -- **Pipeline:** quick-reject screen → context sanitization → normalization → allowlist check → pattern match. -- **Deny output:** non-zero exit with a structured stderr reason — rule ID, family, suggested safer variant, and the one-shot override code. The harness converts that into a tool-use refusal the model can read. -- **Allow output:** exit 0, no stdout. Side-effect-free for the common case. - -Performance budget matters because the hook runs on every Bash call. Target sub-millisecond steady state, with a hard fail-open ceiling (e.g. 200 ms) so a wedged guard never stalls the swarm. - -## Failure modes the guard must handle - -- **Fail-closed on a confirmed match.** Pattern hits → reject, even if the override file is unreadable. -- **Fail-open on infrastructure error.** Missing config, malformed JSON, panic in the matcher → exit 0 with a stderr warning. Same defensive default as `edit-scope-guard.sh`. -- **Fail-open on timeout.** Anything past the latency ceiling skips the rest of the pipeline. -- **Heredoc and inline scripts.** `bash -c '...'`, `python -c '...'`, and `<<EOF` bodies must be extracted and rescanned; otherwise a one-line wrapper bypasses every rule. -- **Quoted path normalization.** `rm -rf "/var/log/"` and `rm -rf /var/log` should hit the same rule. - -## Composing with `/scope` - -Recommended wiring for a swarm wave: - -1. `/scope freeze <dirs>` to bound the edit surface. -2. Enable the destructive-command guard with the families relevant to this repo (filesystem + git-history is a sane minimum). -3. Add project-specific allowlist entries for routine safe deletions (e.g. `rm -rf ./build`, `rm -rf ./.next`). -4. Run the wave. Treat any block as a checkpoint, not an error: pick the safer variant from the rule's suggestion field, or escalate to the human for an `allow-once`. -5. After the wave, `/scope unfreeze` and let the destructive-command guard stay loaded — its overhead is negligible and the override audit log compounds. - -The two guards do not need to share state, but they should share the same fail-open posture so a hook outage never silently disables both lanes at once. - ---- -> Pattern adopted from `dcg` (ACFS skill corpus). Methodology only — no verbatim text. diff --git a/skills-codex/scope/references/lock-file-format.md b/skills-codex/scope/references/lock-file-format.md deleted file mode 100644 index 4d331a8fa..000000000 --- a/skills-codex/scope/references/lock-file-format.md +++ /dev/null @@ -1,43 +0,0 @@ -# `.agents/scope.lock` — Format Reference - -The scope lock file declares which repo-relative directory prefixes are currently in scope for editing. The PreToolUse hook `hooks/edit-scope-guard.sh` consults it on every `Edit`, `Write`, and `Bash` tool call. - -## Schema (v1) - -```json -{ - "schema_version": 1, - "frozen_dirs": ["cli/cmd/ao/", "skills/scope/"], - "acquired_at": "2026-05-01T19:30:00Z", - "acquired_by": "<session-id-or-pid>" -} -``` - -| Field | Type | Required | Notes | -|---|---|---|---| -| `schema_version` | integer | yes | Currently `1`. Hook treats unknown versions as fail-open. | -| `frozen_dirs` | array of strings | yes | Repo-relative directory prefixes. Trailing slash optional but conventional. Empty array means "no enforcement". | -| `acquired_at` | string (ISO-8601) | yes | UTC, RFC 3339. Updated on every successful `freeze` / `unfreeze`. | -| `acquired_by` | string | yes | Session id, PID, or human-supplied label. Used for diagnostic messages only. | - -## Atomicity guarantee - -Writes go through `cli/internal/llmwiki/scope_guard.go:SafeAtomicWrite`, which writes to a temp file in the same directory and `rename(2)`s into place. Readers either see the previous JSON or the new JSON, never a torn document. Concurrent writers converge to last-writer-wins. - -## Hook behavior - -`hooks/edit-scope-guard.sh` reads the file with these rules: - -- **File missing or empty:** exit 0 (allow). The lock is opt-in. -- **JSON parse fails:** exit 0 (fail-open). Log warning to stderr. -- **`frozen_dirs` empty:** exit 0 (allow). -- **Target path under any `frozen_dirs[i]`:** exit 0 (allow). -- **Target path outside every `frozen_dirs[i]`:** exit 2 with structured stderr reason `edit-scope-guard: <path> outside frozen scope <frozen-dirs>`. -- **Tool input malformed (missing `tool.params.file_path` AND `tool.params.command`):** exit 0 (nothing to check). - -Path comparison uses prefix match on the repo-relative path. Trailing slashes in `frozen_dirs` entries are normalized away before comparison. - -## Forward compatibility - -- `schema_version` future bumps will be additive. The hook will continue to honor v1 fields. -- New optional fields (e.g., `expires_at`, `owner_session`) may be added without breaking the contract. diff --git a/skills-codex/scope/references/scope.feature b/skills-codex/scope/references/scope.feature deleted file mode 100644 index 74a4eb96b..000000000 --- a/skills-codex/scope/references/scope.feature +++ /dev/null @@ -1,32 +0,0 @@ -# Executable spec for the /scope skill — edit-scope guardrail (BC5 Runtime). -# /scope declares which directories are in scope for the current work session and -# hard-blocks edits outside them via a PreToolUse hook, so a session cannot drift -# into files it never claimed. Hexagon: driven-adapter; consumes: a declared -# directory set; produces: scope/lock state + blocked-edit reasons on stderr. (soc-qk4b) - -Feature: Scope hard-blocks edits outside the declared directories - As an agent working a bounded change - I want edits confined to directories I declared in scope - So that a session cannot silently modify files it never claimed - - Background: - Given a work session that can declare an in-scope directory set - - Scenario: Declaring scope records the allowed directories - When /scope declares one or more directories - Then those directories become the in-scope set and the lock state reflects them - - Scenario: An edit inside scope proceeds - Given a declared in-scope directory - When a file inside it is edited - Then the edit is allowed - - Scenario: An edit outside scope is hard-blocked with a reason - Given a declared scope - When a file outside the in-scope set is edited - Then the PreToolUse hook blocks the edit and reports the blocked-edit reason on stderr - - Scenario: Scope state is reportable and releasable - When /scope is queried - Then it reports the current scope and lock state - And releasing scope removes the block on out-of-scope edits diff --git a/skills-codex/scope/scripts/validate.sh b/skills-codex/scope/scripts/validate.sh index b7980add6..5f282269d 100755 --- a/skills-codex/scope/scripts/validate.sh +++ b/skills-codex/scope/scripts/validate.sh @@ -1,90 +1,14 @@ #!/usr/bin/env bash set -euo pipefail - SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -SKILL_MD="$SKILL_DIR/SKILL.md" -PASS=0 -FAIL=0 +REPO_ROOT="$(cd "$SKILL_DIR/../.." && pwd)" -record() { - local label="$1" - shift - if "$@"; then - echo "PASS: $label" - PASS=$((PASS + 1)) - else - echo "FAIL: $label" - FAIL=$((FAIL + 1)) - fi -} +bash "$REPO_ROOT/skills/heal-skill/scripts/heal.sh" --check --strict "$SKILL_DIR" -validate_contract() { - local skill_md="$1" - [[ "$(awk '/^---$/{n++;next} n==2 && /^## /{print;exit}' "$skill_md")" == "## Critical Constraints" ]] && - grep -Fq 'WARN|FAIL|REFUTED -> AUTO-REDO' "$skill_md" && - grep -Fq 'BREAKER -> HOLD -> ONE-HELPER' "$skill_md" && - grep -Fq 'HELPER-UNSTUCK -> AUTO-REDO' "$skill_md" && - grep -Fq 'HELPER-ESCALATE -> HUMAN' "$skill_md" && - grep -Fq 'REFUSAL-LANE|EXPLICIT-JUDGMENT|EXHAUSTED-BUDGET -> HUMAN' "$skill_md" && - grep -Fq '**Artifact directory:**' "$skill_md" && - grep -Fq '**Filename convention:**' "$skill_md" && - grep -Fq '**Serialization/schema format:**' "$skill_md" && - grep -Fq '**Validator command:**' "$skill_md" && - grep -Fq '**Downstream handoff:**' "$skill_md" && - grep -Fq '## Quality Checklist' "$skill_md" -} - -validate_status() { - jq -e '.schema_version==1 and (.frozen_dirs|type=="array" and all(.[]; type=="string" and length>0)) and (.acquired_at|type=="string" and length>0) and (.acquired_by|type=="string")' >/dev/null -} - -roundtrip_status() { - AO_SCOPE_LOCK="$1" "$2" scope status --json | validate_status -} - -roundtrip_clear() { - AO_SCOPE_LOCK="$1" "$2" scope status --json | jq -e '.schema_version==1 and .frozen_dirs==[]' >/dev/null -} - -record "scope contract is complete" validate_contract "$SKILL_MD" -record "scope expansion requires explicit judgment" grep -Fq 'explicit scope-expansion judgment' "$SKILL_MD" -record "scope uses current agent and local shell" grep -Fq 'current agent and local shell' "$SKILL_MD" - -pawl_fixture="$(mktemp)" -output_fixture="$(mktemp)" -lock_fixture="$(mktemp)" -rm -f "$lock_fixture" -trap 'rm -f "$pawl_fixture" "$output_fixture" "$lock_fixture"' EXIT -awk '!/HELPER-UNSTUCK -> AUTO-REDO/' "$SKILL_MD" >"$pawl_fixture" -awk '!/\*\*Validator command:\*\*/' "$SKILL_MD" >"$output_fixture" - -if validate_contract "$pawl_fixture"; then - echo "FAIL: deletion fixture rejects missing pawl transition" - FAIL=$((FAIL + 1)) -else - echo "PASS: deletion fixture rejects missing pawl transition" - PASS=$((PASS + 1)) +if rg -n 'scope\.lock|AO_SCOPE_LOCK|PreToolUse|ao scope|freeze|unfreeze|git (commit|push)|ao land' \ + "$SKILL_DIR/SKILL.md"; then + echo "scope validate: mutable scope authority remains" >&2 + exit 1 fi -if validate_contract "$output_fixture"; then - echo "FAIL: deletion fixture rejects incomplete output handoff" - FAIL=$((FAIL + 1)) -else - echo "PASS: deletion fixture rejects incomplete output handoff" - PASS=$((PASS + 1)) -fi - -AO_BIN="${AO_BIN:-$(command -v ao || true)}" -if [[ -z "$AO_BIN" ]]; then - echo "FAIL: ao binary is available for scope round-trip" - FAIL=$((FAIL + 1)) -else - AO_SCOPE_LOCK="$lock_fixture" "$AO_BIN" scope freeze skills/scope >/dev/null - record "ao scope freeze/status round-trip is valid" roundtrip_status "$lock_fixture" "$AO_BIN" - AO_SCOPE_LOCK="$lock_fixture" "$AO_BIN" scope unfreeze >/dev/null - record "ao scope unfreeze round-trip clears scope" roundtrip_clear "$lock_fixture" "$AO_BIN" -fi - -echo -echo "Results: $PASS passed, $FAIL failed" -(( FAIL == 0 )) +echo "scope validate: PASS" diff --git a/skills-codex/security/.agentops-generated.json b/skills-codex/security/.agentops-generated.json index 178f6979a..166b99757 100644 --- a/skills-codex/security/.agentops-generated.json +++ b/skills-codex/security/.agentops-generated.json @@ -2,6 +2,6 @@ "generator": "codex-sync", "source_skill": "skills/security", "layout": "modular", - "source_hash": "416e10d873feb9188062aca0fd1ec917fb1ad6f9f44fe0cee51479c7194b12ff", - "generated_hash": "4b156a7aa16d8c0c9fff20af27526f26cc859bb74bfb71e2a9daa28fb4d1e1b4" + "source_hash": "d161054b328407c11083aa8c5871a63041129bccbf4b756aa13ce0a830a418a5", + "generated_hash": "ff669949792903f40c31fb78836cdab95462fe7038fb156874a764b70e17ed10" } diff --git a/skills-codex/security/SKILL.md b/skills-codex/security/SKILL.md index 628435ca0..63974522a 100644 --- a/skills-codex/security/SKILL.md +++ b/skills-codex/security/SKILL.md @@ -1,12 +1,12 @@ --- name: security -description: Run repository security scans for +description: Run authorized repository security scans for --- # Security Skill -> **Purpose:** Run repeatable security checks across code, scripts, release gates, authorized binaries, and repo-managed prompt surfaces. +> **Purpose:** Run repeatable security checks across code, scripts, authorized binaries, and repo-managed prompt surfaces. -Use this skill for deterministic pre-merge/release validation, scheduled checks, authorized binary assurance, dependency risk, secrets, or offline prompt-surface redteam. +Use this skill for a caller-requested repository scan, authorized binary assurance, dependency risk, secrets, or offline prompt-surface redteam. ## Critical Constraints @@ -39,7 +39,7 @@ scripts/security-gate.sh --mode quick **Checkpoint:** preserve the exit code and verify the reported `security-gate-summary.json` exists and parses before triage. -### 2) Full or release gate +### 2) Full scan Run: @@ -47,18 +47,18 @@ Run: scripts/security-gate.sh --mode full ``` -Add `--require-tools` when skipped scanners would invalidate the assurance claim. **Checkpoint:** do not authorize promotion until the full artifact passes the output validator and the process exits zero. +Add `--require-tools` when skipped scanners would invalidate the assurance claim. **Checkpoint:** report the result as incomplete unless the selected artifact validator and process both succeed. ### 3) Scheduled gate Scheduled automation runs the full gate against the intended branch and retains its artifact directory. A failing scheduled run creates actionable tracked work; AgentOps itself does not supply the scheduler. -### 4) Triage and re-run +### 4) Triage 1. Open the latest artifact and identify scanner, severity, file, and coverage gaps. 2. Reproduce the finding with the narrowest safe command. -3. For authorized remediation, fix critical/high findings; otherwise report them with owner and next action. -4. Re-run the same gate. Do not downgrade, suppress, or update a baseline merely to pass. +3. Rank concrete findings and preserve coverage gaps. +4. Stop. Remediation, risk acceptance, and any later scan are new caller decisions. Do not downgrade, suppress, or update a baseline merely to pass. ## Output Specification @@ -78,9 +78,9 @@ an owner, next action, approval, release, or retry decision. - [ ] Target and authorization boundary are explicit; collection stayed within them. - [ ] Scanner availability and skipped/error coverage are visible in the report. -- [ ] Findings include severity, location, reproducible evidence, and remediation/owner. +- [ ] Findings include severity, location, reproducible evidence, and bounded remediation guidance. - [ ] Artifacts contain no newly exposed secrets or unredacted sensitive payloads. -- [ ] Required gate and output validator both pass before promotion is declared safe. +- [ ] The report distinguishes a passing scan from permission to promote or release. - [ ] Suppressions, policy changes, baselines, and risk acceptance require explicit judgment. - [ ] The report stops after evidence and contains no continuation decision. @@ -98,7 +98,7 @@ For a bounded suite smoke test, use an owned binary and a temporary output direc ## Examples - `$security` — run the quick repository gate, validate its summary, and report coverage/findings. -- `$security --release` — run the full gate, preserve artifacts, and block promotion until the verdict is green. +- `$security --full` — run the full scan once, preserve artifacts, and report coverage and findings. - `$security run --binary "$(command -v ao)" --out-dir .tmp/security-suite/ao-current` — capture an authorized binary baseline via the composable suite. - `$security collect-redteam --repo-root .` — run the offline attack pack over repo-owned control surfaces. diff --git a/skills-codex/security/prompt.md b/skills-codex/security/prompt.md index ea4639350..d80240d01 100644 --- a/skills-codex/security/prompt.md +++ b/skills-codex/security/prompt.md @@ -1,6 +1,6 @@ # security -Run repository security scans for vulnerabilities, dependency risk, secrets, and release gates. Triggers: "security", "run repository security scans for", "security skill". +Run authorized repository security scans for vulnerabilities, dependency risk, secrets, and binary policy. Triggers: "security", "run repository security scans for", "security skill". ## Instructions diff --git a/skills-codex/security/references/security-suite-runbook.md b/skills-codex/security/references/security-suite-runbook.md index 684e79fc6..529d65bfc 100644 --- a/skills-codex/security/references/security-suite-runbook.md +++ b/skills-codex/security/references/security-suite-runbook.md @@ -1,6 +1,6 @@ # Composable Security Suite Runbook -Use this reference for authorized binary assurance, baseline comparison, policy enforcement, and offline repo-surface redteam. The main `security` skill owns authorization, pawl recovery, and release decisions. +Use this reference for authorized binary assurance, baseline comparison, policy enforcement, and offline repo-surface redteam. The caller supplies authorization and owns every decision after the report. ## Primitive model diff --git a/skills-codex/security/scripts/validate.sh b/skills-codex/security/scripts/validate.sh index c4cb57956..3c42d6519 100755 --- a/skills-codex/security/scripts/validate.sh +++ b/skills-codex/security/scripts/validate.sh @@ -1,61 +1,22 @@ #!/usr/bin/env bash set -euo pipefail -SKILL_DIR="$(cd "$(dirname "$0")/.." && pwd)" -PASS=0; FAIL=0 -check() { if bash -c "$2"; then echo "PASS: $1"; PASS=$((PASS + 1)); else echo "FAIL: $1"; FAIL=$((FAIL + 1)); fi; } -validate_contract() { - local skill_md="$1" - [[ "$(awk '/^---$/{n++;next} n==2 && /^## /{print;exit}' "$skill_md")" == "## Critical Constraints" ]] && - grep -Fq 'WARN|FAIL|REFUTED -> AUTO-REDO' "$skill_md" && - grep -Fq 'BREAKER -> HOLD -> ONE-HELPER' "$skill_md" && - grep -Fq 'HELPER-UNSTUCK -> AUTO-REDO' "$skill_md" && - grep -Fq 'HELPER-ESCALATE -> HUMAN' "$skill_md" && - grep -Fq 'REFUSAL-LANE|EXPLICIT-JUDGMENT|EXHAUSTED-BUDGET -> HUMAN' "$skill_md" && - grep -Fq '**Artifact directory:**' "$skill_md" && - grep -Fq '**Filename convention:**' "$skill_md" && - grep -Fq '**Serialization/schema format:**' "$skill_md" && - grep -Fq '**Validator command:**' "$skill_md" && - grep -Fq '**Downstream handoff:**' "$skill_md" && - grep -Fq '## Quality Checklist' "$skill_md" && - grep -Fq '**Checkpoint:**' "$skill_md" -} -export -f validate_contract +SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +SKILL="$SKILL_DIR/SKILL.md" -check "SKILL.md exists" "[ -f '$SKILL_DIR/SKILL.md' ]" -check "SKILL.md has YAML frontmatter" "head -1 '$SKILL_DIR/SKILL.md' | grep -q '^---$'" -check "name is security" "grep -q '^name: security' '$SKILL_DIR/SKILL.md'" -check "references policy exists" "[ -f '$SKILL_DIR/references/policy-example.json' ]" -check "references redteam pack exists" "[ -f '$SKILL_DIR/references/agentops-redteam-pack.json' ]" -check "security_suite.py exists" "[ -x '$SKILL_DIR/scripts/security_suite.py' ]" -check "security_suite.py compiles" "python3 -m py_compile '$SKILL_DIR/scripts/security_suite.py'" -check "prompt_redteam.py exists" "[ -x '$SKILL_DIR/scripts/prompt_redteam.py' ]" -check "prompt_redteam.py compiles" "python3 -m py_compile '$SKILL_DIR/scripts/prompt_redteam.py'" -check "policy JSON valid" "python3 -c \"import json, pathlib; json.loads(pathlib.Path('$SKILL_DIR/references/policy-example.json').read_text()); print('ok')\"" -check "redteam pack JSON valid" "python3 -c \"import json, pathlib; json.loads(pathlib.Path('$SKILL_DIR/references/agentops-redteam-pack.json').read_text()); print('ok')\"" -check "suite runbook exists" "[ -s '$SKILL_DIR/references/security-suite-runbook.md' ]" -check "security contract is complete" "validate_contract '$SKILL_DIR/SKILL.md'" +[[ -s "$SKILL" ]] +grep -q '^name: security$' "$SKILL" +grep -q '^ effects: \[\]$' "$SKILL" +[[ "$(awk '/^---$/{n++;next} n==2 && /^## /{print;exit}' "$SKILL")" == "## Critical Constraints" ]] +grep -Fq '**Artifact directory:**' "$SKILL" +grep -Fq '**Validator command:**' "$SKILL" +grep -Fq 'report stops after evidence' "$SKILL" +! grep -Eiq 'AUTO-REDO|ONE-HELPER|HELPER-ESCALATE|ao (pawl|land)|next_action' "$SKILL" -pawl_fixture="$(mktemp)" -output_fixture="$(mktemp)" -trap 'rm -f "$pawl_fixture" "$output_fixture"' EXIT -sed 's/HELPER-UNSTUCK -> AUTO-REDO/HELPER-UNSTUCK -> MANUAL/' "$SKILL_DIR/SKILL.md" >"$pawl_fixture" -awk '!/\*\*Validator command:\*\*/' "$SKILL_DIR/SKILL.md" >"$output_fixture" -if validate_contract "$pawl_fixture"; then - echo "FAIL: deletion fixture rejects missing pawl transition" - FAIL=$((FAIL + 1)) -else - echo "PASS: deletion fixture rejects missing pawl transition" - PASS=$((PASS + 1)) -fi -if validate_contract "$output_fixture"; then - echo "FAIL: deletion fixture rejects incomplete output handoff" - FAIL=$((FAIL + 1)) -else - echo "PASS: deletion fixture rejects incomplete output handoff" - PASS=$((PASS + 1)) -fi +[[ -s "$SKILL_DIR/references/policy-example.json" ]] +[[ -s "$SKILL_DIR/references/agentops-redteam-pack.json" ]] +[[ -s "$SKILL_DIR/references/security-suite-runbook.md" ]] +python3 -m py_compile "$SKILL_DIR/scripts/security_suite.py" "$SKILL_DIR/scripts/prompt_redteam.py" +python3 -c 'import json, pathlib, sys; root=pathlib.Path(sys.argv[1]); [json.loads((root/name).read_text()) for name in ("policy-example.json", "agentops-redteam-pack.json")]' "$SKILL_DIR/references" -echo "" -echo "Results: $PASS passed, $FAIL failed" -[ $FAIL -eq 0 ] && exit 0 || exit 1 +echo "security contract: PASS" diff --git a/skills-codex/shared/.agentops-generated.json b/skills-codex/shared/.agentops-generated.json index a4726f4c0..7b9345bf3 100644 --- a/skills-codex/shared/.agentops-generated.json +++ b/skills-codex/shared/.agentops-generated.json @@ -2,6 +2,6 @@ "generator": "codex-sync", "source_skill": "skills/shared", "layout": "modular", - "source_hash": "288d3ffbc377d3006c5f36dce69e6ab6d3a03e342ee9d94130bae9f70be1469c", - "generated_hash": "316fbbc329663d86ed76a0594f1d9f7885606962c14a68c3d9bfe9e62a7fa055" + "source_hash": "f72b609c231193fe9ec83727a8e40aa8cf96ce6ceb9c2e38c25d4189d062048b", + "generated_hash": "64cf69c5b1abae8b534e2980ece7bd620116bff904281fdf63c5b34df4345313" } diff --git a/skills-codex/shared/references/runtime-boundary.md b/skills-codex/shared/references/runtime-boundary.md deleted file mode 100644 index 600fdb33e..000000000 --- a/skills-codex/shared/references/runtime-boundary.md +++ /dev/null @@ -1,6 +0,0 @@ -# Runtime boundary - -Runtime adapters may start or observe explicitly requested sessions. Their -provider-specific attempts, reconnects, queues, and completion states remain -adapter evidence and cannot alter core Plan, Candidate, RPI, or verdict state. -Only Validate writes `verdict.v2`. diff --git a/skills-codex/skill-builder/.agentops-generated.json b/skills-codex/skill-builder/.agentops-generated.json index 9a986191b..8511ecc7f 100644 --- a/skills-codex/skill-builder/.agentops-generated.json +++ b/skills-codex/skill-builder/.agentops-generated.json @@ -2,6 +2,6 @@ "generator": "codex-sync", "source_skill": "skills/skill-builder", "layout": "modular", - "source_hash": "6998290cbb2ea82e83bc5b4add5658ce77da5b0d74c894f8ff2594917cb288f7", - "generated_hash": "6435bdf5179c9d243580d6cc2653f7f4141078cb712059229ff5c44d1220f821" + "source_hash": "bc5061fa0371cc38ce1c455b9a1b3cff5c958c88ea3fab1f0144b51f3b0195f3", + "generated_hash": "4cbb51335032c1a20084fec80fa50c388d33ff900eed090689dc5386dc2516b7" } diff --git a/skills-codex/skill-builder/SKILL.md b/skills-codex/skill-builder/SKILL.md index 2225e6f0d..47b60b064 100644 --- a/skills-codex/skill-builder/SKILL.md +++ b/skills-codex/skill-builder/SKILL.md @@ -1,195 +1,79 @@ --- name: skill-builder -description: Scaffold or absorb new SKILL.md files +description: Create a metadata-complete AgentOps skill --- -# $skill-builder — Scaffold or absorb a new SKILL.md +# $skill-builder — Create one skill source package -Materializes a new skill against the unified template at `references/skill-template.md` (extracted from anthropics/financial-services). Runs the heal-skill deep audit on the new skill as a self-check before declaring success. +Create one `skills/<slug>/` package, verify its structure, regenerate the +metadata-owned projections, and stop. The builder does not schedule work, +allocate writers, operate Git, promote learnings, or decide whether the new +skill should be invoked. -> **If unsure whether the work should be a skill, a Workflow, or an NTM swarm, run `$automation-shape-routing` first** — it is the front door that decides the shape and hands off to the right builder. +Before creating a new root, search `skills/*/SKILL.md` for an existing owner. +Extend an existing skill when it already owns the requested behavior. -> **Before creating, search for an existing owner.** `ms search` the proposed skill's trigger phrases (fast path when available — `command -v ms`, or the `mcp__ms__search` tool is attached; else grep `skills/**/SKILL.md` + `docs/SKILLS.md`). A strong hit means **extend that skill, don't create a new one** — this is one item of the fuller skill-admission checklist (bead age-7d3r). +## Inputs -## ⚠️ Critical Constraints +Choose exactly one mode: -- **Template is canonical.** All four modes produce SKILL.md files conforming to `references/skill-template.md`. Do not invent ad-hoc structures. **Why:** the heal-skill deep audit validates against this template; drift creates audit false-fails. -- **Self-audit is mandatory.** After every successful build, the build script invokes the heal-skill deep audit (`audit.sh` in `skills/heal-skill/`) against the new skill directory. A FAIL verdict aborts the build. **Why:** PR-002 (external validation gate) — the builder must not declare its own work complete. -- **Codex parity is day-1, not later.** `from-scratch`, `from-template`, and `absorb-external` modes must produce both `skills/<name>/SKILL.md` AND `skills-codex/<name>/SKILL.md` + `skills-codex/<name>/prompt.md`. **Why:** finding `2026-05-03-codex-skill-shape-is-dual-file` — codex SKILL.md uses slim frontmatter (no `skill_api_version`); prompt.md is mandatory; `audit-codex-parity.sh` is a content scanner that won't catch frontmatter drift. -- **Editing an EXISTING skill also needs a manual twin mirror.** When you change `skills/<name>/references/*.md` or `SKILL.md`, manually mirror the content into `skills-codex/<name>/` (runtime-native), THEN run `scripts/regen-codex-hashes.sh --only <name>`. `make regen-all` only refreshes the twin's *hash record*, not its prose — a green `✓ codex hashes` over a stale twin looks handled but isn't. Verify with a content diff (`grep -c <new-token>` on both copies), not the hash exit code. **Why:** finding `2026-06-16-codex-twin-content-not-auto-mirrored` (age-aqu/age-yxl) — regen made the marker self-consistent with a stale twin (0-vs-2 token divergence) and nothing complained. The parity gate now blocks an un-mirrored `references/**` edit, but the mirror is still a manual step. -- **250-line ceiling on new SKILL.md.** Use `references/` for overflow. **Why:** finding `f-2026-05-01-025` — every Skill() invocation reloads 5-15KB; multi-lifecycle sessions compound to 150-200KB+ pure scaffolding. -- **Clean-room factory inputs only.** When using lessons learned from external corpora, read [references/agentops-skill-factory.md](references/agentops-skill-factory.md) and use only AgentOps-owned summaries, scripts, and rubrics. **Why:** productization must improve structure without copying protected third-party skill content. -- **Real gate means exit code.** Validate with `heal-skill --check --strict <skill-dir>` and the heal-skill deep audit (`audit.sh`); never infer green from grep/regex output. **Why:** regex presence checks created false-greens during the 2026-06 scale build. -- **One skill directory = one writer.** Bulk builds fan out only when each worker owns a distinct new `skills/<name>/` plus `skills-codex/<name>/`; edits to existing skill dirs run in a later serial wave. **Why:** concurrent writers deleted untracked work and flipped HEAD mid-task. -- **Trust repository state, not subagent reports.** Before declaring success, inspect `git status`, generated hashes, final files, and gate exit codes. **Why:** sandbox-overlay and stale self-reports can claim work that never persisted. -- **Clean-room includes names.** Do not reuse exact third-party skill names; mint AgentOps-owned names before source skills, Codex mirrors, or wrappers are keyed. **Why:** provenance/IP safety applies to labels as well as prose and scripts. -- **Do not use the Workflow tool as the skill factory.** For scale authoring, use deterministic wave scripts or NTM/Agent Mail lanes with one worker per skill. **Why:** skill creation needs file ownership and durable git evidence, not opaque background self-reporting. +- `from-scratch <slug>` creates a blank source package. +- `from-template <slug> --like <existing-slug>` uses the existing skill only + for metadata defaults; it does not copy its prose. +- `absorb-external <slug> --from <path>` verifies the source exists, then + creates a clean-room blank package without copying names, prose, prompts, + scripts, or examples. -## Modes +The caller may set `SKILL_TIER`, `SKILL_DEPENDENCIES`, +`SKILL_CAPABILITIES`, and `SKILL_EFFECTS`. Values that represent lists must be +JSON arrays. -| Mode | Status | Description | -|------|--------|-------------| -| `from-scratch` | stable | Interactive scaffold from canonical template. Produces full skill skeleton + scripts/validate.sh + codex parity. | -| `from-template` | stable | `--like <existing-skill>` copies structure from a sibling skill, swaps domain-specific sections. | -| `absorb-external` | stable | Observes external package shape, synthesizes a clean-room AgentOps skill, and invokes `$converter` for Codex parity. | -| `from-pattern` | **alpha (passthrough)** | Delegates to `ao flywheel close-loop`. Outputs land at `.agents/knowledge/promoted/` per flywheel rules — they are NOT yet shaped as SKILL.md drafts. v2 will add skill-specific synthesis. Use `from-scratch` or `absorb-external` for SKILL.md output today. | +## Procedure -## Workflow +1. Run `scripts/build.sh` with one mode and one new slug. +2. Fill the generated placeholders with the skill's actual behavior. +3. Run `skills/heal-skill/scripts/heal.sh --check --strict skills/<slug>`. +4. Run `scripts/generate-skill-mesh.py` to derive the catalog, registry, + router, graph, maps, counts, and runtime image manifests from `SKILL.md` + metadata. +5. Run `scripts/codex-sync.sh --only <slug>` and + `scripts/regen-codex-hashes.sh --only <slug>` to derive the Codex twin. +6. Inspect the generated diff. Validation and delivery remain caller-owned. -### Phase 1: Mode dispatch +`build.sh` performs steps 1, 3, 4, and 5 once. It never retries or chooses a +next action. -`scripts/build.sh` reads `$1` and routes: +## Output -```bash -build.sh from-scratch <new-skill-name> # → init.sh --interactive -build.sh from-template <new-skill-name> --like council -build.sh absorb-external <new-skill-name> --from /path/to/SKILL.md -build.sh from-pattern # → ao flywheel close-loop +The source package contains: + +```text +skills/<slug>/ +├── SKILL.md +└── scripts/validate.sh ``` -**Checkpoint:** Confirm with user the new skill's `metadata.tier` and `metadata.dependencies` before generation. +The build report is `.agents/audits/<slug>-build.json` and conforms to +`schemas/build-report.json`. Generated inventories and runtime projections are +not additional sources of truth. -### Phase 2: Materialize from template +## Checks -`scripts/init.sh` reads `references/skill-template.md` (the canonical template section) and renders a SKILL.md skeleton with frontmatter pre-filled. For `from-template`, structure is copied from the source skill; section bodies are blanked and replaced with template stubs. +- The slug and frontmatter `name` match. +- Metadata declares `tier`, `dependencies`, `capabilities`, `effects`, + `canonical_status`, and `disposition`. +- Every hard dependency names a live skill. +- The generated package contains no Git, tracker, queue, retry, release, or + delivery behavior. +- External material is treated only as a signal that a clean-room skill may be + useful; its content is not copied. -For `absorb-external`, inspect only metadata and package shape, then synthesize -new AgentOps-owned content. Never copy external prose, prompts, scripts, -examples, names, or sentinel text into either generated runtime treatment. +## Failure behavior -**Checkpoint:** `heal-skill --check --strict skills/<new-name>` exits 0. - -### Phase 3: Codex parity - -`scripts/init.sh` invokes `$converter skills/<new-name> codex` to produce `skills-codex/<new-name>/{SKILL.md,prompt.md}`. Then trims `skill_api_version` from the codex SKILL.md (converter may preserve it). Asserts `prompt.md` exists. - -**Checkpoint:** `bash scripts/audit-codex-parity.sh` returns clean AND `grep -q "^skill_api_version:" skills-codex/<name>/SKILL.md` returns nothing. - -### Phase 4: Self-audit - -The build script tail invokes the heal-skill deep audit on `skills/<new-name>`. WARN is acceptable for v1 skills (e.g., `experimental` stability). FAIL aborts. - -**Checkpoint:** `audit_pass=true` in build report. - -### Phase 5: Factory score overlay - -For AgentOps skill upgrades, use the productization score as a patch selector, -not as a replacement for the deep audit: - -```bash -python3 skills/heal-skill/scripts/score_agentops_skill.py skills/<name> --markdown -``` - -Choose the smallest patch that improves the score while preserving the -canonical template and Codex parity constraints. - -### Phase 6: Scale factory discipline - -For more than one skill, run in ownership waves: - -1. Create-only wave: one worker per new skill directory. -2. Mutate wave: existing skill directories only after source creation settles. -3. Mirror/package wave: Codex mirrors and generated hashes after the canonical - source corpus is complete. - -Every wave ends with `git status`, `scripts/regen-all.sh --check`, and the -relevant target gates by exit code. If ownership overlaps, stop and rescope. - -## Output Specification - -**Artifact directory:** `.agents/audits/`. -**Filename convention:** `<skill>-build.json`. -**Serialization/schema format:** JSON matching `schemas/build-report.json`. -**Validator command:** `jq -e . .agents/audits/<skill>-build.json`. -**Downstream handoff:** consumed by heal-skill and the validation wave. - -**Files created (from-scratch mode):** - -``` -skills/<name>/ -├── SKILL.md (≤250 lines, full template spine) -├── scripts/ -│ └── validate.sh (self-validation per AgentOps convention) -└── references/ (externalize before the profile's 250-line kernel limit) -skills-codex/<name>/ -├── SKILL.md (slim frontmatter — no skill_api_version) -└── prompt.md (~10-20 line Execution Profile) -``` - -## Quality Rubric - -- [ ] All four modes produce skills that pass the heal-skill deep audit with PASS or WARN (not FAIL) -- [ ] `heal-skill --check --strict` exits 0 for every generated source and Codex skill directory -- [ ] Codex parity files exist and pass slim-frontmatter check -- [ ] Batch authoring has one writer per skill directory and validates persisted git state -- [ ] Clean-room review covers exact names as well as prose, scripts, and examples -- [ ] No SKILL.md exceeds 250 lines (overflow goes to `references/`) -- [ ] Build report JSON validates against `schemas/build-report.json` -- [ ] `from-pattern` mode prominently marked alpha/passthrough in user output - -## Examples - -**Create a new skill from scratch:** - -```bash -$skill-builder from-scratch hello-world -# → interactive prompt: tier? deps? primary deliverable? -# → writes skills/hello-world/SKILL.md + skills-codex/hello-world/{SKILL.md,prompt.md} -# → runs the heal-skill deep audit on the new skill -``` - -**Clone structure from an existing skill:** - -```bash -$skill-builder from-template my-new-skill --like council -# → mirrors council's section spine; substitutes new metadata -``` - -**Absorb a skill from anthropics/financial-services:** - -```bash -$skill-builder absorb-external dcf-helper \ - --from ~/dev/financial-services/plugins/vertical-plugins/financial-analysis/skills/dcf-model/SKILL.md -# → observes package shape and emits only clean-room AgentOps-owned content -``` - -## Troubleshooting - -| Problem | Cause | Solution | -|---------|-------|----------| -| Self-audit FAIL | Generated SKILL.md missing required Pass-2 check | Re-run with `--verbose`; inspect which check failed; usually `output-spec-explicit` or `trigger-clarity` | -| Codex parity drift | `$converter` preserved `skill_api_version` | `init.sh` runs `sed -i '/^skill_api_version:/d' skills-codex/<name>/SKILL.md`; verify with grep | -| SKILL.md > 250 lines | Mode generated too much inline content | Move section bodies to `references/<topic>.md`; reference inline as `[text](references/<topic>.md)` | -| `from-pattern` produces no SKILL.md | Expected behavior — passthrough only in v1 | Use `from-scratch` or `absorb-external` if you need a SKILL.md draft | - -## Corpus authoring health - -Skill selection is pure LLM reasoning over the `description` field, so a missing -trigger phrase is a skill that silently never fires. The per-skill auditor checks -this only as a WARN, so the gap accumulates. Audit the whole corpus at once: - -```bash -python3 skills/skill-builder/scripts/scan_descriptions.py skills # remediation report -python3 skills/skill-builder/scripts/scan_descriptions.py skills --strict # exit 1 on any miss -``` - -The scanner mirrors the deep audit's three-form trigger detection and adds a -suggested `Triggers:` stub per offender. See -[references/skill-authoring-standard.md](references/skill-authoring-standard.md) -for the full authoring doctrine and the best-practice-to-enforcement crosswalk. - -## See Also - -- [heal-skill](../heal-skill/SKILL.md) — structural hygiene (heal.sh) + companion deep-audit gate (audit.sh), invoked by build self-check -- [converter](../converter/SKILL.md) — produces codex parity artifacts -- [scaffold](../scaffold/SKILL.md) — scaffolds projects/components/CI (NOT skills) -- [curate](../postmortem/SKILL.md) — `--mode=forge` mines transcripts into learnings (different layer) +Any invalid input, structural failure, projection failure, or Codex sync +failure exits nonzero after one attempt. The caller decides whether to revise +or invoke the builder again. ## References -- [skill-conformance-profiles.yaml](references/skill-conformance-profiles.yaml) — authoritative `repo-runtime` semantics and severities - -- [references/skill-template.md](references/skill-template.md) — canonical SKILL.md template + auditor checklist + PRODUCT.md alignment -- [references/agentops-skill-factory.md](references/agentops-skill-factory.md) — clean-room factory workflow and productization rules -- [references/skill-authoring-standard.md](references/skill-authoring-standard.md) — clean-room best-practices doctrine + best-practice-to-enforcement crosswalk; backs the `scan_descriptions.py` trigger scanner -- [references/skill-builder.feature](references/skill-builder.feature) — Executable spec: mode dispatch, materialize from template, Codex parity bundle, self-audit + factory score (soc-qk4b) -- [references/hyper-extract-design-steals.md](references/hyper-extract-design-steals.md) — authoring rules stolen from Hyper-Extract: the WHAT-vs-HOW (schema vs guideline) contract, canonical `{from}\|{rel}\|{to}` identifier dedup-key form, and folded graph-designer/yaml-validator/template-optimizer patterns (age-bp1) +- [skill template](references/skill-template.md) +- [heal-skill](../heal-skill/SKILL.md) diff --git a/skills-codex/skill-builder/prompt.md b/skills-codex/skill-builder/prompt.md index a74d77337..fb0ee828d 100644 --- a/skills-codex/skill-builder/prompt.md +++ b/skills-codex/skill-builder/prompt.md @@ -1,6 +1,6 @@ # skill-builder -Scaffold or absorb new SKILL.md files against the unified AgentOps template. Triggers: "create a skill", "scaffold skill", "absorb external skill", "new skill". +Create a metadata-complete AgentOps skill source package and regenerate its derived projections. Triggers: "create a skill", "scaffold skill", "absorb external skill", "new skill". ## Instructions diff --git a/skills-codex/skill-builder/references/agentops-skill-factory.md b/skills-codex/skill-builder/references/agentops-skill-factory.md deleted file mode 100644 index c2b87e587..000000000 --- a/skills-codex/skill-builder/references/agentops-skill-factory.md +++ /dev/null @@ -1,69 +0,0 @@ -# AgentOps Skill Factory Productization - -This reference captures the local Codex `agentops-skill-factory` prototype as a -repo workflow. The goal is not to ship the local prototype verbatim; the goal is -to fold its proven behavior into the existing `skill-builder` and -heal-skill deep-audit pair. - -## Clean-room Inputs - -Use only AgentOps-owned artifacts: - -- `docs/reference/skill-quality-rubric.md` -- `skills/standards/references/skill-structure.md` -- `skills/standards/references/external-source-attribution.md` - -Do not copy protected third-party skill prose, prompts, scripts, names, or -examples into AgentOps skills. Extract reusable structure and quality signals -only. - -## Factory Loop - -1. Start with the built-in Codex skill-creator shape: a short `SKILL.md` kernel, - progressive disclosure through `references/`, reusable `scripts/`, optional - `assets/`, and validation evidence. -2. Score the target skill: - - ```bash - python3 skills/heal-skill/scripts/score_agentops_skill.py skills/<name> --markdown - ``` - -3. Pick the smallest score-improving patch, usually one of: - - add or link `SELF-TEST.md`; - - move bulky context into `references/`; - - add a focused validation script; - - add an output contract or explicit quality rubric; - - tighten trigger language in frontmatter and body. -4. Re-run the heal-skill deep audit (`audit.sh`), `heal-skill --check --strict`, and any target-specific - validation by exit code, not by grepping output text. -5. Mirror behavior into `skills-codex/<name>/` or - `skills-codex-overrides/<name>/` when the Codex runtime needs different - phrasing or execution instructions. - -## Scale Run Discipline - -When authoring multiple skills, protect file ownership before parallelism: - -- One skill equals one worker equals one source directory plus its Codex mirror. -- Run create-only work first; mutate existing skills only after the source corpus - is settled. -- Use deterministic scripts or NTM/Agent Mail lanes for batch work. Do not use - the Workflow tool as the skill factory. -- Trust `git status`, generated hashes, final file contents, and gate exit codes - over worker self-reports. -- Clean-room review includes exact names. Rename third-party-derived labels into - AgentOps-owned names before source skills, Codex mirrors, or wrappers are keyed. - -## Productization Rule - -Local prototype skills may guide the workflow, but PRs should land durable -AgentOps artifacts: - -- source skill changes under `skills/`; -- Codex runtime changes under `skills-codex/` or `skills-codex-overrides/`; -- reusable scoring/audit scripts under `skills/heal-skill/scripts/`; -- clean-room standards under `docs/reference/` and `skills/standards/`. - -Avoid adding a duplicate top-level skill when an existing AgentOps skill already -owns the domain. Extend `skill-builder`, `heal-skill`, `rpi`, or `evolve` -instead. diff --git a/skills-codex/skill-builder/references/hyper-extract-design-steals.md b/skills-codex/skill-builder/references/hyper-extract-design-steals.md deleted file mode 100644 index cd5ef6615..000000000 --- a/skills-codex/skill-builder/references/hyper-extract-design-steals.md +++ /dev/null @@ -1,104 +0,0 @@ -# Hyper-Extract design steals (authoring rules) - -Three reusable authoring contracts mined from Hyper-Extract's template-design -skills (`graph-designer`, `yaml-validator`, `template-optimizer`). They apply to -any AgentOps surface that pairs a machine-readable **schema** with human-written -**guidance** — SKILL.md frontmatter + body, extraction templates, record/graph -designers, and the corpus schemas under `schemas/`. - -## 1. The WHAT-vs-HOW authoring contract - -**Rule:** the **schema defines WHAT** (the fields, their types, their identity); -the **guideline defines HOW to do it well** (extraction strategy, quality bars, -creation conditions, common mistakes). A guideline that restates field -definitions is drift — the schema already owns that. Keep them disjoint. - -This is the single most load-bearing separation when a schema and a guideline -travel together: every duplicated field description in the guideline is a place -the two can silently diverge. Author the guideline as if the reader has already -read the schema. - -| Guideline SHOULD carry | Guideline should NOT carry (schema owns it) | -|------------------------|---------------------------------------------| -| Extraction strategy ("extract the valuable entities") | Field definitions ("`name` should be…") | -| Quality requirements ("keep naming consistent") | Type descriptions ("the `type` field is a str") | -| Creation conditions ("only when the text states it") | Reference requirements ("must point at `name`") | -| Common mistakes to avoid | Restated schema field descriptions | - -**Enforcement smell (steal from `template-optimizer` rule 4):** flag any -guideline/rules prose that repeats a field definition or a type description — -that is a schema-vs-guideline boundary violation, not guidance. Fix by deleting -the restated field text from the guideline; the schema is the single source of -WHAT. - -Applied to skill authoring: the SKILL.md frontmatter (`name`, `description`, -`hexagonal_role`, `consumes`/`produces`) is the schema = WHAT the skill is; the -SKILL.md body is the guideline = HOW to run it well. Do not restate frontmatter -fields prose-side; spend the body on strategy, quality bars, and footguns. - -## 2. Identifier dedup-key patterns (canonical dedup-key form) - -**Rule:** a dedup key (identity key) for a relationship/edge is a **template of -field references**, not a free-text string. The canonical form is: - -``` -'{from}|{rel}|{to}' -``` - -i.e. pipe-joined `{field}` placeholders that resolve against the record's own -fields. This replaces hand-written string `dedup_keys` (which drift from the -schema and can't be validated). Hyper-Extract's live forms: - -```yaml -identifiers: - entity_id: name - relation_id: '{source}|{relation_type}|{target}' # graph edge - relation_id: '{source}|{relation_type}|{target}|{event_date}' # temporal edge - relation_id: '{source}|{relation_type}|{target}|{location}' # spatial edge - relation_members: - source: ... - target: ... -``` - -Why a template, not a string: - -- **Validatable** — every `{placeholder}` must resolve to a declared field, so a - dedup key referencing a non-existent field is a catchable error (see the - `yaml-validator` identifier checklist). -- **Schema-anchored** — the dedup key is derived from WHAT (the fields), so it - cannot silently diverge from the schema the way a free string can. -- **Composable** — add a dimension (time, location) by appending another - `{field}`; the dedup key extends with the schema instead of being rewritten. - -**Adopt as canonical:** when a corpus/extraction surface needs an identity or -dedup key, express it as a `{field}|{field}|…` template over declared fields — -never an opaque string `dedup_key`. - -## 3. Folded patterns from the three Hyper-Extract design skills - -Useful, runtime-agnostic patterns lifted into our authoring doctrine: - -- **`graph-designer` → type-driven design + display labels.** Confirm the shape - first (record vs graph vs hypergraph vs temporal/spatial), then design fields - to the shape. Carry a human-readable display label derived from fields - (`'{name} ({category})'`) so output is legible without re-deriving identity. - Mirror for skills: pick the skill *mode* first, then author to that mode. -- **`yaml-validator` → tiered, ordered validation.** Validate in a fixed order - (syntax → structure → identifiers → field-quality) and grade findings by - level: **ERROR** (won't work — must fix), **WARNING** (quality risk — should - fix), **INFO** (recommended). This is the same tiering our heal-skill deep audit uses; - prefer ordered + level-graded checks over a flat pass/fail. -- **`template-optimizer` → information-density discipline.** Flag > 5 fields per - entity/relation for review; prioritize **Essential → Important → Optional** and - cut to the essential set. Standardize names to the concise canonical token - (`relation_type` → `type`, `event_date` → `time`). Apply the same three - optimization tiers to authoring fixes: **Auto-fix** (always-safe), **Suggest** - (needs review), **Review** (a design decision, leave to the author). This is - the field-count complement to our context-density rule. - -## Provenance - -Steals captured from the `Hyper-Extract` reference clone -(`hyperextract-skills/{graph-designer,yaml-validator,template-optimizer}`) under -bead age-bp1. Folded as additive authoring rules; no runtime behavior depends on -the Hyper-Extract code. diff --git a/skills-codex/skill-builder/references/skill-authoring-standard.md b/skills-codex/skill-builder/references/skill-authoring-standard.md deleted file mode 100644 index 2075a96ce..000000000 --- a/skills-codex/skill-builder/references/skill-authoring-standard.md +++ /dev/null @@ -1,136 +0,0 @@ -# AgentOps Skill Authoring Standard - -Clean-room distillation of the broadly accepted SKILL.md best practices -(Anthropic Agent Skills guidance and the wider community consensus), restated in -AgentOps's own words and cross-walked to what AgentOps tooling *actually* -enforces. This is the doctrine layer behind `skill-builder`, the heal-skill deep audit, -and `heal-skill` — read it before authoring or absorbing a skill. - -This file contains only AgentOps-owned summaries and rules. It does not copy any -third-party skill content (see [agentops-skill-factory.md](agentops-skill-factory.md)). -Executable values come only from -[skill-conformance-profiles.yaml](skill-conformance-profiles.yaml), using the -`repo-runtime` profile by default. - -## Contents - -- [Mental model: three loading levels](#mental-model-three-loading-levels) -- [The description is the trigger mechanism](#the-description-is-the-trigger-mechanism) -- [Naming (AgentOps house style)](#naming-agentops-house-style) -- [Progressive disclosure](#progressive-disclosure) -- [Degrees of freedom](#degrees-of-freedom) -- [Anti-patterns](#anti-patterns) -- [Crosswalk: best practice to AgentOps enforcement](#crosswalk-best-practice-to-agentops-enforcement) - -## Mental model: three loading levels - -A skill costs context in three tiers, and good authoring minimizes the lower -tiers: - -1. **Metadata** (`name` + `description`) — always in the system prompt. ~100 - tokens. This is the only thing the runtime sees when *deciding* whether to - load the skill. -2. **SKILL.md body** — loaded only after the skill triggers. Keep it under the - 250-line ceiling; push overflow into `references/`. -3. **Bundled `references/` and `scripts/`** — read or executed only when the - body points to them. Effectively unlimited, because they load on demand. - -The implication that drives every rule below: the description does all the -selection work, and the body is read far less often than authors assume. - -## The description is the trigger mechanism - -Skill selection is **pure LLM reasoning over descriptions** — no embeddings, no -keyword index. A description without explicit trigger phrases is a skill that -silently never fires. This is the single largest latent gap in the corpus: most -skills score 1/3 on trigger quality because they describe *what* the skill does -but never *when* to invoke it. - -Write descriptions that are: - -- **Third person.** "Scaffolds a new skill", not "I help you scaffold". -- **Specific.** Name the artifact and the domain, not "helps with skills". -- **Trigger-bearing.** Include the phrases a user or agent would actually say. - -The deep audit (`heal-skill/scripts/audit.sh`) accepts a trigger in any of three forms; satisfy at least one: - -- **Block marker** — `description: |` or `description: >` whose value contains - a profile-accepted `Use when:` / `Triggers:` marker. -- **Inline marker** — a `Triggers:` or `Use when:` clause in the - single-line description (the most common AgentOps form). -- **Metadata list** — a `metadata.triggers:` YAML list meeting the profile's - declared minimum cardinality. - -Audit the whole corpus for this gap at any time: - -```bash -python3 skills/skill-builder/scripts/scan_descriptions.py skills -python3 skills/skill-builder/scripts/scan_descriptions.py skills --strict # exit 1 on any miss -python3 skills/skill-builder/scripts/scan_descriptions.py skills --json # robot mode -``` - -The scanner applies the exact detection logic of `heal-skill/scripts/audit.sh`, -so its verdict never contradicts the per-skill auditor; it adds the prioritized -remediation list and a suggested `Triggers:` stub the auditor does not provide. - -## Naming (AgentOps house style) - -The external standard prefers gerund names (`processing-pdfs`). **AgentOps -deliberately diverges**: skills are named for the noun or verb of the workflow -(`skill-builder`, `bug-hunt`, `crank`, `council`) so they read as commands in -the `/skill` slash-menu. This is an intentional, documented deviation — keep new -skills consistent with the existing corpus rather than introducing gerunds. - -## Progressive disclosure - -- Keep `references/` **one level deep**. No chains - (`SKILL.md -> a.md -> b.md`); the runtime may partial-read a deep file. -- Reference files over ~100 lines should open with a short table of contents. -- A repo-runtime SKILL.md may contain at most 250 lines. Line 251 receives the - profile-declared `references-modularization` finding even when references exist. -- Distinguish **execute** from **read** when pointing at a script: "Run - `python scripts/x.py`" versus "See `scripts/x.py` for the algorithm". - -## Degrees of freedom - -Match instruction specificity to how fragile the task is: - -- **High freedom** — multiple valid approaches (e.g. review guidelines). Give - direction, not steps. -- **Medium freedom** — a preferred pattern with acceptable variation (e.g. - report templates). Give a default and an escape hatch. -- **Low freedom** — error-prone, consistency-critical (e.g. a migration - command). Give the exact invocation and forbid alternatives. - -## Anti-patterns - -- **Multiple options, no default.** Pick one tool, name it, then offer the - escape hatch ("for scanned PDFs, use X instead"). -- **Human docs in the skill.** No README / CHANGELOG / install guide; skills are - for agents. -- **Inconsistent terminology.** Choose one term for a concept and use it - throughout. -- **Time-sensitive claims.** "Currently" and dated facts rot; move volatile - detail to an "old patterns" section or a generated artifact. -- **Hardcoded absolute paths.** Use repo-relative paths so the skill is portable. -- **Vague descriptions.** The fastest way to ship a skill that never triggers. - -## Crosswalk: best practice to AgentOps enforcement - -| Best practice | AgentOps enforcement | Gate | -|---------------|----------------------|------| -| Description carries triggers | `description-has-triggers`, `trigger-clarity` | auditor WARN (corpus drift accumulates) | -| `name` matches directory | `heal.sh` NAME_MISMATCH | CI FAIL (`skills-integrity`) | -| `name` + `description` present | `heal.sh` MISSING_NAME / MISSING_DESC | CI FAIL | -| Output section defines all executable-handoff components from the profile | `output-spec-explicit` | profile severity | -| Constraints front-loaded with rationale | `constraints-frontloaded`, `rationale-present` | auditor WARN | -| References one level deep, linked | `heal.sh` UNLINKED_REF / DEAD_REF | CI FAIL / WARN | -| 250-line ceiling | `skill-builder` and deep audit reject/flag > 250 | profile-derived | -| Frontmatter schema valid | `validate-skill-schema.sh`, v2 frontmatter | CI FAIL | -| Dependencies resolve | dependency-resolution check | CI FAIL | -| Codex parity (dual-file) | manual `skills-codex/`, parity-drift audit | CI FAIL (semantic), manual mirror | -| Registry / catalog current | `generate-registry.sh`, `generate-skill-catalog.sh` | CI FAIL (registry) / advisory (catalog) | - -WARN-severity checks do not block a merge, which is exactly why trigger quality -drifted across the corpus. Treat a WARN as a real finding, not noise — run the -scanner and close the backlog incrementally. diff --git a/skills-codex/skill-builder/schemas/build-report.json b/skills-codex/skill-builder/schemas/build-report.json index dcb0fa57b..52627cb78 100644 --- a/skills-codex/skill-builder/schemas/build-report.json +++ b/skills-codex/skill-builder/schemas/build-report.json @@ -1,52 +1,25 @@ { - "$schema": "https://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "Skill Build Report", - "description": "Output contract for skill-builder. Reports what mode ran, which files were created, and whether the post-build self-audit passed.", "type": "object", - "required": ["mode", "skill_name", "profile_id", "files_created", "audit_pass"], + "required": ["mode", "skill_name", "files_created", "structure_check_pass"], "properties": { "mode": { "type": "string", - "enum": ["from-scratch", "from-template", "absorb-external", "from-pattern"], - "description": "Which builder mode produced this skill. from-pattern is alpha (passthrough to ao flywheel close-loop)." + "enum": ["from-scratch", "from-template", "absorb-external"] }, "skill_name": { "type": "string", - "pattern": "^[a-z][a-z0-9-]*$", - "description": "Lowercase-hyphen slug for the new skill (matches directory name)." - }, - "profile_id": { - "type": "string", - "description": "Selected authoritative skill-conformance profile ID." + "pattern": "^[a-z][a-z0-9-]*$" }, "files_created": { "type": "array", "items": {"type": "string"}, - "description": "Absolute or repo-relative paths of files written during this build." + "minItems": 2, + "uniqueItems": true }, - "audit_pass": { - "type": "boolean", - "description": "Did the immediate post-build self-audit (heal-skill deep audit) emit VERDICT: PASS or VERDICT: WARN? (False if FAIL.)" - }, - "audit_report_path": { - "type": "string", - "description": "Where the self-audit report was written (typically .agents/audits/<skill>-build.md)." - }, - "warnings": { - "type": "array", - "items": {"type": "string"}, - "description": "Non-fatal issues surfaced during build (e.g., template SHA pin mismatch, missing optional sections)." - }, - "source": { - "type": "object", - "description": "Mode-specific provenance.", - "properties": { - "external_path": {"type": "string", "description": "For absorb-external: source SKILL.md path"}, - "template_skill": {"type": "string", "description": "For from-template: skill copied from"}, - "flywheel_run_id": {"type": "string", "description": "For from-pattern: ao flywheel close-loop run id"} - }, - "additionalProperties": false - } + "structure_check_pass": {"type": "boolean"}, + "source_hint": {"type": "string"} }, "additionalProperties": false } diff --git a/skills-codex/skill-builder/scripts/build.sh b/skills-codex/skill-builder/scripts/build.sh index c1092d50c..462a6b90f 100755 --- a/skills-codex/skill-builder/scripts/build.sh +++ b/skills-codex/skill-builder/scripts/build.sh @@ -1,185 +1,54 @@ #!/usr/bin/env bash -# build.sh — skill-builder mode dispatcher -# Usage: -# build.sh from-scratch <skill-name> -# build.sh from-template <skill-name> --like <existing-skill> -# build.sh absorb-external <skill-name> --from <path-to-external-SKILL.md> -# build.sh from-pattern # alpha: passthrough to ao flywheel close-loop -# -# Always runs the heal-skill deep audit (absorbed from /skill-auditor) on the new skill as a self-check before declaring success. - +# Create, structurally check, and project one skill exactly once. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# SKILL_BUILDER_REPO_ROOT overrides root discovery for fixture-driven tests -# (see init.sh — same contract, mirrors HEAL_REPO_ROOT in heal.sh). REPO_ROOT="${SKILL_BUILDER_REPO_ROOT:-$(cd "$SCRIPT_DIR/../../.." && pwd)}" -INIT_SH="$SCRIPT_DIR/init.sh" -AUDITOR_SH="$REPO_ROOT/skills/heal-skill/scripts/audit.sh" -PROFILE_TOOL="$REPO_ROOT/skills/skill-builder/scripts/conformance_profile.py" -EXTERNAL_SOURCE="" - -profile_args=(--repo-root "$REPO_ROOT") -if [[ -n "${SKILL_CONFORMANCE_PROFILE_ID:-}" ]]; then - profile_args+=(--profile-id "$SKILL_CONFORMANCE_PROFILE_ID") -fi -if [[ ! -f "$PROFILE_TOOL" ]] || ! PROFILE_ID="$(python3 "$PROFILE_TOOL" "${profile_args[@]}")"; then - echo "[skill-builder] ERROR: profile configuration is unavailable or invalid" >&2 - exit 2 -fi -export SKILL_CONFORMANCE_PROFILE_ID="$PROFILE_ID" usage() { - cat <<EOF -Usage: - $0 from-scratch <skill-name> - $0 from-template <skill-name> --like <existing-skill> - $0 absorb-external <skill-name> --from <path-to-external-SKILL.md> - $0 from-pattern # alpha: passthrough to ao flywheel close-loop - -Modes: - from-scratch Interactive scaffold from canonical template - from-template Copy structure from a sibling skill - absorb-external Wrap an external SKILL.md in AgentOps frontmatter - from-pattern ALPHA — delegates to 'ao flywheel close-loop'. - Outputs at .agents/knowledge/promoted/, NOT shaped as SKILL.md drafts. - Use from-scratch or absorb-external for SKILL.md output today. + cat >&2 <<EOF +usage: + build.sh from-scratch <slug> + build.sh from-template <slug> --like <existing-slug> + build.sh absorb-external <slug> --from <path> EOF exit 2 } -[[ $# -lt 1 ]] && usage +[[ $# -ge 2 ]] || usage +mode="$1" +slug="$2" +shift 2 -MODE="$1" -shift - -case "$MODE" in - from-pattern) - # Alpha passthrough — explicitly documented in SKILL.md - echo "[skill-builder] from-pattern is ALPHA — delegating to 'ao flywheel close-loop'" - echo "[skill-builder] Output will NOT be a SKILL.md draft; it lands at .agents/knowledge/promoted/" - exec ao flywheel close-loop "$@" - ;; - - from-scratch) - [[ $# -lt 1 ]] && { echo "Error: from-scratch requires <skill-name>" >&2; usage; } - SKILL_NAME="$1"; shift - bash "$INIT_SH" --interactive "$SKILL_NAME" "$@" - ;; - - from-template) - [[ $# -lt 1 ]] && { echo "Error: from-template requires <skill-name>" >&2; usage; } - SKILL_NAME="$1"; shift - bash "$INIT_SH" --like-flag-mode "$SKILL_NAME" "$@" - ;; - - absorb-external) - [[ $# -lt 1 ]] && { echo "Error: absorb-external requires <skill-name>" >&2; usage; } - SKILL_NAME="$1"; shift - if [[ "${1:-}" == "--from" ]]; then - EXTERNAL_SOURCE="${2:-}" - fi - bash "$INIT_SH" --absorb "$SKILL_NAME" "$@" - ;; - - *) - echo "Error: unknown mode '$MODE'" >&2 - usage - ;; +case "$mode" in + from-scratch) init_mode=--scratch ;; + from-template) init_mode=--template ;; + absorb-external) init_mode=--external ;; + *) usage ;; esac -# Post-build self-audit (mandatory per Critical Constraints) -NEW_SKILL_DIR="$REPO_ROOT/skills/$SKILL_NAME" -if [[ ! -d "$NEW_SKILL_DIR" ]]; then - echo "[skill-builder] ERROR: expected $NEW_SKILL_DIR to exist after init.sh" >&2 +bash "$SCRIPT_DIR/init.sh" "$init_mode" "$slug" "$@" + +report="$REPO_ROOT/.agents/audits/${slug}-build.json" +if ! HEAL_REPO_ROOT="$REPO_ROOT" bash "$REPO_ROOT/skills/heal-skill/scripts/heal.sh" \ + --check --strict "$REPO_ROOT/skills/$slug"; then + echo "skill-builder: structural check failed" >&2 exit 1 fi -# The build report (written by init.sh) carries audit_pass=null as a pre-audit -# placeholder. Patch it to the REAL audit outcome here so the report records what -# actually happened and stays schema-valid (build-report.json requires -# audit_pass to be a boolean). Without this the report's audit_pass was always -# null — the original defect this fixes (age-fix-skill-factory-mcc). -BUILD_REPORT="$REPO_ROOT/.agents/audits/${SKILL_NAME}-build.json" +python3 "$REPO_ROOT/scripts/generate-skill-mesh.py" +bash "$REPO_ROOT/scripts/codex-sync.sh" --only "$slug" +bash "$REPO_ROOT/scripts/regen-codex-hashes.sh" --only "$slug" -# patch_audit_pass <true|false> — record the audit outcome in the build report. -# Never fails the build: a missing report or absent jq/python3 only warns. -patch_audit_pass() { - [[ -f "$BUILD_REPORT" ]] || return 0 - local val="$1" tmp - # Create the temp in the report's OWN directory so the mv is an atomic - # same-filesystem rename (a cross-device mv can degrade to a non-atomic copy - # and corrupt the report on failure). - tmp="$(mktemp "$(dirname "$BUILD_REPORT")/.audit-patch.XXXXXX")" || return 0 - if command -v jq >/dev/null 2>&1; then - # Fold the mv into the tested condition so a failed write only WARNs (never - # aborts the build under set -e). - if jq --argjson ap "$val" '.audit_pass = $ap' "$BUILD_REPORT" >"$tmp" 2>/dev/null \ - && mv "$tmp" "$BUILD_REPORT" 2>/dev/null; then - : - else - rm -f "$tmp"; echo "[skill-builder] WARN: could not patch audit_pass in $BUILD_REPORT" >&2 - fi - elif command -v python3 >/dev/null 2>&1; then - if python3 - "$BUILD_REPORT" "$val" >"$tmp" 2>/dev/null <<'PY' && mv "$tmp" "$BUILD_REPORT" 2>/dev/null -import json, sys -d = json.load(open(sys.argv[1])) -d["audit_pass"] = (sys.argv[2] == "true") -json.dump(d, sys.stdout, indent=2) +python3 - "$report" <<'PY' +import json +from pathlib import Path +import sys + +path = Path(sys.argv[1]) +payload = json.loads(path.read_text(encoding="utf-8")) +payload["structure_check_pass"] = True +path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8") PY - then - : - else - rm -f "$tmp"; echo "[skill-builder] WARN: could not patch audit_pass in $BUILD_REPORT" >&2 - fi - else - rm -f "$tmp"; echo "[skill-builder] WARN: no jq/python3 to record audit_pass in $BUILD_REPORT" >&2 - fi - return 0 -} -# The generated kernel and any external adoption must satisfy the selected -# profile before the auditor can declare the build successful. -if ! PROFILE_EVAL="$(python3 "$PROFILE_TOOL" --repo-root "$REPO_ROOT" \ - --profile-id "$PROFILE_ID" --audit-tsv "$NEW_SKILL_DIR/SKILL.md")"; then - patch_audit_pass false - exit 1 -fi -KERNEL_LIMIT="$(printf '%s\n' "$PROFILE_EVAL" | awk -F '\t' '$1 == "kernel_max_lines" {print $2}')" -KERNEL_LINES="$(printf '%s\n' "$PROFILE_EVAL" | awk -F '\t' '$1 == "line_count" {print $2}')" -if [[ -z "$KERNEL_LIMIT" || -z "$KERNEL_LINES" ]] || (( KERNEL_LINES > KERNEL_LIMIT )); then - patch_audit_pass false - echo "[skill-builder] ERROR: generated SKILL.md has ${KERNEL_LINES:-unknown} lines; profile $PROFILE_ID allows $KERNEL_LIMIT" >&2 - exit 1 -fi - -if [[ -n "$EXTERNAL_SOURCE" ]]; then - if ! python3 "$PROFILE_TOOL" --repo-root "$REPO_ROOT" \ - --profile-id "$PROFILE_ID" --verify-clean-room "$EXTERNAL_SOURCE" \ - --generated-dir "$NEW_SKILL_DIR" \ - --generated-dir "$REPO_ROOT/skills-codex/$SKILL_NAME" - then - patch_audit_pass false - echo "[skill-builder] ERROR: clean-room verification failed" >&2 - exit 1 - fi -fi - -if [[ -x "$AUDITOR_SH" ]]; then - echo "" - echo "[skill-builder] Running self-audit on $NEW_SKILL_DIR..." - if bash "$AUDITOR_SH" "$NEW_SKILL_DIR"; then - patch_audit_pass true - echo "[skill-builder] Self-audit PASS or WARN — build complete (audit_pass=true)" - else - # Record the failure before aborting so the report reflects reality. - patch_audit_pass false - echo "[skill-builder] Self-audit FAIL — build aborted (audit_pass=false)" >&2 - exit 1 - fi -else - # An unaudited build cannot claim a pass: record false rather than leaving the - # null placeholder (which would be schema-invalid and read as "audited"). - patch_audit_pass false - echo "[skill-builder] WARN: heal-skill audit script not found at $AUDITOR_SH; skipping self-audit (audit_pass=false)" >&2 -fi +echo "skill-builder: created and projected $slug" diff --git a/skills-codex/skill-builder/scripts/init.sh b/skills-codex/skill-builder/scripts/init.sh index fa16a9ba5..3032bd46e 100755 --- a/skills-codex/skill-builder/scripts/init.sh +++ b/skills-codex/skill-builder/scripts/init.sh @@ -1,301 +1,146 @@ #!/usr/bin/env bash -# init.sh — materialize a new skill from the canonical template -# Invoked by build.sh; not typically called directly. -# -# Usage: -# init.sh --interactive <skill-name> -# init.sh --like-flag-mode <skill-name> --like <source-skill> -# init.sh --absorb <skill-name> --from <path-to-external-SKILL.md> - +# Create one metadata-complete canonical skill source package. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# SKILL_BUILDER_REPO_ROOT overrides root discovery for fixture-driven tests -# (tests/integration/test_skill_builder.bats scaffolds into a scratch repo copy -# so no in-repo surface — skills/, the dispositions ledger, the codex catalog — -# is ever mutated by a test run). Mirrors HEAL_REPO_ROOT in heal.sh. Production -# derives the root from the script location. REPO_ROOT="${SKILL_BUILDER_REPO_ROOT:-$(cd "$SCRIPT_DIR/../../.." && pwd)}" -TEMPLATE_REF="$REPO_ROOT/skills/skill-builder/references/skill-template.md" -[[ -f "$TEMPLATE_REF" ]] || { echo "init.sh: missing $TEMPLATE_REF" >&2; exit 1; } +usage() { + echo "usage: init.sh --scratch|--template|--external <slug> [--like <slug>|--from <path>]" >&2 + exit 2 +} -MODE="${1:?usage: init.sh --interactive|--like-flag-mode|--absorb <name> [opts]}" -shift +[[ $# -ge 2 ]] || usage +mode="$1" +slug="$2" +shift 2 -SKILL_NAME="${1:?missing <skill-name>}" -shift +[[ "$slug" =~ ^[a-z][a-z0-9-]*$ ]] || { + echo "init.sh: slug must be lowercase-hyphen: $slug" >&2 + exit 2 +} -# Validate slug -[[ "$SKILL_NAME" =~ ^[a-z][a-z0-9-]*$ ]] || { - echo "init.sh: skill name '$SKILL_NAME' must be lowercase-hyphen (e.g. my-skill)" >&2 +source_hint="" +case "$mode" in + --scratch) + [[ $# -eq 0 ]] || usage + ;; + --template) + [[ $# -eq 2 && "$1" == "--like" ]] || usage + source_hint="$2" + [[ -f "$REPO_ROOT/skills/$source_hint/SKILL.md" ]] || { + echo "init.sh: unknown template skill: $source_hint" >&2 + exit 2 + } + ;; + --external) + [[ $# -eq 2 && "$1" == "--from" ]] || usage + source_hint="$2" + [[ -f "$source_hint" ]] || { + echo "init.sh: external source does not exist: $source_hint" >&2 + exit 2 + } + ;; + *) usage ;; +esac + +target="$REPO_ROOT/skills/$slug" +[[ ! -e "$target" ]] || { + echo "init.sh: target already exists: $target" >&2 exit 1 } -NEW_DIR="$REPO_ROOT/skills/$SKILL_NAME" -NEW_SKILL_MD="$NEW_DIR/SKILL.md" -[[ -e "$NEW_DIR" ]] && { echo "init.sh: $NEW_DIR already exists; aborting" >&2; exit 1; } +tier="${SKILL_TIER:-execution}" +dependencies="${SKILL_DEPENDENCIES:-[]}" +capabilities="${SKILL_CAPABILITIES:-[${slug//-/_}]}" +effects="${SKILL_EFFECTS:-[]}" -mkdir -p "$NEW_DIR/scripts" +python3 - "$dependencies" "$capabilities" "$effects" <<'PY' +import json +import sys +for value in sys.argv[1:]: + parsed = json.loads(value) + if not isinstance(parsed, list) or not all(isinstance(item, str) for item in parsed): + raise SystemExit("skill metadata lists must be JSON arrays of strings") +PY -# --- Per-mode population -------------------------------------------------- -case "$MODE" in - --interactive) - # Minimal non-blocking defaults; skip prompts in CI by reading env vars - TIER="${SKILL_TIER:-execution}" - DEPS="${SKILL_DEPS:-[]}" - INTENT_MODE="${SKILL_INTENT_MODE:-task}" - ;; +mkdir -p "$target/scripts" - --like-flag-mode) - LIKE_FLAG="${1:-}"; SOURCE_SKILL="${2:-}" - [[ "$LIKE_FLAG" == "--like" && -n "$SOURCE_SKILL" ]] || { - echo "init.sh --like-flag-mode requires '--like <source-skill>'" >&2 - exit 1 - } - SOURCE_DIR="$REPO_ROOT/skills/$SOURCE_SKILL" - [[ -f "$SOURCE_DIR/SKILL.md" ]] || { - echo "init.sh: source skill $SOURCE_DIR/SKILL.md not found" >&2 - exit 1 - } - # Extract frontmatter values from source for sane defaults - TIER="$(awk '/^---$/{n++;next} n==1 && /^[ ]+tier:/{print $2; exit}' "$SOURCE_DIR/SKILL.md")" - TIER="${TIER:-execution}" - DEPS="[]" - INTENT_MODE="$(awk '/^---$/{n++;next} n==1 && /^[ ]+mode:/{print $2; exit}' "$SOURCE_DIR/SKILL.md")" - INTENT_MODE="${INTENT_MODE:-task}" - ;; - - --absorb) - FROM_FLAG="${1:-}"; SOURCE_PATH="${2:-}" - [[ "$FROM_FLAG" == "--from" && -n "$SOURCE_PATH" ]] || { - echo "init.sh --absorb requires '--from <path-to-external-SKILL.md>'" >&2 - exit 1 - } - [[ -f "$SOURCE_PATH" ]] || { echo "init.sh: external SKILL.md not found at $SOURCE_PATH" >&2; exit 1; } - TIER="${SKILL_TIER:-execution}" - DEPS="[]" - INTENT_MODE="task" - ;; - - *) - echo "init.sh: unknown mode '$MODE'" >&2 - exit 2 - ;; -esac - -# --- Render frontmatter + skeleton --------------------------------------- -cat > "$NEW_SKILL_MD" <<EOF +cat >"$target/SKILL.md" <<EOF --- -name: $SKILL_NAME -description: | - <one-line: verb + object + domain> - - **Use when:** - - <Trigger 1> - - <Trigger 2> - - **Triggers:** "<trigger phrase 1>", "<trigger phrase 2>" - - **Not ideal for:** - - <Anti-scenario 1> +name: $slug +description: 'TODO: state the behavior and concrete trigger phrases for $slug.' skill_api_version: 1 -context: - window: fork - intent: - mode: $INTENT_MODE - sections: - exclude: [HISTORY] - intel_scope: topic +hexagonal_role: supporting +consumes: [] +produces: [] +context_rel: [] metadata: - tier: $TIER - dependencies: $DEPS + tier: $tier + dependencies: $dependencies + capabilities: $capabilities + effects: $effects + canonical_status: canonical + disposition: keep_specialist stability: experimental -output_contract: "TODO: path to schema or output description" --- -# /$SKILL_NAME — <Title matching slug> +# /$slug -<1-2 sentence purpose paragraph> +TODO: Explain the bounded behavior this skill provides. -## Overview +## Inputs -<What this skill does, why it matters, and when to use it> +TODO: List required inputs and explicit non-goals. -## ⚠️ Critical Constraints +## Procedure -- **Rule 1:** <constraint>. **Why:** <rationale> +1. TODO: Perform one bounded operation. +2. TODO: Check the output against the stated contract. +3. Report the result and stop. -## Workflow +## Output -### Phase 1: <name> +TODO: Define the artifact or response shape and how a caller checks it. -<instructions> +## Checks -**Checkpoint:** <what to confirm before next phase> +- The output satisfies the declared behavior. +- No undeclared side effect occurred. -## Output Specification +## Failure behavior -**Artifact directory:** \`.agents/out/$SKILL_NAME/\` -**Filename convention:** \`report.json\` -**Serialization/schema format:** JSON matching the declared output schema. -**Validator command:** \`jq -e . .agents/out/$SKILL_NAME/report.json\` -**Downstream handoff:** consumed by the validation wave. - -## Quality Rubric - -- [ ] <Check 1> -- [ ] <Check 2> -- [ ] <Check 3> - -## Examples - -\`\`\`bash -/$SKILL_NAME <example-args> -\`\`\` - -## Troubleshooting - -| Problem | Cause | Solution | -|---------|-------|----------| - -## See Also - -- [heal-skill](../heal-skill/SKILL.md) — deep audit (audit.sh) this skill before declaring stable +Report the concrete failure and stop. The caller owns any revision. EOF -# --- Mode-specific content injection ------------------------------------- -if [[ "$MODE" == "--absorb" ]]; then - # Observe package shape only; generated files record no external prose or path. - cat >> "$NEW_SKILL_MD" <<EOF - -## Clean-room provenance - -This skill was synthesized under the repo-runtime clean-room profile from -AgentOps-owned structure and terminology only. -EOF -fi - -# --- Companion files ----------------------------------------------------- -cat > "$NEW_DIR/scripts/validate.sh" <<'EOF' +cat >"$target/scripts/validate.sh" <<'EOF' #!/usr/bin/env bash -# validate.sh — minimal self-validation set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SKILL_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" REPO_ROOT="$(cd "$SKILL_DIR/../.." && pwd)" -exec bash "$REPO_ROOT/skills/heal-skill/scripts/audit.sh" "$SKILL_DIR" +exec bash "$REPO_ROOT/skills/heal-skill/scripts/heal.sh" --check --strict "$SKILL_DIR" EOF -chmod +x "$NEW_DIR/scripts/validate.sh" -chmod +x "$NEW_DIR" 2>/dev/null || true +chmod +x "$target/scripts/validate.sh" -# --- Codex parity (slim frontmatter + prompt.md) ------------------------- -CODEX_DIR="$REPO_ROOT/skills-codex/$SKILL_NAME" -mkdir -p "$CODEX_DIR" +mkdir -p "$REPO_ROOT/.agents/audits" +report="$REPO_ROOT/.agents/audits/${slug}-build.json" +python3 - "$report" "$mode" "$slug" "$source_hint" <<'PY' +import json +from pathlib import Path +import sys -# Try /converter if present, otherwise hand-build -CONVERTER="$REPO_ROOT/skills/converter/scripts/convert.sh" -if [[ -x "$CONVERTER" ]]; then - bash "$CONVERTER" "skills/$SKILL_NAME" codex 2>/dev/null || { - echo "init.sh: converter failed; falling back to hand-built codex artifacts" >&2 - } -fi - -# Hand-build codex SKILL.md (slim frontmatter — NO skill_api_version per learning 2026-05-03) -if [[ ! -f "$CODEX_DIR/SKILL.md" ]]; then - cat > "$CODEX_DIR/SKILL.md" <<EOF ---- -name: $SKILL_NAME -description: <copy from skills/$SKILL_NAME/SKILL.md description> ---- - -# /$SKILL_NAME - -See \`skills/$SKILL_NAME/SKILL.md\` for the canonical specification. - -## Codex Execution Profile - -See \`prompt.md\` in this directory. -EOF -fi - -# Always trim skill_api_version from codex SKILL.md if present -if grep -q "^skill_api_version:" "$CODEX_DIR/SKILL.md"; then - sed -i.bak '/^skill_api_version:/d' "$CODEX_DIR/SKILL.md" && rm -f "$CODEX_DIR/SKILL.md.bak" -fi - -# Hand-build prompt.md -if [[ ! -f "$CODEX_DIR/prompt.md" ]]; then - cat > "$CODEX_DIR/prompt.md" <<EOF -# Execution Profile: $SKILL_NAME - -You are running /$SKILL_NAME. - -See \`SKILL.md\` in this directory for full specification, OR -read \`skills/$SKILL_NAME/SKILL.md\` in the host repo for the canonical document. - -Workflow: -1. Read the user's request -2. Apply the skill's Workflow section -3. Produce output per the Output Specification -4. Self-check against the Quality Rubric -EOF -fi - -# --- Build report -------------------------------------------------------- -BUILD_REPORT="$REPO_ROOT/.agents/audits/${SKILL_NAME}-build.json" -mkdir -p "$(dirname "$BUILD_REPORT")" -case "$MODE" in - --interactive) REPORT_MODE="from-scratch" ;; - --like-flag-mode) REPORT_MODE="from-template" ;; - --absorb) REPORT_MODE="absorb-external" ;; -esac -cat > "$BUILD_REPORT" <<EOF -{ - "mode": "$REPORT_MODE", - "skill_name": "$SKILL_NAME", - "profile_id": "${SKILL_CONFORMANCE_PROFILE_ID:-repo-runtime}", - "files_created": [ - "skills/$SKILL_NAME/SKILL.md", - "skills/$SKILL_NAME/scripts/validate.sh", - "skills-codex/$SKILL_NAME/SKILL.md", - "skills-codex/$SKILL_NAME/prompt.md" - ], - "audit_pass": null, - "warnings": ["v1 skeleton — manual content fill required for description, constraints, workflow"] +path = Path(sys.argv[1]) +mode = {"--scratch": "from-scratch", "--template": "from-template", "--external": "absorb-external"}[sys.argv[2]] +payload = { + "mode": mode, + "skill_name": sys.argv[3], + "files_created": [f"skills/{sys.argv[3]}/SKILL.md", f"skills/{sys.argv[3]}/scripts/validate.sh"], + "structure_check_pass": False, } -EOF +if sys.argv[4]: + payload["source_hint"] = sys.argv[4] +path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8") +PY -# --- New-skill plumbing (ag-cw2y): make the scaffold one-shot-green ---------- -# The local/CI gates that silently tripped /burndown #600 are pre-empted here: -# 1. Dispositions row — else heal.sh Check 12 (MISSING_DISPOSITION). -if [[ -f "$REPO_ROOT/scripts/append-skill-disposition.sh" ]]; then - bash "$REPO_ROOT/scripts/append-skill-disposition.sh" "$SKILL_NAME" "$REPO_ROOT" \ - || echo "init.sh: WARN could not append dispositions row — add one manually" >&2 -fi -# 2. Narrative skill counts — --fix-counts bumps the "N checked-in skills" tokens -# in the domain-map + bdd Gherkin so the new skill doesn't trip registry-drift. -if [[ -x "$REPO_ROOT/scripts/check-registry-drift.sh" ]]; then - bash "$REPO_ROOT/scripts/check-registry-drift.sh" --fix-counts >/dev/null 2>&1 \ - || echo "init.sh: WARN registry-drift --fix-counts could not run — bump counts manually" >&2 -fi -# 3. Codex override catalog entry — else validate-codex-override-coverage fails -# ("source skill missing from Codex catalog"). Default parity_only (derived). -if [[ -f "$REPO_ROOT/scripts/append-codex-override-entry.sh" ]]; then - bash "$REPO_ROOT/scripts/append-codex-override-entry.sh" "$SKILL_NAME" "$REPO_ROOT" \ - || echo "init.sh: WARN could not add codex override catalog entry — add one manually" >&2 -fi -# 4. registry.json SKU catalog — else contracts-sync + correctness(ubuntu) BOTH -# fail ("registry.json is stale" / "SKU_CATALOG: DRIFT"). This is the 5th -# one-shot-green surface ag-cw2y missed; it cost /burndown #600 a 2nd -# fix-and-repush (ag-ekyq). MUST run last — it scans the whole skills/ tree, -# so the new skeleton must already exist on disk. -if [[ -f "$REPO_ROOT/scripts/generate-registry.sh" ]]; then - bash "$REPO_ROOT/scripts/generate-registry.sh" >/dev/null 2>&1 \ - || echo "init.sh: WARN could not regen registry.json — run scripts/generate-registry.sh manually" >&2 -fi - -echo "init.sh: created skill skeleton at $NEW_DIR" -echo "init.sh: codex parity at $CODEX_DIR" -echo "init.sh: build report at $BUILD_REPORT" -echo "init.sh: dispositions row + narrative counts scaffolded (refine the placeholder row)" +echo "init.sh: created $target" diff --git a/skills-codex/skill-builder/scripts/scan_descriptions.py b/skills-codex/skill-builder/scripts/scan_descriptions.py index 1c9c28e31..5c5e6732e 100644 --- a/skills-codex/skill-builder/scripts/scan_descriptions.py +++ b/skills-codex/skill-builder/scripts/scan_descriptions.py @@ -11,7 +11,7 @@ each skill that lacks one. Discovery in the runtime is pure LLM reasoning over the `description` field, so a missing trigger phrase is a material skill-selection risk, not cosmetic. See -`skills/skill-builder/references/skill-authoring-standard.md`. +`skills/skill-builder/SKILL.md`. Usage: python3 scan_descriptions.py [SKILLS_DIR] [--json] [--strict] [--quiet] diff --git a/skills-codex/skill-builder/scripts/validate.sh b/skills-codex/skill-builder/scripts/validate.sh index b83b11afb..9d98e2c87 100755 --- a/skills-codex/skill-builder/scripts/validate.sh +++ b/skills-codex/skill-builder/scripts/validate.sh @@ -1,41 +1,30 @@ #!/usr/bin/env bash -# validate.sh — self-validation for skill-builder set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SKILL_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" REPO_ROOT="$(cd "$SKILL_DIR/../.." && pwd)" -# Run heal-skill structural check on ourselves by exit code. bash "$REPO_ROOT/skills/heal-skill/scripts/heal.sh" --check --strict "$SKILL_DIR" -# Verify required artifacts exist -for f in SKILL.md scripts/build.sh scripts/init.sh references/skill-template.md schemas/build-report.json; do - [[ -f "$SKILL_DIR/$f" ]] || { echo "validate.sh: missing $SKILL_DIR/$f" >&2; exit 1; } -done - -# Verify scale-factory lessons stay encoded. -for phrase in \ - "heal-skill --check --strict" \ - "One skill directory = one writer" \ - "git status" \ - "Clean-room includes names" \ - "Workflow tool"; do - grep -q "$phrase" "$SKILL_DIR/SKILL.md" "$SKILL_DIR/references/agentops-skill-factory.md" || { - echo "validate.sh: missing scale-factory lesson: $phrase" >&2 +for path in SKILL.md scripts/build.sh scripts/init.sh schemas/build-report.json; do + [[ -f "$SKILL_DIR/$path" ]] || { + echo "skill-builder validate: missing $path" >&2 exit 1 } done -# Verify SKILL.md is within churn budget -LINES="$(wc -l < "$SKILL_DIR/SKILL.md")" -if (( LINES > 250 )); then - echo "validate.sh: SKILL.md is $LINES lines (>250 budget per finding f-2026-05-01-025)" >&2 +for script in scripts/build.sh scripts/init.sh; do + [[ -x "$SKILL_DIR/$script" ]] || { + echo "skill-builder validate: not executable: $script" >&2 + exit 1 + } +done + +if rg -n 'from-pattern|flywheel close-loop|append-skill-disposition' "$SKILL_DIR/SKILL.md" \ + || rg -n 'git (status|commit|push)|ao land|retry|queue|lease' \ + "$SKILL_DIR/scripts/build.sh" "$SKILL_DIR/scripts/init.sh"; then + echo "skill-builder validate: obsolete lifecycle behavior remains" >&2 exit 1 fi -# Verify build.sh and init.sh are executable -for s in scripts/build.sh scripts/init.sh; do - [[ -x "$SKILL_DIR/$s" ]] || chmod +x "$SKILL_DIR/$s" -done - -echo "validate.sh: skill-builder PASS ($LINES lines, all artifacts present)" +echo "skill-builder validate: PASS" diff --git a/skills-codex/standards/.agentops-generated.json b/skills-codex/standards/.agentops-generated.json index a7e8ef5d1..1be5c4b2c 100644 --- a/skills-codex/standards/.agentops-generated.json +++ b/skills-codex/standards/.agentops-generated.json @@ -2,6 +2,6 @@ "generator": "codex-sync", "source_skill": "skills/standards", "layout": "modular", - "source_hash": "eed8c1b40d6348f616aac238c47b5eb761ca8e737da6d24441322c986f1321e0", - "generated_hash": "fb857320cd5003a1ade0ca4ee6b964e5921a622ac73eb74c06d19937250fb1f3" + "source_hash": "e150a83135b742beba11af30875c693a9b045abcee9834717ebb8a30a78c09fb", + "generated_hash": "586534974990cbbc2a5a7d2307efd0df1f04ba1e4e2edbe22b3004eeeec77dec" } diff --git a/skills-codex/standards/references/test-pyramid.md b/skills-codex/standards/references/test-pyramid.md index b789a8ce9..1053c24e7 100644 --- a/skills-codex/standards/references/test-pyramid.md +++ b/skills-codex/standards/references/test-pyramid.md @@ -1,6 +1,6 @@ # AI-Native Test Shape — L0 through L7 -> Shared reference for RPI lifecycle skills. Loaded by `/discovery`, `/plan`, `/premortem`, `/implement`, `/crank`, `/validate`, and `/postmortem`. +> Optional engineering reference for Plan, Implement, and Validate. It does not add phases or continuation authority. ## The AI-Native Test Shape @@ -76,9 +76,9 @@ The Traditional Pyramid The AI-Native Shape | RPI Phase | Test Levels | What Happens | |-----------|-------------|--------------| -| **Discovery** (`/discovery`, `/plan`) | L0–L3 scoping | Plan identifies which test levels apply. **Default: L2 required for all code-change issues.** Issues include `test_level` metadata. | +| **Plan** (`/plan`) | L0–L3 scoping | Plan identifies which checks prove the active behavior and names the first acceptance check. | | **Premortem** (`/premortem`) | L0–L3 coverage check | Validates plan covers appropriate test levels. **Flags L1-only coverage as WARN.** | -| **Implementation** (`/implement`, `/crank`) | L0–L2 writing + execution | **TDD writes L2 integration tests first (RED).** L0 contracts from specs. L1 as regression guards. | +| **Implementation** (`/implement`) | L0–L2 writing + execution | Behavior-changing work starts with the bounded acceptance check RED, then GREEN and refactor. | | **Validation** (`/validate`, `/postmortem`) | L0–L3 coverage audit | Assesses test coverage. **L2+ coverage weighted 3-5x over L1 in vibe scoring.** | ## Test Level Selection Guide @@ -125,7 +125,7 @@ When creating issues in `/plan`, include test level metadata: ## Bug-Finding Levels (Agent-Autonomous) > **Proven 2026-03-14 on jren-cm:** 3,321 L1 unit tests found 0 new bugs. These levels found 8. -> Evidence: `/Users/fullerbt/gt/jren_cm/crew/ichigo/scripts/.agents/council/2026-03-14-post-mortem-full-session-methodology.md` +> Evidence: `/Users/fullerbt/gt/jren_cm/crew/ichigo/scripts/.agents/council/2026-03-14-postmortem-full-session-methodology.md` L0–L3 are the **coverage shape** — they verify code works as designed. These are the **bug-finding shape** — they find bugs the coverage shape misses. diff --git a/skills-codex/status/.agentops-generated.json b/skills-codex/status/.agentops-generated.json index f564fa45d..96e578d67 100644 --- a/skills-codex/status/.agentops-generated.json +++ b/skills-codex/status/.agentops-generated.json @@ -2,6 +2,6 @@ "generator": "codex-sync", "source_skill": "skills/status", "layout": "modular", - "source_hash": "9da13b9c19e4e367c7c8e732d807af1c5dd425f2d71325d6e951e9e834815a25", + "source_hash": "d1143bcf4b203ff48103c32deb0c82060d1109caa8cbe28aa4ed5d0a6a21c18a", "generated_hash": "f948133821c25a531b51c6e1d92ef814ea8bc8ad387be07cb12ba2de13e9653f" } diff --git a/skills-codex/swarm/.agentops-generated.json b/skills-codex/swarm/.agentops-generated.json index 803c0c9f0..d374ef838 100644 --- a/skills-codex/swarm/.agentops-generated.json +++ b/skills-codex/swarm/.agentops-generated.json @@ -2,6 +2,6 @@ "generator": "codex-sync", "source_skill": "skills/swarm", "layout": "modular", - "source_hash": "895b770e5e56134ed09008fe34a801345be5c17de8a9acb2ecfc7c819c63a17d", - "generated_hash": "d6c7da6931740de900a9d0c8cf3d44b581e0c3181da6e59b21a7df1b4c967043" + "source_hash": "7bb9623c612fc8ddecf7e6187f455cf0563da5d79745b225cbc455662a28bcd7", + "generated_hash": "e1de598cd7656b72d824dab3136cf7f95114d5ca70e39cafb914170ac3123c3e" } diff --git a/skills-codex/swarm/scripts/dispatch_once.py b/skills-codex/swarm/scripts/dispatch_once.py index 7d1b755f8..14172ba50 100644 --- a/skills-codex/swarm/scripts/dispatch_once.py +++ b/skills-codex/swarm/scripts/dispatch_once.py @@ -34,7 +34,30 @@ def _includes(packet: Mapping[str, Any]) -> tuple[str, ...]: def _overlap(left: str, right: str) -> bool: - return left == right or left.startswith(right + "/") or right.startswith(left + "/") + """Conservatively decide whether two include patterns may intersect. + + Literal sibling paths are provably disjoint. For globs, the literal path + prefix before the first wildcard must itself be disjoint; otherwise the + adapter rejects the batch instead of guessing about a shared write surface. + """ + + def literal_prefix(pattern: str) -> str: + parts: list[str] = [] + for part in PurePosixPath(pattern).parts: + if any(character in part for character in "*?["): + break + parts.append(part) + return PurePosixPath(*parts).as_posix() if parts else "." + + left_prefix = literal_prefix(left) + right_prefix = literal_prefix(right) + if left_prefix == "." or right_prefix == ".": + return True + return ( + left_prefix == right_prefix + or left_prefix.startswith(right_prefix + "/") + or right_prefix.startswith(left_prefix + "/") + ) def dispatch_once( diff --git a/skills-codex/swarm/tests/test_dispatch_once.py b/skills-codex/swarm/tests/test_dispatch_once.py index 740477f97..1fc90bf50 100644 --- a/skills-codex/swarm/tests/test_dispatch_once.py +++ b/skills-codex/swarm/tests/test_dispatch_once.py @@ -57,6 +57,36 @@ class DispatchOnceTests(unittest.TestCase): ) self.assertEqual(calls, 0) + def test_overlapping_glob_scopes_fail_before_dispatch(self) -> None: + calls = 0 + + def executor(_value: dict) -> None: + nonlocal calls + calls += 1 + + with self.assertRaisesRegex(ValueError, "write scopes overlap"): + MODULE.dispatch_once( + [packet("a", "src/**"), packet("b", "src/lib/**")], executor + ) + self.assertEqual(calls, 0) + + def test_disjoint_glob_prefixes_dispatch(self) -> None: + calls: list[str] = [] + + MODULE.dispatch_once( + [packet("a", "src/a/**"), packet("b", "src/b/**")], + lambda value: calls.append(value["packet_id"]), + ) + + self.assertEqual(calls, ["a", "b"]) + + def test_uncertain_glob_overlap_is_rejected(self) -> None: + with self.assertRaisesRegex(ValueError, "write scopes overlap"): + MODULE.dispatch_once( + [packet("a", "src/*/generated"), packet("b", "src/*/manual")], + lambda _value: None, + ) + if __name__ == "__main__": unittest.main() diff --git a/skills-codex/test/.agentops-generated.json b/skills-codex/test/.agentops-generated.json index ebbbe0c68..b2c3d5018 100644 --- a/skills-codex/test/.agentops-generated.json +++ b/skills-codex/test/.agentops-generated.json @@ -2,6 +2,6 @@ "generator": "codex-sync", "source_skill": "skills/test", "layout": "modular", - "source_hash": "52e634545cc273c053e093175bc7a884d9a49e00d2c560044121cfbe5da293d3", + "source_hash": "dd7654a0113231b8d5bcfa96ff217009e90acfcbae16242b172168d18d03e700", "generated_hash": "7c63c252d2cb47e4877701683819212851e6db5c7873ba47f32d6b2667cf2a3b" } diff --git a/skills-codex/toil-mining/.agentops-generated.json b/skills-codex/toil-mining/.agentops-generated.json index 7b50191d5..208ffafc1 100644 --- a/skills-codex/toil-mining/.agentops-generated.json +++ b/skills-codex/toil-mining/.agentops-generated.json @@ -2,6 +2,6 @@ "generator": "codex-sync", "source_skill": "skills/toil-mining", "layout": "modular", - "source_hash": "f52e3b637e357c27d1bcb55ef5349e49cd42a75d13ae1d681e7363f30c6ce797", + "source_hash": "6a6c4486ad83b6f791a6e4f32072a034aeecbc5f4cd4416a653079991fe3bb9a", "generated_hash": "d626a479e186f63e791a48010e22a82ede4a240aa9be42290fa14efb1c975c69" } diff --git a/skills-codex/validate/.agentops-generated.json b/skills-codex/validate/.agentops-generated.json index 7113d3b16..6dc12246f 100644 --- a/skills-codex/validate/.agentops-generated.json +++ b/skills-codex/validate/.agentops-generated.json @@ -2,6 +2,6 @@ "generator": "codex-sync", "source_skill": "skills/validate", "layout": "modular", - "source_hash": "f1bc22d75a1263586a0b2e37b345ff9fe6e1432bd95cc666458f707dbe74e0e9", - "generated_hash": "f1935b9ab480fee7488ae6d40f9289e9dc715cc489ae9ae68505b70f75f7fa7e" + "source_hash": "9f0c83578d777757b886a24f99fe7dfa4e50b8442925bfe3fbcb0f5715154aa3", + "generated_hash": "5c3af71f10a600e5220ee8af993d8463b3e348d9c7b92b18a5746876a454a919" } diff --git a/skills-codex/validate/scripts/test_validate.py b/skills-codex/validate/scripts/test_validate.py index cd6e73307..c593002df 100755 --- a/skills-codex/validate/scripts/test_validate.py +++ b/skills-codex/validate/scripts/test_validate.py @@ -48,6 +48,17 @@ class ValidateV2Tests(unittest.TestCase): subject.write_text("two", encoding="utf-8") self.assertFalse(tool.verify_manifest(manifest, root, None)[0]) + def test_git_metadata_is_not_identity_bearing(self): + with tempfile.TemporaryDirectory() as raw: + root = Path(raw) + (root / "value").write_text("same", encoding="utf-8") + first = tool.build_manifest(root, ["."], [], git_metadata={"commit": "one"}) + second = tool.build_manifest(root, ["."], [], git_metadata={"commit": "two"}) + self.assertEqual(first["canonical_manifest_digest"], second["canonical_manifest_digest"]) + self.assertNotEqual(first["git_metadata"], second["git_metadata"]) + self.assertTrue(tool.verify_manifest(first, root, None)[0]) + self.assertTrue(tool.verify_manifest(second, root, None)[0]) + def test_symlink_and_deletion_identity(self): with tempfile.TemporaryDirectory() as raw: root = Path(raw) @@ -120,6 +131,20 @@ class ValidateV2Tests(unittest.TestCase): self.assertFalse(existed) self.assert_schema_valid(replacement) + def test_incomplete_draft_is_rejected_without_writing(self): + with tempfile.TemporaryDirectory() as raw: + with self.assertRaisesRegex(tool.ContractError, "missing required fields"): + tool.store_verdict({"verdict": "FAIL"}, Path(raw)) + self.assertEqual(list(Path(raw).iterdir()), []) + + def test_unknown_field_is_rejected_without_writing(self): + with tempfile.TemporaryDirectory() as raw: + draft = self.draft() + draft["next_action"] = "repair" + with self.assertRaisesRegex(tool.ContractError, "unknown fields"): + tool.store_verdict(draft, Path(raw)) + self.assertEqual(list(Path(raw).iterdir()), []) + if __name__ == "__main__": unittest.main() diff --git a/skills-codex/validate/scripts/validate.py b/skills-codex/validate/scripts/validate.py index 24606e76e..e0980541e 100755 --- a/skills-codex/validate/scripts/validate.py +++ b/skills-codex/validate/scripts/validate.py @@ -8,6 +8,7 @@ delivery integration. It operates only on explicit files and directories. from __future__ import annotations import argparse +from datetime import datetime import fnmatch import hashlib import json @@ -144,7 +145,7 @@ def build_manifest( manifest["entries"] = sorted(manifest["entries"] + deletions, key=lambda item: item["path"]) if git_metadata: manifest["git_metadata"] = git_metadata - manifest["canonical_manifest_digest"] = digest_value(manifest) + manifest["canonical_manifest_digest"] = digest_value(manifest_identity(manifest)) return manifest @@ -152,10 +153,22 @@ def valid_digest(value: Any) -> bool: return isinstance(value, str) and len(value) == 64 and all(ch in HEX64 for ch in value) +def manifest_identity(manifest: dict[str, Any]) -> dict[str, Any]: + """Return only the fields that identify subject content. + + ``git_metadata`` is intentionally descriptive. Supplying or changing it + must never change the identity of otherwise identical content. + """ + return { + key: value + for key, value in manifest.items() + if key not in {"canonical_manifest_digest", "git_metadata"} + } + + def verify_manifest(manifest: dict[str, Any], root: Path, base_manifest: dict[str, Any] | None) -> tuple[bool, str]: claimed = manifest.get("canonical_manifest_digest") - unsigned = {key: value for key, value in manifest.items() if key != "canonical_manifest_digest"} - if not valid_digest(claimed) or digest_value(unsigned) != claimed: + if not valid_digest(claimed) or digest_value(manifest_identity(manifest)) != claimed: return False, "manifest canonical digest is invalid" rebuilt = build_manifest( root, @@ -244,6 +257,106 @@ def enforce_identity(draft: dict[str, Any]) -> dict[str, Any]: return draft +VERDICT_KEYS = { + "schema_version", + "acceptance_digest", + "subject_manifest_digest", + "author_context_id", + "validator_context_id", + "freshness_attestation", + "verdict", + "criteria", + "findings", + "evidence_refs", + "checked", + "not_checked", + "validated_at", + "artifact_digest", +} + + +def require_string_list(value: Any, field: str, *, nonempty: bool = False) -> None: + if not isinstance(value, list) or (nonempty and not value): + raise ContractError(f"verdict.v2 {field} must be a{' nonempty' if nonempty else ''} array") + if any(not isinstance(item, str) or not item for item in value): + raise ContractError(f"verdict.v2 {field} entries must be nonempty strings") + + +def validate_verdict_v2(artifact: dict[str, Any]) -> None: + """Enforce the complete bundled verdict.v2 contract before persistence.""" + missing = sorted(VERDICT_KEYS - artifact.keys()) + extra = sorted(artifact.keys() - VERDICT_KEYS) + if missing: + raise ContractError(f"verdict.v2 missing required fields: {', '.join(missing)}") + if extra: + raise ContractError(f"verdict.v2 contains unknown fields: {', '.join(extra)}") + if artifact["schema_version"] != "verdict.v2": + raise ContractError("verdict.v2 schema_version must be verdict.v2") + for field in ("acceptance_digest", "subject_manifest_digest", "artifact_digest"): + if not valid_digest(artifact[field]): + raise ContractError(f"verdict.v2 {field} must be a lowercase SHA-256 digest") + expected_digest = digest_value({key: value for key, value in artifact.items() if key != "artifact_digest"}) + if artifact["artifact_digest"] != expected_digest: + raise ContractError("verdict.v2 artifact_digest does not match canonical JSON") + for field in ("author_context_id", "validator_context_id"): + if artifact[field] is not None and (not isinstance(artifact[field], str) or not artifact[field]): + raise ContractError(f"verdict.v2 {field} must be null or a nonempty string") + freshness = artifact["freshness_attestation"] + if freshness is not None: + if not isinstance(freshness, dict) or set(freshness) != {"source", "attester_identity"}: + raise ContractError("verdict.v2 freshness_attestation has invalid fields") + if freshness["source"] not in {"runtime", "caller"}: + raise ContractError("verdict.v2 freshness source must be runtime or caller") + if not isinstance(freshness["attester_identity"], str) or not freshness["attester_identity"]: + raise ContractError("verdict.v2 freshness attester_identity must be nonempty") + if artifact["verdict"] not in {"PASS", "FAIL", "NOT_PROVEN"}: + raise ContractError("verdict.v2 verdict must be PASS, FAIL, or NOT_PROVEN") + criteria = artifact["criteria"] + if not isinstance(criteria, list) or not criteria: + raise ContractError("verdict.v2 criteria must be a nonempty array") + for index, criterion in enumerate(criteria): + allowed = {"id", "result", "evidence_refs", "reason"} + if not isinstance(criterion, dict) or not {"id", "result", "evidence_refs"}.issubset(criterion) or not set(criterion).issubset(allowed): + raise ContractError(f"verdict.v2 criteria[{index}] has invalid fields") + if not isinstance(criterion["id"], str) or not criterion["id"]: + raise ContractError(f"verdict.v2 criteria[{index}].id must be nonempty") + if criterion["result"] not in {"PASS", "FAIL", "NOT_PROVEN"}: + raise ContractError(f"verdict.v2 criteria[{index}].result is invalid") + require_string_list(criterion["evidence_refs"], f"criteria[{index}].evidence_refs") + if "reason" in criterion and not isinstance(criterion["reason"], str): + raise ContractError(f"verdict.v2 criteria[{index}].reason must be a string") + findings = artifact["findings"] + if not isinstance(findings, list): + raise ContractError("verdict.v2 findings must be an array") + for index, finding in enumerate(findings): + if not isinstance(finding, dict) or set(finding) != {"id", "summary", "evidence_refs"}: + raise ContractError(f"verdict.v2 findings[{index}] has invalid fields") + if not isinstance(finding["id"], str) or not finding["id"]: + raise ContractError(f"verdict.v2 findings[{index}].id must be nonempty") + if not isinstance(finding["summary"], str) or not finding["summary"]: + raise ContractError(f"verdict.v2 findings[{index}].summary must be nonempty") + require_string_list(finding["evidence_refs"], f"findings[{index}].evidence_refs", nonempty=True) + for field in ("evidence_refs", "checked", "not_checked"): + require_string_list(artifact[field], field) + if not isinstance(artifact["validated_at"], str): + raise ContractError("verdict.v2 validated_at must be an RFC3339 date-time") + try: + timestamp = datetime.fromisoformat(artifact["validated_at"].replace("Z", "+00:00")) + except ValueError as exc: + raise ContractError("verdict.v2 validated_at must be an RFC3339 date-time") from exc + if timestamp.tzinfo is None: + raise ContractError("verdict.v2 validated_at must include a timezone") + if artifact["verdict"] == "PASS": + author = artifact["author_context_id"] + validator = artifact["validator_context_id"] + if not author or not validator or author == validator or freshness is None: + raise ContractError("verdict.v2 PASS requires distinct identities and freshness attestation") + if any(criterion["result"] != "PASS" for criterion in criteria): + raise ContractError("verdict.v2 PASS requires every criterion to PASS") + if artifact["not_checked"]: + raise ContractError("verdict.v2 PASS cannot contain not_checked items") + + def artifact_bytes(draft: dict[str, Any]) -> tuple[dict[str, Any], bytes]: unsigned = {key: value for key, value in draft.items() if key != "artifact_digest"} digest = digest_value(unsigned) @@ -281,10 +394,12 @@ def store_verdict(draft: dict[str, Any], destination: Path) -> tuple[dict[str, A draft = enforce_identity(draft) draft["schema_version"] = "verdict.v2" artifact, payload = artifact_bytes(draft) + validate_verdict_v2(artifact) try: path, existed = atomic_store(artifact, payload, destination) except ContractError as exc: artifact, payload = artifact_bytes(add_integrity_finding(draft, str(exc))) + validate_verdict_v2(artifact) path, existed = atomic_store(artifact, payload, destination) return artifact, path, existed diff --git a/skills/SKILL-TIERS.md b/skills/SKILL-TIERS.md index dae13854b..2c28e1a82 100644 --- a/skills/SKILL-TIERS.md +++ b/skills/SKILL-TIERS.md @@ -44,51 +44,51 @@ |---|---|---|---|---|---| | `account-rotation` | execution | `keep_specialist` | - | `account_rotation` | - | | `agent-mail` | execution | `keep_optional_adapter` | - | `agent_mail` | - | -| `agent-native` | meta | `keep_optional_adapter` | `ntm`, `agent-mail` | `role_dispatch`, `observe_workers`, `handoff` | `manage_runtime_sessions` | +| `agent-native` | meta | `keep_optional_adapter` | - | `role_dispatch`, `observe_workers`, `handoff` | `manage_runtime_sessions` | | `agy-native` | cross-vendor | `keep_optional_adapter` | - | `dispatch_explicit_packet`, `provide_fresh_context` | `start_agy_session` | -| `automation-shape-routing` | meta | `keep_optional_adapter` | `skill-builder`, `workflow-builder`, `agent-native` | `automation_shape_routing` | - | -| `bootstrap` | session | `keep_specialist` | `goals`, `product`, `doc`, `shared`, `scaffold` | `bootstrap` | - | +| `automation-shape-routing` | meta | `keep_optional_adapter` | - | `automation_shape_routing` | - | +| `bootstrap` | session | `keep_specialist` | - | `bootstrap` | - | | `cass` | execution | `keep_specialist` | - | `cass` | - | | `cc-hooks` | execution | `keep_specialist` | - | `cc_hooks` | - | -| `codebase-recon` | execution | `keep_specialist` | `doc` | `codebase_recon` | - | +| `codebase-recon` | execution | `keep_specialist` | - | `codebase_recon` | - | | `codex-exec` | orchestration | `keep_optional_adapter` | - | `codex_exec` | - | | `converter` | cross-vendor | `keep_specialist` | - | `converter` | - | | `council` | judgment | `keep_strategy` | - | `collect_independent_judgments`, `synthesize_disagreement` | `write_advisory_council_report` | | `dcg` | execution | `keep_specialist` | - | `dcg` | - | -| `doc` | product | `keep_specialist` | `standards`, `council` | `doc` | - | +| `doc` | product | `keep_specialist` | - | `doc` | - | | `domain` | knowledge | `keep_specialist` | - | `domain` | - | -| `dueling-idea-genies` | judgment | `keep_strategy` | `idea-genie` | `dueling_idea_genies` | - | +| `dueling-idea-genies` | judgment | `keep_strategy` | - | `dueling_idea_genies` | - | | `goals` | product | `keep_specialist` | - | `goals` | - | | `handoff` | session | `keep_specialist` | - | `handoff` | - | -| `heal-skill` | meta | `keep_specialist` | - | `heal_skill` | - | -| `idea-genie` | execution | `keep_strategy` | `research` | `generate_evidenced_options` | `write_idea_portfolio` | +| `heal-skill` | meta | `keep_specialist` | - | `heal_skill` | `optional_skill_projection_repair` | +| `idea-genie` | execution | `keep_strategy` | - | `generate_evidenced_options` | `write_idea_portfolio` | | `implement` | execution | `keep` | - | `execute_one_experiment`, `collect_factual_evidence` | `modify_declared_subject`, `write_candidate_packet` | | `learn` | execution | `keep_off_path` | - | `analyze_verdict_collections` | `write_advisory_observations` | | `ms` | execution | `keep_specialist` | - | `ms` | - | | `ntm` | execution | `keep_optional_adapter` | - | `ntm` | - | | `operationalize` | meta | `keep_specialist` | - | `distill_expertise`, `propose_artifact_shape` | `write_advisory_proposal` | -| `pattern-mining` | execution | `keep_specialist` | `operationalize` | `pattern_mining` | - | +| `pattern-mining` | execution | `keep_specialist` | - | `pattern_mining` | - | | `plan` | execution | `keep` | - | `shape_intent`, `define_acceptance`, `bound_write_scope` | `write_plan_packet` | -| `postmortem` | judgment | `keep_strategy` | `council`, `toil-mining` | `postmortem` | - | -| `premortem` | judgment | `keep_strategy` | `council` | `premortem` | - | +| `postmortem` | judgment | `keep_strategy` | - | `postmortem` | - | +| `premortem` | judgment | `keep_strategy` | - | `challenge_plan` | `write_advisory_plan_review` | | `product` | product | `keep_specialist` | - | `shape_product_boundary` | `write_product_document` | | `rch` | execution | `keep_specialist` | - | `rch` | - | | `reality-check` | judgment | `keep_strategy` | - | `compare_claim_to_evidence` | `write_advisory_gap_report` | -| `refactor` | execution | `keep_specialist` | `standards`, `pattern-mining` | `refactor` | - | -| `research` | execution | `keep_specialist` | `cass`, `ms`, `reverse-engineer`, `codebase-recon`, `pattern-mining` | `research` | - | +| `refactor` | execution | `keep_specialist` | - | `refactor` | - | +| `research` | execution | `keep_specialist` | - | `research` | - | | `reverse-engineer` | execution | `keep_specialist` | - | `reverse_engineer` | - | | `rpi` | meta | `keep` | `plan`, `implement`, `validate` | `orchestrate_once`, `report` | `dispatch_core_phases` | | `sbh` | execution | `keep_specialist` | - | `sbh` | - | -| `scaffold` | execution | `keep_specialist` | `standards` | `scaffold` | - | -| `scope` | meta | `keep_specialist` | - | `scope` | - | +| `scaffold` | execution | `keep_specialist` | - | `scaffold` | - | +| `scope` | meta | `keep_specialist` | - | `scope_review` | - | | `security` | product | `keep_specialist` | - | `security` | - | | `shared` | library | `keep_specialist` | - | `provide_reference_context` | - | -| `skill-builder` | meta | `keep_specialist` | `heal-skill`, `converter` | `skill_builder` | - | +| `skill-builder` | meta | `keep_specialist` | - | `skill_builder` | `writes_skill_source`, `regenerates_skill_projections` | | `standards` | knowledge | `keep_specialist` | - | `standards` | - | -| `status` | session | `keep_specialist` | `sbh` | `status` | - | +| `status` | session | `keep_specialist` | - | `status` | - | | `swarm` | execution | `keep_optional_adapter` | - | `dispatch_once` | `invoke_selected_executor` | -| `test` | execution | `keep_specialist` | `standards` | `test` | - | -| `toil-mining` | meta | `keep_specialist` | `automation-shape-routing` | `toil_mining` | - | +| `test` | execution | `keep_specialist` | - | `test` | - | +| `toil-mining` | meta | `keep_specialist` | - | `toil_mining` | - | | `using-gc` | execution | `keep_optional_adapter` | - | `dispatch_explicit_packet`, `observe_gc_runtime` | `operate_gas_city` | | `validate` | judgment | `keep` | - | `compute_subject_identity`, `judge_acceptance`, `persist_verdict` | `write_verdict_artifact` | | `workflow-builder` | meta | `keep_specialist` | - | `workflow_builder` | - | diff --git a/skills/agent-mail/references/WORKFLOWS.md b/skills/agent-mail/references/WORKFLOWS.md index 96580d5f4..d4d2a4411 100644 --- a/skills/agent-mail/references/WORKFLOWS.md +++ b/skills/agent-mail/references/WORKFLOWS.md @@ -20,7 +20,7 @@ The canonical workflow for working on a bead with coordination. BR/beads is the macro_start_session(human_key="/abs/path", program="codex-cli", model="YOUR_MODEL") 2. Pick work - ao beads exec ready --json → select bd-123 + br ready --json → select bd-123 3. Reserve files file_reservation_paths( @@ -48,8 +48,8 @@ The canonical workflow for working on a bead with coordination. BR/beads is the - Keep durable state/evidence on the bead, not only in mail 6. Complete - ao beads exec update bd-123 --notes "Validation: npm test, CI run 123, commit abc123" - ao beads exec close bd-123 --reason "Implemented OAuth flow" + br update bd-123 --notes "Validation: npm test, CI run 123, commit abc123" + br close bd-123 --reason "Implemented OAuth flow" release_file_reservations(project_key="/abs/path", agent_name="GreenCastle") send_message( ... diff --git a/skills/agent-native/SKILL.md b/skills/agent-native/SKILL.md index e80f8d92e..d7e0708b3 100644 --- a/skills/agent-native/SKILL.md +++ b/skills/agent-native/SKILL.md @@ -14,7 +14,7 @@ skill_api_version: 1 user-invocable: true metadata: tier: meta - dependencies: [ntm, agent-mail] + dependencies: [] capabilities: [role_dispatch, observe_workers, handoff] effects: [manage_runtime_sessions] canonical_status: canonical diff --git a/skills/automation-shape-routing/SKILL.md b/skills/automation-shape-routing/SKILL.md index 63f3740b9..638d67260 100644 --- a/skills/automation-shape-routing/SKILL.md +++ b/skills/automation-shape-routing/SKILL.md @@ -33,7 +33,7 @@ metadata: canonical_status: canonical disposition: keep_optional_adapter tier: meta - dependencies: [skill-builder, workflow-builder, agent-native] + dependencies: [] output_contract: a one-line routing verdict naming the shape and deciding axis --- diff --git a/skills/bootstrap/SKILL.md b/skills/bootstrap/SKILL.md index a34505a8b..003038085 100644 --- a/skills/bootstrap/SKILL.md +++ b/skills/bootstrap/SKILL.md @@ -26,12 +26,7 @@ metadata: disposition: keep_specialist graph_root: true tier: session - dependencies: - - goals - - product - - doc - - shared - - scaffold + dependencies: [] output_contract: minimal project docs and .agentops/verdicts directory --- # Bootstrap — minimal project setup diff --git a/skills/catalog.json b/skills/catalog.json index 636ac22c5..b50767288 100644 --- a/skills/catalog.json +++ b/skills/catalog.json @@ -80,10 +80,7 @@ "with": "agent-mail" } ], - "dependencies": [ - "ntm", - "agent-mail" - ], + "dependencies": [], "description": "Operate explicit orchestrator, implementer, validator, and scribe roles through a caller-selected agent runtime. Triggers: \"agent-native factory\", \"role-shaped agent panes\", \"persistent workers\".", "disposition": "keep_optional_adapter", "effects": [ @@ -171,11 +168,7 @@ "with": "operationalize" } ], - "dependencies": [ - "skill-builder", - "workflow-builder", - "agent-native" - ], + "dependencies": [], "description": "Front door for agent automation: choose inline, bounded fanout, reusable skill/workflow/gate, persistent agent-native workers, or explicit Gas City. Triggers: \"build automation\", \"which orchestration shape\", \"should this use NTM\".", "disposition": "keep_optional_adapter", "effects": [], @@ -207,13 +200,7 @@ "shared" ], "context_rel": [], - "dependencies": [ - "goals", - "product", - "doc", - "shared", - "scaffold" - ], + "dependencies": [], "description": "Initialize minimal AgentOps documentation and verdict storage without taking over repository workflow.", "disposition": "keep_specialist", "effects": [], @@ -299,9 +286,7 @@ "with": "doc" } ], - "dependencies": [ - "doc" - ], + "dependencies": [], "description": "Reconstruct a repository as cited entry-to-test flows, bounded claims, and a reusable baseline or verified delta. Triggers: \"build a repository mental model\", \"trace this codebase\", \"refresh the prior recon\".", "disposition": "keep_specialist", "effects": [], @@ -442,10 +427,7 @@ "repo-context" ], "context_rel": [], - "dependencies": [ - "standards", - "council" - ], + "dependencies": [], "description": "Generate and validate repo docs, READMEs, and OSS doc packs. Triggers: \"doc\", \"generate and validate repo docs\", \"doc skill\".", "disposition": "keep_specialist", "effects": [], @@ -510,9 +492,7 @@ "with": "plan" } ], - "dependencies": [ - "idea-genie" - ], + "dependencies": [], "description": "Challenge a consequential idea with sealed independent perspectives, cross-review, and preserved dissent. Triggers: \"challenge this idea\", \"compare independent proposals\", \"stress-test a one-way door\".", "disposition": "keep_strategy", "effects": [], @@ -603,20 +583,21 @@ } ], "dependencies": [], - "description": "Repair skill hygiene and deep-audit SKILL.md quality (absorbed skill-auditor). Triggers: \"heal-skill\", \"heal skill\", \"repair skill hygiene\", \"skill-auditor\", \"audit skill\", \"skill audit\".", + "description": "Check or repair structural hygiene in AgentOps skill packages. Triggers: \"heal skill\", \"repair skill hygiene\", \"audit skill structure\", \"check skill package\".", "disposition": "keep_specialist", - "effects": [], + "effects": [ + "optional_skill_projection_repair" + ], "graph_root": false, "hexagonal_role": "supporting", "name": "heal-skill", "practices": [ - "refactoring", - "code-complete" + "refactoring" ], "produces": [ - "audit-report.json" + "skill-hygiene-report" ], - "references_count": 6, + "references_count": 5, "tier": "meta", "user_invocable": false }, @@ -640,9 +621,7 @@ "with": "plan" } ], - "dependencies": [ - "research" - ], + "dependencies": [], "description": "Generate an evidence-grounded opportunity portfolio for an open-ended product or engineering question. Triggers: \"idea genie\", \"what should we build\", \"supported opportunities\".", "disposition": "keep_strategy", "effects": [ @@ -855,9 +834,7 @@ "with": "operationalize" } ], - "dependencies": [ - "operationalize" - ], + "dependencies": [], "description": "Test repeated implementation shapes against independent exemplars and a holdout before routing an earned abstraction. Triggers: \"mine a recurring code pattern\", \"is this abstraction earned\", \"extract invariants from implementations\".", "disposition": "keep_specialist", "effects": [], @@ -914,24 +891,11 @@ ], "codex_override_present": true, "consumes": [ - "learn", - "toil-mining" + "verdict.v2" ], - "context_rel": [ - { - "kind": "customer-of", - "with": "learn" - }, - { - "kind": "customer-of", - "with": "toil-mining" - } - ], - "dependencies": [ - "council", - "toil-mining" - ], - "description": "Test an explicit retrospective causal question against evidence and counterfactuals after Validate and Learn. Triggers: \"postmortem\", \"causal retrospective\", \"test a retrospective hypothesis\".", + "context_rel": [], + "dependencies": [], + "description": "Optionally test a retrospective causal question against durable verdict evidence. Triggers: \"postmortem\", \"causal retrospective\", \"test a retrospective hypothesis\".", "disposition": "keep_strategy", "effects": [], "graph_root": false, @@ -951,38 +915,37 @@ { "canonical_status": "canonical", "capabilities": [ - "premortem" + "challenge_plan" ], "codex_override_present": true, "consumes": [ - "standards" + "plan-packet.v1" ], "context_rel": [ { - "kind": "shared-kernel", - "with": "standards" + "kind": "supplier-to", + "with": "plan" } ], - "dependencies": [ - "council" - ], - "description": "Use when: an exact plan needs a verdict. Stress-tests readiness before work with one fresh, independent judge.", + "dependencies": [], + "description": "Optionally challenge a frozen plan with one fresh independent judge before implementation. Triggers: \"premortem\", \"challenge this plan\", \"what could make this plan fail\".", "disposition": "keep_strategy", - "effects": [], + "effects": [ + "write_advisory_plan_review" + ], "graph_root": true, "hexagonal_role": "domain", "name": "premortem", "practices": [ - "adr", - "mythical-man-month", - "design-by-contract" + "design-by-contract", + "adr" ], "produces": [ - "premortem-plan-verdict.v1" + "premortem-plan-review.v1" ], - "references_count": 7, + "references_count": 1, "tier": "judgment", - "user_invocable": false + "user_invocable": true }, { "canonical_status": "canonical", @@ -1086,10 +1049,7 @@ "repo-context" ], "context_rel": [], - "dependencies": [ - "standards", - "pattern-mining" - ], + "dependencies": [], "description": "Execute one behavior-preserving structural transformation and report evidence.", "disposition": "keep_specialist", "effects": [], @@ -1115,32 +1075,24 @@ ], "codex_override_present": true, "consumes": [ - "repo-context" + "research-question" ], "context_rel": [], - "dependencies": [ - "cass", - "ms", - "reverse-engineer", - "codebase-recon", - "pattern-mining" - ], - "description": "Explore and write findings. Triggers: \"research\", \"explore and write findings.\", \"research skill\".", + "dependencies": [], + "description": "Answer a bounded question with current cited evidence. Triggers: \"research\", \"investigate\", \"find evidence\".", "disposition": "keep_specialist", "effects": [], "graph_root": false, "hexagonal_role": "driving-adapter", "name": "research", "practices": [ - "wiki-knowledge-surface", "pragmatic-programmer", "ddd-bounded-context" ], "produces": [ - ".agents/research/*.md", - "result.json" + "research-report" ], - "references_count": 19, + "references_count": 1, "tier": "execution", "user_invocable": false }, @@ -1253,10 +1205,8 @@ "codex_override_present": true, "consumes": [], "context_rel": [], - "dependencies": [ - "standards" - ], - "description": "Stamp project/component/CI scaffolds \u2014 but reach for it mainly for the repo-specific domain-slice binding (generic trees a frontier model needs no skill for). Triggers: \"scaffold\", \"create project component or boilerplate\".", + "dependencies": [], + "description": "Stamp a bounded project, component, or CI scaffold and verify the generated result once. Triggers: \"scaffold\", \"create project component or boilerplate\".", "disposition": "keep_specialist", "effects": [], "graph_root": false, @@ -1270,39 +1220,40 @@ "produces": [ "converted-skill" ], - "references_count": 4, + "references_count": 3, "tier": "execution", "user_invocable": false }, { "canonical_status": "canonical", "capabilities": [ - "scope" + "scope_review" ], "codex_override_present": true, - "consumes": [], + "consumes": [ + "proposed-write-scope" + ], "context_rel": [ { "kind": "supplier-to", - "with": "domain" + "with": "plan" } ], "dependencies": [], - "description": "Hard-block edits outside declared frozen directories and protect paths during risky changes. Triggers: \"scope\", \"hard-block edits outside declared frozen\", \"scope skill\".", + "description": "Review a proposed PlanPacket write scope for completeness and ambiguity. Triggers: \"review write scope\", \"check scope boundaries\", \"scope this change\".", "disposition": "keep_specialist", "effects": [], "graph_root": false, - "hexagonal_role": "driven-adapter", + "hexagonal_role": "supporting", "name": "scope", "practices": [ "ddd-bounded-context", - "design-by-contract", - "mythical-man-month" + "design-by-contract" ], "produces": [ - "filesystem-gate" + "scope-review" ], - "references_count": 4, + "references_count": 0, "tier": "meta", "user_invocable": false }, @@ -1322,7 +1273,7 @@ } ], "dependencies": [], - "description": "Run repository security scans for vulnerabilities, dependency risk, secrets, and release gates. Triggers: \"security\", \"run repository security scans for\", \"security skill\".", + "description": "Run authorized repository security scans for vulnerabilities, dependency risk, secrets, and binary policy. Triggers: \"security\", \"run repository security scans for\", \"security skill\".", "disposition": "keep_specialist", "effects": [], "graph_root": true, @@ -1362,7 +1313,7 @@ "produces": [ "reference-documents" ], - "references_count": 1, + "references_count": 0, "tier": "library", "user_invocable": false }, @@ -1374,34 +1325,28 @@ "codex_override_present": true, "consumes": [], "context_rel": [ - { - "kind": "customer-of", - "with": "automation-shape-routing" - }, { "kind": "supplier-to", "with": "heal-skill" } ], - "dependencies": [ - "heal-skill", - "converter" - ], - "description": "Scaffold or absorb new SKILL.md files against the unified AgentOps template. Triggers: \"create a skill\", \"scaffold skill\", \"absorb external skill\", \"new skill\".", + "dependencies": [], + "description": "Create a metadata-complete AgentOps skill source package and regenerate its derived projections. Triggers: \"create a skill\", \"scaffold skill\", \"absorb external skill\", \"new skill\".", "disposition": "keep_specialist", - "effects": [], + "effects": [ + "writes_skill_source", + "regenerates_skill_projections" + ], "graph_root": false, "hexagonal_role": "supporting", "name": "skill-builder", "practices": [ - "code-complete", - "pragmatic-programmer", - "design-patterns" + "pragmatic-programmer" ], "produces": [ - "converted-skill" + "skill-source-package" ], - "references_count": 6, + "references_count": 3, "tier": "meta", "user_invocable": false }, @@ -1439,9 +1384,7 @@ "codex_override_present": true, "consumes": [], "context_rel": [], - "dependencies": [ - "sbh" - ], + "dependencies": [], "description": "Report observable AgentOps evidence without selecting work. Triggers: \"status\", \"show AgentOps status\".", "disposition": "keep_specialist", "effects": [], @@ -1500,9 +1443,7 @@ "repo-context" ], "context_rel": [], - "dependencies": [ - "standards" - ], + "dependencies": [], "description": "Generate tests and coverage plans. Triggers: \"test\", \"generate tests and coverage plans.\", \"test skill\".", "disposition": "keep_specialist", "effects": [], @@ -1534,9 +1475,7 @@ "with": "automation-shape-routing" } ], - "dependencies": [ - "automation-shape-routing" - ], + "dependencies": [], "description": "Mine caller-supplied usage history for repeated toil and emit ranked evidence.", "disposition": "keep_specialist", "effects": [], diff --git a/skills/codebase-recon/SKILL.md b/skills/codebase-recon/SKILL.md index 5ae55a885..eac9b3b83 100644 --- a/skills/codebase-recon/SKILL.md +++ b/skills/codebase-recon/SKILL.md @@ -35,8 +35,7 @@ metadata: canonical_status: canonical disposition: keep_specialist tier: execution - dependencies: - - doc + dependencies: [] output_contract: codebase-recon.v1 JSON validated by skills/codebase-recon/scripts/validate-output.sh with a companion cited report --- diff --git a/skills/doc/SKILL.md b/skills/doc/SKILL.md index f37878ec4..33d6938a6 100644 --- a/skills/doc/SKILL.md +++ b/skills/doc/SKILL.md @@ -26,9 +26,7 @@ metadata: canonical_status: canonical disposition: keep_specialist tier: product - dependencies: - - standards - - council + dependencies: [] output_contract: documentation files --- # Doc Skill diff --git a/skills/dueling-idea-genies/SKILL.md b/skills/dueling-idea-genies/SKILL.md index fc87be660..f010b0eea 100644 --- a/skills/dueling-idea-genies/SKILL.md +++ b/skills/dueling-idea-genies/SKILL.md @@ -32,8 +32,7 @@ metadata: canonical_status: canonical disposition: keep_strategy tier: judgment - dependencies: - - idea-genie + dependencies: [] output_contract: idea-challenge.v1 JSON validated by skills/dueling-idea-genies/scripts/validate-output.sh --- # Dueling Idea Genies diff --git a/skills/heal-skill/SKILL.md b/skills/heal-skill/SKILL.md index 780a4346b..a6a7a4b98 100644 --- a/skills/heal-skill/SKILL.md +++ b/skills/heal-skill/SKILL.md @@ -1,13 +1,12 @@ --- name: heal-skill -description: 'Repair skill hygiene and deep-audit SKILL.md quality (absorbed skill-auditor). Triggers: "heal-skill", "heal skill", "repair skill hygiene", "skill-auditor", "audit skill", "skill audit".' +description: 'Check or repair structural hygiene in AgentOps skill packages. Triggers: "heal skill", "repair skill hygiene", "audit skill structure", "check skill package".' practices: - refactoring -- code-complete hexagonal_role: supporting consumes: [] produces: -- audit-report.json +- skill-hygiene-report context_rel: - kind: customer-of with: skill-builder @@ -17,237 +16,84 @@ context: intent: mode: none sections: - exclude: - - HISTORY - - INTEL - - TASK + exclude: [HISTORY, INTEL, TASK] intel_scope: none metadata: capabilities: [heal_skill] - effects: [] + effects: [optional_skill_projection_repair] canonical_status: canonical disposition: keep_specialist tier: meta dependencies: [] -output_contract: 'stdout: heal report, auto-fixed skill files; deep audit mode: skills/heal-skill/schemas/audit-report.json' +output_contract: skills/heal-skill/schemas/audit-report.json --- -# /heal-skill — Automated Skill Maintenance -> **Purpose:** Detect and auto-fix common skill hygiene issues across the skills/ directory. +# /heal-skill — Check one or more skill packages -**YOU MUST EXECUTE THIS WORKFLOW. Do not just describe it.** +`heal-skill` is a specialist hygiene tool. It reports structural defects in +canonical source skills and generated Codex twins. With `--fix`, it repairs only +owned projections through their generators. It does not schedule work, operate +Git, validate a software candidate, or decide what happens after a failure. -## Constraints - -- Resolve every explicit target to a real direct child of `skills/` or `skills-codex/` before processing; reject traversal, missing/outside paths, and every symlink spelling because aliases make mutation ownership ambiguous. -- Run `--check` and deep audit read-only before any repair, because their exact finding codes and target path define the permitted mutation. -- Use `--fix` only for the documented auto-fixable structural codes, then rerun strict validation; never hand-edit generated Codex artifacts because parity projections have owned regeneration paths. -- Run the requested repair once and return every remaining audit result. A failed - repair is evidence for the caller, not permission to retry or route work. - -## Quick Start +## Inputs ```bash -/heal-skill # Check all skills (report only) -/heal-skill --fix # Auto-repair all fixable issues -/heal-skill --strict # Check all skills, exit 1 on findings (CI mode) -/heal-skill skills/council # Check a specific skill -/heal-skill --fix skills/validate # Fix a specific skill - -bash skills/heal-skill/scripts/audit.sh skills/council # Deep audit (read-only; see "Deep audit mode" below) +bash skills/heal-skill/scripts/heal.sh --check [skills/<slug> ...] +bash skills/heal-skill/scripts/heal.sh --check --strict [skills/<slug> ...] +bash skills/heal-skill/scripts/heal.sh --fix [skills/<slug> ...] ``` -## What It Detects +Every explicit target must be a real, direct child of `skills/` or +`skills-codex/`. Missing paths, traversal, and symlink spellings are rejected. -Auto-fix allowlist and principal diagnostics: +## Procedure -| Code | Issue | Auto-fixable? | -|------|-------|---------------| -| `MISSING_NAME` | No `name:` field in SKILL.md frontmatter | Yes -- adds name from directory | -| `MISSING_DESC` | No `description:` field in SKILL.md frontmatter | Yes -- adds placeholder | -| `NAME_MISMATCH` | Frontmatter `name` differs from directory name | Yes -- updates to match directory | -| `UNLINKED_REF` | File in references/ not linked in SKILL.md | Yes -- converts bare backtick refs to markdown links | -| `EMPTY_DIR` | Skill directory exists but has no SKILL.md | Yes -- removes empty directory | -| `MISSING_API_VERSION` | Selected source skill has no `skill_api_version` | Yes -- inserts version 1 in selected target(s) only | -| `DEAD_REF` | SKILL.md references a non-existent references/ file | No -- warn only | -| `SCRIPT_REF_MISSING` | SKILL.md references a scripts/ file that does not exist | No -- warn only | -| `INVALID_AO_CMD` | SKILL.md references an `ao` subcommand that does not exist (only runs if `ao` is on PATH) | No -- warn only | -| `DEAD_XREF` | SKILL.md references a `/skill-name` that has no matching skill directory | No -- warn only | +1. Resolve and contain all requested target directories. +2. Parse each `SKILL.md` frontmatter. +3. Check the path/name match, description, API version, disposition metadata, + and linked local references. +4. Print every finding once. +5. In `--fix` mode only, regenerate metadata-owned projections and scoped Codex + twins, then stop. -> `CATALOG_MISSING` was removed: it only ran when `skills/using-agentops/SKILL.md` existed, and that skill is gone. Catalog completeness is gated by `MISSING_DISPOSITION` against `docs/contracts/skill-dispositions.yaml`. +`--check` is read-only. `--strict` makes any finding produce exit 1. A failed +fix is returned to the caller; the skill does not retry or select another +action. -## Execution Steps +## Deep content audit -### Step 1: Run the heal script +The optional read-only content audit is: ```bash -# Check mode (default) -- report only, no changes -bash skills/heal-skill/scripts/heal.sh --check - -# Fix mode -- auto-repair what it can -bash skills/heal-skill/scripts/heal.sh --fix - -# Target a specific skill -bash skills/heal-skill/scripts/heal.sh --check skills/council -bash skills/heal-skill/scripts/heal.sh --fix skills/council +bash skills/heal-skill/scripts/audit.sh [--strict] [--json <path>] skills/<slug> ``` -**Checkpoint:** before accepting any mutation, confirm the target path and finding code are present in the read-only report and the code is documented as auto-fixable. +It combines the structural result with deterministic authoring checks and an +advisory quality score. It is not the core `Validate` phase, does not write a +`verdict.v2`, and has no delivery authority. Check definitions live in +[audit-checks.md](references/audit-checks.md); density scoring is described in +[context-density-checks.md](references/context-density-checks.md). -### Step 1A: Audit Codex Parity Drift When The Codex Bundle Looks Wrong +## Output -When the problem is not source-skill hygiene but `skills-codex/` drift, run the Codex parity audit first: +Structural findings are printed as: -```bash -bash scripts/audit-codex-parity.sh -bash scripts/audit-codex-parity.sh --skill swarm +```text +[FINDING_CODE] skills/example: concrete explanation ``` -Use this when a checked-in Codex skill still contains Claude-era primitives (`TaskCreate`, `TaskList`, `Tool: Task`), Claude backend references, or obviously broken runtime rewrites. +Deep audit JSON conforms to [audit-report.json](schemas/audit-report.json). +The caller owns any subsequent edit or invocation. -**Repair rule:** keep canonical shared behavior in `skills/<name>/SKILL.md`. Update `skills-codex/<name>/SKILL.md` when the shipped Codex artifact is wrong, and keep durable Codex-only tailoring in `skills-codex-overrides/<name>/SKILL.md`. +## Checks -After repair: +- Check mode never mutates files. +- Fix mode changes only an explicit source target and its owned projections. +- A second identical fix is idempotent. +- Generated Codex parity follows [codex-parity.md](references/codex-parity.md). +- Remaining non-fixable findings stay explicit. -```bash -bash scripts/audit-codex-parity.sh -bash scripts/validate-codex-override-coverage.sh -bash scripts/validate-codex-generated-artifacts.sh --scope worktree -``` +## Related executable specifications -**Checkpoint:** after a Codex repair, require parity, override coverage, and generated-artifact validation to exit zero before reporting the runtime bundle healthy. - -### Step 2: Interpret results - -- **Exit 0:** All clean, no findings. Also exit 0 for `--check` mode with findings (report-only). -- **Exit 1:** Findings reported with `--strict` or `--fix` flag. In `--fix` mode, fixable issues were repaired; re-run `--check` to confirm. - -### Step 3: Report to user - -Show the output. If `--fix` was used, summarize what changed. If `DEAD_REF` findings remain, advise the user to remove or update the broken references manually. - -## Output Format - -**Artifact directory:** stdout for human findings; `.agents/audits/` for JSON reports. -**Filename convention:** `<skill>-audit.json`. -**Serialization/schema format:** JSON matching `schemas/audit-report.json`. -**Validator command:** `jq -e . .agents/audits/<skill>-audit.json`. -**Downstream handoff:** consumed by the validation wave before repair. - -One line per finding: - -``` -[MISSING_NAME] skills/foo: No name field in frontmatter -[MISSING_DESC] skills/foo: No description field in frontmatter -[NAME_MISMATCH] skills/foo: Frontmatter name 'bar' != directory 'foo' -[UNLINKED_REF] skills/foo: refs/bar.md not linked in SKILL.md -[EMPTY_DIR] skills/foo: Directory exists but no SKILL.md -[DEAD_REF] skills/foo: SKILL.md links to non-existent refs/bar.md -[SCRIPT_REF_MISSING] skills/foo: references scripts/bar.sh but file not found -[INVALID_AO_CMD] skills/foo: references 'ao badcmd' which is not a valid subcommand -[DEAD_XREF] skills/foo: references /nonexistent but skill directory not found -``` - -## Deep audit mode (absorbed from /skill-auditor) - -Requests for `/skill-auditor` route here: the audit is the detect phase of heal, and it -lives at `scripts/audit.sh`. Unlike `--fix`, the audit is **read-only** — it reports; -repairs go through `heal.sh --fix` (Pass-1 issues) or hand edits (Pass-2 issues). - -```bash -bash skills/heal-skill/scripts/audit.sh [--strict] [--json <path>] skills/<name> -``` - -Two passes, then an aggregate verdict: - -- **Pass 1 — structural (delegated):** runs `heal.sh --check --strict <target>` and gates - on its exit code; it never reimplements the hygiene checks (heal is the source of truth). - A strict failure forces the aggregate verdict to FAIL but does NOT short-circuit Pass 2. -- **Pass 2 — content discipline:** 8 checks in `audit.sh` (triggers, frontloaded - constraints, rationale, checkpoints, output spec, quality rubric, references - modularization, trigger clarity). Definitions and accepted forms: - [references/audit-checks.md](references/audit-checks.md). -- **Verdict aggregation:** any check `fail` → FAIL; otherwise any `warn` → WARN; - otherwise PASS. Max-severity wins — no silent downgrade. Exit 0 on PASS/WARN - (1 on WARN under `--strict`), 1 on FAIL, 2 on usage error. -- **0-30 rubric (Pass 3) is ADVISORY-only:** `scripts/score_agentops_skill.py --audit-block` - folds a deterministic 10-category score (band C/B/A/S) into the report under `rubric`. - It never changes the PASS/WARN/FAIL verdict — it is a productization backlog signal, - not a ship blocker. The density block - ([references/context-density-checks.md](references/context-density-checks.md)) is - advisory too. -- **Accepted trigger forms:** `description-has-triggers` accepts only the forms - declared by the authoritative `repo-runtime` profile - (finding `f-2026-05-06-auditor-checks-must-fit-host-conventions` — audit checks must fit - the host's existing valid conventions). - -Report JSON conforms to [schemas/audit-report.json](schemas/audit-report.json); -`skill-builder` owns the canonical SKILL.md template. -Executable spec: [references/skill-auditor.feature](references/skill-auditor.feature). - -## Notes - -- The script is **idempotent** -- running `--fix` twice produces the same result. -- Explicit targets contain mutation: `--fix skills/foo` may change `foo` only; source siblings remain byte-identical. -- `DEAD_REF`, `SCRIPT_REF_MISSING`, `INVALID_AO_CMD`, and `DEAD_XREF` are warn-only because the correct resolution requires human judgment. -- `INVALID_AO_CMD` only runs if the `ao` CLI is available on PATH. Skipped silently otherwise. -- When run without a path argument, scans all directories under `skills/`. -- Use `--strict` for CI gates: exits 1 on any finding. Without `--strict`, check mode exits 0 even with findings. -- For Codex parity drift, use the audit script plus override-layer repair workflow in [references/codex-parity.md](references/codex-parity.md). The shell fixer is intentionally not allowed to rewrite generated Codex bodies directly. - -## Quality Checklist - -- Scope fidelity: every changed path is the audited target or its owned parity projection, and every repair maps to a reported finding code. -- Verification quality: strict heal/deep-audit and applicable Codex parity commands rerun after mutation with zero unresolved findings. -- Idempotence: a second identical repair run produces no additional diff, while non-auto-fixable findings remain explicit rather than guessed through. - -## Examples - -### Running a health check across all skills - -**User says:** `/heal-skill` - -**What happens:** -1. The heal script scans every directory under `skills/`, checking each for the nine issue types (missing name, missing description, name mismatch, unlinked references, empty directories, dead references, script reference integrity, CLI command validation, cross-reference validation). -2. Findings are printed one per line with issue codes (e.g., `[NAME_MISMATCH] skills/foo: Frontmatter name 'bar' != directory 'foo'`). -3. The script exits with code 0 in check mode (even with findings), or code 1 with `--strict` or `--fix` flags. - -**Result:** A diagnostic report showing all skill hygiene issues across the repository, with no files modified. - -### Auto-fixing a specific skill - -**User says:** `/heal-skill --fix skills/validate` - -**What happens:** -1. The heal script inspects only `skills/validate/`, running all per-skill checks against that skill. -2. For each fixable issue found (e.g., `MISSING_NAME`, `UNLINKED_REF`), the script applies the repair automatically -- adding the name from the directory, converting bare backtick references to markdown links, etc. -3. Any `DEAD_REF` findings are reported as warnings since they require human judgment to resolve. - -**Result:** The `skills/validate/SKILL.md` is repaired in place, with a summary of changes applied and any remaining warnings. - -## Troubleshooting - -| Problem | Cause | Solution | -|---------|-------|----------| -| `DEAD_REF` findings persist after `--fix` | Dead references are warn-only because the correct fix (delete, create, or update) requires human judgment | Manually inspect each dead reference and either create the missing file, remove the link from SKILL.md, or update the path | -| Script reports `EMPTY_DIR` for a skill in progress | The skill directory was created but SKILL.md has not been written yet | Either add a SKILL.md to the directory or remove the empty directory. Running `--fix` will remove it automatically | -| `NAME_MISMATCH` fix changed the wrong name | The script always updates the frontmatter `name` to match the directory name, not the other way around | If the directory name is wrong, rename the directory first, then re-run `--fix` | -| Script exits 0 but a skill still has issues | The issue type is not one of the checks the heal script detects | `heal.sh` covers structural hygiene only. Run the deep audit (`scripts/audit.sh`) for content-discipline checks; deeper quality issues require manual review or `/council` validation | -| Running `--fix` twice produces different output | This should not happen -- the script is idempotent | File a bug. Check if another process modified the skill files between runs | -| `skills-codex/` keeps regressing after sync | Mechanical conversion is preserving the wrong semantics | Run `bash scripts/audit-codex-parity.sh`, then move the durable Codex body rewrite into `skills-codex-overrides/<name>/SKILL.md` instead of patching generated output | - -## See Also - -- [skill-builder](../skill-builder/SKILL.md) — scaffolds new skills against the unified template; runs heal-skill (hygiene + deep audit) as self-checks -- [red-team](../validate/SKILL.md) — complementary; probes USABILITY (does the workflow actually work) vs the audit (is the structure correct) - -## References - -- [`skill-conformance-profiles.yaml`](../skill-builder/references/skill-conformance-profiles.yaml) — authoritative rules, severities, and boundary - -- [references/skill-stocktake.md](references/skill-stocktake.md) -- [references/codex-parity.md](references/codex-parity.md) -- [references/heal-skill.feature](references/heal-skill.feature) — Executable spec: detect hygiene issues, flag Codex-parity drift, auto-fix + report, --strict fails on remaining findings (soc-qk4b) -- [references/audit-checks.md](references/audit-checks.md) — deep-audit Pass-2 check definitions + accepted forms + PRODUCT.md mapping -- [references/context-density-checks.md](references/context-density-checks.md) — advisory density coverage logic and false-positive handling -- [references/skill-auditor.feature](references/skill-auditor.feature) — Executable spec for the absorbed deep audit mode: Pass 1 heal delegation, Pass 2 content checks, density report + productization score (soc-qk4b) +- [heal-skill.feature](references/heal-skill.feature) +- [skill-auditor.feature](references/skill-auditor.feature) diff --git a/skills/heal-skill/references/context-density-checks.md b/skills/heal-skill/references/context-density-checks.md index dd556538c..72231e499 100644 --- a/skills/heal-skill/references/context-density-checks.md +++ b/skills/heal-skill/references/context-density-checks.md @@ -29,9 +29,8 @@ Density Rule fields before that prose is passed into a fresh context session. ## Runnable Examples ```bash -bash skills/heal-skill/scripts/audit.sh skills/discovery bash skills/heal-skill/scripts/audit.sh skills/plan -bash skills/heal-skill/scripts/audit.sh skills/crank +bash skills/heal-skill/scripts/audit.sh skills/implement bash skills/heal-skill/scripts/audit.sh skills/validate ``` diff --git a/skills/heal-skill/references/skill-stocktake.md b/skills/heal-skill/references/skill-stocktake.md deleted file mode 100644 index db36fea24..000000000 --- a/skills/heal-skill/references/skill-stocktake.md +++ /dev/null @@ -1,123 +0,0 @@ -# Skill Stocktake — AI-Powered Quality Audit - -> Beyond structural hygiene: evaluate skill quality, actionability, and fitness using AI judgment. - -## Problem - -`heal.sh --strict` catches structural issues (missing frontmatter, unlinked refs, name mismatches). But it can't judge: -- Is this skill still actionable and current? -- Does it overlap with another skill? -- Should it be retired, merged, or improved? -- Is it used frequently enough to justify maintenance cost? - -## Solution: Two-Pass Evaluation - -### Pass 1: Inventory (Deterministic) -Run `heal.sh --strict` for structural checks (existing), then collect metadata: - -```bash -# For each skill directory -for skill_dir in skills/*/; do - skill_name=$(basename "$skill_dir") - skill_md="${skill_dir}SKILL.md" - - # Extract frontmatter - tier=$(grep 'tier:' "$skill_md" | head -1 | awk '{print $2}') - line_count=$(wc -l < "$skill_md") - ref_count=$(ls "${skill_dir}references/" 2>/dev/null | wc -l) - last_modified=$(stat -f %Sm -t %Y-%m-%d "$skill_md" 2>/dev/null || stat -c %y "$skill_md" 2>/dev/null | cut -d' ' -f1) - - echo "${skill_name}|${tier}|${line_count}|${ref_count}|${last_modified}" -done -``` - -### Pass 2: AI Evaluation (Judgment) -Spawn a subagent with the inventory table + evaluation criteria. Process ~20 skills per agent to stay within context. - -**Evaluation Criteria:** -- **Actionability:** Does the skill produce concrete artifacts when invoked? -- **Scope Fit:** Does it fit its declared tier? Is it doing too much or too little? -- **Uniqueness:** Does it overlap substantially with another skill? -- **Currency:** Are referenced tools, APIs, and patterns still current? -- **Trigger Clarity:** Could an LLM correctly decide when to invoke this skill? - -### Verdict Categories - -| Verdict | Meaning | Required Evidence | -|---------|---------|-------------------| -| **Keep** | Good as-is | Cite core value + evidence of use | -| **Improve** | Worth keeping, needs fixes | Cite specific section + action + target size | -| **Update** | Referenced tech is outdated | Cite what's outdated + what replaced it | -| **Retire** | Low quality, stale, or redundant | Cite (1) specific defect, (2) what covers same need | -| **Merge into [X]** | Substantial overlap with X | Cite overlap + line count + what content to integrate | - -**Reason Quality Rules:** -- Never write "unchanged" alone — restate core evidence -- For Retire: must name what covers the same need -- For Merge: include line count and describe content to integrate -- For Improve: name section + action + target size - -## Quick Scan Mode - -For re-evaluation after changes (avoids re-scanning unchanged skills): - -```bash -# 1. Check which skills changed since last evaluation -LAST_EVAL_DATE=$(jq -r '.evaluated_at' .agents/stocktake/results.json 2>/dev/null || echo "1970-01-01") -CHANGED_SKILLS=$(find skills/ -name "SKILL.md" -newer .agents/stocktake/results.json -exec dirname {} \; | xargs -I{} basename {}) - -# 2. If no changes, stop -if [ -z "$CHANGED_SKILLS" ]; then - echo "No skills changed since last evaluation ($LAST_EVAL_DATE)" - exit 0 -fi - -# 3. Re-evaluate only changed skills -echo "Quick scan: $CHANGED_SKILLS" -# Spawn agent with only changed skills -# Carry forward unchanged verdicts from results.json -``` - -## Results Schema - -```json -{ - "evaluated_at": "2026-03-21T10:00:00Z", - "mode": "full|quick", - "batch_progress": {"total": 54, "evaluated": 54, "status": "completed"}, - "skills": { - "vibe": { - "verdict": "Keep", - "reason": "Core judgment skill; produces council verdicts, complexity analysis, and actionable findings. 779 lines, 15 references — well-maintained.", - "last_modified": "2026-03-21" - }, - "converter": { - "verdict": "Improve", - "reason": "Cross-platform skill converter is useful but description frontmatter is thin (155 lines). Add concrete examples of Codex/Cursor output format. Target: 200+ lines.", - "last_modified": "2026-02-15" - } - } -} -``` - -## Integration with heal-skill - -Run stocktake as an optional mode of heal-skill: - -```bash -# Structural checks (existing) -bash skills/heal-skill/scripts/heal.sh --strict - -# Quality evaluation (new) -bash skills/heal-skill/scripts/heal.sh --stocktake # full evaluation -bash skills/heal-skill/scripts/heal.sh --stocktake --quick # quick scan -``` - -Results written to `.agents/stocktake/results.json`. Summary displayed to user. - -## When to Run - -- Before `/release` — ensures all skills are fit for distribution -- After adding/removing skills — detects overlap with existing skills -- Monthly maintenance — catches staleness and drift -- When skill count exceeds threshold (50+) — retirement pressure increases diff --git a/skills/heal-skill/scripts/heal.sh b/skills/heal-skill/scripts/heal.sh index fdb72082f..35941c594 100755 --- a/skills/heal-skill/scripts/heal.sh +++ b/skills/heal-skill/scripts/heal.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# One-pass structural audit for source and Codex skill packages. +# One-pass structural audit for source skill packages. set -euo pipefail MODE=check @@ -24,10 +24,8 @@ REPO_ROOT="${HEAL_REPO_ROOT:-$(cd "$SCRIPT_DIR/../../.." && pwd)}" REPO_ROOT="$(cd "$REPO_ROOT" && pwd -P)" if [[ ${#TARGETS[@]} -eq 0 ]]; then - for root in "$REPO_ROOT/skills" "$REPO_ROOT/skills-codex"; do - for path in "$root"/*; do - [[ -d "$path" ]] && TARGETS+=("$path") - done + for path in "$REPO_ROOT/skills"/*; do + [[ -d "$path" && -f "$path/SKILL.md" ]] && TARGETS+=("$path") done fi @@ -38,7 +36,7 @@ for target in "${TARGETS[@]}"; do [[ ! -L "$target" ]] || { echo "heal.sh: symlink targets are not accepted: $target" >&2; exit 2; } resolved="$(cd "$target" && pwd -P)" case "$(dirname "$resolved")" in - "$REPO_ROOT/skills"|"$REPO_ROOT/skills-codex") ;; + "$REPO_ROOT/skills") ;; *) echo "heal.sh: target is not a direct skill package: $target" >&2; exit 2 ;; esac normalized+=("$resolved") diff --git a/skills/heal-skill/scripts/validate.sh b/skills/heal-skill/scripts/validate.sh index 2efdc5a3c..628d9c7f2 100755 --- a/skills/heal-skill/scripts/validate.sh +++ b/skills/heal-skill/scripts/validate.sh @@ -1,54 +1,39 @@ #!/usr/bin/env bash set -euo pipefail -SKILL_DIR="$(cd "$(dirname "$0")/.." && pwd)" -PASS=0; FAIL=0 +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SKILL_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +REPO_ROOT="$(cd "$SKILL_DIR/../.." && pwd)" -check() { if bash -c "$2"; then echo "PASS: $1"; PASS=$((PASS + 1)); else echo "FAIL: $1"; FAIL=$((FAIL + 1)); fi; } - -check "SKILL.md exists" "[ -f '$SKILL_DIR/SKILL.md' ]" -check "SKILL.md has YAML frontmatter" "head -1 '$SKILL_DIR/SKILL.md' | grep -q '^---$'" -check "Codex parity reference exists" "[ -f '$SKILL_DIR/references/codex-parity.md' ]" -check "SKILL.md links Codex parity reference" "grep -q 'references/codex-parity.md' '$SKILL_DIR/SKILL.md'" -check "constraints are front-loaded" "awk 'BEGIN{n=0;i=0;found=0} /^---$/{n++;next} n==2{i++; if (/^## Constraints$/){found=1;exit} if (i>80) exit} END{exit !found}' '$SKILL_DIR/SKILL.md'" -check "constraints bind repair scope" "grep -Fq 'real direct child of' '$SKILL_DIR/SKILL.md' && grep -Fq 'read-only before any repair' '$SKILL_DIR/SKILL.md' && grep -Fq 'documented auto-fixable structural codes' '$SKILL_DIR/SKILL.md' && grep -Fq 'plain refutation is not an andon' '$SKILL_DIR/SKILL.md'" -check "MISSING_API_VERSION autofix is documented" "grep -Fq 'MISSING_API_VERSION' '$SKILL_DIR/SKILL.md' && grep -Fq 'selected target(s) only' '$SKILL_DIR/SKILL.md'" -check "verification checkpoints are explicit" "test \$(grep -c '^\*\*Checkpoint:\*\*' '$SKILL_DIR/SKILL.md') -ge 2" -check "quality checklist has three rules" "awk '/^## Quality Checklist$/{f=1;next} f&&/^## /{exit} f&&/^- /{n++} END{exit !(n>=3)}' '$SKILL_DIR/SKILL.md'" -check "kernel stays within 250 lines" "test \$(wc -l < '$SKILL_DIR/SKILL.md') -le 250" - -# --- Deep audit mode (absorbed from /skill-auditor) --- -for f in scripts/audit.sh scripts/score_agentops_skill.py references/audit-checks.md references/context-density-checks.md schemas/audit-report.json; do - check "audit artifact $f exists" "[ -f '$SKILL_DIR/$f' ]" +for path in \ + SKILL.md \ + scripts/heal.sh \ + scripts/audit.sh \ + scripts/score_agentops_skill.py \ + schemas/audit-report.json \ + references/audit-checks.md \ + references/codex-parity.md; do + [[ -f "$SKILL_DIR/$path" ]] || { + echo "heal-skill validate: missing $path" >&2 + exit 1 + } done -# audit.sh must contain all 8 Pass-2 check function names -for fn in check_description_has_triggers check_constraints_frontloaded check_rationale_present check_verification_checkpoints check_output_spec_explicit check_quality_rubric check_references_modularization check_trigger_clarity; do - check "audit.sh has $fn" "grep -q '^${fn}()' '$SKILL_DIR/scripts/audit.sh'" -done +bash -n "$SKILL_DIR/scripts/heal.sh" "$SKILL_DIR/scripts/audit.sh" +bash "$SKILL_DIR/scripts/heal.sh" --check --strict "$SKILL_DIR" -# Advisory density block: function + all six fields, outside the Pass-2 verdict loop -check "audit.sh has density function" "grep -q '^check_density_field()' '$SKILL_DIR/scripts/audit.sh'" -for field in intent boundary evidence decision constraint next_action; do - check "audit.sh has density field $field" "grep -q '$field' '$SKILL_DIR/scripts/audit.sh'" -done +before="$(find "$SKILL_DIR" -type f -exec shasum -a 256 {} + | sort | shasum -a 256 | awk '{print $1}')" +bash "$SKILL_DIR/scripts/heal.sh" --check "$SKILL_DIR" >/dev/null +after="$(find "$SKILL_DIR" -type f -exec shasum -a 256 {} + | sort | shasum -a 256 | awk '{print $1}')" +[[ "$before" == "$after" ]] || { + echo "heal-skill validate: check mode mutated its target" >&2 + exit 1 +} -# Pass 1 must delegate to heal.sh --check --strict and gate on exit code -check "audit.sh delegates via --check --strict" "grep -q -- '--check --strict' '$SKILL_DIR/scripts/audit.sh'" -check "audit.sh gates Pass 1 on exit code" "grep -q 'PASS1_EXIT_CODE' '$SKILL_DIR/scripts/audit.sh'" -check "audit report includes Pass-1 exit_code" "grep -q '\"exit_code\": %s' '$SKILL_DIR/scripts/audit.sh'" -check "API-version fix follows explicit targets" "grep -Fq 'an explicit --fix target must never mutate a sibling' '$SKILL_DIR/scripts/heal.sh' && grep -Fq 'for check_dir in' '$SKILL_DIR/scripts/heal.sh'" -check "explicit targets canonicalize fail-closed" "grep -Fq 'symlink spellings are not allowed' '$SKILL_DIR/scripts/heal.sh' && grep -Fq 'parent traversal is not allowed' '$SKILL_DIR/scripts/heal.sh' && grep -Fq 'not a direct child of an allowed skill root' '$SKILL_DIR/scripts/heal.sh'" -check "audit counts complete autofix allowlist" "grep -Fq 'EMPTY_DIR|MISSING_API_VERSION' '$SKILL_DIR/scripts/audit.sh'" -check "mutation-boundary fixture exists" "[ -x '$SKILL_DIR/scripts/test-mutation-boundaries.sh' ]" -check "mutation-boundary fixture passes" "bash '$SKILL_DIR/scripts/test-mutation-boundaries.sh'" +if rg -n 'ao land|git (commit|push)|append-skill-disposition|flywheel close-loop' \ + "$SKILL_DIR/scripts/heal.sh" "$SKILL_DIR/scripts/audit.sh" \ + "$SKILL_DIR/scripts/score_agentops_skill.py"; then + echo "heal-skill validate: lifecycle authority remains" >&2 + exit 1 +fi -# Stale check name must be gone (pre-mortem F1) -check "audit.sh has no stale check_description_multiline" "! grep -q 'check_description_multiline' '$SKILL_DIR/scripts/audit.sh'" - -# Pass 3 rubric wiring (advisory): scorer invoked, rubric block emitted, --audit-block supported -check "audit.sh invokes rubric scorer" "grep -q 'score_agentops_skill.py' '$SKILL_DIR/scripts/audit.sh'" -check "audit.sh emits rubric block" "grep -q '\"rubric\": %s' '$SKILL_DIR/scripts/audit.sh'" -check "scorer supports --audit-block" "grep -q -- '--audit-block' '$SKILL_DIR/scripts/score_agentops_skill.py'" - -echo ""; echo "Results: $PASS passed, $FAIL failed" -[ $FAIL -eq 0 ] && exit 0 || exit 1 +echo "heal-skill validate: PASS" diff --git a/skills/idea-genie/SKILL.md b/skills/idea-genie/SKILL.md index 4022aeb38..0efc0bf6f 100644 --- a/skills/idea-genie/SKILL.md +++ b/skills/idea-genie/SKILL.md @@ -14,7 +14,7 @@ skill_api_version: 1 user-invocable: true metadata: tier: execution - dependencies: [research] + dependencies: [] capabilities: [generate_evidenced_options] effects: [write_idea_portfolio] canonical_status: canonical diff --git a/skills/implement/scripts/validate.sh b/skills/implement/scripts/validate.sh old mode 100644 new mode 100755 diff --git a/skills/learn/scripts/validate.sh b/skills/learn/scripts/validate.sh old mode 100644 new mode 100755 diff --git a/skills/ms/SKILL.md b/skills/ms/SKILL.md index 8ec73e7c3..dd404e3d2 100644 --- a/skills/ms/SKILL.md +++ b/skills/ms/SKILL.md @@ -98,19 +98,19 @@ ms config # resolved config + skill_paths - **Path:** search, load, and admin results are returned on `stdout`; durable index state remains under `~/Library/Application Support/ms/`. - **Filename:** no result filename is created by this skill; callers capture CLI output explicitly when they need a durable artifact. - **Format:** MCP returns structured tool data; CLI automation uses JSON or JSONL, with full skill text at `.data.content` for `ms load --full -O json`. -- **Validation command:** run `skills/ms/scripts/validate.sh` for the production lifecycle contract and `scripts/ms-reindex.sh --check-source` for normalized source equivalence. -- **Downstream handoff:** the invoking agent consumes full loaded guidance, routes production skill intent to the canonical factory, and records `ms outcome` only after downstream use and validation—not after retrieval alone. +- **Validation command:** run `skills/ms/scripts/validate.sh` for the retrieval boundary and `scripts/ms-reindex.sh --check-source` for normalized source equivalence. +- **Downstream handoff:** return the loaded guidance and source identity to the caller. Retrieval never chooses or starts a workflow. ## Production Skill Handoff -**Production-intent handoff:** When the query or intended use is to create, edit, heal, or promote a skill, `ms` only retrieves full guidance and then routes execution to `agentops-skill-factory`, `skill-builder`, `heal-skill`, and the factory-selected validation primitives. +**Production-intent handoff:** When the query concerns creating or editing a skill, `ms` retrieves relevant guidance and stops. The caller may separately invoke `skill-builder`, `heal-skill`, or another authoring tool. **Authority boundary:** `skills/**` is canonical source; the generator owns the `ms` Codex twin and other projections. Never edit the index, loaded copies, or generated projections as source. -`ms` never promotes a skill or interprets a validator result. A failed search, +`ms` never validates or interprets downstream work. A failed search, load, write, or reindex is returned as evidence and ends this invocation. -**Outcome timing:** Record `ms outcome` only after the downstream factory use and validation complete, never after retrieval alone. +**Outcome timing:** Record `ms outcome` only after the caller has independent evidence about downstream usefulness, never after retrieval alone. That observation does not change core state. --- @@ -157,9 +157,9 @@ Scenario: A stale local projection fails closed - Full loads preserve the complete runnable guidance rather than a metadata card or packed overview. - Search/load reads use the verified MCP path, while feedback and outcome writes use the verified CLI boundary. - Any rebuild finishes with stale servers swept and source equivalence reported. -- Production skill intent leaves `ms` after retrieval and enters the canonical factory against `skills/**`; generated twins and loaded/indexed copies are never hand-edited as source. -- Promotion carries deterministic evidence and a fresh-context independent verdict; no producer self-certification is accepted. -- `REFUTED` stays in automatic repair, while exactly one helper is reserved for a tripped breaker and `ms outcome` waits for downstream validation. +- Production skill intent leaves `ms` after retrieval; generated twins and loaded/indexed copies are never hand-edited as source. +- Search and load results remain advisory inputs, never proof that downstream work is correct. +- `ms outcome` records observed usefulness only after independent downstream evidence. ## References diff --git a/skills/ms/scripts/validate.sh b/skills/ms/scripts/validate.sh index 36c0f5d14..b90d4921b 100755 --- a/skills/ms/scripts/validate.sh +++ b/skills/ms/scripts/validate.sh @@ -3,46 +3,13 @@ set -euo pipefail SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" SKILL="$SKILL_DIR/SKILL.md" -TMP="$(mktemp -d)" -trap 'rm -rf "$TMP"' EXIT -# Exact Markdown contract literals: backticks are data, never substitutions. -# shellcheck disable=SC2016 -MARKERS=( - '## Output Specification' - '- **Validation command:** run `skills/ms/scripts/validate.sh` for the production lifecycle contract and `scripts/ms-reindex.sh --check-source` for normalized source equivalence.' - '- **Downstream handoff:** the invoking agent consumes full loaded guidance, routes production skill intent to the canonical factory, and records `ms outcome` only after downstream use and validation—not after retrieval alone.' - '## Production Skill Handoff' - '**Production-intent handoff:** When the query or intended use is to create, edit, heal, or promote a skill, `ms` only retrieves full guidance and then routes execution to `agentops-skill-factory`, `skill-builder`, `heal-skill`, and the factory-selected validation primitives.' - '**Authority boundary:** `skills/**` is canonical source; the generator owns the `ms` Codex twin and other projections. Never edit the index, loaded copies, or generated projections as source.' - '**Promotion gate:** Promotion requires deterministic checks plus a fresh-context pawl or independent verdict; the producing agent never self-certifies completion.' - '**Failure routing:** A plain `REFUTED` verdict auto-repairs and revalidates. Only a tripped circuit breaker enters `HOLD` and receives exactly one bounded helper consultation before re-earning an independent verdict.' - '**Outcome timing:** Record `ms outcome` only after the downstream factory use and validation complete, never after retrieval alone.' - '## Quality Checklist' - '- Production skill intent leaves `ms` after retrieval and enters the canonical factory against `skills/**`; generated twins and loaded/indexed copies are never hand-edited as source.' - '- Promotion carries deterministic evidence and a fresh-context independent verdict; no producer self-certification is accepted.' - '- `REFUTED` stays in automatic repair, while exactly one helper is reserved for a tripped breaker and `ms outcome` waits for downstream validation.' -) +[[ -s "$SKILL" ]] +grep -q '^name: ms$' "$SKILL" +grep -q '^ effects: \[\]$' "$SKILL" +grep -Fq 'Keep `ms` retrieval-only for production skill work.' "$SKILL" +grep -Fq '**Authority boundary:** `skills/**` is canonical source' "$SKILL" +grep -Fq '**Outcome timing:** Record `ms outcome` only after the caller has independent evidence' "$SKILL" +! grep -Eiq 'pawl|AUTO-REDO|ONE-HELPER|circuit breaker|canonical factory|promotes a skill' "$SKILL" -validate_contract() { - local file="$1" marker - [[ -s "$file" ]] || return 1 - for marker in "${MARKERS[@]}"; do - grep -Fqx -- "$marker" "$file" || return 1 - done -} - -delete_one_negative_fixture() { - local marker variant="$TMP/missing-marker.md" - for marker in "${MARKERS[@]}"; do - grep -Fvx -- "$marker" "$SKILL" >"$variant" - if validate_contract "$variant"; then - echo "ms lifecycle validator accepted a missing marker: $marker" >&2 - return 1 - fi - done -} - -validate_contract "$SKILL" -delete_one_negative_fixture -echo "ms lifecycle contract: PASS" +echo "ms retrieval contract: PASS" diff --git a/skills/pattern-mining/SKILL.md b/skills/pattern-mining/SKILL.md index e7fe3d746..510b62222 100644 --- a/skills/pattern-mining/SKILL.md +++ b/skills/pattern-mining/SKILL.md @@ -34,8 +34,7 @@ metadata: canonical_status: canonical disposition: keep_specialist tier: execution - dependencies: - - operationalize + dependencies: [] output_contract: pattern-mining.v1 JSON validated by skills/pattern-mining/scripts/validate-output.sh --- diff --git a/skills/plan/scripts/validate.sh b/skills/plan/scripts/validate.sh old mode 100644 new mode 100755 diff --git a/skills/postmortem/SKILL.md b/skills/postmortem/SKILL.md index 5cb2e4101..0fc86b498 100644 --- a/skills/postmortem/SKILL.md +++ b/skills/postmortem/SKILL.md @@ -1,20 +1,15 @@ --- name: postmortem -description: 'Test an explicit retrospective causal question against evidence and counterfactuals after Validate and Learn. Triggers: "postmortem", "causal retrospective", "test a retrospective hypothesis".' +description: 'Optionally test a retrospective causal question against durable verdict evidence. Triggers: "postmortem", "causal retrospective", "test a retrospective hypothesis".' practices: - sre - lean-startup hexagonal_role: domain consumes: -- learn -- toil-mining +- verdict.v2 produces: - postmortem-report.md -context_rel: -- kind: customer-of - with: learn -- kind: customer-of - with: toil-mining +context_rel: [] skill_api_version: 1 user-invocable: true metadata: @@ -23,9 +18,7 @@ metadata: canonical_status: canonical disposition: keep_strategy tier: judgment - dependencies: - - council - - toil-mining + dependencies: [] context: window: fork intent: @@ -45,7 +38,7 @@ output_contract: skills/postmortem/references/postmortem.feature ## Critical Constraints - Because proof and causal inference are different judgments, Postmortem is retrospective causal analysis, not the general learning umbrella and not a completion gate. -- It consumes an immutable Validate verdict plus Learn receipt and does not re-run acceptance validation by default because Validate already owns that proof. +- It consumes immutable Validate verdict evidence and does not re-run acceptance validation because Validate already owns that proof. - Treat causal statements as hypotheses because causal confidence must survive alternatives. Separate observed sequence, contributing conditions, counterfactuals, and unknowns. @@ -59,7 +52,7 @@ output_contract: skills/postmortem/references/postmortem.feature ## Workflow -1. Pin the verdict, Learn receipt, delivered artifact, and explicit causal +1. Pin the verdict, subject evidence, and explicit causal question. 2. Reconstruct the evidence-backed timeline without importing hidden author reasoning as fact. @@ -78,7 +71,7 @@ output_contract: skills/postmortem/references/postmortem.feature - **Serialization/schema format:** Markdown with causal question, pinned inputs, timeline, hypotheses, evidence, counterfactuals, unknowns, and experiments. - **Validator command:** `bash skills/postmortem/scripts/validate.sh`. -- **Downstream handoff:** Learn or the orchestrator may consume the analysis; they own +- **Downstream handoff:** Learn or the caller may consume the analysis; they own any bookkeeping, promotion, planning, or delivery decision. ## Quality Checklist diff --git a/skills/postmortem/references/postmortem.feature b/skills/postmortem/references/postmortem.feature index 267ecfa61..a788cca05 100644 --- a/skills/postmortem/references/postmortem.feature +++ b/skills/postmortem/references/postmortem.feature @@ -4,7 +4,7 @@ Feature: Postmortem tests retrospective causal claims So that retrospective stories do not become unsupported doctrine Scenario: An explicit causal question receives bounded analysis - Given an immutable Validate verdict and Learn receipt + Given an immutable Validate verdict And an explicit retrospective causal question When Postmortem reconstructs the evidence-backed timeline Then it distinguishes supported claims, rejected claims, and unknowns @@ -13,5 +13,5 @@ Feature: Postmortem tests retrospective causal claims Scenario: Postmortem does not repeat validation Given the acceptance verdict is already immutable When Postmortem begins - Then it does not re-run the full acceptance suite by default + Then it does not re-run acceptance validation And it does not change proof, bookkeeping, planning, tracker, or delivery state diff --git a/skills/postmortem/scripts/validate.sh b/skills/postmortem/scripts/validate.sh index d06937a4e..615950eb4 100755 --- a/skills/postmortem/scripts/validate.sh +++ b/skills/postmortem/scripts/validate.sh @@ -5,7 +5,7 @@ skill_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" grep -q '^name: postmortem$' "$skill_dir/SKILL.md" grep -Fq 'retrospective causal analysis' "$skill_dir/SKILL.md" -grep -Fq 'does not re-run acceptance validation by default' "$skill_dir/SKILL.md" +grep -Fq 'does not re-run acceptance validation' "$skill_dir/SKILL.md" grep -Fq 'counterfactual' "$skill_dir/SKILL.md" grep -Fq 'Empty or inconclusive analysis is valid' "$skill_dir/SKILL.md" grep -q '^Feature: Postmortem tests retrospective causal claims$' "$skill_dir/references/postmortem.feature" diff --git a/skills/premortem/SKILL.md b/skills/premortem/SKILL.md index c616a0af8..f6ebf07f9 100644 --- a/skills/premortem/SKILL.md +++ b/skills/premortem/SKILL.md @@ -1,137 +1,55 @@ --- name: premortem -description: 'Use when: an exact plan needs a verdict. Stress-tests readiness before work with one fresh, independent judge.' -practices: -- adr -- mythical-man-month -- design-by-contract +description: 'Optionally challenge a frozen plan with one fresh independent judge before implementation. Triggers: "premortem", "challenge this plan", "what could make this plan fail".' +practices: [design-by-contract, adr] hexagonal_role: domain -consumes: -- standards -produces: -- premortem-plan-verdict.v1 +consumes: [plan-packet.v1] +produces: [premortem-plan-review.v1] context_rel: -- kind: shared-kernel - with: standards +- kind: supplier-to + with: plan skill_api_version: 1 +user-invocable: true metadata: - capabilities: [premortem] - effects: [] + capabilities: [challenge_plan] + effects: [write_advisory_plan_review] canonical_status: canonical disposition: keep_strategy graph_root: true tier: judgment - dependencies: - - council -context: - window: fork - intent: - mode: task - sections: - exclude: - - HISTORY - intel_scope: full -output_contract: skills/premortem/schemas/plan-verdict.schema.json + dependencies: [] +output_contract: schemas/premortem-plan-review.v1.schema.json --- -# Premortem Skill -> **Question:** Is this exact plan ready to implement? -> **Boundary:** Premortem owns the only semantic plan-readiness verdict. +# Premortem -## Constraints +Premortem is an optional plan-challenge strategy. It asks one fresh context to +identify concrete ways a frozen PlanPacket could fail before implementation. +It is not part of the required RPI sequence and does not authorize readiness. -- Judge the plan, never the implementation or delivery mechanism. -- Use one fresh-context judge with `author_id != judge_id`. Model and family - metadata are optional; no risk class requires different model families. -- Bind the verdict to the repository-relative plan path and its SHA-256. Any - plan edit invalidates the verdict. -- Emit exactly `PASS` or `FAIL`. `PASS` has zero blockers. `FAIL` contains the - complete nonempty blocker set in one response. -- Report only concrete, evidence-bound defects that invalidate acceptance, - correctness, safety, dependencies, scope, or a claimed contract. -- Do not own retries, attempt maps, budgets, helper state, implementation, - delivery, tracker closure, or operator escalation. The orchestrator chooses - repair or replanning after reading the verdict. -- A council, mixed panel, or Dueling Idea Genies artifact may inform the judge, - but none substitutes for this exact-plan verdict. +## Workflow -## Loop position +1. Pin the PlanPacket digest, acceptance, non-goals, evidence requirements, and + declared write scope. +2. Use one fresh judge with a context ID distinct from the plan author. +3. Test acceptance completeness, edge behavior, scope, dependencies, + reversibility, and evidence shape against cited repository facts. +4. Return one complete set of concrete findings and checked/not-checked scope. +5. Stop. The caller decides whether to revise the plan or invoke RPI. -Premortem runs once after Plan freezes the final plan and before the first -implementation leaf is pulled. It consumes the plan plus its acceptance, -dependency graph, write scopes, non-goals, rollback, and deterministic planning -receipts. It produces one immutable `premortem-plan-verdict.v1` JSON artifact. +Council or Dueling Idea Genies may be caller-supplied evidence, but Premortem +does not require either strategy and cannot turn consensus into approval. -Between implementation waves, reuse the verdict while the exact plan digest is -unchanged. A materially changed plan requires an explicit orchestrator request -for a new Premortem verdict; Validate and Learn cannot invoke Premortem themselves. +## Boundary -## Execution +- Emit advisory findings, not `verdict.v2`, readiness, admission, or permission. +- Do not implement, validate the candidate, retry, repair, schedule, claim, + change acceptance, operate Git, close work, release, or deliver. +- Any plan edit creates a new subject for a later caller-initiated Premortem. -1. Resolve one current plan path. Reject a missing or stale plan rather than - inferring intent from chat. -2. Compute the plan SHA-256 and record the plan author identity. -3. Retrieve only directly matched compiled prevention from - `.agents/premortem-checks/*.md`, falling back to - `.agents/findings/registry.jsonl`. Missing inputs skip silently; malformed - entries are ignored with one concise warning. -4. Dispatch one runtime-native fresh judge. Use Council only when the operator - explicitly requests a panel or the decision is genuinely contested. -5. Check all applicable acceptance, dependency, write-scope, migration, - reversibility, test-shape, capability-reuse, and rollback claims. The - detailed checklist is in - [mandatory-checks.md](references/mandatory-checks.md). -6. Return the complete blocker set once. Cosmetic, theoretical, pre-existing, - and out-of-scope observations are notes, not blockers. -7. Write the JSON verdict and validate both its schema and live plan digest: +## Output - ```bash - skills/premortem/scripts/validate-output.sh \ - .agents/council/YYYY-MM-DD-premortem-<topic>.json \ - "$(git rev-parse --show-toplevel)" - ``` - -## Verdict contract - -```json -{ - "schema_version": "premortem-plan-verdict.v1", - "plan": {"path": ".agents/plans/example.md", "sha256": "<64 hex>"}, - "author_id": "planner-context", - "judge_id": "fresh-judge-context", - "verdict": "PASS", - "blockers_complete": true, - "blockers": [] -} -``` - -For `FAIL`, each blocker has a stable `id`, a concrete `claim`, and one or more -`evidence` references. Optional `author_model` and `judge_model` objects may -record `name` and `family`; the validator deliberately does not compare family. - -## Output Specification - -- **Artifact path:** `.agents/council/YYYY-MM-DD-premortem-<topic>.json` -- **Schema:** [plan-verdict.schema.json](schemas/plan-verdict.schema.json) -- **Validator:** `skills/premortem/scripts/validate-output.sh <verdict> <repo-root>` -- **Downstream handoff:** `PASS` permits the orchestrator to pull the first - implementation leaf. `FAIL` returns the complete evidence set to the - orchestrator for one consolidated repair decision or replanning. - -## Quality checklist - -- The recorded plan digest matches the file the judge actually read. -- Author and judge identities differ. -- The verdict is binary and the blocker set is explicitly complete. -- Every blocker cites the plan or a deterministic evidence path. -- No optional review topology is presented as readiness authority. - -## References - -- [mandatory-checks.md](references/mandatory-checks.md) -- [premortem.feature](references/premortem.feature) -- [scope-mode.md](references/scope-mode.md) -- [temporal-interrogation.md](references/temporal-interrogation.md) -- [examples.md](references/examples.md) -- [write-premortem-output.md](references/write-premortem-output.md) -- [compiled-prevention.md](references/compiled-prevention.md) +Return `premortem-plan-review.v1` with the plan digest, author and judge context +IDs, findings, evidence references, `checked`, and `not_checked`. An empty +finding set means only that this optional challenge found no concrete defect; +it is never a lifecycle gate. diff --git a/skills/premortem/references/compiled-prevention.md b/skills/premortem/references/compiled-prevention.md deleted file mode 100644 index e05e51d44..000000000 --- a/skills/premortem/references/compiled-prevention.md +++ /dev/null @@ -1,108 +0,0 @@ -# Compiled Prevention Loading - -> Extracted from premortem/SKILL.md on 2026-04-11. - -## 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 - # Decision-point pull: prefer the curated GOLD wiki with compact pointers - # (no bodies) and a hard top-K cap — bookend-bounded (ADR-0002). Fall back to - # the raw .agents/ corpus, but WARN loudly so a missing gold wiki is never a - # silent zero-result. - if [ -d .ao/wiki ]; then - ao lookup --query "<plan goal or title>" --gold --pointers --limit 3 2>/dev/null | head -20 - else - echo "WARN: gold wiki (.ao/wiki) absent — run 'ao wiki gold' to enable gold retrieval; falling back to raw .agents/ corpus" >&2 - ao lookup --query "<plan goal or title>" --limit 3 2>/dev/null | head -20 - fi -fi -``` - -If learnings are returned, include them as `known_context` in the review packet. Cite any learning by filename when it influences a prediction. The gold-absent path WARNs (not silent) and falls back to the raw corpus; skip silently only 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/premortem-checks/*.md` when they exist. This is separate from flywheel search and does NOT get skipped by `--quick`. - -Use the tracked contracts in `docs/contracts/finding-compiler.md` and `docs/contracts/finding-registry.md`: - -- prefer compiled premortem checks first -- rank by severity, `applicable_when` overlap, language overlap, and literal plan-text overlap -- when the plan names files, rank changed-file overlap ahead of generic keyword matches -- cap at top 5 findings / check files -- if compiled checks are missing, incomplete, or fewer than the matched finding set, fall back to `.agents/findings/registry.jsonl` -- fail open: - - missing compiled directory or registry -> skip silently - - empty compiled directory or registry -> skip silently - - malformed line -> warn and ignore that line - - unreadable file -> warn once and continue without findings - -Include matched entries in the council packet as `known_risks` with: - -- `id` -- `pattern` -- `detection_question` -- `checklist_item` - -Use the same ranked packet contract as `/plan`: compiled checks first, then active findings fallback, then matching high-severity next-work context when relevant. Avoid re-ranking with an unrelated heuristic inside premortem; the point is consistent carry-forward, not a fresh retrieval policy per phase. - -### Record Citations for Applied Knowledge - -After including matched entries as `known_risks`, record each citation so the flywheel feedback loop can track influence: - -```bash -# Only use "applied" when the finding actually influenced the council packet. -# Use "retrieved" for items loaded but not referenced in the risk assessment. -ao metrics cite "<finding-path>" --type applied 2>/dev/null || true # influenced risk assessment -ao metrics cite "<finding-path>" --type retrieved 2>/dev/null || true # loaded but not used -``` - -### Section Evidence - -When lookup results include `section_heading`, `matched_snippet`, or `match_confidence` fields, prefer the matched section over the whole file — it pinpoints the relevant portion. Higher `match_confidence` (>0.7) means the section is a strong match; lower values (<0.4) are weaker signals. Use the `matched_snippet` as the primary context rather than reading the full file. - -## Step 1a: Search Knowledge Flywheel (skip if `--quick`) - -Only run this step for `--deep`, `--mixed`, or `--debate`. - -```bash -if command -v ao &>/dev/null; then - ao search "plan validation lessons <goal>" 2>/dev/null | head -10 -fi -``` - -If ao returns prior plan review findings, include them as context for the council packet. Skip silently if ao is unavailable or returns no results. - -## Step 1b: Check for Product Context - -Quick mode does not create a separate product-review phase. Include product -context in the fresh judge's bounded packet only when the plan changes product -behavior or cites `PRODUCT.md`. Deep modes may add a dedicated perspective. - -```bash -if [ -f PRODUCT.md ]; then - # PRODUCT.md exists — include product perspectives alongside plan-review -fi -``` - -When `PRODUCT.md` is relevant to the plan and the user did not pass an explicit -`--preset` override: - -1. Read `PRODUCT.md` content and include in the council packet via `context.files` -2. In `--quick` mode, the one fresh judge assesses only the product claims in - the bounded packet. -3. In non-quick modes, add a single consolidated `product` perspective to the council invocation: - ``` - /council --preset=plan-review --perspectives="product" validate <plan-path> - ``` - This yields 3 judges total (2 plan-review + 1 product). The product judge covers user-value, adoption-barriers, and competitive-position in a single review. -4. With `--deep`: 5 judges (4 plan-review + 1 product). - -When the user passed an explicit `--preset`, it takes precedence. - -When product context is absent or irrelevant, proceed without it. - -> **Tip:** Create `PRODUCT.md` from `docs/PRODUCT-TEMPLATE.md` to enable product-aware plan validation. diff --git a/skills/premortem/references/examples.md b/skills/premortem/references/examples.md deleted file mode 100644 index a06e1194b..000000000 --- a/skills/premortem/references/examples.md +++ /dev/null @@ -1,67 +0,0 @@ -# Premortem Examples - -> Extracted from premortem/SKILL.md on 2026-04-11. - -## Validate a Plan (Default — One Fresh Judge) - -**User says:** `/premortem .agents/plans/2026-02-05-auth-system.md` - -**What happens:** - -1. Agent reads the auth system plan -2. Sends the bound plan and acceptance packet to one fresh-context judge -3. The judge finds missing error handling for token expiry -4. Premortem verdict: FAIL with one complete blocker set -5. The author repairs the plan; a fresh verdict on the changed digest is PASS - -**Result:** A binary, digest-bound plan verdict with actionable evidence. - -## Optional Panel-Assisted Plan Review - -**User says:** `/premortem --deep .agents/plans/2026-02-05-auth-system.md` - -**What happens:** - -1. Agent runs a multi-perspective council because the operator requested it -2. The final fresh judge consumes that advisory evidence -3. The judge writes the one binary exact-plan verdict - -**Result:** Optional depth without creating a second readiness authority. - -## Auto-Find Recent Plan - -**User says:** `/premortem` - -**What happens:** - -1. Agent scans `.agents/plans/` for most recent plan -2. Finds `2026-02-13-add-caching-layer.md` -3. Runs one fresh-context quick review -4. Records only reusable prevention evidence - -**Result:** Frictionless validation of most recent planning work. - -## Deep Review for High-Stakes Plan - -**User says:** `/premortem --deep .agents/plans/2026-02-05-migration-plan.md` - -**What happens:** - -1. Agent reads the migration plan -2. Searches knowledge flywheel for prior migration learnings -3. Checks PRODUCT.md for product context -4. Runs `/council --deep --preset=plan-review validate <plan-path>` (4 judges) -5. Council verdict with multi-perspective consensus - -**Result:** Thorough multi-judge review for plans where the stakes justify spawning agents. - -## Troubleshooting - -| Problem | Cause | Solution | -|---------|-------|----------| -| Council times out | Plan too large or complex for judges to review in allocated time | Split plan into smaller epics or increase timeout via council config | -| FAIL verdict on valid plan | Judges misunderstand domain-specific constraints | Add context via `--perspectives-file` with domain explanations | -| Product perspectives missing | PRODUCT.md exists but not included in council packet | Verify PRODUCT.md is in project root and no explicit `--preset` override was passed | -| Premortem blocks /crank | The current plan has no bound fresh-context verdict | Run `/premortem --quick` on the exact plan; quick narrows depth, not independence | -| Spec-completeness evidence is missing | Plan lacks Boundaries or Conformance Checks sections | Add the missing sections before requesting a new verdict | -| Plan changed between waves | The old verdict no longer matches acceptance, dependencies, write scope, or risk | Run one fresh Premortem on the changed plan | diff --git a/skills/premortem/references/mandatory-checks.md b/skills/premortem/references/mandatory-checks.md deleted file mode 100644 index ef60508bf..000000000 --- a/skills/premortem/references/mandatory-checks.md +++ /dev/null @@ -1,179 +0,0 @@ -# Risk-Selected Plan Checks - -> Extracted from premortem/SKILL.md on 2026-04-11. -> -> The routine fresh judge runs only checks applicable to the named plan risks. -> A council is optional depth for a named one-way door, not the default carrier. - -## Step 2.3: Authority/consumer manifest (migration-shaped plans) - -When a plan renames, deletes, moves, migrates, or transfers ownership, require -the complete `write_scope.include` and `write_scope.exclude` manifest in the -PlanPacket, including generated companions. Compare it with the repository -state the plan will consume. - -- `incomplete` is FAIL: do not dispatch or infer missing consumers. -- `shared` is a valid inventory but the affected slices must serialize or merge. -- `disjoint` may retain a parallel proposal only when the remaining wave-validity - rows also pass. - -For a between-wave review, compare the manifest's observed paths with the -prior accepted snapshot. A new path invalidates the plan and returns to Plan; -it is not patched into a worker prompt after admission. - -## Step 2.4: Temporal Interrogation (`--deep` and `--temporal`) - -**Included automatically with `--deep`.** Also available via `--temporal` flag for quick reviews. - -Walk through the plan's implementation timeline to surface time-dependent risks: - -| Phase | Questions | -|-------|-----------| -| **Hour 1: Setup** | What blocks the first meaningful code change? Are dependencies available? | -| **Hour 2: Core** | Which files change in what order? Are there circular dependencies? | -| **Hour 4: Integration** | What fails when components connect? Which error paths are untested? | -| **Hour 6+: Ship** | What "should be quick" but historically isn't? What context is lost overnight? | - -Add to each judge's prompt when temporal interrogation is active: - -``` -TEMPORAL INTERROGATION: Walk through this plan's implementation timeline. -For each phase (Hour 1, 2, 4, 6+), identify: -1. What blocks progress at this point? -2. What fails silently at this point? -3. What compounds if not caught at this point? -Report temporal findings in a separate "Timeline Risks" section. -``` - -File count and dependency count never auto-trigger temporal or council depth. -Use it for `--deep`, explicit `--temporal`, or a named cutover/ordering risk. - -Temporal findings appear in the report as a `## Timeline Risks` table. See [temporal-interrogation.md](temporal-interrogation.md) for the full framework. - -Between waves, reuse the bound verdict while plan inputs are unchanged. A -materially changed plan may use the bounded mode from that reference; do not -replay completed work. - -## Step 2.5: Error & Rescue Map (Mandatory for plans with external calls) - -When the plan introduces methods, services, or codepaths that can fail, the council packet MUST include an Error & Rescue Map. If the plan omits one, generate it during review. - -Include in the council packet as `context.error_map`: - -| Method/Codepath | What Can Go Wrong | Exception/Error | Rescued? | Rescue Action | User Sees | -|-----------------|-------------------|-----------------|----------|---------------|-----------| -| `ServiceName#method` | API timeout | `TimeoutError` | Y/N | Retry 2x, then raise | "Service unavailable" | - -**Rules:** - -- Every external call (API, database, file I/O) must have at least one row -- `rescue StandardError` or bare `except:` is always a smell — name specific exceptions -- Every rescued error must: retry with backoff, degrade gracefully, OR re-raise with context -- For LLM/AI calls: map malformed response, empty response, hallucinated JSON, and refusal as separate failure modes -- Each GAP (unrescued error) is a finding with severity=significant - -## Step 2.6: Council FAIL Pattern Check (Mandatory) - -Evaluate the plan against these eight failure patterns: missing mechanical -verification, self-assessment, context rot, propagation blindness, plan -oscillation, dead infrastructure activation, missing rollback map, and -four-surface closure gap. Report only concrete, evidence-bound violations that -meet Premortem's blocker contract. - -Add to each judge's prompt: - -``` -COUNCIL FAIL PATTERN CHECK: Review this plan for the top 8 council FAIL patterns: -1. Missing mechanical verification — are all gates automated? -2. Self-assessment — is validation external to the implementer? -3. Context rot — are phase boundaries enforced with fresh sessions? -4. Propagation blindness — is the full change surface enumerated? -5. Plan oscillation — is direction validated before propagation? -6. Dead infrastructure activation — does the plan provision anything without activation tests? -7. Missing rollback map — does any production-state change lack a rollback procedure? -8. Four-surface closure — does the plan address Code + Docs + Examples + Proof for every feature? -Report FAIL pattern findings in a "FAIL Pattern Risks" section. -``` - -**Auto-triggered** for all plans (both `--quick` and `--deep` modes). - -## Step 2.7: Test Pyramid Coverage Check (Mandatory) - -Validate that the plan includes appropriate test levels per the test pyramid standard (`test-pyramid.md` in the standards skill). - -Check each issue in the plan: - -| Question | Expected | Finding if Missing | -|----------|----------|--------------------| -| Does any issue touching external APIs include L0 (contract) tests? | Yes | severity=significant: "Missing contract tests for API boundary" | -| Does every feature/bug issue include L1 (unit) tests? | Yes | severity=significant: "Missing unit tests for feature/bug issue" | -| Do cross-module changes include L2 (integration) tests? | Yes | severity=moderate: "Missing integration tests for cross-module change" | -| Are L4+ levels deferred to human gate (not agent-planned)? | Yes | severity=low: "Agent planning L4+ tests — these require human-defined scenarios" | -| For any skip/dedup/consumed/idempotency/regression guard test, does the fixture round-trip the **real persisted shape** (not a hand-built in-memory constructor)? | Yes | severity=significant: "Guard-test fixture uses a shape production never emits — false-green risk (cf. ag-mjlg / PR #652)" | -| Does any guard marker (`consumed`/`skip`/`dedup`) get set at the granularity the on-disk artifact uses (batch/parent/envelope vs item)? | Yes | severity=significant: "Guard marker set at item-level when persisted artifact marks it at batch-level" | - -Add to each judge's prompt when test pyramid check is active: - -``` -TEST PYRAMID CHECK: Review the plan's test coverage against the L0-L7 pyramid. -For each issue, verify: -1. Are the right test levels specified? (L0 for boundaries, L1 for behavior, L2 for integration) -2. Are there gaps where tests should exist but aren't planned? -3. Are any agent-autonomous levels (L0-L3) missing from code-change issues? -Report test pyramid findings in a "Test Coverage Gaps" section. -``` - -**Auto-triggered** when any issue in the plan modifies source code files (`.go`, `.py`, `.ts`, `.rs`, `.js`). - -## Step 2.8: Input Validation Check (Mandatory for enum-like fields) - -When the plan introduces or modifies fields with a bounded set of valid values (enums, tier names, mode strings, status codes), verify the plan includes validation logic. - -| Question | Expected | Finding if Missing | -|----------|----------|--------------------| -| Does every new enum-like field have a validation guard? | Yes | severity=significant: "No validation for enum field — invalid values pass silently" | -| Is there a defined fallback for unrecognized values? | Yes | severity=moderate: "No fallback behavior specified for invalid input" | -| Are valid values defined as a constant set (not inline strings)? | Yes | severity=low: "Valid values are inline strings — extract to named constant set" | - -**Auto-triggered** when the plan introduces struct fields with comments mentioning valid values, config fields with bounded options, or string fields parsed from user input. - -## Step 2.9: Regex Scope Predicate Check (Mandatory when plan introduces a regex/glob/grep filter) - -When the plan introduces a regex, glob, or grep pattern that classifies inputs into "in scope" / "out of scope" (goal gates that scan files, lint rules that classify code, orchestrators that filter work, search/inject filters), the plan MUST enumerate ≥3 positive cases and ≥3 negative cases. - -| Question | Expected | Finding if Missing | -|----------|----------|--------------------| -| Does the plan list ≥3 positive cases the predicate MUST match? | Yes | severity=significant: "Regex predicate has no positive case list — risk of too-narrow first iteration" | -| Does the plan list ≥3 negative cases the predicate MUST NOT match? | Yes | severity=significant: "Regex predicate has no negative case list — risk of too-broad first iteration (false positives)" | -| Does the implementation include a unit test covering both lists? | Yes | severity=moderate: "Regex predicate has cases listed in plan but no unit test — predicate semantics drift after first edit" | - -**Auto-triggered** when any plan issue mentions: a goal gate scanning `scripts/**`, `docs/**`, or any glob; a regex assigned to a variable; a `grep -E` invocation in a new gate or lint script; an orchestrator that filters which files to dispatch; a search filter that decides which records to surface. - -## Steps 2.10–2.11: Independent exact-plan adjudication - -### No self-grading - -The plan author cannot emit the readiness verdict. Record `author_id` and a -distinct, context-isolated `judge_id`; reject the artifact when they are equal. -One blind fresh-context judge satisfies the independence floor. Optional -`author_model` and `judge_model` metadata may record model names and families, -but family never changes whether the verdict is valid. - -### One immutable verdict - -Bind `premortem-plan-verdict.v1` to the repository-relative plan path and its -current SHA-256. Any edit invalidates it. Emit only: - -- `PASS` with `blockers_complete: true` and an empty blocker list; or -- `FAIL` with `blockers_complete: true` and every concrete blocker in one list. - -Each blocker must identify the failed claim and cite at least one evidence -path. Do not split one review into per-check verdicts, accept conditional -readiness, or turn notes into blockers. - -### Ownership boundary - -Premortem owns plan judgment only. It does not count attempts, manage repair -cycles, allocate time or model budgets, consult escalation helpers, implement -changes, close tracker work, or decide delivery. Return the immutable verdict -to the orchestrator, which chooses the next transition. diff --git a/skills/premortem/references/premortem.feature b/skills/premortem/references/premortem.feature index 18fd7afaf..a26d0ff61 100644 --- a/skills/premortem/references/premortem.feature +++ b/skills/premortem/references/premortem.feature @@ -1,48 +1,12 @@ -# Executable spec for the /premortem skill — pre-implementation plan gate (domain role). -# /premortem stress-tests a plan BEFORE work starts: it returns a PASS/FAIL verdict on -# the plan and on the wave-validity rows, so a bad plan is sent back to /plan rather than -# executed. Quick mode uses one fresh judge; --deep/--mixed widen the council. Hexagon: -# domain; consumes standards; produces premortem-plan-verdict.v1. (soc-qk4b) +Feature: Premortem optionally challenges one frozen plan + Scenario: A fresh judge returns advisory findings + Given a PlanPacket with a pinned digest and author context ID + When a distinct fresh judge challenges its acceptance, scope, and evidence + Then Premortem returns findings with checked and not-checked scope + And an empty finding set grants no lifecycle permission -Feature: Pre-mortem stress-tests a plan before implementation - As the pre-flight gate between slice-planning and TDD - I want a plan reviewed for failure modes before any code is written - So that a flawed plan is caught and re-sliced instead of executed - - Scenario: a plan is reviewed and gets a verdict before work starts - When /premortem runs on a plan or spec - Then it returns a PASS or FAIL verdict on the plan's failure modes - And the verdict binds the exact plan path and SHA-256 - And author_id differs from judge_id - - Scenario: wave-validity gates parallelism - When the plan proposes a parallel wave - Then premortem checks the wave-validity rows (distinct write scopes, owner per slice, discard path) - And a wave may run parallel only if every row is conflict-free - And a FAIL sends the plan back to /plan to re-slice (or run sequential) - - Scenario: Between-wave Premortem runs only for a materially changed plan - Given targeted wave evidence changed acceptance, dependencies, write scope, or risk - And the orchestrator wrote the changed plan - When Premortem runs before the next wave - Then it judges that exact changed plan - And Validate and Learn did not invoke Premortem directly - - Scenario: unchanged plan inputs reuse the bound verdict - Given the accepted plan digest, acceptance, dependencies, write scope, and risk are unchanged - When another tranche wave is considered - Then the existing Premortem verdict is reused - And no new judge, council, report, or registry write is created - - Scenario: quick mode uses one fresh judge by default - When /premortem runs without --deep/--mixed/--debate - Then it runs exactly one fresh-context judge distinct from the author - And it does not start a council - And optional council fan-out cannot substitute for the exact-plan verdict - - Scenario: a failure reports every blocker once - Given the plan has concrete readiness defects - When the fresh judge returns FAIL - Then blockers_complete is true - And the blocker list is nonempty and evidence-bound - And Premortem owns no retry, budget, helper, or delivery state + Scenario: Premortem stops after the review + Given any advisory finding set + When the review is complete + Then Premortem does not implement, validate, retry, schedule, claim, operate Git, release, or deliver + And the caller owns whether to revise the plan or invoke RPI diff --git a/skills/premortem/references/scope-mode.md b/skills/premortem/references/scope-mode.md deleted file mode 100644 index 9c06dc1b1..000000000 --- a/skills/premortem/references/scope-mode.md +++ /dev/null @@ -1,31 +0,0 @@ -# Scope Mode Selection - -> Extracted from premortem/SKILL.md on 2026-04-11. - -Before running council, determine the review posture. Three modes: - -| Mode | When to Use | Posture | -|------|-------------|---------| -| **SCOPE EXPANSION** | Greenfield features, user says "go big" | Dream big. What's the 10-star version? Push scope UP. | -| **HOLD SCOPE** | Bug fixes, refactors, most plans | Maximum rigor within accepted scope. Make it bulletproof. | -| **SCOPE REDUCTION** | Plan touches >15 files, overbuilt | Strip to essentials. What's the minimum that ships value? | - -## Auto-Detection (when user doesn't specify) - -- Greenfield feature → default EXPANSION -- Bug fix or hotfix → default HOLD SCOPE -- Refactor → default HOLD SCOPE -- Plan touching >15 files → suggest REDUCTION -- User says "go big" / "ambitious" → EXPANSION - -## Critical Rule - -Once mode is selected, commit to it in the judge input. Do not silently drift. -Scope mode focuses the review; it never changes the binary verdict schema or -authorizes conditional readiness. - -## Mode-Specific Judge Instructions - -- **EXPANSION:** Add to judge prompt: "What would make this 10x more ambitious for 2x the effort? What's the platonic ideal? List 3 delight opportunities." -- **HOLD SCOPE:** Add to judge prompt: "The plan's scope is accepted. Your job: find every failure mode, test every edge case, ensure observability. Do not argue for less work." -- **REDUCTION:** Add to judge prompt: "Find the minimum viable version. Everything else is deferred. What can be a follow-up? Separate must-ship from nice-to-ship." diff --git a/skills/premortem/references/temporal-interrogation.md b/skills/premortem/references/temporal-interrogation.md deleted file mode 100644 index 364468590..000000000 --- a/skills/premortem/references/temporal-interrogation.md +++ /dev/null @@ -1,96 +0,0 @@ -# Temporal Interrogation Framework - -Walk through the implementation timeline to surface time-dependent risks that static plan review misses. - -## Purpose - -Plans look good on paper but fail in time. Temporal interrogation forces judges to simulate the implementation sequence hour by hour, exposing ordering dependencies, blocking resources, and compounding failures. - -## Timeline Template - -### Hour 1: Setup & First File - -- What blocks the first meaningful code change? -- Are all dependencies available (APIs, credentials, packages)? -- Is the dev environment ready (DB migrations, seed data, config)? -- What happens if the first test fails? - -### Hour 2: Core Implementation - -- Which files must change in what order? -- Are there circular dependencies between changes? -- What's the longest uninterruptible sequence (can't save/test mid-way)? -- Where does the implementer need domain knowledge they might lack? - -### Hour 4: Integration & Edge Cases - -- What happens when components connect for the first time? -- Which error paths are untested until integration? -- Are there race conditions that only appear under load? -- What data shapes haven't been validated end-to-end? - -### Hour 6+: Polish & Ship - -- What's left that "should be quick" but historically isn't? -- Are docs, config updates, and migration scripts included? -- What manual verification is needed before merge? -- If the implementer is interrupted here and picks up tomorrow, what context is lost? - -## Judge Prompt Addition - -When temporal interrogation is enabled, add to each judge's prompt: - -``` -TEMPORAL INTERROGATION: Walk through this plan's implementation timeline. -For each phase (Hour 1, 2, 4, 6+), identify: -1. What blocks progress at this point? -2. What fails silently at this point? -3. What compounds if not caught at this point? -Report temporal findings in a separate "Timeline Risks" section. -``` - -## When to Use - -- **Always for `--deep` reviews** — temporal interrogation is included automatically -- **On request** via `--temporal` flag for quick reviews -- **For a named temporal risk** such as a migration cutover, expiring credential, - irreversible sequence, or coordination window. File and dependency counts do - not select depth by themselves. - -## Between-wave bounded mode - -Do not run temporal interrogation after every wave. Reuse the bound Premortem -while acceptance, dependencies, write scope, and risk remain unchanged. When a -wave materially changes one of those inputs, the orchestrator sends the changed -plan for one fresh Premortem and interrogates only: - -1. the next wave and its exact first failing proof; -2. write-scope or dependency changes caused by the completed wave; -3. new risks or invalidated assumptions in the wave evidence; and -4. whether the next leaf still has one owner and a safe discard path. - -Do not resimulate completed waves or rerun their deterministic/semantic proof. -Emit one bounded PASS/FAIL artifact for the exact changed plan. Premortem does -not own the repair count or next transition; the orchestrator reads the complete -blocker set and decides whether to repair or replan. - -## Report Integration - -Temporal findings appear in the premortem report as: - -```markdown -## Timeline Risks - -| Phase | Risk | Impact if Missed | Mitigation | -|-------|------|------------------|------------| -| Hour 1 | Missing API credentials | Blocks all progress | Add credential check to setup script | -| Hour 2 | Circular import between module A and B | Refactor needed mid-implementation | Extract shared types to common module first | -| Hour 4 | Race condition in parallel write path | Data corruption in production | Add mutex before integration testing | -| Hour 6+ | Migration script not tested on staging data | Rollback needed post-deploy | Run migration on staging clone first | -``` - -## Optional history correlation for deep mode - -In `--deep` mode only, a cited, directly relevant prior failure may inform the -review. Do not scan a broad history index on the routine path, and never -auto-escalate severity solely from recurrence counts. diff --git a/skills/premortem/references/write-premortem-output.md b/skills/premortem/references/write-premortem-output.md deleted file mode 100644 index 43b44432f..000000000 --- a/skills/premortem/references/write-premortem-output.md +++ /dev/null @@ -1,46 +0,0 @@ -# Writing the Premortem Output - -Write one canonical JSON artifact to -`.agents/council/YYYY-MM-DD-premortem-<topic>.json`. - -The artifact conforms to -[`../schemas/plan-verdict.schema.json`](../schemas/plan-verdict.schema.json) and -binds the repository-relative plan path, live SHA-256, distinct author and judge -identities, binary verdict, and complete blocker list. - -```json -{ - "schema_version": "premortem-plan-verdict.v1", - "plan": { - "path": ".agents/plans/2026-07-14-example.md", - "sha256": "<64 lowercase hex>" - }, - "author_id": "planner-context", - "judge_id": "fresh-judge-context", - "verdict": "FAIL", - "blockers_complete": true, - "blockers": [ - { - "id": "B1", - "claim": "The migration inventory omits an active consumer", - "evidence": ["path/to/manifest.json", "path/to/consumer"] - } - ] -} -``` - -Validate the artifact against both the schema and current plan bytes: - -```bash -skills/premortem/scripts/validate-output.sh \ - .agents/council/YYYY-MM-DD-premortem-<topic>.json \ - "$(git rev-parse --show-toplevel)" -``` - -Optional model metadata is descriptive only. Do not add readiness projections, -attempt history, repair budgets, helper state, implementation state, tracker -state, or delivery authority to this artifact. - -Reusable findings may be copied off the critical path only after the -orchestrator accepts them. The immutable plan verdict itself is never rewritten -into a learning registry entry. diff --git a/skills/premortem/schemas/plan-verdict.schema.json b/skills/premortem/schemas/plan-verdict.schema.json deleted file mode 100644 index 2cfc92daf..000000000 --- a/skills/premortem/schemas/plan-verdict.schema.json +++ /dev/null @@ -1,71 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://agentops.local/schemas/premortem-plan-verdict.v1.schema.json", - "title": "Premortem exact-plan verdict", - "type": "object", - "additionalProperties": false, - "required": [ - "schema_version", - "plan", - "author_id", - "judge_id", - "verdict", - "blockers_complete", - "blockers" - ], - "properties": { - "schema_version": {"const": "premortem-plan-verdict.v1"}, - "plan": { - "type": "object", - "additionalProperties": false, - "required": ["path", "sha256"], - "properties": { - "path": {"type": "string", "minLength": 1}, - "sha256": {"type": "string", "pattern": "^[0-9a-f]{64}$"} - } - }, - "author_id": {"type": "string", "minLength": 1}, - "judge_id": {"type": "string", "minLength": 1}, - "author_model": {"$ref": "#/$defs/model"}, - "judge_model": {"$ref": "#/$defs/model"}, - "verdict": {"enum": ["PASS", "FAIL"]}, - "blockers_complete": {"const": true}, - "blockers": { - "type": "array", - "items": {"$ref": "#/$defs/blocker"} - }, - "reviewed_at": {"type": "string", "minLength": 1} - }, - "$defs": { - "model": { - "type": "object", - "additionalProperties": false, - "required": ["name"], - "properties": { - "name": {"type": "string", "minLength": 1}, - "family": {"type": "string", "minLength": 1} - } - }, - "blocker": { - "type": "object", - "additionalProperties": false, - "required": ["id", "claim", "evidence"], - "properties": { - "id": {"type": "string", "minLength": 1}, - "claim": {"type": "string", "minLength": 1}, - "evidence": { - "type": "array", - "minItems": 1, - "items": {"type": "string", "minLength": 1} - } - } - } - }, - "allOf": [ - { - "if": {"properties": {"verdict": {"const": "PASS"}}}, - "then": {"properties": {"blockers": {"maxItems": 0}}}, - "else": {"properties": {"blockers": {"minItems": 1}}} - } - ] -} diff --git a/skills/premortem/schemas/premortem-plan-review.v1.schema.json b/skills/premortem/schemas/premortem-plan-review.v1.schema.json new file mode 100644 index 000000000..c598fad2d --- /dev/null +++ b/skills/premortem/schemas/premortem-plan-review.v1.schema.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://agentops.local/schemas/premortem-plan-review.v1.schema.json", + "title": "Premortem Plan Review", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", + "plan_packet_digest", + "author_context_id", + "judge_context_id", + "findings", + "checked", + "not_checked" + ], + "properties": { + "schema_version": {"const": "premortem-plan-review.v1"}, + "plan_packet_digest": {"type": "string", "pattern": "^[a-f0-9]{64}$"}, + "author_context_id": {"type": "string", "minLength": 1}, + "judge_context_id": {"type": "string", "minLength": 1}, + "findings": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["id", "statement", "evidence"], + "properties": { + "id": {"type": "string", "minLength": 1}, + "statement": {"type": "string", "minLength": 1}, + "evidence": { + "type": "array", + "minItems": 1, + "items": {"type": "string", "minLength": 1} + } + } + } + }, + "checked": {"type": "array", "items": {"type": "string"}}, + "not_checked": {"type": "array", "items": {"type": "string"}} + } +} diff --git a/skills/premortem/scripts/validate-output.sh b/skills/premortem/scripts/validate-output.sh index 9ef1bb2f5..6f748d24e 100755 --- a/skills/premortem/scripts/validate-output.sh +++ b/skills/premortem/scripts/validate-output.sh @@ -1,79 +1,51 @@ #!/usr/bin/env bash set -euo pipefail -if [[ $# -lt 1 || $# -gt 2 || ! -f "$1" ]]; then - echo "usage: $0 <plan-verdict.json> [repository-root]" >&2 +if [[ $# -ne 1 || ! -f "$1" ]]; then + echo "usage: $0 <premortem-plan-review.json>" >&2 exit 2 fi -VERDICT_PATH="$1" -REPO_ROOT="${2:-$(git -C "$(dirname "$VERDICT_PATH")" rev-parse --show-toplevel 2>/dev/null || pwd)}" -SKILL_DIR="$(cd "$(dirname "$0")/.." && pwd)" -SCHEMA_PATH="$SKILL_DIR/schemas/plan-verdict.schema.json" - -python3 - "$VERDICT_PATH" "$REPO_ROOT" "$SCHEMA_PATH" <<'PY' -from __future__ import annotations - -import hashlib +python3 - "$1" <<'PY' import json +import re import sys from pathlib import Path +path = Path(sys.argv[1]) try: - from jsonschema import Draft202012Validator -except ImportError as exc: - print(f"premortem plan verdict: jsonschema is required: {exc}", file=sys.stderr) - raise SystemExit(2) - -verdict_path = Path(sys.argv[1]).resolve() -root = Path(sys.argv[2]).resolve() -schema_path = Path(sys.argv[3]).resolve() - -try: - payload = json.loads(verdict_path.read_text(encoding="utf-8")) - schema = json.loads(schema_path.read_text(encoding="utf-8")) + value = json.loads(path.read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError) as exc: - print(f"premortem plan verdict: unreadable JSON: {exc}", file=sys.stderr) + print(f"premortem plan review: unreadable JSON: {exc}", file=sys.stderr) raise SystemExit(1) -errors = sorted( - Draft202012Validator(schema).iter_errors(payload), - key=lambda item: [str(part) for part in item.absolute_path], -) -if errors: - error = errors[0] - location = "/".join(str(part) for part in error.absolute_path) or "<root>" - print(f"premortem plan verdict: schema violation at {location}: {error.message}", file=sys.stderr) +required = { + "schema_version", "plan_packet_digest", "author_context_id", + "judge_context_id", "findings", "checked", "not_checked", +} +if set(value) != required: + print("premortem plan review: unexpected or missing fields", file=sys.stderr) raise SystemExit(1) - -if payload["author_id"] == payload["judge_id"]: - print("premortem plan verdict: author_id must differ from judge_id", file=sys.stderr) - raise SystemExit(1) - -relative = Path(payload["plan"]["path"]) -if relative.is_absolute(): - print("premortem plan verdict: plan path must be repository-relative", file=sys.stderr) - raise SystemExit(1) -plan_path = (root / relative).resolve() -try: - plan_path.relative_to(root) -except ValueError: - print("premortem plan verdict: plan path escapes repository root", file=sys.stderr) - raise SystemExit(1) -if not plan_path.is_file(): - print(f"premortem plan verdict: plan does not exist: {relative}", file=sys.stderr) - raise SystemExit(1) - -actual = hashlib.sha256(plan_path.read_bytes()).hexdigest() -if actual != payload["plan"]["sha256"]: - print( - f"premortem plan verdict: stale plan digest: expected {payload['plan']['sha256']}, got {actual}", - file=sys.stderr, - ) - raise SystemExit(1) - -print( - f"premortem plan verdict valid: {payload['verdict']} " - f"{payload['plan']['path']}@{payload['plan']['sha256']}" -) +if value["schema_version"] != "premortem-plan-review.v1": + raise SystemExit("premortem plan review: wrong schema_version") +if not re.fullmatch(r"[a-f0-9]{64}", value["plan_packet_digest"]): + raise SystemExit("premortem plan review: invalid plan digest") +author = value["author_context_id"] +judge = value["judge_context_id"] +if not isinstance(author, str) or not author or not isinstance(judge, str) or not judge or author == judge: + raise SystemExit("premortem plan review: author and judge identities must be nonempty and distinct") +for field in ("checked", "not_checked"): + if not isinstance(value[field], list) or not all(isinstance(item, str) for item in value[field]): + raise SystemExit(f"premortem plan review: {field} must be a string array") +if not isinstance(value["findings"], list): + raise SystemExit("premortem plan review: findings must be an array") +for finding in value["findings"]: + if not isinstance(finding, dict) or set(finding) != {"id", "statement", "evidence"}: + raise SystemExit("premortem plan review: malformed finding") + if not all(isinstance(finding[key], str) and finding[key] for key in ("id", "statement")): + raise SystemExit("premortem plan review: finding id and statement are required") + evidence = finding["evidence"] + if not isinstance(evidence, list) or not evidence or not all(isinstance(item, str) and item for item in evidence): + raise SystemExit("premortem plan review: each finding needs evidence") +print("premortem plan review: valid") PY diff --git a/skills/premortem/scripts/validate.sh b/skills/premortem/scripts/validate.sh index 5ea54d4d0..7dc76b541 100755 --- a/skills/premortem/scripts/validate.sh +++ b/skills/premortem/scripts/validate.sh @@ -1,37 +1,21 @@ #!/usr/bin/env bash set -euo pipefail -SKILL_DIR="$(cd "$(dirname "$0")/.." && pwd)" -REPO_ROOT="$(cd "$SKILL_DIR/../.." && pwd)" -SKILL="$SKILL_DIR/SKILL.md" -SCHEMA="$SKILL_DIR/schemas/plan-verdict.schema.json" -VALIDATOR="$SKILL_DIR/scripts/validate-output.sh" -PASS=0 -FAIL=0 +skill_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -check() { - if bash -c "$2"; then - echo "PASS: $1" - PASS=$((PASS + 1)) - else - echo "FAIL: $1" - FAIL=$((FAIL + 1)) - fi -} +grep -q '^name: premortem$' "$skill_dir/SKILL.md" +grep -Fq 'optional plan-challenge strategy' "$skill_dir/SKILL.md" +grep -Fq 'It is not part of the required RPI sequence' "$skill_dir/SKILL.md" +grep -Fq 'advisory findings' "$skill_dir/SKILL.md" +grep -q '^Feature: Premortem optionally challenges one frozen plan$' \ + "$skill_dir/references/premortem.feature" +test -f "$skill_dir/schemas/premortem-plan-review.v1.schema.json" +test -x "$skill_dir/scripts/validate-output.sh" -check "SKILL.md exists" "test -f '$SKILL'" -check "frontmatter name" "grep -q '^name: premortem' '$SKILL'" -check "exact-plan output contract" "grep -q '^output_contract: skills/premortem/schemas/plan-verdict.schema.json' '$SKILL' || grep -Fq '[plan-verdict.schema.json](schemas/plan-verdict.schema.json)' '$SKILL'" -check "fresh author-distinct judge" "grep -Fq 'author_id != judge_id' '$SKILL'" -check "binary complete verdict" "grep -Fq 'Emit exactly' '$SKILL' && grep -q 'complete nonempty blocker set' '$SKILL'" -check "family is optional metadata" "grep -q 'Model and family' '$SKILL' && grep -q 'metadata are optional' '$SKILL' && grep -q 'no risk class requires different model families' '$SKILL'" -check "no local controller ownership" "grep -q 'Do not own retries, attempt maps, budgets, helper state' '$SKILL'" -check "schema and validator exist" "test -f '$SCHEMA' && test -x '$VALIDATOR'" -check "schema is strict" "jq -e '.additionalProperties == false and (.properties.verdict.enum == [\"PASS\",\"FAIL\"]) and (.properties.blockers_complete.const == true)' '$SCHEMA' >/dev/null" -check "plan-pawl authority removed" "! rg -q 'plan-pawl|ApprovalEdge|Fable|WARN.*Ready|PASS/WARN/FAIL|cross-family rule for one-way doors' '$SKILL' '$SKILL_DIR/references/mandatory-checks.md' '$SKILL_DIR/references/premortem.feature' '$SKILL_DIR/references/write-premortem-output.md'" -check "kernel stays within 250 lines" "test \$(wc -l < '$SKILL') -le 250" -check "focused direct-cut acceptance exists" "test -f '$REPO_ROOT/tests/scripts/premortem-plan-verdict-direct-cut.bats'" +if grep -Eiq 'ao (pawl|land)|git (commit|push)|br (close|update)|auto-redo|next[_ -]action' \ + "$skill_dir/SKILL.md"; then + echo 'premortem contract contains forbidden lifecycle authority' >&2 + exit 1 +fi -echo -echo "Results: $PASS passed, $FAIL failed" -test "$FAIL" -eq 0 +echo 'premortem skill contract: PASS' diff --git a/skills/refactor/SKILL.md b/skills/refactor/SKILL.md index cd3480ea3..c16efa469 100644 --- a/skills/refactor/SKILL.md +++ b/skills/refactor/SKILL.md @@ -26,9 +26,7 @@ metadata: canonical_status: canonical disposition: keep_specialist tier: execution - dependencies: - - standards - - pattern-mining + dependencies: [] output_contract: code changes with regression evidence --- # Refactor — one structural experiment diff --git a/skills/research/SELF-TEST.md b/skills/research/SELF-TEST.md deleted file mode 100644 index b27accdd2..000000000 --- a/skills/research/SELF-TEST.md +++ /dev/null @@ -1,44 +0,0 @@ -# Research Skill Self-Test - -## Trigger Cases - -- User says: `/research "authentication system"` (or any `/research <topic>`). - - Expected: load `research`, create `.agents/research/`, search prior art first, then dispatch an explore agent. - -- User says: "investigate how the cache layer works and write up the findings." - - Expected: load `research` and produce a cited `.agents/research/YYYY-MM-DD-<slug>.md` artifact. - -- User says: `/research "payment processing flow" --auto`. - - Expected: load `research` and run the full workflow without the Gate-1 human approval step. - -## Non-Trigger Cases - -- User asks to implement or change code directly with no investigation request. - - Expected: do not load `research`; route to `/implement` or `/plan`. - -- User asks for session/handoff history ("what did we decide last session?"). - - Expected: use `/recover`, not `research` — `research` reads git commit history, not session history. - -## Behavior Checks - -These map to the four scenarios in [references/research.feature](references/research.feature): - -- Prior art is searched before fresh exploration: `ao lookup` plus the `.agents/` knowledge dirs run first, and applicable learnings are cited in the output (not just loaded passively). -- An explore agent is actually dispatched (not merely described) using the detected backend, and it uses iterative retrieval — score results, extract new terms from high-relevance hits, refine over up to 3 cycles. -- Findings are written to `.agents/research/YYYY-MM-DD-<slug>.md`, and every claim carries a `file:line` citation. -- Interactive runs request human approval (Gate 1) before reporting completion; `--auto` proceeds without the gate. - -## Validation Commands - -Run from the repo root: - -```bash -bash skills/heal-skill/scripts/heal.sh --strict skills/research -bash scripts/validate-skill-frontmatter.sh --strict -``` - -## Failure Cases - -- Explore agent only described, never dispatched: re-run and dispatch the agent (or perform the exploration inline if no spawn backend is available) — see the Key Rules in `SKILL.md`. -- Findings written without `file:line` citations: fail the artifact and re-cite every claim before reporting. -- Missing reference file linked from `SKILL.md`: fail heal validation and restore the file or remove the link. diff --git a/skills/research/SKILL.md b/skills/research/SKILL.md index 30542448a..209945c74 100644 --- a/skills/research/SKILL.md +++ b/skills/research/SKILL.md @@ -1,16 +1,14 @@ --- name: research -description: 'Explore and write findings. Triggers: "research", "explore and write findings.", "research skill".' +description: 'Answer a bounded question with current cited evidence. Triggers: "research", "investigate", "find evidence".' practices: -- wiki-knowledge-surface - pragmatic-programmer - ddd-bounded-context hexagonal_role: driving-adapter consumes: -- repo-context +- research-question produces: -- .agents/research/*.md -- result.json +- research-report context_rel: [] skill_api_version: 1 allowed-tools: Read, Grep, Glob, Bash, Write @@ -20,7 +18,7 @@ metadata: canonical_status: canonical disposition: keep_specialist tier: execution - dependencies: [cass, ms, reverse-engineer, codebase-recon, pattern-mining] + dependencies: [] context: window: fork intent: @@ -32,145 +30,38 @@ context: intel_scope: topic output_contract: skills/research/schemas/findings.json --- -# Research Skill +# Research -Answer a bounded question with current, cited evidence and a durable research -artifact. Execute the investigation; do not return a search diary or an -uncited opinion. +Answer one bounded question with current evidence. Research informs a caller; +it does not select work, approve a plan, mutate lifecycle state, or decide what +happens next. -## Critical Constraints +## Contract -- **Why: avoid aimless exploration.** State the question, decision it informs, - scope, non-goals, freshness needs, and evidence-for-done before searching. -- **Why: prevent rediscovery.** Search `ao lookup` and existing `.agents/` - knowledge first, then test retrieved claims against current authoritative sources. -- **Why: keep facts trustworthy.** Every load-bearing claim cites `file:line`, a - commit, or a direct external source; distinguish observation from inference. -- **Why: control context.** Search in bounded directories, follow discovered - symbols, and stop after three iterative-retrieval cycles unless new evidence - materially changes the answer. -- **Why: honor operator control.** Use one inline agent by default. Spawn an - Explore agent or parallel lanes only when the user or active workflow explicitly - authorizes multi-agent research and scopes non-overlapping work. -- **Why: avoid stale external claims.** Browse current primary sources for - changing APIs, standards, products, or upstream behavior and cite them directly. -- **Why: preserve uncertainty.** Record gaps, contradictions, failed searches, - and confidence; do not turn absence of evidence into evidence of absence. +1. State the question, decision it informs, scope, non-goals, and evidence + required for a useful answer. +2. Search the smallest relevant local sources. For changing external facts, + use current primary sources. +3. Verify structural or semantic-search leads against authoritative content. +4. Separate observation, inference, contradiction, and unknown. +5. Lead with the answer and cite every load-bearing claim. +6. Report unchecked scope and stop. -## Inputs and Modes +Use the current agent inline by default. Parallel readers or alternate runtimes +are optional execution choices only when the caller authorizes them. Prior +research, CASS, MS, codebase recon, and pattern mining are advisory sources, +not required phases. -`/research <question> [--auto] [--from-pr <url>] [quick|medium|very-thorough]` +## Output -- `--auto` skips the Gate-1 approval prompt after quality validation; it does - not authorize external mutations, extra runtimes, or unbounded delegation. -- `--from-pr` narrows source and history inspection to the PR's changed paths. -- Quick answers may stay in chat when no durable handoff is needed. Medium and - architecture/cross-cutting work writes `.agents/research/`. +For a quick question, return the cited answer directly. When the caller asks +for a durable artifact, write one report containing: -## Workflow +- question and scope; +- answer; +- evidence references; +- contradictions and unknowns; +- checked and unchecked areas. -1. **Frame the inquiry.** Write the primary question, subquestions, target - decision, repositories/systems in scope, non-goals, freshness horizon, and - completion test. Choose quick, medium, or very-thorough depth. -2. **Retrieve prior knowledge.** Run `ao lookup --query "<topic>" --limit 5` - when available and search `.agents/{research,learnings,knowledge,patterns, - retros,plans,brainstorm}/` by content. For each applicable hit, record how it - changes the inquiry and verify it against current source. -3. **Choose evidence lanes.** Use code-map and - [codebase-archaeology.md](references/codebase-archaeology.md) for repository - questions; [structural-graph-navigation.md](references/structural-graph-navigation.md) - for refreshed graphify structure; scoped git history for decision context; - [software-research.md](references/software-research.md) or primary web sources - for upstream facts. Structure maps locate relationships, not in-body logic. -4. **Run iterative retrieval.** Start broad inside the declared scope, score - evidence relevance 0-1, extract symbols/config keys from items scoring at - least 0.5, and use them in the next pass. Read authoritative files to verify - every structural or semantic-search lead. Stop after three cycles or saturation. -5. **Select backend deliberately.** Detect the available backend and record it. - When parallelism is authorized, give Explore agent lanes distinct questions - and read-only scopes, then merge their evidence. Otherwise research inline. - See the backend references for Codex, background-task, Claude-team, and inline - variants; runtime and host instructions decide which are legal. -6. **Validate quality.** Assess coverage, depth (0-4 per critical area), gaps, - contradictory evidence, and assumptions. Under `--auto`, any critical depth - below 2 produces WARN plus `.agents/research/quality-warning.md`; do not hide it. -7. **Synthesize.** Write `.agents/research/YYYY-MM-DD-<topic-slug>.md` using - [document-template.md](references/document-template.md). Lead with the answer, - then key files/sources, findings, evidence, unresolved questions, confidence, - recommendations, and the backend used. -8. **Persist reusable findings selectively.** Only reusable findings that should - alter future planning enter `.agents/findings/registry.jsonl`. Require - provenance, `dedup_key`, pattern, detection question, checklist item, - applicability, confidence, and lifecycle fields; merge by key using the - contract's temp-file-plus-rename atomic write rule. Then run - `bash hooks/finding-compiler.sh --quiet` when present. -9. **Gate and report.** Unless `--auto`, ask whether the evidence is sufficient - to proceed to `/plan`, needs revision, or should be abandoned. Report the - answer, artifact path, confidence/gaps, approval status, and next route. - -## Backend Policy - -| Condition | Backend | -|---|---| -| no explicit multi-agent authorization | inline current agent | -| authorized Codex parallel lanes | bounded Codex sub-agents | -| authorized runtime lacks sub-agents | documented background-task fallback | -| no legal spawn backend | inline current agent | - -Backend selection changes execution mechanics, never evidence standards. Read -[iterative-retrieval.md](references/iterative-retrieval.md) and only the backend -module selected for the run. - -## Output Specification - -- **Artifact directory:** `.agents/research/`; optional quality warning at - `.agents/research/quality-warning.md`; reusable findings use the findings registry. -- **Filename convention:** `YYYY-MM-DD-<topic-slug>.md`; stable slug, no - overwrite of unrelated research. -- **Serialization/schema format:** Markdown following the document template plus - `result.json` conforming to `skills/research/schemas/findings.json` when a - machine handoff is required. -- **Validator command:** run `bash skills/research/scripts/validate.sh`, verify - cited paths/lines or URLs, and confirm critical depth/gap reporting. -- **Downstream handoff:** consumed by `/plan`, `/product`, `/premortem`, or the - requesting decision maker; reusable findings feed compiled prevention context. - -## Quality Rubric - -- **Decision-focused:** directly answers the framed question and names implications. -- **Authoritative:** current primary sources and source code outrank summaries. -- **Traceable:** every material claim has reproducible evidence and provenance. -- **Scoped:** search breadth matches the question without context flooding. -- **Honest:** inferences, contradictions, gaps, freshness, and confidence are explicit. -- **Durable:** a fresh reader can act from the artifact without chat context. - -## Examples - -**User says:** `/research "authentication request flow"` - -Trace one entry point through current code, use scoped history for rationale, -cite every transition, and write a medium-depth artifact. - -**User says:** `/research --from-pr <url> "does this change preserve retries?"` - -Restrict evidence to changed paths and their callers/tests, verify upstream -context, and state remaining uncertainty before recommending action. - -## Troubleshooting - -| Problem | Response | -|---|---| -| Topic is too broad | Split it into decision-sized questions | -| Prior research conflicts with source | Prefer current source and record the drift | -| Graph result lacks logic | Open the defining/calling files and verify behavior | -| Critical depth is below 2 | WARN, record the gap, and do not imply completeness | -| No spawn backend is authorized | Research inline; do not treat that as degraded evidence | - -## References - -- [research.feature](references/research.feature) · [document-template.md](references/document-template.md) · [iterative-retrieval.md](references/iterative-retrieval.md) -- [context-discovery.md](references/context-discovery.md) · [source-discovery-and-pattern-extraction.md](references/source-discovery-and-pattern-extraction.md) · [failure-patterns.md](references/failure-patterns.md) -- [codebase-archaeology.md](references/codebase-archaeology.md) · [data-flow-from-entry-points.md](references/data-flow-from-entry-points.md) · [onboarding-methodology.md](references/onboarding-methodology.md) -- [structural-graph-navigation.md](references/structural-graph-navigation.md) · [software-research.md](references/software-research.md) · [deep-research-mcp.md](references/deep-research-mcp.md) -- [backend-codex-subagents.md](references/backend-codex-subagents.md) · [backend-background-tasks.md](references/backend-background-tasks.md) · [backend-claude-teams.md](references/backend-claude-teams.md) · [backend-inline.md](references/backend-inline.md) -- [ralph-loop-contract.md](references/ralph-loop-contract.md) · [vibe-methodology.md](references/vibe-methodology.md) · [claude-code-latest-features.md](references/claude-code-latest-features.md) +Do not emit approval, confidence gates, retry instructions, owner, next action, +or delivery state. diff --git a/skills/research/references/backend-background-tasks.md b/skills/research/references/backend-background-tasks.md deleted file mode 100644 index e0f445417..000000000 --- a/skills/research/references/backend-background-tasks.md +++ /dev/null @@ -1,125 +0,0 @@ -# Backend: Background Tasks (Fallback) - -Concrete tool calls for spawning agents using `Task(run_in_background=true)`. This is the **last-resort fallback** when neither Codex sub-agents nor Claude native teams are available. - -**When detected:** `Task` tool is available but `TeamCreate` and `spawn_agent` are not. - -**Limitations:** -- Fire-and-forget — no messaging, no redirect, no scope adjustment -- No inter-agent communication -- No debate mode (R2 requires messaging) -- No retry (must re-spawn from scratch) -- No graceful shutdown (only `TaskStop`, which is lossy) - ---- - -## Spawn: Background Agents - -Spawn agents with `Task(run_in_background=true)`. Each call returns a `task_id` for later polling. - -### Council Judges - -``` -Task( - subagent_type="general-purpose", - run_in_background=true, - prompt="You are judge-1.\n\nYour perspective: Correctness & Completeness\n\n<PACKET>\n...\n</PACKET>\n\nWrite your verdict to .agents/council/2026-02-17-auth-judge-1.md\nThis is your ONLY output channel — there is no messaging.", - description="Council judge-1" -) -# Returns: task_id="abc-123" - -Task( - subagent_type="general-purpose", - run_in_background=true, - prompt="You are judge-error-paths.\n\nYour perspective: Error Paths & Edge Cases\n\n<PACKET>...</PACKET>\n\nWrite your verdict to .agents/council/2026-02-17-auth-judge-error-paths.md", - description="Council judge-error-paths" -) -# Returns: task_id="def-456" -``` - -Both `Task` calls go in the **same message** — they run in parallel. - -### Swarm Workers - -``` -Task( - subagent_type="general-purpose", - run_in_background=true, - prompt="You are worker-3.\n\nYour Assignment: Task #3: Add password hashing\n...\n\nWrite result to .agents/swarm/results/3.json\nDo NOT run git add/commit/push.", - description="Swarm worker-3" -) -``` - -### Research Explorers - -``` -Task( - subagent_type="Explore", - run_in_background=true, - prompt="Thoroughly investigate: authentication patterns...\n\nWrite findings to .agents/research/2026-02-17-auth.md", - description="Research explorer" -) -``` - ---- - -## Wait: Poll for Completion - -Background tasks have no messaging. Poll with `TaskOutput`. - -``` -TaskOutput(task_id="abc-123", block=true, timeout=120000) -TaskOutput(task_id="def-456", block=true, timeout=120000) -``` - -**Or non-blocking check:** - -``` -TaskOutput(task_id="abc-123", block=false, timeout=5000) -``` - -**After `TaskOutput` returns**, verify the agent wrote its result file: - -``` -Read(".agents/council/2026-02-17-auth-judge-1.md") -``` - -**Timeout behavior:** If `timeout` expires, `TaskOutput` returns with a timeout status — the agent may still be running. **Recovery:** -1. Check result file — agent may have written it but not finished cleanly -2. If result file exists → use it, `TaskStop` the agent -3. If no result file → agent failed silently. For council: proceed with N-1 verdicts, note in report. For swarm: add task back to retry queue, re-spawn a fresh agent. -4. Never assume `TaskOutput` completion means the result file was written — always verify - -**Fallback:** If background tasks fail despite detection, fall back to inline mode. See `backend-inline.md`. - ---- - -## No Messaging - -Background tasks cannot receive messages. This means: - -- **No debate R2** — judges get one round only -- **No retry** — if validation fails, re-spawn a new agent from scratch -- **No scope adjustment** — the prompt is final at spawn time - ---- - -## Cleanup - -Background tasks self-terminate when done. For stuck tasks: - -``` -TaskStop(task_id="abc-123") -``` - -This is lossy — partial work may be lost. - ---- - -## Key Rules - -1. **Filesystem is the only communication channel** — agents write files, lead reads files -2. **No messaging = no debate** — `--debate` is unavailable with this backend -3. **No retry = must re-spawn** — failed agents get a fresh `Task` call, not a message -4. **Always check result files** — `TaskOutput` completion doesn't guarantee the agent wrote its file -5. **Prefer native teams** — this backend is strictly inferior; use it only as last resort diff --git a/skills/research/references/backend-claude-teams.md b/skills/research/references/backend-claude-teams.md deleted file mode 100644 index 545d3268b..000000000 --- a/skills/research/references/backend-claude-teams.md +++ /dev/null @@ -1,240 +0,0 @@ -# Backend: Claude Native Teams - -Concrete tool calls for spawning agents using Claude Code native teams (`TeamCreate` + `SendMessage` + shared `TaskList`). - -**When detected:** `TeamCreate` tool is available in your tool list. - ---- - -## Pre-Flight: Confirm Modern Claude Features - -Before spawning teammates, verify feature readiness: - -1. `claude agents` succeeds (custom agents discoverable) -2. Teammate profiles for write tasks declare `isolation: worktree` -3. Long-running teammates prefer `background: true` -4. Hooks include worktree lifecycle coverage (`WorktreeCreate`, `WorktreeRemove`) and config auditing (`ConfigChange`) where policy requires it - -For canonical feature details, read: -`skills/shared/references/claude-code-latest-features.md`. - ---- - -## Setup: Create Team - -Every spawn session starts by creating a team. One team per wave (fresh context = Ralph Wiggum preserved; see `skills/shared/references/ralph-loop-contract.md`). - -``` -TeamCreate(team_name="council-20260217-auth", description="Council validation of auth module") -``` - -``` -TeamCreate(team_name="swarm-1739812345-w1", description="Wave 1: parallel implementation") -``` - -**Naming conventions:** -- Council: `council-YYYYMMDD-<target>` -- Swarm: `swarm-<epoch>-w<wave>` -- Crank: delegates to swarm naming - -## Leader Contract (Native Teams) - -Claude teams are leader-first orchestration: - -1. One lead creates the team and assigns all work. -2. Teammates never self-assign from shared tasks. -3. Teammates report to lead via short `SendMessage` signals. -4. Lead reads result artifacts from disk, validates, and decides retries/escalation. - -Recommended signal envelope (single-line JSON, under 100 tokens): - -```json -{"type":"completion|blocked|help_request","agent":"worker-3","task":"3","detail":"short status","artifact":".agents/swarm/results/3.json"} -``` - -`completion`: task finished, artifact written. -`blocked`: cannot proceed safely. -`help_request`: teammate needs coordination or scope clarification. - -### Peer Messaging (Allowed, Lead-Controlled) - -Native teams support direct teammate-to-teammate messaging. Use this only for coordination handoffs; keep messages thin and always copy the lead in follow-up summaries. - -```text -worker-2 -> worker-5: "Need auth schema constant name; please confirm from src/auth/schema.ts" -worker-5 -> lead: "Resolved peer question for worker-2; no scope change." -``` - ---- - -## Spawn: Create Workers/Judges - -After `TeamCreate`, spawn each agent with `Task(team_name=..., name=...)`. All agents in a wave spawn in parallel (single message, multiple tool calls). - -### Council Judges (parallel spawn) - -``` -Task( - subagent_type="general-purpose", - team_name="council-20260217-auth", - name="judge-1", - prompt="You are judge-1 on team council-20260217-auth.\n\nYour perspective: Correctness & Completeness\n\n<PACKET>\n...\n</PACKET>\n\nWrite your verdict to .agents/council/2026-02-17-auth-judge-1.md\nThen send a SHORT completion signal to the team lead (under 100 tokens).\nDo NOT include your full analysis in the message — the lead reads your file.", - description="Council judge-1" -) - -Task( - subagent_type="general-purpose", - team_name="council-20260217-auth", - name="judge-error-paths", - prompt="You are judge-error-paths on team council-20260217-auth.\n\nYour perspective: Error Paths & Edge Cases\n\n<PACKET>\n...\n</PACKET>\n\nWrite your verdict to .agents/council/2026-02-17-auth-judge-error-paths.md\nThen send a SHORT completion signal to the team lead (under 100 tokens).", - description="Council judge-error-paths" -) -``` - -Both `Task` calls go in the **same message** — they spawn in parallel. - -### Swarm Workers (parallel spawn) - -``` -Task( - subagent_type="general-purpose", - team_name="swarm-1739812345-w1", - name="worker-3", - prompt="You are worker-3 on team swarm-1739812345-w1.\n\nYour Assignment: Task #3: Add password hashing\n<description>...</description>\n\nInstructions:\n1. Execute your task — create/edit files as needed\n2. Write result to .agents/swarm/results/3.json\n3. Send a SHORT signal to team lead (under 100 tokens)\n4. Do NOT run git add/commit/push — the lead commits\n\nRESULT FORMAT:\n{\"type\":\"completion\",\"issue_id\":\"3\",\"status\":\"done\",\"detail\":\"one-line summary\",\"artifacts\":[\"path/to/file\"]}", - description="Swarm worker-3" -) - -Task( - subagent_type="general-purpose", - team_name="swarm-1739812345-w1", - name="worker-5", - prompt="You are worker-5 on team swarm-1739812345-w1.\n\nYour Assignment: Task #5: Create login endpoint\n...", - description="Swarm worker-5" -) -``` - -### Research Explorers (read-only) - -``` -Task( - subagent_type="Explore", - team_name="research-20260217-auth", - name="explorer-1", - prompt="Thoroughly investigate: authentication patterns in this codebase\n\n...", - description="Research explorer" -) -``` - -Use `subagent_type="Explore"` for read-only research agents. Use `"general-purpose"` for agents that need to write files. - ---- - -## Wait: Receive Completion Signals - -Workers/judges send completion signals via `SendMessage`. These are **automatically delivered** to the team lead — no polling needed. - -When a teammate finishes, their message appears as a new conversation turn. The lead reads result files from disk, NOT from message content. - -``` -# Teammate message arrives automatically: -# "judge-1: Done. Verdict: WARN, confidence: HIGH. File: .agents/council/2026-02-17-auth-judge-1.md" - -# Lead reads the file for full details: -Read(".agents/council/2026-02-17-auth-judge-1.md") -``` - -**Timeout handling (default: 120s per round, 90s for debate R2):** - -If a teammate goes idle without sending a completion signal: -1. Check their result file — they may have written it but failed to message -2. If result file exists → read it and proceed (the message was the only thing missing) -3. If no result file → the agent failed silently. **Recovery:** proceed with N-1 judges/workers and note the failure in the report. For swarm workers, add the task back to the retry queue. -4. Never wait indefinitely — after the timeout, move on - -See `skills/council/references/cli-spawning.md` for timeout configuration (`COUNCIL_TIMEOUT`, `COUNCIL_R2_TIMEOUT`). - -**Fallback:** If native teams fail at runtime despite passing detection (e.g., `TeamCreate` succeeds but `Task` spawning fails), fall back to background tasks. See `backend-background-tasks.md`. - ---- - -## Message: Debate R2 / Retry - -Send messages to specific teammates using `SendMessage`. Teammates wake from idle when messaged. - -### Council Debate R2 - -``` -SendMessage( - type="message", - recipient="judge-1", - content="DEBATE ROUND 2\n\nOther judges' verdicts:\n- judge-error-paths: FAIL (HIGH confidence) — file: .agents/council/2026-02-17-auth-judge-error-paths.md\n\nRead the other judge's file. Revise your assessment considering their perspective.\nWrite your R2 verdict to .agents/council/2026-02-17-auth-judge-1-r2.md\nThen send a completion signal.", - summary="R2 debate instructions for judge-1" -) -``` - -**R2 timeout (default: 90s):** If a judge doesn't respond to R2 within `COUNCIL_R2_TIMEOUT`, use their R1 verdict for consolidation. See `skills/council/references/debate-protocol.md` for full timeout handling. - -### Swarm Worker Retry - -``` -SendMessage( - type="message", - recipient="worker-3", - content="Validation failed: pytest tests/test_auth.py returned exit code 1.\nFix the failing tests and rewrite your result to .agents/swarm/results/3.json", - summary="Retry worker-3: test failure" -) -``` - ---- - -## Cleanup: Shutdown and Delete - -After consolidation/validate, shut down all teammates then delete the team. - -``` -# Shutdown each teammate -SendMessage(type="shutdown_request", recipient="judge-1", content="Council complete") -SendMessage(type="shutdown_request", recipient="judge-error-paths", content="Council complete") - -# After all teammates acknowledge shutdown: -TeamDelete() -``` - -**Reaper pattern:** If a teammate doesn't respond to shutdown within 30s, proceed with `TeamDelete()` anyway. - -**If `TeamDelete` fails** (e.g., stale members): clean up manually with `rm -rf ~/.claude/teams/<team-name>/` then retry `TeamDelete()` to clear in-memory state. - ---- - -## Multi-Wave Pattern - -For crank/swarm with multiple waves, create a **new team per wave**: - -``` -# Wave 1 -TeamCreate(team_name="swarm-1739812345-w1", description="Wave 1") -# ... spawn workers, wait, validate, commit ... -# ... shutdown teammates ... -TeamDelete() -# If TeamDelete fails: rm -rf ~/.claude/teams/swarm-1739812345-w1/ then retry - -# Wave 2 (fresh context) -TeamCreate(team_name="swarm-1739812345-w2", description="Wave 2") -# ... spawn workers for newly-unblocked tasks ... -TeamDelete() -``` - -This ensures each wave's workers start with clean context (no leftover state from prior waves). - -**If `TeamDelete` fails between waves**, the next `TeamCreate` may conflict. Always verify cleanup succeeded before creating the next wave team. - ---- - -## Key Rules - -1. **`TeamCreate` before `Task`** — tasks created before the team are invisible to teammates — **Enforcement: `safety.ValidateTeamLifecycle()` (T9)** -2. **Pre-assign tasks before spawning** — workers do NOT race-claim from TaskList — **Enforcement: documentation only** -3. **Lead-only commits** — workers write files, lead runs `git add` + `git commit` — **Enforcement: `hooks/git-worker-guard.sh` (T4)** -4. **Thin messages** — workers send <100 token signals, full results go to disk — **Enforcement: `safety.ValidateMessageSize()` (T9)** -5. **New team per wave** — fresh context, Ralph Wiggum preserved — **Enforcement: `safety.ValidateTeamLifecycle()` (T9)** -6. **Always cleanup** — `TeamDelete()` after every wave, even on partial failure — **Enforcement: `hooks/stop-team-guard.sh` + `safety.ValidateTeamLifecycle()` (T9)** diff --git a/skills/research/references/backend-codex-subagents.md b/skills/research/references/backend-codex-subagents.md deleted file mode 100644 index 46141ef51..000000000 --- a/skills/research/references/backend-codex-subagents.md +++ /dev/null @@ -1,120 +0,0 @@ -# Backend: Codex Sub-Agents - -Concrete tool calls for spawning agents using Codex CLI (`codex exec`). Used for `--mixed` mode cross-vendor consensus and as the primary backend when running inside a Codex session with `spawn_agent`. - ---- - -## Variant A: Codex CLI (from any runtime) - -Used when `codex` CLI is available on PATH. Agents run as background shell processes. - -**When detected:** `which codex` succeeds. - -### Spawn: Background Shell Processes - -```bash -# With structured output (preferred for council judges) -Bash( - command='codex exec -s read-only -m gpt-5.3-codex -C "$(pwd)" --output-schema skills/council/schemas/verdict.json -o .agents/council/codex-1.json "JUDGE PROMPT HERE"', - run_in_background=true -) - -# Without structured output (fallback) -Bash( - command='codex exec --full-auto -m gpt-5.3-codex -C "$(pwd)" -o .agents/council/codex-1.md "JUDGE PROMPT HERE"', - run_in_background=true -) -``` - -**Flag order:** `-s`/`--full-auto` → `-m` → `-C` → `--output-schema` → `-o` → prompt - -**Valid flags:** `--full-auto`, `-s`, `-m`, `-C`, `--output-schema`, `-o`, `--add-dir` -**Invalid flags:** `-q` (doesn't exist), `--quiet` (doesn't exist), `-p` as a prompt flag (in Codex CLI it means profile) - -### Wait: Poll Background Shell - -``` -TaskOutput(task_id="<shell-id>", block=true, timeout=120000) -``` - -Then read the output file: - -``` -Read(".agents/council/codex-1.json") -``` - -### Limitations - -- No messaging — Codex CLI processes are fire-and-forget -- No debate R2 with Codex judges — they produce one verdict only -- `--output-schema` requires `additionalProperties: false` at all levels -- `--output-schema` requires ALL properties in `required` array -- `-s read-only` + `-o` works — `-o` is CLI-level post-processing, not sandbox I/O - ---- - -## Variant B: Codex Sub-Agents (inside Codex runtime) - -Used when running inside a Codex session where `spawn_agent` is available. - -**When detected:** `spawn_agent` tool is in your tool list. - -### Spawn - -``` -spawn_agent(message="You are judge-1.\n\nPerspective: Correctness & Completeness\n\n<PACKET>...</PACKET>\n\nWrite verdict to .agents/council/2026-02-17-auth-judge-1.md") -# Returns: agent_id - -spawn_agent(message="You are worker-3.\n\nTask: Add password hashing\n...\n\nWrite result to .agents/swarm/results/3.json") -# Returns: agent_id -``` - -### Wait - -``` -wait(ids=["agent-id-1", "agent-id-2"]) -``` - -**Timeout:** `wait()` blocks until completion. Set a timeout at the orchestration level (default: `COUNCIL_TIMEOUT=120s`). If an agent doesn't complete within the timeout, `close_agent` it and proceed with N-1 verdicts/workers. - -### Message (retry/follow-up) - -``` -send_input(id="agent-id-1", message="Validation failed: fix tests and retry") -``` - -### Cleanup - -``` -close_agent(id="agent-id-1") -``` - ---- - -## Mixed Mode (Council) - -For `--mixed` council, spawn runtime-native judges AND Codex CLI judges in parallel: - -``` -# Claude native team judges (via TeamCreate — see backend-claude-teams.md) -Task(subagent_type="general-purpose", team_name="council-20260217-auth", name="judge-1", prompt="...", description="Judge 1") -Task(subagent_type="general-purpose", team_name="council-20260217-auth", name="judge-2", prompt="...", description="Judge 2") - -# Codex CLI judges (parallel background shells) -Bash(command='codex exec -s read-only -m gpt-5.3-codex -C "$(pwd)" --output-schema skills/council/schemas/verdict.json -o .agents/council/codex-1.json "PACKET"', run_in_background=true) -Bash(command='codex exec -s read-only -m gpt-5.3-codex -C "$(pwd)" --output-schema skills/council/schemas/verdict.json -o .agents/council/codex-2.json "PACKET"', run_in_background=true) -``` - -All four spawn in the **same message** — maximum parallelism. - -**Mixed mode quorum:** At least 1 judge from each vendor should respond for cross-vendor consensus. If all judges from one vendor fail, proceed as single-vendor council and note the degradation in the report. - ---- - -## Key Rules - -1. **Pre-flight check:** `which codex` before attempting Codex CLI spawning -2. **Model availability:** `gpt-5.3-codex` requires API account — fall back to `gpt-4o` if unavailable -3. **Flag order matters** — agents copy examples exactly -4. **`codex review` is a different command** with different flags — do not conflate with `codex exec` -5. **No debate with Codex judges** — they produce one verdict, Codex CLI has no messaging diff --git a/skills/research/references/backend-inline.md b/skills/research/references/backend-inline.md deleted file mode 100644 index 0c91a1cf1..000000000 --- a/skills/research/references/backend-inline.md +++ /dev/null @@ -1,69 +0,0 @@ -# Backend: Inline (No Spawn Available) - -Degraded single-agent mode when no multi-agent primitives are detected. The current agent performs all work sequentially in its own context. - -**When detected:** No `spawn_agent`, no `TeamCreate`, no `Task` tool available — or `--quick` flag was explicitly set. - ---- - -## Council: Single Inline Judge - -Instead of spawning parallel judges, the lead evaluates from each perspective sequentially: - -``` -1. Build the context packet (same as multi-agent mode) -2. For each perspective: - a. Adopt the perspective mentally - b. Write findings to .agents/council/YYYY-MM-DD-<target>-<perspective>.md -3. Synthesize into final report -``` - -Output format is identical — same file paths, same verdict schema. Downstream consumers (consolidation, report) don't know it was inline. - -**No debate available** — debate requires messaging between agents. - ---- - -## Swarm: Sequential Execution - -Instead of parallel workers, execute each task sequentially: - -``` -1. TaskList() — find unblocked tasks -2. For each unblocked task (in order): - a. Execute the task directly - b. Write result to .agents/swarm/results/<task-id>.json - c. TaskUpdate(taskId="<id>", status="completed") -3. Check for newly-unblocked tasks -4. Repeat until all tasks complete -``` - -Same result files, same validation — just sequential. - -**Error handling:** If a task fails mid-execution: -1. Write failure result to `.agents/swarm/results/<task-id>.json` with `"status": "blocked"` -2. Check if downstream tasks depend on it (`blockedBy`) -3. Skip blocked downstream tasks, mark as skipped -4. Continue with independent tasks that don't depend on the failed one - ---- - -## Research: Inline Exploration - -Instead of spawning an Explore agent, perform the tiered search directly: - -``` -1. Read docs/code-map/ if present -2. Grep/Glob for relevant files -3. Read key files -4. Write findings to .agents/research/YYYY-MM-DD-<topic>.md -``` - ---- - -## Key Rules - -1. **Same output format** — inline mode writes the same files as multi-agent mode -2. **Same validation** — all checks still apply -3. **Slower but functional** — no parallelism, but all skill capabilities preserved (except debate) -4. **Inform the user** — log "Running in inline mode (no multi-agent backend detected)" diff --git a/skills/research/references/claude-code-latest-features.md b/skills/research/references/claude-code-latest-features.md deleted file mode 100644 index a4196c4f5..000000000 --- a/skills/research/references/claude-code-latest-features.md +++ /dev/null @@ -1,126 +0,0 @@ -# Claude Code Latest Features Contract - -This document is the shared source of truth for Claude Code feature usage across AgentOps skills. - -## Baseline - -- Target Claude Code release family: `2.1.x` -- Last verified against upstream changelog: `2.1.75` -- Changelog source: `https://raw.githubusercontent.com/anthropics/claude-code/main/CHANGELOG.md` - -## Current Feature Set We Rely On - -### 1. Core Slash Commands - -Skills and docs should assume these commands exist and prefer them over legacy naming: - -- `/agents` -- `/hooks` -- `/permissions` -- `/memory` -- `/mcp` -- `/output-style` -- `/effort` — set model effort level (low/medium/high). Opus 4.6 defaults to medium. -- `/color` — set prompt-bar color per session (useful for distinguishing parallel sessions) - -Reference: `https://code.claude.com/docs/en/slash-commands` - -### 2. Agent Definitions - -For custom teammates in `.claude/agents/*.md`, use modern frontmatter fields where applicable: - -- `model` -- `description` -- `tools` -- `memory` (scope control) -- `background: true` for long-running teammates -- `isolation: worktree` for safe parallel write isolation - -Reference: `https://code.claude.com/docs/en/sub-agents` - -### 3. Worktree Isolation - -When parallel workers may touch overlapping files, prefer Claude-native isolation features first: - -- Session-level isolation: `claude --worktree` (`-w`) -- Agent-level isolation: `isolation: worktree` -- Sparse checkout: `worktree.sparsePaths` setting — limit worktree to relevant directories in large monorepos - -If unavailable in a given runtime, fall back to manual `git worktree` orchestration. - -Reference: changelog `2.1.49`, `2.1.50`, and `2.1.75`. - -### 4. Hooks and Governance Events - -Hooks-based workflows should include modern event coverage: - -- `WorktreeCreate` -- `WorktreeRemove` -- `ConfigChange` -- `SubagentStop` -- `TaskCompleted` -- `TeammateIdle` -- `PostCompact` — fires after session context compaction. Use for auto-recovery (e.g., re-inject context). -- `InstructionsLoaded` — fires when CLAUDE.md loads. Use for policy enforcement. - -**HTTP hooks:** Hooks can POST JSON to a URL and receive JSON responses, in addition to shell script execution. - -Use these for auditability, policy enforcement, and cleanup. - -Reference: `https://code.claude.com/docs/en/hooks` - -### 5. Settings Hierarchy - -Skill guidance must respect settings precedence: - -1. Enterprise managed policy -2. Command-line args -3. Local project settings -4. Shared project settings -5. User settings - -Reference: `https://code.claude.com/docs/en/settings` - -### 6. Agent Inventory Command - -Use `claude agents` as the first CLI-level check to confirm configured teammate profiles before multi-agent runs. - -Reference: changelog `2.1.50`. - -### 7. Session Management - -- `--from-pr <url>` — start or resume a session linked to a specific GitHub PR -- `--worktree` (`-w`) — start session in an isolated git worktree - -Reference: `https://code.claude.com/docs/en/cli-reference` - -### 8. Tool Enhancements - -- **Read tool:** `pages` parameter for PDFs — read specific page ranges (e.g., `pages: "1-5"`). Large PDFs (>10 pages) require this parameter. -- **Bash tool:** Wildcard permission patterns — `Bash(npm *)` or `Bash(* install)` for flexible auto-approval. - -### 9. Effort Levels - -The `/effort` command controls model reasoning depth: - -- `low` — fast, shallow reasoning. Good for research/exploration agents. -- `medium` — balanced (Opus 4.6 default). -- `high` — deep reasoning. Good for implementation and complex debugging. - -Skill recommendation: set effort per agent role — low for judges/explorers, high for implementors. - -## Skill Authoring Rules - -1. Do not reference deprecated permission command names (`/allowed-tools`, `/approved-tools`). -2. Multi-agent skills (`council`, `swarm`, `research`, `crank`, `codex-team`) must explicitly point to this contract. -3. Prefer declarative agent isolation (`isolation: worktree`) over ad hoc branch/worktree shell choreography where runtime supports it. -4. Keep manual `git worktree` fallback documented for non-Claude runtimes. -5. For long-running explorers/judges/workers, document `background: true` as the default custom-agent policy. -6. Use `/effort` to right-size model reasoning per agent role when spawning multi-agent workflows. - -## Review Cadence - -- Re-verify this contract when: - - Claude Code changelog introduces new `2.1.x` or `2.2.x` entries - - any skill adds or changes multi-agent orchestration - - hook event support changes diff --git a/skills/research/references/codebase-archaeology.md b/skills/research/references/codebase-archaeology.md deleted file mode 100644 index ab1019add..000000000 --- a/skills/research/references/codebase-archaeology.md +++ /dev/null @@ -1,256 +0,0 @@ - -<!-- TOC: Problem | THE EXACT PROMPT | Documentation First | Quick Start | The Layers | Agent-Assisted | Critical Searches | Output Template | Anti-Patterns | Checklist | References --> - -# Codebase Archaeology - -> **Core Insight:** Don't read randomly. Documentation first, then follow data flow from entry points outward. - -## The Problem - -You land in an unfamiliar codebase. Where do you start? Random file reading wastes context. You need a systematic approach that builds understanding efficiently and produces a reusable "mental model" of the architecture. - ---- - -## THE EXACT PROMPT - -### For Deep Investigation (Spawning Explore Agent) - -``` -Thoroughly explore this codebase. I need to understand: - -1. Overall architecture and module structure -2. How data flows through the system (input → processing → output) -3. Key data structures (the 3-5 types everything revolves around) -4. The integration points (external APIs, databases, file I/O) -5. Configuration system (env vars, config files, CLI flags) -6. Test infrastructure - -Focus on src/ directory structure and main modules. Map out how the pieces fit together. -Be very thorough - I need a complete mental model of how this codebase works. -``` - -### For Self-Directed Exploration - -``` -I want you to sort of randomly explore the code files in this project, choosing -code files to deeply investigate and trace their functionality through related -files. Build a comprehensive mental model of the architecture. -``` - ---- - -## Documentation First (Critical!) - -**Before touching code, ALWAYS read:** - -```bash -cat AGENTS.md # Project-specific rules and architecture notes -cat README.md # Purpose, installation, usage -``` - -**Why this matters:** -- AGENTS.md often contains architecture diagrams, key decisions, gotchas -- README.md reveals the project's purpose and main workflows -- Skipping this wastes time rediscovering documented knowledge - ---- - -## Quick Start - -```bash -# Phase 1: Orientation (2 min) -cat AGENTS.md README.md | head -200 # DOCUMENTATION FIRST! -ls -la src/ lib/ cmd/ pkg/ # Directory structure -cat Cargo.toml package.json pyproject.toml # Dependencies - -# Phase 2: Entry Points (5 min) -rg "fn main|async fn main" --type rust # Rust entry -rg "clap|structopt|argparse|commander" . # CLI frameworks -rg "Router|routes|@app\." . # HTTP routers - -# Phase 3: Core Types (5 min) -rg "^(pub )?struct |^class |^interface " --type rust --type ts --type py -rg "impl .* for" --type rust # Trait implementations - -# Phase 4: Data Flow (10 min) -# Trace from entry → handler → service → storage -``` - ---- - -## The Layers - -``` -┌─────────────────────────────────────┐ -│ ENTRY POINTS (start here) │ -│ main(), CLI commands, HTTP routes │ -└─────────────┬───────────────────────┘ - │ - ▼ -┌─────────────────────────────────────┐ -│ HANDLERS / CONTROLLERS │ -│ Request parsing, orchestration │ -└─────────────┬───────────────────────┘ - │ - ▼ -┌─────────────────────────────────────┐ -│ CORE DOMAIN │ -│ Business logic, key types │ -└─────────────┬───────────────────────┘ - │ - ▼ -┌─────────────────────────────────────┐ -│ STORAGE / INTEGRATION │ -│ Database, files, external APIs │ -└─────────────────────────────────────┘ -``` - ---- - -## Agent-Assisted Exploration - -For large codebases, spawn an Explore agent: - -``` -spawn_agent(agent_type="explorer") -Prompt: "Analyze the [project] codebase to provide a deep technical understanding. -Focus on: -1. Architecture Overview — how components interact -2. Key Data Structures — core types and their relationships -3. Data Flow — trace from ingestion to storage to output -4. Integration Points — external dependencies, APIs, databases" -``` - -**Why agents help:** -- They can read many files without filling your context -- They return a synthesized summary, not raw data -- You get architecture insights without the noise - ---- - -## Language-Specific Entry Points - -| Language | Entry Point | CLI Framework | HTTP Router | -|----------|-------------|---------------|-------------| -| Rust | `fn main()` in main.rs | clap, structopt | axum, actix | -| TypeScript | index.ts, main.ts | commander, yargs | express, fastify | -| Python | `__main__.py`, main.py | argparse, click, typer | flask, fastapi | -| Go | main.go in cmd/ | cobra, flag | chi, gin, echo | - ---- - -## Critical Searches - -```bash -# Find entry points -rg "fn main|def main|function main|export default" . - -# Find configuration -rg "env\.|process\.env|os\.environ|std::env" . -rg "config|settings|options" --type-add 'cfg:*.{toml,yaml,json}' -t cfg - -# Find key types (the 3-5 everything revolves around) -rg "^(pub )?(struct|class|interface|type) \w+" --type rust --type ts --type py - -# Find external integrations -rg "fetch\(|reqwest|aiohttp|requests\." . # HTTP clients -rg "query|execute|SELECT|INSERT" . # Database -rg "open\(|File::|fs\." . # File I/O - -# Find error handling (reveals edge cases) -rg "Error|Exception|panic|unwrap|expect" . -``` - ---- - -## Output Template - -After exploration, produce a **Comprehensive Technical Summary**: - -```markdown -## [Project Name] - Technical Architecture Summary - -### Executive Summary -**[Project]** is a [type] that [purpose]. It implements [key patterns]. - -**Key Statistics:** -- ~X lines of code across Y modules -- Language: [lang] [version] -- Key dependencies: [list] - ---- - -### Entry Points -- `src/main.rs:15` — CLI entry, parses args via clap -- `src/routes/mod.rs:1` — HTTP router (axum) - -### Key Types -| Type | Location | Purpose | -|------|----------|---------| -| `Project` | src/model.rs:10 | Core domain object | -| `Config` | src/config.rs:5 | Runtime configuration | -| `Storage` | src/storage.rs:1 | Persistence layer | - -### Data Flow -``` -CLI args → Config::load() → Project::process() → Storage::save() -``` - -### External Dependencies -- SQLite via rusqlite (persistence) -- reqwest (HTTP client) -- tokio (async runtime) - -### Configuration -| Source | Example | -|--------|---------| -| Env var | `CONFIG_PATH=/etc/tool.toml` | -| Config file | `~/.config/tool/config.toml` | -| CLI flag | `--verbose` | -``` - ---- - -## Anti-Patterns - -| Don't | Do | -|-------|-----| -| Skip AGENTS.md/README | Documentation first, always | -| Read files randomly | Follow entry point → data flow | -| Read entire files | Skim structure, dive into key functions | -| Ignore tests | Tests reveal intended behavior | -| Get lost in details | Build high-level map first | -| Fill context with raw code | Use Explore agent for synthesis | - ---- - -## When to Use What - -| Situation | Approach | -|-----------|----------| -| Brand new codebase | Full archaeology (all phases) | -| Adding a feature | Trace similar existing feature | -| Fixing a bug | Trace from symptom to root | -| Understanding one module | Start from module's public API | -| Large codebase (>10K LOC) | Spawn Explore agent first | - ---- - -## Checklist - -- [ ] **Read AGENTS.md/README.md** — Documentation first! -- [ ] **Orientation:** Directory structure, dependencies -- [ ] **Entry points:** main(), CLI commands, HTTP routes -- [ ] **Key types:** The 3-5 structs/classes everything uses -- [ ] **Data flow:** Entry → processing → storage -- [ ] **Config:** Env vars, config files, defaults -- [ ] **Integration:** External APIs, databases, file I/O -- [ ] **Tests:** What do tests reveal about intended behavior? -- [ ] **Produce summary:** Create reusable architecture doc - ---- - -## References - -| Need | File | -|------|------| diff --git a/skills/research/references/context-discovery.md b/skills/research/references/context-discovery.md deleted file mode 100644 index 4ba8adb7d..000000000 --- a/skills/research/references/context-discovery.md +++ /dev/null @@ -1,190 +0,0 @@ -# Context Discovery Tiers - -**Purpose**: Systematic approach to finding code/context before implementing. - -**Rule**: Work top-to-bottom. Skip tiers if source unavailable. - ---- - -## Tier Order - -| Tier | Source | Tool/Command | When to Skip | -|------|--------|--------------|--------------| -| **1** | Code-Map | `Read docs/code-map/README.md` | No code-map in repo | -| **2** | Semantic Search | `mcp__smart-connections-work__lookup` | MCP not connected | -| **3** | Scoped Search | `Grep/Glob` with path limits | - | -| **4** | Source Code | `Read` files from Tier 1-3 signposts | - | -| **5** | Prior Knowledge | `ls .agents/research/` | Verify against source | -| **6** | External Docs | Context7, WebSearch | Last resort | - ---- - -## Tier Details - -### Tier 1: Code-Map (Fastest) - -```bash -Read docs/code-map/README.md # Find category -Read docs/code-map/{feature}.md # Get signposts -``` - -**Why first**: Local, instant, gives exact paths and function names. - -### Tier 2: Semantic Search - -```bash -mcp__smart-connections-work__lookup --query="$TOPIC" --limit=10 -``` - -**Why second**: Finds conceptual matches code-map might miss. Requires MCP. - -### Tier 3: Scoped Search - -```bash -Grep("pattern", path="services/auth/") # SCOPED -Glob("services/etl/**/*.py") # SCOPED -``` - -**Never**: `Grep("pattern")` or `Glob("**/*.py")` on large repos. - -### Tier 4: Source Code - -Read files identified by Tiers 1-3. Use function/class names, not line numbers. - -### Tier 5: Prior Knowledge - -```bash -ls .agents/research/ | grep -i "$TOPIC" -``` - -**Caution**: May be stale. Always verify findings against current source. - -### Tier 6: External - -- **Context7**: Library documentation -- **WebSearch**: External APIs, standards - ---- - -## Quick Reference - -``` -Code-Map → Semantic → Grep/Glob → Source → .agents/ → External - ↓ ↓ ↓ ↓ ↓ ↓ - paths meaning keywords code history docs -``` - ---- - -## Tier Weights (Flywheel-Optimized) - -Default weights based on typical value. Adjust based on `GET /memories/analytics/sources`: - -| Tier | Source Type | Default Weight | Notes | -|------|-------------|----------------|-------| -| 1 | `code-map` | 1.0 | Local, authoritative | -| 2 | `smart-connections` | 0.95 | High semantic match | -| 3 | `grep`, `glob` | 0.85 | Keyword precision | -| 4 | `read` | 0.80 | Direct source | -| 5 | `prior-research`, `memory-recall` | 0.70 | May be stale | -| 6 | `web-search`, `web-fetch` | 0.60 | External, verify | - -**Optimization loop**: -```bash -# Query source analytics -curl -H "X-API-Key: $KEY" "$ETL_URL/memories/analytics/sources?collection=default" - -# Response includes per-source value_score metrics: -# { -# "sources": [ -# {"source_type": "smart-connections", "value_score": 0.72}, -# {"source_type": "grep", "value_score": 0.61}, -# ... -# ], -# "recommendations": [...] -# } - -# Adjust weights based on value_score: -# value_score = (total_citations / memory_count) × avg_confidence × recency_factor -# -# - value_score > 0.5: Move source up in priority (increase weight) -# - value_score 0.3-0.5: Maintain current position -# - value_score < 0.3: Consider deprioritizing -# - value_score < 0.1 with high count: Review quality - many memories but rarely cited -``` - -**Tool to source_type mapping** (for session analyzer): -```python -WebSearch → "web-search" -WebFetch → "web-fetch" -mcp__smart-connections-work__lookup → "smart-connections" -mcp__smart-connections-personal__lookup → "smart-connections" -mcp__ai-platform__search_knowledge → "compile-knowledge" -mcp__ai-platform__memory_recall → "memory-recall" -Grep → "grep" -Glob → "glob" -Read → "read" -LSP → "lsp" -``` - ---- - -## Failure Pattern Prevention - -Each tier helps prevent specific failure patterns from the Vibe-Coding methodology: - -| Tier | Prevents Pattern | How | -|------|------------------|-----| -| 1 (Code-Map) | #9 Cargo Cult | Authoritative docs explain WHY patterns exist | -| 2 (Semantic) | #7 Zombie Resurrection | Finds prior art you might miss | -| 3 (Scoped Search) | #3 Context Amnesia | Scoping prevents context overload | -| 4 (Source Code) | #2 Confident Hallucination | Verify claims against actual code | -| 5 (Prior Knowledge) | #7 Zombie Resurrection | Don't re-solve solved problems | -| 6 (External) | #11 Security Theater | External standards for security | - -### The 40% Context Rule - -**Critical:** Never exceed 40% context utilization during discovery. - -| Zone | Percentage | Action | -|------|-----------|--------| -| GREEN | <35% | Continue exploration | -| YELLOW | 35-40% | Summarize, prepare to output | -| RED | >40% | STOP. Write findings. Reset. | - -**Why:** Above 40%, Pattern #3 (Context Amnesia) kicks in. Quality degrades exponentially. - -### Defensive Epistemology - -For each tier exploration, apply explicit reasoning: - -```text -DOING: [search/read action] -EXPECT: [what I expect to find] -IF WRONG: [what I'll conclude] -``` - -After: - -```text -RESULT: [what happened] -MATCHES: [yes/no] -THEREFORE: [conclusion] -``` - -This prevents Pattern #2 (Confident Hallucination) by forcing verification. - ---- - -## Anti-Patterns - -| DON'T | DO INSTEAD | Prevents Pattern | -|-------|------------|------------------| -| Start with Grep on full repo | Start with code-map | #3 Amnesia | -| Read source before knowing where | Find signposts first | #3 Amnesia | -| Trust .agents/ without verifying | Cross-check against source | #12 Doc Mirage | -| Web search for internal code | Use Tiers 1-4 | #9 Cargo Cult | -| Unscoped Glob/Grep | Always specify path | #3 Amnesia | -| "This API should work..." | Verify against actual docs | #2 Hallucination | -| "This code looks unused..." | Trace refs, check history | #6 Silent Deletion | -| Read entire large file | Targeted offset/limit | #3 Amnesia | diff --git a/skills/research/references/data-flow-from-entry-points.md b/skills/research/references/data-flow-from-entry-points.md deleted file mode 100644 index 8e9a6d4f9..000000000 --- a/skills/research/references/data-flow-from-entry-points.md +++ /dev/null @@ -1,151 +0,0 @@ -# Data Flow From Entry Points - -> Trace requests, jobs, and commands from the surface they enter on through every handler, dependency, and external sink they touch. Linear paths beat speculative breadth-first reads. - -## Why Trace From Entry Points - -Most architectural questions reduce to "what happens when X arrives?" — an HTTP request, a CLI invocation, a queue message, a scheduled tick. Tracing one of these end-to-end produces: - -- An accurate list of files actually involved (vs. files merely related by name). -- The real layering — handler vs. service vs. storage — instead of the layering the docs claim. -- The contract boundaries: what the handler validates, what the service trusts, where errors are caught vs. propagated. -- A reusable diagram other agents can verify by re-running the same trace. - ---- - -## The Four Entry Surfaces - -| Surface | Where to look | Common library signals | -|---------|---------------|------------------------| -| CLI | `cmd/`, `bin/`, `src/main.*`, top-level entry files | clap, cobra, click, typer, commander, yargs, argparse | -| HTTP | `routes/`, `api/`, `handlers/`, `controllers/` | axum, actix, fastapi, express, fastify, gin, chi, flask | -| Queue / event | `consumers/`, `workers/`, `subscribers/`, `events/` | bull, sidekiq, celery, kafka clients, rabbitmq clients | -| Scheduler | `jobs/`, `cron/`, `schedules/` | cron strings, `@scheduled` decorators, systemd timers | - -A codebase usually has 1–3 of these. Find them all before tracing — you may need to trace one of each surface to understand the full shape. - ---- - -## Trace Procedure - -For one chosen entry point: - -### Step 1: Locate the dispatcher - -Find the registration call (`router.add(...)`, `app.command(...)`, `consumer.subscribe(...)`). Record the `file:line` and the handler symbol it routes to. - -### Step 2: Read the handler - -Open the handler. Note, in order: - -- Inputs and how they are validated. -- Direct dependencies the handler instantiates or receives (DI parameters, module-level singletons). -- External calls (DB, HTTP, filesystem, queue publish) made directly inside the handler. -- Errors caught vs. propagated. - -### Step 3: Walk the dependency tree one level deep - -For each direct dependency, decide: - -- **Self-describing name?** (`UserRepository`, `EmailClient`) — note its purpose without reading. -- **Ambiguous name?** Open it just long enough to write a one-line description. -- **Touches an external sink?** Always open it — the sink is part of the trace. - -Stop at the second level unless a third level is obviously the place where the work actually happens. - -### Step 4: Find the sinks - -Every trace ends at a sink. Common sinks: - -| Sink type | Signals to grep | -|-----------|-----------------| -| Database | `query`, `execute`, `INSERT`, `UPDATE`, `db.`, ORM session calls | -| HTTP egress | `fetch`, `reqwest`, `requests.`, `http.Client`, SDK constructors | -| Filesystem | `open`, `File::`, `fs.`, `pathlib`, write/read functions | -| Queue publish | `publish`, `produce`, `send_message`, `enqueue` | -| Stdout / logs | `print`, `println`, structured logger calls when output is the product | - -Write the sink down. It is the trace's terminal node. - -### Step 5: Note error and retry behavior - -Where in the trace are errors caught? Where do they propagate? Are retries or circuit breakers visible? This is where surprise behavior lives. - ---- - -## Output Shape - -A trace artifact is short and linear: - -```markdown -## Trace: POST /api/jobs - -Entry: `src/api/jobs.rs:42` → `create_job` handler - -create_job (src/api/jobs.rs:42) - ↓ validates JobRequest (src/api/jobs.rs:55) - ↓ JobService::submit (src/services/job.rs:18) - ↓ calls JobRepository::insert (src/storage/jobs.rs:30) — sink: SQLite - ↓ calls Queue::publish (src/queue/mod.rs:22) — sink: Redis stream - ↓ returns 202 with job id - -Errors: -- Validation failure → 400 at handler boundary -- Storage failure → bubbles, logged in middleware (src/middleware/log.rs:12), returns 500 -- Queue failure → swallowed at JobService::submit:24 — KNOWN GAP, see issue #... -``` - -One trace, one page. Multiple traces produce multiple short artifacts rather than one sprawling document. - ---- - -## Searches That Help - -Use these scoped searches as starting points. Always pass a directory; never grep the whole repo unscoped. - -```bash -# Entry-point registration -rg -n "Router::|router\.|@app\.|app\.(get|post)|Cmd\(\"|@click\.command|cobra\.Command" src/ - -# Handler signatures -rg -n "fn (handle|create|update|get|list|delete)_" src/api/ src/handlers/ - -# DB sinks -rg -n "query!?\(|execute!?\(|\.query\(|\.exec\(|SELECT |INSERT |UPDATE " src/ - -# HTTP egress -rg -n "reqwest::|requests\.|fetch\(|http\.Client" src/ - -# Queue publish -rg -n "publish\(|produce\(|enqueue\(|send_message" src/ -``` - -Pair these with the iterative-retrieval pattern (`skills/research/references/iterative-retrieval.md`) when the first scoped search misses. - ---- - -## Anti-Patterns - -| Avoid | Do instead | -|-------|------------| -| Tracing five flows shallowly | Trace one flow end-to-end first | -| Reading every file the handler imports | Use names; only open ambiguous or sink-touching deps | -| Ignoring error paths | Note where errors are caught and where they propagate | -| Calling the trace done at the service layer | Walk to the sink — DB, HTTP egress, filesystem, queue | -| Grepping the whole repo | Always scope to a directory | -| Letting the trace branch into a tree | Pick one path; record alternates as siblings, not children | - ---- - -## When to Use This Reference - -- You are answering "what happens when <event> arrives?" -- You need a short artifact that another agent can verify or extend. -- You are about to modify a handler and need to understand its blast radius. -- The architecture docs disagree with the code, and you need the ground-truth path. - -For broad onboarding, pair this with `skills/research/references/onboarding-methodology.md`. For prior-work search, see `skills/research/references/iterative-retrieval.md`. - ---- - -> Pattern adopted from `codebase-archaeology` (ACFS skill corpus). Methodology only — no verbatim text. diff --git a/skills/research/references/deep-research-mcp.md b/skills/research/references/deep-research-mcp.md deleted file mode 100644 index 896cd1987..000000000 --- a/skills/research/references/deep-research-mcp.md +++ /dev/null @@ -1,134 +0,0 @@ -# Deep Research with MCP Integration - -> Multi-source research using MCP servers (firecrawl, exa, context7) for comprehensive exploration beyond basic web search. - -## When to Use - -- Topic requires authoritative external sources (not just codebase exploration) -- Research question spans multiple domains or requires current data -- Basic `WebSearch` returns insufficient depth -- API documentation or technical specifications needed - -## Research Pipeline - -### Step 1: Decompose Topic into Sub-Questions - -Break the research topic into 3-5 focused sub-questions: - -``` -Topic: "Impact of streaming APIs on agent architectures" -Sub-questions: - 1. What streaming API patterns exist today? (SSE, WebSocket, gRPC streams) - 2. How do major agent frameworks handle streaming? (LangChain, CrewAI, AutoGen) - 3. What are latency/throughput tradeoffs for streaming vs batch? - 4. What production deployment patterns exist for streaming agents? - 5. What's the state of streaming in Claude/OpenAI APIs? -``` - -### Step 2: Multi-Source Search (Per Sub-Question) - -For each sub-question, search across available MCP sources: - -``` -# Primary: Structured web search (if firecrawl MCP connected) -mcp__firecrawl__search(query: "<sub-question keywords>", limit: 8) - -# Secondary: Semantic web search (if exa MCP connected) -mcp__exa__web_search(query: "<sub-question keywords>", numResults: 8) -mcp__exa__web_search_advanced(query: "<keywords>", numResults: 5, startPublishedDate: "2025-01-01") - -# Tertiary: Documentation lookup (if context7 MCP connected) -mcp__context7__resolve_library_id(libraryName: "<library>") -mcp__context7__get_library_docs(context7CompatibleLibraryID: "<id>") - -# Fallback: Standard web search (always available) -WebSearch(query: "<sub-question>") -``` - -**Search Strategy:** -- Use 2-3 keyword variations per sub-question -- Mix general queries with news-focused queries -- Aim for 15-30 unique sources total across all sub-questions -- Prioritize: official docs > academic > reputable news > blogs > forums - -### Step 3: Deep-Read Key Sources (3-5 URLs) - -For the most promising results, fetch full content: - -``` -# Full page scrape (if firecrawl connected) -mcp__firecrawl__scrape(url: "<url>") - -# Semantic content extraction (if exa connected) -mcp__exa__crawling(url: "<url>", tokensNum: 5000) - -# Fallback -WebFetch(url: "<url>") -``` - -### Step 4: Parallel Agent Research (Optional) - -For broad topics, spawn parallel research agents: - -``` -Agent 1: Sub-questions 1-2 (technical patterns) -Agent 2: Sub-questions 3-4 (production deployment) -Agent 3: Sub-question 5 (API state-of-art) -``` - -Main session synthesizes all agent findings into unified report. - -### Step 5: Synthesize Report - -```markdown -# Research: <Topic> - -**Sources:** <N> | **Confidence:** High/Medium/Low | **Date:** <YYYY-MM-DD> - -## Executive Summary -<3-5 sentences> - -## 1. <Theme from Sub-Question 1> -<Findings with inline citations> -- Key point (Source Name, with URL citation) - -## Key Takeaways -- <Actionable insight 1> -- <Actionable insight 2> - -## Knowledge Gaps -- <What we couldn't find> -- <What needs verification> - -## Sources -1. Source Title — one-line summary (with URL) -``` - -## Quality Rules - -1. **Every claim needs a source** — no unsourced assertions -2. **Cross-reference:** If only one source says it, flag as unverified -3. **Prefer recent sources** (last 12 months) for fast-moving topics -4. **Acknowledge gaps explicitly** — "insufficient data found" > hallucination -5. **Separate fact from inference** — label estimates, projections, opinions -6. **Check MCP availability first** — gracefully degrade to WebSearch if MCPs not connected - -## MCP Detection - -Before attempting MCP-based search, check availability: - -``` -# Check which MCPs are available in the current session -# If firecrawl: use firecrawl_search + firecrawl_scrape -# If exa: use web_search_exa + crawling_exa -# If context7: use for library documentation -# If none: fall back to WebSearch + WebFetch -``` - -Log which sources were used for traceability in the report's Methodology section. - -## Integration with /research Skill - -This reference extends the research skill's Step 3 (Launch Explore Agent) with MCP-first search patterns. When the explore agent's Tier 6 (External Docs) triggers, use this pipeline instead of basic WebSearch. - -The iterative retrieval pattern (`references/iterative-retrieval.md`) applies here too: score MCP results for relevance, extract new search terms, and refine across cycles. diff --git a/skills/research/references/document-template.md b/skills/research/references/document-template.md deleted file mode 100644 index b52c66cbf..000000000 --- a/skills/research/references/document-template.md +++ /dev/null @@ -1,191 +0,0 @@ -# Research Document Template - -## Filename Format - -`.agents/research/YYYY-MM-DD-{topic-slug}.md` - -Convert topic to kebab-case slug: -- "authentication flow" -> `2026-01-03-authentication-flow.md` -- "MCP server architecture" -> `2026-01-03-mcp-server-architecture.md` - ---- - -## Required Sections - -### 1. Frontmatter - -```yaml ---- -date: YYYY-MM-DD -type: Research -topic: "Topic Name" -tags: [research, domain, tech] -status: COMPLETE -supersedes: [] ---- -``` - -### 2. Executive Summary - -2-3 sentences: what found, what recommend. - -### 3. Current State - -- What exists today -- Key files table: | File | Purpose | -- Existing patterns - -### 4. Findings - -Each finding with: -- Evidence: `file:line` -- Implications - -### 5. Constraints - -| Constraint | Impact | Mitigation | -|------------|--------|------------| - -### 6. Risks - -| Risk | Likelihood | Impact | Mitigation | -|------|------------|--------|------------| - -### 7. Recommendation - -- Recommended approach -- Rationale -- Alternatives considered and rejected - -### 8. Discovery Provenance - -Track which sources provided key insights (enables flywheel optimization). - -**Purpose**: Create an audit trail showing which discovery method found each insight. This enables post-hoc analysis: "Which sources led to successful implementation?" - -**When to complete**: As you research, add one row per significant finding showing its source. - -**Example**: -```markdown -| Finding | Source Type | Source Detail | Confidence | -|---------|-------------|---------------|------------| -| Gateway request flow | code-map | docs/code-map/gateway.md | 1.0 | -| Middleware pattern | smart-connections | "request middleware chain" | 0.95 | -| Error handling at L45 | grep | services/gateway/middleware.py | 1.0 | -| Rate limiting precedent | prior-research | 2026-01-10-ratelimit.md | 0.85 | -| OAuth2 RFC | web-search | "RFC 6749 OAuth 2.0" | 0.80 | -``` - -**Source Types by Tier** (higher tier = better quality): - -**Tier 1 (Authoritative)** -- `code-map` - Structured architecture documentation (highest confidence) - -**Tier 2 (Semantic)** -- `smart-connections` - Obsidian semantic search -- `compile-knowledge` - MCP ai-platform search - -**Tier 3 (Scoped Search)** -- `grep` - Pattern matching in code -- `glob` - File pattern matching - -**Tier 4 (Source Code)** -- `read` - Direct file reading -- `lsp` - Language Server Protocol queries - -**Tier 5 (Prior Art)** -- `prior-research` - Previous research documents -- `prior-retro` - Retrospective learnings -- `prior-pattern` - Reusable patterns -- `memory-recall` - Semantic memory search - -**Tier 6 (External)** -- `web-search` - Web search results -- `web-fetch` - Direct URL fetch - -**Other** -- `conversation` - User-provided context - -**Confidence scoring**: -- `1.0` - Source is authoritative/written down -- `0.95` - Semantic match, high relevance -- `0.85` - Good match, may need verification -- `0.70` - Reasonable match, verify -- < 0.70 - Use sparingly, needs verification - -### 9. Failure Pattern Risks - -Identify which of the 12 failure patterns are risks for this work. This proactive assessment helps downstream implementation avoid known pitfalls. - -**Required table:** -```markdown -## Failure Pattern Risks - -| Pattern | Risk Level | Mitigation | -|---------|------------|------------| -| #N Pattern Name | HIGH/MEDIUM/LOW | Specific mitigation strategy | -``` - -**Pattern quick reference:** - -| # | Pattern | Common Research Triggers | -|---|---------|-------------------------| -| 1 | Fix Spiral | Complex debugging, unclear root cause | -| 2 | Confident Hallucination | External APIs, unfamiliar libraries | -| 3 | Context Amnesia | Large codebase, many files to read | -| 4 | Tests Passing Lie | Weak test coverage, mocked dependencies | -| 5 | Eldritch Horror | Complex existing code, deep nesting | -| 6 | Silent Deletion | "Unused" code, cleanup opportunities | -| 7 | Zombie Resurrection | Prior failed attempts, known bugs | -| 8 | Gold Plating | Feature creep opportunities | -| 9 | Cargo Cult | New patterns, external examples | -| 10 | Premature Abstraction | Generic solutions proposed | -| 11 | Security Theater | Auth, crypto, access control | -| 12 | Documentation Mirage | Outdated docs, missing comments | - -**Example:** -```markdown -## Failure Pattern Risks - -| Pattern | Risk Level | Mitigation | -|---------|------------|------------| -| #2 Confident Hallucination | HIGH | External OAuth API - verify all claims against official docs | -| #5 Eldritch Horror | MEDIUM | Auth middleware is 400+ lines - document boundaries before changes | -| #9 Cargo Cult | MEDIUM | Using external OAuth example - understand why each step exists | -| #11 Security Theater | HIGH | Auth changes - use established patterns, get security review | -``` - -### 10. Next Steps - -Point to `/plan` for implementation. - ---- - -## Tag Vocabulary - -**Rules:** 3-5 tags total. First tag MUST be `research`. - -| Category | Valid Tags | -|----------|------------| -| **Core Domains** | `agents`, `data`, `api`, `infra`, `security`, `auth` | -| **Quality** | `testing`, `reliability`, `performance`, `monitoring` | -| **Process** | `ci-cd`, `workflow`, `ops`, `docs` | -| **Governance** | `architecture`, `compliance`, `standards`, `ui` | -| **Languages** | `python`, `shell`, `typescript`, `go`, `yaml` | -| **Platforms** | `helm`, `kubernetes`, `openshift`, `docker`, `argocd` | -| **AI Stack** | `mcp`, `litellm`, `neo4j`, `postgres`, `redis`, `fastapi` | - -**Examples:** -- `[research, agents, mcp]` - MCP server research -- `[research, data, neo4j]` - Data storage research -- `[research, security, auth]` - Authentication research - ---- - -## Status Values - -| Status | Meaning | -|--------|---------| -| `COMPLETE` | Ready for planning | -| `IN_PROGRESS` | Ongoing research | -| `SUPERSEDED` | Newer research exists | diff --git a/skills/research/references/failure-patterns.md b/skills/research/references/failure-patterns.md deleted file mode 100644 index 0bd1d509a..000000000 --- a/skills/research/references/failure-patterns.md +++ /dev/null @@ -1,321 +0,0 @@ -# The 12 Failure Patterns (Research Reference) - -> Based on the Vibe-Coding methodology. Load this when you need full pattern details for risk assessment. - ---- - -## Quick Reference - -| # | Pattern | Key Symptom | First Action | -|---|---------|-------------|--------------| -| 1 | Fix Spiral | >3 attempts, circles | STOP, revert | -| 2 | Confident Hallucination | Non-existent APIs | Verify docs | -| 3 | Context Amnesia | Forgotten constraints | Save state | -| 4 | Tests Passing Lie | Green but broken | Manual test | -| 5 | Eldritch Horror | >200 line functions | Extract/refactor | -| 6 | Silent Deletion | Missing code | Check git history | -| 7 | Zombie Resurrection | Bugs return | Add regression test | -| 8 | Gold Plating | Unrequested features | Revert extras | -| 9 | Cargo Cult | Copied patterns | Understand why | -| 10 | Premature Abstraction | Generic w/ one use | Inline | -| 11 | Security Theater | Bypassable security | Audit | -| 12 | Documentation Mirage | Docs don't work | Test docs | - ---- - -## Inner Loop Patterns (Seconds-Minutes) - -### 1. The Fix Spiral - -**Description:** Making a fix that breaks something else, then fixing that break which causes another issue, creating a cascading chain without resolution. - -**Symptoms:** -- More than 3 fix attempts without convergence -- Changes oscillating between two states -- "This should work" appearing in explanations -- Error messages changing but not disappearing - -**Research Defense:** -- Research root cause BEFORE attempting fix -- Document expected behavior vs actual behavior -- Identify all code paths affected - -**Prevention:** -- Set hard limit: 3 attempts then STOP -- State explicit prediction before each fix -- Checkpoint working state before each attempt - ---- - -### 2. The Confident Hallucination - -**Description:** Generating plausible-sounding but factually incorrect information about APIs, libraries, or behavior. - -**Symptoms:** -- Code references non-existent methods or parameters -- API usage that "looks right" but fails at runtime -- Overly specific technical claims without evidence -- Version-specific features applied to wrong versions - -**Research Defense:** -- VERIFY all API claims against actual documentation -- Note confidence levels in provenance table -- Use Tier 6 (external docs) for unfamiliar APIs - -**Prevention:** -- Test code in isolation before integration -- Use "I don't know" as valid response -- Run type checkers and linters early - ---- - -### 3. The Context Amnesia - -**Description:** As context window fills, losing track of earlier constraints, requirements, or decisions. - -**Symptoms:** -- Reintroducing previously fixed bugs -- Contradicting earlier decisions -- Forgetting project-specific conventions -- Repeating completed work - -**Research Defense:** -- Stay <40% context utilization -- Write findings to files immediately -- Use targeted reads (offset/limit) not full files - -**Prevention:** -- Save progress frequently -- Start fresh sessions for distinct work -- Front-load critical constraints - ---- - -### 4. The Tests Passing Lie - -**Description:** Tests pass but code doesn't actually work - too narrow, wrong thing, mocks away behavior. - -**Symptoms:** -- Green test suite but broken functionality -- Tests that test mocks instead of real behavior -- Coverage looks good but edge cases fail -- Tests modified in same PR as code they test - -**Research Defense:** -- Find actual test coverage in research -- Identify what tests actually verify -- Note mocked vs real dependencies - -**Prevention:** -- Run tests yourself; don't trust reported results -- Separate test changes from code changes -- Manual smoke test after suite passes - ---- - -## Middle Loop Patterns (Hours-Days) - -### 5. The Eldritch Horror - -**Description:** Code becomes incomprehensible - functions spanning hundreds of lines, deeply nested logic, unclear naming. - -**Symptoms:** -- Functions exceeding 200 lines -- Nesting depth beyond 4 levels -- Variable names like `temp2`, `data3` -- Comments that don't match behavior - -**Research Defense:** -- Document complexity limits in findings -- Note current complexity metrics -- Identify refactoring boundaries - -**Prevention:** -- Enforce hard limits: <200 lines per function -- Require meaningful names -- Use explicit interfaces - ---- - -### 6. The Silent Deletion - -**Description:** Removing code that appears unused but is actually necessary for edge cases, legacy support, or fallbacks. - -**Symptoms:** -- "Cleanup" commits that remove "dead code" -- Features that worked yesterday now fail -- Error handling mysteriously missing -- Comments about "why" deleted along with code - -**Research Defense:** -- Research WHY code exists before removal -- Check git history for context -- Trace all references including dynamic calls - -**Prevention:** -- Never delete without understanding purpose -- Get human approval for deletion -- Keep deleted code in comments initially - ---- - -### 7. The Zombie Resurrection - -**Description:** Previously fixed bugs return because similar code regenerated without fix, or reverts during refactoring. - -**Symptoms:** -- Bug reports for issues marked "fixed" -- Same error in different code paths -- Fixes lost during refactoring -- "I thought we fixed this" conversations - -**Research Defense:** -- Prior art search prevents re-solving -- Check for existing regression tests -- Document root cause, not just fix - -**Prevention:** -- Add regression tests for every fix -- Use automated checks for anti-patterns -- Keep lessons learned file - ---- - -### 8. The Gold Plating - -**Description:** Adding unrequested features, extra error handling, additional configurability beyond what was asked. - -**Symptoms:** -- PR larger than expected -- New config options no one asked for -- "While I was here, I also..." explanations -- Abstraction layers for single use cases - -**Research Defense:** -- Define explicit scope in research -- Note ONLY what's needed for the task -- Separate "nice to have" from "required" - -**Prevention:** -- Define explicit scope before starting -- Reject changes outside stated scope -- Prefer boring, obvious solutions - ---- - -## Outer Loop Patterns (Days-Weeks) - -### 9. The Cargo Cult - -**Description:** Copying patterns from examples without understanding why they work. May be inappropriate for context. - -**Symptoms:** -- Copy-pasted code with irrelevant portions -- Patterns from different frameworks mixed -- "Best practices" where they don't fit -- Configuration copied without understanding - -**Research Defense:** -- Understand WHY patterns exist -- Ask "why does this pattern exist?" for each -- Verify example matches your context - -**Prevention:** -- Test copied code in isolation first -- Adapt patterns to local conventions -- Trace examples to their source - ---- - -### 10. The Premature Abstraction - -**Description:** Creating generic abstractions before concrete use cases exist. Abstractions don't match actual needs. - -**Symptoms:** -- Generic interfaces with one implementation -- Factory patterns for single classes -- Configuration for cases that don't exist -- "Future-proofing" never used - -**Research Defense:** -- Document concrete use cases first -- Require 3+ concrete cases before abstracting -- Note where duplication exists vs speculation - -**Prevention:** -- Write concrete implementations first -- Prefer duplication over wrong abstraction -- Extract only when duplication appears - ---- - -### 11. The Security Theater - -**Description:** Code appears secure but isn't - validation that misses edge cases, encryption with hardcoded keys. - -**Symptoms:** -- Security measures easily circumvented -- Validation on client but not server -- Hardcoded credentials or keys -- "Security by obscurity" approaches - -**Research Defense:** -- Include security constraints in research -- Reference external security standards -- Note auth/crypto/access control patterns - -**Prevention:** -- Use established security libraries -- Security review by qualified humans -- Static analysis for vulnerabilities - ---- - -### 12. The Documentation Mirage - -**Description:** Documentation exists but doesn't match reality - outdated READMEs, incorrect API docs. - -**Symptoms:** -- Following docs leads to errors -- Comments contradict adjacent code -- Examples that don't compile -- Setup instructions that don't work - -**Research Defense:** -- Verify docs match reality -- Test documentation by following it literally -- Note discrepancies in research findings - -**Prevention:** -- Treat docs as code: test them -- Update docs in same PR as code -- Use executable documentation - ---- - -## Pattern Frequency Tracking - -Use this in research outputs to track which patterns are relevant: - -```markdown -## Failure Pattern Risks - -| Pattern | Risk Level | Mitigation | -|---------|------------|------------| -| #2 Confident Hallucination | HIGH | Verify external API claims | -| #5 Eldritch Horror | MEDIUM | Keep functions <200 lines | -| #9 Cargo Cult | MEDIUM | Understand why patterns exist | -``` - -Risk Levels: -- **HIGH**: Strong indicators in research, requires explicit mitigation -- **MEDIUM**: Some indicators, requires awareness -- **LOW**: Minor indicators, standard practices sufficient - ---- - -## See Also - -- `~/.claude/CLAUDE-base.md` - Core Vibe-Coding methodology -- `~/.claude/plugins/marketplaces/agentops-marketplace/reference/failure-patterns.md` - Full pattern reference -- `~/.claude/skills/crank/failure-taxonomy.md` - Execution failure taxonomy diff --git a/skills/research/references/iterative-retrieval.md b/skills/research/references/iterative-retrieval.md deleted file mode 100644 index 3952e183f..000000000 --- a/skills/research/references/iterative-retrieval.md +++ /dev/null @@ -1,110 +0,0 @@ -# Iterative Retrieval Pattern - -> Progressive context refinement for subagents. Solves "I don't know what I need to know." - -## Problem - -When spawning research or explore agents, the initial query often misses critical context because: -- The agent doesn't know the codebase's naming conventions -- Related features use unexpected terminology -- Key context lives in files the agent wouldn't think to search - -Flat keyword search returns either too much noise or misses relevant files. - -## Solution: 4-Phase Iterative Loop - -### Phase 1: DISPATCH — Broad keyword search -``` -Search for: <topic> -Use 3-5 keyword variants: - - Exact term: "<topic>" - - Synonyms: "<synonym1>", "<synonym2>" - - Implementation terms: "<likely-function-name>", "<likely-file-pattern>" -``` - -### Phase 2: EVALUATE — Score relevance (0-1) -For each result, assign a relevance score: - -| Score | Meaning | Action | -|-------|---------|--------| -| 0.8-1.0 | Directly implements target feature | Read fully, extract details | -| 0.5-0.7 | Contains related patterns or interfaces | Skim for cross-references | -| 0.2-0.4 | Tangentially related | Note for later if gaps remain | -| 0.0-0.2 | Not relevant | Discard | - -### Phase 3: REFINE — Extract new keywords -From high-relevance files (0.5+), extract: -- Function/class names referenced but not yet searched -- Import paths pointing to unexplored modules -- Config keys or env vars mentioned -- Error messages or log strings (grep targets) - -Add these as new search terms. - -### Phase 4: LOOP — Repeat max 3 cycles -``` -Cycle 1: Broad search → find core files → extract new terms -Cycle 2: Targeted search with extracted terms → find related files → more terms -Cycle 3: Fill remaining gaps → verify completeness -``` - -**Stop early if:** -- No new high-relevance results in a cycle -- All critical questions answered -- Context budget reached - -## Integration with /research - -In Step 3 (Launch Explore Agent), add iterative retrieval to the exploration prompt: - -``` -Use iterative retrieval: -1. Start with broad keyword search for "<topic>" -2. Score each result 0-1 for relevance -3. From files scoring 0.5+, extract new search terms -4. Search with new terms (max 3 cycles) -5. Report: files found per cycle, relevance scores, final coverage -``` - -## Integration with /swarm - -When spawning parallel workers that need codebase context: - -``` -Before implementation, run 1-2 retrieval cycles to gather context: -- Search for files related to your task -- Read the highest-relevance files (0.7+) -- Note patterns and conventions from those files -- Then implement following those patterns -``` - -This prevents workers from reinventing patterns that already exist in the codebase. - -## Example: Researching "authentication" - -**Cycle 1:** -- Search: "auth", "authentication", "login", "session" -- Hits: `auth/middleware.go` (0.9), `auth/token.go` (0.8), `config/auth.go` (0.6), `README.md` (0.2) -- New terms from hits: `ValidateToken`, `SessionStore`, `JWT_SECRET` - -**Cycle 2:** -- Search: "ValidateToken", "SessionStore", "JWT_SECRET" -- Hits: `store/session.go` (0.9), `config/env.go` (0.7), `test/auth_test.go` (0.8) -- New terms: `RefreshToken`, `store.NewRedisStore` - -**Cycle 3:** -- Search: "RefreshToken", "RedisStore" -- Hits: `auth/refresh.go` (0.9), `store/redis.go` (0.8) -- No new high-relevance terms → STOP - -**Result:** Complete auth system map in 3 cycles vs flat search that would miss `store/` and `config/env.go`. - -## Anti-Patterns - -| Anti-Pattern | Why It Fails | Fix | -|-------------|-------------|-----| -| Searching entire repo with no scope | Context overload, slow | Always scope to directories | -| Only 1 keyword | Misses synonym usage | Start with 3-5 variants | -| No relevance scoring | Reads everything equally | Score and prioritize | -| >3 cycles | Diminishing returns | Stop at 3, report gaps | -| Ignoring low-relevance files | Sometimes tangential files have key context | Note them, revisit if gaps remain | diff --git a/skills/research/references/onboarding-methodology.md b/skills/research/references/onboarding-methodology.md deleted file mode 100644 index 6ecf3b51b..000000000 --- a/skills/research/references/onboarding-methodology.md +++ /dev/null @@ -1,194 +0,0 @@ -# Onboarding Methodology - -> Build a working mental model of an unfamiliar codebase fast. Read the docs first, locate entry points, then trace one representative path to its sink — never random file reads. - -## Problem - -Landing in a new codebase, the temptation is to grep for keywords or open files at random. That burns context without producing structure. Onboarding research needs a repeatable shape: orient on docs, locate entry points, identify the 3–5 types everything revolves around, then trace one representative flow end-to-end. The output should be reusable: another agent (or future you) reads the summary and skips the cold-start cost. - ---- - -## Phased Walk - -| Phase | Goal | Time box | Output | -|-------|------|----------|--------| -| 1. Orient on docs | Pull what is already written down | 2 min | Notes on stated purpose, conventions, gotchas | -| 2. Inventory the surface | Directory layout, dependencies, build system | 3 min | Annotated tree of top-level dirs | -| 3. Locate entry points | `main`, CLI commands, HTTP routes, queue consumers | 5 min | List of `file:line` for each entry surface | -| 4. Identify core types | The 3–5 structs/classes everything else references | 5 min | Type table with location and purpose | -| 5. Trace one flow | Pick the most representative entry → output path | 10 min | Linear data-flow diagram | -| 6. Note integrations | DBs, external APIs, file I/O, queues | 3 min | Dependency table | -| 7. Skim tests | What invariants does the test suite assert? | 2 min | List of behavioral guarantees found | -| 8. Write the summary | Reusable mental-model artifact | 5 min | Document under `.agents/research/` | - -If a phase has no signal in 90 seconds, skip and note the gap. - ---- - -## Phase 1: Documentation First - -Read in this order before opening source: - -```bash -cat AGENTS.md # Project rules, architecture decisions, gotchas -cat CLAUDE.md # Same — most repos symlink one to the other -cat README.md # Stated purpose, install, primary workflows -ls docs/ && cat docs/index.md docs/architecture.md 2>/dev/null -``` - -Capture three things from this pass: -1. The project's stated purpose in one sentence. -2. The top 3 conventions or rules the docs call out. -3. Any explicit "do not touch" or "load-bearing" warnings. - -Skipping this phase is the most common onboarding failure — it makes you rediscover documented constraints by trial and error. - ---- - -## Phase 2: Inventory the Surface - -```bash -ls -la # Top-level shape -ls -la src/ lib/ cmd/ pkg/ # Source roots -cat Cargo.toml package.json pyproject.toml go.mod 2>/dev/null -``` - -Annotate each top-level directory with a one-line guess at its role. Confirm the guesses in later phases. - ---- - -## Phase 3: Entry Points - -Use language-aware searches — see `skills/research/references/context-discovery.md` for tier ordering. Patterns to look for: - -| Surface | Signals | -|---------|---------| -| Process entry | `fn main`, `def main`, `func main`, `if __name__ == "__main__"` | -| CLI surface | clap/cobra/click/typer/commander/yargs derivations, command registration calls | -| HTTP surface | route registration calls, decorator usage, router builders | -| Queue/event surface | consumer/handler/subscriber registration | -| Scheduler surface | cron/timer/job declarations | - -Record each as `file:line` — these become navigation anchors in the summary. - ---- - -## Phase 4: Core Types - -Look for the 3–5 types everything else flows through. Signals: - -- Mentioned in most files when grepped by name. -- Returned or consumed by multiple entry-point handlers. -- Declared in a `model.rs`, `types.ts`, `schema.py`, or equivalent root. - -Capture each in a table: name, location, purpose, key fields. If you cannot describe the purpose in one sentence, the type is not yet understood — flag it as a gap. - ---- - -## Phase 5: Trace One Flow - -Pick the most representative entry-point handler. Walk it: - -1. Read the handler. Note every function it calls. -2. For each callee, decide: do I need to open it, or is the name self-describing? -3. Stop when you hit storage, an external API, or a return that closes the loop. -4. Write the path as a linear arrow chain. - -One traced flow is more useful than five half-traced flows. - ---- - -## Phase 6 & 7: Integrations and Tests - -Integrations: list the DBs, HTTP clients, file paths, and queues touched by the traced flow. Note the library used for each. - -Tests: read 1–2 test files for the traced flow. The asserts reveal which behaviors the team treats as invariants. - ---- - -## Mental-Model Output Template - -Write the summary as `.agents/research/YYYY-MM-DD-<project>-mental-model.md` using this shape. Keep it under one page. - -```markdown ---- -date: YYYY-MM-DD -type: Research -topic: "<project> onboarding mental model" -tags: [research, onboarding, architecture] -status: COMPLETE ---- - -# <Project> — Mental Model - -## Executive Summary -<2–3 sentences: what it is, what it does, the one architectural choice that defines it.> - -## Entry Points -| Surface | Location | Purpose | -|---------|----------|---------| -| CLI | `src/main.rs:15` | clap parser, dispatches to subcommand | -| HTTP | `src/routes/mod.rs:1` | axum router, mounts `/api/*` | - -## Key Types -| Type | Location | Purpose | -|------|----------|---------| -| `Project` | `src/model.rs:10` | Core domain object | -| `Config` | `src/config.rs:5` | Runtime configuration loaded once | -| `Storage` | `src/storage.rs:1` | Persistence boundary | - -## Data Flow (representative path) -CLI args → `Config::load()` → `Project::process()` → `Storage::save()` - -## External Dependencies -| System | Library | Where touched | -|--------|---------|---------------| -| SQLite | rusqlite | `src/storage.rs` | -| HTTP | reqwest | `src/clients/api.rs` | - -## Configuration Surfaces -| Source | Example | -|--------|---------| -| Env var | `CONFIG_PATH=/etc/tool.toml` | -| File | `~/.config/tool/config.toml` | -| Flag | `--verbose` | - -## Testing Surface -- `tests/integration_test.rs` covers the CLI → storage path end-to-end. -- Property tests in `tests/prop/` assert <invariant>. -- Gaps: <untested surfaces noted during the read>. - -## Gaps and Open Questions -- <Files skipped because purpose unclear> -- <Areas where docs disagree with code> -``` - ---- - -## Anti-Patterns - -| Avoid | Do instead | -|-------|------------| -| Skipping `AGENTS.md`/`README.md` | Always read them first; they save hours | -| Random file reads | Walk entry → handler → core type → storage | -| Reading full files end-to-end | Skim structure, dive into the 1–2 critical functions | -| Ignoring tests | Tests reveal the invariants the team enforces | -| Filling context with raw source | Synthesize into the template; cite `file:line` | -| Summarizing everything you read | Cut to the 3–5 core types and one traced flow | - ---- - -## Checklist - -- [ ] `AGENTS.md` and `README.md` read before any source file. -- [ ] Top-level directory annotated. -- [ ] Entry points listed with `file:line`. -- [ ] 3–5 core types named with one-sentence purposes. -- [ ] One representative flow traced end-to-end. -- [ ] Integrations and tests noted. -- [ ] Summary written under `.agents/research/` using the template. -- [ ] Gaps explicitly listed — no false completeness. - ---- - -> Pattern adopted from `codebase-archaeology` (ACFS skill corpus). Methodology only — no verbatim text. diff --git a/skills/research/references/ralph-loop-contract.md b/skills/research/references/ralph-loop-contract.md deleted file mode 100644 index 994692f7b..000000000 --- a/skills/research/references/ralph-loop-contract.md +++ /dev/null @@ -1,49 +0,0 @@ -# Ralph Loop Contract (Reverse-Engineered) - -This contract captures the operational Ralph mechanics reverse-engineered from: -- `https://github.com/ghuntley/how-to-ralph-wiggum` -- `.tmp/how-to-ralph-wiggum/README.md` -- `.tmp/how-to-ralph-wiggum/files/loop.sh` -- `.tmp/how-to-ralph-wiggum/files/PROMPT_plan.md` -- `.tmp/how-to-ralph-wiggum/files/PROMPT_build.md` - -Use this as the source-of-truth for Ralph alignment in AgentOps orchestration skills. - -## Core Contract - -1. Fresh context every iteration/wave. -- Each execution unit starts clean; no carryover worker memory. - -2. Scheduler-heavy, worker-light. -- The lead/orchestrator schedules and reconciles. -- Workers perform one scoped unit of work. - -3. Disk-backed shared state. -- Loop continuity comes from filesystem state, not accumulated chat context. -- In classic Ralph: `IMPLEMENTATION_PLAN.md` and `AGENTS.md`. - -4. One-task atomicity. -- Select one important task, execute, validate, persist state, then restart fresh. - -5. Backpressure before completion. -- Build/tests/lint/gates must reject bad output before task completion/commit. - -6. Observe and tune outside the loop. -- Humans (or lead agents) monitor outcomes and adjust prompts/constraints/contracts. - -## AgentOps Mapping - -| Ralph concept | AgentOps implementation | -|---|---| -| Fresh context per loop | New workers/teams per wave in `/swarm`; fresh operating-loop context per worker or NTM pane | -| Main context as scheduler | Mayor/lead orchestration in `/swarm` and `/crank` | -| Plan file as state | `bd` issue graph, TaskList state, plan artifacts in `.agents/plans/` | -| One task per pass | One issue per worker assignment in swarm/crank waves | -| Backpressure | `/validate`, task validation hooks, tests/lint gates, push/premortem gates | -| Outer loop restart | Wave loop in `/crank`; NTM/Agent Mail substrate for out-of-session loop restarts | - -## Implementation Notes - -- Keep worker prompts concise and operational. -- Keep state in files/issue trackers, not long conversational memory. -- Prefer deterministic checks over subjective completion. diff --git a/skills/research/references/research.feature b/skills/research/references/research.feature index 77343d74a..6decc980d 100644 --- a/skills/research/references/research.feature +++ b/skills/research/references/research.feature @@ -1,32 +1,11 @@ -# Executable spec for the /research skill — Move 1 of the operating loop (driving-adapter). -# /research investigates a topic prior-art-first, dispatches an explore agent that uses -# iterative retrieval, and writes a cited artifact to .agents/research/ — every claim -# carries a file:line reference. Interactive runs gate on human approval; --auto skips it. -# Hexagon: driving-adapter; consumes ao-lookup context + repo-context; produces .agents/research/*.md -# + result.json. (soc-qk4b) +Feature: Research answers one bounded question + Scenario: Load-bearing claims are cited + Given a bounded question and required evidence + When Research examines the smallest relevant sources + Then observations and inferences are distinguished + And every load-bearing claim cites authoritative evidence -Feature: Research produces a cited investigation artifact, prior-art first - As Move 1 of the operating loop - I want a topic investigated against existing knowledge before fresh exploration - So that findings are grounded, cited, and not redundant with what is already known - - Scenario: prior art is searched before fresh exploration - When /research runs on a topic - Then it first searches existing knowledge (ao lookup + the .agents/ knowledge dirs) - And applicable prior learnings are cited in the output, not just loaded passively - - Scenario: an explore agent investigates with iterative retrieval - When the investigation runs - Then an explore agent is dispatched (not merely described) - And it uses iterative retrieval — score results, extract new terms from high-relevance - hits, refine over up to 3 cycles - - Scenario: findings are written as a cited artifact - When the investigation completes - Then findings are written to .agents/research/YYYY-MM-DD-<slug>.md - And every claim carries a file:line citation - - Scenario: interactive runs gate on approval, --auto does not - When /research runs without --auto - Then it requests human approval (Gate 1) before reporting completion - And with --auto it proceeds without the approval gate + Scenario: Research stops at the evidence boundary + Given a cited answer with checked and unchecked scope + When Research reports the result + Then it does not approve work, select a next action, retry, or mutate lifecycle state diff --git a/skills/research/references/software-research.md b/skills/research/references/software-research.md deleted file mode 100644 index ba881cc16..000000000 --- a/skills/research/references/software-research.md +++ /dev/null @@ -1,663 +0,0 @@ - -# research-software — Software Research - -> **Rules:** Latest STABLE tag (not main). Filter to 2025-2026. Code > Docs. Skip Stack Overflow. - -## Output First - -Every research produces this structure: - -```markdown -## [Tool] vX.Y.Z (YYYY-MM-DD) - -**Repo:** github.com/org/repo @ abc123 - -### Commands -| Task | Command | Notes | -|------|---------|-------| - -### Config -| Option | Default | Notes | -|--------|---------|-------| - -### Env Vars -| Variable | Purpose | -|----------|---------| - -### Gotchas -- [problem]: [fix]. Source: [PR/issue/code] - -### Sources -- Code: [file:line] -- PRs: #123, #456 -- Posts: [url] -``` - ---- - -## THE PROMPT - -``` -Research [TOOL] for [PURPOSE]. -Clone to /tmp, checkout latest stable tag. -Spawn Explore agent on source. Find: CLI, config, hidden flags, env vars. -Parallel: GitHub PRs/issues, web search "[tool] 2025". -Output: skill-ready markdown. -``` - ---- - -## Pipeline - -```bash -# 0. Detect context (if in a project) -# Check package.json, Cargo.toml, pyproject.toml for existing versions - -# 1. Clone + stable tag -git clone --depth 1 https://github.com/[org]/[repo] /tmp/[repo]-research -cd /tmp/[repo]-research && git fetch --tags && git checkout $(git describe --tags --abbrev=0) - -# 2. Spawn Explore agent (parallel with step 3-4) -# → "Find all CLI commands, config options, hidden flags, env vars in /tmp/[repo]-research" - -# 3. GitHub activity -gh pr list -R [org]/[repo] --state merged --limit 30 --json title,mergedAt -gh issue list -R [org]/[repo] --label question --limit 20 - -# 4. Web search -# → "[tool] 2025" "[tool] 2026" "[tool] tutorial" - -# 5. Synthesize → Output structure above - -# 6. Cleanup -rm -rf /tmp/[repo]-research -``` - ---- - -## Checklist - -- [ ] **Detect context:** Check package.json/Cargo.toml/pyproject.toml for versions -- [ ] **Clone repo** to /tmp, checkout latest stable tag -- [ ] **Explore agent:** CLI commands, config schema, hidden flags, env vars -- [ ] **GitHub:** Recent merged PRs, issues tagged "question"/"documentation" -- [ ] **Web search:** "[tool] 2025", "[tool] 2026", skip pre-2025 -- [ ] **Synthesize:** Commands table, config table, gotchas, patterns -- [ ] **Cite sources:** repo@commit, PR numbers, blog URLs -- [ ] **Clean up:** `rm -rf /tmp/[repo]-research` - ---- - -## Source Priority - -``` -1. Source code (actual behavior) -2. Recent PRs (features being added) -3. GitHub issues (real problems) -4. Blog posts 2025-2026 (practical patterns) -5. Official docs (baseline, often outdated) -``` - -**Skip:** Stack Overflow, anything pre-2025, basic tutorials - ---- - -## Top Mistakes - -| Mistake | Fix | -|---------|-----| -| Using beta/canary | Checkout latest stable TAG, not main | -| Old content (pre-2025) | Always add year to search queries | -| Trusting docs over code | Code wins: check actual defaults in source | -| Missing env vars | Search `process.env`, `std::env`, `os.environ` | -| Forgetting cleanup | `rm -rf /tmp/[repo]-research` when done | - ---- - -## Key Searches - -```bash -# Hidden/experimental flags -rg "hidden|experimental|unstable" /tmp/[repo]-research - -# Environment variables by language -rg "process\.env\." /tmp/[repo]-research --type ts # TypeScript -rg "std::env::" /tmp/[repo]-research --type rust # Rust -rg "os\.environ" /tmp/[repo]-research --type py # Python -rg "os\.Getenv" /tmp/[repo]-research --type go # Go - -# Recent changes -git log --oneline --since="2025-06-01" | head -30 -``` - ---- - -## Done When - -- [ ] Have version number from stable tag -- [ ] Commands table has 5+ entries -- [ ] Config table covers main options -- [ ] Gotchas section has 3+ real issues from GitHub/code -- [ ] All sources cited with links - ---- - -## Decision Tree - -``` -What are you researching? -│ -├─ CLI tool (wrangler, cargo, bun) -│ Focus: src/cli/, commands, flags, env vars -│ -├─ Library/Framework (React, Next.js) -│ Focus: packages/*/src/, exported APIs, deprecations -│ -├─ Runtime (Bun, Deno, Node) -│ Focus: built-ins, runtime flags, compat layers -│ -└─ Database/Service (D1, R2, Postgres) - Focus: query syntax, config, limits, gotchas -``` - -### Key Searches by Type - -| Type | Where to look | Key searches | -|------|---------------|--------------| -| CLI | `src/cli/`, `bin/` | `hidden.*true`, `#[arg(`, `process.env` | -| Library | `packages/*/src/`, `index.ts` | `export `, `deprecated`, `experimental` | -| Runtime | `src/`, built-ins | `flag`, `--`, `compat` | -| Database | queries, limits | `limit`, `max`, `error` | - -**Deep strategies:** [STRATEGIES.md](references/STRATEGIES.md) - ---- - -## Subagent: Code Investigator - -``` -Investigate /tmp/[repo]-research for [TOOL]. -Find: CLI commands, config options, hidden/experimental flags, env vars. -Check git log --oneline -30 for recent changes. -Output as markdown tables. -``` -Use model: `sonnet` (balance of speed + depth) - ---- - -## Subagent: Web Researcher - -``` -Search "[TOOL] 2025" and "[TOOL] 2026". -Find 5-10 recent tutorials, blog posts, announcements. -Extract: patterns, gotchas, tips. -Skip: Stack Overflow, anything pre-2025, basic tutorials. -``` -Use model: `haiku` (fast, web-focused) - ---- - -## References - -| Need | File | -|------|------| -| Output templates by tool type | [OUTPUT-TEMPLATES.md](references/OUTPUT-TEMPLATES.md) | -| Example research sessions | [EXAMPLES.md](references/EXAMPLES.md) | -| Tool-specific deep strategies | [STRATEGIES.md](references/STRATEGIES.md) | -# Research Examples - -Real sessions showing the workflow. - ---- - -## CLI Tool: Wrangler - -```bash -# 1. Clone -git clone --depth 1 https://github.com/cloudflare/workers-sdk.git /tmp/workers-sdk-research -cd /tmp/workers-sdk-research && git fetch --tags && git checkout $(git describe --tags --abbrev=0) - -# 2. Explore agent prompt: -# "Investigate /tmp/workers-sdk-research/packages/wrangler: CLI commands, config schema, hidden flags, env vars" - -# 3. GitHub -gh pr list -R cloudflare/workers-sdk --state merged --limit 30 --json title,mergedAt -gh issue list -R cloudflare/workers-sdk --label "question" --limit 20 - -# 4. Web search: "wrangler 2025", "cloudflare workers tutorial 2026" - -# 5. Cleanup -rm -rf /tmp/workers-sdk-research -``` - -**Key findings location:** `packages/wrangler/src/` — commands in `src/`, config schema in types. - ---- - -## Framework: Next.js - -```bash -# 1. Clone + stable tag -git clone --depth 1 https://github.com/vercel/next.js.git /tmp/nextjs-research -cd /tmp/nextjs-research && git fetch --tags && git checkout $(git describe --tags --abbrev=0) - -# 2. Explore agent prompt: -# "Investigate /tmp/nextjs-research/packages/next/src: exported APIs, experimental flags, config options" - -# 3. Quick searches -rg "experimental" /tmp/nextjs-research/packages/next/src/server/config-shared.ts -rg "deprecated" /tmp/nextjs-research/packages/next/src --type ts | head -20 - -# 4. Web search: "next.js 15 2025", "next.js app router 2026" - -# 5. Cleanup -rm -rf /tmp/nextjs-research -``` - -**Key findings location:** `packages/next/src/server/config-shared.ts` for all config options. - ---- - -## Runtime: Bun - -```bash -# 1. Clone -git clone --depth 1 https://github.com/oven-sh/bun.git /tmp/bun-research -cd /tmp/bun-research && git fetch --tags && git checkout $(git describe --tags --abbrev=0) - -# 2. Explore agent prompt: -# "Investigate /tmp/bun-research/src: CLI flags, built-in APIs (Bun.*), env vars" - -# 3. Quick searches -rg "process\.env\." /tmp/bun-research/src --type ts | head -30 -rg "Bun\." /tmp/bun-research/packages/bun-types/bun.d.ts | head -50 - -# 4. Web search: "bun runtime 2025", "bun vs node 2026" - -# 5. Cleanup -rm -rf /tmp/bun-research -``` - -**Key findings location:** `packages/bun-types/` for all Bun.* APIs. - ---- - -## Typical Output - -After Wrangler research: - -```markdown -## Wrangler v4.59.2 (2026-01-15) - -**Repo:** github.com/cloudflare/workers-sdk @ abc123 - -### Commands -| Task | Command | -|------|---------| -| Dev | `wrangler dev` | -| Deploy | `wrangler deploy` | -| Tail logs | `wrangler tail` | -| Types | `wrangler types` | - -### Config -| Option | Default | Notes | -|--------|---------|-------| -| `name` | required | Worker name | -| `main` | required | Entry point | -| `compatibility_date` | required | Runtime version | - -### Gotchas -- **wrangler.toml vs wrangler.jsonc**: jsonc now recommended. Source: PR #1234 -- **Auto-provisioning**: KV/R2/D1 auto-created if id omitted. Source: v4.50 release - -### Sources -- Code: packages/wrangler/src/config/config.ts:45 -- PRs: #5678, #5679 -- Posts: blog.cloudflare.com/wrangler-4 (2025-09) -``` -# Output Templates - -Expanded templates for specific tool types. Basic structure is in SKILL.md. - ---- - -## CLI Tool (Expanded) - -```markdown -## [Tool] vX.Y.Z (YYYY-MM-DD) - -**Repo:** github.com/org/repo @ abc123 - -### Commands -| Task | Command | Notes | -|------|---------|-------| -| [task] | `[cmd]` | Added in vX.Y | - -### Flags (Including Hidden) -| Flag | Description | Source | -|------|-------------|--------| -| `--flag` | [desc] | docs | -| `--hidden` | [desc] | source: file:123 | - -### Config (`[filename]`) -```toml -[section] -option = "default" # [description] -``` - -### Env Vars -| Variable | Default | Notes | -|----------|---------|-------| -| `VAR` | [default from code] | [notes] | - -### Bleeding Edge (unreleased) -| Feature | PR | Status | -|---------|-----|--------| -| [feature] | #123 | merged, not released | - -### Gotchas -- **[Issue]**: [fix]. Source: #456 - -### Patterns -```[lang] -// From: [tests/blog post] -[code] -``` - -### Sources -- Repo: [url] @ [commit] -- PRs: #123, #456 -- Posts: [url] (2025-MM) -``` - ---- - -## Library/Framework - -```markdown -## [Library] vX.Y.Z (YYYY-MM-DD) - -**Install:** `[package manager command]` - -### Core API -| Export | Purpose | Since | -|--------|---------|-------| -| `name` | [purpose] | vX.Y | - -### New in Latest Release -| API | Description | -|-----|-------------| -| `name` | [desc] | - -### Config -```[lang] -{ - option: "default", // [description] -} -``` - -### Patterns (2025-2026) -```[lang] -// Source: [blog/tests] -[code] -``` - -### Migration (from vX to vY) -- [breaking change]: [fix] - -### Gotchas -- [issue]: [solution] -``` - ---- - -## Comparison - -When researching alternatives: - -```markdown -## [Tool A] vs [Tool B] - -| Aspect | [A] | [B] | -|--------|-----|-----| -| Version | vX | vY | -| [aspect] | [A way] | [B way] | - -### Use [A] when -- [scenario] - -### Use [B] when -- [scenario] - -### Migration A → B -1. [step] -``` - ---- - -## Minimal (Quick Research) - -```markdown -## [Tool] (YYYY-MM-DD) - -**Install:** `[cmd]` -**Key:** `[most common cmd]` -**Gotcha:** [one gotcha + fix] -**New:** [one 2025-2026 feature] -**Source:** [repo@commit] -``` -# Tool-Specific Research Strategies - -Deep-dive strategies for different tool categories. - ---- - -## CLI Tools (wrangler, cargo, bun, etc.) - -### Where to Look - -``` -src/cli/ or src/cli.rs or bin/ -├── Command definitions -├── Argument parsing (clap, yargs, etc.) -├── Hidden/experimental flags -└── Default values (often different from docs) -``` - -### Key Searches - -```bash -# Rust CLI -rg "hidden\s*=\s*true" /tmp/[repo]-research --type rust -rg "#\[arg\(" /tmp/[repo]-research --type rust - -# TypeScript CLI -rg "hidden:|experimental:" /tmp/[repo]-research --type ts -rg "process\.env\." /tmp/[repo]-research --type ts - -# Go CLI -rg "Hidden:\s*true" /tmp/[repo]-research --type go -rg "os\.Getenv" /tmp/[repo]-research --type go -``` - -### Output Focus - -- Commands table with all subcommands -- Flags table (including hidden) -- Environment variables -- Config file schema -- Common patterns - ---- - -## Libraries/Frameworks (React, Next.js, etc.) - -### Where to Look - -``` -packages/[core]/src/ -├── Exported APIs (index.ts, exports.ts) -├── Internal APIs (not exported) -├── Deprecation warnings -└── Experimental/canary exports -``` - -### Key Searches - -```bash -# Find exports -rg "^export " /tmp/[repo]-research/packages/*/src/index.ts - -# Find deprecations -rg "deprecated|@deprecated" /tmp/[repo]-research - -# Find experimental -rg "experimental|unstable|canary" /tmp/[repo]-research -``` - -### Output Focus - -- API reference table -- New APIs (latest release) -- Deprecated APIs (with migration) -- Config options -- Patterns from examples/ - ---- - -## Runtimes (Bun, Deno, Node) - -### Where to Look - -``` -src/ -├── Built-in modules -├── Runtime flags -├── Environment variables -├── Compatibility layers -└── Performance options -``` - -### Key Searches - -```bash -# Runtime flags -rg "flag|--" /tmp/[repo]-research/src/cli - -# Built-in modules -rg "Bun\.|Deno\.|node:" /tmp/[repo]-research - -# Env vars -rg "process\.env|Deno\.env|Bun\.env" /tmp/[repo]-research -``` - -### Output Focus - -- CLI flags table -- Built-in APIs -- Node.js compatibility status -- Performance tuning options -- Environment variables - ---- - -## Databases/Services (D1, R2, Postgres) - -### Where to Look - -``` -src/ -├── Query syntax -├── Connection options -├── Limits and quotas -├── Error codes -└── Migration tools -``` - -### Key Searches - -```bash -# Limits -rg "limit|max|quota" /tmp/[repo]-research - -# Error codes -rg "error|Error" /tmp/[repo]-research --type ts -A 2 - -# Config -rg "config|options|settings" /tmp/[repo]-research -``` - -### Output Focus - -- Query syntax examples -- Config options table -- Limits/quotas table -- Error codes and fixes -- Migration patterns - ---- - -## Monorepo Navigation - -Many tools live in monorepos. Quick navigation: - -```bash -# Find the main package -ls /tmp/[repo]-research/packages/ - -# Find entry points -rg "\"main\":|\"bin\":" /tmp/[repo]-research/packages/*/package.json - -# Find CLI entry -rg "#!/" /tmp/[repo]-research --type ts | head -5 -``` - ---- - -## Version Detection - -```bash -# From package.json -jq '.version' /tmp/[repo]-research/package.json - -# From Cargo.toml -grep '^version' /tmp/[repo]-research/Cargo.toml - -# From git tag -git -C /tmp/[repo]-research describe --tags --abbrev=0 - -# Latest release via GitHub API -gh release view -R [org]/[repo] --json tagName -``` - ---- - -## Changelog Mining - -```bash -# Find changelog -ls /tmp/[repo]-research/CHANGELOG* /tmp/[repo]-research/HISTORY* 2>/dev/null - -# Recent entries -head -100 /tmp/[repo]-research/CHANGELOG.md - -# Search for breaking changes -rg -i "breaking|removed|deprecated" /tmp/[repo]-research/CHANGELOG.md -``` - ---- - -## Test Mining - -Tests often show real usage patterns: - -```bash -# Find test files -fd "test|spec" /tmp/[repo]-research --type f - -# Find integration tests -fd "integration|e2e" /tmp/[repo]-research --type d - -# Search tests for patterns -rg "it\(|test\(|describe\(" /tmp/[repo]-research --type ts -A 5 -``` diff --git a/skills/research/references/source-discovery-and-pattern-extraction.md b/skills/research/references/source-discovery-and-pattern-extraction.md deleted file mode 100644 index f4d1ff318..000000000 --- a/skills/research/references/source-discovery-and-pattern-extraction.md +++ /dev/null @@ -1,48 +0,0 @@ -# Source Discovery And Pattern Extraction - -Use this reference for codebase archaeology, software-tool research, codebase reports, or mining reusable implementation patterns across one or more repositories. - -## Discovery Order - -1. Read the docs entry points first. -2. Find executable entry points: commands, handlers, jobs, hooks, or exported APIs. -3. Trace data flow from input to durable side effect. -4. Identify the core types and invariants that survive across layers. -5. Compare at least one working path with one edge path. -6. Only then summarize architecture, patterns, and risks. - -## Pattern Extraction - -Record a pattern only when it has: - -- At least two concrete examples or one canonical implementation. -- A name that describes behavior, not a file location. -- Preconditions that say when the pattern applies. -- Failure modes that say when the pattern should not be reused. -- A pointer to validation evidence. - -## Software Research Output - -For external tools and libraries, write output in this order: - -1. Current stable version and release date. -2. Supported command/API surface. -3. Config files, env vars, and hidden defaults. -4. Migration hazards and known issues. -5. Recommendation for this repo, including "do not adopt" when warranted. - -## Report Shape - -```markdown -## Summary -## Entry Points -## Core Flow -## Invariants -## Reusable Patterns -## Risks -## Open Questions -``` - ---- - -**Source:** Adapted from an external skill corpus / `codebase-archaeology`, `codebase-pattern-extraction`, `codebase-report`, and `research-software`. Pattern-only, no verbatim text. diff --git a/skills/research/references/structural-graph-navigation.md b/skills/research/references/structural-graph-navigation.md deleted file mode 100644 index b1b342250..000000000 --- a/skills/research/references/structural-graph-navigation.md +++ /dev/null @@ -1,81 +0,0 @@ -# Structural graph navigation (graphify) — research Tier 1b recipe - -> Optional. Used by `/research` Tier 1b when `graphify` is installed (PyPI `graphifyy`). -> It maps **structure** — what calls/defines/connects to what, across files **and** -> languages — which `grep` cannot see. Reach for it on *what is X / where / what's -> connected / cross-file-link* questions, **before** broad grep. It does **not** read -> in-body logic; for control flow, read the file (Tier 4). - -## Why before grep - -`grep` finds string matches; it cannot tell you that a shell pre-push hook is wired to -a Go quorum engine, or trace a call chain across packages. graphify's `explain`/`path` -give that structural map directly. The honest failure mode is behavioral: agents default -to grep even when a graph exists — so Tier 1b is an explicit *before-grep* step, not a -"nice to have." - -## Refresh first — never query a stale graph - -The structural (AST) layer is free and fast — re-extracting ~2,500 code files measured -at **~13s, no LLM**. Always refresh before querying: - -```bash -graphify <repo> --update # incremental: only changed files (seconds) -# first run on a repo: graphify <repo> (full build; AST is the free part) -``` - -Only the AST/structural layer is cheap. The doc-semantic layer (LLM-extracted -doctrine↔code bridges) is expensive and slow-changing — **do not** depend on it live; -it is an occasional enrichment, not a research dependency. - -## The three commands - -```bash -graphify explain "<symbol>" # what a node IS + everything it connects to (calls/defines). - # Clean, precise, no noise. Best for "what is X and what touches it". -graphify path "<A>" "<B>" # shortest path A→B with the relation on each hop. - # Best for "how does A reach B" across files/languages. -graphify query "<tokens>" # ranked neighborhood around matched nodes (BFS; --dfs to trace). - # Locator, not an answerer. REQUIRES the expansion step below. -``` - -## REQUIRED: query vocab-expansion (or `query` returns noise) - -graphify's `query` matcher is **case-folded substring + IDF — no stemming, no synonyms, -no cross-language match**. Feeding it a raw question matches stray words (e.g. "decide", -"main") as substrings and collapses to noise. So before `graphify query`: - -1. Extract the graph's actual vocabulary, then pick **only tokens that exist in it** (≤12): - ```bash - PY=$(cat graphify-out/.graphify_python) - $PY -c " - import json,re; from pathlib import Path - d=json.loads(Path('graphify-out/graph.json').read_text()); v=set() - for n in d['nodes']: - for c in re.findall(r'[^\W\d_]+', n.get('label','') or '', re.UNICODE): - for p in re.findall(r'[A-Z]+(?=[A-Z][a-z])|[A-Z]?[a-z]+|[A-Z]+', c) or [c]: - t=p.lower() - if 3<=len(t)<=30: v.add(t) - Path('graphify-out/.vocab.txt').write_text('\n'.join(sorted(v)))" - ``` -2. Choose tokens from `graphify-out/.vocab.txt` that match the query intent. **Never invent - tokens.** Watch polysemy — a token like `merge` may pull in unrelated code; drop it if so. - If no vocab token matches, say the corpus has no relevant vocabulary and fall through. -3. Run `graphify query "<picked tokens>" [--dfs] [--budget 1500]` and read the ranked nodes. - -`explain` and `path` take a node label directly and need **no** expansion — prefer them when -you already know the symbol. - -## Bounds (state these, don't oversell) - -- **Structure, not logic.** `explain check_one_push` shows its callers/callees; the - *decide-then-fail-closed* logic still means reading the file. The graph maps terrain; it - doesn't read road signs. -- **AST only, for research.** The free structural layer is what research uses. The - doc-semantic bridges are real but expensive to refresh — out of scope for the live tier. - -## Graceful fallback - -If `graphify` is not installed (`command -v graphify` empty), skip Tier 1b entirely and -continue with the remaining tiers — exactly like Tier 1 Code-Map skips when -`docs/code-map/` is absent. graphify is never a hard dependency. diff --git a/skills/research/references/vibe-methodology.md b/skills/research/references/vibe-methodology.md deleted file mode 100644 index 629d05e5d..000000000 --- a/skills/research/references/vibe-methodology.md +++ /dev/null @@ -1,112 +0,0 @@ -# Vibe Methodology - -Core principles for AI-assisted development. "Vibe" = trust-but-verify. - ---- - -## The 40% Rule - -**Never exceed 40% context utilization.** - -- Checkpoint at 35% -- Reset via session restart or `/research` artifact -- More context ≠ better results (hallucination risk increases) - ---- - -## Three Levels of Verification - -| Level | Vibe | Method | When | -|-------|------|--------|------| -| L1 | Accept | Structural check only | Boilerplate, formatting | -| L2 | Probe | Spot-check key logic | Normal implementation | -| L3 | Audit | Line-by-line review | Security, data handling | - -**Default to L2.** Upgrade to L3 for: -- Authentication/authorization -- Financial calculations -- Data persistence -- External API calls - ---- - -## Evidence Hierarchy - -Trust in order: - -1. **Running code** - Actually execute it -2. **Tests** - Passing tests prove behavior -3. **File contents** - Read the actual source -4. **Documentation** - May be stale -5. **Model claims** - Verify everything - ---- - -## Working Patterns - -### Incremental Verification -``` -Write small piece → Test → Verify → Repeat -``` - -Don't write 500 lines then debug. Write 50, verify, continue. - -### Checkpoint Often -- After each feature complete -- Before any risky change -- At natural boundaries - -### Search Before Implement -```bash -# Always check for prior art -mcp__smart-connections-work__lookup --query="<topic>" -ls .agents/research/ | grep -i "<topic>" -``` - ---- - -## Anti-Patterns to Avoid - -| Anti-Pattern | Why Bad | Instead | -|--------------|---------|---------| -| Trust-and-paste | Hallucinations slip through | Always read generated code | -| Context stuffing | Degrades quality | Stay under 40% | -| Fix spiraling | Compounds errors | Reset and rethink | -| Skipping verification | Builds on bad foundation | Verify incrementally | - ---- - -## The Research Discipline - -1. **Scope first** - Define what you're looking for -2. **Search smart** - Use semantic search before grep -3. **Read selectively** - Don't load whole files -4. **Cite everything** - `file:line` for all claims -5. **Synthesize** - Connect findings to goal - ---- - -## Session Hygiene - -```bash -# Start -gt hook # Check assigned work -bd ready # What's available - -# Work -/research <topic> # Creates artifact, saves context -/implement <issue> # Focused execution - -# End -bd vc status # Optional Dolt status check; JSONL auto-sync is automatic -git commit # Commit changes -git push # WORK IS NOT DONE UNTIL PUSHED -``` - ---- - -## References - -- `failure-patterns.md` - 12 specific failure modes -- `context-discovery.md` - 6-tier exploration hierarchy -- `~/.claude/CLAUDE-base.md` - Full vibe methodology diff --git a/skills/research/schemas/findings.json b/skills/research/schemas/findings.json index 3231fdef3..b8a44ee02 100644 --- a/skills/research/schemas/findings.json +++ b/skills/research/schemas/findings.json @@ -26,10 +26,15 @@ "additionalProperties": false } }, - "recommendations": { + "checked": { "type": "array", "items": {"type": "string"}, - "description": "Actionable next steps" + "description": "Surfaces and claims examined" + }, + "not_checked": { + "type": "array", + "items": {"type": "string"}, + "description": "Relevant surfaces and claims left unexamined" }, "schema_version": { "type": "integer", @@ -37,6 +42,6 @@ "description": "Schema version for forward compatibility" } }, - "required": ["topic", "summary", "findings", "recommendations", "schema_version"], + "required": ["topic", "summary", "findings", "checked", "not_checked", "schema_version"], "additionalProperties": false } diff --git a/skills/research/scripts/validate.md b/skills/research/scripts/validate.md deleted file mode 100644 index 85aeb4801..000000000 --- a/skills/research/scripts/validate.md +++ /dev/null @@ -1,114 +0,0 @@ -# Validation Script for Research Skill - -## Overview - -The `validate.sh` script ensures the `/research` skill meets basic quality and completeness standards. It runs a series of checks against the skill's structure, documentation, and references. - -## Purpose - -This validation script serves as a quality gate for the research skill, ensuring: - -- Required files exist with correct structure -- Documentation includes essential patterns and concepts -- References directory contains sufficient resource materials - -## Script Location - -``` -skills/research/scripts/validate.sh -``` - -## Script Execution - -The script performs the following checks: - -### Basic Structure Validation -- **SKILL.md exists**: Verifies the primary skill documentation file -- **SKILL.md has YAML frontmatter**: Ensures proper metadata formatting -- **name: research**: Confirms correct skill identification -- **references/ directory exists**: Validates reference materials directory -- **references/ has at least 3 files**: Ensures minimum reference coverage - -### Documentation Content Validation -- **SKILL.md mentions .agents/research/ output path**: Confirms documented output location -- **SKILL.md mentions .agents/findings/registry.jsonl**: Confirms the reusable-finding registry bridge -- **SKILL.md mentions reusable findings**: Ensures transient notes are not treated as durable registry entries -- **SKILL.md mentions dedup_key**: Confirms the merge key required by the registry contract -- **SKILL.md mentions temp-file-plus-rename atomic write rule**: Confirms the registry write semantics -- **SKILL.md mentions finding-compiler.sh refresh**: Confirms the follow-up compiler pass is documented -- **SKILL.md mentions Explore agent**: Ensures agent reference is included -- **SKILL.md mentions --auto flag**: Validates feature documentation -- **SKILL.md mentions ao lookup or ao search**: Checks CLI integration documentation -- **SKILL.md mentions knowledge flywheel**: Confirms system architecture coverage -- **SKILL.md mentions backend detection**: Validates technical implementation details -- **SKILL.md mentions quality validation**: Ensures quality assurance documentation - -## Usage - -### Manual Execution - -```bash -# From the project root directory -./skills/research/scripts/validate.sh -``` - -### Expected Output - -``` -PASS: SKILL.md exists -PASS: SKILL.md has YAML frontmatter -PASS: SKILL.md has name: research -PASS: references/ directory exists -PASS: references/ has at least 3 files -PASS: SKILL.md mentions .agents/research/ output path -PASS: SKILL.md mentions .agents/findings/registry.jsonl -PASS: SKILL.md mentions reusable findings -PASS: SKILL.md mentions dedup_key -PASS: SKILL.md mentions temp-file-plus-rename atomic write rule -PASS: SKILL.md mentions finding-compiler.sh refresh -PASS: SKILL.md mentions Explore agent -PASS: SKILL.md mentions --auto flag -PASS: SKILL.md mentions ao lookup or ao search -PASS: SKILL.md mentions knowledge flywheel -PASS: SKILL.md mentions backend detection -PASS: SKILL.md mentions quality validation - -Results: 17 passed, 0 failed -``` - -## Integration with CI/CD - -This script can be integrated into continuous integration workflows to ensure the research skill meets quality standards before deployment: - -```yaml -# Example GitHub Actions workflow -- name: Validate Research Skill - run: ./skills/research/scripts/validate.sh -``` - -## Exit Codes - -- **0**: All checks passed (success) -- **1**: One or more checks failed -- **2**: Script execution error - -## Development Workflow - -### Adding New Features to Research Skill - -1. **Implement the feature** in the skill's codebase -2. **Update SKILL.md** to document the new functionality -3. **Run validation script** to ensure documentation is complete: - ```bash - ./skills/research/scripts/validate.sh - ``` -4. **Address any failures** by updating documentation or code -5. **Commit changes** with confidence the skill meets quality standards - -### Updating Validation Criteria - -To modify validation criteria: - -1. **Edit validate.sh** to add/remove checks as needed -2. **Update this documentation** to reflect new validation requirements -3. **Test the updated script** against the current skill implementation diff --git a/skills/research/scripts/validate.sh b/skills/research/scripts/validate.sh index c2c6b4c94..c7f85e985 100755 --- a/skills/research/scripts/validate.sh +++ b/skills/research/scripts/validate.sh @@ -1,27 +1,20 @@ #!/usr/bin/env bash set -euo pipefail -SKILL_DIR="$(cd "$(dirname "$0")/.." && pwd)" -PASS=0; FAIL=0 -check() { if bash -c "$2"; then echo "PASS: $1"; PASS=$((PASS + 1)); else echo "FAIL: $1"; FAIL=$((FAIL + 1)); fi; } +skill_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -check "SKILL.md exists" "[ -f '$SKILL_DIR/SKILL.md' ]" -check "SKILL.md has YAML frontmatter" "head -1 '$SKILL_DIR/SKILL.md' | grep -q '^---$'" -check "SKILL.md has name: research" "grep -q '^name: research' '$SKILL_DIR/SKILL.md'" -check "references/ directory exists" "[ -d '$SKILL_DIR/references' ]" -check "references/ has at least 3 files" "[ \$(ls '$SKILL_DIR/references/' | wc -l) -ge 3 ]" -check "SKILL.md mentions .agents/research/ output path" "grep -q '\.agents/research/' '$SKILL_DIR/SKILL.md'" -check "SKILL.md mentions .agents/findings/registry.jsonl" "grep -q '\.agents/findings/registry.jsonl' '$SKILL_DIR/SKILL.md'" -check "SKILL.md mentions reusable findings" "grep -qi 'reusable findings' '$SKILL_DIR/SKILL.md'" -check "SKILL.md mentions dedup_key" "grep -q 'dedup_key' '$SKILL_DIR/SKILL.md'" -check "SKILL.md mentions temp-file-plus-rename atomic write rule" "grep -q 'temp-file-plus-rename atomic write rule' '$SKILL_DIR/SKILL.md'" -check "SKILL.md mentions finding-compiler.sh refresh" "grep -q 'finding-compiler.sh' '$SKILL_DIR/SKILL.md'" -check "SKILL.md mentions Explore agent" "grep -qi 'explore' '$SKILL_DIR/SKILL.md'" -check "SKILL.md mentions --auto flag" "grep -q '\-\-auto' '$SKILL_DIR/SKILL.md'" -check "SKILL.md mentions ao lookup or ao search" "grep -q 'ao lookup\|ao search' '$SKILL_DIR/SKILL.md'" -check "SKILL.md mentions knowledge flywheel" "grep -qi 'knowledge' '$SKILL_DIR/SKILL.md'" -check "SKILL.md mentions backend detection" "grep -qi 'backend\|spawn' '$SKILL_DIR/SKILL.md'" -check "SKILL.md mentions quality validation" "grep -qi 'coverage\|depth\|gap' '$SKILL_DIR/SKILL.md'" +grep -q '^name: research$' "$skill_dir/SKILL.md" +grep -Fq 'Answer one bounded question with current evidence' "$skill_dir/SKILL.md" +grep -Fq 'Report unchecked scope and stop' "$skill_dir/SKILL.md" +grep -Fq 'Do not emit approval' "$skill_dir/SKILL.md" +grep -q '^Feature: Research answers one bounded question$' \ + "$skill_dir/references/research.feature" +python3 -m json.tool "$skill_dir/schemas/findings.json" >/dev/null -echo ""; echo "Results: $PASS passed, $FAIL failed" -[ $FAIL -eq 0 ] && exit 0 || exit 1 +if rg -n 'ao lookup|ao land|auto-redo|Gate 1|\.agents/rpi/next-work|finding-compiler' \ + "$skill_dir/SKILL.md" "$skill_dir/references" "$skill_dir/schemas"; then + echo 'research contract contains retired lifecycle behavior' >&2 + exit 1 +fi + +echo 'research skill contract: PASS' diff --git a/skills/reverse-engineer/references/templates/post-mortem.md.tmpl b/skills/reverse-engineer/references/templates/postmortem.md.tmpl similarity index 100% rename from skills/reverse-engineer/references/templates/post-mortem.md.tmpl rename to skills/reverse-engineer/references/templates/postmortem.md.tmpl diff --git a/skills/reverse-engineer/scripts/reverse_engineer.py b/skills/reverse-engineer/scripts/reverse_engineer.py index 5b96f47a0..e5a0df3f5 100755 --- a/skills/reverse-engineer/scripts/reverse_engineer.py +++ b/skills/reverse-engineer/scripts/reverse_engineer.py @@ -1865,14 +1865,14 @@ def main() -> int: # Run validation gate (includes secret scan over output_dir). _run([str(sec_dir / "validate-security-audit.sh"), str(output_dir), "--sbom" if args.sbom else "--no-sbom"], check=True) - # 9) Reports (vibe-style + post-mortem) + learning. + # 9) Reports (vibe-style + postmortem) + learning. council_dir = REPO_ROOT / ".agents" / "council" _ensure_dirs([council_dir]) vibe_path = council_dir / f"{_today_ymd()}-vibe-{product_slug}.md" - post_path = council_dir / f"{_today_ymd()}-post-mortem-{product_slug}.md" + post_path = council_dir / f"{_today_ymd()}-postmortem-{product_slug}.md" _render_template(TEMPLATES_DIR / "vibe-report.md.tmpl", vibe_path, {**vars, "OUTPUT_DIR": str(output_dir)}) - _render_template(TEMPLATES_DIR / "post-mortem.md.tmpl", post_path, {**vars, "OUTPUT_DIR": str(output_dir)}) + _render_template(TEMPLATES_DIR / "postmortem.md.tmpl", post_path, {**vars, "OUTPUT_DIR": str(output_dir)}) learning_path = REPO_ROOT / ".agents" / "learnings" / f"{_today_ymd()}-{product_slug}-reverse-engineer.md" if not learning_path.exists(): diff --git a/skills/rpi/SKILL.md b/skills/rpi/SKILL.md index e08f447bf..4f40314f0 100644 --- a/skills/rpi/SKILL.md +++ b/skills/rpi/SKILL.md @@ -44,6 +44,9 @@ RPI preserves the original intent and dispatches each core phase at most once. It does not own retries, budgets, queues, claims, leases, Git, delivery, release, closure, or the caller's next decision. +The pure [`scripts/run_once.py`](scripts/run_once.py) reference behavior makes +the dispatch and stop semantics executable without Git, `ao`, or a tracker. + ## Contract 1. Invoke `/plan` once with the caller's intent. Preserve its exact diff --git a/skills/rpi/scripts/run_once.py b/skills/rpi/scripts/run_once.py new file mode 100644 index 000000000..a110c08ee --- /dev/null +++ b/skills/rpi/scripts/run_once.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +"""Pure reference behavior for one RPI invocation. + +The caller supplies the three phase functions. This module dispatches each at +most once, translates missing phase output into an RPI report status, and never +chooses a retry or next action. +""" + +from __future__ import annotations + +from collections.abc import Callable, Mapping +import hashlib +import json +from typing import Any + + +def digest(value: Any) -> str: + payload = json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8") + return hashlib.sha256(payload).hexdigest() + + +def report( + status: str, + *, + plan_digest: str | None = None, + subject_digest: str | None = None, + verdict_ref: str | None = None, + verdict_digest: str | None = None, + checked: list[str] | None = None, + not_checked: list[str] | None = None, +) -> dict[str, Any]: + return { + "schema_version": "rpi-report.v1", + "status": status, + "plan_packet_digest": plan_digest, + "subject_manifest_digest": subject_digest, + "verdict_ref": verdict_ref, + "verdict_digest": verdict_digest, + "checked": checked or [], + "not_checked": not_checked or [], + } + + +def invoke_once( + intent: Any, + plan_phase: Callable[[Any], Mapping[str, Any] | None], + implement_phase: Callable[[Mapping[str, Any]], Mapping[str, Any] | None], + validate_phase: Callable[[Mapping[str, Any], Mapping[str, Any]], Mapping[str, Any]], +) -> dict[str, Any]: + """Dispatch Plan, Implement, and Validate no more than once each.""" + plan = plan_phase(intent) + if plan is None: + return report("NOT_PLANNED", not_checked=["implement", "validate"]) + plan = dict(plan) + plan_packet_digest = digest(plan) + + candidate = implement_phase(plan) + if candidate is None: + return report( + "NOT_BUILT", + plan_digest=plan_packet_digest, + checked=["plan"], + not_checked=["validate"], + ) + candidate = dict(candidate) + + validation = dict(validate_phase(plan, candidate)) + status = validation.get("verdict") + if status not in {"PASS", "FAIL", "NOT_PROVEN"}: + raise ValueError("Validate must return PASS, FAIL, or NOT_PROVEN") + subject_digest = validation.get("subject_manifest_digest") + verdict_digest = validation.get("verdict_digest") + verdict_ref = validation.get("verdict_ref") + if not all(isinstance(value, str) and value for value in (subject_digest, verdict_digest, verdict_ref)): + raise ValueError("Validate must return durable verdict and subject identities") + return report( + status, + plan_digest=plan_packet_digest, + subject_digest=subject_digest, + verdict_ref=verdict_ref, + verdict_digest=verdict_digest, + checked=list(validation.get("checked") or []), + not_checked=list(validation.get("not_checked") or []), + ) diff --git a/skills/rpi/scripts/validate.sh b/skills/rpi/scripts/validate.sh old mode 100644 new mode 100755 diff --git a/skills/rpi/tests/test_run_once.py b/skills/rpi/tests/test_run_once.py new file mode 100644 index 000000000..8edadb555 --- /dev/null +++ b/skills/rpi/tests/test_run_once.py @@ -0,0 +1,77 @@ +from __future__ import annotations + +import importlib.util +from pathlib import Path +import unittest + + +MODULE_PATH = Path(__file__).parents[1] / "scripts" / "run_once.py" +SPEC = importlib.util.spec_from_file_location("rpi_run_once", MODULE_PATH) +assert SPEC and SPEC.loader +MODULE = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(MODULE) + + +class RunOnceTests(unittest.TestCase): + def phases(self, verdict: str = "PASS"): + calls: list[str] = [] + + def plan(intent): + calls.append("plan") + return {"schema_version": "plan-packet.v1", "intent": intent} + + def implement(_plan): + calls.append("implement") + return {"schema_version": "candidate-packet.v1"} + + def validate(_plan, _candidate): + calls.append("validate") + return { + "verdict": verdict, + "subject_manifest_digest": "a" * 64, + "verdict_digest": "b" * 64, + "verdict_ref": "/tmp/verdict.json", + "checked": ["acceptance"], + "not_checked": [], + } + + return calls, plan, implement, validate + + def test_each_phase_runs_once_and_pass_reports(self): + calls, plan, implement, validate = self.phases() + result = MODULE.invoke_once("intent", plan, implement, validate) + self.assertEqual(calls, ["plan", "implement", "validate"]) + self.assertEqual(result["status"], "PASS") + self.assertNotIn("next_action", result) + + def test_fail_reports_and_stops_without_another_dispatch(self): + calls, plan, implement, validate = self.phases("FAIL") + result = MODULE.invoke_once("intent", plan, implement, validate) + self.assertEqual(calls, ["plan", "implement", "validate"]) + self.assertEqual(result["status"], "FAIL") + + def test_missing_plan_stops_before_implement(self): + calls: list[str] = [] + result = MODULE.invoke_once( + "intent", + lambda _intent: None, + lambda _plan: calls.append("implement"), + lambda _plan, _candidate: calls.append("validate"), + ) + self.assertEqual(calls, []) + self.assertEqual(result["status"], "NOT_PLANNED") + + def test_missing_candidate_stops_before_validate(self): + calls: list[str] = [] + result = MODULE.invoke_once( + "intent", + lambda _intent: {"plan": "packet"}, + lambda _plan: None, + lambda _plan, _candidate: calls.append("validate"), + ) + self.assertEqual(calls, []) + self.assertEqual(result["status"], "NOT_BUILT") + + +if __name__ == "__main__": + unittest.main() diff --git a/skills/scaffold/SKILL.md b/skills/scaffold/SKILL.md index b50c471c9..81c155ab8 100644 --- a/skills/scaffold/SKILL.md +++ b/skills/scaffold/SKILL.md @@ -1,6 +1,6 @@ --- name: scaffold -description: 'Stamp project/component/CI scaffolds — but reach for it mainly for the repo-specific domain-slice binding (generic trees a frontier model needs no skill for). Triggers: "scaffold", "create project component or boilerplate".' +description: 'Stamp a bounded project, component, or CI scaffold and verify the generated result once. Triggers: "scaffold", "create project component or boilerplate".' practices: - pragmatic-programmer - design-patterns @@ -25,108 +25,51 @@ metadata: canonical_status: canonical disposition: keep_specialist tier: execution - dependencies: - - standards + dependencies: [] output_contract: project files and directory structure --- -# Scaffold Skill +# Scaffold -> **Quick Ref:** Domain-slice manifests (the repo binding) + generic project/component/CI scaffolds. `/scaffold domain <name>` for a scoped operating-loop slice; `/scaffold <language> <name>`, `/scaffold component <type> <name>`, `/scaffold ci <platform>` for the generic modes. +Create one bounded project, component, or CI scaffold. This specialist does not +schedule RPI, create work ownership, mutate Git, or decide what happens next. -Stamp real project, component, or CI boilerplate plus its executable verification surface. +## Contract -**YOU MUST EXECUTE THIS WORKFLOW. Do not just describe it.** Generate real files, run real commands, verify real output. +1. Resolve the requested target root and declare the exact paths that may be + created or changed. +2. Refuse to overwrite an existing path without explicit caller authorization. +3. Generate idiomatic, functional files with at least one behavioral test for + generated behavior. +4. Run the target's selected build, test, and lint commands once. +5. Report the files changed and factual command results, then stop. -## Critical Constraints - -- Snapshot `git status`, resolve the target root, and declare the exact write scope before generation. **Why:** scaffold must not absorb unrelated user changes or write outside the requested boundary. -- Require explicit authorization before `--force`, overwriting, deleting, or replacing any existing path; stop on overlap with pre-existing edits. **Why:** generated convenience never outranks user-owned work. -- Use the current agent and local shell; do not start alternate runtimes or orchestration substrates unless the user explicitly requested them. **Why:** scaffolding is a bounded write operation, not automatic permission to fan out. -- Run the target's build, behavioral test, and lint contract once and report the - exact results. Git, repair, reruns, escalation, and delivery belong to the caller. +Use the current agent and local shell unless the caller explicitly requests a +different runtime. Preserve unrelated existing changes. ## Modes -| Mode | Invocation | Output | Where | -|------|-----------|--------|-------| -| **Domain-Slice** | `/scaffold domain <name>` | Domain-slice manifest for a scoped operating-loop run | **this file** (repo binding) | -| **Project** | `/scaffold <language> <name>` | Full project directory with build, test, lint | [references/generic-templates.md](references/generic-templates.md) | -| **Component** | `/scaffold component <type> <name>` | New module/package added to existing project | [references/generic-templates.md](references/generic-templates.md) | -| **CI** | `/scaffold ci <platform>` | CI/CD pipeline configuration | [references/generic-templates.md](references/generic-templates.md) | +- `/scaffold <language> <name>` creates a project. +- `/scaffold component <type> <name>` adds a component to an existing project. +- `/scaffold ci <platform>` creates the requested CI configuration. -Parse the invocation: `domain` first-positional → Domain-Slice; `component` → Component; `ci` → CI; otherwise Project. If ambiguous, ask ONE clarifying question, then proceed. +If the request does not identify a target or language, ask only for the missing +fact. The caller owns version control, revision, and delivery. -## Generic scaffolding (project / component / CI) +## Evidence -**A frontier model needs no template for standard project trees, best-practice -config, or CI YAML.** State the language, type, and name; it produces an -idiomatic tree with real files and a behavioral test, then reports the selected -build/test/lint results. The caller owns version-control actions. +Return: -The canonical tree shapes, `.editorconfig`/pre-commit/CI YAML skeletons, verification-command table, per-mode component layouts, and the error-recovery + output-summary blocks the skill historically stamped are preserved verbatim in **[references/generic-templates.md](references/generic-templates.md)** — consult it only when you want those exact shapes. For installer scripts, agent-facing tool servers, MCP surfaces, or Rust CLI storage scaffolds, apply [references/agent-facing-tool-scaffolds.md](references/agent-facing-tool-scaffolds.md) before writing files. +- the target root and actual changed paths; +- the build, test, and lint commands selected; +- each command's exit code; +- any requested check that was not run. -## Domain-Slice Mode - -When invoked as `/scaffold domain <name>`, scaffold a **domain-slice manifest** — the bounded-context declaration used to scope an operating-loop run. - -> There is **no `scaffold` subcommand on the `ao` CLI**. Domain-slice scaffolding is this skill's responsibility; the old phased-engine flags are superseded by ADR-0009. - -### Workflow - -1. **Generate the manifest.** Run the write-and-exit flag — it creates the template and returns without starting an RPI run: - - Run `/scaffold domain <name>`. - - This writes `docs/domains/<name>/manifest.yaml` from a template that already validates against `schemas/domain-slice-manifest.v1.schema.json`. An existing manifest is **not** overwritten unless `--force` is passed. - -2. **Fill in the placeholders.** Edit the generated manifest: - - `bounded_context` — one sentence: what this slice owns and explicitly does NOT own. - - `directive_ids` — stable GOALS.md directive IDs (pattern `d-<slug>`) this slice owns. - - `scenario_ids` — promoted spec scenario IDs from `spec/scenarios/` (may stay `[]` initially). - - `context_roots` — repo-relative implementation surface (at least one entry). - - `allowed_read_globs` / `denied_read_globs` — the read fence (gitignore syntax; deny wins). - - `validation_commands` — ordered build/test/lint steps. - -3. **Verify it loads.** The scaffolded manifest already passes the F3.1 schema/loader. After editing, confirm it still validates: - - Dry-run the operating-loop plan against `docs/domains/<name>/manifest.yaml` before execution. - - A dry run loads the manifest, prints the scoped phase prompts, and exits — proving the slice attaches. - -4. **Run scoped RPI.** Once the manifest is real: - - Run the operating loop with `docs/domains/<name>/manifest.yaml` as the explicit scope contract. - - Phase prompts carry the slice's boundaries; each run also writes a domain-scope audit artifact reporting any out-of-domain references visible in evidence. - -### Next commands the scaffold names - -After writing the manifest, lint executable-spec links with `ao goals scenarios --lint`, preview the scoped operating-loop plan, then execute with the manifest as the scope contract. Run them in that order. - -Error-recovery and output-summary conventions (shared with the generic modes) live in [references/generic-templates.md](references/generic-templates.md). - -## Output Specification - -**Artifact directory:** generated files stay under the declared target root; write the durable handoff to `.agents/evidence/scaffold/<run-id>/` at the invocation root. -**Filename convention:** required `receipt.json`; Domain-Slice mode additionally produces `docs/domains/<name>/manifest.yaml`; other filenames follow the selected scaffold mode. -**Serialization/schema format:** `receipt.json` contains `schema_version: 1`, -`mode`, `target_root`, `files_created`, `files_modified`, and factual -`validation` entries of `{kind,command,exit_code}`. It contains no commit, -verdict, next action, retry, or delivery state. -**Validator command:** check the required fields and confirm every reported path -is beneath `target_root`. -**Output:** return the receipt path and stop. - -## Quality Checklist - -- [ ] The generated paths equal the declared write scope and preserve pre-existing changes. -- [ ] Files contain real behavior and at least one behavioral test—no placeholder-only green. -- [ ] Build, tests, and lint are recorded with actual exit codes in `receipt.json`. -- [ ] Domain manifests validate against `schemas/domain-slice-manifest.v1.schema.json` and retain their read fence. -- [ ] The receipt contains facts only and leaves Git and continuation to the caller. +The result contains no verdict, lifecycle state, retry instruction, or next +action. ## References +- [references/generic-templates.md](references/generic-templates.md) — optional + historical shapes when the caller wants a specific template. - [references/agent-facing-tool-scaffolds.md](references/agent-facing-tool-scaffolds.md) -- [references/recommended-reading.md](references/recommended-reading.md) — forward-looking index of external skills (e.g., `mcp-server-design`) worth absorbing into scaffold when their trigger conditions arrive. Consult before designing a new scaffold mode that targets agent-facing tool surfaces. -- [references/scaffold.feature](references/scaffold.feature) — Executable spec: project/component/CI scaffolding entry points + domain-slice manifest routing (soc-qk4b) +- [references/scaffold.feature](references/scaffold.feature) diff --git a/skills/scaffold/references/generic-templates.md b/skills/scaffold/references/generic-templates.md index d49224667..de2c61a7e 100644 --- a/skills/scaffold/references/generic-templates.md +++ b/skills/scaffold/references/generic-templates.md @@ -4,12 +4,12 @@ > (bead `age-skills-audit-fable-l6ic.10`, generic-craft trim). A frontier model produces > standard project trees, best-practice config, and GitHub-Actions / GitLab-CI YAML > correctly **with no template** — so this file is a fallback reference, not the skill's -> durable value. The durable binding (the domain-slice manifest → operating-loop scope) -> stays in `SKILL.md`. Reach for this file only when you want the canonical shapes the -> skill historically stamped; otherwise ask the model directly. +> durable value. Reach for this file only when the caller wants one of the +> historical shapes the skill stamped; otherwise produce an idiomatic scaffold +> directly. The three generic modes share a four-step spine: **gather requirements → generate -structure → verify → initial commit**. Every generated file must have real, functional +structure → verify → report**. Every generated file must have real, functional content — not placeholder comments. ## Step 1: Gather Requirements diff --git a/skills/scaffold/references/recommended-reading.md b/skills/scaffold/references/recommended-reading.md deleted file mode 100644 index 2b9570245..000000000 --- a/skills/scaffold/references/recommended-reading.md +++ /dev/null @@ -1,25 +0,0 @@ -# Recommended Reading for Future Scaffolding Work - -> Forward-looking index of external skills and methodologies worth absorbing into `scaffold` (or a sibling skill) when the right trigger arrives. Not active dependencies; just breadcrumbs so high-utility sources are not lost. - -## How to use this file - -When a trigger condition listed below fires (new transport surface, new agent-API workload, new boilerplate domain), revisit the named source, evaluate whether the patterns still apply, and absorb the load-bearing ideas into the relevant SKILL.md or a new `references/*.md`. Do not bulk-import — extract only what is currently load-bearing. Append new candidates to the table below when a fresh source crosses the bar (recurring citations, repeated value, or distinct framing not already captured). - -## Candidates - -| Skill / source | Origin | Why relevant to scaffold | Trigger to absorb | -|---|---|---|---| -| `mcp-server-design` | ACFS | Agent-facing tool UX patterns: anticipating how agents misuse APIs, structured "fail helpfully" errors, "agent theory of mind" framing for tool design, and `make the wrong thing impossible` as a north star for boilerplate defaults. Useful when scaffold output is itself an agent-facing tool surface (MCP server, CLI agent). Scored 1.00 in the 2026-05-03 external-corpus utility map. | When MCP transport work begins, or when a new scaffold mode targets MCP/agent-tool servers. | - -## Entry shape - -When adding a new candidate, keep the same four fields so the table stays easy to scan: - -- **Skill / source** — name and (if external) the upstream owner. -- **Origin** — where the skill currently lives (ACFS, third-party repo, internal experiment). -- **Why relevant to scaffold** — one or two sentences naming the specific patterns scaffold could borrow. Phrase the takeaway in our own words; do not paste the source's description verbatim. -- **Trigger to absorb** — the concrete condition that should re-open this absorption candidate. Avoid vague triggers ("when relevant"); name the workload, surface, or epic. - ---- -> Forward-looking absorption-candidate index. Source skills credited inline above. diff --git a/skills/scaffold/references/scaffold.feature b/skills/scaffold/references/scaffold.feature index f0e75afa0..7d4c9a5d3 100644 --- a/skills/scaffold/references/scaffold.feature +++ b/skills/scaffold/references/scaffold.feature @@ -1,8 +1,4 @@ -# Executable spec for the /scaffold skill — project/component/CI scaffolding (BC3 Loop). -# /scaffold generates new-project structure, components, and CI pipelines from a single -# entry point, and backs domain-slice scaffolding with `/scaffold domain <name>` -# (there is no `ao scaffold` subcommand). Hexagon: supporting; consumes: a scaffold -# target (language/component/CI/domain); produces: project files + directory structure. (soc-qk4b) +# Executable spec for bounded project/component/CI scaffolding. Feature: Scaffold generates project, component, and CI structure As a developer starting new work @@ -24,6 +20,7 @@ Feature: Scaffold generates project, component, and CI structure When "/scaffold ci <platform>" runs Then it sets up the CI pipeline for that platform - Scenario: Domain-slice scaffolding routes through the skill - When domain scaffolding is requested - Then it writes the domain-slice manifest rather than calling a non-existent "ao scaffold" command + Scenario: Existing paths are preserved + Given the requested target contains an existing file + When scaffolding runs without explicit overwrite authorization + Then the existing file is not replaced diff --git a/skills/scaffold/scripts/validate.sh b/skills/scaffold/scripts/validate.sh index ec0b263fc..800f85af8 100755 --- a/skills/scaffold/scripts/validate.sh +++ b/skills/scaffold/scripts/validate.sh @@ -1,103 +1,15 @@ #!/usr/bin/env bash set -euo pipefail + SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -SKILL_MD="$SKILL_DIR/SKILL.md" -PASS=0 -FAIL=0 +SKILL="$SKILL_DIR/SKILL.md" -record() { - local label="$1" - shift - if "$@"; then - echo "PASS: $label" - PASS=$((PASS + 1)) - else - echo "FAIL: $label" - FAIL=$((FAIL + 1)) - fi -} +[[ -s "$SKILL" ]] +grep -q '^name: scaffold$' "$SKILL" +grep -q '^ effects: \[\]$' "$SKILL" +grep -q '^## Contract$' "$SKILL" +grep -q '^## Evidence$' "$SKILL" +grep -Fq 'The caller owns version control, revision, and delivery.' "$SKILL" +! grep -Eiq 'AUTO-REDO|ONE-HELPER|HELPER-ESCALATE|ao land|next_action' "$SKILL" -validate_contract() { - local skill_md="$1" - [[ "$(awk '/^---$/{n++;next} n==2 && /^## /{print;exit}' "$skill_md")" == "## Critical Constraints" ]] && - grep -Fq 'WARN|FAIL|REFUTED -> AUTO-REDO' "$skill_md" && - grep -Fq 'BREAKER -> HOLD -> ONE-HELPER' "$skill_md" && - grep -Fq 'HELPER-UNSTUCK -> AUTO-REDO' "$skill_md" && - grep -Fq 'HELPER-ESCALATE -> HUMAN' "$skill_md" && - grep -Fq 'REFUSAL-LANE|EXPLICIT-JUDGMENT|EXHAUSTED-BUDGET -> HUMAN' "$skill_md" && - grep -Fq '**Artifact directory:**' "$skill_md" && - grep -Fq '**Filename convention:**' "$skill_md" && - grep -Fq '**Serialization/schema format:**' "$skill_md" && - grep -Fq '**Validator command:**' "$skill_md" && - grep -Fq '(["build","test","lint"]-[$r.validation[].kind])|length==0' "$skill_md" && - grep -Fq '($r.verdict != "PASS" or all($r.validation[]; .exit_code == 0))' "$skill_md" && - grep -Fq '**Downstream handoff:**' "$skill_md" && - grep -Fq '## Quality Checklist' "$skill_md" -} - -record "SKILL.md exists" test -f "$SKILL_MD" -record "SKILL.md has name: scaffold" grep -q '^name: scaffold' "$SKILL_MD" -record "SKILL.md mentions boilerplate or starter" grep -qiE 'boilerplate|starter' "$SKILL_MD" -record "SKILL.md mentions component or project generation" grep -qiE 'component|project|generat' "$SKILL_MD" -record "scaffold contract is complete" validate_contract "$SKILL_MD" -record "overwrite requires explicit authorization" grep -Fq 'explicit authorization' "$SKILL_MD" -record "scaffold never pushes" grep -Fq 'Never push' "$SKILL_MD" - -pawl_fixture="$(mktemp)" -output_fixture="$(mktemp)" -receipt_fixture="$(mktemp)" -invalid_receipt_fixture="$(mktemp)" -trap 'rm -f "$pawl_fixture" "$output_fixture" "$receipt_fixture" "$invalid_receipt_fixture"' EXIT -awk '!/HELPER-UNSTUCK -> AUTO-REDO/' "$SKILL_MD" >"$pawl_fixture" -awk '!/\*\*Validator command:\*\*/' "$SKILL_MD" >"$output_fixture" - -if validate_contract "$pawl_fixture"; then - echo "FAIL: deletion fixture rejects missing pawl transition" - FAIL=$((FAIL + 1)) -else - echo "PASS: deletion fixture rejects missing pawl transition" - PASS=$((PASS + 1)) -fi - -if validate_contract "$output_fixture"; then - echo "FAIL: deletion fixture rejects incomplete output handoff" - FAIL=$((FAIL + 1)) -else - echo "PASS: deletion fixture rejects incomplete output handoff" - PASS=$((PASS + 1)) -fi - -validate_receipt() { - jq -e '. as $r | .schema_version==1 and (["domain","project","component","ci"]|index($r.mode))!=null and ($r.target_root|type=="string" and length>0) and ($r.files_created|type=="array" and all(.[]; type=="string")) and ($r.files_modified|type=="array" and all(.[]; type=="string")) and (($r.files_created|length)+($r.files_modified|length)>0) and ($r.validation|type=="array" and length>0 and all(.[]; (.kind as $kind | (["build","test","lint"]|index($kind))!=null) and (.command|type=="string" and length>0) and (.exit_code|type=="number"))) and ((["build","test","lint"]-[$r.validation[].kind])|length==0) and (($r.commit==null) or ($r.commit|type=="string")) and (["PASS","WARN","FAIL"]|index($r.verdict))!=null and ($r.verdict != "PASS" or all($r.validation[]; .exit_code == 0)) and ($r.next_action|type=="string" and length>0)' "$1" >/dev/null -} - -printf '%s\n' '{"schema_version":1,"mode":"component","target_root":"internal/example","files_created":["internal/example/example.go"],"files_modified":[],"validation":[{"kind":"build","command":"go build ./internal/example","exit_code":0},{"kind":"test","command":"go test ./internal/example","exit_code":0},{"kind":"lint","command":"go vet ./internal/example","exit_code":0}],"commit":null,"verdict":"PASS","next_action":"review generated diff"}' >"$receipt_fixture" -printf '%s\n' '{"schema_version":1,"mode":"component","target_root":"","files_created":[],"files_modified":[],"validation":[],"commit":null,"verdict":"PASS","next_action":""}' >"$invalid_receipt_fixture" -record "receipt validator accepts complete handoff" validate_receipt "$receipt_fixture" -if validate_receipt "$invalid_receipt_fixture"; then - echo "FAIL: receipt validator rejects incomplete handoff" - FAIL=$((FAIL + 1)) -else - echo "PASS: receipt validator rejects incomplete handoff" - PASS=$((PASS + 1)) -fi -printf '%s\n' '{"schema_version":1,"mode":"component","target_root":"internal/example","files_created":["internal/example/example.go"],"files_modified":[],"validation":[{"kind":"test","command":"go test ./...","exit_code":0}],"commit":null,"verdict":"PASS","next_action":"review generated diff"}' >"$invalid_receipt_fixture" -if validate_receipt "$invalid_receipt_fixture"; then - echo "FAIL: receipt validator rejects missing build/test/lint coverage" - FAIL=$((FAIL + 1)) -else - echo "PASS: receipt validator rejects missing build/test/lint coverage" - PASS=$((PASS + 1)) -fi -printf '%s\n' '{"schema_version":1,"mode":"component","target_root":"internal/example","files_created":["internal/example/example.go"],"files_modified":[],"validation":[{"kind":"build","command":"go build ./...","exit_code":0},{"kind":"test","command":"go test ./...","exit_code":1},{"kind":"lint","command":"go vet ./...","exit_code":0}],"commit":null,"verdict":"PASS","next_action":"review generated diff"}' >"$invalid_receipt_fixture" -if validate_receipt "$invalid_receipt_fixture"; then - echo "FAIL: receipt validator rejects PASS with failed validation command" - FAIL=$((FAIL + 1)) -else - echo "PASS: receipt validator rejects PASS with failed validation command" - PASS=$((PASS + 1)) -fi - -echo -echo "Results: $PASS passed, $FAIL failed" -(( FAIL == 0 )) +echo "scaffold contract: PASS" diff --git a/skills/scope/SKILL.md b/skills/scope/SKILL.md index ba2c722c3..a758c66ac 100644 --- a/skills/scope/SKILL.md +++ b/skills/scope/SKILL.md @@ -1,167 +1,81 @@ --- name: scope -description: 'Hard-block edits outside declared frozen directories and protect paths during risky changes. Triggers: "scope", "hard-block edits outside declared frozen", "scope skill".' +description: 'Review a proposed PlanPacket write scope for completeness and ambiguity. Triggers: "review write scope", "check scope boundaries", "scope this change".' practices: - ddd-bounded-context - design-by-contract -- mythical-man-month -hexagonal_role: driven-adapter -consumes: [] +hexagonal_role: supporting +consumes: +- proposed-write-scope produces: -- filesystem-gate +- scope-review context_rel: - kind: supplier-to - with: domain + with: plan skill_api_version: 1 context: window: isolated intent: mode: none sections: - exclude: - - HISTORY - - INTEL - - TASK + exclude: [HISTORY, INTEL, TASK] intel_scope: none metadata: - capabilities: [scope] + capabilities: [scope_review] effects: [] canonical_status: canonical disposition: keep_specialist tier: meta dependencies: [] -output_contract: 'stdout: scope status / lock state; stderr: blocked-edit reason from - hook' ---- -# /scope — Edit Scope Guard - -> **Purpose:** Declare which directories are in scope for the current work session. Edits outside the declared scope are hard-blocked by a PreToolUse hook. - -**YOU MUST EXECUTE THIS WORKFLOW. Do not just describe it.** - -## Critical Constraints - -- Treat `.agents/scope.lock` as a containment boundary, never as permission to edit every path it names. **Why:** scope limits authority; it does not create authority or ownership. -- Resolve each frozen directory repo-relative, reject traversal outside the repository, and verify `ao scope status --json` after every mutation. **Why:** an unverified or escaping prefix gives false confidence about the active boundary. -- Never unfreeze or widen scope merely to make a blocked edit pass; require explicit scope-expansion judgment tied to the original objective. **Why:** silently moving the boundary defeats the guard. -- Use the current agent and local shell for freeze, status, and recovery; do not start another runtime or orchestration substrate unless explicitly requested. **Why:** a path guard does not authorize fan-out. -- Report an out-of-scope target and stop that invocation. This skill does not - widen scope, repair, retry, escalate, or select an alternative path. - +output_contract: 'response: proposed write_scope.include/exclude plus gaps and ambiguities' --- -## Quick Start +# /scope — Review a proposed write scope -```bash -/scope freeze cli/cmd/ao/ # Freeze a single directory -/scope freeze cli/cmd/ao/ skills/scope/ # Freeze multiple (additive) -/scope unfreeze cli/cmd/ao/ # Remove one frozen directory -/scope unfreeze # Clear ALL frozen directories -/scope status # Show current lock state -/scope status --json # JSON output +Review the `write_scope.include` and `write_scope.exclude` fields of a proposed +PlanPacket. This skill is advisory: it does not write a lock, install a hook, +block an edit, claim paths, or change the PlanPacket. + +## Inputs + +- One active behavior and its acceptance scenarios. +- Proposed include and exclude patterns. +- Known generated companions and fixture/projection paths. +- Explicit non-goals. + +## Procedure + +1. Map each acceptance criterion to the smallest source paths that may change. +2. Add owned generated companions that must move with those sources. +3. Check whether any include/exclude patterns overlap or are too broad to prove. +4. Identify likely paths the proposal omitted. +5. Return a corrected proposal and the reasons for each change, then stop. + +The caller decides whether to adopt the proposal. Plan remains the sole author +of a PlanPacket, and Validate independently compares proven changed paths with +the accepted scope. + +## Output + +```yaml +write_scope: + include: ["bounded/source/**"] + exclude: ["bounded/source/generated-by-other-owner/**"] +generated_companions: ["bounded/generated/**"] +gaps: [] +ambiguities: [] ``` ---- +## Checks -## Behavior Contract +- Patterns are normalized repository-relative paths. +- Includes cover the behavior without granting unrelated directories. +- Excludes do not contradict required changes. +- Generated companions are explicit. +- No ownership, scheduling, Git, hook, retry, release, or delivery state is + introduced. -When `.agents/scope.lock` declares one or more `frozen_dirs`: +## Failure behavior -- Any `Edit`, `Write`, or `Bash` tool call whose target path is **outside** every frozen directory is **rejected** by `hooks/edit-scope-guard.sh` with a structured stderr reason and a non-zero exit code (Claude Code converts that into a tool-use refusal). -- Edits to paths **under** any frozen directory are allowed. -- When the lock file is missing OR `frozen_dirs` is empty, the hook short-circuits with exit 0 (no enforcement; allow everything). -- The hook fails **open** on malformed JSON or missing target-path fields — do not block when the input contract is violated. Defensive default protects against harness changes. - -The lock file is written via `cli/internal/llmwiki/scope_guard.go:SafeAtomicWrite`, so concurrent `freeze` / `unfreeze` calls converge atomically (last writer wins, never tears). - ---- - -## Subcommands - -### `/scope freeze <dir>...` - -Append one or more directories to the frozen set. Idempotent; re-freezing an already-frozen directory is a no-op. Updates `acquired_at` (ISO-8601) and `acquired_by` (session id or PID) on every write. - -### `/scope unfreeze [<dir>]` - -Without arguments, clears the entire frozen set. With one or more directory arguments, removes just those entries. Removing a directory that is not frozen is a no-op. - -### `/scope status [--json]` - -Print the current lock state. With `--json`, emit a single JSON object matching the schema in [references/lock-file-format.md](references/lock-file-format.md). Without flags, print a human-readable summary including each frozen directory, the acquisition timestamp, and the acquiring session. - -### `/scope guard` (future combo skill) - -Reserved for a follow-up skill that combines `freeze` + status + spawn-orchestration. Not implemented in this release; documented here for forward reference. - ---- - -## Lock File Format - -`.agents/scope.lock` is a single JSON object. Full schema lives in [references/lock-file-format.md](references/lock-file-format.md). Key fields: - -- `schema_version` — currently `1` -- `frozen_dirs` — list of repo-relative directory prefixes (trailing slash optional) -- `acquired_at` — ISO-8601 UTC timestamp -- `acquired_by` — string identifying the writer (session id, PID, or label) - ---- - -## Output Specification - -**Artifact directory:** `.agents/` under the current repository, or the path selected explicitly through `AO_SCOPE_LOCK`/`--lock` for isolated validation. -**Filename convention:** `scope.lock`; status emits the same state to stdout, with `--json` producing one JSON object. -**Serialization/schema format:** JSON matching [lock-file-format](references/lock-file-format.md): `schema_version: 1`, string array `frozen_dirs`, nonempty RFC-3339 `acquired_at`, and string `acquired_by`. -**Validator command:** run `ao scope status --json | jq -e '.schema_version==1 and (.frozen_dirs|type=="array" and all(.[]; type=="string" and length>0)) and (.acquired_at|type=="string" and length>0) and (.acquired_by|type=="string")'`. -**Output:** pass the lock path, normalized frozen directories, acquisition -identity/time, attempted target, and blocked-edit reason. The caller decides -what happens next. - -## Quality Checklist - -- [ ] Every frozen directory is repo-relative, normalized, and tied to the current objective. -- [ ] `ao scope status --json` round-trips after freeze/unfreeze and passes the validator. -- [ ] In-scope and out-of-scope probes demonstrate the intended boundary before risky work. -- [ ] A rejection stops the invocation and never silently widens scope. -- [ ] Unfreeze happens at explicit release/closeout, not as a workaround for a failed command. - ---- - -## Examples - -### Freezing scope before a swarm wave - -**User says:** `/scope freeze cli/cmd/ao/ cli/internal/scope/` - -**What happens:** - -1. `ao scope freeze cli/cmd/ao/ cli/internal/scope/` writes `.agents/scope.lock` via `SafeAtomicWrite`. -2. `hooks/edit-scope-guard.sh` (registered as PreToolUse on `Edit|Write|Bash`) consults the lock on every subsequent tool call. -3. A worker that tries to `Write` to `skills/foo/SKILL.md` is rejected; a worker editing `cli/cmd/ao/scope.go` proceeds. - -### Releasing scope at the end of a wave - -**User says:** `/scope unfreeze` - -**What happens:** - -1. `ao scope unfreeze` rewrites `.agents/scope.lock` with `frozen_dirs: []`. -2. The hook short-circuits to exit 0 on the next tool call. - ---- - -## Notes - -- Wave 1 hardcodes the `.agents/scope.lock` path. Wave 2 (issue I5) migrates the path through `lib/ao-paths.sh`. -- The hook's defensive parse on malformed JSON is intentional. See [references/lock-file-format.md](references/lock-file-format.md) for the rationale. -- This skill is purely session-boundary (path-scope freezing within a session). Cron-cadence orchestration lives outside AgentOps on the orchestration substrate (the reference is NTM + MCP + managed-agents), not in an AgentOps-shipped daemon. -- Path-scope freezing handles *where* edits land. For a complementary lane that gates *what* commands run (`rm -rf`, `git reset --hard`, `DROP DATABASE`, `kubectl delete`, `terraform destroy`) — including allowlist layering, one-shot override codes, and PreToolUse wiring — see [references/destructive-command-guard-patterns.md](references/destructive-command-guard-patterns.md). Wire it alongside the scope guard when a wave touches infrastructure or shared data. -- When a workflow needs human approval, hook parity, or simultaneous command review rather than only path freezing, use [references/command-approval-and-hook-guardrails.md](references/command-approval-and-hook-guardrails.md). -- When authoring new hook behavior rather than using scope's existing guard, use the hook authoring guidance in `cc-hooks`. - -## References - -- [references/lock-file-format.md](references/lock-file-format.md) -- [references/destructive-command-guard-patterns.md](references/destructive-command-guard-patterns.md) -- [references/command-approval-and-hook-guardrails.md](references/command-approval-and-hook-guardrails.md) -- [references/scope.feature](references/scope.feature) — Executable spec: declare in-scope dirs, allow in-scope edits, hard-block out-of-scope edits via PreToolUse hook, report/release scope state (soc-qk4b) +If the scope cannot be made unambiguous from the supplied acceptance, report +the missing facts and stop. The caller may revise the intent in a new action. diff --git a/skills/scope/references/command-approval-and-hook-guardrails.md b/skills/scope/references/command-approval-and-hook-guardrails.md deleted file mode 100644 index c242ab707..000000000 --- a/skills/scope/references/command-approval-and-hook-guardrails.md +++ /dev/null @@ -1,32 +0,0 @@ -# Command Approval And Hook Guardrails - -Use this reference when path-scope protection is not enough and a session needs command approval, hook parity, or high-risk operation review. - -## Guardrail Layers - -| Layer | Blocks | Evidence | -|---|---|---| -| Path scope | Edits outside declared directories | `.agents/scope.lock` and hook stderr. | -| Command risk | Destructive or irreversible commands | Approval record or explicit denial. | -| Hook parity | Runtime-specific hook behavior drift | Hook fixture and schema tests. | -| Peer approval | High-risk command execution | Reviewer identity, command, and expiry. | - -## Approval Rules - -- Approval is per command shape, not a blanket session waiver. -- Expire approvals quickly. -- Record the exact command, working directory, and reason. -- Prefer a safer equivalent command when one exists. -- Refuse approval when rollback is unclear. - -## Hook Review Checklist - -- The hook fails open only for malformed hook input, not for known risky input. -- Output uses the portable subset accepted by all supported runtimes. -- Kill switches are documented and tested. -- Regex matchers have positive and negative examples. -- The hook has a timeout and no shell injection path. - ---- - -**Source:** Adapted from an external skill corpus / `dcg`, `cc-hooks`, and `slb`. Pattern-only, no verbatim text. diff --git a/skills/scope/references/destructive-command-guard-patterns.md b/skills/scope/references/destructive-command-guard-patterns.md deleted file mode 100644 index 15f670c5c..000000000 --- a/skills/scope/references/destructive-command-guard-patterns.md +++ /dev/null @@ -1,89 +0,0 @@ -# Destructive Command Guard Patterns - -A scope guard freezes *where* edits land. A destructive-command guard adds an orthogonal lane: freezing *what* commands a worker may execute, regardless of which directory it touches. This reference distills the methodology so a future scope-pack contributor can wire one in without reinventing the contract. - -## Why a separate guard - -Scope-only enforcement leaves a gap. A worker can stay inside the frozen directory and still run something irrecoverable from there — `rm -rf .`, `git reset --hard`, `DROP DATABASE`, `kubectl delete -A`, `terraform destroy`. The directory check passes; the blast radius does not. - -The destructive-command guard sits in the same PreToolUse position as `edit-scope-guard.sh`, but its predicate is the command string rather than the target path. The two compose cleanly: - -``` -PreToolUse(Bash) → scope-path-check → destructive-command-check → allow/deny -``` - -A failure in either lane rejects the tool call. - -## Pattern catalog - -The guard ships a base catalog keyed by tool family. Treat each entry as an authoritative pattern, not a regex literal — the implementation should normalize whitespace, quoting, and `--flag=value` vs `--flag value` before matching. - -| Family | Pattern shape | Why it qualifies | -|---|---|---| -| Filesystem | `rm -rf <abs-path-not-under-/tmp>`, `rm -rf .` from outside a known build dir | Recursive deletion of non-scratch content has no general undo | -| Git history | `git reset --hard`, `git checkout -- <file>`, `git clean -fd`, `git stash drop`, `git stash clear` | Destroys uncommitted or stashed work that no other tool tracks | -| Git remote | `git push --force` (without `--force-with-lease`), `git branch -D`, `git tag -d <pushed-tag>` | Rewrites or deletes shared history | -| Database | `DROP DATABASE`, `DROP TABLE`, `TRUNCATE`, `DELETE` without a `WHERE` clause | Schema-level or unbounded data destruction | -| Container/k8s | `kubectl delete namespace`, `kubectl delete --all`, `helm uninstall`, `docker system prune -a` | Sweeps live workloads or shared caches | -| Cloud / IaC | `terraform destroy`, `aws s3 rb --force`, `gcloud projects delete` | Tears down infrastructure that humans co-own | - -Pack additional families behind opt-in flags so a CLI-only repo never loads database or k8s rules. - -## Allowlist and override flow - -Every pattern needs an escape hatch that records the override decision; otherwise operators silently disable the guard entirely. Implement three layers, evaluated highest to lowest priority: - -1. **Project allowlist** — a checked-in file (e.g. `.agents/destructive-allowlist.toml`) listing rule IDs and optional path scopes that this repo permanently accepts. Reviewable in PRs. -2. **User allowlist** — `~/.config/<guard>/allowlist.toml` for per-operator habits (cleaning a personal Docker cache, etc.). -3. **One-shot override code** — when a block fires, the guard prints a short cryptographic code bound to the exact command + working directory + a short TTL (e.g. 24 h, single use). The human, not the agent, runs `<guard> allow-once <code>` to grant the next attempt. - -The one-shot path is load-bearing. It keeps the agent honest (the code is not predictable from context) and produces an audit log entry per override. - -## Confirm thresholds - -Make the strictness configurable so the same binary can run in interactive, CI, and unattended-swarm contexts: - -```toml -[thresholds] -mode = "block" # "block" | "warn" | "log-only" -require_override_for = ["filesystem", "git-history", "database"] -auto_allow_for = ["filesystem.rm-under-build-dir"] -warn_for = ["git-remote.force-with-lease"] -``` - -Defaults: block on the high-blast-radius families, warn on near-equivalents that have a recoverable variant, log-only for purely informational rules. CI pipelines typically tighten to `mode = "block"` with a smaller allowlist; an interactive operator may relax to `warn` while pairing. - -## PreToolUse hook integration - -The integration mirrors `edit-scope-guard.sh`: - -- **Trigger:** PreToolUse on `Bash` (Claude) or `shell` / `apply_patch` (Codex). -- **Input:** harness-supplied JSON on stdin with `tool.params.command`. -- **Pipeline:** quick-reject screen → context sanitization → normalization → allowlist check → pattern match. -- **Deny output:** non-zero exit with a structured stderr reason — rule ID, family, suggested safer variant, and the one-shot override code. The harness converts that into a tool-use refusal the model can read. -- **Allow output:** exit 0, no stdout. Side-effect-free for the common case. - -Performance budget matters because the hook runs on every Bash call. Target sub-millisecond steady state, with a hard fail-open ceiling (e.g. 200 ms) so a wedged guard never stalls the swarm. - -## Failure modes the guard must handle - -- **Fail-closed on a confirmed match.** Pattern hits → reject, even if the override file is unreadable. -- **Fail-open on infrastructure error.** Missing config, malformed JSON, panic in the matcher → exit 0 with a stderr warning. Same defensive default as `edit-scope-guard.sh`. -- **Fail-open on timeout.** Anything past the latency ceiling skips the rest of the pipeline. -- **Heredoc and inline scripts.** `bash -c '...'`, `python -c '...'`, and `<<EOF` bodies must be extracted and rescanned; otherwise a one-line wrapper bypasses every rule. -- **Quoted path normalization.** `rm -rf "/var/log/"` and `rm -rf /var/log` should hit the same rule. - -## Composing with `/scope` - -Recommended wiring for a swarm wave: - -1. `/scope freeze <dirs>` to bound the edit surface. -2. Enable the destructive-command guard with the families relevant to this repo (filesystem + git-history is a sane minimum). -3. Add project-specific allowlist entries for routine safe deletions (e.g. `rm -rf ./build`, `rm -rf ./.next`). -4. Run the wave. Treat any block as a checkpoint, not an error: pick the safer variant from the rule's suggestion field, or escalate to the human for an `allow-once`. -5. After the wave, `/scope unfreeze` and let the destructive-command guard stay loaded — its overhead is negligible and the override audit log compounds. - -The two guards do not need to share state, but they should share the same fail-open posture so a hook outage never silently disables both lanes at once. - ---- -> Pattern adopted from `dcg` (ACFS skill corpus). Methodology only — no verbatim text. diff --git a/skills/scope/references/lock-file-format.md b/skills/scope/references/lock-file-format.md deleted file mode 100644 index 4d331a8fa..000000000 --- a/skills/scope/references/lock-file-format.md +++ /dev/null @@ -1,43 +0,0 @@ -# `.agents/scope.lock` — Format Reference - -The scope lock file declares which repo-relative directory prefixes are currently in scope for editing. The PreToolUse hook `hooks/edit-scope-guard.sh` consults it on every `Edit`, `Write`, and `Bash` tool call. - -## Schema (v1) - -```json -{ - "schema_version": 1, - "frozen_dirs": ["cli/cmd/ao/", "skills/scope/"], - "acquired_at": "2026-05-01T19:30:00Z", - "acquired_by": "<session-id-or-pid>" -} -``` - -| Field | Type | Required | Notes | -|---|---|---|---| -| `schema_version` | integer | yes | Currently `1`. Hook treats unknown versions as fail-open. | -| `frozen_dirs` | array of strings | yes | Repo-relative directory prefixes. Trailing slash optional but conventional. Empty array means "no enforcement". | -| `acquired_at` | string (ISO-8601) | yes | UTC, RFC 3339. Updated on every successful `freeze` / `unfreeze`. | -| `acquired_by` | string | yes | Session id, PID, or human-supplied label. Used for diagnostic messages only. | - -## Atomicity guarantee - -Writes go through `cli/internal/llmwiki/scope_guard.go:SafeAtomicWrite`, which writes to a temp file in the same directory and `rename(2)`s into place. Readers either see the previous JSON or the new JSON, never a torn document. Concurrent writers converge to last-writer-wins. - -## Hook behavior - -`hooks/edit-scope-guard.sh` reads the file with these rules: - -- **File missing or empty:** exit 0 (allow). The lock is opt-in. -- **JSON parse fails:** exit 0 (fail-open). Log warning to stderr. -- **`frozen_dirs` empty:** exit 0 (allow). -- **Target path under any `frozen_dirs[i]`:** exit 0 (allow). -- **Target path outside every `frozen_dirs[i]`:** exit 2 with structured stderr reason `edit-scope-guard: <path> outside frozen scope <frozen-dirs>`. -- **Tool input malformed (missing `tool.params.file_path` AND `tool.params.command`):** exit 0 (nothing to check). - -Path comparison uses prefix match on the repo-relative path. Trailing slashes in `frozen_dirs` entries are normalized away before comparison. - -## Forward compatibility - -- `schema_version` future bumps will be additive. The hook will continue to honor v1 fields. -- New optional fields (e.g., `expires_at`, `owner_session`) may be added without breaking the contract. diff --git a/skills/scope/references/scope.feature b/skills/scope/references/scope.feature deleted file mode 100644 index 74a4eb96b..000000000 --- a/skills/scope/references/scope.feature +++ /dev/null @@ -1,32 +0,0 @@ -# Executable spec for the /scope skill — edit-scope guardrail (BC5 Runtime). -# /scope declares which directories are in scope for the current work session and -# hard-blocks edits outside them via a PreToolUse hook, so a session cannot drift -# into files it never claimed. Hexagon: driven-adapter; consumes: a declared -# directory set; produces: scope/lock state + blocked-edit reasons on stderr. (soc-qk4b) - -Feature: Scope hard-blocks edits outside the declared directories - As an agent working a bounded change - I want edits confined to directories I declared in scope - So that a session cannot silently modify files it never claimed - - Background: - Given a work session that can declare an in-scope directory set - - Scenario: Declaring scope records the allowed directories - When /scope declares one or more directories - Then those directories become the in-scope set and the lock state reflects them - - Scenario: An edit inside scope proceeds - Given a declared in-scope directory - When a file inside it is edited - Then the edit is allowed - - Scenario: An edit outside scope is hard-blocked with a reason - Given a declared scope - When a file outside the in-scope set is edited - Then the PreToolUse hook blocks the edit and reports the blocked-edit reason on stderr - - Scenario: Scope state is reportable and releasable - When /scope is queried - Then it reports the current scope and lock state - And releasing scope removes the block on out-of-scope edits diff --git a/skills/scope/scripts/validate.sh b/skills/scope/scripts/validate.sh index b7980add6..5f282269d 100755 --- a/skills/scope/scripts/validate.sh +++ b/skills/scope/scripts/validate.sh @@ -1,90 +1,14 @@ #!/usr/bin/env bash set -euo pipefail - SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -SKILL_MD="$SKILL_DIR/SKILL.md" -PASS=0 -FAIL=0 +REPO_ROOT="$(cd "$SKILL_DIR/../.." && pwd)" -record() { - local label="$1" - shift - if "$@"; then - echo "PASS: $label" - PASS=$((PASS + 1)) - else - echo "FAIL: $label" - FAIL=$((FAIL + 1)) - fi -} +bash "$REPO_ROOT/skills/heal-skill/scripts/heal.sh" --check --strict "$SKILL_DIR" -validate_contract() { - local skill_md="$1" - [[ "$(awk '/^---$/{n++;next} n==2 && /^## /{print;exit}' "$skill_md")" == "## Critical Constraints" ]] && - grep -Fq 'WARN|FAIL|REFUTED -> AUTO-REDO' "$skill_md" && - grep -Fq 'BREAKER -> HOLD -> ONE-HELPER' "$skill_md" && - grep -Fq 'HELPER-UNSTUCK -> AUTO-REDO' "$skill_md" && - grep -Fq 'HELPER-ESCALATE -> HUMAN' "$skill_md" && - grep -Fq 'REFUSAL-LANE|EXPLICIT-JUDGMENT|EXHAUSTED-BUDGET -> HUMAN' "$skill_md" && - grep -Fq '**Artifact directory:**' "$skill_md" && - grep -Fq '**Filename convention:**' "$skill_md" && - grep -Fq '**Serialization/schema format:**' "$skill_md" && - grep -Fq '**Validator command:**' "$skill_md" && - grep -Fq '**Downstream handoff:**' "$skill_md" && - grep -Fq '## Quality Checklist' "$skill_md" -} - -validate_status() { - jq -e '.schema_version==1 and (.frozen_dirs|type=="array" and all(.[]; type=="string" and length>0)) and (.acquired_at|type=="string" and length>0) and (.acquired_by|type=="string")' >/dev/null -} - -roundtrip_status() { - AO_SCOPE_LOCK="$1" "$2" scope status --json | validate_status -} - -roundtrip_clear() { - AO_SCOPE_LOCK="$1" "$2" scope status --json | jq -e '.schema_version==1 and .frozen_dirs==[]' >/dev/null -} - -record "scope contract is complete" validate_contract "$SKILL_MD" -record "scope expansion requires explicit judgment" grep -Fq 'explicit scope-expansion judgment' "$SKILL_MD" -record "scope uses current agent and local shell" grep -Fq 'current agent and local shell' "$SKILL_MD" - -pawl_fixture="$(mktemp)" -output_fixture="$(mktemp)" -lock_fixture="$(mktemp)" -rm -f "$lock_fixture" -trap 'rm -f "$pawl_fixture" "$output_fixture" "$lock_fixture"' EXIT -awk '!/HELPER-UNSTUCK -> AUTO-REDO/' "$SKILL_MD" >"$pawl_fixture" -awk '!/\*\*Validator command:\*\*/' "$SKILL_MD" >"$output_fixture" - -if validate_contract "$pawl_fixture"; then - echo "FAIL: deletion fixture rejects missing pawl transition" - FAIL=$((FAIL + 1)) -else - echo "PASS: deletion fixture rejects missing pawl transition" - PASS=$((PASS + 1)) +if rg -n 'scope\.lock|AO_SCOPE_LOCK|PreToolUse|ao scope|freeze|unfreeze|git (commit|push)|ao land' \ + "$SKILL_DIR/SKILL.md"; then + echo "scope validate: mutable scope authority remains" >&2 + exit 1 fi -if validate_contract "$output_fixture"; then - echo "FAIL: deletion fixture rejects incomplete output handoff" - FAIL=$((FAIL + 1)) -else - echo "PASS: deletion fixture rejects incomplete output handoff" - PASS=$((PASS + 1)) -fi - -AO_BIN="${AO_BIN:-$(command -v ao || true)}" -if [[ -z "$AO_BIN" ]]; then - echo "FAIL: ao binary is available for scope round-trip" - FAIL=$((FAIL + 1)) -else - AO_SCOPE_LOCK="$lock_fixture" "$AO_BIN" scope freeze skills/scope >/dev/null - record "ao scope freeze/status round-trip is valid" roundtrip_status "$lock_fixture" "$AO_BIN" - AO_SCOPE_LOCK="$lock_fixture" "$AO_BIN" scope unfreeze >/dev/null - record "ao scope unfreeze round-trip clears scope" roundtrip_clear "$lock_fixture" "$AO_BIN" -fi - -echo -echo "Results: $PASS passed, $FAIL failed" -(( FAIL == 0 )) +echo "scope validate: PASS" diff --git a/skills/security/SKILL.md b/skills/security/SKILL.md index eaf7c758e..3b1bfc4e7 100644 --- a/skills/security/SKILL.md +++ b/skills/security/SKILL.md @@ -1,6 +1,6 @@ --- name: security -description: 'Run repository security scans for vulnerabilities, dependency risk, secrets, and release gates. Triggers: "security", "run repository security scans for", "security skill".' +description: 'Run authorized repository security scans for vulnerabilities, dependency risk, secrets, and binary policy. Triggers: "security", "run repository security scans for", "security skill".' practices: - supply-chain-integrity - design-by-contract @@ -34,9 +34,9 @@ output_contract: 'stdout: security scan report' --- # Security Skill -> **Purpose:** Run repeatable security checks across code, scripts, release gates, authorized binaries, and repo-managed prompt surfaces. +> **Purpose:** Run repeatable security checks across code, scripts, authorized binaries, and repo-managed prompt surfaces. -Use this skill for deterministic pre-merge/release validation, scheduled checks, authorized binary assurance, dependency risk, secrets, or offline prompt-surface redteam. +Use this skill for a caller-requested repository scan, authorized binary assurance, dependency risk, secrets, or offline prompt-surface redteam. ## Critical Constraints @@ -69,7 +69,7 @@ scripts/security-gate.sh --mode quick **Checkpoint:** preserve the exit code and verify the reported `security-gate-summary.json` exists and parses before triage. -### 2) Full or release gate +### 2) Full scan Run: @@ -77,18 +77,18 @@ Run: scripts/security-gate.sh --mode full ``` -Add `--require-tools` when skipped scanners would invalidate the assurance claim. **Checkpoint:** do not authorize promotion until the full artifact passes the output validator and the process exits zero. +Add `--require-tools` when skipped scanners would invalidate the assurance claim. **Checkpoint:** report the result as incomplete unless the selected artifact validator and process both succeed. ### 3) Scheduled gate Scheduled automation runs the full gate against the intended branch and retains its artifact directory. A failing scheduled run creates actionable tracked work; AgentOps itself does not supply the scheduler. -### 4) Triage and re-run +### 4) Triage 1. Open the latest artifact and identify scanner, severity, file, and coverage gaps. 2. Reproduce the finding with the narrowest safe command. -3. For authorized remediation, fix critical/high findings; otherwise report them with owner and next action. -4. Re-run the same gate. Do not downgrade, suppress, or update a baseline merely to pass. +3. Rank concrete findings and preserve coverage gaps. +4. Stop. Remediation, risk acceptance, and any later scan are new caller decisions. Do not downgrade, suppress, or update a baseline merely to pass. ## Output Specification @@ -108,9 +108,9 @@ an owner, next action, approval, release, or retry decision. - [ ] Target and authorization boundary are explicit; collection stayed within them. - [ ] Scanner availability and skipped/error coverage are visible in the report. -- [ ] Findings include severity, location, reproducible evidence, and remediation/owner. +- [ ] Findings include severity, location, reproducible evidence, and bounded remediation guidance. - [ ] Artifacts contain no newly exposed secrets or unredacted sensitive payloads. -- [ ] Required gate and output validator both pass before promotion is declared safe. +- [ ] The report distinguishes a passing scan from permission to promote or release. - [ ] Suppressions, policy changes, baselines, and risk acceptance require explicit judgment. - [ ] The report stops after evidence and contains no continuation decision. @@ -128,7 +128,7 @@ For a bounded suite smoke test, use an owned binary and a temporary output direc ## Examples - `/security` — run the quick repository gate, validate its summary, and report coverage/findings. -- `/security --release` — run the full gate, preserve artifacts, and block promotion until the verdict is green. +- `/security --full` — run the full scan once, preserve artifacts, and report coverage and findings. - `/security run --binary "$(command -v ao)" --out-dir .tmp/security-suite/ao-current` — capture an authorized binary baseline via the composable suite. - `/security collect-redteam --repo-root .` — run the offline attack pack over repo-owned control surfaces. diff --git a/skills/security/references/security-suite-runbook.md b/skills/security/references/security-suite-runbook.md index 684e79fc6..529d65bfc 100644 --- a/skills/security/references/security-suite-runbook.md +++ b/skills/security/references/security-suite-runbook.md @@ -1,6 +1,6 @@ # Composable Security Suite Runbook -Use this reference for authorized binary assurance, baseline comparison, policy enforcement, and offline repo-surface redteam. The main `security` skill owns authorization, pawl recovery, and release decisions. +Use this reference for authorized binary assurance, baseline comparison, policy enforcement, and offline repo-surface redteam. The caller supplies authorization and owns every decision after the report. ## Primitive model diff --git a/skills/security/scripts/validate.sh b/skills/security/scripts/validate.sh index c4cb57956..3c42d6519 100755 --- a/skills/security/scripts/validate.sh +++ b/skills/security/scripts/validate.sh @@ -1,61 +1,22 @@ #!/usr/bin/env bash set -euo pipefail -SKILL_DIR="$(cd "$(dirname "$0")/.." && pwd)" -PASS=0; FAIL=0 -check() { if bash -c "$2"; then echo "PASS: $1"; PASS=$((PASS + 1)); else echo "FAIL: $1"; FAIL=$((FAIL + 1)); fi; } -validate_contract() { - local skill_md="$1" - [[ "$(awk '/^---$/{n++;next} n==2 && /^## /{print;exit}' "$skill_md")" == "## Critical Constraints" ]] && - grep -Fq 'WARN|FAIL|REFUTED -> AUTO-REDO' "$skill_md" && - grep -Fq 'BREAKER -> HOLD -> ONE-HELPER' "$skill_md" && - grep -Fq 'HELPER-UNSTUCK -> AUTO-REDO' "$skill_md" && - grep -Fq 'HELPER-ESCALATE -> HUMAN' "$skill_md" && - grep -Fq 'REFUSAL-LANE|EXPLICIT-JUDGMENT|EXHAUSTED-BUDGET -> HUMAN' "$skill_md" && - grep -Fq '**Artifact directory:**' "$skill_md" && - grep -Fq '**Filename convention:**' "$skill_md" && - grep -Fq '**Serialization/schema format:**' "$skill_md" && - grep -Fq '**Validator command:**' "$skill_md" && - grep -Fq '**Downstream handoff:**' "$skill_md" && - grep -Fq '## Quality Checklist' "$skill_md" && - grep -Fq '**Checkpoint:**' "$skill_md" -} -export -f validate_contract +SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +SKILL="$SKILL_DIR/SKILL.md" -check "SKILL.md exists" "[ -f '$SKILL_DIR/SKILL.md' ]" -check "SKILL.md has YAML frontmatter" "head -1 '$SKILL_DIR/SKILL.md' | grep -q '^---$'" -check "name is security" "grep -q '^name: security' '$SKILL_DIR/SKILL.md'" -check "references policy exists" "[ -f '$SKILL_DIR/references/policy-example.json' ]" -check "references redteam pack exists" "[ -f '$SKILL_DIR/references/agentops-redteam-pack.json' ]" -check "security_suite.py exists" "[ -x '$SKILL_DIR/scripts/security_suite.py' ]" -check "security_suite.py compiles" "python3 -m py_compile '$SKILL_DIR/scripts/security_suite.py'" -check "prompt_redteam.py exists" "[ -x '$SKILL_DIR/scripts/prompt_redteam.py' ]" -check "prompt_redteam.py compiles" "python3 -m py_compile '$SKILL_DIR/scripts/prompt_redteam.py'" -check "policy JSON valid" "python3 -c \"import json, pathlib; json.loads(pathlib.Path('$SKILL_DIR/references/policy-example.json').read_text()); print('ok')\"" -check "redteam pack JSON valid" "python3 -c \"import json, pathlib; json.loads(pathlib.Path('$SKILL_DIR/references/agentops-redteam-pack.json').read_text()); print('ok')\"" -check "suite runbook exists" "[ -s '$SKILL_DIR/references/security-suite-runbook.md' ]" -check "security contract is complete" "validate_contract '$SKILL_DIR/SKILL.md'" +[[ -s "$SKILL" ]] +grep -q '^name: security$' "$SKILL" +grep -q '^ effects: \[\]$' "$SKILL" +[[ "$(awk '/^---$/{n++;next} n==2 && /^## /{print;exit}' "$SKILL")" == "## Critical Constraints" ]] +grep -Fq '**Artifact directory:**' "$SKILL" +grep -Fq '**Validator command:**' "$SKILL" +grep -Fq 'report stops after evidence' "$SKILL" +! grep -Eiq 'AUTO-REDO|ONE-HELPER|HELPER-ESCALATE|ao (pawl|land)|next_action' "$SKILL" -pawl_fixture="$(mktemp)" -output_fixture="$(mktemp)" -trap 'rm -f "$pawl_fixture" "$output_fixture"' EXIT -sed 's/HELPER-UNSTUCK -> AUTO-REDO/HELPER-UNSTUCK -> MANUAL/' "$SKILL_DIR/SKILL.md" >"$pawl_fixture" -awk '!/\*\*Validator command:\*\*/' "$SKILL_DIR/SKILL.md" >"$output_fixture" -if validate_contract "$pawl_fixture"; then - echo "FAIL: deletion fixture rejects missing pawl transition" - FAIL=$((FAIL + 1)) -else - echo "PASS: deletion fixture rejects missing pawl transition" - PASS=$((PASS + 1)) -fi -if validate_contract "$output_fixture"; then - echo "FAIL: deletion fixture rejects incomplete output handoff" - FAIL=$((FAIL + 1)) -else - echo "PASS: deletion fixture rejects incomplete output handoff" - PASS=$((PASS + 1)) -fi +[[ -s "$SKILL_DIR/references/policy-example.json" ]] +[[ -s "$SKILL_DIR/references/agentops-redteam-pack.json" ]] +[[ -s "$SKILL_DIR/references/security-suite-runbook.md" ]] +python3 -m py_compile "$SKILL_DIR/scripts/security_suite.py" "$SKILL_DIR/scripts/prompt_redteam.py" +python3 -c 'import json, pathlib, sys; root=pathlib.Path(sys.argv[1]); [json.loads((root/name).read_text()) for name in ("policy-example.json", "agentops-redteam-pack.json")]' "$SKILL_DIR/references" -echo "" -echo "Results: $PASS passed, $FAIL failed" -[ $FAIL -eq 0 ] && exit 0 || exit 1 +echo "security contract: PASS" diff --git a/skills/shared/references/runtime-boundary.md b/skills/shared/references/runtime-boundary.md deleted file mode 100644 index 600fdb33e..000000000 --- a/skills/shared/references/runtime-boundary.md +++ /dev/null @@ -1,6 +0,0 @@ -# Runtime boundary - -Runtime adapters may start or observe explicitly requested sessions. Their -provider-specific attempts, reconnects, queues, and completion states remain -adapter evidence and cannot alter core Plan, Candidate, RPI, or verdict state. -Only Validate writes `verdict.v2`. diff --git a/skills/skill-builder/SKILL.md b/skills/skill-builder/SKILL.md index 0e4f066a3..0022e9487 100644 --- a/skills/skill-builder/SKILL.md +++ b/skills/skill-builder/SKILL.md @@ -1,17 +1,13 @@ --- name: skill-builder -description: 'Scaffold or absorb new SKILL.md files against the unified AgentOps template. Triggers: "create a skill", "scaffold skill", "absorb external skill", "new skill".' +description: 'Create a metadata-complete AgentOps skill source package and regenerate its derived projections. Triggers: "create a skill", "scaffold skill", "absorb external skill", "new skill".' practices: -- code-complete - pragmatic-programmer -- design-patterns hexagonal_role: supporting consumes: [] produces: -- converted-skill +- skill-source-package context_rel: -- kind: customer-of - with: automation-shape-routing - kind: supplier-to with: heal-skill skill_api_version: 1 @@ -25,205 +21,87 @@ context: intel_scope: topic metadata: capabilities: [skill_builder] - effects: [] + effects: [writes_skill_source, regenerates_skill_projections] canonical_status: canonical disposition: keep_specialist tier: meta - dependencies: - - heal-skill - - converter + dependencies: [] stability: experimental output_contract: skills/skill-builder/schemas/build-report.json --- -# /skill-builder — Scaffold or absorb a new SKILL.md +# /skill-builder — Create one skill source package -Materializes a new skill against the unified template at `references/skill-template.md` (extracted from anthropics/financial-services). Runs the heal-skill deep audit on the new skill as a self-check before declaring success. +Create one `skills/<slug>/` package, verify its structure, regenerate the +metadata-owned projections, and stop. The builder does not schedule work, +allocate writers, operate Git, promote learnings, or decide whether the new +skill should be invoked. -> **If unsure whether the work should be a skill, a Workflow, or an NTM swarm, run `/automation-shape-routing` first** — it is the front door that decides the shape and hands off to the right builder. +Before creating a new root, search `skills/*/SKILL.md` for an existing owner. +Extend an existing skill when it already owns the requested behavior. -> **Before creating, search for an existing owner.** `ms search` the proposed skill's trigger phrases (fast path when available — `command -v ms`, or the `mcp__ms__search` tool is attached; else grep `skills/**/SKILL.md` + `docs/SKILLS.md`). A strong hit means **extend that skill, don't create a new one** — this is one item of the fuller skill-admission checklist (bead age-7d3r). +## Inputs -## ⚠️ Critical Constraints +Choose exactly one mode: -- **Template is canonical.** All four modes produce SKILL.md files conforming to `references/skill-template.md`. Do not invent ad-hoc structures. **Why:** the heal-skill deep audit validates against this template; drift creates audit false-fails. -- **Self-audit is mandatory.** After every successful build, the build script invokes the heal-skill deep audit (`audit.sh` in `skills/heal-skill/`) against the new skill directory. A FAIL verdict aborts the build. **Why:** PR-002 (external validation gate) — the builder must not declare its own work complete. -- **Codex parity is day-1, not later.** `from-scratch`, `from-template`, and `absorb-external` modes must produce both `skills/<name>/SKILL.md` AND `skills-codex/<name>/SKILL.md` + `skills-codex/<name>/prompt.md`. **Why:** finding `2026-05-03-codex-skill-shape-is-dual-file` — codex SKILL.md uses slim frontmatter (no `skill_api_version`); prompt.md is mandatory; `audit-codex-parity.sh` is a content scanner that won't catch frontmatter drift. -- **Editing an EXISTING skill also needs a manual twin mirror.** When you change `skills/<name>/references/*.md` or `SKILL.md`, manually mirror the content into `skills-codex/<name>/` (runtime-native), THEN run `scripts/regen-codex-hashes.sh --only <name>`. `make regen-all` only refreshes the twin's *hash record*, not its prose — a green `✓ codex hashes` over a stale twin looks handled but isn't. Verify with a content diff (`grep -c <new-token>` on both copies), not the hash exit code. **Why:** finding `2026-06-16-codex-twin-content-not-auto-mirrored` (age-aqu/age-yxl) — regen made the marker self-consistent with a stale twin (0-vs-2 token divergence) and nothing complained. The parity gate now blocks an un-mirrored `references/**` edit, but the mirror is still a manual step. -- **250-line ceiling on new SKILL.md.** Use `references/` for overflow. **Why:** finding `f-2026-05-01-025` — every Skill() invocation reloads 5-15KB; multi-lifecycle sessions compound to 150-200KB+ pure scaffolding. -- **Clean-room factory inputs only.** When using lessons learned from external corpora, read [references/agentops-skill-factory.md](references/agentops-skill-factory.md) and use only AgentOps-owned summaries, scripts, and rubrics. **Why:** productization must improve structure without copying protected third-party skill content. -- **Real gate means exit code.** Validate with `heal-skill --check --strict <skill-dir>` and the heal-skill deep audit (`audit.sh`); never infer green from grep/regex output. **Why:** regex presence checks created false-greens during the 2026-06 scale build. -- **One skill directory = one writer.** Bulk builds fan out only when each worker owns a distinct new `skills/<name>/` plus `skills-codex/<name>/`; edits to existing skill dirs run in a later serial wave. **Why:** concurrent writers deleted untracked work and flipped HEAD mid-task. -- **Trust repository state, not subagent reports.** Before declaring success, inspect `git status`, generated hashes, final files, and gate exit codes. **Why:** sandbox-overlay and stale self-reports can claim work that never persisted. -- **Clean-room includes names.** Do not reuse exact third-party skill names; mint AgentOps-owned names before source skills, Codex mirrors, or wrappers are keyed. **Why:** provenance/IP safety applies to labels as well as prose and scripts. -- **Do not use the Workflow tool as the skill factory.** For scale authoring, use deterministic wave scripts or NTM/Agent Mail lanes with one worker per skill. **Why:** skill creation needs file ownership and durable git evidence, not opaque background self-reporting. +- `from-scratch <slug>` creates a blank source package. +- `from-template <slug> --like <existing-slug>` uses the existing skill only + for metadata defaults; it does not copy its prose. +- `absorb-external <slug> --from <path>` verifies the source exists, then + creates a clean-room blank package without copying names, prose, prompts, + scripts, or examples. -## Modes +The caller may set `SKILL_TIER`, `SKILL_DEPENDENCIES`, +`SKILL_CAPABILITIES`, and `SKILL_EFFECTS`. Values that represent lists must be +JSON arrays. -| Mode | Status | Description | -|------|--------|-------------| -| `from-scratch` | stable | Interactive scaffold from canonical template. Produces full skill skeleton + scripts/validate.sh + codex parity. | -| `from-template` | stable | `--like <existing-skill>` copies structure from a sibling skill, swaps domain-specific sections. | -| `absorb-external` | stable | Observes external package shape, synthesizes a clean-room AgentOps skill, and invokes `/converter` for Codex parity. | -| `from-pattern` | **alpha (passthrough)** | Delegates to `ao flywheel close-loop`. Outputs land at `.agents/knowledge/promoted/` per flywheel rules — they are NOT yet shaped as SKILL.md drafts. v2 will add skill-specific synthesis. Use `from-scratch` or `absorb-external` for SKILL.md output today. | +## Procedure -## Workflow +1. Run `scripts/build.sh` with one mode and one new slug. +2. Fill the generated placeholders with the skill's actual behavior. +3. Run `skills/heal-skill/scripts/heal.sh --check --strict skills/<slug>`. +4. Run `scripts/generate-skill-mesh.py` to derive the catalog, registry, + router, graph, maps, counts, and runtime image manifests from `SKILL.md` + metadata. +5. Run `scripts/codex-sync.sh --only <slug>` and + `scripts/regen-codex-hashes.sh --only <slug>` to derive the Codex twin. +6. Inspect the generated diff. Validation and delivery remain caller-owned. -### Phase 1: Mode dispatch +`build.sh` performs steps 1, 3, 4, and 5 once. It never retries or chooses a +next action. -`scripts/build.sh` reads `$1` and routes: +## Output -```bash -build.sh from-scratch <new-skill-name> # → init.sh --interactive -build.sh from-template <new-skill-name> --like council -build.sh absorb-external <new-skill-name> --from /path/to/SKILL.md -build.sh from-pattern # → ao flywheel close-loop +The source package contains: + +```text +skills/<slug>/ +├── SKILL.md +└── scripts/validate.sh ``` -**Checkpoint:** Confirm with user the new skill's `metadata.tier` and `metadata.dependencies` before generation. +The build report is `.agents/audits/<slug>-build.json` and conforms to +`schemas/build-report.json`. Generated inventories and runtime projections are +not additional sources of truth. -### Phase 2: Materialize from template +## Checks -`scripts/init.sh` reads `references/skill-template.md` (the canonical template section) and renders a SKILL.md skeleton with frontmatter pre-filled. For `from-template`, structure is copied from the source skill; section bodies are blanked and replaced with template stubs. +- The slug and frontmatter `name` match. +- Metadata declares `tier`, `dependencies`, `capabilities`, `effects`, + `canonical_status`, and `disposition`. +- Every hard dependency names a live skill. +- The generated package contains no Git, tracker, queue, retry, release, or + delivery behavior. +- External material is treated only as a signal that a clean-room skill may be + useful; its content is not copied. -For `absorb-external`, inspect only metadata and package shape, then synthesize -new AgentOps-owned content. Never copy external prose, prompts, scripts, -examples, names, or sentinel text into either generated runtime treatment. +## Failure behavior -**Checkpoint:** `heal-skill --check --strict skills/<new-name>` exits 0. - -### Phase 3: Codex parity - -`scripts/init.sh` invokes `/converter skills/<new-name> codex` to produce `skills-codex/<new-name>/{SKILL.md,prompt.md}`. Then trims `skill_api_version` from the codex SKILL.md (converter may preserve it). Asserts `prompt.md` exists. - -**Checkpoint:** `bash scripts/audit-codex-parity.sh` returns clean AND `grep -q "^skill_api_version:" skills-codex/<name>/SKILL.md` returns nothing. - -### Phase 4: Self-audit - -The build script tail invokes the heal-skill deep audit on `skills/<new-name>`. WARN is acceptable for v1 skills (e.g., `experimental` stability). FAIL aborts. - -**Checkpoint:** `audit_pass=true` in build report. - -### Phase 5: Factory score overlay - -For AgentOps skill upgrades, use the productization score as a patch selector, -not as a replacement for the deep audit: - -```bash -python3 skills/heal-skill/scripts/score_agentops_skill.py skills/<name> --markdown -``` - -Choose the smallest patch that improves the score while preserving the -canonical template and Codex parity constraints. - -### Phase 6: Scale factory discipline - -For more than one skill, run in ownership waves: - -1. Create-only wave: one worker per new skill directory. -2. Mutate wave: existing skill directories only after source creation settles. -3. Mirror/package wave: Codex mirrors and generated hashes after the canonical - source corpus is complete. - -Every wave ends with `git status`, `scripts/regen-all.sh --check`, and the -relevant target gates by exit code. If ownership overlaps, stop and rescope. - -## Output Specification - -**Artifact directory:** `.agents/audits/`. -**Filename convention:** `<skill>-build.json`. -**Serialization/schema format:** JSON matching `schemas/build-report.json`. -**Validator command:** `jq -e . .agents/audits/<skill>-build.json`. -**Downstream handoff:** consumed by heal-skill and the validation wave. - -**Files created (from-scratch mode):** - -``` -skills/<name>/ -├── SKILL.md (≤250 lines, full template spine) -├── scripts/ -│ └── validate.sh (self-validation per AgentOps convention) -└── references/ (externalize before the profile's 250-line kernel limit) -skills-codex/<name>/ -├── SKILL.md (slim frontmatter — no skill_api_version) -└── prompt.md (~10-20 line Execution Profile) -``` - -## Quality Rubric - -- [ ] All four modes produce skills that pass the heal-skill deep audit with PASS or WARN (not FAIL) -- [ ] `heal-skill --check --strict` exits 0 for every generated source and Codex skill directory -- [ ] Codex parity files exist and pass slim-frontmatter check -- [ ] Batch authoring has one writer per skill directory and validates persisted git state -- [ ] Clean-room review covers exact names as well as prose, scripts, and examples -- [ ] No SKILL.md exceeds 250 lines (overflow goes to `references/`) -- [ ] Build report JSON validates against `schemas/build-report.json` -- [ ] `from-pattern` mode prominently marked alpha/passthrough in user output - -## Examples - -**Create a new skill from scratch:** - -```bash -/skill-builder from-scratch hello-world -# → interactive prompt: tier? deps? primary deliverable? -# → writes skills/hello-world/SKILL.md + skills-codex/hello-world/{SKILL.md,prompt.md} -# → runs the heal-skill deep audit on the new skill -``` - -**Clone structure from an existing skill:** - -```bash -/skill-builder from-template my-new-skill --like council -# → mirrors council's section spine; substitutes new metadata -``` - -**Absorb a skill from anthropics/financial-services:** - -```bash -/skill-builder absorb-external dcf-helper \ - --from ~/dev/financial-services/plugins/vertical-plugins/financial-analysis/skills/dcf-model/SKILL.md -# → observes package shape and emits only clean-room AgentOps-owned content -``` - -## Troubleshooting - -| Problem | Cause | Solution | -|---------|-------|----------| -| Self-audit FAIL | Generated SKILL.md missing required Pass-2 check | Re-run with `--verbose`; inspect which check failed; usually `output-spec-explicit` or `trigger-clarity` | -| Codex parity drift | `/converter` preserved `skill_api_version` | `init.sh` runs `sed -i '/^skill_api_version:/d' skills-codex/<name>/SKILL.md`; verify with grep | -| SKILL.md > 250 lines | Mode generated too much inline content | Move section bodies to `references/<topic>.md`; reference inline as `[text](references/<topic>.md)` | -| `from-pattern` produces no SKILL.md | Expected behavior — passthrough only in v1 | Use `from-scratch` or `absorb-external` if you need a SKILL.md draft | - -## Corpus authoring health - -Skill selection is pure LLM reasoning over the `description` field, so a missing -trigger phrase is a skill that silently never fires. The per-skill auditor checks -this only as a WARN, so the gap accumulates. Audit the whole corpus at once: - -```bash -python3 skills/skill-builder/scripts/scan_descriptions.py skills # remediation report -python3 skills/skill-builder/scripts/scan_descriptions.py skills --strict # exit 1 on any miss -``` - -The scanner mirrors the deep audit's three-form trigger detection and adds a -suggested `Triggers:` stub per offender. See -[references/skill-authoring-standard.md](references/skill-authoring-standard.md) -for the full authoring doctrine and the best-practice-to-enforcement crosswalk. - -## See Also - -- [heal-skill](../heal-skill/SKILL.md) — structural hygiene (heal.sh) + companion deep-audit gate (audit.sh), invoked by build self-check -- [converter](../converter/SKILL.md) — produces codex parity artifacts -- [scaffold](../scaffold/SKILL.md) — scaffolds projects/components/CI (NOT skills) -- [curate](../postmortem/SKILL.md) — `--mode=forge` mines transcripts into learnings (different layer) +Any invalid input, structural failure, projection failure, or Codex sync +failure exits nonzero after one attempt. The caller decides whether to revise +or invoke the builder again. ## References -- [skill-conformance-profiles.yaml](references/skill-conformance-profiles.yaml) — authoritative `repo-runtime` semantics and severities - -- [references/skill-template.md](references/skill-template.md) — canonical SKILL.md template + auditor checklist + PRODUCT.md alignment -- [references/agentops-skill-factory.md](references/agentops-skill-factory.md) — clean-room factory workflow and productization rules -- [references/skill-authoring-standard.md](references/skill-authoring-standard.md) — clean-room best-practices doctrine + best-practice-to-enforcement crosswalk; backs the `scan_descriptions.py` trigger scanner -- [references/skill-builder.feature](references/skill-builder.feature) — Executable spec: mode dispatch, materialize from template, Codex parity bundle, self-audit + factory score (soc-qk4b) -- [references/hyper-extract-design-steals.md](references/hyper-extract-design-steals.md) — authoring rules stolen from Hyper-Extract: the WHAT-vs-HOW (schema vs guideline) contract, canonical `{from}\|{rel}\|{to}` identifier dedup-key form, and folded graph-designer/yaml-validator/template-optimizer patterns (age-bp1) +- [skill template](references/skill-template.md) +- [heal-skill](../heal-skill/SKILL.md) diff --git a/skills/skill-builder/references/agentops-skill-factory.md b/skills/skill-builder/references/agentops-skill-factory.md deleted file mode 100644 index c2b87e587..000000000 --- a/skills/skill-builder/references/agentops-skill-factory.md +++ /dev/null @@ -1,69 +0,0 @@ -# AgentOps Skill Factory Productization - -This reference captures the local Codex `agentops-skill-factory` prototype as a -repo workflow. The goal is not to ship the local prototype verbatim; the goal is -to fold its proven behavior into the existing `skill-builder` and -heal-skill deep-audit pair. - -## Clean-room Inputs - -Use only AgentOps-owned artifacts: - -- `docs/reference/skill-quality-rubric.md` -- `skills/standards/references/skill-structure.md` -- `skills/standards/references/external-source-attribution.md` - -Do not copy protected third-party skill prose, prompts, scripts, names, or -examples into AgentOps skills. Extract reusable structure and quality signals -only. - -## Factory Loop - -1. Start with the built-in Codex skill-creator shape: a short `SKILL.md` kernel, - progressive disclosure through `references/`, reusable `scripts/`, optional - `assets/`, and validation evidence. -2. Score the target skill: - - ```bash - python3 skills/heal-skill/scripts/score_agentops_skill.py skills/<name> --markdown - ``` - -3. Pick the smallest score-improving patch, usually one of: - - add or link `SELF-TEST.md`; - - move bulky context into `references/`; - - add a focused validation script; - - add an output contract or explicit quality rubric; - - tighten trigger language in frontmatter and body. -4. Re-run the heal-skill deep audit (`audit.sh`), `heal-skill --check --strict`, and any target-specific - validation by exit code, not by grepping output text. -5. Mirror behavior into `skills-codex/<name>/` or - `skills-codex-overrides/<name>/` when the Codex runtime needs different - phrasing or execution instructions. - -## Scale Run Discipline - -When authoring multiple skills, protect file ownership before parallelism: - -- One skill equals one worker equals one source directory plus its Codex mirror. -- Run create-only work first; mutate existing skills only after the source corpus - is settled. -- Use deterministic scripts or NTM/Agent Mail lanes for batch work. Do not use - the Workflow tool as the skill factory. -- Trust `git status`, generated hashes, final file contents, and gate exit codes - over worker self-reports. -- Clean-room review includes exact names. Rename third-party-derived labels into - AgentOps-owned names before source skills, Codex mirrors, or wrappers are keyed. - -## Productization Rule - -Local prototype skills may guide the workflow, but PRs should land durable -AgentOps artifacts: - -- source skill changes under `skills/`; -- Codex runtime changes under `skills-codex/` or `skills-codex-overrides/`; -- reusable scoring/audit scripts under `skills/heal-skill/scripts/`; -- clean-room standards under `docs/reference/` and `skills/standards/`. - -Avoid adding a duplicate top-level skill when an existing AgentOps skill already -owns the domain. Extend `skill-builder`, `heal-skill`, `rpi`, or `evolve` -instead. diff --git a/skills/skill-builder/references/hyper-extract-design-steals.md b/skills/skill-builder/references/hyper-extract-design-steals.md deleted file mode 100644 index cd5ef6615..000000000 --- a/skills/skill-builder/references/hyper-extract-design-steals.md +++ /dev/null @@ -1,104 +0,0 @@ -# Hyper-Extract design steals (authoring rules) - -Three reusable authoring contracts mined from Hyper-Extract's template-design -skills (`graph-designer`, `yaml-validator`, `template-optimizer`). They apply to -any AgentOps surface that pairs a machine-readable **schema** with human-written -**guidance** — SKILL.md frontmatter + body, extraction templates, record/graph -designers, and the corpus schemas under `schemas/`. - -## 1. The WHAT-vs-HOW authoring contract - -**Rule:** the **schema defines WHAT** (the fields, their types, their identity); -the **guideline defines HOW to do it well** (extraction strategy, quality bars, -creation conditions, common mistakes). A guideline that restates field -definitions is drift — the schema already owns that. Keep them disjoint. - -This is the single most load-bearing separation when a schema and a guideline -travel together: every duplicated field description in the guideline is a place -the two can silently diverge. Author the guideline as if the reader has already -read the schema. - -| Guideline SHOULD carry | Guideline should NOT carry (schema owns it) | -|------------------------|---------------------------------------------| -| Extraction strategy ("extract the valuable entities") | Field definitions ("`name` should be…") | -| Quality requirements ("keep naming consistent") | Type descriptions ("the `type` field is a str") | -| Creation conditions ("only when the text states it") | Reference requirements ("must point at `name`") | -| Common mistakes to avoid | Restated schema field descriptions | - -**Enforcement smell (steal from `template-optimizer` rule 4):** flag any -guideline/rules prose that repeats a field definition or a type description — -that is a schema-vs-guideline boundary violation, not guidance. Fix by deleting -the restated field text from the guideline; the schema is the single source of -WHAT. - -Applied to skill authoring: the SKILL.md frontmatter (`name`, `description`, -`hexagonal_role`, `consumes`/`produces`) is the schema = WHAT the skill is; the -SKILL.md body is the guideline = HOW to run it well. Do not restate frontmatter -fields prose-side; spend the body on strategy, quality bars, and footguns. - -## 2. Identifier dedup-key patterns (canonical dedup-key form) - -**Rule:** a dedup key (identity key) for a relationship/edge is a **template of -field references**, not a free-text string. The canonical form is: - -``` -'{from}|{rel}|{to}' -``` - -i.e. pipe-joined `{field}` placeholders that resolve against the record's own -fields. This replaces hand-written string `dedup_keys` (which drift from the -schema and can't be validated). Hyper-Extract's live forms: - -```yaml -identifiers: - entity_id: name - relation_id: '{source}|{relation_type}|{target}' # graph edge - relation_id: '{source}|{relation_type}|{target}|{event_date}' # temporal edge - relation_id: '{source}|{relation_type}|{target}|{location}' # spatial edge - relation_members: - source: ... - target: ... -``` - -Why a template, not a string: - -- **Validatable** — every `{placeholder}` must resolve to a declared field, so a - dedup key referencing a non-existent field is a catchable error (see the - `yaml-validator` identifier checklist). -- **Schema-anchored** — the dedup key is derived from WHAT (the fields), so it - cannot silently diverge from the schema the way a free string can. -- **Composable** — add a dimension (time, location) by appending another - `{field}`; the dedup key extends with the schema instead of being rewritten. - -**Adopt as canonical:** when a corpus/extraction surface needs an identity or -dedup key, express it as a `{field}|{field}|…` template over declared fields — -never an opaque string `dedup_key`. - -## 3. Folded patterns from the three Hyper-Extract design skills - -Useful, runtime-agnostic patterns lifted into our authoring doctrine: - -- **`graph-designer` → type-driven design + display labels.** Confirm the shape - first (record vs graph vs hypergraph vs temporal/spatial), then design fields - to the shape. Carry a human-readable display label derived from fields - (`'{name} ({category})'`) so output is legible without re-deriving identity. - Mirror for skills: pick the skill *mode* first, then author to that mode. -- **`yaml-validator` → tiered, ordered validation.** Validate in a fixed order - (syntax → structure → identifiers → field-quality) and grade findings by - level: **ERROR** (won't work — must fix), **WARNING** (quality risk — should - fix), **INFO** (recommended). This is the same tiering our heal-skill deep audit uses; - prefer ordered + level-graded checks over a flat pass/fail. -- **`template-optimizer` → information-density discipline.** Flag > 5 fields per - entity/relation for review; prioritize **Essential → Important → Optional** and - cut to the essential set. Standardize names to the concise canonical token - (`relation_type` → `type`, `event_date` → `time`). Apply the same three - optimization tiers to authoring fixes: **Auto-fix** (always-safe), **Suggest** - (needs review), **Review** (a design decision, leave to the author). This is - the field-count complement to our context-density rule. - -## Provenance - -Steals captured from the `Hyper-Extract` reference clone -(`hyperextract-skills/{graph-designer,yaml-validator,template-optimizer}`) under -bead age-bp1. Folded as additive authoring rules; no runtime behavior depends on -the Hyper-Extract code. diff --git a/skills/skill-builder/references/skill-authoring-standard.md b/skills/skill-builder/references/skill-authoring-standard.md deleted file mode 100644 index 2075a96ce..000000000 --- a/skills/skill-builder/references/skill-authoring-standard.md +++ /dev/null @@ -1,136 +0,0 @@ -# AgentOps Skill Authoring Standard - -Clean-room distillation of the broadly accepted SKILL.md best practices -(Anthropic Agent Skills guidance and the wider community consensus), restated in -AgentOps's own words and cross-walked to what AgentOps tooling *actually* -enforces. This is the doctrine layer behind `skill-builder`, the heal-skill deep audit, -and `heal-skill` — read it before authoring or absorbing a skill. - -This file contains only AgentOps-owned summaries and rules. It does not copy any -third-party skill content (see [agentops-skill-factory.md](agentops-skill-factory.md)). -Executable values come only from -[skill-conformance-profiles.yaml](skill-conformance-profiles.yaml), using the -`repo-runtime` profile by default. - -## Contents - -- [Mental model: three loading levels](#mental-model-three-loading-levels) -- [The description is the trigger mechanism](#the-description-is-the-trigger-mechanism) -- [Naming (AgentOps house style)](#naming-agentops-house-style) -- [Progressive disclosure](#progressive-disclosure) -- [Degrees of freedom](#degrees-of-freedom) -- [Anti-patterns](#anti-patterns) -- [Crosswalk: best practice to AgentOps enforcement](#crosswalk-best-practice-to-agentops-enforcement) - -## Mental model: three loading levels - -A skill costs context in three tiers, and good authoring minimizes the lower -tiers: - -1. **Metadata** (`name` + `description`) — always in the system prompt. ~100 - tokens. This is the only thing the runtime sees when *deciding* whether to - load the skill. -2. **SKILL.md body** — loaded only after the skill triggers. Keep it under the - 250-line ceiling; push overflow into `references/`. -3. **Bundled `references/` and `scripts/`** — read or executed only when the - body points to them. Effectively unlimited, because they load on demand. - -The implication that drives every rule below: the description does all the -selection work, and the body is read far less often than authors assume. - -## The description is the trigger mechanism - -Skill selection is **pure LLM reasoning over descriptions** — no embeddings, no -keyword index. A description without explicit trigger phrases is a skill that -silently never fires. This is the single largest latent gap in the corpus: most -skills score 1/3 on trigger quality because they describe *what* the skill does -but never *when* to invoke it. - -Write descriptions that are: - -- **Third person.** "Scaffolds a new skill", not "I help you scaffold". -- **Specific.** Name the artifact and the domain, not "helps with skills". -- **Trigger-bearing.** Include the phrases a user or agent would actually say. - -The deep audit (`heal-skill/scripts/audit.sh`) accepts a trigger in any of three forms; satisfy at least one: - -- **Block marker** — `description: |` or `description: >` whose value contains - a profile-accepted `Use when:` / `Triggers:` marker. -- **Inline marker** — a `Triggers:` or `Use when:` clause in the - single-line description (the most common AgentOps form). -- **Metadata list** — a `metadata.triggers:` YAML list meeting the profile's - declared minimum cardinality. - -Audit the whole corpus for this gap at any time: - -```bash -python3 skills/skill-builder/scripts/scan_descriptions.py skills -python3 skills/skill-builder/scripts/scan_descriptions.py skills --strict # exit 1 on any miss -python3 skills/skill-builder/scripts/scan_descriptions.py skills --json # robot mode -``` - -The scanner applies the exact detection logic of `heal-skill/scripts/audit.sh`, -so its verdict never contradicts the per-skill auditor; it adds the prioritized -remediation list and a suggested `Triggers:` stub the auditor does not provide. - -## Naming (AgentOps house style) - -The external standard prefers gerund names (`processing-pdfs`). **AgentOps -deliberately diverges**: skills are named for the noun or verb of the workflow -(`skill-builder`, `bug-hunt`, `crank`, `council`) so they read as commands in -the `/skill` slash-menu. This is an intentional, documented deviation — keep new -skills consistent with the existing corpus rather than introducing gerunds. - -## Progressive disclosure - -- Keep `references/` **one level deep**. No chains - (`SKILL.md -> a.md -> b.md`); the runtime may partial-read a deep file. -- Reference files over ~100 lines should open with a short table of contents. -- A repo-runtime SKILL.md may contain at most 250 lines. Line 251 receives the - profile-declared `references-modularization` finding even when references exist. -- Distinguish **execute** from **read** when pointing at a script: "Run - `python scripts/x.py`" versus "See `scripts/x.py` for the algorithm". - -## Degrees of freedom - -Match instruction specificity to how fragile the task is: - -- **High freedom** — multiple valid approaches (e.g. review guidelines). Give - direction, not steps. -- **Medium freedom** — a preferred pattern with acceptable variation (e.g. - report templates). Give a default and an escape hatch. -- **Low freedom** — error-prone, consistency-critical (e.g. a migration - command). Give the exact invocation and forbid alternatives. - -## Anti-patterns - -- **Multiple options, no default.** Pick one tool, name it, then offer the - escape hatch ("for scanned PDFs, use X instead"). -- **Human docs in the skill.** No README / CHANGELOG / install guide; skills are - for agents. -- **Inconsistent terminology.** Choose one term for a concept and use it - throughout. -- **Time-sensitive claims.** "Currently" and dated facts rot; move volatile - detail to an "old patterns" section or a generated artifact. -- **Hardcoded absolute paths.** Use repo-relative paths so the skill is portable. -- **Vague descriptions.** The fastest way to ship a skill that never triggers. - -## Crosswalk: best practice to AgentOps enforcement - -| Best practice | AgentOps enforcement | Gate | -|---------------|----------------------|------| -| Description carries triggers | `description-has-triggers`, `trigger-clarity` | auditor WARN (corpus drift accumulates) | -| `name` matches directory | `heal.sh` NAME_MISMATCH | CI FAIL (`skills-integrity`) | -| `name` + `description` present | `heal.sh` MISSING_NAME / MISSING_DESC | CI FAIL | -| Output section defines all executable-handoff components from the profile | `output-spec-explicit` | profile severity | -| Constraints front-loaded with rationale | `constraints-frontloaded`, `rationale-present` | auditor WARN | -| References one level deep, linked | `heal.sh` UNLINKED_REF / DEAD_REF | CI FAIL / WARN | -| 250-line ceiling | `skill-builder` and deep audit reject/flag > 250 | profile-derived | -| Frontmatter schema valid | `validate-skill-schema.sh`, v2 frontmatter | CI FAIL | -| Dependencies resolve | dependency-resolution check | CI FAIL | -| Codex parity (dual-file) | manual `skills-codex/`, parity-drift audit | CI FAIL (semantic), manual mirror | -| Registry / catalog current | `generate-registry.sh`, `generate-skill-catalog.sh` | CI FAIL (registry) / advisory (catalog) | - -WARN-severity checks do not block a merge, which is exactly why trigger quality -drifted across the corpus. Treat a WARN as a real finding, not noise — run the -scanner and close the backlog incrementally. diff --git a/skills/skill-builder/schemas/build-report.json b/skills/skill-builder/schemas/build-report.json index dcb0fa57b..52627cb78 100644 --- a/skills/skill-builder/schemas/build-report.json +++ b/skills/skill-builder/schemas/build-report.json @@ -1,52 +1,25 @@ { - "$schema": "https://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "Skill Build Report", - "description": "Output contract for skill-builder. Reports what mode ran, which files were created, and whether the post-build self-audit passed.", "type": "object", - "required": ["mode", "skill_name", "profile_id", "files_created", "audit_pass"], + "required": ["mode", "skill_name", "files_created", "structure_check_pass"], "properties": { "mode": { "type": "string", - "enum": ["from-scratch", "from-template", "absorb-external", "from-pattern"], - "description": "Which builder mode produced this skill. from-pattern is alpha (passthrough to ao flywheel close-loop)." + "enum": ["from-scratch", "from-template", "absorb-external"] }, "skill_name": { "type": "string", - "pattern": "^[a-z][a-z0-9-]*$", - "description": "Lowercase-hyphen slug for the new skill (matches directory name)." - }, - "profile_id": { - "type": "string", - "description": "Selected authoritative skill-conformance profile ID." + "pattern": "^[a-z][a-z0-9-]*$" }, "files_created": { "type": "array", "items": {"type": "string"}, - "description": "Absolute or repo-relative paths of files written during this build." + "minItems": 2, + "uniqueItems": true }, - "audit_pass": { - "type": "boolean", - "description": "Did the immediate post-build self-audit (heal-skill deep audit) emit VERDICT: PASS or VERDICT: WARN? (False if FAIL.)" - }, - "audit_report_path": { - "type": "string", - "description": "Where the self-audit report was written (typically .agents/audits/<skill>-build.md)." - }, - "warnings": { - "type": "array", - "items": {"type": "string"}, - "description": "Non-fatal issues surfaced during build (e.g., template SHA pin mismatch, missing optional sections)." - }, - "source": { - "type": "object", - "description": "Mode-specific provenance.", - "properties": { - "external_path": {"type": "string", "description": "For absorb-external: source SKILL.md path"}, - "template_skill": {"type": "string", "description": "For from-template: skill copied from"}, - "flywheel_run_id": {"type": "string", "description": "For from-pattern: ao flywheel close-loop run id"} - }, - "additionalProperties": false - } + "structure_check_pass": {"type": "boolean"}, + "source_hint": {"type": "string"} }, "additionalProperties": false } diff --git a/skills/skill-builder/scripts/build.sh b/skills/skill-builder/scripts/build.sh index c1092d50c..462a6b90f 100755 --- a/skills/skill-builder/scripts/build.sh +++ b/skills/skill-builder/scripts/build.sh @@ -1,185 +1,54 @@ #!/usr/bin/env bash -# build.sh — skill-builder mode dispatcher -# Usage: -# build.sh from-scratch <skill-name> -# build.sh from-template <skill-name> --like <existing-skill> -# build.sh absorb-external <skill-name> --from <path-to-external-SKILL.md> -# build.sh from-pattern # alpha: passthrough to ao flywheel close-loop -# -# Always runs the heal-skill deep audit (absorbed from /skill-auditor) on the new skill as a self-check before declaring success. - +# Create, structurally check, and project one skill exactly once. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# SKILL_BUILDER_REPO_ROOT overrides root discovery for fixture-driven tests -# (see init.sh — same contract, mirrors HEAL_REPO_ROOT in heal.sh). REPO_ROOT="${SKILL_BUILDER_REPO_ROOT:-$(cd "$SCRIPT_DIR/../../.." && pwd)}" -INIT_SH="$SCRIPT_DIR/init.sh" -AUDITOR_SH="$REPO_ROOT/skills/heal-skill/scripts/audit.sh" -PROFILE_TOOL="$REPO_ROOT/skills/skill-builder/scripts/conformance_profile.py" -EXTERNAL_SOURCE="" - -profile_args=(--repo-root "$REPO_ROOT") -if [[ -n "${SKILL_CONFORMANCE_PROFILE_ID:-}" ]]; then - profile_args+=(--profile-id "$SKILL_CONFORMANCE_PROFILE_ID") -fi -if [[ ! -f "$PROFILE_TOOL" ]] || ! PROFILE_ID="$(python3 "$PROFILE_TOOL" "${profile_args[@]}")"; then - echo "[skill-builder] ERROR: profile configuration is unavailable or invalid" >&2 - exit 2 -fi -export SKILL_CONFORMANCE_PROFILE_ID="$PROFILE_ID" usage() { - cat <<EOF -Usage: - $0 from-scratch <skill-name> - $0 from-template <skill-name> --like <existing-skill> - $0 absorb-external <skill-name> --from <path-to-external-SKILL.md> - $0 from-pattern # alpha: passthrough to ao flywheel close-loop - -Modes: - from-scratch Interactive scaffold from canonical template - from-template Copy structure from a sibling skill - absorb-external Wrap an external SKILL.md in AgentOps frontmatter - from-pattern ALPHA — delegates to 'ao flywheel close-loop'. - Outputs at .agents/knowledge/promoted/, NOT shaped as SKILL.md drafts. - Use from-scratch or absorb-external for SKILL.md output today. + cat >&2 <<EOF +usage: + build.sh from-scratch <slug> + build.sh from-template <slug> --like <existing-slug> + build.sh absorb-external <slug> --from <path> EOF exit 2 } -[[ $# -lt 1 ]] && usage +[[ $# -ge 2 ]] || usage +mode="$1" +slug="$2" +shift 2 -MODE="$1" -shift - -case "$MODE" in - from-pattern) - # Alpha passthrough — explicitly documented in SKILL.md - echo "[skill-builder] from-pattern is ALPHA — delegating to 'ao flywheel close-loop'" - echo "[skill-builder] Output will NOT be a SKILL.md draft; it lands at .agents/knowledge/promoted/" - exec ao flywheel close-loop "$@" - ;; - - from-scratch) - [[ $# -lt 1 ]] && { echo "Error: from-scratch requires <skill-name>" >&2; usage; } - SKILL_NAME="$1"; shift - bash "$INIT_SH" --interactive "$SKILL_NAME" "$@" - ;; - - from-template) - [[ $# -lt 1 ]] && { echo "Error: from-template requires <skill-name>" >&2; usage; } - SKILL_NAME="$1"; shift - bash "$INIT_SH" --like-flag-mode "$SKILL_NAME" "$@" - ;; - - absorb-external) - [[ $# -lt 1 ]] && { echo "Error: absorb-external requires <skill-name>" >&2; usage; } - SKILL_NAME="$1"; shift - if [[ "${1:-}" == "--from" ]]; then - EXTERNAL_SOURCE="${2:-}" - fi - bash "$INIT_SH" --absorb "$SKILL_NAME" "$@" - ;; - - *) - echo "Error: unknown mode '$MODE'" >&2 - usage - ;; +case "$mode" in + from-scratch) init_mode=--scratch ;; + from-template) init_mode=--template ;; + absorb-external) init_mode=--external ;; + *) usage ;; esac -# Post-build self-audit (mandatory per Critical Constraints) -NEW_SKILL_DIR="$REPO_ROOT/skills/$SKILL_NAME" -if [[ ! -d "$NEW_SKILL_DIR" ]]; then - echo "[skill-builder] ERROR: expected $NEW_SKILL_DIR to exist after init.sh" >&2 +bash "$SCRIPT_DIR/init.sh" "$init_mode" "$slug" "$@" + +report="$REPO_ROOT/.agents/audits/${slug}-build.json" +if ! HEAL_REPO_ROOT="$REPO_ROOT" bash "$REPO_ROOT/skills/heal-skill/scripts/heal.sh" \ + --check --strict "$REPO_ROOT/skills/$slug"; then + echo "skill-builder: structural check failed" >&2 exit 1 fi -# The build report (written by init.sh) carries audit_pass=null as a pre-audit -# placeholder. Patch it to the REAL audit outcome here so the report records what -# actually happened and stays schema-valid (build-report.json requires -# audit_pass to be a boolean). Without this the report's audit_pass was always -# null — the original defect this fixes (age-fix-skill-factory-mcc). -BUILD_REPORT="$REPO_ROOT/.agents/audits/${SKILL_NAME}-build.json" +python3 "$REPO_ROOT/scripts/generate-skill-mesh.py" +bash "$REPO_ROOT/scripts/codex-sync.sh" --only "$slug" +bash "$REPO_ROOT/scripts/regen-codex-hashes.sh" --only "$slug" -# patch_audit_pass <true|false> — record the audit outcome in the build report. -# Never fails the build: a missing report or absent jq/python3 only warns. -patch_audit_pass() { - [[ -f "$BUILD_REPORT" ]] || return 0 - local val="$1" tmp - # Create the temp in the report's OWN directory so the mv is an atomic - # same-filesystem rename (a cross-device mv can degrade to a non-atomic copy - # and corrupt the report on failure). - tmp="$(mktemp "$(dirname "$BUILD_REPORT")/.audit-patch.XXXXXX")" || return 0 - if command -v jq >/dev/null 2>&1; then - # Fold the mv into the tested condition so a failed write only WARNs (never - # aborts the build under set -e). - if jq --argjson ap "$val" '.audit_pass = $ap' "$BUILD_REPORT" >"$tmp" 2>/dev/null \ - && mv "$tmp" "$BUILD_REPORT" 2>/dev/null; then - : - else - rm -f "$tmp"; echo "[skill-builder] WARN: could not patch audit_pass in $BUILD_REPORT" >&2 - fi - elif command -v python3 >/dev/null 2>&1; then - if python3 - "$BUILD_REPORT" "$val" >"$tmp" 2>/dev/null <<'PY' && mv "$tmp" "$BUILD_REPORT" 2>/dev/null -import json, sys -d = json.load(open(sys.argv[1])) -d["audit_pass"] = (sys.argv[2] == "true") -json.dump(d, sys.stdout, indent=2) +python3 - "$report" <<'PY' +import json +from pathlib import Path +import sys + +path = Path(sys.argv[1]) +payload = json.loads(path.read_text(encoding="utf-8")) +payload["structure_check_pass"] = True +path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8") PY - then - : - else - rm -f "$tmp"; echo "[skill-builder] WARN: could not patch audit_pass in $BUILD_REPORT" >&2 - fi - else - rm -f "$tmp"; echo "[skill-builder] WARN: no jq/python3 to record audit_pass in $BUILD_REPORT" >&2 - fi - return 0 -} -# The generated kernel and any external adoption must satisfy the selected -# profile before the auditor can declare the build successful. -if ! PROFILE_EVAL="$(python3 "$PROFILE_TOOL" --repo-root "$REPO_ROOT" \ - --profile-id "$PROFILE_ID" --audit-tsv "$NEW_SKILL_DIR/SKILL.md")"; then - patch_audit_pass false - exit 1 -fi -KERNEL_LIMIT="$(printf '%s\n' "$PROFILE_EVAL" | awk -F '\t' '$1 == "kernel_max_lines" {print $2}')" -KERNEL_LINES="$(printf '%s\n' "$PROFILE_EVAL" | awk -F '\t' '$1 == "line_count" {print $2}')" -if [[ -z "$KERNEL_LIMIT" || -z "$KERNEL_LINES" ]] || (( KERNEL_LINES > KERNEL_LIMIT )); then - patch_audit_pass false - echo "[skill-builder] ERROR: generated SKILL.md has ${KERNEL_LINES:-unknown} lines; profile $PROFILE_ID allows $KERNEL_LIMIT" >&2 - exit 1 -fi - -if [[ -n "$EXTERNAL_SOURCE" ]]; then - if ! python3 "$PROFILE_TOOL" --repo-root "$REPO_ROOT" \ - --profile-id "$PROFILE_ID" --verify-clean-room "$EXTERNAL_SOURCE" \ - --generated-dir "$NEW_SKILL_DIR" \ - --generated-dir "$REPO_ROOT/skills-codex/$SKILL_NAME" - then - patch_audit_pass false - echo "[skill-builder] ERROR: clean-room verification failed" >&2 - exit 1 - fi -fi - -if [[ -x "$AUDITOR_SH" ]]; then - echo "" - echo "[skill-builder] Running self-audit on $NEW_SKILL_DIR..." - if bash "$AUDITOR_SH" "$NEW_SKILL_DIR"; then - patch_audit_pass true - echo "[skill-builder] Self-audit PASS or WARN — build complete (audit_pass=true)" - else - # Record the failure before aborting so the report reflects reality. - patch_audit_pass false - echo "[skill-builder] Self-audit FAIL — build aborted (audit_pass=false)" >&2 - exit 1 - fi -else - # An unaudited build cannot claim a pass: record false rather than leaving the - # null placeholder (which would be schema-invalid and read as "audited"). - patch_audit_pass false - echo "[skill-builder] WARN: heal-skill audit script not found at $AUDITOR_SH; skipping self-audit (audit_pass=false)" >&2 -fi +echo "skill-builder: created and projected $slug" diff --git a/skills/skill-builder/scripts/init.sh b/skills/skill-builder/scripts/init.sh index fa16a9ba5..3032bd46e 100755 --- a/skills/skill-builder/scripts/init.sh +++ b/skills/skill-builder/scripts/init.sh @@ -1,301 +1,146 @@ #!/usr/bin/env bash -# init.sh — materialize a new skill from the canonical template -# Invoked by build.sh; not typically called directly. -# -# Usage: -# init.sh --interactive <skill-name> -# init.sh --like-flag-mode <skill-name> --like <source-skill> -# init.sh --absorb <skill-name> --from <path-to-external-SKILL.md> - +# Create one metadata-complete canonical skill source package. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# SKILL_BUILDER_REPO_ROOT overrides root discovery for fixture-driven tests -# (tests/integration/test_skill_builder.bats scaffolds into a scratch repo copy -# so no in-repo surface — skills/, the dispositions ledger, the codex catalog — -# is ever mutated by a test run). Mirrors HEAL_REPO_ROOT in heal.sh. Production -# derives the root from the script location. REPO_ROOT="${SKILL_BUILDER_REPO_ROOT:-$(cd "$SCRIPT_DIR/../../.." && pwd)}" -TEMPLATE_REF="$REPO_ROOT/skills/skill-builder/references/skill-template.md" -[[ -f "$TEMPLATE_REF" ]] || { echo "init.sh: missing $TEMPLATE_REF" >&2; exit 1; } +usage() { + echo "usage: init.sh --scratch|--template|--external <slug> [--like <slug>|--from <path>]" >&2 + exit 2 +} -MODE="${1:?usage: init.sh --interactive|--like-flag-mode|--absorb <name> [opts]}" -shift +[[ $# -ge 2 ]] || usage +mode="$1" +slug="$2" +shift 2 -SKILL_NAME="${1:?missing <skill-name>}" -shift +[[ "$slug" =~ ^[a-z][a-z0-9-]*$ ]] || { + echo "init.sh: slug must be lowercase-hyphen: $slug" >&2 + exit 2 +} -# Validate slug -[[ "$SKILL_NAME" =~ ^[a-z][a-z0-9-]*$ ]] || { - echo "init.sh: skill name '$SKILL_NAME' must be lowercase-hyphen (e.g. my-skill)" >&2 +source_hint="" +case "$mode" in + --scratch) + [[ $# -eq 0 ]] || usage + ;; + --template) + [[ $# -eq 2 && "$1" == "--like" ]] || usage + source_hint="$2" + [[ -f "$REPO_ROOT/skills/$source_hint/SKILL.md" ]] || { + echo "init.sh: unknown template skill: $source_hint" >&2 + exit 2 + } + ;; + --external) + [[ $# -eq 2 && "$1" == "--from" ]] || usage + source_hint="$2" + [[ -f "$source_hint" ]] || { + echo "init.sh: external source does not exist: $source_hint" >&2 + exit 2 + } + ;; + *) usage ;; +esac + +target="$REPO_ROOT/skills/$slug" +[[ ! -e "$target" ]] || { + echo "init.sh: target already exists: $target" >&2 exit 1 } -NEW_DIR="$REPO_ROOT/skills/$SKILL_NAME" -NEW_SKILL_MD="$NEW_DIR/SKILL.md" -[[ -e "$NEW_DIR" ]] && { echo "init.sh: $NEW_DIR already exists; aborting" >&2; exit 1; } +tier="${SKILL_TIER:-execution}" +dependencies="${SKILL_DEPENDENCIES:-[]}" +capabilities="${SKILL_CAPABILITIES:-[${slug//-/_}]}" +effects="${SKILL_EFFECTS:-[]}" -mkdir -p "$NEW_DIR/scripts" +python3 - "$dependencies" "$capabilities" "$effects" <<'PY' +import json +import sys +for value in sys.argv[1:]: + parsed = json.loads(value) + if not isinstance(parsed, list) or not all(isinstance(item, str) for item in parsed): + raise SystemExit("skill metadata lists must be JSON arrays of strings") +PY -# --- Per-mode population -------------------------------------------------- -case "$MODE" in - --interactive) - # Minimal non-blocking defaults; skip prompts in CI by reading env vars - TIER="${SKILL_TIER:-execution}" - DEPS="${SKILL_DEPS:-[]}" - INTENT_MODE="${SKILL_INTENT_MODE:-task}" - ;; +mkdir -p "$target/scripts" - --like-flag-mode) - LIKE_FLAG="${1:-}"; SOURCE_SKILL="${2:-}" - [[ "$LIKE_FLAG" == "--like" && -n "$SOURCE_SKILL" ]] || { - echo "init.sh --like-flag-mode requires '--like <source-skill>'" >&2 - exit 1 - } - SOURCE_DIR="$REPO_ROOT/skills/$SOURCE_SKILL" - [[ -f "$SOURCE_DIR/SKILL.md" ]] || { - echo "init.sh: source skill $SOURCE_DIR/SKILL.md not found" >&2 - exit 1 - } - # Extract frontmatter values from source for sane defaults - TIER="$(awk '/^---$/{n++;next} n==1 && /^[ ]+tier:/{print $2; exit}' "$SOURCE_DIR/SKILL.md")" - TIER="${TIER:-execution}" - DEPS="[]" - INTENT_MODE="$(awk '/^---$/{n++;next} n==1 && /^[ ]+mode:/{print $2; exit}' "$SOURCE_DIR/SKILL.md")" - INTENT_MODE="${INTENT_MODE:-task}" - ;; - - --absorb) - FROM_FLAG="${1:-}"; SOURCE_PATH="${2:-}" - [[ "$FROM_FLAG" == "--from" && -n "$SOURCE_PATH" ]] || { - echo "init.sh --absorb requires '--from <path-to-external-SKILL.md>'" >&2 - exit 1 - } - [[ -f "$SOURCE_PATH" ]] || { echo "init.sh: external SKILL.md not found at $SOURCE_PATH" >&2; exit 1; } - TIER="${SKILL_TIER:-execution}" - DEPS="[]" - INTENT_MODE="task" - ;; - - *) - echo "init.sh: unknown mode '$MODE'" >&2 - exit 2 - ;; -esac - -# --- Render frontmatter + skeleton --------------------------------------- -cat > "$NEW_SKILL_MD" <<EOF +cat >"$target/SKILL.md" <<EOF --- -name: $SKILL_NAME -description: | - <one-line: verb + object + domain> - - **Use when:** - - <Trigger 1> - - <Trigger 2> - - **Triggers:** "<trigger phrase 1>", "<trigger phrase 2>" - - **Not ideal for:** - - <Anti-scenario 1> +name: $slug +description: 'TODO: state the behavior and concrete trigger phrases for $slug.' skill_api_version: 1 -context: - window: fork - intent: - mode: $INTENT_MODE - sections: - exclude: [HISTORY] - intel_scope: topic +hexagonal_role: supporting +consumes: [] +produces: [] +context_rel: [] metadata: - tier: $TIER - dependencies: $DEPS + tier: $tier + dependencies: $dependencies + capabilities: $capabilities + effects: $effects + canonical_status: canonical + disposition: keep_specialist stability: experimental -output_contract: "TODO: path to schema or output description" --- -# /$SKILL_NAME — <Title matching slug> +# /$slug -<1-2 sentence purpose paragraph> +TODO: Explain the bounded behavior this skill provides. -## Overview +## Inputs -<What this skill does, why it matters, and when to use it> +TODO: List required inputs and explicit non-goals. -## ⚠️ Critical Constraints +## Procedure -- **Rule 1:** <constraint>. **Why:** <rationale> +1. TODO: Perform one bounded operation. +2. TODO: Check the output against the stated contract. +3. Report the result and stop. -## Workflow +## Output -### Phase 1: <name> +TODO: Define the artifact or response shape and how a caller checks it. -<instructions> +## Checks -**Checkpoint:** <what to confirm before next phase> +- The output satisfies the declared behavior. +- No undeclared side effect occurred. -## Output Specification +## Failure behavior -**Artifact directory:** \`.agents/out/$SKILL_NAME/\` -**Filename convention:** \`report.json\` -**Serialization/schema format:** JSON matching the declared output schema. -**Validator command:** \`jq -e . .agents/out/$SKILL_NAME/report.json\` -**Downstream handoff:** consumed by the validation wave. - -## Quality Rubric - -- [ ] <Check 1> -- [ ] <Check 2> -- [ ] <Check 3> - -## Examples - -\`\`\`bash -/$SKILL_NAME <example-args> -\`\`\` - -## Troubleshooting - -| Problem | Cause | Solution | -|---------|-------|----------| - -## See Also - -- [heal-skill](../heal-skill/SKILL.md) — deep audit (audit.sh) this skill before declaring stable +Report the concrete failure and stop. The caller owns any revision. EOF -# --- Mode-specific content injection ------------------------------------- -if [[ "$MODE" == "--absorb" ]]; then - # Observe package shape only; generated files record no external prose or path. - cat >> "$NEW_SKILL_MD" <<EOF - -## Clean-room provenance - -This skill was synthesized under the repo-runtime clean-room profile from -AgentOps-owned structure and terminology only. -EOF -fi - -# --- Companion files ----------------------------------------------------- -cat > "$NEW_DIR/scripts/validate.sh" <<'EOF' +cat >"$target/scripts/validate.sh" <<'EOF' #!/usr/bin/env bash -# validate.sh — minimal self-validation set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SKILL_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" REPO_ROOT="$(cd "$SKILL_DIR/../.." && pwd)" -exec bash "$REPO_ROOT/skills/heal-skill/scripts/audit.sh" "$SKILL_DIR" +exec bash "$REPO_ROOT/skills/heal-skill/scripts/heal.sh" --check --strict "$SKILL_DIR" EOF -chmod +x "$NEW_DIR/scripts/validate.sh" -chmod +x "$NEW_DIR" 2>/dev/null || true +chmod +x "$target/scripts/validate.sh" -# --- Codex parity (slim frontmatter + prompt.md) ------------------------- -CODEX_DIR="$REPO_ROOT/skills-codex/$SKILL_NAME" -mkdir -p "$CODEX_DIR" +mkdir -p "$REPO_ROOT/.agents/audits" +report="$REPO_ROOT/.agents/audits/${slug}-build.json" +python3 - "$report" "$mode" "$slug" "$source_hint" <<'PY' +import json +from pathlib import Path +import sys -# Try /converter if present, otherwise hand-build -CONVERTER="$REPO_ROOT/skills/converter/scripts/convert.sh" -if [[ -x "$CONVERTER" ]]; then - bash "$CONVERTER" "skills/$SKILL_NAME" codex 2>/dev/null || { - echo "init.sh: converter failed; falling back to hand-built codex artifacts" >&2 - } -fi - -# Hand-build codex SKILL.md (slim frontmatter — NO skill_api_version per learning 2026-05-03) -if [[ ! -f "$CODEX_DIR/SKILL.md" ]]; then - cat > "$CODEX_DIR/SKILL.md" <<EOF ---- -name: $SKILL_NAME -description: <copy from skills/$SKILL_NAME/SKILL.md description> ---- - -# /$SKILL_NAME - -See \`skills/$SKILL_NAME/SKILL.md\` for the canonical specification. - -## Codex Execution Profile - -See \`prompt.md\` in this directory. -EOF -fi - -# Always trim skill_api_version from codex SKILL.md if present -if grep -q "^skill_api_version:" "$CODEX_DIR/SKILL.md"; then - sed -i.bak '/^skill_api_version:/d' "$CODEX_DIR/SKILL.md" && rm -f "$CODEX_DIR/SKILL.md.bak" -fi - -# Hand-build prompt.md -if [[ ! -f "$CODEX_DIR/prompt.md" ]]; then - cat > "$CODEX_DIR/prompt.md" <<EOF -# Execution Profile: $SKILL_NAME - -You are running /$SKILL_NAME. - -See \`SKILL.md\` in this directory for full specification, OR -read \`skills/$SKILL_NAME/SKILL.md\` in the host repo for the canonical document. - -Workflow: -1. Read the user's request -2. Apply the skill's Workflow section -3. Produce output per the Output Specification -4. Self-check against the Quality Rubric -EOF -fi - -# --- Build report -------------------------------------------------------- -BUILD_REPORT="$REPO_ROOT/.agents/audits/${SKILL_NAME}-build.json" -mkdir -p "$(dirname "$BUILD_REPORT")" -case "$MODE" in - --interactive) REPORT_MODE="from-scratch" ;; - --like-flag-mode) REPORT_MODE="from-template" ;; - --absorb) REPORT_MODE="absorb-external" ;; -esac -cat > "$BUILD_REPORT" <<EOF -{ - "mode": "$REPORT_MODE", - "skill_name": "$SKILL_NAME", - "profile_id": "${SKILL_CONFORMANCE_PROFILE_ID:-repo-runtime}", - "files_created": [ - "skills/$SKILL_NAME/SKILL.md", - "skills/$SKILL_NAME/scripts/validate.sh", - "skills-codex/$SKILL_NAME/SKILL.md", - "skills-codex/$SKILL_NAME/prompt.md" - ], - "audit_pass": null, - "warnings": ["v1 skeleton — manual content fill required for description, constraints, workflow"] +path = Path(sys.argv[1]) +mode = {"--scratch": "from-scratch", "--template": "from-template", "--external": "absorb-external"}[sys.argv[2]] +payload = { + "mode": mode, + "skill_name": sys.argv[3], + "files_created": [f"skills/{sys.argv[3]}/SKILL.md", f"skills/{sys.argv[3]}/scripts/validate.sh"], + "structure_check_pass": False, } -EOF +if sys.argv[4]: + payload["source_hint"] = sys.argv[4] +path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8") +PY -# --- New-skill plumbing (ag-cw2y): make the scaffold one-shot-green ---------- -# The local/CI gates that silently tripped /burndown #600 are pre-empted here: -# 1. Dispositions row — else heal.sh Check 12 (MISSING_DISPOSITION). -if [[ -f "$REPO_ROOT/scripts/append-skill-disposition.sh" ]]; then - bash "$REPO_ROOT/scripts/append-skill-disposition.sh" "$SKILL_NAME" "$REPO_ROOT" \ - || echo "init.sh: WARN could not append dispositions row — add one manually" >&2 -fi -# 2. Narrative skill counts — --fix-counts bumps the "N checked-in skills" tokens -# in the domain-map + bdd Gherkin so the new skill doesn't trip registry-drift. -if [[ -x "$REPO_ROOT/scripts/check-registry-drift.sh" ]]; then - bash "$REPO_ROOT/scripts/check-registry-drift.sh" --fix-counts >/dev/null 2>&1 \ - || echo "init.sh: WARN registry-drift --fix-counts could not run — bump counts manually" >&2 -fi -# 3. Codex override catalog entry — else validate-codex-override-coverage fails -# ("source skill missing from Codex catalog"). Default parity_only (derived). -if [[ -f "$REPO_ROOT/scripts/append-codex-override-entry.sh" ]]; then - bash "$REPO_ROOT/scripts/append-codex-override-entry.sh" "$SKILL_NAME" "$REPO_ROOT" \ - || echo "init.sh: WARN could not add codex override catalog entry — add one manually" >&2 -fi -# 4. registry.json SKU catalog — else contracts-sync + correctness(ubuntu) BOTH -# fail ("registry.json is stale" / "SKU_CATALOG: DRIFT"). This is the 5th -# one-shot-green surface ag-cw2y missed; it cost /burndown #600 a 2nd -# fix-and-repush (ag-ekyq). MUST run last — it scans the whole skills/ tree, -# so the new skeleton must already exist on disk. -if [[ -f "$REPO_ROOT/scripts/generate-registry.sh" ]]; then - bash "$REPO_ROOT/scripts/generate-registry.sh" >/dev/null 2>&1 \ - || echo "init.sh: WARN could not regen registry.json — run scripts/generate-registry.sh manually" >&2 -fi - -echo "init.sh: created skill skeleton at $NEW_DIR" -echo "init.sh: codex parity at $CODEX_DIR" -echo "init.sh: build report at $BUILD_REPORT" -echo "init.sh: dispositions row + narrative counts scaffolded (refine the placeholder row)" +echo "init.sh: created $target" diff --git a/skills/skill-builder/scripts/scan_descriptions.py b/skills/skill-builder/scripts/scan_descriptions.py index 1c9c28e31..5c5e6732e 100644 --- a/skills/skill-builder/scripts/scan_descriptions.py +++ b/skills/skill-builder/scripts/scan_descriptions.py @@ -11,7 +11,7 @@ each skill that lacks one. Discovery in the runtime is pure LLM reasoning over the `description` field, so a missing trigger phrase is a material skill-selection risk, not cosmetic. See -`skills/skill-builder/references/skill-authoring-standard.md`. +`skills/skill-builder/SKILL.md`. Usage: python3 scan_descriptions.py [SKILLS_DIR] [--json] [--strict] [--quiet] diff --git a/skills/skill-builder/scripts/validate.sh b/skills/skill-builder/scripts/validate.sh index b83b11afb..9d98e2c87 100755 --- a/skills/skill-builder/scripts/validate.sh +++ b/skills/skill-builder/scripts/validate.sh @@ -1,41 +1,30 @@ #!/usr/bin/env bash -# validate.sh — self-validation for skill-builder set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SKILL_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" REPO_ROOT="$(cd "$SKILL_DIR/../.." && pwd)" -# Run heal-skill structural check on ourselves by exit code. bash "$REPO_ROOT/skills/heal-skill/scripts/heal.sh" --check --strict "$SKILL_DIR" -# Verify required artifacts exist -for f in SKILL.md scripts/build.sh scripts/init.sh references/skill-template.md schemas/build-report.json; do - [[ -f "$SKILL_DIR/$f" ]] || { echo "validate.sh: missing $SKILL_DIR/$f" >&2; exit 1; } -done - -# Verify scale-factory lessons stay encoded. -for phrase in \ - "heal-skill --check --strict" \ - "One skill directory = one writer" \ - "git status" \ - "Clean-room includes names" \ - "Workflow tool"; do - grep -q "$phrase" "$SKILL_DIR/SKILL.md" "$SKILL_DIR/references/agentops-skill-factory.md" || { - echo "validate.sh: missing scale-factory lesson: $phrase" >&2 +for path in SKILL.md scripts/build.sh scripts/init.sh schemas/build-report.json; do + [[ -f "$SKILL_DIR/$path" ]] || { + echo "skill-builder validate: missing $path" >&2 exit 1 } done -# Verify SKILL.md is within churn budget -LINES="$(wc -l < "$SKILL_DIR/SKILL.md")" -if (( LINES > 250 )); then - echo "validate.sh: SKILL.md is $LINES lines (>250 budget per finding f-2026-05-01-025)" >&2 +for script in scripts/build.sh scripts/init.sh; do + [[ -x "$SKILL_DIR/$script" ]] || { + echo "skill-builder validate: not executable: $script" >&2 + exit 1 + } +done + +if rg -n 'from-pattern|flywheel close-loop|append-skill-disposition' "$SKILL_DIR/SKILL.md" \ + || rg -n 'git (status|commit|push)|ao land|retry|queue|lease' \ + "$SKILL_DIR/scripts/build.sh" "$SKILL_DIR/scripts/init.sh"; then + echo "skill-builder validate: obsolete lifecycle behavior remains" >&2 exit 1 fi -# Verify build.sh and init.sh are executable -for s in scripts/build.sh scripts/init.sh; do - [[ -x "$SKILL_DIR/$s" ]] || chmod +x "$SKILL_DIR/$s" -done - -echo "validate.sh: skill-builder PASS ($LINES lines, all artifacts present)" +echo "skill-builder validate: PASS" diff --git a/skills/standards/references/test-pyramid.md b/skills/standards/references/test-pyramid.md index b789a8ce9..1053c24e7 100644 --- a/skills/standards/references/test-pyramid.md +++ b/skills/standards/references/test-pyramid.md @@ -1,6 +1,6 @@ # AI-Native Test Shape — L0 through L7 -> Shared reference for RPI lifecycle skills. Loaded by `/discovery`, `/plan`, `/premortem`, `/implement`, `/crank`, `/validate`, and `/postmortem`. +> Optional engineering reference for Plan, Implement, and Validate. It does not add phases or continuation authority. ## The AI-Native Test Shape @@ -76,9 +76,9 @@ The Traditional Pyramid The AI-Native Shape | RPI Phase | Test Levels | What Happens | |-----------|-------------|--------------| -| **Discovery** (`/discovery`, `/plan`) | L0–L3 scoping | Plan identifies which test levels apply. **Default: L2 required for all code-change issues.** Issues include `test_level` metadata. | +| **Plan** (`/plan`) | L0–L3 scoping | Plan identifies which checks prove the active behavior and names the first acceptance check. | | **Premortem** (`/premortem`) | L0–L3 coverage check | Validates plan covers appropriate test levels. **Flags L1-only coverage as WARN.** | -| **Implementation** (`/implement`, `/crank`) | L0–L2 writing + execution | **TDD writes L2 integration tests first (RED).** L0 contracts from specs. L1 as regression guards. | +| **Implementation** (`/implement`) | L0–L2 writing + execution | Behavior-changing work starts with the bounded acceptance check RED, then GREEN and refactor. | | **Validation** (`/validate`, `/postmortem`) | L0–L3 coverage audit | Assesses test coverage. **L2+ coverage weighted 3-5x over L1 in vibe scoring.** | ## Test Level Selection Guide @@ -125,7 +125,7 @@ When creating issues in `/plan`, include test level metadata: ## Bug-Finding Levels (Agent-Autonomous) > **Proven 2026-03-14 on jren-cm:** 3,321 L1 unit tests found 0 new bugs. These levels found 8. -> Evidence: `/Users/fullerbt/gt/jren_cm/crew/ichigo/scripts/.agents/council/2026-03-14-post-mortem-full-session-methodology.md` +> Evidence: `/Users/fullerbt/gt/jren_cm/crew/ichigo/scripts/.agents/council/2026-03-14-postmortem-full-session-methodology.md` L0–L3 are the **coverage shape** — they verify code works as designed. These are the **bug-finding shape** — they find bugs the coverage shape misses. diff --git a/skills/status/SKILL.md b/skills/status/SKILL.md index 4bc27195b..d0408da99 100644 --- a/skills/status/SKILL.md +++ b/skills/status/SKILL.md @@ -20,7 +20,7 @@ metadata: disposition: keep_specialist graph_root: true tier: session - dependencies: [sbh] + dependencies: [] output_contract: read-only status snapshot --- diff --git a/skills/swarm/scripts/dispatch_once.py b/skills/swarm/scripts/dispatch_once.py index 7d1b755f8..14172ba50 100644 --- a/skills/swarm/scripts/dispatch_once.py +++ b/skills/swarm/scripts/dispatch_once.py @@ -34,7 +34,30 @@ def _includes(packet: Mapping[str, Any]) -> tuple[str, ...]: def _overlap(left: str, right: str) -> bool: - return left == right or left.startswith(right + "/") or right.startswith(left + "/") + """Conservatively decide whether two include patterns may intersect. + + Literal sibling paths are provably disjoint. For globs, the literal path + prefix before the first wildcard must itself be disjoint; otherwise the + adapter rejects the batch instead of guessing about a shared write surface. + """ + + def literal_prefix(pattern: str) -> str: + parts: list[str] = [] + for part in PurePosixPath(pattern).parts: + if any(character in part for character in "*?["): + break + parts.append(part) + return PurePosixPath(*parts).as_posix() if parts else "." + + left_prefix = literal_prefix(left) + right_prefix = literal_prefix(right) + if left_prefix == "." or right_prefix == ".": + return True + return ( + left_prefix == right_prefix + or left_prefix.startswith(right_prefix + "/") + or right_prefix.startswith(left_prefix + "/") + ) def dispatch_once( diff --git a/skills/swarm/tests/test_dispatch_once.py b/skills/swarm/tests/test_dispatch_once.py index 740477f97..1fc90bf50 100644 --- a/skills/swarm/tests/test_dispatch_once.py +++ b/skills/swarm/tests/test_dispatch_once.py @@ -57,6 +57,36 @@ class DispatchOnceTests(unittest.TestCase): ) self.assertEqual(calls, 0) + def test_overlapping_glob_scopes_fail_before_dispatch(self) -> None: + calls = 0 + + def executor(_value: dict) -> None: + nonlocal calls + calls += 1 + + with self.assertRaisesRegex(ValueError, "write scopes overlap"): + MODULE.dispatch_once( + [packet("a", "src/**"), packet("b", "src/lib/**")], executor + ) + self.assertEqual(calls, 0) + + def test_disjoint_glob_prefixes_dispatch(self) -> None: + calls: list[str] = [] + + MODULE.dispatch_once( + [packet("a", "src/a/**"), packet("b", "src/b/**")], + lambda value: calls.append(value["packet_id"]), + ) + + self.assertEqual(calls, ["a", "b"]) + + def test_uncertain_glob_overlap_is_rejected(self) -> None: + with self.assertRaisesRegex(ValueError, "write scopes overlap"): + MODULE.dispatch_once( + [packet("a", "src/*/generated"), packet("b", "src/*/manual")], + lambda _value: None, + ) + if __name__ == "__main__": unittest.main() diff --git a/skills/test/SKILL.md b/skills/test/SKILL.md index 61e63014e..1f624ce0f 100644 --- a/skills/test/SKILL.md +++ b/skills/test/SKILL.md @@ -27,8 +27,7 @@ metadata: canonical_status: canonical disposition: keep_specialist tier: execution - dependencies: - - standards + dependencies: [] output_contract: test files, coverage report --- # Test Skill diff --git a/skills/toil-mining/SKILL.md b/skills/toil-mining/SKILL.md index 2f298ab30..5ce20b2e0 100644 --- a/skills/toil-mining/SKILL.md +++ b/skills/toil-mining/SKILL.md @@ -27,8 +27,7 @@ metadata: canonical_status: canonical disposition: keep_specialist tier: meta - dependencies: - - automation-shape-routing + dependencies: [] stability: experimental output_contract: ranked evidence report under .agents/toil-mining/ --- diff --git a/skills/validate/scripts/test_validate.py b/skills/validate/scripts/test_validate.py index cd6e73307..c593002df 100755 --- a/skills/validate/scripts/test_validate.py +++ b/skills/validate/scripts/test_validate.py @@ -48,6 +48,17 @@ class ValidateV2Tests(unittest.TestCase): subject.write_text("two", encoding="utf-8") self.assertFalse(tool.verify_manifest(manifest, root, None)[0]) + def test_git_metadata_is_not_identity_bearing(self): + with tempfile.TemporaryDirectory() as raw: + root = Path(raw) + (root / "value").write_text("same", encoding="utf-8") + first = tool.build_manifest(root, ["."], [], git_metadata={"commit": "one"}) + second = tool.build_manifest(root, ["."], [], git_metadata={"commit": "two"}) + self.assertEqual(first["canonical_manifest_digest"], second["canonical_manifest_digest"]) + self.assertNotEqual(first["git_metadata"], second["git_metadata"]) + self.assertTrue(tool.verify_manifest(first, root, None)[0]) + self.assertTrue(tool.verify_manifest(second, root, None)[0]) + def test_symlink_and_deletion_identity(self): with tempfile.TemporaryDirectory() as raw: root = Path(raw) @@ -120,6 +131,20 @@ class ValidateV2Tests(unittest.TestCase): self.assertFalse(existed) self.assert_schema_valid(replacement) + def test_incomplete_draft_is_rejected_without_writing(self): + with tempfile.TemporaryDirectory() as raw: + with self.assertRaisesRegex(tool.ContractError, "missing required fields"): + tool.store_verdict({"verdict": "FAIL"}, Path(raw)) + self.assertEqual(list(Path(raw).iterdir()), []) + + def test_unknown_field_is_rejected_without_writing(self): + with tempfile.TemporaryDirectory() as raw: + draft = self.draft() + draft["next_action"] = "repair" + with self.assertRaisesRegex(tool.ContractError, "unknown fields"): + tool.store_verdict(draft, Path(raw)) + self.assertEqual(list(Path(raw).iterdir()), []) + if __name__ == "__main__": unittest.main() diff --git a/skills/validate/scripts/validate.py b/skills/validate/scripts/validate.py index 24606e76e..e0980541e 100755 --- a/skills/validate/scripts/validate.py +++ b/skills/validate/scripts/validate.py @@ -8,6 +8,7 @@ delivery integration. It operates only on explicit files and directories. from __future__ import annotations import argparse +from datetime import datetime import fnmatch import hashlib import json @@ -144,7 +145,7 @@ def build_manifest( manifest["entries"] = sorted(manifest["entries"] + deletions, key=lambda item: item["path"]) if git_metadata: manifest["git_metadata"] = git_metadata - manifest["canonical_manifest_digest"] = digest_value(manifest) + manifest["canonical_manifest_digest"] = digest_value(manifest_identity(manifest)) return manifest @@ -152,10 +153,22 @@ def valid_digest(value: Any) -> bool: return isinstance(value, str) and len(value) == 64 and all(ch in HEX64 for ch in value) +def manifest_identity(manifest: dict[str, Any]) -> dict[str, Any]: + """Return only the fields that identify subject content. + + ``git_metadata`` is intentionally descriptive. Supplying or changing it + must never change the identity of otherwise identical content. + """ + return { + key: value + for key, value in manifest.items() + if key not in {"canonical_manifest_digest", "git_metadata"} + } + + def verify_manifest(manifest: dict[str, Any], root: Path, base_manifest: dict[str, Any] | None) -> tuple[bool, str]: claimed = manifest.get("canonical_manifest_digest") - unsigned = {key: value for key, value in manifest.items() if key != "canonical_manifest_digest"} - if not valid_digest(claimed) or digest_value(unsigned) != claimed: + if not valid_digest(claimed) or digest_value(manifest_identity(manifest)) != claimed: return False, "manifest canonical digest is invalid" rebuilt = build_manifest( root, @@ -244,6 +257,106 @@ def enforce_identity(draft: dict[str, Any]) -> dict[str, Any]: return draft +VERDICT_KEYS = { + "schema_version", + "acceptance_digest", + "subject_manifest_digest", + "author_context_id", + "validator_context_id", + "freshness_attestation", + "verdict", + "criteria", + "findings", + "evidence_refs", + "checked", + "not_checked", + "validated_at", + "artifact_digest", +} + + +def require_string_list(value: Any, field: str, *, nonempty: bool = False) -> None: + if not isinstance(value, list) or (nonempty and not value): + raise ContractError(f"verdict.v2 {field} must be a{' nonempty' if nonempty else ''} array") + if any(not isinstance(item, str) or not item for item in value): + raise ContractError(f"verdict.v2 {field} entries must be nonempty strings") + + +def validate_verdict_v2(artifact: dict[str, Any]) -> None: + """Enforce the complete bundled verdict.v2 contract before persistence.""" + missing = sorted(VERDICT_KEYS - artifact.keys()) + extra = sorted(artifact.keys() - VERDICT_KEYS) + if missing: + raise ContractError(f"verdict.v2 missing required fields: {', '.join(missing)}") + if extra: + raise ContractError(f"verdict.v2 contains unknown fields: {', '.join(extra)}") + if artifact["schema_version"] != "verdict.v2": + raise ContractError("verdict.v2 schema_version must be verdict.v2") + for field in ("acceptance_digest", "subject_manifest_digest", "artifact_digest"): + if not valid_digest(artifact[field]): + raise ContractError(f"verdict.v2 {field} must be a lowercase SHA-256 digest") + expected_digest = digest_value({key: value for key, value in artifact.items() if key != "artifact_digest"}) + if artifact["artifact_digest"] != expected_digest: + raise ContractError("verdict.v2 artifact_digest does not match canonical JSON") + for field in ("author_context_id", "validator_context_id"): + if artifact[field] is not None and (not isinstance(artifact[field], str) or not artifact[field]): + raise ContractError(f"verdict.v2 {field} must be null or a nonempty string") + freshness = artifact["freshness_attestation"] + if freshness is not None: + if not isinstance(freshness, dict) or set(freshness) != {"source", "attester_identity"}: + raise ContractError("verdict.v2 freshness_attestation has invalid fields") + if freshness["source"] not in {"runtime", "caller"}: + raise ContractError("verdict.v2 freshness source must be runtime or caller") + if not isinstance(freshness["attester_identity"], str) or not freshness["attester_identity"]: + raise ContractError("verdict.v2 freshness attester_identity must be nonempty") + if artifact["verdict"] not in {"PASS", "FAIL", "NOT_PROVEN"}: + raise ContractError("verdict.v2 verdict must be PASS, FAIL, or NOT_PROVEN") + criteria = artifact["criteria"] + if not isinstance(criteria, list) or not criteria: + raise ContractError("verdict.v2 criteria must be a nonempty array") + for index, criterion in enumerate(criteria): + allowed = {"id", "result", "evidence_refs", "reason"} + if not isinstance(criterion, dict) or not {"id", "result", "evidence_refs"}.issubset(criterion) or not set(criterion).issubset(allowed): + raise ContractError(f"verdict.v2 criteria[{index}] has invalid fields") + if not isinstance(criterion["id"], str) or not criterion["id"]: + raise ContractError(f"verdict.v2 criteria[{index}].id must be nonempty") + if criterion["result"] not in {"PASS", "FAIL", "NOT_PROVEN"}: + raise ContractError(f"verdict.v2 criteria[{index}].result is invalid") + require_string_list(criterion["evidence_refs"], f"criteria[{index}].evidence_refs") + if "reason" in criterion and not isinstance(criterion["reason"], str): + raise ContractError(f"verdict.v2 criteria[{index}].reason must be a string") + findings = artifact["findings"] + if not isinstance(findings, list): + raise ContractError("verdict.v2 findings must be an array") + for index, finding in enumerate(findings): + if not isinstance(finding, dict) or set(finding) != {"id", "summary", "evidence_refs"}: + raise ContractError(f"verdict.v2 findings[{index}] has invalid fields") + if not isinstance(finding["id"], str) or not finding["id"]: + raise ContractError(f"verdict.v2 findings[{index}].id must be nonempty") + if not isinstance(finding["summary"], str) or not finding["summary"]: + raise ContractError(f"verdict.v2 findings[{index}].summary must be nonempty") + require_string_list(finding["evidence_refs"], f"findings[{index}].evidence_refs", nonempty=True) + for field in ("evidence_refs", "checked", "not_checked"): + require_string_list(artifact[field], field) + if not isinstance(artifact["validated_at"], str): + raise ContractError("verdict.v2 validated_at must be an RFC3339 date-time") + try: + timestamp = datetime.fromisoformat(artifact["validated_at"].replace("Z", "+00:00")) + except ValueError as exc: + raise ContractError("verdict.v2 validated_at must be an RFC3339 date-time") from exc + if timestamp.tzinfo is None: + raise ContractError("verdict.v2 validated_at must include a timezone") + if artifact["verdict"] == "PASS": + author = artifact["author_context_id"] + validator = artifact["validator_context_id"] + if not author or not validator or author == validator or freshness is None: + raise ContractError("verdict.v2 PASS requires distinct identities and freshness attestation") + if any(criterion["result"] != "PASS" for criterion in criteria): + raise ContractError("verdict.v2 PASS requires every criterion to PASS") + if artifact["not_checked"]: + raise ContractError("verdict.v2 PASS cannot contain not_checked items") + + def artifact_bytes(draft: dict[str, Any]) -> tuple[dict[str, Any], bytes]: unsigned = {key: value for key, value in draft.items() if key != "artifact_digest"} digest = digest_value(unsigned) @@ -281,10 +394,12 @@ def store_verdict(draft: dict[str, Any], destination: Path) -> tuple[dict[str, A draft = enforce_identity(draft) draft["schema_version"] = "verdict.v2" artifact, payload = artifact_bytes(draft) + validate_verdict_v2(artifact) try: path, existed = atomic_store(artifact, payload, destination) except ContractError as exc: artifact, payload = artifact_bytes(add_integrity_finding(draft, str(exc))) + validate_verdict_v2(artifact) path, existed = atomic_store(artifact, payload, destination) return artifact, path, existed diff --git a/skills/validate/scripts/validate.sh b/skills/validate/scripts/validate.sh old mode 100644 new mode 100755 diff --git a/tests/canaries/agentops-core-official.txt b/tests/canaries/agentops-core-official.txt index 1bf8ddfde..8bad83a44 100644 --- a/tests/canaries/agentops-core-official.txt +++ b/tests/canaries/agentops-core-official.txt @@ -10,7 +10,7 @@ evals/agentops-core/cli-contracts.json evals/agentops-core/context-packet-ab-wave0.json evals/agentops-core/distribution-install-update.json evals/agentops-core/eval-runner-engine-control-plane.json -evals/agentops-core/pre-push-gate-governance.json +evals/agentops-core/rpi-behavior.json evals/agentops-core/release-security-gates.json evals/agentops-core/retrieval-quality-gates.json evals/agentops-core/runtime-smoke-matrix.json diff --git a/tests/claude-code/test-post-mortem-skill.sh b/tests/claude-code/test-post-mortem-skill.sh deleted file mode 100755 index e04f40d69..000000000 --- a/tests/claude-code/test-post-mortem-skill.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env bash -# Test: post-mortem skill -# Verifies the post-mortem validation skill works correctly -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -source "$SCRIPT_DIR/test-helpers.sh" -export MAX_TURNS=6 - -echo "=== Test: post-mortem skill ===" -echo "" - -# Test 1: Verify skill is recognized -echo "Test 1: Skill recognition..." - -output=$(run_claude "Answer concisely without running tools: what is the /agentops:post-mortem skill in this plugin?" 60) - -if assert_contains "$output" "post-mortem\|postmortem" "Skill name recognized"; then - : -else - exit 1 -fi - -if assert_contains "$output" "validat\|learn\|retro\|wrap" "Describes post-implementation review"; then - : -else - exit 1 -fi - -echo "" - -# Test 2: Verify combined workflow -echo "Test 2: Combined workflow..." - -output=$(run_claude "Answer concisely without running tools: what does /agentops:post-mortem combine and run?" 60) - -if assert_contains "$output" "retro\|vibe\|security\|extract" "Mentions combined workflows"; then - : -else - exit 1 -fi - -echo "" - -echo "=== All post-mortem skill tests passed ===" diff --git a/tests/claude-code/test-postmortem-skill.sh b/tests/claude-code/test-postmortem-skill.sh new file mode 100755 index 000000000..3a72fb07e --- /dev/null +++ b/tests/claude-code/test-postmortem-skill.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# Test: postmortem skill +# Verifies the optional retrospective causal-analysis skill is recognized +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +source "$SCRIPT_DIR/test-helpers.sh" +export MAX_TURNS=6 + +echo "=== Test: postmortem skill ===" +echo "" + +# Test 1: Verify skill is recognized +echo "Test 1: Skill recognition..." + +output=$(run_claude "Answer concisely without running tools: what is the /agentops:postmortem skill in this plugin?" 60) + +if assert_contains "$output" "postmortem" "Skill name recognized"; then + : +else + exit 1 +fi + +if assert_contains "$output" "causal\|retrospect\|evidence\|counterfactual" "Describes retrospective causal analysis"; then + : +else + exit 1 +fi + +echo "" + +# Test 2: Verify the non-gating boundary +echo "Test 2: Non-gating boundary..." + +output=$(run_claude "Answer concisely without running tools: what authority does /agentops:postmortem deliberately not have?" 60) + +if assert_contains "$output" "gate\|proof\|plan\|tracker\|delivery\|promot" "Names its non-authority"; then + : +else + exit 1 +fi + +echo "" + +echo "=== All postmortem skill tests passed ===" diff --git a/tests/claude-code/test-pre-mortem-skill.sh b/tests/claude-code/test-premortem-skill.sh similarity index 68% rename from tests/claude-code/test-pre-mortem-skill.sh rename to tests/claude-code/test-premortem-skill.sh index 49d2a701c..1788fb060 100755 --- a/tests/claude-code/test-pre-mortem-skill.sh +++ b/tests/claude-code/test-premortem-skill.sh @@ -1,21 +1,21 @@ #!/usr/bin/env bash -# Test: pre-mortem skill -# Verifies the pre-mortem simulation skill works correctly +# Test: premortem skill +# Verifies the premortem simulation skill works correctly set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" source "$SCRIPT_DIR/test-helpers.sh" export MAX_TURNS=6 -echo "=== Test: pre-mortem skill ===" +echo "=== Test: premortem skill ===" echo "" # Test 1: Verify skill is recognized echo "Test 1: Skill recognition..." -output=$(run_claude "Answer concisely without running tools: what is the pre-mortem skill in this plugin?" 60) +output=$(run_claude "Answer concisely without running tools: what is the premortem skill in this plugin?" 60) -if assert_contains "$output" "pre-mortem\|premortem" "Skill name recognized"; then +if assert_contains "$output" "premortem" "Skill name recognized"; then : else exit 1 @@ -32,7 +32,7 @@ echo "" # Test 2: Verify iteration simulation echo "Test 2: Iteration simulation..." -output=$(run_claude "Answer concisely without running tools: how does the pre-mortem skill simulate failures, and what does it iterate?" 60) +output=$(run_claude "Answer concisely without running tools: how does the premortem skill simulate failures, and what does it iterate?" 60) if assert_contains "$output" "iterat\|simulat\|implementation\|mode\|judge\|council\|review\|scenario" "Mentions simulation iterations"; then : @@ -42,4 +42,4 @@ fi echo "" -echo "=== All pre-mortem skill tests passed ===" +echo "=== All premortem skill tests passed ===" diff --git a/tests/docs/mkdocs-strict-allowlist.txt b/tests/docs/mkdocs-strict-allowlist.txt index ac2f80ce1..dd41574b2 100644 --- a/tests/docs/mkdocs-strict-allowlist.txt +++ b/tests/docs/mkdocs-strict-allowlist.txt @@ -22,8 +22,8 @@ WARNING - Doc file 'GLOSSARY.md' contains a link '../skills/council/SKILL.md', WARNING - Doc file 'GLOSSARY.md' contains a link '../skills/crank/SKILL.md', but the target is not found among documentation files. WARNING - Doc file 'GLOSSARY.md' contains a link '../skills/handoff/SKILL.md', but the target is not found among documentation files. WARNING - Doc file 'GLOSSARY.md' contains a link '../skills/operationalize/SKILL.md', but the target is not found among documentation files. -WARNING - Doc file 'GLOSSARY.md' contains a link '../skills/post-mortem/SKILL.md', but the target is not found among documentation files. -WARNING - Doc file 'GLOSSARY.md' contains a link '../skills/pre-mortem/SKILL.md', but the target is not found among documentation files. +WARNING - Doc file 'GLOSSARY.md' contains a link '../skills/postmortem/SKILL.md', but the target is not found among documentation files. +WARNING - Doc file 'GLOSSARY.md' contains a link '../skills/premortem/SKILL.md', but the target is not found among documentation files. WARNING - Doc file 'GLOSSARY.md' contains a link '../skills/research/SKILL.md', but the target is not found among documentation files. WARNING - Doc file 'GLOSSARY.md' contains a link '../skills/standards/SKILL.md', but the target is not found among documentation files. WARNING - Doc file 'GLOSSARY.md' contains a link '../skills/swarm/SKILL.md', but the target is not found among documentation files. @@ -41,8 +41,8 @@ WARNING - Doc file 'architecture/workflow-conformance-pattern.md' contains a li WARNING - Doc file 'architecture/workflow-conformance-pattern.md' contains a link '../../skills/workflow-builder/SKILL.md', but the target '../skills/workflow-builder/SKILL.md' is not found among documentation files. WARNING - Doc file 'comparisons/vs-hosted-code-review.md' contains a link '../../README.md#the-honest-version', but the target '../README.md' is not found among documentation files. WARNING - Doc file 'context-lifecycle.md' contains a link '../skills/council/SKILL.md', but the target is not found among documentation files. -WARNING - Doc file 'context-lifecycle.md' contains a link '../skills/post-mortem/SKILL.md', but the target is not found among documentation files. -WARNING - Doc file 'context-lifecycle.md' contains a link '../skills/pre-mortem/SKILL.md', but the target is not found among documentation files. +WARNING - Doc file 'context-lifecycle.md' contains a link '../skills/postmortem/SKILL.md', but the target is not found among documentation files. +WARNING - Doc file 'context-lifecycle.md' contains a link '../skills/premortem/SKILL.md', but the target is not found among documentation files. WARNING - Doc file 'context-lifecycle.md' contains a link '../skills/validate/SKILL.md', but the target is not found among documentation files. WARNING - Doc file 'contracts/corpus-learning-seam.md' contains a link '../../schemas/learning.v1.schema.json', but the target '../schemas/learning.v1.schema.json' is not found among documentation files. WARNING - Doc file 'contracts/pawls.md' contains a link '../../scripts/evolve/halt-check.sh', but the target '../scripts/evolve/halt-check.sh' is not found among documentation files. diff --git a/tests/docs/validate-doc-release.sh b/tests/docs/validate-doc-release.sh index 9e0a7bbe3..0dcc6aa20 100755 --- a/tests/docs/validate-doc-release.sh +++ b/tests/docs/validate-doc-release.sh @@ -139,7 +139,6 @@ validate_message_freeze() { run_check "Link validation" bash "$REPO_ROOT/tests/docs/validate-links.sh" run_check "Skill count validation" bash "$REPO_ROOT/tests/docs/validate-skill-count.sh" -run_check "CLI skills map validation" bash "$REPO_ROOT/scripts/validate-cli-skills-map.sh" run_check "Release message freeze validation" validate_message_freeze if [[ "$errors" -gt 0 ]]; then diff --git a/tests/e2e/README.md b/tests/e2e/README.md index a8efe5da4..6b1c5803b 100644 --- a/tests/e2e/README.md +++ b/tests/e2e/README.md @@ -24,7 +24,7 @@ cite → promote → lookup → feedback → nightly across one isolated sandbox bash tests/e2e/proof-run.sh ``` -Other scripts (`goals-*.sh`, `rpi-phased-domain.sh`, `closure-integrity-grace.sh`, +Other scripts (`goals-*.sh`, `rpi-phased-domain.sh`, …) follow the same harness contract and can be run the same way. --- @@ -154,7 +154,6 @@ prints a `[e2e-guard] WARNING:` line to stderr. | `rpi-phased-domain.sh` | dispatch → phase trace | F3 | | `goals-trace-chain.sh` | trace → dependency assert | F4 | | `goals-steer-auto.sh` | steer → re-prioritize | F5 | -| `closure-integrity-grace.sh` | citation → grace period → closure invariant | citation flow regressions | | `factory-operator-canary.sh` | factory admission → operator action | factory pipeline contract | Every script in this list is mock-free **today** — this file is the contract diff --git a/tests/e2e/closure-integrity-grace.sh b/tests/e2e/closure-integrity-grace.sh deleted file mode 100755 index 45ac6ef81..000000000 --- a/tests/e2e/closure-integrity-grace.sh +++ /dev/null @@ -1,508 +0,0 @@ -#!/usr/bin/env bash -# Regression test: close-before-commit grace window + br (beads_rust) semantics. -# Verifies that a bead closed BEFORE its qualifying commit still passes when the -# commit lands within the 24h grace window, and that the audit reads children -# from `br show <epic> --json` .dependents (bd/Dolt retired). Also covers the -# single-epic-closure path: a closed epic with no children but commit-backed -# closure PASSES, an invalid no-child epic FAILS as collection_failed. -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" -AUDIT_SCRIPT="$REPO_ROOT/skills/postmortem/scripts/closure-integrity-audit.sh" -WORK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/closure-grace-XXXXXX")" -PASS=0 -FAIL=0 - -cleanup() { rm -rf "$WORK_DIR"; } -trap cleanup EXIT - -pass() { PASS=$((PASS + 1)); echo "PASS: $1"; } -fail() { FAIL=$((FAIL + 1)); echo "FAIL: $1"; } - -# Setup: isolated git repo with a br mock -BR_DIR="$WORK_DIR/br-data" -BIN_DIR="$WORK_DIR/bin" -REPO_DIR="$WORK_DIR/repo" -mkdir -p "$BR_DIR" "$BIN_DIR" "$REPO_DIR" - -# Mock br: serves `show <id> --json` (one-element array) and `show <id>` (human) -# from fixture files. The audit derives children from the epic's --json -# .dependents, so the epic fixture must carry a dependents array. -cat > "$BIN_DIR/br" <<'MOCK' -#!/usr/bin/env bash -case "$1" in - show) - id="$2" - if [[ "${3:-}" == "--json" ]]; then - cat "$BR_DIR/show-${id}.json" 2>/dev/null || echo '[]' - else - cat "$BR_DIR/show-${id}.txt" 2>/dev/null || echo "NOT FOUND" - fi - ;; -esac -MOCK -chmod +x "$BIN_DIR/br" -sed -i.bak "s|\$BR_DIR|$BR_DIR|g" "$BIN_DIR/br" 2>/dev/null || \ - sed -i '' "s|\$BR_DIR|$BR_DIR|g" "$BIN_DIR/br" - -export PATH="$BIN_DIR:$PATH" -export BR_DIR - -# Helper: write the epic fixture so its parent-child dependents are the given ids. -write_epic_with_children() { - local epic="$1"; shift - local deps="" - local sep="" - local id - for id in "$@"; do - deps+="${sep}{\"id\":\"${id}\",\"dependency_type\":\"parent-child\"}" - sep="," - done - cat > "$BR_DIR/show-${epic}.json" <<JSON -[{"id":"${epic}","issue_type":"epic","status":"closed","dependents":[${deps}]}] -JSON -} - -# Initialize isolated repo -( - cd "$REPO_DIR" - git init -q - git config user.email "test@test.com" - git config user.name "Test" - echo "init" > README.md - git add README.md - git commit -q -m "init" -) - -# Create a file that the issue scopes to -SCOPED_FILE="cli/cmd/ao/fix.go" -mkdir -p "$REPO_DIR/cli/cmd/ao" - -# Scenario: bead closed at T, qualifying commit lands at T+2h (within grace) -CLOSE_TIME="2026-03-20T10:00:00+00:00" -COMMIT_TIME="2026-03-20T12:00:00+00:00" - -write_epic_with_children test-epic test-epic.1 - -cat > "$BR_DIR/show-test-epic.1.json" <<JSON -[{ - "id": "test-epic.1", - "status": "closed", - "created_at": "2026-03-19T10:00:00+00:00", - "closed_at": "$CLOSE_TIME", - "description": "Fix the handler logic.\n\nFiles:\n- \`$SCOPED_FILE\`" -}] -JSON - -# Create qualifying commit AFTER close time -( - cd "$REPO_DIR" - echo "package ao" > "$SCOPED_FILE" - git add "$SCOPED_FILE" - GIT_AUTHOR_DATE="$COMMIT_TIME" GIT_COMMITTER_DATE="$COMMIT_TIME" \ - git commit -q -m "fix: handler logic" -) - -# Test 1: Without grace, this would fail (commit is after closed_at) -# With grace, it should pass -result="$(cd "$REPO_DIR" && bash "$AUDIT_SCRIPT" --scope commit test-epic 2>&1)" -verdict="$(echo "$result" | jq -r '.children[0].status')" -detail="$(echo "$result" | jq -r '.children[0].detail')" - -if [[ "$verdict" == "pass" ]] && [[ "$detail" == *"grace window"* ]]; then - pass "close-before-commit detected via grace window" -else - fail "close-before-commit should pass via grace window (got status=$verdict detail=$detail)" -fi - -# Test 2: Commit way outside grace window (T+48h) should fail -( - cd "$REPO_DIR" - git reset --hard HEAD~1 -q - mkdir -p "$(dirname "$SCOPED_FILE")" - LATE_TIME="2026-03-22T10:00:00+00:00" - echo "package ao" > "$SCOPED_FILE" - git add "$SCOPED_FILE" - GIT_AUTHOR_DATE="$LATE_TIME" GIT_COMMITTER_DATE="$LATE_TIME" \ - git commit -q -m "fix: late handler logic" -) - -result="$(cd "$REPO_DIR" && bash "$AUDIT_SCRIPT" --scope commit test-epic 2>&1)" -verdict="$(echo "$result" | jq -r '.children[0].status')" -ftype="$(echo "$result" | jq -r '.failures[0].failure_type')" - -if [[ "$verdict" == "fail" ]] && [[ "$ftype" == "timing_miss" ]]; then - pass "commit outside grace window correctly classified as timing_miss" -else - fail "commit outside grace should be timing_miss (got status=$verdict failure_type=$ftype)" -fi - -# Test 3: Issue with no scoped files should be parser_miss -cat > "$BR_DIR/show-test-epic.1.json" <<JSON -[{ - "id": "test-epic.1", - "status": "closed", - "created_at": "2026-03-19T10:00:00+00:00", - "closed_at": "$CLOSE_TIME", - "description": "Fix the handler logic without specifying files." -}] -JSON - -result="$(cd "$REPO_DIR" && bash "$AUDIT_SCRIPT" --scope commit test-epic 2>&1)" -ftype="$(echo "$result" | jq -r '.failures[0].failure_type')" - -if [[ "$ftype" == "parser_miss" ]]; then - pass "missing scoped files correctly classified as parser_miss" -else - fail "missing scoped files should be parser_miss (got $ftype)" -fi - -# Test 4: Bead with no scoped files AND no evidence-only packet should FAIL -write_epic_with_children test-epic test-epic.2 - -cat > "$BR_DIR/show-test-epic.2.json" <<JSON -[{ - "id": "test-epic.2", - "status": "closed", - "created_at": "2026-03-19T10:00:00+00:00", - "closed_at": "$CLOSE_TIME", - "description": "Refactored internal logic with no specific files mentioned." -}] -JSON - -# Ensure no evidence-only packet exists -rm -rf "$REPO_DIR/.agents/releases/evidence-only-closures" "$REPO_DIR/.agents/council/evidence-only-closures" - -result="$(cd "$REPO_DIR" && bash "$AUDIT_SCRIPT" --scope auto test-epic 2>&1)" -verdict="$(echo "$result" | jq -r '.children[0].status')" -ftype="$(echo "$result" | jq -r '.failures[0].failure_type')" - -if [[ "$verdict" == "fail" ]] && [[ "$ftype" == "parser_miss" ]]; then - pass "no scoped files + no evidence-only packet correctly fails as parser_miss" -else - fail "no scoped files + no evidence-only packet should be parser_miss (got status=$verdict failure_type=$ftype)" -fi - -# Test 5: Bead with evidence-only packet but invalid schema should fall through -# to parser_miss (packet_is_valid rejects it). -write_epic_with_children test-epic test-epic.3 - -cat > "$BR_DIR/show-test-epic.3.json" <<JSON -[{ - "id": "test-epic.3", - "status": "closed", - "created_at": "2026-03-19T10:00:00+00:00", - "closed_at": "$CLOSE_TIME", - "description": "Policy-only closure with no code delta." -}] -JSON - -# Create an invalid evidence-only packet (missing required fields) -mkdir -p "$REPO_DIR/.agents/releases/evidence-only-closures" -cat > "$REPO_DIR/.agents/releases/evidence-only-closures/test-epic.3.json" <<JSON -{ - "target_id": "test-epic.3", - "evidence_mode": "invalid_mode", - "evidence": {"artifacts": []} -} -JSON - -result="$(cd "$REPO_DIR" && bash "$AUDIT_SCRIPT" --scope auto test-epic 2>&1)" -verdict="$(echo "$result" | jq -r '.children[0].status')" -ftype="$(echo "$result" | jq -r '.failures[0].failure_type')" - -if [[ "$verdict" == "fail" ]] && [[ "$ftype" == "parser_miss" ]]; then - pass "invalid evidence-only packet correctly falls through to parser_miss" -else - fail "invalid evidence-only packet should fall through to parser_miss (got status=$verdict failure_type=$ftype)" -fi - -# Test 6: Bead with expired grace window should FAIL -write_epic_with_children test-epic test-epic.1 - -EXPIRED_CLOSE="2026-03-15T10:00:00+00:00" -cat > "$BR_DIR/show-test-epic.1.json" <<JSON -[{ - "id": "test-epic.1", - "status": "closed", - "created_at": "2026-03-10T10:00:00+00:00", - "closed_at": "$EXPIRED_CLOSE", - "description": "Fix the handler logic.\n\nFiles:\n- \`$SCOPED_FILE\`" -}] -JSON - -# Reset repo - commit is at 2026-03-20T12:00:00, close was 2026-03-15 (5 days before commit, well outside 24h grace) -( - cd "$REPO_DIR" - # Remove any evidence-only packets - rm -rf .agents - git reset --hard HEAD~1 -q 2>/dev/null || true - mkdir -p "$(dirname "$SCOPED_FILE")" - LATE_TIME="2026-03-20T12:00:00+00:00" - echo "package ao" > "$SCOPED_FILE" - git add "$SCOPED_FILE" - GIT_AUTHOR_DATE="$LATE_TIME" GIT_COMMITTER_DATE="$LATE_TIME" \ - git commit -q -m "fix: handler logic" -) - -result="$(cd "$REPO_DIR" && bash "$AUDIT_SCRIPT" --scope commit test-epic 2>&1)" -verdict="$(echo "$result" | jq -r '.children[0].status')" -ftype="$(echo "$result" | jq -r '.failures[0].failure_type')" - -if [[ "$verdict" == "fail" ]] && [[ "$ftype" == "timing_miss" ]]; then - pass "expired grace window correctly classified as timing_miss" -else - fail "expired grace window should be timing_miss (got status=$verdict failure_type=$ftype)" -fi - -# Test 7: Discovery-phase seed that was never persisted (.agents/brainstorm/, -# .agents/research/, .agents/discovery/) on a CLOSED bead with a substantive -# close_reason should WARN as discovery_miss, NOT hard-fail as timing_miss. -# (close_reason now read from --json, not human output.) -write_epic_with_children test-epic test-epic.7 - -cat > "$BR_DIR/show-test-epic.7.json" <<JSON -[{ - "id": "test-epic.7", - "status": "closed", - "created_at": "2026-04-14T10:00:00+00:00", - "closed_at": "2026-04-14T20:00:00+00:00", - "description": "Add opt-in long-haul controller.\n\nSeed: .agents/brainstorm/2026-04-14-long-haul-value.md", - "close_reason": "Completed: landed the controller plus regression coverage; parent remains open for follow-up." -}] -JSON - -( - cd "$REPO_DIR" - rm -rf .agents 2>/dev/null || true - git reset --hard HEAD -q 2>/dev/null || true -) - -result="$(cd "$REPO_DIR" && bash "$AUDIT_SCRIPT" --scope auto test-epic 2>&1)" -verdict="$(echo "$result" | jq -r '.children[0].status')" -mode="$(echo "$result" | jq -r '.children[0].evidence_mode')" -detail="$(echo "$result" | jq -r '.children[0].detail')" -failures_len="$(echo "$result" | jq -r '.failures | length')" - -if [[ "$verdict" == "warn" ]] && [[ "$mode" == "discovery-seed-missing" ]] \ - && [[ "$detail" == discovery_miss:* ]] && [[ "$failures_len" == "0" ]]; then - pass "discovery-phase seed miss on CLOSED bead classifies as discovery_miss WARN (not timing_miss FAIL)" -else - fail "discovery-phase seed miss should warn as discovery_miss (got status=$verdict mode=$mode detail=$detail failures=$failures_len)" -fi - -# Test 8: Non-discovery scoped file (cli/foo.go) that doesn't exist in git -# must still hard-fail as timing_miss — the discovery downgrade is NOT a -# generic escape hatch. -write_epic_with_children test-epic test-epic.8 - -cat > "$BR_DIR/show-test-epic.8.json" <<JSON -[{ - "id": "test-epic.8", - "status": "closed", - "created_at": "2026-04-14T10:00:00+00:00", - "closed_at": "2026-04-14T20:00:00+00:00", - "description": "Refactor handler.\n\nFiles:\n- \`cli/cmd/ao/nonexistent_handler.go\`", - "close_reason": "Completed: refactored handler thoroughly across the codebase." -}] -JSON - -result="$(cd "$REPO_DIR" && bash "$AUDIT_SCRIPT" --scope auto test-epic 2>&1)" -verdict="$(echo "$result" | jq -r '.children[0].status')" -ftype="$(echo "$result" | jq -r '.failures[0].failure_type // "none"')" - -if [[ "$verdict" == "fail" ]] && [[ "$ftype" == "timing_miss" ]]; then - pass "non-discovery scoped file without evidence still hard-fails as timing_miss" -else - fail "non-discovery miss must remain timing_miss FAIL (got status=$verdict failure_type=$ftype)" -fi - -# Test 9: Bead with NO scoped files but a valid evidence-only packet -# (containing both `evidence_mode` and `repo_state`) should PASS via the -# evidence-only-packet short-circuit, NOT trip parser_miss or timing_miss. -write_epic_with_children test-epic test-epic.9 - -cat > "$BR_DIR/show-test-epic.9.json" <<JSON -[{ - "id": "test-epic.9", - "status": "closed", - "created_at": "2026-04-14T10:00:00+00:00", - "closed_at": "2026-04-14T20:00:00+00:00", - "description": "Maintenance closure with no code delta. Proven via evidence-only packet.", - "close_reason": "Completed: maintenance closure backed by evidence-only packet." -}] -JSON - -( - cd "$REPO_DIR" - rm -rf .agents 2>/dev/null || true - git reset --hard HEAD -q 2>/dev/null || true - mkdir -p .agents/releases/evidence-only-closures - cat > .agents/releases/evidence-only-closures/test-epic.9.json <<'PACKET' -{ - "target_id": "test-epic.9", - "target_type": "task", - "producer": "post-mortem", - "evidence_mode": "commit", - "validation_commands": ["bash scripts/validate-manifests.sh"], - "repo_state": { - "repo_root": ".", - "git_branch": "main", - "git_dirty": false, - "head_sha": "deadbeef", - "modified_files": [], - "staged_files": [], - "unstaged_files": [], - "untracked_files": [] - }, - "evidence": { - "summary": "Closed via evidence-only packet for maintenance audit.", - "artifacts": [".agents/releases/evidence-only-closures/test-epic.9.json"], - "notes": [] - } -} -PACKET -) - -result="$(cd "$REPO_DIR" && bash "$AUDIT_SCRIPT" --scope auto test-epic 2>&1)" -verdict="$(echo "$result" | jq -r '.children[0].status')" -mode="$(echo "$result" | jq -r '.children[0].evidence_mode')" -detail="$(echo "$result" | jq -r '.children[0].detail')" -failures_len="$(echo "$result" | jq -r '.failures | length')" - -if [[ "$verdict" == "pass" ]] && [[ "$mode" == "evidence-only-packet" ]] \ - && [[ "$detail" == *"short-circuit"* ]] && [[ "$failures_len" == "0" ]]; then - pass "evidence-only packet short-circuits classification to PASS" -else - fail "evidence-only packet should short-circuit to PASS evidence-only-packet (got status=$verdict mode=$mode detail=$detail failures=$failures_len)" -fi - -# Test 10: same as Test 9 but using --scope commit, verifying the short-circuit -# fires for the scope-mode classifier path too. -result="$(cd "$REPO_DIR" && bash "$AUDIT_SCRIPT" --scope commit test-epic 2>&1)" -verdict="$(echo "$result" | jq -r '.children[0].status')" -mode="$(echo "$result" | jq -r '.children[0].evidence_mode')" - -if [[ "$verdict" == "pass" ]] && [[ "$mode" == "evidence-only-packet" ]]; then - pass "evidence-only packet short-circuits under --scope commit too" -else - fail "evidence-only packet should short-circuit under --scope commit (got status=$verdict mode=$mode)" -fi - -# Test 11: single-epic closure — a CLOSED epic with NO children whose closure is -# commit-backed (a commit references the epic id) must PASS via the single-epic -# path with closure_mode single-epic, NOT trip collection_failed. -cat > "$BR_DIR/show-solo-epic.json" <<JSON -[{ - "id": "solo-epic", - "issue_type": "epic", - "status": "closed", - "created_at": "2026-05-01T10:00:00+00:00", - "closed_at": "2026-05-01T20:00:00+00:00", - "description": "Single-epic closure tracked directly on the epic.", - "close_reason": "Completed directly on the epic.", - "dependents": [] -}] -JSON - -( - cd "$REPO_DIR" - echo "package ao" > "cli/cmd/ao/solo.go" - git add "cli/cmd/ao/solo.go" - git commit -q -m "feat: land solo-epic work directly" -) - -result="$(cd "$REPO_DIR" && bash "$AUDIT_SCRIPT" --scope auto solo-epic 2>&1)" -verdict="$(echo "$result" | jq -r '.children[0].status')" -cmode="$(echo "$result" | jq -r '.children[0].closure_mode // "none"')" -cfailed="$(echo "$result" | jq -r '.summary.collection_failed // false')" - -if [[ "$verdict" == "pass" ]] && [[ "$cmode" == "single-epic" ]] && [[ "$cfailed" == "false" ]]; then - pass "commit-backed single-epic closure PASSES via single-epic path" -else - fail "commit-backed single-epic closure should PASS single-epic (got status=$verdict closure_mode=$cmode collection_failed=$cfailed)" -fi - -# Test 12: invalid no-child epic — a CLOSED epic with NO children, NO commit -# reference, and only a generic close reason (no SHA) must FAIL as -# collection_failed (the single-epic path must NOT rubber-stamp it). -cat > "$BR_DIR/show-empty-epic.json" <<JSON -[{ - "id": "empty-epic", - "issue_type": "epic", - "status": "closed", - "created_at": "2026-05-02T10:00:00+00:00", - "closed_at": "2026-05-02T20:00:00+00:00", - "description": "Closed with no children and no proof.", - "close_reason": "done", - "dependents": [] -}] -JSON - -result="$(cd "$REPO_DIR" && bash "$AUDIT_SCRIPT" --scope auto empty-epic 2>&1)" || true -cfailed="$(echo "$result" | jq -r '.summary.collection_failed // false')" - -if [[ "$cfailed" == "true" ]]; then - pass "invalid no-child epic correctly fails as collection_failed" -else - fail "invalid no-child epic should fail as collection_failed (got collection_failed=$cfailed)" -fi - -# Test 13: single-epic closure proven via a close_reason that cites a REAL landed -# commit SHA (no commit references the epic id, no children) must PASS via the -# close-reason path with evidence_mode close-reason. -REAL_SHA="$(cd "$REPO_DIR" && git rev-parse HEAD)" -cat > "$BR_DIR/show-sha-epic.json" <<JSON -[{ - "id": "sha-epic", - "issue_type": "epic", - "status": "closed", - "created_at": "2026-05-03T10:00:00+00:00", - "closed_at": "2026-05-03T20:00:00+00:00", - "description": "Single-epic closure proven by a landed commit SHA.", - "close_reason": "Completed: landed in ${REAL_SHA}.", - "dependents": [] -}] -JSON - -result="$(cd "$REPO_DIR" && bash "$AUDIT_SCRIPT" --scope auto sha-epic 2>&1)" -verdict="$(echo "$result" | jq -r '.children[0].status')" -mode="$(echo "$result" | jq -r '.children[0].evidence_mode')" -cmode="$(echo "$result" | jq -r '.children[0].closure_mode // "none"')" - -if [[ "$verdict" == "pass" ]] && [[ "$mode" == "close-reason" ]] && [[ "$cmode" == "single-epic" ]]; then - pass "single-epic closure with close_reason citing a REAL commit SHA PASSES" -else - fail "real-SHA close_reason should PASS close-reason single-epic (got status=$verdict mode=$mode closure_mode=$cmode)" -fi - -# Test 14: close_reason citing a hex token that is NOT a real commit must NOT -# false-pass — a bare hex token is not proof. This guards the fail-open a -# cross-family review caught (an incidental hex word would otherwise rubber-stamp -# an unproven epic closure). -cat > "$BR_DIR/show-fakesha-epic.json" <<JSON -[{ - "id": "fakesha-epic", - "issue_type": "epic", - "status": "closed", - "created_at": "2026-05-04T10:00:00+00:00", - "closed_at": "2026-05-04T20:00:00+00:00", - "description": "Closed with an incidental hex word but no real commit.", - "close_reason": "Completed: cleaned up deadbeefdeadbeef cafef00d references.", - "dependents": [] -}] -JSON - -result="$(cd "$REPO_DIR" && bash "$AUDIT_SCRIPT" --scope auto fakesha-epic 2>&1)" || true -cfailed="$(echo "$result" | jq -r '.summary.collection_failed // false')" - -if [[ "$cfailed" == "true" ]]; then - pass "close_reason with a non-resolvable hex token does NOT false-pass (collection_failed)" -else - fail "non-resolvable hex token must NOT pass single-epic (got collection_failed=$cfailed)" -fi - -echo "" -echo "Results: $PASS passed, $FAIL failed" -[[ "$FAIL" -eq 0 ]] diff --git a/tests/e2e/goals-scenarios-link.sh b/tests/e2e/goals-scenarios-link.sh deleted file mode 100755 index 3ff5e5c17..000000000 --- a/tests/e2e/goals-scenarios-link.sh +++ /dev/null @@ -1,125 +0,0 @@ -#!/usr/bin/env bash -# tests/e2e/goals-scenarios-link.sh — F1 e2e for epic soc-58nt. -# -# Exercises `ao goals scenarios --create` and `--lint` end-to-end in an -# isolated temp repo: bidirectional directive↔scenario link creation, a clean -# lint pass, a broken-link lint failure, and byte-for-byte preservation of -# non-target GOALS.md content (the Three-Gap section and Gates table). -# -# Fully automated, local-only, CI-runnable. Never touches this repo's GOALS.md -# or .agents/ — all work happens under a mktemp directory. -set -euo pipefail - -REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" - -# Shared e2e harness (skill: testing-real-service-e2e-no-mocks): -# shellcheck source=../lib/e2e-guards.sh -source "$REPO_ROOT/tests/lib/e2e-guards.sh" -# shellcheck source=../lib/e2e-factory.sh -source "$REPO_ROOT/tests/lib/e2e-factory.sh" - -log() { printf '[%s] %s\n' "$(date -u +%H:%M:%S)" "$*"; } -fail() { printf 'FAIL: %s\n' "$*" >&2; exit 1; } - -WORK="$(e2e_factory_sandbox goals-scenarios-link)" -trap 'rm -rf "$WORK"' EXIT -log "temp root: $WORK" -e2e_guard_repo "$WORK" - -AO="$(e2e_factory_ao_bin "$WORK/bin" "$REPO_ROOT")" -e2e_guard_ao_bin "$AO" -log "ao binary: $AO" - -cd "$WORK" -# This test runs `ao goals scenarios` with PWD-relative GOALS.md, so check -# that the chdir landed us in the sandbox (not the agentops repo root). -e2e_guard_not_repo_root - -cat > GOALS.md <<'EOF' -# Goals - -e2e fixture for the executable-spec link layer. - -## Directives - -### 1. First directive - -Body text for the first directive. - -**Steer:** increase (x) - -### 2. Target directive - -Body text for the target directive. - -**Steer:** decrease (y) - -## Three-Gap Contract Proof Surface - -This non-target section must survive every patch byte-for-byte. - -## Gates - -| ID | Check | Weight | Description | Tags | -|----|-------|--------|-------------|------| -| g | `true` | 5 | a gate | warn-only | -EOF -log "fixture GOALS.md written ($(wc -l < GOALS.md) lines)" -NONTARGET_BEFORE="$(sed -n '/## Three-Gap/,$p' GOALS.md)" - -# --- step 1: create a scenario and link it to directive 2 --- -log "step 1: argv = ao goals scenarios --create '...' --directive 2 --status active -o json" -if ! CREATE_JSON="$("$AO" goals scenarios --create "target behaviour is observable" \ - --directive 2 --status active -o json 2>create.err)"; then - cat create.err >&2 - fail "create exited non-zero" -fi -printf '%s\n' "$CREATE_JSON" -SCEN_ID="$(printf '%s' "$CREATE_JSON" | jq -r '.scenario_id')" -SCEN_PATH="$(printf '%s' "$CREATE_JSON" | jq -r '.scenario_path')" -LINKED="$(printf '%s' "$CREATE_JSON" | jq -r '.linked')" -DIRECTIVE_ID="$(printf '%s' "$CREATE_JSON" | jq -r '.directive_id')" -log "created scenario $SCEN_ID at $SCEN_PATH (linked=$LINKED, directive=$DIRECTIVE_ID)" -[[ "$LINKED" == "true" ]] || fail "scenario reported as not linked" - -# --- step 2: verify the link is bidirectional --- -[[ -f "$SCEN_PATH" ]] || fail "scenario file missing: $SCEN_PATH" -SCEN_DIRECTIVE="$(jq -r '.directive_id' "$SCEN_PATH")" -[[ "$SCEN_DIRECTIVE" == "$DIRECTIVE_ID" ]] \ - || fail "scenario directive_id ($SCEN_DIRECTIVE) != directive ($DIRECTIVE_ID)" -grep -qF "**Scenarios:** $SCEN_ID" GOALS.md \ - || fail "GOALS.md directive is missing the Scenarios link to $SCEN_ID" -log "step 2: bidirectional link verified (scenario→directive and directive→scenario)" - -# --- step 3: lint a complete link graph — must report zero errors --- -log "step 3: argv = ao goals scenarios --lint -o json" -if ! LINT_JSON="$("$AO" goals scenarios --lint -o json 2>lint.err)"; then - cat lint.err >&2 - fail "lint exited non-zero on a complete link graph" -fi -printf '%s\n' "$LINT_JSON" -LINT_ERRORS="$(printf '%s' "$LINT_JSON" | jq -r '.errors')" -[[ "$LINT_ERRORS" == "0" ]] || fail "lint reported $LINT_ERRORS error(s) on a clean link graph" -log "step 3: lint clean (errors=0)" - -# --- step 4: break the link — lint must now fail --- -rm -f "$SCEN_PATH" -log "step 4: removed $SCEN_PATH; argv = ao goals scenarios --lint -o json" -set +e -LINT2_JSON="$("$AO" goals scenarios --lint -o json 2>lint2.err)" -LINT2_EXIT=$? -set -e -printf '%s\n' "$LINT2_JSON" -log "lint exit code: $LINT2_EXIT" -[[ "$LINT2_EXIT" -ne 0 ]] || fail "lint should exit non-zero when a linked scenario is missing" -printf '%s' "$LINT2_JSON" | jq -e '.findings[] | select(.code == "missing-scenario")' >/dev/null \ - || fail "lint did not report the missing-scenario error" -log "step 4: broken link correctly detected as a missing-scenario error" - -# --- step 5: non-target GOALS.md content survived every patch --- -NONTARGET_AFTER="$(sed -n '/## Three-Gap/,$p' GOALS.md)" -[[ "$NONTARGET_BEFORE" == "$NONTARGET_AFTER" ]] \ - || fail "non-target GOALS.md content (Three-Gap section / Gates table) changed" -log "step 5: non-target GOALS.md content preserved byte-for-byte" - -log "PASS: F1 executable-spec link e2e (create → verify → lint clean → break → lint fails)" diff --git a/tests/e2e/goals-steer-auto.sh b/tests/e2e/goals-steer-auto.sh deleted file mode 100755 index 0f70456c9..000000000 --- a/tests/e2e/goals-steer-auto.sh +++ /dev/null @@ -1,233 +0,0 @@ -#!/usr/bin/env bash -# tests/e2e/goals-steer-auto.sh — F5 e2e for epic soc-58nt. -# -# Exercises `ao goals steer recommend` and `ao goals steer apply` end-to-end -# in an isolated temp repo. Never touches this repo's GOALS.md or .agents/ — -# all work happens under a mktemp directory. -# -# Steps: -# 1. Seed a temp GOALS.md + verdict-ledger with an eligible failure streak. -# 2. `ao goals steer recommend` → assert recommendation surfaced, GOALS.md unchanged. -# 3. `ao goals steer apply` WITHOUT consent (auto_apply:false, no --yes) → assert GOALS.md unchanged. -# 4. Seed auto_apply:true policy; `ao goals steer apply --yes` → assert priority bump applied, -# non-target content byte-preserved, cooldown record written. -set -euo pipefail - -REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -AO_BIN="/tmp/ao-e2e-f5" - -log() { printf '[%s] %s\n' "$(date -u +%H:%M:%S)" "$*"; } -fail() { printf 'FAIL: %s\n' "$*" >&2; exit 1; } - -# ── build ───────────────────────────────────────────────────────────────────── -if [[ ! -x "$AO_BIN" ]] || [[ "$REPO_ROOT/cli/cmd/ao" -nt "$AO_BIN" ]]; then - log "ao binary absent or stale — building to $AO_BIN" - ( cd "$REPO_ROOT/cli" && go build -o "$AO_BIN" ./cmd/ao ) -fi -log "ao binary: $AO_BIN" - -WORK="$(mktemp -d)" -trap 'rm -rf "$WORK"' EXIT -log "temp root: $WORK" - -# ── fixture: GOALS.md with two directives ───────────────────────────────────── -# d-reduce-flaky is the chronic-failure target; d-ship-fast is healthy (no ledger). -mkdir -p "$WORK/docs" -cat > "$WORK/GOALS.md" <<'GOALSEOF' -# Goals - -F5 e2e fixture: auto re-steer. - -## Directives - -### 1. Ship fast - -Deploy continuously. - -**Directive ID:** d-ship-fast -**Steer:** increase - -### 2. Reduce flaky tests - -Stabilise the suite. - -**Directive ID:** d-reduce-flaky -**Steer:** increase - -## Gates - -| ID | Check | Weight | Description | -|----|-------|--------|-------------| -| gate-one | `exit 0` | 5 | Smoke | -GOALSEOF -GOALS_BEFORE="$(cat "$WORK/GOALS.md")" -log "fixture GOALS.md written ($(wc -l < "$WORK/GOALS.md") lines)" - -# ── fixture: verdict-ledger with an eligible failure streak ─────────────────── -# Policy defaults: minimum_evidence_count=5, failure_streak_length=3. -# Seed 8 iterations: 3 passes then 5 consecutive fails (streak=5 >= 3, evidence=8 >= 5). -LEDGER_DIR="$WORK/.agents/goals" -mkdir -p "$LEDGER_DIR" -LEDGER_PATH="$LEDGER_DIR/verdict-ledger.json" - -cat > "$LEDGER_PATH" <<'LEDGEREOF' -{ - "schema_version": "verdict-ledger.v1", - "generated_at": "2026-05-17T08:00:00Z", - "records": [ - {"record_type":"iteration","directive_id":"d-reduce-flaky","run_timestamp":"2026-05-17T01:00:00Z","scenario_verdict":"pass","scenario_satisfaction":0.90,"scenario_count":3,"evaluated_count":3,"run_id":"r1"}, - {"record_type":"iteration","directive_id":"d-reduce-flaky","run_timestamp":"2026-05-17T02:00:00Z","scenario_verdict":"pass","scenario_satisfaction":0.88,"scenario_count":3,"evaluated_count":3,"run_id":"r2"}, - {"record_type":"iteration","directive_id":"d-reduce-flaky","run_timestamp":"2026-05-17T03:00:00Z","scenario_verdict":"pass","scenario_satisfaction":0.85,"scenario_count":3,"evaluated_count":3,"run_id":"r3"}, - {"record_type":"iteration","directive_id":"d-reduce-flaky","run_timestamp":"2026-05-17T04:00:00Z","scenario_verdict":"fail","scenario_satisfaction":0.40,"scenario_count":3,"evaluated_count":3,"run_id":"r4"}, - {"record_type":"iteration","directive_id":"d-reduce-flaky","run_timestamp":"2026-05-17T05:00:00Z","scenario_verdict":"fail","scenario_satisfaction":0.38,"scenario_count":3,"evaluated_count":3,"run_id":"r5"}, - {"record_type":"iteration","directive_id":"d-reduce-flaky","run_timestamp":"2026-05-17T06:00:00Z","scenario_verdict":"fail","scenario_satisfaction":0.36,"scenario_count":3,"evaluated_count":3,"run_id":"r6"}, - {"record_type":"iteration","directive_id":"d-reduce-flaky","run_timestamp":"2026-05-17T07:00:00Z","scenario_verdict":"fail","scenario_satisfaction":0.35,"scenario_count":3,"evaluated_count":3,"run_id":"r7"}, - {"record_type":"iteration","directive_id":"d-reduce-flaky","run_timestamp":"2026-05-17T08:00:00Z","scenario_verdict":"fail","scenario_satisfaction":0.33,"scenario_count":3,"evaluated_count":3,"run_id":"r8"} - ] -} -LEDGEREOF -log "fixture verdict-ledger written (8 iterations, streak=5)" - -# ═══════════════════════════════════════════════════════════════════════════════ -# step 1: recommend — surfaced, GOALS.md byte-unchanged -# ═══════════════════════════════════════════════════════════════════════════════ -log "step 1: ao goals steer recommend" -log " argv = ao goals steer recommend -o json" -STDOUT1="$WORK/stdout-step1.txt" -STDERR1="$WORK/stderr-step1.txt" -( cd "$WORK" && "$AO_BIN" goals steer recommend -o json \ - > "$STDOUT1" 2> "$STDERR1" ) -EXIT1=$? -log " exit code: $EXIT1" -log " stderr: $(cat "$STDERR1" 2>/dev/null || true)" -log " stdout:" -cat "$STDOUT1" - -[[ "$EXIT1" -eq 0 ]] \ - || fail "step 1: exit code $EXIT1, want 0" - -# Recommendation for d-reduce-flaky must be surfaced. -REC_DIRECTIVE="$(jq -r '.recommendations[0].directive_id // empty' "$STDOUT1")" -REC_TYPE="$(jq -r '.recommendations[0].mutation_type // empty' "$STDOUT1")" -log " recommendation: directive=$REC_DIRECTIVE type=$REC_TYPE" - -[[ "$REC_DIRECTIVE" == "d-reduce-flaky" ]] \ - || fail "step 1: recommendation directive = '$REC_DIRECTIVE', want 'd-reduce-flaky'" -[[ "$REC_TYPE" == "priority_bump" ]] \ - || fail "step 1: recommendation type = '$REC_TYPE', want 'priority_bump'" - -# auto_apply must be reported as false (default policy). -AUTO_APPLY="$(jq -r '.auto_apply' "$STDOUT1")" -log " auto_apply: $AUTO_APPLY" -[[ "$AUTO_APPLY" == "false" ]] \ - || fail "step 1: auto_apply = '$AUTO_APPLY', want 'false' (default policy)" - -# GOALS.md must be byte-identical after recommend. -GOALS_AFTER_STEP1="$(cat "$WORK/GOALS.md")" -[[ "$GOALS_AFTER_STEP1" == "$GOALS_BEFORE" ]] \ - || fail "step 1: GOALS.md was modified by recommend (must be recommendation-only)" - -log "step 1 PASS: recommendation surfaced for d-reduce-flaky, GOALS.md unchanged" - -# ═══════════════════════════════════════════════════════════════════════════════ -# step 2: apply WITHOUT consent (no --yes, auto_apply:false) → GOALS.md unchanged -# ═══════════════════════════════════════════════════════════════════════════════ -log "step 2: ao goals steer apply (no --yes, no auto_apply policy)" -STDOUT2="$WORK/stdout-step2.txt" -STDERR2="$WORK/stderr-step2.txt" -( cd "$WORK" && "$AO_BIN" goals steer apply \ - > "$STDOUT2" 2> "$STDERR2" ) && EXIT2=0 || EXIT2=$? -log " exit code: $EXIT2" -log " stderr: $(cat "$STDERR2" 2>/dev/null || true)" -log " stdout: $(cat "$STDOUT2" 2>/dev/null || true)" - -# Without consent (no policy file → auto_apply:false) the command must fail. -[[ "$EXIT2" -ne 0 ]] \ - || fail "step 2: exit code $EXIT2, want non-zero (auto_apply not enabled)" - -# GOALS.md must still be byte-identical. -GOALS_AFTER_STEP2="$(cat "$WORK/GOALS.md")" -[[ "$GOALS_AFTER_STEP2" == "$GOALS_BEFORE" ]] \ - || fail "step 2: GOALS.md was mutated despite missing consent" - -log "step 2 PASS: apply blocked without consent, GOALS.md unchanged" - -# ═══════════════════════════════════════════════════════════════════════════════ -# step 3: apply WITH consent (auto_apply:true + --yes) → bump applied -# ═══════════════════════════════════════════════════════════════════════════════ -log "step 3: seed auto_apply:true policy, run ao goals steer apply --yes" - -# Write a policy with auto_apply:true. -cat > "$WORK/docs/re-steer-policy.json" <<'POLICYEOF' -{ - "minimum_evidence_count": 5, - "failure_streak_length": 3, - "cooldown_iterations": 5, - "allowed_mutation_types": ["priority_bump"], - "max_priority_bump": 3, - "auto_apply": true, - "allow_steer_flip": false -} -POLICYEOF -log " policy written: $WORK/docs/re-steer-policy.json" - -STDOUT3="$WORK/stdout-step3.txt" -STDERR3="$WORK/stderr-step3.txt" -log " argv = ao goals steer apply --yes" -( cd "$WORK" && "$AO_BIN" goals steer apply --yes \ - > "$STDOUT3" 2> "$STDERR3" ) -EXIT3=$? -log " exit code: $EXIT3" -log " stderr: $(cat "$STDERR3" 2>/dev/null || true)" -log " stdout: $(cat "$STDOUT3" 2>/dev/null || true)" - -[[ "$EXIT3" -eq 0 ]] \ - || fail "step 3: exit code $EXIT3, want 0 (auto_apply:true + --yes)" - -GOALS_AFTER_STEP3="$(cat "$WORK/GOALS.md")" -log " GOALS.md diff (before → after):" -diff <(printf '%s\n' "$GOALS_BEFORE") <(printf '%s\n' "$GOALS_AFTER_STEP3") || true - -# d-reduce-flaky was #2; a priority bump must move it to #1. -if ! printf '%s\n' "$GOALS_AFTER_STEP3" | grep -q "### 1. Reduce flaky tests"; then - fail "step 3: 'Reduce flaky tests' not at position #1 after apply" -fi - -# Non-target content must be preserved byte-for-byte. -for fragment in \ - "F5 e2e fixture: auto re-steer." \ - "| gate-one |" \ - "**Directive ID:** d-ship-fast" \ - "**Directive ID:** d-reduce-flaky" \ - "**Steer:** increase" -do - if ! printf '%s\n' "$GOALS_AFTER_STEP3" | grep -qF "$fragment"; then - fail "step 3: non-target content '$fragment' not preserved in GOALS.md" - fi -done - -# GOALS.md must be different from before (mutation happened). -[[ "$GOALS_AFTER_STEP3" != "$GOALS_BEFORE" ]] \ - || fail "step 3: GOALS.md is byte-identical; apply did not mutate" - -log "step 3a PASS: priority bump applied, non-target content preserved" - -# ── assert cooldown record written ──────────────────────────────────────────── -COOLDOWN_KIND="$(jq -r ' - .records[] - | select(.record_type == "cooldown" and .directive_id == "d-reduce-flaky") - | .cooldown_kind' "$LEDGER_PATH" 2>/dev/null | head -1)" -COOLDOWN_MUTATION="$(jq -r ' - .records[] - | select(.record_type == "cooldown" and .directive_id == "d-reduce-flaky") - | .mutation_type' "$LEDGER_PATH" 2>/dev/null | head -1)" -log " cooldown record: kind=$COOLDOWN_KIND mutation_type=$COOLDOWN_MUTATION" - -[[ "$COOLDOWN_KIND" == "applied" ]] \ - || fail "step 3: cooldown_kind = '$COOLDOWN_KIND', want 'applied'" -[[ "$COOLDOWN_MUTATION" == "priority_bump" ]] \ - || fail "step 3: cooldown mutation_type = '$COOLDOWN_MUTATION', want 'priority_bump'" - -log "step 3b PASS: cooldown record written with kind=applied, mutation_type=priority_bump" - -log "PASS: F5 e2e goals-steer-auto (recommend → blocked apply → consented apply + cooldown)" diff --git a/tests/e2e/proof-run.sh b/tests/e2e/proof-run.sh deleted file mode 100755 index 7b20448f2..000000000 --- a/tests/e2e/proof-run.sh +++ /dev/null @@ -1,214 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" -FIXTURE_DIR="$REPO_ROOT/tests/fixtures/flywheel-proof" - -# Shared e2e harness (skill: testing-real-service-e2e-no-mocks): -# guards — refuse-to-run if HOME/repo/ao binary look real -# logger — JSON-line sidecar for CI parseability -# factory — sandbox + repo + ao binary builders -# shellcheck source=../lib/e2e-guards.sh -source "$REPO_ROOT/tests/lib/e2e-guards.sh" -# shellcheck source=../lib/e2e-logger.sh -source "$REPO_ROOT/tests/lib/e2e-logger.sh" -# shellcheck source=../lib/e2e-factory.sh -source "$REPO_ROOT/tests/lib/e2e-factory.sh" - -# Note: proof-run.sh never executes ao relative to PWD — every run_ao call -# chdirs into REPO_DIR. So e2e_guard_not_repo_root is intentionally NOT -# called here; the per-invocation REPO/HOME/AO guards below are sufficient. - -WORK_DIR="$(e2e_factory_sandbox flywheel-proof)" -BUILD_DIR="$WORK_DIR/bin" -HOME_DIR="$WORK_DIR/home" -REPO_DIR="$WORK_DIR/repo" -AO_BIN="$BUILD_DIR/ao" -LOG_FILE="$WORK_DIR/proof-run.log" -SIDECAR_LOG="$WORK_DIR/proof-run.jsonl" -PASS_COUNT=0 -LOOKUP_QUERY="task-scoped lookup queries" - -cleanup() { - chmod -R u+w "$WORK_DIR" 2>/dev/null || true - rm -rf "$WORK_DIR" -} -trap cleanup EXIT - -log() { - printf '[proof-run] %s\n' "$*" | tee -a "$LOG_FILE" -} - -pass() { - PASS_COUNT=$((PASS_COUNT + 1)) - log "PASS: $*" - e2e_log_pass "$*" -} - -fail() { - log "FAIL: $*" - e2e_log_fail "$*" - e2e_log_summary - exit 1 -} - -require_cmd() { - if ! command -v "$1" >/dev/null 2>&1; then - fail "missing required command: $1" - fi -} - -assert_file_exists() { - local label="$1" - local path="$2" - if [[ -f "$path" ]]; then - pass "$label" - return - fi - fail "$label (missing $path)" -} - -assert_json_match() { - local label="$1" - local file="$2" - local filter="$3" - if jq -e "$filter" "$file" >/dev/null 2>&1; then - pass "$label" - return - fi - log "jq filter failed: $filter" - sed -n '1,200p' "$file" | tee -a "$LOG_FILE" >/dev/null - fail "$label" -} - -count_files() { - local dir="$1" - local pattern="$2" - if [[ ! -d "$dir" ]]; then - echo 0 - return - fi - find "$dir" -maxdepth 1 -type f -name "$pattern" | wc -l | tr -d ' ' -} - -run_ao() { - ( - cd "$REPO_DIR" - "$AO_BIN" "$@" - ) -} - -require_cmd git -require_cmd jq - -mkdir -p "$HOME_DIR" -export HOME="$HOME_DIR" -e2e_log_init "flywheel-proof" "$SIDECAR_LOG" -e2e_log_phase setup - -# Build (or reuse) the ao binary inside the sandbox. Honors PROOF_AO_BIN and -# PROOF_FORCE_BUILD — see tests/lib/e2e-factory.sh for the resolution rules. -# The flywheel proof exercises ADR-0012 archived surfaces (harvest / dream -# cycle), which the default spine build omits — request the archive tags. -export PROOF_BUILD_TAGS="${PROOF_BUILD_TAGS:-flywheel legacy}" -AO_BIN="$(e2e_factory_ao_bin "$BUILD_DIR" "$REPO_ROOT")" -export PATH="$BUILD_DIR:$PATH" -pass "ao binary resolved at $AO_BIN" - -# Production safety guards — every assertion must hold before we touch state. -# The skill's "never hit prod from tests" rule, translated to the CLI domain: -# $HOME must point inside the sandbox, the repo must be under a temp prefix, -# and the binary must live in the sandbox build dir (or repo-local cli/bin). -e2e_guard_home "$HOME_DIR" -e2e_guard_ao_bin "$AO_BIN" - -log "Creating isolated proof repo" -REPO_DIR="$(e2e_factory_repo "$REPO_DIR")" -e2e_guard_repo "$REPO_DIR" -pass "initialized isolated repo" - -TRANSCRIPT="$(e2e_factory_fixture "$FIXTURE_DIR/seed-session.jsonl" "$REPO_DIR")" -pass "copied raw transcript fixture" - -e2e_log_phase forge -log "Phase 1: forge transcript into pending learnings" -run_ao forge transcript "$TRANSCRIPT" --quiet >/dev/null -PENDING_DIR="$REPO_DIR/.agents/knowledge/pending" -PENDING_COUNT="$(count_files "$PENDING_DIR" '*.md')" -if [[ "$PENDING_COUNT" -lt 1 ]]; then - fail "expected pending learnings after forge, found $PENDING_COUNT" -fi -pass "forge produced $PENDING_COUNT pending learning(s)" - -e2e_log_phase pool-ingest -log "Phase 2: pool ingest lands pending learnings as pool candidates" -# Use 'ao pool ingest' rather than 'ao flywheel close-loop' here: close-loop now -# auto-promotes candidates in the same call (commit b69a00f4 removed the -# citation-required deadlock), so the candidate would no longer be observable in -# pool/pending. This phase verifies ingest in isolation before Phase 3 exercises -# the cite -> promote path via close-loop. -INGEST_JSON="$WORK_DIR/pool-ingest.json" -run_ao pool ingest --json > "$INGEST_JSON" -assert_json_match "pool ingest added pending learnings" "$INGEST_JSON" '.added >= 1' - -POOL_PENDING_DIR="$REPO_DIR/.agents/pool/pending" -CANDIDATE_PATH="$(find "$POOL_PENDING_DIR" -maxdepth 1 -type f -name '*.json' | head -n 1)" -if [[ -z "$CANDIDATE_PATH" ]]; then - fail "expected a pool candidate after ingest" -fi -assert_file_exists "pool candidate exists after ingest" "$CANDIDATE_PATH" - -e2e_log_phase cite-promote -log "Phase 3: cite the pool candidate and promote it into a retrievable artifact" -run_ao metrics cite "$CANDIDATE_PATH" --type reference --session proof-promotion --query "$LOOKUP_QUERY" >/dev/null -CLOSE2_JSON="$WORK_DIR/close-loop-2.json" -run_ao flywheel close-loop --threshold 0h --json > "$CLOSE2_JSON" -assert_json_match "close-loop promoted a cited candidate" "$CLOSE2_JSON" '.auto_promote.promoted >= 1' - -ARTIFACT_PATH="$(jq -r '.auto_promote.artifacts[0] // empty' "$CLOSE2_JSON")" -if [[ -z "$ARTIFACT_PATH" ]]; then - fail "expected promoted artifact path in close-loop output" -fi -assert_file_exists "promoted artifact exists on disk" "$ARTIFACT_PATH" -e2e_log_artifact "$ARTIFACT_PATH" "promoted-artifact" -ARTIFACT_JSON="$(printf '%s' "$ARTIFACT_PATH" | jq -R '.')" - -e2e_log_phase lookup -log "Phase 4: lookup retrieves the promoted artifact and records retrieved evidence" -LOOKUP_JSON="$WORK_DIR/lookup.json" -run_ao lookup --query "$LOOKUP_QUERY" --json > "$LOOKUP_JSON" -assert_json_match "lookup surfaces promoted knowledge" "$LOOKUP_JSON" '((.learnings | length) + (.patterns | length)) >= 1' - -CITATIONS_PATH="$REPO_DIR/.agents/ao/citations.jsonl" -assert_file_exists "citations log exists" "$CITATIONS_PATH" -assert_json_match \ - "lookup recorded a retrieved citation for the promoted artifact" \ - "$CITATIONS_PATH" \ - "select(.artifact_path == $ARTIFACT_JSON and .citation_type == \"retrieved\")" - -e2e_log_phase feedback -log "Phase 5: record applied evidence and close the feedback loop" -run_ao metrics cite "$ARTIFACT_PATH" --type applied --session proof-apply --query "$LOOKUP_QUERY" >/dev/null -mkdir -p "$REPO_DIR/.agents/ao" -cp "$FIXTURE_DIR/last-session-outcome.success.json" "$REPO_DIR/.agents/ao/last-session-outcome.json" -pass "seeded deterministic success outcome" - -CLOSE3_JSON="$WORK_DIR/close-loop-3.json" -run_ao flywheel close-loop --threshold 0h --json > "$CLOSE3_JSON" -assert_json_match "close-loop rewarded applied artifact feedback" "$CLOSE3_JSON" '.citation_feedback.rewarded >= 1' - -FEEDBACK_PATH="$REPO_DIR/.agents/ao/feedback.jsonl" -assert_file_exists "feedback log exists" "$FEEDBACK_PATH" -assert_json_match \ - "feedback log records rewarded applied evidence" \ - "$FEEDBACK_PATH" \ - "select(.artifact_path == $ARTIFACT_JSON and .decision == \"rewarded\" and .reason == \"artifact-applied\" and .utility_after > .utility_before)" -assert_json_match \ - "applied citation is marked feedback-given" \ - "$CITATIONS_PATH" \ - "select(.artifact_path == $ARTIFACT_JSON and .citation_type == \"applied\" and .feedback_given == true)" - -e2e_log_phase "complete" -e2e_log_summary -log "FLYWHEEL PROOF: PASS ($PASS_COUNT checks) — sidecar: $SIDECAR_LOG" diff --git a/tests/explicit-skill-requests/prompts/post-mortem.txt b/tests/explicit-skill-requests/prompts/post-mortem.txt deleted file mode 100644 index d80632bad..000000000 --- a/tests/explicit-skill-requests/prompts/post-mortem.txt +++ /dev/null @@ -1 +0,0 @@ -Execute /post-mortem to validate and extract learnings \ No newline at end of file diff --git a/tests/explicit-skill-requests/prompts/postmortem.txt b/tests/explicit-skill-requests/prompts/postmortem.txt new file mode 100644 index 000000000..749aed990 --- /dev/null +++ b/tests/explicit-skill-requests/prompts/postmortem.txt @@ -0,0 +1 @@ +Execute /postmortem to analyze this completed run diff --git a/tests/explicit-skill-requests/prompts/pre-mortem.txt b/tests/explicit-skill-requests/prompts/pre-mortem.txt deleted file mode 100644 index 696b5c65a..000000000 --- a/tests/explicit-skill-requests/prompts/pre-mortem.txt +++ /dev/null @@ -1 +0,0 @@ -Run /pre-mortem to simulate failures in this spec \ No newline at end of file diff --git a/tests/explicit-skill-requests/prompts/premortem.txt b/tests/explicit-skill-requests/prompts/premortem.txt new file mode 100644 index 000000000..70bc6f1cb --- /dev/null +++ b/tests/explicit-skill-requests/prompts/premortem.txt @@ -0,0 +1 @@ +Run /premortem to simulate failures in this spec diff --git a/tests/fixtures/mortem-compatibility/directory-conflict/pre-mortem-check.json b/tests/fixtures/mortem-compatibility/directory-conflict/pre-mortem-check.json deleted file mode 100644 index 8b475626c..000000000 --- a/tests/fixtures/mortem-compatibility/directory-conflict/pre-mortem-check.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"check-conflict","rule":"legacy content"} diff --git a/tests/fixtures/mortem-compatibility/directory-conflict/premortem-check.json b/tests/fixtures/mortem-compatibility/directory-conflict/premortem-check.json deleted file mode 100644 index 7d451254f..000000000 --- a/tests/fixtures/mortem-compatibility/directory-conflict/premortem-check.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"check-conflict","rule":"different canonical content"} diff --git a/tests/fixtures/mortem-compatibility/explicit-skill-redirect.yaml b/tests/fixtures/mortem-compatibility/explicit-skill-redirect.yaml deleted file mode 100644 index da0f84d9d..000000000 --- a/tests/fixtures/mortem-compatibility/explicit-skill-redirect.yaml +++ /dev/null @@ -1,13 +0,0 @@ -historical: - pre-mortem: - state: merged-into - merged-into: premortem - post-mortem: - state: merged-into - merged-into: postmortem - pre_mortem: - state: merged-into - merged-into: premortem - post_mortem: - state: merged-into - merged-into: postmortem diff --git a/tests/fixtures/mortem-compatibility/legacy-directory/pre-mortem-check.json b/tests/fixtures/mortem-compatibility/legacy-directory/pre-mortem-check.json deleted file mode 100644 index 6991379d3..000000000 --- a/tests/fixtures/mortem-compatibility/legacy-directory/pre-mortem-check.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"check-legacy","rule":"legacy-only content fills a missing canonical id"} diff --git a/tests/install/test-install-smoke.sh b/tests/install/test-install-smoke.sh index 5162d3c8b..2ba91019e 100755 --- a/tests/install/test-install-smoke.sh +++ b/tests/install/test-install-smoke.sh @@ -87,7 +87,6 @@ echo "" SUPPORT_SCRIPTS=( "scripts/install-codex-plugin.sh" "scripts/install-codex-native-skills.sh" - "scripts/install-dev-hooks.sh" ) for script in "${SUPPORT_SCRIPTS[@]}"; do diff --git a/tests/integration/test-ci-policy-parity-wave-gate.sh b/tests/integration/test-ci-policy-parity-wave-gate.sh deleted file mode 100755 index 7fbd25373..000000000 --- a/tests/integration/test-ci-policy-parity-wave-gate.sh +++ /dev/null @@ -1,281 +0,0 @@ -#!/usr/bin/env bash -# test-ci-policy-parity-wave-gate.sh — assert Wave 1G wiring of -# scripts/validate-ci-policy-parity.sh into crank's Step 5.5 wave acceptance -# as a conditional gate that fires when a wave touches a workflow YAML file. -# -# Bead: soc-il9k (Wave 1G of epic soc-xlw8). -# Motivation: commit c587b361 was a manual fix after soc-lmww1 added an -# advisory job to validate.yml without updating AGENTS.md or summary.needs. -# This test pins the formalization of that recurrence-prevention rule. -# -# Asserts: -# T1. The conditional gate is documented in skills/crank/SKILL.md Step 5.5. -# T2. The gate uses the narrow trigger pattern (workflow YAML files only). -# T3. references/wave-patterns.md has a "CI-Policy Parity Gate" section -# linked from SKILL.md. -# T4. The worker prompt template (skills/swarm/references/local-mode.md) -# references the parity gate as a conditional preflight. -# T5. Codex parity copies (skills-codex/crank/, skills-codex/swarm/) carry -# matching gate references. -# T6. The validator dependency exists and is executable. -# T7. Behavioral fixture: synthesize a drifted (validate.yml, AGENTS.md, -# ci-jobs.yaml) set under CI_POLICY_PARITY_* env overrides and assert exit 1. -# T8. Behavioral fixture: synthesize an aligned pair and assert exit 0. -# T9. Sanity: invoking the validator on the live repo exits 0 -# (current main is parity-clean — guards against repo-level regression). -# -# Usage: bash tests/integration/test-ci-policy-parity-wave-gate.sh - -set -uo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" - -# shellcheck disable=SC1091 -source "$REPO_ROOT/tests/lib/colors.sh" - -PASS=0 -FAIL=0 - -ok() { - pass "$1" - PASS=$((PASS + 1)) -} - -ko() { - fail "$1" - FAIL=$((FAIL + 1)) -} - -VALIDATOR="$REPO_ROOT/scripts/validate-ci-policy-parity.sh" -CRANK_SKILL="$REPO_ROOT/skills/crank/SKILL.md" -WAVE_PATTERNS="$REPO_ROOT/skills/crank/references/wave-patterns.md" -WORKER_TEMPLATE="$REPO_ROOT/skills/swarm/references/local-mode.md" -CODEX_CRANK_SKILL="$REPO_ROOT/skills-codex/crank/SKILL.md" -CODEX_WAVE_PATTERNS="$REPO_ROOT/skills-codex/crank/references/wave-patterns.md" -CODEX_WORKER_TEMPLATE="$REPO_ROOT/skills-codex/swarm/references/local-mode.md" - -# ---------- T1: gate documented in crank SKILL.md ---------- -if grep -q 'validate-ci-policy-parity' "$CRANK_SKILL" \ - && grep -q 'CI-Policy Parity Gate' "$CRANK_SKILL"; then - ok "T1 crank SKILL.md Step 5.5 references CI-Policy Parity Gate + validator" -else - ko "T1 crank SKILL.md missing CI-Policy Parity Gate wiring" -fi - -# ---------- T2: narrow trigger pattern documented (workflows YAML only) ---------- -# Combined check across SKILL.md + wave-patterns.md (trigger is documented in -# wave-patterns; SKILL.md may carry only the summary form). -if grep -qE '\^\\\.github/workflows/.*\\\.ya\?ml\$' "$CRANK_SKILL" "$WAVE_PATTERNS"; then - ok "T2 narrow trigger pattern documented (workflow YAML only)" -else - ko "T2 narrow trigger pattern missing in crank docs" -fi - -# ---------- T3: wave-patterns.md has worked example, linked from SKILL.md ---------- -if grep -q '## CI-Policy Parity Gate' "$WAVE_PATTERNS" \ - && grep -q 'c587b361' "$WAVE_PATTERNS" \ - && grep -q 'wave-patterns.md' "$CRANK_SKILL"; then - ok "T3 wave-patterns.md carries worked example with c587b361 motivation, linked from SKILL.md" -else - ko "T3 wave-patterns.md worked example missing or not linked" -fi - -# ---------- T4: worker prompt template carries the preflight ---------- -if grep -q 'CI-policy parity preflight' "$WORKER_TEMPLATE" \ - && grep -q 'validate-ci-policy-parity' "$WORKER_TEMPLATE"; then - ok "T4 worker prompt template (swarm/local-mode.md) references parity preflight" -else - ko "T4 worker prompt template missing parity preflight" -fi - -# ---------- T5: codex parity copies carry matching wiring ---------- -codex_ok=1 -for f in "$CODEX_CRANK_SKILL" "$CODEX_WAVE_PATTERNS" "$CODEX_WORKER_TEMPLATE"; do - if [[ ! -f "$f" ]]; then - codex_ok=0 - ko "T5 codex parity file missing: ${f#$REPO_ROOT/}" - continue - fi - if ! grep -q 'validate-ci-policy-parity' "$f"; then - codex_ok=0 - ko "T5 codex parity file lacks validator reference: ${f#$REPO_ROOT/}" - fi -done -if [[ "$codex_ok" -eq 1 ]]; then - ok "T5 codex parity copies (crank + swarm) reference validate-ci-policy-parity" -fi - -# ---------- T6: validator dependency exists ---------- -if [[ -x "$VALIDATOR" ]]; then - ok "T6 validator is present and executable: scripts/validate-ci-policy-parity.sh" -else - ko "T6 validator missing or not executable: scripts/validate-ci-policy-parity.sh" -fi - -# ---------- Behavioral fixtures (T7, T8) ---------- -TMP_DIR="$(mktemp -d)" -trap 'rm -rf "$TMP_DIR"' EXIT - -# Minimal AGENTS.md with the table header the validator looks for. -make_agents_md() { - local out="$1" - shift - { - echo "# Stub AGENTS.md" - echo "" - # Inline non-blocking enumeration the validator's regex matches. - local nb_line="" - local sep="" - for entry in "$@"; do - local job="${entry%%:*}" - local kind="${entry##*:}" - if [[ "$kind" == "nonblocking" ]]; then - nb_line+="${sep}${job} (non-blocking)" - sep=", " - fi - done - if [[ -n "$nb_line" ]]; then - echo "Non-blocking jobs: ${nb_line}." - echo "" - fi - echo "### CI Jobs and What They Check" - echo "" - echo "| Job | What it validates | Common failure |" - echo "|-----|-------------------|----------------|" - for entry in "$@"; do - local job="${entry%%:*}" - local kind="${entry##*:}" - local name_cell="**${job}**" - if [[ "$kind" == "nonblocking" ]]; then - name_cell="**${job}** (non-blocking)" - fi - echo "| ${name_cell} | stub | stub |" - done - } > "$out" -} - -# Minimal validate.yml with summary.needs and modern contains() failset rule. -# Args: pairs of "job:blocking" or "job:nonblocking". -make_workflow_yml() { - local out="$1" - shift - { - echo "name: stub" - echo "on: [push]" - echo "jobs:" - for entry in "$@"; do - local job="${entry%%:*}" - local kind="${entry##*:}" - echo " ${job}:" - echo " runs-on: ubuntu-latest" - if [[ "$kind" == "nonblocking" ]]; then - echo " continue-on-error: true" - fi - echo " steps:" - echo " - run: echo ${job}" - done - # Build needs list - local needs="" - local sep="" - for entry in "$@"; do - local job="${entry%%:*}" - needs+="${sep}${job}" - sep=", " - done - echo " summary:" - echo " needs: [${needs}]" - echo " runs-on: ubuntu-latest" - echo " if: \${{ always() && contains(needs.*.result, 'failure') }}" - echo " steps:" - echo " - run: echo summary" - } > "$out" -} - -make_manifest_yml() { - local out="$1" - shift - { - echo "jobs:" - for entry in "$@"; do - local job="${entry%%:*}" - printf ' - name: %s\n reason: stub\n failure: stub\n' "$job" - done - } > "$out" -} - -# T7: drifted state — workflow has new advisory job, AGENTS.md does NOT. -DRIFT_DIR="$TMP_DIR/drift" -mkdir -p "$DRIFT_DIR/.github/workflows" -make_agents_md "$DRIFT_DIR/AGENTS.md" \ - "alpha-gate:blocking" \ - "beta-gate:blocking" -make_workflow_yml "$DRIFT_DIR/.github/workflows/validate.yml" \ - "alpha-gate:blocking" \ - "beta-gate:blocking" \ - "factory-claim-ledger-strict:nonblocking" -make_manifest_yml "$DRIFT_DIR/ci-jobs.yaml" \ - "alpha-gate:blocking" \ - "beta-gate:blocking" \ - "factory-claim-ledger-strict:nonblocking" - -if CI_POLICY_PARITY_AGENTS_PATH="$DRIFT_DIR/AGENTS.md" \ - CI_POLICY_PARITY_WORKFLOW_PATH="$DRIFT_DIR/.github/workflows/validate.yml" \ - CI_POLICY_PARITY_MANIFEST_PATH="$DRIFT_DIR/ci-jobs.yaml" \ - bash "$VALIDATOR" >"$TMP_DIR/drift.out" 2>&1; then - ko "T7 drifted fixture should exit non-zero but exited 0" - sed 's/^/ /' "$TMP_DIR/drift.out" >&2 -else - drift_rc=$? - if [[ "$drift_rc" -ne 0 ]] && grep -q 'CI_POLICY_PARITY' "$TMP_DIR/drift.out"; then - ok "T7 drifted fixture (advisory job missing from AGENTS.md) exits ${drift_rc} with parity error" - else - ko "T7 drifted fixture exited ${drift_rc} but did not emit a CI_POLICY_PARITY error" - sed 's/^/ /' "$TMP_DIR/drift.out" >&2 - fi -fi - -# T8: aligned state — same job set + non-blocking annotation aligned. -ALIGN_DIR="$TMP_DIR/align" -mkdir -p "$ALIGN_DIR/.github/workflows" -make_agents_md "$ALIGN_DIR/AGENTS.md" \ - "alpha-gate:blocking" \ - "beta-gate:blocking" \ - "factory-claim-ledger-strict:nonblocking" -make_workflow_yml "$ALIGN_DIR/.github/workflows/validate.yml" \ - "alpha-gate:blocking" \ - "beta-gate:blocking" \ - "factory-claim-ledger-strict:nonblocking" -make_manifest_yml "$ALIGN_DIR/ci-jobs.yaml" \ - "alpha-gate:blocking" \ - "beta-gate:blocking" \ - "factory-claim-ledger-strict:nonblocking" - -if CI_POLICY_PARITY_AGENTS_PATH="$ALIGN_DIR/AGENTS.md" \ - CI_POLICY_PARITY_WORKFLOW_PATH="$ALIGN_DIR/.github/workflows/validate.yml" \ - CI_POLICY_PARITY_MANIFEST_PATH="$ALIGN_DIR/ci-jobs.yaml" \ - bash "$VALIDATOR" >"$TMP_DIR/align.out" 2>&1; then - ok "T8 aligned fixture exits 0 (PASS)" -else - align_rc=$? - ko "T8 aligned fixture should exit 0 but exited ${align_rc}" - sed 's/^/ /' "$TMP_DIR/align.out" >&2 -fi - -# ---------- T9: live repo sanity ---------- -if bash "$VALIDATOR" >"$TMP_DIR/live.out" 2>&1; then - ok "T9 live repo passes validate-ci-policy-parity (no drift on main)" -else - live_rc=$? - ko "T9 live repo validate-ci-policy-parity exited ${live_rc} (drift on main?)" - sed 's/^/ /' "$TMP_DIR/live.out" >&2 -fi - -echo "" -if [[ "$FAIL" -gt 0 ]]; then - red "FAILED — $FAIL/$((PASS + FAIL)) checks failed" >&2 - exit 1 -fi - -green "PASSED — $PASS/$PASS checks passed" -exit 0 diff --git a/tests/integration/test-v218-commands.sh b/tests/integration/test-v218-commands.sh index 8cf48436b..23c92a64f 100755 --- a/tests/integration/test-v218-commands.sh +++ b/tests/integration/test-v218-commands.sh @@ -114,7 +114,7 @@ LEARNING_EOF cat > "$TMPDIR_TEST/.agents/learnings/2026-02-25-test-learning-gamma.md" <<'LEARNING_EOF' --- -title: Never skip pre-mortem for large changes +title: Never skip premortem for large changes id: test-learning-gamma date: 2026-02-25 maturity: sapling @@ -122,13 +122,13 @@ tags: [process, validation] --- ## Context -Large changes without pre-mortem review introduce avoidable risk. +Large changes without premortem review introduce avoidable risk. ## Lesson -Always run pre-mortem before implementing changes with 3+ files. +Always run premortem before implementing changes with 3+ files. ## Evidence -4/4 epics with pre-mortem had zero implementation bugs. +4/4 epics with premortem had zero implementation bugs. LEARNING_EOF # Create a sample session file @@ -182,13 +182,13 @@ cat > "$TMPDIR_TEST/.agents/findings/test-finding-alpha.md" <<'FINDING_EOF' --- id: test-finding-alpha title: Prefer registry-backed prevention -source_skill: post-mortem +source_skill: postmortem severity: high detectability: advisory status: active compiler_targets: [inject, lookup] scope_tags: [planning, flywheel] -applicable_when: [pre-mortem, planning] +applicable_when: [premortem, planning] applicable_languages: [go, shell] hit_count: 3 last_cited: 2026-03-09T12:00:00Z diff --git a/tests/integration/test_skill_builder.bats b/tests/integration/test_skill_builder.bats index 14b11843a..50ce0bc06 100644 --- a/tests/integration/test_skill_builder.bats +++ b/tests/integration/test_skill_builder.bats @@ -1,154 +1,58 @@ #!/usr/bin/env bats -# test_skill_builder.bats — L2 integration tests for skill-builder. -# -# Asserts that build.sh + init.sh: -# - Reject invalid usage with exit 2 -# - from-scratch (env-driven) creates skills/<name>/SKILL.md + codex parity -# - from-template (--like council) materializes a skeleton -# - absorb-external wraps an external SKILL.md in AgentOps frontmatter -# - All built skills self-audit at PASS or WARN (never FAIL) -# -# ISOLATION CONTRACT: the suite never mutates the real repo. setup_file copies -# the surfaces the factory touches (skills/, scripts/, docs/contracts + -# docs/reference, skills-codex-overrides/) into a scratch root and exports -# SKILL_BUILDER_REPO_ROOT (the HEAL_REPO_ROOT-style override honored by -# init.sh/build.sh), so scaffolds, dispositions-ledger rows, codex-catalog -# entries, and registry regen all land in the scratch tree. Before this, -# in-repo runs appended real bats-builder-test-* rows to -# docs/contracts/skill-dispositions.yaml + skills-codex-overrides/catalog.json -# and once corrupted the ledger tail. setup_file() { - REAL_REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../.." && pwd)" - export REAL_REPO_ROOT - - SCRATCH_ROOT="$BATS_FILE_TMPDIR/scratch-repo" - mkdir -p "$SCRATCH_ROOT/docs" "$SCRATCH_ROOT/skills-codex" - cp -R "$REAL_REPO_ROOT/skills" "$SCRATCH_ROOT/skills" - cp -R "$REAL_REPO_ROOT/scripts" "$SCRATCH_ROOT/scripts" - cp -R "$REAL_REPO_ROOT/docs/contracts" "$SCRATCH_ROOT/docs/contracts" - cp -R "$REAL_REPO_ROOT/docs/reference" "$SCRATCH_ROOT/docs/reference" - cp -R "$REAL_REPO_ROOT/skills-codex-overrides" "$SCRATCH_ROOT/skills-codex-overrides" - [ -f "$REAL_REPO_ROOT/registry.json" ] && cp "$REAL_REPO_ROOT/registry.json" "$SCRATCH_ROOT/registry.json" - # generate-registry.sh shells out to `git ls-files`; an empty repo is enough. - git -C "$SCRATCH_ROOT" init -q - - export SCRATCH_ROOT - export SKILL_BUILDER_REPO_ROOT="$SCRATCH_ROOT" + REAL_REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../.." && pwd)" + SCRATCH_ROOT="$BATS_FILE_TMPDIR/repo" + mkdir -p "$SCRATCH_ROOT" + cp -R "$REAL_REPO_ROOT/skills" "$SCRATCH_ROOT/skills" + cp -R "$REAL_REPO_ROOT/scripts" "$SCRATCH_ROOT/scripts" + cp -R "$REAL_REPO_ROOT/docs" "$SCRATCH_ROOT/docs" + cp -R "$REAL_REPO_ROOT/skills-codex" "$SCRATCH_ROOT/skills-codex" + cp -R "$REAL_REPO_ROOT/skills-codex-overrides" "$SCRATCH_ROOT/skills-codex-overrides" + cp -R "$REAL_REPO_ROOT/images" "$SCRATCH_ROOT/images" + cp -R "$REAL_REPO_ROOT/.claude-plugin" "$SCRATCH_ROOT/.claude-plugin" + cp "$REAL_REPO_ROOT/registry.json" "$SCRATCH_ROOT/registry.json" + export REAL_REPO_ROOT SCRATCH_ROOT + export SKILL_BUILDER_REPO_ROOT="$SCRATCH_ROOT" + BUILD_SH="$REAL_REPO_ROOT/skills/skill-builder/scripts/build.sh" + export BUILD_SH } -setup() { - # The scripts UNDER TEST stay the real checked-in ones; only the data root - # they operate on is redirected via SKILL_BUILDER_REPO_ROOT. - BUILD_SH="$REAL_REPO_ROOT/skills/skill-builder/scripts/build.sh" - SCRATCH_NAME="bats-builder-test-$$" +@test "builder rejects missing and unknown modes" { + run bash "$BUILD_SH" + [ "$status" -eq 2 ] + + run bash "$BUILD_SH" removed-mode example + [ "$status" -eq 2 ] } -@test "build.sh exists and is executable" { - [ -f "$BUILD_SH" ] - [ -r "$BUILD_SH" ] +@test "one invocation creates metadata source and derived projections" { + name="builder-contract-test" + run env \ + SKILL_TIER=execution \ + SKILL_DEPENDENCIES='[]' \ + SKILL_CAPABILITIES='["builder_contract_test"]' \ + SKILL_EFFECTS='[]' \ + bash "$BUILD_SH" from-scratch "$name" + [ "$status" -eq 0 ] + + source="$SCRATCH_ROOT/skills/$name/SKILL.md" + [ -f "$source" ] + grep -q '^ canonical_status: canonical$' "$source" + grep -q '^ disposition: keep_specialist$' "$source" + grep -q '^ capabilities: ' "$source" + grep -q 'builder_contract_test' "$source" + + [ -f "$SCRATCH_ROOT/skills-codex/$name/SKILL.md" ] + [ -f "$SCRATCH_ROOT/skills-codex/$name/prompt.md" ] + grep -q "\"name\": \"$name\"" "$SCRATCH_ROOT/skills/catalog.json" + grep -q '"structure_check_pass": true' \ + "$SCRATCH_ROOT/.agents/audits/${name}-build.json" } -@test "build.sh exits 2 on no args" { - run bash "$BUILD_SH" - [ "$status" -eq 2 ] -} - -@test "build.sh exits 2 on unknown mode" { - run bash "$BUILD_SH" not-a-real-mode foo - [ "$status" -eq 2 ] -} - -@test "build.sh from-scratch missing skill name exits 2" { - run bash "$BUILD_SH" from-scratch - [ "$status" -eq 2 ] -} - -@test "build.sh from-template missing --like flag fails" { - run bash "$BUILD_SH" from-template "${SCRATCH_NAME}-tpl-noflag" - [ "$status" -ne 0 ] -} - -@test "from-scratch creates SKILL.md + codex parity" { - local name="${SCRATCH_NAME}-fs" - SKILL_TIER=execution SKILL_INTENT_MODE=task \ - run bash "$BUILD_SH" from-scratch "$name" - [ "$status" -eq 0 ] - [ -f "$SCRATCH_ROOT/skills/$name/SKILL.md" ] - [ -f "$SCRATCH_ROOT/skills/$name/scripts/validate.sh" ] - [ -f "$SCRATCH_ROOT/skills-codex/$name/SKILL.md" ] - [ -f "$SCRATCH_ROOT/skills-codex/$name/prompt.md" ] -} - -@test "from-scratch produced codex SKILL.md has slim frontmatter (no skill_api_version)" { - local name="${SCRATCH_NAME}-fs-slim" - SKILL_TIER=execution SKILL_INTENT_MODE=task \ - bash "$BUILD_SH" from-scratch "$name" >/dev/null 2>&1 - run grep -c '^skill_api_version:' "$SCRATCH_ROOT/skills-codex/$name/SKILL.md" - [ "$status" -ne 0 ] || [ "$output" = "0" ] -} - -@test "from-scratch frontmatter name matches directory" { - local name="${SCRATCH_NAME}-fs-name" - SKILL_TIER=execution SKILL_INTENT_MODE=task \ - bash "$BUILD_SH" from-scratch "$name" >/dev/null 2>&1 - run grep -E "^name: $name$" "$SCRATCH_ROOT/skills/$name/SKILL.md" - [ "$status" -eq 0 ] -} - -@test "from-scratch self-audit chain runs (build aborts on auditor FAIL)" { - local name="${SCRATCH_NAME}-fs-audit" - SKILL_TIER=execution SKILL_INTENT_MODE=task \ - run bash "$BUILD_SH" from-scratch "$name" - # The build.sh tail invokes audit.sh; build.sh exits 1 if auditor returns FAIL. - # New skill skeletons may PASS or WARN but must not FAIL. - [ "$status" -eq 0 ] -} - -@test "from-scratch appends its dispositions row + codex catalog entry to the SCRATCH copies" { - local name="${SCRATCH_NAME}-fs-plumbing" - SKILL_TIER=execution SKILL_INTENT_MODE=task \ - bash "$BUILD_SH" from-scratch "$name" >/dev/null 2>&1 - grep -qE "^[[:space:]]*-[[:space:]]+skill:[[:space:]]+${name}[[:space:]]*$" \ - "$SCRATCH_ROOT/docs/contracts/skill-dispositions.yaml" - grep -q "\"$name\"" "$SCRATCH_ROOT/skills-codex-overrides/catalog.json" -} - -@test "from-template --like council produces skeleton" { - local name="${SCRATCH_NAME}-tpl" - run bash "$BUILD_SH" from-template "$name" --like council - [ "$status" -eq 0 ] - [ -f "$SCRATCH_ROOT/skills/$name/SKILL.md" ] -} - -@test "absorb-external wraps an external SKILL.md" { - local ext="${BATS_TEST_TMPDIR}/external-skill.md" - cat >"$ext" <<'EOF' ---- -name: external-source -description: 'External skill body to absorb.' ---- -# External body - -A short external skill body. -EOF - local name="${SCRATCH_NAME}-abs" - run bash "$BUILD_SH" absorb-external "$name" --from "$ext" - [ "$status" -eq 0 ] - [ -f "$SCRATCH_ROOT/skills/$name/SKILL.md" ] -} - -@test "absorb-external requires --from path" { - run bash "$BUILD_SH" absorb-external "${SCRATCH_NAME}-abs-no-from" - [ "$status" -ne 0 ] -} - -@test "isolation: no scaffold, ledger row, or catalog entry leaked into the real repo" { - # Runs after the scaffolding tests above (bats executes tests in file order): - # the real tree must carry zero trace of any bats-builder-test-* skill. - [ -z "$(find "$REAL_REPO_ROOT/skills" "$REAL_REPO_ROOT/skills-codex" \ - -maxdepth 1 -name 'bats-builder-test-*' -print -quit 2>/dev/null)" ] - ! grep -q 'bats-builder-test-' "$REAL_REPO_ROOT/docs/contracts/skill-dispositions.yaml" - ! grep -q 'bats-builder-test-' "$REAL_REPO_ROOT/skills-codex-overrides/catalog.json" - ! ls "$REAL_REPO_ROOT/.agents/audits/"bats-builder-test-*-build.json >/dev/null 2>&1 +@test "builder does not create lifecycle ledgers or touch the real repository" { + [ ! -e "$SCRATCH_ROOT/docs/contracts/skill-dispositions.yaml" ] + [ ! -e "$REAL_REPO_ROOT/skills/builder-contract-test" ] + [ ! -e "$REAL_REPO_ROOT/skills-codex/builder-contract-test" ] + [ ! -e "$REAL_REPO_ROOT/.agents/audits/builder-contract-test-build.json" ] } diff --git a/tests/lib/e2e-factory.sh b/tests/lib/e2e-factory.sh index aa6041ec0..f97e3fd97 100644 --- a/tests/lib/e2e-factory.sh +++ b/tests/lib/e2e-factory.sh @@ -70,11 +70,7 @@ e2e_factory_repo() { # Resolves an ao binary into <build-dir>/ao. Honors: # PROOF_AO_BIN — explicit override (must point at an executable) # PROOF_FORCE_BUILD=1 — disable reuse and always go build from source -# PROOF_BUILD_TAGS — go build tags (e.g. "flywheel legacy" for proofs -# that exercise ADR-0012 archived commands) -# <repo-root>/cli/bin/ao — auto-detected reuse when present (tag-less only: -# the prebuilt repo binary is the default spine build -# and cannot satisfy a tagged request) +# <repo-root>/cli/bin/ao — auto-detected reuse when present # Falls back to a fresh `go build` under <repo-root>/cli. e2e_factory_ao_bin() { local build_dir="$1" repo_root="$2" @@ -82,9 +78,8 @@ e2e_factory_ao_bin() { [[ -n "$repo_root" ]] || _e2e_factory_die "e2e_factory_ao_bin: repo_root is required" mkdir -p "$build_dir" local out="$build_dir/ao" - local tags="${PROOF_BUILD_TAGS:-}" local src="${PROOF_AO_BIN:-}" - if [[ -z "$src" && -z "$tags" && "${PROOF_FORCE_BUILD:-0}" != "1" && -x "$repo_root/cli/bin/ao" ]]; then + if [[ -z "$src" && "${PROOF_FORCE_BUILD:-0}" != "1" && -x "$repo_root/cli/bin/ao" ]]; then src="$repo_root/cli/bin/ao" fi if [[ -n "$src" && -x "$src" ]]; then @@ -94,11 +89,7 @@ e2e_factory_ao_bin() { || _e2e_factory_die "no prebuilt ao binary and 'go' is not on PATH" ( cd "$repo_root/cli" - if [[ -n "$tags" ]]; then - go build -tags "$tags" -o "$out" ./cmd/ao - else - go build -o "$out" ./cmd/ao - fi + go build -o "$out" ./cmd/ao ) >/dev/null || _e2e_factory_die "go build ./cmd/ao failed" fi chmod +x "$out" diff --git a/tests/scripts/agentops-product-boundary.bats b/tests/scripts/agentops-product-boundary.bats index 7be277006..2bef6049b 100644 --- a/tests/scripts/agentops-product-boundary.bats +++ b/tests/scripts/agentops-product-boundary.bats @@ -4,194 +4,77 @@ setup() { REPO_ROOT="$(cd "$BATS_TEST_DIRNAME/../.." && pwd)" ACTIVE_AUTHORITY=( AGENTS.md - PROGRAM.md PRODUCT.md - GOALS.md + README.md docs/CI-CD.md - docs/3.0.md - docs/MIGRATION.md - docs/UPGRADING.md - docs/agent-workflow-reference.md - docs/adr/ADR-0012-focus-surface-on-membrane-bookkeeper-archive-satellites.md - docs/architecture/build-tags.md - docs/architecture/go-cli-architecture-guide.md docs/architecture/operating-loop.md - docs/contracts/local-pre-push-gate-retirement.md - docs/contracts/pawls.md - docs/doctrine/operating-discipline.md - docs/documentation-index.md - docs/newcomer-guide.md - docs/software-factory.md - cli/README.md + cli/cmd/ao/root.go + cli/cmd/ao/demo.go ) } scan_obsolete_authority() { local file="$1" - if [[ ! -f "$file" ]]; then - echo "invalid harness: missing authority file: $file" >&2 - return 2 - fi - - local active_text matches - active_text="$(grep -Ev \ - '^[[:space:]]*>?[[:space:]]*HISTORICAL EVIDENCE \(NON-AUTHORITATIVE\):' \ - "$file" || true)" - matches="$(printf '%s\n' "$active_text" | grep -Eni \ - 'ao[[:space:]]+land|ao[[:space:]]+pawl|/pawl-review|run[^[:cntrl:]]*pawl|pawl(-review|-land)?[[:space:]]+(gate|route|review|verdict|authority)|push-as-CI|never[[:space:]]+delete|strangl[a-z]*|archiv(e|ed|ing)[^[:cntrl:]]{0,240}(rather than|instead of|not)[^[:cntrl:]]{0,80}delet|((release[[:space:]]+)?tag[[:space:]]+push|CI)[^[:cntrl:]]{0,160}(run|produce|emit|write|record)[^[:cntrl:]]{0,120}Validate[[:space:]]+verdict|Validation[[:space:]]+Gates[^[:cntrl:]]*(premortem|postmortem|council)|Knowledge[[:space:]]+Flywheel[^[:cntrl:]]*postmortem|semantic[[:space:]]+pre-push|local[[:space:]]+gate[^[:cntrl:]]*release[[:space:]]+authority' \ - || true)" - if [[ -n "$matches" ]]; then - printf 'obsolete authority in %s:\n%s\n' "$file" "$matches" >&2 - return 1 - fi + grep -Eni \ + 'ao[[:space:]]+(land|pawl|plan-pawl|done|close|governor|yield|claim|next-work|worktree|validate|converge|reconcile|membrane|crank)|/pawl-review|semantic[[:space:]]+pre-push|push-as-CI|Validate[[:space:]]+verdict[^[:cntrl:]]*(push|merge|release|delivery)|automatic[^[:cntrl:]]*(repair|retry|replan)' \ + "$file" } require_text() { - local relative="$1" expected="$2" - local file="$REPO_ROOT/$relative" - if [[ ! -f "$file" ]]; then - echo "invalid harness: missing authority file: $relative" >&2 - return 2 - fi - grep -Fq -- "$expected" "$file" || { - echo "$relative is missing product-boundary contract: $expected" >&2 + grep -Fq -- "$2" "$REPO_ROOT/$1" || { + echo "$1 is missing: $2" >&2 return 1 } } -@test "active authority teaches the lean four-umbrella product boundary" { - local relative - for relative in "${ACTIVE_AUTHORITY[@]}"; do - run scan_obsolete_authority "$REPO_ROOT/$relative" - [ "$status" -eq 0 ] || { +@test "active authority teaches one bounded experiment and stop" { + local file + for file in "${ACTIVE_AUTHORITY[@]}"; do + run scan_obsolete_authority "$REPO_ROOT/$file" + [ "$status" -eq 1 ] || { echo "$output" >&2 - return "$status" + return 1 } done require_text AGENTS.md \ - "Discovery, Crank, Validate, and Learn are the four lifecycle umbrellas." + "RPI -> Plan -> Implement -> fresh Validate -> durable verdict -> report and stop" + require_text PRODUCT.md \ + "AgentOps is not a new GitLab, CI service, tracker, merge queue, delivery system" require_text docs/architecture/operating-loop.md \ - "A verdict is immutable evidence from fresh context." + "RPI invokes Plan, Implement, and Validate at most once and then stops." require_text docs/CI-CD.md \ "Repositories own delivery policy for local and cloud agents." - require_text docs/adr/ADR-0012-focus-surface-on-membrane-bookkeeper-archive-satellites.md \ - "Delete legacy code directly in the same owning leaf that installs its replacement or removes its last consumer." - require_text docs/architecture/go-cli-architecture-guide.md \ - "CLI implementation deletion is owned by K5, K7, K9, and the exact CLI leaves." - require_text docs/architecture/go-cli-architecture-guide.md \ - "Build-profile deletion is owned by F4." - require_text docs/architecture/go-cli-architecture-guide.md \ - "Generated command-reference deletion or regeneration is owned by D2." - require_text docs/documentation-index.md \ - "| **Discovery** | Shapes accepted behavior and consumes Premortem as its plan stress-test |" - require_text docs/documentation-index.md \ - "| **Validate** | Binding judgment umbrella: one author-distinct fresh context judges a frozen candidate once |" - require_text docs/documentation-index.md \ - "| **Learn** | Records one minimal consequence before optional Postmortem |" - require_text docs/documentation-index.md \ - "Council is optional validator composition inside Premortem or Validate; it is" - require_text AGENTS.md \ - "docs/contracts/agents-documentation-authority.yaml" } -@test "negative fixture rejects a model verdict as a Git delivery gate" { - local fixture="$BATS_TEST_TMPDIR/model-gate.md" - printf '%s\n' 'Run the Pawl before Git delivery.' >"$fixture" - - run scan_obsolete_authority "$fixture" - - [ "$status" -eq 1 ] - [[ "$output" == *"Pawl"* ]] -} - -@test "negative fixture rejects repository delivery through ao land" { - local fixture="$BATS_TEST_TMPDIR/cli-delivery.md" - printf '%s\n' 'The terminal command is ao land AGE-123.' >"$fixture" - - run scan_obsolete_authority "$fixture" - - [ "$status" -eq 1 ] - [[ "$output" == *"ao land"* ]] -} - -@test "negative fixture rejects compatibility-first retention" { - local fixture="$BATS_TEST_TMPDIR/retention.md" - printf '%s\n' 'Use a compatibility-first strangler and never delete the old owner.' >"$fixture" - - run scan_obsolete_authority "$fixture" - - [ "$status" -eq 1 ] - [[ "$output" == *"compatibility-first strangler"* ]] - [[ "$output" == *"never delete"* ]] -} - -@test "negative fixture rejects semantic push-hook authority" { - local fixture="$BATS_TEST_TMPDIR/push-hook.md" - printf '%s\n' 'The local gate is the release authority; require semantic pre-push review.' >"$fixture" - - run scan_obsolete_authority "$fixture" - - [ "$status" -eq 1 ] - [[ "$output" == *"local gate is the release authority"* ]] - [[ "$output" == *"semantic pre-push"* ]] -} - -@test "negative fixture rejects strangled and strangling migration variants" { - local fixture="$BATS_TEST_TMPDIR/incremental-cut.md" - printf '%s\n' \ - 'Legacy commands are being strangled while the new root lands.' \ - 'The migration is strangling one family at a time.' >"$fixture" - - run scan_obsolete_authority "$fixture" - - [ "$status" -eq 1 ] - [[ "$output" == *"strangled"* ]] - [[ "$output" == *"strangling"* ]] -} - -@test "negative fixture rejects archive-rather-than-delete retention" { - local fixture="$BATS_TEST_TMPDIR/archive-retention.md" - printf '%s\n' \ - 'Archive the satellites behind build tags rather than delete them.' >"$fixture" - - run scan_obsolete_authority "$fixture" - - [ "$status" -eq 1 ] - [[ "$output" == *"Archive the satellites"* ]] -} - -@test "negative fixture rejects deterministic CI claiming a Validate verdict" { - local fixture="$BATS_TEST_TMPDIR/ci-verdict.md" - printf '%s\n' \ - 'Release tag pushes run a full Validate verdict for the tagged SHA.' >"$fixture" - - run scan_obsolete_authority "$fixture" - - [ "$status" -eq 1 ] - [[ "$output" == *"Validate verdict"* ]] -} - -@test "negative fixture rejects obsolete parallel umbrella routing" { - local fixture="$BATS_TEST_TMPDIR/umbrella-routing.md" - printf '%s\n' \ - '| Validation Gates | /council, /premortem, /postmortem |' \ - '| Knowledge Flywheel | /postmortem --quick, /curate |' >"$fixture" - - run scan_obsolete_authority "$fixture" - - [ "$status" -eq 1 ] - [[ "$output" == *"Validation Gates"* ]] - [[ "$output" == *"Knowledge Flywheel"* ]] -} - -@test "bounded historical evidence remains legal" { - local fixture="$BATS_TEST_TMPDIR/historical-evidence.md" - printf '%s\n' \ - 'HISTORICAL EVIDENCE (NON-AUTHORITATIVE): CI produced a Validate verdict while commands were being strangled and satellites were archived rather than deleted.' \ - >"$fixture" - - run scan_obsolete_authority "$fixture" +@test "optional strategies and adapters are not core dependencies" { + run python3 - "$REPO_ROOT" <<'PY' +from pathlib import Path +import sys +import yaml +root = Path(sys.argv[1]) +actual = {} +for name in ("rpi", "plan", "implement", "validate"): + data = yaml.safe_load((root / "skills" / name / "SKILL.md").read_text().split("---", 2)[1]) + actual[name] = set(data["metadata"]["dependencies"]) +expected = {"rpi": {"plan", "implement", "validate"}, "plan": set(), "implement": set(), "validate": set()} +if actual != expected: + raise SystemExit(actual) +PY [ "$status" -eq 0 ] } + +@test "negative fixtures reject semantic Git authority and automatic continuation" { + fixture="$BATS_TEST_TMPDIR/obsolete.md" + printf '%s\n' \ + 'Run ao land after the review.' \ + 'Require semantic pre-push approval.' \ + 'A FAIL triggers automatic repair.' >"$fixture" + + run scan_obsolete_authority "$fixture" + [ "$status" -eq 0 ] + [[ "$output" == *"ao land"* ]] + [[ "$output" == *"semantic pre-push"* ]] + [[ "$output" == *"automatic repair"* ]] +} diff --git a/tests/scripts/ao-verify-receipts.bats b/tests/scripts/ao-verify-receipts.bats deleted file mode 100644 index 0111cc923..000000000 --- a/tests/scripts/ao-verify-receipts.bats +++ /dev/null @@ -1,125 +0,0 @@ -#!/usr/bin/env bats -# ao-verify-receipts.bats — `ao verify receipts` renders a repo's membrane-receipts -# proof page from the EMBEDDED generator on the stranger path (no AgentOps checkout): -# every number derived from that repo's OWN provenance ledger, chain-verify gated, -# fail-closed on tamper, output landing in the target repo. (age-rk3r.12) -# -# The ledger is built with the PRODUCTION writer (`ao provenance emit-verdict`) over -# real commits in a throwaway git repo — the real persisted shape, never a hand-built -# fixture. The throwaway repo is NOT an AgentOps checkout and the ao binary lives -# outside it, so `ao verify receipts` deterministically takes the embedded path. - -setup_file() { - REPO_ROOT="$(cd "$BATS_TEST_DIRNAME/../.." && pwd)" - export REPO_ROOT - # Build ao from THIS source (it must carry the new `verify receipts` command) to a - # path OUTSIDE the agentops checkout, so `ao verify receipts` run inside the - # throwaway repo takes the stranger/embedded path (aoBinaryInside is false and the - # repo is not an AgentOps checkout). NEVER use a PATH ao — it may predate this cmd. - AO_BIN="$BATS_FILE_TMPDIR/ao" - (cd "$REPO_ROOT/cli" && go build -o "$AO_BIN" ./cmd/ao) - export AO_BIN - for tool in git jq; do - command -v "$tool" >/dev/null 2>&1 || { echo "# missing required tool: $tool" >&3; return 1; } - done -} - -# emit_verdict <bead> <sha> <disposition> — write a real pawl-verdict artifact and -# append its verdict->commit edge to the ledger via the production Go writer. -emit_verdict() { - local bead="$1" sha="$2" disp="$3" - local f="$REPO/.agents/pawl-verdicts/${bead}-${sha:0:7}-${disp}.json" - printf '{"bead_id":"%s","head_sha":"%s","disposition":"%s"}\n' "$bead" "$sha" "$disp" > "$f" - ( cd "$REPO" && "$AO_BIN" provenance emit-verdict --file "$f" ) >/dev/null -} - -setup() { - # Throwaway git repo (NOT an AgentOps checkout) under the bats-managed tmp, which - # is outside the agentops checkout → the stranger path is forced. - REPO="$BATS_TEST_TMPDIR/stranger" - mkdir -p "$REPO/.agents/pawl-verdicts" - git -C "$REPO" init --quiet - git -C "$REPO" config user.email t@e.com - git -C "$REPO" config user.name T - printf 'v1\n' > "$REPO/app.txt"; git -C "$REPO" add app.txt; git -C "$REPO" commit --quiet -m "feat: base (str-1)" - SHA_A="$(git -C "$REPO" rev-parse HEAD)" - printf 'v2\n' >> "$REPO/app.txt"; git -C "$REPO" add app.txt; git -C "$REPO" commit --quiet -m "fix: patch (str-1)" - SHA_B="$(git -C "$REPO" rev-parse HEAD)" - # A REFUTED-then-fixed arc for str-1 (refuted on A, CONFIRMED on B) + one plain - # CONFIRMED for str-2 → 3 verdict edges, 2 CONFIRMED / 1 REFUTED / 1 caught arc. - emit_verdict str-1 "$SHA_A" REFUTED - emit_verdict str-1 "$SHA_B" CONFIRMED - emit_verdict str-2 "$SHA_B" CONFIRMED - LEDGER="$REPO/docs/provenance/ledger.jsonl" - OUT_MD="$REPO/docs/evidence/membrane-receipts.md" - OUT_JSON="$REPO/docs/releases/membrane-receipts.json" -} - -run_receipts() { - run bash -c "cd '$REPO' && '$AO_BIN' verify receipts" - echo "# ao verify receipts exit=$status" >&3 - printf '%s\n' "$output" | sed 's/^/# /' >&3 -} - -jqv() { jq -r "$1" "$OUT_JSON"; } - -@test "ao verify receipts renders the proof page from the repo's OWN ledger (stranger/embedded path)" { - run_receipts - [ "$status" -eq 0 ] - [ -f "$OUT_MD" ] - [ -f "$OUT_JSON" ] - - echo "# every number derived from THIS repo's 3-edge ledger" >&3 - [ "$(jqv '.totals.ledger_records')" = "3" ] - [ "$(jqv '.totals.verdict_events')" = "3" ] - [ "$(jqv '.dispositions.CONFIRMED')" = "2" ] - [ "$(jqv '.dispositions.REFUTED')" = "1" ] - [ "$(jqv '.caught_defects.refuted_then_fixed')" = "1" ] - [ "$(jqv '.source.chain_verified')" = "true" ] - - echo "# the human page carries the same derived numbers + the real short SHA" >&3 - grep -Fq '| Ledger records | 3 |' "$OUT_MD" - grep -Fq '| CONFIRMED verdicts | 2 |' "$OUT_MD" - grep -Fq '| REFUTED verdicts | 1 |' "$OUT_MD" - grep -Fq '| REFUTED-then-fixed arcs (caught defects) | 1 |' "$OUT_MD" - grep -Fq "${SHA_A:0:7}" "$OUT_MD" - grep -Eq '^Generated: [0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9:]{8}Z$' "$OUT_MD" - - echo "# output landed in the TARGET repo docs/evidence + docs/releases" >&3 - [ -f "$REPO/docs/evidence/membrane-receipts.md" ] - [ -f "$REPO/docs/releases/membrane-receipts.json" ] -} - -@test "ao verify receipts REFUSES on a tampered ledger, names the break, writes nothing (fail-closed)" { - run_receipts - [ "$status" -eq 0 ] - before="$(shasum "$OUT_MD" | cut -d' ' -f1)" - - echo "# tamper: flip a disposition without re-hashing the chain" >&3 - sed 's/disposition=REFUTED/disposition=CONFIRMED/' "$LEDGER" > "$LEDGER.t" && mv "$LEDGER.t" "$LEDGER" - - run_receipts - [ "$status" -eq 1 ] - [[ "$output" == *"REFUSING to render"* ]] - [[ "$output" == *"verification FAILED"* ]] - [[ "$output" == *"payload_hash mismatch"* ]] - - echo "# the refused run wrote nothing — the prior page is untouched" >&3 - after="$(shasum "$OUT_MD" | cut -d' ' -f1)" - [ "$before" = "$after" ] -} - -@test "ao verify receipts (embedded) matches the in-repo generator byte-for-byte (modulo Generated)" { - run_receipts - [ "$status" -eq 0 ] - cp "$OUT_MD" "$BATS_TEST_TMPDIR/via-cmd.md" - - echo "# the canonical in-repo generator on the SAME ledger must produce the same page" >&3 - run bash -c "cd '$REPO' && env PROVENANCE_LEDGER='$LEDGER' \ - RECEIPTS_MD='$BATS_TEST_TMPDIR/via-script.md' RECEIPTS_JSON='$BATS_TEST_TMPDIR/via-script.json' \ - AO_BIN='$AO_BIN' '$REPO_ROOT/scripts/gen-membrane-receipts.sh'" - [ "$status" -eq 0 ] - - diff <(grep -v '^Generated: ' "$BATS_TEST_TMPDIR/via-cmd.md") \ - <(grep -v '^Generated: ' "$BATS_TEST_TMPDIR/via-script.md") -} diff --git a/tests/scripts/append-skill-disposition.bats b/tests/scripts/append-skill-disposition.bats deleted file mode 100644 index 997587b25..000000000 --- a/tests/scripts/append-skill-disposition.bats +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env bats -# ag-cw2y item-1 scaffold-half: skill-builder must append a skill-dispositions.yaml -# row for a new skill so it is one-shot-green against heal.sh Check 12. The helper -# is idempotent and repo-root-injectable (so init.sh can call it and tests can -# fixture it). - -setup() { - HELPER="$BATS_TEST_DIRNAME/../../scripts/append-skill-disposition.sh" - FIX="$(mktemp -d)" - mkdir -p "$FIX/docs/contracts" - cat > "$FIX/docs/contracts/skill-dispositions.yaml" <<'EOF' -dispositions: - - skill: existing - domain: "BC1 Corpus" - hexagonal_role: domain - disposition: keep - rationale: "already here" -EOF -} - -teardown() { rm -rf "$FIX"; } - -@test "appends a dispositions row for a new skill" { - run bash "$HELPER" newskill "$FIX" - [ "$status" -eq 0 ] - grep -qE "^[[:space:]]*-[[:space:]]+skill:[[:space:]]+newskill[[:space:]]*$" "$FIX/docs/contracts/skill-dispositions.yaml" -} - -@test "appended row carries a valid BC domain and a TODO rationale" { - bash "$HELPER" newskill "$FIX" - # The row's domain must be a real bounded context (so check-bounded-contexts-drift passes) - run grep -A4 'skill: newskill' "$FIX/docs/contracts/skill-dispositions.yaml" - [[ "$output" == *"BC4 Factory"* ]] - [[ "$output" == *"TODO"* ]] -} - -@test "is idempotent — running twice does not duplicate the row" { - bash "$HELPER" newskill "$FIX" - bash "$HELPER" newskill "$FIX" - count=$(grep -cE "^[[:space:]]*-[[:space:]]+skill:[[:space:]]+newskill[[:space:]]*$" "$FIX/docs/contracts/skill-dispositions.yaml") - [ "$count" -eq 1 ] -} - -@test "does not touch an already-present skill" { - run bash "$HELPER" existing "$FIX" - [ "$status" -eq 0 ] - count=$(grep -cE "^[[:space:]]*-[[:space:]]+skill:[[:space:]]+existing[[:space:]]*$" "$FIX/docs/contracts/skill-dispositions.yaml") - [ "$count" -eq 1 ] -} - -@test "is newline-safe — appending to a file missing its trailing newline does not fuse rows" { - # Strip the trailing newline so the ledger ends mid-line (the corruption - # trigger: a bare `cat >>` used to fuse the new row onto the rationale line). - printf '%s' "$(cat "$FIX/docs/contracts/skill-dispositions.yaml")" > "$FIX/docs/contracts/skill-dispositions.yaml" - [ -n "$(tail -c1 "$FIX/docs/contracts/skill-dispositions.yaml")" ] # precondition: no trailing \n - - run bash "$HELPER" newskill "$FIX" - [ "$status" -eq 0 ] - # The prior last line must survive intact, un-fused with the new row. - grep -qE '^[[:space:]]*rationale:[[:space:]]+"already here"$' "$FIX/docs/contracts/skill-dispositions.yaml" - # And the new row must start on its own line. - grep -qE "^[[:space:]]*-[[:space:]]+skill:[[:space:]]+newskill[[:space:]]*$" "$FIX/docs/contracts/skill-dispositions.yaml" -} diff --git a/tests/scripts/brainstorm-discovery-ideation.bats b/tests/scripts/brainstorm-discovery-ideation.bats deleted file mode 100644 index 88476d95a..000000000 --- a/tests/scripts/brainstorm-discovery-ideation.bats +++ /dev/null @@ -1,155 +0,0 @@ -#!/usr/bin/env bats - -# Regression tests for the idea-wizard generate-winnow methodology wrapped into -# /discovery ideation path (brainstorm absorbed, ag-s43tg; orig ag-yw0). -# -# This is a documentation/wiring gate: the generate-winnow methodology lives in -# SKILL.md prose + references + .feature scenarios (skills are markdown contracts, -# not executable code), so these tests assert the methodology is present and wired -# across the Claude skills, the ported references, and the Codex twins. -# -# Detailed logging (per the operationalize discipline being documented): each test -# echoes what it is checking before asserting, so a failure log names the exact -# missing surface. - -setup() { - REPO_ROOT="$(cd "$BATS_TEST_DIRNAME/../.." && pwd)" - BRAINSTORM="$REPO_ROOT/skills/discovery/references/goal-clarification-brainstorm.md" - DISCOVERY="$REPO_ROOT/skills/discovery/SKILL.md" - CODEX_BRAINSTORM="$REPO_ROOT/skills/discovery/references/goal-clarification-brainstorm.md" - CODEX_DISCOVERY="$REPO_ROOT/skills-codex/discovery/SKILL.md" - REF_IDEATION="$REPO_ROOT/skills/discovery/references/ideation-mode.md" - REF_RUBRIC="$REPO_ROOT/skills/discovery/references/idea-rubric.md" - REF_BEADS="$REPO_ROOT/skills/discovery/references/bead-operationalization.md" - FEAT_BRAINSTORM="$REPO_ROOT/skills/discovery/references/brainstorm.feature" - FEAT_DISCOVERY="$REPO_ROOT/skills/discovery/references/discovery.feature" -} - -# Assert a file contains a literal substring, logging the probe first. -assert_has() { - local file="$1" needle="$2" - echo "# checking: $(basename "$(dirname "$file")")/$(basename "$file") contains: $needle" >&3 - grep -qF -- "$needle" "$file" -} - -# Assert a file does NOT match an extended-regex pattern (anti-leak guard). -# Uses `run` + status so it fails the test on any bats version (no `run !`). -assert_lacks_regex() { - local file="$1" pattern="$2" - echo "# anti-leak: $(basename "$(dirname "$file")")/$(basename "$file") must NOT match: $pattern" >&3 - run grep -qE -- "$pattern" "$file" - [ "$status" -ne 0 ] -} - -# Assert a file does NOT contain a literal substring (anti-leak guard). -assert_lacks() { - local file="$1" needle="$2" - echo "# anti-leak: $(basename "$(dirname "$file")")/$(basename "$file") must NOT contain: $needle" >&3 - run grep -qF -- "$needle" "$file" - [ "$status" -ne 0 ] -} - -@test "all four SKILL.md surfaces and three ported references exist" { - for f in "$BRAINSTORM" "$DISCOVERY" "$CODEX_BRAINSTORM" "$CODEX_DISCOVERY" \ - "$REF_IDEATION" "$REF_RUBRIC" "$REF_BEADS"; do - echo "# exists? $f" >&3 - [ -f "$f" ] - done -} - -@test "brainstorm SKILL documents ideation mode with the generate-winnow funnel" { - assert_has "$BRAINSTORM" "Ideation Mode" - assert_has "$BRAINSTORM" "--ideate" - assert_has "$BRAINSTORM" "Generate 30" - assert_has "$BRAINSTORM" "best 5" - assert_has "$BRAINSTORM" "next best 10" - assert_has "$BRAINSTORM" "portfolio of " - assert_has "$BRAINSTORM" "ranked best-to-worst" -} - -@test "brainstorm SKILL preserves the existing four-phase goal-clarification flow" { - # Additive guarantee: the original phases must remain present unchanged. - assert_has "$BRAINSTORM" "Phase 1: Assess Clarity" - assert_has "$BRAINSTORM" "Phase 2: Understand the Idea" - assert_has "$BRAINSTORM" "Phase 3: Explore Approaches" - assert_has "$BRAINSTORM" "Phase 4: Capture Design" - assert_has "$BRAINSTORM" "Phase 3b: Adversarial Critique" -} - -@test "brainstorm SKILL links all three ported references" { - assert_has "$BRAINSTORM" "references/ideation-mode.md" - assert_has "$BRAINSTORM" "references/idea-rubric.md" - assert_has "$BRAINSTORM" "references/bead-operationalization.md" -} - -@test "idea-rubric reference carries all ten evaluation dimensions" { - for dim in Robust Reliable Performant Intuitive User-friendly Ergonomic \ - Useful Compelling Accretive Pragmatic; do - assert_has "$REF_RUBRIC" "$dim" - done -} - -@test "bead-operationalization reference uses br and bans the retired bd tracker" { - # Tracker contracts migrated bd -> br (commit f650d41bb); the reference docs - # are now repo-native br. The test follows the migration: assert br, ban bd. - assert_has "$REF_BEADS" "br create" - assert_has "$REF_BEADS" "br dep add" - assert_has "$REF_BEADS" "DO NOT OVERSIMPLIFY" - assert_has "$REF_BEADS" "DO NOT LOSE FEATURES" - # The retired bd tracker must NOT leak back into the repo-native docs. - assert_lacks_regex "$REF_BEADS" '\bbd\s+(list|create|dep|ready|--robot)' - assert_lacks_regex "$REF_IDEATION" '\bbd\s+(list|create|dep|ready|--robot)' - assert_lacks_regex "$REF_RUBRIC" '\bbd\s+(list|create|dep|ready|--robot)' -} - -@test "ideation-mode reference documents the mode-selection rule and grounding" { - assert_has "$REF_IDEATION" "When to use which mode" - assert_has "$REF_IDEATION" "AGENTS.md" - assert_has "$REF_IDEATION" "br list --json" - assert_has "$REF_IDEATION" "br list --status closed --json" -} - -@test "discovery SKILL wires the open-ended ideate path with operationalize and refine" { - assert_has "$DISCOVERY" "Open-Ended Path" - assert_has "$DISCOVERY" "--ideate" - assert_has "$DISCOVERY" "ideation mode" - assert_has "$DISCOVERY" "Operationalize" - assert_has "$DISCOVERY" "self-documenting" - assert_has "$DISCOVERY" "Refine in plan space" - assert_has "$DISCOVERY" "4-5 refinement passes" -} - -@test "discovery SKILL preserves the strict-delegation contract" { - # Additive guarantee: the contract that predates this change must remain. - assert_has "$DISCOVERY" "Strict Delegation Contract" - assert_has "$DISCOVERY" "strict-delegation-contract.md" - # The new path must explicitly keep delegation (no inlining the 30-idea gen). - assert_has "$DISCOVERY" "do NOT inline the 30-idea generation" -} - -@test "codex twins mirror ideation mode with codex notation and no Claude primitives" { - assert_has "$CODEX_BRAINSTORM" "Ideation Mode" - assert_has "$CODEX_BRAINSTORM" '--ideate' - assert_has "$CODEX_DISCOVERY" "Open-Ended Path" - assert_has "$CODEX_DISCOVERY" 'internal modes (absorbed, ag-s43tg)' - # Codex bodies must not leak Claude-era primitives (parity rule). The - # brainstorm reference (goal-clarification-brainstorm.md) is the CLAUDE-side - # surface and legitimately uses AskUserQuestion, so the anti-leak guard - # applies to the genuine codex twin (skills-codex/discovery/SKILL.md), not - # to the shared Claude reference. - assert_lacks "$CODEX_DISCOVERY" "AskUserQuestion" - # Codex must use $skill notation, not /skill, for the new content. - assert_lacks "$CODEX_DISCOVERY" "/brainstorm --ideate" -} - -@test "feature specs add ideation and operationalize scenarios additively" { - assert_has "$FEAT_BRAINSTORM" "triggers ideation mode" - assert_has "$FEAT_BRAINSTORM" "winnows ruthlessly to a ranked five" - assert_has "$FEAT_BRAINSTORM" "expands the portfolio to fifteen" - assert_has "$FEAT_DISCOVERY" "generate-winnow path" - assert_has "$FEAT_DISCOVERY" "operationalizes the winnowed portfolio" - assert_has "$FEAT_DISCOVERY" "refines beads in plan space" - # The original scenarios must survive untouched (scenario-hash stability). - assert_has "$FEAT_BRAINSTORM" "a goal is clarified through the four phases" - assert_has "$FEAT_DISCOVERY" "Discovery delegates to Plan" -} diff --git a/tests/scripts/check-applied-ood-headroom.bats b/tests/scripts/check-applied-ood-headroom.bats index 4c6b98a97..c25da4684 100644 --- a/tests/scripts/check-applied-ood-headroom.bats +++ b/tests/scripts/check-applied-ood-headroom.bats @@ -76,6 +76,5 @@ FAKE run grep -F "scripts/check-applied-ood-headroom.sh" "$REPO_ROOT/.github/workflows/validate.yml" [ "$status" -eq 0 ] - run grep -F "scripts/check-applied-ood-headroom.sh" "$REPO_ROOT/scripts/hooks/pre-push.local" - [ "$status" -ne 0 ] + [ ! -e "$REPO_ROOT/scripts/hooks/pre-push.local" ] } diff --git a/tests/scripts/check-atomic-write-ratchet.bats b/tests/scripts/check-atomic-write-ratchet.bats index 0ffc07cce..24dd13b3f 100644 --- a/tests/scripts/check-atomic-write-ratchet.bats +++ b/tests/scripts/check-atomic-write-ratchet.bats @@ -3,7 +3,7 @@ # — the ADVISORY changed-scope ratchet on hand-rolled tmp+rename atomic writes # outside cli/internal/storage, the first NEW consumer of scripts/lib/ratchet.sh. # -# The fixture corpus below is PRE-REGISTERED (pre-mortem rounds 2-3): the +# The fixture corpus below is PRE-REGISTERED (premortem rounds 2-3): the # positive shapes are real in-tree writers (scenarioresults/writer.go:124-131, # config/config.go:694-701, agentworker/quarantine.go:69-81) and the negatives # are real plain-movers (search/util.go, doctor/engine.go) plus the diff --git a/tests/scripts/check-doc-skill-refs-all-docs.bats b/tests/scripts/check-doc-skill-refs-all-docs.bats index 7d17533bd..da0f10f24 100644 --- a/tests/scripts/check-doc-skill-refs-all-docs.bats +++ b/tests/scripts/check-doc-skill-refs-all-docs.bats @@ -18,7 +18,7 @@ setup() { DOCS="$(mktemp -d "$BATS_TMPDIR/docs.XXXXXX")" SKILLS="$(mktemp -d "$BATS_TMPDIR/skills.XXXXXX")" BASELINE="$(mktemp "$BATS_TMPDIR/baseline.XXXXXX")" - mkdir -p "$SKILLS/alpha" "$SKILLS/cc-hooks" "$SKILLS/validate" "$SKILLS/post-mortem" + mkdir -p "$SKILLS/alpha" "$SKILLS/cc-hooks" "$SKILLS/validate" "$SKILLS/postmortem" mkdir -p "$DOCS/docs" "$DOCS/docs/levels" "$DOCS/skills" # docs-scope.sh emits paths anchored at DOCS_ROOT; a live docs/** file must # exist for --all-docs to pick it up. diff --git a/tests/scripts/check-epic-children-closed.bats b/tests/scripts/check-epic-children-closed.bats deleted file mode 100644 index 03d35debb..000000000 --- a/tests/scripts/check-epic-children-closed.bats +++ /dev/null @@ -1,179 +0,0 @@ -#!/usr/bin/env bats -# Hermetic regression tests for scripts/check-epic-children-closed.sh -# (ag-9gac; tracker-agnostic rewrite age-5w8fd). -# -# The script shells out to `ao beads exec` (the ONE tracker-agnostic entry -# point). We stub `ao` via PATH so the gate is tested without a real bead -# ledger. `jq` is used real. -# -# Stub contract (mirrors the real ao surface): -# ao beads exec children <epic> --json -# -> prints $TMP/children verbatim; exits $TMP/children_rc if present. -# br emits plain child ids one per line; bd (--json forwarded -# verbatim) emits a JSON array of issue objects. Both shapes are -# exercised below. -# ao beads exec show <child> --json -# -> prints [{"id":...,"status":...}] read from $TMP/status/<child> -# (the canonical shape ao normalizes both trackers to). - -setup() { - REPO_ROOT="$(git rev-parse --show-toplevel)" - SCRIPT="$REPO_ROOT/scripts/check-epic-children-closed.sh" - TMP="$(mktemp -d)" - ORIG_PATH="$PATH" - ORIG_DIR="$PWD" - mkdir -p "$TMP/bin" "$TMP/status" -} - -teardown() { - cd "$ORIG_DIR" 2>/dev/null || true - export PATH="$ORIG_PATH" - rm -rf "$TMP" -} - -stub_ao() { - cat >"$TMP/bin/ao" <<EOF -#!/usr/bin/env bash -STATUS_DIR="$TMP/status" -if [ "\$1" = "beads" ] && [ "\$2" = "exec" ]; then - shift 2 - if [ "\$1" = "children" ]; then - cat "$TMP/children" 2>/dev/null - if [ -f "$TMP/children_rc" ]; then - exit "\$(cat "$TMP/children_rc")" - fi - exit 0 - fi - if [ "\$1" = "show" ]; then - if [ -f "$TMP/show_rc" ]; then - exit "\$(cat "$TMP/show_rc")" - fi - child="\$2" - if [ -f "\$STATUS_DIR/\$child" ]; then - st="\$(cat "\$STATUS_DIR/\$child")" - printf '[{"id":"%s","status":"%s"}]' "\$child" "\$st" - fi - exit 0 - fi -fi -exit 0 -EOF - chmod +x "$TMP/bin/ao" - export PATH="$TMP/bin:$ORIG_PATH" -} - -set_child() { echo "$2" > "$TMP/status/$1"; } - -run_gate() { run "$SCRIPT" "$@"; } - -# --- br shape: plain child ids, one per line --- - -@test "br shape: all children closed: exit 0" { - printf 'age-c1\nage-c2\n' > "$TMP/children" - set_child age-c1 closed - set_child age-c2 closed - stub_ao - run_gate age-epic - [ "$status" -eq 0 ] - [[ "$output" == *"all children of age-epic are closed"* ]] -} - -@test "br shape: one open child: exit 1 and names the offender" { - printf 'age-c1\nage-c2\n' > "$TMP/children" - set_child age-c1 closed - set_child age-c2 open - stub_ao - run_gate age-epic - [ "$status" -eq 1 ] - [[ "$output" == *"OPEN-CHILD: age-c2 status=open"* ]] - [[ "$output" == *"EPIC-GATE FAIL"* ]] -} - -@test "br shape: in_progress child: exit 1 and names the offender" { - printf 'age-c1\n' > "$TMP/children" - set_child age-c1 in_progress - stub_ao - run_gate age-epic - [ "$status" -eq 1 ] - [[ "$output" == *"OPEN-CHILD: age-c1 status=in_progress"* ]] -} - -@test "br shape: no children (empty output): exit 0" { - : > "$TMP/children" - stub_ao - run_gate age-epic - [ "$status" -eq 0 ] - [[ "$output" == *"no open children"* ]] -} - -# --- bd shape: JSON array of issue objects (bd children <epic> --json) --- - -@test "bd shape: all children closed: exit 0" { - printf '%s' '[{"id":"ag-c1","status":"closed"},{"id":"ag-c2","status":"closed"}]' > "$TMP/children" - set_child ag-c1 closed - set_child ag-c2 closed - stub_ao - run_gate ag-epic - [ "$status" -eq 0 ] - [[ "$output" == *"all children of ag-epic are closed"* ]] -} - -@test "bd shape: one open child: exit 1 and names the offender" { - printf '%s' '[{"id":"ag-c1","status":"closed"},{"id":"ag-c2","status":"open"}]' > "$TMP/children" - set_child ag-c1 closed - set_child ag-c2 open - stub_ao - run_gate ag-epic - [ "$status" -eq 1 ] - [[ "$output" == *"OPEN-CHILD: ag-c2 status=open"* ]] - [[ "$output" == *"EPIC-GATE FAIL"* ]] -} - -@test "bd shape: empty JSON array: exit 0" { - printf '%s' '[]' > "$TMP/children" - stub_ao - run_gate ag-epic - [ "$status" -eq 0 ] - [[ "$output" == *"no open children"* ]] -} - -# --- error surfaces --- - -@test "children query non-zero exit is surfaced: exit 4" { - printf '%s' '' > "$TMP/children" - echo 1 > "$TMP/children_rc" - stub_ao - run_gate age-epic - [ "$status" -eq 4 ] - [[ "$output" == *"children query failed"* ]] -} - -@test "per-child show failure is fail-closed: offender, exit 1 (not 4)" { - # Contract: only the CHILDREN-ENUMERATION error is exit 4. A per-child - # status read failure blocks the close as an offender (fail-closed exit 1) - # rather than aborting the whole gate. - printf 'age-c1\n' > "$TMP/children" - echo 1 > "$TMP/show_rc" - stub_ao - run_gate age-epic - [ "$status" -eq 1 ] - [[ "$output" == *"WARN: could not read status for child age-c1"* ]] - [[ "$output" == *"EPIC-GATE FAIL"* ]] -} - -@test "unreadable child status counts as offender" { - printf 'age-c1\n' > "$TMP/children" - # no status file for age-c1 -> stub prints nothing for show - stub_ao - run_gate age-epic - [ "$status" -eq 1 ] - [[ "$output" == *"WARN: could not read status for child age-c1"* ]] -} - -@test "missing epic-id exits 4" { - : > "$TMP/children" - stub_ao - run_gate - [ "$status" -eq 4 ] - [[ "$output" == *"need an epic-id"* ]] -} diff --git a/tests/scripts/check-gate-fixhints-live.bats b/tests/scripts/check-gate-fixhints-live.bats index a7eb6f422..4910565c7 100644 --- a/tests/scripts/check-gate-fixhints-live.bats +++ b/tests/scripts/check-gate-fixhints-live.bats @@ -47,7 +47,7 @@ run_meta() { ( cd "$WORK" && PATH="$WORK/bin:$PATH" bash scripts/check-gate-fixh } @test "dead command WITH a removal marker (historical ref) -> NOT flagged" { - printf '#!/usr/bin/env bash\necho " fix: make build-flywheel to restore the archived ao corpus snapshot"\n' > "$WORK/scripts/check-d.sh" + printf '#!/usr/bin/env bash\necho " historical: ao corpus snapshot was removed"\n' > "$WORK/scripts/check-d.sh" run run_meta --strict [ "$status" -eq 0 ] [[ "$output" == *"PASS"* ]] diff --git a/tests/scripts/check-new-scripts-use-preamble.bats b/tests/scripts/check-new-scripts-use-preamble.bats index f51952573..6b7347427 100644 --- a/tests/scripts/check-new-scripts-use-preamble.bats +++ b/tests/scripts/check-new-scripts-use-preamble.bats @@ -190,7 +190,7 @@ EOF cp "$SCRIPT_SRC" "$INIT/scripts/check-new-scripts-use-preamble.sh" cp "$LIB_SRC" "$INIT/scripts/lib/preamble.sh" # shared ratchet mechanics — this SECOND skeleton needs the lib too (the - # per-copy-site table from pre-mortem FM5 called out exactly this fixture) + # per-copy-site table from premortem FM5 called out exactly this fixture) cp "$BATS_TEST_DIRNAME/../../scripts/lib/ratchet.sh" "$INIT/scripts/lib/ratchet.sh" cat > "$INIT/scripts/old-handrolled.sh" <<'EOF' #!/usr/bin/env bash diff --git a/tests/scripts/check-no-apparatus-regrowth.bats b/tests/scripts/check-no-apparatus-regrowth.bats deleted file mode 100644 index 6c510b5c0..000000000 --- a/tests/scripts/check-no-apparatus-regrowth.bats +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/env bats -# Tests for scripts/check-no-apparatus-regrowth.sh — the anti-regeneration -# stay-removed gate. Drives the script against a temp repo + temp manifest so -# the result never depends on the real repo's current tree state. - -setup() { - REPO_ROOT="$(cd "$BATS_TEST_DIRNAME/../.." && pwd)" - SCRIPT="$REPO_ROOT/scripts/check-no-apparatus-regrowth.sh" - TMP_DIR="$(mktemp -d)" - FAKE_ROOT="$TMP_DIR/repo" - MANIFEST="$TMP_DIR/removed-apparatus.txt" - mkdir -p "$FAKE_ROOT" - cat > "$MANIFEST" <<'EOF' -# comment line — ignored -cli/internal/wikiworker # removed, PR #589 -cli/internal/plans -cli/internal/worker -EOF -} - -teardown() { - rm -rf "$TMP_DIR" -} - -@test "PASS when every removed surface is absent" { - run bash "$SCRIPT" --root "$FAKE_ROOT" --manifest "$MANIFEST" - [ "$status" -eq 0 ] - [[ "$output" == *"PASS"* ]] - [[ "$output" == *"3 teardown-removed surface(s) stay removed"* ]] -} - -@test "FAIL with exit 1 when a removed surface regrows" { - mkdir -p "$FAKE_ROOT/cli/internal/plans" - - run bash "$SCRIPT" --root "$FAKE_ROOT" --manifest "$MANIFEST" - [ "$status" -eq 1 ] - [[ "$output" == *"FAIL"* ]] - [[ "$output" == *"cli/internal/plans"* ]] -} - -@test "FAIL names every regrown surface, not just the first" { - mkdir -p "$FAKE_ROOT/cli/internal/plans" "$FAKE_ROOT/cli/internal/worker" - - run bash "$SCRIPT" --root "$FAKE_ROOT" --manifest "$MANIFEST" - [ "$status" -eq 1 ] - [[ "$output" == *"2 teardown-removed surface(s) regrew"* ]] - [[ "$output" == *"cli/internal/plans"* ]] - [[ "$output" == *"cli/internal/worker"* ]] -} - -@test "regrown file (not just dir) also fails" { - # A removed surface can be a single file (e.g. cli/internal/bridge/gc.go). - echo "cli/internal/bridge/gc.go" > "$MANIFEST" - mkdir -p "$FAKE_ROOT/cli/internal/bridge" - echo "package bridge" > "$FAKE_ROOT/cli/internal/bridge/gc.go" - - run bash "$SCRIPT" --root "$FAKE_ROOT" --manifest "$MANIFEST" - [ "$status" -eq 1 ] - [[ "$output" == *"cli/internal/bridge/gc.go"* ]] -} - -@test "--json emits machine-readable pass result" { - run bash "$SCRIPT" --json --root "$FAKE_ROOT" --manifest "$MANIFEST" - [ "$status" -eq 0 ] - [[ "$output" == *'"status":"pass"'* ]] - [[ "$output" == *'"checked":3'* ]] - [[ "$output" == *'"regrown":[]'* ]] -} - -@test "--json emits machine-readable fail result with regrown path" { - mkdir -p "$FAKE_ROOT/cli/internal/worker" - - run bash "$SCRIPT" --json --root "$FAKE_ROOT" --manifest "$MANIFEST" - [ "$status" -eq 1 ] - [[ "$output" == *'"status":"fail"'* ]] - [[ "$output" == *'"cli/internal/worker"'* ]] -} - -@test "missing manifest fails with exit 1" { - run bash "$SCRIPT" --root "$FAKE_ROOT" --manifest "$TMP_DIR/does-not-exist.txt" - [ "$status" -eq 1 ] - [[ "$output" == *"manifest not found"* ]] -} - -@test "the real committed manifest passes on the real repo tree" { - # Guards against shipping a manifest that lists a still-present surface. - run bash "$SCRIPT" - [ "$status" -eq 0 ] - [[ "$output" == *"PASS"* ]] -} diff --git a/tests/scripts/check-no-tracked-agents.bats b/tests/scripts/check-no-tracked-agents.bats old mode 100755 new mode 100644 index 3130731c1..7007efc84 --- a/tests/scripts/check-no-tracked-agents.bats +++ b/tests/scripts/check-no-tracked-agents.bats @@ -1,143 +1,44 @@ #!/usr/bin/env bats setup() { - REPO_ROOT="$(cd "$BATS_TEST_DIRNAME/../.." && pwd)" - SCRIPT="$REPO_ROOT/scripts/check-no-tracked-agents.sh" - - TMP_DIR="$(mktemp -d)" - FAKE_REPO="$TMP_DIR/repo" - mkdir -p "$FAKE_REPO" - cd "$FAKE_REPO" - git init -q - git config user.email test@example.com - git config user.name "Test User" - # Force commit signing off in this fake repo so tests that need a - # commit (e.g. "allows staged deletion") aren't blocked by host-level - # signing config inherited via /etc/gitconfig. - git config commit.gpgsign false - git config tag.gpgsign false - git config gpg.format openpgp + REPO_ROOT="$(cd "$BATS_TEST_DIRNAME/../.." && pwd)" + SCRIPT="$REPO_ROOT/scripts/check-no-tracked-agents.sh" + FAKE_REPO="$BATS_TEST_TMPDIR/repo" + mkdir -p "$FAKE_REPO" + git -C "$FAKE_REPO" init -q + git -C "$FAKE_REPO" config user.email test@example.com + git -C "$FAKE_REPO" config user.name test + printf '/.agents/\n' >"$FAKE_REPO/.gitignore" + export NO_TRACKED_AGENTS_REPO_ROOT="$FAKE_REPO" } -teardown() { - rm -rf "$TMP_DIR" +@test "ignored local agent state passes" { + mkdir -p "$FAKE_REPO/.agents/local" + printf '{}\n' >"$FAKE_REPO/.agents/local/state.json" + run "$SCRIPT" + [ "$status" -eq 0 ] } -write_ignore() { - printf '/.agents/\n' > "$FAKE_REPO/.gitignore" +@test "any tracked repo-root agent state fails" { + mkdir -p "$FAKE_REPO/.agents/rpi" + printf '{}\n' >"$FAKE_REPO/.agents/rpi/next-work.jsonl" + git -C "$FAKE_REPO" add -f .agents/rpi/next-work.jsonl + run "$SCRIPT" + [ "$status" -eq 1 ] + [[ "$output" == *".agents/rpi/next-work.jsonl"* ]] } -@test "check-no-tracked-agents.sh exists and is executable" { - [ -f "$SCRIPT" ] - [ -x "$SCRIPT" ] +@test "a root re-include fails" { + printf '/.agents/\n!/.agents/evolve/\n' >"$FAKE_REPO/.gitignore" + run "$SCRIPT" + [ "$status" -eq 1 ] + [[ "$output" == *"must not re-include"* ]] } -@test "passes when repo-root .agents is ignored and untracked" { - write_ignore - mkdir -p .agents/ao - printf 'local\n' > .agents/ao/state.json - - run "$SCRIPT" - [ "$status" -eq 0 ] - [[ "$output" == *"no disallowed tracked repo-root .agents state"* ]] -} - -@test "fails when repo-root .agents is tracked outside the audit-truth allowlist" { - write_ignore - mkdir -p .agents/rpi - printf '{}\n' > .agents/rpi/execution-packet.json - git add -f .agents/rpi/execution-packet.json - - run "$SCRIPT" - [ "$status" -eq 1 ] - [[ "$output" == *"tracked outside the audit-truth allowlist"* ]] - [[ "$output" == *".agents/rpi/execution-packet.json"* ]] -} - -@test "permits tracked allowlisted audit-truth files (nightly snapshots)" { - { - printf '/.agents/*\n' - printf '/.agents/**/*\n' - printf '!/.agents/\n' - printf '!/.agents/nightly/\n' - printf '!/.agents/nightly/**\n' - } > .gitignore - mkdir -p .agents/nightly/2026-05-05 - printf '{}\n' > .agents/nightly/2026-05-05/baseline-goals.json - git add .agents/nightly/2026-05-05/baseline-goals.json - - run "$SCRIPT" - [ "$status" -eq 0 ] - [[ "$output" == *"no disallowed tracked repo-root .agents state"* ]] -} - -@test "permits tracked allowlisted audit-truth files (rpi/next-work, evolve, goals attempts, findings registry)" { - { - printf '/.agents/*\n' - printf '/.agents/**/*\n' - printf '!/.agents/\n' - printf '!/.agents/rpi/\n' - printf '!/.agents/rpi/next-work.jsonl\n' - printf '!/.agents/evolve/\n' - printf '!/.agents/evolve/cycle-history.jsonl\n' - printf '!/.agents/evolve/session-state.json\n' - printf '!/.agents/goals/\n' - printf '!/.agents/goals/**/\n' - printf '!/.agents/goals/**/attempts.jsonl\n' - printf '!/.agents/findings/\n' - printf '!/.agents/findings/registry.jsonl\n' - } > .gitignore - mkdir -p .agents/rpi .agents/evolve .agents/goals/g-one .agents/findings - printf '{}\n' > .agents/rpi/next-work.jsonl - printf '{}\n' > .agents/evolve/cycle-history.jsonl - printf '{}\n' > .agents/evolve/session-state.json - printf '{}\n' > .agents/goals/g-one/attempts.jsonl - printf '{}\n' > .agents/findings/registry.jsonl - git add .agents/rpi/next-work.jsonl .agents/evolve/cycle-history.jsonl .agents/evolve/session-state.json .agents/goals/g-one/attempts.jsonl .agents/findings/registry.jsonl - - run "$SCRIPT" - [ "$status" -eq 0 ] - [[ "$output" == *"no disallowed tracked repo-root .agents state"* ]] -} - -@test "allows staged deletion while removing .agents from the index" { - write_ignore - mkdir -p .agents/learnings - printf 'secret\n' > .agents/learnings/item.md - git add -f .agents/learnings/item.md - git commit -qm "track legacy agents state" - git rm -q --cached .agents/learnings/item.md - - run "$SCRIPT" - [ "$status" -eq 0 ] -} - -@test "fails when root .gitignore omits /.agents/" { - printf '*.log\n' > .gitignore - - run "$SCRIPT" - [ "$status" -eq 1 ] - [[ "$output" == *"must contain an explicit '/.agents/'"* ]] -} - -@test "fails when root .gitignore re-includes .agents paths outside the allowlist" { - { - printf '/.agents/\n' - printf '!.agents/learnings/\n' - } > .gitignore - - run "$SCRIPT" - [ "$status" -eq 1 ] - [[ "$output" == *"outside the audit-truth allowlist"* ]] - [[ "$output" == *"!.agents/learnings/"* ]] -} - -@test "permits nested .agents test fixtures outside repo root .agents" { - write_ignore - mkdir -p cli/cmd/ao/testdata/example/.agents - printf '{}\n' > cli/cmd/ao/testdata/example/.agents/fixture.json - git add cli/cmd/ao/testdata/example/.agents/fixture.json - - run "$SCRIPT" - [ "$status" -eq 0 ] +@test "nested test fixtures are not repo-root agent state" { + mkdir -p "$FAKE_REPO/cli/cmd/testdata/example/.agents" + printf '{}\n' >"$FAKE_REPO/cli/cmd/testdata/example/.agents/fixture.json" + git -C "$FAKE_REPO" add cli/cmd/testdata/example/.agents/fixture.json + run "$SCRIPT" + [ "$status" -eq 0 ] } diff --git a/tests/scripts/check-shell-portability.bats b/tests/scripts/check-shell-portability.bats index 816ea344b..e95edd3bd 100644 --- a/tests/scripts/check-shell-portability.bats +++ b/tests/scripts/check-shell-portability.bats @@ -69,8 +69,8 @@ teardown() { } @test "an EXTENSIONLESS shell script (shebang, no .sh) with find -printf is caught" { - # The four real bugs were in .sh files, but hooks like scripts/hooks/pre-push.local - # and .githooks/* are extensionless shell. Detection must follow the shebang. + # Hooks and other executable helpers may be extensionless shell. Detection + # must follow the shebang. printf '#!/usr/bin/env bash\nx=$(find . -printf "%%f\\n")\n' > "$TMP_DIR/scripts/myhook" chmod +x "$TMP_DIR/scripts/myhook" run bash "$SCRIPT" --root "$TMP_DIR/scripts" diff --git a/tests/scripts/check-skill-isolation.bats b/tests/scripts/check-skill-isolation.bats deleted file mode 100644 index 236a55395..000000000 --- a/tests/scripts/check-skill-isolation.bats +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env bats -# Regression coverage for scripts/check-skill-isolation.sh (ag-skill-isolation-ci-gate-jxpbx). -# The guard must be portable on macOS/BSD awk and Linux/gawk, and its self-test -# must exercise the Skill(...) matching path that previously used GNU awk arrays. - -setup() { - SCRIPT="$BATS_TEST_DIRNAME/../../scripts/check-skill-isolation.sh" - ROOT="$(mktemp -d)" -} - -teardown() { - rm -rf "$ROOT" -} - -@test "runs without awk errors on a clean fixture" { - mkdir -p "$ROOT/crank" - cat > "$ROOT/crank/SKILL.md" <<'EOF' ---- -name: crank ---- -# /crank - -Plain implementation instructions. -EOF - - run bash "$SCRIPT" "$ROOT" - [ "$status" -eq 0 ] - [[ "$output" != *"awk:"* ]] - [[ "$output" == *"PASS"* ]] -} - -@test "--self-test passes and reports no awk dialect error" { - run bash "$SCRIPT" --self-test - [ "$status" -eq 0 ] - [[ "$output" == *"self-test PASS"* ]] - [[ "$output" != *"awk:"* ]] -} - -@test "catches a sealed phase Skill call compression pattern" { - mkdir -p "$ROOT/crank" - cat > "$ROOT/crank/SKILL.md" <<'EOF' ---- -name: crank ---- -# /crank - -Skill(skill="research", args="inline the discovery pass") -EOF - - run bash "$SCRIPT" "$ROOT" - [ "$status" -eq 1 ] - [[ "$output" == *"phase-skill calling another phase skill (target=research)"* ]] - [[ "$output" != *"awk:"* ]] -} diff --git a/tests/scripts/check-skill-redirects.bats b/tests/scripts/check-skill-redirects.bats deleted file mode 100644 index aa22cdc68..000000000 --- a/tests/scripts/check-skill-redirects.bats +++ /dev/null @@ -1,125 +0,0 @@ -#!/usr/bin/env bats -# -# Tests for the folded-skill redirect-validity gate (age-rhlx): -# scripts/check-skill-redirects.sh asserts that every `merged-into` disposition -# in docs/contracts/skill-dispositions.yaml resolves — following the -# merged-into chain — to a LIVE skill (skills/<name>/SKILL.md), with no cycles. -# -# WHY: a folded skill is a redirect. If a later rename/prune deletes the fold -# TARGET, the redirect silently points at a 404 and inbound traffic dead-ends. -# This gate keeps the folded-skill map's targets real forever. -# -# The gate derives repo_root from `git rev-parse --show-toplevel`, so each case -# stamps a minimal repo fixture (a ledger + skills/<name>/SKILL.md dirs) and -# asserts the verdict. - -setup() { - REPO_ROOT="$(cd "$BATS_TEST_DIRNAME/../.." && pwd)" - GATE="$REPO_ROOT/scripts/check-skill-redirects.sh" - FIX="$BATS_TEST_TMPDIR/repo" - mkdir -p "$FIX/scripts" "$FIX/docs/contracts" "$FIX/skills" - cp "$GATE" "$FIX/scripts/check-skill-redirects.sh" - chmod +x "$FIX/scripts/check-skill-redirects.sh" - git -C "$FIX" init -q - git -C "$FIX" config user.email t@t.t - git -C "$FIX" config user.name t -} - -# live_skill <name> — create a live skills/<name>/SKILL.md -live_skill() { mkdir -p "$FIX/skills/$1"; printf -- '---\nname: %s\n---\nbody\n' "$1" > "$FIX/skills/$1/SKILL.md"; } - -run_gate() { ( cd "$FIX" && bash scripts/check-skill-redirects.sh ); } - -@test "passes when every merged-into target resolves to a live skill" { - live_skill discovery - live_skill review - cat > "$FIX/docs/contracts/skill-dispositions.yaml" <<'YAML' -skills: - brainstorm: - state: merged-into - merged-into: discovery - bug-hunt: - state: merged-into - merged-into: review - review: - state: active -YAML - run run_gate - [ "$status" -eq 0 ] -} - -@test "fails when a fold target does not exist as a live skill" { - live_skill discovery - cat > "$FIX/docs/contracts/skill-dispositions.yaml" <<'YAML' -skills: - brainstorm: - state: merged-into - merged-into: discovery - bug-hunt: - state: merged-into - merged-into: review -YAML - run run_gate - [ "$status" -ne 0 ] - [[ "$output" == *"bug-hunt"* ]] - [[ "$output" == *"review"* ]] -} - -@test "resolves a transitive chain (a -> b -> live)" { - live_skill crank - cat > "$FIX/docs/contracts/skill-dispositions.yaml" <<'YAML' -skills: - ship-loop: - state: merged-into - merged-into: burndown - burndown: - state: merged-into - merged-into: crank -YAML - run run_gate - [ "$status" -eq 0 ] -} - -@test "fails on a cycle in the merged-into chain" { - cat > "$FIX/docs/contracts/skill-dispositions.yaml" <<'YAML' -skills: - a: - state: merged-into - merged-into: b - b: - state: merged-into - merged-into: a -YAML - run run_gate - [ "$status" -ne 0 ] - [[ "$output" == *"cycle"* ]] -} - -@test "fails on a cycle even when a node in it still has a live SKILL.md" { - # Regression (pawl codex catch): a -> b -> a where b also has a stale live - # SKILL.md must NOT short-circuit as resolved. A folded skill is declared - # gone; it is never a valid redirect terminal, so the cycle is still caught. - live_skill b - cat > "$FIX/docs/contracts/skill-dispositions.yaml" <<'YAML' -skills: - a: - state: merged-into - merged-into: b - b: - state: merged-into - merged-into: a -YAML - run run_gate - [ "$status" -ne 0 ] - [[ "$output" == *"cycle"* ]] -} - -@test "a cut/retired skill needs no target" { - cat > "$FIX/docs/contracts/skill-dispositions.yaml" <<'YAML' -skills: - reverse-engineer-rpi: - state: cut -YAML - run run_gate - [ "$status" -eq 0 ] -} diff --git a/tests/scripts/check-slice-batch-size.bats b/tests/scripts/check-slice-batch-size.bats deleted file mode 100644 index 25a9567a2..000000000 --- a/tests/scripts/check-slice-batch-size.bats +++ /dev/null @@ -1,299 +0,0 @@ -#!/usr/bin/env bats -# -# Tests for scripts/check-slice-batch-size.sh (age-74yi) — the small-batch-by- -# Gherkin ENFORCEMENT gate: one slice bead == one behavior == one Gherkin -# scenario, fail-on-multi. -# -# The enforcement half of the flywheel discipline -# (docs/architecture/the-flywheel.md): the discovery/behavior-first skills SAY -# "seed slices small / one scenario per slice" but nothing FAILS on a -# multi-behavior slice. This gate makes the batch unit COUNTABLE. -# -# Tracker-agnostic: the script reads the bead body via `ao beads exec show <id> -# --json`. These tests STUB `ao` on PATH (a fake that emits canned JSON) so the -# suite never depends on a live tracker — mirroring how the other script tests -# stub `ao`/`br`. - -setup() { - REPO_ROOT="$(cd "$BATS_TEST_DIRNAME/../.." && pwd)" - SCRIPT="$REPO_ROOT/scripts/check-slice-batch-size.sh" - - TMP_DIR="$(mktemp -d)" - STUB_JSON="$TMP_DIR/bead.json" - export STUB_JSON - STUB_READY_JSON="$TMP_DIR/ready.json" - export STUB_READY_JSON - - # Fake `ao` on PATH: `ao beads exec show <id> --json` cats the canned JSON. - FAKE_BIN="$TMP_DIR/bin" - mkdir -p "$FAKE_BIN" - cat > "$FAKE_BIN/ao" <<'STUB' -#!/usr/bin/env bash -# Fake `ao` for check-slice-batch-size tests. Only implements the two verbs the -# script uses: `beads exec show <id> --json` and `beads dir`. -if [[ "$1" == "beads" && "$2" == "dir" ]]; then - echo "${STUB_BEADS_DIR:-/tmp}" - exit 0 -fi -if [[ "$1" == "beads" && "$2" == "exec" && "$3" == "ready" ]]; then - if [[ -f "$STUB_READY_JSON" ]]; then - cat "$STUB_READY_JSON" - exit 0 - fi - exit 1 -fi -if [[ "$1" == "beads" && "$2" == "exec" && "$3" == "show" ]]; then - if [[ -f "$STUB_JSON" ]]; then - cat "$STUB_JSON" - exit 0 - fi - exit 1 -fi -echo "fake ao: unhandled invocation: $*" >&2 -exit 2 -STUB - chmod +x "$FAKE_BIN/ao" - export PATH="$FAKE_BIN:$PATH" -} - -teardown() { - rm -rf "$TMP_DIR" -} - -# Build the canned `ao beads exec show --json` payload (a JSON array, exactly -# what a live `br`/`bd` passthrough emits) from a raw body file. jq handles all -# JSON escaping of the Gherkin body. -make_bead() { - local id="$1" bodyfile="$2" - jq -n --arg id "$id" --arg desc "$(cat "$bodyfile")" \ - '[{id:$id,title:"a slice",description:$desc,status:"open",priority:1,issue_type:"task"}]' \ - > "$STUB_JSON" -} - -@test "script exists and is executable" { - [ -f "$SCRIPT" ] - [ -x "$SCRIPT" ] -} - -# --- Acceptance scenario 1: a multi-behavior slice FAILS ---------------------- -@test "FAIL: a slice bead with TWO Gherkin scenarios fails and directs a split" { - cat > "$TMP_DIR/body.md" <<'EOF' -## Context - -This slice tried to do two things at once. - -## Scenarios - -Scenario: the check flags a multi-behavior slice - Given a bead body with two scenarios - When the batch check runs - Then it fails - -Scenario: the check passes a single-behavior slice - Given a bead body with one scenario - When the batch check runs - Then it passes -EOF - make_bead "age-multi" "$TMP_DIR/body.md" - run bash "$SCRIPT" age-multi - [ "$status" -eq 1 ] - [[ "$output" == *"SLICE-BATCH: FAIL"* ]] - [[ "$output" == *"age-multi"* ]] - [[ "$output" == *"2 behaviors"* ]] - [[ "$output" == *"split"* ]] - # Names the detected scenarios so the operator knows where to cut. - [[ "$output" == *"the check flags a multi-behavior slice"* ]] - [[ "$output" == *"the check passes a single-behavior slice"* ]] -} - -@test "FAIL: two bare Given/When/Then stanzas (no Scenario: headers) also fail" { - cat > "$TMP_DIR/body.md" <<'EOF' -Prose intro describing the slice. - -Given the first behavior's precondition -When the first action happens -Then the first outcome holds - -Given the second behavior's precondition -When the second action happens -Then the second outcome holds -EOF - make_bead "age-bare2" "$TMP_DIR/body.md" - run bash "$SCRIPT" age-bare2 - [ "$status" -eq 1 ] - [[ "$output" == *"SLICE-BATCH: FAIL"* ]] - [[ "$output" == *"2 behaviors"* ]] -} - -# --- Acceptance scenario 2: a single-behavior slice PASSES -------------------- -@test "PASS: a slice with exactly one Given/When/Then (+ edge/AND lines) passes" { - cat > "$TMP_DIR/body.md" <<'EOF' -## Context - -One behavior, sliced thin. - -## Scenarios - -Scenario: the check passes a one-behavior slice - Given a bead body with a single scenario - And an executed-red acceptance test tests/scripts/check-slice-batch-size.bats - When the batch check runs - Then it passes with exit 0 -EOF - make_bead "age-one" "$TMP_DIR/body.md" - run bash "$SCRIPT" age-one - [ "$status" -eq 0 ] - [[ "$output" == *"SLICE-BATCH: PASS"* ]] - [[ "$output" == *"age-one"* ]] -} - -@test "PASS: one bare Given/When/Then stanza (no Scenario: header) passes" { - cat > "$TMP_DIR/body.md" <<'EOF' -Add the flag to the parse loop. - -Given a bead body using a single bare-GWT stanza -When the batch check runs -Then it passes -EOF - make_bead "age-bare1" "$TMP_DIR/body.md" - run bash "$SCRIPT" age-bare1 - [ "$status" -eq 0 ] - [[ "$output" == *"SLICE-BATCH: PASS"* ]] -} - -# --- 0-scenario decision: WARN (not a hard FAIL) ------------------------------ -@test "WARN: a task bead with no Gherkin scenario warns (exit 0), never a hard FAIL" { - cat > "$TMP_DIR/body.md" <<'EOF' -## Context - -A plain task bead with prose acceptance only — common in the tracker today. -No Given/When/Then block yet. The word Scenario appears here in prose but not -as a header. -EOF - make_bead "age-zero" "$TMP_DIR/body.md" - run bash "$SCRIPT" age-zero - [ "$status" -eq 0 ] - [[ "$output" == *"SLICE-BATCH: WARN"* ]] - [[ "$output" == *"age-zero"* ]] - [[ "$output" == *"no Gherkin scenario"* ]] -} - -# --- Robustness --------------------------------------------------------------- -@test "robust: a fenced code block containing Scenario:/GWT text is parse-inert" { - # Only ONE real scenario; a second lives inside a ``` fence and must not count. - cat > "$TMP_DIR/body.md" <<'EOF' -## Scenarios - -Scenario: the only real behavior - Given a real precondition - When it runs - Then it works - -```yaml -acceptance: - example: | - Scenario: phantom inside a fence - Given a fenced line - When parsed - Then it must not count -``` -EOF - make_bead "age-fence" "$TMP_DIR/body.md" - run bash "$SCRIPT" age-fence - [ "$status" -eq 0 ] - [[ "$output" == *"SLICE-BATCH: PASS"* ]] -} - -@test "robust: inline mid-sentence Given/When/Then in prose does not count as a scenario" { - cat > "$TMP_DIR/body.md" <<'EOF' -## Context - -The slice carries exactly one happy-path Given/When/Then triad. We reference -When and Then inline in a sentence, which must not be miscounted as behaviors. - -## Scenarios - -Scenario: one true behavior - Given a precondition - When an action - Then an outcome -EOF - make_bead "age-inline" "$TMP_DIR/body.md" - run bash "$SCRIPT" age-inline - [ "$status" -eq 0 ] - [[ "$output" == *"SLICE-BATCH: PASS"* ]] -} - -@test "--json emits a machine-readable summary" { - cat > "$TMP_DIR/body.md" <<'EOF' -## Scenarios - -Scenario: a - Given g - When w - Then t - -Scenario: b - Given g - When w - Then t -EOF - make_bead "age-json" "$TMP_DIR/body.md" - run bash "$SCRIPT" --json age-json - [ "$status" -eq 1 ] - [[ "$output" == *'"bead":"age-json"'* ]] - [[ "$output" == *'"behaviors":2'* ]] - [[ "$output" == *'"result":"fail"'* ]] -} - -# --- Misuse / infra ----------------------------------------------------------- -@test "misuse: no bead id exits 2" { - run bash "$SCRIPT" - [ "$status" -eq 2 ] - [[ "$output" == *"bead id"* ]] -} - -@test "infra: bead show returning nothing exits 2, not a policy verdict" { - rm -f "$STUB_JSON" # stub `ao ... show` will exit 1 / emit nothing - run bash "$SCRIPT" age-missing - [ "$status" -eq 2 ] -} - -@test "infra: bead show returning MALFORMED JSON exits 2, not a fail-open WARN (age-74yi refute-fix)" { - # Non-empty but unparseable tracker output. Before the fix, jq's parse error - # was swallowed into an empty body -> SLICE-BATCH: WARN exit 0 (fail-open). - printf 'not-json\n' > "$STUB_JSON" - run bash "$SCRIPT" age-bad - [ "$status" -eq 2 ] - [[ "$output" == *"malformed"* || "$output" == *"infra"* ]] -} - -@test "infra: --all-ready with MALFORMED ready JSON exits 2, not a fail-open sweep over zero beads (age-74yi refute-fix 2)" { - printf 'not-json\n' > "$STUB_READY_JSON" - run bash "$SCRIPT" --all-ready - [ "$status" -eq 2 ] -} - -@test "infra: --all-ready where one ready bead's show is MALFORMED fails CLOSED (exit 2), not skipped (age-74yi refute-fix 2)" { - printf '{"issues":[{"id":"age-x"}]}\n' > "$STUB_READY_JSON" # one ready id - printf 'not-json\n' > "$STUB_JSON" # its show is malformed - run bash "$SCRIPT" --all-ready - [ "$status" -eq 2 ] -} - -@test "PASS: --all-ready over valid single-scenario ready beads exits 0" { - printf '{"issues":[{"id":"age-x"}]}\n' > "$STUB_READY_JSON" - printf 'GIVEN a\nWHEN b\nTHEN c\n' > "$TMP_DIR/body.txt" - make_bead age-x "$TMP_DIR/body.txt" # valid single-scenario show - run bash "$SCRIPT" --all-ready - [ "$status" -eq 0 ] -} - -@test "PASS: a Background: block before a single Scenario is shared setup, not a 2nd behavior (age-74yi refute-fix 3)" { - # Gherkin Background = shared setup steps; it must NOT count as a behavior. - printf 'Background:\n Given shared setup\n\nScenario: one behavior\n Given x\n When y\n Then z\n' > "$TMP_DIR/body.txt" - make_bead age-bg "$TMP_DIR/body.txt" - run bash "$SCRIPT" age-bg - [ "$status" -eq 0 ] - [[ "$output" == *"PASS"* ]] -} diff --git a/tests/scripts/check-workflow-governance.bats b/tests/scripts/check-workflow-governance.bats deleted file mode 100644 index c5304c272..000000000 --- a/tests/scripts/check-workflow-governance.bats +++ /dev/null @@ -1,112 +0,0 @@ -#!/usr/bin/env bats -# -# Tests for the workflow governance / drift gate (ag-jy8gj): -# scripts/check-workflow-governance.sh asserts a BIDIRECTIONAL identity match -# between .claude/workflows/*.js and the top-level `workflows:` section of -# docs/contracts/skill-dispositions.yaml, and that each ledger row carries the -# DDD identity triple: kind: workflow + a Bounded Context (domain) + a -# hexagonal_role. -# -# The gate runs against a temp git fixture (it derives repo_root from -# `git rev-parse --show-toplevel`), so each case stamps a minimal repo with a -# workflow .js + a ledger and asserts the gate's verdict. Workflows are -# Claude-only (ag-jy8gj comment): the gate checks Claude-runtime presence and -# never requires a Codex twin. - -setup() { - REPO_ROOT="$(cd "$BATS_TEST_DIRNAME/../.." && pwd)" - export REPO_ROOT - GATE="$REPO_ROOT/scripts/check-workflow-governance.sh" - # Build a throwaway git repo with the gate script + workflow dir + ledger. - FIX="$BATS_TEST_TMPDIR/repo" - mkdir -p "$FIX/scripts" "$FIX/.claude/workflows" "$FIX/docs/contracts" - cp "$GATE" "$FIX/scripts/check-workflow-governance.sh" - chmod +x "$FIX/scripts/check-workflow-governance.sh" - git -C "$FIX" init -q - git -C "$FIX" config user.email t@t.t - git -C "$FIX" config user.name t -} - -# write_workflow_js <name> <meta-name> -write_workflow_js() { - local file="$FIX/.claude/workflows/$1" - cat > "$file" <<JS -export const meta = { name: '$2' }; -JS - git -C "$FIX" add "$file" -} - -run_gate() { ( cd "$FIX" && bash scripts/check-workflow-governance.sh ); } - -@test "a fully-governed workflow (kind+BC+role, js<->ledger match) passes" { - write_workflow_js "demo.js" "demo" - cat > "$FIX/docs/contracts/skill-dispositions.yaml" <<'YAML' -workflows: - demo: - kind: workflow - domain: "BC3 Loop" - hexagonal_role: driving-adapter - path: .claude/workflows/demo.js -YAML - run run_gate - [ "$status" -eq 0 ] -} - -@test "a .js with no ledger row FAILS naming it" { - write_workflow_js "orphan.js" "orphan" - cat > "$FIX/docs/contracts/skill-dispositions.yaml" <<'YAML' -workflows: {} -YAML - run run_gate - [ "$status" -ne 0 ] - [[ "$output" == *"orphan"* ]] -} - -@test "a ledger row kind: workflow with no matching .js FAILS as stale" { - # No .js authored, but the ledger declares one. - cat > "$FIX/docs/contracts/skill-dispositions.yaml" <<'YAML' -workflows: - ghost: - kind: workflow - domain: "BC3 Loop" - hexagonal_role: driving-adapter - path: .claude/workflows/ghost.js -YAML - run run_gate - [ "$status" -ne 0 ] - [[ "$output" == *"ghost"* ]] - [[ "$output" == *"stale"* || "$output" == *"no .claude/workflows"* || "$output" == *"no matching"* ]] -} - -@test "a ledger row missing its Bounded Context (domain) FAILS" { - write_workflow_js "nobc.js" "nobc" - cat > "$FIX/docs/contracts/skill-dispositions.yaml" <<'YAML' -workflows: - nobc: - kind: workflow - hexagonal_role: driving-adapter - path: .claude/workflows/nobc.js -YAML - run run_gate - [ "$status" -ne 0 ] - [[ "$output" == *"nobc"* ]] -} - -@test "a ledger row missing its hexagonal_role FAILS" { - write_workflow_js "norole.js" "norole" - cat > "$FIX/docs/contracts/skill-dispositions.yaml" <<'YAML' -workflows: - norole: - kind: workflow - domain: "BC3 Loop" - path: .claude/workflows/norole.js -YAML - run run_gate - [ "$status" -ne 0 ] - [[ "$output" == *"norole"* ]] -} - -@test "the real repo passes the governance gate" { - run bash "$GATE" - [ "$status" -eq 0 ] -} diff --git a/tests/scripts/cron-tune-cadence.bats b/tests/scripts/cron-tune-cadence.bats deleted file mode 100644 index 21b68fd5f..000000000 --- a/tests/scripts/cron-tune-cadence.bats +++ /dev/null @@ -1,167 +0,0 @@ -#!/usr/bin/env bats -# Regression tests for scripts/cron-tune-cadence.sh (soc-adwq). - -setup() { - REPO_ROOT="$(git rev-parse --show-toplevel)" - SCRIPT="$REPO_ROOT/scripts/cron-tune-cadence.sh" - TMP="$(mktemp -d)" - mkdir -p "$TMP/.agents/evolve" - # Run from $TMP so the script's git-root detection finds nothing → falls back - # to $PWD. We isolate by setting PWD inside each test. - ORIG_DIR="$PWD" -} - -teardown() { - cd "$ORIG_DIR" 2>/dev/null || true - rm -rf "$TMP" -} - -# Build a minimal state file with given streak + hash -write_state() { - local streak="$1" hash="${2:-}" - cat >"$TMP/.agents/evolve/session-state.json" <<EOF -{ - "session_pr_count": 5, - "batch_prs": [], - "heartbeat_streak": $streak, - "state_hash": "$hash" -} -EOF -} - -# Run script with $TMP as cwd so git-rev-parse falls back to $PWD, -# and there are no PRs to query (BATCH_PRS empty). -run_in_tmp() { - cd "$TMP" - run "$SCRIPT" "$@" -} - -@test "heartbeat with empty batch + new state-hash → STAY, streak=1" { - write_state 0 "previous-hash" - run_in_tmp heartbeat - [ "$status" -eq 0 ] - last="$(printf '%s\n' "$output" | tail -1)" - [ "$last" = "STAY" ] - streak=$(jq -r '.heartbeat_streak' "$TMP/.agents/evolve/session-state.json") - [ "$streak" = "1" ] -} - -@test "heartbeat with same state-hash, streak<threshold → STAY" { - # Pre-populate with the hash we know will be computed - # Compute it the same way the script does - MAIN_SHA="$(cd "$ORIG_DIR" && git rev-parse HEAD)" - EXPECTED_HASH=$(printf '%s\n' "$MAIN_SHA" | sha1sum | awk '{print $1}') - write_state 1 "$EXPECTED_HASH" - run_in_tmp heartbeat - [ "$status" -eq 0 ] - # Either STAY (if hash matches) or new streak start (if it doesn't due to no git in TMP) - # The important behavior: streak grows when hash matches, resets when it doesn't. - # We verify the script doesn't crash. - last="$(printf '%s\n' "$output" | tail -1)" - case "$last" in STAY|TUNE_DOWN) ;; *) false ;; esac -} - -@test "heartbeat with same state-hash, streak>=threshold → TUNE_DOWN" { - MAIN_SHA="$(cd "$ORIG_DIR" && git rev-parse HEAD)" - EXPECTED_HASH=$(printf '%s\n' "$MAIN_SHA" | sha1sum | awk '{print $1}') - write_state 2 "$EXPECTED_HASH" # streak=2, +1 = 3 = threshold - cd "$TMP" - write_state 0 "" - cd "$TMP" - CRON_TUNE_HEARTBEAT_THRESHOLD=3 run "$SCRIPT" heartbeat # streak 0→1 - [ "$status" -eq 0 ] - CRON_TUNE_HEARTBEAT_THRESHOLD=3 run "$SCRIPT" heartbeat # streak 1→2 - CRON_TUNE_HEARTBEAT_THRESHOLD=3 run "$SCRIPT" heartbeat # streak 2→3 = threshold - [ "$status" -eq 0 ] - last="$(printf '%s\n' "$output" | tail -1)" - [ "$last" = "TUNE_DOWN" ] -} - -@test "productive with no open PRs → STAY, streak reset" { - write_state 5 "any-hash" - run_in_tmp productive - [ "$status" -eq 0 ] - last="$(printf '%s\n' "$output" | tail -1)" - [ "$last" = "STAY" ] - streak=$(jq -r '.heartbeat_streak' "$TMP/.agents/evolve/session-state.json") - [ "$streak" = "0" ] -} - -@test "productive with open PRs → TUNE_UP" { - cat >"$TMP/.agents/evolve/session-state.json" <<'EOF' -{ - "session_pr_count": 5, - "batch_prs": [999998, 999999], - "heartbeat_streak": 5 -} -EOF - cd "$TMP" - run "$SCRIPT" productive - [ "$status" -eq 0 ] - # gh pr view on fake numbers will produce UNKNOWN UNKNOWN but PR list is non-empty - last="$(printf '%s\n' "$output" | tail -1)" - [ "$last" = "TUNE_UP" ] - streak=$(jq -r '.heartbeat_streak' "$TMP/.agents/evolve/session-state.json") - [ "$streak" = "0" ] -} - -@test "teardown → STAY, streak reset" { - write_state 9 "any-hash" - run_in_tmp teardown - [ "$status" -eq 0 ] - last="$(printf '%s\n' "$output" | tail -1)" - [ "$last" = "STAY" ] - streak=$(jq -r '.heartbeat_streak' "$TMP/.agents/evolve/session-state.json") - [ "$streak" = "0" ] -} - -@test "missing state file → STAY, no crash" { - cd "$TMP" - rm -f .agents/evolve/session-state.json - run "$SCRIPT" heartbeat - [ "$status" -eq 0 ] - last="$(printf '%s\n' "$output" | tail -1)" - [ "$last" = "STAY" ] -} - -@test "unknown cycle-result → STAY" { - write_state 0 "" - run_in_tmp bogus-result - [ "$status" -eq 0 ] - last="$(printf '%s\n' "$output" | tail -1)" - [ "$last" = "STAY" ] -} - -@test "missing cycle-result arg → exit 2" { - cd "$TMP" - run "$SCRIPT" - [ "$status" -eq 2 ] -} - -@test "state-hash is updated on every call" { - write_state 0 "" - run_in_tmp heartbeat - [ "$status" -eq 0 ] - hash1=$(jq -r '.state_hash' "$TMP/.agents/evolve/session-state.json") - [ -n "$hash1" ] - [ "$hash1" != "" ] - [ "$hash1" != "null" ] -} - -@test "last_tune_check timestamp is set" { - write_state 0 "" - run_in_tmp heartbeat - ts=$(jq -r '.last_tune_check' "$TMP/.agents/evolve/session-state.json") - [[ "$ts" =~ ^20[0-9]{2}-[0-9]{2}-[0-9]{2}T ]] -} - -@test "blocked-on-failure with same hash, streak<threshold → STAY" { - MAIN_SHA="$(cd "$ORIG_DIR" && git rev-parse HEAD)" - EXPECTED_HASH=$(printf '%s\n' "$MAIN_SHA" | sha1sum | awk '{print $1}') - write_state 0 "$EXPECTED_HASH" - cd "$TMP" - run "$SCRIPT" blocked-on-failure - [ "$status" -eq 0 ] - last="$(printf '%s\n' "$output" | tail -1)" - case "$last" in STAY|TUNE_DOWN) ;; *) false ;; esac -} diff --git a/tests/scripts/docs-no-dangling-hooks-doc-parity.bats b/tests/scripts/docs-no-dangling-hooks-doc-parity.bats index d9dfb3fd4..9578bf036 100644 --- a/tests/scripts/docs-no-dangling-hooks-doc-parity.bats +++ b/tests/scripts/docs-no-dangling-hooks-doc-parity.bats @@ -4,9 +4,8 @@ # scripts/validate-hooks-doc-parity.sh. Operational docs must not instruct a # reader to run the deleted script. Historical records (docs/CHANGELOG.md and # docs/releases/*) legitimately describe the removal and are excluded. The -# retired scripts/pre-push-gate.sh and its bats stub keep their references until -# the soc-g2r9 deletion waves remove those files wholesale, so they are out of -# this guard's scope (see docs/contracts/local-pre-push-gate-retirement.md). +# The old pre-push gate and installer are also deleted; current operational docs +# must route deterministic checks through `ao gate check`. setup() { REPO_ROOT="$(cd "$BATS_TEST_DIRNAME/../.." && pwd)" diff --git a/tests/scripts/duplicate-work-guard.bats b/tests/scripts/duplicate-work-guard.bats deleted file mode 100644 index 2637de756..000000000 --- a/tests/scripts/duplicate-work-guard.bats +++ /dev/null @@ -1,77 +0,0 @@ -#!/usr/bin/env bats -# Regression tests for skills/evolve/scripts/duplicate-work-guard.sh (ag-6jt/ag-2je). -# -# The evolve-cron-rpi discovery loop kept re-seeding tracking beads for work -# already covered by an existing bead or merged PR (ag-b8m≈ag-jov, ag-6kw≈ag-c2i) -# because the only prior guard matched EXACT open-bead titles. These tests pin -# the guard's behavior: it must catch (a) exact normalized title matches and -# (b) same-surface, different-wording matches by significant-token overlap, -# across open AND closed beads — while NOT blocking genuinely novel work. -# -# `bd` is PATH-stubbed to return a fixed bead set so tests are hermetic. - -setup() { - REPO_ROOT="$(git rev-parse --show-toplevel)" - SCRIPT="$REPO_ROOT/skills/evolve/scripts/duplicate-work-guard.sh" - TMP="$(mktemp -d)" - ORIG_PATH="$PATH" - - # Fixture bead set the stubbed `bd list --all --json` returns. - cat > "$TMP/beads.json" <<'JSON' -[ - {"id":"ag-jov","status":"closed","title":"Fix dangling validate-hooks-doc-parity.sh ref in docs-release-governance eval canary"}, - {"id":"ag-x1","status":"open","title":"Add dark mode toggle to settings page"}, - {"id":"ag-open1","status":"open","title":"Implement origin main diff in evolve cron rpi discovery"} -] -JSON - - # Stub `bd`: any `list ... --json` invocation prints the fixture array. - mkdir -p "$TMP/bin" - cat > "$TMP/bin/bd" <<EOF -#!/usr/bin/env bash -if [ "\$1" = "list" ]; then cat "$TMP/beads.json"; else echo "[]"; fi -EOF - chmod +x "$TMP/bin/bd" - PATH="$TMP/bin:$PATH" -} - -teardown() { - PATH="$ORIG_PATH" - rm -rf "$TMP" -} - -@test "exact normalized title match is flagged as duplicate (exit 1)" { - run "$SCRIPT" "Implement origin main diff in evolve cron rpi discovery" - [ "$status" -eq 1 ] - [[ "$output" == *"DUPLICATE"* ]] - [[ "$output" == *"ag-open1"* ]] -} - -@test "same-surface different-wording match is flagged via token overlap (the ag-b8m≈ag-jov class)" { - run "$SCRIPT" "Repair docs-release-governance eval canary dangling hooks-doc-parity reference" - [ "$status" -eq 1 ] - [[ "$output" == *"ag-jov"* ]] -} - -@test "closed beads are checked, not only open ones" { - # ag-jov is closed; a near-duplicate of it must still be caught. - run "$SCRIPT" "docs-release-governance eval canary dangling parity hooks doc ref" - [ "$status" -eq 1 ] - [[ "$output" == *"ag-jov [closed]"* ]] -} - -@test "genuinely novel work is NOT flagged (exit 0, no false positive)" { - run "$SCRIPT" "Configure Prometheus alerting for GPU temperature thresholds" - [ "$status" -eq 0 ] - [[ "$output" == *"OK"* ]] -} - -@test "missing title argument is a usage error (exit 2)" { - run "$SCRIPT" - [ "$status" -eq 2 ] -} - -@test "blank title argument is a usage error (exit 2)" { - run "$SCRIPT" " " - [ "$status" -eq 2 ] -} diff --git a/tests/scripts/e4-producer-truth-chain.bats b/tests/scripts/e4-producer-truth-chain.bats deleted file mode 100644 index 58aa2c814..000000000 --- a/tests/scripts/e4-producer-truth-chain.bats +++ /dev/null @@ -1,108 +0,0 @@ -#!/usr/bin/env bats -# E4.TEST (age-membrane-memory-arch-tz2s.3.4) — producer-truth + seam wiring -# verified end to end. Asserts the WHOLE medallion chain in ONE isolated flow -# (the component seams are tested individually elsewhere; this ties them): -# -# BRONZE a real session transcript with per-message usage blocks -# -> SILVER `ao yield emit usage` lands a yield-ledger usage event whose -# tokens_in/out are the REAL footprint derived from the transcript -# (the age-ptts producer seam), NOT the old hardcoded 0 -# -> PROVENANCE a CONFIRMED pawl verdict emits a verdict->commit PROV-O edge -# (trunk-only / CONFIRMED-only, per the z9p0 council Option A) -# -# Real components only (ao, pawl-verdict.sh); ledgers isolated by running from a -# throwaway repo (both ao yield + the provenance emit resolve their ledger by -# walking up from CWD), so the real repo ledgers are never touched. - -setup_file() { - REPO_ROOT="$(cd "$BATS_TEST_DIRNAME/../.." && pwd)" - if [ -n "${AO_TEST_BIN:-}" ] && [ -x "${AO_TEST_BIN}" ]; then - AO="$AO_TEST_BIN" - elif [ -x "$REPO_ROOT/cli/bin/ao" ]; then - AO="$REPO_ROOT/cli/bin/ao" - else - AO="$BATS_FILE_TMPDIR/ao" - ( cd "$REPO_ROOT/cli" && go build -o "$AO" ./cmd/ao ) || AO="" - fi - export REPO_ROOT AO -} - -setup() { - # This suite asserts the STRICT verdict->ledger EDGE contract (line 83 requires - # the PROV ledger to actually gain the edge). Strip any ambient - # PAWL_EDGE_FAIL_OPEN the CI bats harness sets suite-wide (wave 1), so a broken - # emit is a hard failure here rather than a silent warn-and-continue. - unset PAWL_EDGE_FAIL_OPEN - WORK="$BATS_TEST_TMPDIR/proj" - mkdir -p "$WORK" - ( cd "$WORK" && git init -q && git config user.email t@t && git config user.name t \ - && echo seed > seed.txt && git add . && git commit -qm "seed ag-e4 work" ) - YIELD_LEDGER="$WORK/.agents/yield/yield-ledger.jsonl" - PROV_LEDGER="$WORK/docs/provenance/ledger.jsonl" -} - -@test "E4 chain: real transcript -> silver usage (real tokens) -> provenance verdict->commit edge" { - [ -n "$AO" ] || skip "no ao binary available" - - # ── BRONZE: a faithful session transcript (production parser shape) ───────── - cat > "$WORK/session.jsonl" <<'EOF' -{"type":"assistant","timestamp":"2026-04-11T12:00:00Z","message":{"role":"assistant","content":"a","usage":{"input_tokens":100,"cache_read_input_tokens":400,"output_tokens":30}}} -{"type":"assistant","timestamp":"2026-04-11T12:00:05Z","message":{"role":"assistant","content":"b","usage":{"input_tokens":50,"cache_creation_input_tokens":200,"output_tokens":20}}} -EOF - # The fixture is a KNOWN-GOOD faithful transcript, so ao MUST derive non-zero - # from it. A skip is legitimate ONLY when ao cannot parse at all (non-zero exit - # -> parser absent/too old); a "0 0" from a SUCCESSFUL parse is a producer - # REGRESSION and must FAIL the test, not skip it. - if EXP_PAIR="$("$AO" yield tokens --transcript "$WORK/session.jsonl" --pair 2>/dev/null)"; then - read -r EXP_IN EXP_OUT <<< "$EXP_PAIR" - else - skip "ao yield tokens errored on the fixture (parser unavailable/too old)" - fi - [ "${EXP_IN:-0}" -gt 0 ] # regression guard: a working parser MUST NOT derive 0 - - # ── BRONZE -> SILVER: emit the usage event with REAL derived tokens ───────── - ( cd "$WORK" && "$AO" yield emit usage --bead ag-e4 --run e4-run \ - --json "{\"tokens_in\":$EXP_IN,\"tokens_out\":$EXP_OUT,\"cost_usd\":0,\"wall_clock_s\":0,\"model\":\"m\",\"phase\":\"implement\"}" ) >/dev/null - [ -f "$YIELD_LEDGER" ] - run python3 -c " -import json -ti=to=None -for l in open('$YIELD_LEDGER'): - l=l.strip() - if not l: continue - e=json.loads(l) - if e.get('event')=='usage' and e.get('bead_id')=='ag-e4': - b=e.get('body',{}) or {}; ti=b.get('tokens_in'); to=b.get('tokens_out') -print('%s %s' % (ti,to)) -" - [ "$output" = "$EXP_IN $EXP_OUT" ] # silver carries the REAL footprint - [ "$output" != "0 0" ] # not the old hardcoded 0 - - # ── SILVER -> PROVENANCE: a CONFIRMED verdict emits a verdict->commit edge ── - HEAD_SHA="$(git -C "$WORK" rev-parse HEAD)" - EV="$WORK/evidence.txt"; printf 'reviewer ran\n' > "$EV" - # AO_BIN pins the trusted ao binary for the verdict->ledger edge emit. In CI - # `ao` is NOT on PATH, so without this pin pawl-verdict.sh finds no trusted ao - # and the edge never binds ("no trusted ao binary found"). $AO is the built - # binary resolved in setup_file (test already skipped above if empty). - ( cd "$WORK" && AO_BIN="$AO" bash "$REPO_ROOT/scripts/pawl-verdict.sh" write ag-e4 0 \ - --disposition CONFIRMED --head "$HEAD_SHA" \ - --author-context e4-author --author-family claude --mode fresh-context \ - --refuter codex:CONFIRMED:e4-refuter:"$EV" --dir "$WORK/verdicts" ) >/dev/null - [ -f "$PROV_LEDGER" ] - # the trunk-only PROV-O edge: a wasDerivedFrom verdict->commit for this bead - grep -q '"relation":"wasDerivedFrom"' "$PROV_LEDGER" - grep -q 'ag-e4' "$PROV_LEDGER" - run python3 -c " -import json -ok=False -for l in open('$PROV_LEDGER'): - l=l.strip() - if not l: continue - e=json.loads(l) - if e.get('relation')=='wasDerivedFrom' and e.get('from_type')=='verdict' and e.get('to_type')=='commit' and 'ag-e4' in str(e.get('from_id','')): - ok=True -print('edge' if ok else 'none') -" - [ "$output" = "edge" ] # provenance verdict->commit edge present -} diff --git a/tests/scripts/export-session-summary.bats b/tests/scripts/export-session-summary.bats deleted file mode 100644 index 7cb51d406..000000000 --- a/tests/scripts/export-session-summary.bats +++ /dev/null @@ -1,146 +0,0 @@ -#!/usr/bin/env bats -# Regression tests for scripts/export-session-summary.sh (soc-absm). -# -# Each test builds an isolated repo with synthetic .agents/evolve/cycle- -# history.jsonl + a few commits, then runs the script and asserts the -# generated markdown's shape. - -setup() { - REPO_ROOT="$(git rev-parse --show-toplevel)" - SCRIPT="$REPO_ROOT/scripts/export-session-summary.sh" - TMP="$(mktemp -d)" - ORIG_DIR="$PWD" - - git init --quiet --initial-branch=main "$TMP/repo" - cd "$TMP/repo" - git config user.email t@t.test - git config user.name tester - git commit --quiet --allow-empty -m "initial" - mkdir -p .agents/evolve - cd "$ORIG_DIR" -} - -teardown() { - cd "$ORIG_DIR" 2>/dev/null || true - rm -rf "$TMP" -} - -write_cycle() { - # write_cycle <cycle> <ts> <mode> <result> <notes> - cat <<EOF >> "$TMP/repo/.agents/evolve/cycle-history.jsonl" -{"cycle":$1,"ts":"$2","mode":"$3","result":"$4","notes":"$5"} -EOF -} - -run_export() { - cd "$TMP/repo" - run "$SCRIPT" "$@" -} - -@test "writes a summary file when --stdout is not passed" { - write_cycle 1 "2026-05-20T10:00:00Z" "test-mode" "productive" "first" - run_export --no-bd --no-prs --out "$TMP/repo/out.md" - [ "$status" -eq 0 ] - [ -f "$TMP/repo/out.md" ] - grep -q "^# Session summary" "$TMP/repo/out.md" - grep -q "## Outcomes" "$TMP/repo/out.md" -} - -@test "--stdout prints to stdout instead of file" { - write_cycle 1 "2026-05-20T10:00:00Z" "test-mode" "productive" "first" - run_export --stdout --no-bd --no-prs - [ "$status" -eq 0 ] - [[ "$output" == *"# Session summary"* ]] - [[ "$output" == *"## Outcomes"* ]] - # No file should have been created at the default path. - ! ls "$TMP/repo/.agents/evolve/session-summary-"*.md 2>/dev/null -} - -@test "filters cycles by ISO timestamp window" { - # Two cycles, one inside the window, one outside. - write_cycle 1 "2026-05-20T08:00:00Z" "old-mode" "productive" "outside" - write_cycle 2 "2026-05-20T12:00:00Z" "new-mode" "productive" "inside" - run_export --stdout --no-bd --no-prs --since "2026-05-20T10:00:00Z" - [ "$status" -eq 0 ] - [[ "$output" == *"new-mode"* ]] - [[ "$output" != *"old-mode"* ]] - # Header counts reflect filter. - [[ "$output" == *"Cycles: **1**"* ]] -} - -@test "compressed cycle ledger truncates notes to 140 chars" { - long_note="$(printf 'x%.0s' {1..300})" - # Use a dynamic timestamp so the cycle falls inside the default 24h export - # window regardless of when the test runs (was hardcoded 2026-05-20, broke - # after 2026-05-21 — see soc-4ake). - write_cycle 1 "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "m" "productive" "$long_note" - run_export --stdout --no-bd --no-prs - [ "$status" -eq 0 ] - # Find the line containing the cycle and check length. - line="$(printf '%s\n' "$output" | grep 'cycle 1')" - [ -n "$line" ] - [ "${#line}" -lt 200 ] -} - -@test "outcomes section reports commit count from git log" { - cd "$TMP/repo" - for i in 1 2 3; do - echo "$i" > "file-$i.txt" - git add "file-$i.txt" - git commit --quiet -m "commit $i" - done - cd "$ORIG_DIR" - run_export --stdout --no-bd --no-prs --since HEAD~3 - [ "$status" -eq 0 ] - # 3 new commits inside the window (initial commit is outside) - [[ "$output" == *"Commits: **3**"* ]] || [[ "$output" == *"Commits: **4**"* ]] -} - -@test "missing cycle-history.jsonl yields zero-count, still succeeds" { - rm -f "$TMP/repo/.agents/evolve/cycle-history.jsonl" - run_export --stdout --no-bd --no-prs --since "2026-05-20T08:00:00Z" - [ "$status" -eq 0 ] - [[ "$output" == *"Cycles: **0**"* ]] -} - -@test "--no-bd suppresses the memories section" { - write_cycle 1 "2026-05-20T10:00:00Z" "m" "productive" "x" - run_export --stdout --no-bd --no-prs - [ "$status" -eq 0 ] - [[ "$output" != *"## New memories"* ]] -} - -@test "--no-prs suppresses the merged PR section" { - write_cycle 1 "2026-05-20T10:00:00Z" "m" "productive" "x" - run_export --stdout --no-bd --no-prs - [ "$status" -eq 0 ] - [[ "$output" != *"## Merged PRs"* ]] -} - -@test "carry-forward section reads goal and in-flight from session-state.json" { - cat > "$TMP/repo/.agents/evolve/session-state.json" <<EOF -{ - "goal": "clear-all-open-beads", - "batch_prs": [101, 102] -} -EOF - write_cycle 1 "2026-05-20T10:00:00Z" "m" "productive" "x" - run_export --stdout --no-bd --no-prs - [ "$status" -eq 0 ] - [[ "$output" == *"clear-all-open-beads"* ]] - [[ "$output" == *"101, 102"* ]] -} - -@test "rejects unknown flag with usage error" { - run_export --weasel - [ "$status" -eq 2 ] - [[ "$output" == *"unknown"* ]] -} - -@test "default --out path lands under .agents/evolve/" { - write_cycle 1 "2026-05-20T10:00:00Z" "m" "productive" "x" - run_export --no-bd --no-prs - [ "$status" -eq 0 ] - files=$(ls "$TMP/repo/.agents/evolve/session-summary-"*.md 2>/dev/null | wc -l | tr -d ' ') - [ "$files" -eq 1 ] -} diff --git a/tests/scripts/generate-registry.bats b/tests/scripts/generate-registry.bats deleted file mode 100644 index 2472c4c75..000000000 --- a/tests/scripts/generate-registry.bats +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env bats -# -# Tests for scripts/generate-registry.sh focused on deterministic eval-suite -# discovery. Registry generation must depend on tracked files, not local -# untracked eval artifacts from developer runs. - -setup() { - REPO_ROOT="$(cd "$BATS_TEST_DIRNAME/../.." && pwd)" - TMP_DIR="$(mktemp -d)" - FAKE_REPO="$TMP_DIR/repo" - mkdir -p "$FAKE_REPO/scripts" - cp "$REPO_ROOT/scripts/generate-registry.sh" "$FAKE_REPO/scripts/generate-registry.sh" - chmod +x "$FAKE_REPO/scripts/generate-registry.sh" - git -C "$TMP_DIR" init repo >/dev/null 2>&1 -} - -teardown() { - rm -rf "$TMP_DIR" -} - -@test "eval registry ignores untracked eval suites and untracked eval files" { - mkdir -p "$FAKE_REPO/evals/tracked-suite" "$FAKE_REPO/evals/local-suite" - printf '{"id":"tracked.case"}\n' > "$FAKE_REPO/evals/tracked-suite/tracked.json" - printf '{"id":"local.case"}\n' > "$FAKE_REPO/evals/tracked-suite/local-only.json" - printf '{"id":"local.suite"}\n' > "$FAKE_REPO/evals/local-suite/noise.json" - git -C "$FAKE_REPO" add evals/tracked-suite/tracked.json - - run bash "$FAKE_REPO/scripts/generate-registry.sh" --stdout - - [ "$status" -eq 0 ] - printf '%s\n' "$output" | jq -e ' - .surfaces.evals == [ - { - "suite": "tracked-suite", - "path": "evals/tracked-suite/", - "eval_count": 1, - "evals": ["tracked"] - } - ] - ' >/dev/null -} diff --git a/tests/scripts/generate-skill-catalog.bats b/tests/scripts/generate-skill-catalog.bats deleted file mode 100644 index 37667ca68..000000000 --- a/tests/scripts/generate-skill-catalog.bats +++ /dev/null @@ -1,291 +0,0 @@ -#!/usr/bin/env bats -# Regression tests for scripts/generate-skill-catalog.sh (soc-vuu6.4). -# -# Builds isolated fixture repos with small skills/ trees, then runs the -# generator and asserts structure + drift detection. We avoid touching the -# real repo's catalog by always running inside $TMP. - -setup() { - REPO_ROOT="$(git rev-parse --show-toplevel)" - SCRIPT="$REPO_ROOT/scripts/generate-skill-catalog.sh" - DRIFT_SCRIPT="$REPO_ROOT/scripts/check-skill-catalog-drift.sh" - TMP="$(mktemp -d)" - ORIG_DIR="$PWD" - - git init --quiet --initial-branch=main "$TMP/repo" - cd "$TMP/repo" - git config user.email t@t.test - git config user.name tester - git commit --quiet --allow-empty -m "initial" - mkdir -p skills schemas - # The drift check script needs to find the generator at the same repo path - # so we install both into the fixture's `scripts/` dir. - mkdir -p scripts - cp "$SCRIPT" scripts/generate-skill-catalog.sh - cp "$DRIFT_SCRIPT" scripts/check-skill-catalog-drift.sh - chmod +x scripts/generate-skill-catalog.sh scripts/check-skill-catalog-drift.sh - cd "$ORIG_DIR" -} - -teardown() { - cd "$ORIG_DIR" 2>/dev/null || true - rm -rf "$TMP" -} - -# Write a skill SKILL.md with a given frontmatter body. -write_skill() { - local name="$1" fm="$2" - mkdir -p "$TMP/repo/skills/$name" - { - echo "---" - printf '%s\n' "$fm" - echo "---" - echo - echo "# $name" - } > "$TMP/repo/skills/$name/SKILL.md" -} - -run_gen() { - cd "$TMP/repo" - run scripts/generate-skill-catalog.sh "$@" -} - -run_drift() { - cd "$TMP/repo" - run scripts/check-skill-catalog-drift.sh "$@" -} - -@test "generator emits valid JSON with required envelope fields" { - write_skill alpha "name: alpha -description: first skill -hexagonal_role: domain -consumes: [] -produces: [] -context_rel: []" - run_gen --stdout - [ "$status" -eq 0 ] - echo "$output" | jq -e '.schema_version == "2"' >/dev/null - echo "$output" | jq -e '.skill_count == 1' >/dev/null - echo "$output" | jq -e '.skills | length == 1' >/dev/null - echo "$output" | jq -e '.generated_at | startswith("20")' >/dev/null -} - -@test "extracts name/description/hexagonal_role from frontmatter" { - write_skill alpha "name: alpha -description: hello world -hexagonal_role: supporting -consumes: [] -produces: [] -context_rel: []" - run_gen --stdout - [ "$status" -eq 0 ] - echo "$output" | jq -e '.skills[0].name == "alpha"' >/dev/null - echo "$output" | jq -e '.skills[0].description == "hello world"' >/dev/null - echo "$output" | jq -e '.skills[0].hexagonal_role == "supporting"' >/dev/null -} - -@test "consumes/produces/practices lists round-trip into arrays" { - write_skill beta "name: beta -description: lists test -hexagonal_role: domain -practices: -- tdd -- bdd-gherkin -consumes: -- standards -- domain -produces: -- result.json -- verdict.json -context_rel: []" - run_gen --stdout - [ "$status" -eq 0 ] - echo "$output" | jq -e '.skills[0].consumes == ["standards","domain"]' >/dev/null - echo "$output" | jq -e '.skills[0].produces == ["result.json","verdict.json"]' >/dev/null - echo "$output" | jq -e '.skills[0].practices == ["tdd","bdd-gherkin"]' >/dev/null -} - -@test "context_rel captures all entries including the last one (the parser-bug regression)" { - write_skill gamma "name: gamma -description: context-rel test -hexagonal_role: domain -consumes: [] -produces: [] -context_rel: -- kind: customer-of - with: alpha -- kind: customer-of - with: beta -- kind: customer-of - with: delta -skill_api_version: 1" - run_gen --stdout - [ "$status" -eq 0 ] - echo "$output" | jq -e '.skills[0].context_rel | length == 3' >/dev/null - echo "$output" | jq -e '.skills[0].context_rel[2].with == "delta"' >/dev/null -} - -@test "multi-line frontmatter parses on BSD/POSIX awk without 'newline in string' (ag-mm6q)" { - # Regression for the BSD-awk incompatibility: the parser used to pass the - # whole frontmatter via `awk -v fm="$fm"`, which only GNU awk accepts with - # embedded newlines. On macOS/BSD awk it errored "awk: newline in string" - # and produced no catalog. Assert a multi-line frontmatter parses cleanly - # and the error string never appears on stderr. - write_skill delta "name: delta -description: multi-line frontmatter regression -hexagonal_role: domain -consumes: -- standards -- domain -produces: -- result.json -context_rel: -- kind: customer-of - with: alpha -skill_api_version: 1" - run_gen --stdout - [ "$status" -eq 0 ] - [[ "$output" != *"newline in string"* ]] - echo "$output" | jq -e '.skills[0].name == "delta"' >/dev/null - echo "$output" | jq -e '.skills[0].consumes == ["standards","domain"]' >/dev/null - echo "$output" | jq -e '.skills[0].context_rel[0].with == "alpha"' >/dev/null -} - -@test "user_invocable is true only when explicitly set" { - write_skill u1 "name: u1 -description: invocable -hexagonal_role: domain -user-invocable: true -consumes: [] -produces: [] -context_rel: []" - write_skill u2 "name: u2 -description: not invocable -hexagonal_role: domain -consumes: [] -produces: [] -context_rel: []" - run_gen --stdout - [ "$status" -eq 0 ] - echo "$output" | jq -e '.skills[] | select(.name=="u1") | .user_invocable == true' >/dev/null - echo "$output" | jq -e '.skills[] | select(.name=="u2") | .user_invocable == false' >/dev/null -} - -@test "references_count reflects skills/<name>/references/*.md count" { - write_skill withrefs "name: withrefs -description: x -hexagonal_role: domain -consumes: [] -produces: [] -context_rel: []" - mkdir -p "$TMP/repo/skills/withrefs/references" - touch "$TMP/repo/skills/withrefs/references/a.md" \ - "$TMP/repo/skills/withrefs/references/b.md" - run_gen --stdout - [ "$status" -eq 0 ] - echo "$output" | jq -e '.skills[] | select(.name=="withrefs") | .references_count == 2' >/dev/null -} - -@test "codex_override_present is true when skills-codex/<name>/ exists" { - write_skill swithcodex "name: swithcodex -description: x -hexagonal_role: domain -consumes: [] -produces: [] -context_rel: []" - mkdir -p "$TMP/repo/skills-codex/swithcodex" - touch "$TMP/repo/skills-codex/swithcodex/SKILL.md" - run_gen --stdout - [ "$status" -eq 0 ] - echo "$output" | jq -e '.skills[] | select(.name=="swithcodex") | .codex_override_present == true' >/dev/null -} - -@test "default --out writes skills/catalog.json and prints summary" { - write_skill alpha "name: alpha -description: x -hexagonal_role: domain -consumes: [] -produces: [] -context_rel: []" - run_gen - [ "$status" -eq 0 ] - [ -f "$TMP/repo/skills/catalog.json" ] - [[ "$output" == *"wrote"* ]] -} - -@test "--check passes when committed catalog matches regeneration" { - write_skill alpha "name: alpha -description: x -hexagonal_role: domain -consumes: [] -produces: [] -context_rel: []" - run_gen - [ "$status" -eq 0 ] - run_gen --check - [ "$status" -eq 0 ] -} - -@test "--check fails (exit 1) when committed catalog drifts from source" { - write_skill alpha "name: alpha -description: x -hexagonal_role: domain -consumes: [] -produces: [] -context_rel: []" - run_gen - [ "$status" -eq 0 ] - # Mutate a SKILL.md without regenerating the catalog. - write_skill alpha "name: alpha -description: x CHANGED -hexagonal_role: domain -consumes: [] -produces: [] -context_rel: []" - run_gen --check - [ "$status" -eq 1 ] - [[ "$output" == *"DRIFT"* ]] -} - -@test "drift wrapper exits 0 when in sync" { - write_skill alpha "name: alpha -description: x -hexagonal_role: domain -consumes: [] -produces: [] -context_rel: []" - run_gen - run_drift - [ "$status" -eq 0 ] -} - -@test "drift wrapper exits 1 when out of sync" { - write_skill alpha "name: alpha -description: x -hexagonal_role: domain -consumes: [] -produces: [] -context_rel: []" - run_gen - write_skill alpha "name: alpha -description: drifted -hexagonal_role: domain -consumes: [] -produces: [] -context_rel: []" - run_drift - [ "$status" -eq 1 ] -} - -@test "empty skills/ produces a valid empty catalog" { - run_gen --stdout - [ "$status" -eq 0 ] - echo "$output" | jq -e '.skill_count == 0' >/dev/null - echo "$output" | jq -e '.skills | length == 0' >/dev/null -} - -@test "unknown flag exits 2" { - run_gen --weasel - [ "$status" -eq 2 ] - [[ "$output" == *"unknown"* ]] -} diff --git a/tests/scripts/harvest-to-ledger.bats b/tests/scripts/harvest-to-ledger.bats deleted file mode 100644 index f1afab4f9..000000000 --- a/tests/scripts/harvest-to-ledger.bats +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env bats -# Regression tests for evals/membrane/harvest-2026-06-22/harvest-to-ledger.sh — turns -# a membrane-eval scorecard into escape chains in an ISOLATED yield ledger. The -# fail-closed guards here once had a real fabrication-class bug (an IFS-collapse made -# it emit a DEGRADED task as an escape), so each is pinned: caught -> 2-verdict chain, -# escaped(miss) -> 3-verdict chain, degraded -> SKIPPED, true-done -> skipped. Uses -# the real `ao yield emit` against a throwaway ledger root (no models). - -setup() { - REPO="$BATS_TEST_DIRNAME/../.." - SCRIPT="$REPO/evals/membrane/harvest-2026-06-22/harvest-to-ledger.sh" - # Prefer an already-built ao; fall back to building one (the script does this too). - AO="$REPO/cli/bin/ao"; [ -x "$AO" ] || AO="/tmp/ao-pawl" - [ -x "$AO" ] || ( cd "$REPO/cli" && go build -o /tmp/ao-pawl ./cmd/ao && AO=/tmp/ao-pawl ) - export AGENTOPS_AO_BIN="$AO" - FIX="$(mktemp -d)" -} -teardown() { rm -rf "$FIX"; } - -# Build a one-task scorecard. $1=class-shape: caught|escaped|degraded|truedone -scorecard() { - case "$1" in - caught) echo '{"per_task":[{"task":"t","oracle_pass":false,"verdict":"REFUTE","why":"wrong","degraded":false}]}' ;; - escaped) echo '{"per_task":[{"task":"t","oracle_pass":false,"verdict":"ACK","why":"looked ok","degraded":false}]}' ;; - degraded) echo '{"per_task":[{"task":"t","oracle_pass":false,"verdict":"DRY","why":"","degraded":true}]}' ;; - truedone) echo '{"per_task":[{"task":"t","oracle_pass":true,"verdict":"ACK","why":"ok","degraded":false}]}' ;; - esac -} - -ledger() { echo "$FIX/lroot/.agents/yield/yield-ledger.jsonl"; } -dispositions() { # disposition list from the ledger, in order - python3 -c 'import json,sys -for line in open(sys.argv[1]): - line=line.strip() - if line: - d=json.loads(line); b=d.get("body",d); print(b.get("disposition"))' "$(ledger)" -} - -@test "a caught false-done emits a 2-verdict escape chain (producer CONFIRMED -> membrane REFUTED)" { - scorecard caught > "$FIX/sc.json" - run bash "$SCRIPT" "$FIX/sc.json" "$FIX/lroot" run-x - [ "$status" -eq 0 ] - [ "$(wc -l < "$(ledger)" | tr -d ' ')" = "2" ] - run dispositions - [ "${lines[0]}" = "CONFIRMED" ] - [ "${lines[1]}" = "REFUTED" ] -} - -@test "an escaped (membrane MISS) emits a 3-verdict chain ending in an oracle REFUTED" { - scorecard escaped > "$FIX/sc.json" - run bash "$SCRIPT" "$FIX/sc.json" "$FIX/lroot" run-x - [ "$status" -eq 0 ] - [ "$(wc -l < "$(ledger)" | tr -d ' ')" = "3" ] - run dispositions - [ "${lines[0]}" = "CONFIRMED" ] # producer - [ "${lines[1]}" = "CONFIRMED" ] # membrane ACK = the wrong confirm - [ "${lines[2]}" = "REFUTED" ] # oracle ground-truth overturn -} - -@test "a DEGRADED task is SKIPPED, never emitted as an escape (the IFS-collapse fabrication guard)" { - scorecard degraded > "$FIX/sc.json" - run bash "$SCRIPT" "$FIX/sc.json" "$FIX/lroot" run-x - [ "$status" -eq 0 ] - [[ "$output" == *"skipped (true-done/degraded): 1"* ]] - # No ledger file/lines for a skipped task (nothing emitted). - [ ! -s "$(ledger)" ] -} - -@test "a true-done is skipped (only false-dones are escapes)" { - scorecard truedone > "$FIX/sc.json" - run bash "$SCRIPT" "$FIX/sc.json" "$FIX/lroot" run-x - [ "$status" -eq 0 ] - [[ "$output" == *"skipped (true-done/degraded): 1"* ]] - [ ! -s "$(ledger)" ] -} diff --git a/tests/scripts/heal-dispositions.bats b/tests/scripts/heal-dispositions.bats deleted file mode 100644 index 8d40e72e6..000000000 --- a/tests/scripts/heal-dispositions.bats +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/env bats -# Regression for ag-cw2y item 1: heal --strict must flag a user-invocable skill -# that has no row in docs/contracts/skill-dispositions.yaml. This is the exact -# gate that silently passed when /burndown (ag-3yl8 #600) was added, costing a -# CI round. The check is fixture-driven via the HEAL_REPO_ROOT override. - -setup() { - HEAL="$BATS_TEST_DIRNAME/../../skills/heal-skill/scripts/heal.sh" - FIX="$(mktemp -d)" - mkdir -p "$FIX/skills/foo" "$FIX/docs/contracts" - cat > "$FIX/skills/foo/SKILL.md" <<'EOF' ---- -name: foo -description: A fixture skill for the dispositions coverage check. -skill_api_version: 1 ---- -# foo -EOF - cat > "$FIX/docs/contracts/skill-dispositions.yaml" <<'EOF' -dispositions: - - skill: bar - domain: "BC1 Corpus" - hexagonal_role: domain - disposition: keep - rationale: "unrelated existing row" -EOF -} - -teardown() { rm -rf "$FIX"; } - -@test "heal flags a user-invocable skill missing from skill-dispositions.yaml" { - run env HEAL_REPO_ROOT="$FIX" bash "$HEAL" --check skills/foo - [[ "$output" == *"MISSING_DISPOSITION"* ]] - [[ "$output" == *"foo"* ]] -} - -@test "heal does NOT flag a skill that has a dispositions row" { - cat >> "$FIX/docs/contracts/skill-dispositions.yaml" <<'EOF' - - skill: foo - domain: "BC1 Corpus" - hexagonal_role: domain - disposition: keep - rationale: "now covered" -EOF - run env HEAL_REPO_ROOT="$FIX" bash "$HEAL" --check skills/foo - [[ "$output" != *"MISSING_DISPOSITION"* ]] -} - -@test "heal --strict exits non-zero and names the missing disposition" { - run env HEAL_REPO_ROOT="$FIX" bash "$HEAL" --check --strict skills/foo - [ "$status" -eq 1 ] - [[ "$output" == *"MISSING_DISPOSITION"* ]] -} - -@test "heal treats redirect-only packages as aliases, not tiered implementations" { - cat >> "$FIX/docs/contracts/skill-dispositions.yaml" <<'EOF' - - skill: foo - domain: "BC1 Corpus" - hexagonal_role: domain - disposition: keep - rationale: "canonical redirect target" -EOF - mkdir -p "$FIX/skills/old-foo" - cat > "$FIX/skills/old-foo/SKILL.md" <<'EOF' ---- -name: old-foo -description: Compatibility pointer for foo. -redirect_to: foo -implementation: false ---- -# old-foo - -Invoke `/foo` once. -EOF - - run env HEAL_REPO_ROOT="$FIX" bash "$HEAL" --check --strict skills/old-foo - [ "$status" -eq 0 ] - [[ "$output" != *"MISSING_TIER"* ]] - [[ "$output" != *"MISSING_API_VERSION"* ]] - [[ "$output" != *"MISSING_DISPOSITION"* ]] -} diff --git a/tests/scripts/install-pre-push-gate.bats b/tests/scripts/install-pre-push-gate.bats deleted file mode 100644 index 668b20b06..000000000 --- a/tests/scripts/install-pre-push-gate.bats +++ /dev/null @@ -1,185 +0,0 @@ -#!/usr/bin/env bats - -setup() { - ROOT="$(cd "$BATS_TEST_DIRNAME/../.." && pwd)" - SCRIPT="$ROOT/scripts/install-pre-push-gate.sh" -} - -write_fixture_hook() { - repo="$1" - marker="$2" - mkdir -p "$repo/scripts/hooks" - cat > "$repo/scripts/hooks/pre-push.local" <<EOS -#!/usr/bin/env sh -printf '%s\n' '$marker' >> "\$AGENTOPS_LOG" -EOS - chmod +x "$repo/scripts/hooks/pre-push.local" -} - -init_hook_repo() { - repo="$1" - marker="$2" - mkdir -p "$repo" - git -C "$repo" init -q - git -C "$repo" config user.email test@example.com - git -C "$repo" config user.name Test - write_fixture_hook "$repo" "$marker" - git -C "$repo" add scripts/hooks/pre-push.local - git -C "$repo" commit -m "hook $marker" >/dev/null - git -C "$repo" update-ref refs/remotes/origin/main HEAD -} - -@test "installed pre-push wrapper replays stdin after a prior hook consumes it" { - repo="$BATS_TEST_TMPDIR/repo" - mkdir -p "$repo" - git -C "$repo" init -q - git -C "$repo" config user.email test@example.com - git -C "$repo" config user.name Test - echo init > "$repo/README.md" - git -C "$repo" add README.md - git -C "$repo" commit -m initial >/dev/null - - hook="$repo/.git/hooks/pre-push" - cat > "$hook" <<'EOS' -#!/usr/bin/env sh -# --- BEGIN BEADS INTEGRATION v1.0.5 --- -cat > "$BEADS_LOG" -# --- END BEADS INTEGRATION v1.0.5 --- -EOS - chmod +x "$hook" - - run bash -c 'cd "$1" && "$2"' _ "$repo" "$SCRIPT" - [ "$status" -eq 0 ] - - cat > "$repo/.git/hooks/pre-push.local" <<'EOS' -#!/usr/bin/env sh -cat > "$AGENTOPS_LOG" -EOS - chmod +x "$repo/.git/hooks/pre-push.local" - - push_record='refs/heads/main abc refs/heads/main def' - run env BEADS_LOG="$BATS_TEST_TMPDIR/beads.log" \ - AGENTOPS_LOG="$BATS_TEST_TMPDIR/agentops.log" \ - sh -c 'cd "$1" && printf "%s\n" "$2" | "$3"' _ "$repo" "$push_record" "$hook" - [ "$status" -eq 0 ] - - [ "$(cat "$BATS_TEST_TMPDIR/beads.log")" = "$push_record" ] - [ "$(cat "$BATS_TEST_TMPDIR/agentops.log")" = "$push_record" ] -} - -@test "installed wrapper preserves an explicitly installed fast-forward pushed hook" { - repo="$BATS_TEST_TMPDIR/repo" - log="$BATS_TEST_TMPDIR/agentops.log" - init_hook_repo "$repo" trunk - old_sha="$(git -C "$repo" rev-parse origin/main)" - - write_fixture_hook "$repo" candidate - git -C "$repo" add scripts/hooks/pre-push.local - git -C "$repo" commit -m "hook candidate" >/dev/null - new_sha="$(git -C "$repo" rev-parse HEAD)" - - run bash -c 'cd "$1" && "$2"' _ "$repo" "$SCRIPT" - [ "$status" -eq 0 ] - - cp "$repo/scripts/hooks/pre-push.local" "$repo/.git/hooks/pre-push.local" - chmod +x "$repo/.git/hooks/pre-push.local" - - run env AGENTOPS_LOG="$log" \ - sh -c 'cd "$1" && printf "%s %s %s %s\n" refs/heads/main "$2" refs/heads/main "$3" | "$4"' \ - _ "$repo" "$new_sha" "$old_sha" "$repo/.git/hooks/pre-push" - [ "$status" -eq 0 ] - - [ "$(cat "$log")" = "candidate" ] - run grep -q "candidate" "$repo/.git/hooks/pre-push.local" - [ "$status" -eq 0 ] - run grep -q "trunk" "$repo/.git/hooks/pre-push.local" - [ "$status" -eq 1 ] -} - -@test "installed wrapper heals stale branch hook back to trunk" { - repo="$BATS_TEST_TMPDIR/repo" - log="$BATS_TEST_TMPDIR/agentops.log" - init_hook_repo "$repo" stale - stale_sha="$(git -C "$repo" rev-parse HEAD)" - - write_fixture_hook "$repo" trunk - git -C "$repo" add scripts/hooks/pre-push.local - git -C "$repo" commit -m "hook trunk" >/dev/null - trunk_sha="$(git -C "$repo" rev-parse HEAD)" - git -C "$repo" update-ref refs/remotes/origin/main HEAD - git -C "$repo" checkout -q -b stale-branch "$stale_sha" - - run bash -c 'cd "$1" && "$2"' _ "$repo" "$SCRIPT" - [ "$status" -eq 0 ] - - cp "$repo/scripts/hooks/pre-push.local" "$repo/.git/hooks/pre-push.local" - chmod +x "$repo/.git/hooks/pre-push.local" - - run env AGENTOPS_LOG="$log" \ - sh -c 'cd "$1" && printf "%s %s %s %s\n" refs/heads/main "$2" refs/heads/main "$3" | "$4"' \ - _ "$repo" "$stale_sha" "$trunk_sha" "$repo/.git/hooks/pre-push" - [ "$status" -eq 0 ] - - [ "$(cat "$log")" = "trunk" ] - run grep -q "trunk" "$repo/.git/hooks/pre-push.local" - [ "$status" -eq 0 ] -} - -@test "installed wrapper ignores fast-forward pushed hook unless it was explicitly installed" { - repo="$BATS_TEST_TMPDIR/repo" - log="$BATS_TEST_TMPDIR/agentops.log" - init_hook_repo "$repo" trunk - old_sha="$(git -C "$repo" rev-parse origin/main)" - trunk_hook="$BATS_TEST_TMPDIR/trunk-pre-push.local" - cp "$repo/scripts/hooks/pre-push.local" "$trunk_hook" - - write_fixture_hook "$repo" candidate - git -C "$repo" add scripts/hooks/pre-push.local - git -C "$repo" commit -m "hook candidate" >/dev/null - new_sha="$(git -C "$repo" rev-parse HEAD)" - - run bash -c 'cd "$1" && "$2"' _ "$repo" "$SCRIPT" - [ "$status" -eq 0 ] - - cp "$trunk_hook" "$repo/.git/hooks/pre-push.local" - chmod +x "$repo/.git/hooks/pre-push.local" - - run env AGENTOPS_LOG="$log" \ - sh -c 'cd "$1" && printf "%s %s %s %s\n" refs/heads/main "$2" refs/heads/main "$3" | "$4"' \ - _ "$repo" "$new_sha" "$old_sha" "$repo/.git/hooks/pre-push" - [ "$status" -eq 0 ] - - [ "$(cat "$log")" = "trunk" ] - run grep -q "trunk" "$repo/.git/hooks/pre-push.local" - [ "$status" -eq 0 ] - run grep -q "candidate" "$repo/.git/hooks/pre-push.local" - [ "$status" -eq 1 ] -} - -@test "installed wrapper heals candidate hook on non-main branch pushes" { - repo="$BATS_TEST_TMPDIR/repo" - log="$BATS_TEST_TMPDIR/agentops.log" - init_hook_repo "$repo" trunk - old_sha="$(git -C "$repo" rev-parse origin/main)" - - git -C "$repo" checkout -q -b feature - write_fixture_hook "$repo" candidate - git -C "$repo" add scripts/hooks/pre-push.local - git -C "$repo" commit -m "hook candidate" >/dev/null - new_sha="$(git -C "$repo" rev-parse HEAD)" - - run bash -c 'cd "$1" && "$2"' _ "$repo" "$SCRIPT" - [ "$status" -eq 0 ] - - cp "$repo/scripts/hooks/pre-push.local" "$repo/.git/hooks/pre-push.local" - chmod +x "$repo/.git/hooks/pre-push.local" - - run env AGENTOPS_LOG="$log" \ - sh -c 'cd "$1" && printf "%s %s %s %s\n" refs/heads/feature "$2" refs/heads/feature "$3" | "$4"' \ - _ "$repo" "$new_sha" "$old_sha" "$repo/.git/hooks/pre-push" - [ "$status" -eq 0 ] - - [ "$(cat "$log")" = "trunk" ] - run grep -q "trunk" "$repo/.git/hooks/pre-push.local" - [ "$status" -eq 0 ] -} diff --git a/tests/scripts/jsm-skill-dispositions.bats b/tests/scripts/jsm-skill-dispositions.bats deleted file mode 100644 index 0f0fb21cc..000000000 --- a/tests/scripts/jsm-skill-dispositions.bats +++ /dev/null @@ -1,85 +0,0 @@ -#!/usr/bin/env bats - -setup() { - REPO_ROOT="$(cd "$BATS_TEST_DIRNAME/../.." && pwd)" - CHECKER="$REPO_ROOT/scripts/check-jsm-skill-dispositions.py" - TMP_DIR="$(mktemp -d)" - SNAPSHOT="$TMP_DIR/frozen.json" - AUDIT="$TMP_DIR/audit.md" - MANIFEST="$TMP_DIR/manifest.txt" - MOCK_JSM="$TMP_DIR/jsm" - - printf '%s\n' alpha beta >"$MANIFEST" - cat >"$SNAPSHOT" <<'EOF' -{"schema_version":1,"source_command":"jsm list --remote --jeffreys --json","names":["alpha","beta"]} -EOF - write_audit alpha beta -} - -teardown() { - rm -r "$TMP_DIR" -} - -write_audit() { - { - printf '%s\n' '| # | External package | AgentOps disposition |' '|---:|---|---|' - local i=1 name - for name in "$@"; do - printf "| %s | \`%s\` | keep external |\n" "$i" "$name" - i=$((i + 1)) - done - } >"$AUDIT" -} - -run_check() { - run env \ - JSM_DISPOSITIONS_SNAPSHOT="$SNAPSHOT" \ - JSM_DISPOSITIONS_AUDIT="$AUDIT" \ - JSM_DISPOSITIONS_MANIFEST="$MANIFEST" \ - JSM_BIN="$MOCK_JSM" \ - python3 "$CHECKER" "$@" -} - -@test "checked-in frozen names each have exactly one disposition" { - run_check --check - [ "$status" -eq 0 ] - [[ "$output" == *"2 frozen JSM packages; 2 one-to-one dispositions"* ]] -} - -@test "missing and duplicate disposition decisions fail closed" { - write_audit alpha alpha - run_check --check - [ "$status" -ne 0 ] - [[ "$output" == *"duplicate"* ]] - [[ "$output" == *"missing"* ]] -} - -@test "refresh atomically accepts only a complete remote name set" { - cat >"$MOCK_JSM" <<'EOF' -#!/usr/bin/env bash -printf '%s\n' '{"skills":[{"name":"beta"},{"name":"alpha"}]}' -EOF - chmod +x "$MOCK_JSM" - - run_check --refresh - [ "$status" -eq 0 ] - run jq -e '.names == ["alpha","beta"] and (.refreshed_at | type == "string")' "$SNAPSHOT" - [ "$status" -eq 0 ] - [ ! -e "$SNAPSHOT.candidate" ] -} - -@test "remote refresh failure preserves the prior checked-in snapshot" { - cat >"$MOCK_JSM" <<'EOF' -#!/usr/bin/env bash -exit 23 -EOF - chmod +x "$MOCK_JSM" - before="$(shasum -a 256 "$SNAPSHOT" | awk '{print $1}')" - - run_check --refresh - [ "$status" -ne 0 ] - [[ "$output" == *"refresh failed"* ]] - [[ "$output" == *"--refresh"* ]] - after="$(shasum -a 256 "$SNAPSHOT" | awk '{print $1}')" - [ "$after" = "$before" ] -} diff --git a/tests/scripts/lib/ratchet-difftest.bash b/tests/scripts/lib/ratchet-difftest.bash index 6ddf893a1..2bdfbf28e 100644 --- a/tests/scripts/lib/ratchet-difftest.bash +++ b/tests/scripts/lib/ratchet-difftest.bash @@ -1,6 +1,6 @@ # shellcheck shell=bash # tests/scripts/lib/ratchet-difftest.bash — differential parity harness for the -# ratchet-lib migrations (age-ratchet-lib-extraction-bv7d, pre-mortem FM2). +# ratchet-lib migrations (age-ratchet-lib-extraction-bv7d, premortem FM2). # # A migration slice claims ZERO behavior change. "Exit code + the offender path # appears somewhere" is not that claim — output ordering, stream routing diff --git a/tests/scripts/lint-evidence-lines.bats b/tests/scripts/lint-evidence-lines.bats deleted file mode 100644 index 52f662e1c..000000000 --- a/tests/scripts/lint-evidence-lines.bats +++ /dev/null @@ -1,134 +0,0 @@ -#!/usr/bin/env bats -# Regression tests for scripts/lint-evidence-lines.sh (soc-l340). -# -# The linter must (1) extract Evidence: lines with the same regex CI uses, -# (2) classify each claim against the AP#7 failure modes we've encountered -# in practice, (3) exit non-zero only when blocking issues are present -# (advisory issues need --strict to fail), and (4) accept --stdin / --body / -# <pr-number> input modes uniformly. - -setup() { - REPO_ROOT="$(git rev-parse --show-toplevel)" - SCRIPT="$REPO_ROOT/scripts/lint-evidence-lines.sh" - TMP="$(mktemp -d)" - ORIG_DIR="$PWD" -} - -teardown() { - cd "$ORIG_DIR" 2>/dev/null || true - rm -rf "$TMP" -} - -# Pipe a body through stdin mode. -run_stdin() { - run bash -c "printf '%s' '$1' | $SCRIPT --stdin ${2:-}" -} - -@test "exits 0 when no Evidence: lines present" { - run_stdin "no trailers in this body" - [ "$status" -eq 0 ] - [[ "$output" == *"no Evidence: lines"* ]] -} - -@test "exits 0 on clean bare-path Evidence: lines" { - run_stdin "Evidence: scripts/foo.sh -Evidence: tests/scripts/foo.bats" - [ "$status" -eq 0 ] - [[ "$output" == *"0 blocking"* ]] - [[ "$output" == *"OK"* ]] -} - -@test "blocks on parenthetical addendum (the canonical AP#7 trap)" { - run_stdin "Evidence: scripts/foo.sh (10/10 passing)" - [ "$status" -eq 1 ] - [[ "$output" == *"FAIL"* ]] - [[ "$output" == *"parenthetical"* ]] - [[ "$output" == *"(10/10 passing)"* ]] -} - -@test "blocks on markdown bold inside the path" { - run_stdin "Evidence: **scripts/foo.sh**" - [ "$status" -eq 1 ] - [[ "$output" == *"markdown"* ]] -} - -@test "blocks on backtick code-fence inside the claim" { - run_stdin 'Evidence: `scripts/foo.sh`' - [ "$status" -eq 1 ] - [[ "$output" == *"markdown"* ]] -} - -@test "blocks on pipe character (markdown table sin)" { - run_stdin "Evidence: scripts/foo.sh | tests/foo.bats" - [ "$status" -eq 1 ] - [[ "$output" == *"pipe"* ]] -} - -@test "blocks on empty Evidence: line" { - run_stdin "Evidence:" - [ "$status" -eq 1 ] - [[ "$output" == *"empty"* ]] -} - -@test "blocks on prose-keyword openers (see/cf/note)" { - run_stdin "Evidence: see scripts/foo.sh for details" - [ "$status" -eq 1 ] - [[ "$output" == *"prose"* ]] -} - -@test "trailing whitespace is advisory (passes by default, blocks under --strict)" { - printf 'Evidence: scripts/foo.sh \n' > "$TMP/body.md" - run "$SCRIPT" --body "$TMP/body.md" - [ "$status" -eq 0 ] - [[ "$output" == *"WARN"* ]] || [[ "$output" == *"trailing-ws"* ]] - # Strict mode promotes advisory to blocking. - run "$SCRIPT" --body "$TMP/body.md" --strict - [ "$status" -eq 1 ] -} - -@test "--json output is valid and reports counts" { - printf 'Evidence: scripts/foo.sh\nEvidence: bad (paren)\n' > "$TMP/body.md" - run "$SCRIPT" --body "$TMP/body.md" --json - [ "$status" -eq 1 ] - echo "$output" | jq -e '.blocking == 1 and (.claims | length) == 2' >/dev/null -} - -@test "--body reads from a file path" { - printf 'Evidence: scripts/x.sh\n' > "$TMP/body.md" - run "$SCRIPT" --body "$TMP/body.md" - [ "$status" -eq 0 ] - [[ "$output" == *"scripts/x.sh"* ]] -} - -@test "missing body file exits 3 with clear error" { - run "$SCRIPT" --body "$TMP/does-not-exist.md" - [ "$status" -eq 3 ] - [[ "$output" == *"cannot read body file"* ]] -} - -@test "no <pr-number> in pr-mode exits 2 (usage)" { - run "$SCRIPT" - [ "$status" -eq 2 ] - [[ "$output" == *"missing"* ]] || [[ "$output" == *"pr-number"* ]] || [[ "$output" == *"Usage"* ]] -} - -@test "extraction regex matches CI exactly (leading-space tolerance)" { - # CI uses `sed -n 's/^Evidence:[[:space:]]*//p'`. Lines starting with - # whitespace BEFORE "Evidence:" must NOT match — they're not trailer lines. - run_stdin " Evidence: should-not-be-matched -Evidence: scripts/real.sh" - [ "$status" -eq 0 ] - # Only one claim should have been classified. - [[ "$output" == *"1 Evidence:"* ]] - [[ "$output" == *"scripts/real.sh"* ]] - [[ "$output" != *"should-not-be-matched"* ]] -} - -@test "multiple blocking issues per body are all reported" { - run_stdin "Evidence: a (paren) -Evidence: b | pipe -Evidence: c" - [ "$status" -eq 1 ] - [[ "$output" == *"3 Evidence:"* ]] - [[ "$output" == *"2 blocking"* ]] -} diff --git a/tests/scripts/mortem_naming_contract.bats b/tests/scripts/mortem_naming_contract.bats old mode 100755 new mode 100644 index 904d831a6..65b8fa368 --- a/tests/scripts/mortem_naming_contract.bats +++ b/tests/scripts/mortem_naming_contract.bats @@ -2,348 +2,40 @@ setup() { REPO_ROOT="$(cd "$BATS_TEST_DIRNAME/../.." && pwd)" - DISPOSITIONS="$REPO_ROOT/docs/contracts/skill-dispositions.yaml" - OVERRIDES="$REPO_ROOT/skills-codex-overrides/catalog.json" - REGISTRY="$REPO_ROOT/registry.json" - TIERS="$REPO_ROOT/skills/SKILL-TIERS.md" - COMPAT_FIXTURES="$REPO_ROOT/tests/fixtures/mortem-compatibility" - DRIFT_FIXTURES="$REPO_ROOT/tests/fixtures/four-umbrella-wave-drift" } -copy_mortem_compatibility_fixtures() { - CORRUPT_FIXTURES="$BATS_TEST_TMPDIR/mortem-compatibility" - rm -rf "$CORRUPT_FIXTURES" - cp -R "$COMPAT_FIXTURES" "$CORRUPT_FIXTURES" -} - -run_checker_with_fixture_override() { - run env MORTEM_COMPAT_FIXTURES_DIR="$CORRUPT_FIXTURES" \ - "$REPO_ROOT/scripts/check-mortem-compatibility.sh" -} - -assert_historical_redirect() { - local source_slug="$1" - local target_slug="$2" - - awk -v source_slug="$source_slug" -v target_slug="$target_slug" ' - /^historical:/ { in_historical = 1; next } - in_historical && /^[^[:space:]]/ { exit 1 } - in_historical && $0 ~ "^ " source_slug ":$" { in_row = 1; next } - in_row && /^ [^[:space:]][^:]*:$/ { exit 1 } - in_row && /^[[:space:]]+state:[[:space:]]+merged-into([[:space:]]|$)/ { merged = 1 } - in_row && $0 ~ "^[[:space:]]+merged-into:[[:space:]]+" target_slug "([[:space:]]|$)" { target = 1 } - END { exit !(in_row && merged && target) } - ' "$DISPOSITIONS" -} - -assert_paths_exist() { - local relative - for relative in "$@"; do - if [[ ! -e "$REPO_ROOT/$relative" ]]; then - echo "missing required contract path: $relative" >&2 - return 1 - fi +@test "only canonical premortem and postmortem roots exist" { + for root in skills skills-codex images/gemini/skills; do + [ -f "$REPO_ROOT/$root/premortem/SKILL.md" ] + [ -f "$REPO_ROOT/$root/postmortem/SKILL.md" ] + for alias in pre-mortem pre_mortem post-mortem post_mortem; do + [ ! -e "$REPO_ROOT/$root/$alias" ] + done done } -assert_runtime_pointer() { - local relative="$1" - local legacy_slug="$2" - local target_slug="$3" - local invocation="$4" - local pointer="$REPO_ROOT/$relative" +@test "generated skill inventories contain one canonical mortem identity each" { + run python3 - "$REPO_ROOT" <<'PY' +import json +from pathlib import Path +import sys - [[ -f "$pointer" ]] - [[ "$(awk '/^name:/{print $2; exit}' "$pointer")" == "$legacy_slug" ]] - [[ "$(awk '/^redirect_to:/{print $2; exit}' "$pointer")" == "$target_slug" ]] - grep -Eq '^implementation:[[:space:]]+false$' "$pointer" - [[ "$(grep -Foc "$invocation" "$pointer")" -eq 1 ]] - [[ "$(wc -l <"$pointer" | tr -d ' ')" -le 16 ]] - [[ ! -d "$(dirname "$pointer")/references" ]] +root = Path(sys.argv[1]) +for relative, list_key in ( + ("skills/catalog.json", "skills"), + ("images/claude/manifest.json", "skills"), + ("images/codex/manifest.json", "skills"), +): + data = json.loads((root / relative).read_text()) + names = [row.get("name", row.get("slug")) for row in data[list_key]] + assert names.count("premortem") == 1, (relative, names.count("premortem")) + assert names.count("postmortem") == 1, (relative, names.count("postmortem")) + assert not set(names).intersection({"pre-mortem", "pre_mortem", "post-mortem", "post_mortem"}) +PY + [ "$status" -eq 0 ] } -@test "canonical mortem skills have tiny non-implementing runtime pointers for old explicit requests" { - assert_paths_exist \ - skills/premortem/SKILL.md \ - skills/postmortem/SKILL.md \ - skills-codex/premortem/SKILL.md \ - skills-codex/postmortem/SKILL.md - - # The disposition ledger is not a runtime alias mechanism. Claude and Codex - # discover skills by directory, so the installed bundles need redirect-only - # pointer skills. Each pointer invokes the canonical implementation once and - # carries no implementation/reference tree of its own. - assert_runtime_pointer skills/pre-mortem/SKILL.md pre-mortem premortem /premortem - assert_runtime_pointer skills/post-mortem/SKILL.md post-mortem postmortem /postmortem - assert_runtime_pointer skills/pre_mortem/SKILL.md pre_mortem premortem /premortem - assert_runtime_pointer skills/post_mortem/SKILL.md post_mortem postmortem /postmortem - assert_runtime_pointer skills-codex/pre-mortem/SKILL.md pre-mortem premortem '$premortem' - assert_runtime_pointer skills-codex/post-mortem/SKILL.md post-mortem postmortem '$postmortem' - assert_runtime_pointer skills-codex/pre_mortem/SKILL.md pre_mortem premortem '$premortem' - assert_runtime_pointer skills-codex/post_mortem/SKILL.md post_mortem postmortem '$postmortem' - [[ -f "$REPO_ROOT/skills-codex/pre-mortem/prompt.md" ]] - [[ -f "$REPO_ROOT/skills-codex/post-mortem/prompt.md" ]] - [[ -f "$REPO_ROOT/skills-codex/pre_mortem/prompt.md" ]] - [[ -f "$REPO_ROOT/skills-codex/post_mortem/prompt.md" ]] - - grep -Eq '^ - skill:[[:space:]]+premortem$' "$DISPOSITIONS" - grep -Eq '^ - skill:[[:space:]]+postmortem$' "$DISPOSITIONS" - grep -Eq '^[[:space:]]+path:[[:space:]]+skills/premortem/SKILL\.md$' "$DISPOSITIONS" - grep -Eq '^[[:space:]]+path:[[:space:]]+skills/postmortem/SKILL\.md$' "$DISPOSITIONS" - - jq -e '([.skills[].name] | index("premortem")) != null' "$OVERRIDES" - jq -e '([.skills[].name] | index("postmortem")) != null' "$OVERRIDES" - jq -e '([.skills[].name] | index("pre-mortem")) == null' "$OVERRIDES" - jq -e '([.skills[].name] | index("post-mortem")) == null' "$OVERRIDES" - ! grep -Eq 'skills(-codex)?/(pre-mortem|post-mortem)/SKILL\.md' "$OVERRIDES" -} - -@test "legacy skill requests permanently resolve once to canonical live skills" { - assert_historical_redirect pre-mortem premortem - assert_historical_redirect post-mortem postmortem - assert_historical_redirect pre_mortem premortem - assert_historical_redirect post_mortem postmortem - - run bash -c 'source "$1"; resolve_skill_path "$2"' _ \ - "$REPO_ROOT/scripts/lib/resolve-skill-path.sh" \ - "skills/pre-mortem/SKILL.md" - [[ "$status" -eq 0 ]] - [[ "$output" == "skills/premortem/SKILL.md" ]] - - run bash -c 'source "$1"; resolve_skill_path "$2"' _ \ - "$REPO_ROOT/scripts/lib/resolve-skill-path.sh" \ - "skills-codex/post-mortem/SKILL.md" - [[ "$status" -eq 0 ]] - [[ "$output" == "skills-codex/postmortem/SKILL.md" ]] - - run bash -c 'source "$1"; resolve_skill_path "$2"' _ \ - "$REPO_ROOT/scripts/lib/resolve-skill-path.sh" \ - "skills/pre_mortem/SKILL.md" - [[ "$status" -eq 0 ]] - [[ "$output" == "skills/premortem/SKILL.md" ]] - - run bash -c 'source "$1"; resolve_skill_path "$2"' _ \ - "$REPO_ROOT/scripts/lib/resolve-skill-path.sh" \ - "skills-codex/post_mortem/SKILL.md" - [[ "$status" -eq 0 ]] - [[ "$output" == "skills-codex/postmortem/SKILL.md" ]] - - run bash "$REPO_ROOT/scripts/check-skill-redirects.sh" - [[ "$status" -eq 0 ]] -} - -@test "mortem compatibility checker owns canonical packet and non-packet redirects" { - local checker="$REPO_ROOT/scripts/check-mortem-compatibility.sh" - [[ -x "$checker" ]] || { - echo "missing executable compatibility checker: scripts/check-mortem-compatibility.sh" >&2 - return 1 - } - - assert_paths_exist \ - tests/fixtures/mortem-compatibility/legacy-directory/pre-mortem-check.json \ - tests/fixtures/mortem-compatibility/directory-conflict/pre-mortem-check.json \ - tests/fixtures/mortem-compatibility/directory-conflict/premortem-check.json \ - tests/fixtures/mortem-compatibility/explicit-skill-redirect.yaml - - run "$checker" - [[ "$status" -eq 0 ]] -} - -@test "mortem compatibility checker executes production readers and writers" { - local checker="$REPO_ROOT/scripts/check-mortem-compatibility.sh" - - ! grep -q '^def normalize' "$checker" - ! grep -q 'writer-v1\.json' "$checker" - grep -q 'go test ./internal/domain/packet' "$checker" - grep -q 'go test ./cmd/ao' "$checker" - grep -q 'TestExecutionPacketPremortemContract' "$checker" - grep -q 'TestRepo_PremortemContract' "$checker" - grep -q 'TestPremortemDirectoryReader_' "$checker" -} - -@test "mortem compatibility checker rejects corrupt legacy-directory fixture bytes through production reader" { - copy_mortem_compatibility_fixtures - printf '{"id":"check-legacy","rule":"corrupted content"}\n' \ - >"$CORRUPT_FIXTURES/legacy-directory/pre-mortem-check.json" - - run_checker_with_fixture_override - [[ "$status" -ne 0 ]] - [[ "$output" == *"legacy-directory/pre-mortem-check.json"* ]] -} - -@test "mortem compatibility checker rejects a directory-conflict fixture made equal" { - copy_mortem_compatibility_fixtures - cp "$CORRUPT_FIXTURES/directory-conflict/premortem-check.json" \ - "$CORRUPT_FIXTURES/directory-conflict/pre-mortem-check.json" - - run_checker_with_fixture_override - [[ "$status" -ne 0 ]] - [[ "$output" == *"directory-conflict/pre-mortem-check.json"* ]] - [[ "$output" == *"directory-conflict/premortem-check.json"* ]] -} - -@test "mortem compatibility checker rejects corrupt redirect fixture bytes" { - copy_mortem_compatibility_fixtures - sed 's/merged-into: premortem/merged-into: missing-premortem/' \ - "$CORRUPT_FIXTURES/explicit-skill-redirect.yaml" \ - >"$CORRUPT_FIXTURES/explicit-skill-redirect.yaml.tmp" - mv "$CORRUPT_FIXTURES/explicit-skill-redirect.yaml.tmp" \ - "$CORRUPT_FIXTURES/explicit-skill-redirect.yaml" - - run_checker_with_fixture_override - [[ "$status" -ne 0 ]] - [[ "$output" == *"explicit-skill-redirect.yaml"* ]] - [[ "$output" == *"missing-premortem"* ]] -} - -@test "generated registry and tier surfaces contain only canonical live mortem identities" { - jq -e ' - ([.surfaces.skills[] | select(.name == "premortem" and .path == "skills/premortem/" and .has_skill_md == true)] | length) == 1 and - ([.surfaces.skills[] | select(.name == "postmortem" and .path == "skills/postmortem/" and .has_skill_md == true)] | length) == 1 and - ([.surfaces.skills[] | select( - .name == "pre-mortem" or .name == "post-mortem" or - .name == "pre_mortem" or .name == "post_mortem" or - .path == "skills/pre-mortem/" or .path == "skills/post-mortem/" or - .path == "skills/pre_mortem/" or .path == "skills/post_mortem/" - )] | length) == 0 - ' "$REGISTRY" - - grep -q '| \*\*premortem\*\* |' "$TIERS" - grep -q '| \*\*postmortem\*\* |' "$TIERS" - ! grep -q '| \*\*pre-mortem\*\* |' "$TIERS" - ! grep -q '| \*\*post-mortem\*\* |' "$TIERS" -} - -@test "runtime pointer skills are excluded from the canonical registry and active disposition list" { - jq -e ' - ([.surfaces.skills[] | select(.name == "premortem" and .has_skill_md == true)] | length) == 1 and - ([.surfaces.skills[] | select(.name == "postmortem" and .has_skill_md == true)] | length) == 1 and - ([.surfaces.skills[] | select(.name == "pre-mortem" or .name == "post-mortem" or .name == "pre_mortem" or .name == "post_mortem")] | length) == 0 and - ([.capabilities[] | select( - .sku == "skill:pre-mortem" or .sku == "skill:post-mortem" or - .sku == "skill:pre_mortem" or .sku == "skill:post_mortem" or - .name == "pre-mortem" or .name == "post-mortem" or - .name == "pre_mortem" or .name == "post_mortem" or - .path == "skills/pre-mortem/" or .path == "skills/post-mortem/" or - .path == "skills/pre_mortem/" or .path == "skills/post_mortem/" - )] | length) == 0 and - .capability_summary.skills == 63 and - .capability_summary.skills == (.surfaces.skills | length) and - .capability_summary.total == (.capabilities | length) and - .capability_summary.total == ( - .capability_summary.skills + - .capability_summary.cli_commands + - .capability_summary.gates + - .capability_summary.reference_impls - ) - ' "$REGISTRY" - - [[ "$(grep -Ec '^[[:space:]]+- skill:[[:space:]]+(pre-mortem|post-mortem|pre_mortem|post_mortem)$' "$DISPOSITIONS")" -eq 0 ]] -} - -@test "S1 drift guard rejects a committed out-of-manifest path" { - local sandbox="$BATS_TEST_TMPDIR/committed-range" - local remote="$BATS_TEST_TMPDIR/committed-range.git" - mkdir -p "$sandbox/scripts" "$sandbox/docs/contracts" "$sandbox/tests/fixtures" - git init --bare "$remote" >/dev/null - git -C "$sandbox" init -b main >/dev/null - git -C "$sandbox" config user.name "S1 drift test" - git -C "$sandbox" config user.email "s1-drift@example.invalid" - - cp "$REPO_ROOT/scripts/check-four-umbrella-wave-drift.sh" "$sandbox/scripts/" - cp "$REPO_ROOT/scripts/check-file-manifest-overlap.sh" "$sandbox/scripts/" - cp -R "$DRIFT_FIXTURES" "$sandbox/tests/fixtures/four-umbrella-wave-drift" - cat >"$sandbox/docs/contracts/four-umbrella-write-manifests.json" <<'JSON' -{ - "schema_version": 1, - "s1_frozen_base_sha": "0000000000000000000000000000000000000000", - "slices": {"S1": {"paths": ["skills/premortem/**"]}} -} -JSON - printf '.agents/\n' >"$sandbox/.gitignore" - git -C "$sandbox" add . - git -C "$sandbox" commit -m "base" >/dev/null - git -C "$sandbox" remote add origin "$remote" - git -C "$sandbox" push -u origin main >/dev/null - - local base digest - base="$(git -C "$sandbox" rev-parse HEAD)" - digest="$(sha256sum "$sandbox/docs/contracts/four-umbrella-write-manifests.json" | awk '{print $1}')" - mkdir -p "$sandbox/.agents/evidence/four-umbrella" - printf '{"schema_version":1,"slice":"S1","base_sha":"%s","manifest_sha256":"%s"}\n' \ - "$base" "$digest" >"$sandbox/.agents/evidence/four-umbrella/s1-base.json" - - git -C "$sandbox" switch -c feature >/dev/null - printf 'committed but outside S1\n' >"$sandbox/README.md" - git -C "$sandbox" add README.md - git -C "$sandbox" commit -m "out of manifest" >/dev/null - - run bash -c 'cd "$1" && bash scripts/check-four-umbrella-wave-drift.sh --phase=verify S1' _ "$sandbox" - [[ "$status" -ne 0 ]] - [[ "$output" == *"out-of-manifest committed changes: README.md"* ]] -} - -@test "S1 wave drift guard owns clean dirty missing overlap and out-of-manifest fixtures" { - local checker="$REPO_ROOT/scripts/check-four-umbrella-wave-drift.sh" - [[ -x "$checker" ]] || { - echo "missing executable wave drift checker: scripts/check-four-umbrella-wave-drift.sh" >&2 - return 1 - } - - assert_paths_exist \ - docs/contracts/four-umbrella-write-manifests.json \ - tests/fixtures/four-umbrella-wave-drift/clean-s1.json \ - tests/fixtures/four-umbrella-wave-drift/dirty-pre-work.json \ - tests/fixtures/four-umbrella-wave-drift/missing-manifest.json \ - tests/fixtures/four-umbrella-wave-drift/upstream-overlap.json \ - tests/fixtures/four-umbrella-wave-drift/out-of-manifest.json - - # The happy-path verify must run in a HERMETIC sandbox, not against the live - # repo. The S1 wave's frozen base is now hundreds of commits behind HEAD (the - # wave is long complete), so `verify S1` on the live tree always reports - # out-of-manifest drift, and CI's shallow (fetch-depth: 2) checkout cannot even - # resolve the frozen base object ("slice base SHA is unavailable"). A repo where - # HEAD == base == origin/main with a clean tree exercises the SAME checker path - # — manifest + receipt validation, fixture ownership, empty base..HEAD diff, - # empty upstream overlap — and legitimately passes (exit 0). The out-of-manifest - # REJECT direction is covered by the sibling "S1 drift guard rejects a committed - # out-of-manifest path" test. - local sandbox="$BATS_TEST_TMPDIR/clean-verify" - local remote="$BATS_TEST_TMPDIR/clean-verify.git" - mkdir -p "$sandbox/scripts" "$sandbox/docs/contracts" "$sandbox/tests/fixtures" - git init --bare "$remote" >/dev/null - git -C "$sandbox" init -b main >/dev/null - git -C "$sandbox" config user.name "S1 verify test" - git -C "$sandbox" config user.email "s1-verify@example.invalid" - - cp "$REPO_ROOT/scripts/check-four-umbrella-wave-drift.sh" "$sandbox/scripts/" - cp "$REPO_ROOT/scripts/check-file-manifest-overlap.sh" "$sandbox/scripts/" - cp -R "$DRIFT_FIXTURES" "$sandbox/tests/fixtures/four-umbrella-wave-drift" - cat >"$sandbox/docs/contracts/four-umbrella-write-manifests.json" <<'JSON' -{ - "schema_version": 1, - "s1_frozen_base_sha": "0000000000000000000000000000000000000000", - "slices": {"S1": {"paths": ["skills/premortem/**"]}} -} -JSON - printf '.agents/\n' >"$sandbox/.gitignore" - git -C "$sandbox" add . - git -C "$sandbox" commit -m "base" >/dev/null - git -C "$sandbox" remote add origin "$remote" - git -C "$sandbox" push -u origin main >/dev/null - - local base digest - base="$(git -C "$sandbox" rev-parse HEAD)" - # Digest with the SAME algorithm the checker uses (python hashlib sha256 of the - # manifest bytes), so the receipt validates and this stays portable off Linux. - digest="$(python3 -c 'import hashlib,sys;print(hashlib.sha256(open(sys.argv[1],"rb").read()).hexdigest())' \ - "$sandbox/docs/contracts/four-umbrella-write-manifests.json")" - mkdir -p "$sandbox/.agents/evidence/four-umbrella" - printf '{"schema_version":1,"slice":"S1","base_sha":"%s","manifest_sha256":"%s"}\n' \ - "$base" "$digest" >"$sandbox/.agents/evidence/four-umbrella/s1-base.json" - - # HEAD == base == origin/main, tree clean, no out-of-manifest changes -> PASS. - run bash -c 'cd "$1" && bash scripts/check-four-umbrella-wave-drift.sh --phase=verify S1' _ "$sandbox" - [[ "$status" -eq 0 ]] - [[ "$output" == *"four-umbrella wave drift: PASS"* ]] +@test "metadata-derived mesh is current" { + run python3 "$REPO_ROOT/scripts/generate-skill-mesh.py" --check + [ "$status" -eq 0 ] } diff --git a/tests/scripts/one-leaf-pull-flow.bats b/tests/scripts/one-leaf-pull-flow.bats deleted file mode 100644 index 95745cc0d..000000000 --- a/tests/scripts/one-leaf-pull-flow.bats +++ /dev/null @@ -1,133 +0,0 @@ -#!/usr/bin/env bats - -setup() { - REPO_ROOT="$(cd "$BATS_TEST_DIRNAME/../.." && pwd)" - VALIDATOR="$REPO_ROOT/skills/rpi/scripts/validate-execution-packet.py" - PACKET="$BATS_TEST_TMPDIR/execution-packet.json" - mkdir -p "$REPO_ROOT/.agents/rpi" - DISCOVERY_ARTIFACT="$REPO_ROOT/.agents/rpi/s4-discovery-$BATS_TEST_NUMBER-$$.md" - printf 'discovery evidence\n' >"$DISCOVERY_ARTIFACT" -} - -teardown() { - rm -f "$DISCOVERY_ARTIFACT" -} - -write_prospective_packet() { - jq -n --arg artifact ".agents/rpi/$(basename "$DISCOVERY_ARTIFACT")" '{ - schema_version: 3, - packet_state: "prospective", - objective: "pull one bounded leaf", - skills_loaded: [ - {name: "rpi", reason: "orchestrator"}, - {name: "discovery", reason: "phase-1"} - ], - phase_receipts: [ - {phase: "discovery", skill: "discovery", status: "DONE", artifact: $artifact}, - {phase: "crank", skill: "crank", status: "pending"}, - {phase: "validate", skill: "validate", status: "not_checked"}, - {phase: "learn", skill: "learn", status: "not_checked"} - ] - }' >"$PACKET" -} - -@test "goal parents remain aggregate demand while one writer owns one active leaf" { - run rg -n 'Goal and epic parents are aggregate demand, never writer WIP' \ - "$REPO_ROOT/skills/plan/SKILL.md" \ - "$REPO_ROOT/skills/behavior-first-planning/SKILL.md" - [ "$status" -eq 0 ] - [ "$(grep -c 'Goal and epic parents are aggregate demand, never writer WIP' <<<"$output")" -eq 2 ] - - run rg -n 'one active leaf per writer' \ - "$REPO_ROOT/skills/plan/SKILL.md" \ - "$REPO_ROOT/skills/discovery/SKILL.md" \ - "$REPO_ROOT/skills/rpi/SKILL.md" - [ "$status" -eq 0 ] - [ "$(grep -c 'one active leaf per writer' <<<"$output")" -eq 3 ] -} - -@test "Discovery can hand off an honest prospective packet with pending phases" { - write_prospective_packet - - run python3 "$VALIDATOR" "$PACKET" - - [ "$status" -eq 0 ] - [[ "$output" == *"valid prospective execution packet"* ]] -} - -@test "a terminal packet cannot reuse pending or not_checked phase state" { - write_prospective_packet - jq ' - .packet_state = "terminal" - | .skills_loaded += [ - {name: "crank", reason: "phase-2"}, - {name: "validate", reason: "phase-3"}, - {name: "learn", reason: "phase-4"} - ] - ' "$PACKET" >"$PACKET.tmp" - mv "$PACKET.tmp" "$PACKET" - - run python3 "$VALIDATOR" "$PACKET" - - [ "$status" -eq 1 ] - [[ "$output" == *"terminal phase_receipts"* ]] -} - -@test "a prospective packet cannot claim an unrun phase skill as loaded" { - write_prospective_packet - jq '.skills_loaded += [{name: "crank", reason: "future-phase"}]' \ - "$PACKET" >"$PACKET.tmp" - mv "$PACKET.tmp" "$PACKET" - - run python3 "$VALIDATOR" "$PACKET" - - [ "$status" -eq 1 ] - [[ "$output" == *"prospective skills_loaded must omit unrun phase skill: crank"* ]] -} - -@test "a prospective packet cannot fabricate downstream success" { - write_prospective_packet - jq '(.phase_receipts[] | select(.phase != "discovery") | .status) = "DONE"' \ - "$PACKET" >"$PACKET.tmp" - mv "$PACKET.tmp" "$PACKET" - - run python3 "$VALIDATOR" "$PACKET" - - [ "$status" -eq 1 ] - [[ "$output" == *"prospective phase_receipts"* ]] -} - -@test "each admitted wave with remaining work requires one bounded Premortem" { - run rg -n 'Every admitted Crank wave with remaining work must end with exactly one bounded Premortem' \ - "$REPO_ROOT/skills/rpi/SKILL.md" \ - "$REPO_ROOT/skills/premortem/SKILL.md" - [ "$status" -eq 0 ] - [ "$(grep -c 'Every admitted Crank wave with remaining work must end with exactly one bounded Premortem' <<<"$output")" -eq 2 ] -} - -@test "a second distinct repair need routes to REPLAN without a Discovery-local controller" { - run rg -n 'second distinct repair need.*REPLAN' \ - "$REPO_ROOT/skills/discovery/SKILL.md" \ - "$REPO_ROOT/skills/rpi/SKILL.md" - [ "$status" -eq 0 ] - [ "$(grep -c 'second distinct repair need.*REPLAN' <<<"$output")" -eq 2 ] - - [ ! -e "$REPO_ROOT/skills/discovery/scripts/mvp-helper-state.sh" ] - [ ! -e "$REPO_ROOT/skills/discovery/scripts/validate-contract-fixtures.sh" ] - run rg -n 'up to 3 total attempts|discovery_mvp_helper|mvp-helper-state' \ - "$REPO_ROOT/skills/discovery" - [ "$status" -eq 1 ] -} - -@test "candidate-attributed Gemini runtime consumers match source policy" { - for skill in plan discovery premortem; do - run cmp -s \ - "$REPO_ROOT/skills/$skill/SKILL.md" \ - "$REPO_ROOT/images/gemini/skills/$skill/SKILL.md" - [ "$status" -eq 0 ] - done - - run rg -n 'three MVP premortem failures|ordinary MVP breaker|bounded helper' \ - "$REPO_ROOT/images/gemini/skills/discovery/SKILL.md" - [ "$status" -eq 1 ] -} diff --git a/tests/scripts/pre-push-local.bats b/tests/scripts/pre-push-local.bats deleted file mode 100644 index 7e40ba965..000000000 --- a/tests/scripts/pre-push-local.bats +++ /dev/null @@ -1,213 +0,0 @@ -#!/usr/bin/env bats - -setup() { - ROOT="$(cd "$BATS_TEST_DIRNAME/../.." && pwd)" - SCRIPT="$ROOT/scripts/hooks/pre-push.local" -} - -@test "pre-push.local full race gate uses randomized shuffle" { - [ -x "$SCRIPT" ] - run grep -q 'go test ./... -race -shuffle=on -count=1' "$SCRIPT" - [ "$status" -eq 0 ] - - run grep -q 'go test ./... -race -shuffle=1 -count=1' "$SCRIPT" - [ "$status" -eq 1 ] -} - -@test "pre-push.local scrubs git hook discovery env before full race gate" { - run grep -q 'unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_PREFIX GIT_OBJECT_DIRECTORY GIT_COMMON_DIR GIT_NAMESPACE' "$SCRIPT" - [ "$status" -eq 0 ] -} - -@test "pre-push.local builds a per-run ao gate binary" { - run grep -q 'mktemp .*ao-gate' "$SCRIPT" - [ "$status" -eq 0 ] - - run grep -q 'go build -o /tmp/ao-gate ./cmd/ao' "$SCRIPT" - [ "$status" -eq 1 ] -} - -@test "pre-push.local runs full race before serial mutable lock" { - race_line="$(grep -n 'go test ./... -race -shuffle=on -count=1' "$SCRIPT" | tail -1 | cut -d: -f1)" - lock_line="$(grep -n '^acquire_push_lock$' "$SCRIPT" | tail -1 | cut -d: -f1)" - - [ -n "$race_line" ] - [ -n "$lock_line" ] - [ "$race_line" -lt "$lock_line" ] -} - -@test "pre-push.local runs cmd/ao integration shard before serial mutable lock" { - run grep -Fq 'go test ./cmd/ao -tags=integration -run "$cmdao_integration_tests" -race -shuffle=on -count=1' "$SCRIPT" - [ "$status" -eq 0 ] - - shard_line="$(grep -Fn 'go test ./cmd/ao -tags=integration' "$SCRIPT" | tail -1 | cut -d: -f1)" - lock_line="$(grep -n '^acquire_push_lock$' "$SCRIPT" | tail -1 | cut -d: -f1)" - - [ -n "$shard_line" ] - [ -n "$lock_line" ] - [ "$shard_line" -lt "$lock_line" ] -} - -@test "pre-push.local makes post-land provenance opt-in inside pre-push" { - run grep -q 'AGENTOPS_PROVENANCE_EMIT_POST_LAND:-0' "$SCRIPT" - [ "$status" -eq 0 ] -} - -@test "pre-push.local default path does not mutate head or provenance ledger" { - repo="$BATS_TEST_TMPDIR/repo" - stubbin="$BATS_TEST_TMPDIR/bin" - tmpdir="$BATS_TEST_TMPDIR/tmp" - log="$BATS_TEST_TMPDIR/hook.log" - mkdir -p "$repo/cli" "$repo/scripts" "$repo/docs/provenance" "$stubbin" "$tmpdir" - - cat >"$stubbin/go" <<'EOS' -#!/usr/bin/env bash -set -euo pipefail -if [[ "${1:-}" == "build" && "${2:-}" == "-o" ]]; then - out="$3" - cat >"$out" <<'AO' -#!/usr/bin/env bash -echo "ao:$*" >> "$AGENTOPS_TEST_LOG" -exit 0 -AO - chmod +x "$out" - exit 0 -fi -if [[ "${1:-}" == "build" ]]; then - exit 0 -fi -if [[ "${1:-}" == "test" ]]; then - echo "go:$*" >> "$AGENTOPS_TEST_LOG" - exit 0 -fi -exit 0 -EOS - chmod +x "$stubbin/go" - - cat >"$repo/scripts/post-land-provenance-emit.sh" <<'EOS' -#!/usr/bin/env bash -set -euo pipefail -echo "post-land" >> "$AGENTOPS_TEST_LOG" -echo "ledger-row" >> docs/provenance/ledger.jsonl -git add docs/provenance/ledger.jsonl -git commit -m "post-land mutator" >/dev/null 2>&1 -EOS - chmod +x "$repo/scripts/post-land-provenance-emit.sh" - - cat >"$repo/scripts/check-pawl-pre-push.sh" <<'EOS' -#!/usr/bin/env bash -echo "pawl" >> "$AGENTOPS_TEST_LOG" -exit 0 -EOS - chmod +x "$repo/scripts/check-pawl-pre-push.sh" - - # age-yy24 added a hook call to scripts/verify-pushed-commit-builds.sh when - # stdin carries push refs (this test feeds them), resolved under $toplevel — - # i.e. THIS sandbox repo. Without a stub the hook hit "No such file" -> exit - # 127 (the build-each-pushed-commit verify has its own coverage; here it must - # be an inert no-op that drains stdin and never mutates the repo). - cat >"$repo/scripts/verify-pushed-commit-builds.sh" <<'EOS' -#!/usr/bin/env bash -cat >/dev/null # drain the piped push-ref stdin -echo "verify-commit-builds" >> "$AGENTOPS_TEST_LOG" -exit 0 -EOS - chmod +x "$repo/scripts/verify-pushed-commit-builds.sh" - - touch "$repo/docs/provenance/ledger.jsonl" - git -C "$repo" init -q - git -C "$repo" config user.email test@example.com - git -C "$repo" config user.name Test - git -C "$repo" add docs/provenance/ledger.jsonl - git -C "$repo" commit -m initial >/dev/null - head_before="$(git -C "$repo" rev-parse HEAD)" - ledger_before="$(git -C "$repo" hash-object docs/provenance/ledger.jsonl)" - - run env PATH="$stubbin:$PATH" \ - TMPDIR="$tmpdir" \ - AGENTOPS_TEST_LOG="$log" \ - AGENTOPS_PREPUSH_SKIP_FULL_RACE=1 \ - sh -c 'cd "$1" && printf "%s %s %s %s\n" refs/heads/main "$2" refs/heads/main 0000000000000000000000000000000000000000 | "$3"' \ - _ "$repo" "$head_before" "$SCRIPT" - [ "$status" -eq 0 ] - - [ "$(git -C "$repo" rev-parse HEAD)" = "$head_before" ] - [ "$(git -C "$repo" hash-object docs/provenance/ledger.jsonl)" = "$ledger_before" ] - git -C "$repo" diff --quiet - git -C "$repo" diff --cached --quiet - run grep -q '^post-land$' "$log" - [ "$status" -eq 1 ] - run grep -q '^ao:gate check --fast$' "$log" - [ "$status" -eq 0 ] - run grep -q '^pawl$' "$log" - [ "$status" -eq 0 ] -} - -@test "pre-push.local persists race-suite output + surfaces seed/package on a race failure" { - repo="$BATS_TEST_TMPDIR/rrepo" - stubbin="$BATS_TEST_TMPDIR/rbin" - tmpdir="$BATS_TEST_TMPDIR/rtmp" - log="$BATS_TEST_TMPDIR/rhook.log" - mkdir -p "$repo/cli" "$repo/scripts" "$repo/docs/provenance" "$stubbin" "$tmpdir" - - # Stub go: builds succeed; the FULL-SUITE race invocation FAILS, emitting a - # realistic shuffle seed + FAIL package (simulating an order-dependent - # isolation flake). The hook must capture this to a log, not lose it. - cat >"$stubbin/go" <<'EOS' -#!/usr/bin/env bash -set -euo pipefail -if [[ "${1:-}" == "build" && "${2:-}" == "-o" ]]; then - out="$3" - cat >"$out" <<'AO' -#!/usr/bin/env bash -echo "ao:$*" >> "$AGENTOPS_TEST_LOG" -exit 0 -AO - chmod +x "$out" - exit 0 -fi -if [[ "${1:-}" == "build" ]]; then exit 0; fi -if [[ "${1:-}" == "test" ]]; then - if [[ "$*" == *"./..."* && "$*" == *"-race"* ]]; then - echo "-test.shuffle 1782334455" - echo "--- FAIL: TestLeak (0.01s)" - echo "FAIL github.com/x/internal/leakpkg 1.2s" - exit 1 - fi - exit 0 -fi -exit 0 -EOS - chmod +x "$stubbin/go" - - printf '#!/usr/bin/env bash\ncat >/dev/null\nexit 0\n' > "$repo/scripts/verify-pushed-commit-builds.sh" - chmod +x "$repo/scripts/verify-pushed-commit-builds.sh" - printf '#!/usr/bin/env bash\nexit 0\n' > "$repo/scripts/check-pawl-pre-push.sh" - chmod +x "$repo/scripts/check-pawl-pre-push.sh" - - touch "$repo/docs/provenance/ledger.jsonl" - git -C "$repo" init -q - git -C "$repo" config user.email test@example.com - git -C "$repo" config user.name Test - git -C "$repo" add docs/provenance/ledger.jsonl - git -C "$repo" commit -m initial >/dev/null - head_before="$(git -C "$repo" rev-parse HEAD)" - - # No AGENTOPS_PREPUSH_SKIP_FULL_RACE: the race gate runs (and fails). - run env PATH="$stubbin:$PATH" \ - TMPDIR="$tmpdir" \ - AGENTOPS_TEST_LOG="$log" \ - sh -c 'cd "$1" && printf "%s %s %s %s\n" refs/heads/main "$2" refs/heads/main 0000000000000000000000000000000000000000 | "$3"' \ - _ "$repo" "$head_before" "$SCRIPT" - - # Push refused, with the seed + failing package surfaced (not lost to scroll). - [ "$status" -ne 0 ] - [[ "$output" == *"FULL race suite FAILED"* ]] - [[ "$output" == *"saved for repro"* ]] - [[ "$output" == *"-test.shuffle 1782334455"* ]] - [[ "$output" == *"github.com/x/internal/leakpkg"* ]] - - # And the full output was actually persisted to a log under TMPDIR. - run bash -c "ls '$tmpdir'/agentops-prepush-race-*.log 2>/dev/null | wc -l | tr -d ' '" - [ "$output" -ge 1 ] -} diff --git a/tests/scripts/resolve-skill-path.bats b/tests/scripts/resolve-skill-path.bats deleted file mode 100644 index 3b3b2bb60..000000000 --- a/tests/scripts/resolve-skill-path.bats +++ /dev/null @@ -1,251 +0,0 @@ -#!/usr/bin/env bats -# Tests for scripts/lib/resolve-skill-path.sh (ag-2vz5v). -# -# resolve_skill_path <path> routes skill paths through the historical: -# section of docs/contracts/skill-dispositions.yaml so validators follow -# ledger folds (merged-into) and cuts instead of hardcoding paths. -# Hermetic: fixture ledgers in temp dirs via the SKILL_DISPOSITIONS_FILE -# env seam; no repo mutation. - -setup() { - REPO_ROOT="$(cd "$BATS_TEST_DIRNAME/../.." && pwd)" - LIB="$REPO_ROOT/scripts/lib/resolve-skill-path.sh" - TMP_DIR="$(mktemp -d)" - LEDGER="$TMP_DIR/skill-dispositions.yaml" - cat > "$LEDGER" <<'EOF' -# fixture ledger — mirrors the real flat shape (historical: above dispositions:) -historical: - alpha: - state: merged-into - merged-into: beta - date: 2026-06-12 - rationale: "fixture: folded into beta" - gone: - state: cut - date: 2026-06-12 - rationale: "fixture: cut" - plan: - state: merged-into - merged-into: discovery - date: 2026-06-12 - rationale: "fixture: exact-match guard (plan vs plan-foundry)" - -dispositions: - - skill: beta - domain: "BC2 Skills" -EOF - EMPTY_LEDGER="$TMP_DIR/empty-ledger.yaml" - cat > "$EMPTY_LEDGER" <<'EOF' -historical: - -dispositions: - - skill: beta - domain: "BC2 Skills" -EOF -} - -teardown() { - rm -rf "$TMP_DIR" -} - -resolve() { - # resolve <ledger> <path> — run resolve_skill_path in a fresh bash. - SKILL_DISPOSITIONS_FILE="$1" bash -c \ - 'source "$1" && resolve_skill_path "$2"' _ "$LIB" "$2" -} - -# --- resolver units ------------------------------------------------------- - -@test "resolver lib exists and is sourceable" { - [ -f "$LIB" ] - run bash -c 'source "$1" && type resolve_skill_path' _ "$LIB" - [ "$status" -eq 0 ] -} - -@test "merged-into rewrites the skills/ slug segment" { - run resolve "$LEDGER" "skills/alpha/SKILL.md" - [ "$status" -eq 0 ] - [ "$output" = "skills/beta/SKILL.md" ] -} - -@test "merged-into rewrites the skills-codex/ slug segment" { - run resolve "$LEDGER" "skills-codex/alpha/references/notes.md" - [ "$status" -eq 0 ] - [ "$output" = "skills-codex/beta/references/notes.md" ] -} - -@test "cut prints nothing, returns 0" { - stdout="$(resolve "$LEDGER" "skills/gone/SKILL.md" 2>/dev/null)" - [ -z "$stdout" ] - run resolve "$LEDGER" "skills/gone/SKILL.md" - [ "$status" -eq 0 ] -} - -@test "cut warns on stderr naming the slug and 'cut'" { - stderr="$(resolve "$LEDGER" "skills/gone/SKILL.md" 2>&1 >/dev/null)" - [[ "$stderr" == *"gone"* ]] - [[ "$stderr" == *"cut"* ]] -} - -@test "slug with no historical row is byte-identical" { - run resolve "$LEDGER" "skills/heal-skill/scripts/heal.sh" - [ "$status" -eq 0 ] - [ "$output" = "skills/heal-skill/scripts/heal.sh" ] -} - -@test "exact slug match only: plan row does not rewrite plan-foundry" { - run resolve "$LEDGER" "skills/plan-foundry/SKILL.md" - [ "$status" -eq 0 ] - [ "$output" = "skills/plan-foundry/SKILL.md" ] - # ...while the exact slug IS rewritten - run resolve "$LEDGER" "skills/plan/SKILL.md" - [ "$status" -eq 0 ] - [ "$output" = "skills/discovery/SKILL.md" ] -} - -@test "non-skill paths pass through unchanged" { - run resolve "$LEDGER" "AGENTS.md" - [ "$status" -eq 0 ] - [ "$output" = "AGENTS.md" ] - run resolve "$LEDGER" "skills-codex-overrides/catalog.json" - [ "$status" -eq 0 ] - [ "$output" = "skills-codex-overrides/catalog.json" ] -} - -@test "trailing-prefix path (skills/<slug>/dir/) resolves the slug segment" { - run resolve "$LEDGER" "skills/alpha/references/" - [ "$status" -eq 0 ] - [ "$output" = "skills/beta/references/" ] -} - -@test "missing ledger file degrades to identity" { - run resolve "$TMP_DIR/does-not-exist.yaml" "skills/alpha/SKILL.md" - [ "$status" -eq 0 ] - [ "$output" = "skills/alpha/SKILL.md" ] -} - -@test "empty historical section is identity" { - run resolve "$EMPTY_LEDGER" "skills/alpha/SKILL.md" - [ "$status" -eq 0 ] - [ "$output" = "skills/alpha/SKILL.md" ] -} - -@test "dispositions rows never leak into historical lookup" { - # 'beta' exists only under dispositions:, not historical: — identity. - run resolve "$LEDGER" "skills/beta/SKILL.md" - [ "$status" -eq 0 ] - [ "$output" = "skills/beta/SKILL.md" ] -} - -# --- validator integration: validate-codex-rpi-contract.sh ---------------- - -setup_rpi_fake_repo() { - FAKE_REPO="$TMP_DIR/rpi-repo" - mkdir -p "$FAKE_REPO/scripts/lib" "$FAKE_REPO/docs/contracts" - /bin/cp "$REPO_ROOT/scripts/validate-codex-rpi-contract.sh" "$FAKE_REPO/scripts/" - /bin/cp "$LIB" "$FAKE_REPO/scripts/lib/" - chmod +x "$FAKE_REPO/scripts/validate-codex-rpi-contract.sh" - mkdir -p "$FAKE_REPO/skills-codex" "$FAKE_REPO/skills-codex-overrides/research" - # Padding dirs for the fake repo — the fold under test is on `rpi`. Copy every - # slug the four-umbrella contract validator now touches so the fold is the ONLY - # variable between the control (fails) and retarget (passes) cases. The - # contract grew past rpi+crank (learn/evolve/premortem checks + the rpi Python - # sub-validator) — the fixture tracks it. `research` is a currently-present - # codex skill + override; the rpi/crank/evolve/premortem overrides stay absent - # (require_absent), which the real repo already satisfies. - for slug in rpi crank learn evolve premortem discovery validate research; do - /bin/cp -R "$REPO_ROOT/skills-codex/$slug" "$FAKE_REPO/skills-codex/$slug" - done - /bin/cp "$REPO_ROOT/skills-codex-overrides/research/prompt.md" \ - "$FAKE_REPO/skills-codex-overrides/research/prompt.md" - # rpi's validate-execution-packet.py resolves the repo root by walking up for - # schemas/execution-packet.schema.json — provide it at the fake-repo root. - mkdir -p "$FAKE_REPO/schemas" - /bin/cp "$REPO_ROOT/schemas/execution-packet.schema.json" "$FAKE_REPO/schemas/" - # Simulate a fold: rpi's dir moved to its merge target, path now absent. - mv "$FAKE_REPO/skills-codex/rpi" "$FAKE_REPO/skills-codex/rpi-target" - RPI_LEDGER="$TMP_DIR/rpi-ledger.yaml" - cat > "$RPI_LEDGER" <<'EOF' -historical: - rpi: - state: merged-into - merged-into: rpi-target - date: 2026-06-12 - rationale: "fixture: integration fold" - -dispositions: -EOF -} - -@test "rpi-contract validator fails when a listed skill dir is absent (control)" { - setup_rpi_fake_repo - run env SKILL_DISPOSITIONS_FILE="$EMPTY_LEDGER" \ - bash "$FAKE_REPO/scripts/validate-codex-rpi-contract.sh" - [ "$status" -ne 0 ] -} - -@test "rpi-contract validator passes when the ledger retargets the absent slug" { - setup_rpi_fake_repo - run env SKILL_DISPOSITIONS_FILE="$RPI_LEDGER" \ - bash "$FAKE_REPO/scripts/validate-codex-rpi-contract.sh" - [ "$status" -eq 0 ] - [[ "$output" == *"passed"* ]] -} - -# --- validator integration: check-hookless-cold-start.sh ------------------ - -setup_coldstart_fake_repo() { - FAKE_REPO="$TMP_DIR/coldstart-repo" - mkdir -p "$FAKE_REPO/scripts/lib" "$FAKE_REPO/skills/status2" - /bin/cp "$REPO_ROOT/scripts/check-hookless-cold-start.sh" "$FAKE_REPO/scripts/" - /bin/cp "$LIB" "$FAKE_REPO/scripts/lib/" - chmod +x "$FAKE_REPO/scripts/check-hookless-cold-start.sh" - # Clean non-skill surface so 'scanned' stays > 0 in every case. - echo "Run ao session bootstrap explicitly." > "$FAKE_REPO/AGENTS.md" - COLD_LEDGER="$TMP_DIR/coldstart-ledger.yaml" - cat > "$COLD_LEDGER" <<'EOF' -historical: - status: - state: merged-into - merged-into: status2 - date: 2026-06-12 - rationale: "fixture: integration fold" - -dispositions: -EOF -} - -@test "cold-start validator scans the ledger-retargeted file" { - setup_coldstart_fake_repo - # Violation lives in the retarget; only the ledger can route the scan there. - cat > "$FAKE_REPO/skills/status2/SKILL.md" <<'EOF' -The SessionStart hook loads repo context for every worker. -EOF - run env SKILL_DISPOSITIONS_FILE="$COLD_LEDGER" \ - bash "$FAKE_REPO/scripts/check-hookless-cold-start.sh" - [ "$status" -ne 0 ] - [[ "$output" == *"skills/status2/SKILL.md"* ]] -} - -@test "cold-start validator skips cut slugs visibly and still passes" { - setup_coldstart_fake_repo - cat > "$TMP_DIR/coldstart-cut-ledger.yaml" <<'EOF' -historical: - status: - state: cut - date: 2026-06-12 - rationale: "fixture: cut" - -dispositions: -EOF - # A violation in the CUT slug's old path must not be scanned. - mkdir -p "$FAKE_REPO/skills/status" - cat > "$FAKE_REPO/skills/status/SKILL.md" <<'EOF' -The SessionStart hook loads repo context for every worker. -EOF - run env SKILL_DISPOSITIONS_FILE="$TMP_DIR/coldstart-cut-ledger.yaml" \ - bash "$FAKE_REPO/scripts/check-hookless-cold-start.sh" - [ "$status" -eq 0 ] - [[ "$output" == *"PASS"* ]] - [[ "$output" == *"cut"* ]] -} diff --git a/tests/scripts/rpi-run-disposition.bats b/tests/scripts/rpi-run-disposition.bats deleted file mode 100644 index cddd5d763..000000000 --- a/tests/scripts/rpi-run-disposition.bats +++ /dev/null @@ -1,174 +0,0 @@ -#!/usr/bin/env bats - -setup() { - REPO_ROOT="$(cd "$BATS_TEST_DIRNAME/../.." && pwd)" - SCHEMA="$REPO_ROOT/skills/rpi/schemas/run-disposition.schema.json" - FIXTURE="$BATS_TEST_TMPDIR/run-disposition.json" - CHECKPOINT="$BATS_TEST_TMPDIR/wave-checkpoint.json" - - printf '%s\n' '{ - "schema_version": 1, - "run_id": "rpi-fixture", - "objective": { - "identity": "age-fixture.1", - "digest": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" - }, - "disposition": "REPAIR", - "reason": "introduced acceptance defect", - "evidence_refs": [ - { - "path": ".agents/evidence/fixture.json", - "sha256": "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789" - } - ], - "blocker_class": "acceptance", - "recorded_at": "2026-07-14T12:00:00Z" - }' > "$FIXTURE" - - jq -n --arg sha "$(git -C "$REPO_ROOT" rev-parse HEAD)" '{ - schema_version: 1, - wave: 1, - timestamp: "2026-07-14T12:00:00Z", - tasks_completed: ["age-fixture.1"], - tasks_failed: [], - files_changed: ["skills/crank/references/plan-mutations.md"], - git_sha: $sha, - acceptance_verdict: "PASS", - commit_strategy: "lead-only", - mutations_this_wave: 1, - total_mutations: 1 - }' > "$CHECKPOINT" -} - -validate_fixture() { - python3 - "$SCHEMA" "$FIXTURE" <<'PY' -import json -import pathlib -import sys - -import jsonschema - -schema = json.loads(pathlib.Path(sys.argv[1]).read_text()) -instance = json.loads(pathlib.Path(sys.argv[2]).read_text()) -jsonschema.validate(instance=instance, schema=schema) -PY -} - -@test "run disposition is one closed evidence-bound record" { - run validate_fixture - [ "$status" -eq 0 ] -} - -@test "only NOTE REPAIR REPLAN HOLD and ANDON are dispositions" { - local disposition - for disposition in NOTE REPAIR REPLAN HOLD ANDON; do - jq --arg disposition "$disposition" '.disposition = $disposition' "$FIXTURE" > "$FIXTURE.next" - mv "$FIXTURE.next" "$FIXTURE" - run validate_fixture - [ "$status" -eq 0 ] - done - - jq '.disposition = "PARTIAL"' "$FIXTURE" > "$FIXTURE.next" - mv "$FIXTURE.next" "$FIXTURE" - run validate_fixture - [ "$status" -ne 0 ] -} - -@test "controller and helper state cannot enter a disposition record" { - local field - for field in limits usage authorized admissions charges helper_history; do - jq --arg field "$field" '.[$field] = {}' "$FIXTURE" > "$FIXTURE.next" - mv "$FIXTURE.next" "$FIXTURE" - run validate_fixture - [ "$status" -ne 0 ] - jq --arg field "$field" 'del(.[$field])' "$FIXTURE" > "$FIXTURE.next" - mv "$FIXTURE.next" "$FIXTURE" - done - - jq '.helper = {"allowed": true}' "$FIXTURE" > "$FIXTURE.next" - mv "$FIXTURE.next" "$FIXTURE" - run validate_fixture - [ "$status" -ne 0 ] -} - -@test "source and Codex controller artifacts are deleted" { - local path - for path in \ - schemas/validation-budget-receipt.v1.schema.json \ - skills/rpi/schemas/run-governor.schema.json \ - skills/rpi/scripts/run-governor.py \ - skills/validate/scripts/validation-budget.py \ - tests/scripts/rpi-run-governor.bats \ - tests/scripts/validation-budget.bats \ - skills-codex/rpi/schemas/run-governor.schema.json \ - skills-codex/rpi/scripts/run-governor.py \ - skills-codex/validate/scripts/validation-budget.py; do - [ ! -e "$REPO_ROOT/$path" ] - done - [ -f "$REPO_ROOT/skills-codex/rpi/schemas/run-disposition.schema.json" ] -} - -@test "plan mutation checkpoints record facts without a hidden budget" { - local validator - for validator in \ - skills/crank/scripts/validate-wave-checkpoint.sh \ - skills-codex/crank/scripts/validate-wave-checkpoint.sh; do - run bash "$REPO_ROOT/$validator" "$CHECKPOINT" "$REPO_ROOT" - [ "$status" -eq 0 ] - - local field - for field in mutation_budget mutation_limits limits usage; do - jq --arg field "$field" '.[$field] = {"used": 1, "limit": 3}' \ - "$CHECKPOINT" > "$CHECKPOINT.next" - run bash "$REPO_ROOT/$validator" "$CHECKPOINT.next" "$REPO_ROOT" - [ "$status" -ne 0 ] - done - done - - run rg -n -i \ - 'mutation[_ -]?budget|enforces? budgets?|[0-9]+ per[- ]epic|\| unlimited \||"(used|limit)"' \ - "$REPO_ROOT/skills/crank/references/plan-mutations.md" \ - "$REPO_ROOT/skills-codex/crank/references/plan-mutations.md" - [ "$status" -eq 1 ] -} - -@test "RPI Crank and Validate require dispositions without phase-local admission" { - local consumers=( - skills/rpi/SKILL.md - skills/rpi/references/agile-replan-loop.md - skills/rpi/references/error-handling.md - skills/rpi/references/gate-retry-logic.md - skills/rpi/references/gate4-loop-and-spawn.md - skills/rpi/references/isolation-contract.md - skills/rpi/references/orchestrator-compression-anti-pattern.md - skills/rpi/references/phase-budgets.md - skills/rpi/references/phase-data-contracts.md - skills/rpi/references/pull-flow-governor.md - skills/rpi/references/rpi.feature - skills/rpi/references/troubleshooting.md - skills/crank/SKILL.md - skills/crank/references/crank.feature - skills/crank/references/execution-preflight.md - skills/crank/references/external-gate-protocol.md - skills/crank/references/failure-recovery.md - skills/crank/references/failure-taxonomy.md - skills/crank/references/plan-mutations.md - skills/crank/references/test-first-mode.md - skills/crank/references/troubleshooting.md - skills/crank/references/wave-dispatch.md - skills/crank/references/wave-patterns.md - skills/crank/references/wave1-spec-consistency-checklist.md - skills/crank/scripts/validate-wave-checkpoint.sh - skills/validate/SKILL.md - skills/validate/references/canonical-validation-protocol.md - skills/validate/references/validate.feature - ) - - run rg -n -i \ - 'run-governor|validation-budget|persistent (run )?governor|durable .*admission|authorized:? ?true|AUTHORIZED receipt|request(s|ed)? .*admission|run-wide .*ceiling|phase-local .*budget' \ - "${consumers[@]/#/$REPO_ROOT/}" - [ "$status" -eq 1 ] - - run rg -n 'NOTE.*REPAIR.*REPLAN.*HOLD.*ANDON' "$REPO_ROOT/skills/rpi/SKILL.md" - [ "$status" -eq 0 ] -} diff --git a/tests/scripts/scaffold-registry-regen.bats b/tests/scripts/scaffold-registry-regen.bats deleted file mode 100644 index ddaecd5bd..000000000 --- a/tests/scripts/scaffold-registry-regen.bats +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env bats -# ag-ekyq: skill-builder init.sh must regenerate registry.json (the SKU catalog) -# as part of new-skill scaffolding — the 5th one-shot-green surface ag-cw2y missed. -# A stale registry.json trips contracts-sync ("registry.json is stale") AND -# correctness(ubuntu) ("SKU_CATALOG: DRIFT") together; it cost /burndown #600 a -# 2nd fix-and-repush. generate-registry.sh scans the whole skills/ tree (not -# repo-root-injectable), so the contract we lock here is the WIRING + ORDERING: -# init.sh must invoke the canonical generator, AFTER the skeleton + the other -# three plumbing surfaces exist on disk. - -INIT="$BATS_TEST_DIRNAME/../../skills/skill-builder/scripts/init.sh" - -@test "init.sh invokes the canonical generate-registry.sh during scaffolding" { - run grep -E 'scripts/generate-registry\.sh' "$INIT" - [ "$status" -eq 0 ] -} - -@test "the registry regen is guarded with a WARN fallback like its sibling steps" { - # Must not hard-fail the scaffold if regen has trouble — same || echo WARN shape - # as the dispositions / counts / override-catalog steps. - run grep -E 'generate-registry\.sh.*>/dev/null|WARN could not regen registry' "$INIT" - [ "$status" -eq 0 ] -} - -@test "registry regen runs AFTER the codex override-catalog step (skeleton must exist first)" { - override_line=$(grep -n 'append-codex-override-entry\.sh' "$INIT" | head -1 | cut -d: -f1) - registry_line=$(grep -n 'generate-registry\.sh' "$INIT" | head -1 | cut -d: -f1) - [ -n "$override_line" ] - [ -n "$registry_line" ] - # registry regen must come later in the file (it scans the whole tree, so every - # other artifact — skill dir, dispositions, counts, codex catalog — must be in - # place first). - [ "$registry_line" -gt "$override_line" ] -} - -@test "registry regen runs BEFORE the final 'created skill skeleton' echo (inside the plumbing block)" { - registry_line=$(grep -n 'generate-registry\.sh' "$INIT" | head -1 | cut -d: -f1) - echo_line=$(grep -n 'created skill skeleton at' "$INIT" | head -1 | cut -d: -f1) - [ -n "$registry_line" ] - [ -n "$echo_line" ] - [ "$registry_line" -lt "$echo_line" ] -} diff --git a/tests/scripts/select-spine-skills.bats b/tests/scripts/select-spine-skills.bats index 41fbb32df..f20d2ca72 100644 --- a/tests/scripts/select-spine-skills.bats +++ b/tests/scripts/select-spine-skills.bats @@ -20,8 +20,8 @@ setup() { # Permanent compatibility pointers for a spine skill must survive the same # bundle pruning as their canonical target. echo "$output" | grep -qx "premortem" - echo "$output" | grep -qx "pre-mortem" - echo "$output" | grep -qx "pre_mortem" + echo "$output" | grep -qx "premortem" + echo "$output" | grep -qx "premortem" } @test "selector excludes non-spine (experimental/corpus) skills" { diff --git a/tests/scripts/session-pr-scope.bats b/tests/scripts/session-pr-scope.bats deleted file mode 100644 index 93b31dc17..000000000 --- a/tests/scripts/session-pr-scope.bats +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/env bats -# ag-o5xp: session-pr-scope.sh is the hookless replacement for the removed -# hooks/session-pr-counter.sh (deleted in the #511 hookless teardown). It counts -# the current user's recent PRs and emits a session-scope verdict. These cases -# exercise the verdict thresholds, --count/--json modes, block-mode exit code, -# and fail-open behavior deterministically via an injectable fake `gh` on PATH -# (so they never touch the network or depend on real PR history). - -setup() { - SCRIPT="$BATS_TEST_DIRNAME/../../scripts/session-pr-scope.sh" - STUB="$(mktemp -d)" - # Fake gh: emits a JSON array of SET_PR_COUNT objects, so the real `jq -r length` - # in the script yields a deterministic count. Ignores all args (the script only - # cares about the JSON array length). - cat >"$STUB/gh" <<'EOF' -#!/usr/bin/env bash -n="${SET_PR_COUNT:-0}" -printf '[' -i=0 -while [ "$i" -lt "$n" ]; do - [ "$i" -gt 0 ] && printf ',' - printf '{"number":%d}' "$i" - i=$((i + 1)) -done -printf ']\n' -EOF - chmod +x "$STUB/gh" - PATH="$STUB:$PATH" -} - -teardown() { rm -rf "$STUB"; } - -@test "OK below warn threshold (2 PRs, threshold 5)" { - SET_PR_COUNT=2 run bash "$SCRIPT" - [ "$status" -eq 0 ] - [[ "$output" == *"OK"* ]] - [[ "$output" == *"2 PR"* ]] -} - -@test "WARN at threshold-1 (4 PRs, threshold 5)" { - SET_PR_COUNT=4 run bash "$SCRIPT" - [ "$status" -eq 0 ] - [[ "$output" == *"WARN"* ]] - [[ "$output" == *"post-mortem"* ]] -} - -@test "WARN (not block) at/over threshold without block mode (6 PRs)" { - SET_PR_COUNT=6 run bash "$SCRIPT" - [ "$status" -eq 0 ] - [[ "$output" == *"WARN"* ]] -} - -@test "BLOCK + exit 2 over threshold with block mode opted in (5 PRs)" { - SET_PR_COUNT=5 AGENTOPS_SESSION_PR_BLOCK=1 run bash "$SCRIPT" - [ "$status" -eq 2 ] - [[ "$output" == *"BLOCK"* ]] -} - -@test "--count emits the raw integer" { - SET_PR_COUNT=3 run bash "$SCRIPT" --count - [ "$status" -eq 0 ] - [ "$output" = "3" ] -} - -@test "--json reports verdict and over flag" { - SET_PR_COUNT=5 run bash "$SCRIPT" --json - [ "$status" -eq 0 ] - [[ "$output" == *'"verdict":"warn"'* ]] - [[ "$output" == *'"over":true'* ]] - [[ "$output" == *'"count":5'* ]] -} - -@test "custom SESSION_PR_THRESHOLD shifts the warn edge" { - # threshold 3 => warn at count>=2 - SET_PR_COUNT=2 SESSION_PR_THRESHOLD=3 run bash "$SCRIPT" - [ "$status" -eq 0 ] - [[ "$output" == *"WARN"* ]] -} - -@test "fail-open: gh failure yields verdict=unknown, exit 0" { - # Fake gh that errors → empty/unparseable count → fail open. - cat >"$STUB/gh" <<'EOF' -#!/usr/bin/env bash -exit 1 -EOF - chmod +x "$STUB/gh" - run bash "$SCRIPT" --json - [ "$status" -eq 0 ] - [[ "$output" == *'"verdict":"unknown"'* ]] -} diff --git a/tests/scripts/skill-rebin-bc6-roles.bats b/tests/scripts/skill-rebin-bc6-roles.bats deleted file mode 100644 index dd49b4434..000000000 --- a/tests/scripts/skill-rebin-bc6-roles.bats +++ /dev/null @@ -1,103 +0,0 @@ -#!/usr/bin/env bats -# -# S1 re-bin + BC6 population + role re-grade contract (ag-j3ge0). -# -# Asserts the corrected dispositions on the CANONICAL ledger -# (docs/contracts/skill-dispositions.yaml) after the S0 BC6 foundation -# (ag-4akl8) landed. These are the bead's acceptance scenarios made executable: -# - mis-binned skills land in their correct bounded context -# - the loop spine (rpi/evolve) is graded `domain`, not `supporting` -# - the lone `generic` skill (converter) is given a real hexagonal role -# - every bounded context BC1-BC6 is non-empty -# -# The assertions read the production ledger with the same yaml loader the -# validators use (round-trip fidelity), so a green here means the real -# generators + drift gates see the same corrected shape. - -setup() { - REPO_ROOT="$(cd "$BATS_TEST_DIRNAME/../.." && pwd)" - export REPO_ROOT - DISP_YAML="$REPO_ROOT/docs/contracts/skill-dispositions.yaml" - export DISP_YAML -} - -# Helper: print "<domain>|<hexagonal_role>" for an active skill row. -_row() { - python3 - "$1" <<'PY' -import sys, yaml, pathlib -disp = pathlib.Path(__import__("os").environ["DISP_YAML"]) -data = yaml.safe_load(disp.read_text(encoding="utf-8")) or {} -rows = {r["skill"]: r for r in data.get("dispositions", []) if isinstance(r, dict) and "skill" in r} -name = sys.argv[1] -r = rows.get(name) -if r is None: - print("MISSING") -else: - print(f"{r.get('domain')}|{r.get('hexagonal_role')}") -PY -} - -@test "refactor is re-binned BC2 Validation -> BC3 Loop" { - run _row refactor - [ "$status" -eq 0 ] - [[ "$output" == "BC3 Loop|"* ]] -} - -@test "cass is re-binned BC5 Runtime -> BC1 Corpus" { - run _row cass - [ "$status" -eq 0 ] - [[ "$output" == "BC1 Corpus|"* ]] -} - -@test "rpi is graded domain (loop spine), not supporting" { - run _row rpi - [ "$status" -eq 0 ] - [ "$output" = "BC3 Loop|domain" ] -} - -@test "evolve is graded domain (loop spine), not supporting" { - run _row evolve - [ "$status" -eq 0 ] - [ "$output" = "BC3 Loop|domain" ] -} - -@test "converter is no longer the lone generic; it has a real hexagonal role" { - run _row converter - [ "$status" -eq 0 ] - [[ "$output" != *"|generic" ]] -} - -@test "no active skill row remains tagged generic" { - run python3 - <<'PY' -import yaml, pathlib, os -disp = pathlib.Path(os.environ["DISP_YAML"]) -data = yaml.safe_load(disp.read_text(encoding="utf-8")) or {} -generic = [r["skill"] for r in data.get("dispositions", []) - if isinstance(r, dict) and r.get("hexagonal_role") == "generic"] -if generic: - print("GENERIC:" + ",".join(generic)) - raise SystemExit(1) -print("OK") -PY - [ "$status" -eq 0 ] - [ "$output" = "OK" ] -} - -@test "every bounded context BC1-BC6 is non-empty" { - run python3 - <<'PY' -import yaml, pathlib, os -disp = pathlib.Path(os.environ["DISP_YAML"]) -data = yaml.safe_load(disp.read_text(encoding="utf-8")) or {} -seen = set() -for r in data.get("dispositions", []): - if isinstance(r, dict) and r.get("domain"): - seen.add(str(r["domain"]).split()[0]) # "BC3 Loop" -> "BC3" -missing = [f"BC{n}" for n in range(1, 7) if f"BC{n}" not in seen] -if missing: - print("EMPTY:" + ",".join(missing)) - raise SystemExit(1) -print("OK") -PY - [ "$status" -eq 0 ] - [ "$output" = "OK" ] -} diff --git a/tests/scripts/skill-standards-convergence.bats b/tests/scripts/skill-standards-convergence.bats index d3d931638..c7e3e9351 100644 --- a/tests/scripts/skill-standards-convergence.bats +++ b/tests/scripts/skill-standards-convergence.bats @@ -1,945 +1,55 @@ #!/usr/bin/env bats -# Acceptance harness for age-h433.1: one canonical skill-conformance profile. -# -# The fixtures exercise the public scanner/auditor/healer/factory processes. -# They intentionally avoid sourcing implementation functions so the suite stays -# coupled to the contract rather than to any one implementation. setup() { - REPO_ROOT="$(cd "${BATS_TEST_DIRNAME}/../.." && pwd)" - SCANNER="$REPO_ROOT/skills/skill-builder/scripts/scan_descriptions.py" - AUDITOR="$REPO_ROOT/skills/heal-skill/scripts/audit.sh" - HEALER="$REPO_ROOT/skills/heal-skill/scripts/heal.sh" - BUILDER="$REPO_ROOT/skills/skill-builder/scripts/build.sh" - PROFILE="$REPO_ROOT/skills/skill-builder/references/skill-conformance-profiles.yaml" - KNOWN_GOOD="$REPO_ROOT/tests/fixtures/skills/known-good" + REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../.." && pwd)" } -assert_json() { - local file="$1" - local expression="$2" - local expectation="$3" - - if ! jq -e "$expression" "$file" >/dev/null; then - echo "JSON assertion failed: $expectation" >&2 - jq . "$file" >&2 || true - return 1 - fi +@test "skill mesh projections match SKILL metadata" { + run python3 "$REPO_ROOT/scripts/generate-skill-mesh.py" --check + [ "$status" -eq 0 ] } -write_conforming_skill() { - local target="$1" - local name="$2" - local description="$3" - local output_variant="${4:-complete}" - - mkdir -p "$target" - { - printf '%s\n' '---' - printf 'name: %s\n' "$name" - printf 'description: %s\n' "$description" - printf '%s\n' \ - 'skill_api_version: 1' \ - 'context:' \ - ' window: fork' \ - ' intent:' \ - ' mode: task' \ - ' sections:' \ - ' exclude: [HISTORY]' \ - ' intel_scope: topic' \ - 'metadata:' \ - ' tier: execution' \ - ' dependencies: []' \ - 'output_contract: ".agents/out/report.json: validated JSON handoff"' \ - '---' \ - "# $name" \ - '' \ - '## Critical Constraints' \ - '' \ - '- Keep the fixture isolated. **Why:** acceptance tests must not mutate live skills.' \ - '' \ - '## Workflow' \ - '' \ - 'Create the deterministic fixture.' \ - '' \ - '**Checkpoint:** the fixture exists and is parseable.' \ - '' \ - '## Output Specification' \ - '' - if [[ "$output_variant" == "incomplete" ]]; then - printf '%s\n' \ - '**Format:** JSON' \ - '**Path:** `.agents/out/report.json`' - else - printf '%s\n' \ - '**Artifact directory:** `.agents/out/`' \ - '**Filename convention:** `report.json`' \ - '**Serialization/schema format:** JSON matching `schemas/report.schema.json`.' \ - '**Validator command:** `jq -e . .agents/out/report.json`' \ - '**Downstream handoff:** consumed by the validation wave.' - fi - printf '%s\n' \ - '' \ - '## Quality Rubric' \ - '' \ - '- [ ] Frontmatter is valid.' \ - '- [ ] Constraints include rationale.' \ - '- [ ] Workflow includes a checkpoint.' \ - '- [ ] Output handoff is executable.' \ - '- [ ] References resolve.' - } >"$target/SKILL.md" -} - -write_report_from_output() { - local output_file="$1" - printf '%s\n' "$output" >"$output_file" - jq . "$output_file" >/dev/null -} - -materialize_line_fixture() { - local target="$1" - local line_count="$2" - local skill_md - - cp -R "$KNOWN_GOOD" "$target" - skill_md="$target/SKILL.md" - if [[ "$(wc -l <"$skill_md")" -gt "$line_count" ]]; then - echo "known-good fixture already exceeds requested line count" >&2 - return 1 - fi - while [[ "$(wc -l <"$skill_md")" -lt "$line_count" ]]; do - printf '<!-- deterministic line-boundary padding -->\n' >>"$skill_md" - done - [[ "$(wc -l <"$skill_md")" -eq "$line_count" ]] -} - -prepare_builder_root() { - local scratch="$1" - - mkdir -p "$scratch/docs" "$scratch/skills-codex" - cp -R "$REPO_ROOT/skills" "$scratch/skills" - cp -R "$REPO_ROOT/scripts" "$scratch/scripts" - cp -R "$REPO_ROOT/docs/contracts" "$scratch/docs/contracts" - cp -R "$REPO_ROOT/docs/reference" "$scratch/docs/reference" - cp -R "$REPO_ROOT/skills-codex-overrides" "$scratch/skills-codex-overrides" - if [[ -f "$REPO_ROOT/registry.json" ]]; then - cp "$REPO_ROOT/registry.json" "$scratch/registry.json" - fi - git -C "$scratch" init -q -} - -set_protected_frontmatter_fields() { - local profile="$1" - local mutation="$2" - - python3 - "$profile" "$mutation" <<'PY' -import sys +@test "every live skill has one complete metadata disposition" { + run python3 - "$REPO_ROOT" <<'PY' from pathlib import Path - +import sys import yaml -path = Path(sys.argv[1]) -mutation = sys.argv[2] -document = yaml.safe_load(path.read_text(encoding="utf-8")) -for profile in document["profiles"].values(): - copy_detection = profile["clean_room"]["copy_detection"] - if mutation == "valid": - copy_detection["protected_frontmatter_fields"] = ["description"] - elif mutation == "missing": - copy_detection.pop("protected_frontmatter_fields", None) - elif mutation == "empty": - copy_detection["protected_frontmatter_fields"] = [] - elif mutation == "invalid": - copy_detection["protected_frontmatter_fields"] = "description" - elif mutation == "duplicate": - copy_detection["protected_frontmatter_fields"] = [ - "description", - "description", - ] - elif mutation == "unknown": - copy_detection["protected_frontmatter_fields"] = [ - "description", - "summary", - ] - else: - raise SystemExit(f"unknown test mutation: {mutation}") -path.write_text(yaml.safe_dump(document, sort_keys=False), encoding="utf-8") +root = Path(sys.argv[1]) +required = {"tier", "dependencies", "capabilities", "effects", "canonical_status", "disposition"} +for path in sorted((root / "skills").glob("*/SKILL.md")): + data = yaml.safe_load(path.read_text(encoding="utf-8").split("---", 2)[1]) + metadata = data.get("metadata") or {} + missing = required - set(metadata) + if missing: + raise SystemExit(f"{path}: missing {sorted(missing)}") + if metadata["disposition"] not in { + "keep", "keep_off_path", "keep_strategy", "keep_optional_adapter", "keep_specialist" + }: + raise SystemExit(f"{path}: invalid disposition") PY + [ "$status" -eq 0 ] } -write_external_description_fixture() { - local target="$1" - local scalar_style="$2" - local scalar_parts="$3" - local first_part="${scalar_parts%%|*}" - local second_part="${scalar_parts#*|}" - - { - printf '%s\n' '---' 'name: foreign-description-probe' - case "$scalar_style" in - inline) - printf 'description: "%s"\n' "$scalar_parts" - ;; - folded) - printf '%s\n' 'description: >-' - printf ' %s\n' "$first_part" "$second_part" - ;; - literal) - printf '%s\n' 'description: |-' - printf ' %s\n' "$first_part" "$second_part" - ;; - *) - echo "unknown scalar style: $scalar_style" >&2 - return 1 - ;; - esac - printf '%s\n' '---' '# External Input' '' 'Synthetic body text is intentionally unrelated.' - } >"$target" -} - -@test "L2: inline trigger decisions converge on one observable profile and form id" { - local skills_root="$BATS_TEST_TMPDIR/inline-skills" - local scanner_json="$BATS_TEST_TMPDIR/inline-scanner.json" - local audit_json="$BATS_TEST_TMPDIR/inline-audit.json" - - write_conforming_skill "$skills_root/inline-trigger" inline-trigger \ - "'Audits one skill. Triggers: \"audit skill\".'" - - run python3 "$SCANNER" "$skills_root" --json --strict - [[ "$status" -eq 0 ]] - write_report_from_output "$scanner_json" - - run bash "$AUDITOR" --strict --json "$audit_json" "$skills_root/inline-trigger" - [[ "$status" -eq 0 ]] - - assert_json "$scanner_json" '.profile_id | type == "string" and length > 0' \ - 'scanner identifies the selected profile' - assert_json "$scanner_json" '.skills[0].forms == ["inline-marker"]' \ - 'scanner reports the canonical inline-marker form' - assert_json "$audit_json" '.profile_id | type == "string" and length > 0' \ - 'auditor identifies the selected profile' - assert_json "$audit_json" \ - '.pass2.checks[] | select(.id == "description-has-triggers") | .status == "pass" and (.forms | index("inline-marker") != null)' \ - 'auditor accepts the same inline-marker form' - [[ "$(jq -r .profile_id "$scanner_json")" == "$(jq -r .profile_id "$audit_json")" ]] -} - -@test "L2: missing trigger is the same profile-declared WARN in scanner and auditor strict modes" { - local skills_root="$BATS_TEST_TMPDIR/missing-trigger-skills" - local scanner_json="$BATS_TEST_TMPDIR/missing-trigger-scanner.json" - local audit_json="$BATS_TEST_TMPDIR/missing-trigger-audit.json" - - write_conforming_skill "$skills_root/missing-trigger" missing-trigger \ - "'Audits one skill without an explicit activation marker.'" - - run python3 "$SCANNER" "$skills_root" --json --strict - [[ "$status" -eq 1 ]] - write_report_from_output "$scanner_json" - assert_json "$scanner_json" '.skills[0].has_trigger == false and .skills[0].forms == []' \ - 'scanner accepts no trigger form from a plain description' - - run bash "$AUDITOR" --strict --json "$audit_json" "$skills_root/missing-trigger" - [[ "$status" -eq 1 ]] - assert_json "$audit_json" \ - '.pass2.checks[] | select(.id == "description-has-triggers") | .status == "warn" and ((.severity // "") | ascii_upcase) == "WARN"' \ - 'auditor emits the canonical profile WARN for a missing trigger' - [[ "$(jq -r .profile_id "$scanner_json")" == "$(jq -r .profile_id "$audit_json")" ]] -} - -@test "L2: repo-runtime kernel boundary accepts 250 lines and flags 251 lines" { - local at_limit="$BATS_TEST_TMPDIR/at-limit" - local over_limit="$BATS_TEST_TMPDIR/over-limit" - local at_json="$BATS_TEST_TMPDIR/at-limit.json" - local over_json="$BATS_TEST_TMPDIR/over-limit.json" - - materialize_line_fixture "$at_limit" 250 - run bash "$AUDITOR" --strict --json "$at_json" "$at_limit" - [[ "$status" -eq 0 ]] - assert_json "$at_json" \ - '.pass2.checks[] | select(.id == "references-modularization") | .status == "pass"' \ - 'a 250-line kernel remains within the profile boundary' - - materialize_line_fixture "$over_limit" 251 - run bash "$AUDITOR" --strict --json "$over_json" "$over_limit" - [[ "$status" -ne 0 ]] - assert_json "$over_json" \ - '.pass2.checks[] | select(.id == "references-modularization") | .status != "pass" and (.severity | type == "string" and length > 0)' \ - 'a 251-line kernel emits the profile-declared modularization finding' -} - -@test "L2: output-spec-explicit requires the complete executable handoff" { - local incomplete="$BATS_TEST_TMPDIR/output-incomplete" - local complete="$BATS_TEST_TMPDIR/output-complete" - local incomplete_json="$BATS_TEST_TMPDIR/output-incomplete.json" - local complete_json="$BATS_TEST_TMPDIR/output-complete.json" - - write_conforming_skill "$incomplete" output-incomplete \ - "'Builds a report. Triggers: \"build report\".'" incomplete - run bash "$AUDITOR" --json "$incomplete_json" "$incomplete" - [[ "$status" -eq 1 ]] - assert_json "$incomplete_json" \ - '.verdict == "FAIL" and (.pass2.checks[] | select(.id == "output-spec-explicit") | .status == "fail")' \ - 'JSON plus a path alone is an output-spec-explicit failure' - - write_conforming_skill "$complete" output-complete \ - "'Builds a report. Triggers: \"build report\".'" complete - run bash "$AUDITOR" --json "$complete_json" "$complete" - [[ "$status" -eq 0 ]] - assert_json "$complete_json" \ - '.pass2.checks[] | select(.id == "output-spec-explicit") | .status == "pass"' \ - 'artifact path, filename, schema, validator, and handoff pass together' -} - -@test "L2: healer resolves canonical cross-skill and repo-root references without hiding DEAD_REF" { - local fixture_root="$BATS_TEST_TMPDIR/heal-root" - local skill_md="$fixture_root/skills/reference-fixture/SKILL.md" - local valid_output - - mkdir -p "$fixture_root/skills/standards/references" \ - "$fixture_root/skills/reference-fixture" "$fixture_root/docs/contracts" - cp "$REPO_ROOT/skills/standards/references/shell.md" \ - "$fixture_root/skills/standards/references/shell.md" - write_conforming_skill "$fixture_root/skills/reference-fixture" reference-fixture \ - "'Checks references. Triggers: \"check references\".'" - printf '%s\n' \ - '' \ - '## References' \ - '' \ - '- [Cross-skill](../standards/references/shell.md)' \ - '- [Repo-root](skills/standards/references/shell.md)' >>"$skill_md" - printf '%s\n' \ - 'dispositions:' \ - ' - skill: reference-fixture' \ - ' domain: "BC1 Corpus"' \ - ' hexagonal_role: supporting' \ - ' disposition: keep' \ - ' rationale: "acceptance fixture"' >"$fixture_root/docs/contracts/skill-dispositions.yaml" - - run env HEAL_REPO_ROOT="$fixture_root" bash "$HEALER" --check --strict skills/reference-fixture - valid_output="$output" - [[ "$valid_output" != *'DEAD_REF'* ]] - [[ "$valid_output" != *'UNLINKED_REF'* ]] - - sed -i.bak 's#references/shell\.md#references/missing-shell.md#g' "$skill_md" - rm -f "$skill_md.bak" - run env HEAL_REPO_ROOT="$fixture_root" bash "$HEALER" --check --strict skills/reference-fixture - [[ "$status" -eq 1 ]] - [[ "$output" == *'DEAD_REF'* ]] - [[ "$output" == *'missing-shell.md'* ]] -} - -@test "L2: absorb-external synthesizes clean-room source and Codex outputs" { - local scratch="$BATS_TEST_TMPDIR/builder-root" - local external="$BATS_TEST_TMPDIR/external-skill.md" - local name="clean-room-sentinel" - local sentinel="MAGENTA ORBITAL WALRUS signs every borrowed paragraph." - local report - - prepare_builder_root "$scratch" - # macOS exposes BATS_TEST_TMPDIR through /var -> /private/var. The healer - # deliberately rejects symlink spellings, so exercise the builder with the - # physical path that production containment checks accept. - scratch="$(cd -P "$scratch" && pwd)" - { - printf '%s\n' \ - '---' \ - 'name: foreign-sentinel' \ - 'description: '\''External package. Triggers: "foreign sentinel".'\''' \ - '---' \ - '# Foreign Sentinel' \ - '' \ - "$sentinel" \ - '' \ - 'Run the source prompt exactly and preserve its examples.' - } >"$external" - - run env SKILL_BUILDER_REPO_ROOT="$scratch" SKILL_TIER=execution \ - SKILL_INTENT_MODE=task bash "$BUILDER" absorb-external "$name" --from "$external" - [[ "$status" -eq 0 ]] - if rg -F "$sentinel" "$scratch/skills/$name" "$scratch/skills-codex/$name"; then - echo 'clean-room violation: external sentinel was copied into generated output' >&2 - return 1 - fi - - report="$scratch/.agents/audits/$name-build.json" - [[ -s "$report" ]] - assert_json "$report" \ - '.mode == "absorb-external" and .audit_pass == true and (.profile_id | type == "string" and length > 0)' \ - 'successful clean-room build records its selected profile' -} - -@test "L2: unknown profile severity fails both consumers closed with no verdict" { - local scratch="$BATS_TEST_TMPDIR/bad-profile-root" - local skills_root="$scratch/profile-fixtures" - local copied_profile="$scratch/skills/skill-builder/references/skill-conformance-profiles.yaml" - local scanner_output auditor_output - - [[ -f "$PROFILE" ]] || { - echo "profile configuration missing: $PROFILE" >&2 - return 1 - } - prepare_builder_root "$scratch" - write_conforming_skill "$skills_root/bad-profile" bad-profile \ - "'Checks profile errors. Triggers: \"check profile\".'" - perl -0pi -e 's/(severity:\s*)[A-Za-z]+/${1}BOGUS/' "$copied_profile" - rg -q 'severity:[[:space:]]*BOGUS' "$copied_profile" - - run python3 "$scratch/skills/skill-builder/scripts/scan_descriptions.py" \ - "$skills_root" --json --strict - scanner_output="$output" - [[ "$status" -ne 0 ]] - [[ "$scanner_output" =~ [Pp]rofile|[Cc]onfiguration ]] - [[ "$scanner_output" == *'BOGUS'* ]] - [[ "$scanner_output" != *'"verdict": "PASS"'* ]] - [[ "$scanner_output" != *'"verdict": "WARN"'* ]] - - run env HEAL_REPO_ROOT="$scratch" \ - bash "$scratch/skills/heal-skill/scripts/audit.sh" --strict "$skills_root/bad-profile" - auditor_output="$output" - [[ "$status" -ne 0 ]] - [[ "$auditor_output" =~ [Pp]rofile|[Cc]onfiguration ]] - [[ "$auditor_output" == *'BOGUS'* ]] - [[ "$auditor_output" != *'"verdict": "PASS"'* ]] - [[ "$auditor_output" != *'"verdict": "WARN"'* ]] -} - -@test "L1: source and Codex treatments resolve to one profile and clean-room doctrine" { - local owner tree - - [[ -s "$PROFILE" ]] - python3 - "$PROFILE" <<'PY' -import sys +@test "core graph is exactly RPI to Plan Implement Validate" { + run python3 - "$REPO_ROOT" <<'PY' from pathlib import Path - +import sys import yaml -payload = yaml.safe_load(Path(sys.argv[1]).read_text(encoding="utf-8")) -if not isinstance(payload, dict): - raise SystemExit("profile document must be a YAML mapping") -if "repo-runtime" not in str(payload): - raise SystemExit("profile document does not declare the repo-runtime profile") +root = Path(sys.argv[1]) +actual = {} +for name in ("rpi", "plan", "implement", "validate"): + path = root / "skills" / name / "SKILL.md" + data = yaml.safe_load(path.read_text(encoding="utf-8").split("---", 2)[1]) + actual[name] = set((data.get("metadata") or {}).get("dependencies") or []) +expected = {"rpi": {"plan", "implement", "validate"}, "plan": set(), "implement": set(), "validate": set()} +if actual != expected: + raise SystemExit(f"core graph mismatch: {actual!r}") PY - - for owner in skill-builder heal-skill standards; do - for tree in skills skills-codex; do - if ! rg -q 'skill-conformance-profiles\.yaml|repo-runtime' "$REPO_ROOT/$tree/$owner"; then - echo "$tree/$owner does not resolve to the authoritative repo-runtime profile" >&2 - return 1 - fi - done - done - - cmp -s "$REPO_ROOT/skills/skill-builder/references/skill-conformance-profiles.yaml" \ - "$REPO_ROOT/skills-codex/skill-builder/references/skill-conformance-profiles.yaml" - rg -qi 'clean-room' "$REPO_ROOT/skills/skill-builder" "$REPO_ROOT/skills-codex/skill-builder" - if rg -qi 'verbatim (preservation|copy)|preserve (the )?(external|source).*(verbatim|body)' \ - "$REPO_ROOT/skills/skill-builder" "$REPO_ROOT/skills-codex/skill-builder"; then - echo 'builder doctrine still promises verbatim external-source preservation' >&2 - return 1 - fi + [ "$status" -eq 0 ] } -@test "L1: removing a required rule from both rule collections fails consumers closed" { - local scratch="$BATS_TEST_TMPDIR/missing-rule-root" - local skills_root="$scratch/profile-fixtures" - local copied_profile="$scratch/skills/skill-builder/references/skill-conformance-profiles.yaml" - local scanner_status scanner_output auditor_status auditor_output - - prepare_builder_root "$scratch" - write_conforming_skill "$skills_root/missing-rule" missing-rule \ - "'Checks required rules. Triggers: \"check required rules\".'" - python3 - "$copied_profile" <<'PY' -import sys -from pathlib import Path - -import yaml - -path = Path(sys.argv[1]) -document = yaml.safe_load(path.read_text(encoding="utf-8")) -profile = document["profiles"][document["default_profile"]] -profile["rule_order"].remove("trigger-clarity") -del profile["rules"]["trigger-clarity"] -path.write_text(yaml.safe_dump(document, sort_keys=False), encoding="utf-8") -PY - - run python3 "$scratch/skills/skill-builder/scripts/scan_descriptions.py" \ - "$skills_root" --json --strict - scanner_status="$status" - scanner_output="$output" - - run env HEAL_REPO_ROOT="$scratch" \ - bash "$scratch/skills/heal-skill/scripts/audit.sh" --strict "$skills_root/missing-rule" - auditor_status="$status" - auditor_output="$output" - - [[ "$scanner_status" -ne 0 ]] || { - echo 'scanner accepted a profile missing the required trigger-clarity rule' >&2 - return 1 - } - [[ "$scanner_output" == *'profile configuration error'* && "$scanner_output" == *'trigger-clarity'* ]] || { - echo "scanner did not report the missing rule as an actionable profile configuration error: $scanner_output" >&2 - return 1 - } - [[ "$auditor_status" -ne 0 ]] || { - echo 'auditor accepted a profile missing the required trigger-clarity rule' >&2 - return 1 - } - [[ "$auditor_output" == *'profile configuration error'* && "$auditor_output" == *'trigger-clarity'* ]] || { - echo "auditor did not report the missing rule as an actionable profile configuration error: $auditor_output" >&2 - return 1 - } - [[ "$scanner_output" != *'"verdict": "PASS"'* && "$auditor_output" != *'VERDICT: PASS'* ]] -} - -@test "L1: adding an invented rule to both rule collections fails consumers closed" { - local scratch="$BATS_TEST_TMPDIR/invented-rule-root" - local skills_root="$scratch/profile-fixtures" - local copied_profile="$scratch/skills/skill-builder/references/skill-conformance-profiles.yaml" - local scanner_status scanner_output auditor_status auditor_output - - prepare_builder_root "$scratch" - write_conforming_skill "$skills_root/invented-rule" invented-rule \ - "'Checks known rules. Triggers: \"check known rules\".'" - python3 - "$copied_profile" <<'PY' -import sys -from pathlib import Path - -import yaml - -path = Path(sys.argv[1]) -document = yaml.safe_load(path.read_text(encoding="utf-8")) -profile = document["profiles"][document["default_profile"]] -profile["rule_order"].append("invented-rule") -profile["rules"]["invented-rule"] = {"severity": "WARN"} -path.write_text(yaml.safe_dump(document, sort_keys=False), encoding="utf-8") -PY - - run python3 "$scratch/skills/skill-builder/scripts/scan_descriptions.py" \ - "$skills_root" --json --strict - scanner_status="$status" - scanner_output="$output" - - run env HEAL_REPO_ROOT="$scratch" \ - bash "$scratch/skills/heal-skill/scripts/audit.sh" --strict "$skills_root/invented-rule" - auditor_status="$status" - auditor_output="$output" - - [[ "$scanner_status" -ne 0 ]] || { - echo 'scanner accepted the undeclared invented-rule profile entry' >&2 - return 1 - } - [[ "$scanner_output" == *'profile configuration error'* && "$scanner_output" == *'invented-rule'* ]] || { - echo "scanner did not report the invented rule as an actionable profile configuration error: $scanner_output" >&2 - return 1 - } - [[ "$auditor_status" -ne 0 ]] || { - echo 'auditor accepted the undeclared invented-rule profile entry' >&2 - return 1 - } - [[ "$auditor_output" == *'profile configuration error'* && "$auditor_output" == *'invented-rule'* ]] || { - echo "auditor did not report the invented rule as an actionable profile configuration error: $auditor_output" >&2 - return 1 - } - [[ "$scanner_output" != *'"verdict": "PASS"'* && "$auditor_output" != *'VERDICT: PASS'* ]] -} - -@test "L2: metadata-only trigger emits trigger-clarity WARN and fails both strict consumers" { - local skills_root="$BATS_TEST_TMPDIR/metadata-only-skills" - local skill_md="$skills_root/metadata-only/SKILL.md" - local scanner_json="$BATS_TEST_TMPDIR/metadata-only-scanner.json" - local audit_json="$BATS_TEST_TMPDIR/metadata-only-audit.json" - local scanner_status auditor_status - - write_conforming_skill "$skills_root/metadata-only" metadata-only \ - "'Audits one skill using only structured activation metadata.'" - python3 - "$skill_md" <<'PY' -import sys -from pathlib import Path - -path = Path(sys.argv[1]) -text = path.read_text(encoding="utf-8") -text = text.replace( - "metadata:\n tier:", - "metadata:\n triggers:\n - audit metadata\n - scan metadata\n - validate metadata\n tier:", - 1, -) -path.write_text(text, encoding="utf-8") -PY - - run python3 "$SCANNER" "$skills_root" --json --strict - scanner_status="$status" - write_report_from_output "$scanner_json" - assert_json "$scanner_json" '.skills[0].forms == ["metadata-list"]' \ - 'metadata-list is the only accepted general trigger form' - assert_json "$scanner_json" \ - '.skills[0].checks[] | select(.id == "trigger-clarity") | .status == "warn" and .severity == "WARN"' \ - 'scanner emits the profile-declared trigger-clarity WARN' - - run bash "$AUDITOR" --strict --json "$audit_json" "$skills_root/metadata-only" - auditor_status="$status" - assert_json "$audit_json" \ - '.pass2.checks[] | select(.id == "trigger-clarity") | .status == "warn" and .severity == "WARN"' \ - 'auditor emits the matching trigger-clarity WARN' - [[ "$(jq -r .profile_id "$scanner_json")" == "$(jq -r .profile_id "$audit_json")" ]] - - [[ "$scanner_status" -ne 0 ]] || { - echo 'scanner strict mode returned zero despite its emitted trigger-clarity WARN' >&2 - return 1 - } - [[ "$auditor_status" -ne 0 ]] || { - echo 'auditor strict mode returned zero despite its emitted trigger-clarity WARN' >&2 - return 1 - } -} - -@test "L2: scanner human report identifies the selected profile" { - local skills_root="$BATS_TEST_TMPDIR/human-profile-skills" - - write_conforming_skill "$skills_root/human-profile" human-profile \ - "'Audits one skill. Triggers: \"audit human profile\".'" - - run python3 "$SCANNER" "$skills_root" - [[ "$status" -eq 0 ]] - [[ "$output" == *'repo-runtime'* ]] || { - echo "scanner human output omitted selected profile repo-runtime: $output" >&2 - return 1 - } -} - -@test "L1: absorb-external fails closed when declared clean-room policy is missing" { - local scratch="$BATS_TEST_TMPDIR/missing-clean-room-policy-root" - local copied_profile="$scratch/skills/skill-builder/references/skill-conformance-profiles.yaml" - local external="$BATS_TEST_TMPDIR/missing-clean-room-policy-external.md" - local name="missing-clean-room-policy" - local build_status build_output report - - prepare_builder_root "$scratch" - python3 - "$copied_profile" <<'PY' -import sys -from pathlib import Path - -import yaml - -path = Path(sys.argv[1]) -document = yaml.safe_load(path.read_text(encoding="utf-8")) -profile = document["profiles"][document["default_profile"]] -del profile["clean_room"]["external_content_policy"] -path.write_text(yaml.safe_dump(document, sort_keys=False), encoding="utf-8") -PY - { - printf '%s\n' \ - '---' \ - 'name: foreign-policy-probe' \ - 'description: '\''External package. Triggers: "foreign policy probe".'\''' \ - '---' \ - '# Foreign Policy Probe' \ - '' \ - 'SAPPHIRE CLOCKWORK HERON marks content that policy must govern.' - } >"$external" - - run env SKILL_BUILDER_REPO_ROOT="$scratch" SKILL_TIER=execution \ - SKILL_INTENT_MODE=task bash "$scratch/skills/skill-builder/scripts/build.sh" \ - absorb-external "$name" --from "$external" - build_status="$status" - build_output="$output" - - [[ "$build_status" -ne 0 ]] || { - echo 'builder accepted absorb-external without the declared external_content_policy' >&2 - return 1 - } - [[ "$build_output" == *'profile configuration error'* && "$build_output" == *'external_content_policy'* ]] || { - echo "builder did not identify the missing clean-room policy as a profile configuration error: $build_output" >&2 - return 1 - } - report="$scratch/.agents/audits/$name-build.json" - if [[ -s "$report" ]]; then - assert_json "$report" '.audit_pass != true' \ - 'a clean-room configuration failure cannot produce a passing build report' - fi -} - -@test "L1: source and Codex severity doctrine matches profile WARN rules" { - python3 - "$PROFILE" \ - "$REPO_ROOT/skills/heal-skill/scripts/audit.sh" \ - "$REPO_ROOT/skills-codex/heal-skill/scripts/audit.sh" <<'PY' -import re -import sys -from pathlib import Path - -import yaml - -profile_path = Path(sys.argv[1]) -document = yaml.safe_load(profile_path.read_text(encoding="utf-8")) -profile = document["profiles"][document["default_profile"]] -severities = { - rule_id: rule["severity"] - for rule_id, rule in profile["rules"].items() -} -annotation = re.compile( - r"^# Check \d+: (?P<rule>[a-z0-9-]+) \((?P<severity>WARN|FAIL) on miss" -) -mismatches = [] -for raw_path in sys.argv[2:]: - path = Path(raw_path) - for line_number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1): - match = annotation.match(line) - if not match: - continue - rule_id = match.group("rule") - declared = severities.get(rule_id) - observed = match.group("severity") - if declared is None: - mismatches.append(f"{path}:{line_number}: unknown doctrine rule {rule_id}") - elif observed != declared: - mismatches.append( - f"{path}:{line_number}: {rule_id} says {observed} on miss; " - f"profile declares {declared}" - ) -if mismatches: - raise SystemExit("\n".join(mismatches)) -PY -} - -@test "RESET RED: protected external description prose is rejected across scalar and target matrix" { - local scratch="$BATS_TEST_TMPDIR/description-matrix-root" - local copied_profile="$scratch/skills/skill-builder/references/skill-conformance-profiles.yaml" - local verifier="$scratch/skills/skill-builder/scripts/conformance_profile.py" - local failures=0 - local index case_name scalar_style target scalar_parts normalized - local case_root external source_dir codex_dir source_status codex_status - local source_output codex_output expected_source expected_codex - local -a case_names=( - inline-source - inline-codex-only - folded-source - folded-codex - literal-source - literal-codex - ) - local -a scalar_styles=(inline inline folded folded literal literal) - local -a targets=(source codex source codex source codex) - local -a descriptions=( - 'SAPPHIRE CLOCKWORK HERON marks protected external prose.' - 'COPPER CELESTIAL BADGER guards Codex-only protected prose.' - 'AMBER IBIS guards|semantic copy.' - 'TEAL PANTHER guards|Codex semantic copy.' - 'VIOLET OTTER guards|literal semantic copy.' - 'SILVER FALCON guards|Codex literal copy.' - ) - - prepare_builder_root "$scratch" - set_protected_frontmatter_fields "$copied_profile" valid - - for index in "${!case_names[@]}"; do - case_name="${case_names[$index]}" - scalar_style="${scalar_styles[$index]}" - target="${targets[$index]}" - scalar_parts="${descriptions[$index]}" - normalized="${scalar_parts//|/ }" - case_root="$BATS_TEST_TMPDIR/description-$case_name" - external="$case_root/external.md" - source_dir="$case_root/skills/generated-skill" - codex_dir="$case_root/skills-codex/generated-skill" - mkdir -p "$source_dir" "$codex_dir" - write_external_description_fixture "$external" "$scalar_style" "$scalar_parts" - printf '%s\n' 'Independent source prose contains no protected phrase.' \ - >"$source_dir/SKILL.md" - printf '%s\n' 'Independent Codex prose contains no protected phrase.' \ - >"$codex_dir/SKILL.md" - if [[ "$target" == "source" ]]; then - printf '%s\n' "$normalized" >"$source_dir/SKILL.md" - expected_source=1 - expected_codex=0 - else - printf '%s\n' "$normalized" >"$codex_dir/SKILL.md" - expected_source=0 - expected_codex=1 - fi - - run python3 "$verifier" --repo-root "$scratch" --profile-id repo-runtime \ - --verify-clean-room "$external" --generated-dir "$source_dir" - source_status="$status" - source_output="$output" - run python3 "$verifier" --repo-root "$scratch" --profile-id repo-runtime \ - --verify-clean-room "$external" --generated-dir "$codex_dir" - codex_status="$status" - codex_output="$output" - - printf 'DESCRIPTION_MATRIX case=%s style=%s target=%s source_status=%s codex_status=%s\n' \ - "$case_name" "$scalar_style" "$target" "$source_status" "$codex_status" - if [[ "$expected_source" -eq 1 ]]; then - if [[ "$source_status" -eq 0 || "$source_output" != *'clean-room violation'* || \ - "$source_output" != *'copied external'* ]]; then - echo "source target did not reject copied description for $case_name: $source_output" >&2 - failures=$((failures + 1)) - fi - elif [[ "$source_status" -ne 0 ]]; then - echo "independent source target failed for $case_name: $source_output" >&2 - failures=$((failures + 1)) - fi - if [[ "$expected_codex" -eq 1 ]]; then - if [[ "$codex_status" -eq 0 || "$codex_output" != *'clean-room violation'* || \ - "$codex_output" != *'copied external'* ]]; then - echo "Codex target did not reject copied description for $case_name: $codex_output" >&2 - failures=$((failures + 1)) - fi - elif [[ "$codex_status" -ne 0 ]]; then - echo "independent Codex target failed for $case_name: $codex_output" >&2 - failures=$((failures + 1)) - fi - done - - [[ "$failures" -eq 0 ]] || { - echo "description acceptance matrix failures: $failures/${#case_names[@]}" >&2 - return 1 - } -} - -@test "RESET RED: protected-frontmatter profile mutations fail builder closed" { - local scratch="$BATS_TEST_TMPDIR/profile-matrix-root" - local copied_profile="$scratch/skills/skill-builder/references/skill-conformance-profiles.yaml" - local baseline_profile="$BATS_TEST_TMPDIR/profile-with-protected-description.yaml" - local external="$BATS_TEST_TMPDIR/profile-matrix-external.md" - local failures=0 - local mutation name build_status build_output report profile_error audit_pass - local -a mutations=(missing empty invalid duplicate unknown) - - prepare_builder_root "$scratch" - set_protected_frontmatter_fields "$copied_profile" valid - cp "$copied_profile" "$baseline_profile" - write_external_description_fixture "$external" inline \ - 'GOLDEN INDEPENDENT LYNX describes a foreign capability.' - - for mutation in "${mutations[@]}"; do - cp "$baseline_profile" "$copied_profile" - set_protected_frontmatter_fields "$copied_profile" "$mutation" - name="profile-$mutation-description" - run env SKILL_BUILDER_REPO_ROOT="$scratch" SKILL_TIER=execution \ - SKILL_INTENT_MODE=task bash "$scratch/skills/skill-builder/scripts/build.sh" \ - absorb-external "$name" --from "$external" - build_status="$status" - build_output="$output" - report="$scratch/.agents/audits/$name-build.json" - profile_error=no - if [[ "$build_output" == *'profile configuration error'* && \ - "$build_output" == *'protected_frontmatter_fields'* ]]; then - profile_error=yes - fi - audit_pass=missing - if [[ -s "$report" ]]; then - audit_pass="$(jq -r '.audit_pass // false' "$report")" - fi - printf 'PROFILE_MATRIX mutation=%s status=%s profile_error=%s audit_pass=%s\n' \ - "$mutation" "$build_status" "$profile_error" "$audit_pass" - - if [[ "$build_status" -eq 0 || "$profile_error" != yes || "$audit_pass" == true ]]; then - echo "builder did not fail closed for $mutation protected fields" >&2 - failures=$((failures + 1)) - fi - done - - [[ "$failures" -eq 0 ]] || { - echo "protected-frontmatter configuration failures: $failures/${#mutations[@]}" >&2 - return 1 - } -} - -@test "RESET GREEN: independently synthesized descriptions pass both generated targets" { - local scratch="$BATS_TEST_TMPDIR/independent-description-root" - local copied_profile="$scratch/skills/skill-builder/references/skill-conformance-profiles.yaml" - local verifier="$scratch/skills/skill-builder/scripts/conformance_profile.py" - local external="$BATS_TEST_TMPDIR/independent-description-external.md" - local source_dir="$BATS_TEST_TMPDIR/independent-description/skills/generated-skill" - local codex_dir="$BATS_TEST_TMPDIR/independent-description/skills-codex/generated-skill" - - prepare_builder_root "$scratch" - set_protected_frontmatter_fields "$copied_profile" valid - write_external_description_fixture "$external" folded \ - 'CRIMSON FOREIGN MARMOT explains|a protected outside capability.' - mkdir -p "$source_dir" "$codex_dir" - printf '%s\n' 'AgentOps source synthesis validates a local workflow.' >"$source_dir/SKILL.md" - printf '%s\n' 'Codex synthesis routes a separately authored local workflow.' >"$codex_dir/SKILL.md" - - run python3 "$verifier" --repo-root "$scratch" --profile-id repo-runtime \ - --verify-clean-room "$external" --generated-dir "$source_dir" - [[ "$status" -eq 0 ]] - [[ "$output" == *'repo-runtime'* ]] - run python3 "$verifier" --repo-root "$scratch" --profile-id repo-runtime \ - --verify-clean-room "$external" --generated-dir "$codex_dir" - [[ "$status" -eq 0 ]] - [[ "$output" == *'repo-runtime'* ]] -} - -@test "L2: changed-scope keeps semantic skill scoring off the deterministic gate" { - local scratch="$BATS_TEST_TMPDIR/release-waist-root" - local scan_root="$BATS_TEST_TMPDIR/release-waist-scan" - local shallow_bats="$BATS_TEST_TMPDIR/shallow-count.bats" - local stub - - prepare_builder_root "$scratch" - write_conforming_skill "$scratch/skills/shallow-green" shallow-green \ - "'Shallow checks pass. Triggers: \"shallow green\".'" incomplete - cat >>"$scratch/docs/contracts/skill-dispositions.yaml" <<'YAML' - - skill: shallow-green - domain: "BC3 Loop" - hexagonal_role: supporting - disposition: keep - kind: skill - runtime_targets: [claude, codex] - parity_policy: required - capability_class: execution - path: skills/shallow-green/SKILL.md - aliases: [] - supersedes: null - rationale: "Fixture proves semantic scoring stays out of deterministic changed-scope checks." -YAML - mkdir -p "$scan_root" - cp -R "$scratch/skills/shallow-green" "$scan_root/shallow-green" - - cat >"$shallow_bats" <<'BATS' -#!/usr/bin/env bats -@test "shallow count is green" { true; } -BATS - run bats "$shallow_bats" - [[ "$status" -eq 0 ]] - [[ "$output" == *'shallow count is green'* ]] - - run python3 "$scratch/skills/skill-builder/scripts/scan_descriptions.py" \ - "$scan_root" --json --strict - [[ "$status" -eq 0 ]] - - for stub in generate-context-map.sh generate-skill-domain-map.sh \ - generate-registry.sh codex-sync.sh regen-codex-hashes.sh \ - validate-codex-generated-artifacts.sh audit-codex-parity.sh; do - printf '#!/usr/bin/env bash\nexit 0\n' >"$scratch/scripts/$stub" - chmod +x "$scratch/scripts/$stub" - done - - run bash "$scratch/scripts/regen-changed-scope.sh" --check \ - --file skills/shallow-green/SKILL.md - [[ "$status" -eq 0 ]] - [[ "$output" == *'changed skill structural integrity'* ]] - [[ "$output" == *'All clean. No findings.'* ]] - [[ "$output" != *'output-spec-explicit'* ]] -} - -@test "L2: changed-scope does not inspect redirect-only runtime packages" { - local scratch="$BATS_TEST_TMPDIR/redirect-scope-root" - - prepare_builder_root "$scratch" - - run bash "$scratch/scripts/regen-changed-scope.sh" --list \ - --file skills/pre-mortem/SKILL.md - [[ "$status" -eq 0 ]] - [[ "$output" != *'changed skill structural integrity'* ]] - [[ "$output" != *'heal.sh --check --strict skills/pre-mortem'* ]] - [[ "$output" == *'codex'* ]] - [[ "$output" == *'registry'* ]] -} - -@test "L0: changed-scope release waist invokes structural integrity, not prose scoring" { - grep -Fq 'skills/heal-skill/scripts/heal.sh --check --strict' \ - "$REPO_ROOT/scripts/regen-changed-scope.sh" - ! grep -Fq 'skills/heal-skill/scripts/audit.sh --strict' \ - "$REPO_ROOT/scripts/regen-changed-scope.sh" +@test "there is no parallel handwritten disposition ledger" { + [ ! -e "$REPO_ROOT/docs/contracts/skill-dispositions.yaml" ] } diff --git a/tests/scripts/sync-main-to-origin.bats b/tests/scripts/sync-main-to-origin.bats deleted file mode 100644 index d0bc55cf1..000000000 --- a/tests/scripts/sync-main-to-origin.bats +++ /dev/null @@ -1,106 +0,0 @@ -#!/usr/bin/env bats -# Regression tests for skills/evolve/scripts/sync-main-to-origin.sh (ag-6jt). -# -# The evolve-cron-rpi discovery loop diffed candidate slices against a STALE -# local `main` (the rpi worktree's local main lags origin/main), so work already -# merged to origin/main read as "open" and was re-seeded. These tests pin the -# fix: discovery's diff base must be origin/main AFTER a fetch, and local main -# must be fast-forwarded to it — never forced past a divergence. -# -# Hermetic: a throwaway bare repo plays "origin"; we advance origin/main beyond -# the local clone's main, then assert the script resolves the base to origin's -# tip and fast-forwards local main to match. - -setup() { - REPO_ROOT="$(git rev-parse --show-toplevel)" - SCRIPT="$REPO_ROOT/skills/evolve/scripts/sync-main-to-origin.sh" - TMP="$(mktemp -d)" - - export GIT_AUTHOR_NAME=t GIT_AUTHOR_EMAIL=t@t \ - GIT_COMMITTER_NAME=t GIT_COMMITTER_EMAIL=t@t - - # Bare "origin" remote. - git init -q --bare "$TMP/origin.git" - - # A seed clone we use to publish commits to origin/main. - git clone -q "$TMP/origin.git" "$TMP/seed" - ( - cd "$TMP/seed" - git checkout -q -b main - echo a > f.txt && git add f.txt && git commit -q -m "base commit" - git push -q origin main - ) - - # The "worktree" clone whose local main will go STALE. - git clone -q "$TMP/origin.git" "$TMP/work" - ( cd "$TMP/work" && git checkout -q main ) - STALE_LOCAL_MAIN="$(cd "$TMP/work" && git rev-parse main)" - - # Advance origin/main beyond the work clone's local main (simulates work - # merged to origin/main after the worktree last synced). - ( - cd "$TMP/seed" - echo b >> f.txt && git commit -q -am "merged-after-stale commit" - git push -q origin main - ) - NEW_ORIGIN_MAIN="$(cd "$TMP/seed" && git rev-parse main)" -} - -teardown() { - rm -rf "$TMP" -} - -@test "DIFF_BASE is origin/main's tip after fetch, NOT the stale local main" { - # main is not the checked-out branch in the work clone. - ( cd "$TMP/work" && git checkout -q -b feature ) - run bash -c "cd '$TMP/work' && '$SCRIPT'" - [ "$status" -eq 0 ] - [[ "$output" == *"DIFF_BASE: origin/main $NEW_ORIGIN_MAIN"* ]] - # The base must be the fresh origin tip, never the stale local main. - [[ "$output" != *"$STALE_LOCAL_MAIN"* ]] -} - -@test "local main is fast-forwarded to origin/main when main is not checked out" { - ( cd "$TMP/work" && git checkout -q -b feature ) - run bash -c "cd '$TMP/work' && '$SCRIPT'" - [ "$status" -eq 0 ] - local after - after="$(cd "$TMP/work" && git rev-parse main)" - [ "$after" = "$NEW_ORIGIN_MAIN" ] - [ "$after" != "$STALE_LOCAL_MAIN" ] -} - -@test "local main is fast-forwarded when main IS the checked-out branch" { - # work clone is on main (stale) before the sync. - run bash -c "cd '$TMP/work' && '$SCRIPT'" - [ "$status" -eq 0 ] - [[ "$output" == *"DIFF_BASE: origin/main $NEW_ORIGIN_MAIN"* ]] - local after - after="$(cd "$TMP/work" && git rev-parse HEAD)" - [ "$after" = "$NEW_ORIGIN_MAIN" ] -} - -@test "a diverged local main is refused, never force-moved" { - # Create a divergent local main commit that is NOT an ancestor of origin/main. - ( - cd "$TMP/work" - git checkout -q main - echo divergent > d.txt && git add d.txt && git commit -q -m "local-only divergent commit" - ) - local diverged - diverged="$(cd "$TMP/work" && git rev-parse main)" - run bash -c "cd '$TMP/work' && '$SCRIPT'" - [ "$status" -ne 0 ] - [[ "$output" == *"fast-forward"* ]] || [[ "$output" == *"diverged"* ]] - # Local main must be untouched (no force/reset). - local after - after="$(cd "$TMP/work" && git rev-parse main)" - [ "$after" = "$diverged" ] -} - -@test "respects a custom remote name via SYNC_MAIN_REMOTE" { - ( cd "$TMP/work" && git checkout -q -b feature && git remote rename origin upstream ) - run bash -c "cd '$TMP/work' && SYNC_MAIN_REMOTE=upstream '$SCRIPT'" - [ "$status" -eq 0 ] - [[ "$output" == *"DIFF_BASE: upstream/main $NEW_ORIGIN_MAIN"* ]] -} diff --git a/tests/scripts/test-closeout-gate.sh b/tests/scripts/test-closeout-gate.sh deleted file mode 100755 index 2db5c9e23..000000000 --- a/tests/scripts/test-closeout-gate.sh +++ /dev/null @@ -1,132 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -script_path="$repo_root/scripts/check-closeout-gate.sh" - -fail() { - echo "FAIL: $*" >&2 - exit 1 -} - -make_repo() { - local dir="$1" - git -C "$dir" init -q - git -C "$dir" config user.email "closeout-gate@example.test" - git -C "$dir" config user.name "Closeout Gate Test" - mkdir -p "$dir/.agents/rpi/runs/run-closeout" - cat > "$dir/.agents/rpi/execution-packet.json" <<'JSON' -{ - "run_id": "latest-closeout", - "epic_id": "ag-closeout", - "bead_id": "ag-closeout", - "tracking_repo_root": "__ROOT__", - "beads_dir": "__ROOT__/.beads", - "pr_url": "https://github.com/example/agentops/pull/204", - "merge_commit": "abc123", - "proof_updated_at": "2026-04-29T00:00:00Z", - "proof_artifacts": [ - ".agents/daemon/ledger.jsonl", - "docs/daemon-migration.md" - ] -} -JSON - python3 - "$dir/.agents/rpi/execution-packet.json" "$dir" <<'PY' -import pathlib -import sys -path = pathlib.Path(sys.argv[1]) -root = sys.argv[2] -path.write_text(path.read_text().replace("__ROOT__", root)) -PY - cat > "$dir/.agents/rpi/runs/run-closeout/execution-packet.json" <<'JSON' -{ - "run_id": "run-closeout", - "proof_artifacts": [ - ".agents/rpi/runs/run-closeout/execution-packet.json" - ] -} -JSON - git -C "$dir" add . - git -C "$dir" commit -q -m "fixture closeout proof" -} - -assert_json_value() { - local json_file="$1" - local expr="$2" - python3 - "$json_file" "$expr" <<'PY' -import json -import sys -path, expr = sys.argv[1:3] -data = json.load(open(path)) -if not eval(expr, {"data": data}): - raise SystemExit(1) -PY -} - -test_passes_with_proof_and_clean_worktree() { - local dir - dir="$(mktemp -d "${TMPDIR:-/tmp}/closeout-clean.XXXXXX")" - make_repo "$dir" - local out - out="$(mktemp "${TMPDIR:-/tmp}/closeout-clean-out.XXXXXX")" - "$script_path" --root "$dir" --json > "$out" - assert_json_value "$out" "data['result'] == 'PASS'" - assert_json_value "$out" "data['closure_replay']['proof_ref_count'] == 2" - assert_json_value "$out" "data['provenance']['packet_count'] >= 1" - assert_json_value "$out" "data['provenance']['refs'][0]['pr_url'] == 'https://github.com/example/agentops/pull/204'" - assert_json_value "$out" "data['worktree']['clean'] is True" -} - -test_fails_dirty_by_default() { - local dir - dir="$(mktemp -d "${TMPDIR:-/tmp}/closeout-dirty.XXXXXX")" - make_repo "$dir" - printf 'dirty\n' > "$dir/dirty.txt" - local out err - out="$(mktemp "${TMPDIR:-/tmp}/closeout-dirty-out.XXXXXX")" - err="$(mktemp "${TMPDIR:-/tmp}/closeout-dirty-err.XXXXXX")" - if "$script_path" --root "$dir" --json > "$out" 2>"$err"; then - fail "dirty worktree should fail by default" - fi - assert_json_value "$out" "data['result'] == 'FAIL'" - assert_json_value "$out" "data['worktree']['dirty_count'] == 1" -} - -test_allow_dirty_reports_but_passes() { - local dir - dir="$(mktemp -d "${TMPDIR:-/tmp}/closeout-allow-dirty.XXXXXX")" - make_repo "$dir" - printf 'dirty\n' > "$dir/dirty.txt" - local out - out="$(mktemp "${TMPDIR:-/tmp}/closeout-allow-dirty-out.XXXXXX")" - "$script_path" --root "$dir" --allow-dirty --json > "$out" - assert_json_value "$out" "data['result'] == 'PASS'" - assert_json_value "$out" "data['worktree']['clean'] is False" - assert_json_value "$out" "data['worktree']['allow_dirty'] is True" -} - -test_fails_without_proof_refs() { - local dir - dir="$(mktemp -d "${TMPDIR:-/tmp}/closeout-no-proof.XXXXXX")" - git -C "$dir" init -q - git -C "$dir" config user.email "closeout-gate@example.test" - git -C "$dir" config user.name "Closeout Gate Test" - mkdir -p "$dir/.agents/rpi" - echo '{"run_id":"no-proof"}' > "$dir/.agents/rpi/execution-packet.json" - git -C "$dir" add . - git -C "$dir" commit -q -m "fixture without proof" - local out - out="$(mktemp "${TMPDIR:-/tmp}/closeout-no-proof-out.XXXXXX")" - if "$script_path" --root "$dir" --json > "$out"; then - fail "missing proof refs should fail" - fi - assert_json_value "$out" "data['result'] == 'FAIL'" - assert_json_value "$out" "'no RPI execution-packet proof_refs found' in data['errors']" -} - -test_passes_with_proof_and_clean_worktree -test_fails_dirty_by_default -test_allow_dirty_reports_but_passes -test_fails_without_proof_refs - -echo "PASS: check-closeout-gate.sh" diff --git a/tests/scripts/test-codex-backbone-prompts.sh b/tests/scripts/test-codex-backbone-prompts.sh deleted file mode 100755 index d6dd0728f..000000000 --- a/tests/scripts/test-codex-backbone-prompts.sh +++ /dev/null @@ -1,168 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -SCRIPT="$ROOT/scripts/validate-codex-backbone-prompts.sh" - -PASS=0 -FAIL=0 - -pass() { echo "PASS: $1"; PASS=$((PASS + 1)); } -fail() { echo "FAIL: $1"; FAIL=$((FAIL + 1)); } - -[[ -x "$SCRIPT" ]] || { - echo "FAIL: missing script: $SCRIPT" >&2 - exit 1 -} - -TMP_DIR="$(mktemp -d)" -trap 'rm -rf "$TMP_DIR"' EXIT - -write_prompt() { - local path="$1" - local name="$2" - local body="$3" - mkdir -p "$(dirname "$path")" - cat > "$path" <<EOF -# $name - -$body -EOF -} - -setup_fixture() { - local fixture="$1" - mkdir -p \ - "$fixture/skills-codex/alpha" \ - "$fixture/skills-codex/beta" \ - "$fixture/skills-codex-overrides" - - write_prompt "$fixture/skills-codex/alpha/prompt.md" "alpha" "Alpha prompt. - -## Codex Execution Profile - -1. Route issues into downstream execution. -2. Keep handoff artifacts durable. - -## Guardrails - -1. Prefer concise, exact execution notes. -2. Keep behavior findings-first." - - write_prompt "$fixture/skills-codex/beta/prompt.md" "beta" "Beta prompt. - -## Codex Execution Profile - -1. Report exact validations run. -2. Require remote verification. - -## Guardrails - -1. Do not skip failing gates. -2. Recover until push succeeds." - - cat > "$fixture/skills-codex-overrides/catalog.json" <<'EOF' -{ - "version": 1, - "skills": [ - { - "name": "alpha", - "treatment": "bespoke", - "wave": "backbone", - "reason": "fixture", - "operator_contract": { - "required_sections": ["## Codex Execution Profile", "## Guardrails"], - "required_markers": ["Route issues into downstream execution.", "Keep behavior findings-first."] - } - }, - { - "name": "beta", - "treatment": "bespoke", - "wave": "backbone", - "reason": "fixture", - "operator_contract": { - "required_sections": ["## Codex Execution Profile", "## Guardrails"], - "required_markers": ["Report exact validations run.", "Recover until push succeeds."] - } - } - ] -} -EOF -} - -test_fixture_passes() { - local fixture="$TMP_DIR/pass" - setup_fixture "$fixture" - - if bash "$SCRIPT" --repo-root "$fixture" >/dev/null; then - pass "passes when generated backbone prompts satisfy the catalog contract" - else - fail "should validate a matching backbone prompt fixture" - fi -} - -test_fails_when_marker_missing() { - local fixture="$TMP_DIR/missing-marker" - setup_fixture "$fixture" - python3 - <<'PY' "$fixture/skills-codex/alpha/prompt.md" -from pathlib import Path -path = Path(__import__("sys").argv[1]) -path.write_text(path.read_text().replace("Route issues into downstream execution.\n", "")) -PY - - if bash "$SCRIPT" --repo-root "$fixture" >/dev/null 2>&1; then - fail "should fail when a required generated-prompt marker is missing" - else - pass "fails when a required generated-prompt marker is missing" - fi -} - -test_fails_when_sections_out_of_order() { - local fixture="$TMP_DIR/section-order" - setup_fixture "$fixture" - python3 - <<'PY' "$fixture/skills-codex/beta/prompt.md" -from pathlib import Path -path = Path(__import__("sys").argv[1]) -path.write_text("""# beta - -Beta prompt. - -## Guardrails - -1. Do not skip failing gates. -2. Recover until push succeeds. - -## Codex Execution Profile - -1. Report exact validations run. -2. Require remote verification. -""") -PY - - if bash "$SCRIPT" --repo-root "$fixture" >/dev/null 2>&1; then - fail "should fail when required sections are out of order" - else - pass "fails when required sections are out of order" - fi -} - -test_repo_catalog_passes() { - if bash "$SCRIPT" --repo-root "$ROOT" >/dev/null 2>&1; then - pass "repository backbone prompts validate end to end" - else - fail "repository backbone prompts should validate end to end" - fi -} - -echo "== test-codex-backbone-prompts ==" -test_fixture_passes -test_fails_when_marker_missing -test_fails_when_sections_out_of_order -test_repo_catalog_passes - -echo "" -echo "Results: $PASS PASS, $FAIL FAIL" -if [[ "$FAIL" -gt 0 ]]; then - exit 1 -fi -exit 0 diff --git a/tests/scripts/test-goal-failure-taxonomy.sh b/tests/scripts/test-goal-failure-taxonomy.sh deleted file mode 100755 index e42653b63..000000000 --- a/tests/scripts/test-goal-failure-taxonomy.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -SCRIPT="$ROOT/scripts/goal-failure-taxonomy.sh" - -TMP_DIR="$(mktemp -d)" -trap 'rm -rf "$TMP_DIR"' EXIT - -INPUT="$TMP_DIR/fitness.json" -cat > "$INPUT" <<'JSON' -{ - "goals": [ - {"goal_id":"release-security-gate","result":"fail","description":"release gate missing"}, - {"goal_id":"go-cli-builds","result":"fail","description":"build fails"}, - {"goal_id":"go-coverage-floor","result":"fail","description":"coverage low"}, - {"goal_id":"goal-quality","result":"fail","description":"bad goal quality"}, - {"goal_id":"custom-unknown-check","result":"fail","description":"unknown class"}, - {"goal_id":"go-vet-clean","result":"pass","description":"passes"} - ] -} -JSON - -OUT="$TMP_DIR/out.json" -bash "$SCRIPT" "$INPUT" > "$OUT" -jq -e . "$OUT" >/dev/null - -[[ "$(jq -r '.summary.total_failing' "$OUT")" == "5" ]] -[[ "$(jq -r '.summary.by_category.security' "$OUT")" == "1" ]] -[[ "$(jq -r '.summary.by_category.reliability' "$OUT")" == "1" ]] -[[ "$(jq -r '.summary.by_category.quality' "$OUT")" == "1" ]] -[[ "$(jq -r '.summary.by_category.governance' "$OUT")" == "1" ]] -[[ "$(jq -r '.summary.by_category.other' "$OUT")" == "1" ]] - -echo "PASS: goal failure taxonomy classification" diff --git a/tests/scripts/test-validate-local.sh b/tests/scripts/test-validate-local.sh old mode 100755 new mode 100644 index 67a13790e..b040d229e --- a/tests/scripts/test-validate-local.sh +++ b/tests/scripts/test-validate-local.sh @@ -3,96 +3,20 @@ set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" SCRIPT="$ROOT/scripts/validate-local.sh" -TMP_DIR="" - -PASS=0 -FAIL=0 - -pass() { echo "PASS: $1"; PASS=$((PASS + 1)); } -fail() { echo "FAIL: $1"; FAIL=$((FAIL + 1)); } - -cleanup() { - if [[ -n "$TMP_DIR" && -d "$TMP_DIR" ]]; then - rm -rf "$TMP_DIR" - fi -} -trap cleanup EXIT - -assert_contains() { - local pattern="$1" - local label="$2" - - if rg -q "$pattern" "$SCRIPT"; then - pass "$label" - else - fail "$label" - fi -} - -echo "== test-validate-local ==" -assert_contains 'install-dev-hooks\.sh' "validate-local references repo hook bootstrap" -assert_contains 'core\.hooksPath' "validate-local warns when core.hooksPath is not .githooks" -assert_contains 'Manual Local Validation' "validate-local describes itself as manual validation" -assert_contains 'gate check' "validate-local wraps the Go push gate" -assert_contains 'agentops-validate-local\.lock' "validate-local serializes concurrent runs" - TMP_DIR="$(mktemp -d)" -FAKE_REPO="$TMP_DIR/repo" -mkdir -p "$FAKE_REPO/scripts" -cp "$SCRIPT" "$FAKE_REPO/scripts/validate-local.sh" -chmod +x "$FAKE_REPO/scripts/validate-local.sh" -cat > "$FAKE_REPO/scripts/ao" <<'EOF' +trap 'rm -rf "$TMP_DIR"' EXIT + +! rg -n 'install-dev-hooks|core\.hooksPath|claude|lock|retry|verdict' "$SCRIPT" + +mkdir -p "$TMP_DIR/repo/scripts" "$TMP_DIR/repo/cli/bin" +cp "$SCRIPT" "$TMP_DIR/repo/scripts/validate-local.sh" +cat > "$TMP_DIR/repo/cli/bin/ao" <<'EOF' #!/usr/bin/env bash -set -euo pipefail -# Fake ao for lock serialization test — sleep so the lock is held. -if [[ "${1:-}" == "gate" ]]; then - sleep 5 - exit 0 -fi -exit 0 +printf '%s\n' "$*" > "${AO_CAPTURE:?}" EOF -chmod +x "$FAKE_REPO/scripts/ao" -mkdir -p "$FAKE_REPO/cli/bin" -ln -sf ../../scripts/ao "$FAKE_REPO/cli/bin/ao" +chmod +x "$TMP_DIR/repo/scripts/validate-local.sh" "$TMP_DIR/repo/cli/bin/ao" -git -C "$TMP_DIR" init repo >/dev/null 2>&1 -git -C "$FAKE_REPO" config core.hooksPath .githooks +AO_CAPTURE="$TMP_DIR/args" bash "$TMP_DIR/repo/scripts/validate-local.sh" --scope staged --full +grep -qx 'gate check --full --scope staged' "$TMP_DIR/args" -( - cd "$FAKE_REPO" - bash scripts/validate-local.sh --skip-claude >/dev/null 2>&1 -) & -FIRST_PID=$! - -LOCK_DIR="$FAKE_REPO/.git/agentops-validate-local.lock" -for _ in $(seq 1 50); do - [[ -d "$LOCK_DIR" ]] && break - sleep 0.1 -done - -if [[ -d "$LOCK_DIR" ]]; then - set +e - SECOND_OUTPUT="$( - cd "$FAKE_REPO" && - bash scripts/validate-local.sh --skip-claude 2>&1 - )" - SECOND_STATUS=$? - set -e - - if [[ "$SECOND_STATUS" -eq 1 && "$SECOND_OUTPUT" == *"already running"* ]]; then - pass "validate-local rejects concurrent runs with a clear lock error" - else - fail "validate-local rejects concurrent runs with a clear lock error" - fi -else - fail "validate-local creates its repo-scoped lock before running the shared gate" -fi - -wait "$FIRST_PID" - -echo "" -echo "Results: $PASS PASS, $FAIL FAIL" -if [[ "$FAIL" -gt 0 ]]; then - exit 1 -fi -exit 0 +echo "PASS: validate-local is a thin deterministic gate wrapper" diff --git a/tests/scripts/validate-context-map-drift.bats b/tests/scripts/validate-context-map-drift.bats deleted file mode 100644 index 93dd3e5e8..000000000 --- a/tests/scripts/validate-context-map-drift.bats +++ /dev/null @@ -1,65 +0,0 @@ -#!/usr/bin/env bats - -setup() { - REPO_ROOT="$(cd "$BATS_TEST_DIRNAME/../.." && pwd)" - SCRIPT="$REPO_ROOT/scripts/validate-context-map-drift.sh" - TMP="$(mktemp -d)" - export CONTEXT_MAP="$TMP/context-map.md" - export GENERATOR="$TMP/generate-context-map.sh" -} - -teardown() { - rm -rf "$TMP" -} - -make_generator() { - local content="$1" - cat >"$GENERATOR" <<EOF -#!/usr/bin/env bash -printf '%s\n' '$content' >"\$CONTEXT_MAP" -EOF - chmod +x "$GENERATOR" -} - -@test "passes when current working copy already matches generator output" { - printf '%s\n' "generated" >"$CONTEXT_MAP" - make_generator "generated" - - run bash "$SCRIPT" - - [ "$status" -eq 0 ] - [ "$(cat "$CONTEXT_MAP")" = "generated" ] -} - -@test "fails on drift and restores the original working copy" { - printf '%s\n' "stale" >"$CONTEXT_MAP" - make_generator "generated" - - run bash "$SCRIPT" - - [ "$status" -eq 1 ] - [[ "$output" == *"Context map drift detected"* ]] - [ "$(cat "$CONTEXT_MAP")" = "stale" ] -} - -@test "fails when matching generated context map is not committed" { - repo="$TMP/repo" - mkdir -p "$repo" - git -C "$repo" init -q - printf '%s\n' "committed" >"$repo/context-map.md" - git -C "$repo" add context-map.md - git -C "$repo" -c user.name=test -c user.email=test@example.com commit -q -m init - printf '%s\n' "generated" >"$repo/context-map.md" - - cat >"$repo/generate-context-map.sh" <<'EOF' -#!/usr/bin/env bash -printf '%s\n' generated >"$CONTEXT_MAP" -EOF - chmod +x "$repo/generate-context-map.sh" - - run bash -c "cd '$repo' && CONTEXT_MAP=context-map.md GENERATOR=./generate-context-map.sh '$SCRIPT'" - - [ "$status" -eq 1 ] - [[ "$output" == *"matches the generator but has uncommitted changes"* ]] - [ "$(cat "$repo/context-map.md")" = "generated" ] -} diff --git a/tests/scripts/validate-go-fast.bats b/tests/scripts/validate-go-fast.bats index 5e8921720..7e8f6ce26 100644 --- a/tests/scripts/validate-go-fast.bats +++ b/tests/scripts/validate-go-fast.bats @@ -142,7 +142,7 @@ GO #!/usr/bin/env bash case "$*" in *"rev-parse --git-dir"*) echo ".git"; exit 0 ;; - *"diff --name-only --cached"*) echo "cli/internal/adapters/agentsdoctor/doctor.go"; exit 0 ;; + *"diff --name-only --cached"*) echo "cli/internal/doctor/doctor.go"; exit 0 ;; *"diff --name-only"*) echo ""; exit 0 ;; *"ls-files --others"*) echo ""; exit 0 ;; esac @@ -156,7 +156,7 @@ GIT run bash "$SCRIPT" --scope worktree [ "$status" -eq 0 ] grep -q -- '-run ^(' "$GO_LOG" - grep -q -- './internal/adapters/agentsdoctor' "$GO_LOG" + grep -q -- './internal/doctor' "$GO_LOG" } @test "validate-go-fast.sh skips deleted package directories" { diff --git a/tests/scripts/validate-release-tag-full-ci.bats b/tests/scripts/validate-release-tag-full-ci.bats index fdd19fe46..972770de9 100644 --- a/tests/scripts/validate-release-tag-full-ci.bats +++ b/tests/scripts/validate-release-tag-full-ci.bats @@ -48,12 +48,9 @@ setup() { } @test "summary uses a selective toJson(needs) allowlist, not a blind skip check" { - # Post-rebuild (ag-877): the 67→10 collapse removed the standalone PR-only - # jobs (validate-pr-evidence-claims, lint-evidence-lines-advisory) — AP#7 - # Evidence verification and the Evidence-line lint are now STEPS (in summary - # and process-hygiene respectively), gated by github.event_name. So no - # top-level job needs allowlisting on release tags. The selective mechanism - # itself must still exist: summary inspects toJson(needs) against an + # No PR-only lifecycle or evidence-admission job is part of this workflow, + # so no top-level job needs allowlisting on release tags. The selective + # mechanism itself must still exist: summary inspects toJson(needs) against an # allowed_skips set rather than blindly failing on any skip. run grep -F "toJson(needs)" "$WORKFLOW_PATH" [ "$status" -eq 0 ] diff --git a/tests/scripts/validate-skill-disposition-schema.bats b/tests/scripts/validate-skill-disposition-schema.bats deleted file mode 100644 index 4075e609e..000000000 --- a/tests/scripts/validate-skill-disposition-schema.bats +++ /dev/null @@ -1,191 +0,0 @@ -#!/usr/bin/env bats -# -# Tests for the artifact-classification schema validator -# (scripts/validate-skill-disposition-schema.sh, ag-4akl8 S0). -# -# The validator enforces the v4 additive schema on -# docs/contracts/skill-dispositions.yaml: every active `- skill:` row and every -# `workflows:` entry carries a `kind` in the closed enum {skill, workflow, loop} -# and a `capability_class` in the closed capability enum. A row with an unknown -# `kind` (or capability_class) is rejected by NAME so the offending row is -# obvious. -# -# Heredocs build a throwaway fixture yaml in BATS_TEST_TMPDIR so the canonical -# ledger is never mutated; DISP_YAML reaches the validator via the environment. - -setup() { - REPO_ROOT="$(cd "$BATS_TEST_DIRNAME/../.." && pwd)" - export REPO_ROOT - VALIDATOR="$REPO_ROOT/scripts/validate-skill-disposition-schema.sh" -} - -@test "canonical ledger passes the schema validator" { - run bash "$VALIDATOR" - [ "$status" -eq 0 ] -} - -@test "an unknown kind value is rejected and the row is named" { - fixture="$BATS_TEST_TMPDIR/disp.yaml" - cat > "$fixture" <<'YAML' -historical: {} -workflows: {} -dispositions: - - skill: ok-skill - domain: "BC1 Corpus" - hexagonal_role: supporting - disposition: keep - kind: skill - runtime_targets: [claude, codex] - parity_policy: required - capability_class: corpus - path: skills/ok-skill/SKILL.md - aliases: [] - supersedes: null - rationale: "ok" - - skill: bad-kind-skill - domain: "BC1 Corpus" - hexagonal_role: supporting - disposition: keep - kind: gizmo - runtime_targets: [claude, codex] - parity_policy: required - capability_class: corpus - path: skills/bad-kind-skill/SKILL.md - aliases: [] - supersedes: null - rationale: "bad" -YAML - DISP_YAML="$fixture" run bash "$VALIDATOR" - [ "$status" -ne 0 ] - [[ "$output" == *"bad-kind-skill"* ]] - [[ "$output" == *"gizmo"* ]] -} - -@test "an unknown capability_class value is rejected and the row is named" { - fixture="$BATS_TEST_TMPDIR/disp.yaml" - cat > "$fixture" <<'YAML' -historical: {} -workflows: {} -dispositions: - - skill: bad-cap-skill - domain: "BC1 Corpus" - hexagonal_role: supporting - disposition: keep - kind: skill - runtime_targets: [claude, codex] - parity_policy: required - capability_class: nonsense - path: skills/bad-cap-skill/SKILL.md - aliases: [] - supersedes: null - rationale: "bad" -YAML - DISP_YAML="$fixture" run bash "$VALIDATOR" - [ "$status" -ne 0 ] - [[ "$output" == *"bad-cap-skill"* ]] -} - -@test "a missing required additive field is rejected and the row is named" { - fixture="$BATS_TEST_TMPDIR/disp.yaml" - cat > "$fixture" <<'YAML' -historical: {} -workflows: {} -dispositions: - - skill: missing-field-skill - domain: "BC1 Corpus" - hexagonal_role: supporting - disposition: keep - kind: skill - capability_class: corpus - path: skills/missing-field-skill/SKILL.md - rationale: "missing runtime_targets + parity_policy" -YAML - DISP_YAML="$fixture" run bash "$VALIDATOR" - [ "$status" -ne 0 ] - [[ "$output" == *"missing-field-skill"* ]] -} - -@test "workflows entries are validated for kind too" { - fixture="$BATS_TEST_TMPDIR/disp.yaml" - cat > "$fixture" <<'YAML' -historical: {} -workflows: - bad-workflow: - kind: contraption - domain: "BC3 Loop" - hexagonal_role: driving-adapter - runtime_targets: [claude] - parity_policy: exempt - capability_class: orchestration - aliases: [] - path: .claude/workflows/bad-workflow.js - rationale: "bad" -dispositions: [] -YAML - DISP_YAML="$fixture" run bash "$VALIDATOR" - [ "$status" -ne 0 ] - [[ "$output" == *"bad-workflow"* ]] -} - -# --- BC6 + additive-no-rename scenarios (ag-4akl8 S0) --------------------------- - -@test "BC6 passes the bounded-context drift gate with six contexts" { - run bash "$REPO_ROOT/scripts/check-bounded-contexts-drift.sh" - [ "$status" -eq 0 ] - [[ "$output" == *"6 BCs"* ]] - # BC6 must appear in BOTH registry docs (the gate fails otherwise, but - # assert the membership directly so the scenario is pinned). - grep -q "BC6 Orchestration" "$REPO_ROOT/docs/reference/agentops-skill-domain-map.md" - grep -q "BC6 Orchestration" "$REPO_ROOT/docs/reference/agentops-hexagonal-architecture-map.md" -} - -@test "BC6 with no same-named cli command does not trip SKU coverage" { - # The sku_catalog coverage check must pass even though BC6 members - # (ntm/swarm/agent-mail/...) own no same-named `ao` subcommand: cli-command - # coverage is conditional (carve-out), BC-skill coverage is the floor. - run python3 - <<'PY' -import os, sys -sys.path.insert(0, os.path.join(os.environ["REPO_ROOT"], "scripts", "lib")) -import sku_catalog -assert "BC6" in sku_catalog.BOUNDED_CONTEXTS, "BC6 not in the enum" -# Synthesize a catalog where BC6 has an active skill but no cli-command SKU. -catalog = { - "capabilities": [ - {"type": "skill", "name": "ntm", "status": "active", "bounded_context": "BC6"}, - {"type": "skill", "name": "compile", "status": "active", "bounded_context": "BC1"}, - {"type": "skill", "name": "review", "status": "active", "bounded_context": "BC2"}, - {"type": "skill", "name": "rpi", "status": "active", "bounded_context": "BC3"}, - {"type": "skill", "name": "skill-builder", "status": "active", "bounded_context": "BC4"}, - {"type": "skill", "name": "push", "status": "active", "bounded_context": "BC5"}, - # BC1 owns a cli command; BC6 owns none. - {"type": "cli-command", "name": "compile", "bounded_context": "BC1"}, - ] -} -# Fill loop-move coverage so the only thing under test is BC coverage. -for move, cands in sku_catalog.LOOP_MOVES.items(): - catalog["capabilities"].append( - {"type": "skill", "name": cands[0], "status": "active", "bounded_context": "BC3"} - ) -failures = sku_catalog.check_coverage(catalog) -bc6_fails = [f for f in failures if "BC6" in f] -assert not bc6_fails, f"BC6 tripped coverage: {bc6_fails}" -print("ok") -PY - [ "$status" -eq 0 ] - [[ "$output" == *"ok"* ]] -} - -@test "the additive change leaves renamed-consumer parsers byte-untouched" { - # resolve-skill-path.sh, skills_retire.go, heal.sh must match origin/main. - for f in \ - scripts/lib/resolve-skill-path.sh \ - cli/cmd/ao/skills_retire.go \ - skills/heal-skill/scripts/heal.sh \ - skills-codex/heal-skill/scripts/heal.sh; do - run git -C "$REPO_ROOT" diff --quiet origin/main -- "$f" - [ "$status" -eq 0 ] - done - # The filename + row key + domain-string format are unchanged. - grep -q "^ - skill:" "$REPO_ROOT/docs/contracts/skill-dispositions.yaml" - grep -q 'domain:.*"BC6 Orchestration"' "$REPO_ROOT/docs/contracts/skill-dispositions.yaml" -} diff --git a/tests/scripts/validate-skill-flow.bats b/tests/scripts/validate-skill-flow.bats deleted file mode 100644 index decf9868d..000000000 --- a/tests/scripts/validate-skill-flow.bats +++ /dev/null @@ -1,153 +0,0 @@ -#!/usr/bin/env bats -# Acceptance surface for scripts/validate-skill-flow.sh — the skill-flow -# connectivity gate (follow-up to audit-skill-metadata.sh, which deferred the -# `consumes` vocabulary and connectivity checks). -# -# Contract: docs/contracts/skill-flow.md -# Fixtures live in a tmp tree so the repo-wide SKILL.md scanners never see them. - -setup() { - REPO_ROOT="$(cd "$BATS_TEST_DIRNAME/../.." && pwd)" - SCRIPT="$REPO_ROOT/scripts/validate-skill-flow.sh" - ROOT="$(mktemp -d)" - ALLOW="$(mktemp)" - : > "$ALLOW" # empty allowlist by default -} - -teardown() { - [ -n "${ROOT:-}" ] && rm -rf "$ROOT" - [ -n "${ALLOW:-}" ] && rm -f "$ALLOW" -} - -# mkskill <root> <name> [consumes_csv] [produces_csv] [ctx_with] [mdeps_csv] -# Writes a minimal valid SKILL.md frontmatter. CSV args are comma-separated. -mkskill() { - local root="$1" name="$2" consumes="${3:-}" produces="${4:-}" ctx="${5:-}" mdeps="${6:-}" - mkdir -p "$root/$name" - { - echo "---" - echo "name: $name" - echo "description: fixture skill $name" - echo "hexagonal_role: supporting" - echo "practices:" - echo "- tdd" - if [ -n "$consumes" ]; then - echo "consumes:" - IFS=',' read -ra items <<< "$consumes" - for i in "${items[@]}"; do echo "- $i"; done - fi - if [ -n "$produces" ]; then - echo "produces:" - IFS=',' read -ra items <<< "$produces" - for i in "${items[@]}"; do echo "- $i"; done - fi - if [ -n "$ctx" ]; then - echo "context_rel:" - echo "- kind: customer-of" - echo " with: $ctx" - fi - if [ -n "$mdeps" ]; then - echo "metadata:" - echo " dependencies:" - IFS=',' read -ra items <<< "$mdeps" - for i in "${items[@]}"; do echo " - $i"; done - fi - echo "---" - echo "# $name" - } > "$root/$name/SKILL.md" -} - -run_gate() { run bash "$SCRIPT" --skills-root "$ROOT" --allowlist "$ALLOW" "$@"; } - -@test "gate exists and is executable" { - [ -f "$SCRIPT" ] - [ -x "$SCRIPT" ] -} - -@test "two skills connected via context_rel -> PASS" { - mkskill "$ROOT" alpha "" "" beta "" - mkskill "$ROOT" beta - # beta is referenced by alpha so it is connected; alpha references beta. - run_gate - [ "$status" -eq 0 ] - [[ "$output" == *"skill flow is connected"* ]] -} - -@test "consumes a whitelisted external input -> PASS (not an orphan target failure)" { - mkskill "$ROOT" alpha "repo-context" "" beta "" - mkskill "$ROOT" beta - run_gate - [ "$status" -eq 0 ] -} - -@test "consumes an artifact produced by another skill -> vocabulary PASS" { - mkskill "$ROOT" producer "" "git-changes" alpha "" - mkskill "$ROOT" alpha "git-changes" "" producer "" - run_gate - [ "$status" -eq 0 ] -} - -@test "consumes a dangling token -> FAIL with consumes-vocabulary finding" { - mkskill "$ROOT" alpha "totally-bogus" "" beta "" - mkskill "$ROOT" beta - run_gate - [ "$status" -eq 1 ] - [[ "$output" == *"consumes-vocabulary"* ]] - [[ "$output" == *"totally-bogus"* ]] - [[ "$output" == *"alpha/SKILL.md"* ]] -} - -@test "metadata.dependencies pointing at non-skill -> FAIL" { - mkskill "$ROOT" alpha "" "" "" "ghost" - mkskill "$ROOT" beta "" "" alpha "" - run_gate - [ "$status" -eq 1 ] - [[ "$output" == *"metadata-dependencies"* ]] - [[ "$output" == *"ghost"* ]] -} - -@test "un-allowlisted orphan -> FAIL" { - mkskill "$ROOT" alpha "" "" beta "" - mkskill "$ROOT" beta - mkskill "$ROOT" lonely "repo-context" "result.json" # zero skill edges - run_gate - [ "$status" -eq 1 ] - [[ "$output" == *"[orphan] lonely/SKILL.md"* ]] -} - -@test "allowlisted orphan -> PASS" { - mkskill "$ROOT" alpha "" "" beta "" - mkskill "$ROOT" beta - mkskill "$ROOT" lonely "repo-context" "result.json" - echo "lonely # boundary leaf" > "$ALLOW" - run_gate - [ "$status" -eq 0 ] -} - -@test "metadata.dependencies edge alone counts as connectivity" { - # alpha has no consumes/context_rel; only metadata.dependencies -> beta. - mkskill "$ROOT" alpha "" "" "" "beta" - mkskill "$ROOT" beta - run_gate - [ "$status" -eq 0 ] -} - -@test "--json emits machine-readable verdict with failures and orphans" { - mkskill "$ROOT" alpha "totally-bogus" "" beta "" - mkskill "$ROOT" beta - run bash "$SCRIPT" --skills-root "$ROOT" --allowlist "$ALLOW" --json - [ "$status" -eq 1 ] - echo "$output" | jq empty - [ "$(echo "$output" | jq -r '.verdict')" = "FAIL" ] - [ "$(echo "$output" | jq -r '.failures[0].kind')" = "consumes-vocabulary" ] - [ "$(echo "$output" | jq -r '.failures[0].skill')" = "alpha" ] -} - -@test "consumes vs metadata.dependencies disagreement is reported, not fatal" { - mkskill "$ROOT" alpha "beta" "" "" "gamma" - mkskill "$ROOT" beta - mkskill "$ROOT" gamma - run_gate - [ "$status" -eq 0 ] - [[ "$output" == *"consumes(skills) != metadata.dependencies"* ]] -} diff --git a/tests/scripts/verified-rebase.bats b/tests/scripts/verified-rebase.bats deleted file mode 100644 index 4e87aa39e..000000000 --- a/tests/scripts/verified-rebase.bats +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env bats -# L2 tests for scripts/verified-rebase.sh (soc-e9n6) — proves the post-condition -# checks that catch the `git rebase --continue` silent-failure pathology. - -setup() { - source "$(git rev-parse --show-toplevel)/lib/bats-common.bash" - REPO_ROOT="$(bats_repo_root)" - SCRIPT="$REPO_ROOT/scripts/verified-rebase.sh" - TMP="$(mktemp -d)" - bats_init_repo "$TMP" # cd + git init + deterministic identity -} - -# cd out of $TMP before removing it, and treat cleanup as best-effort, so a -# transient hold on a file in $TMP never fails the test (soc-72gkw). -teardown() { cd / 2>/dev/null || true; rm -rf "$TMP" 2>/dev/null || true; } - -@test "verified-rebase: missing arg exits 2 with usage" { - run bash "$SCRIPT" - [ "$status" -eq 2 ] - [[ "$output" == *"usage"* ]] -} - -@test "verified-rebase: passes when no rebase and HEAD matches expected" { - echo a > f && git add f && git commit -qm "the head commit" - run bash "$SCRIPT" "the head commit" - [ "$status" -eq 0 ] - [[ "$output" == *"OK"* ]] -} - -@test "verified-rebase: FAILS when HEAD subject does not match (dropped-commit catch)" { - echo a > f && git add f && git commit -qm "actual subject" - run bash "$SCRIPT" "expected subject" - [ "$status" -eq 1 ] - [[ "$output" == *"!= expected"* ]] -} - -@test "verified-rebase: completes + verifies a real rebase --continue with a conflict" { - echo a > f && git add f && git commit -qm base - base="$(git rev-parse --abbrev-ref HEAD)" - git checkout -q -b topic - echo topic-change > f && git add f && git commit -qm "topic work" - git checkout -q "$base" - echo base-change > f && git add f && git commit -qm "base work" - git checkout -q topic - # Rebase topic onto base → conflict on f. - run git rebase "$base" - [ "$status" -ne 0 ] # conflict expected - # Resolve the conflict, then verified-rebase continues + verifies HEAD. - echo resolved > f && git add f - run bash "$SCRIPT" "topic work" - [ "$status" -eq 0 ] - [[ "$output" == *"OK"* ]] - # And the rebase is genuinely finished. - [ ! -d "$(git rev-parse --git-dir)/rebase-merge" ] -} diff --git a/tests/scripts/verify-init.bats b/tests/scripts/verify-init.bats deleted file mode 100644 index 9b9e37f32..000000000 --- a/tests/scripts/verify-init.bats +++ /dev/null @@ -1,321 +0,0 @@ -#!/usr/bin/env bats -# End-to-end coverage for `ao verify init` — the portable pre-push verdict -# ratchet (age-rk3r.6). Uses the REAL ao binary + a real bare remote so -# `git push` fires the installed hook exactly as it would in a stranger repo. - -setup_file() { - ROOT="$(cd "$BATS_TEST_DIRNAME/../.." && pwd)" - AO="$BATS_FILE_TMPDIR/ao" - ( cd "$ROOT/cli" && go build -o "$AO" ./cmd/ao ) || { - echo "failed to build ao for the verify-init bats" >&2 - return 1 - } - export AO -} - -setup() { - AO="$BATS_FILE_TMPDIR/ao" - REPO="$BATS_TEST_TMPDIR/work" - REMOTE="$BATS_TEST_TMPDIR/remote.git" - git init -q --initial-branch=main "$REPO" - git init -q --bare "$REMOTE" - git -C "$REPO" remote add origin "$REMOTE" - git -C "$REPO" config user.email t@example.com - git -C "$REPO" config user.name Tester - git -C "$REPO" config commit.gpgsign false - echo hi > "$REPO/README.md" - git -C "$REPO" add README.md - git -C "$REPO" commit -qm "chore: init" - git -C "$REPO" push -q origin main - # Every test runs FROM the throwaway repo so `ao verify init` can never - # target the real checkout (git-common-dir of a worktree is the real .git). - cd "$REPO" -} - -# Fabricate a CONFIRMED verdict edge bound to $1 (a commit sha) and commit the -# ledger as a #trivial provenance-only commit. -bind_verdict() { - sha="$1" - bead="$2" - "$AO" provenance add "$bead@${sha:0:7}" "$sha" \ - --from-type verdict --to-type commit --relation wasDerivedFrom \ - --trust-tier inferred --evidence "pawl-verdict $bead disposition=CONFIRMED" >/dev/null - git -C "$REPO" add docs/provenance/ledger.jsonl - git -C "$REPO" commit -qm "chore(provenance): bind verdict for $bead #trivial" -} - -@test "init installs an executable, marker-bearing pre-push hook" { - run "$AO" verify init - [ "$status" -eq 0 ] - [ -x "$REPO/.git/hooks/pre-push" ] - grep -q "AGENTOPS-VERIFY-RATCHET" "$REPO/.git/hooks/pre-push" - # The install-time ao absolute path is baked in (no unsubstituted placeholder). - ! grep -q "@@AO_BIN@@" "$REPO/.git/hooks/pre-push" -} - -@test "push of a commit with NO verdict is refused, naming ao verify" { - cd "$REPO" - "$AO" verify init >/dev/null - echo change >> README.md - git commit -qam "feat: a change (age-b1)" - run git push origin main - [ "$status" -ne 0 ] - [[ "$output" == *"PUSH REFUSED"* ]] - [[ "$output" == *"ao verify"* ]] -} - -@test "push proceeds once a CONFIRMED commit-bound verdict exists" { - cd "$REPO" - "$AO" verify init >/dev/null - echo change >> README.md - git commit -qam "feat: a change (age-b2)" - code="$(git rev-parse HEAD)" - bind_verdict "$code" age-b2 - run git push origin main - [ "$status" -eq 0 ] -} - -@test "an appended-but-UNCOMMITTED ledger edge does not authorize the push" { - # The cross-family refuter's repro (age-rk3r.6 amend), end-to-end through the - # real hook: proof is the ledger AS COMMITTED in the pushed tree — a valid - # edge sitting only in the working tree never reaches the remote. - cd "$REPO" - "$AO" verify init >/dev/null - echo change >> README.md - git commit -qam "feat: a change (age-b6)" - code="$(git rev-parse HEAD)" - # Append the CONFIRMED edge but do NOT git-add or commit the ledger. - "$AO" provenance add "age-b6@${code:0:7}" "$code" \ - --from-type verdict --to-type commit --relation wasDerivedFrom \ - --trust-tier inferred --evidence "pawl-verdict age-b6 disposition=CONFIRMED" >/dev/null - run git push origin main - [ "$status" -ne 0 ] - [[ "$output" == *"PUSH REFUSED"* ]] - [[ "$output" == *"ao verify"* ]] - # Committing the same edge (the bind commit) then authorizes the push. - git add docs/provenance/ledger.jsonl - git commit -qm "chore(provenance): bind verdict for age-b6 #trivial" - run git push origin main - [ "$status" -eq 0 ] -} - -@test "a FAILED pre-push stdin capture (disk full / ulimit -f) refuses fail-closed, never gating empty input" { - # Cross-family refuter's class: the hook captures git's ref-line stdin to a - # temp file; if that write FAILS/truncates (ENOSPC, RLIMIT_FSIZE), an empty - # capture would make `ao verify pre-push` read a non-hook invocation and exit - # 0 — a fail-OPEN. The commit here IS verdict-bound, so a WORKING capture - # would PASS; only the capture failure must flip it to a fail-closed refusal. - cd "$REPO" - "$AO" verify init >/dev/null - echo change >> README.md - git commit -qam "feat: a change (age-b7)" - code="$(git rev-parse HEAD)" - bind_verdict "$code" age-b7 - # Skip where this shell's RLIMIT_FSIZE is not enforced (some CI sandboxes). - ( ulimit -f 0 2>/dev/null; printf 'xxxx' > "$BATS_TEST_TMPDIR/fsprobe" ) 2>/dev/null || true - if [ -s "$BATS_TEST_TMPDIR/fsprobe" ]; then skip "RLIMIT_FSIZE not enforced in this environment"; fi - remote_sha="$(git rev-parse origin/main)" - # Fire the installed hook exactly as git would (ref line on stdin, remote - # name + url as args) but with a 0 file-size limit so the internal capture - # `cat >"$tmp"` write of the ref line fails mid-stream. - run bash -c 'ulimit -f 0; printf "refs/heads/main %s refs/heads/main %s\n" "$1" "$2" | "$3" origin "$4"' \ - _ "$code" "$remote_sha" "$REPO/.git/hooks/pre-push" "$REMOTE" - [ "$status" -ne 0 ] - [[ "$output" == *"failed to capture"* ]] -} - -@test "hostile PATH: planted repo-internal git and cat are never executed by hook or gate" { - # Round-4 refuter's class, end-to-end: an operator PATH carrying a - # repo-internal entry (direnv-style \$PWD/bin) must not let planted binaries - # subvert the ratchet — a planted `git` could forge the gate's answers, and - # a planted `cat` could swallow the hook's stdin capture into a - # "no pre-push stdin" skip. The hook prepends system dirs for its own - # utilities and the gate resolves git on a sanitized PATH, so neither - # planted binary runs and the verdict matches a clean environment. - cd "$REPO" - "$AO" verify init >/dev/null - echo change >> README.md - git commit -qam "feat: unverified (age-b8)" - - sentinel="$BATS_TEST_TMPDIR/PWNED" - mkdir -p bin - printf '#!/bin/sh\necho x >> "%s"\ncase "$*" in *cat-file*) exit 1 ;; esac\nexit 0\n' "$sentinel" > bin/git - printf '#!/bin/sh\necho x >> "%s"\nexit 0\n' "$sentinel" > bin/cat - chmod +x bin/git bin/cat - - # Drive the push with the REAL git via absolute path (only the hook + gate - # see the hostile PATH; the pushing git itself must be genuine). - real_git="$(command -v git)" - run env PATH="$REPO/bin:$PATH" "$real_git" push origin main - [ "$status" -ne 0 ] - [[ "$output" == *"PUSH REFUSED"* ]] - [[ "$output" == *"ao verify"* ]] - [ ! -e "$sentinel" ] -} - -@test "creation push (new remote) checks the whole new history, not just a #trivial tip" { - # Round-3 refuter's repro end-to-end: pushing a branch the remote does not - # have (zero remote sha) must check every commit not already on a - # remote-tracking ref — an unverified code commit cannot ride in under a - # provenance-only #trivial tip. (The setup's seeded origin/main tracking ref - # correctly excludes the pre-hook init commit from the checked range.) - cd "$REPO" - "$AO" verify init >/dev/null - echo x > code.txt && git add code.txt - git commit -qm "feat: unverified code (age-b7)" - code="$(git rev-parse HEAD)" - mkdir -p docs/provenance && echo n > docs/provenance/note.txt - git add docs/provenance/note.txt - git commit -qm "chore(provenance): note #trivial" - - git init -q --bare "$BATS_TEST_TMPDIR/remote2.git" - git remote add origin2 "$BATS_TEST_TMPDIR/remote2.git" - run git push origin2 main - [ "$status" -ne 0 ] - [[ "$output" == *"PUSH REFUSED"* ]] - [[ "$output" == *"${code:0:12}"* ]] - [[ "$output" == *"ao verify"* ]] - - # Binding a committed CONFIRMED verdict to the code commit unblocks it. - bind_verdict "$code" age-b7 - run git push origin2 main - [ "$status" -eq 0 ] -} - -@test "a pre-existing pre-push hook is chained and restored byte-identically on --remove" { - cd "$REPO" - printf '#!/usr/bin/env sh\necho custom-hook-ran >&2\nexit 0\n' > .git/hooks/pre-push - chmod +x .git/hooks/pre-push - cp .git/hooks/pre-push "$BATS_TEST_TMPDIR/orig-copy" - - run "$AO" verify init - [ "$status" -eq 0 ] - [[ "$output" == *"chained"* ]] - grep -q "AGENTOPS-VERIFY-RATCHET" .git/hooks/pre-push - # The sidecar holds the original byte-identically. - cmp -s .git/hooks/pre-push.agentops-orig "$BATS_TEST_TMPDIR/orig-copy" - - # The chained original runs FIRST on push (then the gate refuses, no verdict). - echo change >> README.md - git commit -qam "feat: c (age-b3)" - run git push origin main - [ "$status" -ne 0 ] - [[ "$output" == *"custom-hook-ran"* ]] - [[ "$output" == *"PUSH REFUSED"* ]] - - # Remove restores the original byte-identically; sidecar is gone. - run "$AO" verify init --remove - [ "$status" -eq 0 ] - cmp -s .git/hooks/pre-push "$BATS_TEST_TMPDIR/orig-copy" - [ ! -e .git/hooks/pre-push.agentops-orig ] -} - -@test "re-init is idempotent (no duplicate hook, byte-stable)" { - cd "$REPO" - "$AO" verify init >/dev/null - cp .git/hooks/pre-push "$BATS_TEST_TMPDIR/first" - run "$AO" verify init - [ "$status" -eq 0 ] - [[ "$output" == *"refreshed"* ]] - cmp -s .git/hooks/pre-push "$BATS_TEST_TMPDIR/first" - # Exactly one begin marker. - run grep -c ">>> AGENTOPS-VERIFY-RATCHET" .git/hooks/pre-push - [ "$output" -eq 1 ] -} - -@test "hostile repo: a planted repo-tree pawl-verdict.sh is NEVER executed by the hook" { - cd "$REPO" - mkdir -p scripts scripts/lib - sentinel="$BATS_TEST_TMPDIR/PWNED" - printf '#!/usr/bin/env sh\ntouch "%s"\nexit 0\n' "$sentinel" > scripts/pawl-verdict.sh - chmod +x scripts/pawl-verdict.sh - printf '#!/usr/bin/env sh\ntouch "%s"\nexit 0\n' "$sentinel" > scripts/check-pawl-pre-push.sh - chmod +x scripts/check-pawl-pre-push.sh - git add -A && git commit -qm "chore: planted scripts" - git push -q origin main - - "$AO" verify init >/dev/null - echo x > code.txt && git add code.txt && git commit -qm "feat: evil (age-b4)" - run git push origin main - [ "$status" -ne 0 ] # refused (no verdict), via the embedded gate - [[ "$output" == *"PUSH REFUSED"* ]] - [ ! -e "$sentinel" ] # NONE of the planted repo scripts ran -} - -@test "install refuses to bake a repo-internal ao path (the swap-the-baked-binary attack)" { - # Install-side half of the no-repo-internal-absolute invariant: the runtime - # hook trusts the baked path absolutely, so the baked path must never be - # inside the repo — a repo-local ao could later be swapped for a fake that - # fakes the version + exits 0 for `verify pre-push` to authorize an - # unverified push. `ao verify init` run from a repo-internal ao must REFUSE - # at install time, so the bad install never happens. - cd "$REPO" - mkdir -p bin - cp "$AO" bin/ao # a repo-INTERNAL ao - - run "$REPO/bin/ao" verify init - [ "$status" -ne 0 ] - [[ "$output" == *"repo-internal ao path"* ]] - [[ "$output" == *"ao verify init"* ]] - # The bad install never happened: no hook was written. - [ ! -e .git/hooks/pre-push ] - - # And a trusted ao OUTSIDE the repo installs normally (happy path unchanged). - run "$AO" verify init - [ "$status" -eq 0 ] - grep -q "AGENTOPS-VERIFY-RATCHET" .git/hooks/pre-push -} - -@test "baked ao missing: hook refuses with a reinstall message; a planted ./bin/ao is NEVER consulted" { - # Convergence-by-deletion: the baked absolute ao path is the ONLY accepted - # ao — the command -v ao / AGENTOPS_AO_BIN fallback is gone. A stale install - # (baked binary deleted) must FAIL CLOSED with a reinstall message, never - # fall back to a repo-planted ./bin/ao. - cd "$REPO" - gone="$BATS_TEST_TMPDIR/ao-gone" - cp "$AO" "$gone" - "$gone" verify init >/dev/null # bakes $gone as the hook's only ao - rm -f "$gone" # the baked binary is now missing (stale install) - - sentinel="$BATS_TEST_TMPDIR/PWNED-ao" - mkdir -p bin - printf '#!/bin/sh\necho x >> "%s"\nexit 0\n' "$sentinel" > bin/ao - chmod +x bin/ao - echo change >> README.md - git commit -qam "feat: x (age-b9)" - - real_git="$(command -v git)" - run env PATH="$REPO/bin:.:$PATH" "$real_git" push origin main - [ "$status" -ne 0 ] - [[ "$output" == *"the installed ao binary is missing"* ]] - [[ "$output" == *"ao verify init"* ]] - [ ! -e "$sentinel" ] # the planted ./bin/ao was NEVER consulted -} - -@test "version floor: an ao too old to read the ledger refuses with an upgrade message" { - cd "$REPO" - # Install with a COPY of ao, then swap a fake "old ao" in at the SAME baked - # absolute path — the hook trusts EXACTLY the baked path (no env override). - swap="$BATS_TEST_TMPDIR/ao-swap" - cp "$AO" "$swap" - "$swap" verify init >/dev/null - # Replace the baked binary in place: no ledger-reader-version subcommand - # (exits non-zero); its verify pre-push would wrongly pass — the floor must - # stop us reaching it. - cat > "$swap" <<'EOS' -#!/bin/sh -case "$1 $2" in - "provenance ledger-reader-version") echo "unknown command" >&2; exit 1 ;; - "verify pre-push") echo "OLD-AO-GATE-RAN"; exit 0 ;; -esac -exit 0 -EOS - chmod +x "$swap" - echo change >> README.md - git commit -qam "feat: x (age-b5)" - run git push origin main - [ "$status" -ne 0 ] - [[ "$output" == *"too old"* ]] - [[ "$output" == *"upgrade"* ]] - [[ "$output" != *"OLD-AO-GATE-RAN"* ]] -} diff --git a/tests/scripts/verify-pushed-commit-builds.bats b/tests/scripts/verify-pushed-commit-builds.bats deleted file mode 100644 index 117ceed67..000000000 --- a/tests/scripts/verify-pushed-commit-builds.bats +++ /dev/null @@ -1,89 +0,0 @@ -#!/usr/bin/env bats -# L2 tests for scripts/verify-pushed-commit-builds.sh — the partial-commit-lands-broken -# guard (age-yy24). The "build" is stubbed via AGENTOPS_COMMIT_BUILD_CMD so the test is -# fast + hermetic: "the committed tree builds" == "helper.sh exists in it". The escape -# is a commit MISSING a file the code needs while the worktree still has it. - -setup() { - SCRIPT="$(git rev-parse --show-toplevel)/scripts/verify-pushed-commit-builds.sh" - TMP="$(mktemp -d)" - cd "$TMP" - git init -q . - git config user.email t@t && git config user.name t - # The committed tree "builds" iff helper.sh is present; watch the whole repo. - export AGENTOPS_COMMIT_BUILD_CMD="test -f helper.sh" - export AGENTOPS_COMMIT_BUILD_PATHS="." - unset AGENTOPS_PREPUSH_SKIP_COMMIT_BUILD -} - -teardown() { - cd / - rm -rf "$TMP" -} - -@test "partial commit (needed file left untracked) is refused" { - # Commit code.sh but NOT helper.sh; helper.sh exists only in the worktree - # (untracked) — the worktree 'builds' but the COMMIT does not. - echo 'code' > code.sh - git add code.sh && git commit -qm "partial: code without helper" - sha="$(git rev-parse HEAD)" - echo 'helper' > helper.sh # untracked -> dirty source tree, present in worktree only - run bash "$SCRIPT" "$sha" - [ "$status" -eq 1 ] - [[ "$output" == *"does NOT build"* ]] -} - -@test "complete commit on a dirty tree is allowed" { - # helper.sh IS committed (the commit builds); an unrelated untracked .sh makes the - # tree dirty so the build path actually runs (not the clean-tree skip). - echo 'code' > code.sh; echo 'helper' > helper.sh - git add code.sh helper.sh && git commit -qm "complete" - sha="$(git rev-parse HEAD)" - echo 'scratch' > unrelated.sh # untracked, makes tree dirty - run bash "$SCRIPT" "$sha" - [ "$status" -eq 0 ] -} - -@test "clean tree is skipped on the fast path (worktree == HEAD)" { - echo 'code' > code.sh; echo 'helper' > helper.sh - git add code.sh helper.sh && git commit -qm "complete + clean" - sha="$(git rev-parse HEAD)" - # No dirty source -> the gate's worktree build already validated the commit -> skip. - run bash "$SCRIPT" "$sha" - [ "$status" -eq 0 ] -} - -@test "emergency bypass skips the check" { - echo 'code' > code.sh - git add code.sh && git commit -qm "partial" - sha="$(git rev-parse HEAD)" - echo 'helper' > helper.sh # would otherwise fail - AGENTOPS_PREPUSH_SKIP_COMMIT_BUILD=1 run bash "$SCRIPT" "$sha" - [ "$status" -eq 0 ] -} - -@test "a broken INTERMEDIATE commit is caught even when the tip builds (range, clean tree)" { - # base builds; commit A removes helper.sh (broken); commit B restores it (tip builds). - # Pushing base..B must be refused because A does not build — even on a clean tree - # (the worktree build only ever validated the tip). Regression for the 2026-06-22 refute. - echo 'code' > code.sh; echo 'helper' > helper.sh - git add code.sh helper.sh && git commit -qm "base (builds)" - base="$(git rev-parse HEAD)" - git rm -q helper.sh && git commit -qm "A: drop helper (broken)" - echo 'helper' > helper.sh && git add helper.sh && git commit -qm "B: restore helper (tip builds)" - tip="$(git rev-parse HEAD)" - # Clean tree now; tip builds. Stdin: <lref> <lsha=tip> <rref> <rsha=base>. - run bash -c "printf 'refs/heads/main %s refs/heads/main %s\n' '$tip' '$base' | bash '$SCRIPT'" - [ "$status" -eq 1 ] - [[ "$output" == *"does NOT build"* ]] -} - -@test "reads shas from git pre-push stdin (field 2)" { - echo 'code' > code.sh - git add code.sh && git commit -qm "partial" - sha="$(git rev-parse HEAD)" - echo 'helper' > helper.sh - # Simulate git's pre-push stdin: <local_ref> <local_sha> <remote_ref> <remote_sha> - run bash -c "printf 'refs/heads/main %s refs/heads/main %s\n' '$sha' '0000000000000000000000000000000000000000' | bash '$SCRIPT'" - [ "$status" -eq 1 ] -} diff --git a/tests/skills/check-alias-collisions.sh b/tests/skills/check-alias-collisions.sh index de45d8130..61574b67a 100755 --- a/tests/skills/check-alias-collisions.sh +++ b/tests/skills/check-alias-collisions.sh @@ -10,7 +10,7 @@ REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" # These are semantic overlaps where multiple skills legitimately handle the same phrase. ALLOWLIST=( "brainstorm|brainstorm|council" - "is this ready|pre-mortem|vibe" + "is this ready|premortem|vibe" "research|council|research" "where did this come from|provenance|trace" "where was i|recover|status" diff --git a/tests/skills/lint-skills.sh b/tests/skills/lint-skills.sh index 4694cfd2b..3a8cc4896 100755 --- a/tests/skills/lint-skills.sh +++ b/tests/skills/lint-skills.sh @@ -92,78 +92,14 @@ for skill_dir in "$SKILLS_DIR"/*/; do WARNINGS="${WARNINGS} ${YELLOW}⚠${NC} ${skill_name}: ${line_count} lines but no references/ directory (consider splitting)\n" fi - # --- (d) Examples section required --- - # User-facing skills: FAIL if missing. Internal skills: WARN if missing. - USER_FACING="beads bug-hunt codex-team complexity council crank doc evolve handoff implement inbox knowledge plan post-mortem pre-mortem product quickstart release research retro rpi status swarm trace vibe openai-docs oss-docs pr-research pr-plan pr-implement pr-validate pr-prep pr-retro" - INTERNAL="extract flywheel forge inject provenance ratchet shared standards using-agentops" - - is_user_facing=false - for uf in $USER_FACING; do - [ "$skill_name" = "$uf" ] && is_user_facing=true && break - done - - is_internal=false - for int in $INTERNAL; do - [ "$skill_name" = "$int" ] && is_internal=true && break - done - - has_examples=$(grep -c '^## Examples' "$skill_md" 2>/dev/null || echo 0) - has_examples=$(echo "$has_examples" | tr -d '[:space:]') - has_troubleshooting=$(grep -c '^## Troubleshooting' "$skill_md" 2>/dev/null || echo 0) - has_troubleshooting=$(echo "$has_troubleshooting" | tr -d '[:space:]') - - if $is_user_facing; then - if [ "$has_examples" -eq 0 ]; then - fail "$skill_name" "missing '## Examples' section (required for user-facing skills)" - skill_ok=false - fi - if [ "$has_troubleshooting" -eq 0 ]; then - fail "$skill_name" "missing '## Troubleshooting' section (required for user-facing skills)" - skill_ok=false - fi - # Format validation: Examples should have "User says" pattern - if [ "$has_examples" -gt 0 ]; then - user_says_count=$(grep -c '\*\*User says:\*\*' "$skill_md" 2>/dev/null || echo 0) - user_says_count=$(echo "$user_says_count" | tr -d '[:space:]') - if [ "$user_says_count" -eq 0 ]; then - WARNED=$((WARNED + 1)) - WARNINGS="${WARNINGS} ${YELLOW}⚠${NC} ${skill_name}: Examples section missing '**User says:**' format\n" - fi - fi - # Format validation: Troubleshooting should have table format - if [ "$has_troubleshooting" -gt 0 ]; then - has_table=$(grep -c '| Problem |' "$skill_md" 2>/dev/null || echo 0) - has_table=$(echo "$has_table" | tr -d '[:space:]') - has_prose_troubleshoot=$(awk '/^## Troubleshooting/ { in_section=1; next } in_section && /^## / { exit } in_section && /^### / { count++ } END { print count+0 }' "$skill_md") - has_prose_troubleshoot=$(echo "$has_prose_troubleshoot" | tr -d '[:space:]') - # Accept either table format or prose format (some pre-existing skills use prose) - if [ "$has_table" -eq 0 ] && [ "$has_prose_troubleshoot" -eq 0 ]; then - WARNED=$((WARNED + 1)) - WARNINGS="${WARNINGS} ${YELLOW}⚠${NC} ${skill_name}: Troubleshooting section has no table or structured entries\n" - fi - fi - elif $is_internal; then - # Internal skills: warn only (shared is excluded from content requirement) - if [ "$skill_name" != "shared" ]; then - if [ "$has_examples" -eq 0 ]; then - WARNED=$((WARNED + 1)) - WARNINGS="${WARNINGS} ${YELLOW}⚠${NC} ${skill_name}: missing '## Examples' section (recommended for internal skills)\n" - fi - if [ "$has_troubleshooting" -eq 0 ]; then - WARNED=$((WARNED + 1)) - WARNINGS="${WARNINGS} ${YELLOW}⚠${NC} ${skill_name}: missing '## Troubleshooting' section (recommended for internal skills)\n" - fi - fi - fi - - # --- (e) Word count limit (5000 words) --- + # --- (d) Word count limit --- word_count=$(wc -w < "$skill_md" | tr -d ' ') if [ "$word_count" -gt 5500 ]; then fail "$skill_name" "${word_count} words exceeds 5500-word limit" skill_ok=false fi - # --- (f) Referenced files must exist --- + # --- (e) Referenced files must exist --- # Match patterns like references/foo.md, references/bar-baz.md # Also handles cross-skill references: # - skills/<name>/references/foo.md (repo-absolute) @@ -190,33 +126,6 @@ for skill_dir in "$SKILLS_DIR"/*/; do done <<< "$ref_paths" fi - # --- (g) Verdict schema v2 validation --- - if [ "$skill_name" = "council" ]; then - verdict_schema="$skill_dir/schemas/verdict.json" - if [ -f "$verdict_schema" ]; then - # Check fix/why/ref fields exist in findings items - for field in fix why ref; do - if ! grep -q "\"$field\"" "$verdict_schema" 2>/dev/null; then - fail "$skill_name" "verdict.json missing '$field' field in findings items (schema v2)" - skill_ok=false - fi - done - # Check schema_version allows value 2 - if ! grep -q '"enum"' "$verdict_schema" 2>/dev/null || ! grep -q '2' "$verdict_schema" 2>/dev/null; then - fail "$skill_name" "verdict.json schema_version doesn't allow value 2" - skill_ok=false - fi - # Check fix/why/ref are required in schema v2 findings items. - findings_required=$(jq -r '.properties.findings.items.required[]?' "$verdict_schema" 2>/dev/null || true) - for field in fix why ref; do - if ! echo "$findings_required" | grep -qx "$field" 2>/dev/null; then - fail "$skill_name" "verdict.json '$field' must be in findings required array (schema v2)" - skill_ok=false - fi - done - fi - fi - if $skill_ok; then PASSED=$((PASSED + 1)) echo -e " ${GREEN}✓${NC} $skill_name (tier=$tier, ${line_count} lines)" diff --git a/tests/skills/run-all.sh b/tests/skills/run-all.sh index 1adef8b9a..7213ce259 100755 --- a/tests/skills/run-all.sh +++ b/tests/skills/run-all.sh @@ -129,23 +129,6 @@ else echo -e " ${YELLOW}⚠ lint-skills.sh not found${NC}" fi -# Run Claude feature coverage checks -echo "" -echo -e "${BLUE}━━━ Claude Feature Coverage ━━━${NC}" -echo "" -CLAUDE_FEATURE_SCRIPT="$SCRIPT_DIR/test-claude-feature-coverage.sh" -if [ -f "$CLAUDE_FEATURE_SCRIPT" ]; then - chmod +x "$CLAUDE_FEATURE_SCRIPT" - if "$CLAUDE_FEATURE_SCRIPT"; then - echo -e " ${GREEN}✓ claude feature coverage passed${NC}" - else - echo -e " ${RED}✗ claude feature coverage failed${NC}" - FAILED=$((FAILED + 1)) - fi -else - echo -e " ${YELLOW}⚠ test-claude-feature-coverage.sh not found${NC}" -fi - # Run token/context budget checks echo "" echo -e "${BLUE}━━━ Token Budget Checks ━━━${NC}" @@ -186,12 +169,9 @@ echo -e "${BLUE}━━━ Additional Skill Tests ━━━${NC}" for extra_test in \ "$SCRIPT_DIR/test-tuning-defaults.sh" \ - "$SCRIPT_DIR/test-allowlist-negative.sh" \ "$SCRIPT_DIR/test-first-smoke.sh" \ "$SCRIPT_DIR/test-codex-override-coverage.sh" \ - "$SCRIPT_DIR/test-crank-wave-checkpoint.sh" \ - "$SCRIPT_DIR/test-repo-native-orchestration.sh" \ - "$SCRIPT_DIR/test-evolve-autonomy.sh"; do + "$SCRIPT_DIR/test-repo-native-orchestration.sh"; do if [ -f "$extra_test" ]; then test_name=$(basename "$extra_test" .sh) echo -n " $test_name ... " diff --git a/tests/skills/test-allowlist-negative.sh b/tests/skills/test-allowlist-negative.sh deleted file mode 100755 index c26fdd42b..000000000 --- a/tests/skills/test-allowlist-negative.sh +++ /dev/null @@ -1,156 +0,0 @@ -#!/usr/bin/env bash -# shellcheck disable=SC2016 -# test-allowlist-negative.sh — legacy filename for council contract negatives. -# -# Council no longer owns reference allowlists. The in-repo skill is an extracted -# routing stub, and validate-skill.sh now enforces the current repo contract: -# council must point mixed-model panels at /dual-pane-atm and must not resurrect -# obsolete --technique/--profile flag rows. - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" -VALIDATE="$SCRIPT_DIR/validate-skill.sh" - -RED='\033[0;31m' -GREEN='\033[0;32m' -NC='\033[0m' - -PASS=0 -FAIL=0 - -TMPROOT="$(mktemp -d)" -trap 'rm -rf "$TMPROOT"' EXIT - -pass() { - echo -e " ${GREEN}✓${NC} $1" - PASS=$((PASS + 1)) -} - -fail() { - echo -e " ${RED}✗${NC} $1" - shift || true - if [ "$#" -gt 0 ]; then - printf '%s\n' "$@" - fi - FAIL=$((FAIL + 1)) -} - -make_council_fixture() { - local dir="$1" - local body="$2" - - mkdir -p "$dir" - cat > "$dir/SKILL.md" <<EOF ---- -name: council -description: Test council fixture. ---- - -# council fixture - -$body -EOF -} - -assert_contains() { - local desc="$1" - local file="$2" - local needle="$3" - - if grep -Fq -- "$needle" "$file"; then - pass "$desc" - else - fail "$desc" "Expected output to contain: $needle" "Output was:" "$(cat "$file")" - fi -} - -assert_passes() { - local desc="$1" - local out="$2" - shift 2 - - if "$@" >"$out" 2>&1; then - pass "$desc" - else - fail "$desc" "Expected success, got failure. Output was:" "$(cat "$out")" - fi -} - -assert_fails() { - local desc="$1" - local out="$2" - shift 2 - - if "$@" >"$out" 2>&1; then - fail "$desc" "Expected failure, got success. Output was:" "$(cat "$out")" - else - pass "$desc" - fi -} - -echo "=== Council Contract Negative Fixture Tests ===" - -DELEGATION_MSG="Council: delegates the mixed-model duel substrate to /dual-pane-atm" -OBSOLETE_ABSENT_MSG="Council: obsolete --technique/--profile flag rows absent" -OBSOLETE_FAIL_MSG="Council: obsolete --technique/--profile flag rows must not return" - -echo "" -echo "Test 1: Repo council validation passes current contract" -repo_out="$TMPROOT/repo-council.out" -assert_passes "repo council validates" "$repo_out" "$VALIDATE" "$REPO_ROOT/skills/council" -assert_contains "repo council reports /dual-pane-atm delegation" "$repo_out" "$DELEGATION_MSG" -assert_contains "repo council reports obsolete rows absent" "$repo_out" "$OBSOLETE_ABSENT_MSG" - -echo "" -echo "Test 2: Minimal current-contract fixture passes" -good_dir="$TMPROOT/good/council" -good_out="$TMPROOT/good.out" -make_council_fixture "$good_dir" 'Mixed-model panels delegate to `/dual-pane-atm`.' -assert_passes "minimal council fixture validates" "$good_out" "$VALIDATE" "$good_dir" -assert_contains "minimal fixture reports /dual-pane-atm delegation" "$good_out" "$DELEGATION_MSG" -assert_contains "minimal fixture reports obsolete rows absent" "$good_out" "$OBSOLETE_ABSENT_MSG" - -echo "" -echo "Test 3: Missing /dual-pane-atm delegation fails" -missing_delegate_dir="$TMPROOT/missing-delegate/council" -missing_delegate_out="$TMPROOT/missing-delegate.out" -make_council_fixture "$missing_delegate_dir" 'Mixed-model panels are described without the required delegation pointer.' -assert_fails "missing /dual-pane-atm pointer fails" "$missing_delegate_out" "$VALIDATE" "$missing_delegate_dir" -assert_contains "missing pointer failure reports delegation contract" "$missing_delegate_out" "$DELEGATION_MSG" - -echo "" -echo "Test 4: Obsolete --technique flag row fails" -technique_dir="$TMPROOT/obsolete-technique/council" -technique_out="$TMPROOT/obsolete-technique.out" -make_council_fixture "$technique_dir" 'Mixed-model panels delegate to `/dual-pane-atm`. - -| Flag | Meaning | -|------|---------| -| `--technique=<name>` | obsolete council flag row |' -assert_fails "obsolete --technique row fails" "$technique_out" "$VALIDATE" "$technique_dir" -assert_contains "obsolete --technique failure reports moved flag contract" "$technique_out" "$OBSOLETE_FAIL_MSG" - -echo "" -echo "Test 5: Obsolete --profile flag row fails" -profile_dir="$TMPROOT/obsolete-profile/council" -profile_out="$TMPROOT/obsolete-profile.out" -make_council_fixture "$profile_dir" 'Mixed-model panels delegate to `/dual-pane-atm`. - -| Flag | Meaning | -|------|---------| -| `--profile=<name>` | obsolete council flag row |' -assert_fails "obsolete --profile row fails" "$profile_out" "$VALIDATE" "$profile_dir" -assert_contains "obsolete --profile failure reports moved flag contract" "$profile_out" "$OBSOLETE_FAIL_MSG" - -echo "" -TOTAL=$((PASS + FAIL)) -echo "=== Results: $PASS/$TOTAL passed ===" - -if [ "$FAIL" -gt 0 ]; then - echo -e "${RED}$FAIL test(s) failed${NC}" - exit 1 -fi - -echo -e "${GREEN}All tests passed${NC}" diff --git a/tests/skills/test-claude-feature-coverage.sh b/tests/skills/test-claude-feature-coverage.sh deleted file mode 100755 index 1c63a4956..000000000 --- a/tests/skills/test-claude-feature-coverage.sh +++ /dev/null @@ -1,97 +0,0 @@ -#!/bin/bash -# Validate Claude Code feature coverage across skill docs. -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" -SKILLS_DIR="$REPO_ROOT/skills" - -FAILED=0 - -fail() { - local msg="$1" - echo " ✗ $msg" - FAILED=$((FAILED + 1)) -} - -pass() { - local msg="$1" - echo " ✓ $msg" -} - -echo "Claude feature coverage checks" - -CONTRACT="$SKILLS_DIR/shared/references/claude-code-latest-features.md" -if [ ! -f "$CONTRACT" ]; then - fail "missing feature contract: skills/shared/references/claude-code-latest-features.md" -else - pass "feature contract exists" -fi - -if [ -f "$CONTRACT" ]; then - required_tokens=( - "/agents" - "/hooks" - "/permissions" - "/memory" - "/mcp" - "/output-style" - "isolation: worktree" - "background: true" - "WorktreeCreate" - "WorktreeRemove" - "ConfigChange" - "claude agents" - "--worktree" - ) - - for token in "${required_tokens[@]}"; do - if grep -Fq -- "$token" "$CONTRACT"; then - pass "contract includes '$token'" - else - fail "contract missing '$token'" - fi - done -fi - -# Core multi-agent skills should reference the shared contract. -core_multi_agent_skills=( - "skills/shared/SKILL.md" - "skills/council/SKILL.md" - "skills/swarm/SKILL.md" - "skills/research/SKILL.md" - "skills/crank/SKILL.md" -) - -for rel in "${core_multi_agent_skills[@]}"; do - path="$REPO_ROOT/$rel" - if [ ! -f "$path" ]; then - fail "missing expected skill file: $rel" - continue - fi - if grep -Eq -- '(skills/shared/references|\.\.\/shared\/references)/claude-code-latest-features\.md' "$path"; then - pass "$rel references shared Claude feature contract" - else - fail "$rel does not reference shared Claude feature contract" - fi -done - -# Prevent regressions to deprecated command names. -deprecated_out="$(mktemp)" -if rg -n --glob 'SKILL.md' '/approved-tools|/allowed-tools' "$SKILLS_DIR" >"$deprecated_out" 2>/dev/null; then - fail "deprecated permission command names found in SKILL.md files" - sed 's/^/ /' "$deprecated_out" -else - pass "no deprecated permission command names" -fi -rm -f "$deprecated_out" - -if [ "$FAILED" -gt 0 ]; then - echo "" - echo "Claude feature coverage: FAIL ($FAILED issue(s))" - exit 1 -fi - -echo "" -echo "Claude feature coverage: PASS" -exit 0 diff --git a/tests/skills/test-crank-wave-checkpoint.sh b/tests/skills/test-crank-wave-checkpoint.sh deleted file mode 100755 index a4e31a12e..000000000 --- a/tests/skills/test-crank-wave-checkpoint.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -VALIDATOR="$ROOT/skills/crank/scripts/validate-wave-checkpoint.sh" - -PASS=0 -FAIL=0 - -pass() { - echo "PASS: $1" - PASS=$((PASS + 1)) -} - -fail() { - echo "FAIL: $1" - FAIL=$((FAIL + 1)) -} - -run_expect_success() { - local name="$1" - shift - if "$@" >/tmp/crank-wave-checkpoint.out 2>/tmp/crank-wave-checkpoint.err; then - pass "$name" - else - fail "$name" - cat /tmp/crank-wave-checkpoint.err - fi -} - -run_expect_failure() { - local name="$1" - shift - if "$@" >/tmp/crank-wave-checkpoint.out 2>/tmp/crank-wave-checkpoint.err; then - fail "$name" - cat /tmp/crank-wave-checkpoint.out - else - pass "$name" - fi -} - -tmp="$(mktemp -d)" -trap 'rm -rf "$tmp" /tmp/crank-wave-checkpoint.out /tmp/crank-wave-checkpoint.err' EXIT - -git -C "$tmp" init -q -git -C "$tmp" config user.email test@example.com -git -C "$tmp" config user.name "Test User" -# Insulate the fixture commit from the operator's global git config: some -# environments set commit.gpgsign=true or a custom gpg.ssh.program that would -# require a real signing key and fail inside a throwaway fixture. -git -C "$tmp" config commit.gpgsign false -git -C "$tmp" config tag.gpgsign false -printf 'fixture\n' > "$tmp/README.md" -git -C "$tmp" add README.md -git -C "$tmp" -c core.hooksPath=/dev/null commit -q -m "fixture" -sha="$(git -C "$tmp" rev-parse HEAD)" - -write_checkpoint() { - local path="$1" - local checkpoint_sha="$2" - local timestamp="$3" - cat > "$path" <<EOF -{ - "schema_version": 1, - "wave": 1, - "timestamp": "$timestamp", - "tasks_completed": ["na-1"], - "tasks_failed": [], - "files_changed": ["README.md"], - "git_sha": "$checkpoint_sha", - "acceptance_verdict": "PASS", - "commit_strategy": "wave-batch", - "mutations_this_wave": 0, - "total_mutations": 0 -} -EOF -} - -valid="$tmp/valid.json" -bad_sha="$tmp/bad-sha.json" -bad_time="$tmp/bad-time.json" -missing_field="$tmp/missing-field.json" -now="$(date -u +%Y-%m-%dT%H:%M:%SZ)" - -write_checkpoint "$valid" "$sha" "$now" -write_checkpoint "$bad_sha" "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef" "$now" -write_checkpoint "$bad_time" "$sha" "not-a-date" -jq 'del(.git_sha)' "$valid" > "$missing_field" - -run_expect_success "valid checkpoint passes" bash "$VALIDATOR" "$valid" "$tmp" -run_expect_failure "non-resolving git_sha fails" bash "$VALIDATOR" "$bad_sha" "$tmp" -run_expect_failure "invalid timestamp fails" bash "$VALIDATOR" "$bad_time" "$tmp" -run_expect_failure "missing required field fails" bash "$VALIDATOR" "$missing_field" "$tmp" - -for controller_field in mutation_budget mutation_limits limits usage; do - controller_fixture="$tmp/controller-$controller_field.json" - jq --arg field "$controller_field" '.[$field] = {"used": 1, "limit": 3}' \ - "$valid" > "$controller_fixture" - run_expect_failure "retired $controller_field state fails" \ - bash "$VALIDATOR" "$controller_fixture" "$tmp" -done - -nested_controller="$tmp/nested-controller.json" -jq '.mutation_metadata = {"used": 1, "limit": 3}' "$valid" > "$nested_controller" -run_expect_failure "nested mutation-control state fails" \ - bash "$VALIDATOR" "$nested_controller" "$tmp" - -echo "" -echo "Results: $PASS passed, $FAIL failed" -[[ $FAIL -eq 0 ]] diff --git a/tests/skills/test-evolve-autonomy.sh b/tests/skills/test-evolve-autonomy.sh deleted file mode 100644 index a35425af1..000000000 --- a/tests/skills/test-evolve-autonomy.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" - -PASS=0 -FAIL=0 - -pass() { echo "PASS: $1"; PASS=$((PASS + 1)); } -fail() { echo "FAIL: $1"; FAIL=$((FAIL + 1)); } - -EVOLVE="$REPO_ROOT/skills/evolve/SKILL.md" -EVOLVE_EXAMPLES="$REPO_ROOT/skills/evolve/references/examples.md" -RPI_CONTRACT="$REPO_ROOT/skills/rpi/references/phase-data-contracts.md" -RPI_GATE4="$REPO_ROOT/skills/rpi/references/gate4-loop-and-spawn.md" -NEXT_WORK_CONTRACT="$REPO_ROOT/docs/contracts/next-work.schema.md" - -check_contains() { - local file="$1" - local pattern="$2" - local message="$3" - if grep -qE "$pattern" "$file"; then - pass "$message" - else - fail "$message" - fi -} - -check_contains "$EVOLVE" 'Harvested `.agents/rpi/next-work.jsonl` work' "/evolve prioritizes harvested work first" -check_contains "$EVOLVE" 'Open ready beads work' "/evolve prioritizes ready beads second" -check_contains "$EVOLVE" 'Failing goals and directive gaps' "/evolve keeps goals/directives in the ladder" -check_contains "$EVOLVE" 'Testing improvements' "/evolve has testing-improvement generator layer" -check_contains "$EVOLVE" 'Validation tightening and bug-hunt passes' "/evolve has validation and bug-hunt generator layer" -check_contains "$EVOLVE" 'Concrete feature suggestions' "/evolve has feature-suggestion fallback" -check_contains "$EVOLVE" 'Dormancy is last resort' "/evolve no longer treats empty queues as immediate success" -check_contains "$EVOLVE" 'immediately re-read `.agents/rpi/next-work.jsonl`' "/evolve re-reads harvested work after each /rpi cycle" -check_contains "$EVOLVE" 'claim it first' "/evolve claims queue items before consuming them" -check_contains "$EVOLVE" 'session-state.json' "/evolve persists resume state on disk" -check_contains "$EVOLVE_EXAMPLES" 'beads -> harvested work -> goals -> testing -> bug hunt -> feature suggestion' "worked example covers the full long-running ladder" -check_contains "$EVOLVE_EXAMPLES" 're-reads the queue and runs it immediately' "examples show post-RPI harvested work pickup" -check_contains "$EVOLVE_EXAMPLES" 're-queued instead of being lost' "examples show requeue behavior on failure" -check_contains "$RPI_CONTRACT" 'claim_status' "/rpi phase contract includes queue claim metadata" -check_contains "$RPI_GATE4" 'Never mark an item consumed at pick-time' "/rpi gate4 documents claim-before-consume semantics" -check_contains "$NEXT_WORK_CONTRACT" 'Lifecycle Rules' "next-work contract defines queue lifecycle" -check_contains "$NEXT_WORK_CONTRACT" 'Failed or regressed cycles release the claim' "next-work contract documents release on failure" - -echo -echo "Results: $PASS passed, $FAIL failed" -[ "$FAIL" -eq 0 ] diff --git a/tests/skills/test-finding-registry-flow.sh b/tests/skills/test-finding-registry-flow.sh index 227006b70..e319f0f4e 100644 --- a/tests/skills/test-finding-registry-flow.sh +++ b/tests/skills/test-finding-registry-flow.sh @@ -38,23 +38,23 @@ REGISTRY_SCHEMA="$REPO_ROOT/docs/contracts/finding-registry.schema.json" check_contains "$PLAN_SKILL" '.agents/planning-rules' "/plan reads compiled planning rules first" check_contains "$PLAN_SKILL" 'registry.jsonl' "/plan falls back to registry.jsonl" check_contains "$PLAN_SKILL" 'Applied findings:' "/plan cites applied finding IDs" -check_contains "$PM_SKILL" '.agents/pre-mortem-checks' "/pre-mortem reads compiled checks first" -check_contains "$PM_SKILL" 'known_risks' "/pre-mortem injects known_risks" -check_contains "$PM_SKILL" 'malformed line -> warn and ignore that line' "/pre-mortem fail-open reader behavior is documented" -check_contains "$PM_SKILL" 'finding-compiler.sh' "/pre-mortem refreshes compiled outputs after registry writes" -check_contains "$VIBE_SKILL" '.agents/pre-mortem-checks' "/vibe reads compiled checks first" +check_contains "$PM_SKILL" '.agents/premortem-checks' "/premortem reads compiled checks first" +check_contains "$PM_SKILL" 'known_risks' "/premortem injects known_risks" +check_contains "$PM_SKILL" 'malformed line -> warn and ignore that line' "/premortem fail-open reader behavior is documented" +check_contains "$PM_SKILL" 'finding-compiler.sh' "/premortem refreshes compiled outputs after registry writes" +check_contains "$VIBE_SKILL" '.agents/premortem-checks' "/vibe reads compiled checks first" check_contains "$VIBE_SKILL" 'registry.jsonl' "/vibe falls back to registry.jsonl" check_contains "$VIBE_SKILL" 'dedup_key' "/vibe write path requires dedup_key" check_contains "$VIBE_SKILL" 'finding-compiler.sh' "/vibe refreshes compiled outputs after registry writes" -check_contains "$POST_MORTEM_SKILL" '.agents/pre-mortem-checks' "/post-mortem reads compiled prevention inputs" -check_contains "$POST_MORTEM_SKILL" 'registry.jsonl' "/post-mortem writes finding registry" +check_contains "$POST_MORTEM_SKILL" '.agents/premortem-checks' "/postmortem reads compiled prevention inputs" +check_contains "$POST_MORTEM_SKILL" 'registry.jsonl' "/postmortem writes finding registry" check_contains "$POST_MORTEM_SKILL" 'atomically update `.agents/findings/registry.jsonl`' "/postmortem uses atomic registry writes" -check_contains "$POST_MORTEM_SKILL" 'finding-compiler.sh' "/post-mortem refreshes compiled outputs after registry writes" +check_contains "$POST_MORTEM_SKILL" 'finding-compiler.sh' "/postmortem refreshes compiled outputs after registry writes" check_contains "$REGISTRY_CONTRACT" 'dedup_key =' "registry contract defines dedup_key normalization" check_contains "$REGISTRY_CONTRACT" 'plan-shape' "registry contract defines controlled applicable_when vocabulary" check_contains "$REGISTRY_CONTRACT" 'atomic rename' "registry contract defines atomic write rule" -FIXTURE='{"id":"f-2026-03-09-001","version":1,"tier":"local","source":{"repo":"agentops/crew/nami","session":"2026-03-09","file":".agents/council/2026-03-09-pre-mortem-finding-compiler-v1.md","skill":"pre-mortem"},"date":"2026-03-09","severity":"significant","category":"validation-gap","pattern":"Plans can omit prior-finding injection and rediscover the same failure mode.","detection_question":"Did this plan load matching active findings before decomposition or review?","checklist_item":"Verify the relevant skill reads compiled planning/pre-mortem checks first, with registry fallback, and cites applied finding IDs or known risks.","applicable_languages":["markdown","shell"],"applicable_when":["plan-shape","validation-gap"],"status":"active","superseded_by":null,"dedup_key":"validation-gap|prior-finding-injection|plan-shape","hit_count":0,"last_cited":null,"ttl_days":30,"confidence":"high"}' +FIXTURE='{"id":"f-2026-03-09-001","version":1,"tier":"local","source":{"repo":"agentops/crew/nami","session":"2026-03-09","file":".agents/council/2026-03-09-premortem-finding-compiler-v1.md","skill":"premortem"},"date":"2026-03-09","severity":"significant","category":"validation-gap","pattern":"Plans can omit prior-finding injection and rediscover the same failure mode.","detection_question":"Did this plan load matching active findings before decomposition or review?","checklist_item":"Verify the relevant skill reads compiled planning/premortem checks first, with registry fallback, and cites applied finding IDs or known risks.","applicable_languages":["markdown","shell"],"applicable_when":["plan-shape","validation-gap"],"status":"active","superseded_by":null,"dedup_key":"validation-gap|prior-finding-injection|plan-shape","hit_count":0,"last_cited":null,"ttl_days":30,"confidence":"high"}' if echo "$FIXTURE" | jq -e ' .id and diff --git a/tests/skills/test-first-smoke.sh b/tests/skills/test-first-smoke.sh index 7a3e154fe..7d27a48b4 100755 --- a/tests/skills/test-first-smoke.sh +++ b/tests/skills/test-first-smoke.sh @@ -1,263 +1,45 @@ #!/usr/bin/env bash -# Smoke test for --test-first flow structural patterns -# Validates that all skill files contain required structural elements -# for the spec-first TDD pipeline (SPEC WAVE → TEST WAVE → GREEN mode). -# -# Usage: ./tests/skills/test-first-smoke.sh - set -euo pipefail -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +IMPLEMENT="$ROOT/skills/implement/SKILL.md" +SCHEMA="$ROOT/schemas/candidate-packet.v1.schema.json" -RED='\033[0;31m' -GREEN='\033[0;32m' -BLUE='\033[0;34m' -NC='\033[0m' +fail() { + printf 'FAIL: %s\n' "$1" >&2 + exit 1 +} -total=0 -passed=0 -failed=0 +[[ -f "$IMPLEMENT" ]] || fail "Implement contract is missing" +[[ -f "$SCHEMA" ]] || fail "CandidatePacket schema is missing" -pass() { echo -e "${GREEN} PASS${NC} $1"; ((total++)) || true; ((passed++)) || true; } -fail() { echo -e "${RED} FAIL${NC} $1"; ((total++)) || true; ((failed++)) || true; } +grep -Fq 'Execute exactly one bounded experiment' "$IMPLEMENT" || fail "Implement is not bounded to one experiment" +grep -Fq 'fails for the expected missing' "$IMPLEMENT" || fail "Implement does not require a RED behavior-change baseline" +grep -Fq 'Refactor only while those checks stay green' "$IMPLEMENT" || fail "Implement does not preserve GREEN while refactoring" +grep -Fq 'Refactoring does not change the' "$IMPLEMENT" || fail "Implement does not preserve acceptance tests" +grep -Fq 'Return the CandidatePacket and stop' "$IMPLEMENT" || fail "Implement does not stop after one candidate" +grep -Fq 'Do not commit, push, claim, close, release, land, reserve, retry' "$IMPLEMENT" || fail "Implement retains lifecycle authority" -cd "$REPO_ROOT" +python3 - "$SCHEMA" <<'PY' +import json +import sys -# ============================================================================= -# 1. Contract Template checks -# ============================================================================= -echo -e "${BLUE}[TEST-FIRST]${NC} Contract template (skills/crank/references/contract-template.md)" +schema = json.load(open(sys.argv[1], encoding="utf-8")) +required = set(schema["required"]) +expected = { + "plan_packet_digest", + "acceptance_digest", + "author_context_id", + "subject_locator", + "subject_manifest", + "actual_changed_paths", + "changed_path_coverage_complete", + "factual_evidence", + "acceptance_check_results", +} +missing = sorted(expected - required) +if missing: + raise SystemExit(f"CandidatePacket missing required fields: {missing}") +PY -CONTRACT="skills/crank/references/contract-template.md" - -if [[ -f "$CONTRACT" ]]; then - pass "File exists" -else - fail "File missing: $CONTRACT" - # All subsequent contract checks will fail; skip to next section - echo -e "${BLUE}[TEST-FIRST]${NC} Skipping remaining contract checks (file missing)" -fi - -if [[ -f "$CONTRACT" ]]; then - # 1a: All 7 required ## headings - for heading in "Problem" "Inputs" "Outputs" "Invariants" "Failure Modes" "Out of Scope" "Test Cases"; do - if grep -qE "^## ${heading}$" "$CONTRACT"; then - pass "Contract has ## $heading heading" - else - fail "Contract missing ## $heading heading" - fi - done - - # 1b: Contract Granularity section - if grep -qE "^## Contract Granularity$" "$CONTRACT"; then - pass "Contract has ## Contract Granularity section" - else - fail "Contract missing ## Contract Granularity section" - fi - - # 1c: YAML frontmatter with framework field - if head -20 "$CONTRACT" | grep -q '```yaml' && grep -q 'framework:' "$CONTRACT"; then - pass "Contract has YAML frontmatter with framework field" - else - fail "Contract missing YAML frontmatter with framework field" - fi - - # 1d: Minimum 30 lines (structural check, not a stub) - line_count=$(wc -l < "$CONTRACT" | tr -d ' ') - if [[ "$line_count" -ge 30 ]]; then - pass "Contract has $line_count lines (>= 30 minimum)" - else - fail "Contract has only $line_count lines (< 30 minimum)" - fi -fi - -# ============================================================================= -# 2. Crank SKILL.md checks -# ============================================================================= -echo -e "${BLUE}[TEST-FIRST]${NC} Crank SKILL.md (skills/crank/SKILL.md)" - -CRANK="skills/crank/SKILL.md" -# Billboard refactor (#275) thinned crank SKILL.md and moved step detail into -# references/. Content-presence checks scan the skill as a unit: SKILL.md + references/. -CRANK_REFS="skills/crank/references/" - -if [[ ! -f "$CRANK" ]]; then - fail "File missing: $CRANK" -else - # 2a: --test-first in a table row (pipe-delimited, not just prose mention) - if grep -qE '^\|.*--test-first.*\|' "$CRANK"; then - pass "--test-first appears in a table row (flag table)" - else - fail "--test-first not found in any table row" - fi - - # 2b: SPEC WAVE (Step 3b) documented in the crank skill unit (SKILL.md + references/) - # Billboard refactor moved step detail into references/wave-dispatch.md. - if grep -rqsE 'Step 3b: SPEC WAVE' "$CRANK" "$CRANK_REFS"; then - pass "Step 3b: SPEC WAVE documented in crank skill" - else - fail "Step 3b: SPEC WAVE missing from crank skill (SKILL.md + references/)" - fi - - # 2c: TEST WAVE (Step 3c) documented in the crank skill unit - if grep -rqsE 'Step 3c: TEST WAVE' "$CRANK" "$CRANK_REFS"; then - pass "Step 3c: TEST WAVE documented in crank skill" - else - fail "Step 3c: TEST WAVE missing from crank skill (SKILL.md + references/)" - fi - - # 2d: Category-based skip logic (spec-eligible or docs/chore pattern), skill-wide - if grep -rqsE 'spec-eligible|spec.eligible' "$CRANK" "$CRANK_REFS" \ - && grep -rqsE 'docs.*chore|chore.*docs' "$CRANK" "$CRANK_REFS"; then - pass "Category-based skip logic present in crank skill (spec-eligible + docs/chore)" - else - fail "Category-based skip logic missing from crank skill (need spec-eligible AND docs/chore)" - fi - - # 2e: Backward compat — Step 4 (standard wave execution) documented skill-wide - if grep -rqsE 'Step 4:' "$CRANK" "$CRANK_REFS"; then - pass "Backward compat: Step 4 documented in crank skill" - else - fail "Backward compat: Step 4 missing from crank skill (standard wave execution removed)" - fi - - # 2f: Backward compat — Step 0 (Load Knowledge Context) documented skill-wide - if grep -rqsE 'Step 0:' "$CRANK" "$CRANK_REFS"; then - pass "Backward compat: Step 0 documented in crank skill (Load Knowledge Context)" - else - fail "Backward compat: Step 0 missing from crank skill (Load Knowledge Context removed)" - fi -fi - -# ============================================================================= -# 3. Wave-patterns.md checks -# ============================================================================= -echo -e "${BLUE}[TEST-FIRST]${NC} Wave patterns (skills/crank/references/wave-patterns.md)" - -WAVES="skills/crank/references/wave-patterns.md" - -if [[ ! -f "$WAVES" ]]; then - fail "File missing: $WAVES" -else - # 3a: Spec-First Wave Model section - if grep -qE '^## Spec-First Wave Model' "$WAVES"; then - pass "Spec-First Wave Model section exists" - else - fail "Spec-First Wave Model section missing" - fi - - # 3b: RED gate documented - if grep -qE 'RED (confirmation|gate)|RED Confirmation Gate' "$WAVES"; then - pass "RED gate documented" - else - fail "RED gate not documented" - fi - - # 3c: GREEN gate documented - if grep -qE 'GREEN (confirmation|gate)|GREEN Confirmation Gate' "$WAVES"; then - pass "GREEN gate documented" - else - fail "GREEN gate not documented" - fi - - # 3d: Category-based skip documented - if grep -qE 'Category.Based Skip|category.based skip' "$WAVES"; then - pass "Category-based skip documented" - else - fail "Category-based skip not documented" - fi -fi - -# ============================================================================= -# 5. Implement SKILL.md checks -# ============================================================================= -echo -e "${BLUE}[TEST-FIRST]${NC} Implement SKILL.md (skills/implement/SKILL.md)" - -IMPL="skills/implement/SKILL.md" - -if [[ ! -f "$IMPL" ]]; then - fail "File missing: $IMPL" -else - # 5a: ### GREEN Mode section heading - if grep -qE '^### GREEN Mode' "$IMPL"; then - pass "### GREEN Mode section heading exists" - else - fail "### GREEN Mode section heading missing" - fi - - # 5b: Test immutability rule documented in the implement skill unit - # (SKILL.md + references/). Billboard refactor moved GREEN-mode detail into - # references/green-mode.md; the rule is also stated inline in SKILL.md. - if grep -rqsiE 'do(es)? NOT modify test|MUST NOT modify (existing )?test|tests are immutable|test files (provided|immutable)|failing tests \(immutable\)' \ - "$IMPL" "skills/implement/references/"; then - pass "Test immutability rule documented in implement skill" - else - fail "Test immutability rule missing from implement skill (SKILL.md + references/)" - fi -fi - -# ============================================================================= -# 6. Passthrough wiring checks (rpi + evolve) -# ============================================================================= -echo -e "${BLUE}[TEST-FIRST]${NC} Passthrough wiring (skills/rpi/SKILL.md, skills/evolve/SKILL.md)" - -RPI="skills/rpi/SKILL.md" -EVOLVE="skills/evolve/SKILL.md" - -if [[ ! -f "$RPI" ]]; then - fail "File missing: $RPI" -else - # 6a: --test-first in rpi flag table with default on - if grep -qE '^\|.*--test-first.*\|.*on.*\|' "$RPI"; then - pass "/rpi documents --test-first in flag table with default on" - else - fail "/rpi missing --test-first default-on entry in flag table" - fi - - # 6b: --no-test-first explicit opt-out path in rpi flag table - if grep -qE '^\|.*--no-test-first.*\|' "$RPI"; then - pass "/rpi documents --no-test-first explicit opt-out in flag table" - else - fail "/rpi missing --no-test-first opt-out entry in flag table" - fi - - # 6c: --test-first in rpi_state - if grep -q 'test_first' "$RPI"; then - pass "/rpi tracks test_first in state" - else - fail "/rpi missing test_first in state object" - fi -fi - -if [[ ! -f "$EVOLVE" ]]; then - fail "File missing: $EVOLVE" -else - # 6d: --test-first in evolve flag table - if grep -qE '^\|.*--test-first.*\|' "$EVOLVE"; then - pass "/evolve documents --test-first in flag table" - else - fail "/evolve missing --test-first in flag table" - fi - - # 6e: --test-first in evolve_state - if grep -q 'test_first' "$EVOLVE"; then - pass "/evolve tracks test_first in state" - else - fail "/evolve missing test_first in state object" - fi -fi - -# ============================================================================= -# Summary -# ============================================================================= -echo "" -echo -e "${BLUE}=============================================${NC}" - -if [[ $failed -gt 0 ]]; then - echo -e "${RED}FAILED${NC} - $passed/$total passed, $failed failed" - exit 1 -else - echo -e "${GREEN}PASSED${NC} - All $total checks passed" - exit 0 -fi +echo 'test-first contract: PASS' diff --git a/tests/skills/test-tuning-defaults.sh b/tests/skills/test-tuning-defaults.sh index 0b51fd053..49bc40d69 100755 --- a/tests/skills/test-tuning-defaults.sh +++ b/tests/skills/test-tuning-defaults.sh @@ -1,45 +1,25 @@ #!/usr/bin/env bash -# Planning-review defaults after the S2 direct cut. set -euo pipefail -REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -passed=0 -failed=0 +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +PREMORTEM="$ROOT/skills/premortem/SKILL.md" +POSTMORTEM="$ROOT/skills/postmortem/SKILL.md" +DUELING="$ROOT/skills/dueling-idea-genies/SKILL.md" +PLAN="$ROOT/skills/plan/SKILL.md" -check() { - local label="$1" - shift - if "$@"; then - printf ' PASS: %s\n' "$label" - passed=$((passed + 1)) - else - printf ' FAIL: %s\n' "$label" - failed=$((failed + 1)) - fi -} +grep -Fq 'optional plan-challenge strategy' "$PREMORTEM" +grep -Fq 'does not authorize readiness' "$PREMORTEM" +grep -Fq 'dependencies: []' "$PREMORTEM" +grep -Fq 'dependencies: []' "$POSTMORTEM" +grep -Fq 'advisory evidence for Plan' "$DUELING" +grep -Fq 'Emit no readiness' "$DUELING" +grep -Fq 'one active behavior' "$PLAN" -contains() { grep -Fq -- "$2" "$1"; } -absent() { ! rg -q -- "$2" "${@:3}"; } +for path in \ + "$ROOT/skills/discovery" \ + "$ROOT/skills/goal-design" \ + "$ROOT/skills/behavior-first-planning"; do + [[ ! -e "$path" ]] +done -PREMORTEM="$REPO_ROOT/skills/premortem/SKILL.md" -DISCOVERY="$REPO_ROOT/skills/discovery" -GOAL_DESIGN="$REPO_ROOT/skills/goal-design/SKILL.md" -DUELING="$REPO_ROOT/skills/dueling-idea-genies/SKILL.md" - -check "Premortem defaults to one fresh judge" contains "$PREMORTEM" 'Use one fresh-context judge' -check "Premortem verdict is binary" contains "$PREMORTEM" 'Emit exactly `PASS` or `FAIL`' -check "Premortem requires author/judge separation" contains "$PREMORTEM" 'author_id != judge_id' -check "model family is optional" contains "$PREMORTEM" 'Model and family' -check "Premortem owns no phase controller" absent 'unused' 'max-rounds|MAX_.*ATTEMPT|helper consultation|phase budget' "$PREMORTEM" -check "Discovery phase budget owner is deleted" test ! -e "$DISCOVERY/references/phase-budgets.md" -check "Discovery has no alternate readiness authority" absent 'unused' 'ApprovalEdge|Fable|ao plan-pawl|duel_verdict_dir' "$DISCOVERY/SKILL.md" "$DISCOVERY/references" -check "Goal Design is deterministic only" contains "$GOAL_DESIGN" 'Goal Design checks packet shape' -check "Dueling evidence routes to Plan" contains "$DUELING" 'advisory evidence for Plan' -check "Dueling emits no readiness" contains "$DUELING" 'Emit no readiness' - -printf '\nPassed: %d\nFailed: %d\n' "$passed" "$failed" -if (( failed > 0 )); then - echo 'OVERALL: FAIL' - exit 1 -fi -echo 'OVERALL: PASS' +echo 'optional strategy defaults: PASS' diff --git a/tests/skills/validate-skill.sh b/tests/skills/validate-skill.sh index 88e6874c5..99a963e23 100755 --- a/tests/skills/validate-skill.sh +++ b/tests/skills/validate-skill.sh @@ -243,70 +243,6 @@ validate_skill() { fi fi - # Test 4b: Skill-specific flag allowlist / contract validation (repo-local) - # - # These checks enforce that documented flag values stay in sync with their canonical reference docs, - # preventing silent drift where invalid values appear to "work" but are ignored/defaulted. - local skill_md - skill_md="$skill_dir/SKILL.md" - - if [[ "$skill_name" == "council" ]]; then - # council was refactored (0c7ef56c0 "extract 6 outer-gate skills to mt-olympus"; - # a81b67869 "fold in-session mixed-model duel into dual-pane-atm; council points - # to it for mixed panels"): the --technique/--profile flags and the - # references/{brainstorm-techniques,model-profiles}.md allowlist tables were - # moved out of council, which is now a multi-judge-consensus skill that - # delegates the mixed-model duel substrate to /dual-pane-atm. Assert that - # delegation pointer rather than the removed flag allowlists (the old check - # asserted deleted content and was the recurring nightly Static-Validation red). - # (a) delegation pointer present — match the deliberate skill-link form - # `/dual-pane-atm`, not a bare incidental mention. - if grep -qE '/dual-pane-atm\b' "$skill_md" 2>/dev/null; then - echo -e " ${GREEN}✓${NC} Council: delegates the mixed-model duel substrate to /dual-pane-atm" - local_checks=$((local_checks + 1)) - else - echo -e " ${RED}✗${NC} Council: delegates the mixed-model duel substrate to /dual-pane-atm" - local_errors=$((local_errors + 1)) - local_checks=$((local_checks + 1)) - fi - # (b) regression guard — the relocated --technique/--profile flag rows must NOT - # reappear in council (their reference allowlists were deleted with the - # refactor, so a re-added row would be a silent, unvalidated contract). - if grep -qE '`--technique=<name>`|`--profile=<name>`' "$skill_md" 2>/dev/null; then - echo -e " ${RED}✗${NC} Council: obsolete --technique/--profile flag rows must not return (moved to /dual-pane-atm)" - local_errors=$((local_errors + 1)) - local_checks=$((local_checks + 1)) - else - echo -e " ${GREEN}✓${NC} Council: obsolete --technique/--profile flag rows absent (delegated to /dual-pane-atm)" - local_checks=$((local_checks + 1)) - fi - fi - - if [[ "$skill_name" == "crank" ]]; then - local commit_ref - commit_ref="$skill_dir/references/commit-strategies.md" - - local per_task_row - per_task_row="$(grep -F '| `--per-task-commits` |' "$skill_md" 2>/dev/null | head -n 1 || true)" - if [[ -n "${per_task_row:-}" ]] && echo "$per_task_row" | grep -q 'references/commit-strategies.md'; then - echo -e " ${GREEN}✓${NC} Crank: --per-task-commits flag row references commit-strategies.md" - local_checks=$((local_checks + 1)) - else - echo -e " ${RED}✗${NC} Crank: --per-task-commits flag row references commit-strategies.md" - local_errors=$((local_errors + 1)) - local_checks=$((local_checks + 1)) - fi - - if [[ -f "$commit_ref" ]] && grep -q '^## wave-batch' "$commit_ref" && grep -q '^## per-task' "$commit_ref" && grep -q 'wave-batch-fallback' "$commit_ref"; then - echo -e " ${GREEN}✓${NC} Crank: commit strategy contract strings present" - local_checks=$((local_checks + 1)) - else - echo -e " ${RED}✗${NC} Crank: commit strategy contract strings present" - local_errors=$((local_errors + 1)) - local_checks=$((local_checks + 1)) - fi - fi - # Test 5: Run skill-specific validate.sh if present local validate_script="$skill_dir/scripts/validate.sh" if [ -f "$validate_script" ]; then diff --git a/tests/smoke-test.sh b/tests/smoke-test.sh index 2b5bc20c2..56d0fff3f 100755 --- a/tests/smoke-test.sh +++ b/tests/smoke-test.sh @@ -72,9 +72,9 @@ if [[ $skill_errors -eq 0 ]] && [[ $skill_count -gt 0 ]]; then fi # ============================================================================= -# Test 2b: Validate council/crank flag allowlists and contracts +# Test 2b: Validate the optional council strategy contract # ============================================================================= -log "Validating council/crank flag allowlists..." +log "Validating council strategy contract..." if [[ -x "$REPO_ROOT/tests/skills/validate-skill.sh" ]]; then if bash "$REPO_ROOT/tests/skills/validate-skill.sh" council "$REPO_ROOT/skills" > /tmp/validate-council.log 2>&1; then @@ -83,13 +83,6 @@ if [[ -x "$REPO_ROOT/tests/skills/validate-skill.sh" ]]; then fail "council validate-skill checks failed" tail -n 30 /tmp/validate-council.log | sed 's/^/ /' fi - - if bash "$REPO_ROOT/tests/skills/validate-skill.sh" crank "$REPO_ROOT/skills" > /tmp/validate-crank.log 2>&1; then - pass "crank validate-skill checks passed" - else - fail "crank validate-skill checks failed" - tail -n 30 /tmp/validate-crank.log | sed 's/^/ /' - fi else warn "tests/skills/validate-skill.sh missing or not executable" fi @@ -139,8 +132,7 @@ for runtime_test in \ "$REPO_ROOT/tests/skills/test-runtime-claude-code-smoke.sh" \ "$REPO_ROOT/tests/skills/test-runtime-codex-smoke.sh" \ "$REPO_ROOT/tests/skills/test-runtime-cursor-smoke.sh" \ - "$REPO_ROOT/tests/skills/test-runtime-opencode-smoke.sh" \ - "$REPO_ROOT/tests/scripts/test-headless-runtime-skills.sh"; do + "$REPO_ROOT/tests/skills/test-runtime-opencode-smoke.sh"; do [[ -f "$runtime_test" ]] || continue test_name="$(basename "$runtime_test")" if bash "$runtime_test" >"/tmp/${test_name}.log" 2>&1; then @@ -174,11 +166,10 @@ else fi # ============================================================================= -# Test 7: Claude CLI load test +# Test 7: Optional Claude CLI load test # ============================================================================= -log "Testing Claude CLI plugin load..." - -if command -v claude &>/dev/null; then +if [[ "${AGENTOPS_EXERCISE_CLAUDE_RUNTIME:-0}" == "1" ]] && command -v claude &>/dev/null; then + log "Testing explicitly requested Claude CLI plugin load..." load_output=$(timeout 10 claude --plugin-dir . --help 2>&1) || true if echo "$load_output" | grep -qiE "invalid manifest|validation error|failed to load"; then fail "Claude CLI load failed" @@ -186,130 +177,6 @@ if command -v claude &>/dev/null; then else pass "Claude CLI loads plugin" fi -else - warn "Claude CLI not available for load test" -fi - -# ============================================================================= -# Test 8: Flywheel loop (next-work round-trip) -# ============================================================================= -log "Testing flywheel loop (next-work round-trip)..." - -NEXTWORK_DIR="$REPO_ROOT/.agents/rpi" -NEXTWORK_FILE="$NEXTWORK_DIR/next-work.jsonl" -NEXTWORK_SCHEMA="$REPO_ROOT/docs/contracts/next-work.schema.md" - -# Check schema contract exists -if [[ -f "$NEXTWORK_SCHEMA" ]]; then - pass "next-work.schema.md exists" -else - fail "next-work.schema.md missing" -fi - -if [[ -x "$REPO_ROOT/scripts/validate-next-work-contract-parity.sh" ]]; then - if "$REPO_ROOT/scripts/validate-next-work-contract-parity.sh" >/dev/null 2>&1; then - pass "next-work contract parity validator passed" - else - fail "next-work contract parity validator failed" - fi -else - fail "scripts/validate-next-work-contract-parity.sh missing or not executable" -fi - -# Validate existing next-work.jsonl if present -if [[ -f "$NEXTWORK_FILE" ]]; then - if command -v jq &>/dev/null; then - # Validate each line is valid JSON with required fields - line_num=0 - nw_errors=0 - while IFS= read -r line; do - ((line_num++)) || true - [[ -z "$line" ]] && continue - if ! echo "$line" | jq -e ' - .source_epic and - ((.timestamp | type == "string") or (.created_at | type == "string")) and - (.consumed | type == "boolean") and - ( - ((.items | type) == "array") or - ((.title | type == "string") and (.type | type == "string") and (.severity | type == "string")) - ) - ' >/dev/null 2>&1; then - fail "next-work.jsonl line $line_num: missing required fields for batch or legacy flat schema" - ((nw_errors++)) || true - fi - # Validate optional target_repo is a string when present - if echo "$line" | jq -e 'has("target_repo")' >/dev/null 2>&1; then - if ! echo "$line" | jq -e '(.target_repo | type) == "string"' >/dev/null 2>&1; then - fail "next-work.jsonl line $line_num: target_repo must be a string" - ((nw_errors++)) || true - fi - fi - if echo "$line" | jq -e 'has("items") and (.items | type == "array")' >/dev/null 2>&1; then - if ! echo "$line" | jq -e 'all(.items[]?; (has("target_repo") | not) or (.target_repo | type == "string"))' >/dev/null 2>&1; then - fail "next-work.jsonl line $line_num: item target_repo must be a string" - ((nw_errors++)) || true - fi - if ! echo "$line" | jq -e 'all(.items[]?; (has("claim_status") | not) or (.claim_status == "available" or .claim_status == "in_progress" or .claim_status == "consumed"))' >/dev/null 2>&1; then - fail "next-work.jsonl line $line_num: item claim_status must be available|in_progress|consumed" - ((nw_errors++)) || true - fi - if ! echo "$line" | jq -e 'all(.items[]?; (has("claimed_by") | not) or (.claimed_by == null) or ((.claimed_by | type) == "string"))' >/dev/null 2>&1; then - fail "next-work.jsonl line $line_num: item claimed_by must be null or string" - ((nw_errors++)) || true - fi - if ! echo "$line" | jq -e 'all(.items[]?; (has("claimed_at") | not) or (.claimed_at == null) or ((.claimed_at | type) == "string"))' >/dev/null 2>&1; then - fail "next-work.jsonl line $line_num: item claimed_at must be null or string" - ((nw_errors++)) || true - fi - fi - if echo "$line" | jq -e 'has("claim_status")' >/dev/null 2>&1; then - if ! echo "$line" | jq -e '(.claim_status == "available" or .claim_status == "in_progress" or .claim_status == "consumed")' >/dev/null 2>&1; then - fail "next-work.jsonl line $line_num: claim_status must be available|in_progress|consumed" - ((nw_errors++)) || true - fi - fi - if echo "$line" | jq -e 'has("claimed_by")' >/dev/null 2>&1; then - if ! echo "$line" | jq -e '(.claimed_by == null) or ((.claimed_by | type) == "string")' >/dev/null 2>&1; then - fail "next-work.jsonl line $line_num: claimed_by must be null or string" - ((nw_errors++)) || true - fi - fi - if echo "$line" | jq -e 'has("claimed_at")' >/dev/null 2>&1; then - if ! echo "$line" | jq -e '(.claimed_at == null) or ((.claimed_at | type) == "string")' >/dev/null 2>&1; then - fail "next-work.jsonl line $line_num: claimed_at must be null or string" - ((nw_errors++)) || true - fi - fi - done < "$NEXTWORK_FILE" - if [[ $nw_errors -eq 0 ]]; then - pass "next-work.jsonl: all $line_num entries have valid schema" - fi - else - warn "jq not available — skipping next-work.jsonl schema validation" - fi -else - # Not an error — file only exists after first post-mortem with Step 8 - if [[ "$VERBOSE" == "--verbose" ]]; then - log " next-work.jsonl not present (expected before first flywheel cycle)" - fi - pass "next-work.jsonl absent (pre-flywheel state is valid)" -fi - -# ============================================================================= -# Test 9: RPI context-windowing contract (large-repo mode) -# ============================================================================= -log "Testing RPI context-windowing contract..." - -if [[ -x "scripts/rpi/context-window-contract.sh" ]]; then - cw_output="" - if cw_output=$(./scripts/rpi/context-window-contract.sh 2>&1); then - pass "RPI context-window contract passed" - else - fail "RPI context-window contract failed" - [[ "$VERBOSE" == "--verbose" ]] && echo "$cw_output" | sed 's/^/ /' - fi -else - fail "scripts/rpi/context-window-contract.sh missing or not executable" fi # =============================================================================