brigade-cli 0.9.2__tar.gz → 0.10.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 (247) hide show
  1. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/PKG-INFO +4 -4
  2. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/QUICKSTART.md +2 -2
  3. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/README.md +1 -1
  4. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/pyproject.toml +3 -3
  5. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/__init__.py +1 -1
  6. brigade_cli-0.10.0/src/brigade/actionqueue.py +118 -0
  7. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/center_cmd.py +24 -146
  8. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/chat_cmd.py +1 -4
  9. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/cli.py +13 -0
  10. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/context_cmd.py +1 -24
  11. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/daily_cmd.py +1 -17
  12. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/doctor.py +13 -0
  13. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/handoff_cmd.py +1 -5
  14. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/learn_cmd.py +202 -19
  15. brigade_cli-0.10.0/src/brigade/localio.py +77 -0
  16. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/managed.py +118 -2
  17. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/memory_cmd.py +141 -4
  18. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/operator_cmd.py +30 -5
  19. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/pantry_cmd.py +1 -5
  20. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/phases_cmd.py +1 -17
  21. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/projects_cmd.py +1 -18
  22. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/registry.py +16 -1
  23. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/release_cmd.py +19 -81
  24. brigade_cli-0.10.0/src/brigade/reportstore.py +125 -0
  25. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/repos_cmd.py +42 -202
  26. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/research_cmd.py +1 -5
  27. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/runbook_cmd.py +1 -10
  28. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/scrub.py +35 -6
  29. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/security_cmd.py +1 -18
  30. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/skills_cmd.py +2 -12
  31. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/hermes/memory-handoff.harness.json +1 -1
  32. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/hermes/model-lanes.harness.json +1 -1
  33. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/hermes/workspace.harness.json +1 -1
  34. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/memory/cards/content-safety.md +1 -1
  35. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/memory/chat-memory-sweep.example.json +1 -1
  36. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/memory/memory-care.example.json +1 -1
  37. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/openclaw/README.md +1 -1
  38. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/policies/personal.json +1 -1
  39. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/policies/public-content.json +1 -1
  40. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/policies/public-repo.json +1 -1
  41. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/tools_cmd.py +1 -14
  42. brigade_cli-0.10.0/src/brigade/work_cmd/__init__.py +392 -0
  43. brigade_cli-0.10.0/src/brigade/work_cmd/config.py +796 -0
  44. brigade_cli-0.10.0/src/brigade/work_cmd/constants.py +506 -0
  45. brigade_cli-0.10.0/src/brigade/work_cmd/helpers.py +345 -0
  46. brigade_cli-0.10.0/src/brigade/work_cmd/ledger.py +1669 -0
  47. brigade_cli-0.10.0/src/brigade/work_cmd/services.py +5983 -0
  48. brigade_cli-0.10.0/src/brigade/work_cmd/session.py +2633 -0
  49. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade_cli.egg-info/PKG-INFO +4 -4
  50. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade_cli.egg-info/SOURCES.txt +12 -2
  51. brigade_cli-0.10.0/tests/test_managed.py +368 -0
  52. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_memory_cmd.py +90 -3
  53. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_operator_cmd.py +20 -0
  54. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_phase36_cmd.py +153 -0
  55. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_registry.py +19 -0
  56. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_scrub.py +57 -0
  57. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_work_cmd.py +138 -138
  58. brigade_cli-0.10.0/tests/test_work_cmd_facade.py +159 -0
  59. brigade_cli-0.9.2/src/brigade/work_cmd.py +0 -11795
  60. brigade_cli-0.9.2/tests/test_managed.py +0 -155
  61. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/LICENSE +0 -0
  62. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/MANIFEST.in +0 -0
  63. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/setup.cfg +0 -0
  64. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/__main__.py +0 -0
  65. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/aboyeur.py +0 -0
  66. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/add.py +0 -0
  67. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/agents.py +0 -0
  68. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/budgets.py +0 -0
  69. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/budgets_cmd.py +0 -0
  70. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/config.py +0 -0
  71. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/dogfood_cmd.py +0 -0
  72. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/fragments.py +0 -0
  73. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/handoff.py +0 -0
  74. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/hermes_adapter.py +0 -0
  75. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/ingest.py +0 -0
  76. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/install.py +0 -0
  77. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/notifications_cmd.py +0 -0
  78. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/proc.py +0 -0
  79. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/prompt.py +0 -0
  80. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/py.typed +0 -0
  81. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/reconfigure.py +0 -0
  82. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/research/__init__.py +0 -0
  83. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/research/config.py +0 -0
  84. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/research/engine.py +0 -0
  85. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/research/extract.py +0 -0
  86. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/research/handoff.py +0 -0
  87. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/research/llm.py +0 -0
  88. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/research/registry.py +0 -0
  89. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/research/report.py +0 -0
  90. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/research/sources/__init__.py +0 -0
  91. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/research/sources/cli.py +0 -0
  92. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/research/sources/local.py +0 -0
  93. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/research/sources/web.py +0 -0
  94. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/research/types.py +0 -0
  95. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/roadmap_cmd.py +0 -0
  96. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/roster.py +0 -0
  97. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/roster_cmd.py +0 -0
  98. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/runs_cmd.py +0 -0
  99. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/selection.py +0 -0
  100. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/station.py +0 -0
  101. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/status.py +0 -0
  102. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/adal/memory-handoffs/TEMPLATE.md +0 -0
  103. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/aider/memory-handoffs/TEMPLATE.md +0 -0
  104. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/antigravity/memory-handoffs/TEMPLATE.md +0 -0
  105. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/claude/memory-handoffs/TEMPLATE.md +0 -0
  106. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/codex/memory-handoffs/TEMPLATE.md +0 -0
  107. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/continue/memory-handoffs/TEMPLATE.md +0 -0
  108. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/copilot/memory-handoffs/TEMPLATE.md +0 -0
  109. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/cursor/memory-handoffs/TEMPLATE.md +0 -0
  110. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/depth/repo.json +0 -0
  111. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/depth/workspace.json +0 -0
  112. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/generic/harness-adapter-checklist.md +0 -0
  113. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/generic/memory-contract.md +0 -0
  114. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/goose/memory-handoffs/TEMPLATE.md +0 -0
  115. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/handoff/handoff-sources.example.json +0 -0
  116. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/handoff/openclaw-ingest-receipt.example.json +0 -0
  117. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/harnesses/adal.json +0 -0
  118. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/harnesses/aider.json +0 -0
  119. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/harnesses/antigravity.json +0 -0
  120. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/harnesses/claude.json +0 -0
  121. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/harnesses/codex.json +0 -0
  122. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/harnesses/continue.json +0 -0
  123. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/harnesses/copilot.json +0 -0
  124. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/harnesses/cursor.json +0 -0
  125. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/harnesses/goose.json +0 -0
  126. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/harnesses/hermes.json +0 -0
  127. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/harnesses/kimi.json +0 -0
  128. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/harnesses/openclaw.json +0 -0
  129. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/harnesses/opencode.json +0 -0
  130. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/harnesses/openhands.json +0 -0
  131. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/harnesses/pi.json +0 -0
  132. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/harnesses/qwen.json +0 -0
  133. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/hermes/README.md +0 -0
  134. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/hermes/memory-handoffs/TEMPLATE.md +0 -0
  135. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/hooks/pre-push +0 -0
  136. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/includes/publisher.json +0 -0
  137. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/kimi/memory-handoffs/TEMPLATE.md +0 -0
  138. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/memory/cards/backup-restic.md +0 -0
  139. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/memory/cards/chat-surface-crawlers.md +0 -0
  140. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/memory/cards/handoff-flow.md +0 -0
  141. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/memory/cards/memory-architecture.md +0 -0
  142. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/memory/cards/memory-care-staleness.md +0 -0
  143. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/memory/cards/memory-scanner.md +0 -0
  144. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/memory/cards/multi-workspace-handoff-admin.md +0 -0
  145. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/memory/cards/obsidian-notes.md +0 -0
  146. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/memory/cards/pipeline-standups.md +0 -0
  147. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/memory/cards/tokenjuice-output-compaction.md +0 -0
  148. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/openclaw/acp-escalation.openclaw.json +0 -0
  149. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/openclaw/memory-sweep-cron.openclaw.json +0 -0
  150. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/openclaw/model-aliases.openclaw.json +0 -0
  151. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/openclaw/ollama-memory-search.openclaw.json +0 -0
  152. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/opencode/memory-handoffs/TEMPLATE.md +0 -0
  153. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/openhands/memory-handoffs/TEMPLATE.md +0 -0
  154. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/pi/memory-handoffs/TEMPLATE.md +0 -0
  155. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/qwen/memory-handoffs/TEMPLATE.md +0 -0
  156. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/scripts/backup-restic.sh +0 -0
  157. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/skills/note/SKILL.md +0 -0
  158. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/workspace/AGENTS.md +0 -0
  159. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/workspace/CLAUDE.md +0 -0
  160. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/workspace/HEARTBEAT.md +0 -0
  161. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/workspace/IDENTITY.md +0 -0
  162. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/workspace/INSTALL_FOR_AGENTS.md +0 -0
  163. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/workspace/MEMORY.md +0 -0
  164. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/workspace/SAFETY_RULES.md +0 -0
  165. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/workspace/SOUL.md +0 -0
  166. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/workspace/TOOLS.md +0 -0
  167. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/workspace/USER.md +0 -0
  168. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/workspace/rules/acceptance-driven-work.md +0 -0
  169. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/workspace/rules/issue-tdd-loop.md +0 -0
  170. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates.py +0 -0
  171. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/toml_compat.py +0 -0
  172. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/untrusted.py +0 -0
  173. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/untrusted_cmd.py +0 -0
  174. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade_cli.egg-info/dependency_links.txt +0 -0
  175. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade_cli.egg-info/entry_points.txt +0 -0
  176. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade_cli.egg-info/requires.txt +0 -0
  177. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade_cli.egg-info/top_level.txt +0 -0
  178. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_aboyeur.py +0 -0
  179. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_add.py +0 -0
  180. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_agents.py +0 -0
  181. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_budgets.py +0 -0
  182. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_cli_alias.py +0 -0
  183. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_cli_help.py +0 -0
  184. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_config.py +0 -0
  185. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_doctor.py +0 -0
  186. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_dogfood_cmd.py +0 -0
  187. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_fragments.py +0 -0
  188. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_gitignore.py +0 -0
  189. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_handoff.py +0 -0
  190. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_handoff_cmd.py +0 -0
  191. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_ingest.py +0 -0
  192. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_init.py +0 -0
  193. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_install.py +0 -0
  194. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_neutrality.py +0 -0
  195. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_notifications_cmd.py +0 -0
  196. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_pantry_cmd.py +0 -0
  197. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_phase100_cmd.py +0 -0
  198. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_phase101_cmd.py +0 -0
  199. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_phase165_cmd.py +0 -0
  200. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_phase37_cmd.py +0 -0
  201. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_phase38_cmd.py +0 -0
  202. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_phase39_cmd.py +0 -0
  203. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_phase40_cmd.py +0 -0
  204. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_phase41_cmd.py +0 -0
  205. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_phase42_cmd.py +0 -0
  206. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_phase43_cmd.py +0 -0
  207. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_phase44_cmd.py +0 -0
  208. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_phase45_cmd.py +0 -0
  209. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_phase46_50_cmd.py +0 -0
  210. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_phase51_55_cmd.py +0 -0
  211. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_phase56_60_cmd.py +0 -0
  212. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_phase96_cmd.py +0 -0
  213. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_phase97_cmd.py +0 -0
  214. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_phase98_cmd.py +0 -0
  215. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_phase99_cmd.py +0 -0
  216. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_privacy_regression.py +0 -0
  217. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_proc.py +0 -0
  218. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_prompt.py +0 -0
  219. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_reconfigure.py +0 -0
  220. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_release_cmd.py +0 -0
  221. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_repos_cmd.py +0 -0
  222. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_research_cli_sources.py +0 -0
  223. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_research_cmd.py +0 -0
  224. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_research_config.py +0 -0
  225. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_research_engine.py +0 -0
  226. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_research_extract.py +0 -0
  227. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_research_handoff.py +0 -0
  228. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_research_llm.py +0 -0
  229. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_research_local_sources.py +0 -0
  230. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_research_registry.py +0 -0
  231. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_research_report.py +0 -0
  232. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_research_types.py +0 -0
  233. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_research_web.py +0 -0
  234. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_roadmap_cmd.py +0 -0
  235. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_roster.py +0 -0
  236. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_roster_cmd.py +0 -0
  237. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_run_cli.py +0 -0
  238. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_runbook_cmd.py +0 -0
  239. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_runs_cmd.py +0 -0
  240. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_security_cmd.py +0 -0
  241. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_selection.py +0 -0
  242. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_skills_cmd.py +0 -0
  243. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_station.py +0 -0
  244. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_status.py +0 -0
  245. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_toml_compat.py +0 -0
  246. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_untrusted.py +0 -0
  247. {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_untrusted_cmd.py +0 -0
@@ -1,14 +1,14 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: brigade-cli
3
- Version: 0.9.2
3
+ Version: 0.10.0
4
4
  Summary: AI agent memory, handoffs, and local guardrails for Codex, Claude Code, OpenCode, Hermes, and OpenClaw.
5
5
  Author-email: Solomon Neas <srneas@gmail.com>
6
6
  License: MIT
7
7
  Project-URL: Homepage, https://brigade.tools
8
8
  Project-URL: Repository, https://github.com/escoffier-labs/brigade
9
- Project-URL: Cookbook, https://github.com/solomonneas/solos-cookbook
9
+ Project-URL: Cookbook, https://github.com/escoffier-labs/solos-cookbook
10
10
  Project-URL: OpenClaw, https://github.com/solomonneas/openclaw
11
- Project-URL: ContentGuard, https://github.com/solomonneas/content-guard
11
+ Project-URL: ContentGuard, https://github.com/escoffier-labs/content-guard
12
12
  Project-URL: AgentPantry, https://github.com/escoffier-labs/agentpantry
13
13
  Project-URL: Issues, https://github.com/escoffier-labs/brigade/issues
14
14
  Keywords: agents,ai-agents,agent-memory,agent-handoffs,ai-memory,openclaw,claude-code,codex,opencode,memory,bootstrap,brigade,brigade-cli,operator,local-first,guardrails,agents-md
@@ -54,7 +54,7 @@ So I hand-rolled the fixes, one incident at a time: a slim `MEMORY.md` index poi
54
54
 
55
55
  Two incidents shaped the design more than anything I planned. First, a nightly "dreaming" job that auto-promoted session fragments quietly bloated `MEMORY.md` to 41KB, way past the 12KB bootstrap budget, so every session started with truncated memory and nobody noticed for weeks. Auto-promotion died that day. Everything goes through review now. Second, I found 195 handoff notes sitting unread across 35 repos because the ingester had a hardcoded three-repo allowlist and nothing warned about the coverage gap. Silence is the failure mode. Every part of Brigade that lints, warns, or writes a receipt exists because something once failed quietly.
56
56
 
57
- That system now runs 482 memory cards and survives daily multi-agent work. But explaining it to anyone meant: clone six repos, write these crons, keep your index slim, watch for staleness, and whatever you do, turn auto-promotion off. Brigade is that setup packaged as one installable CLI. The full production stack is documented in the [solos-cookbook](https://github.com/solomonneas/solos-cookbook) if you want to see where it came from.
57
+ That system now runs 482 memory cards and survives daily multi-agent work. But explaining it to anyone meant: clone six repos, write these crons, keep your index slim, watch for staleness, and whatever you do, turn auto-promotion off. Brigade is that setup packaged as one installable CLI. The full production stack is documented in the [solos-cookbook](https://github.com/escoffier-labs/solos-cookbook) if you want to see where it came from.
58
58
 
59
59
  ## The loop
60
60
 
@@ -96,11 +96,11 @@ brigade reconfigure --target . --harnesses claude --prune
96
96
 
97
97
  The starter handoff template lives at `<inbox>/TEMPLATE.md`. Copy it to a new dated file (e.g. `2026-05-16-1430-fixed-X.md`), fill it in, and the ingester promotes safe card handoffs into `memory/cards/`, appends targeted updates to the right file, and kicks ambiguous material to the review inbox.
98
98
 
99
- See the [Solo Cookbook](https://github.com/solomonneas/solos-cookbook) for the longer-form guidance on what makes a good handoff and when to use which routing.
99
+ See the [Solo Cookbook](https://github.com/escoffier-labs/solos-cookbook) for the longer-form guidance on what makes a good handoff and when to use which routing.
100
100
 
101
101
  ## Next steps
102
102
 
103
- - Read [the cookbook](https://github.com/solomonneas/solos-cookbook) for the deep version of every concept here.
103
+ - Read [the cookbook](https://github.com/escoffier-labs/solos-cookbook) for the deep version of every concept here.
104
104
  - Customize `USER.md` and `TOOLS.md` with your real preferences and runbooks (kept private; do not commit personal details).
105
105
  - Wire the ingester on a cron or a manual end-of-day workflow.
106
106
  - Add a memory-care staleness scan when your card set starts to matter. See `memory/cards/memory-care-staleness.md`.
@@ -25,7 +25,7 @@ So I hand-rolled the fixes, one incident at a time: a slim `MEMORY.md` index poi
25
25
 
26
26
  Two incidents shaped the design more than anything I planned. First, a nightly "dreaming" job that auto-promoted session fragments quietly bloated `MEMORY.md` to 41KB, way past the 12KB bootstrap budget, so every session started with truncated memory and nobody noticed for weeks. Auto-promotion died that day. Everything goes through review now. Second, I found 195 handoff notes sitting unread across 35 repos because the ingester had a hardcoded three-repo allowlist and nothing warned about the coverage gap. Silence is the failure mode. Every part of Brigade that lints, warns, or writes a receipt exists because something once failed quietly.
27
27
 
28
- That system now runs 482 memory cards and survives daily multi-agent work. But explaining it to anyone meant: clone six repos, write these crons, keep your index slim, watch for staleness, and whatever you do, turn auto-promotion off. Brigade is that setup packaged as one installable CLI. The full production stack is documented in the [solos-cookbook](https://github.com/solomonneas/solos-cookbook) if you want to see where it came from.
28
+ That system now runs 482 memory cards and survives daily multi-agent work. But explaining it to anyone meant: clone six repos, write these crons, keep your index slim, watch for staleness, and whatever you do, turn auto-promotion off. Brigade is that setup packaged as one installable CLI. The full production stack is documented in the [solos-cookbook](https://github.com/escoffier-labs/solos-cookbook) if you want to see where it came from.
29
29
 
30
30
  ## The loop
31
31
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "brigade-cli"
7
- version = "0.9.2"
7
+ version = "0.10.0"
8
8
  description = "AI agent memory, handoffs, and local guardrails for Codex, Claude Code, OpenCode, Hermes, and OpenClaw."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -28,9 +28,9 @@ research = ["playwright>=1.40"]
28
28
  [project.urls]
29
29
  Homepage = "https://brigade.tools"
30
30
  Repository = "https://github.com/escoffier-labs/brigade"
31
- Cookbook = "https://github.com/solomonneas/solos-cookbook"
31
+ Cookbook = "https://github.com/escoffier-labs/solos-cookbook"
32
32
  OpenClaw = "https://github.com/solomonneas/openclaw"
33
- ContentGuard = "https://github.com/solomonneas/content-guard"
33
+ ContentGuard = "https://github.com/escoffier-labs/content-guard"
34
34
  AgentPantry = "https://github.com/escoffier-labs/agentpantry"
35
35
  Issues = "https://github.com/escoffier-labs/brigade/issues"
36
36
 
@@ -1,3 +1,3 @@
1
1
  """Brigade: local operator-system CLI for agent workspaces."""
2
2
 
3
- __version__ = "0.9.2"
3
+ __version__ = "0.10.0"
@@ -0,0 +1,118 @@
1
+ """Shared primitives for the single-file action-queue lifecycle.
2
+
3
+ These helpers were extracted from near-identical private copies behind
4
+ `center actions` (center_cmd), `repos actions` (repos_cmd), and
5
+ `repos release actions` (repos_cmd). Each station keeps its own paths,
6
+ payload builders, write envelope, and output text; this module owns the
7
+ store read, id-prefix lookup, status stamping, fingerprint-deduped merge,
8
+ and the archive split/append.
9
+
10
+ The phase-ledger queue in phases_cmd stays local: it stores one JSON file
11
+ per action and stamps `reviewed_at`/`review_reason` instead of the
12
+ `started_at`/`completed_at`/`deferred_at` lifecycle fields below.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ from pathlib import Path
18
+ from typing import Any
19
+
20
+ from .localio import read_json_dict
21
+
22
+
23
+ def read_actions(path: Path) -> list[dict[str, Any]]:
24
+ """Read the "actions" list from a single-file store; [] when missing or invalid."""
25
+ payload = read_json_dict(path)
26
+ actions = payload.get("actions") if isinstance(payload, dict) else None
27
+ return [item for item in actions if isinstance(item, dict)] if isinstance(actions, list) else []
28
+
29
+
30
+ def find_action(
31
+ actions: list[dict[str, Any]],
32
+ action_id: str,
33
+ *,
34
+ id_field: str,
35
+ label: str,
36
+ ) -> tuple[dict[str, Any] | None, str | None]:
37
+ """Find the unique action whose id_field value starts with action_id.
38
+
39
+ Returns (action, None) on a unique match, otherwise (None, error) where the
40
+ error is a "{label} not found" or "{label} id is ambiguous" message.
41
+ """
42
+ matches = [action for action in actions if str(action.get(id_field) or "").startswith(action_id)]
43
+ if not matches:
44
+ return None, f"{label} not found: {action_id}"
45
+ if len(matches) > 1:
46
+ return None, f"{label} id is ambiguous: {action_id}"
47
+ return matches[0], None
48
+
49
+
50
+ def stamp_status(action: dict[str, Any], status: str, *, now: str, reason: str | None = None) -> None:
51
+ """Apply a lifecycle status transition in place with the shared timestamp fields."""
52
+ action["status"] = status
53
+ action["updated_at"] = now
54
+ if status == "active":
55
+ action["started_at"] = now
56
+ elif status == "done":
57
+ action["completed_at"] = now
58
+ elif status == "deferred":
59
+ action["deferred_at"] = now
60
+ action["defer_reason"] = reason or "deferred"
61
+
62
+
63
+ def merge_planned(
64
+ existing: list[dict[str, Any]],
65
+ archived: list[dict[str, Any]],
66
+ planned: list[dict[str, Any]],
67
+ ) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
68
+ """Append planned actions with new source_fingerprint values to existing in place.
69
+
70
+ Fingerprints already present in existing or archived are skipped.
71
+ Returns (created, skipped).
72
+ """
73
+ fingerprints = {str(action.get("source_fingerprint")) for action in existing}
74
+ fingerprints.update(str(action.get("source_fingerprint")) for action in archived)
75
+ created: list[dict[str, Any]] = []
76
+ skipped: list[dict[str, Any]] = []
77
+ for action in planned:
78
+ if str(action.get("source_fingerprint")) in fingerprints:
79
+ skipped.append(action)
80
+ continue
81
+ created.append(action)
82
+ existing.append(action)
83
+ fingerprints.add(str(action.get("source_fingerprint")))
84
+ return created, skipped
85
+
86
+
87
+ def split_archived_completed(
88
+ actions: list[dict[str, Any]],
89
+ *,
90
+ now: str,
91
+ ) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
92
+ """Split actions into (archived copies of done actions, remaining actions).
93
+
94
+ Done actions are copied with status "archived" and archived_at/updated_at
95
+ set to now; all other actions pass through unchanged.
96
+ """
97
+ archived: list[dict[str, Any]] = []
98
+ remaining: list[dict[str, Any]] = []
99
+ for action in actions:
100
+ if action.get("status") == "done":
101
+ archived_action = dict(action)
102
+ archived_action["status"] = "archived"
103
+ archived_action["archived_at"] = now
104
+ archived_action["updated_at"] = now
105
+ archived.append(archived_action)
106
+ else:
107
+ remaining.append(action)
108
+ return archived, remaining
109
+
110
+
111
+ def append_archive(path: Path, actions: list[dict[str, Any]]) -> None:
112
+ """Append actions to a JSONL archive, creating parent directories; no-op when empty."""
113
+ if not actions:
114
+ return
115
+ path.parent.mkdir(parents=True, exist_ok=True)
116
+ with path.open("a") as handle:
117
+ for action in actions:
118
+ handle.write(json.dumps(action, sort_keys=True) + "\n")
@@ -5,15 +5,15 @@ import html
5
5
  import hashlib
6
6
  import json
7
7
  import re
8
- import shutil
9
8
  import subprocess
10
9
  import sys
11
- from datetime import datetime, timezone
10
+ from datetime import datetime
12
11
  from pathlib import Path
13
12
  from typing import Any
14
13
  from uuid import uuid4
15
14
 
16
- from . import chat_cmd, context_cmd, handoff_cmd, learn_cmd, memory_cmd, notifications_cmd, pantry_cmd, phases_cmd, projects_cmd, release_cmd, repos_cmd, research_cmd, roadmap_cmd, security_cmd, tools_cmd, work_cmd
15
+ from . import actionqueue, chat_cmd, context_cmd, handoff_cmd, learn_cmd, memory_cmd, notifications_cmd, pantry_cmd, phases_cmd, projects_cmd, release_cmd, repos_cmd, reportstore, research_cmd, roadmap_cmd, security_cmd, tools_cmd, work_cmd
16
+ from .localio import read_json_dict as _read_json, read_jsonl_dicts as _read_jsonl, utc_now as _now, write_json as _write_json
17
17
 
18
18
  SCHEMA_VERSION = 1
19
19
  SCHEMA_MANIFEST_VERSION = 1
@@ -46,23 +46,6 @@ CENTER_REQUIRED_ITEM_FIELDS = {
46
46
  }
47
47
 
48
48
 
49
- def _read_json(path: Path) -> dict[str, Any] | None:
50
- try:
51
- payload = json.loads(path.read_text())
52
- except (OSError, json.JSONDecodeError):
53
- return None
54
- return payload if isinstance(payload, dict) else None
55
-
56
-
57
- def _write_json(path: Path, payload: dict[str, Any]) -> None:
58
- path.parent.mkdir(parents=True, exist_ok=True)
59
- path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n")
60
-
61
-
62
- def _now() -> datetime:
63
- return datetime.now(timezone.utc)
64
-
65
-
66
49
  def _parse_time(value: object) -> datetime | None:
67
50
  if not isinstance(value, str) or not value:
68
51
  return None
@@ -569,26 +552,6 @@ def _iter_json_files(root: Path, pattern: str) -> list[dict[str, Any]]:
569
552
  return items
570
553
 
571
554
 
572
- def _read_jsonl(path: Path) -> list[dict[str, Any]]:
573
- if not path.is_file():
574
- return []
575
- records: list[dict[str, Any]] = []
576
- try:
577
- lines = path.read_text().splitlines()
578
- except OSError:
579
- return []
580
- for line in lines:
581
- if not line.strip():
582
- continue
583
- try:
584
- payload = json.loads(line)
585
- except json.JSONDecodeError:
586
- continue
587
- if isinstance(payload, dict):
588
- records.append(payload)
589
- return records
590
-
591
-
592
555
  def _actions_root(target: Path) -> Path:
593
556
  return target / ".brigade" / "center" / "actions"
594
557
 
@@ -602,13 +565,7 @@ def _actions_archive_path(target: Path) -> Path:
602
565
 
603
566
 
604
567
  def _read_actions(target: Path) -> list[dict[str, Any]]:
605
- payload = _read_json(_actions_path(target))
606
- if payload is None:
607
- return []
608
- actions = payload.get("actions")
609
- if not isinstance(actions, list):
610
- return []
611
- return [item for item in actions if isinstance(item, dict)]
568
+ return actionqueue.read_actions(_actions_path(target))
612
569
 
613
570
 
614
571
  def _read_action_archive(target: Path) -> list[dict[str, Any]]:
@@ -626,13 +583,7 @@ def _write_actions(target: Path, actions: list[dict[str, Any]]) -> None:
626
583
 
627
584
 
628
585
  def _append_action_archive(target: Path, actions: list[dict[str, Any]]) -> None:
629
- if not actions:
630
- return
631
- path = _actions_archive_path(target)
632
- path.parent.mkdir(parents=True, exist_ok=True)
633
- with path.open("a") as handle:
634
- for action in actions:
635
- handle.write(json.dumps(action, sort_keys=True) + "\n")
586
+ actionqueue.append_archive(_actions_archive_path(target), actions)
636
587
 
637
588
 
638
589
  def _activity(target: Path) -> list[dict[str, Any]]:
@@ -1065,22 +1016,6 @@ def _readiness_waivers_path(target: Path) -> Path:
1065
1016
  return _readiness_root(target) / "waivers.jsonl"
1066
1017
 
1067
1018
 
1068
- def _read_jsonl(path: Path) -> list[dict[str, Any]]:
1069
- if not path.is_file():
1070
- return []
1071
- records: list[dict[str, Any]] = []
1072
- for line in path.read_text().splitlines():
1073
- if not line.strip():
1074
- continue
1075
- try:
1076
- item = json.loads(line)
1077
- except json.JSONDecodeError:
1078
- continue
1079
- if isinstance(item, dict):
1080
- records.append(item)
1081
- return records
1082
-
1083
-
1084
1019
  def _write_jsonl(path: Path, records: list[dict[str, Any]]) -> None:
1085
1020
  path.parent.mkdir(parents=True, exist_ok=True)
1086
1021
  with path.open("w") as handle:
@@ -1467,32 +1402,18 @@ def _report_diffs_root(target: Path) -> Path:
1467
1402
 
1468
1403
 
1469
1404
  def _report_json_path(path: Path) -> Path:
1470
- return path / "CENTER_EVIDENCE.json" if path.is_dir() else path
1405
+ return reportstore.bundle_json_path(path, "CENTER_EVIDENCE.json")
1471
1406
 
1472
1407
 
1473
1408
  def _read_report(path: Path) -> dict[str, Any] | None:
1474
- payload = _read_json(_report_json_path(path))
1475
- if payload is not None:
1476
- payload.setdefault("path", str(_report_json_path(path).parent))
1477
- return payload
1409
+ return reportstore.read_bundle(path, "CENTER_EVIDENCE.json")
1478
1410
 
1479
1411
 
1480
1412
  def _reports(target: Path, *, include_archived: bool = False) -> list[dict[str, Any]]:
1481
1413
  roots = [_reports_root(target)]
1482
1414
  if include_archived:
1483
1415
  roots.append(_reports_archive_root(target))
1484
- reports: list[dict[str, Any]] = []
1485
- for root in roots:
1486
- if not root.is_dir():
1487
- continue
1488
- for child in root.iterdir():
1489
- if child.name.endswith("archive") or not child.is_dir():
1490
- continue
1491
- payload = _read_report(child)
1492
- if payload is not None:
1493
- reports.append(payload)
1494
- reports.sort(key=lambda item: str(item.get("created_at") or item.get("report_id") or ""), reverse=True)
1495
- return reports
1416
+ return reportstore.list_bundles(roots, _read_report, id_field="report_id", skip_child=lambda name: name.endswith("archive"))
1496
1417
 
1497
1418
 
1498
1419
  def latest_report(target: Path) -> dict[str, Any] | None:
@@ -1511,15 +1432,7 @@ def latest_report_diff(target: Path) -> dict[str, Any] | None:
1511
1432
 
1512
1433
  def _resolve_report(target: Path, report_id: str) -> tuple[dict[str, Any] | None, str | None]:
1513
1434
  reports = _reports(target, include_archived=True)
1514
- if report_id == "latest":
1515
- latest = latest_report(target)
1516
- return (latest, None) if latest else (None, "operator report not found: latest")
1517
- matches = [item for item in reports if str(item.get("report_id") or "").startswith(report_id)]
1518
- if not matches:
1519
- return None, f"operator report not found: {report_id}"
1520
- if len(matches) > 1:
1521
- return None, f"operator report id is ambiguous: {report_id}"
1522
- return matches[0], None
1435
+ return reportstore.resolve_bundle(reports, report_id, id_field="report_id", label="operator report", latest=lambda: latest_report(target))
1523
1436
 
1524
1437
 
1525
1438
  def _receipt_references(payload: dict[str, Any]) -> list[str]:
@@ -1771,9 +1684,12 @@ def _report_html(markdown: str, payload: dict[str, Any]) -> str:
1771
1684
 
1772
1685
  def _write_report_bundle(report_dir: Path, payload: dict[str, Any]) -> None:
1773
1686
  markdown = _report_markdown(payload)
1774
- _write_json(report_dir / "CENTER_EVIDENCE.json", payload)
1775
- (report_dir / "OPERATOR_REPORT.md").write_text(markdown)
1776
- (report_dir / "OPERATOR_REPORT.html").write_text(_report_html(markdown, payload))
1687
+ reportstore.write_bundle(
1688
+ report_dir,
1689
+ payload,
1690
+ evidence_name="CENTER_EVIDENCE.json",
1691
+ documents={"OPERATOR_REPORT.md": markdown, "OPERATOR_REPORT.html": _report_html(markdown, payload)},
1692
+ )
1777
1693
 
1778
1694
 
1779
1695
  def report_health(target: Path) -> dict[str, Any]:
@@ -1937,12 +1853,10 @@ def report_archive(*, target: Path, report_id: str, json_output: bool = False) -
1937
1853
  if not source.is_dir():
1938
1854
  print(f"error: operator report path is missing: {source}", file=sys.stderr)
1939
1855
  return 2
1940
- destination = _reports_archive_root(target) / source.name
1941
- destination.parent.mkdir(parents=True, exist_ok=True)
1942
- if destination.exists():
1856
+ destination, moved = reportstore.move_bundle(source, _reports_archive_root(target))
1857
+ if not moved:
1943
1858
  print(f"error: archived operator report already exists: {destination}", file=sys.stderr)
1944
1859
  return 2
1945
- shutil.move(str(source), str(destination))
1946
1860
  payload = {"schema_version": SCHEMA_VERSION, "target": str(target), "report_id": report.get("report_id"), "status": "archived", "archive_path": str(destination)}
1947
1861
  if json_output:
1948
1862
  print(json.dumps(payload, indent=2, sort_keys=True))
@@ -2221,7 +2135,7 @@ def report_closeout(
2221
2135
  json_output: bool = False,
2222
2136
  ) -> int:
2223
2137
  target = target.expanduser().resolve()
2224
- if status not in {"reviewed", "deferred", "superseded", "archived"}:
2138
+ if status not in reportstore.CLOSEOUT_STATUSES:
2225
2139
  print("error: --status must be one of reviewed, deferred, superseded, archived", file=sys.stderr)
2226
2140
  return 2
2227
2141
  report, error = _resolve_report(target, report_id)
@@ -2246,9 +2160,7 @@ def report_closeout(
2246
2160
  "deferred_item_ids": deferred,
2247
2161
  "report_fingerprint": report.get("report_fingerprint") or _fingerprint_payload({"reviews": report.get("reviews"), "activity": report.get("activity")}),
2248
2162
  }
2249
- closeout_path = report_path / "CLOSEOUT.json"
2250
- payload["path"] = str(closeout_path)
2251
- _write_json(closeout_path, payload)
2163
+ closeout_path = reportstore.write_closeout(report_path, payload)
2252
2164
  report["closeout"] = payload
2253
2165
  _write_json(report_path / "CENTER_EVIDENCE.json", report)
2254
2166
  if json_output:
@@ -2335,12 +2247,8 @@ def _planned_actions(report: dict[str, Any]) -> list[dict[str, Any]]:
2335
2247
 
2336
2248
  def _find_action(target: Path, action_id: str) -> tuple[list[dict[str, Any]], dict[str, Any] | None, str | None]:
2337
2249
  actions = _read_actions(target)
2338
- matches = [item for item in actions if str(item.get("action_id") or "").startswith(action_id)]
2339
- if not matches:
2340
- return actions, None, f"action not found: {action_id}"
2341
- if len(matches) > 1:
2342
- return actions, None, f"action id is ambiguous: {action_id}"
2343
- return actions, matches[0], None
2250
+ action, error = actionqueue.find_action(actions, action_id, id_field="action_id", label="action")
2251
+ return actions, action, error
2344
2252
 
2345
2253
 
2346
2254
  def _action_counts(actions: list[dict[str, Any]]) -> dict[str, int]:
@@ -2594,17 +2502,7 @@ def actions_build(*, target: Path, report_id: str = "latest", allow_unreviewed:
2594
2502
  return 2
2595
2503
  planned = _planned_actions(report)
2596
2504
  existing = _read_actions(target)
2597
- existing_fingerprints = {str(item.get("source_fingerprint")) for item in existing}
2598
- existing_fingerprints.update(str(item.get("source_fingerprint")) for item in _read_action_archive(target))
2599
- created: list[dict[str, Any]] = []
2600
- skipped: list[dict[str, Any]] = []
2601
- for action in planned:
2602
- if str(action.get("source_fingerprint")) in existing_fingerprints:
2603
- skipped.append(action)
2604
- continue
2605
- created.append(action)
2606
- existing.append(action)
2607
- existing_fingerprints.add(str(action.get("source_fingerprint")))
2505
+ created, skipped = actionqueue.merge_planned(existing, _read_action_archive(target), planned)
2608
2506
  _write_actions(target, existing)
2609
2507
  payload = {
2610
2508
  "schema_version": SCHEMA_VERSION,
@@ -2697,16 +2595,7 @@ def _set_action_status(
2697
2595
  if action is None:
2698
2596
  print(f"error: {error}", file=sys.stderr)
2699
2597
  return 1 if error and "not found" in error else 2
2700
- now = _now().isoformat()
2701
- action["status"] = status
2702
- action["updated_at"] = now
2703
- if status == "active":
2704
- action["started_at"] = now
2705
- elif status == "done":
2706
- action["completed_at"] = now
2707
- elif status == "deferred":
2708
- action["deferred_at"] = now
2709
- action["defer_reason"] = reason or "deferred"
2598
+ actionqueue.stamp_status(action, status, now=_now().isoformat(), reason=reason)
2710
2599
  _write_actions(target, actions)
2711
2600
  payload = {
2712
2601
  "schema_version": SCHEMA_VERSION,
@@ -2740,18 +2629,7 @@ def actions_defer(*, target: Path, action_id: str, reason: str, json_output: boo
2740
2629
  def actions_archive_completed(*, target: Path, json_output: bool = False) -> int:
2741
2630
  target = target.expanduser().resolve()
2742
2631
  actions = _read_actions(target)
2743
- now = _now().isoformat()
2744
- archived: list[dict[str, Any]] = []
2745
- remaining: list[dict[str, Any]] = []
2746
- for action in actions:
2747
- if action.get("status") == "done":
2748
- archived_action = dict(action)
2749
- archived_action["status"] = "archived"
2750
- archived_action["archived_at"] = now
2751
- archived_action["updated_at"] = now
2752
- archived.append(archived_action)
2753
- else:
2754
- remaining.append(action)
2632
+ archived, remaining = actionqueue.split_archived_completed(actions, now=_now().isoformat())
2755
2633
  _write_actions(target, remaining)
2756
2634
  _append_action_archive(target, archived)
2757
2635
  payload = {
@@ -12,6 +12,7 @@ from typing import Any
12
12
  from . import toml_compat as tomllib
13
13
  from .install import apply_gitignore
14
14
  from .selection import Selection
15
+ from .localio import utc_now as _now
15
16
 
16
17
  CONFIG_REL_PATH = ".brigade/chat-surfaces.toml"
17
18
  OUTPUT_ROOT_REL_PATH = ".brigade/chat-memory-sweeps"
@@ -140,10 +141,6 @@ DEFAULT_SURFACES = (
140
141
  CONFIDENCE_RANK = {"high": 0, "medium": 1, "normal": 1, "low": 2}
141
142
 
142
143
 
143
- def _now() -> datetime:
144
- return datetime.now(timezone.utc)
145
-
146
-
147
144
  def _config_path(target: Path) -> Path:
148
145
  return target / CONFIG_REL_PATH
149
146
 
@@ -1203,6 +1203,10 @@ def _build_parser() -> argparse.ArgumentParser:
1203
1203
  p_memory_care_plan_fixes = memory_care_sub.add_parser("plan-fixes", help="Plan safe memory-care metadata fixes without writing files.")
1204
1204
  p_memory_care_plan_fixes.add_argument("--target", "-t", type=Path, default=Path("."), help="Repo or workspace to inspect.")
1205
1205
  p_memory_care_plan_fixes.add_argument("--json", action="store_true", help="Print machine-readable JSON.")
1206
+ p_memory_care_backfill = memory_care_sub.add_parser("backfill", help="Backfill missing reviewed/freshness card metadata from git history (dry-run by default).")
1207
+ p_memory_care_backfill.add_argument("--target", "-t", type=Path, default=Path("."), help="Repo or workspace to update.")
1208
+ p_memory_care_backfill.add_argument("--apply", action="store_true", help="Write the derived metadata into card frontmatter and record a receipt.")
1209
+ p_memory_care_backfill.add_argument("--json", action="store_true", help="Print machine-readable JSON.")
1206
1210
  p_memory_care_status = memory_care_sub.add_parser("status", help="Show local memory-care status.")
1207
1211
  p_memory_care_status.add_argument("--target", "-t", type=Path, default=Path("."), help="Repo or workspace to inspect.")
1208
1212
  p_memory_care_status.add_argument("--json", action="store_true", help="Print machine-readable JSON.")
@@ -2147,6 +2151,11 @@ def _build_parser() -> argparse.ArgumentParser:
2147
2151
  p_learn_import.add_argument("--target", "-t", type=Path, default=Path("."), help="Repo or workspace to update.")
2148
2152
  p_learn_import.add_argument("--dry-run", action="store_true", help="Report without writing imports.")
2149
2153
  p_learn_import.add_argument("--json", action="store_true", help="Print machine-readable JSON.")
2154
+ p_learn_import_learnings = learn_sub.add_parser("import-learnings", help="Import structured .learnings/ markdown log entries into the work inbox.")
2155
+ p_learn_import_learnings.add_argument("--target", "-t", type=Path, default=Path("."), help="Repo or workspace to update.")
2156
+ p_learn_import_learnings.add_argument("--file", action="append", default=[], help="Override the .learnings file to read. May be repeated.")
2157
+ p_learn_import_learnings.add_argument("--dry-run", action="store_true", help="Report without writing imports.")
2158
+ p_learn_import_learnings.add_argument("--json", action="store_true", help="Print machine-readable JSON.")
2150
2159
  p_learn_skill_candidates = learn_sub.add_parser("skill-candidates", help="Find repeatable learning patterns that could become reviewed skills.")
2151
2160
  p_learn_skill_candidates.add_argument("--target", "-t", type=Path, default=Path("."), help="Repo or workspace to inspect.")
2152
2161
  p_learn_skill_candidates.add_argument("--min-count", type=int, default=2, help="Minimum repeated evidence count required.")
@@ -3876,6 +3885,8 @@ def main(argv=None) -> int:
3876
3885
  return learn_cmd.doctor(target=args.target, json_output=args.json)
3877
3886
  if args.learn_command == "import-issues":
3878
3887
  return learn_cmd.import_issues(target=args.target, dry_run=args.dry_run, json_output=args.json)
3888
+ if args.learn_command == "import-learnings":
3889
+ return learn_cmd.import_learnings(target=args.target, files=args.file or None, dry_run=args.dry_run, json_output=args.json)
3879
3890
  if args.learn_command == "skill-candidates":
3880
3891
  return learn_cmd.skill_candidates(target=args.target, min_count=args.min_count, source=args.source, json_output=args.json)
3881
3892
  if args.learn_command == "propose-skill":
@@ -4058,6 +4069,8 @@ def main(argv=None) -> int:
4058
4069
  )
4059
4070
  if args.memory_care_command == "scan":
4060
4071
  return memory_cmd.scan(target=args.target, json_output=args.json)
4072
+ if args.memory_care_command == "backfill":
4073
+ return memory_cmd.backfill(target=args.target, apply=args.apply, json_output=args.json)
4061
4074
  if args.memory_care_command == "plan-fixes":
4062
4075
  return memory_cmd.plan_fixes(target=args.target, json_output=args.json)
4063
4076
  if args.memory_care_command == "status":
@@ -10,6 +10,7 @@ from typing import Any
10
10
  from uuid import uuid4
11
11
 
12
12
  from . import work_cmd
13
+ from .localio import read_json_dict as _read_json, stable_hash as _stable_hash, utc_now as _now, write_json as _write_json
13
14
 
14
15
  OK = "ok"
15
16
  WARN = "warn"
@@ -19,10 +20,6 @@ SYNC_CONFIG_REL_PATH = ".brigade/context/sync-targets.json"
19
20
  CONTEXT_PACK_STALE_HOURS = 72
20
21
 
21
22
 
22
- def _now() -> datetime:
23
- return datetime.now(timezone.utc)
24
-
25
-
26
23
  def _context_root(target: Path) -> Path:
27
24
  return target / ".brigade" / "context"
28
25
 
@@ -43,26 +40,6 @@ def _sync_plans_root(target: Path) -> Path:
43
40
  return _context_root(target) / "sync-plans"
44
41
 
45
42
 
46
- def _write_json(path: Path, payload: dict[str, Any]) -> None:
47
- path.parent.mkdir(parents=True, exist_ok=True)
48
- path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n")
49
-
50
-
51
- def _read_json(path: Path) -> dict[str, Any] | None:
52
- try:
53
- payload = json.loads(path.read_text())
54
- except (OSError, json.JSONDecodeError):
55
- return None
56
- return payload if isinstance(payload, dict) else None
57
-
58
-
59
- def _stable_hash(value: object) -> str:
60
- rendered = json.dumps(value, sort_keys=True, separators=(",", ":"), default=str)
61
- import hashlib
62
-
63
- return hashlib.sha256(rendered.encode("utf-8")).hexdigest()[:16]
64
-
65
-
66
43
  def _parse_iso_datetime(value: object) -> datetime | None:
67
44
  if not isinstance(value, str) or not value.strip():
68
45
  return None
@@ -19,6 +19,7 @@ from typing import Any
19
19
  from uuid import uuid4
20
20
 
21
21
  from . import center_cmd, context_cmd, handoff_cmd, memory_cmd, notifications_cmd, phases_cmd, security_cmd, toml_compat as tomllib, tools_cmd, work_cmd
22
+ from .localio import read_json_dict as _read_json, utc_now as _now, write_json as _write_json
22
23
 
23
24
  SCHEMA_VERSION = 1
24
25
  RUN_STATUSES = {"reviewed", "deferred", "blocked", "archived"}
@@ -51,10 +52,6 @@ class _DailyStatusSectionTimeout(Exception):
51
52
  self.label = label
52
53
 
53
54
 
54
- def _now() -> datetime:
55
- return datetime.now(timezone.utc)
56
-
57
-
58
55
  def _daily_root(target: Path) -> Path:
59
56
  return target / ".brigade" / "daily"
60
57
 
@@ -210,19 +207,6 @@ def _hardening_phases() -> list[dict[str, Any]]:
210
207
  return phases
211
208
 
212
209
 
213
- def _read_json(path: Path) -> dict[str, Any] | None:
214
- try:
215
- payload = json.loads(path.read_text())
216
- except (OSError, json.JSONDecodeError):
217
- return None
218
- return payload if isinstance(payload, dict) else None
219
-
220
-
221
- def _write_json(path: Path, payload: dict[str, Any]) -> None:
222
- path.parent.mkdir(parents=True, exist_ok=True)
223
- path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n")
224
-
225
-
226
210
  def _schema(name: str) -> dict[str, Any]:
227
211
  return {
228
212
  "name": name,