m1-leftovers: the CLI executes operations, the scanner's per-site hole closes, and the guidance surfaces tell the truth again - #14
Conversation
The converged frame for the post-Milestone-1 leftovers arc: 27 confirmed claims, 24 confirmed honesty conditions, 15 scope entries with provenance. Re-captures the 12 deliberately uncovered prior-frame targets verbatim (operator decision q1) so the next plan leg computes coverage natively. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EkC4zsVUmYrRCwSWSe4aLp
Three c19 escalation signals (security-sensitive work, hard-to-reverse publishes/posts, planned concurrency work) made the rigorous form mandatory. Findings adjudicated by the operator: tracker hygiene (c29), benign-argv fixture boundary for the scanner slice (c30), re-verify the second consumer's seam at M4 (c31). One non-blocking park: merge-order vs version-bump collisions, to mirror as a plan risk. Nine lens/surface scope entries (s16-s24) record what was examined, including the clean lenses; probes verified the 12 re-captures byte-identical and no consumer-name leak in any committed artifact. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EkC4zsVUmYrRCwSWSe4aLp
13 confirmed tasks covering all 54 targets of the challenged frame, with per-task operator instructions, acceptance criteria, and an honest dependency graph (waves: 9-wide opening, then the shared-file CLI chain, boundary verification, close-out). Four first-class risks, including the d3/d8 merge-order mirror of park v1. Frame amendment c32/h27 records that git and env-lifecycle CLI verbs ride their M3/M4 engines; this arc ships fs, process, and env describe. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EkC4zsVUmYrRCwSWSe4aLp
…urface Both files still described the pre-Milestone-1 scaffold. README's Status section claimed no operation/environment/policy/runner had been built and the axes matrix claimed neither runner existed; CLAUDE.md's "Not built yet" list still named process.exec/process.shell (t84) and the policy/operation CLI nouns (t85) as unbuilt. All false since 0.12.0-0.13.0 shipped HostRunner execution, fs.*/process.* handlers, and the policy/operation CLI nouns. Rewrite both to state what is actually built (the operation core, HostRunner, fs.*/process.* handlers, and the policy/operation CLI nouns) versus what genuinely is not (ContainerRunner, colleague delegation, and the env/fs/ process/git CLI noun groups), and flip the environment matrix's Built? column for the Host rows to Yes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EkC4zsVUmYrRCwSWSe4aLp
…xample in the release runbook The runbook said 'colleague is the second sanctioned base dependency', which inverts the relationship — colleague cannot be a dependency of itself. shell-cli is the second name colleague's zero-deps guard must allow-list. The example pin floor was also stale at >=0.8,<0.9 against a current 0.13.1 release. Found while drafting the colleague migration proposal, which points colleague at this runbook. Recorded as deviation d3 of the m1-leftovers plan. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EkC4zsVUmYrRCwSWSe4aLp
…dings Closes three of the four findings from issue #7's adversarial live test of scripts/colleague_inventory.py. 1. Per-site pinning (finding 2, the architectural one). ALLOWLIST answers "may this module spawn at all"; it could not see three brand-new spawns added to the already-allow-listed tools.py, and --check returned exit 0. SITE_BASELINE now pins each module's spawn-site *shape* at PINNED_SHA — the sorted multiset of canonical call names, with a :shell marker for a literal shell=True — and any delta fails --check. Line numbers are deliberately not part of the signature, so an unrelated edit above a spawn is not a false alarm. --emit-baseline regenerates the literal, and refuses to emit from a partial scan. 2. Module-prefix resolution (finding 1). The 12-entry literal set became a per-module table matched on the module prefix, so subprocess.getoutput, getstatusoutput, the os.exec*/spawn*/posix_spawn*/fork family, pty, the multiprocessing and process-pool constructors and runpy all resolve — and asyncio.subprocess.create_subprocess_exec resolves the same as asyncio.create_subprocess_exec, which was the sharpest evasion in the report. The prefix test is dotted-boundary aware: ostrich.system is not os.system. 3. Scope-aware bindings (finding 6). Name resolution now walks real Python scopes (module/function/class/comprehension, with class bodies invisible to nested functions) instead of a flat ast.walk, and an unbound base resolves to nothing instead of to itself. A parameter named os, os = self.os, for os in hosts, from mypkg.compat import subprocess, and a local named run beside a real from subprocess import run all stop being findings. A gate that cries wolf on a variable named os gets disabled, which is its own failure mode. Verified against the pinned colleague checkout (28fee29): the old and new scanners produce byte-identical findings — 21 sites, 15 modules, 13 debt, two shell=True — so the widened set and the scope rewrite change nothing about real colleague, and --check still exits 0. With one benign subprocess.run appended to that checkout's tools.py, the old scanner exits 0 and the new one exits 1 naming the delta. debt_remaining is still derived from ALLOWLIST intersected with what the scan observed; the site baseline is not allowed to become its source, and a test pins that. The honest ceiling is unchanged: this detects accidental and careless drift from a pinned baseline. Indirection (sp = subprocess, getattr, dynamic import), sibling re-export inside colleague/, exec() of a source string, and non-literal shell= spellings remain open and are listed in the module docstring rather than papered over. Every committed fixture is source text for the AST parser and is never executed; argv is only `true` or `echo`. test_fixture_sources_stay_benign keeps issue #7's illustrative payloads out of the repository. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EkC4zsVUmYrRCwSWSe4aLp
Six verbs — read, list, stat, write, edit, media — fronting the registered fs.* operations through `shell.operations.execute()`. The CLI is a front end over the identical engine, not a second implementation: this module opens no file, resolves no path, and decides nothing about the root. Two tests enforce that rather than trusting it — each verb's structured output is compared against a result the test produced by calling execute() itself, and the operation ids the CLI reports are retrieved through `shell operation show`, which reads the evidence record only execute() writes. New handler `shell/fs/stat.py` (`fs.stat`, intent=OBSERVE): existence, kind, size_bytes, mtime and permission bits. Two decisions are deliberate and pinned by tests: * a missing path is a SUCCEEDED observation reporting `exists: false`, not a failure — absence is one of the answers a stat exists to give. This diverges from fs.read on purpose, and the divergence is pinned so nobody "fixes" it; * confinement resolves, description does not. `_safe_path` resolves (following symlinks) and refuses anything landing outside the work root, with a byte-identical message to fs.read's — asserted as equality, so the two ports cannot drift. `lstat` on the literal path then describes what is actually there, so an in-root symlink is reported as a symlink rather than silently described as its target. Doing either half the other way round would be a real defect. Preview by default: write and edit carry apply=True only when --apply is given. A preview reports status `previewed`, never `succeeded`; text mode adds a stderr diagnostic naming the remedy. Exit code is 0 because the CLI did what was asked — "did the write happen" is the status field's question, not the exit code's, and the two are not conflated. Streams: in --json mode the full structured result reaches stdout on every path including failures, so an agent never has to re-run a mutation to learn its outcome. In text mode a failure writes nothing to stdout — its rendering IS the error message, and `shell fs read missing.txt > out.txt` must not land "no such file" in out.txt looking like file content. Also: `KIND` constant added to shell/fs/media.py so the CLI references each handler's own kind instead of re-spelling a literal; catalog entries for all eight fs paths; and the Status/Verbs prose in the explain root, `learn` and `overview` updated — they claimed the fs group was not exposed, which this commit makes false. README.md and CLAUDE.md carry the same stale claim and are owned by a sibling task this wave; they are deliberately untouched here. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EkC4zsVUmYrRCwSWSe4aLp
…sion classes t6 closed the architectural finding from issue #7 — SITE_BASELINE pins each module's spawn-site shape, so a new spawn in an already-allow-listed module now fails --check. The survey section still asserted that hole as live. Rewritten to state what closed, what remains open (indirection, structure, non-literal shell= spellings), and the two deliberate trades the scope-aware rewrite introduces. The drift-detector framing and the 'read a green --check as...' sentence are unchanged, because they stay true. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EkC4zsVUmYrRCwSWSe4aLp
The CLI hand-picks which fs verbs carry --apply; the engine decides which operations preview, from the intent each handler declared at registration. Nothing structural ties those two together, so a handler whose intent changed would leave a verb that previews forever with no flag to escape it, or a mutation that applies with no way to preview it first. Asserts the correspondence over every verb, in both directions -- four verbs report requires_apply=False and carry no flag, two report True and carry one -- so the guard is not vacuously green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EkC4zsVUmYrRCwSWSe4aLp
t7 made three surfaces false at once: README and CLAUDE.md both said no CLI verb executes an fs operation, and the handler inventory omitted fs.stat. The learn/overview/catalog text was corrected inside t7 itself; these two files were outside its scope by the wave's file-ownership rule. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EkC4zsVUmYrRCwSWSe4aLp
…y flag Part A -- `shell process exec` and `shell process shell`, the first CLI verbs that run a process. Both build an Operation and dispatch it through shell.operations.execute(): no second implementation, no reaching around the pipeline. Both declare intent=EXECUTE, so both preview by default and need --apply; a preview reports `previewed`, starts nothing, and predicts nothing -- no effects, no exit code. The profile asymmetry is enforced in the CLI, and enforced *early*. `exec` accepts project and control; `shell` accepts project only, because a shell re-interprets a string after the gate has read it, so which program runs is not settled by what was gated. The handlers already refuse this, but the pipeline's preview branch sits between the gate and the handler -- so an unapplied `process shell --profile control` would have come back `previewed`, telling the caller it was fine and merely unapplied. The CLI therefore refuses before building the operation, and it does so by calling the handler's own require_profile() with the kind's own accepted-profile tuple and hint constant, so the refusal is the library's sentence rather than a paraphrase of it. That required promoting _EXEC_PROFILES / _SHELL_PROFILES and the two inline hint strings to public module constants. Nothing here is path-confined and the surfaces say so rather than leaving it to be inferred: output.confinement keeps path_confined / uses_shell / gate_inspects_reinterpreted_string as separate keys on an applied result, every result including a preview carries the runner's evidence.isolation_note, and a text-mode preview names the missing confinement on stderr alongside --apply. A test writes a file outside --root and watches the operation succeed, so the claim cannot be quietly upgraded later. The command's own exit code is not passed through. 1 and 2 already mean user and environment error in this CLI and 3+ is reserved, so a command exiting 1 would be indistinguishable from a bad flag -- and a non-zero exit is not a failure of the operation, which was "run this and observe it". It lives in output.exit_code, with a stderr diagnostic in text mode so 0 is never read as approval. Part B -- --apply is now derived from the operation registry for both noun groups. The CLI used to hand-pick which verbs carried the flag while the engine independently decided which operations preview; t7 pinned the correspondence but could not prevent a divergence, and a second group was about to repeat the pattern. shell/cli/_operation.py:add_apply_flag() asks normalize() the same question execute() asks, and is now the only place in shell/ that spells --apply -- asserted by a source scan, so a future noun group cannot reintroduce the list. tests/test_cli_fs.py passes unmodified; its correspondence assertion is now trivially true and is kept because a regression would make it load-bearing again. The derivation itself is tested against handlers registered for the test alone. The policy-source flags moved into the same shared module: `policy` evaluates a snapshot and `process` executes under one, and the two must not be able to resolve one differently. `shell process` is consequently the first CLI verb the run_command gate actually governs. learn, overview and the explain root said "no CLI verb runs a shell command yet". That is now false, so they were corrected -- and still name env and git as unexposed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EkC4zsVUmYrRCwSWSe4aLp
…troy Adds shell/cli/_commands/env.py with one real verb, `describe`, plus the required `overview`. It renders the two-axis Environment (workspace vs runner, never flattened into one "mode") that `shell fs`/`shell process` would build from the same --root/--read-only-path/--env flags, as Environment.to_dict() verbatim, in text and --json. The host runner's no-isolation posture (isolation="none", isolation_note) surfaces verbatim in both renderings. `env create`/`env destroy` are deliberately absent: no OperationIntent .LIFECYCLE handler is registered and there is no ContainerRunner, so either verb would front nothing. The catalog entry, the global overview, and the noun's own overview all say so explicitly and name Milestone 4 as where they land. `shell env create`/`destroy` fail as ordinary unrecognized subcommands through the existing CliError path (exit 1, error:/hint:, no traceback) -- pinned by test, not left to accident. wired into shell/cli/__init__.py; catalog entries added to shell/explain/catalog.py; shell/cli/_commands/overview.py's global verb list and status notes updated to mention env (describe only). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EkC4zsVUmYrRCwSWSe4aLp
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EkC4zsVUmYrRCwSWSe4aLp
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EkC4zsVUmYrRCwSWSe4aLp
|
/agentic_review |
PR Summary by QodoWire CLI fs/process/env to operations.execute; add fs.stat; close scanner per-site drift hole
AI Description
Diagram
High-Level Assessment
Files changed (35)
|
… call per pytest.raises block (S5778) _emit always returned 0 on every non-raising path, so the -> int signature misled a reader into thinking it computed a variable exit code. Extract the terminal raise ladder (DENIED + catch-all) into _raise_for_status(result) -> NoReturn, and change _emit to -> None; the docstring's denial explanation travels with the branch that moved. The call site in _run() now calls _emit() then returns EXIT_SUCCESS explicitly. Mirrors the parallel refactor of process.py's _emit so the two modules stay structurally aligned. Also hoist the argv list above two `with pytest.raises(SystemExit)` blocks in test_cli_fs.py that called str(work) inside the block, leaving exactly one invocation (main(argv)) that can raise. Swept the whole file for the same pattern; these were the only two instances. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EkC4zsVUmYrRCwSWSe4aLp
S3516/S3776 (process.py:332): _emit was typed -> int but every path either returned 0 or raised CliError, so the return value carried no information and the branching pushed cognitive complexity to 17. Extract the DENIED / TIMED_OUT / catch-all raise ladder into a new `_raise_for_status(result) -> NoReturn` helper; _emit now returns None and delegates to it. The docstring sentence about a policy denial landing in a redirected build.log travels with the ladder into _raise_for_status, since that is the code it now describes. The call site in _run replaces `return _emit(...)` with `_emit(...)` followed by `return EXIT_SUCCESS`. Verified with the `cognitive_complexity` package: _emit drops from 17 to 12 (_raise_for_status is 5), both under the 15 threshold. S5778 (tests/test_cli_process.py:1158): hoisted the argv list for test_an_invalid_profile_is_a_structured_error above the `pytest.raises` block so only `main(argv)` remains inside it. shell/cli/_commands/fs.py and tests/test_cli_fs.py are untouched — a sibling agent owns the identical S3516 fix there. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EkC4zsVUmYrRCwSWSe4aLp
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EkC4zsVUmYrRCwSWSe4aLp
|




Closes the gap between "Milestone 1 done" and the issue #1 definition of done.
Full plan-versus-actual accounting is committed at
docs/deliveries/2026-07-28-m1-leftovers.md— read that as the review map; this description is its headline.
The arc ran the devague flow end to end:
/scope→/think→/challenge→/spec-to-plan→/assign-to-workforce→/summarize-delivery. The spec,plan, frame state and delivery summary are all committed, so every claim below
traces to a confirmed claim or a recorded deviation.
What ships
The CLI executes operations for the first time. Three noun groups, all
fronting
operations.execute()rather than reimplementing anything:shell fs read|list|stat|write|edit|media, plus a new confinedfs.stathandler. A missing path is
SUCCEEDEDwithexists: false, deliberatelydiverging from
fs.readand pinned so nobody reconciles them by accident.shell process exec|shell.process shellrefuses thecontrolprofile inthe library's own words — a raw string is re-interpreted by a shell after
the gate read it, so which program runs is not settled by what was gated. The
refusal precedes the preview, so previewing cannot mask it.
shell env describe.env create/env destroyare deliberately absent:OperationIntent.LIFECYCLEhas no registered handler and noContainerRunnerexists, so they would be a facade in front of nothing. The catalog says so and
names Milestone 4.
--applyis now derived from the registry. Previously the CLI hand-pickedwhich verbs got the flag while the engine independently decided which operations
preview — nothing tied them together, so flipping a handler's intent would have
left a verb previewing forever with no flag to escape it.
shell/cli/_operation.pyis now the only module in
shell/that spells--apply, and it asksnormalize(...).requires_apply— the same questionexecute()asks.tests/test_cli_fs.pypasses byte-unmodified across that refactor, which isthe proof it changed no behaviour.
The issue #7 architectural finding is closed.
SITE_BASELINEpins eachmodule's spawn-site shape, so a new spawn added to an already-allow-listed
module now fails
--check. Verified adversarially: appending one benignsubprocess.run(["true"])totools.pyin a pinned clone yieldstools.py: baseline 2 site(s), observed 3 (+subprocess.run×1)and exit 1 — theexact case that returned exit 0 with no signal in the original live test.
Line numbers are excluded from the shape deliberately, or any refactor above a
spawn would false-alarm; the baseline is generated by a new
--emit-baseline,never hand-typed, so
PINNED_SHAand the baseline must move in one reviewed diff.The rewrite is behaviour-neutral on real colleague — old and new findings are
byte-identical over the pinned checkout (21 sites / 15 modules / 13 debt) — and
the six false positives from §6 are gone, including the one that flagged a
parameter named
os.What this deliberately does not claim
Indirection (
sp = subprocess,getattr, dynamic import), sibling re-exportinside
colleague/,exec()of a source string, and non-literalshell=spellings all remain open, documented in the module docstring and tracked in
Pin the scanner's remaining evasion classes as expected-to-evade fixtures (successor to #7) #13. Read a green
--checkas "the inventory has not drifted in the waysthis scanner can see."
env describesurfacesisolation: noneverbatim rather than softening it.fan-out (deviation
d1) because they are full devague arcs whoseconverge/confirmmoves are user-only gates.d2— filingon colleague, devex and this repo is irreversible and outward-facing, so
posting waits for operator review.
Guidance surfaces
Every wave of this arc falsified at least one guidance surface, so README,
CLAUDE.mdand the explain catalog were corrected as each landed. Also fixed:docs/release-runbook.mdclaimed "colleague is the second sanctioned basedependency" — inverted, since colleague cannot be a dependency of itself
(deviation
d3, found while drafting the migration proposal that cites thatvery runbook).
Worth recording: an independent re-verification during this arc confirmed that
every line number CLAUDE.md's colleague survey cites still holds at colleague
v1.52.1, a full minor version after the 1.51.0 snapshot. Nothing drifted.
Evidence
skips are the documented
KNOWN GAP, unchanged.black,isort,flake8,bandit,markdownlint-cli2: all clean.teken cli doctor . --strict: exit 0, 26/26.colleague_inventory.py --checkagainst the pinned checkout: exit 0.the repo tree, the arc diff, the drafts and
.devaguestate;dependencies = []withtest_zero_depsgreen; not74bfixture; the fivechosen-limit test files byte-unchanged across the arc.
Follow-ups filed during the run
#10 (the pinned colleague SHA is not on colleague's
main— fragileprovenance, and the reason a fresh developer checkout has a red baseline),
#11 (four evidence-record gaps, including
created_resourcesbeing adefined-but-never-populated slot), #12 (preview misses knowable confinement
refusals; the registry lacks
accepted_profiles; five copies of_safe_path),#13 (pin the remaining scanner evasion classes as expected-to-evade fixtures).
Note for the reviewer
Merging publishes 0.14.0 to PyPI. Nothing consumes this package yet, so the
blast radius is the index entry itself.