feat(manifest): per-file MD directory loader (PRD 0011)
test / unit (pull_request) Successful in 13s
test / integration (pull_request) Successful in 22s

Manifest.resolve walks $HOME/.claude-bottle/{bottles,agents}/ and
$CWD/.claude-bottle/agents/ instead of reading claude-bottle.json.
A bottles/ subdir under $CWD is logged as a warn and ignored —
the filesystem layout IS the trust boundary, no resolver check
needed.

If claude-bottle.json exists alongside no .claude-bottle/ dir at
either location, dies with a clear pointer at the README — the
manifest format changed and we don't silently fall back.

Manifest.from_md_dirs(home, cwd) is the programmatic entry point
tests use to build a Manifest from fixture directories without
touching os.environ. Manifest.from_json_obj is preserved for
tests that still want to build manifests in-memory.

Bottle / agent frontmatter goes through Bottle.from_dict /
Agent.from_dict — same validators as today's JSON path. Unknown
top-level frontmatter keys die with a "did you mean" pointer
listing accepted keys. Filenames that don't match [a-z][a-z0-9-]*
are skipped with a warn.

Agent files accept the Claude Code subagent passthrough fields
(name, description, model, color, memory) so the same file can
drop into ~/.claude/agents/ — claude-bottle ignores them at
launch but doesn't reject.

The dry-run integration test ships a real MD fixture tree now;
all 200 unit + 17 integration tests stay green.
This commit is contained in:
2026-05-24 22:15:02 -04:00
parent 8c1e4d0220
commit 6ba5f9a9d3
3 changed files with 575 additions and 55 deletions
+17 -7
View File
@@ -20,13 +20,23 @@ class TestDryRunPlan(unittest.TestCase):
def test_dry_run_emits_structured_plan(self):
work_dir = Path(tempfile.mkdtemp())
try:
manifest = work_dir / "claude-bottle.json"
manifest.write_text(json.dumps({
"bottles": {"dev": {"egress": {"allowlist": ["example.org"]}}},
"agents": {
"demo": {"skills": [], "prompt": "", "bottle": "dev"},
},
}))
# PRD 0011 layout: per-file MD under .claude-bottle/.
# work_dir doubles as $HOME and as cwd for this test.
cb = work_dir / ".claude-bottle"
(cb / "bottles").mkdir(parents=True)
(cb / "agents").mkdir(parents=True)
(cb / "bottles" / "dev.md").write_text(
"---\n"
"egress:\n"
" allowlist:\n"
" - example.org\n"
"---\n"
)
(cb / "agents" / "demo.md").write_text(
"---\n"
"bottle: dev\n"
"---\n"
)
# Under act_runner with a host-mounted docker socket, the
# `docker network ls` / `docker ps -a` calls from inside the