Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
dcee1f7
spec: m1-leftovers (devague /think)
OriNachum Jul 28, 2026
9a6fcc9
challenge: rigorous pass over the m1-leftovers frame
OriNachum Jul 28, 2026
9cfd080
plan: m1-leftovers (devague /spec-to-plan)
OriNachum Jul 28, 2026
bd8d62d
docs: refresh README/CLAUDE status sections to match shipped 0.13.1 s…
OriNachum Jul 28, 2026
fe2a20e
merge t1: docs-honesty refresh — README + CLAUDE.md describe the 0.13…
OriNachum Jul 28, 2026
bc21bdb
docs: fix the inverted sanctioned-dependency sentence and stale pin e…
OriNachum Jul 28, 2026
44cfacc
t6: per-site pinning, module-prefix spawn resolution, scope-aware bin…
OriNachum Jul 28, 2026
b005830
merge t6: scanner hardening — per-site pinning, wider call set, scope…
OriNachum Jul 28, 2026
2b07270
t7: the `shell fs` CLI noun group, plus a confined fs.stat handler
OriNachum Jul 28, 2026
51a9bd6
docs: CLAUDE.md records the closed per-site hole and the residual eva…
OriNachum Jul 28, 2026
f72e918
t7: pin the --apply flag to the engine's own preview decision
OriNachum Jul 28, 2026
511e272
merge t7: CLI fs noun group + confined fs.stat handler
OriNachum Jul 28, 2026
b92cb15
docs: record the shipped fs CLI group and fs.stat handler
OriNachum Jul 28, 2026
5eb1fe3
t8: the `shell process` CLI noun group, and a registry-derived --appl…
OriNachum Jul 28, 2026
10244ce
merge t8: CLI process noun group + registry-derived --apply flag
OriNachum Jul 28, 2026
c57d967
docs: record the shipped process CLI group and registry-derived --apply
OriNachum Jul 28, 2026
a8312d8
t9: the `shell env describe` CLI verb, and the deferral of create/des…
OriNachum Jul 28, 2026
72bfca5
merge t9: shell env describe (create/destroy deferred to their M4 eng…
OriNachum Jul 28, 2026
e817747
docs: record shell env describe and the deliberate create/destroy def…
OriNachum Jul 28, 2026
5337159
chore: bump to 0.14.0 for the m1-leftovers arc
OriNachum Jul 28, 2026
746182b
delivery: m1-leftovers arc summary (partial run, plan-versus-actual)
OriNachum Jul 28, 2026
a72325a
chore: sync uv.lock to 0.14.0 and commit the deviation ledger
OriNachum Jul 28, 2026
b3fd973
fix(sonar): fs._emit no longer returns a meaningless int (S3516), one…
OriNachum Jul 28, 2026
fe005b8
merge: Sonar S3516/S5778 fixes in the fs CLI group
OriNachum Jul 28, 2026
9726bf6
fix(sonar): split process._emit's raise ladder into _raise_for_status
OriNachum Jul 28, 2026
dea7e9a
merge: Sonar S3516/S3776/S5778 fixes in the process CLI group
OriNachum Jul 28, 2026
91da217
docs: record the Sonar-driven _emit refactor in the 0.14.0 entry
OriNachum Jul 28, 2026
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 .devague/current
Original file line number Diff line number Diff line change
@@ -1 +1 @@
guarded-local-operations-plane
m1-leftovers
2 changes: 1 addition & 1 deletion .devague/current_plan
Original file line number Diff line number Diff line change
@@ -1 +1 @@
guarded-local-operations-plane
m1-leftovers
45 changes: 45 additions & 0 deletions .devague/deliveries/m1-leftovers.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"plan_slug": "m1-leftovers",
"schema_version": 1,
"created": "2026-07-28T07:16:34Z",
"updated": "2026-07-28T07:27:51Z",
"deviations": [
{
"id": "d1",
"what": "t10 and t11 are lifted out of the fan-out and will run as their own scope-think-challenge-plan sessions with the operator after this arc lands",
"task_ref": "t10",
"reason": "Both are full devague arcs whose converge/confirm moves are user-only gates, and running devague moves inside parallel task worktrees would write conflicting .devague state - which the assign-to-workforce contract forbids. Operator decision at gate 2, 2026-07-28.",
"affects": [
"t11",
"t13"
],
"origin": "user",
"status": "approved",
"classification": "acceptable"
},
{
"id": "d2",
"what": "t2, t3, t4 and t13 produce reviewed DRAFTS during the fan-out; the actual public posting happens after operator review, so their acceptance criteria complete at post time rather than at merge time",
"task_ref": "t2",
"reason": "Filing issues and comments on colleague, devex and this repo are irreversible outward-facing actions; the operator chose draft-then-review at gate 2, 2026-07-28. These tasks therefore produce no repo mutation and need no worktree.",
"affects": [
"t3",
"t4",
"t13"
],
"origin": "user",
"status": "approved",
"classification": "acceptable"
},
{
"id": "d3",
"what": "docs/release-runbook.md is corrected in-arc: the inverted sentence 'colleague is the second sanctioned base dependency' becomes 'shell-cli is...', and the stale example floor shell-cli>=0.8,<0.9 becomes >=0.13,<0.14",
"task_ref": "t1",
"reason": "Found by the t2 drafting agent while sourcing the colleague migration proposal, which points colleague at this very runbook. The plan scoped the docs-honesty slice (c3/t1) to README and CLAUDE.md only, so this file was out of scope; leaving a factual inversion in the document the proposal cites would undercut the proposal. Two-line factual fix, no behaviour change.",
"affects": [],
"origin": "llm",
"status": "proposed",
"classification": "acceptable"
}
]
}
763 changes: 763 additions & 0 deletions .devague/frames/m1-leftovers.json

Large diffs are not rendered by default.

567 changes: 567 additions & 0 deletions .devague/plans/m1-leftovers.json

Large diffs are not rendered by default.

18 changes: 12 additions & 6 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,14 @@ jobs:
#
# WHAT THIS JOB DOES NOT DO: it is not an enforcement boundary and must not
# be described as one. An adversarial live test (issue #7) landed 30
# executed evasions at exit 0 — most importantly, ALLOWLIST is keyed per
# MODULE, so a new spawn added to any of the 15 already-allow-listed
# modules passes green. Read a green run as "the inventory has not drifted
# in the ways this scanner can see", never as "no new unmediated spawn path
# was added".
# executed evasions at exit 0. The architectural one is closed — SITE_BASELINE
# now pins each module's spawn-site shape, so a new spawn added to an
# already-allow-listed module (tools.py included) fails this job. What
# remains open is indirection (`sp = subprocess`, getattr, dynamic import),
# sibling re-export inside colleague/, exec() of a source string, and
# non-literal `shell=` spellings. Read a green run as "the inventory has not
# drifted in the ways this scanner can see", never as "no new unmediated
# spawn path was added".
#
# agentculture/colleague is public, so this clones it at the pinned SHA
# rather than depending on a local checkout. If the clone fails the job
Expand Down Expand Up @@ -174,7 +177,10 @@ jobs:
echo '```'
} >> "$GITHUB_STEP_SUMMARY"

- name: Gate — no unclassified spawn paths
# Two independent checks behind one exit code: a spawn in a module the
# ALLOWLIST does not classify, and a per-module delta from SITE_BASELINE
# (the pinned spawn-site shapes). Either one fails the job.
- name: Gate — unclassified spawn paths and per-site baseline drift
run: python3 scripts/colleague_inventory.py .colleague-src --check

- name: Drift gate — handler source hashes
Expand Down
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,28 @@ All notable changes to this project will be documented in this file.
Format follows [Keep a Changelog](https://keepachangelog.com/). This project
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.14.0] - 2026-07-28

### Added

- `shell fs read|list|stat|write|edit|media` — the first CLI group whose verbs EXECUTE operations rather than introspect them. It holds no filesystem logic: every verb goes through `operations.execute()`, so the CLI and the library share one engine. Proven, not asserted — each verb's `--json` is compared field-by-field against a result the test obtained from `execute()` itself, and the CLI's operation id is round-tripped through `shell operation show`, which reads the evidence record only `execute()` writes (t7).
- `fs.stat` — a confined structured-metadata handler. A missing path is `SUCCEEDED` with `exists: false`, deliberately diverging from `fs.read`'s failure and pinned so nobody reconciles them by accident. Description uses `lstat`, so an in-root symlink reports as `symlink` rather than as its target (t7).
- `shell process exec|shell` — both preview by default. `process shell` refuses the `control` profile in the 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 it cannot be masked by previewing (t8).
- `shell env describe` — renders the selected environment's two axes as separate facts, never one overloaded mode, and surfaces the host runner's no-isolation posture verbatim. `env create`/`env destroy` are deliberately absent: `OperationIntent.LIFECYCLE` has no registered handler and no `ContainerRunner` exists, so they would be a facade. The catalog states the deferral and names Milestone 4 (t9).
- `SITE_BASELINE` in `scripts/colleague_inventory.py` — pins each module's spawn-site SHAPE (sorted multiset of canonical call names, `:shell` suffix for a literal `shell=True`). Line numbers are excluded deliberately, or any refactor above a spawn would be a false alarm. Generated by the new `--emit-baseline`, never hand-typed, so `PINNED_SHA` and the baseline move in one reviewed diff (t6).

### Changed

- The `--apply` flag is now DERIVED from each kind's registered intent at parser-build time via `shell/cli/_operation.py`, the only module in `shell/` that spells `--apply`. Previously the CLI hand-picked which verbs got the flag while the engine independently decided which operations preview — nothing structural tied them together, so flipping a handler's intent would have left a verb previewing forever with no flag to escape it. `tests/test_cli_fs.py` passes byte-unmodified, which is the proof the refactor changed no behaviour (t8).
- `scripts/colleague_inventory.py` closes the architectural finding of issue #7: a new spawn added to an ALREADY-ALLOW-LISTED module now fails `--check`. Previously `ALLOWLIST` was keyed per module, so three new spawns in `tools.py` — two of them `shell=True` — returned exit 0 with no signal. The widened call set (module-prefix resolution, so `asyncio.subprocess.create_subprocess_exec` no longer slips past) and the scope-aware binding rewrite are behaviour-neutral on real colleague: old and new findings are byte-identical over the pinned checkout, same 21 sites / 15 modules / 13 debt (t6).
- The unbound-base fall-through is dropped rather than downgraded to a warning, clearing the six false positives from issue #7 §6 — a gate that flags a parameter named `os` gets disabled, which is its own failure mode. Scope-aware bindings also stop one legitimate `from subprocess import run` from making every later local named `run` a finding (t6).
- README.md and CLAUDE.md describe the shipped surface again. Both had claimed no operation core, no runner, and an introspection-only CLI — all false since 0.13.0. Each wave of this arc falsified at least one guidance surface, so they were rewritten as each landed (t1, plus merge-time updates).

### Fixed

- `docs/release-runbook.md` asserted "colleague is the second sanctioned base dependency, alongside agentfront" — inverted, since 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 0.13.1 release. Found while drafting the colleague migration proposal, which points colleague at this very runbook.
- `_emit` in both CLI operation groups was annotated `-> int` while every `return` in it returned `0` and every other path raised — a signature that told a reader it computed a variable exit code when it did not. The terminal raise ladder is now a `_raise_for_status(...) -> NoReturn` helper in each module, `_emit` is honestly `-> None`, and each call site returns the existing `EXIT_SUCCESS`. `process`'s cognitive complexity drops 17 -> 12 as a consequence, measured against SonarSource's own algorithm before and after. No behaviour change: the suite held at 860 with no test modified beyond three `pytest.raises` blocks that had a second throwing call (`str(work)`) inside them (SonarCloud S3516, S3776, S5778 on PR #14).

## [0.13.1] - 2026-07-19

### Fixed
Expand Down
73 changes: 57 additions & 16 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ Identity is settled and must not drift: repo token `shell-cli`, console command
**The operation core is built; the CLI has not caught up to it yet.** Keep those
two facts separate — conflating them is how this section goes stale.

Built and green in the **library** (Milestone 1, v0.11.0+):
Built and green in the **library** (Milestone 1, v0.11.0-0.13.1):

- `shell/operations.py` — `Operation`, the handler registry, and the lifecycle
pipeline: `normalize -> rewrite -> policy gate -> preview branch -> handler ->
Expand All @@ -59,18 +59,45 @@ Built and green in the **library** (Milestone 1, v0.11.0+):
- `shell/evidence.py` — the evidence contract, wired into `execute`.
- `shell/runners/host.py` — `HostRunner` with real execution: process groups,
timeout/cancel escalation, per-platform orphan-prevention honesty.
- `shell/fs/` — `fs.read`, `fs.list`, `fs.write`, `fs.edit`, `fs.media`.

**Not built yet:** `process.exec` / `process.shell` handlers (t84); the CLI noun
groups `env` / `fs` / `process` / `git` / `policy` / `operation` (t85);
- `shell/fs/` — `fs.read`, `fs.list`, `fs.stat`, `fs.write`, `fs.edit`, `fs.media`.
- `shell/process/` — `process.exec` (argv, `project` or `control`) and
`process.shell` (a model-authored string, `project` only, since a shell
re-interprets it after the gate reads it) (t84).

Built and green in the **CLI**: the introspection verbs (`whoami`, `learn`,
`explain`, `overview`, `doctor`, `cli`) plus two operation-aware nouns —
`shell policy check` / `shell policy explain`, which call the exact gate
function `execute()` uses rather than a re-derivation, and `shell operation
show`, which retrieves a persisted evidence record (t85).

`shell fs read|list|stat|write|edit|media` and `shell process exec|shell` are
the CLI groups that *execute* operations rather than introspecting them.
Neither holds operation logic of its own — every verb goes through
`operations.execute()`, so the CLI and the library share one engine.
`process shell` accepts `project` only, refusing `control` in the library's own
words. The `--apply` flag is **derived from each kind's registered intent** at
parser-build time (`shell/cli/_operation.py` is the only module that spells
`--apply`), so a handler whose intent changes cannot leave a verb previewing
forever with no flag to escape it.

`shell env describe` renders the selected two-axis environment (workspace and
runner as separate facts, never one overloaded mode) and surfaces the host
runner's no-isolation posture. `env create` / `env destroy` are deliberately
**absent**, not overlooked: `OperationIntent.LIFECYCLE` has no registered
handler and there is no `ContainerRunner`, so they would be a facade in front
of nothing. They land with their engine in Milestone 4, and the catalog says
so.

**Not built yet:** the `git` noun group and `env create` / `env destroy`;
`ContainerRunner` (Milestone 4); colleague delegation (Milestone 2).

So the **CLI still exposes only the introspection verbs** — `whoami`, `learn`,
`explain`, `overview`, `doctor`, `cli`. That is why `learn`, `overview`, and the
`explain` root still carry a Status section saying so, and it remains accurate
*about the CLI*. What is no longer true anywhere is "the primitives are not
extracted yet." Keep those surfaces honest as t84/t85 land; they are the first
thing an agent consumer reads.
So the CLI both runs operations and introspects them. What is no longer true
anywhere: "the primitives are not extracted yet", "the CLI exposes only the
introspection verbs", and "no CLI verb executes an `fs.*` or `process.*`
operation" — all three were accurate at some point and none is now. Keep
`learn`, `overview`, the `explain` catalog, this section, and README's Status
honest as `env` and `git` land; they are the first thing an agent consumer
reads, and every wave of this work has falsified at least one of them.

## Commands

Expand Down Expand Up @@ -562,11 +589,25 @@ as one.** An earlier version of this file, of `t73`, and of the issue #1 §17
comment all claimed a new unclassified path would fail CI immediately. An
adversarial live test landed **30 executed evasions at exit 0** and that claim
is retracted; the full inventory is
[issue #7](https://github.com/agentculture/shell-cli/issues/7). The one that
matters architecturally: `ALLOWLIST` is keyed per **module**, not per **site**,
so a brand-new `shell=True` spawn added to any of the 15 already-allow-listed
modules — `tools.py` included — passes green with no signal. Widening the
detected-call set does not fix that; per-site pinning would.
[issue #7](https://github.com/agentculture/shell-cli/issues/7).

The architectural hole in that report is **closed**. `ALLOWLIST` is keyed per
module, but `SITE_BASELINE` now pins each module's spawn-site *shape* — the
sorted multiset of canonical call names, with a `:shell` suffix for a literal
`shell=True` — so a new spawn added to an already-allow-listed module,
`tools.py` included, fails `--check`. Line numbers are deliberately excluded
from the shape, or every refactor above a spawn would be a false alarm. The
baseline is generated by `--emit-baseline`, never hand-typed, so `PINNED_SHA`
and the baseline move in one reviewed diff.

What remains open, and is documented in the module docstring: **indirection**
(`sp = subprocess`, `getattr`, `importlib`, `functools.partial` — only `import`
statements bind), **structure** (sibling re-export inside `colleague/`, `exec()`
of a source string, a spawn outside the package, a symlinked subdir), and
**non-literal `shell=` spellings** (`shell=SH`, `**{"shell": True}`). Two
consequences of the scope-aware binding rewrite are deliberate trades: rebinding
`subprocess` inside a function hides every spawn in it, and removing one spawn
while adding another of the same shape in the same module is invisible.

Read a green `--check` as *"the inventory has not drifted in the ways this
scanner can see"*, never as *"no new unmediated spawn path was added"*.
Expand Down
46 changes: 33 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,31 @@ surface drops the disclaimer or claims isolation.

## Status

**Scaffold, with the release pipeline hardened.** The agent-first CLI skeleton
(`whoami`, `learn`, `explain`, `overview`, `doctor`, `cli`) is real and green,
and publishing is gated on the full quality suite plus a built-wheel smoke test.

**No operation, environment, policy, or runner has been built yet.** The
introspection verbs below are the entire implemented surface today.
**The operation core is built, and the CLI has started catching up to it.** The
library has `Operation`, `OperationResult`, `Environment`, the ported policy
evaluator, the evidence contract, and a `HostRunner` with real execution —
`fs.read`, `fs.list`, `fs.stat`, `fs.write`, `fs.edit`, `fs.media`,
`process.exec`, and `process.shell` are all built and green.

The CLI exposes the introspection verbs (`whoami`, `learn`, `explain`,
`overview`, `doctor`, `cli`), two operation-aware nouns — `policy
check`/`policy explain` evaluate the same gate `execute()` uses, and `operation
show` retrieves persisted evidence — and two groups whose verbs **execute**
operations: `shell fs read|list|stat|write|edit|media` and `shell process
exec|shell`. They front `operations.execute()` rather than reimplementing
anything, so the CLI and the library share one engine. Mutation and execution
verbs preview by default and require `--apply`, and the flag is derived from
each kind's registered intent rather than hand-maintained. `process shell`
accepts the `project` profile only — a raw string is re-interpreted by a shell
after the gate read it, so it cannot honestly carry control-plane trust.

`shell env describe` renders the selected environment's two axes and the
runner's posture. `env create`/`env destroy` are deliberately absent until the
lifecycle engine exists, rather than shipped as a facade.

**Still library-only:** the `git` noun group does not exist yet, and neither
does a container runner or colleague delegation.
Publishing is gated on the full quality suite plus a built-wheel smoke test.

[Issue #1](https://github.com/agentculture/shell-cli/issues/1) is the source of
truth for scope; the converged spec and the buildable plan live in
Expand All @@ -74,16 +93,17 @@ intent -> Operation -> policy + preview -> environment backend
selects what may be observed and changed; a runner axis selects what executes
it.

**Every row below is a design target, not a description of shipped behaviour.**
Neither runner exists yet. The `Intended guarantee` column says what each
combination is *meant* to provide once built; today it provides nothing, because
there is nothing there. Read it as a specification, and do not rely on any row
as a present-tense security property.
**The Container rows are a design target, not a description of shipped
behaviour.** `HostRunner` is built and is what every shipped operation runs
through today; `ContainerRunner` does not exist yet. The `Intended guarantee`
column says what each combination provides for the Host rows now, and what the
Container rows are *meant* to provide once built — read the Container rows as a
specification and do not rely on them as a present-tense security property.

| Workspace | Runner | Intended guarantee | Built? |
|---|---|---|---|
| Checkout | Host | Guarded host execution; no isolation | No |
| Worktree | Host | Reviewable/recoverable changes; no process isolation | No |
| Checkout | Host | Guarded host execution; no isolation | Yes |
| Worktree | Host | Reviewable/recoverable changes; no process isolation | Yes |
| Checkout | Container | Execution isolation against the selected checkout | No |
| Worktree | Container | Preferred autonomous mode | No |

Expand Down
Loading
Loading