mirror of
https://github.com/boshu2/agentops.git
synced 2026-09-14 15:08:13 +08:00
2ea3c12b7a
## Why
/evolve self-stops in operator-driven cron sessions because its defaults
assume agent-as-operator. `--mode=loop` flips the contract: operator
owns stop signals; CLI mechanically refuses self-written STOPs.
## What changed
| Surface | Change |
|---|---|
| `cli/cmd/ao/evolve.go` | New `--mode string` flag (burst\|loop,
default burst) + `validateEvolveMode` helper called from `runEvolve` |
| `cli/cmd/ao/evolve_write_stop_marker.go` | New subcommand; refuses
with exit 1 under `--mode=loop`, writes `.agents/evolve/<MARKER>` under
burst |
| `cli/cmd/ao/evolve_test.go` | L1: `--mode=invalid` rejected; mode flag
registered with burst default |
| `cli/cmd/ao/evolve_write_stop_marker_test.go` | L2 table tests: burst
writes (DORMANT body assertion), loop refuses {dormant,stop,kill}, no
file written under refusal |
| `skills/evolve/SKILL.md` | Mode-aware HTML comments in Steps 1/3/7 +
Flag table row (only edited Steps 1/3/7 + Flag table; verified via grep
`^### Step`) |
| `skills/evolve/references/loop-mode.md` | New reference doc with
invariants + CLI primitives |
| `docs/plans/2026-05-21-evolve-loop-epic-design.md` | Epic design memo
(consumed by all 6 sub-beads of soc-g2qd) |
| `cli/docs/COMMANDS.md` | Regenerated via
`scripts/generate-cli-reference.sh` |
|
`skills-codex/{.agentops-manifest.json,evolve/.agentops-generated.json}`
| Post-edit hook source-hash refresh; `check-codex-parity-drift.sh` PASS
|
## How tested
- `go test -count=1 ./cmd/ao/` — 10175 pass, 1 skip (full suite green
after CLI-docs regen; pre-regen `TestCobraConformance` correctly flagged
the doc drift)
- `go build ./... && go vet ./...` — green
- `grep '^### Step' skills/evolve/SKILL.md` confirms only Steps 1/3/7
edited (12 Step markers preserved at same positions)
- `bash scripts/check-codex-parity-drift.sh` — PASS
- Table tests assert exact marker filename (DORMANT/STOP/KILL) + body
content for burst; for loop they assert error substring AND that no
marker file was created.
## §A7 fallback decision
**Chose in-file HTML comments** (not two-file split). Each Step comment
is one line; no conditional sections needed. Two-file split would have
ballooned the surface for a single-line contract reminder. CLI
mechanical enforcement (`write-stop-marker` refusal) is the real
invariant; SKILL.md comments only document intent for human readers.
See: docs/plans/2026-05-21-evolve-loop-epic-design.md §A1 + §A7
## TODOs the worker leaves for the epic
- Wire `cli/internal/evolve.preferences.Load()` into
`detectEvolveWriteStopMarkerMode` once soc-6svt lands (currently
flag-only).
- `ao evolve operator-stop` (Wave 2) — separate code path explicit
operator override.
- `ao evolve blocked` (Wave 2) — typed blocked event instead of STOP.
Closes-scenario: soc-hwax#mode-loop-flag
Bounded-context: BC5-Runtime
Evidence: cli/cmd/ao/evolve_write_stop_marker.go
92 lines
4.3 KiB
Bash
Executable File
92 lines
4.3 KiB
Bash
Executable File
#!/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
|
|
|
|
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
|
|
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
|
|
errors=1
|
|
fi
|
|
|
|
if [[ ! -f "$REPO_ROOT/.gitignore" ]]; then
|
|
echo "ERROR: root .gitignore missing; cannot enforce /.agents/ ignore policy." >&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
|
|
fi
|
|
|
|
echo "no disallowed tracked repo-root .agents state"
|