brigade-cli 0.5.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 (69) hide show
  1. brigade/__init__.py +3 -0
  2. brigade/__main__.py +5 -0
  3. brigade/cli.py +258 -0
  4. brigade/config.py +65 -0
  5. brigade/doctor.py +393 -0
  6. brigade/fragments.py +64 -0
  7. brigade/handoff.py +23 -0
  8. brigade/ingest.py +298 -0
  9. brigade/install.py +217 -0
  10. brigade/prompt.py +135 -0
  11. brigade/py.typed +0 -0
  12. brigade/reconfigure.py +64 -0
  13. brigade/registry.py +39 -0
  14. brigade/scrub.py +90 -0
  15. brigade/selection.py +66 -0
  16. brigade/station.py +36 -0
  17. brigade/status.py +24 -0
  18. brigade/templates/claude/memory-handoffs/TEMPLATE.md +57 -0
  19. brigade/templates/codex/memory-handoffs/TEMPLATE.md +57 -0
  20. brigade/templates/depth/repo.json +12 -0
  21. brigade/templates/depth/workspace.json +30 -0
  22. brigade/templates/generic/harness-adapter-checklist.md +55 -0
  23. brigade/templates/generic/memory-contract.md +41 -0
  24. brigade/templates/harnesses/claude.json +12 -0
  25. brigade/templates/harnesses/codex.json +11 -0
  26. brigade/templates/harnesses/hermes.json +16 -0
  27. brigade/templates/harnesses/openclaw.json +17 -0
  28. brigade/templates/hermes/README.md +25 -0
  29. brigade/templates/hermes/memory-handoff.harness.json +36 -0
  30. brigade/templates/hermes/model-lanes.harness.json +17 -0
  31. brigade/templates/hermes/workspace.harness.json +30 -0
  32. brigade/templates/hooks/pre-push +36 -0
  33. brigade/templates/includes/publisher.json +15 -0
  34. brigade/templates/memory/cards/backup-restic.md +126 -0
  35. brigade/templates/memory/cards/chat-surface-crawlers.md +103 -0
  36. brigade/templates/memory/cards/content-safety.md +54 -0
  37. brigade/templates/memory/cards/handoff-flow.md +70 -0
  38. brigade/templates/memory/cards/memory-architecture.md +56 -0
  39. brigade/templates/memory/cards/memory-care-staleness.md +58 -0
  40. brigade/templates/memory/cards/memory-scanner.md +98 -0
  41. brigade/templates/memory/cards/multi-workspace-handoff-admin.md +63 -0
  42. brigade/templates/memory/cards/obsidian-notes.md +82 -0
  43. brigade/templates/memory/cards/pipeline-standups.md +88 -0
  44. brigade/templates/memory/cards/tokenjuice-output-compaction.md +106 -0
  45. brigade/templates/openclaw/README.md +40 -0
  46. brigade/templates/openclaw/acp-escalation.openclaw.json +33 -0
  47. brigade/templates/openclaw/model-aliases.openclaw.json +21 -0
  48. brigade/templates/openclaw/ollama-memory-search.openclaw.json +24 -0
  49. brigade/templates/policies/public-content.json +28 -0
  50. brigade/templates/policies/public-repo.json +27 -0
  51. brigade/templates/scripts/backup-restic.sh +156 -0
  52. brigade/templates/skills/note/SKILL.md +173 -0
  53. brigade/templates/workspace/AGENTS.md +146 -0
  54. brigade/templates/workspace/CLAUDE.md +48 -0
  55. brigade/templates/workspace/HEARTBEAT.md +41 -0
  56. brigade/templates/workspace/IDENTITY.md +27 -0
  57. brigade/templates/workspace/INSTALL_FOR_AGENTS.md +61 -0
  58. brigade/templates/workspace/MEMORY.md +102 -0
  59. brigade/templates/workspace/SAFETY_RULES.md +164 -0
  60. brigade/templates/workspace/SOUL.md +92 -0
  61. brigade/templates/workspace/TOOLS.md +116 -0
  62. brigade/templates/workspace/USER.md +88 -0
  63. brigade/templates.py +88 -0
  64. brigade_cli-0.5.0.dist-info/METADATA +211 -0
  65. brigade_cli-0.5.0.dist-info/RECORD +69 -0
  66. brigade_cli-0.5.0.dist-info/WHEEL +5 -0
  67. brigade_cli-0.5.0.dist-info/entry_points.txt +3 -0
  68. brigade_cli-0.5.0.dist-info/licenses/LICENSE +21 -0
  69. brigade_cli-0.5.0.dist-info/top_level.txt +1 -0
brigade/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Solomon's Mise en Place: installable starter kit for an agent kitchen."""
2
+
3
+ __version__ = "0.5.0"
brigade/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Enable `python -m brigade`."""
2
+ from .cli import main
3
+
4
+ if __name__ == "__main__":
5
+ raise SystemExit(main())
brigade/cli.py ADDED
@@ -0,0 +1,258 @@
1
+ """brigade command-line entrypoint."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ from . import __version__
9
+ from .prompt import prompt_for_selection # imported here so tests can monkeypatch cli.prompt_for_selection
10
+
11
+
12
+ def _build_parser() -> argparse.ArgumentParser:
13
+ parser = argparse.ArgumentParser(
14
+ prog="brigade",
15
+ description="Brigade: run your agent brigade. Operator-system CLI for agent workspaces.",
16
+ )
17
+ parser.add_argument(
18
+ "--version", action="version", version=f"brigade {__version__}"
19
+ )
20
+ sub = parser.add_subparsers(dest="command", metavar="<command>")
21
+ sub.required = True
22
+
23
+ # init
24
+ p_init = sub.add_parser("init", help="Materialize a selection into a target directory.")
25
+ p_init.add_argument("--target", "-t", type=Path, default=Path("."), help="Where to install.")
26
+ p_init.add_argument("--force", action="store_true", help="Overwrite existing files.")
27
+ p_init.add_argument(
28
+ "--allow-home",
29
+ action="store_true",
30
+ help="Override the safety guard that refuses to install directly into $HOME.",
31
+ )
32
+ p_init.add_argument(
33
+ "--no-gitignore",
34
+ dest="update_gitignore",
35
+ action="store_false",
36
+ default=True,
37
+ help="Do not create or update the target's .gitignore.",
38
+ )
39
+ p_init.add_argument("--dry-run", action="store_true", help="Show what would happen.")
40
+ p_init.add_argument(
41
+ "--depth",
42
+ choices=["repo", "workspace"],
43
+ default=None,
44
+ help="Install depth: 'repo' (minimal) or 'workspace' (full home). "
45
+ "Omit for an interactive prompt.",
46
+ )
47
+ p_init.add_argument(
48
+ "--harnesses",
49
+ default=None,
50
+ help="Comma-separated harness ids: claude, codex, openclaw, hermes. "
51
+ "Pass 'none' for a generic install with no harness-specific files.",
52
+ )
53
+ p_init.add_argument(
54
+ "--owner",
55
+ default=None,
56
+ help="Override the canonical memory owner. Must be 'this-repo' or one of --harnesses.",
57
+ )
58
+ p_init.add_argument(
59
+ "--include",
60
+ dest="includes",
61
+ action="append",
62
+ default=[],
63
+ help="Optional add-on (currently: 'publisher'). May be repeated.",
64
+ )
65
+
66
+ # doctor
67
+ p_doctor = sub.add_parser("doctor", help="Verify a target workspace.")
68
+ p_doctor.add_argument("--target", "-t", type=Path, default=Path("."))
69
+ p_doctor.add_argument(
70
+ "--harness",
71
+ choices=["generic", "openclaw", "hermes"],
72
+ default="generic",
73
+ )
74
+
75
+ # status
76
+ p_status = sub.add_parser("status", help="Show which stations are present and healthy.")
77
+ p_status.add_argument("--target", "-t", type=Path, default=Path("."))
78
+
79
+ # scrub
80
+ p_scrub = sub.add_parser("scrub", help="Run content-guard against a target.")
81
+ p_scrub.add_argument("--target", "-t", type=Path, default=Path("."))
82
+ p_scrub.add_argument(
83
+ "--policy",
84
+ default="public-repo",
85
+ help="Policy file name (looks under .brigade/policies, then content-guard/policies) or path.",
86
+ )
87
+ p_scrub.add_argument("--dry-run", action="store_true")
88
+
89
+ # handoff-template
90
+ p_ht = sub.add_parser("handoff-template", help="Print the handoff TEMPLATE.md.")
91
+ p_ht.add_argument(
92
+ "--target",
93
+ "-t",
94
+ type=Path,
95
+ default=None,
96
+ help="Prefer the target's installed TEMPLATE.md when present.",
97
+ )
98
+
99
+ # ingest
100
+ p_ing = sub.add_parser("ingest", help="Process .claude/memory-handoffs/*.md into canonical memory.")
101
+ p_ing.add_argument("--target", "-t", type=Path, default=Path("."))
102
+ p_ing.add_argument("--dry-run", action="store_true")
103
+ p_ing.add_argument(
104
+ "--promote-cards",
105
+ action="store_true",
106
+ help="Auto-promote create-card / update-card handoffs (default off; opt-in).",
107
+ )
108
+ p_ing.add_argument(
109
+ "--route-documents",
110
+ action="store_true",
111
+ help="Auto-route no-card handoffs to TOOLS.md/USER.md/rules/.learnings (default off; opt-in).",
112
+ )
113
+
114
+ # openclaw-fragments
115
+ p_ocf = sub.add_parser("openclaw-fragments", help="Write OpenClaw config fragments for manual review.")
116
+ p_ocf.add_argument("--out", "-o", type=Path, required=True, help="Output directory.")
117
+
118
+ # hermes-fragments
119
+ p_hf = sub.add_parser("hermes-fragments", help="Write Hermes adapter fragments (experimental).")
120
+ p_hf.add_argument("--out", "-o", type=Path, required=True, help="Output directory.")
121
+
122
+ # reconfigure
123
+ p_recon = sub.add_parser("reconfigure", help="Adjust an existing install to a new Selection.")
124
+ p_recon.add_argument("--target", "-t", type=Path, default=Path("."))
125
+ p_recon.add_argument("--depth", choices=["repo", "workspace"], default=None)
126
+ p_recon.add_argument("--harnesses", default=None)
127
+ p_recon.add_argument("--owner", default=None)
128
+ p_recon.add_argument("--include", dest="includes", action="append", default=[])
129
+ p_recon.add_argument("--prune", action="store_true",
130
+ help="Remove files for harnesses no longer selected.")
131
+
132
+ return parser
133
+
134
+
135
+ def main(argv=None) -> int:
136
+ parser = _build_parser()
137
+ args = parser.parse_args(argv)
138
+ cmd = args.command
139
+
140
+ if cmd == "init":
141
+ # New v0.3.0 path: --depth/--harnesses build a Selection directly.
142
+ if getattr(args, "depth", None) is not None or getattr(args, "harnesses", None) is not None:
143
+ from .selection import Selection, KNOWN_HARNESSES, resolve_owner
144
+ from .install import install_selection
145
+
146
+ depth = args.depth or "repo"
147
+ if args.harnesses is None or args.harnesses == "":
148
+ harnesses = ["claude"]
149
+ elif args.harnesses == "none":
150
+ harnesses = []
151
+ else:
152
+ harnesses = [h.strip() for h in args.harnesses.split(",") if h.strip()]
153
+ for h in harnesses:
154
+ if h not in KNOWN_HARNESSES:
155
+ print(f"error: unknown harness {h!r} (valid: {KNOWN_HARNESSES})", file=sys.stderr)
156
+ return 2
157
+ try:
158
+ owner = resolve_owner(harnesses, override=args.owner)
159
+ except ValueError as exc:
160
+ print(f"error: {exc}", file=sys.stderr)
161
+ return 2
162
+ sel = Selection(depth=depth, harnesses=harnesses, owner=owner, includes=list(args.includes))
163
+ return install_selection(
164
+ target=args.target,
165
+ selection=sel,
166
+ force=getattr(args, "force", False),
167
+ dry_run=getattr(args, "dry_run", False),
168
+ allow_home=getattr(args, "allow_home", False),
169
+ )
170
+
171
+ # No selection flags: interactive prompt.
172
+ from .prompt import NonInteractiveError
173
+ from .install import install_selection
174
+ try:
175
+ sel = prompt_for_selection()
176
+ except NonInteractiveError as exc:
177
+ print(f"error: {exc}", file=sys.stderr)
178
+ return 2
179
+ return install_selection(
180
+ target=args.target,
181
+ selection=sel,
182
+ force=getattr(args, "force", False),
183
+ dry_run=getattr(args, "dry_run", False),
184
+ allow_home=getattr(args, "allow_home", False),
185
+ )
186
+ if cmd == "doctor":
187
+ from . import doctor as doctor_mod
188
+
189
+ return doctor_mod.run(target=args.target, harness=args.harness)
190
+ if cmd == "status":
191
+ from . import status as status_mod
192
+
193
+ return status_mod.run(target=args.target)
194
+ if cmd == "scrub":
195
+ from . import scrub as scrub_mod
196
+
197
+ return scrub_mod.run(target=args.target, policy=args.policy, dry_run=args.dry_run)
198
+ if cmd == "handoff-template":
199
+ from . import handoff as handoff_mod
200
+
201
+ return handoff_mod.run(target=args.target)
202
+ if cmd == "ingest":
203
+ from . import ingest as ingest_mod
204
+
205
+ return ingest_mod.run(
206
+ target=args.target,
207
+ dry_run=args.dry_run,
208
+ promote_cards=args.promote_cards,
209
+ route_documents=args.route_documents,
210
+ )
211
+ if cmd == "openclaw-fragments":
212
+ from . import fragments as frag_mod
213
+
214
+ return frag_mod.write_fragments(args.out, harness="openclaw")
215
+ if cmd == "hermes-fragments":
216
+ from . import fragments as frag_mod
217
+
218
+ return frag_mod.write_fragments(args.out, harness="hermes")
219
+ if cmd == "reconfigure":
220
+ from .config import load_config
221
+ from .reconfigure import reconfigure as _reconfigure
222
+ from .selection import Selection, KNOWN_HARNESSES, resolve_owner
223
+
224
+ existing = load_config(args.target)
225
+ if existing is None:
226
+ print("error: no .brigade/config.json in target. Run `brigade init` first.", file=sys.stderr)
227
+ return 2
228
+
229
+ depth = args.depth or existing.selection.depth
230
+ if args.harnesses is None:
231
+ harnesses = list(existing.selection.harnesses)
232
+ elif args.harnesses == "none":
233
+ harnesses = []
234
+ else:
235
+ harnesses = [h.strip() for h in args.harnesses.split(",") if h.strip()]
236
+ for h in harnesses:
237
+ if h not in KNOWN_HARNESSES:
238
+ print(f"error: unknown harness {h!r}", file=sys.stderr)
239
+ return 2
240
+ owner = resolve_owner(harnesses, override=args.owner)
241
+ includes = list(args.includes) if args.includes else list(existing.selection.includes)
242
+ new_sel = Selection(depth=depth, harnesses=harnesses, owner=owner, includes=includes)
243
+ return _reconfigure(args.target, new_selection=new_sel, prune=args.prune)
244
+
245
+ parser.error(f"unknown command: {cmd}")
246
+ return 2
247
+
248
+
249
+ def main_deprecated(argv=None) -> int:
250
+ print(
251
+ "warning: the 'solo-mise' command is deprecated; use 'brigade' instead.",
252
+ file=sys.stderr,
253
+ )
254
+ return main(argv)
255
+
256
+
257
+ if __name__ == "__main__": # pragma: no cover
258
+ sys.exit(main())
brigade/config.py ADDED
@@ -0,0 +1,65 @@
1
+ """Read/write .brigade/config.json - the per-target source of truth."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import Optional
8
+
9
+ from .selection import Selection
10
+
11
+
12
+ WORKSPACE_DIRNAME = ".brigade"
13
+ LEGACY_WORKSPACE_DIRNAMES = (".solo-mise",)
14
+ CONFIG_REL_PATH = f"{WORKSPACE_DIRNAME}/config.json"
15
+ SUPPORTED_VERSIONS = (1,)
16
+
17
+
18
+ @dataclass
19
+ class Config:
20
+ version: int
21
+ selection: Selection
22
+
23
+
24
+ def config_path(target: Path) -> Path:
25
+ return target / CONFIG_REL_PATH
26
+
27
+
28
+ def write_config(target: Path, cfg: Config) -> None:
29
+ cfg.selection.validate()
30
+ path = config_path(target)
31
+ path.parent.mkdir(parents=True, exist_ok=True)
32
+ payload = {
33
+ "version": cfg.version,
34
+ "depth": cfg.selection.depth,
35
+ "harnesses": list(cfg.selection.harnesses),
36
+ "owner": cfg.selection.owner,
37
+ "includes": list(cfg.selection.includes),
38
+ }
39
+ path.write_text(json.dumps(payload, indent=2) + "\n")
40
+
41
+
42
+ def load_config(target: Path) -> Optional[Config]:
43
+ path = config_path(target)
44
+ if not path.is_file():
45
+ for legacy in LEGACY_WORKSPACE_DIRNAMES:
46
+ legacy_path = target / legacy / "config.json"
47
+ if legacy_path.is_file():
48
+ path = legacy_path
49
+ break
50
+ else:
51
+ return None
52
+ data = json.loads(path.read_text())
53
+ version = data.get("version")
54
+ if version not in SUPPORTED_VERSIONS:
55
+ raise ValueError(
56
+ f"unsupported config version: {version!r} (supported: {SUPPORTED_VERSIONS})"
57
+ )
58
+ sel = Selection(
59
+ depth=data.get("depth", ""),
60
+ harnesses=list(data.get("harnesses", [])),
61
+ owner=data.get("owner", "this-repo"),
62
+ includes=list(data.get("includes", [])),
63
+ )
64
+ sel.validate()
65
+ return Config(version=version, selection=sel)