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/reconfigure.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""brigade reconfigure - adjust an existing install to a new Selection."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import shutil
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from .config import Config, load_config, write_config
|
|
9
|
+
from .install import apply_gitignore, install_selection, resolve_manifests
|
|
10
|
+
from .selection import Selection
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
_WRITER_DIRS = {
|
|
14
|
+
"claude": ".claude",
|
|
15
|
+
"codex": ".codex",
|
|
16
|
+
}
|
|
17
|
+
_READER_DIRS = {
|
|
18
|
+
"openclaw": ".brigade/openclaw",
|
|
19
|
+
"hermes": ".brigade/hermes",
|
|
20
|
+
}
|
|
21
|
+
_HARNESS_BRIDGE_FILES = {
|
|
22
|
+
"claude": ["CLAUDE.md"],
|
|
23
|
+
"codex": [],
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def reconfigure(target: Path, new_selection: Selection, prune: bool) -> int:
|
|
28
|
+
target = target.expanduser().resolve()
|
|
29
|
+
new_selection.validate()
|
|
30
|
+
existing = load_config(target)
|
|
31
|
+
old_harnesses = set(existing.selection.harnesses) if existing else set()
|
|
32
|
+
new_harnesses = set(new_selection.harnesses)
|
|
33
|
+
|
|
34
|
+
added = new_harnesses - old_harnesses
|
|
35
|
+
removed = old_harnesses - new_harnesses
|
|
36
|
+
|
|
37
|
+
# Re-run install with force=True to lay down baseline + new-harness files.
|
|
38
|
+
# install_selection is idempotent: unchanged files re-render identically.
|
|
39
|
+
rc = install_selection(target, new_selection, force=True)
|
|
40
|
+
if rc != 0:
|
|
41
|
+
return rc
|
|
42
|
+
|
|
43
|
+
# Prune removed harnesses if requested.
|
|
44
|
+
if prune:
|
|
45
|
+
for h in sorted(removed):
|
|
46
|
+
wdir = _WRITER_DIRS.get(h)
|
|
47
|
+
if wdir and (target / wdir).is_dir():
|
|
48
|
+
shutil.rmtree(target / wdir)
|
|
49
|
+
for bridge in _HARNESS_BRIDGE_FILES.get(h, []):
|
|
50
|
+
bp = target / bridge
|
|
51
|
+
if bp.is_file():
|
|
52
|
+
bp.unlink()
|
|
53
|
+
rdir = _READER_DIRS.get(h)
|
|
54
|
+
if rdir and (target / rdir).is_dir():
|
|
55
|
+
shutil.rmtree(target / rdir)
|
|
56
|
+
print(f"brigade: pruned {h}")
|
|
57
|
+
|
|
58
|
+
print(f"brigade: reconfigured -> harnesses={','.join(new_selection.harnesses) or '(none)'}")
|
|
59
|
+
if added:
|
|
60
|
+
print(f" added: {','.join(sorted(added))}")
|
|
61
|
+
if removed:
|
|
62
|
+
verb = "pruned" if prune else "orphaned (use --prune to delete)"
|
|
63
|
+
print(f" removed: {','.join(sorted(removed))} ({verb})")
|
|
64
|
+
return 0
|
brigade/registry.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""The built-in station registry."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Optional, Tuple
|
|
5
|
+
|
|
6
|
+
from . import doctor as _doctor
|
|
7
|
+
from .station import Station
|
|
8
|
+
|
|
9
|
+
CORE = Station(
|
|
10
|
+
name="core",
|
|
11
|
+
summary="workspace bootstrap and harness adapters",
|
|
12
|
+
aliases=("mise",),
|
|
13
|
+
doctor=_doctor.core_station_checks,
|
|
14
|
+
)
|
|
15
|
+
MEMORY = Station(
|
|
16
|
+
name="memory",
|
|
17
|
+
summary="handoff inbox, ingest, and memory-care",
|
|
18
|
+
aliases=("garde",),
|
|
19
|
+
doctor=_doctor.memory_station_checks,
|
|
20
|
+
)
|
|
21
|
+
GUARD = Station(
|
|
22
|
+
name="guard",
|
|
23
|
+
summary="publish safety and content scrub",
|
|
24
|
+
aliases=("pass",),
|
|
25
|
+
doctor=_doctor.guard_station_checks,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
_BUILTIN: Tuple[Station, ...] = (CORE, MEMORY, GUARD)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def all_stations() -> Tuple[Station, ...]:
|
|
32
|
+
return _BUILTIN
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def resolve(name_or_alias: str) -> Optional[Station]:
|
|
36
|
+
for station in _BUILTIN:
|
|
37
|
+
if station.matches(name_or_alias):
|
|
38
|
+
return station
|
|
39
|
+
return None
|
brigade/scrub.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""`brigade scrub` - run the content-guard scanner against a target."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def run(
|
|
11
|
+
target: Path,
|
|
12
|
+
policy: str = "public-repo",
|
|
13
|
+
dry_run: bool = False,
|
|
14
|
+
) -> int:
|
|
15
|
+
target = target.expanduser().resolve()
|
|
16
|
+
scanner_dir = Path(
|
|
17
|
+
os.environ.get("CONTENT_GUARD_DIR", str(Path.home() / "repos" / "content-guard"))
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
if not scanner_dir.is_dir():
|
|
21
|
+
print(
|
|
22
|
+
f"brigade scrub: content-guard not found at {scanner_dir}",
|
|
23
|
+
file=sys.stderr,
|
|
24
|
+
)
|
|
25
|
+
print(
|
|
26
|
+
"brigade scrub: clone https://github.com/solomonneas/content-guard "
|
|
27
|
+
"or set CONTENT_GUARD_DIR",
|
|
28
|
+
file=sys.stderr,
|
|
29
|
+
)
|
|
30
|
+
return 2
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
policy_path = _resolve_policy(target, scanner_dir, policy)
|
|
34
|
+
except ValueError as exc:
|
|
35
|
+
print(f"brigade scrub: {exc}", file=sys.stderr)
|
|
36
|
+
return 4
|
|
37
|
+
if not policy_path.is_file():
|
|
38
|
+
print(f"brigade scrub: policy not found: {policy_path}", file=sys.stderr)
|
|
39
|
+
return 3
|
|
40
|
+
|
|
41
|
+
cmd = [
|
|
42
|
+
sys.executable,
|
|
43
|
+
"-m",
|
|
44
|
+
"content_guard",
|
|
45
|
+
"scan",
|
|
46
|
+
str(target),
|
|
47
|
+
"--policy",
|
|
48
|
+
str(policy_path),
|
|
49
|
+
]
|
|
50
|
+
if dry_run:
|
|
51
|
+
print("brigade scrub: would run:")
|
|
52
|
+
print(" ", " ".join(cmd))
|
|
53
|
+
print(f" PYTHONPATH={scanner_dir / 'src'}")
|
|
54
|
+
return 0
|
|
55
|
+
|
|
56
|
+
env = os.environ.copy()
|
|
57
|
+
existing_pp = env.get("PYTHONPATH", "")
|
|
58
|
+
env["PYTHONPATH"] = (
|
|
59
|
+
f"{scanner_dir / 'src'}{os.pathsep}{existing_pp}" if existing_pp else str(scanner_dir / "src")
|
|
60
|
+
)
|
|
61
|
+
return subprocess.call(cmd, env=env)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _resolve_policy(target: Path, scanner_dir: Path, policy: str) -> Path:
|
|
65
|
+
"""Resolve a policy name to a JSON path.
|
|
66
|
+
|
|
67
|
+
Lookup order:
|
|
68
|
+
1. If `policy` looks like a path (contains `/` or `\\` or ends in `.json`),
|
|
69
|
+
treat it as a literal file path and use it as-is.
|
|
70
|
+
2. Otherwise, treat it as a basename and search the safe lookup chain:
|
|
71
|
+
`<target>/.brigade/policies/<policy>.json`, then
|
|
72
|
+
`<scanner_dir>/policies/<policy>.json`.
|
|
73
|
+
"""
|
|
74
|
+
looks_like_path = "/" in policy or "\\" in policy or policy.endswith(".json")
|
|
75
|
+
if looks_like_path:
|
|
76
|
+
return Path(policy)
|
|
77
|
+
|
|
78
|
+
# Bare name: must be a simple slug, no path segments.
|
|
79
|
+
safe = policy.strip()
|
|
80
|
+
if not safe or any(c in safe for c in ("/", "\\", "..")):
|
|
81
|
+
raise ValueError(f"unsafe policy name: {policy!r}")
|
|
82
|
+
|
|
83
|
+
candidates = [
|
|
84
|
+
target / ".brigade" / "policies" / f"{safe}.json",
|
|
85
|
+
scanner_dir / "policies" / f"{safe}.json",
|
|
86
|
+
]
|
|
87
|
+
for c in candidates:
|
|
88
|
+
if c.is_file():
|
|
89
|
+
return c
|
|
90
|
+
return candidates[0] # caller prints "not found" with this path
|
brigade/selection.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Selection data model: depth + harnesses + owner + includes."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import List, Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
KNOWN_DEPTHS = ("repo", "workspace")
|
|
9
|
+
KNOWN_HARNESSES = ("claude", "codex", "openclaw", "hermes")
|
|
10
|
+
KNOWN_INCLUDES = ("publisher",)
|
|
11
|
+
|
|
12
|
+
# Higher priority owners come first. The first harness in this list that
|
|
13
|
+
# also appears in the selection becomes the canonical memory owner unless
|
|
14
|
+
# the user passes --owner.
|
|
15
|
+
HARNESS_PRIORITY = ["openclaw", "hermes", "claude", "codex", "this-repo"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class Selection:
|
|
20
|
+
depth: str
|
|
21
|
+
harnesses: List[str] = field(default_factory=list)
|
|
22
|
+
owner: str = "this-repo"
|
|
23
|
+
includes: List[str] = field(default_factory=list)
|
|
24
|
+
|
|
25
|
+
def validate(self) -> None:
|
|
26
|
+
if self.depth not in KNOWN_DEPTHS:
|
|
27
|
+
raise ValueError(
|
|
28
|
+
f"unknown depth: {self.depth!r} (valid: {KNOWN_DEPTHS})"
|
|
29
|
+
)
|
|
30
|
+
for h in self.harnesses:
|
|
31
|
+
if h not in KNOWN_HARNESSES:
|
|
32
|
+
raise ValueError(
|
|
33
|
+
f"unknown harness: {h!r} (valid: {KNOWN_HARNESSES})"
|
|
34
|
+
)
|
|
35
|
+
for inc in self.includes:
|
|
36
|
+
if inc not in KNOWN_INCLUDES:
|
|
37
|
+
raise ValueError(
|
|
38
|
+
f"unknown include: {inc!r} (valid: {KNOWN_INCLUDES})"
|
|
39
|
+
)
|
|
40
|
+
if self.owner != "this-repo" and self.owner not in self.harnesses:
|
|
41
|
+
raise ValueError(
|
|
42
|
+
f"owner {self.owner!r} not in selected harnesses {self.harnesses}"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def resolve_owner(harnesses: List[str], override: Optional[str] = None) -> str:
|
|
47
|
+
"""Pick the canonical memory owner.
|
|
48
|
+
|
|
49
|
+
If override is provided, it must be 'this-repo' or one of the selected
|
|
50
|
+
harnesses. Otherwise the first entry in HARNESS_PRIORITY that also appears
|
|
51
|
+
in `harnesses` wins; if none match, returns 'this-repo'.
|
|
52
|
+
"""
|
|
53
|
+
if override is not None:
|
|
54
|
+
if override == "this-repo":
|
|
55
|
+
return override
|
|
56
|
+
if override not in harnesses:
|
|
57
|
+
raise ValueError(
|
|
58
|
+
f"owner override {override!r} not in selected harnesses {harnesses}"
|
|
59
|
+
)
|
|
60
|
+
return override
|
|
61
|
+
for candidate in HARNESS_PRIORITY:
|
|
62
|
+
if candidate == "this-repo":
|
|
63
|
+
continue
|
|
64
|
+
if candidate in harnesses:
|
|
65
|
+
return candidate
|
|
66
|
+
return "this-repo"
|
brigade/station.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""The station contract: a registered, health-checkable unit of Brigade."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Callable, List, Optional, Tuple
|
|
7
|
+
|
|
8
|
+
# (status, name, detail), reusing the doctor vocabulary.
|
|
9
|
+
CheckResult = Tuple[str, str, str]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class DoctorContext:
|
|
14
|
+
"""Resolved workspace facts shared across station doctors."""
|
|
15
|
+
target: Path
|
|
16
|
+
selection: object | None # brigade.selection.Selection | None (avoids import cycle)
|
|
17
|
+
harnesses: List[str] = field(default_factory=list)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class Station:
|
|
22
|
+
"""A unit of Brigade.
|
|
23
|
+
|
|
24
|
+
v1 stations are all built-in: their behavior lives inside the brigade
|
|
25
|
+
package and only `doctor` is exercised. Managed stations (install/wire/
|
|
26
|
+
verbs) arrive in Plan 2 and will extend this contract; do not add those
|
|
27
|
+
fields until they are needed (YAGNI).
|
|
28
|
+
"""
|
|
29
|
+
name: str
|
|
30
|
+
summary: str
|
|
31
|
+
doctor: Optional[Callable[[DoctorContext], List[CheckResult]]]
|
|
32
|
+
aliases: Tuple[str, ...] = ()
|
|
33
|
+
kind: str = "builtin"
|
|
34
|
+
|
|
35
|
+
def matches(self, name_or_alias: str) -> bool:
|
|
36
|
+
return name_or_alias == self.name or name_or_alias in self.aliases
|
brigade/status.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""`brigade status` - show which stations are present and healthy."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from . import doctor as _doctor
|
|
7
|
+
from .registry import all_stations
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def run(target: Path) -> int:
|
|
11
|
+
ctx = _doctor.build_context(target)
|
|
12
|
+
print(f"brigade status: {ctx.target}")
|
|
13
|
+
width = max((len(s.name) for s in all_stations()), default=8)
|
|
14
|
+
for station in all_stations():
|
|
15
|
+
checks = station.doctor(ctx) if station.doctor else []
|
|
16
|
+
ok = sum(1 for s, _, _ in checks if s == _doctor.OK)
|
|
17
|
+
warn = sum(1 for s, _, _ in checks if s == _doctor.WARN)
|
|
18
|
+
fail = sum(1 for s, _, _ in checks if s == _doctor.FAIL)
|
|
19
|
+
health = "issues" if fail else ("ok" if ok else "empty")
|
|
20
|
+
print(
|
|
21
|
+
f" {station.name.ljust(width)} [{health}] "
|
|
22
|
+
f"{ok} ok, {warn} warn, {fail} fail - {station.summary}"
|
|
23
|
+
)
|
|
24
|
+
return 0
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Memory Handoff
|
|
2
|
+
|
|
3
|
+
## Type
|
|
4
|
+
|
|
5
|
+
setup | workflow | bugfix | decision | security | preference | research | project-context
|
|
6
|
+
|
|
7
|
+
## Title
|
|
8
|
+
|
|
9
|
+
Short, specific title. No private identifiers.
|
|
10
|
+
|
|
11
|
+
## Summary
|
|
12
|
+
|
|
13
|
+
2-4 sentences. What happened and why it matters for future sessions.
|
|
14
|
+
|
|
15
|
+
## Durable facts
|
|
16
|
+
|
|
17
|
+
- Fact 1
|
|
18
|
+
- Fact 2
|
|
19
|
+
- Fact 3
|
|
20
|
+
|
|
21
|
+
## Evidence
|
|
22
|
+
|
|
23
|
+
- files changed: `path/to/file`
|
|
24
|
+
- commands run: `the exact command`
|
|
25
|
+
- error strings: `verbatim error`
|
|
26
|
+
|
|
27
|
+
## Recommended memory action
|
|
28
|
+
|
|
29
|
+
create-card | update-card | no-card
|
|
30
|
+
|
|
31
|
+
## Target card
|
|
32
|
+
|
|
33
|
+
card-name-if-known.md
|
|
34
|
+
|
|
35
|
+
## Suggested card content
|
|
36
|
+
|
|
37
|
+
If `create-card` or `update-card`, paste the exact card content here. Must start with YAML frontmatter:
|
|
38
|
+
|
|
39
|
+
```markdown
|
|
40
|
+
---
|
|
41
|
+
topic: ...
|
|
42
|
+
category: ...
|
|
43
|
+
tags: [list]
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
# Card title
|
|
47
|
+
|
|
48
|
+
Card body. Use ### or deeper for subsections; ## headings parse as new handoff sections.
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Target document
|
|
52
|
+
|
|
53
|
+
If `no-card`, name one of: `TOOLS.md` | `USER.md` | `rules/<name>.md` | `.learnings/LEARNINGS.md` | `.learnings/ERRORS.md` | `.learnings/FEATURE_REQUESTS.md`
|
|
54
|
+
|
|
55
|
+
## Suggested document content
|
|
56
|
+
|
|
57
|
+
If `no-card`, the exact text to append. No `##` headings (use `###` or deeper).
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Memory Handoff
|
|
2
|
+
|
|
3
|
+
## Type
|
|
4
|
+
|
|
5
|
+
setup | workflow | bugfix | decision | security | preference | research | project-context
|
|
6
|
+
|
|
7
|
+
## Title
|
|
8
|
+
|
|
9
|
+
Short, specific title. No private identifiers.
|
|
10
|
+
|
|
11
|
+
## Summary
|
|
12
|
+
|
|
13
|
+
2-4 sentences. What happened and why it matters for future sessions.
|
|
14
|
+
|
|
15
|
+
## Durable facts
|
|
16
|
+
|
|
17
|
+
- Fact 1
|
|
18
|
+
- Fact 2
|
|
19
|
+
- Fact 3
|
|
20
|
+
|
|
21
|
+
## Evidence
|
|
22
|
+
|
|
23
|
+
- files changed: `path/to/file`
|
|
24
|
+
- commands run: `the exact command`
|
|
25
|
+
- error strings: `verbatim error`
|
|
26
|
+
|
|
27
|
+
## Recommended memory action
|
|
28
|
+
|
|
29
|
+
create-card | update-card | no-card
|
|
30
|
+
|
|
31
|
+
## Target card
|
|
32
|
+
|
|
33
|
+
card-name-if-known.md
|
|
34
|
+
|
|
35
|
+
## Suggested card content
|
|
36
|
+
|
|
37
|
+
If `create-card` or `update-card`, paste the exact card content here. Must start with YAML frontmatter:
|
|
38
|
+
|
|
39
|
+
```markdown
|
|
40
|
+
---
|
|
41
|
+
topic: ...
|
|
42
|
+
category: ...
|
|
43
|
+
tags: [list]
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
# Card title
|
|
47
|
+
|
|
48
|
+
Card body. Use ### or deeper for subsections; ## headings parse as new handoff sections.
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Target document
|
|
52
|
+
|
|
53
|
+
If `no-card`, name one of: `TOOLS.md` | `USER.md` | `rules/<name>.md` | `.learnings/LEARNINGS.md` | `.learnings/ERRORS.md` | `.learnings/FEATURE_REQUESTS.md`
|
|
54
|
+
|
|
55
|
+
## Suggested document content
|
|
56
|
+
|
|
57
|
+
If `no-card`, the exact text to append. No `##` headings (use `###` or deeper).
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "repo",
|
|
3
|
+
"description": "Repo-local baseline: harness-neutral bootstrap files + publish guard.",
|
|
4
|
+
"files": [
|
|
5
|
+
{"src": "workspace/AGENTS.md", "dst": "AGENTS.md"},
|
|
6
|
+
{"src": "workspace/SAFETY_RULES.md", "dst": "SAFETY_RULES.md"},
|
|
7
|
+
{"src": "workspace/INSTALL_FOR_AGENTS.md", "dst": "INSTALL_FOR_AGENTS.md"},
|
|
8
|
+
{"src": "hooks/pre-push", "dst": "hooks/pre-push", "mode": "0755"},
|
|
9
|
+
{"src": "policies/public-repo.json", "dst": ".brigade/policies/public-repo.json"}
|
|
10
|
+
],
|
|
11
|
+
"dirs": []
|
|
12
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "workspace",
|
|
3
|
+
"description": "Full agent kitchen: extends repo baseline + memory folders + starter cards + safety files.",
|
|
4
|
+
"extends": "repo",
|
|
5
|
+
"files": [
|
|
6
|
+
{"src": "workspace/MEMORY.md", "dst": "MEMORY.md"},
|
|
7
|
+
{"src": "workspace/TOOLS.md", "dst": "TOOLS.md"},
|
|
8
|
+
{"src": "workspace/USER.md", "dst": "USER.md"},
|
|
9
|
+
{"src": "workspace/SOUL.md", "dst": "SOUL.md"},
|
|
10
|
+
{"src": "workspace/IDENTITY.md", "dst": "IDENTITY.md"},
|
|
11
|
+
{"src": "workspace/HEARTBEAT.md", "dst": "HEARTBEAT.md"},
|
|
12
|
+
{"src": "memory/cards/memory-architecture.md", "dst": "memory/cards/memory-architecture.md"},
|
|
13
|
+
{"src": "memory/cards/handoff-flow.md", "dst": "memory/cards/handoff-flow.md"},
|
|
14
|
+
{"src": "memory/cards/content-safety.md", "dst": "memory/cards/content-safety.md"},
|
|
15
|
+
{"src": "memory/cards/memory-scanner.md", "dst": "memory/cards/memory-scanner.md"},
|
|
16
|
+
{"src": "memory/cards/memory-care-staleness.md", "dst": "memory/cards/memory-care-staleness.md"},
|
|
17
|
+
{"src": "memory/cards/multi-workspace-handoff-admin.md", "dst": "memory/cards/multi-workspace-handoff-admin.md"},
|
|
18
|
+
{"src": "memory/cards/tokenjuice-output-compaction.md", "dst": "memory/cards/tokenjuice-output-compaction.md"},
|
|
19
|
+
{"src": "memory/cards/chat-surface-crawlers.md", "dst": "memory/cards/chat-surface-crawlers.md"},
|
|
20
|
+
{"src": "memory/cards/pipeline-standups.md", "dst": "memory/cards/pipeline-standups.md"},
|
|
21
|
+
{"src": "memory/cards/obsidian-notes.md", "dst": "memory/cards/obsidian-notes.md"},
|
|
22
|
+
{"src": "memory/cards/backup-restic.md", "dst": "memory/cards/backup-restic.md"},
|
|
23
|
+
{"src": "skills/note/SKILL.md", "dst": "skills/note/SKILL.md"},
|
|
24
|
+
{"src": "scripts/backup-restic.sh", "dst": "scripts/backup-restic.sh", "mode": "0755"}
|
|
25
|
+
],
|
|
26
|
+
"dirs": [
|
|
27
|
+
"memory/cards/decay",
|
|
28
|
+
"memory/handoff-inbox"
|
|
29
|
+
]
|
|
30
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Harness Adapter Checklist
|
|
2
|
+
|
|
3
|
+
Use this when wiring a new harness into the `brigade` contract.
|
|
4
|
+
|
|
5
|
+
## 1. Bootstrap loading
|
|
6
|
+
|
|
7
|
+
- [ ] Identify which file the harness loads first when starting a session.
|
|
8
|
+
- [ ] Confirm the harness can be configured to load `AGENTS.md` + (its own bridge file).
|
|
9
|
+
- [ ] If the harness has its own preference file (e.g. `CLAUDE.md`, `GEMINI.md`, `AGENTS.md`), keep both: ours and theirs.
|
|
10
|
+
|
|
11
|
+
## 2. Memory ownership
|
|
12
|
+
|
|
13
|
+
- [ ] Decide who owns canonical durable memory. Often this is the harness with the longest-lived context.
|
|
14
|
+
- [ ] Configure all *other* harnesses to write handoffs, not direct memory edits.
|
|
15
|
+
- [ ] If multiple harnesses think they own memory, you have a bug; pick one.
|
|
16
|
+
|
|
17
|
+
## 3. Handoff inbox
|
|
18
|
+
|
|
19
|
+
- [ ] Create `.claude/memory-handoffs/` in every repo the harness works in.
|
|
20
|
+
- [ ] Drop the closeout instruction into the harness's instruction file.
|
|
21
|
+
- [ ] Test by running a small task and looking for an emitted handoff.
|
|
22
|
+
|
|
23
|
+
## 4. Routing
|
|
24
|
+
|
|
25
|
+
- [ ] Confirm `brigade ingest --target <workspace> --dry-run` finds the handoffs.
|
|
26
|
+
- [ ] Confirm the auto-promote rules match the handoffs the harness actually writes (filename regex + frontmatter).
|
|
27
|
+
- [ ] Watch `memory/handoff-inbox/` for a week; if it fills up, refine handoff quality, do not loosen the rules.
|
|
28
|
+
|
|
29
|
+
## 5. Doctor
|
|
30
|
+
|
|
31
|
+
- [ ] Run `brigade doctor --target <workspace> --harness <harness>`.
|
|
32
|
+
- [ ] All checks `OK` or explicitly `MANUAL ACTION NEEDED` with a follow-up step.
|
|
33
|
+
|
|
34
|
+
## 6. Publish gate
|
|
35
|
+
|
|
36
|
+
- [ ] `hooks/pre-push` installed.
|
|
37
|
+
- [ ] `git config core.hooksPath hooks` run once.
|
|
38
|
+
- [ ] `content-guard` installed and policy points at `public-repo.json` or stricter.
|
|
39
|
+
|
|
40
|
+
## 7. Verification
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# Bootstrap files in place
|
|
44
|
+
ls AGENTS.md CLAUDE.md
|
|
45
|
+
|
|
46
|
+
# Handoff infrastructure in place
|
|
47
|
+
ls .claude/memory-handoffs/TEMPLATE.md
|
|
48
|
+
ls memory/cards/
|
|
49
|
+
|
|
50
|
+
# Ingest loop alive (after writing a sample handoff)
|
|
51
|
+
brigade ingest --target . --dry-run
|
|
52
|
+
|
|
53
|
+
# Publish gate live
|
|
54
|
+
hooks/pre-push </dev/null || echo "(hook needs git push to exercise)"
|
|
55
|
+
```
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Harness Memory Contract
|
|
2
|
+
|
|
3
|
+
Any harness that wants to plug into `brigade` must answer six questions.
|
|
4
|
+
|
|
5
|
+
| Contract field | Meaning |
|
|
6
|
+
|----------------|---------|
|
|
7
|
+
| `bootstrap_files` | Files loaded into the agent's starting context |
|
|
8
|
+
| `memory_owner` | System responsible for canonical durable memory |
|
|
9
|
+
| `handoff_inbox` | Directory where side harnesses write handoff markdown |
|
|
10
|
+
| `routing_targets` | Allowed memory outputs: cards, tools, user prefs, rules, learnings |
|
|
11
|
+
| `doctor_checks` | Commands that prove the harness can see the expected files |
|
|
12
|
+
| `publish_gate` | Content-guard or equivalent scan before public output leaves the repo |
|
|
13
|
+
|
|
14
|
+
## Reference mapping (OpenClaw)
|
|
15
|
+
|
|
16
|
+
| Field | Value |
|
|
17
|
+
|-------|-------|
|
|
18
|
+
| `bootstrap_files` | `AGENTS.md`, `CLAUDE.md`, `SOUL.md`, `USER.md`, `TOOLS.md`, `MEMORY.md`, `IDENTITY.md`, `HEARTBEAT.md`, `SAFETY_RULES.md`, `INSTALL_FOR_AGENTS.md` |
|
|
19
|
+
| `memory_owner` | OpenClaw workspace at `~/.openclaw/workspace/` |
|
|
20
|
+
| `handoff_inbox` | `.claude/memory-handoffs/` in each repo |
|
|
21
|
+
| `routing_targets` | `memory/cards/*.md`, `TOOLS.md`, `USER.md`, `rules/*.md`, `.learnings/*.md` |
|
|
22
|
+
| `doctor_checks` | Existence of bootstrap files + jq queries against `openclaw.json` |
|
|
23
|
+
| `publish_gate` | `content-guard` via `hooks/pre-push` |
|
|
24
|
+
|
|
25
|
+
## Reference mapping (Hermes - experimental)
|
|
26
|
+
|
|
27
|
+
Same shape; map `memory_owner` to your Hermes config root and `doctor_checks` to whatever Hermes exposes.
|
|
28
|
+
|
|
29
|
+
## Reference mapping (generic / no orchestrator)
|
|
30
|
+
|
|
31
|
+
If you do not have an orchestrator yet, the contract still works:
|
|
32
|
+
|
|
33
|
+
| Field | Value |
|
|
34
|
+
|-------|-------|
|
|
35
|
+
| `memory_owner` | this repo until you wire one |
|
|
36
|
+
| `handoff_inbox` | `.claude/memory-handoffs/` |
|
|
37
|
+
| `routing_targets` | same |
|
|
38
|
+
| `doctor_checks` | `brigade doctor --target . --harness generic` |
|
|
39
|
+
| `publish_gate` | `brigade scrub` and `hooks/pre-push` |
|
|
40
|
+
|
|
41
|
+
`brigade ingest --target .` is enough on its own; you do not need OpenClaw or Hermes installed to run the loop.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "claude",
|
|
3
|
+
"role": "writer",
|
|
4
|
+
"description": "Claude Code bridge + handoff inbox.",
|
|
5
|
+
"files": [
|
|
6
|
+
{"src": "workspace/CLAUDE.md", "dst": "CLAUDE.md"},
|
|
7
|
+
{"src": "claude/memory-handoffs/TEMPLATE.md", "dst": ".claude/memory-handoffs/TEMPLATE.md"}
|
|
8
|
+
],
|
|
9
|
+
"dirs": [
|
|
10
|
+
".claude/memory-handoffs/processed"
|
|
11
|
+
]
|
|
12
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "codex",
|
|
3
|
+
"role": "writer",
|
|
4
|
+
"description": "Codex handoff inbox. (AGENTS.md is in the depth baseline; no separate bridge file today.)",
|
|
5
|
+
"files": [
|
|
6
|
+
{"src": "codex/memory-handoffs/TEMPLATE.md", "dst": ".codex/memory-handoffs/TEMPLATE.md"}
|
|
7
|
+
],
|
|
8
|
+
"dirs": [
|
|
9
|
+
".codex/memory-handoffs/processed"
|
|
10
|
+
]
|
|
11
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "hermes",
|
|
3
|
+
"role": "reader",
|
|
4
|
+
"description": "Hermes adapter fragments + doctor checks. Experimental.",
|
|
5
|
+
"files": [
|
|
6
|
+
{"src": "hermes/workspace.harness.json", "dst": ".brigade/hermes/workspace.harness.json"},
|
|
7
|
+
{"src": "hermes/memory-handoff.harness.json", "dst": ".brigade/hermes/memory-handoff.harness.json"},
|
|
8
|
+
{"src": "hermes/model-lanes.harness.json", "dst": ".brigade/hermes/model-lanes.harness.json"},
|
|
9
|
+
{"src": "hermes/README.md", "dst": ".brigade/hermes/README.md"}
|
|
10
|
+
],
|
|
11
|
+
"dirs": [],
|
|
12
|
+
"post_install_notes": [
|
|
13
|
+
"Hermes support is experimental. Validate against your real Hermes install before relying on it.",
|
|
14
|
+
"Open an issue with your config layout to help promote the adapter from experimental to tested."
|
|
15
|
+
]
|
|
16
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "openclaw",
|
|
3
|
+
"role": "reader",
|
|
4
|
+
"description": "OpenClaw config fragments + cron stubs + doctor checks.",
|
|
5
|
+
"files": [
|
|
6
|
+
{"src": "openclaw/model-aliases.openclaw.json", "dst": ".brigade/openclaw/model-aliases.openclaw.json"},
|
|
7
|
+
{"src": "openclaw/ollama-memory-search.openclaw.json", "dst": ".brigade/openclaw/ollama-memory-search.openclaw.json"},
|
|
8
|
+
{"src": "openclaw/acp-escalation.openclaw.json", "dst": ".brigade/openclaw/acp-escalation.openclaw.json"},
|
|
9
|
+
{"src": "openclaw/README.md", "dst": ".brigade/openclaw/README.md"}
|
|
10
|
+
],
|
|
11
|
+
"dirs": [],
|
|
12
|
+
"post_install_notes": [
|
|
13
|
+
"Inspect fragments under `.brigade/openclaw/` before merging into ~/.openclaw/openclaw.json.",
|
|
14
|
+
"Run `brigade doctor --target <workspace>` to confirm wiring.",
|
|
15
|
+
"Never let `openclaw doctor --fix` (the OpenClaw tool) rewrite provider prefixes blindly."
|
|
16
|
+
]
|
|
17
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Hermes Adapter (Experimental)
|
|
2
|
+
|
|
3
|
+
`brigade` supports Hermes through the same harness contract as OpenClaw. This adapter is **experimental** until it has been validated against a real Hermes install.
|
|
4
|
+
|
|
5
|
+
## What this gives you
|
|
6
|
+
|
|
7
|
+
- `workspace.harness.json` - which bootstrap files Hermes should load
|
|
8
|
+
- `memory-handoff.harness.json` - the handoff inbox and routing targets
|
|
9
|
+
- `model-lanes.harness.json` - suggested model alias names
|
|
10
|
+
|
|
11
|
+
## What it does not do yet
|
|
12
|
+
|
|
13
|
+
- Validate against the live Hermes config schema
|
|
14
|
+
- Generate Hermes-specific plugin entries
|
|
15
|
+
- Replace `brigade hermes doctor` with anything beyond file existence checks
|
|
16
|
+
|
|
17
|
+
## Contributing
|
|
18
|
+
|
|
19
|
+
If you run Hermes and have working config, open an issue at <https://github.com/solomonneas/brigade/issues> with:
|
|
20
|
+
|
|
21
|
+
- the file Hermes loads as its primary bootstrap file
|
|
22
|
+
- the path where Hermes expects memory handoffs (if any)
|
|
23
|
+
- the command that ingests handoffs into canonical memory
|
|
24
|
+
|
|
25
|
+
That lets the adapter be promoted from experimental to tested.
|