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.
- beadhub/__init__.py +12 -0
- beadhub/api.py +260 -0
- beadhub/auth.py +101 -0
- beadhub/aweb_context.py +65 -0
- beadhub/aweb_introspection.py +70 -0
- beadhub/beads_sync.py +514 -0
- beadhub/cli.py +330 -0
- beadhub/config.py +65 -0
- beadhub/db.py +129 -0
- beadhub/defaults/invariants/01-tracking-bdh-only.md +11 -0
- beadhub/defaults/invariants/02-communication-mail-first.md +36 -0
- beadhub/defaults/invariants/03-communication-chat.md +60 -0
- beadhub/defaults/invariants/04-identity-no-impersonation.md +17 -0
- beadhub/defaults/invariants/05-collaborate.md +12 -0
- beadhub/defaults/roles/backend.md +55 -0
- beadhub/defaults/roles/coordinator.md +44 -0
- beadhub/defaults/roles/frontend.md +77 -0
- beadhub/defaults/roles/implementer.md +73 -0
- beadhub/defaults/roles/reviewer.md +56 -0
- beadhub/defaults/roles/startup-expert.md +93 -0
- beadhub/defaults.py +262 -0
- beadhub/events.py +704 -0
- beadhub/internal_auth.py +121 -0
- beadhub/jsonl.py +68 -0
- beadhub/logging.py +62 -0
- beadhub/migrations/beads/001_initial.sql +70 -0
- beadhub/migrations/beads/002_search_indexes.sql +20 -0
- beadhub/migrations/server/001_initial.sql +279 -0
- beadhub/names.py +33 -0
- beadhub/notifications.py +275 -0
- beadhub/pagination.py +125 -0
- beadhub/presence.py +495 -0
- beadhub/rate_limit.py +152 -0
- beadhub/redis_client.py +11 -0
- beadhub/roles.py +35 -0
- beadhub/routes/__init__.py +1 -0
- beadhub/routes/agents.py +303 -0
- beadhub/routes/bdh.py +655 -0
- beadhub/routes/beads.py +778 -0
- beadhub/routes/claims.py +141 -0
- beadhub/routes/escalations.py +471 -0
- beadhub/routes/init.py +348 -0
- beadhub/routes/mcp.py +338 -0
- beadhub/routes/policies.py +833 -0
- beadhub/routes/repos.py +538 -0
- beadhub/routes/status.py +568 -0
- beadhub/routes/subscriptions.py +362 -0
- beadhub/routes/workspaces.py +1642 -0
- beadhub/workspace_config.py +202 -0
- beadhub-0.1.0.dist-info/METADATA +254 -0
- beadhub-0.1.0.dist-info/RECORD +54 -0
- beadhub-0.1.0.dist-info/WHEEL +4 -0
- beadhub-0.1.0.dist-info/entry_points.txt +2 -0
- 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
|