8000 aloop stop --clear: refuse every host's agent shell (M10.4) by adeelahmad · Pull Request #110 · adeelahmad/aloop · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ original plan.
| **M7 — Any host** | v0.18 | Session 9: `HostAdapter`, capability matrix, `.aladdin/config.yaml`, host swap, metrics (§4.9–4.10) | done: `HostAdapter` + `host check` in CI, capability matrix, usage accuracy, `.aladdin/config.yaml` with migration, `host swap`, the full metrics table incl. context-window share (tests: `host-adapter`, `portable-config`, `host-swap`, `metrics`); DeepSeek Harness (`dsh`) as a fourth host, tested on its real runtime (tests: `dsh-plugin`, `dsh-runtime`); a measured live dsh run is open (needs a DeepSeek key) |
| **M8 — Aladdin 1.0** | v1.0 | Session 10: `/aladdin` + `aladdin` CLI with aliases, site, measured public runs (§4.11). The rename to aloop shipped early in v0.11.0 | in progress: `bin/aladdin` + `/aladdin` with `aloop`/`agentic` aliases (test: `aladdin-cli.test.py`); natural-language auto-trigger on every host (`intent-trigger`, test: `intent-trigger.test.py`); README reordered product-first; site at `docs/index.html` (Pages switch is the owner's). evidence ledger `docs/evidence/README.md` + `run-summary` (test: `evidence.test.py`). `e2e-live.sh --host codex|opencode` runs the same loop on those hosts (`aloop install <host>` into the run repo, then `codex exec --json` / `opencode run --format json`; `--dry-run` prints the command; test: `e2e-live-hosts.test.py`). Open: the measured public runs themselves (this environment has no `codex`/`opencode` CLI: run `make e2e-live YES=1 HOST=codex` and `HOST=opencode` on a machine with both and add the rows; reason: a number we cannot measure is not published) Mac release run (2026-10-19) found two bugs Linux CI never sees, both fixed with tests: `stats sprint-close` counted 0 attempts when the project sat behind a symlink (macOS `/var` → `/private/var`; paths are now compared by realpath), and `make conformance` hung when another aloop checkout's `plugin/bin` was ahead on PATH (`ensure-tools --resolve` returned that checkout's shim; any aloop bin dir is now skipped). RED is now verified on any language: a non-cargo repo runs its `standards.md` matrix's test command, and a RED whose tests pass or do not collect is blocked (pytest, `go test`; test: `any-language.test.py`). Reason: the north star is any intent, and RED was dynamically checked only for Rust. GREEN and the final gate now refuse Python/Go/JS skips, and the final gate finds a planned `def`/`func` test (before, it found only `fn`, so every planned Python test blocked). `INTENT=slugify` is the Python fixture. Run 36 (slugify, the first Python run): 11/12, 11m37s, $0.83, 5 wakes, 0 plugin-source reads, 15 blocks (9 caught by self-checks). The sprint merged a correct `slugify` with 13 passing tests and the final gate passed. The one failed check was the fixture's own: bare `pytest` leaves the repo root off `sys.path` (now `python3 -m pytest`). It found two gate bugs that Rust runs had hidden. First, a worker's worktree has no `docs/agents/` (git-ignored), so the gates never found `standards.md` and ran the default Rust matrix: RED blocked 4 times on `cargo fmt`, and in a Rust project a custom matrix was silently swapped for the default. Second, `ctx-symbols conflicts` walked the merged task checkout under `.aladdin/worktrees/` and reported `slugify` as a HIGH duplicate, so the structural reviewer was rejected 4 times. Fixed with tests: the gates read the story's sprint `standards.md` (else this tree, else the main tree), and a non-cargo project with no matrix blocks with a message naming `standards.md`; ctx-symbols skips `.aladdin/`. Run 37 on these fixes: 12/12, 9m42s, $0.69, 1 wake, 11 blocks (7 caught by self-checks). It found that a `standards.md` whose matrix is a table parses empty, so RED blocked 3 times; `gate-standards-cited` now refuses a standards.md with no runnable `## Gate matrix` block (test: `any-language.test.py`). Run 38 (`INTENT=roman`, the first Go run): 12/12, 14m15s, $1.02, 3 wakes, 9 blocks (7 caught by self-checks). It found that the sprint dir's `init.md` had no `type: init` header (the supervisor began it with a dispatch block), so the structural reviewer was blocked 7 times; `aloop budget contract` now writes that header (test: `budget-contract.test.py`). Run 39 on the fix: 12/12, 7m52s, $0.56, 0 init.md blocks. It found that GREEN's default scope let it touch Rust test files only, so a Go GREEN removing RED's `_test.go` shim was blocked 10 times; GREEN now shares RED's any-language test globs (test: `any-language.test.py`). Run 40 on it: 12/12, 13m03s, $0.75, 1 wake, 0 GREEN scope blocks (5 blocks, all caught by self-checks). The structural reviewer's HIGH foundation-poisoning finding in runs 38 and 40 was a false positive: a worker's `selfcheck` resolved a stale 0.11.0 `ctx-symbols` (before the `.aladdin/` exclusion) ahead of the session's current build; `ensure-tools --resolve` now skips a stale own build (test: `ensure-tools.test.py`). RED is verified on JS too (a test file that cannot load what it imports is blocked; test: `any-language.test.py`), and `INTENT=camel` is the JS fixture. Run 41 (camel, the first JS run): 12/12, 6m45s, $0.56, 1 wake, 2 blocks (both RED self-checks refusing a source edit). The loop is now measured on Rust, Python, Go and JS. Run 42 (`INTENT=extend`, the first brownfield intent: add `compare` to a crate whose `parse` already works and is tested): 13/13, 7m11s, $0.67, 2 blocks (both self-check catches); the seed's code and tests were left unchanged and still pass (reason: every earlier fixture started from an empty library, and most real intents land on existing code). Run 43 (`INTENT=modify`: change that existing `parse` to accept a leading `v` and trim whitespace): 13/13, 9m10s, $0.67, 6 blocks, all self-check catches; the change was 3 lines in `parse`, with its signature and seed tests kept. `INTENT=cli` (an application: a wc-like binary tested by running it, checked by running the merged binary) is ready; run 44 (the retry after the usage limit lifted): 13/13, 24m55s, $1.54, 6 blocks (all self-check catches), and the merged binary works. Its cost came from planning: 2 stories and 7 tasks for a small tool, so 7 RED/SCAFFOLD/GREEN cycles and 56 supervisor turns (runs 42–43: 1–2 tasks, 16–30 turns). The planner now sizes tasks up to the budget instead of one per behaviour (reason: per-task cost is a fixed ~8 supervisor turns). Run 45 on it: 3 tasks (from 7), 13/13, 16m22s, $1.07. Its HIGH foundation-poisoning halts were `ctx-symbols` reading a test's `const USAGE` as a duplicate of the binary's; a test constant no longer counts (test: `conflicts.rs`). Run 46 on both fixes: 3 tasks, 13/13, 15m19s, $0.99, no review halt, 6/6 blocks self-check catches. Its 4 `targets` "symbol is not defined" blocks were T3 modifying symbols T2 plans in an existing file; a symbol another target plans now counts (test: `semantic-targets.test.py` case 8). Run 47 on it: 13/13, $1.04, 2 blocks (from 6), no "not defined"; but 5 tasks in 2 stories, so planner task count still varies. The spread is now bounded: before execution, `targets check` refuses two adjacent tasks of one story on the same files with 8 or fewer tests between them (replayed on past plans it would have merged runs 23, 40, 43, 44 and 47 and passed every other; test: `semantic-targets.test.py` case 9). Run 48 on it: 2 tasks, 13/13, $0.76, 25 supervisor turns (run 44: 7 tasks, $1.54, 56). One GREEN self-check block in each of runs 46–48 was a false positive: the Rust suppression grep matched an iterator's `args().skip(1)` (a JS pattern); Rust now checks only `#[ignore]` (also with a reason, which it used to miss) and `cfg(not(test))` (test: `suppression-contract.test.py`). Planner friction next: across runs 36–41 the most frequent block was `targets` "create needs planned_symbol" on a new test file (8); a test-file create now takes it from the task's `tests` (test: `semantic-targets.test.py`). The `gate-validate-artifact` plan.md refusals (4) were the persona's bare `stage: 1`, now quoted and checked (test: `persona-shapes.test.py`). Next: the final-gate block order (3). Running every suite under a locally built bash 3.2 found that the `standards.md` gate matrix was silently replaced by the default Rust matrix (`mapfile` does not exist in bash 3.2). It is fixed and guarded by `bash-portable.test.py`. A `macos` CI job now runs the host tests and the conformance fixtures on macOS (reason: both were found only by hand on the owner's Mac). |
| **M9 — Mission layer** | v0.13 | The "Missionary Aladdin Genie" spec (2026-10-17): a human-set mission above goals; fixed roles (research, product testing, state, evaluation, planning, ethics) build a confirmed baseline and a reproducible evaluation layer from user journeys; goals derived from the gap list become Aladdin intents; re-evaluate on a cadence and after each sprint; GA4 usage informs goals and is never a gate; every element has a deterministic check (§4.12). Reason: the owner's mission-driven loop, stacked on Aladdin | in progress: M9.0 core (`aloop mission init / check / approve / status`, sealed mission + approval events, area discovery, test glob; test: `mission-core.test.py`); M9.2 baseline, M9.3 journeys and M9.10 assess/evaluate (`mission-{baseline,journeys,evaluate}.test.py`). Open tracks: M9.1, M9.4–M9.9; the task graph is in §4.12, track notes in `plugin/mission/README.md`; hardened after an adversarial review (ledger records over status fields) |
| **M10 — Agent habitat** | v1.1 | Containment around the gates (§4.13): contained workers, a security and supply-chain gate, fetched content treated as data, one stop for a whole project. Reason: a 2026-10-20 comparison with Replit's "agent habitat" (orchestration, error recovery, safety and sandboxing, networking, identity) found aloop strong on orchestration, recovery and gates but missing containment | in progress: M10.2 dependency and secret halves done: GREEN refuses a dependency added to a manifest (Cargo.toml, package.json, requirements*.txt, pyproject.toml, go.mod) that the approved plan does not name (`bin/deps`), and a credential (private key, fixed-prefix token) in the lines it adds; `gate-final` refuses one anywhere in the project (`bin/secrets`, built in rather than a matrix line so it runs on every stack with no install; test: `supply-chain.test.py`); live run 51: no refusal, 13/13, $0.94). Next: M10.2 dependency audit (`cargo audit`, `npm audit`, `pip-audit`, `govulncheck`) in the final gate's matrix, which needs the standards role to write it and the tool on the host; M10.4 done: `aloop stop [REASON]` stops every agent of the project at its next tool call on every host (the check sits in `budget hook pre`), `mission tick` answers STOPPED, and only the human's own terminal clears it (`stop --clear`; events `run.stopped`/`run.resumed`; test: `project-stop.test.py`). Next for M10.4: a row in the committed mission ledger so a clone stays stopped, and the agent-shell markers of Codex, OpenCode and dsh (M9.31's list) for `--clear`; M10.1 and M10.3 planned; plan approval as a gate is M0.9 |
| **M10 — Agent habitat** | v1.1 | Containment around the gates (§4.13): contained workers, a security and supply-chain gate, fetched content treated as data, one stop for a whole project. Reason: a 2026-10-20 comparison with Replit's "agent habitat" (orchestration, error recovery, safety and sandboxing, networking, identity) found aloop strong on orchestration, recovery and gates but missing containment | in progress: M10.2 dependency and secret halves done: GREEN refuses a dependency added to a manifest (Cargo.toml, package.json, requirements*.txt, pyproject.toml, go.mod) that the approved plan does not name (`bin/deps`), and a credential (private key, fixed-prefix token) in the lines it adds; `gate-final` refuses one anywhere in the project (`bin/secrets`, built in rather than a matrix line so it runs on every stack with no install; test: `supply-chain.test.py`); live run 51: no refusal, 13/13, $0.94). Next: M10.2 dependency audit (`cargo audit`, `npm audit`, `pip-audit`, `govulncheck`) in the final gate's matrix, which needs the standards role to write it and the tool on the host; M10.4 done: `aloop stop [REASON]` stops every agent of the project at its next tool call on every host (the check sits in `budget hook pre`), `mission tick` answers STOPPED, and only the human's own terminal clears it (`stop --clear`; events `run.stopped`/`run.resumed`; test: `project-stop.test.py`). `--clear` refuses the agent shells of all four hosts (`CLAUDECODE`, `DSH_SHELL`, `CODEX_THREAD_ID`, and `ALOOP_AGENT_SHELL` once M9.31 makes the OpenCode plugin export it). Next for M10.4: a row in the committed mission ledger so a clone stays stopped; M10.1 and M10.3 planned; plan approval as a gate is M0.9 |

**Order rationale:** identity and conformance fixtures first, so every later change is
measured against today's behaviour; budgets and handoff next, because they shape every run;
Expand Down
2 changes: 1 addition & 1 deletion plugin/bin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ ctx-symbols) → WARN + grep fallback; never a silent pass.
| `intent` | the internal Intent Contract (ROADMAP §4.1): `new` issues the canonical `intent_id` + skeleton under `docs/agents/intents/`; `check FILE [--mode reviewed\|autonomous]` validates fields and the clarification policy (exit 2 invalid, 3 an `ask` needs the human); `summary FILE` lists defaults/assumptions/spikes for approval. Run by `gate-intake`. |
| `ids` | canonical identity + referential integrity (ROADMAP §4.5): `freeze SPRINT_DIR` checks the structured references (story frontmatter ↔ dir, dirs ↔ stories.md, validate.md ↔ tasks.md headings, `split_from` → a retired task) and issues frozen UUIDv7 ids in `sprintN/ids.json`; a retired alias is never reused · `check` · `resolve ALIAS`. Run by `gate-stage2-complete`; events then carry `story_uid`/`task_uid`. |
| `deps` | supply chain (ROADMAP M10.2): `added --base REF` lists the dependencies a change adds to Cargo.toml, package.json, requirements*.txt, pyproject.toml or go.mod · `unplanned --base REF --story DIR` the ones the story's plan does not name (exit 2); gate-green-verify blocks on them. No third-party parser. |
| `stop` | one stop for the whole project (ROADMAP M10.4): `aloop stop [REASON]` sets the stop flag (`.aladdin/state/STOP`) and records `run.stopped`; while it is set `budget hook pre`, which every host runs on every tool call, refuses each agent's acting calls and ends it at its next read, and `mission tick` answers STOPPED. `--status` (exit 3 when stopped). `--clear` resumes and records `run.resumed`, from the human's own terminal only (a shell with `CLAUDECODE` set is refused). |
| `stop` | one stop for the whole project (ROADMAP M10.4): `aloop stop [REASON]` sets the stop flag (`.aladdin/state/STOP`) and records `run.stopped`; while it is set `budget hook pre`, which every host runs on every tool call, refuses each agent's acting calls and ends it at its next read, and `mission tick` answers STOPPED. `--status` (exit 3 when stopped). `--clear` resumes and records `run.resumed`, from the human's own terminal only (a shell an agent host runs, `CLAUDECODE`, `DSH_SHELL`, `CODEX_THREAD_ID` or `ALOOP_AGENT_SHELL` set, is refused). |
| `secrets` | secret scan (ROADMAP M10.2): `scan [--base REF]` reports `path:line: kind` for a private key or a fixed-prefix token (AWS, GitHub, GitLab, Slack, Stripe, Google, Anthropic, OpenAI), never the text; with `--base` only added lines. gate-green-verify scans the task's added lines, gate-final the whole project (exit 2). `aloop: allow-secret` on a line exempts a fixture. |
| `targets` | semantic execution map (ROADMAP §4.6): the planner's `<story>/targets.json` — change targets and, with `tasks`, the whole semantic plan. `render STORY_DIR [--check]` writes tasks.md / validate.md / plan.md as deterministic views over it (the gate refuses a hand-edited view) · `check STORY_DIR` (run by `gate-stage2-complete`) · `target Tn STORY_DIR` (file:line, or `planned` for the shadow workspace) · `shadow` · `scope Tn STORY_DIR` (the diff scope gate-green-verify derives when SCOPE_GLOBS is unset) · `context Cn --budget N` (just the target's definition) · `refs\|callers\|callees` via the LSP server in `AGENTIC_LSP_COMMAND` (`_lsp.py`: references + call hierarchy); no server or a missing capability → exit 4, never guessed. Backend: ctx-symbols, else a definition regex flagged weaker. |
| `impact` | optional cross-project impact provider seam (ROADMAP §4.8): `query consumers\|rationale\|migration\|standards SUBJECT [--task]` runs `AGENTIC_IMPACT_PROVIDER` (defaults.md or env), keeps only results with a `source_ref`, saves them under `docs/agents/impact/` and emits `artifact.created`. No provider or a failing one → WARN, exit 0. Builds no index. |
Expand Down
6 changes: 4 additions & 2 deletions plugin/bin/stop
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ import _events # noqa: E402
import _paths # noqa: E402
import _stop # noqa: E402

# set in every shell an agent host runs (its Bash tool): a resume never comes from there
AGENT_SHELL = ("CLAUDECODE",)
# set in every shell an agent host runs (its Bash tool): a resume never comes from there. Claude
# Code, dsh and Codex set theirs; aloop's OpenCode plugin exports ALOOP_AGENT_SHELL (the door
# gate's list, M9.31)
AGENT_SHELL = ("CLAUDECODE", "DSH_SHELL", "CODEX_THREAD_ID", "ALOOP_AGENT_SHELL")


def now():
Expand Down
8 changes: 4 additions & 4 deletions scripts/test/project-stop.test.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@
atexit.register(shutil.rmtree, T, ignore_errors=True)
REPO = os.path.join(T, "repo")
WT = os.path.join(T, "wt")
AGENT_VARS = ("CLAUDECODE", "AGENTIC_HOST", "SKIP_HOOKS", "AGENTIC_NO_BUDGET", "CODEX_SANDBOX",
"OPENCODE", "DSH_SESSION")
AGENT_VARS = ("CLAUDECODE", "DSH_SHELL", "CODEX_THREAD_ID", "ALOOP_AGENT_SHELL", "AGENTIC_HOST", "SKIP_HOOKS",
"AGENTIC_NO_BUDGET")
ENV = {k: v for k, v in os.environ.items() if k not in AGENT_VARS}
ENV.update(HOME=os.path.join(T, "home"), PYTHONDONTWRITEBYTECODE="1")
n = [0]
Expand Down Expand Up @@ -115,7 +115,7 @@ def denied(out):
ok("mission tick answers STOPPED (the human's) with the reason while the project is stopped")

# 6) only the human clears it: an agent host's shell is refused, the human's own terminal is not
for var in ("CLAUDECODE",):
for var in ("CLAUDECODE", "DSH_SHELL", "CODEX_THREAD_ID", "ALOOP_AGENT_SHELL"):
r = run(STOP, "--clear", env=dict(ENV, **{var: "1"}))
assert r.returncode == 2 and "terminal" in r.stderr, r
assert run(STOP, "--status").returncode == 3
Expand All @@ -126,7 +126,7 @@ def denied(out):
assert not denied(hook("pre", "Bash", REPO, **SUPERVISOR)) and not denied(hook("pre", "Write", WT, **WORKER))
r = run(os.path.join(BIN, "mission"), "tick", "--json", "--peek")
assert json.loads(r.stdout)["action"] != "STOPPED", r.stdout
ok("`stop --clear` is refused in an agent's shell; from the human's terminal it resumes everything and records run.resumed")
ok("`stop --clear` is refused in an agent's shell on every host (Claude Code, dsh, Codex, OpenCode); from the human's terminal it resumes everything and records run.resumed")

# 7) the stop records are the human's: `events emit` cannot forge them
for name in ("run.stopped", "run.resumed"):
Expand Down
Loading
0