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.
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/PKG-INFO +4 -4
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/QUICKSTART.md +2 -2
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/README.md +1 -1
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/pyproject.toml +3 -3
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/__init__.py +1 -1
- brigade_cli-0.10.0/src/brigade/actionqueue.py +118 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/center_cmd.py +24 -146
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/chat_cmd.py +1 -4
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/cli.py +13 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/context_cmd.py +1 -24
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/daily_cmd.py +1 -17
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/doctor.py +13 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/handoff_cmd.py +1 -5
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/learn_cmd.py +202 -19
- brigade_cli-0.10.0/src/brigade/localio.py +77 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/managed.py +118 -2
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/memory_cmd.py +141 -4
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/operator_cmd.py +30 -5
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/pantry_cmd.py +1 -5
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/phases_cmd.py +1 -17
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/projects_cmd.py +1 -18
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/registry.py +16 -1
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/release_cmd.py +19 -81
- brigade_cli-0.10.0/src/brigade/reportstore.py +125 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/repos_cmd.py +42 -202
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/research_cmd.py +1 -5
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/runbook_cmd.py +1 -10
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/scrub.py +35 -6
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/security_cmd.py +1 -18
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/skills_cmd.py +2 -12
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/hermes/memory-handoff.harness.json +1 -1
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/hermes/model-lanes.harness.json +1 -1
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/hermes/workspace.harness.json +1 -1
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/memory/cards/content-safety.md +1 -1
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/memory/chat-memory-sweep.example.json +1 -1
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/memory/memory-care.example.json +1 -1
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/openclaw/README.md +1 -1
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/policies/personal.json +1 -1
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/policies/public-content.json +1 -1
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/policies/public-repo.json +1 -1
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/tools_cmd.py +1 -14
- brigade_cli-0.10.0/src/brigade/work_cmd/__init__.py +392 -0
- brigade_cli-0.10.0/src/brigade/work_cmd/config.py +796 -0
- brigade_cli-0.10.0/src/brigade/work_cmd/constants.py +506 -0
- brigade_cli-0.10.0/src/brigade/work_cmd/helpers.py +345 -0
- brigade_cli-0.10.0/src/brigade/work_cmd/ledger.py +1669 -0
- brigade_cli-0.10.0/src/brigade/work_cmd/services.py +5983 -0
- brigade_cli-0.10.0/src/brigade/work_cmd/session.py +2633 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade_cli.egg-info/PKG-INFO +4 -4
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade_cli.egg-info/SOURCES.txt +12 -2
- brigade_cli-0.10.0/tests/test_managed.py +368 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_memory_cmd.py +90 -3
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_operator_cmd.py +20 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_phase36_cmd.py +153 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_registry.py +19 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_scrub.py +57 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_work_cmd.py +138 -138
- brigade_cli-0.10.0/tests/test_work_cmd_facade.py +159 -0
- brigade_cli-0.9.2/src/brigade/work_cmd.py +0 -11795
- brigade_cli-0.9.2/tests/test_managed.py +0 -155
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/LICENSE +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/MANIFEST.in +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/setup.cfg +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/__main__.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/aboyeur.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/add.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/agents.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/budgets.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/budgets_cmd.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/config.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/dogfood_cmd.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/fragments.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/handoff.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/hermes_adapter.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/ingest.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/install.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/notifications_cmd.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/proc.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/prompt.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/py.typed +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/reconfigure.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/research/__init__.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/research/config.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/research/engine.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/research/extract.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/research/handoff.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/research/llm.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/research/registry.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/research/report.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/research/sources/__init__.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/research/sources/cli.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/research/sources/local.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/research/sources/web.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/research/types.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/roadmap_cmd.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/roster.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/roster_cmd.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/runs_cmd.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/selection.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/station.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/status.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/adal/memory-handoffs/TEMPLATE.md +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/aider/memory-handoffs/TEMPLATE.md +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/antigravity/memory-handoffs/TEMPLATE.md +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/claude/memory-handoffs/TEMPLATE.md +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/codex/memory-handoffs/TEMPLATE.md +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/continue/memory-handoffs/TEMPLATE.md +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/copilot/memory-handoffs/TEMPLATE.md +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/cursor/memory-handoffs/TEMPLATE.md +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/depth/repo.json +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/depth/workspace.json +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/generic/harness-adapter-checklist.md +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/generic/memory-contract.md +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/goose/memory-handoffs/TEMPLATE.md +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/handoff/handoff-sources.example.json +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/handoff/openclaw-ingest-receipt.example.json +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/harnesses/adal.json +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/harnesses/aider.json +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/harnesses/antigravity.json +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/harnesses/claude.json +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/harnesses/codex.json +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/harnesses/continue.json +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/harnesses/copilot.json +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/harnesses/cursor.json +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/harnesses/goose.json +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/harnesses/hermes.json +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/harnesses/kimi.json +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/harnesses/openclaw.json +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/harnesses/opencode.json +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/harnesses/openhands.json +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/harnesses/pi.json +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/harnesses/qwen.json +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/hermes/README.md +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/hermes/memory-handoffs/TEMPLATE.md +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/hooks/pre-push +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/includes/publisher.json +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/kimi/memory-handoffs/TEMPLATE.md +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/memory/cards/backup-restic.md +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/memory/cards/chat-surface-crawlers.md +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/memory/cards/handoff-flow.md +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/memory/cards/memory-architecture.md +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/memory/cards/memory-care-staleness.md +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/memory/cards/memory-scanner.md +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/memory/cards/multi-workspace-handoff-admin.md +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/memory/cards/obsidian-notes.md +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/memory/cards/pipeline-standups.md +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/memory/cards/tokenjuice-output-compaction.md +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/openclaw/acp-escalation.openclaw.json +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/openclaw/memory-sweep-cron.openclaw.json +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/openclaw/model-aliases.openclaw.json +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/openclaw/ollama-memory-search.openclaw.json +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/opencode/memory-handoffs/TEMPLATE.md +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/openhands/memory-handoffs/TEMPLATE.md +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/pi/memory-handoffs/TEMPLATE.md +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/qwen/memory-handoffs/TEMPLATE.md +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/scripts/backup-restic.sh +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/skills/note/SKILL.md +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/workspace/AGENTS.md +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/workspace/CLAUDE.md +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/workspace/HEARTBEAT.md +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/workspace/IDENTITY.md +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/workspace/INSTALL_FOR_AGENTS.md +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/workspace/MEMORY.md +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/workspace/SAFETY_RULES.md +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/workspace/SOUL.md +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/workspace/TOOLS.md +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/workspace/USER.md +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/workspace/rules/acceptance-driven-work.md +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates/workspace/rules/issue-tdd-loop.md +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/templates.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/toml_compat.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/untrusted.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade/untrusted_cmd.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade_cli.egg-info/dependency_links.txt +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade_cli.egg-info/entry_points.txt +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade_cli.egg-info/requires.txt +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/src/brigade_cli.egg-info/top_level.txt +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_aboyeur.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_add.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_agents.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_budgets.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_cli_alias.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_cli_help.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_config.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_doctor.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_dogfood_cmd.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_fragments.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_gitignore.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_handoff.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_handoff_cmd.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_ingest.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_init.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_install.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_neutrality.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_notifications_cmd.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_pantry_cmd.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_phase100_cmd.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_phase101_cmd.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_phase165_cmd.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_phase37_cmd.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_phase38_cmd.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_phase39_cmd.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_phase40_cmd.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_phase41_cmd.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_phase42_cmd.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_phase43_cmd.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_phase44_cmd.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_phase45_cmd.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_phase46_50_cmd.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_phase51_55_cmd.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_phase56_60_cmd.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_phase96_cmd.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_phase97_cmd.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_phase98_cmd.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_phase99_cmd.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_privacy_regression.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_proc.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_prompt.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_reconfigure.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_release_cmd.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_repos_cmd.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_research_cli_sources.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_research_cmd.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_research_config.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_research_engine.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_research_extract.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_research_handoff.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_research_llm.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_research_local_sources.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_research_registry.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_research_report.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_research_types.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_research_web.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_roadmap_cmd.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_roster.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_roster_cmd.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_run_cli.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_runbook_cmd.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_runs_cmd.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_security_cmd.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_selection.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_skills_cmd.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_station.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_status.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_toml_compat.py +0 -0
- {brigade_cli-0.9.2 → brigade_cli-0.10.0}/tests/test_untrusted.py +0 -0
- {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.
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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.
|
|
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/
|
|
31
|
+
Cookbook = "https://github.com/escoffier-labs/solos-cookbook"
|
|
32
32
|
OpenClaw = "https://github.com/solomonneas/openclaw"
|
|
33
|
-
ContentGuard = "https://github.com/
|
|
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
|
|
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
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)
|
|
1941
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
2339
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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,
|