context-guard-cli 2.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. context_guard/__init__.py +1 -0
  2. context_guard/_data/hosts/antigravity/hooks.snippet.json +16 -0
  3. context_guard/_data/hosts/antigravity/rules/context-guard.md +15 -0
  4. context_guard/_data/hosts/claude-code/commands/cg-continue.md +13 -0
  5. context_guard/_data/hosts/claude-code/commands/cg-new.md +11 -0
  6. context_guard/_data/hosts/claude-code/mcp.snippet.json +7 -0
  7. context_guard/_data/hosts/claude-code/settings.snippet.json +12 -0
  8. context_guard/_data/hosts/opencode/agent.snippet.json +7 -0
  9. context_guard/_data/hosts/opencode/commands/cg-continue.md +16 -0
  10. context_guard/_data/hosts/opencode/commands/cg-new.md +12 -0
  11. context_guard/_data/hosts/opencode/mcp.snippet.json +9 -0
  12. context_guard/_data/hosts/opencode/permissions.snippet.json +12 -0
  13. context_guard/_data/phases/execute.md +99 -0
  14. context_guard/_data/phases/plan.md +136 -0
  15. context_guard/_data/phases/verify.md +129 -0
  16. context_guard/guard/__init__.py +1 -0
  17. context_guard/guard/assets.py +94 -0
  18. context_guard/guard/cli.py +307 -0
  19. context_guard/guard/commands.py +811 -0
  20. context_guard/guard/errors.py +71 -0
  21. context_guard/guard/locking.py +181 -0
  22. context_guard/guard/manifest.py +69 -0
  23. context_guard/guard/migrate.py +288 -0
  24. context_guard/guard/paths.py +199 -0
  25. context_guard/guard/setup.py +476 -0
  26. context_guard/guard/transaction.py +403 -0
  27. context_guard/mcp_server.py +280 -0
  28. context_guard_cli-2.1.0.dist-info/METADATA +296 -0
  29. context_guard_cli-2.1.0.dist-info/RECORD +32 -0
  30. context_guard_cli-2.1.0.dist-info/WHEEL +4 -0
  31. context_guard_cli-2.1.0.dist-info/entry_points.txt +4 -0
  32. context_guard_cli-2.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1 @@
1
+ """context-guard package."""
@@ -0,0 +1,16 @@
1
+ {
2
+ "hooks": {
3
+ "PreToolUse": [
4
+ {
5
+ "matcher": {
6
+ "tool": "run_command",
7
+ "commandPattern": "^\\s*(cg|context-guard)\\s+approve\\b"
8
+ },
9
+ "action": {
10
+ "decision": "deny",
11
+ "message": "human-only command — ask the user to run it"
12
+ }
13
+ }
14
+ ]
15
+ }
16
+ }
@@ -0,0 +1,15 @@
1
+ <!-- context-guard:begin -->
2
+ ## ACTIVE PERSISTENCE CONTRACT: context-guard
3
+ MANDATORY BOOTSTRAP — read before responding to anything in a project that
4
+ has a `.context-guard/` directory:
5
+ 1. Read `AGENTS.md` at the project root and follow it as your state contract.
6
+ 2. State manager binary: `cg` (or `context-guard`). Operative subcommands:
7
+ begin | commit | rollback | checkpoint | status | next-task | validate
8
+ Human-only subcommand (never run by the agent): `cg approve`.
9
+ If `commit` returns EXIT_APPROVAL_REQUIRED (6): stop and ask the user to
10
+ run `cg approve --change <name>` themselves.
11
+ 3. Check `.context-guard/changes/*/manifest.json` for an active change and
12
+ act accordingly (cold start, resume via `cg status`, or report a stuck
13
+ lock — never fix a stuck lock by editing the manifest by hand).
14
+ 4. Phase instructions live in `.context-guard/phases/{plan,execute,verify}.md`.
15
+ <!-- context-guard:end -->
@@ -0,0 +1,13 @@
1
+ ---
2
+ description: Resume the active context-guard change from where it left off
3
+ ---
4
+
5
+ Change (optional): $ARGUMENTS
6
+
7
+ 1. Run `cg status [--change $ARGUMENTS]`.
8
+ 2. Load and follow, exactly, the file matching the reported `lock_phase`:
9
+ - PLAN -> `.context-guard/phases/plan.md`
10
+ - EXECUTE -> `.context-guard/phases/execute.md`
11
+ - VERIFY -> `.context-guard/phases/verify.md`
12
+ 3. If the lock is held by another agent, stop and report the conflict —
13
+ do not retry automatically.
@@ -0,0 +1,11 @@
1
+ ---
2
+ description: Start a new context-guard change and run its PLAN phase
3
+ ---
4
+
5
+ Change name: $ARGUMENTS (ask the user for one if empty).
6
+
7
+ 1. Run `cg new $ARGUMENTS`
8
+ 2. Load `.context-guard/phases/plan.md` and follow it exactly for this
9
+ change, through its human review gate.
10
+ 3. Never run `cg commit --next-phase EXECUTE` without an explicit
11
+ go-ahead from the user in this chat.
@@ -0,0 +1,7 @@
1
+ {
2
+ "mcpServers": {
3
+ "context-guard": {
4
+ "command": "context-guard-mcp"
5
+ }
6
+ }
7
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "_comment": "Merge into .claude/settings.json. PLAN.md 0.6: cg approve is cooperative by itself; putting it on the ask list is the hard control that pairs with it.",
3
+ "permissions": {
4
+ "ask": [
5
+ "Bash(cg approve*)",
6
+ "Bash(context-guard approve*)"
7
+ ],
8
+ "deny": [
9
+ "Edit(.context-guard/**/manifest.json)"
10
+ ]
11
+ }
12
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "context-guard": {
3
+ "mode": "all",
4
+ "description": "Context Guard — transactional memory for AI coding agents",
5
+ "prompt": "Read AGENTS.md at the project root and follow it as your state contract. Phase instructions live in .context-guard/phases/{plan,execute,verify}.md. Use the cg CLI (begin/commit/rollback/checkpoint/status/next-task/validate); cg approve is a human-only command. If commit returns exit code 6 (APPROVAL_REQUIRED), stop and ask the user to run cg approve themselves."
6
+ }
7
+ }
@@ -0,0 +1,16 @@
1
+ ---
2
+ description: Resume the active context-guard change from where it left off
3
+ agent: context-guard
4
+ ---
5
+
6
+ !`cg status --format json`
7
+
8
+ Change (optional): $ARGUMENTS. The status above is already current — do not
9
+ re-run `cg status` yourself.
10
+
11
+ 1. Load and follow, exactly, the file matching the reported `lock_phase`:
12
+ - PLAN -> `.context-guard/phases/plan.md`
13
+ - EXECUTE -> `.context-guard/phases/execute.md`
14
+ - VERIFY -> `.context-guard/phases/verify.md`
15
+ 2. If the lock is held by another agent, stop and report the conflict —
16
+ do not retry automatically.
@@ -0,0 +1,12 @@
1
+ ---
2
+ description: Start a new context-guard change and run its PLAN phase
3
+ agent: context-guard
4
+ ---
5
+
6
+ Change name: $ARGUMENTS (ask the user for one if empty).
7
+
8
+ 1. Run `cg new $ARGUMENTS`
9
+ 2. Load `.context-guard/phases/plan.md` and follow it exactly for this
10
+ change, through its human review gate.
11
+ 3. Never run `cg commit --next-phase EXECUTE` without an explicit
12
+ go-ahead from the user in this chat.
@@ -0,0 +1,9 @@
1
+ {
2
+ "mcp": {
3
+ "context-guard": {
4
+ "type": "local",
5
+ "command": ["context-guard-mcp"],
6
+ "enabled": true
7
+ }
8
+ }
9
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "$schema": "https://opencode.ai/config.json",
3
+ "permission": {
4
+ "bash": {
5
+ "cg approve*": "ask",
6
+ "context-guard approve*": "ask"
7
+ },
8
+ "edit": {
9
+ ".context-guard/**/manifest.json": "deny"
10
+ }
11
+ }
12
+ }
@@ -0,0 +1,99 @@
1
+ # EXECUTE Phase
2
+
3
+ ## Purpose
4
+
5
+ EXECUTE is the only phase that changes the project's source code. It works
6
+ through `tasks.md` task by task until the change is implemented.
7
+
8
+ **Prerequisite:** `lock_phase == EXECUTE` — this only happens after the human
9
+ approved the PLAN commit.
10
+
11
+ ## What to do
12
+
13
+ ### Step 1: Begin the transaction
14
+
15
+ ```bash
16
+ cg begin --context <path> --phase EXECUTE --change <change-name>
17
+ ```
18
+
19
+ ### Step 2: Read the context
20
+
21
+ Before writing any code:
22
+
23
+ 1. **Plan** — read `objective.md` for intent and scope.
24
+ 2. **Tasks** — read `tasks.md` for the full breakdown.
25
+ 3. **Existing code** — read the files each task actually touches.
26
+
27
+ ### Step 3: Detect TDD mode
28
+
29
+ Check, in order: a project config declaring `tdd: true`, existing test
30
+ patterns in the codebase (a `tests/` directory with a consistent style is a
31
+ strong signal), otherwise assume standard mode (code first).
32
+
33
+ ### Step 4: Implement tasks
34
+
35
+ **TDD mode (RED → GREEN → REFACTOR)**, for each task:
36
+ 1. Read the task description and any related acceptance criteria.
37
+ 2. **RED**: write a test describing the expected behavior → confirm it FAILS.
38
+ 3. **GREEN**: implement the minimum code to make it pass → confirm it PASSES.
39
+ 4. **REFACTOR**: clean up without changing behavior → confirm it still PASSES.
40
+ 5. Check the task off in `tasks.md` (`- [x]`).
41
+
42
+ > CRITICAL: run tests in a real terminal. Simulating or inferring results is
43
+ > not allowed.
44
+
45
+ **Standard mode**, for each task: read the description, read existing code
46
+ patterns, write the code, check the task off.
47
+
48
+ Default batch size: 3 tasks per invocation, adjusted to available context.
49
+ For task lists that support parallel work, `cg next-task --context <path>
50
+ --change <name>` and `cg claim-task` let multiple agents work the same
51
+ change without stepping on each other — each claim carries a lease, so a
52
+ crashed agent's tasks become available again automatically; see `AGENTS.md`.
53
+
54
+ ### Step 5: Verify progress with the middleware
55
+
56
+ ```bash
57
+ cg check-completion --context <path> --change <change-name>
58
+ ```
59
+
60
+ Reports `total`, `completed`, and whether every task is done — the agent
61
+ never counts checkboxes by hand.
62
+
63
+ ### Step 6: Report
64
+
65
+ ```markdown
66
+ ## Implementation progress
67
+
68
+ **Change**: {change-name}
69
+ **Mode**: {TDD | Standard}
70
+
71
+ ### Completed tasks
72
+ - [x] [T001] {description}
73
+
74
+ ### Files changed
75
+ | File | Action | What happened |
76
+ |------|--------|----------------|
77
+
78
+ ### Deviations from the plan
79
+ {List, or "None — implementation matches the plan."}
80
+
81
+ ### Issues found
82
+ {List, or "None."}
83
+
84
+ ### Status
85
+ {N}/{total} tasks complete. {Ready for VERIFY / next batch pending}
86
+ ```
87
+
88
+ ## Rules
89
+
90
+ - Always follow the plan's decisions — do not improvise architecture.
91
+ - Always match existing code patterns and conventions.
92
+ - Check tasks off in `tasks.md` as you complete them, never in bulk at the
93
+ end.
94
+ - If the plan turns out to be wrong or incomplete, write it down as a
95
+ deviation — never diverge silently.
96
+ - If a task is blocked, stop and report it instead of skipping ahead.
97
+
98
+ Once every task is checked off, run `cg commit --context <path> --change
99
+ <change-name> --next-phase VERIFY` and move on to `phases/verify.md`.
@@ -0,0 +1,136 @@
1
+ # PLAN Phase
2
+
3
+ ## Purpose
4
+
5
+ PLAN absorbs exploration, proposal, and scoping into a single block of work.
6
+ It produces `objective.md` and `tasks.md` and submits them to a **mandatory
7
+ human review** before the change is allowed into EXECUTE.
8
+
9
+ ```
10
+ draft → human review → commit
11
+ ```
12
+
13
+ The commit into EXECUTE is the event that closes PLAN. **The agent must
14
+ never run that commit without an explicit go-ahead from the human.**
15
+
16
+ ## What to do
17
+
18
+ ### Step 1: Begin the transaction
19
+
20
+ ```bash
21
+ cg begin --context <path> --phase PLAN --change <change-name>
22
+ ```
23
+
24
+ If the change is new, `cg new <change-name> --context <path>` does this for
25
+ you and scaffolds `objective.md`, `snapshot.md`, `tasks.md`,
26
+ `review-report.md`, `verify-report.md` in
27
+ `.context-guard/changes/<change-name>/`, all starting as `[PENDING]`.
28
+
29
+ ### Step 2: Discover the stack
30
+
31
+ ```bash
32
+ ls package.json pyproject.toml composer.json go.mod Cargo.toml docker-compose.yml 2>/dev/null
33
+ ```
34
+
35
+ Read whatever manifests exist to identify the stack, framework, and tooling
36
+ before analyzing anything.
37
+
38
+ ### Step 3: Explore and analyze
39
+
40
+ - Is this new functionality, a bug fix, or a refactor?
41
+ - Read the relevant code: entry points, affected modules, existing tests.
42
+ - Compare approaches if there are real alternatives.
43
+
44
+ ### Step 4: Draft the artifacts
45
+
46
+ Write directly to disk, replacing the `[PENDING]` scaffolds:
47
+
48
+ **`objective.md`**
49
+ ```markdown
50
+ # Objective: {Change Title}
51
+
52
+ ## Intent
53
+ {What problem this solves and why}
54
+
55
+ ## Scope
56
+ ### In scope
57
+ - {deliverable}
58
+ ### Out of scope
59
+ - {deferred}
60
+
61
+ ## Success criteria
62
+ - [ ] {measurable outcome}
63
+
64
+ ## Open questions
65
+ - [ ] {unresolved question — mark blocking ones with [!]}
66
+ ```
67
+
68
+ **`tasks.md`**
69
+ ```markdown
70
+ # Tasks: {Change Title}
71
+
72
+ ## Phase 1: {e.g. Infrastructure}
73
+ - [ ] [T001] {atomic task with a concrete file path}
74
+
75
+ ## Phase 2: {e.g. Core implementation}
76
+ - [ ] [T002] {atomic task}
77
+
78
+ ## Phase 3: {e.g. Testing}
79
+ - [ ] [T003] {atomic task}
80
+ ```
81
+
82
+ Task rules: one task = one file or logical module (no "monster tasks"); IDs
83
+ follow `[Txxx]` — `check-completion` and `next-task` parse them; group by
84
+ infrastructure → implementation → testing; reference success criteria as
85
+ acceptance checks.
86
+
87
+ Optionally update `snapshot.md` with a short state-of-the-world summary if
88
+ the change spans multiple sessions — `cg validate` requires the file to
89
+ exist (alongside `objective.md`) and checks size and language, though it
90
+ does not check for a leftover `[PENDING]`; that is `commit`'s job.
91
+
92
+ ### Step 5: Human review gate
93
+
94
+ **The agent cannot proceed on its own — this is the one barrier only a human
95
+ can cross.**
96
+
97
+ 1. Present `objective.md` and `tasks.md` with an executive summary.
98
+ 2. List architecture decisions and open questions explicitly.
99
+ 3. Run `cg validate --context <path> --change <change-name>` and fix
100
+ anything it flags (missing file, oversized artifact, non-English text) —
101
+ it does not check for `[PENDING]`; confirm by eye that both artifacts
102
+ are actually filled in before asking for approval.
103
+ 4. Ask the human, in this conversation, to review and confirm.
104
+ 5. Ask them to record it — **never run this yourself**:
105
+
106
+ ```bash
107
+ cg approve --context <path> --change <change-name> --by <who>
108
+ ```
109
+
110
+ Without it, Step 6 fails with `APPROVAL_REQUIRED` (exit 6). The approval is
111
+ spent by the commit it authorizes, so if the plan is revised afterwards the
112
+ human has to approve again. The harness's permission prompt on `cg approve`
113
+ (configured per `adapters/*/PERMISSIONS.md`) is what makes that confirmation
114
+ hard to skip rather than merely polite.
115
+
116
+ ### Step 6: Commit and close PLAN
117
+
118
+ Only after the human confirms:
119
+
120
+ ```bash
121
+ cg commit --context <path> --change <change-name> --next-phase EXECUTE
122
+ ```
123
+
124
+ This advances `lock_phase` to `EXECUTE`, releases the phase lock, and
125
+ auto-generates a checkpoint of the DAG state. Report to the user that PLAN is
126
+ locked and EXECUTE is open, with the suggested next command: `/cg-continue`.
127
+
128
+ ## Rules
129
+
130
+ - Never commit into EXECUTE without explicit human approval.
131
+ - If the human requests changes, regenerate only the sections asked for, not
132
+ the whole artifact.
133
+ - Blocking open questions (`[!]`) must be resolved before requesting review.
134
+ - Always read the real code — never assume about the codebase.
135
+ - `objective.md` and `tasks.md` are separate artifacts with separate
136
+ purposes; do not merge them into one file.
@@ -0,0 +1,129 @@
1
+ # VERIFY Phase
2
+
3
+ ## Purpose
4
+
5
+ VERIFY is the final quality gate. It proves — with evidence from real
6
+ execution, not static reasoning alone — that the implementation is complete
7
+ and correct. If the verdict is APPROVED, the **archive step** runs
8
+ immediately as part of this same phase.
9
+
10
+ ## What to do
11
+
12
+ ### Step 1: Begin the transaction
13
+
14
+ ```bash
15
+ cg begin --context <path> --phase VERIFY --change <change-name>
16
+ ```
17
+
18
+ ### Step 2: Read the context
19
+
20
+ 1. **Plan** — `objective.md` for intent and success criteria.
21
+ 2. **Tasks** — `tasks.md` for what was supposed to happen.
22
+
23
+ ### Step 3: Check completeness
24
+
25
+ ```bash
26
+ cg check-completion --context <path> --change <change-name>
27
+ ```
28
+
29
+ List any incomplete tasks. Mark CRITICAL if core tasks are unfinished,
30
+ WARNING if only cleanup tasks remain.
31
+
32
+ ### Step 4: Check correctness against the objective
33
+
34
+ For each success criterion in `objective.md`: is there real evidence in the
35
+ codebase that it's met? Mark CRITICAL if a criterion has no implementation,
36
+ WARNING if only partially covered.
37
+
38
+ ### Step 5: Run tests and build for real
39
+
40
+ > CRITICAL: use a real terminal. Simulating or inferring results is not
41
+ > allowed.
42
+
43
+ Detect and run the project's test command (`package.json` scripts, `pytest`/
44
+ `pyproject.toml`, a `Makefile` target, or whatever `objective.md` calls out)
45
+ and its build/typecheck command if one exists. A missing build step is a
46
+ WARNING, not a CRITICAL — some projects have none.
47
+
48
+ ### Step 6: Write `review-report.md` and `verify-report.md`
49
+
50
+ `review-report.md` covers static findings (code quality, plan adherence,
51
+ deviations). `verify-report.md` covers dynamic evidence:
52
+
53
+ ```markdown
54
+ ## Verification report
55
+
56
+ **Change**: {change-name}
57
+
58
+ ### Completeness
59
+ | Metric | Value |
60
+ |--------|-------|
61
+ | Total tasks | {N} |
62
+ | Completed | {N} |
63
+ | Incomplete | {N} |
64
+
65
+ ### Build and test run
66
+ **Build**: pass / fail
67
+ **Tests**: {N} passed / {N} failed / {N} skipped
68
+
69
+ ### Issues found
70
+ **CRITICAL**: {list, or "None"}
71
+ **WARNING**: {list, or "None"}
72
+
73
+ ### Verdict
74
+ {APPROVED / APPROVED WITH WARNINGS / REJECTED}
75
+ ```
76
+
77
+ ### Step 7: Decide
78
+
79
+ ```text
80
+ If there are CRITICAL issues:
81
+ → run `cg rollback --context <path> --change <change-name>`
82
+ → report the issues to the user; the change returns to EXECUTE
83
+
84
+ If the verdict is APPROVED or APPROVED WITH WARNINGS:
85
+ → run `cg commit --context <path> --change <change-name> --next-phase ARCHIVE`
86
+ → continue immediately to Step 8 (archive), in the same invocation
87
+ ```
88
+
89
+ ---
90
+
91
+ ### Step 8: ARCHIVE — close out the change
92
+
93
+ This runs automatically after an APPROVED verdict, in the same `/cg-continue`
94
+ invocation that ran VERIFY. There is no separate manual archive command for
95
+ this path.
96
+
97
+ 1. Confirm `verify-report.md` and `review-report.md` contain no `[PENDING]`
98
+ and no unresolved CRITICAL issue. If they do, abort.
99
+ 2. Check `git status --porcelain`: a dirty tree blocks archiving — ask the
100
+ user to commit first.
101
+ 3. Run:
102
+ ```bash
103
+ cg archive --context <path> --change <change-name>
104
+ ```
105
+ This copies `.context-guard/changes/<change-name>/` to
106
+ `.context-guard/changes/archive/<change-name>/` and removes the live
107
+ change directory. The archive is an audit trail — never edit or delete an
108
+ archived change.
109
+ 4. Report to the user:
110
+ ```markdown
111
+ ## Change archived
112
+
113
+ **Change**: {change-name}
114
+ **Archived at**: .context-guard/changes/archive/{change-name}/
115
+
116
+ The change has been planned, implemented, verified, and archived.
117
+ Ready for the next `/cg-new`.
118
+ ```
119
+
120
+ ## Rules
121
+
122
+ - Always read real source and run real tests — static analysis alone is not
123
+ verification.
124
+ - CRITICAL issues must be resolved before archiving.
125
+ - WARNING issues should be resolved but do not block.
126
+ - Do not fix problems found during VERIFY — only report them; fixes happen
127
+ back in EXECUTE.
128
+ - Never archive while `verify-report.md` has an unresolved CRITICAL issue.
129
+ - Never modify or delete an already-archived change.
@@ -0,0 +1 @@
1
+ # context-guard middleware package
@@ -0,0 +1,94 @@
1
+ """Accessors for the artifacts shipped as package data (PLAN-2.1 F1).
2
+
3
+ Everything a host needs — the three phase documents, each host's slash
4
+ commands, rule files and config snippets — lives under `context_guard/_data/`
5
+ and is reached through `importlib.resources`. Never through paths relative to
6
+ this file or to the working directory: the package has to work installed from
7
+ a wheel, with no clone of the repo anywhere on the machine.
8
+
9
+ Names are validated against a fixed allowlist rather than joined onto the data
10
+ directory. These accessors are called with values that come from CLI flags, so
11
+ a joined path would turn `cg` into a reader for arbitrary files.
12
+ """
13
+
14
+ from importlib import resources
15
+
16
+ from .errors import EXIT_GENERIC, GuardError
17
+
18
+ PHASES = ("plan", "execute", "verify")
19
+
20
+ HOSTS = ("claude-code", "opencode", "antigravity")
21
+
22
+ # Which snippets each host ships, keyed by the short name callers pass. The
23
+ # file is always "<name>.snippet.json"; the mapping exists to be an allowlist,
24
+ # not to spell out a naming convention.
25
+ SNIPPETS = {
26
+ "claude-code": ("settings", "mcp"),
27
+ "opencode": ("agent", "permissions", "mcp"),
28
+ "antigravity": ("hooks",),
29
+ }
30
+
31
+
32
+ class AssetNotFoundError(GuardError):
33
+ """A requested phase, host or snippet is not part of the packaged data."""
34
+
35
+ def __init__(self, message):
36
+ super().__init__(f"FAIL|ASSET_NOT_FOUND|{message}", EXIT_GENERIC)
37
+
38
+
39
+ def _data_root():
40
+ """The `_data` directory as a Traversable.
41
+
42
+ `files()` targets the package and joins from there, so this resolves the
43
+ same whether the package sits in a source checkout or in site-packages.
44
+ """
45
+ return resources.files("context_guard").joinpath("_data")
46
+
47
+
48
+ def get_phase(name):
49
+ """Return the text of `_data/phases/<name>.md`.
50
+
51
+ `name` must be one of PHASES — anything else raises, including a value that
52
+ happens to name a real file elsewhere in the tree.
53
+ """
54
+ if name not in PHASES:
55
+ raise AssetNotFoundError(f"unknown phase '{name}' (expected one of {', '.join(PHASES)})")
56
+ return _data_root().joinpath("phases", f"{name}.md").read_text(encoding="utf-8")
57
+
58
+
59
+ def iter_host_files(host):
60
+ """Yield `(relpath, text)` for every file this host installs.
61
+
62
+ `relpath` is relative to the host's directory and always uses forward
63
+ slashes, because callers join it onto a target directory to write the file
64
+ out. Raises for an unknown host instead of yielding nothing: a `cg setup`
65
+ that installs zero files and exits 0 looks exactly like success.
66
+ """
67
+ if host not in HOSTS:
68
+ raise AssetNotFoundError(f"unknown host '{host}' (expected one of {', '.join(HOSTS)})")
69
+
70
+ root = _data_root().joinpath("hosts", host)
71
+
72
+ def walk(node, prefix):
73
+ # Sorted so the file order — and therefore any output listing what was
74
+ # installed — is stable across platforms and filesystems.
75
+ for child in sorted(node.iterdir(), key=lambda c: c.name):
76
+ relpath = f"{prefix}{child.name}"
77
+ if child.is_dir():
78
+ yield from walk(child, f"{relpath}/")
79
+ else:
80
+ yield relpath, child.read_text(encoding="utf-8")
81
+
82
+ yield from walk(root, "")
83
+
84
+
85
+ def read_snippet(host, name):
86
+ """Return the text of `_data/hosts/<host>/<name>.snippet.json`."""
87
+ if host not in HOSTS:
88
+ raise AssetNotFoundError(f"unknown host '{host}' (expected one of {', '.join(HOSTS)})")
89
+ if name not in SNIPPETS[host]:
90
+ raise AssetNotFoundError(
91
+ f"host '{host}' has no '{name}' snippet "
92
+ f"(expected one of {', '.join(SNIPPETS[host])})")
93
+ return _data_root().joinpath(
94
+ "hosts", host, f"{name}.snippet.json").read_text(encoding="utf-8")