8000 Plan approval is a gate: no worker before the human's typed line (M0.9) by adeelahmad · Pull Request #99 · 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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,14 @@ tag in lockstep. Tag releases as `vMAJOR.MINOR.PATCH`.
## [Unreleased]

### Added
- Plan approval is a gate (ROADMAP M0.9): `gate-dispatch` starts no execution role (RED, SCAFFOLD, GREEN, structural review, final gate) of a sprint until the human approved that sprint's plan as it is now. `events authority --sprint N` exits 3 with the line to type, `approve SN <code>` (reviewed, the code naming the sha256 of the sprint's plan files) or `autonomous SN <code>` (autonomy for that sprint, re-plans included, the code naming the authority record); the task contract's `STORY_DIR` and task must be a live story and task of that plan's frozen `ids.json`; the line must be among the human's captured prompts and typed after the sprint's last decision (M9.17's mechanism). A changed plan, a re-plan in reviewed mode, a roll-back to an old line, another sprint's line, a negated line and a hand-edited or emitted record all block (`plan.approved` and `human.approval` cannot be `events emit`ted); an approved autonomous mission grants its own sprints autonomy with no line. `events approval SPRINT|TASK_ID` is the check. Codex's `spawn_agent` (`agent_type`, `message`) and OpenCode's `task` now reach `gate-dispatch` too, and `claude -p`, `codex exec` and `opencode run` from a tool call are blocked in every aloop project, not only where a mission is set. `e2e-live.sh` pre-approves with an `autonomous S1 <code>` line in its prompt. Reason: the north star allows no human help beyond approval, and the approval was a playbook rule a supervisor could skip. A 4-lens adversarial review found 9 defects: 7 fixed with a check red on the first head (cross-sprint or unplanned `STORY_DIR`, a retyped line kept stale, mission autonomy over a non-mission sprint, `transcripts prune` dropping the prompts grants cite, `codex plugin add` refused, a code past column 200, a crash on a malformed run-log line), 2 in the playbook (intake reuses a mission's handed-off intent; a reviewed re-plan waits for the human's new line). `transcripts prune` now keeps the human's prompts. Test: `plan-approval.test.py` (20 checks, red on master), plus the host suites.
- Core agent principles and the one-way / two-way door rule (ROADMAP §4.12, M9.30): seven core principles from Amazon's Leadership Principles, the door rule first, go to every aloop agent at session start (`aloop mission principles show --core`). A two-way door (a branch, a local edit, a test, a draft PR) is decided, not asked. A one-way door (`aloop mission doors list`: a push or merge to the default branch, a force-push, a release, a deploy, a message sent outside, a secret, dropped data, …) is stopped by a PreToolUse gate with `ASK[door] <id>` until the human records an authorization in their own terminal (`aloop mission doors authorize ID --by NAME --reason TEXT [--until DATE] [--once DIGEST]`, a hash-chained ledger row); a wall (switching a gate off, forging a human record) is never crossed. While a run is live, a question to the human must name a door the gate stopped and nobody authorized. Reason: Adeel's request that agents decide two-way doors themselves and ask only at a one-way door not already authorized. Test: `mission-doors.test.py`. Hardened by an adversarial review (15 findings, each a test): the gate reads Bash as the shell does (comments, line continuations, heredoc bodies, `xargs`, `find -exec`, `echo … | sh`), a branch condition reads the directory the command acts on (`git -C`, `cd`), settings are guarded wherever they live, dry runs and `command -v` are two-way, the newest decision by time counts and a revoke holds on every branch, a once-grant is crossed once under a lock, a broken ledger is named, and `doors authorize` refuses an agent's shell.
- DeepSeek Harness (dsh) host (ROADMAP §4.9): `aloop install dsh` (alias `deepseek`; `--project P` or `--global`) renders a `dsh --profile aloop` profile in `$DSH_HOME` and installs the skills. Its native plugin (`plugin/hosts/dsh/aloop.mjs`) runs the same gates as Claude Code on dsh's own events: a deny before a tool runs, a block after an invalid artifact write, and a stop gate that steers a role back up to 3 times before the supervisor is told it is BLOCKED (or STUCK, released unverified). Roles are agent presets on two subagent tools, one per model route (`deepseek-v4-pro` for planning, `deepseek-flash` for work by default; `MODEL_PLANNING_DSH` / `MODEL_WORKER_DSH` override). A writer's `aloop bind` moves its working directory into its worktree, and `host-adapter` confines its commands, edits and `working_directory` there. Usage is recorded Claude-shaped per role, so budgets, the ledger and `run-summary` work unchanged. `make e2e-live HOST=dsh` drives it live and exits 2 with the fix when no `DEEPSEEK_API_KEY` is set. Reason: DeepSeek's own harness is the cheapest route to a strong model. The profile records the project it serves (or `--global`), so another project's `aloop-init` never re-renders it, and gates get the worker shell's environment (no API key). The mission layer's six roles run on dsh too: presets on their route group's subagent tool, `gate-mission-scope` on every write and command, and `gate-mission-artifact` at each role's start and stop; the mission skill installs as `aloop-mission`. A second review (3 lenses; 11 confirmed findings, each fixed with a test) closed: a working directory that vanished (dsh's tools move it back to the main tree, so the plugin judges `ensure()`'s directory and `host-adapter` refuses to run from a missing one), gate switches from the project's `.env` (dsh loads it; `SKIP_HOOKS`, `GATE_RUN_TESTS`, `REPO_DIR`, `AGENTIC_*` from it never reach a gate), an interrupted Stop that dropped the sprint-close notice, and the shared profile: its owner is the main tree, an older unrecorded profile is claimed only by the project that instal 8000 led it, and a project it does not serve is told so by `install status`, `aloop-init` and the session itself. Tests: `dsh-plugin.test.mjs` (40 checks, emulated), `dsh-runtime.test.mjs` (32 checks on the real pinned dsh 0.2.1-rc.2 with a scripted model; the CI hosts job installs it) and `dsh-install.test.py` (9).
- Brand kit and repo front page: `docs/brand/` holds the mark (an **a** whose bowl is a red-to-green loop passing a gold gate, with a 16 px cut), lockups, light and dark README banners, favicons, app icons, the site's OG card and the GitHub social preview, all generated by `docs/brand/gen_brand.py` from one palette with WCAG checks (rules in `docs/brand/README.md`). The README leads with the mission layer, the agent principles and the door rule, the supported hosts and the measured demo runs; the site carries the favicon and Open Graph/Twitter card metadata; `SECURITY.md` and bug/feature issue forms are new.
- A mission's gap lists count only as `mission evaluate` computed them (ROADMAP §4.12, M9.27): `evaluate` records `eval.completed` in the committed ledger, and `mission tick`, `evaluate check` and `goal` take the newest eval whose content that record covers, with a matching seal, in the ledger's order. An eval file written by hand, or edited (or re-stamped) after it was recorded, no longer sends the loop to `RESEARCH` as "goal met" or makes `goal derive` refuse what tick handed out, a clock that is behind no longer reorders them, and a fresh checkout derives and closes goals from the evals it committed. An eval or state named as journey or baseline evidence needs its record too. Tests: `mission-tick` check 32, `mission-journeys`.
- A mission's stop at a spend cap holds on a fresh checkout (ROADMAP §4.12, M9.24): the first `STOP_BUDGET` tick records a `mission.budget_exhausted` ledger row for the mission's content (once per content), and `mission tick` stops on it whatever the checkout's token ledger says, until a raised cap changes the content.
- Lifting a mission block is the human's own words (ROADMAP §4.12, M9.51): `aloop mission tick --force --by` now asks (exit 3) for the human's typed `lift <mission> <code>` line, the code bound to this occurrence of that block, and records a `mission.lifted` ledger row, so `--by` alone, a lift only the run log has (or a replayed one), an old line or a line typed for an earlier block lift nothing, a lift of 3 failed goals reaches only those goals whatever `--now` says, and a fresh checkout keeps a lifted block lifted. `/aloop:mission` counts a lift among the typed grants it relays. A lift recorded before this (run data only) no longer counts.
- The mission layer's six fixed roles (ROADMAP §4.12, M9.9): `market-researcher`, `product-tester`, `state-assessor`, `evaluator`, `mission-planner` and `ethics-reviewer`, each a persona with a four-case eval suite, a model route (the four that judge on the planning route, the two that measure on the worker route), hook matchers on Claude Code, Codex, OpenCode and dsh, and Codex/OpenCode agents and dsh presets from `aloop install` (which also renders the mission skill for OpenCode as `aloop-mission` once it ships). `plugin/mission/roles.json` names the paths each role writes. `gate-mission-scope` (PreToolUse) is the role path guard: a mission role never writes source, mission.json or another role's dir, never commits and never runs a human's approval; a builder never writes `docs/agents/mission/` or runs a writing `mission` command; the supervisor never hand-writes a role's artifact. No one hand-writes `docs/agents/mission/ledger.jsonl` or the captured prompts a grant reads (`<run data>/transcripts/`), runs a hook's `transcripts prompt|record|…`, or starts a host session (`claude -p`, `codex exec`, `opencode run`, `dsh <profile>`) from a tool call where a mission is set (its prompt would count as the human's words, M9.17), and no dispatched agent runs `mission baseline restore`, `events authority` or an `events emit` of a human event. `gate-mission-artifact` records what exists when a role starts and, at its stop, requires an artifact in its own paths that passes the core contract and its area's check, refuses a deleted artifact, a change to another role's or a ledger that no longer verifies, and records `artifact.created|updated` as provenance. A mission write never arms the sprint lock. The scope gate reads a Bash command as the shell would (`_shellread.py`: quotes, comments, line continuations, heredocs, wrappers such as `timeout` and `env`, `bash -c`, `eval`, a script run by an interpreter, variables and `$(mktemp)`). Each area's check runs once per artifact with its id or subject, an ethics hard line is the reviewer's finding, not a defect, and a derived goal whose only blocks are its pending ethics review waits on the Ethics Reviewer, not on the planner. Test: `mission-roles.test.py` (33 checks, with each role run through the real journeys, baseline, assess, evaluate and ethics, goal, research and judge areas), `mission-scope-bash.test.py` (225 commands), `opencode-plugin.test.mjs`.
- The mission layer's six fixed roles (ROADMAP §4.12, M9.9): `market-researcher`, `product-tester`, `state-assessor`, `evaluator`, `mission-planner` and `ethics-reviewer`, each a persona with a four-case eval suite, a model route (the four that judge on the planning route, the two that measure on the worker route), hook matchers on Claude Code, Codex, OpenCode and dsh, and Codex/OpenCode agents and dsh presets from `aloop install` (which also renders the mission skill for OpenCode as `aloop-mission` once it ships). `plugin/mission/roles.json` names the paths each role writes. `gate-mission-scope` (PreToolUse) is the role path guard: a mission role never writes source, mission.json or another role's dir, never commits and never runs a human's approval; a builder never writes `docs/agents/mission/` or runs a writing `mission` command; the supervisor never hand-writes a role's artifact. No one hand-writes `docs/agents/mission/ledger.jsonl` or the captured prompts a grant reads (`<run data>/transcripts/`), runs a hook's `transcripts prompt|record|…`, or starts a host session (`claude -p`, `codex exec`, `opencode run`, `dsh <profile>`) from a tool call (in every aloop project since M0.9; its prompt would count as the human's words, M9.17), and no dispatched agent runs `mission baseline restore`, `events authority` or an `events emit` of a human event. `gate-mission-artifact` records what exists when a role starts and, at its stop, requires an artifact in its own paths that passes the core contract and its area's check, refuses a deleted artifact, a change to another role's or a ledger that no longer verifies, and records `artifact.created|updated` as provenance. A mission write never arms the sprint lock. The scope gate reads a Bash command as the shell would (`_shellread.py`: quotes, comments, line continuations, heredocs, wrappers such as `timeout` and `env`, `bash -c`, `eval`, a script run by an interpreter, variables and `$(mktemp)`). Each area's check runs once per artifact with its id or subject, an ethics hard line is the reviewer's finding, not a defect, and a derived goal whose only blocks are its pending ethics review waits on the Ethics Reviewer, not on the planner. Test: `mission-roles.test.py` (33 checks, with each role run through the real journeys, baseline, assess, evaluate and ethics, goal, research and judge areas), `mission-scope-bash.test.py` (225 commands), `opencode-plugin.test.mjs`.
- The mission loop end to end, offline (ROADMAP §4.12, M9.13): `mission-e2e.test.py` drives `mission tick` from a new mission through research, a journey, a confirmed baseline, a gap list, a goal, its intents and one Aladdin sprint per intent to GOAL MET and the next raise of the bar, with zero tokens and each step done through the CLI the way its role or the human does it. A fresh clone hands out the same action. Ethics reviews and hard lines are now ledger rows (M9.24), so a clone no longer asks for every review again or forgets a hard line; the ledger row wins over its run-log copy, and a broken ledger makes every ethics verdict `broken`. Tests: `mission-e2e.test.py`, `mission-ethics.test.py`.
- A mission baseline binds the mission's direction (ROADMAP §4.12, M9.25): `direction_sha256` covers the mission id, statement, mode and constraints, so raising a budget cap or changing the cadence (and approving it) keeps the confirmed baseline, while a new direction still asks for a new version. Reason: M9.8's budget test, a raised cap asked for a new baseline though "best" had not changed. Test: `mission-baseline.test.py`.
- Mission approvals are the human's own words (ROADMAP §4.12, M9.17): `mission approve`, `baseline confirm|restore`, `journeys approve`, `mission ethics consent` and `mission telemetry consent` record only when a prompt the `UserPromptSubmit` hook captured has a line such as `approve msn_… 3f2a9c1b` (the verb and the first 8 hex of the content's sha256); until then they ask (exit 3) with that line. `--by` alone, a negated line, another code, a line older than a later decision about the subject (for a baseline, any version's confirmation) and a hand-appended ledger row without such a line grant nothing. Reason: an approval was only as strong as `--by`, so a role could approve its own mission change. Tests: `mission-core.test.py`, `mission-baseline.test.py`.
Expand Down
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ shellcheck: ## Lint gate scripts (skipped with a warning if shellcheck is absent
pycheck: ## Byte-compile the Python hook scripts
@python3 -m py_compile plugin/bin/budget plugin/bin/stats plugin/bin/aloop-init plugin/bin/gate-commit-author plugin/bin/_paths.py \
plugin/bin/_usage.py plugin/bin/_events.py plugin/bin/events plugin/bin/intent plugin/bin/lessons plugin/bin/ids plugin/bin/targets plugin/bin/impact plugin/bin/timebox plugin/bin/sweep plugin/bin/capabilities plugin/bin/models plugin/bin/_routes.py plugin/bin/host plugin/bin/_host.py plugin/bin/config plugin/bin/_config.py plugin/bin/layout plugin/bin/_lsp.py scripts/test/fixtures/fake-lsp.py plugin/bin/_timebox.py plugin/bin/host-adapter plugin/bin/install \
plugin/bin/mission plugin/bin/_mission.py plugin/bin/_mission_roles.py plugin/bin/_shellread.py plugin/bin/gate-mission-scope plugin/bin/gate-mission-artifact $$(ls plugin/mission/*.py 2>/dev/null) \
plugin/bin/_approval.py plugin/bin/mission plugin/bin/_mission.py plugin/bin/_mission_roles.py plugin/bin/_shellread.py plugin/bin/gate-mission-scope plugin/bin/gate-mission-artifact $$(ls plugin/mission/*.py 2>/dev/null) \
&& rm -rf plugin/bin/__pycache__ plugin/mission/__pycache__ && echo "python ok"

.PHONY: test-hosts
Expand Down Expand Up @@ -124,6 +124,7 @@ test-hosts: ## Budget/worktree regression + Codex hooks emulation + OpenCode and
python3 scripts/test/planner-stuck.test.py
python3 scripts/test/hook-matchers.test.py
python3 scripts/test/dispatch.test.py
python3 scripts/test/plan-approval.test.py
python3 scripts/test/log-execution.test.py
python3 scripts/test/ensure-tools.test.py
python3 scripts/test/fail-closed.test.py
Expand Down
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,9 @@ its tests and the resolved change targets (`targets context`), never the whole r
Determinism is structural, not vibes. Hooks enforce what the model cannot talk its way past:

- the supervisor cannot write production source (`gate-supervisor-scope`), commit on main during a
sprint, or dispatch with its backends missing (`gate-tooling`) or on a model the project did not
pick (`gate-dispatch`);
sprint, or dispatch with its backends missing (`gate-tooling`), on a model the project did not
pick, or before the human approved the sprint's plan, or granted it autonomy, in a line they typed
(`gate-dispatch`);
- a worker cannot leave its worktree, touch files outside its task, or pass a stop whose gate failed;
- a gate that crashes **blocks** (`_failclosed.sh`), never a silent pass, and never a false block when a
backend is absent (it degrades to a flagged, weaker check);
Expand Down Expand Up @@ -330,7 +331,7 @@ structural, not luck:
blocking it from doing the context-heavy work itself.

Live runs: `make smoke-live YES=1` (hook wiring, ~$0.004) and `make e2e-live YES=1` (the whole
loop, ~$1; each run writes `summary.json` and a row for [docs/evidence/](docs/evidence/README.md)). Offline: `make ci`, which runs Rust fmt/clippy/tests, shellcheck, 54 Python/Node suites plus every `mission-*.test.py`,
loop, ~$1; each run writes `summary.json` and a row for [docs/evidence/](docs/evidence/README.md)). Offline: `make ci`, which runs Rust fmt/clippy/tests, shellcheck, 55 Python/Node suites plus every `mission-*.test.py`,
the gate conformance fixtures (`make conformance`) and eval-suite validation. Every ROADMAP
requirement is tracked with its test in [docs/TRACKING.md](docs/TRACKING.md).

Expand Down
5 changes: 4 additions & 1 deletion docs/OVERVIEW.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,10 @@ The model can ignore a skill; it cannot ignore an exit code.
- `standards` produces a cited coding standard;
- `planner` writes the sprint contract and, per story, the semantic plan `targets.json`
(tasks, tests, change targets) from which `tasks.md` / `validate.md` / `plan.md` are rendered.
The supervisor then **stops for approval**.
The supervisor then **stops for approval**, and that stop is a gate: `gate-dispatch` starts no
worker until the human types the `approve SN <code>` line naming this exact plan, and a changed plan
needs a new line; or they grant the sprint autonomy (`autonomous SN <code>`, or an approved
autonomous mission), which covers its re-plans (M0.9).
3. **Execution** — human absent. For each task, a worker runs in its own git worktree:
- **RED** writes tests that must fail *by assertion* and touch no production code;
- **SCAFFOLD** creates each planned symbol exactly once, as a stub;
Expand Down
6 changes: 4 additions & 2 deletions docs/ROADMAP.md

Large diffs are not rendered by default.

Loading
Loading
0