brigade-cli 0.5.0__tar.gz → 0.6.0__tar.gz
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.
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/PKG-INFO +27 -7
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/README.md +26 -6
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/pyproject.toml +1 -1
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/__init__.py +1 -1
- brigade_cli-0.6.0/src/brigade/add.py +38 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/cli.py +9 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/doctor.py +10 -0
- brigade_cli-0.6.0/src/brigade/managed.py +149 -0
- brigade_cli-0.6.0/src/brigade/proc.py +37 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/registry.py +10 -1
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/station.py +1 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade_cli.egg-info/PKG-INFO +27 -7
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade_cli.egg-info/SOURCES.txt +6 -0
- brigade_cli-0.6.0/tests/test_add.py +42 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/tests/test_doctor.py +29 -0
- brigade_cli-0.6.0/tests/test_managed.py +57 -0
- brigade_cli-0.6.0/tests/test_proc.py +22 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/tests/test_registry.py +10 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/LICENSE +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/MANIFEST.in +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/QUICKSTART.md +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/setup.cfg +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/__main__.py +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/config.py +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/fragments.py +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/handoff.py +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/ingest.py +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/install.py +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/prompt.py +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/py.typed +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/reconfigure.py +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/scrub.py +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/selection.py +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/status.py +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/claude/memory-handoffs/TEMPLATE.md +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/codex/memory-handoffs/TEMPLATE.md +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/depth/repo.json +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/depth/workspace.json +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/generic/harness-adapter-checklist.md +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/generic/memory-contract.md +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/harnesses/claude.json +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/harnesses/codex.json +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/harnesses/hermes.json +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/harnesses/openclaw.json +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/hermes/README.md +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/hermes/memory-handoff.harness.json +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/hermes/model-lanes.harness.json +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/hermes/workspace.harness.json +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/hooks/pre-push +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/includes/publisher.json +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/memory/cards/backup-restic.md +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/memory/cards/chat-surface-crawlers.md +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/memory/cards/content-safety.md +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/memory/cards/handoff-flow.md +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/memory/cards/memory-architecture.md +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/memory/cards/memory-care-staleness.md +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/memory/cards/memory-scanner.md +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/memory/cards/multi-workspace-handoff-admin.md +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/memory/cards/obsidian-notes.md +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/memory/cards/pipeline-standups.md +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/memory/cards/tokenjuice-output-compaction.md +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/openclaw/README.md +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/openclaw/acp-escalation.openclaw.json +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/openclaw/model-aliases.openclaw.json +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/openclaw/ollama-memory-search.openclaw.json +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/policies/public-content.json +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/policies/public-repo.json +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/scripts/backup-restic.sh +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/skills/note/SKILL.md +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/workspace/AGENTS.md +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/workspace/CLAUDE.md +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/workspace/HEARTBEAT.md +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/workspace/IDENTITY.md +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/workspace/INSTALL_FOR_AGENTS.md +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/workspace/MEMORY.md +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/workspace/SAFETY_RULES.md +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/workspace/SOUL.md +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/workspace/TOOLS.md +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/workspace/USER.md +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates.py +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade_cli.egg-info/dependency_links.txt +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade_cli.egg-info/entry_points.txt +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade_cli.egg-info/requires.txt +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade_cli.egg-info/top_level.txt +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/tests/test_cli_alias.py +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/tests/test_config.py +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/tests/test_fragments.py +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/tests/test_gitignore.py +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/tests/test_handoff.py +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/tests/test_ingest.py +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/tests/test_init.py +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/tests/test_install.py +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/tests/test_neutrality.py +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/tests/test_prompt.py +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/tests/test_reconfigure.py +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/tests/test_scrub.py +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/tests/test_selection.py +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/tests/test_station.py +0 -0
- {brigade_cli-0.5.0 → brigade_cli-0.6.0}/tests/test_status.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: brigade-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Run your agent brigade: an operator-system CLI that bootstraps, checks, and operates agent workspaces across harnesses.
|
|
5
5
|
Author-email: Solomon Neas <srneas@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -22,13 +22,13 @@ Requires-Dist: pytest>=7; extra == "dev"
|
|
|
22
22
|
Dynamic: license-file
|
|
23
23
|
|
|
24
24
|
<p align="center">
|
|
25
|
-
<img src="docs/assets/
|
|
25
|
+
<img src="docs/assets/brigade-social-preview.png" alt="Brigade">
|
|
26
26
|
</p>
|
|
27
27
|
|
|
28
|
-
<h1 align="center">
|
|
28
|
+
<h1 align="center">Brigade</h1>
|
|
29
29
|
|
|
30
30
|
<p align="center">
|
|
31
|
-
<strong>
|
|
31
|
+
<strong>Run your agent brigade.</strong>
|
|
32
32
|
</p>
|
|
33
33
|
|
|
34
34
|
<p align="center">
|
|
@@ -43,8 +43,7 @@ Dynamic: license-file
|
|
|
43
43
|
</p>
|
|
44
44
|
|
|
45
45
|
<p align="center">
|
|
46
|
-
<code>brigade</code> is the
|
|
47
|
-
It gives you the workspace skeleton, handoff inbox, conservative ingester, and publish guard that make a multi-agent setup usable without leaking private junk into public repos.
|
|
46
|
+
<code>brigade</code> is the operator-system CLI for agent workspaces. It gives you the workspace skeleton, handoff inbox, conservative ingester, and publish guards that make a multi-agent setup usable without leaking private junk into public repos.
|
|
48
47
|
</p>
|
|
49
48
|
|
|
50
49
|
## What this is
|
|
@@ -148,6 +147,27 @@ Re-running `brigade init` against an existing target is safe. It refuses to over
|
|
|
148
147
|
|
|
149
148
|
See [QUICKSTART.md](QUICKSTART.md) for setup, verification, and the ingest flow.
|
|
150
149
|
|
|
150
|
+
## Managed stations
|
|
151
|
+
|
|
152
|
+
Some stations can install and wire external tools for you. Run `brigade add <station>` to install any tool attached to that station that is not already on your PATH, then wire its default config. Tools are never imported in process; Brigade shells out to each CLI, so the boundary stays model-neutral and mixed-language.
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
brigade add memory # memory-doctor + bootstrap-doctor
|
|
156
|
+
brigade add guard # content-guard
|
|
157
|
+
brigade add tokens # tokenjuice
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
The four managed tools:
|
|
161
|
+
|
|
162
|
+
| Station | Tool | What it does |
|
|
163
|
+
|---|---|---|
|
|
164
|
+
| `memory` | `memory-doctor` | memory index health, dead-link lint, handoff counts |
|
|
165
|
+
| `memory` | `bootstrap-doctor` | bootstrap-file size and limit audit |
|
|
166
|
+
| `guard` | `content-guard` | policy-driven content scanning |
|
|
167
|
+
| `tokens` | `tokenjuice` | output compaction via host hooks |
|
|
168
|
+
|
|
169
|
+
`brigade doctor` folds installed tools into its report and surfaces each tool's own health. A tool that is not installed is never a failure: it shows up as a non-failing `[todo]` hint telling you to run `brigade add <station>`. That keeps doctor green on a bare host while still pointing you at what is available to add.
|
|
170
|
+
|
|
151
171
|
### What a green doctor looks like
|
|
152
172
|
|
|
153
173
|
```text
|
|
@@ -202,7 +222,7 @@ Token-heavy terminal work gets the same treatment: make the wrapper explicit, ma
|
|
|
202
222
|
|
|
203
223
|
## Related
|
|
204
224
|
|
|
205
|
-
- [
|
|
225
|
+
- [Cookbook](https://github.com/solomonneas/solos-cookbook): the long-form companion guide and reference docs
|
|
206
226
|
- [content-guard](https://github.com/solomonneas/content-guard): the publish-gate scanner used by the pre-push hook
|
|
207
227
|
- [OpenClaw](https://github.com/openclaw/openclaw): the reference memory owner
|
|
208
228
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="docs/assets/
|
|
2
|
+
<img src="docs/assets/brigade-social-preview.png" alt="Brigade">
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
|
-
<h1 align="center">
|
|
5
|
+
<h1 align="center">Brigade</h1>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
|
-
<strong>
|
|
8
|
+
<strong>Run your agent brigade.</strong>
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
@@ -20,8 +20,7 @@
|
|
|
20
20
|
</p>
|
|
21
21
|
|
|
22
22
|
<p align="center">
|
|
23
|
-
<code>brigade</code> is the
|
|
24
|
-
It gives you the workspace skeleton, handoff inbox, conservative ingester, and publish guard that make a multi-agent setup usable without leaking private junk into public repos.
|
|
23
|
+
<code>brigade</code> is the operator-system CLI for agent workspaces. It gives you the workspace skeleton, handoff inbox, conservative ingester, and publish guards that make a multi-agent setup usable without leaking private junk into public repos.
|
|
25
24
|
</p>
|
|
26
25
|
|
|
27
26
|
## What this is
|
|
@@ -125,6 +124,27 @@ Re-running `brigade init` against an existing target is safe. It refuses to over
|
|
|
125
124
|
|
|
126
125
|
See [QUICKSTART.md](QUICKSTART.md) for setup, verification, and the ingest flow.
|
|
127
126
|
|
|
127
|
+
## Managed stations
|
|
128
|
+
|
|
129
|
+
Some stations can install and wire external tools for you. Run `brigade add <station>` to install any tool attached to that station that is not already on your PATH, then wire its default config. Tools are never imported in process; Brigade shells out to each CLI, so the boundary stays model-neutral and mixed-language.
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
brigade add memory # memory-doctor + bootstrap-doctor
|
|
133
|
+
brigade add guard # content-guard
|
|
134
|
+
brigade add tokens # tokenjuice
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
The four managed tools:
|
|
138
|
+
|
|
139
|
+
| Station | Tool | What it does |
|
|
140
|
+
|---|---|---|
|
|
141
|
+
| `memory` | `memory-doctor` | memory index health, dead-link lint, handoff counts |
|
|
142
|
+
| `memory` | `bootstrap-doctor` | bootstrap-file size and limit audit |
|
|
143
|
+
| `guard` | `content-guard` | policy-driven content scanning |
|
|
144
|
+
| `tokens` | `tokenjuice` | output compaction via host hooks |
|
|
145
|
+
|
|
146
|
+
`brigade doctor` folds installed tools into its report and surfaces each tool's own health. A tool that is not installed is never a failure: it shows up as a non-failing `[todo]` hint telling you to run `brigade add <station>`. That keeps doctor green on a bare host while still pointing you at what is available to add.
|
|
147
|
+
|
|
128
148
|
### What a green doctor looks like
|
|
129
149
|
|
|
130
150
|
```text
|
|
@@ -179,7 +199,7 @@ Token-heavy terminal work gets the same treatment: make the wrapper explicit, ma
|
|
|
179
199
|
|
|
180
200
|
## Related
|
|
181
201
|
|
|
182
|
-
- [
|
|
202
|
+
- [Cookbook](https://github.com/solomonneas/solos-cookbook): the long-form companion guide and reference docs
|
|
183
203
|
- [content-guard](https://github.com/solomonneas/content-guard): the publish-gate scanner used by the pre-push hook
|
|
184
204
|
- [OpenClaw](https://github.com/openclaw/openclaw): the reference memory owner
|
|
185
205
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "brigade-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.6.0"
|
|
8
8
|
description = "Run your agent brigade: an operator-system CLI that bootstraps, checks, and operates agent workspaces across harnesses."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""`brigade add <station>` - install and wire a station's managed tools."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from . import doctor as _doctor
|
|
8
|
+
from . import managed
|
|
9
|
+
from .registry import resolve as resolve_station
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def run(target: Path, station: str) -> int:
|
|
13
|
+
st = resolve_station(station)
|
|
14
|
+
if st is None:
|
|
15
|
+
print(f"error: unknown station {station!r}", file=sys.stderr)
|
|
16
|
+
return 2
|
|
17
|
+
|
|
18
|
+
tools = managed.for_station(st.name)
|
|
19
|
+
if not tools:
|
|
20
|
+
print(f"station {st.name!r} has no managed tools to add.")
|
|
21
|
+
return 0
|
|
22
|
+
|
|
23
|
+
ctx = _doctor.build_context(target)
|
|
24
|
+
rc = 0
|
|
25
|
+
for tool in tools:
|
|
26
|
+
if tool.detect():
|
|
27
|
+
print(f" [skip] {tool.name} already installed")
|
|
28
|
+
else:
|
|
29
|
+
print(f" [install] {tool.name}: {' '.join(tool.install_args)}")
|
|
30
|
+
r = managed.proc.run(tool.install_args, timeout=300)
|
|
31
|
+
if r.code != 0:
|
|
32
|
+
print(f" [fail] {tool.name} install exited {r.code}: {r.stderr.strip()[:120]}", file=sys.stderr)
|
|
33
|
+
rc = 1
|
|
34
|
+
continue
|
|
35
|
+
for status, name, detail in tool.wire(ctx):
|
|
36
|
+
print(f" [{status.lower()}] {name}: {detail}")
|
|
37
|
+
print(f"\nRun `brigade doctor --target {target}` to verify.")
|
|
38
|
+
return rc
|
|
@@ -76,6 +76,11 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
76
76
|
p_status = sub.add_parser("status", help="Show which stations are present and healthy.")
|
|
77
77
|
p_status.add_argument("--target", "-t", type=Path, default=Path("."))
|
|
78
78
|
|
|
79
|
+
# add
|
|
80
|
+
p_add = sub.add_parser("add", help="Install and wire a station's managed tools.")
|
|
81
|
+
p_add.add_argument("station", help="Station to add tools for (e.g. memory, guard, tokens).")
|
|
82
|
+
p_add.add_argument("--target", "-t", type=Path, default=Path("."))
|
|
83
|
+
|
|
79
84
|
# scrub
|
|
80
85
|
p_scrub = sub.add_parser("scrub", help="Run content-guard against a target.")
|
|
81
86
|
p_scrub.add_argument("--target", "-t", type=Path, default=Path("."))
|
|
@@ -191,6 +196,10 @@ def main(argv=None) -> int:
|
|
|
191
196
|
from . import status as status_mod
|
|
192
197
|
|
|
193
198
|
return status_mod.run(target=args.target)
|
|
199
|
+
if cmd == "add":
|
|
200
|
+
from . import add as add_mod
|
|
201
|
+
|
|
202
|
+
return add_mod.run(target=args.target, station=args.station)
|
|
194
203
|
if cmd == "scrub":
|
|
195
204
|
from . import scrub as scrub_mod
|
|
196
205
|
|
|
@@ -59,8 +59,13 @@ def guard_station_checks(ctx: DoctorContext) -> List[CheckResult]:
|
|
|
59
59
|
return _check_publish_gate(ctx.target)
|
|
60
60
|
|
|
61
61
|
|
|
62
|
+
def tokens_station_checks(ctx: DoctorContext) -> List[CheckResult]:
|
|
63
|
+
return []
|
|
64
|
+
|
|
65
|
+
|
|
62
66
|
def run(target: Path, harness: str = "generic") -> int:
|
|
63
67
|
from .registry import all_stations
|
|
68
|
+
from . import managed
|
|
64
69
|
|
|
65
70
|
ctx = build_context(target, harness)
|
|
66
71
|
print(f"brigade doctor: target {ctx.target}")
|
|
@@ -79,6 +84,11 @@ def run(target: Path, harness: str = "generic") -> int:
|
|
|
79
84
|
for station in all_stations():
|
|
80
85
|
if station.doctor is not None:
|
|
81
86
|
checks.extend(station.doctor(ctx))
|
|
87
|
+
for tool in managed.for_station(station.name):
|
|
88
|
+
if tool.detect():
|
|
89
|
+
checks.extend(tool.doctor(ctx))
|
|
90
|
+
else:
|
|
91
|
+
checks.append((MANUAL, f"{station.name}: {tool.name}", f"not installed; run `brigade add {station.name}`"))
|
|
82
92
|
return _report(checks)
|
|
83
93
|
|
|
84
94
|
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Managed tools: external CLIs Brigade can install, wire, and health-check.
|
|
2
|
+
|
|
3
|
+
Each tool attaches to a station. The core never imports these tools; it shells
|
|
4
|
+
out via brigade.proc. Absent tools are reported as MANUAL (a hint to install),
|
|
5
|
+
never as a hard failure.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Callable, List, Optional, Tuple
|
|
12
|
+
|
|
13
|
+
from . import proc
|
|
14
|
+
from .doctor import OK, WARN, FAIL, MANUAL
|
|
15
|
+
from .station import CheckResult, DoctorContext
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class ManagedTool:
|
|
20
|
+
name: str # e.g. "memory-doctor"
|
|
21
|
+
station: str # "memory" | "guard" | "tokens"
|
|
22
|
+
command: str # the binary name to detect on PATH
|
|
23
|
+
summary: str
|
|
24
|
+
install_args: List[str] # argv to install (pipx/npm/pip)
|
|
25
|
+
wire: Callable[[DoctorContext], List[CheckResult]] # lay config; returns notes
|
|
26
|
+
doctor: Callable[[DoctorContext], List[CheckResult]] # health via proc
|
|
27
|
+
|
|
28
|
+
def detect(self) -> bool:
|
|
29
|
+
return proc.which(self.command) is not None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# ---- adapters -------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
def _noop_wire(ctx: DoctorContext) -> List[CheckResult]:
|
|
35
|
+
return []
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# memory-doctor and bootstrap-doctor inspect the operator's canonical memory and
|
|
39
|
+
# bootstrap files (host-global), not a per-target workspace, so their findings are
|
|
40
|
+
# advisory: labeled operator-scoped and never FAIL a workspace doctor run.
|
|
41
|
+
def _memory_doctor_doctor(ctx: DoctorContext) -> List[CheckResult]:
|
|
42
|
+
name = "memory-doctor (operator memory)"
|
|
43
|
+
r = proc.run(["memory-doctor", "status", "--json"])
|
|
44
|
+
if r.code == 2:
|
|
45
|
+
return [(WARN, name, "installed but unwired (memory/handoffs dir missing)")]
|
|
46
|
+
data = r.json()
|
|
47
|
+
if data is None:
|
|
48
|
+
return [(WARN, name, f"unexpected output (exit {r.code})")]
|
|
49
|
+
dead = data.get("dead_links", 0)
|
|
50
|
+
status = WARN if dead else OK
|
|
51
|
+
return [(status, name, f"cards={data.get('cards')}, dead_links={dead}, pending={data.get('pending_handoffs')}")]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _bootstrap_doctor_doctor(ctx: DoctorContext) -> List[CheckResult]:
|
|
55
|
+
name = "bootstrap-doctor (operator files)"
|
|
56
|
+
r = proc.run(["bootstrap-doctor", "status", "--json"])
|
|
57
|
+
data = r.json()
|
|
58
|
+
if data is None:
|
|
59
|
+
return [(WARN, name, f"installed but unwired or errored (exit {r.code})")]
|
|
60
|
+
rows = data.get("rows", [])
|
|
61
|
+
bad = [row for row in rows if row.get("severity") in ("hard", "missing", "unreadable")]
|
|
62
|
+
soft = [row for row in rows if row.get("severity") == "soft"]
|
|
63
|
+
if bad:
|
|
64
|
+
return [(WARN, name, f"{len(bad)} file(s) over hard limit / missing (advisory)")]
|
|
65
|
+
if soft:
|
|
66
|
+
return [(WARN, name, f"{len(soft)} file(s) in soft band")]
|
|
67
|
+
return [(OK, name, f"{len(rows)} bootstrap file(s) within limits")]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _content_guard_doctor(ctx: DoctorContext) -> List[CheckResult]:
|
|
71
|
+
# A "tool present + policy loads" check: scan this plan's own clean string.
|
|
72
|
+
r = proc.run(["content-guard", "scan", "--policy", "public-repo", "--json"], env=None)
|
|
73
|
+
data = r.json()
|
|
74
|
+
if data is None and r.code not in (0, 1):
|
|
75
|
+
return [(WARN, "content-guard", f"installed but not runnable (exit {r.code})")]
|
|
76
|
+
return [(OK, "content-guard", "installed; public-repo policy loads")]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _content_guard_wire(ctx: DoctorContext) -> List[CheckResult]:
|
|
80
|
+
# content-guard ships bundled policies; nothing to lay down for the default.
|
|
81
|
+
return [(OK, "content-guard: policy", "using bundled public-repo policy")]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _tokenjuice_doctor(ctx: DoctorContext) -> List[CheckResult]:
|
|
85
|
+
r = proc.run(["tokenjuice", "doctor", "hooks", "--format", "json"])
|
|
86
|
+
data = r.json()
|
|
87
|
+
if data is None:
|
|
88
|
+
return [(WARN, "tokenjuice", f"installed but doctor output unreadable (exit {r.code})")]
|
|
89
|
+
status = data.get("status", "unknown")
|
|
90
|
+
mapping = {"ok": OK, "warn": WARN, "disabled": MANUAL, "broken": FAIL}
|
|
91
|
+
return [(mapping.get(status, WARN), "tokenjuice", f"hook status: {status}")]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _tokenjuice_wire(ctx: DoctorContext) -> List[CheckResult]:
|
|
95
|
+
# Wiring installs a host hook; which host depends on the workspace's harnesses.
|
|
96
|
+
hosts = [h for h in ctx.harnesses if h in ("claude", "codex", "cursor")]
|
|
97
|
+
if not hosts:
|
|
98
|
+
return [(MANUAL, "tokenjuice: wire", "no hookable harness selected; run `tokenjuice install <host>` manually")]
|
|
99
|
+
notes: List[CheckResult] = []
|
|
100
|
+
for h in hosts:
|
|
101
|
+
host = "claude-code" if h == "claude" else h
|
|
102
|
+
r = proc.run(["tokenjuice", "install", host])
|
|
103
|
+
notes.append((OK if r.code == 0 else WARN, f"tokenjuice: install {host}", r.stderr.strip()[:80] or "installed"))
|
|
104
|
+
return notes
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# ---- registry -------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
_TOOLS: Tuple[ManagedTool, ...] = (
|
|
110
|
+
ManagedTool(
|
|
111
|
+
name="memory-doctor", station="memory", command="memory-doctor",
|
|
112
|
+
summary="memory index health, dead-link lint, handoff counts",
|
|
113
|
+
install_args=["pipx", "install", "git+https://github.com/solomonneas/memory-doctor"],
|
|
114
|
+
wire=_noop_wire, doctor=_memory_doctor_doctor,
|
|
115
|
+
),
|
|
116
|
+
ManagedTool(
|
|
117
|
+
name="bootstrap-doctor", station="memory", command="bootstrap-doctor",
|
|
118
|
+
summary="bootstrap-file size/limit audit",
|
|
119
|
+
install_args=["pipx", "install", "git+https://github.com/solomonneas/bootstrap-doctor"],
|
|
120
|
+
wire=_noop_wire, doctor=_bootstrap_doctor_doctor,
|
|
121
|
+
),
|
|
122
|
+
ManagedTool(
|
|
123
|
+
name="content-guard", station="guard", command="content-guard",
|
|
124
|
+
summary="policy-driven content scanning",
|
|
125
|
+
install_args=["pipx", "install", "git+https://github.com/solomonneas/content-guard"],
|
|
126
|
+
wire=_content_guard_wire, doctor=_content_guard_doctor,
|
|
127
|
+
),
|
|
128
|
+
ManagedTool(
|
|
129
|
+
name="tokenjuice", station="tokens", command="tokenjuice",
|
|
130
|
+
summary="output compaction via host hooks",
|
|
131
|
+
install_args=["npm", "install", "-g", "tokenjuice"],
|
|
132
|
+
wire=_tokenjuice_wire, doctor=_tokenjuice_doctor,
|
|
133
|
+
),
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def all_tools() -> Tuple[ManagedTool, ...]:
|
|
138
|
+
return _TOOLS
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def for_station(station: str) -> Tuple[ManagedTool, ...]:
|
|
142
|
+
return tuple(t for t in _TOOLS if t.station == station)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def resolve(name: str) -> Optional[ManagedTool]:
|
|
146
|
+
for t in _TOOLS:
|
|
147
|
+
if t.name == name:
|
|
148
|
+
return t
|
|
149
|
+
return None
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Run external tool CLIs and capture their results. No tool is imported in-process."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import List, Optional
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class Result:
|
|
13
|
+
code: int
|
|
14
|
+
stdout: str
|
|
15
|
+
stderr: str
|
|
16
|
+
|
|
17
|
+
def json(self) -> Optional[object]:
|
|
18
|
+
try:
|
|
19
|
+
return json.loads(self.stdout)
|
|
20
|
+
except (json.JSONDecodeError, ValueError):
|
|
21
|
+
return None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def which(cmd: str) -> Optional[str]:
|
|
25
|
+
return shutil.which(cmd)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def run(args: List[str], timeout: float = 30.0, env: Optional[dict] = None) -> Result:
|
|
29
|
+
try:
|
|
30
|
+
cp = subprocess.run(
|
|
31
|
+
args, capture_output=True, text=True, timeout=timeout, env=env, check=False
|
|
32
|
+
)
|
|
33
|
+
return Result(code=cp.returncode, stdout=cp.stdout, stderr=cp.stderr)
|
|
34
|
+
except FileNotFoundError:
|
|
35
|
+
return Result(code=127, stdout="", stderr=f"command not found: {args[0]}")
|
|
36
|
+
except subprocess.TimeoutExpired:
|
|
37
|
+
return Result(code=124, stdout="", stderr=f"timeout after {timeout}s")
|
|
@@ -17,15 +17,24 @@ MEMORY = Station(
|
|
|
17
17
|
summary="handoff inbox, ingest, and memory-care",
|
|
18
18
|
aliases=("garde",),
|
|
19
19
|
doctor=_doctor.memory_station_checks,
|
|
20
|
+
tools=("memory-doctor", "bootstrap-doctor"),
|
|
20
21
|
)
|
|
21
22
|
GUARD = Station(
|
|
22
23
|
name="guard",
|
|
23
24
|
summary="publish safety and content scrub",
|
|
24
25
|
aliases=("pass",),
|
|
25
26
|
doctor=_doctor.guard_station_checks,
|
|
27
|
+
tools=("content-guard",),
|
|
28
|
+
)
|
|
29
|
+
TOKENS = Station(
|
|
30
|
+
name="tokens",
|
|
31
|
+
summary="output compaction",
|
|
32
|
+
aliases=(),
|
|
33
|
+
doctor=_doctor.tokens_station_checks,
|
|
34
|
+
tools=("tokenjuice",),
|
|
26
35
|
)
|
|
27
36
|
|
|
28
|
-
_BUILTIN: Tuple[Station, ...] = (CORE, MEMORY, GUARD)
|
|
37
|
+
_BUILTIN: Tuple[Station, ...] = (CORE, MEMORY, GUARD, TOKENS)
|
|
29
38
|
|
|
30
39
|
|
|
31
40
|
def all_stations() -> Tuple[Station, ...]:
|
|
@@ -31,6 +31,7 @@ class Station:
|
|
|
31
31
|
doctor: Optional[Callable[[DoctorContext], List[CheckResult]]]
|
|
32
32
|
aliases: Tuple[str, ...] = ()
|
|
33
33
|
kind: str = "builtin"
|
|
34
|
+
tools: Tuple[str, ...] = ()
|
|
34
35
|
|
|
35
36
|
def matches(self, name_or_alias: str) -> bool:
|
|
36
37
|
return name_or_alias == self.name or name_or_alias in self.aliases
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: brigade-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Run your agent brigade: an operator-system CLI that bootstraps, checks, and operates agent workspaces across harnesses.
|
|
5
5
|
Author-email: Solomon Neas <srneas@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -22,13 +22,13 @@ Requires-Dist: pytest>=7; extra == "dev"
|
|
|
22
22
|
Dynamic: license-file
|
|
23
23
|
|
|
24
24
|
<p align="center">
|
|
25
|
-
<img src="docs/assets/
|
|
25
|
+
<img src="docs/assets/brigade-social-preview.png" alt="Brigade">
|
|
26
26
|
</p>
|
|
27
27
|
|
|
28
|
-
<h1 align="center">
|
|
28
|
+
<h1 align="center">Brigade</h1>
|
|
29
29
|
|
|
30
30
|
<p align="center">
|
|
31
|
-
<strong>
|
|
31
|
+
<strong>Run your agent brigade.</strong>
|
|
32
32
|
</p>
|
|
33
33
|
|
|
34
34
|
<p align="center">
|
|
@@ -43,8 +43,7 @@ Dynamic: license-file
|
|
|
43
43
|
</p>
|
|
44
44
|
|
|
45
45
|
<p align="center">
|
|
46
|
-
<code>brigade</code> is the
|
|
47
|
-
It gives you the workspace skeleton, handoff inbox, conservative ingester, and publish guard that make a multi-agent setup usable without leaking private junk into public repos.
|
|
46
|
+
<code>brigade</code> is the operator-system CLI for agent workspaces. It gives you the workspace skeleton, handoff inbox, conservative ingester, and publish guards that make a multi-agent setup usable without leaking private junk into public repos.
|
|
48
47
|
</p>
|
|
49
48
|
|
|
50
49
|
## What this is
|
|
@@ -148,6 +147,27 @@ Re-running `brigade init` against an existing target is safe. It refuses to over
|
|
|
148
147
|
|
|
149
148
|
See [QUICKSTART.md](QUICKSTART.md) for setup, verification, and the ingest flow.
|
|
150
149
|
|
|
150
|
+
## Managed stations
|
|
151
|
+
|
|
152
|
+
Some stations can install and wire external tools for you. Run `brigade add <station>` to install any tool attached to that station that is not already on your PATH, then wire its default config. Tools are never imported in process; Brigade shells out to each CLI, so the boundary stays model-neutral and mixed-language.
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
brigade add memory # memory-doctor + bootstrap-doctor
|
|
156
|
+
brigade add guard # content-guard
|
|
157
|
+
brigade add tokens # tokenjuice
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
The four managed tools:
|
|
161
|
+
|
|
162
|
+
| Station | Tool | What it does |
|
|
163
|
+
|---|---|---|
|
|
164
|
+
| `memory` | `memory-doctor` | memory index health, dead-link lint, handoff counts |
|
|
165
|
+
| `memory` | `bootstrap-doctor` | bootstrap-file size and limit audit |
|
|
166
|
+
| `guard` | `content-guard` | policy-driven content scanning |
|
|
167
|
+
| `tokens` | `tokenjuice` | output compaction via host hooks |
|
|
168
|
+
|
|
169
|
+
`brigade doctor` folds installed tools into its report and surfaces each tool's own health. A tool that is not installed is never a failure: it shows up as a non-failing `[todo]` hint telling you to run `brigade add <station>`. That keeps doctor green on a bare host while still pointing you at what is available to add.
|
|
170
|
+
|
|
151
171
|
### What a green doctor looks like
|
|
152
172
|
|
|
153
173
|
```text
|
|
@@ -202,7 +222,7 @@ Token-heavy terminal work gets the same treatment: make the wrapper explicit, ma
|
|
|
202
222
|
|
|
203
223
|
## Related
|
|
204
224
|
|
|
205
|
-
- [
|
|
225
|
+
- [Cookbook](https://github.com/solomonneas/solos-cookbook): the long-form companion guide and reference docs
|
|
206
226
|
- [content-guard](https://github.com/solomonneas/content-guard): the publish-gate scanner used by the pre-push hook
|
|
207
227
|
- [OpenClaw](https://github.com/openclaw/openclaw): the reference memory owner
|
|
208
228
|
|
|
@@ -5,6 +5,7 @@ README.md
|
|
|
5
5
|
pyproject.toml
|
|
6
6
|
src/brigade/__init__.py
|
|
7
7
|
src/brigade/__main__.py
|
|
8
|
+
src/brigade/add.py
|
|
8
9
|
src/brigade/cli.py
|
|
9
10
|
src/brigade/config.py
|
|
10
11
|
src/brigade/doctor.py
|
|
@@ -12,6 +13,8 @@ src/brigade/fragments.py
|
|
|
12
13
|
src/brigade/handoff.py
|
|
13
14
|
src/brigade/ingest.py
|
|
14
15
|
src/brigade/install.py
|
|
16
|
+
src/brigade/managed.py
|
|
17
|
+
src/brigade/proc.py
|
|
15
18
|
src/brigade/prompt.py
|
|
16
19
|
src/brigade/py.typed
|
|
17
20
|
src/brigade/reconfigure.py
|
|
@@ -72,6 +75,7 @@ src/brigade_cli.egg-info/dependency_links.txt
|
|
|
72
75
|
src/brigade_cli.egg-info/entry_points.txt
|
|
73
76
|
src/brigade_cli.egg-info/requires.txt
|
|
74
77
|
src/brigade_cli.egg-info/top_level.txt
|
|
78
|
+
tests/test_add.py
|
|
75
79
|
tests/test_cli_alias.py
|
|
76
80
|
tests/test_config.py
|
|
77
81
|
tests/test_doctor.py
|
|
@@ -81,7 +85,9 @@ tests/test_handoff.py
|
|
|
81
85
|
tests/test_ingest.py
|
|
82
86
|
tests/test_init.py
|
|
83
87
|
tests/test_install.py
|
|
88
|
+
tests/test_managed.py
|
|
84
89
|
tests/test_neutrality.py
|
|
90
|
+
tests/test_proc.py
|
|
85
91
|
tests/test_prompt.py
|
|
86
92
|
tests/test_reconfigure.py
|
|
87
93
|
tests/test_registry.py
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# tests/test_add.py
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from brigade import add as add_mod
|
|
5
|
+
from brigade import managed
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_add_installs_and_wires_station_tools(monkeypatch, tmp_target, capsys):
|
|
9
|
+
calls = []
|
|
10
|
+
monkeypatch.setattr(managed.proc, "which", lambda c: None) # not yet installed
|
|
11
|
+
|
|
12
|
+
def fake_run(args, **kw):
|
|
13
|
+
calls.append(args)
|
|
14
|
+
return managed.proc.Result(0, "", "")
|
|
15
|
+
|
|
16
|
+
monkeypatch.setattr(managed.proc, "run", fake_run)
|
|
17
|
+
rc = add_mod.run(target=tmp_target, station="guard")
|
|
18
|
+
out = capsys.readouterr().out
|
|
19
|
+
assert rc == 0
|
|
20
|
+
# content-guard install args were invoked
|
|
21
|
+
assert any("content-guard" in " ".join(a) for a in calls)
|
|
22
|
+
assert "content-guard" in out
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_add_unknown_station_errors(tmp_target, capsys):
|
|
26
|
+
rc = add_mod.run(target=tmp_target, station="nope")
|
|
27
|
+
assert rc == 2
|
|
28
|
+
assert "unknown station" in capsys.readouterr().err.lower()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_add_skips_install_when_already_present(monkeypatch, tmp_target):
|
|
32
|
+
calls = []
|
|
33
|
+
monkeypatch.setattr(managed.proc, "which", lambda c: "/x/" + c) # already installed
|
|
34
|
+
|
|
35
|
+
def fake_run(args, **kw):
|
|
36
|
+
calls.append(args)
|
|
37
|
+
return managed.proc.Result(0, "", "")
|
|
38
|
+
|
|
39
|
+
monkeypatch.setattr(managed.proc, "run", fake_run)
|
|
40
|
+
add_mod.run(target=tmp_target, station="guard")
|
|
41
|
+
# no install argv (pipx/npm) should have run, only wire
|
|
42
|
+
assert not any(a[:1] in (["pipx"], ["npm"], ["pip"]) for a in calls)
|
|
@@ -210,3 +210,32 @@ def test_doctor_falls_back_to_v0_2_behavior_when_no_config(tmp_target: Path, cap
|
|
|
210
210
|
doctor_mod.run(tmp_target)
|
|
211
211
|
out = capsys.readouterr().out
|
|
212
212
|
assert "doctor" in out
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def test_doctor_includes_installed_managed_tool(monkeypatch, tmp_target, capsys):
|
|
216
|
+
from brigade.install import install_selection
|
|
217
|
+
from brigade.selection import Selection
|
|
218
|
+
from brigade import managed
|
|
219
|
+
install_selection(tmp_target, Selection(depth="workspace", harnesses=["claude"], owner="claude", includes=[]))
|
|
220
|
+
|
|
221
|
+
# Pretend content-guard is installed and healthy.
|
|
222
|
+
monkeypatch.setattr(managed.proc, "which", lambda c: "/x/" + c if c == "content-guard" else None)
|
|
223
|
+
monkeypatch.setattr(managed.proc, "run", lambda args, **kw: managed.proc.Result(0, '{"ok": true}', ""))
|
|
224
|
+
|
|
225
|
+
doctor_mod.run(target=tmp_target, harness="generic")
|
|
226
|
+
out = capsys.readouterr().out
|
|
227
|
+
assert "content-guard" in out
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def test_doctor_reports_absent_tool_as_manual(monkeypatch, tmp_target, capsys):
|
|
231
|
+
from brigade.install import install_selection
|
|
232
|
+
from brigade.selection import Selection
|
|
233
|
+
from brigade import managed
|
|
234
|
+
install_selection(tmp_target, Selection(depth="workspace", harnesses=["claude"], owner="claude", includes=[]))
|
|
235
|
+
monkeypatch.setattr(managed.proc, "which", lambda c: None) # nothing installed
|
|
236
|
+
|
|
237
|
+
rc = doctor_mod.run(target=tmp_target, harness="generic")
|
|
238
|
+
out = capsys.readouterr().out
|
|
239
|
+
# absent managed tools must not fail the run
|
|
240
|
+
assert rc == 0
|
|
241
|
+
assert "not installed" in out
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from brigade import managed
|
|
4
|
+
from brigade.station import DoctorContext
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_all_tools_declare_required_fields():
|
|
8
|
+
for t in managed.all_tools():
|
|
9
|
+
assert t.name and t.station and t.command
|
|
10
|
+
assert callable(t.doctor)
|
|
11
|
+
assert callable(t.wire)
|
|
12
|
+
assert isinstance(t.install_args, list) and t.install_args
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_tools_attach_to_known_stations():
|
|
16
|
+
stations = {t.station for t in managed.all_tools()}
|
|
17
|
+
assert stations <= {"memory", "guard", "tokens"}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_for_station_filters():
|
|
21
|
+
names = {t.name for t in managed.for_station("memory")}
|
|
22
|
+
assert names == {"memory-doctor", "bootstrap-doctor"}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_detect_uses_which(monkeypatch):
|
|
26
|
+
t = managed.resolve("content-guard")
|
|
27
|
+
monkeypatch.setattr(managed.proc, "which", lambda c: None)
|
|
28
|
+
assert t.detect() is False
|
|
29
|
+
monkeypatch.setattr(managed.proc, "which", lambda c: "/usr/bin/" + c)
|
|
30
|
+
assert t.detect() is True
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_memory_doctor_doctor_parses_status(monkeypatch):
|
|
34
|
+
t = managed.resolve("memory-doctor")
|
|
35
|
+
monkeypatch.setattr(managed.proc, "which", lambda c: "/x/" + c)
|
|
36
|
+
|
|
37
|
+
def fake_run(args, **kw):
|
|
38
|
+
return managed.proc.Result(code=0, stdout='{"cards": 4, "dead_links": 0, "pending_handoffs": 1}', stderr="")
|
|
39
|
+
|
|
40
|
+
monkeypatch.setattr(managed.proc, "run", fake_run)
|
|
41
|
+
ctx = DoctorContext(target=Path("/tmp/ws"), selection=None, harnesses=[])
|
|
42
|
+
results = t.doctor(ctx)
|
|
43
|
+
assert any(status == "OK" and "memory-doctor" in name for status, name, _ in results)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_tokenjuice_doctor_reads_status_field_not_exit(monkeypatch):
|
|
47
|
+
t = managed.resolve("tokenjuice")
|
|
48
|
+
monkeypatch.setattr(managed.proc, "which", lambda c: "/x/" + c)
|
|
49
|
+
|
|
50
|
+
def fake_run(args, **kw):
|
|
51
|
+
# exit 0 but status warn -> must surface as WARN, not OK
|
|
52
|
+
return managed.proc.Result(code=0, stdout='{"status": "warn", "integrations": {}}', stderr="")
|
|
53
|
+
|
|
54
|
+
monkeypatch.setattr(managed.proc, "run", fake_run)
|
|
55
|
+
ctx = DoctorContext(target=Path("/tmp/ws"), selection=None, harnesses=[])
|
|
56
|
+
results = t.doctor(ctx)
|
|
57
|
+
assert any(status == "WARN" for status, _, _ in results)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from brigade import proc
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_run_captures_exit_and_output():
|
|
5
|
+
r = proc.run(["python3", "-c", "import sys; print('hi'); sys.exit(3)"])
|
|
6
|
+
assert r.code == 3
|
|
7
|
+
assert r.stdout.strip() == "hi"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_run_json_parses_stdout():
|
|
11
|
+
r = proc.run(["python3", "-c", "print('{\"a\": 1}')"])
|
|
12
|
+
assert r.json() == {"a": 1}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_run_json_returns_none_on_nonjson():
|
|
16
|
+
r = proc.run(["python3", "-c", "print('not json')"])
|
|
17
|
+
assert r.json() is None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_which_detects_present_and_absent():
|
|
21
|
+
assert proc.which("python3") is not None
|
|
22
|
+
assert proc.which("definitely-not-a-real-binary-xyz") is None
|
|
@@ -16,3 +16,13 @@ def test_resolve_by_name_and_alias():
|
|
|
16
16
|
assert registry.resolve("garde").name == "memory"
|
|
17
17
|
assert registry.resolve("pass").name == "guard"
|
|
18
18
|
assert registry.resolve("nope") is None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_stations_declare_attached_tools():
|
|
22
|
+
from brigade import registry
|
|
23
|
+
memory = registry.resolve("memory")
|
|
24
|
+
guard = registry.resolve("guard")
|
|
25
|
+
tokens = registry.resolve("tokens")
|
|
26
|
+
assert set(memory.tools) == {"memory-doctor", "bootstrap-doctor"}
|
|
27
|
+
assert set(guard.tools) == {"content-guard"}
|
|
28
|
+
assert tokens is not None and set(tokens.tools) == {"tokenjuice"}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/claude/memory-handoffs/TEMPLATE.md
RENAMED
|
File without changes
|
{brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/codex/memory-handoffs/TEMPLATE.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/generic/harness-adapter-checklist.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/hermes/memory-handoff.harness.json
RENAMED
|
File without changes
|
{brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/hermes/model-lanes.harness.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/memory/cards/chat-surface-crawlers.md
RENAMED
|
File without changes
|
{brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/memory/cards/content-safety.md
RENAMED
|
File without changes
|
|
File without changes
|
{brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/memory/cards/memory-architecture.md
RENAMED
|
File without changes
|
{brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/memory/cards/memory-care-staleness.md
RENAMED
|
File without changes
|
{brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/memory/cards/memory-scanner.md
RENAMED
|
File without changes
|
|
File without changes
|
{brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/memory/cards/obsidian-notes.md
RENAMED
|
File without changes
|
{brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/memory/cards/pipeline-standups.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/openclaw/acp-escalation.openclaw.json
RENAMED
|
File without changes
|
{brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/openclaw/model-aliases.openclaw.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/workspace/INSTALL_FOR_AGENTS.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|