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/doctor.py
ADDED
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
"""`brigade doctor` - verify a target workspace is wired correctly."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Callable, List, Tuple
|
|
11
|
+
|
|
12
|
+
CheckResult = Tuple[str, str, str] # (status, name, detail)
|
|
13
|
+
OK = "OK"
|
|
14
|
+
WARN = "WARN"
|
|
15
|
+
FAIL = "FAIL"
|
|
16
|
+
MANUAL = "MANUAL"
|
|
17
|
+
|
|
18
|
+
from .station import DoctorContext
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def build_context(target: Path, harness: str = "generic") -> DoctorContext:
|
|
22
|
+
target = target.expanduser().resolve()
|
|
23
|
+
from .config import load_config
|
|
24
|
+
|
|
25
|
+
sel = None
|
|
26
|
+
try:
|
|
27
|
+
cfg = load_config(target)
|
|
28
|
+
except (ValueError, json.JSONDecodeError):
|
|
29
|
+
cfg = None
|
|
30
|
+
if cfg is not None:
|
|
31
|
+
sel = cfg.selection
|
|
32
|
+
harnesses = list(sel.harnesses)
|
|
33
|
+
elif harness in ("openclaw", "hermes"):
|
|
34
|
+
harnesses = ["claude", harness]
|
|
35
|
+
else:
|
|
36
|
+
harnesses = ["claude"]
|
|
37
|
+
return DoctorContext(target=target, selection=sel, harnesses=harnesses)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def core_station_checks(ctx: DoctorContext) -> List[CheckResult]:
|
|
41
|
+
checks: List[CheckResult] = []
|
|
42
|
+
checks.extend(_check_workspace_files(ctx.target))
|
|
43
|
+
if "openclaw" in ctx.harnesses:
|
|
44
|
+
checks.extend(_check_openclaw())
|
|
45
|
+
if "hermes" in ctx.harnesses:
|
|
46
|
+
checks.extend(_check_hermes(ctx.target))
|
|
47
|
+
checks.extend(_check_orphan_inboxes(ctx.target, ctx.harnesses))
|
|
48
|
+
return checks
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def memory_station_checks(ctx: DoctorContext) -> List[CheckResult]:
|
|
52
|
+
checks: List[CheckResult] = []
|
|
53
|
+
checks.extend(_check_handoff_inboxes(ctx.target, ctx.selection, ctx.harnesses))
|
|
54
|
+
checks.extend(_check_memory_care(ctx.target))
|
|
55
|
+
return checks
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def guard_station_checks(ctx: DoctorContext) -> List[CheckResult]:
|
|
59
|
+
return _check_publish_gate(ctx.target)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def run(target: Path, harness: str = "generic") -> int:
|
|
63
|
+
from .registry import all_stations
|
|
64
|
+
|
|
65
|
+
ctx = build_context(target, harness)
|
|
66
|
+
print(f"brigade doctor: target {ctx.target}")
|
|
67
|
+
if ctx.selection is not None:
|
|
68
|
+
sel = ctx.selection
|
|
69
|
+
print(
|
|
70
|
+
f" harnesses: {', '.join(sel.harnesses) or '(none)'} "
|
|
71
|
+
f"(owner={sel.owner}, depth={sel.depth})"
|
|
72
|
+
)
|
|
73
|
+
else:
|
|
74
|
+
print(
|
|
75
|
+
f" harnesses: (legacy target, no config; assuming {', '.join(ctx.harnesses)})"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
checks: List[CheckResult] = []
|
|
79
|
+
for station in all_stations():
|
|
80
|
+
if station.doctor is not None:
|
|
81
|
+
checks.extend(station.doctor(ctx))
|
|
82
|
+
return _report(checks)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _check_workspace_files(target: Path) -> List[CheckResult]:
|
|
86
|
+
results: List[CheckResult] = []
|
|
87
|
+
required = ["AGENTS.md"]
|
|
88
|
+
optional = [
|
|
89
|
+
"CLAUDE.md",
|
|
90
|
+
"MEMORY.md",
|
|
91
|
+
"TOOLS.md",
|
|
92
|
+
"USER.md",
|
|
93
|
+
"SAFETY_RULES.md",
|
|
94
|
+
"INSTALL_FOR_AGENTS.md",
|
|
95
|
+
]
|
|
96
|
+
for name in required:
|
|
97
|
+
path = target / name
|
|
98
|
+
if path.is_file():
|
|
99
|
+
results.append((OK, f"bootstrap: {name}", str(path)))
|
|
100
|
+
else:
|
|
101
|
+
results.append((FAIL, f"bootstrap: {name}", f"missing at {path}"))
|
|
102
|
+
for name in optional:
|
|
103
|
+
path = target / name
|
|
104
|
+
if path.is_file():
|
|
105
|
+
results.append((OK, f"bootstrap: {name}", str(path)))
|
|
106
|
+
else:
|
|
107
|
+
results.append((WARN, f"bootstrap: {name}", f"not present at {path}"))
|
|
108
|
+
return results
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# Writer harness -> inbox-dir prefix. Only writer harnesses have an inbox.
|
|
112
|
+
_WRITER_INBOXES = {
|
|
113
|
+
"claude": ".claude/memory-handoffs",
|
|
114
|
+
"codex": ".codex/memory-handoffs",
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _check_handoff_inboxes(
|
|
119
|
+
target: Path, sel, selected_harnesses: List[str]
|
|
120
|
+
) -> List[CheckResult]:
|
|
121
|
+
results: List[CheckResult] = []
|
|
122
|
+
writers = selected_harnesses
|
|
123
|
+
for h in writers:
|
|
124
|
+
rel = _WRITER_INBOXES.get(h)
|
|
125
|
+
if rel is None:
|
|
126
|
+
continue # reader harness, no inbox
|
|
127
|
+
inbox = target / rel
|
|
128
|
+
if inbox.is_dir():
|
|
129
|
+
results.append((OK, f"handoff: {h} inbox", str(inbox)))
|
|
130
|
+
else:
|
|
131
|
+
results.append((FAIL, f"handoff: {h} inbox", f"missing at {inbox}"))
|
|
132
|
+
tmpl = inbox / "TEMPLATE.md"
|
|
133
|
+
if tmpl.is_file():
|
|
134
|
+
results.append((OK, f"handoff: {h} TEMPLATE.md", str(tmpl)))
|
|
135
|
+
else:
|
|
136
|
+
results.append(
|
|
137
|
+
(WARN, f"handoff: {h} TEMPLATE.md", f"missing at {tmpl}")
|
|
138
|
+
)
|
|
139
|
+
processed = inbox / "processed"
|
|
140
|
+
if processed.is_dir():
|
|
141
|
+
results.append((OK, f"handoff: {h} processed/", str(processed)))
|
|
142
|
+
else:
|
|
143
|
+
results.append(
|
|
144
|
+
(WARN, f"handoff: {h} processed/", f"missing at {processed}")
|
|
145
|
+
)
|
|
146
|
+
cards = target / "memory" / "cards"
|
|
147
|
+
if cards.is_dir():
|
|
148
|
+
results.append((OK, "memory: cards/", str(cards)))
|
|
149
|
+
else:
|
|
150
|
+
results.append(
|
|
151
|
+
(
|
|
152
|
+
WARN,
|
|
153
|
+
"memory: cards/",
|
|
154
|
+
f"missing at {cards}; ingester cannot promote cards",
|
|
155
|
+
)
|
|
156
|
+
)
|
|
157
|
+
return results
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _check_orphan_inboxes(
|
|
161
|
+
target: Path, selected_harnesses: List[str]
|
|
162
|
+
) -> List[CheckResult]:
|
|
163
|
+
results: List[CheckResult] = []
|
|
164
|
+
for h, rel in _WRITER_INBOXES.items():
|
|
165
|
+
if h in selected_harnesses:
|
|
166
|
+
continue
|
|
167
|
+
inbox = target / rel
|
|
168
|
+
if inbox.is_dir():
|
|
169
|
+
results.append(
|
|
170
|
+
(
|
|
171
|
+
WARN,
|
|
172
|
+
f"orphan: {h} inbox",
|
|
173
|
+
f"{inbox} exists but {h} is not in config; "
|
|
174
|
+
f"remove or add to config (unselected harness)",
|
|
175
|
+
)
|
|
176
|
+
)
|
|
177
|
+
return results
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _check_memory_care(target: Path) -> List[CheckResult]:
|
|
181
|
+
results: List[CheckResult] = []
|
|
182
|
+
decay_dir = target / "memory" / "cards" / "decay"
|
|
183
|
+
scan = decay_dir / "scan-latest.json"
|
|
184
|
+
queue = decay_dir / "refresh-queue.json"
|
|
185
|
+
|
|
186
|
+
if decay_dir.is_dir():
|
|
187
|
+
results.append((OK, "memory-care: decay/", str(decay_dir)))
|
|
188
|
+
else:
|
|
189
|
+
results.append(
|
|
190
|
+
(
|
|
191
|
+
WARN,
|
|
192
|
+
"memory-care: decay/",
|
|
193
|
+
f"missing at {decay_dir}; staleness scanner not wired",
|
|
194
|
+
)
|
|
195
|
+
)
|
|
196
|
+
return results
|
|
197
|
+
|
|
198
|
+
if scan.is_file():
|
|
199
|
+
detail = str(scan)
|
|
200
|
+
try:
|
|
201
|
+
data = json.loads(scan.read_text())
|
|
202
|
+
scan_date = data.get("scan_date")
|
|
203
|
+
counts = data.get("counts", {})
|
|
204
|
+
if scan_date:
|
|
205
|
+
detail = f"{scan} (scan_date={scan_date}, stale={counts.get('stale', 'unknown')})"
|
|
206
|
+
except json.JSONDecodeError:
|
|
207
|
+
detail = f"invalid JSON: {scan}"
|
|
208
|
+
results.append((WARN, "memory-care: scan-latest", detail))
|
|
209
|
+
else:
|
|
210
|
+
results.append((OK, "memory-care: scan-latest", detail))
|
|
211
|
+
else:
|
|
212
|
+
results.append((WARN, "memory-care: scan-latest", f"missing at {scan}"))
|
|
213
|
+
|
|
214
|
+
if queue.is_file():
|
|
215
|
+
detail = str(queue)
|
|
216
|
+
try:
|
|
217
|
+
data = json.loads(queue.read_text())
|
|
218
|
+
cards = data.get("cards", [])
|
|
219
|
+
if isinstance(cards, list):
|
|
220
|
+
detail = f"{queue} ({len(cards)} queued)"
|
|
221
|
+
except json.JSONDecodeError:
|
|
222
|
+
detail = f"invalid JSON: {queue}"
|
|
223
|
+
results.append((WARN, "memory-care: refresh-queue", detail))
|
|
224
|
+
else:
|
|
225
|
+
results.append((OK, "memory-care: refresh-queue", detail))
|
|
226
|
+
else:
|
|
227
|
+
results.append((WARN, "memory-care: refresh-queue", f"missing at {queue}"))
|
|
228
|
+
|
|
229
|
+
return results
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _check_publish_gate(target: Path) -> List[CheckResult]:
|
|
233
|
+
results: List[CheckResult] = []
|
|
234
|
+
hook = target / "hooks" / "pre-push"
|
|
235
|
+
if hook.is_file():
|
|
236
|
+
results.append((OK, "publish: hooks/pre-push", str(hook)))
|
|
237
|
+
if not os.access(hook, os.X_OK):
|
|
238
|
+
results.append(
|
|
239
|
+
(WARN, "publish: hooks/pre-push", "exists but not executable; run `chmod +x hooks/pre-push`")
|
|
240
|
+
)
|
|
241
|
+
else:
|
|
242
|
+
results.append((WARN, "publish: hooks/pre-push", f"missing at {hook}"))
|
|
243
|
+
|
|
244
|
+
scanner_dir = Path(os.environ.get("CONTENT_GUARD_DIR", str(Path.home() / "repos" / "content-guard")))
|
|
245
|
+
if scanner_dir.is_dir():
|
|
246
|
+
results.append((OK, "publish: content-guard", str(scanner_dir)))
|
|
247
|
+
else:
|
|
248
|
+
results.append(
|
|
249
|
+
(MANUAL, "publish: content-guard", f"not found at {scanner_dir}; install or set CONTENT_GUARD_DIR")
|
|
250
|
+
)
|
|
251
|
+
return results
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _check_openclaw() -> List[CheckResult]:
|
|
255
|
+
"""Inspect ~/.openclaw/openclaw.json for the wiring brigade expects."""
|
|
256
|
+
results: List[CheckResult] = []
|
|
257
|
+
config = Path.home() / ".openclaw" / "openclaw.json"
|
|
258
|
+
if not config.is_file():
|
|
259
|
+
results.append((MANUAL, "openclaw: config", f"not found at {config}; install OpenClaw first"))
|
|
260
|
+
return results
|
|
261
|
+
try:
|
|
262
|
+
data = json.loads(config.read_text())
|
|
263
|
+
except json.JSONDecodeError as exc:
|
|
264
|
+
results.append((FAIL, "openclaw: config", f"invalid JSON: {exc}"))
|
|
265
|
+
return results
|
|
266
|
+
results.append((OK, "openclaw: config", str(config)))
|
|
267
|
+
|
|
268
|
+
plugins = data.get("plugins", {}).get("entries", {})
|
|
269
|
+
if plugins:
|
|
270
|
+
results.append((OK, "openclaw: plugins", f"{len(plugins)} entries"))
|
|
271
|
+
else:
|
|
272
|
+
results.append((WARN, "openclaw: plugins", "no plugin entries configured"))
|
|
273
|
+
|
|
274
|
+
primary = (
|
|
275
|
+
data.get("agents", {}).get("defaults", {}).get("model", {}).get("primary")
|
|
276
|
+
)
|
|
277
|
+
if primary:
|
|
278
|
+
results.append((OK, "openclaw: primary model", primary))
|
|
279
|
+
else:
|
|
280
|
+
results.append((WARN, "openclaw: primary model", "agents.defaults.model.primary unset"))
|
|
281
|
+
|
|
282
|
+
# jq sanity (optional)
|
|
283
|
+
if shutil.which("jq"):
|
|
284
|
+
results.append((OK, "openclaw: jq", "present"))
|
|
285
|
+
else:
|
|
286
|
+
results.append((WARN, "openclaw: jq", "missing; merge helpers will not work"))
|
|
287
|
+
results.extend(_check_openclaw_cron_jobs())
|
|
288
|
+
return results
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _check_openclaw_cron_jobs() -> List[CheckResult]:
|
|
292
|
+
results: List[CheckResult] = []
|
|
293
|
+
jobs_path = Path.home() / ".openclaw" / "cron" / "jobs.json"
|
|
294
|
+
if not jobs_path.is_file():
|
|
295
|
+
return [
|
|
296
|
+
(
|
|
297
|
+
WARN,
|
|
298
|
+
"openclaw: cron jobs",
|
|
299
|
+
f"not found at {jobs_path}; handoff ingest and memory-care schedules unknown",
|
|
300
|
+
)
|
|
301
|
+
]
|
|
302
|
+
|
|
303
|
+
try:
|
|
304
|
+
data = json.loads(jobs_path.read_text())
|
|
305
|
+
except json.JSONDecodeError as exc:
|
|
306
|
+
return [(WARN, "openclaw: cron jobs", f"invalid JSON: {exc}")]
|
|
307
|
+
|
|
308
|
+
jobs = data.get("jobs", [])
|
|
309
|
+
if not isinstance(jobs, list):
|
|
310
|
+
return [(WARN, "openclaw: cron jobs", "jobs.json has no jobs array")]
|
|
311
|
+
|
|
312
|
+
expected = [
|
|
313
|
+
("openclaw: handoff ingest cron", "Claude Memory Handoff Ingest"),
|
|
314
|
+
("openclaw: card decay scanner", "Card Decay Scanner (Daily)"),
|
|
315
|
+
("openclaw: card decay refresh", "Card Decay Auto-Refresh (Safe)"),
|
|
316
|
+
]
|
|
317
|
+
for check_name, job_name in expected:
|
|
318
|
+
job = _find_job(jobs, job_name)
|
|
319
|
+
if job is None:
|
|
320
|
+
results.append((WARN, check_name, f"missing job named {job_name!r}"))
|
|
321
|
+
continue
|
|
322
|
+
if not job.get("enabled", False):
|
|
323
|
+
results.append((WARN, check_name, f"{job_name!r} exists but is disabled"))
|
|
324
|
+
continue
|
|
325
|
+
results.append((OK, check_name, _format_schedule(job.get("schedule"))))
|
|
326
|
+
|
|
327
|
+
weekly = _find_job(jobs, "Card Decay Deep Report (Weekly)")
|
|
328
|
+
if weekly is not None and weekly.get("enabled", False):
|
|
329
|
+
results.append((OK, "openclaw: card decay weekly", _format_schedule(weekly.get("schedule"))))
|
|
330
|
+
return results
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _find_job(jobs: list, name: str) -> dict | None:
|
|
334
|
+
for job in jobs:
|
|
335
|
+
if isinstance(job, dict) and job.get("name") == name:
|
|
336
|
+
return job
|
|
337
|
+
return None
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _format_schedule(schedule) -> str:
|
|
341
|
+
if not isinstance(schedule, dict):
|
|
342
|
+
return "enabled; schedule not specified"
|
|
343
|
+
kind = schedule.get("kind")
|
|
344
|
+
if kind == "cron":
|
|
345
|
+
return f"enabled; cron {schedule.get('expr', '<missing expr>')} {schedule.get('tz', '')}".strip()
|
|
346
|
+
if kind == "every":
|
|
347
|
+
every_ms = schedule.get("everyMs")
|
|
348
|
+
if isinstance(every_ms, int):
|
|
349
|
+
return f"enabled; every {every_ms // 60000} min"
|
|
350
|
+
return "enabled; every schedule"
|
|
351
|
+
return f"enabled; {kind or 'unknown'} schedule"
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _check_hermes(target: Path) -> List[CheckResult]:
|
|
355
|
+
results: List[CheckResult] = []
|
|
356
|
+
fragments_dir = target / ".brigade" / "hermes"
|
|
357
|
+
expected = [
|
|
358
|
+
"workspace.harness.json",
|
|
359
|
+
"memory-handoff.harness.json",
|
|
360
|
+
"model-lanes.harness.json",
|
|
361
|
+
]
|
|
362
|
+
for name in expected:
|
|
363
|
+
path = fragments_dir / name
|
|
364
|
+
if path.is_file():
|
|
365
|
+
results.append((OK, f"hermes: {name}", str(path)))
|
|
366
|
+
else:
|
|
367
|
+
results.append((WARN, f"hermes: {name}", f"missing at {path}; run `brigade hermes-fragments`"))
|
|
368
|
+
results.append(
|
|
369
|
+
(MANUAL, "hermes: install validation", "Hermes adapter is experimental; validate against your install")
|
|
370
|
+
)
|
|
371
|
+
return results
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _report(checks: List[CheckResult]) -> int:
|
|
375
|
+
width = max((len(name) for _, name, _ in checks), default=20)
|
|
376
|
+
failed = 0
|
|
377
|
+
manual = 0
|
|
378
|
+
for status, name, detail in checks:
|
|
379
|
+
marker = {
|
|
380
|
+
OK: " [ok] ",
|
|
381
|
+
WARN: " [warn]",
|
|
382
|
+
FAIL: " [fail]",
|
|
383
|
+
MANUAL: " [todo]",
|
|
384
|
+
}[status]
|
|
385
|
+
print(f"{marker} {name.ljust(width)} {detail}")
|
|
386
|
+
if status == FAIL:
|
|
387
|
+
failed += 1
|
|
388
|
+
elif status == MANUAL:
|
|
389
|
+
manual += 1
|
|
390
|
+
print()
|
|
391
|
+
summary = f"summary: {len(checks)} checks, {failed} failed, {manual} manual"
|
|
392
|
+
print(summary)
|
|
393
|
+
return 1 if failed else 0
|
brigade/fragments.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""`brigade openclaw-fragments` / `hermes-fragments` - write config fragments.
|
|
2
|
+
|
|
3
|
+
These never mutate a live config. They drop JSON fragments into the chosen
|
|
4
|
+
output directory so the user can `jq -s '.[0] * .[1]'` them in by hand.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import shutil
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from .templates import template_root
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
HARNESS_FILES = {
|
|
16
|
+
"openclaw": [
|
|
17
|
+
"model-aliases.openclaw.json",
|
|
18
|
+
"ollama-memory-search.openclaw.json",
|
|
19
|
+
"acp-escalation.openclaw.json",
|
|
20
|
+
"README.md",
|
|
21
|
+
],
|
|
22
|
+
"hermes": [
|
|
23
|
+
"workspace.harness.json",
|
|
24
|
+
"memory-handoff.harness.json",
|
|
25
|
+
"model-lanes.harness.json",
|
|
26
|
+
"README.md",
|
|
27
|
+
],
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def write_fragments(out: Path, harness: str) -> int:
|
|
32
|
+
if harness not in HARNESS_FILES:
|
|
33
|
+
print(f"brigade: unknown harness: {harness}", file=sys.stderr)
|
|
34
|
+
return 2
|
|
35
|
+
|
|
36
|
+
out = out.expanduser().resolve()
|
|
37
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
38
|
+
|
|
39
|
+
src_dir = template_root() / harness
|
|
40
|
+
for name in HARNESS_FILES[harness]:
|
|
41
|
+
src = src_dir / name
|
|
42
|
+
if not src.is_file():
|
|
43
|
+
print(f"brigade: template missing: {src}", file=sys.stderr)
|
|
44
|
+
return 3
|
|
45
|
+
dest = out / name
|
|
46
|
+
shutil.copyfile(src, dest)
|
|
47
|
+
|
|
48
|
+
print(f"brigade: wrote {harness} fragments to {out}")
|
|
49
|
+
print()
|
|
50
|
+
print("Next steps:")
|
|
51
|
+
print(f" - inspect each fragment under {out}")
|
|
52
|
+
if harness == "openclaw":
|
|
53
|
+
print(
|
|
54
|
+
" - merge with: jq -s '.[0] * .[1]' ~/.openclaw/openclaw.json "
|
|
55
|
+
f"{out}/<fragment>.json > /tmp/merged.json"
|
|
56
|
+
)
|
|
57
|
+
print(
|
|
58
|
+
" - verify with: brigade doctor --target ~/.openclaw/workspace --harness openclaw"
|
|
59
|
+
)
|
|
60
|
+
elif harness == "hermes":
|
|
61
|
+
print(
|
|
62
|
+
" - the Hermes adapter is experimental; validate against your real Hermes install"
|
|
63
|
+
)
|
|
64
|
+
return 0
|
brigade/handoff.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""`brigade handoff-template` - print the handoff TEMPLATE.md to stdout."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from .templates import template_root
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def run(target: Path | None = None) -> int:
|
|
11
|
+
"""Print the handoff template. If `target` is given and has its own
|
|
12
|
+
TEMPLATE.md, that copy is preferred (matches the user's installed version)."""
|
|
13
|
+
if target is not None:
|
|
14
|
+
local = target.expanduser().resolve() / ".claude" / "memory-handoffs" / "TEMPLATE.md"
|
|
15
|
+
if local.is_file():
|
|
16
|
+
sys.stdout.write(local.read_text())
|
|
17
|
+
return 0
|
|
18
|
+
packaged = template_root() / "claude" / "memory-handoffs" / "TEMPLATE.md"
|
|
19
|
+
if not packaged.is_file():
|
|
20
|
+
print("error: packaged TEMPLATE.md missing", file=sys.stderr)
|
|
21
|
+
return 1
|
|
22
|
+
sys.stdout.write(packaged.read_text())
|
|
23
|
+
return 0
|