Skip to content
Open
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
53 changes: 53 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,59 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
`agent-ultra bob hook-install` to regenerate the hooks.**

### Added
- **bob surgical lane** (`agent-ultra bob surgical "<task>" --files ...`,
`run_surgical` from Python) — the sanctioned lightweight tier for INERT
doc/config edits: a real two-lens review fan-out (ultracode-backed,
fingerprint-pinned like steps 6/7) + an operator quiz (`passed` requires
the captured answer) + its own fail-closed gate. Entry criteria are
objective and validated at start: inert types only (prose + known-inert
dotfiles; structured config needs the operator's explicit
`AGENT_ULTRA_SURGICAL_ALLOW_CONFIG=1`), a deny list of
executable-by-context files (CI workflows, hook managers, task runners,
package manifests, tool configs — by name, path, and shape), declared
files as the exact scope, and a 100-line/256-KiB text-only diff budget.
**Surgical narrows Bob, it never replaces it (C1):** `run.json`'s mode is
a request the gate verifies — the surgical gate is used only when the
actual staged set independently qualifies; code staged under a
"surgical" run routes to the FULL gate and its full receipt-chain
demands, and a full-mode run never auto-downgrades.

- **bob run lifecycle + fan-out integrity hardening.** A run can no longer
be dropped quietly, and fan-out proof got stricter:
- *No silent orphaning.* `BobRun.start(force=True)` (CLI:
`bob run --operator-force`, renamed from `--force`) now leaves a
durable abandonment record instead of silently superseding the
unproven run. New `bob abandon --operator-abandon [--reason]` releases
an unproven run explicitly — refused without the flag — logging to
`.agent-ultra/bob/abandoned.jsonl` plus `abandoned.json` in the run
dir, and it announces loudly on stderr.
- *Operator escapes are agent-unreachable.* The command broker
classifies `--operator-abandon` / `--operator-force` /
`--operator-unbounded` as DANGEROUS, so a broker-mediated agent shell
cannot auto-run them.
- *Cloned fan-out rejected.* A step-6/7 ultracode run whose completed
agents all produced IDENTICAL output (per-agent `output_sha256` in the
run receipt) is one review wearing N hats — the gate now blocks it.
Demo mode (`--allow-mock`) is exempt: the offline mock answers every
agent identically by design, and mock evidence is already only
acceptable there.
- **Breaking:** `bob run --force` is now `bob run --operator-force`.

- **bob run scope — a run is bound to what it declared.** Every bob run now
declares its target scope at start (`BobRun.start(..., scope=[...])`;
`run_bob` declares the generated test+impl files automatically) and the
gate rejects staged files outside it, so a run opened for module A cannot
smuggle in edits to module B. Scope is mandatory and fail-closed: a
no-scope start refuses, and a scope stripped from `run.json` afterwards
authorizes nothing. Expansion goes only through the validated, logged
mechanism (`agent-ultra bob scope-add`, recorded in `scope-log.jsonl`).
`.git/hooks` and `.agent-ultra` paths are rejected as scope entries
always — a run cannot declare authority over its own enforcement. The
only unbounded form is `operator_unbounded=True`, a Python-API parameter
deliberately absent from the CLI (an agent driving the CLI cannot reach
it), loudly announced and durably logged. **Breaking:** `BobRun.start`
now requires `scope=`; `run_bob` callers are unaffected.

- **bob** (`agent_ultra.bob`) — the 10-step enforced build pipeline
(SPEC → RED → GREEN → REFACTOR → CODE-QUALITY → SECURITY-FANOUT → WORKFLOW
→ ULTRA → QUIZ → COMMIT) composing ultracode fan-out, the adversarial
Expand Down
81 changes: 81 additions & 0 deletions docs/bob-the-builder.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,45 @@ artifact, unverifiable chain) is a failure, not a warning. On block, the
errors name exactly which step to redo. Staged files that appear in no
receipt also block (`*.md`, `.agent-ultra/*`, and `.gitignore` are exempt).

### Run scope: a run is FOR something

Every run declares its target scope at start — the files/dirs/globs the run
is for. `run_bob` declares the generated test + implementation files
automatically; from Python pass `scope=[...]` to `BobRun.start`. The gate
then rejects any staged file **outside** the declared scope, so a run opened
for module A cannot smuggle in edits to module B:

```
out-of-scope: staged file other/thing.py is outside the run's declared scope
```

Rules:

- **Scope is mandatory.** `BobRun.start` refuses a run with no scope, and a
scope stripped from `run.json` after the fact unbinds nothing — a no-scope
run authorizes NO staged files (fail closed).
- **Expansion is explicit, validated, and logged.** If the work legitimately
grows, `agent-ultra bob scope-add <paths...>` appends to the scope and
records the expansion in `scope-log.jsonl`. Nothing else widens a run.
- **Infrastructure is never in scope.** `.git/hooks` and `.agent-ultra`
paths are rejected at start and at scope-add, always — a run cannot
declare authority over the machinery that enforces it.
- **A run cannot be dropped by starting another.** `start` refuses while
an unproven run is ACTIVE, and a marker naming a deleted run directory is
a tampering sentinel (the gate fails closed), not "no run". The only
sanctioned drop is `agent-ultra bob abandon --operator-abandon
[--reason]` — durably recorded in `.agent-ultra/bob/abandoned.jsonl`.
The command broker classifies all `--operator-*` escape flags as
DANGEROUS, so a broker-mediated agent shell cannot auto-run them.
- **A cloned fan-out is one review.** If every completed agent of a
step-6/7 ultracode run produced identical output, the gate blocks — the
per-agent `output_sha256` in the run receipt exposes the clone.
- **The operator escape is not a CLI flag.** `BobRun.start(...,
operator_unbounded=True)` opens a run with no scope binding; it is a
Python-API parameter on purpose (no CLI verb exposes it, so an agent
driving the CLI cannot reach it), it is announced loudly on stderr, and
it is durably recorded in `scope-log.jsonl`.

### Commit lifecycle: mark at pre-commit, seal at post-commit

Sealing inside pre-commit is a deadlock: the pass would release ACTIVE
Expand Down Expand Up @@ -127,6 +166,48 @@ which reads git's staged list. Install both: `assert_bob_done` guards
completion claims, the pre-commit hook guards the commit itself and catches
any staged file that no receipt covers.

## The surgical lane (inert edits only)

Typo-class doc/config edits do not need RED/GREEN and a five-agent sweep.
The surgical lane is the sanctioned lightweight tier — and it NARROWS Bob,
it never replaces it:

```bash
# edit the files first, then:
agent-ultra bob surgical "fix readme wording" --files README.md
agent-ultra bob surgical "fix readme wording" --files README.md --mock # offline demo
```

The lane runs a real two-lens review fan-out (accuracy + safety, ultracode-
backed and fingerprint-pinned exactly like steps 6/7), an operator quiz
(`passed` requires your captured answer; an unanswered quiz is honestly
`skipped`), and its own gate.

Entry criteria are objective and validated at start — anything else raises
and the answer is the full pipeline:

- **Inert types only.** Prose (`.md`/`.rst`/`.txt`/...) and known-inert
dotfiles auto-qualify. Structured config (yaml/json/toml/ini/cfg) is
dual-use: it needs the operator's explicit
`AGENT_ULTRA_SURGICAL_ALLOW_CONFIG=1` AND must clear the denylist.
- **The denylist is executable-by-context files** — CI workflows, git-hook
managers, task runners, package manifests, tool configs (matched by name,
path, and shape) — regardless of extension. A doc ABOUT Dockerfiles is a
doc; a `Dockerfile` is not.
- **Declared files are the exact scope.** The gate rejects any staged file
that was not declared, is not inert, or is denied; `scope-add` on a
surgical run refuses risky paths.
- **A small diff budget** (100 lines / 256 KiB, binary edits rejected),
fail-closed on any git error.

**Mode downgrade cannot launder code (C1).** `run.json`'s mode field is a
REQUEST the gate verifies, never a trusted assertion: `bob gate` routes to
the surgical gate ONLY when the actual staged set independently qualifies
(all inert, none denied, all declared). Flip a run's mode to "surgical"
with code staged and you get the FULL gate — which demands the full receipt
chain that run never produced. A full-mode run never auto-downgrades
either, even when only inert files are staged: surgical is opt-in at start.

## Trust boundary (read this)

bob resists the failure modes an agent actually falls into: **skipping** the
Expand Down
94 changes: 91 additions & 3 deletions src/agent_ultra/adapters/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ def _bob_run(args) -> int:
pool = _build_pool(args)
outcome = run_bob(args.task, args.workspace, mock=args.mock, pool=pool,
interactive=False if args.no_quiz else None,
force=args.force)
force=args.operator_force)
return 0 if outcome.passed else 1


Expand Down Expand Up @@ -351,6 +351,53 @@ def _bob_seal(args) -> int:
return 1


def _bob_scope_add(args) -> int:
from ..bob import BobRun
run = BobRun.active(args.workspace)
if run is None or run.marker_orphaned():
print("no active bob run — nothing to expand", file=sys.stderr)
return 2
added, errors = run.add_scope(args.paths)
for e in errors:
print(f"SCOPE REJECTED — {e}", file=sys.stderr)
if added:
print(f"scope expanded (logged): +{added}")
print(f"scope now: {run.scope()}")
return 1 if errors else 0


def _bob_surgical(args) -> int:
from ..bob import run_surgical
pool = None
if not args.mock:
pool = _build_pool(args)
outcome = run_surgical(args.task, args.workspace, args.files,
mock=args.mock, pool=pool,
interactive=not args.no_quiz)
return 0 if outcome.passed else 1


def _bob_abandon(args) -> int:
from ..bob import BobRun
if not args.operator_abandon:
print("ABANDON REFUSED — requires the explicit --operator-abandon "
"flag (operator only; an agent cannot abandon an unproven "
"run)", file=sys.stderr)
return 1
try:
name = BobRun.abandon(args.workspace, operator=True,
reason=args.reason)
except ValueError as e:
print(f"ABANDON REFUSED — {e}", file=sys.stderr)
return 1
if name is None:
print("no active bob run — nothing to abandon")
return 0
print(f"run {name} abandoned (logged to .agent-ultra/bob/"
"abandoned.jsonl)")
return 0


def _bob_hooks(args) -> int:
from ..bob.pipeline import install_hooks
try:
Expand Down Expand Up @@ -548,8 +595,9 @@ def _uc_common(sp):
br.add_argument("--workspace", default=".")
br.add_argument("--no-quiz", action="store_true",
help="record the quiz step as skipped (non-interactive)")
br.add_argument("--force", action="store_true",
help="abandon an existing unproven active run and start fresh")
br.add_argument("--operator-force", action="store_true",
help="OPERATOR ONLY: abandon an existing unproven "
"active run (durably logged) and start fresh")
br.add_argument("--mock", action="store_true",
help="offline demo on the bundled sample task (no key)")
br.add_argument("--mode", default="single", choices=["single", "mixed"])
Expand Down Expand Up @@ -596,6 +644,46 @@ def _uc_common(sp):
bs.add_argument("--workspace", default=".")
bs.set_defaults(func=_bob_status)

ba = bsub.add_parser("scope-add",
help="expand the active run's declared scope — "
"the approved, validated, logged mechanism "
"(infrastructure paths always rejected)")
ba.add_argument("paths", nargs="+",
help="files/dirs/globs to add to the run's scope")
ba.add_argument("--workspace", default=".")
ba.set_defaults(func=_bob_scope_add)

bab = bsub.add_parser("abandon",
help="OPERATOR ONLY: release an unproven "
"ACTIVE run with a durable abandonment "
"record — the only sanctioned way to "
"drop a run without proving it")
bab.add_argument("--operator-abandon", action="store_true",
help="required confirmation flag (operator only)")
bab.add_argument("--reason", default="",
help="why the run is abandoned")
bab.add_argument("--workspace", default=".")
bab.set_defaults(func=_bob_abandon)

bsg = bsub.add_parser(
"surgical",
help="lightweight lane for INERT doc/config edits: review "
"fan-out + operator quiz + gate. Code never qualifies — "
"it routes to the full pipeline")
bsg.add_argument("task")
bsg.add_argument("--files", nargs="+", required=True,
help="the inert files this run may edit (its "
"exact scope)")
bsg.add_argument("--workspace", default=".")
bsg.add_argument("--mock", action="store_true",
help="offline demo (no key)")
bsg.add_argument("--no-quiz", action="store_true",
help="record the quiz as skipped (non-interactive)")
bsg.add_argument("--mode", default="single",
choices=["single", "mixed"])
bsg.add_argument("--config", default="")
bsg.set_defaults(func=_bob_surgical)

gp = sub.add_parser("panel-gate",
help="block REPORT unless a valid panel execution "
"receipt proves real panel-agent calls happened")
Expand Down
11 changes: 7 additions & 4 deletions src/agent_ultra/bob/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,23 @@
"""

from .receipts import (
STEPS, GATED_STEPS, BobReceiptError,
STEPS, GATED_STEPS, SURGICAL_GATED_STEPS, BobReceiptError,
build_step_receipt, load_receipt, validate_chain,
)
from .pipeline import (
BobRun, BobProofError, GateResult,
gate_check, run_pytest_step, assert_bob_done, load_or_create_key,
install_hooks,
install_hooks, scope_contains, scope_entry_is_infra,
surgical_gate_check,
)
from .runner import run_bob, BobOutcome
from .runner import run_bob, run_surgical, BobOutcome

__all__ = [
"STEPS", "GATED_STEPS", "BobReceiptError",
"build_step_receipt", "load_receipt", "validate_chain",
"BobRun", "BobProofError", "GateResult",
"gate_check", "run_pytest_step", "assert_bob_done", "load_or_create_key",
"install_hooks", "run_bob", "BobOutcome",
"install_hooks", "scope_contains", "scope_entry_is_infra",
"surgical_gate_check", "SURGICAL_GATED_STEPS", "run_surgical",
"run_bob", "BobOutcome",
]
Loading
Loading