8000 gate-supervisor-scope: the supervisor never commits on main during a sprint by adeelahmad · Pull Request #28 · 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: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ tag in lockstep. Tag releases as `vMAJOR.MINOR.PATCH`.

## [Unreleased]

### Added
- `gate-supervisor-scope` now also enforces **the supervisor never commits on main**: a `git commit` from the main agent in the main tree while a sprint is live is blocked (a commit inside a task worktree, the initial commit of an unborn repo, `task-worktree merge` and a stale lock pass). Reason: live e2e run 16's `Track Cargo.lock` commit on main traced to nothing; the rule was prose in SKILL.md only. Test: `scripts/test/supervisor-scope.test.py`.

## [0.12.0] - 2026-10-18

The first release of **aloop** (Aladdin's Loop), the plugin formerly published as agentic-agile.
Expand Down
2 changes: 1 addition & 1 deletion docs/ROADMAP.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/TRACKING.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Status of every requirement in [ROADMAP.md](ROADMAP.md), in the four states §8
| 4.7 | Event stream `aladdin.event/1` | IMPLEMENTED_TESTED | `scripts/test/events.test.py` |
| 4.7 | Decision rationale + authority record | IMPLEMENTED_TESTED | `scripts/test/events.test.py` |
| 4.7 | Lineage: commit → intent → approver | IMPLEMENTED_TESTED | `scripts/test/commit-trace.test.py` |
| 4.7 | Every commit on main is a task's: the supervisor's own `git commit` on main is blocked mid-sprint | IMPLEMENTED_TESTED | `scripts/test/supervisor-scope.test.py` |
| 4.7 | Commit messages from rationale; graph export | IMPLEMENTED_TESTED | `scripts/test/commit-trace.test.py` |
| 4.8 | Cross-project impact provider seam | IMPLEMENTED_TESTED | `scripts/test/impact-provider.test.py` |
| 4.9 | Usage reporting: classes apart, exact or marked, unreported classes named | IMPLEMENTED_TESTED | `scripts/test/budget-meter.test.py` |
Expand Down
2 changes: 1 addition & 1 deletion plugin/bin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ ctx-symbols) → WARN + grep fallback; never a silent pass.
## Gates (which hook fires each)
| script | event · matcher | checks | exit 2 when |
|--------|-----------------|--------|-------------|
| gate-supervisor-scope | PreToolUse · Write\|Edit\|MultiEdit\|Bash | MAIN agent (no `agent_type`) may not write production source mid-sprint, nor hand-author planning artifacts (stories/tasks/validate/plan/intake/standards.md — dispatch the activity); sub-agents (`agent_type` present) exempt | supervisor writes source / hand-writes a planner-owned artifact |
| gate-supervisor-scope | PreToolUse · Write\|Edit\|MultiEdit\|Bash | MAIN agent (no `agent_type`) may not write production source mid-sprint, nor hand-author planning artifacts (stories/tasks/validate/plan/intake/standards.md — dispatch the activity), nor `git commit` on main (commits on main arrive through `task-worktree merge`); sub-agents (`agent_type` present) exempt | supervisor writes source / hand-writes a planner-owned artifact / commits on main |
| gate-dispatch | PreToolUse · Agent\|Task | the MAIN agent's dispatch of an execution role opens with `TASK_ID: <id>` and that task's contract `<run-data>/state/tasks/<id>.env` exists (repos with a run-data dir only) | no TASK_ID line / contract not written yet |
| gate-tooling | SubagentStart · exec roles + supervisor self-check | md-db + ctx-symbols installed (execution may not run grep-degraded); runs `ensure-tools --sync` first | a backend is missing and can't be built |
| gate-validate-artifact | PostToolUse · Write | md-db-validate the written artifact | artifact malformed |
Expand Down
73 changes: 56 additions & 17 deletions plugin/bin/gate-supervisor-scope
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,22 @@ file="$(field file_path)"
[ -n "$file" ] || file="$(field filePath)"
session="$(field session_id)"

root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=/dev/null
[ -n "${AGENTIC_HOME:-}" ] || . "$HERE/_paths.sh"
lock="$AGENTIC_HOME/state/sprint.lock" # fixed path (bin/_paths.sh), git-ignored

lock_recent() {
[ -f "$lock" ] || return 1
local now m
now="$(date +%s 2>/dev/null)" || return 1
# GNU first: on Linux `stat -f` means "filesystem status" and prints a report instead of
# failing, which used to crash this arm (and a crashed hook does not block).
m="$(stat -c %Y "$lock" 2>/dev/null)" || m="$(stat -f %m "$lock" 2>/dev/null)" || return 1
case "$m" in ''|*[!0-9]*) return 1 ;; esac
[ $(( now - m )) -lt "${AGENTIC_SPRINT_LOCK_TTL:-14400}" ]
}
# Codex edits files with apply_patch: tool_input is {"command": "<patch text>"}, the paths
# live in the patch headers. Check each path exactly as if it were a Write.
if [ "$tool" = "apply_patch" ] && command -v python3 >/dev/null 2>&1; then
Expand All @@ -58,7 +74,46 @@ fi
# A dispatched agent's Bash may READ the task contract but not rewrite it (redirect, tee,
# sed -i, cp/mv/rm/...). Heuristic: the command names the contract AND has a write verb.
if [ "$tool" = "Bash" ]; then
[ -n "$(field agent_type)" ] || exit 0
if [ -z "$(field agent_type)" ]; then
# MAIN agent: it never commits on main. Every commit on main arrives through
# `task-worktree merge`, which is what makes it traceable to its task (events trace).
# Live e2e run 16: a supervisor's `Track Cargo.lock` commit was the one commit that
# traced to nothing. Judged only while a sprint is live here (lock), in the MAIN tree
# (a commit inside a task worktree is a worker's, even if the supervisor runs it), and
# never for an unborn repo (the initial commit is the worktree base SKILL.md asks for).
[ -n "$session" ] || exit 0
[ -f "$lock" ] || exit 0
[ "$(head -1 "$lock" 2>/dev/null)" = "$session" ] || lock_recent || exit 0
cmd="$(printf '%s' "$payload" | python3 -c 'import json,sys; print((json.load(sys.stdin).get("tool_input") or {}).get("command") or "")' 2>/dev/null)"
# Per simple command: `git [-C <dir>] [opts] commit`. Prints the -C dir (or "") per hit.
hits="$(printf '%s' "$cmd" | python3 -c '
import re, sys
cmd = sys.stdin.read()
for seg in re.split(r"[;&|\n]+", cmd):
m = re.search(r"(?:^|[\s(])git(?:\s+-C\s+([^\s;&|]+))?(?:\s+-[-\w=]+)*\s+commit(?=\s|$)", seg)
if m: print(m.group(1) or ".")' 2>/dev/null)"
[ -n "$hits" ] || exit 0
. "$HERE/_failclosed.sh"
cwd="$(field cwd)"; [ -n "$cwd" ] || cwd="$PWD"
while IFS= read -r d; do
[ -n "$d" ] || continue
case "$d" in /*) dir="$d" ;; *) dir="$cwd/$d" ;; esac
git -C "$dir" rev-parse -q --verify HEAD >/dev/null 2>&1 || continue # unborn: initial commit
gd="$(cd "$dir" && cd "$(git rev-parse --git-dir 2>/dev/null)" 2>/dev/null && pwd -P)"
cd_="$(cd "$dir" && cd "$(git rev-parse --git-common-dir 2>/dev/null)" 2>/dev/null && pwd -P)"
[ -n "$gd" ] && [ "$gd" = "$cd_" ] || continue # linked worktree: a worker's
echo "BLOCK[$GATE_NAME]: the supervisor never commits on main during a sprint:" >&2
echo " $cmd" >&2
echo " Every commit on main arrives through \`aloop task-worktree merge\`, which is what makes it" >&2
echo " traceable to its task and intent (events trace). An untracked file you notice (a lockfile," >&2
echo " a generated file) is not yours to commit: leave it, or add it to the task whose worker" >&2
echo " produces it. (Deliberate out-of-band commit outside the pipeline? Re-run with SKIP_HOOKS=1.)" >&2
exit 2
done <<EOF_HITS
$hits
EOF_HITS
exit 0
fi
# Per simple command (split on ; | & newlines): a redirect INTO the contract, or a write
# verb in the same segment as a contract path.
if printf '%s' "$payload" | python3 -c '
Expand Down Expand Up @@ -99,12 +154,6 @@ if [ -n "$agent_type" ]; then
exit 0
fi

root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=/dev/null
[ -n "${AGENTIC_HOME:-}" ] || . "$HERE/_paths.sh"
lock="$AGENTIC_HOME/state/sprint.lock" # fixed path (bin/_paths.sh), git-ignored

# Fallback (only if a payload ever lacks agent_type): a worktree task.env naming a
# dispatched role still exempts that agent.
for te in "$root/.aladdin/task.env" "$root/.agentic/task.env"; do
Expand Down Expand Up @@ -172,16 +221,6 @@ if [ -f "$lock" ]; then
# shellcheck source=/dev/null
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_failclosed.sh"
fi
lock_recent() {
[ -f "$lock" ] || return 1
local now m
now="$(date +%s 2>/dev/null)" || return 1
# GNU first: on Linux `stat -f` means "filesystem status" and prints a report instead of
# failing, which used to crash this arm (and a crashed hook does not block).
m="$(stat -c %Y "$lock" 2>/dev/null)" || m="$(stat -f %m "$lock" 2>/dev/null)" || return 1
case "$m" in ''|*[!0-9]*) return 1 ;; esac
[ $(( now - m )) -lt "${AGENTIC_SPRINT_LOCK_TTL:-14400}" ]
}
if [ -f "$lock" ] && { [ "$(head -1 "$lock" 2>/dev/null)" = "$session" ] || lock_recent; }; then
echo "BLOCK[$GATE_NAME]: the supervisor may not write production source during a sprint:" >&2
echo " $file" >&2
Expand Down
3 changes: 2 additions & 1 deletion plugin/skills/aloop/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -533,7 +533,8 @@ Your job during execution is small, and that is on purpose (it keeps your contex
which is what makes it traceable to its task and intent (`events trace`). An untracked file you
notice (a lockfile, a generated file) is not yours to commit: leave it, or add it to the task
whose worker produces it (live run 16: a supervisor's `Track Cargo.lock` commit was the one
commit on main that traced to nothing).
commit on main that traced to nothing). `gate-supervisor-scope` blocks such a commit while the
sprint is live; the initial commit of an unborn repo and commits inside a task worktree pass.

On a host without background task notifications, `sweep wait --timeout 1800` is the
sleep: it blocks without a model call until an event or attempt state changes, then sweeps.
Expand Down
50 changes: 50 additions & 0 deletions scripts/test/supervisor-scope.test.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,54 @@ def bash(cmd, agent_type="aloop:green-worker"):
assert r.returncode == 0, (cmd, r)
ok("a sub-agent may still read the contract and redirect other output")


# ── The supervisor never commits on main (live e2e run 16: a `Track Cargo.lock` commit on main
# traced to nothing). A `git commit` from the MAIN agent in the main tree, while the sprint
# lock is live, is blocked; a worker's commit in its worktree, the initial commit of an
# unborn repo, `task-worktree merge` and a stale lock all pass.
subprocess.run(["git", "config", "user.email", "t@t"], cwd=T, check=True)
subprocess.run(["git", "config", "user.name", "t"], cwd=T, check=True)


def sup(cmd, cwd=None, session="s-main"):
p = {"session_id": session, "cwd": cwd or T, "tool_name": "Bash", "tool_input": {"command": cmd}}
return subprocess.run([GATE], input=json.dumps(p), cwd=cwd or T, env=ENV, capture_output=True, text=True)


open(LOCK, "w").write("s-main\n")
r = sup("git add -A && git commit -m init")
assert r.returncode == 0, r
ok("unborn HEAD: the initial commit (the worktree base) is allowed even with a live lock")

open(os.path.join(T, "README"), "w").write("x\n")
subprocess.run(["git", "add", "-A"], cwd=T, check=True)
subprocess.run(["git", "commit", "-qm", "init"], cwd=T, check=True)
for cmd in ("git add Cargo.lock && git commit -m 'Track Cargo.lock'", "git commit -am wip",
"cargo build; git -C %s commit -m x" % T, "git commit --amend --no-edit"):
r = sup(cmd)
assert r.returncode == 2 and "never commits on main" in r.stderr, (cmd, r)
ok("live sprint: the supervisor's git commit on main is blocked")

for cmd in ("git status", "git log --oneline", "aloop task-worktree merge S1-01-T1", "git add -A", "git diff", "echo commit"):
r = sup(cmd)
assert r.returncode == 0, (cmd, r)
ok("live sprint: reads, staging and task-worktree merge pass")

WT = os.path.join(T, ".aladdin", "worktrees", "S1-01-T1")
subprocess.run(["git", "worktree", "add", "-q", "-b", "t1", WT], cwd=T, check=True)
r = sup("git add -A && git commit -m 'S1-01-T1 RED'", cwd=WT)
assert r.returncode == 0, r
r = sup("git -C %s commit -m 'S1-01-T1 GREEN'" % WT)
assert r.returncode == 0, r
ok("a commit inside a task worktree passes (that is where work is committed)")
r = bash("git add -A && git commit -m 'S1-01-T1 RED'")
assert r.returncode == 0, r
ok("a dispatched worker's git commit is never judged here")

os.utime(LOCK, (old, old))
open(LOCK, "w").close(); open(LOCK, "w").write("s-other\n"); os.utime(LOCK, (old, old))
r = sup("git commit -am wip")
assert r.returncode == 0, r
ok("stale lock: a commit on main outside any live sprint is allowed")

print("\n%d checks passed" % n)
Loading
0