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.
- brigade/__init__.py +3 -0
- brigade/__main__.py +5 -0
- brigade/cli.py +258 -0
- brigade/config.py +65 -0
- brigade/doctor.py +393 -0
- brigade/fragments.py +64 -0
- brigade/handoff.py +23 -0
- brigade/ingest.py +298 -0
- brigade/install.py +217 -0
- brigade/prompt.py +135 -0
- brigade/py.typed +0 -0
- brigade/reconfigure.py +64 -0
- brigade/registry.py +39 -0
- brigade/scrub.py +90 -0
- brigade/selection.py +66 -0
- brigade/station.py +36 -0
- brigade/status.py +24 -0
- brigade/templates/claude/memory-handoffs/TEMPLATE.md +57 -0
- brigade/templates/codex/memory-handoffs/TEMPLATE.md +57 -0
- brigade/templates/depth/repo.json +12 -0
- brigade/templates/depth/workspace.json +30 -0
- brigade/templates/generic/harness-adapter-checklist.md +55 -0
- brigade/templates/generic/memory-contract.md +41 -0
- brigade/templates/harnesses/claude.json +12 -0
- brigade/templates/harnesses/codex.json +11 -0
- brigade/templates/harnesses/hermes.json +16 -0
- brigade/templates/harnesses/openclaw.json +17 -0
- brigade/templates/hermes/README.md +25 -0
- brigade/templates/hermes/memory-handoff.harness.json +36 -0
- brigade/templates/hermes/model-lanes.harness.json +17 -0
- brigade/templates/hermes/workspace.harness.json +30 -0
- brigade/templates/hooks/pre-push +36 -0
- brigade/templates/includes/publisher.json +15 -0
- brigade/templates/memory/cards/backup-restic.md +126 -0
- brigade/templates/memory/cards/chat-surface-crawlers.md +103 -0
- brigade/templates/memory/cards/content-safety.md +54 -0
- brigade/templates/memory/cards/handoff-flow.md +70 -0
- brigade/templates/memory/cards/memory-architecture.md +56 -0
- brigade/templates/memory/cards/memory-care-staleness.md +58 -0
- brigade/templates/memory/cards/memory-scanner.md +98 -0
- brigade/templates/memory/cards/multi-workspace-handoff-admin.md +63 -0
- brigade/templates/memory/cards/obsidian-notes.md +82 -0
- brigade/templates/memory/cards/pipeline-standups.md +88 -0
- brigade/templates/memory/cards/tokenjuice-output-compaction.md +106 -0
- brigade/templates/openclaw/README.md +40 -0
- brigade/templates/openclaw/acp-escalation.openclaw.json +33 -0
- brigade/templates/openclaw/model-aliases.openclaw.json +21 -0
- brigade/templates/openclaw/ollama-memory-search.openclaw.json +24 -0
- brigade/templates/policies/public-content.json +28 -0
- brigade/templates/policies/public-repo.json +27 -0
- brigade/templates/scripts/backup-restic.sh +156 -0
- brigade/templates/skills/note/SKILL.md +173 -0
- brigade/templates/workspace/AGENTS.md +146 -0
- brigade/templates/workspace/CLAUDE.md +48 -0
- brigade/templates/workspace/HEARTBEAT.md +41 -0
- brigade/templates/workspace/IDENTITY.md +27 -0
- brigade/templates/workspace/INSTALL_FOR_AGENTS.md +61 -0
- brigade/templates/workspace/MEMORY.md +102 -0
- brigade/templates/workspace/SAFETY_RULES.md +164 -0
- brigade/templates/workspace/SOUL.md +92 -0
- brigade/templates/workspace/TOOLS.md +116 -0
- brigade/templates/workspace/USER.md +88 -0
- brigade/templates.py +88 -0
- brigade_cli-0.5.0.dist-info/METADATA +211 -0
- brigade_cli-0.5.0.dist-info/RECORD +69 -0
- brigade_cli-0.5.0.dist-info/WHEEL +5 -0
- brigade_cli-0.5.0.dist-info/entry_points.txt +3 -0
- brigade_cli-0.5.0.dist-info/licenses/LICENSE +21 -0
- brigade_cli-0.5.0.dist-info/top_level.txt +1 -0
brigade/__init__.py
ADDED
brigade/__main__.py
ADDED
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)
|