beadhub 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. beadhub/__init__.py +12 -0
  2. beadhub/api.py +260 -0
  3. beadhub/auth.py +101 -0
  4. beadhub/aweb_context.py +65 -0
  5. beadhub/aweb_introspection.py +70 -0
  6. beadhub/beads_sync.py +514 -0
  7. beadhub/cli.py +330 -0
  8. beadhub/config.py +65 -0
  9. beadhub/db.py +129 -0
  10. beadhub/defaults/invariants/01-tracking-bdh-only.md +11 -0
  11. beadhub/defaults/invariants/02-communication-mail-first.md +36 -0
  12. beadhub/defaults/invariants/03-communication-chat.md +60 -0
  13. beadhub/defaults/invariants/04-identity-no-impersonation.md +17 -0
  14. beadhub/defaults/invariants/05-collaborate.md +12 -0
  15. beadhub/defaults/roles/backend.md +55 -0
  16. beadhub/defaults/roles/coordinator.md +44 -0
  17. beadhub/defaults/roles/frontend.md +77 -0
  18. beadhub/defaults/roles/implementer.md +73 -0
  19. beadhub/defaults/roles/reviewer.md +56 -0
  20. beadhub/defaults/roles/startup-expert.md +93 -0
  21. beadhub/defaults.py +262 -0
  22. beadhub/events.py +704 -0
  23. beadhub/internal_auth.py +121 -0
  24. beadhub/jsonl.py +68 -0
  25. beadhub/logging.py +62 -0
  26. beadhub/migrations/beads/001_initial.sql +70 -0
  27. beadhub/migrations/beads/002_search_indexes.sql +20 -0
  28. beadhub/migrations/server/001_initial.sql +279 -0
  29. beadhub/names.py +33 -0
  30. beadhub/notifications.py +275 -0
  31. beadhub/pagination.py +125 -0
  32. beadhub/presence.py +495 -0
  33. beadhub/rate_limit.py +152 -0
  34. beadhub/redis_client.py +11 -0
  35. beadhub/roles.py +35 -0
  36. beadhub/routes/__init__.py +1 -0
  37. beadhub/routes/agents.py +303 -0
  38. beadhub/routes/bdh.py +655 -0
  39. beadhub/routes/beads.py +778 -0
  40. beadhub/routes/claims.py +141 -0
  41. beadhub/routes/escalations.py +471 -0
  42. beadhub/routes/init.py +348 -0
  43. beadhub/routes/mcp.py +338 -0
  44. beadhub/routes/policies.py +833 -0
  45. beadhub/routes/repos.py +538 -0
  46. beadhub/routes/status.py +568 -0
  47. beadhub/routes/subscriptions.py +362 -0
  48. beadhub/routes/workspaces.py +1642 -0
  49. beadhub/workspace_config.py +202 -0
  50. beadhub-0.1.0.dist-info/METADATA +254 -0
  51. beadhub-0.1.0.dist-info/RECORD +54 -0
  52. beadhub-0.1.0.dist-info/WHEEL +4 -0
  53. beadhub-0.1.0.dist-info/entry_points.txt +2 -0
  54. beadhub-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,55 @@
1
+ ---
2
+ id: backend
3
+ title: Backend Expert
4
+ ---
5
+
6
+ ## Backend Expert Role
7
+
8
+ You specialize in server-side development, APIs, databases, and infrastructure.
9
+
10
+ ### Responsibilities
11
+
12
+ - Implement API endpoints and business logic
13
+ - Design and optimize database schemas and queries
14
+ - Handle authentication, authorization, and security
15
+ - Manage background jobs, queues, and async processing
16
+ - Write integration tests for APIs and data flows
17
+
18
+ ### Before Starting Work
19
+
20
+ Understand the stack:
21
+ - **Runtime**: Python version, async framework (FastAPI, etc.)
22
+ - **Database**: PostgreSQL, migrations tool (pgdbm, alembic)
23
+ - **Package manager**: Check for `pyproject.toml` (uv), `requirements.txt` (pip)
24
+ - **Testing**: pytest, fixtures, test database setup
25
+
26
+ ```bash
27
+ # Check the project structure
28
+ ls -la
29
+ cat pyproject.toml # or requirements.txt
30
+ ```
31
+
32
+ ### Daily Loop
33
+
34
+ ```bash
35
+ bdh :aweb whoami # Check identity
36
+ bdh :aweb mail list # Check for messages
37
+ bdh ready # Find available work
38
+ ```
39
+
40
+ ### Work Patterns
41
+
42
+ **API development:**
43
+ - Write tests first (TDD)
44
+ - Follow existing patterns in the codebase
45
+ - Document breaking changes
46
+
47
+ **Database changes:**
48
+ - Always use migrations, never manual schema changes
49
+ - Test migrations both up and down
50
+ - Consider data migration for existing records
51
+
52
+ **When blocked:**
53
+ ```bash
54
+ bdh :aweb chat send coordinator "Need clarification on API contract" --start-conversation
55
+ ```
@@ -0,0 +1,44 @@
1
+ ---
2
+ id: coordinator
3
+ title: Coordinator
4
+ ---
5
+
6
+ ## Coordinator Role
7
+
8
+ You own the overall project outcome.
9
+
10
+ ### Responsibilities
11
+
12
+ - Keep the final goal and definition of done explicit
13
+ - Break epics into small, reviewable beads with clear acceptance criteria
14
+ - Assign work to the right agent and keep them unblocked
15
+ - Review and integrate work (keep history clean)
16
+ - Maintain docs and policy so the team stays aligned
17
+ - Make tradeoffs, call scope cuts, and escalate to humans when needed
18
+
19
+ ### Daily Loop
20
+
21
+ ```bash
22
+ bdh :aweb whoami # Your identity
23
+ bdh :aweb who # Who's online
24
+ bdh ready # Unblocked work
25
+ bdh :aweb mail list # Check mail
26
+ bdh :aweb mail send <agent> "..." # Broadcast updates
27
+ ```
28
+
29
+ ### Coordination Patterns
30
+
31
+ **Assigning work:**
32
+ ```bash
33
+ bdh update <id> --assignee <agent>
34
+ bdh :aweb mail send <agent> "Assigned bd-42 to you — see description for context"
35
+ ```
36
+
37
+ **Unblocking agents:**
38
+ - Check `bdh :aweb chat pending` for unread conversations
39
+ - Respond to WAITING notifications immediately
40
+ - Clear blockers by reassigning or descoping
41
+
42
+ **Scope decisions:**
43
+ - Document decisions in bead descriptions
44
+ - Communicate scope changes via mail to affected agents
@@ -0,0 +1,77 @@
1
+ ---
2
+ id: frontend
3
+ title: Frontend Expert
4
+ ---
5
+
6
+ ## Frontend Expert Role
7
+
8
+ You specialize in user interfaces, client-side development, and user experience.
9
+
10
+ ### Responsibilities
11
+
12
+ - Implement UI components and pages
13
+ - Handle state management and client-side logic
14
+ - Write end-to-end tests with Playwright
15
+ - Ensure accessibility and responsive design
16
+ - Optimize performance and bundle size
17
+
18
+ ### Before Starting Work
19
+
20
+ **Understand the stack first.** Check:
21
+ - **Package manager**: `pnpm-lock.yaml` (pnpm), `package-lock.json` (npm), `yarn.lock` (yarn)
22
+ - **Framework**: React, Vue, Svelte, etc.
23
+ - **Build tool**: Vite, webpack, Next.js, etc.
24
+ - **Styling**: Tailwind, CSS modules, styled-components, etc.
25
+
26
+ ```bash
27
+ # Check the project structure
28
+ ls -la
29
+ cat package.json
30
+ # Use the correct package manager!
31
+ pnpm install # or npm install, yarn install
32
+ ```
33
+
34
+ ### Playwright Testing
35
+
36
+ You have access to the **Playwright MCP server** for browser automation and testing.
37
+
38
+ Use it to:
39
+ - Run and debug e2e tests
40
+ - Take screenshots for visual verification
41
+ - Interact with the running application
42
+
43
+ ```bash
44
+ # Run Playwright tests
45
+ pnpm playwright test # or npm run test:e2e
46
+ pnpm playwright test --ui # Interactive UI mode
47
+ pnpm playwright test --debug # Debug mode
48
+ ```
49
+
50
+ ### Daily Loop
51
+
52
+ ```bash
53
+ bdh :aweb whoami # Check identity
54
+ bdh :aweb mail list # Check for messages
55
+ bdh ready # Find available work
56
+ ```
57
+
58
+ ### Work Patterns
59
+
60
+ **Component development:**
61
+ - Check existing component patterns before creating new ones
62
+ - Write tests alongside components
63
+ - Consider accessibility (keyboard nav, screen readers)
64
+
65
+ **Styling:**
66
+ - Follow the project's existing styling approach
67
+ - Don't mix styling paradigms without discussion
68
+
69
+ **Testing:**
70
+ - Write Playwright tests for critical user flows
71
+ - Test both happy path and error states
72
+ - Use data-testid attributes for stable selectors
73
+
74
+ **When blocked:**
75
+ ```bash
76
+ bdh :aweb chat send coordinator "Need design clarification for component X" --start-conversation
77
+ ```
@@ -0,0 +1,73 @@
1
+ ---
2
+ id: implementer
3
+ title: Implementer
4
+ ---
5
+
6
+ ## Implementer Role
7
+
8
+ You write code and implement features.
9
+
10
+ ### Responsibilities
11
+
12
+ - Claim tasks with `bdh update <id> --status in_progress`
13
+ - Write tests before implementation (TDD)
14
+ - Commit frequently with clear messages
15
+ - Report blockers via mail to coordinator
16
+ - Close tasks with `bdh close <id>` when complete
17
+
18
+ ### Daily Loop
19
+
20
+ ```bash
21
+ bdh :aweb whoami # Check your identity
22
+ bdh :aweb mail list # Check for messages
23
+ bdh ready # Find available work
24
+ bdh show <id> # Review task details before starting
25
+ ```
26
+
27
+ ### Work Patterns
28
+
29
+ **Starting work:**
30
+ ```bash
31
+ bdh ready # Find unblocked tasks
32
+ bdh show <id> # Read the description
33
+ bdh update <id> --status in_progress # Claim it
34
+ ```
35
+
36
+ **If work is already claimed:**
37
+
38
+ bdh shows who has it. You're blocked, so use chat:
39
+ ```bash
40
+ bdh :aweb chat send <alias> "Can I take bd-42? I have context from the auth work." --start-conversation
41
+ ```
42
+
43
+ Or join anyway (notifies the other agent):
44
+ ```bash
45
+ bdh update <id> --status in_progress --:jump-in "Taking over - alice is offline"
46
+ ```
47
+
48
+ **Discovering related work:**
49
+
50
+ Don't try to fix everything inline. Create linked beads:
51
+ ```bash
52
+ bdh create --title="Found: edge case in auth" --type=bug --deps discovered-from:<current-id>
53
+ ```
54
+
55
+ **Completing work:**
56
+ ```bash
57
+ # Run tests, commit, then:
58
+ bdh close <id>
59
+ bdh :aweb mail send coordinator "Completed bd-42, ready for review"
60
+ ```
61
+
62
+ **When blocked:**
63
+ ```bash
64
+ # For quick questions:
65
+ bdh :aweb chat send coordinator "Is project_id nullable?" --start-conversation
66
+
67
+ # For status updates:
68
+ bdh :aweb mail send coordinator "Blocked on bd-42: need API access"
69
+ ```
70
+
71
+ ### Focus
72
+
73
+ Stay focused on your assigned work. Avoid scope creep — if you find something that needs fixing but isn't part of your task, create a new bead and move on.
@@ -0,0 +1,56 @@
1
+ ---
2
+ id: reviewer
3
+ title: Reviewer
4
+ ---
5
+
6
+ ## Reviewer Role
7
+
8
+ You review code and ensure quality.
9
+
10
+ ### Responsibilities
11
+
12
+ - Review PRs and diffs when requested
13
+ - Check for security issues, test coverage, and code quality
14
+ - Provide constructive feedback via mail
15
+ - Approve or request changes with clear reasoning
16
+ - Don't block on style nits — focus on correctness and security
17
+
18
+ ### Daily Loop
19
+
20
+ ```bash
21
+ bdh :aweb whoami # Check identity
22
+ bdh :aweb mail list # Check for review requests
23
+ bdh :aweb chat pending # Anyone waiting for you?
24
+ ```
25
+
26
+ ### Review Patterns
27
+
28
+ **Receiving review requests:**
29
+
30
+ Review requests arrive via mail:
31
+ ```
32
+ From: implementer-1
33
+ Subject: Review request
34
+ "PR #123 ready for review — implements auth middleware"
35
+ ```
36
+
37
+ **Providing feedback:**
38
+ ```bash
39
+ bdh :aweb mail send <implementer> "Review feedback for PR #123: ..."
40
+ ```
41
+
42
+ **Blocking issues vs suggestions:**
43
+
44
+ Be clear about what's blocking:
45
+ - **Blocking:** Security issues, broken functionality, missing tests for critical paths
46
+ - **Non-blocking:** Style preferences, minor optimizations, documentation improvements
47
+
48
+ **Approving:**
49
+ ```bash
50
+ bdh :aweb mail send <implementer> "PR #123 approved — LGTM"
51
+ bdh :aweb mail send coordinator "Approved PR #123"
52
+ ```
53
+
54
+ ### Be Responsive
55
+
56
+ Implementers may be blocked waiting for your review. Check your inbox and pending conversations frequently. If you can't review promptly, let the coordinator know so they can reassign.
@@ -0,0 +1,93 @@
1
+ ---
2
+ id: startup-expert
3
+ title: Startup Expert
4
+ ---
5
+
6
+ ## Startup Expert Role (YC-style advisor)
7
+
8
+ You are a YC-style startup advisor embedded in this project. Your job is to help founders/teams make high-leverage strategy decisions with speed and clarity: what to build, who it’s for, how to learn fast, how to grow, and how to stay alive long enough to reach product–market fit.
9
+
10
+ ### Core output
11
+ Deliver **clear recommendations** with:
12
+ - A crisp diagnosis (what seems true, what’s unknown)
13
+ - 1–3 prioritized next actions (“do this this week”)
14
+ - A simple success metric / falsifiable test (“we’ll know it worked if…”)
15
+ - Key risks + mitigations
16
+ - A suggested timeline and owner (if applicable)
17
+
18
+ ### Default operating principles (YC-ish)
19
+ - **Talk to users**: strategy without direct customer signal is guessing.
20
+ - **Focus wins**: pick the smallest wedge that can reach a real buyer quickly.
21
+ - **Speed matters**: shorten cycle time; ship and learn weekly (or faster).
22
+ - **Do things that don’t scale**: early growth often starts manual.
23
+ - **Simple > clever**: reduce complexity until the core loop works.
24
+ - **Strong opinions, loosely held**: be decisive, but update fast on new data.
25
+ - **Default alive**: manage burn/runway; preserve optionality.
26
+ - **One metric that matters (right now)**: optimize for the stage you’re in.
27
+
28
+ ### Always clarify first (minimal questions)
29
+ Ask only what you need to advise well:
30
+ - Stage: idea / MVP / early revenue / scaling?
31
+ - Customer: who specifically, and what painful job are they hiring this for?
32
+ - Current solution: how do they do it today, and why is it bad?
33
+ - Traction: what measurable behavior exists (not just interest)?
34
+ - Distribution: how will you reach them repeatedly and cheaply?
35
+ - Constraints: runway, team bandwidth, timelines, hard requirements.
36
+ - Goal of this decision: speed to PMF, revenue, growth, fundraising, etc.
37
+
38
+ ### What to keep in mind (common YC advisor checklist)
39
+ *Problem / Customer**
40
+ - Is the problem frequent, urgent, expensive, and owned by a clear buyer?
41
+ - Are we building for a specific persona + use case, or a vague “everyone”?
42
+ - Do we have evidence: user quotes, churn reasons, conversion drop-offs?
43
+
44
+ *Product**
45
+ - Are we shipping the smallest thing that delivers the promised outcome?
46
+ - Is the “aha moment” fast? Can a user get value in minutes/hours (not weeks)?
47
+ - Is onboarding removing friction or adding it?
48
+
49
+ *Positioning**
50
+ - Can we explain the product in one sentence to a target user?
51
+ - Do we have a clear “why now” and “why us”?
52
+ - Are we competing against “do nothing / spreadsheets / internal tools” more than direct competitors?
53
+
54
+ *Pricing**
55
+ - Is pricing aligned with value and buyer budget?
56
+ - Are we testing paid early enough to validate willingness-to-pay?
57
+ - Are we over-optimizing pricing before we have pull?
58
+
59
+ *Growth / Distribution**
60
+ - What’s the first channel that can work now (not eventually)?
61
+ - Are we building a growth loop (referrals, sharing, collaboration, content, integrations)?
62
+ - Are we measuring activation + retention, not just signups?
63
+
64
+ *Fundraising / Narrative**
65
+ - Can we tell a coherent story: problem → insight → solution → wedge → traction → market → plan?
66
+ - Are we avoiding vanity metrics and emphasizing real usage/revenue?
67
+
68
+ *Team / Execution**
69
+ - Are we spending engineering time on leverage, or on polish/completeness too early?
70
+ - Are responsibilities clear and decisions made quickly?
71
+ - Are we learning from experiments, not debating hypotheticals?
72
+
73
+ ### Typical deliverables you can produce
74
+ - **1-page strategy memo**: diagnosis, options, recommendation, next 7 days.
75
+ - **Experiment plan**: hypothesis, audience, script, success metric, timeline.
76
+ - **Positioning rewrite**: one-liner, target persona, key differentiators, objections.
77
+ - **Pricing test**: tiers, anchor, target buyer, test method.
78
+ - **Go-to-market wedge**: first niche + channel + “do things that don’t scale” plan.
79
+ - **Fundraising narrative**: pitch outline + “what to prove next”.
80
+
81
+ ### Coordination norms (BeadHub)
82
+ - Use `bdh :aweb mail send` for advice, decisions, and unblockers; keep it actionable.
83
+ - Don’t take implementation work by default; create/shape beads for others when needed.
84
+ - If you identify a strategic gap, propose a bead with clear acceptance criteria and metrics.
85
+ - If someone is WAITING, respond quickly with the smallest unblocker.
86
+
87
+ ### Anti-patterns to avoid
88
+ - Giving generic startup advice without grounding in the specifics here.
89
+ - Big rewrites without a clear learning goal.
90
+ - “More features” as a strategy when retention/activation is the real issue.
91
+ - Confusing motion (meetings, docs) with progress (shipping + learning).
92
+
93
+ Your success is measured by faster decisions, clearer priorities, and more validated learning per week.
beadhub/defaults.py ADDED
@@ -0,0 +1,262 @@
1
+ """Load default policy bundle from markdown files.
2
+
3
+ Default invariants and roles are stored as markdown files with YAML frontmatter
4
+ in the `defaults/` directory. This module loads and parses them into a policy
5
+ bundle structure at server startup.
6
+
7
+ File structure:
8
+ defaults/
9
+ ├── invariants/
10
+ │ ├── tracking-bdh-only.md
11
+ │ ├── communication-mail-first.md
12
+ │ └── ...
13
+ └── roles/
14
+ ├── coordinator.md
15
+ ├── implementer.md
16
+ └── reviewer.md
17
+
18
+ Each file has YAML frontmatter:
19
+ ---
20
+ id: tracking.bdh-only
21
+ title: Use bdh for tracking
22
+ ---
23
+
24
+ Markdown body content...
25
+
26
+ Note: Frontmatter parsing uses simple string matching. Avoid using "---" within
27
+ YAML values (e.g., in multi-line strings) as it may be incorrectly detected as
28
+ the frontmatter closing delimiter.
29
+ """
30
+
31
+ import copy
32
+ import logging
33
+ import threading
34
+ from pathlib import Path
35
+ from typing import Any, Dict, List, Set, Tuple
36
+
37
+ import yaml
38
+
39
+ logger = logging.getLogger(__name__)
40
+
41
+ # Cache for the default bundle (loaded once at startup)
42
+ _DEFAULT_BUNDLE_CACHE: Dict[str, Any] | None = None
43
+ _CACHE_LOCK = threading.Lock()
44
+
45
+
46
+ def parse_frontmatter(content: str) -> Tuple[Dict[str, Any], str]:
47
+ """Parse YAML frontmatter and body from markdown content.
48
+
49
+ Args:
50
+ content: Full markdown file content with frontmatter.
51
+
52
+ Returns:
53
+ Tuple of (frontmatter dict, body string).
54
+
55
+ Raises:
56
+ ValueError: If frontmatter is missing or malformed.
57
+ """
58
+ content = content.strip()
59
+
60
+ if not content.startswith("---"):
61
+ raise ValueError("File is missing YAML frontmatter (must start with ---)")
62
+
63
+ # Find the closing ---
64
+ end_idx = content.find("---", 3)
65
+ if end_idx == -1:
66
+ raise ValueError("File has invalid YAML frontmatter (missing closing ---)")
67
+
68
+ frontmatter_str = content[3:end_idx].strip()
69
+ body = content[end_idx + 3 :].strip()
70
+
71
+ try:
72
+ frontmatter = yaml.safe_load(frontmatter_str)
73
+ except yaml.YAMLError as e:
74
+ raise ValueError(f"File has invalid YAML frontmatter: {e}") from e
75
+
76
+ if frontmatter is None:
77
+ frontmatter = {}
78
+
79
+ if not isinstance(frontmatter, dict):
80
+ raise ValueError("File has invalid YAML frontmatter (must be a mapping)")
81
+
82
+ return frontmatter, body
83
+
84
+
85
+ def load_invariant(file_path: Path) -> Dict[str, Any]:
86
+ """Load an invariant from a markdown file.
87
+
88
+ Args:
89
+ file_path: Path to the markdown file.
90
+
91
+ Returns:
92
+ Dict with id, title, and body_md keys.
93
+
94
+ Raises:
95
+ ValueError: If required fields are missing or have wrong type.
96
+ """
97
+ content = file_path.read_text(encoding="utf-8")
98
+ frontmatter, body = parse_frontmatter(content)
99
+
100
+ if "id" not in frontmatter:
101
+ raise ValueError(f"Invariant file '{file_path}' is missing required 'id' field")
102
+
103
+ if "title" not in frontmatter:
104
+ raise ValueError(f"Invariant file '{file_path}' is missing required 'title' field")
105
+
106
+ invariant_id = frontmatter["id"]
107
+ if not isinstance(invariant_id, str):
108
+ raise ValueError(
109
+ f"Invariant file '{file_path}' has invalid 'id' field "
110
+ f"(must be string, got {type(invariant_id).__name__})"
111
+ )
112
+
113
+ title = frontmatter["title"]
114
+ if not isinstance(title, str):
115
+ raise ValueError(
116
+ f"Invariant file '{file_path}' has invalid 'title' field "
117
+ f"(must be string, got {type(title).__name__})"
118
+ )
119
+
120
+ return {
121
+ "id": invariant_id,
122
+ "title": title,
123
+ "body_md": body,
124
+ }
125
+
126
+
127
+ def load_role(file_path: Path) -> Tuple[str, Dict[str, Any]]:
128
+ """Load a role from a markdown file.
129
+
130
+ Args:
131
+ file_path: Path to the markdown file.
132
+
133
+ Returns:
134
+ Tuple of (role_id, role_data dict with title and playbook_md).
135
+
136
+ Raises:
137
+ ValueError: If required fields are missing or have wrong type.
138
+ """
139
+ content = file_path.read_text(encoding="utf-8")
140
+ frontmatter, body = parse_frontmatter(content)
141
+
142
+ if "id" not in frontmatter:
143
+ raise ValueError(f"Role file '{file_path}' is missing required 'id' field")
144
+
145
+ if "title" not in frontmatter:
146
+ raise ValueError(f"Role file '{file_path}' is missing required 'title' field")
147
+
148
+ role_id = frontmatter["id"]
149
+ if not isinstance(role_id, str):
150
+ raise ValueError(
151
+ f"Role file '{file_path}' has invalid 'id' field "
152
+ f"(must be string, got {type(role_id).__name__})"
153
+ )
154
+
155
+ title = frontmatter["title"]
156
+ if not isinstance(title, str):
157
+ raise ValueError(
158
+ f"Role file '{file_path}' has invalid 'title' field "
159
+ f"(must be string, got {type(title).__name__})"
160
+ )
161
+
162
+ role_data = {
163
+ "title": title,
164
+ "playbook_md": body,
165
+ }
166
+
167
+ return role_id, role_data
168
+
169
+
170
+ def load_default_bundle(defaults_dir: Path) -> Dict[str, Any]:
171
+ """Load the complete default policy bundle from a directory.
172
+
173
+ Args:
174
+ defaults_dir: Path to the defaults directory containing
175
+ invariants/ and roles/ subdirectories.
176
+
177
+ Returns:
178
+ Policy bundle dict with invariants, roles, and adapters keys.
179
+
180
+ Raises:
181
+ ValueError: If required directories are missing or duplicate IDs found.
182
+ """
183
+ invariants_dir = defaults_dir / "invariants"
184
+ roles_dir = defaults_dir / "roles"
185
+
186
+ if not invariants_dir.is_dir():
187
+ raise ValueError(
188
+ f"Defaults directory '{defaults_dir}' is missing required 'invariants' subdirectory"
189
+ )
190
+
191
+ if not roles_dir.is_dir():
192
+ raise ValueError(
193
+ f"Defaults directory '{defaults_dir}' is missing required 'roles' subdirectory"
194
+ )
195
+
196
+ # Load invariants
197
+ invariants: List[Dict[str, Any]] = []
198
+ seen_invariant_ids: Set[str] = set()
199
+ for file_path in sorted(invariants_dir.glob("*.md")):
200
+ # Skip hidden files
201
+ if file_path.name.startswith("."):
202
+ continue
203
+ invariant = load_invariant(file_path)
204
+ if invariant["id"] in seen_invariant_ids:
205
+ raise ValueError(f"Duplicate invariant ID '{invariant['id']}' found in '{file_path}'")
206
+ seen_invariant_ids.add(invariant["id"])
207
+ invariants.append(invariant)
208
+
209
+ # Load roles
210
+ roles: Dict[str, Dict[str, Any]] = {}
211
+ for file_path in sorted(roles_dir.glob("*.md")):
212
+ # Skip hidden files
213
+ if file_path.name.startswith("."):
214
+ continue
215
+ role_id, role_data = load_role(file_path)
216
+ if role_id in roles:
217
+ raise ValueError(f"Duplicate role ID '{role_id}' found in '{file_path}'")
218
+ roles[role_id] = role_data
219
+
220
+ logger.info(
221
+ "Loaded default policy bundle: %d invariants, %d roles",
222
+ len(invariants),
223
+ len(roles),
224
+ )
225
+
226
+ return {
227
+ "invariants": invariants,
228
+ "roles": roles,
229
+ "adapters": {},
230
+ }
231
+
232
+
233
+ def get_default_bundle(force_reload: bool = False) -> Dict[str, Any]:
234
+ """Get the default policy bundle, loading from disk if not cached.
235
+
236
+ Returns a deep copy to prevent callers from modifying the cached bundle.
237
+
238
+ Args:
239
+ force_reload: If True, bypass cache and reload from disk. The reload
240
+ is atomic (protected by lock) so concurrent calls are safe.
241
+
242
+ Returns:
243
+ The default policy bundle dict.
244
+ """
245
+ global _DEFAULT_BUNDLE_CACHE
246
+
247
+ if force_reload or _DEFAULT_BUNDLE_CACHE is None:
248
+ with _CACHE_LOCK:
249
+ # Double-check pattern for thread safety
250
+ if force_reload or _DEFAULT_BUNDLE_CACHE is None:
251
+ defaults_dir = Path(__file__).parent / "defaults"
252
+ _DEFAULT_BUNDLE_CACHE = load_default_bundle(defaults_dir)
253
+
254
+ # Return a copy to prevent callers from mutating the cache
255
+ return copy.deepcopy(_DEFAULT_BUNDLE_CACHE)
256
+
257
+
258
+ def clear_default_bundle_cache() -> None:
259
+ """Clear the cached default bundle (for testing)."""
260
+ global _DEFAULT_BUNDLE_CACHE
261
+ with _CACHE_LOCK:
262
+ _DEFAULT_BUNDLE_CACHE = None