Commit ccd69e2
Feat/apply modularity review (#12)
* test(cli): add opt-in end-to-end orchestrator<->agent tests
Add real-container E2E tests that validate the full round-trip between the
orchestrator (the q CLI) and agents running in Apple containers:
- test_claude_agent_e2e.py: spawn a Claude agent, then assert status.json,
the persisted log, the [agent:status] markers and the worktree commit all
reach the orchestrator.
- test_pi_agent_e2e.py: the same round-trip for a PI agent (local mlx_lm
backend), asserting the report is tagged agent_kind=pi.
Tests are gated by STACKAI_E2E=1 via collect_ignore_glob, so the default
pytest run and the mutmut suite never collect them — they require Apple
Container CLI and are local-only, like the acceptance suite. Adds the
`e2e` marker and a `make e2e-test` target (excluded from CI and local-qa).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(cli): centralise Makefile target names in a contract module
The CLI's knowledge of which config/Makefile targets exist was scattered
across five command modules as bare string literals, with nothing to catch
a renamed target until a user hit the failure at runtime.
Add src/container_cli/targets.py — a Target StrEnum that is the single
source of truth for the 19 targets the CLI invokes. The command modules
now reference Target.SPAWN etc.; run_make is typed against Target. Because
StrEnum members equal their string value, every existing assertion stays
green unchanged.
tests/test_targets.py adds a contract test that parses config/Makefile and
fails if any Target member is missing — a rename is now caught in CI.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(cli): consolidate worktree and status logic in utils.py
_agents_home() was duplicated byte-for-byte in agents.py and pi_agents.py,
and the status command bodies were near-identical copies that also knew the
private .agent/status.json layout. run/shell shared an identical body too.
- Add utils.agents_home() — the single definition of the worktree-root
rule (was duplicated 2x in Python).
- Add utils.print_agent_status(branch, label=...) — the single reader of
.agent/status.json; agents.status and pi.status now delegate to it,
keeping their [status] / [pi-status] message tags via the label arg.
- Extract run._coordinator() so run and shell stop duplicating their body.
Behaviour is unchanged: status still reads the file directly and still
exits 1 when it is missing, so no .feature file changes. The acceptance
conftest now patches find_git_root in utils (one patch instead of two),
and the moved TestAgentsHome tests live in test_utils.py alongside new
coverage for print_agent_status.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(cli): drop the dead Typer apps from build/network/run
build.py, network.py and run.py each created a typer.Typer() app and
decorated their functions with @app.command(), but main.py never mounted
those apps — it registers the bare functions directly. The app objects
were dead code that misled readers into expecting a mounted sub-app.
Remove the unused typer.Typer() and @app.command() decorators from the
three modules; their functions are now plain `def`s, registered by
main.py exactly as before. agents.py and pi_agents.py keep their Typer
app because they genuinely are mounted as sub-apps — that becomes the one
consistent pattern. The q CLI surface is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(cli): locate config/Makefile by walking up in the contract test
test_targets.py resolved the repo root with a fixed parent index, which
pointed at the wrong directory when the test ran from the mutmut copy
under mutants/ (one extra path level). Walk up from the test file until a
config/Makefile is found instead — correct both in tests/ and mutants/tests/.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs: document the E2E tests and refresh the CLI reference
- Add docs/agents/e2e-tests.md: prerequisites, how to run `make e2e-test`,
what each round-trip test asserts, and the cost/CI caveats.
- cli.md: add targets.py to the package structure, note the Target
contract and the tests/e2e/ suite.
- CLAUDE.md: list `make e2e-test` in the Testing section.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* add: modularity review
* feat(agents): add --model flag (default opus), persist lifecycle markers, bump bundled claude CLI
Add a --model option to `q spawn` so the orchestrator governs the headless
agent's Claude model instead of inheriting the host's interactive
~/.claude/settings.json preference (which can belong to a different account
than the container's OAuth token). The flag flows CLI -> Makefile
(MODEL ?= opus) -> -e AGENT_MODEL -> entrypoint (`claude --model "$AGENT_MODEL"`),
default opus.
Fixes a second bug that the original failure masked: emit_marker() echoed
[agent:status] events only to the container's stdout, so once the --rm
container was gone `q agents summary` fell back to agent.log and printed
"(no structured events found)". emit_marker now also appends to agent.log
(group redirect so the open error is suppressed cleanly in the shellspec
mock env), and tee -a preserves the early starting/working markers across
the claude run.
Bumps the bundled claude CLI in the Wolfi image. The previous version
(2.1.138) had a bug rejecting Opus with `400 role 'system' is not supported
on this model`. The Dockerfile now ends the install RUN with
`&& claude --version`, both as a build-time trace and as a cache-busting
tail so a plain rebuild picks up newer CLIs without forcing --no-cache.
Tests and docs:
- new acceptance scenario for `q spawn --model opus`
- unit tests for MODEL presence/absence in make_vars
- shellspec asserts default opus and AGENT_MODEL=sonnet override
- SKILL.md, docs/agents/{cli,container-agent,spawn-agent-skill}.md
Gates after rebuild to claude 2.1.149: ruff, 100 unit/acceptance, 59
shellspec, mutation 95.2%, and make e2e-test (Claude round-trip) all
green; PI e2e remains opt-in (local mlx server).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test(e2e): opt-in mlx_lm.server autostart fixture for the PI round-trip
Add a session-scoped fixture in tests/e2e/conftest.py that — when
STACKAI_E2E_AUTOSTART_MLX=1 is exported — spawns mlx_lm.server itself,
waits for /v1/models to respond, yields to the tests, and tears the
subprocess down on session teardown. Without the env var, or when a
server is already reachable on the URL, the fixture is a no-op (a
manually-started server is honoured and not touched).
The invocation is the exact set of flags the PI agent expects (model
mlx-community/gemma-4-26b-a4b-it-4bit, port 8080, --temp 0.9,
--top-p 0.95, 6 GB prompt cache, etc.) — no coupling to the iac CLI:
the e2e suite must run standalone. Boot timeout is configurable via
STACKAI_E2E_MLX_BOOT_TIMEOUT (default 600s) to absorb first-run model
downloads, and the subprocess log lands in $TMPDIR/stackai-e2e-mlx.log
for diagnosis.
Verified end-to-end on a warm cache: both the Claude and PI
round-trips pass in ~33s.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: deimagjas <deimagjas@127.0.0.1>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>1 parent f2a2fa8 commit ccd69e2
31 files changed
Lines changed: 1483 additions & 141 deletions
File tree
- .claude/skills/spawn-agent
- app/cli
- src/container_cli
- commands
- tests
- acceptance
- features
- e2e
- config
- spec
- docs
- agents
- modularity-review/2026-05-21
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
46 | 46 | | |
47 | 47 | | |
48 | 48 | | |
| 49 | + | |
49 | 50 | | |
50 | 51 | | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
51 | 60 | | |
52 | 61 | | |
53 | 62 | | |
| |||
143 | 152 | | |
144 | 153 | | |
145 | 154 | | |
| 155 | + | |
146 | 156 | | |
147 | 157 | | |
148 | 158 | | |
| |||
405 | 415 | | |
406 | 416 | | |
407 | 417 | | |
| 418 | + | |
| 419 | + | |
408 | 420 | | |
409 | 421 | | |
410 | 422 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
53 | 53 | | |
54 | 54 | | |
55 | 55 | | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
56 | 59 | | |
57 | 60 | | |
58 | 61 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
42 | 42 | | |
43 | 43 | | |
44 | 44 | | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
45 | 52 | | |
46 | 53 | | |
47 | 54 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
25 | 25 | | |
26 | 26 | | |
27 | 27 | | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
28 | 31 | | |
29 | 32 | | |
30 | 33 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | | - | |
4 | | - | |
5 | | - | |
6 | 3 | | |
7 | 4 | | |
8 | 5 | | |
9 | 6 | | |
10 | | - | |
| 7 | + | |
| 8 | + | |
11 | 9 | | |
12 | 10 | | |
13 | 11 | | |
14 | 12 | | |
15 | | - | |
16 | | - | |
17 | | - | |
18 | | - | |
19 | | - | |
20 | | - | |
21 | | - | |
22 | | - | |
23 | 13 | | |
24 | 14 | | |
25 | 15 | | |
26 | 16 | | |
27 | 17 | | |
28 | 18 | | |
29 | 19 | | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
30 | 24 | | |
31 | 25 | | |
32 | 26 | | |
| |||
37 | 31 | | |
38 | 32 | | |
39 | 33 | | |
40 | | - | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
41 | 37 | | |
42 | 38 | | |
43 | 39 | | |
44 | 40 | | |
45 | 41 | | |
46 | | - | |
| 42 | + | |
47 | 43 | | |
48 | 44 | | |
49 | 45 | | |
50 | 46 | | |
51 | 47 | | |
52 | 48 | | |
53 | 49 | | |
54 | | - | |
| 50 | + | |
55 | 51 | | |
56 | 52 | | |
57 | 53 | | |
58 | 54 | | |
59 | 55 | | |
60 | 56 | | |
61 | 57 | | |
62 | | - | |
| 58 | + | |
63 | 59 | | |
64 | 60 | | |
65 | 61 | | |
66 | 62 | | |
67 | 63 | | |
68 | 64 | | |
69 | 65 | | |
70 | | - | |
| 66 | + | |
71 | 67 | | |
72 | 68 | | |
73 | 69 | | |
74 | 70 | | |
75 | 71 | | |
76 | 72 | | |
77 | 73 | | |
78 | | - | |
79 | | - | |
80 | | - | |
81 | | - | |
82 | | - | |
83 | | - | |
84 | | - | |
85 | | - | |
| 74 | + | |
86 | 75 | | |
87 | 76 | | |
88 | 77 | | |
89 | 78 | | |
90 | 79 | | |
91 | 80 | | |
92 | 81 | | |
93 | | - | |
| 82 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | | - | |
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
2 | 5 | | |
3 | 6 | | |
4 | 7 | | |
5 | 8 | | |
6 | 9 | | |
| 10 | + | |
7 | 11 | | |
8 | 12 | | |
9 | | - | |
10 | | - | |
11 | 13 | | |
12 | | - | |
13 | 14 | | |
14 | 15 | | |
15 | 16 | | |
| |||
20 | 21 | | |
21 | 22 | | |
22 | 23 | | |
23 | | - | |
| 24 | + | |
24 | 25 | | |
25 | 26 | | |
26 | | - | |
27 | 27 | | |
28 | 28 | | |
29 | | - | |
| 29 | + | |
30 | 30 | | |
31 | 31 | | |
32 | | - | |
33 | 32 | | |
34 | 33 | | |
35 | | - | |
| 34 | + | |
36 | 35 | | |
37 | 36 | | |
38 | | - | |
39 | 37 | | |
40 | 38 | | |
41 | | - | |
| 39 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | | - | |
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
2 | 5 | | |
3 | 6 | | |
4 | 7 | | |
5 | 8 | | |
6 | 9 | | |
| 10 | + | |
7 | 11 | | |
8 | 12 | | |
9 | | - | |
10 | | - | |
11 | 13 | | |
12 | | - | |
13 | 14 | | |
14 | 15 | | |
15 | 16 | | |
| |||
20 | 21 | | |
21 | 22 | | |
22 | 23 | | |
23 | | - | |
| 24 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
10 | 10 | | |
11 | 11 | | |
12 | 12 | | |
13 | | - | |
14 | | - | |
15 | | - | |
16 | 13 | | |
17 | 14 | | |
18 | 15 | | |
19 | 16 | | |
20 | | - | |
| 17 | + | |
| 18 | + | |
21 | 19 | | |
22 | 20 | | |
23 | 21 | | |
24 | 22 | | |
25 | | - | |
26 | | - | |
27 | | - | |
28 | | - | |
29 | | - | |
30 | | - | |
31 | | - | |
32 | | - | |
33 | 23 | | |
34 | 24 | | |
35 | 25 | | |
| |||
43 | 33 | | |
44 | 34 | | |
45 | 35 | | |
46 | | - | |
| 36 | + | |
47 | 37 | | |
48 | 38 | | |
49 | 39 | | |
| |||
87 | 77 | | |
88 | 78 | | |
89 | 79 | | |
90 | | - | |
| 80 | + | |
91 | 81 | | |
92 | 82 | | |
93 | 83 | | |
94 | 84 | | |
95 | 85 | | |
96 | | - | |
| 86 | + | |
97 | 87 | | |
98 | 88 | | |
99 | 89 | | |
100 | 90 | | |
101 | 91 | | |
102 | 92 | | |
103 | 93 | | |
104 | | - | |
| 94 | + | |
105 | 95 | | |
106 | 96 | | |
107 | 97 | | |
108 | 98 | | |
109 | 99 | | |
110 | 100 | | |
111 | 101 | | |
112 | | - | |
| 102 | + | |
113 | 103 | | |
114 | 104 | | |
115 | 105 | | |
116 | 106 | | |
117 | 107 | | |
118 | 108 | | |
119 | 109 | | |
120 | | - | |
| 110 | + | |
121 | 111 | | |
122 | 112 | | |
123 | 113 | | |
124 | 114 | | |
125 | 115 | | |
126 | 116 | | |
127 | 117 | | |
128 | | - | |
129 | | - | |
130 | | - | |
131 | | - | |
132 | | - | |
133 | | - | |
134 | | - | |
| 118 | + | |
0 commit comments