brigade-cli 0.7.0__tar.gz → 0.8.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.
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/PKG-INFO +55 -36
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/README.md +54 -35
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/pyproject.toml +1 -1
- brigade_cli-0.8.0/src/brigade/budgets.py +83 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/cli.py +14 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/doctor.py +5 -14
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/handoff_cmd.py +50 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/ingest.py +59 -11
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/repos_cmd.py +132 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/work_cmd.py +7 -3
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade_cli.egg-info/PKG-INFO +55 -36
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade_cli.egg-info/SOURCES.txt +2 -0
- brigade_cli-0.8.0/tests/test_budgets.py +32 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_handoff_cmd.py +50 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_ingest.py +62 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_repos_cmd.py +79 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/LICENSE +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/MANIFEST.in +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/QUICKSTART.md +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/setup.cfg +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/__init__.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/__main__.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/aboyeur.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/add.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/agents.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/center_cmd.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/chat_cmd.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/config.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/context_cmd.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/daily_cmd.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/dogfood_cmd.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/fragments.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/handoff.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/install.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/learn_cmd.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/managed.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/memory_cmd.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/phases_cmd.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/proc.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/projects_cmd.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/prompt.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/py.typed +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/reconfigure.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/registry.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/release_cmd.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/roadmap_cmd.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/roster.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/roster_cmd.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/runs_cmd.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/scrub.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/security_cmd.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/selection.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/station.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/status.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/claude/memory-handoffs/TEMPLATE.md +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/codex/memory-handoffs/TEMPLATE.md +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/depth/repo.json +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/depth/workspace.json +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/generic/harness-adapter-checklist.md +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/generic/memory-contract.md +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/handoff/handoff-sources.example.json +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/harnesses/claude.json +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/harnesses/codex.json +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/harnesses/hermes.json +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/harnesses/openclaw.json +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/hermes/README.md +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/hermes/memory-handoff.harness.json +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/hermes/model-lanes.harness.json +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/hermes/workspace.harness.json +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/hooks/pre-push +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/includes/publisher.json +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/memory/cards/backup-restic.md +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/memory/cards/chat-surface-crawlers.md +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/memory/cards/content-safety.md +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/memory/cards/handoff-flow.md +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/memory/cards/memory-architecture.md +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/memory/cards/memory-care-staleness.md +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/memory/cards/memory-scanner.md +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/memory/cards/multi-workspace-handoff-admin.md +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/memory/cards/obsidian-notes.md +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/memory/cards/pipeline-standups.md +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/memory/cards/tokenjuice-output-compaction.md +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/memory/chat-memory-sweep.example.json +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/memory/memory-care.example.json +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/openclaw/README.md +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/openclaw/acp-escalation.openclaw.json +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/openclaw/memory-sweep-cron.openclaw.json +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/openclaw/model-aliases.openclaw.json +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/openclaw/ollama-memory-search.openclaw.json +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/policies/public-content.json +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/policies/public-repo.json +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/scripts/backup-restic.sh +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/skills/note/SKILL.md +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/workspace/AGENTS.md +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/workspace/CLAUDE.md +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/workspace/HEARTBEAT.md +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/workspace/IDENTITY.md +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/workspace/INSTALL_FOR_AGENTS.md +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/workspace/MEMORY.md +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/workspace/SAFETY_RULES.md +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/workspace/SOUL.md +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/workspace/TOOLS.md +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/workspace/USER.md +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/workspace/rules/acceptance-driven-work.md +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/workspace/rules/issue-tdd-loop.md +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/toml_compat.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/tools_cmd.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade_cli.egg-info/dependency_links.txt +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade_cli.egg-info/entry_points.txt +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade_cli.egg-info/requires.txt +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade_cli.egg-info/top_level.txt +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_aboyeur.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_add.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_agents.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_cli_alias.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_config.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_doctor.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_dogfood_cmd.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_fragments.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_gitignore.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_handoff.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_init.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_install.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_managed.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_memory_cmd.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_neutrality.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_phase100_cmd.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_phase101_cmd.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_phase165_cmd.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_phase36_cmd.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_phase37_cmd.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_phase38_cmd.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_phase39_cmd.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_phase40_cmd.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_phase41_cmd.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_phase42_cmd.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_phase43_cmd.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_phase44_cmd.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_phase45_cmd.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_phase46_50_cmd.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_phase51_55_cmd.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_phase56_60_cmd.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_phase96_cmd.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_phase97_cmd.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_phase98_cmd.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_phase99_cmd.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_privacy_regression.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_proc.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_prompt.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_reconfigure.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_registry.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_release_cmd.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_roadmap_cmd.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_roster.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_roster_cmd.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_run_cli.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_runs_cmd.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_scrub.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_security_cmd.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_selection.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_station.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_status.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_toml_compat.py +0 -0
- {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_work_cmd.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: brigade-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.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
|
|
@@ -60,6 +60,52 @@ It is meant for people running real tools, real docs, and real automation across
|
|
|
60
60
|
|
|
61
61
|
The cookbook explains the why. This package gives you the kitchen.
|
|
62
62
|
|
|
63
|
+
## The design
|
|
64
|
+
|
|
65
|
+
One memory owner stays canonical.
|
|
66
|
+
That is typically OpenClaw or Hermes when present, otherwise `this-repo`.
|
|
67
|
+
Writer harnesses drop handoffs into their own inboxes, and the ingester scans all of them.
|
|
68
|
+
|
|
69
|
+
```mermaid
|
|
70
|
+
flowchart TB
|
|
71
|
+
CC["<b>Claude Code</b>"]
|
|
72
|
+
CX["<b>Codex</b>"]
|
|
73
|
+
CCI[".claude/memory-handoffs/"]
|
|
74
|
+
CXI[".codex/memory-handoffs/"]
|
|
75
|
+
CC --> CCI
|
|
76
|
+
CX --> CXI
|
|
77
|
+
|
|
78
|
+
ING(["<b>brigade ingest</b>"])
|
|
79
|
+
CCI --> ING
|
|
80
|
+
CXI --> ING
|
|
81
|
+
|
|
82
|
+
OUT["memory/cards/*.md · TOOLS.md · USER.md<br/>rules/*.md · .learnings/*.md"]
|
|
83
|
+
ING --> OUT
|
|
84
|
+
|
|
85
|
+
classDef harness fill:#e0f2fe,stroke:#0284c7,color:#075985;
|
|
86
|
+
classDef inbox fill:#f1f5f9,stroke:#94a3b8,color:#334155;
|
|
87
|
+
classDef ingest fill:#fef3c7,stroke:#d97706,color:#92400e;
|
|
88
|
+
classDef store fill:#dcfce7,stroke:#16a34a,color:#166534;
|
|
89
|
+
class CC,CX harness;
|
|
90
|
+
class CCI,CXI inbox;
|
|
91
|
+
class ING ingest;
|
|
92
|
+
class OUT store;
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The ingester is intentionally conservative.
|
|
96
|
+
Safe card handoffs become cards.
|
|
97
|
+
Targeted updates append to the right file.
|
|
98
|
+
Ambiguous material gets kicked out for review instead of being trusted automatically.
|
|
99
|
+
|
|
100
|
+
For users running multiple agent homes, treat the owner workspace as the hub.
|
|
101
|
+
Remote or secondary workspaces can write handoffs into their own per-harness inboxes.
|
|
102
|
+
A trusted sync can pull those files into a staging inbox on the owner.
|
|
103
|
+
That keeps agents informed without creating multiple canonical memories.
|
|
104
|
+
|
|
105
|
+
Token-heavy terminal work gets the same treatment.
|
|
106
|
+
Make the wrapper explicit, make the escape hatch obvious, and tell every harness what is happening.
|
|
107
|
+
The TokenJuice starter card documents Claude Code's PreToolUse wrapper path, Codex's hook setup, and the savings model.
|
|
108
|
+
|
|
63
109
|
## What you get
|
|
64
110
|
|
|
65
111
|
Brigade has grown from a bootstrap kit into a local control plane for agent work. The current public surface includes:
|
|
@@ -84,8 +130,10 @@ The installable source files live under `src/brigade/templates/`; root workspace
|
|
|
84
130
|
|
|
85
131
|
See [`ROADMAP.md`](ROADMAP.md) for the daily-driver, scanner inbox, chat-surface scanner, and memory-card decay roadmap. The active phase queue for roadmap completion hardening is tracked in [`docs/phase-61-100-plan.md`](docs/phase-61-100-plan.md).
|
|
86
132
|
The production-hardening queue for the daily operator system is tracked in [`docs/phase-115-164-plan.md`](docs/phase-115-164-plan.md).
|
|
133
|
+
|
|
87
134
|
Long unattended phase work is audited through the local phase execution ledger described in [`docs/phase-execution-ledger.md`](docs/phase-execution-ledger.md). Future multi-phase work is not complete unless each phase has ledger evidence or an explicit deferral.
|
|
88
135
|
Phase ledger closeouts let an operator mark completed phase evidence as reviewed, deferred, blocked, or archived, and stale unreviewed completed phases surface in doctor output.
|
|
136
|
+
|
|
89
137
|
Phase execution sessions group a declared AFK range into one local record with current phase, status, commit and test counts, report references, closeout state, and the next recommended command.
|
|
90
138
|
Session next/resume commands identify the safest next local command and record resume metadata without executing hidden work.
|
|
91
139
|
Session checkpoints record local recovery points with safe summaries, notes, current next-step state, and suggested commands without executing the suggested command.
|
|
@@ -93,6 +141,7 @@ Session checkpoint list/show/compare commands inspect those local recovery point
|
|
|
93
141
|
Session checkpoint import commands route blocked or stale checkpoint issues into the normal work inbox as deduped local tasks.
|
|
94
142
|
Session next/resume output includes the latest checkpoint summary and issue counts when checkpoint recovery metadata exists.
|
|
95
143
|
Session recovery notes record safe summaries, notes, and evidence labels for AFK resume context, with list/show/closeout commands and activity timeline entries.
|
|
144
|
+
|
|
96
145
|
Daily planning can surface checkpoint issues as local candidates that point at checkpoint import commands instead of hiding AFK recovery drift.
|
|
97
146
|
Daily run can also write one local phase session checkpoint as its single bounded action when the selected session needs safe AFK recovery metadata.
|
|
98
147
|
Session risk output summarizes next-step blockers, checkpoint drift, open recovery notes, and phase doctor issues in one read-only view.
|
|
@@ -100,6 +149,7 @@ Session verification output rolls up expected, passed, failed, skipped, and defe
|
|
|
100
149
|
Session privacy output rolls up clean, blocked, and missing privacy checks across a whole AFK session range.
|
|
101
150
|
Session handoff output rolls up linted, drafted, failed, deferred, and missing handoff evidence across a whole AFK session range.
|
|
102
151
|
Session report bundles collect the phase records, checks, actions, imports, commits, tests, and blockers into local Markdown and JSON evidence.
|
|
152
|
+
|
|
103
153
|
The daily driver can surface active phase sessions and run exactly one safe session step, such as building a session report or writing session closeout metadata.
|
|
104
154
|
Release and operator review surfaces include phase session state so stale or unreported AFK work blocks publish review visibly.
|
|
105
155
|
Release doctor also reports blocked or stale phase-session checkpoint evidence before publish review.
|
|
@@ -109,6 +159,7 @@ Work brief includes the latest phase-session checkpoint and compare summary in t
|
|
|
109
159
|
Phase action planning can turn blocked or stale phase-session checkpoint issues into local phase actions.
|
|
110
160
|
Session checkpoint archive moves old recovery points into local JSONL metadata so they stop driving latest-checkpoint health.
|
|
111
161
|
Session report bundles include a recovery section with checkpoint and recovery-note summaries.
|
|
162
|
+
|
|
112
163
|
`brigade work phases evidence add` appends local files, tests, report ids, handoff paths, and notes to a phase record without running commands.
|
|
113
164
|
`brigade work phases verify plan/record` keeps expected verification and recorded outcomes visible without executing tests.
|
|
114
165
|
`brigade work phases reconcile` checks recorded commit and push evidence against local git state without changing git.
|
|
@@ -119,13 +170,16 @@ Session report bundles include a recovery section with checkpoint and recovery-n
|
|
|
119
170
|
`brigade work phases session import-issues` routes unresolved AFK session blockers into the work inbox with phase-session provenance and dedupe.
|
|
120
171
|
`brigade work phases goal scaffold` writes a local editable `/goal` draft from ledger state, session evidence, blockers, and roadmap references without copying private evidence.
|
|
121
172
|
`brigade work phases session gate` is the final read-only AFK claim check, and release evidence includes its latest result.
|
|
173
|
+
|
|
122
174
|
Phase ledger compare checks make it clear when local HEAD, referenced files, reports, or doctor issue counts drift after a phase is recorded.
|
|
123
175
|
Phase ledger action queues turn those ledger issues into local metadata-only next steps without executing commands.
|
|
124
176
|
The daily driver can select those phase-ledger actions when they block AFK or release completion, then start one action or build one phase report as a bounded local step.
|
|
177
|
+
|
|
125
178
|
Release readiness and candidate compare include phase closeout and report references so publish review can catch unreviewed or stale phase evidence.
|
|
126
179
|
Phase report closeouts let an operator review, defer, supersede, or archive a generated phase report without changing its evidence.
|
|
127
180
|
Phase report compare checks saved report bundles against current ledger state before relying on them.
|
|
128
181
|
Work brief and center status include open phase action counts so ledger follow-ups stay visible in the daily loop.
|
|
182
|
+
|
|
129
183
|
Open phase actions can be imported into the normal work inbox when they need a reviewed task.
|
|
130
184
|
Release candidate evidence includes the latest phase report compare summary.
|
|
131
185
|
The current AFK ledger hardening tranche is described in [`docs/phase-226-250-plan.md`](docs/phase-226-250-plan.md).
|
|
@@ -1143,41 +1197,6 @@ The normal exception is your own configured tooling:
|
|
|
1143
1197
|
- the `pre-push` hook runs the local `content-guard` scanner before commits leave the machine
|
|
1144
1198
|
- `brigade security enrich` can call MISP only when you explicitly configure and run the `misp` provider
|
|
1145
1199
|
|
|
1146
|
-
## The design
|
|
1147
|
-
|
|
1148
|
-
One memory owner stays canonical.
|
|
1149
|
-
That is typically OpenClaw or Hermes when present, otherwise `this-repo`.
|
|
1150
|
-
Writer harnesses drop handoffs into their own inboxes, and the ingester scans all of them.
|
|
1151
|
-
|
|
1152
|
-
```text
|
|
1153
|
-
Claude Code Codex
|
|
1154
|
-
| |
|
|
1155
|
-
v v
|
|
1156
|
-
.claude/memory-handoffs/ .codex/memory-handoffs/
|
|
1157
|
-
\ /
|
|
1158
|
-
\ /
|
|
1159
|
-
v v
|
|
1160
|
-
brigade ingest
|
|
1161
|
-
|
|
|
1162
|
-
v
|
|
1163
|
-
memory/cards/*.md, TOOLS.md, USER.md,
|
|
1164
|
-
rules/*.md, .learnings/*.md
|
|
1165
|
-
```
|
|
1166
|
-
|
|
1167
|
-
The ingester is intentionally conservative.
|
|
1168
|
-
Safe card handoffs become cards.
|
|
1169
|
-
Targeted updates append to the right file.
|
|
1170
|
-
Ambiguous material gets kicked out for review instead of being trusted automatically.
|
|
1171
|
-
|
|
1172
|
-
For users running multiple agent homes, treat the owner workspace as the hub.
|
|
1173
|
-
Remote or secondary workspaces can write handoffs into their own per-harness inboxes.
|
|
1174
|
-
A trusted sync can pull those files into a staging inbox on the owner.
|
|
1175
|
-
That keeps agents informed without creating multiple canonical memories.
|
|
1176
|
-
|
|
1177
|
-
Token-heavy terminal work gets the same treatment.
|
|
1178
|
-
Make the wrapper explicit, make the escape hatch obvious, and tell every harness what is happening.
|
|
1179
|
-
The TokenJuice starter card documents Claude Code's PreToolUse wrapper path, Codex's hook setup, and the savings model.
|
|
1180
|
-
|
|
1181
1200
|
## Maintenance and utility commands
|
|
1182
1201
|
|
|
1183
1202
|
A few commands sit outside the daily loop:
|
|
@@ -37,6 +37,52 @@ It is meant for people running real tools, real docs, and real automation across
|
|
|
37
37
|
|
|
38
38
|
The cookbook explains the why. This package gives you the kitchen.
|
|
39
39
|
|
|
40
|
+
## The design
|
|
41
|
+
|
|
42
|
+
One memory owner stays canonical.
|
|
43
|
+
That is typically OpenClaw or Hermes when present, otherwise `this-repo`.
|
|
44
|
+
Writer harnesses drop handoffs into their own inboxes, and the ingester scans all of them.
|
|
45
|
+
|
|
46
|
+
```mermaid
|
|
47
|
+
flowchart TB
|
|
48
|
+
CC["<b>Claude Code</b>"]
|
|
49
|
+
CX["<b>Codex</b>"]
|
|
50
|
+
CCI[".claude/memory-handoffs/"]
|
|
51
|
+
CXI[".codex/memory-handoffs/"]
|
|
52
|
+
CC --> CCI
|
|
53
|
+
CX --> CXI
|
|
54
|
+
|
|
55
|
+
ING(["<b>brigade ingest</b>"])
|
|
56
|
+
CCI --> ING
|
|
57
|
+
CXI --> ING
|
|
58
|
+
|
|
59
|
+
OUT["memory/cards/*.md · TOOLS.md · USER.md<br/>rules/*.md · .learnings/*.md"]
|
|
60
|
+
ING --> OUT
|
|
61
|
+
|
|
62
|
+
classDef harness fill:#e0f2fe,stroke:#0284c7,color:#075985;
|
|
63
|
+
classDef inbox fill:#f1f5f9,stroke:#94a3b8,color:#334155;
|
|
64
|
+
classDef ingest fill:#fef3c7,stroke:#d97706,color:#92400e;
|
|
65
|
+
classDef store fill:#dcfce7,stroke:#16a34a,color:#166534;
|
|
66
|
+
class CC,CX harness;
|
|
67
|
+
class CCI,CXI inbox;
|
|
68
|
+
class ING ingest;
|
|
69
|
+
class OUT store;
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The ingester is intentionally conservative.
|
|
73
|
+
Safe card handoffs become cards.
|
|
74
|
+
Targeted updates append to the right file.
|
|
75
|
+
Ambiguous material gets kicked out for review instead of being trusted automatically.
|
|
76
|
+
|
|
77
|
+
For users running multiple agent homes, treat the owner workspace as the hub.
|
|
78
|
+
Remote or secondary workspaces can write handoffs into their own per-harness inboxes.
|
|
79
|
+
A trusted sync can pull those files into a staging inbox on the owner.
|
|
80
|
+
That keeps agents informed without creating multiple canonical memories.
|
|
81
|
+
|
|
82
|
+
Token-heavy terminal work gets the same treatment.
|
|
83
|
+
Make the wrapper explicit, make the escape hatch obvious, and tell every harness what is happening.
|
|
84
|
+
The TokenJuice starter card documents Claude Code's PreToolUse wrapper path, Codex's hook setup, and the savings model.
|
|
85
|
+
|
|
40
86
|
## What you get
|
|
41
87
|
|
|
42
88
|
Brigade has grown from a bootstrap kit into a local control plane for agent work. The current public surface includes:
|
|
@@ -61,8 +107,10 @@ The installable source files live under `src/brigade/templates/`; root workspace
|
|
|
61
107
|
|
|
62
108
|
See [`ROADMAP.md`](ROADMAP.md) for the daily-driver, scanner inbox, chat-surface scanner, and memory-card decay roadmap. The active phase queue for roadmap completion hardening is tracked in [`docs/phase-61-100-plan.md`](docs/phase-61-100-plan.md).
|
|
63
109
|
The production-hardening queue for the daily operator system is tracked in [`docs/phase-115-164-plan.md`](docs/phase-115-164-plan.md).
|
|
110
|
+
|
|
64
111
|
Long unattended phase work is audited through the local phase execution ledger described in [`docs/phase-execution-ledger.md`](docs/phase-execution-ledger.md). Future multi-phase work is not complete unless each phase has ledger evidence or an explicit deferral.
|
|
65
112
|
Phase ledger closeouts let an operator mark completed phase evidence as reviewed, deferred, blocked, or archived, and stale unreviewed completed phases surface in doctor output.
|
|
113
|
+
|
|
66
114
|
Phase execution sessions group a declared AFK range into one local record with current phase, status, commit and test counts, report references, closeout state, and the next recommended command.
|
|
67
115
|
Session next/resume commands identify the safest next local command and record resume metadata without executing hidden work.
|
|
68
116
|
Session checkpoints record local recovery points with safe summaries, notes, current next-step state, and suggested commands without executing the suggested command.
|
|
@@ -70,6 +118,7 @@ Session checkpoint list/show/compare commands inspect those local recovery point
|
|
|
70
118
|
Session checkpoint import commands route blocked or stale checkpoint issues into the normal work inbox as deduped local tasks.
|
|
71
119
|
Session next/resume output includes the latest checkpoint summary and issue counts when checkpoint recovery metadata exists.
|
|
72
120
|
Session recovery notes record safe summaries, notes, and evidence labels for AFK resume context, with list/show/closeout commands and activity timeline entries.
|
|
121
|
+
|
|
73
122
|
Daily planning can surface checkpoint issues as local candidates that point at checkpoint import commands instead of hiding AFK recovery drift.
|
|
74
123
|
Daily run can also write one local phase session checkpoint as its single bounded action when the selected session needs safe AFK recovery metadata.
|
|
75
124
|
Session risk output summarizes next-step blockers, checkpoint drift, open recovery notes, and phase doctor issues in one read-only view.
|
|
@@ -77,6 +126,7 @@ Session verification output rolls up expected, passed, failed, skipped, and defe
|
|
|
77
126
|
Session privacy output rolls up clean, blocked, and missing privacy checks across a whole AFK session range.
|
|
78
127
|
Session handoff output rolls up linted, drafted, failed, deferred, and missing handoff evidence across a whole AFK session range.
|
|
79
128
|
Session report bundles collect the phase records, checks, actions, imports, commits, tests, and blockers into local Markdown and JSON evidence.
|
|
129
|
+
|
|
80
130
|
The daily driver can surface active phase sessions and run exactly one safe session step, such as building a session report or writing session closeout metadata.
|
|
81
131
|
Release and operator review surfaces include phase session state so stale or unreported AFK work blocks publish review visibly.
|
|
82
132
|
Release doctor also reports blocked or stale phase-session checkpoint evidence before publish review.
|
|
@@ -86,6 +136,7 @@ Work brief includes the latest phase-session checkpoint and compare summary in t
|
|
|
86
136
|
Phase action planning can turn blocked or stale phase-session checkpoint issues into local phase actions.
|
|
87
137
|
Session checkpoint archive moves old recovery points into local JSONL metadata so they stop driving latest-checkpoint health.
|
|
88
138
|
Session report bundles include a recovery section with checkpoint and recovery-note summaries.
|
|
139
|
+
|
|
89
140
|
`brigade work phases evidence add` appends local files, tests, report ids, handoff paths, and notes to a phase record without running commands.
|
|
90
141
|
`brigade work phases verify plan/record` keeps expected verification and recorded outcomes visible without executing tests.
|
|
91
142
|
`brigade work phases reconcile` checks recorded commit and push evidence against local git state without changing git.
|
|
@@ -96,13 +147,16 @@ Session report bundles include a recovery section with checkpoint and recovery-n
|
|
|
96
147
|
`brigade work phases session import-issues` routes unresolved AFK session blockers into the work inbox with phase-session provenance and dedupe.
|
|
97
148
|
`brigade work phases goal scaffold` writes a local editable `/goal` draft from ledger state, session evidence, blockers, and roadmap references without copying private evidence.
|
|
98
149
|
`brigade work phases session gate` is the final read-only AFK claim check, and release evidence includes its latest result.
|
|
150
|
+
|
|
99
151
|
Phase ledger compare checks make it clear when local HEAD, referenced files, reports, or doctor issue counts drift after a phase is recorded.
|
|
100
152
|
Phase ledger action queues turn those ledger issues into local metadata-only next steps without executing commands.
|
|
101
153
|
The daily driver can select those phase-ledger actions when they block AFK or release completion, then start one action or build one phase report as a bounded local step.
|
|
154
|
+
|
|
102
155
|
Release readiness and candidate compare include phase closeout and report references so publish review can catch unreviewed or stale phase evidence.
|
|
103
156
|
Phase report closeouts let an operator review, defer, supersede, or archive a generated phase report without changing its evidence.
|
|
104
157
|
Phase report compare checks saved report bundles against current ledger state before relying on them.
|
|
105
158
|
Work brief and center status include open phase action counts so ledger follow-ups stay visible in the daily loop.
|
|
159
|
+
|
|
106
160
|
Open phase actions can be imported into the normal work inbox when they need a reviewed task.
|
|
107
161
|
Release candidate evidence includes the latest phase report compare summary.
|
|
108
162
|
The current AFK ledger hardening tranche is described in [`docs/phase-226-250-plan.md`](docs/phase-226-250-plan.md).
|
|
@@ -1120,41 +1174,6 @@ The normal exception is your own configured tooling:
|
|
|
1120
1174
|
- the `pre-push` hook runs the local `content-guard` scanner before commits leave the machine
|
|
1121
1175
|
- `brigade security enrich` can call MISP only when you explicitly configure and run the `misp` provider
|
|
1122
1176
|
|
|
1123
|
-
## The design
|
|
1124
|
-
|
|
1125
|
-
One memory owner stays canonical.
|
|
1126
|
-
That is typically OpenClaw or Hermes when present, otherwise `this-repo`.
|
|
1127
|
-
Writer harnesses drop handoffs into their own inboxes, and the ingester scans all of them.
|
|
1128
|
-
|
|
1129
|
-
```text
|
|
1130
|
-
Claude Code Codex
|
|
1131
|
-
| |
|
|
1132
|
-
v v
|
|
1133
|
-
.claude/memory-handoffs/ .codex/memory-handoffs/
|
|
1134
|
-
\ /
|
|
1135
|
-
\ /
|
|
1136
|
-
v v
|
|
1137
|
-
brigade ingest
|
|
1138
|
-
|
|
|
1139
|
-
v
|
|
1140
|
-
memory/cards/*.md, TOOLS.md, USER.md,
|
|
1141
|
-
rules/*.md, .learnings/*.md
|
|
1142
|
-
```
|
|
1143
|
-
|
|
1144
|
-
The ingester is intentionally conservative.
|
|
1145
|
-
Safe card handoffs become cards.
|
|
1146
|
-
Targeted updates append to the right file.
|
|
1147
|
-
Ambiguous material gets kicked out for review instead of being trusted automatically.
|
|
1148
|
-
|
|
1149
|
-
For users running multiple agent homes, treat the owner workspace as the hub.
|
|
1150
|
-
Remote or secondary workspaces can write handoffs into their own per-harness inboxes.
|
|
1151
|
-
A trusted sync can pull those files into a staging inbox on the owner.
|
|
1152
|
-
That keeps agents informed without creating multiple canonical memories.
|
|
1153
|
-
|
|
1154
|
-
Token-heavy terminal work gets the same treatment.
|
|
1155
|
-
Make the wrapper explicit, make the escape hatch obvious, and tell every harness what is happening.
|
|
1156
|
-
The TokenJuice starter card documents Claude Code's PreToolUse wrapper path, Codex's hook setup, and the savings model.
|
|
1157
|
-
|
|
1158
1177
|
## Maintenance and utility commands
|
|
1159
1178
|
|
|
1160
1179
|
A few commands sit outside the daily loop:
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "brigade-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.8.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"
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Canonical size/staleness budgets for the brigade operator system.
|
|
2
|
+
|
|
3
|
+
This is the single source of truth for the numbers that govern how much content
|
|
4
|
+
may live in bootstrap files and memory cards, how long handoffs may sit before
|
|
5
|
+
they count as a stalled backlog, and how stale a memory-care scan may get.
|
|
6
|
+
|
|
7
|
+
brigade's own `doctor`, `ingest`, `handoff`, and `repos` stations all import
|
|
8
|
+
from here so the preventive guards and the post-hoc warnings can never disagree.
|
|
9
|
+
Satellite tools (bootstrap-doctor, memory-doctor) are intended to depend on
|
|
10
|
+
brigade and consume these definitions rather than redeclaring them, so updating
|
|
11
|
+
a budget here updates every downstream consumer.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
# --- Bootstrap files -------------------------------------------------------
|
|
18
|
+
# OpenClaw loads these into the session prefix every turn. There is an empirical
|
|
19
|
+
# soft ceiling around 12,000 chars per file before content is silently truncated
|
|
20
|
+
# mid-session. Per-file budgets stay below that with headroom.
|
|
21
|
+
BOOTSTRAP_BUDGETS: dict[str, int] = {
|
|
22
|
+
"AGENTS.md": 12_000,
|
|
23
|
+
"CLAUDE.md": 6_000,
|
|
24
|
+
"MEMORY.md": 7_000,
|
|
25
|
+
"TOOLS.md": 10_000,
|
|
26
|
+
"USER.md": 8_000,
|
|
27
|
+
"SAFETY_RULES.md": 10_000,
|
|
28
|
+
"INSTALL_FOR_AGENTS.md": 8_000,
|
|
29
|
+
"SOUL.md": 8_000,
|
|
30
|
+
"IDENTITY.md": 4_000,
|
|
31
|
+
"HEARTBEAT.md": 5_000,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# Flat whole-file thresholds for the simpler bootstrap auditor model (one soft
|
|
35
|
+
# warning level and one hard limit applied across tracked files), as opposed to
|
|
36
|
+
# the per-file BOOTSTRAP_BUDGETS above. Canonical here so the bootstrap-doctor
|
|
37
|
+
# satellite sources them instead of redeclaring its own (which had drifted).
|
|
38
|
+
# Invariant: soft < hard < ceiling. The ceiling is the empirical truncation point.
|
|
39
|
+
DEFAULT_BOOTSTRAP_SOFT_LIMIT = 10_000
|
|
40
|
+
DEFAULT_BOOTSTRAP_HARD_LIMIT = 11_500
|
|
41
|
+
BOOTSTRAP_HARD_LIMIT_CEILING = 12_000
|
|
42
|
+
|
|
43
|
+
# --- Memory cards ----------------------------------------------------------
|
|
44
|
+
MEMORY_CARD_BUDGET_BYTES = 8_000
|
|
45
|
+
|
|
46
|
+
# --- MEMORY.md index -------------------------------------------------------
|
|
47
|
+
# The flat index should stay short; detail belongs in topic cards.
|
|
48
|
+
MEMORY_INDEX_MAX_LINES = 180
|
|
49
|
+
|
|
50
|
+
# --- Staleness thresholds --------------------------------------------------
|
|
51
|
+
# A memory-care decay scan older than this is considered stale.
|
|
52
|
+
MEMORY_CARE_SCAN_STALE_DAYS = 7
|
|
53
|
+
# A handoff inbox with pending files older than this is a stalled backlog:
|
|
54
|
+
# handoffs are being written but nothing is ingesting them.
|
|
55
|
+
HANDOFF_BACKLOG_STALE_DAYS = 3
|
|
56
|
+
HANDOFF_BACKLOG_STALE_SECONDS = HANDOFF_BACKLOG_STALE_DAYS * 24 * 60 * 60
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def bootstrap_budget(name: str) -> int | None:
|
|
60
|
+
"""Byte budget for a bootstrap file basename, or None if it is not tracked."""
|
|
61
|
+
return BOOTSTRAP_BUDGETS.get(name)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def is_bootstrap_target(rel_path: str) -> bool:
|
|
65
|
+
"""True if a routing target basename is a budgeted bootstrap file."""
|
|
66
|
+
return Path(rel_path).name in BOOTSTRAP_BUDGETS
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def route_would_exceed_budget(dest: Path, addition: str) -> tuple[bool, int | None]:
|
|
70
|
+
"""Whether appending `addition` to a bootstrap file would exceed its budget.
|
|
71
|
+
|
|
72
|
+
Returns (would_exceed, budget). Non-bootstrap targets (e.g. .learnings/*)
|
|
73
|
+
are never guarded: (False, None).
|
|
74
|
+
"""
|
|
75
|
+
budget = BOOTSTRAP_BUDGETS.get(dest.name)
|
|
76
|
+
if budget is None:
|
|
77
|
+
return False, None
|
|
78
|
+
try:
|
|
79
|
+
existing = dest.stat().st_size if dest.is_file() else 0
|
|
80
|
+
except OSError:
|
|
81
|
+
existing = 0
|
|
82
|
+
projected = existing + len(addition.encode("utf-8"))
|
|
83
|
+
return projected > budget, budget
|
|
@@ -361,6 +361,12 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
361
361
|
p_repos_import.add_argument("--target", "-t", type=Path, default=Path("."), help="Repo or workspace to inspect.")
|
|
362
362
|
p_repos_import.add_argument("--dry-run", action="store_true", help="Show counts without writing imports.")
|
|
363
363
|
p_repos_import.add_argument("--json", action="store_true", help="Print machine-readable JSON.")
|
|
364
|
+
p_repos_ingest = repos_sub.add_parser("ingest", help="Ingest every fleet repo's handoffs into the canonical owner.")
|
|
365
|
+
p_repos_ingest.add_argument("--target", "-t", type=Path, default=Path("."), help="Canonical memory owner (where the fleet config lives).")
|
|
366
|
+
p_repos_ingest.add_argument("--apply", action="store_true", help="Write changes. Default is a dry run.")
|
|
367
|
+
p_repos_ingest.add_argument("--no-promote-cards", action="store_true", help="Do not auto-promote cards.")
|
|
368
|
+
p_repos_ingest.add_argument("--no-route-documents", action="store_true", help="Do not auto-route documents.")
|
|
369
|
+
p_repos_ingest.add_argument("--json", action="store_true", help="Print machine-readable JSON.")
|
|
364
370
|
p_repos_health_commands = repos_sub.add_parser("health-commands", help="Inspect configured optional repo health commands.")
|
|
365
371
|
p_repos_health_commands.add_argument("--target", "-t", type=Path, default=Path("."), help="Repo or workspace to inspect.")
|
|
366
372
|
p_repos_health_commands.add_argument("--json", action="store_true", help="Print machine-readable JSON.")
|
|
@@ -2489,6 +2495,14 @@ def main(argv=None) -> int:
|
|
|
2489
2495
|
return repos_cmd.doctor(target=args.target, json_output=args.json)
|
|
2490
2496
|
if args.repos_command == "import-issues":
|
|
2491
2497
|
return repos_cmd.import_issues(target=args.target, dry_run=args.dry_run, json_output=args.json)
|
|
2498
|
+
if args.repos_command == "ingest":
|
|
2499
|
+
return repos_cmd.ingest_fleet(
|
|
2500
|
+
target=args.target,
|
|
2501
|
+
apply=args.apply,
|
|
2502
|
+
promote_cards=not args.no_promote_cards,
|
|
2503
|
+
route_documents=not args.no_route_documents,
|
|
2504
|
+
json_output=args.json,
|
|
2505
|
+
)
|
|
2492
2506
|
if args.repos_command == "health-commands":
|
|
2493
2507
|
return repos_cmd.health_commands(target=args.target, json_output=args.json)
|
|
2494
2508
|
if args.repos_command == "discover":
|
|
@@ -17,20 +17,11 @@ WARN = "WARN"
|
|
|
17
17
|
FAIL = "FAIL"
|
|
18
18
|
MANUAL = "MANUAL"
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
"USER.md": 8_000,
|
|
26
|
-
"SAFETY_RULES.md": 10_000,
|
|
27
|
-
"INSTALL_FOR_AGENTS.md": 8_000,
|
|
28
|
-
"SOUL.md": 8_000,
|
|
29
|
-
"IDENTITY.md": 4_000,
|
|
30
|
-
"HEARTBEAT.md": 5_000,
|
|
31
|
-
}
|
|
32
|
-
MEMORY_CARD_BUDGET_BYTES = 8_000
|
|
33
|
-
MEMORY_CARE_SCAN_STALE_DAYS = 7
|
|
20
|
+
from .budgets import (
|
|
21
|
+
BOOTSTRAP_BUDGETS,
|
|
22
|
+
MEMORY_CARD_BUDGET_BYTES,
|
|
23
|
+
MEMORY_CARE_SCAN_STALE_DAYS,
|
|
24
|
+
)
|
|
34
25
|
|
|
35
26
|
from .station import DoctorContext
|
|
36
27
|
|
|
@@ -19,6 +19,12 @@ WRITER_INBOXES = (".claude/memory-handoffs", ".codex/memory-handoffs")
|
|
|
19
19
|
IGNORED_HANDOFF_NAMES = {"TEMPLATE.md"}
|
|
20
20
|
DEFAULT_STALE_AFTER_MINUTES = 90
|
|
21
21
|
HANDOFF_DRAFT_STALE_HOURS = 72
|
|
22
|
+
# A handoff inbox with pending files whose oldest entry is older than this is
|
|
23
|
+
# treated as a stalled backlog: handoffs are being written but nothing is
|
|
24
|
+
# ingesting them. Catches the silent pile-up where a repo's inbox is never
|
|
25
|
+
# reached by the canonical ingester (e.g. an uncovered repo in the fleet).
|
|
26
|
+
# Canonical value lives in budgets.py so doctor/ingest/repos all agree.
|
|
27
|
+
from .budgets import HANDOFF_BACKLOG_STALE_SECONDS as BACKLOG_STALE_SECONDS
|
|
22
28
|
MAX_INGESTOR_WARNING_SIGNALS = 5
|
|
23
29
|
CARD_ACTIONS = ("create-card", "update-card")
|
|
24
30
|
NO_CARD_ACTION = "no-card"
|
|
@@ -68,6 +74,7 @@ class InboxHealth:
|
|
|
68
74
|
pending: int
|
|
69
75
|
processed: int
|
|
70
76
|
watched: bool
|
|
77
|
+
oldest_pending_age_seconds: int | None = None
|
|
71
78
|
|
|
72
79
|
def as_dict(self) -> dict[str, Any]:
|
|
73
80
|
return {
|
|
@@ -77,6 +84,7 @@ class InboxHealth:
|
|
|
77
84
|
"pending": self.pending,
|
|
78
85
|
"processed": self.processed,
|
|
79
86
|
"watched": self.watched,
|
|
87
|
+
"oldest_pending_age_seconds": self.oldest_pending_age_seconds,
|
|
80
88
|
}
|
|
81
89
|
|
|
82
90
|
|
|
@@ -322,6 +330,29 @@ def doctor_checks(target: Path, sources: Path | None = None) -> list[tuple[str,
|
|
|
322
330
|
)
|
|
323
331
|
checks.append((level, f"handoff_watch: {inbox.inbox}", detail))
|
|
324
332
|
|
|
333
|
+
stale_backlog = [
|
|
334
|
+
inbox
|
|
335
|
+
for inbox in health.inboxes
|
|
336
|
+
if inbox.pending
|
|
337
|
+
and inbox.oldest_pending_age_seconds is not None
|
|
338
|
+
and inbox.oldest_pending_age_seconds >= BACKLOG_STALE_SECONDS
|
|
339
|
+
]
|
|
340
|
+
if stale_backlog:
|
|
341
|
+
pending_total = sum(inbox.pending for inbox in stale_backlog)
|
|
342
|
+
oldest = max(
|
|
343
|
+
inbox.oldest_pending_age_seconds or 0 for inbox in stale_backlog
|
|
344
|
+
)
|
|
345
|
+
oldest_days = oldest // (24 * 60 * 60)
|
|
346
|
+
names = ", ".join(inbox.inbox for inbox in stale_backlog)
|
|
347
|
+
checks.append(
|
|
348
|
+
(
|
|
349
|
+
WARN,
|
|
350
|
+
"handoff_backlog",
|
|
351
|
+
f"{pending_total} pending handoff(s) not ingested, oldest {oldest_days}d old "
|
|
352
|
+
f"({names}); ingester is not reaching this inbox",
|
|
353
|
+
)
|
|
354
|
+
)
|
|
355
|
+
|
|
325
356
|
if source_config := _source_config_for_checks(health.target, health.sources_path):
|
|
326
357
|
for watched_inbox in source_config.watched:
|
|
327
358
|
watched_path = watched_inbox.root / watched_inbox.inbox
|
|
@@ -2475,9 +2506,28 @@ def _inspect_inbox(target: Path, rel: str, watched: tuple[WatchedInbox, ...]) ->
|
|
|
2475
2506
|
pending=_count_pending(path),
|
|
2476
2507
|
processed=_count_processed(path),
|
|
2477
2508
|
watched=_is_watched(target, rel, watched),
|
|
2509
|
+
oldest_pending_age_seconds=_oldest_pending_age_seconds(path),
|
|
2478
2510
|
)
|
|
2479
2511
|
|
|
2480
2512
|
|
|
2513
|
+
def _oldest_pending_age_seconds(path: Path) -> int | None:
|
|
2514
|
+
"""Age in seconds of the oldest pending handoff, or None if the inbox is empty."""
|
|
2515
|
+
if not path.is_dir():
|
|
2516
|
+
return None
|
|
2517
|
+
oldest_mtime: float | None = None
|
|
2518
|
+
for candidate in path.glob("*.md"):
|
|
2519
|
+
if not candidate.is_file():
|
|
2520
|
+
continue
|
|
2521
|
+
if candidate.name.startswith(".") or candidate.name in IGNORED_HANDOFF_NAMES:
|
|
2522
|
+
continue
|
|
2523
|
+
mtime = candidate.stat().st_mtime
|
|
2524
|
+
if oldest_mtime is None or mtime < oldest_mtime:
|
|
2525
|
+
oldest_mtime = mtime
|
|
2526
|
+
if oldest_mtime is None:
|
|
2527
|
+
return None
|
|
2528
|
+
return max(0, int(time.time() - oldest_mtime))
|
|
2529
|
+
|
|
2530
|
+
|
|
2481
2531
|
def _count_pending(path: Path) -> int:
|
|
2482
2532
|
if not path.is_dir():
|
|
2483
2533
|
return 0
|
|
@@ -16,6 +16,8 @@ from datetime import datetime, timezone
|
|
|
16
16
|
from pathlib import Path
|
|
17
17
|
from typing import Dict, List
|
|
18
18
|
|
|
19
|
+
from . import budgets
|
|
20
|
+
|
|
19
21
|
SECTION_RE = re.compile(r"^##\s+(?P<name>.+?)\s*$", re.MULTILINE)
|
|
20
22
|
SAFE_CARD_NAME_RE = re.compile(r"^[A-Za-z0-9._-]+\.md$")
|
|
21
23
|
SAFE_RULE_PATH_RE = re.compile(r"^rules/[A-Za-z0-9._-]+\.md$")
|
|
@@ -85,24 +87,58 @@ def run(
|
|
|
85
87
|
dry_run: bool = False,
|
|
86
88
|
promote_cards: bool = False,
|
|
87
89
|
route_documents: bool = False,
|
|
90
|
+
owner: Path | None = None,
|
|
88
91
|
) -> int:
|
|
89
92
|
"""Process handoffs.
|
|
90
93
|
|
|
94
|
+
Reads handoffs from `target`'s writer inboxes. By default the resulting
|
|
95
|
+
cards/documents and review drafts are written back into `target` itself.
|
|
96
|
+
Pass `owner` to write them into a different canonical memory owner instead
|
|
97
|
+
(the fleet model: many writer repos, one owner); processed handoffs are
|
|
98
|
+
still archived in the source repo they came from.
|
|
99
|
+
|
|
91
100
|
`promote_cards` and `route_documents` are opt-in. With neither flag,
|
|
92
101
|
every handoff routes to the review inbox so a human picks the action.
|
|
93
102
|
Match the cookbook wrapper by passing both flags explicitly.
|
|
94
103
|
"""
|
|
95
|
-
|
|
96
|
-
|
|
104
|
+
source = target.expanduser().resolve()
|
|
105
|
+
owner = source if owner is None else owner.expanduser().resolve()
|
|
106
|
+
stats = IngestStats()
|
|
107
|
+
rc = ingest_into(
|
|
108
|
+
source=source,
|
|
109
|
+
owner=owner,
|
|
110
|
+
stats=stats,
|
|
111
|
+
dry_run=dry_run,
|
|
112
|
+
promote_cards=promote_cards,
|
|
113
|
+
route_documents=route_documents,
|
|
114
|
+
)
|
|
115
|
+
if rc != 0:
|
|
116
|
+
return rc
|
|
117
|
+
_report(stats, dry_run=dry_run)
|
|
118
|
+
return 0
|
|
97
119
|
|
|
98
|
-
|
|
120
|
+
|
|
121
|
+
def ingest_into(
|
|
122
|
+
*,
|
|
123
|
+
source: Path,
|
|
124
|
+
owner: Path,
|
|
125
|
+
stats: IngestStats,
|
|
126
|
+
dry_run: bool = False,
|
|
127
|
+
promote_cards: bool = False,
|
|
128
|
+
route_documents: bool = False,
|
|
129
|
+
) -> int:
|
|
130
|
+
"""Ingest `source`'s handoffs into `owner`'s memory, accumulating `stats`.
|
|
131
|
+
|
|
132
|
+
Returns 0 on success, 2 if `source` has no handoff inbox. Used directly by
|
|
133
|
+
the fleet driver so it can sweep many sources into one owner and report once.
|
|
134
|
+
"""
|
|
135
|
+
inbox_dir = owner / "memory" / "handoff-inbox"
|
|
136
|
+
handoff_dirs = _resolve_inbox_paths(source)
|
|
99
137
|
if not handoff_dirs:
|
|
100
|
-
legacy =
|
|
138
|
+
legacy = source / ".claude" / "memory-handoffs"
|
|
101
139
|
print(f"brigade ingest: no handoff inbox at {legacy}", file=sys.stderr)
|
|
102
140
|
return 2
|
|
103
141
|
|
|
104
|
-
stats = IngestStats()
|
|
105
|
-
|
|
106
142
|
for handoffs_dir in handoff_dirs:
|
|
107
143
|
processed_dir = handoffs_dir / "processed"
|
|
108
144
|
for path in _list_handoffs(handoffs_dir):
|
|
@@ -110,14 +146,14 @@ def run(
|
|
|
110
146
|
sections = parse(path)
|
|
111
147
|
outcome = decide(
|
|
112
148
|
sections,
|
|
113
|
-
target=
|
|
149
|
+
target=owner,
|
|
114
150
|
promote_cards=promote_cards,
|
|
115
151
|
route_documents=route_documents,
|
|
116
152
|
)
|
|
117
153
|
action = _execute(
|
|
118
154
|
outcome,
|
|
119
155
|
handoff_path=path,
|
|
120
|
-
target=
|
|
156
|
+
target=owner,
|
|
121
157
|
sections=sections,
|
|
122
158
|
inbox_dir=inbox_dir,
|
|
123
159
|
processed_dir=processed_dir,
|
|
@@ -132,8 +168,6 @@ def run(
|
|
|
132
168
|
stats.inboxed += 1
|
|
133
169
|
elif action.kind == "skipped":
|
|
134
170
|
stats.skipped += 1
|
|
135
|
-
|
|
136
|
-
_report(stats, dry_run=dry_run)
|
|
137
171
|
return 0
|
|
138
172
|
|
|
139
173
|
|
|
@@ -198,7 +232,21 @@ def decide(
|
|
|
198
232
|
"inboxed",
|
|
199
233
|
reason="document content contains `##` headings (would parse as new section)",
|
|
200
234
|
)
|
|
201
|
-
|
|
235
|
+
dest = target / document
|
|
236
|
+
# Bootstrap files load into the session prefix every turn and silently
|
|
237
|
+
# truncate past ~12k chars. Refuse an append that would push the file
|
|
238
|
+
# over its budget; route to the inbox so a human trims or re-homes it.
|
|
239
|
+
addition = "\n\n" + content.strip() + "\n"
|
|
240
|
+
would_exceed, budget = budgets.route_would_exceed_budget(dest, addition)
|
|
241
|
+
if would_exceed:
|
|
242
|
+
return Outcome(
|
|
243
|
+
"inboxed",
|
|
244
|
+
reason=(
|
|
245
|
+
f"bootstrap budget guard: appending to {document} would exceed "
|
|
246
|
+
f"its {budget}B budget; trim the file or promote to a memory card"
|
|
247
|
+
),
|
|
248
|
+
)
|
|
249
|
+
return Outcome("routed", dest=dest)
|
|
202
250
|
|
|
203
251
|
if action == "":
|
|
204
252
|
return Outcome("inboxed", reason="missing `Recommended memory action`")
|