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.
Files changed (99) hide show
  1. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/PKG-INFO +27 -7
  2. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/README.md +26 -6
  3. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/pyproject.toml +1 -1
  4. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/__init__.py +1 -1
  5. brigade_cli-0.6.0/src/brigade/add.py +38 -0
  6. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/cli.py +9 -0
  7. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/doctor.py +10 -0
  8. brigade_cli-0.6.0/src/brigade/managed.py +149 -0
  9. brigade_cli-0.6.0/src/brigade/proc.py +37 -0
  10. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/registry.py +10 -1
  11. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/station.py +1 -0
  12. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade_cli.egg-info/PKG-INFO +27 -7
  13. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade_cli.egg-info/SOURCES.txt +6 -0
  14. brigade_cli-0.6.0/tests/test_add.py +42 -0
  15. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/tests/test_doctor.py +29 -0
  16. brigade_cli-0.6.0/tests/test_managed.py +57 -0
  17. brigade_cli-0.6.0/tests/test_proc.py +22 -0
  18. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/tests/test_registry.py +10 -0
  19. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/LICENSE +0 -0
  20. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/MANIFEST.in +0 -0
  21. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/QUICKSTART.md +0 -0
  22. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/setup.cfg +0 -0
  23. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/__main__.py +0 -0
  24. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/config.py +0 -0
  25. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/fragments.py +0 -0
  26. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/handoff.py +0 -0
  27. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/ingest.py +0 -0
  28. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/install.py +0 -0
  29. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/prompt.py +0 -0
  30. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/py.typed +0 -0
  31. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/reconfigure.py +0 -0
  32. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/scrub.py +0 -0
  33. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/selection.py +0 -0
  34. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/status.py +0 -0
  35. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/claude/memory-handoffs/TEMPLATE.md +0 -0
  36. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/codex/memory-handoffs/TEMPLATE.md +0 -0
  37. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/depth/repo.json +0 -0
  38. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/depth/workspace.json +0 -0
  39. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/generic/harness-adapter-checklist.md +0 -0
  40. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/generic/memory-contract.md +0 -0
  41. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/harnesses/claude.json +0 -0
  42. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/harnesses/codex.json +0 -0
  43. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/harnesses/hermes.json +0 -0
  44. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/harnesses/openclaw.json +0 -0
  45. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/hermes/README.md +0 -0
  46. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/hermes/memory-handoff.harness.json +0 -0
  47. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/hermes/model-lanes.harness.json +0 -0
  48. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/hermes/workspace.harness.json +0 -0
  49. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/hooks/pre-push +0 -0
  50. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/includes/publisher.json +0 -0
  51. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/memory/cards/backup-restic.md +0 -0
  52. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/memory/cards/chat-surface-crawlers.md +0 -0
  53. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/memory/cards/content-safety.md +0 -0
  54. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/memory/cards/handoff-flow.md +0 -0
  55. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/memory/cards/memory-architecture.md +0 -0
  56. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/memory/cards/memory-care-staleness.md +0 -0
  57. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/memory/cards/memory-scanner.md +0 -0
  58. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/memory/cards/multi-workspace-handoff-admin.md +0 -0
  59. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/memory/cards/obsidian-notes.md +0 -0
  60. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/memory/cards/pipeline-standups.md +0 -0
  61. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/memory/cards/tokenjuice-output-compaction.md +0 -0
  62. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/openclaw/README.md +0 -0
  63. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/openclaw/acp-escalation.openclaw.json +0 -0
  64. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/openclaw/model-aliases.openclaw.json +0 -0
  65. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/openclaw/ollama-memory-search.openclaw.json +0 -0
  66. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/policies/public-content.json +0 -0
  67. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/policies/public-repo.json +0 -0
  68. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/scripts/backup-restic.sh +0 -0
  69. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/skills/note/SKILL.md +0 -0
  70. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/workspace/AGENTS.md +0 -0
  71. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/workspace/CLAUDE.md +0 -0
  72. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/workspace/HEARTBEAT.md +0 -0
  73. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/workspace/IDENTITY.md +0 -0
  74. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/workspace/INSTALL_FOR_AGENTS.md +0 -0
  75. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/workspace/MEMORY.md +0 -0
  76. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/workspace/SAFETY_RULES.md +0 -0
  77. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/workspace/SOUL.md +0 -0
  78. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/workspace/TOOLS.md +0 -0
  79. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates/workspace/USER.md +0 -0
  80. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade/templates.py +0 -0
  81. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade_cli.egg-info/dependency_links.txt +0 -0
  82. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade_cli.egg-info/entry_points.txt +0 -0
  83. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade_cli.egg-info/requires.txt +0 -0
  84. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/src/brigade_cli.egg-info/top_level.txt +0 -0
  85. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/tests/test_cli_alias.py +0 -0
  86. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/tests/test_config.py +0 -0
  87. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/tests/test_fragments.py +0 -0
  88. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/tests/test_gitignore.py +0 -0
  89. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/tests/test_handoff.py +0 -0
  90. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/tests/test_ingest.py +0 -0
  91. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/tests/test_init.py +0 -0
  92. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/tests/test_install.py +0 -0
  93. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/tests/test_neutrality.py +0 -0
  94. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/tests/test_prompt.py +0 -0
  95. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/tests/test_reconfigure.py +0 -0
  96. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/tests/test_scrub.py +0 -0
  97. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/tests/test_selection.py +0 -0
  98. {brigade_cli-0.5.0 → brigade_cli-0.6.0}/tests/test_station.py +0 -0
  99. {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.5.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/solo-mise-banner.png" alt="Solomon's Mise en Place banner">
25
+ <img src="docs/assets/brigade-social-preview.png" alt="Brigade">
26
26
  </p>
27
27
 
28
- <h1 align="center">Solomon's Mise en Place</h1>
28
+ <h1 align="center">Brigade</h1>
29
29
 
30
30
  <p align="center">
31
- <strong>Mise en place for agent memory.</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 installable starter kit behind <a href="https://github.com/solomonneas/solos-cookbook">Solomon's Guide to Cookin' with Gas</a>.
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
- - [Solomon's Cookbook](https://github.com/solomonneas/solos-cookbook): the long-form guide and reference docs
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/solo-mise-banner.png" alt="Solomon's Mise en Place banner">
2
+ <img src="docs/assets/brigade-social-preview.png" alt="Brigade">
3
3
  </p>
4
4
 
5
- <h1 align="center">Solomon's Mise en Place</h1>
5
+ <h1 align="center">Brigade</h1>
6
6
 
7
7
  <p align="center">
8
- <strong>Mise en place for agent memory.</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 installable starter kit behind <a href="https://github.com/solomonneas/solos-cookbook">Solomon's Guide to Cookin' with Gas</a>.
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
- - [Solomon's Cookbook](https://github.com/solomonneas/solos-cookbook): the long-form guide and reference docs
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.5.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"
@@ -1,3 +1,3 @@
1
1
  """Solomon's Mise en Place: installable starter kit for an agent kitchen."""
2
2
 
3
- __version__ = "0.5.0"
3
+ __version__ = "0.6.0"
@@ -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.5.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/solo-mise-banner.png" alt="Solomon's Mise en Place banner">
25
+ <img src="docs/assets/brigade-social-preview.png" alt="Brigade">
26
26
  </p>
27
27
 
28
- <h1 align="center">Solomon's Mise en Place</h1>
28
+ <h1 align="center">Brigade</h1>
29
29
 
30
30
  <p align="center">
31
- <strong>Mise en place for agent memory.</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 installable starter kit behind <a href="https://github.com/solomonneas/solos-cookbook">Solomon's Guide to Cookin' with Gas</a>.
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
- - [Solomon's Cookbook](https://github.com/solomonneas/solos-cookbook): the long-form guide and reference docs
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