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.
Files changed (165) hide show
  1. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/PKG-INFO +55 -36
  2. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/README.md +54 -35
  3. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/pyproject.toml +1 -1
  4. brigade_cli-0.8.0/src/brigade/budgets.py +83 -0
  5. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/cli.py +14 -0
  6. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/doctor.py +5 -14
  7. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/handoff_cmd.py +50 -0
  8. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/ingest.py +59 -11
  9. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/repos_cmd.py +132 -0
  10. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/work_cmd.py +7 -3
  11. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade_cli.egg-info/PKG-INFO +55 -36
  12. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade_cli.egg-info/SOURCES.txt +2 -0
  13. brigade_cli-0.8.0/tests/test_budgets.py +32 -0
  14. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_handoff_cmd.py +50 -0
  15. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_ingest.py +62 -0
  16. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_repos_cmd.py +79 -0
  17. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/LICENSE +0 -0
  18. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/MANIFEST.in +0 -0
  19. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/QUICKSTART.md +0 -0
  20. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/setup.cfg +0 -0
  21. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/__init__.py +0 -0
  22. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/__main__.py +0 -0
  23. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/aboyeur.py +0 -0
  24. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/add.py +0 -0
  25. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/agents.py +0 -0
  26. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/center_cmd.py +0 -0
  27. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/chat_cmd.py +0 -0
  28. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/config.py +0 -0
  29. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/context_cmd.py +0 -0
  30. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/daily_cmd.py +0 -0
  31. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/dogfood_cmd.py +0 -0
  32. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/fragments.py +0 -0
  33. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/handoff.py +0 -0
  34. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/install.py +0 -0
  35. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/learn_cmd.py +0 -0
  36. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/managed.py +0 -0
  37. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/memory_cmd.py +0 -0
  38. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/phases_cmd.py +0 -0
  39. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/proc.py +0 -0
  40. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/projects_cmd.py +0 -0
  41. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/prompt.py +0 -0
  42. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/py.typed +0 -0
  43. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/reconfigure.py +0 -0
  44. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/registry.py +0 -0
  45. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/release_cmd.py +0 -0
  46. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/roadmap_cmd.py +0 -0
  47. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/roster.py +0 -0
  48. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/roster_cmd.py +0 -0
  49. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/runs_cmd.py +0 -0
  50. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/scrub.py +0 -0
  51. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/security_cmd.py +0 -0
  52. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/selection.py +0 -0
  53. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/station.py +0 -0
  54. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/status.py +0 -0
  55. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/claude/memory-handoffs/TEMPLATE.md +0 -0
  56. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/codex/memory-handoffs/TEMPLATE.md +0 -0
  57. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/depth/repo.json +0 -0
  58. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/depth/workspace.json +0 -0
  59. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/generic/harness-adapter-checklist.md +0 -0
  60. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/generic/memory-contract.md +0 -0
  61. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/handoff/handoff-sources.example.json +0 -0
  62. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/harnesses/claude.json +0 -0
  63. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/harnesses/codex.json +0 -0
  64. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/harnesses/hermes.json +0 -0
  65. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/harnesses/openclaw.json +0 -0
  66. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/hermes/README.md +0 -0
  67. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/hermes/memory-handoff.harness.json +0 -0
  68. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/hermes/model-lanes.harness.json +0 -0
  69. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/hermes/workspace.harness.json +0 -0
  70. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/hooks/pre-push +0 -0
  71. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/includes/publisher.json +0 -0
  72. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/memory/cards/backup-restic.md +0 -0
  73. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/memory/cards/chat-surface-crawlers.md +0 -0
  74. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/memory/cards/content-safety.md +0 -0
  75. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/memory/cards/handoff-flow.md +0 -0
  76. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/memory/cards/memory-architecture.md +0 -0
  77. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/memory/cards/memory-care-staleness.md +0 -0
  78. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/memory/cards/memory-scanner.md +0 -0
  79. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/memory/cards/multi-workspace-handoff-admin.md +0 -0
  80. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/memory/cards/obsidian-notes.md +0 -0
  81. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/memory/cards/pipeline-standups.md +0 -0
  82. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/memory/cards/tokenjuice-output-compaction.md +0 -0
  83. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/memory/chat-memory-sweep.example.json +0 -0
  84. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/memory/memory-care.example.json +0 -0
  85. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/openclaw/README.md +0 -0
  86. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/openclaw/acp-escalation.openclaw.json +0 -0
  87. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/openclaw/memory-sweep-cron.openclaw.json +0 -0
  88. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/openclaw/model-aliases.openclaw.json +0 -0
  89. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/openclaw/ollama-memory-search.openclaw.json +0 -0
  90. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/policies/public-content.json +0 -0
  91. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/policies/public-repo.json +0 -0
  92. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/scripts/backup-restic.sh +0 -0
  93. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/skills/note/SKILL.md +0 -0
  94. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/workspace/AGENTS.md +0 -0
  95. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/workspace/CLAUDE.md +0 -0
  96. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/workspace/HEARTBEAT.md +0 -0
  97. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/workspace/IDENTITY.md +0 -0
  98. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/workspace/INSTALL_FOR_AGENTS.md +0 -0
  99. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/workspace/MEMORY.md +0 -0
  100. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/workspace/SAFETY_RULES.md +0 -0
  101. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/workspace/SOUL.md +0 -0
  102. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/workspace/TOOLS.md +0 -0
  103. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/workspace/USER.md +0 -0
  104. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/workspace/rules/acceptance-driven-work.md +0 -0
  105. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates/workspace/rules/issue-tdd-loop.md +0 -0
  106. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/templates.py +0 -0
  107. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/toml_compat.py +0 -0
  108. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade/tools_cmd.py +0 -0
  109. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade_cli.egg-info/dependency_links.txt +0 -0
  110. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade_cli.egg-info/entry_points.txt +0 -0
  111. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade_cli.egg-info/requires.txt +0 -0
  112. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/src/brigade_cli.egg-info/top_level.txt +0 -0
  113. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_aboyeur.py +0 -0
  114. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_add.py +0 -0
  115. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_agents.py +0 -0
  116. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_cli_alias.py +0 -0
  117. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_config.py +0 -0
  118. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_doctor.py +0 -0
  119. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_dogfood_cmd.py +0 -0
  120. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_fragments.py +0 -0
  121. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_gitignore.py +0 -0
  122. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_handoff.py +0 -0
  123. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_init.py +0 -0
  124. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_install.py +0 -0
  125. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_managed.py +0 -0
  126. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_memory_cmd.py +0 -0
  127. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_neutrality.py +0 -0
  128. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_phase100_cmd.py +0 -0
  129. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_phase101_cmd.py +0 -0
  130. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_phase165_cmd.py +0 -0
  131. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_phase36_cmd.py +0 -0
  132. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_phase37_cmd.py +0 -0
  133. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_phase38_cmd.py +0 -0
  134. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_phase39_cmd.py +0 -0
  135. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_phase40_cmd.py +0 -0
  136. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_phase41_cmd.py +0 -0
  137. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_phase42_cmd.py +0 -0
  138. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_phase43_cmd.py +0 -0
  139. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_phase44_cmd.py +0 -0
  140. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_phase45_cmd.py +0 -0
  141. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_phase46_50_cmd.py +0 -0
  142. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_phase51_55_cmd.py +0 -0
  143. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_phase56_60_cmd.py +0 -0
  144. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_phase96_cmd.py +0 -0
  145. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_phase97_cmd.py +0 -0
  146. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_phase98_cmd.py +0 -0
  147. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_phase99_cmd.py +0 -0
  148. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_privacy_regression.py +0 -0
  149. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_proc.py +0 -0
  150. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_prompt.py +0 -0
  151. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_reconfigure.py +0 -0
  152. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_registry.py +0 -0
  153. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_release_cmd.py +0 -0
  154. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_roadmap_cmd.py +0 -0
  155. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_roster.py +0 -0
  156. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_roster_cmd.py +0 -0
  157. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_run_cli.py +0 -0
  158. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_runs_cmd.py +0 -0
  159. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_scrub.py +0 -0
  160. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_security_cmd.py +0 -0
  161. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_selection.py +0 -0
  162. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_station.py +0 -0
  163. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_status.py +0 -0
  164. {brigade_cli-0.7.0 → brigade_cli-0.8.0}/tests/test_toml_compat.py +0 -0
  165. {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.7.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.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
- BOOTSTRAP_BUDGETS = {
21
- "AGENTS.md": 12_000,
22
- "CLAUDE.md": 6_000,
23
- "MEMORY.md": 7_000,
24
- "TOOLS.md": 10_000,
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
- target = target.expanduser().resolve()
96
- inbox_dir = target / "memory" / "handoff-inbox"
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
- handoff_dirs = _resolve_inbox_paths(target)
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 = target / ".claude" / "memory-handoffs"
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=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=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
- return Outcome("routed", dest=target / document)
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`")